diff --git a/.changes/1.13.0.md b/.changes/1.13.0.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.changes/README.md b/.changes/README.md new file mode 100644 index 0000000000..5e458fd3aa --- /dev/null +++ b/.changes/README.md @@ -0,0 +1,5 @@ +# Changelog + +This directory contains changelog entries for each release of Terraform. +The only important folder for changes is the `vX.XX` folder corresponding with the Terraform version released from this branch. +All other folders are just there to make backports easier. You can remove folders with releases we won't allow backports for. \ No newline at end of file diff --git a/.changes/footer-with-experiments.md b/.changes/footer-with-experiments.md new file mode 100644 index 0000000000..6aba0a091e --- /dev/null +++ b/.changes/footer-with-experiments.md @@ -0,0 +1,10 @@ +EXPERIMENTS: + +Experiments are only enabled in alpha releases of Terraform CLI. The following features are not yet available in stable releases. + +- The new command `terraform rpcapi` exposes some Terraform Core functionality through an RPC interface compatible with [`go-plugin`](https://github.com/hashicorp/go-plugin). The exact RPC API exposed here is currently subject to change at any time, because it's here primarily as a vehicle to support the [Terraform Stacks](https://www.hashicorp.com/blog/terraform-stacks-explained) private preview and so will be broken if necessary to respond to feedback from private preview participants, or possibly for other reasons. Do not use this mechanism yet outside of Terraform Stacks private preview. +- The experimental "deferred actions" feature, enabled by passing the `-allow-deferral` option to `terraform plan`, permits `count` and `for_each` arguments in `module`, `resource`, and `data` blocks to have unknown values and allows providers to react more flexibly to unknown values. This experiment is under active development, and so it's not yet useful to participate in this experiment + +## Previous Releases + +For information on prior major and minor releases, refer to their changelogs: diff --git a/.changes/footer.md b/.changes/footer.md new file mode 100644 index 0000000000..259cdc4e33 --- /dev/null +++ b/.changes/footer.md @@ -0,0 +1,3 @@ +## Previous Releases + +For information on prior major and minor releases, refer to their changelogs: diff --git a/.changes/previous-releases.md b/.changes/previous-releases.md new file mode 100644 index 0000000000..e0843cb107 --- /dev/null +++ b/.changes/previous-releases.md @@ -0,0 +1,18 @@ +- [v1.12](https://github.com/hashicorp/terraform/blob/v1.12/CHANGELOG.md) +- [v1.11](https://github.com/hashicorp/terraform/blob/v1.11/CHANGELOG.md) +- [v1.10](https://github.com/hashicorp/terraform/blob/v1.10/CHANGELOG.md) +- [v1.9](https://github.com/hashicorp/terraform/blob/v1.9/CHANGELOG.md) +- [v1.8](https://github.com/hashicorp/terraform/blob/v1.8/CHANGELOG.md) +- [v1.7](https://github.com/hashicorp/terraform/blob/v1.7/CHANGELOG.md) +- [v1.6](https://github.com/hashicorp/terraform/blob/v1.6/CHANGELOG.md) +- [v1.5](https://github.com/hashicorp/terraform/blob/v1.5/CHANGELOG.md) +- [v1.4](https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md) +- [v1.3](https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md) +- [v1.2](https://github.com/hashicorp/terraform/blob/v1.2/CHANGELOG.md) +- [v1.1](https://github.com/hashicorp/terraform/blob/v1.1/CHANGELOG.md) +- [v1.0](https://github.com/hashicorp/terraform/blob/v1.0/CHANGELOG.md) +- [v0.15](https://github.com/hashicorp/terraform/blob/v0.15/CHANGELOG.md) +- [v0.14](https://github.com/hashicorp/terraform/blob/v0.14/CHANGELOG.md) +- [v0.13](https://github.com/hashicorp/terraform/blob/v0.13/CHANGELOG.md) +- [v0.12](https://github.com/hashicorp/terraform/blob/v0.12/CHANGELOG.md) +- [v0.11 and earlier](https://github.com/hashicorp/terraform/blob/v0.11/CHANGELOG.md) diff --git a/.changes/v1.11/.gitkeep b/.changes/v1.11/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.changes/v1.11/BUG FIXES-20250402-143931.yaml b/.changes/v1.11/BUG FIXES-20250402-143931.yaml new file mode 100644 index 0000000000..4971dd6277 --- /dev/null +++ b/.changes/v1.11/BUG FIXES-20250402-143931.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'write-only attributes: internal providers should set write-only attributes to null' +time: 2025-04-02T14:39:31.672249+02:00 +custom: + Issue: "36824" diff --git a/.changes/v1.12/.gitkeep b/.changes/v1.12/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.changes/v1.12/ENHANCEMENTS-20250303-171838.yaml b/.changes/v1.12/ENHANCEMENTS-20250303-171838.yaml new file mode 100644 index 0000000000..2669acf1d5 --- /dev/null +++ b/.changes/v1.12/ENHANCEMENTS-20250303-171838.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'backend/oss: Supports more standard environment variables to keep same with provider setting' +time: 2025-03-03T17:18:38.679213+08:00 +custom: + Issue: "36581" diff --git a/.changes/v1.12/ENHANCEMENTS-20250417-182036.yaml b/.changes/v1.12/ENHANCEMENTS-20250417-182036.yaml new file mode 100644 index 0000000000..5237a73160 --- /dev/null +++ b/.changes/v1.12/ENHANCEMENTS-20250417-182036.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: '`import` blocks: Now support importing a resource via a new identity attribute. This is mutually exclusive with the `id` attribute' +time: 2025-04-17T18:20:36.814657+02:00 +custom: + Issue: "36703" diff --git a/.changes/v1.12/NEW FEATURES-20250410-154805.yaml b/.changes/v1.12/NEW FEATURES-20250410-154805.yaml new file mode 100644 index 0000000000..f4fa50a042 --- /dev/null +++ b/.changes/v1.12/NEW FEATURES-20250410-154805.yaml @@ -0,0 +1,5 @@ +kind: NEW FEATURES +body: Added Terraform backend implementation for OCI Object Storage +time: 2025-04-10T15:48:05.919664+05:30 +custom: + Issue: "34465" diff --git a/.changes/v1.13/.gitkeep b/.changes/v1.13/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/.changes/v1.13/ENHANCEMENTS-20250508-130135.yaml b/.changes/v1.13/ENHANCEMENTS-20250508-130135.yaml new file mode 100644 index 0000000000..60eb0e653c --- /dev/null +++ b/.changes/v1.13/ENHANCEMENTS-20250508-130135.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: Filesystem functions are now checked for consistent results to catch invalid data during apply +time: 2025-05-08T13:01:35.450576-04:00 +custom: + Issue: "37001" diff --git a/.changie.yaml b/.changie.yaml new file mode 100644 index 0000000000..ea6357b52b --- /dev/null +++ b/.changie.yaml @@ -0,0 +1,32 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +changesDir: .changes +unreleasedDir: v1.13 +versionFooterPath: version_footer.tpl.md +changelogPath: CHANGELOG.md +versionExt: md +versionFormat: '## {{.Version}} ({{.Time.Format "January 2, 2006"}})' +kindFormat: "{{.Kind}}:" +changeFormat: "* {{.Body}} {{- if .Custom.Issue }} ([#{{.Custom.Issue}}](https://github.com/hashicorp/terraform/issues/{{.Custom.Issue}})){{- end}}" +custom: + - key: Issue + label: Issue/PR Number + type: int + minInt: 1 +kinds: + - label: NEW FEATURES + - label: ENHANCEMENTS + - label: BUG FIXES + - label: NOTES + - label: UPGRADE NOTES + - label: BREAKING CHANGES +newlines: + afterChangelogHeader: 0 + beforeKind: 1 + afterKind: 1 + afterChange: 1 + afterVersion: 1 + beforeChangelogVersion: 0 + endOfVersion: 2 +envPrefix: CHANGIE_ diff --git a/.copywrite.hcl b/.copywrite.hcl new file mode 100644 index 0000000000..ad3027b008 --- /dev/null +++ b/.copywrite.hcl @@ -0,0 +1,21 @@ +schema_version = 1 + +project { + license = "BUSL-1.1" + copyright_year = 2024 + + # (OPTIONAL) A list of globs that should not have copyright/license headers. + # Supports doublestar glob patterns for more flexibility in defining which + # files or folders should be ignored + header_ignore = [ + "**/*.tf", + "**/testdata/**", + "**/*.pb.go", + "**/*_string.go", + "**/mock*.go", + ".changes/**", + # these directories have their own copywrite config + "docs/plugin-protocol/**", + "internal/tfplugin*/**" + ] +} diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6114a69135..1026407579 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,26 +1,21 @@ # Contributing to Terraform -This repository contains only Terraform core, which includes the command line interface and the main graph engine. Providers are implemented as plugins that each have their own repository linked from the [Terraform Registry index](https://registry.terraform.io/browse/providers). Instructions for developing each provider are usually in the associated README file. For more information, see [the provider development overview](https://www.terraform.io/docs/plugins/provider.html). - ---- - -**Note:** Due to current low staffing on the Terraform Core team at HashiCorp, **we are not routinely reviewing and merging community-submitted pull requests**. We do hope to begin processing them again soon once we're back up to full staffing again, but for the moment we need to ask for patience. Thanks! - -**Additional note:** The intent of the prior comment was to provide clarity for the community around what to expect for a small part of the work related to Terraform. This does not affect other PR reviews, such as those for Terraform providers. We expect that the relevant team will be appropriately staffed within the coming weeks, which should allow us to get back to normal community PR review practices. For the broader context and information on HashiCorp’s continued commitment to and investment in Terraform, see [this blog post](https://www.hashicorp.com/blog/terraform-community-contributions). - ---- - **All communication on GitHub, the community forum, and other HashiCorp-provided communication channels is subject to [the HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines).** -This document provides guidance on Terraform contribution recommended practices. It covers what we're looking for in order to help set some expectations and help you get the most out of participation in this project. +This repository contains Terraform core, which includes the command line interface and the main graph engine. -To record a bug report, enhancement proposal, or give any other product feedback, please [open a GitHub issue](https://github.com/hashicorp/terraform/issues/new/choose) using the most appropriate issue template. Please do fill in all of the information the issue templates request, because we've seen from experience that this will maximize the chance that we'll be able to act on your feedback. +Providers are implemented as plugins that each have their own repository linked from the [Terraform Registry index](https://registry.terraform.io/browse/providers). Instructions for developing each provider are usually in the associated README file. For more information, see [the provider development overview](https://developer.hashicorp.com/terraform/plugin). + +This document provides guidance on Terraform contribution recommended practices. It covers what we're looking for in order to help set expectations and help you get the most out of participation in this project. + +To report a bug, an enhancement proposal, or give any other product feedback, please [open a GitHub issue](https://github.com/hashicorp/terraform/issues/new/choose) using the most appropriate issue template. Please fill in all of the information the issue templates request. This will maximize our ability to act on your feedback. --- -- [Contributing Fixes](#contributing-fixes) +- [Introduction](#Introduction) +- [Contributing a Pull Request](#contributing-a-pull-request) - [Proposing a Change](#proposing-a-change) - [Caveats & areas of special concern](#caveats--areas-of-special-concern) - [State Storage Backends](#state-storage-backends) @@ -28,6 +23,9 @@ To record a bug report, enhancement proposal, or give any other product feedback - [Maintainers](#maintainers) - [Pull Request Lifecycle](#pull-request-lifecycle) - [Getting Your Pull Requests Merged Faster](#getting-your-pull-requests-merged-faster) + - [Changelog entries](#changelog-entries) + - [Create a change file using `changie`](#create-a-change-file-using-changie) + - [Backport a PR to a past release](#backport-a-pr-to-a-past-release) - [PR Checks](#pr-checks) - [Terraform CLI/Core Development Environment](#terraform-clicore-development-environment) - [Acceptance Tests: Testing interactions with external services](#acceptance-tests-testing-interactions-with-external-services) @@ -36,15 +34,31 @@ To record a bug report, enhancement proposal, or give any other product feedback -## Contributing Fixes +## Introduction -It can be tempting to want to dive into an open source project and help _build the thing_ you believe you're missing. It's a wonderful and helpful intention. However, Terraform is a complex tool. Many seemingly simple changes can have serious effects on other areas of the code and it can take some time to become familiar with the effects of even basic changes. The Terraform team is not immune to unintended and sometimes undesirable changes. We do take our work seriously, and appreciate the globally diverse community that relies on Terraform for workflows of all sizes and criticality. +One of the great things about publicly available source code is that you can dive into the project and help _build the thing_ you believe is missing. It's a wonderful and generous instinct. However, Terraform is a complex tool. Even simple changes can have a serious impact on other areas of the code and it can take some time to become familiar with the effects of even basic changes. The Terraform team is not immune to unintended and sometimes undesirable consequences. We take our work seriously, and appreciate the responsibility of maintaining software for a globally diverse community that relies on Terraform for workflows of all sizes and criticality. -As a result of Terraform's complexity and high bar for stability, the most straightforward way to start helping with the Terraform project is to pick an existing bug and [get to work](#terraform-clicore-development-environment). +As a result of Terraform's complexity and high bar for stability, the most straightforward way to help with the Terraform project is to [file a feature request or bug report](https://github.com/hashicorp/terraform/issues/new/choose), following the template to fully express your desired use case. -For new contributors we've labeled a few issues with `Good First Issue` as a nod to issues which will help get you familiar with Terraform development, while also providing an onramp to the codebase itself. +If you believe you can also implement the solution for your bug or feature, we request that you first discuss the proposed solution with the core maintainer team. This discussion happens in GitHub, on the issue you created to describe the bug or feature. This discussion gives the core team a chance to explore any missing best practices or unintended consequences of the proposed change. Participating in this discussion and getting the go-ahead from a core maintainer is the only way to ensure your code is reviewed for inclusion with the project. It is also possible that the proposed solution is not workable, and will save you time writing code that will not be used due to unforeseen unintended consequences. Please read the section [Proposing a Change](#proposing-a-change) for the full details on this process. + +(As a side note, this is how we work internally at HashiCorp as well. Changes are proposed internally via an RFC process, in which all impacted teams are able to review the proposed changes and give feedback before any code is written. Written communication of changes via the RFC process is a core pillar of our internal coordination.) + + +## Contributing a Pull Request + +If you are a new contributor to Terraform, or looking to get started committing to the Terraform ecosystem, here are a couple of tips to get started. + +First, the easiest way to get started is to make fixes or improvements to the documentation. This can be done completely within GitHub, no need to even clone the project! + +Beyond documentation improvements, it is easiest to contribute to Terraform on the edges. If you are looking for a good starting place to contribute, finding and resolving issues in the providers is the best first step. These projects have huge breadth of coverage and are always looking for contributors to fix issues that might not otherwise get the attention of a maintainer. + +Closer to home, within the Terraform core repository, working in areas like functions or backends tend to have less harmful unintended interactions with the core of Terraform (but, also, are not currently a high priority to be reviewed, so please discuss any changes with the team before you start.) It gets more difficult to contribute as you get closer to the core functionality (e.g., manipulating the graph and core language features). For these types of changes, please start with the [Proposing a Change](#proposing-a-change) section to understand how we think about managing this process. + +Once you are ready to write code, please see the section [Terraform CLI/Core Development Environment](#terraform-clicore-development-environment) to create your dev environment. Please read the documentation, and don't be afraid to ask questions in our [community forum](https://discuss.hashicorp.com/c/terraform-core/27). + +You may see the `Good First Issue` label on issues in the Terraform repository on GitHub. We use this label to maintain a list of issues for new internal core team members to ramp up the codebase. That said, if you are feeling particularly ambitious, you can follow our process to propose a solution. Other HashiCorp repositories (for example, https://github.com/hashicorp/terraform-provider-aws/) do use the `Good First Issue` to indicate good issues for external contributors to get started. -Read the documentation, and don't be afraid to [ask questions](https://discuss.hashicorp.com/c/terraform-core/27). ## Proposing a Change @@ -52,13 +66,14 @@ In order to be respectful of the time of community contributors, we aim to discu If the bug you wish to fix or enhancement you wish to implement isn't already covered by a GitHub issue that contains feedback from the Terraform team, please do start a discussion (either in [a new GitHub issue](https://github.com/hashicorp/terraform/issues/new/choose) or an existing one, as appropriate) before you invest significant development time. If you mention your intent to implement the change described in your issue, the Terraform team can, as best as possible, prioritize including implementation-related feedback in the subsequent discussion. -At this time, we do not have a formal process for reviewing outside proposals that significantly change Terraform's workflow, its primary usage patterns, and its language. Additionally, some seemingly simple proposals can have deep effects across Terraform, which is why we strongly suggest starting with an issue-based proposal. +At this time, we do not have a formal process for reviewing outside proposals that significantly change Terraform's workflow, its primary usage patterns, and its language. Additionally, some seemingly simple proposals can have deep effects across Terraform, which is why we strongly suggest starting with an issue-based proposal. Also, we do not normally accept minor changes in comments or help text. For large proposals that could entail a significant design phase, we wish to be up front with potential contributors that, unfortunately, we are unlikely to be able to give prompt feedback. We are still interested to hear about your use-cases so that we can consider ways to meet them as part of other larger projects. Most changes will involve updates to the test suite, and changes to Terraform's documentation. The Terraform team can advise on different testing strategies for specific scenarios, and may ask you to revise the specific phrasing of your proposed documentation prose to match better with the standard "voice" of Terraform's documentation. -This repository is primarily maintained by a small team at HashiCorp along with their other responsibilities, so unfortunately we cannot always respond promptly to pull requests, particularly if they do not relate to an existing GitHub issue where the Terraform team has already participated and indicated willingness to work on the issue or accept PRs for the proposal. We *are* grateful for all contributions however, and will give feedback on pull requests as soon as we're able. +We cannot always respond promptly to pull requests, particularly if they do not relate to an existing GitHub issue where the Terraform team has already participated and indicated willingness to work on the issue or accept PRs for the proposal. We *are* grateful for all contributions however, and will give feedback on pull requests as soon as we are able. + ### Caveats & areas of special concern @@ -66,13 +81,18 @@ There are some areas of Terraform which are of special concern to the Terraform #### State Storage Backends -The Terraform team is not merging PRs for new state storage backends at the current time. Our priority regarding state storage backends is to find maintainers for existing backends and remove those backends without maintainers. +The Terraform team is not merging PRs for new state storage backends. Our priority regarding state storage backends is to find maintainers for existing backends and remove those backends without maintainers. -Please see the [CODEOWNERS](https://github.com/hashicorp/terraform/blob/main/CODEOWNERS) file for the status of a given backend. Community members with an interest in a particular standard backend are welcome to help maintain it. +Please see the [CODEOWNERS](https://github.com/hashicorp/terraform/blob/main/CODEOWNERS) file for the status of a given backend. Community members with an interest in a particular backend are welcome to offer to maintain it. -Currently, merging state storage backends places a significant burden on the Terraform team. The team must set up an environment and cloud service provider account, or a new database/storage/key-value service, in order to build and test remote state storage backends. The time and complexity of doing so prevents us from moving Terraform forward in other ways. +In terms of setting expectations, there are three categories of backends in the Terraform repository: backends maintained by the core team (ex.: http); backends maintained by one of HashiCorp's provider teams (e.g. AWS S3, Azure, etc); and backends maintained by third party maintainers (ex.: Postgres, COS). + +* Backends maintained by the core team are unlikely to see accepted contributions. We are triaging incoming pull requests, but these are not highly prioritized against our other work. The smaller and more-contained the change, the more likely it will be reviewed (please see also [Proposing a Change](#proposing-a-change)). + +* Backends maintained by one of HashiCorp's provider teams review contributions irregularly. There is no official commitment, typically once every few months one of the maintainers will review a number of backend PRs relating to their provider. The S3 and Azure backends tend to see the most on-going development. + +* Backends maintained by third-party maintainers are reviewed at the whim and availability of those maintainers. When the maintainer gives a positive code review to the pull request, the core team will do a review and merge the changes. -We are working to remove ourselves from the critical path of state storage backends by moving them towards a plugin model. In the meantime, we won't be accepting new remote state backends into Terraform. #### Provisioners @@ -80,17 +100,18 @@ Provisioners are an area of concern in Terraform for a number of reasons. Chiefl There are two main types of provisioners in Terraform, the generic provisioners (`file`,`local-exec`, and `remote-exec`) and the tool-specific provisioners (`chef`, `habbitat`, `puppet` & `salt-masterless`). **The tool-specific provisioners [are deprecated](https://discuss.hashicorp.com/t/notice-terraform-to-begin-deprecation-of-vendor-tool-specific-provisioners-starting-in-terraform-0-13-4/13997).** In practice this means we will not be accepting PRs for these areas of the codebase. -From our [documentation](https://www.terraform.io/docs/provisioners/index.html): +From our [documentation](https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax): > ... they [...] add a considerable amount of complexity and uncertainty to Terraform usage.[...] we still recommend attempting to solve it [your problem] using other techniques first, and use provisioners only if there is no other option. The Terraform team is in the process of building a way forward which continues to decrease reliance on provisioners. In the mean time however, as our documentation indicates, they are a tool of last resort. As such expect that PRs and issues for provisioners are not high in priority. -Please see the [CODEOWNERS](https://github.com/hashicorp/terraform/blob/main/CODEOWNERS) file for the status of a given provisioner. Community members with an interest in a particular provisioner are welcome to help maintain it. +Please see the [CODEOWNERS](https://github.com/hashicorp/terraform/blob/main/CODEOWNERS) file for the status of a given provisioner. + #### Maintainers -Maintainers are key contributors to our Open Source project. They contribute their time and expertise and we ask that the community take extra special care to be mindful of this when interacting with them. +Maintainers are key contributors to our community project. They contribute their time and expertise and we ask that the community take extra special care to be mindful of this when interacting with them. For code that has a listed maintainer or maintainers in our [CODEOWNERS](https://github.com/hashicorp/terraform/blob/main/CODEOWNERS) file, the Terraform team will highlight them for participation in PRs which relate to the area of code they maintain. The expectation is that a maintainer will review the code and work with the PR contributor before the code is merged by the Terraform team. @@ -98,14 +119,16 @@ There is no expectation on response time for our maintainers; they may be indisp If an an unmaintained area of code interests you and you'd like to become a maintainer, you may simply make a PR against our [CODEOWNERS](https://github.com/hashicorp/terraform/blob/main/CODEOWNERS) file with your github handle attached to the approriate area. If there is a maintainer or team of maintainers for that area, please coordinate with them as necessary. + ### Pull Request Lifecycle 1. You are welcome to submit a [draft pull request](https://github.blog/2019-02-14-introducing-draft-pull-requests/) for commentary or review before it is fully completed. It's also a good idea to include specific questions or items you'd like feedback on. 2. Once you believe your pull request is ready to be merged you can create your pull request. -3. When time permits Terraform's core team members will look over your contribution and either merge, or provide comments letting you know if there is anything left to do. It may take some time for us to respond. We may also have questions that we need answered about the code, either because something doesn't make sense to us or because we want to understand your thought process. We kindly ask that you do not target specific team members. -4. If we have requested changes, you can either make those changes or, if you disagree with the suggested changes, we can have a conversation about our reasoning and agree on a path forward. This may be a multi-step process. Our view is that pull requests are a chance to collaborate, and we welcome conversations about how to do things better. It is the contributor's responsibility to address any changes requested. While reviewers are happy to give guidance, it is unsustainable for us to perform the coding work necessary to get a PR into a mergeable state. -5. Once all outstanding comments and checklist items have been addressed, your contribution will be merged! Merged PRs may or may not be included in the next release based on changes the Terraform teams deems as breaking or not. The core team takes care of updating the [CHANGELOG.md](https://github.com/hashicorp/terraform/blob/main/CHANGELOG.md) as they merge. -6. In some cases, we might decide that a PR should be closed without merging. We'll make sure to provide clear reasoning when this happens. Following the recommended process above is one of the ways to ensure you don't spend time on a PR we can't or won't merge. +3. If your change is user-facing, add a short description in a [changelog entry](#changelog-entries). +4. When time permits Terraform's core team members will look over your contribution and either merge, or provide comments letting you know if there is anything left to do. It may take some time for us to respond. We may also have questions that we need answered about the code, either because something doesn't make sense to us or because we want to understand your thought process. We kindly ask that you do not target specific team members. +5. If we have requested changes, you can either make those changes or, if you disagree with the suggested changes, we can have a conversation about our reasoning and agree on a path forward. This may be a multi-step process. Our view is that pull requests are a chance to collaborate, and we welcome conversations about how to do things better. It is the contributor's responsibility to address any changes requested. While reviewers are happy to give guidance, it is unsustainable for us to perform the coding work necessary to get a PR into a mergeable state. +6. Once all outstanding comments and checklist items have been addressed, your contribution will be merged! Merged PRs may or may not be included in the next release based on changes the Terraform teams deems as breaking or not. The core team takes care of updating the [CHANGELOG.md](https://github.com/hashicorp/terraform/blob/main/CHANGELOG.md) as they merge. +7. In some cases, we might decide that a PR should be closed without merging. We'll make sure to provide clear reasoning when this happens. Following the recommended process above is one of the ways to ensure you don't spend time on a PR we can't or won't merge. #### Getting Your Pull Requests Merged Faster @@ -119,12 +142,45 @@ If we request changes, try to make those changes in a timely manner. Otherwise, Even with everyone making their best effort to be responsive, it can be time-consuming to get a PR merged. It can be frustrating to deal with the back-and-forth as we make sure that we understand the changes fully. Please bear with us, and please know that we appreciate the time and energy you put into the project. +#### Changelog entries + +If your PR's changes are not user-facing add the label `no-changelog-needed`. If this label isn't present and your PR doesn't include any change files a Github Action workflow will prompt you to add whichever is needed. + +If your PR's changes are user-facing then you will need to add a change file in your PR. See the next section for how to create one. The change file will need to be created in the `.changes/v1.XX/` folder that matches the version number present in [version/VERSION on the main branch](https://github.com/hashicorp/terraform/blob/main/version/VERSION). + +This is different if you are backporting your changes to an earlier release version. In that case, put the change file in the `.changes/v1.XX/` folder for the earliest version that the change is being backported into. For example if a PR was labelled 1.11-backport and 1.10-backport then the change file should be created in the `.changes/v1.10/` folder only. + + +#### Create a change file using `changie` + +If your change is user-facing you can use `npx changie new` to create a new changelog entry via your terminal. The command is interactive and you will need to: select which kind of change you're introducing, provide a short description, and enter either the number of the GitHub issue your PR closes or your PR's number. + +Make sure to select the correct kind of change: + + +| Change kind | When to use | +|------------------|-------------| +| NEW FEATURES | Use this if you've added new, separate functionality to Terraform. For example, introduction of ephemeral resources. | +| ENHANCEMENTS | Use this if you've improved existing functionality in Terraform. Examples include: adding a new field to a remote-state backend, or adding a new environment variable to use when configuring Terraform. | +| BUG FIXES | Use this if you've fixed a user-facing issue. Examples include: crash fixes, improvements to error feedback, regression fixes. | +| NOTES | This is used for changes that are unlikely to cause user-facing issues but might have edge cases. For example, changes to how the Terraform binary is built. | +| UPGRADE NOTES | Use this if you've introduced a change that forces users to take action when upgrading, or changes Terraform's behaviour notably. For example, deprecating a field on a remote-state backend or changing the output of Terraform operations. | +| BREAKING CHANGES | Use this if you've introduced a change that could make a valid Terraform configuration stop working after a user upgrades Terraform versions. This might be paired with an upgrade note change file. Examples include: removing a field on a remote-state backend, changing a builtin function's behavior, making validation stricter. | + +#### Backport a PR to a past release + +PRs can be backported to previous release version as part of preparing a patch release. For example, a fix for a bug could be merged into main but also backported to one or two previous minor versions. + +If you want to backport your PR then the PR needs to have one or more [backport labels](https://github.com/hashicorp/terraform/labels?q=backport) added. The PR reviewer will then ensure that the PR is merged into those versions' release branches, as well as merged into `main`. + ### PR Checks The following checks run when a PR is opened: - Contributor License Agreement (CLA): If this is your first contribution to Terraform you will be asked to sign the CLA. - Tests: tests include unit tests and acceptance tests, and all tests must pass before a PR can be merged. +- Change files: PRs that include user-facing changes should include change files (see [Pull Request Lifecycle](#pull-request-lifecycle)). Automation will verify if PRs are labelled correctly and/or contain appropriate change files. +- Vercel: this is an internal tool that does not run correctly for external contributors. We are aware of this and work around it for external contributions. ---- @@ -136,7 +192,7 @@ Terraform providers are not maintained in this repository; you can find relevant repository and relevant issue tracker for each provider within the [Terraform Registry index](https://registry.terraform.io/browse/providers). -This repository also does not include the source code for some other parts of the Terraform product including Terraform Cloud, Terraform Enterprise, and the Terraform Registry. Those components are not open source, though if you have feedback about them (including bug reports) please do feel free to [open a GitHub issue on this repository](https://github.com/hashicorp/terraform/issues/new/choose). +This repository also does not include the source code for some other parts of the Terraform product including HCP Terraform, Terraform Enterprise, and the Terraform Registry. The source for those components is not publicly available. If you have feedback about these products, including bug reports, please email [tf-cloud@hashicorp.support](mailto:tf-cloud@hashicorp.support) or [open a support request](https://support.hashicorp.com/hc/en-us/requests/new). --- @@ -144,7 +200,7 @@ If you wish to work on the Terraform CLI source code, you'll first need to insta At this time the Terraform development environment is targeting only Linux and Mac OS X systems. While Terraform itself is compatible with Windows, unfortunately the unit test suite currently contains Unix-specific assumptions around maximum path lengths, path separators, etc. -Refer to the file [`.go-version`](https://github.com/hashicorp/terraform/blob/main/.go-version) to see which version of Go Terraform is currently built with. Other versions will often work, but if you run into any build or testing problems please try with the specific Go version indicated. You can optionally simplify the installation of multiple specific versions of Go on your system by installing [`goenv`](https://github.com/syndbg/goenv), which reads `.go-version` and automatically selects the correct Go version. +Refer to the file [`.go-version`](https://github.com/hashicorp/terraform/blob/main/.go-version) to see which version of Go Terraform is currently built with. As of Go 1.21, the `go` command (e.g. in `go build`) will automatically install the version of the Go toolchain corresponding to the version specified in `go.mod`, if it is newer than the version you have installed. The version in `go.mod` is considered the _minimum_ compatible Go version for Terraform, while the version in `.go-version` is what the production binary is actually built with. Use Git to clone this repository into a location of your choice. Terraform is using [Go Modules](https://blog.golang.org/using-go-modules), and so you should *not* clone it inside your `GOPATH`. @@ -180,7 +236,7 @@ go test ./internal/addrs Terraform's unit test suite is self-contained, using mocks and local files to help ensure that it can run offline and is unlikely to be broken by changes to outside systems. -However, several Terraform components interact with external services, such as the automatic provider installation mechanism, the Terraform Registry, Terraform Cloud, etc. +However, several Terraform components interact with external services, such as the automatic provider installation mechanism, the Terraform Registry, HCP Terraform, Terraform Enterprise, etc. There are some optional tests in the Terraform CLI codebase that *do* interact with external services, which we collectively refer to as "acceptance tests". You can enable these by setting the environment variable `TF_ACC=1` when running the tests. We recommend focusing only on the specific package you are working on when enabling acceptance tests, both because it can help the test run to complete faster and because you are less likely to encounter failures due to drift in systems unrelated to your current goal: diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index c4d9de4995..643149eb63 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + name: Bug Report description: Let us know about an unexpected error, a crash, or an incorrect behavior. labels: ["bug", "new"] @@ -9,10 +12,11 @@ body: The [hashicorp/terraform](https://github.com/hashicorp/terraform) issue tracker is reserved for bug reports relating to the core Terraform CLI application and configuration language. - For general usage questions, please see: https://www.terraform.io/community.html. + For general usage questions, please see the [Community Forum](https://discuss.hashicorp.com/c/terraform-core/27). ## If your issue relates to: - * **Terraform Cloud/Enterprise**: please email tf-cloud@hashicorp.support or [open a new request](https://support.hashicorp.com/hc/en-us/requests/new). + * **HCP Terraform or Terraform Enterprise**: please email tf-cloud@hashicorp.support or [open a new request](https://support.hashicorp.com/hc/en-us/requests/new). + * **Terraform Registry**: please email terraform-registry@hashicorp.com. * **AWS Terraform Provider**: Open an issue at [hashicorp/terraform-provider-aws](https://github.com/hashicorp/terraform-provider-aws/issues/new/choose). * **Azure Terraform Provider**: Open an issue at [hashicorp/terraform-provider-azurerm](https://github.com/hashicorp/terraform-provider-azurerm/issues/new/choose). * **Other Terraform Providers**: Please open an issue in the provider's own repository, which can be found by searching the [Terraform Registry](https://registry.terraform.io/browse/providers). @@ -26,7 +30,7 @@ body: * Set defaults on (or omit) any variables. The person reproducing it should not need to invent variable settings * If multiple steps are required, such as running terraform twice, consider scripting it in a simple shell script. Providing a script can be easier than explaining what changes to make to the config between runs. * Omit any unneeded complexity: remove variables, conditional statements, functions, modules, providers, and resources that are not needed to trigger the bug - * When possible, use the [null resource](https://www.terraform.io/docs/providers/null/resource.html) provider rather than a real provider in order to minimize external dependencies. We know this isn't always feasible. The Terraform Core team doesn't have deep domain knowledge in every provider, or access to every cloud platform for reproduction cases. + * When possible, use the [null resource](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) provider rather than a real provider in order to minimize external dependencies. We know this isn't always feasible. The Terraform Core team doesn't have deep domain knowledge in every provider, or access to every cloud platform for reproduction cases. - type: textarea id: tf-version @@ -57,8 +61,11 @@ body: attributes: label: Debug Output description: Full debug output can be obtained by running Terraform with the environment variable `TF_LOG=trace`. Please create a GitHub Gist containing the debug output. Please do _not_ paste the debug output in the issue, since debug output is long. Debug output may contain sensitive information. Please review it before posting publicly. - placeholder: ...link to gist... - value: + placeholder: + value: | + ``` + ...debug output, or link to a gist... + ``` validations: required: true - type: textarea @@ -117,6 +124,17 @@ body: value: validations: required: false + - type: textarea + id: tf-genai + attributes: + label: Generative AI / LLM assisted development? + description: | + If you used a generative AI / LLM tool to assist in the development of your config, please let us know which tool you used here. + ex. ChatGPT 3.5 / CoPilot / AWS Q / etc. + placeholder: LLM assistance tool? + value: + validations: + required: false - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2c525cbbf2..bb6b8e81fd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,11 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + blank_issues_enabled: false contact_links: - - name: Terraform Cloud/Enterprise Troubleshooting and Feature Requests + - name: HCP Terraform and Terraform Enterprise Troubleshooting and Feature Requests url: https://support.hashicorp.com/hc/en-us/requests/new - about: For issues and feature requests related to the Terraform Cloud/Enterprise platform, please submit a HashiCorp support request or email tf-cloud@hashicorp.support + about: For issues and feature requests related to HCP Terraform or Terraform Enterprise, please submit a HashiCorp support request or email tf-cloud@hashicorp.support - name: AWS Terraform Provider Feedback and Questions url: https://github.com/hashicorp/terraform-provider-aws about: The AWS Terraform Provider has its own repository, any provider related issues or questions should be directed there. @@ -17,4 +20,4 @@ contact_links: about: Plugin SDK has its own repository, any SDK and provider development related issues or questions should be directed there. - name: Terraform Usage, Language, or Workflow Questions url: https://discuss.hashicorp.com/c/terraform-core - about: Please ask and answer language or workflow related questions through the Terraform Core Community Forum. \ No newline at end of file + about: Please ask and answer language or workflow related questions through the Terraform Core Community Forum. diff --git a/.github/ISSUE_TEMPLATE/documentation_issue.yml b/.github/ISSUE_TEMPLATE/documentation_issue.yml new file mode 100644 index 0000000000..44da6b2888 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation_issue.yml @@ -0,0 +1,76 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +name: Documentation Issue +description: Report an issue or suggest a change in the documentation. +labels: ["documentation", "new"] +body: + - type: markdown + attributes: + value: | + # Thank you for opening a documentation change request. + + Please only use the [hashicorp/terraform](https://github.com/hashicorp/terraform) `Documentation` issue type to report problems with the documentation on [https://developer.hashicorp.com/terraform/docs](). Only technical writers (not engineers) monitor this issue type. Report Terraform bugs or feature requests with the `Bug report` or `Feature Request` issue types instead to get engineering attention. + + For general usage questions, please see the [Community Forum](https://discuss.hashicorp.com/c/terraform-core/27). + + - type: textarea + id: tf-version + attributes: + label: Terraform Version + description: Run `terraform version` to show the version, and paste the result below. If you're not using the latest version, please check to see if something related to your request has already been implemented in a later version. + render: shell + placeholder: ...output of `terraform version`... + value: + validations: + required: true + + - type: textarea + id: tf-affected-pages + attributes: + label: Affected Pages + description: | + Link to the pages relevant to your documentation change request. + placeholder: + value: + validations: + required: false + + - type: textarea + id: tf-problem + attributes: + label: What is the docs issue? + description: What problems or suggestions do you have about the documentation? + placeholder: + value: + validations: + required: true + + - type: textarea + id: tf-proposal + attributes: + label: Proposal + description: What documentation changes would fix this issue and where would you expect to find them? Are one or more page headings unclear? Do one or more pages need additional context, examples, or warnings? Do we need a new page or section dedicated to a specific topic? Your ideas help us understand what you and other users need from our documentation and how we can improve the content. + placeholder: + value: + validations: + required: false + + - type: textarea + id: tf-references + attributes: + label: References + description: | + Are there any other open or closed GitHub issues related to the problem or solution you described? If so, list them below. For example: + ``` + - #6017 + ``` + placeholder: + value: + validations: + required: false + + - type: markdown + attributes: + value: | + **Note:** If the submit button is disabled and you have filled out all required fields, please check that you did not forget a **Title** for the issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 9549ba8157..9d30a8bd7c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + name: Feature Request description: Suggest a new feature or other enhancement. labels: ["enhancement", "new"] @@ -9,10 +12,10 @@ body: The [hashicorp/terraform](https://github.com/hashicorp/terraform) issue tracker is reserved for feature requests relating to the core Terraform CLI application and configuration language. - For general usage questions, please see: https://www.terraform.io/community.html. + For general usage questions, please see the [Community Forum](https://discuss.hashicorp.com/c/terraform-core/27). ## If your feature request relates to: - * **Terraform Cloud/Enterprise**: please email tf-cloud@hashicorp.support or [open a new request](https://support.hashicorp.com/hc/en-us/requests/new). + * **HCP Terraform or Terraform Enterprise**: please email tf-cloud@hashicorp.support or [open a new request](https://support.hashicorp.com/hc/en-us/requests/new). * **AWS Terraform Provider**: Open an issue at [hashicorp/terraform-provider-aws](https://github.com/hashicorp/terraform-provider-aws/issues/new/choose). * **Azure Terraform Provider**: Open an issue at [hashicorp/terraform-provider-azurerm](https://github.com/hashicorp/terraform-provider-azurerm/issues/new/choose). * **Other Terraform Providers**: Please open an issue in the provider's own repository, which can be found by searching the [Terraform Registry](https://registry.terraform.io/browse/providers). diff --git a/.github/actions/equivalence-test/action.yml b/.github/actions/equivalence-test/action.yml new file mode 100644 index 0000000000..cbfc51505e --- /dev/null +++ b/.github/actions/equivalence-test/action.yml @@ -0,0 +1,79 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +name: equivalence-test +description: "Execute the suite of Terraform equivalence tests in testing/equivalence-tests and update the golden files." +inputs: + target-equivalence-test-version: + description: "The version of the Terraform equivalence tests to use." + default: "0.3.0" + target-os: + description: "Current operating system" + default: "linux" + target-arch: + description: "Current architecture" + default: "amd64" + current-branch: + description: "What branch are we currently on?" + required: true + new-branch: + description: "Name of new branch to be created for the review." + required: true + reviewers: + description: "Comma-separated list of GitHub usernames to request review from." + required: true + message: + description: "Message to include in the commit." + required: true +runs: + using: "composite" + steps: + + - name: "download equivalence test binary" + shell: bash + run: | + ./.github/scripts/equivalence-test.sh download_equivalence_test_binary \ + ${{ inputs.target-equivalence-test-version }} \ + ./bin/equivalence-tests \ + ${{ inputs.target-os }} \ + ${{ inputs.target-arch }} + + - name: Build terraform + shell: bash + run: ./.github/scripts/equivalence-test.sh build_terraform_binary ./bin/terraform + + - name: "run and update equivalence tests" + id: execute + shell: bash + run: | + ./bin/equivalence-tests update \ + --tests=testing/equivalence-tests/tests \ + --goldens=testing/equivalence-tests/outputs \ + --binary=$(pwd)/bin/terraform + + git add --intent-to-add testing/equivalence-tests/outputs + changed=$(git diff --quiet -- testing/equivalence-tests/outputs || echo true) + echo "changed=$changed" >> "${GITHUB_OUTPUT}" + + - name: "branch, commit, and push changes" + if: steps.execute.outputs.changed == 'true' + shell: bash + run: | + git config user.name "hc-github-team-tf-core" + git config user.email "github-team-tf-core@hashicorp.com" + git checkout -b ${{ inputs.new-branch }} + git add testing/equivalence-tests/outputs + git commit -m "Update equivalence test golden files." + git push --set-upstream origin ${{ inputs.new-branch }} + + - name: "create pull request" + if: steps.execute.outputs.changed == 'true' + shell: bash + run: | + gh pr create \ + --draft \ + --base ${{ inputs.current-branch }} \ + --head ${{ inputs.new-branch }} \ + --title "Update equivalence test golden files" \ + --body '${{ inputs.message }}' \ + --reviewer ${{ inputs.reviewers }} diff --git a/.github/actions/go-version/action.yml b/.github/actions/go-version/action.yml index 9c12343b93..f00a478166 100644 --- a/.github/actions/go-version/action.yml +++ b/.github/actions/go-version/action.yml @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + name: 'Determine Go Toolchain Version' description: 'Uses the .go-version file to determine which Go toolchain to use for any Go-related actions downstream.' outputs: @@ -20,4 +23,4 @@ runs: # complex for automation. run: | echo "Building with Go $(cat .go-version)" - echo "::set-output name=version::$(cat .go-version)" + echo "version=$(cat .go-version)" >> "$GITHUB_OUTPUT" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..2a25a17388 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,30 @@ +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: daily + labels: + - dependencies + - go + - security + # Disable regular version updates and only use Dependabot for security updates + open-pull-requests-limit: 0 + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - dependencies + - build + - security + reviewers: + - hashicorp/terraform-core + groups: + github-actions-breaking: + update-types: + - major + github-actions-backward-compatible: + update-types: + - minor + - patch diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..9253d50315 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,46 @@ + + + + +Fixes # + +## Target Release + + + +1.13.x + +## CHANGELOG entry + + + +- [ ] This change is user-facing and I added a changelog entry. +- [ ] This change is not user-facing. diff --git a/.github/scripts/e2e_test_linux_darwin.sh b/.github/scripts/e2e_test_linux_darwin.sh new file mode 100755 index 0000000000..be0b9daa1c --- /dev/null +++ b/.github/scripts/e2e_test_linux_darwin.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +set -uo pipefail + +if [[ $arch == 'arm' || $arch == 'arm64' ]] +then + export DIR=$(mktemp -d) + unzip -d $DIR "${e2e_cache_path}/terraform-e2etest_${os}_${arch}.zip" + unzip -d $DIR "./terraform_${version}_${os}_${arch}.zip" + sudo chmod +x $DIR/e2etest + docker run --platform=linux/arm64 -v $DIR:/src -w /src arm64v8/alpine ./e2etest -test.v +else + unzip "${e2e_cache_path}/terraform-e2etest_${os}_${arch}.zip" + unzip "./terraform_${version}_${os}_${arch}.zip" + TF_ACC=1 ./e2etest -test.v +fi \ No newline at end of file diff --git a/.github/scripts/equivalence-test.sh b/.github/scripts/equivalence-test.sh new file mode 100755 index 0000000000..948c107c56 --- /dev/null +++ b/.github/scripts/equivalence-test.sh @@ -0,0 +1,157 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +set -uo pipefail + +function usage { + cat <<-'EOF' +Usage: ./equivalence-test.sh [] [] + +Description: + This script will handle various commands related to the execution of the + Terraform equivalence tests. + +Commands: + get_target_branch + get_target_branch returns the default target branch for a given Terraform + version. + + target_branch=$(./equivalence-test.sh get_target_branch v1.4.3); target_branch=v1.4 + target_branch=$(./equivalence-test.sh get_target_branch 1.4.3); target_branch=v1.4 + + download_equivalence_test_binary + download_equivalence_test_binary downloads the equivalence testing binary + for a given version and places it at the target path. + + ./equivalence-test.sh download_equivalence_test_binary 0.3.0 ./bin/terraform-equivalence-testing linux amd64 + + build_terraform_binary + download_terraform_binary builds the Terraform binary and places it at the + target path. + + ./equivalence-test.sh build_terraform_binary ./bin/terraform +EOF +} + +function download_equivalence_test_binary { + VERSION="${1:-}" + TARGET="${2:-}" + OS="${3:-}" + ARCH="${4:-}" + + if [[ -z "$VERSION" || -z "$TARGET" || -z "$OS" || -z "$ARCH" ]]; then + echo "missing at least one of [, , , ] arguments" + usage + exit 1 + fi + + curl \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/hashicorp/terraform-equivalence-testing/releases" > releases.json + + ASSET="terraform-equivalence-testing_v${VERSION}_${OS}_${ARCH}.zip" + ASSET_ID=$(jq -r --arg VERSION "v$VERSION" --arg ASSET "$ASSET" '.[] | select(.name == $VERSION) | .assets[] | select(.name == $ASSET) | .id' releases.json) + + mkdir -p zip + curl -L \ + -H "Accept: application/octet-stream" \ + "https://api.github.com/repos/hashicorp/terraform-equivalence-testing/releases/assets/$ASSET_ID" > "zip/$ASSET" + + mkdir -p bin + unzip -p "zip/$ASSET" terraform-equivalence-testing > "$TARGET" + chmod u+x "$TARGET" + rm -r zip + rm releases.json +} + +function build_terraform_binary { + TARGET="${1:-}" + + if [[ -z "$TARGET" ]]; then + echo "target argument" + usage + exit 1 + fi + + go build -o "$TARGET" ./ + chmod u+x "$TARGET" +} + +function get_target_branch { + VERSION="${1:-}" + + if [ -z "$VERSION" ]; then + echo "missing argument" + usage + exit 1 + fi + + + # Split off the build metadata part, if any + # (we won't actually include it in our final version, and handle it only for + # completeness against semver syntax.) + IFS='+' read -ra VERSION BUILD_META <<< "$VERSION" + + # Separate out the prerelease part, if any + IFS='-' read -r BASE_VERSION PRERELEASE <<< "$VERSION" + + # Separate out major, minor and patch versions. + IFS='.' read -r MAJOR_VERSION MINOR_VERSION PATCH_VERSION <<< "$BASE_VERSION" + + if [[ "$PRERELEASE" == *"alpha"* ]]; then + TARGET_BRANCH=main + else + if [[ $MAJOR_VERSION = v* ]]; then + TARGET_BRANCH=${MAJOR_VERSION}.${MINOR_VERSION} + else + TARGET_BRANCH=v${MAJOR_VERSION}.${MINOR_VERSION} + fi + fi + + echo "$TARGET_BRANCH" +} + +function main { + case "$1" in + get_target_branch) + if [ "${#@}" != 2 ]; then + echo "invalid number of arguments" + usage + exit 1 + fi + + get_target_branch "$2" + + ;; + download_equivalence_test_binary) + if [ "${#@}" != 5 ]; then + echo "invalid number of arguments" + usage + exit 1 + fi + + download_equivalence_test_binary "$2" "$3" "$4" "$5" + + ;; + build_terraform_binary) + if [ "${#@}" != 2 ]; then + echo "invalid number of arguments" + usage + exit 1 + fi + + build_terraform_binary "$2" + + ;; + *) + echo "unrecognized command $*" + usage + exit 1 + + ;; + esac +} + +main "$@" +exit $? diff --git a/.github/scripts/get_product_version.sh b/.github/scripts/get_product_version.sh new file mode 100755 index 0000000000..8d553632f5 --- /dev/null +++ b/.github/scripts/get_product_version.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +set -uo pipefail + +# Trim the "v" prefix, if any. +VERSION="${RAW_VERSION#v}" + +# Split off the build metadata part, if any +# (we won't actually include it in our final version, and handle it only for +# compleness against semver syntax.) +IFS='+' read -ra VERSION BUILD_META <<< "$VERSION" + +# Separate out the prerelease part, if any +# (version.go expects it to be in a separate variable) +IFS='-' read -r BASE_VERSION PRERELEASE <<< "$VERSION" + +EXPERIMENTS_ENABLED=0 +if [[ "$PRERELEASE" == alpha* ]]; then +EXPERIMENTS_ENABLED=1 +fi +if [[ "$PRERELEASE" == dev* ]]; then +EXPERIMENTS_ENABLED=1 +fi + +LDFLAGS="-w -s" +if [[ "$EXPERIMENTS_ENABLED" == 1 ]]; then +LDFLAGS="${LDFLAGS} -X 'main.experimentsAllowed=yes'" +fi +LDFLAGS="${LDFLAGS} -X 'github.com/hashicorp/terraform/version.dev=no'" + +echo "Building Terraform CLI ${VERSION}" +if [[ "$EXPERIMENTS_ENABLED" == 1 ]]; then +echo "This build allows use of experimental features" +fi +echo "product-version=${VERSION}" | tee -a "${GITHUB_OUTPUT}" +echo "product-version-base=${BASE_VERSION}" | tee -a "${GITHUB_OUTPUT}" +echo "product-version-pre=${PRERELEASE}" | tee -a "${GITHUB_OUTPUT}" +echo "experiments=${EXPERIMENTS_ENABLED}" | tee -a "${GITHUB_OUTPUT}" +echo "go-ldflags=${LDFLAGS}" | tee -a "${GITHUB_OUTPUT}" \ No newline at end of file diff --git a/.github/scripts/verify_docker b/.github/scripts/verify_docker new file mode 100755 index 0000000000..31c9dd744b --- /dev/null +++ b/.github/scripts/verify_docker @@ -0,0 +1,47 @@ +#!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + + +set -euo pipefail + +# verify_docker invokes the given Docker image with the argument `version` and inspects its output. +# If its output doesn't match the version given, the script will exit 1 and report why it failed. +# This is meant to be run as part of the build workflow to verify the built image meets some basic +# criteria for validity. +# +# Because this is meant to be run as the `smoke_test` for the docker-build workflow, the script expects +# the image name parameter to be provided by the `IMAGE_NAME` environment variable, rather than a +# positional argument. + +function usage { + echo "IMAGE_NAME= ./verify_docker " +} + +function main { + local image_name="${IMAGE_NAME:-}" + local expect_version="${1:-}" + local got_version + + if [[ -z "${image_name}" ]]; then + echo "ERROR: IMAGE_NAME is not set" + usage + exit 1 + fi + + if [[ -z "${expect_version}" ]]; then + echo "ERROR: expected version argument is required" + usage + exit 1 + fi + + got_version="$( awk '{print $2}' <(head -n1 <(docker run --rm "${image_name}" version)) )" + if [ "${got_version}" != "${expect_version}" ]; then + echo "Test FAILED" + echo "Got: ${got_version}, Want: ${expect_version}" + exit 1 + fi + echo "Test PASSED" +} + +main "$@" diff --git a/.github/workflows/build-terraform-cli.yml b/.github/workflows/build-terraform-cli.yml new file mode 100644 index 0000000000..8aeb11f76b --- /dev/null +++ b/.github/workflows/build-terraform-cli.yml @@ -0,0 +1,99 @@ +--- +name: build_terraform + +# This workflow is intended to be called by the build workflow. The crt make +# targets that are utilized automatically determine build metadata and +# handle building and packing Terraform. + +on: + workflow_call: + inputs: + cgo-enabled: + type: string + default: 0 + required: true + goos: + required: true + type: string + goarch: + required: true + type: string + go-version: + type: string + package-name: + type: string + default: terraform + product-version: + type: string + required: true + ld-flags: + type: string + required: true + runson: + type: string + required: true + +jobs: + build: + runs-on: ${{ inputs.runson }} + name: Terraform ${{ inputs.goos }} ${{ inputs.goarch }} v${{ inputs.product-version }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ inputs.go-version }} + - name: Build Terraform + env: + GOOS: ${{ inputs.goos }} + GOARCH: ${{ inputs.goarch }} + GO_LDFLAGS: ${{ inputs.ld-flags }} + ACTIONSOS: ${{ inputs.runson }} + CGO_ENABLED: ${{ inputs.cgo-enabled }} + uses: hashicorp/actions-go-build@37358f6098ef21b09542d84a9814ebb843aa4e3e # v1 + with: + product_name: ${{ inputs.package-name }} + product_version: ${{ inputs.product-version }} + go_version: ${{ inputs.go-version }} + os: ${{ inputs.goos }} + arch: ${{ inputs.goarch }} + reproducible: nope + instructions: |- + go build -ldflags "${{ inputs.ld-flags }}" -o "$BIN_PATH" -trimpath -buildvcs=false + cp LICENSE "$TARGET_DIR/LICENSE.txt" + - name: Copy license file to config_dir + if: ${{ inputs.goos == 'linux' }} + env: + LICENSE_DIR: ".release/linux/package/usr/share/doc/${{ inputs.package-name }}" + run: | + mkdir -p "$LICENSE_DIR" && cp LICENSE "$LICENSE_DIR/LICENSE.txt" + - if: ${{ inputs.goos == 'linux' }} + uses: hashicorp/actions-packaging-linux@8d55a640bb30b5508f16757ea908b274564792d4 # v1.9 + with: + name: "terraform" + description: "Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned." + arch: ${{ inputs.goarch }} + version: ${{ inputs.product-version }} + maintainer: "HashiCorp" + homepage: "https://terraform.io/" + license: "BUSL-1.1" + binary: "dist/terraform" + deb_depends: "git" + rpm_depends: "git" + config_dir: ".release/linux/package/" + - if: ${{ inputs.goos == 'linux' }} + name: Determine package file names + run: | + echo "RPM_PACKAGE=$(basename out/*.rpm)" >> $GITHUB_ENV + echo "DEB_PACKAGE=$(basename out/*.deb)" >> $GITHUB_ENV + - if: ${{ inputs.goos == 'linux' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ env.RPM_PACKAGE }} + path: out/${{ env.RPM_PACKAGE }} + if-no-files-found: error + - if: ${{ inputs.goos == 'linux' }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: ${{ env.DEB_PACKAGE }} + path: out/${{ env.DEB_PACKAGE }} + if-no-files-found: error diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ee68fec8ca..210c126388 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,31 +1,19 @@ -name: Build Terraform CLI Packages +name: build # If you want to test changes to this file before merging to a main branch, # push them up to a branch whose name has the prefix "build-workflow-dev/", # which is a special prefix that triggers this workflow even though it's not # actually a release branch. -# NOTE: This workflow is currently used only to verify that all commits to a -# release branch are buildable. It's set up to generate some artifacts that -# might in principle be consumed by a downstream release process, but currently -# they are not used in this way and official Terraform CLI releases are instead -# built using a separate process maintained elsewhere. We intend to adopt this -# new process fully later, once other HashiCorp-internal tooling is ready. -# -# Currently this process produces what should be working packages but packages -# NOT suitable for distribution to end-users as official releases, because it -# doesn't include a step to ensure that "terraform version" (and similar) will -# report the intended version number. Consequently we can safely use these -# results for testing purposes, but not yet for release purposes. See the -# "build" job below for a FIXME comment related to version numbers. - on: workflow_dispatch: push: branches: - main - 'v[0-9]+.[0-9]+' - - build-workflow-dev/* + - releng/** + - tsccr-auto-pinning/** + - dependabot/** tags: - 'v[0-9]+.[0-9]+.[0-9]+*' @@ -42,71 +30,28 @@ jobs: runs-on: ubuntu-latest outputs: product-version: ${{ steps.get-product-version.outputs.product-version }} - product-version-base: ${{ steps.get-product-version.outputs.product-version-base }} - product-version-pre: ${{ steps.get-product-version.outputs.product-version-pre }} - experiments: ${{ steps.get-product-version.outputs.experiments }} - go-ldflags: ${{ steps.get-product-version.outputs.go-ldflags }} + product-version-base: ${{ steps.get-product-version.outputs.base-product-version }} + product-version-pre: ${{ steps.get-product-version.outputs.prerelease-product-version }} + experiments: ${{ steps.get-ldflags.outputs.experiments }} + go-ldflags: ${{ steps.get-ldflags.outputs.go-ldflags }} + pkg-name: ${{ steps.get-pkg-name.outputs.pkg-name }} steps: - - uses: actions/checkout@v3 - - name: Git Describe - id: git-describe + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: Get Package Name + id: get-pkg-name run: | - # The actions/checkout action tries hard to fetch as little as - # possible, to the extent that even with "depth: 0" it fails to - # produce enough tag metadata for us to "describe" successfully. - # We'll therefore re-fetch the tags here to make sure we will - # select the most accurate version number. - git fetch origin --force --tags --quiet --unshallow - git log --tags --simplify-by-decoration --decorate-refs='refs/tags/v*' --pretty=format:'%h %<|(35)%S %ci' --max-count 15 --topo-order - set -e - RAW_VERSION=$(git describe --tags --match='v*' ${GITHUB_SHA}) - echo " - - Raw version is ${RAW_VERSION}" - echo "::set-output name=raw-version::${RAW_VERSION}" + pkg_name=${{ env.PKG_NAME }} + echo "pkg-name=${pkg_name}" | tee -a "${GITHUB_OUTPUT}" - name: Decide version number id: get-product-version - shell: bash + uses: hashicorp/actions-set-product-version@d9b52fb778068099ca4c5e28e1ca0fee2544e114 # v2 + - name: Determine experiments + id: get-ldflags env: - RAW_VERSION: ${{ steps.git-describe.outputs.raw-version }} - run: | - # Trim the "v" prefix, if any. - VERSION="${RAW_VERSION#v}" - - # Split off the build metadata part, if any - # (we won't actually include it in our final version, and handle it only for - # compleness against semver syntax.) - IFS='+' read -ra VERSION BUILD_META <<< "$VERSION" - - # Separate out the prerelease part, if any - # (version.go expects it to be in a separate variable) - IFS='-' read -r BASE_VERSION PRERELEASE <<< "$VERSION" - - EXPERIMENTS_ENABLED=0 - if [[ "$PRERELEASE" == alpha* ]]; then - EXPERIMENTS_ENABLED=1 - fi - if [[ "$PRERELEASE" == dev* ]]; then - EXPERIMENTS_ENABLED=1 - fi - - LDFLAGS="-w -s" - if [[ "$EXPERIMENTS_ENABLED" == 1 ]]; then - LDFLAGS="${LDFLAGS} -X 'main.experimentsAllowed=yes'" - fi - LDFLAGS="${LDFLAGS} -X 'github.com/hashicorp/terraform/version.Version=${BASE_VERSION}'" - LDFLAGS="${LDFLAGS} -X 'github.com/hashicorp/terraform/version.Prerelease=${PRERELEASE}'" - - echo "Building Terraform CLI ${VERSION}" - if [[ "$EXPERIMENTS_ENABLED" == 1 ]]; then - echo "This build allows use of experimental features" - fi - echo "::set-output name=product-version::${VERSION}" - echo "::set-output name=product-version-base::${BASE_VERSION}" - echo "::set-output name=product-version-pre::${PRERELEASE}" - echo "::set-output name=experiments::${EXPERIMENTS_ENABLED}" - echo "::set-output name=go-ldflags::${LDFLAGS}" + RAW_VERSION: ${{ steps.get-product-version.outputs.product-version }} + shell: bash + run: .github/scripts/get_product_version.sh - name: Report chosen version number run: | [ -n "${{steps.get-product-version.outputs.product-version}}" ] @@ -119,7 +64,7 @@ jobs: go-version: ${{ steps.get-go-version.outputs.version }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Determine Go version id: get-go-version uses: ./.github/actions/go-version @@ -132,165 +77,54 @@ jobs: filepath: ${{ steps.generate-metadata-file.outputs.filepath }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Generate package metadata id: generate-metadata-file - uses: hashicorp/actions-generate-metadata@v1 + uses: hashicorp/actions-generate-metadata@fdbc8803a0e53bcbb912ddeee3808329033d6357 # v1.1.1 with: version: ${{ needs.get-product-version.outputs.product-version }} product: ${{ env.PKG_NAME }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: metadata.json path: ${{ steps.generate-metadata-file.outputs.filepath }} build: name: Build for ${{ matrix.goos }}_${{ matrix.goarch }} - runs-on: ${{ matrix.runson }} needs: - get-product-version - get-go-version + uses: ./.github/workflows/build-terraform-cli.yml + with: + goarch: ${{ matrix.goarch }} + goos: ${{ matrix.goos }} + go-version: ${{ needs.get-go-version.outputs.go-version }} + package-name: ${{ needs.get-product-version.outputs.pkg-name }} + product-version: ${{ needs.get-product-version.outputs.product-version }} + ld-flags: ${{ needs.get-product-version.outputs.go-ldflags }} + cgo-enabled: ${{ matrix.cgo-enabled }} + runson: ${{ matrix.runson }} + secrets: inherit strategy: matrix: include: - - {goos: "freebsd", goarch: "386", runson: "ubuntu-latest"} - - {goos: "freebsd", goarch: "amd64", runson: "ubuntu-latest"} - - {goos: "freebsd", goarch: "arm", runson: "ubuntu-latest"} - - {goos: "linux", goarch: "386", runson: "ubuntu-latest"} - - {goos: "linux", goarch: "amd64", runson: "ubuntu-latest"} - - {goos: "linux", goarch: "arm", runson: "ubuntu-latest"} - - {goos: "linux", goarch: "arm64", runson: "ubuntu-latest"} - - {goos: "openbsd", goarch: "386", runson: "ubuntu-latest"} - - {goos: "openbsd", goarch: "amd64", runson: "ubuntu-latest"} - - {goos: "solaris", goarch: "amd64", runson: "ubuntu-latest"} - - {goos: "windows", goarch: "386", runson: "ubuntu-latest"} - - {goos: "windows", goarch: "amd64", runson: "ubuntu-latest"} - - {goos: "darwin", goarch: "amd64", runson: "macos-latest"} - - {goos: "darwin", goarch: "arm64", runson: "macos-latest"} + - {goos: "freebsd", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "freebsd", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "freebsd", goarch: "arm", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "linux", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "linux", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "linux", goarch: "arm", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "linux", goarch: "arm64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "openbsd", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "openbsd", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "solaris", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "windows", goarch: "386", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "windows", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "darwin", goarch: "amd64", runson: "ubuntu-latest", cgo-enabled: "0"} + - {goos: "darwin", goarch: "arm64", runson: "ubuntu-latest", cgo-enabled: "0"} fail-fast: false - env: - FULL_VERSION: ${{ needs.get-product-version.outputs.product-version }} - BASE_VERSION: ${{ needs.get-product-version.outputs.product-version-base }} - VERSION_PRERELEASE: ${{ needs.get-product-version.outputs.product-version-pre }} - EXPERIMENTS_ENABLED: ${{ needs.get-product-version.outputs.experiments }} - GO_LDFLAGS: ${{ needs.get-product-version.outputs.go-ldflags }} - - steps: - - uses: actions/checkout@v2 - - - name: Install Go toolchain - uses: actions/setup-go@v2 - with: - go-version: ${{ needs.get-go-version.outputs.go-version }} - - # FIXME: We're not currently setting the hard-coded version string in - # version/version.go at any point here, which means that the packages - # this process builds are not suitable for release. Once we're using - # Go 1.18 we may begin using the version information automatically - # embedded by the Go toolchain, at which point we won't need any - # special steps during build, but failing that we'll need to rework - # the version/version.go package so we can more readily update it - # using linker flags rather than direct code modification. - - - name: Build - env: - GOOS: ${{ matrix.goos }} - GOARCH: ${{ matrix.goarch }} - ACTIONSOS: ${{ matrix.runson }} - run: | - mkdir dist out - if [ "$ACTIONSOS" == "macos-latest" ] && [ "$GOOS" == "darwin" ]; then - # When building for macOS _on_ macOS we must force CGo to get - # correct hostname resolution behavior. (This must be conditional - # because other cross-compiles won't have suitable headers - # available to use CGo; darwin_amd64 has suitable headers to - # cross-build for darwin_arm64.) - export CGO_ENABLED=1 - fi - set -x - go build -ldflags "${GO_LDFLAGS}" -o dist/ . - zip -r -j out/${{ env.PKG_NAME }}_${FULL_VERSION}_${{ matrix.goos }}_${{ matrix.goarch }}.zip dist/ - - - uses: actions/upload-artifact@v2 - with: - name: ${{ env.PKG_NAME }}_${{ env.FULL_VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip - path: out/${{ env.PKG_NAME }}_${{ env.FULL_VERSION }}_${{ matrix.goos }}_${{ matrix.goarch }}.zip - - package-linux: - name: "Build Linux distro packages for ${{ matrix.arch }}" - runs-on: ubuntu-latest - needs: - - get-product-version - - build - strategy: - matrix: - include: - - {arch: "386"} - - {arch: "amd64"} - - {arch: "arm"} - - {arch: "arm64"} - fail-fast: false - - env: - os: linux - arch: ${{matrix.arch}} - version: ${{needs.get-product-version.outputs.product-version}} - - steps: - - name: "Download Terraform CLI package" - uses: actions/download-artifact@v2 - id: clipkg - with: - name: terraform_${{ env.version }}_${{ env.os }}_${{ env.arch }}.zip - path: . - - name: Extract packages - run: | - mkdir -p dist - (cd dist && unzip "../terraform_${{ env.version }}_${{ env.os }}_${{ env.arch }}.zip") - mkdir -p out - - name: Build Linux distribution packages - uses: hashicorp/actions-packaging-linux@v1 - with: - name: "terraform" - description: "Terraform enables you to safely and predictably create, change, and improve infrastructure. It is an open source tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned." - arch: ${{ matrix.arch }} - version: ${{ env.version }} - maintainer: "HashiCorp" - homepage: "https://terraform.io/" - license: "MPL-2.0" - binary: "dist/terraform" - deb_depends: "git" - rpm_depends: "git" - - name: Gather Linux distribution package filenames - run: | - echo "RPM_PACKAGE=$(basename out/*.rpm)" >> $GITHUB_ENV - echo "DEB_PACKAGE=$(basename out/*.deb)" >> $GITHUB_ENV - - name: "Save .rpm package" - uses: actions/upload-artifact@v2 - with: - name: ${{ env.RPM_PACKAGE }} - path: out/${{ env.RPM_PACKAGE }} - - name: "Save .deb package" - uses: actions/upload-artifact@v2 - with: - name: ${{ env.DEB_PACKAGE }} - path: out/${{ env.DEB_PACKAGE }} - - # TODO: homebrew packages for macOS - #package-homebrew: - # name: Build Homebrew package for darwin_${{ matrix.arch }} - # runs-on: macos-latest - # needs: - # - get-product-version - # - build - # strategy: - # matrix: - # arch: ["amd64", "arm64"] - # fail-fast: false - # ... - package-docker: name: Build Docker image for linux_${{ matrix.arch }} runs-on: ubuntu-latest @@ -299,61 +133,64 @@ jobs: - build strategy: matrix: - arch: ["amd64"] + arch: ["amd64", "386", "arm", "arm64"] fail-fast: false - env: repo: "terraform" version: ${{needs.get-product-version.outputs.product-version}} - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Build Docker images - uses: hashicorp/actions-docker-build@v1 + uses: hashicorp/actions-docker-build@11d43ef520c65f58683d048ce9b47d6617893c9a # v2 with: pkg_name: "terraform_${{env.version}}" version: ${{env.version}} bin_name: terraform target: default arch: ${{matrix.arch}} - dockerfile: .github/workflows/build-Dockerfile + dockerfile: build.Dockerfile + smoke_test: .github/scripts/verify_docker v${{ env.version }} tags: | docker.io/hashicorp/${{env.repo}}:${{env.version}} - 986891699432.dkr.ecr.us-east-1.amazonaws.com/hashicorp/${{env.repo}}:${{env.version}} + public.ecr.aws/hashicorp/${{env.repo}}:${{env.version}} e2etest-build: name: Build e2etest for ${{ matrix.goos }}_${{ matrix.goarch }} runs-on: ubuntu-latest + outputs: + e2e-cache-key: ${{ steps.set-cache-values.outputs.e2e-cache-key }} + e2e-cache-path: ${{ steps.set-cache-values.outputs.e2e-cache-path }} needs: - get-product-version - get-go-version strategy: matrix: - # We build test harnesses only for the v1.0 Compatibility Promises - # supported platforms. Even within that set, we can only run on - # architectures for which we have GitHub Actions runners available, - # which is currently only amd64 (x64). - # TODO: GitHub Actions does support _self-hosted_ arm and arm64 - # runners, so we could potentially run some ourselves to run our - # tests there, but at the time of writing there is no documented - # support for darwin_arm64 (macOS on Apple Silicon). include: - {goos: "darwin", goarch: "amd64"} - #- {goos: "darwin", goarch: "arm64"} + - {goos: "darwin", goarch: "arm64"} - {goos: "windows", goarch: "amd64"} + - {goos: "windows", goarch: "386"} + - {goos: "linux", goarch: "386"} - {goos: "linux", goarch: "amd64"} - #- {goos: "linux", goarch: "arm"} - #- {goos: "linux", goarch: "arm64"} + - {goos: "linux", goarch: "arm"} + - {goos: "linux", goarch: "arm64"} fail-fast: false env: build_script: ./internal/command/e2etest/make-archive.sh steps: - - uses: actions/checkout@v2 + - name: Set Cache Values + id: set-cache-values + run: | + cache_key=e2e-cache-${{ github.sha }} + cache_path=internal/command/e2etest/build + echo "e2e-cache-key=${cache_key}" | tee -a "${GITHUB_OUTPUT}" + echo "e2e-cache-path=${cache_path}" | tee -a "${GITHUB_OUTPUT}" + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Install Go toolchain - uses: actions/setup-go@v2 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ needs.get-go-version.outputs.go-version }} @@ -369,15 +206,15 @@ jobs: # that "terraform version" is returning that version number. bash ./internal/command/e2etest/make-archive.sh - - uses: actions/upload-artifact@v2 + - name: Save test harness to cache + uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: - name: terraform-e2etest_${{ matrix.goos }}_${{ matrix.goarch }}.zip - path: internal/command/e2etest/build/terraform-e2etest_${{ matrix.goos }}_${{ matrix.goarch }}.zip - if-no-files-found: error + path: ${{ steps.set-cache-values.outputs.e2e-cache-path }} + key: ${{ steps.set-cache-values.outputs.e2e-cache-key }}_${{ matrix.goos }}_${{ matrix.goarch }} - e2etest-linux: - name: e2etest for linux_${{ matrix.goarch }} - runs-on: ubuntu-latest + e2e-test: + name: Run e2e test for ${{ matrix.goos }}_${{ matrix.goarch }} + runs-on: ${{ matrix.runson }} needs: - get-product-version - build @@ -385,13 +222,17 @@ jobs: strategy: matrix: include: - - {goarch: "amd64"} - #- {goarch: "arm64"} - #- {goarch: "arm"} + - { runson: ubuntu-latest, goos: linux, goarch: "amd64" } + - { runson: ubuntu-latest, goos: linux, goarch: "386" } + - { runson: ubuntu-latest, goos: linux, goarch: "arm" } + - { runson: ubuntu-latest, goos: linux, goarch: "arm64" } + - { runson: macos-latest, goos: darwin, goarch: "amd64" } + - { runson: windows-latest, goos: windows, goarch: "amd64" } + - { runson: windows-latest, goos: windows, goarch: "386" } fail-fast: false env: - os: linux + os: ${{ matrix.goos }} arch: ${{ matrix.goarch }} version: ${{needs.get-product-version.outputs.product-version}} @@ -402,140 +243,81 @@ jobs: # and e2etest package for this platform. (This helps ensure that we're # really testing the release package and not inadvertently testing a # fresh build from source.) - - name: "Download e2etest package" - uses: actions/download-artifact@v2 + - name: Checkout repo + if: ${{ (matrix.goos == 'linux') || (matrix.goos == 'darwin') }} + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - name: "Restore cache" + uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 id: e2etestpkg with: - name: terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip - path: . + path: ${{ needs.e2etest-build.outputs.e2e-cache-path }} + key: ${{ needs.e2etest-build.outputs.e2e-cache-key }}_${{ matrix.goos }}_${{ matrix.goarch }} + fail-on-cache-miss: true + enableCrossOsArchive: true - name: "Download Terraform CLI package" - uses: actions/download-artifact@v2 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 id: clipkg with: name: terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip path: . - name: Extract packages + if: ${{ matrix.goos == 'windows' }} run: | - unzip "./terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip" + unzip "${{ needs.e2etest-build.outputs.e2e-cache-path }}/terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip" unzip "./terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip" - - name: Run E2E Tests - run: | - TF_ACC=1 ./e2etest -test.v - - e2etest-darwin: - name: e2etest for darwin_${{ matrix.goarch }} - runs-on: macos-latest - needs: - - get-product-version - - build - - e2etest-build - strategy: - matrix: - include: - - {goarch: "amd64"} - #- {goarch: "arm64"} - fail-fast: false - - env: - os: darwin - arch: ${{ matrix.goarch }} - version: ${{needs.get-product-version.outputs.product-version}} - - steps: - # NOTE: This intentionally _does not_ check out the source code - # for the commit/tag we're building, because by now we should - # have everything we need in the combination of CLI release package - # and e2etest package for this platform. (This helps ensure that we're - # really testing the release package and not inadvertently testing a - # fresh build from source.) - - name: "Download e2etest package" - uses: actions/download-artifact@v2 - id: e2etestpkg + - name: Set up QEMU + uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0 + if: ${{ contains(matrix.goarch, 'arm') }} with: - name: terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip - path: . - - name: "Download Terraform CLI package" - uses: actions/download-artifact@v2 - id: clipkg - with: - name: terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip - path: . - - name: Extract packages - run: | - unzip "./terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip" - unzip "./terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip" - - name: Run E2E Tests - run: | - TF_ACC=1 ./e2etest -test.v - - e2etest-windows: - name: e2etest for windows_${{ matrix.goarch }} - runs-on: windows-latest - needs: - - get-product-version - - build - - e2etest-build - strategy: - matrix: - include: - - {goarch: "amd64"} - fail-fast: false - - env: - os: windows - arch: ${{ matrix.goarch }} - version: ${{needs.get-product-version.outputs.product-version}} - - steps: - # NOTE: This intentionally _does not_ check out the source code - # for the commit/tag we're building, because by now we should - # have everything we need in the combination of CLI release package - # and e2etest package for this platform. (This helps ensure that we're - # really testing the release package and not inadvertently testing a - # fresh build from source.) - - name: "Download e2etest package" - uses: actions/download-artifact@v2 - id: e2etestpkg - with: - name: terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip - path: . - - name: "Download Terraform CLI package" - uses: actions/download-artifact@v2 - id: clipkg - with: - name: terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip - path: . - - name: Extract packages - shell: pwsh - run: | - Expand-Archive -LiteralPath 'terraform-e2etest_${{ env.os }}_${{ env.arch }}.zip' -DestinationPath '.' - Expand-Archive -LiteralPath 'terraform_${{env.version}}_${{ env.os }}_${{ env.arch }}.zip' -DestinationPath '.' - - name: Run E2E Tests + platforms: all + - name: Run E2E Tests (Darwin & Linux) + id: get-product-version + shell: bash + if: ${{ (matrix.goos == 'linux') || (matrix.goos == 'darwin') }} + env: + e2e_cache_path: ${{ needs.e2etest-build.outputs.e2e-cache-path }} + run: .github/scripts/e2e_test_linux_darwin.sh + - name: Run E2E Tests (Windows) + if: ${{ matrix.goos == 'windows' }} env: TF_ACC: 1 shell: cmd - run: | - e2etest.exe -test.v + run: e2etest.exe -test.v - docs-source-package: - name: "Build documentation bundle" + + e2e-test-exec: + name: Run terraform-exec test for linux amd64 runs-on: ubuntu-latest needs: - get-product-version + - get-go-version + - build env: + os: ${{ matrix.goos }} + arch: ${{ matrix.goarch }} version: ${{needs.get-product-version.outputs.product-version}} steps: - - uses: actions/checkout@v2 - # FIXME: We should include some sort of pre-validation step here, to - # confirm that the doc content is mechanically valid so that the - # publishing pipeline will be able to render all content without errors. - - name: "Create documentation source bundle" - run: | - (cd website && zip -9 -r ../terraform-cli-docs-source_${{ env.version }}.zip .) - - uses: actions/upload-artifact@v2 + - name: Install Go toolchain + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: - name: terraform-cli-docs-source_${{ env.version }}.zip - path: terraform-cli-docs-source_${{ env.version }}.zip - if-no-files-found: error + go-version: ${{ needs.get-go-version.outputs.go-version }} + - name: Download Terraform CLI package + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + id: clipkg + with: + name: terraform_${{ env.version }}_linux_amd64.zip + path: . + - name: Checkout terraform-exec repo + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: hashicorp/terraform-exec + path: terraform-exec + - name: Run terraform-exec end-to-end tests + run: | + FULL_RELEASE_VERSION="${{ env.version }}" + unzip terraform_${FULL_RELEASE_VERSION}_linux_amd64.zip + export TFEXEC_E2ETEST_TERRAFORM_PATH="$(pwd)/terraform" + cd terraform-exec + go test -race -timeout=30m -v ./tfexec/internal/e2etest diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000000..3bf05a2cfb --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,194 @@ +# This workflow makes sure contributors don't forget to add a changelog entry or explicitly opt-out of it. +# +# Do not extend this workflow to include checking out the code (e.g. for building and testing purposes) while the pull_request_target trigger is used. +# Instead, see use of workflow_run in https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ + +name: Changelog + +on: + # The pull_request_target trigger event allows PRs raised from forks to have write permissions and access secrets. + # We uses it in this workflow to enable writing comments to the PR. + pull_request_target: + types: + - opened + - ready_for_review + - reopened + - synchronize + - labeled + - unlabeled + +# This workflow runs for not-yet-reviewed external contributions. +# Following a pull_request_target trigger the workflow would have write permissions, +# so we intentionally restrict the permissions to only include write access on pull-requests. +permissions: + contents: read + pull-requests: write + +jobs: + check-changelog-entry: + name: "Check Changelog Entry" + runs-on: ubuntu-latest + concurrency: + group: changelog-${{ github.head_ref }} + cancel-in-progress: true + + steps: + - name: "Changed files" + uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 + id: changelog + with: + filters: | + changes: + - '.changes/*/*.yaml' + changelog: + - 'CHANGELOG.md' + version: + - 'version/VERSION' + list-files: json + + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + sparse-checkout: | + version/VERSION + .changie.yaml + .changes/ + sparse-checkout-cone-mode: false + ref: ${{ github.ref }} # Ref refers to the target branch of this PR + + - name: "Check for changelog entry" + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 + with: + script: | + const fs = require("fs"); + async function createOrUpdateChangelogComment(commentDetails, deleteComment) { + const commentStart = "## Changelog Warning" + + const body = commentStart + "\n\n" + commentDetails; + const { number: issue_number } = context.issue; + const { owner, repo } = context.repo; + + // List all comments + const allComments = (await github.rest.issues.listComments({ + issue_number, + owner, + repo, + })).data; + + const existingComment = allComments.find(c => c.body.startsWith(commentStart)); + const comment_id = existingComment?.id; + + if (deleteComment) { + if (existingComment) { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id, + }); + } + return; + } + + core.setFailed(commentDetails); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + } + + const changesPresent = ${{steps.changelog.outputs.changes}}; + const changedChangesFiles = ${{steps.changelog.outputs.changes_files}}; + const changelogChangesPresent = ${{steps.changelog.outputs.changelog}}; + const versionChangesPresent = ${{steps.changelog.outputs.version}}; + + const prLabels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + const backportLabels = prLabels.data.filter(label => label.name.endsWith("-backport")); + const backportVersions = backportLabels.map(label => label.name.split("-")[0]); + + const currentVersionFile = fs.readFileSync("./version/VERSION", "utf-8"); + const currentVersionParts = currentVersionFile.split("."); + currentVersionParts.pop(); + const currentVersion = currentVersionParts.join("."); + + const allVersions = [currentVersion, ...backportVersions] + allVersions.sort((a, b) => { + const as = a.split(".").map(Number); + const bs = b.split(".").map(Number); + + if (as[0] !== bs[0]) { + return as[0] - bs[0]; + } + + if (as[1] !== bs[1]) { + return as[1] - bs[1]; + } + }); + + const noChangelogNeededLabel = prLabels.data.find(label => label.name === 'no-changelog-needed'); + const dependenciesLabel = prLabels.data.find(label => label.name === 'dependencies'); + + // We want to prohibit contributors from directly changing the CHANGELOG.md, it's + // generated so all changes will be lost during the release process. + // Therefore we only allow the changelog to change together with the version. + // In very rare cases where we generate changes in the changelog without changing the + // version we will just ignore this failing check. + if (changelogChangesPresent && !versionChangesPresent) { + await createOrUpdateChangelogComment("Please don't edit the CHANGELOG.md manually. We use changie to control the Changelog generation, please use `npx changie new` to create a new changelog entry."); + return; + } + + if (dependenciesLabel) { + return; + } + + if (noChangelogNeededLabel) { + if (changesPresent) { + await createOrUpdateChangelogComment("Please remove either the 'no-changelog-needed' label or the changelog entry from this PR."); + return; + } + + // Nothing to complain about, so delete any existing comment + await createOrUpdateChangelogComment("", true); + return; + } + + // We only want to have a changelog entry for the oldest version this PR will + // land in. + const onlyExpectedChangeVersion = allVersions[0] + const missingChangelogEntry = !changedChangesFiles.some(filePath => filePath.includes("/v"+onlyExpectedChangeVersion+"/")) + const unexpectedChangelogEntry = changedChangesFiles.filter(filePath => !filePath.includes("/v"+onlyExpectedChangeVersion+"/")) + + + if (missingChangelogEntry) { + await createOrUpdateChangelogComment(`Currently this PR would target a v${onlyExpectedChangeVersion} release. Please add a changelog entry for in the .changes/v${onlyExpectedChangeVersion} folder, or discuss which release you'd like to target with your reviewer. If you believe this change does not need a changelog entry, please add the 'no-changelog-needed' label.`); + return; + } + + if (unexpectedChangelogEntry.length > 0) { + await createOrUpdateChangelogComment(`Please remove the changelog entry for the following paths: ${unexpectedChangelogEntry.join(", ")}. If you believe this change does not need a changelog entry, please add the 'no-changelog-needed' label.`); + return; + } + + // Nothing to complain about, so delete any existing comment + await createOrUpdateChangelogComment("", true); + + - name: Validate changie fragment is valid + uses: miniscruff/changie-action@6dcc2533cac0495148ed4046c438487e4dceaa23 # v2.0.0 + with: + version: latest + args: merge -u "." --dry-run diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index c275445c9f..8a3526f882 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -17,11 +17,14 @@ name: Quick Checks on: pull_request: + types: + - opened + - ready_for_review + - reopened + - synchronize push: branches: - - main - - 'v[0-9]+.[0-9]+' - - checks-workflow-dev/* + - '*' tags: - 'v[0-9]+.[0-9]+.[0-9]+*' @@ -38,31 +41,24 @@ jobs: steps: - name: "Fetch source code" - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Determine Go version id: go uses: ./.github/actions/go-version - name: Install Go toolchain - uses: actions/setup-go@v2 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ steps.go.outputs.version }} - - # NOTE: This cache is shared so the following step must always be - # identical across the unit-tests, e2e-tests, and consistency-checks - # jobs, or else weird things could happen. - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: "~/go/pkg" - key: go-mod-${{ hashFiles('go.sum') }} - restore-keys: | - go-mod- + cache-dependency-path: go.sum - name: "Unit tests" run: | - go test ./... + # We run tests for all packages from all modules in this repository. + for dir in $(go list -m -f '{{.Dir}}' github.com/hashicorp/terraform/...); do + (cd $dir && go test -cover "./...") + done race-tests: name: "Race Tests" @@ -70,27 +66,17 @@ jobs: steps: - name: "Fetch source code" - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Determine Go version id: go uses: ./.github/actions/go-version - name: Install Go toolchain - uses: actions/setup-go@v2 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ steps.go.outputs.version }} - - # NOTE: This cache is shared so the following step must always be - # identical across the unit-tests, e2e-tests, and consistency-checks - # jobs, or else weird things could happen. - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: "~/go/pkg" - key: go-mod-${{ hashFiles('go.sum') }} - restore-keys: | - go-mod- + cache-dependency-path: go.sum # The race detector add significant time to the unit tests, so only run # it for select packages. @@ -108,27 +94,17 @@ jobs: steps: - name: "Fetch source code" - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Determine Go version id: go uses: ./.github/actions/go-version - name: Install Go toolchain - uses: actions/setup-go@v2 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ steps.go.outputs.version }} - - # NOTE: This cache is shared so the following step must always be - # identical across the unit-tests, e2e-tests, and consistency-checks - # jobs, or else weird things could happen. - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: "~/go/pkg" - key: go-mod-${{ hashFiles('go.sum') }} - restore-keys: | - go-mod- + cache-dependency-path: go.sum - name: "End-to-end tests" run: | @@ -140,7 +116,7 @@ jobs: steps: - name: "Fetch source code" - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 # We need to do comparisons against the main branch. @@ -149,31 +125,24 @@ jobs: uses: ./.github/actions/go-version - name: Install Go toolchain - uses: actions/setup-go@v2 + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 with: go-version: ${{ steps.go.outputs.version }} - - # NOTE: This cache is shared so the following step must always be - # identical across the unit-tests, e2e-tests, and consistency-checks - # jobs, or else weird things could happen. - - name: Cache Go modules - uses: actions/cache@v3 - with: - path: "~/go/pkg" - key: go-mod-${{ hashFiles('go.sum') }} - restore-keys: | - go-mod- + cache-dependency-path: go.sum - name: "go.mod and go.sum consistency check" run: | - go mod tidy - if [[ -n "$(git status --porcelain)" ]]; then - echo >&2 "ERROR: go.mod/go.sum are not up-to-date. Run 'go mod tidy' and then commit the updated files." + make syncdeps + CHANGED="$(git status --porcelain)" + if [[ -n "$CHANGED" ]]; then + git diff + echo >&2 "ERROR: go.mod/go.sum files are not up-to-date. Run 'make syncdeps' and then commit the updated files." + echo >&2 $'Affected files:\n'"$CHANGED" exit 1 fi - name: Cache protobuf tools - uses: actions/cache@v3 + uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: "tools/protobuf-compile/.workdir" key: protobuf-tools-${{ hashFiles('tools/protobuf-compile/protobuf-compile.go') }} @@ -182,7 +151,7 @@ jobs: - name: "Code consistency checks" run: | - make fmtcheck importscheck generate staticcheck exhaustive protobuf + make fmtcheck importscheck vetcheck copyright generate staticcheck exhaustive protobuf if [[ -n "$(git status --porcelain)" ]]; then echo >&2 "ERROR: Generated files are inconsistent. Run 'make generate' and 'make protobuf' locally and then commit the updated files." git >&2 status --porcelain diff --git a/.github/workflows/equivalence-test-diff.yml b/.github/workflows/equivalence-test-diff.yml new file mode 100644 index 0000000000..bcae6e2121 --- /dev/null +++ b/.github/workflows/equivalence-test-diff.yml @@ -0,0 +1,73 @@ +name: equivalence-test-diff + +on: + pull_request: + types: + - opened + - synchronize + - ready_for_review + - reopened + +permissions: + contents: read + pull-requests: write + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + equivalence-test-diff: + name: "Equivalence Test Diff" + runs-on: ubuntu-latest + + steps: + - name: Fetch source code + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Determine Go version + id: go + uses: ./.github/actions/go-version + + - name: Install Go toolchain + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ steps.go.outputs.version }} + cache-dependency-path: go.sum + + - name: Download testing framework + shell: bash + run: | + ./.github/scripts/equivalence-test.sh download_equivalence_test_binary \ + 0.5.0 \ + ./bin/equivalence-tests \ + linux \ + amd64 + + - name: Build terraform + shell: bash + run: ./.github/scripts/equivalence-test.sh build_terraform_binary ./bin/terraform + + - name: Run equivalence tests + id: equivalence-tests + shell: bash {0} # we want to capture the exit code + run: | + ./bin/equivalence-tests diff \ + --tests=testing/equivalence-tests/tests \ + --goldens=testing/equivalence-tests/outputs \ + --binary=$(pwd)/bin/terraform + echo "exit-code=$?" >> "${GITHUB_OUTPUT}" + + - name: Equivalence tests failed + if: steps.equivalence-tests.outputs.exit-code == 1 # 1 is the exit code for failure + shell: bash + run: | + gh pr comment ${{ github.event.pull_request.number }} \ + --body "The equivalence tests failed. Please investigate [here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + exit 1 # fail the job + + - name: Equivalence tests changed + if: steps.equivalence-tests.outputs.exit-code == 2 # 2 is the exit code for changed + shell: bash + run: | + gh pr comment ${{ github.event.pull_request.number }} \ + --body "The equivalence tests will be updated. Please verify the changes [here](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." diff --git a/.github/workflows/equivalence-test-manual-update.yml b/.github/workflows/equivalence-test-manual-update.yml new file mode 100644 index 0000000000..0c4d403951 --- /dev/null +++ b/.github/workflows/equivalence-test-manual-update.yml @@ -0,0 +1,54 @@ +name: equivalence-tests-manual + +on: + workflow_dispatch: + inputs: + target-branch: + type: string + description: "Which branch should be updated?" + required: true + new-branch: + type: string + description: "Name of new branch to be created for the review." + required: true + equivalence-test-version: + type: string + description: 'Equivalence testing framework version to use (no v prefix, eg. 0.5.0).' + default: "0.5.0" + required: true + +permissions: + contents: write + pull-requests: write + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + run-equivalence-tests: + name: "Run equivalence tests" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ inputs.target-branch }} + + - name: Determine Go version + id: go + uses: ./.github/actions/go-version + + - name: Install Go toolchain + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ steps.go.outputs.version }} + cache-dependency-path: go.sum + + - uses: ./.github/actions/equivalence-test + with: + target-equivalence-test-version: ${{ inputs.equivalence-test-version }} + target-os: linux + target-arch: amd64 + current-branch: ${{ inputs.target-branch }} + new-branch: ${{ inputs.new-branch }} + reviewers: ${{ github.actor }} + message: "Update equivalence test golden files." diff --git a/.github/workflows/equivalence-test-update.yml b/.github/workflows/equivalence-test-update.yml new file mode 100644 index 0000000000..2924453792 --- /dev/null +++ b/.github/workflows/equivalence-test-update.yml @@ -0,0 +1,69 @@ +name: equivalence-test-update + +on: + pull_request_target: + types: [ closed ] + +permissions: + contents: write + pull-requests: write + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + check: + name: "Should run equivalence tests?" + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.target_branch.outputs.should_run }} + steps: + - name: target_branch + id: target_branch + run: | + merged='${{ github.event.pull_request.merged }}' + target_branch='${{ github.event.pull_request.base.ref }}' + + targets_release_branch=false + if [ "$target_branch" == "main" ]; then + targets_release_branch=true + elif [ "$target_branch" =~ ^v[0-9]+\.[0-9]+$ ]; then + targets_release_branch=true + fi + + should_run=false + if [ "$merged" == "true" ] && [ "$targets_release_branch" == "true" ]; then + should_run=true + fi + + echo "should_run=$should_run" >> ${GITHUB_OUTPUT} + run-equivalence-tests: + name: "Run equivalence tests" + needs: + - check + if: needs.check.outputs.should_run == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + ref: ${{ inputs.target-branch }} + + - name: Determine Go version + id: go + uses: ./.github/actions/go-version + + - name: Install Go toolchain + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 + with: + go-version: ${{ steps.go.outputs.version }} + cache-dependency-path: go.sum + + - uses: ./.github/actions/equivalence-test + with: + target-equivalence-test-version: 0.5.0 + target-os: linux + target-arch: amd64 + current-branch: ${{ github.event.pull_request.base.ref }} + new-branch: equivalence-testing/${{ github.event.pull_request.head.ref }} + reviewers: ${{ github.event.pull_request.merged_by.login }} + message: "Update equivalence test golden files after ${{ github.event.pull_request.html_url }}." diff --git a/.github/workflows/issue-comment-created.yml b/.github/workflows/issue-comment-created.yml index b8c4d6bfac..c560aea6b7 100644 --- a/.github/workflows/issue-comment-created.yml +++ b/.github/workflows/issue-comment-created.yml @@ -8,7 +8,7 @@ jobs: issue_comment_triage: runs-on: ubuntu-latest steps: - - uses: actions-ecosystem/action-remove-labels@v1 + - uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 with: labels: | stale diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index ed67648c78..35f434dd5a 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -8,16 +8,17 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2 + - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1 with: + process-only: 'issues, prs' github-token: ${{ github.token }} - issue-lock-comment: > + issue-comment: > I'm going to lock this issue because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. - issue-lock-inactive-days: '30' - pr-lock-comment: > + issue-inactive-days: '30' + pr-comment: > I'm going to lock this pull request because it has been closed for _30 days_ ⏳. This helps our maintainers find and focus on the active contributions. If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. - pr-lock-inactive-days: '30' + pr-inactive-days: '30' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 08b438da4c..a3bb89406f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,16 +1,23 @@ --- name: Backport Assistant Runner - + on: pull_request_target: types: - closed - + pull_request: + types: + - labeled + +permissions: + contents: write # to push to a new branch + pull-requests: write # to create a new PR + jobs: backport: - if: github.event.pull_request.merged + if: github.event.pull_request runs-on: ubuntu-latest - container: hashicorpdev/backport-assistant:0.2.1 + container: hashicorpdev/backport-assistant:0.4.7@sha256:36f9d4fba82b9454f1f62bf76c8078fafe3ab0be71356cb96af6d56ac4482cd8 steps: - name: Run Backport Assistant run: | @@ -18,4 +25,5 @@ jobs: env: BACKPORT_LABEL_REGEXP: "(?P\\d+\\.\\d+)-backport" BACKPORT_TARGET_TEMPLATE: "v{{.target}}" - GITHUB_TOKEN: ${{ secrets.ELEVATED_GITHUB_TOKEN }} + BACKPORT_CREATE_DRAFT_ALWAYS: true + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/merged-pr.yml b/.github/workflows/merged-pr.yml deleted file mode 100644 index df1249a811..0000000000 --- a/.github/workflows/merged-pr.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Merged Pull Request -permissions: - pull-requests: write - -# only trigger on pull request closed events -on: - pull_request_target: - types: [ closed ] - -jobs: - merge_job: - # this job will only run if the PR has been merged - if: github.event.pull_request.merged == true - runs-on: ubuntu-latest - steps: - - uses: actions/github-script@v5 - with: - script: | - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: "Reminder for the merging maintainer: if this is a user-visible change, please update the changelog on the appropriate release branch." - }) diff --git a/.gitignore b/.gitignore index cc34a88b19..30fd56c026 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ website/node_modules *.test *.iml +go.work* + /terraform website/vendor @@ -25,3 +27,6 @@ vendor/ # Coverage coverage.txt + +# IDEs +.vscode/ \ No newline at end of file diff --git a/.go-version b/.go-version index 815d5ca06d..e4a973f913 100644 --- a/.go-version +++ b/.go-version @@ -1 +1 @@ -1.19.0 +1.24.2 diff --git a/.release/ci.hcl b/.release/ci.hcl new file mode 100644 index 0000000000..c7a9cc6325 --- /dev/null +++ b/.release/ci.hcl @@ -0,0 +1,97 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +schema = "2" + +project "terraform" { + // the team key is not used by CRT currently + team = "terraform" + slack { + notification_channel = "C011WJ112MD" + } + github { + organization = "hashicorp" + repository = "terraform" + + release_branches = [ + "main", + "release/**", + "v**.**", + ] + } +} + +event "build" { + depends = ["merge"] + action "build" { + organization = "hashicorp" + repository = "terraform" + workflow = "build" + } +} + +// Read more about what the `prepare` workflow does here: +// https://hashicorp.atlassian.net/wiki/spaces/RELENG/pages/2489712686/Dec+7th+2022+-+Introducing+the+new+Prepare+workflow +event "prepare" { + depends = ["build"] + + action "prepare" { + organization = "hashicorp" + repository = "crt-workflows-common" + workflow = "prepare" + depends = ["build"] + } + + notification { + on = "fail" + } +} + +## These are promotion and post-publish events +## they should be added to the end of the file after the verify event stanza. + +event "trigger-staging" { +// This event is dispatched by the bob trigger-promotion command +// and is required - do not delete. +} + +event "promote-staging" { + depends = ["trigger-staging"] + action "promote-staging" { + organization = "hashicorp" + repository = "crt-workflows-common" + workflow = "promote-staging" + config = "release-metadata.hcl" + } + + notification { + on = "always" + } +} + +event "trigger-production" { +// This event is dispatched by the bob trigger-promotion command +// and is required - do not delete. +} + +event "promote-production" { + depends = ["trigger-production"] + action "promote-production" { + organization = "hashicorp" + repository = "crt-workflows-common" + workflow = "promote-production" + } + + promotion-events { + update-ironbank = true + post-promotion { + organization = "hashicorp" + repository = "terraform-releases" + workflow = "crt-hook-tfc-upload" + } + } + + notification { + on = "always" + } +} diff --git a/.release/release-metadata.hcl b/.release/release-metadata.hcl index 5a0f95f1ca..93c81e4ddd 100644 --- a/.release/release-metadata.hcl +++ b/.release/release-metadata.hcl @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + url_docker_registry_dockerhub = "https://hub.docker.com/r/hashicorp/terraform" url_docker_registry_ecr = "https://gallery.ecr.aws/hashicorp/terraform" url_license = "https://github.com/hashicorp/terraform/blob/main/LICENSE" diff --git a/.release/security-scan.hcl b/.release/security-scan.hcl new file mode 100644 index 0000000000..bbee030e7f --- /dev/null +++ b/.release/security-scan.hcl @@ -0,0 +1,16 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +container { + dependencies = false + alpine_secdb = true + secrets = false +} + +binary { + secrets = true + go_modules = true + osv = false + oss_index = true + nvd = false +} \ No newline at end of file diff --git a/BUGPROCESS.md b/BUGPROCESS.md index 2126f4fbf4..faad74d0cf 100644 --- a/BUGPROCESS.md +++ b/BUGPROCESS.md @@ -3,7 +3,7 @@ The Terraform Core team has adopted a more structured bug triage process than we When a bug report is filed, our goal is to either: 1. Get it to a state where it is ready for engineering to fix it in an upcoming Terraform release, or -2. Close it explain why, if we can't help +2. Close it and explain why, if we can't help ## Process diff --git a/BUILDING.md b/BUILDING.md new file mode 100644 index 0000000000..66584de4e2 --- /dev/null +++ b/BUILDING.md @@ -0,0 +1,42 @@ +# Building from Source + +Pre-built binaries are available for download for a variety of supported platforms through the [HashiCorp Releases website](https://releases.hashicorp.com/terraform/). + +However, if you'd like to build Terraform yourself, you can do so using the Go build toolchain and the options specified in this document. + +## Prerequisites + +1. Ensure you've installed the Go language version specified in [`.go-version`](https://github.com/hashicorp/terraform/blob/main/.go-version). +2. Clone this repository to a location of your choice. + +## Terraform Build Options + +Terraform accepts certain options passed using `ldflags` at build time which control the behavior of the resulting binary. + +### Dev Version Reporting + +Terraform will include a `-dev` flag when reporting its own version (ex: 1.5.0-dev) unless `version.dev` is set to `no`: + +``` +go build -ldflags "-w -s -X 'github.com/hashicorp/terraform/version.dev=no'" -o bin/ . +``` + +### Experimental Features + +Experimental features of Terraform will be disabled unless `main.experimentsAllowed` is set to `yes`: + +``` +go build -ldflags "-w -s -X 'main.experimentsAllowed=yes'" -o bin/ . +``` + +In the official build process for Terraform, experiments are only allowed in alpha release builds. We recommend that third-party distributors follow that convention in order to reduce user confusion. + +## Go Options + +For the most part, the Terraform release process relies on the Go toolchain defaults for the target operating system and processor architecture. + +### `CGO_ENABLED` + +One exception is the `CGO_ENABLED` option, which is set explicitly when building Terraform binaries. For most platforms, we build with `CGO_ENABLED=0` in order to produce a statically linked binary. For MacOS/Darwin operating systems, we build with `CGO_ENABLED=1` to avoid a platform-specific issue with DNS resolution. + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 506909d474..c815d0a383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,82 +1,32 @@ -## 1.3.0 (Unreleased) +## 1.13.0 (Unreleased) -NEW FEATURES: - -* **Optional attributes for object type constraints:** When declaring an input variable whose type constraint includes an object type, you can now declare individual attributes as optional, and specify a default value to use if the caller doesn't set it. For example: - - ```terraform - variable "with_optional_attribute" { - type = object({ - a = string # a required attribute - b = optional(string) # an optional attribute - c = optional(number, 127) # an optional attribute with a default value - }) - } - ``` - - Assigning `{ a = "foo" }` to this variable will result in the value `{ a = "foo", b = null, c = 127 }`. - -* Added functions: `startswith` and `endswith` allow you to check whether a given string has a specified prefix or suffix. ([#31220](https://github.com/hashicorp/terraform/issues/31220)) - -UPGRADE NOTES: - -* `terraform show -json`: Output changes now include more detail about the unknown-ness of the planned value. Previously, a planned output would be marked as either fully known or partially unknown, with the `after_unknown` field having value `false` or `true` respectively. Now outputs correctly expose the full structure of unknownness for complex values, allowing consumers of the JSON output format to determine which values in a collection are known only after apply. -* `terraform import`: The `-allow-missing-config` has been removed, and at least an empty configuration block must exist to import a resource. -* Consumers of the JSON output format expecting on the `after_unknown` field to be only `false` or `true` should be updated to support [the change representation](https://www.terraform.io/internals/json-format#change-representation) described in the documentation, and as was already used for resource changes. ([#31235](https://github.com/hashicorp/terraform/issues/31235)) -* AzureRM Backend: This release concludes [the deprecation cycle started in Terraform v1.1](https://www.terraform.io/language/upgrade-guides/1-1#preparation-for-removing-azure-ad-graph-support-in-the-azurerm-backend) for the `azurerm` backend's support of "ADAL" authentication. This backend now supports only "MSAL" (Microsoft Graph) authentication. - - This follows from [Microsoft's own deprecation of Azure AD Graph](https://docs.microsoft.com/en-us/graph/migrate-azure-ad-graph-faq), and so you must follow the migration instructions presented in that Azure documentation to adopt Microsoft Graph and then change your backend configuration to use MSAL authentication before upgrading to Terraform v1.3. -* When making requests to HTTPS servers, Terraform will now reject invalid handshakes that have duplicate extensions, as required by RFC 5246 section 7.4.1.4 and RFC 8446 section 4.2. This may cause new errors when interacting with existing buggy or misconfigured TLS servers, but should not affect correct servers. - - This only applies to requests made directly by Terraform CLI, such as provider installation and remote state storage. Terraform providers are separate programs which decide their own policy for handling of TLS handshakes. - -ENHANCEMENTS: - -* config: Optional attributes for object type constraints, as described under new features above. ([#31154](https://github.com/hashicorp/terraform/issues/31154)) -* config: New built-in function `timecmp` allows determining the ordering relationship between two timestamps while taking potentially-different UTC offsets into account. ([#31154](https://github.com/hashicorp/terraform/issues/31154)) -* `terraform fmt` now accepts multiple target paths, allowing formatting of several individual files at once. ([#31687](https://github.com/hashicorp/terraform/issues/31687)) -* When reporting an error message related to a function call, Terraform will now include contextual information about the signature of the function that was being called, as an aid to understanding why the call might have failed. ([#31299](https://github.com/hashicorp/terraform/issues/31299)) -* When reporting an error or warning message that isn't caused by values being unknown or marked as sensitive, Terraform will no longer mention any values having those characteristics in the contextual information presented alongside the error. Terraform will still return this information for the small subset of error messages that are specifically about unknown values or sensitive values being invalid in certain contexts. ([#31299](https://github.com/hashicorp/terraform/issues/31299)) -* The Terraform CLI now calls `PlanResourceChange` for compatible providers when destroying resource instances. ([#31179](https://github.com/hashicorp/terraform/issues/31179)) -* The AzureRM Backend now only supports MSAL (and Microsoft Graph) and no longer makes use of ADAL (and Azure Active Directory Graph) for authentication ([#31070](https://github.com/hashicorp/terraform/issues/31070)) -* The COS backend now supports global acceleration. ([#31425](https://github.com/hashicorp/terraform/issues/31425)) -* providercache: include host in provider installation error ([#31524](https://github.com/hashicorp/terraform/issues/31524)) -* refactoring: `moved` blocks can now be used to move resources to and from external modules ([#31556](https://github.com/hashicorp/terraform/issues/31556)) -* When showing the progress of a remote operation running in Terraform Cloud, Terraform CLI will include information about pre-plan run tasks ([#31617](https://github.com/hashicorp/terraform/issues/31617)) - -BUG FIXES: - -* config: Terraform was not previously evaluating preconditions and postconditions during the apply phase for resource instances that didn't have any changes pending, which was incorrect because the outcome of a condition can potentially be affected by changes to _other_ objects in the configuration. Terraform will now always check the conditions for every resource instance included in a plan during the apply phase, even for resource instances that have "no-op" changes. This means that some failures that would previously have been detected only by a subsequent run will now be detected during the same run that caused them, thereby giving the feedback at the appropriate time. ([#31491](https://github.com/hashicorp/terraform/issues/31491)) -* `terraform show -json`: Fixed missing unknown markers in the encoding of partially unknown tuples and sets. ([#31236](https://github.com/hashicorp/terraform/issues/31236)) -* `terraform output` CLI help documentation is now more consistent with web-based documentation. ([#29354](https://github.com/hashicorp/terraform/issues/29354)) -* getproviders: account for occasionally missing Host header in errors ([#31542](https://github.com/hashicorp/terraform/issues/31542)) -* core: Do not create "delete" changes for nonexistent outputs ([#31471](https://github.com/hashicorp/terraform/issues/31471)) -* configload: validate implied provider names in submodules to avoid crash ([#31573](https://github.com/hashicorp/terraform/issues/31573)) -* core: `import` fails when resources or modules are expanded with for each, or input from data sources is required ([#31283](https://github.com/hashicorp/terraform/issues/31283)) EXPERIMENTS: -* This release concludes the `module_variable_optional_attrs` experiment, which started in Terraform v0.14.0. The final design of the optional attributes feature is similar to the experimental form in the previous releases, but with two major differences: - * The `optional` function-like modifier for declaring an optional attribute now accepts an optional second argument for specifying a default value to use when the attribute isn't set by the caller. If not specified, the default value is a null value of the appropriate type as before. - * The built-in `defaults` function, previously used to meet the use-case of replacing null values with default values, will not graduate to stable and has been removed. Use the second argument of `optional` inline in your type constraint to declare default values instead. +Experiments are only enabled in alpha releases of Terraform CLI. The following features are not yet available in stable releases. - If you have any experimental modules that were participating in this experiment, you will need to remove the experiment opt-in and adopt the new syntax for declaring default values in order to migrate your existing module to the stablized version of this feature. If you are writing a shared module for others to use, we recommend declaring that your module requires Terraform v1.3.0 or later to give specific feedback when using the new feature on older Terraform versions, in place of the previous declaration to use the experimental form of this feature: - - ```hcl - terraform { - required_version = ">= 1.3.0" - } - ``` +- The new command `terraform rpcapi` exposes some Terraform Core functionality through an RPC interface compatible with [`go-plugin`](https://github.com/hashicorp/go-plugin). The exact RPC API exposed here is currently subject to change at any time, because it's here primarily as a vehicle to support the [Terraform Stacks](https://www.hashicorp.com/blog/terraform-stacks-explained) private preview and so will be broken if necessary to respond to feedback from private preview participants, or possibly for other reasons. Do not use this mechanism yet outside of Terraform Stacks private preview. +- The experimental "deferred actions" feature, enabled by passing the `-allow-deferral` option to `terraform plan`, permits `count` and `for_each` arguments in `module`, `resource`, and `data` blocks to have unknown values and allows providers to react more flexibly to unknown values. This experiment is under active development, and so it's not yet useful to participate in this experiment ## Previous Releases -For information on prior major and minor releases, see their changelogs: +For information on prior major and minor releases, refer to their changelogs: -* [v1.2](https://github.com/hashicorp/terraform/blob/v1.2/CHANGELOG.md) -* [v1.1](https://github.com/hashicorp/terraform/blob/v1.1/CHANGELOG.md) -* [v1.0](https://github.com/hashicorp/terraform/blob/v1.0/CHANGELOG.md) -* [v0.15](https://github.com/hashicorp/terraform/blob/v0.15/CHANGELOG.md) -* [v0.14](https://github.com/hashicorp/terraform/blob/v0.14/CHANGELOG.md) -* [v0.13](https://github.com/hashicorp/terraform/blob/v0.13/CHANGELOG.md) -* [v0.12](https://github.com/hashicorp/terraform/blob/v0.12/CHANGELOG.md) -* [v0.11 and earlier](https://github.com/hashicorp/terraform/blob/v0.11/CHANGELOG.md) +- [v1.12](https://github.com/hashicorp/terraform/blob/v1.12/CHANGELOG.md) +- [v1.11](https://github.com/hashicorp/terraform/blob/v1.11/CHANGELOG.md) +- [v1.10](https://github.com/hashicorp/terraform/blob/v1.10/CHANGELOG.md) +- [v1.9](https://github.com/hashicorp/terraform/blob/v1.9/CHANGELOG.md) +- [v1.8](https://github.com/hashicorp/terraform/blob/v1.8/CHANGELOG.md) +- [v1.7](https://github.com/hashicorp/terraform/blob/v1.7/CHANGELOG.md) +- [v1.6](https://github.com/hashicorp/terraform/blob/v1.6/CHANGELOG.md) +- [v1.5](https://github.com/hashicorp/terraform/blob/v1.5/CHANGELOG.md) +- [v1.4](https://github.com/hashicorp/terraform/blob/v1.4/CHANGELOG.md) +- [v1.3](https://github.com/hashicorp/terraform/blob/v1.3/CHANGELOG.md) +- [v1.2](https://github.com/hashicorp/terraform/blob/v1.2/CHANGELOG.md) +- [v1.1](https://github.com/hashicorp/terraform/blob/v1.1/CHANGELOG.md) +- [v1.0](https://github.com/hashicorp/terraform/blob/v1.0/CHANGELOG.md) +- [v0.15](https://github.com/hashicorp/terraform/blob/v0.15/CHANGELOG.md) +- [v0.14](https://github.com/hashicorp/terraform/blob/v0.14/CHANGELOG.md) +- [v0.13](https://github.com/hashicorp/terraform/blob/v0.13/CHANGELOG.md) +- [v0.12](https://github.com/hashicorp/terraform/blob/v0.12/CHANGELOG.md) +- [v0.11 and earlier](https://github.com/hashicorp/terraform/blob/v0.11/CHANGELOG.md) diff --git a/CODEOWNERS b/CODEOWNERS index bb6272e619..7d1c1fb167 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,27 +1,56 @@ # Each line is a file pattern followed by one or more owners. # More on CODEOWNERS files: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners -# Remote-state backend # Maintainer -/internal/backend/remote-state/artifactory Unmaintained -/internal/backend/remote-state/azure @hashicorp/terraform-azure -/internal/backend/remote-state/consul @hashicorp/consul @remilapeyre -/internal/backend/remote-state/cos @likexian -/internal/backend/remote-state/etcdv2 Unmaintained -/internal/backend/remote-state/etcdv3 Unmaintained -/internal/backend/remote-state/gcs @hashicorp/terraform-google +# The rules are evaluated in order, if a file matches multiple patterns, the last match "wins". +# We want to have a default rule for all files +* @hashicorp/terraform-core + +# Entries that are commented out have maintainers that are not in the +# HashiCorp organization and so cannot be automatically added as reviewers. +# +# We retain those as documentation of who agreed to maintain, but they +# cannot be used automatically by GitHub's pull request workflow and would +# make GitHub consider this file invalid if not commented. + +# Remote-state backend # Maintainer +/internal/backend/remote-state/azure @hashicorp/terraform-core @hashicorp/terraform-azure +#/internal/backend/remote-state/consul Unmaintained +#/internal/backend/remote-state/cos @likexian +/internal/backend/remote-state/gcs @hashicorp/terraform-core @hashicorp/tf-eco-hybrid-cloud /internal/backend/remote-state/http @hashicorp/terraform-core -/internal/backend/remote-state/manta Unmaintained -/internal/backend/remote-state/oss @xiaozhu36 -/internal/backend/remote-state/pg @remilapeyre -/internal/backend/remote-state/s3 @hashicorp/terraform-aws -/internal/backend/remote-state/swift Unmaintained -/internal/backend/remote-state/kubernetes @jrhouston @alexsomesan +#/internal/backend/remote-state/oss @xiaozhu36 +#/internal/backend/remote-state/pg @remilapeyre +/internal/backend/remote-state/s3 @hashicorp/terraform-core @hashicorp/terraform-aws +/internal/backend/remote-state/kubernetes @hashicorp/terraform-core @hashicorp/tf-eco-hybrid-cloud +#/internal/backend/remote-state/oci @ravinitp @pvkrishnachaitanya + +# Cloud backend +/internal/backend/remote @hashicorp/terraform-core @hashicorp/tf-core-cloud +/internal/cloud @hashicorp/terraform-core @hashicorp/tf-core-cloud # Provisioners -builtin/provisioners/chef Deprecated builtin/provisioners/file @hashicorp/terraform-core -builtin/provisioners/habitat Deprecated builtin/provisioners/local-exec @hashicorp/terraform-core -builtin/provisioners/puppet Deprecated builtin/provisioners/remote-exec @hashicorp/terraform-core -builtin/provisioners/salt-masterless Deprecated + +# engineering and web presence get notified of, and can approve changes to web tooling, but not content at developer.hashicorp.com/terraform/docs + +/website/ @hashicorp/web-presence @hashicorp/terraform-core +/website/data/ +/website/public/ +/website/content/ + +# education and engineering get notified of, and can approve changes to web content at developer.hashicorp.com/terraform/docs + +/website/data/ @hashicorp/terraform-education @hashicorp/terraform-core +/website/docs/ @hashicorp/terraform-education @hashicorp/terraform-core +/website/img/ @hashicorp/terraform-education @hashicorp/terraform-core +/website/README.md @hashicorp/terraform-education @hashicorp/terraform-core +/website/public/ @hashicorp/terraform-education @hashicorp/terraform-core +/website/content/ @hashicorp/terraform-education @hashicorp/terraform-core + +# Backend maintainers also get co-ownership of their backend docs pages +/website/docs/language/backend/azurerm.mdx @hashicorp/terraform-education @hashicorp/terraform-core @hashicorp/terraform-azure +/website/docs/language/backend/gcs.mdx @hashicorp/terraform-education @hashicorp/terraform-core @hashicorp/tf-eco-hybrid-cloud +/website/docs/language/backend/kubernetes.mdx @hashicorp/terraform-education @hashicorp/terraform-core @hashicorp/tf-eco-hybrid-cloud +/website/docs/language/backend/s3.mdx @hashicorp/terraform-education @hashicorp/terraform-core @hashicorp/terraform-aws diff --git a/Dockerfile b/Dockerfile index 1e1bb97603..b5bd033dc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + # This Dockerfile builds on golang:alpine by building Terraform from source # using the current working directory. # @@ -10,7 +13,7 @@ FROM docker.mirror.hashicorp.services/golang:alpine LABEL maintainer="HashiCorp Terraform Team " -RUN apk add --no-cache git bash openssh +RUN apk add --no-cache git bash openssh ca-certificates ENV TF_DEV=true ENV TF_RELEASE=1 diff --git a/LICENSE b/LICENSE index c33dcc7c92..8142708df2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,354 +1,92 @@ -Mozilla Public License, version 2.0 - -1. Definitions - -1.1. “Contributor” - - means each individual or legal entity that creates, contributes to the - creation of, or owns Covered Software. - -1.2. “Contributor Version” - - means the combination of the Contributions of others (if any) used by a - Contributor and that particular Contributor’s Contribution. - -1.3. “Contribution” - - means Covered Software of a particular Contributor. - -1.4. “Covered Software” - - means Source Code Form to which the initial Contributor has attached the - notice in Exhibit A, the Executable Form of such Source Code Form, and - Modifications of such Source Code Form, in each case including portions - thereof. - -1.5. “Incompatible With Secondary Licenses” - means - - a. that the initial Contributor has attached the notice described in - Exhibit B to the Covered Software; or - - b. that the Covered Software was made available under the terms of version - 1.1 or earlier of the License, but not also under the terms of a - Secondary License. - -1.6. “Executable Form” - - means any form of the work other than Source Code Form. - -1.7. “Larger Work” - - means a work that combines Covered Software with other material, in a separate - file or files, that is not Covered Software. - -1.8. “License” - - means this document. - -1.9. “Licensable” - - means having the right to grant, to the maximum extent possible, whether at the - time of the initial grant or subsequently, any and all of the rights conveyed by - this License. - -1.10. “Modifications” - - means any of the following: - - a. any file in Source Code Form that results from an addition to, deletion - from, or modification of the contents of Covered Software; or - - b. any new file in Source Code Form that contains any Covered Software. - -1.11. “Patent Claims” of a Contributor - - means any patent claim(s), including without limitation, method, process, - and apparatus claims, in any patent Licensable by such Contributor that - would be infringed, but for the grant of the License, by the making, - using, selling, offering for sale, having made, import, or transfer of - either its Contributions or its Contributor Version. - -1.12. “Secondary License” - - means either the GNU General Public License, Version 2.0, the GNU Lesser - General Public License, Version 2.1, the GNU Affero General Public - License, Version 3.0, or any later versions of those licenses. - -1.13. “Source Code Form” - - means the form of the work preferred for making modifications. - -1.14. “You” (or “Your”) - - means an individual or a legal entity exercising rights under this - License. For legal entities, “You” includes any entity that controls, is - controlled by, or is under common control with You. For purposes of this - definition, “control” means (a) the power, direct or indirect, to cause - the direction or management of such entity, whether by contract or - otherwise, or (b) ownership of more than fifty percent (50%) of the - outstanding shares or beneficial ownership of such entity. - - -2. License Grants and Conditions - -2.1. Grants - - Each Contributor hereby grants You a world-wide, royalty-free, - non-exclusive license: - - a. under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or as - part of a Larger Work; and - - b. under Patent Claims of such Contributor to make, use, sell, offer for - sale, have made, import, and otherwise transfer either its Contributions - or its Contributor Version. - -2.2. Effective Date - - The licenses granted in Section 2.1 with respect to any Contribution become - effective for each Contribution on the date the Contributor first distributes - such Contribution. - -2.3. Limitations on Grant Scope - - The licenses granted in this Section 2 are the only rights granted under this - License. No additional rights or licenses will be implied from the distribution - or licensing of Covered Software under this License. Notwithstanding Section - 2.1(b) above, no patent license is granted by a Contributor: - - a. for any code that a Contributor has removed from Covered Software; or - - b. for infringements caused by: (i) Your and any other third party’s - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - - c. under Patent Claims infringed by Covered Software in the absence of its - Contributions. - - This License does not grant any rights in the trademarks, service marks, or - logos of any Contributor (except as may be necessary to comply with the - notice requirements in Section 3.4). - -2.4. Subsequent Licenses - - No Contributor makes additional grants as a result of Your choice to - distribute the Covered Software under a subsequent version of this License - (see Section 10.2) or under the terms of a Secondary License (if permitted - under the terms of Section 3.3). - -2.5. Representation - - Each Contributor represents that the Contributor believes its Contributions - are its original creation(s) or it has sufficient rights to grant the - rights to its Contributions conveyed by this License. - -2.6. Fair Use - - This License is not intended to limit any rights You have under applicable - copyright doctrines of fair use, fair dealing, or other equivalents. - -2.7. Conditions - - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in - Section 2.1. - - -3. Responsibilities - -3.1. Distribution of Source Form - - All distribution of Covered Software in Source Code Form, including any - Modifications that You create or to which You contribute, must be under the - terms of this License. You must inform recipients that the Source Code Form - of the Covered Software is governed by the terms of this License, and how - they can obtain a copy of this License. You may not attempt to alter or - restrict the recipients’ rights in the Source Code Form. - -3.2. Distribution of Executable Form - - If You distribute Covered Software in Executable Form then: - - a. such Covered Software must also be made available in Source Code Form, - as described in Section 3.1, and You must inform recipients of the - Executable Form how they can obtain a copy of such Source Code Form by - reasonable means in a timely manner, at a charge no more than the cost - of distribution to the recipient; and - - b. You may distribute such Executable Form under the terms of this License, - or sublicense it under different terms, provided that the license for - the Executable Form does not attempt to limit or alter the recipients’ - rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - - You may create and distribute a Larger Work under terms of Your choice, - provided that You also comply with the requirements of this License for the - Covered Software. If the Larger Work is a combination of Covered Software - with a work governed by one or more Secondary Licenses, and the Covered - Software is not Incompatible With Secondary Licenses, this License permits - You to additionally distribute such Covered Software under the terms of - such Secondary License(s), so that the recipient of the Larger Work may, at - their option, further distribute the Covered Software under the terms of - either this License or such Secondary License(s). - -3.4. Notices - - You may not remove or alter the substance of any license notices (including - copyright notices, patent notices, disclaimers of warranty, or limitations - of liability) contained within the Source Code Form of the Covered - Software, except that You may alter any license notices to the extent - required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - - You may choose to offer, and to charge a fee for, warranty, support, - indemnity or liability obligations to one or more recipients of Covered - Software. However, You may do so only on Your own behalf, and not on behalf - of any Contributor. You must make it absolutely clear that any such - warranty, support, indemnity, or liability obligation is offered by You - alone, and You hereby agree to indemnify every Contributor for any - liability incurred by such Contributor as a result of warranty, support, - indemnity or liability terms You offer. You may include additional - disclaimers of warranty and limitations of liability specific to any - jurisdiction. - -4. Inability to Comply Due to Statute or Regulation - - If it is impossible for You to comply with any of the terms of this License - with respect to some or all of the Covered Software due to statute, judicial - order, or regulation then You must: (a) comply with the terms of this License - to the maximum extent possible; and (b) describe the limitations and the code - they affect. Such description must be placed in a text file included with all - distributions of the Covered Software under this License. Except to the - extent prohibited by statute or regulation, such description must be - sufficiently detailed for a recipient of ordinary skill to be able to - understand it. - -5. Termination - -5.1. The rights granted under this License will terminate automatically if You - fail to comply with any of its terms. However, if You become compliant, - then the rights granted under this License from a particular Contributor - are reinstated (a) provisionally, unless and until such Contributor - explicitly and finally terminates Your grants, and (b) on an ongoing basis, - if such Contributor fails to notify You of the non-compliance by some - reasonable means prior to 60 days after You have come back into compliance. - Moreover, Your grants from a particular Contributor are reinstated on an - ongoing basis if such Contributor notifies You of the non-compliance by - some reasonable means, this is the first time You have received notice of - non-compliance with this License from such Contributor, and You become - compliant prior to 30 days after Your receipt of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent - infringement claim (excluding declaratory judgment actions, counter-claims, - and cross-claims) alleging that a Contributor Version directly or - indirectly infringes any patent, then the rights granted to You by any and - all Contributors for the Covered Software under Section 2.1 of this License - shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user - license agreements (excluding distributors and resellers) which have been - validly granted by You or Your distributors under this License prior to - termination shall survive termination. - -6. Disclaimer of Warranty - - Covered Software is provided under this License on an “as is” basis, without - warranty of any kind, either expressed, implied, or statutory, including, - without limitation, warranties that the Covered Software is free of defects, - merchantable, fit for a particular purpose or non-infringing. The entire - risk as to the quality and performance of the Covered Software is with You. - Should any Covered Software prove defective in any respect, You (not any - Contributor) assume the cost of any necessary servicing, repair, or - correction. This disclaimer of warranty constitutes an essential part of this - License. No use of any Covered Software is authorized under this License - except under this disclaimer. - -7. Limitation of Liability - - Under no circumstances and under no legal theory, whether tort (including - negligence), contract, or otherwise, shall any Contributor, or anyone who - distributes Covered Software as permitted above, be liable to You for any - direct, indirect, special, incidental, or consequential damages of any - character including, without limitation, damages for lost profits, loss of - goodwill, work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses, even if such party shall have been - informed of the possibility of such damages. This limitation of liability - shall not apply to liability for death or personal injury resulting from such - party’s negligence to the extent applicable law prohibits such limitation. - Some jurisdictions do not allow the exclusion or limitation of incidental or - consequential damages, so this exclusion and limitation may not apply to You. - -8. Litigation - - Any litigation relating to this License may be brought only in the courts of - a jurisdiction where the defendant maintains its principal place of business - and such litigation shall be governed by laws of that jurisdiction, without - reference to its conflict-of-law provisions. Nothing in this Section shall - prevent a party’s ability to bring cross-claims or counter-claims. - -9. Miscellaneous - - This License represents the complete agreement concerning the subject matter - hereof. If any provision of this License is held to be unenforceable, such - provision shall be reformed only to the extent necessary to make it - enforceable. Any law or regulation which provides that the language of a - contract shall be construed against the drafter shall not be used to construe - this License against a Contributor. - - -10. Versions of the License - -10.1. New Versions - - Mozilla Foundation is the license steward. Except as provided in Section - 10.3, no one other than the license steward has the right to modify or - publish new versions of this License. Each version will be given a - distinguishing version number. - -10.2. Effect of New Versions - - You may distribute the Covered Software under the terms of the version of - the License under which You originally received the Covered Software, or - under the terms of any subsequent version published by the license - steward. - -10.3. Modified Versions - - If you create software not governed by this License, and you want to - create a new license for such software, you may create and use a modified - version of this License if you rename the license and remove any - references to the name of the license steward (except to note that such - modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses - If You choose to distribute Source Code Form that is Incompatible With - Secondary Licenses under the terms of this version of the License, the - notice described in Exhibit B of this License must be attached. - -Exhibit A - Source Code Form License Notice - - This Source Code Form is subject to the - terms of the Mozilla Public License, v. - 2.0. If a copy of the MPL was not - distributed with this file, You can - obtain one at - http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular file, then -You may include the notice in a location (such as a LICENSE file in a relevant -directory) where a recipient would be likely to look for such a notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - “Incompatible With Secondary Licenses” Notice - - This Source Code Form is “Incompatible - With Secondary Licenses”, as defined by - the Mozilla Public License, v. 2.0. - +License text copyright (c) 2020 MariaDB Corporation Ab, All Rights Reserved. +"Business Source License" is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: HashiCorp, Inc. +Licensed Work: Terraform Version 1.6.0 or later. The Licensed Work is (c) 2024 + HashiCorp, Inc. +Additional Use Grant: You may make production use of the Licensed Work, provided + Your use does not include offering the Licensed Work to third + parties on a hosted or embedded basis in order to compete with + HashiCorp's paid version(s) of the Licensed Work. For purposes + of this license: + + A "competitive offering" is a Product that is offered to third + parties on a paid basis, including through paid support + arrangements, that significantly overlaps with the capabilities + of HashiCorp's paid version(s) of the Licensed Work. If Your + Product is not a competitive offering when You first make it + generally available, it will not become a competitive offering + later due to HashiCorp releasing a new version of the Licensed + Work with additional capabilities. In addition, Products that + are not provided on a paid basis are not competitive. + + "Product" means software that is offered to end users to manage + in their own environments or offered as a service on a hosted + basis. + + "Embedded" means including the source code or executable code + from the Licensed Work in a competitive offering. "Embedded" + also means packaging the competitive offering in such a way + that the Licensed Work must be accessed or downloaded for the + competitive offering to operate. + + Hosting or using the Licensed Work(s) for internal purposes + within an organization is not considered a competitive + offering. HashiCorp considers your organization to include all + of your affiliates under common control. + + For binding interpretive guidance on using HashiCorp products + under the Business Source License, please visit our FAQ. + (https://www.hashicorp.com/license-faq) +Change Date: Four years from the date the Licensed Work is published. +Change License: MPL 2.0 + +For information about alternative licensing arrangements for the Licensed Work, +please contact licensing@hashicorp.com. + +Notice + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. diff --git a/Makefile b/Makefile index 6592e46e92..3253d8f23a 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,3 @@ -WEBSITE_REPO=github.com/hashicorp/terraform-website -VERSION?="0.3.44" -PWD=$$(pwd) -DOCKER_IMAGE="hashicorp/terraform-website:full" -DOCKER_IMAGE_LOCAL="hashicorp-terraform-website-local" -DOCKER_RUN_FLAGS=--interactive \ - --rm \ - --tty \ - --workdir "/website" \ - --volume "$(shell pwd):/website/ext/terraform" \ - --volume "$(shell pwd)/website:/website/preview" \ - --publish "3000:3000" \ - -e "IS_CONTENT_PREVIEW=true" \ - -e "PREVIEW_FROM_REPO=terraform" \ - -e "NAV_DATA_DIRNAME=./preview/data" \ - -e "CONTENT_DIRNAME=./preview/docs" \ - -e "CURRENT_GIT_BRANCH=$$(git rev-parse --abbrev-ref HEAD)" - # generate runs `go generate` to build the dynamically generated # source files, except the protobuf stubs which are built instead with # "make protobuf". @@ -38,32 +20,40 @@ fmtcheck: importscheck: "$(CURDIR)/scripts/goimportscheck.sh" +vetcheck: + @echo "==> Checking that the code complies with go vet requirements" + @go vet ./... + staticcheck: "$(CURDIR)/scripts/staticcheck.sh" exhaustive: "$(CURDIR)/scripts/exhaustive.sh" -# Default: run this if working on the website locally to run in watch mode. +copyright: + "$(CURDIR)/scripts/copyright.sh" --plan + +copyrightfix: + "$(CURDIR)/scripts/copyright.sh" + +syncdeps: + "$(CURDIR)/scripts/syncdeps.sh" + +# Run this if working on the website locally to run in watch mode. website: - @echo "==> Downloading latest Docker image..." - @docker pull ${DOCKER_IMAGE} - @echo "==> Starting website in Docker..." - @docker run ${DOCKER_RUN_FLAGS} ${DOCKER_IMAGE} npm start + $(MAKE) -C website website +# Use this if you have run `website/build-local` to use the locally built image. website/local: - @echo "==> Starting website in Docker..." - @docker run ${DOCKER_RUN_FLAGS} ${DOCKER_IMAGE_LOCAL} npm start + $(MAKE) -C website website/local -.PHONY: website/build-local +# Run this to generate a new local Docker image. website/build-local: - @echo "==> Building local Docker image" - @docker build https://github.com/hashicorp/terraform-website.git\#master \ - -t $(DOCKER_IMAGE_LOCAL) + $(MAKE) -C website website/build-local # disallow any parallelism (-j) for Make. This is necessary since some # commands during the build process create temporary files that collide # under parallel conditions. .NOTPARALLEL: -.PHONY: fmtcheck importscheck generate protobuf website website-test staticcheck website/local website/build-local +.PHONY: fmtcheck importscheck vetcheck generate protobuf staticcheck syncdeps website website/local website/build-local diff --git a/README.md b/README.md index e8509e3760..ff9e27d11d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # Terraform -- Website: https://www.terraform.io +- Website: https://developer.hashicorp.com/terraform - Forums: [HashiCorp Discuss](https://discuss.hashicorp.com/c/terraform-core) -- Documentation: [https://www.terraform.io/docs/](https://www.terraform.io/docs/) -- Tutorials: [HashiCorp's Learn Platform](https://learn.hashicorp.com/terraform) +- Documentation: [https://developer.hashicorp.com/terraform/docs](https://developer.hashicorp.com/terraform/docs) +- Tutorials: [HashiCorp's Learn Platform](https://developer.hashicorp.com/terraform/tutorials) - Certification Exam: [HashiCorp Certified: Terraform Associate](https://www.hashicorp.com/certification/#hashicorp-certified-terraform-associate) -Terraform +Terraform Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions. @@ -24,10 +24,10 @@ For more information, refer to the [What is Terraform?](https://www.terraform.io ## Getting Started & Documentation -Documentation is available on the [Terraform website](https://www.terraform.io): +Documentation is available on the [Terraform website](https://developer.hashicorp.com/terraform): -- [Introduction](https://www.terraform.io/intro) -- [Documentation](https://www.terraform.io/docs) +- [Introduction](https://developer.hashicorp.com/terraform/intro) +- [Documentation](https://developer.hashicorp.com/terraform/docs) If you're new to Terraform and want to get started creating infrastructure, please check out our [Getting Started guides](https://learn.hashicorp.com/terraform#getting-started) on HashiCorp's learning platform. There are also [additional guides](https://learn.hashicorp.com/terraform#operations-and-development) to continue your learning. @@ -35,7 +35,7 @@ Show off your Terraform knowledge by passing a certification exam. Visit the [ce ## Developing Terraform -This repository contains only Terraform core, which includes the command line interface and the main graph engine. Providers are implemented as plugins, and Terraform can automatically download providers that are published on [the Terraform Registry](https://registry.terraform.io). HashiCorp develops some providers, and others are developed by other organizations. For more information, see [Extending Terraform](https://www.terraform.io/docs/extend/index.html). +This repository contains only Terraform core, which includes the command line interface and the main graph engine. Providers are implemented as plugins, and Terraform can automatically download providers that are published on [the Terraform Registry](https://registry.terraform.io). HashiCorp develops some providers, and others are developed by other organizations. For more information, refer to [Plugin development](https://developer.hashicorp.com/terraform/plugin). - To learn more about compiling Terraform and contributing suggested changes, refer to [the contributing guide](.github/CONTRIBUTING.md). @@ -45,4 +45,4 @@ This repository contains only Terraform core, which includes the command line in ## License -[Mozilla Public License v2.0](https://github.com/hashicorp/terraform/blob/main/LICENSE) +[Business Source License 1.1](https://github.com/hashicorp/terraform/blob/main/LICENSE) diff --git a/.github/workflows/build-Dockerfile b/build.Dockerfile similarity index 61% rename from .github/workflows/build-Dockerfile rename to build.Dockerfile index c0ea5b89d3..2a4db85828 100644 --- a/.github/workflows/build-Dockerfile +++ b/build.Dockerfile @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + # This Dockerfile is not intended for general use, but is rather used to # produce our "light" release packages as part of our official release # pipeline. @@ -29,7 +32,22 @@ LABEL version=$PRODUCT_VERSION # Historical Terraform-specific label preserved for backward compatibility. LABEL "com.hashicorp.terraform.version"="${PRODUCT_VERSION}" -RUN apk add --no-cache git openssh +# @see https://specs.opencontainers.org/image-spec/annotations/?v=v1.0.1#pre-defined-annotation-keys +LABEL org.opencontainers.image.title=${BIN_NAME} \ + org.opencontainers.image.description="Terraform enables you to safely and predictably create, change, and improve infrastructure" \ + org.opencontainers.image.authors="HashiCorp Terraform Team " \ + org.opencontainers.image.url="https://www.terraform.io/" \ + org.opencontainers.image.documentation="https://www.terraform.io/docs" \ + org.opencontainers.image.source="https://github.com/hashicorp/terraform" \ + org.opencontainers.image.version=${PRODUCT_VERSION} \ + org.opencontainers.image.revision=${PRODUCT_REVISION} \ + org.opencontainers.image.vendor="HashiCorp" \ + org.opencontainers.image.licenses="BUSL-1.1" + +RUN apk add --no-cache git openssh ca-certificates + +# Copy the license file as per Legal requirement +COPY LICENSE "/usr/share/doc/${BIN_NAME}/LICENSE.txt" # The hashicorp/actions-docker-build GitHub Action extracts the appropriate # release package for our target architecture into the current working diff --git a/catalog-info.yaml b/catalog-info.yaml new file mode 100644 index 0000000000..006570bac5 --- /dev/null +++ b/catalog-info.yaml @@ -0,0 +1,17 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 +# +# Intended for internal HashiCorp use only +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: terraform + description: HashiCorp Terraform + annotations: + github.com/project-slug: hashicorp/terraform + jira/project-key: TF + jira/label: terraform +spec: + type: library + owner: team-tf-core + lifecycle: production diff --git a/checkpoint.go b/checkpoint.go index 31cc29bf75..8a20ff4463 100644 --- a/checkpoint.go +++ b/checkpoint.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( + "context" "fmt" "log" "path/filepath" @@ -8,6 +12,7 @@ import ( "github.com/hashicorp/go-checkpoint" "github.com/hashicorp/terraform/internal/command" "github.com/hashicorp/terraform/internal/command/cliconfig" + "go.opentelemetry.io/otel/codes" ) func init() { @@ -18,7 +23,7 @@ var checkpointResult chan *checkpoint.CheckResponse // runCheckpoint runs a HashiCorp Checkpoint request. You can read about // Checkpoint here: https://github.com/hashicorp/go-checkpoint. -func runCheckpoint(c *cliconfig.Config) { +func runCheckpoint(ctx context.Context, c *cliconfig.Config) { // If the user doesn't want checkpoint at all, then return. if c.DisableCheckpoint { log.Printf("[INFO] Checkpoint disabled. Not running.") @@ -26,6 +31,10 @@ func runCheckpoint(c *cliconfig.Config) { return } + ctx, span := tracer.Start(ctx, "HashiCorp Checkpoint") + _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here + defer span.End() + configDir, err := cliconfig.ConfigDir() if err != nil { log.Printf("[ERR] Checkpoint setup error: %s", err) @@ -52,7 +61,10 @@ func runCheckpoint(c *cliconfig.Config) { }) if err != nil { log.Printf("[ERR] Checkpoint error: %s", err) + span.SetStatus(codes.Error, err.Error()) resp = nil + } else { + span.SetStatus(codes.Ok, "checkpoint request succeeded") } checkpointResult <- resp diff --git a/codecov.yml b/codecov.yml index 5aeb0a38b4..bb15a33275 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + comment: layout: "flags, files" behavior: default diff --git a/commands.go b/commands.go index ea1f320030..48e3fc773d 100644 --- a/commands.go +++ b/commands.go @@ -1,15 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( + "context" "os" "os/signal" - "github.com/mitchellh/cli" - + "github.com/hashicorp/cli" "github.com/hashicorp/go-plugin" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/auth" "github.com/hashicorp/terraform-svchost/disco" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command" "github.com/hashicorp/terraform/internal/command/cliconfig" @@ -17,6 +21,7 @@ import ( "github.com/hashicorp/terraform/internal/command/webbrowser" "github.com/hashicorp/terraform/internal/getproviders" pluginDiscovery "github.com/hashicorp/terraform/internal/plugin/discovery" + "github.com/hashicorp/terraform/internal/rpcapi" "github.com/hashicorp/terraform/internal/terminal" ) @@ -49,6 +54,7 @@ var HiddenCommands map[string]struct{} var Ui cli.Ui func initCommands( + ctx context.Context, originalWorkingDir string, streams *terminal.Streams, config *cliconfig.Config, @@ -85,7 +91,7 @@ func initCommands( View: views.NewView(streams).SetRunningInAutomation(inAutomation), Color: true, - GlobalPluginDirs: globalPluginDirs(), + GlobalPluginDirs: cliconfig.GlobalPluginDirs(), Ui: Ui, Services: services, @@ -95,7 +101,10 @@ func initCommands( CLIConfigDir: configDir, PluginCacheDir: config.PluginCacheDir, - ShutdownCh: makeShutdownCh(), + PluginCacheMayBreakDependencyLockFile: config.PluginCacheMayBreakDependencyLockFile, + + ShutdownCh: makeShutdownCh(), + CallerContext: ctx, ProviderSource: providerSrc, ProviderDevOverrides: providerDevOverrides, @@ -207,6 +216,24 @@ func initCommands( }, nil }, + "metadata": func() (cli.Command, error) { + return &command.MetadataCommand{ + Meta: meta, + }, nil + }, + + "metadata functions": func() (cli.Command, error) { + return &command.MetadataFunctionsCommand{ + Meta: meta, + }, nil + }, + + "modules": func() (cli.Command, error) { + return &command.ModulesCommand{ + Meta: meta, + }, nil + }, + "output": func() (cli.Command, error) { return &command.OutputCommand{ Meta: meta, @@ -351,6 +378,12 @@ func initCommands( }, nil }, + "state identities": func() (cli.Command, error) { + return &command.StateIdentitiesCommand{ + Meta: meta, + }, nil + }, + "state rm": func() (cli.Command, error) { return &command.StateRmCommand{ StateMeta: command.StateMeta{ @@ -394,6 +427,25 @@ func initCommands( }, } + if meta.AllowExperimentalFeatures { + Commands["cloud"] = func() (cli.Command, error) { + return &command.CloudCommand{ + Meta: meta, + }, nil + } + + // "rpcapi" is handled a bit differently because the whole point of + // this interface is to bypass the CLI layer so wrapping automation can + // get as-direct-as-possible access to Terraform Core functionality, + // without interference from behaviors that are intended for CLI + // end-user convenience. We bypass the "command" package entirely + // for this command in particular. + Commands["rpcapi"] = rpcapi.CLICommandFactory(rpcapi.CommandFactoryOpts{ + ExperimentsAllowed: meta.AllowExperimentalFeatures, + ShutdownCh: meta.ShutdownCh, + }) + } + PrimaryCommands = []string{ "init", "validate", @@ -403,11 +455,11 @@ func initCommands( } HiddenCommands = map[string]struct{}{ - "env": struct{}{}, - "internal-plugin": struct{}{}, - "push": struct{}{}, + "env": {}, + "internal-plugin": {}, + "push": {}, + "rpcapi": {}, } - } // makeShutdownCh creates an interrupt listener and returns a channel. @@ -429,6 +481,6 @@ func makeShutdownCh() <-chan struct{} { } func credentialsSource(config *cliconfig.Config) (auth.CredentialsSource, error) { - helperPlugins := pluginDiscovery.FindPlugins("credentials", globalPluginDirs()) + helperPlugins := pluginDiscovery.FindPlugins("credentials", cliconfig.GlobalPluginDirs()) return config.CredentialsSource(helperPlugins) } diff --git a/docs/README.md b/docs/README.md index e4aca29356..d18d8ee5f4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,13 +4,18 @@ This directory contains some documentation about the Terraform Core codebase, aimed at readers who are interested in making code contributions. If you're looking for information on _using_ Terraform, please instead refer -to [the main Terraform CLI documentation](https://www.terraform.io/docs/cli/index.html). +to [the main Terraform CLI documentation](https://developer.hashicorp.com/terraform/cli). ## Terraform Core Architecture Documents -* [Terraform Core Architecture Summary](./architecture.md): an overview of the - main components of Terraform Core and how they interact. This is the best - starting point if you are diving in to this codebase for the first time. +* [Modules Runtime Architecture Summary](./architecture.md): an overview of the + main components of Terraform Core related to planning and applying modules. + This is the best starting point if you are diving in to this codebase for the + first time. + +* [Stacks Runtime Architecture Summary](../internal/stacks/README.md): an + overview of the main components of Terraform Core related to planning and + applying stack configurations. * [Resource Instance Change Lifecycle](./resource-instance-change-lifecycle.md): a description of the steps in validating, planning, and applying a change @@ -21,7 +26,7 @@ to [the main Terraform CLI documentation](https://www.terraform.io/docs/cli/inde SDK and so wish to conform to them. (If you are planning to write a new provider using the _official_ SDK then - please refer to [the Extend documentation](https://www.terraform.io/docs/extend/index.html) + please refer to [the Plugin development documentation](https://developer.hashicorp.com/terraform/plugin) instead; it presents similar information from the perspective of the SDK API, rather than the plugin wire protocol.) @@ -31,11 +36,21 @@ to [the main Terraform CLI documentation](https://www.terraform.io/docs/cli/inde This documentation is for SDK developers, and is not necessary reading for those implementing a provider using the official SDK. +* [Terraform Core RPC API](../internal/rpcapi/README.md): an integration point + for external software that needs to integrate Terraform Core functionality. + +* [Upgrading Terraform's Dependencies](./dependency-upgrades.md): guidance on + some special details that arise when we upgrade Go Module dependencies, due + to this codebase containing Terraform CLI, Terraform Core, and the various + remote state backends which all have some overlapping dependencies. + * [How Terraform Uses Unicode](./unicode.md): an overview of the various features of Terraform that rely on Unicode and how to change those features to adopt new versions of Unicode. +* [Deadlock-free Promises](../internal/promising/README.md): a utility package + used by the Terraform Stacks runtime to coordinate its concurrent work. + ## Contribution Guides -* [Maintainer Etiquette](./maintainer-etiquette.md): guidelines and expectations - for those who serve as Pull Request reviewers, issue triagers, etc. +* [Contributing to Terraform](../.github/CONTRIBUTING.md): a complete guideline for those who want to contribute to this project. diff --git a/docs/architecture.md b/docs/architecture.md index 0c93b16f1d..971d32ca39 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,6 +8,8 @@ We assume some familiarity with user-facing Terraform concepts like configuration, state, CLI workflow, etc. The Terraform website has documentation on these ideas. +**Links to Go documentation assume you are running [`pkgsite`](https://pkg.go.dev/golang.org/x/pkgsite/cmd/pkgsite) locally, with the default URL of localhost:8080.** + ## Terraform Request Flow The following diagram shows an approximation of how a user command is @@ -23,7 +25,7 @@ in more detail in a corresponding section below. Each time a user runs the `terraform` program, aside from some initial bootstrapping in the root package (not shown in the diagram) execution transfers immediately into one of the "command" implementations in -[the `command` package](https://pkg.go.dev/github.com/hashicorp/terraform/internal/command). +[the `command` package](http://localhost:8080/github.com/hashicorp/terraform/internal/command). The mapping between the user-facing command names and their corresponding `command` package types can be found in the `commands.go` file in the root of the repository. @@ -35,13 +37,13 @@ but it applies to the main Terraform workflow commands `terraform plan` and For these commands, the role of the command implementation is to read and parse any command line arguments, command line options, and environment variables that are needed for the given command and use them to produce a -[`backend.Operation`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/backend#Operation) +[`backendrun.Operation`](http://localhost:8080/github.com/hashicorp/terraform/internal/backend/backendrun#Operation) object that describes an action to be taken. An _operation_ consists of: * The action to be taken (e.g. "plan", "apply"). -* The name of the [workspace](https://www.terraform.io/docs/state/workspaces.html) +* The name of the [workspace](https://developer.hashicorp.com/terraform/language/state/workspaces) where the action will be taken. * Root module input variables to use for the action. * For the "plan" operation, a path to the directory containing the configuration's root module. @@ -50,25 +52,25 @@ An _operation_ consists of: "force" flag, etc. The operation is then passed to the currently-selected -[backend](https://www.terraform.io/docs/backends/index.html). Each backend name +[backend](https://developer.hashicorp.com/terraform/language/backend). Each backend name corresponds to an implementation of -[`backend.Backend`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/backend#Backend), using a +[`backend.Backend`](http://localhost:8080/github.com/hashicorp/terraform/internal/backend#Backend), using a mapping table in -[the `backend/init` package](https://pkg.go.dev/github.com/hashicorp/terraform/internal/backend/init). +[the `backend/init` package](http://localhost:8080/github.com/hashicorp/terraform/internal/backend/init). Backends that are able to execute operations additionally implement -[`backend.Enhanced`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/backend#Enhanced); +[`backendrun.OperationsBackend`](http://localhost:8080/github.com/hashicorp/terraform/internal/backend/backendrun#OperationsBackend); the command-handling code calls `Operation` with the operation it has constructed, and then the backend is responsible for executing that action. Backends that execute operations, however, do so as an architectural implementation detail and not a general feature of backends. That is, the term 'backend' as a Terraform feature is used to refer to a plugin that determines where Terraform stores its state snapshots - only the default `local` -backend and Terraform Cloud's backends (`remote`, `cloud`) perform operations. +backend and HCP Terraform's backends (`remote`, `cloud`) perform operations. Thus, most backends do _not_ implement this interface, and so the `command` package wraps these backends in an instance of -[`local.Local`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/backend/local#Local), +[`local.Local`](http://localhost:8080/github.com/hashicorp/terraform/internal/backend/local#Local), causing the operation to be executed locally within the `terraform` process itself. ## Backends @@ -78,19 +80,19 @@ A _backend_ determines where Terraform should store its state snapshots. As described above, the `local` backend also executes operations on behalf of most other backends. It uses a _state manager_ (either -[`statemgr.Filesystem`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/states/statemgr#Filesystem) if the +[`statemgr.Filesystem`](http://localhost:8080/github.com/hashicorp/terraform/internal/states/statemgr#Filesystem) if the local backend is being used directly, or an implementation provided by whatever backend is being wrapped) to retrieve the current state for the workspace specified in the operation, then uses the _config loader_ to load and do initial processing/validation of the configuration specified in the operation. It then uses these, along with the other settings given in the operation, to construct a -[`terraform.Context`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#Context), +[`terraform.Context`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#Context), which is the main object that actually performs Terraform operations. The `local` backend finally calls an appropriate method on that context to begin execution of the relevant command, such as -[`Plan`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#Context.Plan) +[`Plan`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#Context.Plan) or [`Apply`](), which in turn constructs a graph using a _graph builder_, described in a later section. @@ -98,21 +100,21 @@ described in a later section. ## Configuration Loader The top-level configuration structure is represented by model types in -[package `configs`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/configs). -A whole configuration (the root module plus all of its descendent modules) +[package `configs`](http://localhost:8080/github.com/hashicorp/terraform/internal/configs). +A whole configuration (the root module plus all of its descendant modules) is represented by -[`configs.Config`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/configs#Config). +[`configs.Config`](http://localhost:8080/github.com/hashicorp/terraform/internal/configs#Config). The `configs` package contains some low-level functionality for constructing configuration objects, but the main entry point is in the sub-package -[`configload`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/configs/configload]), +[`configload`](http://localhost:8080/github.com/hashicorp/terraform/internal/configs/configload), via -[`configload.Loader`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/configs/configload#Loader). +[`configload.Loader`](http://localhost:8080/github.com/hashicorp/terraform/internal/configs/configload#Loader). A loader deals with all of the details of installing child modules (during `terraform init`) and then locating those modules again when a configuration is loaded by a backend. It takes the path to a root module and recursively loads all of the child modules to produce a single -[`configs.Config`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/configs#Config) +[`configs.Config`](http://localhost:8080/github.com/hashicorp/terraform/internal/configs#Config) representing the entire configuration. Terraform expects configuration files written in the Terraform language, which @@ -121,37 +123,37 @@ is a DSL built on top of cannot be interpreted until we build and walk the graph, since they depend on the outcome of other parts of the configuration, and so these parts of the configuration remain represented as the low-level HCL types -[`hcl.Body`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Body) +[`hcl.Body`](http://localhost:8080/github.com/hashicorp/hcl/v2/#Body) and -[`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression), +[`hcl.Expression`](http://localhost:8080/github.com/hashicorp/hcl/v2/#Expression), allowing Terraform to interpret them at a more appropriate time. ## State Manager A _state manager_ is responsible for storing and retrieving snapshots of the -[Terraform state](https://www.terraform.io/docs/language/state/index.html) +[Terraform state](https://developer.hashicorp.com/terraform/language/state) for a particular workspace. Each manager is an implementation of some combination of interfaces in -[the `statemgr` package](https://pkg.go.dev/github.com/hashicorp/terraform/internal/states/statemgr), +[the `statemgr` package](http://localhost:8080/github.com/hashicorp/terraform/internal/states/statemgr), with most practical managers implementing the full set of operations described by -[`statemgr.Full`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/states/statemgr#Full) +[`statemgr.Full`](http://localhost:8080/github.com/hashicorp/terraform/internal/states/statemgr#Full) provided by a _backend_. The smaller interfaces exist primarily for use in other function signatures to be explicit about what actions the function might take on the state manager; there is little reason to write a state manager that does not implement all of `statemgr.Full`. The implementation -[`statemgr.Filesystem`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/states/statemgr#Filesystem) is used +[`statemgr.Filesystem`](http://localhost:8080/github.com/hashicorp/terraform/internal/states/statemgr#Filesystem) is used by default (by the `local` backend) and is responsible for the familiar `terraform.tfstate` local file that most Terraform users start with, before -they switch to [remote state](https://www.terraform.io/docs/language/state/remote.html). +they switch to [remote state](https://developer.hashicorp.com/terraform/language/state/remote). Other implementations of `statemgr.Full` are used to implement remote state. Each of these saves and retrieves state via a remote network service appropriate to the backend that creates it. A state manager accepts and returns a state snapshot as a -[`states.State`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/states#State) +[`states.State`](http://localhost:8080/github.com/hashicorp/terraform/internal/states#State) object. The state manager is responsible for exactly how that object is serialized and stored, but all state managers at the time of writing use the same JSON serialization format, storing the resulting JSON bytes in some @@ -160,7 +162,7 @@ kind of arbitrary blob store. ## Graph Builder A _graph builder_ is called by a -[`terraform.Context`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#Context) +[`terraform.Context`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#Context) method (e.g. `Plan` or `Apply`) to produce the graph that will be used to represent the necessary steps for that operation and the dependency relationships between them. @@ -170,7 +172,7 @@ In most cases, the graphs each represent a specific object in the configuration, or something derived from those configuration objects. For example, each `resource` block in the configuration has one corresponding -[`GraphNodeConfigResource`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#GraphNodeConfigResource) +[`GraphNodeConfigResource`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#GraphNodeConfigResource) vertex representing it in the "plan" graph. (Terraform Core uses terminology inconsistently, describing graph _vertices_ also as graph _nodes_ in various places. These both describe the same concept.) @@ -187,26 +189,26 @@ graph from the set of changes described in the plan that is being applied. The graph builders all work in terms of a sequence of _transforms_, which are implementations of -[`terraform.GraphTransformer`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#GraphTransformer). +[`terraform.GraphTransformer`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#GraphTransformer). Implementations of this interface just take a graph and mutate it in any way needed, and so the set of available transforms is quite varied. Some important examples include: -* [`ConfigTransformer`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#ConfigTransformer), +* [`ConfigTransformer`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#ConfigTransformer), which creates a graph vertex for each `resource` block in the configuration. -* [`StateTransformer`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#StateTransformer), +* [`StateTransformer`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#StateTransformer), which creates a graph vertex for each resource instance currently tracked in the state. -* [`ReferenceTransformer`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#ReferenceTransformer), +* [`ReferenceTransformer`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#ReferenceTransformer), which analyses the configuration to find dependencies between resources and other objects and creates any necessary "happens after" edges for these. -* [`ProviderTransformer`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#ProviderTransformer), +* [`ProviderTransformer`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#ProviderTransformer), which associates each resource or resource instance with exactly one provider configuration (implementing - [the inheritance rules](https://www.terraform.io/docs/language/modules/develop/providers.html)) + [the inheritance rules](https://developer.hashicorp.com/terraform/language/modules/develop/providers)) and then creates "happens after" edges to ensure that the providers are initialized before taking any actions with the resources that belong to them. @@ -217,7 +219,7 @@ builder uses a different subset of these depending on the needs of the operation that is being performed. The result of graph building is a -[`terraform.Graph`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#Graph), which +[`terraform.Graph`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#Graph), which can then be processed using a _graph walker_. ## Graph Walk @@ -225,17 +227,17 @@ can then be processed using a _graph walker_. The process of walking the graph visits each vertex of that graph in a way which respects the "happens after" edges in the graph. The walk algorithm itself is implemented in -[the low-level `dag` package](https://pkg.go.dev/github.com/hashicorp/terraform/internal/dag#AcyclicGraph.Walk) +[the low-level `dag` package](http://localhost:8080/github.com/hashicorp/terraform/internal/dag#AcyclicGraph.Walk) (where "DAG" is short for [_Directed Acyclic Graph_](https://en.wikipedia.org/wiki/Directed_acyclic_graph)), in -[`AcyclicGraph.Walk`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/dag#AcyclicGraph.Walk). +[`AcyclicGraph.Walk`](http://localhost:8080/github.com/hashicorp/terraform/internal/dag#AcyclicGraph.Walk). However, the "interesting" Terraform walk functionality is implemented in -[`terraform.ContextGraphWalker`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#ContextGraphWalker), +[`terraform.ContextGraphWalker`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#ContextGraphWalker), which implements a small set of higher-level operations that are performed during the graph walk: * `EnterPath` is called once for each module in the configuration, taking a module address and returning a - [`terraform.EvalContext`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#EvalContext) + [`terraform.EvalContext`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#EvalContext) that tracks objects within that module. `terraform.Context` is the _global_ context for the entire operation, while `terraform.EvalContext` is a context for processing within a single module, and is the primary means @@ -247,7 +249,7 @@ will evaluate multiple vertices concurrently. Vertex evaluation code must therefore make careful use of concurrency primitives such as mutexes in order to coordinate access to shared objects such as the `states.State` object. In most cases, we use the helper wrapper -[`states.SyncState`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/states#SyncState) +[`states.SyncState`](http://localhost:8080/github.com/hashicorp/terraform/internal/states#SyncState) to safely implement concurrent reads and writes from the shared state. ## Vertex Evaluation @@ -280,27 +282,27 @@ a plan operation would include the following high-level steps: this operation. Each execution step for a vertex is an implementation of -[`terraform.Execute`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/erraform#Execute). +[`terraform.GraphNodeExecutable.Execute`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#GraphNodeExecutable.Execute). As with graph transforms, the behavior of these implementations varies widely: whereas graph transforms can take any action against the graph, an `Execute` implementation can take any action against the `EvalContext`. The implementation of `terraform.EvalContext` used in real processing (as opposed to testing) is -[`terraform.BuiltinEvalContext`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#BuiltinEvalContext). +[`terraform.BuiltinEvalContext`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#BuiltinEvalContext). It provides coordinated access to plugins, the current state, and the current plan via the `EvalContext` interface methods. In order to be executed, a vertex must implement -[`terraform.GraphNodeExecutable`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#GraphNodeExecutable), +[`terraform.GraphNodeExecutable`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#GraphNodeExecutable), which has a single `Execute` method that handles. There are numerous `Execute` implementations with different behaviors, but some prominent examples are: -* [NodePlannableResource.Execute](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#NodePlannableResourceInstance.Execute), which handles the `plan` operation. +* [`NodePlannableResource.Execute`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#NodePlannableResourceInstance.Execute), which handles the `plan` operation. -* [`NodeApplyableResourceInstance.Execute`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#NodeApplyableResourceInstance.Execute), which handles the main `apply` operation. +* [`NodeApplyableResourceInstance.Execute`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#NodeApplyableResourceInstance.Execute), which handles the main `apply` operation. -* [`NodeDestroyResourceInstance.Execute`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#EvalWriteState), which handles the main `destroy` operation. +* [`NodeDestroyResourceInstance.Execute`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#EvalWriteState), which handles the main `destroy` operation. A vertex must complete successfully before the graph walk will begin evaluation for other vertices that have "happens after" edges. Evaluation can fail with one @@ -320,11 +322,11 @@ The high-level process for expression evaluation is: to. For example, the expression `aws_instance.example[1]` refers to one of the instances created by a `resource "aws_instance" "example"` block in configuration. This analysis is performed by - [`lang.References`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/lang#References), + [`lang.References`](http://localhost:8080/github.com/hashicorp/terraform/internal/lang#References), or more often one of the helper wrappers around it: - [`lang.ReferencesInBlock`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/lang#ReferencesInBlock) + [`lang.ReferencesInBlock`](http://localhost:8080/github.com/hashicorp/terraform/internal/lang#ReferencesInBlock) or - [`lang.ReferencesInExpr`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/lang#ReferencesInExpr) + [`lang.ReferencesInExpr`](http://localhost:8080/github.com/hashicorp/terraform/internal/lang#ReferencesInExpr) 1. Retrieve from the state the data for the objects that are referred to and create a lookup table of the values from these objects that the @@ -334,18 +336,18 @@ The high-level process for expression evaluation is: them. 1. Ask HCL to evaluate each attribute's expression (a - [`hcl.Expression`](https://pkg.go.dev/github.com/hashicorp/hcl/v2/#Expression) + [`hcl.Expression`](http://localhost:8080/github.com/hashicorp/hcl/v2/#Expression) object) against the data and function lookup tables. In practice, steps 2 through 4 are usually run all together using one -of the methods on [`lang.Scope`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/lang#Scope); +of the methods on [`lang.Scope`](http://localhost:8080/github.com/hashicorp/terraform/internal/lang#Scope); most commonly, -[`lang.EvalBlock`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/lang#Scope.EvalBlock) +[`lang.EvalBlock`](http://localhost:8080/github.com/hashicorp/terraform/internal/lang#Scope.EvalBlock) or -[`lang.EvalExpr`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/lang#Scope.EvalExpr). +[`lang.EvalExpr`](http://localhost:8080/github.com/hashicorp/terraform/internal/lang#Scope.EvalExpr). Expression evaluation produces a dynamic value represented as a -[`cty.Value`](https://pkg.go.dev/github.com/zclconf/go-cty/cty#Value). +[`cty.Value`](http://localhost:8080/github.com/zclconf/go-cty/cty#Value). This Go type represents values from the Terraform language and such values are eventually passed to provider plugins. @@ -367,7 +369,7 @@ known when the main graph is constructed, but become known while evaluating other vertices in the main graph. This special behavior applies to vertex objects that implement -[`terraform.GraphNodeDynamicExpandable`](https://pkg.go.dev/github.com/hashicorp/terraform/internal/terraform#GraphNodeDynamicExpandable). +[`terraform.GraphNodeDynamicExpandable`](http://localhost:8080/github.com/hashicorp/terraform/internal/terraform#GraphNodeDynamicExpandable). Such vertices have their own nested _graph builder_, _graph walk_, and _vertex evaluation_ steps, with the same behaviors as described in these sections for the main graph. The difference is in which graph transforms diff --git a/docs/debugging-configs/vscode/debug-automated-tests/launch.json b/docs/debugging-configs/vscode/debug-automated-tests/launch.json new file mode 100644 index 0000000000..6d327091f5 --- /dev/null +++ b/docs/debugging-configs/vscode/debug-automated-tests/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Highlight a test's name with your cursor and run this debugger configuration + // from the debugger tools in the left-side Activity Bar. + // This will result in the equivalent of this command being run using the debugger: + // `go test -v -run ^$ ` + "name": "Run selected test", + "request": "launch", + "type": "go", + "args": [ + "-test.v", + "-test.run", + "^${selectedText}$" + ], + // Environment variables can be set from a file or as key-value pairs in the configuration. + // "env": { + // "MY_ENV": "my-value", + // }, + // "envFile": "./vscode/private.env", + "mode": "auto", + "program": "${fileDirname}", + "showLog": true // dlv's logs + } + ] +} \ No newline at end of file diff --git a/docs/debugging-configs/vscode/launch-from-vscode-debugger/launch.json b/docs/debugging-configs/vscode/launch-from-vscode-debugger/launch.json new file mode 100644 index 0000000000..57ec66869e --- /dev/null +++ b/docs/debugging-configs/vscode/launch-from-vscode-debugger/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Runs Terraform using the Terraform configuration specified via the chdir argument + "name": "Run Terraform in debug mode", + "request": "launch", + "type": "go", + "args": [ + "-chdir=", + // Change this array to perform different terraform commands with different arguments. + "plan", + "-var='name=value'" + ], + // "cwd": "", // An alternative to using the -chdir global flag above. + // Environment variables can be set from a file or as key-value pairs in the configuration. + // "env": { + // "MY_ENV": "my-value", + // }, + // "envFile": "./vscode/private.env", + "mode": "debug", + "program": "${workspaceFolder}", + "console": "integratedTerminal", // allows responding to y/n in terminal, e.g. in apply command. + "showLog": false // dlv's logs + } + ] +} \ No newline at end of file diff --git a/docs/debugging-configs/vscode/launch-via-cli/launch.json b/docs/debugging-configs/vscode/launch-via-cli/launch.json new file mode 100644 index 0000000000..bdcb0cb076 --- /dev/null +++ b/docs/debugging-configs/vscode/launch-via-cli/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Connect to dlv server", + "type": "go", + "debugAdapter": "dlv-dap", + "request": "attach", + "mode": "remote", + "remotePath": "${workspaceFolder}", + "port": 2345, + "host": "127.0.0.1", + } + ] +} diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 0000000000..d81c4f01ec --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,97 @@ +# How to Debug Terraform + +Contents: +- [Debugging automated tests](#debugging-automated-tests) + - [Debugging automated tests in VSCode](#debugging-automated-tests-in-vscode) +- [Debugging Terraform operations that use real Terraform configurations](#debugging-terraform-operations-that-use-real-terraform-configurations) + - [Launch Terraform with the `dlv` CLI tool](#launch-terraform-with-the-dlv-cli-tool) + - [Launch Terraform with VS Code's debugging tool](#launch-terraform-with-vs-codes-debugging-tool) + + +As Terraform is written in Go you may use [Delve](https://github.com/go-delve/delve) to debug it. + +GoLand includes [debugging features](https://www.jetbrains.com/help/go/debugging-code.html), and the [Go extension for VS Code](https://code.visualstudio.com/docs/languages/go#_debugging) makes it easy to use Delve when debugging Go codebases in VS Code. + +## Debugging automated tests + +Debugging an automated test is often the most straightforward workflow for debugging a section of the codebase. For example, the Go extension for VS Code](https://code.visualstudio.com/docs/languages/go#_debugging) adds `run test | debug test` options above all tests in a `*_test.go` file. These allow debugging without any prior configuration. + +### Debugging automated tests in VSCode + +As described above, debugging tests in VS Code is easily achieved through the Go extension. + +If you need more control over how tests are run while debugging, e.g. environment variable values, look at the [example debugger launch configuration 'Run selected test'](./debugging-configs/vscode/debug-automated-tests/launch.json). You can adapt this example to create your own [launch configuration file](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations). + +When using this launch configuration you must highlight a test's name before starting the debugger: + +

+ Debugging a single test using the example 'Run selected test' debugger configuration shared in this repository +

+ + +## Debugging Terraform operations that use real Terraform configurations + +### Launch Terraform with the `dlv` CLI tool + +In this workflow you: +* Build Terraform using compiler flags. +* Start a debug server with a command containing the terraform command you want to debug. + * This command is run in the working directory that contains your Terraform configuration. +* Connect to the debug server to monitor progress through breakpoints. + +#### 1. Compile & Start Debug Server + +One way to do it is to compile a binary with the [appropriate compiler flags](https://pkg.go.dev/cmd/compile#hdr-Command_Line): + +```sh +go install -gcflags="all=-N -l" +``` + +This enables you to then execute the compiled binary via Delve, pass any arguments and spin up a debug server which you can then connect to: + +```sh +# Update the path to the terraform binary if your install directory is influenced by $GOPATH or $GOBIN +dlv exec $HOME/go/bin/terraform --headless --listen :2345 --log -- apply +``` + +#### 2a. Connect via CLI + +You may connect to the headless debug server via Delve CLI + +```sh +dlv connect :2345 +``` + +#### 2b. Connect from VS Code + +The repository provides [an example 'Connect to dlv server' launch configuration](./debugging-configs/vscode/launch-via-cli/launch.json), making it possible to use VS Code's native debugging integration via the [Go extension for VS Code](https://code.visualstudio.com/docs/languages/go#_debugging): + +

+ vscode debugger +

+ + +### Launch Terraform with VS Code's debugging tool + +In this workflow you: +* Update the debugger's launch configuration to point at the directory containing your Terraform configuration. +* Start the debugger through VS Code and monitor progress through breakpoints. + +#### 1. Update the debugger's launch configuration + +Look at the [example debugger launch configuration 'Run Terraform in debug mode'](./debugging-configs/vscode/launch-from-vscode-debugger/launch.json). You can adapt this example to create your own [launch configuration file](https://code.visualstudio.com/docs/editor/debugging#_launch-configurations). + +To use this launch configuration: +* Prepare a local Terraform project. +* Get the absolute path to that directory. +* Update the launch configuration to use that path, either in the `-chdir` argument or as a `cwd` attribute in the launch configuration. +* Make sure the `args` array's element reflect the command you'd like to debug. +* Provide any required environment variables through the `env` or `envFile` attributes. + +#### 2. Run the launch configuration in VS Code + +Navigate to the Run and Debug view in the left-side Activity Bar. After selecting the `Run Terraform in debug mode` configuration in the Run and Debug view from the left-side, press the green arrow. + +This is equivalent to running a Terraform CLI command in the local Terraform project's directory. For example, if you run and debug a plan command that saves a plan file, that plan file will be created. + +This workflow is useful if you need to set up a complicated prior state to replicate a bug or if you want to debug code behaviour given a specific configuration. \ No newline at end of file diff --git a/docs/dependency-upgrades.md b/docs/dependency-upgrades.md new file mode 100644 index 0000000000..aa29a115c2 --- /dev/null +++ b/docs/dependency-upgrades.md @@ -0,0 +1,153 @@ +# Upgrading Terraform's Library Dependencies + +This codebase depends on a variety of external Go modules. At the time of +writing, a Terraform CLI build includes the union of all dependencies required +by Terraform CLI and Core itself and each of the remote state backends. + +Because the remote state backends have quite a different set of needs than +Terraform itself -- special client libraries, in particular -- we've declared +them as being separate Go Modules with their own `go.mod` and `go.sum` files, +even though we don't intend to ever publish them separately. The goal is just +to help track which dependencies are used by each of these components, and to +more easily determine which of the components are affected by a particular +dependency upgrade so that we can make sure to do the appropriate testing. + +## Dependency Synchronization + +Because all of these components ultimately link into the same executable, there +can be only one version of each distinct module and thus all of the modules +must agree on which version to use. + +The makefile target `syncdeps` runs a script which synchronizes all of the +modules to declare compatible dependencies, selecting the newest version of +each external module selected across all of the internal modules: + +```shell +make syncdeps +``` + +After running this, use `git status` to see what's been changed. If you've +changed the dependencies of any of the modules then that should typically +cause an update to the root module, because that one imports all of the others. + +## Upgrading a Dependency + +To select a newer version of one of the dependencies, use `go get` in the +root to specify which version to use: + +```shell +go get example.com/foo/bar@v1.0.0 +``` + +Or, if you just want to move to the latest stable release, you can use the +`latest` pseudo-version: + +```shell +go get example.com/foo/bar@latest +``` + +Then run `make syncdeps` to update any of the child modules that also use +this dependency. The remote state backends use only a subset of the packages +in Terraform CLI/Core, so not all dependency updates will affect the remote +state backends, and an update might affect only a subset of the backends. + +When you open the pull request for your change, our code owners rules will +automatically request review from the team that maintains any affected remote +state backend. The affected teams can judge whether the update seems likely +to affect their backend and run their acceptance tests if so, before approving +the pull request. As usual, these PRs should also be reviewed by at least +one member of the Terraform Core team since they are ultimately responsible +for the complete set of dependencies used in Terraform CLI releases. + +**Note:** Currently our code owners rules are simplistic and will request +review for _any_ change under a remote state backend module directory, but +in practice an update that only changes a backend's `go.sum` cannot affect +the runtime behavior of the backend, and so those review requests are not +strictly required. You should therefore remove the review requests for +any backend whose only diff is the `go.sum` file once you've opened the +pull request. + +## Dependabot Updates + +When Dependabot automatically opens a pull request to upgrade a dependency, +unfortunately it isn't smart enough to automatically synchronize the change +across the modules and so the code consistency checks for the change will +typically fail. + +To apply the proposed change, you'll need to check out the branch that +Dependabot created on your development system, run `make syncdeps`, add +all of the files that get modified, and then amend Dependabot's commit using +`git commit --amend`. + +After you've done this, use `git push --force` to replace Dependabot's original +commit with your new commit, and then wait for GitHub to re-run the PR +checks. The code consistency checks should now pass. + +We've configured Dependabot to monitor only the root `go.mod` file for potential +upgrades, because that one aggregates the dependencies for all other child +modules. Therefore there should never be a Dependabot upgrade targeting a +module in a subdirectory. If one _does_ get created somehow, you should close +it and perform the same upgrade at the root of the repository instead, using +the instructions in [Upgrading a Dependency](#upgrading-a-dependency) above. + +## Dependencies with Special Requirements + +Most of our dependencies can be treated generically, but a few have some +special constraints due to how Terraform uses them: + +* HCL, cty, and their indirect dependencies `golang.org/x/text` and + `github.com/apparentlymart/go-textseg` all include logic based on Unicode + specifications, and so should be updated with care to make sure that + Terraform's support for Unicode follows a consistent Unicode version + throughout. + + Additionally, each time we adopt a new minor release of Go, we may need to + upgrade some or all of these dependencies to match the Unicode version used + by the Go standard library. + + For more information, refer to [How Terraform Uses Unicode](unicode.md). + + (This concern does not apply if the new version we're upgrading to is built + for the same version of Unicode that Terraform was already using.) + +* `github.com/hashicorp/go-getter` represents a significant part of Terraform + CLI's remote module installer, and is the final interpreter of Terraform's + module source address syntax. Because the module source address syntax is + protected by the Terraform v1.x Compatibility Promises, for each upgrade + we must make sure that: + + - The upgrade doesn't expand the source address syntax in a way that is + undesirable from a Terraform product standpoint or in a way that we would + not feel comfortable supporting indefinitely under the compatibility + promises. + - The upgrade doesn't break any already-supported source address forms + that would therefore cause the next Terraform version to break the + v1.x compatibility promises. + + Terraform's use of `go-getter` is all encapsulated in `internal/getmodules` + and is set up to try to minimize the possibility that a go-getter upgrade + would immediately introduce new functionality, but that encapsulation cannot + prevent adoption of changes made to pre-existing functionality that + Terraform already exposes. + +* `github.com/hashicorp/go-tfe` -- the client library for the HCP Terraform + API -- includes various types corresponding to HCP Terraform API + requests and responses. The internal package `internal/cloud` contains mock + implementations of some of those types, which may need to be updated when + the client library is upgraded. + + These upgrades should typically be done only in conjunction with a project + that will somehow use the new features through the Cloud integration, so + that the team working on that project can perform any needed updates to + the mocks as part of their work. + +* `go.opentelemetry.io/otel` and the other supporting OpenTelemetry modules + should typically be upgraded together in lockstep, because some of the + modules define interfaces that other modules implement, and strange behavior + can emerge if one is upgraded without the other. + + The main modules affected by this rule are the ones under the + `go.opentelemetry.io/otel` prefix. The "contrib" packages can be trickier + to upgrade because they tend to have dependencies that overlap with ours + and so might affect non-telemetry-related behavior, and so it's acceptable + for those to lag slightly behind to reduce risk in routine upgrades. diff --git a/docs/images/vscode-debugging-test.png b/docs/images/vscode-debugging-test.png new file mode 100644 index 0000000000..0262c3cf01 Binary files /dev/null and b/docs/images/vscode-debugging-test.png differ diff --git a/docs/images/vscode-debugging.png b/docs/images/vscode-debugging.png new file mode 100644 index 0000000000..c8e6f27f71 Binary files /dev/null and b/docs/images/vscode-debugging.png differ diff --git a/docs/maintainer-etiquette.md b/docs/maintainer-etiquette.md deleted file mode 100644 index e273f220b4..0000000000 --- a/docs/maintainer-etiquette.md +++ /dev/null @@ -1,95 +0,0 @@ -# Maintainer's Etiquette - -Are you a core maintainer of Terraform? Great! Here's a few notes -to help you get comfortable when working on the project. - -This documentation is somewhat outdated since it still includes provider-related -information even though providers are now developed in their own separate -codebases, but the general information is still valid. - -## Expectations - -We value the time you spend on the project and as such your maintainer status -doesn't imply any obligations to do any specific work. - -### Your PRs - -These apply to all contributors, but maintainers should lead by examples! :wink: - - - for `provider/*` PRs it's useful to attach test results & advise on how to run the relevant tests - - for `bug` fixes it's useful to attach repro case, ideally in a form of a test - -### PRs/issues from others - - - you're welcomed to triage (attach labels to) other PRs and issues - - we generally use 2-label system (= at least 2 labels per issue/PR) where one label is generic and other one API-specific, e.g. `enhancement` & `provider/aws` - -## Merging - - - you're free to review PRs from the community or other HC employees and give :+1: / :-1: - - if the PR submitter has push privileges (recognizable via `Collaborator`, `Member` or `Owner` badge) - we expect **the submitter** to merge their own PR after receiving a positive review from either HC employee or another maintainer. _Exceptions apply - see below._ - - we prefer to use the GitHub's interface or API to do this, just click the green button - - squash? - - squash when you think the commit history is irrelevant (will not be helpful for any readers in T+6months) - - Add the new PR to the **Changelog** if it may affect the user (almost any PR except test changes and docs updates) - - we prefer to use the GitHub's web interface to modify the Changelog and use `[GH-12345]` to format the PR number. These will be turned into links as part of the release process. Breaking changes should be always documented separately. - -## Release process - - - HC employees are responsible for cutting new releases - - The employee cutting the release will always notify all maintainers via Slack channel before & after each release - so you can avoid merging PRs during the release process. - -## Exceptions - -Any PR that is significantly changing or even breaking user experience cross-providers should always get at least one :+1: from a HC employee prior to merge. - -It is generally advisable to leave PRs labelled as `core` for HC employees to review and merge. - -Examples include: - - adding/changing/removing a CLI (sub)command or a [flag](https://github.com/hashicorp/terraform/pull/12939) - - introduce a new feature like [Environments](https://github.com/hashicorp/terraform/pull/12182) or [Shadow Graph](https://github.com/hashicorp/terraform/pull/9334) - - changing config (HCL) like [adding support for lists](https://github.com/hashicorp/terraform/pull/6322) - - change of the [build process or test environment](https://github.com/hashicorp/terraform/pull/9355) - -## Breaking Changes - - - we always try to avoid breaking changes where possible and/or defer them to the nearest major release - - [state migration](https://github.com/hashicorp/terraform/blob/2fe5976aec290f4b53f07534f4cde13f6d877a3f/helper/schema/resource.go#L33-L56) may help you avoid breaking changes, see [example](https://github.com/hashicorp/terraform/blob/351c6bed79abbb40e461d3f7d49fe4cf20bced41/builtin/providers/aws/resource_aws_route53_record_migrate.go) - - either way BCs should be clearly documented in special section of the Changelog - - Any BC must always receive at least one :+1: from HC employee prior to merge, two :+1:s are advisable - - ### Examples of Breaking Changes - - - https://github.com/hashicorp/terraform/pull/12396 - - https://github.com/hashicorp/terraform/pull/13872 - - https://github.com/hashicorp/terraform/pull/13752 - -## Unsure? - -If you're unsure about anything, ask in the committer's Slack channel. - -## New Providers - -These will require :+1: and some extra effort from HC employee. - -We expect all acceptance tests to be as self-sustainable as possible -to keep the bar for running any acceptance test low for anyone -outside of HashiCorp or core maintainers team. - -We expect any test to run **in parallel** alongside any other test (even the same test). -To ensure this is possible, we need all tests to avoid sharing namespaces or using static unique names. -In rare occasions this may require the use of mutexes in the resource code. - -### New Remote-API-based provider (e.g. AWS, Google Cloud, PagerDuty, Atlas) - -We will need some details about who to contact or where to register for a new account -and generally we can't merge providers before ensuring we have a way to test them nightly, -which usually involves setting up a new account and obtaining API credentials. - -### Local provider (e.g. MySQL, PostgreSQL, Kubernetes, Vault) - -We will need either Terraform configs that will set up the underlying test infrastructure -(e.g. GKE cluster for Kubernetes) or Dockerfile(s) that will prepare test environment (e.g. MySQL) -and expose the endpoint for testing. - diff --git a/docs/planning-behaviors.md b/docs/planning-behaviors.md index ecb6fb3011..a9ce2151c0 100644 --- a/docs/planning-behaviors.md +++ b/docs/planning-behaviors.md @@ -20,7 +20,7 @@ their behaviors in a way comparable to the resource instance behaviors. This is developer-oriented documentation rather than user-oriented documentation. See -[the main Terraform documentation](https://www.terraform.io/docs) for +[the main Terraform documentation](https://developer.hashicorp.com/terraform/docs) for information on existing planning behaviors and other behaviors as viewed from an end-user perspective. diff --git a/docs/plugin-protocol/.copywrite.hcl b/docs/plugin-protocol/.copywrite.hcl new file mode 100644 index 0000000000..2c144ddb7d --- /dev/null +++ b/docs/plugin-protocol/.copywrite.hcl @@ -0,0 +1,6 @@ +schema_version = 1 + +project { + license = "MPL-2.0" + copyright_year = 2024 +} diff --git a/docs/plugin-protocol/LICENSE b/docs/plugin-protocol/LICENSE new file mode 100644 index 0000000000..e25da5fad9 --- /dev/null +++ b/docs/plugin-protocol/LICENSE @@ -0,0 +1,355 @@ +Copyright (c) 2014 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. diff --git a/docs/plugin-protocol/README.md b/docs/plugin-protocol/README.md index de92501818..99fd4befa7 100644 --- a/docs/plugin-protocol/README.md +++ b/docs/plugin-protocol/README.md @@ -10,7 +10,7 @@ the SDK's API. ---- **If you want to write a plugin for Terraform, please refer to -[Extending Terraform](https://www.terraform.io/docs/extend/index.html) instead.** +[Plugin development](https://developer.hashicorp.com/terraform/plugin) instead.** This documentation is for those who are developing _Terraform SDKs_, rather than those implementing plugins. @@ -56,8 +56,7 @@ do not follow this strategy. The authoritative definition for each protocol version is in this directory as a Protocol Buffers (protobuf) service definition. The files follow the -naming pattern `tfpluginX.Y.proto`, where X is the major version and Y -is the minor version. +naming pattern `tfpluginX.proto`, where X is the major version. ### Major and minor versioning @@ -211,3 +210,22 @@ new file in this directory for each new minor version and consider all previously-tagged definitions as immutable. The outdated comments in those files are retained in order to keep the promise of immutability, even though it is now incorrect. + +## Updating the plugin protocol in Terraform core + +> Note: This section's audience is contributors to Terraform, not developers creating a new SDK. + +New features added to Terraform core often require an update to the plugin protocol. These changes are reflected in a new minor version of the plugin protocol. + +A new minor release of Terraform should only introduce one new minor release of the plugin-protocol. The minor release numbers of the plugin protocol and Terraform do not match, but they should have a one-to-one relationship. + +### Add updates to a new plugin protocol minor version + +1) Edit the `.proto` files for Protocol 5 and 6 in `docs/plugin-protocol/`. + * If this is the first bigger change after a Terraform release, you may want to increase the minor protocol version in the file header +1) Commit your changes. +1) Run `make protobuf`. + * This will use the symlinks in the `internal/tfplugin*` directories to access the latest minor versions' `.proto` files. + * You should see diffs in `internal/tfplugin5/tfplugin5.pb.go` and `internal/tfplugin6/tfplugin6.pb.go`. +1) Run `make generate`. + * You should see diffs in `internal/plugin/mock_proto/mock.go` and `internal/plugin6/mock_proto/mock.go`. \ No newline at end of file diff --git a/docs/plugin-protocol/object-wire-format.md b/docs/plugin-protocol/object-wire-format.md index 5e1809c819..2b57493e2b 100644 --- a/docs/plugin-protocol/object-wire-format.md +++ b/docs/plugin-protocol/object-wire-format.md @@ -63,7 +63,7 @@ The key-value pairs representing nested block types have values based on The MessagePack serialization of an attribute value depends on the value of the `type` field of the corresponding `Schema.Attribute` message. The `type` field is a compact JSON serialization of a -[Terraform type constraint](https://www.terraform.io/docs/configuration/types.html), +[Terraform type constraint](https://developer.hashicorp.com/terraform/language/expressions/type-constraints), which consists either of a single string value (for primitive types) or a two-element array giving a type kind and a type argument. @@ -76,8 +76,7 @@ in the table below, regardless of type: * An unknown value (that is, a placeholder for a value that will be decided only during the apply operation) is represented as a [MessagePack extension](https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types) - value whose type identifier is zero and whose value is unspecified and - meaningless. + value, described in more detail below. | `type` Pattern | MessagePack Representation | |---|---| @@ -91,6 +90,64 @@ in the table below, regardless of type: | `["tuple",TYPES]` | A MessagePack array with one element per element described by the `TYPES` array. The element values are constructed by applying these same mapping rules to the corresponding element of `TYPES`. | | `"dynamic"` | A MessagePack array with exactly two elements. The first element is a MessagePack binary value containing a JSON-serialized type constraint in the same format described in this table. The second element is the result of applying these same mapping rules to the value with the type given in the first element. This special type constraint represents values whose types will be decided only at runtime. | +Unknown values have two possible representations, both using +[MessagePack extension](https://github.com/msgpack/msgpack/blob/master/spec.md#extension-types) +values. + +The older encoding is for unrefined unknown values and uses an extension +code of zero, with the extension value payload completely ignored. + +Newer Terraform versions can produce "refined" unknown values which carry some +additional information that constrains the possible range of the final value/ +Refined unknown values have extension code 12 and then the extension object's +payload is a MessagePack-encoded map using integer keys to represent different +kinds of refinement: + +* `1` represents "nullness", and the value of that key will be a boolean + value that is true if the value is definitely null or false if it is + definitely not null. If this key isn't present at all then the value may or + may not be null. It's not actually useful to encode that an unknown value + is null; use a known null value instead in that case, because there is only + one null value of each type. +* `2` represents string prefix, and the value is a string that the final + value is known to begin with. This is valid only for unknown values of string + type. +* `3` and `4` represent the lower and upper bounds respectively of a number + value, and the value of both is a two-element msgpack array whose + first element is a valid encoding of a number (as in the table above) + and whose second element is a boolean value that is true for an inclusive + bound and false for an exclusive bound. This is valid only for unknown values + of number type. +* `5` and `6` represent the lower and upper bounds respectively of the length + of a collection value. The value of both is an integer representing an + inclusive bound. This is valid only for unknown values of the three kinds of + collection types: list, set, and map. + +Unknown value refinements are an optional way to reduce the range of possible +values for situations where that makes it possible to produce a known result +for unknown inputs or where it allows detecting an error during the planning +phase that would otherwise be detected only during the apply phase. It's always +safe to ignore refinements and just treat an unknown value as wholly unknown, +but considering refinements may allow a more precise answer. A provider that +produces refined values in its planned new state (from `PlanResourceChange`) +_must_ honor those refinements in the final state (from `ApplyResourceChange`). + +Unmarshalling code should ignore refinement map keys that they don't know about, +because future versions of the protocol might define additional refinements. + +When encoding an unknown value without any refinements, always use the older +format with extension code zero instead of using extension code 12 with an +empty refinement map. Any refined unknown value _must_ have at least one +refinement map entry. This rule ensures backward compatibility with older +implementations that predate the value refinements concept. + +A server implementation of the protocol should treat _any_ MessagePack extension +code as representing an unknown value, but should ignore the payload of that +extension value entirely unless the extension code is 12 to indicate that +the body represents refinements. Future versions of this protocol may define +specific formats for other extension codes, but they will always represent +unknown values. + ### `Schema.NestedBlock` Mapping Rules for MessagePack The MessagePack serialization of a collection of blocks of a particular type @@ -155,7 +212,7 @@ The properties representing nested block types have property values based on The JSON serialization of an attribute value depends on the value of the `type` field of the corresponding `Schema.Attribute` message. The `type` field is a compact JSON serialization of a -[Terraform type constraint](https://www.terraform.io/docs/configuration/types.html), +[Terraform type constraint](https://developer.hashicorp.com/terraform/language/expressions/type-constraints), which consists either of a single string value (for primitive types) or a two-element array giving a type kind and a type argument. diff --git a/docs/plugin-protocol/tfplugin5.0.proto b/docs/plugin-protocol/tfplugin5.0.proto deleted file mode 100644 index 624ad2a88b..0000000000 --- a/docs/plugin-protocol/tfplugin5.0.proto +++ /dev/null @@ -1,353 +0,0 @@ -// Terraform Plugin RPC protocol version 5.0 -// -// This file defines version 5.0 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will be updated in-place in the source Terraform repository for -// any minor versions of protocol 5, but later minor versions will always be -// backwards compatible. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; - -package tfplugin5; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message Stop { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -// Schema is the configuration schema for a Resource, Provider, or Provisioner. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - } - - message Attribute { - string name = 1; - bytes type = 2; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response); - rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response); - rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc Configure(Configure.Request) returns (Configure.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - } -} - -message PrepareProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - DynamicValue prepared_config = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceTypeConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataSourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message Configure { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 5; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 4; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} - -service Provisioner { - rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response); - rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response); - rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response); - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProvisionerSchema { - message Request { - } - message Response { - Schema provisioner = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateProvisionerConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ProvisionResource { - message Request { - DynamicValue config = 1; - DynamicValue connection = 2; - } - message Response { - string output = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin5.1.proto b/docs/plugin-protocol/tfplugin5.1.proto deleted file mode 100644 index 8f01ad96f3..0000000000 --- a/docs/plugin-protocol/tfplugin5.1.proto +++ /dev/null @@ -1,353 +0,0 @@ -// Terraform Plugin RPC protocol version 5.1 -// -// This file defines version 5.1 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will be updated in-place in the source Terraform repository for -// any minor versions of protocol 5, but later minor versions will always be -// backwards compatible. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; - -package tfplugin5; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message Stop { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -// Schema is the configuration schema for a Resource, Provider, or Provisioner. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - } - - message Attribute { - string name = 1; - bytes type = 2; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response); - rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response); - rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc Configure(Configure.Request) returns (Configure.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - } -} - -message PrepareProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - DynamicValue prepared_config = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceTypeConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataSourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message Configure { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 5; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 4; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} - -service Provisioner { - rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response); - rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response); - rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response); - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProvisionerSchema { - message Request { - } - message Response { - Schema provisioner = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateProvisionerConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ProvisionResource { - message Request { - DynamicValue config = 1; - DynamicValue connection = 2; - } - message Response { - string output = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin5.2.proto b/docs/plugin-protocol/tfplugin5.2.proto deleted file mode 100644 index 1c29f03965..0000000000 --- a/docs/plugin-protocol/tfplugin5.2.proto +++ /dev/null @@ -1,369 +0,0 @@ -// Terraform Plugin RPC protocol version 5.2 -// -// This file defines version 5.2 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will not be updated. Any minor versions of protocol 5 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; -option go_package = "github.com/hashicorp/terraform/internal/tfplugin5"; - -package tfplugin5; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message Stop { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -enum StringKind { - PLAIN = 0; - MARKDOWN = 1; -} - -// Schema is the configuration schema for a Resource, Provider, or Provisioner. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - string description = 4; - StringKind description_kind = 5; - bool deprecated = 6; - } - - message Attribute { - string name = 1; - bytes type = 2; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - StringKind description_kind = 8; - bool deprecated = 9; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response); - rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response); - rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc Configure(Configure.Request) returns (Configure.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - Schema provider_meta = 5; - } -} - -message PrepareProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - DynamicValue prepared_config = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceTypeConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataSourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message Configure { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - DynamicValue provider_meta = 4; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - DynamicValue provider_meta = 6; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 5; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - DynamicValue provider_meta = 6; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 4; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - DynamicValue provider_meta = 3; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} - -service Provisioner { - rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response); - rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response); - rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response); - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProvisionerSchema { - message Request { - } - message Response { - Schema provisioner = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateProvisionerConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ProvisionResource { - message Request { - DynamicValue config = 1; - DynamicValue connection = 2; - } - message Response { - string output = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin5.3.proto b/docs/plugin-protocol/tfplugin5.3.proto deleted file mode 100644 index 5fa53f2339..0000000000 --- a/docs/plugin-protocol/tfplugin5.3.proto +++ /dev/null @@ -1,381 +0,0 @@ -// Terraform Plugin RPC protocol version 5.3 -// -// This file defines version 5.3 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will not be updated. Any minor versions of protocol 5 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; -option go_package = "github.com/hashicorp/terraform/internal/tfplugin5"; - -package tfplugin5; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message Stop { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -enum StringKind { - PLAIN = 0; - MARKDOWN = 1; -} - -// Schema is the configuration schema for a Resource, Provider, or Provisioner. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - string description = 4; - StringKind description_kind = 5; - bool deprecated = 6; - } - - message Attribute { - string name = 1; - bytes type = 2; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - StringKind description_kind = 8; - bool deprecated = 9; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response); - rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response); - rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc Configure(Configure.Request) returns (Configure.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - Schema provider_meta = 5; - ServerCapabilities server_capabilities = 6; - } - - - // ServerCapabilities allows providers to communicate extra information - // regarding supported protocol features. This is used to indicate - // availability of certain forward-compatible changes which may be optional - // in a major protocol version, but cannot be tested for directly. - message ServerCapabilities { - // The plan_destroy capability signals that a provider expects a call - // to PlanResourceChange when a resource is going to be destroyed. - bool plan_destroy = 1; - } -} - -message PrepareProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - DynamicValue prepared_config = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceTypeConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataSourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message Configure { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - DynamicValue provider_meta = 4; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - DynamicValue provider_meta = 6; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 5; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - DynamicValue provider_meta = 6; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 4; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - DynamicValue provider_meta = 3; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} - -service Provisioner { - rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response); - rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response); - rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response); - rpc Stop(Stop.Request) returns (Stop.Response); -} - -message GetProvisionerSchema { - message Request { - } - message Response { - Schema provisioner = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateProvisionerConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ProvisionResource { - message Request { - DynamicValue config = 1; - DynamicValue connection = 2; - } - message Response { - string output = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin5.proto b/docs/plugin-protocol/tfplugin5.proto new file mode 100644 index 0000000000..a44af32fce --- /dev/null +++ b/docs/plugin-protocol/tfplugin5.proto @@ -0,0 +1,813 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Terraform Plugin RPC protocol version 5.9 +// +// This file defines version 5.9 of the RPC protocol. To implement a plugin +// against this protocol, copy this definition into your own codebase and +// use protoc to generate stubs for your target language. +// +// Any minor versions of protocol 5 to follow should modify this file while +// maintaining backwards compatibility. Breaking changes, if any are required, +// will come in a subsequent major version with its own separate proto definition. +// +// Note that only the proto files included in a release tag of Terraform are +// official protocol releases. Proto files taken from other commits may include +// incomplete changes or features that did not make it into a final release. +// In all reasonable cases, plugin developers should take the proto file from +// the tag of the most recent release of Terraform, and not from the main +// branch or any other development branch. +// +syntax = "proto3"; +option go_package = "github.com/hashicorp/terraform/internal/tfplugin5"; + +import "google/protobuf/timestamp.proto"; + +package tfplugin5; + +// DynamicValue is an opaque encoding of terraform data, with the field name +// indicating the encoding scheme used. +message DynamicValue { + bytes msgpack = 1; + bytes json = 2; +} + +message Diagnostic { + enum Severity { + INVALID = 0; + ERROR = 1; + WARNING = 2; + } + Severity severity = 1; + string summary = 2; + string detail = 3; + AttributePath attribute = 4; +} + +message FunctionError { + string text = 1; + // The optional function_argument records the index position of the + // argument which caused the error. + optional int64 function_argument = 2; +} + +message AttributePath { + message Step { + oneof selector { + // Set "attribute_name" to represent looking up an attribute + // in the current object value. + string attribute_name = 1; + // Set "element_key_*" to represent looking up an element in + // an indexable collection type. + string element_key_string = 2; + int64 element_key_int = 3; + } + } + repeated Step steps = 1; +} + +message Stop { + message Request { + } + message Response { + string Error = 1; + } +} + +// RawState holds the stored state for a resource to be upgraded by the +// provider. It can be in one of two formats, the current json encoded format +// in bytes, or the legacy flatmap format as a map of strings. +message RawState { + bytes json = 1; + map flatmap = 2; +} + +enum StringKind { + PLAIN = 0; + MARKDOWN = 1; +} + +// ResourceIdentitySchema represents the structure and types of data used to identify +// a managed resource type. Effectively, resource identity is a versioned object +// that can be used to compare resources, whether already managed and/or being +// discovered. +message ResourceIdentitySchema { + // IdentityAttribute represents one value of data within resource identity. These + // are always used in resource identity comparisons. + message IdentityAttribute { + // name is the identity attribute name + string name = 1; + + // type is the identity attribute type + bytes type = 2; + + // required_for_import when enabled signifies that this attribute must be + // defined for ImportResourceState to complete successfully + bool required_for_import = 3; + + // optional_for_import when enabled signifies that this attribute is not + // required for ImportResourceState, because it can be supplied by the + // provider. It is still possible to supply this attribute during import. + bool optional_for_import = 4; + + // description is a human-readable description of the attribute in Markdown + string description = 5; + } + + // version is the identity version and separate from the Schema version. + // Any time the structure or format of identity_attributes changes, this version + // should be incremented. Versioning implicitly starts at 0 and by convention + // should be incremented by 1 each change. + // + // When comparing identity_attributes data, differing versions should always be treated + // as inequal. + int64 version = 1; + + // identity_attributes are the individual value definitions which define identity data + // for a managed resource type. This information is used to decode DynamicValue of + // identity data. + // + // These attributes are intended for permanent identity data and must be wholly + // representative of all data necessary to compare two managed resource instances + // with no other data. This generally should include account, endpoint, location, + // and automatically generated identifiers. For some resources, this may include + // configuration-based data, such as a required name which must be unique. + repeated IdentityAttribute identity_attributes = 2; +} + +message ResourceIdentityData { + // identity_data is the resource identity data for the given definition. It should + // be decoded using the identity schema. + // + // This data is considered permanent for the identity version and suitable for + // longer-term storage. + DynamicValue identity_data = 1; +} + +// Schema is the configuration schema for a Resource, Provider, or Provisioner. +message Schema { + message Block { + int64 version = 1; + repeated Attribute attributes = 2; + repeated NestedBlock block_types = 3; + string description = 4; + StringKind description_kind = 5; + bool deprecated = 6; + } + + message Attribute { + string name = 1; + bytes type = 2; + string description = 3; + bool required = 4; + bool optional = 5; + bool computed = 6; + bool sensitive = 7; + StringKind description_kind = 8; + bool deprecated = 9; + bool write_only = 10; + } + + message NestedBlock { + enum NestingMode { + INVALID = 0; + SINGLE = 1; + LIST = 2; + SET = 3; + MAP = 4; + GROUP = 5; + } + + string type_name = 1; + Block block = 2; + NestingMode nesting = 3; + int64 min_items = 4; + int64 max_items = 5; + } + + // The version of the schema. + // Schemas are versioned, so that providers can upgrade a saved resource + // state when the schema is changed. + int64 version = 1; + + // Block is the top level configuration block for this schema. + Block block = 2; +} + +// ServerCapabilities allows providers to communicate extra information +// regarding supported protocol features. This is used to indicate +// availability of certain forward-compatible changes which may be optional +// in a major protocol version, but cannot be tested for directly. +message ServerCapabilities { + // The plan_destroy capability signals that a provider expects a call + // to PlanResourceChange when a resource is going to be destroyed. + bool plan_destroy = 1; + + // The get_provider_schema_optional capability indicates that this + // provider does not require calling GetProviderSchema to operate + // normally, and the caller can used a cached copy of the provider's + // schema. + bool get_provider_schema_optional = 2; + + // The move_resource_state capability signals that a provider supports the + // MoveResourceState RPC. + bool move_resource_state = 3; +} + +// ClientCapabilities allows Terraform to publish information regarding +// supported protocol features. This is used to indicate availability of +// certain forward-compatible changes which may be optional in a major +// protocol version, but cannot be tested for directly. +message ClientCapabilities { + // The deferral_allowed capability signals that the client is able to + // handle deferred responses from the provider. + bool deferral_allowed = 1; + + // The write_only_attributes_allowed capability signals that the client + // is able to handle write_only attributes for managed resources. + bool write_only_attributes_allowed = 2; +} + +message Function { + // parameters is the ordered list of positional function parameters. + repeated Parameter parameters = 1; + + // variadic_parameter is an optional final parameter which accepts + // zero or more argument values, in which Terraform will send an + // ordered list of the parameter type. + Parameter variadic_parameter = 2; + + // Return is the function return parameter. + Return return = 3; + + // summary is the human-readable shortened documentation for the function. + string summary = 4; + + // description is human-readable documentation for the function. + string description = 5; + + // description_kind is the formatting of the description. + StringKind description_kind = 6; + + // deprecation_message is human-readable documentation if the + // function is deprecated. + string deprecation_message = 7; + + message Parameter { + // name is the human-readable display name for the parameter. + string name = 1; + + // type is the type constraint for the parameter. + bytes type = 2; + + // allow_null_value when enabled denotes that a null argument value can + // be passed to the provider. When disabled, Terraform returns an error + // if the argument value is null. + bool allow_null_value = 3; + + // allow_unknown_values when enabled denotes that only wholly known + // argument values will be passed to the provider. When disabled, + // Terraform skips the function call entirely and assumes an unknown + // value result from the function. + bool allow_unknown_values = 4; + + // description is human-readable documentation for the parameter. + string description = 5; + + // description_kind is the formatting of the description. + StringKind description_kind = 6; + } + + message Return { + // type is the type constraint for the function result. + bytes type = 1; + } +} + +// Deferred is a message that indicates that change is deferred for a reason. +message Deferred { + // Reason is the reason for deferring the change. + enum Reason { + // UNKNOWN is the default value, and should not be used. + UNKNOWN = 0; + // RESOURCE_CONFIG_UNKNOWN is used when the config is partially unknown and the real + // values need to be known before the change can be planned. + RESOURCE_CONFIG_UNKNOWN = 1; + // PROVIDER_CONFIG_UNKNOWN is used when parts of the provider configuration + // are unknown, e.g. the provider configuration is only known after the apply is done. + PROVIDER_CONFIG_UNKNOWN = 2; + // ABSENT_PREREQ is used when a hard dependency has not been satisfied. + ABSENT_PREREQ = 3; + } + + // reason is the reason for deferring the change. + Reason reason = 1; +} + + +service Provider { + //////// Information about what a provider supports/expects + + // GetMetadata returns upfront information about server capabilities and + // supported resource types without requiring the server to instantiate all + // schema information, which may be memory intensive. This RPC is optional, + // where clients may receive an unimplemented RPC error. Clients should + // ignore the error and call the GetSchema RPC as a fallback. + rpc GetMetadata(GetMetadata.Request) returns (GetMetadata.Response); + + // GetSchema returns schema information for the provider, data resources, + // and managed resources. + rpc GetSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); + rpc PrepareProviderConfig(PrepareProviderConfig.Request) returns (PrepareProviderConfig.Response); + rpc ValidateResourceTypeConfig(ValidateResourceTypeConfig.Request) returns (ValidateResourceTypeConfig.Response); + rpc ValidateDataSourceConfig(ValidateDataSourceConfig.Request) returns (ValidateDataSourceConfig.Response); + rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); + + // GetResourceIdentitySchemas returns the identity schemas for all managed + // resources. + rpc GetResourceIdentitySchemas(GetResourceIdentitySchemas.Request) returns (GetResourceIdentitySchemas.Response); + // UpgradeResourceIdentityData should return the upgraded resource identity + // data for a managed resource type. + rpc UpgradeResourceIdentity(UpgradeResourceIdentity.Request) returns (UpgradeResourceIdentity.Response); + + //////// One-time initialization, called before other functions below + rpc Configure(Configure.Request) returns (Configure.Response); + + //////// Managed Resource Lifecycle + rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); + rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); + rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); + rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); + rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response); + rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); + + //////// Ephemeral Resource Lifecycle + rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response); + rpc OpenEphemeralResource(OpenEphemeralResource.Request) returns (OpenEphemeralResource.Response); + rpc RenewEphemeralResource(RenewEphemeralResource.Request) returns (RenewEphemeralResource.Response); + rpc CloseEphemeralResource(CloseEphemeralResource.Request) returns (CloseEphemeralResource.Response); + + // GetFunctions returns the definitions of all functions. + rpc GetFunctions(GetFunctions.Request) returns (GetFunctions.Response); + + //////// Provider-contributed Functions + rpc CallFunction(CallFunction.Request) returns (CallFunction.Response); + + //////// Graceful Shutdown + rpc Stop(Stop.Request) returns (Stop.Response); +} + +message GetMetadata { + message Request { + } + + message Response { + ServerCapabilities server_capabilities = 1; + repeated Diagnostic diagnostics = 2; + repeated DataSourceMetadata data_sources = 3; + repeated ResourceMetadata resources = 4; + // functions returns metadata for any functions. + repeated FunctionMetadata functions = 5; + repeated EphemeralMetadata ephemeral_resources = 6; + } + + message EphemeralMetadata { + string type_name = 1; + } + + message FunctionMetadata { + // name is the function name. + string name = 1; + } + + message DataSourceMetadata { + string type_name = 1; + } + + message ResourceMetadata { + string type_name = 1; + } +} + +message GetProviderSchema { + message Request { + } + message Response { + Schema provider = 1; + map resource_schemas = 2; + map data_source_schemas = 3; + map functions = 7; + map ephemeral_resource_schemas = 8; + repeated Diagnostic diagnostics = 4; + Schema provider_meta = 5; + ServerCapabilities server_capabilities = 6; + } +} + +message PrepareProviderConfig { + message Request { + DynamicValue config = 1; + } + message Response { + DynamicValue prepared_config = 1; + repeated Diagnostic diagnostics = 2; + } +} + +message UpgradeResourceState { + // Request is the message that is sent to the provider during the + // UpgradeResourceState RPC. + // + // This message intentionally does not include configuration data as any + // configuration-based or configuration-conditional changes should occur + // during the PlanResourceChange RPC. Additionally, the configuration is + // not guaranteed to exist (in the case of resource destruction), be wholly + // known, nor match the given prior state, which could lead to unexpected + // provider behaviors for practitioners. + message Request { + string type_name = 1; + + // version is the schema_version number recorded in the state file + int64 version = 2; + + // raw_state is the raw states as stored for the resource. Core does + // not have access to the schema of prior_version, so it's the + // provider's responsibility to interpret this value using the + // appropriate older schema. The raw_state will be the json encoded + // state, or a legacy flat-mapped format. + RawState raw_state = 3; + } + message Response { + // new_state is a msgpack-encoded data structure that, when interpreted with + // the _current_ schema for this resource type, is functionally equivalent to + // that which was given in prior_state_raw. + DynamicValue upgraded_state = 1; + + // diagnostics describes any errors encountered during migration that could not + // be safely resolved, and warnings about any possibly-risky assumptions made + // in the upgrade process. + repeated Diagnostic diagnostics = 2; + } +} + +message GetResourceIdentitySchemas { + message Request { + } + + message Response { + // identity_schemas is a mapping of resource type names to their identity schemas. + map identity_schemas = 1; + + // diagnostics is the collection of warning and error diagnostics for this request. + repeated Diagnostic diagnostics = 2; + } +} + +message UpgradeResourceIdentity { + message Request { + // type_name is the managed resource type name + string type_name = 1; + + // version is the version of the resource identity data to upgrade + int64 version = 2; + + // raw_identity is the raw identity as stored for the resource. Core does + // not have access to the identity schema of prior_version, so it's the + // provider's responsibility to interpret this value using the + // appropriate older schema. The raw_identity will be json encoded. + RawState raw_identity = 3; + } + + message Response { + // upgraded_identity returns the upgraded resource identity data + ResourceIdentityData upgraded_identity = 1; + + // diagnostics is the collection of warning and error diagnostics for this request + repeated Diagnostic diagnostics = 2; + } +} + +message ValidateResourceTypeConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + ClientCapabilities client_capabilities = 3; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ValidateDataSourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ValidateEphemeralResourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message Configure { + message Request { + string terraform_version = 1; + DynamicValue config = 2; + ClientCapabilities client_capabilities = 3; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ReadResource { + // Request is the message that is sent to the provider during the + // ReadResource RPC. + // + // This message intentionally does not include configuration data as any + // configuration-based or configuration-conditional changes should occur + // during the PlanResourceChange RPC. Additionally, the configuration is + // not guaranteed to be wholly known nor match the given prior state, which + // could lead to unexpected provider behaviors for practitioners. + message Request { + string type_name = 1; + DynamicValue current_state = 2; + bytes private = 3; + DynamicValue provider_meta = 4; + ClientCapabilities client_capabilities = 5; + ResourceIdentityData current_identity = 6; + } + message Response { + DynamicValue new_state = 1; + repeated Diagnostic diagnostics = 2; + bytes private = 3; + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 4; + ResourceIdentityData new_identity = 5; + } +} + +message PlanResourceChange { + message Request { + string type_name = 1; + DynamicValue prior_state = 2; + DynamicValue proposed_new_state = 3; + DynamicValue config = 4; + bytes prior_private = 5; + DynamicValue provider_meta = 6; + ClientCapabilities client_capabilities = 7; + ResourceIdentityData prior_identity = 8; + } + + message Response { + DynamicValue planned_state = 1; + repeated AttributePath requires_replace = 2; + bytes planned_private = 3; + repeated Diagnostic diagnostics = 4; + + + // This may be set only by the helper/schema "SDK" in the main Terraform + // repository, to request that Terraform Core >=0.12 permit additional + // inconsistencies that can result from the legacy SDK type system + // and its imprecise mapping to the >=0.12 type system. + // The change in behavior implied by this flag makes sense only for the + // specific details of the legacy SDK type system, and are not a general + // mechanism to avoid proper type handling in providers. + // + // ==== DO NOT USE THIS ==== + // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== + // ==== DO NOT USE THIS ==== + bool legacy_type_system = 5; + + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 6; + + ResourceIdentityData planned_identity = 7; + } +} + +message ApplyResourceChange { + message Request { + string type_name = 1; + DynamicValue prior_state = 2; + DynamicValue planned_state = 3; + DynamicValue config = 4; + bytes planned_private = 5; + DynamicValue provider_meta = 6; + ResourceIdentityData planned_identity = 7; + } + message Response { + DynamicValue new_state = 1; + bytes private = 2; + repeated Diagnostic diagnostics = 3; + + // This may be set only by the helper/schema "SDK" in the main Terraform + // repository, to request that Terraform Core >=0.12 permit additional + // inconsistencies that can result from the legacy SDK type system + // and its imprecise mapping to the >=0.12 type system. + // The change in behavior implied by this flag makes sense only for the + // specific details of the legacy SDK type system, and are not a general + // mechanism to avoid proper type handling in providers. + // + // ==== DO NOT USE THIS ==== + // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== + // ==== DO NOT USE THIS ==== + bool legacy_type_system = 4; + + ResourceIdentityData new_identity = 5; + } +} + +message ImportResourceState { + message Request { + string type_name = 1; + string id = 2; + ClientCapabilities client_capabilities = 3; + ResourceIdentityData identity = 4; + } + + message ImportedResource { + string type_name = 1; + DynamicValue state = 2; + bytes private = 3; + ResourceIdentityData identity = 4; + } + + message Response { + repeated ImportedResource imported_resources = 1; + repeated Diagnostic diagnostics = 2; + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 3; + } +} + +message MoveResourceState { + message Request { + // The address of the provider the resource is being moved from. + string source_provider_address = 1; + + // The resource type that the resource is being moved from. + string source_type_name = 2; + + // The schema version of the resource type that the resource is being + // moved from. + int64 source_schema_version = 3; + + // The raw state of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + RawState source_state = 4; + + // The resource type that the resource is being moved to. + string target_type_name = 5; + + // The private state of the resource being moved. + bytes source_private = 6; + + // The raw identity of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + RawState source_identity = 7; + + // The identity schema version of the resource type that the resource + // is being moved from. + int64 source_identity_schema_version = 8; + } + + message Response { + // The state of the resource after it has been moved. + DynamicValue target_state = 1; + + // Any diagnostics that occurred during the move. + repeated Diagnostic diagnostics = 2; + + // The private state of the resource after it has been moved. + bytes target_private = 3; + + ResourceIdentityData target_identity = 4; + } +} + +message ReadDataSource { + message Request { + string type_name = 1; + DynamicValue config = 2; + DynamicValue provider_meta = 3; + ClientCapabilities client_capabilities = 4; + } + message Response { + DynamicValue state = 1; + repeated Diagnostic diagnostics = 2; + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 3; + } +} + +service Provisioner { + rpc GetSchema(GetProvisionerSchema.Request) returns (GetProvisionerSchema.Response); + rpc ValidateProvisionerConfig(ValidateProvisionerConfig.Request) returns (ValidateProvisionerConfig.Response); + rpc ProvisionResource(ProvisionResource.Request) returns (stream ProvisionResource.Response); + rpc Stop(Stop.Request) returns (Stop.Response); +} + +message GetProvisionerSchema { + message Request { + } + message Response { + Schema provisioner = 1; + repeated Diagnostic diagnostics = 2; + } +} + +message ValidateProvisionerConfig { + message Request { + DynamicValue config = 1; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ProvisionResource { + message Request { + DynamicValue config = 1; + DynamicValue connection = 2; + } + message Response { + string output = 1; + repeated Diagnostic diagnostics = 2; + } +} + +message OpenEphemeralResource { + message Request { + string type_name = 1; + DynamicValue config = 2; + ClientCapabilities client_capabilities = 3; + } + message Response { + repeated Diagnostic diagnostics = 1; + optional google.protobuf.Timestamp renew_at = 2; + DynamicValue result = 3; + optional bytes private = 4; + Deferred deferred = 5; + } +} + +message RenewEphemeralResource { + message Request { + string type_name = 1; + optional bytes private = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + optional google.protobuf.Timestamp renew_at = 2; + optional bytes private = 3; + } +} + +message CloseEphemeralResource { + message Request { + string type_name = 1; + optional bytes private = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message GetFunctions { + message Request {} + + message Response { + // functions is a mapping of function names to definitions. + map functions = 1; + + // diagnostics is any warnings or errors. + repeated Diagnostic diagnostics = 2; + } +} + +message CallFunction { + message Request { + string name = 1; + repeated DynamicValue arguments = 2; + } + message Response { + DynamicValue result = 1; + FunctionError error = 2; + } +} diff --git a/docs/plugin-protocol/tfplugin6.0.proto b/docs/plugin-protocol/tfplugin6.0.proto deleted file mode 100644 index 4d8dc060e1..0000000000 --- a/docs/plugin-protocol/tfplugin6.0.proto +++ /dev/null @@ -1,321 +0,0 @@ -// Terraform Plugin RPC protocol version 6.0 -// -// This file defines version 6.0 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will not be updated. Any minor versions of protocol 6 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; -option go_package = "github.com/hashicorp/terraform/internal/tfplugin6"; - -package tfplugin6; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message StopProvider { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -enum StringKind { - PLAIN = 0; - MARKDOWN = 1; -} - -// Schema is the configuration schema for a Resource or Provider. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - string description = 4; - StringKind description_kind = 5; - bool deprecated = 6; - } - - message Attribute { - string name = 1; - bytes type = 2; - Object nested_type = 10; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - StringKind description_kind = 8; - bool deprecated = 9; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - message Object { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - } - - repeated Attribute attributes = 1; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response); - rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response); - rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc StopProvider(StopProvider.Request) returns (StopProvider.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - Schema provider_meta = 5; - } -} - -message ValidateProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ConfigureProvider { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - DynamicValue provider_meta = 4; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - DynamicValue provider_meta = 6; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - DynamicValue provider_meta = 6; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - DynamicValue provider_meta = 3; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin6.1.proto b/docs/plugin-protocol/tfplugin6.1.proto deleted file mode 100644 index 3f6dead35e..0000000000 --- a/docs/plugin-protocol/tfplugin6.1.proto +++ /dev/null @@ -1,324 +0,0 @@ -// Terraform Plugin RPC protocol version 6.1 -// -// This file defines version 6.1 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will not be updated. Any minor versions of protocol 6 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; -option go_package = "github.com/hashicorp/terraform/internal/tfplugin6"; - -package tfplugin6; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message StopProvider { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -enum StringKind { - PLAIN = 0; - MARKDOWN = 1; -} - -// Schema is the configuration schema for a Resource or Provider. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - string description = 4; - StringKind description_kind = 5; - bool deprecated = 6; - } - - message Attribute { - string name = 1; - bytes type = 2; - Object nested_type = 10; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - StringKind description_kind = 8; - bool deprecated = 9; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - message Object { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - } - - repeated Attribute attributes = 1; - NestingMode nesting = 3; - - // MinItems and MaxItems were never used in the protocol, and have no - // effect on validation. - int64 min_items = 4 [deprecated = true]; - int64 max_items = 5 [deprecated = true]; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response); - rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response); - rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc StopProvider(StopProvider.Request) returns (StopProvider.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - Schema provider_meta = 5; - } -} - -message ValidateProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ConfigureProvider { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - DynamicValue provider_meta = 4; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - DynamicValue provider_meta = 6; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - DynamicValue provider_meta = 6; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - DynamicValue provider_meta = 3; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin6.2.proto b/docs/plugin-protocol/tfplugin6.2.proto deleted file mode 100644 index da5e58eafe..0000000000 --- a/docs/plugin-protocol/tfplugin6.2.proto +++ /dev/null @@ -1,350 +0,0 @@ -// Terraform Plugin RPC protocol version 6.2 -// -// This file defines version 6.2 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will not be updated. Any minor versions of protocol 6 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; -option go_package = "github.com/hashicorp/terraform/internal/tfplugin6"; - -package tfplugin6; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message StopProvider { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -enum StringKind { - PLAIN = 0; - MARKDOWN = 1; -} - -// Schema is the configuration schema for a Resource or Provider. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - string description = 4; - StringKind description_kind = 5; - bool deprecated = 6; - } - - message Attribute { - string name = 1; - bytes type = 2; - Object nested_type = 10; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - StringKind description_kind = 8; - bool deprecated = 9; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - message Object { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - } - - repeated Attribute attributes = 1; - NestingMode nesting = 3; - - // MinItems and MaxItems were never used in the protocol, and have no - // effect on validation. - int64 min_items = 4 [deprecated = true]; - int64 max_items = 5 [deprecated = true]; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response); - rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response); - rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc StopProvider(StopProvider.Request) returns (StopProvider.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - Schema provider_meta = 5; - } -} - -message ValidateProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ConfigureProvider { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - DynamicValue provider_meta = 4; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - DynamicValue provider_meta = 6; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 5; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - DynamicValue provider_meta = 6; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 4; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - DynamicValue provider_meta = 3; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin6.3.proto b/docs/plugin-protocol/tfplugin6.3.proto deleted file mode 100644 index b87effe434..0000000000 --- a/docs/plugin-protocol/tfplugin6.3.proto +++ /dev/null @@ -1,362 +0,0 @@ -// Terraform Plugin RPC protocol version 6.3 -// -// This file defines version 6.3 of the RPC protocol. To implement a plugin -// against this protocol, copy this definition into your own codebase and -// use protoc to generate stubs for your target language. -// -// This file will not be updated. Any minor versions of protocol 6 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. -// -// Note that only the proto files included in a release tag of Terraform are -// official protocol releases. Proto files taken from other commits may include -// incomplete changes or features that did not make it into a final release. -// In all reasonable cases, plugin developers should take the proto file from -// the tag of the most recent release of Terraform, and not from the main -// branch or any other development branch. -// -syntax = "proto3"; -option go_package = "github.com/hashicorp/terraform/internal/tfplugin6"; - -package tfplugin6; - -// DynamicValue is an opaque encoding of terraform data, with the field name -// indicating the encoding scheme used. -message DynamicValue { - bytes msgpack = 1; - bytes json = 2; -} - -message Diagnostic { - enum Severity { - INVALID = 0; - ERROR = 1; - WARNING = 2; - } - Severity severity = 1; - string summary = 2; - string detail = 3; - AttributePath attribute = 4; -} - -message AttributePath { - message Step { - oneof selector { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - string attribute_name = 1; - // Set "element_key_*" to represent looking up an element in - // an indexable collection type. - string element_key_string = 2; - int64 element_key_int = 3; - } - } - repeated Step steps = 1; -} - -message StopProvider { - message Request { - } - message Response { - string Error = 1; - } -} - -// RawState holds the stored state for a resource to be upgraded by the -// provider. It can be in one of two formats, the current json encoded format -// in bytes, or the legacy flatmap format as a map of strings. -message RawState { - bytes json = 1; - map flatmap = 2; -} - -enum StringKind { - PLAIN = 0; - MARKDOWN = 1; -} - -// Schema is the configuration schema for a Resource or Provider. -message Schema { - message Block { - int64 version = 1; - repeated Attribute attributes = 2; - repeated NestedBlock block_types = 3; - string description = 4; - StringKind description_kind = 5; - bool deprecated = 6; - } - - message Attribute { - string name = 1; - bytes type = 2; - Object nested_type = 10; - string description = 3; - bool required = 4; - bool optional = 5; - bool computed = 6; - bool sensitive = 7; - StringKind description_kind = 8; - bool deprecated = 9; - } - - message NestedBlock { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - GROUP = 5; - } - - string type_name = 1; - Block block = 2; - NestingMode nesting = 3; - int64 min_items = 4; - int64 max_items = 5; - } - - message Object { - enum NestingMode { - INVALID = 0; - SINGLE = 1; - LIST = 2; - SET = 3; - MAP = 4; - } - - repeated Attribute attributes = 1; - NestingMode nesting = 3; - - // MinItems and MaxItems were never used in the protocol, and have no - // effect on validation. - int64 min_items = 4 [deprecated = true]; - int64 max_items = 5 [deprecated = true]; - } - - // The version of the schema. - // Schemas are versioned, so that providers can upgrade a saved resource - // state when the schema is changed. - int64 version = 1; - - // Block is the top level configuration block for this schema. - Block block = 2; -} - -service Provider { - //////// Information about what a provider supports/expects - rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); - rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response); - rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response); - rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response); - rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); - - //////// One-time initialization, called before other functions below - rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response); - - //////// Managed Resource Lifecycle - rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); - rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); - rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); - rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); - - rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); - - //////// Graceful Shutdown - rpc StopProvider(StopProvider.Request) returns (StopProvider.Response); -} - -message GetProviderSchema { - message Request { - } - message Response { - Schema provider = 1; - map resource_schemas = 2; - map data_source_schemas = 3; - repeated Diagnostic diagnostics = 4; - Schema provider_meta = 5; - ServerCapabilities server_capabilities = 6; - } - - - // ServerCapabilities allows providers to communicate extra information - // regarding supported protocol features. This is used to indicate - // availability of certain forward-compatible changes which may be optional - // in a major protocol version, but cannot be tested for directly. - message ServerCapabilities { - // The plan_destroy capability signals that a provider expects a call - // to PlanResourceChange when a resource is going to be destroyed. - bool plan_destroy = 1; - } -} - -message ValidateProviderConfig { - message Request { - DynamicValue config = 1; - } - message Response { - repeated Diagnostic diagnostics = 2; - } -} - -message UpgradeResourceState { - message Request { - string type_name = 1; - - // version is the schema_version number recorded in the state file - int64 version = 2; - - // raw_state is the raw states as stored for the resource. Core does - // not have access to the schema of prior_version, so it's the - // provider's responsibility to interpret this value using the - // appropriate older schema. The raw_state will be the json encoded - // state, or a legacy flat-mapped format. - RawState raw_state = 3; - } - message Response { - // new_state is a msgpack-encoded data structure that, when interpreted with - // the _current_ schema for this resource type, is functionally equivalent to - // that which was given in prior_state_raw. - DynamicValue upgraded_state = 1; - - // diagnostics describes any errors encountered during migration that could not - // be safely resolved, and warnings about any possibly-risky assumptions made - // in the upgrade process. - repeated Diagnostic diagnostics = 2; - } -} - -message ValidateResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ValidateDataResourceConfig { - message Request { - string type_name = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ConfigureProvider { - message Request { - string terraform_version = 1; - DynamicValue config = 2; - } - message Response { - repeated Diagnostic diagnostics = 1; - } -} - -message ReadResource { - message Request { - string type_name = 1; - DynamicValue current_state = 2; - bytes private = 3; - DynamicValue provider_meta = 4; - } - message Response { - DynamicValue new_state = 1; - repeated Diagnostic diagnostics = 2; - bytes private = 3; - } -} - -message PlanResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue proposed_new_state = 3; - DynamicValue config = 4; - bytes prior_private = 5; - DynamicValue provider_meta = 6; - } - - message Response { - DynamicValue planned_state = 1; - repeated AttributePath requires_replace = 2; - bytes planned_private = 3; - repeated Diagnostic diagnostics = 4; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 5; - } -} - -message ApplyResourceChange { - message Request { - string type_name = 1; - DynamicValue prior_state = 2; - DynamicValue planned_state = 3; - DynamicValue config = 4; - bytes planned_private = 5; - DynamicValue provider_meta = 6; - } - message Response { - DynamicValue new_state = 1; - bytes private = 2; - repeated Diagnostic diagnostics = 3; - - // This may be set only by the helper/schema "SDK" in the main Terraform - // repository, to request that Terraform Core >=0.12 permit additional - // inconsistencies that can result from the legacy SDK type system - // and its imprecise mapping to the >=0.12 type system. - // The change in behavior implied by this flag makes sense only for the - // specific details of the legacy SDK type system, and are not a general - // mechanism to avoid proper type handling in providers. - // - // ==== DO NOT USE THIS ==== - // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== - // ==== DO NOT USE THIS ==== - bool legacy_type_system = 4; - } -} - -message ImportResourceState { - message Request { - string type_name = 1; - string id = 2; - } - - message ImportedResource { - string type_name = 1; - DynamicValue state = 2; - bytes private = 3; - } - - message Response { - repeated ImportedResource imported_resources = 1; - repeated Diagnostic diagnostics = 2; - } -} - -message ReadDataSource { - message Request { - string type_name = 1; - DynamicValue config = 2; - DynamicValue provider_meta = 3; - } - message Response { - DynamicValue state = 1; - repeated Diagnostic diagnostics = 2; - } -} diff --git a/docs/plugin-protocol/tfplugin6.proto b/docs/plugin-protocol/tfplugin6.proto new file mode 100644 index 0000000000..441c8da1e4 --- /dev/null +++ b/docs/plugin-protocol/tfplugin6.proto @@ -0,0 +1,793 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Terraform Plugin RPC protocol version 6.9 +// +// This file defines version 6.9 of the RPC protocol. To implement a plugin +// against this protocol, copy this definition into your own codebase and +// use protoc to generate stubs for your target language. +// +// Any minor versions of protocol 6 to follow should modify this file while +// maintaining backwards compatibility. Breaking changes, if any are required, +// will come in a subsequent major version with its own separate proto definition. +// +// Note that only the proto files included in a release tag of Terraform are +// official protocol releases. Proto files taken from other commits may include +// incomplete changes or features that did not make it into a final release. +// In all reasonable cases, plugin developers should take the proto file from +// the tag of the most recent release of Terraform, and not from the main +// branch or any other development branch. +// +syntax = "proto3"; +option go_package = "github.com/hashicorp/terraform/internal/tfplugin6"; + +import "google/protobuf/timestamp.proto"; + +package tfplugin6; + +// DynamicValue is an opaque encoding of terraform data, with the field name +// indicating the encoding scheme used. +message DynamicValue { + bytes msgpack = 1; + bytes json = 2; +} + +message Diagnostic { + enum Severity { + INVALID = 0; + ERROR = 1; + WARNING = 2; + } + Severity severity = 1; + string summary = 2; + string detail = 3; + AttributePath attribute = 4; +} + +message FunctionError { + string text = 1; + // The optional function_argument records the index position of the + // argument which caused the error. + optional int64 function_argument = 2; +} + +message AttributePath { + message Step { + oneof selector { + // Set "attribute_name" to represent looking up an attribute + // in the current object value. + string attribute_name = 1; + // Set "element_key_*" to represent looking up an element in + // an indexable collection type. + string element_key_string = 2; + int64 element_key_int = 3; + } + } + repeated Step steps = 1; +} + +message StopProvider { + message Request { + } + message Response { + string Error = 1; + } +} + +// RawState holds the stored state for a resource to be upgraded by the +// provider. It can be in one of two formats, the current json encoded format +// in bytes, or the legacy flatmap format as a map of strings. +message RawState { + bytes json = 1; + map flatmap = 2; +} + +enum StringKind { + PLAIN = 0; + MARKDOWN = 1; +} + +// ResourceIdentitySchema represents the structure and types of data used to identify +// a managed resource type. Effectively, resource identity is a versioned object +// that can be used to compare resources, whether already managed and/or being +// discovered. +message ResourceIdentitySchema { + // IdentityAttribute represents one value of data within resource identity. These + // are always used in resource identity comparisons. + message IdentityAttribute { + // name is the identity attribute name + string name = 1; + + // type is the identity attribute type + bytes type = 2; + + // required_for_import when enabled signifies that this attribute must be + // defined for ImportResourceState to complete successfully + bool required_for_import = 3; + + // optional_for_import when enabled signifies that this attribute is not + // required for ImportResourceState, because it can be supplied by the + // provider. It is still possible to supply this attribute during import. + bool optional_for_import = 4; + + // description is a human-readable description of the attribute in Markdown + string description = 5; + } + + // version is the identity version and separate from the Schema version. + // Any time the structure or format of identity_attributes changes, this version + // should be incremented. Versioning implicitly starts at 0 and by convention + // should be incremented by 1 each change. + // + // When comparing identity_attributes data, differing versions should always be treated + // as inequal. + int64 version = 1; + + // identity_attributes are the individual value definitions which define identity data + // for a managed resource type. This information is used to decode DynamicValue of + // identity data. + // + // These attributes are intended for permanent identity data and must be wholly + // representative of all data necessary to compare two managed resource instances + // with no other data. This generally should include account, endpoint, location, + // and automatically generated identifiers. For some resources, this may include + // configuration-based data, such as a required name which must be unique. + repeated IdentityAttribute identity_attributes = 2; +} + +message ResourceIdentityData { + // identity_data is the resource identity data for the given definition. It should + // be decoded using the identity schema. + // + // This data is considered permanent for the identity version and suitable for + // longer-term storage. + DynamicValue identity_data = 1; +} + +// Schema is the configuration schema for a Resource or Provider. +message Schema { + message Block { + int64 version = 1; + repeated Attribute attributes = 2; + repeated NestedBlock block_types = 3; + string description = 4; + StringKind description_kind = 5; + bool deprecated = 6; + } + + message Attribute { + string name = 1; + bytes type = 2; + Object nested_type = 10; + string description = 3; + bool required = 4; + bool optional = 5; + bool computed = 6; + bool sensitive = 7; + StringKind description_kind = 8; + bool deprecated = 9; + bool write_only = 11; + } + + message NestedBlock { + enum NestingMode { + INVALID = 0; + SINGLE = 1; + LIST = 2; + SET = 3; + MAP = 4; + GROUP = 5; + } + + string type_name = 1; + Block block = 2; + NestingMode nesting = 3; + int64 min_items = 4; + int64 max_items = 5; + } + + message Object { + enum NestingMode { + INVALID = 0; + SINGLE = 1; + LIST = 2; + SET = 3; + MAP = 4; + } + + repeated Attribute attributes = 1; + NestingMode nesting = 3; + + // MinItems and MaxItems were never used in the protocol, and have no + // effect on validation. + int64 min_items = 4 [deprecated = true]; + int64 max_items = 5 [deprecated = true]; + } + + // The version of the schema. + // Schemas are versioned, so that providers can upgrade a saved resource + // state when the schema is changed. + int64 version = 1; + + // Block is the top level configuration block for this schema. + Block block = 2; +} + +message Function { + // parameters is the ordered list of positional function parameters. + repeated Parameter parameters = 1; + + // variadic_parameter is an optional final parameter which accepts + // zero or more argument values, in which Terraform will send an + // ordered list of the parameter type. + Parameter variadic_parameter = 2; + + // Return is the function return parameter. + Return return = 3; + + // summary is the human-readable shortened documentation for the function. + string summary = 4; + + // description is human-readable documentation for the function. + string description = 5; + + // description_kind is the formatting of the description. + StringKind description_kind = 6; + + // deprecation_message is human-readable documentation if the + // function is deprecated. + string deprecation_message = 7; + + message Parameter { + // name is the human-readable display name for the parameter. + string name = 1; + + // type is the type constraint for the parameter. + bytes type = 2; + + // allow_null_value when enabled denotes that a null argument value can + // be passed to the provider. When disabled, Terraform returns an error + // if the argument value is null. + bool allow_null_value = 3; + + // allow_unknown_values when enabled denotes that only wholly known + // argument values will be passed to the provider. When disabled, + // Terraform skips the function call entirely and assumes an unknown + // value result from the function. + bool allow_unknown_values = 4; + + // description is human-readable documentation for the parameter. + string description = 5; + + // description_kind is the formatting of the description. + StringKind description_kind = 6; + } + + message Return { + // type is the type constraint for the function result. + bytes type = 1; + } +} + +// ServerCapabilities allows providers to communicate extra information +// regarding supported protocol features. This is used to indicate +// availability of certain forward-compatible changes which may be optional +// in a major protocol version, but cannot be tested for directly. +message ServerCapabilities { + // The plan_destroy capability signals that a provider expects a call + // to PlanResourceChange when a resource is going to be destroyed. + bool plan_destroy = 1; + + // The get_provider_schema_optional capability indicates that this + // provider does not require calling GetProviderSchema to operate + // normally, and the caller can used a cached copy of the provider's + // schema. + bool get_provider_schema_optional = 2; + + // The move_resource_state capability signals that a provider supports the + // MoveResourceState RPC. + bool move_resource_state = 3; +} + +// ClientCapabilities allows Terraform to publish information regarding +// supported protocol features. This is used to indicate availability of +// certain forward-compatible changes which may be optional in a major +// protocol version, but cannot be tested for directly. +message ClientCapabilities { + // The deferral_allowed capability signals that the client is able to + // handle deferred responses from the provider. + bool deferral_allowed = 1; + + // The write_only_attributes_allowed capability signals that the client + // is able to handle write_only attributes for managed resources. + bool write_only_attributes_allowed = 2; +} + +// Deferred is a message that indicates that change is deferred for a reason. +message Deferred { + // Reason is the reason for deferring the change. + enum Reason { + // UNKNOWN is the default value, and should not be used. + UNKNOWN = 0; + // RESOURCE_CONFIG_UNKNOWN is used when the config is partially unknown and the real + // values need to be known before the change can be planned. + RESOURCE_CONFIG_UNKNOWN = 1; + // PROVIDER_CONFIG_UNKNOWN is used when parts of the provider configuration + // are unknown, e.g. the provider configuration is only known after the apply is done. + PROVIDER_CONFIG_UNKNOWN = 2; + // ABSENT_PREREQ is used when a hard dependency has not been satisfied. + ABSENT_PREREQ = 3; + } + + // reason is the reason for deferring the change. + Reason reason = 1; +} + +service Provider { + //////// Information about what a provider supports/expects + + // GetMetadata returns upfront information about server capabilities and + // supported resource types without requiring the server to instantiate all + // schema information, which may be memory intensive. This RPC is optional, + // where clients may receive an unimplemented RPC error. Clients should + // ignore the error and call the GetProviderSchema RPC as a fallback. + rpc GetMetadata(GetMetadata.Request) returns (GetMetadata.Response); + + // GetSchema returns schema information for the provider, data resources, + // and managed resources. + rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); + rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response); + rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response); + rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response); + rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response); + + // GetResourceIdentitySchemas returns the identity schemas for all managed + // resources. + rpc GetResourceIdentitySchemas(GetResourceIdentitySchemas.Request) returns (GetResourceIdentitySchemas.Response); + // UpgradeResourceIdentityData should return the upgraded resource identity + // data for a managed resource type. + rpc UpgradeResourceIdentity(UpgradeResourceIdentity.Request) returns (UpgradeResourceIdentity.Response); + + //////// One-time initialization, called before other functions below + rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response); + + //////// Managed Resource Lifecycle + rpc ReadResource(ReadResource.Request) returns (ReadResource.Response); + rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response); + rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response); + rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response); + rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response); + rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response); + + //////// Ephemeral Resource Lifecycle + rpc ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfig.Request) returns (ValidateEphemeralResourceConfig.Response); + rpc OpenEphemeralResource(OpenEphemeralResource.Request) returns (OpenEphemeralResource.Response); + rpc RenewEphemeralResource(RenewEphemeralResource.Request) returns (RenewEphemeralResource.Response); + rpc CloseEphemeralResource(CloseEphemeralResource.Request) returns (CloseEphemeralResource.Response); + + // GetFunctions returns the definitions of all functions. + rpc GetFunctions(GetFunctions.Request) returns (GetFunctions.Response); + + //////// Provider-contributed Functions + rpc CallFunction(CallFunction.Request) returns (CallFunction.Response); + + //////// Graceful Shutdown + rpc StopProvider(StopProvider.Request) returns (StopProvider.Response); +} + +message GetMetadata { + message Request { + } + + message Response { + ServerCapabilities server_capabilities = 1; + repeated Diagnostic diagnostics = 2; + repeated DataSourceMetadata data_sources = 3; + repeated ResourceMetadata resources = 4; + // functions returns metadata for any functions. + repeated FunctionMetadata functions = 5; + repeated EphemeralMetadata ephemeral_resources = 6; + } + + message EphemeralMetadata { + string type_name = 1; + } + + message FunctionMetadata { + // name is the function name. + string name = 1; + } + + message DataSourceMetadata { + string type_name = 1; + } + + message ResourceMetadata { + string type_name = 1; + } +} + +message GetProviderSchema { + message Request { + } + message Response { + Schema provider = 1; + map resource_schemas = 2; + map data_source_schemas = 3; + map functions = 7; + map ephemeral_resource_schemas = 8; + repeated Diagnostic diagnostics = 4; + Schema provider_meta = 5; + ServerCapabilities server_capabilities = 6; + } +} + +message ValidateProviderConfig { + message Request { + DynamicValue config = 1; + } + message Response { + repeated Diagnostic diagnostics = 2; + } +} + +message UpgradeResourceState { + // Request is the message that is sent to the provider during the + // UpgradeResourceState RPC. + // + // This message intentionally does not include configuration data as any + // configuration-based or configuration-conditional changes should occur + // during the PlanResourceChange RPC. Additionally, the configuration is + // not guaranteed to exist (in the case of resource destruction), be wholly + // known, nor match the given prior state, which could lead to unexpected + // provider behaviors for practitioners. + message Request { + string type_name = 1; + + // version is the schema_version number recorded in the state file + int64 version = 2; + + // raw_state is the raw states as stored for the resource. Core does + // not have access to the schema of prior_version, so it's the + // provider's responsibility to interpret this value using the + // appropriate older schema. The raw_state will be the json encoded + // state, or a legacy flat-mapped format. + RawState raw_state = 3; + } + message Response { + // new_state is a msgpack-encoded data structure that, when interpreted with + // the _current_ schema for this resource type, is functionally equivalent to + // that which was given in prior_state_raw. + DynamicValue upgraded_state = 1; + + // diagnostics describes any errors encountered during migration that could not + // be safely resolved, and warnings about any possibly-risky assumptions made + // in the upgrade process. + repeated Diagnostic diagnostics = 2; + } +} + +message GetResourceIdentitySchemas { + message Request { + } + + message Response { + // identity_schemas is a mapping of resource type names to their identity schemas. + map identity_schemas = 1; + + // diagnostics is the collection of warning and error diagnostics for this request. + repeated Diagnostic diagnostics = 2; + } +} + +message UpgradeResourceIdentity { + message Request { + // type_name is the managed resource type name + string type_name = 1; + + // version is the version of the resource identity data to upgrade + int64 version = 2; + + // raw_identity is the raw identity as stored for the resource. Core does + // not have access to the identity schema of prior_version, so it's the + // provider's responsibility to interpret this value using the + // appropriate older schema. The raw_identity will be json encoded. + RawState raw_identity = 3; + } + + message Response { + // upgraded_identity returns the upgraded resource identity data + ResourceIdentityData upgraded_identity = 1; + + // diagnostics is the collection of warning and error diagnostics for this request + repeated Diagnostic diagnostics = 2; + } +} + +message ValidateResourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + ClientCapabilities client_capabilities = 3; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ValidateDataResourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ValidateEphemeralResourceConfig { + message Request { + string type_name = 1; + DynamicValue config = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ConfigureProvider { + message Request { + string terraform_version = 1; + DynamicValue config = 2; + ClientCapabilities client_capabilities = 3; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message ReadResource { + // Request is the message that is sent to the provider during the + // ReadResource RPC. + // + // This message intentionally does not include configuration data as any + // configuration-based or configuration-conditional changes should occur + // during the PlanResourceChange RPC. Additionally, the configuration is + // not guaranteed to be wholly known nor match the given prior state, which + // could lead to unexpected provider behaviors for practitioners. + message Request { + string type_name = 1; + DynamicValue current_state = 2; + bytes private = 3; + DynamicValue provider_meta = 4; + ClientCapabilities client_capabilities = 5; + ResourceIdentityData current_identity = 6; + } + message Response { + DynamicValue new_state = 1; + repeated Diagnostic diagnostics = 2; + bytes private = 3; + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 4; + ResourceIdentityData new_identity = 5; + } +} + +message PlanResourceChange { + message Request { + string type_name = 1; + DynamicValue prior_state = 2; + DynamicValue proposed_new_state = 3; + DynamicValue config = 4; + bytes prior_private = 5; + DynamicValue provider_meta = 6; + ClientCapabilities client_capabilities = 7; + ResourceIdentityData prior_identity = 8; + } + + message Response { + DynamicValue planned_state = 1; + repeated AttributePath requires_replace = 2; + bytes planned_private = 3; + repeated Diagnostic diagnostics = 4; + + // This may be set only by the helper/schema "SDK" in the main Terraform + // repository, to request that Terraform Core >=0.12 permit additional + // inconsistencies that can result from the legacy SDK type system + // and its imprecise mapping to the >=0.12 type system. + // The change in behavior implied by this flag makes sense only for the + // specific details of the legacy SDK type system, and are not a general + // mechanism to avoid proper type handling in providers. + // + // ==== DO NOT USE THIS ==== + // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== + // ==== DO NOT USE THIS ==== + bool legacy_type_system = 5; + + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 6; + + ResourceIdentityData planned_identity = 7; + } +} + +message ApplyResourceChange { + message Request { + string type_name = 1; + DynamicValue prior_state = 2; + DynamicValue planned_state = 3; + DynamicValue config = 4; + bytes planned_private = 5; + DynamicValue provider_meta = 6; + ResourceIdentityData planned_identity = 7; + } + message Response { + DynamicValue new_state = 1; + bytes private = 2; + repeated Diagnostic diagnostics = 3; + + // This may be set only by the helper/schema "SDK" in the main Terraform + // repository, to request that Terraform Core >=0.12 permit additional + // inconsistencies that can result from the legacy SDK type system + // and its imprecise mapping to the >=0.12 type system. + // The change in behavior implied by this flag makes sense only for the + // specific details of the legacy SDK type system, and are not a general + // mechanism to avoid proper type handling in providers. + // + // ==== DO NOT USE THIS ==== + // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== + // ==== DO NOT USE THIS ==== + bool legacy_type_system = 4; + + ResourceIdentityData new_identity = 5; + } +} + +message ImportResourceState { + message Request { + string type_name = 1; + string id = 2; + ClientCapabilities client_capabilities = 3; + ResourceIdentityData identity = 4; + } + + message ImportedResource { + string type_name = 1; + DynamicValue state = 2; + bytes private = 3; + ResourceIdentityData identity = 4; + } + + message Response { + repeated ImportedResource imported_resources = 1; + repeated Diagnostic diagnostics = 2; + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 3; + } +} + +message MoveResourceState { + message Request { + // The address of the provider the resource is being moved from. + string source_provider_address = 1; + + // The resource type that the resource is being moved from. + string source_type_name = 2; + + // The schema version of the resource type that the resource is being + // moved from. + int64 source_schema_version = 3; + + // The raw state of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + RawState source_state = 4; + + // The resource type that the resource is being moved to. + string target_type_name = 5; + + // The private state of the resource being moved. + bytes source_private = 6; + + // The raw identity of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + RawState source_identity = 7; + + // The identity schema version of the resource type that the resource + // is being moved from. + int64 source_identity_schema_version = 8; + } + + message Response { + // The state of the resource after it has been moved. + DynamicValue target_state = 1; + + // Any diagnostics that occurred during the move. + repeated Diagnostic diagnostics = 2; + + // The private state of the resource after it has been moved. + bytes target_private = 3; + + ResourceIdentityData target_identity = 4; + } +} + +message ReadDataSource { + message Request { + string type_name = 1; + DynamicValue config = 2; + DynamicValue provider_meta = 3; + ClientCapabilities client_capabilities = 4; + } + message Response { + DynamicValue state = 1; + repeated Diagnostic diagnostics = 2; + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred deferred = 3; + } +} + +message OpenEphemeralResource { + message Request { + string type_name = 1; + DynamicValue config = 2; + ClientCapabilities client_capabilities = 3; + } + message Response { + repeated Diagnostic diagnostics = 1; + optional google.protobuf.Timestamp renew_at = 2; + DynamicValue result = 3; + optional bytes private = 4; + Deferred deferred = 5; + } +} + +message RenewEphemeralResource { + message Request { + string type_name = 1; + optional bytes private = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + optional google.protobuf.Timestamp renew_at = 2; + optional bytes private = 3; + } +} + +message CloseEphemeralResource { + message Request { + string type_name = 1; + optional bytes private = 2; + } + message Response { + repeated Diagnostic diagnostics = 1; + } +} + +message GetFunctions { + message Request {} + + message Response { + // functions is a mapping of function names to definitions. + map functions = 1; + + // diagnostics is any warnings or errors. + repeated Diagnostic diagnostics = 2; + } +} + +message CallFunction { + message Request { + string name = 1; + repeated DynamicValue arguments = 2; + } + message Response { + DynamicValue result = 1; + FunctionError error = 2; + } +} diff --git a/docs/resource-instance-change-lifecycle.md b/docs/resource-instance-change-lifecycle.md index 0849bea628..2c1b59c094 100644 --- a/docs/resource-instance-change-lifecycle.md +++ b/docs/resource-instance-change-lifecycle.md @@ -33,9 +33,11 @@ The various object values used in different parts of this process are: which a provider may use as a starting point for its planning operation. The built-in logic primarily deals with the expected behavior for attributes - marked in the schema as both "optional" _and_ "computed", which means that - the user may either set it or may leave it unset to allow the provider - to choose a value instead. + marked in the schema as "computed". If an attribute is only "computed", + Terraform expects the value to only be chosen by the provider and it will + preserve any Prior State. If an attribute is marked as "computed" and + "optional", this means that the user may either set it or may leave it + unset to allow the provider to choose a value. Terraform Core therefore constructs the proposed new state by taking the attribute value from Configuration if it is non-null, and then using the @@ -245,6 +247,8 @@ information as the **Previous Run State** but does so in a way that conforms to the current version of the resource type schema, which therefore allows Terraform Core to interact with the data fully for subsequent steps. +No unknown values are permitted in the **Updated State**. + ### ReadResource Although Terraform typically expects to have exclusive control over any remote @@ -287,7 +291,7 @@ following two situations for each attribute: This operation returns the **Prior State** to use for the next call to `PlanResourceChange`, thus completing the circle and beginning this process -over again. +over again. No unknown values are permitted in the **Prior State**. ## Handling of Nested Blocks in Configuration diff --git a/docs/unicode.md b/docs/unicode.md index e6f627d211..efcb442dc7 100644 --- a/docs/unicode.md +++ b/docs/unicode.md @@ -45,6 +45,31 @@ The other subsystems described below should always be set up to match themselves with `unicode.Version` and generate an error if they cannot, but that isn't true of all of them. +## Unicode Identifier Rules in HCL + +_Identifier and Pattern Syntax_ (TF31) is a Unicode standards annex which +describe a set of rules for tokenizing "identifiers", such as variable names +in a programming language. + +HCL uses a superset of that specification for its own identifier tokenization +rules, and so it includes some code derived from the TF31 data tables that +describe which characters belong to the "ID_Start" and "ID_Continue" classes. + +Since Terraform is the primary user of HCL, it's typically Terraform's adoption +of a new Unicode version which drives HCL to adopt one. To update the Unicode +tables to a new version: +* Edit `hclsyntax/generate.go`'s line which runs `unicode2ragel.rb` to specify + the URL of the `DerivedCoreProperties.txt` data file for the intended Unicode + version. +* Run `go generate ./hclsyntax` to run the generation code to update both + `unicode_derived.rl` and, indirectly, `scan_tokens.go`. (You will need both + a Ruby interpreter and the Ragel state machine compiler on your system in + order to complete this step.) +* Run all the tests to check for regressions: `go test ./...` +* If all looks good, commit all of the changes and open a PR to HCL. +* Once that PR is merged and released, update Terraform to use the new version + of HCL. + ## Unicode Text Segmentation _Text Segmentation_ (TR29) is a Unicode standards annex which describes diff --git a/experiments.go b/experiments.go index f28d27e5e1..d279a86482 100644 --- a/experiments.go +++ b/experiments.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main // experimentsAllowed can be set to any non-empty string using Go linker diff --git a/go.mod b/go.mod index 3407e47df1..173e78c4cc 100644 --- a/go.mod +++ b/go.mod @@ -1,195 +1,337 @@ module github.com/hashicorp/terraform +go 1.24.2 + +godebug winsymlink=0 + require ( - cloud.google.com/go/storage v1.10.0 - github.com/Azure/azure-sdk-for-go v59.2.0+incompatible - github.com/Azure/go-autorest/autorest v0.11.24 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 + github.com/ProtonMail/go-crypto v1.1.3 github.com/agext/levenshtein v1.2.3 - github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501 - github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 - github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible github.com/apparentlymart/go-cidr v1.1.0 - github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 github.com/apparentlymart/go-shquot v0.0.1 github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 - github.com/apparentlymart/go-versions v1.0.1 + github.com/apparentlymart/go-versions v1.0.2 github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 - github.com/aws/aws-sdk-go v1.42.35 github.com/bgentry/speakeasy v0.1.0 github.com/bmatcuk/doublestar v1.1.5 github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e - github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f github.com/davecgh/go-spew v1.1.1 github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6 github.com/go-test/deep v1.0.3 - github.com/golang/mock v1.6.0 - github.com/google/go-cmp v0.5.8 - github.com/google/uuid v1.2.0 - github.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5 - github.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d - github.com/hashicorp/aws-sdk-go-base v0.7.1 - github.com/hashicorp/consul/api v1.9.1 - github.com/hashicorp/consul/sdk v0.8.0 - github.com/hashicorp/errwrap v1.1.0 - github.com/hashicorp/go-azure-helpers v0.31.1 + github.com/google/go-cmp v0.7.0 + github.com/google/uuid v1.6.0 + github.com/hashicorp/cli v1.1.7 github.com/hashicorp/go-checkpoint v0.5.0 github.com/hashicorp/go-cleanhttp v0.5.2 - github.com/hashicorp/go-getter v1.6.2 - github.com/hashicorp/go-hclog v0.15.0 - github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/go-plugin v1.4.3 - github.com/hashicorp/go-retryablehttp v0.7.1 - github.com/hashicorp/go-tfe v1.7.0 + github.com/hashicorp/go-getter v1.7.8 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-plugin v1.6.3 + github.com/hashicorp/go-retryablehttp v0.7.7 + github.com/hashicorp/go-slug v0.16.3 + github.com/hashicorp/go-tfe v1.74.1 github.com/hashicorp/go-uuid v1.0.3 - github.com/hashicorp/go-version v1.6.0 - github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f - github.com/hashicorp/hcl/v2 v2.13.0 - github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2 - github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c - github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 - github.com/jmespath/go-jmespath v0.4.0 - github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926 + github.com/hashicorp/go-version v1.7.0 + github.com/hashicorp/hcl v1.0.0 + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 + github.com/hashicorp/jsonapi v1.3.2 + github.com/hashicorp/terraform-registry-address v0.2.4 + github.com/hashicorp/terraform-svchost v0.1.1 + github.com/hashicorp/terraform/internal/backend/remote-state/azure v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/consul v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/cos v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/gcs v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/oci v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/oss v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/pg v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/backend/remote-state/s3 v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 - github.com/lib/pq v1.10.3 - github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82 - github.com/manicminer/hamilton v0.44.0 github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88 - github.com/mattn/go-isatty v0.0.12 - github.com/mattn/go-shellwords v1.0.4 - github.com/mitchellh/cli v1.1.4 + github.com/mattn/go-isatty v0.0.20 + github.com/mattn/go-shellwords v1.0.12 github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db - github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/gox v1.0.1 - github.com/mitchellh/mapstructure v1.1.2 - github.com/mitchellh/reflectwalk v1.0.2 - github.com/nishanths/exhaustive v0.7.11 - github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db - github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 - github.com/pkg/errors v0.9.1 + github.com/nishanths/exhaustive v0.12.0 + github.com/packer-community/winrmcp v0.0.0-20221126162354-6e900dd2c68f + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/posener/complete v1.2.3 - github.com/spf13/afero v1.2.2 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 - github.com/tencentyun/cos-go-sdk-v5 v0.7.29 - github.com/tombuildsstuff/giovanni v0.15.1 - github.com/xanzy/ssh-agent v0.3.1 + github.com/spf13/afero v1.9.5 + github.com/xanzy/ssh-agent v0.3.3 github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 - github.com/zclconf/go-cty v1.11.0 - github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b - github.com/zclconf/go-cty-yaml v1.0.2 - golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f - golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f - golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b - golang.org/x/text v0.3.7 - golang.org/x/tools v0.1.11 - google.golang.org/api v0.44.0-impersonate-preview - google.golang.org/grpc v1.47.0 - google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 - google.golang.org/protobuf v1.27.1 - honnef.co/go/tools v0.3.0 - k8s.io/api v0.23.4 - k8s.io/apimachinery v0.23.4 - k8s.io/client-go v0.23.4 - k8s.io/utils v0.0.0-20211116205334-6203023598ed + github.com/zclconf/go-cty v1.16.2 + github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 + github.com/zclconf/go-cty-yaml v1.1.0 + go.opentelemetry.io/contrib/exporters/autoexport v0.45.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 + go.opentelemetry.io/otel v1.34.0 + go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/trace v1.34.0 + go.uber.org/mock v0.4.0 + golang.org/x/crypto v0.36.0 + golang.org/x/mod v0.24.0 + golang.org/x/net v0.38.0 + golang.org/x/oauth2 v0.27.0 + golang.org/x/sys v0.31.0 + golang.org/x/term v0.30.0 + golang.org/x/text v0.23.0 + golang.org/x/tools v0.31.0 + golang.org/x/tools/cmd/cover v0.1.0-deprecated + google.golang.org/grpc v1.69.4 + google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 + google.golang.org/protobuf v1.36.5 + honnef.co/go/tools v0.6.0 ) require ( - cloud.google.com/go v0.81.0 // indirect + cloud.google.com/go v0.110.10 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/storage v1.30.1 // indirect + github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect - github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect - github.com/Azure/go-autorest/autorest/azure/cli v0.4.4 // indirect - github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect - github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect - github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect - github.com/Azure/go-autorest/logger v0.2.1 // indirect - github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/Azure/go-autorest/autorest v0.11.30 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.2 // indirect + github.com/Azure/go-autorest/tracing v0.6.1 // indirect github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c // indirect - github.com/BurntSushi/toml v0.4.1 // indirect + github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d // indirect - github.com/Masterminds/goutils v1.1.0 // indirect - github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.0 // indirect - github.com/Microsoft/go-winio v0.5.0 // indirect - github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.2.0 // indirect + github.com/Masterminds/sprig/v3 v3.2.3 // indirect + github.com/Microsoft/go-winio v0.5.2 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501 // indirect + github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 // indirect + github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible // indirect github.com/antchfx/xmlquery v1.3.5 // indirect github.com/antchfx/xpath v1.1.10 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go v1.44.122 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.0 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.57 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 // indirect + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.8 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.38.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect + github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.33.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d // indirect - github.com/creack/pty v1.1.18 // indirect - github.com/dimchansky/utfbom v1.1.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/cli/go-gh/v2 v2.11.2 // indirect + github.com/cli/safeexec v1.0.1 // indirect + github.com/cloudflare/circl v1.4.0 // indirect + github.com/creack/pty v1.1.17 // indirect github.com/dylanmei/iso8601 v0.1.0 // indirect - github.com/fatih/color v1.9.0 // indirect - github.com/go-logr/logr v1.2.0 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/errors v0.22.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/strfmt v0.23.0 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gofrs/flock v0.10.0 // indirect github.com/gofrs/uuid v4.0.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v4 v4.2.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-github/v45 v45.2.0 // indirect + github.com/google/go-github/v62 v62.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/googleapis/gax-go/v2 v2.0.5 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect + github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62 // indirect + github.com/hashicorp/consul/api v1.13.0 // indirect + github.com/hashicorp/copywrite v0.20.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-azure-helpers v0.72.0 // indirect + github.com/hashicorp/go-azure-sdk/resource-manager v0.20250131.1134653 // indirect + github.com/hashicorp/go-azure-sdk/sdk v0.20250131.1134653 // indirect + github.com/hashicorp/go-cty v1.4.1 // indirect github.com/hashicorp/go-immutable-radix v1.0.0 // indirect - github.com/hashicorp/go-msgpack v0.5.4 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-slug v0.9.1 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect - github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d // indirect - github.com/hashicorp/serf v0.9.5 // indirect - github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect - github.com/huandu/xstrings v1.3.2 // indirect - github.com/imdario/mergo v0.3.12 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/serf v0.9.6 // indirect + github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/huandu/xstrings v1.3.3 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackofallops/giovanni v0.28.0 // indirect + github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect + github.com/jedib0t/go-pretty/v6 v6.5.9 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/jstemmer/go-junit-report v0.9.1 // indirect - github.com/klauspost/compress v1.11.2 // indirect - github.com/kr/pretty v0.2.1 // indirect - github.com/manicminer/hamilton-autorest v0.2.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/klauspost/compress v1.15.11 // indirect + github.com/knadh/koanf v1.5.0 // indirect + github.com/lib/pq v1.10.3 // indirect + github.com/mailru/easyjson v0.7.6 // indirect github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect - github.com/mattn/go-colorable v0.1.6 // indirect - github.com/mitchellh/go-testing-interface v1.0.4 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mergestat/timediff v0.0.3 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/iochan v1.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mozillazg/go-httpheader v0.3.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect github.com/oklog/run v1.0.0 // indirect - github.com/satori/go.uuid v1.2.0 // indirect - github.com/sergi/go-diff v1.2.0 // indirect - github.com/shopspring/decimal v1.2.0 // indirect - github.com/spf13/cast v1.3.1 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/oracle/oci-go-sdk/v65 v65.89.1 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/samber/lo v1.47.0 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/sony/gobreaker v0.5.0 // indirect + github.com/spf13/cast v1.5.0 // indirect + github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/objx v0.1.1 // indirect - github.com/ulikunitz/xz v0.5.8 // indirect - github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect - github.com/vmihailenco/tagparser v0.1.1 // indirect - go.opencensus.io v0.23.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e // indirect - golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 // indirect + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 // indirect + github.com/tencentyun/cos-go-sdk-v5 v0.7.42 // indirect + github.com/thanhpk/randstr v1.0.6 // indirect + github.com/ulikunitz/xz v0.5.10 // indirect + 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 + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.mongodb.org/mongo-driver v1.16.1 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/proto/otlp v1.0.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.155.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.66.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/klog/v2 v2.30.0 // indirect - k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect - sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect + k8s.io/api v0.25.5 // indirect + k8s.io/apimachinery v0.25.5 // indirect + k8s.io/client-go v0.25.5 // indirect + k8s.io/klog/v2 v2.70.1 // indirect + k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect sigs.k8s.io/yaml v1.2.0 // indirect + software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect ) -go 1.18 +// Some of the packages in this codebase are split into separate Go modules +// just so that we can more easily determine how dependency updates might +// affect components with different code ownership. These modules are never +// actually published separately from the toplevel module, and so we exclusively +// use the "pseudo-revisions" generated by the Go toolchain to describe them +// in go.mod files. +// +// If you change the dependencies of any one of these components, use +// "make syncdeps" to resynchronize the others. That will then make sure that +// your updates will be visible to the code owners of each affected component. +// +// In the long run we want to make each of these either move to another codebase +// or get deleted entirely, but as long as we keep maintaining this all together +// in one codebase this is a pragmatic compromise to help us understand the +// impact of and responsibilities for dependency upgrades. +// +// We don't expect to add any new modules here because the ones that are here +// are here just as technical debt. If you _do_ end up adding another one, +// you'll need to add similar replace directives to each of the other modules +// so that they all agree with each other that we're never publishing any of +// these modules as a separate unit. (But please add to this only as a last +// resort!) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ./internal/backend/remote-state/azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ./internal/backend/remote-state/consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ./internal/backend/remote-state/cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ./internal/backend/remote-state/gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ./internal/backend/remote-state/kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ./internal/backend/remote-state/oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ./internal/backend/remote-state/pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ./internal/backend/remote-state/s3 + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oci => ./internal/backend/remote-state/oci + +replace github.com/hashicorp/terraform/internal/legacy => ./internal/legacy + +tool ( + github.com/hashicorp/copywrite + github.com/nishanths/exhaustive/cmd/exhaustive + go.uber.org/mock/mockgen + golang.org/x/tools/cmd/cover + golang.org/x/tools/cmd/goimports + golang.org/x/tools/cmd/stringer + honnef.co/go/tools/cmd/staticcheck +) diff --git a/go.sum b/go.sum index 204fd15ee3..48783e0a90 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -15,120 +16,684 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= +cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= +cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= +cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= +cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= +cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= +cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= +cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= +cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= +cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= +cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= +cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= +cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= +cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= +cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= +cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= +cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= +cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= +cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= +cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= +cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= +cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= +cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= +cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= +cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= +cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= +cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= +cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= +cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= +cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= +cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= +cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= +cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= +cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= +cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= +cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= +cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= +cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= +cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= +cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= +cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= +cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= +cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= +cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= +cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= +cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= +cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= +cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= +cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= +cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= +cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= +cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= +cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= +cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= +cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= +cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= +cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= +cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= +cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= +cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= +cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= +cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= +cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= +cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= +cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= +cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= +cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= +cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= +cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= +cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= +cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= +cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= +cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= +cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= +cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= +cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= +cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= +cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= +cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= +cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= +cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= +cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= +cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= +cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= +cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= +cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= +cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= +cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= +cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= +cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= +cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= +cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= +cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= +cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= +cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= +cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= +cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= +cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= +cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= +cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= +cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= +cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= +cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= +cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= +cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= +cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= +cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= +cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= +cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= +cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= +cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= +cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= +cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= +cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= +cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= +cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= +cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= +cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= +cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= +cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= +cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= +cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= +cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= +cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= +cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= +cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= +cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= +cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= +cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= +cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= +cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= +cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= +cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= +cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= +cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= +cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= +cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= +cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= +cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= +cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= +cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= +cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= +cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= +cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= +cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= +cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= +cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= +cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= +cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= +cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= +cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= +cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= +cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= +cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= +cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= +cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= +cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= +cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= +cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= +cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= +cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= +cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= +cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= +cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= +cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= +cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= +cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= +cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= +cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= +cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= +cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= +cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= +cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= +cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= +cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= +cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= +cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= +cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= +cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= +cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= +cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM= +cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= +cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= +cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= +cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= +cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= +cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= +cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= +cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= +cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= +cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= +cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= +cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= +cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= +cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= +cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= +cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= +cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= +cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= +cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= +cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= +cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= +cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= +cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= +cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= +cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= +cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= +cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= +cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= +cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= +cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= +cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= +cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= +cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= +cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= +cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= +cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= +cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= +cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= +cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= +cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= +cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= +cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= +cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= +cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= +cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= +cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= +cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= +cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= +cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= +cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= +cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= +cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= +cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= +cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= +cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= +cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= +cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= +cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= +cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= +cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= +cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= +cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= +cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= +cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= +cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= +cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= +cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= +cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= +cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= +cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= +cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= +cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= +cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= +cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= +cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= +cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= +cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= +cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= +cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= +cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= +cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= +cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= +cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= +cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= +cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= +cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= +cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= +cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= +cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= +cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= +cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= +cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= +cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= +cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= +cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= +cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= +cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= +cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= +cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= +cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= +cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= +cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= +cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= +cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= +cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= +cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= +cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= +cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= +cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= +cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= +cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= +cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= +cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= +cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= +cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= +cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= +cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= +cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= +cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= +cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= +cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= +cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= +cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= +cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= +cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= +cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= +cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= +cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= +cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= +cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= +cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= +cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= +cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= +cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= +cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= +cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= +cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= +cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= +cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= +cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= +cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= +cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= +cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= +cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= +cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= +cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= +cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= +cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= +cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= +cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= +cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= +cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= +cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= +cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= +cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= +cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= +cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= +cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= +cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/azure-sdk-for-go v45.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v47.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= -github.com/Azure/azure-sdk-for-go v59.2.0+incompatible h1:mbxiZy1K820hQ+dI+YIO/+a0wQDYqOu18BAGe4lXjVk= -github.com/Azure/azure-sdk-for-go v59.2.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= +git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.3/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest v0.11.10/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE= -github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= -github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.14/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ= -github.com/Azure/go-autorest/autorest/adal v0.9.18/go.mod h1:XVVeme+LZwABT8K5Lc3hA4nAe8LDBVle26gTrguhhPQ= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.0/go.mod h1:JljT387FplPzBA31vUcvsetLKF3pec5bdAxjVU4kI2s= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.4 h1:iuooz5cZL6VRcO7DVSFYxRcouqn6bFVE/e77Wts50Zk= -github.com/Azure/go-autorest/autorest/azure/cli v0.4.4/go.mod h1:yAQ2b6eP/CmLPnmLvxtT1ALIY3OR1oFcCqVBi8vHiTc= -github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= +github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= +github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk= -github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE= -github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= -github.com/Azure/go-autorest/autorest/validation v0.3.1 h1:AgyqjAd94fwNAoTjl/WQXg4VvFeRFpO+UhNyRXqF1ac= -github.com/Azure/go-autorest/autorest/validation v0.3.1/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E= -github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= github.com/Azure/go-ntlmssp v0.0.0-20180810175552-4a21cbd618b4/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28= github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= -github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= +github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChrisTrenkamp/goxpath v0.0.0-20170922090931-c385f95c6022/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d h1:W1diKnDQkXxNDhghdBSbQ4LI/E1aJNTwpqPp3KtlB8w= github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d/go.mod h1:nuWgzSkT5PnyOd+272uUmV0dnAnAn42Mk7PiQC5VzN4= -github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg= -github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/sprig/v3 v3.2.0 h1:P1ekkbuU73Ui/wS0nK1HOM37hh4xdfZo485UPf8rc+Y= -github.com/Masterminds/sprig/v3 v3.2.0/go.mod h1:tWhwTbUTndesPNeF0C900vKoq283u6zp4APT9vaF3SI= -github.com/Microsoft/go-winio v0.5.0 h1:Elr9Wn+sGKPlkaBvwu4mTrxtmOp3F3yV9qhaHbXGjwU= -github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g= +github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +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/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= -github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af h1:DBNMBMuMiWYu0b+8KMJuWmfCkcxl09JwdlqwDZZ6U14= -github.com/abdullin/seq v0.0.0-20160510034733-d5467c17e7af/go.mod h1:5Jv4cbFiHJMsVxt52+i0Ha45fjshj6wxYr1r19tB9bw= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= -github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= +github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501 h1:Ij3S0pNUMgHlhx3Ew8g9RNrt59EKhHYdMODGtFXJfSc= github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 h1:FrF4uxA24DF3ARNXVbUin3wa5fDLaB1Cy8mKks/LRz4= github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible h1:ABQ7FF+IxSFHDMOTtjCfmMDMHiCq6EsAoCV/9sFinaM= github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antchfx/xmlquery v1.3.5 h1:I7TuBRqsnfFuL11ruavGm911Awx9IqSdiU6W/ztSmVw= github.com/antchfx/xmlquery v1.3.5/go.mod h1:64w0Xesg2sTaawIdNqMB+7qaW/bSqkQm+ssPaCMWNnc= github.com/antchfx/xpath v1.1.10 h1:cJ0pOvEdN/WvYXxvRrzQH9x5QWKpzHacYO8qzCcDYAg= github.com/antchfx/xpath v1.1.10/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= +github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= +github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= -github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= -github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I= -github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-shquot v0.0.1 h1:MGV8lwxF4zw75lN7e0MGs7o6AFYn7L6AZaExUpLh0Mo= github.com/apparentlymart/go-shquot v0.0.1/go.mod h1:lw58XsE5IgUXZ9h0cxnypdx31p9mPFIVEQ9P3c7MlrU= -github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13 h1:JtuelWqyixKApmXm3qghhZ7O96P6NKpyrlSIe8Rwnhw= github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13/go.mod h1:7kfpUbyCdGJ9fDRCp3fopPQi5+cKNHgTE4ZuNrO71Cw= -github.com/apparentlymart/go-versions v1.0.1 h1:ECIpSn0adcYNsBfSRwdDdz9fWlL+S/6EUd9+irwkBgU= -github.com/apparentlymart/go-versions v1.0.1/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -137,21 +702,95 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= -github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.31.9/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/aws/aws-sdk-go v1.42.35 h1:N4N9buNs4YlosI9N0+WYrq8cIZwdgv34yRbxzZlTvFs= -github.com/aws/aws-sdk-go v1.42.35/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= +github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/config v1.29.4 h1:ObNqKsDYFGr2WxnoXKOhCvTlf3HhwtoGgc+KmZ4H5yg= +github.com/aws/aws-sdk-go-v2/config v1.29.4/go.mod h1:j2/AF7j/qxVmsNIChw1tWfsVKOayJoGRDjg1Tgq7NPk= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.57 h1:kFQDsbdBAR3GZsB8xA+51ptEnq9TIj3tS4MuP5b+TcQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.57/go.mod h1:2kerxPUUbTagAr/kkaHiqvj/bcYHzi2qiJS/ZinllU0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.22 h1:MUD/42Etbj6sVZ0HpOe4G/4+wDF7ZJhqZXSqNKZokPM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.22/go.mod h1:wp0iN4VH1riPNX68N8MU+mz/7ggSeWc+zBhsdALp+zM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.8 h1:D4Dhqf6FEw//4mEFsxtBYMNSmdSg0LAy+A+DVvH0dts= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.8/go.mod h1:+pfCvXbSNLZ7lG+tydnY5IN4WUoz+WsGDrl2rg2DEew= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.10 h1:u/MwkFwRkKRDvy7D76/khJTk8HMp4mC5sZKErU53jos= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.10/go.mod h1:Gid0WEVky3EWbkeXiS67kHhbiK+q3/wO/hvPh7plR0c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.12 h1:V1h3Cxmn0tN5EhL31uvqSLKsMlPlqiYxRwAEdwNeIJ8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.12/go.mod h1:KzXJPn2wqsZJlNSx70gmDkRDVTmyF/RRXxTP2yMxUwc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2 h1:dyC+iA2+Yc7iDMDh0R4eT6fi8TgBduc+BOWCy6Br0/o= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.12 h1:5LZIyHvSAu2DeC9X6P9c3ALFTSDu/oyJ5Cq0rLbe2mk= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.12/go.mod h1:W7OKlS05LPMcLvQamv12gv/hSQlWAyU1lh98jwMVf2k= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.12 h1:8TMY/uvatjnLqllJhW0WOfAQSdLQl525yuaA0Uq1ejk= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.12/go.mod h1:LG6s2xJm3K9X9ee5EmYyOveXOgVK4jtunBJBXFJ2TqE= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 h1:fqg6c1KVrc3SYWma/egWue5rKI4G2+M4wMQN2JosNAA= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.12/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0 h1:R9d0v+iobRHSaE4wKUnXFiZp53AL4ED5MzgEMwGTZag= +github.com/bradleyfalzon/ghinstallation/v2 v2.11.0/go.mod h1:0LWKQwOHewXO/1acI6TtyE0Xc4ObDb2rFN7eHBAG71M= +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/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -159,80 +798,145 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5O github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/cli/go-gh/v2 v2.11.2 h1:oad1+sESTPNTiTvh3I3t8UmxuovNDxhwLzeMHk45Q9w= +github.com/cli/go-gh/v2 v2.11.2/go.mod h1:vVFhi3TfjseIW26ED9itAR8gQK0aVThTm8sYrsZ5QTI= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= +github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d h1:t5Wuyh53qYyg9eqn4BbnlIT+vmhyww0TatL+zT3uWgI= -github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= -github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= -github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= -github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dylanmei/iso8601 v0.1.0 h1:812NGQDBcqquTfH5Yeo7lwR0nzx/cKdsmf3qMjPURUI= github.com/dylanmei/iso8601 v0.1.0/go.mod h1:w9KhXSgIyROl1DefbMYIE7UVSIvELTbMrCfx+QkYnoQ= github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6 h1:zWydSUQBJApHwpQ4guHi+mGyQN/8yN6xbKWdDtL3ZNM= github.com/dylanmei/winrmtest v0.0.0-20210303004826-fbc9ae56efb6/go.mod h1:6BLLhzn1VEiJ4veuAGhINBTrBlV889Wd+aU4auxKOww= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= +github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= +github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= +github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= +github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= +github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= +github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w= +github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c= +github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 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/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k= +github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= -github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= -github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/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/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -246,7 +950,6 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -265,12 +968,20 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -283,8 +994,16 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= +github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -294,8 +1013,10 @@ github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -305,53 +1026,82 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gophercloud/gophercloud v0.6.1-0.20191122030953-d8ac278c1c9d/go.mod h1:ozGNgr9KYOVATV5jsgHl/ceCDXGuguqOZAzoQ/2vcNM= -github.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5 h1:Ciwp7ro4LyptUOkili/TX/ecuYr7vGtEIFnOOOKUjD8= -github.com/gophercloud/gophercloud v0.10.1-0.20200424014253-c3bfe50899e5/go.mod h1:gmC5oQqMDOMO1t1gq5DquX/yAU808e/4mzjjDA76+Ss= -github.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d h1:fduaPzWwIfvOMLuHk2Al3GZH0XbUqG8MbElPop+Igzs= -github.com/gophercloud/utils v0.0.0-20200423144003-7c72efc7435d/go.mod h1:ehWUbLQJPqS0Ep+CxeD559hsm9pthPXadJNKwZkp43w= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/hashicorp/aws-sdk-go-base v0.7.1 h1:7s/aR3hFn74tYPVihzDyZe7y/+BorN70rr9ZvpV3j3o= -github.com/hashicorp/aws-sdk-go-base v0.7.1/go.mod h1:2fRjWDv3jJBeN6mVWFHV6hFTNeFBx2gpDLQaZNxUVAY= -github.com/hashicorp/consul/api v1.9.1 h1:SngrdG2L62qqLsUz85qcPhFZ78rPf8tcD5qjMgs6MME= -github.com/hashicorp/consul/api v1.9.1/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62 h1:gZvwm6umNtCdZxD+H7my06k4wo6PQLgVwwilZIwWlyM= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62/go.mod h1:I/S6699SKgH+TrDK7Y+pmTTzajLulyUbKYb/ngj64UA= +github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU= +github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU= +github.com/hashicorp/consul/api v1.13.0 h1:2hnLQ0GjQvw7f3O61jMO8gbasZviZTrt9R8WzgiirHc= +github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= 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/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-azure-helpers v0.12.0/go.mod h1:Zc3v4DNeX6PDdy7NljlYpnrdac1++qNW0I4U+ofGwpg= -github.com/hashicorp/go-azure-helpers v0.31.1 h1:lgwZLcyMheoLUj7dJfsrsa7ZpRvOIbsfFhttLi6ml78= -github.com/hashicorp/go-azure-helpers v0.31.1/go.mod h1:gcutZ/Hf/O7YN9M3UIvyZ9l0Rxv7Yrc9x5sSfM9cuSw= +github.com/hashicorp/go-azure-helpers v0.72.0 h1:eOCpXBkDYYMESkOBC0LgPMNUdiLPtQxv7sCW/hHdUbw= +github.com/hashicorp/go-azure-helpers v0.72.0/go.mod h1:Omirva2gGNrhN4pigurl5RN7u3BoaN0bco8ZSbdaNy8= +github.com/hashicorp/go-azure-sdk/resource-manager v0.20250131.1134653 h1:KuDCZKWoOByX5MUyFRNWLl4Gy6wpZCwJ7Ez1mbUwouo= +github.com/hashicorp/go-azure-sdk/resource-manager v0.20250131.1134653/go.mod h1:AawbnS/Kkp/IURMJVzmvD+Co2zK91lKFqYYDbenCpGU= +github.com/hashicorp/go-azure-sdk/sdk v0.20250131.1134653 h1:Bd+glHUD1mdal1zn0NgoS4wDFhUB8Qfw61j0nZEnC5A= +github.com/hashicorp/go-azure-sdk/sdk v0.20250131.1134653/go.mod h1:oI5R0fTbBx3K/sJBK5R/OlEy8ozdQjvctxVU9v3EDkc= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 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= 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-getter v1.6.2 h1:7jX7xcB+uVCliddZgeKyNxv0xoT7qL5KDtH7rU4IqIk= -github.com/hashicorp/go-getter v1.6.2/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.15.0 h1:qMuK0wxsoW4D0ddCCYwPSTm4KQv1X1ke3WmPWZ0Mvsk= -github.com/hashicorp/go-hclog v0.15.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +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-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -361,154 +1111,203 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 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.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= -github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= -github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= -github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +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.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +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-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= -github.com/hashicorp/go-slug v0.9.1 h1:gYNVJ3t0jAWx8AT2eYZci3Xd7NBHyjayW9AR1DU4ki0= -github.com/hashicorp/go-slug v0.9.1/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4= -github.com/hashicorp/go-sockaddr v1.0.0 h1:GeH6tui99pF4NJgfnhp+L6+FfobzVW3Ah46sLo0ICXs= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= -github.com/hashicorp/go-tfe v1.7.0 h1:GELRhS5dizF6giwjZBqUC/xPaSuNYB+hWRtUnf6i8K8= -github.com/hashicorp/go-tfe v1.7.0/go.mod h1:E8a90lC4kjU5Lc2c0D+SnWhUuyuoCIVm4Ewzv3jCD3A= +github.com/hashicorp/go-tfe v1.74.1 h1:I/8fOwSYox17IZV7SULIQH0ZRPNL2g/biW6hHWnOTVY= +github.com/hashicorp/go-tfe v1.74.1/go.mod h1:kGHWMZ3HHjitgqON8nBZ4kPVJ3cLbzM4JMgmNVMs9aQ= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws= -github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= -github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90= -github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= -github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= -github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d h1:9ARUJJ1VVynB176G1HCwleORqCaXm/Vx0uUi0dL26I0= -github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d/go.mod h1:Yog5+CPEM3c99L1CL2CFCYoSzgWm5vTU58idbRUaLik= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +github.com/hashicorp/jsonapi v1.3.2 h1:gP3fX2ZT7qXi+PbwieptzkspIohO2kCSiBUvUTBAbMs= +github.com/hashicorp/jsonapi v1.3.2/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM= +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/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= -github.com/hashicorp/memberlist v0.2.2 h1:5+RffWKwqJ71YPu9mWsF7ZOscZmwfasdA8kbdC7AO2g= -github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= -github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= -github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2 h1:l+bLFvHjqtgNQwWxwrFX9PemGAAO2P1AGZM7zlMNvCs= -github.com/hashicorp/terraform-config-inspect v0.0.0-20210209133302-4fd17a0faac2/go.mod h1:Z0Nnk4+3Cy89smEbrq+sl1bxc9198gIP4I7wcQF6Kqs= -github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c h1:D8aRO6+mTqHfLsK/BC3j5OAoogv1WLRWzY1AaTo3rBg= -github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI= -github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= -github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +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-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-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4= +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-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/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= +github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= -github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= -github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackofallops/giovanni v0.28.0 h1:fxn55SnxL2Rj3hgkkgQS9UKlIRXkkTZ5WcnE04JCBRE= +github.com/jackofallops/giovanni v0.28.0/go.mod h1:CyzRgZyts4YSI/1iZF8poqdn9I6J8xpmg1iMpvhthTs= +github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= +github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= +github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926 h1:kie3qOosvRKqwij2HGzXWffwpXvcqfPPXRUw8I4F/mg= -github.com/joyent/triton-go v0.0.0-20180313100802-d8f9c0314926/go.mod h1:U+RSyWxWd04xTqnuOQxnai7XGS2PrPY2cfGoDKtMHjA= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ= -github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82 h1:wnfcqULT+N2seWf6y4yHzmi7GD2kNx4Ute0qArktD48= -github.com/lusis/go-artifactory v0.0.0-20160115162124-7e4ce345df82/go.mod h1:y54tfGmO3NKssKveTEFFzH8C/akrSOy/iW9qEAUDV84= +github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= +github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/manicminer/hamilton v0.43.0/go.mod h1:lbVyngC+/nCWuDp8UhC6Bw+bh7jcP/E+YwqzHTmzemk= -github.com/manicminer/hamilton v0.44.0 h1:mLb4Vxbt2dsAvOpaB7xd/5D8LaTTX6ACwVP4TmW8qwE= -github.com/manicminer/hamilton v0.44.0/go.mod h1:lbVyngC+/nCWuDp8UhC6Bw+bh7jcP/E+YwqzHTmzemk= -github.com/manicminer/hamilton-autorest v0.2.0 h1:dDL+t2DrQza0EfNYINYCvXISeNwVqzgVAQh+CH/19ZU= -github.com/manicminer/hamilton-autorest v0.2.0/go.mod h1:NselDpNTImEmOc/fa41kPg6YhDt/6S95ejWbTGZ6tlg= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88 h1:cxuVcCvCLD9yYDbRCWw0jSgh1oT6P6mv3aJDKK5o7X4= github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88/go.mod h1:a2HXwefeat3evJHxFXSayvRHpYEPJYtErl4uIzfaUqY= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.4 h1:xmZZyxuP+bYKAKkA9ABYXVNJ+G/Wf3R8d8vAP3LDJJk= -github.com/mattn/go-shellwords v1.0.4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mergestat/timediff v0.0.3 h1:ucCNh4/ZrTPjFZ081PccNbhx9spymCJkFxSzgVuPU+Y= +github.com/mergestat/timediff v0.0.3/go.mod h1:yvMUaRu2oetc+9IbPLYBJviz6sA7xz8OXMDfhBl7YSI= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= +github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= -github.com/mitchellh/cli v1.1.4 h1:qj8czE26AU4PbiaPXK5uVmMSM+V5BYsFBiM9HhGRLUA= -github.com/mitchellh/cli v1.1.4/go.mod h1:vTLESy5mRhKOs9KDp0/RATawxP1UqBmdrpVRMnpcvKQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb h1:GRiLv4rgyqjqzxbhJke65IYUf4NCOOvrPOJbV/sPxkM= github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb/go.mod h1:OaY7UOoTkkrX3wRwjpYRKafIkkyeD0UtweSHAWWiqQM= github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.4 h1:ZU1VNC02qyufSZsjjs7+khruk2fKvbQ3TwRV/IBCeFA= -github.com/mitchellh/go-testing-interface v1.0.4/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= @@ -517,168 +1316,303 @@ github.com/mitchellh/gox v1.0.1/go.mod h1:ED6BioOGXMswlXa2zxfh/xdd5QhwYliBFn9V18 github.com/mitchellh/iochan v1.0.0 h1:C+X3KsSTLFVBr/tK1eYN/vs4rJcvsiLU338UhYPJWeY= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= github.com/mozillazg/go-httpheader v0.3.0 h1:3brX5z8HTH+0RrNA1362Rc3HsaxyWEKtGY45YrhuINM= github.com/mozillazg/go-httpheader v0.3.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nishanths/exhaustive v0.7.11 h1:xV/WU3Vdwh5BUH4N06JNUznb6d5zhRPOnlgCrpNYNKA= -github.com/nishanths/exhaustive v0.7.11/go.mod h1:gX+MP7DWMKJmNa1HfMozK+u04hQd3na9i0hyqf3/dOI= +github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= +github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db h1:9uViuKtx1jrlXLBW/pMnhOfzn3iSEdLase/But/IZRU= -github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= -github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/oracle/oci-go-sdk/v65 v65.89.1 h1:8sVjxYPNQ83yqUgZKkdeUA0CnSodmL1Bme2oxq8gyKg= +github.com/oracle/oci-go-sdk/v65 v65.89.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= +github.com/packer-community/winrmcp v0.0.0-20221126162354-6e900dd2c68f h1:sWm3fnjG6kxvDuGiQf46Io5xCTj3QJfNJIeICJ4g1kw= +github.com/packer-community/winrmcp v0.0.0-20221126162354-6e900dd2c68f/go.mod h1:f6Izs6JvFTdnRbziASagjZ2vmf55NSIkC/weStxCHqk= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23 h1:dofHuld+js7eKSemxqTVIo8yRlpRw+H1SdpzZxWruBc= -github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= +github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.194/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232 h1:kwsWbh4rEw42ZDe9/812ebhbwNZxlQyZ2sTmxBOKhN4= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.232/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.194/go.mod h1:yrBKWhChnDqNz1xuXdSbWXG56XawEq0G5j1lg4VwBD4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 h1:DYtBXB7sVc3EOW5horg8j55cLZynhsLYhHrvQ/jXKKM= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 h1:PlkFOALQZ9BLUyX8EalATUQD5xEn1Sz34C+Rw5VSpvk= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588/go.mod h1:vPvXNb+zBZVJfZCIKWcYxLpGzgScKKgiPUArobWZ+nU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 h1:5Tbi+jyZ2MojC6GK8V6hchwtnkP2IuENUTqSisbYOlA= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233/go.mod h1:sX14+NSvMjOhNFaMtP2aDy6Bss8PyFXij21gpY6+DAs= -github.com/tencentyun/cos-go-sdk-v5 v0.7.29 h1:uwRBzc70Wgtc5iQQCowqecfRT0OpCXUOZzodZHOOEDs= -github.com/tencentyun/cos-go-sdk-v5 v0.7.29/go.mod h1:4E4+bQ2gBVJcgEC9Cufwylio4mXOct2iu05WjgEBx1o= -github.com/tombuildsstuff/giovanni v0.15.1 h1:CVRaLOJ7C/eercCrKIsarfJ4SZoGMdBL9Q2deFDUXco= -github.com/tombuildsstuff/giovanni v0.15.1/go.mod h1:0TZugJPEtqzPlMpuJHYfXY6Dq2uLPrXf98D2XQSxNbA= -github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/tencentyun/cos-go-sdk-v5 v0.7.42 h1:Up1704BJjI5orycXKjpVpvuOInt9GC5pqY4knyE9Uds= +github.com/tencentyun/cos-go-sdk-v5 v0.7.42/go.mod h1:LUFnaqRmGk6pEHOaRmdn2dCZR2j0cSsM5xowWFPTPao= +github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o= +github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= -github.com/vmihailenco/msgpack/v4 v4.3.12 h1:07s4sz9IReOgdikxLTKNbBdqDMLsjPKXwvCazn8G65U= -github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= -github.com/vmihailenco/tagparser v0.1.1 h1:quXMXlA39OCbd2wAdTsGDlK9RkOk6Wuw+x37wVyIuWY= -github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= -github.com/xanzy/ssh-agent v0.3.1 h1:AmzO1SSWxw73zxFZPRwaMN1MohDw8UyHnmuxyceTEGo= -github.com/xanzy/ssh-agent v0.3.1/go.mod h1:QIE4lCeL7nkC25x+yA3LBIYfwCc1TFziCtG7cBAac6w= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557 h1:Jpn2j6wHkC9wJv5iMfJhKqrZJx3TahFx+7sbZ7zQdxs= github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= -github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= -github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= -github.com/zclconf/go-cty v1.11.0 h1:726SxLdi2SDnjY+BStqB9J1hNp4+2WlzyXLuimibIe0= -github.com/zclconf/go-cty v1.11.0/go.mod h1:s9IfD1LK5ccNMSWCVFCE2rJfHiZgi7JijgeWIMfhLvA= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= -github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= -github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0= -github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.mongodb.org/mongo-driver v1.16.1 h1:rIVLL3q0IHM39dvE+z2ulZLp9ENZKThVfuvN/IiN4l8= +go.mongodb.org/mongo-driver v1.16.1/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/exporters/autoexport v0.45.0 h1:KU3hwb3O+fc2F15lltmDgtH/QNfXZ7fvYGrZcKFDHxw= +go.opentelemetry.io/contrib/exporters/autoexport v0.45.0/go.mod h1:9hFI4YY6Ehe9enzw9qGlKAjJGQAtEo75Ysrb3byOZtI= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0 h1:bFkfHqO3IoO0VlUAuFxUhf5zctq/OD8H0wq77hxoeN4= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0/go.mod h1:2Wj/UyCzrPIweApqPFgXXRNZrpoz/sbU8UxeM6Dby3Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0 h1:Nw7Dv4lwvGrI68+wULbcq7su9K2cebeCUrDjVrUJHxM= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.19.0/go.mod h1:1MsF6Y7gTqosgoZvHlzcaaM8DIMNZgJh87ykokoNH7Y= +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.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.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= -golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +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-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e h1:qyrTQ++p1afMkO4DPEeLGq/3oTsdlvdH4vqZUBWzUKM= -golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= +golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= +golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -690,7 +1624,6 @@ golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRu golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= @@ -703,14 +1636,20 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -718,13 +1657,12 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -735,7 +1673,6 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -745,15 +1682,42 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -766,8 +1730,25 @@ golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f h1:Qmd2pbz05z7z6lm0DrgQVVPuBm92jqujBKMHMOlOQEw= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -779,32 +1760,43 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -818,8 +1810,9 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -829,39 +1822,118 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e h1:w36l2Uw3dRan1K3TyXriXvY+6T56GNmlKGcqiQUJDfM= -golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.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.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.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/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +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.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.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.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= -golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -875,13 +1947,13 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191203134012-c197fd4bf371/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -896,7 +1968,6 @@ golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjs golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -906,22 +1977,46 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= -golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= -golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/tools/cmd/cover v0.1.0-deprecated h1:Rwy+mWYz6loAF+LnG1jHG/JWMHRMMC2/1XX3Ejkx9lA= +golang.org/x/tools/cmd/cover v0.1.0-deprecated/go.mod h1:hMDiIvlpN1NoVgmjLjUJE9tMHyxHjFX7RuQ+rW12mSA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= +gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= +gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= +gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -943,19 +2038,56 @@ google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34q google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.44.0-impersonate-preview h1:/7twr+Es8HH6URUkpfcnn6mCadE7NCYmogHzBXNQZh4= -google.golang.org/api v0.44.0-impersonate-preview/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= +google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= +google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= +google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= +google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +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 v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -990,17 +2122,114 @@ google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= +google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= +google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= +google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= +google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= +google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= +google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= +google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= +google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1017,11 +2246,33 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0 h1:M1YKkFIboKNieVO5DLUEVzQfGwJD30Nv2jfUgzb5UcE= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= +google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= +google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 h1:TLkBREm4nIsEcexnCjgQd5GQWaHcqMzwQV0TX9pq8S0= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1034,8 +2285,16 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 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.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1044,18 +2303,16 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= @@ -1063,6 +2320,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1072,31 +2330,65 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.3.0 h1:2LdYUZ7CIxnYgskbUZfY7FPggmqnh6shBqfWa8Tn3XU= -honnef.co/go/tools v0.3.0/go.mod h1:vlRD9XErLMGT+mDuofSr0mMMquscM/1nQqtRSsh6m70= -k8s.io/api v0.23.4 h1:85gnfXQOWbJa1SiWGpE9EEtHs0UVvDyIsSMpEtl2D4E= -k8s.io/api v0.23.4/go.mod h1:i77F4JfyNNrhOjZF7OwwNJS5Y1S9dpwvb9iYRYRczfI= -k8s.io/apimachinery v0.23.4 h1:fhnuMd/xUL3Cjfl64j5ULKZ1/J9n8NuQEgNL+WXWfdM= -k8s.io/apimachinery v0.23.4/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= -k8s.io/client-go v0.23.4 h1:YVWvPeerA2gpUudLelvsolzH7c2sFoXXR5wM/sWqNFU= -k8s.io/client-go v0.23.4/go.mod h1:PKnIL4pqLuvYUK1WU7RLTMYKPiIh7MYShLshtRY9cj0= -k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +honnef.co/go/tools v0.6.0 h1:TAODvD3knlq75WCp2nyGJtT4LeRV/o7NN9nYPeVJXf8= +honnef.co/go/tools v0.6.0/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= +k8s.io/api v0.25.5 h1:mqyHf7aoaYMpdvO87mqpol+Qnsmo+y09S0PMIXwiZKo= +k8s.io/api v0.25.5/go.mod h1:RzplZX0Z8rV/WhSTfEvnyd91bBhBQTRWo85qBQwRmb8= +k8s.io/apimachinery v0.25.5 h1:SQomYHvv+aO43qdu3QKRf9YuI0oI8w3RrOQ1qPbAUGY= +k8s.io/apimachinery v0.25.5/go.mod h1:1S2i1QHkmxc8+EZCIxe/fX5hpldVXk4gvnJInMEb8D4= +k8s.io/client-go v0.25.5 h1:7QWVK0Ph4bLn0UwotPTc2FTgm8shreQXyvXnnHDd8rE= +k8s.io/client-go v0.25.5/go.mod h1:bOeoaUUdpyz3WDFGo+Xm3nOQFh2KuYXRDwrvbAPtFQA= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= -k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= -k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= -k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= +modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= +modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= +modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= +modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= +modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= +modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= +modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= +modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= +modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= +modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= +modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= +modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= -sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= -sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/help.go b/help.go index 9e38f8510c..0a7fdbac2a 100644 --- a/help.go +++ b/help.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -7,7 +10,7 @@ import ( "sort" "strings" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) // helpFunc is a cli.HelpFunc that can be used to output the help CLI instructions for Terraform. @@ -57,7 +60,7 @@ All other commands: Global options (use these before the subcommand, if any): -chdir=DIR Switch to a different working directory before executing the given subcommand. - -help Show this help output, or the help for a specified subcommand. + -help Show this help output or the help for a specified subcommand. -version An alias for the "version" subcommand. `, listCommands(commands, PrimaryCommands, maxKeyLen), listCommands(commands, otherCommands, maxKeyLen)) diff --git a/internal/addrs/action.go b/internal/addrs/action.go new file mode 100644 index 0000000000..4dc5ec6567 --- /dev/null +++ b/internal/addrs/action.go @@ -0,0 +1,329 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" + "strings" +) + +// Action is an address for an action block within configuration, which +// contains potentially-multiple action instances if that configuration +// block uses "count" or "for_each". +type Action struct { + referenceable + Type string + Name string +} + +func (a Action) String() string { + return fmt.Sprintf("action.%s.%s", a.Type, a.Name) +} + +func (a Action) Equal(o Action) bool { + return a.Name == o.Name && a.Type == o.Type +} + +func (a Action) Less(o Action) bool { + switch { + case a.Type != o.Type: + return a.Type < o.Type + + case a.Name != o.Name: + return a.Name < o.Name + + default: + return false + } +} + +func (a Action) UniqueKey() UniqueKey { + return a // An Action is its own UniqueKey +} + +func (a Action) uniqueKeySigil() {} + +// Instance produces the address for a specific instance of the receiver +// that is identified by the given key. +func (a Action) Instance(key InstanceKey) ActionInstance { + return ActionInstance{ + Action: a, + Key: key, + } +} + +// Absolute returns an AbsAction from the receiver and the given module +// instance address. +func (a Action) Absolute(module ModuleInstance) AbsAction { + return AbsAction{ + Module: module, + Action: a, + } +} + +// InModule returns a ConfigAction from the receiver and the given module +// address. +func (a Action) InModule(module Module) ConfigAction { + return ConfigAction{ + Module: module, + Action: a, + } +} + +// ImpliedProvider returns the implied provider type name, for e.g. the "aws" in +// "aws_instance" +func (a Action) ImpliedProvider() string { + typeName := a.Type + if under := strings.Index(typeName, "_"); under != -1 { + typeName = typeName[:under] + } + + return typeName +} + +// ActionInstance is an address for a specific instance of an action. +// When an action is defined in configuration with "count" or "for_each" it +// produces zero or more instances, which can be addressed using this type. +type ActionInstance struct { + referenceable + Action Action + Key InstanceKey +} + +func (a ActionInstance) ContainingAction() Action { + return a.Action +} + +func (a ActionInstance) String() string { + if a.Key == NoKey { + return a.Action.String() + } + return a.Action.String() + a.Key.String() +} + +func (a ActionInstance) Equal(o ActionInstance) bool { + return a.Key == o.Key && a.Action.Equal(o.Action) +} + +func (a ActionInstance) Less(o ActionInstance) bool { + if !a.Action.Equal(o.Action) { + return a.Action.Less(o.Action) + } + + if a.Key != o.Key { + return InstanceKeyLess(a.Key, o.Key) + } + + return false +} + +func (a ActionInstance) UniqueKey() UniqueKey { + return a // An ActionInstance is its own UniqueKey +} + +func (a ActionInstance) uniqueKeySigil() {} + +// Absolute returns an AbsActionInstance from the receiver and the given module +// instance address. +func (a ActionInstance) Absolute(module ModuleInstance) AbsActionInstance { + return AbsActionInstance{ + Module: module, + Action: a, + } +} + +// AbsAction is an absolute address for an action under a given module path. +type AbsAction struct { + Module ModuleInstance + Action Action +} + +// Action returns the address of a particular action within the receiver. +func (m ModuleInstance) Action(typeName string, name string) AbsAction { + return AbsAction{ + Module: m, + Action: Action{ + Type: typeName, + Name: name, + }, + } +} + +// Instance produces the address for a specific instance of the receiver that is +// identified by the given key. +func (a AbsAction) Instance(key InstanceKey) AbsActionInstance { + return AbsActionInstance{ + Module: a.Module, + Action: a.Action.Instance(key), + } +} + +// Config returns the unexpanded ConfigAction for this AbsAction. +func (a AbsAction) Config() ConfigAction { + return ConfigAction{ + Module: a.Module.Module(), + Action: a.Action, + } +} + +func (a AbsAction) String() string { + if len(a.Module) == 0 { + return a.Action.String() + } + return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String()) +} + +// AffectedAbsAction returns the AbsAction. +func (a AbsAction) AffectedAbsAction() AbsAction { + return a +} + +func (a AbsAction) Equal(o AbsAction) bool { + return a.Module.Equal(o.Module) && a.Action.Equal(o.Action) +} + +func (a AbsAction) Less(o AbsAction) bool { + if !a.Module.Equal(o.Module) { + return a.Module.Less(o.Module) + } + + if !a.Action.Equal(o.Action) { + return a.Action.Less(o.Action) + } + + return false +} + +type absActionKey string + +func (a absActionKey) uniqueKeySigil() {} + +func (a AbsAction) UniqueKey() UniqueKey { + return absActionKey(a.String()) +} + +// AbsActionInstance is an absolute address for an action instance under a +// given module path. +type AbsActionInstance struct { + Module ModuleInstance + Action ActionInstance +} + +// ActionInstance returns the address of a particular action instance within the receiver. +func (m ModuleInstance) ActionInstance(typeName string, name string, key InstanceKey) AbsActionInstance { + return AbsActionInstance{ + Module: m, + Action: ActionInstance{ + Action: Action{ + Type: typeName, + Name: name, + }, + Key: key, + }, + } +} + +// ContainingAction returns the address of the action that contains the +// receiving action instance. In other words, it discards the key portion of the +// address to produce an AbsAction value. +func (a AbsActionInstance) ContainingAction() AbsAction { + return AbsAction{ + Module: a.Module, + Action: a.Action.ContainingAction(), + } +} + +// ConfigAction returns the address of the configuration block that declared +// this instance. +func (a AbsActionInstance) ConfigAction() ConfigAction { + return ConfigAction{ + Module: a.Module.Module(), + Action: a.Action.Action, + } +} + +func (a AbsActionInstance) String() string { + if len(a.Module) == 0 { + return a.Action.String() + } + return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String()) +} + +// AffectedAbsAction returns the AbsAction for the instance. +func (a AbsActionInstance) AffectedAbsAction() AbsAction { + return AbsAction{ + Module: a.Module, + Action: a.Action.Action, + } +} + +func (a AbsActionInstance) Equal(o AbsActionInstance) bool { + return a.Module.Equal(o.Module) && a.Action.Equal(o.Action) +} + +// Less returns true if the receiver should sort before the given other value +// in a sorted list of addresses. +func (a AbsActionInstance) Less(o AbsActionInstance) bool { + if !a.Module.Equal(o.Module) { + return a.Module.Less(o.Module) + } + + if !a.Action.Equal(o.Action) { + return a.Action.Less(o.Action) + } + + return false +} + +type absActionInstanceKey string + +func (a AbsActionInstance) UniqueKey() UniqueKey { + return absActionInstanceKey(a.String()) +} + +func (r absActionInstanceKey) uniqueKeySigil() {} + +// ConfigAction is the address for an action within the configuration. +type ConfigAction struct { + Module Module + Action Action +} + +// Action returns the address of a particular action within the module. +func (m Module) Action(typeName string, name string) ConfigAction { + return ConfigAction{ + Module: m, + Action: Action{ + Type: typeName, + Name: name, + }, + } +} + +// Absolute produces the address for the receiver within a specific module instance. +func (a ConfigAction) Absolute(module ModuleInstance) AbsAction { + return AbsAction{ + Module: module, + Action: a.Action, + } +} + +func (a ConfigAction) String() string { + if len(a.Module) == 0 { + return a.Action.String() + } + return fmt.Sprintf("%s.%s", a.Module.String(), a.Action.String()) +} + +func (a ConfigAction) Equal(o ConfigAction) bool { + return a.Module.Equal(o.Module) && a.Action.Equal(o.Action) +} + +func (a ConfigAction) UniqueKey() UniqueKey { + return configActionKey(a.String()) +} + +type configActionKey string + +func (k configActionKey) uniqueKeySigil() {} diff --git a/internal/addrs/action_test.go b/internal/addrs/action_test.go new file mode 100644 index 0000000000..9415958759 --- /dev/null +++ b/internal/addrs/action_test.go @@ -0,0 +1,343 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" + "testing" +) + +func TestActionEqual(t *testing.T) { + actions := []Action{ + {Type: "foo", Name: "bar"}, + {Type: "the", Name: "bloop"}, + } + for _, r := range actions { + t.Run(r.String(), func(t *testing.T) { + if !r.Equal(r) { + t.Fatalf("expected %#v to be equal to itself", r) + } + }) + } + + // not equal + testCases := []struct { + right Action + left Action + }{ + { + Action{Type: "a", Name: "b"}, + Action{Type: "b", Name: "b"}, + }, + { + Action{Type: "a", Name: "b"}, + Action{Type: "a", Name: "c"}, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) { + if tc.left.Equal(tc.right) { + t.Fatalf("expected %#v not to be equal to %#v", tc.left, tc.right) + } + + if tc.right.Equal(tc.left) { + t.Fatalf("expected %#v not to be equal to %#v", tc.right, tc.left) + } + }) + } +} + +func TestActionInstanceEqual(t *testing.T) { + actions := []ActionInstance{ + { + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + { + Action: Action{Type: "the", Name: "bloop"}, + Key: StringKey("fish"), + }, + } + for _, r := range actions { + t.Run(r.String(), func(t *testing.T) { + if !r.Equal(r) { + t.Fatalf("expected %#v to be equal to itself", r) + } + }) + } + + // not equal + testCases := []struct { + right ActionInstance + left ActionInstance + }{ + { + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: IntKey(1), + }, + }, + { + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + ActionInstance{ + Action: Action{Type: "baz", Name: "bat"}, + Key: IntKey(1), + }, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) { + if tc.left.Equal(tc.right) { + t.Fatalf("expected %#v not to be equal to %#v", tc.left, tc.right) + } + + if tc.right.Equal(tc.left) { + t.Fatalf("expected %#v not to be equal to %#v", tc.right, tc.left) + } + }) + } +} + +func TestAbsActionInstanceEqual(t *testing.T) { + actions := []AbsActionInstance{ + { + RootModuleInstance, + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + }, + { + mustParseModuleInstanceStr("module.child"), + ActionInstance{ + Action: Action{Type: "the", Name: "bloop"}, + Key: StringKey("fish"), + }, + }, + } + + for _, r := range actions { + t.Run(r.String(), func(t *testing.T) { + if !r.Equal(r) { + t.Fatalf("expected %#v to be equal to itself", r) + } + }) + } + + // not equal + testCases := []struct { + right AbsActionInstance + left AbsActionInstance + }{ + { // different keys + AbsActionInstance{ + RootModuleInstance, + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + }, + AbsActionInstance{ + RootModuleInstance, + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: IntKey(1), + }, + }, + }, + + { // different module + AbsActionInstance{ + RootModuleInstance, + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + }, + AbsActionInstance{ + mustParseModuleInstanceStr("module.child[1]"), + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: NoKey, + }, + }, + }, + + { // totally different + AbsActionInstance{ + RootModuleInstance, + ActionInstance{ + Action: Action{Type: "oof", Name: "rab"}, + Key: NoKey, + }, + }, + AbsActionInstance{ + mustParseModuleInstanceStr("module.foo"), + ActionInstance{ + Action: Action{Type: "foo", Name: "bar"}, + Key: IntKey(11), + }, + }, + }, + } + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) { + if tc.left.Equal(tc.right) { + t.Fatalf("expected %#v not to be equal to %#v", tc.left, tc.right) + } + + if tc.right.Equal(tc.left) { + t.Fatalf("expected %#v not to be equal to %#v", tc.right, tc.left) + } + }) + } +} + +// TestConfigActionEqual +func TestConfigActionEqual(t *testing.T) { + actions := []ConfigAction{ + { + RootModule, + Action{Type: "foo", Name: "bar"}, + }, + { + Module{"child"}, + Action{Type: "the", Name: "bloop"}, + }, + } + for _, r := range actions { + t.Run(r.String(), func(t *testing.T) { + if !r.Equal(r) { + t.Fatalf("expected %#v to be equal to itself", r) + } + }) + } + + // not equal + testCases := []struct { + right ConfigAction + left ConfigAction + }{ + { // different name + ConfigAction{ + RootModule, + Action{Type: "foo", Name: "bar"}, + }, + ConfigAction{ + RootModule, + Action{Type: "foo", Name: "baz"}, + }, + }, + // different type + { + ConfigAction{ + RootModule, + Action{Type: "foo", Name: "bar"}, + }, + ConfigAction{ + RootModule, + Action{Type: "baz", Name: "bar"}, + }, + }, + // different Module + { + ConfigAction{ + RootModule, + Action{Type: "foo", Name: "bar"}, + }, + ConfigAction{ + Module{"mod"}, + Action{Type: "foo", Name: "bar"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s = %s", tc.left, tc.right), func(t *testing.T) { + if tc.left.Equal(tc.right) { + t.Fatalf("expected %#v not to be equal to %#v", tc.left, tc.right) + } + + if tc.right.Equal(tc.left) { + t.Fatalf("expected %#v not to be equal to %#v", tc.right, tc.left) + } + }) + } +} + +// TestAbsActionUniqueKey +func TestAbsActionUniqueKey(t *testing.T) { + actionAddr1 := Action{ + Type: "a", + Name: "b1", + }.Absolute(RootModuleInstance) + actionAddr2 := Action{ + Type: "a", + Name: "b2", + }.Absolute(RootModuleInstance) + actionAddr3 := Action{ + Type: "a", + Name: "in_module", + }.Absolute(RootModuleInstance.Child("boop", NoKey)) + + tests := []struct { + Receiver AbsAction + Other UniqueKeyer + WantEqual bool + }{ + { + actionAddr1, + actionAddr1, + true, + }, + { + actionAddr1, + actionAddr2, + false, + }, + { + actionAddr1, + actionAddr3, + false, + }, + { + actionAddr3, + actionAddr3, + true, + }, + { + actionAddr1, + actionAddr1.Instance(NoKey), + false, // no-key instance key is distinct from its resource even though they have the same String result + }, + { + actionAddr1, + actionAddr1.Instance(IntKey(1)), + false, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%s matches %T %s?", test.Receiver, test.Other, test.Other), func(t *testing.T) { + rKey := test.Receiver.UniqueKey() + oKey := test.Other.UniqueKey() + + gotEqual := rKey == oKey + if gotEqual != test.WantEqual { + t.Errorf( + "wrong result\nreceiver: %s\nother: %s (%T)\ngot: %t\nwant: %t", + test.Receiver, test.Other, test.Other, + gotEqual, test.WantEqual, + ) + } + }) + } +} diff --git a/internal/addrs/check.go b/internal/addrs/check.go index 430b50c990..8a152aa374 100644 --- a/internal/addrs/check.go +++ b/internal/addrs/check.go @@ -1,251 +1,134 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs -import ( - "fmt" +import "fmt" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// Check is the address of a check rule within a checkable object. +// Check is the address of a check block within a module. // -// This represents the check rule globally within a configuration, and is used -// during graph evaluation to identify a condition result object to update with -// the result of check rule evaluation. -// -// The check address is not distinct from resource traversals, and check rule -// values are not intended to be available to the language, so the address is -// not Referenceable. -// -// Note also that the check address is only relevant within the scope of a run, -// as reordering check blocks between runs will result in their addresses -// changing. Check is therefore for internal use only and should not be exposed -// in durable artifacts such as state snapshots. +// For now, checks do not support meta arguments such as "count" or "for_each" +// so this address uniquely describes a single check within a module. type Check struct { - Container Checkable - Type CheckType - Index int -} - -func NewCheck(container Checkable, typ CheckType, index int) Check { - return Check{ - Container: container, - Type: typ, - Index: index, - } + referenceable + Name string } func (c Check) String() string { - container := c.Container.String() - switch c.Type { - case ResourcePrecondition: - return fmt.Sprintf("%s.precondition[%d]", container, c.Index) - case ResourcePostcondition: - return fmt.Sprintf("%s.postcondition[%d]", container, c.Index) - case OutputPrecondition: - return fmt.Sprintf("%s.precondition[%d]", container, c.Index) - default: - // This should not happen - return fmt.Sprintf("%s.condition[%d]", container, c.Index) + return fmt.Sprintf("check.%s", c.Name) +} + +// InModule returns a ConfigCheck from the receiver and the given module +// address. +func (c Check) InModule(modAddr Module) ConfigCheck { + return ConfigCheck{ + Module: modAddr, + Check: c, } } +// Absolute returns an AbsCheck from the receiver and the given module instance +// address. +func (c Check) Absolute(modAddr ModuleInstance) AbsCheck { + return AbsCheck{ + Module: modAddr, + Check: c, + } +} + +func (c Check) Equal(o Check) bool { + return c.Name == o.Name +} + func (c Check) UniqueKey() UniqueKey { - return checkKey{ - ContainerKey: c.Container.UniqueKey(), - Type: c.Type, - Index: c.Index, - } + return c // A Check is its own UniqueKey } -type checkKey struct { - ContainerKey UniqueKey - Type CheckType - Index int -} +func (c Check) uniqueKeySigil() {} -func (k checkKey) uniqueKeySigil() {} - -// CheckType describes a category of check. We use this only to establish -// uniqueness for Check values, and do not expose this concept of "check types" -// (which is subject to change in future) in any durable artifacts such as -// state snapshots. +// ConfigCheck is an address for a check block within a configuration. // -// (See [CheckableKind] for an enumeration that we _do_ use externally, to -// describe the type of object being checked rather than the type of the check -// itself.) -type CheckType int +// This contains a Check address and a Module address, meaning this describes +// a check block within the entire configuration. +type ConfigCheck struct { + Module Module + Check Check +} -//go:generate go run golang.org/x/tools/cmd/stringer -type=CheckType check.go +var _ ConfigCheckable = ConfigCheck{} -const ( - InvalidCondition CheckType = 0 - ResourcePrecondition CheckType = 1 - ResourcePostcondition CheckType = 2 - OutputPrecondition CheckType = 3 -) +func (c ConfigCheck) UniqueKey() UniqueKey { + return configCheckUniqueKey(c.String()) +} -// Description returns a human-readable description of the check type. This is -// presented in the user interface through a diagnostic summary. -func (c CheckType) Description() string { - switch c { - case ResourcePrecondition: - return "Resource precondition" - case ResourcePostcondition: - return "Resource postcondition" - case OutputPrecondition: - return "Module output value precondition" - default: - // This should not happen - return "Condition" +func (c ConfigCheck) configCheckableSigil() {} + +func (c ConfigCheck) CheckableKind() CheckableKind { + return CheckableCheck +} + +func (c ConfigCheck) String() string { + if len(c.Module) == 0 { + return c.Check.String() } + return fmt.Sprintf("%s.%s", c.Module, c.Check) } -// Checkable is an interface implemented by all address types that can contain -// condition blocks. -type Checkable interface { - UniqueKeyer - - checkableSigil() - - // Check returns the address of an individual check rule of a specified - // type and index within this checkable container. - Check(CheckType, int) Check - - // ConfigCheckable returns the address of the configuration construct that - // this Checkable belongs to. - // - // Checkable objects can potentially be dynamically declared during a - // plan operation using constructs like resource for_each, and so - // ConfigCheckable gives us a way to talk about the static containers - // those dynamic objects belong to, in case we wish to group together - // dynamic checkable objects into their static checkable for reporting - // purposes. - ConfigCheckable() ConfigCheckable - - CheckableKind() CheckableKind - String() string -} - -var ( - _ Checkable = AbsResourceInstance{} - _ Checkable = AbsOutputValue{} -) - -// CheckableKind describes the different kinds of checkable objects. -type CheckableKind rune - -//go:generate go run golang.org/x/tools/cmd/stringer -type=CheckableKind check.go - -const ( - CheckableKindInvalid CheckableKind = 0 - CheckableResource CheckableKind = 'R' - CheckableOutputValue CheckableKind = 'O' -) - -// ConfigCheckable is an interfaces implemented by address types that represent -// configuration constructs that can have Checkable addresses associated with -// them. +// AbsCheck is an absolute address for a check block under a given module path. // -// This address type therefore in a sense represents a container for zero or -// more checkable objects all declared by the same configuration construct, -// so that we can talk about these groups of checkable objects before we're -// ready to decide how many checkable objects belong to each one. -type ConfigCheckable interface { - UniqueKeyer - - configCheckableSigil() - - CheckableKind() CheckableKind - String() string +// This contains an actual ModuleInstance address (compared to the Module within +// a ConfigCheck), meaning this uniquely describes a check block within the +// entire configuration after any "count" or "foreach" meta arguments have been +// evaluated on the containing module. +type AbsCheck struct { + Module ModuleInstance + Check Check } -var ( - _ ConfigCheckable = ConfigResource{} - _ ConfigCheckable = ConfigOutputValue{} -) +var _ Checkable = AbsCheck{} -// ParseCheckableStr attempts to parse the given string as a Checkable address -// of the given kind. +func (c AbsCheck) UniqueKey() UniqueKey { + return absCheckUniqueKey(c.String()) +} + +func (c AbsCheck) checkableSigil() {} + +// CheckRule returns an address for a given rule type within the check block. // -// This should be the opposite of Checkable.String for any Checkable address -// type, as long as "kind" is set to the value returned by the address's -// CheckableKind method. -// -// We do not typically expect users to write out checkable addresses as input, -// but we use them as part of some of our wire formats for persisting check -// results between runs. -func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(src), "", hcl.InitialPos) - diags = diags.Append(parseDiags) - if parseDiags.HasErrors() { - return nil, diags - } - - path, remain, diags := parseModuleInstancePrefix(traversal) - if diags.HasErrors() { - return nil, diags - } - - if remain.IsRelative() { - // (relative means that there's either nothing left or what's next isn't an identifier) - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid checkable address", - Detail: "Module path must be followed by either a resource instance address or an output value address.", - Subject: remain.SourceRange().Ptr(), - }) - return nil, diags - } - - // We use "kind" to disambiguate here because unfortunately we've - // historically never reserved "output" as a possible resource type name - // and so it is in principle possible -- albeit unlikely -- that there - // might be a resource whose type is literally "output". - switch kind { - case CheckableResource: - riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain) - diags = diags.Append(moreDiags) - if diags.HasErrors() { - return nil, diags - } - return riAddr, diags - - case CheckableOutputValue: - if len(remain) != 2 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid checkable address", - Detail: "Output address must have only one attribute part after the keyword 'output', giving the name of the output value.", - Subject: remain.SourceRange().Ptr(), - }) - return nil, diags - } - if remain.RootName() != "output" { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid checkable address", - Detail: "Output address must follow the module address with the keyword 'output'.", - Subject: remain.SourceRange().Ptr(), - }) - return nil, diags - } - if step, ok := remain[1].(hcl.TraverseAttr); !ok { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid checkable address", - Detail: "Output address must have only one attribute part after the keyword 'output', giving the name of the output value.", - Subject: remain.SourceRange().Ptr(), - }) - return nil, diags - } else { - return OutputValue{Name: step.Name}.Absolute(path), diags - } - - default: - panic(fmt.Sprintf("unsupported CheckableKind %s", kind)) +// There will be at most one CheckDataResource rule within a check block (with +// an index of 0). There will be at least one, but potentially many, +// CheckAssertion rules within a check block. +func (c AbsCheck) CheckRule(typ CheckRuleType, i int) CheckRule { + return CheckRule{ + Container: c, + Type: typ, + Index: i, } } + +// ConfigCheckable returns the ConfigCheck address for this absolute reference. +func (c AbsCheck) ConfigCheckable() ConfigCheckable { + return ConfigCheck{ + Module: c.Module.Module(), + Check: c.Check, + } +} + +func (c AbsCheck) CheckableKind() CheckableKind { + return CheckableCheck +} + +func (c AbsCheck) String() string { + if len(c.Module) == 0 { + return c.Check.String() + } + return fmt.Sprintf("%s.%s", c.Module, c.Check) +} + +type configCheckUniqueKey string + +func (k configCheckUniqueKey) uniqueKeySigil() {} + +type absCheckUniqueKey string + +func (k absCheckUniqueKey) uniqueKeySigil() {} diff --git a/internal/addrs/check_rule.go b/internal/addrs/check_rule.go new file mode 100644 index 0000000000..f6c558e942 --- /dev/null +++ b/internal/addrs/check_rule.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" +) + +// CheckRule is the address of a check rule within a checkable object. +// +// This represents the check rule globally within a configuration, and is used +// during graph evaluation to identify a condition result object to update with +// the result of check rule evaluation. +// +// The check address is not distinct from resource traversals, and check rule +// values are not intended to be available to the language, so the address is +// not Referenceable. +// +// Note also that the check address is only relevant within the scope of a run, +// as reordering check blocks between runs will result in their addresses +// changing. CheckRule is therefore for internal use only and should not be +// exposed in durable artifacts such as state snapshots. +type CheckRule struct { + Container Checkable + Type CheckRuleType + Index int +} + +func NewCheckRule(container Checkable, typ CheckRuleType, index int) CheckRule { + return CheckRule{ + Container: container, + Type: typ, + Index: index, + } +} + +func (c CheckRule) String() string { + container := c.Container.String() + switch c.Type { + case ResourcePrecondition: + return fmt.Sprintf("%s.precondition[%d]", container, c.Index) + case ResourcePostcondition: + return fmt.Sprintf("%s.postcondition[%d]", container, c.Index) + case OutputPrecondition: + return fmt.Sprintf("%s.precondition[%d]", container, c.Index) + case CheckDataResource: + return fmt.Sprintf("%s.data[%d]", container, c.Index) + case CheckAssertion: + return fmt.Sprintf("%s.assert[%d]", container, c.Index) + case InputValidation: + return fmt.Sprintf("%s.validation[%d]", container, c.Index) + default: + // This should not happen + return fmt.Sprintf("%s.condition[%d]", container, c.Index) + } +} + +func (c CheckRule) UniqueKey() UniqueKey { + return checkRuleKey{ + ContainerKey: c.Container.UniqueKey(), + Type: c.Type, + Index: c.Index, + } +} + +type checkRuleKey struct { + ContainerKey UniqueKey + Type CheckRuleType + Index int +} + +func (k checkRuleKey) uniqueKeySigil() {} + +// CheckRuleType describes a category of check. We use this only to establish +// uniqueness for Check values, and do not expose this concept of "check types" +// (which is subject to change in future) in any durable artifacts such as +// state snapshots. +// +// (See [CheckableKind] for an enumeration that we _do_ use externally, to +// describe the type of object being checked rather than the type of the check +// itself.) +type CheckRuleType int + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=CheckRuleType check_rule.go + +const ( + InvalidCondition CheckRuleType = 0 + ResourcePrecondition CheckRuleType = 1 + ResourcePostcondition CheckRuleType = 2 + OutputPrecondition CheckRuleType = 3 + CheckDataResource CheckRuleType = 4 + CheckAssertion CheckRuleType = 5 + InputValidation CheckRuleType = 6 +) + +// Description returns a human-readable description of the check type. This is +// presented in the user interface through a diagnostic summary. +func (c CheckRuleType) Description() string { + switch c { + case ResourcePrecondition: + return "Resource precondition" + case ResourcePostcondition: + return "Resource postcondition" + case OutputPrecondition: + return "Module output value precondition" + case CheckDataResource: + return "Check block data resource" + case CheckAssertion: + return "Check block assertion" + case InputValidation: + return "Input variable validation" + default: + // This should not happen + return "Condition" + } +} diff --git a/internal/addrs/check_rule_diagnostic.go b/internal/addrs/check_rule_diagnostic.go new file mode 100644 index 0000000000..181ca9369c --- /dev/null +++ b/internal/addrs/check_rule_diagnostic.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// DiagnosticExtraCheckRule provides an interface for diagnostic ExtraInfo to +// retrieve an embedded CheckRule from within a tfdiags.Diagnostic. +type DiagnosticExtraCheckRule interface { + // DiagnosticOriginatesFromCheckRule returns the CheckRule that the + // surrounding diagnostic originated from. + DiagnosticOriginatesFromCheckRule() CheckRule +} + +// DiagnosticOriginatesFromCheckRule checks if the provided diagnostic contains +// a CheckRule as ExtraInfo and returns that CheckRule and true if it does. This +// function returns an empty CheckRule and false if the diagnostic does not +// contain a CheckRule. +func DiagnosticOriginatesFromCheckRule(diag tfdiags.Diagnostic) (CheckRule, bool) { + maybe := tfdiags.ExtraInfo[DiagnosticExtraCheckRule](diag) + if maybe == nil { + return CheckRule{}, false + } + return maybe.DiagnosticOriginatesFromCheckRule(), true +} + +// CheckRuleDiagnosticExtra is an object that can be attached to diagnostics +// that originate from check rules. +// +// It implements the DiagnosticExtraCheckRule interface for retrieving the +// concrete CheckRule that spawned the diagnostic. +// +// It also implements the tfdiags.DiagnosticExtraDoNotConsolidate interface, to +// stop diagnostics created by check blocks being consolidated. +// +// It also implements the tfdiags.DiagnosticExtraUnwrapper interface, as nested +// data blocks will attach this struct but do want to lose any extra info +// embedded in the original diagnostic. +type CheckRuleDiagnosticExtra struct { + CheckRule CheckRule + + wrapped interface{} +} + +var ( + _ DiagnosticExtraCheckRule = (*CheckRuleDiagnosticExtra)(nil) + _ tfdiags.DiagnosticExtraDoNotConsolidate = (*CheckRuleDiagnosticExtra)(nil) + _ tfdiags.DiagnosticExtraUnwrapper = (*CheckRuleDiagnosticExtra)(nil) + _ tfdiags.DiagnosticExtraWrapper = (*CheckRuleDiagnosticExtra)(nil) +) + +func (c *CheckRuleDiagnosticExtra) UnwrapDiagnosticExtra() interface{} { + return c.wrapped +} + +func (c *CheckRuleDiagnosticExtra) WrapDiagnosticExtra(inner interface{}) { + if c.wrapped != nil { + // This is a logical inconsistency, the caller should know whether they + // have already wrapped an extra or not. + panic("Attempted to wrap a diagnostic extra into a CheckRuleDiagnosticExtra that is already wrapping a different extra. This is a bug in Terraform, please report it.") + } + c.wrapped = inner +} + +func (c *CheckRuleDiagnosticExtra) DoNotConsolidateDiagnostic() bool { + // Do not consolidate warnings from check blocks. + return c.CheckRule.Container.CheckableKind() == CheckableCheck +} + +func (c *CheckRuleDiagnosticExtra) DiagnosticOriginatesFromCheckRule() CheckRule { + return c.CheckRule +} diff --git a/internal/addrs/check_rule_diagnostic_test.go b/internal/addrs/check_rule_diagnostic_test.go new file mode 100644 index 0000000000..8c3e1534f8 --- /dev/null +++ b/internal/addrs/check_rule_diagnostic_test.go @@ -0,0 +1,111 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestCheckRuleDiagnosticExtra_WrapsExtra(t *testing.T) { + var originals tfdiags.Diagnostics + originals = originals.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "original error", + Detail: "this is an error", + Extra: "extra", + }) + + overridden := tfdiags.OverrideAll(originals, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper { + return &CheckRuleDiagnosticExtra{} + }) + + if overridden[0].ExtraInfo().(*CheckRuleDiagnosticExtra).wrapped.(string) != "extra" { + t.Errorf("unexpected extra info: %v", overridden[0].ExtraInfo()) + } +} + +func TestCheckRuleDiagnosticExtra_Unwraps(t *testing.T) { + var originals tfdiags.Diagnostics + originals = originals.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "original error", + Detail: "this is an error", + Extra: "extra", + }) + + overridden := tfdiags.OverrideAll(originals, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper { + return &CheckRuleDiagnosticExtra{} + }) + + result := tfdiags.ExtraInfo[string](overridden[0]) + if result != "extra" { + t.Errorf("unexpected extra info: %v", result) + } +} + +func TestCheckRuleDiagnosticExtra_DoNotConsolidate(t *testing.T) { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "original error", + Detail: "this is an error", + Extra: &CheckRuleDiagnosticExtra{ + CheckRule: NewCheckRule(AbsOutputValue{ + Module: RootModuleInstance, + OutputValue: OutputValue{ + Name: "output", + }, + }, OutputPrecondition, 0), + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "original error", + Detail: "this is an error", + Extra: &CheckRuleDiagnosticExtra{ + CheckRule: NewCheckRule(AbsCheck{ + Module: RootModuleInstance, + Check: Check{ + Name: "check", + }, + }, CheckAssertion, 0), + }, + }) + + if tfdiags.DoNotConsolidateDiagnostic(diags[0]) { + t.Errorf("first diag should be consolidated but was not") + } + + if !tfdiags.DoNotConsolidateDiagnostic(diags[1]) { + t.Errorf("second diag should not be consolidated but was") + } + +} + +func TestDiagnosticOriginatesFromCheckRule_Passes(t *testing.T) { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "original error", + Detail: "this is an error", + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "original error", + Detail: "this is an error", + Extra: &CheckRuleDiagnosticExtra{}, + }) + + if _, ok := DiagnosticOriginatesFromCheckRule(diags[0]); ok { + t.Errorf("first diag did not originate from check rule but thinks it did") + } + + if _, ok := DiagnosticOriginatesFromCheckRule(diags[1]); !ok { + t.Errorf("second diag did originate from check rule but this it did not") + } +} diff --git a/internal/addrs/checkable.go b/internal/addrs/checkable.go new file mode 100644 index 0000000000..19a5c8afcf --- /dev/null +++ b/internal/addrs/checkable.go @@ -0,0 +1,194 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Checkable is an interface implemented by all address types that can contain +// condition blocks. +type Checkable interface { + UniqueKeyer + + checkableSigil() + + // CheckRule returns the address of an individual check rule of a specified + // type and index within this checkable container. + CheckRule(CheckRuleType, int) CheckRule + + // ConfigCheckable returns the address of the configuration construct that + // this Checkable belongs to. + // + // Checkable objects can potentially be dynamically declared during a + // plan operation using constructs like resource for_each, and so + // ConfigCheckable gives us a way to talk about the static containers + // those dynamic objects belong to, in case we wish to group together + // dynamic checkable objects into their static checkable for reporting + // purposes. + ConfigCheckable() ConfigCheckable + + CheckableKind() CheckableKind + String() string +} + +var ( + _ Checkable = AbsResourceInstance{} + _ Checkable = AbsOutputValue{} +) + +// CheckableKind describes the different kinds of checkable objects. +type CheckableKind rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=CheckableKind checkable.go + +const ( + CheckableKindInvalid CheckableKind = 0 + CheckableResource CheckableKind = 'R' + CheckableOutputValue CheckableKind = 'O' + CheckableCheck CheckableKind = 'C' + CheckableInputVariable CheckableKind = 'I' +) + +// ConfigCheckable is an interfaces implemented by address types that represent +// configuration constructs that can have Checkable addresses associated with +// them. +// +// This address type therefore in a sense represents a container for zero or +// more checkable objects all declared by the same configuration construct, +// so that we can talk about these groups of checkable objects before we're +// ready to decide how many checkable objects belong to each one. +type ConfigCheckable interface { + UniqueKeyer + + configCheckableSigil() + + CheckableKind() CheckableKind + String() string +} + +var ( + _ ConfigCheckable = ConfigResource{} + _ ConfigCheckable = ConfigOutputValue{} +) + +// ParseCheckableStr attempts to parse the given string as a Checkable address +// of the given kind. +// +// This should be the opposite of Checkable.String for any Checkable address +// type, as long as "kind" is set to the value returned by the address's +// CheckableKind method. +// +// We do not typically expect users to write out checkable addresses as input, +// but we use them as part of some of our wire formats for persisting check +// results between runs. +func ParseCheckableStr(kind CheckableKind, src string) (Checkable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(src), "", hcl.InitialPos) + diags = diags.Append(parseDiags) + if parseDiags.HasErrors() { + return nil, diags + } + + path, remain, diags := parseModuleInstancePrefix(traversal, false) + if diags.HasErrors() { + return nil, diags + } + + if remain.IsRelative() { + // (relative means that there's either nothing left or what's next isn't an identifier) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid checkable address", + Detail: "Module path must be followed by either a resource instance address or an output value address.", + Subject: remain.SourceRange().Ptr(), + }) + return nil, diags + } + + getCheckableName := func(keyword string, descriptor string) (string, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var name string + + if len(remain) != 2 { + diags = diags.Append(hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid checkable address", + Detail: fmt.Sprintf("%s address must have only one attribute part after the keyword '%s', giving the name of the %s.", cases.Title(language.English, cases.NoLower).String(keyword), keyword, descriptor), + Subject: remain.SourceRange().Ptr(), + }) + } + + if remain.RootName() != keyword { + diags = diags.Append(hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid checkable address", + Detail: fmt.Sprintf("%s address must follow the module address with the keyword '%s'.", cases.Title(language.English, cases.NoLower).String(keyword), keyword), + Subject: remain.SourceRange().Ptr(), + }) + } + if step, ok := remain[1].(hcl.TraverseAttr); !ok { + diags = diags.Append(hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid checkable address", + Detail: fmt.Sprintf("%s address must have only one attribute part after the keyword '%s', giving the name of the %s.", cases.Title(language.English, cases.NoLower).String(keyword), keyword, descriptor), + Subject: remain.SourceRange().Ptr(), + }) + } else { + name = step.Name + } + + return name, diags + } + + // We use "kind" to disambiguate here because unfortunately we've + // historically never reserved "output" as a possible resource type name + // and so it is in principle possible -- albeit unlikely -- that there + // might be a resource whose type is literally "output". + switch kind { + case CheckableResource: + riAddr, moreDiags := parseResourceInstanceUnderModule(path, false, remain) + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return nil, diags + } + return riAddr, diags + + case CheckableOutputValue: + name, nameDiags := getCheckableName("output", "output value") + diags = diags.Append(nameDiags) + if diags.HasErrors() { + return nil, diags + } + return OutputValue{Name: name}.Absolute(path), diags + + case CheckableCheck: + name, nameDiags := getCheckableName("check", "check block") + diags = diags.Append(nameDiags) + if diags.HasErrors() { + return nil, diags + } + return Check{Name: name}.Absolute(path), diags + + case CheckableInputVariable: + name, nameDiags := getCheckableName("var", "variable value") + diags = diags.Append(nameDiags) + if diags.HasErrors() { + return nil, diags + } + return InputVariable{Name: name}.Absolute(path), diags + + default: + panic(fmt.Sprintf("unsupported CheckableKind %s", kind)) + } +} diff --git a/internal/addrs/checkablekind_string.go b/internal/addrs/checkablekind_string.go index 8987cd02bf..80578d4b7e 100644 --- a/internal/addrs/checkablekind_string.go +++ b/internal/addrs/checkablekind_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=CheckableKind check.go"; DO NOT EDIT. +// Code generated by "stringer -type=CheckableKind checkable.go"; DO NOT EDIT. package addrs @@ -11,22 +11,30 @@ func _() { _ = x[CheckableKindInvalid-0] _ = x[CheckableResource-82] _ = x[CheckableOutputValue-79] + _ = x[CheckableCheck-67] + _ = x[CheckableInputVariable-73] } const ( _CheckableKind_name_0 = "CheckableKindInvalid" - _CheckableKind_name_1 = "CheckableOutputValue" - _CheckableKind_name_2 = "CheckableResource" + _CheckableKind_name_1 = "CheckableCheck" + _CheckableKind_name_2 = "CheckableInputVariable" + _CheckableKind_name_3 = "CheckableOutputValue" + _CheckableKind_name_4 = "CheckableResource" ) func (i CheckableKind) String() string { switch { case i == 0: return _CheckableKind_name_0 - case i == 79: + case i == 67: return _CheckableKind_name_1 - case i == 82: + case i == 73: return _CheckableKind_name_2 + case i == 79: + return _CheckableKind_name_3 + case i == 82: + return _CheckableKind_name_4 default: return "CheckableKind(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/internal/addrs/checkruletype_string.go b/internal/addrs/checkruletype_string.go new file mode 100644 index 0000000000..18726e211a --- /dev/null +++ b/internal/addrs/checkruletype_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type=CheckRuleType check_rule.go"; DO NOT EDIT. + +package addrs + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[InvalidCondition-0] + _ = x[ResourcePrecondition-1] + _ = x[ResourcePostcondition-2] + _ = x[OutputPrecondition-3] + _ = x[CheckDataResource-4] + _ = x[CheckAssertion-5] + _ = x[InputValidation-6] +} + +const _CheckRuleType_name = "InvalidConditionResourcePreconditionResourcePostconditionOutputPreconditionCheckDataResourceCheckAssertionInputValidation" + +var _CheckRuleType_index = [...]uint8{0, 16, 36, 57, 75, 92, 106, 121} + +func (i CheckRuleType) String() string { + if i < 0 || i >= CheckRuleType(len(_CheckRuleType_index)-1) { + return "CheckRuleType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _CheckRuleType_name[_CheckRuleType_index[i]:_CheckRuleType_index[i+1]] +} diff --git a/internal/addrs/checktype_string.go b/internal/addrs/checktype_string.go deleted file mode 100644 index 8c2fcebd4f..0000000000 --- a/internal/addrs/checktype_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=CheckType check.go"; DO NOT EDIT. - -package addrs - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[InvalidCondition-0] - _ = x[ResourcePrecondition-1] - _ = x[ResourcePostcondition-2] - _ = x[OutputPrecondition-3] -} - -const _CheckType_name = "InvalidConditionResourcePreconditionResourcePostconditionOutputPrecondition" - -var _CheckType_index = [...]uint8{0, 16, 36, 57, 75} - -func (i CheckType) String() string { - if i < 0 || i >= CheckType(len(_CheckType_index)-1) { - return "CheckType(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _CheckType_name[_CheckType_index[i]:_CheckType_index[i+1]] -} diff --git a/internal/addrs/count_attr.go b/internal/addrs/count_attr.go index 0be5c02648..981014c91c 100644 --- a/internal/addrs/count_attr.go +++ b/internal/addrs/count_attr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // CountAttr is the address of an attribute of the "count" object in diff --git a/internal/addrs/doc.go b/internal/addrs/doc.go index 46093314fe..4c83a0602e 100644 --- a/internal/addrs/doc.go +++ b/internal/addrs/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package addrs contains types that represent "addresses", which are // references to specific objects within a Terraform configuration or // state. diff --git a/internal/addrs/for_each_attr.go b/internal/addrs/for_each_attr.go index 6b0c06096c..bf4151f21e 100644 --- a/internal/addrs/for_each_attr.go +++ b/internal/addrs/for_each_attr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // ForEachAttr is the address of an attribute referencing the current "for_each" object in diff --git a/internal/addrs/graph.go b/internal/addrs/graph.go new file mode 100644 index 0000000000..59a38e8f25 --- /dev/null +++ b/internal/addrs/graph.go @@ -0,0 +1,214 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform/internal/dag" +) + +// DirectedGraph represents a directed graph whose nodes are addresses of +// type T. +// +// This graph type supports directed edges between pairs of addresses, and +// because Terraform most commonly uses graphs to represent dependency +// relationships it uses "dependency" and "dependent" as the names of the +// endpoints of an edge, even though technically this data structure could +// be used to represent other kinds of directed relationships if needed. +// When used as an operation dependency graph, the "dependency" must be visited +// before the "dependent". +// +// This data structure is not concurrency-safe for writes and so callers must +// supply suitable synchronization primitives if modifying a graph concurrently +// with readers or other writers. Concurrent reads of an already-constructed +// graph are safe. +type DirectedGraph[T UniqueKeyer] struct { + // Our dag.AcyclicGraph implementation is a little quirky but also + // well-tested and stable, so we'll use that for the underlying + // graph operations and just wrap a slightly nicer address-oriented + // API around it. + // Reusing this does mean that some of our operations end up allocating + // more than they would need to otherwise, so perhaps we'll revisit this + // in future if it seems to be causing performance problems. + g *dag.AcyclicGraph + + // dag.AcyclicGraph can only support node types that are either + // comparable using == or that implement a special "hashable" + // interface, so we'll use our UniqueKeyer technique to produce + // suitable node values but we need a sidecar structure to remember + // which real address value belongs to each node value. + nodes map[UniqueKey]T +} + +func NewDirectedGraph[T UniqueKeyer]() DirectedGraph[T] { + return DirectedGraph[T]{ + g: &dag.AcyclicGraph{}, + nodes: map[UniqueKey]T{}, + } +} + +func (g DirectedGraph[T]) Add(addr T) { + k := addr.UniqueKey() + g.nodes[k] = addr + g.g.Add(k) +} + +func (g DirectedGraph[T]) Has(addr T) bool { + k := addr.UniqueKey() + _, ok := g.nodes[k] + return ok +} + +func (g DirectedGraph[T]) Remove(addr T) { + k := addr.UniqueKey() + g.g.Remove(k) + delete(g.nodes, k) +} + +func (g DirectedGraph[T]) AllNodes() Set[T] { + ret := make(Set[T], len(g.nodes)) + for _, addr := range g.nodes { + ret.Add(addr) + } + return ret +} + +// AddDependency records that the first address depends on the second. +// +// If either address is not already in the graph then it will be implicitly +// added as part of this operation. +func (g DirectedGraph[T]) AddDependency(dependent, dependency T) { + g.Add(dependent) + g.Add(dependency) + g.g.Connect(dag.BasicEdge(dependent.UniqueKey(), dependency.UniqueKey())) +} + +// DirectDependenciesOf returns only the direct dependencies of the given +// dependent address. +func (g DirectedGraph[T]) DirectDependenciesOf(addr T) Set[T] { + k := addr.UniqueKey() + ret := MakeSet[T]() + raw := g.g.DownEdges(k) + for otherKI := range raw { + ret.Add(g.nodes[otherKI.(UniqueKey)]) + } + return ret +} + +// TransitiveDependenciesOf returns both direct and indirect dependencies of the +// given dependent address. +// +// This operation is valid only for an acyclic graph, and will panic if +// the graph contains cycles. +func (g DirectedGraph[T]) TransitiveDependenciesOf(addr T) Set[T] { + k := addr.UniqueKey() + ret := MakeSet[T]() + for otherKI := range g.g.Ancestors(k) { + ret.Add(g.nodes[otherKI.(UniqueKey)]) + } + return ret +} + +// DirectDependentsOf returns only the direct dependents of the given +// dependency address. +func (g DirectedGraph[T]) DirectDependentsOf(addr T) Set[T] { + k := addr.UniqueKey() + ret := MakeSet[T]() + raw := g.g.UpEdges(k) + for otherKI := range raw { + ret.Add(g.nodes[otherKI.(UniqueKey)]) + } + return ret +} + +// TransitiveDependentsOf returns both direct and indirect dependents of the +// given dependency address. +// +// This operation is valid only for an acyclic graph, and will panic if +// the graph contains cycles. +func (g DirectedGraph[T]) TransitiveDependentsOf(addr T) Set[T] { + k := addr.UniqueKey() + ret := MakeSet[T]() + for otherKI := range g.g.Descendants(k) { + ret.Add(g.nodes[otherKI.(UniqueKey)]) + } + return ret +} + +// TopologicalOrder returns one possible topological sort of the addresses +// in the graph. +// +// There are often multiple possible sort orders that preserve the required +// dependency ordering. The exact order returned by this function is undefined +// and may vary between calls against the same graph. +func (g DirectedGraph[T]) TopologicalOrder() []T { + raw := g.g.TopologicalOrder() + if len(raw) == 0 { + return nil + } + ret := make([]T, len(raw)) + for i, k := range raw { + ret[i] = g.nodes[k.(UniqueKey)] + } + return ret +} + +// StringForComparison outputs a string representing the topology of the +// graph in a form intended for convenient equality testing by string comparison. +// +// For best results all possible dynamic types of T should implement +// fmt.Stringer. +func (g DirectedGraph[T]) StringForComparison() string { + var buf bytes.Buffer + + stringRepr := func(v any) string { + switch v := v.(type) { + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%#v", v) + } + } + + // We want the addresses in a consistent order but it doesn't really + // matter what that order is, so we'll just do it lexically by each + // type's standard string representation. + nodeKeys := make([]UniqueKey, 0, len(g.nodes)) + for k := range g.nodes { + nodeKeys = append(nodeKeys, k) + } + sort.Slice(nodeKeys, func(i, j int) bool { + iStr := stringRepr(g.nodes[nodeKeys[i]]) + jStr := stringRepr(g.nodes[nodeKeys[j]]) + return iStr < jStr + }) + + for _, k := range nodeKeys { + addr := g.nodes[k] + fmt.Fprintf(&buf, "%s\n", stringRepr(addr)) + + deps := g.DirectDependenciesOf(addr) + if len(deps) == 0 { + continue + } + + depKeys := make([]UniqueKey, 0, len(deps)) + for k := range deps { + depKeys = append(depKeys, k) + } + sort.Slice(depKeys, func(i, j int) bool { + iStr := stringRepr(g.nodes[depKeys[i]]) + jStr := stringRepr(g.nodes[depKeys[j]]) + return iStr < jStr + }) + for _, k := range depKeys { + fmt.Fprintf(&buf, " %s\n", stringRepr(g.nodes[k])) + } + } + + return buf.String() +} diff --git a/internal/addrs/graph_test.go b/internal/addrs/graph_test.go new file mode 100644 index 0000000000..34ae396710 --- /dev/null +++ b/internal/addrs/graph_test.go @@ -0,0 +1,158 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "strings" + "testing" +) + +func TestGraph(t *testing.T) { + a := LocalValue{Name: "a"} + b := LocalValue{Name: "b"} + c := LocalValue{Name: "c"} + d := LocalValue{Name: "d"} + + g := NewDirectedGraph[LocalValue]() + + g.AddDependency(d, c) + g.AddDependency(d, b) + g.AddDependency(c, b) + g.AddDependency(b, a) + + t.Run("StringForComparison", func(t *testing.T) { + gotStr := strings.TrimSpace(g.StringForComparison()) + wantStr := strings.TrimSpace(` +local.a +local.b + local.a +local.c + local.b +local.d + local.b + local.c +`) + if gotStr != wantStr { + t.Errorf("wrong string representation\ngot:\n%s\n\nwant:\n%s", gotStr, wantStr) + } + }) + + t.Run("direct dependencies of a", func(t *testing.T) { + deps := g.DirectDependenciesOf(a) + if got, want := len(deps), 0; got != want { + t.Errorf("a has %d dependencies, but should have %d", got, want) + } + }) + t.Run("direct dependencies of b", func(t *testing.T) { + deps := g.DirectDependenciesOf(b) + if got, want := len(deps), 1; got != want { + t.Errorf("b has %d dependencies, but should have %d", got, want) + } + if !deps.Has(a) { + t.Errorf("b does not depend on a, but should") + } + }) + t.Run("direct dependencies of d", func(t *testing.T) { + deps := g.DirectDependenciesOf(d) + if got, want := len(deps), 2; got != want { + t.Errorf("d has %d dependencies, but should have %d", got, want) + } + if !deps.Has(b) { + t.Errorf("d does not depend on b, but should") + } + if !deps.Has(c) { + t.Errorf("d does not depend on c, but should") + } + }) + t.Run("direct dependents of a", func(t *testing.T) { + depnts := g.DirectDependentsOf(a) + if got, want := len(depnts), 1; got != want { + t.Errorf("a has %d dependents, but should have %d", got, want) + } + if !depnts.Has(b) { + t.Errorf("b does not depend on a, but should") + } + }) + t.Run("direct dependents of b", func(t *testing.T) { + depnts := g.DirectDependentsOf(b) + if got, want := len(depnts), 2; got != want { + t.Errorf("b has %d dependents, but should have %d", got, want) + } + if !depnts.Has(c) { + t.Errorf("c does not depend on b, but should") + } + if !depnts.Has(d) { + t.Errorf("d does not depend on b, but should") + } + }) + t.Run("direct dependents of d", func(t *testing.T) { + depnts := g.DirectDependentsOf(d) + if got, want := len(depnts), 0; got != want { + t.Errorf("d has %d dependents, but should have %d", got, want) + } + }) + t.Run("transitive dependencies of a", func(t *testing.T) { + deps := g.TransitiveDependenciesOf(a) + if got, want := len(deps), 0; got != want { + t.Errorf("a has %d transitive dependencies, but should have %d", got, want) + } + }) + t.Run("transitive dependencies of b", func(t *testing.T) { + deps := g.TransitiveDependenciesOf(b) + if got, want := len(deps), 1; got != want { + t.Errorf("b has %d transitive dependencies, but should have %d", got, want) + } + if !deps.Has(a) { + t.Errorf("b does not depend on a, but should") + } + }) + t.Run("transitive dependencies of d", func(t *testing.T) { + deps := g.TransitiveDependenciesOf(d) + if got, want := len(deps), 3; got != want { + t.Errorf("d has %d transitive dependencies, but should have %d", got, want) + } + if !deps.Has(a) { + t.Errorf("d does not depend on a, but should") + } + if !deps.Has(b) { + t.Errorf("d does not depend on b, but should") + } + if !deps.Has(c) { + t.Errorf("d does not depend on c, but should") + } + }) + t.Run("transitive dependents of a", func(t *testing.T) { + depnts := g.TransitiveDependentsOf(a) + if got, want := len(depnts), 3; got != want { + t.Errorf("a has %d transitive dependents, but should have %d", got, want) + } + if !depnts.Has(b) { + t.Errorf("b does not depend on a, but should") + } + if !depnts.Has(c) { + t.Errorf("c does not depend on a, but should") + } + if !depnts.Has(d) { + t.Errorf("d does not depend on a, but should") + } + }) + t.Run("transitive dependents of b", func(t *testing.T) { + depnts := g.TransitiveDependentsOf(b) + if got, want := len(depnts), 2; got != want { + t.Errorf("b has %d transitive dependents, but should have %d", got, want) + } + if !depnts.Has(c) { + t.Errorf("c does not depend on b, but should") + } + if !depnts.Has(d) { + t.Errorf("d does not depend on b, but should") + } + }) + t.Run("transitive dependents of d", func(t *testing.T) { + depnts := g.TransitiveDependentsOf(d) + if got, want := len(depnts), 0; got != want { + t.Errorf("d has %d transitive dependents, but should have %d", got, want) + } + }) +} diff --git a/internal/addrs/input_variable.go b/internal/addrs/input_variable.go index e85743bcdf..79dc3691ca 100644 --- a/internal/addrs/input_variable.go +++ b/internal/addrs/input_variable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -29,6 +32,13 @@ func (v InputVariable) Absolute(m ModuleInstance) AbsInputVariableInstance { } } +func (v InputVariable) InModule(module Module) ConfigInputVariable { + return ConfigInputVariable{ + Module: module, + Variable: v, + } +} + // AbsInputVariableInstance is the address of an input variable within a // particular module instance. type AbsInputVariableInstance struct { @@ -36,6 +46,8 @@ type AbsInputVariableInstance struct { Variable InputVariable } +var _ Checkable = AbsInputVariableInstance{} + // InputVariable returns the absolute address of the input variable of the // given name inside the receiving module instance. func (m ModuleInstance) InputVariable(name string) AbsInputVariableInstance { @@ -54,3 +66,61 @@ func (v AbsInputVariableInstance) String() string { return fmt.Sprintf("%s.%s", v.Module.String(), v.Variable.String()) } + +func (v AbsInputVariableInstance) UniqueKey() UniqueKey { + return absInputVariableInstanceUniqueKey(v.String()) +} + +func (v AbsInputVariableInstance) checkableSigil() {} + +func (v AbsInputVariableInstance) CheckRule(typ CheckRuleType, i int) CheckRule { + return CheckRule{ + Container: v, + Type: typ, + Index: i, + } +} + +func (v AbsInputVariableInstance) ConfigCheckable() ConfigCheckable { + return ConfigInputVariable{ + Module: v.Module.Module(), + Variable: v.Variable, + } +} + +func (v AbsInputVariableInstance) CheckableKind() CheckableKind { + return CheckableInputVariable +} + +type ConfigInputVariable struct { + Module Module + Variable InputVariable +} + +var _ ConfigCheckable = ConfigInputVariable{} + +func (v ConfigInputVariable) UniqueKey() UniqueKey { + return configInputVariableUniqueKey(v.String()) +} + +func (v ConfigInputVariable) configCheckableSigil() {} + +func (v ConfigInputVariable) CheckableKind() CheckableKind { + return CheckableInputVariable +} + +func (v ConfigInputVariable) String() string { + if len(v.Module) == 0 { + return v.Variable.String() + } + + return fmt.Sprintf("%s.%s", v.Module.String(), v.Variable.String()) +} + +type configInputVariableUniqueKey string + +func (k configInputVariableUniqueKey) uniqueKeySigil() {} + +type absInputVariableInstanceUniqueKey string + +func (k absInputVariableInstanceUniqueKey) uniqueKeySigil() {} diff --git a/internal/addrs/instance_key.go b/internal/addrs/instance_key.go index 2d46bfcbe0..1340edee81 100644 --- a/internal/addrs/instance_key.go +++ b/internal/addrs/instance_key.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -50,6 +53,27 @@ func ParseInstanceKey(key cty.Value) (InstanceKey, error) { // of a configuration object that does not use "count" or "for_each" at all. var NoKey InstanceKey +// WildcardKey represents the "unknown" value of an InstanceKey. This is used +// within the deferral logic to express absolute module and resource addresses +// that are not known at the time of planning. +var WildcardKey InstanceKey = &wildcardKey{} + +// wildcardKey is a special kind of InstanceKey that represents the "unknown" +// value of an InstanceKey. This is used within the deferral logic to express +// absolute module and resource addresses that are not known at the time of +// planning. +type wildcardKey struct{} + +func (w *wildcardKey) instanceKeySigil() {} + +func (w *wildcardKey) String() string { + return "[*]" +} + +func (w *wildcardKey) Value() cty.Value { + return cty.DynamicVal +} + // IntKey is the InstanceKey representation representing integer indices, as // used when the "count" argument is specified or if for_each is used with // a sequence type. @@ -135,6 +159,11 @@ const ( NoKeyType InstanceKeyType = 0 IntKeyType InstanceKeyType = 'I' StringKeyType InstanceKeyType = 'S' + + // UnknownKeyType is a placeholder key type for situations where Terraform + // doesn't yet know which key type to use. There are no [InstanceKey] + // values of this type. + UnknownKeyType InstanceKeyType = '?' ) // toHCLQuotedString is a helper which formats the given string in a way that diff --git a/internal/addrs/instance_key_test.go b/internal/addrs/instance_key_test.go index 0d12888bf0..8267edd86a 100644 --- a/internal/addrs/instance_key_test.go +++ b/internal/addrs/instance_key_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/local_value.go b/internal/addrs/local_value.go index 601765006c..5928cc20f8 100644 --- a/internal/addrs/local_value.go +++ b/internal/addrs/local_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -52,3 +55,18 @@ func (v AbsLocalValue) String() string { } return fmt.Sprintf("%s.%s", v.Module.String(), v.LocalValue.String()) } + +func (v AbsLocalValue) UniqueKey() UniqueKey { + return absLocalValueKey{ + moduleKey: v.Module.UniqueKey(), + valueKey: v.LocalValue.UniqueKey(), + } +} + +type absLocalValueKey struct { + moduleKey UniqueKey + valueKey UniqueKey +} + +// uniqueKeySigil implements UniqueKey. +func (absLocalValueKey) uniqueKeySigil() {} diff --git a/internal/addrs/map.go b/internal/addrs/map.go index 87b1aae266..c2fffbbbff 100644 --- a/internal/addrs/map.go +++ b/internal/addrs/map.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // Map represents a mapping whose keys are address types that implement @@ -7,6 +10,12 @@ package addrs // type cannot work with the typical Go map access syntax, and so instead has // a method-based syntax. Use this type only for situations where the key // type isn't guaranteed to always be a valid key for a standard Go map. +// +// This implementation of Map is specific to our [UniqueKey] and [UniqueKeyer] +// convention here in package addrs, which predated Go supporting type +// parameters. For types outside of addrs consider using the generalized version +// in sibling package "collections". Perhaps one day we'll rework this +// addrs-specific implementation to use [collections.Map] instead. type Map[K UniqueKeyer, V any] struct { // Elems is the internal data structure of the map. // diff --git a/internal/addrs/map_test.go b/internal/addrs/map_test.go index e5a84f03d9..147b85c81b 100644 --- a/internal/addrs/map_test.go +++ b/internal/addrs/map_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/module.go b/internal/addrs/module.go index 83a5cfdd22..4d71034d19 100644 --- a/internal/addrs/module.go +++ b/internal/addrs/module.go @@ -1,7 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/tfdiags" ) // Module is an address for a module call within configuration. This is @@ -63,6 +69,14 @@ func (m Module) Equal(other Module) bool { return true } +type moduleKey string + +func (m Module) UniqueKey() UniqueKey { + return moduleKey(m.String()) +} + +func (mk moduleKey) uniqueKeySigil() {} + func (m Module) targetableSigil() { // Module is targetable } @@ -165,3 +179,101 @@ func (m Module) Ancestors() []Module { func (m Module) configMoveableSigil() { // ModuleInstance is moveable } + +// parseModulePrefix attempts to parse the given traversal as an unkeyed module +// address, suffixed by an arbitrary (but valid) address remainder, which is +// also returned. +// +// Error diagnostics are returned if parsing according to the above conditions +// fails: in particular if the traversal represents a keyed module instance +// address rather than an unkeyed module. +func parseModulePrefix(traversal hcl.Traversal) (Module, hcl.Traversal, tfdiags.Diagnostics) { + remain := traversal + var mod Module + var diags tfdiags.Diagnostics + +LOOP: + for len(remain) > 0 { + var next string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + next = tt.Name + case hcl.TraverseAttr: + next = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Module address prefix must be followed by dot and then a name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + + if next != "module" { + break + } + + kwRange := remain[0].SourceRange() + remain = remain[1:] + // If we have the prefix "module" then we should be followed by an + // module call name, as an attribute. + if len(remain) == 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: &kwRange, + }) + break + } + + var moduleName string + switch tt := remain[0].(type) { + case hcl.TraverseAttr: + moduleName = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + remain = remain[1:] + + if len(remain) > 0 { + if _, ok := remain[0].(hcl.TraverseIndex); ok { + // Then we have a module instance key, which is invalid + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module instance keys not allowed", + Detail: "Module address must be a module (e.g. \"module.foo\"), not a module instance (e.g. \"module.foo[1]\").", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + } + + mod = append(mod, moduleName) + } + + var retRemain hcl.Traversal + if len(remain) > 0 { + retRemain = make(hcl.Traversal, len(remain)) + copy(retRemain, remain) + // The first element here might be either a TraverseRoot or a + // TraverseAttr, depending on whether we had a module address on the + // front. To make life easier for callers, we'll normalize to always + // start with a TraverseRoot. + if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { + retRemain[0] = hcl.TraverseRoot{ + Name: tt.Name, + SrcRange: tt.SrcRange, + } + } + } + + return mod, retRemain, diags +} diff --git a/internal/addrs/module_call.go b/internal/addrs/module_call.go index 709b1e302c..95a8fb603d 100644 --- a/internal/addrs/module_call.go +++ b/internal/addrs/module_call.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -56,6 +59,23 @@ func (c AbsModuleCall) absMoveableSigil() { // AbsModuleCall is "moveable". } +// StaticModule returns the static module path for the receiver. +// +// In other words, it effectively discards all of the dynamic instance keys +// along the path to this call, while retaining the static module names. +// +// Given a representation of module.a["foo"].module.b, this would return +// the [Module]-based representation of module.a.module.b, discarding the +// first step's dynamic instance key "foo". +func (c AbsModuleCall) StaticModule() Module { + ret := make(Module, len(c.Module), len(c.Module)+1) + for i, step := range c.Module { + ret[i] = step.Name + } + ret = append(ret, c.Call.Name) + return ret +} + func (c AbsModuleCall) String() string { if len(c.Module) == 0 { return "module." + c.Call.Name @@ -190,3 +210,28 @@ func (co ModuleCallInstanceOutput) AbsOutputValue(caller ModuleInstance) AbsOutp moduleAddr := co.Call.ModuleInstance(caller) return moduleAddr.OutputValue(co.Name) } + +type AbsModuleCallOutput struct { + Call AbsModuleCall + Name string +} + +func (c AbsModuleCall) Output(name string) AbsModuleCallOutput { + return AbsModuleCallOutput{ + Call: c, + Name: name, + } +} + +func (m AbsModuleCallOutput) ConfigOutputValue() ConfigOutputValue { + return ConfigOutputValue{ + Module: m.Call.StaticModule(), + OutputValue: OutputValue{ + Name: m.Name, + }, + } +} + +func (co AbsModuleCallOutput) String() string { + return fmt.Sprintf("%s.%s", co.Call.String(), co.Name) +} diff --git a/internal/addrs/module_call_test.go b/internal/addrs/module_call_test.go new file mode 100644 index 0000000000..69c287e62e --- /dev/null +++ b/internal/addrs/module_call_test.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" +) + +func TestAbsModuleCallOutput(t *testing.T) { + testCases := map[string]struct { + input AbsModuleCall + expected string + }{ + "simple": { + input: AbsModuleCall{ + Module: ModuleInstance{}, + Call: ModuleCall{ + Name: "hello", + }, + }, + expected: "module.hello.foo", + }, + "nested": { + input: AbsModuleCall{ + Module: ModuleInstance{ + ModuleInstanceStep{ + Name: "child", + InstanceKey: NoKey, + }, + }, + Call: ModuleCall{ + Name: "hello", + }, + }, + expected: "module.child.module.hello.foo", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + output := tc.input.Output("foo") + if output.String() != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, output.String()) + } + }) + } +} + +func TestAbsModuleCallOutput_ConfigOutputValue(t *testing.T) { + testCases := map[string]struct { + input AbsModuleCall + expected string + }{ + "simple": { + input: AbsModuleCall{ + Module: ModuleInstance{}, + Call: ModuleCall{ + Name: "hello", + }, + }, + expected: "module.hello.output.foo", + }, + "nested": { + input: AbsModuleCall{ + Module: ModuleInstance{ + ModuleInstanceStep{ + Name: "child", + InstanceKey: NoKey, + }, + }, + Call: ModuleCall{ + Name: "hello", + }, + }, + expected: "module.child.module.hello.output.foo", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + output := tc.input.Output("foo").ConfigOutputValue() + if output.String() != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, output.String()) + } + }) + } +} diff --git a/internal/addrs/module_instance.go b/internal/addrs/module_instance.go index f197dc144f..5ebce0ab95 100644 --- a/internal/addrs/module_instance.go +++ b/internal/addrs/module_instance.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -27,7 +30,7 @@ var ( ) func ParseModuleInstance(traversal hcl.Traversal) (ModuleInstance, tfdiags.Diagnostics) { - mi, remain, diags := parseModuleInstancePrefix(traversal) + mi, remain, diags := parseModuleInstancePrefix(traversal, false) if len(remain) != 0 { if len(remain) == len(traversal) { diags = diags.Append(&hcl.Diagnostic{ @@ -77,7 +80,7 @@ func ParseModuleInstanceStr(str string) (ModuleInstance, tfdiags.Diagnostics) { return addr, diags } -func parseModuleInstancePrefix(traversal hcl.Traversal) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) { +func parseModuleInstancePrefix(traversal hcl.Traversal, allowPartial bool) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) { remain := traversal var mi ModuleInstance var diags tfdiags.Diagnostics @@ -138,7 +141,8 @@ LOOP: } if len(remain) > 0 { - if idx, ok := remain[0].(hcl.TraverseIndex); ok { + switch idx := remain[0].(type) { + case hcl.TraverseIndex: remain = remain[1:] switch idx.Key.Type() { @@ -166,6 +170,12 @@ LOOP: Subject: idx.SourceRange().Ptr(), }) } + + case hcl.TraverseSplat: + if allowPartial { + remain = remain[1:] + step.InstanceKey = WildcardKey + } } } @@ -388,6 +398,17 @@ func (m ModuleInstance) Call() (ModuleInstance, ModuleCall) { } } +// AbsCall returns the same information as [ModuleInstance.Call], but returns +// it as a single [AbsModuleCall] value rather than the containing module +// and the local call address separately. +func (m ModuleInstance) AbsCall() AbsModuleCall { + container, call := m.Call() + return AbsModuleCall{ + Module: container, + Call: call, + } +} + // CallInstance returns the module call instance address that corresponds to // the given module instance, along with the address of the module instance // that contains it. @@ -501,6 +522,18 @@ func (m ModuleInstance) Module() Module { return ret } +// ContainingModule returns the address of the module instance as if the last +// step wasn't instanced. For example, it turns module.child[0] into +// module.child and module[0].child[0] into module[0].child. +func (m ModuleInstance) ContainingModule() ModuleInstance { + if len(m) == 0 { + return nil + } + + ret := m.Parent() + return ret.Child(m[len(m)-1].Name, NoKey) +} + func (m ModuleInstance) AddrType() TargetableAddrType { return ModuleInstanceAddrType } diff --git a/internal/addrs/module_instance_test.go b/internal/addrs/module_instance_test.go index 393bcd57e5..82e5c7a65a 100644 --- a/internal/addrs/module_instance_test.go +++ b/internal/addrs/module_instance_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -81,14 +84,14 @@ func TestModuleInstanceEqual_false(t *testing.T) { func BenchmarkStringShort(b *testing.B) { addr, _ := ParseModuleInstanceStr(`module.foo`) for n := 0; n < b.N; n++ { - addr.String() + _ = addr.String() } } func BenchmarkStringLong(b *testing.B) { addr, _ := ParseModuleInstanceStr(`module.southamerica-brazil-region.module.user-regional-desktops.module.user-name`) for n := 0; n < b.N; n++ { - addr.String() + _ = addr.String() } } @@ -161,6 +164,47 @@ func TestModuleInstance_IsDeclaredByCall(t *testing.T) { } } +func TestModuleInstance_ContainingModule(t *testing.T) { + tcs := map[string]struct { + module string + expected string + }{ + "no_instances": { + module: "module.parent.module.child", + expected: "module.parent.module.child", + }, + "last_instance": { + module: "module.parent.module.child[0]", + expected: "module.parent.module.child", + }, + "middle_instance": { + module: "module.parent[0].module.child", + expected: "module.parent[0].module.child", + }, + "all_instances": { + module: "module.parent[0].module.child[0]", + expected: "module.parent[0].module.child", + }, + "single_no_instance": { + module: "module.parent", + expected: "module.parent", + }, + "single_instance": { + module: "module.parent[0]", + expected: "module.parent", + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + module := mustParseModuleInstanceStr(tc.module) + actual, expected := module.ContainingModule().String(), tc.expected + if actual != expected { + t.Errorf("expected: %s\nactual: %s", expected, actual) + } + }) + } +} + func mustParseModuleInstanceStr(str string) ModuleInstance { mi, diags := ParseModuleInstanceStr(str) if diags.HasErrors() { diff --git a/internal/addrs/module_package.go b/internal/addrs/module_package.go index e1c82e36ed..0fd2272f76 100644 --- a/internal/addrs/module_package.go +++ b/internal/addrs/module_package.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/module_source.go b/internal/addrs/module_source.go index 905b77e2f5..e2ab072097 100644 --- a/internal/addrs/module_source.go +++ b/internal/addrs/module_source.go @@ -1,17 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( - "fmt" "path" "strings" tfaddr "github.com/hashicorp/terraform-registry-address" - "github.com/hashicorp/terraform/internal/getmodules" ) // ModuleSource is the general type for all three of the possible module source -// address types. The concrete implementations of this are ModuleSourceLocal, -// ModuleSourceRegistry, and ModuleSourceRemote. +// address types. The concrete implementations of this are [ModuleSourceLocal], +// [ModuleSourceRegistry], and [ModuleSourceRemote]. +// +// The parser for this address type lives in package moduleaddrs, because remote +// module source address parsing depends on go-getter and that's too heavy a +// dependency to impose on everything that imports this package addrs. type ModuleSource interface { // String returns a full representation of the address, including any // additional components that are typically implied by omission in @@ -38,76 +43,6 @@ var _ ModuleSource = ModuleSourceLocal("") var _ ModuleSource = ModuleSourceRegistry{} var _ ModuleSource = ModuleSourceRemote{} -var moduleSourceLocalPrefixes = []string{ - "./", - "../", - ".\\", - "..\\", -} - -// ParseModuleSource parses a module source address as given in the "source" -// argument inside a "module" block in the configuration. -// -// For historical reasons this syntax is a bit overloaded, supporting three -// different address types: -// - Local paths starting with either ./ or ../, which are special because -// Terraform considers them to belong to the same "package" as the caller. -// - Module registry addresses, given as either NAMESPACE/NAME/SYSTEM or -// HOST/NAMESPACE/NAME/SYSTEM, in which case the remote registry serves -// as an indirection over the third address type that follows. -// - Various URL-like and other heuristically-recognized strings which -// we currently delegate to the external library go-getter. -// -// There is some ambiguity between the module registry addresses and go-getter's -// very liberal heuristics and so this particular function will typically treat -// an invalid registry address as some other sort of remote source address -// rather than returning an error. If you know that you're expecting a -// registry address in particular, use ParseModuleSourceRegistry instead, which -// can therefore expose more detailed error messages about registry address -// parsing in particular. -func ParseModuleSource(raw string) (ModuleSource, error) { - if isModuleSourceLocal(raw) { - localAddr, err := parseModuleSourceLocal(raw) - if err != nil { - // This is to make sure we really return a nil ModuleSource in - // this case, rather than an interface containing the zero - // value of ModuleSourceLocal. - return nil, err - } - return localAddr, nil - } - - // For historical reasons, whether an address is a registry - // address is defined only by whether it can be successfully - // parsed as one, and anything else must fall through to be - // parsed as a direct remote source, where go-getter might - // then recognize it as a filesystem path. This is odd - // but matches behavior we've had since Terraform v0.10 which - // existing modules may be relying on. - // (Notice that this means that there's never any path where - // the registry source parse error gets returned to the caller, - // which is annoying but has been true for many releases - // without it posing a serious problem in practice.) - if ret, err := ParseModuleSourceRegistry(raw); err == nil { - return ret, nil - } - - // If we get down here then we treat everything else as a - // remote address. In practice there's very little that - // go-getter doesn't consider invalid input, so even invalid - // nonsense will probably interpreted as _something_ here - // and then fail during installation instead. We can't - // really improve this situation for historical reasons. - remoteAddr, err := parseModuleSourceRemote(raw) - if err != nil { - // This is to make sure we really return a nil ModuleSource in - // this case, rather than an interface containing the zero - // value of ModuleSourceRemote. - return nil, err - } - return remoteAddr, nil -} - // ModuleSourceLocal is a ModuleSource representing a local path reference // from the caller's directory to the callee's directory within the same // module package. @@ -129,53 +64,13 @@ func ParseModuleSource(raw string) (ModuleSource, error) { // ModuleSourceLocal values directly, except in tests where we can ensure // the value meets our assumptions. Use ParseModuleSource instead if the // input string is not hard-coded in the program. +// +// The parser for this address type lives in package moduleaddrs. It doesn't +// really need to because it doesn't have any special dependencies, but the +// remote source address parser needs to live over there and so it's clearer +// to just have all of the parsers live together in that other package. type ModuleSourceLocal string -func parseModuleSourceLocal(raw string) (ModuleSourceLocal, error) { - // As long as we have a suitable prefix (detected by ParseModuleSource) - // there is no failure case for local paths: we just use the "path" - // package's cleaning logic to remove any redundant "./" and "../" - // sequences and any duplicate slashes and accept whatever that - // produces. - - // Although using backslashes (Windows-style) is non-idiomatic, we do - // allow it and just normalize it away, so the rest of Terraform will - // only see the forward-slash form. - if strings.Contains(raw, `\`) { - // Note: We use string replacement rather than filepath.ToSlash - // here because the filepath package behavior varies by current - // platform, but we want to interpret configured paths the same - // across all platforms: these are virtual paths within a module - // package, not physical filesystem paths. - raw = strings.ReplaceAll(raw, `\`, "/") - } - - // Note that we could've historically blocked using "//" in a path here - // in order to avoid confusion with the subdir syntax in remote addresses, - // but we historically just treated that as the same as a single slash - // and so we continue to do that now for compatibility. Clean strips those - // out and reduces them to just a single slash. - clean := path.Clean(raw) - - // However, we do need to keep a single "./" on the front if it isn't - // a "../" path, or else it would be ambigous with the registry address - // syntax. - if !strings.HasPrefix(clean, "../") { - clean = "./" + clean - } - - return ModuleSourceLocal(clean), nil -} - -func isModuleSourceLocal(raw string) bool { - for _, prefix := range moduleSourceLocalPrefixes { - if strings.HasPrefix(raw, prefix) { - return true - } - } - return false -} - func (s ModuleSourceLocal) moduleSource() {} func (s ModuleSourceLocal) String() string { @@ -196,37 +91,17 @@ func (s ModuleSourceLocal) ForDisplay() string { // combination of a ModuleSourceRegistry and a module version number into // a concrete ModuleSourceRemote that Terraform will then download and // install. +// +// The parser for this address type lives in package moduleaddrs. It doesn't +// really need to because it doesn't have any special dependencies, but the +// remote source address parser needs to live over there and so it's clearer +// to just have all of the parsers live together in that other package. type ModuleSourceRegistry tfaddr.Module // DefaultModuleRegistryHost is the hostname used for registry-based module // source addresses that do not have an explicit hostname. const DefaultModuleRegistryHost = tfaddr.DefaultModuleRegistryHost -// ParseModuleSourceRegistry is a variant of ParseModuleSource which only -// accepts module registry addresses, and will reject any other address type. -// -// Use this instead of ParseModuleSource if you know from some other surrounding -// context that an address is intended to be a registry address rather than -// some other address type, which will then allow for better error reporting -// due to the additional information about user intent. -func ParseModuleSourceRegistry(raw string) (ModuleSource, error) { - // Before we delegate to the "real" function we'll just make sure this - // doesn't look like a local source address, so we can return a better - // error message for that situation. - if isModuleSourceLocal(raw) { - return ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw) - } - - src, err := tfaddr.ParseModuleSource(raw) - if err != nil { - return nil, err - } - return ModuleSourceRegistry{ - Package: src.Package, - Subdir: src.Subdir, - }, nil -} - func (s ModuleSourceRegistry) moduleSource() {} func (s ModuleSourceRegistry) String() string { @@ -249,6 +124,10 @@ func (s ModuleSourceRegistry) ForDisplay() string { // A ModuleSourceRemote can optionally include a "subdirectory" path, which // means that it's selecting a sub-directory of the given package to use as // the entry point into the package. +// +// The parser for this address type lives in package moduleaddrs, because remote +// module source address parsing depends on go-getter and that's too heavy a +// dependency to impose on everything that imports this package addrs. type ModuleSourceRemote struct { // Package is the address of the remote package that the requested // module belongs to. @@ -263,63 +142,20 @@ type ModuleSourceRemote struct { Subdir string } -func parseModuleSourceRemote(raw string) (ModuleSourceRemote, error) { - var subDir string - raw, subDir = getmodules.SplitPackageSubdir(raw) - if strings.HasPrefix(subDir, "../") { - return ModuleSourceRemote{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir) - } - - // A remote source address is really just a go-getter address resulting - // from go-getter's "detect" phase, which adds on the prefix specifying - // which protocol it should use and possibly also adjusts the - // protocol-specific part into different syntax. - // - // Note that for historical reasons this can potentially do network - // requests in order to disambiguate certain address types, although - // that's a legacy thing that is only for some specific, less-commonly-used - // address types. Most just do local string manipulation. We should - // aim to remove the network requests over time, if possible. - norm, moreSubDir, err := getmodules.NormalizePackageAddress(raw) - if err != nil { - // We must pass through the returned error directly here because - // the getmodules package has some special error types it uses - // for certain cases where the UI layer might want to include a - // more helpful error message. - return ModuleSourceRemote{}, err - } - - if moreSubDir != "" { - switch { - case subDir != "": - // The detector's own subdir goes first, because the - // subdir we were given is conceptually relative to - // the subdirectory that we just detected. - subDir = path.Join(moreSubDir, subDir) - default: - subDir = path.Clean(moreSubDir) - } - if strings.HasPrefix(subDir, "../") { - // This would suggest a bug in a go-getter detector, but - // we'll catch it anyway to avoid doing something confusing - // downstream. - return ModuleSourceRemote{}, fmt.Errorf("detected subdirectory path %q of %q leads outside of the module package", subDir, norm) - } - } - - return ModuleSourceRemote{ - Package: ModulePackage(norm), - Subdir: subDir, - }, nil -} - func (s ModuleSourceRemote) moduleSource() {} func (s ModuleSourceRemote) String() string { + base := s.Package.String() + if s.Subdir != "" { - return s.Package.String() + "//" + s.Subdir + // Address contains query string + if strings.Contains(base, "?") { + parts := strings.SplitN(base, "?", 2) + return parts[0] + "//" + s.Subdir + "?" + parts[1] + } + return base + "//" + s.Subdir } - return s.Package.String() + return base } func (s ModuleSourceRemote) ForDisplay() string { diff --git a/internal/addrs/module_test.go b/internal/addrs/module_test.go index 67cfc6ba2d..ed7a90decc 100644 --- a/internal/addrs/module_test.go +++ b/internal/addrs/module_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -84,13 +87,13 @@ func TestModuleString(t *testing.T) { func BenchmarkModuleStringShort(b *testing.B) { module := Module{"a", "b"} for n := 0; n < b.N; n++ { - module.String() + _ = module.String() } } func BenchmarkModuleStringLong(b *testing.B) { module := Module{"southamerica-brazil-region", "user-regional-desktop", "user-name"} for n := 0; n < b.N; n++ { - module.String() + _ = module.String() } } diff --git a/internal/addrs/move_endpoint.go b/internal/addrs/move_endpoint.go index 765b66f5ee..3bfa86e571 100644 --- a/internal/addrs/move_endpoint.go +++ b/internal/addrs/move_endpoint.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -81,7 +85,7 @@ func (e *MoveEndpoint) MightUnifyWith(other *MoveEndpoint) bool { return from != nil && to != nil } -// ConfigMovable transforms the reciever into a ConfigMovable by resolving it +// ConfigMoveable transforms the reciever into a ConfigMoveable by resolving it // relative to the given base module, which should be the module where // the MoveEndpoint expression was found. // @@ -123,7 +127,7 @@ func (e *MoveEndpoint) ConfigMoveable(baseModule Module) ConfigMoveable { // it with the address of the module where it was declared in order to get // an absolute address relative to the root module. func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal) + path, remain, diags := parseModuleInstancePrefix(traversal, false) if diags.HasErrors() { return nil, diags } @@ -137,7 +141,7 @@ func ParseMoveEndpoint(traversal hcl.Traversal) (*MoveEndpoint, tfdiags.Diagnost }, diags } - riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain) + riAddr, moreDiags := parseResourceInstanceUnderModule(path, false, remain) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags diff --git a/internal/addrs/move_endpoint_kind.go b/internal/addrs/move_endpoint_kind.go index cd8adab8f7..cf1c0dd965 100644 --- a/internal/addrs/move_endpoint_kind.go +++ b/internal/addrs/move_endpoint_kind.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import "fmt" @@ -6,7 +9,7 @@ import "fmt" // address can refer to. type MoveEndpointKind rune -//go:generate go run golang.org/x/tools/cmd/stringer -type MoveEndpointKind +//go:generate go tool golang.org/x/tools/cmd/stringer -type MoveEndpointKind const ( // MoveEndpointModule indicates that a move endpoint either refers to diff --git a/internal/addrs/move_endpoint_module.go b/internal/addrs/move_endpoint_module.go index f2c1408d66..2e1cdcf5bc 100644 --- a/internal/addrs/move_endpoint_module.go +++ b/internal/addrs/move_endpoint_module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -431,7 +434,7 @@ func (e *MoveEndpointInModule) NestedWithin(other *MoveEndpointInModule) bool { // matchModuleInstancePrefix is an internal helper to decide whether the given // module instance address refers to either the module where the move endpoint -// was declared or some descendent of that module. +// was declared or some descendant of that module. // // If so, it will split the given address into two parts: the "prefix" part // which corresponds with the module where the statement was declared, and diff --git a/internal/addrs/move_endpoint_module_test.go b/internal/addrs/move_endpoint_module_test.go index c1643d44c2..ce85f0f3e6 100644 --- a/internal/addrs/move_endpoint_module_test.go +++ b/internal/addrs/move_endpoint_module_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -7,6 +10,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -1115,7 +1119,7 @@ func TestMoveEndpointChainAndNested(t *testing.T) { }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].module.bar.resource.baz").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].module.bar.resource_type.baz").ContainingResource(), Other: AbsModuleCall{ Module: mustParseModuleInstanceStr("module.foo[2]"), Call: ModuleCall{Name: "bar"}, @@ -1125,7 +1129,7 @@ func TestMoveEndpointChainAndNested(t *testing.T) { }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].module.bar[3].resource.baz[2]"), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].module.bar[3].resource_type.baz[2]"), Other: AbsModuleCall{ Module: mustParseModuleInstanceStr("module.foo[2]"), Call: ModuleCall{Name: "bar"}, @@ -1152,14 +1156,14 @@ func TestMoveEndpointChainAndNested(t *testing.T) { }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), Other: mustParseModuleInstanceStr("module.foo[2]"), CanChainFrom: false, NestedWithin: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].module.bar.resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].module.bar.resource_type.baz"), Other: mustParseModuleInstanceStr("module.foo[2]"), CanChainFrom: false, NestedWithin: true, @@ -1170,28 +1174,28 @@ func TestMoveEndpointChainAndNested(t *testing.T) { Module: mustParseModuleInstanceStr("module.foo[2]"), Call: ModuleCall{Name: "bar"}, }, - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), CanChainFrom: false, NestedWithin: false, }, { Endpoint: mustParseModuleInstanceStr("module.foo[2]"), - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), CanChainFrom: false, NestedWithin: false, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), CanChainFrom: true, NestedWithin: false, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz[2]").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz[2]").ContainingResource(), CanChainFrom: false, NestedWithin: true, }, @@ -1201,53 +1205,53 @@ func TestMoveEndpointChainAndNested(t *testing.T) { Module: mustParseModuleInstanceStr("module.foo[2]"), Call: ModuleCall{Name: "bar"}, }, - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), CanChainFrom: false, }, { Endpoint: mustParseModuleInstanceStr("module.foo[2]"), - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), CanChainFrom: false, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), CanChainFrom: false, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), CanChainFrom: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("resource_type.baz"), EndpointMod: Module{"foo"}, - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), CanChainFrom: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), - Other: mustParseAbsResourceInstanceStr("resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), + Other: mustParseAbsResourceInstanceStr("resource_type.baz"), OtherMod: Module{"foo"}, CanChainFrom: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("resource_type.baz"), EndpointMod: Module{"foo"}, - Other: mustParseAbsResourceInstanceStr("resource.baz"), + Other: mustParseAbsResourceInstanceStr("resource_type.baz"), OtherMod: Module{"foo"}, CanChainFrom: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("resource.baz").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("resource_type.baz").ContainingResource(), EndpointMod: Module{"foo"}, - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), CanChainFrom: true, }, @@ -1272,29 +1276,29 @@ func TestMoveEndpointChainAndNested(t *testing.T) { }, { - Endpoint: mustParseAbsResourceInstanceStr("resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("resource_type.baz"), EndpointMod: Module{"foo"}, - Other: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz").ContainingResource(), NestedWithin: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource.baz"), - Other: mustParseAbsResourceInstanceStr("resource.baz").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("module.foo[2].resource_type.baz"), + Other: mustParseAbsResourceInstanceStr("resource_type.baz").ContainingResource(), OtherMod: Module{"foo"}, NestedWithin: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("resource.baz"), + Endpoint: mustParseAbsResourceInstanceStr("resource_type.baz"), EndpointMod: Module{"foo"}, - Other: mustParseAbsResourceInstanceStr("resource.baz").ContainingResource(), + Other: mustParseAbsResourceInstanceStr("resource_type.baz").ContainingResource(), OtherMod: Module{"foo"}, NestedWithin: true, }, { - Endpoint: mustParseAbsResourceInstanceStr("ressurce.baz").ContainingResource(), + Endpoint: mustParseAbsResourceInstanceStr("resource_type.baz").ContainingResource(), EndpointMod: Module{"foo"}, Other: mustParseModuleInstanceStr("module.foo[2]"), NestedWithin: true, @@ -1403,7 +1407,7 @@ func TestSelectsModule(t *testing.T) { { Endpoint: &MoveEndpointInModule{ module: mustParseModuleInstanceStr("module.foo").Module(), - relSubject: mustParseAbsResourceInstanceStr(`module.bar.resource.name["key"]`), + relSubject: mustParseAbsResourceInstanceStr(`module.bar.resource_type.name["key"]`), }, Addr: mustParseModuleInstanceStr(`module.foo[1].module.bar`), Selects: true, @@ -1417,7 +1421,7 @@ func TestSelectsModule(t *testing.T) { }, { Endpoint: &MoveEndpointInModule{ - relSubject: mustParseAbsResourceInstanceStr(`module.bar.module.baz["key"].resource.name`).ContainingResource(), + relSubject: mustParseAbsResourceInstanceStr(`module.bar.module.baz["key"].resource_type.name`).ContainingResource(), }, Addr: mustParseModuleInstanceStr(`module.bar.module.baz["key"]`), Selects: true, @@ -1425,7 +1429,7 @@ func TestSelectsModule(t *testing.T) { { Endpoint: &MoveEndpointInModule{ module: mustParseModuleInstanceStr("module.nope").Module(), - relSubject: mustParseAbsResourceInstanceStr(`module.bar.resource.name["key"]`), + relSubject: mustParseAbsResourceInstanceStr(`module.bar.resource_type.name["key"]`), }, Addr: mustParseModuleInstanceStr(`module.foo[1].module.bar`), Selects: false, @@ -1439,7 +1443,7 @@ func TestSelectsModule(t *testing.T) { }, { Endpoint: &MoveEndpointInModule{ - relSubject: mustParseAbsResourceInstanceStr(`module.nope.module.baz["key"].resource.name`).ContainingResource(), + relSubject: mustParseAbsResourceInstanceStr(`module.nope.module.baz["key"].resource_type.name`).ContainingResource(), }, Addr: mustParseModuleInstanceStr(`module.bar.module.baz["key"]`), Selects: false, diff --git a/internal/addrs/move_endpoint_test.go b/internal/addrs/move_endpoint_test.go index 602c7e9719..f5c9df41b5 100644 --- a/internal/addrs/move_endpoint_test.go +++ b/internal/addrs/move_endpoint_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/moveable.go b/internal/addrs/moveable.go index b9d2f87f07..4c309dd778 100644 --- a/internal/addrs/moveable.go +++ b/internal/addrs/moveable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // AbsMoveable is an interface implemented by address types that can be either @@ -8,7 +11,7 @@ package addrs // Note that AbsMoveable represents an absolute address relative to the root // of the configuration, which is different than the direct representation // of these in configuration where the author gives an address relative to -// the current module where the address is defined. The type MoveEndpoint +// the current module where the address is defined. type AbsMoveable interface { absMoveableSigil() UniqueKeyer @@ -41,16 +44,17 @@ var ( // the configuration, rather than an instance of that object created by // module expansion. // -// Note that ConfigMovable represents an absolute address relative to the root +// Note that ConfigMoveable represents an absolute address relative to the root // of the configuration, which is different than the direct representation // of these in configuration where the author gives an address relative to // the current module where the address is defined. The type MoveEndpoint // represents the relative form given directly in configuration. type ConfigMoveable interface { configMoveableSigil() + UniqueKeyer } -// The following are all of the possible ConfigMovable address types: +// The following are all of the possible ConfigMoveable address types: var ( _ ConfigMoveable = ConfigResource{} _ ConfigMoveable = Module(nil) diff --git a/internal/addrs/output_value.go b/internal/addrs/output_value.go index ff76556b2a..0c23b0cdb9 100644 --- a/internal/addrs/output_value.go +++ b/internal/addrs/output_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -5,6 +8,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -12,10 +16,11 @@ import ( // that is defining it. // // This is related to but separate from ModuleCallOutput, which represents -// a module output from the perspective of its parent module. Since output -// values cannot be represented from the module where they are defined, -// OutputValue is not Referenceable, while ModuleCallOutput is. +// a module output from the perspective of its parent module. Outputs are +// referencable from the testing scope, in general terraform operation users +// will be referencing ModuleCallOutput. type OutputValue struct { + referenceable Name string } @@ -23,6 +28,16 @@ func (v OutputValue) String() string { return "output." + v.Name } +func (v OutputValue) Equal(o OutputValue) bool { + return v.Name == o.Name +} + +func (v OutputValue) UniqueKey() UniqueKey { + return v // An OutputValue is its own UniqueKey +} + +func (v OutputValue) uniqueKeySigil() {} + // Absolute converts the receiver into an absolute address within the given // module instance. func (v OutputValue) Absolute(m ModuleInstance) AbsOutputValue { @@ -62,8 +77,8 @@ func (m ModuleInstance) OutputValue(name string) AbsOutputValue { } } -func (v AbsOutputValue) Check(t CheckType, i int) Check { - return Check{ +func (v AbsOutputValue) CheckRule(t CheckRuleType, i int) CheckRule { + return CheckRule{ Container: v, Type: t, Index: i, @@ -78,7 +93,7 @@ func (v AbsOutputValue) String() string { } func (v AbsOutputValue) Equal(o AbsOutputValue) bool { - return v.OutputValue == o.OutputValue && v.Module.Equal(o.Module) + return v.OutputValue.Equal(o.OutputValue) && v.Module.Equal(o.Module) } func (v AbsOutputValue) ConfigOutputValue() ConfigOutputValue { @@ -111,7 +126,7 @@ type absOutputValueUniqueKey string func (k absOutputValueUniqueKey) uniqueKeySigil() {} func ParseAbsOutputValue(traversal hcl.Traversal) (AbsOutputValue, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal) + path, remain, diags := parseModuleInstancePrefix(traversal, false) if diags.HasErrors() { return AbsOutputValue{}, diags } diff --git a/internal/addrs/output_value_test.go b/internal/addrs/output_value_test.go index 8e7bbeae34..bfeb673e9b 100644 --- a/internal/addrs/output_value_test.go +++ b/internal/addrs/output_value_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -112,7 +115,7 @@ func TestParseAbsOutputValueStr(t *testing.T) { t.Run(input, func(t *testing.T) { got, diags := ParseAbsOutputValueStr(input) for _, problem := range deep.Equal(got, tc.want) { - t.Errorf(problem) + t.Error(problem) } if len(diags) > 0 { gotErr := diags.Err().Error() diff --git a/internal/addrs/parse_ref.go b/internal/addrs/parse_ref.go index bd5bcc7c51..9e4de92c15 100644 --- a/internal/addrs/parse_ref.go +++ b/internal/addrs/parse_ref.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -6,8 +9,9 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" ) // Reference describes a reference to an address with source location @@ -19,7 +23,7 @@ type Reference struct { } // DisplayString returns a string that approximates the subject and remaining -// traversal of the reciever in a way that resembles the Terraform language +// traversal of the receiver in a way that resembles the Terraform language // syntax that could've produced it. // // It's not guaranteed to actually be a valid Terraform language expression, @@ -55,7 +59,7 @@ func (r *Reference) DisplayString() string { return ret.String() } -// ParseRef attempts to extract a referencable address from the prefix of the +// ParseRef attempts to extract a referenceable address from the prefix of the // given traversal, which must be an absolute traversal or this function // will panic. // @@ -79,6 +83,67 @@ func ParseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { return ref, diags } +// ParseRefFromTestingScope adds check blocks and outputs into the available +// references returned by ParseRef. +// +// The testing files and functionality have a slightly expanded referencing +// scope and so should use this function to retrieve references. +func ParseRefFromTestingScope(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { + root := traversal.RootName() + rootRange := traversal[0].SourceRange() + + var diags tfdiags.Diagnostics + var reference *Reference + + switch root { + case "output": + name, rng, remain, outputDiags := parseSingleAttrRef(traversal) + reference = &Reference{ + Subject: OutputValue{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + } + diags = outputDiags + case "check": + name, rng, remain, checkDiags := parseSingleAttrRef(traversal) + reference = &Reference{ + Subject: Check{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + } + diags = checkDiags + case "run": + name, rng, remain, runDiags := parseSingleAttrRef(traversal) + reference = &Reference{ + Subject: Run{Name: name}, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + } + diags = runDiags + case "plan", "state": + // These names are all pre-emptively reserved in the hope of landing + // some version of referencing the plan and state files in test + // assertions. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved symbol name", + Detail: fmt.Sprintf("The symbol name %q is reserved for use in a future Terraform version. If you are using a provider that already uses this as a resource type name, add the prefix \"resource.\" to force interpretation as a resource type name.", root), + Subject: rootRange.Ptr(), + }) + return nil, diags + } + + if reference != nil { + if len(reference.Remaining) == 0 { + reference.Remaining = nil + } + return reference, diags + } + + // If it's not an output or a check block, then just parse it as normal. + return ParseRef(traversal) +} + // ParseRefStr is a helper wrapper around ParseRef that takes a string // and parses it with the HCL native syntax traversal parser before // interpreting it. @@ -108,6 +173,22 @@ func ParseRefStr(str string) (*Reference, tfdiags.Diagnostics) { return ref, diags } +// ParseRefStrFromTestingScope matches ParseRefStr except it supports the +// references supported by ParseRefFromTestingScope. +func ParseRefStrFromTestingScope(str string) (*Reference, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) + diags = diags.Append(parseDiags) + if parseDiags.HasErrors() { + return nil, diags + } + + ref, targetDiags := ParseRefFromTestingScope(traversal) + diags = diags.Append(targetDiags) + return ref, diags +} + func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -166,6 +247,19 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { remain := traversal[1:] // trim off "resource" so we can use our shared resource reference parser return parseResourceRef(ManagedResourceMode, rootRange, remain) + case "ephemeral": + if len(traversal) < 3 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and the resource name.`, + Subject: traversal.SourceRange().Ptr(), + }) + return nil, diags + } + remain := traversal[1:] // trim off "ephemeral" so we can use our shared resource reference parser + return parseResourceRef(EphemeralResourceMode, rootRange, remain) + case "local": name, rng, remain, diags := parseSingleAttrRef(traversal) return &Reference{ @@ -290,6 +384,18 @@ func parseRef(traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { }) return nil, diags + case "action": + if len(traversal) < 3 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "action" object must be followed by two attribute names: the action type and the action name.`, + Subject: traversal.SourceRange().Ptr(), + }) + return nil, diags + } + remain := traversal[1:] // trim off "action" + return parseActionRef(rootRange, remain) default: return parseResourceRef(ManagedResourceMode, rootRange, traversal) } @@ -315,13 +421,40 @@ func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Tra case hcl.TraverseAttr: typeName = tt.Name default: - // If it isn't a TraverseRoot then it must be a "data" reference. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid reference", - Detail: `The "data" object does not support this operation.`, - Subject: traversal[0].SourceRange().Ptr(), - }) + switch mode { + case ManagedResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "resource" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + case DataResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "data" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + case EphemeralResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The "ephemeral" object does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + default: + // Shouldn't get here because the above should be exhaustive for + // all of the resource modes. But we'll still return a + // minimally-passable error message so that the won't totally + // misbehave if we forget to update this in future. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The left operand does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + } return nil, diags } @@ -330,14 +463,16 @@ func parseResourceRef(mode ResourceMode, startRange hcl.Range, traversal hcl.Tra var what string switch mode { case DataResourceMode: - what = "data source" + what = "a data source" + case EphemeralResourceMode: + what = "an ephemeral resource type" default: - what = "resource type" + what = "a resource type" } diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid reference", - Detail: fmt.Sprintf(`A reference to a %s must be followed by at least one attribute access, specifying the resource name.`, what), + Detail: fmt.Sprintf(`A reference to %s must be followed by at least one attribute access, specifying the resource name.`, what), Subject: traversal[1].SourceRange().Ptr(), }) return nil, diags @@ -415,3 +550,90 @@ func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Travers }) return "", hcl.Range{}, nil, diags } + +// similar to parseResourceRef, but for Actions (which don't have Modes, so it's simpler!) +func parseActionRef(startRange hcl.Range, traversal hcl.Traversal) (*Reference, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if len(traversal) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `A reference to an action type must be followed by at least one attribute access, specifying the action name.`, + Subject: hcl.RangeBetween(traversal[0].SourceRange(), traversal[len(traversal)-1].SourceRange()).Ptr(), + }) + return nil, diags + } + + var typeName, name string + switch tt := traversal[0].(type) { // Could be either root or attr, depending on our resource mode + case hcl.TraverseRoot: + typeName = tt.Name + case hcl.TraverseAttr: + typeName = tt.Name + default: + // Shouldn't get here, but we'll still return a minimally-passable error + // message. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `The left operand does not support this operation.`, + Subject: traversal[0].SourceRange().Ptr(), + }) + } + + attrTrav, ok := traversal[1].(hcl.TraverseAttr) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: `A reference to an action must be followed by at least one attribute access, specifying the action name.`, + Subject: traversal[1].SourceRange().Ptr(), + }) + return nil, diags + } + name = attrTrav.Name + rng := hcl.RangeBetween(startRange, attrTrav.SrcRange) + remain := traversal[2:] + + actionAddr := Action{ + Type: typeName, + Name: name, + } + actionInstAddr := ActionInstance{ + Action: actionAddr, + Key: NoKey, + } + + if len(remain) == 0 { + // This might actually be a reference to the collection of all instances + // of the resource, but we don't have enough context here to decide + // so we'll let the caller resolve that ambiguity. + return &Reference{ + Subject: actionAddr, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + }, diags + } + + if idxTrav, ok := remain[0].(hcl.TraverseIndex); ok { + var err error + actionInstAddr.Key, err = ParseInstanceKey(idxTrav.Key) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid index key", + Detail: fmt.Sprintf("Invalid index for resource instance: %s.", err), + Subject: &idxTrav.SrcRange, + }) + return nil, diags + } + remain = remain[1:] + rng = hcl.RangeBetween(rng, idxTrav.SrcRange) + } + + return &Reference{ + Subject: actionInstAddr, + SourceRange: tfdiags.SourceRangeFromHCL(rng), + Remaining: remain, + }, diags +} diff --git a/internal/addrs/parse_ref_test.go b/internal/addrs/parse_ref_test.go index 52c9b2cd33..721dedcbd5 100644 --- a/internal/addrs/parse_ref_test.go +++ b/internal/addrs/parse_ref_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -6,17 +9,168 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" ) +func TestParseRefInTestingScope(t *testing.T) { + tests := []struct { + Input string + Want *Reference + WantErr string + }{ + { + `output.value`, + &Reference{ + Subject: OutputValue{ + Name: "value", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12}, + }, + }, + ``, + }, + { + `output`, + nil, + `The "output" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `output["foo"]`, + nil, + `The "output" object does not support this operation.`, + }, + + { + `check.health`, + &Reference{ + Subject: Check{ + Name: "health", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12}, + }, + }, + ``, + }, + { + `check`, + nil, + `The "check" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `check["foo"]`, + nil, + `The "check" object does not support this operation.`, + }, + { + `run.zero`, + &Reference{ + Subject: Run{ + Name: "zero", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8}, + }, + }, + ``, + }, + { + `run.zero.value`, + &Reference{ + Subject: Run{ + Name: "zero", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "value", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 9, Byte: 8}, + End: hcl.Pos{Line: 1, Column: 15, Byte: 14}, + }, + }, + }, + }, + ``, + }, + { + `run`, + nil, + `The "run" object cannot be accessed directly. Instead, access one of its attributes.`, + }, + { + `run["foo"]`, + nil, + `The "run" object does not support this operation.`, + }, + + // Sanity check at least one of the others works to verify it does + // fall through to the core function. + { + `count.index`, + &Reference{ + Subject: CountAttr{ + Name: "index", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 12, Byte: 11}, + }, + }, + ``, + }, + } + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1}) + if travDiags.HasErrors() { + t.Fatal(travDiags.Error()) + } + + got, diags := ParseRefFromTestingScope(traversal) + + switch len(diags) { + case 0: + if test.WantErr != "" { + t.Fatalf("succeeded; want error: %s", test.WantErr) + } + case 1: + if test.WantErr == "" { + t.Fatalf("unexpected diagnostics: %s", diags.Err()) + } + if got, want := diags[0].Description().Detail, test.WantErr; got != want { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) + } + default: + t.Fatalf("too many diagnostics: %s", diags.Err()) + } + + if diags.HasErrors() { + return + } + + for _, problem := range deep.Equal(got, test.Want) { + t.Error(problem) + } + }) + } +} + func TestParseRef(t *testing.T) { tests := []struct { Input string Want *Reference WantErr string }{ - // count { `count.index`, @@ -208,6 +362,104 @@ func TestParseRef(t *testing.T) { `The "data" object must be followed by two attribute names: the data source type and the resource name.`, }, + // ephemeral + { + `ephemeral.external.foo`, + &Reference{ + Subject: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 23, Byte: 22}, + }, + }, + ``, + }, + { + `ephemeral.external.foo.bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 23, Byte: 22}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 23, Byte: 22}, + End: hcl.Pos{Line: 1, Column: 27, Byte: 26}, + }, + }, + }, + }, + ``, + }, + { + `ephemeral.external.foo["baz"].bar`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29}, + }, + Remaining: hcl.Traversal{ + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 30, Byte: 29}, + End: hcl.Pos{Line: 1, Column: 34, Byte: 33}, + }, + }, + }, + }, + ``, + }, + { + `ephemeral.external.foo["baz"]`, + &Reference{ + Subject: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "external", + Name: "foo", + }, + Key: StringKey("baz"), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29}, + }, + }, + ``, + }, + { + `ephemeral`, + nil, + `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and the resource name.`, + }, + { + `ephemeral.external`, + nil, + `The "ephemeral" object must be followed by two attribute names: the ephemeral resource type and the resource name.`, + }, + // local { `local.foo`, @@ -716,6 +968,94 @@ func TestParseRef(t *testing.T) { nil, `A reference to a resource type must be followed by at least one attribute access, specifying the resource name.`, }, + + // Should interpret checks, outputs, and runs as resource types. + { + `output.value`, + &Reference{ + Subject: Resource{ + Mode: ManagedResourceMode, + Type: "output", + Name: "value", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12}, + }, + }, + ``, + }, + { + `check.health`, + &Reference{ + Subject: Resource{ + Mode: ManagedResourceMode, + Type: "check", + Name: "health", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 13, Byte: 12}, + }, + }, + ``, + }, + { + `run.zero`, + &Reference{ + Subject: Resource{ + Mode: ManagedResourceMode, + Type: "run", + Name: "zero", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 9, Byte: 8}, + }, + }, + ``, + }, + + // Actions + { + `action`, nil, + `The "action" object must be followed by two attribute names: the action type and the action name.`, + }, + { + `action.foo`, nil, + `The "action" object must be followed by two attribute names: the action type and the action name.`, + }, + { + `action.foo.bar`, + &Reference{ + Subject: Action{ + Type: "foo", + Name: "bar", + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 15, Byte: 14}, + }, + }, + ``, + }, + { + `action.foo.bar[1]`, + &Reference{ + Subject: ActionInstance{ + Action: Action{ + Type: "foo", + Name: "bar", + }, + Key: IntKey(1), + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 18, Byte: 17}, + }, + }, + ``, + }, } for _, test := range tests { @@ -748,7 +1088,7 @@ func TestParseRef(t *testing.T) { } for _, problem := range deep.Equal(got, test.Want) { - t.Errorf(problem) + t.Error(problem) } }) } diff --git a/internal/addrs/parse_target.go b/internal/addrs/parse_target.go index 378e4de6a5..33147b9208 100644 --- a/internal/addrs/parse_target.go +++ b/internal/addrs/parse_target.go @@ -1,11 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -25,7 +28,19 @@ type Target struct { // If error diagnostics are returned then the Target value is invalid and // must not be used. func ParseTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) { - path, remain, diags := parseModuleInstancePrefix(traversal) + return parseTarget(traversal, false) +} + +// ParsePartialTarget is like ParseTarget, but it allows the given traversal +// to support the [*] wildcard syntax for resource instances. These indicate +// a "partial" resource address that refers to all potential instances of a +// resource or module. +func ParsePartialTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) { + return parseTarget(traversal, true) +} + +func parseTarget(traversal hcl.Traversal, allowPartial bool) (*Target, tfdiags.Diagnostics) { + path, remain, diags := parseModuleInstancePrefix(traversal, allowPartial) if diags.HasErrors() { return nil, diags } @@ -39,7 +54,7 @@ func ParseTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) { }, diags } - riAddr, moreDiags := parseResourceInstanceUnderModule(path, remain) + riAddr, moreDiags := parseResourceInstanceUnderModule(path, allowPartial, remain) diags = diags.Append(moreDiags) if diags.HasErrors() { return nil, diags @@ -62,7 +77,92 @@ func ParseTarget(traversal hcl.Traversal) (*Target, tfdiags.Diagnostics) { }, diags } -func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { +// parseConfigResourceUnderModule attempts to parse the given traversal as the +// address for a ConfigResource in the context of the given module. +// +// Error diagnostics are returned if the resource address contains an instance +// key. +func parseConfigResourceUnderModule(moduleAddr Module, remain hcl.Traversal) (ConfigResource, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + mode := ManagedResourceMode + if remain.RootName() == "data" { + mode = DataResourceMode + remain = remain[1:] + } + + if len(remain) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Resource specification must include a resource type and name.", + Subject: remain.SourceRange().Ptr(), + }) + return ConfigResource{}, diags + } + + var typeName, name string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + typeName = tt.Name + case hcl.TraverseAttr: + typeName = tt.Name + default: + switch mode { + case ManagedResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A resource type name is required.", + Subject: remain[0].SourceRange().Ptr(), + }) + case DataResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A data source name is required.", + Subject: remain[0].SourceRange().Ptr(), + }) + default: + panic("unknown mode") + } + return ConfigResource{}, diags + } + + switch tt := remain[1].(type) { + case hcl.TraverseAttr: + name = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A resource name is required.", + Subject: remain[1].SourceRange().Ptr(), + }) + return ConfigResource{}, diags + } + + remain = remain[2:] + if len(remain) > 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Resource instance keys not allowed", + Detail: "Resource address must be a resource (e.g. \"test_instance.foo\"), not a resource instance (e.g. \"test_instance.foo[1]\").", + Subject: remain[0].SourceRange().Ptr(), + }) + return ConfigResource{}, diags + } + return ConfigResource{ + Module: moduleAddr, + Resource: Resource{ + Mode: mode, + Type: typeName, + Name: name, + }, + }, diags +} + +func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, allowPartial bool, remain hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { // Note that this helper is used as part of both ParseTarget and // ParseMoveEndpoint, so its error messages should be generic // enough to suit both situations. @@ -70,9 +170,26 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav var diags tfdiags.Diagnostics mode := ManagedResourceMode - if remain.RootName() == "data" { + switch remain.RootName() { + case "data": mode = DataResourceMode remain = remain[1:] + case "ephemeral": + mode = EphemeralResourceMode + remain = remain[1:] + case "resource": + // Starting a resource address with "resource" is optional, so we'll + // just ignore it. + remain = remain[1:] + case "count", "each", "local", "module", "path", "self", "terraform", "var", "template", "lazy", "arg": + // These are all reserved words that are not valid as resource types. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: fmt.Sprintf("The keyword %q is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with \"resource.\".", remain.RootName()), + Subject: remain.SourceRange().Ptr(), + }) + return AbsResourceInstance{}, diags } if len(remain) < 2 { @@ -107,6 +224,13 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav Detail: "A data source name is required.", Subject: remain[0].SourceRange().Ptr(), }) + case EphemeralResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "An ephemeral resource type name is required.", + Subject: remain[0].SourceRange().Ptr(), + }) default: panic("unknown mode") } @@ -131,7 +255,8 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav case 0: return moduleAddr.ResourceInstance(mode, typeName, name, NoKey), diags case 1: - if tt, ok := remain[0].(hcl.TraverseIndex); ok { + switch tt := remain[0].(type) { + case hcl.TraverseIndex: key, err := ParseInstanceKey(tt.Key) if err != nil { diags = diags.Append(&hcl.Diagnostic{ @@ -144,7 +269,20 @@ func parseResourceInstanceUnderModule(moduleAddr ModuleInstance, remain hcl.Trav } return moduleAddr.ResourceInstance(mode, typeName, name, key), diags - } else { + case hcl.TraverseSplat: + if allowPartial { + return moduleAddr.ResourceInstance(mode, typeName, name, WildcardKey), diags + } + + // Otherwise, return an error. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Resource instance key must be given in square brackets.", + Subject: remain[0].SourceRange().Ptr(), + }) + return AbsResourceInstance{}, diags + default: diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid address", @@ -281,11 +419,39 @@ func ParseAbsResourceStr(str string) (AbsResource, tfdiags.Diagnostics) { // If error diagnostics are returned then the AbsResource value is invalid and // must not be used. func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { - addr, diags := ParseTarget(traversal) + target, diags := ParseTarget(traversal) if diags.HasErrors() { return AbsResourceInstance{}, diags } + addr, validateDiags := validateResourceFromTarget(target, traversal.SourceRange().Ptr()) + diags = diags.Append(validateDiags) + return addr, diags +} + +// ParsePartialResourceInstance attempts to interpret the given traversal as a +// partial absolute resource instance address, using the same syntax as expected +// by ParsePartialTarget. +// +// If no error diagnostics are returned, the returned target includes the +// address that was extracted and the source range it was extracted from. +// +// If error diagnostics are returned then the AbsResource value is invalid and +// must not be used. +func ParsePartialResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfdiags.Diagnostics) { + target, diags := ParsePartialTarget(traversal) + if diags.HasErrors() { + return AbsResourceInstance{}, diags + } + + addr, validateDiags := validateResourceFromTarget(target, traversal.SourceRange().Ptr()) + diags = diags.Append(validateDiags) + return addr, diags +} + +func validateResourceFromTarget(addr *Target, src *hcl.Range) (AbsResourceInstance, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch tt := addr.Subject.(type) { case AbsResource: @@ -299,7 +465,7 @@ func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfd Severity: hcl.DiagError, Summary: "Invalid address", Detail: "A resource instance address is required here. The module path must be followed by a resource instance specification.", - Subject: traversal.SourceRange().Ptr(), + Subject: src, }) return AbsResourceInstance{}, diags @@ -308,7 +474,7 @@ func ParseAbsResourceInstance(traversal hcl.Traversal) (AbsResourceInstance, tfd Severity: hcl.DiagError, Summary: "Invalid address", Detail: "A resource address is required here.", - Subject: traversal.SourceRange().Ptr(), + Subject: src, }) return AbsResourceInstance{}, diags @@ -341,6 +507,32 @@ func ParseAbsResourceInstanceStr(str string) (AbsResourceInstance, tfdiags.Diagn return addr, diags } +// ParsePartialResourceInstanceStr is a helper wrapper around +// ParsePartialResourceInstance that takes a string and parses it with the HCL +// native syntax traversal parser before interpreting it. +// +// Error diagnostics are returned if either the parsing fails or the analysis +// of the traversal fails. There is no way for the caller to distinguish the +// two kinds of diagnostics programmatically. If error diagnostics are returned +// the returned address may be incomplete. +// +// Since this function has no context about the source of the given string, +// any returned diagnostics will not have meaningful source location +// information. +func ParsePartialResourceInstanceStr(str string) (AbsResourceInstance, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + traversal, parseDiags := hclsyntax.ParseTraversalPartial([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) + diags = diags.Append(parseDiags) + if parseDiags.HasErrors() { + return AbsResourceInstance{}, diags + } + + addr, addrDiags := ParsePartialResourceInstance(traversal) + diags = diags.Append(addrDiags) + return addr, diags +} + // ModuleAddr returns the module address portion of the subject of // the recieving target. // diff --git a/internal/addrs/parse_target_test.go b/internal/addrs/parse_target_test.go index 6e838d0e51..781f386848 100644 --- a/internal/addrs/parse_target_test.go +++ b/internal/addrs/parse_target_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -6,6 +9,7 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -83,6 +87,24 @@ func TestParseTarget(t *testing.T) { }, ``, }, + { + `resource.aws_instance.foo`, + &Target{ + Subject: AbsResource{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + }, + Module: RootModuleInstance, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 26, Byte: 25}, + }, + }, + ``, + }, { `aws_instance.foo[1]`, &Target{ @@ -143,6 +165,45 @@ func TestParseTarget(t *testing.T) { }, ``, }, + { + `ephemeral.aws_instance.foo`, + &Target{ + Subject: AbsResource{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "aws_instance", + Name: "foo", + }, + Module: RootModuleInstance, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 27, Byte: 26}, + }, + }, + ``, + }, + { + `ephemeral.aws_instance.foo[1]`, + &Target{ + Subject: AbsResourceInstance{ + Resource: ResourceInstance{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "aws_instance", + Name: "foo", + }, + Key: IntKey(1), + }, + Module: RootModuleInstance, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29}, + }, + }, + ``, + }, { `module.foo.aws_instance.bar`, &Target{ @@ -249,6 +310,27 @@ func TestParseTarget(t *testing.T) { }, ``, }, + { + `module.foo.module.bar.ephemeral.aws_instance.baz`, + &Target{ + Subject: AbsResource{ + Resource: Resource{ + Mode: EphemeralResourceMode, + Type: "aws_instance", + Name: "baz", + }, + Module: ModuleInstance{ + {Name: "foo"}, + {Name: "bar"}, + }, + }, + SourceRange: tfdiags.SourceRange{ + Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0}, + End: tfdiags.SourcePos{Line: 1, Column: 49, Byte: 48}, + }, + }, + ``, + }, { `module.foo.module.bar[0].data.aws_instance.baz`, &Target{ @@ -349,6 +431,61 @@ func TestParseTarget(t *testing.T) { nil, `Unexpected extra operators after address.`, }, + { + `each.key`, + nil, + `The keyword "each" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `module.foo[1].each`, + nil, + `The keyword "each" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `count.index`, + nil, + `The keyword "count" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `local.value`, + nil, + `The keyword "local" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `path.root`, + nil, + `The keyword "path" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `self.id`, + nil, + `The keyword "self" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `terraform.planning`, + nil, + `The keyword "terraform" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `var.foo`, + nil, + `The keyword "var" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `template`, + nil, + `The keyword "template" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `lazy`, + nil, + `The keyword "lazy" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, + { + `arg`, + nil, + `The keyword "arg" is reserved and cannot be used to target a resource address. If you are targeting a resource type that uses a reserved keyword, please prefix your address with "resource.".`, + }, } for _, test := range tests { @@ -381,7 +518,7 @@ func TestParseTarget(t *testing.T) { } for _, problem := range deep.Equal(got, test.Want) { - t.Errorf(problem) + t.Error(problem) } }) } diff --git a/internal/addrs/partial_expanded.go b/internal/addrs/partial_expanded.go new file mode 100644 index 0000000000..d68f80eac8 --- /dev/null +++ b/internal/addrs/partial_expanded.go @@ -0,0 +1,865 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// PartialExpandedModule represents a set of module instances which all share +// a common known parent module instance but the remaining call instance keys +// are not yet known. +type PartialExpandedModule struct { + // expandedPrefix is the initial part of the module address whose expansion + // is already complete and so has exact instance keys. + expandedPrefix ModuleInstance + + // unexpandedSuffix is the remainder of the module address whose instance + // keys are not known yet. This is a slight abuse of type [Module] because + // it's representing a relative path from expandedPrefix rather than a + // path from the root module as usual, so this value must never be exposed + // in the public API of this package. + // + // This can be zero-length in PartialExpandedModule values used as part + // of the internals of a PartialExpandedResource, but should never be + // zero-length in a publicly-exposed PartialExpandedModule because that + // would make this just a degenerate ModuleInstance. + unexpandedSuffix Module +} + +// ParsePartialExpandedModule parses a module address traversal and returns a +// PartialExpandedModule representing the known and unknown parts of the +// address. +// +// It returns the parsed PartialExpandedModule, the remaining traversal steps +// that were not consumed by this function, and any diagnostics that were +// generated during parsing. +func ParsePartialExpandedModule(traversal hcl.Traversal) (PartialExpandedModule, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + remain := traversal + var partial PartialExpandedModule + + // We'll step through the traversal steps and build up the known prefix + // of the module address. When we reach a call with an unknown index, we'll + // switch to building up the unexpanded suffix. + expanded := true + +LOOP: + for len(remain) > 0 { + var next string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + next = tt.Name + case hcl.TraverseAttr: + next = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Module address prefix must be followed by dot and then a name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + + if next != "module" { + break + } + + kwRange := remain[0].SourceRange() + remain = remain[1:] + if len(remain) == 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: &kwRange, + }) + break + } + + var moduleName string + switch tt := remain[0].(type) { + case hcl.TraverseAttr: + moduleName = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break LOOP + } + remain = remain[1:] + + if expanded { + + step := ModuleInstanceStep{ + Name: moduleName, + } + + if len(remain) > 0 { + if idx, ok := remain[0].(hcl.TraverseIndex); ok { + remain = remain[1:] + + if !idx.Key.IsKnown() { + // We'll switch to building up the unexpanded suffix + // starting with this step. + expanded = false + partial.unexpandedSuffix = append(partial.unexpandedSuffix, moduleName) + continue + } + + switch idx.Key.Type() { + case cty.String: + step.InstanceKey = StringKey(idx.Key.AsString()) + case cty.Number: + var idxInt int + err := gocty.FromCtyValue(idx.Key, &idxInt) + if err == nil { + step.InstanceKey = IntKey(idxInt) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: fmt.Sprintf("Invalid module index: %s.", err), + Subject: idx.SourceRange().Ptr(), + }) + } + default: + // Should never happen, because no other types are allowed in traversal indices. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Invalid module key: must be either a string or an integer.", + Subject: idx.SourceRange().Ptr(), + }) + } + } + } + + partial.expandedPrefix = append(partial.expandedPrefix, step) + continue + } + + // Otherwise, we'll process this as an unexpanded suffix. + partial.unexpandedSuffix = append(partial.unexpandedSuffix, moduleName) + + if len(remain) > 0 { + if _, ok := remain[0].(hcl.TraverseIndex); ok { + // Then we have a module instance key. We're now parsing the + // unexpanded suffix of the module address, so we'll just + // ignore it. + remain = remain[1:] + } + } + } + + var retRemain hcl.Traversal + if len(remain) > 0 { + retRemain = make(hcl.Traversal, len(remain)) + copy(retRemain, remain) + // The first element here might be either a TraverseRoot or a + // TraverseAttr, depending on whether we had a module address on the + // front. To make life easier for callers, we'll normalize to always + // start with a TraverseRoot. + if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { + retRemain[0] = hcl.TraverseRoot{ + Name: tt.Name, + SrcRange: tt.SrcRange, + } + } + } + + return partial, retRemain, diags +} + +func (m ModuleInstance) UnexpandedChild(call ModuleCall) PartialExpandedModule { + return PartialExpandedModule{ + expandedPrefix: m, + unexpandedSuffix: Module{call.Name}, + } +} + +// PartialModule reverses the process of UnknownModuleInstance by converting a +// ModuleInstance back into a PartialExpandedModule. +func (m ModuleInstance) PartialModule() PartialExpandedModule { + pem := PartialExpandedModule{} + for _, step := range m { + if step.InstanceKey == WildcardKey { + pem.unexpandedSuffix = append(pem.unexpandedSuffix, step.Name) + continue + } + pem.expandedPrefix = append(pem.expandedPrefix, step) + } + return pem +} + +// UnknownModuleInstance expands the receiver to a full ModuleInstance by +// replacing the unknown instance keys with a wildcard value. +func (pem PartialExpandedModule) UnknownModuleInstance() ModuleInstance { + base := pem.expandedPrefix + for _, call := range pem.unexpandedSuffix { + base = append(base, ModuleInstanceStep{ + Name: call, + InstanceKey: WildcardKey, + }) + } + return base +} + +// LevelsKnown returns the number of module path segments of the address that +// have known instance keys. +// +// This might be useful, for example, for preferring a more-specifically-known +// address over a less-specifically-known one when selecting a placeholder +// value to use to represent an object beneath an unexpanded module address. +func (pem PartialExpandedModule) LevelsKnown() int { + return len(pem.expandedPrefix) +} + +// MatchesInstance returns true if and only if the given module instance +// belongs to the recieving partially-expanded module address pattern. +func (pem PartialExpandedModule) MatchesInstance(inst ModuleInstance) bool { + // Total length must always match. + if len(inst) != (len(pem.expandedPrefix) + len(pem.unexpandedSuffix)) { + return false + } + + // The known prefix must match exactly. + givenExpandedPrefix := inst[:len(pem.expandedPrefix)] + if !givenExpandedPrefix.Equal(pem.expandedPrefix) { + return false + } + + // The known suffix must match the call names, even though we don't yet + // know the specific instance keys. + givenExpandedSuffix := inst[len(pem.expandedPrefix):] + for i := range pem.unexpandedSuffix { + if pem.unexpandedSuffix[i] != givenExpandedSuffix[i].Name { + return false + } + } + + // If we passed all the filters above then it's a match. + return true +} + +// MatchesPartial returns true if and only if the receiver represents the same +// static module as the other given module and the receiver's known instance +// keys are a prefix of the other module's. +func (pem PartialExpandedModule) MatchesPartial(other PartialExpandedModule) bool { + // The two addresses must represent the same static module, regardless + // of the instance keys of those modules. + if !pem.Module().Equal(other.Module()) { + return false + } + + if len(pem.expandedPrefix) > len(other.expandedPrefix) { + return false + } + + thisPrefix := pem.expandedPrefix + otherPrefix := other.expandedPrefix[:len(pem.expandedPrefix)] + return thisPrefix.Equal(otherPrefix) +} + +// Module returns the unexpanded module address that this pattern originated +// from. +func (pem PartialExpandedModule) Module() Module { + ret := pem.expandedPrefix.Module() + return append(ret, pem.unexpandedSuffix...) +} + +// KnownPrefix returns the longest possible ModuleInstance address made of +// known segments of this partially-expanded module instance address. +func (pem PartialExpandedModule) KnownPrefix() ModuleInstance { + if len(pem.expandedPrefix) == 0 { + return nil + } + + // Although we can't enforce it with the Go compiler, our convention is + // that we never mutate address values outside of this package and so + // we'll expose our pem.expandedPrefix buffer directly here and trust that + // the caller will play nice with it. However, we do force the unused + // capacity to zero so that the caller can safely construct child addresses, + // which would append new steps to the end. + return pem.expandedPrefix[:len(pem.expandedPrefix):len(pem.expandedPrefix)] +} + +// FirstUnexpandedCall returns the address of the first step in the module +// path whose instance keys are not yet known, discarding any subsequent +// calls beneath it. +func (pem PartialExpandedModule) FirstUnexpandedCall() AbsModuleCall { + // NOTE: This assumes that there's always at least one element in + // unexpandedSuffix because it should only be used with the public-facing + // version of PartialExpandedModule where that contract always holds. It's + // not safe to use this for the PartialExpandedModule value hidden in the + // internals of PartialExpandedResource. + return AbsModuleCall{ + Module: pem.KnownPrefix(), + Call: ModuleCall{ + Name: pem.unexpandedSuffix[0], + }, + } +} + +// UnexpandedSuffix returns the local addresses of all of the calls whose +// instances are not yet expanded, in the module tree traversal order. +// +// Method KnownPrefix concatenated with UnexpandedSuffix (assuming that were +// actually possible) represents the whole module path that the +// PartialExpandedModule encapsulates. +func (pem PartialExpandedModule) UnexpandedSuffix() []ModuleCall { + if len(pem.unexpandedSuffix) == 0 { + // Should never happen for any publicly-visible value of this type, + // because we should always have at least one unexpanded call, + // but we'll allow it anyway since we have a reasonable return value + // for that case. + return nil + } + + // A []ModuleCall is the only representation of a non-rooted chain of + // module calls that we're allowed to export in our public API, and so + // we'll transform our not-quite-allowed unrooted "Module" value in that + // form externally. + ret := make([]ModuleCall, len(pem.unexpandedSuffix)) + for i, name := range pem.unexpandedSuffix { + ret[i].Name = name + } + return ret +} + +// Child returns the address of a child of the receiver that belongs to the +// given module call. +func (pem PartialExpandedModule) Child(call ModuleCall) PartialExpandedModule { + return PartialExpandedModule{ + expandedPrefix: pem.expandedPrefix, + unexpandedSuffix: append(pem.unexpandedSuffix, call.Name), + } +} + +// Resource returns the address of a resource within the receiver. +func (pem PartialExpandedModule) Resource(resource Resource) PartialExpandedResource { + return PartialExpandedResource{ + module: pem, + resource: resource, + } +} + +// String returns a string representation of the pattern where the known +// prefix uses the normal module instance address syntax and the unknown +// suffix steps use a similar syntax but with "[*]" as a placeholder to +// represent instance keys that aren't yet known. +func (pem PartialExpandedModule) String() string { + var buf strings.Builder + if len(pem.expandedPrefix) != 0 { + buf.WriteString(pem.expandedPrefix.String()) + } + for i, callName := range pem.unexpandedSuffix { + if i > 0 || len(pem.expandedPrefix) != 0 { + buf.WriteByte('.') + } + buf.WriteString("module.") + buf.WriteString(callName) + buf.WriteString("[*]") + } + return buf.String() +} + +func (pem PartialExpandedModule) UniqueKey() UniqueKey { + return partialExpandedModuleKey(pem.String()) +} + +type partialExpandedModuleKey string + +var _ UniqueKey = partialExpandedModuleKey("") + +func (partialExpandedModuleKey) uniqueKeySigil() {} + +// PartialExpandedResource represents a set of resource instances which all share +// a common known parent module instance but the remaining call instance keys +// are not yet known and the resource's own instance keys are not yet known. +// +// A PartialExpandedResource with a fully-known module instance address is +// semantically interchangable with an [AbsResource], which is useful when we +// need to represent an assortment of variously-unknown resource instance +// addresses, but [AbsResource] is preferable in situations where the module +// instance address is _always_ known and it's only the resource instance +// key that is not represented. +type PartialExpandedResource struct { + // module is the partially-expanded module instance address that this + // resource belongs to. + // + // This value can actually represent a fully-expanded module if its + // unexpandedSuffix field is zero-length, in which case it's only the + // resource itself that's unexpanded, which would make this equivalent + // to an AbsResource. + // + // We mustn't directly expose this value in the public API because + // external callers must never see a PartialExpandedModule that is + // actually fully-expanded; that should be a ModuleInstance instead. + module PartialExpandedModule + resource Resource +} + +// ParsePartialExpandedResource parses a resource address traversal and returns +// a PartialExpandedResource representing the known and unknown parts of the +// address. +func ParsePartialExpandedResource(traversal hcl.Traversal) (PartialExpandedResource, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + pem, remain, diags := ParsePartialExpandedModule(traversal) + if len(remain) == 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Resource address must be a module address followed by a resource address.", + Subject: traversal.SourceRange().Ptr(), + }) + return PartialExpandedResource{}, nil, diags + } + + // We know that remain[0] is a hcl.TraverseRoot object as the + // ParsePartialExpandedModule function always returns a hcl.TraverseRoot + // object as the first element in the remain slice. + + mode := ManagedResourceMode + if remain.RootName() == "data" { + mode = DataResourceMode + remain = remain[1:] + } else if remain.RootName() == "resource" { + // Starting a resource address with "resource" is optional, so we'll + // just ignore it if it's present. + remain = remain[1:] + } + + if len(remain) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "Resource specification must include a resource type and name.", + Subject: remain.SourceRange().Ptr(), + }) + return PartialExpandedResource{}, nil, diags + } + + var typeName, name string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + typeName = tt.Name + case hcl.TraverseAttr: + typeName = tt.Name + default: + switch mode { + case ManagedResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A resource type name is required.", + Subject: remain[0].SourceRange().Ptr(), + }) + case DataResourceMode: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A data source name is required.", + Subject: remain[0].SourceRange().Ptr(), + }) + default: + panic("unknown mode") + } + return PartialExpandedResource{}, nil, diags + } + + switch tt := remain[1].(type) { + case hcl.TraverseAttr: + name = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address", + Detail: "A resource name is required.", + Subject: remain[1].SourceRange().Ptr(), + }) + return PartialExpandedResource{}, nil, diags + } + + remain = remain[2:] + if len(remain) > 0 { + if _, ok := remain[0].(hcl.TraverseIndex); ok { + // Then we have a resource instance key. Since, we're building a + // PartialExpandedResource, we'll just ignore it. + remain = remain[1:] + } + } + + return PartialExpandedResource{ + module: pem, + resource: Resource{ + Mode: mode, + Type: typeName, + Name: name, + }, + }, remain, diags +} + +// UnexpandedResource returns the address of a child resource expressed as a +// [PartialExpandedResource]. +// +// The result always has a fully-qualified module instance address and is +// therefore semantically equivalent to an [AbsResource], so this variannt +// should be used only in contexts where we might also be storing resources +// belonging to not-fully-expanded modules and need to use the same static +// address type for all of them. +func (m ModuleInstance) UnexpandedResource(resource Resource) PartialExpandedResource { + return PartialExpandedResource{ + module: PartialExpandedModule{ + expandedPrefix: m, + }, + resource: resource, + } +} + +// UnexpandedResource returns the receiver reinterpreted as a +// [PartialExpandedResource], which is an alternative form we use in situations +// where we might also need to mix in resources belonging to not-yet-fully-known +// module instance addresses. +func (r AbsResource) UnexpandedResource() PartialExpandedResource { + return PartialExpandedResource{ + module: PartialExpandedModule{ + expandedPrefix: r.Module, + }, + resource: r.Resource, + } +} + +// PartialResource reverses UnknownResourceInstance by converting the +// AbsResourceInstance back into a PartialExpandedResource. +func (r AbsResourceInstance) PartialResource() PartialExpandedResource { + return PartialExpandedResource{ + module: r.Module.PartialModule(), + resource: r.Resource.Resource, + } +} + +// UnknownResourceInstance returns an [AbsResourceInstance] that represents the +// same resource as the receiver but with all instance keys replaced with a +// wildcard value. +func (per PartialExpandedResource) UnknownResourceInstance() AbsResourceInstance { + return AbsResourceInstance{ + Module: per.module.UnknownModuleInstance(), + Resource: per.resource.Instance(WildcardKey), + } +} + +// MatchesInstance returns true if and only if the given resource instance +// belongs to the recieving partially-expanded resource address pattern. +func (per PartialExpandedResource) MatchesInstance(inst AbsResourceInstance) bool { + if !per.module.MatchesInstance(inst.Module) { + return false + } + return inst.Resource.Resource.Equal(per.resource) +} + +// MatchesResource returns true if and only if the given resource belongs to +// the recieving partially-expanded resource address pattern. +func (per PartialExpandedResource) MatchesResource(inst AbsResource) bool { + if !per.module.MatchesInstance(inst.Module) { + return false + } + return inst.Resource.Equal(per.resource) +} + +// MatchesPartial returns true if the underlying partial module address matches +// the given partial module address and the resource type and name match the +// receiver's resource type and name. +func (per PartialExpandedResource) MatchesPartial(other PartialExpandedResource) bool { + if !per.module.MatchesPartial(other.module) { + return false + } + return per.resource.Equal(other.resource) +} + +// AbsResource returns the single [AbsResource] that this address represents +// if this pattern is specific enough to match only a single resource, or +// the zero value of AbsResource if not. +// +// The second return value is true if and only if the returned address is valid. +func (per PartialExpandedResource) AbsResource() (AbsResource, bool) { + if len(per.module.unexpandedSuffix) != 0 { + return AbsResource{}, false + } + + return AbsResource{ + Module: per.module.expandedPrefix, + Resource: per.resource, + }, true +} + +// ConfigResource returns the unexpanded resource address that this +// partially-expanded resource address originates from. +func (per PartialExpandedResource) ConfigResource() ConfigResource { + return ConfigResource{ + Module: per.module.Module(), + Resource: per.resource, + } +} + +// Resource returns just the leaf resource address that this partially-expanded +// resource address uses, discarding the containing module instance information +// altogether. +func (per PartialExpandedResource) Resource() Resource { + return per.resource +} + +// KnownModuleInstancePrefix returns the longest possible ModuleInstance address +// made of known segments of the module instances that this set of resource +// instances all belong to. +// +// If the whole module instance address is known and only the resource +// instances are not then this returns the full prefix, which will be the same +// as the module from a successful return value from +// [PartialExpandedResource.AbsResource]. +func (per PartialExpandedResource) KnownModuleInstancePrefix() ModuleInstance { + return per.module.KnownPrefix() +} + +// ModuleInstance returns the fully-qualified [ModuleInstance] that this +// partial-expanded resource belongs to, but only if its module instance +// address is fully known. +// +// The second return value is false if the module instance address is not +// fully expanded, in which case the first return value is invalid. Use +// [PartialExpandedResource.PartialExpandedModule] instead in that case. +func (per PartialExpandedResource) ModuleInstance() (ModuleInstance, bool) { + if len(per.module.unexpandedSuffix) != 0 { + return nil, false + } + return per.module.expandedPrefix, true +} + +// PartialExpandedModule returns a [PartialExpandedModule] address describing +// the partially-unknown module instance address that the resource belongs to, +// but only if the module instance address is not fully known. +// +// The second return value is false if the module instance address is actually +// fully expanded, in which case the first return value is invalid. Use +// [PartialExpandedResource.ModuleInstance] instead in that case. +func (per PartialExpandedResource) PartialExpandedModule() (PartialExpandedModule, bool) { + if len(per.module.unexpandedSuffix) == 0 { + return PartialExpandedModule{}, false + } + return per.module, true +} + +// IsTargetedBy returns true if and only if the given targetable address might +// target the resource instances that could exist if the receiver were fully +// expanded. +func (per PartialExpandedResource) IsTargetedBy(addr Targetable) bool { + + compareModule := func(module Module) bool { + // We'll step through each step in the module address and compare it + // to the known prefix and unexpanded suffix of the receiver. If we + // find a mismatch then we know the receiver can't be targeted by this + // address. + for ix, step := range module { + if ix >= len(per.module.expandedPrefix) { + ix = ix - len(per.module.expandedPrefix) + if ix >= len(per.module.unexpandedSuffix) { + // Then the target address has more steps than the receiver + // and so can't possibly target it. + return false + } + if step != per.module.unexpandedSuffix[ix] { + // Then the target address has a different step at this + // position than the receiver does, so it can't target it. + return false + } + } else { + if step != per.module.expandedPrefix[ix].Name { + // Then the target address has a different step at this + // position than the receiver does, so it can't target it. + return false + } + } + } + + // If we make it here then the target address is a prefix of the + // receivers module address, so it could potentially target the + // receiver. + return true + } + + compareModuleInstance := func(inst ModuleInstance) bool { + // We'll step through each step in the module address and compare it + // to the known prefix and unexpanded suffix of the receiver. If we + // find a mismatch then we know the receiver can't be targeted by this + // address. + for ix, step := range inst { + if ix >= len(per.module.expandedPrefix) { + ix = ix - len(per.module.expandedPrefix) + if ix >= len(per.module.unexpandedSuffix) { + // Then the target address has more steps than the receiver + // and so can't possibly target it. + return false + } + if step.Name != per.module.unexpandedSuffix[ix] { + // Then the target address has a different step at this + // position than the receiver does, so it can't target it. + return false + } + } else { + if step.Name != per.module.expandedPrefix[ix].Name || (step.InstanceKey != NoKey && step.InstanceKey != per.module.expandedPrefix[ix].InstanceKey) { + // Then the target address has a different step at this + // position than the receiver does, so it can't target it. + return false + } + } + } + + // If we make it here then the target address is a prefix of the + // receivers module address, so it could potentially target the + // receiver. + return true + } + + switch addr.AddrType() { + case ConfigResourceAddrType: + addr := addr.(ConfigResource) + if !compareModule(addr.Module) { + return false + } + return addr.Resource.Equal(per.resource) + case AbsResourceAddrType: + addr := addr.(AbsResource) + if !compareModuleInstance(addr.Module) { + return false + } + return addr.Resource.Equal(per.resource) + case AbsResourceInstanceAddrType: + addr := addr.(AbsResourceInstance) + if !compareModuleInstance(addr.Module) { + return false + } + return addr.Resource.Resource.Equal(per.resource) + case ModuleAddrType: + return compareModule(addr.(Module)) + case ModuleInstanceAddrType: + return compareModuleInstance(addr.(ModuleInstance)) + } + + return false +} + +// String returns a string representation of the pattern which uses the special +// placeholder "[*]" to represent positions where instance keys are not yet +// known. +func (per PartialExpandedResource) String() string { + moduleAddr := per.module.String() + if len(moduleAddr) != 0 { + return moduleAddr + "." + per.resource.String() + "[*]" + } + return per.resource.String() + "[*]" +} + +func (per PartialExpandedResource) UniqueKey() UniqueKey { + // If this address is equivalent to an AbsResource address then we'll + // return its instance key here so that function Equivalent will consider + // the two as equivalent. + if ar, ok := per.AbsResource(); ok { + return ar.UniqueKey() + } + // For not-fully-expanded module paths we'll use a distinct address type + // since there is no other address type equivalent to those. + return partialExpandedResourceKey(per.String()) +} + +type partialExpandedResourceKey string + +var _ UniqueKey = partialExpandedModuleKey("") + +func (partialExpandedResourceKey) uniqueKeySigil() {} + +// InPartialExpandedModule is a generic type used for all address types that +// represent objects that exist inside module instances but do not have any +// expansion capability of their own beyond just the containing module +// expansion. +// +// Although not enforced by the type system, this type should be used only for +// address types T that are combined with a ModuleInstance value in a type +// whose name starts with "Abs". For example, [LocalValue] is a reasonable T +// because [AbsLocalValue] represents a local value inside a particular module +// instance. InPartialExpandedModule[LocalValue] is therefore like an +// [AbsLocalValue] whose module path isn't fully known yet. +// +// This type is here primarily just to have implementations of [UniqueKeyer] +// so we can store partially-evaluated objects from unexpanded modules in +// collections for later reference downstream. +type InPartialExpandedModule[T interface { + UniqueKeyer + fmt.Stringer +}] struct { + Module PartialExpandedModule + Local T +} + +// ObjectInPartialExpandedModule is a constructor for [InPartialExpandedModule] +// that's here primarily just to benefit from function type parameter inference +// to avoid manually writing out type T when constructing such a value. +func ObjectInPartialExpandedModule[T interface { + UniqueKeyer + fmt.Stringer +}](module PartialExpandedModule, local T) InPartialExpandedModule[T] { + return InPartialExpandedModule[T]{ + Module: module, + Local: local, + } +} + +var _ UniqueKeyer = InPartialExpandedModule[LocalValue]{} + +// ModuleLevelsKnown returns the number of module path segments of the address +// that have known instance keys. +// +// This might be useful, for example, for preferring a more-specifically-known +// address over a less-specifically-known one when selecting a placeholder +// value to use to represent an object beneath an unexpanded module address. +func (in InPartialExpandedModule[T]) ModuleLevelsKnown() int { + return in.Module.LevelsKnown() +} + +// String returns a string representation of the pattern which uses the special +// placeholder "[*]" to represent positions where module instance keys are not +// yet known. +func (in InPartialExpandedModule[T]) String() string { + moduleAddr := in.Module.String() + if len(moduleAddr) != 0 { + return moduleAddr + "." + in.Local.String() + } + return in.Local.String() +} + +func (in InPartialExpandedModule[T]) UniqueKey() UniqueKey { + return inPartialExpandedModuleUniqueKey{ + moduleKey: in.Module.UniqueKey(), + localKey: in.Local.UniqueKey(), + } +} + +type inPartialExpandedModuleUniqueKey struct { + moduleKey UniqueKey + localKey UniqueKey +} + +func (inPartialExpandedModuleUniqueKey) uniqueKeySigil() {} diff --git a/internal/addrs/partial_expanded_test.go b/internal/addrs/partial_expanded_test.go new file mode 100644 index 0000000000..8f067fa4e2 --- /dev/null +++ b/internal/addrs/partial_expanded_test.go @@ -0,0 +1,407 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "fmt" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" +) + +func TestPartialExpandedResourceIsTargetedBy(t *testing.T) { + + tcs := []struct { + per string + target string + want bool + }{ + { + "test.a", + "test.a", + true, + }, + { + "test.a", + "test.a[0]", + true, + }, + { + "test.a[*]", + "test.a", + true, + }, + { + "test.a[*]", + "test.a[0]", + true, + }, + { + "test.a[*]", + "test.a[\"key\"]", + true, + }, + { + "module.mod.test.a", + "module.mod.test.a", + true, + }, + { + "module.mod[1].test.a", + "module.mod[0].test.a", + false, + }, + { + "module.mod.test.a[*]", + "module.mod.test.a", + true, + }, + { + "module.mod.test.a[*]", + "module.mod.test.a[0]", + true, + }, + { + "module.mod.test.a[*]", + "module.mod.test.a[\"key\"]", + true, + }, + { + "module.mod.test.a[*]", + "module.mod[0].test.a", + false, + }, + { + "module.mod[1].test.a[*]", + "module.mod[\"key\"].test.a[0]", + false, + }, + { + "module.mod[*].test.a", + "module.mod.test.a", + true, + }, + { + "module.mod[*].test.a", + "module.mod.test.a[0]", + true, + }, + { + "module.mod[*].test.a", + "module.mod[0].test.a", + true, + }, + { + "module.mod[*].test.a", + "module.mod[\"key\"].test.a", + true, + }, + } + + for _, tc := range tcs { + t.Run(fmt.Sprintf("PartialResource(%q).IsTargetedBy(%q)", tc.per, tc.target), func(t *testing.T) { + per := mustParsePartialResourceInstanceStr(tc.per).PartialResource() + target := mustParseTarget(tc.target) + + got := per.IsTargetedBy(target) + if got != tc.want { + t.Errorf("PartialResource(%q).IsTargetedBy(%q): got %v; want %v", tc.per, tc.target, got, tc.want) + } + }) + } + +} + +func TestParsePartialExpandedModule(t *testing.T) { + + // these functions are a bit weird, as the normal parsing supported by + // HCL can't put unknown values into the instance keys. So we need to + // build the traversals in the same way the thing that is calling these + // functions does. + + tcs := []struct { + traversal func(t *testing.T) (string, hcl.Traversal) + want PartialExpandedModule + remain int + }{ + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.mod" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + return addr, traversal + }, + want: PartialExpandedModule{ + expandedPrefix: ModuleInstance{ + { + Name: "mod", + }, + }, + }, + remain: 0, + }, + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.mod[0]" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + // Hack the key into an unknown value. + traversal[2] = hcl.TraverseIndex{ + Key: cty.UnknownVal(cty.Number), + } + return "module.mod[*]", traversal + }, + want: PartialExpandedModule{ + unexpandedSuffix: Module{ + "mod", + }, + }, + remain: 0, + }, + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.child.module.grandchild" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + return addr, traversal + }, + want: PartialExpandedModule{ + expandedPrefix: ModuleInstance{ + { + Name: "child", + }, + { + Name: "grandchild", + }, + }, + }, + remain: 0, + }, + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.child[0].module.grandchild" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + return addr, traversal + }, + want: PartialExpandedModule{ + expandedPrefix: ModuleInstance{ + { + Name: "child", + InstanceKey: IntKey(0), + }, + { + Name: "grandchild", + }, + }, + }, + remain: 0, + }, + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.child[0].module.grandchild" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + traversal[2] = hcl.TraverseIndex{ + Key: cty.UnknownVal(cty.Number), + } + return "module.child[*].module.grandchild", traversal + }, + want: PartialExpandedModule{ + unexpandedSuffix: Module{ + "child", + "grandchild", + }, + }, + remain: 0, + }, + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.child.module.grandchild[0]" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + traversal[4] = hcl.TraverseIndex{ + Key: cty.UnknownVal(cty.Number), + } + return "module.child.module.grandchild[*]", traversal + }, + want: PartialExpandedModule{ + expandedPrefix: ModuleInstance{ + { + Name: "child", + }, + }, + unexpandedSuffix: Module{ + "grandchild", + }, + }, + remain: 0, + }, + { + traversal: func(t *testing.T) (string, hcl.Traversal) { + addr := "module.child.module.grandchild[0].resource_type.resource_name" + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %v", diags) + } + traversal[4] = hcl.TraverseIndex{ + Key: cty.UnknownVal(cty.Number), + } + return "module.child.module.grandchild[*].resource_type.resource_name", traversal + }, + want: PartialExpandedModule{ + expandedPrefix: ModuleInstance{ + { + Name: "child", + }, + }, + unexpandedSuffix: Module{ + "grandchild", + }, + }, + remain: 2, + }, + } + + for _, tc := range tcs { + addr, traversal := tc.traversal(t) + t.Run(addr, func(t *testing.T) { + module, rest, diags := ParsePartialExpandedModule(traversal) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + if !module.expandedPrefix.Equal(tc.want.expandedPrefix) { + t.Errorf("got expandedPrefix %v; want %v", module.expandedPrefix, tc.want.expandedPrefix) + } + if !module.unexpandedSuffix.Equal(tc.want.unexpandedSuffix) { + t.Errorf("got unexpandedSuffix %v; want %v", module.unexpandedSuffix, tc.want.unexpandedSuffix) + } + if len(rest) != tc.remain { + t.Errorf("got %d remaining traversals; want %d", len(rest), tc.remain) + } + }) + } + +} + +func TestParsePartialExpandedResource(t *testing.T) { + + tcs := []struct { + addr string + want PartialExpandedResource + remain int + }{ + { + addr: "resource_type.resource_name", + want: PartialExpandedResource{ + resource: Resource{ + Mode: ManagedResourceMode, + Type: "resource_type", + Name: "resource_name", + }, + }, + remain: 0, + }, + { + addr: "module.mod.resource_type.resource_name", + want: PartialExpandedResource{ + module: PartialExpandedModule{ + expandedPrefix: ModuleInstance{ + { + Name: "mod", + }, + }, + }, + resource: Resource{ + Mode: ManagedResourceMode, + Type: "resource_type", + Name: "resource_name", + }, + }, + }, + { + addr: "resource_type.resource_name[0]", + want: PartialExpandedResource{ + resource: Resource{ + Mode: ManagedResourceMode, + Type: "resource_type", + Name: "resource_name", + }, + }, + remain: 0, + }, + { + addr: "resource_type.resource_name[0].attr", + want: PartialExpandedResource{ + resource: Resource{ + Mode: ManagedResourceMode, + Type: "resource_type", + Name: "resource_name", + }, + }, + remain: 1, + }, + { + addr: "resource.resource_type.resource_name", + want: PartialExpandedResource{ + resource: Resource{ + Mode: ManagedResourceMode, + Type: "resource_type", + Name: "resource_name", + }, + }, + remain: 0, + }, + } + + for _, tc := range tcs { + t.Run(tc.addr, func(t *testing.T) { + traversal, traversalDiags := hclsyntax.ParseTraversalAbs([]byte(tc.addr), "", hcl.InitialPos) + if len(traversalDiags) > 0 { + t.Fatalf("unexpected diagnostics: %v", traversalDiags) + } + + partial, rest, diags := ParsePartialExpandedResource(traversal) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + if !partial.module.expandedPrefix.Equal(tc.want.module.expandedPrefix) { + t.Errorf("got expandedPrefix %v; want %v", partial.module.expandedPrefix, tc.want.module.expandedPrefix) + } + if !partial.module.unexpandedSuffix.Equal(tc.want.module.unexpandedSuffix) { + t.Errorf("got unexpandedSuffix %v; want %v", partial.module.unexpandedSuffix, tc.want.module.unexpandedSuffix) + } + if !partial.resource.Equal(tc.want.resource) { + t.Errorf("got resource %v; want %v", partial.resource, tc.want.resource) + } + if len(rest) != tc.remain { + t.Errorf("got %d remaining traversals; want %d", len(rest), tc.remain) + } + }) + } +} + +func mustParsePartialResourceInstanceStr(s string) AbsResourceInstance { + r, diags := ParsePartialResourceInstanceStr(s) + if diags.HasErrors() { + panic(diags.ErrWithWarnings().Error()) + } + return r +} diff --git a/internal/addrs/path_attr.go b/internal/addrs/path_attr.go index 9de5e134d1..e61ae64829 100644 --- a/internal/addrs/path_attr.go +++ b/internal/addrs/path_attr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // PathAttr is the address of an attribute of the "path" object in diff --git a/internal/addrs/provider.go b/internal/addrs/provider.go index 7130de1ea1..1f1dfe7198 100644 --- a/internal/addrs/provider.go +++ b/internal/addrs/provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/provider_config.go b/internal/addrs/provider_config.go index 3790c46d6b..21f36fc70f 100644 --- a/internal/addrs/provider_config.go +++ b/internal/addrs/provider_config.go @@ -1,12 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( "fmt" "strings" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" ) @@ -50,6 +54,7 @@ type LocalProviderConfig struct { } var _ ProviderConfig = LocalProviderConfig{} +var _ UniqueKeyer = LocalProviderConfig{} // NewDefaultLocalProviderConfig returns the address of the default (un-aliased) // configuration for the provider with the given local type name. @@ -84,6 +89,14 @@ func (pc LocalProviderConfig) StringCompact() string { return pc.LocalName } +// UniqueKey implements UniqueKeyer. +func (pc LocalProviderConfig) UniqueKey() UniqueKey { + // LocalProviderConfig acts as its own unique key. + return pc +} + +func (pc LocalProviderConfig) uniqueKeySigil() {} + // AbsProviderConfig is the absolute address of a provider configuration // within a particular module instance. type AbsProviderConfig struct { @@ -93,6 +106,7 @@ type AbsProviderConfig struct { } var _ ProviderConfig = AbsProviderConfig{} +var _ UniqueKeyer = AbsProviderConfig{} // ParseAbsProviderConfig parses the given traversal as an absolute provider // configuration address. The following are examples of traversals that can be @@ -108,7 +122,7 @@ var _ ProviderConfig = AbsProviderConfig{} // This type of address is typically not used prominently in the UI, except in // error messages that refer to provider configurations. func ParseAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { - modInst, remain, diags := parseModuleInstancePrefix(traversal) + modInst, remain, diags := parseModuleInstancePrefix(traversal, false) var ret AbsProviderConfig // Providers cannot resolve within module instances, so verify that there @@ -247,7 +261,7 @@ func ParseLegacyAbsProviderConfigStr(str string) (AbsProviderConfig, tfdiags.Dia // // We will not use this address form for any new file formats. func ParseLegacyAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { - modInst, remain, diags := parseModuleInstancePrefix(traversal) + modInst, remain, diags := parseModuleInstancePrefix(traversal, false) var ret AbsProviderConfig // Providers cannot resolve within module instances, so verify that there @@ -408,3 +422,73 @@ func (pc AbsProviderConfig) String() string { return strings.Join(parts, ".") } + +func (pc AbsProviderConfig) Equal(other AbsProviderConfig) bool { + if !pc.Provider.Equals(other.Provider) { + return false + } + if pc.Alias != other.Alias { + return false + } + if !pc.Module.Equal(other.Module) { + return false + } + return true +} + +// UniqueKey returns a unique key suitable for including the receiver in a +// generic collection type such as `Map` or `Set`. +// +// As a special case, the [UniqueKey] for an AbsProviderConfig that belongs +// to the root module is equal to the UniqueKey of the [RootProviderConfig] +// address describing the same provider configuration. [Equivalent] will +// return true if given an [AbsProviderConfig] and a [RootProviderConfig] +// that both represent the same address. +// +// Non-root provider configurations never have keys equal to a +// [RootProviderConfig]. +func (pc AbsProviderConfig) UniqueKey() UniqueKey { + if pc.Module.IsRoot() { + return RootProviderConfig{pc.Provider, pc.Alias}.UniqueKey() + } + return absProviderConfigUniqueKey(pc.String()) +} + +type absProviderConfigUniqueKey string + +func (k absProviderConfigUniqueKey) uniqueKeySigil() {} + +// RootProviderConfig is essentially a special variant of AbsProviderConfig +// for situations where only root module provider configurations are allowed. +// +// It represents the same configuration as a corresponding [AbsProviderConfig] +// whose Module field is set to [RootModule]. +type RootProviderConfig struct { + Provider Provider + Alias string +} + +// AbsProviderConfig returns the [AbsProviderConfig] value that represents the +// same provider configuration as the receiver. +// +// Specifically, it sets [AbsProviderConfig.Module] to [RootModule] and +// preserves the two other corresponding fields between these two types. +func (p RootProviderConfig) AbsProviderConfig() AbsProviderConfig { + return AbsProviderConfig{ + Module: RootModule, + Provider: p.Provider, + Alias: p.Alias, + } +} + +func (p RootProviderConfig) String() string { + return p.AbsProviderConfig().String() +} + +// UniqueKey returns a comparable unique key for the reciever suitable for +// use in generic collection types such as [Set] and [Map]. +func (p RootProviderConfig) UniqueKey() UniqueKey { + // A RootProviderConfig is inherently comparable and so can be its own key + return p +} +func (p RootProviderConfig) uniqueKeySigil() {} diff --git a/internal/addrs/provider_config_test.go b/internal/addrs/provider_config_test.go index 9dca4600ed..5caa382bdd 100644 --- a/internal/addrs/provider_config_test.go +++ b/internal/addrs/provider_config_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/provider_test.go b/internal/addrs/provider_test.go index 0f50475736..c7ee613349 100644 --- a/internal/addrs/provider_test.go +++ b/internal/addrs/provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( @@ -429,7 +432,7 @@ func TestParseProviderSourceStr(t *testing.T) { for name, test := range tests { got, diags := ParseProviderSourceString(name) for _, problem := range deep.Equal(got, test.Want) { - t.Errorf(problem) + t.Error(problem) } if len(diags) > 0 { if test.Err == false { diff --git a/internal/addrs/referenceable.go b/internal/addrs/referenceable.go index fbbc753d45..27d61bd704 100644 --- a/internal/addrs/referenceable.go +++ b/internal/addrs/referenceable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // Referenceable is an interface implemented by all address types that can diff --git a/internal/addrs/remove_target.go b/internal/addrs/remove_target.go new file mode 100644 index 0000000000..a683f95431 --- /dev/null +++ b/internal/addrs/remove_target.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Like MoveEndpoint, RemoveTarget is a wrapping struct that captures the result +// of decoding an HCL traversal representing a relative path from the current +// module to a removeable object. +// +// Remove targets are somewhat simpler than move endpoints, in that they deal +// only with resources and modules defined in configuration, not instances of +// those objects as recorded in state. We are therefore able to determine the +// ConfigMoveable up front, since specifying any resource or module instance key +// in a removed block is invalid. +// +// An interesting quirk of RemoveTarget is that RelSubject denotes a +// configuration object that, if the removed block is valid, should no longer +// exist in configuration. This "last known address" is used to locate and delete +// the appropriate state objects, or, in the case in which the user has forgotten +// to remove the object from configuration, to report the address of that block +// in an error diagnostic. +type RemoveTarget struct { + // SourceRange is the location of the target address in configuration. + SourceRange tfdiags.SourceRange + + // RelSubject, like MoveEndpoint's relSubject, abuses an absolute address + // type to represent a relative address. + RelSubject ConfigMoveable +} + +func (t *RemoveTarget) ObjectKind() RemoveTargetKind { + return removeTargetKind(t.RelSubject) +} + +func (t *RemoveTarget) String() string { + if t.ObjectKind() == RemoveTargetModule { + return t.RelSubject.(Module).String() + } else if t.ObjectKind() == RemoveTargetResource { + return t.RelSubject.(ConfigResource).String() + } + // No other valid address types + panic("Usupported remove target kind") +} + +func (t *RemoveTarget) Equal(other *RemoveTarget) bool { + switch { + case (t == nil) != (other == nil): + return false + case t == nil: + return true + default: + // We can safely compare string representations, since the Subject is a + // simple module or resource address. + return t.String() == other.String() && t.SourceRange == other.SourceRange + } +} + +func ParseRemoveTarget(traversal hcl.Traversal) (*RemoveTarget, tfdiags.Diagnostics) { + path, remain, diags := parseModulePrefix(traversal) + if diags.HasErrors() { + return nil, diags + } + + rng := tfdiags.SourceRangeFromHCL(traversal.SourceRange()) + + if len(remain) == 0 { + return &RemoveTarget{ + RelSubject: path, + SourceRange: rng, + }, diags + } + + rAddr, moreDiags := parseConfigResourceUnderModule(path, remain) + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return nil, diags + } + + if rAddr.Resource.Mode == DataResourceMode { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Data source address not allowed", + Detail: "Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.", + Subject: rng.ToHCL().Ptr(), + }) + } + + return &RemoveTarget{ + RelSubject: rAddr, + SourceRange: rng, + }, diags +} diff --git a/internal/addrs/remove_target_kind.go b/internal/addrs/remove_target_kind.go new file mode 100644 index 0000000000..edbe97e62c --- /dev/null +++ b/internal/addrs/remove_target_kind.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import "fmt" + +// RemoveTargetKind represents the different kinds of object that a remove +// target address can refer to. +type RemoveTargetKind rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type RemoveTargetKind + +const ( + // RemoveTargetModule indicates that a remove target refers to + // all instances of a particular module call. + RemoveTargetModule RemoveTargetKind = 'M' + + // RemoveTargetResource indicates that a remove target refers to + // all instances of a particular resource. + RemoveTargetResource RemoveTargetKind = 'R' +) + +func removeTargetKind(addr ConfigMoveable) RemoveTargetKind { + switch addr := addr.(type) { + case Module: + return RemoveTargetModule + case ConfigResource: + return RemoveTargetResource + default: + // The above should be exhaustive for all ConfigMoveable types. + panic(fmt.Sprintf("unsupported address type %T", addr)) + } +} diff --git a/internal/addrs/remove_target_test.go b/internal/addrs/remove_target_test.go new file mode 100644 index 0000000000..f01d203f01 --- /dev/null +++ b/internal/addrs/remove_target_test.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func TestParseRemoveTarget(t *testing.T) { + tests := []struct { + Input string + Want ConfigMoveable + WantErr string + }{ + { + `test_instance.bar`, + ConfigResource{ + Module: RootModule, + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }, + }, + ``, + }, + { + `module.foo.test_instance.bar`, + ConfigResource{ + Module: []string{"foo"}, + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }, + }, + ``, + }, + { + `module.foo.module.baz.test_instance.bar`, + ConfigResource{ + Module: []string{"foo", "baz"}, + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }, + }, + ``, + }, + { + `data.test_ds.moo`, + nil, + `Data source address not allowed: Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.`, + }, + { + `module.foo.data.test_ds.noo`, + nil, + `Data source address not allowed: Data sources are never destroyed, so they are not valid targets of removed blocks. To remove the data source from state, remove the data source block from configuration.`, + }, + { + `test_instance.foo[0]`, + nil, + `Resource instance keys not allowed: Resource address must be a resource (e.g. "test_instance.foo"), not a resource instance (e.g. "test_instance.foo[1]").`, + }, + { + `module.foo[0].test_instance.bar`, + nil, + `Module instance keys not allowed: Module address must be a module (e.g. "module.foo"), not a module instance (e.g. "module.foo[1]").`, + }, + { + `module.foo.test_instance.bar[0]`, + nil, + `Resource instance keys not allowed: Resource address must be a resource (e.g. "test_instance.foo"), not a resource instance (e.g. "test_instance.foo[1]").`, + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.InitialPos) + if hclDiags.HasErrors() { + // We're not trying to test the HCL parser here, so any + // failures at this point are likely to be bugs in the + // test case itself. + t.Fatalf("syntax error: %s", hclDiags.Error()) + } + + remT, diags := ParseRemoveTarget(traversal) + + switch { + case test.WantErr != "": + if !diags.HasErrors() { + t.Fatalf("unexpected success\nwant error: %s", test.WantErr) + } + gotErr := diags.Err().Error() + if gotErr != test.WantErr { + t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr) + } + default: + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err().Error()) + } + if diff := cmp.Diff(test.Want, remT.RelSubject); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + } + }) + } +} diff --git a/internal/addrs/removetargetkind_string.go b/internal/addrs/removetargetkind_string.go new file mode 100644 index 0000000000..a7beb70435 --- /dev/null +++ b/internal/addrs/removetargetkind_string.go @@ -0,0 +1,29 @@ +// Code generated by "stringer -type RemoveTargetKind"; DO NOT EDIT. + +package addrs + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[RemoveTargetModule-77] + _ = x[RemoveTargetResource-82] +} + +const ( + _RemoveTargetKind_name_0 = "RemoveTargetModule" + _RemoveTargetKind_name_1 = "RemoveTargetResource" +) + +func (i RemoveTargetKind) String() string { + switch { + case i == 77: + return _RemoveTargetKind_name_0 + case i == 82: + return _RemoveTargetKind_name_1 + default: + return "RemoveTargetKind(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/addrs/resource.go b/internal/addrs/resource.go index f400bcb421..2c5d4bca8b 100644 --- a/internal/addrs/resource.go +++ b/internal/addrs/resource.go @@ -1,8 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( + "encoding/hex" "fmt" + "math/rand" "strings" + "time" ) // Resource is an address for a resource block within configuration, which @@ -21,6 +27,8 @@ func (r Resource) String() string { return fmt.Sprintf("%s.%s", r.Type, r.Name) case DataResourceMode: return fmt.Sprintf("data.%s.%s", r.Type, r.Name) + case EphemeralResourceMode: + return fmt.Sprintf("ephemeral.%s.%s", r.Type, r.Name) default: // Should never happen, but we'll return a string here rather than // crashing just in case it does. @@ -32,6 +40,22 @@ func (r Resource) Equal(o Resource) bool { return r.Mode == o.Mode && r.Name == o.Name && r.Type == o.Type } +func (r Resource) Less(o Resource) bool { + switch { + case r.Mode != o.Mode: + return r.Mode == DataResourceMode + + case r.Type != o.Type: + return r.Type < o.Type + + case r.Name != o.Name: + return r.Name < o.Name + + default: + return false + } +} + func (r Resource) UniqueKey() UniqueKey { return r // A Resource is its own UniqueKey } @@ -100,6 +124,18 @@ func (r ResourceInstance) Equal(o ResourceInstance) bool { return r.Key == o.Key && r.Resource.Equal(o.Resource) } +func (r ResourceInstance) Less(o ResourceInstance) bool { + if !r.Resource.Equal(o.Resource) { + return r.Resource.Less(o.Resource) + } + + if r.Key != o.Key { + return InstanceKeyLess(r.Key, o.Key) + } + + return false +} + func (r ResourceInstance) UniqueKey() UniqueKey { return r // A ResourceInstance is its own UniqueKey } @@ -195,6 +231,18 @@ func (r AbsResource) Equal(o AbsResource) bool { return r.Module.Equal(o.Module) && r.Resource.Equal(o.Resource) } +func (r AbsResource) Less(o AbsResource) bool { + if !r.Module.Equal(o.Module) { + return r.Module.Less(o.Module) + } + + if !r.Resource.Equal(o.Resource) { + return r.Resource.Less(o.Resource) + } + + return false +} + func (r AbsResource) absMoveableSigil() { // AbsResource is moveable } @@ -249,6 +297,29 @@ func (r AbsResourceInstance) ConfigResource() ConfigResource { } } +// CurrentObject returns the address of the resource instance's "current" +// object, which is the one used for expression evaluation etc. +func (r AbsResourceInstance) CurrentObject() AbsResourceInstanceObject { + return AbsResourceInstanceObject{ + ResourceInstance: r, + DeposedKey: NotDeposed, + } +} + +// DeposedObject returns the address of a "deposed" object for the receiving +// resource instance, which appears only if a create-before-destroy replacement +// succeeds the create step but fails the destroy step, making the original +// object live on as a desposed object. +// +// If the given [DeposedKey] is [NotDeposed] then this is equivalent to +// [AbsResourceInstance.CurrentObject]. +func (r AbsResourceInstance) DeposedObject(key DeposedKey) AbsResourceInstanceObject { + return AbsResourceInstanceObject{ + ResourceInstance: r, + DeposedKey: key, + } +} + // TargetContains implements Targetable by returning true if the given other // address is equal to the receiver. func (r AbsResourceInstance) TargetContains(other Targetable) bool { @@ -289,8 +360,8 @@ func (r AbsResourceInstance) AffectedAbsResource() AbsResource { } } -func (r AbsResourceInstance) Check(t CheckType, i int) Check { - return Check{ +func (r AbsResourceInstance) CheckRule(t CheckRuleType, i int) CheckRule { + return CheckRule{ Container: r, Type: t, Index: i, @@ -308,30 +379,15 @@ func (r AbsResourceInstance) Equal(o AbsResourceInstance) bool { // Less returns true if the receiver should sort before the given other value // in a sorted list of addresses. func (r AbsResourceInstance) Less(o AbsResourceInstance) bool { - switch { - - case len(r.Module) != len(o.Module): - return len(r.Module) < len(o.Module) - - case r.Module.String() != o.Module.String(): + if !r.Module.Equal(o.Module) { return r.Module.Less(o.Module) - - case r.Resource.Resource.Mode != o.Resource.Resource.Mode: - return r.Resource.Resource.Mode == DataResourceMode - - case r.Resource.Resource.Type != o.Resource.Resource.Type: - return r.Resource.Resource.Type < o.Resource.Resource.Type - - case r.Resource.Resource.Name != o.Resource.Resource.Name: - return r.Resource.Resource.Name < o.Resource.Resource.Name - - case r.Resource.Key != o.Resource.Key: - return InstanceKeyLess(r.Resource.Key, o.Resource.Key) - - default: - return false - } + + if !r.Resource.Equal(o.Resource) { + return r.Resource.Less(o.Resource) + } + + return false } // AbsResourceInstance is a Checkable @@ -437,7 +493,7 @@ func (k configResourceKey) uniqueKeySigil() {} // resource lifecycle has a slightly different address format. type ResourceMode rune -//go:generate go run golang.org/x/tools/cmd/stringer -type ResourceMode +//go:generate go tool golang.org/x/tools/cmd/stringer -type ResourceMode const ( // InvalidResourceMode is the zero value of ResourceMode and is not @@ -451,4 +507,127 @@ const ( // DataResourceMode indicates a data resource, as defined by // "data" blocks in configuration. DataResourceMode ResourceMode = 'D' + + // EphemeralResourceMode indicates an ephemeral resource, as defined by + // "ephemeral" blocks in configuration. + EphemeralResourceMode ResourceMode = 'E' ) + +// AbsResourceInstanceObject represents one of the specific remote objects +// associated with a resource instance. +// +// When DeposedKey is [NotDeposed], this represents the "current" object. +// Otherwise, this represents a deposed object with the given key. +// +// The distinction between "current" and "deposed" objects is a planning and +// state concern that isn't reflected directly in configuration, so there +// are no "ConfigResourceInstanceObject" or "ResourceInstanceObject" address +// types. +type AbsResourceInstanceObject struct { + ResourceInstance AbsResourceInstance + DeposedKey DeposedKey +} + +// String returns a string that could be used to refer to this object +// in the UI, but is not necessarily suitable for use as a unique key. +func (o AbsResourceInstanceObject) String() string { + if o.DeposedKey != NotDeposed { + return fmt.Sprintf("%s deposed object %s", o.ResourceInstance, o.DeposedKey) + } + return o.ResourceInstance.String() +} + +// IsCurrent returns true only if this address is for a "current" object. +func (o AbsResourceInstanceObject) IsCurrent() bool { + return o.DeposedKey == NotDeposed +} + +// IsCurrent returns true only if this address is for a "deposed" object. +func (o AbsResourceInstanceObject) IsDeposed() bool { + return o.DeposedKey != NotDeposed +} + +// UniqueKey implements [UniqueKeyer] +func (o AbsResourceInstanceObject) UniqueKey() UniqueKey { + return absResourceInstanceObjectKey{ + resourceInstanceKey: o.ResourceInstance.UniqueKey(), + deposedKey: o.DeposedKey, + } +} + +// Less describes the "natural order" of resource instance object addresses. +// +// Objects that differ in the resource instance address sort in the natural +// order of AbsResourceInstance. Objects belonging to the same resource +// instance sort by deposed key, with non-deposed ("current") objects sorting +// first. +func (o AbsResourceInstanceObject) Less(other AbsResourceInstanceObject) bool { + switch { + case !o.ResourceInstance.Equal(other.ResourceInstance): + return o.ResourceInstance.Less(other.ResourceInstance) + default: + return o.DeposedKey < other.DeposedKey + } +} + +type absResourceInstanceObjectKey struct { + resourceInstanceKey UniqueKey + deposedKey DeposedKey +} + +func (absResourceInstanceObjectKey) uniqueKeySigil() {} + +// DeposedKey is a 8-character hex string used to uniquely identify deposed +// instance objects in the state. +// +// The zero value of this type is [NotDeposed] and represents a "current" +// object, not deposed at all. All other valid values of this type are strings +// containing exactly eight lowercase hex characters. +type DeposedKey string + +// NotDeposed is a special invalid value of DeposedKey that is used to represent +// the absense of a deposed key, typically when referring to the "current" object +// for a particular resource instance. It must not be used as an actual deposed +// key. +const NotDeposed = DeposedKey("") + +var deposedKeyRand = rand.New(rand.NewSource(time.Now().UnixNano())) + +// NewDeposedKey generates a pseudo-random deposed key. Because of the short +// length of these keys, uniqueness is not a natural consequence and so the +// caller should test to see if the generated key is already in use and generate +// another if so, until a unique key is found. +func NewDeposedKey() DeposedKey { + v := deposedKeyRand.Uint32() + return DeposedKey(fmt.Sprintf("%08x", v)) +} + +// ParseDeposedKey parses a string that is expected to be a deposed key, +// returning an error if it doesn't conform to the expected syntax. +func ParseDeposedKey(raw string) (DeposedKey, error) { + if len(raw) != 8 { + return "00000000", fmt.Errorf("must be eight hexadecimal digits") + } + if raw != strings.ToLower(raw) { + return "00000000", fmt.Errorf("must use lowercase hex digits") + } + _, err := hex.DecodeString(raw) + if err != nil { + return "00000000", fmt.Errorf("must be eight hexadecimal digits") + } + return DeposedKey(raw), nil +} + +func (k DeposedKey) String() string { + return string(k) +} + +func (k DeposedKey) GoString() string { + ks := string(k) + switch { + case ks == "": + return "states.NotDeposed" + default: + return fmt.Sprintf("states.DeposedKey(%q)", ks) + } +} diff --git a/internal/addrs/resource_phase.go b/internal/addrs/resource_phase.go index c62a7fc836..9977549179 100644 --- a/internal/addrs/resource_phase.go +++ b/internal/addrs/resource_phase.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import "fmt" diff --git a/internal/addrs/resource_test.go b/internal/addrs/resource_test.go index d68d2b5d4a..e5f6559bbf 100644 --- a/internal/addrs/resource_test.go +++ b/internal/addrs/resource_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/resourcemode_string.go b/internal/addrs/resourcemode_string.go index 0b5c33f8ee..a2b727a9b9 100644 --- a/internal/addrs/resourcemode_string.go +++ b/internal/addrs/resourcemode_string.go @@ -11,20 +11,26 @@ func _() { _ = x[InvalidResourceMode-0] _ = x[ManagedResourceMode-77] _ = x[DataResourceMode-68] + _ = x[EphemeralResourceMode-69] } const ( _ResourceMode_name_0 = "InvalidResourceMode" - _ResourceMode_name_1 = "DataResourceMode" + _ResourceMode_name_1 = "DataResourceModeEphemeralResourceMode" _ResourceMode_name_2 = "ManagedResourceMode" ) +var ( + _ResourceMode_index_1 = [...]uint8{0, 16, 37} +) + func (i ResourceMode) String() string { switch { case i == 0: return _ResourceMode_name_0 - case i == 68: - return _ResourceMode_name_1 + case 68 <= i && i <= 69: + i -= 68 + return _ResourceMode_name_1[_ResourceMode_index_1[i]:_ResourceMode_index_1[i+1]] case i == 77: return _ResourceMode_name_2 default: diff --git a/internal/addrs/run.go b/internal/addrs/run.go new file mode 100644 index 0000000000..92f7d86b16 --- /dev/null +++ b/internal/addrs/run.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import "fmt" + +// Run is the address of a run block within a testing file. +// +// Run blocks are only accessible from within the same testing file, and they +// do not support any meta-arguments like "count" or "for_each". So this address +// uniquely describes a run block from within a single testing file. +type Run struct { + referenceable + Name string +} + +func (r Run) String() string { + return fmt.Sprintf("run.%s", r.Name) +} + +func (r Run) Equal(run Run) bool { + return r.Name == run.Name +} + +func (r Run) UniqueKey() UniqueKey { + return r // A Run is its own UniqueKey +} + +func (r Run) uniqueKeySigil() {} diff --git a/internal/addrs/self.go b/internal/addrs/self.go index 64c8f6ecf3..abdccd250b 100644 --- a/internal/addrs/self.go +++ b/internal/addrs/self.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // Self is the address of the special object "self" that behaves as an alias diff --git a/internal/addrs/set.go b/internal/addrs/set.go index 70e9b4aaa0..193f09a3ff 100644 --- a/internal/addrs/set.go +++ b/internal/addrs/set.go @@ -1,5 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs +import ( + "sort" +) + // Set represents a set of addresses of types that implement UniqueKeyer. // // Modify the set only by the methods on this type. This type exposes its @@ -7,6 +14,12 @@ package addrs // by ranging over the map values, but making direct modifications could // potentially make the set data invalid or inconsistent, leading to undefined // behavior elsewhere. +// +// This implementation of Set is specific to our [UniqueKey] and [UniqueKeyer] +// convention here in package addrs, which predated Go supporting type +// parameters. For types outside of addrs consider using the generalized version +// in sibling package "collections". Perhaps one day we'll rework this +// addrs-specific implementation to use [collections.Set] instead. type Set[T UniqueKeyer] map[UniqueKey]T func MakeSet[T UniqueKeyer](elems ...T) Set[T] { @@ -65,3 +78,36 @@ func (s Set[T]) Intersection(other Set[T]) Set[T] { } return ret } + +// Sorted returns a slice of all of the elements of the receiving set, sorted +// into an order defined by the given callback function. +// +// The callback should return true if the first element should sort before +// the second, or false otherwise. +func (s Set[T]) Sorted(less func(i, j T) bool) []T { + if len(s) == 0 { + return nil + } + ret := make([]T, 0, len(s)) + for _, elem := range s { + ret = append(ret, elem) + } + sort.Slice(ret, func(i, j int) bool { + return less(ret[i], ret[j]) + }) + return ret +} + +// SetSortedNatural returns a slice containing the elements of the given set +// sorted into their "natural" order, as defined by the type's method "Less". +// +// For element types that don't have a natural order, or to sort by something +// other than the natural order, use [Set.Sorted] instead. +func SetSortedNatural[T interface { + UniqueKeyer + Less(T) bool +}](set Set[T]) []T { + return set.Sorted(func(i, j T) bool { + return i.Less(j) + }) +} diff --git a/internal/addrs/set_test.go b/internal/addrs/set_test.go new file mode 100644 index 0000000000..aa4574688e --- /dev/null +++ b/internal/addrs/set_test.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package addrs + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestSetSortedNatural(t *testing.T) { + // We're using AbsResourceInstance here just because it happens to + // implement the required Less method, but this is intended as a test + // of SetSortedNatural itself, not of any particular type's Less + // implementation. + boop1 := AbsResourceInstance{ + Module: RootModuleInstance, + Resource: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test", + Name: "boop1", + }, + Key: NoKey, + }, + } + boop2 := AbsResourceInstance{ + Module: RootModuleInstance, + Resource: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test", + Name: "boop2", + }, + Key: NoKey, + }, + } + boop3 := AbsResourceInstance{ + Module: RootModuleInstance, + Resource: ResourceInstance{ + Resource: Resource{ + Mode: ManagedResourceMode, + Type: "test", + Name: "boop3", + }, + Key: NoKey, + }, + } + s := MakeSet( + boop3, + boop2, + boop1, + ) + + got := SetSortedNatural(s) + want := []AbsResourceInstance{ + boop1, + boop2, + boop3, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} diff --git a/internal/addrs/target_test.go b/internal/addrs/target_test.go index 69792c9744..2149a96cbc 100644 --- a/internal/addrs/target_test.go +++ b/internal/addrs/target_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/addrs/targetable.go b/internal/addrs/targetable.go index 1aa3ef1ff7..d179d8129e 100644 --- a/internal/addrs/targetable.go +++ b/internal/addrs/targetable.go @@ -1,8 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // Targetable is an interface implemented by all address types that can be // used as "targets" for selecting sub-graphs of a graph. type Targetable interface { + UniqueKeyer + targetableSigil() // TargetContains returns true if the receiver is considered to contain diff --git a/internal/addrs/terraform_attr.go b/internal/addrs/terraform_attr.go index d3d11677c6..3296c89857 100644 --- a/internal/addrs/terraform_attr.go +++ b/internal/addrs/terraform_attr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // TerraformAttr is the address of an attribute of the "terraform" object in diff --git a/internal/addrs/unique_key.go b/internal/addrs/unique_key.go index f57e884f7b..b8c706e475 100644 --- a/internal/addrs/unique_key.go +++ b/internal/addrs/unique_key.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs // UniqueKey is an interface implemented by values that serve as unique map @@ -22,6 +25,6 @@ type UniqueKeyer interface { UniqueKey() UniqueKey } -func Equivalent[T UniqueKeyer](a, b T) bool { +func Equivalent[T, U UniqueKeyer](a T, b U) bool { return a.UniqueKey() == b.UniqueKey() } diff --git a/internal/addrs/unique_key_test.go b/internal/addrs/unique_key_test.go index 0926a0c377..5a636ecd95 100644 --- a/internal/addrs/unique_key_test.go +++ b/internal/addrs/unique_key_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package addrs import ( diff --git a/internal/backend/backend.go b/internal/backend/backend.go index 4124b2abdb..747d2c9a5c 100644 --- a/internal/backend/backend.go +++ b/internal/backend/backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package backend provides interfaces that the CLI uses to interact with // Terraform. A backend provides the abstraction that allows the same CLI // to simultaneously support both local and remote operations for seamlessly @@ -5,27 +8,13 @@ package backend import ( - "context" "errors" - "io/ioutil" - "log" - "os" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/command/clistate" - "github.com/hashicorp/terraform/internal/command/views" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configload" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/depsfile" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/plans/planfile" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/hashicorp/terraform/internal/terraform" - "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" ) // DefaultStateName is the name of the default, initial state that every @@ -109,314 +98,9 @@ type Backend interface { // DeleteWorkspace cannot prevent deleting a state that is in use. It is // the responsibility of the caller to hold a Lock for the state manager // belonging to this workspace before calling this method. - DeleteWorkspace(name string) error + DeleteWorkspace(name string, force bool) error // States returns a list of the names of all of the workspaces that exist // in this backend. Workspaces() ([]string, error) } - -// Enhanced implements additional behavior on top of a normal backend. -// -// 'Enhanced' backends are an implementation detail only, and are no longer reflected as an external -// 'feature' of backends. In other words, backends refer to plugins for remote state snapshot -// storage only, and the Enhanced interface here is a necessary vestige of the 'local' and -// remote/cloud backends only. -type Enhanced interface { - Backend - - // Operation performs a Terraform operation such as refresh, plan, apply. - // It is up to the implementation to determine what "performing" means. - // This DOES NOT BLOCK. The context returned as part of RunningOperation - // should be used to block for completion. - // If the state used in the operation can be locked, it is the - // responsibility of the Backend to lock the state for the duration of the - // running operation. - Operation(context.Context, *Operation) (*RunningOperation, error) -} - -// Local implements additional behavior on a Backend that allows local -// operations in addition to remote operations. -// -// This enables more behaviors of Terraform that require more data such -// as `console`, `import`, `graph`. These require direct access to -// configurations, variables, and more. Not all backends may support this -// so we separate it out into its own optional interface. -type Local interface { - // LocalRun uses information in the Operation to prepare a set of objects - // needed to start running that operation. - // - // The operation doesn't need a Type set, but it needs various other - // options set. This is a rather odd API that tries to treat all - // operations as the same when they really aren't; see the local and remote - // backend's implementations of this to understand what this actually - // does, because this operation has no well-defined contract aside from - // "whatever it already does". - LocalRun(*Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics) -} - -// LocalRun represents the assortment of objects that we can collect or -// calculate from an Operation object, which we can then use for local -// operations. -// -// The operation methods on terraform.Context (Plan, Apply, Import, etc) each -// generate new artifacts which supersede parts of the LocalRun object that -// started the operation, so callers should be careful to use those subsequent -// artifacts instead of the fields of LocalRun where appropriate. The LocalRun -// data intentionally doesn't update as a result of calling methods on Context, -// in order to make data flow explicit. -// -// This type is a weird architectural wart resulting from the overly-general -// way our backend API models operations, whereby we behave as if all -// Terraform operations have the same inputs and outputs even though they -// are actually all rather different. The exact meaning of the fields in -// this type therefore vary depending on which OperationType was passed to -// Local.Context in order to create an object of this type. -type LocalRun struct { - // Core is an already-initialized Terraform Core context, ready to be - // used to run operations such as Plan and Apply. - Core *terraform.Context - - // Config is the configuration we're working with, which typically comes - // from either config files directly on local disk (when we're creating - // a plan, or similar) or from a snapshot embedded in a plan file - // (when we're applying a saved plan). - Config *configs.Config - - // InputState is the state that should be used for whatever is the first - // method call to a context created with CoreOpts. When creating a plan - // this will be the previous run state, but when applying a saved plan - // this will be the prior state recorded in that plan. - InputState *states.State - - // PlanOpts are options to pass to a Plan or Plan-like operation. - // - // This is nil when we're applying a saved plan, because the plan itself - // contains enough information about its options to apply it. - PlanOpts *terraform.PlanOpts - - // Plan is a plan loaded from a saved plan file, if our operation is to - // apply that saved plan. - // - // This is nil when we're not applying a saved plan. - Plan *plans.Plan -} - -// An operation represents an operation for Terraform to execute. -// -// Note that not all fields are supported by all backends and can result -// in an error if set. All backend implementations should show user-friendly -// errors explaining any incorrectly set values. For example, the local -// backend doesn't support a PlanId being set. -// -// The operation options are purposely designed to have maximal compatibility -// between Terraform and Terraform Servers (a commercial product offered by -// HashiCorp). Therefore, it isn't expected that other implementation support -// every possible option. The struct here is generalized in order to allow -// even partial implementations to exist in the open, without walling off -// remote functionality 100% behind a commercial wall. Anyone can implement -// against this interface and have Terraform interact with it just as it -// would with HashiCorp-provided Terraform Servers. -type Operation struct { - // Type is the operation to perform. - Type OperationType - - // PlanId is an opaque value that backends can use to execute a specific - // plan for an apply operation. - // - // PlanOutBackend is the backend to store with the plan. This is the - // backend that will be used when applying the plan. - PlanId string - PlanRefresh bool // PlanRefresh will do a refresh before a plan - PlanOutPath string // PlanOutPath is the path to save the plan - PlanOutBackend *plans.Backend - - // ConfigDir is the path to the directory containing the configuration's - // root module. - ConfigDir string - - // ConfigLoader is a configuration loader that can be used to load - // configuration from ConfigDir. - ConfigLoader *configload.Loader - - // DependencyLocks represents the locked dependencies associated with - // the configuration directory given in ConfigDir. - // - // Note that if field PlanFile is set then the plan file should contain - // its own dependency locks. The backend is responsible for correctly - // selecting between these two sets of locks depending on whether it - // will be using ConfigDir or PlanFile to get the configuration for - // this operation. - DependencyLocks *depsfile.Locks - - // Hooks can be used to perform actions triggered by various events during - // the operation's lifecycle. - Hooks []terraform.Hook - - // Plan is a plan that was passed as an argument. This is valid for - // plan and apply arguments but may not work for all backends. - PlanFile *planfile.Reader - - // The options below are more self-explanatory and affect the runtime - // behavior of the operation. - PlanMode plans.Mode - AutoApprove bool - Targets []addrs.Targetable - ForceReplace []addrs.AbsResourceInstance - Variables map[string]UnparsedVariableValue - - // Some operations use root module variables only opportunistically or - // don't need them at all. If this flag is set, the backend must treat - // all variables as optional and provide an unknown value for any required - // variables that aren't set in order to allow partial evaluation against - // the resulting incomplete context. - // - // This flag is honored only if PlanFile isn't set. If PlanFile is set then - // the variables set in the plan are used instead, and they must be valid. - AllowUnsetVariables bool - - // View implements the logic for all UI interactions. - View views.Operation - - // Input/output/control options. - UIIn terraform.UIInput - UIOut terraform.UIOutput - - // StateLocker is used to lock the state while providing UI feedback to the - // user. This will be replaced by the Backend to update the context. - // - // If state locking is not necessary, this should be set to a no-op - // implementation of clistate.Locker. - StateLocker clistate.Locker - - // Workspace is the name of the workspace that this operation should run - // in, which controls which named state is used. - Workspace string -} - -// HasConfig returns true if and only if the operation has a ConfigDir value -// that refers to a directory containing at least one Terraform configuration -// file. -func (o *Operation) HasConfig() bool { - return o.ConfigLoader.IsConfigDir(o.ConfigDir) -} - -// Config loads the configuration that the operation applies to, using the -// ConfigDir and ConfigLoader fields within the receiving operation. -func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir) - diags = diags.Append(hclDiags) - return config, diags -} - -// ReportResult is a helper for the common chore of setting the status of -// a running operation and showing any diagnostics produced during that -// operation. -// -// If the given diagnostics contains errors then the operation's result -// will be set to backend.OperationFailure. It will be set to -// backend.OperationSuccess otherwise. It will then use o.View.Diagnostics -// to show the given diagnostics before returning. -// -// Callers should feel free to do each of these operations separately in -// more complex cases where e.g. diagnostics are interleaved with other -// output, but terminating immediately after reporting error diagnostics is -// common and can be expressed concisely via this method. -func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics) { - if diags.HasErrors() { - op.Result = OperationFailure - } else { - op.Result = OperationSuccess - } - if o.View != nil { - o.View.Diagnostics(diags) - } else { - // Shouldn't generally happen, but if it does then we'll at least - // make some noise in the logs to help us spot it. - if len(diags) != 0 { - log.Printf( - "[ERROR] Backend needs to report diagnostics but View is not set:\n%s", - diags.ErrWithWarnings(), - ) - } - } -} - -// RunningOperation is the result of starting an operation. -type RunningOperation struct { - // For implementers of a backend, this context should not wrap the - // passed in context. Otherwise, cancelling the parent context will - // immediately mark this context as "done" but those aren't the semantics - // we want: we want this context to be done only when the operation itself - // is fully done. - context.Context - - // Stop requests the operation to complete early, by calling Stop on all - // the plugins. If the process needs to terminate immediately, call Cancel. - Stop context.CancelFunc - - // Cancel is the context.CancelFunc associated with the embedded context, - // and can be called to terminate the operation early. - // Once Cancel is called, the operation should return as soon as possible - // to avoid running operations during process exit. - Cancel context.CancelFunc - - // Result is the exit status of the operation, populated only after the - // operation has completed. - Result OperationResult - - // PlanEmpty is populated after a Plan operation completes without error - // to note whether a plan is empty or has changes. - PlanEmpty bool - - // State is the final state after the operation completed. Persisting - // this state is managed by the backend. This should only be read - // after the operation completes to avoid read/write races. - State *states.State -} - -// OperationResult describes the result status of an operation. -type OperationResult int - -const ( - // OperationSuccess indicates that the operation completed as expected. - OperationSuccess OperationResult = 0 - - // OperationFailure indicates that the operation encountered some sort - // of error, and thus may have been only partially performed or not - // performed at all. - OperationFailure OperationResult = 1 -) - -func (r OperationResult) ExitStatus() int { - return int(r) -} - -// If the argument is a path, Read loads it and returns the contents, -// otherwise the argument is assumed to be the desired contents and is simply -// returned. -func ReadPathOrContents(poc string) (string, error) { - if len(poc) == 0 { - return poc, nil - } - - path := poc - if path[0] == '~' { - var err error - path, err = homedir.Expand(path) - if err != nil { - return path, err - } - } - - if _, err := os.Stat(path); err == nil { - contents, err := ioutil.ReadFile(path) - if err != nil { - return string(contents), err - } - return string(contents), nil - } - - return poc, nil -} diff --git a/internal/backend/backendbase/base.go b/internal/backend/backendbase/base.go new file mode 100644 index 0000000000..4dcce8db9c --- /dev/null +++ b/internal/backend/backendbase/base.go @@ -0,0 +1,123 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendbase + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Base is a partial implementation of [backend.Backend] that can be embedded +// into another implementer to handle most of the configuration schema +// wrangling. +// +// Specifically it implements the ConfigSchema and PrepareConfig methods. +// Implementers embedding this base type must still implement all of the other +// Backend methods. +type Base struct { + // Schema is the schema for the backend configuration. + // + // This shares the same configuration schema model as used in provider + // schemas for resource types, etc, but only a subset of the model is + // actually meaningful for backends. In particular, it doesn't make sense + // to define "computed" attributes because objects conforming to the + // schema are only use for input based on the configuration, and can't + // export any data for use elsewhere in the configuration. + Schema *configschema.Block + + // SDKLikeDefaults is an optional addition for backends that were built + // to rely heavily on the legacy SDK's support for default values, both + // hard-coded and taken from environment variables. + // + // If this is non-empty then any key specified here must match a + // primitive-typed toplevel attribute in Schema, and PrepareConfig will + // arrange for the default values to be inserted before it returns. + // + // As a special case, if the value in the configuration is unset (null), + // none of the environment variables are non-empty, and the fallback + // value is empty, then the attribute value will be left as null in the + // object returned by PrepareConfig. In all other situations an attribute + // specified here is definitely not null. + SDKLikeDefaults SDKLikeDefaults +} + +// ConfigSchema returns the configuration schema for the backend. +func (b Base) ConfigSchema() *configschema.Block { + return b.Schema +} + +// PrepareConfig coerces the given value to the backend's schema if possible, +// and emits deprecation warnings if any deprecated arguments have values +// assigned to them. +func (b Base) PrepareConfig(configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if configVal.IsNull() { + // We expect the backend configuration to be an object, so if it's + // null for some reason (e.g. because of an interrupt), we'll turn + // it into an empty object so that we can still coerce it + configVal = cty.EmptyObjectVal + } + + schema := b.Schema + + v, err := schema.CoerceValue(configVal) + if err != nil { + var path cty.Path + if err, ok := err.(cty.PathError); ok { + path = err.Path + } + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid backend configuration", + fmt.Sprintf("The backend configuration is incorrect: %s.", tfdiags.FormatError(err)), + path, + )) + return cty.DynamicVal, diags + } + + cty.Walk(v, func(path cty.Path, v cty.Value) (bool, error) { + if v.IsNull() { + // Null values for deprecated arguments do not generate deprecation + // warnings, because that represents the argument not being set. + return false, nil + } + + // If this path refers to a schema attribute then it might be + // deprecated, in which case we need to return a warning. + attr := schema.AttributeByPath(path) + if attr == nil { + return true, nil + } + if attr.Deprecated { + // The configschema model only has a boolean flag for whether the + // argument is deprecated or not, so this warning message is + // generic. Backends that want to return a custom message should + // leave this flag unset and instead implement a check inside + // their Configure method that returns a warning diagnostic. + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Warning, + "Deprecated provider argument", + fmt.Sprintf("The argument %s is deprecated. Refer to the backend documentation for more information.", tfdiags.FormatCtyPath(path)), + path, + )) + } + + return false, nil + }) + + if len(b.SDKLikeDefaults) != 0 { + var err error + v, err = b.SDKLikeDefaults.ApplyTo(v) + if err != nil { + diags = diags.Append(err) + } + } + + return v, diags +} diff --git a/internal/backend/backendbase/base_test.go b/internal/backend/backendbase/base_test.go new file mode 100644 index 0000000000..f9f3ca0a2f --- /dev/null +++ b/internal/backend/backendbase/base_test.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendbase + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestBase_coerceError(t *testing.T) { + // This tests that we return errors if type coersion fails. + // This doesn't thoroughly test all cases because we're just delegating + // to the configschema package's coersion function, which is already + // tested in its own package. + + b := Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + }, + }, + }, + } + // This is a fake body just to give us something to correlate the + // diagnostic attribute paths against so we can test that the + // errors are properly annotated. In the real implementation + // the command package logic would evaluate the diagnostics against + // the real HCL body written by the end-user. + // + // Because we're using MockExprLiteral for the expressions here, + // the source range for each expression is just the fake filename + // "MockExprLiteral". If the PrepareConfig function fails to properly + // annotate its diagnostics then the source range won't be populated + // at all. + body := hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "foo": { + Expr: hcltest.MockExprLiteral(cty.StringVal("")), + }, + }, + }) + + t.Run("error", func(t *testing.T) { + _, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ + // This is incorrect because the schema wants a string + "foo": cty.MapValEmpty(cty.String), + })) + gotDiags := diags.InConfigBody(body, "") + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid backend configuration", + Detail: "The backend configuration is incorrect: .foo: string required.", + Subject: &hcl.Range{Filename: "MockExprLiteral"}, + }) + + tfdiags.AssertDiagnosticsMatch(t, gotDiags, wantDiags) + }) +} + +func TestBase_deprecatedArg(t *testing.T) { + b := Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "not_deprecated": { + Type: cty.String, + Optional: true, + }, + "deprecated": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "nested": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "deprecated": { + Type: cty.String, + Optional: true, + Deprecated: true, + }, + }, + }, + }, + }, + }, + } + // This is a fake body just to give us something to correlate the + // diagnostic attribute paths against so we can test that the + // warnings are properly annotated. In the real implementation + // the command package logic would evaluate the diagnostics against + // the real HCL body written by the end-user. + // + // Because we're using MockExprLiteral for the expressions here, + // the source range for each expression is just the fake filename + // "MockExprLiteral". If the PrepareConfig function fails to properly + // annotate its diagnostics then the source range won't be populated + // at all. + body := hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "deprecated": { + Expr: hcltest.MockExprLiteral(cty.StringVal("")), + }, + }, + Blocks: hcl.Blocks{ + { + Type: "nested", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "deprecated": { + Expr: hcltest.MockExprLiteral(cty.StringVal("")), + }, + }, + }), + }, + { + Type: "nested", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "deprecated": { + Expr: hcltest.MockExprLiteral(cty.StringVal("")), + }, + }, + }), + }, + }, + }) + + t.Run("nothing deprecated", func(t *testing.T) { + got, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ + "not_deprecated": cty.StringVal("hello"), + })) + if len(diags) != 0 { + t.Errorf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error()) + } + want := cty.ObjectVal(map[string]cty.Value{ + "deprecated": cty.NullVal(cty.String), + "not_deprecated": cty.StringVal("hello"), + "nested": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "deprecated": cty.String, + })), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + t.Run("toplevel deprecated", func(t *testing.T) { + _, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ + "deprecated": cty.StringVal("hello"), + })) + + gotDiags := diags.InConfigBody(body, "") + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated provider argument", + Detail: "The argument .deprecated is deprecated. Refer to the backend documentation for more information.", + Subject: &hcl.Range{Filename: "MockExprLiteral"}, + }) + tfdiags.AssertDiagnosticsMatch(t, wantDiags, gotDiags) + }) + t.Run("nested deprecated", func(t *testing.T) { + _, diags := b.PrepareConfig(cty.ObjectVal(map[string]cty.Value{ + "nested": cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "deprecated": cty.StringVal("hello"), + }), + cty.ObjectVal(map[string]cty.Value{ + "deprecated": cty.StringVal("hello"), + }), + }), + })) + gotDiags := diags.InConfigBody(body, "") + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated provider argument", + Detail: "The argument .nested[0].deprecated is deprecated. Refer to the backend documentation for more information.", + Subject: &hcl.Range{Filename: "MockExprLiteral"}, + }) + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Deprecated provider argument", + Detail: "The argument .nested[1].deprecated is deprecated. Refer to the backend documentation for more information.", + Subject: &hcl.Range{Filename: "MockExprLiteral"}, + }) + tfdiags.AssertDiagnosticsMatch(t, wantDiags, gotDiags) + }) +} + +func TestBase_nullCrash(t *testing.T) { + // This test ensures that we don't crash while applying defaults to + // a null value + + b := Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Required: true, + }, + }, + }, + SDKLikeDefaults: SDKLikeDefaults{ + "foo": { + Fallback: "fallback", + }, + }, + } + + t.Run("error", func(t *testing.T) { + // We pass an explicit null value here to simulate an interrupt + _, gotDiags := b.PrepareConfig(cty.NullVal(cty.Object(map[string]cty.Type{ + "foo": cty.String, + }))) + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid backend configuration", + Detail: "The backend configuration is incorrect: attribute \"foo\" is required.", + }) + if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } + }) +} diff --git a/internal/backend/backendbase/doc.go b/internal/backend/backendbase/doc.go new file mode 100644 index 0000000000..854afc73d7 --- /dev/null +++ b/internal/backend/backendbase/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package backendbase contains helpers for implementing the parts of remote +// state backends that tend to be treated similarly across all implementations. +package backendbase diff --git a/internal/backend/backendbase/helper.go b/internal/backend/backendbase/helper.go new file mode 100644 index 0000000000..9d4c944ccf --- /dev/null +++ b/internal/backend/backendbase/helper.go @@ -0,0 +1,161 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendbase + +import ( + "fmt" + "math/big" + "os" + + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" +) + +// GetPathDefault traverses the steps of the given path through the given +// value, and then returns either that value or the value given in def, +// if the found value was null. +// +// This function expects the given path to be valid for the given value, and +// will panic if not. This should be used only for values that have already +// been coerced into a known-good data type, which is typically achieved by +// passing the value that was returned by [Base.PrepareConfig], which is also +// the value passed to [Backend.Configure]. +func GetPathDefault(v cty.Value, path cty.Path, def cty.Value) cty.Value { + v, err := path.Apply(v) + if err != nil { + panic(fmt.Sprintf("invalid path: %s", tfdiags.FormatError(err))) + } + if v.IsNull() { + return def + } + return v +} + +// GetAttrDefault is like [GetPathDefault] but more convenient for the common +// case of looking up a single top-level attribute. +func GetAttrDefault(v cty.Value, attrName string, def cty.Value) cty.Value { + return GetPathDefault(v, cty.GetAttrPath(attrName), def) +} + +// GetPathEnvDefault is like [GetPathDefault] except that the default value +// is taken from an environment variable of the name given in defEnv, returned +// as a string value. +// +// If that environment variable is unset or has an empty-string value then +// the result is null, as a convenience to callers so that they don't need to +// handle both null-ness and empty-string-ness as variants of "unset". +// +// This function panics in the same situations as [GetPathDefault]. +func GetPathEnvDefault(v cty.Value, path cty.Path, defEnv string) cty.Value { + v, err := path.Apply(v) + if err != nil { + panic(fmt.Sprintf("invalid path: %s", tfdiags.FormatError(err))) + } + if v.IsNull() { + if defStr := os.Getenv(defEnv); defStr != "" { + return cty.StringVal(defStr) + } + } + return v +} + +// GetAttrEnvDefault is like [GetPathEnvDefault] but more convenient for the +// common case of looking up a single top-level attribute. +func GetAttrEnvDefault(v cty.Value, attrName string, defEnv string) cty.Value { + return GetPathEnvDefault(v, cty.GetAttrPath(attrName), defEnv) +} + +// GetPathEnvDefaultFallback is like [GetPathEnvDefault] except that if +// neither the attribute nor the environment variable are set then instead +// of returning null it will return the given fallback value. +// +// Unless the fallback value is null itself, this function guarantees to never +// return null. +func GetPathEnvDefaultFallback(v cty.Value, path cty.Path, defEnv string, fallback cty.Value) cty.Value { + ret := GetPathEnvDefault(v, path, defEnv) + if ret.IsNull() { + return fallback + } + return ret +} + +// GetAttrEnvDefaultFallback is like [GetPathEnvDefault] except that if +// neither the attribute nor the environment variable are set then instead +// of returning null it will return the given fallback value. +// +// Unless the fallback value is null itself, this function guarantees to never +// return null. +func GetAttrEnvDefaultFallback(v cty.Value, attrName string, defEnv string, fallback cty.Value) cty.Value { + ret := GetAttrEnvDefault(v, attrName, defEnv) + if ret.IsNull() { + return fallback + } + return ret +} + +// IntValue converts a cty value into a Go int64, or returns an error if that's +// not possible. +func IntValue(v cty.Value) (int64, error) { + v, err := convert.Convert(v, cty.Number) + if err != nil { + return 0, err + } + if v.IsNull() { + return 0, fmt.Errorf("must not be null") + } + bf := v.AsBigFloat() + ret, acc := bf.Int64() + if acc != big.Exact { + return 0, fmt.Errorf("must not be a whole number") + } + return ret, nil +} + +// BoolValue converts a cty value Go bool, or returns an error if that's not +// possible. +func BoolValue(v cty.Value) (bool, error) { + v, err := convert.Convert(v, cty.Bool) + if err != nil { + return false, err + } + if v.IsNull() { + return false, fmt.Errorf("must not be null") + } + return v.True(), nil +} + +// MustBoolValue converts a cty value Go bool, or panics if that's not possible. +func MustBoolValue(v cty.Value) bool { + ret, err := BoolValue(v) + if err != nil { + panic(fmt.Sprintf("MustBoolValue: %s", err)) + } + return ret +} + +// ErrorAsDiagnostics wraps a non-nil error as a tfdiags.Diagnostics. +// +// Panics if the given error is nil, since the caller should only be using +// this if they've encountered a non-nil error. +// +// This is here just as a temporary measure to preserve the old treatment of +// errors returned from legacy helper/schema-based backend implementations, +// so that we can minimize the churn caused in the first iteration of adopting +// backendbase. +// +// In the long run backends should produce higher-quality diagnostics directly +// themselves, but we wanted to first complete the deprecation of the +// legacy/helper/schema package with only mechanical code updates and then +// save diagnostic quality improvements for a later time. +func ErrorAsDiagnostics(err error) tfdiags.Diagnostics { + if err == nil { + panic("ErrorAsDiagnostics with nil error") + } + var diags tfdiags.Diagnostics + // This produces a very low-quality diagnostic message, but it matches + // how legacy helper/schema dealt with the same situation. + diags = diags.Append(err) + return diags +} diff --git a/internal/backend/backendbase/helper_test.go b/internal/backend/backendbase/helper_test.go new file mode 100644 index 0000000000..8be2ecb626 --- /dev/null +++ b/internal/backend/backendbase/helper_test.go @@ -0,0 +1,183 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendbase + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestGetPathDefault(t *testing.T) { + tests := map[string]struct { + Value cty.Value + Path cty.Path + Default cty.Value + Want cty.Value + }{ + // The test cases here don't aim to exhaustively test all possible + // cty.Path values, because we're just delegating to cty.Path.Apply + // and that's already tested upstream. + + "attribute is set": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + cty.GetAttrPath("a"), + cty.StringVal("default"), + cty.StringVal("a value"), + }, + "attribute is not set": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.String), + }), + cty.GetAttrPath("a"), + cty.StringVal("default"), + cty.StringVal("default"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := GetPathDefault(test.Value, test.Path, test.Default) + if !test.Want.RawEquals(got) { + t.Errorf( + "wrong result\nvalue: %#v\npath: %#v\ndefault: %#v\n\ngot: %#v\nwant: %#v", + test.Value, + test.Path, + test.Default, + got, + test.Want, + ) + } + }) + } +} + +func TestGetAttrDefault(t *testing.T) { + tests := map[string]struct { + Value cty.Value + Attr string + Default cty.Value + Want cty.Value + }{ + "attribute is set": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + "a", + cty.StringVal("default"), + cty.StringVal("a value"), + }, + "attribute is not set": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.String), + }), + "a", + cty.StringVal("default"), + cty.StringVal("default"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := GetAttrDefault(test.Value, test.Attr, test.Default) + if !test.Want.RawEquals(got) { + t.Errorf( + "wrong result\nvalue: %#v\nattr: %#v\ndefault: %#v\n\ngot: %#v\nwant: %#v", + test.Value, + test.Attr, + test.Default, + got, + test.Want, + ) + } + }) + } +} + +func TestGetPathEnvDefault(t *testing.T) { + // This one is actually testing both GetPathEnvDefault and GetAttrEnvDefault + // together, since they are both really just the same functionality exposed + // in two different ways. + + t.Setenv("DEFAULT_VALUE_SET", "default") + t.Setenv("DEFAULT_VALUE_EMPTY", "") + + tests := map[string]struct { + Value cty.Value + Attr string + EnvVar string + Want cty.Value + }{ + // The test cases here don't aim to exhaustively test all possible + // cty.Path values, because we're just delegating to cty.Path.Apply + // and that's already tested upstream. + + "attribute is set": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + "a", + "DEFAULT_VALUE_SET", + cty.StringVal("a value"), + }, + "attribute is not set, but environment is": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.String), + }), + "a", + "DEFAULT_VALUE_SET", + cty.StringVal("default"), + }, + "neither attribute or environment are set": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.String), + }), + "a", + "DEFAULT_VALUE_UNSET", + cty.NullVal(cty.String), + }, + "attribute is not set, and environment variable is empty": { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.NullVal(cty.String), + }), + "a", + "DEFAULT_VALUE_EMPTY", + cty.NullVal(cty.String), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + t.Run("by attr", func(t *testing.T) { + got := GetAttrEnvDefault(test.Value, test.Attr, test.EnvVar) + if !test.Want.RawEquals(got) { + t.Errorf( + "wrong result\nvalue: %#v\nattr: %#v\nvariable: %#v\n\ngot: %#v\nwant: %#v", + test.Value, + test.Attr, + test.EnvVar, + got, + test.Want, + ) + } + }) + t.Run("by path", func(t *testing.T) { + path := cty.GetAttrPath(test.Attr) + got := GetPathEnvDefault(test.Value, path, test.EnvVar) + if !test.Want.RawEquals(got) { + t.Errorf( + "wrong result\nvalue: %#v\npath: %#v\nvariable: %#v\n\ngot: %#v\nwant: %#v", + test.Value, + path, + test.EnvVar, + got, + test.Want, + ) + } + }) + }) + } +} diff --git a/internal/backend/backendbase/sdklike.go b/internal/backend/backendbase/sdklike.go new file mode 100644 index 0000000000..9d53cbf6b5 --- /dev/null +++ b/internal/backend/backendbase/sdklike.go @@ -0,0 +1,287 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendbase + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" +) + +// SDKLikeData offers an approximation of the legack SDK "ResourceData" API +// as a stopgap measure to help migrate all of the remote state backend +// implementations away from the legacy SDK. +// +// It's designed to wrap an object returned by [Base.PrepareConfig] which +// should therefore already have a fixed, known data type. Therefore the +// methods assume that the caller already knows what type each attribute +// should have and will panic if a caller asks for an incompatible type. +type SDKLikeData struct { + v cty.Value +} + +func NewSDKLikeData(v cty.Value) SDKLikeData { + return SDKLikeData{v} +} + +// String extracts a string attribute from a configuration object +// in a similar way to how the legacy SDK would interpret an attribute +// of type schema.TypeString, or panics if the wrapped object isn't of a +// suitable type. +func (d SDKLikeData) String(attrPath string) string { + v := d.GetAttr(attrPath, cty.String) + if v.IsNull() { + return "" + } + return v.AsString() +} + +// Int extracts a string attribute from a configuration object +// in a similar way to how the legacy SDK would interpret an attribute +// of type schema.TypeInt, or panics if the wrapped object isn't of a +// suitable type. +// +// Since the Terraform language does not have an integers-only type, this +// can fail dynamically (returning an error) if the given value has a +// fractional component. +func (d SDKLikeData) Int64(attrPath string) (int64, error) { + // Legacy SDK used strconv.ParseInt to interpret values, so we'll + // follow its lead here for maximal compatibility. + v := d.GetAttr(attrPath, cty.String) + if v.IsNull() { + return 0, nil + } + return strconv.ParseInt(v.AsString(), 0, 0) +} + +// Bool extracts a string attribute from a configuration object +// in a similar way to how the legacy SDK would interpret an attribute +// of type schema.TypeBool, or panics if the wrapped object isn't of a +// suitable type. +func (d SDKLikeData) Bool(attrPath string) bool { + // Legacy SDK used strconv.ParseBool to interpret values, but it + // did so only after the configuration was interpreted by HCL and + // thus HCL's more constrained definition of bool still "won", + // and we follow that tradition here. + v := d.GetAttr(attrPath, cty.Bool) + if v.IsNull() { + return false + } + return v.True() +} + +// GetAttr is just a thin wrapper around [cty.Path.Apply] that accepts +// a legacy-SDK-like dot-separated string as attribute path, instead of +// a [cty.Path] directly. +// +// It uses [SDKLikePath] to interpret the given path, and so the limitations +// of that function apply equally to this function. +// +// This function will panic if asked to extract a path that isn't compatible +// with the object type of the enclosed value. +func (d SDKLikeData) GetAttr(attrPath string, wantType cty.Type) cty.Value { + path := SDKLikePath(attrPath) + v, err := path.Apply(d.v) + if err != nil { + panic("invalid attribute path: " + err.Error()) + } + v, err = convert.Convert(v, wantType) + if err != nil { + panic("incorrect attribute type: " + err.Error()) + } + return v +} + +// SDKLikePath interprets a subset of the legacy SDK attribute path syntax -- +// identifiers separated by dots -- into a cty.Path. +// +// This is designed only for migrating historical remote system backends that +// were originally written using the SDK, and so it's limited only to the +// simple cases they use. It's not suitable for the more complex legacy SDK +// uses made by Terraform providers. +func SDKLikePath(rawPath string) cty.Path { + var ret cty.Path + remain := rawPath + for { + dot := strings.IndexByte(remain, '.') + last := false + if dot == -1 { + dot = len(remain) + last = true + } + + attrName := remain[:dot] + ret = append(ret, cty.GetAttrStep{Name: attrName}) + if last { + return ret + } + remain = remain[dot+1:] + } +} + +// SDKLikeEnvDefault emulates an SDK-style "EnvDefaultFunc" by taking the +// result of [SDKLikeData.String] and a series of environment variable names. +// +// If the given string is already non-empty then it just returns it directly. +// Otherwise it returns the value of the first environment variable that has +// a non-empty value. If everything turns out empty, the result is an empty +// string. +func SDKLikeEnvDefault(v string, envNames ...string) string { + if v == "" { + for _, envName := range envNames { + v = os.Getenv(envName) + if v != "" { + return v + } + } + } + return v +} + +// SDKLikeRequiredWithEnvDefault is a convenience wrapper around +// [SDKLikeEnvDefault] which returns an error if the result is still the +// empty string even after trying all of the fallback environment variables. +// +// This wrapper requires an additional argument specifying the attribute name +// just because that becomes part of the returned error message. +func SDKLikeRequiredWithEnvDefault(attrPath string, v string, envNames ...string) (string, error) { + ret := SDKLikeEnvDefault(v, envNames...) + if ret == "" { + return "", fmt.Errorf("attribute %q is required", attrPath) + } + return ret, nil +} + +// SDKLikeDefaults captures legacy-SDK-like default values to help fill the +// gap in abstraction level between the legacy SDK and Terraform's own +// configuration schema model. +type SDKLikeDefaults map[string]SDKLikeDefault + +type SDKLikeDefault struct { + EnvVars []string + Fallback string + + // Required is for situations where an argument is optional to set + // in the configuration but _must_ eventually be set through the + // combination of the configuration and the environment variables + // in this object. + // + // It doesn't make sense to set Fallback non-empty when this flag is + // set, because an attribute with a non-empty fallback is always + // effectively present. + Required bool +} + +// ApplyTo is a convenience helper that allows inserting default +// values from environment variables into many different string attributes of +// an object value all at once, approximating what the legacy SDK would've +// done when the schema included an "EnvDefaultFunc". +// +// Like all of the "SDK-like" helpers. this expects that the base object has +// already been coerced into the correct type for a backend's schema and +// so this will panic if any of the keys in envVars do not match existing +// attributes in base, and if the value in any of those attributes is not +// of a cty primitive type. +func (d SDKLikeDefaults) ApplyTo(base cty.Value) (cty.Value, error) { + attrTypes := base.Type().AttributeTypes() + retAttrs := make(map[string]cty.Value, len(attrTypes)) + for attrName, ty := range attrTypes { + defs, hasDefs := d[attrName] + givenVal := base.GetAttr(attrName) + if !hasDefs { + // Just pass through verbatim any attributes that are not + // accounted for in our defaults. + retAttrs[attrName] = givenVal + continue + } + + // The legacy SDK shims convert all values into strings (for flatmap) + // and then do their work in terms of that, so we'll follow suit here. + vStr, err := convert.Convert(givenVal, cty.String) + if err != nil { + panic("cannot apply environment variable defaults for " + ty.GoString()) + } + + rawStr := "" + if !vStr.IsNull() { + rawStr = vStr.AsString() + } + + if rawStr == "" { + for _, envName := range defs.EnvVars { + rawStr = os.Getenv(envName) + if rawStr != "" { + break + } + } + } + if rawStr == "" { + rawStr = defs.Fallback + } + if defs.Required && rawStr == "" { + return cty.NilVal, fmt.Errorf("argument %q is required", attrName) + } + + // As a special case, if we still have an empty string and the original + // value was null then we'll preserve the null. This is a compromise, + // assuming that SDKLikeData knows how to treat a null value as a + // zero value anyway and if we preserve the null then the recipient + // of this result can still use the cty.Value result directly to + // distinguish between the value being set explicitly to empty in + // the config vs. being entirely unset. + if rawStr == "" && givenVal.IsNull() { + retAttrs[attrName] = givenVal + continue + } + + // By the time we get here, rawStr should be empty only if the original + // value was unset and all of the fallback environment variables were + // also unset. Otherwise, rawStr contains a string representation of + // a value that we now need to convert back to the type that was + // originally wanted. + switch ty { + case cty.String: + retAttrs[attrName] = cty.StringVal(rawStr) + case cty.Bool: + if rawStr == "" { + rawStr = "false" + } + + // Legacy SDK uses strconv.ParseBool and therefore tolerates a + // variety of different string representations of true and false, + // so we'll do the same here. The config itself can't use those + // alternate forms because HCL's definition of bool prevails there, + // but the environment variables can use any of these forms. + bv, err := strconv.ParseBool(rawStr) + if err != nil { + return cty.NilVal, fmt.Errorf("invalid value for %q: %s", attrName, err) + } + retAttrs[attrName] = cty.BoolVal(bv) + case cty.Number: + if rawStr == "" { + rawStr = "0" + } + + // This case is a little trickier because cty.Number could be + // representing either an integer or a float, which each have + // different interpretations in the legacy SDK. Therefore we'll + // try integer first and use its result if successful, but then + // try float as a fallback if not. + if iv, err := strconv.ParseInt(rawStr, 0, 0); err == nil { + retAttrs[attrName] = cty.NumberIntVal(iv) + } else if fv, err := strconv.ParseFloat(rawStr, 64); err == nil { + retAttrs[attrName] = cty.NumberFloatVal(fv) + } else { + return cty.NilVal, fmt.Errorf("invalid value for %q: must be a number", attrName) + } + default: + panic("cannot apply environment variable defaults for " + ty.GoString()) + } + } + return cty.ObjectVal(retAttrs), nil +} diff --git a/internal/backend/backendbase/sdklike_test.go b/internal/backend/backendbase/sdklike_test.go new file mode 100644 index 0000000000..bca3b8a4eb --- /dev/null +++ b/internal/backend/backendbase/sdklike_test.go @@ -0,0 +1,283 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendbase + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestSDKLikePath(t *testing.T) { + tests := []struct { + Input string + Want cty.Path + }{ + { + "foo", + cty.GetAttrPath("foo"), + }, + { + "foo.bar", + cty.GetAttrPath("foo").GetAttr("bar"), + }, + { + "foo.bar.baz", + cty.GetAttrPath("foo").GetAttr("bar").GetAttr("baz"), + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got := SDKLikePath(test.Input) + if !test.Want.Equals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestSDKLikeEnvDefault(t *testing.T) { + t.Setenv("FALLBACK_A", "fallback a") + t.Setenv("FALLBACK_B", "fallback b") + t.Setenv("FALLBACK_UNSET", "") + t.Setenv("FALLBACK_UNSET_1", "") + t.Setenv("FALLBACK_UNSET_2", "") + + tests := map[string]struct { + Value string + EnvNames []string + Want string + }{ + "value is set": { + "hello", + []string{"FALLBACK_A", "FALLBACK_B"}, + "hello", + }, + "value is not set, but both fallbacks are": { + "", + []string{"FALLBACK_A", "FALLBACK_B"}, + "fallback a", + }, + "value is not set, and first callback isn't set": { + "", + []string{"FALLBACK_UNSET", "FALLBACK_B"}, + "fallback b", + }, + "value is not set, and second callback isn't set": { + "", + []string{"FALLBACK_A", "FALLBACK_UNSET"}, + "fallback a", + }, + "nothing is set": { + "", + []string{"FALLBACK_UNSET_1", "FALLBACK_UNSET_2"}, + "", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := SDKLikeEnvDefault(test.Value, test.EnvNames...) + if got != test.Want { + t.Errorf("wrong result\nvalue: %s\nenvs: %s\n\ngot: %s\nwant: %s", test.Value, test.EnvNames, got, test.Want) + } + }) + } +} + +func TestSDKLikeRequiredWithEnvDefault(t *testing.T) { + // This intentionally doesn't duplicate all of the test cases from + // TestSDKLikeEnvDefault, since SDKLikeRequiredWithEnvDefault is + // just a thin wrapper which adds an error check. + + t.Setenv("FALLBACK_UNSET", "") + _, err := SDKLikeRequiredWithEnvDefault("attr_name", "", "FALLBACK_UNSET") + if err == nil { + t.Fatalf("unexpected success; want error") + } + if got, want := err.Error(), `attribute "attr_name" is required`; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } +} + +func TestSDKLikeData(t *testing.T) { + d := NewSDKLikeData(cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "int": cty.NumberIntVal(5), + "float": cty.NumberFloatVal(0.5), + "bool": cty.True, + + "null_string": cty.NullVal(cty.String), + "null_number": cty.NullVal(cty.Number), + "null_bool": cty.NullVal(cty.Bool), + })) + + t.Run("string", func(t *testing.T) { + got := d.String("string") + want := "hello" + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("null string", func(t *testing.T) { + got := d.String("null_string") + want := "" + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("int as string", func(t *testing.T) { + // This is allowed as a convenience for backends that want to + // allow environment-based default values for integer values, + // since environment variables are always strings and so they'd + // need to do their own parsing afterwards anyway. + got := d.String("int") + want := "5" + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("bool as string", func(t *testing.T) { + // This is allowed as a convenience for backends that want to + // allow environment-based default values for bool values, + // since environment variables are always strings and so they'd + // need to do their own parsing afterwards anyway. + got := d.String("bool") + want := "true" + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + + t.Run("int", func(t *testing.T) { + got, err := d.Int64("int") + want := int64(5) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("int with fractional part", func(t *testing.T) { + got, err := d.Int64("float") + if err == nil { + t.Fatalf("unexpected success; want error\ngot: %#v", got) + } + // Legacy SDK exposed the strconv.ParseInt implementation detail in + // its error message, and so for now we do the same. Maybe we'll + // improve this later, but it would probably be better to wean + // the backends off using the "SDKLike" helper altogether instead. + if got, want := err.Error(), `strconv.ParseInt: parsing "0.5": invalid syntax`; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + }) + t.Run("null number as int", func(t *testing.T) { + got, err := d.Int64("null_number") + want := int64(0) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + + t.Run("bool", func(t *testing.T) { + got := d.Bool("bool") + want := true + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("null bool", func(t *testing.T) { + // Assuming false for a null is quite questionable, but it's what + // the legacy SDK did and so we'll follow its lead. + got := d.Bool("null_bool") + want := false + if got != want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) +} + +func TestSDKLikeApplyEnvDefaults(t *testing.T) { + t.Setenv("FALLBACK_BEEP", "beep from environment") + t.Setenv("FALLBACK_UNUSED", "unused from environment") + t.Setenv("FALLBACK_EMPTY", "") + + t.Run("success", func(t *testing.T) { + defs := SDKLikeDefaults{ + "string_set_fallback": { + Fallback: "fallback not used", + }, + "string_set_env": { + EnvVars: []string{"FALLBACK_UNUSED"}, + }, + "string_fallback_null": { + Fallback: "boop from fallback", + }, + "string_fallback_empty": { + Fallback: "boop from fallback", + }, + "string_env_null": { + EnvVars: []string{"FALLBACK_BEEP", "FALLBACK_UNUSED"}, + Fallback: "unused", + }, + "string_env_empty": { + EnvVars: []string{"FALLBACK_BEEP", "FALLBACK_UNUSED"}, + Fallback: "unused", + }, + "string_env_unsetfirst": { + EnvVars: []string{"FALLBACK_EMPTY", "FALLBACK_BEEP"}, + Fallback: "unused", + }, + "string_env_unsetsecond": { + EnvVars: []string{"FALLBACK_BEEP", "FALLBACK_EMPTY"}, + Fallback: "unused", + }, + "string_nothing_null": { + EnvVars: []string{"FALLBACK_EMPTY"}, + }, + "string_nothing_empty": { + EnvVars: []string{"FALLBACK_EMPTY"}, + }, + } + got, err := defs.ApplyTo(cty.ObjectVal(map[string]cty.Value{ + "string_set_fallback": cty.StringVal("set in config"), + "string_set_env": cty.StringVal("set in config"), + "string_fallback_null": cty.NullVal(cty.String), + "string_fallback_empty": cty.StringVal(""), + "string_env_null": cty.NullVal(cty.String), + "string_env_empty": cty.StringVal(""), + "string_env_unsetfirst": cty.NullVal(cty.String), + "string_env_unsetsecond": cty.NullVal(cty.String), + "string_nothing_null": cty.NullVal(cty.String), + "string_nothing_empty": cty.StringVal(""), + "passthru": cty.EmptyObjectVal, + })) + want := cty.ObjectVal(map[string]cty.Value{ + "string_set_fallback": cty.StringVal("set in config"), + "string_set_env": cty.StringVal("set in config"), + "string_fallback_null": cty.StringVal("boop from fallback"), + "string_fallback_empty": cty.StringVal("boop from fallback"), + "string_env_null": cty.StringVal("beep from environment"), + "string_env_empty": cty.StringVal("beep from environment"), + "string_env_unsetfirst": cty.StringVal("beep from environment"), + "string_env_unsetsecond": cty.StringVal("beep from environment"), + "string_nothing_null": cty.NullVal(cty.String), + "string_nothing_empty": cty.StringVal(""), + "passthru": cty.EmptyObjectVal, + }) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) +} diff --git a/internal/backend/cli.go b/internal/backend/backendrun/cli.go similarity index 95% rename from internal/backend/cli.go rename to internal/backend/backendrun/cli.go index 0a73b122fa..01fd90b0d0 100644 --- a/internal/backend/cli.go +++ b/internal/backend/backendrun/cli.go @@ -1,9 +1,13 @@ -package backend +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendrun import ( - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/mitchellh/colorstring" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" ) @@ -22,7 +26,7 @@ import ( // on other methods (such as State, Operation) if CLI initialization was not // done with all required fields. type CLI interface { - Backend + backend.Backend // CLIInit is called once with options. The options passed to this // function may not be modified after calling this since they can be diff --git a/internal/backend/backendrun/doc.go b/internal/backend/backendrun/doc.go new file mode 100644 index 0000000000..aeb4b2e65f --- /dev/null +++ b/internal/backend/backendrun/doc.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package backendrun contains the additional types and helpers used by the +// few backends that actually run operations against Terraform configurations. +// +// Backends that only provide state storage should not use anything in this +// package. +package backendrun diff --git a/internal/backend/backendrun/local_run.go b/internal/backend/backendrun/local_run.go new file mode 100644 index 0000000000..790936d0f8 --- /dev/null +++ b/internal/backend/backendrun/local_run.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendrun + +import ( + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Local implements additional behavior on a Backend that allows local +// operations in addition to remote operations. +// +// This enables more behaviors of Terraform that require more data such +// as `console`, `import`, `graph`. These require direct access to +// configurations, variables, and more. Not all backends may support this +// so we separate it out into its own optional interface. +type Local interface { + // LocalRun uses information in the Operation to prepare a set of objects + // needed to start running that operation. + // + // The operation doesn't need a Type set, but it needs various other + // options set. This is a rather odd API that tries to treat all + // operations as the same when they really aren't; see the local and remote + // backend's implementations of this to understand what this actually + // does, because this operation has no well-defined contract aside from + // "whatever it already does". + LocalRun(*Operation) (*LocalRun, statemgr.Full, tfdiags.Diagnostics) +} + +// LocalRun represents the assortment of objects that we can collect or +// calculate from an Operation object, which we can then use for local +// operations. +// +// The operation methods on terraform.Context (Plan, Apply, Import, etc) each +// generate new artifacts which supersede parts of the LocalRun object that +// started the operation, so callers should be careful to use those subsequent +// artifacts instead of the fields of LocalRun where appropriate. The LocalRun +// data intentionally doesn't update as a result of calling methods on Context, +// in order to make data flow explicit. +// +// This type is a weird architectural wart resulting from the overly-general +// way our backend API models operations, whereby we behave as if all +// Terraform operations have the same inputs and outputs even though they +// are actually all rather different. The exact meaning of the fields in +// this type therefore vary depending on which OperationType was passed to +// Local.Context in order to create an object of this type. +type LocalRun struct { + // Core is an already-initialized Terraform Core context, ready to be + // used to run operations such as Plan and Apply. + Core *terraform.Context + + // Config is the configuration we're working with, which typically comes + // from either config files directly on local disk (when we're creating + // a plan, or similar) or from a snapshot embedded in a plan file + // (when we're applying a saved plan). + Config *configs.Config + + // InputState is the state that should be used for whatever is the first + // method call to a context created with CoreOpts. When creating a plan + // this will be the previous run state, but when applying a saved plan + // this will be the prior state recorded in that plan. + InputState *states.State + + // PlanOpts are options to pass to a Plan or Plan-like operation. + // + // This is nil when we're applying a saved plan, because the plan itself + // contains enough information about its options to apply it. + PlanOpts *terraform.PlanOpts + + // Plan is a plan loaded from a saved plan file, if our operation is to + // apply that saved plan. + // + // This is nil when we're not applying a saved plan. + Plan *plans.Plan +} diff --git a/internal/backend/backendrun/operation.go b/internal/backend/backendrun/operation.go new file mode 100644 index 0000000000..94c72ab5ad --- /dev/null +++ b/internal/backend/backendrun/operation.go @@ -0,0 +1,257 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendrun + +import ( + "context" + "log" + + svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/command/clistate" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/planfile" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// HostAlias describes a list of aliases that should be used when initializing an +// [OperationsBackend]. +type HostAlias struct { + From svchost.Hostname + To svchost.Hostname +} + +// OperationsBackend is an extension of [backend.Backend] for the few backends +// that can directly perform Terraform operations. +// +// Most backends are used only for remote state storage, and those should not +// implement this interface or import anything from this package. +type OperationsBackend interface { + backend.Backend + + // Operation performs a Terraform operation such as refresh, plan, apply. + // It is up to the implementation to determine what "performing" means. + // This DOES NOT BLOCK. The context returned as part of RunningOperation + // should be used to block for completion. + // If the state used in the operation can be locked, it is the + // responsibility of the Backend to lock the state for the duration of the + // running operation. + Operation(context.Context, *Operation) (*RunningOperation, error) + + // ServiceDiscoveryAliases returns a mapping of Alias -> Target hosts to + // configure. + ServiceDiscoveryAliases() ([]HostAlias, error) +} + +// An operation represents an operation for Terraform to execute. +// +// Note that not all fields are supported by all backends and can result +// in an error if set. All backend implementations should show user-friendly +// errors explaining any incorrectly set values. For example, the local +// backend doesn't support a PlanId being set. +// +// The operation options are purposely designed to have maximal compatibility +// between Terraform and Terraform Servers (a commercial product offered by +// HashiCorp). Therefore, it isn't expected that other implementation support +// every possible option. The struct here is generalized in order to allow +// even partial implementations to exist in the open, without walling off +// remote functionality 100% behind a commercial wall. Anyone can implement +// against this interface and have Terraform interact with it just as it +// would with HashiCorp-provided Terraform Servers. +type Operation struct { + // Type is the operation to perform. + Type OperationType + + // PlanId is an opaque value that backends can use to execute a specific + // plan for an apply operation. + // + // PlanOutBackend is the backend to store with the plan. This is the + // backend that will be used when applying the plan. + PlanId string + PlanRefresh bool // PlanRefresh will do a refresh before a plan + PlanOutPath string // PlanOutPath is the path to save the plan + PlanOutBackend *plans.Backend + + // ConfigDir is the path to the directory containing the configuration's + // root module. + ConfigDir string + + // ConfigLoader is a configuration loader that can be used to load + // configuration from ConfigDir. + ConfigLoader *configload.Loader + + // DependencyLocks represents the locked dependencies associated with + // the configuration directory given in ConfigDir. + // + // Note that if field PlanFile is set then the plan file should contain + // its own dependency locks. The backend is responsible for correctly + // selecting between these two sets of locks depending on whether it + // will be using ConfigDir or PlanFile to get the configuration for + // this operation. + DependencyLocks *depsfile.Locks + + // Hooks can be used to perform actions triggered by various events during + // the operation's lifecycle. + Hooks []terraform.Hook + + // Plan is a plan that was passed as an argument. This is valid for + // plan and apply arguments but may not work for all backends. + PlanFile *planfile.WrappedPlanFile + + // The options below are more self-explanatory and affect the runtime + // behavior of the operation. + PlanMode plans.Mode + AutoApprove bool + Targets []addrs.Targetable + ForceReplace []addrs.AbsResourceInstance + Variables map[string]UnparsedVariableValue + StatePersistInterval int + + // Some operations use root module variables only opportunistically or + // don't need them at all. If this flag is set, the backend must treat + // all variables as optional and provide an unknown value for any required + // variables that aren't set in order to allow partial evaluation against + // the resulting incomplete context. + // + // This flag is honored only if PlanFile isn't set. If PlanFile is set then + // the variables set in the plan are used instead, and they must be valid. + AllowUnsetVariables bool + + // DeferralAllowed enables experimental support for automatically performing + // a partial plan if some objects are not yet plannable. + // + // IMPORTANT: When configuring an Operation, you should only set a value for + // this field if Terraform was built with experimental features enabled. + DeferralAllowed bool + + // View implements the logic for all UI interactions. + View views.Operation + + // Input/output/control options. + UIIn terraform.UIInput + UIOut terraform.UIOutput + + // StateLocker is used to lock the state while providing UI feedback to the + // user. This will be replaced by the Backend to update the context. + // + // If state locking is not necessary, this should be set to a no-op + // implementation of clistate.Locker. + StateLocker clistate.Locker + + // Workspace is the name of the workspace that this operation should run + // in, which controls which named state is used. + Workspace string + + // GenerateConfigOut tells the operation both that it should generate config + // for unmatched import targets and where any generated config should be + // written to. + GenerateConfigOut string +} + +// HasConfig returns true if and only if the operation has a ConfigDir value +// that refers to a directory containing at least one Terraform configuration +// file. +func (o *Operation) HasConfig() bool { + return o.ConfigLoader.IsConfigDir(o.ConfigDir) +} + +// Config loads the configuration that the operation applies to, using the +// ConfigDir and ConfigLoader fields within the receiving operation. +func (o *Operation) Config() (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + config, hclDiags := o.ConfigLoader.LoadConfig(o.ConfigDir) + diags = diags.Append(hclDiags) + return config, diags +} + +// ReportResult is a helper for the common chore of setting the status of +// a running operation and showing any diagnostics produced during that +// operation. +// +// If the given diagnostics contains errors then the operation's result +// will be set to backendrun.OperationFailure. It will be set to +// backendrun.OperationSuccess otherwise. It will then use o.View.Diagnostics +// to show the given diagnostics before returning. +// +// Callers should feel free to do each of these operations separately in +// more complex cases where e.g. diagnostics are interleaved with other +// output, but terminating immediately after reporting error diagnostics is +// common and can be expressed concisely via this method. +func (o *Operation) ReportResult(op *RunningOperation, diags tfdiags.Diagnostics) { + if diags.HasErrors() { + op.Result = OperationFailure + } else { + op.Result = OperationSuccess + } + if o.View != nil { + o.View.Diagnostics(diags) + } else { + // Shouldn't generally happen, but if it does then we'll at least + // make some noise in the logs to help us spot it. + if len(diags) != 0 { + log.Printf( + "[ERROR] Backend needs to report diagnostics but View is not set:\n%s", + diags.ErrWithWarnings(), + ) + } + } +} + +// RunningOperation is the result of starting an operation. +type RunningOperation struct { + // For implementers of a backend, this context should not wrap the + // passed in context. Otherwise, cancelling the parent context will + // immediately mark this context as "done" but those aren't the semantics + // we want: we want this context to be done only when the operation itself + // is fully done. + context.Context + + // Stop requests the operation to complete early, by calling Stop on all + // the plugins. If the process needs to terminate immediately, call Cancel. + Stop context.CancelFunc + + // Cancel is the context.CancelFunc associated with the embedded context, + // and can be called to terminate the operation early. + // Once Cancel is called, the operation should return as soon as possible + // to avoid running operations during process exit. + Cancel context.CancelFunc + + // Result is the exit status of the operation, populated only after the + // operation has completed. + Result OperationResult + + // PlanEmpty is populated after a Plan operation completes to note whether + // a plan is empty or has changes. This is only used in the CLI to determine + // the exit status because the plan value is not available at that point. + PlanEmpty bool + + // State is the final state after the operation completed. Persisting + // this state is managed by the backend. This should only be read + // after the operation completes to avoid read/write races. + State *states.State +} + +// OperationResult describes the result status of an operation. +type OperationResult int + +const ( + // OperationSuccess indicates that the operation completed as expected. + OperationSuccess OperationResult = 0 + + // OperationFailure indicates that the operation encountered some sort + // of error, and thus may have been only partially performed or not + // performed at all. + OperationFailure OperationResult = 1 +) + +func (r OperationResult) ExitStatus() int { + return int(r) +} diff --git a/internal/backend/operation_type.go b/internal/backend/backendrun/operation_type.go similarity index 57% rename from internal/backend/operation_type.go rename to internal/backend/backendrun/operation_type.go index f2c84de2cb..22abaf8a4f 100644 --- a/internal/backend/operation_type.go +++ b/internal/backend/backendrun/operation_type.go @@ -1,6 +1,9 @@ -package backend +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 -//go:generate go run golang.org/x/tools/cmd/stringer -type=OperationType operation_type.go +package backendrun + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=OperationType operation_type.go // OperationType is an enum used with Operation to specify the operation // type to perform for Terraform. diff --git a/internal/backend/operationtype_string.go b/internal/backend/backendrun/operationtype_string.go similarity index 97% rename from internal/backend/operationtype_string.go rename to internal/backend/backendrun/operationtype_string.go index fe84d848dd..2761b0676c 100644 --- a/internal/backend/operationtype_string.go +++ b/internal/backend/backendrun/operationtype_string.go @@ -1,6 +1,6 @@ // Code generated by "stringer -type=OperationType operation_type.go"; DO NOT EDIT. -package backend +package backendrun import "strconv" diff --git a/internal/backend/unparsed_value.go b/internal/backend/backendrun/unparsed_value.go similarity index 98% rename from internal/backend/unparsed_value.go rename to internal/backend/backendrun/unparsed_value.go index e7eadea9a1..0262d3ed63 100644 --- a/internal/backend/unparsed_value.go +++ b/internal/backend/backendrun/unparsed_value.go @@ -1,13 +1,17 @@ -package backend +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendrun import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // UnparsedVariableValue represents a variable value provided by the caller diff --git a/internal/backend/unparsed_value_test.go b/internal/backend/backendrun/unparsed_value_test.go similarity index 98% rename from internal/backend/unparsed_value_test.go rename to internal/backend/backendrun/unparsed_value_test.go index 8807d243d7..0aae48b95a 100644 --- a/internal/backend/unparsed_value_test.go +++ b/internal/backend/backendrun/unparsed_value_test.go @@ -1,4 +1,7 @@ -package backend +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package backendrun import ( "strings" diff --git a/internal/backend/init/deprecate_test.go b/internal/backend/init/deprecate_test.go index f84cab808d..e94b697df5 100644 --- a/internal/backend/init/deprecate_test.go +++ b/internal/backend/init/deprecate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package init import ( diff --git a/internal/backend/init/init.go b/internal/backend/init/init.go index a7be1d552a..4c15ce5baa 100644 --- a/internal/backend/init/init.go +++ b/internal/backend/init/init.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package init contains the list of backends that can be initialized and // basic helper functions for initializing those backends. package init @@ -10,9 +13,9 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend/backendrun" backendLocal "github.com/hashicorp/terraform/internal/backend/local" backendRemote "github.com/hashicorp/terraform/internal/backend/remote" - backendArtifactory "github.com/hashicorp/terraform/internal/backend/remote-state/artifactory" backendAzure "github.com/hashicorp/terraform/internal/backend/remote-state/azure" backendConsul "github.com/hashicorp/terraform/internal/backend/remote-state/consul" backendCos "github.com/hashicorp/terraform/internal/backend/remote-state/cos" @@ -20,11 +23,10 @@ import ( backendHTTP "github.com/hashicorp/terraform/internal/backend/remote-state/http" backendInmem "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" backendKubernetes "github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes" - backendManta "github.com/hashicorp/terraform/internal/backend/remote-state/manta" + backendOCI "github.com/hashicorp/terraform/internal/backend/remote-state/oci" backendOSS "github.com/hashicorp/terraform/internal/backend/remote-state/oss" backendPg "github.com/hashicorp/terraform/internal/backend/remote-state/pg" backendS3 "github.com/hashicorp/terraform/internal/backend/remote-state/s3" - backendSwift "github.com/hashicorp/terraform/internal/backend/remote-state/swift" backendCloud "github.com/hashicorp/terraform/internal/cloud" ) @@ -66,42 +68,20 @@ func Init(services *disco.Disco) { "oss": func() backend.Backend { return backendOSS.New() }, "pg": func() backend.Backend { return backendPg.New() }, "s3": func() backend.Backend { return backendS3.New() }, + "oci": func() backend.Backend { return backendOCI.New() }, - // Terraform Cloud 'backend' + // HCP Terraform 'backend' // This is an implementation detail only, used for the cloud package "cloud": func() backend.Backend { return backendCloud.New(services) }, - - // FIXME: remove deprecated backends for v1.3 - // Deprecated backends. - "azure": func() backend.Backend { - return deprecateBackend( - backendAzure.New(), - `Warning: "azure" name is deprecated, please use "azurerm"`, - ) - }, - "artifactory": func() backend.Backend { - return deprecateBackend( - backendArtifactory.New(), - `Warning: "artifactory" backend is deprecated, and will be removed in a future release."`, - ) - }, - "manta": func() backend.Backend { - return deprecateBackend( - backendManta.New(), - `Warning: "manta" backend is deprecated, and will be removed in a future release."`, - ) - }, - "swift": func() backend.Backend { - return deprecateBackend( - backendSwift.New(), - `Warning: "swift" backend is deprecated, and will be removed in a future release."`, - ) - }, } RemovedBackends = map[string]string{ - "etcd": `The "etcd" backend is not supported in Terraform v1.3 or later.`, - "etcdv3": `The "etcdv3" backend is not supported in Terraform v1.3 or later.`, + "artifactory": `The "artifactory" backend is not supported in Terraform v1.3 or later.`, + "azure": `The "azure" backend name has been removed, please use "azurerm".`, + "etcd": `The "etcd" backend is not supported in Terraform v1.3 or later.`, + "etcdv3": `The "etcdv3" backend is not supported in Terraform v1.3 or later.`, + "manta": `The "manta" backend is not supported in Terraform v1.3 or later.`, + "swift": `The "swift" backend is not supported in Terraform v1.3 or later.`, } } @@ -150,15 +130,15 @@ func (b deprecatedBackendShim) PrepareConfig(obj cty.Value) (cty.Value, tfdiags. // warning during validation. func deprecateBackend(b backend.Backend, message string) backend.Backend { // Since a Backend wrapped by deprecatedBackendShim can no longer be - // asserted as an Enhanced or Local backend, disallow those types here + // asserted as an Operations Backend or Local backend, disallow those types here // entirely. If something other than a basic backend.Backend needs to be // deprecated, we can add that functionality to schema.Backend or the // backend itself. - if _, ok := b.(backend.Enhanced); ok { - panic("cannot use DeprecateBackend on an Enhanced Backend") + if _, ok := b.(backendrun.OperationsBackend); ok { + panic("cannot use DeprecateBackend on a Backend that supports operations") } - if _, ok := b.(backend.Local); ok { + if _, ok := b.(backendrun.Local); ok { panic("cannot use DeprecateBackend on a Local Backend") } diff --git a/internal/backend/init/init_test.go b/internal/backend/init/init_test.go index 609e97ce4f..e697e74189 100644 --- a/internal/backend/init/init_test.go +++ b/internal/backend/init/init_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package init import ( @@ -22,11 +25,6 @@ func TestInit_backend(t *testing.T) { {"inmem", "*inmem.Backend"}, {"pg", "*pg.Backend"}, {"s3", "*s3.Backend"}, - - {"azure", "init.deprecatedBackendShim"}, - {"artifactory", "init.deprecatedBackendShim"}, - {"manta", "init.deprecatedBackendShim"}, - {"swift", "init.deprecatedBackendShim"}, } // Make sure we get the requested backend diff --git a/internal/backend/local/backend.go b/internal/backend/local/backend.go index dd6c9cc56f..32ed5015bb 100644 --- a/internal/backend/local/backend.go +++ b/internal/backend/local/backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -12,6 +15,7 @@ import ( "sync" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/logging" @@ -28,7 +32,7 @@ const ( DefaultBackupExtension = ".backup" ) -// Local is an implementation of EnhancedBackend that performs all operations +// Local is an implementation of backendrun.OperationsBackend that performs all operations // locally. This is the "default" backend and implements normal Terraform // behavior as it is well known. type Local struct { @@ -55,6 +59,13 @@ type Local struct { // and will override what'd be built from the State* fields if non-empty. // While the interpretation of the State* fields depends on the active // workspace, the OverrideState* fields are always used literally. + // + // OverrideStatePath is set as a result of the -state flag + // OverrideStateOutPath is set as a result of the -state-out flag + // OverrideStateBackupPath is set as a result of the -state-backup flag + // + // Note: these flags are only accepted by some commands. + // Importantly, they are not accepted by `init`. OverrideStatePath string OverrideStateOutPath string OverrideStateBackupPath string @@ -75,9 +86,9 @@ type Local struct { OpInput bool OpValidation bool - // Backend, if non-nil, will use this backend for non-enhanced behavior. + // Backend, if non-nil, will use this backend for non-operations behavior. // This allows local behavior with remote state storage. It is a way to - // "upgrade" a non-enhanced backend to an enhanced backend with typical + // "upgrade" a non-operations backend to an operations backend with typical // behavior. // // If this is nil, local performs normal state loading and storage. @@ -88,6 +99,7 @@ type Local struct { } var _ backend.Backend = (*Local)(nil) +var _ backendrun.OperationsBackend = (*Local)(nil) // New returns a new initialized local backend. func New() *Local { @@ -95,7 +107,7 @@ func New() *Local { } // NewWithBackend returns a new local backend initialized with a -// dedicated backend for non-enhanced behavior. +// dedicated backend for non-operations/state-storage behavior. func NewWithBackend(backend backend.Backend) *Local { return &Local{ Backend: backend, @@ -180,6 +192,10 @@ func (b *Local) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } +func (b *Local) ServiceDiscoveryAliases() ([]backendrun.HostAlias, error) { + return []backendrun.HostAlias{}, nil +} + func (b *Local) Workspaces() ([]string, error) { // If we have a backend handling state, defer to that. if b.Backend != nil { @@ -214,10 +230,10 @@ func (b *Local) Workspaces() ([]string, error) { // DeleteWorkspace removes a workspace. // // The "default" workspace cannot be removed. -func (b *Local) DeleteWorkspace(name string) error { +func (b *Local) DeleteWorkspace(name string, force bool) error { // If we have a backend handling state, defer to that. if b.Backend != nil { - return b.Backend.DeleteWorkspace(name) + return b.Backend.DeleteWorkspace(name, force) } if name == "" { @@ -261,7 +277,7 @@ func (b *Local) StateMgr(name string) (statemgr.Full, error) { return s, nil } -// Operation implements backend.Enhanced +// Operation implements backendrun.OperationsBackend // // This will initialize an in-memory terraform.Context to perform the // operation within this process. @@ -269,19 +285,19 @@ func (b *Local) StateMgr(name string) (statemgr.Full, error) { // The given operation parameter will be merged with the ContextOpts on // the structure with the following rules. If a rule isn't specified and the // name conflicts, assume that the field is overwritten if set. -func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { +func (b *Local) Operation(ctx context.Context, op *backendrun.Operation) (*backendrun.RunningOperation, error) { if op.View == nil { panic("Operation called with nil View") } // Determine the function to call for our operation - var f func(context.Context, context.Context, *backend.Operation, *backend.RunningOperation) + var f func(context.Context, context.Context, *backendrun.Operation, *backendrun.RunningOperation) switch op.Type { - case backend.OperationTypeRefresh: + case backendrun.OperationTypeRefresh: f = b.opRefresh - case backend.OperationTypePlan: + case backendrun.OperationTypePlan: f = b.opPlan - case backend.OperationTypeApply: + case backendrun.OperationTypeApply: f = b.opApply default: return nil, fmt.Errorf( @@ -297,7 +313,7 @@ func (b *Local) Operation(ctx context.Context, op *backend.Operation) (*backend. // Build our running operation // the runninCtx is only used to block until the operation returns. runningCtx, done := context.WithCancel(context.Background()) - runningOp := &backend.RunningOperation{ + runningOp := &backendrun.RunningOperation{ Context: runningCtx, } @@ -344,7 +360,7 @@ func (b *Local) opWait( // try to force a PersistState just in case the process is terminated // before we can complete. - if err := opStateMgr.PersistState(); err != nil { + if err := opStateMgr.PersistState(nil); err != nil { // We can't error out from here, but warn the user if there was an error. // If this isn't transient, we will catch it again below, and // attempt to save the state another way. @@ -379,8 +395,14 @@ func (b *Local) opWait( return } -// StatePaths returns the StatePath, StateOutPath, and StateBackupPath as -// configured from the CLI. +// StatePaths returns the StatePath, StateOutPath, and StateBackupPath for a given workspace name. +// This value is affected by: +// +// * Default versus non-default workspace. +// +// * Values from the configuration. +// +// * Values configured from the CLI. func (b *Local) StatePaths(name string) (stateIn, stateOut, backupOut string) { statePath := b.OverrideStatePath stateOutPath := b.OverrideStateOutPath diff --git a/internal/backend/local/backend_apply.go b/internal/backend/local/backend_apply.go index a6c6ea9f0b..413f92afc6 100644 --- a/internal/backend/local/backend_apply.go +++ b/internal/backend/local/backend_apply.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -5,9 +8,15 @@ import ( "errors" "fmt" "log" + "time" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" @@ -23,8 +32,8 @@ var testHookStopPlanApply func() func (b *Local) opApply( stopCtx context.Context, cancelCtx context.Context, - op *backend.Operation, - runningOp *backend.RunningOperation) { + op *backendrun.Operation, + runningOp *backendrun.RunningOperation) { log.Printf("[INFO] backend/local: starting Apply operation") var diags, moreDiags tfdiags.Diagnostics @@ -53,13 +62,13 @@ func (b *Local) opApply( op.ReportResult(runningOp, diags) return } - // the state was locked during succesfull context creation; unlock the state + // the state was locked during successful context creation; unlock the state // when the operation completes defer func() { diags := op.StateLocker.Unlock() if diags.HasErrors() { op.View.Diagnostics(diags) - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure } }() @@ -68,26 +77,42 @@ func (b *Local) opApply( // operation. runningOp.State = lr.InputState + schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + op.ReportResult(runningOp, diags) + return + } + // stateHook uses schemas for when it periodically persists state to the + // persistent storage backend. + stateHook.Schemas = schemas + stateHook.PersistInterval = time.Duration(op.StatePersistInterval) * time.Second + var plan *plans.Plan + combinedPlanApply := false // If we weren't given a plan, then we refresh/plan if op.PlanFile == nil { + combinedPlanApply = true // Perform the plan log.Printf("[INFO] backend/local: apply calling Plan") plan, moreDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { + // If Terraform Core generated a partial plan despite the errors + // then we'll make a best effort to render it. Terraform Core + // promises that if it returns a non-nil plan along with errors + // then the plan won't necessarily contain all of the needed + // actions but that any it does include will be properly-formed. + // plan.Errored will be true in this case, which our plan + // renderer can rely on to tailor its messaging. + if plan != nil && (len(plan.Changes.Resources) != 0 || len(plan.Changes.Outputs) != 0) { + op.View.Plan(plan, schemas) + } op.ReportResult(runningOp, diags) return } - schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) - diags = diags.Append(moreDiags) - if moreDiags.HasErrors() { - op.ReportResult(runningOp, diags) - return - } - - trivialPlan := !plan.CanApply() + trivialPlan := !plan.Applyable hasUI := op.UIOut != nil && op.UIIn != nil mustConfirm := hasUI && !op.AutoApprove && !trivialPlan op.View.Plan(plan, schemas) @@ -103,7 +128,7 @@ func (b *Local) opApply( // forced to abort, and no errors were returned from Plan. if stopCtx.Err() != nil { diags = diags.Append(errors.New("execution halted")) - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure op.ReportResult(runningOp, diags) return } @@ -156,12 +181,48 @@ func (b *Local) opApply( } if v != "yes" { op.View.Cancelled(op.PlanMode) - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure return } + } else { + // If we didn't ask for confirmation from the user, and they have + // included any failing checks in their configuration, then they + // will see a very confusing output after the apply operation + // completes. This is because all the diagnostics from the plan + // operation will now be shown alongside the diagnostics from the + // apply operation. For check diagnostics, the plan output is + // irrelevant and simple noise after the same set of checks have + // been executed again during the apply stage. As such, we are going + // to remove all diagnostics marked as check diagnostics at this + // stage, so we will only show the user the check results from the + // apply operation. + // + // Note, if we did ask for approval then we would have displayed the + // plan check results at that point which is useful as the user can + // use them to make a decision about whether to apply the changes. + // It's just that if we didn't ask for approval then showing the + // user the checks from the plan alongside the checks from the apply + // is needlessly confusing. + var filteredDiags tfdiags.Diagnostics + for _, diag := range diags { + if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok && rule.Container.CheckableKind() == addrs.CheckableCheck { + continue + } + filteredDiags = filteredDiags.Append(diag) + } + diags = filteredDiags } } else { plan = lr.Plan + if plan.Errored { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot apply incomplete plan", + "Terraform encountered an error when generating this plan, so it cannot be applied.", + )) + op.ReportResult(runningOp, diags) + return + } for _, change := range plan.Changes.Resources { if change.Action != plans.NoOp { op.View.PlannedChange(change) @@ -172,6 +233,174 @@ func (b *Local) opApply( // Set up our hook for continuous state updates stateHook.StateMgr = opState + applyTimeValues := make(terraform.InputValues, plan.ApplyTimeVariables.Len()) + + // In a combined plan/apply run, getting the context already gathers the interactive + // input, therefore we need to make sure to pass the ephemeral variables to the applyOpts. + if combinedPlanApply { + for varName, v := range lr.PlanOpts.SetVariables { + decl, ok := lr.Config.Module.Variables[varName] + if !ok { + continue // This should never happen, but we'll ignore it if it does. + } + + if v.SourceType == terraform.ValueFromInput && decl.Ephemeral { + applyTimeValues[varName] = v + } + } + } + + if len(op.Variables) != 0 { + // Undeclared variables cause warnings during plan, but will show up + // again here during apply. Their handling is tricky though, because it + // depends on how they were declared, and is subject to compatibility + // constraints. Collect any suspect values as we go, and then use the + // same parsing logic from the plan to generate the diagnostics. + undeclaredVariables := map[string]backendrun.UnparsedVariableValue{} + + parsedVars, _ := backendrun.ParseVariableValues(op.Variables, lr.Config.Module.Variables) + + for varName := range op.Variables { + parsedVar, parsed := parsedVars[varName] + + decl, ok := lr.Config.Module.Variables[varName] + if !ok || !parsed { + // We'll try to parse this and handle diagnostics for missing + // variables with ParseUndeclaredVariableValues after. + undeclaredVariables[varName] = op.Variables[varName] + continue + } + + var rng *hcl.Range + if parsedVar.HasSourceRange() { + rng = parsedVar.SourceRange.ToHCL().Ptr() + } + + // If the var is declared as ephemeral in config, go ahead and handle it + if decl.Ephemeral { + // Determine whether this is an apply-time variable, i.e. an + // ephemeral variable that was set (non-null) during the + // planning phase. + applyTimeVar := false + for avName := range plan.ApplyTimeVariables.All() { + if varName == avName { + applyTimeVar = true + } + } + + // If this isn't an apply-time variable, it's not valid to + // set it during apply. + if !applyTimeVar { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral variable was not set during planning", + Detail: fmt.Sprintf( + "The ephemeral input variable %q was not set during the planning phase, and so must remain unset during the apply phase.", + varName, + ), + Subject: rng, + }) + continue + } + + // If this is an apply-time variable, the user must supply a + // value during apply: it can't be null. + if applyTimeVar && parsedVar.Value.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral variable must be set for apply", + Detail: fmt.Sprintf( + "The ephemeral input variable %q was set during the planning phase, and so must be set again during the apply phase.", + varName, + ), + }) + continue + } + + // If we get here, we are in possession of a non-null + // ephemeral apply-time input variable, and need only pass + // its value on to the ApplyOpts. + applyTimeValues[varName] = parsedVar + } else { + // If a non-ephemeral variable is set differently between plan and apply, we should emit a diagnostic. + plannedVariableValue, ok := plan.VariableValues[varName] + if !ok { + // We'll catch this with ParseUndeclaredVariableValues after + undeclaredVariables[varName] = op.Variables[varName] + continue + } + + plannedVar, err := plannedVariableValue.Decode(cty.DynamicPseudoType) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Could not decode variable value from plan", + Detail: fmt.Sprintf("The variable %s could not be decoded from the plan. %s. This is a bug in Terraform, please report it.", varName, err), + Subject: rng, + }) + } else { + // The user can't override the planned variables, so we + // error when possible to avoid confusion. + if parsedVar.Value.Equals(plannedVar).False() { + switch parsedVar.SourceType { + case terraform.ValueFromAutoFile: + // If the parsed variables comes from an auto-file, + // it's not input directly by the user so we have to ignore it. + continue + case terraform.ValueFromEnvVar: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Ignoring variable when applying a saved plan", + Detail: fmt.Sprintf("The variable %s cannot be overriden when applying a saved plan file, "+ + "because a saved plan includes the variable values that were set when it was created. "+ + "The saved plan specifies %s as the value whereas during apply the value %s was %s. "+ + "To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true.", + varName, tfdiags.CompactValueStr(plannedVar), tfdiags.CompactValueStr(parsedVar.Value), + parsedVar.SourceType.DiagnosticLabel()), + Subject: rng, + }) + case terraform.ValueFromCLIArg, terraform.ValueFromNamedFile: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't change variable when applying a saved plan", + Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options when "+ + "applying a saved plan file, because a saved plan includes the variable values that were "+ + "set when it was created. The saved plan specifies %s as the value whereas during apply "+ + "the value %s was %s. To declare an ephemeral variable which is not saved in the plan "+ + "file, use ephemeral = true.", + varName, tfdiags.CompactValueStr(plannedVar), tfdiags.CompactValueStr(parsedVar.Value), + parsedVar.SourceType.DiagnosticLabel()), + Subject: rng, + }) + default: + // Other SourceTypes should never reach this point because + // - ValueFromConfig - supplied plan already contains the original configuration + // - ValueFromInput - we disable prompt when plan file is supplied + // - ValueFromCaller - only used in tests + panic(fmt.Sprintf("Attempted to change variable %s when applying a saved plan. "+ + "The saved plan specifies %s as the value whereas during apply the value %s was %s. "+ + "This is a bug in Terraform, please report it.", + varName, tfdiags.CompactValueStr(plannedVar), tfdiags.CompactValueStr(parsedVar.Value), + parsedVar.SourceType.DiagnosticLabel())) + } + } + } + } + + } + _, undeclaredDiags := backendrun.ParseUndeclaredVariableValues(undeclaredVariables, map[string]*configs.Variable{}) + // always add hard errors here, and add warnings if we're not in a + // combined op which just emitted those same warnings already. + if undeclaredDiags.HasErrors() || !combinedPlanApply { + diags = diags.Append(undeclaredDiags) + } + + if diags.HasErrors() { + op.ReportResult(runningOp, diags) + return + } + } + // Start the apply in a goroutine so that we can be interrupted. var applyState *states.State var applyDiags tfdiags.Diagnostics @@ -179,8 +408,11 @@ func (b *Local) opApply( go func() { defer logging.PanicHandler() defer close(doneCh) + log.Printf("[INFO] backend/local: apply calling Apply") - applyState, applyDiags = lr.Core.Apply(plan, lr.Config) + applyState, applyDiags = lr.Core.Apply(plan, lr.Config, &terraform.ApplyOpts{ + SetVariables: applyTimeValues, + }) }() if b.opWait(doneCh, stopCtx, cancelCtx, lr.Core, opState, op.View) { @@ -198,7 +430,7 @@ func (b *Local) opApply( // Store the final state runningOp.State = applyState - err := statemgr.WriteAndPersist(opState, applyState) + err := statemgr.WriteAndPersist(opState, applyState, schemas) if err != nil { // Export the state file from the state manager and assign the new // state. This is needed to preserve the existing serial and lineage. diff --git a/internal/backend/local/backend_apply_test.go b/internal/backend/local/backend_apply_test.go index 493000a802..34e7030dbf 100644 --- a/internal/backend/local/backend_apply_test.go +++ b/internal/backend/local/backend_apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -12,7 +15,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -24,7 +27,6 @@ import ( "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" - "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -45,7 +47,7 @@ func TestLocal_applyBasic(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("operation failed") } @@ -72,11 +74,55 @@ test_instance.foo: t.Fatalf("unexpected error output:\n%s", errOutput) } } +func TestLocal_applyCheck(t *testing.T) { + b := TestLocal(t) + + p := TestLocalProvider(t, b, "test", applyFixtureSchema()) + p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("yes"), + "ami": cty.StringVal("bar"), + })} + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-check") + defer configCleanup() + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("bad: %s", err) + } + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatal("operation failed") + } + + if p.ReadResourceCalled { + t.Fatal("ReadResource should not be called") + } + + if !p.PlanResourceChangeCalled { + t.Fatal("diff should be called") + } + + if !p.ApplyResourceChangeCalled { + t.Fatal("apply should be called") + } + + d := done(t) + if errOutput := d.Stderr(); errOutput != "" { + t.Fatalf("unexpected error output:\n%s", errOutput) + } + + if stdOutput := d.Stdout(); strings.Contains(stdOutput, "Check block assertion known after apply") { + // As we are running an auto approved plan the warning that was + // generated during the plan should have been hidden. + t.Fatalf("std output contained unexpected check output:\n%s", stdOutput) + } +} func TestLocal_applyEmptyDir(t *testing.T) { b := TestLocal(t) - p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) + p := TestLocalProvider(t, b, "test", providers.ProviderSchema{}) p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{NewState: cty.ObjectVal(map[string]cty.Value{"id": cty.StringVal("yes")})} op, configCleanup, done := testOperationApply(t, "./testdata/empty") @@ -87,7 +133,7 @@ func TestLocal_applyEmptyDir(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("operation succeeded; want error") } @@ -110,7 +156,7 @@ func TestLocal_applyEmptyDir(t *testing.T) { func TestLocal_applyEmptyDirDestroy(t *testing.T) { b := TestLocal(t) - p := TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) + p := TestLocalProvider(t, b, "test", providers.ProviderSchema{}) p.ApplyResourceChangeResponse = &providers.ApplyResourceChangeResponse{} op, configCleanup, done := testOperationApply(t, "./testdata/empty") @@ -122,7 +168,7 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("apply operation failed") } @@ -140,12 +186,14 @@ func TestLocal_applyEmptyDirDestroy(t *testing.T) { func TestLocal_applyError(t *testing.T) { b := TestLocal(t) - schema := &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + schema := providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "ami": {Type: cty.String, Optional: true}, - "id": {Type: cty.String, Computed: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ami": {Type: cty.String, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, }, }, }, @@ -186,7 +234,7 @@ func TestLocal_applyError(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("operation succeeded; want failure") } @@ -241,7 +289,7 @@ func TestLocal_applyBackendFail(t *testing.T) { output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatalf("apply succeeded; want error") } @@ -282,7 +330,7 @@ func TestLocal_applyRefreshFalse(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -313,10 +361,10 @@ func (s failingState) WriteState(state *states.State) error { return errors.New("fake failure") } -func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationApply(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) @@ -326,8 +374,8 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")) - return &backend.Operation{ - Type: backend.OperationTypeApply, + return &backendrun.Operation{ + Type: backendrun.OperationTypeApply, ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewNoopLocker(), @@ -339,13 +387,15 @@ func testOperationApply(t *testing.T, configDir string) (*backend.Operation, fun // applyFixtureSchema returns a schema suitable for processing the // configuration in testdata/apply . This schema should be // assigned to a mock provider named "test". -func applyFixtureSchema() *terraform.ProviderSchema { - return &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ +func applyFixtureSchema() providers.ProviderSchema { + return providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "ami": {Type: cty.String, Optional: true}, - "id": {Type: cty.String, Computed: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ami": {Type: cty.String, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, }, }, }, @@ -379,7 +429,7 @@ func TestApply_applyCanceledAutoApprove(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } diff --git a/internal/backend/local/backend_local.go b/internal/backend/local/backend_local.go index 6082bfdf6c..11f9dce01a 100644 --- a/internal/backend/local/backend_local.go +++ b/internal/backend/local/backend_local.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -7,25 +10,30 @@ import ( "sort" "strings" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "maps" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) -// backend.Local implementation. -func (b *Local) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) { +// backendrun.Local implementation. +func (b *Local) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) { // Make sure the type is invalid. We use this as a way to know not // to ask for input/validate. We're modifying this through a pointer, // so we're mutating an object that belongs to the caller here, which // seems bad but we're preserving it for now until we have time to // properly design this API, vs. just preserving whatever it currently // happens to do. - op.Type = backend.OperationTypeInvalid + op.Type = backendrun.OperationTypeInvalid op.StateLocker = op.StateLocker.WithContext(context.Background()) @@ -33,7 +41,7 @@ func (b *Local) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful return lr, stateMgr, diags } -func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) { +func (b *Local) localRun(op *backendrun.Operation) (*backendrun.LocalRun, *configload.Snapshot, statemgr.Full, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Get the latest state. @@ -62,7 +70,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. return nil, nil, nil, diags } - ret := &backend.LocalRun{} + ret := &backendrun.LocalRun{} // Initialize our context options var coreOpts terraform.ContextOpts @@ -74,7 +82,12 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. var ctxDiags tfdiags.Diagnostics var configSnap *configload.Snapshot - if op.PlanFile != nil { + if op.PlanFile.IsCloud() { + diags = diags.Append(fmt.Errorf("error: using a saved cloud plan when executing Terraform locally is not supported")) + return nil, nil, nil, diags + } + + if lp, ok := op.PlanFile.Local(); ok { var stateMeta *statemgr.SnapshotMeta // If the statemgr implements our optional PersistentMeta interface then we'll // additionally verify that the state snapshot in the plan file has @@ -83,8 +96,8 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. m := sm.StateSnapshotMeta() stateMeta = &m } - log.Printf("[TRACE] backend/local: populating backend.LocalRun from plan file") - ret, configSnap, ctxDiags = b.localRunForPlanFile(op, op.PlanFile, ret, &coreOpts, stateMeta) + log.Printf("[TRACE] backend/local: populating backendrun.LocalRun from plan file") + ret, configSnap, ctxDiags = b.localRunForPlanFile(op, lp, ret, &coreOpts, stateMeta) if ctxDiags.HasErrors() { diags = diags.Append(ctxDiags) return nil, nil, nil, diags @@ -94,7 +107,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. // available if we need to generate diagnostic message snippets. op.ConfigLoader.ImportSourcesFromSnapshot(configSnap) } else { - log.Printf("[TRACE] backend/local: populating backend.LocalRun for current working directory") + log.Printf("[TRACE] backend/local: populating backendrun.LocalRun for current working directory") ret, configSnap, ctxDiags = b.localRunDirect(op, ret, &coreOpts, s) } diags = diags.Append(ctxDiags) @@ -104,7 +117,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. // If we have an operation, then we automatically do the input/validate // here since every option requires this. - if op.Type != backend.OperationTypeInvalid { + if op.Type != backendrun.OperationTypeInvalid { // If input asking is enabled, then do that if op.PlanFile == nil && b.OpInput { mode := terraform.InputModeProvider @@ -120,7 +133,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. // If validation is enabled, validate if b.OpValidation { log.Printf("[TRACE] backend/local: running validation operation") - validateDiags := ret.Core.Validate(ret.Config) + validateDiags := ret.Core.Validate(ret.Config, nil) diags = diags.Append(validateDiags) } } @@ -128,7 +141,7 @@ func (b *Local) localRun(op *backend.Operation) (*backend.LocalRun, *configload. return ret, configSnap, s, diags } -func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) { +func (b *Local) localRunDirect(op *backendrun.Operation, run *backendrun.LocalRun, coreOpts *terraform.ContextOpts, s statemgr.Full) (*backendrun.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Load the configuration using the caller-provided configuration loader. @@ -165,7 +178,7 @@ func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, cor )) } - var rawVariables map[string]backend.UnparsedVariableValue + var rawVariables map[string]backendrun.UnparsedVariableValue if op.AllowUnsetVariables { // Rather than prompting for input, we'll just stub out the required // but unset variables with unknown values to represent that they are @@ -180,18 +193,20 @@ func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, cor rawVariables = b.interactiveCollectVariables(context.TODO(), op.Variables, config.Module.Variables, op.UIIn) } - variables, varDiags := backend.ParseVariableValues(rawVariables, config.Module.Variables) + variables, varDiags := backendrun.ParseVariableValues(rawVariables, config.Module.Variables) diags = diags.Append(varDiags) if diags.HasErrors() { return nil, nil, diags } planOpts := &terraform.PlanOpts{ - Mode: op.PlanMode, - Targets: op.Targets, - ForceReplace: op.ForceReplace, - SetVariables: variables, - SkipRefresh: op.Type != backend.OperationTypeRefresh && !op.PlanRefresh, + Mode: op.PlanMode, + Targets: op.Targets, + ForceReplace: op.ForceReplace, + SetVariables: variables, + SkipRefresh: op.Type != backendrun.OperationTypeRefresh && !op.PlanRefresh, + GenerateConfigPath: op.GenerateConfigOut, + DeferralAllowed: op.DeferralAllowed, } run.PlanOpts = planOpts @@ -208,7 +223,7 @@ func (b *Local) localRunDirect(op *backend.Operation, run *backend.LocalRun, cor return run, configSnap, diags } -func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, run *backend.LocalRun, coreOpts *terraform.ContextOpts, currentStateMeta *statemgr.SnapshotMeta) (*backend.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) { +func (b *Local) localRunForPlanFile(op *backendrun.Operation, pf *planfile.Reader, run *backendrun.LocalRun, coreOpts *terraform.ContextOpts, currentStateMeta *statemgr.SnapshotMeta) (*backendrun.LocalRun, *configload.Snapshot, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics const errSummary = "Invalid plan file" @@ -226,6 +241,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, return nil, snap, diags } loader := configload.NewLoaderFromSnapshot(snap) + loader.AllowLanguageExperiments(op.ConfigLoader.AllowsLanguageExperiments()) config, configDiags := loader.LoadConfig(snap.Modules[""].Dir) diags = diags.Append(configDiags) if configDiags.HasErrors() { @@ -284,6 +300,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, )) return nil, snap, diags } + if currentStateMeta != nil { // If the caller sets this, we require that the stored prior state // has the same metadata, which is an extra safety check that nothing @@ -361,7 +378,7 @@ func (b *Local) localRunForPlanFile(op *backend.Operation, pf *planfile.Reader, // messages that variables are not set rather than reporting that input failed: // the primary resolution to missing variables is to provide them by some other // means. -func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backend.UnparsedVariableValue { +func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[string]backendrun.UnparsedVariableValue, vcs map[string]*configs.Variable, uiInput terraform.UIInput) map[string]backendrun.UnparsedVariableValue { var needed []string if b.OpInput && uiInput != nil { for name, vc := range vcs { @@ -384,16 +401,20 @@ func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[st // If we get here then we're planning to prompt for at least one additional // variable's value. sort.Strings(needed) // prompt in lexical order - ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) - for k, v := range existing { - ret[k] = v - } + ret := make(map[string]backendrun.UnparsedVariableValue, len(vcs)) + maps.Copy(ret, existing) // don't use clone here, so we can have a non-nil map + for _, name := range needed { vc := vcs[name] + query := fmt.Sprintf("var.%s", name) + if vc.Ephemeral { + query += " (ephemeral)" + } rawValue, err := uiInput.Input(ctx, &terraform.InputOpts{ Id: fmt.Sprintf("var.%s", name), - Query: fmt.Sprintf("var.%s", name), + Query: query, Description: vc.Description, + Secret: vc.Sensitive, }) if err != nil { // Since interactive prompts are best-effort, we'll just continue @@ -431,7 +452,7 @@ func (b *Local) interactiveCollectVariables(ctx context.Context, existing map[st // the given map unchanged if no additions are required. If additions are // required then the result will be a new map containing everything in the // given map plus additional elements. -func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backend.UnparsedVariableValue { +func (b *Local) stubUnsetRequiredVariables(existing map[string]backendrun.UnparsedVariableValue, vcs map[string]*configs.Variable) map[string]backendrun.UnparsedVariableValue { var missing bool // Do we need to add anything? for name, vc := range vcs { if !vc.Required() { @@ -446,10 +467,9 @@ func (b *Local) stubUnsetRequiredVariables(existing map[string]backend.UnparsedV } // If we get down here then there's at least one variable value to add. - ret := make(map[string]backend.UnparsedVariableValue, len(vcs)) - for k, v := range existing { - ret[k] = v - } + ret := make(map[string]backendrun.UnparsedVariableValue, len(vcs)) + maps.Copy(ret, existing) // don't use clone here, so we can return a non-nil map + for name, vc := range vcs { if !vc.Required() { continue @@ -465,7 +485,7 @@ type unparsedInteractiveVariableValue struct { Name, RawValue string } -var _ backend.UnparsedVariableValue = unparsedInteractiveVariableValue{} +var _ backendrun.UnparsedVariableValue = unparsedInteractiveVariableValue{} func (v unparsedInteractiveVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -485,7 +505,7 @@ type unparsedUnknownVariableValue struct { WantType cty.Type } -var _ backend.UnparsedVariableValue = unparsedUnknownVariableValue{} +var _ backendrun.UnparsedVariableValue = unparsedUnknownVariableValue{} func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { return &terraform.InputValue{ @@ -493,3 +513,27 @@ func (v unparsedUnknownVariableValue) ParseVariableValue(mode configs.VariablePa SourceType: terraform.ValueFromInput, }, nil } + +type unparsedTestVariableValue struct { + Expr hcl.Expression +} + +var _ backendrun.UnparsedVariableValue = unparsedTestVariableValue{} + +func (v unparsedTestVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + value, valueDiags := v.Expr.Value(&hcl.EvalContext{ + Functions: lang.TestingFunctions(), + }) + diags = diags.Append(valueDiags) + if valueDiags.HasErrors() { + return nil, diags + } + + return &terraform.InputValue{ + Value: value, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(v.Expr.Range()), + }, diags +} diff --git a/internal/backend/local/backend_local_test.go b/internal/backend/local/backend_local_test.go index 05573f0f9d..788886b83d 100644 --- a/internal/backend/local/backend_local_test.go +++ b/internal/backend/local/backend_local_test.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( + "context" "fmt" "os" "path/filepath" @@ -9,6 +13,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -17,6 +22,7 @@ import ( "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -28,14 +34,14 @@ func TestLocalRun(t *testing.T) { configDir := "./testdata/empty" b := TestLocal(t) - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() streams, _ := terminal.StreamsForTesting(t) view := views.NewView(streams) stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Workspace: backend.DefaultStateName, @@ -59,14 +65,14 @@ func TestLocalRun_error(t *testing.T) { // should then cause LocalRun to return with the state unlocked. b.Backend = backendWithStateStorageThatFailsRefresh{} - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() streams, _ := terminal.StreamsForTesting(t) view := views.NewView(streams) stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, Workspace: backend.DefaultStateName, @@ -82,11 +88,46 @@ func TestLocalRun_error(t *testing.T) { assertBackendStateUnlocked(t, b) } +func TestLocalRun_cloudPlan(t *testing.T) { + configDir := "./testdata/apply" + b := TestLocal(t) + + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") + defer configCleanup() + + planPath := "./testdata/plan-bookmark/bookmark.json" + + planFile, err := planfile.OpenWrapped(planPath) + if err != nil { + t.Fatalf("unexpected error reading planfile: %s", err) + } + + streams, _ := terminal.StreamsForTesting(t) + view := views.NewView(streams) + stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) + + op := &backendrun.Operation{ + ConfigDir: configDir, + ConfigLoader: configLoader, + PlanFile: planFile, + Workspace: backend.DefaultStateName, + StateLocker: stateLocker, + } + + _, _, diags := b.LocalRun(op) + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + + // LocalRun() unlocks the state on failure + assertBackendStateUnlocked(t, b) +} + func TestLocalRun_stalePlan(t *testing.T) { configDir := "./testdata/apply" b := TestLocal(t) - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() // Write an empty state file with serial 3 @@ -118,7 +159,7 @@ func TestLocalRun_stalePlan(t *testing.T) { } plan := &plans.Plan{ UIMode: plans.NormalMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), Backend: plans.Backend{ Type: "local", Config: backendConfigRaw, @@ -142,7 +183,7 @@ func TestLocalRun_stalePlan(t *testing.T) { if err := planfile.Create(planPath, planfileArgs); err != nil { t.Fatalf("unexpected error writing planfile: %s", err) } - planFile, err := planfile.Open(planPath) + planFile, err := planfile.OpenWrapped(planPath) if err != nil { t.Fatalf("unexpected error reading planfile: %s", err) } @@ -151,7 +192,7 @@ func TestLocalRun_stalePlan(t *testing.T) { view := views.NewView(streams) stateLocker := clistate.NewLocker(0, views.NewStateLocker(arguments.ViewHuman, view)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanFile: planFile, @@ -189,7 +230,7 @@ func (b backendWithStateStorageThatFailsRefresh) Configure(cty.Value) tfdiags.Di return nil } -func (b backendWithStateStorageThatFailsRefresh) DeleteWorkspace(name string) error { +func (b backendWithStateStorageThatFailsRefresh) DeleteWorkspace(name string, force bool) error { return fmt.Errorf("unimplemented") } @@ -221,7 +262,7 @@ func (s *stateStorageThatFailsRefresh) State() *states.State { return nil } -func (s *stateStorageThatFailsRefresh) GetRootOutputValues() (map[string]*states.OutputValue, error) { +func (s *stateStorageThatFailsRefresh) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { return nil, fmt.Errorf("unimplemented") } @@ -233,6 +274,6 @@ func (s *stateStorageThatFailsRefresh) RefreshState() error { return fmt.Errorf("intentionally failing for testing purposes") } -func (s *stateStorageThatFailsRefresh) PersistState() error { +func (s *stateStorageThatFailsRefresh) PersistState(schemas *schemarepo.Schemas) error { return fmt.Errorf("unimplemented") } diff --git a/internal/backend/local/backend_plan.go b/internal/backend/local/backend_plan.go index b27f98c688..3937a4948b 100644 --- a/internal/backend/local/backend_plan.go +++ b/internal/backend/local/backend_plan.go @@ -1,11 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( "context" "fmt" + "io" "log" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" @@ -18,8 +23,8 @@ import ( func (b *Local) opPlan( stopCtx context.Context, cancelCtx context.Context, - op *backend.Operation, - runningOp *backend.RunningOperation) { + op *backendrun.Operation, + runningOp *backendrun.RunningOperation) { log.Printf("[INFO] backend/local: starting Plan operation") @@ -50,11 +55,28 @@ func (b *Local) opPlan( return } + if len(op.GenerateConfigOut) > 0 { + if op.PlanMode != plans.NormalMode { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid generate-config-out flag", + "Config can only be generated during a normal plan operation, and not during a refresh-only or destroy plan.")) + op.ReportResult(runningOp, diags) + return + } + + diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut)) + if diags.HasErrors() { + op.ReportResult(runningOp, diags) + return + } + } + if b.ContextOpts == nil { b.ContextOpts = new(terraform.ContextOpts) } - // Get our context + // Set up backend and get our context lr, configSnap, opState, ctxDiags := b.localRun(op) diags = diags.Append(ctxDiags) if ctxDiags.HasErrors() { @@ -67,7 +89,7 @@ func (b *Local) opPlan( diags := op.StateLocker.Unlock() if diags.HasErrors() { op.View.Diagnostics(diags) - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure } }() @@ -90,19 +112,28 @@ func (b *Local) opPlan( // If we get in here then the operation was cancelled, which is always // considered to be a failure. log.Printf("[INFO] backend/local: plan operation was force-cancelled by interrupt") - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure return } log.Printf("[INFO] backend/local: plan operation completed") - diags = diags.Append(planDiags) - if planDiags.HasErrors() { + // NOTE: We intentionally don't stop here on errors because we always want + // to try to present a partial plan report and, if the user chose to, + // generate a partial saved plan file for external analysis. + // Plan() may produce some diagnostic warnings which were already + // produced when setting up context above, so we deduplicate them here. + diags = diags.AppendWithoutDuplicates(planDiags...) + + // Even if there are errors we need to handle anything that may be + // contained within the plan, so only exit if there is no data at all. + if plan == nil { + runningOp.PlanEmpty = true op.ReportResult(runningOp, diags) return } // Record whether this plan includes any side-effects that could be applied. - runningOp.PlanEmpty = !plan.CanApply() + runningOp.PlanEmpty = !plan.Applyable // Save the plan to disk if path := op.PlanOutPath; path != "" { @@ -153,21 +184,72 @@ func (b *Local) opPlan( } } - // Render the plan + // Render the plan, if we produced one. + // (This might potentially be a partial plan with Errored set to true) schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { op.ReportResult(runningOp, diags) return } + + // Write out any generated config, before we render the plan. + wroteConfig, moreDiags := maybeWriteGeneratedConfig(plan, op.GenerateConfigOut) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + op.ReportResult(runningOp, diags) + return + } + op.View.Plan(plan, schemas) - // If we've accumulated any warnings along the way then we'll show them - // here just before we show the summary and next steps. If we encountered - // errors then we would've returned early at some other point above. - op.View.Diagnostics(diags) + // If we've accumulated any diagnostics along the way then we'll show them + // here just before we show the summary and next steps. This can potentially + // include errors, because we intentionally try to show a partial plan + // above even if Terraform Core encountered an error partway through + // creating it. + op.ReportResult(runningOp, diags) if !runningOp.PlanEmpty { - op.View.PlanNextStep(op.PlanOutPath) + if wroteConfig { + op.View.PlanNextStep(op.PlanOutPath, op.GenerateConfigOut) + } else { + op.View.PlanNextStep(op.PlanOutPath, "") + } } } + +func maybeWriteGeneratedConfig(plan *plans.Plan, out string) (wroteConfig bool, diags tfdiags.Diagnostics) { + if genconfig.ShouldWriteConfig(out) { + diags := genconfig.ValidateTargetFile(out) + if diags.HasErrors() { + return false, diags + } + + var writer io.Writer + for _, c := range plan.Changes.Resources { + change := genconfig.Change{ + Addr: c.Addr.String(), + GeneratedConfig: c.GeneratedConfig, + } + if c.Importing != nil { + change.ImportID = c.Importing.ID + } + + var moreDiags tfdiags.Diagnostics + writer, wroteConfig, moreDiags = change.MaybeWriteConfig(writer, out) + if moreDiags.HasErrors() { + return false, diags.Append(moreDiags) + } + } + } + + if wroteConfig { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Config generation is experimental", + "Generating configuration during import is currently experimental, and the generated configuration format may change in future versions.")) + } + + return wroteConfig, diags +} diff --git a/internal/backend/local/backend_plan_test.go b/internal/backend/local/backend_plan_test.go index 5121d45e70..ace870fe5e 100644 --- a/internal/backend/local/backend_plan_test.go +++ b/internal/backend/local/backend_plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -7,8 +10,10 @@ import ( "strings" "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -17,10 +22,10 @@ import ( "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" - "github.com/zclconf/go-cty/cty" ) func TestLocal_planBasic(t *testing.T) { @@ -36,7 +41,7 @@ func TestLocal_planBasic(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -74,7 +79,7 @@ func TestLocal_planInAutomation(t *testing.T) { t.Fatalf("unexpected error: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -85,7 +90,7 @@ func TestLocal_planInAutomation(t *testing.T) { func TestLocal_planNoConfig(t *testing.T) { b := TestLocal(t) - TestLocalProvider(t, b, "test", &terraform.ProviderSchema{}) + TestLocalProvider(t, b, "test", providers.ProviderSchema{}) op, configCleanup, done := testOperationPlan(t, "./testdata/empty") defer configCleanup() @@ -99,7 +104,7 @@ func TestLocal_planNoConfig(t *testing.T) { output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("plan operation succeeded; want failure") } @@ -141,7 +146,7 @@ func TestLocal_plan_context_error(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationFailure { + if run.Result != backendrun.OperationFailure { t.Fatalf("plan operation succeeded") } @@ -209,7 +214,7 @@ func TestLocal_planOutputsChanged(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } if run.PlanEmpty { @@ -265,7 +270,7 @@ func TestLocal_planModuleOutputsChanged(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } if !run.PlanEmpty { @@ -307,7 +312,7 @@ func TestLocal_planTainted(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } if !p.ReadResourceCalled { @@ -386,7 +391,7 @@ func TestLocal_planDeposedOnly(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } if !p.ReadResourceCalled { @@ -477,7 +482,7 @@ func TestLocal_planTainted_createBeforeDestroy(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } if !p.ReadResourceCalled { @@ -520,7 +525,7 @@ func TestLocal_planRefreshFalse(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -569,7 +574,7 @@ func TestLocal_planDestroy(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -621,7 +626,7 @@ func TestLocal_planDestroy_withDataSources(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -629,7 +634,7 @@ func TestLocal_planDestroy_withDataSources(t *testing.T) { t.Fatal("plan should not be empty") } - // Data source should still exist in the the plan file + // Data source should still exist in the plan file plan := testReadPlan(t, planPath) if len(plan.Changes.Resources) != 2 { t.Fatalf("Expected exactly 1 resource for destruction, %d given: %q", @@ -694,7 +699,7 @@ func TestLocal_planOutPathNoChange(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("plan operation failed") } @@ -709,10 +714,10 @@ func TestLocal_planOutPathNoChange(t *testing.T) { } } -func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationPlan(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) @@ -722,8 +727,8 @@ func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")) - return &backend.Operation{ - Type: backend.OperationTypePlan, + return &backendrun.Operation{ + Type: backendrun.OperationTypePlan, ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewNoopLocker(), @@ -851,32 +856,60 @@ func testReadPlan(t *testing.T, path string) *plans.Plan { // planFixtureSchema returns a schema suitable for processing the // configuration in testdata/plan . This schema should be // assigned to a mock provider named "test". -func planFixtureSchema() *terraform.ProviderSchema { - return &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ +func planFixtureSchema() providers.ProviderSchema { + return providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "ami": {Type: cty.String, Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "network_interface": { - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "device_index": {Type: cty.Number, Optional: true}, - "description": {Type: cty.String, Optional: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "network_interface": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "device_index": {Type: cty.Number, Optional: true}, + "description": {Type: cty.String, Optional: true}, + }, }, }, }, }, }, }, - DataSources: map[string]*configschema.Block{ + DataSources: map[string]providers.Schema{ "test_ds": { - Attributes: map[string]*configschema.Attribute{ - "filter": {Type: cty.String, Required: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "filter": {Type: cty.String, Required: true}, + }, }, }, }, } } + +func TestLocal_invalidOptions(t *testing.T) { + b := TestLocal(t) + TestLocalProvider(t, b, "test", planFixtureSchema()) + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan") + defer configCleanup() + op.PlanRefresh = true + op.PlanMode = plans.RefreshOnlyMode + op.ForceReplace = []addrs.AbsResourceInstance{mustResourceInstanceAddr("test_instance.foo")} + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + <-run.Done() + if run.Result == backendrun.OperationSuccess { + t.Fatalf("plan operation failed") + } + + if errOutput := done(t).Stderr(); errOutput == "" { + t.Fatal("expected error output") + } +} diff --git a/internal/backend/local/backend_refresh.go b/internal/backend/local/backend_refresh.go index 8ce3b6aff1..14e9301d9c 100644 --- a/internal/backend/local/backend_refresh.go +++ b/internal/backend/local/backend_refresh.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -6,7 +9,7 @@ import ( "log" "os" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -16,8 +19,8 @@ import ( func (b *Local) opRefresh( stopCtx context.Context, cancelCtx context.Context, - op *backend.Operation, - runningOp *backend.RunningOperation) { + op *backendrun.Operation, + runningOp *backendrun.RunningOperation) { var diags tfdiags.Diagnostics @@ -52,13 +55,13 @@ func (b *Local) opRefresh( return } - // the state was locked during succesfull context creation; unlock the state + // the state was locked during successful context creation; unlock the state // when the operation completes defer func() { diags := op.StateLocker.Unlock() if diags.HasErrors() { op.View.Diagnostics(diags) - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure } }() @@ -73,6 +76,14 @@ func (b *Local) opRefresh( )) } + // get schemas before writing state + schemas, moreDiags := lr.Core.Schemas(lr.Config, lr.InputState) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + op.ReportResult(runningOp, diags) + return + } + // Perform the refresh in a goroutine so we can be interrupted var newState *states.State var refreshDiags tfdiags.Diagnostics @@ -96,7 +107,7 @@ func (b *Local) opRefresh( return } - err := statemgr.WriteAndPersist(opState, newState) + err := statemgr.WriteAndPersist(opState, newState, schemas) if err != nil { diags = diags.Append(fmt.Errorf("failed to write state: %w", err)) op.ReportResult(runningOp, diags) diff --git a/internal/backend/local/backend_refresh_test.go b/internal/backend/local/backend_refresh_test.go index 886c184181..8bb13c7f5d 100644 --- a/internal/backend/local/backend_refresh_test.go +++ b/internal/backend/local/backend_refresh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -7,7 +10,7 @@ import ( "testing" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -60,18 +63,22 @@ test_instance.foo: func TestLocal_refreshInput(t *testing.T) { b := TestLocal(t) - schema := &terraform.ProviderSchema{ - Provider: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "value": {Type: cty.String, Optional: true}, + schema := providers.ProviderSchema{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + }, }, }, - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "foo": {Type: cty.String, Optional: true}, - "ami": {Type: cty.String, Optional: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "foo": {Type: cty.String, Optional: true}, + "ami": {Type: cty.String, Optional: true}, + }, }, }, }, @@ -151,17 +158,21 @@ test_instance.foo: func TestLocal_refreshValidateProviderConfigured(t *testing.T) { b := TestLocal(t) - schema := &terraform.ProviderSchema{ - Provider: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "value": {Type: cty.String, Optional: true}, + schema := providers.ProviderSchema{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + }, }, }, - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, - "ami": {Type: cty.String, Optional: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, }, }, }, @@ -214,7 +225,7 @@ func TestLocal_refresh_context_error(t *testing.T) { t.Fatalf("bad: %s", err) } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("operation succeeded; want failure") } assertBackendStateUnlocked(t, b) @@ -253,10 +264,10 @@ func TestLocal_refreshEmptyState(t *testing.T) { assertBackendStateUnlocked(t, b) } -func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationRefresh(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewOperation(arguments.ViewHuman, false, views.NewView(streams)) @@ -266,8 +277,8 @@ func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, f depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/test")) - return &backend.Operation{ - Type: backend.OperationTypeRefresh, + return &backendrun.Operation{ + Type: backendrun.OperationTypeRefresh, ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewNoopLocker(), @@ -294,13 +305,15 @@ func testRefreshState() *states.State { // refreshFixtureSchema returns a schema suitable for processing the // configuration in testdata/refresh . This schema should be // assigned to a mock provider named "test". -func refreshFixtureSchema() *terraform.ProviderSchema { - return &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ +func refreshFixtureSchema() providers.ProviderSchema { + return providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "ami": {Type: cty.String, Optional: true}, - "id": {Type: cty.String, Computed: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ami": {Type: cty.String, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, }, }, }, diff --git a/internal/backend/local/backend_test.go b/internal/backend/local/backend_test.go index eff227466d..090b0f0b2c 100644 --- a/internal/backend/local/backend_test.go +++ b/internal/backend/local/backend_test.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( "errors" + "fmt" "os" "path/filepath" "reflect" @@ -9,23 +13,689 @@ import ( "testing" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) func TestLocal_impl(t *testing.T) { - var _ backend.Enhanced = New() - var _ backend.Local = New() - var _ backend.CLI = New() + var _ backendrun.OperationsBackend = New() + var _ backendrun.Local = New() + var _ backendrun.CLI = New() } func TestLocal_backend(t *testing.T) { - testTmpDir(t) + _ = testTmpDir(t) b := New() backend.TestBackendStates(t, b) backend.TestBackendStateLocks(t, b, b) } +func TestLocal_PrepareConfig(t *testing.T) { + // Setup + _ = testTmpDir(t) + + b := New() + + // PATH ATTR + // Empty string path attribute isn't valid + config := cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal(""), + "workspace_dir": cty.NullVal(cty.String), + }) + _, diags := b.PrepareConfig(config) + if !diags.HasErrors() { + t.Fatalf("expected an error from PrepareConfig but got none") + } + expectedErr := `The "path" attribute value must not be empty` + if !strings.Contains(diags.Err().Error(), expectedErr) { + t.Fatalf("expected an error containing %q, got: %q", expectedErr, diags.Err()) + } + + // PrepareConfig doesn't enforce the path value has .tfstate extension + config = cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal("path/to/state/my-state.docx"), + "workspace_dir": cty.NullVal(cty.String), + }) + _, diags = b.PrepareConfig(config) + if diags.HasErrors() { + t.Fatalf("unexpected error returned from PrepareConfig") + } + + // WORKSPACE_DIR ATTR + // Empty string workspace_dir attribute isn't valid + config = cty.ObjectVal(map[string]cty.Value{ + "path": cty.NullVal(cty.String), + "workspace_dir": cty.StringVal(""), + }) + _, diags = b.PrepareConfig(config) + if !diags.HasErrors() { + t.Fatalf("expected an error from PrepareConfig but got none") + } + expectedErr = `The "workspace_dir" attribute value must not be empty` + if !strings.Contains(diags.Err().Error(), expectedErr) { + t.Fatalf("expected an error containing %q, got: %q", expectedErr, diags.Err()) + } + + // Existence of directory isn't checked during PrepareConfig + // (Non-existent directories are created as a side-effect of WriteState) + config = cty.ObjectVal(map[string]cty.Value{ + "path": cty.NullVal(cty.String), + "workspace_dir": cty.StringVal("this/does/not/exist"), + }) + _, diags = b.PrepareConfig(config) + if diags.HasErrors() { + t.Fatalf("unexpected error returned from PrepareConfig") + } +} + +// The `path` attribute should only affect the default workspace's state +// file location and name. +// +// Non-default workspaces' states names and locations are unaffected. +func TestLocal_useOfPathAttribute(t *testing.T) { + // Setup + td := testTmpDir(t) + + b := New() + + // Configure local state-storage backend (skip call to PrepareConfig) + path := "path/to/foobar.tfstate" + config := cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal(path), // Set + "workspace_dir": cty.NullVal(cty.String), + }) + diags := b.Configure(config) + if diags.HasErrors() { + t.Fatalf("unexpected error returned from Configure") + } + + // State file at the `path` location doesn't exist yet + workspace := backend.DefaultStateName + stmgr, err := b.StateMgr(workspace) + if err != nil { + t.Fatalf("unexpected error returned from StateMgr") + } + defaultStatePath := fmt.Sprintf("%s/%s", td, path) + if _, err := os.Stat(defaultStatePath); !strings.Contains(err.Error(), "no such file or directory") { + if err != nil { + t.Fatalf("expected \"no such file or directory\" error when accessing file %q, got: %s", path, err) + } + t.Fatalf("expected the state file %q to not exist, but it did", path) + } + + // Writing to the default workspace's state creates a file + // at the `path` location. + // Directories are created to enable the path. + s := states.NewState() + s.RootOutputValues = map[string]*states.OutputValue{ + "foobar": { + Value: cty.StringVal("foobar"), + }, + } + err = stmgr.WriteState(s) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + _, err = os.Stat(defaultStatePath) + if err != nil { + // The file should exist post-WriteState + t.Fatalf("unexpected error when getting stats on the state file %q", path) + } + + // Writing to a non-default workspace's state creates a file + // that's unaffected by the `path` location + workspace = "fizzbuzz" + stmgr, err = b.StateMgr(workspace) + if err != nil { + t.Fatalf("unexpected error returned from StateMgr") + } + fizzbuzzStatePath := fmt.Sprintf("%s/terraform.tfstate.d/%s/terraform.tfstate", td, workspace) + err = stmgr.WriteState(s) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + + // The file should exist post-WriteState + checkState(t, fizzbuzzStatePath, s.String()) +} + +// Using non-tfstate file extensions in the value of the `path` attribute +// doesn't affect writing to state +func TestLocal_pathAttributeWrongExtension(t *testing.T) { + // Setup + td := testTmpDir(t) + + b := New() + + // The path value doesn't have the expected .tfstate file extension + path := "foobar.docx" + fullPath := fmt.Sprintf("%s/%s", td, path) + config := cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal(path), // Set + "workspace_dir": cty.NullVal(cty.String), + }) + diags := b.Configure(config) + if diags.HasErrors() { + t.Fatalf("unexpected error returned from Configure") + } + + // Writing to the default workspace's state creates a file + workspace := backend.DefaultStateName + stmgr, err := b.StateMgr(workspace) + if err != nil { + t.Fatalf("unexpected error returned from StateMgr") + } + s := states.NewState() + s.RootOutputValues = map[string]*states.OutputValue{ + "foobar": { + Value: cty.StringVal("foobar"), + }, + } + err = stmgr.WriteState(s) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + + // The file should exist post-WriteState, despite the odd file extension, + // be readable, and contain the correct state + checkState(t, fullPath, s.String()) +} + +// The `workspace_dir` attribute should only affect where non-default workspaces' +// state files are saved. +// +// The default workspace's name and location are unaffected by this attribute. +func TestLocal_useOfWorkspaceDirAttribute(t *testing.T) { + // Setup + td := testTmpDir(t) + + b := New() + + // Configure local state-storage backend (skip call to PrepareConfig) + workspaceDir := "path/to/workspaces" + config := cty.ObjectVal(map[string]cty.Value{ + "path": cty.NullVal(cty.String), + "workspace_dir": cty.StringVal(workspaceDir), // set + }) + diags := b.Configure(config) + if diags.HasErrors() { + t.Fatalf("unexpected error returned from Configure") + } + + // Writing to the default workspace's state creates a file. + // As path attribute was left null, the default location + // ./terraform.tfstate is used. + // Unaffected by the `workspace_dir` location. + workspace := backend.DefaultStateName + defaultStatePath := fmt.Sprintf("%s/terraform.tfstate", td) + stmgr, err := b.StateMgr(workspace) + if err != nil { + t.Fatalf("unexpected error returned from StateMgr") + } + s := states.NewState() + s.RootOutputValues = map[string]*states.OutputValue{ + "foobar": { + Value: cty.StringVal("foobar"), + }, + } + err = stmgr.WriteState(s) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + // Assert state + checkState(t, defaultStatePath, s.String()) + + // Writing to a non-default workspace's state creates a file + // that's affected by the `workspace_dir` location + workspace = "fizzbuzz" + fizzbuzzStatePath := fmt.Sprintf("%s/%s/%s/terraform.tfstate", td, workspaceDir, workspace) + stmgr, err = b.StateMgr(workspace) + if err != nil { + t.Fatalf("unexpected error returned from StateMgr") + } + err = stmgr.WriteState(s) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + // Assert state + checkState(t, fizzbuzzStatePath, s.String()) +} + +// When using the local state storage you cannot delete the default workspace's state +func TestLocal_cannotDeleteDefaultState(t *testing.T) { + // Setup + _ = testTmpDir(t) + dflt := backend.DefaultStateName + expectedStates := []string{dflt} + + b := New() + + // Only default workspace exists initially. + states, err := b.Workspaces() + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected []string{%q}, got %q", dflt, states) + } + + // Attempt to delete default state - force=false + err = b.DeleteWorkspace(dflt, false) + if err == nil { + t.Fatal("expected error but there was none") + } + expectedErr := "cannot delete default state" + if err.Error() != expectedErr { + t.Fatalf("expected error %q, got: %q", expectedErr, err) + } + + // Setting force=true doesn't change outcome + err = b.DeleteWorkspace(dflt, true) + if err == nil { + t.Fatal("expected error but there was none") + } + if err.Error() != expectedErr { + t.Fatalf("expected error %q, got: %q", expectedErr, err) + } +} + +func TestLocal_addAndRemoveStates(t *testing.T) { + // Setup + _ = testTmpDir(t) + dflt := backend.DefaultStateName + expectedStates := []string{dflt} + + b := New() + + // Only the default workspace exists initially. + states, err := b.Workspaces() + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected []string{%q}, got %q", dflt, states) + } + + // Calling StateMgr with a new workspace name creates that workspace's state file. + expectedA := "test_A" + if _, err := b.StateMgr(expectedA); err != nil { + t.Fatal(err) + } + + states, err = b.Workspaces() + if err != nil { + t.Fatal(err) + } + + expectedStates = append(expectedStates, expectedA) + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + + // Creating another workspace appends it to the list of present workspaces. + expectedB := "test_B" + if _, err := b.StateMgr(expectedB); err != nil { + t.Fatal(err) + } + + states, err = b.Workspaces() + if err != nil { + t.Fatal(err) + } + + expectedStates = append(expectedStates, expectedB) + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + + // Can delete a given workspace + if err := b.DeleteWorkspace(expectedA, true); err != nil { + t.Fatal(err) + } + + states, err = b.Workspaces() + if err != nil { + t.Fatal(err) + } + + expectedStates = []string{dflt, expectedB} + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + + // Can delete another workspace + if err := b.DeleteWorkspace(expectedB, true); err != nil { + t.Fatal(err) + } + + states, err = b.Workspaces() + if err != nil { + t.Fatal(err) + } + + expectedStates = []string{dflt} + if !reflect.DeepEqual(states, expectedStates) { + t.Fatalf("expected %q, got %q", expectedStates, states) + } + + // You cannot delete the default workspace + if err := b.DeleteWorkspace(dflt, true); err == nil { + t.Fatal("expected error deleting default state") + } +} + +func TestLocal_StatePaths_defaultWorkspace(t *testing.T) { + + // Default paths are returned for the default workspace + // when nothing is set via config or overrides + b := New() + path, out, back := b.StatePaths("") + + if path != DefaultStateFilename { + t.Fatalf("expected %q, got %q", DefaultStateFilename, path) + } + + if out != DefaultStateFilename { + t.Fatalf("expected %q, got %q", DefaultStateFilename, out) + } + + dfltBackup := DefaultStateFilename + DefaultBackupExtension + if back != dfltBackup { + t.Fatalf("expected %q, got %q", dfltBackup, back) + } + + // If `path` is set in the config, this impacts returned paths for the default workspace + b = New() + configPath := "new-path.tfstate" + b.StatePath = configPath // equivalent of path = "new-path.tfstate" in config + b.StateOutPath = configPath // equivalent of path = "new-path.tfstate" in config + + path, out, back = b.StatePaths("") + + if path != configPath { + t.Fatalf("expected %q, got %q", configPath, path) + } + + if out != configPath { + t.Fatalf("expected %q, got %q", configPath, out) + } + + altBackup := configPath + DefaultBackupExtension + if back != altBackup { + t.Fatalf("expected %q, got %q", altBackup, back) + } + + // If overrides are set, they override default values or those from config + b = New() + b.StatePath = configPath // equivalent of path = "new-path.tfstate" in config + b.StateOutPath = configPath // equivalent of path = "new-path.tfstate" in config + override := "override.tfstate" + b.OverrideStatePath = override + b.OverrideStateOutPath = override + b.OverrideStateBackupPath = override + + path, out, back = b.StatePaths("") + + if path != override { + t.Fatalf("expected %q, got %q", override, path) + } + + if out != override { + t.Fatalf("expected %q, got %q", override, out) + } + + if back != override { + t.Fatalf("expected %q, got %q", override, back) + } +} + +func TestLocal_StatePaths_nonDefaultWorkspace(t *testing.T) { + + // Default paths are returned for a custom workspace + // when nothing is set via config or overrides + b := New() + workspace := "test_env" + path, out, back := b.StatePaths(workspace) + + expectedPath := filepath.Join(DefaultWorkspaceDir, workspace, DefaultStateFilename) + expectedOut := expectedPath + expectedBackup := expectedPath + DefaultBackupExtension + + if path != expectedPath { + t.Fatalf("expected %q, got %q", expectedPath, path) + } + + if out != expectedOut { + t.Fatalf("expected %q, got %q", expectedOut, out) + } + + if back != expectedBackup { + t.Fatalf("expected %q, got %q", expectedBackup, back) + } + + // This is unaffected by a user setting the path attribute + b = New() + b.StatePath = "path-from-config.tfstate" // equivalent of setting path = "path-from-config.tfstate" in config + b.StateOutPath = "path-from-config.tfstate" + + path, out, back = b.StatePaths(workspace) + + if path != expectedPath { + t.Fatalf("expected %q, got %q", expectedPath, path) + } + + if out != expectedOut { + t.Fatalf("expected %q, got %q", expectedOut, out) + } + + if back != expectedBackup { + t.Fatalf("expected %q, got %q", expectedBackup, back) + } + + // If a user set working_dir in config it affects returned values + b = New() + workingDir := "my/alternative/state/dir" + b.StateWorkspaceDir = workingDir // equivalent of setting working_dir = "my/alternative/state/dir" in config + + path, out, back = b.StatePaths(workspace) + + expectedPath = filepath.Join(workingDir, workspace, DefaultStateFilename) + expectedOut = filepath.Join(workingDir, workspace, DefaultStateFilename) + expectedBackup = filepath.Join(workingDir, workspace, DefaultStateFilename) + DefaultBackupExtension + + if path != expectedPath { + t.Fatalf("expected %q, got %q", expectedPath, path) + } + + if out != expectedOut { + t.Fatalf("expected %q, got %q", expectedOut, out) + } + + if back != expectedBackup { + t.Fatalf("expected %q, got %q", expectedBackup, back) + } + + // Overrides affect returned values regardless of config + b = New() + b.StateWorkspaceDir = workingDir // equivalent of setting working_dir = "my/alternative/state/dir" in config + override := "override.tfstate" + b.OverrideStatePath = override + b.OverrideStateOutPath = override + b.OverrideStateBackupPath = override + + path, out, back = b.StatePaths(workspace) + + if path != override { + t.Fatalf("expected %q, got %q", override, path) + } + + if out != override { + t.Fatalf("expected %q, got %q", override, out) + } + + if back != override { + t.Fatalf("expected %q, got %q", override, back) + } +} + +// TestLocal_PathsConflictWith does not include testing the effects of CLI commands -state, -state-out, and -state-backup +// because PathsConflictWith is only used during state migrations, and the init command does not accept those flags. +// Those flags would cause the local backend struct to have override fields set. +func TestLocal_PathsConflictWith(t *testing.T) { + // Create a working directory with default and non-default workspace states + td := testTmpDir(t) + exampleState := states.NewState() + exampleState.RootOutputValues = map[string]*states.OutputValue{ + "foobar": { + Value: cty.StringVal("foobar"), + }, + } + foobar := "foobar" + originalBackend := New() + + // Create a default workspace state file in a non-root directory + originalBackend.StatePath = "foobar/terraform.tfstate" + defaultStatePath := filepath.Join(td, originalBackend.StatePath) + stmgrDefault, _ := originalBackend.StateMgr("") + err := stmgrDefault.WriteState(exampleState) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + checkState(t, defaultStatePath, exampleState.String()) + + // Create a non-default workspace and state file there + stmgrFoobar, _ := originalBackend.StateMgr(foobar) + err = stmgrFoobar.WriteState(exampleState) + if err != nil { + t.Fatalf("unexpected error returned from WriteState") + } + foobarStatePath := filepath.Join(td, DefaultWorkspaceDir, foobar, DefaultStateFilename) + checkState(t, foobarStatePath, exampleState.String()) + + // Scenario where: + // * original backend has state for a 'foobar' workspace at terraform.tfstate.d/foobar/terraform.tfstate + // * new local backend is configured via `path` to store 'default' state at terraform.tfstate.d/foobar/terraform.tfstate + scenario1 := New() + scenario1.StatePath = foobarStatePath + + if !originalBackend.PathsConflictWith(scenario1) { + t.Fatal("expected conflict but got none") + } + + // Scenario where: + // * original backend has state for the default workspace at ./foobar/terrform.tfstate + // * local backend is configured to store non-default workspace state in the root dir + // this means a foobar workspace would also store state at ./foobar/terrform.tfstate + scenario2 := New() + scenario2.StateWorkspaceDir = "." + + if !originalBackend.PathsConflictWith(scenario2) { + t.Fatal("expected conflict but got none") + } +} + +// a local backend which returns errors for methods to +// verify it's being called. +type testDelegateBackend struct { + *Local +} + +var errTestDelegatePrepareConfig = errors.New("prepare config called") +var errTestDelegateConfigure = errors.New("configure called") +var errTestDelegateState = errors.New("state called") +var errTestDelegateStates = errors.New("states called") +var errTestDelegateDeleteState = errors.New("delete called") + +func (b *testDelegateBackend) ConfigSchema() *configschema.Block { + return nil +} + +func (b *testDelegateBackend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + return cty.NilVal, diags.Append(errTestDelegatePrepareConfig) +} + +func (b *testDelegateBackend) Configure(obj cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + return diags.Append(errTestDelegateConfigure) +} + +func (b *testDelegateBackend) StateMgr(name string) (statemgr.Full, error) { + return nil, errTestDelegateState +} + +func (b *testDelegateBackend) Workspaces() ([]string, error) { + return nil, errTestDelegateStates +} + +func (b *testDelegateBackend) DeleteWorkspace(name string, force bool) error { + return errTestDelegateDeleteState +} + +// Verify that all backend.Backend methods are dispatched to the correct Backend when +// the local backend created with a separate state storage backend. +// +// The Local struct type implements both backendrun.OperationsBackend and backend.Backend interfaces. +// If the Local struct is not created with a separate state storage backend then it'll use its own +// backend.Backend method implementations. If a separate state storage backend IS supplied, then +// it should pass those method calls through to the separate backend.Backend. +func TestLocal_callsMethodsOnStateBackend(t *testing.T) { + // assign a separate backend where we can read the state + b := NewWithBackend(&testDelegateBackend{}) + + if schema := b.ConfigSchema(); schema != nil { + t.Fatal("expected a nil schema, got:", schema) + } + + if _, diags := b.PrepareConfig(cty.NilVal); !diags.HasErrors() { + t.Fatal("expected errTestDelegatePrepareConfig error, got:", diags) + } + + if diags := b.Configure(cty.NilVal); !diags.HasErrors() { + t.Fatal("expected errTestDelegateConfigure error, got:", diags) + } + + if _, err := b.StateMgr("test"); err != errTestDelegateState { + t.Fatal("expected errTestDelegateState, got:", err) + } + + if _, err := b.Workspaces(); err != errTestDelegateStates { + t.Fatal("expected errTestDelegateStates, got:", err) + } + + if err := b.DeleteWorkspace("test", true); err != errTestDelegateDeleteState { + t.Fatal("expected errTestDelegateDeleteState, got:", err) + } +} + +// testTmpDir changes into a tmp dir and change back automatically when the test +// and all its subtests complete. +func testTmpDir(t *testing.T) string { + tmp := t.TempDir() + + old, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + + t.Cleanup(func() { + // ignore errors and try to clean up + os.Chdir(old) + }) + + return tmp +} + func checkState(t *testing.T, path, expected string) { t.Helper() // Read the state @@ -46,201 +716,3 @@ func checkState(t *testing.T, path, expected string) { t.Fatalf("state does not match! actual:\n%s\n\nexpected:\n%s", actual, expected) } } - -func TestLocal_StatePaths(t *testing.T) { - b := New() - - // Test the defaults - path, out, back := b.StatePaths("") - - if path != DefaultStateFilename { - t.Fatalf("expected %q, got %q", DefaultStateFilename, path) - } - - if out != DefaultStateFilename { - t.Fatalf("expected %q, got %q", DefaultStateFilename, out) - } - - dfltBackup := DefaultStateFilename + DefaultBackupExtension - if back != dfltBackup { - t.Fatalf("expected %q, got %q", dfltBackup, back) - } - - // check with env - testEnv := "test_env" - path, out, back = b.StatePaths(testEnv) - - expectedPath := filepath.Join(DefaultWorkspaceDir, testEnv, DefaultStateFilename) - expectedOut := expectedPath - expectedBackup := expectedPath + DefaultBackupExtension - - if path != expectedPath { - t.Fatalf("expected %q, got %q", expectedPath, path) - } - - if out != expectedOut { - t.Fatalf("expected %q, got %q", expectedOut, out) - } - - if back != expectedBackup { - t.Fatalf("expected %q, got %q", expectedBackup, back) - } - -} - -func TestLocal_addAndRemoveStates(t *testing.T) { - testTmpDir(t) - dflt := backend.DefaultStateName - expectedStates := []string{dflt} - - b := New() - states, err := b.Workspaces() - if err != nil { - t.Fatal(err) - } - - if !reflect.DeepEqual(states, expectedStates) { - t.Fatalf("expected []string{%q}, got %q", dflt, states) - } - - expectedA := "test_A" - if _, err := b.StateMgr(expectedA); err != nil { - t.Fatal(err) - } - - states, err = b.Workspaces() - if err != nil { - t.Fatal(err) - } - - expectedStates = append(expectedStates, expectedA) - if !reflect.DeepEqual(states, expectedStates) { - t.Fatalf("expected %q, got %q", expectedStates, states) - } - - expectedB := "test_B" - if _, err := b.StateMgr(expectedB); err != nil { - t.Fatal(err) - } - - states, err = b.Workspaces() - if err != nil { - t.Fatal(err) - } - - expectedStates = append(expectedStates, expectedB) - if !reflect.DeepEqual(states, expectedStates) { - t.Fatalf("expected %q, got %q", expectedStates, states) - } - - if err := b.DeleteWorkspace(expectedA); err != nil { - t.Fatal(err) - } - - states, err = b.Workspaces() - if err != nil { - t.Fatal(err) - } - - expectedStates = []string{dflt, expectedB} - if !reflect.DeepEqual(states, expectedStates) { - t.Fatalf("expected %q, got %q", expectedStates, states) - } - - if err := b.DeleteWorkspace(expectedB); err != nil { - t.Fatal(err) - } - - states, err = b.Workspaces() - if err != nil { - t.Fatal(err) - } - - expectedStates = []string{dflt} - if !reflect.DeepEqual(states, expectedStates) { - t.Fatalf("expected %q, got %q", expectedStates, states) - } - - if err := b.DeleteWorkspace(dflt); err == nil { - t.Fatal("expected error deleting default state") - } -} - -// a local backend which returns sentinel errors for NamedState methods to -// verify it's being called. -type testDelegateBackend struct { - *Local - - // return a sentinel error on these calls - stateErr bool - statesErr bool - deleteErr bool -} - -var errTestDelegateState = errors.New("state called") -var errTestDelegateStates = errors.New("states called") -var errTestDelegateDeleteState = errors.New("delete called") - -func (b *testDelegateBackend) StateMgr(name string) (statemgr.Full, error) { - if b.stateErr { - return nil, errTestDelegateState - } - s := statemgr.NewFilesystem("terraform.tfstate") - return s, nil -} - -func (b *testDelegateBackend) Workspaces() ([]string, error) { - if b.statesErr { - return nil, errTestDelegateStates - } - return []string{"default"}, nil -} - -func (b *testDelegateBackend) DeleteWorkspace(name string) error { - if b.deleteErr { - return errTestDelegateDeleteState - } - return nil -} - -// verify that the MultiState methods are dispatched to the correct Backend. -func TestLocal_multiStateBackend(t *testing.T) { - // assign a separate backend where we can read the state - b := NewWithBackend(&testDelegateBackend{ - stateErr: true, - statesErr: true, - deleteErr: true, - }) - - if _, err := b.StateMgr("test"); err != errTestDelegateState { - t.Fatal("expected errTestDelegateState, got:", err) - } - - if _, err := b.Workspaces(); err != errTestDelegateStates { - t.Fatal("expected errTestDelegateStates, got:", err) - } - - if err := b.DeleteWorkspace("test"); err != errTestDelegateDeleteState { - t.Fatal("expected errTestDelegateDeleteState, got:", err) - } -} - -// testTmpDir changes into a tmp dir and change back automatically when the test -// and all its subtests complete. -func testTmpDir(t *testing.T) { - tmp := t.TempDir() - - old, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - if err := os.Chdir(tmp); err != nil { - t.Fatal(err) - } - - t.Cleanup(func() { - // ignore errors and try to clean up - os.Chdir(old) - }) -} diff --git a/internal/backend/local/cli.go b/internal/backend/local/cli.go index 41d2477c6a..4dc927756c 100644 --- a/internal/backend/local/cli.go +++ b/internal/backend/local/cli.go @@ -1,13 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( "log" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" ) -// backend.CLI impl. -func (b *Local) CLIInit(opts *backend.CLIOpts) error { +// backendrun.CLI impl. +func (b *Local) CLIInit(opts *backendrun.CLIOpts) error { b.ContextOpts = opts.ContextOpts b.OpInput = opts.Input b.OpValidation = opts.Validation diff --git a/internal/backend/local/hook_state.go b/internal/backend/local/hook_state.go index 4c11496c25..1759c638b2 100644 --- a/internal/backend/local/hook_state.go +++ b/internal/backend/local/hook_state.go @@ -1,8 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( + "log" "sync" + "time" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" @@ -15,6 +21,20 @@ type StateHook struct { sync.Mutex StateMgr statemgr.Writer + + // If PersistInterval is nonzero then for any new state update after + // the duration has elapsed we'll try to persist a state snapshot + // to the persistent backend too. + // That's only possible if field Schemas is valid, because the + // StateMgr.PersistState function for some backends needs schemas. + PersistInterval time.Duration + + // Schemas are the schemas to use when persisting state due to + // PersistInterval. This is ignored if PersistInterval is zero, + // and PersistInterval is ignored if this is nil. + Schemas *schemarepo.Schemas + + intermediatePersist statemgr.IntermediateStatePersistInfo } var _ terraform.Hook = (*StateHook)(nil) @@ -23,11 +43,71 @@ func (h *StateHook) PostStateUpdate(new *states.State) (terraform.HookAction, er h.Lock() defer h.Unlock() + h.intermediatePersist.RequestedPersistInterval = h.PersistInterval + + if h.intermediatePersist.LastPersist.IsZero() { + // The first PostStateUpdate starts the clock for intermediate + // calls to PersistState. + h.intermediatePersist.LastPersist = time.Now() + } + if h.StateMgr != nil { if err := h.StateMgr.WriteState(new); err != nil { return terraform.HookActionHalt, err } + if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.PersistInterval != 0 && h.Schemas != nil { + if h.shouldPersist() { + err := mgrPersist.PersistState(h.Schemas) + if err != nil { + return terraform.HookActionHalt, err + } + h.intermediatePersist.LastPersist = time.Now() + } else { + log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr) + } + } } return terraform.HookActionContinue, nil } + +func (h *StateHook) Stopping() { + h.Lock() + defer h.Unlock() + + // If Terraform has been asked to stop then that might mean that a hard + // kill signal will follow shortly in case Terraform doesn't stop + // quickly enough, and so we'll try to persist the latest state + // snapshot in the hope that it'll give the user less recovery work to + // do if they _do_ subsequently hard-kill Terraform during an apply. + + if mgrPersist, ok := h.StateMgr.(statemgr.Persister); ok && h.Schemas != nil { + // While we're in the stopping phase we'll try to persist every + // new state update to maximize every opportunity we get to avoid + // losing track of objects that have been created or updated. + // Terraform Core won't start any new operations after it's been + // stopped, so at most we should see one more PostStateUpdate + // call per already-active request. + h.intermediatePersist.ForcePersist = true + + if h.shouldPersist() { + err := mgrPersist.PersistState(h.Schemas) + if err != nil { + // This hook can't affect Terraform Core's ongoing behavior, + // but it's a best effort thing anyway so we'll just emit a + // log to aid with debugging. + log.Printf("[ERROR] Failed to persist state after interruption: %s", err) + } + } else { + log.Printf("[DEBUG] State storage %T declined to persist a state snapshot", h.StateMgr) + } + } + +} + +func (h *StateHook) shouldPersist() bool { + if m, ok := h.StateMgr.(statemgr.IntermediateStateConditionalPersister); ok { + return m.ShouldPersistIntermediateState(&h.intermediatePersist) + } + return statemgr.DefaultIntermediateStatePersistRule(&h.intermediatePersist) +} diff --git a/internal/backend/local/hook_state_test.go b/internal/backend/local/hook_state_test.go index 6e86ac728f..886e57d67c 100644 --- a/internal/backend/local/hook_state_test.go +++ b/internal/backend/local/hook_state_test.go @@ -1,8 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( + "fmt" "testing" + "time" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/schemarepo" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" ) @@ -27,3 +35,264 @@ func TestStateHook(t *testing.T) { t.Fatalf("bad state: %#v", is.State()) } } + +func TestStateHookStopping(t *testing.T) { + is := &testPersistentState{} + hook := &StateHook{ + StateMgr: is, + Schemas: &schemarepo.Schemas{}, + PersistInterval: 4 * time.Hour, + intermediatePersist: statemgr.IntermediateStatePersistInfo{ + LastPersist: time.Now(), + }, + } + + s := statemgr.TestFullInitialState() + action, err := hook.PostStateUpdate(s) + if err != nil { + t.Fatalf("unexpected error from PostStateUpdate: %s", err) + } + if got, want := action, terraform.HookActionContinue; got != want { + t.Fatalf("wrong hookaction %#v; want %#v", got, want) + } + if is.Written == nil || !is.Written.Equal(s) { + t.Fatalf("mismatching state written") + } + if is.Persisted != nil { + t.Fatalf("persisted too soon") + } + + // We'll now force lastPersist to be long enough ago that persisting + // should be due on the next call. + hook.intermediatePersist.LastPersist = time.Now().Add(-5 * time.Hour) + hook.PostStateUpdate(s) + if is.Written == nil || !is.Written.Equal(s) { + t.Fatalf("mismatching state written") + } + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + hook.PostStateUpdate(s) + if is.Written == nil || !is.Written.Equal(s) { + t.Fatalf("mismatching state written") + } + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + + gotLog := is.CallLog + wantLog := []string{ + // Initial call before we reset lastPersist + "WriteState", + + // Write and then persist after we reset lastPersist + "WriteState", + "PersistState", + + // Final call when persisting wasn't due yet. + "WriteState", + } + if diff := cmp.Diff(wantLog, gotLog); diff != "" { + t.Fatalf("wrong call log so far\n%s", diff) + } + + // We'll reset the log now before we try seeing what happens after + // we use "Stopped". + is.CallLog = is.CallLog[:0] + is.Persisted = nil + + hook.Stopping() + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + + is.Persisted = nil + hook.PostStateUpdate(s) + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + is.Persisted = nil + hook.PostStateUpdate(s) + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + + gotLog = is.CallLog + wantLog = []string{ + // "Stopping" immediately persisted + "PersistState", + + // PostStateUpdate then writes and persists on every call, + // on the assumption that we're now bailing out after + // being cancelled and trying to save as much state as we can. + "WriteState", + "PersistState", + "WriteState", + "PersistState", + } + if diff := cmp.Diff(wantLog, gotLog); diff != "" { + t.Fatalf("wrong call log once in stopping mode\n%s", diff) + } +} + +func TestStateHookCustomPersistRule(t *testing.T) { + is := &testPersistentStateThatRefusesToPersist{} + hook := &StateHook{ + StateMgr: is, + Schemas: &schemarepo.Schemas{}, + PersistInterval: 4 * time.Hour, + intermediatePersist: statemgr.IntermediateStatePersistInfo{ + LastPersist: time.Now(), + }, + } + + s := statemgr.TestFullInitialState() + action, err := hook.PostStateUpdate(s) + if err != nil { + t.Fatalf("unexpected error from PostStateUpdate: %s", err) + } + if got, want := action, terraform.HookActionContinue; got != want { + t.Fatalf("wrong hookaction %#v; want %#v", got, want) + } + if is.Written == nil || !is.Written.Equal(s) { + t.Fatalf("mismatching state written") + } + if is.Persisted != nil { + t.Fatalf("persisted too soon") + } + + // We'll now force lastPersist to be long enough ago that persisting + // should be due on the next call. + hook.intermediatePersist.LastPersist = time.Now().Add(-5 * time.Hour) + hook.PostStateUpdate(s) + if is.Written == nil || !is.Written.Equal(s) { + t.Fatalf("mismatching state written") + } + if is.Persisted != nil { + t.Fatalf("has a persisted state, but shouldn't") + } + hook.PostStateUpdate(s) + if is.Written == nil || !is.Written.Equal(s) { + t.Fatalf("mismatching state written") + } + if is.Persisted != nil { + t.Fatalf("has a persisted state, but shouldn't") + } + + gotLog := is.CallLog + wantLog := []string{ + // Initial call before we reset lastPersist + "WriteState", + "ShouldPersistIntermediateState", + // Previous call should return false, preventing a "PersistState" call + + // Write and then decline to persist + "WriteState", + "ShouldPersistIntermediateState", + // Previous call should return false, preventing a "PersistState" call + + // Final call before we start "stopping". + "WriteState", + "ShouldPersistIntermediateState", + // Previous call should return false, preventing a "PersistState" call + } + if diff := cmp.Diff(wantLog, gotLog); diff != "" { + t.Fatalf("wrong call log so far\n%s", diff) + } + + // We'll reset the log now before we try seeing what happens after + // we use "Stopped". + is.CallLog = is.CallLog[:0] + is.Persisted = nil + + hook.Stopping() + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + + is.Persisted = nil + hook.PostStateUpdate(s) + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + is.Persisted = nil + hook.PostStateUpdate(s) + if is.Persisted == nil || !is.Persisted.Equal(s) { + t.Fatalf("mismatching state persisted") + } + + gotLog = is.CallLog + wantLog = []string{ + "ShouldPersistIntermediateState", + // Previous call should return true, allowing the following "PersistState" call + "PersistState", + "WriteState", + "ShouldPersistIntermediateState", + // Previous call should return true, allowing the following "PersistState" call + "PersistState", + "WriteState", + "ShouldPersistIntermediateState", + // Previous call should return true, allowing the following "PersistState" call + "PersistState", + } + if diff := cmp.Diff(wantLog, gotLog); diff != "" { + t.Fatalf("wrong call log once in stopping mode\n%s", diff) + } +} + +type testPersistentState struct { + CallLog []string + + Written *states.State + Persisted *states.State +} + +var _ statemgr.Writer = (*testPersistentState)(nil) +var _ statemgr.Persister = (*testPersistentState)(nil) + +func (sm *testPersistentState) WriteState(state *states.State) error { + sm.CallLog = append(sm.CallLog, "WriteState") + sm.Written = state + return nil +} + +func (sm *testPersistentState) PersistState(schemas *schemarepo.Schemas) error { + if schemas == nil { + return fmt.Errorf("no schemas") + } + sm.CallLog = append(sm.CallLog, "PersistState") + sm.Persisted = sm.Written + return nil +} + +type testPersistentStateThatRefusesToPersist struct { + CallLog []string + + Written *states.State + Persisted *states.State +} + +var _ statemgr.Writer = (*testPersistentStateThatRefusesToPersist)(nil) +var _ statemgr.Persister = (*testPersistentStateThatRefusesToPersist)(nil) +var _ statemgr.IntermediateStateConditionalPersister = (*testPersistentStateThatRefusesToPersist)(nil) + +func (sm *testPersistentStateThatRefusesToPersist) WriteState(state *states.State) error { + sm.CallLog = append(sm.CallLog, "WriteState") + sm.Written = state + return nil +} + +func (sm *testPersistentStateThatRefusesToPersist) PersistState(schemas *schemarepo.Schemas) error { + if schemas == nil { + return fmt.Errorf("no schemas") + } + sm.CallLog = append(sm.CallLog, "PersistState") + sm.Persisted = sm.Written + return nil +} + +// ShouldPersistIntermediateState implements IntermediateStateConditionalPersister +func (sm *testPersistentStateThatRefusesToPersist) ShouldPersistIntermediateState(info *statemgr.IntermediateStatePersistInfo) bool { + sm.CallLog = append(sm.CallLog, "ShouldPersistIntermediateState") + return info.ForcePersist +} diff --git a/internal/backend/local/local_test.go b/internal/backend/local/local_test.go index e447921e09..2feb0da0d1 100644 --- a/internal/backend/local/local_test.go +++ b/internal/backend/local/local_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( diff --git a/internal/backend/local/test.go b/internal/backend/local/test.go new file mode 100644 index 0000000000..1a8ff7a177 --- /dev/null +++ b/internal/backend/local/test.go @@ -0,0 +1,362 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package local + +import ( + "context" + "fmt" + "log" + "path/filepath" + "slices" + + "github.com/zclconf/go-cty/cty" + + "maps" + + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/junit" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/moduletest/graph" + hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type TestSuiteRunner struct { + Config *configs.Config + + TestingDirectory string + + // Global variables comes from the main configuration directory, + // and the Global Test Variables are loaded from the test directory. + GlobalVariables map[string]backendrun.UnparsedVariableValue + GlobalTestVariables map[string]backendrun.UnparsedVariableValue + + Opts *terraform.ContextOpts + + View views.Test + JUnit junit.JUnit + + // Stopped and Cancelled track whether the user requested the testing + // process to be interrupted. Stopped is a nice graceful exit, we'll still + // tidy up any state that was created and mark the tests with relevant + // `skipped` status updates. Cancelled is a hard stop right now exit, we + // won't attempt to clean up any state left hanging, and tests will just + // be left showing `pending` as the status. We will still print out the + // destroy summary diagnostics that tell the user what state has been left + // behind and needs manual clean up. + Stopped bool + Cancelled bool + + // StoppedCtx and CancelledCtx allow in progress Terraform operations to + // respond to external calls from the test command. + StoppedCtx context.Context + CancelledCtx context.Context + + // Filter restricts exactly which test files will be executed. + Filter []string + + // Verbose tells the runner to print out plan files during each test run. + Verbose bool + + Concurrency int + semaphore terraform.Semaphore +} + +func (runner *TestSuiteRunner) Stop() { + runner.Stopped = true +} + +func (runner *TestSuiteRunner) IsStopped() bool { + return runner.Stopped +} + +func (runner *TestSuiteRunner) Cancel() { + runner.Cancelled = true +} + +func (runner *TestSuiteRunner) Test() (moduletest.Status, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if runner.Concurrency < 1 { + runner.Concurrency = 10 + } + runner.semaphore = terraform.NewSemaphore(runner.Concurrency) + + suite, suiteDiags := runner.collectTests() + diags = diags.Append(suiteDiags) + if suiteDiags.HasErrors() { + return moduletest.Error, diags + } + + runner.View.Abstract(suite) + + // We have two sets of variables that are available to different test files. + // Test files in the root directory have access to the GlobalVariables only, + // while test files in the test directory have access to the union of + // GlobalVariables and GlobalTestVariables. + testDirectoryGlobalVariables := make(map[string]backendrun.UnparsedVariableValue) + maps.Copy(testDirectoryGlobalVariables, runner.GlobalVariables) + // We're okay to overwrite the global variables in case of name + // collisions, as the test directory variables should take precedence. + maps.Copy(testDirectoryGlobalVariables, runner.GlobalTestVariables) + + suite.Status = moduletest.Pass + for _, name := range slices.Sorted(maps.Keys(suite.Files)) { + if runner.Cancelled { + return suite.Status, diags + } + + file := suite.Files[name] + evalCtx := graph.NewEvalContext(&graph.EvalContextOpts{ + CancelCtx: runner.CancelledCtx, + StopCtx: runner.StoppedCtx, + Verbose: runner.Verbose, + Render: runner.View, + }) + + for _, run := range file.Runs { + // Pre-initialise the prior outputs, so we can easily tell between + // a run block that doesn't exist and a run block that hasn't been + // executed yet. + // (moduletest.EvalContext treats cty.NilVal as "not visited yet") + evalCtx.SetOutput(run, cty.NilVal) + } + + currentGlobalVariables := runner.GlobalVariables + if filepath.Dir(file.Name) == runner.TestingDirectory { + // If the file is in the test directory, we'll use the union of the + // global variables and the global test variables. + currentGlobalVariables = testDirectoryGlobalVariables + } + + evalCtx.VariableCaches = hcltest.NewVariableCaches(func(vc *hcltest.VariableCaches) { + maps.Copy(vc.GlobalVariables, currentGlobalVariables) + vc.FileVariables = file.Config.Variables + }) + fileRunner := &TestFileRunner{ + Suite: runner, + EvalContext: evalCtx, + } + + runner.View.File(file, moduletest.Starting) + fileRunner.Test(file) + runner.View.File(file, moduletest.Complete) + suite.Status = suite.Status.Merge(file.Status) + } + + runner.View.Conclusion(suite) + + if runner.JUnit != nil { + artifactDiags := runner.JUnit.Save(suite) + diags = diags.Append(artifactDiags) + if artifactDiags.HasErrors() { + return moduletest.Error, diags + } + } + + return suite.Status, diags +} + +func (runner *TestSuiteRunner) collectTests() (*moduletest.Suite, tfdiags.Diagnostics) { + runCount := 0 + fileCount := 0 + + var diags tfdiags.Diagnostics + suite := &moduletest.Suite{ + Files: func() map[string]*moduletest.File { + files := make(map[string]*moduletest.File) + + if len(runner.Filter) > 0 { + for _, name := range runner.Filter { + file, ok := runner.Config.Module.Tests[name] + if !ok { + // If the filter is invalid, we'll simply skip this + // entry and print a warning. But we could still execute + // any other tests within the filter. + diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Unknown test file", + fmt.Sprintf("The specified test file, %s, could not be found.", name))) + continue + } + + fileCount++ + + var runs []*moduletest.Run + for ix, run := range file.Runs { + config := runner.Config + if run.ConfigUnderTest != nil { + config = run.ConfigUnderTest + } + runs = append(runs, moduletest.NewRun(run, config, ix)) + + } + + runCount += len(runs) + files[name] = moduletest.NewFile(name, file, runs) + } + + return files + } + + // Otherwise, we'll just do all the tests in the directory! + for name, file := range runner.Config.Module.Tests { + fileCount++ + + var runs []*moduletest.Run + for ix, run := range file.Runs { + config := runner.Config + if run.ConfigUnderTest != nil { + config = run.ConfigUnderTest + } + runs = append(runs, moduletest.NewRun(run, config, ix)) + } + + runCount += len(runs) + files[name] = moduletest.NewFile(name, file, runs) + } + return files + }(), + } + + log.Printf("[DEBUG] TestSuiteRunner: found %d files with %d run blocks", fileCount, runCount) + + return suite, diags +} + +type TestFileRunner struct { + // Suite contains all the helpful metadata about the test that we need + // during the execution of a file. + Suite *TestSuiteRunner + EvalContext *graph.EvalContext +} + +func (runner *TestFileRunner) Test(file *moduletest.File) { + log.Printf("[TRACE] TestFileRunner: executing test file %s", file.Name) + + // The file validation only returns warnings so we'll just add them without + // checking anything about them. + file.Diagnostics = file.Diagnostics.Append(file.Config.Validate(runner.Suite.Config)) + + // We'll execute the tests in the file. First, mark the overall status as + // being skipped. This will ensure that if we've cancelled and the files not + // going to do anything it'll be marked as skipped. + file.Status = file.Status.Merge(moduletest.Skip) + if len(file.Runs) == 0 { + // If we have zero run blocks then we'll just mark the file as passed. + file.Status = file.Status.Merge(moduletest.Pass) + return + } + + // Build the graph for the file. + b := graph.TestGraphBuilder{ + File: file, + GlobalVars: runner.EvalContext.VariableCaches.GlobalVariables, + ContextOpts: runner.Suite.Opts, + } + g, diags := b.Build() + file.Diagnostics = file.Diagnostics.Append(diags) + if walkCancelled := runner.renderPreWalkDiags(file); walkCancelled { + return + } + + // walk and execute the graph + diags = runner.walkGraph(g) + + // If the graph walk was terminated, we don't want to add the diagnostics. + // The error the user receives will just be: + // Failure! 0 passed, 1 failed. + // exit status 1 + if runner.EvalContext.Cancelled() { + file.UpdateStatus(moduletest.Error) + log.Printf("[TRACE] TestFileRunner: graph walk terminated for %s", file.Name) + return + } + + file.Diagnostics = file.Diagnostics.Append(diags) +} + +// walkGraph goes through the graph and execute each run it finds. +func (runner *TestFileRunner) walkGraph(g *terraform.Graph) tfdiags.Diagnostics { + sem := runner.Suite.semaphore + + // Walk the graph. + walkFn := func(v dag.Vertex) (diags tfdiags.Diagnostics) { + if runner.EvalContext.Cancelled() { + // If the graph walk has been cancelled, the node should just return immediately. + // For now, this means a hard stop has been requested, in this case we don't + // even stop to mark future test runs as having been skipped. They'll + // just show up as pending in the printed summary. We will quickly + // just mark the overall file status has having errored to indicate + // it was interrupted. + return + } + + // the walkFn is called asynchronously, and needs to be recovered + // separately in the case of a panic. + defer logging.PanicHandler() + + log.Printf("[TRACE] vertex %q: starting visit (%T)", dag.VertexName(v), v) + + defer func() { + if r := recover(); r != nil { + // If the walkFn panics, we get confusing logs about how the + // visit was complete. To stop this, we'll catch the panic log + // that the vertex panicked without finishing and re-panic. + log.Printf("[ERROR] vertex %q panicked", dag.VertexName(v)) + panic(r) // re-panic + } + + if diags.HasErrors() { + for _, diag := range diags { + if diag.Severity() == tfdiags.Error { + desc := diag.Description() + log.Printf("[ERROR] vertex %q error: %s", dag.VertexName(v), desc.Summary) + } + } + log.Printf("[TRACE] vertex %q: visit complete, with errors", dag.VertexName(v)) + } else { + log.Printf("[TRACE] vertex %q: visit complete", dag.VertexName(v)) + } + }() + + // Acquire a lock on the semaphore + sem.Acquire() + defer sem.Release() + + if executable, ok := v.(graph.GraphNodeExecutable); ok { + diags = executable.Execute(runner.EvalContext) + } + return + } + + return g.AcyclicGraph.Walk(walkFn) +} + +func (runner *TestFileRunner) renderPreWalkDiags(file *moduletest.File) (walkCancelled bool) { + errored := file.Diagnostics.HasErrors() + // Some runs may have errored during the graph build, but we didn't fail immediately + // as we still wanted to gather all the diagnostics. + // Now we go through the runs and if there are any errors, we'll update the + // file status to be errored. + for _, run := range file.Runs { + if run.Status == moduletest.Error { + errored = true + runner.Suite.View.Run(run, file, moduletest.Complete, 0) + } + } + if errored { + // print a teardown message even though there was no teardown to run + runner.Suite.View.File(file, moduletest.TearDown) + file.Status = file.Status.Merge(moduletest.Error) + return true + } + + return false +} diff --git a/internal/backend/local/testdata/apply-check/main.tf b/internal/backend/local/testdata/apply-check/main.tf new file mode 100644 index 0000000000..5782be8f47 --- /dev/null +++ b/internal/backend/local/testdata/apply-check/main.tf @@ -0,0 +1,10 @@ +resource "test_instance" "foo" { + ami = "bar" +} + +check "test_instance_exists" { + assert { + condition = test_instance.foo.id != null + error_message = "value should have been computed" + } +} diff --git a/internal/backend/local/testdata/plan-bookmark/bookmark.json b/internal/backend/local/testdata/plan-bookmark/bookmark.json new file mode 100644 index 0000000000..0a1c73302a --- /dev/null +++ b/internal/backend/local/testdata/plan-bookmark/bookmark.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 1, + "run_id": "run-GXfuHMkbyHccAGUg", + "hostname": "app.terraform.io" +} diff --git a/internal/backend/local/testing.go b/internal/backend/local/testing.go index 3b9a3c40fb..2b3a4926f6 100644 --- a/internal/backend/local/testing.go +++ b/internal/backend/local/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package local import ( @@ -10,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" @@ -39,28 +43,11 @@ func TestLocal(t *testing.T) *Local { // TestLocalProvider modifies the ContextOpts of the *Local parameter to // have a provider with the given name. -func TestLocalProvider(t *testing.T, b *Local, name string, schema *terraform.ProviderSchema) *terraform.MockProvider { +func TestLocalProvider(t *testing.T, b *Local, name string, schema providers.ProviderSchema) *testing_provider.MockProvider { // Build a mock resource provider for in-memory operations - p := new(terraform.MockProvider) + p := new(testing_provider.MockProvider) - if schema == nil { - schema = &terraform.ProviderSchema{} // default schema is empty - } - p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: schema.Provider}, - ProviderMeta: providers.Schema{Block: schema.ProviderMeta}, - ResourceTypes: map[string]providers.Schema{}, - DataSources: map[string]providers.Schema{}, - } - for name, res := range schema.ResourceTypes { - p.GetProviderSchemaResponse.ResourceTypes[name] = providers.Schema{ - Block: res, - Version: int64(schema.ResourceTypeSchemaVersions[name]), - } - } - for name, dat := range schema.DataSources { - p.GetProviderSchemaResponse.DataSources[name] = providers.Schema{Block: dat} - } + p.GetProviderSchemaResponse = &schema p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { // this is a destroy plan, @@ -70,19 +57,19 @@ func TestLocalProvider(t *testing.T, b *Local, name string, schema *terraform.Pr return resp } - rSchema, _ := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName) - if rSchema == nil { - rSchema = &configschema.Block{} // default schema is empty + rSchema := schema.SchemaForResourceType(addrs.ManagedResourceMode, req.TypeName) + if rSchema.Body == nil { + rSchema.Body = &configschema.Block{} // default schema is empty } plannedVals := map[string]cty.Value{} - for name, attrS := range rSchema.Attributes { + for name, attrS := range rSchema.Body.Attributes { val := req.ProposedNewState.GetAttr(name) if attrS.Computed && val.IsNull() { val = cty.UnknownVal(attrS.Type) } plannedVals[name] = val } - for name := range rSchema.BlockTypes { + for name := range rSchema.Body.BlockTypes { // For simplicity's sake we just copy the block attributes over // verbatim, since this package's mock providers are all relatively // simple -- we're testing the backend, not esoteric provider features. @@ -112,7 +99,6 @@ func TestLocalProvider(t *testing.T, b *Local, name string, schema *terraform.Pr } return p - } // TestLocalSingleState is a backend implementation that wraps Local @@ -135,7 +121,7 @@ func (b *TestLocalSingleState) Workspaces() ([]string, error) { return nil, backend.ErrWorkspacesNotSupported } -func (b *TestLocalSingleState) DeleteWorkspace(string) error { +func (b *TestLocalSingleState) DeleteWorkspace(string, bool) error { return backend.ErrWorkspacesNotSupported } @@ -177,11 +163,11 @@ func (b *TestLocalNoDefaultState) Workspaces() ([]string, error) { return filtered, nil } -func (b *TestLocalNoDefaultState) DeleteWorkspace(name string) error { +func (b *TestLocalNoDefaultState) DeleteWorkspace(name string, force bool) error { if name == backend.DefaultStateName { return backend.ErrDefaultWorkspaceNotSupported } - return b.Local.DeleteWorkspace(name) + return b.Local.DeleteWorkspace(name, force) } func (b *TestLocalNoDefaultState) StateMgr(name string) (statemgr.Full, error) { diff --git a/internal/backend/remote-state/artifactory/backend.go b/internal/backend/remote-state/artifactory/backend.go deleted file mode 100644 index bf2bfcf7e5..0000000000 --- a/internal/backend/remote-state/artifactory/backend.go +++ /dev/null @@ -1,102 +0,0 @@ -package artifactory - -import ( - "context" - - cleanhttp "github.com/hashicorp/go-cleanhttp" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" - artifactory "github.com/lusis/go-artifactory/src/artifactory.v401" -) - -func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "username": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("ARTIFACTORY_USERNAME", nil), - Description: "Username", - }, - "password": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("ARTIFACTORY_PASSWORD", nil), - Description: "Password", - }, - "url": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("ARTIFACTORY_URL", nil), - Description: "Artfactory base URL", - }, - "repo": &schema.Schema{ - Type: schema.TypeString, - Required: true, - Description: "The repository name", - }, - "subpath": &schema.Schema{ - Type: schema.TypeString, - Required: true, - Description: "Path within the repository", - }, - }, - } - - b := &Backend{Backend: s} - b.Backend.ConfigureFunc = b.configure - return b -} - -type Backend struct { - *schema.Backend - - client *ArtifactoryClient -} - -func (b *Backend) configure(ctx context.Context) error { - data := schema.FromContextBackendConfig(ctx) - - userName := data.Get("username").(string) - password := data.Get("password").(string) - url := data.Get("url").(string) - repo := data.Get("repo").(string) - subpath := data.Get("subpath").(string) - - clientConf := &artifactory.ClientConfig{ - BaseURL: url, - Username: userName, - Password: password, - Transport: cleanhttp.DefaultPooledTransport(), - } - nativeClient := artifactory.NewClient(clientConf) - - b.client = &ArtifactoryClient{ - nativeClient: &nativeClient, - userName: userName, - password: password, - url: url, - repo: repo, - subpath: subpath, - } - return nil -} - -func (b *Backend) Workspaces() ([]string, error) { - return nil, backend.ErrWorkspacesNotSupported -} - -func (b *Backend) DeleteWorkspace(string) error { - return backend.ErrWorkspacesNotSupported -} - -func (b *Backend) StateMgr(name string) (statemgr.Full, error) { - if name != backend.DefaultStateName { - return nil, backend.ErrWorkspacesNotSupported - } - return &remote.State{ - Client: b.client, - }, nil -} diff --git a/internal/backend/remote-state/artifactory/client.go b/internal/backend/remote-state/artifactory/client.go deleted file mode 100644 index 672e1b7f46..0000000000 --- a/internal/backend/remote-state/artifactory/client.go +++ /dev/null @@ -1,63 +0,0 @@ -package artifactory - -import ( - "crypto/md5" - "fmt" - "strings" - - "github.com/hashicorp/terraform/internal/states/remote" - artifactory "github.com/lusis/go-artifactory/src/artifactory.v401" -) - -const ARTIF_TFSTATE_NAME = "terraform.tfstate" - -type ArtifactoryClient struct { - nativeClient *artifactory.ArtifactoryClient - userName string - password string - url string - repo string - subpath string -} - -func (c *ArtifactoryClient) Get() (*remote.Payload, error) { - p := fmt.Sprintf("%s/%s/%s", c.repo, c.subpath, ARTIF_TFSTATE_NAME) - output, err := c.nativeClient.Get(p, make(map[string]string)) - if err != nil { - if strings.Contains(err.Error(), "404") { - return nil, nil - } - return nil, err - } - - // TODO: migrate to using X-Checksum-Md5 header from artifactory - // needs to be exposed by go-artifactory first - - hash := md5.Sum(output) - payload := &remote.Payload{ - Data: output, - MD5: hash[:md5.Size], - } - - // If there was no data, then return nil - if len(payload.Data) == 0 { - return nil, nil - } - - return payload, nil -} - -func (c *ArtifactoryClient) Put(data []byte) error { - p := fmt.Sprintf("%s/%s/%s", c.repo, c.subpath, ARTIF_TFSTATE_NAME) - if _, err := c.nativeClient.Put(p, string(data), make(map[string]string)); err == nil { - return nil - } else { - return fmt.Errorf("Failed to upload state: %v", err) - } -} - -func (c *ArtifactoryClient) Delete() error { - p := fmt.Sprintf("%s/%s/%s", c.repo, c.subpath, ARTIF_TFSTATE_NAME) - err := c.nativeClient.Delete(p) - return err -} diff --git a/internal/backend/remote-state/artifactory/client_test.go b/internal/backend/remote-state/artifactory/client_test.go deleted file mode 100644 index d34aff5388..0000000000 --- a/internal/backend/remote-state/artifactory/client_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package artifactory - -import ( - "testing" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/zclconf/go-cty/cty" -) - -func TestArtifactoryClient_impl(t *testing.T) { - var _ remote.Client = new(ArtifactoryClient) -} - -func TestArtifactoryFactory(t *testing.T) { - // This test just instantiates the client. Shouldn't make any actual - // requests nor incur any costs. - - config := make(map[string]cty.Value) - config["url"] = cty.StringVal("http://artifactory.local:8081/artifactory") - config["repo"] = cty.StringVal("terraform-repo") - config["subpath"] = cty.StringVal("myproject") - - // For this test we'll provide the credentials as config. The - // acceptance tests implicitly test passing credentials as - // environment variables. - config["username"] = cty.StringVal("test") - config["password"] = cty.StringVal("testpass") - - b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", config)) - - state, err := b.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatalf("Error for valid config: %s", err) - } - - artifactoryClient := state.(*remote.State).Client.(*ArtifactoryClient) - - if artifactoryClient.nativeClient.Config.BaseURL != "http://artifactory.local:8081/artifactory" { - t.Fatalf("Incorrect url was populated") - } - if artifactoryClient.nativeClient.Config.Username != "test" { - t.Fatalf("Incorrect username was populated") - } - if artifactoryClient.nativeClient.Config.Password != "testpass" { - t.Fatalf("Incorrect password was populated") - } - if artifactoryClient.repo != "terraform-repo" { - t.Fatalf("Incorrect repo was populated") - } - if artifactoryClient.subpath != "myproject" { - t.Fatalf("Incorrect subpath was populated") - } -} diff --git a/internal/backend/remote-state/azure/api_client.go b/internal/backend/remote-state/azure/api_client.go new file mode 100644 index 0000000000..cbf2d3d964 --- /dev/null +++ b/internal/backend/remote-state/azure/api_client.go @@ -0,0 +1,284 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package azure + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/storageaccounts" + "github.com/hashicorp/go-azure-sdk/sdk/auth" + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/environments" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/version" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/blobs" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/containers" +) + +type Client struct { + environment environments.Environment + storageAccountName string + + // Storage ARM client is used for looking up the blob endpoint, or/and listing access key (if not specified). + storageAccountsClient *storageaccounts.StorageAccountsClient + // This is only non-nil if the config has specified to lookup the blob endpoint + accountDetail *AccountDetails + + // Caching + containersClient *containers.Client + blobsClient *blobs.Client + + // Only one of them shall be specified + accessKey string + sasToken string + azureAdStorageAuth auth.Authorizer +} + +func buildClient(ctx context.Context, config BackendConfig) (*Client, error) { + client := Client{ + environment: config.AuthConfig.Environment, + storageAccountName: config.StorageAccountName, + } + + var armAuthRequired bool + switch { + case config.AccessKey != "": + client.accessKey = config.AccessKey + case config.SasToken != "": + sasToken := config.SasToken + if strings.TrimSpace(sasToken) == "" { + return nil, fmt.Errorf("sasToken cannot be empty") + } + client.sasToken = strings.TrimPrefix(sasToken, "?") + case config.UseAzureADAuthentication: + var err error + client.azureAdStorageAuth, err = auth.NewAuthorizerFromCredentials(ctx, *config.AuthConfig, config.AuthConfig.Environment.Storage) + if err != nil { + return nil, fmt.Errorf("unable to build authorizer for Storage API: %+v", err) + } + default: + // AAD authentication (ARM scope) is required only when no auth method is specified, which falls back to listing the access key via ARM API. + armAuthRequired = true + } + + // If `config.LookupBlobEndpoint` is true, we need to authenticate with ARM to lookup the blob endpoint + if config.LookupBlobEndpoint { + armAuthRequired = true + } + + if armAuthRequired { + resourceManagerAuth, err := auth.NewAuthorizerFromCredentials(ctx, *config.AuthConfig, config.AuthConfig.Environment.ResourceManager) + if err != nil { + return nil, fmt.Errorf("unable to build authorizer for Resource Manager API: %+v", err) + } + + // When using Azure CLI to auth, the user can leave the "subscription_id" unspecified. In this case the subscription id is inferred from + // the Azure CLI default subscription. + if config.SubscriptionID == "" { + if cachedAuth, ok := resourceManagerAuth.(*auth.CachedAuthorizer); ok { + if cliAuth, ok := cachedAuth.Source.(*auth.AzureCliAuthorizer); ok && cliAuth.DefaultSubscriptionID != "" { + config.SubscriptionID = cliAuth.DefaultSubscriptionID + } + } + } + if config.SubscriptionID == "" { + return nil, fmt.Errorf("subscription id not specified") + } + + // Setup the SA client. + client.storageAccountsClient, err = storageaccounts.NewStorageAccountsClientWithBaseURI(config.AuthConfig.Environment.ResourceManager) + if err != nil { + return nil, fmt.Errorf("building Storage Accounts client: %+v", err) + } + client.configureClient(client.storageAccountsClient.Client, resourceManagerAuth) + + // Populating the storage account detail + storageAccountId := commonids.NewStorageAccountID(config.SubscriptionID, config.ResourceGroupName, client.storageAccountName) + resp, err := client.storageAccountsClient.GetProperties(ctx, storageAccountId, storageaccounts.DefaultGetPropertiesOperationOptions()) + if err != nil { + return nil, fmt.Errorf("retrieving %s: %+v", storageAccountId, err) + } + if resp.Model == nil { + return nil, fmt.Errorf("retrieving %s: model was nil", storageAccountId) + } + client.accountDetail, err = populateAccountDetails(storageAccountId, *resp.Model) + if err != nil { + return nil, fmt.Errorf("populating details for %s: %+v", storageAccountId, err) + } + } + + return &client, nil +} + +func (c *Client) getBlobClient(ctx context.Context) (bc *blobs.Client, err error) { + if c.blobsClient != nil { + return c.blobsClient, nil + } + + defer func() { + if err == nil { + c.blobsClient = bc + } + }() + + var baseUri string + if c.accountDetail != nil { + // Use the actual blob endpoint if available + pBaseUri, err := c.accountDetail.DataPlaneEndpoint(EndpointTypeBlob) + if err != nil { + return nil, err + } + baseUri = *pBaseUri + } else { + baseUri, err = naiveStorageAccountBlobBaseURL(c.environment, c.storageAccountName) + if err != nil { + return nil, err + } + } + + blobsClient, err := blobs.NewWithBaseUri(baseUri) + if err != nil { + return nil, fmt.Errorf("new blob client: %v", err) + } + + switch { + case c.sasToken != "": + log.Printf("[DEBUG] Building the Blob Client from a SAS Token") + c.configureClient(blobsClient.Client, nil) + blobsClient.Client.AppendRequestMiddleware(func(r *http.Request) (*http.Request, error) { + if r.URL.RawQuery == "" { + r.URL.RawQuery = c.sasToken + } else if !strings.Contains(r.URL.RawQuery, c.sasToken) { + r.URL.RawQuery = fmt.Sprintf("%s&%s", r.URL.RawQuery, c.sasToken) + } + return r, nil + }) + return blobsClient, nil + + case c.accessKey != "": + log.Printf("[DEBUG] Building the Blob Client from an Access Key") + authorizer, err := auth.NewSharedKeyAuthorizer(c.storageAccountName, c.accessKey, auth.SharedKey) + if err != nil { + return nil, fmt.Errorf("new shared key authorizer: %v", err) + } + c.configureClient(blobsClient.Client, authorizer) + return blobsClient, nil + + case c.azureAdStorageAuth != nil: + log.Printf("[DEBUG] Building the Blob Client from AAD auth") + c.configureClient(blobsClient.Client, c.azureAdStorageAuth) + return blobsClient, nil + + default: + // Neither shared access key, sas token, or AAD Auth were specified so we have to call the management plane API to get the key. + log.Printf("[DEBUG] Building the Blob Client from an Access Key (key is listed using client credentials)") + key, err := c.accountDetail.AccountKey(ctx, c.storageAccountsClient) + if err != nil { + return nil, fmt.Errorf("retrieving key for Storage Account %q: %s", c.storageAccountName, err) + } + authorizer, err := auth.NewSharedKeyAuthorizer(c.storageAccountName, *key, auth.SharedKey) + if err != nil { + return nil, fmt.Errorf("new shared key authorizer: %v", err) + } + c.configureClient(blobsClient.Client, authorizer) + return blobsClient, nil + } +} + +func (c *Client) getContainersClient(ctx context.Context) (cc *containers.Client, err error) { + if c.containersClient != nil { + return c.containersClient, nil + } + + defer func() { + if err == nil { + c.containersClient = cc + } + }() + + var baseUri string + if c.accountDetail != nil { + // Use the actual blob endpoint if available + pBaseUri, err := c.accountDetail.DataPlaneEndpoint(EndpointTypeBlob) + if err != nil { + return nil, err + } + baseUri = *pBaseUri + } else { + baseUri, err = naiveStorageAccountBlobBaseURL(c.environment, c.storageAccountName) + if err != nil { + return nil, err + } + } + + containersClient, err := containers.NewWithBaseUri(baseUri) + if err != nil { + return nil, fmt.Errorf("new container client: %v", err) + } + + switch { + case c.sasToken != "": + log.Printf("[DEBUG] Building the Container Client from a SAS Token") + c.configureClient(containersClient.Client, nil) + containersClient.Client.AppendRequestMiddleware(func(r *http.Request) (*http.Request, error) { + if r.URL.RawQuery == "" { + r.URL.RawQuery = c.sasToken + } else if !strings.Contains(r.URL.RawQuery, c.sasToken) { + r.URL.RawQuery = fmt.Sprintf("%s&%s", r.URL.RawQuery, c.sasToken) + } + return r, nil + }) + return containersClient, nil + + case c.accessKey != "": + log.Printf("[DEBUG] Building the Container Client from an Access Key") + authorizer, err := auth.NewSharedKeyAuthorizer(c.storageAccountName, c.accessKey, auth.SharedKey) + if err != nil { + return nil, fmt.Errorf("new shared key authorizer: %v", err) + } + c.configureClient(containersClient.Client, authorizer) + return containersClient, nil + + case c.azureAdStorageAuth != nil: + log.Printf("[DEBUG] Building the Container Client from AAD auth") + c.configureClient(containersClient.Client, c.azureAdStorageAuth) + return containersClient, nil + + default: + // Neither shared access key, sas token, or AAD Auth were specified so we have to call the management plane API to get the key. + log.Printf("[DEBUG] Building the Container Client from an Access Key (key is listed using user credentials)") + key, err := c.accountDetail.AccountKey(ctx, c.storageAccountsClient) + if err != nil { + return nil, fmt.Errorf("retrieving key for Storage Account %q: %s", c.storageAccountName, err) + } + authorizer, err := auth.NewSharedKeyAuthorizer(c.storageAccountName, *key, auth.SharedKey) + if err != nil { + return nil, fmt.Errorf("new shared key authorizer: %v", err) + } + c.configureClient(containersClient.Client, authorizer) + return containersClient, nil + } +} + +func (c *Client) configureClient(client client.BaseClient, authorizer auth.Authorizer) { + client.SetAuthorizer(authorizer) + client.SetUserAgent(buildUserAgent(client.GetUserAgent())) +} + +func buildUserAgent(userAgent string) string { + userAgent = strings.TrimSpace(fmt.Sprintf("%s %s", userAgent, httpclient.TerraformUserAgent(version.Version))) + + // append the CloudShell version to the user agent if it exists + if azureAgent := os.Getenv("AZURE_HTTP_USER_AGENT"); azureAgent != "" { + userAgent = fmt.Sprintf("%s %s", userAgent, azureAgent) + } + + return userAgent +} diff --git a/internal/backend/remote-state/azure/arm_client.go b/internal/backend/remote-state/azure/arm_client.go deleted file mode 100644 index 13493ab13d..0000000000 --- a/internal/backend/remote-state/azure/arm_client.go +++ /dev/null @@ -1,244 +0,0 @@ -package azure - -import ( - "context" - "fmt" - "log" - "os" - "time" - - "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources" - armStorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-01-01/storage" - "github.com/Azure/go-autorest/autorest" - "github.com/Azure/go-autorest/autorest/azure" - "github.com/hashicorp/go-azure-helpers/authentication" - "github.com/hashicorp/go-azure-helpers/sender" - "github.com/hashicorp/terraform/internal/httpclient" - "github.com/hashicorp/terraform/version" - "github.com/manicminer/hamilton/environments" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" -) - -type ArmClient struct { - // These Clients are only initialized if an Access Key isn't provided - groupsClient *resources.GroupsClient - storageAccountsClient *armStorage.AccountsClient - containersClient *containers.Client - blobsClient *blobs.Client - - // azureAdStorageAuth is only here if we're using AzureAD Authentication but is an Authorizer for Storage - azureAdStorageAuth *autorest.Authorizer - - accessKey string - environment azure.Environment - resourceGroupName string - storageAccountName string - sasToken string -} - -func buildArmClient(ctx context.Context, config BackendConfig) (*ArmClient, error) { - env, err := authentication.AzureEnvironmentByNameFromEndpoint(ctx, config.MetadataHost, config.Environment) - if err != nil { - return nil, err - } - - client := ArmClient{ - environment: *env, - resourceGroupName: config.ResourceGroupName, - storageAccountName: config.StorageAccountName, - } - - // if we have an Access Key - we don't need the other clients - if config.AccessKey != "" { - client.accessKey = config.AccessKey - return &client, nil - } - - // likewise with a SAS token - if config.SasToken != "" { - client.sasToken = config.SasToken - return &client, nil - } - - builder := authentication.Builder{ - ClientID: config.ClientID, - SubscriptionID: config.SubscriptionID, - TenantID: config.TenantID, - CustomResourceManagerEndpoint: config.CustomResourceManagerEndpoint, - MetadataHost: config.MetadataHost, - Environment: config.Environment, - ClientSecretDocsLink: "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/service_principal_client_secret", - - // Service Principal (Client Certificate) - ClientCertPassword: config.ClientCertificatePassword, - ClientCertPath: config.ClientCertificatePath, - - // Service Principal (Client Secret) - ClientSecret: config.ClientSecret, - - // Managed Service Identity - MsiEndpoint: config.MsiEndpoint, - - // OIDC - IDTokenRequestURL: config.OIDCRequestURL, - IDTokenRequestToken: config.OIDCRequestToken, - - // Feature Toggles - SupportsAzureCliToken: true, - SupportsClientCertAuth: true, - SupportsClientSecretAuth: true, - SupportsManagedServiceIdentity: config.UseMsi, - SupportsOIDCAuth: config.UseOIDC, - UseMicrosoftGraph: true, - } - armConfig, err := builder.Build() - if err != nil { - return nil, fmt.Errorf("Error building ARM Config: %+v", err) - } - - oauthConfig, err := armConfig.BuildOAuthConfig(env.ActiveDirectoryEndpoint) - if err != nil { - return nil, err - } - - hamiltonEnv, err := environments.EnvironmentFromString(config.Environment) - if err != nil { - return nil, err - } - - sender := sender.BuildSender("backend/remote-state/azure") - log.Printf("[DEBUG] Obtaining an MSAL / Microsoft Graph token for Resource Manager..") - auth, err := armConfig.GetMSALToken(ctx, hamiltonEnv.ResourceManager, sender, oauthConfig, env.TokenAudience) - if err != nil { - return nil, err - } - - if config.UseAzureADAuthentication { - log.Printf("[DEBUG] Obtaining an MSAL / Microsoft Graph token for Storage..") - storageAuth, err := armConfig.GetMSALToken(ctx, hamiltonEnv.Storage, sender, oauthConfig, env.ResourceIdentifiers.Storage) - if err != nil { - return nil, err - } - client.azureAdStorageAuth = &storageAuth - } - - accountsClient := armStorage.NewAccountsClientWithBaseURI(env.ResourceManagerEndpoint, armConfig.SubscriptionID) - client.configureClient(&accountsClient.Client, auth) - client.storageAccountsClient = &accountsClient - - groupsClient := resources.NewGroupsClientWithBaseURI(env.ResourceManagerEndpoint, armConfig.SubscriptionID) - client.configureClient(&groupsClient.Client, auth) - client.groupsClient = &groupsClient - - return &client, nil -} - -func (c ArmClient) getBlobClient(ctx context.Context) (*blobs.Client, error) { - if c.sasToken != "" { - log.Printf("[DEBUG] Building the Blob Client from a SAS Token") - storageAuth, err := autorest.NewSASTokenAuthorizer(c.sasToken) - if err != nil { - return nil, fmt.Errorf("Error building Authorizer: %+v", err) - } - - blobsClient := blobs.NewWithEnvironment(c.environment) - c.configureClient(&blobsClient.Client, storageAuth) - return &blobsClient, nil - } - - if c.azureAdStorageAuth != nil { - blobsClient := blobs.NewWithEnvironment(c.environment) - c.configureClient(&blobsClient.Client, *c.azureAdStorageAuth) - return &blobsClient, nil - } - - accessKey := c.accessKey - if accessKey == "" { - log.Printf("[DEBUG] Building the Blob Client from an Access Token (using user credentials)") - keys, err := c.storageAccountsClient.ListKeys(ctx, c.resourceGroupName, c.storageAccountName, "") - if err != nil { - return nil, fmt.Errorf("Error retrieving keys for Storage Account %q: %s", c.storageAccountName, err) - } - - if keys.Keys == nil { - return nil, fmt.Errorf("Nil key returned for storage account %q", c.storageAccountName) - } - - accessKeys := *keys.Keys - accessKey = *accessKeys[0].Value - } - - storageAuth, err := autorest.NewSharedKeyAuthorizer(c.storageAccountName, accessKey, autorest.SharedKey) - if err != nil { - return nil, fmt.Errorf("Error building Authorizer: %+v", err) - } - - blobsClient := blobs.NewWithEnvironment(c.environment) - c.configureClient(&blobsClient.Client, storageAuth) - return &blobsClient, nil -} - -func (c ArmClient) getContainersClient(ctx context.Context) (*containers.Client, error) { - if c.sasToken != "" { - log.Printf("[DEBUG] Building the Container Client from a SAS Token") - storageAuth, err := autorest.NewSASTokenAuthorizer(c.sasToken) - if err != nil { - return nil, fmt.Errorf("Error building Authorizer: %+v", err) - } - - containersClient := containers.NewWithEnvironment(c.environment) - c.configureClient(&containersClient.Client, storageAuth) - return &containersClient, nil - } - - if c.azureAdStorageAuth != nil { - containersClient := containers.NewWithEnvironment(c.environment) - c.configureClient(&containersClient.Client, *c.azureAdStorageAuth) - return &containersClient, nil - } - - accessKey := c.accessKey - if accessKey == "" { - log.Printf("[DEBUG] Building the Container Client from an Access Token (using user credentials)") - keys, err := c.storageAccountsClient.ListKeys(ctx, c.resourceGroupName, c.storageAccountName, "") - if err != nil { - return nil, fmt.Errorf("Error retrieving keys for Storage Account %q: %s", c.storageAccountName, err) - } - - if keys.Keys == nil { - return nil, fmt.Errorf("Nil key returned for storage account %q", c.storageAccountName) - } - - accessKeys := *keys.Keys - accessKey = *accessKeys[0].Value - } - - storageAuth, err := autorest.NewSharedKeyAuthorizer(c.storageAccountName, accessKey, autorest.SharedKey) - if err != nil { - return nil, fmt.Errorf("Error building Authorizer: %+v", err) - } - - containersClient := containers.NewWithEnvironment(c.environment) - c.configureClient(&containersClient.Client, storageAuth) - return &containersClient, nil -} - -func (c *ArmClient) configureClient(client *autorest.Client, auth autorest.Authorizer) { - client.UserAgent = buildUserAgent() - client.Authorizer = auth - client.Sender = buildSender() - client.SkipResourceProviderRegistration = false - client.PollingDuration = 60 * time.Minute -} - -func buildUserAgent() string { - userAgent := httpclient.TerraformUserAgent(version.Version) - - // append the CloudShell version to the user agent if it exists - if azureAgent := os.Getenv("AZURE_HTTP_USER_AGENT"); azureAgent != "" { - userAgent = fmt.Sprintf("%s %s", userAgent, azureAgent) - } - - return userAgent -} diff --git a/internal/backend/remote-state/azure/backend.go b/internal/backend/remote-state/azure/backend.go index 4a41c96458..d82e172101 100644 --- a/internal/backend/remote-state/azure/backend.go +++ b/internal/backend/remote-state/azure/backend.go @@ -1,9 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package azure import ( "context" "fmt" + "time" + "github.com/hashicorp/go-azure-sdk/sdk/auth" + "github.com/hashicorp/go-azure-sdk/sdk/environments" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/legacy/helper/schema" ) @@ -12,6 +18,19 @@ import ( func New() backend.Backend { s := &schema.Backend{ Schema: map[string]*schema.Schema{ + "subscription_id": { + Type: schema.TypeString, + Optional: true, + Description: "The Subscription ID where the Storage Account is located.", + DefaultFunc: schema.EnvDefaultFunc("ARM_SUBSCRIPTION_ID", ""), + }, + + "resource_group_name": { + Type: schema.TypeString, + Optional: true, + Description: "The Resource Group where the Storage Account is located.", + }, + "storage_account_name": { Type: schema.TypeString, Required: true, @@ -21,147 +40,201 @@ func New() backend.Backend { "container_name": { Type: schema.TypeString, Required: true, - Description: "The container name.", + Description: "The container name to use in the Storage Account.", }, "key": { Type: schema.TypeString, Required: true, - Description: "The blob key.", + Description: "The blob key to use in the Storage Container.", }, - "metadata_host": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("ARM_METADATA_HOST", ""), - Description: "The Metadata URL which will be used to obtain the Cloud Environment.", + "lookup_blob_endpoint": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_USE_DNS_ZONE_ENDPOINT", false), + Description: "Whether to look up the storage account blob endpoint. This is necessary when the storage account uses the Azure DNS zone endpoint.", + }, + + "snapshot": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether to enable automatic blob snapshotting.", + DefaultFunc: schema.EnvDefaultFunc("ARM_SNAPSHOT", false), }, "environment": { Type: schema.TypeString, Optional: true, - Description: "The Azure cloud environment.", + Description: "The Cloud Environment which should be used. Possible values are public, usgovernment, and china. Defaults to public. Not used and should not be specified when `metadata_host` is specified.", DefaultFunc: schema.EnvDefaultFunc("ARM_ENVIRONMENT", "public"), }, + "metadata_host": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_METADATA_HOSTNAME", "ARM_METADATA_HOST"}, ""), // TODO: remove support for `METADATA_HOST` in a future version + Description: "The Hostname which should be used for the Azure Metadata Service.", + }, + "access_key": { Type: schema.TypeString, Optional: true, - Description: "The access key.", + Description: "The access key to use when authenticating using a Storage Access Key.", DefaultFunc: schema.EnvDefaultFunc("ARM_ACCESS_KEY", ""), }, "sas_token": { Type: schema.TypeString, Optional: true, - Description: "A SAS Token used to interact with the Blob Storage Account.", + Description: "The SAS Token to use when authenticating using a SAS Token.", DefaultFunc: schema.EnvDefaultFunc("ARM_SAS_TOKEN", ""), }, - "snapshot": { - Type: schema.TypeBool, - Optional: true, - Description: "Enable/Disable automatic blob snapshotting", - DefaultFunc: schema.EnvDefaultFunc("ARM_SNAPSHOT", false), - }, - - "resource_group_name": { - Type: schema.TypeString, - Optional: true, - Description: "The resource group name.", - }, - - "client_id": { - Type: schema.TypeString, - Optional: true, - Description: "The Client ID.", - DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""), - }, - - "endpoint": { - Type: schema.TypeString, - Optional: true, - Description: "A custom Endpoint used to access the Azure Resource Manager API's.", - DefaultFunc: schema.EnvDefaultFunc("ARM_ENDPOINT", ""), - }, - - "subscription_id": { - Type: schema.TypeString, - Optional: true, - Description: "The Subscription ID.", - DefaultFunc: schema.EnvDefaultFunc("ARM_SUBSCRIPTION_ID", ""), - }, - "tenant_id": { Type: schema.TypeString, Optional: true, - Description: "The Tenant ID.", + Description: "The Tenant ID to use when authenticating using Azure Active Directory.", DefaultFunc: schema.EnvDefaultFunc("ARM_TENANT_ID", ""), }, - // Service Principal (Client Certificate) specific - "client_certificate_password": { + "client_id": { Type: schema.TypeString, Optional: true, - Description: "The password associated with the Client Certificate specified in `client_certificate_path`", - DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE_PASSWORD", ""), + Description: "The Client ID to use when authenticating using Azure Active Directory.", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID", ""), }, + + "client_id_file_path": { + Type: schema.TypeString, + Optional: true, + Description: "The path to a file containing the Client ID which should be used.", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_ID_FILE_PATH", nil), + }, + + "endpoint": { + Type: schema.TypeString, + Optional: true, + Deprecated: "`endpoint` is deprecated in favor of `msi_endpoint`, it will be removed in a future version of Terraform", + }, + + // Client Certificate specific fields + "client_certificate": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE", ""), + Description: "Base64 encoded PKCS#12 certificate bundle to use when authenticating as a Service Principal using a Client Certificate", + }, + "client_certificate_path": { Type: schema.TypeString, Optional: true, - Description: "The path to the PFX file used as the Client Certificate when authenticating as a Service Principal", + Description: "The path to the Client Certificate associated with the Service Principal for use when authenticating as a Service Principal using a Client Certificate.", DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE_PATH", ""), }, - // Service Principal (Client Secret) specific + "client_certificate_password": { + Type: schema.TypeString, + Optional: true, + Description: "The password associated with the Client Certificate. For use when authenticating as a Service Principal using a Client Certificate", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_CERTIFICATE_PASSWORD", ""), + }, + + // Client Secret specific fields "client_secret": { Type: schema.TypeString, Optional: true, - Description: "The Client Secret.", + Description: "The Client Secret which should be used. For use When authenticating as a Service Principal using a Client Secret.", DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET", ""), }, - // Managed Service Identity specific - "use_msi": { - Type: schema.TypeBool, - Optional: true, - Description: "Should Managed Service Identity be used?", - DefaultFunc: schema.EnvDefaultFunc("ARM_USE_MSI", false), - }, - "msi_endpoint": { + "client_secret_file_path": { Type: schema.TypeString, Optional: true, - Description: "The Managed Service Identity Endpoint.", - DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""), + Description: "The path to a file containing the Client Secret which should be used. For use When authenticating as a Service Principal using a Client Secret.", + DefaultFunc: schema.EnvDefaultFunc("ARM_CLIENT_SECRET_FILE_PATH", nil), }, - // OIDC auth specific fields + // OIDC specific fields "use_oidc": { Type: schema.TypeBool, Optional: true, DefaultFunc: schema.EnvDefaultFunc("ARM_USE_OIDC", false), - Description: "Allow OIDC to be used for authentication", + Description: "Allow OpenID Connect to be used for authentication", }, - "oidc_request_url": { + "ado_pipeline_service_connection_id": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_OIDC_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_URL"}, ""), - Description: "The URL for the OIDC provider from which to request an ID token", + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID", "ARM_OIDC_AZURE_SERVICE_CONNECTION_ID"}, nil), + Description: "The Azure DevOps Pipeline Service Connection ID.", }, "oidc_request_token": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_OIDC_REQUEST_TOKEN", "ACTIONS_ID_TOKEN_REQUEST_TOKEN"}, ""), - Description: "The bearer token for the request to the OIDC provider", + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_OIDC_REQUEST_TOKEN", "ACTIONS_ID_TOKEN_REQUEST_TOKEN", "SYSTEM_ACCESSTOKEN"}, nil), + Description: "The bearer token for the request to the OIDC provider. For use when authenticating as a Service Principal using OpenID Connect.", + }, + + "oidc_request_url": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ARM_OIDC_REQUEST_URL", "ACTIONS_ID_TOKEN_REQUEST_URL", "SYSTEM_OIDCREQUESTURI"}, nil), + Description: "The URL for the OIDC provider from which to request an ID token. For use when authenticating as a Service Principal using OpenID Connect.", + }, + + "oidc_token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_OIDC_TOKEN", ""), + Description: "The OIDC ID token for use when authenticating as a Service Principal using OpenID Connect.", + }, + + "oidc_token_file_path": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_OIDC_TOKEN_FILE_PATH", ""), + Description: "The path to a file containing an OIDC ID token for use when authenticating as a Service Principal using OpenID Connect.", + }, + + // Managed Identity specific fields + "use_msi": { + Type: schema.TypeBool, + Optional: true, + Description: "Allow Managed Identity to be used for Authentication.", + DefaultFunc: schema.EnvDefaultFunc("ARM_USE_MSI", false), + }, + + "msi_endpoint": { + Type: schema.TypeString, + Optional: true, + Description: "The path to a custom endpoint for Managed Identity - in most circumstances this should be detected automatically.", + DefaultFunc: schema.EnvDefaultFunc("ARM_MSI_ENDPOINT", ""), + }, + + // Azure CLI specific fields + "use_cli": { + Type: schema.TypeBool, + Optional: true, + Default: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_USE_CLI", true), + Description: "Allow Azure CLI to be used for Authentication.", + }, + + // Azure AKS Workload Identity fields + "use_aks_workload_identity": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_USE_AKS_WORKLOAD_IDENTITY", false), + Description: "Allow Azure AKS Workload Identity to be used for Authentication.", }, // Feature Flags "use_azuread_auth": { Type: schema.TypeBool, Optional: true, - Description: "Should Terraform use AzureAD Authentication to access the Blob?", + Description: "Whether to use Azure Active Directory authentication to access the Storage Data Plane APIs.", DefaultFunc: schema.EnvDefaultFunc("ARM_USE_AZUREAD", false), }, }, @@ -176,7 +249,7 @@ type Backend struct { *schema.Backend // The fields below are set from configure - armClient *ArmClient + apiClient *Client containerName string keyName string accountName string @@ -184,33 +257,20 @@ type Backend struct { } type BackendConfig struct { - // Required - StorageAccountName string - - // Optional - AccessKey string - ClientID string - ClientCertificatePassword string - ClientCertificatePath string - ClientSecret string - CustomResourceManagerEndpoint string - MetadataHost string - Environment string - MsiEndpoint string - OIDCRequestURL string - OIDCRequestToken string - ResourceGroupName string - SasToken string - SubscriptionID string - TenantID string - UseMsi bool - UseOIDC bool - UseAzureADAuthentication bool + AuthConfig *auth.Credentials + SubscriptionID string + ResourceGroupName string + StorageAccountName string + LookupBlobEndpoint bool + AccessKey string + SasToken string + UseAzureADAuthentication bool } func (b *Backend) configure(ctx context.Context) error { - if b.containerName != "" { - return nil + // This is to make the go-azure-sdk/sdk/client Client happy. + if _, ok := ctx.Deadline(); !ok { + ctx, _ = context.WithTimeout(ctx, 5*time.Minute) } // Grab the resource data @@ -220,38 +280,112 @@ func (b *Backend) configure(ctx context.Context) error { b.keyName = data.Get("key").(string) b.snapshot = data.Get("snapshot").(bool) - config := BackendConfig{ - AccessKey: data.Get("access_key").(string), - ClientID: data.Get("client_id").(string), - ClientCertificatePassword: data.Get("client_certificate_password").(string), - ClientCertificatePath: data.Get("client_certificate_path").(string), - ClientSecret: data.Get("client_secret").(string), - CustomResourceManagerEndpoint: data.Get("endpoint").(string), - MetadataHost: data.Get("metadata_host").(string), - Environment: data.Get("environment").(string), - MsiEndpoint: data.Get("msi_endpoint").(string), - OIDCRequestURL: data.Get("oidc_request_url").(string), - OIDCRequestToken: data.Get("oidc_request_token").(string), - ResourceGroupName: data.Get("resource_group_name").(string), - SasToken: data.Get("sas_token").(string), - StorageAccountName: data.Get("storage_account_name").(string), - SubscriptionID: data.Get("subscription_id").(string), - TenantID: data.Get("tenant_id").(string), - UseMsi: data.Get("use_msi").(bool), - UseOIDC: data.Get("use_oidc").(bool), - UseAzureADAuthentication: data.Get("use_azuread_auth").(bool), + var clientCertificateData []byte + if encodedCert := data.Get("client_certificate").(string); encodedCert != "" { + var err error + clientCertificateData, err = decodeCertificate(encodedCert) + if err != nil { + return err + } } - armClient, err := buildArmClient(context.TODO(), config) + oidcToken, err := getOidcToken(data) if err != nil { return err } - thingsNeededToLookupAccessKeySpecified := config.AccessKey == "" && config.SasToken == "" && config.ResourceGroupName == "" - if thingsNeededToLookupAccessKeySpecified && !config.UseAzureADAuthentication { - return fmt.Errorf("Either an Access Key / SAS Token or the Resource Group for the Storage Account must be specified - or Azure AD Authentication must be enabled") + clientSecret, err := getClientSecret(data) + if err != nil { + return err } - b.armClient = armClient + clientId, err := getClientId(data) + if err != nil { + return err + } + + tenantId, err := getTenantId(data) + if err != nil { + return err + } + + var ( + env *environments.Environment + + envName = data.Get("environment").(string) + metadataHost = data.Get("metadata_host").(string) + ) + + if metadataHost != "" { + logEntry("[DEBUG] Configuring cloud environment from Metadata Service at %q", metadataHost) + if env, err = environments.FromEndpoint(ctx, fmt.Sprintf("https://%s", metadataHost)); err != nil { + return err + } + } else { + logEntry("[DEBUG] Configuring built-in cloud environment by name: %q", envName) + if env, err = environments.FromName(envName); err != nil { + return err + } + } + + var ( + enableAzureCli = data.Get("use_cli").(bool) + enableManagedIdentity = data.Get("use_msi").(bool) + enableOidc = data.Get("use_oidc").(bool) || data.Get("use_aks_workload_identity").(bool) + ) + + authConfig := &auth.Credentials{ + Environment: *env, + ClientID: *clientId, + TenantID: *tenantId, + + ClientCertificateData: clientCertificateData, + ClientCertificatePath: data.Get("client_certificate_path").(string), + ClientCertificatePassword: data.Get("client_certificate_password").(string), + ClientSecret: *clientSecret, + + OIDCAssertionToken: *oidcToken, + OIDCTokenRequestURL: data.Get("oidc_request_url").(string), + OIDCTokenRequestToken: data.Get("oidc_request_token").(string), + ADOPipelineServiceConnectionID: data.Get("ado_pipeline_service_connection_id").(string), + + CustomManagedIdentityEndpoint: data.Get("msi_endpoint").(string), + + EnableAuthenticatingUsingClientCertificate: true, + EnableAuthenticatingUsingClientSecret: true, + EnableAuthenticatingUsingAzureCLI: enableAzureCli, + EnableAuthenticatingUsingManagedIdentity: enableManagedIdentity, + EnableAuthenticationUsingOIDC: enableOidc, + EnableAuthenticationUsingGitHubOIDC: enableOidc, + EnableAuthenticationUsingADOPipelineOIDC: enableOidc, + } + + backendConfig := BackendConfig{ + AuthConfig: authConfig, + SubscriptionID: data.Get("subscription_id").(string), + ResourceGroupName: data.Get("resource_group_name").(string), + StorageAccountName: data.Get("storage_account_name").(string), + LookupBlobEndpoint: data.Get("lookup_blob_endpoint").(bool), + AccessKey: data.Get("access_key").(string), + SasToken: data.Get("sas_token").(string), + UseAzureADAuthentication: data.Get("use_azuread_auth").(bool), + } + + needToLookupAccessKey := backendConfig.AccessKey == "" && backendConfig.SasToken == "" && !backendConfig.UseAzureADAuthentication + if backendConfig.ResourceGroupName == "" { + if needToLookupAccessKey { + return fmt.Errorf("One of `access_key`, `sas_token`, `use_azuread_auth` and `resource_group_name` must be specifieid") + } + if backendConfig.LookupBlobEndpoint { + return fmt.Errorf("`resource_group_name` is required when `lookup_blob_endpoint` is set") + } + } + + client, err := buildClient(ctx, backendConfig) + if err != nil { + return err + } + + b.apiClient = client return nil } diff --git a/internal/backend/remote-state/azure/backend_state.go b/internal/backend/remote-state/azure/backend_state.go index 6a1a9c02f0..6be25afadb 100644 --- a/internal/backend/remote-state/azure/backend_state.go +++ b/internal/backend/remote-state/azure/backend_state.go @@ -1,17 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package azure import ( - "context" "fmt" "sort" "strings" + "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/blobs" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/containers" ) const ( @@ -26,14 +29,14 @@ func (b *Backend) Workspaces() ([]string, error) { Prefix: &prefix, } - ctx := context.TODO() - client, err := b.armClient.getContainersClient(ctx) + ctx := newCtx() + client, err := b.apiClient.getContainersClient(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("retrieving container client: %v", err) } - resp, err := client.ListBlobs(ctx, b.armClient.storageAccountName, b.containerName, params) + resp, err := client.ListBlobs(ctx, b.containerName, params) if err != nil { - return nil, err + return nil, fmt.Errorf("listing blobs: %v", err) } envs := map[string]struct{}{} @@ -58,19 +61,19 @@ func (b *Backend) Workspaces() ([]string, error) { return result, nil } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } - ctx := context.TODO() - client, err := b.armClient.getBlobClient(ctx) + ctx := newCtx() + client, err := b.apiClient.getBlobClient(ctx) if err != nil { return err } - if resp, err := client.Delete(ctx, b.armClient.storageAccountName, b.containerName, b.path(name), blobs.DeleteInput{}); err != nil { - if resp.Response.StatusCode != 404 { + if resp, err := client.Delete(ctx, b.containerName, b.path(name), blobs.DeleteInput{}); err != nil { + if !response.WasNotFound(resp.HttpResponse) { return err } } @@ -79,8 +82,8 @@ func (b *Backend) DeleteWorkspace(name string) error { } func (b *Backend) StateMgr(name string) (statemgr.Full, error) { - ctx := context.TODO() - blobClient, err := b.armClient.getBlobClient(ctx) + ctx := newCtx() + blobClient, err := b.apiClient.getBlobClient(ctx) if err != nil { return nil, err } @@ -131,7 +134,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { err = lockUnlock(err) return nil, err } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { err = lockUnlock(err) return nil, err } diff --git a/internal/backend/remote-state/azure/backend_test.go b/internal/backend/remote-state/azure/backend_test.go index fa5b0a9f16..2c72898b58 100644 --- a/internal/backend/remote-state/azure/backend_test.go +++ b/internal/backend/remote-state/azure/backend_test.go @@ -1,12 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package azure import ( - "context" "os" "testing" "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/acctest" ) func TestBackend_impl(t *testing.T) { @@ -14,6 +15,8 @@ func TestBackend_impl(t *testing.T) { } func TestBackendConfig(t *testing.T) { + t.Parallel() + // This test just instantiates the client. Shouldn't make any actual // requests nor incur any costs. @@ -40,146 +43,254 @@ func TestBackendConfig(t *testing.T) { } func TestAccBackendAccessKeyBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { - armClient.destroyTestResources(ctx, res) + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "access_key": m.storageAccessKey, + "environment": m.env.Name, })).(*Backend) backend.TestBackendStates(t, b) } func TestAccBackendSASTokenBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) - sasToken, err := buildSasToken(res.storageAccountName, res.storageAccountAccessKey) + sasToken, err := buildSasToken(m.names.storageAccountName, m.storageAccessKey) if err != nil { t.Fatalf("Error building SAS Token: %+v", err) } + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, "sas_token": *sasToken, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "environment": m.env.Name, })).(*Backend) backend.TestBackendStates(t, b) } -func TestAccBackendOIDCBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) +func TestAccBackendGithubOIDCBasic(t *testing.T) { + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) - if err != nil { - t.Fatalf("Error creating Test Resources: %q", err) + testAccAzureBackendRunningInGitHubActions(t) + + oidcRequestToken := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN") + if oidcRequestToken == "" { + t.Fatalf("Missing ACTIONS_ID_TOKEN_REQUEST_TOKEN") } + oidcRequestURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") + if oidcRequestURL == "" { + t.Fatalf("Missing ACTIONS_ID_TOKEN_REQUEST_URL") + } + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) + if err != nil { + m.destroyTestResources(ctx) + t.Fatalf("Error creating Test Resources: %q", err) + } + defer m.destroyTestResources(ctx) + + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, "use_oidc": true, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "oidc_request_token": oidcRequestToken, + "oidc_request_url": oidcRequestURL, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "environment": m.env.Name, + })).(*Backend) + + backend.TestBackendStates(t, b) +} + +func TestAccBackendADOPipelinesOIDCBasic(t *testing.T) { + t.Parallel() + + testAccAzureBackendRunningInADOPipelines(t) + + oidcRequestToken := os.Getenv("SYSTEM_ACCESSTOKEN") + if oidcRequestToken == "" { + t.Fatalf("Missing SYSTEM_ACCESSTOKEN") + } + + oidcRequestURL := os.Getenv("SYSTEM_OIDCREQUESTURI") + if oidcRequestURL == "" { + t.Fatalf("Missing SYSTEM_OIDCREQUESTURI") + } + + adoPipelineServiceConnectionId := os.Getenv("ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID") + if adoPipelineServiceConnectionId == "" { + t.Fatalf("Missing ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID") + } + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) + if err != nil { + m.destroyTestResources(ctx) + t.Fatalf("Error creating Test Resources: %q", err) + } + defer m.destroyTestResources(ctx) + + clearARMEnv() + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "use_oidc": true, + "oidc_request_token": oidcRequestToken, + "oidc_request_url": oidcRequestURL, + "ado_pipeline_service_connection_id": adoPipelineServiceConnectionId, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "environment": m.env.Name, })).(*Backend) backend.TestBackendStates(t, b) } func TestAccBackendAzureADAuthBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - res.useAzureADAuth = true - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { - armClient.destroyTestResources(ctx, res) + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, "use_azuread_auth": true, + "environment": m.env.Name, + })).(*Backend) + + backend.TestBackendStates(t, b) +} + +func TestAccBackendAzureADAuthBasicWithBlobEndpointLookup(t *testing.T) { + t.Parallel() + + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) + if err != nil { + m.destroyTestResources(ctx) + t.Fatalf("Error creating Test Resources: %q", err) + } + defer m.destroyTestResources(ctx) + + clearARMEnv() + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "use_azuread_auth": true, + "environment": m.env.Name, + "lookup_blob_endpoint": true, })).(*Backend) backend.TestBackendStates(t, b) } func TestAccBackendManagedServiceIdentityBasic(t *testing.T) { - testAccAzureBackendRunningInAzure(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackendRunningInAzure(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, "use_msi": true, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "tenant_id": m.tenantId, + "environment": m.env.Name, })).(*Backend) backend.TestBackendStates(t, b) } func TestAccBackendServicePrincipalClientCertificateBasic(t *testing.T) { + t.Parallel() + testAccAzureBackend(t) clientCertPassword := os.Getenv("ARM_CLIENT_CERTIFICATE_PASSWORD") @@ -188,128 +299,95 @@ func TestAccBackendServicePrincipalClientCertificateBasic(t *testing.T) { t.Skip("Skipping since `ARM_CLIENT_CERTIFICATE_PATH` is not specified!") } - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + ctx := newCtx() + m := BuildTestMeta(t, ctx) - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, "client_certificate_password": clientCertPassword, "client_certificate_path": clientCertPath, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "environment": m.env.Name, })).(*Backend) backend.TestBackendStates(t, b) } func TestAccBackendServicePrincipalClientSecretBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) - if err != nil { - t.Fatalf("Error creating Test Resources: %q", err) - } - - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), - })).(*Backend) - - backend.TestBackendStates(t, b) -} - -func TestAccBackendServicePrincipalClientSecretCustomEndpoint(t *testing.T) { testAccAzureBackend(t) - // this is only applicable for Azure Stack. - endpoint := os.Getenv("ARM_ENDPOINT") - if endpoint == "" { - t.Skip("Skipping as ARM_ENDPOINT isn't configured") - } + ctx := newCtx() + m := BuildTestMeta(t, ctx) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) - - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": endpoint, + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "environment": m.env.Name, })).(*Backend) backend.TestBackendStates(t, b) } func TestAccBackendAccessKeyLocked(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + + clearARMEnv() b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "access_key": m.storageAccessKey, + "environment": m.env.Name, })).(*Backend) b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "access_key": m.storageAccessKey, + "environment": m.env.Name, })).(*Backend) backend.TestBackendStateLocks(t, b1, b2) @@ -320,42 +398,44 @@ func TestAccBackendAccessKeyLocked(t *testing.T) { } func TestAccBackendServicePrincipalLocked(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + + clearARMEnv() b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "environment": m.env.Name, })).(*Backend) b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "environment": m.env.Name, })).(*Backend) backend.TestBackendStateLocks(t, b1, b2) diff --git a/internal/backend/remote-state/azure/client.go b/internal/backend/remote-state/azure/client.go index 5d22767954..5c335c43f6 100644 --- a/internal/backend/remote-state/azure/client.go +++ b/internal/backend/remote-state/azure/client.go @@ -1,18 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package azure import ( "context" "encoding/base64" "encoding/json" + "errors" "fmt" "log" - "net/http" + "time" - "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-azure-helpers/lang/response" "github.com/hashicorp/go-uuid" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/blobs" + "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs" ) const ( @@ -21,6 +26,15 @@ const ( lockInfoMetaKey = "terraformlockid" ) +const veryLongTimeout = 9999 * time.Hour + +// newCtx creates a context with a (meaningless) deadline. +// This is only to make the go-azure-sdk/sdk/client Client happy. +func newCtx() context.Context { + ctx, _ := context.WithTimeout(context.TODO(), veryLongTimeout) + return ctx +} + type RemoteClient struct { giovanniBlobClient blobs.Client accountName string @@ -36,17 +50,21 @@ func (c *RemoteClient) Get() (*remote.Payload, error) { options.LeaseID = &c.leaseID } - ctx := context.TODO() - blob, err := c.giovanniBlobClient.Get(ctx, c.accountName, c.containerName, c.keyName, options) + ctx := newCtx() + blob, err := c.giovanniBlobClient.Get(ctx, c.containerName, c.keyName, options) if err != nil { - if blob.Response.IsHTTPStatus(http.StatusNotFound) { + if response.WasNotFound(blob.HttpResponse) { return nil, nil } return nil, err } + if blob.Contents == nil { + return nil, nil + } + payload := &remote.Payload{ - Data: blob.Contents, + Data: *blob.Contents, } // If there was no data, then return nil @@ -70,22 +88,22 @@ func (c *RemoteClient) Put(data []byte) error { putOptions.LeaseID = &c.leaseID } - ctx := context.TODO() + ctx := newCtx() if c.snapshot { snapshotInput := blobs.SnapshotInput{LeaseID: options.LeaseID} log.Printf("[DEBUG] Snapshotting existing Blob %q (Container %q / Account %q)", c.keyName, c.containerName, c.accountName) - if _, err := c.giovanniBlobClient.Snapshot(ctx, c.accountName, c.containerName, c.keyName, snapshotInput); err != nil { + if _, err := c.giovanniBlobClient.Snapshot(ctx, c.containerName, c.keyName, snapshotInput); err != nil { return fmt.Errorf("error snapshotting Blob %q (Container %q / Account %q): %+v", c.keyName, c.containerName, c.accountName, err) } log.Print("[DEBUG] Created blob snapshot") } - blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, getOptions) + blob, err := c.giovanniBlobClient.GetProperties(ctx, c.containerName, c.keyName, getOptions) if err != nil { - if blob.StatusCode != 404 { + if !response.WasNotFound(blob.HttpResponse) { return err } } @@ -94,7 +112,7 @@ func (c *RemoteClient) Put(data []byte) error { putOptions.Content = &data putOptions.ContentType = &contentType putOptions.MetaData = blob.MetaData - _, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.accountName, c.containerName, c.keyName, putOptions) + _, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.containerName, c.keyName, putOptions) return err } @@ -106,10 +124,10 @@ func (c *RemoteClient) Delete() error { options.LeaseID = &c.leaseID } - ctx := context.TODO() - resp, err := c.giovanniBlobClient.Delete(ctx, c.accountName, c.containerName, c.keyName, options) + ctx := newCtx() + resp, err := c.giovanniBlobClient.Delete(ctx, c.containerName, c.keyName, options) if err != nil { - if !resp.IsHTTPStatus(http.StatusNotFound) { + if !response.WasNotFound(resp.HttpResponse) { return err } } @@ -132,7 +150,7 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { getLockInfoErr := func(err error) error { lockInfo, infoErr := c.getLockInfo() if infoErr != nil { - err = multierror.Append(err, infoErr) + err = errors.Join(err, infoErr) } return &statemgr.LockError{ @@ -145,13 +163,13 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { ProposedLeaseID: &info.ID, LeaseDuration: -1, } - ctx := context.TODO() + ctx := newCtx() // obtain properties to see if the blob lease is already in use. If the blob doesn't exist, create it - properties, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, blobs.GetPropertiesInput{}) + properties, err := c.giovanniBlobClient.GetProperties(ctx, c.containerName, c.keyName, blobs.GetPropertiesInput{}) if err != nil { // error if we had issues getting the blob - if !properties.Response.IsHTTPStatus(http.StatusNotFound) { + if !response.WasNotFound(properties.HttpResponse) { return "", getLockInfoErr(err) } // if we don't find the blob, we need to build it @@ -161,7 +179,7 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { ContentType: &contentType, } - _, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.accountName, c.containerName, c.keyName, putGOptions) + _, err = c.giovanniBlobClient.PutBlockBlob(ctx, c.containerName, c.keyName, putGOptions) if err != nil { return "", getLockInfoErr(err) } @@ -172,7 +190,7 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { return "", getLockInfoErr(fmt.Errorf("state blob is already locked")) } - leaseID, err := c.giovanniBlobClient.AcquireLease(ctx, c.accountName, c.containerName, c.keyName, leaseOptions) + leaseID, err := c.giovanniBlobClient.AcquireLease(ctx, c.containerName, c.keyName, leaseOptions) if err != nil { return "", getLockInfoErr(err) } @@ -193,8 +211,8 @@ func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { options.LeaseID = &c.leaseID } - ctx := context.TODO() - blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, options) + ctx := newCtx() + blob, err := c.giovanniBlobClient.GetProperties(ctx, c.containerName, c.keyName, options) if err != nil { return nil, err } @@ -220,8 +238,8 @@ func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { // writes info to blob meta data, deletes metadata entry if info is nil func (c *RemoteClient) writeLockInfo(info *statemgr.LockInfo) error { - ctx := context.TODO() - blob, err := c.giovanniBlobClient.GetProperties(ctx, c.accountName, c.containerName, c.keyName, blobs.GetPropertiesInput{LeaseID: &c.leaseID}) + ctx := newCtx() + blob, err := c.giovanniBlobClient.GetProperties(ctx, c.containerName, c.keyName, blobs.GetPropertiesInput{LeaseID: &c.leaseID}) if err != nil { return err } @@ -241,7 +259,7 @@ func (c *RemoteClient) writeLockInfo(info *statemgr.LockInfo) error { MetaData: blob.MetaData, } - _, err = c.giovanniBlobClient.SetMetaData(ctx, c.accountName, c.containerName, c.keyName, opts) + _, err = c.giovanniBlobClient.SetMetaData(ctx, c.containerName, c.keyName, opts) return err } @@ -266,8 +284,8 @@ func (c *RemoteClient) Unlock(id string) error { return lockErr } - ctx := context.TODO() - _, err = c.giovanniBlobClient.ReleaseLease(ctx, c.accountName, c.containerName, c.keyName, id) + ctx := newCtx() + _, err = c.giovanniBlobClient.ReleaseLease(ctx, c.containerName, c.keyName, blobs.ReleaseLeaseInput{LeaseID: id}) if err != nil { lockErr.Err = err return lockErr diff --git a/internal/backend/remote-state/azure/client_test.go b/internal/backend/remote-state/azure/client_test.go index 5cc5d6a846..78814448c0 100644 --- a/internal/backend/remote-state/azure/client_test.go +++ b/internal/backend/remote-state/azure/client_test.go @@ -1,41 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package azure import ( - "context" - "os" "testing" "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/acctest" "github.com/hashicorp/terraform/internal/states/remote" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/blobs" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/blobs" ) -func TestRemoteClient_impl(t *testing.T) { - var _ remote.Client = new(RemoteClient) - var _ remote.ClientLocker = new(RemoteClient) -} +var _ remote.Client = new(RemoteClient) +var _ remote.ClientLocker = new(RemoteClient) func TestRemoteClientAccessKeyBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "access_key": m.storageAccessKey, + "environment": m.env.Name, })).(*Backend) state, err := b.StateMgr(backend.DefaultStateName) @@ -47,28 +47,30 @@ func TestRemoteClientAccessKeyBasic(t *testing.T) { } func TestRemoteClientManagedServiceIdentityBasic(t *testing.T) { - testAccAzureBackendRunningInAzure(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackendRunningInAzure(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, "use_msi": true, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "tenant_id": m.tenantId, + "environment": m.env.Name, })).(*Backend) state, err := b.StateMgr(backend.DefaultStateName) @@ -80,30 +82,32 @@ func TestRemoteClientManagedServiceIdentityBasic(t *testing.T) { } func TestRemoteClientSasTokenBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) - sasToken, err := buildSasToken(res.storageAccountName, res.storageAccountAccessKey) + sasToken, err := buildSasToken(m.names.storageAccountName, m.storageAccessKey) if err != nil { t.Fatalf("Error building SAS Token: %+v", err) } + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, "sas_token": *sasToken, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "environment": m.env.Name, })).(*Backend) state, err := b.StateMgr(backend.DefaultStateName) @@ -115,29 +119,31 @@ func TestRemoteClientSasTokenBasic(t *testing.T) { } func TestRemoteClientServicePrincipalBasic(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + clearARMEnv() b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "environment": m.env.Name, })).(*Backend) state, err := b.StateMgr(backend.DefaultStateName) @@ -149,34 +155,36 @@ func TestRemoteClientServicePrincipalBasic(t *testing.T) { } func TestRemoteClientAccessKeyLocks(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + + clearARMEnv() b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "access_key": m.storageAccessKey, + "environment": m.env.Name, })).(*Backend) b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "access_key": res.storageAccountAccessKey, - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "access_key": m.storageAccessKey, + "environment": m.env.Name, })).(*Backend) s1, err := b1.StateMgr(backend.DefaultStateName) @@ -193,42 +201,44 @@ func TestRemoteClientAccessKeyLocks(t *testing.T) { } func TestRemoteClientServicePrincipalLocks(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) + + clearARMEnv() b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "environment": m.env.Name, })).(*Backend) b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "storage_account_name": res.storageAccountName, - "container_name": res.storageContainerName, - "key": res.storageKeyName, - "resource_group_name": res.resourceGroup, - "subscription_id": os.Getenv("ARM_SUBSCRIPTION_ID"), - "tenant_id": os.Getenv("ARM_TENANT_ID"), - "client_id": os.Getenv("ARM_CLIENT_ID"), - "client_secret": os.Getenv("ARM_CLIENT_SECRET"), - "environment": os.Getenv("ARM_ENVIRONMENT"), - "endpoint": os.Getenv("ARM_ENDPOINT"), + "subscription_id": m.subscriptionId, + "resource_group_name": m.names.resourceGroup, + "storage_account_name": m.names.storageAccountName, + "container_name": m.names.storageContainerName, + "key": m.names.storageKeyName, + "tenant_id": m.tenantId, + "client_id": m.clientId, + "client_secret": m.clientSecret, + "environment": m.env.Name, })).(*Backend) s1, err := b1.StateMgr(backend.DefaultStateName) @@ -245,32 +255,34 @@ func TestRemoteClientServicePrincipalLocks(t *testing.T) { } func TestPutMaintainsMetaData(t *testing.T) { - testAccAzureBackend(t) - rs := acctest.RandString(4) - res := testResourceNames(rs, "testState") - armClient := buildTestClient(t, res) + t.Parallel() - ctx := context.TODO() - err := armClient.buildTestResources(ctx, &res) - defer armClient.destroyTestResources(ctx, res) + testAccAzureBackend(t) + + ctx := newCtx() + m := BuildTestMeta(t, ctx) + + err := m.buildTestResources(ctx) if err != nil { + m.destroyTestResources(ctx) t.Fatalf("Error creating Test Resources: %q", err) } + defer m.destroyTestResources(ctx) headerName := "acceptancetest" expectedValue := "f3b56bad-33ad-4b93-a600-7a66e9cbd1eb" - client, err := armClient.getBlobClient(ctx) + client, err := m.getBlobClient(ctx) if err != nil { t.Fatalf("Error building Blob Client: %+v", err) } - _, err = client.PutBlockBlob(ctx, res.storageAccountName, res.storageContainerName, res.storageKeyName, blobs.PutBlockBlobInput{}) + _, err = client.PutBlockBlob(ctx, m.names.storageContainerName, m.names.storageKeyName, blobs.PutBlockBlobInput{}) if err != nil { t.Fatalf("Error Creating Block Blob: %+v", err) } - blobReference, err := client.GetProperties(ctx, res.storageAccountName, res.storageContainerName, res.storageKeyName, blobs.GetPropertiesInput{}) + blobReference, err := client.GetProperties(ctx, m.names.storageContainerName, m.names.storageKeyName, blobs.GetPropertiesInput{}) if err != nil { t.Fatalf("Error loading MetaData: %+v", err) } @@ -279,28 +291,28 @@ func TestPutMaintainsMetaData(t *testing.T) { opts := blobs.SetMetaDataInput{ MetaData: blobReference.MetaData, } - _, err = client.SetMetaData(ctx, res.storageAccountName, res.storageContainerName, res.storageKeyName, opts) + _, err = client.SetMetaData(ctx, m.names.storageContainerName, m.names.storageKeyName, opts) if err != nil { t.Fatalf("Error setting MetaData: %+v", err) } // update the metadata using the Backend remoteClient := RemoteClient{ - keyName: res.storageKeyName, - containerName: res.storageContainerName, - accountName: res.storageAccountName, + keyName: m.names.storageKeyName, + containerName: m.names.storageContainerName, + accountName: m.names.storageAccountName, giovanniBlobClient: *client, } - bytes := []byte(acctest.RandString(20)) + bytes := []byte(randString(20)) err = remoteClient.Put(bytes) if err != nil { t.Fatalf("Error putting data: %+v", err) } // Verify it still exists - blobReference, err = client.GetProperties(ctx, res.storageAccountName, res.storageContainerName, res.storageKeyName, blobs.GetPropertiesInput{}) + blobReference, err = client.GetProperties(ctx, m.names.storageContainerName, m.names.storageKeyName, blobs.GetPropertiesInput{}) if err != nil { t.Fatalf("Error loading MetaData: %+v", err) } diff --git a/internal/backend/remote-state/azure/go.mod b/internal/backend/remote-state/azure/go.mod new file mode 100644 index 0000000000..56a0de8134 --- /dev/null +++ b/internal/backend/remote-state/azure/go.mod @@ -0,0 +1,91 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/azure + +go 1.24.2 + +require ( + github.com/hashicorp/go-azure-helpers v0.72.0 + github.com/hashicorp/go-azure-sdk/resource-manager v0.20250131.1134653 + github.com/hashicorp/go-azure-sdk/sdk v0.20250131.1134653 + github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 + github.com/jackofallops/giovanni v0.28.0 +) + +require ( + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.30 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.2 // indirect + github.com/Azure/go-autorest/tracing v0.6.1 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-cty v1.4.1 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/logutils v1.0.0 // indirect + github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/spf13/afero v1.9.5 // indirect + 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 + github.com/zclconf/go-cty v1.16.2 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.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.31.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/protobuf v1.36.5 // indirect + software.sslmate.com/src/go-pkcs12 v0.4.0 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/azure/go.sum b/internal/backend/remote-state/azure/go.sum new file mode 100644 index 0000000000..073bbf61a9 --- /dev/null +++ b/internal/backend/remote-state/azure/go.sum @@ -0,0 +1,693 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= +github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/go-azure-helpers v0.72.0 h1:eOCpXBkDYYMESkOBC0LgPMNUdiLPtQxv7sCW/hHdUbw= +github.com/hashicorp/go-azure-helpers v0.72.0/go.mod h1:Omirva2gGNrhN4pigurl5RN7u3BoaN0bco8ZSbdaNy8= +github.com/hashicorp/go-azure-sdk/resource-manager v0.20250131.1134653 h1:KuDCZKWoOByX5MUyFRNWLl4Gy6wpZCwJ7Ez1mbUwouo= +github.com/hashicorp/go-azure-sdk/resource-manager v0.20250131.1134653/go.mod h1:AawbnS/Kkp/IURMJVzmvD+Co2zK91lKFqYYDbenCpGU= +github.com/hashicorp/go-azure-sdk/sdk v0.20250131.1134653 h1:Bd+glHUD1mdal1zn0NgoS4wDFhUB8Qfw61j0nZEnC5A= +github.com/hashicorp/go-azure-sdk/sdk v0.20250131.1134653/go.mod h1:oI5R0fTbBx3K/sJBK5R/OlEy8ozdQjvctxVU9v3EDkc= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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-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-plugin-sdk/v2 v2.36.1 h1:WNMsTLkZf/3ydlgsuXePa3jvZFwAJhruxTxP/c1Viuw= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.1/go.mod h1:P6o64QS97plG44iFzSM6rAn6VJIC/Sy9a9IkEtl79K4= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jackofallops/giovanni v0.28.0 h1:fxn55SnxL2Rj3hgkkgQS9UKlIRXkkTZ5WcnE04JCBRE= +github.com/jackofallops/giovanni v0.28.0/go.mod h1:CyzRgZyts4YSI/1iZF8poqdn9I6J8xpmg1iMpvhthTs= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.15.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +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 v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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= +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= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/internal/backend/remote-state/azure/helpers.go b/internal/backend/remote-state/azure/helpers.go new file mode 100644 index 0000000000..9ecce81c7e --- /dev/null +++ b/internal/backend/remote-state/azure/helpers.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// This file is copied from terraform-provider-azurerm: internal/provider/helpers.go + +package azure + +import ( + "encoding/base64" + "fmt" + "log" + "os" + "strings" + + "github.com/hashicorp/terraform/internal/legacy/helper/schema" +) + +// logEntry avoids log entries showing up in test output +func logEntry(f string, v ...interface{}) { + if os.Getenv("TF_LOG") == "" { + return + } + + if os.Getenv("TF_ACC") != "" { + return + } + + log.Printf(f, v...) +} + +func decodeCertificate(clientCertificate string) ([]byte, error) { + var pfx []byte + if clientCertificate != "" { + out := make([]byte, base64.StdEncoding.DecodedLen(len(clientCertificate))) + n, err := base64.StdEncoding.Decode(out, []byte(clientCertificate)) + if err != nil { + return pfx, fmt.Errorf("could not decode client certificate data: %v", err) + } + pfx = out[:n] + } + return pfx, nil +} + +func getOidcToken(d *schema.ResourceData) (*string, error) { + idToken := strings.TrimSpace(d.Get("oidc_token").(string)) + + if path := d.Get("oidc_token_file_path").(string); path != "" { + fileTokenRaw, err := os.ReadFile(path) + + if err != nil { + return nil, fmt.Errorf("reading OIDC Token from file %q: %v", path, err) + } + + fileToken := strings.TrimSpace(string(fileTokenRaw)) + + if idToken != "" && idToken != fileToken { + return nil, fmt.Errorf("mismatch between supplied OIDC token and supplied OIDC token file contents - please either remove one or ensure they match") + } + + idToken = fileToken + } + + if d.Get("use_aks_workload_identity").(bool) && os.Getenv("AZURE_FEDERATED_TOKEN_FILE") != "" { + path := os.Getenv("AZURE_FEDERATED_TOKEN_FILE") + fileTokenRaw, err := os.ReadFile(os.Getenv("AZURE_FEDERATED_TOKEN_FILE")) + + if err != nil { + return nil, fmt.Errorf("reading OIDC Token from file %q provided by AKS Workload Identity: %v", path, err) + } + + fileToken := strings.TrimSpace(string(fileTokenRaw)) + + if idToken != "" && idToken != fileToken { + return nil, fmt.Errorf("mismatch between supplied OIDC token and OIDC token file contents provided by AKS Workload Identity - please either remove one, ensure they match, or disable use_aks_workload_identity") + } + + idToken = fileToken + } + + return &idToken, nil +} + +func getClientId(d *schema.ResourceData) (*string, error) { + clientId := strings.TrimSpace(d.Get("client_id").(string)) + + if path := d.Get("client_id_file_path").(string); path != "" { + fileClientIdRaw, err := os.ReadFile(path) + + if err != nil { + return nil, fmt.Errorf("reading Client ID from file %q: %v", path, err) + } + + fileClientId := strings.TrimSpace(string(fileClientIdRaw)) + + if clientId != "" && clientId != fileClientId { + return nil, fmt.Errorf("mismatch between supplied Client ID and supplied Client ID file contents - please either remove one or ensure they match") + } + + clientId = fileClientId + } + + if d.Get("use_aks_workload_identity").(bool) && os.Getenv("AZURE_CLIENT_ID") != "" { + aksClientId := os.Getenv("AZURE_CLIENT_ID") + if clientId != "" && clientId != aksClientId { + return nil, fmt.Errorf("mismatch between supplied Client ID and that provided by AKS Workload Identity - please remove, ensure they match, or disable use_aks_workload_identity") + } + clientId = aksClientId + } + + return &clientId, nil +} + +func getClientSecret(d *schema.ResourceData) (*string, error) { + clientSecret := strings.TrimSpace(d.Get("client_secret").(string)) + + if path := d.Get("client_secret_file_path").(string); path != "" { + fileSecretRaw, err := os.ReadFile(path) + + if err != nil { + return nil, fmt.Errorf("reading Client Secret from file %q: %v", path, err) + } + + fileSecret := strings.TrimSpace(string(fileSecretRaw)) + + if clientSecret != "" && clientSecret != fileSecret { + return nil, fmt.Errorf("mismatch between supplied Client Secret and supplied Client Secret file contents - please either remove one or ensure they match") + } + + clientSecret = fileSecret + } + + return &clientSecret, nil +} + +func getTenantId(d *schema.ResourceData) (*string, error) { + tenantId := strings.TrimSpace(d.Get("tenant_id").(string)) + + if d.Get("use_aks_workload_identity").(bool) && os.Getenv("AZURE_TENANT_ID") != "" { + aksTenantId := os.Getenv("AZURE_TENANT_ID") + if tenantId != "" && tenantId != aksTenantId { + return nil, fmt.Errorf("mismatch between supplied Tenant ID and that provided by AKS Workload Identity - please remove, ensure they match, or disable use_aks_workload_identity") + } + tenantId = aksTenantId + } + + return &tenantId, nil +} diff --git a/internal/backend/remote-state/azure/helpers_test.go b/internal/backend/remote-state/azure/helpers_test.go index 812b3f5a85..ba8aa58da1 100644 --- a/internal/backend/remote-state/azure/helpers_test.go +++ b/internal/backend/remote-state/azure/helpers_test.go @@ -1,19 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package azure import ( "context" "fmt" "log" + "math/rand" "os" "strings" "testing" "time" - "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources" - armStorage "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2021-01-01/storage" - "github.com/Azure/go-autorest/autorest" + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" sasStorage "github.com/hashicorp/go-azure-helpers/storage" - "github.com/tombuildsstuff/giovanni/storage/2018-11-09/blob/containers" + "github.com/hashicorp/go-azure-sdk/resource-manager/resources/2024-03-01/resourcegroups" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/storageaccounts" + "github.com/hashicorp/go-azure-sdk/sdk/auth" + "github.com/hashicorp/go-azure-sdk/sdk/environments" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/blobs" + "github.com/jackofallops/giovanni/storage/2023-11-03/blob/containers" ) const ( @@ -48,57 +56,34 @@ func testAccAzureBackendRunningInGitHubActions(t *testing.T) { } } -func buildTestClient(t *testing.T, res resourceNames) *ArmClient { - subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") - tenantID := os.Getenv("ARM_TENANT_ID") - clientID := os.Getenv("ARM_CLIENT_ID") - clientSecret := os.Getenv("ARM_CLIENT_SECRET") - msiEnabled := strings.EqualFold(os.Getenv("ARM_USE_MSI"), "true") - environment := os.Getenv("ARM_ENVIRONMENT") +// these kind of tests can only run when within ADO Pipelines (e.g. OIDC) +func testAccAzureBackendRunningInADOPipelines(t *testing.T) { + testAccAzureBackend(t) - hasCredentials := (clientID != "" && clientSecret != "") || msiEnabled - if !hasCredentials { - t.Fatal("Azure credentials missing or incomplete") + if os.Getenv("TF_RUNNING_IN_ADO_PIPELINES") == "" { + t.Skip("Skipping test since not running in ADO Pipelines") } +} - if subscriptionID == "" { - t.Fatalf("Missing ARM_SUBSCRIPTION_ID") +// clearARMEnv cleans up the azure related environment variables. +// This is to ensure the configuration only comes from HCL, which avoids +// env vars for test setup interfere the behavior. +// +// NOTE: Since `go test` runs all test cases in a single process, clearing +// environment has a whole process impact to other test cases. While this +// impact can be eliminated given all the tests are implemented in a similar +// pattern that those env vars will be consumed at the very begining. The test +// runner has to ensure to set a **big enough parallelism**. +func clearARMEnv() { + for _, evexp := range os.Environ() { + k, _, ok := strings.Cut(evexp, "=") + if !ok { + continue + } + if strings.HasPrefix(k, "ARM_") { + os.Unsetenv(k) + } } - - if tenantID == "" { - t.Fatalf("Missing ARM_TENANT_ID") - } - - if environment == "" { - t.Fatalf("Missing ARM_ENVIRONMENT") - } - - // location isn't used in this method, but is in the other test methods - location := os.Getenv("ARM_LOCATION") - if location == "" { - t.Fatalf("Missing ARM_LOCATION") - } - - // Endpoint is optional (only for Stack) - endpoint := os.Getenv("ARM_ENDPOINT") - - armClient, err := buildArmClient(context.TODO(), BackendConfig{ - SubscriptionID: subscriptionID, - TenantID: tenantID, - ClientID: clientID, - ClientSecret: clientSecret, - CustomResourceManagerEndpoint: endpoint, - Environment: environment, - ResourceGroupName: res.resourceGroup, - StorageAccountName: res.storageAccountName, - UseMsi: msiEnabled, - UseAzureADAuthentication: res.useAzureADAuth, - }) - if err != nil { - t.Fatalf("Failed to build ArmClient: %+v", err) - } - - return armClient } func buildSasToken(accountName, accessKey string) (*string, error) { @@ -112,6 +97,7 @@ func buildSasToken(accountName, accessKey string) (*string, error) { signedProtocol := "https,http" signedIp := "" signedVersion := sasSignedVersion + signedEncryptionScope := "" utcNow := time.Now().UTC() @@ -120,7 +106,7 @@ func buildSasToken(accountName, accessKey string) (*string, error) { endDate := utcNow.Add(time.Hour * 24).Format(time.RFC3339) sasToken, err := sasStorage.ComputeAccountSASToken(accountName, accessKey, permissions, services, resourceTypes, - startDate, endDate, signedProtocol, signedIp, signedVersion) + startDate, endDate, signedProtocol, signedIp, signedVersion, signedEncryptionScope) if err != nil { return nil, fmt.Errorf("Error computing SAS Token: %+v", err) } @@ -129,100 +115,234 @@ func buildSasToken(accountName, accessKey string) (*string, error) { } type resourceNames struct { - resourceGroup string - location string - storageAccountName string - storageContainerName string - storageKeyName string - storageAccountAccessKey string - useAzureADAuth bool + resourceGroup string + storageAccountName string + storageContainerName string + storageKeyName string } func testResourceNames(rString string, keyName string) resourceNames { return resourceNames{ resourceGroup: fmt.Sprintf("acctestRG-backend-%s-%s", strings.Replace(time.Now().Local().Format("060102150405.00"), ".", "", 1), rString), - location: os.Getenv("ARM_LOCATION"), storageAccountName: fmt.Sprintf("acctestsa%s", rString), storageContainerName: "acctestcont", storageKeyName: keyName, - useAzureADAuth: false, } } -func (c *ArmClient) buildTestResources(ctx context.Context, names *resourceNames) error { - log.Printf("Creating Resource Group %q", names.resourceGroup) - _, err := c.groupsClient.CreateOrUpdate(ctx, names.resourceGroup, resources.Group{Location: &names.location}) +type TestMeta struct { + names resourceNames + + clientId string + clientSecret string + + tenantId string + subscriptionId string + location string + env environments.Environment + + // This is populated during test resource deploying + storageAccessKey string + + // This is populated during test resoruce deploying + blobBaseUri string + + resourceGroupsClient *resourcegroups.ResourceGroupsClient + storageAccountsClient *storageaccounts.StorageAccountsClient +} + +func BuildTestMeta(t *testing.T, ctx context.Context) *TestMeta { + names := testResourceNames(randString(10), "testState") + + subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID") + if subscriptionID == "" { + t.Fatalf("Missing ARM_SUBSCRIPTION_ID") + } + + tenantID := os.Getenv("ARM_TENANT_ID") + if tenantID == "" { + t.Fatalf("Missing ARM_TENANT_ID") + } + + location := os.Getenv("ARM_LOCATION") + if location == "" { + t.Fatalf("Missing ARM_LOCATION") + } + + clientID := os.Getenv("ARM_CLIENT_ID") + clientSecret := os.Getenv("ARM_CLIENT_SECRET") + + environment := "public" + if v := os.Getenv("ARM_ENVIRONMENT"); v != "" { + environment = v + } + env, err := environments.FromName(environment) if err != nil { + t.Fatalf("Failed to build environment for %s: %v", environment, err) + } + + // For deploying test resources, we support the followings: + // - Client secret: For most of the tests + // - Client certificate: For client certificate related tests + // - MSI: For MSI related tests + // - OIDC: For OIDC related tests + authConfig := &auth.Credentials{ + Environment: *env, + TenantID: tenantID, + ClientID: clientID, + ClientSecret: clientSecret, + ClientCertificatePath: os.Getenv("ARM_CLIENT_CERTIFICATE_PATH"), + ClientCertificatePassword: os.Getenv("ARM_CLIENT_CERTIFICATE_PASSWORD"), + OIDCTokenRequestURL: getEnvvars("ACTIONS_ID_TOKEN_REQUEST_URL", "SYSTEM_OIDCREQUESTURI"), + OIDCTokenRequestToken: getEnvvars("ACTIONS_ID_TOKEN_REQUEST_TOKEN", "SYSTEM_ACCESSTOKEN"), + ADOPipelineServiceConnectionID: os.Getenv("ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID"), + + EnableAuthenticatingUsingClientSecret: true, + EnableAuthenticatingUsingClientCertificate: true, + EnableAuthenticatingUsingManagedIdentity: true, + EnableAuthenticationUsingGitHubOIDC: true, + EnableAuthenticationUsingADOPipelineOIDC: true, + } + + resourceManagerAuth, err := auth.NewAuthorizerFromCredentials(ctx, *authConfig, env.ResourceManager) + if err != nil { + t.Fatalf("unable to build authorizer for Resource Manager API: %+v", err) + } + + resourceGroupsClient, err := resourcegroups.NewResourceGroupsClientWithBaseURI(env.ResourceManager) + if err != nil { + t.Fatalf("building Resource Groups client: %+v", err) + } + resourceGroupsClient.Client.SetAuthorizer(resourceManagerAuth) + + storageAccountsClient, err := storageaccounts.NewStorageAccountsClientWithBaseURI(env.ResourceManager) + if err != nil { + t.Fatalf("building Storage Accounts client: %+v", err) + } + storageAccountsClient.Client.SetAuthorizer(resourceManagerAuth) + + return &TestMeta{ + names: names, + + clientId: clientID, + clientSecret: clientSecret, + + tenantId: tenantID, + subscriptionId: subscriptionID, + location: location, + env: *env, + + resourceGroupsClient: resourceGroupsClient, + storageAccountsClient: storageAccountsClient, + } +} + +func (c *TestMeta) buildTestResources(ctx context.Context) error { + log.Printf("Creating Resource Group %q", c.names.resourceGroup) + rgid := commonids.NewResourceGroupID(c.subscriptionId, c.names.resourceGroup) + if _, err := c.resourceGroupsClient.CreateOrUpdate(ctx, rgid, resourcegroups.ResourceGroup{Location: c.location}); err != nil { return fmt.Errorf("failed to create test resource group: %s", err) } - log.Printf("Creating Storage Account %q in Resource Group %q", names.storageAccountName, names.resourceGroup) - storageProps := armStorage.AccountCreateParameters{ - Sku: &armStorage.Sku{ - Name: armStorage.StandardLRS, - Tier: armStorage.Standard, + log.Printf("Creating Storage Account %q in Resource Group %q", c.names.storageAccountName, c.names.resourceGroup) + storageProps := storageaccounts.StorageAccountCreateParameters{ + Kind: storageaccounts.KindStorageVTwo, + Sku: storageaccounts.Sku{ + Name: storageaccounts.SkuNameStandardLRS, + Tier: pointer.To(storageaccounts.SkuTierStandard), }, - Location: &names.location, + Location: c.location, } - if names.useAzureADAuth { - allowSharedKeyAccess := false - storageProps.AccountPropertiesCreateParameters = &armStorage.AccountPropertiesCreateParameters{ - AllowSharedKeyAccess: &allowSharedKeyAccess, - } - } - future, err := c.storageAccountsClient.Create(ctx, names.resourceGroup, names.storageAccountName, storageProps) - if err != nil { + + said := commonids.NewStorageAccountID(c.subscriptionId, c.names.resourceGroup, c.names.storageAccountName) + if err := c.storageAccountsClient.CreateThenPoll(ctx, said, storageProps); err != nil { return fmt.Errorf("failed to create test storage account: %s", err) } - err = future.WaitForCompletionRef(ctx, c.storageAccountsClient.Client) + // Populate the storage account access key + resp, err := c.storageAccountsClient.GetProperties(ctx, said, storageaccounts.DefaultGetPropertiesOperationOptions()) if err != nil { - return fmt.Errorf("failed waiting for the creation of storage account: %s", err) + return fmt.Errorf("retrieving %s: %+v", said, err) + } + if resp.Model == nil { + return fmt.Errorf("unexpected null model of %s", said) + } + accountDetail, err := populateAccountDetails(said, *resp.Model) + if err != nil { + return fmt.Errorf("populating details for %s: %+v", said, err) } - containersClient := containers.NewWithEnvironment(c.environment) - if names.useAzureADAuth { - containersClient.Client.Authorizer = *c.azureAdStorageAuth - } else { - log.Printf("fetching access key for storage account") - resp, err := c.storageAccountsClient.ListKeys(ctx, names.resourceGroup, names.storageAccountName, "") - if err != nil { - return fmt.Errorf("failed to list storage account keys %s:", err) - } + accountKey, err := accountDetail.AccountKey(ctx, c.storageAccountsClient) + if err != nil { + return fmt.Errorf("listing access key for %s: %+v", said, err) + } + c.storageAccessKey = *accountKey - keys := *resp.Keys - accessKey := *keys[0].Value - names.storageAccountAccessKey = accessKey + blobBaseUri, err := accountDetail.DataPlaneEndpoint(EndpointTypeBlob) + if err != nil { + return err + } + c.blobBaseUri = *blobBaseUri - storageAuth, err := autorest.NewSharedKeyAuthorizer(names.storageAccountName, accessKey, autorest.SharedKey) - if err != nil { - return fmt.Errorf("Error building Authorizer: %+v", err) - } - - containersClient.Client.Authorizer = storageAuth + containersClient, err := containers.NewWithBaseUri(*blobBaseUri) + if err != nil { + return fmt.Errorf("failed to new container client: %v", err) } - log.Printf("Creating Container %q in Storage Account %q (Resource Group %q)", names.storageContainerName, names.storageAccountName, names.resourceGroup) - _, err = containersClient.Create(ctx, names.storageAccountName, names.storageContainerName, containers.CreateInput{}) + authorizer, err := auth.NewSharedKeyAuthorizer(c.names.storageAccountName, *accountKey, auth.SharedKey) if err != nil { + return fmt.Errorf("new shared key authorizer: %v", err) + } + containersClient.Client.Authorizer = authorizer + + log.Printf("Creating Container %q in Storage Account %q (Resource Group %q)", c.names.storageContainerName, c.names.storageAccountName, c.names.resourceGroup) + if _, err = containersClient.Create(ctx, c.names.storageContainerName, containers.CreateInput{}); err != nil { return fmt.Errorf("failed to create storage container: %s", err) } return nil } -func (c ArmClient) destroyTestResources(ctx context.Context, resources resourceNames) error { - log.Printf("[DEBUG] Deleting Resource Group %q..", resources.resourceGroup) - future, err := c.groupsClient.Delete(ctx, resources.resourceGroup) - if err != nil { +func (c *TestMeta) destroyTestResources(ctx context.Context) error { + log.Printf("[DEBUG] Deleting Resource Group %q..", c.names.resourceGroup) + rgid := commonids.NewResourceGroupID(c.subscriptionId, c.names.resourceGroup) + if err := c.resourceGroupsClient.DeleteThenPoll(ctx, rgid, resourcegroups.DefaultDeleteOperationOptions()); err != nil { return fmt.Errorf("Error deleting Resource Group: %+v", err) } - - log.Printf("[DEBUG] Waiting for deletion of Resource Group %q..", resources.resourceGroup) - err = future.WaitForCompletionRef(ctx, c.groupsClient.Client) - if err != nil { - return fmt.Errorf("Error waiting for the deletion of Resource Group: %+v", err) - } - return nil } + +func (c *TestMeta) getBlobClient(ctx context.Context) (bc *blobs.Client, err error) { + blobsClient, err := blobs.NewWithBaseUri(c.blobBaseUri) + if err != nil { + return nil, fmt.Errorf("new blob client: %v", err) + } + + authorizer, err := auth.NewSharedKeyAuthorizer(c.names.storageAccountName, c.storageAccessKey, auth.SharedKey) + if err != nil { + return nil, fmt.Errorf("new shared key authorizer: %v", err) + } + blobsClient.Client.SetAuthorizer(authorizer) + + return blobsClient, nil +} + +// randString generates a random alphanumeric string of the length specified +func randString(strlen int) string { + const charSet = "abcdefghijklmnopqrstuvwxyz012346789" + result := make([]byte, strlen) + for i := 0; i < strlen; i++ { + result[i] = charSet[rand.Intn(len(charSet))] + } + return string(result) +} + +// getEnvvars return the first non-empty env var specified. If none is found, it returns empty string. +func getEnvvars(envvars ...string) string { + for _, envvar := range envvars { + if v := os.Getenv(envvar); v != "" { + return v + } + } + return "" +} diff --git a/internal/backend/remote-state/azure/sender.go b/internal/backend/remote-state/azure/sender.go deleted file mode 100644 index 958273e83d..0000000000 --- a/internal/backend/remote-state/azure/sender.go +++ /dev/null @@ -1,64 +0,0 @@ -package azure - -import ( - "log" - "net/http" - "net/http/httputil" - - "github.com/Azure/go-autorest/autorest" - "github.com/hashicorp/terraform/internal/logging" -) - -func buildSender() autorest.Sender { - return autorest.DecorateSender(&http.Client{ - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - }, - }, withRequestLogging()) -} - -func withRequestLogging() autorest.SendDecorator { - return func(s autorest.Sender) autorest.Sender { - return autorest.SenderFunc(func(r *http.Request) (*http.Response, error) { - // only log if logging's enabled - logLevel := logging.CurrentLogLevel() - if logLevel == "" { - return s.Do(r) - } - - // strip the authorization header prior to printing - authHeaderName := "Authorization" - auth := r.Header.Get(authHeaderName) - if auth != "" { - r.Header.Del(authHeaderName) - } - - // dump request to wire format - if dump, err := httputil.DumpRequestOut(r, true); err == nil { - log.Printf("[DEBUG] Azure Backend Request: \n%s\n", dump) - } else { - // fallback to basic message - log.Printf("[DEBUG] Azure Backend Request: %s to %s\n", r.Method, r.URL) - } - - // add the auth header back - if auth != "" { - r.Header.Add(authHeaderName, auth) - } - - resp, err := s.Do(r) - if resp != nil { - // dump response to wire format - if dump, err2 := httputil.DumpResponse(resp, true); err2 == nil { - log.Printf("[DEBUG] Azure Backend Response for %s: \n%s\n", r.URL, dump) - } else { - // fallback to basic message - log.Printf("[DEBUG] Azure Backend Response: %s for %s\n", resp.Status, r.URL) - } - } else { - log.Printf("[DEBUG] Request to %s completed with no response", r.URL) - } - return resp, err - }) - } -} diff --git a/internal/backend/remote-state/azure/storage_client_helpers.go b/internal/backend/remote-state/azure/storage_client_helpers.go new file mode 100644 index 0000000000..61197a705d --- /dev/null +++ b/internal/backend/remote-state/azure/storage_client_helpers.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package azure + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "github.com/hashicorp/go-azure-sdk/resource-manager/storage/2023-01-01/storageaccounts" + "github.com/hashicorp/go-azure-sdk/sdk/environments" +) + +// This file is referencing the terraform-provider-azurerm: internal/services/storage/client/helpers.go + +type EndpointType string + +const ( + EndpointTypeBlob = "blob" + EndpointTypeDfs = "dfs" + EndpointTypeFile = "file" + EndpointTypeQueue = "queue" + EndpointTypeTable = "table" +) + +type AccountDetails struct { + Kind storageaccounts.Kind + IsHnsEnabled bool + StorageAccountId commonids.StorageAccountId + + accountKey *string + + // primaryBlobEndpoint is the Primary Blob Endpoint for the Data Plane API for this Storage Account + // e.g. `https://{account}.blob.core.windows.net` + primaryBlobEndpoint *string + + // primaryDfsEndpoint is the Primary Dfs Endpoint for the Data Plane API for this Storage Account + // e.g. `https://sale.dfs.core.windows.net` + primaryDfsEndpoint *string + + // primaryFileEndpoint is the Primary File Endpoint for the Data Plane API for this Storage Account + // e.g. `https://{account}.file.core.windows.net` + primaryFileEndpoint *string + + // primaryQueueEndpoint is the Primary Queue Endpoint for the Data Plane API for this Storage Account + // e.g. `https://{account}.queue.core.windows.net` + primaryQueueEndpoint *string + + // primaryTableEndpoint is the Primary Table Endpoint for the Data Plane API for this Storage Account + // e.g. `https://{account}.table.core.windows.net` + primaryTableEndpoint *string +} + +func (ad *AccountDetails) AccountKey(ctx context.Context, client *storageaccounts.StorageAccountsClient) (*string, error) { + if ad.accountKey != nil { + return ad.accountKey, nil + } + + opts := storageaccounts.DefaultListKeysOperationOptions() + opts.Expand = pointer.To(storageaccounts.ListKeyExpandKerb) + listKeysResp, err := client.ListKeys(ctx, ad.StorageAccountId, opts) + if err != nil { + return nil, fmt.Errorf("listing Keys for %s: %+v", ad.StorageAccountId, err) + } + + if model := listKeysResp.Model; model != nil && model.Keys != nil { + for _, key := range *model.Keys { + if key.Permissions == nil || key.Value == nil { + continue + } + + if *key.Permissions == storageaccounts.KeyPermissionFull { + ad.accountKey = key.Value + break + } + } + } + + if ad.accountKey == nil { + return nil, fmt.Errorf("unable to determine the Write Key for %s", ad.StorageAccountId) + } + + return ad.accountKey, nil +} + +func (ad *AccountDetails) DataPlaneEndpoint(endpointType EndpointType) (*string, error) { + var baseUri *string + switch endpointType { + case EndpointTypeBlob: + baseUri = ad.primaryBlobEndpoint + + case EndpointTypeDfs: + baseUri = ad.primaryDfsEndpoint + + case EndpointTypeFile: + baseUri = ad.primaryFileEndpoint + + case EndpointTypeQueue: + baseUri = ad.primaryQueueEndpoint + + case EndpointTypeTable: + baseUri = ad.primaryTableEndpoint + + default: + return nil, fmt.Errorf("internal-error: unrecognised endpoint type %q when building storage client", endpointType) + } + + if baseUri == nil { + return nil, fmt.Errorf("determining %s endpoint for %s: missing primary endpoint", endpointType, ad.StorageAccountId) + } + return baseUri, nil +} + +func populateAccountDetails(accountId commonids.StorageAccountId, account storageaccounts.StorageAccount) (*AccountDetails, error) { + out := AccountDetails{ + Kind: pointer.From(account.Kind), + StorageAccountId: accountId, + } + + if account.Properties == nil { + return nil, fmt.Errorf("populating details for %s: `model.Properties` was nil", accountId) + } + if account.Properties.PrimaryEndpoints == nil { + return nil, fmt.Errorf("populating details for %s: `model.Properties.PrimaryEndpoints` was nil", accountId) + } + + props := *account.Properties + out.IsHnsEnabled = pointer.From(props.IsHnsEnabled) + + endpoints := *props.PrimaryEndpoints + if endpoints.Blob != nil { + endpoint := strings.TrimSuffix(*endpoints.Blob, "/") + out.primaryBlobEndpoint = pointer.To(endpoint) + } + if endpoints.Dfs != nil { + endpoint := strings.TrimSuffix(*endpoints.Dfs, "/") + out.primaryDfsEndpoint = pointer.To(endpoint) + } + if endpoints.File != nil { + endpoint := strings.TrimSuffix(*endpoints.File, "/") + out.primaryFileEndpoint = pointer.To(endpoint) + } + if endpoints.Queue != nil { + endpoint := strings.TrimSuffix(*endpoints.Queue, "/") + out.primaryQueueEndpoint = pointer.To(endpoint) + } + if endpoints.Table != nil { + endpoint := strings.TrimSuffix(*endpoints.Table, "/") + out.primaryTableEndpoint = pointer.To(endpoint) + } + + return &out, nil +} + +// naiveStorageAccountBlobBaseURL naively construct the storage account blob endpoint URL instead of +// learning from the storage account response. This can be incorrect if private dns zone is used. +func naiveStorageAccountBlobBaseURL(e environments.Environment, accountName string) (string, error) { + pDomainSuffix, ok := e.Storage.DomainSuffix() + if !ok { + return "", fmt.Errorf("no storage domain suffix defined for environment: %s", e.Name) + } + return fmt.Sprintf("https://%s.blob.%s", accountName, *pDomainSuffix), nil +} diff --git a/internal/backend/remote-state/consul/backend.go b/internal/backend/remote-state/consul/backend.go index 8846969813..8c2270d760 100644 --- a/internal/backend/remote-state/consul/backend.go +++ b/internal/backend/remote-state/consul/backend.go @@ -1,120 +1,107 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package consul import ( - "context" "net" "strings" "time" consulapi "github.com/hashicorp/consul/api" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" ) // New creates a new backend for Consul remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "path": &schema.Schema{ - Type: schema.TypeString, - Required: true, - Description: "Path to store state in Consul", - }, - - "access_token": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Access token for a Consul ACL", - Default: "", // To prevent input - }, - - "address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Address to the Consul Cluster", - Default: "", // To prevent input - }, - - "scheme": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Scheme to communicate to Consul with", - Default: "", // To prevent input - }, - - "datacenter": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "Datacenter to communicate with", - Default: "", // To prevent input - }, - - "http_auth": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "HTTP Auth in the format of 'username:password'", - Default: "", // To prevent input - }, - - "gzip": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Description: "Compress the state data using gzip", - Default: false, - }, - - "lock": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Description: "Lock state access", - Default: true, - }, - - "ca_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "A path to a PEM-encoded certificate authority used to verify the remote agent's certificate.", - DefaultFunc: schema.EnvDefaultFunc("CONSUL_CACERT", ""), - }, - - "cert_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "A path to a PEM-encoded certificate provided to the remote agent; requires use of key_file.", - DefaultFunc: schema.EnvDefaultFunc("CONSUL_CLIENT_CERT", ""), - }, - - "key_file": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "A path to a PEM-encoded private key, required if cert_file is specified.", - DefaultFunc: schema.EnvDefaultFunc("CONSUL_CLIENT_KEY", ""), + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "path": { + Type: cty.String, + Required: true, + Description: "Path to store state in Consul", + }, + "access_token": { + Type: cty.String, + Optional: true, + Description: "Access token for a Consul ACL", + }, + "address": { + Type: cty.String, + Optional: true, + Description: "Address to the Consul Cluster", + }, + "scheme": { + Type: cty.String, + Optional: true, + Description: "Scheme to communicate to Consul with", + }, + "datacenter": { + Type: cty.String, + Optional: true, + Description: "Datacenter to communicate with", + }, + "http_auth": { + Type: cty.String, + Optional: true, + Description: "HTTP Auth in the format of 'username:password'", + }, + "gzip": { + Type: cty.Bool, + Optional: true, + Description: "Compress the state data using gzip", + }, + "lock": { + Type: cty.Bool, + Optional: true, + Description: "Lock state access", + }, + "ca_file": { + Type: cty.String, + Optional: true, + Description: "A path to a PEM-encoded certificate authority used to verify the remote agent's certificate", + }, + "cert_file": { + Type: cty.String, + Optional: true, + Description: "A path to a PEM-encoded certificate provided to the remote agent; requires use of key_file", + }, + "key_file": { + Type: cty.String, + Optional: true, + Description: "A path to a PEM-encoded private key, required if cert_file is specified", + }, + }, }, }, } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result } type Backend struct { - *schema.Backend + backendbase.Base // The fields below are set from configure - client *consulapi.Client - configData *schema.ResourceData - lock bool + client *consulapi.Client + path string + gzip bool + lock bool } -func (b *Backend) configure(ctx context.Context) error { - // Grab the resource data - b.configData = schema.FromContextBackendConfig(ctx) - - // Store the lock information - b.lock = b.configData.Get("lock").(bool) - - data := b.configData +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { + b.path = configVal.GetAttr("path").AsString() + b.gzip = backendbase.MustBoolValue( + backendbase.GetAttrDefault(configVal, "gzip", cty.False), + ) + b.lock = backendbase.MustBoolValue( + backendbase.GetAttrDefault(configVal, "lock", cty.True), + ) // Configure the client config := consulapi.DefaultConfig() @@ -122,31 +109,32 @@ func (b *Backend) configure(ctx context.Context) error { // replace the default Transport Dialer to reduce the KeepAlive config.Transport.DialContext = dialContext - if v, ok := data.GetOk("access_token"); ok && v.(string) != "" { - config.Token = v.(string) + empty := cty.StringVal("") + if v := backendbase.GetAttrDefault(configVal, "access_token", empty); v != empty { + config.Token = v.AsString() } - if v, ok := data.GetOk("address"); ok && v.(string) != "" { - config.Address = v.(string) + if v := backendbase.GetAttrDefault(configVal, "address", empty); v != empty { + config.Address = v.AsString() } - if v, ok := data.GetOk("scheme"); ok && v.(string) != "" { - config.Scheme = v.(string) + if v := backendbase.GetAttrDefault(configVal, "scheme", empty); v != empty { + config.Scheme = v.AsString() } - if v, ok := data.GetOk("datacenter"); ok && v.(string) != "" { - config.Datacenter = v.(string) + if v := backendbase.GetAttrDefault(configVal, "datacenter", empty); v != empty { + config.Datacenter = v.AsString() } - if v, ok := data.GetOk("ca_file"); ok && v.(string) != "" { - config.TLSConfig.CAFile = v.(string) + if v := backendbase.GetAttrEnvDefaultFallback(configVal, "ca_file", "CONSUL_CACERT", empty); v != empty { + config.TLSConfig.CAFile = v.AsString() } - if v, ok := data.GetOk("cert_file"); ok && v.(string) != "" { - config.TLSConfig.CertFile = v.(string) + if v := backendbase.GetAttrEnvDefaultFallback(configVal, "cert_file", "CONSUL_CLIENT_CERT", empty); v != empty { + config.TLSConfig.CertFile = v.AsString() } - if v, ok := data.GetOk("key_file"); ok && v.(string) != "" { - config.TLSConfig.KeyFile = v.(string) + if v := backendbase.GetAttrEnvDefaultFallback(configVal, "key_file", "CONSUL_CLIENT_KEY", empty); v != empty { + config.TLSConfig.KeyFile = v.AsString() } - if v, ok := data.GetOk("http_auth"); ok && v.(string) != "" { - auth := v.(string) + if v := backendbase.GetAttrDefault(configVal, "http_auth", empty); v != empty { + auth := v.AsString() var username, password string if strings.Contains(auth, ":") { @@ -165,7 +153,7 @@ func (b *Backend) configure(ctx context.Context) error { client, err := consulapi.NewClient(config) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } b.client = client diff --git a/internal/backend/remote-state/consul/backend_state.go b/internal/backend/remote-state/consul/backend_state.go index be1841eb8d..779102b61d 100644 --- a/internal/backend/remote-state/consul/backend_state.go +++ b/internal/backend/remote-state/consul/backend_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package consul import ( @@ -16,7 +19,7 @@ const ( func (b *Backend) Workspaces() ([]string, error) { // List our raw path - prefix := b.configData.Get("path").(string) + keyEnvPrefix + prefix := b.path + keyEnvPrefix keys, _, err := b.client.KV().Keys(prefix, "/", nil) if err != nil { return nil, err @@ -49,13 +52,13 @@ func (b *Backend) Workspaces() ([]string, error) { return result, nil } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } // Determine the path of the data - path := b.path(name) + path := b.statePath(name) // Delete it. We just delete it without any locking since // the DeleteState API is documented as such. @@ -65,10 +68,10 @@ func (b *Backend) DeleteWorkspace(name string) error { func (b *Backend) StateMgr(name string) (statemgr.Full, error) { // Determine the path of the data - path := b.path(name) + path := b.statePath(name) // Determine whether to gzip or not - gzip := b.configData.Get("gzip").(bool) + gzip := b.gzip // Build the state client var stateMgr = &remote.State{ @@ -120,7 +123,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { err = lockUnlock(err) return nil, err } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { err = lockUnlock(err) return nil, err } @@ -134,8 +137,8 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { return stateMgr, nil } -func (b *Backend) path(name string) string { - path := b.configData.Get("path").(string) +func (b *Backend) statePath(name string) string { + path := b.path if name != backend.DefaultStateName { path += fmt.Sprintf("%s%s", keyEnvPrefix, name) } diff --git a/internal/backend/remote-state/consul/backend_test.go b/internal/backend/remote-state/consul/backend_test.go index 6b6700825a..4a7e2fdfda 100644 --- a/internal/backend/remote-state/consul/backend_test.go +++ b/internal/backend/remote-state/consul/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package consul import ( diff --git a/internal/backend/remote-state/consul/client.go b/internal/backend/remote-state/consul/client.go index 31dfecb0fd..03ab51c31b 100644 --- a/internal/backend/remote-state/consul/client.go +++ b/internal/backend/remote-state/consul/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package consul import ( @@ -14,7 +17,7 @@ import ( "time" consulapi "github.com/hashicorp/consul/api" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" ) @@ -231,7 +234,11 @@ func (c *RemoteClient) Put(data []byte) error { } // transaction was rolled back if !ok { - return fmt.Errorf("consul CAS failed with transaction errors: %v", resp.Errors) + var resultErr error + for _, respError := range resp.Errors { + resultErr = errors.Join(resultErr, errors.New(respError.What)) + } + return fmt.Errorf("consul CAS failed with transaction errors: %w", resultErr) } if len(resp.Results) != 1 { @@ -443,7 +450,7 @@ func (c *RemoteClient) lock() (string, error) { err = c.putLockInfo(c.info) if err != nil { if unlockErr := c.unlock(c.info.ID); unlockErr != nil { - err = multierror.Append(err, unlockErr) + err = errors.Join(err, unlockErr) } return "", err @@ -595,11 +602,11 @@ func (c *RemoteClient) unlock(id string) error { var errs error if _, err := kv.Delete(c.lockPath()+lockInfoSuffix, nil); err != nil { - errs = multierror.Append(errs, err) + errs = errors.Join(errs, err) } if err := c.consulLock.Unlock(); err != nil { - errs = multierror.Append(errs, err) + errs = errors.Join(errs, err) } // the monitoring goroutine may be in a select on the lockCh, so we need to diff --git a/internal/backend/remote-state/consul/client_test.go b/internal/backend/remote-state/consul/client_test.go index 2a4acf06b9..2c887ba53b 100644 --- a/internal/backend/remote-state/consul/client_test.go +++ b/internal/backend/remote-state/consul/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package consul import ( diff --git a/internal/backend/remote-state/consul/go.mod b/internal/backend/remote-state/consul/go.mod new file mode 100644 index 0000000000..bbfd31de80 --- /dev/null +++ b/internal/backend/remote-state/consul/go.mod @@ -0,0 +1,76 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/consul + +go 1.24.2 + +require ( + github.com/hashicorp/consul/api v1.13.0 + github.com/hashicorp/consul/sdk v0.8.0 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/zclconf/go-cty v1.16.2 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-immutable-radix v1.0.0 // indirect + github.com/hashicorp/go-msgpack v0.5.4 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-sockaddr v1.0.2 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/golang-lru v0.5.1 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/serf v0.9.6 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pascaldekloe/goe v0.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/consul/go.sum b/internal/backend/remote-state/consul/go.sum new file mode 100644 index 0000000000..2a8cf6ef17 --- /dev/null +++ b/internal/backend/remote-state/consul/go.sum @@ -0,0 +1,688 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da h1:8GUt8eRujhVEGZFFEjBj46YV4rDjvGrNxb0KMWYkL2I= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/consul/api v1.13.0 h1:2hnLQ0GjQvw7f3O61jMO8gbasZviZTrt9R8WzgiirHc= +github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= +github.com/hashicorp/consul/sdk v0.8.0 h1:OJtKBtEjboEZvG6AOUdh4Z1Zbyu0WcxQ0qatRrZHTVU= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +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-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-msgpack v0.5.4 h1:SFT72YqIkOcLdWJUYcriVX7hbrZpwc/f7h8aW2NUqrA= +github.com/hashicorp/go-msgpack v0.5.4/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.3.0 h1:8+567mCcFDnS5ADl7lrpxPMWiFCElyUEeW0gtj34fMA= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.6 h1:uuEX1kLR6aoda1TBttmJQKDLZE1Ob7KN0NPdE7EtCDc= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/cos/backend.go b/internal/backend/remote-state/cos/backend.go index 667fdd81dd..fe5fab41b8 100644 --- a/internal/backend/remote-state/cos/backend.go +++ b/internal/backend/remote-state/cos/backend.go @@ -1,35 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cos import ( "context" + "encoding/json" "fmt" + "io" + "io/ioutil" + "log" "net/http" "net/url" + "os" + "regexp" + "runtime" + "strconv" "strings" "time" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/mitchellh/go-homedir" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + sts "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts/v20180813" tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" "github.com/tencentyun/cos-go-sdk-v5" ) // Default value from environment variable const ( - PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" - PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" - PROVIDER_REGION = "TENCENTCLOUD_REGION" + PROVIDER_SECRET_ID = "TENCENTCLOUD_SECRET_ID" + PROVIDER_SECRET_KEY = "TENCENTCLOUD_SECRET_KEY" + PROVIDER_SECURITY_TOKEN = "TENCENTCLOUD_SECURITY_TOKEN" + PROVIDER_REGION = "TENCENTCLOUD_REGION" + PROVIDER_ENDPOINT = "TENCENTCLOUD_ENDPOINT" + PROVIDER_DOMAIN = "TENCENTCLOUD_DOMAIN" + PROVIDER_ASSUME_ROLE_ARN = "TENCENTCLOUD_ASSUME_ROLE_ARN" + PROVIDER_ASSUME_ROLE_SESSION_NAME = "TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME" + PROVIDER_ASSUME_ROLE_SESSION_DURATION = "TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION" + PROVIDER_ASSUME_ROLE_EXTERNAL_ID = "TENCENTCLOUD_ASSUME_ROLE_EXTERNAL_ID" + PROVIDER_SHARED_CREDENTIALS_DIR = "TENCENTCLOUD_SHARED_CREDENTIALS_DIR" + PROVIDER_PROFILE = "TENCENTCLOUD_PROFILE" + PROVIDER_CAM_ROLE_NAME = "TENCENTCLOUD_CAM_ROLE_NAME" +) + +const ( + DEFAULT_PROFILE = "default" ) // Backend implements "backend".Backend for tencentCloud cos type Backend struct { *schema.Backend + credential *common.Credential cosContext context.Context cosClient *cos.Client tagClient *tag.Client + stsClient *sts.Client region string bucket string @@ -37,6 +66,16 @@ type Backend struct { key string encrypt bool acl string + domain string +} + +type CAMResponse struct { + TmpSecretId string `json:"TmpSecretId"` + TmpSecretKey string `json:"TmpSecretKey"` + ExpiredTime int64 `json:"ExpiredTime"` + Expiration string `json:"Expiration"` + Token string `json:"Token"` + Code string `json:"Code"` } // New creates a new backend for TencentCloud cos remote state. @@ -45,17 +84,24 @@ func New() backend.Backend { Schema: map[string]*schema.Schema{ "secret_id": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_ID, nil), Description: "Secret id of Tencent Cloud", }, "secret_key": { Type: schema.TypeString, - Required: true, + Optional: true, DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECRET_KEY, nil), Description: "Secret key of Tencent Cloud", Sensitive: true, }, + "security_token": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SECURITY_TOKEN, nil), + Description: "TencentCloud Security Token of temporary access credentials. It can be sourced from the `TENCENTCLOUD_SECURITY_TOKEN` environment variable. Notice: for supported products, please refer to: [temporary key supported products](https://intl.cloud.tencent.com/document/product/598/10588).", + Sensitive: true, + }, "region": { Type: schema.TypeString, Required: true, @@ -68,6 +114,18 @@ func New() backend.Backend { Required: true, Description: "The name of the COS bucket", }, + "endpoint": { + Type: schema.TypeString, + Optional: true, + Description: "The custom endpoint for the COS API, e.g. http://cos-internal.{Region}.tencentcos.cn. Both HTTP and HTTPS are accepted.", + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ENDPOINT, nil), + }, + "domain": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_DOMAIN, nil), + Description: "The root domain of the API request. Default is tencentcloudapi.com.", + }, "prefix": { Type: schema.TypeString, Optional: true, @@ -119,6 +177,69 @@ func New() backend.Backend { Description: "Whether to enable global Acceleration", Default: false, }, + "assume_role": { + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + Description: "The `assume_role` block. If provided, terraform will attempt to assume this role using the supplied credentials.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "role_arn": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_ARN, nil), + Description: "The ARN of the role to assume. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_ARN`.", + }, + "session_name": { + Type: schema.TypeString, + Required: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_SESSION_NAME, nil), + Description: "The session name to use when making the AssumeRole call. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME`.", + }, + "session_duration": { + Type: schema.TypeInt, + Required: true, + DefaultFunc: func() (interface{}, error) { + if v := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_DURATION); v != "" { + return strconv.Atoi(v) + } + return 7200, nil + }, + ValidateFunc: validateIntegerInRange(0, 43200), + Description: "The duration of the session when making the AssumeRole call. Its value ranges from 0 to 43200(seconds), and default is 7200 seconds. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION`.", + }, + "policy": { + Type: schema.TypeString, + Optional: true, + Description: "A more restrictive policy when making the AssumeRole call. Its content must not contains `principal` elements. Notice: more syntax references, please refer to: [policies syntax logic](https://intl.cloud.tencent.com/document/product/598/10603).", + }, + "external_id": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_ASSUME_ROLE_EXTERNAL_ID, nil), + Description: "External role ID, which can be obtained by clicking the role name in the CAM console. It can contain 2-128 letters, digits, and symbols (=,.@:/-). Regex: [\\w+=,.@:/-]*. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_EXTERNAL_ID`.", + }, + }, + }, + }, + "shared_credentials_dir": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_SHARED_CREDENTIALS_DIR, nil), + Description: "The directory of the shared credentials. It can also be sourced from the `TENCENTCLOUD_SHARED_CREDENTIALS_DIR` environment variable. If not set this defaults to ~/.tccli.", + }, + "profile": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_PROFILE, nil), + Description: "The profile name as set in the shared credentials. It can also be sourced from the `TENCENTCLOUD_PROFILE` environment variable. If not set, the default profile created with `tccli configure` will be used.", + }, + "cam_role_name": { + Type: schema.TypeString, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc(PROVIDER_CAM_ROLE_NAME, nil), + Description: "The name of the CVM instance CAM role. It can be sourced from the `TENCENTCLOUD_CAM_ROLE_NAME` environment variable.", + }, }, } @@ -128,6 +249,21 @@ func New() backend.Backend { return result } +func validateIntegerInRange(min, max int64) schema.SchemaValidateFunc { + return func(v interface{}, k string) (ws []string, errors []error) { + value := int64(v.(int)) + if value < min { + errors = append(errors, fmt.Errorf( + "%q cannot be lower than %d: %d", k, min, value)) + } + if value > max { + errors = append(errors, fmt.Errorf( + "%q cannot be higher than %d: %d", k, max, value)) + } + return + } +} + // configure init cos client func (b *Backend) configure(ctx context.Context) error { if b.cosClient != nil { @@ -158,27 +294,358 @@ func (b *Backend) configure(ctx context.Context) error { return err } + if v, ok := data.GetOk("domain"); ok { + b.domain = v.(string) + log.Printf("[DEBUG] Backend: set domain for TencentCloud API client. Domain: [%s]", b.domain) + } + // set url as endpoint when provided + // "http://{Bucket}.cos-internal.{Region}.tencentcos.cn" + if v, ok := data.GetOk("endpoint"); ok { + endpoint := v.(string) + + re := regexp.MustCompile(`^(http(s)?)://cos-internal\.([^.]+)\.tencentcos\.cn$`) + matches := re.FindStringSubmatch(endpoint) + if len(matches) != 4 { + return fmt.Errorf("Invalid URL: %v must be: %v", endpoint, "http(s)://cos-internal.{Region}.tencentcos.cn") + } + + protocol := matches[1] + region := matches[3] + + // URL after converting + newUrl := fmt.Sprintf("%s://%s.cos-internal.%s.tencentcos.cn", protocol, b.bucket, region) + u, err = url.Parse(newUrl) + log.Printf("[DEBUG] Backend: set COS URL as: [%s]", newUrl) + } + if err != nil { + return err + } + + var getProviderConfig = func(key string) string { + var str string + value, err := getConfigFromProfile(data, key) + if err == nil && value != nil { + str = value.(string) + } + + return str + } + + var ( + secretId string + secretKey string + securityToken string + ) + + // get auth from tf/env + if v, ok := data.GetOk("secret_id"); ok { + secretId = v.(string) + } + + if v, ok := data.GetOk("secret_key"); ok { + secretKey = v.(string) + } + + if v, ok := data.GetOk("security_token"); ok { + securityToken = v.(string) + } + + // get auth from tccli + if secretId == "" && secretKey == "" && securityToken == "" { + secretId = getProviderConfig("secretId") + secretKey = getProviderConfig("secretKey") + securityToken = getProviderConfig("token") + } + + // get auth from CAM role name + if v, ok := data.GetOk("cam_role_name"); ok { + camRoleName := v.(string) + if camRoleName != "" { + camResp, err := getAuthFromCAM(camRoleName) + if err != nil { + return err + } + + secretId = camResp.TmpSecretId + secretKey = camResp.TmpSecretKey + securityToken = camResp.Token + } + } + + // init credential by AKSK & TOKEN + b.credential = common.NewTokenCredential(secretId, secretKey, securityToken) + // update credential if assume role exist + err = handleAssumeRole(data, b) + if err != nil { + return err + } + b.cosClient = cos.NewClient( &cos.BaseURL{BucketURL: u}, &http.Client{ Timeout: 60 * time.Second, Transport: &cos.AuthorizationTransport{ - SecretID: data.Get("secret_id").(string), - SecretKey: data.Get("secret_key").(string), + SecretID: b.credential.SecretId, + SecretKey: b.credential.SecretKey, + SessionToken: b.credential.Token, }, }, ) - credential := common.NewCredential( - data.Get("secret_id").(string), - data.Get("secret_key").(string), - ) - - cpf := profile.NewClientProfile() - cpf.HttpProfile.ReqMethod = "POST" - cpf.HttpProfile.ReqTimeout = 300 - cpf.Language = "en-US" - b.tagClient, err = tag.NewClient(credential, b.region, cpf) - + b.tagClient = b.UseTagClient() return err } + +func handleAssumeRole(data *schema.ResourceData, b *Backend) error { + var ( + assumeRoleArn string + assumeRoleSessionName string + assumeRoleSessionDuration int + assumeRolePolicy string + assumeRoleExternalId string + ) + + // get assume role from credential + if providerConfig["role-arn"] != nil { + assumeRoleArn = providerConfig["role-arn"].(string) + } + + if providerConfig["role-session-name"] != nil { + assumeRoleSessionName = providerConfig["role-session-name"].(string) + } + + if assumeRoleArn != "" && assumeRoleSessionName != "" { + assumeRoleSessionDuration = 7200 + } + + // get assume role from env + envRoleArn := os.Getenv(PROVIDER_ASSUME_ROLE_ARN) + envSessionName := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_NAME) + if envRoleArn != "" && envSessionName != "" { + assumeRoleArn = envRoleArn + assumeRoleSessionName = envSessionName + if envSessionDuration := os.Getenv(PROVIDER_ASSUME_ROLE_SESSION_DURATION); envSessionDuration != "" { + var err error + assumeRoleSessionDuration, err = strconv.Atoi(envSessionDuration) + if err != nil { + return err + } + } + + if assumeRoleSessionDuration == 0 { + assumeRoleSessionDuration = 7200 + } + + assumeRoleExternalId = os.Getenv(PROVIDER_ASSUME_ROLE_EXTERNAL_ID) + } + + // get assume role from tf + assumeRoleList := data.Get("assume_role").(*schema.Set).List() + if len(assumeRoleList) == 1 { + assumeRole := assumeRoleList[0].(map[string]interface{}) + assumeRoleArn = assumeRole["role_arn"].(string) + assumeRoleSessionName = assumeRole["session_name"].(string) + assumeRoleSessionDuration = assumeRole["session_duration"].(int) + assumeRolePolicy = assumeRole["policy"].(string) + assumeRoleExternalId = assumeRole["external_id"].(string) + } + + if assumeRoleArn != "" && assumeRoleSessionName != "" { + err := b.updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName, assumeRoleSessionDuration, assumeRolePolicy, assumeRoleExternalId) + if err != nil { + return err + } + } + + return nil +} + +func (b *Backend) updateCredentialWithSTS(assumeRoleArn, assumeRoleSessionName string, assumeRoleSessionDuration int, assumeRolePolicy string, assumeRoleExternalId string) error { + // assume role by STS + request := sts.NewAssumeRoleRequest() + request.RoleArn = &assumeRoleArn + request.RoleSessionName = &assumeRoleSessionName + duration := uint64(assumeRoleSessionDuration) + request.DurationSeconds = &duration + if assumeRolePolicy != "" { + policy := url.QueryEscape(assumeRolePolicy) + request.Policy = &policy + } + + if assumeRoleExternalId != "" { + request.ExternalId = &assumeRoleExternalId + } + + response, err := b.UseStsClient().AssumeRole(request) + if err != nil { + return err + } + // update credentials by result of assume role + b.credential = common.NewTokenCredential( + *response.Response.Credentials.TmpSecretId, + *response.Response.Credentials.TmpSecretKey, + *response.Response.Credentials.Token, + ) + + return nil +} + +// UseStsClient returns sts client for service +func (b *Backend) UseStsClient() *sts.Client { + if b.stsClient != nil { + return b.stsClient + } + cpf := b.NewClientProfile(300) + b.stsClient, _ = sts.NewClient(b.credential, b.region, cpf) + b.stsClient.WithHttpTransport(&LogRoundTripper{}) + + return b.stsClient +} + +// UseTagClient returns tag client for service +func (b *Backend) UseTagClient() *tag.Client { + if b.tagClient != nil { + return b.tagClient + } + cpf := b.NewClientProfile(300) + cpf.Language = "en-US" + b.tagClient, _ = tag.NewClient(b.credential, b.region, cpf) + return b.tagClient +} + +// NewClientProfile returns a new ClientProfile +func (b *Backend) NewClientProfile(timeout int) *profile.ClientProfile { + cpf := profile.NewClientProfile() + + // all request use method POST + cpf.HttpProfile.ReqMethod = "POST" + // request timeout + cpf.HttpProfile.ReqTimeout = timeout + // request domain + cpf.HttpProfile.RootDomain = b.domain + + return cpf +} + +func getAuthFromCAM(roleName string) (camResp *CAMResponse, err error) { + url := fmt.Sprintf("http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials/%s", roleName) + log.Printf("[CRITAL] Request CAM security credentials url: %s\n", url) + // maximum waiting time + client := &http.Client{Timeout: 2 * time.Second} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return + } + + resp, err := client.Do(req) + if err != nil { + log.Printf("[CRITAL] Request CAM security credentials resp err: %s", err.Error()) + return + } + + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("[CRITAL] Request CAM security credentials body read err: %s", err.Error()) + return + } + + err = json.Unmarshal(body, &camResp) + if err != nil { + log.Printf("[CRITAL] Request CAM security credentials resp json err: %s", err.Error()) + return + } + + return +} + +var providerConfig map[string]interface{} + +func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{}, error) { + if providerConfig == nil { + var ( + profile string + sharedCredentialsDir string + credentialPath string + configurePath string + ) + + if v, ok := d.GetOk("profile"); ok { + profile = v.(string) + } else { + profile = DEFAULT_PROFILE + } + + if v, ok := d.GetOk("shared_credentials_dir"); ok { + sharedCredentialsDir = v.(string) + } + + tmpSharedCredentialsDir, err := homedir.Expand(sharedCredentialsDir) + if err != nil { + return nil, err + } + + if tmpSharedCredentialsDir == "" { + credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("HOME"), profile) + configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("HOME"), profile) + if runtime.GOOS == "windows" { + credentialPath = fmt.Sprintf("%s/.tccli/%s.credential", os.Getenv("USERPROFILE"), profile) + configurePath = fmt.Sprintf("%s/.tccli/%s.configure", os.Getenv("USERPROFILE"), profile) + } + } else { + credentialPath = fmt.Sprintf("%s/%s.credential", tmpSharedCredentialsDir, profile) + configurePath = fmt.Sprintf("%s/%s.configure", tmpSharedCredentialsDir, profile) + } + + providerConfig = make(map[string]interface{}) + _, err = os.Stat(credentialPath) + if !os.IsNotExist(err) { + data, err := ioutil.ReadFile(credentialPath) + if err != nil { + return nil, err + } + + config := map[string]interface{}{} + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + for k, v := range config { + if strValue, ok := v.(string); ok { + providerConfig[k] = strings.TrimSpace(strValue) + } + } + } + + _, err = os.Stat(configurePath) + if !os.IsNotExist(err) { + data, err := ioutil.ReadFile(configurePath) + if err != nil { + return nil, err + } + + config := map[string]interface{}{} + err = json.Unmarshal(data, &config) + if err != nil { + return nil, err + } + + outerLoop: + for k, v := range config { + if k == "_sys_param" { + tmpMap := v.(map[string]interface{}) + for tmpK, tmpV := range tmpMap { + if tmpK == "region" { + providerConfig[tmpK] = strings.TrimSpace(tmpV.(string)) + break outerLoop + } + } + } + } + } + } + + return providerConfig[ProfileKey], nil +} diff --git a/internal/backend/remote-state/cos/backend_state.go b/internal/backend/remote-state/cos/backend_state.go index ab92cfb7c0..245eb7c08b 100644 --- a/internal/backend/remote-state/cos/backend_state.go +++ b/internal/backend/remote-state/cos/backend_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cos import ( @@ -57,7 +60,7 @@ func (b *Backend) Workspaces() ([]string, error) { } // DeleteWorkspace deletes the named workspaces. The "default" state cannot be deleted. -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { log.Printf("[DEBUG] delete workspace, workspace: %v", name) if name == backend.DefaultStateName || name == "" { @@ -126,7 +129,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { err = lockUnlock(err) return nil, err } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { err = lockUnlock(err) return nil, err } diff --git a/internal/backend/remote-state/cos/backend_test.go b/internal/backend/remote-state/cos/backend_test.go index eb9038ff35..1cf1766a84 100644 --- a/internal/backend/remote-state/cos/backend_test.go +++ b/internal/backend/remote-state/cos/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cos import ( @@ -116,6 +119,27 @@ func TestRemoteClientWithEncryption(t *testing.T) { remote.TestClient(t, rs.Client) } +func TestRemoteClientWithEndpoint(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be := setupBackendWithEndpoint(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be) + + ss, err := be.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + rs, ok := ss.(*remote.State) + if !ok { + t.Fatalf("wrong state manager type\ngot: %T\nwant: %T", ss, rs) + } + + remote.TestClient(t, rs.Client) +} + func TestRemoteLocks(t *testing.T) { t.Parallel() @@ -198,6 +222,44 @@ func TestBackendWithEncryption(t *testing.T) { backend.TestBackendStateLocks(t, be0, be1) } +func TestBackendWithEndpoint(t *testing.T) { + t.Parallel() + + bucket := bucketName(t) + + be0 := setupBackendWithEndpoint(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be0) + + be1 := setupBackendWithEndpoint(t, bucket, defaultPrefix, defaultKey, false) + defer teardownBackend(t, be1) + + backend.TestBackendStates(t, be0) + backend.TestBackendStateLocks(t, be0, be1) + backend.TestBackendStateForceUnlock(t, be0, be1) +} + +func setupBackendWithEndpoint(t *testing.T, bucket, prefix, key string, encrypt bool) backend.Backend { + t.Helper() + + skip := os.Getenv("TF_COS_APPID") == "" + if skip { + t.Skip("This test requires setting the TF_COS_APPID environment variable") + } + + if os.Getenv(PROVIDER_REGION) == "" { + os.Setenv(PROVIDER_REGION, "ap-guangzhou") + } + + appId := os.Getenv("TF_COS_APPID") + region := os.Getenv(PROVIDER_REGION) + + if os.Getenv(PROVIDER_ENDPOINT) == "" { + os.Setenv(PROVIDER_ENDPOINT, fmt.Sprintf("http://%s.cos-internal.%s.tencentcos.cn", bucket+appId, region)) + } + + return setupBackend(t, bucket, prefix, key, encrypt) +} + func setupBackend(t *testing.T, bucket, prefix, key string, encrypt bool) backend.Backend { t.Helper() @@ -220,6 +282,10 @@ func setupBackend(t *testing.T, bucket, prefix, key string, encrypt bool) backen "key": key, } + if os.Getenv(PROVIDER_ENDPOINT) != "" { + config["endpoint"] = os.Getenv(PROVIDER_ENDPOINT) + } + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)) be := b.(*Backend) diff --git a/internal/backend/remote-state/cos/client.go b/internal/backend/remote-state/cos/client.go index 818bc129f5..67085f4884 100644 --- a/internal/backend/remote-state/cos/client.go +++ b/internal/backend/remote-state/cos/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cos import ( @@ -5,6 +8,7 @@ import ( "context" "crypto/md5" "encoding/json" + "errors" "fmt" "io/ioutil" "log" @@ -12,11 +16,11 @@ import ( "strings" "time" - multierror "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" tag "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag/v20180813" "github.com/tencentyun/cos-go-sdk-v5" + + "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states/statemgr" ) const ( @@ -141,7 +145,7 @@ func (c *remoteClient) lockError(err error) *statemgr.LockError { info, infoErr := c.lockInfo() if infoErr != nil { - lockErr.Err = multierror.Append(lockErr.Err, infoErr) + lockErr.Err = errors.Join(lockErr.Err, infoErr) } else { lockErr.Info = info } diff --git a/internal/backend/remote-state/cos/go.mod b/internal/backend/remote-state/cos/go.mod new file mode 100644 index 0000000000..cda6ba7a7d --- /dev/null +++ b/internal/backend/remote-state/cos/go.mod @@ -0,0 +1,67 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/cos + +go 1.24.2 + +require ( + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 + github.com/mitchellh/go-homedir v1.1.0 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 + github.com/tencentyun/cos-go-sdk-v5 v0.7.42 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/clbanning/mxj v1.8.4 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/mozillazg/go-httpheader v0.3.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/zclconf/go-cty v1.16.2 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/cos/go.sum b/internal/backend/remote-state/cos/go.sum new file mode 100644 index 0000000000..ca59131d50 --- /dev/null +++ b/internal/backend/remote-state/cos/go.sum @@ -0,0 +1,607 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I= +github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60= +github.com/mozillazg/go-httpheader v0.3.0 h1:3brX5z8HTH+0RrNA1362Rc3HsaxyWEKtGY45YrhuINM= +github.com/mozillazg/go-httpheader v0.3.0/go.mod h1:PuT8h0pw6efvp8ZeUec1Rs7dwjK08bt6gKSReGMqtdA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588 h1:DYtBXB7sVc3EOW5horg8j55cLZynhsLYhHrvQ/jXKKM= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588 h1:PlkFOALQZ9BLUyX8EalATUQD5xEn1Sz34C+Rw5VSpvk= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588/go.mod h1:vPvXNb+zBZVJfZCIKWcYxLpGzgScKKgiPUArobWZ+nU= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233 h1:5Tbi+jyZ2MojC6GK8V6hchwtnkP2IuENUTqSisbYOlA= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233/go.mod h1:sX14+NSvMjOhNFaMtP2aDy6Bss8PyFXij21gpY6+DAs= +github.com/tencentyun/cos-go-sdk-v5 v0.7.42 h1:Up1704BJjI5orycXKjpVpvuOInt9GC5pqY4knyE9Uds= +github.com/tencentyun/cos-go-sdk-v5 v0.7.42/go.mod h1:LUFnaqRmGk6pEHOaRmdn2dCZR2j0cSsM5xowWFPTPao= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/cos/transport.go b/internal/backend/remote-state/cos/transport.go new file mode 100644 index 0000000000..e2002e8a6a --- /dev/null +++ b/internal/backend/remote-state/cos/transport.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cos + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" +) + +const REQUEST_CLIENT = "TENCENTCLOUD_API_REQUEST_CLIENT" + +var ReqClient = "Terraform-latest" + +func SetReqClient(name string) { + if name == "" { + return + } + ReqClient = name +} + +type LogRoundTripper struct { +} + +func (me *LogRoundTripper) RoundTrip(request *http.Request) (response *http.Response, errRet error) { + + var inBytes, outBytes []byte + + var start = time.Now() + + defer func() { me.log(inBytes, outBytes, errRet, start) }() + + bodyReader, errRet := request.GetBody() + if errRet != nil { + return + } + var headName = "X-TC-Action" + + if envReqClient := os.Getenv(REQUEST_CLIENT); envReqClient != "" { + ReqClient = envReqClient + } + + request.Header.Set("X-TC-RequestClient", ReqClient) + inBytes = []byte(fmt.Sprintf("%s, request: ", request.Header[headName])) + requestBody, errRet := ioutil.ReadAll(bodyReader) + if errRet != nil { + return + } + inBytes = append(inBytes, requestBody...) + + headName = "X-TC-Region" + appendMessage := []byte(fmt.Sprintf( + ", (host %+v, region:%+v)", + request.Header["Host"], + request.Header[headName], + )) + + inBytes = append(inBytes, appendMessage...) + + response, errRet = http.DefaultTransport.RoundTrip(request) + if errRet != nil { + return + } + outBytes, errRet = ioutil.ReadAll(response.Body) + if errRet != nil { + return + } + response.Body = ioutil.NopCloser(bytes.NewBuffer(outBytes)) + return +} + +func (me *LogRoundTripper) log(in []byte, out []byte, err error, start time.Time) { + var buf bytes.Buffer + buf.WriteString("######") + tag := "[DEBUG]" + if err != nil { + tag = "[CRITICAL]" + } + buf.WriteString(tag) + if len(in) > 0 { + buf.WriteString("tencentcloud-sdk-go: ") + buf.Write(in) + } + if len(out) > 0 { + buf.WriteString("; response:") + err := json.Compact(&buf, out) + if err != nil { + out := bytes.Replace(out, + []byte("\n"), + []byte(""), + -1) + out = bytes.Replace(out, + []byte(" "), + []byte(""), + -1) + buf.Write(out) + } + } + + if err != nil { + buf.WriteString("; error:") + buf.WriteString(err.Error()) + } + + costFormat := fmt.Sprintf(",cost %s", time.Since(start).String()) + buf.WriteString(costFormat) + + log.Println(buf.String()) +} diff --git a/internal/backend/remote-state/gcs/backend.go b/internal/backend/remote-state/gcs/backend.go index 0478a95ab1..fb2c24a90d 100644 --- a/internal/backend/remote-state/gcs/backend.go +++ b/internal/backend/remote-state/gcs/backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package gcs implements remote storage of state on Google Cloud Storage (GCS). package gcs @@ -10,105 +13,157 @@ import ( "strings" "cloud.google.com/go/storage" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/httpclient" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/zclconf/go-cty/cty" "golang.org/x/oauth2" "google.golang.org/api/impersonate" "google.golang.org/api/option" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/internal/tfdiags" ) // Backend implements "backend".Backend for GCS. -// Input(), Validate() and Configure() are implemented by embedding *schema.Backend. -// State(), DeleteState() and States() are implemented explicitly. +// Schema() and PrepareConfig() are implemented by embedding backendbase.Base. +// Configure(), State(), DeleteState() and States() are implemented explicitly. type Backend struct { - *schema.Backend + backendbase.Base - storageClient *storage.Client - storageContext context.Context + storageClient *storage.Client bucketName string prefix string encryptionKey []byte + kmsKeyName string } func New() backend.Backend { - b := &Backend{} - b.Backend = &schema.Backend{ - ConfigureFunc: b.configure, - Schema: map[string]*schema.Schema{ - "bucket": { - Type: schema.TypeString, - Required: true, - Description: "The name of the Google Cloud Storage bucket", - }, + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bucket": { + Type: cty.String, + Required: true, + Description: "The name of the Google Cloud Storage bucket", + }, - "prefix": { - Type: schema.TypeString, - Optional: true, - Description: "The directory where state files will be saved inside the bucket", - }, + "prefix": { + Type: cty.String, + Optional: true, + Description: "The directory where state files will be saved inside the bucket", + }, - "credentials": { - Type: schema.TypeString, - Optional: true, - Description: "Google Cloud JSON Account Key", - Default: "", - }, + "credentials": { + Type: cty.String, + Optional: true, + Description: "Google Cloud JSON Account Key", + }, - "access_token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_OAUTH_ACCESS_TOKEN", - }, nil), - Description: "An OAuth2 token used for GCP authentication", - }, + "access_token": { + Type: cty.String, + Optional: true, + Description: "An OAuth2 token used for GCP authentication", + }, - "impersonate_service_account": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", - }, nil), - Description: "The service account to impersonate for all Google API Calls", - }, + "impersonate_service_account": { + Type: cty.String, + Optional: true, + Description: "The service account to impersonate for all Google API Calls", + }, - "impersonate_service_account_delegates": { - Type: schema.TypeList, - Optional: true, - Description: "The delegation chain for the impersonated service account", - Elem: &schema.Schema{Type: schema.TypeString}, - }, + "impersonate_service_account_delegates": { + Type: cty.List(cty.String), + Optional: true, + Description: "The delegation chain for the impersonated service account", + }, - "encryption_key": { - Type: schema.TypeString, - Optional: true, - Description: "A 32 byte base64 encoded 'customer supplied encryption key' used to encrypt all state.", - Default: "", + "encryption_key": { + Type: cty.String, + Optional: true, + Description: "A 32 byte base64 encoded 'customer supplied encryption key' used when reading and writing state files in the bucket.", + }, + + "kms_encryption_key": { + Type: cty.String, + Optional: true, + Description: "A Cloud KMS key ('customer managed encryption key') used when reading and writing state files in the bucket. Format should be 'projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}'.", + }, + + "storage_custom_endpoint": { + Type: cty.String, + Optional: true, + }, + }, + }, + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "prefix": { + Fallback: "", + }, + "credentials": { + Fallback: "", + }, + "access_token": { + EnvVars: []string{"GOOGLE_OAUTH_ACCESS_TOKEN"}, + }, + "impersonate_service_account": { + EnvVars: []string{ + "GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT", + "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT", + }, + }, + "encryption_key": { + EnvVars: []string{"GOOGLE_ENCRYPTION_KEY"}, + }, + "kms_encryption_key": { + EnvVars: []string{"GOOGLE_KMS_ENCRYPTION_KEY"}, + }, + "storage_custom_endpoint": { + EnvVars: []string{ + "GOOGLE_BACKEND_STORAGE_CUSTOM_ENDPOINT", + "GOOGLE_STORAGE_CUSTOM_ENDPOINT", + }, + }, }, }, } - - return b } -func (b *Backend) configure(ctx context.Context) error { +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { if b.storageClient != nil { return nil } - // ctx is a background context with the backend config added. - // Since no context is passed to remoteClient.Get(), .Lock(), etc. but - // one is required for calling the GCP API, we're holding on to this - // context here and re-use it later. - b.storageContext = ctx + // TODO: Update the Backend API to pass the real context.Context from + // the running command. + ctx := context.TODO() - data := schema.FromContextBackendConfig(b.storageContext) + data := backendbase.NewSDKLikeData(configVal) - b.bucketName = data.Get("bucket").(string) - b.prefix = strings.TrimLeft(data.Get("prefix").(string), "/") + if data.String("encryption_key") != "" && data.String("kms_encryption_key") != "" { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("can't set both encryption_key and kms_encryption_key"), + ) + } + // The above catches the main case where both of the arguments are set to + // a non-empty value, but we also want to reject the situation where + // both are present in the configuration regardless of what values were + // assigned to them. (This check doesn't take the environment variables + // into account, so must allow neither to be set in the main configuration.) + if !(configVal.GetAttr("encryption_key").IsNull() || configVal.GetAttr("kms_encryption_key").IsNull()) { + // This rejects a configuration like: + // encryption_key = "" + // kms_encryption_key = "" + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("can't set both encryption_key and kms_encryption_key"), + ) + } + + b.bucketName = data.String("bucket") + b.prefix = strings.TrimLeft(data.String("prefix"), "/") if b.prefix != "" && !strings.HasSuffix(b.prefix, "/") { b.prefix = b.prefix + "/" } @@ -120,12 +175,12 @@ func (b *Backend) configure(ctx context.Context) error { var creds string var tokenSource oauth2.TokenSource - if v, ok := data.GetOk("access_token"); ok { + if v := data.String("access_token"); v != "" { tokenSource = oauth2.StaticTokenSource(&oauth2.Token{ - AccessToken: v.(string), + AccessToken: v, }) - } else if v, ok := data.GetOk("credentials"); ok { - creds = v.(string) + } else if v := data.String("credentials"); v != "" { + creds = v } else if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { creds = v } else { @@ -137,30 +192,38 @@ func (b *Backend) configure(ctx context.Context) error { } else if creds != "" { // to mirror how the provider works, we accept the file path or the contents - contents, err := backend.ReadPathOrContents(creds) + contents, err := readPathOrContents(creds) if err != nil { - return fmt.Errorf("Error loading credentials: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("Error loading credentials: %s", err), + ) } if !json.Valid([]byte(contents)) { - return fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path") + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path"), + ) } credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents))) } // Service Account Impersonation - if v, ok := data.GetOk("impersonate_service_account"); ok { - ServiceAccount := v.(string) + if v := data.String("impersonate_service_account"); v != "" { + ServiceAccount := v var delegates []string - if v, ok := data.GetOk("impersonate_service_account_delegates"); ok { - d := v.([]interface{}) - if len(delegates) > 0 { - delegates = make([]string, len(d)) - } - for _, delegate := range d { - delegates = append(delegates, delegate.(string)) + delegatesVal := data.GetAttr("impersonate_service_account_delegates", cty.List(cty.String)) + if !delegatesVal.IsNull() && delegatesVal.LengthInt() != 0 { + delegates = make([]string, 0, delegatesVal.LengthInt()) + for it := delegatesVal.ElementIterator(); it.Next(); { + _, v := it.Element() + if v.IsNull() { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("impersonate_service_account_delegates elements must not be null"), + ) + } + delegates = append(delegates, v.AsString()) } } @@ -171,7 +234,7 @@ func (b *Backend) configure(ctx context.Context) error { }, credOptions...) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } opts = append(opts, option.WithTokenSource(ts)) @@ -181,22 +244,29 @@ func (b *Backend) configure(ctx context.Context) error { } opts = append(opts, option.WithUserAgent(httpclient.UserAgentString())) - client, err := storage.NewClient(b.storageContext, opts...) + + // Custom endpoint for storage API + if storageEndpoint := data.String("storage_custom_endpoint"); storageEndpoint != "" { + endpoint := option.WithEndpoint(storageEndpoint) + opts = append(opts, endpoint) + } + client, err := storage.NewClient(ctx, opts...) if err != nil { - return fmt.Errorf("storage.NewClient() failed: %v", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("storage.NewClient() failed: %v", err), + ) } b.storageClient = client - key := data.Get("encryption_key").(string) - if key == "" { - key = os.Getenv("GOOGLE_ENCRYPTION_KEY") - } - + // Customer-supplied encryption + key := data.String("encryption_key") if key != "" { - kc, err := backend.ReadPathOrContents(key) + kc, err := readPathOrContents(key) if err != nil { - return fmt.Errorf("Error loading encryption key: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("Error loading encryption key: %s", err), + ) } // The GCS client expects a customer supplied encryption key to be @@ -207,10 +277,18 @@ func (b *Backend) configure(ctx context.Context) error { // https://github.com/GoogleCloudPlatform/google-cloud-go/blob/def681/storage/storage.go#L1181 k, err := base64.StdEncoding.DecodeString(kc) if err != nil { - return fmt.Errorf("Error decoding encryption key: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("Error decoding encryption key: %s", err), + ) } b.encryptionKey = k } + // Customer-managed encryption + kmsName := data.String("kms_encryption_key") + if kmsName != "" { + b.kmsKeyName = kmsName + } + return nil } diff --git a/internal/backend/remote-state/gcs/backend_internal_test.go b/internal/backend/remote-state/gcs/backend_internal_test.go new file mode 100644 index 0000000000..824518245c --- /dev/null +++ b/internal/backend/remote-state/gcs/backend_internal_test.go @@ -0,0 +1,253 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package gcs + +import ( + "encoding/base64" + "reflect" + "strings" + "testing" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/zclconf/go-cty/cty" +) + +func TestBackendConfig_encryptionKey(t *testing.T) { + preCheckEnvironmentVariables(t) + // Cannot use t.Parallel as t.SetEnv used + + // This function is required because the key input is changed internally in the backend's code. + // This function is a quick way to help us get an expected value for tests, but ideally in future + // the test and the code under test will use a reusable function to avoid logic duplication. + expectedValue := func(key string) []byte { + if key == "" { + return nil + } + + v, err := base64.StdEncoding.DecodeString(key) + if err != nil { + t.Fatalf("error in test setup: %s", err.Error()) + } + return v + } + + cases := map[string]struct { + config map[string]interface{} + envs map[string]string + want []byte + }{ + "unset in config and ENVs": { + config: map[string]interface{}{ + "bucket": "foobar", + }, + want: expectedValue(""), + }, + + "set in config only": { + config: map[string]interface{}{ + "bucket": "foobar", + "encryption_key": encryptionKey, + }, + want: expectedValue(encryptionKey), + }, + + "set in config and GOOGLE_ENCRYPTION_KEY": { + config: map[string]interface{}{ + "bucket": "foobar", + "encryption_key": encryptionKey, + }, + envs: map[string]string{ + "GOOGLE_ENCRYPTION_KEY": encryptionKey2, // Different + }, + want: expectedValue(encryptionKey), + }, + + "set in GOOGLE_ENCRYPTION_KEY only": { + config: map[string]interface{}{ + "bucket": "foobar", + }, + envs: map[string]string{ + "GOOGLE_ENCRYPTION_KEY": encryptionKey2, + }, + want: expectedValue(encryptionKey2), + }, + + "set in config as empty string and in GOOGLE_ENCRYPTION_KEY": { + config: map[string]interface{}{ + "bucket": "foobar", + "encryption_key": "", + }, + envs: map[string]string{ + "GOOGLE_ENCRYPTION_KEY": encryptionKey2, + }, + want: expectedValue(encryptionKey2), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + for k, v := range tc.envs { + t.Setenv(k, v) + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(tc.config)) + be := b.(*Backend) + + if !reflect.DeepEqual(be.encryptionKey, tc.want) { + t.Fatalf("unexpected encryption_key value: wanted %v, got %v", tc.want, be.encryptionKey) + } + }) + } +} + +func TestBackendConfig_kmsKey(t *testing.T) { + preCheckEnvironmentVariables(t) + // Cannot use t.Parallel() due to t.Setenv + + cases := map[string]struct { + config map[string]interface{} + envs map[string]string + want string + }{ + "unset in config and ENVs": { + config: map[string]interface{}{ + "bucket": "foobar", + }, + }, + + "set in config only": { + config: map[string]interface{}{ + "bucket": "foobar", + "kms_encryption_key": "value from config", + }, + want: "value from config", + }, + + "set in config and GOOGLE_KMS_ENCRYPTION_KEY": { + config: map[string]interface{}{ + "bucket": "foobar", + "kms_encryption_key": "value from config", + }, + envs: map[string]string{ + "GOOGLE_KMS_ENCRYPTION_KEY": "value from GOOGLE_KMS_ENCRYPTION_KEY", + }, + want: "value from config", + }, + + "set in GOOGLE_KMS_ENCRYPTION_KEY only": { + config: map[string]interface{}{ + "bucket": "foobar", + }, + envs: map[string]string{ + "GOOGLE_KMS_ENCRYPTION_KEY": "value from GOOGLE_KMS_ENCRYPTION_KEY", + }, + want: "value from GOOGLE_KMS_ENCRYPTION_KEY", + }, + + "set in config as empty string and in GOOGLE_KMS_ENCRYPTION_KEY": { + config: map[string]interface{}{ + "bucket": "foobar", + "kms_encryption_key": "", + }, + envs: map[string]string{ + "GOOGLE_KMS_ENCRYPTION_KEY": "value from GOOGLE_KMS_ENCRYPTION_KEY", + }, + want: "value from GOOGLE_KMS_ENCRYPTION_KEY", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + for k, v := range tc.envs { + t.Setenv(k, v) + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(tc.config)) + be := b.(*Backend) + + if be.kmsKeyName != tc.want { + t.Fatalf("unexpected kms_encryption_key value: wanted %v, got %v", tc.want, be.kmsKeyName) + } + }) + } +} + +func TestStateFile(t *testing.T) { + t.Parallel() + + cases := []struct { + prefix string + name string + wantStateFile string + wantLockFile string + }{ + {"state", "default", "state/default.tfstate", "state/default.tflock"}, + {"state", "test", "state/test.tfstate", "state/test.tflock"}, + {"state", "test", "state/test.tfstate", "state/test.tflock"}, + {"state", "test", "state/test.tfstate", "state/test.tflock"}, + } + for _, c := range cases { + b := &Backend{ + prefix: c.prefix, + } + + if got := b.stateFile(c.name); got != c.wantStateFile { + t.Errorf("stateFile(%q) = %q, want %q", c.name, got, c.wantStateFile) + } + + if got := b.lockFile(c.name); got != c.wantLockFile { + t.Errorf("lockFile(%q) = %q, want %q", c.name, got, c.wantLockFile) + } + } +} + +func TestBackendEncryptionKeyEmptyConflict(t *testing.T) { + // This test is for the edge case where encryption_key and + // kms_encryption_key are both set in the configuration but set to empty + // strings. The "SDK-like" helpers treat unset as empty string, so + // we need an extra rule to catch them both being set to empty string + // directly inside the configuration, and this test covers that + // special case. + // + // The following assumes that the validation check we're testing will, if + // failing, always block attempts to reach any real GCP services, and so + // this test should be fine to run without an acceptance testing opt-in. + + // This test is for situations where these environment variables are not set. + t.Setenv("GOOGLE_ENCRYPTION_KEY", "") + t.Setenv("GOOGLE_KMS_ENCRYPTION_KEY", "") + + backend := New() + schema := backend.ConfigSchema() + rawVal := cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("fake-placeholder"), + + // These are both empty strings but should still be considered as + // set when we enforce teh rule that they can't both be set at once. + "encryption_key": cty.StringVal(""), + "kms_encryption_key": cty.StringVal(""), + }) + // The following mimicks how the terraform_remote_state data source + // treats its "config" argument, which is a realistic situation where + // we take an arbitrary object and try to force it to conform to the + // backend's schema. + configVal, err := schema.CoerceValue(rawVal) + if err != nil { + t.Fatalf("unexpected coersion error: %s", err) + } + configVal, diags := backend.PrepareConfig(configVal) + if diags.HasErrors() { + t.Fatalf("unexpected PrepareConfig error: %s", diags.Err().Error()) + } + + configDiags := backend.Configure(configVal) + if !configDiags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + gotErr := configDiags.Err().Error() + wantErr := `can't set both encryption_key and kms_encryption_key` + if !strings.Contains(gotErr, wantErr) { + t.Errorf("wrong error\ngot: %s\nwant substring: %s", gotErr, wantErr) + } +} diff --git a/internal/backend/remote-state/gcs/backend_state.go b/internal/backend/remote-state/gcs/backend_state.go index ee764efb4c..cd67683e5c 100644 --- a/internal/backend/remote-state/gcs/backend_state.go +++ b/internal/backend/remote-state/gcs/backend_state.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package gcs import ( + "context" "fmt" "path" "sort" @@ -23,10 +27,12 @@ const ( // Workspaces returns a list of names for the workspaces found on GCS. The default // state is always returned as the first element in the slice. func (b *Backend) Workspaces() ([]string, error) { + ctx := context.TODO() + states := []string{backend.DefaultStateName} bucket := b.storageClient.Bucket(b.bucketName) - objs := bucket.Objects(b.storageContext, &storage.Query{ + objs := bucket.Objects(ctx, &storage.Query{ Delimiter: "/", Prefix: b.prefix, }) @@ -55,7 +61,7 @@ func (b *Backend) Workspaces() ([]string, error) { } // DeleteWorkspace deletes the named workspaces. The "default" state cannot be deleted. -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { if name == backend.DefaultStateName { return fmt.Errorf("cowardly refusing to delete the %q state", name) } @@ -75,12 +81,12 @@ func (b *Backend) client(name string) (*remoteClient, error) { } return &remoteClient{ - storageContext: b.storageContext, - storageClient: b.storageClient, - bucketName: b.bucketName, - stateFilePath: b.stateFile(name), - lockFilePath: b.lockFile(name), - encryptionKey: b.encryptionKey, + storageClient: b.storageClient, + bucketName: b.bucketName, + stateFilePath: b.stateFile(name), + lockFilePath: b.lockFile(name), + encryptionKey: b.encryptionKey, + kmsKeyName: b.kmsKeyName, }, nil } @@ -131,7 +137,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { if err := st.WriteState(states.NewState()); err != nil { return nil, unlock(err) } - if err := st.PersistState(); err != nil { + if err := st.PersistState(nil); err != nil { return nil, unlock(err) } diff --git a/internal/backend/remote-state/gcs/backend_test.go b/internal/backend/remote-state/gcs/backend_test.go index bbdd5c61a6..c874ff390f 100644 --- a/internal/backend/remote-state/gcs/backend_test.go +++ b/internal/backend/remote-state/gcs/backend_test.go @@ -1,6 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package gcs import ( + "context" + "encoding/json" "fmt" "log" "os" @@ -8,72 +13,142 @@ import ( "testing" "time" + kms "cloud.google.com/go/kms/apiv1" "cloud.google.com/go/storage" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/states/remote" + "google.golang.org/api/option" + kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1" ) const ( noPrefix = "" noEncryptionKey = "" + noKmsKeyName = "" ) // See https://cloud.google.com/storage/docs/using-encryption-keys#generating_your_own_encryption_key -var encryptionKey = "yRyCOikXi1ZDNE0xN3yiFsJjg7LGimoLrGFcLZgQoVk=" +const encryptionKey = "yRyCOikXi1ZDNE0xN3yiFsJjg7LGimoLrGFcLZgQoVk=" +const encryptionKey2 = "cRKXxV+HVAITvGlqxLJL8hZ95VTFHT2djkoRQjBpQls=" -func TestStateFile(t *testing.T) { - t.Parallel() +// KMS key ring name and key name are hardcoded here and re-used because key rings (and keys) cannot be deleted +// Test code asserts their presence and creates them if they're absent. They're not deleted at the end of tests. +// See: https://cloud.google.com/kms/docs/faq#cannot_delete +const ( + keyRingName = "tf-gcs-backend-acc-tests" + keyName = "tf-test-key-1" + kmsRole = "roles/cloudkms.cryptoKeyEncrypterDecrypter" // GCS service account needs this binding on the created key +) - cases := []struct { - prefix string - name string - wantStateFile string - wantLockFile string +var keyRingLocation = os.Getenv("GOOGLE_REGION") + +func preCheckTestAcc(t *testing.T) { + if v := os.Getenv("TF_ACC"); v == "" { + t.Skip("TF_ACC must be set to run acceptance tests, as they provision real resources") + } +} + +func preCheckEnvironmentVariables(t *testing.T) { + + // TODO - implement a separate function specific to credentials which will iterate through a list of ENV names and return first value found. + + credentials := []string{ + "GOOGLE_BACKEND_CREDENTIALS", + "GOOGLE_CREDENTIALS", + } + credsFound := false + for _, name := range credentials { + v := os.Getenv(name) + if v != "" { + credsFound = true + break + } + } + if !credsFound { + // Skipping tests because hashicorp/terraform repo doesn't have credentials set up. + // In future we should enable tests to run automatically, and make this code fail when no creds are set. + t.Skip("credentials need to be provided via GOOGLE_BACKEND_CREDENTIALS or GOOGLE_CREDENTIALS environment variable but neither is set") + } +} + +func TestAccBackendConfig_credentials(t *testing.T) { + // Cannot use t.Parallel() due to t.Setenv + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) + + credentials := os.Getenv("GOOGLE_BACKEND_CREDENTIALS") + if credentials == "" { + credentials = os.Getenv("GOOGLE_CREDENTIALS") + } + if credentials == "" { + t.Fatalf("unable to access credentials from the test environment") + } + + t.Setenv("GOOGLE_BACKEND_CREDENTIALS", "") // unset value + t.Setenv("GOOGLE_CREDENTIALS", "") // unset value + + cases := map[string]struct { + config map[string]interface{} + envs map[string]string + want string }{ - {"state", "default", "state/default.tfstate", "state/default.tflock"}, - {"state", "test", "state/test.tfstate", "state/test.tflock"}, - {"state", "test", "state/test.tfstate", "state/test.tflock"}, - {"state", "test", "state/test.tfstate", "state/test.tflock"}, + "empty credentials in config doesn't affect use of GOOGLE_BACKEND_CREDENTIALS": { + config: map[string]interface{}{ + "bucket": "tf-test-testaccbackendconfig_credentials_1", + "credentials": "", + }, + envs: map[string]string{ + "GOOGLE_BACKEND_CREDENTIALS": credentials, + }, + }, + "empty credentials in config doesn't affect use of GOOGLE_CREDENTIALS": { + config: map[string]interface{}{ + "bucket": "tf-test-testaccbackendconfig_credentials_2", + "credentials": "", + }, + envs: map[string]string{ + "GOOGLE_CREDENTIALS": credentials, + }, + }, + "credentials in config that isn't an empty string takes precedence over credentials from the environment": { + config: map[string]interface{}{ + "bucket": "tf-test-testaccbackendconfig_credentials_3", + "credentials": credentials, + }, + envs: map[string]string{ + "GOOGLE_CREDENTIALS": "foobar", // Would cause an error if used + }, + }, } - for _, c := range cases { - b := &Backend{ - prefix: c.prefix, - } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + for k, v := range tc.envs { + t.Setenv(k, v) + } - if got := b.stateFile(c.name); got != c.wantStateFile { - t.Errorf("stateFile(%q) = %q, want %q", c.name, got, c.wantStateFile) - } + be0 := setupBackend(t, tc.config) + defer teardownBackend(t, be0, noPrefix) - if got := b.lockFile(c.name); got != c.wantLockFile { - t.Errorf("lockFile(%q) = %q, want %q", c.name, got, c.wantLockFile) - } + be1 := setupBackend(t, tc.config) + + backend.TestBackendStates(t, be0) + backend.TestBackendStateLocks(t, be0, be1) + }) } + } -func TestRemoteClient(t *testing.T) { +func TestAccRemoteClient(t *testing.T) { t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) bucket := bucketName(t) - be := setupBackend(t, bucket, noPrefix, noEncryptionKey) - defer teardownBackend(t, be, noPrefix) - - ss, err := be.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatalf("be.StateMgr(%q) = %v", backend.DefaultStateName, err) + config := map[string]interface{}{ + "bucket": bucket, } - - rs, ok := ss.(*remote.State) - if !ok { - t.Fatalf("be.StateMgr(): got a %T, want a *remote.State", ss) - } - - remote.TestClient(t, rs.Client) -} -func TestRemoteClientWithEncryption(t *testing.T) { - t.Parallel() - - bucket := bucketName(t) - be := setupBackend(t, bucket, noPrefix, encryptionKey) + be := setupBackend(t, config) defer teardownBackend(t, be, noPrefix) ss, err := be.StateMgr(backend.DefaultStateName) @@ -89,11 +164,42 @@ func TestRemoteClientWithEncryption(t *testing.T) { remote.TestClient(t, rs.Client) } -func TestRemoteLocks(t *testing.T) { +func TestAccRemoteClientWithEncryption(t *testing.T) { t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) bucket := bucketName(t) - be := setupBackend(t, bucket, noPrefix, noEncryptionKey) + config := map[string]interface{}{ + "bucket": bucket, + "encryption_key": encryptionKey, + } + be := setupBackend(t, config) + defer teardownBackend(t, be, noPrefix) + + ss, err := be.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("be.StateMgr(%q) = %v", backend.DefaultStateName, err) + } + + rs, ok := ss.(*remote.State) + if !ok { + t.Fatalf("be.StateMgr(): got a %T, want a *remote.State", ss) + } + + remote.TestClient(t, rs.Client) +} + +func TestAccRemoteLocks(t *testing.T) { + t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) + + bucket := bucketName(t) + config := map[string]interface{}{ + "bucket": bucket, + } + be := setupBackend(t, config) defer teardownBackend(t, be, noPrefix) remoteClient := func() (remote.Client, error) { @@ -122,52 +228,106 @@ func TestRemoteLocks(t *testing.T) { remote.TestRemoteLocks(t, c0, c1) } -func TestBackend(t *testing.T) { +func TestAccBackend(t *testing.T) { t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) bucket := bucketName(t) - be0 := setupBackend(t, bucket, noPrefix, noEncryptionKey) + config := map[string]interface{}{ + "bucket": bucket, + } + be0 := setupBackend(t, config) defer teardownBackend(t, be0, noPrefix) - be1 := setupBackend(t, bucket, noPrefix, noEncryptionKey) + be1 := setupBackend(t, config) backend.TestBackendStates(t, be0) backend.TestBackendStateLocks(t, be0, be1) backend.TestBackendStateForceUnlock(t, be0, be1) } -func TestBackendWithPrefix(t *testing.T) { +func TestAccBackendWithPrefix(t *testing.T) { t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) prefix := "test/prefix" bucket := bucketName(t) - be0 := setupBackend(t, bucket, prefix, noEncryptionKey) + config := map[string]interface{}{ + "bucket": bucket, + "prefix": prefix, + "encryption_key": encryptionKey, + } + be0 := setupBackend(t, config) defer teardownBackend(t, be0, prefix) - be1 := setupBackend(t, bucket, prefix+"/", noEncryptionKey) + config["prefix"] = prefix + "/" + be1 := setupBackend(t, config) backend.TestBackendStates(t, be0) backend.TestBackendStateLocks(t, be0, be1) } -func TestBackendWithEncryption(t *testing.T) { + +func TestAccBackendWithCustomerSuppliedEncryption(t *testing.T) { t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) bucket := bucketName(t) - be0 := setupBackend(t, bucket, noPrefix, encryptionKey) + config := map[string]interface{}{ + "bucket": bucket, + "encryption_key": encryptionKey, + } + be0 := setupBackend(t, config) defer teardownBackend(t, be0, noPrefix) - be1 := setupBackend(t, bucket, noPrefix, encryptionKey) + be1 := setupBackend(t, config) + + backend.TestBackendStates(t, be0) + backend.TestBackendStateLocks(t, be0, be1) +} + +func TestAccBackendWithCustomerManagedKMSEncryption(t *testing.T) { + t.Parallel() + preCheckTestAcc(t) + preCheckEnvironmentVariables(t) + + projectID := os.Getenv("GOOGLE_PROJECT") + bucket := bucketName(t) + + // Taken from global variables in test file + kmsDetails := map[string]string{ + "project": projectID, + "location": keyRingLocation, + "ringName": keyRingName, + "keyName": keyName, + } + + kmsName := setupKmsKey(t, kmsDetails) + + config := map[string]interface{}{ + "bucket": bucket, + "prefix": noPrefix, + "kms_encryption_key": kmsName, + } + + be0 := setupBackend(t, config) + defer teardownBackend(t, be0, noPrefix) + + be1 := setupBackend(t, config) backend.TestBackendStates(t, be0) backend.TestBackendStateLocks(t, be0, be1) } // setupBackend returns a new GCS backend. -func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { +func setupBackend(t *testing.T, config map[string]interface{}) backend.Backend { t.Helper() + ctx := context.Background() projectID := os.Getenv("GOOGLE_PROJECT") if projectID == "" || os.Getenv("TF_ACC") == "" { @@ -176,18 +336,12 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { "the TF_ACC and GOOGLE_PROJECT environment variables are set.") } - config := map[string]interface{}{ - "bucket": bucket, - "prefix": prefix, - "encryption_key": key, - } - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)) be := b.(*Backend) // create the bucket if it doesn't exist - bkt := be.storageClient.Bucket(bucket) - _, err := bkt.Attrs(be.storageContext) + bkt := be.storageClient.Bucket(config["bucket"].(string)) + _, err := bkt.Attrs(ctx) if err != nil { if err != storage.ErrBucketNotExist { t.Fatal(err) @@ -196,7 +350,7 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { attrs := &storage.BucketAttrs{ Location: os.Getenv("GOOGLE_REGION"), } - err := bkt.Create(be.storageContext, projectID, attrs) + err := bkt.Create(ctx, projectID, attrs) if err != nil { t.Fatal(err) } @@ -205,6 +359,120 @@ func setupBackend(t *testing.T, bucket, prefix, key string) backend.Backend { return b } +// setupKmsKey asserts that a KMS key chain and key exist and necessary IAM bindings are in place +// If the key ring or key do not exist they are created and permissions are given to the GCS Service account +func setupKmsKey(t *testing.T, keyDetails map[string]string) string { + t.Helper() + + projectID := os.Getenv("GOOGLE_PROJECT") + if projectID == "" || os.Getenv("TF_ACC") == "" { + t.Skip("This test creates a KMS key ring and key in Cloud KMS. " + + "Since this may incur costs, it will only run if " + + "the TF_ACC and GOOGLE_PROJECT environment variables are set.") + } + + // KMS Client + ctx := context.Background() + opts, err := testGetClientOptions(t) + if err != nil { + e := fmt.Errorf("testGetClientOptions() failed: %s", err) + t.Fatal(e) + } + c, err := kms.NewKeyManagementClient(ctx, opts...) + if err != nil { + e := fmt.Errorf("kms.NewKeyManagementClient() failed: %v", err) + t.Fatal(e) + } + defer c.Close() + + // Get KMS key ring, create if doesn't exist + reqGetKeyRing := &kmspb.GetKeyRingRequest{ + Name: fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", keyDetails["project"], keyDetails["location"], keyDetails["ringName"]), + } + var keyRing *kmspb.KeyRing + keyRing, err = c.GetKeyRing(ctx, reqGetKeyRing) + if err != nil { + if !strings.Contains(err.Error(), "NotFound") { + // Handle unexpected error that isn't related to the key ring not being made yet + t.Fatal(err) + } + // Create key ring that doesn't exist + t.Logf("Cloud KMS key ring `%s` not found: creating key ring", + fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", keyDetails["project"], keyDetails["location"], keyDetails["ringName"]), + ) + reqCreateKeyRing := &kmspb.CreateKeyRingRequest{ + Parent: fmt.Sprintf("projects/%s/locations/%s", keyDetails["project"], keyDetails["location"]), + KeyRingId: keyDetails["ringName"], + } + keyRing, err = c.CreateKeyRing(ctx, reqCreateKeyRing) + if err != nil { + t.Fatal(err) + } + t.Logf("Cloud KMS key ring `%s` created successfully", keyRing.Name) + } + + // Get KMS key, create if doesn't exist (and give GCS service account permission to use) + reqGetKey := &kmspb.GetCryptoKeyRequest{ + Name: fmt.Sprintf("%s/cryptoKeys/%s", keyRing.Name, keyDetails["keyName"]), + } + var key *kmspb.CryptoKey + key, err = c.GetCryptoKey(ctx, reqGetKey) + if err != nil { + if !strings.Contains(err.Error(), "NotFound") { + // Handle unexpected error that isn't related to the key not being made yet + t.Fatal(err) + } + // Create key that doesn't exist + t.Logf("Cloud KMS key `%s` not found: creating key", + fmt.Sprintf("%s/cryptoKeys/%s", keyRing.Name, keyDetails["keyName"]), + ) + reqCreateKey := &kmspb.CreateCryptoKeyRequest{ + Parent: keyRing.Name, + CryptoKeyId: keyDetails["keyName"], + CryptoKey: &kmspb.CryptoKey{ + Purpose: kmspb.CryptoKey_ENCRYPT_DECRYPT, + }, + } + key, err = c.CreateCryptoKey(ctx, reqCreateKey) + if err != nil { + t.Fatal(err) + } + t.Logf("Cloud KMS key `%s` created successfully", key.Name) + } + + // Get GCS Service account email, check has necessary permission on key + // Note: we cannot reuse the backend's storage client (like in the setupBackend function) + // because the KMS key needs to exist before the backend buckets are made in the test. + sc, err := storage.NewClient(ctx, opts...) //reuse opts from KMS client + if err != nil { + e := fmt.Errorf("storage.NewClient() failed: %v", err) + t.Fatal(e) + } + defer sc.Close() + gcsServiceAccount, err := sc.ServiceAccount(ctx, keyDetails["project"]) + if err != nil { + t.Fatal(err) + } + + // Assert Cloud Storage service account has permission to use this key. + member := fmt.Sprintf("serviceAccount:%s", gcsServiceAccount) + iamHandle := c.ResourceIAM(key.Name) + policy, err := iamHandle.Policy(ctx) + if err != nil { + t.Fatal(err) + } + if ok := policy.HasRole(member, kmsRole); !ok { + // Add the missing permissions + t.Logf("Granting GCS service account %s %s role on key %s", gcsServiceAccount, kmsRole, key.Name) + policy.Add(member, kmsRole) + err = iamHandle.SetPolicy(ctx, policy) + if err != nil { + t.Fatal(err) + } + } + return key.Name +} + // teardownBackend deletes all states from be except the default state. func teardownBackend(t *testing.T, be backend.Backend, prefix string) { t.Helper() @@ -212,7 +480,7 @@ func teardownBackend(t *testing.T, be backend.Backend, prefix string) { if !ok { t.Fatalf("be is a %T, want a *gcsBackend", be) } - ctx := gcsBE.storageContext + ctx := context.Background() bucket := gcsBE.storageClient.Bucket(gcsBE.bucketName) objs := bucket.Objects(ctx, nil) @@ -242,3 +510,36 @@ func bucketName(t *testing.T) string { return strings.ToLower(name) } + +// getClientOptions returns the []option.ClientOption needed to configure Google API clients +// that are required in acceptance tests but are not part of the gcs backend itself +func testGetClientOptions(t *testing.T) ([]option.ClientOption, error) { + t.Helper() + + var creds string + if v := os.Getenv("GOOGLE_BACKEND_CREDENTIALS"); v != "" { + creds = v + } else { + creds = os.Getenv("GOOGLE_CREDENTIALS") + } + if creds == "" { + t.Skip("This test required credentials to be supplied via" + + "the GOOGLE_CREDENTIALS or GOOGLE_BACKEND_CREDENTIALS environment variables.") + } + + var opts []option.ClientOption + var credOptions []option.ClientOption + + contents, err := readPathOrContents(creds) + if err != nil { + return nil, fmt.Errorf("error loading credentials: %s", err) + } + if !json.Valid([]byte(contents)) { + return nil, fmt.Errorf("the string provided in credentials is neither valid json nor a valid file path") + } + credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents))) + opts = append(opts, credOptions...) + opts = append(opts, option.WithUserAgent(httpclient.UserAgentString())) + + return opts, nil +} diff --git a/internal/backend/remote-state/gcs/client.go b/internal/backend/remote-state/gcs/client.go index 58402fbde0..def842c148 100644 --- a/internal/backend/remote-state/gcs/client.go +++ b/internal/backend/remote-state/gcs/client.go @@ -1,32 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package gcs import ( + "context" "encoding/json" + "errors" "fmt" "io/ioutil" "strconv" "cloud.google.com/go/storage" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" - "golang.org/x/net/context" ) // remoteClient is used by "state/remote".State to read and write // blobs representing state. // Implements "state/remote".ClientLocker type remoteClient struct { - storageContext context.Context - storageClient *storage.Client - bucketName string - stateFilePath string - lockFilePath string - encryptionKey []byte + storageClient *storage.Client + bucketName string + stateFilePath string + lockFilePath string + encryptionKey []byte + kmsKeyName string } func (c *remoteClient) Get() (payload *remote.Payload, err error) { - stateFileReader, err := c.stateFile().NewReader(c.storageContext) + ctx := context.TODO() + stateFileReader, err := c.stateFile().NewReader(ctx) if err != nil { if err == storage.ErrObjectNotExist { return nil, nil @@ -41,7 +46,7 @@ func (c *remoteClient) Get() (payload *remote.Payload, err error) { return nil, fmt.Errorf("Failed to read state file from %v: %v", c.stateFileURL(), err) } - stateFileAttrs, err := c.stateFile().Attrs(c.storageContext) + stateFileAttrs, err := c.stateFile().Attrs(ctx) if err != nil { return nil, fmt.Errorf("Failed to read state file attrs from %v: %v", c.stateFileURL(), err) } @@ -55,8 +60,12 @@ func (c *remoteClient) Get() (payload *remote.Payload, err error) { } func (c *remoteClient) Put(data []byte) error { + ctx := context.TODO() err := func() error { - stateFileWriter := c.stateFile().NewWriter(c.storageContext) + stateFileWriter := c.stateFile().NewWriter(ctx) + if len(c.kmsKeyName) > 0 { + stateFileWriter.KMSKeyName = c.kmsKeyName + } if _, err := stateFileWriter.Write(data); err != nil { return err } @@ -70,7 +79,8 @@ func (c *remoteClient) Put(data []byte) error { } func (c *remoteClient) Delete() error { - if err := c.stateFile().Delete(c.storageContext); err != nil { + ctx := context.TODO() + if err := c.stateFile().Delete(ctx); err != nil { return fmt.Errorf("Failed to delete state file %v: %v", c.stateFileURL(), err) } @@ -80,6 +90,8 @@ func (c *remoteClient) Delete() error { // Lock writes to a lock file, ensuring file creation. Returns the generation // number, which must be passed to Unlock(). func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { + ctx := context.TODO() + // update the path we're using // we can't set the ID until the info is written info.Path = c.lockFileURL() @@ -90,7 +102,7 @@ func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { } lockFile := c.lockFile() - w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(c.storageContext) + w := lockFile.If(storage.Conditions{DoesNotExist: true}).NewWriter(ctx) err = func() error { if _, err := w.Write(infoJson); err != nil { return err @@ -108,12 +120,14 @@ func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { } func (c *remoteClient) Unlock(id string) error { + ctx := context.TODO() + gen, err := strconv.ParseInt(id, 10, 64) if err != nil { return fmt.Errorf("Lock ID should be numerical value, got '%s'", id) } - if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(c.storageContext); err != nil { + if err := c.lockFile().If(storage.Conditions{GenerationMatch: gen}).Delete(ctx); err != nil { return c.lockError(err) } @@ -127,7 +141,7 @@ func (c *remoteClient) lockError(err error) *statemgr.LockError { info, infoErr := c.lockInfo() if infoErr != nil { - lockErr.Err = multierror.Append(lockErr.Err, infoErr) + lockErr.Err = errors.Join(lockErr.Err, infoErr) } else { lockErr.Info = info } @@ -137,7 +151,9 @@ func (c *remoteClient) lockError(err error) *statemgr.LockError { // lockInfo reads the lock file, parses its contents and returns the parsed // LockInfo struct. func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { - r, err := c.lockFile().NewReader(c.storageContext) + ctx := context.TODO() + + r, err := c.lockFile().NewReader(ctx) if err != nil { return nil, err } @@ -156,7 +172,7 @@ func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) { // We use the Generation as the ID, so overwrite the ID in the json. // This can't be written into the Info, since the generation isn't known // until it's written. - attrs, err := c.lockFile().Attrs(c.storageContext) + attrs, err := c.lockFile().Attrs(ctx) if err != nil { return nil, err } diff --git a/internal/backend/remote-state/gcs/go.mod b/internal/backend/remote-state/gcs/go.mod new file mode 100644 index 0000000000..aba2db7ae2 --- /dev/null +++ b/internal/backend/remote-state/gcs/go.mod @@ -0,0 +1,86 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/gcs + +go 1.24.2 + +require ( + cloud.google.com/go/kms v1.15.5 + cloud.google.com/go/storage v1.30.1 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/mitchellh/go-homedir v1.1.0 + github.com/zclconf/go-cty v1.16.2 + golang.org/x/oauth2 v0.27.0 + google.golang.org/api v0.155.0 + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 +) + +require ( + cloud.google.com/go v0.110.10 // indirect + cloud.google.com/go/compute/metadata v0.5.2 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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/time v0.9.0 // indirect + golang.org/x/tools v0.31.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // 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.5 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/gcs/go.sum b/internal/backend/remote-state/gcs/go.sum new file mode 100644 index 0000000000..c02090f278 --- /dev/null +++ b/internal/backend/remote-state/gcs/go.sum @@ -0,0 +1,605 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/kms v1.15.5 h1:pj1sRfut2eRbD9pFRjNnPNg/CzJPuQAzUujMIM1vVeM= +cloud.google.com/go/kms v1.15.5/go.mod h1:cU2H5jnp6G2TDpUGZyqTCoy1n16fbubHZjmVXSMtwDI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 h1:QVw89YDxXxEe+l8gU8ETbOasdwEV+avkR75ZzsVV9WI= +github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.1.0 h1:tntQDh69XqOCOZsDz0lVJQez/2L6Uu2PdjCQwWCJ3bM= +github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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.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.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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/gcs/path_or_contents.go b/internal/backend/remote-state/gcs/path_or_contents.go new file mode 100644 index 0000000000..55e467faf3 --- /dev/null +++ b/internal/backend/remote-state/gcs/path_or_contents.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package gcs + +import ( + "io/ioutil" + "os" + + "github.com/mitchellh/go-homedir" +) + +// If the argument is a path, Read loads it and returns the contents, +// otherwise the argument is assumed to be the desired contents and is simply +// returned. +func readPathOrContents(poc string) (string, error) { + if len(poc) == 0 { + return poc, nil + } + + path := poc + if path[0] == '~' { + var err error + path, err = homedir.Expand(path) + if err != nil { + return path, err + } + } + + if _, err := os.Stat(path); err == nil { + contents, err := ioutil.ReadFile(path) + if err != nil { + return string(contents), err + } + return string(contents), nil + } + + return poc, nil +} diff --git a/internal/backend/backend_test.go b/internal/backend/remote-state/gcs/path_or_contents_test.go similarity index 89% rename from internal/backend/backend_test.go rename to internal/backend/remote-state/gcs/path_or_contents_test.go index c94b8622bd..48b7ee2c1a 100644 --- a/internal/backend/backend_test.go +++ b/internal/backend/remote-state/gcs/path_or_contents_test.go @@ -1,4 +1,7 @@ -package backend +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package gcs import ( "io" @@ -20,7 +23,7 @@ func TestReadPathOrContents_Path(t *testing.T) { } f.Close() - contents, err := ReadPathOrContents(f.Name()) + contents, err := readPathOrContents(f.Name()) if err != nil { t.Fatalf("err: %s", err) @@ -45,7 +48,7 @@ func TestReadPathOrContents_TildePath(t *testing.T) { r := strings.NewReplacer(home, "~") homePath := r.Replace(f.Name()) - contents, err := ReadPathOrContents(homePath) + contents, err := readPathOrContents(homePath) if err != nil { t.Fatalf("err: %s", err) @@ -76,7 +79,7 @@ func TestRead_PathNoPermission(t *testing.T) { t.Fatalf("err: %s", err) } - contents, err := ReadPathOrContents(f.Name()) + contents, err := readPathOrContents(f.Name()) if err == nil { t.Fatal("Expected error, got none!") @@ -89,7 +92,7 @@ func TestRead_PathNoPermission(t *testing.T) { func TestReadPathOrContents_Contents(t *testing.T) { input := "hello" - contents, err := ReadPathOrContents(input) + contents, err := readPathOrContents(input) if err != nil { t.Fatalf("err: %s", err) @@ -102,7 +105,7 @@ func TestReadPathOrContents_Contents(t *testing.T) { func TestReadPathOrContents_TildeContents(t *testing.T) { input := "~/hello/notafile" - contents, err := ReadPathOrContents(input) + contents, err := readPathOrContents(input) if err != nil { t.Fatalf("err: %s", err) diff --git a/internal/backend/remote-state/http/backend.go b/internal/backend/remote-state/http/backend.go index 863e3f00f1..fbed04b784 100644 --- a/internal/backend/remote-state/http/backend.go +++ b/internal/backend/remote-state/http/backend.go @@ -1,169 +1,231 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package http import ( - "context" "crypto/tls" + "crypto/x509" + "errors" "fmt" "log" "net/http" "net/url" "time" - "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-retryablehttp" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" ) func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "address": &schema.Schema{ - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_ADDRESS", nil), - Description: "The address of the REST endpoint", - }, - "update_method": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UPDATE_METHOD", "POST"), - Description: "HTTP method to use when updating state", - }, - "lock_address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_ADDRESS", nil), - Description: "The address of the lock REST endpoint", - }, - "unlock_address": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil), - Description: "The address of the unlock REST endpoint", - }, - "lock_method": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_METHOD", "LOCK"), - Description: "The HTTP method to use when locking", - }, - "unlock_method": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"), - Description: "The HTTP method to use when unlocking", - }, - "username": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_USERNAME", nil), - Description: "The username for HTTP basic authentication", - }, - "password": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_PASSWORD", nil), - Description: "The password for HTTP basic authentication", - }, - "skip_cert_verification": &schema.Schema{ - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: "Whether to skip TLS verification.", - }, - "retry_max": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_MAX", 2), - Description: "The number of HTTP request retries.", - }, - "retry_wait_min": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MIN", 1), - Description: "The minimum time in seconds to wait between HTTP request attempts.", - }, - "retry_wait_max": &schema.Schema{ - Type: schema.TypeInt, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30), - Description: "The maximum time in seconds to wait between HTTP request attempts.", + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "address": { + Type: cty.String, + Optional: true, // Must be set but can be set using the TF_HTTP_ADDRESS environment variable + Description: "The address of the REST endpoint", + }, + "update_method": { + Type: cty.String, + Optional: true, + Description: "HTTP method to use when updating state", + }, + "lock_address": { + Type: cty.String, + Optional: true, + Description: "The address of the lock REST endpoint", + }, + "unlock_address": { + Type: cty.String, + Optional: true, + Description: "The address of the unlock REST endpoint", + }, + "lock_method": { + Type: cty.String, + Optional: true, + Description: "The HTTP method to use when locking", + }, + "unlock_method": { + Type: cty.String, + Optional: true, + Description: "The HTTP method to use when unlocking", + }, + "username": { + Type: cty.String, + Optional: true, + Description: "The username for HTTP basic authentication", + }, + "password": { + Type: cty.String, + Optional: true, + Description: "The password for HTTP basic authentication", + }, + "skip_cert_verification": { + Type: cty.Bool, + Optional: true, + Description: "Whether to skip TLS verification", + }, + "retry_max": { + Type: cty.Number, + Optional: true, + Description: "The number of HTTP request retries", + }, + "retry_wait_min": { + Type: cty.Number, + Optional: true, + Description: "The minimum time in seconds to wait between HTTP request attempts", + }, + "retry_wait_max": { + Type: cty.Number, + Optional: true, + Description: "The maximum time in seconds to wait between HTTP request attempts", + }, + "client_ca_certificate_pem": { + Type: cty.String, + Optional: true, + Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication", + }, + "client_certificate_pem": { + Type: cty.String, + Optional: true, + Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication", + }, + "client_private_key_pem": { + Type: cty.String, + Optional: true, + Description: "A PEM-encoded private key, required if client_certificate_pem is specified", + }, + }, }, }, } - - b := &Backend{Backend: s} - b.Backend.ConfigureFunc = b.configure - return b } type Backend struct { - *schema.Backend + backendbase.Base client *httpClient } -func (b *Backend) configure(ctx context.Context) error { - data := schema.FromContextBackendConfig(ctx) - - address := data.Get("address").(string) +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { + address := backendbase.GetAttrEnvDefaultFallback( + configVal, "address", + "TF_HTTP_ADDRESS", cty.StringVal(""), + ).AsString() + if address == "" { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("address argument is required"), + ) + } updateURL, err := url.Parse(address) if err != nil { - return fmt.Errorf("failed to parse address URL: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("failed to parse address URL: %s", err), + ) } if updateURL.Scheme != "http" && updateURL.Scheme != "https" { - return fmt.Errorf("address must be HTTP or HTTPS") + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("address must be HTTP or HTTPS"), + ) } - updateMethod := data.Get("update_method").(string) + updateMethod := backendbase.GetAttrEnvDefaultFallback( + configVal, "update_method", + "TF_HTTP_UPDATE_METHOD", cty.StringVal("POST"), + ).AsString() var lockURL *url.URL - if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" { + if v := backendbase.GetAttrEnvDefault(configVal, "lock_address", "TF_HTTP_LOCK_ADDRESS"); !v.IsNull() { var err error - lockURL, err = url.Parse(v.(string)) + lockURL, err = url.Parse(v.AsString()) if err != nil { - return fmt.Errorf("failed to parse lockAddress URL: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("failed to parse lock_address URL: %s", err), + ) } if lockURL.Scheme != "http" && lockURL.Scheme != "https" { - return fmt.Errorf("lockAddress must be HTTP or HTTPS") + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("lock_address must be HTTP or HTTPS"), + ) } } - - lockMethod := data.Get("lock_method").(string) + lockMethod := backendbase.GetAttrEnvDefaultFallback( + configVal, "lock_method", + "TF_HTTP_LOCK_METHOD", cty.StringVal("LOCK"), + ).AsString() var unlockURL *url.URL - if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" { + if v := backendbase.GetAttrEnvDefault(configVal, "unlock_address", "TF_HTTP_UNLOCK_ADDRESS"); !v.IsNull() { var err error - unlockURL, err = url.Parse(v.(string)) + unlockURL, err = url.Parse(v.AsString()) if err != nil { - return fmt.Errorf("failed to parse unlockAddress URL: %s", err) + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("failed to parse unlock_address URL: %s", err), + ) } if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" { - return fmt.Errorf("unlockAddress must be HTTP or HTTPS") + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("unlock_address must be HTTP or HTTPS"), + ) } } + unlockMethod := backendbase.GetAttrEnvDefaultFallback( + configVal, "unlock_method", + "TF_HTTP_UNLOCK_METHOD", cty.StringVal("UNLOCK"), + ).AsString() - unlockMethod := data.Get("unlock_method").(string) - - client := cleanhttp.DefaultPooledClient() - - if data.Get("skip_cert_verification").(bool) { - // ignores TLS verification - client.Transport.(*http.Transport).TLSClientConfig = &tls.Config{ - InsecureSkipVerify: true, - } + retryMax, err := backendbase.IntValue( + backendbase.GetAttrEnvDefaultFallback( + configVal, "retry_max", + "TF_HTTP_RETRY_MAX", cty.NumberIntVal(2), + ), + ) + if err != nil { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("invalid retry_max: %s", err), + ) + } + retryWaitMin, err := backendbase.IntValue( + backendbase.GetAttrEnvDefaultFallback( + configVal, "retry_wait_min", + "TF_HTTP_RETRY_WAIT_MIN", cty.NumberIntVal(1), + ), + ) + if err != nil { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("invalid retry_wait_min: %s", err), + ) + } + retryWaitMax, err := backendbase.IntValue( + backendbase.GetAttrEnvDefaultFallback( + configVal, "retry_wait_max", + "TF_HTTP_RETRY_WAIT_MAX", cty.NumberIntVal(30), + ), + ) + if err != nil { + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("invalid retry_wait_max: %s", err), + ) } rClient := retryablehttp.NewClient() - rClient.HTTPClient = client - rClient.RetryMax = data.Get("retry_max").(int) - rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second - rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second + rClient.RetryMax = int(retryMax) + rClient.RetryWaitMin = time.Duration(retryWaitMin) * time.Second + rClient.RetryWaitMax = time.Duration(retryWaitMax) * time.Second rClient.Logger = log.New(logging.LogOutput(), "", log.Flags()) + if err = b.configureTLS(rClient, configVal); err != nil { + return backendbase.ErrorAsDiagnostics(err) + } b.client = &httpClient{ URL: updateURL, @@ -174,8 +236,14 @@ func (b *Backend) configure(ctx context.Context) error { UnlockURL: unlockURL, UnlockMethod: unlockMethod, - Username: data.Get("username").(string), - Password: data.Get("password").(string), + Username: backendbase.GetAttrEnvDefaultFallback( + configVal, "username", + "TF_HTTP_USERNAME", cty.StringVal(""), + ).AsString(), + Password: backendbase.GetAttrEnvDefaultFallback( + configVal, "password", + "TF_HTTP_PASSWORD", cty.StringVal(""), + ).AsString(), // accessible only for testing use Client: rClient, @@ -183,6 +251,61 @@ func (b *Backend) configure(ctx context.Context) error { return nil } +// configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made. +func (b *Backend) configureTLS(client *retryablehttp.Client, configVal cty.Value) error { + // If there are no conditions needing to configure TLS, leave the client untouched + skipCertVerification := backendbase.MustBoolValue( + backendbase.GetAttrDefault(configVal, "skip_cert_verification", cty.False), + ) + clientCACertificatePem := backendbase.GetAttrEnvDefaultFallback( + configVal, "client_ca_certificate_pem", + "TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", cty.StringVal(""), + ).AsString() + clientCertificatePem := backendbase.GetAttrEnvDefaultFallback( + configVal, "client_certificate_pem", + "TF_HTTP_CLIENT_CERTIFICATE_PEM", cty.StringVal(""), + ).AsString() + clientPrivateKeyPem := backendbase.GetAttrEnvDefaultFallback( + configVal, "client_private_key_pem", + "TF_HTTP_CLIENT_PRIVATE_KEY_PEM", cty.StringVal(""), + ).AsString() + if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" { + return nil + } + if clientCertificatePem != "" && clientPrivateKeyPem == "" { + return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not") + } + if clientPrivateKeyPem != "" && clientCertificatePem == "" { + return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not") + } + + // TLS configuration is needed; create an object and configure it + var tlsConfig tls.Config + client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig + + if skipCertVerification { + // ignores TLS verification + tlsConfig.InsecureSkipVerify = true + } + if clientCACertificatePem != "" { + // trust servers based on a CA + tlsConfig.RootCAs = x509.NewCertPool() + if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) { + return errors.New("failed to append certs") + } + } + if clientCertificatePem != "" && clientPrivateKeyPem != "" { + // attach a client certificate to the TLS handshake (aka mTLS) + certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem)) + if err != nil { + return fmt.Errorf("cannot load client certificate: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{certificate} + } + + return nil +} + func (b *Backend) StateMgr(name string) (statemgr.Full, error) { if name != backend.DefaultStateName { return nil, backend.ErrWorkspacesNotSupported @@ -195,6 +318,6 @@ func (b *Backend) Workspaces() ([]string, error) { return nil, backend.ErrWorkspacesNotSupported } -func (b *Backend) DeleteWorkspace(string) error { +func (b *Backend) DeleteWorkspace(string, bool) error { return backend.ErrWorkspacesNotSupported } diff --git a/internal/backend/remote-state/http/backend_test.go b/internal/backend/remote-state/http/backend_test.go index 9f32273cba..dcf7f4d420 100644 --- a/internal/backend/remote-state/http/backend_test.go +++ b/internal/backend/remote-state/http/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package http import ( diff --git a/internal/backend/remote-state/http/client.go b/internal/backend/remote-state/http/client.go index 71668c0a2d..afa3495488 100644 --- a/internal/backend/remote-state/http/client.go +++ b/internal/backend/remote-state/http/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package http import ( diff --git a/internal/backend/remote-state/http/client_test.go b/internal/backend/remote-state/http/client_test.go index c8bd121c73..b91cd1cacb 100644 --- a/internal/backend/remote-state/http/client_test.go +++ b/internal/backend/remote-state/http/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package http import ( diff --git a/internal/backend/remote-state/http/mock_server_test.go b/internal/backend/remote-state/http/mock_server_test.go new file mode 100644 index 0000000000..56f2f82d2a --- /dev/null +++ b/internal/backend/remote-state/http/mock_server_test.go @@ -0,0 +1,100 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: server_test.go +// +// Generated by this command: +// +// mockgen -package http -source server_test.go -destination mock_server_test.go +// + +// Package http is a generated GoMock package. +package http + +import ( + http "net/http" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockHttpServerCallback is a mock of HttpServerCallback interface. +type MockHttpServerCallback struct { + ctrl *gomock.Controller + recorder *MockHttpServerCallbackMockRecorder +} + +// MockHttpServerCallbackMockRecorder is the mock recorder for MockHttpServerCallback. +type MockHttpServerCallbackMockRecorder struct { + mock *MockHttpServerCallback +} + +// NewMockHttpServerCallback creates a new mock instance. +func NewMockHttpServerCallback(ctrl *gomock.Controller) *MockHttpServerCallback { + mock := &MockHttpServerCallback{ctrl: ctrl} + mock.recorder = &MockHttpServerCallbackMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHttpServerCallback) EXPECT() *MockHttpServerCallbackMockRecorder { + return m.recorder +} + +// StateDELETE mocks base method. +func (m *MockHttpServerCallback) StateDELETE(req *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StateDELETE", req) +} + +// StateDELETE indicates an expected call of StateDELETE. +func (mr *MockHttpServerCallbackMockRecorder) StateDELETE(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateDELETE", reflect.TypeOf((*MockHttpServerCallback)(nil).StateDELETE), req) +} + +// StateGET mocks base method. +func (m *MockHttpServerCallback) StateGET(req *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StateGET", req) +} + +// StateGET indicates an expected call of StateGET. +func (mr *MockHttpServerCallbackMockRecorder) StateGET(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateGET", reflect.TypeOf((*MockHttpServerCallback)(nil).StateGET), req) +} + +// StateLOCK mocks base method. +func (m *MockHttpServerCallback) StateLOCK(req *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StateLOCK", req) +} + +// StateLOCK indicates an expected call of StateLOCK. +func (mr *MockHttpServerCallbackMockRecorder) StateLOCK(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateLOCK", reflect.TypeOf((*MockHttpServerCallback)(nil).StateLOCK), req) +} + +// StatePOST mocks base method. +func (m *MockHttpServerCallback) StatePOST(req *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StatePOST", req) +} + +// StatePOST indicates an expected call of StatePOST. +func (mr *MockHttpServerCallbackMockRecorder) StatePOST(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatePOST", reflect.TypeOf((*MockHttpServerCallback)(nil).StatePOST), req) +} + +// StateUNLOCK mocks base method. +func (m *MockHttpServerCallback) StateUNLOCK(req *http.Request) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "StateUNLOCK", req) +} + +// StateUNLOCK indicates an expected call of StateUNLOCK. +func (mr *MockHttpServerCallbackMockRecorder) StateUNLOCK(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateUNLOCK", reflect.TypeOf((*MockHttpServerCallback)(nil).StateUNLOCK), req) +} diff --git a/internal/backend/remote-state/http/server_test.go b/internal/backend/remote-state/http/server_test.go new file mode 100644 index 0000000000..f52659f3c9 --- /dev/null +++ b/internal/backend/remote-state/http/server_test.go @@ -0,0 +1,425 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package http + +//go:generate go tool go.uber.org/mock/mockgen -package $GOPACKAGE -source $GOFILE -destination mock_$GOFILE + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "os" + "os/signal" + "path/filepath" + "reflect" + "strings" + "sync" + "syscall" + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/states" + "github.com/zclconf/go-cty/cty" + "go.uber.org/mock/gomock" +) + +const sampleState = ` +{ + "version": 4, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "remote": { + "type": "http", + "config": { + "path": "local-state.tfstate" + } + } +} +` + +type ( + HttpServerCallback interface { + StateGET(req *http.Request) + StatePOST(req *http.Request) + StateDELETE(req *http.Request) + StateLOCK(req *http.Request) + StateUNLOCK(req *http.Request) + } + httpServer struct { + r *http.ServeMux + data map[string]string + locks map[string]string + lock sync.RWMutex + + httpServerCallback HttpServerCallback + } + httpServerOpt func(*httpServer) +) + +func withHttpServerCallback(callback HttpServerCallback) httpServerOpt { + return func(s *httpServer) { + s.httpServerCallback = callback + } +} + +func newHttpServer(opts ...httpServerOpt) *httpServer { + r := http.NewServeMux() + s := &httpServer{ + r: r, + data: make(map[string]string), + locks: make(map[string]string), + } + for _, opt := range opts { + opt(s) + } + s.data["sample"] = sampleState + r.HandleFunc("/state/", s.handleState) + return s +} + +func (h *httpServer) getResource(req *http.Request) string { + switch pathParts := strings.SplitN(req.URL.Path, string(filepath.Separator), 3); len(pathParts) { + case 3: + return pathParts[2] + default: + return "" + } +} + +func (h *httpServer) handleState(writer http.ResponseWriter, req *http.Request) { + switch req.Method { + case "GET": + h.handleStateGET(writer, req) + case "POST": + h.handleStatePOST(writer, req) + case "DELETE": + h.handleStateDELETE(writer, req) + case "LOCK": + h.handleStateLOCK(writer, req) + case "UNLOCK": + h.handleStateUNLOCK(writer, req) + } +} + +func (h *httpServer) handleStateGET(writer http.ResponseWriter, req *http.Request) { + if h.httpServerCallback != nil { + defer h.httpServerCallback.StateGET(req) + } + resource := h.getResource(req) + + h.lock.RLock() + defer h.lock.RUnlock() + + if state, ok := h.data[resource]; ok { + _, _ = io.WriteString(writer, state) + } else { + writer.WriteHeader(http.StatusNotFound) + } +} + +func (h *httpServer) handleStatePOST(writer http.ResponseWriter, req *http.Request) { + if h.httpServerCallback != nil { + defer h.httpServerCallback.StatePOST(req) + } + defer req.Body.Close() + resource := h.getResource(req) + + data, err := io.ReadAll(req.Body) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return + } + + h.lock.Lock() + defer h.lock.Unlock() + + h.data[resource] = string(data) + writer.WriteHeader(http.StatusOK) +} + +func (h *httpServer) handleStateDELETE(writer http.ResponseWriter, req *http.Request) { + if h.httpServerCallback != nil { + defer h.httpServerCallback.StateDELETE(req) + } + resource := h.getResource(req) + + h.lock.Lock() + defer h.lock.Unlock() + + delete(h.data, resource) + writer.WriteHeader(http.StatusOK) +} + +func (h *httpServer) handleStateLOCK(writer http.ResponseWriter, req *http.Request) { + if h.httpServerCallback != nil { + defer h.httpServerCallback.StateLOCK(req) + } + defer req.Body.Close() + resource := h.getResource(req) + + data, err := io.ReadAll(req.Body) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return + } + + h.lock.Lock() + defer h.lock.Unlock() + + if existingLock, ok := h.locks[resource]; ok { + writer.WriteHeader(http.StatusLocked) + _, _ = io.WriteString(writer, existingLock) + } else { + h.locks[resource] = string(data) + _, _ = io.WriteString(writer, existingLock) + } +} + +func (h *httpServer) handleStateUNLOCK(writer http.ResponseWriter, req *http.Request) { + if h.httpServerCallback != nil { + defer h.httpServerCallback.StateUNLOCK(req) + } + defer req.Body.Close() + resource := h.getResource(req) + + data, err := io.ReadAll(req.Body) + if err != nil { + writer.WriteHeader(http.StatusBadRequest) + return + } + var lockInfo map[string]interface{} + if err = json.Unmarshal(data, &lockInfo); err != nil { + writer.WriteHeader(http.StatusInternalServerError) + return + } + + h.lock.Lock() + defer h.lock.Unlock() + + if existingLock, ok := h.locks[resource]; ok { + var existingLockInfo map[string]interface{} + if err = json.Unmarshal([]byte(existingLock), &existingLockInfo); err != nil { + writer.WriteHeader(http.StatusInternalServerError) + return + } + lockID := lockInfo["ID"].(string) + existingID := existingLockInfo["ID"].(string) + if lockID != existingID { + writer.WriteHeader(http.StatusConflict) + _, _ = io.WriteString(writer, existingLock) + } else { + delete(h.locks, resource) + _, _ = io.WriteString(writer, existingLock) + } + } else { + writer.WriteHeader(http.StatusConflict) + } +} + +func (h *httpServer) handler() http.Handler { + return h.r +} + +func NewHttpTestServer(opts ...httpServerOpt) (*httptest.Server, error) { + clientCAData, err := os.ReadFile("testdata/certs/ca.cert.pem") + if err != nil { + return nil, err + } + clientCAs := x509.NewCertPool() + clientCAs.AppendCertsFromPEM(clientCAData) + + cert, err := tls.LoadX509KeyPair("testdata/certs/server.crt", "testdata/certs/server.key") + if err != nil { + return nil, err + } + + h := newHttpServer(opts...) + s := httptest.NewUnstartedServer(h.handler()) + s.TLS = &tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: clientCAs, + Certificates: []tls.Certificate{cert}, + } + + s.StartTLS() + return s, nil +} + +func TestMTLSServer_NoCertFails(t *testing.T) { + // Ensure that no calls are made to the server - everything is blocked by the tls.RequireAndVerifyClientCert + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockCallback := NewMockHttpServerCallback(ctrl) + + // Fire up a test server + ts, err := NewHttpTestServer(withHttpServerCallback(mockCallback)) + if err != nil { + t.Fatalf("unexpected error creating test server: %v", err) + } + defer ts.Close() + + // Configure the backend to the pre-populated sample state + url := ts.URL + "/state/sample" + conf := map[string]cty.Value{ + "address": cty.StringVal(url), + "skip_cert_verification": cty.BoolVal(true), + } + b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend) + if nil == b { + t.Fatal("nil backend") + } + + // Now get a state manager and check that it fails to refresh the state + sm, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("unexpected error fetching StateMgr with %s: %v", backend.DefaultStateName, err) + } + err = sm.RefreshState() + if nil == err { + t.Error("expected error when refreshing state without a client cert") + } else if !strings.Contains(err.Error(), "remote error: tls: certificate required") { + t.Errorf("expected the error to report missing tls credentials: %v", err) + } +} + +func TestMTLSServer_WithCertPasses(t *testing.T) { + // Ensure that the expected amount of calls is made to the server + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockCallback := NewMockHttpServerCallback(ctrl) + + // Two or three (not testing the caching here) calls to GET + mockCallback.EXPECT(). + StateGET(gomock.Any()). + MinTimes(2). + MaxTimes(3) + // One call to the POST to write the data + mockCallback.EXPECT(). + StatePOST(gomock.Any()) + + // Fire up a test server + ts, err := NewHttpTestServer(withHttpServerCallback(mockCallback)) + if err != nil { + t.Fatalf("unexpected error creating test server: %v", err) + } + defer ts.Close() + + // Configure the backend to the pre-populated sample state, and with all the test certs lined up + url := ts.URL + "/state/sample" + caData, err := os.ReadFile("testdata/certs/ca.cert.pem") + if err != nil { + t.Fatalf("error reading ca certs: %v", err) + } + clientCertData, err := os.ReadFile("testdata/certs/client.crt") + if err != nil { + t.Fatalf("error reading client cert: %v", err) + } + clientKeyData, err := os.ReadFile("testdata/certs/client.key") + if err != nil { + t.Fatalf("error reading client key: %v", err) + } + conf := map[string]cty.Value{ + "address": cty.StringVal(url), + "lock_address": cty.StringVal(url), + "unlock_address": cty.StringVal(url), + "client_ca_certificate_pem": cty.StringVal(string(caData)), + "client_certificate_pem": cty.StringVal(string(clientCertData)), + "client_private_key_pem": cty.StringVal(string(clientKeyData)), + } + b := backend.TestBackendConfig(t, New(), configs.SynthBody("synth", conf)).(*Backend) + if nil == b { + t.Fatal("nil backend") + } + + // Now get a state manager, fetch the state, and ensure that the "foo" output is not set + sm, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("unexpected error fetching StateMgr with %s: %v", backend.DefaultStateName, err) + } + if err = sm.RefreshState(); err != nil { + t.Fatalf("unexpected error calling RefreshState: %v", err) + } + state := sm.State() + if nil == state { + t.Fatal("nil state") + } + stateFoo := state.OutputValue(addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) + if stateFoo != nil { + t.Errorf("expected nil foo from state; got %v", stateFoo) + } + + // Create a new state that has "foo" set to "bar" and ensure that state is as expected + state = states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), + false) + }) + stateFoo = state.OutputValue(addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) + if nil == stateFoo { + t.Fatal("nil foo after building state with foo populated") + } + if foo := stateFoo.Value.AsString(); foo != "bar" { + t.Errorf("Expected built state foo value to be bar; got %s", foo) + } + + // Ensure the change hasn't altered the current state manager state by checking "foo" and comparing states + curState := sm.State() + curStateFoo := curState.OutputValue(addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) + if curStateFoo != nil { + t.Errorf("expected session manager state to be unaltered and still nil, but got: %v", curStateFoo) + } + if reflect.DeepEqual(state, curState) { + t.Errorf("expected %v != %v; but they were equal", state, curState) + } + + // Write the new state, persist, and refresh + if err = sm.WriteState(state); err != nil { + t.Errorf("error writing state: %v", err) + } + if err = sm.PersistState(nil); err != nil { + t.Errorf("error persisting state: %v", err) + } + if err = sm.RefreshState(); err != nil { + t.Errorf("error refreshing state: %v", err) + } + + // Get the state again and verify that is now the same as state and has the "foo" value set to "bar" + curState = sm.State() + if !reflect.DeepEqual(state, curState) { + t.Errorf("expected %v == %v; but they were unequal", state, curState) + } + curStateFoo = curState.OutputValue(addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) + if nil == curStateFoo { + t.Fatal("nil foo") + } + if foo := curStateFoo.Value.AsString(); foo != "bar" { + t.Errorf("expected foo to be bar, but got: %s", foo) + } +} + +// TestRunServer allows running the server for local debugging; it runs until ctl-c is received +func TestRunServer(t *testing.T) { + if _, ok := os.LookupEnv("TEST_RUN_SERVER"); !ok { + t.Skip("TEST_RUN_SERVER not set") + } + s, err := NewHttpTestServer() + if err != nil { + t.Fatalf("unexpected error creating test server: %v", err) + } + defer s.Close() + + t.Log(s.URL) + + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + // wait until signal + <-ctx.Done() +} diff --git a/internal/backend/remote-state/http/testdata/certs/ca.cert.pem b/internal/backend/remote-state/http/testdata/certs/ca.cert.pem new file mode 100644 index 0000000000..81e6201e91 --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/ca.cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFBzCCAu+gAwIBAgIUFPfAxSWlzjWAdQAW+uDbciQm3SowDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC5jYTAgFw0yMjEwMTMyMTE4MTlaGA8zMDIyMDIx +MzIxMTgxOVowEjEQMA4GA1UEAwwHdGVzdC5jYTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAJUdKvIM9q7H8TLqj0O6qHUnbE0N3dnNNGVtyO7Nkn4t7urx +X4qmQ6nMzKlC5YhGIlOKO4X0kPXf623+bP+jUf9qAFLkx5SK9TDerhh3e9y9+0YY +C+CM8bQdJD7jFN1oOcKTJipNbjVXCqWqrBXJg91v3p4kyUvGUv05d3pU9nQvKd7R +BGdWh68hjPFqdFso+A1ggxwJ4pEQCllxLu60RpRFwPoup/BeblPz9f3voeqhxT1J +RLviG6HhpMxh44qNh8UrWGyaAk2C5c0rghBUHdfx/RgP2cYuUo5fhPYOHhO0lX80 +0LebXA6nwOhVeHNvrRfjEJS3tTWaFXyaOUiJT2QX2nG0i6cx6pS8dLMMSFLjMSX6 +bTH3KtTR+UrOfC3B47FOO5U++EnBg3WiZCKp+i8+5Sc3MjTw4B8cmydYr59hNWrk +8zrfG1uE6WvxKg1bRc1FcixERcLnIbRH6LE3hHXzYlLoJ8+q9zP0EGqGHycSlv+C +E+6QMMKU0u2tHnixqhlt79ad6bpC52VS3lFt3Fh/TEKWjS1rn2hYZKGSymJpbPFn +q1RQZcxZWjKjqi5UEuAVGfBc4+HLZHq2Vq9umjLn0nuVixjBeBsCBaFC/amksFEJ +fAmMXDERO7Hb4vePq1t9iusWrRPhkvZt6R1Pozg1Ls+xSJQE09n3jWd0/fMhAgMB +AAGjUzBRMB0GA1UdDgQWBBSe+CLJRDjlHurYVRcXvhXyohcVdDAfBgNVHSMEGDAW +gBSe+CLJRDjlHurYVRcXvhXyohcVdDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4ICAQBluWhlAuG7CfMP31aJzl9AucHHLwfAECKg35XTPiF+YrL7rcbQ +0dQyErCXyx7lLGEMqfNxVW48JtLCAATAZk3PwaQdU5OTcKA6Q/mQJwagfgmCVC5+ +Y4fdc7HhfOkGCOQ7aqyJ/EmygafgShreNimRDgIFomEs2hEEKAfvq2YBKcfcDyS7 +vCJZgzKoDmFe4DJjnYN/Gmj/4ak1kwtkoTkwdBlK+zWfbWHSUweXjCvbPPhKCPfy +3Vu++BIW7402aLsP4xyQY/HPGErV3l1TpY3FdCENGQXANF/gPDWj/Q92OdTMRL0U +XXSshNT3YjCxUH3M4A07A11TQwXZRFs2AkZyjJ6M5XNd36FswHh7fSjNLThU6h2V +dI0y/rU4y24KG7KeUayTE1HLGGDskZdXSOL2vH/MTvpheKnLE8fQrKb/SgY+l9RA +fIKwjDfMSL11luuSUIdevt5CEGFms8hpLU1RG2z/qSYz3If/dhN6YdiFJ54Qhjw9 +J5UO4eucsCm3MmsX2jUsDUIjHu92Rt7a3N21lVwzAifwwUzlDrY5xFrtpdhiSEAd +HFmIQOEr3C9xqD3v3b/4N9SoOjZS2j4xk+GQ8XZeTDYf8ZlkXvXHWwEHbVqj0toe +WDooC6oivNJAEs2GxJpyLmmfxIbRjE1sdmVZtmlSb3hY0Rme1SF9FoyZDw== +-----END CERTIFICATE----- diff --git a/internal/backend/remote-state/http/testdata/certs/ca.key b/internal/backend/remote-state/http/testdata/certs/ca.key new file mode 100644 index 0000000000..e297d3aa8c --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCVHSryDPaux/Ey +6o9Duqh1J2xNDd3ZzTRlbcjuzZJ+Le7q8V+KpkOpzMypQuWIRiJTijuF9JD13+tt +/mz/o1H/agBS5MeUivUw3q4Yd3vcvftGGAvgjPG0HSQ+4xTdaDnCkyYqTW41Vwql +qqwVyYPdb96eJMlLxlL9OXd6VPZ0Lyne0QRnVoevIYzxanRbKPgNYIMcCeKREApZ +cS7utEaURcD6LqfwXm5T8/X976HqocU9SUS74huh4aTMYeOKjYfFK1hsmgJNguXN +K4IQVB3X8f0YD9nGLlKOX4T2Dh4TtJV/NNC3m1wOp8DoVXhzb60X4xCUt7U1mhV8 +mjlIiU9kF9pxtIunMeqUvHSzDEhS4zEl+m0x9yrU0flKznwtweOxTjuVPvhJwYN1 +omQiqfovPuUnNzI08OAfHJsnWK+fYTVq5PM63xtbhOlr8SoNW0XNRXIsREXC5yG0 +R+ixN4R182JS6CfPqvcz9BBqhh8nEpb/ghPukDDClNLtrR54saoZbe/Wnem6Qudl +Ut5RbdxYf0xClo0ta59oWGShkspiaWzxZ6tUUGXMWVoyo6ouVBLgFRnwXOPhy2R6 +tlavbpoy59J7lYsYwXgbAgWhQv2ppLBRCXwJjFwxETux2+L3j6tbfYrrFq0T4ZL2 +bekdT6M4NS7PsUiUBNPZ941ndP3zIQIDAQABAoICACEM6/3mfa7TxlRgxQxgDQKa +kFir4CZsY1av9L9pdTTefXw5r9GNdKXoLNy/ZRzFXsphczwHrzGwRgCFSieHTZ9t +IVE+QDZebmY8lR37LcsJmO46WjeVReWEKAqATpmchmDoOKdbrjfIaSW7JJVXqxCj +wRYQVUWkWbSiziahOlcaNQ+cCHvXJA/fQdwomk2yUPi2EZlfX4aDpaeZfKuP7azj +oRhSywpuA8o74qQ8PwlAffVNjhyOy00gNGTQtZx6LkO3jcvUfvorL0BAin2QB2Vb +z5tLuBtDHS1NYq0fB++aMSCW1kQ7/TWKXSmh+Cat9BG9VGmCJnoRAv4xOM0pEh1o +vui18+UT2tJ4OZLP8tOH1A0OMTF98EojmKwUlStnkm+vNgdU0IWPFZng77qL+rJd +9sR9BkT9gfW+0EMUMG25ocNV4/t01O0q95oH3F3LQ7iIKGzzErX//2qGteaHEu9u +Cbd1QniQDKzMEJV0hHpWxAcZcJx4Wje7dPgDCRTv2juU8sWM7d43KAAQ+tQpLkem +yzK0UAQzSnWS2QjrR44hujYmf4zPcMsQFBSvztP7dbtwKuTQbiQRYn4ZCqcCv/DQ +RpI69NoulWO7kHhbZqqtiWxcmtdLSwN+9Gx/x6sgYSemx5h8rti0lIBi/Pzfq39U +WuiGg9yjUSU1zqdtDdihAoIBAQDI6XRf/umV3gdba1krxW4q+g5ly3eJlYd/BKTC +xYNx9ixjOJ+J1ov1gF2P+60HDhYQ9bsoPMhHfJU6qXR5Hu0xZSKK7TPkcJHH6WHm +ErcqtgJiADtl7sfo/GTn45MaF71fTXSgrjCMLGA99IYPooMVWE+TrFEYNOcPgO4x +hNq0n0C29ORSr+9oqStCuJ5a+iDvL7KGnmsyun1HuWUKVdxbt4CPpMwsQWcBLfVg +Ispd5q5fG/DPDZFnha5XLbAPWeLn+1mweK4Y4Jugr593o6S9q04jlLa5wLDMCXUN +fPXJFJcg+vcvcZ0IlfvFsfZ8IrO/UMHqOeMUhTt8s4KoMYAFAoIBAQC9/9+aC+Tq +H4t1Dl+GZAavsVlP7kFekmAK02GJ103qmFcITpcx8cx3eA/0oZGpvQCHsJGHoa1P +EaMtKVITokkvOTJB/zxvSw6K5oCx1qEGoEqyXTs2rNLVchGwunpE4C6Ikhw5+gew +e299nmLE6bckStCLVINDWQOjRJ0Jl26rmdGk/3wLgliZNVKu/Yhsr4RY+FZoErOk +YulZp648GfvuwXZUdAWIdmg4JfOrcizmhya3L0qteOZ7FpqKXCPmDgXD3E4IVdMJ +CRfywxkqXCHxRlN49/9I2y3B3eaWkGStqgvzHbrMR6uobn4+YRkjgam4ILnUO6Vt +Zy1R3HHvSH1tAoIBAQC9WGc44UC64RkF61GKkvKUxj0zamIp5CZiarnsZcDPcjW6 +/O4+NViJ8oQ64fHbqEbbjPrpnP8TgDITqwf97kuUNcAsNgilzgFV6nk9H35IXmg4 +fAd+tV7qEJP4ht1nxd/PJWw40nEmadv6B60gpwPq5eN5RPjYW2M3lUbmnFKRz1Rq +GLnlw7FZbbU7mEqFax4GzWjuvfZBRMg1BGBZMToPpg0fUyyouKqezfVmuOMHRBQp +xmdYe20Bp1b7Ci/XB9t0zcllKxbIk0WYVmtvkWX86qkll03uGc+FO5R5Nb9d1m3n +wx2aNPTN1qwFUQb/TqUgNLfMSunbuQSrLXKBmMURAoIBAQC9wkXiJqr0IZk4yagi +ItiCtI/MwtpKx8pgRYmPD5fkC04xH7zlxuc9Eo5s9sjyS6+x1WkjmxfqdmUQf8pX +jaemIGvPekkzpjTaCSjTdNbSNVklFvRCwQy43PpKFZR0IaqX/8VtKghv/Hf3cC6Z +GAsvlgD+huOqaca2U5q7r6B6hl/ZeMi8/eva6GSyHMkaM5ns+enie3srXRZN0qiz +ogf6BwJViqLUDd485bqdqqSpgKXsIrFk2/DlUkf6k9fOtoaPfQH6VS02QvzGGpCR +u/6yaFiJ4rX2X+EtVKAuE/xZbhINN84OpC4PRHuVdYiT67ZEDXtLOl8YCwo6Tf8E +ytNpAoIBADWxq0izh7P7pGW58JhA7wl3vCUFUJ77JC4pjYzKlBsMi7OcZrlzNi2J +4rtO8JO5S8eG5erEA1FuPb6LCPqzetKTD+xKKxgEcICkWuH6RWdRq82bkVqL2gQ7 +tp7qdfwNl0K6XNnB+VaCPAzsrFJJZnoRIz3BQBocT3Fwxe/XwS1KDjLere//bgHR +9jxYZRHKr72Y9lTMWMW2ygxmdlWk37pv4rsQK31HOGo8JtRVOISZnHLSQwMVNQ25 +5IincDO5FGjOQxFxrGjw+YQkAcQC8PkpiKU7hY396FHEvrr8xqTl/TPuJaAuSbvW +g3yddc0Zj29o1jw56J043a37q1CmmsI= +-----END PRIVATE KEY----- diff --git a/internal/backend/remote-state/http/testdata/certs/client.crt b/internal/backend/remote-state/http/testdata/certs/client.crt new file mode 100644 index 0000000000..1d5a74459c --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/client.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIUJsntRGo85J+ZJAb73snhKsM1oVowDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC5jYTAgFw0yMjEwMTMyMTE4NDBaGA8zMDIyMDIx +MzIxMTg0MFowFjEUMBIGA1UEAwwLdGVzdC5jbGllbnQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCUQebHvDL2ksHcNh6hw0xMCbPxwrBd+qQVFGf/2wL3 +Dk8Ls/NgKQqkoG4WPi4vIuu277+7ngqUZcFbDL/MO7uAWlzthFkhI8IyXeB8t+cj +liqwdgfRFLvoae1PG6ZoFrTXgOsW3tW8SRC8Kax7RdJjEMU1yWEC6OwiH/gabqZM ++i2qSwOgPnxAalljbWDJU0kj+zRfw35W4L/Q9quMid8KQYE71wQoiiBFYFcx552c +kL30xEpKat5ffB42sBpDzO3S/dM0k36im3wEFJHaEW2q4+0Ns9/PQ2OxIfoRC+lD +qYVPeljNSK2n+PSZDjswpZtqK68RD0AM0PmuPqV7Q2DPGoCXpwcq3lczlFH69T7z +s7izG8cnmyi9eWiXgPFWG5JzyeQi2P5fMumF1UVpG2NHyDyEGfXME8dh4S6BpAUJ +9BAXjatjzA5bq4CGS1w/pFrUvhiVQY7byGDqrtTiDa1f48T2CkvVmRUIiKlrvnDe +ezCnJ6P28D0yISyJLN45sQhuyw5idaXHl2AsvDRDFj2iZ/WhY7tCf0O/DSCuI1uZ +WcFXHdRFn9RoGuUqy97+6rPZaB+xNnx83O9pG+Hrx2iz2pSD/pb0b9xKH5VvN1pN +JjtaoMXod1+2z8XdTUzPvkeZyDXIasaZwmSEOOZmGgoRe+KE4ZBlk4XBlm5p2Q6U +RwIDAQABo1MwUTAPBgNVHREECDAGhwR/AAABMB0GA1UdDgQWBBQ/5KcOS58ZKYth +wRpJ+VKCcwJdpTAfBgNVHSMEGDAWgBSe+CLJRDjlHurYVRcXvhXyohcVdDANBgkq +hkiG9w0BAQsFAAOCAgEABiy0c+7T7dam8IjejbDlamAMvDCWFoVW+mLjsGwaS7vx +jmtGig5E08q7axf32iAkfwzi/vEwt66uWGVctUm6/EqH2XvlqZXcsMGiAuWYwJ2Q +DXowHlcIoIRC958qA+6cCAdxoUnTpYSdWWMR+QZ9XDB9MaAZJ+zKhb8nEETl9jGR +Z9iaSEnupposxt5NMvNUU8dTjjjv430WvZnvZaTvegLIQ5QaHeECUQ61Nm18tEey +cPiMu2TN8uO4m67lj4kyXaS3wD7zNuZph55g4vNbQrffTEHUZSFqrr1fyG+7Y+fb +F9hzbhqBgCnYQ5JaxtVbqFAvwDFWRoq2G9gARi/Yuf34djoP09IZvbRymZWJ5857 +KRCT6mBestfOzu2oIz6lDO44fFiejOTDCSDHZ2Try3xAsqS4LAZjWNSqfBIJwABi +bNTWV2yxtlnqEkaPtGYSwQLdF8MTBRbxzsiELktgdgt7XcfarhEKj9iHWirEt0Cw +POnl8S8GzwpsSAomijlLhfyU0J1+p6UP0zJE4YOjKZFv5ddmBCeSTwj0gwVSsSNg +ff7T7IvkTcIMZUlrskeMY4svXpI5FeG+sXXNp2J/iz4XIQdcdpB3t+fDCUcic9Fq +ILJKT1sQpjv4gyAO2BJd4D7clUJwDC059+dh3dDC9d51uHvCra2F/+FGeodQRuU= +-----END CERTIFICATE----- diff --git a/internal/backend/remote-state/http/testdata/certs/client.csr b/internal/backend/remote-state/http/testdata/certs/client.csr new file mode 100644 index 0000000000..ec0f313710 --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/client.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEfTCCAmUCAQAwFjEUMBIGA1UEAwwLdGVzdC5jbGllbnQwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCUQebHvDL2ksHcNh6hw0xMCbPxwrBd+qQVFGf/ +2wL3Dk8Ls/NgKQqkoG4WPi4vIuu277+7ngqUZcFbDL/MO7uAWlzthFkhI8IyXeB8 +t+cjliqwdgfRFLvoae1PG6ZoFrTXgOsW3tW8SRC8Kax7RdJjEMU1yWEC6OwiH/ga +bqZM+i2qSwOgPnxAalljbWDJU0kj+zRfw35W4L/Q9quMid8KQYE71wQoiiBFYFcx +552ckL30xEpKat5ffB42sBpDzO3S/dM0k36im3wEFJHaEW2q4+0Ns9/PQ2OxIfoR +C+lDqYVPeljNSK2n+PSZDjswpZtqK68RD0AM0PmuPqV7Q2DPGoCXpwcq3lczlFH6 +9T7zs7izG8cnmyi9eWiXgPFWG5JzyeQi2P5fMumF1UVpG2NHyDyEGfXME8dh4S6B +pAUJ9BAXjatjzA5bq4CGS1w/pFrUvhiVQY7byGDqrtTiDa1f48T2CkvVmRUIiKlr +vnDeezCnJ6P28D0yISyJLN45sQhuyw5idaXHl2AsvDRDFj2iZ/WhY7tCf0O/DSCu +I1uZWcFXHdRFn9RoGuUqy97+6rPZaB+xNnx83O9pG+Hrx2iz2pSD/pb0b9xKH5Vv +N1pNJjtaoMXod1+2z8XdTUzPvkeZyDXIasaZwmSEOOZmGgoRe+KE4ZBlk4XBlm5p +2Q6URwIDAQABoCIwIAYJKoZIhvcNAQkOMRMwETAPBgNVHREECDAGhwR/AAABMA0G +CSqGSIb3DQEBCwUAA4ICAQAFUKmXAcULGC1idSXVWRnhzzr6qnl4K2QZVse8kNsk +BD+ePZp7/jc9URP+ykFhVHc1gOy0VgvNm6qePS9ccPTQrRxmXUmMrV2ead9z4h4O +OnnIyfxLxO+Kd1lJ/1UU8CNs3tDQnxEvtx1hYBIDNsyB4bAsfGVBGzBrsoHEjZOg +zTvvPEnH/GpnEITTwK9J6tZ2zanE0K5z2NcSHPjzO0z92sAkcfTIZovcsVCGR3j4 +UDBMWAgK9vybG5G6taQyducU7/kMLcEP5ayG0qIeIrS2GRmOqSixAQQ+Qk6Ucs4w +HD3/9oue5vWJEG0j86jEchdg3OCbHbQEje8Bf39xhpICel45EdGsxc61kiB/c5Lu +8kYQTXDr9P1wtAag5XLmv/nf6pzlQ+LthU/2/EH0r948Rj2Yz4HOOHsfPuB/izF8 +NTAH/VBgp2c/VRjEYd0YQ4X3AS+Q8BwBeR8+OUJu97AIWnM8kjTcRa1ybCGMkQ3L +IjGWgIYnICEmiEJhLo/y7jMSdRwUT9g5zz3koqChzeFSU1LuH/yE2B6GfneblDK+ +B7WDOkUEbHfJ5q0TZwWEgQdpcY5OH+o78NfJpTvgNtPV3B83+g+DdAW2jtgMZ6do +Rb7V+uPvbU9VC2Ng7jacewMtfM3PKugIZ034UUjebQ7/N5ZD01xuJKOG/w2LuUGh +GQ== +-----END CERTIFICATE REQUEST----- diff --git a/internal/backend/remote-state/http/testdata/certs/client.key b/internal/backend/remote-state/http/testdata/certs/client.key new file mode 100644 index 0000000000..392a800712 --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCUQebHvDL2ksHc +Nh6hw0xMCbPxwrBd+qQVFGf/2wL3Dk8Ls/NgKQqkoG4WPi4vIuu277+7ngqUZcFb +DL/MO7uAWlzthFkhI8IyXeB8t+cjliqwdgfRFLvoae1PG6ZoFrTXgOsW3tW8SRC8 +Kax7RdJjEMU1yWEC6OwiH/gabqZM+i2qSwOgPnxAalljbWDJU0kj+zRfw35W4L/Q +9quMid8KQYE71wQoiiBFYFcx552ckL30xEpKat5ffB42sBpDzO3S/dM0k36im3wE +FJHaEW2q4+0Ns9/PQ2OxIfoRC+lDqYVPeljNSK2n+PSZDjswpZtqK68RD0AM0Pmu +PqV7Q2DPGoCXpwcq3lczlFH69T7zs7izG8cnmyi9eWiXgPFWG5JzyeQi2P5fMumF +1UVpG2NHyDyEGfXME8dh4S6BpAUJ9BAXjatjzA5bq4CGS1w/pFrUvhiVQY7byGDq +rtTiDa1f48T2CkvVmRUIiKlrvnDeezCnJ6P28D0yISyJLN45sQhuyw5idaXHl2As +vDRDFj2iZ/WhY7tCf0O/DSCuI1uZWcFXHdRFn9RoGuUqy97+6rPZaB+xNnx83O9p +G+Hrx2iz2pSD/pb0b9xKH5VvN1pNJjtaoMXod1+2z8XdTUzPvkeZyDXIasaZwmSE +OOZmGgoRe+KE4ZBlk4XBlm5p2Q6URwIDAQABAoICAC6TP3l6/bWpqB5SoC/oZzUy +DSZDp912SorWxM9DkfxkMd/20dvhONc8ESmKsj6bpVpsmhrKTP+Osf41FKIIF+D8 +QlpZrBh1n+HrzQTRT1tGJzYVdmIwNdIPSP6DrLThgUF8Xh5qtdG3UHsUSnvVlQEL +OTErCP99hgU4btx662Kea68mbsauKqGf52INcAz/Tahwl+UHyM5pP8lZXM5DV97k +ckGGzGch8X5qBCqI3WJctFhLPB2B0kdD+kfq7e1j2Ujh9bJ8LZnO59huT92mgQHh +Jc0at5Jo1M5GYsVtLQRVIqyzvmcLUIbG9qyIpH6lYBwsCgz9cf00v2OGib0eDzC2 +ZqeiotDiul5f6vtNw1YqDdrZWSxRfwqoqzeZX0/bypw6+UTGri+lU7RSRsA845gd +gMjcAd2WocqSNhPBTVPivIDmzHfSMomfnJHCw+aKcm/o6fxcSp4g8pPzpx52h0Eb +tO7rTKTlmglZ8Cc59CPmRqLq+Pk+lHgxTDOUOxZANCuBih4MrDJ2NFnnZxervjPM +te3VlJu8nE5mNuHhT1czekU01lPHQa2E4f5Q74bWpYg71KntN2Po8oUaQQjcX72N +b9N0TzeBrR2TQD/j2S1Mz4ZoStOwOovHdtPZOmfYN30OMX8JzqZLhF4Dfyx8T1JC +Pd1089N0HbX7XIXuEKJtAoIBAQC8xIVQMqg7A9O3u4i2Afq6nASHDM1tnH1l8Ua+ +T2Z6kmPBgjPb+tBrX6YeD13yDxKvfsEr9GnyuQQJdvqNwjVkGNg3Z6HX/HyHIUij +bub3LvpyQzYNkpb2qcoka+AIWDvbpmstetobQ6OK9F914ur29L9XXSp0diZn+1Ff +JqZFfZwgkhsz0Q8HZxT4FfbV+2k6PWk3RPriyKLgZAd3OXswbx/6K7owdUgrOhYA +vUXRae0UrNi0Y2kanzzoBdBLDS3ChML5VBMPIrHac4A97FJS88aewEgMC0E1wlin +J7nwVAubAG7YzEpQeP/4Wp2j9hfwqtO8JlaJL1vygAgQG42NAoIBAQDJD7v0qDal +cuGaHEQLhEVOu7JtQwHn7LmJQyamqCmlL0mDwQOixhEh58sQFUeNYEGKwEGWF+Tk +hA8sAYk4jagUF5sCkOQoWdFWna4uPqlpwozFc/Wj3jKoiYOGn4SFeEJdgQG3rMDM +oepVvaNOljJnNlntKZHUwOM0F6xxV4dXyqnPn+nXmM/Iywd+LSsMN5w8c4IFE+Da +WKrbKMobdaARtx9Lpv7ESObLX3eCRqL1KbuRN2a000Ojfv4kprH1XxMdCWUxXoLk +ac1I29cvx0FFYJfIr3CScdwaKiwGKguk8IMIih3dLulgnaqJ2vUjI3qyQMEFRMBW +3HxFAk3VU6IjAoIBAFRmMYz3+UvZnDHMAYYPQIFq/IM9cCQQEekghZbVfWZUSZHd +mz5B2CoJ7AYIrOJrZtlcfRYgA7bojiuFLOVw7dpBWXr8NNqTI0Jv2UBpd48RTB0G +fAZ5glHq/FxodxSEDs9YixcclKQYC+k29e+Jc7DTITH4j+DearGXJny6lSEA1muh +p9P1JxkSN8fsWh62eAf4KTDzAJGhT2Gwl73wz2mKZeu+3VKJPalGIUxXU/4btErI +NWQCBp5GkD7VSpoj3E/aeCpuMs9Tnd2kQrRtEynPoQCdzBjGd3OH34dtNa+EhGPb +P7RjMt7kGt559X23rGCIoH7BTXOs3xl/sRsylokCggEBAILXEmEr9iPElrtLGZzE +/rU1v+8KY/shObvxTv21ASTVmOl8eXk7m3qM9MAKmP2PXheE9SlPc0yiA52Hglyj +EnXAxsbsswzvJiNPiUHe1TBVwnXb+EYjGqRCmKzKsdqJX+apRQzaBr0jwPL67YL+ +it5PqEWFf7kLrM8BeN5pL1IaOFc8oVgDwXPRa5bYneLdbXaJVFspjHGKseTcrmkg +KoJcwKjii3gAWPCPt523ieQwvDbL7rJNqP6Eba48LCKZND75FjkCX/t0PnrjVS1q +ZTdYnG2kfYVPQwRj3TJFuj4jpaGw/64oEQcmkwwSyOOM+xN0wCdFjkT4RoZB8ZSZ +UDECggEAQ7nnkDKqL2SGsC2tuXpOO1rn2Ifp71hK9TiSUh6QIEk3parO5ualayH+ +UUsav++GIexHIxH5sxbeO6wCurRbrA/64tTXRYh/T9tIkfI4wstgRoFCMPN5CdIs +Q1s48wH1KfQWz1UiNM0rwJKs2kDIWOj9bZotq9Ir3dXYoKgr4sotQFZUVyq2n5Z7 +jE0/bYPHI8+3WXaZsLEzBA167/6IUzIoM5QEgKYP3999CEu2ZKewjnElPMflDJWm +OGT5JYz9SjwKH/9ngGcpIo8i35LSj5R9cK9Sf6dTKo2YZAU1U8yjfaRXIVAmSBFS +SXbUSo1aOU/ZWOnVKdyjhPBcPZMEqQ== +-----END PRIVATE KEY----- diff --git a/internal/backend/remote-state/http/testdata/certs/server.crt b/internal/backend/remote-state/http/testdata/certs/server.crt new file mode 100644 index 0000000000..d16cc62368 --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCzCCAvOgAwIBAgIUEJ4OCw9X1j5TegymXZENMgfdBZcwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHdGVzdC5jYTAgFw0yMjEwMTMyMTE4MzBaGA8zMDIyMDIx +MzIxMTgzMFowFjEUMBIGA1UEAwwLdGVzdC5zZXJ2ZXIwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDBTTBoca0tn2EAxbQLXw1diEH5+YltZUFz5gH3aSDf +H+uKame4iFsPybsUstmUqy3D9ZTzjNcauAhB75+RgePn4D0/qePPjdsFz11jxacA +AMkg/mPLPtrkEAiRzvSXXoYN1Gq6uNdGricyKGtSKzqQh158W2ZfLKUKvgGlQ8RD +3RFsFanQS6aiNwPgFK1SeFFt4wTJbtpvKNpJSe/XDDmkyMIN6/pVRo56v2PvsERA +mUQJ+blyhOy7Egt0/uF7JUklzLIi5eKjv2JpcVwH83OAovKl1vy2mfiCZSjCwQeB +ahnWgAaAaJir7uOVNDNXI4qM/KySKN7Nfyo+9sRuNBjMb5b0N8s70YIAyzoylJ0U +E8x+a7XyMJlPvpARGXuNSDwR9rRytpGZIeMPcO1YQh9+V5k7il6/70thNYW5/Cb0 +PsHU5XOSlmOsOLcr1JaD2YTJJflVyhrPwMSAphRuUCFuyQinnHi59Rk2rNuTj/R3 +dFrSqtcfnvjbc+1KbyDFpnyf40W/EPmS4mF3UPRD4oRsvXpy85E25uh5Q+R4MMdd +g3KHsZ+SiObUBa33kd9rG6peJz0cvkJIhBzbJcXPzN2EMgow2C5MYKjmNXclYWIH +ypkFSo6OxFHhIQdeh3Ga7pZqJaOVUA8wm2olIjRQgQFJjRRc6KU2w95lJlFvHXlW +0QIDAQABo1MwUTAPBgNVHREECDAGhwR/AAABMB0GA1UdDgQWBBQpv1S2rSSjgJ7a +xONcLKxYRE3qJzAfBgNVHSMEGDAWgBSe+CLJRDjlHurYVRcXvhXyohcVdDANBgkq +hkiG9w0BAQsFAAOCAgEASpVE6Pj/sPf5heCDI8miF3Xw65BkLMCCL4ZUOugtK0Hg +dbcnaMd6Hwf+mEs/2jaD+2xah489fX4KynJnQ68VpnTMT4yYcsEfvwmZA7Bqo/Ef +MwyFJe/E+Y1mAu7KQodLZ1E13cGVQKDQVwQ5ueyRD3C0bY3glMKfnXvnIIEMiSCg +UTAstj4Z0h9KYrVSRRVfCGOtlvFPo8jg+yPVPsDqGHn2hOH+FYoHv8V1/gGrXJTe +HcTHFIAIkBefHAXCaCYYq3Qfp/ZBpuT5N4bwQtHKmgv5hhyy0kaZRFfE98WkGdSk +Yg5wZRIX6UbjPdyiEnhQdOrnGDehKf9iwv1q98B9hgXzEzdK0e3bR8UY2MRvs/Vz +L2BBDkJHsTo9P1q6zAsmfVNhQPGrEH2pDir8yYpXPz/ocZa7GghJ/RPrYirVTHZp +fNxoMkNfgfVQpSsFvvI/fMGfhG65TQJdq82rAJ5tRRRs69uA00NCggKRWmEdVYpV +jWuMiLrE5U2tHruMytM/ek6kjhzmNpJgPG2alsJHgVb5G8elcCuC0Dx5HjnwbR60 +8V1v2z5kgU9dkT05vZ5RPmNyuv+VP+8Qx/NPCMrf1SaQffW4PaP3YUaRwzJYzEP/ +ZDUOmPsgUMLwj/jT3sEkSc1qUByui2A0QJk2dQzcbNfvpWoBQ+q7m2OHkmzXZCc= +-----END CERTIFICATE----- diff --git a/internal/backend/remote-state/http/testdata/certs/server.csr b/internal/backend/remote-state/http/testdata/certs/server.csr new file mode 100644 index 0000000000..92d06b74a6 --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/server.csr @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIEfTCCAmUCAQAwFjEUMBIGA1UEAwwLdGVzdC5zZXJ2ZXIwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQDBTTBoca0tn2EAxbQLXw1diEH5+YltZUFz5gH3 +aSDfH+uKame4iFsPybsUstmUqy3D9ZTzjNcauAhB75+RgePn4D0/qePPjdsFz11j +xacAAMkg/mPLPtrkEAiRzvSXXoYN1Gq6uNdGricyKGtSKzqQh158W2ZfLKUKvgGl +Q8RD3RFsFanQS6aiNwPgFK1SeFFt4wTJbtpvKNpJSe/XDDmkyMIN6/pVRo56v2Pv +sERAmUQJ+blyhOy7Egt0/uF7JUklzLIi5eKjv2JpcVwH83OAovKl1vy2mfiCZSjC +wQeBahnWgAaAaJir7uOVNDNXI4qM/KySKN7Nfyo+9sRuNBjMb5b0N8s70YIAyzoy +lJ0UE8x+a7XyMJlPvpARGXuNSDwR9rRytpGZIeMPcO1YQh9+V5k7il6/70thNYW5 +/Cb0PsHU5XOSlmOsOLcr1JaD2YTJJflVyhrPwMSAphRuUCFuyQinnHi59Rk2rNuT +j/R3dFrSqtcfnvjbc+1KbyDFpnyf40W/EPmS4mF3UPRD4oRsvXpy85E25uh5Q+R4 +MMddg3KHsZ+SiObUBa33kd9rG6peJz0cvkJIhBzbJcXPzN2EMgow2C5MYKjmNXcl +YWIHypkFSo6OxFHhIQdeh3Ga7pZqJaOVUA8wm2olIjRQgQFJjRRc6KU2w95lJlFv +HXlW0QIDAQABoCIwIAYJKoZIhvcNAQkOMRMwETAPBgNVHREECDAGhwR/AAABMA0G +CSqGSIb3DQEBCwUAA4ICAQA5BmbXy/UXXNXe0WHR1gxx5nwmJ1CyNy+efVq4cl8Z +ltxaTWy8IZOGN3YHY2ZhmKccm7ecNq1Kv9FUctPe6+97HXb2rL0rB0gO1AyxWJKU +edzls63/0n+AnQqwnPQdgL9N5vIw/0avLo3U8F+kI5hbYfG7fvw3zHdJIMiLTRsn +qKvkF2TMBxr06nrlJsQqG90k9xS3iX7DqssDq3niVgAwP2NbS2wDXk7/6R40LNx9 +RzFHDyHplF/3ySjctkx7kkAPdamGr8NNs7kQkVZGKmD25V7i5ggoGx9lo3AiBbmT +9Keac43vhlC4Bj9zW2O6Ih9TP9sDhp6iA4NtdNnK9tfn59Av6J4pB6EhMzaLtu4J +jqc5b3+Wvq1xv0Sm2Y+JjuawT7jgrT4vnSEqqkFTTV6igzctatOCxz4ejl3Q2sD0 +OjlArZWX9kY2yyuFt6LhlM3We0IDUQjEf0JtA9EFixbm+ieHbPEFHFiD0w9uN/VI +cYzxnubGgvv2wN1N+YHNRFFOWyT+Ty7Hp0Kz3dh8g+DY4vxvsfG6XfnvPT5StSKd +ACEfl8HoSET/qJZIkuIhErzzUNNK4+4QzQav7auZUQUrdK6P+rryE3lZauZ3rV+9 +ZXWT3PG1qHuWNNriTrC6n4tpa8m5UkZMdeoK2pS3y3SLDCJJV7Q3WHCZEVddIPdV +Ew== +-----END CERTIFICATE REQUEST----- diff --git a/internal/backend/remote-state/http/testdata/certs/server.key b/internal/backend/remote-state/http/testdata/certs/server.key new file mode 100644 index 0000000000..881229b098 --- /dev/null +++ b/internal/backend/remote-state/http/testdata/certs/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDBTTBoca0tn2EA +xbQLXw1diEH5+YltZUFz5gH3aSDfH+uKame4iFsPybsUstmUqy3D9ZTzjNcauAhB +75+RgePn4D0/qePPjdsFz11jxacAAMkg/mPLPtrkEAiRzvSXXoYN1Gq6uNdGricy +KGtSKzqQh158W2ZfLKUKvgGlQ8RD3RFsFanQS6aiNwPgFK1SeFFt4wTJbtpvKNpJ +Se/XDDmkyMIN6/pVRo56v2PvsERAmUQJ+blyhOy7Egt0/uF7JUklzLIi5eKjv2Jp +cVwH83OAovKl1vy2mfiCZSjCwQeBahnWgAaAaJir7uOVNDNXI4qM/KySKN7Nfyo+ +9sRuNBjMb5b0N8s70YIAyzoylJ0UE8x+a7XyMJlPvpARGXuNSDwR9rRytpGZIeMP +cO1YQh9+V5k7il6/70thNYW5/Cb0PsHU5XOSlmOsOLcr1JaD2YTJJflVyhrPwMSA +phRuUCFuyQinnHi59Rk2rNuTj/R3dFrSqtcfnvjbc+1KbyDFpnyf40W/EPmS4mF3 +UPRD4oRsvXpy85E25uh5Q+R4MMddg3KHsZ+SiObUBa33kd9rG6peJz0cvkJIhBzb +JcXPzN2EMgow2C5MYKjmNXclYWIHypkFSo6OxFHhIQdeh3Ga7pZqJaOVUA8wm2ol +IjRQgQFJjRRc6KU2w95lJlFvHXlW0QIDAQABAoICAAnye228f9FrtLWx9tBo/UqV +YvPGrBRFlCcvLGW7crYYsenHDPxZg/odgvOPOpQkdO/zGM2prz4QP1iJSLhXq084 +4l3+05rQLXewkpk6SBw/bho1DRSd8OywiIhcUojhk9ttVWqzbVyVRK4Xl2I8mEBs +vud+WpfGN94EJhiHkrd9TlK2EK2H3xTU6O2kksC+MU6K0qm8+x+iRg1kcSOrXOIG +dLn7rT+rKFTXuYBRnUmHuZEb2Tez8Gy2AoHsRdUs94Uq8fXKx61ugVV0wGwmUojJ +mdv/4rRQ2xF2vDC9dzHpMFgx8WO1PjoGyo5Yh9XRneUgcY757HE9vIJN95DGPIpd +vCYaGrGA/JipOmTrmMoFurhdwdiyzAzUsXV5AKKo+PNEsSz+36y/xa2z7PlvnBR2 +rpKw/ocsRoaKiI9pG7b9ty0QyiY/teVTpt0sQDIvpZwx60wYhkziFl8sn6CGQWQm +a1bFzb+5ZrEMj7gCeffOJmQSvpn2fGzlyp3RrkyaRGK4YhWU9SJsPUAnxRF5yoOm +EzwYFYC0AScPdywS3nA4IWIeKnuydGH+6M/Cqk9qkiGrflKFpCn2eBvFWMTuoUYd +/jyE2t4th/T1qsqKbJqKiRAz4dQlrqWdN6SnBk8MRhDbqtbbSREIdU1z58qD3kuf +0thbm9SrDRV4UgYiUckLAoIBAQDqt3NLq5OgOsEOuoYYqQYWDWNiTusrvsutood+ ++AXPcxZKR8O3g/gUGuhyKE3a9DMYnFoLCaTqX1iDqg/KUemoqc2+A9pZ+QAXRXnE +R/4PFh1Sgyhyrwz30svUVs9FYpiP65ZiY6LhDmSL6bl1u4DYJUhgXiHkF6q+KryN +M1uLpSmUOTOwJf6tltYgMPMegDXQE2VZ7VnQMN1m2rhDRmycM14CyJSSWZAyCzbJ +ylDeKWs4wxATLrWIGUPLzqua4/uILsUeAzvyOCrCgJHSEDdITGdQJClF17bZH9xG +H6pIA0VPoWq480lE+gw9Mwu1m4QjOOM0RHF5nm6YKFJLIdCjAoIBAQDS1FuEtuVI +6oQa2Sh7EILZq/gyVrXmeLLYUQChYBFZxK0PVkbFT+ztkHnD2gBz4exXUEaGpBNA +6Yr8iz6VCNdQ0KcrvzFINZwcRJTeSxLXArQ2LkDJmDZJoIC1cSurrU8ot94EHC94 +qjGQW4K3qFZIiyXHQJNgSrYaHADmDi9sQQNmyP0pSIY/q9Gn/2DLADItIt/0iFz0 +kc07kF6l/1JADSiUHMzHhUxh04LXo2LRQVHYWaK0DrVI50wXivOCARFkQrFdrszX +ymFf7d6AskIiAIBNYXmb3of2NSwzWx6RnZI9YVpw2277xzVh2vDxI2Pc0ordAzFk +YY0DLGFNeI37AoIBAHnHkt949xBUS6RrrHWRBOJeMelozuWUibLeN/TtlH4s1SzX +DTnjE8zCpUXNmY930ib7wFAnwdQEgjVV//lWBKiI6YGkGB9EbQKl/maTf8KuE6qi ++FKAdncCfNT/8WyrmkJZ1l3YGkMwp4RcUOg/z7rVpTaywFzK1sDyBYAxXFcY63jH +MQU8wWWpdBGhtBJoLQN3fMdquYWmRMk/xAjLukBU+nrxPPyt0X3Viaiq+sg5rzL1 +Khr5yiACE8Xjxe+ISBJBSe6neOvUroLaGE5oMXamhZf0GyHsqScAO9Z6SWwxnj2R +n4C0YZiTL9R07qdcN/PaaS/OLx4N0I3Lpd7rfYcCggEAdgBHrPNVR8eC4ygSYTbv +lfeLtlkT/Ignya0kxi3n6C+NkVz/xWYjvR+1F2qIAFQ+HOygXLGu2REeKpWhFHdb +VC9EsdaUNc9Trfqwu+6W/+LSjNS8jFj2YaVFBMjv4WniOW8YA4LnCwlvLlYZxsOg +b3/6SBibpDSM0fZEhn8ACf4lcj0ifR3Ljg2UDgyA134nl13CrbI5HOYSUblPUGek +WJdE1Al+kFnKU6K3xAv9vhNqRMZ+q3rj+ocC7tZlzqjcXBp7/Wxd2JW8hJ21gKDF +JRTUuvrIvvYBcUt3jtL8PBJOjK5VmX8oEiIAfeG2I7FkLm9lK6ii14VGELWhTGQi +SwKCAQEAkZTSBq7tJ5CwiwKW3ubTEyDWugO3IplXAn8NMzGwSG5NF6qLqTpRAlDk +IJ7JEuMY0KKNDj4qCy+EEyR7a3+P1QcYquBAAFcwsaq+49ix+MxApVUjj2RT7yzt +IT3J1NP782AAVMUVK2n/tBvhRnDPmofhwCXKxP8t1SGbUCX+2I5IcAL3aQgSrDsF +uyUPCSL08f6SJWDQa7k9RFg2vnJgJjPJnvf+xuI6jJrbOJUcmUfBmTcYzjWKZvRB +RctFOLbbrfsY3D2jgW/CUw/jbrwUokwm4VatzMCgHlZi6WJIGJftDP4b1MJACe02 ++AXVqLYxuaMTIdm5Ahyl1sCNrOl8nQ== +-----END PRIVATE KEY----- diff --git a/internal/backend/remote-state/http/testdata/gencerts.sh b/internal/backend/remote-state/http/testdata/gencerts.sh new file mode 100755 index 0000000000..ebb987d8de --- /dev/null +++ b/internal/backend/remote-state/http/testdata/gencerts.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# +# Generates certs required for mTLS testing: +# - ca.key and ca.cert.pem are self-signed, used as the source of truth for client and server to verify each other. +# - client.key and client.crt are the client's key and cert (signed by the ca key and cert) +# - server.key and server.crt are the server's key and cert (signed by the ca key and cert) + +set -ex + +# I was doing this on M1 mac and needed newer openssl to add the SAN IP; please export OPENSSL when invoking as needed +OPENSSL="${OPENSSL:-openssl}" + +# Nuke and recreate the certs dir +rm -rf certs +mkdir certs +cd certs || exit 1 + +# CA +"$OPENSSL" genrsa -out ca.key 4096 +"$OPENSSL" req -new -x509 -days 365000 -key ca.key -out ca.cert.pem + +# Server +"$OPENSSL" genrsa -out server.key 4096 +"$OPENSSL" req -new -key server.key -out server.csr -addext 'subjectAltName = IP:127.0.0.1' +"$OPENSSL" x509 -req -days 365000 -in server.csr -CA ca.cert.pem -CAkey ca.key -CAcreateserial -out server.crt -copy_extensions copy + +# Client +"$OPENSSL" genrsa -out client.key 4096 +"$OPENSSL" req -new -key client.key -out client.csr -addext 'subjectAltName = IP:127.0.0.1' +"$OPENSSL" x509 -req -days 365000 -in client.csr -CA ca.cert.pem -CAkey ca.key -CAcreateserial -out client.crt -copy_extensions copy diff --git a/internal/backend/remote-state/inmem/backend.go b/internal/backend/remote-state/inmem/backend.go index 7f8f56ef20..1a5d8cbc17 100644 --- a/internal/backend/remote-state/inmem/backend.go +++ b/internal/backend/remote-state/inmem/backend.go @@ -1,18 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package inmem import ( - "context" "errors" "fmt" "sort" "sync" "time" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" statespkg "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" ) // we keep the states and locks in package-level variables, so that they can be @@ -42,26 +48,26 @@ func Reset() { // New creates a new backend for Inmem remote state. func New() backend.Backend { - // Set the schema - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "lock_id": &schema.Schema{ - Type: schema.TypeString, - Optional: true, - Description: "initializes the state in a locked configuration", + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "lock_id": { + Type: cty.String, + Optional: true, + Description: "initializes the state in a locked configuration", + }, + }, }, }, } - backend := &Backend{Backend: s} - backend.Backend.ConfigureFunc = backend.configure - return backend } type Backend struct { - *schema.Backend + backendbase.Base } -func (b *Backend) configure(ctx context.Context) error { +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { states.Lock() defer states.Unlock() @@ -74,10 +80,9 @@ func (b *Backend) configure(ctx context.Context) error { } // set the default client lock info per the test config - data := schema.FromContextBackendConfig(ctx) - if v, ok := data.GetOk("lock_id"); ok && v.(string) != "" { + if v := configVal.GetAttr("lock_id"); !v.IsNull() { info := statemgr.NewLockInfo() - info.ID = v.(string) + info.ID = v.AsString() info.Operation = "test" info.Info = "test config" @@ -101,7 +106,7 @@ func (b *Backend) Workspaces() ([]string, error) { return workspaces, nil } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { states.Lock() defer states.Unlock() @@ -141,7 +146,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { if err := s.WriteState(statespkg.NewState()); err != nil { return nil, err } - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { return nil, err } } diff --git a/internal/backend/remote-state/inmem/backend_test.go b/internal/backend/remote-state/inmem/backend_test.go index 395199890a..34cc852a2b 100644 --- a/internal/backend/remote-state/inmem/backend_test.go +++ b/internal/backend/remote-state/inmem/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package inmem import ( @@ -82,7 +85,7 @@ func TestRemoteState(t *testing.T) { t.Fatal(err) } - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatal(err) } diff --git a/internal/backend/remote-state/inmem/client.go b/internal/backend/remote-state/inmem/client.go index 5f404567fd..b75bc521bf 100644 --- a/internal/backend/remote-state/inmem/client.go +++ b/internal/backend/remote-state/inmem/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package inmem import ( diff --git a/internal/backend/remote-state/inmem/client_test.go b/internal/backend/remote-state/inmem/client_test.go index a9fb56b6e1..5133da4a99 100644 --- a/internal/backend/remote-state/inmem/client_test.go +++ b/internal/backend/remote-state/inmem/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package inmem import ( diff --git a/internal/backend/remote-state/kubernetes/backend.go b/internal/backend/remote-state/kubernetes/backend.go index 907cda9e24..cd2f61ee44 100644 --- a/internal/backend/remote-state/kubernetes/backend.go +++ b/internal/backend/remote-state/kubernetes/backend.go @@ -1,17 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package kubernetes import ( "bytes" - "context" "fmt" "log" "os" "path/filepath" + "strconv" + "strings" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - "github.com/hashicorp/terraform/version" "github.com/mitchellh/go-homedir" + "github.com/zclconf/go-cty/cty" k8sSchema "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -19,6 +21,12 @@ import ( restclient "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" ) // Modified from github.com/terraform-providers/terraform-provider-kubernetes @@ -41,152 +49,191 @@ var ( // New creates a new backend for kubernetes remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "secret_suffix": { - Type: schema.TypeString, - Required: true, - Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`.", - }, - "labels": { - Type: schema.TypeMap, - Optional: true, - Description: "Map of additional labels to be applied to the secret.", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "namespace": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_NAMESPACE", "default"), - Description: "Namespace to store the secret in.", - }, - "in_cluster_config": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_IN_CLUSTER_CONFIG", false), - Description: "Used to authenticate to the cluster from inside a pod.", - }, - "load_config_file": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_LOAD_CONFIG_FILE", true), - Description: "Load local kubeconfig.", - }, - "host": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_HOST", ""), - Description: "The hostname (in form of URI) of Kubernetes master.", - }, - "username": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_USER", ""), - Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", - }, - "password": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_PASSWORD", ""), - Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", - }, - "insecure": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_INSECURE", false), - Description: "Whether server should be accessed without verifying the TLS certificate.", - }, - "client_certificate": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_CERT_DATA", ""), - Description: "PEM-encoded client certificate for TLS authentication.", - }, - "client_key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLIENT_KEY_DATA", ""), - Description: "PEM-encoded client certificate key for TLS authentication.", - }, - "cluster_ca_certificate": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CLUSTER_CA_CERT_DATA", ""), - Description: "PEM-encoded root certificates bundle for TLS authentication.", - }, - "config_paths": { - Type: schema.TypeList, - Elem: &schema.Schema{Type: schema.TypeString}, - Optional: true, - Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", - }, - "config_path": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CONFIG_PATH", ""), - Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", - }, - "config_context": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX", ""), - }, - "config_context_auth_info": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_AUTH_INFO", ""), - Description: "", - }, - "config_context_cluster": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_CTX_CLUSTER", ""), - Description: "", - }, - "token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("KUBE_TOKEN", ""), - Description: "Token to authentifcate a service account.", - }, - "exec": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "api_version": { - Type: schema.TypeString, - Required: true, - }, - "command": { - Type: schema.TypeString, - Required: true, - }, - "env": { - Type: schema.TypeMap, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "args": { - Type: schema.TypeList, - Optional: true, - Elem: &schema.Schema{Type: schema.TypeString}, + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "secret_suffix": { + Type: cty.String, + Required: true, + Description: "Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`. Note that the backend may append its own numeric index to the secret name when chunking large state files into multiple secrets. In this case, there will be multiple secrets named in the format: `tfstate-{workspace}-{secret_suffix}-{index}`.", + }, + "labels": { + Type: cty.Map(cty.String), + Optional: true, + Description: "Map of additional labels to be applied to the secret.", + }, + "namespace": { + Type: cty.String, + Optional: true, + Description: "Namespace to store the secret in.", + }, + "in_cluster_config": { + Type: cty.Bool, + Optional: true, + Description: "Used to authenticate to the cluster from inside a pod.", + }, + "load_config_file": { + Type: cty.Bool, + Optional: true, + Description: "Load local kubeconfig.", + }, + "host": { + Type: cty.String, + Optional: true, + Description: "The hostname (in form of URI) of Kubernetes master.", + }, + "username": { + Type: cty.String, + Optional: true, + Description: "The username to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + }, + "password": { + Type: cty.String, + Optional: true, + Description: "The password to use for HTTP basic authentication when accessing the Kubernetes master endpoint.", + }, + "insecure": { + Type: cty.Bool, + Optional: true, + Description: "Whether server should be accessed without verifying the TLS certificate.", + }, + "client_certificate": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded client certificate for TLS authentication.", + }, + "client_key": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded client certificate key for TLS authentication.", + }, + "cluster_ca_certificate": { + Type: cty.String, + Optional: true, + Description: "PEM-encoded root certificates bundle for TLS authentication.", + }, + "config_paths": { + Type: cty.List(cty.String), + Optional: true, + Description: "A list of paths to kube config files. Can be set with KUBE_CONFIG_PATHS environment variable.", + }, + "config_path": { + Type: cty.String, + Optional: true, + Description: "Path to the kube config file. Can be set with KUBE_CONFIG_PATH environment variable.", + }, + "config_context": { + Type: cty.String, + Optional: true, + }, + "config_context_auth_info": { + Type: cty.String, + Optional: true, + Description: "", + }, + "config_context_cluster": { + Type: cty.String, + Optional: true, + Description: "", + }, + "token": { + Type: cty.String, + Optional: true, + Description: "Token to authentifcate a service account.", + }, + "proxy_url": { + Type: cty.String, + Optional: true, + Description: "URL to the proxy to be used for all API requests", + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "exec": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "api_version": { + Type: cty.String, + Required: true, + }, + "command": { + Type: cty.String, + Required: true, + }, + "env": { + Type: cty.Map(cty.String), + Optional: true, + }, + "args": { + Type: cty.List(cty.String), + Optional: true, + }, + }, }, }, }, - Description: "Use a credential plugin to authenticate.", + }, + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "namespace": { + EnvVars: []string{"KUBE_NAMESPACE"}, + Fallback: "default", + }, + "in_cluster_config": { + EnvVars: []string{"KUBE_IN_CLUSTER_CONFIG"}, + Fallback: "false", + }, + "load_config_file": { + EnvVars: []string{"KUBE_LOAD_CONFIG_FILE"}, + Fallback: "true", + }, + "host": { + EnvVars: []string{"KUBE_HOST"}, + }, + "username": { + EnvVars: []string{"KUBE_USER"}, + }, + "password": { + EnvVars: []string{"KUBE_PASSWORD"}, + }, + "insecure": { + EnvVars: []string{"KUBE_INSECURE"}, + Fallback: "false", + }, + "client_certificate": { + EnvVars: []string{"KUBE_CLIENT_CERT_DATA"}, + }, + "client_key": { + EnvVars: []string{"KUBE_CLIENT_KEY_DATA"}, + }, + "cluster_ca_certificate": { + EnvVars: []string{"KUBE_CLUSTER_CA_CERT_DATA"}, + }, + "config_path": { + EnvVars: []string{"KUBE_CONFIG_PATH"}, + }, + "config_context": { + EnvVars: []string{"KUBE_CTX"}, + }, + "config_context_auth_info": { + EnvVars: []string{"KUBE_CTX_AUTH_INFO"}, + }, + "config_context_cluster": { + EnvVars: []string{"KUBE_CTX_CLUSTER"}, + }, + "token": { + EnvVars: []string{"KUBE_TOKEN"}, + }, + "proxy_url": { + EnvVars: []string{"KUBE_PROXY_URL"}, + }, }, }, } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result } type Backend struct { - *schema.Backend + backendbase.Base // The fields below are set from configure kubernetesSecretClient dynamic.ResourceInterface @@ -225,68 +272,79 @@ func (b Backend) KubernetesLeaseClient() (coordinationv1.LeaseInterface, error) return b.kubernetesLeaseClient, nil } -func (b *Backend) configure(ctx context.Context) error { +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { if b.config != nil { return nil } - // Grab the resource data - data := schema.FromContextBackendConfig(ctx) + data := backendbase.NewSDKLikeData(configVal) cfg, err := getInitialConfig(data) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } // Overriding with static configuration cfg.UserAgent = fmt.Sprintf("HashiCorp/1.0 Terraform/%s", version.String()) - if v, ok := data.GetOk("host"); ok { - cfg.Host = v.(string) + if v := data.String("host"); v != "" { + cfg.Host = v } - if v, ok := data.GetOk("username"); ok { - cfg.Username = v.(string) + if v := data.String("username"); v != "" { + cfg.Username = v } - if v, ok := data.GetOk("password"); ok { - cfg.Password = v.(string) + if v := data.String("password"); v != "" { + cfg.Password = v } - if v, ok := data.GetOk("insecure"); ok { - cfg.Insecure = v.(bool) + cfg.Insecure = data.Bool("insecure") + if v := data.String("cluster_ca_certificate"); v != "" { + cfg.CAData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("cluster_ca_certificate"); ok { - cfg.CAData = bytes.NewBufferString(v.(string)).Bytes() + if v := data.String("client_certificate"); v != "" { + cfg.CertData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("client_certificate"); ok { - cfg.CertData = bytes.NewBufferString(v.(string)).Bytes() + if v := data.String("client_key"); v != "" { + cfg.KeyData = bytes.NewBufferString(v).Bytes() } - if v, ok := data.GetOk("client_key"); ok { - cfg.KeyData = bytes.NewBufferString(v.(string)).Bytes() - } - if v, ok := data.GetOk("token"); ok { - cfg.BearerToken = v.(string) + if v := data.String("token"); v != "" { + cfg.BearerToken = v } - if v, ok := data.GetOk("labels"); ok { + if v := data.GetAttr("labels", cty.Map(cty.String)); !v.IsNull() { labels := map[string]string{} - for k, vv := range v.(map[string]interface{}) { - labels[k] = vv.(string) + for it := v.ElementIterator(); it.Next(); { + kV, vV := it.Element() + if vV.IsNull() { + vV = cty.StringVal("") + } + labels[kV.AsString()] = vV.AsString() } b.labels = labels } - ns := data.Get("namespace").(string) + ns := data.String("namespace") b.namespace = ns - b.nameSuffix = data.Get("secret_suffix").(string) + + b.nameSuffix = data.String("secret_suffix") + if hasNumericSuffix(b.nameSuffix, "-") { + // If the last segment is a number, it's considered invalid. + // The backend automatically appends its own numeric suffix when chunking large state files into multiple secrets. + // Allowing a user-defined numeric suffix could cause conflicts with this mechanism. + return backendbase.ErrorAsDiagnostics( + fmt.Errorf("secret_suffix must not end with '-', got %q", b.nameSuffix), + ) + } + b.config = cfg return nil } -func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { +func getInitialConfig(data backendbase.SDKLikeData) (*restclient.Config, error) { var cfg *restclient.Config var err error - inCluster := data.Get("in_cluster_config").(bool) + inCluster := data.Bool("in_cluster_config") if inCluster { cfg, err = restclient.InClusterConfig() if err != nil { @@ -305,16 +363,14 @@ func getInitialConfig(data *schema.ResourceData) (*restclient.Config, error) { return cfg, err } -func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { +func tryLoadingConfigFile(d backendbase.SDKLikeData) (*restclient.Config, error) { loader := &clientcmd.ClientConfigLoadingRules{} configPaths := []string{} - if v, ok := d.Get("config_path").(string); ok && v != "" { + if v := d.String("config_path"); v != "" { configPaths = []string{v} - } else if v, ok := d.Get("config_paths").([]interface{}); ok && len(v) > 0 { - for _, p := range v { - configPaths = append(configPaths, p.(string)) - } + } else if v := d.GetAttr("config_paths", cty.List(cty.String)); !v.IsNull() { + configPaths = append(configPaths, decodeListOfString(v)...) } else if v := os.Getenv("KUBE_CONFIG_PATHS"); v != "" { configPaths = filepath.SplitList(v) } @@ -339,44 +395,58 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { overrides := &clientcmd.ConfigOverrides{} ctxSuffix := "; default context" - ctx, ctxOk := d.GetOk("config_context") - authInfo, authInfoOk := d.GetOk("config_context_auth_info") - cluster, clusterOk := d.GetOk("config_context_cluster") - if ctxOk || authInfoOk || clusterOk { + configCtx := d.String("config_context") + authInfo := d.String("config_context_auth_info") + cluster := d.String("config_context_cluster") + if configCtx != "" || authInfo != "" || cluster != "" { ctxSuffix = "; overriden context" - if ctxOk { - overrides.CurrentContext = ctx.(string) + if configCtx != "" { + overrides.CurrentContext = configCtx ctxSuffix += fmt.Sprintf("; config ctx: %s", overrides.CurrentContext) log.Printf("[DEBUG] Using custom current context: %q", overrides.CurrentContext) } overrides.Context = clientcmdapi.Context{} - if authInfoOk { - overrides.Context.AuthInfo = authInfo.(string) + if authInfo != "" { + overrides.Context.AuthInfo = authInfo ctxSuffix += fmt.Sprintf("; auth_info: %s", overrides.Context.AuthInfo) } - if clusterOk { - overrides.Context.Cluster = cluster.(string) + if cluster != "" { + overrides.Context.Cluster = cluster ctxSuffix += fmt.Sprintf("; cluster: %s", overrides.Context.Cluster) } log.Printf("[DEBUG] Using overidden context: %#v", overrides.Context) } - if v, ok := d.GetOk("exec"); ok { - exec := &clientcmdapi.ExecConfig{} - if spec, ok := v.([]interface{})[0].(map[string]interface{}); ok { - exec.APIVersion = spec["api_version"].(string) - exec.Command = spec["command"].(string) - exec.Args = expandStringSlice(spec["args"].([]interface{})) - for kk, vv := range spec["env"].(map[string]interface{}) { - exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{Name: kk, Value: vv.(string)}) + // exec is a nested block with nesting mode NestingSingle, so GetAttr + // will return a value of an object type that will be null if the block + // isn't present at all. + if v := d.GetAttr("exec", cty.DynamicPseudoType); !v.IsNull() { + spec := backendbase.NewSDKLikeData(v) + exec := &clientcmdapi.ExecConfig{ + APIVersion: spec.String("api_version"), + Command: spec.String("command"), + Args: decodeListOfString(spec.GetAttr("args", cty.List(cty.String))), + } + if envMap := spec.GetAttr("env", cty.Map(cty.String)); !envMap.IsNull() { + for it := envMap.ElementIterator(); it.Next(); { + kV, vV := it.Element() + if vV.IsNull() { + vV = cty.StringVal("") + } + exec.Env = append(exec.Env, clientcmdapi.ExecEnvVar{ + Name: kV.AsString(), + Value: vV.AsString(), + }) } - } else { - return nil, fmt.Errorf("Failed to parse exec") } overrides.AuthInfo.Exec = exec } + if v := d.String("proxy_url"); v != "" { + overrides.ClusterDefaults.ProxyURL = v + } + cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loader, overrides) cfg, err := cc.ClientConfig() if err != nil { @@ -391,15 +461,31 @@ func tryLoadingConfigFile(d *schema.ResourceData) (*restclient.Config, error) { return cfg, nil } -func expandStringSlice(s []interface{}) []string { - result := make([]string, len(s), len(s)) - for k, v := range s { - // Handle the Terraform parser bug which turns empty strings in lists to nil. - if v == nil { - result[k] = "" +func decodeListOfString(v cty.Value) []string { + if v.IsNull() { + return nil + } + ret := make([]string, 0, v.LengthInt()) + for it := v.ElementIterator(); it.Next(); { + _, vV := it.Element() + if vV.IsNull() { + ret = append(ret, "") } else { - result[k] = v.(string) + ret = append(ret, vV.AsString()) } } - return result + return ret +} + +func hasNumericSuffix(value, substr string) bool { + // Find the last occurrence of '-' and get the part after it + if idx := strings.LastIndex(value, substr); idx != -1 { + lastPart := value[idx+1:] + // Try to convert the last part to an integer. + if _, err := strconv.Atoi(lastPart); err == nil { + return true + } + } + // Return false if no '-' is found or if the last part isn't numeric + return false } diff --git a/internal/backend/remote-state/kubernetes/backend_state.go b/internal/backend/remote-state/kubernetes/backend_state.go index 56aa089ff8..0d932b9f12 100644 --- a/internal/backend/remote-state/kubernetes/backend_state.go +++ b/internal/backend/remote-state/kubernetes/backend_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package kubernetes import ( @@ -6,11 +9,12 @@ import ( "fmt" "sort" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Workspaces returns a list of names for the workspaces found in k8s. The default @@ -60,7 +64,7 @@ func (b *Backend) Workspaces() ([]string, error) { return states, nil } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -96,7 +100,8 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { return nil, err } - secretName, err := c.createSecretName() + // get base secret name + secretName, err := c.createSecretName(0) if err != nil { return nil, err } @@ -123,7 +128,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { if err := stateMgr.WriteState(states.NewState()); err != nil { return nil, unlock(err) } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { return nil, unlock(err) } diff --git a/internal/backend/remote-state/kubernetes/backend_test.go b/internal/backend/remote-state/kubernetes/backend_test.go index e24689f0fb..699dc16098 100644 --- a/internal/backend/remote-state/kubernetes/backend_test.go +++ b/internal/backend/remote-state/kubernetes/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package kubernetes import ( @@ -194,3 +197,24 @@ func cleanupK8sResources(t *testing.T) { t.Fatal(errs) } } + +func Test_hasNumericSuffix(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"my-secret-123", true}, + {"my-secret-abcd", false}, + {"nodashhere", false}, + {"another-secret-45abc", false}, + {"some-thing-1", true}, + {"some-thing-1-23", true}, + } + + for _, tt := range tests { + isNumeric := hasNumericSuffix(tt.input, "-") + if isNumeric != tt.expected { + t.Errorf("expected %v, got %v for input %s", tt.expected, isNumeric, tt.input) + } + } +} diff --git a/internal/backend/remote-state/kubernetes/client.go b/internal/backend/remote-state/kubernetes/client.go index 12447c36e0..4367f86bad 100644 --- a/internal/backend/remote-state/kubernetes/client.go +++ b/internal/backend/remote-state/kubernetes/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package kubernetes import ( @@ -9,10 +12,9 @@ import ( "encoding/json" "errors" "fmt" + "strconv" "strings" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -21,6 +23,11 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" // Import to initialize client auth plugins. "k8s.io/utils/pointer" + "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states/statemgr" + + "maps" + coordinationv1 "k8s.io/api/coordination/v1" coordinationclientv1 "k8s.io/client-go/kubernetes/typed/coordination/v1" ) @@ -30,7 +37,10 @@ const ( tfstateSecretSuffixKey = "tfstateSecretSuffix" tfstateWorkspaceKey = "tfstateWorkspace" tfstateLockInfoAnnotation = "app.terraform.io/lock-info" - managedByKey = "app.kubernetes.io/managed-by" + + managedByKey = "app.kubernetes.io/managed-by" + + defaultChunkSize = 1048576 ) type RemoteClient struct { @@ -43,34 +53,32 @@ type RemoteClient struct { } func (c *RemoteClient) Get() (payload *remote.Payload, err error) { - secretName, err := c.createSecretName() + secretList, err := c.getSecrets() if err != nil { return nil, err } - secret, err := c.kubernetesSecretClient.Get(context.Background(), secretName, metav1.GetOptions{}) - if err != nil { - if k8serrors.IsNotFound(err) { - return nil, nil - } - return nil, err - } - secretData := getSecretData(secret) - stateRaw, ok := secretData[tfstateKey] - if !ok { - // The secret exists but there is no state in it + if len(secretList) == 0 { return nil, nil } - stateRawString := stateRaw.(string) + var data []string + for _, secret := range secretList { + secretData := getSecretData(&secret) + stateRaw, ok := secretData[tfstateKey] + if !ok { + // The secret exists but there is no state in it + return nil, nil + } + data = append(data, stateRaw.(string)) + } - state, err := uncompressState(stateRawString) + state, err := uncompressState(data) if err != nil { return nil, err } md5 := md5.Sum(state) - p := &remote.Payload{ Data: state, MD5: md5[:], @@ -78,58 +86,135 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) { return p, nil } +func (c *RemoteClient) getSecrets() ([]unstructured.Unstructured, error) { + ls := metav1.SetAsLabelSelector(c.getLabels()) + res, err := c.kubernetesSecretClient.List(context.Background(), + metav1.ListOptions{ + LabelSelector: metav1.FormatLabelSelector(ls), + }, + ) + if err != nil { + return []unstructured.Unstructured{}, err + } + + // NOTE we need to sort the list as the k8s API will return + // the list sorted by name which will corrupt the state when + // the number of secrets is greater than 10 + items := make([]unstructured.Unstructured, len(res.Items)) + for _, item := range res.Items { + name := item.GetName() + nameParts := strings.Split(name, "-") + // Because large Terraform state files are split into multiple secrets, + // we parse the index from the secret name. + index, err := strconv.Atoi(nameParts[len(nameParts)-1]) + if err != nil { + index = 0 + } + items[index] = item + } + return items, nil +} + func (c *RemoteClient) Put(data []byte) error { ctx := context.Background() - secretName, err := c.createSecretName() - if err != nil { - return err - } payload, err := compressState(data) if err != nil { return err } - secret, err := c.getSecret(secretName) + chunks := chunkPayload(payload, defaultChunkSize) + existingSecrets, err := c.getSecrets() if err != nil { - if !k8serrors.IsNotFound(err) { + return err + } + + for idx, data := range chunks { + secretName, err := c.createSecretName(idx) + if err != nil { return err } - secret = &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": metav1.ObjectMeta{ - Name: secretName, - Namespace: c.namespace, - Labels: c.getLabels(), - Annotations: map[string]string{"encoding": "gzip"}, + secret, err := c.getSecret(secretName) + if err != nil { + if !k8serrors.IsNotFound(err) { + return err + } + secret = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": metav1.ObjectMeta{ + Name: secretName, + Namespace: c.namespace, + Labels: c.getLabels(), + Annotations: map[string]string{"encoding": "gzip"}, + }, }, - }, + } + secret, err = c.kubernetesSecretClient.Create(ctx, secret, metav1.CreateOptions{}) + if err != nil { + return err + } } - secret, err = c.kubernetesSecretClient.Create(ctx, secret, metav1.CreateOptions{}) + setState(secret, data) + _, err = c.kubernetesSecretClient.Update(ctx, secret, metav1.UpdateOptions{}) if err != nil { return err } } - setState(secret, payload) - _, err = c.kubernetesSecretClient.Update(ctx, secret, metav1.UpdateOptions{}) - return err + // remove old secrets + existingSecretCount := len(existingSecrets) + newSecretCount := len(chunks) + if existingSecretCount == newSecretCount { + return nil + } + for i := newSecretCount; i < existingSecretCount; i++ { + secretName, err := c.createSecretName(i) + if err != nil { + return err + } + err = c.deleteSecret(secretName) + if err != nil { + return err + } + } + return nil +} + +// chunkPayload splits the state payload into byte arrays of the given size +func chunkPayload(buf []byte, size int) [][]byte { + chunks := make([][]byte, 0, len(buf)/size+1) + for len(buf) >= size { + var chunk []byte + chunk, buf = buf[:size], buf[size:] + chunks = append(chunks, chunk) + } + if len(buf) > 0 { + chunks = append(chunks, buf) + } + return chunks } // Delete the state secret func (c *RemoteClient) Delete() error { - secretName, err := c.createSecretName() + secretList, err := c.getSecrets() if err != nil { return err } - err = c.deleteSecret(secretName) - if err != nil { - if !k8serrors.IsNotFound(err) { + for i, _ := range secretList { + secretName, err := c.createSecretName(i) + if err != nil { return err } + + err = c.deleteSecret(secretName) + if err != nil { + if !k8serrors.IsNotFound(err) { + return err + } + } } leaseName, err := c.createLeaseName() @@ -268,12 +353,7 @@ func (c *RemoteClient) getLabels() map[string]string { tfstateWorkspaceKey: c.workspace, managedByKey: "terraform", } - - if len(c.labels) != 0 { - for k, v := range c.labels { - l[k] = v - } - } + maps.Copy(l, c.labels) return l } @@ -320,9 +400,13 @@ func (c *RemoteClient) deleteLease(name string) error { return c.kubernetesLeaseClient.Delete(context.Background(), name, delOps) } -func (c *RemoteClient) createSecretName() (string, error) { +func (c *RemoteClient) createSecretName(idx int) (string, error) { secretName := strings.Join([]string{tfstateKey, c.workspace, c.nameSuffix}, "-") + if idx > 0 { + secretName = fmt.Sprintf("%s-part-%d", secretName, idx) + } + errs := validation.IsDNS1123Subdomain(secretName) if len(errs) > 0 { k8sInfo := ` @@ -336,7 +420,7 @@ The workspace name and key must adhere to Kubernetes naming conventions.` } func (c *RemoteClient) createLeaseName() (string, error) { - n, err := c.createSecretName() + n, err := c.createSecretName(0) if err != nil { return "", err } @@ -355,14 +439,18 @@ func compressState(data []byte) ([]byte, error) { return b.Bytes(), nil } -func uncompressState(data string) ([]byte, error) { - decode, err := base64.StdEncoding.DecodeString(data) - if err != nil { - return nil, err +func uncompressState(data []string) ([]byte, error) { + var rawData []byte + for _, chunk := range data { + decode, err := base64.StdEncoding.DecodeString(chunk) + if err != nil { + return nil, err + } + rawData = append(rawData, decode...) } b := new(bytes.Buffer) - gz, err := gzip.NewReader(bytes.NewReader(decode)) + gz, err := gzip.NewReader(bytes.NewReader(rawData)) if err != nil { return nil, err } diff --git a/internal/backend/remote-state/kubernetes/client_test.go b/internal/backend/remote-state/kubernetes/client_test.go index 08e615423e..01ade15aa6 100644 --- a/internal/backend/remote-state/kubernetes/client_test.go +++ b/internal/backend/remote-state/kubernetes/client_test.go @@ -1,9 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package kubernetes import ( + "math/rand" "testing" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" ) @@ -54,6 +60,79 @@ func TestRemoteClientLocks(t *testing.T) { remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client) } +func TestLargeState(t *testing.T) { + testACC(t) + defer cleanupK8sResources(t) + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "secret_suffix": secretSuffix, + })) + + s, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + // generate a very large state + largeState := generateLargeState(20) + err = s.WriteState(largeState) + if err != nil { + t.Fatal(err) + } + err = s.PersistState(nil) + if err != nil { + t.Fatal(err) + } + err = s.RefreshState() + if err != nil { + t.Fatal(err) + } + + // shrink it down + largeState = generateLargeState(3) + err = s.WriteState(largeState) + if err != nil { + t.Fatal(err) + } + err = s.PersistState(nil) + if err != nil { + t.Fatal(err) + } + err = s.RefreshState() + if err != nil { + t.Fatal(err) + } +} + +func generateLargeState(size int) *states.State { + bigState := states.NewState() + dataSize := defaultChunkSize * size + chars := "abcdefghijklmnopqrstuvwxyz" + testData := make([]byte, dataSize) + for i := 0; i < dataSize; i++ { + testData[i] = chars[rand.Intn(len(chars))] + } + bigState.SyncWrapper().SetResourceInstanceCurrent( + addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "large_resource", + Name: "foo", + }, + }.Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"test_data":"` + string(testData) + `"}`), + Status: states.ObjectReady, + SchemaVersion: 0, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + return bigState +} + func TestForceUnlock(t *testing.T) { testACC(t) defer cleanupK8sResources(t) diff --git a/internal/backend/remote-state/kubernetes/go.mod b/internal/backend/remote-state/kubernetes/go.mod new file mode 100644 index 0000000000..097002b61e --- /dev/null +++ b/internal/backend/remote-state/kubernetes/go.mod @@ -0,0 +1,101 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes + +go 1.24.2 + +require ( + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/mitchellh/go-homedir v1.1.0 + github.com/zclconf/go-cty v1.16.2 + k8s.io/api v0.25.5 + k8s.io/apimachinery v0.25.5 + k8s.io/client-go v0.25.5 + k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed +) + +require ( + cloud.google.com/go/compute/metadata v0.5.2 // indirect + github.com/Azure/go-autorest v14.2.0+incompatible // indirect + github.com/Azure/go-autorest/autorest v0.11.30 // indirect + github.com/Azure/go-autorest/autorest/adal v0.9.24 // indirect + github.com/Azure/go-autorest/autorest/date v0.3.1 // indirect + github.com/Azure/go-autorest/logger v0.2.2 // indirect + github.com/Azure/go-autorest/tracing v0.6.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.8.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.5 // indirect + github.com/go-openapi/swag v0.19.14 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.27.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 + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.31.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.70.1 // indirect + k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect + sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.2.0 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/kubernetes/go.sum b/internal/backend/remote-state/kubernetes/go.sum new file mode 100644 index 0000000000..0aae074e03 --- /dev/null +++ b/internal/backend/remote-state/kubernetes/go.sum @@ -0,0 +1,739 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.30 h1:iaZ1RGz/ALZtN5eq4Nr1SOFSlf2E4pDI3Tcsl+dZPVE= +github.com/Azure/go-autorest/autorest v0.11.30/go.mod h1:t1kpPIOpIVX7annvothKvb0stsrXa37i7b+xpmBW8Fs= +github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= +github.com/Azure/go-autorest/autorest/adal v0.9.24 h1:BHZfgGsGwdkHDyZdtQRQk1WeUdW0m2WPAwuHZwUi5i4= +github.com/Azure/go-autorest/autorest/adal v0.9.24/go.mod h1:7T1+g0PYFmACYW5LlG2fcoPiPlFHjClyRGL7dRlP5c8= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/date v0.3.1 h1:o9Z8Jyt+VJJTCZ/UORishuHOusBwolhjokt9s5k8I4w= +github.com/Azure/go-autorest/autorest/date v0.3.1/go.mod h1:Dz/RDmXlfiFFS/eW+b/xMUSFs1tboPVy6UjgADToWDM= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= +github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= +github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/logger v0.2.2 h1:hYqBsEBywrrOSW24kkOCXRcKfKhK76OzLTfF+MYDE2o= +github.com/Azure/go-autorest/logger v0.2.2/go.mod h1:I5fg9K52o+iuydlWfa9T5K6WFos9XYr9dYTFzpqgibw= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsful9L/2nyR0= +github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.8.0 h1:eCZ8ulSerjdAiaNpF7GxXIE7ZCMo1moN1qX+S609eVw= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= +github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= +github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= +github.com/golang-jwt/jwt/v4 v4.5.0/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/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.25.5 h1:mqyHf7aoaYMpdvO87mqpol+Qnsmo+y09S0PMIXwiZKo= +k8s.io/api v0.25.5/go.mod h1:RzplZX0Z8rV/WhSTfEvnyd91bBhBQTRWo85qBQwRmb8= +k8s.io/apimachinery v0.25.5 h1:SQomYHvv+aO43qdu3QKRf9YuI0oI8w3RrOQ1qPbAUGY= +k8s.io/apimachinery v0.25.5/go.mod h1:1S2i1QHkmxc8+EZCIxe/fX5hpldVXk4gvnJInMEb8D4= +k8s.io/client-go v0.25.5 h1:7QWVK0Ph4bLn0UwotPTc2FTgm8shreQXyvXnnHDd8rE= +k8s.io/client-go v0.25.5/go.mod h1:bOeoaUUdpyz3WDFGo+Xm3nOQFh2KuYXRDwrvbAPtFQA= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.70.1 h1:7aaoSdahviPmR+XkS7FyxlkkXs6tHISSG03RxleQAVQ= +k8s.io/klog/v2 v2.70.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 h1:MQ8BAZPZlWk3S9K4a9NCkIFQtZShWqoha7snGixVgEA= +k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1/go.mod h1:C/N6wCaBHeBHkHUesQOQy2/MZqGgMAFPqGsGQLdbZBU= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed h1:jAne/RjBTyawwAy0utX5eqigAwz/lQhTmy+Hr/Cpue4= +k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 h1:iXTIw73aPyC+oRdyqqvVJuloN1p0AC/kzH07hu3NE+k= +sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/backend/remote-state/manta/backend.go b/internal/backend/remote-state/manta/backend.go deleted file mode 100644 index 3a7a21bc5a..0000000000 --- a/internal/backend/remote-state/manta/backend.go +++ /dev/null @@ -1,206 +0,0 @@ -package manta - -import ( - "context" - "encoding/pem" - "errors" - "fmt" - "io/ioutil" - "os" - - "github.com/hashicorp/errwrap" - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - triton "github.com/joyent/triton-go" - "github.com/joyent/triton-go/authentication" - "github.com/joyent/triton-go/storage" -) - -func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "account": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_ACCOUNT", "SDC_ACCOUNT"}, ""), - }, - - "user": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_USER", "SDC_USER"}, ""), - }, - - "url": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"MANTA_URL"}, "https://us-east.manta.joyent.com"), - }, - - "key_material": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_MATERIAL", "SDC_KEY_MATERIAL"}, ""), - }, - - "key_id": { - Type: schema.TypeString, - Required: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{"TRITON_KEY_ID", "SDC_KEY_ID"}, ""), - }, - - "insecure_skip_tls_verify": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("TRITON_SKIP_TLS_VERIFY", false), - }, - - "path": { - Type: schema.TypeString, - Required: true, - }, - - "object_name": { - Type: schema.TypeString, - Optional: true, - Default: "terraform.tfstate", - }, - }, - } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result -} - -type Backend struct { - *schema.Backend - data *schema.ResourceData - - // The fields below are set from configure - storageClient *storage.StorageClient - path string - objectName string -} - -type BackendConfig struct { - AccountId string - Username string - KeyId string - AccountUrl string - KeyMaterial string - SkipTls bool -} - -func (b *Backend) configure(ctx context.Context) error { - if b.path != "" { - return nil - } - - data := schema.FromContextBackendConfig(ctx) - - config := &BackendConfig{ - AccountId: data.Get("account").(string), - AccountUrl: data.Get("url").(string), - KeyId: data.Get("key_id").(string), - SkipTls: data.Get("insecure_skip_tls_verify").(bool), - } - - if v, ok := data.GetOk("user"); ok { - config.Username = v.(string) - } - - if v, ok := data.GetOk("key_material"); ok { - config.KeyMaterial = v.(string) - } - - b.path = data.Get("path").(string) - b.objectName = data.Get("object_name").(string) - - // If object_name is not set, try the deprecated objectName. - if b.objectName == "" { - b.objectName = data.Get("objectName").(string) - } - - var validationError *multierror.Error - - if data.Get("account").(string) == "" { - validationError = multierror.Append(validationError, errors.New("`Account` must be configured for the Triton provider")) - } - if data.Get("key_id").(string) == "" { - validationError = multierror.Append(validationError, errors.New("`Key ID` must be configured for the Triton provider")) - } - if b.path == "" { - validationError = multierror.Append(validationError, errors.New("`Path` must be configured for the Triton provider")) - } - - if validationError != nil { - return validationError - } - - var signer authentication.Signer - var err error - - if config.KeyMaterial == "" { - input := authentication.SSHAgentSignerInput{ - KeyID: config.KeyId, - AccountName: config.AccountId, - Username: config.Username, - } - signer, err = authentication.NewSSHAgentSigner(input) - if err != nil { - return errwrap.Wrapf("Error Creating SSH Agent Signer: {{err}}", err) - } - } else { - var keyBytes []byte - if _, err = os.Stat(config.KeyMaterial); err == nil { - keyBytes, err = ioutil.ReadFile(config.KeyMaterial) - if err != nil { - return fmt.Errorf("Error reading key material from %s: %s", - config.KeyMaterial, err) - } - block, _ := pem.Decode(keyBytes) - if block == nil { - return fmt.Errorf( - "Failed to read key material '%s': no key found", config.KeyMaterial) - } - - if block.Headers["Proc-Type"] == "4,ENCRYPTED" { - return fmt.Errorf( - "Failed to read key '%s': password protected keys are\n"+ - "not currently supported. Please decrypt the key prior to use.", config.KeyMaterial) - } - - } else { - keyBytes = []byte(config.KeyMaterial) - } - - input := authentication.PrivateKeySignerInput{ - KeyID: config.KeyId, - PrivateKeyMaterial: keyBytes, - AccountName: config.AccountId, - Username: config.Username, - } - - signer, err = authentication.NewPrivateKeySigner(input) - if err != nil { - return errwrap.Wrapf("Error Creating SSH Private Key Signer: {{err}}", err) - } - } - - clientConfig := &triton.ClientConfig{ - MantaURL: config.AccountUrl, - AccountName: config.AccountId, - Username: config.Username, - Signers: []authentication.Signer{signer}, - } - triton, err := storage.NewClient(clientConfig) - if err != nil { - return err - } - - b.storageClient = triton - - return nil -} diff --git a/internal/backend/remote-state/manta/backend_state.go b/internal/backend/remote-state/manta/backend_state.go deleted file mode 100644 index 925d82083d..0000000000 --- a/internal/backend/remote-state/manta/backend_state.go +++ /dev/null @@ -1,145 +0,0 @@ -package manta - -import ( - "context" - "errors" - "fmt" - "path" - "sort" - "strings" - - tritonErrors "github.com/joyent/triton-go/errors" - "github.com/joyent/triton-go/storage" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" -) - -func (b *Backend) Workspaces() ([]string, error) { - result := []string{backend.DefaultStateName} - - objs, err := b.storageClient.Dir().List(context.Background(), &storage.ListDirectoryInput{ - DirectoryName: path.Join(mantaDefaultRootStore, b.path), - }) - if err != nil { - if tritonErrors.IsResourceNotFound(err) { - return result, nil - } - return nil, err - } - - for _, obj := range objs.Entries { - if obj.Type == "directory" && obj.Name != "" { - result = append(result, obj.Name) - } - } - - sort.Strings(result[1:]) - return result, nil -} - -func (b *Backend) DeleteWorkspace(name string) error { - if name == backend.DefaultStateName || name == "" { - return fmt.Errorf("can't delete default state") - } - - //firstly we need to delete the state file - err := b.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, b.statePath(name), b.objectName), - }) - if err != nil { - return err - } - - //then we need to delete the state folder - err = b.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, b.statePath(name)), - }) - if err != nil { - return err - } - - return nil -} - -func (b *Backend) StateMgr(name string) (statemgr.Full, error) { - if name == "" { - return nil, errors.New("missing state name") - } - - client := &RemoteClient{ - storageClient: b.storageClient, - directoryName: b.statePath(name), - keyName: b.objectName, - } - - stateMgr := &remote.State{Client: client} - - //if this isn't the default state name, we need to create the object so - //it's listed by States. - if name != backend.DefaultStateName { - // take a lock on this state while we write it - lockInfo := statemgr.NewLockInfo() - lockInfo.Operation = "init" - lockId, err := client.Lock(lockInfo) - if err != nil { - return nil, fmt.Errorf("failed to lock manta state: %s", err) - } - - // Local helper function so we can call it multiple places - lockUnlock := func(parent error) error { - if err := stateMgr.Unlock(lockId); err != nil { - return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) - } - return parent - } - - // Grab the value - if err := stateMgr.RefreshState(); err != nil { - err = lockUnlock(err) - return nil, err - } - - // If we have no state, we have to create an empty state - if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(states.NewState()); err != nil { - err = lockUnlock(err) - return nil, err - } - if err := stateMgr.PersistState(); err != nil { - err = lockUnlock(err) - return nil, err - } - } - - // Unlock, the state should now be initialized - if err := lockUnlock(nil); err != nil { - return nil, err - } - - } - - return stateMgr, nil -} - -func (b *Backend) client() *RemoteClient { - return &RemoteClient{} -} - -func (b *Backend) statePath(name string) string { - if name == backend.DefaultStateName { - return b.path - } - - return path.Join(b.path, name) -} - -const errStateUnlock = ` -Error unlocking Manta state. Lock ID: %s - -Error: %s - -You may have to force-unlock this state in order to use it again. -` diff --git a/internal/backend/remote-state/manta/backend_test.go b/internal/backend/remote-state/manta/backend_test.go deleted file mode 100644 index 180b2aece3..0000000000 --- a/internal/backend/remote-state/manta/backend_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package manta - -import ( - "context" - "fmt" - "os" - "path" - "testing" - "time" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/joyent/triton-go/storage" -) - -func testACC(t *testing.T) { - skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_MANTA_TEST") == "" - if skip { - t.Log("Manta backend tests require setting TF_ACC or TF_MANTA_TEST") - t.Skip() - } - skip = os.Getenv("TRITON_ACCOUNT") == "" && os.Getenv("SDC_ACCOUNT") == "" - if skip { - t.Fatal("Manta backend tests require setting TRITON_ACCOUNT or SDC_ACCOUNT") - } - skip = os.Getenv("TRITON_KEY_ID") == "" && os.Getenv("SDC_KEY_ID") == "" - if skip { - t.Fatal("Manta backend tests require setting TRITON_KEY_ID or SDC_KEY_ID") - } -} - -func TestBackend_impl(t *testing.T) { - var _ backend.Backend = new(Backend) -} - -func TestBackend(t *testing.T) { - testACC(t) - - directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) - keyName := "testState" - - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "path": directory, - "object_name": keyName, - })).(*Backend) - - createMantaFolder(t, b.storageClient, directory) - defer deleteMantaFolder(t, b.storageClient, directory) - - backend.TestBackendStates(t, b) -} - -func TestBackendLocked(t *testing.T) { - testACC(t) - - directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) - keyName := "testState" - - b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "path": directory, - "object_name": keyName, - })).(*Backend) - - b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "path": directory, - "object_name": keyName, - })).(*Backend) - - createMantaFolder(t, b1.storageClient, directory) - defer deleteMantaFolder(t, b1.storageClient, directory) - - backend.TestBackendStateLocks(t, b1, b2) - backend.TestBackendStateForceUnlock(t, b1, b2) -} - -func createMantaFolder(t *testing.T, mantaClient *storage.StorageClient, directoryName string) { - // Be clear about what we're doing in case the user needs to clean - // this up later. - //t.Logf("creating Manta directory %s", directoryName) - err := mantaClient.Dir().Put(context.Background(), &storage.PutDirectoryInput{ - DirectoryName: path.Join(mantaDefaultRootStore, directoryName), - }) - if err != nil { - t.Fatal("failed to create test Manta directory:", err) - } -} - -func deleteMantaFolder(t *testing.T, mantaClient *storage.StorageClient, directoryName string) { - //warning := "WARNING: Failed to delete the test Manta directory. It may have been left in your Manta account and may incur storage charges. (error was %s)" - - // first we have to get rid of the env objects, or we can't delete the directory - objs, err := mantaClient.Dir().List(context.Background(), &storage.ListDirectoryInput{ - DirectoryName: path.Join(mantaDefaultRootStore, directoryName), - }) - if err != nil { - t.Fatal("failed to retrieve directory listing") - } - - for _, obj := range objs.Entries { - if obj.Type == "directory" { - ojs, err := mantaClient.Dir().List(context.Background(), &storage.ListDirectoryInput{ - DirectoryName: path.Join(mantaDefaultRootStore, directoryName, obj.Name), - }) - if err != nil { - t.Fatal("failed to retrieve directory listing") - } - for _, oj := range ojs.Entries { - err := mantaClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, directoryName, obj.Name, oj.Name), - }) - if err != nil { - t.Fatal(err) - } - } - } - - err := mantaClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, directoryName, obj.Name), - }) - if err != nil { - t.Fatal(err) - } - } - - err = mantaClient.Dir().Delete(context.Background(), &storage.DeleteDirectoryInput{ - DirectoryName: path.Join(mantaDefaultRootStore, directoryName), - }) - if err != nil { - t.Fatal("failed to delete manta directory") - } -} diff --git a/internal/backend/remote-state/manta/client.go b/internal/backend/remote-state/manta/client.go deleted file mode 100644 index 29d3659514..0000000000 --- a/internal/backend/remote-state/manta/client.go +++ /dev/null @@ -1,200 +0,0 @@ -package manta - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "log" - "path" - - uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" - tritonErrors "github.com/joyent/triton-go/errors" - "github.com/joyent/triton-go/storage" -) - -const ( - mantaDefaultRootStore = "/stor" - lockFileName = "tflock" -) - -type RemoteClient struct { - storageClient *storage.StorageClient - directoryName string - keyName string - statePath string -} - -func (c *RemoteClient) Get() (*remote.Payload, error) { - output, err := c.storageClient.Objects().Get(context.Background(), &storage.GetObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, c.keyName), - }) - if err != nil { - if tritonErrors.IsResourceNotFound(err) { - return nil, nil - } - return nil, err - } - defer output.ObjectReader.Close() - - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, output.ObjectReader); err != nil { - return nil, fmt.Errorf("Failed to read remote state: %s", err) - } - - payload := &remote.Payload{ - Data: buf.Bytes(), - } - - // If there was no data, then return nil - if len(payload.Data) == 0 { - return nil, nil - } - - return payload, nil - -} - -func (c *RemoteClient) Put(data []byte) error { - contentType := "application/json" - contentLength := int64(len(data)) - - params := &storage.PutObjectInput{ - ContentType: contentType, - ContentLength: uint64(contentLength), - ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, c.keyName), - ObjectReader: bytes.NewReader(data), - } - - log.Printf("[DEBUG] Uploading remote state to Manta: %#v", params) - err := c.storageClient.Objects().Put(context.Background(), params) - if err != nil { - return err - } - - return nil -} - -func (c *RemoteClient) Delete() error { - err := c.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, c.keyName), - }) - - return err -} - -func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { - //At Joyent, we want to make sure that the State directory exists before we interact with it - //We don't expect users to have to create it in advance - //The order of operations of Backend State as follows: - // * Get - if this doesn't exist then we continue as though it's new - // * Lock - we make sure that the state directory exists as it's the entrance to writing to Manta - // * Put - put the state up there - // * Unlock - unlock the directory - //We can always guarantee that the user can put their state in the specified location because of this - err := c.storageClient.Dir().Put(context.Background(), &storage.PutDirectoryInput{ - DirectoryName: path.Join(mantaDefaultRootStore, c.directoryName), - }) - if err != nil { - return "", err - } - - //firstly we want to check that a lock doesn't already exist - lockErr := &statemgr.LockError{} - lockInfo, err := c.getLockInfo() - if err != nil { - if !tritonErrors.IsResourceNotFound(err) { - lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) - return "", lockErr - } - } - - if lockInfo != nil { - lockErr := &statemgr.LockError{ - Err: fmt.Errorf("A lock is already acquired"), - Info: lockInfo, - } - return "", lockErr - } - - info.Path = path.Join(c.directoryName, lockFileName) - - if info.ID == "" { - lockID, err := uuid.GenerateUUID() - if err != nil { - return "", err - } - - info.ID = lockID - } - - data := info.Marshal() - - contentType := "application/json" - contentLength := int64(len(data)) - - params := &storage.PutObjectInput{ - ContentType: contentType, - ContentLength: uint64(contentLength), - ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, lockFileName), - ObjectReader: bytes.NewReader(data), - ForceInsert: true, - } - - log.Printf("[DEBUG] Creating manta state lock: %#v", params) - err = c.storageClient.Objects().Put(context.Background(), params) - if err != nil { - return "", err - } - - return info.ID, nil -} - -func (c *RemoteClient) Unlock(id string) error { - lockErr := &statemgr.LockError{} - - lockInfo, err := c.getLockInfo() - if err != nil { - lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) - return lockErr - } - lockErr.Info = lockInfo - - if lockInfo.ID != id { - lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) - return lockErr - } - - err = c.storageClient.Objects().Delete(context.Background(), &storage.DeleteObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, lockFileName), - }) - - return err -} - -func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { - output, err := c.storageClient.Objects().Get(context.Background(), &storage.GetObjectInput{ - ObjectPath: path.Join(mantaDefaultRootStore, c.directoryName, lockFileName), - }) - if err != nil { - return nil, err - } - - defer output.ObjectReader.Close() - - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, output.ObjectReader); err != nil { - return nil, fmt.Errorf("Failed to read lock info: %s", err) - } - - lockInfo := &statemgr.LockInfo{} - err = json.Unmarshal(buf.Bytes(), lockInfo) - if err != nil { - return nil, err - } - - return lockInfo, nil -} diff --git a/internal/backend/remote-state/manta/client_test.go b/internal/backend/remote-state/manta/client_test.go deleted file mode 100644 index f5b9a516f5..0000000000 --- a/internal/backend/remote-state/manta/client_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package manta - -import ( - "testing" - - "fmt" - "time" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/states/remote" -) - -func TestRemoteClient_impl(t *testing.T) { - var _ remote.Client = new(RemoteClient) - var _ remote.ClientLocker = new(RemoteClient) -} - -func TestRemoteClient(t *testing.T) { - testACC(t) - directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) - keyName := "testState" - - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "path": directory, - "object_name": keyName, - })).(*Backend) - - createMantaFolder(t, b.storageClient, directory) - defer deleteMantaFolder(t, b.storageClient, directory) - - state, err := b.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatal(err) - } - - remote.TestClient(t, state.(*remote.State).Client) -} - -func TestRemoteClientLocks(t *testing.T) { - testACC(t) - directory := fmt.Sprintf("terraform-remote-manta-test-%x", time.Now().Unix()) - keyName := "testState" - - b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "path": directory, - "object_name": keyName, - })).(*Backend) - - b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "path": directory, - "object_name": keyName, - })).(*Backend) - - createMantaFolder(t, b1.storageClient, directory) - defer deleteMantaFolder(t, b1.storageClient, directory) - - s1, err := b1.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatal(err) - } - - s2, err := b2.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatal(err) - } - - remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client) -} diff --git a/internal/backend/remote-state/oci/auth.go b/internal/backend/remote-state/oci/auth.go new file mode 100644 index 0000000000..055efb9240 --- /dev/null +++ b/internal/backend/remote-state/oci/auth.go @@ -0,0 +1,470 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/http" + "os" + "path" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + + "github.com/hashicorp/terraform/version" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/common/auth" + "github.com/oracle/oci-go-sdk/v65/objectstorage" + "github.com/zclconf/go-cty/cty" +) + +var ApiKeyConfigAttributes = [5]string{UserOcidAttrName, FingerprintAttrName, PrivateKeyAttrName, PrivateKeyPathAttrName, PrivateKeyPasswordAttrName} + +type ociAuthConfigProvider struct { + authType string + configFileProfile string + region string + tenancyOcid string + userOcid string + fingerprint string + privateKey string + privateKeyPath string + privateKeyPassword string +} + +func newOciAuthConfigProvider(obj cty.Value) ociAuthConfigProvider { + p := ociAuthConfigProvider{} + + if authVal, ok := getBackendAttrWithDefault(obj, AuthAttrName, AuthAPIKeySetting); ok { + p.authType = authVal.AsString() + } + + if configFileProfileVal, ok := getBackendAttr(obj, ConfigFileProfileAttrName); ok { + p.configFileProfile = configFileProfileVal.AsString() + } + if regionVal, ok := getBackendAttr(obj, RegionAttrName); ok { + p.region = regionVal.AsString() + } + + if tenancyOcidVal, ok := getBackendAttr(obj, TenancyOcidAttrName); ok { + p.tenancyOcid = tenancyOcidVal.AsString() + } + + if userOcidVal, ok := getBackendAttr(obj, UserOcidAttrName); ok { + p.userOcid = userOcidVal.AsString() + } + + if fingerprintVal, ok := getBackendAttr(obj, FingerprintAttrName); ok { + p.fingerprint = fingerprintVal.AsString() + } + + if privateKeyVal, ok := getBackendAttr(obj, PrivateKeyAttrName); ok { + p.privateKey = privateKeyVal.AsString() + } + + if privateKeyPathVal, ok := getBackendAttr(obj, PrivateKeyPathAttrName); ok { + p.privateKeyPath = privateKeyPathVal.AsString() + } + + if privateKeyPasswordVal, ok := getBackendAttr(obj, PrivateKeyPasswordAttrName); ok { + p.privateKeyPassword = privateKeyPasswordVal.AsString() + } + + return p +} +func (p ociAuthConfigProvider) AuthType() (common.AuthConfig, error) { + return common.AuthConfig{ + AuthType: common.UnknownAuthenticationType, + IsFromConfigFile: false, + OboToken: nil, + }, + fmt.Errorf("unsupported, keep the interface") +} + +func (p ociAuthConfigProvider) TenancyOCID() (string, error) { + if p.tenancyOcid != "" { + return p.tenancyOcid, nil + } + return "", fmt.Errorf("can not get %s from Terraform backend configuration", TenancyOcidAttrName) +} + +func (p ociAuthConfigProvider) UserOCID() (string, error) { + if p.userOcid != "" { + return p.userOcid, nil + } + return "", fmt.Errorf("can not get %s from Terraform backend configuration", UserOcidAttrName) +} + +func (p ociAuthConfigProvider) KeyFingerprint() (string, error) { + if p.fingerprint != "" { + return p.fingerprint, nil + } + return "", fmt.Errorf("can not get %s from Terraform backend configuration", FingerprintAttrName) +} + +func (p ociAuthConfigProvider) Region() (string, error) { + if p.region != "" { + return p.region, nil + } + return "", fmt.Errorf("can not get %s from Terraform backend configuration", RegionAttrName) +} +func (p ociAuthConfigProvider) KeyID() (string, error) { + tenancy, err := p.TenancyOCID() + if err != nil { + return "", err + } + + user, err := p.UserOCID() + if err != nil { + return "", err + } + + fingerprint, err := p.KeyFingerprint() + if err != nil { + return "", err + } + return fmt.Sprintf("%s/%s/%s", tenancy, user, fingerprint), nil +} + +func (p ociAuthConfigProvider) PrivateRSAKey() (key *rsa.PrivateKey, err error) { + + if p.privateKey != "" { + keyData := strings.ReplaceAll(p.privateKey, "\\n", "\n") // Ensure \n is replaced by actual newlines + return common.PrivateKeyFromBytesWithPassword([]byte(keyData), []byte(p.privateKeyPassword)) + } + + if p.privateKeyPath != "" { + resolvedPath := expandPath(p.privateKeyPath) + pemFileContent, readFileErr := os.ReadFile(resolvedPath) + if readFileErr != nil { + return nil, fmt.Errorf("can not read private key from: '%s', Error: %q", p.privateKeyPath, readFileErr) + } + return common.PrivateKeyFromBytesWithPassword(pemFileContent, []byte(p.privateKeyPassword)) + } + + return nil, fmt.Errorf("can not get private_key or private_key_path from Terraform configuration") +} + +func (p ociAuthConfigProvider) getConfigProviders() ([]common.ConfigurationProvider, error) { + var configProviders []common.ConfigurationProvider + logger := logWithOperation("AuthConfigProvider") + logger.Debug(fmt.Sprintf("Using %s authentication", p.authType)) + switch strings.ToLower(p.authType) { + case strings.ToLower(AuthAPIKeySetting): + // No additional config providers needed + case strings.ToLower(AuthInstancePrincipalSetting): + + logger.Info("Attempting to authenticate using instance principal credentials") + if p.region == "" { + return nil, fmt.Errorf("unable to determine region from Terraform backend configuration while using Instance Principal") + } + + // Used to modify InstancePrincipal auth clients so that `accept_local_certs` is honored for auth clients as well + instancePrincipalAuthClientModifier := func(client common.HTTPRequestDispatcher) (common.HTTPRequestDispatcher, error) { + if acceptLocalCerts := getEnvSettingWithBlankDefault(AcceptLocalCerts); acceptLocalCerts != "" { + if value, err := strconv.ParseBool(acceptLocalCerts); err == nil { + modifiedClient := buildHttpClient() + modifiedClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = value + return modifiedClient, nil + } + } + return client, nil + } + + cfg, err := auth.InstancePrincipalConfigurationForRegionWithCustomClient(common.StringToRegion(p.region), instancePrincipalAuthClientModifier) + if err != nil { + return nil, err + } + logger.Debug(" Configuration provided by: %s", cfg) + + configProviders = append(configProviders, cfg) + case strings.ToLower(AuthInstancePrincipalWithCertsSetting): + logger.Info("Attempting to authenticate using instance principal with certificates") + + if p.region == "" { + return nil, fmt.Errorf("unable to determine region from Terraform backend configuration while using Instance Principal with certificates") + } + + defaultCertsDir, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("can not get working directory for current os platform") + } + + certsDir := filepath.Clean(getEnvSettingWithDefault("test_certificates_location", defaultCertsDir)) + leafCertificateBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "ip_cert.pem")) + if err != nil { + return nil, fmt.Errorf("can not read leaf certificate from %s", filepath.Join(certsDir, "ip_cert.pem")) + } + + leafPrivateKeyBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "ip_key.pem")) + if err != nil { + return nil, fmt.Errorf("can not read leaf private key from %s", filepath.Join(certsDir, "ip_key.pem")) + } + + leafPassphraseBytes := []byte{} + if _, err := os.Stat(certsDir + "/leaf_passphrase"); !os.IsNotExist(err) { + leafPassphraseBytes, err = getCertificateFileBytes(filepath.Join(certsDir + "leaf_passphrase")) + if err != nil { + return nil, fmt.Errorf("can not read leafPassphraseBytes from %s", filepath.Join(certsDir+"leaf_passphrase")) + } + } + + intermediateCertificateBytes, err := getCertificateFileBytes(filepath.Join(certsDir, "intermediate.pem")) + if err != nil { + return nil, fmt.Errorf("can not read intermediate certificate from %s", filepath.Join(certsDir, "intermediate.pem")) + } + + intermediateCertificatesBytes := [][]byte{ + intermediateCertificateBytes, + } + + cfg, err := auth.InstancePrincipalConfigurationWithCerts(common.StringToRegion(p.region), leafCertificateBytes, leafPassphraseBytes, leafPrivateKeyBytes, intermediateCertificatesBytes) + if err != nil { + return nil, err + } + logger.Debug(" Configuration provided by: %s", cfg) + + configProviders = append(configProviders, cfg) + + case strings.ToLower(AuthSecurityToken): + logger.Info("Attempting to authenticate using security token") + if p.region == "" { + return nil, fmt.Errorf("can not get %s from Terraform configuration (SecurityToken)", RegionAttrName) + } + // if region is part of the provider block make sure it is part of the final configuration too, and overwrites the region in the profile. + + regionProvider := common.NewRawConfigurationProvider("", "", p.region, "", "", nil) + configProviders = append(configProviders, regionProvider) + + if p.configFileProfile == "" { + return nil, fmt.Errorf("missing profile in provider block %v", ConfigFileProfileAttrName) + } + + defaultPath := path.Join(getHomeFolder(), DefaultConfigDirName, DefaultConfigFileName) + if err := checkProfile(p.configFileProfile, defaultPath); err != nil { + return nil, err + } + securityTokenBasedAuthConfigProvider, err := common.ConfigurationProviderForSessionTokenWithProfile(defaultPath, p.configFileProfile, p.privateKeyPassword) + if err != nil { + return nil, fmt.Errorf("could not create security token based auth config provider %v", err) + } + configProviders = append(configProviders, securityTokenBasedAuthConfigProvider) + case strings.ToLower(ResourcePrincipal): + logger.Info("Attempting to authenticate using resource principal credentials") + var err error + var resourcePrincipalAuthConfigProvider auth.ConfigurationProviderWithClaimAccess + + if p.region == "" { + logger.Debug("did not get %s from Terraform configuration (ResourcePrincipal), falling back to environment variable", RegionAttrName) + resourcePrincipalAuthConfigProvider, err = auth.ResourcePrincipalConfigurationProvider() + } else { + resourcePrincipalAuthConfigProvider, err = auth.ResourcePrincipalConfigurationProviderForRegion(common.StringToRegion(p.region)) + } + if err != nil { + return nil, err + } + configProviders = append(configProviders, resourcePrincipalAuthConfigProvider) + case strings.ToLower(AuthOKEWorkloadIdentity): + logger.Info("Attempting to authenticate using OKE workload identity") + okeWorkloadIdentityConfigProvider, err := auth.OkeWorkloadIdentityConfigurationProvider() + if err != nil { + return nil, fmt.Errorf("can not get oke workload indentity based auth config provider %v", err) + } + configProviders = append(configProviders, okeWorkloadIdentityConfigProvider) + default: + return nil, fmt.Errorf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity) + } + + return configProviders, nil +} +func (p ociAuthConfigProvider) getSdkConfigProvider() (common.ConfigurationProvider, error) { + + configProviders, err := p.getConfigProviders() + if err != nil { + return nil, err + } + + configProviders = append(configProviders, p) + //In GoSDK, the first step is to check if AuthType exists, + //for composite provider, we only check the first provider in the list for the AuthType. + //Then SDK will based on the AuthType to Create the actual provider if it's a valid value. + //If not, then SDK will base on the order in the composite provider list to check for necessary info (tenancyid, userID, fingerprint, region, keyID). + if p.configFileProfile == "" { + configProviders = append(configProviders, common.DefaultConfigProvider()) + } else { + defaultPath := path.Join(getHomeFolder(), DefaultConfigDirName, DefaultConfigFileName) + err := checkProfile(p.configFileProfile, defaultPath) + if err != nil { + return nil, err + } + configProviders = append(configProviders, common.CustomProfileConfigProvider(defaultPath, p.configFileProfile)) + } + sdkConfigProvider, err := common.ComposingConfigurationProvider(configProviders) + if err != nil { + return nil, err + } + + return sdkConfigProvider, nil +} +func buildHttpClient() (httpClient *http.Client) { + httpClient = &http.Client{ + Timeout: getDurationFromEnvVar(HTTPRequestTimeOut, DefaultRequestTimeout), + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: getDurationFromEnvVar(DialContextConnectionTimeout, DefaultConnectionTimeout), + }).DialContext, + TLSHandshakeTimeout: getDurationFromEnvVar(TLSHandshakeTimeout, DefaultTLSHandshakeTimeout), + TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12}, + Proxy: http.ProxyFromEnvironment, + }, + } + return +} + +func getCertificateFileBytes(certificateFileFullPath string) (pemRaw []byte, err error) { + absFile, err := filepath.Abs(certificateFileFullPath) + if err != nil { + return nil, fmt.Errorf("can't form absolute path of %s: %v", certificateFileFullPath, err) + } + + if pemRaw, err = os.ReadFile(absFile); err != nil { + return nil, fmt.Errorf("can't read %s: %v", certificateFileFullPath, err) + } + return +} +func UserAgentFromEnv() string { + + userAgentFromEnv := getEnvSettingWithBlankDefault(UserAgentSDKNameEnv) + if userAgentFromEnv == "" { + userAgentFromEnv = getEnvSettingWithBlankDefault(UserAgentTerraformNameEnv) + } + if userAgentFromEnv == "" { + userAgentFromEnv = DefaultUserAgentBackendName + } + + return userAgentFromEnv +} + +// OboTokenProvider interface that wraps information about auth tokens so the sdk client can make calls +// on behalf of a different authorized user +type OboTokenProvider interface { + OboToken() (string, error) +} + +// EmptyOboTokenProvider always provides an empty obo token +type emptyOboTokenProvider struct{} + +// OboToken provides the obo token +func (provider emptyOboTokenProvider) OboToken() (string, error) { + return "", nil +} + +type oboTokenProviderFromEnv struct{} + +func (p oboTokenProviderFromEnv) OboToken() (string, error) { + // priority token from path than token from environment + if tokenPath := getEnvSettingWithBlankDefault(OboTokenPath); tokenPath != "" { + tokenByte, err := os.ReadFile(tokenPath) + if err != nil { + return "", err + } + return string(tokenByte), nil + } + return getEnvSettingWithBlankDefault(OboTokenAttrName), nil +} +func buildConfigureClient(configProvider common.ConfigurationProvider, httpClient *http.Client) (*objectstorage.ObjectStorageClient, error) { + + userAgentName := UserAgentFromEnv() + userAgent := fmt.Sprintf(UserAgentFormatter, common.Version(), runtime.Version(), runtime.GOOS, runtime.GOARCH, version.String(), userAgentName) + + useOboToken, err := strconv.ParseBool(getEnvSettingWithDefault("use_obo_token", "false")) + if err != nil { + return nil, err + } + setSDKLogger() + client, err := objectstorage.NewObjectStorageClientWithConfigurationProvider(configProvider) + if err != nil { + return nil, err + } + if hostOverride := getClientHostOverride(); hostOverride != "" { + client.Host = hostOverride + } + requestSigner := common.DefaultRequestSigner(configProvider) + var oboTokenProvider OboTokenProvider + oboTokenProvider = emptyOboTokenProvider{} + if useOboToken { + // Add Obo token to the default list and Update the signer + httpHeadersToSign := append(common.DefaultGenericHeaders(), RequestHeaderOpcOboToken) + requestSigner = common.RequestSigner(configProvider, httpHeadersToSign, common.DefaultBodyHeaders()) + oboTokenProvider = oboTokenProviderFromEnv{} + } + + client.HTTPClient = httpClient + client.UserAgent = userAgent + client.Signer = requestSigner + client.Interceptor = func(r *http.Request) error { + if oboToken, err := oboTokenProvider.OboToken(); err == nil && oboToken != "" { + r.Header.Set(RequestHeaderOpcOboToken, oboToken) + } + return nil + } + + domainNameOverride := getEnvSettingWithBlankDefault(DomainNameOverrideEnv) + + if domainNameOverride != "" { + hasCorrectDomainName := getEnvSettingWithBlankDefault(HasCorrectDomainNameEnv) + re := regexp.MustCompile(`(.*?)[-\w]+\.\w+$`) // (capture: preamble) match: d0main-name . tld end-of-string + if hasCorrectDomainName == "" || !strings.HasSuffix(client.Host, hasCorrectDomainName) { + client.Host = re.ReplaceAllString(client.Host, "${1}"+domainNameOverride) // non-match conveniently returns original string + } + } + + customCertLoc := getEnvSettingWithBlankDefault(CustomCertLocationEnv) + + if customCertLoc != "" { + cert, err := os.ReadFile(customCertLoc) + if err != nil { + return &client, err + } + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(cert); !ok { + return nil, fmt.Errorf("failed to append custom cert to the pool") + } + // install the certificates in the client + httpClient.Transport.(*http.Transport).TLSClientConfig.RootCAs = pool + } + + if acceptLocalCerts := getEnvSettingWithBlankDefault(AcceptLocalCerts); acceptLocalCerts != "" { + if boolVal, err := strconv.ParseBool(acceptLocalCerts); err == nil { + httpClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = boolVal + } + } + + return &client, nil +} +func getClientHostOverride() string { + // Get the host URL override for clients + + clientHostOverridesString := getEnvSettingWithBlankDefault(ClientHostOverridesEnv) + if clientHostOverridesString == "" { + return "" + } + + clientHostFlags := strings.Split(clientHostOverridesString, ColonDelimiter) + for _, item := range clientHostFlags { + clientNameHost := strings.Split(item, EqualToOperatorDelimiter) + if clientNameHost == nil || len(clientNameHost) != 2 { + continue + } + if clientNameHost[0] == ObjectStorageClientName { + return clientNameHost[1] + } + } + return "" +} diff --git a/internal/backend/remote-state/oci/backend.go b/internal/backend/remote-state/oci/backend.go new file mode 100644 index 0000000000..26c6a0cc3a --- /dev/null +++ b/internal/backend/remote-state/oci/backend.go @@ -0,0 +1,268 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "fmt" + "path" + "strings" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +var ( + lockFileSuffix = ".lock" +) + +func New() backend.Backend { + return &Backend{} +} + +// New creates a new backend for oci remote state. +func (b *Backend) ConfigSchema() *configschema.Block { + return &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + KeyAttrName: { + Type: cty.String, + Optional: true, + Description: "The name of the state file stored on the remote backend.", + }, + BucketAttrName: { + Type: cty.String, + Required: true, + Description: "The name of the OCI Object Storage bucket.", + }, + NamespaceAttrName: { + Type: cty.String, + Required: true, + Description: "The namespace of the OCI Object Storage.", + }, + RegionAttrName: { + Type: cty.String, + Optional: true, + Description: "OCI region where the bucket is located.", + }, + TenancyOcidAttrName: { + Type: cty.String, + Optional: true, + Description: "The OCID of the tenancy.", + }, + UserOcidAttrName: { + Type: cty.String, + Optional: true, + Description: "The OCID of the user.", + }, + FingerprintAttrName: { + Type: cty.String, + Optional: true, + Description: "The fingerprint of the user's API key.", + }, + PrivateKeyAttrName: { + Type: cty.String, + Sensitive: true, + Optional: true, + Description: "The private key for API authentication.", + }, + PrivateKeyPathAttrName: { + Type: cty.String, + Optional: true, + Description: "Path to the private key file.", + }, + PrivateKeyPasswordAttrName: { + Type: cty.String, + Sensitive: true, + Optional: true, + Description: "Passphrase for the private key, if required.", + }, + AuthAttrName: { + Type: cty.String, + Optional: true, + Description: "Authentication method (API key, Instance Principal, Resource Principal, etc.).", + }, + + ConfigFileProfileAttrName: { + Type: cty.String, + Optional: true, + Description: "Profile name from the OCI config file.", + }, + WorkspaceKeyPrefixAttrName: { + Type: cty.String, + Optional: true, + Description: "The prefix applied to the non-default state path inside the bucket.", + }, + KmsKeyIdAttrName: { + Type: cty.String, + Optional: true, + Description: "The OCID of a master encryption key used to call the Key Management service to generate a data encryption key or to encrypt or decrypt a data encryption key.", + }, + SseCustomerKeyAttrName: { + Type: cty.String, + Optional: true, + Description: "The optional header that specifies the base64-encoded 256-bit encryption key to use to encrypt or decrypt the data.", + Sensitive: true, + }, + SseCustomerKeySHA256AttrName: { + Type: cty.String, + Optional: true, + Description: "The optional header that specifies the base64-encoded SHA256 hash of the encryption key. This value is used to check the integrity of the encryption key.", + }, + SseCustomerAlgorithmAttrName: { + Type: cty.String, + Optional: true, + Description: "The optional header that specifies \"AES256\" as the encryption algorithm.", + }, + }, + } +} + +type Backend struct { + configProvider ociAuthConfigProvider + bucket string + key string + namespace string + workspaceKeyPrefix string + kmsKeyID string + SSECustomerKey string + SSECustomerKeySHA256 string + SSECustomerAlgorithm string + client *RemoteClient +} + +func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { + + var diags tfdiags.Diagnostics + if obj.IsNull() { + + diags.Append(tfdiags.AttributeValue(tfdiags.Error, "Invalid Configuration", "Received null configuration for OCI backend.", cty.GetAttrPath("."))) + return obj, diags + } + if bucketVal := obj.GetAttr(BucketAttrName); bucketVal.IsNull() || bucketVal.AsString() == "" { + diags = diags.Append(requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName))) + } else { + validateStringBucketName(bucketVal.AsString(), cty.GetAttrPath(BucketAttrName), &diags) + } + if namespaceVal := obj.GetAttr(NamespaceAttrName); namespaceVal.IsNull() || namespaceVal.AsString() == "" { + diags = diags.Append(requiredAttributeErrDiag(cty.GetAttrPath(NamespaceAttrName))) + } + if keyVal, ok := getBackendAttrWithDefault(obj, KeyAttrName, defaultKeyValue); ok { + validateStringObjectPath(keyVal.AsString(), cty.GetAttrPath(KeyAttrName), &diags) + } + if workspaceKeyPrefixVal, ok := getBackendAttrWithDefault(obj, WorkspaceKeyPrefixAttrName, defaultWorkspaceEnvPrefix); ok { + validateStringWorkspacePrefix(workspaceKeyPrefixVal.AsString(), cty.GetAttrPath(WorkspaceKeyPrefixAttrName), &diags) + } + authVal, ok := getBackendAttr(obj, AuthAttrName) + if ok && len(authVal.AsString()) > 0 { + + switch strings.ToLower(authVal.AsString()) { + case strings.ToLower(AuthAPIKeySetting): + //Nothing to do + return obj, diags + case strings.ToLower(AuthInstancePrincipalSetting), strings.ToLower(AuthInstancePrincipalWithCertsSetting), strings.ToLower(ResourcePrincipal), strings.ToLower(AuthSecurityToken): + region, _ := getBackendAttr(obj, RegionAttrName) + if region.AsString() == "" { + diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, + "Missing region attribute required", + fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", RegionAttrName, authVal.AsString()), cty.GetAttrPath(RegionAttrName), + )) + } + if strings.ToLower(authVal.AsString()) == strings.ToLower(AuthSecurityToken) { + profileVal, _ := getBackendAttr(obj, ConfigFileProfileAttrName) + if profileVal.IsNull() || profileVal.AsString() == "" { + diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, + "Missing config_file_profile attribute required", + fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", ConfigFileProfileAttrName, authVal.AsString()), cty.GetAttrPath(ConfigFileProfileAttrName), + )) + } + } + default: + diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, + "Invalid authentication method", + fmt.Sprintf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity), cty.GetAttrPath(AuthAttrName), + )) + } + } + + customerKey, _ := getBackendAttr(obj, SseCustomerKeyAttrName) + customerKeySHA, _ := getBackendAttr(obj, SseCustomerKeySHA256AttrName) + kmsKeyId, _ := getBackendAttr(obj, KmsKeyIdAttrName) + privateKey, _ := getBackendAttr(obj, PrivateKeyAttrName) + privateKeyPath, _ := getBackendAttr(obj, PrivateKeyPathAttrName) + + if (!customerKey.IsNull() && len(customerKey.AsString()) > 0) && (customerKeySHA.IsNull() || len(customerKeySHA.AsString()) == 0) { + diags = diags.Append(attributeErrDiag( + "Invalid Attribute Combination", + ` sse_customer_key and its SHA both required.`, + cty.GetAttrPath(SseCustomerKeySHA256AttrName))) + } + if !customerKey.IsNull() && len(customerKey.AsString()) > 0 && !kmsKeyId.IsNull() && len(kmsKeyId.AsString()) > 0 { + diags = diags.Append(attributeErrDiag( + "Invalid Attribute Combination", + `Only one of kms_key_id, sse_customer_key can be set.`, + cty.GetAttrPath(KmsKeyIdAttrName), + )) + } + if !privateKey.IsNull() && len(privateKey.AsString()) > 0 && !privateKeyPath.IsNull() && len(privateKeyPath.AsString()) > 0 { + diags = diags.Append(attributeErrDiag( + "Invalid Attribute Combination", + `Only one of private_key, private_key_path can be set.`, + cty.GetAttrPath(PrivateKeyPathAttrName), + )) + } + return obj, diags +} +func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if bucketVal, ok := getBackendAttr(obj, BucketAttrName); ok { + b.bucket = bucketVal.AsString() + } + if namespaceVal, ok := getBackendAttr(obj, NamespaceAttrName); ok { + b.namespace = namespaceVal.AsString() + } + if keyVal, ok := getBackendAttrWithDefault(obj, KeyAttrName, defaultKeyValue); ok { + b.key = keyVal.AsString() + } + + if workspaceKeyPrefixVal, ok := getBackendAttrWithDefault(obj, WorkspaceKeyPrefixAttrName, defaultWorkspaceEnvPrefix); ok { + b.workspaceKeyPrefix = workspaceKeyPrefixVal.AsString() + } + + if kmsKeyIdVal, ok := getBackendAttr(obj, KmsKeyIdAttrName); ok { + b.kmsKeyID = kmsKeyIdVal.AsString() + } + if customerKeyVal, ok := getBackendAttr(obj, SseCustomerKeyAttrName); ok { + b.SSECustomerKey = customerKeyVal.AsString() + } + if customerKeySHA256Val, ok := getBackendAttr(obj, SseCustomerKeySHA256AttrName); ok { + b.SSECustomerKeySHA256 = customerKeySHA256Val.AsString() + } + if customerAlgorithmVal, ok := getBackendAttrWithDefault(obj, SseCustomerAlgorithmAttrName, DefaultAlgorithm); ok { + b.SSECustomerAlgorithm = customerAlgorithmVal.AsString() + } + b.configProvider = newOciAuthConfigProvider(obj) + + err := b.configureRemoteClient() + if err != nil { + diags = append(diags, backendbase.ErrorAsDiagnostics(err)[0]) + } + return diags +} + +func (b *Backend) path(name string) string { + if name == backend.DefaultStateName { + return b.key + } + + return path.Join(b.workspaceKeyPrefix, name, b.key) +} + +// getLockFilePath returns the path to the lock file for the given Terraform state. +// For `default.tfstate`, the lock file is stored at `default.tfstate.tflock`. +func (b *Backend) getLockFilePath(name string) string { + return b.path(name) + lockFileSuffix +} diff --git a/internal/backend/remote-state/oci/backend_state.go b/internal/backend/remote-state/oci/backend_state.go new file mode 100644 index 0000000000..009b43d1aa --- /dev/null +++ b/internal/backend/remote-state/oci/backend_state.go @@ -0,0 +1,201 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/objectstorage" +) + +const errStateUnlock = ` +Error unlocking oci state. Lock ID: %s + +Error: %s + +You may have to force-unlock this state in order to use it again. +` + +func (b *Backend) StateMgr(name string) (statemgr.Full, error) { + b.client.path = b.path(name) + b.client.lockFilePath = b.getLockFilePath(name) + stateMgr := &remote.State{Client: &RemoteClient{ + objectStorageClient: b.client.objectStorageClient, + bucketName: b.bucket, + path: b.path(name), + lockFilePath: b.getLockFilePath(name), + namespace: b.namespace, + kmsKeyID: b.kmsKeyID, + + SSECustomerKey: b.SSECustomerKey, + SSECustomerKeySHA256: b.SSECustomerKeySHA256, + SSECustomerAlgorithm: b.SSECustomerAlgorithm, + }} + // Check to see if this state already exists. + // If we're trying to force-unlock a state, we can't take the lock before + // fetching the state. If the state doesn't exist, we have to assume this + // is a normal create operation, and take the lock at that point. + // + // If we need to force-unlock, but for some reason the state no longer + // exists, the user will have to use aws tools to manually fix the + // situation. + existing, err := b.Workspaces() + if err != nil { + return nil, err + } + + exists := false + for _, s := range existing { + if s == name { + exists = true + break + } + } + + // We need to create the object so it's listed by States. + if !exists { + // take a lock on this state while we write it + lockInfo := statemgr.NewLockInfo() + lockInfo.Operation = "init" + lockId, err := b.client.Lock(lockInfo) + if err != nil { + return nil, fmt.Errorf("failed to lock oci state: %s", err) + } + + // Local helper function so we can call it multiple places + lockUnlock := func(parent error) error { + if err := stateMgr.Unlock(lockId); err != nil { + return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) + } + return parent + } + + // Grab the value + // This is to ensure that no one beat us to writing a state between + // the `exists` check and taking the lock. + if err := stateMgr.RefreshState(); err != nil { + err = lockUnlock(err) + return nil, err + } + + // If we have no state, we have to create an empty state + if v := stateMgr.State(); v == nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { + err = lockUnlock(err) + return nil, err + } + if err := stateMgr.PersistState(nil); err != nil { + err = lockUnlock(err) + return nil, err + } + } + + // Unlock, the state should now be initialized + if err := lockUnlock(nil); err != nil { + return nil, err + } + + } + + return stateMgr, nil +} + +func (b *Backend) configureRemoteClient() error { + + configProvider, err := b.configProvider.getSdkConfigProvider() + if err != nil { + return err + } + + client, err := buildConfigureClient(configProvider, buildHttpClient()) + if err != nil { + return err + } + + b.client = &RemoteClient{ + objectStorageClient: client, + bucketName: b.bucket, + namespace: b.namespace, + kmsKeyID: b.kmsKeyID, + + SSECustomerKey: b.SSECustomerKey, + SSECustomerKeySHA256: b.SSECustomerKeySHA256, + SSECustomerAlgorithm: b.SSECustomerAlgorithm, + } + return nil +} + +func (b *Backend) Workspaces() ([]string, error) { + logger := logWithOperation("listWorkspaces") + const maxKeys = 1000 + + ctx := context.TODO() + wss := []string{backend.DefaultStateName} + start := common.String("") + if b.client == nil { + err := b.configureRemoteClient() + if err != nil { + return nil, err + } + } + for { + listObjectReq := objectstorage.ListObjectsRequest{ + BucketName: common.String(b.bucket), + NamespaceName: common.String(b.namespace), + Prefix: common.String(b.workspaceKeyPrefix), + Start: start, + Limit: common.Int(maxKeys), + } + listObjectResponse, err := b.client.objectStorageClient.ListObjects(ctx, listObjectReq) + if err != nil { + logger.Error("Failed to list workspaces in Object Storage backend: %v", err) + return nil, err + } + + for _, object := range listObjectResponse.Objects { + key := *object.Name + if strings.HasPrefix(key, b.workspaceKeyPrefix) && strings.HasSuffix(key, b.key) { + name := strings.TrimPrefix(key, b.workspaceKeyPrefix+"/") + name = strings.TrimSuffix(name, b.key) + name = strings.TrimSuffix(name, "/") + + if name != "" { + wss = append(wss, name) + } + } + } + if len(listObjectResponse.Objects) < maxKeys { + break + } + start = listObjectResponse.NextStartWith + + } + + return uniqueStrings(wss), nil +} + +func (b *Backend) DeleteWorkspace(name string, force bool) error { + + if name == backend.DefaultStateName || name == "" { + return fmt.Errorf("can't delete default state") + } + if b.client == nil { + err := b.configureRemoteClient() + if err != nil { + return err + } + } + + b.client.path = b.path(name) + b.client.lockFilePath = b.getLockFilePath(name) + return b.client.Delete() + +} diff --git a/internal/backend/remote-state/oci/backend_test.go b/internal/backend/remote-state/oci/backend_test.go new file mode 100644 index 0000000000..a67434710c --- /dev/null +++ b/internal/backend/remote-state/oci/backend_test.go @@ -0,0 +1,475 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/objectstorage" +) + +func TestBackendBasic(t *testing.T) { + testACC(t) + + ctx := context.Background() + + bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix()) + keyName := "testState.json" + namespace := getEnvSettingWithBlankDefault(NamespaceAttrName) + compartmentId := getEnvSettingWithBlankDefault("compartment_id") + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "namespace": namespace, + })).(*Backend) + + response := createOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, namespace, compartmentId) + defer deleteOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, *response.ETag, namespace) + + backend.TestBackendStates(t, b) +} +func TestBackendLocked_ForceUnlock(t *testing.T) { + testACC(t) + ctx := context.Background() + bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix()) + keyName := "testState.json" + namespace := getEnvSettingWithBlankDefault(NamespaceAttrName) + compartmentId := getEnvSettingWithBlankDefault("compartment_id") + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "namespace": namespace, + })).(*Backend) + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "namespace": namespace, + })).(*Backend) + response := createOCIBucket(ctx, t, b1.client.objectStorageClient, bucketName, namespace, compartmentId) + defer deleteOCIBucket(ctx, t, b1.client.objectStorageClient, bucketName, *response.ETag, namespace) + // Test state locking and force-unlock + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateLocksInWS(t, b1, b2, "testenv") + backend.TestBackendStateForceUnlock(t, b1, b2) + backend.TestBackendStateForceUnlockInWS(t, b1, b2, "testenv") +} +func TestBackendBasic_multipart_Upload(t *testing.T) { + testACC(t) + + ctx := context.Background() + DefaultFilePartSize = 100 // 100 Bytes + bucketName := fmt.Sprintf("terraform-remote-oci-test-%x", time.Now().Unix()) + keyName := "testState.json" + namespace := getEnvSettingWithBlankDefault(NamespaceAttrName) + compartmentId := getEnvSettingWithBlankDefault("compartment_id") + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "namespace": namespace, + })).(*Backend) + + response := createOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, namespace, compartmentId) + defer deleteOCIBucket(ctx, t, b.client.objectStorageClient, bucketName, *response.ETag, namespace) + + backend.TestBackendStates(t, b) +} + +// Helper functions to create and delete OCI bucket +func createOCIBucket(ctx context.Context, t *testing.T, client *objectstorage.ObjectStorageClient, bucketName, namespace, compartmentId string) objectstorage.CreateBucketResponse { + req := objectstorage.CreateBucketRequest{ + NamespaceName: common.String(namespace), + CreateBucketDetails: objectstorage.CreateBucketDetails{ + Name: common.String(bucketName), + CompartmentId: common.String(compartmentId), + Versioning: objectstorage.CreateBucketDetailsVersioningEnabled, + }, + } + + response, err := client.CreateBucket(ctx, req) + if err != nil { + t.Fatalf("failed to create OCI bucket: %v", err) + } + return response +} + +func deleteOCIBucket(ctx context.Context, t *testing.T, client *objectstorage.ObjectStorageClient, bucketName, etag, namespace string) { + request := objectstorage.ListObjectVersionsRequest{ + BucketName: common.String(bucketName), + NamespaceName: common.String(namespace), + Prefix: common.String(""), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + response, err := client.ListObjectVersions(context.Background(), request) + if err != nil { + t.Fatalf("failed to list(First page) OCI bucket objects: %v", err) + } + + request.Page = response.OpcNextPage + + for request.Page != nil { + request.RequestMetadata.RetryPolicy = getDefaultRetryPolicy() + + listResponse, err := client.ListObjectVersions(context.Background(), request) + if err != nil { + t.Fatalf("failed to list OCI bucket objects: %v", err) + } + response.Items = append(response.Items, listResponse.Items...) + request.Page = listResponse.OpcNextPage + } + + var diagErr tfdiags.Diagnostics + + for _, objectVersion := range response.Items { + + deleteObjectVersionRequest := objectstorage.DeleteObjectRequest{ + BucketName: common.String(bucketName), + NamespaceName: common.String(namespace), + ObjectName: objectVersion.Name, + VersionId: objectVersion.VersionId, + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + _, err := client.DeleteObject(context.Background(), deleteObjectVersionRequest) + if err != nil { + diagErr = diagErr.Append(err) + } + } + if diagErr != nil { + t.Fatalf("error while deleting object from bucket: %v", diagErr.Err()) + } + + req := objectstorage.DeleteBucketRequest{ + NamespaceName: common.String(namespace), + BucketName: common.String(bucketName), + IfMatch: common.String(etag), + } + + _, err = client.DeleteBucket(ctx, req) + if err != nil { + t.Fatalf("failed to delete OCI bucket: %v", err) + } +} + +// verify that we are doing ACC tests or the oci backend tests specifically +func testACC(t *testing.T) { + skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_OCI_BACKEND_TEST") == "" + if skip { + t.Log("oci backend tests require setting TF_ACC or TF_OCI_BACKEND_TEST") + t.Skip() + } +} + +func TestOCIBackendConfig_PrepareConfigValidation(t *testing.T) { + cases := map[string]struct { + config cty.Value + expectedDiags tfdiags.Diagnostics + mock func() + }{ + "null bucket": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.NullVal(cty.String), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + }), + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName)), + }, + }, + "empty bucket": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal(""), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + }), + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(cty.GetAttrPath(BucketAttrName)), + }, + }, + "null namespace": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.NullVal(cty.String), + KeyAttrName: cty.StringVal("test-key"), + }), + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(cty.GetAttrPath("namespace")), + }, + }, + "empty namespace": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal(""), + KeyAttrName: cty.StringVal("test-key"), + }), + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(cty.GetAttrPath("namespace")), + }, + }, + "key with leading slash": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("/leading-slash"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/" and also not contain consecutive "/"`, + cty.GetAttrPath(KeyAttrName), + ), + }, + }, + "key with trailing slash": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("trailing-slash/"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/" and also not contain consecutive "/"`, + cty.GetAttrPath(KeyAttrName), + ), + }, + }, + "key with double slash": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test/with/double//slash"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/" and also not contain consecutive "/"`, + cty.GetAttrPath(KeyAttrName), + ), + }, + }, + "workspace_key_prefix with leading slash": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + WorkspaceKeyPrefixAttrName: cty.StringVal("/env"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start with "/" and also not contain consecutive "/"`, + cty.GetAttrPath(WorkspaceKeyPrefixAttrName), + ), + }, + }, + "encryption key conflict": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + KmsKeyIdAttrName: cty.StringVal("ocid1.key.oc1..example"), + SseCustomerKeyAttrName: cty.StringVal("base64-encoded-key"), + SseCustomerKeySHA256AttrName: cty.StringVal("base64-encoded-key md5"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + `Only one of kms_key_id, sse_customer_key can be set.`, + cty.GetAttrPath(KmsKeyIdAttrName), + ), + }, + }, + "Invalid encryption key combination": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + SseCustomerKeyAttrName: cty.StringVal("base64-encoded-key"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + ` sse_customer_key and its SHA both required.`, + cty.GetAttrPath(SseCustomerKeySHA256AttrName)), + }, + }, + "private_key and private_key_path conflict": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + PrivateKeyAttrName: cty.StringVal("-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"), + PrivateKeyPathAttrName: cty.StringVal("/path/to/key.pem"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + `Only one of private_key, private_key_path can be set.`, + cty.GetAttrPath(PrivateKeyPathAttrName), + ), + }, + }, + "invalid auth method": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + AuthAttrName: cty.StringVal("invalid-auth"), + }), + expectedDiags: tfdiags.Diagnostics{ + tfdiags.AttributeValue(tfdiags.Error, + "Invalid authentication method", + fmt.Sprintf("auth must be one of '%s' or '%s' or '%s' or '%s' or '%s' or '%s'", AuthAPIKeySetting, AuthInstancePrincipalSetting, AuthInstancePrincipalWithCertsSetting, AuthSecurityToken, ResourcePrincipal, AuthOKEWorkloadIdentity), cty.GetAttrPath(AuthAttrName), + ), + }, + }, + "missing region for InstancePrinciple auth": { + config: cty.ObjectVal(map[string]cty.Value{ + BucketAttrName: cty.StringVal("test-bucket"), + NamespaceAttrName: cty.StringVal("test-namespace"), + KeyAttrName: cty.StringVal("test-key"), + AuthAttrName: cty.StringVal(AuthInstancePrincipalSetting), + }), + expectedDiags: tfdiags.Diagnostics{ + tfdiags.AttributeValue(tfdiags.Error, + "Missing region attribute required", + fmt.Sprintf("The attribute %q is required by the backend for %s authentication.\n\n", RegionAttrName, AuthInstancePrincipalSetting), cty.GetAttrPath(RegionAttrName), + ), + }, + mock: func() { + os.Setenv("OCI_region", "") + os.Setenv("region", "") + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + + b := New() + if tc.mock != nil { + tc.mock() + } + _, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config)) + + if diff := cmp.Diff(valDiags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } + +} + +func populateSchema(t *testing.T, schema *configschema.Block, value cty.Value) cty.Value { + ty := schema.ImpliedType() + var path cty.Path + val, err := unmarshal(value, ty, path) + if err != nil { + t.Fatalf("populating schema: %s", err) + } + return val +} + +func unmarshal(value cty.Value, ty cty.Type, path cty.Path) (cty.Value, error) { + switch { + case ty.IsPrimitiveType(): + return value, nil + // case ty.IsListType(): + // return unmarshalList(value, ty.ElementType(), path) + case ty.IsSetType(): + return unmarshalSet(value, ty.ElementType(), path) + case ty.IsMapType(): + return unmarshalMap(value, ty.ElementType(), path) + // case ty.IsTupleType(): + // return unmarshalTuple(value, ty.TupleElementTypes(), path) + case ty.IsObjectType(): + return unmarshalObject(value, ty.AttributeTypes(), path) + default: + return cty.NilVal, path.NewErrorf("unsupported type %s", ty.FriendlyName()) + } +} + +func unmarshalSet(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) { + if dec.IsNull() { + return dec, nil + } + + length := dec.LengthInt() + + if length == 0 { + return cty.SetValEmpty(ety), nil + } + + vals := make([]cty.Value, 0, length) + dec.ForEachElement(func(key, val cty.Value) (stop bool) { + vals = append(vals, val) + return + }) + + return cty.SetVal(vals), nil +} +func unmarshalMap(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) { + if dec.IsNull() { + return dec, nil + } + + length := dec.LengthInt() + + if length == 0 { + return cty.MapValEmpty(ety), nil + } + + vals := make(map[string]cty.Value, length) + dec.ForEachElement(func(key, val cty.Value) (stop bool) { + vals[key.AsString()] = val + return + }) + + return cty.MapVal(vals), nil +} + +func unmarshalObject(dec cty.Value, atys map[string]cty.Type, path cty.Path) (cty.Value, error) { + if dec.IsNull() { + return dec, nil + } + valueTy := dec.Type() + + vals := make(map[string]cty.Value, len(atys)) + path = append(path, nil) + for key, aty := range atys { + path[len(path)-1] = cty.IndexStep{ + Key: cty.StringVal(key), + } + + if !valueTy.HasAttribute(key) { + vals[key] = cty.NullVal(aty) + } else { + val, err := unmarshal(dec.GetAttr(key), aty, path) + if err != nil { + return cty.DynamicVal, err + } + vals[key] = val + } + } + + return cty.ObjectVal(vals), nil +} diff --git a/internal/backend/remote-state/oci/client.go b/internal/backend/remote-state/oci/client.go new file mode 100644 index 0000000000..beb24ae04a --- /dev/null +++ b/internal/backend/remote-state/oci/client.go @@ -0,0 +1,355 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/objectstorage" +) + +type RemoteClient struct { + objectStorageClient *objectstorage.ObjectStorageClient + namespace string + bucketName string + path string + lockFilePath string + kmsKeyID string + SSECustomerKey string + SSECustomerKeySHA256 string + SSECustomerAlgorithm string +} + +func (c *RemoteClient) Get() (*remote.Payload, error) { + logger := logWithOperation("download-state-file").Named(c.path) + logger.Info("Downloading remote state") + ctx := context.WithValue(context.Background(), "logger", logger) + payload, err := c.getObject(ctx) + if err != nil || len(payload.Data) == 0 { + return nil, err + } + // md5 hash of whole state + sum := md5.Sum(payload.Data) + payload.MD5 = sum[:] + return payload, nil +} + +func (c *RemoteClient) getObject(ctx context.Context) (*remote.Payload, error) { + logger := ctx.Value("logger").(hclog.Logger) + headRequest := objectstorage.HeadObjectRequest{ + NamespaceName: common.String(c.namespace), + ObjectName: common.String(c.path), + BucketName: common.String(c.bucketName), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + if c.SSECustomerKey != "" && c.SSECustomerKeySHA256 != "" { + headRequest.OpcSseCustomerKey = common.String(c.SSECustomerKey) + headRequest.OpcSseCustomerKeySha256 = common.String(c.SSECustomerKeySHA256) + headRequest.OpcSseCustomerAlgorithm = common.String(c.SSECustomerAlgorithm) + } + // Get object from OCI + headResponse, headErr := c.objectStorageClient.HeadObject(ctx, headRequest) + if headErr != nil { + var ociHeadErr common.ServiceError + if errors.As(headErr, &ociHeadErr) && ociHeadErr.GetHTTPStatusCode() == 404 { + logger.Debug(" State file '%s' not found. Initializing Terraform state...", c.path) + return &remote.Payload{}, nil + } else { + return nil, fmt.Errorf("failed to access object '%s' in bucket '%s': %w", c.path, c.bucketName, headErr) + } + } + + getRequest := objectstorage.GetObjectRequest{ + NamespaceName: common.String(c.namespace), + ObjectName: common.String(c.path), + BucketName: common.String(c.bucketName), + IfMatch: headResponse.ETag, + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + if c.SSECustomerKey != "" && c.SSECustomerKeySHA256 != "" { + getRequest.OpcSseCustomerKey = common.String(c.SSECustomerKey) + getRequest.OpcSseCustomerKeySha256 = common.String(c.SSECustomerKeySHA256) + getRequest.OpcSseCustomerAlgorithm = common.String(c.SSECustomerAlgorithm) + } + // Get object from OCI + getResponse, err := c.objectStorageClient.GetObject(ctx, getRequest) + if err != nil { + var ociErr common.ServiceError + if errors.As(err, &ociErr) { + return nil, fmt.Errorf("failed to access object HttpStatusCode: %d\nOpcRequestId: %s\n message: %s\n ErrorCode: %s", ociErr.GetHTTPStatusCode(), ociErr.GetOpcRequestID(), ociErr.GetMessage(), ociErr.GetCode()) + + } + return nil, fmt.Errorf("failed to access object '%s' in bucket '%s': %w", c.path, c.bucketName, err) + } + defer getResponse.Content.Close() + + // Read object content + contentArray, err := io.ReadAll(getResponse.Content) + if err != nil { + return nil, fmt.Errorf("unable to read 'content' from response: %w", err) + } + + // Compute MD5 hash + md5Hash := getResponse.ContentMd5 + if md5Hash == nil || len(*md5Hash) == 0 { + md5Hash = getResponse.OpcMultipartMd5 + } + // Construct payload + payload := &remote.Payload{ + Data: contentArray, + MD5: []byte(*md5Hash), + } + + // Return an error instead of `nil, nil` if the object is empty + if len(payload.Data) == 0 { + return nil, fmt.Errorf("object %q is empty", c.path) + } + + return payload, nil +} + +func (c *RemoteClient) Put(data []byte) error { + logger := logWithOperation("upload-state-file").Named(c.path) + ctx := context.WithValue(context.Background(), "logger", logger) + dataSize := int64(len(data)) + sum := md5.Sum(data) + var err error + if dataSize > DefaultFilePartSize { + logger.Info("Using Multipart Feature") + var multipartUploadData = MultipartUploadData{ + client: c, + Data: data, + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + err = multipartUploadData.multiPartUploadImpl(ctx) + if err != nil && dataSize <= MaxFilePartSize { + logger.Error(fmt.Sprintf("Multipart upload failed, falling back to single part upload: %v", err)) + err = c.uploadSinglePartObject(ctx, data, sum[:]) + } + } else { + err = c.uploadSinglePartObject(ctx, data, sum[:]) + } + if err != nil { + return err + } + + return nil +} + +func (c *RemoteClient) uploadSinglePartObject(ctx context.Context, data, sum []byte) error { + logger := ctx.Value("logger").(hclog.Logger).Named("singlePartUpload") + logger.Info("Uploading single part object") + if len(data) == 0 { + return fmt.Errorf("uploadSinglePartObject: data is empty") + } + + contentType := "application/json" + + putRequest := objectstorage.PutObjectRequest{ + ContentType: common.String(contentType), + NamespaceName: common.String(c.namespace), + ObjectName: common.String(c.path), + BucketName: common.String(c.bucketName), + PutObjectBody: io.NopCloser(bytes.NewReader(data)), + ContentMD5: common.String(base64.StdEncoding.EncodeToString(sum)), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + // Handle encryption settings + if c.kmsKeyID != "" { + putRequest.OpcSseKmsKeyId = common.String(c.kmsKeyID) + } else if c.SSECustomerKey != "" && c.SSECustomerKeySHA256 != "" { + putRequest.OpcSseCustomerKey = common.String(c.SSECustomerKey) + putRequest.OpcSseCustomerKeySha256 = common.String(c.SSECustomerKeySHA256) + putRequest.OpcSseCustomerAlgorithm = common.String(c.SSECustomerAlgorithm) + } + + logger.Info(fmt.Sprintf("Uploading remote state: %s", c.path)) + + putResponse, err := c.objectStorageClient.PutObject(ctx, putRequest) + if err != nil { + return fmt.Errorf("failed to upload object: %w", err) + } + + logger.Info("Uploaded state file response: %+v\n", putResponse) + return nil +} + +func (c *RemoteClient) Delete() error { + + return c.DeleteAllObjectVersions() +} +func (c *RemoteClient) DeleteAllObjectVersions() error { + request := objectstorage.ListObjectVersionsRequest{ + BucketName: common.String(c.bucketName), + NamespaceName: common.String(c.namespace), + Prefix: common.String(c.path), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + response, err := c.objectStorageClient.ListObjectVersions(context.Background(), request) + if err != nil { + return err + } + + request.Page = response.OpcNextPage + + for request.Page != nil { + request.RequestMetadata.RetryPolicy = getDefaultRetryPolicy() + + listResponse, err := c.objectStorageClient.ListObjectVersions(context.Background(), request) + if err != nil { + return err + } + response.Items = append(response.Items, listResponse.Items...) + request.Page = listResponse.OpcNextPage + } + + var diagErr tfdiags.Diagnostics + + for _, objectVersion := range response.Items { + + deleteObjectVersionRequest := objectstorage.DeleteObjectRequest{ + BucketName: common.String(c.bucketName), + NamespaceName: common.String(c.namespace), + ObjectName: objectVersion.Name, + VersionId: objectVersion.VersionId, + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + _, err := c.objectStorageClient.DeleteObject(context.Background(), deleteObjectVersionRequest) + if err != nil { + diagErr = diagErr.Append(err) + } + } + if diagErr != nil { + return diagErr.Err() + } + + return nil +} + +func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { + logger := logWithOperation("lock-state-file").Named(c.lockFilePath) + logger.Info("Locking remote state") + ctx := context.TODO() + info.Path = c.path + infoBytes, err := json.Marshal(info) + if err != nil { + return "", err + } + + putObjReq := objectstorage.PutObjectRequest{ + BucketName: common.String(c.bucketName), + NamespaceName: common.String(c.namespace), + ObjectName: common.String(c.lockFilePath), + IfNoneMatch: common.String("*"), + PutObjectBody: io.NopCloser(bytes.NewReader(infoBytes)), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + putResponse, putErr := c.objectStorageClient.PutObject(ctx, putObjReq) + if putErr != nil { + lockInfo, _, err := c.getLockInfo(ctx) + if err != nil { + putErr = errors.Join(putErr, err) + } + return "", &statemgr.LockError{ + Err: putErr, + Info: lockInfo, + } + } + logger.Info("state lock response code: %+d\n", putResponse.RawResponse.StatusCode) + return info.ID, nil + +} + +// getLockInfo retrieves and parses a lock file from an oci bucket. +func (c *RemoteClient) getLockInfo(ctx context.Context) (*statemgr.LockInfo, string, error) { + // Attempt to retrieve the lock file from + getRequest := objectstorage.GetObjectRequest{ + NamespaceName: common.String(c.namespace), + ObjectName: common.String(c.lockFilePath), + BucketName: common.String(c.bucketName), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + getResponse, err := c.objectStorageClient.GetObject(ctx, getRequest) + if err != nil { + return nil, "", fmt.Errorf("failed to get existing lock file: %w", err) + } + lockByteData, err := io.ReadAll(getResponse.Content) + if err != nil { + return nil, *getResponse.ETag, fmt.Errorf("failed to read existing lock file content: %w", err) + } + lockInfo := &statemgr.LockInfo{} + if err := json.Unmarshal(lockByteData, lockInfo); err != nil { + return lockInfo, "", fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err) + } + return lockInfo, *getResponse.ETag, nil +} +func (c *RemoteClient) Unlock(id string) error { + ctx := context.TODO() + logger := logWithOperation("unlock-state-file").Named(c.lockFilePath) + logger.Info("unlocking remote state") + lockInfo, etag, err := c.getLockInfo(ctx) + + if err != nil { + return fmt.Errorf("Failed to retrieve lock information from OCI Object Storage: %w", err) + } + // Verify that the provided lock ID matches the lock ID of the retrieved lock file. + if lockInfo.ID != id { + return &statemgr.LockError{ + Info: lockInfo, + Err: fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID), + } + } + + deleteRequest := objectstorage.DeleteObjectRequest{ + NamespaceName: common.String(c.namespace), + ObjectName: common.String(c.lockFilePath), + BucketName: common.String(c.bucketName), + IfMatch: common.String(etag), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + deleteResponse, err := c.objectStorageClient.DeleteObject(ctx, deleteRequest) + if err != nil { + return &statemgr.LockError{ + Info: lockInfo, + Err: err, + } + } + logger.Info("Unlock response: %v\n", deleteResponse.RawResponse.StatusCode) + return nil +} diff --git a/internal/backend/remote-state/oci/constants.go b/internal/backend/remote-state/oci/constants.go new file mode 100644 index 0000000000..5f66ef052a --- /dev/null +++ b/internal/backend/remote-state/oci/constants.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import "time" + +const ( + AuthAPIKeySetting = "ApiKey" + AuthInstancePrincipalSetting = "InstancePrincipal" + AuthInstancePrincipalWithCertsSetting = "InstancePrincipalWithCerts" + AuthSecurityToken = "SecurityToken" + AuthOKEWorkloadIdentity = "OKEWorkloadIdentity" + ResourcePrincipal = "ResourcePrincipal" + + OciEnvPrefix = "OCI_" + defaultWorkspaceEnvPrefix = "tf-state-env" + + AuthAttrName = "auth" + TenancyOcidAttrName = "tenancy_ocid" + UserOcidAttrName = "user_ocid" + FingerprintAttrName = "fingerprint" + PrivateKeyAttrName = "private_key" + PrivateKeyPathAttrName = "private_key_path" + PrivateKeyPasswordAttrName = "private_key_password" + RegionAttrName = "region" + WorkspaceKeyPrefixAttrName = "workspace_key_prefix" + KmsKeyIdAttrName = "kms_key_id" + SseCustomerKeyAttrName = "sse_customer_key" + SseCustomerKeySHA256AttrName = "sse_customer_key_sha256" + SseCustomerAlgorithmAttrName = "sse_customer_algorithm" + DefaultAlgorithm = "AES256" + + KeyAttrName = "key" + defaultKeyValue = "terraform.tfstate" + BucketAttrName = "bucket" + NamespaceAttrName = "namespace" + + ConfigFileProfileAttrName = "config_file_profile" + + AcceptLocalCerts = "accept_local_certs" + + // HTTPRequestTimeout specifies the maximum duration for completing an HTTP request. + HTTPRequestTimeOut = "HTTP_REQUEST_TIMEOUT" + DefaultRequestTimeout = 0 + // DialContextConnectionTimeout defines the timeout for establishing a connection during a network dial operation. + DialContextConnectionTimeout = "DIAL_CONTEXT_CONNECTION_TIMEOUT" + DefaultConnectionTimeout = 10 * time.Second + // TLSHandshakeTimeout indicates the maximum time allowed for the TLS handshake process. + TLSHandshakeTimeout = "TLS_HANDSHAKE_TIMEOUT" + DefaultTLSHandshakeTimeout = 10 * time.Second + + OboTokenAttrName = "obo_token" + OboTokenPath = "obo_token_path" + DefaultConfigFileName = "config" + DefaultConfigDirName = ".oci" + + UserAgentTerraformNameEnv = "OCI_APPEND_USER_AGENT" + UserAgentSDKNameEnv = "OCI_SDK_APPEND_USER_AGENT" + DefaultUserAgentBackendName = "Oracle-TerraformBackend" + UserAgentFormatter = "Oracle-GoSDK/%s (go/%s; %s/%s; terraform-cli/%s) %s" + RequestHeaderOpcOboToken = "opc-obo-token" + DomainNameOverrideEnv = "domain_name_override" + HasCorrectDomainNameEnv = "has_correct_domain_name" + ClientHostOverridesEnv = "CLIENT_HOST_OVERRIDES" + CustomCertLocationEnv = "custom_cert_location" + + ColonDelimiter = ";" + EqualToOperatorDelimiter = "=" + DotDelimiter = "." + ObjectStorageClientName = "oci_object_storage.ObjectStorageClient" +) diff --git a/internal/backend/remote-state/oci/go.mod b/internal/backend/remote-state/oci/go.mod new file mode 100644 index 0000000000..d79a1d1832 --- /dev/null +++ b/internal/backend/remote-state/oci/go.mod @@ -0,0 +1,46 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/oci + +go 1.24.2 + +require ( + github.com/google/go-cmp v0.7.0 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/oracle/oci-go-sdk/v65 v65.89.1 + github.com/zclconf/go-cty v1.16.2 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/gofrs/flock v0.10.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/sony/gobreaker v0.5.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect +) + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/oci/go.sum b/internal/backend/remote-state/oci/go.sum new file mode 100644 index 0000000000..a0e9230c8b --- /dev/null +++ b/internal/backend/remote-state/oci/go.sum @@ -0,0 +1,640 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/flock v0.10.0 h1:SHMXenfaB03KbroETaCMtbBg3Yn29v4w1r+tgy4ff4k= +github.com/gofrs/flock v0.10.0/go.mod h1:FirDy1Ing0mI2+kB6wk+vyyAH+e6xiE+EYA0jnzV9jc= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/oracle/oci-go-sdk/v65 v65.89.1 h1:8sVjxYPNQ83yqUgZKkdeUA0CnSodmL1Bme2oxq8gyKg= +github.com/oracle/oci-go-sdk/v65 v65.89.1/go.mod h1:u6XRPsw9tPziBh76K7GrrRXPa8P8W3BQeqJ6ZZt9VLA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/oci/log.go b/internal/backend/remote-state/oci/log.go new file mode 100644 index 0000000000..b9a09802ee --- /dev/null +++ b/internal/backend/remote-state/oci/log.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "sync" + + hclog "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/internal/logging" + "github.com/oracle/oci-go-sdk/v65/common" +) + +var ( + loggerFunc = sync.OnceValue(func() hclog.Logger { + l := logging.HCLogger() + return l.Named("backend-oracle_oci") + }) +) + +type backendLogger struct { + hclog.Logger +} + +func setSDKLogger() { + sdklogger := NewBackendLogger(loggerFunc().With("component", "oci-go-sdk")) + common.SetSDKLogger(sdklogger) +} +func NewBackendLogger(l hclog.Logger) backendLogger { + return backendLogger{l} +} + +// This fuction is needed for oci-go-sdk +func (l backendLogger) LogLevel() int { + return int(l.Logger.GetLevel()) +} +func (l backendLogger) Log(logLevel int, format string, v ...interface{}) error { + l.Logger.Log(hclog.Level(logLevel), format, v...) + return nil +} +func logWithOperation(operation string) hclog.Logger { + log := loggerFunc().With( + "operation", operation, + ) + if id, err := uuid.GenerateUUID(); err == nil { + log = log.With( + "req_id", id, + ) + + } + return log +} diff --git a/internal/backend/remote-state/oci/multipart_upload.go b/internal/backend/remote-state/oci/multipart_upload.go new file mode 100644 index 0000000000..fcda59fde6 --- /dev/null +++ b/internal/backend/remote-state/oci/multipart_upload.go @@ -0,0 +1,252 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "bytes" + "context" + "crypto/md5" + "encoding/base64" + "fmt" + "io" + "sync" + + "github.com/hashicorp/go-hclog" + "github.com/oracle/oci-go-sdk/v65/objectstorage" + + "github.com/oracle/oci-go-sdk/v65/common" +) + +var DefaultFilePartSize int64 = 128 * 1024 * 1024 // 128MB +const MaxFilePartSize int64 = 50 * 1024 * 1024 * 1024 // 50GB +const defaultNumberOfGoroutines = 10 +const MaxCount int64 = 10000 + +type MultipartUploadData struct { + client *RemoteClient + Data []byte + RequestMetadata common.RequestMetadata +} + +type objectStorageUploadPartResponse struct { + response objectstorage.UploadPartResponse + partNumber *int + error error +} + +type objectStorageMultiPartUploadContext struct { + client *RemoteClient + sourceBlocks chan objectStorageSourceBlock + osUploadPartResponses chan objectStorageUploadPartResponse + wg *sync.WaitGroup + errChan chan error + multipartUploadResponse objectstorage.CreateMultipartUploadResponse + multipartUploadRequest objectstorage.CreateMultipartUploadRequest + logger hclog.Logger +} + +type objectStorageSourceBlock struct { + section *io.SectionReader + blockNumber *int +} + +func (multipartUploadData MultipartUploadData) multiPartUploadImpl(ctx context.Context) error { + logger := ctx.Value("logger").(hclog.Logger).Named("multiPartUpload") + sourceBlocks, err := multipartUploadData.objectMultiPartSplit() + if err != nil { + return fmt.Errorf("error splitting source data: %s", err) + } + + multipartUploadRequest := &objectstorage.CreateMultipartUploadRequest{ + NamespaceName: common.String(multipartUploadData.client.namespace), + BucketName: common.String(multipartUploadData.client.bucketName), + RequestMetadata: multipartUploadData.RequestMetadata, + CreateMultipartUploadDetails: objectstorage.CreateMultipartUploadDetails{ + Object: common.String(multipartUploadData.client.path), + }, + } + if multipartUploadData.client.kmsKeyID != "" { + multipartUploadRequest.OpcSseKmsKeyId = common.String(multipartUploadData.client.kmsKeyID) + } else if multipartUploadData.client.SSECustomerKey != "" && multipartUploadData.client.SSECustomerKeySHA256 != "" { + multipartUploadRequest.OpcSseCustomerKey = common.String(multipartUploadData.client.SSECustomerKey) + multipartUploadRequest.OpcSseCustomerKeySha256 = common.String(multipartUploadData.client.SSECustomerKeySHA256) + multipartUploadRequest.OpcSseCustomerAlgorithm = common.String(multipartUploadData.client.SSECustomerAlgorithm) + } + + multipartUploadResponse, err := multipartUploadData.client.objectStorageClient.CreateMultipartUpload(context.Background(), *multipartUploadRequest) + if err != nil { + return fmt.Errorf("error creating multipart upload: %s", err) + } + + workerCount := defaultNumberOfGoroutines + osUploadPartResponses := make(chan objectStorageUploadPartResponse, len(sourceBlocks)) + sourceBlocksChan := make(chan objectStorageSourceBlock, len(sourceBlocks)) + + wg := &sync.WaitGroup{} + wg.Add(len(sourceBlocks)) + + // Push all source blocks into the channel + for _, sourceBlock := range sourceBlocks { + sourceBlocksChan <- sourceBlock + } + close(sourceBlocksChan) + errChan := make(chan error, workerCount) + // Start workers + for i := 0; i < workerCount; i++ { + go func() { + ctx := &objectStorageMultiPartUploadContext{ + client: multipartUploadData.client, + wg: wg, + errChan: errChan, + multipartUploadResponse: multipartUploadResponse, + multipartUploadRequest: *multipartUploadRequest, + sourceBlocks: sourceBlocksChan, + osUploadPartResponses: osUploadPartResponses, + logger: logger, + } + ctx.uploadPartsWorker() + }() + } + + wg.Wait() + close(osUploadPartResponses) + close(errChan) + + // Collect errors from workers + for workerErr := range errChan { + if workerErr != nil { + return workerErr + } + } + commitMultipartUploadPartDetails := make([]objectstorage.CommitMultipartUploadPartDetails, len(sourceBlocks)) + i := 0 + for response := range osUploadPartResponses { + if response.error != nil || response.partNumber == nil || response.response.ETag == nil { + return fmt.Errorf("failed to upload part: %s", response.error) + } + partNumber, etag := *response.partNumber, *response.response.ETag + commitMultipartUploadPartDetails[i] = objectstorage.CommitMultipartUploadPartDetails{ + PartNum: common.Int(partNumber), + Etag: common.String(etag), + } + i++ + } + + if len(commitMultipartUploadPartDetails) != len(sourceBlocks) { + abortReq := objectstorage.AbortMultipartUploadRequest{ + UploadId: multipartUploadResponse.MultipartUpload.UploadId, + NamespaceName: multipartUploadResponse.Namespace, + BucketName: multipartUploadResponse.Bucket, + ObjectName: multipartUploadResponse.Object, + } + _, abortErr := multipartUploadData.client.objectStorageClient.AbortMultipartUpload(context.Background(), abortReq) + if abortErr != nil { + logger.Error(fmt.Sprintf("Failed to abort multipart upload: %s", abortErr)) + } + return fmt.Errorf("not all parts uploaded successfully, multipart upload aborted") + } + + commitMultipartUploadRequest := objectstorage.CommitMultipartUploadRequest{ + UploadId: multipartUploadResponse.MultipartUpload.UploadId, + NamespaceName: multipartUploadResponse.Namespace, + BucketName: multipartUploadResponse.Bucket, + ObjectName: multipartUploadResponse.Object, + OpcClientRequestId: multipartUploadResponse.OpcClientRequestId, + RequestMetadata: multipartUploadRequest.RequestMetadata, + CommitMultipartUploadDetails: objectstorage.CommitMultipartUploadDetails{ + PartsToCommit: commitMultipartUploadPartDetails, + }, + } + _, err = multipartUploadData.client.objectStorageClient.CommitMultipartUpload(context.Background(), commitMultipartUploadRequest) + if err != nil { + return fmt.Errorf("failed to commit multipart upload: %s", err) + } + + return nil +} +func (m MultipartUploadData) objectMultiPartSplit() ([]objectStorageSourceBlock, error) { + dataSize := int64(len(m.Data)) + offsets, partSize, err := SplitSizeToOffsetsAndLimits(dataSize) + if err != nil { + return nil, fmt.Errorf("error splitting data into parts: %s", err) + } + sourceBlocks := make([]objectStorageSourceBlock, len(offsets)) + for i := range offsets { + start := offsets[i] + end := start + partSize + if end > dataSize { + end = dataSize + } + sourceBlocks[i] = objectStorageSourceBlock{ + section: io.NewSectionReader(bytes.NewReader(m.Data), start, end-start), + blockNumber: common.Int(i + 1), + } + } + return sourceBlocks, nil +} + +/* +SplitSizeToOffsetsAndLimits splits a file size into chunks based on DefaultFilePartSize. +Returns the byte offsets and byte limits for each chunk. +Returns an error if the size exceeds MaxCount parts. +*/ +func SplitSizeToOffsetsAndLimits(size int64) ([]int64, int64, error) { + partSize := DefaultFilePartSize + totalParts := (size + partSize - 1) / partSize + if totalParts > MaxCount { + return nil, 0, fmt.Errorf("file exceeds maximum part count") + } + offsets := make([]int64, totalParts) + for i := range offsets { + offsets[i] = int64(i) * partSize + } + return offsets, partSize, nil +} + +func (ctx *objectStorageMultiPartUploadContext) uploadPartsWorker() { + for block := range ctx.sourceBlocks { + buffer := make([]byte, block.section.Size()) + _, err := block.section.Read(buffer) + if err != nil { + ctx.errChan <- fmt.Errorf("error reading source block %d: %w", block.blockNumber, err) + return + } + tmpLength := int64(len(buffer)) + sum := md5.Sum(buffer) + uploadPartRequest := &objectstorage.UploadPartRequest{ + UploadId: ctx.multipartUploadResponse.UploadId, + ObjectName: ctx.multipartUploadResponse.Object, + NamespaceName: ctx.multipartUploadResponse.Namespace, + BucketName: ctx.multipartUploadResponse.Bucket, + ContentLength: &tmpLength, + UploadPartBody: io.NopCloser(bytes.NewReader(buffer)), + UploadPartNum: block.blockNumber, + ContentMD5: common.String(base64.StdEncoding.EncodeToString(sum[:])), + RequestMetadata: common.RequestMetadata{ + RetryPolicy: getDefaultRetryPolicy(), + }, + } + + if ctx.client.kmsKeyID != "" { + uploadPartRequest.OpcSseKmsKeyId = common.String(ctx.client.kmsKeyID) + } else if ctx.client.SSECustomerKey != "" && ctx.client.SSECustomerKeySHA256 != "" { + uploadPartRequest.OpcSseCustomerKey = common.String(ctx.client.SSECustomerKey) + uploadPartRequest.OpcSseCustomerKeySha256 = common.String(ctx.client.SSECustomerKeySHA256) + uploadPartRequest.OpcSseCustomerAlgorithm = common.String(ctx.client.SSECustomerAlgorithm) + } + + response, err := ctx.client.objectStorageClient.UploadPart(context.Background(), *uploadPartRequest) + if err != nil { + ctx.errChan <- fmt.Errorf("failed to upload part %d: %w", *block.blockNumber, err) + return + } + ctx.osUploadPartResponses <- objectStorageUploadPartResponse{ + response: response, + error: nil, + partNumber: block.blockNumber, + } + ctx.wg.Done() + + } +} diff --git a/internal/backend/remote-state/oci/retry.go b/internal/backend/remote-state/oci/retry.go new file mode 100644 index 0000000000..eb4dd402a4 --- /dev/null +++ b/internal/backend/remote-state/oci/retry.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "net" + "strings" + "time" + + "github.com/oracle/oci-go-sdk/v65/common" +) + +var ( + LongRetryTime = 10 * time.Minute + RetryableStatus = map[int]bool{ + 429: true, // Too Many Requests + 500: true, // Internal Server Error + 503: true, // Service Unavailable + } +) + +func getDefaultRetryPolicy() *common.RetryPolicy { + startTime := time.Now() + return &common.RetryPolicy{ + MaximumNumberAttempts: 5, + ShouldRetryOperation: func(response common.OCIOperationResponse) bool { + return shouldRetry(response, startTime) + }, + NextDuration: func(response common.OCIOperationResponse) time.Duration { + return getRetryBackoffDuration(response, startTime) + }, + } +} + +func shouldRetry(response common.OCIOperationResponse, startTime time.Time) bool { + if elapsed := time.Since(startTime); elapsed > LongRetryTime { + return false + } + if response.Response == nil || response.Response.HTTPResponse() == nil { + return false + } + + statusCode := response.Response.HTTPResponse().StatusCode + if RetryableStatus[statusCode] { + return true + } + return response.Error != nil && isNetworkError(response.Error) +} + +func getRetryBackoffDuration(response common.OCIOperationResponse, startTime time.Time) time.Duration { + attempt := response.AttemptNumber + if attempt > 5 { + attempt = 5 + } + return time.Duration(2*attempt*attempt) * time.Second +} + +func isNetworkError(err error) bool { + if netErr, ok := err.(net.Error); ok && (netErr.Timeout() || netErr.Temporary()) { + return true + } + return strings.Contains(err.Error(), "i/o timeout") +} diff --git a/internal/backend/remote-state/oci/util.go b/internal/backend/remote-state/oci/util.go new file mode 100644 index 0000000000..e0ae624c96 --- /dev/null +++ b/internal/backend/remote-state/oci/util.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package oci + +import ( + "fmt" + "os" + "path" + "regexp" + "strings" + "time" + + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func getEnvSettingWithBlankDefault(s string) string { + return getEnvSettingWithDefault(s, "") +} + +func getEnvSettingWithDefault(s string, dv string) string { + v := os.Getenv(OciEnvPrefix + s) + if v != "" { + return v + } + v = os.Getenv(s) + if v != "" { + return v + } + return dv +} +func getDurationFromEnvVar(varName string, defaultValue time.Duration) time.Duration { + valueStr := getEnvSettingWithDefault(varName, fmt.Sprint(defaultValue)) + duration, err := time.ParseDuration(valueStr) + if err != nil { + loggerFunc().Error("ERROR while parsing env variable %s value: %v", varName, err) + return defaultValue + } + return duration +} +func getHomeFolder() string { + if os.Getenv("OCI_HOME_OVERRIDE") != "" { + return os.Getenv("OCI_HOME_OVERRIDE") + } + home, err := os.UserHomeDir() + if err != nil { + loggerFunc().Error("ERROR while getting home directory: %v", err) + return "" + } + return home +} +func checkProfile(profile string, path string) (err error) { + var profileRegex = regexp.MustCompile(`^\[(.*)\]`) + data, err := os.ReadFile(path) + if err != nil { + return err + } + content := string(data) + splitContent := strings.Split(content, "\n") + for _, line := range splitContent { + if match := profileRegex.FindStringSubmatch(line); match != nil && len(match) > 1 && match[1] == profile { + return nil + } + } + + return fmt.Errorf("configuration file did not contain profile: %s", profile) +} + +// cleans and expands the path if it contains a tilde , returns the expanded path or the input path as is if not expansion +// was performed +func expandPath(filepath string) string { + if strings.HasPrefix(filepath, fmt.Sprintf("~%c", os.PathSeparator)) { + filepath = path.Join(getHomeFolder(), filepath[2:]) + } + return path.Clean(filepath) +} + +func getBackendAttrWithDefault(obj cty.Value, attrName, def string) (cty.Value, bool) { + value := backendbase.GetAttrDefault(obj, attrName, cty.StringVal(getEnvSettingWithDefault(attrName, def))) + return value, value.IsKnown() && !value.IsNull() +} + +func getBackendAttr(obj cty.Value, attrName string) (cty.Value, bool) { + return getBackendAttrWithDefault(obj, attrName, "") +} +func uniqueStrings(input []string) []string { + seen := make(map[string]bool) + var result []string + + for _, val := range input { + if !seen[val] { + seen[val] = true + result = append(result, val) + } + } + return result +} + +func validateStringObjectPath(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if strings.HasPrefix(val, "/") || strings.HasSuffix(val, "/") || strings.Contains(val, "//") { + *diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, + "Invalid Value", + `The value must not start or end with "/" and also not contain consecutive "/"`, + path.Copy(), + )) + } +} + +func validateStringWorkspacePrefix(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if strings.HasPrefix(val, "/") || strings.Contains(val, "//") { + *diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, + "Invalid Value", + `The value must not start with "/" and also not contain consecutive "/"`, + path.Copy(), + )) + } +} + +func validateStringBucketName(val string, path cty.Path, diags *tfdiags.Diagnostics) { + match, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, val) + if !match { + *diags = diags.Append(tfdiags.AttributeValue(tfdiags.Error, + "Invalid Value", + `The bucket name can only include alphanumeric characters, underscores (_), and hyphens (-).`, + path.Copy(), + )) + } +} +func requiredAttributeErrDiag(path cty.Path) tfdiags.Diagnostic { + return tfdiags.AttributeValue(tfdiags.Error, + "Missing Required Value", + fmt.Sprintf("The attribute %q is required by the backend.\n\n", path)+ + "Refer to the backend documentation for additional information which attributes are required.", + path, + ) +} +func attributeErrDiag(summary, detail string, path cty.Path) tfdiags.Diagnostic { + return tfdiags.AttributeValue( + tfdiags.Error, + summary, + detail, + path, + ) +} + +func attributeWarningDiag(summary, detail string, path cty.Path) tfdiags.Diagnostic { + return tfdiags.AttributeValue( + tfdiags.Warning, + summary, + detail, + path, + ) +} diff --git a/internal/backend/remote-state/oss/backend.go b/internal/backend/remote-state/oss/backend.go index 2d488ce14c..2750b98c67 100644 --- a/internal/backend/remote-state/oss/backend.go +++ b/internal/backend/remote-state/oss/backend.go @@ -1,10 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package oss import ( "context" "encoding/json" "fmt" - "github.com/aliyun/alibaba-cloud-sdk-go/sdk/endpoints" "io/ioutil" "log" "net/http" @@ -16,6 +18,8 @@ import ( "strings" "time" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk/endpoints" + "github.com/aliyun/alibaba-cloud-sdk-go/sdk" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials" "github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests" @@ -36,23 +40,23 @@ import ( // Deprecated in favor of flattening assume_role_* options func deprecatedAssumeRoleSchema() *schema.Schema { return &schema.Schema{ - Type: schema.TypeSet, - Optional: true, - MaxItems: 1, - Deprecated: "use assume_role_* options instead", + Type: schema.TypeSet, + Optional: true, + MaxItems: 1, + //Deprecated: "use assume_role_* options instead", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "role_arn": { Type: schema.TypeString, Required: true, Description: "The ARN of a RAM role to assume prior to making API calls.", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_ARN", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_ASSUME_ROLE_ARN", "ALIBABA_CLOUD_ROLE_ARN"}, ""), }, "session_name": { Type: schema.TypeString, Optional: true, Description: "The session name to use when assuming the role.", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_SESSION_NAME", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_ASSUME_ROLE_SESSION_NAME", "ALIBABA_CLOUD_ROLE_SESSION_NAME"}, ""), }, "policy": { Type: schema.TypeString, @@ -91,27 +95,27 @@ func New() backend.Backend { Type: schema.TypeString, Optional: true, Description: "Alibaba Cloud Access Key ID", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ACCESS_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_ID")), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_ACCESS_KEY", "ALIBABA_CLOUD_ACCESS_KEY_ID", "ALICLOUD_ACCESS_KEY_ID"}, ""), }, "secret_key": { Type: schema.TypeString, Optional: true, Description: "Alibaba Cloud Access Secret Key", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECRET_KEY", os.Getenv("ALICLOUD_ACCESS_KEY_SECRET")), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_SECRET_KEY", "ALIBABA_CLOUD_ACCESS_KEY_SECRET", "ALICLOUD_ACCESS_KEY_SECRET"}, ""), }, "security_token": { Type: schema.TypeString, Optional: true, Description: "Alibaba Cloud Security Token", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SECURITY_TOKEN", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_SECURITY_TOKEN", "ALIBABA_CLOUD_SECURITY_TOKEN"}, ""), }, "ecs_role_name": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ECS_ROLE_NAME", os.Getenv("ALICLOUD_ECS_ROLE_NAME")), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_ECS_ROLE_NAME", "ALIBABA_CLOUD_ECS_METADATA"}, ""), Description: "The RAM Role Name attached on a ECS instance for API operations. You can retrieve this from the 'Access Control' section of the Alibaba Cloud console.", }, @@ -119,25 +123,25 @@ func New() backend.Backend { Type: schema.TypeString, Optional: true, Description: "The region of the OSS bucket.", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_REGION", os.Getenv("ALICLOUD_DEFAULT_REGION")), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_REGION", "ALIBABA_CLOUD_REGION", "ALICLOUD_DEFAULT_REGION"}, ""), }, "sts_endpoint": { Type: schema.TypeString, Optional: true, Description: "A custom endpoint for the STS API", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_STS_ENDPOINT", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_STS_ENDPOINT", "ALIBABA_CLOUD_STS_ENDPOINT"}, ""), }, "tablestore_endpoint": { Type: schema.TypeString, Optional: true, Description: "A custom endpoint for the TableStore API", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_TABLESTORE_ENDPOINT", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_TABLESTORE_ENDPOINT", "ALIBABA_CLOUD_TABLESTORE_ENDPOINT"}, ""), }, "endpoint": { Type: schema.TypeString, Optional: true, Description: "A custom endpoint for the OSS API", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_OSS_ENDPOINT", os.Getenv("OSS_ENDPOINT")), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_OSS_ENDPOINT", "ALIBABA_CLOUD_OSS_ENDPOINT", "OSS_ENDPOINT"}, ""), }, "bucket": { @@ -172,6 +176,12 @@ func New() backend.Backend { }, Default: "terraform.tfstate", }, + "tablestore_instance_name": { + Type: schema.TypeString, + Optional: true, + Description: "The instance name of tableStore table belongs", + Default: "", + }, "tablestore_table": { Type: schema.TypeString, @@ -207,27 +217,27 @@ func New() backend.Backend { "shared_credentials_file": { Type: schema.TypeString, Optional: true, - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_SHARED_CREDENTIALS_FILE", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_SHARED_CREDENTIALS_FILE", "ALIBABA_CLOUD_CREDENTIALS_FILE"}, ""), Description: "This is the path to the shared credentials file. If this is not set and a profile is specified, `~/.aliyun/config.json` will be used.", }, "profile": { Type: schema.TypeString, Optional: true, Description: "This is the Alibaba Cloud profile name as set in the shared credentials file. It can also be sourced from the `ALICLOUD_PROFILE` environment variable.", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_PROFILE", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_PROFILE", "ALIBABA_CLOUD_PROFILE"}, ""), }, "assume_role": deprecatedAssumeRoleSchema(), "assume_role_role_arn": { Type: schema.TypeString, Optional: true, Description: "The ARN of a RAM role to assume prior to making API calls.", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_ARN", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_ASSUME_ROLE_ARN", "ALIBABA_CLOUD_ROLE_ARN"}, ""), }, "assume_role_session_name": { Type: schema.TypeString, Optional: true, Description: "The session name to use when assuming the role.", - DefaultFunc: schema.EnvDefaultFunc("ALICLOUD_ASSUME_ROLE_SESSION_NAME", ""), + DefaultFunc: schema.MultiEnvDefaultFunc([]string{"ALICLOUD_ASSUME_ROLE_SESSION_NAME", "ALIBABA_CLOUD_ROLE_SESSION_NAME"}, ""), }, "assume_role_policy": { Type: schema.TypeString, @@ -291,24 +301,26 @@ func (b *Backend) configure(ctx context.Context) error { b.serverSideEncryption = d.Get("encrypt").(bool) b.acl = d.Get("acl").(string) - var getBackendConfig = func(str string, key string) string { - if str == "" { - value, err := getConfigFromProfile(d, key) - if err == nil && value != nil { - str = value.(string) + var getBackendConfig = func(schemaKey string, profileKey string) string { + if schemaKey != "" { + if v, ok := d.GetOk(schemaKey); ok && v != nil && v.(string) != "" { + return v.(string) } } - return str + if v, err := getConfigFromProfile(d, profileKey); err == nil && v != nil { + return v.(string) + } + return "" } - accessKey := getBackendConfig(d.Get("access_key").(string), "access_key_id") - secretKey := getBackendConfig(d.Get("secret_key").(string), "access_key_secret") - securityToken := getBackendConfig(d.Get("security_token").(string), "sts_token") - region := getBackendConfig(d.Get("region").(string), "region_id") + accessKey := getBackendConfig("access_key", "access_key_id") + secretKey := getBackendConfig("secret_key", "access_key_secret") + region := getBackendConfig("region", "region_id") + securityToken := getBackendConfig("security_token", "sts_token") stsEndpoint := d.Get("sts_endpoint").(string) endpoint := d.Get("endpoint").(string) - schma := "https" + protocol := "https" roleArn := getBackendConfig("", "ram_role_arn") sessionName := getBackendConfig("", "ram_session_name") @@ -360,7 +372,7 @@ func (b *Backend) configure(ctx context.Context) error { } if accessKey == "" { - ecsRoleName := getBackendConfig(d.Get("ecs_role_name").(string), "ram_role_name") + ecsRoleName := getBackendConfig("ecs_role_name", "ram_role_name") subAccessKeyId, subAccessKeySecret, subSecurityToken, err := getAuthCredentialByEcsRoleName(ecsRoleName) if err != nil { return err @@ -379,12 +391,13 @@ func (b *Backend) configure(ctx context.Context) error { if endpoint == "" { endpointsResponse, err := b.getOSSEndpointByRegion(accessKey, secretKey, securityToken, region) if err != nil { - return err - } - for _, endpointItem := range endpointsResponse.Endpoints.Endpoint { - if endpointItem.Type == "openAPI" { - endpoint = endpointItem.Endpoint - break + log.Printf("[WARN] getting oss endpoint failed and using oss-%s.aliyuncs.com instead. Error: %#v.", region, err) + } else { + for _, endpointItem := range endpointsResponse.Endpoints.Endpoint { + if endpointItem.Type == "openAPI" { + endpoint = endpointItem.Endpoint + break + } } } if endpoint == "" { @@ -392,7 +405,7 @@ func (b *Backend) configure(ctx context.Context) error { } } if !strings.HasPrefix(endpoint, "http") { - endpoint = fmt.Sprintf("%s://%s", schma, endpoint) + endpoint = fmt.Sprintf("%s://%s", protocol, endpoint) } log.Printf("[DEBUG] Instantiate OSS client using endpoint: %#v", endpoint) var options []oss.ClientOption @@ -409,13 +422,16 @@ func (b *Backend) configure(ctx context.Context) error { client, err := oss.New(endpoint, accessKey, secretKey, options...) b.ossClient = client otsEndpoint := d.Get("tablestore_endpoint").(string) + otsInstanceName := d.Get("tablestore_instance_name").(string) if otsEndpoint != "" { if !strings.HasPrefix(otsEndpoint, "http") { - otsEndpoint = fmt.Sprintf("%s://%s", schma, otsEndpoint) + otsEndpoint = fmt.Sprintf("%s://%s", protocol, otsEndpoint) } b.otsEndpoint = otsEndpoint - parts := strings.Split(strings.TrimPrefix(strings.TrimPrefix(otsEndpoint, "https://"), "http://"), ".") - b.otsClient = tablestore.NewClientWithConfig(otsEndpoint, parts[0], accessKey, secretKey, securityToken, tablestore.NewDefaultTableStoreConfig()) + if otsInstanceName == "" { + otsInstanceName = strings.Split(strings.TrimPrefix(strings.TrimPrefix(otsEndpoint, "https://"), "http://"), ".")[0] + } + b.otsClient = tablestore.NewClientWithConfig(otsEndpoint, otsInstanceName, accessKey, secretKey, securityToken, tablestore.NewDefaultTableStoreConfig()) } b.otsTable = d.Get("tablestore_table").(string) @@ -549,9 +565,13 @@ func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{ } current := d.Get("profile").(string) // Set CredsFilename, expanding home directory - profilePath, err := homedir.Expand(d.Get("shared_credentials_file").(string)) - if err != nil { - return nil, err + var profilePath string + if v, ok := d.GetOk("shared_credentials_file"); ok { + path, err := homedir.Expand(v.(string)) + if err != nil { + return nil, err + } + profilePath = path } if profilePath == "" { profilePath = fmt.Sprintf("%s/.aliyun/config.json", os.Getenv("HOME")) @@ -560,7 +580,7 @@ func getConfigFromProfile(d *schema.ResourceData, ProfileKey string) (interface{ } } providerConfig = make(map[string]interface{}) - _, err = os.Stat(profilePath) + _, err := os.Stat(profilePath) if !os.IsNotExist(err) { data, err := ioutil.ReadFile(profilePath) if err != nil { diff --git a/internal/backend/remote-state/oss/backend_state.go b/internal/backend/remote-state/oss/backend_state.go index 77a2775f8a..2f32bf267d 100644 --- a/internal/backend/remote-state/oss/backend_state.go +++ b/internal/backend/remote-state/oss/backend_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package oss import ( @@ -96,7 +99,7 @@ func (b *Backend) Workspaces() ([]string, error) { return result, nil } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -160,7 +163,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { err = lockUnlock(err) return nil, err } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { err = lockUnlock(err) return nil, err } diff --git a/internal/backend/remote-state/oss/backend_test.go b/internal/backend/remote-state/oss/backend_test.go index e9bc887163..31d78ffd88 100644 --- a/internal/backend/remote-state/oss/backend_test.go +++ b/internal/backend/remote-state/oss/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package oss import ( diff --git a/internal/backend/remote-state/oss/client.go b/internal/backend/remote-state/oss/client.go index 78d835ae13..86669dad96 100644 --- a/internal/backend/remote-state/oss/client.go +++ b/internal/backend/remote-state/oss/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package oss import ( @@ -5,6 +8,7 @@ import ( "crypto/md5" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "log" @@ -12,9 +16,7 @@ import ( "github.com/aliyun/aliyun-oss-go-sdk/oss" "github.com/aliyun/aliyun-tablestore-go-sdk/tablestore" - "github.com/hashicorp/go-multierror" uuid "github.com/hashicorp/go-uuid" - "github.com/pkg/errors" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -180,23 +182,22 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { }, } - log.Printf("[DEBUG] Recording state lock in tablestore: %#v", putParams) + log.Printf("[DEBUG] Recording state lock in tablestore: %#v; LOCKID:%s", putParams, c.lockPath()) _, err := c.otsClient.PutRow(&tablestore.PutRowRequest{ PutRowChange: putParams, }) if err != nil { - log.Printf("[WARN] Error storing state lock in tablestore: %#v", err) + err = fmt.Errorf("invoking PutRow got an error: %#v", err) lockInfo, infoErr := c.getLockInfo() if infoErr != nil { - log.Printf("[WARN] Error getting lock info: %#v", err) - err = multierror.Append(err, infoErr) + err = errors.Join(err, fmt.Errorf("\ngetting lock info got an error: %#v", infoErr)) } lockErr := &statemgr.LockError{ Err: err, Info: lockInfo, } - log.Printf("[WARN] state lock error: %#v", lockErr) + log.Printf("[ERROR] state lock error: %s", lockErr.Error()) return "", lockErr } @@ -386,13 +387,11 @@ func (c *RemoteClient) Unlock(id string) error { }, }, Condition: &tablestore.RowCondition{ - RowExistenceExpectation: tablestore.RowExistenceExpectation_EXPECT_EXIST, + RowExistenceExpectation: tablestore.RowExistenceExpectation_IGNORE, }, }, } - log.Printf("[DEBUG] Deleting state lock from tablestore: %#v", params) - _, err = c.otsClient.DeleteRow(params) if err != nil { diff --git a/internal/backend/remote-state/oss/client_test.go b/internal/backend/remote-state/oss/client_test.go index 1fc62792be..5166737fdb 100644 --- a/internal/backend/remote-state/oss/client_test.go +++ b/internal/backend/remote-state/oss/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package oss import ( diff --git a/internal/backend/remote-state/oss/go.mod b/internal/backend/remote-state/oss/go.mod new file mode 100644 index 0000000000..cde535c5e9 --- /dev/null +++ b/internal/backend/remote-state/oss/go.mod @@ -0,0 +1,77 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/oss + +go 1.24.2 + +require ( + github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501 + github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 + github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible + github.com/hashicorp/go-cleanhttp v0.5.2 + github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/hashicorp/terraform/internal/legacy v0.0.0-00010101000000-000000000000 + github.com/jmespath/go-jmespath v0.4.0 + github.com/mitchellh/go-homedir v1.1.0 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/satori/go.uuid v1.2.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/stretchr/testify v1.8.4 // indirect + github.com/zclconf/go-cty v1.16.2 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/ini.v1 v1.66.2 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/oss/go.sum b/internal/backend/remote-state/oss/go.sum new file mode 100644 index 0000000000..620693f452 --- /dev/null +++ b/internal/backend/remote-state/oss/go.sum @@ -0,0 +1,633 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501 h1:Ij3S0pNUMgHlhx3Ew8g9RNrt59EKhHYdMODGtFXJfSc= +github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501/go.mod h1:RcDobYh8k5VP6TNybz9m++gL3ijVI5wueVr0EM10VsU= +github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70 h1:FrF4uxA24DF3ARNXVbUin3wa5fDLaB1Cy8mKks/LRz4= +github.com/aliyun/aliyun-oss-go-sdk v0.0.0-20190103054945-8205d1f41e70/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible h1:ABQ7FF+IxSFHDMOTtjCfmMDMHiCq6EsAoCV/9sFinaM= +github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible/go.mod h1:LDQHRZylxvcg8H7wBIDfvO5g/cy4/sz1iucBlc2l3Jw= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= +github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/pg/backend.go b/internal/backend/remote-state/pg/backend.go index cdcfb3a6e4..7ba0fd3f88 100644 --- a/internal/backend/remote-state/pg/backend.go +++ b/internal/backend/remote-state/pg/backend.go @@ -1,13 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package pg import ( - "context" "database/sql" "fmt" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" "github.com/lib/pq" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendbase" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" ) const ( @@ -17,79 +23,91 @@ const ( // New creates a new backend for Postgres remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "conn_str": { - Type: schema.TypeString, - Required: true, - Description: "Postgres connection string; a `postgres://` URL", + return &Backend{ + Base: backendbase.Base{ + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "conn_str": { + Type: cty.String, + Optional: true, + Description: "Postgres connection string; a `postgres://` URL", + }, + "schema_name": { + Type: cty.String, + Optional: true, + Description: "Name of the automatically managed Postgres schema to store state", + }, + "skip_schema_creation": { + Type: cty.Bool, + Optional: true, + Description: "If set to `true`, Terraform won't try to create the Postgres schema", + }, + "skip_table_creation": { + Type: cty.Bool, + Optional: true, + Description: "If set to `true`, Terraform won't try to create the Postgres table", + }, + "skip_index_creation": { + Type: cty.Bool, + Optional: true, + Description: "If set to `true`, Terraform won't try to create the Postgres index", + }, + }, }, - - "schema_name": { - Type: schema.TypeString, - Optional: true, - Description: "Name of the automatically managed Postgres schema to store state", - Default: "terraform_remote_state", - }, - - "skip_schema_creation": { - Type: schema.TypeBool, - Optional: true, - Description: "If set to `true`, Terraform won't try to create the Postgres schema", - Default: false, - }, - - "skip_table_creation": { - Type: schema.TypeBool, - Optional: true, - Description: "If set to `true`, Terraform won't try to create the Postgres table", - }, - - "skip_index_creation": { - Type: schema.TypeBool, - Optional: true, - Description: "If set to `true`, Terraform won't try to create the Postgres index", + SDKLikeDefaults: backendbase.SDKLikeDefaults{ + "conn_str": { + EnvVars: []string{"PG_CONN_STR"}, + }, + "schema_name": { + EnvVars: []string{"PG_SCHEMA_NAME"}, + Fallback: "terraform_remote_state", + }, + "skip_schema_creation": { + EnvVars: []string{"PG_SKIP_SCHEMA_CREATION"}, + Fallback: "false", + }, + "skip_table_creation": { + EnvVars: []string{"PG_SKIP_TABLE_CREATION"}, + Fallback: "false", + }, + "skip_index_creation": { + EnvVars: []string{"PG_SKIP_INDEX_CREATION"}, + Fallback: "false", + }, }, }, } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result } type Backend struct { - *schema.Backend + backendbase.Base // The fields below are set from configure db *sql.DB - configData *schema.ResourceData connStr string schemaName string } -func (b *Backend) configure(ctx context.Context) error { - // Grab the resource data - b.configData = schema.FromContextBackendConfig(ctx) - data := b.configData +func (b *Backend) Configure(configVal cty.Value) tfdiags.Diagnostics { + data := backendbase.NewSDKLikeData(configVal) - b.connStr = data.Get("conn_str").(string) - b.schemaName = pq.QuoteIdentifier(data.Get("schema_name").(string)) + b.connStr = data.String("conn_str") + b.schemaName = pq.QuoteIdentifier(data.String("schema_name")) db, err := sql.Open("postgres", b.connStr) if err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } // Prepare database schema, tables, & indexes. var query string - if !data.Get("skip_schema_creation").(bool) { + if !data.Bool("skip_schema_creation") { // list all schemas to see if it exists var count int query = `select count(1) from information_schema.schemata where schema_name = $1` - if err := db.QueryRow(query, data.Get("schema_name").(string)).Scan(&count); err != nil { - return err + if err := db.QueryRow(query, data.String("schema_name")).Scan(&count); err != nil { + return backendbase.ErrorAsDiagnostics(err) } // skip schema creation if schema already exists @@ -99,14 +117,14 @@ func (b *Backend) configure(ctx context.Context) error { // tries to create the schema query = `CREATE SCHEMA IF NOT EXISTS %s` if _, err := db.Exec(fmt.Sprintf(query, b.schemaName)); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } } } - if !data.Get("skip_table_creation").(bool) { + if !data.Bool("skip_table_creation") { if _, err := db.Exec("CREATE SEQUENCE IF NOT EXISTS public.global_states_id_seq AS bigint"); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } query = `CREATE TABLE IF NOT EXISTS %s.%s ( @@ -115,14 +133,14 @@ func (b *Backend) configure(ctx context.Context) error { data text )` if _, err := db.Exec(fmt.Sprintf(query, b.schemaName, statesTableName)); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } } - if !data.Get("skip_index_creation").(bool) { + if !data.Bool("skip_index_creation") { query = `CREATE UNIQUE INDEX IF NOT EXISTS %s ON %s.%s (name)` if _, err := db.Exec(fmt.Sprintf(query, statesIndexName, b.schemaName, statesTableName)); err != nil { - return err + return backendbase.ErrorAsDiagnostics(err) } } diff --git a/internal/backend/remote-state/pg/backend_state.go b/internal/backend/remote-state/pg/backend_state.go index 2700c51969..6e8aaa1667 100644 --- a/internal/backend/remote-state/pg/backend_state.go +++ b/internal/backend/remote-state/pg/backend_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package pg import ( @@ -35,7 +38,7 @@ func (b *Backend) Workspaces() ([]string, error) { return result, nil } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } @@ -99,7 +102,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { err = lockUnlock(err) return nil, err } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { err = lockUnlock(err) return nil, err } diff --git a/internal/backend/remote-state/pg/backend_test.go b/internal/backend/remote-state/pg/backend_test.go index da058483d8..f9da1a927c 100644 --- a/internal/backend/remote-state/pg/backend_test.go +++ b/internal/backend/remote-state/pg/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package pg // Create the test database: createdb terraform_backend_pg_test @@ -6,29 +9,39 @@ package pg import ( "database/sql" "fmt" + "net/url" "os" + "strings" "testing" + "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" "github.com/lib/pq" - _ "github.com/lib/pq" ) // Function to skip a test unless in ACCeptance test mode. // // A running Postgres server identified by env variable // DATABASE_URL is required for acceptance tests. -func testACC(t *testing.T) { +func testACC(t *testing.T) string { skip := os.Getenv("TF_ACC") == "" if skip { t.Log("pg backend tests require setting TF_ACC") t.Skip() } - if os.Getenv("DATABASE_URL") == "" { - os.Setenv("DATABASE_URL", "postgres://localhost/terraform_backend_pg_test?sslmode=disable") + databaseUrl, found := os.LookupEnv("DATABASE_URL") + if !found { + databaseUrl = "postgres://localhost/terraform_backend_pg_test?sslmode=disable" + os.Setenv("DATABASE_URL", databaseUrl) } + u, err := url.Parse(databaseUrl) + if err != nil { + t.Fatal(err) + } + return u.Path[1:] } func TestBackend_impl(t *testing.T) { @@ -36,48 +49,172 @@ func TestBackend_impl(t *testing.T) { } func TestBackendConfig(t *testing.T) { - testACC(t) + databaseName := testACC(t) connStr := getDatabaseUrl() - schemaName := pq.QuoteIdentifier(fmt.Sprintf("terraform_%s", t.Name())) - config := backend.TestWrapConfig(map[string]interface{}{ - "conn_str": connStr, - "schema_name": schemaName, - }) - schemaName = pq.QuoteIdentifier(schemaName) - - dbCleaner, err := sql.Open("postgres", connStr) - if err != nil { - t.Fatal(err) - } - defer dbCleaner.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)) - - b := backend.TestBackendConfig(t, New(), config).(*Backend) - - if b == nil { - t.Fatal("Backend could not be configured") + testCases := []struct { + Name string + EnvVars map[string]string + Config map[string]interface{} + ExpectConfigurationError string + ExpectConnectionError string + }{ + { + Name: "valid-config", + Config: map[string]interface{}{ + "conn_str": connStr, + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + }, + { + Name: "missing-conn_str-defaults-to-localhost", + EnvVars: map[string]string{ + "PGSSLMODE": "disable", + "PGDATABASE": databaseName, + }, + Config: map[string]interface{}{ + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + }, + { + Name: "conn-str-env-var", + EnvVars: map[string]string{ + "PG_CONN_STR": connStr, + }, + Config: map[string]interface{}{ + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + }, + { + Name: "setting-credentials-using-env-vars", + EnvVars: map[string]string{ + "PGUSER": "baduser", + "PGPASSWORD": "badpassword", + }, + Config: map[string]interface{}{ + "conn_str": connStr, + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + ExpectConnectionError: `password authentication failed for user "baduser"`, + }, + { + Name: "host-in-env-vars", + EnvVars: map[string]string{ + "PGHOST": "hostthatdoesnotexist", + }, + Config: map[string]interface{}{ + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + ExpectConnectionError: `no such host`, + }, + { + Name: "boolean-env-vars", + EnvVars: map[string]string{ + "PGSSLMODE": "disable", + "PG_SKIP_SCHEMA_CREATION": "f", + "PG_SKIP_TABLE_CREATION": "f", + "PG_SKIP_INDEX_CREATION": "f", + "PGDATABASE": databaseName, + }, + Config: map[string]interface{}{ + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + }, + { + Name: "wrong-boolean-env-vars", + EnvVars: map[string]string{ + "PGSSLMODE": "disable", + "PG_SKIP_SCHEMA_CREATION": "foo", + "PGDATABASE": databaseName, + }, + Config: map[string]interface{}{ + "schema_name": fmt.Sprintf("terraform_%s", t.Name()), + }, + ExpectConfigurationError: `invalid value for "skip_schema_creation"`, + }, } - _, err = b.db.Query(fmt.Sprintf("SELECT name, data FROM %s.%s LIMIT 1", schemaName, statesTableName)) - if err != nil { - t.Fatal(err) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + for k, v := range tc.EnvVars { + t.Setenv(k, v) + } + + config := backend.TestWrapConfig(tc.Config) + schemaName := pq.QuoteIdentifier(tc.Config["schema_name"].(string)) + + dbCleaner, err := sql.Open("postgres", connStr) + if err != nil { + t.Fatal(err) + } + defer dbCleaner.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", schemaName)) + + var diags tfdiags.Diagnostics + b := New().(*Backend) + schema := b.ConfigSchema() + spec := schema.DecoderSpec() + obj, decDiags := hcldec.Decode(config, spec, nil) + diags = diags.Append(decDiags) + + newObj, valDiags := b.PrepareConfig(obj) + diags = diags.Append(valDiags.InConfigBody(config, "")) + + if tc.ExpectConfigurationError != "" { + if !diags.HasErrors() { + t.Fatal("error expected but got none") + } + if !strings.Contains(diags.ErrWithWarnings().Error(), tc.ExpectConfigurationError) { + t.Fatalf("failed to find %q in %s", tc.ExpectConfigurationError, diags.ErrWithWarnings()) + } + return + } else if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + obj = newObj + + confDiags := b.Configure(obj) + if tc.ExpectConnectionError != "" { + err := confDiags.InConfigBody(config, "").ErrWithWarnings() + if err == nil { + t.Fatal("error expected but got none") + } + if !strings.Contains(err.Error(), tc.ExpectConnectionError) { + t.Fatalf("failed to find %q in %s", tc.ExpectConnectionError, err) + } + return + } else if len(confDiags) != 0 { + confDiags = confDiags.InConfigBody(config, "") + t.Fatal(confDiags.ErrWithWarnings()) + } + + if b == nil { + t.Fatal("Backend could not be configured") + } + + _, err = b.db.Query(fmt.Sprintf("SELECT name, data FROM %s.%s LIMIT 1", schemaName, statesTableName)) + if err != nil { + t.Fatal(err) + } + + _, err = b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + s, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + c := s.(*remote.State).Client.(*RemoteClient) + if c.Name != backend.DefaultStateName { + t.Fatal("RemoteClient name is not configured") + } + + backend.TestBackendStates(t, b) + }) } - _, err = b.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatal(err) - } - - s, err := b.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatal(err) - } - c := s.(*remote.State).Client.(*RemoteClient) - if c.Name != backend.DefaultStateName { - t.Fatal("RemoteClient name is not configured") - } - - backend.TestBackendStates(t, b) } func TestBackendConfigSkipOptions(t *testing.T) { @@ -243,7 +380,7 @@ func TestBackendStates(t *testing.T) { if err != nil { t.Fatal(err) } - defer dbCleaner.Query("DROP SCHEMA IF EXISTS %s CASCADE", pq.QuoteIdentifier(schemaName)) + defer dbCleaner.Query(fmt.Sprintf("DROP SCHEMA IF EXISTS %s CASCADE", pq.QuoteIdentifier(schemaName))) config := backend.TestWrapConfig(map[string]interface{}{ "conn_str": connStr, @@ -330,7 +467,7 @@ func TestBackendConcurrentLock(t *testing.T) { t.Fatalf("failed to lock first state: %v", err) } - if err = s1.PersistState(); err != nil { + if err = s1.PersistState(nil); err != nil { t.Fatalf("failed to persist state: %v", err) } @@ -343,7 +480,7 @@ func TestBackendConcurrentLock(t *testing.T) { t.Fatalf("failed to lock second state: %v", err) } - if err = s2.PersistState(); err != nil { + if err = s2.PersistState(nil); err != nil { t.Fatalf("failed to persist state: %v", err) } diff --git a/internal/backend/remote-state/pg/client.go b/internal/backend/remote-state/pg/client.go index 7ff9cd2468..2ce79cc4ba 100644 --- a/internal/backend/remote-state/pg/client.go +++ b/internal/backend/remote-state/pg/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package pg import ( diff --git a/internal/backend/remote-state/pg/client_test.go b/internal/backend/remote-state/pg/client_test.go index 7bf21ac848..e1f38311ad 100644 --- a/internal/backend/remote-state/pg/client_test.go +++ b/internal/backend/remote-state/pg/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package pg // Create the test database: createdb terraform_backend_pg_test diff --git a/internal/backend/remote-state/pg/go.mod b/internal/backend/remote-state/pg/go.mod new file mode 100644 index 0000000000..312aa899c2 --- /dev/null +++ b/internal/backend/remote-state/pg/go.mod @@ -0,0 +1,55 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/pg + +go 1.24.2 + +require ( + github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/lib/pq v1.10.3 + github.com/zclconf/go-cty v1.16.2 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/pg/go.sum b/internal/backend/remote-state/pg/go.sum new file mode 100644 index 0000000000..fa820b37ca --- /dev/null +++ b/internal/backend/remote-state/pg/go.sum @@ -0,0 +1,581 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/lib/pq v1.10.3 h1:v9QZf2Sn6AmjXtQeFpdoq/eaNtYP6IN+7lcrygsIAtg= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/s3/backend.go b/internal/backend/remote-state/s3/backend.go index 98aa1c561e..be1af3f6fd 100644 --- a/internal/backend/remote-state/s3/backend.go +++ b/internal/backend/remote-state/s3/backend.go @@ -1,279 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package s3 import ( "context" "encoding/base64" - "errors" "fmt" + "os" + "regexp" "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + "github.com/aws/aws-sdk-go-v2/service/s3" + awsbase "github.com/hashicorp/aws-sdk-go-base/v2" + baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" + "github.com/hashicorp/aws-sdk-go-base/v2/validation" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/s3" - awsbase "github.com/hashicorp/aws-sdk-go-base" "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" ) -// New creates a new backend for S3 remote state. func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "bucket": { - Type: schema.TypeString, - Required: true, - Description: "The name of the S3 bucket", - }, - - "key": { - Type: schema.TypeString, - Required: true, - Description: "The path to the state file inside the bucket", - ValidateFunc: func(v interface{}, s string) ([]string, []error) { - // s3 will strip leading slashes from an object, so while this will - // technically be accepted by s3, it will break our workspace hierarchy. - if strings.HasPrefix(v.(string), "/") { - return nil, []error{errors.New("key must not start with '/'")} - } - return nil, nil - }, - }, - - "region": { - Type: schema.TypeString, - Required: true, - Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).", - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "AWS_REGION", - "AWS_DEFAULT_REGION", - }, nil), - }, - - "dynamodb_endpoint": { - Type: schema.TypeString, - Optional: true, - Description: "A custom endpoint for the DynamoDB API", - DefaultFunc: schema.EnvDefaultFunc("AWS_DYNAMODB_ENDPOINT", ""), - }, - - "endpoint": { - Type: schema.TypeString, - Optional: true, - Description: "A custom endpoint for the S3 API", - DefaultFunc: schema.EnvDefaultFunc("AWS_S3_ENDPOINT", ""), - }, - - "iam_endpoint": { - Type: schema.TypeString, - Optional: true, - Description: "A custom endpoint for the IAM API", - DefaultFunc: schema.EnvDefaultFunc("AWS_IAM_ENDPOINT", ""), - }, - - "sts_endpoint": { - Type: schema.TypeString, - Optional: true, - Description: "A custom endpoint for the STS API", - DefaultFunc: schema.EnvDefaultFunc("AWS_STS_ENDPOINT", ""), - }, - - "encrypt": { - Type: schema.TypeBool, - Optional: true, - Description: "Whether to enable server side encryption of the state file", - Default: false, - }, - - "acl": { - Type: schema.TypeString, - Optional: true, - Description: "Canned ACL to be applied to the state file", - Default: "", - }, - - "access_key": { - Type: schema.TypeString, - Optional: true, - Description: "AWS access key", - Default: "", - }, - - "secret_key": { - Type: schema.TypeString, - Optional: true, - Description: "AWS secret key", - Default: "", - }, - - "kms_key_id": { - Type: schema.TypeString, - Optional: true, - Description: "The ARN of a KMS Key to use for encrypting the state", - Default: "", - }, - - "dynamodb_table": { - Type: schema.TypeString, - Optional: true, - Description: "DynamoDB table for state locking and consistency", - Default: "", - }, - - "profile": { - Type: schema.TypeString, - Optional: true, - Description: "AWS profile name", - Default: "", - }, - - "shared_credentials_file": { - Type: schema.TypeString, - Optional: true, - Description: "Path to a shared credentials file", - Default: "", - }, - - "token": { - Type: schema.TypeString, - Optional: true, - Description: "MFA token", - Default: "", - }, - - "skip_credentials_validation": { - Type: schema.TypeBool, - Optional: true, - Description: "Skip the credentials validation via STS API.", - Default: false, - }, - - "skip_region_validation": { - Type: schema.TypeBool, - Optional: true, - Description: "Skip static validation of region name.", - Default: false, - }, - - "skip_metadata_api_check": { - Type: schema.TypeBool, - Optional: true, - Description: "Skip the AWS Metadata API check.", - Default: false, - }, - - "sse_customer_key": { - Type: schema.TypeString, - Optional: true, - Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).", - DefaultFunc: schema.EnvDefaultFunc("AWS_SSE_CUSTOMER_KEY", ""), - Sensitive: true, - ValidateFunc: func(v interface{}, s string) ([]string, []error) { - key := v.(string) - if key != "" && len(key) != 44 { - return nil, []error{errors.New("sse_customer_key must be 44 characters in length (256 bits, base64 encoded)")} - } - return nil, nil - }, - }, - - "role_arn": { - Type: schema.TypeString, - Optional: true, - Description: "The role to be assumed", - Default: "", - }, - - "session_name": { - Type: schema.TypeString, - Optional: true, - Description: "The session name to use when assuming the role.", - Default: "", - }, - - "external_id": { - Type: schema.TypeString, - Optional: true, - Description: "The external ID to use when assuming the role", - Default: "", - }, - - "assume_role_duration_seconds": { - Type: schema.TypeInt, - Optional: true, - Description: "Seconds to restrict the assume role session duration.", - }, - - "assume_role_policy": { - Type: schema.TypeString, - Optional: true, - Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", - Default: "", - }, - - "assume_role_policy_arns": { - Type: schema.TypeSet, - Optional: true, - Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - - "assume_role_tags": { - Type: schema.TypeMap, - Optional: true, - Description: "Assume role session tags.", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - - "assume_role_transitive_tag_keys": { - Type: schema.TypeSet, - Optional: true, - Description: "Assume role session tag keys to pass to any subsequent sessions.", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - - "workspace_key_prefix": { - Type: schema.TypeString, - Optional: true, - Description: "The prefix applied to the non-default state path inside the bucket.", - Default: "env:", - ValidateFunc: func(v interface{}, s string) ([]string, []error) { - prefix := v.(string) - if strings.HasPrefix(prefix, "/") || strings.HasSuffix(prefix, "/") { - return nil, []error{errors.New("workspace_key_prefix must not start or end with '/'")} - } - return nil, nil - }, - }, - - "force_path_style": { - Type: schema.TypeBool, - Optional: true, - Description: "Force s3 to use path style api.", - Default: false, - }, - - "max_retries": { - Type: schema.TypeInt, - Optional: true, - Description: "The maximum number of times an AWS API request is retried on retryable failure.", - Default: 5, - }, - }, - } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result + return &Backend{} } type Backend struct { - *schema.Backend - - // The fields below are set from configure - s3Client *s3.S3 - dynClient *dynamodb.DynamoDB + awsConfig aws.Config + s3Client *s3.Client + dynClient *dynamodb.Client bucketName string keyName string @@ -282,127 +46,1576 @@ type Backend struct { acl string kmsKeyID string ddbTable string + useLockFile bool workspaceKeyPrefix string + skipS3Checksum bool } -func (b *Backend) configure(ctx context.Context) error { - if b.s3Client != nil { - return nil - } +// ConfigSchema returns a description of the expected configuration +// structure for the receiving backend. +func (b *Backend) ConfigSchema() *configschema.Block { + return &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bucket": { + Type: cty.String, + Required: true, + Description: "The name of the S3 bucket", + }, + "key": { + Type: cty.String, + Required: true, + Description: "The path to the state file inside the bucket", + }, + "region": { + Type: cty.String, + Optional: true, + Description: "AWS region of the S3 Bucket and DynamoDB Table (if used).", + }, + "allowed_account_ids": { + Type: cty.Set(cty.String), + Optional: true, + Description: "List of allowed AWS account IDs.", + }, + "dynamodb_endpoint": { + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the DynamoDB API", + Deprecated: true, + }, + "ec2_metadata_service_endpoint": { + Type: cty.String, + Optional: true, + Description: "Address of the EC2 metadata service (IMDS) endpoint to use.", + }, + "ec2_metadata_service_endpoint_mode": { + Type: cty.String, + Optional: true, + Description: "Mode to use in communicating with the metadata service.", + }, + "endpoint": { + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the S3 API", + Deprecated: true, + }, - // Grab the resource data - data := schema.FromContextBackendConfig(ctx) + "endpoints": endpointsSchema.SchemaAttribute(), - if !data.Get("skip_region_validation").(bool) { - if err := awsbase.ValidateRegion(data.Get("region").(string)); err != nil { - return err - } - } + "forbidden_account_ids": { + Type: cty.Set(cty.String), + Optional: true, + Description: "List of forbidden AWS account IDs.", + }, + "iam_endpoint": { + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the IAM API", + Deprecated: true, + }, + "sts_endpoint": { + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the STS API", + Deprecated: true, + }, + "sts_region": { + Type: cty.String, + Optional: true, + Description: "AWS region for STS.", + }, + "encrypt": { + Type: cty.Bool, + Optional: true, + Description: "Whether to enable server side encryption of the state file", + }, + "acl": { + Type: cty.String, + Optional: true, + Description: "Canned ACL to be applied to the state file", + }, + "access_key": { + Type: cty.String, + Optional: true, + Description: "AWS access key", + }, + "secret_key": { + Type: cty.String, + Optional: true, + Description: "AWS secret key", + }, + "kms_key_id": { + Type: cty.String, + Optional: true, + Description: "The ARN of a KMS Key to use for encrypting the state", + }, + "dynamodb_table": { + Type: cty.String, + Optional: true, + Description: "DynamoDB table for state locking and consistency", + Deprecated: true, + }, + "use_lockfile": { + Type: cty.Bool, + Optional: true, + Description: "Whether to use a lockfile for locking the state file.", + }, + "profile": { + Type: cty.String, + Optional: true, + Description: "AWS profile name", + }, + "retry_mode": { + Type: cty.String, + Optional: true, + Description: "Specifies how retries are attempted.", + }, + "shared_config_files": { + Type: cty.Set(cty.String), + Optional: true, + Description: "List of paths to shared config files", + }, + "shared_credentials_file": { + Type: cty.String, + Optional: true, + Description: "Path to a shared credentials file", + Deprecated: true, + }, + "shared_credentials_files": { + Type: cty.Set(cty.String), + Optional: true, + Description: "List of paths to shared credentials files", + }, + "token": { + Type: cty.String, + Optional: true, + Description: "MFA token", + }, + "skip_credentials_validation": { + Type: cty.Bool, + Optional: true, + Description: "Skip the credentials validation via STS API. Useful for testing and for AWS API implementations that do not have STS available.", + }, + "skip_requesting_account_id": { + Type: cty.Bool, + Optional: true, + Description: "Skip the requesting account ID. Useful for AWS API implementations that do not have the IAM, STS API, or metadata API.", + }, + "skip_metadata_api_check": { + Type: cty.Bool, + Optional: true, + Description: "Skip the AWS Metadata API check.", + }, + "skip_region_validation": { + Type: cty.Bool, + Optional: true, + Description: "Skip static validation of region name.", + }, + "skip_s3_checksum": { + Type: cty.Bool, + Optional: true, + Description: "Do not include checksum when uploading S3 Objects. Useful for some S3-Compatible APIs.", + }, + "sse_customer_key": { + Type: cty.String, + Optional: true, + Description: "The base64-encoded encryption key to use for server-side encryption with customer-provided keys (SSE-C).", + Sensitive: true, + }, - b.bucketName = data.Get("bucket").(string) - b.keyName = data.Get("key").(string) - b.acl = data.Get("acl").(string) - b.workspaceKeyPrefix = data.Get("workspace_key_prefix").(string) - b.serverSideEncryption = data.Get("encrypt").(bool) - b.kmsKeyID = data.Get("kms_key_id").(string) - b.ddbTable = data.Get("dynamodb_table").(string) + "workspace_key_prefix": { + Type: cty.String, + Optional: true, + Description: "The prefix applied to the non-default state path inside the bucket.", + }, - customerKeyString := data.Get("sse_customer_key").(string) - if customerKeyString != "" { - if b.kmsKeyID != "" { - return errors.New(encryptionKeyConflictError) - } + "force_path_style": { + Type: cty.Bool, + Optional: true, + Description: "Enable path-style S3 URLs.", + Deprecated: true, + }, - var err error - b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKeyString) - if err != nil { - return fmt.Errorf("Failed to decode sse_customer_key: %s", err.Error()) - } - } + "use_path_style": { + Type: cty.Bool, + Optional: true, + Description: "Enable path-style S3 URLs.", + }, - cfg := &awsbase.Config{ - AccessKey: data.Get("access_key").(string), - AssumeRoleARN: data.Get("role_arn").(string), - AssumeRoleDurationSeconds: data.Get("assume_role_duration_seconds").(int), - AssumeRoleExternalID: data.Get("external_id").(string), - AssumeRolePolicy: data.Get("assume_role_policy").(string), - AssumeRoleSessionName: data.Get("session_name").(string), - CallerDocumentationURL: "https://www.terraform.io/docs/language/settings/backends/s3.html", - CallerName: "S3 Backend", - CredsFilename: data.Get("shared_credentials_file").(string), - DebugLogging: logging.IsDebugOrHigher(), - IamEndpoint: data.Get("iam_endpoint").(string), - MaxRetries: data.Get("max_retries").(int), - Profile: data.Get("profile").(string), - Region: data.Get("region").(string), - SecretKey: data.Get("secret_key").(string), - SkipCredsValidation: data.Get("skip_credentials_validation").(bool), - SkipMetadataApiCheck: data.Get("skip_metadata_api_check").(bool), - StsEndpoint: data.Get("sts_endpoint").(string), - Token: data.Get("token").(string), - UserAgentProducts: []*awsbase.UserAgentProduct{ - {Name: "APN", Version: "1.0"}, - {Name: "HashiCorp", Version: "1.0"}, - {Name: "Terraform", Version: version.String()}, + "max_retries": { + Type: cty.Number, + Optional: true, + Description: "The maximum number of times an AWS API request is retried on retryable failure.", + }, + + "assume_role": assumeRoleSchema.SchemaAttribute(), + + "assume_role_with_web_identity": assumeRoleWithWebIdentitySchema.SchemaAttribute(), + + "custom_ca_bundle": { + Type: cty.String, + Optional: true, + Description: "File containing custom root and intermediate certificates.", + }, + + "http_proxy": { + Type: cty.String, + Optional: true, + Description: "URL of a proxy to use for HTTP requests when accessing the AWS API.", + }, + + "https_proxy": { + Type: cty.String, + Optional: true, + Description: "URL of a proxy to use for HTTPS requests when accessing the AWS API.", + }, + + "no_proxy": { + Type: cty.String, + Optional: true, + Description: "Comma-separated list of hosts that should not use HTTP or HTTPS proxies.", + }, + + "insecure": { + Type: cty.Bool, + Optional: true, + Description: "Whether to explicitly allow the backend to perform insecure SSL requests.", + }, + "use_fips_endpoint": { + Type: cty.Bool, + Optional: true, + Description: "Force the backend to resolve endpoints with FIPS capability.", + }, + "use_dualstack_endpoint": { + Type: cty.Bool, + Optional: true, + Description: "Force the backend to resolve endpoints with DualStack capability.", + }, }, } - - if policyARNSet := data.Get("assume_role_policy_arns").(*schema.Set); policyARNSet.Len() > 0 { - for _, policyARNRaw := range policyARNSet.List() { - policyARN, ok := policyARNRaw.(string) - - if !ok { - continue - } - - cfg.AssumeRolePolicyARNs = append(cfg.AssumeRolePolicyARNs, policyARN) - } - } - - if tagMap := data.Get("assume_role_tags").(map[string]interface{}); len(tagMap) > 0 { - cfg.AssumeRoleTags = make(map[string]string) - - for k, vRaw := range tagMap { - v, ok := vRaw.(string) - - if !ok { - continue - } - - cfg.AssumeRoleTags[k] = v - } - } - - if transitiveTagKeySet := data.Get("assume_role_transitive_tag_keys").(*schema.Set); transitiveTagKeySet.Len() > 0 { - for _, transitiveTagKeyRaw := range transitiveTagKeySet.List() { - transitiveTagKey, ok := transitiveTagKeyRaw.(string) - - if !ok { - continue - } - - cfg.AssumeRoleTransitiveTagKeys = append(cfg.AssumeRoleTransitiveTagKeys, transitiveTagKey) - } - } - - sess, err := awsbase.GetSession(cfg) - if err != nil { - return fmt.Errorf("error configuring S3 Backend: %w", err) - } - - b.dynClient = dynamodb.New(sess.Copy(&aws.Config{ - Endpoint: aws.String(data.Get("dynamodb_endpoint").(string)), - })) - b.s3Client = s3.New(sess.Copy(&aws.Config{ - Endpoint: aws.String(data.Get("endpoint").(string)), - S3ForcePathStyle: aws.Bool(data.Get("force_path_style").(bool)), - })) - - return nil } -const encryptionKeyConflictError = `Cannot have both kms_key_id and sse_customer_key set. +var assumeRoleSchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "role_arn": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Required: true, + Description: "The role to be assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateARN( + validateIAMRoleARN, + ), + }, + }, + }, -The kms_key_id is used for encryption with KMS-Managed Keys (SSE-KMS) -while sse_customer_key is used for encryption with customer-managed keys (SSE-C). + "duration": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", + }, + validateString{ + Validators: []stringValidator{ + validateDuration( + validateDurationBetween(15*time.Minute, 12*time.Hour), + ), + }, + }, + }, + + "external_id": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The external ID to use when assuming the role", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(2, 1224), + validateStringMatches( + regexp.MustCompile(`^[\w+=,.@:\/\-]*$`), + `Value can only contain letters, numbers, or the following characters: =,.@/-`, + ), + }, + }, + }, + + "policy": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateIAMPolicyDocument, + }, + }, + }, + + "policy_arns": setAttribute{ + configschema.Attribute{ + Type: cty.Set(cty.String), + Optional: true, + Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", + }, + validateSet{ + Validators: []setValidator{ + validateSetStringElements( + validateARN( + validateIAMPolicyARN, + ), + ), + }, + }, + }, + + "session_name": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The session name to use when assuming the role.", + }, + validateString{ + Validators: assumeRoleNameValidator, + }, + }, + + "source_identity": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "Source identity specified by the principal assuming the role.", + }, + validateString{ + Validators: assumeRoleNameValidator, + }, + }, + + "tags": mapAttribute{ + configschema.Attribute{ + Type: cty.Map(cty.String), + Optional: true, + Description: "Assume role session tags.", + }, + validateMap{}, + }, + + "transitive_tag_keys": setAttribute{ + configschema.Attribute{ + Type: cty.Set(cty.String), + Optional: true, + Description: "Assume role session tag keys to pass to any subsequent sessions.", + }, + validateSet{}, + }, + }, +} + +var assumeRoleWithWebIdentitySchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "role_arn": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Required: true, + Description: "The role to be assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateARN( + validateIAMRoleARN, + ), + }, + }, + }, + + "duration": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The duration, between 15 minutes and 12 hours, of the role session. Valid time units are ns, us (or µs), ms, s, h, or m.", + }, + validateString{ + Validators: []stringValidator{ + validateDuration( + validateDurationBetween(15*time.Minute, 12*time.Hour), + ), + }, + }, + }, + + "policy": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "IAM Policy JSON describing further restricting permissions for the IAM Role being assumed.", + }, + validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateIAMPolicyDocument, + }, + }, + }, + + "policy_arns": setAttribute{ + configschema.Attribute{ + Type: cty.Set(cty.String), + Optional: true, + Description: "Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed.", + }, + validateSet{ + Validators: []setValidator{ + validateSetStringElements( + validateARN( + validateIAMPolicyARN, + ), + ), + }, + }, + }, + + "session_name": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "The session name to use when assuming the role.", + }, + validateString{ + Validators: assumeRoleNameValidator, + }, + }, + + "web_identity_token": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "Value of a web identity token from an OpenID Connect (OIDC) or OAuth provider.", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(4, 20000), + }, + }, + }, + + "web_identity_token_file": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider.", + }, + validateString{ + Validators: []stringValidator{ + validateStringLenBetween(4, 20000), + }, + }, + }, + }, + validateObject: validateObject{ + Validators: []objectValidator{ + validateExactlyOneOfAttributes( + cty.GetAttrPath("web_identity_token"), + cty.GetAttrPath("web_identity_token_file"), + ), + }, + }, +} + +var endpointsSchema = singleNestedAttribute{ + Attributes: map[string]schemaAttribute{ + "dynamodb": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the DynamoDB API", + Deprecated: true, + }, + validateString{ + Validators: []stringValidator{ + validateStringLegacyURL, + }, + }, + }, + + "iam": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the IAM API", + }, + validateString{ + Validators: []stringValidator{ + validateStringLegacyURL, + }, + }, + }, + + "s3": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the S3 API", + }, + validateString{ + Validators: []stringValidator{ + validateStringLegacyURL, + }, + }, + }, + + "sso": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the IAM Identity Center (formerly known as SSO) API", + }, + validateString{ + Validators: []stringValidator{ + validateStringValidURL, + }, + }, + }, + + "sts": stringAttribute{ + configschema.Attribute{ + Type: cty.String, + Optional: true, + Description: "A custom endpoint for the STS API", + }, + validateString{ + Validators: []stringValidator{ + validateStringLegacyURL, + }, + }, + }, + }, +} + +// PrepareConfig checks the validity of the values in the given +// configuration, and inserts any missing defaults, assuming that its +// structure has already been validated per the schema returned by +// ConfigSchema. +func (b *Backend) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if obj.IsNull() { + return obj, diags + } + + var attrPath cty.Path + + attrPath = cty.GetAttrPath("bucket") + if val := obj.GetAttr("bucket"); val.IsNull() { + diags = diags.Append(requiredAttributeErrDiag(attrPath)) + } else { + bucketValidators := validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + }, + } + bucketValidators.ValidateAttr(val, attrPath, &diags) + } + + attrPath = cty.GetAttrPath("key") + if val := obj.GetAttr("key"); val.IsNull() { + diags = diags.Append(requiredAttributeErrDiag(attrPath)) + } else { + keyValidators := validateString{ + Validators: []stringValidator{ + validateStringNotEmpty, + validateStringS3Path, + validateStringDoesNotContain("//"), + }, + } + keyValidators.ValidateAttr(val, attrPath, &diags) + } + + // Not updating region handling, because validation will be handled by `aws-sdk-go-base` once it is updated + if val := obj.GetAttr("region"); val.IsNull() || val.AsString() == "" { + if os.Getenv("AWS_REGION") == "" && os.Getenv("AWS_DEFAULT_REGION") == "" { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Missing region value", + `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, + cty.GetAttrPath("region"), + )) + } + } + + validateAttributesConflict( + cty.GetAttrPath("kms_key_id"), + cty.GetAttrPath("sse_customer_key"), + )(obj, cty.Path{}, &diags) + + attrPath = cty.GetAttrPath("kms_key_id") + if val := obj.GetAttr("kms_key_id"); !val.IsNull() { + kmsKeyIDValidators := validateString{ + Validators: []stringValidator{ + validateStringKMSKey, + }, + } + kmsKeyIDValidators.ValidateAttr(val, attrPath, &diags) + } + + attrPath = cty.GetAttrPath("workspace_key_prefix") + if val := obj.GetAttr("workspace_key_prefix"); !val.IsNull() { + keyPrefixValidators := validateString{ + Validators: []stringValidator{ + validateStringS3Path, + }, + } + keyPrefixValidators.ValidateAttr(val, attrPath, &diags) + } + + if val := obj.GetAttr("assume_role"); !val.IsNull() { + validateNestedAttribute(assumeRoleSchema, val, cty.GetAttrPath("assume_role"), &diags) + } + + if val := obj.GetAttr("assume_role_with_web_identity"); !val.IsNull() { + validateNestedAttribute(assumeRoleWithWebIdentitySchema, val, cty.GetAttrPath("assume_role_with_web_identity"), &diags) + } + + validateAttributesConflict( + cty.GetAttrPath("shared_credentials_file"), + cty.GetAttrPath("shared_credentials_files"), + )(obj, cty.Path{}, &diags) + + attrPath = cty.GetAttrPath("shared_credentials_file") + if val := obj.GetAttr("shared_credentials_file"); !val.IsNull() { + diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("shared_credentials_files"))) + } + + attrPath = cty.GetAttrPath("dynamodb_table") + if val := obj.GetAttr("dynamodb_table"); !val.IsNull() { + diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("use_lockfile"))) + } + + endpointFields := map[string]string{ + "dynamodb_endpoint": "dynamodb", + "iam_endpoint": "iam", + "endpoint": "s3", + "sts_endpoint": "sts", + } + endpoints := make(map[string]string) + if val := obj.GetAttr("endpoints"); !val.IsNull() { + for _, k := range []string{"dynamodb", "iam", "s3", "sts"} { + if v := val.GetAttr(k); !v.IsNull() { + endpoints[k] = v.AsString() + } + } + } + for k, v := range endpointFields { + if val := obj.GetAttr(k); !val.IsNull() { + diags = diags.Append(deprecatedAttrDiag(cty.GetAttrPath(k), cty.GetAttrPath("endpoints").GetAttr(v))) + if _, ok := endpoints[v]; ok { + diags = diags.Append(wholeBodyErrDiag( + "Conflicting Parameters", + fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`, + pathString(cty.GetAttrPath(k)), + pathString(cty.GetAttrPath("endpoints").GetAttr(v)), + ), + )) + } + } + } + + if val := obj.GetAttr("endpoints"); !val.IsNull() { + validateNestedAttribute(endpointsSchema, val, cty.GetAttrPath("endpoints"), &diags) + } + + endpointValidators := validateString{ + Validators: []stringValidator{ + validateStringLegacyURL, + }, + } + for k := range endpointFields { + if val := obj.GetAttr(k); !val.IsNull() { + attrPath := cty.GetAttrPath(k) + endpointValidators.ValidateAttr(val, attrPath, &diags) + } + } + + if val := obj.GetAttr("ec2_metadata_service_endpoint"); !val.IsNull() { + attrPath := cty.GetAttrPath("ec2_metadata_service_endpoint") + ec2MetadataEndpointValidators := validateString{ + Validators: []stringValidator{ + validateStringValidURL, + }, + } + ec2MetadataEndpointValidators.ValidateAttr(val, attrPath, &diags) + } + + validateAttributesConflict( + cty.GetAttrPath("force_path_style"), + cty.GetAttrPath("use_path_style"), + )(obj, cty.Path{}, &diags) + + attrPath = cty.GetAttrPath("force_path_style") + if val := obj.GetAttr("force_path_style"); !val.IsNull() { + diags = diags.Append(deprecatedAttrDiag(attrPath, cty.GetAttrPath("use_path_style"))) + } + + attrPath = cty.GetAttrPath("retry_mode") + if val := obj.GetAttr("retry_mode"); !val.IsNull() { + retryModeValidators := validateString{ + Validators: []stringValidator{ + validateStringRetryMode, + }, + } + retryModeValidators.ValidateAttr(val, attrPath, &diags) + } + + attrPath = cty.GetAttrPath("ec2_metadata_service_endpoint_mode") + if val := obj.GetAttr("ec2_metadata_service_endpoint_mode"); !val.IsNull() { + endpointModeValidators := validateString{ + Validators: []stringValidator{ + validateStringInSlice(awsbase.EC2MetadataEndpointMode_Values()), + }, + } + endpointModeValidators.ValidateAttr(val, attrPath, &diags) + } + + validateAttributesConflict( + cty.GetAttrPath("allowed_account_ids"), + cty.GetAttrPath("forbidden_account_ids"), + )(obj, cty.Path{}, &diags) + + return obj, diags +} + +// Configure uses the provided configuration to set configuration fields +// within the backend. +// +// The given configuration is assumed to have already been validated +// against the schema returned by ConfigSchema and passed validation +// via PrepareConfig. +func (b *Backend) Configure(obj cty.Value) tfdiags.Diagnostics { + ctx := context.TODO() + log := logger() + log = logWithOperation(log, operationBackendConfigure) + + var diags tfdiags.Diagnostics + if obj.IsNull() { + return diags + } + + var region string + if v, ok := stringAttrOk(obj, "region"); ok { + region = v + } + + if region != "" && !boolAttr(obj, "skip_region_validation") { + if err := validation.SupportedRegion(region); err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid region value", + firstToUpper(err.Error()), + cty.GetAttrPath("region"), + )) + return diags + } + } + + b.bucketName = stringAttr(obj, "bucket") + b.keyName = stringAttr(obj, "key") + + log = log.With( + logKeyBucket, b.bucketName, + logKeyPath, b.keyName, + ) + + b.acl = stringAttr(obj, "acl") + b.workspaceKeyPrefix = stringAttrDefault(obj, "workspace_key_prefix", defaultWorkspaceKeyPrefix) + b.serverSideEncryption = boolAttr(obj, "encrypt") + b.kmsKeyID = stringAttr(obj, "kms_key_id") + b.ddbTable = stringAttr(obj, "dynamodb_table") + b.useLockFile = boolAttr(obj, "use_lockfile") + b.skipS3Checksum = boolAttr(obj, "skip_s3_checksum") + + if _, ok := stringAttrOk(obj, "kms_key_id"); ok { + if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { + diags = diags.Append(wholeBodyErrDiag( + "Invalid encryption configuration", + encryptionKeyConflictEnvVarError, + )) + } + } + + if customerKey, ok := stringAttrOk(obj, "sse_customer_key"); ok { + if len(customerKey) != 44 { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid sse_customer_key value", + "sse_customer_key must be 44 characters in length", + cty.GetAttrPath("sse_customer_key"), + )) + } else { + var err error + if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid sse_customer_key value", + fmt.Sprintf("sse_customer_key must be base64 encoded: %s", err), + cty.GetAttrPath("sse_customer_key"), + )) + } + } + } else if customerKey := os.Getenv("AWS_SSE_CUSTOMER_KEY"); customerKey != "" { + if len(customerKey) != 44 { + diags = diags.Append(tfdiags.WholeContainingBody( + tfdiags.Error, + "Invalid AWS_SSE_CUSTOMER_KEY value", + `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`, + )) + } else { + var err error + if b.customerEncryptionKey, err = base64.StdEncoding.DecodeString(customerKey); err != nil { + diags = diags.Append(tfdiags.WholeContainingBody( + tfdiags.Error, + "Invalid AWS_SSE_CUSTOMER_KEY value", + fmt.Sprintf(`The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded: %s`, err), + )) + } + } + } + + endpointEnvvars := map[string]string{ + "AWS_DYNAMODB_ENDPOINT": "AWS_ENDPOINT_URL_DYNAMODB", + "AWS_IAM_ENDPOINT": "AWS_ENDPOINT_URL_IAM", + "AWS_S3_ENDPOINT": "AWS_ENDPOINT_URL_S3", + "AWS_STS_ENDPOINT": "AWS_ENDPOINT_URL_STS", + "AWS_METADATA_URL": "AWS_EC2_METADATA_SERVICE_ENDPOINT", + } + for envvar, replacement := range endpointEnvvars { + if val := os.Getenv(envvar); val != "" { + diags = diags.Append(deprecatedEnvVarDiag(envvar, replacement)) + } + } + + ctx, baselog := baselogging.NewHcLogger(ctx, log) + + cfg := &awsbase.Config{ + AccessKey: stringAttr(obj, "access_key"), + APNInfo: stdUserAgentProducts(), + CallerDocumentationURL: "https://developer.hashicorp.com/terraform/language/backend/s3", + CallerName: "S3 Backend", + Logger: baselog, + MaxRetries: intAttrDefault(obj, "max_retries", 5), + Profile: stringAttr(obj, "profile"), + HTTPProxyMode: awsbase.HTTPProxyModeLegacy, + Region: stringAttr(obj, "region"), + SecretKey: stringAttr(obj, "secret_key"), + SkipCredsValidation: boolAttr(obj, "skip_credentials_validation"), + SkipRequestingAccountId: boolAttr(obj, "skip_requesting_account_id"), + Token: stringAttr(obj, "token"), + } + + if val, ok := boolAttrOk(obj, "skip_metadata_api_check"); ok { + if val { + cfg.EC2MetadataServiceEnableState = imds.ClientDisabled + } else { + cfg.EC2MetadataServiceEnableState = imds.ClientEnabled + } + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("ec2_metadata_service_endpoint")), + newEnvvarRetriever("AWS_EC2_METADATA_SERVICE_ENDPOINT"), + newEnvvarRetriever("AWS_METADATA_URL"), + ); ok { + cfg.EC2MetadataServiceEndpoint = v + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("ec2_metadata_service_endpoint_mode")), + newEnvvarRetriever("AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE"), + ); ok { + cfg.EC2MetadataServiceEndpointMode = v + } + + if val, ok := stringAttrOk(obj, "shared_credentials_file"); ok { + cfg.SharedCredentialsFiles = []string{ + val, + } + } + if val, ok := stringSetAttrDefaultEnvVarOk(obj, "shared_credentials_files", "AWS_SHARED_CREDENTIALS_FILE"); ok { + cfg.SharedCredentialsFiles = val + } + if val, ok := stringSetAttrDefaultEnvVarOk(obj, "shared_config_files", "AWS_SHARED_CONFIG_FILE"); ok { + cfg.SharedConfigFiles = val + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("custom_ca_bundle")), + newEnvvarRetriever("AWS_CA_BUNDLE"), + ); ok { + cfg.CustomCABundle = v + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("iam")), + newAttributeRetriever(obj, cty.GetAttrPath("iam_endpoint")), + newEnvvarRetriever("AWS_ENDPOINT_URL_IAM"), + newEnvvarRetriever("AWS_IAM_ENDPOINT"), + ); ok { + cfg.IamEndpoint = v + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("sso")), + newEnvvarRetriever("AWS_ENDPOINT_URL_SSO"), + ); ok { + cfg.SsoEndpoint = v + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("sts")), + newAttributeRetriever(obj, cty.GetAttrPath("sts_endpoint")), + newEnvvarRetriever("AWS_ENDPOINT_URL_STS"), + newEnvvarRetriever("AWS_STS_ENDPOINT"), + ); ok { + cfg.StsEndpoint = v + } + + if v, ok := retrieveArgument(&diags, newAttributeRetriever(obj, cty.GetAttrPath("sts_region"))); ok { + cfg.StsRegion = v + } + + if assumeRole := obj.GetAttr("assume_role"); !assumeRole.IsNull() { + ar := awsbase.AssumeRole{} + if val, ok := stringAttrOk(assumeRole, "role_arn"); ok { + ar.RoleARN = val + } + if val, ok := stringAttrOk(assumeRole, "duration"); ok { + duration, _ := time.ParseDuration(val) + ar.Duration = duration + } + if val, ok := stringAttrOk(assumeRole, "external_id"); ok { + ar.ExternalID = val + } + if val, ok := stringAttrOk(assumeRole, "policy"); ok { + ar.Policy = strings.TrimSpace(val) + } + if val, ok := stringSetAttrOk(assumeRole, "policy_arns"); ok { + ar.PolicyARNs = val + } + if val, ok := stringAttrOk(assumeRole, "session_name"); ok { + ar.SessionName = val + } + if val, ok := stringAttrOk(assumeRole, "source_identity"); ok { + ar.SourceIdentity = val + } + if val, ok := stringMapAttrOk(assumeRole, "tags"); ok { + ar.Tags = val + } + if val, ok := stringSetAttrOk(assumeRole, "transitive_tag_keys"); ok { + ar.TransitiveTagKeys = val + } + cfg.AssumeRole = []awsbase.AssumeRole{ar} + } + + if assumeRoleWithWebIdentity := obj.GetAttr("assume_role_with_web_identity"); !assumeRoleWithWebIdentity.IsNull() { + ar := &awsbase.AssumeRoleWithWebIdentity{} + if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "role_arn"); ok { + ar.RoleARN = val + } + if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "duration"); ok { + duration, _ := time.ParseDuration(val) + ar.Duration = duration + } + if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "policy"); ok { + ar.Policy = strings.TrimSpace(val) + } + if val, ok := stringSetAttrOk(assumeRoleWithWebIdentity, "policy_arns"); ok { + ar.PolicyARNs = val + } + if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "session_name"); ok { + ar.SessionName = val + } + if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "web_identity_token"); ok { + ar.WebIdentityToken = val + } + if val, ok := stringAttrOk(assumeRoleWithWebIdentity, "web_identity_token_file"); ok { + ar.WebIdentityTokenFile = val + } + cfg.AssumeRoleWithWebIdentity = ar + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("http_proxy")), + ); ok { + cfg.HTTPProxy = aws.String(v) + } + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("https_proxy")), + ); ok { + cfg.HTTPSProxy = aws.String(v) + } + if val, ok := stringAttrOk(obj, "no_proxy"); ok { + cfg.NoProxy = val + } + + if val, ok := boolAttrOk(obj, "insecure"); ok { + cfg.Insecure = val + } + if val, ok := boolAttrDefaultEnvVarOk(obj, "use_fips_endpoint", "AWS_USE_FIPS_ENDPOINT"); ok { + cfg.UseFIPSEndpoint = val + } + if val, ok := boolAttrDefaultEnvVarOk(obj, "use_dualstack_endpoint", "AWS_USE_DUALSTACK_ENDPOINT"); ok { + cfg.UseDualStackEndpoint = val + } + + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("retry_mode")), + newEnvvarRetriever("AWS_RETRY_MODE"), + ); ok { + cfg.RetryMode = aws.RetryMode(v) + } + + if val, ok := stringSetAttrOk(obj, "allowed_account_ids"); ok { + cfg.AllowedAccountIds = val + } + if val, ok := stringSetAttrOk(obj, "forbidden_account_ids"); ok { + cfg.ForbiddenAccountIds = val + } + + _ /* ctx */, awsConfig, cfgDiags := awsbase.GetAwsConfig(ctx, cfg) + for _, d := range cfgDiags { + diags = diags.Append(tfdiags.Sourceless( + baseSeverityToTerraformSeverity(d.Severity()), + d.Summary(), + d.Detail(), + )) + } + if diags.HasErrors() { + return diags + } + b.awsConfig = awsConfig + + accountID, _, awsDiags := awsbase.GetAwsAccountIDAndPartition(ctx, awsConfig, cfg) + for _, d := range awsDiags { + diags = append(diags, tfdiags.Sourceless( + baseSeverityToTerraformSeverity(d.Severity()), + fmt.Sprintf("Retrieving AWS account details: %s", d.Summary()), + d.Detail(), + )) + } + + err := cfg.VerifyAccountIDAllowed(accountID) + if err != nil { + diags = append(diags, tfdiags.Sourceless( + tfdiags.Error, + "Invalid account ID", + err.Error(), + )) + } + + b.dynClient = dynamodb.NewFromConfig(awsConfig, func(opts *dynamodb.Options) { + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("dynamodb")), + newAttributeRetriever(obj, cty.GetAttrPath("dynamodb_endpoint")), + newEnvvarRetriever("AWS_ENDPOINT_URL_DYNAMODB"), + newEnvvarRetriever("AWS_DYNAMODB_ENDPOINT"), + ); ok { + opts.EndpointResolver = dynamodb.EndpointResolverFromURL(v) //nolint:staticcheck // The replacement is not documented yet (2023/08/03) + } + }) + + b.s3Client = s3.NewFromConfig(awsConfig, s3.WithAPIOptions(addS3WrongRegionErrorMiddleware), + func(opts *s3.Options) { + if v, ok := retrieveArgument(&diags, + newAttributeRetriever(obj, cty.GetAttrPath("endpoints").GetAttr("s3")), + newAttributeRetriever(obj, cty.GetAttrPath("endpoint")), + newEnvvarRetriever("AWS_ENDPOINT_URL_S3"), + newEnvvarRetriever("AWS_S3_ENDPOINT"), + ); ok { + opts.EndpointResolver = s3.EndpointResolverFromURL(v) //nolint:staticcheck // The replacement is not documented yet (2023/08/03) + } + if v, ok := boolAttrOk(obj, "force_path_style"); ok { // deprecated + opts.UsePathStyle = v + } + if v, ok := boolAttrOk(obj, "use_path_style"); ok { + opts.UsePathStyle = v + } + }) + + return diags +} + +func stdUserAgentProducts() *awsbase.APNInfo { + return &awsbase.APNInfo{ + PartnerName: "HashiCorp", + Products: []awsbase.UserAgentProduct{ + {Name: "Terraform", Version: version.String(), Comment: "+https://www.terraform.io"}, + }, + } +} + +type argumentRetriever interface { + Retrieve(diags *tfdiags.Diagnostics) (string, bool) +} + +type attributeRetriever struct { + obj cty.Value + objPath cty.Path + attrPath cty.Path +} + +var _ argumentRetriever = attributeRetriever{} + +func newAttributeRetriever(obj cty.Value, attrPath cty.Path) attributeRetriever { + return attributeRetriever{ + obj: obj, + objPath: cty.Path{}, // Assumes that we're working relative to the root object + attrPath: attrPath, + } +} + +func (r attributeRetriever) Retrieve(diags *tfdiags.Diagnostics) (string, bool) { + val, err := pathSafeApply(r.attrPath, r.obj) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid Path for Schema", + "The S3 Backend unexpectedly provided a path that does not match the schema. "+ + "Please report this to the developers.\n\n"+ + "Path: "+pathString(r.attrPath)+"\n\n"+ + "Error: "+err.Error(), + r.objPath, + )) + } + return stringValueOk(val) +} + +// pathSafeApply applies a `cty.Path` to a `cty.Value`. +// Unlike `path.Apply`, it does not return an error if it encounters a Null value +func pathSafeApply(path cty.Path, obj cty.Value) (cty.Value, error) { + if obj == cty.NilVal || obj.IsNull() { + return obj, nil + } + val := obj + var err error + for _, step := range path { + val, err = step.Apply(val) + if err != nil { + return cty.NilVal, err + } + if val == cty.NilVal || val.IsNull() { + return val, nil + } + } + return val, nil +} + +type envvarRetriever struct { + name string +} + +var _ argumentRetriever = envvarRetriever{} + +func newEnvvarRetriever(name string) envvarRetriever { + return envvarRetriever{ + name: name, + } +} + +func (r envvarRetriever) Retrieve(_ *tfdiags.Diagnostics) (string, bool) { + if v := os.Getenv(r.name); v != "" { + return v, true + } + return "", false +} + +func retrieveArgument(diags *tfdiags.Diagnostics, retrievers ...argumentRetriever) (string, bool) { + for _, retriever := range retrievers { + if v, ok := retriever.Retrieve(diags); ok { + return v, true + } + } + return "", false +} + +func stringValue(val cty.Value) string { + v, _ := stringValueOk(val) + return v +} + +func stringValueOk(val cty.Value) (string, bool) { + if val.IsNull() { + return "", false + } else { + return val.AsString(), true + } +} + +func stringAttr(obj cty.Value, name string) string { + return stringValue(obj.GetAttr(name)) +} + +func stringAttrOk(obj cty.Value, name string) (string, bool) { + return stringValueOk(obj.GetAttr(name)) +} + +func stringAttrDefault(obj cty.Value, name, def string) string { + if v, ok := stringAttrOk(obj, name); !ok { + return def + } else { + return v + } +} + +func stringSetValueOk(val cty.Value) ([]string, bool) { + var list []string + typ := val.Type() + if !typ.IsSetType() { + return nil, false + } + err := gocty.FromCtyValue(val, &list) + if err != nil { + return nil, false + } + return list, true +} + +func stringSetAttrOk(obj cty.Value, name string) ([]string, bool) { + return stringSetValueOk(obj.GetAttr(name)) +} + +// stringSetAttrDefaultEnvVarOk checks for a configured set of strings +// in the provided argument name or environment variables. A configured +// argument takes precedent over environment variables. An environment +// variable is assumed to be as a single item, such as how the singular +// AWS_SHARED_CONFIG_FILE variable aligns with the underlying +// shared_config_files argument. +func stringSetAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) ([]string, bool) { + if v, ok := stringSetValueOk(obj.GetAttr(name)); !ok { + for _, envvar := range envvars { + if v := os.Getenv(envvar); v != "" { + return []string{v}, true + } + } + return nil, false + } else { + return v, true + } +} + +func stringMapValueOk(val cty.Value) (map[string]string, bool) { + var m map[string]string + err := gocty.FromCtyValue(val, &m) + if err != nil { + return nil, false + } + return m, true +} + +func stringMapAttrOk(obj cty.Value, name string) (map[string]string, bool) { + return stringMapValueOk(obj.GetAttr(name)) +} + +func boolAttr(obj cty.Value, name string) bool { + v, _ := boolAttrOk(obj, name) + return v +} + +func boolAttrOk(obj cty.Value, name string) (bool, bool) { + if val := obj.GetAttr(name); val.IsNull() { + return false, false + } else { + return val.True(), true + } +} + +// boolAttrDefaultEnvVarOk checks for a configured bool argument or a non-empty +// value in any of the provided environment variables. If any of the environment +// variables are non-empty, to boolean is considered true. +func boolAttrDefaultEnvVarOk(obj cty.Value, name string, envvars ...string) (bool, bool) { + if val := obj.GetAttr(name); val.IsNull() { + for _, envvar := range envvars { + if v := os.Getenv(envvar); v != "" { + return true, true + } + } + return false, false + } else { + return val.True(), true + } +} + +func intAttr(obj cty.Value, name string) int { + v, _ := intAttrOk(obj, name) + return v +} + +func intAttrOk(obj cty.Value, name string) (int, bool) { + if val := obj.GetAttr(name); val.IsNull() { + return 0, false + } else { + var v int + if err := gocty.FromCtyValue(val, &v); err != nil { + return 0, false + } + return v, true + } +} + +func intAttrDefault(obj cty.Value, name string, def int) int { + if v, ok := intAttrOk(obj, name); !ok { + return def + } else { + return v + } +} + +const encryptionKeyConflictEnvVarError = `Only one of "kms_key_id" and the environment variable "AWS_SSE_CUSTOMER_KEY" can be set. + +The "kms_key_id" is used for encryption with KMS-Managed Keys (SSE-KMS) +while "AWS_SSE_CUSTOMER_KEY" is used for encryption with customer-managed keys (SSE-C). Please choose one or the other.` + +func validateNestedAttribute(objSchema schemaAttribute, obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) { + if obj.IsNull() { + return + } + + na, ok := objSchema.(singleNestedAttribute) + if !ok { + return + } + + validator := objSchema.Validator() + validator.ValidateAttr(obj, objPath, diags) + + for name, attrSchema := range na.Attributes { + attrPath := objPath.GetAttr(name) + attrVal := obj.GetAttr(name) + + if attrVal.IsNull() { + if attrSchema.SchemaAttribute().Required { + *diags = diags.Append(requiredAttributeErrDiag(attrPath)) + } + continue + } + + if a, e := attrVal.Type(), attrSchema.SchemaAttribute().Type; a != e { + *diags = diags.Append(attributeErrDiag( + "Internal Error", + fmt.Sprintf(`Expected type to be %s, got: %s`, e.FriendlyName(), a.FriendlyName()), + attrPath, + )) + continue + } + + if attrVal.IsNull() { + if attrSchema.SchemaAttribute().Required { + *diags = diags.Append(requiredAttributeErrDiag(attrPath)) + } + continue + } + + validator := attrSchema.Validator() + validator.ValidateAttr(attrVal, attrPath, diags) + } +} + +func requiredAttributeErrDiag(path cty.Path) tfdiags.Diagnostic { + return attributeErrDiag( + "Missing Required Value", + fmt.Sprintf("The attribute %q is required by the backend.\n\n", pathString(path))+ + "Refer to the backend documentation for additional information which attributes are required.", + path, + ) +} + +func pathString(path cty.Path) string { + var buf strings.Builder + for i, step := range path { + switch x := step.(type) { + case cty.GetAttrStep: + if i != 0 { + buf.WriteString(".") + } + buf.WriteString(x.Name) + case cty.IndexStep: + val := x.Key + typ := val.Type() + var s string + switch { + case typ == cty.String: + s = val.AsString() + case typ == cty.Number: + num := val.AsBigFloat() + s = num.String() + default: + s = fmt.Sprintf("", typ.FriendlyName()) + } + buf.WriteString(fmt.Sprintf("[%s]", s)) + default: + if i != 0 { + buf.WriteString(".") + } + buf.WriteString(fmt.Sprintf("", x)) + } + } + return buf.String() +} + +type validateSchema interface { + ValidateAttr(cty.Value, cty.Path, *tfdiags.Diagnostics) +} + +type validateString struct { + Validators []stringValidator +} + +func (v validateString) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { + s := val.AsString() + for _, validator := range v.Validators { + validator(s, attrPath, diags) + if diags.HasErrors() { + return + } + } +} + +type validateMap struct{} + +func (v validateMap) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) {} + +type validateSet struct { + Validators []setValidator +} + +func (v validateSet) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { + for _, validator := range v.Validators { + validator(val, attrPath, diags) + if diags.HasErrors() { + return + } + } +} + +type validateObject struct { + Validators []objectValidator +} + +func (v validateObject) ValidateAttr(val cty.Value, attrPath cty.Path, diags *tfdiags.Diagnostics) { + for _, validator := range v.Validators { + validator(val, attrPath, diags) + } +} + +type schemaAttribute interface { + SchemaAttribute() *configschema.Attribute + Validator() validateSchema +} + +var _ schemaAttribute = stringAttribute{} + +type stringAttribute struct { + configschema.Attribute + validateString +} + +func (a stringAttribute) SchemaAttribute() *configschema.Attribute { + return &a.Attribute +} + +func (a stringAttribute) Validator() validateSchema { + return a.validateString +} + +var _ schemaAttribute = setAttribute{} + +type setAttribute struct { + configschema.Attribute + validateSet +} + +func (a setAttribute) SchemaAttribute() *configschema.Attribute { + return &a.Attribute +} + +func (a setAttribute) Validator() validateSchema { + return a.validateSet +} + +var _ schemaAttribute = mapAttribute{} + +type mapAttribute struct { + configschema.Attribute + validateMap +} + +func (a mapAttribute) SchemaAttribute() *configschema.Attribute { + return &a.Attribute +} + +func (a mapAttribute) Validator() validateSchema { + return a.validateMap +} + +type objectSchema map[string]schemaAttribute + +func (s objectSchema) SchemaAttributes() map[string]*configschema.Attribute { + m := make(map[string]*configschema.Attribute, len(s)) + for k, v := range s { + m[k] = v.SchemaAttribute() + } + return m +} + +var _ schemaAttribute = singleNestedAttribute{} + +type singleNestedAttribute struct { + Attributes objectSchema + Required bool + validateObject +} + +func (a singleNestedAttribute) SchemaAttribute() *configschema.Attribute { + return &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: a.Attributes.SchemaAttributes(), + }, + Required: a.Required, + Optional: !a.Required, + } +} + +func (a singleNestedAttribute) Validator() validateSchema { + return a.validateObject +} + +func deprecatedAttrDiag(attr, replacement cty.Path) tfdiags.Diagnostic { + return attributeWarningDiag( + "Deprecated Parameter", + fmt.Sprintf(`The parameter "%s" is deprecated. Use parameter "%s" instead.`, pathString(attr), pathString(replacement)), + attr, + ) +} + +func deprecatedEnvVarDiag(envvar, replacement string) tfdiags.Diagnostic { + return wholeBodyWarningDiag( + "Deprecated Environment Variable", + fmt.Sprintf(`The environment variable "%s" is deprecated. Use environment variable "%s" instead.`, envvar, replacement), + ) +} + +func firstToUpper(s string) string { + r, size := utf8.DecodeRuneInString(s) + if r == utf8.RuneError && size <= 1 { + return s + } + lc := unicode.ToUpper(r) + if r == lc { + return s + } + return string(lc) + s[size:] +} diff --git a/internal/backend/remote-state/s3/backend_complete_test.go b/internal/backend/remote-state/s3/backend_complete_test.go new file mode 100644 index 0000000000..5e42e36fd3 --- /dev/null +++ b/internal/backend/remote-state/s3/backend_complete_test.go @@ -0,0 +1,2448 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "context" + "fmt" + "os" + "path/filepath" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + configtesting "github.com/hashicorp/aws-sdk-go-base/v2/configtesting" + "github.com/hashicorp/aws-sdk-go-base/v2/mockdata" + "github.com/hashicorp/aws-sdk-go-base/v2/servicemocks" + "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +const ( + // Shockingly, this is not defined in the SDK + sharedConfigCredentialsProvider = "SharedConfigCredentials" +) + +type DiagsValidator func(*testing.T, tfdiags.Diagnostics) + +func ExpectNoDiags(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + + expectDiagsCount(t, diags, 0) +} + +func expectDiagsCount(t *testing.T, diags tfdiags.Diagnostics, c int) { + t.Helper() + + if l := len(diags); l != c { + t.Fatalf("Diagnostics: expected %d element, got %d\n%s", c, l, diagnosticsString(diags)) + } +} + +func ExpectDiagsEqual(expected tfdiags.Diagnostics) DiagsValidator { + return func(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + + if diff := cmp.Diff(diags, expected, tfdiags.DiagnosticComparer); diff != "" { + t.Fatalf("unexpected diagnostics difference: %s", diff) + } + } +} + +// ExpectDiagMatching returns a validator expeceting a single Diagnostic with fields matching the expectation +func ExpectDiagMatching(severity tfdiags.Severity, summary matcher, detail matcher) DiagsValidator { + return ExpectDiags( + diagMatching(severity, summary, detail), + ) +} + +type diagValidator func(*testing.T, tfdiags.Diagnostic) + +func ExpectDiags(validators ...diagValidator) DiagsValidator { + return func(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + count := len(validators) + if l := len(diags); l < count { + count = l + } + + for i := 0; i < count; i++ { + validators[i](t, diags[i]) + } + + expectDiagsCount(t, diags, len(validators)) + } +} + +func diagMatching(severity tfdiags.Severity, summary matcher, detail matcher) diagValidator { + return func(t *testing.T, diag tfdiags.Diagnostic) { + t.Helper() + if severity != diag.Severity() || !summary.Match(diag.Description().Summary) || !detail.Match(diag.Description().Detail) { + t.Errorf("expected Diagnostic matching %#v, got %#v", + tfdiags.Sourceless( + severity, + summary.String(), + detail.String(), + ), + diag, + ) + } + } +} + +type matcher interface { + fmt.Stringer + Match(string) bool +} + +type equalsMatcher string + +func (m equalsMatcher) Match(s string) bool { + return string(m) == s +} + +func (m equalsMatcher) String() string { + return string(m) +} + +type regexpMatcher struct { + re *regexp.Regexp +} + +func newRegexpMatcher(re string) regexpMatcher { + return regexpMatcher{ + re: regexp.MustCompile(re), + } +} + +func (m regexpMatcher) Match(s string) bool { + return m.re.MatchString(s) +} + +func (m regexpMatcher) String() string { + return m.re.String() +} + +type ignoreMatcher struct{} + +func (m ignoreMatcher) Match(s string) bool { + return true +} + +func (m ignoreMatcher) String() string { + return "ignored" +} + +// Corrected from aws-sdk-go-base v1 & v2 +const mockStsAssumeRolePolicy = `{ + "Version": "2012-10-17", + "Statement": { + "Effect": "Allow", + "Action": "*", + "Resource": "*" + } + }` + +func TestBackendConfig_Authentication(t *testing.T) { + testCases := map[string]struct { + config map[string]any + EnableEc2MetadataServer bool + EnableEcsCredentialsServer bool + EnableWebIdentityEnvVars bool + // EnableWebIdentityConfig bool // Not supported + EnvironmentVariables map[string]string + ExpectedCredentialsValue aws.Credentials + MockStsEndpoints []*servicemocks.MockEndpoint + SharedConfigurationFile string + SharedCredentialsFile string + ValidateDiags DiagsValidator + }{ + "empty config": { + config: map[string]any{}, + ValidateDiags: ExpectDiagMatching( + tfdiags.Error, + equalsMatcher("No valid credential sources found"), + ignoreMatcher{}, + ), + }, + + "config AccessKey": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ExpectedCredentialsValue: mockdata.MockStaticCredentials, + ValidateDiags: ExpectNoDiags, + }, + + "config Profile shared credentials profile aws_access_key_id": { + config: map[string]any{ + "profile": "SharedCredentialsProfile", + }, + ExpectedCredentialsValue: aws.Credentials{ + AccessKeyID: "ProfileSharedCredentialsAccessKey", + SecretAccessKey: "ProfileSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey + +[SharedCredentialsProfile] +aws_access_key_id = ProfileSharedCredentialsAccessKey +aws_secret_access_key = ProfileSharedCredentialsSecretKey +`, + ValidateDiags: ExpectNoDiags, + }, + + "environment AWS_ACCESS_KEY_ID does not override config Profile": { + config: map[string]any{ + "profile": "SharedCredentialsProfile", + }, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: aws.Credentials{ + AccessKeyID: "ProfileSharedCredentialsAccessKey", + SecretAccessKey: "ProfileSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey + +[SharedCredentialsProfile] +aws_access_key_id = ProfileSharedCredentialsAccessKey +aws_secret_access_key = ProfileSharedCredentialsSecretKey +`, + ValidateDiags: ExpectNoDiags, + }, + + "environment AWS_ACCESS_KEY_ID": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockEnvCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectNoDiags, + }, + + "environment AWS_PROFILE shared credentials profile aws_access_key_id": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedCredentialsProfile", + }, + ExpectedCredentialsValue: aws.Credentials{ + AccessKeyID: "ProfileSharedCredentialsAccessKey", + SecretAccessKey: "ProfileSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey + +[SharedCredentialsProfile] +aws_access_key_id = ProfileSharedCredentialsAccessKey +aws_secret_access_key = ProfileSharedCredentialsSecretKey +`, + ValidateDiags: ExpectNoDiags, + }, + + "environment AWS_SESSION_TOKEN": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + "AWS_SESSION_TOKEN": servicemocks.MockEnvSessionToken, + }, + ExpectedCredentialsValue: mockdata.MockEnvCredentialsWithSessionToken, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "shared credentials default aws_access_key_id": { + config: map[string]any{}, + ExpectedCredentialsValue: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + }, + + "web identity token access key": { + config: map[string]any{}, + EnableWebIdentityEnvVars: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "EC2 metadata access key": { + config: map[string]any{}, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockEc2MetadataCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectNoDiags, + }, + + "ECS credentials access key": { + config: map[string]any{}, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockEcsCredentialsCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "config AccessKey over environment AWS_ACCESS_KEY_ID": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStaticCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectNoDiags, + }, + + "config AccessKey over shared credentials default aws_access_key_id": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStaticCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + ValidateDiags: ExpectNoDiags, + }, + + "config AccessKey over EC2 metadata access key": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStaticCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "config AccessKey over ECS credentials access key": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockStaticCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "environment AWS_ACCESS_KEY_ID over shared credentials default aws_access_key_id": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockEnvCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + ValidateDiags: ExpectNoDiags, + }, + + "environment AWS_ACCESS_KEY_ID over EC2 metadata access key": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockEnvCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "environment AWS_ACCESS_KEY_ID over ECS credentials access key": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockEnvCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "shared credentials default aws_access_key_id over EC2 metadata access key": { + config: map[string]any{}, + ExpectedCredentialsValue: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + }, + + "shared credentials default aws_access_key_id over ECS credentials access key": { + config: map[string]any{}, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + }, + + "ECS credentials access key over EC2 metadata access key": { + config: map[string]any{}, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockEcsCredentialsCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "retrieve region from shared configuration file": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStaticCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: ` +[default] +region = us-east-1 +`, + }, + + "skip EC2 Metadata API check": { + config: map[string]any{ + "skip_metadata_api_check": true, + }, + // The IMDS server must be enabled so that auth will succeed if the IMDS is called + EnableEc2MetadataServer: true, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiagMatching( + tfdiags.Error, + equalsMatcher("No valid credential sources found"), + ignoreMatcher{}, + ), + }, + + "invalid profile name from envvar": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "no-such-profile", + }, + SharedCredentialsFile: ` +[some-profile] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + ValidateDiags: ExpectDiagMatching( + tfdiags.Error, + equalsMatcher("failed to get shared config profile, no-such-profile"), + equalsMatcher(""), + ), + }, + + "invalid profile name from config": { + config: map[string]any{ + "profile": "no-such-profile", + }, + SharedCredentialsFile: ` +[some-profile] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + ValidateDiags: ExpectDiagMatching( + tfdiags.Error, + equalsMatcher("failed to get shared config profile, no-such-profile"), + equalsMatcher(""), + ), + }, + + "AWS_ACCESS_KEY_ID overrides AWS_PROFILE": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + "AWS_PROFILE": "SharedCredentialsProfile", + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey + +[SharedCredentialsProfile] +aws_access_key_id = ProfileSharedCredentialsAccessKey +aws_secret_access_key = ProfileSharedCredentialsSecretKey +`, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ExpectedCredentialsValue: mockdata.MockEnvCredentials, + ValidateDiags: ExpectNoDiags, + }, + + "AWS_ACCESS_KEY_ID does not override invalid profile name from envvar": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + "AWS_PROFILE": "no-such-profile", + }, + SharedCredentialsFile: ` +[some-profile] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + ValidateDiags: ExpectDiagMatching( + tfdiags.Error, + equalsMatcher("failed to get shared config profile, no-such-profile"), + equalsMatcher(""), + ), + }, + } + + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + servicemocks.InitSessionTestEnv(t) + + ctx := context.TODO() + + // Populate required fields + tc.config["region"] = "us-east-1" + tc.config["bucket"] = "bucket" + tc.config["key"] = "key" + + if tc.ValidateDiags == nil { + tc.ValidateDiags = ExpectNoDiags + } + + if tc.EnableEc2MetadataServer { + closeEc2Metadata := servicemocks.AwsMetadataApiMock(append( + servicemocks.Ec2metadata_securityCredentialsEndpoints, + servicemocks.Ec2metadata_instanceIdEndpoint, + servicemocks.Ec2metadata_iamInfoEndpoint, + )) + defer closeEc2Metadata() + } + + if tc.EnableEcsCredentialsServer { + closeEcsCredentials := servicemocks.EcsCredentialsApiMock() + defer closeEcsCredentials() + } + + if tc.EnableWebIdentityEnvVars /*|| tc.EnableWebIdentityConfig*/ { + file, err := os.CreateTemp("", "aws-sdk-go-base-web-identity-token-file") + if err != nil { + t.Fatalf("unexpected error creating temporary web identity token file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(servicemocks.MockWebIdentityToken), 0600) + + if err != nil { + t.Fatalf("unexpected error writing web identity token file: %s", err) + } + + if tc.EnableWebIdentityEnvVars { + t.Setenv("AWS_ROLE_ARN", servicemocks.MockStsAssumeRoleWithWebIdentityArn) + t.Setenv("AWS_ROLE_SESSION_NAME", servicemocks.MockStsAssumeRoleWithWebIdentitySessionName) + t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", file.Name()) + } /*else if tc.EnableWebIdentityConfig { + tc.Config.AssumeRoleWithWebIdentity = &AssumeRoleWithWebIdentity{ + RoleARN: servicemocks.MockStsAssumeRoleWithWebIdentityArn, + SessionName: servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + WebIdentityTokenFile: file.Name(), + } + }*/ + } + + ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints) + defer ts.Close() + + tc.config["endpoints"] = map[string]any{ + "sts": ts.URL, + } + + if tc.SharedConfigurationFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared configuration file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared configuration file: %s", err) + } + + setSharedConfigFile(t, file.Name()) + } + + if tc.SharedCredentialsFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-credentials-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared credentials file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedCredentialsFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared credentials file: %s", err) + } + + tc.config["shared_credentials_files"] = []interface{}{file.Name()} + if tc.ExpectedCredentialsValue.Source == sharedConfigCredentialsProvider { + tc.ExpectedCredentialsValue.Source = sharedConfigCredentialsSource(file.Name()) + } + } + + for k, v := range tc.EnvironmentVariables { + t.Setenv(k, v) + } + + b, diags := configureBackend(t, tc.config) + + tc.ValidateDiags(t, diags) + + if diags.HasErrors() { + return + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials") + } + + if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" { + t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff) + } + }) + } +} + +func TestBackendConfig_Authentication_AssumeRoleNested(t *testing.T) { + testCases := map[string]struct { + config map[string]any + EnableEc2MetadataServer bool + EnableEcsCredentialsServer bool + EnvironmentVariables map[string]string + ExpectedCredentialsValue aws.Credentials + MockStsEndpoints []*servicemocks.MockEndpoint + SharedConfigurationFile string + SharedCredentialsFile string + ValidateDiags DiagsValidator + }{ + // WAS: "config AccessKey config AssumeRoleARN access key" + "from config access_key": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "environment AWS_ACCESS_KEY_ID config AssumeRoleARN access key" + "from environment AWS_ACCESS_KEY_ID": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + EnvironmentVariables: map[string]string{ + "AWS_ACCESS_KEY_ID": servicemocks.MockEnvAccessKey, + "AWS_SECRET_ACCESS_KEY": servicemocks.MockEnvSecretKey, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config Profile shared configuration credential_source Ec2InstanceMetadata" + "from config Profile with Ec2InstanceMetadata source": { + config: map[string]any{ + "profile": "SharedConfigurationProfile", + }, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +credential_source = Ec2InstanceMetadata +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + // WAS: "environment AWS_PROFILE shared configuration credential_source Ec2InstanceMetadata" + "from environment AWS_PROFILE with Ec2InstanceMetadata source": { + config: map[string]any{}, + EnableEc2MetadataServer: true, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +credential_source = Ec2InstanceMetadata +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + // WAS: "config Profile shared configuration source_profile" + "from config Profile with source profile": { + config: map[string]any{ + "profile": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +role_arn = %[1]s +role_session_name = %[2]s +source_profile = SharedConfigurationSourceProfile + +[profile SharedConfigurationSourceProfile] +aws_access_key_id = SharedConfigurationSourceAccessKey +aws_secret_access_key = SharedConfigurationSourceSecretKey +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + // WAS: "environment AWS_PROFILE shared configuration source_profile" + "from environment AWS_PROFILE with source profile": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_PROFILE": "SharedConfigurationProfile", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedConfigurationFile: fmt.Sprintf(` +[profile SharedConfigurationProfile] +role_arn = %[1]s +role_session_name = %[2]s +source_profile = SharedConfigurationSourceProfile + +[profile SharedConfigurationSourceProfile] +aws_access_key_id = SharedConfigurationSourceAccessKey +aws_secret_access_key = SharedConfigurationSourceSecretKey +`, servicemocks.MockStsAssumeRoleArn, servicemocks.MockStsAssumeRoleSessionName), + }, + + // WAS: "shared credentials default aws_access_key_id config AssumeRoleARN access key" + "from default profile": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + SharedCredentialsFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +`, + }, + + // WAS: "EC2 metadata access key config AssumeRoleARN access key" + "from EC2 metadata": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + EnableEc2MetadataServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "ECS credentials access key config AssumeRoleARN access key" + "from ECS credentials": { + config: map[string]any{ + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + EnableEcsCredentialsServer: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRoleDuration" + "with duration": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "duration": "1h", + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"DurationSeconds": "3600"}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRoleExternalID" + "with external ID": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "external_id": servicemocks.MockStsAssumeRoleExternalId, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"ExternalId": servicemocks.MockStsAssumeRoleExternalId}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRolePolicy" + "with policy": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "policy": mockStsAssumeRolePolicy, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Policy": mockStsAssumeRolePolicy}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRolePolicyARNs" + "with policy ARNs": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "policy_arns": []any{servicemocks.MockStsAssumeRolePolicyArn}, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"PolicyArns.member.1.arn": servicemocks.MockStsAssumeRolePolicyArn}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRoleTags" + "with tags": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "tags": map[string]any{ + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, + }, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRoleTransitiveTagKeys" + "with transitive tags": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "tags": map[string]any{ + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, + }, + "transitive_tag_keys": []any{servicemocks.MockStsAssumeRoleTagKey}, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"Tags.member.1.Key": servicemocks.MockStsAssumeRoleTagKey, "Tags.member.1.Value": servicemocks.MockStsAssumeRoleTagValue, "TransitiveTagKeys.member.1": servicemocks.MockStsAssumeRoleTagKey}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "config AssumeRoleSourceIdentity" + "with source identity": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + "source_identity": servicemocks.MockStsAssumeRoleSourceIdentity, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleValidEndpointWithOptions(map[string]string{"SourceIdentity": servicemocks.MockStsAssumeRoleSourceIdentity}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // WAS: "assume role error" + "error": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, + }, + }, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleInvalidEndpointInvalidClientTokenId, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + ValidateDiags: ExpectDiags( + diagMatching( + tfdiags.Error, + equalsMatcher("Cannot assume IAM Role"), + newRegexpMatcher(`IAM Role \(.+\) cannot be assumed.`), + ), + ), + }, + + "empty role_arn": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": "", + }, + }, + ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + cty.GetAttrPath("assume_role").GetAttr("role_arn"), + ), + }), + }, + + "nil role_arn": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "assume_role": map[string]any{ + "role_arn": nil, + }, + }, + ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ + attributeErrDiag( + "Missing Required Value", + `The attribute "assume_role.role_arn" is required by the backend.`+"\n\n"+ + "Refer to the backend documentation for additional information which attributes are required.", + cty.GetAttrPath("assume_role").GetAttr("role_arn"), + ), + }), + }, + } + + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + servicemocks.InitSessionTestEnv(t) + + ctx := context.TODO() + + // Populate required fields + tc.config["region"] = "us-east-1" + tc.config["bucket"] = "bucket" + tc.config["key"] = "key" + + if tc.ValidateDiags == nil { + tc.ValidateDiags = ExpectNoDiags + } + + if tc.EnableEc2MetadataServer { + closeEc2Metadata := servicemocks.AwsMetadataApiMock(append( + servicemocks.Ec2metadata_securityCredentialsEndpoints, + servicemocks.Ec2metadata_instanceIdEndpoint, + servicemocks.Ec2metadata_iamInfoEndpoint, + )) + defer closeEc2Metadata() + } + + if tc.EnableEcsCredentialsServer { + closeEcsCredentials := servicemocks.EcsCredentialsApiMock() + defer closeEcsCredentials() + } + + ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints) + defer ts.Close() + + tc.config["endpoints"] = map[string]any{ + "sts": ts.URL, + } + + if tc.SharedConfigurationFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared configuration file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared configuration file: %s", err) + } + + setSharedConfigFile(t, file.Name()) + } + + if tc.SharedCredentialsFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-credentials-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared credentials file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedCredentialsFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared credentials file: %s", err) + } + + tc.config["shared_credentials_files"] = []interface{}{file.Name()} + if tc.ExpectedCredentialsValue.Source == sharedConfigCredentialsProvider { + tc.ExpectedCredentialsValue.Source = sharedConfigCredentialsSource(file.Name()) + } + } + + for k, v := range tc.EnvironmentVariables { + t.Setenv(k, v) + } + + b, diags := configureBackend(t, tc.config) + + tc.ValidateDiags(t, diags) + + if diags.HasErrors() { + return + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials: %s", err) + } + + if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" { + t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff) + } + }) + } +} + +func TestBackendConfig_Authentication_AssumeRoleWithWebIdentity(t *testing.T) { + testCases := map[string]struct { + config map[string]any + SetConfig bool + ExpandEnvVars bool + EnvironmentVariables map[string]string + SetTokenFileEnvironmentVariable bool + SharedConfigurationFile string + SetSharedConfigurationFile bool + ExpectedCredentialsValue aws.Credentials + ValidateDiags DiagsValidator + MockStsEndpoints []*servicemocks.MockEndpoint + }{ + "config with inline token": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "config with token file": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + }, + }, + SetConfig: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "config with expanded path": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + }, + }, + SetConfig: true, + ExpandEnvVars: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "envvar": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + }, + SetTokenFileEnvironmentVariable: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "shared configuration file": { + config: map[string]any{}, + SharedConfigurationFile: fmt.Sprintf(` +[default] +role_arn = %[1]s +role_session_name = %[2]s +`, servicemocks.MockStsAssumeRoleWithWebIdentityArn, servicemocks.MockStsAssumeRoleWithWebIdentitySessionName), + SetSharedConfigurationFile: true, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "config overrides envvar": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, + }, + }, + EnvironmentVariables: map[string]string{ + "AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, + "AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName, + "AWS_WEB_IDENTITY_TOKEN_FILE": "no-such-file", + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + // "config with file envvar": { + // config: map[string]any{ + // "assume_role_with_web_identity": map[string]any{ + // "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + // "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + // }, + // }, + // SetTokenFileEnvironmentVariable: true, + // ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + // MockStsEndpoints: []*servicemocks.MockEndpoint{ + // servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + // servicemocks.MockStsGetCallerIdentityValidEndpoint, + // }, + // }, + + "envvar overrides shared configuration": { + config: map[string]any{}, + EnvironmentVariables: map[string]string{ + "AWS_ROLE_ARN": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "AWS_ROLE_SESSION_NAME": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + }, + SetTokenFileEnvironmentVariable: true, + SharedConfigurationFile: fmt.Sprintf(` +[default] +role_arn = %[1]s +role_session_name = %[2]s +web_identity_token_file = no-such-file +`, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName), + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "config overrides shared configuration": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, + }, + }, + SharedConfigurationFile: fmt.Sprintf(` +[default] +role_arn = %[1]s +role_session_name = %[2]s +web_identity_token_file = no-such-file +`, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateArn, servicemocks.MockStsAssumeRoleWithWebIdentityAlternateSessionName), + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidEndpoint, + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with duration": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, + "duration": "1h", + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidWithOptions(map[string]string{"DurationSeconds": "3600"}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "with policy": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, + "policy": "{}", + }, + }, + ExpectedCredentialsValue: mockdata.MockStsAssumeRoleWithWebIdentityCredentials, + MockStsEndpoints: []*servicemocks.MockEndpoint{ + servicemocks.MockStsAssumeRoleWithWebIdentityValidWithOptions(map[string]string{"Policy": "{}"}), + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }, + }, + + "invalid empty config": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{}, + }, + ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ + attributeErrDiag( + "Missing Required Value", + `Exactly one of web_identity_token, web_identity_token_file must be set.`, + cty.GetAttrPath("assume_role_with_web_identity"), + ), + attributeErrDiag( + "Missing Required Value", + `The attribute "assume_role_with_web_identity.role_arn" is required by the backend.`+"\n\n"+ + "Refer to the backend documentation for additional information which attributes are required.", + cty.GetAttrPath("assume_role_with_web_identity").GetAttr("role_arn"), + ), + }), + }, + + "invalid no token": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + }, + }, + ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ + attributeErrDiag( + "Missing Required Value", + `Exactly one of web_identity_token, web_identity_token_file must be set.`, + cty.GetAttrPath("assume_role_with_web_identity"), + ), + }), + }, + + "invalid token config conflict": { + config: map[string]any{ + "assume_role_with_web_identity": map[string]any{ + "role_arn": servicemocks.MockStsAssumeRoleWithWebIdentityArn, + "session_name": servicemocks.MockStsAssumeRoleWithWebIdentitySessionName, + "web_identity_token": servicemocks.MockWebIdentityToken, + }, + }, + SetConfig: true, + ValidateDiags: ExpectDiagsEqual(tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + `Only one of web_identity_token, web_identity_token_file can be set.`, + cty.GetAttrPath("assume_role_with_web_identity"), + ), + }), + }, + } + + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + servicemocks.InitSessionTestEnv(t) + + ctx := context.TODO() + + // Populate required fields + tc.config["region"] = "us-east-1" + tc.config["bucket"] = "bucket" + tc.config["key"] = "key" + + if tc.ValidateDiags == nil { + tc.ValidateDiags = ExpectNoDiags + } + + for k, v := range tc.EnvironmentVariables { + t.Setenv(k, v) + } + + ts := servicemocks.MockAwsApiServer("STS", tc.MockStsEndpoints) + defer ts.Close() + + tc.config["endpoints"] = map[string]any{ + "sts": ts.URL, + } + + tempdir, err := os.MkdirTemp("", "temp") + if err != nil { + t.Fatalf("error creating temp dir: %s", err) + } + defer os.Remove(tempdir) + t.Setenv("TMPDIR", tempdir) + + tokenFile, err := os.CreateTemp("", "aws-sdk-go-base-web-identity-token-file") + if err != nil { + t.Fatalf("unexpected error creating temporary web identity token file: %s", err) + } + tokenFileName := tokenFile.Name() + + defer os.Remove(tokenFileName) + + err = os.WriteFile(tokenFileName, []byte(servicemocks.MockWebIdentityToken), 0600) + + if err != nil { + t.Fatalf("unexpected error writing web identity token file: %s", err) + } + + if tc.ExpandEnvVars { + tmpdir := os.Getenv("TMPDIR") + rel, err := filepath.Rel(tmpdir, tokenFileName) + if err != nil { + t.Fatalf("error making path relative: %s", err) + } + t.Logf("relative: %s", rel) + tokenFileName = filepath.Join("$TMPDIR", rel) + t.Logf("env tempfile: %s", tokenFileName) + } + + if tc.SetConfig { + ar := tc.config["assume_role_with_web_identity"].(map[string]any) + ar["web_identity_token_file"] = tokenFileName + } + + if tc.SetTokenFileEnvironmentVariable { + t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenFileName) + } + + if tc.SharedConfigurationFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared configuration file: %s", err) + } + + defer os.Remove(file.Name()) + + if tc.SetSharedConfigurationFile { + tc.SharedConfigurationFile += fmt.Sprintf("web_identity_token_file = %s\n", tokenFileName) + } + + err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared configuration file: %s", err) + } + + tc.config["shared_config_files"] = []any{file.Name()} + } + + tc.config["skip_credentials_validation"] = true + + b, diags := configureBackend(t, tc.config) + + tc.ValidateDiags(t, diags) + + if diags.HasErrors() { + return + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials: %s", err) + } + + if diff := cmp.Diff(credentials, tc.ExpectedCredentialsValue, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" { + t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff) + } + }) + } +} + +var _ configtesting.TestDriver = &testDriver{} + +type testDriver struct { + mode configtesting.TestMode +} + +func (t *testDriver) Init(mode configtesting.TestMode) { + t.mode = mode +} + +func (t testDriver) TestCase() configtesting.TestCaseDriver { + return &testCaseDriver{ + mode: t.mode, + } +} + +var _ configtesting.TestCaseDriver = &testCaseDriver{} + +type testCaseDriver struct { + mode configtesting.TestMode + config configurer +} + +func (d *testCaseDriver) Configuration(fs []configtesting.ConfigFunc) configtesting.Configurer { + config := d.configuration() + for _, f := range fs { + f(config) + } + return config +} + +func (d *testCaseDriver) configuration() *configurer { + if d.config == nil { + d.config = make(configurer, 0) + } + return &d.config +} + +func (d *testCaseDriver) Setup(t *testing.T) { + ts := servicemocks.MockAwsApiServer("STS", []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }) + t.Cleanup(func() { + ts.Close() + }) + d.config.AddEndpoint("sts", ts.URL) +} + +func (d testCaseDriver) Apply(ctx context.Context, t *testing.T) (context.Context, configtesting.Thing) { + t.Helper() + + // Populate required fields + d.config.SetRegion("us-east-1") + d.config.setBucket("bucket") + d.config.setKey("key") + if d.mode == configtesting.TestModeLocal { + d.config.SetSkipCredsValidation(true) + d.config.SetSkipRequestingAccountId(true) + } + + b, diags := configureBackend(t, map[string]any(d.config)) + + var expected tfdiags.Diagnostics + + if diff := cmp.Diff(diags, expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + return ctx, thing(b.awsConfig) +} + +var _ configtesting.Configurer = &configurer{} + +type configurer map[string]any + +func (c configurer) AddEndpoint(k, v string) { + if endpoints, ok := c["endpoints"]; ok { + m := endpoints.(map[string]any) + m[k] = v + } else { + c["endpoints"] = map[string]any{ + k: v, + } + } +} + +func (c configurer) AddSharedConfigFile(f string) { + x := c["shared_config_files"] + if x == nil { + c["shared_config_files"] = []any{f} + } else { + files := x.([]any) + files = append(files, f) + c["shared_config_files"] = files + } +} + +func (c configurer) setBucket(s string) { + c["bucket"] = s +} + +func (c configurer) setKey(s string) { + c["key"] = s +} + +func (c configurer) SetAccessKey(s string) { + c["access_key"] = s +} + +func (c configurer) SetSecretKey(s string) { + c["secret_key"] = s +} + +func (c configurer) SetProfile(s string) { + c["profile"] = s +} + +func (c configurer) SetRegion(s string) { + c["region"] = s +} + +func (c configurer) SetUseFIPSEndpoint(b bool) { + c["use_fips_endpoint"] = b +} + +func (c configurer) SetSkipCredsValidation(b bool) { + c["skip_credentials_validation"] = b +} + +func (c configurer) SetSkipRequestingAccountId(b bool) { + c["skip_requesting_account_id"] = b +} + +var _ configtesting.Thing = thing{} + +type thing aws.Config + +func (t thing) GetCredentials() aws.CredentialsProvider { + return t.Credentials +} + +func (t thing) GetRegion() string { + return t.Region +} + +func TestBackendConfig_Authentication_SSO(t *testing.T) { + configtesting.SSO(t, &testDriver{}) +} + +func TestBackendConfig_Authentication_LegacySSO(t *testing.T) { + configtesting.LegacySSO(t, &testDriver{}) +} + +func TestBackendConfig_Region(t *testing.T) { + testCases := map[string]struct { + config map[string]any + EnvironmentVariables map[string]string + IMDSRegion string + SharedConfigurationFile string + ExpectedRegion string + }{ + // NOT SUPPORTED: region is required + // "no configuration": { + // config: map[string]any{ + // "access_key": servicemocks.MockStaticAccessKey, + // "secret_key": servicemocks.MockStaticSecretKey, + // }, + // ExpectedRegion: "", + // }, + + "config": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "region": "us-east-1", + }, + ExpectedRegion: "us-east-1", + }, + + "AWS_REGION": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_REGION": "us-east-1", + }, + ExpectedRegion: "us-east-1", + }, + "AWS_DEFAULT_REGION": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_DEFAULT_REGION": "us-east-1", + }, + ExpectedRegion: "us-east-1", + }, + "AWS_REGION overrides AWS_DEFAULT_REGION": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_REGION": "us-east-1", + "AWS_DEFAULT_REGION": "us-west-2", + }, + ExpectedRegion: "us-east-1", + }, + + // NOT SUPPORTED: region from shared configuration file + // "shared configuration file": { + // config: map[string]any{ + // "access_key": servicemocks.MockStaticAccessKey, + // "secret_key": servicemocks.MockStaticSecretKey, + // }, + // SharedConfigurationFile: ` + // [default] + // region = us-east-1 + // `, + // ExpectedRegion: "us-east-1", + // }, + + // NOT SUPPORTED: region from IMDS + // "IMDS": { + // config: map[string]any{}, + // IMDSRegion: "us-east-1", + // ExpectedRegion: "us-east-1", + // }, + + "config overrides AWS_REGION": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "region": "us-east-1", + }, + EnvironmentVariables: map[string]string{ + "AWS_REGION": "us-west-2", + }, + ExpectedRegion: "us-east-1", + }, + "config overrides AWS_DEFAULT_REGION": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "region": "us-east-1", + }, + EnvironmentVariables: map[string]string{ + "AWS_DEFAULT_REGION": "us-west-2", + }, + ExpectedRegion: "us-east-1", + }, + + "config overrides IMDS": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + "region": "us-west-2", + }, + IMDSRegion: "us-east-1", + ExpectedRegion: "us-west-2", + }, + + "AWS_REGION overrides shared configuration": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_REGION": "us-east-1", + }, + SharedConfigurationFile: ` +[default] +region = us-west-2 +`, + ExpectedRegion: "us-east-1", + }, + "AWS_DEFAULT_REGION overrides shared configuration": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_DEFAULT_REGION": "us-east-1", + }, + SharedConfigurationFile: ` +[default] +region = us-west-2 +`, + ExpectedRegion: "us-east-1", + }, + + "AWS_REGION overrides IMDS": { + config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + EnvironmentVariables: map[string]string{ + "AWS_REGION": "us-east-1", + }, + IMDSRegion: "us-west-2", + ExpectedRegion: "us-east-1", + }, + } + + for name, tc := range testCases { + tc := tc + + t.Run(name, func(t *testing.T) { + servicemocks.InitSessionTestEnv(t) + + // Populate required fields + tc.config["bucket"] = "bucket" + tc.config["key"] = "key" + + for k, v := range tc.EnvironmentVariables { + t.Setenv(k, v) + } + + if tc.IMDSRegion != "" { + closeEc2Metadata := servicemocks.AwsMetadataApiMock(append( + servicemocks.Ec2metadata_securityCredentialsEndpoints, + servicemocks.Ec2metadata_instanceIdEndpoint, + servicemocks.Ec2metadata_iamInfoEndpoint, + servicemocks.Ec2metadata_instanceIdentityEndpoint(tc.IMDSRegion), + )) + defer closeEc2Metadata() + } + + ts := servicemocks.MockAwsApiServer("STS", []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }) + defer ts.Close() + + tc.config["endpoints"] = map[string]any{ + "sts": ts.URL, + } + + if tc.SharedConfigurationFile != "" { + file, err := os.CreateTemp("", "aws-sdk-go-base-shared-configuration-file") + + if err != nil { + t.Fatalf("unexpected error creating temporary shared configuration file: %s", err) + } + + defer os.Remove(file.Name()) + + err = os.WriteFile(file.Name(), []byte(tc.SharedConfigurationFile), 0600) + + if err != nil { + t.Fatalf("unexpected error writing shared configuration file: %s", err) + } + + setSharedConfigFile(t, file.Name()) + } + + tc.config["skip_credentials_validation"] = true + + b, diags := configureBackend(t, tc.config) + if diags.HasErrors() { + t.Fatalf("configuring backend: %s", diagnosticsString(diags)) + } + + if a, e := b.awsConfig.Region, tc.ExpectedRegion; a != e { + t.Errorf("expected Region %q, got: %q", e, a) + } + }) + } +} + +func setSharedConfigFile(t *testing.T, filename string) { + t.Setenv("AWS_SDK_LOAD_CONFIG", "1") + t.Setenv("AWS_CONFIG_FILE", filename) +} + +func configureBackend(t *testing.T, config map[string]any) (*Backend, tfdiags.Diagnostics) { + b := New().(*Backend) + configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)) + + configSchema, diags := b.PrepareConfig(configSchema) + + if diags.HasErrors() { + return b, diags + } + + confDiags := b.Configure(configSchema) + diags = diags.Append(confDiags) + + return b, diags +} + +func sharedConfigCredentialsSource(filename string) string { + return fmt.Sprintf(sharedConfigCredentialsProvider+": %s", filename) +} + +func TestStsEndpoint(t *testing.T) { + type settype int + const ( + setNone settype = iota + setValid + setInvalid + ) + testcases := map[string]struct { + Config map[string]any + SetServiceEndpoint settype + SetServiceEndpointLegacy settype + SetEnv string + SetInvalidEnv string + // Use string at index 1 for valid endpoint url and index 2 for invalid endpoint url + ConfigFile string + ExpectedCredentials aws.Credentials + }{ + // Service Config + + "service config": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpoint: setValid, + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config overrides service envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpoint: setValid, + SetInvalidEnv: "AWS_ENDPOINT_URL_STS", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config overrides service envvar legacy": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpoint: setValid, + SetInvalidEnv: "AWS_STS_ENDPOINT", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config overrides base envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpoint: setValid, + SetInvalidEnv: "AWS_ENDPOINT_URL", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config overrides service config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test + +[services sts-test] +sts = + endpoint_url = %[2]s +`, + SetServiceEndpoint: setValid, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "service config overrides base config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +endpoint_url = %[2]s +`, + SetServiceEndpoint: setValid, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + // Service Config Legacy + + "service config legacy": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpointLegacy: setValid, + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config legacy overrides service envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpointLegacy: setValid, + SetInvalidEnv: "AWS_ENDPOINT_URL_STS", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config legacy overrides service envvar legacy": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpointLegacy: setValid, + SetInvalidEnv: "AWS_STS_ENDPOINT", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config legacy overrides base envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetServiceEndpointLegacy: setValid, + SetInvalidEnv: "AWS_ENDPOINT_URL", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service config legacy overrides service config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test + +[services sts-test] +sts = + endpoint_url = %[2]s +`, + SetServiceEndpointLegacy: setValid, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "service config legacy overrides base config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +endpoint_url = %[2]s +`, + SetServiceEndpointLegacy: setValid, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + // Service Envvar + + "service envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetEnv: "AWS_ENDPOINT_URL_STS", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service envvar overrides base envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetEnv: "AWS_ENDPOINT_URL_STS", + SetInvalidEnv: "AWS_ENDPOINT_URL", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service envvar overrides service config_file": { + Config: map[string]any{ + "profile": "default", + }, + SetEnv: "AWS_ENDPOINT_URL_STS", + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test + +[services sts-test] +sts = + endpoint_url = %[2]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "service envvar overrides base config_file": { + Config: map[string]any{ + "profile": "default", + }, + SetEnv: "AWS_ENDPOINT_URL_STS", + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +endpoint_url = %[2]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "service envvar overrides service envvar legacy": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetEnv: "AWS_ENDPOINT_URL_STS", + SetInvalidEnv: "AWS_STS_ENDPOINT", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + // Service Envvar Legacy + + "service envvar legacy": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetEnv: "AWS_STS_ENDPOINT", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service envvar legacy overrides base envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetEnv: "AWS_STS_ENDPOINT", + SetInvalidEnv: "AWS_ENDPOINT_URL", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "service envvar legacy overrides service config_file": { + Config: map[string]any{ + "profile": "default", + }, + SetEnv: "AWS_STS_ENDPOINT", + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test + +[services sts-test] +sts = + endpoint_url = %[2]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "service envvar legacy overrides base config_file": { + Config: map[string]any{ + "profile": "default", + }, + SetEnv: "AWS_STS_ENDPOINT", + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +endpoint_url = %[2]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + // Service Config File + + "service config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test + +[services sts-test] +sts = + endpoint_url = %[1]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "service config_file overrides base config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test +endpoint_url = %[2]s + +[services sts-test] +sts = + endpoint_url = %[1]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + // Base envvar + + "base envvar": { + Config: map[string]any{ + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + }, + SetEnv: "AWS_ENDPOINT_URL", + ExpectedCredentials: mockdata.MockStaticCredentials, + }, + + "base envvar overrides service config_file": { + Config: map[string]any{ + "profile": "default", + }, + SetEnv: "AWS_ENDPOINT_URL", + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +services = sts-test + +[services sts-test] +sts = + endpoint_url = %[2]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "base config_file": { + Config: map[string]any{ + "profile": "default", + }, + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +endpoint_url = %[1]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + + "base envvar overrides base config_file": { + Config: map[string]any{ + "profile": "default", + }, + SetEnv: "AWS_ENDPOINT_URL", + ConfigFile: ` +[default] +aws_access_key_id = DefaultSharedCredentialsAccessKey +aws_secret_access_key = DefaultSharedCredentialsSecretKey +endpoint_url = %[2]s +`, + ExpectedCredentials: aws.Credentials{ + AccessKeyID: "DefaultSharedCredentialsAccessKey", + SecretAccessKey: "DefaultSharedCredentialsSecretKey", + Source: sharedConfigCredentialsProvider, + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + + t.Run(name, func(t *testing.T) { + servicemocks.InitSessionTestEnv(t) + + // Populate required fields + testcase.Config["bucket"] = "bucket" + testcase.Config["key"] = "key" + testcase.Config["region"] = "us-west-2" + + ts := servicemocks.MockAwsApiServer("STS", []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityValidEndpoint, + }) + defer ts.Close() + stsEndpoint := ts.URL + + invalidTS := servicemocks.MockAwsApiServer("STS", []*servicemocks.MockEndpoint{ + servicemocks.MockStsGetCallerIdentityInvalidEndpointAccessDenied, + }) + defer invalidTS.Close() + stsInvalidEndpoint := invalidTS.URL + + if testcase.SetServiceEndpoint == setValid { + testcase.Config["endpoints"] = map[string]any{ + "sts": stsEndpoint, + } + } + if testcase.SetServiceEndpointLegacy == setValid { + testcase.Config["sts_endpoint"] = stsEndpoint + } + if testcase.SetEnv != "" { + t.Setenv(testcase.SetEnv, stsEndpoint) + } + if testcase.SetInvalidEnv != "" { + t.Setenv(testcase.SetInvalidEnv, stsInvalidEndpoint) + } + if testcase.ConfigFile != "" { + tempDir := t.TempDir() + filename := writeSharedConfigFile(t, testcase.Config, tempDir, fmt.Sprintf(testcase.ConfigFile, stsEndpoint, stsInvalidEndpoint)) + testcase.ExpectedCredentials.Source = sharedConfigCredentialsSource(filename) + } + + b, diags := configureBackend(t, testcase.Config) + if diags.HasErrors() { + t.Fatalf("configuring backend: %s", diagnosticsString(diags)) + } + + ctx := context.TODO() + + credentialsValue, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("unexpected credentials Retrieve() error: %s", err) + } + + if diff := cmp.Diff(credentialsValue, testcase.ExpectedCredentials, cmpopts.IgnoreFields(aws.Credentials{}, "Expires")); diff != "" { + t.Fatalf("unexpected credentials: (- got, + expected)\n%s", diff) + } + }) + } +} + +func writeSharedConfigFile(t *testing.T, config map[string]any, tempDir, content string) string { + t.Helper() + + file, err := os.Create(filepath.Join(tempDir, "aws-sdk-go-base-shared-configuration-file")) + if err != nil { + t.Fatalf("creating shared configuration file: %s", err) + } + + _, err = file.WriteString(content) + if err != nil { + t.Fatalf(" writing shared configuration file: %s", err) + } + + config["shared_config_files"] = []any{file.Name()} + + return file.Name() +} diff --git a/internal/backend/remote-state/s3/backend_state.go b/internal/backend/remote-state/s3/backend_state.go index 0134c861d0..07726d2458 100644 --- a/internal/backend/remote-state/s3/backend_state.go +++ b/internal/backend/remote-state/s3/backend_state.go @@ -1,50 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package s3 import ( + "context" "errors" "fmt" "path" "sort" "strings" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/smithy-go" + baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" ) +const ( + // defaultWorkspaceKeyPrefix is the default prefix for workspace storage. + // The colon is used to reduce the chance of name conflicts with existing objects. + defaultWorkspaceKeyPrefix = "env:" + // lockFileSuffix defines the suffix for Terraform state lock files. + lockFileSuffix = ".tflock" +) + func (b *Backend) Workspaces() ([]string, error) { const maxKeys = 1000 + ctx := context.TODO() + log := logger() + log = logWithOperation(log, operationBackendWorkspaces) + log = log.With( + logKeyBucket, b.bucketName, + ) + prefix := "" if b.workspaceKeyPrefix != "" { prefix = b.workspaceKeyPrefix + "/" } - params := &s3.ListObjectsInput{ - Bucket: &b.bucketName, + log = log.With( + logKeyBackendWorkspacePrefix, prefix, + ) + + params := &s3.ListObjectsV2Input{ + Bucket: aws.String(b.bucketName), Prefix: aws.String(prefix), - MaxKeys: aws.Int64(maxKeys), + MaxKeys: aws.Int32(maxKeys), } wss := []string{backend.DefaultStateName} - err := b.s3Client.ListObjectsPages(params, func(page *s3.ListObjectsOutput, lastPage bool) bool { + + ctx, baselog := baselogging.NewHcLogger(ctx, log) + ctx = baselogging.RegisterLogger(ctx, baselog) + + pages := s3.NewListObjectsV2Paginator(b.s3Client, params) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + if IsA[*s3types.NoSuchBucket](err) { + return nil, fmt.Errorf(errS3NoSuchBucket, b.bucketName, err) + } + if foo, ok := As[smithy.APIError](err); b.workspaceKeyPrefix == defaultWorkspaceKeyPrefix && ok && foo.ErrorCode() == "AccessDenied" { + log.Warn("Unable to list non-default workspaces", "err", err.Error()) + return wss[:1], nil + } + return nil, fmt.Errorf("Unable to list objects in S3 bucket %q with prefix %q: %w", b.bucketName, prefix, err) + } + for _, obj := range page.Contents { - ws := b.keyEnv(*obj.Key) + ws := b.keyEnv(aws.ToString(obj.Key)) if ws != "" { wss = append(wss, ws) } } - return !lastPage - }) - - if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == s3.ErrCodeNoSuchBucket { - return nil, fmt.Errorf(errS3NoSuchBucket, err) } sort.Strings(wss[1:]) @@ -90,11 +127,19 @@ func (b *Backend) keyEnv(key string) string { return parts[0] } -func (b *Backend) DeleteWorkspace(name string) error { +func (b *Backend) DeleteWorkspace(name string, _ bool) error { + log := logger() + log = logWithOperation(log, operationBackendDeleteWorkspace) + log = log.With( + logKeyBackendWorkspace, name, + ) + if name == backend.DefaultStateName || name == "" { return fmt.Errorf("can't delete default state") } + log.Info("Deleting workspace") + client, err := b.remoteClient(name) if err != nil { return err @@ -119,6 +164,9 @@ func (b *Backend) remoteClient(name string) (*RemoteClient, error) { acl: b.acl, kmsKeyID: b.kmsKeyID, ddbTable: b.ddbTable, + skipS3Checksum: b.skipS3Checksum, + lockFilePath: b.getLockFilePath(name), + useLockFile: b.useLockFile, } return client, nil @@ -184,7 +232,7 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { err = lockUnlock(err) return nil, err } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(nil); err != nil { err = lockUnlock(err) return nil, err } @@ -200,10 +248,6 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, error) { return stateMgr, nil } -func (b *Backend) client() *RemoteClient { - return &RemoteClient{} -} - func (b *Backend) path(name string) string { if name == backend.DefaultStateName { return b.keyName @@ -219,3 +263,26 @@ Error: %s You may have to force-unlock this state in order to use it again. ` + +var _ error = bucketRegionError{} + +type bucketRegionError struct { + requestRegion, bucketRegion string +} + +func newBucketRegionError(requestRegion, bucketRegion string) bucketRegionError { + return bucketRegionError{ + requestRegion: requestRegion, + bucketRegion: bucketRegion, + } +} + +func (err bucketRegionError) Error() string { + return fmt.Sprintf("requested bucket from %q, actual location %q", err.requestRegion, err.bucketRegion) +} + +// getLockFilePath returns the path to the lock file for the given Terraform state. +// For `default.tfstate`, the lock file is stored at `default.tfstate.tflock`. +func (b *Backend) getLockFilePath(name string) string { + return b.path(name) + lockFileSuffix +} diff --git a/internal/backend/remote-state/s3/backend_test.go b/internal/backend/remote-state/s3/backend_test.go index a44f154c08..10e00c0e3e 100644 --- a/internal/backend/remote-state/s3/backend_test.go +++ b/internal/backend/remote-state/s3/backend_test.go @@ -1,21 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package s3 import ( + "context" + "encoding/base64" + "errors" "fmt" + "maps" + "net/http" "net/url" "os" "reflect" + "strings" "testing" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/s3" - awsbase "github.com/hashicorp/aws-sdk-go-base" + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/feature/ec2/imds" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/aws-sdk-go-base/v2/mockdata" + "github.com/hashicorp/aws-sdk-go-base/v2/servicemocks" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" ) var ( @@ -41,10 +65,24 @@ func TestBackend_impl(t *testing.T) { var _ backend.Backend = new(Backend) } -func TestBackendConfig(t *testing.T) { +func TestBackend_InternalValidate(t *testing.T) { + b := New() + + schema := b.ConfigSchema() + if err := schema.InternalValidate(); err != nil { + t.Fatalf("failed InternalValidate: %s", err) + } +} + +func TestBackendConfig_original(t *testing.T) { testACC(t) + + ctx := context.TODO() + + region := "us-west-1" + config := map[string]interface{}{ - "region": "us-west-1", + "region": region, "bucket": "tf-test", "key": "state", "encrypt": true, @@ -53,9 +91,12 @@ func TestBackendConfig(t *testing.T) { b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) - if *b.s3Client.Config.Region != "us-west-1" { + if b.awsConfig.Region != region { t.Fatalf("Incorrect region was populated") } + if b.awsConfig.RetryMaxAttempts != 5 { + t.Fatalf("Default max_retries was not set") + } if b.bucketName != "tf-test" { t.Fatalf("Incorrect bucketName was populated") } @@ -63,7 +104,7 @@ func TestBackendConfig(t *testing.T) { t.Fatalf("Incorrect keyName was populated") } - credentials, err := b.s3Client.Config.Credentials.Get() + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) if err != nil { t.Fatalf("Error when requesting credentials") } @@ -73,235 +114,1078 @@ func TestBackendConfig(t *testing.T) { if credentials.SecretAccessKey == "" { t.Fatalf("No Secret Access Key was populated") } + + // Check S3 Endpoint + expectedS3Endpoint := defaultEndpointS3(region) + var s3Endpoint string + _, err = b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{}, + func(opts *s3.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &s3Endpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Checking S3 Endpoint: Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Checking S3 Endpoint: Unexpected error: %s", err) + } + + if s3Endpoint != expectedS3Endpoint { + t.Errorf("Checking S3 Endpoint: expected endpoint %q, got %q", expectedS3Endpoint, s3Endpoint) + } + + // Check DynamoDB Endpoint + expectedDynamoDBEndpoint := defaultEndpointDynamo(region) + var dynamoDBEndpoint string + _, err = b.dynClient.ListTables(ctx, &dynamodb.ListTablesInput{}, + func(opts *dynamodb.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &dynamoDBEndpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Checking DynamoDB Endpoint: Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Checking DynamoDB Endpoint: Unexpected error: %s", err) + } + + if dynamoDBEndpoint != expectedDynamoDBEndpoint { + t.Errorf("Checking DynamoDB Endpoint: expected endpoint %q, got %q", expectedDynamoDBEndpoint, dynamoDBEndpoint) + } +} + +func TestBackendConfig_withLockfile(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + region := "us-west-1" + + config := map[string]interface{}{ + "region": region, + "bucket": "tf-test", + "key": "state", + "encrypt": true, + "use_lockfile": true, + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) + + if b.awsConfig.Region != region { + t.Fatalf("Incorrect region was populated") + } + if b.awsConfig.RetryMaxAttempts != 5 { + t.Fatalf("Default max_retries was not set") + } + if b.bucketName != "tf-test" { + t.Fatalf("Incorrect bucketName was populated") + } + if b.keyName != "state" { + t.Fatalf("Incorrect keyName was populated") + } + + if b.useLockFile != true { + t.Fatalf("Expected useLockFile to be true") + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials") + } + if credentials.AccessKeyID == "" { + t.Fatalf("No Access Key Id was populated") + } + if credentials.SecretAccessKey == "" { + t.Fatalf("No Secret Access Key was populated") + } + + // Check S3 Endpoint + expectedS3Endpoint := defaultEndpointS3(region) + var s3Endpoint string + _, err = b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{}, + func(opts *s3.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &s3Endpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Checking S3 Endpoint: Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Checking S3 Endpoint: Unexpected error: %s", err) + } + + if s3Endpoint != expectedS3Endpoint { + t.Errorf("Checking S3 Endpoint: expected endpoint %q, got %q", expectedS3Endpoint, s3Endpoint) + } +} + +func TestBackendConfig_multiLock(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + region := "us-west-1" + + config := map[string]interface{}{ + "region": region, + "bucket": "tf-test", + "key": "state", + "encrypt": true, + "use_lockfile": true, + "dynamodb_table": "dynamoTable", + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) + + if b.awsConfig.Region != region { + t.Fatalf("Incorrect region was populated") + } + if b.awsConfig.RetryMaxAttempts != 5 { + t.Fatalf("Default max_retries was not set") + } + if b.bucketName != "tf-test" { + t.Fatalf("Incorrect bucketName was populated") + } + if b.keyName != "state" { + t.Fatalf("Incorrect keyName was populated") + } + + if b.useLockFile != true { + t.Fatalf("Expected useLockFile to be true") + } + + credentials, err := b.awsConfig.Credentials.Retrieve(ctx) + if err != nil { + t.Fatalf("Error when requesting credentials") + } + if credentials.AccessKeyID == "" { + t.Fatalf("No Access Key Id was populated") + } + if credentials.SecretAccessKey == "" { + t.Fatalf("No Secret Access Key was populated") + } + + // Check S3 Endpoint + expectedS3Endpoint := defaultEndpointS3(region) + var s3Endpoint string + _, err = b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{}, + func(opts *s3.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &s3Endpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Checking S3 Endpoint: Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Checking S3 Endpoint: Unexpected error: %s", err) + } + + if s3Endpoint != expectedS3Endpoint { + t.Errorf("Checking S3 Endpoint: expected endpoint %q, got %q", expectedS3Endpoint, s3Endpoint) + } + + // Check DynamoDB Endpoint + expectedDynamoDBEndpoint := defaultEndpointDynamo(region) + var dynamoDBEndpoint string + _, err = b.dynClient.ListTables(ctx, &dynamodb.ListTablesInput{}, + func(opts *dynamodb.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &dynamoDBEndpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Checking DynamoDB Endpoint: Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Checking DynamoDB Endpoint: Unexpected error: %s", err) + } + + if dynamoDBEndpoint != expectedDynamoDBEndpoint { + t.Errorf("Checking DynamoDB Endpoint: expected endpoint %q, got %q", expectedDynamoDBEndpoint, dynamoDBEndpoint) + } +} + +func TestBackendConfig_InvalidRegion(t *testing.T) { + testACC(t) + + cases := map[string]struct { + config map[string]any + expectedDiags tfdiags.Diagnostics + }{ + "with region validation": { + config: map[string]interface{}{ + "region": "nonesuch", + "bucket": "tf-test", + "key": "state", + "skip_credentials_validation": true, + }, + expectedDiags: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid region value", + `Invalid AWS Region: nonesuch`, + cty.GetAttrPath("region"), + ), + }, + }, + "skip region validation": { + config: map[string]interface{}{ + "region": "nonesuch", + "bucket": "tf-test", + "key": "state", + "skip_region_validation": true, + "skip_credentials_validation": true, + }, + expectedDiags: nil, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + b := New() + configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(tc.config)) + + configSchema, diags := b.PrepareConfig(configSchema) + if len(diags) > 0 { + t.Fatal(diags.ErrWithWarnings()) + } + + confDiags := b.Configure(configSchema) + diags = diags.Append(confDiags) + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestBackendConfig_RegionEnvVar(t *testing.T) { + testACC(t) + config := map[string]interface{}{ + "bucket": "tf-test", + "key": "state", + } + + cases := map[string]struct { + vars map[string]string + }{ + "AWS_REGION": { + vars: map[string]string{ + "AWS_REGION": "us-west-1", + }, + }, + + "AWS_DEFAULT_REGION": { + vars: map[string]string{ + "AWS_DEFAULT_REGION": "us-west-1", + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + for k, v := range tc.vars { + os.Setenv(k, v) + } + t.Cleanup(func() { + for k := range tc.vars { + os.Unsetenv(k) + } + }) + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) + + if b.awsConfig.Region != "us-west-1" { + t.Fatalf("Incorrect region was populated") + } + }) + } +} + +func TestBackendConfig_DynamoDBEndpoint(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + region := "us-west-1" + + cases := map[string]struct { + config map[string]any + vars map[string]string + expectedEndpoint string + expectedDiags tfdiags.Diagnostics + }{ + "none": { + expectedEndpoint: defaultEndpointDynamo(region), + }, + "config URL": { + config: map[string]any{ + "endpoints": map[string]any{ + "dynamodb": "https://dynamo.test", + }, + }, + expectedEndpoint: "https://dynamo.test/", + }, + "config hostname": { + config: map[string]any{ + "endpoints": map[string]any{ + "dynamodb": "dynamo.test", + }, + }, + expectedEndpoint: "dynamo.test/", + expectedDiags: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("dynamo.test", cty.GetAttrPath("endpoints").GetAttr("dynamodb")), + }, + }, + "deprecated config URL": { + config: map[string]any{ + "dynamodb_endpoint": "https://dynamo.test", + }, + expectedEndpoint: "https://dynamo.test/", + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("dynamodb_endpoint"), cty.GetAttrPath("endpoints").GetAttr("dynamodb")), + }, + }, + "deprecated config hostname": { + config: map[string]any{ + "dynamodb_endpoint": "dynamo.test", + }, + expectedEndpoint: "dynamo.test/", + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("dynamodb_endpoint"), cty.GetAttrPath("endpoints").GetAttr("dynamodb")), + legacyIncompleteURLDiag("dynamo.test", cty.GetAttrPath("dynamodb_endpoint")), + }, + }, + "config conflict": { + config: map[string]any{ + "dynamodb_endpoint": "https://dynamo.test", + "endpoints": map[string]any{ + "dynamodb": "https://dynamo.test", + }, + }, + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("dynamodb_endpoint"), cty.GetAttrPath("endpoints").GetAttr("dynamodb")), + wholeBodyErrDiag( + "Conflicting Parameters", + fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`, + pathString(cty.GetAttrPath("dynamodb_endpoint")), + pathString(cty.GetAttrPath("endpoints").GetAttr("dynamodb")), + ), + )}, + }, + "envvar": { + vars: map[string]string{ + "AWS_ENDPOINT_URL_DYNAMODB": "https://dynamo.test", + }, + expectedEndpoint: "https://dynamo.test/", + }, + "deprecated envvar": { + vars: map[string]string{ + "AWS_DYNAMODB_ENDPOINT": "https://dynamo.test", + }, + expectedEndpoint: "https://dynamo.test/", + expectedDiags: tfdiags.Diagnostics{ + deprecatedEnvVarDiag("AWS_DYNAMODB_ENDPOINT", "AWS_ENDPOINT_URL_DYNAMODB"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + config := map[string]interface{}{ + "region": region, + "bucket": "tf-test", + "key": "state", + } + + if tc.vars != nil { + for k, v := range tc.vars { + os.Setenv(k, v) + } + t.Cleanup(func() { + for k := range tc.vars { + os.Unsetenv(k) + } + }) + } + + if tc.config != nil { + for k, v := range tc.config { + config[k] = v + } + } + + raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config)) + b := raw.(*Backend) + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if !diags.HasErrors() { + var dynamoDBEndpoint string + _, err := b.dynClient.ListTables(ctx, &dynamodb.ListTablesInput{}, + func(opts *dynamodb.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &dynamoDBEndpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Unexpected error: %s", err) + } + + if dynamoDBEndpoint != tc.expectedEndpoint { + t.Errorf("expected endpoint %q, got %q", tc.expectedEndpoint, dynamoDBEndpoint) + } + } + }) + } +} + +func TestBackendConfig_IAMEndpoint(t *testing.T) { + testACC(t) + + // Doesn't test for expected endpoint, since the IAM endpoint is used internally to `aws-sdk-go-base` + // The mocked tests won't work if the config parameter doesn't work + cases := map[string]struct { + config map[string]any + vars map[string]string + expectedDiags tfdiags.Diagnostics + }{ + "none": {}, + "config URL": { + config: map[string]any{ + "endpoints": map[string]any{ + "iam": "https://iam.test", + }, + }, + }, + "config hostname": { + config: map[string]any{ + "endpoints": map[string]any{ + "iam": "iam.test", + }, + }, + expectedDiags: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("iam.test", cty.GetAttrPath("endpoints").GetAttr("iam")), + }, + }, + "deprecated config URL": { + config: map[string]any{ + "iam_endpoint": "https://iam.test", + }, + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("iam_endpoint"), cty.GetAttrPath("endpoints").GetAttr("iam")), + }, + }, + "deprecated config hostname": { + config: map[string]any{ + "iam_endpoint": "iam.test", + }, + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("iam_endpoint"), cty.GetAttrPath("endpoints").GetAttr("iam")), + legacyIncompleteURLDiag("iam.test", cty.GetAttrPath("iam_endpoint")), + }, + }, + "config conflict": { + config: map[string]any{ + "iam_endpoint": "https://iam.test", + "endpoints": map[string]any{ + "iam": "https://iam.test", + }, + }, + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("iam_endpoint"), cty.GetAttrPath("endpoints").GetAttr("iam")), + wholeBodyErrDiag( + "Conflicting Parameters", + fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`, + pathString(cty.GetAttrPath("iam_endpoint")), + pathString(cty.GetAttrPath("endpoints").GetAttr("iam")), + ), + )}, + }, + "envvar": { + vars: map[string]string{ + "AWS_ENDPOINT_URL_IAM": "https://iam.test", + }, + }, + "deprecated envvar": { + vars: map[string]string{ + "AWS_IAM_ENDPOINT": "https://iam.test", + }, + expectedDiags: tfdiags.Diagnostics{ + deprecatedEnvVarDiag("AWS_IAM_ENDPOINT", "AWS_ENDPOINT_URL_IAM"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + config := map[string]interface{}{ + "region": "us-west-1", + "bucket": "tf-test", + "key": "state", + } + + if tc.vars != nil { + for k, v := range tc.vars { + os.Setenv(k, v) + } + t.Cleanup(func() { + for k := range tc.vars { + os.Unsetenv(k) + } + }) + } + + if tc.config != nil { + for k, v := range tc.config { + config[k] = v + } + } + + _, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config)) + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestBackendConfig_S3Endpoint(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + region := "us-west-1" + + cases := map[string]struct { + config map[string]any + vars map[string]string + expectedEndpoint string + expectedDiags tfdiags.Diagnostics + }{ + "none": { + expectedEndpoint: defaultEndpointS3(region), + }, + "config URL": { + config: map[string]any{ + "endpoints": map[string]any{ + "s3": "https://s3.test", + }, + }, + expectedEndpoint: "https://s3.test/", + }, + "config hostname": { + config: map[string]any{ + "endpoints": map[string]any{ + "s3": "s3.test", + }, + }, + expectedEndpoint: "/s3.test", + expectedDiags: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("s3.test", cty.GetAttrPath("endpoints").GetAttr("s3")), + }, + }, + "deprecated config URL": { + config: map[string]any{ + "endpoint": "https://s3.test", + }, + expectedEndpoint: "https://s3.test/", + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("endpoint"), cty.GetAttrPath("endpoints").GetAttr("s3")), + }, + }, + "deprecated config hostname": { + config: map[string]any{ + "endpoint": "s3.test", + }, + expectedEndpoint: "/s3.test", + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("endpoint"), cty.GetAttrPath("endpoints").GetAttr("s3")), + legacyIncompleteURLDiag("s3.test", cty.GetAttrPath("endpoint")), + }, + }, + "config conflict": { + config: map[string]any{ + "endpoint": "https://s3.test", + "endpoints": map[string]any{ + "s3": "https://s3.test", + }, + }, + expectedDiags: tfdiags.Diagnostics{ + deprecatedAttrDiag(cty.GetAttrPath("endpoint"), cty.GetAttrPath("endpoints").GetAttr("s3")), + wholeBodyErrDiag( + "Conflicting Parameters", + fmt.Sprintf(`The parameters "%s" and "%s" cannot be configured together.`, + pathString(cty.GetAttrPath("endpoint")), + pathString(cty.GetAttrPath("endpoints").GetAttr("s3")), + ), + )}, + }, + "envvar": { + vars: map[string]string{ + "AWS_ENDPOINT_URL_S3": "https://s3.test", + }, + expectedEndpoint: "https://s3.test/", + }, + "deprecated envvar": { + vars: map[string]string{ + "AWS_S3_ENDPOINT": "https://s3.test", + }, + expectedEndpoint: "https://s3.test/", + expectedDiags: tfdiags.Diagnostics{ + deprecatedEnvVarDiag("AWS_S3_ENDPOINT", "AWS_ENDPOINT_URL_S3"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + config := map[string]interface{}{ + "region": region, + "bucket": "tf-test", + "key": "state", + } + + if tc.vars != nil { + for k, v := range tc.vars { + os.Setenv(k, v) + } + t.Cleanup(func() { + for k := range tc.vars { + os.Unsetenv(k) + } + }) + } + + if tc.config != nil { + for k, v := range tc.config { + config[k] = v + } + } + + raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config)) + b := raw.(*Backend) + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if !diags.HasErrors() { + var s3Endpoint string + _, err := b.s3Client.ListBuckets(ctx, &s3.ListBucketsInput{}, + func(opts *s3.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &s3Endpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Unexpected error: %s", err) + } + + if s3Endpoint != tc.expectedEndpoint { + t.Errorf("expected endpoint %q, got %q", tc.expectedEndpoint, s3Endpoint) + } + } + }) + } +} + +func TestBackendConfig_EC2MetadataEndpoint(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + cases := map[string]struct { + config map[string]any + vars map[string]string + expectedEndpoint string + expectedDiags tfdiags.Diagnostics + }{ + "none": { + expectedEndpoint: "http://169.254.169.254/latest/meta-data", + }, + "config URL": { + config: map[string]any{ + "ec2_metadata_service_endpoint": "https://ec2.test", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + }, + "config hostname": { + config: map[string]any{ + "ec2_metadata_service_endpoint": "ec2.test", + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must be a valid URL containing at least a scheme and hostname. Had "ec2.test"`, + cty.GetAttrPath("ec2_metadata_service_endpoint"), + ), + }, + }, + "config IPv4 mode": { + config: map[string]any{ + "ec2_metadata_service_endpoint": "https://ec2.test", + "ec2_metadata_service_endpoint_mode": "IPv4", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + }, + "config IPv6 mode": { + config: map[string]any{ + "ec2_metadata_service_endpoint": "https://ec2.test", + "ec2_metadata_service_endpoint_mode": "IPv6", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + }, + "config invalid mode": { + config: map[string]any{ + "ec2_metadata_service_endpoint": "https://ec2.test", + "ec2_metadata_service_endpoint_mode": "invalid", + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + "Value must be one of [IPv4, IPv6]", + cty.GetAttrPath("ec2_metadata_service_endpoint_mode"), + ), + }, + }, + "envvar": { + vars: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "https://ec2.test", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + }, + "envvar IPv4 mode": { + vars: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "https://ec2.test", + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv4", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + }, + "envvar IPv6 mode": { + vars: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "https://ec2.test", + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "IPv6", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + }, + "envvar invalid mode": { + vars: map[string]string{ + "AWS_EC2_METADATA_SERVICE_ENDPOINT": "https://ec2.test", + "AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE": "invalid", + }, + // expectedEndpoint: "ec2.test", + expectedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "unknown EC2 IMDS endpoint mode, must be either IPv6 or IPv4", + "", + ), + }, + }, + "deprecated envvar": { + vars: map[string]string{ + "AWS_METADATA_URL": "https://ec2.test", + }, + expectedEndpoint: "https://ec2.test/latest/meta-data", + expectedDiags: tfdiags.Diagnostics{ + deprecatedEnvVarDiag("AWS_METADATA_URL", "AWS_EC2_METADATA_SERVICE_ENDPOINT"), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + config := map[string]interface{}{ + "region": "us-west-1", + "bucket": "tf-test", + "key": "state", + } + + if tc.vars != nil { + for k, v := range tc.vars { + os.Setenv(k, v) + } + t.Cleanup(func() { + for k := range tc.vars { + os.Unsetenv(k) + } + }) + } + + if tc.config != nil { + for k, v := range tc.config { + config[k] = v + } + } + + raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config)) + b := raw.(*Backend) + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if !diags.HasErrors() { + var imdsEndpoint string + imdsClient := imds.NewFromConfig(b.awsConfig) + _, err := imdsClient.GetMetadata(ctx, &imds.GetMetadataInput{}, + func(opts *imds.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveEndpointURLMiddleware(t, &imdsEndpoint), + addCancelRequestMiddleware(), + ) + }, + ) + if err == nil { + t.Fatal("Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Unexpected error: %s", err) + } + + if imdsEndpoint != tc.expectedEndpoint { + t.Errorf("expected endpoint %q, got %q", tc.expectedEndpoint, imdsEndpoint) + } + } + }) + } } func TestBackendConfig_AssumeRole(t *testing.T) { testACC(t) - testCases := []struct { + testCases := map[string]struct { Config map[string]interface{} - Description string - MockStsEndpoints []*awsbase.MockEndpoint + MockStsEndpoints []*servicemocks.MockEndpoint }{ - { + "role_arn": { Config: map[string]interface{}{ "bucket": "tf-test", "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "role_arn", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"900"}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, - { + "assume_role_duration_seconds": { Config: map[string]interface{}{ "assume_role_duration_seconds": 3600, "bucket": "tf-test", "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "assume_role_duration_seconds", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"3600"}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, - { + "external_id": { Config: map[string]interface{}{ "bucket": "tf-test", - "external_id": awsbase.MockStsAssumeRoleExternalId, + "external_id": servicemocks.MockStsAssumeRoleExternalId, "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "external_id", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"900"}, - "ExternalId": []string{awsbase.MockStsAssumeRoleExternalId}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, + "ExternalId": []string{servicemocks.MockStsAssumeRoleExternalId}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, - { + "assume_role_policy": { Config: map[string]interface{}{ - "assume_role_policy": awsbase.MockStsAssumeRolePolicy, + "assume_role_policy": servicemocks.MockStsAssumeRolePolicy, "bucket": "tf-test", "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "assume_role_policy", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"900"}, - "Policy": []string{awsbase.MockStsAssumeRolePolicy}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, + "Policy": []string{servicemocks.MockStsAssumeRolePolicy}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, - { + "assume_role_policy_arns": { Config: map[string]interface{}{ - "assume_role_policy_arns": []interface{}{awsbase.MockStsAssumeRolePolicyArn}, + "assume_role_policy_arns": []interface{}{servicemocks.MockStsAssumeRolePolicyArn}, "bucket": "tf-test", "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "assume_role_policy_arns", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"900"}, - "PolicyArns.member.1.arn": []string{awsbase.MockStsAssumeRolePolicyArn}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, + "PolicyArns.member.1.arn": []string{servicemocks.MockStsAssumeRolePolicyArn}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, - { + "assume_role_tags": { Config: map[string]interface{}{ "assume_role_tags": map[string]interface{}{ - awsbase.MockStsAssumeRoleTagKey: awsbase.MockStsAssumeRoleTagValue, + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, }, "bucket": "tf-test", "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "assume_role_tags", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"900"}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, - "Tags.member.1.Key": []string{awsbase.MockStsAssumeRoleTagKey}, - "Tags.member.1.Value": []string{awsbase.MockStsAssumeRoleTagValue}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, + "Tags.member.1.Key": []string{servicemocks.MockStsAssumeRoleTagKey}, + "Tags.member.1.Value": []string{servicemocks.MockStsAssumeRoleTagValue}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, - { + "assume_role_transitive_tag_keys": { Config: map[string]interface{}{ "assume_role_tags": map[string]interface{}{ - awsbase.MockStsAssumeRoleTagKey: awsbase.MockStsAssumeRoleTagValue, + servicemocks.MockStsAssumeRoleTagKey: servicemocks.MockStsAssumeRoleTagValue, }, - "assume_role_transitive_tag_keys": []interface{}{awsbase.MockStsAssumeRoleTagKey}, + "assume_role_transitive_tag_keys": []interface{}{servicemocks.MockStsAssumeRoleTagKey}, "bucket": "tf-test", "key": "state", "region": "us-west-1", - "role_arn": awsbase.MockStsAssumeRoleArn, - "session_name": awsbase.MockStsAssumeRoleSessionName, + "role_arn": servicemocks.MockStsAssumeRoleArn, + "session_name": servicemocks.MockStsAssumeRoleSessionName, }, - Description: "assume_role_transitive_tag_keys", - MockStsEndpoints: []*awsbase.MockEndpoint{ + MockStsEndpoints: []*servicemocks.MockEndpoint{ { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: url.Values{ "Action": []string{"AssumeRole"}, "DurationSeconds": []string{"900"}, - "RoleArn": []string{awsbase.MockStsAssumeRoleArn}, - "RoleSessionName": []string{awsbase.MockStsAssumeRoleSessionName}, - "Tags.member.1.Key": []string{awsbase.MockStsAssumeRoleTagKey}, - "Tags.member.1.Value": []string{awsbase.MockStsAssumeRoleTagValue}, - "TransitiveTagKeys.member.1": []string{awsbase.MockStsAssumeRoleTagKey}, + "RoleArn": []string{servicemocks.MockStsAssumeRoleArn}, + "RoleSessionName": []string{servicemocks.MockStsAssumeRoleSessionName}, + "Tags.member.1.Key": []string{servicemocks.MockStsAssumeRoleTagKey}, + "Tags.member.1.Value": []string{servicemocks.MockStsAssumeRoleTagValue}, + "TransitiveTagKeys.member.1": []string{servicemocks.MockStsAssumeRoleTagKey}, "Version": []string{"2011-06-15"}, }.Encode()}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsAssumeRoleValidResponseBody, ContentType: "text/xml"}, }, { - Request: &awsbase.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, - Response: &awsbase.MockResponse{StatusCode: 200, Body: awsbase.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, + Request: &servicemocks.MockRequest{Method: "POST", Uri: "/", Body: mockStsGetCallerIdentityRequestBody}, + Response: &servicemocks.MockResponse{StatusCode: 200, Body: servicemocks.MockStsGetCallerIdentityValidResponseBody, ContentType: "text/xml"}, }, }, }, } - for _, testCase := range testCases { + for testName, testCase := range testCases { testCase := testCase - t.Run(testCase.Description, func(t *testing.T) { - closeSts, mockStsSession, err := awsbase.GetMockedAwsApiSession("STS", testCase.MockStsEndpoints) + t.Run(testName, func(t *testing.T) { + closeSts, _, stsEndpoint := mockdata.GetMockedAwsApiSession("STS", testCase.MockStsEndpoints) defer closeSts() - if err != nil { - t.Fatalf("unexpected error creating mock STS server: %s", err) - } + testCase.Config["sts_endpoint"] = stsEndpoint - if mockStsSession != nil && mockStsSession.Config != nil { - testCase.Config["sts_endpoint"] = aws.StringValue(mockStsSession.Config.Endpoint) - } - - diags := New().Configure(hcl2shim.HCL2ValueFromConfigValue(testCase.Config)) + b := New() + diags := b.Configure(populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(testCase.Config))) if diags.HasErrors() { for _, diag := range diags { @@ -312,76 +1196,781 @@ func TestBackendConfig_AssumeRole(t *testing.T) { } } -func TestBackendConfig_invalidKey(t *testing.T) { - testACC(t) - cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ - "region": "us-west-1", - "bucket": "tf-test", - "key": "/leading-slash", - "encrypt": true, - "dynamodb_table": "dynamoTable", - }) +func TestBackendConfig_PrepareConfigValidation(t *testing.T) { + cases := map[string]struct { + config cty.Value + expectedDiags tfdiags.Diagnostics + }{ + "null bucket": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.NullVal(cty.String), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(cty.GetAttrPath("bucket")), + }, + }, + "empty bucket": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal(""), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + cty.GetAttrPath("bucket"), + ), + }, + }, - _, diags := New().PrepareConfig(cfg) - if !diags.HasErrors() { - t.Fatal("expected config validation error") + "null key": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.NullVal(cty.String), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(cty.GetAttrPath("key")), + }, + }, + "empty key": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal(""), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + cty.GetAttrPath("key"), + ), + }, + }, + "key with leading slash": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("/leading-slash"), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/"`, + cty.GetAttrPath("key"), + ), + }, + }, + "key with trailing slash": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("trailing-slash/"), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/"`, + cty.GetAttrPath("key"), + ), + }, + }, + "key with double slash": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test/with/double//slash"), + "region": cty.StringVal("us-west-2"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `Value must not contain "//"`, + cty.GetAttrPath("key"), + ), + }, + }, + + "null region": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.NullVal(cty.String), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Missing region value", + `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, + cty.GetAttrPath("region"), + ), + }, + }, + "empty region": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal(""), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Missing region value", + `The "region" attribute or the "AWS_REGION" or "AWS_DEFAULT_REGION" environment variables must be set.`, + cty.GetAttrPath("region"), + ), + }, + }, + + "workspace_key_prefix with leading slash": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + "workspace_key_prefix": cty.StringVal("/env"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/"`, + cty.GetAttrPath("workspace_key_prefix"), + ), + }, + }, + "workspace_key_prefix with trailing slash": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + "workspace_key_prefix": cty.StringVal("env/"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/"`, + cty.GetAttrPath("workspace_key_prefix"), + ), + }, + }, + + "encyrption key conflict": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + "workspace_key_prefix": cty.StringVal("env"), + "sse_customer_key": cty.StringVal("1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o="), + "kms_key_id": cty.StringVal("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + `Only one of kms_key_id, sse_customer_key can be set.`, + cty.Path{}, + ), + }, + }, + + "shared credentials file conflict": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + "shared_credentials_file": cty.StringVal("test"), + "shared_credentials_files": cty.SetVal([]cty.Value{cty.StringVal("test2")}), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + `Only one of shared_credentials_file, shared_credentials_files can be set.`, + cty.Path{}, + ), + attributeWarningDiag( + "Deprecated Parameter", + `The parameter "shared_credentials_file" is deprecated. Use parameter "shared_credentials_files" instead.`, + cty.GetAttrPath("shared_credentials_file"), + ), + }, + }, + + "allowed forbidden account ids conflict": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + "allowed_account_ids": cty.SetVal([]cty.Value{cty.StringVal("012345678901")}), + "forbidden_account_ids": cty.SetVal([]cty.Value{cty.StringVal("012345678901")}), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Attribute Combination", + `Only one of allowed_account_ids, forbidden_account_ids can be set.`, + cty.Path{}, + ), + }, + }, + + "dynamodb_table deprecation": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.StringVal("us-west-2"), + "dynamodb_table": cty.StringVal("test"), + }), + expectedDiags: tfdiags.Diagnostics{ + attributeWarningDiag( + "Deprecated Parameter", + `The parameter "dynamodb_table" is deprecated. Use parameter "use_lockfile" instead.`, + cty.GetAttrPath("dynamodb_table"), + ), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + servicemocks.StashEnv(t) + + b := New() + + _, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config)) + + if diff := cmp.Diff(valDiags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) } } -func TestBackendConfig_invalidSSECustomerKeyLength(t *testing.T) { - testACC(t) - cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ - "region": "us-west-1", - "bucket": "tf-test", - "encrypt": true, - "key": "state", - "dynamodb_table": "dynamoTable", - "sse_customer_key": "key", - }) +func TestBackendConfig_PrepareConfigWithEnvVars(t *testing.T) { + cases := map[string]struct { + config cty.Value + vars map[string]string + expectedErr string + }{ + "region env var AWS_REGION": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.NullVal(cty.String), + }), + vars: map[string]string{ + "AWS_REGION": "us-west-1", + }, + }, + "region env var AWS_DEFAULT_REGION": { + config: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "region": cty.NullVal(cty.String), + }), + vars: map[string]string{ + "AWS_DEFAULT_REGION": "us-west-1", + }, + }, + } - _, diags := New().PrepareConfig(cfg) - if !diags.HasErrors() { - t.Fatal("expected error for invalid sse_customer_key length") + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + servicemocks.StashEnv(t) + + b := New() + + for k, v := range tc.vars { + os.Setenv(k, v) + } + + _, valDiags := b.PrepareConfig(populateSchema(t, b.ConfigSchema(), tc.config)) + if tc.expectedErr != "" { + if valDiags.Err() != nil { + actualErr := valDiags.Err().Error() + if !strings.Contains(actualErr, tc.expectedErr) { + t.Fatalf("unexpected validation result: %v", valDiags.Err()) + } + } else { + t.Fatal("expected an error, got none") + } + } else if valDiags.Err() != nil { + t.Fatalf("expected no error, got %s", valDiags.Err()) + } + }) } } -func TestBackendConfig_invalidSSECustomerKeyEncoding(t *testing.T) { - testACC(t) - cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ - "region": "us-west-1", - "bucket": "tf-test", - "encrypt": true, - "key": "state", - "dynamodb_table": "dynamoTable", - "sse_customer_key": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka", - }) +type proxyCase struct { + url string + expectedProxy string +} - diags := New().Configure(cfg) - if !diags.HasErrors() { - t.Fatal("expected error for failing to decode sse_customer_key") +func TestBackendConfig_Proxy(t *testing.T) { + cases := map[string]struct { + config map[string]any + environmentVariables map[string]string + expectedDiags tfdiags.Diagnostics + urls []proxyCase + }{ + "no config": { + config: map[string]any{}, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "", + }, + }, + }, + + "http_proxy empty string": { + config: map[string]any{ + "http_proxy": "", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "", + }, + }, + }, + + "http_proxy config": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + }, + expectedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Missing HTTPS Proxy", + fmt.Sprintf( + "An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+ + "To specify no proxy for HTTPS, set the HTTPS to an empty string.", + "http://http-proxy.test:1234"), + ), + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + }, + }, + + "https_proxy config": { + config: map[string]any{ + "https_proxy": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "http_proxy config https_proxy config": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + "https_proxy": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "http_proxy config https_proxy config empty string": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + "https_proxy": "", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "", + }, + }, + }, + + "https_proxy config http_proxy config empty string": { + config: map[string]any{ + "http_proxy": "", + "https_proxy": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "http_proxy config https_proxy config no_proxy config": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + "https_proxy": "http://https-proxy.test:1234", + "no_proxy": "dont-proxy.test", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "http://dont-proxy.test", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + { + url: "https://dont-proxy.test", + expectedProxy: "", + }, + }, + }, + + "HTTP_PROXY envvar": { + config: map[string]any{}, + environmentVariables: map[string]string{ + "HTTP_PROXY": "http://http-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "", + }, + }, + }, + + "http_proxy envvar": { + config: map[string]any{}, + environmentVariables: map[string]string{ + "http_proxy": "http://http-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "", + }, + }, + }, + + "HTTPS_PROXY envvar": { + config: map[string]any{}, + environmentVariables: map[string]string{ + "HTTPS_PROXY": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "https_proxy envvar": { + config: map[string]any{}, + environmentVariables: map[string]string{ + "https_proxy": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "http_proxy config HTTPS_PROXY envvar": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + }, + environmentVariables: map[string]string{ + "HTTPS_PROXY": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "http_proxy config https_proxy envvar": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + }, + environmentVariables: map[string]string{ + "https_proxy": "http://https-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + }, + }, + + "http_proxy config NO_PROXY envvar": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + }, + environmentVariables: map[string]string{ + "NO_PROXY": "dont-proxy.test", + }, + expectedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Missing HTTPS Proxy", + fmt.Sprintf( + "An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+ + "To specify no proxy for HTTPS, set the HTTPS to an empty string.", + "http://http-proxy.test:1234"), + ), + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "http://dont-proxy.test", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://dont-proxy.test", + expectedProxy: "", + }, + }, + }, + + "http_proxy config no_proxy envvar": { + config: map[string]any{ + "http_proxy": "http://http-proxy.test:1234", + }, + environmentVariables: map[string]string{ + "no_proxy": "dont-proxy.test", + }, + expectedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Missing HTTPS Proxy", + fmt.Sprintf( + "An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+ + "To specify no proxy for HTTPS, set the HTTPS to an empty string.", + "http://http-proxy.test:1234"), + ), + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "http://dont-proxy.test", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "https://dont-proxy.test", + expectedProxy: "", + }, + }, + }, + + "HTTP_PROXY envvar HTTPS_PROXY envvar NO_PROXY envvar": { + config: map[string]any{}, + environmentVariables: map[string]string{ + "HTTP_PROXY": "http://http-proxy.test:1234", + "HTTPS_PROXY": "http://https-proxy.test:1234", + "NO_PROXY": "dont-proxy.test", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://http-proxy.test:1234", + }, + { + url: "http://dont-proxy.test", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://https-proxy.test:1234", + }, + { + url: "https://dont-proxy.test", + expectedProxy: "", + }, + }, + }, + + "http_proxy config overrides HTTP_PROXY envvar": { + config: map[string]any{ + "http_proxy": "http://config-proxy.test:1234", + }, + environmentVariables: map[string]string{ + "HTTP_PROXY": "http://envvar-proxy.test:1234", + }, + expectedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Missing HTTPS Proxy", + fmt.Sprintf( + "An HTTP proxy was set but no HTTPS proxy was. Using HTTP proxy %q for HTTPS requests. This behavior may change in future versions.\n\n"+ + "To specify no proxy for HTTPS, set the HTTPS to an empty string.", + "http://config-proxy.test:1234"), + ), + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "http://config-proxy.test:1234", + }, + { + url: "https://example.com", + expectedProxy: "http://config-proxy.test:1234", + }, + }, + }, + + "https_proxy config overrides HTTPS_PROXY envvar": { + config: map[string]any{ + "https_proxy": "http://config-proxy.test:1234", + }, + environmentVariables: map[string]string{ + "HTTPS_PROXY": "http://envvar-proxy.test:1234", + }, + urls: []proxyCase{ + { + url: "http://example.com", + expectedProxy: "", + }, + { + url: "https://example.com", + expectedProxy: "http://config-proxy.test:1234", + }, + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + config := map[string]any{ + "region": "us-west-2", + "bucket": "tf-test", + "key": "state", + "skip_credentials_validation": true, + "skip_requesting_account_id": true, + "access_key": servicemocks.MockStaticAccessKey, + "secret_key": servicemocks.MockStaticSecretKey, + } + + for k, v := range tc.environmentVariables { + t.Setenv(k, v) + } + + maps.Copy(config, tc.config) + + raw, diags := testBackendConfigDiags(t, New(), backend.TestWrapConfig(config)) + b := raw.(*Backend) + + tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectedDiags) + + client := b.awsConfig.HTTPClient + bClient, ok := client.(*awshttp.BuildableClient) + if !ok { + t.Fatalf("expected awshttp.BuildableClient, got %T", client) + } + transport := bClient.GetTransport() + proxyF := transport.Proxy + + for _, url := range tc.urls { + req, _ := http.NewRequest("GET", url.url, nil) + pUrl, err := proxyF(req) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if url.expectedProxy != "" { + if pUrl == nil { + t.Errorf("expected proxy for %q, got none", url.url) + } else if pUrl.String() != url.expectedProxy { + t.Errorf("expected proxy %q for %q, got %q", url.expectedProxy, url.url, pUrl.String()) + } + } else { + if pUrl != nil { + t.Errorf("expected no proxy for %q, got %q", url.url, pUrl.String()) + } + } + } + }) } } -func TestBackendConfig_conflictingEncryptionSchema(t *testing.T) { +func TestBackendBasic(t *testing.T) { testACC(t) - cfg := hcl2shim.HCL2ValueFromConfigValue(map[string]interface{}{ - "region": "us-west-1", - "bucket": "tf-test", - "key": "state", - "encrypt": true, - "dynamodb_table": "dynamoTable", - "sse_customer_key": "1hwbcNPGWL+AwDiyGmRidTWAEVmCWMKbEHA+Es8w75o=", - "kms_key_id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab", - }) - diags := New().Configure(cfg) - if !diags.HasErrors() { - t.Fatal("expected error for simultaneous usage of kms_key_id and sse_customer_key") - } -} - -func TestBackend(t *testing.T) { - testACC(t) + ctx := context.TODO() bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" @@ -390,10 +1979,11 @@ func TestBackend(t *testing.T) { "bucket": bucketName, "key": keyName, "encrypt": true, + "region": "us-west-1", })).(*Backend) - createS3Bucket(t, b.s3Client, bucketName) - defer deleteS3Bucket(t, b.s3Client, bucketName) + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) backend.TestBackendStates(t, b) } @@ -401,6 +1991,8 @@ func TestBackend(t *testing.T) { func TestBackendLocked(t *testing.T) { testACC(t) + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "test/state" @@ -409,6 +2001,7 @@ func TestBackendLocked(t *testing.T) { "key": keyName, "encrypt": true, "dynamodb_table": bucketName, + "region": "us-west-1", })).(*Backend) b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ @@ -416,37 +2009,650 @@ func TestBackendLocked(t *testing.T) { "key": keyName, "encrypt": true, "dynamodb_table": bucketName, + "region": "us-west-1", })).(*Backend) - createS3Bucket(t, b1.s3Client, bucketName) - defer deleteS3Bucket(t, b1.s3Client, bucketName) - createDynamoDBTable(t, b1.dynClient, bucketName) - defer deleteDynamoDBTable(t, b1.dynClient, bucketName) + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) backend.TestBackendStateLocks(t, b1, b2) backend.TestBackendStateForceUnlock(t, b1, b2) } -func TestBackendSSECustomerKey(t *testing.T) { +func TestBackendLockedWithFile(t *testing.T) { testACC(t) - bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "bucket": bucketName, - "encrypt": true, - "key": "test-SSE-C", - "sse_customer_key": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=", + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", })).(*Backend) - createS3Bucket(t, b.s3Client, bucketName) - defer deleteS3Bucket(t, b.s3Client, bucketName) + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) - backend.TestBackendStates(t, b) + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackendLockedWithFile_ObjectLock_Compliance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance), + ) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackendLockedWithFile_ObjectLock_Governance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance), + ) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackendLockedWithFileAndDynamoDB(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackendLockedMixedFileAndDynamoDB(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackend_LockFileCleanupOnDynamoDBLock(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": false, // Only use DynamoDB + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, // Use both DynamoDB and lockfile + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + backend.TestBackendStateLocks(t, b1, b2) + + // Attempt to retrieve the lock file from S3. + _, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(b1.bucketName), + Key: aws.String(b1.keyName + ".tflock"), + }) + // We expect an error here, indicating that the lock file does not exist. + // The absence of the lock file is expected, as it should have been + // cleaned up following a failed lock acquisition due to `b1` already + // acquiring a DynamoDB lock. + if err != nil { + if !IsA[*s3types.NoSuchKey](err) { + t.Fatalf("unexpected error: %s", err) + } + } else { + t.Fatalf("expected error, got none") + } +} + +func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Compliance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": false, // Only use DynamoDB + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, // Use both DynamoDB and lockfile + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance), + ) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + backend.TestBackendStateLocks(t, b1, b2) + + // Attempt to retrieve the lock file from S3. + _, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(b1.bucketName), + Key: aws.String(b1.keyName + ".tflock"), + }) + // We expect an error here, indicating that the lock file does not exist. + // The absence of the lock file is expected, as it should have been + // cleaned up following a failed lock acquisition due to `b1` already + // acquiring a DynamoDB lock. + if err != nil { + if !IsA[*s3types.NoSuchKey](err) { + t.Fatalf("unexpected error: %s", err) + } + } else { + t.Fatalf("expected error, got none") + } +} + +func TestBackend_LockFileCleanupOnDynamoDBLock_ObjectLock_Governance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": false, // Only use DynamoDB + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, // Use both DynamoDB and lockfile + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance), + ) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + backend.TestBackendStateLocks(t, b1, b2) + + // Attempt to retrieve the lock file from S3. + _, err := b1.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(b1.bucketName), + Key: aws.String(b1.keyName + ".tflock"), + }) + // We expect an error here, indicating that the lock file does not exist. + // The absence of the lock file is expected, as it should have been + // cleaned up following a failed lock acquisition due to `b1` already + // acquiring a DynamoDB lock. + if err != nil { + if !IsA[*s3types.NoSuchKey](err) { + t.Fatalf("unexpected error: %s", err) + } + } else { + t.Fatalf("expected error, got none") + } +} + +func TestBackend_LockDeletedOutOfBand(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "dynamodb_table": bucketName, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + testBackendStateLockDeletedOutOfBand(ctx, t, b1) +} + +func TestBackend_KmsKeyId(t *testing.T) { + testACC(t) + kmsKeyID := os.Getenv("TF_S3_TEST_KMS_KEY_ID") + if kmsKeyID == "" { + t.Skip("TF_S3_KMS_KEY_ID is empty. Set this variable to an existing KMS key ID to run this test.") + } + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "kms_key_id": kmsKeyID, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "kms_key_id": kmsKeyID, + "use_lockfile": true, + "region": "us-west-2", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackend_ACL(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "test/state" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + "acl": "bucket-owner-full-control", + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + "region": "us-west-2", + "acl": "bucket-owner-full-control", + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + + backend.TestBackendStateLocks(t, b1, b2) + backend.TestBackendStateForceUnlock(t, b1, b2) +} + +func TestBackendConfigKmsKeyId(t *testing.T) { + testACC(t) + + testCases := map[string]struct { + config map[string]any + expectedKeyId string + expectedDiags tfdiags.Diagnostics + }{ + "valid": { + config: map[string]any{ + "kms_key_id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab", + }, + expectedKeyId: "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab", + }, + + "invalid": { + config: map[string]any{ + "kms_key_id": "not-an-arn", + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid KMS Key ID", + `Value must be a valid KMS Key ID, got "not-an-arn"`, + cty.GetAttrPath("kms_key_id"), + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + config := map[string]any{ + "bucket": bucketName, + "encrypt": true, + "key": "test-SSE-KMS", + "region": "us-west-1", + } + maps.Copy(config, tc.config) + + b := New().(*Backend) + configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)) + + configSchema, diags := b.PrepareConfig(configSchema) + + if !diags.HasErrors() { + confDiags := b.Configure(configSchema) + diags = diags.Append(confDiags) + } + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Fatalf("unexpected diagnostics difference: %s", diff) + } + + if tc.expectedKeyId != "" { + if string(b.kmsKeyID) != tc.expectedKeyId { + t.Fatal("unexpected value for KMS key Id") + } + } + }) + } +} + +func TestBackendSSECustomerKey(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + testCases := map[string]struct { + config map[string]any + environmentVariables map[string]string + expectedKey string + expectedDiags tfdiags.Diagnostics + }{ + // config + "config valid": { + config: map[string]any{ + "sse_customer_key": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=", + }, + expectedKey: string(must(base64.StdEncoding.DecodeString("4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk="))), + }, + "config invalid length": { + config: map[string]any{ + "sse_customer_key": "test", + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid sse_customer_key value", + "sse_customer_key must be 44 characters in length", + cty.GetAttrPath("sse_customer_key"), + ), + }, + }, + "config invalid encoding": { + config: map[string]any{ + "sse_customer_key": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka", + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid sse_customer_key value", + "sse_customer_key must be base64 encoded: illegal base64 data at input byte 0", + cty.GetAttrPath("sse_customer_key"), + ), + }, + }, + + // env var + "envvar valid": { + environmentVariables: map[string]string{ + "AWS_SSE_CUSTOMER_KEY": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=", + }, + expectedKey: string(must(base64.StdEncoding.DecodeString("4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk="))), + }, + "envvar invalid length": { + environmentVariables: map[string]string{ + "AWS_SSE_CUSTOMER_KEY": "test", + }, + expectedDiags: tfdiags.Diagnostics{ + wholeBodyErrDiag( + "Invalid AWS_SSE_CUSTOMER_KEY value", + `The environment variable "AWS_SSE_CUSTOMER_KEY" must be 44 characters in length`, + ), + }, + }, + "envvar invalid encoding": { + environmentVariables: map[string]string{ + "AWS_SSE_CUSTOMER_KEY": "====CT70aTYB2JGff7AjQtwbiLkwH4npICay1PWtmdka", + }, + expectedDiags: tfdiags.Diagnostics{ + wholeBodyErrDiag( + "Invalid AWS_SSE_CUSTOMER_KEY value", + `The environment variable "AWS_SSE_CUSTOMER_KEY" must be base64 encoded: illegal base64 data at input byte 0`, + ), + }, + }, + + // conflict + "config kms_key_id and envvar AWS_SSE_CUSTOMER_KEY": { + config: map[string]any{ + "kms_key_id": "arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-ab56-1234567890ab", + }, + environmentVariables: map[string]string{ + "AWS_SSE_CUSTOMER_KEY": "4Dm1n4rphuFgawxuzY/bEfvLf6rYK0gIjfaDSLlfXNk=", + }, + expectedDiags: tfdiags.Diagnostics{ + wholeBodyErrDiag( + "Invalid encryption configuration", + encryptionKeyConflictEnvVarError, + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + config := map[string]any{ + "bucket": bucketName, + "encrypt": true, + "key": "test-SSE-C", + "region": "us-west-1", + } + maps.Copy(config, tc.config) + + oldEnv := os.Environ() // For now, save without clearing + defer servicemocks.PopEnv(oldEnv) + for k, v := range tc.environmentVariables { + os.Setenv(k, v) + } + + b := New().(*Backend) + configSchema := populateSchema(t, b.ConfigSchema(), hcl2shim.HCL2ValueFromConfigValue(config)) + + configSchema, diags := b.PrepareConfig(configSchema) + + if !diags.HasErrors() { + confDiags := b.Configure(configSchema) + diags = diags.Append(confDiags) + } + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Fatalf("unexpected diagnostics difference: %s", diff) + } + + if tc.expectedKey != "" { + if string(b.customerEncryptionKey) != tc.expectedKey { + t.Fatal("unexpected value for customer encryption key") + } + } + + if !diags.HasErrors() { + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + backend.TestBackendStates(t, b) + } + }) + } } // add some extra junk in S3 to try and confuse the env listing. func TestBackendExtraPaths(t *testing.T) { testACC(t) + + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "test/state/tfstate" @@ -456,8 +2662,8 @@ func TestBackendExtraPaths(t *testing.T) { "encrypt": true, })).(*Backend) - createS3Bucket(t, b.s3Client, bucketName) - defer deleteS3Bucket(t, b.s3Client, bucketName) + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) // put multiple states in old env paths. s1 := states.NewState() @@ -477,8 +2683,10 @@ func TestBackendExtraPaths(t *testing.T) { // Write the first state stateMgr := &remote.State{Client: client} - stateMgr.WriteState(s1) - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.WriteState(s1); err != nil { + t.Fatal(err) + } + if err := stateMgr.PersistState(nil); err != nil { t.Fatal(err) } @@ -487,8 +2695,10 @@ func TestBackendExtraPaths(t *testing.T) { // states are equal, the state will not Put to the remote client.path = b.path("s2") stateMgr2 := &remote.State{Client: client} - stateMgr2.WriteState(s2) - if err := stateMgr2.PersistState(); err != nil { + if err := stateMgr2.WriteState(s2); err != nil { + t.Fatal(err) + } + if err := stateMgr2.PersistState(nil); err != nil { t.Fatal(err) } @@ -500,8 +2710,10 @@ func TestBackendExtraPaths(t *testing.T) { // put a state in an env directory name client.path = b.workspaceKeyPrefix + "/error" - stateMgr.WriteState(states.NewState()) - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { + t.Fatal(err) + } + if err := stateMgr.PersistState(nil); err != nil { t.Fatal(err) } if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { @@ -510,8 +2722,10 @@ func TestBackendExtraPaths(t *testing.T) { // add state with the wrong key for an existing env client.path = b.workspaceKeyPrefix + "/s2/notTestState" - stateMgr.WriteState(states.NewState()) - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { + t.Fatal(err) + } + if err := stateMgr.PersistState(nil); err != nil { t.Fatal(err) } if err := checkStateList(b, []string{"default", "s1", "s2"}); err != nil { @@ -524,7 +2738,7 @@ func TestBackendExtraPaths(t *testing.T) { } // delete the real workspace - if err := b.DeleteWorkspace("s2"); err != nil { + if err := b.DeleteWorkspace("s2", true); err != nil { t.Fatal(err) } @@ -544,13 +2758,15 @@ func TestBackendExtraPaths(t *testing.T) { if s2Mgr.(*remote.State).StateSnapshotMeta().Lineage == s2Lineage { t.Fatal("state s2 was not deleted") } - s2 = s2Mgr.State() + _ = s2Mgr.State() // We need the side-effect s2Lineage = stateMgr.StateSnapshotMeta().Lineage // add a state with a key that matches an existing environment dir name client.path = b.workspaceKeyPrefix + "/s2/" - stateMgr.WriteState(states.NewState()) - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.WriteState(states.NewState()); err != nil { + t.Fatal(err) + } + if err := stateMgr.PersistState(nil); err != nil { t.Fatal(err) } @@ -576,6 +2792,9 @@ func TestBackendExtraPaths(t *testing.T) { // of the workspace name itself. func TestBackendPrefixInWorkspace(t *testing.T) { testACC(t) + + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ @@ -584,8 +2803,8 @@ func TestBackendPrefixInWorkspace(t *testing.T) { "workspace_key_prefix": "env", })).(*Backend) - createS3Bucket(t, b.s3Client, bucketName) - defer deleteS3Bucket(t, b.s3Client, bucketName) + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) // get a state that contains the prefix as a substring sMgr, err := b.StateMgr("env-1") @@ -601,8 +2820,218 @@ func TestBackendPrefixInWorkspace(t *testing.T) { } } +// ensure that we create the lock file in the correct location when using a +// workspace prefix. +func TestBackendLockFileWithPrefix(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + + workspacePrefix := "prefix" + key := "test/test-env.tfstate" + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "use_lockfile": true, + "key": key, + "workspace_key_prefix": workspacePrefix, + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, s3BucketWithVersioning) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + // get a state that contains the prefix as a substring + sMgr, err := b.StateMgr("env-1") + if err != nil { + t.Fatal(err) + } + if err := sMgr.RefreshState(); err != nil { + t.Fatal(err) + } + + if err := checkStateList(b, []string{"default", "env-1"}); err != nil { + t.Fatal(err) + } + + // Check if the lock file is created in the correct location + // + // If created and cleaned up correctly, a delete marker should + // be present at the lock file key location. + lockFileKey := fmt.Sprintf("%s/env-1/%s.tflock", workspacePrefix, key) + out, err := b.s3Client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{ + Bucket: aws.String(bucketName), + }) + + found := false + for _, item := range out.DeleteMarkers { + if aws.ToString(item.Key) == lockFileKey { + found = true + } + } + if !found { + t.Fatalf("lock file %q not found in expected location", lockFileKey) + } +} + +func TestBackendRestrictedRoot_Default(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + workspacePrefix := defaultWorkspaceKeyPrefix + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": "test/test-env.tfstate", + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, s3BucketWithPolicy(fmt.Sprintf(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Statement1", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::%[1]s", + "Condition": { + "StringLike": { + "s3:prefix": "%[2]s/*" + } + } + } + ] +}`, bucketName, workspacePrefix))) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + sMgr, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + if err := sMgr.RefreshState(); err != nil { + t.Fatal(err) + } + + if err := checkStateList(b, []string{"default"}); err != nil { + t.Fatal(err) + } +} + +func TestBackendRestrictedRoot_NamedPrefix(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + workspacePrefix := "prefix" + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": "test/test-env.tfstate", + "workspace_key_prefix": workspacePrefix, + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, s3BucketWithPolicy(fmt.Sprintf(`{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Statement1", + "Effect": "Deny", + "Principal": "*", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::%[1]s", + "Condition": { + "StringLike": { + "s3:prefix": "%[2]s/*" + } + } + } + ] +}`, bucketName, workspacePrefix))) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + _, err := b.StateMgr(backend.DefaultStateName) + if err == nil { + t.Fatal("expected AccessDenied error, got none") + } + if s := err.Error(); !strings.Contains(s, fmt.Sprintf("Unable to list objects in S3 bucket %q with prefix %q:", bucketName, workspacePrefix+"/")) { + t.Fatalf("expected AccessDenied error, got: %s", s) + } +} + +func TestBackendWrongRegion(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "testState" + + bucketRegion := "us-west-1" + backendRegion := "us-east-1" + if backendRegion == bucketRegion { + t.Fatalf("bucket region and backend region must not be the same") + } + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "region": backendRegion, + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, bucketRegion) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, bucketRegion) + + if _, err := b.StateMgr(backend.DefaultStateName); err == nil { + t.Fatal("expected error, got none") + } else { + if regionErr, ok := As[bucketRegionError](err); ok { + if a, e := regionErr.bucketRegion, bucketRegion; a != e { + t.Errorf("expected bucket region %q, got %q", e, a) + } + if a, e := regionErr.requestRegion, backendRegion; a != e { + t.Errorf("expected request region %q, got %q", e, a) + } + } else { + t.Fatalf("expected bucket region error, got: %v", err) + } + } +} + +func TestBackendS3ObjectLock(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "testState" + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "region": "us-west-1", + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance), + ) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + backend.TestBackendStates(t, b) +} + func TestKeyEnv(t *testing.T) { testACC(t) + + ctx := context.TODO() + keyName := "some/paths/tfstate" bucket0Name := fmt.Sprintf("terraform-remote-s3-test-%x-0", time.Now().Unix()) @@ -613,8 +3042,8 @@ func TestKeyEnv(t *testing.T) { "workspace_key_prefix": "", })).(*Backend) - createS3Bucket(t, b0.s3Client, bucket0Name) - defer deleteS3Bucket(t, b0.s3Client, bucket0Name) + createS3Bucket(ctx, t, b0.s3Client, bucket0Name, b0.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b0.s3Client, bucket0Name, b0.awsConfig.Region) bucket1Name := fmt.Sprintf("terraform-remote-s3-test-%x-1", time.Now().Unix()) b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ @@ -624,8 +3053,8 @@ func TestKeyEnv(t *testing.T) { "workspace_key_prefix": "project/env:", })).(*Backend) - createS3Bucket(t, b1.s3Client, bucket1Name) - defer deleteS3Bucket(t, b1.s3Client, bucket1Name) + createS3Bucket(ctx, t, b1.s3Client, bucket1Name, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucket1Name, b1.awsConfig.Region) bucket2Name := fmt.Sprintf("terraform-remote-s3-test-%x-2", time.Now().Unix()) b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ @@ -634,8 +3063,8 @@ func TestKeyEnv(t *testing.T) { "encrypt": true, })).(*Backend) - createS3Bucket(t, b2.s3Client, bucket2Name) - defer deleteS3Bucket(t, b2.s3Client, bucket2Name) + createS3Bucket(ctx, t, b2.s3Client, bucket2Name, b2.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b2.s3Client, bucket2Name, b2.awsConfig.Region) if err := testGetWorkspaceForKey(b0, "some/paths/tfstate", ""); err != nil { t.Fatal(err) @@ -662,6 +3091,368 @@ func TestKeyEnv(t *testing.T) { backend.TestBackendStates(t, b2) } +func TestAssumeRole_PrepareConfigValidation(t *testing.T) { + path := cty.GetAttrPath("field") + + cases := map[string]struct { + config map[string]cty.Value + expectedDiags tfdiags.Diagnostics + }{ + "basic": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + }, + }, + + "invalid ARN": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("not an arn"), + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid ARN", + `The value "not an arn" cannot be parsed as an ARN: arn: invalid prefix`, + path.GetAttr("role_arn"), + ), + }, + }, + + "no role_arn": { + config: map[string]cty.Value{}, + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(path.GetAttr("role_arn")), + }, + }, + + "nil role_arn": { + config: map[string]cty.Value{}, + expectedDiags: tfdiags.Diagnostics{ + requiredAttributeErrDiag(path.GetAttr("role_arn")), + }, + }, + + "empty role_arn": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal(""), + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + path.GetAttr("role_arn"), + ), + }, + }, + + "with duration": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "duration": cty.StringVal("2h"), + }, + }, + + "invalid duration": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "duration": cty.StringVal("two hours"), + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Duration", + `The value "two hours" cannot be parsed as a duration: time: invalid duration "two hours"`, + path.GetAttr("duration"), + ), + }, + }, + + "with external_id": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "external_id": cty.StringVal("external-id"), + }, + }, + + "empty external_id": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "external_id": cty.StringVal(""), + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value Length", + `Length must be between 2 and 1224, had 0`, + path.GetAttr("external_id"), + ), + }, + }, + + "with policy": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "policy": cty.StringVal("{}"), + }, + }, + + "invalid policy": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "policy": cty.StringVal(""), + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `The value cannot be empty or all whitespace`, + path.GetAttr("policy"), + ), + }, + }, + + "with policy_arns": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "policy_arns": cty.SetVal([]cty.Value{ + cty.StringVal("arn:aws:iam::123456789012:policy/testpolicy"), + }), + }, + }, + + "invalid policy_arns": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "policy_arns": cty.SetVal([]cty.Value{ + cty.StringVal("not an arn"), + }), + }, + expectedDiags: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid ARN", + `The value "not an arn" cannot be parsed as an ARN: arn: invalid prefix`, + path.GetAttr("policy_arns").IndexString("not an arn"), + ), + }, + }, + + "with session_name": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "session_name": cty.StringVal("session-name"), + }, + }, + + "source_identity": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "source_identity": cty.StringVal("source-identity"), + }, + }, + + "with tags": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "tags": cty.MapVal(map[string]cty.Value{ + "tag-key": cty.StringVal("tag-value"), + }), + }, + }, + + "with transitive_tag_keys": { + config: map[string]cty.Value{ + "role_arn": cty.StringVal("arn:aws:iam::123456789012:role/testrole"), + "transitive_tag_keys": cty.SetVal([]cty.Value{ + cty.StringVal("tag-key"), + }), + }, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + schema := assumeRoleSchema.Attributes + vals := make(map[string]cty.Value, len(schema)) + for name, attrSchema := range schema { + if val, ok := tc.config[name]; ok { + vals[name] = val + } else { + vals[name] = cty.NullVal(attrSchema.SchemaAttribute().Type) + } + } + config := cty.ObjectVal(vals) + + var diags tfdiags.Diagnostics + validateNestedAttribute(assumeRoleSchema, config, path, &diags) + + if diff := cmp.Diff(diags, tc.expectedDiags, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +// TestBackend_CoerceValue verifies a cty.Object can be coerced into +// an s3 backend Block +// +// This serves as a smoke test for use of the terraform_remote_state +// data source with the s3 backend, replicating the process that +// data source uses. The returned value is ignored as the object is +// large (representing the entire s3 backend schema) and the focus of +// this test is early detection of coercion failures. +func TestBackend_CoerceValue(t *testing.T) { + testCases := map[string]struct { + Input cty.Value + WantErr string + }{ + "basic": { + Input: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + }), + }, + "missing bucket": { + Input: cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("test"), + }), + WantErr: `attribute "bucket" is required`, + }, + "missing key": { + Input: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + }), + WantErr: `attribute "key" is required`, + }, + "assume_role": { + Input: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "assume_role": cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("test"), + }), + }), + }, + "assume_role missing role_arn": { + Input: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "assume_role": cty.ObjectVal(map[string]cty.Value{}), + }), + WantErr: `.assume_role: attribute "role_arn" is required`, + }, + "assume_role_with_web_identity": { + Input: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "assume_role_with_web_identity": cty.ObjectVal(map[string]cty.Value{ + "role_arn": cty.StringVal("test"), + }), + }), + }, + "assume_role_with_web_identity missing role_arn": { + Input: cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + "key": cty.StringVal("test"), + "assume_role_with_web_identity": cty.ObjectVal(map[string]cty.Value{}), + }), + WantErr: `.assume_role_with_web_identity: attribute "role_arn" is required`, + }, + } + + for name, test := range testCases { + t.Run(name, func(t *testing.T) { + b := Backend{} + // Skip checking the returned cty.Value as this object will be large. + _, gotErrObj := b.ConfigSchema().CoerceValue(test.Input) + + if gotErrObj == nil { + if test.WantErr != "" { + t.Fatalf("coersion succeeded; want error: %q", test.WantErr) + } + } else { + gotErr := tfdiags.FormatError(gotErrObj) + if gotErr != test.WantErr { + t.Fatalf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantErr) + } + } + }) + } +} + +func testBackendStateLockDeletedOutOfBand(ctx context.Context, t *testing.T, b1 *Backend) { + t.Helper() + + tableName := b1.ddbTable + bucketName := b1.bucketName + s3StateKey := b1.keyName + s3LockKey := s3StateKey + lockFileSuffix + // The dynamoDB LockID value is the full statfile path (not the generated UUID) + ddbLockID := fmt.Sprintf("%s/%s", bucketName, s3StateKey) + + // Get the default state + b1StateMgr, err := b1.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("error: %s", err) + } + if err := b1StateMgr.RefreshState(); err != nil { + t.Fatalf("bad: %s", err) + } + + // Fast exit if this doesn't support locking at all + if _, ok := b1StateMgr.(statemgr.Locker); !ok { + t.Logf("TestBackend: backend %T doesn't support state locking, not testing", b1) + return + } + + t.Logf("testing deletion of a dynamoDB state lock out of band") + + // Reassign so its obvious whats happening + locker := b1StateMgr.(statemgr.Locker) + + info := statemgr.NewLockInfo() + info.Operation = "test" + info.Who = "clientA" + + lockID, err := locker.Lock(info) + if err != nil { + t.Fatal("unable to get initial lock:", err) + } + + getInput := &s3.GetObjectInput{ + Bucket: &bucketName, + Key: &s3LockKey, + } + + // Verify the s3 lock file exists + if _, err = b1.s3Client.GetObject(ctx, getInput); err != nil { + t.Fatal("failed to get s3 lock file:", err) + } + + deleteInput := &dynamodb.DeleteItemInput{ + Key: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: ddbLockID, + }, + }, + TableName: aws.String(tableName), + } + + // Delete the DynamoDB lock out of band + if _, err = b1.dynClient.DeleteItem(ctx, deleteInput); err != nil { + t.Fatal("failed to delete dynamodb item:", err) + } + + if err = locker.Unlock(lockID); err == nil { + t.Fatal("expected unlock failure, no error returned") + } + + // Verify the s3 lock file was still cleaned up by Unlock + _, err = b1.s3Client.GetObject(ctx, getInput) + if err != nil { + if !IsA[*s3types.NoSuchKey](err) { + t.Fatalf("unexpected error getting s3 lock file: %s", err) + } + } else { + t.Fatalf("expected error getting s3 lock file, got none") + } +} + func testGetWorkspaceForKey(b *Backend, key string, expected string) error { if actual := b.keyEnv(key); actual != expected { return fmt.Errorf("incorrect workspace for key[%q]. Expected[%q]: Actual[%q]", key, expected, actual) @@ -681,65 +3472,171 @@ func checkStateList(b backend.Backend, expected []string) error { return nil } -func createS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { +type createS3BucketOptions struct { + versioning bool + objectLockMode s3types.ObjectLockRetentionMode + policy string +} + +type createS3BucketOptionsFunc func(*createS3BucketOptions) + +func createS3Bucket(ctx context.Context, t *testing.T, s3Client *s3.Client, bucketName, region string, optFns ...createS3BucketOptionsFunc) { + t.Helper() + + var opts createS3BucketOptions + for _, f := range optFns { + f(&opts) + } + createBucketReq := &s3.CreateBucketInput{ - Bucket: &bucketName, + Bucket: aws.String(bucketName), + } + if region != "us-east-1" { + createBucketReq.CreateBucketConfiguration = &s3types.CreateBucketConfiguration{ + LocationConstraint: s3types.BucketLocationConstraint(region), + } + } + if opts.objectLockMode != "" { + createBucketReq.ObjectLockEnabledForBucket = aws.Bool(true) } // Be clear about what we're doing in case the user needs to clean // this up later. - t.Logf("creating S3 bucket %s in %s", bucketName, *s3Client.Config.Region) - _, err := s3Client.CreateBucket(createBucketReq) + t.Logf("creating S3 bucket %s in %s", bucketName, region) + _, err := s3Client.CreateBucket(ctx, createBucketReq, s3WithRegion(region)) if err != nil { t.Fatal("failed to create test S3 bucket:", err) } + + if opts.versioning { + _, err := s3Client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{ + Bucket: aws.String(bucketName), + VersioningConfiguration: &s3types.VersioningConfiguration{ + Status: s3types.BucketVersioningStatusEnabled, + }, + }) + if err != nil { + t.Fatalf("failed enabling versioning: %s", err) + } + } + + if opts.objectLockMode != "" { + _, err := s3Client.PutObjectLockConfiguration(ctx, &s3.PutObjectLockConfigurationInput{ + Bucket: aws.String(bucketName), + ObjectLockConfiguration: &s3types.ObjectLockConfiguration{ + ObjectLockEnabled: s3types.ObjectLockEnabledEnabled, + Rule: &s3types.ObjectLockRule{ + DefaultRetention: &s3types.DefaultRetention{ + Days: aws.Int32(1), + Mode: opts.objectLockMode, + }, + }, + }, + }) + if err != nil { + t.Fatalf("failed enabling object locking: %s", err) + } + } + + if opts.policy != "" { + _, err := s3Client.PutBucketPolicy(ctx, &s3.PutBucketPolicyInput{ + Bucket: aws.String(bucketName), + Policy: &opts.policy, + }) + if err != nil { + t.Fatalf("failed setting bucket policy: %s", err) + } + } } -func deleteS3Bucket(t *testing.T, s3Client *s3.S3, bucketName string) { +func s3BucketWithVersioning(opts *createS3BucketOptions) { + opts.versioning = true +} + +func s3BucketWithObjectLock(mode s3types.ObjectLockRetentionMode) createS3BucketOptionsFunc { + return func(opts *createS3BucketOptions) { + opts.objectLockMode = mode + } +} + +func s3BucketWithPolicy(policy string) createS3BucketOptionsFunc { + return func(opts *createS3BucketOptions) { + opts.policy = policy + } +} + +func deleteS3Bucket(ctx context.Context, t *testing.T, s3Client *s3.Client, bucketName, region string) { + t.Helper() + warning := "WARNING: Failed to delete the test S3 bucket. It may have been left in your AWS account and may incur storage charges. (error was %s)" // first we have to get rid of the env objects, or we can't delete the bucket - resp, err := s3Client.ListObjects(&s3.ListObjectsInput{Bucket: &bucketName}) + resp, err := s3Client.ListObjectVersions(ctx, &s3.ListObjectVersionsInput{Bucket: &bucketName}, s3WithRegion(region)) if err != nil { t.Logf(warning, err) return } - for _, obj := range resp.Contents { - if _, err := s3Client.DeleteObject(&s3.DeleteObjectInput{Bucket: &bucketName, Key: obj.Key}); err != nil { + + for _, obj := range resp.Versions { + input := &s3.DeleteObjectInput{ + Bucket: &bucketName, + Key: obj.Key, + VersionId: obj.VersionId, + } + if _, err := s3Client.DeleteObject(ctx, input, s3WithRegion(region)); err != nil { + // this will need cleanup no matter what, so just warn and exit + t.Logf(warning, err) + return + } + } + for _, obj := range resp.DeleteMarkers { + input := &s3.DeleteObjectInput{ + Bucket: &bucketName, + Key: obj.Key, + VersionId: obj.VersionId, + } + if _, err := s3Client.DeleteObject(ctx, input, s3WithRegion(region)); err != nil { // this will need cleanup no matter what, so just warn and exit t.Logf(warning, err) return } } - if _, err := s3Client.DeleteBucket(&s3.DeleteBucketInput{Bucket: &bucketName}); err != nil { + if _, err := s3Client.DeleteBucket(ctx, &s3.DeleteBucketInput{Bucket: &bucketName}, s3WithRegion(region)); err != nil { t.Logf(warning, err) } } +func s3WithRegion(region string) func(o *s3.Options) { + return func(o *s3.Options) { + o.Region = region + } +} + // create the dynamoDB table, and wait until we can query it. -func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) { +func createDynamoDBTable(ctx context.Context, t *testing.T, dynClient *dynamodb.Client, tableName string) { createInput := &dynamodb.CreateTableInput{ - AttributeDefinitions: []*dynamodb.AttributeDefinition{ + AttributeDefinitions: []dynamodbtypes.AttributeDefinition{ { AttributeName: aws.String("LockID"), - AttributeType: aws.String("S"), + AttributeType: dynamodbtypes.ScalarAttributeTypeS, }, }, - KeySchema: []*dynamodb.KeySchemaElement{ + KeySchema: []dynamodbtypes.KeySchemaElement{ { AttributeName: aws.String("LockID"), - KeyType: aws.String("HASH"), + KeyType: dynamodbtypes.KeyTypeHash, }, }, - ProvisionedThroughput: &dynamodb.ProvisionedThroughput{ + ProvisionedThroughput: &dynamodbtypes.ProvisionedThroughput{ ReadCapacityUnits: aws.Int64(5), WriteCapacityUnits: aws.Int64(5), }, TableName: aws.String(tableName), } - _, err := dynClient.CreateTable(createInput) + t.Logf("creating DynamoDB table %s", tableName) + _, err := dynClient.CreateTable(ctx, createInput) if err != nil { t.Fatal(err) } @@ -753,12 +3650,12 @@ func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName s } for { - resp, err := dynClient.DescribeTable(describeInput) + resp, err := dynClient.DescribeTable(ctx, describeInput) if err != nil { t.Fatal(err) } - if *resp.Table.TableStatus == "ACTIVE" { + if resp.Table.TableStatus == dynamodbtypes.TableStatusActive { return } @@ -771,12 +3668,287 @@ func createDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName s } -func deleteDynamoDBTable(t *testing.T, dynClient *dynamodb.DynamoDB, tableName string) { +func deleteDynamoDBTable(ctx context.Context, t *testing.T, dynClient *dynamodb.Client, tableName string) { params := &dynamodb.DeleteTableInput{ TableName: aws.String(tableName), } - _, err := dynClient.DeleteTable(params) + _, err := dynClient.DeleteTable(ctx, params) if err != nil { t.Logf("WARNING: Failed to delete the test DynamoDB table %q. It has been left in your AWS account and may incur charges. (error was %s)", tableName, err) } } + +func populateSchema(t *testing.T, schema *configschema.Block, value cty.Value) cty.Value { + ty := schema.ImpliedType() + var path cty.Path + val, err := unmarshal(value, ty, path) + if err != nil { + t.Fatalf("populating schema: %s", err) + } + return val +} + +func unmarshal(value cty.Value, ty cty.Type, path cty.Path) (cty.Value, error) { + switch { + case ty.IsPrimitiveType(): + return value, nil + // case ty.IsListType(): + // return unmarshalList(value, ty.ElementType(), path) + case ty.IsSetType(): + return unmarshalSet(value, ty.ElementType(), path) + case ty.IsMapType(): + return unmarshalMap(value, ty.ElementType(), path) + // case ty.IsTupleType(): + // return unmarshalTuple(value, ty.TupleElementTypes(), path) + case ty.IsObjectType(): + return unmarshalObject(value, ty.AttributeTypes(), path) + default: + return cty.NilVal, path.NewErrorf("unsupported type %s", ty.FriendlyName()) + } +} + +func unmarshalSet(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) { + if dec.IsNull() { + return dec, nil + } + + length := dec.LengthInt() + + if length == 0 { + return cty.SetValEmpty(ety), nil + } + + vals := make([]cty.Value, 0, length) + dec.ForEachElement(func(key, val cty.Value) (stop bool) { + vals = append(vals, val) + return + }) + + return cty.SetVal(vals), nil +} + +// func unmarshalList(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) { +// if dec.IsNull() { +// return dec, nil +// } + +// length := dec.LengthInt() + +// if length == 0 { +// return cty.ListValEmpty(ety), nil +// } + +// vals := make([]cty.Value, 0, length) +// dec.ForEachElement(func(key, val cty.Value) (stop bool) { +// vals = append(vals, must(unmarshal(val, ety, path.Index(key)))) +// return +// }) + +// return cty.ListVal(vals), nil +// } + +func unmarshalMap(dec cty.Value, ety cty.Type, path cty.Path) (cty.Value, error) { + if dec.IsNull() { + return dec, nil + } + + length := dec.LengthInt() + + if length == 0 { + return cty.MapValEmpty(ety), nil + } + + vals := make(map[string]cty.Value, length) + dec.ForEachElement(func(key, val cty.Value) (stop bool) { + k := stringValue(key) + vals[k] = val + return + }) + + return cty.MapVal(vals), nil +} + +func unmarshalObject(dec cty.Value, atys map[string]cty.Type, path cty.Path) (cty.Value, error) { + if dec.IsNull() { + return dec, nil + } + valueTy := dec.Type() + + vals := make(map[string]cty.Value, len(atys)) + path = append(path, nil) + for key, aty := range atys { + path[len(path)-1] = cty.IndexStep{ + Key: cty.StringVal(key), + } + + if !valueTy.HasAttribute(key) { + vals[key] = cty.NullVal(aty) + } else { + val, err := unmarshal(dec.GetAttr(key), aty, path) + if err != nil { + return cty.DynamicVal, err + } + vals[key] = val + } + } + + return cty.ObjectVal(vals), nil +} + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } else { + return v + } +} + +// testBackendConfigDiags is an equivalent to `backend.TestBackendConfig` which returns the diags to the caller +// instead of failing the test +func testBackendConfigDiags(t *testing.T, b backend.Backend, c hcl.Body) (backend.Backend, tfdiags.Diagnostics) { + t.Helper() + + t.Logf("TestBackendConfig on %T with %#v", b, c) + + var diags tfdiags.Diagnostics + + // To make things easier for test authors, we'll allow a nil body here + // (even though that's not normally valid) and just treat it as an empty + // body. + if c == nil { + c = hcl.EmptyBody() + } + + schema := b.ConfigSchema() + spec := schema.DecoderSpec() + obj, decDiags := hcldec.Decode(c, spec, nil) + diags = diags.Append(decDiags) + + newObj, valDiags := b.PrepareConfig(obj) + diags = diags.Append(valDiags.InConfigBody(c, "")) + + // it's valid for a Backend to have warnings (e.g. a Deprecation) as such we should only raise on errors + if diags.HasErrors() { + return b, diags + } + + obj = newObj + + confDiags := b.Configure(obj) + + return b, diags.Append(confDiags) +} + +func addRetrieveEndpointURLMiddleware(t *testing.T, endpoint *string) func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + retrieveEndpointURLMiddleware(t, endpoint), + middleware.After, + ) + } +} + +func retrieveEndpointURLMiddleware(t *testing.T, endpoint *string) middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Retrieve Endpoint", + func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + t.Helper() + + request, ok := in.Request.(*smithyhttp.Request) + if !ok { + t.Fatalf("Expected *github.com/aws/smithy-go/transport/http.Request, got %s", fullTypeName(in.Request)) + } + + url := request.URL + url.RawQuery = "" + + *endpoint = url.String() + + return next.HandleFinalize(ctx, in) + }) +} + +var errCancelOperation = fmt.Errorf("Test: Cancelling request") + +func addCancelRequestMiddleware() func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + cancelRequestMiddleware(), + middleware.After, + ) + } +} + +// cancelRequestMiddleware creates a Smithy middleware that intercepts the request before sending and cancels it +func cancelRequestMiddleware() middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Cancel Requests", + func(_ context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + return middleware.FinalizeOutput{}, middleware.Metadata{}, errCancelOperation + }) +} + +func fullTypeName(i interface{}) string { + return fullValueTypeName(reflect.ValueOf(i)) +} + +func fullValueTypeName(v reflect.Value) string { + if v.Kind() == reflect.Ptr { + return "*" + fullValueTypeName(reflect.Indirect(v)) + } + + requestType := v.Type() + return fmt.Sprintf("%s.%s", requestType.PkgPath(), requestType.Name()) +} + +func defaultEndpointDynamo(region string) string { + r := dynamodb.NewDefaultEndpointResolverV2() + + ep, err := r.ResolveEndpoint(context.TODO(), dynamodb.EndpointParameters{ + Region: aws.String(region), + }) + if err != nil { + return err.Error() + } + + if ep.URI.Path == "" { + ep.URI.Path = "/" + } + + return ep.URI.String() +} + +func defaultEndpointS3(region string) string { + r := s3.NewDefaultEndpointResolverV2() + + ep, err := r.ResolveEndpoint(context.TODO(), s3.EndpointParameters{ + Region: aws.String(region), + }) + if err != nil { + return err.Error() + } + + if ep.URI.Path == "" { + ep.URI.Path = "/" + } + + return ep.URI.String() +} + +// objectLockPreCheck gates tests using object lock enabled buckets +// by checking for a configured environment variable. +// +// With object lock enabled, the statefile object written to the bucket +// cannot be deleted by the deleteS3Bucket test helper, resulting in an +// orphaned bucket after acceptance tests complete. Deletion of this +// leftover resource must be completed out of band by waiting until the +// default "Compliance" retention period of the objects has expired +// (1 day), emptying the bucket, and deleting it. +// +// Because clean up requires additional action outside the scope of the +// acceptance test, tests including this check are skipped by default. +func objectLockPreCheck(t *testing.T) { + if os.Getenv("TF_S3_OBJECT_LOCK_TEST") == "" { + t.Skip("s3 backend tests using object lock enabled buckets require setting TF_S3_OBJECT_LOCK_TEST") + } +} diff --git a/internal/backend/remote-state/s3/client.go b/internal/backend/remote-state/s3/client.go index 75e89a616a..85ad72bb32 100644 --- a/internal/backend/remote-state/s3/client.go +++ b/internal/backend/remote-state/s3/client.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package s3 import ( "bytes" + "context" "crypto/md5" "encoding/base64" "encoding/hex" @@ -12,26 +16,31 @@ import ( "log" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/aws/aws-sdk-go/service/dynamodb" - "github.com/aws/aws-sdk-go/service/s3" - multierror "github.com/hashicorp/go-multierror" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/dynamodb" + dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + baselogging "github.com/hashicorp/aws-sdk-go-base/v2/logging" + "github.com/hashicorp/go-hclog" uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statemgr" ) -// Store the last saved serial in dynamo with this suffix for consistency checks. const ( - s3EncryptionAlgorithm = "AES256" - stateIDSuffix = "-md5" - s3ErrCodeInternalError = "InternalError" + // s3EncryptionAlgorithm = s3types.ServerSideEncryptionAes256 + s3EncryptionAlgorithm = "AES256" + + // Store the last saved serial in dynamo with this suffix for consistency checks. + stateIDSuffix = "-md5" ) type RemoteClient struct { - s3Client *s3.S3 - dynClient *dynamodb.DynamoDB + s3Client *s3.Client + dynClient *dynamodb.Client bucketName string path string serverSideEncryption bool @@ -39,6 +48,9 @@ type RemoteClient struct { acl string kmsKeyID string ddbTable string + skipS3Checksum bool + lockFilePath string + useLockFile bool } var ( @@ -54,12 +66,20 @@ var ( var testChecksumHook func() func (c *RemoteClient) Get() (payload *remote.Payload, err error) { + ctx := context.TODO() + log := c.logger(operationClientGet) + + ctx, baselog := baselogging.NewHcLogger(ctx, log) + ctx = baselogging.RegisterLogger(ctx, baselog) + + log.Info("Downloading remote state") + deadline := time.Now().Add(consistencyRetryTimeout) // If we have a checksum, and the returned payload doesn't match, we retry // up until deadline. for { - payload, err = c.get() + payload, err = c.get(ctx) if err != nil { return nil, err } @@ -73,10 +93,15 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) { } // verify that this state is what we expect - if expected, err := c.getMD5(); err != nil { - log.Printf("[WARN] failed to fetch state md5: %s", err) + if expected, err := c.getMD5(ctx); err != nil { + log.Warn("failed to fetch state MD5", + "error", err, + ) } else if len(expected) > 0 && !bytes.Equal(expected, digest) { - log.Printf("[WARN] state md5 mismatch: expected '%x', got '%x'", expected, digest) + log.Warn("state MD5 mismatch", + "expected", expected, + "actual", digest, + ) if testChecksumHook != nil { testChecksumHook() @@ -84,11 +109,11 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) { if time.Now().Before(deadline) { time.Sleep(consistencyRetryPollInterval) - log.Println("[INFO] retrying S3 RemoteClient.Get...") + log.Info("retrying S3 RemoteClient.Get") continue } - return nil, fmt.Errorf(errBadChecksumFmt, digest) + return nil, newBadChecksumError(c.bucketName, c.path, digest, expected) } break @@ -97,45 +122,58 @@ func (c *RemoteClient) Get() (payload *remote.Payload, err error) { return payload, err } -func (c *RemoteClient) get() (*remote.Payload, error) { - var output *s3.GetObjectOutput - var err error - - input := &s3.GetObjectInput{ - Bucket: &c.bucketName, - Key: &c.path, +func (c *RemoteClient) get(ctx context.Context) (*remote.Payload, error) { + headInput := &s3.HeadObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.path), } - if c.serverSideEncryption && c.customerEncryptionKey != nil { - input.SetSSECustomerKey(string(c.customerEncryptionKey)) - input.SetSSECustomerAlgorithm(s3EncryptionAlgorithm) - input.SetSSECustomerKeyMD5(c.getSSECustomerKeyMD5()) + headInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + headInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm) + headInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) } - output, err = c.s3Client.GetObject(input) - + headOut, err := c.s3Client.HeadObject(ctx, headInput) if err != nil { - if awserr, ok := err.(awserr.Error); ok { - switch awserr.Code() { - case s3.ErrCodeNoSuchBucket: - return nil, fmt.Errorf(errS3NoSuchBucket, err) - case s3.ErrCodeNoSuchKey: - return nil, nil - } + switch { + case IsA[*s3types.NoSuchBucket](err): + return nil, fmt.Errorf(errS3NoSuchBucket, c.bucketName, err) + case IsA[*s3types.NotFound](err): + return nil, nil } - return nil, err + return nil, fmt.Errorf("Unable to access object %q in S3 bucket %q: %w", c.path, c.bucketName, err) } - defer output.Body.Close() + // Pre-allocate the full buffer to avoid re-allocations and GC + buf := make([]byte, int(aws.ToInt64(headOut.ContentLength))) + w := manager.NewWriteAtBuffer(buf) - buf := bytes.NewBuffer(nil) - if _, err := io.Copy(buf, output.Body); err != nil { - return nil, fmt.Errorf("Failed to read remote state: %s", err) + downloadInput := &s3.GetObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.path), + } + if c.serverSideEncryption && c.customerEncryptionKey != nil { + downloadInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + downloadInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm) + downloadInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) } - sum := md5.Sum(buf.Bytes()) + downloader := manager.NewDownloader(c.s3Client) + + _, err = downloader.Download(ctx, w, downloadInput) + if err != nil { + switch { + case IsA[*s3types.NoSuchBucket](err): + return nil, fmt.Errorf(errS3NoSuchBucket, c.bucketName, err) + case IsA[*s3types.NoSuchKey](err): + return nil, nil + } + return nil, fmt.Errorf("Unable to access object %q in S3 bucket %q: %w", c.path, c.bucketName, err) + } + + sum := md5.Sum(w.Bytes()) payload := &remote.Payload{ - Data: buf.Bytes(), + Data: w.Bytes(), MD5: sum[:], } @@ -148,71 +186,100 @@ func (c *RemoteClient) get() (*remote.Payload, error) { } func (c *RemoteClient) Put(data []byte) error { - contentType := "application/json" - contentLength := int64(len(data)) + return c.put(data) +} - i := &s3.PutObjectInput{ - ContentType: &contentType, - ContentLength: &contentLength, - Body: bytes.NewReader(data), - Bucket: &c.bucketName, - Key: &c.path, +func (c *RemoteClient) put(data []byte, optFns ...func(*s3.Options)) error { + ctx := context.TODO() + log := c.logger(operationClientPut) + + ctx, baselog := baselogging.NewHcLogger(ctx, log) + ctx = baselogging.RegisterLogger(ctx, baselog) + + contentType := "application/json" + + sum := md5.Sum(data) + + input := &s3.PutObjectInput{ + ContentType: aws.String(contentType), + Body: bytes.NewReader(data), + Bucket: aws.String(c.bucketName), + Key: aws.String(c.path), + } + if !c.skipS3Checksum { + input.ChecksumAlgorithm = s3types.ChecksumAlgorithmSha256 } if c.serverSideEncryption { if c.kmsKeyID != "" { - i.SSEKMSKeyId = &c.kmsKeyID - i.ServerSideEncryption = aws.String("aws:kms") + input.SSEKMSKeyId = aws.String(c.kmsKeyID) + input.ServerSideEncryption = s3types.ServerSideEncryptionAwsKms } else if c.customerEncryptionKey != nil { - i.SetSSECustomerKey(string(c.customerEncryptionKey)) - i.SetSSECustomerAlgorithm(s3EncryptionAlgorithm) - i.SetSSECustomerKeyMD5(c.getSSECustomerKeyMD5()) + input.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + input.SSECustomerAlgorithm = aws.String(string(s3EncryptionAlgorithm)) + input.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) } else { - i.ServerSideEncryption = aws.String(s3EncryptionAlgorithm) + input.ServerSideEncryption = s3EncryptionAlgorithm } } if c.acl != "" { - i.ACL = aws.String(c.acl) + input.ACL = s3types.ObjectCannedACL(c.acl) } - log.Printf("[DEBUG] Uploading remote state to S3: %#v", i) + log.Info("Uploading remote state") - _, err := c.s3Client.PutObject(i) + uploader := manager.NewUploader(c.s3Client, func(u *manager.Uploader) { + u.ClientOptions = optFns + }) + _, err := uploader.Upload(ctx, input) if err != nil { - return fmt.Errorf("failed to upload state: %s", err) + return fmt.Errorf("failed to upload state: %w", err) } - sum := md5.Sum(data) - if err := c.putMD5(sum[:]); err != nil { + if err := c.putMD5(ctx, sum[:]); err != nil { // if this errors out, we unfortunately have to error out altogether, // since the next Get will inevitably fail. - return fmt.Errorf("failed to store state MD5: %s", err) - + return fmt.Errorf("failed to store state MD5: %w", err) } return nil } func (c *RemoteClient) Delete() error { - _, err := c.s3Client.DeleteObject(&s3.DeleteObjectInput{ - Bucket: &c.bucketName, - Key: &c.path, + ctx := context.TODO() + log := c.logger(operationClientDelete) + + ctx, baselog := baselogging.NewHcLogger(ctx, log) + ctx = baselogging.RegisterLogger(ctx, baselog) + + log.Info("Deleting remote state") + + _, err := c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.path), }) if err != nil { return err } - if err := c.deleteMD5(); err != nil { - log.Printf("error deleting state md5: %s", err) + if err := c.deleteMD5(ctx); err != nil { + log.Error("deleting state MD5", + "error", err, + ) } return nil } +// Lock attempts to obtain a lock, returning the lock ID if successful. func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { - if c.ddbTable == "" { + ctx := context.TODO() + log := c.logger(operationLockerLock) + + // no file, no dynamodb + if !c.useLockFile && c.ddbTable == "" { return "", nil } @@ -227,54 +294,322 @@ func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { info.ID = lockID } + log = logWithLockInfo(log, info) + ctx, baselog := baselogging.NewHcLogger(ctx, log) + ctx = baselogging.RegisterLogger(ctx, baselog) + + // file only, no dynamodb + if c.useLockFile && c.ddbTable == "" { + log.Info("Attempting to lock remote state (S3 Native only)...") + if err := c.lockWithFile(ctx, info, log); err != nil { + return "", err + } + + log.Info("Locked remote state (S3 Native only)") + return info.ID, nil + } + + // dynamodb only, no file + if !c.useLockFile && c.ddbTable != "" { + log.Info("Attempting to lock remote state (DynamoDB only)...") + if err := c.lockWithDynamoDB(ctx, info); err != nil { + return "", err + } + + log.Info("Locked remote state (DynamoDB only)") + return info.ID, nil + } + + // double locking: dynamodb + file (design decision: both must succeed) + log.Info("Attempting to lock remote state (S3 Native and DynamoDB)...") + if err := c.lockWithFile(ctx, info, log); err != nil { + return "", err + } + + if err := c.lockWithDynamoDB(ctx, info); err != nil { + // Release the file lock if attempting to acquire the DynamoDB lock fails. + if unlockErr := c.unlockWithFile(ctx, info.ID, &statemgr.LockError{}, log); unlockErr != nil { + return "", fmt.Errorf("failed to clean up file lock after DynamoDB lock error: %v; original error: %w", unlockErr, err) + } + + return "", err + } + + log.Info("Locked remote state (S3 Native and DynamoDB)") + return info.ID, nil +} + +// lockWithFile attempts to acquire a lock on the remote state by uploading a lock file to Amazon S3. +// +// This method is used when the S3 native locking mechanism is in use. It uploads a lock file (JSON) +// to an S3 bucket to establish a lock on the state file. If the lock file does not already +// exist, the operation will succeed, acquiring the lock. If the lock file already exists, the operation +// will fail due to a conditional write, indicating that the lock is already held by another Terraform client. +func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo, log hclog.Logger) error { + lockFileJson, err := json.Marshal(info) + if err != nil { + return err + } + + input := &s3.PutObjectInput{ + ContentType: aws.String("application/json"), + Body: bytes.NewReader(lockFileJson), + Bucket: aws.String(c.bucketName), + Key: aws.String(c.lockFilePath), + IfNoneMatch: aws.String("*"), + } + if !c.skipS3Checksum { + input.ChecksumAlgorithm = s3types.ChecksumAlgorithmSha256 + } + + if c.serverSideEncryption { + if c.kmsKeyID != "" { + input.SSEKMSKeyId = aws.String(c.kmsKeyID) + input.ServerSideEncryption = s3types.ServerSideEncryptionAwsKms + } else if c.customerEncryptionKey != nil { + input.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + input.SSECustomerAlgorithm = aws.String(string(s3EncryptionAlgorithm)) + input.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) + } else { + input.ServerSideEncryption = s3EncryptionAlgorithm + } + } + + if c.acl != "" { + input.ACL = s3types.ObjectCannedACL(c.acl) + } + + log.Debug("Uploading lock file") + + uploader := manager.NewUploader(c.s3Client) + _, err = uploader.Upload(ctx, input) + if err != nil { + // Attempt to retrieve lock info from the file, and merge errors if it fails. + lockInfo, infoErr := c.getLockInfoWithFile(ctx) + if infoErr != nil { + err = errors.Join(err, infoErr) + } + + return &statemgr.LockError{ + Err: err, + Info: lockInfo, + } + } + + return nil +} + +func (c *RemoteClient) lockWithDynamoDB(ctx context.Context, info *statemgr.LockInfo) error { putParams := &dynamodb.PutItemInput{ - Item: map[string]*dynamodb.AttributeValue{ - "LockID": {S: aws.String(c.lockPath())}, - "Info": {S: aws.String(string(info.Marshal()))}, + Item: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: c.lockPath(), + }, + "Info": &dynamodbtypes.AttributeValueMemberS{ + Value: string(info.Marshal()), + }, }, TableName: aws.String(c.ddbTable), ConditionExpression: aws.String("attribute_not_exists(LockID)"), } - _, err := c.dynClient.PutItem(putParams) + + _, err := c.dynClient.PutItem(ctx, putParams) if err != nil { - lockInfo, infoErr := c.getLockInfo() + lockInfo, infoErr := c.getLockInfoWithDynamoDB(ctx) if infoErr != nil { - err = multierror.Append(err, infoErr) + err = errors.Join(err, infoErr) } lockErr := &statemgr.LockError{ Err: err, Info: lockInfo, } - return "", lockErr + return lockErr } - return info.ID, nil + return nil } -func (c *RemoteClient) getMD5() ([]byte, error) { +// Unlock releases a lock previously acquired by Lock. +func (c *RemoteClient) Unlock(id string) error { + ctx := context.TODO() + log := c.logger(operationLockerUnlock) + + // no file, no dynamodb + if !c.useLockFile && c.ddbTable == "" { + return nil + } + + log = logWithLockID(log, id) + ctx, baselog := baselogging.NewHcLogger(ctx, log) + ctx = baselogging.RegisterLogger(ctx, baselog) + + lockErr := &statemgr.LockError{} + + // file only, no dynamodb + if c.useLockFile && c.ddbTable == "" { + log.Info("Attempting to unlock remote state (S3 Native only)...") + if err := c.unlockWithFile(ctx, id, lockErr, log); err != nil { + lockErr.Err = err + return lockErr + } + + log.Info("Unlocked remote state (S3 Native only)") + return nil + } + + // dynamodb only, no file + if !c.useLockFile && c.ddbTable != "" { + log.Info("Attempting to unlock remote state (DynamoDB only)...") + if err := c.unlockWithDynamoDB(ctx, id, lockErr); err != nil { + lockErr.Err = err + return lockErr + } + + log.Info("Unlocked remote state (DynamoDB only)") + return nil + } + + // Double unlocking: DynamoDB + file + log.Info("Attempting to unlock remote state (S3 Native and DynamoDB)...") + + ferr := c.unlockWithFile(ctx, id, lockErr, log) + derr := c.unlockWithDynamoDB(ctx, id, lockErr) + + if ferr != nil && derr != nil { + lockErr.Err = fmt.Errorf("failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v", ferr, derr) + return lockErr + } + + if ferr != nil { + lockErr.Err = fmt.Errorf("failed to unlock S3: %v", ferr) + return lockErr + } + + if derr != nil { + lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v", derr) + return lockErr + } + + log.Info("Unlocked remote state (S3 Native and DynamoDB)") + return nil +} + +// unlockWithFile attempts to unlock the remote state by deleting the lock file from Amazon S3. +// +// This method is used when the S3 native locking mechanism is in use, which uses a `.tflock` file +// to manage state locking. The function deletes the lock file to release the lock, allowing other +// Terraform clients to acquire the lock on the same state file. +func (c *RemoteClient) unlockWithFile(ctx context.Context, id string, lockErr *statemgr.LockError, log hclog.Logger) error { + getInput := &s3.GetObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.lockFilePath), + } + + if c.serverSideEncryption && c.customerEncryptionKey != nil { + getInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + getInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm) + getInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) + } + + getOutput, err := c.s3Client.GetObject(ctx, getInput) + if err != nil { + return fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err) + } + defer func() { + if cerr := getOutput.Body.Close(); cerr != nil { + log.Warn(fmt.Sprintf("failed to close S3 object body: %v", cerr)) + } + }() + + data, err := io.ReadAll(getOutput.Body) + if err != nil { + return fmt.Errorf("failed to read the body of the S3 object: %w", err) + } + + lockInfo := &statemgr.LockInfo{} + if err := json.Unmarshal(data, lockInfo); err != nil { + return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err) + } + lockErr.Info = lockInfo + + // Verify that the provided lock ID matches the lock ID of the retrieved lock file. + if lockInfo.ID != id { + return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID) + } + + // Delete the lock file to release the lock. + _, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.lockFilePath), + }) + + if err != nil { + return fmt.Errorf("failed to delete the lock file: %w", err) + } + + log.Debug(fmt.Sprintf("Deleted lock file: '%q'", c.lockFilePath)) + + return nil +} + +func (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error { + // TODO: store the path and lock ID in separate fields, and have proper + // projection expression only delete the lock if both match, rather than + // checking the ID from the info field first. + lockInfo, err := c.getLockInfoWithDynamoDB(ctx) + if err != nil { + return fmt.Errorf("failed to retrieve lock info for lock ID %q: %s", id, err) + } + lockErr.Info = lockInfo + + if lockInfo.ID != id { + return fmt.Errorf("lock ID %q does not match existing lock (%q)", id, lockInfo.ID) + } + + params := &dynamodb.DeleteItemInput{ + Key: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: c.lockPath(), + }, + }, + TableName: aws.String(c.ddbTable), + } + _, err = c.dynClient.DeleteItem(ctx, params) + + if err != nil { + return err + } + return nil +} + +func (c *RemoteClient) getMD5(ctx context.Context) ([]byte, error) { if c.ddbTable == "" { return nil, nil } getParams := &dynamodb.GetItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "LockID": {S: aws.String(c.lockPath() + stateIDSuffix)}, + Key: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: c.lockPath() + stateIDSuffix, + }, }, ProjectionExpression: aws.String("LockID, Digest"), TableName: aws.String(c.ddbTable), ConsistentRead: aws.Bool(true), } - resp, err := c.dynClient.GetItem(getParams) + resp, err := c.dynClient.GetItem(ctx, getParams) if err != nil { - return nil, err + return nil, fmt.Errorf("Unable to retrieve item from DynamoDB table %q: %w", c.ddbTable, err) } var val string - if v, ok := resp.Item["Digest"]; ok && v.S != nil { - val = *v.S + if v, ok := resp.Item["Digest"]; ok { + if v, ok := v.(*dynamodbtypes.AttributeValueMemberS); ok { + val = v.Value + } } sum, err := hex.DecodeString(val) @@ -286,7 +621,7 @@ func (c *RemoteClient) getMD5() ([]byte, error) { } // store the hash of the state so that clients can check for stale state files. -func (c *RemoteClient) putMD5(sum []byte) error { +func (c *RemoteClient) putMD5(ctx context.Context, sum []byte) error { if c.ddbTable == "" { return nil } @@ -296,13 +631,17 @@ func (c *RemoteClient) putMD5(sum []byte) error { } putParams := &dynamodb.PutItemInput{ - Item: map[string]*dynamodb.AttributeValue{ - "LockID": {S: aws.String(c.lockPath() + stateIDSuffix)}, - "Digest": {S: aws.String(hex.EncodeToString(sum))}, + Item: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: c.lockPath() + stateIDSuffix, + }, + "Digest": &dynamodbtypes.AttributeValueMemberS{ + Value: hex.EncodeToString(sum), + }, }, TableName: aws.String(c.ddbTable), } - _, err := c.dynClient.PutItem(putParams) + _, err := c.dynClient.PutItem(ctx, putParams) if err != nil { log.Printf("[WARN] failed to record state serial in dynamodb: %s", err) } @@ -311,41 +650,76 @@ func (c *RemoteClient) putMD5(sum []byte) error { } // remove the hash value for a deleted state -func (c *RemoteClient) deleteMD5() error { +func (c *RemoteClient) deleteMD5(ctx context.Context) error { if c.ddbTable == "" { return nil } params := &dynamodb.DeleteItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "LockID": {S: aws.String(c.lockPath() + stateIDSuffix)}, + Key: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: c.lockPath() + stateIDSuffix, + }, }, TableName: aws.String(c.ddbTable), } - if _, err := c.dynClient.DeleteItem(params); err != nil { - return err + if _, err := c.dynClient.DeleteItem(ctx, params); err != nil { + return fmt.Errorf("Unable to delete item from DynamoDB table %q: %w", c.ddbTable, err) } return nil } -func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { +// getLockInfoWithFile retrieves and parses a lock file from an S3 bucket. +func (c *RemoteClient) getLockInfoWithFile(ctx context.Context) (*statemgr.LockInfo, error) { + // Attempt to retrieve the lock file from S3. + getOutput, err := c.s3Client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.lockFilePath), + }) + if err != nil { + return nil, fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err) + } + defer func() { + if cerr := getOutput.Body.Close(); cerr != nil { + log.Printf("failed to close S3 object body: %v", cerr) + } + }() + + data, err := io.ReadAll(getOutput.Body) + if err != nil { + return nil, fmt.Errorf("failed to read the body of the S3 object: %w", err) + } + + lockInfo := &statemgr.LockInfo{} + if err := json.Unmarshal(data, lockInfo); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err) + } + + return lockInfo, nil +} + +func (c *RemoteClient) getLockInfoWithDynamoDB(ctx context.Context) (*statemgr.LockInfo, error) { getParams := &dynamodb.GetItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "LockID": {S: aws.String(c.lockPath())}, + Key: map[string]dynamodbtypes.AttributeValue{ + "LockID": &dynamodbtypes.AttributeValueMemberS{ + Value: c.lockPath(), + }, }, ProjectionExpression: aws.String("LockID, Info"), TableName: aws.String(c.ddbTable), ConsistentRead: aws.Bool(true), } - resp, err := c.dynClient.GetItem(getParams) + resp, err := c.dynClient.GetItem(ctx, getParams) if err != nil { - return nil, err + return nil, fmt.Errorf("Unable to retrieve item from DynamoDB table %q: %w", c.ddbTable, err) } var infoData string - if v, ok := resp.Item["Info"]; ok && v.S != nil { - infoData = *v.S + if v, ok := resp.Item["Info"]; ok { + if v, ok := v.(*dynamodbtypes.AttributeValueMemberS); ok { + infoData = v.Value + } } lockInfo := &statemgr.LockInfo{} @@ -357,43 +731,6 @@ func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) { return lockInfo, nil } -func (c *RemoteClient) Unlock(id string) error { - if c.ddbTable == "" { - return nil - } - - lockErr := &statemgr.LockError{} - - // TODO: store the path and lock ID in separate fields, and have proper - // projection expression only delete the lock if both match, rather than - // checking the ID from the info field first. - lockInfo, err := c.getLockInfo() - if err != nil { - lockErr.Err = fmt.Errorf("failed to retrieve lock info: %s", err) - return lockErr - } - lockErr.Info = lockInfo - - if lockInfo.ID != id { - lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id) - return lockErr - } - - params := &dynamodb.DeleteItemInput{ - Key: map[string]*dynamodb.AttributeValue{ - "LockID": {S: aws.String(c.lockPath())}, - }, - TableName: aws.String(c.ddbTable), - } - _, err = c.dynClient.DeleteItem(params) - - if err != nil { - lockErr.Err = err - return lockErr - } - return nil -} - func (c *RemoteClient) lockPath() string { return fmt.Sprintf("%s/%s", c.bucketName, c.path) } @@ -403,16 +740,65 @@ func (c *RemoteClient) getSSECustomerKeyMD5() string { return base64.StdEncoding.EncodeToString(b[:]) } -const errBadChecksumFmt = `state data in S3 does not have the expected content. +// logger returns the S3 backend logger configured with the client's bucket and path and the operation +func (c *RemoteClient) logger(operation string) hclog.Logger { + log := logger().With( + logKeyBucket, c.bucketName, + logKeyPath, c.path, + ) + return logWithOperation(log, operation) +} + +var _ error = badChecksumError{} + +type badChecksumError struct { + bucket, key string + digest, expected []byte +} + +func newBadChecksumError(bucket, key string, digest, expected []byte) badChecksumError { + return badChecksumError{ + bucket: bucket, + key: key, + digest: digest, + expected: expected, + } +} + +func (err badChecksumError) Error() string { + return fmt.Sprintf(`state data in S3 does not have the expected content. + +The checksum calculated for the state stored in S3 does not match the checksum +stored in DynamoDB. + +Bucket: %[1]s +Key: %[2]s +Calculated checksum: %[3]x +Stored checksum: %[4]x This may be caused by unusually long delays in S3 processing a previous state -update. Please wait for a minute or two and try again. If this problem -persists, and neither S3 nor DynamoDB are experiencing an outage, you may need -to manually verify the remote state and update the Digest value stored in the -DynamoDB table to the following value: %x -` +update. Please wait for a minute or two and try again. -const errS3NoSuchBucket = `S3 bucket does not exist. +%[5]s +`, err.bucket, err.key, err.digest, err.expected, err.resolutionMsg()) +} + +func (err badChecksumError) resolutionMsg() string { + if len(err.digest) > 0 { + return fmt.Sprintf( + `If this problem persists, and neither S3 nor DynamoDB are experiencing an +outage, you may need to manually verify the remote state and update the Digest +value stored in the DynamoDB table to the following value: %x`, + err.digest, + ) + } else { + return `If this problem persists, and neither S3 nor DynamoDB are experiencing an +outage, you may need to manually verify the remote state and remove the Digest +value stored in the DynamoDB table` + } +} + +const errS3NoSuchBucket = `S3 bucket %q does not exist. The referenced S3 bucket must have been previously created. If the S3 bucket was created within the last minute, please wait for a minute or two and try diff --git a/internal/backend/remote-state/s3/client_test.go b/internal/backend/remote-state/s3/client_test.go index abbd4257c1..69368c30a4 100644 --- a/internal/backend/remote-state/s3/client_test.go +++ b/internal/backend/remote-state/s3/client_test.go @@ -1,13 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package s3 import ( "bytes" + "context" "crypto/md5" + "errors" "fmt" - "strings" + "io" + "maps" "testing" "time" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statefile" @@ -19,8 +31,11 @@ func TestRemoteClient_impl(t *testing.T) { var _ remote.ClientLocker = new(RemoteClient) } -func TestRemoteClient(t *testing.T) { +func TestRemoteClientBasic(t *testing.T) { testACC(t) + + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" @@ -30,8 +45,8 @@ func TestRemoteClient(t *testing.T) { "encrypt": true, })).(*Backend) - createS3Bucket(t, b.s3Client, bucketName) - defer deleteS3Bucket(t, b.s3Client, bucketName) + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) state, err := b.StateMgr(backend.DefaultStateName) if err != nil { @@ -43,6 +58,9 @@ func TestRemoteClient(t *testing.T) { func TestRemoteClientLocks(t *testing.T) { testACC(t) + + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" @@ -60,10 +78,10 @@ func TestRemoteClientLocks(t *testing.T) { "dynamodb_table": bucketName, })).(*Backend) - createS3Bucket(t, b1.s3Client, bucketName) - defer deleteS3Bucket(t, b1.s3Client, bucketName) - createDynamoDBTable(t, b1.dynClient, bucketName) - defer deleteDynamoDBTable(t, b1.dynClient, bucketName) + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) s1, err := b1.StateMgr(backend.DefaultStateName) if err != nil { @@ -81,6 +99,9 @@ func TestRemoteClientLocks(t *testing.T) { // verify that we can unlock a state with an existing lock func TestForceUnlock(t *testing.T) { testACC(t) + + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-force-%x", time.Now().Unix()) keyName := "testState" @@ -98,10 +119,87 @@ func TestForceUnlock(t *testing.T) { "dynamodb_table": bucketName, })).(*Backend) - createS3Bucket(t, b1.s3Client, bucketName) - defer deleteS3Bucket(t, b1.s3Client, bucketName) - createDynamoDBTable(t, b1.dynClient, bucketName) - defer deleteDynamoDBTable(t, b1.dynClient, bucketName) + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) + + // first test with default + s1, err := b1.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + info := statemgr.NewLockInfo() + info.Operation = "test" + info.Who = "clientA" + + lockID, err := s1.Lock(info) + if err != nil { + t.Fatal("unable to get initial lock:", err) + } + + // s1 is now locked, get the same state through s2 and unlock it + s2, err := b2.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal("failed to get default state to force unlock:", err) + } + + if err := s2.Unlock(lockID); err != nil { + t.Fatal("failed to force-unlock default state") + } + + // now try the same thing with a named state + // first test with default + s1, err = b1.StateMgr("test") + if err != nil { + t.Fatal(err) + } + + info = statemgr.NewLockInfo() + info.Operation = "test" + info.Who = "clientA" + + lockID, err = s1.Lock(info) + if err != nil { + t.Fatal("unable to get initial lock:", err) + } + + // s1 is now locked, get the same state through s2 and unlock it + s2, err = b2.StateMgr("test") + if err != nil { + t.Fatal("failed to get named state to force unlock:", err) + } + + if err = s2.Unlock(lockID); err != nil { + t.Fatal("failed to force-unlock named state") + } +} + +func TestForceUnlock_withLockfile(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-force-%x", time.Now().Unix()) + keyName := "testState" + + b1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + })).(*Backend) + + b2 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "encrypt": true, + "use_lockfile": true, + })).(*Backend) + + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) // first test with default s1, err := b1.StateMgr(backend.DefaultStateName) @@ -158,6 +256,8 @@ func TestForceUnlock(t *testing.T) { func TestRemoteClient_clientMD5(t *testing.T) { testACC(t) + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" @@ -167,10 +267,10 @@ func TestRemoteClient_clientMD5(t *testing.T) { "dynamodb_table": bucketName, })).(*Backend) - createS3Bucket(t, b.s3Client, bucketName) - defer deleteS3Bucket(t, b.s3Client, bucketName) - createDynamoDBTable(t, b.dynClient, bucketName) - defer deleteDynamoDBTable(t, b.dynClient, bucketName) + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + createDynamoDBTable(ctx, t, b.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b.dynClient, bucketName) s, err := b.StateMgr(backend.DefaultStateName) if err != nil { @@ -180,11 +280,11 @@ func TestRemoteClient_clientMD5(t *testing.T) { sum := md5.Sum([]byte("test")) - if err := client.putMD5(sum[:]); err != nil { + if err := client.putMD5(ctx, sum[:]); err != nil { t.Fatal(err) } - getSum, err := client.getMD5() + getSum, err := client.getMD5(ctx) if err != nil { t.Fatal(err) } @@ -193,11 +293,11 @@ func TestRemoteClient_clientMD5(t *testing.T) { t.Fatalf("getMD5 returned the wrong checksum: expected %x, got %x", sum[:], getSum) } - if err := client.deleteMD5(); err != nil { + if err := client.deleteMD5(ctx); err != nil { t.Fatal(err) } - if getSum, err := client.getMD5(); err == nil { + if getSum, err := client.getMD5(ctx); err == nil { t.Fatalf("expected getMD5 error, got none. checksum: %x", getSum) } } @@ -206,6 +306,8 @@ func TestRemoteClient_clientMD5(t *testing.T) { func TestRemoteClient_stateChecksum(t *testing.T) { testACC(t) + ctx := context.TODO() + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) keyName := "testState" @@ -215,10 +317,10 @@ func TestRemoteClient_stateChecksum(t *testing.T) { "dynamodb_table": bucketName, })).(*Backend) - createS3Bucket(t, b1.s3Client, bucketName) - defer deleteS3Bucket(t, b1.s3Client, bucketName) - createDynamoDBTable(t, b1.dynClient, bucketName) - defer deleteDynamoDBTable(t, b1.dynClient, bucketName) + createS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b1.s3Client, bucketName, b1.awsConfig.Region) + createDynamoDBTable(ctx, t, b1.dynClient, bucketName) + defer deleteDynamoDBTable(ctx, t, b1.dynClient, bucketName) s1, err := b1.StateMgr(backend.DefaultStateName) if err != nil { @@ -283,8 +385,10 @@ func TestRemoteClient_stateChecksum(t *testing.T) { // fetching an empty state through client1 should now error out due to a // mismatched checksum. - if _, err := client1.Get(); !strings.HasPrefix(err.Error(), errBadChecksumFmt[:80]) { + if _, err := client1.Get(); !IsA[badChecksumError](err) { t.Fatalf("expected state checksum error: got %s", err) + } else if bse, ok := As[badChecksumError](err); ok && len(bse.digest) != 0 { + t.Fatalf("expected empty checksum, got %x", bse.digest) } // put the old state in place of the new, without updating the checksum @@ -294,7 +398,7 @@ func TestRemoteClient_stateChecksum(t *testing.T) { // fetching the wrong state through client1 should now error out due to a // mismatched checksum. - if _, err := client1.Get(); !strings.HasPrefix(err.Error(), errBadChecksumFmt[:80]) { + if _, err := client1.Get(); !IsA[badChecksumError](err) { t.Fatalf("expected state checksum error: got %s", err) } @@ -315,3 +419,234 @@ func TestRemoteClient_stateChecksum(t *testing.T) { t.Fatal(err) } } + +func TestRemoteClientPutLargeUploadWithObjectLock_Compliance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "testState" + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance), + ) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + s1, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + client := s1.(*remote.State).Client + + var state bytes.Buffer + dataW := io.LimitReader(neverEnding('x'), manager.DefaultUploadPartSize*2) + _, err = state.ReadFrom(dataW) + if err != nil { + t.Fatalf("writing dummy data: %s", err) + } + + err = client.Put(state.Bytes()) + if err != nil { + t.Fatalf("putting data: %s", err) + } +} + +func TestRemoteClientLockFileWithObjectLock_Compliance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "testState" + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "use_lockfile": true, + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeCompliance), + ) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + s1, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + client := s1.(*remote.State).Client + + var state bytes.Buffer + dataW := io.LimitReader(neverEnding('x'), manager.DefaultUploadPartSize) + _, err = state.ReadFrom(dataW) + if err != nil { + t.Fatalf("writing dummy data: %s", err) + } + + err = client.Put(state.Bytes()) + if err != nil { + t.Fatalf("putting data: %s", err) + } +} + +func TestRemoteClientLockFileWithObjectLock_Governance(t *testing.T) { + testACC(t) + objectLockPreCheck(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "testState" + + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + "use_lockfile": true, + })).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region, + s3BucketWithVersioning, + s3BucketWithObjectLock(s3types.ObjectLockRetentionModeGovernance), + ) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + s1, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + client := s1.(*remote.State).Client + + var state bytes.Buffer + dataW := io.LimitReader(neverEnding('x'), manager.DefaultUploadPartSize) + _, err = state.ReadFrom(dataW) + if err != nil { + t.Fatalf("writing dummy data: %s", err) + } + + err = client.Put(state.Bytes()) + if err != nil { + t.Fatalf("putting data: %s", err) + } +} + +type neverEnding byte + +func (b neverEnding) Read(p []byte) (n int, err error) { + for i := range p { + p[i] = byte(b) + } + return len(p), nil +} + +func TestRemoteClientSkipS3Checksum(t *testing.T) { + testACC(t) + + ctx := context.TODO() + + bucketName := fmt.Sprintf("terraform-remote-s3-test-%x", time.Now().Unix()) + keyName := "testState" + + testcases := map[string]struct { + config map[string]any + expected string + }{ + "default": { + config: map[string]any{}, + expected: string(s3types.ChecksumAlgorithmSha256), + }, + "true": { + config: map[string]any{ + "skip_s3_checksum": true, + }, + expected: "", + }, + "false": { + config: map[string]any{ + "skip_s3_checksum": false, + }, + expected: string(s3types.ChecksumAlgorithmSha256), + }, + } + + for name, testcase := range testcases { + t.Run(name, func(t *testing.T) { + config := map[string]interface{}{ + "bucket": bucketName, + "key": keyName, + } + maps.Copy(config, testcase.config) + b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) + + createS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + defer deleteS3Bucket(ctx, t, b.s3Client, bucketName, b.awsConfig.Region) + + state, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatal(err) + } + + c := state.(*remote.State).Client + client := c.(*RemoteClient) + + s := statemgr.TestFullInitialState() + sf := &statefile.File{State: s} + var stateBuf bytes.Buffer + if err := statefile.Write(sf, &stateBuf); err != nil { + t.Fatal(err) + } + + var checksum string + err = client.put(stateBuf.Bytes(), func(opts *s3.Options) { + opts.APIOptions = append(opts.APIOptions, + addRetrieveChecksumHeaderMiddleware(t, &checksum), + addCancelRequestMiddleware(), + ) + }) + if err == nil { + t.Fatal("Expected an error, got none") + } else if !errors.Is(err, errCancelOperation) { + t.Fatalf("Unexpected error: %s", err) + } + + if a, e := checksum, testcase.expected; a != e { + t.Fatalf("expected %q, got %q", e, a) + } + }) + } +} + +func addRetrieveChecksumHeaderMiddleware(t *testing.T, checksum *string) func(*middleware.Stack) error { + return func(stack *middleware.Stack) error { + return stack.Finalize.Add( + retrieveChecksumHeaderMiddleware(t, checksum), + middleware.After, + ) + } +} + +func retrieveChecksumHeaderMiddleware(t *testing.T, checksum *string) middleware.FinalizeMiddleware { + return middleware.FinalizeMiddlewareFunc( + "Test: Retrieve Stuff", + func(ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler) (middleware.FinalizeOutput, middleware.Metadata, error) { + t.Helper() + + request, ok := in.Request.(*smithyhttp.Request) + if !ok { + t.Fatalf("Expected *github.com/aws/smithy-go/transport/http.Request, got %s", fullTypeName(in.Request)) + } + + *checksum = request.Header.Get("x-amz-sdk-checksum-algorithm") + + return next.HandleFinalize(ctx, in) + }) +} diff --git a/internal/backend/remote-state/s3/diags.go b/internal/backend/remote-state/s3/diags.go new file mode 100644 index 0000000000..0b0d9939de --- /dev/null +++ b/internal/backend/remote-state/s3/diags.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "fmt" + "strings" + + basediag "github.com/hashicorp/aws-sdk-go-base/v2/diag" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func diagnosticString(diag tfdiags.Diagnostic) string { + var buffer strings.Builder + buffer.WriteString(diag.Severity().String() + ": ") + buffer.WriteString(diag.Description().Summary) + if diag.Description().Detail != "" { + buffer.WriteString("\n\n") + buffer.WriteString(diag.Description().Detail) + } + if path := tfdiags.GetAttribute(diag); len(path) != 0 { + fmt.Fprintf(&buffer, "\nPath: %#v", path) + } + return buffer.String() +} + +func diagnosticsString(diags tfdiags.Diagnostics) string { + l := len(diags) + if l == 0 { + return "" + } + + var buffer strings.Builder + for i, d := range diags { + buffer.WriteString(diagnosticString(d)) + if i < l-1 { + buffer.WriteString(",\n") + } + } + return buffer.String() +} + +func baseSeverityToTerraformSeverity(s basediag.Severity) tfdiags.Severity { + switch s { + case basediag.SeverityWarning: + return tfdiags.Warning + case basediag.SeverityError: + return tfdiags.Error + default: + var zero tfdiags.Severity + return zero + } +} diff --git a/internal/backend/remote-state/s3/errors.go b/internal/backend/remote-state/s3/errors.go new file mode 100644 index 0000000000..065e652c9a --- /dev/null +++ b/internal/backend/remote-state/s3/errors.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "errors" +) + +// IsA indicates whether an error matches an error type +func IsA[T error](err error) bool { + _, ok := As[T](err) + return ok +} + +// As is equivalent to errors.As(), but returns the value in-line +func As[T error](err error) (T, bool) { + var as T + ok := errors.As(err, &as) + return as, ok +} diff --git a/internal/backend/remote-state/s3/go.mod b/internal/backend/remote-state/s3/go.mod new file mode 100644 index 0000000000..efee706436 --- /dev/null +++ b/internal/backend/remote-state/s3/go.mod @@ -0,0 +1,97 @@ +module github.com/hashicorp/terraform/internal/backend/remote-state/s3 + +go 1.24.2 + +require ( + github.com/aws/aws-sdk-go-v2 v1.36.0 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.22 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.8 + github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2 + github.com/aws/smithy-go v1.22.2 + github.com/google/go-cmp v0.7.0 + github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/zclconf/go-cty v1.16.2 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.57 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 // indirect + github.com/aws/aws-sdk-go-v2/service/iam v1.38.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sns v1.33.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sqs v1.37.12 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect +) + +replace github.com/hashicorp/terraform/internal/backend/remote-state/azure => ../azure + +replace github.com/hashicorp/terraform/internal/backend/remote-state/consul => ../consul + +replace github.com/hashicorp/terraform/internal/backend/remote-state/cos => ../cos + +replace github.com/hashicorp/terraform/internal/backend/remote-state/gcs => ../gcs + +replace github.com/hashicorp/terraform/internal/backend/remote-state/kubernetes => ../kubernetes + +replace github.com/hashicorp/terraform/internal/backend/remote-state/oss => ../oss + +replace github.com/hashicorp/terraform/internal/backend/remote-state/pg => ../pg + +replace github.com/hashicorp/terraform/internal/backend/remote-state/s3 => ../s3 + +replace github.com/hashicorp/terraform/internal/legacy => ../../../legacy + +replace github.com/hashicorp/terraform/internal => ../../.. + +replace github.com/hashicorp/terraform => ../../../.. diff --git a/internal/backend/remote-state/s3/go.sum b/internal/backend/remote-state/s3/go.sum new file mode 100644 index 0000000000..c16619d5f7 --- /dev/null +++ b/internal/backend/remote-state/s3/go.sum @@ -0,0 +1,655 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo= +cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.36.0 h1:b1wM5CcE65Ujwn565qcwgtOTT1aT4ADOHHgglKjG7fk= +github.com/aws/aws-sdk-go-v2 v1.36.0/go.mod h1:5PMILGVKiW32oDzjj6RU52yrNrDPUHcbZQYr1sM7qmM= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8 h1:zAxi9p3wsZMIaVCdoiQp2uZ9k1LsZvmAnoTBeZPXom0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.8/go.mod h1:3XkePX5dSaxveLAYY7nsbsZZrKxCyEuE5pM4ziFxyGg= +github.com/aws/aws-sdk-go-v2/config v1.29.4 h1:ObNqKsDYFGr2WxnoXKOhCvTlf3HhwtoGgc+KmZ4H5yg= +github.com/aws/aws-sdk-go-v2/config v1.29.4/go.mod h1:j2/AF7j/qxVmsNIChw1tWfsVKOayJoGRDjg1Tgq7NPk= +github.com/aws/aws-sdk-go-v2/credentials v1.17.57 h1:kFQDsbdBAR3GZsB8xA+51ptEnq9TIj3tS4MuP5b+TcQ= +github.com/aws/aws-sdk-go-v2/credentials v1.17.57/go.mod h1:2kerxPUUbTagAr/kkaHiqvj/bcYHzi2qiJS/ZinllU0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27 h1:7lOW8NUwE9UZekS1DYoiPdVAqZ6A+LheHWb+mHbNOq8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.27/go.mod h1:w1BASFIPOPUae7AgaH4SbjNbfdkxuggLyGfNFTn8ITY= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.22 h1:MUD/42Etbj6sVZ0HpOe4G/4+wDF7ZJhqZXSqNKZokPM= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.22/go.mod h1:wp0iN4VH1riPNX68N8MU+mz/7ggSeWc+zBhsdALp+zM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31 h1:lWm9ucLSRFiI4dQQafLrEOmEDGry3Swrz0BIRdiHJqQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.31/go.mod h1:Huu6GG0YTfbPphQkDSo4dEGmQRTKb9k9G7RdtyQWxuI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31 h1:ACxDklUKKXb48+eg5ROZXi1vDgfMyfIA/WyvqHcHI0o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.31/go.mod h1:yadnfsDwqXeVaohbGc/RaD287PuyRw2wugkh5ZL2J6k= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2 h1:Pg9URiobXy85kgFev3og2CuOZ8JZUBENF+dcgWBaYNk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.2/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31 h1:8IwBjuLdqIO1dGB+dZ9zJEl8wzY3bVYxcs0Xyu/Lsc0= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.31/go.mod h1:8tMBcuVjL4kP/ECEIWTCWtwV2kj6+ouEKl4cqR4iWLw= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.8 h1:D4Dhqf6FEw//4mEFsxtBYMNSmdSg0LAy+A+DVvH0dts= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.39.8/go.mod h1:+pfCvXbSNLZ7lG+tydnY5IN4WUoz+WsGDrl2rg2DEew= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.10 h1:u/MwkFwRkKRDvy7D76/khJTk8HMp4mC5sZKErU53jos= +github.com/aws/aws-sdk-go-v2/service/iam v1.38.10/go.mod h1:Gid0WEVky3EWbkeXiS67kHhbiK+q3/wO/hvPh7plR0c= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2 h1:D4oz8/CzT9bAEYtVhSBmFj2dNOtaHOtMKc2vHBwYizA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.2/go.mod h1:Za3IHqTQ+yNcRHxu1OFucBh0ACZT4j4VQFF0BqpZcLY= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5 h1:siiQ+jummya9OLPDEyHVb2dLW4aOMe22FGDd0sAfuSw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.5.5/go.mod h1:iHVx2J9pWzITdP5MJY6qWfG34TfD9EA+Qi3eV6qQCXw= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.12 h1:V1h3Cxmn0tN5EhL31uvqSLKsMlPlqiYxRwAEdwNeIJ8= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.10.12/go.mod h1:KzXJPn2wqsZJlNSx70gmDkRDVTmyF/RRXxTP2yMxUwc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12 h1:O+8vD2rGjfihBewr5bT+QUfYUHIxCVgG61LHoT59shM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.12/go.mod h1:usVdWJaosa66NMvmCrr08NcWDBRv4E6+YFG2pUdw1Lk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12 h1:tkVNm99nkJnFo1H9IIQb5QkCiPcvCDn3Pos+IeTbGRA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.12/go.mod h1:dIVlquSPUMqEJtx2/W17SM2SuESRaVEhEV9alcMqxjw= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2 h1:dyC+iA2+Yc7iDMDh0R4eT6fi8TgBduc+BOWCy6Br0/o= +github.com/aws/aws-sdk-go-v2/service/s3 v1.75.2/go.mod h1:FHSHmyEUkzRbaFFqqm6bkLAOQHgqhsLmfCahvCBMiyA= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.12 h1:5LZIyHvSAu2DeC9X6P9c3ALFTSDu/oyJ5Cq0rLbe2mk= +github.com/aws/aws-sdk-go-v2/service/sns v1.33.12/go.mod h1:W7OKlS05LPMcLvQamv12gv/hSQlWAyU1lh98jwMVf2k= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.12 h1:8TMY/uvatjnLqllJhW0WOfAQSdLQl525yuaA0Uq1ejk= +github.com/aws/aws-sdk-go-v2/service/sqs v1.37.12/go.mod h1:LG6s2xJm3K9X9ee5EmYyOveXOgVK4jtunBJBXFJ2TqE= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14 h1:c5WJ3iHz7rLIgArznb3JCSQT3uUMiz9DLZhIX+1G8ok= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.14/go.mod h1:+JJQTxB6N4niArC14YNtxcQtwEqzS3o9Z32n7q33Rfs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13 h1:f1L/JtUkVODD+k1+IiSJUUv8A++2qVr+Xvb3xWXETMU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.13/go.mod h1:tvqlFoja8/s0o+UruA1Nrezo/df0PzdunMDDurUfg6U= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.12 h1:fqg6c1KVrc3SYWma/egWue5rKI4G2+M4wMQN2JosNAA= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.12/go.mod h1:7Yn+p66q/jt38qMoVfNvjbm3D89mGBnkwDcijgtih8w= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +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.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62 h1:gZvwm6umNtCdZxD+H7my06k4wo6PQLgVwwilZIwWlyM= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.62/go.mod h1:I/S6699SKgH+TrDK7Y+pmTTzajLulyUbKYb/ngj64UA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +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-getter v1.7.8 h1:mshVHx1Fto0/MydBekWan5zUipGq7jO0novchgMmSiY= +github.com/hashicorp/go-getter v1.7.8/go.mod h1:2c6CboOEb9jG6YvmC9xdD+tyAFsrUaJPedwXDGr0TM4= +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= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +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-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +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/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0 h1:bFkfHqO3IoO0VlUAuFxUhf5zctq/OD8H0wq77hxoeN4= +go.opentelemetry.io/contrib/instrumentation/github.com/aws/aws-sdk-go-v2/otelaws v0.59.0/go.mod h1:2Wj/UyCzrPIweApqPFgXXRNZrpoz/sbU8UxeM6Dby3Q= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= +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/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-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.155.0 h1:vBmGhCYs0djJttDNynWo44zosHlPvHmA0XiN2zP2DtA= +google.golang.org/api v0.155.0/go.mod h1:GI5qK5f40kCpHfPn6+YzGAByIKWv8ujFnmoWm7Igduk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/backend/remote-state/s3/logging.go b/internal/backend/remote-state/s3/logging.go new file mode 100644 index 0000000000..956ecc650a --- /dev/null +++ b/internal/backend/remote-state/s3/logging.go @@ -0,0 +1,89 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "sync" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/states/statemgr" +) + +const ( + logKeyBucket = "tf_backend.s3.bucket" + logKeyPath = "tf_backend.s3.path" +) + +const ( + logKeyBackendLockId = "tf_backend.lock.id" + logKeyBackendLockOperation = "tf_backend.lock.operation" + logKeyBackendLockInfo = "tf_backend.lock.info" + logKeyBackendLockWho = "tf_backend.lock.who" + logKeyBackendLockVersion = "tf_backend.lock.version" + logKeyBackendLockPath = "tf_backend.lock.path" +) + +const ( + logKeyBackendOperation = "tf_backend.operation" + logKeyBackendRequestId = "tf_backend.req_id" // Using "req_id" to match pattern for provider logging + logKeyBackendWorkspace = "tf_backend.workspace" + logKeyBackendWorkspacePrefix = "tf_backend.workspace-prefix" +) + +const ( + // operationBackendConfigSchema = "ConfigSchema" + // operationBackendPrepareConfig = "PrepareConfig" + operationBackendConfigure = "Configure" + // operationBackendStateMgr = "StateMgr" + operationBackendDeleteWorkspace = "DeleteWorkspace" + operationBackendWorkspaces = "Workspaces" +) + +const ( + operationClientGet = "Get" + operationClientPut = "Put" + operationClientDelete = "Delete" +) + +const ( + operationLockerLock = "Lock" + operationLockerUnlock = "Unlock" +) + +var logger = sync.OnceValue(func() hclog.Logger { + l := logging.HCLogger() + return l.Named("backend-s3") +}) + +func logWithOperation(in hclog.Logger, operation string) hclog.Logger { + log := in.With( + logKeyBackendOperation, operation, + ) + if id, err := uuid.GenerateUUID(); err == nil { + log = log.With( + logKeyBackendRequestId, id, + ) + + } + return log +} + +func logWithLockInfo(in hclog.Logger, info *statemgr.LockInfo) hclog.Logger { + return in.With( + logKeyBackendLockId, info.ID, + logKeyBackendLockOperation, info.Operation, + logKeyBackendLockInfo, info.Info, + logKeyBackendLockWho, info.Who, + logKeyBackendLockVersion, info.Version, + logKeyBackendLockPath, info.Path, + ) +} + +func logWithLockID(in hclog.Logger, id string) hclog.Logger { + return in.With( + logKeyBackendLockId, id, + ) +} diff --git a/internal/backend/remote-state/s3/middleware.go b/internal/backend/remote-state/s3/middleware.go new file mode 100644 index 0000000000..160f7b7680 --- /dev/null +++ b/internal/backend/remote-state/s3/middleware.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "context" + "net/http" + + awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/smithy-go" + "github.com/aws/smithy-go/middleware" + smithyhttp "github.com/aws/smithy-go/transport/http" +) + +// This will not be needed once https://github.com/aws/aws-sdk-go-v2/issues/2282 +// is addressed +func addS3WrongRegionErrorMiddleware(stack *middleware.Stack) error { + return stack.Deserialize.Insert( + &s3WrongRegionErrorMiddleware{}, + "ResponseErrorWrapper", + middleware.After, + ) +} + +var _ middleware.DeserializeMiddleware = &s3WrongRegionErrorMiddleware{} + +type s3WrongRegionErrorMiddleware struct{} + +func (m *s3WrongRegionErrorMiddleware) ID() string { + return "tf_S3WrongRegionErrorMiddleware" +} + +func (m *s3WrongRegionErrorMiddleware) HandleDeserialize(ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) ( + out middleware.DeserializeOutput, metadata middleware.Metadata, err error, +) { + out, metadata, err = next.HandleDeserialize(ctx, in) + if err == nil || !IsA[*smithy.GenericAPIError](err) { + return out, metadata, err + } + + resp, ok := out.RawResponse.(*smithyhttp.Response) + if !ok || resp.StatusCode != http.StatusMovedPermanently { + return out, metadata, err + } + + reqRegion := awsmiddleware.GetRegion(ctx) + + bucketRegion := resp.Header.Get("X-Amz-Bucket-Region") + + err = newBucketRegionError(reqRegion, bucketRegion) + + return out, metadata, err +} diff --git a/internal/backend/remote-state/s3/validate.go b/internal/backend/remote-state/s3/validate.go new file mode 100644 index 0000000000..d3a9b2c04f --- /dev/null +++ b/internal/backend/remote-state/s3/validate.go @@ -0,0 +1,530 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +const ( + multiRegionKeyIdPattern = `mrk-[a-f0-9]{32}` + uuidRegexPattern = `[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[ab89][a-f0-9]{3}-[a-f0-9]{12}` + aliasRegexPattern = `alias/[a-zA-Z0-9/_-]+` +) + +func validateKMSKey(path cty.Path, s string) (diags tfdiags.Diagnostics) { + if arn.IsARN(s) { + return validateKMSKeyARN(path, s) + } + return validateKMSKeyID(path, s) +} + +func validateKMSKeyID(path cty.Path, s string) (diags tfdiags.Diagnostics) { + keyIdRegex := regexp.MustCompile(`^` + uuidRegexPattern + `|` + multiRegionKeyIdPattern + `|` + aliasRegexPattern + `$`) + if !keyIdRegex.MatchString(s) { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ID", + fmt.Sprintf("Value must be a valid KMS Key ID, got %q", s), + path, + )) + return diags + } + + return diags +} + +func validateKMSKeyARN(path cty.Path, s string) (diags tfdiags.Diagnostics) { + parsedARN, err := arn.Parse(s) + if err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + fmt.Sprintf("Value must be a valid KMS Key ARN, got %q", s), + path, + )) + return diags + } + + if !isKeyARN(parsedARN) { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + fmt.Sprintf("Value must be a valid KMS Key ARN, got %q", s), + path, + )) + return diags + } + + return diags +} + +func isKeyARN(arn arn.ARN) bool { + return keyIdFromARNResource(arn.Resource) != "" || aliasIdFromARNResource(arn.Resource) != "" +} + +func keyIdFromARNResource(s string) string { + keyIdResourceRegex := regexp.MustCompile(`^key/(` + uuidRegexPattern + `|` + multiRegionKeyIdPattern + `)$`) + matches := keyIdResourceRegex.FindStringSubmatch(s) + if matches == nil || len(matches) != 2 { + return "" + } + + return matches[1] +} + +func aliasIdFromARNResource(s string) string { + aliasIdResourceRegex := regexp.MustCompile(`^(` + aliasRegexPattern + `)$`) + matches := aliasIdResourceRegex.FindStringSubmatch(s) + if matches == nil || len(matches) != 2 { + return "" + } + + return matches[1] +} + +type stringValidator func(val string, path cty.Path, diags *tfdiags.Diagnostics) + +func validateStringNotEmpty(val string, path cty.Path, diags *tfdiags.Diagnostics) { + val = strings.TrimSpace(val) + if len(val) == 0 { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + "The value cannot be empty or all whitespace", + path, + )) + } +} + +func validateStringLenBetween(min, max int) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if l := len(val); l < min || l > max { + *diags = diags.Append(attributeErrDiag( + "Invalid Value Length", + fmt.Sprintf("Length must be between %d and %d, had %d", min, max, l), + path, + )) + } + } +} + +func validateStringMatches(re *regexp.Regexp, description string) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if !re.MatchString(val) { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + description, + path, + )) + } + } +} + +func validateStringDoesNotContain(s string) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if strings.Contains(val, s) { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + fmt.Sprintf(`Value must not contain "%s"`, s), + path, + )) + } + } +} + +func validateStringInSlice(sl []string) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + match := false + for _, s := range sl { + if val == s { + match = true + } + } + if !match { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + fmt.Sprintf("Value must be one of [%s]", strings.Join(sl, ", ")), + path, + )) + } + + } +} + +// validateStringRetryMode ensures the provided value in a valid AWS retry mode +func validateStringRetryMode(val string, path cty.Path, diags *tfdiags.Diagnostics) { + _, err := aws.ParseRetryMode(val) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + err.Error(), + path, + )) + } +} + +// S3 will strip leading slashes from an object, so while this will +// technically be accepted by S3, it will break our workspace hierarchy. +// S3 will recognize objects with a trailing slash as a directory +// so they should not be valid keys +func validateStringS3Path(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if strings.HasPrefix(val, "/") || strings.HasSuffix(val, "/") { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + `The value must not start or end with "/"`, + path, + )) + } +} + +func validateARN(validators ...arnValidator) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + parsedARN, err := arn.Parse(val) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid ARN", + fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", val, err), + path, + )) + return + } + + for _, validator := range validators { + validator(parsedARN, path, diags) + } + } +} + +// Copied from `ValidIAMPolicyJSON` (https://github.com/hashicorp/terraform-provider-aws/blob/ffd1c8a006dcd5a6b58a643df9cc147acb5b7a53/internal/verify/validate.go#L154) +func validateIAMPolicyDocument(val string, path cty.Path, diags *tfdiags.Diagnostics) { + // IAM Policy documents need to be valid JSON, and pass legacy parsing + val = strings.TrimSpace(val) + if first := val[:1]; first != "{" { + switch val[:1] { + case `"`: + // There are some common mistakes that lead to strings appearing + // here instead of objects, so we'll try some heuristics to + // check for those so we might give more actionable feedback in + // these situations. + var content string + var innerContent any + if err := json.Unmarshal([]byte(val), &content); err == nil { + if strings.HasSuffix(content, ".json") { + *diags = diags.Append(attributeErrDiag( + "Invalid IAM Policy Document", + fmt.Sprintf(`Expected a JSON object describing the policy, had a JSON-encoded string. + +The string %q looks like a filename, please pass the contents of the file instead of the filename.`, + content, + ), + path, + )) + return + } else if err := json.Unmarshal([]byte(content), &innerContent); err == nil { + // hint = " (have you double-encoded your JSON data?)" + *diags = diags.Append(attributeErrDiag( + "Invalid IAM Policy Document", + `Expected a JSON object describing the policy, had a JSON-encoded string. + +The string content was valid JSON, your policy document may have been double-encoded.`, + path, + )) + return + } + } + *diags = diags.Append(attributeErrDiag( + "Invalid IAM Policy Document", + "Expected a JSON object describing the policy, had a JSON-encoded string.", + path, + )) + default: + // Generic error for if we didn't find something more specific to say. + *diags = diags.Append(attributeErrDiag( + "Invalid IAM Policy Document", + "Expected a JSON object describing the policy", + path, + )) + } + } else { + var j any + if err := json.Unmarshal([]byte(val), &j); err != nil { + errStr := err.Error() + var jsonErr *json.SyntaxError + if errors.As(err, &jsonErr) { + errStr += fmt.Sprintf(", at byte offset %d", jsonErr.Offset) + } + *diags = diags.Append(attributeErrDiag( + "Invalid JSON Document", + fmt.Sprintf("The JSON document contains an error: %s", errStr), + path, + )) + } + } +} + +func validateStringKMSKey(val string, path cty.Path, diags *tfdiags.Diagnostics) { + ds := validateKMSKey(path, val) + *diags = diags.Append(ds) +} + +// validateStringLegacyURL validates that a string can be parsed generally as a URL, but does +// not ensure that the URL is valid. +func validateStringLegacyURL(val string, path cty.Path, diags *tfdiags.Diagnostics) { + u, err := url.Parse(val) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + fmt.Sprintf("The value %q cannot be parsed as a URL: %s", val, err), + path, + )) + return + } + if u.Scheme == "" || u.Host == "" { + *diags = diags.Append(legacyIncompleteURLDiag(val, path)) + return + } +} + +func legacyIncompleteURLDiag(val string, path cty.Path) tfdiags.Diagnostic { + return attributeWarningDiag( + "Complete URL Expected", + fmt.Sprintf(`The value should be a valid URL containing at least a scheme and hostname. Had %q. + +Using an incomplete URL, such as a hostname only, may work, but may have unexpected behavior.`, val), + path, + ) +} + +// validateStringValidURL validates that a URL is a valid URL, inclding a scheme and host +func validateStringValidURL(val string, path cty.Path, diags *tfdiags.Diagnostics) { + u, err := url.Parse(val) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid Value", + fmt.Sprintf("The value %q cannot be parsed as a URL: %s", val, err), + path, + )) + return + } + if u.Scheme == "" || u.Host == "" { + *diags = diags.Append(invalidURLDiag(val, path)) + return + } +} + +func invalidURLDiag(val string, path cty.Path) tfdiags.Diagnostic { + return attributeErrDiag( + "Invalid Value", + fmt.Sprintf("The value must be a valid URL containing at least a scheme and hostname. Had %q", val), + path, + ) +} + +// Using a val of `cty.ValueSet` would be better here, but we can't get an ElementIterator from a ValueSet +type setValidator func(val cty.Value, path cty.Path, diags *tfdiags.Diagnostics) + +func validateSetStringElements(validators ...stringValidator) setValidator { + return func(val cty.Value, path cty.Path, diags *tfdiags.Diagnostics) { + typ := val.Type() + if eltTyp := typ.ElementType(); eltTyp != cty.String { + *diags = diags.Append(attributeErrDiag( + "Internal Error", + fmt.Sprintf(`Expected type to be %s, got: %s`, cty.Set(cty.String).FriendlyName(), val.Type().FriendlyName()), + path, + )) + return + } + + eltPath := make(cty.Path, len(path)+1) + copy(eltPath, path) + idxIdx := len(path) + + iter := val.ElementIterator() + for iter.Next() { + idx, elt := iter.Element() + + eltPath[idxIdx] = cty.IndexStep{Key: idx} + + for _, validator := range validators { + validator(elt.AsString(), eltPath, diags) + } + } + } +} + +type arnValidator func(val arn.ARN, path cty.Path, diags *tfdiags.Diagnostics) + +func validateIAMRoleARN(val arn.ARN, path cty.Path, diags *tfdiags.Diagnostics) { + if !strings.HasPrefix(val.Resource, "role/") { + *diags = diags.Append(attributeErrDiag( + "Invalid IAM Role ARN", + fmt.Sprintf("Value must be a valid IAM Role ARN, got %q", val), + path, + )) + } +} + +func validateIAMPolicyARN(val arn.ARN, path cty.Path, diags *tfdiags.Diagnostics) { + if !strings.HasPrefix(val.Resource, "policy/") { + *diags = diags.Append(attributeErrDiag( + "Invalid IAM Policy ARN", + fmt.Sprintf("Value must be a valid IAM Policy ARN, got %q", val), + path, + )) + } +} + +func validateDuration(validators ...durationValidator) stringValidator { + return func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + duration, err := time.ParseDuration(val) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("The value %q cannot be parsed as a duration: %s", val, err), + path, + )) + return + } + + for _, validator := range validators { + validator(duration, path, diags) + } + } +} + +type durationValidator func(val time.Duration, path cty.Path, diags *tfdiags.Diagnostics) + +func validateDurationBetween(min, max time.Duration) durationValidator { + return func(val time.Duration, path cty.Path, diags *tfdiags.Diagnostics) { + if val < min || val > max { + *diags = diags.Append(attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, val), + path, + )) + } + } +} + +type objectValidator func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) + +func validateAttributesConflict(paths ...cty.Path) objectValidator { + return func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) { + found := false + for _, path := range paths { + val, err := path.Apply(obj) + if err != nil { + *diags = diags.Append(attributeErrDiag( + "Invalid Path for Schema", + "The S3 Backend unexpectedly provided a path that does not match the schema. "+ + "Please report this to the developers.\n\n"+ + "Path: "+pathString(path)+"\n\n"+ + "Error:"+err.Error(), + objPath, + )) + continue + } + if !val.IsNull() { + if found { + pathStrs := make([]string, len(paths)) + for i, path := range paths { + pathStrs[i] = pathString(path) + } + *diags = diags.Append(invalidAttributeCombinationDiag(objPath, paths)) + } else { + found = true + } + } + } + } +} + +func validateExactlyOneOfAttributes(paths ...cty.Path) objectValidator { + return func(obj cty.Value, objPath cty.Path, diags *tfdiags.Diagnostics) { + var localDiags tfdiags.Diagnostics + found := make(map[string]cty.Path, len(paths)) + for _, path := range paths { + val, err := path.Apply(obj) + if err != nil { + localDiags = localDiags.Append(attributeErrDiag( + "Invalid Path for Schema", + "The S3 Backend unexpectedly provided a path that does not match the schema. "+ + "Please report this to the developers.\n\n"+ + "Path: "+pathString(path)+"\n\n"+ + "Error:"+err.Error(), + objPath, + )) + continue + } + if !val.IsNull() { + found[pathString(path)] = path + } + } + *diags = diags.Append(localDiags) + + if len(found) > 1 { + *diags = diags.Append(invalidAttributeCombinationDiag(objPath, paths)) + return + } + + if len(found) == 0 && !localDiags.HasErrors() { + pathStrs := make([]string, len(paths)) + for i, path := range paths { + pathStrs[i] = pathString(path) + } + *diags = diags.Append(attributeErrDiag( + "Missing Required Value", + fmt.Sprintf(`Exactly one of %s must be set.`, strings.Join(pathStrs, ", ")), + objPath, + )) + } + } +} + +func invalidAttributeCombinationDiag(objPath cty.Path, paths []cty.Path) tfdiags.Diagnostic { + pathStrs := make([]string, len(paths)) + for i, path := range paths { + pathStrs[i] = pathString(path) + } + return attributeErrDiag( + "Invalid Attribute Combination", + fmt.Sprintf(`Only one of %s can be set.`, strings.Join(pathStrs, ", ")), + objPath, + ) +} + +func attributeErrDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic { + return tfdiags.AttributeValue(tfdiags.Error, summary, detail, attrPath.Copy()) +} + +func attributeWarningDiag(summary, detail string, attrPath cty.Path) tfdiags.Diagnostic { + return tfdiags.AttributeValue(tfdiags.Warning, summary, detail, attrPath.Copy()) +} + +func wholeBodyErrDiag(summary, detail string) tfdiags.Diagnostic { + return tfdiags.WholeContainingBody(tfdiags.Error, summary, detail) +} + +func wholeBodyWarningDiag(summary, detail string) tfdiags.Diagnostic { + return tfdiags.WholeContainingBody(tfdiags.Warning, summary, detail) +} + +var assumeRoleNameValidator = []stringValidator{ + validateStringLenBetween(2, 64), + validateStringMatches( + regexp.MustCompile(`^[\w+=,.@\-]*$`), + `Value can only contain letters, numbers, or the following characters: =,.@-`, + ), +} diff --git a/internal/backend/remote-state/s3/validate_test.go b/internal/backend/remote-state/s3/validate_test.go new file mode 100644 index 0000000000..d74f5ff4b6 --- /dev/null +++ b/internal/backend/remote-state/s3/validate_test.go @@ -0,0 +1,840 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package s3 + +import ( + "fmt" + "regexp" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws/arn" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func TestValidateKMSKey(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + in string + expected tfdiags.Diagnostics + }{ + "kms key id": { + in: "57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + "kms key arn": { + in: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + "kms multi-region key id": { + in: "mrk-f827515944fb43f9b902a09d2c8b554f", + }, + "kms multi-region key arn": { + in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681", + }, + "kms key alias": { + in: "alias/arbitrary-key", + }, + "kms key alias arn": { + in: "arn:aws:kms:us-west-2:111122223333:alias/arbitrary-key", + }, + "invalid key": { + in: "$%wrongkey", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ID", + `Value must be a valid KMS Key ID, got "$%wrongkey"`, + path, + ), + }, + }, + "non-kms arn": { + in: "arn:aws:lamda:foo:bar:key/xyz", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:lamda:foo:bar:key/xyz"`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := validateKMSKey(path, testcase.in) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateKeyARN(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + in string + expected tfdiags.Diagnostics + }{ + "kms key id": { + in: "arn:aws:kms:us-west-2:123456789012:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + "kms mrk key id": { + in: "arn:aws:kms:us-west-2:111122223333:key/mrk-a835af0b39c94b86a21a8fc9535df681", + }, + "kms non-key id": { + in: "arn:aws:kms:us-west-2:123456789012:something/else", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:kms:us-west-2:123456789012:something/else"`, + path, + ), + }, + }, + "non-kms arn": { + in: "arn:aws:iam::123456789012:user/David", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "arn:aws:iam::123456789012:user/David"`, + path, + ), + }, + }, + "not an arn": { + in: "not an arn", + expected: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Invalid KMS Key ARN", + `Value must be a valid KMS Key ARN, got "not an arn"`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := validateKMSKeyARN(path, testcase.in) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateStringLenBetween(t *testing.T) { + t.Parallel() + + const min, max = 2, 5 + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + expected tfdiags.Diagnostics + }{ + "valid": { + val: "valid", + }, + + "too short": { + val: "x", + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value Length", + fmt.Sprintf("Length must be between %d and %d, had %d", min, max, 1), + path, + ), + }, + }, + + "too long": { + val: "a very long string", + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value Length", + fmt.Sprintf("Length must be between %d and %d, had %d", min, max, 18), + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateStringLenBetween(min, max)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateStringMatches(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + re *regexp.Regexp + expected tfdiags.Diagnostics + }{ + "valid": { + val: "ok", + re: regexp.MustCompile(`^o[j-l]?$`), + }, + + "invalid": { + val: "not ok", + re: regexp.MustCompile(`^o[j-l]?$`), + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + "Value must be like ok", + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateStringMatches(testcase.re, "Value must be like ok")(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateARN(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + validator arnValidator + expected tfdiags.Diagnostics + }{ + "valid": { + val: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", + }, + + "invalid": { + val: "not an ARN", + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid ARN", + fmt.Sprintf("The value %q cannot be parsed as an ARN: %s", "not an ARN", arnParseError("not an ARN")), + path, + ), + }, + }, + + "fails validator": { + val: "arn:aws:kms:us-west-2:111122223333:key/57ff7a43-341d-46b6-aee3-a450c9de6dc8", + validator: func(val arn.ARN, path cty.Path, diags *tfdiags.Diagnostics) { + *diags = diags.Append(attributeErrDiag( + "Test", + "Test", + path, + )) + }, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Test", + "Test", + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var validators []arnValidator + if testcase.validator != nil { + validators = []arnValidator{ + testcase.validator, + } + } + + var diags tfdiags.Diagnostics + validateARN(validators...)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func arnParseError(s string) error { + _, err := arn.Parse(s) + return err +} + +func TestValidateIAMPolicyDocument(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + expected tfdiags.Diagnostics + }{ + "empty object": { + val: `{}`, + // Valid JSON, not valid IAM policy (but passes provider's test) + }, + "array": { + val: `{"abc":["1","2"]}`, + // Valid JSON, not valid IAM policy (but passes provider's test) + }, + "invalid key": { + val: `{0:"1"}`, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid JSON Document", + "The JSON document contains an error: invalid character '0' looking for beginning of object key string, at byte offset 2", + path, + ), + }, + }, + "leading whitespace": { + val: ` {"xyz": "foo"}`, + // Valid, must be trimmed before passing to AWS + }, + "is a string": { + val: `"blub"`, + // Valid JSON, not valid IAM policy + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid IAM Policy Document", + `Expected a JSON object describing the policy, had a JSON-encoded string.`, + path, + ), + }, + }, + "contains filename": { + val: `"../some-filename.json"`, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid IAM Policy Document", + `Expected a JSON object describing the policy, had a JSON-encoded string. + +The string "../some-filename.json" looks like a filename, please pass the contents of the file instead of the filename.`, + path, + ), + }, + }, + "double encoded": { + val: `"{\"Version\":\"...\"}"`, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid IAM Policy Document", + `Expected a JSON object describing the policy, had a JSON-encoded string. + +The string content was valid JSON, your policy document may have been double-encoded.`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateIAMPolicyDocument(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateSetStringElements(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val cty.Value + validator stringValidator + expected tfdiags.Diagnostics + }{ + "valid": { + val: cty.SetVal([]cty.Value{ + cty.StringVal("valid"), + cty.StringVal("also valid"), + }), + }, + + "fails validator": { + val: cty.SetVal([]cty.Value{ + cty.StringVal("valid"), + cty.StringVal("invalid"), + }), + validator: func(val string, path cty.Path, diags *tfdiags.Diagnostics) { + if val == "invalid" { + *diags = diags.Append(attributeErrDiag( + "Test", + "Test", + path, + )) + } + }, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Test", + "Test", + path.Index(cty.StringVal("invalid")), + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var validators []stringValidator + if testcase.validator != nil { + validators = []stringValidator{ + testcase.validator, + } + } + + var diags tfdiags.Diagnostics + validateSetStringElements(validators...)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +// func TestValidateStringSetValues(t *testing.T) { +// t.Parallel() + +// path := cty.GetAttrPath("field") + +// testcases := map[string]struct { +// val []string +// validator stringValidator +// expected tfdiags.Diagnostics +// }{ +// "valid": { +// val: []string{ +// "valid", +// "also valid", +// }, +// }, + +// "fails validator": { +// val: []string{ +// "valid", +// "invalid", +// }, +// validator: func(val string, path cty.Path, diags *tfdiags.Diagnostics) { +// if val == "invalid" { +// *diags = diags.Append(attributeErrDiag( +// "Test", +// "Test", +// path, +// )) +// } +// }, +// expected: tfdiags.Diagnostics{ +// attributeErrDiag( +// "Test", +// "Test", +// path.Index(cty.StringVal("invalid")), +// ), +// }, +// }, +// } + +// for name, testcase := range testcases { +// testcase := testcase +// t.Run(name, func(t *testing.T) { +// t.Parallel() + +// var validators []stringValidator +// if testcase.validator != nil { +// validators = []stringValidator{ +// testcase.validator, +// } +// } + +// var diags tfdiags.Diagnostics +// validateStringSetValues(validators...)(testcase.val, path, &diags) + +// if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { +// t.Errorf("unexpected diagnostics difference: %s", diff) +// } +// }) +// } +// } + +func TestValidateDuration(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + validator durationValidator + expected tfdiags.Diagnostics + }{ + "valid": { + val: "1h", + }, + + "invalid": { + val: "one hour", + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("The value %q cannot be parsed as a duration: %s", "one hour", durationParseError("one hour")), + path, + ), + }, + }, + + "fails validator": { + val: "1h", + validator: func(val time.Duration, path cty.Path, diags *tfdiags.Diagnostics) { + *diags = diags.Append(attributeErrDiag( + "Test", + "Test", + path, + )) + }, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Test", + "Test", + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var validators []durationValidator + if testcase.validator != nil { + validators = []durationValidator{ + testcase.validator, + } + } + + var diags tfdiags.Diagnostics + validateDuration(validators...)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func durationParseError(s string) error { + _, err := time.ParseDuration(s) + return err +} + +func TestValidateDurationBetween(t *testing.T) { + t.Parallel() + + const min, max = 15 * time.Minute, 12 * time.Hour + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val time.Duration + expected tfdiags.Diagnostics + }{ + "valid": { + val: 1 * time.Hour, + }, + + "too short": { + val: 1 * time.Minute, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, 1*time.Minute), + path, + ), + }, + }, + + "too long": { + val: 24 * time.Hour, + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Duration", + fmt.Sprintf("Duration must be between %s and %s, had %s", min, max, 24*time.Hour), + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateDurationBetween(min, max)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateStringLegacyURL(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + expected tfdiags.Diagnostics + }{ + "no trailing slash": { + val: "https://domain.test", + }, + + "no path": { + val: "https://domain.test/", + }, + + "with path": { + val: "https://domain.test/path", + }, + + "with port no trailing slash": { + val: "https://domain.test:1234", + }, + + "with port no path": { + val: "https://domain.test:1234/", + }, + + "with port with path": { + val: "https://domain.test:1234/path", + }, + + "no scheme no trailing slash": { + val: "domain.test", + expected: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("domain.test", path), + }, + }, + + "no scheme no path": { + val: "domain.test/", + expected: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("domain.test/", path), + }, + }, + + "no scheme with path": { + val: "domain.test/path", + expected: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("domain.test/path", path), + }, + }, + + "no scheme with port": { + val: "domain.test:1234", + expected: tfdiags.Diagnostics{ + legacyIncompleteURLDiag("domain.test:1234", path), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateStringLegacyURL(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestValidateStringValidURL(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + expected tfdiags.Diagnostics + }{ + "no trailing slash": { + val: "https://domain.test", + }, + + "no path": { + val: "https://domain.test/", + }, + + "with path": { + val: "https://domain.test/path", + }, + + "with port no trailing slash": { + val: "https://domain.test:1234", + }, + + "with port no path": { + val: "https://domain.test:1234/", + }, + + "with port with path": { + val: "https://domain.test:1234/path", + }, + + "no scheme no trailing slash": { + val: "domain.test", + expected: tfdiags.Diagnostics{ + invalidURLDiag("domain.test", path), + }, + }, + + "no scheme no path": { + val: "domain.test/", + expected: tfdiags.Diagnostics{ + invalidURLDiag("domain.test/", path), + }, + }, + + "no scheme with path": { + val: "domain.test/path", + expected: tfdiags.Diagnostics{ + invalidURLDiag("domain.test/path", path), + }, + }, + + "no scheme with port": { + val: "domain.test:1234", + expected: tfdiags.Diagnostics{ + invalidURLDiag("domain.test:1234", path), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateStringValidURL(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func Test_validateStringDoesNotContain(t *testing.T) { + t.Parallel() + + path := cty.GetAttrPath("field") + + testcases := map[string]struct { + val string + s string + expected tfdiags.Diagnostics + }{ + "valid": { + val: "foo", + s: "bar", + }, + + "invalid": { + val: "foobarbaz", + s: "bar", + expected: tfdiags.Diagnostics{ + attributeErrDiag( + "Invalid Value", + `Value must not contain "bar"`, + path, + ), + }, + }, + } + + for name, testcase := range testcases { + testcase := testcase + t.Run(name, func(t *testing.T) { + t.Parallel() + + var diags tfdiags.Diagnostics + validateStringDoesNotContain(testcase.s)(testcase.val, path, &diags) + + if diff := cmp.Diff(diags, testcase.expected, tfdiags.DiagnosticComparer); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/backend/remote-state/swift/backend.go b/internal/backend/remote-state/swift/backend.go deleted file mode 100644 index 6084131338..0000000000 --- a/internal/backend/remote-state/swift/backend.go +++ /dev/null @@ -1,485 +0,0 @@ -package swift - -import ( - "context" - "fmt" - "log" - "strconv" - "strings" - "time" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/utils/terraform/auth" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/legacy/helper/schema" - "github.com/hashicorp/terraform/version" -) - -// Use openstackbase.Config as the base/foundation of this provider's -// Config struct. -type Config struct { - auth.Config -} - -// New creates a new backend for Swift remote state. -func New() backend.Backend { - s := &schema.Backend{ - Schema: map[string]*schema.Schema{ - "auth_url": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_AUTH_URL", ""), - Description: descriptions["auth_url"], - }, - - "region_name": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["region_name"], - DefaultFunc: schema.EnvDefaultFunc("OS_REGION_NAME", ""), - }, - - "user_name": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_USERNAME", ""), - Description: descriptions["user_name"], - }, - - "user_id": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_USER_ID", ""), - Description: descriptions["user_name"], - }, - - "application_credential_id": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_APPLICATION_CREDENTIAL_ID", ""), - Description: descriptions["application_credential_id"], - }, - - "application_credential_name": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_APPLICATION_CREDENTIAL_NAME", ""), - Description: descriptions["application_credential_name"], - }, - - "application_credential_secret": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_APPLICATION_CREDENTIAL_SECRET", ""), - Description: descriptions["application_credential_secret"], - }, - - "tenant_id": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "OS_TENANT_ID", - "OS_PROJECT_ID", - }, ""), - Description: descriptions["tenant_id"], - }, - - "tenant_name": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "OS_TENANT_NAME", - "OS_PROJECT_NAME", - }, ""), - Description: descriptions["tenant_name"], - }, - - "password": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, - DefaultFunc: schema.EnvDefaultFunc("OS_PASSWORD", ""), - Description: descriptions["password"], - }, - - "token": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.MultiEnvDefaultFunc([]string{ - "OS_TOKEN", - "OS_AUTH_TOKEN", - }, ""), - Description: descriptions["token"], - }, - - "user_domain_name": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_USER_DOMAIN_NAME", ""), - Description: descriptions["user_domain_name"], - }, - - "user_domain_id": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_USER_DOMAIN_ID", ""), - Description: descriptions["user_domain_id"], - }, - - "project_domain_name": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_PROJECT_DOMAIN_NAME", ""), - Description: descriptions["project_domain_name"], - }, - - "project_domain_id": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_PROJECT_DOMAIN_ID", ""), - Description: descriptions["project_domain_id"], - }, - - "domain_id": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_DOMAIN_ID", ""), - Description: descriptions["domain_id"], - }, - - "domain_name": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_DOMAIN_NAME", ""), - Description: descriptions["domain_name"], - }, - - "default_domain": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_DEFAULT_DOMAIN", "default"), - Description: descriptions["default_domain"], - }, - - "insecure": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_INSECURE", nil), - Description: descriptions["insecure"], - }, - - "endpoint_type": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_ENDPOINT_TYPE", ""), - }, - - "cacert_file": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_CACERT", ""), - Description: descriptions["cacert_file"], - }, - - "cert": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_CERT", ""), - Description: descriptions["cert"], - }, - - "key": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_KEY", ""), - Description: descriptions["key"], - }, - - "swauth": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_SWAUTH", false), - Description: descriptions["swauth"], - }, - - "allow_reauth": { - Type: schema.TypeBool, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_ALLOW_REAUTH", false), - Description: descriptions["allow_reauth"], - }, - - "cloud": { - Type: schema.TypeString, - Optional: true, - DefaultFunc: schema.EnvDefaultFunc("OS_CLOUD", ""), - Description: descriptions["cloud"], - }, - - "max_retries": { - Type: schema.TypeInt, - Optional: true, - Default: 0, - Description: descriptions["max_retries"], - }, - - "disable_no_cache_header": { - Type: schema.TypeBool, - Optional: true, - Default: false, - Description: descriptions["disable_no_cache_header"], - }, - - "path": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["path"], - Deprecated: "Use container instead", - ConflictsWith: []string{"container"}, - }, - - "container": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["container"], - }, - - "archive_path": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["archive_path"], - Deprecated: "Use archive_container instead", - ConflictsWith: []string{"archive_container"}, - }, - - "archive_container": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["archive_container"], - }, - - "expire_after": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["expire_after"], - }, - - "lock": { - Type: schema.TypeBool, - Optional: true, - Description: "Lock state access", - Default: true, - }, - - "state_name": { - Type: schema.TypeString, - Optional: true, - Description: descriptions["state_name"], - Default: "tfstate.tf", - }, - }, - } - - result := &Backend{Backend: s} - result.Backend.ConfigureFunc = result.configure - return result -} - -var descriptions map[string]string - -func init() { - descriptions = map[string]string{ - "auth_url": "The Identity authentication URL.", - - "region_name": "The name of the Region to use.", - - "user_name": "Username to login with.", - - "user_id": "User ID to login with.", - - "application_credential_id": "Application Credential ID to login with.", - - "application_credential_name": "Application Credential name to login with.", - - "application_credential_secret": "Application Credential secret to login with.", - - "tenant_id": "The ID of the Tenant (Identity v2) or Project (Identity v3)\n" + - "to login with.", - - "tenant_name": "The name of the Tenant (Identity v2) or Project (Identity v3)\n" + - "to login with.", - - "password": "Password to login with.", - - "token": "Authentication token to use as an alternative to username/password.", - - "user_domain_name": "The name of the domain where the user resides (Identity v3).", - - "user_domain_id": "The ID of the domain where the user resides (Identity v3).", - - "project_domain_name": "The name of the domain where the project resides (Identity v3).", - - "project_domain_id": "The ID of the domain where the proejct resides (Identity v3).", - - "domain_id": "The ID of the Domain to scope to (Identity v3).", - - "domain_name": "The name of the Domain to scope to (Identity v3).", - - "default_domain": "The name of the Domain ID to scope to if no other domain is specified. Defaults to `default` (Identity v3).", - - "insecure": "Trust self-signed certificates.", - - "cacert_file": "A Custom CA certificate.", - - "endpoint_type": "The catalog endpoint type to use.", - - "cert": "A client certificate to authenticate with.", - - "key": "A client private key to authenticate with.", - - "swauth": "Use Swift's authentication system instead of Keystone.", - - "allow_reauth": "If set to `true`, OpenStack authorization will be perfomed\n" + - "automatically, if the initial auth token get expired. This is useful,\n" + - "when the token TTL is low or the overall Terraform provider execution\n" + - "time expected to be greater than the initial token TTL.", - - "cloud": "An entry in a `clouds.yaml` file to use.", - - "max_retries": "How many times HTTP connection should be retried until giving up.", - - "disable_no_cache_header": "If set to `true`, the HTTP `Cache-Control: no-cache` header will not be added by default to all API requests.", - - "path": "Swift container path to use.", - - "container": "Swift container to create", - - "archive_path": "Swift container path to archive state to.", - - "archive_container": "Swift container to archive state to.", - - "expire_after": "Archive object expiry duration.", - - "state_name": "Name of state object in container", - } -} - -type Backend struct { - *schema.Backend - - // Fields below are set from configure - client *gophercloud.ServiceClient - archive bool - archiveContainer string - expireSecs int - container string - lock bool - stateName string -} - -func (b *Backend) configure(ctx context.Context) error { - if b.client != nil { - return nil - } - - // Grab the resource data - data := schema.FromContextBackendConfig(ctx) - config := &Config{ - auth.Config{ - CACertFile: data.Get("cacert_file").(string), - ClientCertFile: data.Get("cert").(string), - ClientKeyFile: data.Get("key").(string), - Cloud: data.Get("cloud").(string), - DefaultDomain: data.Get("default_domain").(string), - DomainID: data.Get("domain_id").(string), - DomainName: data.Get("domain_name").(string), - EndpointType: data.Get("endpoint_type").(string), - IdentityEndpoint: data.Get("auth_url").(string), - Password: data.Get("password").(string), - ProjectDomainID: data.Get("project_domain_id").(string), - ProjectDomainName: data.Get("project_domain_name").(string), - Region: data.Get("region_name").(string), - Swauth: data.Get("swauth").(bool), - Token: data.Get("token").(string), - TenantID: data.Get("tenant_id").(string), - TenantName: data.Get("tenant_name").(string), - UserDomainID: data.Get("user_domain_id").(string), - UserDomainName: data.Get("user_domain_name").(string), - Username: data.Get("user_name").(string), - UserID: data.Get("user_id").(string), - ApplicationCredentialID: data.Get("application_credential_id").(string), - ApplicationCredentialName: data.Get("application_credential_name").(string), - ApplicationCredentialSecret: data.Get("application_credential_secret").(string), - AllowReauth: data.Get("allow_reauth").(bool), - MaxRetries: data.Get("max_retries").(int), - DisableNoCacheHeader: data.Get("disable_no_cache_header").(bool), - TerraformVersion: version.Version, - }, - } - - if v, ok := data.GetOkExists("insecure"); ok { - insecure := v.(bool) - config.Insecure = &insecure - } - - if err := config.LoadAndValidate(); err != nil { - return err - } - - // Assign state name - b.stateName = data.Get("state_name").(string) - - // Assign Container - b.container = data.Get("container").(string) - if b.container == "" { - // Check deprecated field - b.container = data.Get("path").(string) - } - - // Store the lock information - b.lock = data.Get("lock").(bool) - - // Enable object archiving? - if archiveContainer, ok := data.GetOk("archive_container"); ok { - log.Printf("[DEBUG] Archive_container set, enabling object versioning") - b.archive = true - b.archiveContainer = archiveContainer.(string) - } else if archivePath, ok := data.GetOk("archive_path"); ok { - log.Printf("[DEBUG] Archive_path set, enabling object versioning") - b.archive = true - b.archiveContainer = archivePath.(string) - } - - // Enable object expiry? - if expireRaw, ok := data.GetOk("expire_after"); ok { - expire := expireRaw.(string) - log.Printf("[DEBUG] Requested that remote state expires after %s", expire) - - if strings.HasSuffix(expire, "d") { - log.Printf("[DEBUG] Got a days expire after duration. Converting to hours") - days, err := strconv.Atoi(expire[:len(expire)-1]) - if err != nil { - return fmt.Errorf("Error converting expire_after value %s to int: %s", expire, err) - } - - expire = fmt.Sprintf("%dh", days*24) - log.Printf("[DEBUG] Expire after %s hours", expire) - } - - expireDur, err := time.ParseDuration(expire) - if err != nil { - log.Printf("[DEBUG] Error parsing duration %s: %s", expire, err) - return fmt.Errorf("Error parsing expire_after duration '%s': %s", expire, err) - } - log.Printf("[DEBUG] Seconds duration = %d", int(expireDur.Seconds())) - b.expireSecs = int(expireDur.Seconds()) - } - - var err error - if b.client, err = config.ObjectStorageV1Client(config.Region); err != nil { - return err - } - - return nil -} diff --git a/internal/backend/remote-state/swift/backend_state.go b/internal/backend/remote-state/swift/backend_state.go deleted file mode 100644 index b853b64c96..0000000000 --- a/internal/backend/remote-state/swift/backend_state.go +++ /dev/null @@ -1,208 +0,0 @@ -package swift - -import ( - "fmt" - "strings" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" -) - -const ( - objectEnvPrefix = "env-" - delimiter = "/" -) - -func (b *Backend) Workspaces() ([]string, error) { - client := &RemoteClient{ - client: b.client, - container: b.container, - archive: b.archive, - archiveContainer: b.archiveContainer, - expireSecs: b.expireSecs, - lockState: b.lock, - } - - // List our container objects - objectNames, err := client.ListObjectsNames(objectEnvPrefix, delimiter) - - if err != nil { - return nil, err - } - - // Find the envs, we use a map since we can get duplicates with - // path suffixes. - envs := map[string]struct{}{} - for _, object := range objectNames { - object = strings.TrimPrefix(object, objectEnvPrefix) - object = strings.TrimSuffix(object, delimiter) - - // Ignore objects that still contain a "/" - // as we dont store states in subdirectories - if idx := strings.Index(object, delimiter); idx >= 0 { - continue - } - - // swift is eventually consistent, thus a deleted object may - // be listed in objectList. To ensure consistency, we query - // each object with a "newest" arg set to true - payload, err := client.get(b.objectName(object)) - if err != nil { - return nil, err - } - if payload == nil { - // object doesn't exist anymore. skipping. - continue - } - - envs[object] = struct{}{} - } - - result := make([]string, 1, len(envs)+1) - result[0] = backend.DefaultStateName - - for k := range envs { - result = append(result, k) - } - - return result, nil -} - -func (b *Backend) DeleteWorkspace(name string) error { - if name == backend.DefaultStateName || name == "" { - return fmt.Errorf("can't delete default state") - } - - client := &RemoteClient{ - client: b.client, - container: b.container, - archive: b.archive, - archiveContainer: b.archiveContainer, - expireSecs: b.expireSecs, - objectName: b.objectName(name), - lockState: b.lock, - } - - // Delete our object - err := client.Delete() - - return err -} - -func (b *Backend) StateMgr(name string) (statemgr.Full, error) { - if name == "" { - return nil, fmt.Errorf("missing state name") - } - - client := &RemoteClient{ - client: b.client, - container: b.container, - archive: b.archive, - archiveContainer: b.archiveContainer, - expireSecs: b.expireSecs, - objectName: b.objectName(name), - lockState: b.lock, - } - - var stateMgr statemgr.Full = &remote.State{Client: client} - - // If we're not locking, disable it - if !b.lock { - stateMgr = &statemgr.LockDisabled{Inner: stateMgr} - } - - // Check to see if this state already exists. - // If we're trying to force-unlock a state, we can't take the lock before - // fetching the state. If the state doesn't exist, we have to assume this - // is a normal create operation, and take the lock at that point. - // - // If we need to force-unlock, but for some reason the state no longer - // exists, the user will have to use openstack tools to manually fix the - // situation. - existing, err := b.Workspaces() - if err != nil { - return nil, err - } - - exists := false - for _, s := range existing { - if s == name { - exists = true - break - } - } - - // We need to create the object so it's listed by States. - if !exists { - // the default state always exists - if name == backend.DefaultStateName { - return stateMgr, nil - } - - // Grab a lock, we use this to write an empty state if one doesn't - // exist already. We have to write an empty state as a sentinel value - // so States() knows it exists. - lockInfo := statemgr.NewLockInfo() - lockInfo.Operation = "init" - lockId, err := stateMgr.Lock(lockInfo) - if err != nil { - return nil, fmt.Errorf("failed to lock state in Swift: %s", err) - } - - // Local helper function so we can call it multiple places - lockUnlock := func(parent error) error { - if err := stateMgr.Unlock(lockId); err != nil { - return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err) - } - - return parent - } - - // Grab the value - if err := stateMgr.RefreshState(); err != nil { - err = lockUnlock(err) - return nil, err - } - - // If we have no state, we have to create an empty state - if v := stateMgr.State(); v == nil { - if err := stateMgr.WriteState(states.NewState()); err != nil { - err = lockUnlock(err) - return nil, err - } - if err := stateMgr.PersistState(); err != nil { - err = lockUnlock(err) - return nil, err - } - } - - // Unlock, the state should now be initialized - if err := lockUnlock(nil); err != nil { - return nil, err - } - } - - return stateMgr, nil -} - -func (b *Backend) objectName(name string) string { - if name != backend.DefaultStateName { - name = fmt.Sprintf("%s%s/%s", objectEnvPrefix, name, b.stateName) - } else { - name = b.stateName - } - - return name -} - -const errStateUnlock = ` -Error unlocking Swift state. Lock ID: %s - -Error: %s - -You may have to force-unlock this state in order to use it again. -The Swift backend acquires a lock during initialization to ensure -the minimum required keys are prepared. -` diff --git a/internal/backend/remote-state/swift/backend_test.go b/internal/backend/remote-state/swift/backend_test.go deleted file mode 100644 index 864e3963b1..0000000000 --- a/internal/backend/remote-state/swift/backend_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package swift - -import ( - "fmt" - "os" - "testing" - "time" - - "github.com/hashicorp/terraform/internal/backend" -) - -// verify that we are doing ACC tests or the Swift tests specifically -func testACC(t *testing.T) { - skip := os.Getenv("TF_ACC") == "" && os.Getenv("TF_SWIFT_TEST") == "" - if skip { - t.Log("swift backend tests require setting TF_ACC or TF_SWIFT_TEST") - t.Skip() - } - t.Log("swift backend acceptance tests enabled") -} - -func TestBackend_impl(t *testing.T) { - var _ backend.Backend = new(Backend) -} - -func testAccPreCheck(t *testing.T) { - v := os.Getenv("OS_AUTH_URL") - if v == "" { - t.Fatal("OS_AUTH_URL must be set for acceptance tests") - } -} - -func TestBackendConfig(t *testing.T) { - testACC(t) - - // Build config - container := fmt.Sprintf("terraform-state-swift-testconfig-%x", time.Now().Unix()) - archiveContainer := fmt.Sprintf("%s_archive", container) - - config := map[string]interface{}{ - "archive_container": archiveContainer, - "container": container, - } - - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(config)).(*Backend) - - if b.container != container { - t.Fatal("Incorrect container was provided.") - } - if b.archiveContainer != archiveContainer { - t.Fatal("Incorrect archive_container was provided.") - } -} - -func TestBackend(t *testing.T) { - testACC(t) - - container := fmt.Sprintf("terraform-state-swift-testbackend-%x", time.Now().Unix()) - - be0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "container": container, - })).(*Backend) - - be1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "container": container, - })).(*Backend) - - client := &RemoteClient{ - client: be0.client, - container: be0.container, - } - - defer client.deleteContainer() - - backend.TestBackendStates(t, be0) - backend.TestBackendStateLocks(t, be0, be1) - backend.TestBackendStateForceUnlock(t, be0, be1) -} - -func TestBackendArchive(t *testing.T) { - testACC(t) - - container := fmt.Sprintf("terraform-state-swift-testarchive-%x", time.Now().Unix()) - archiveContainer := fmt.Sprintf("%s_archive", container) - - be0 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "archive_container": archiveContainer, - "container": container, - })).(*Backend) - - be1 := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "archive_container": archiveContainer, - "container": container, - })).(*Backend) - - defer func() { - client := &RemoteClient{ - client: be0.client, - container: be0.container, - } - - aclient := &RemoteClient{ - client: be0.client, - container: be0.archiveContainer, - } - - defer client.deleteContainer() - client.deleteContainer() - aclient.deleteContainer() - }() - - backend.TestBackendStates(t, be0) - backend.TestBackendStateLocks(t, be0, be1) - backend.TestBackendStateForceUnlock(t, be0, be1) -} diff --git a/internal/backend/remote-state/swift/client.go b/internal/backend/remote-state/swift/client.go deleted file mode 100644 index 603554af21..0000000000 --- a/internal/backend/remote-state/swift/client.go +++ /dev/null @@ -1,535 +0,0 @@ -package swift - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/json" - "fmt" - "log" - "sync" - "time" - - "github.com/gophercloud/gophercloud" - "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/containers" - "github.com/gophercloud/gophercloud/openstack/objectstorage/v1/objects" - "github.com/gophercloud/gophercloud/pagination" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statemgr" -) - -const ( - consistencyTimeout = 15 - - // Suffix that will be appended to state file paths - // when locking - lockSuffix = ".lock" - - // The TTL associated with this lock. - lockTTL = 60 * time.Second - - // The Interval associated with this lock periodic renew. - lockRenewInterval = 30 * time.Second - - // The amount of time we will retry to delete a container waiting for - // the objects to be deleted. - deleteRetryTimeout = 60 * time.Second - - // delay when polling the objects - deleteRetryPollInterval = 5 * time.Second -) - -// RemoteClient implements the Client interface for an Openstack Swift server. -// Implements "state/remote".ClientLocker -type RemoteClient struct { - client *gophercloud.ServiceClient - container string - archive bool - archiveContainer string - expireSecs int - objectName string - - mu sync.Mutex - // lockState is true if we're using locks - lockState bool - - info *statemgr.LockInfo - - // lockCancel cancels the Context use for lockRenewPeriodic, and is - // called when unlocking, or before creating a new lock if the lock is - // lost. - lockCancel context.CancelFunc -} - -func (c *RemoteClient) ListObjectsNames(prefix string, delim string) ([]string, error) { - if err := c.ensureContainerExists(); err != nil { - return nil, err - } - - // List our raw path - listOpts := objects.ListOpts{ - Full: false, - Prefix: prefix, - Delimiter: delim, - } - - result := []string{} - pager := objects.List(c.client, c.container, listOpts) - // Define an anonymous function to be executed on each page's iteration - err := pager.EachPage(func(page pagination.Page) (bool, error) { - objectList, err := objects.ExtractNames(page) - if err != nil { - return false, fmt.Errorf("Error extracting names from objects from page %+v", err) - } - for _, object := range objectList { - result = append(result, object) - } - return true, nil - }) - - if err != nil { - return nil, err - } - - return result, nil - -} - -func (c *RemoteClient) Get() (*remote.Payload, error) { - payload, err := c.get(c.objectName) - - // 404 response is to be expected if the object doesn't already exist! - if _, ok := err.(gophercloud.ErrDefault404); ok { - log.Println("[DEBUG] Object doesn't exist to download.") - return nil, nil - } - - return payload, err -} - -// swift is eventually constistent. Consistency -// is ensured by the Get func which will always try -// to retrieve the most recent object -func (c *RemoteClient) Put(data []byte) error { - if c.expireSecs != 0 { - log.Printf("[DEBUG] ExpireSecs = %d", c.expireSecs) - return c.put(c.objectName, data, c.expireSecs, "") - } - - return c.put(c.objectName, data, -1, "") - -} - -func (c *RemoteClient) Delete() error { - return c.delete(c.objectName) -} - -func (c *RemoteClient) Lock(info *statemgr.LockInfo) (string, error) { - c.mu.Lock() - defer c.mu.Unlock() - - if !c.lockState { - return "", nil - } - - log.Printf("[DEBUG] Acquiring Lock %#v on %s/%s", info, c.container, c.objectName) - - // This check only is to ensure we strictly follow the specification. - // Terraform shouldn't ever re-lock, so provide errors for the possible - // states if this is called. - if c.info != nil { - // we have an active lock already - return "", fmt.Errorf("state %q already locked", c.lockFilePath()) - } - - // update the path we're using - info.Path = c.lockFilePath() - - if err := c.writeLockInfo(info, lockTTL, "*"); err != nil { - return "", err - } - - log.Printf("[DEBUG] Acquired Lock %s on %s", info.ID, c.objectName) - - c.info = info - - ctx, cancel := context.WithCancel(context.Background()) - c.lockCancel = cancel - - // keep the lock renewed - go c.lockRenewPeriodic(ctx, info) - - return info.ID, nil -} - -func (c *RemoteClient) Unlock(id string) error { - c.mu.Lock() - - if !c.lockState { - return nil - } - - defer func() { - // The periodic lock renew is canceled - // the lockCancel func may not be nil in most usecases - // but can typically be nil when using a second client - // to ForceUnlock the state based on the same lock Id - if c.lockCancel != nil { - c.lockCancel() - } - c.info = nil - c.mu.Unlock() - }() - - log.Printf("[DEBUG] Releasing Lock %s on %s", id, c.objectName) - - info, err := c.lockInfo() - if err != nil { - return c.lockError(fmt.Errorf("failed to retrieve lock info: %s", err), nil) - } - - c.info = info - - // conflicting lock - if info.ID != id { - return c.lockError(fmt.Errorf("lock id %q does not match existing lock", id), info) - } - - // before the lock object deletion is ordered, we shall - // stop periodic renew - if c.lockCancel != nil { - c.lockCancel() - } - - if err = c.delete(c.lockFilePath()); err != nil { - return c.lockError(fmt.Errorf("error deleting lock with %q: %s", id, err), info) - } - - // Swift is eventually consistent; we have to wait until - // the lock is effectively deleted to return, or raise - // an error if deadline is reached. - - warning := ` -WARNING: Waiting for lock deletion timed out. -Swift has accepted the deletion order of the lock %s/%s. -But as it is eventually consistent, complete deletion -may happen later. -` - deadline := time.Now().Add(deleteRetryTimeout) - for { - if time.Now().Before(deadline) { - info, err := c.lockInfo() - - // 404 response is to be expected if the lock deletion - // has been processed - if _, ok := err.(gophercloud.ErrDefault404); ok { - log.Println("[DEBUG] Lock has been deleted.") - return nil - } - - if err != nil { - return err - } - - // conflicting lock - if info.ID != id { - log.Printf("[DEBUG] Someone else has acquired a lock: %v.", info) - return nil - } - - log.Printf("[DEBUG] Lock is still there, delete again and wait %v.", deleteRetryPollInterval) - c.delete(c.lockFilePath()) - time.Sleep(deleteRetryPollInterval) - continue - } - - return fmt.Errorf(warning, c.container, c.lockFilePath()) - } - -} - -func (c *RemoteClient) get(object string) (*remote.Payload, error) { - log.Printf("[DEBUG] Getting object %s/%s", c.container, object) - result := objects.Download(c.client, c.container, object, objects.DownloadOpts{Newest: true}) - - // Extract any errors from result - _, err := result.Extract() - if err != nil { - return nil, err - } - - bytes, err := result.ExtractContent() - if err != nil { - return nil, err - } - - hash := md5.Sum(bytes) - payload := &remote.Payload{ - Data: bytes, - MD5: hash[:md5.Size], - } - - return payload, nil -} - -func (c *RemoteClient) put(object string, data []byte, deleteAfter int, ifNoneMatch string) error { - log.Printf("[DEBUG] Writing object in %s/%s", c.container, object) - if err := c.ensureContainerExists(); err != nil { - return err - } - - contentType := "application/json" - contentLength := int64(len(data)) - - createOpts := objects.CreateOpts{ - Content: bytes.NewReader(data), - ContentType: contentType, - ContentLength: int64(contentLength), - } - - if deleteAfter >= 0 { - createOpts.DeleteAfter = deleteAfter - } - - if ifNoneMatch != "" { - createOpts.IfNoneMatch = ifNoneMatch - } - - result := objects.Create(c.client, c.container, object, createOpts) - if result.Err != nil { - return result.Err - } - - return nil -} - -func (c *RemoteClient) deleteContainer() error { - log.Printf("[DEBUG] Deleting container %s", c.container) - - warning := ` -WARNING: Waiting for container %s deletion timed out. -It may have been left in your Openstack account and may incur storage charges. -error was: %s -` - - deadline := time.Now().Add(deleteRetryTimeout) - - // Swift is eventually consistent; we have to retry until - // all objects are effectively deleted to delete the container - // If we still have objects in the container, or raise - // an error if deadline is reached - for { - if time.Now().Before(deadline) { - // Remove any objects - c.cleanObjects() - - // Delete the container - log.Printf("[DEBUG] Deleting container %s", c.container) - deleteResult := containers.Delete(c.client, c.container) - if deleteResult.Err != nil { - // container is not found, thus has been deleted - if _, ok := deleteResult.Err.(gophercloud.ErrDefault404); ok { - return nil - } - - // 409 http error is raised when deleting a container with - // remaining objects - if respErr, ok := deleteResult.Err.(gophercloud.ErrUnexpectedResponseCode); ok && respErr.Actual == 409 { - time.Sleep(deleteRetryPollInterval) - log.Printf("[DEBUG] Remaining objects, failed to delete container, retrying...") - continue - } - - return fmt.Errorf(warning, deleteResult.Err) - } - return nil - } - - return fmt.Errorf(warning, c.container, "timeout reached") - } - -} - -// Helper function to delete Swift objects within a container -func (c *RemoteClient) cleanObjects() error { - // Get a slice of object names - objectNames, err := c.objectNames(c.container) - if err != nil { - return err - } - - for _, object := range objectNames { - log.Printf("[DEBUG] Deleting object %s from container %s", object, c.container) - result := objects.Delete(c.client, c.container, object, nil) - if result.Err == nil { - continue - } - - // if object is not found, it has already been deleted - if _, ok := result.Err.(gophercloud.ErrDefault404); !ok { - return fmt.Errorf("Error deleting object %s from container %s: %v", object, c.container, result.Err) - } - } - return nil - -} - -func (c *RemoteClient) delete(object string) error { - log.Printf("[DEBUG] Deleting object %s/%s", c.container, object) - - result := objects.Delete(c.client, c.container, object, nil) - - if result.Err != nil { - return result.Err - } - return nil -} - -func (c *RemoteClient) writeLockInfo(info *statemgr.LockInfo, deleteAfter time.Duration, ifNoneMatch string) error { - err := c.put(c.lockFilePath(), info.Marshal(), int(deleteAfter.Seconds()), ifNoneMatch) - - if httpErr, ok := err.(gophercloud.ErrUnexpectedResponseCode); ok && httpErr.Actual == 412 { - log.Printf("[DEBUG] Couldn't write lock %s. One already exists.", info.ID) - info2, err2 := c.lockInfo() - if err2 != nil { - return fmt.Errorf("Couldn't read lock info: %v", err2) - } - - return c.lockError(err, info2) - } - - if err != nil { - return c.lockError(err, nil) - } - - return nil -} - -func (c *RemoteClient) lockError(err error, conflictingLock *statemgr.LockInfo) *statemgr.LockError { - lockErr := &statemgr.LockError{ - Err: err, - Info: conflictingLock, - } - - return lockErr -} - -// lockInfo reads the lock file, parses its contents and returns the parsed -// LockInfo struct. -func (c *RemoteClient) lockInfo() (*statemgr.LockInfo, error) { - raw, err := c.get(c.lockFilePath()) - if err != nil { - return nil, err - } - - info := &statemgr.LockInfo{} - - if err := json.Unmarshal(raw.Data, info); err != nil { - return nil, err - } - - return info, nil -} - -func (c *RemoteClient) lockRenewPeriodic(ctx context.Context, info *statemgr.LockInfo) error { - log.Printf("[DEBUG] Renew lock %v", info) - - waitDur := lockRenewInterval - lastRenewTime := time.Now() - var lastErr error - for { - if time.Since(lastRenewTime) > lockTTL { - return lastErr - } - select { - case <-time.After(waitDur): - c.mu.Lock() - // Unlock may have released the mu.Lock - // in which case we shouldn't renew the lock - select { - case <-ctx.Done(): - log.Printf("[DEBUG] Stopping Periodic renew of lock %v", info) - return nil - default: - } - - info2, err := c.lockInfo() - if _, ok := err.(gophercloud.ErrDefault404); ok { - log.Println("[DEBUG] Lock has expired trying to reacquire.") - err = nil - } - - if err == nil && (info2 == nil || info.ID == info2.ID) { - info2 = info - log.Printf("[DEBUG] Renewing lock %v.", info) - err = c.writeLockInfo(info, lockTTL, "") - } - - c.mu.Unlock() - - if err != nil { - log.Printf("[ERROR] could not reacquire lock (%v): %s", info, err) - waitDur = time.Second - lastErr = err - continue - } - - // conflicting lock - if info2.ID != info.ID { - return c.lockError(fmt.Errorf("lock id %q does not match existing lock %q", info.ID, info2.ID), info2) - } - - waitDur = lockRenewInterval - lastRenewTime = time.Now() - - case <-ctx.Done(): - log.Printf("[DEBUG] Stopping Periodic renew of lock %s", info.ID) - return nil - } - } -} - -func (c *RemoteClient) lockFilePath() string { - return c.objectName + lockSuffix -} - -func (c *RemoteClient) ensureContainerExists() error { - containerOpts := &containers.CreateOpts{} - - if c.archive { - log.Printf("[DEBUG] Creating archive container %s", c.archiveContainer) - result := containers.Create(c.client, c.archiveContainer, nil) - if result.Err != nil { - log.Printf("[DEBUG] Error creating archive container %s: %s", c.archiveContainer, result.Err) - return result.Err - } - - log.Printf("[DEBUG] Enabling Versioning on container %s", c.container) - containerOpts.VersionsLocation = c.archiveContainer - } - - log.Printf("[DEBUG] Creating container %s", c.container) - result := containers.Create(c.client, c.container, containerOpts) - if result.Err != nil { - return result.Err - } - - return nil -} - -// Helper function to get a list of objects in a Swift container -func (c *RemoteClient) objectNames(container string) (objectNames []string, err error) { - _ = objects.List(c.client, container, nil).EachPage(func(page pagination.Page) (bool, error) { - // Get a slice of object names - names, err := objects.ExtractNames(page) - if err != nil { - return false, fmt.Errorf("Error extracting object names from page: %s", err) - } - for _, object := range names { - objectNames = append(objectNames, object) - } - - return true, nil - }) - return -} diff --git a/internal/backend/remote-state/swift/client_test.go b/internal/backend/remote-state/swift/client_test.go deleted file mode 100644 index 76fcf798e2..0000000000 --- a/internal/backend/remote-state/swift/client_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package swift - -import ( - "fmt" - "testing" - "time" - - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/states/remote" -) - -func TestRemoteClient_impl(t *testing.T) { - var _ remote.Client = new(RemoteClient) -} - -func TestRemoteClient(t *testing.T) { - testACC(t) - - container := fmt.Sprintf("terraform-state-swift-testclient-%x", time.Now().Unix()) - - b := backend.TestBackendConfig(t, New(), backend.TestWrapConfig(map[string]interface{}{ - "container": container, - })).(*Backend) - - state, err := b.StateMgr(backend.DefaultStateName) - if err != nil { - t.Fatal(err) - } - - client := &RemoteClient{ - client: b.client, - container: b.container, - } - - defer client.deleteContainer() - - remote.TestClient(t, state.(*remote.State).Client) -} diff --git a/internal/backend/remote/backend.go b/internal/backend/remote/backend.go index 55e9b46d3a..ac55dc8c57 100644 --- a/internal/backend/remote/backend.go +++ b/internal/backend/remote/backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -12,11 +15,16 @@ import ( "sync" "time" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" svchost "github.com/hashicorp/terraform-svchost" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + backendLocal "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/states/remote" @@ -24,11 +32,7 @@ import ( "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" tfversion "github.com/hashicorp/terraform/version" - "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" - "github.com/zclconf/go-cty/cty" - - backendLocal "github.com/hashicorp/terraform/internal/backend/local" ) const ( @@ -36,9 +40,10 @@ const ( defaultParallelism = 10 stateServiceID = "state.v2" tfeServiceID = "tfe.v2.1" + genericHostname = "localterraform.com" ) -// Remote is an implementation of EnhancedBackend that performs all +// Remote is an implementation of backendrun.OperationsBackend that performs all // operations in a remote backend. type Remote struct { // CLI and Colorize control the CLI output. If CLI is nil then no CLI @@ -73,10 +78,10 @@ type Remote struct { // services is used for service discovery services *disco.Disco - // local, if non-nil, will be used for all enhanced behavior. This + // local, if non-nil, will be used for all OperationsBackend behavior. This // allows local behavior with the remote backend functioning as remote // state storage backend. - local backend.Enhanced + local backendrun.OperationsBackend // forceLocal, if true, will force the use of the local backend. forceLocal bool @@ -92,8 +97,8 @@ type Remote struct { } var _ backend.Backend = (*Remote)(nil) -var _ backend.Enhanced = (*Remote)(nil) -var _ backend.Local = (*Remote)(nil) +var _ backendrun.OperationsBackend = (*Remote)(nil) +var _ backendrun.Local = (*Remote)(nil) // New creates a new initialized remote backend. func New(services *disco.Disco) *Remote { @@ -102,7 +107,7 @@ func New(services *disco.Disco) *Remote { } } -// ConfigSchema implements backend.Enhanced. +// ConfigSchema implements backend.Backend. func (b *Remote) ConfigSchema() *configschema.Block { return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -194,7 +199,29 @@ func (b *Remote) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { return obj, diags } -// Configure implements backend.Enhanced. +func (b *Remote) ServiceDiscoveryAliases() ([]backendrun.HostAlias, error) { + aliasHostname, err := svchost.ForComparison(genericHostname) + if err != nil { + // This should never happen because the hostname is statically defined. + return nil, fmt.Errorf("failed to create backend alias from alias %q. The hostname is not in the correct format. This is a bug in the backend", genericHostname) + } + + targetHostname, err := svchost.ForComparison(b.hostname) + if err != nil { + // This should never happen because the 'to' alias is the backend host, which has likely + // already been evaluated as a svchost.Hostname by now + return nil, fmt.Errorf("failed to create backend alias to target %q. The hostname is not in the correct format", b.hostname) + } + + return []backendrun.HostAlias{ + { + From: aliasHostname, + To: targetHostname, + }, + }, nil +} + +// Configure implements backend.Backend. func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if obj.IsNull() { @@ -518,7 +545,7 @@ func (b *Remote) retryLogHook(attemptNum int, resp *http.Response) { } } -// Workspaces implements backend.Enhanced. +// Workspaces implements backend.Backend. func (b *Remote) Workspaces() ([]string, error) { if b.prefix == "" { return nil, backend.ErrWorkspacesNotSupported @@ -581,8 +608,8 @@ func (b *Remote) WorkspaceNamePattern() string { return "" } -// DeleteWorkspace implements backend.Enhanced. -func (b *Remote) DeleteWorkspace(name string) error { +// DeleteWorkspace implements backend.Backend. +func (b *Remote) DeleteWorkspace(name string, _ bool) error { if b.workspace == "" && name == backend.DefaultStateName { return backend.ErrDefaultWorkspaceNotSupported } @@ -609,7 +636,7 @@ func (b *Remote) DeleteWorkspace(name string) error { return client.Delete() } -// StateMgr implements backend.Enhanced. +// StateMgr implements backend.Backend. func (b *Remote) StateMgr(name string) (statemgr.Full, error) { if b.workspace == "" && name == backend.DefaultStateName { return nil, backend.ErrDefaultWorkspaceNotSupported @@ -671,7 +698,18 @@ func (b *Remote) StateMgr(name string) (statemgr.Full, error) { runID: os.Getenv("TFE_RUN_ID"), } - return &remote.State{Client: client}, nil + return &remote.State{ + Client: client, + + // client.runID will be set if we're running in a HCP Terraform + // or Terraform Enterprise remote execution environment, in which + // case we'll disable intermediate snapshots to avoid extra storage + // costs for Terraform Enterprise customers. + // Other implementations of the remote state protocol should not run + // in contexts where there's a "TFE Run ID" and so are not affected + // by this special case. + DisableIntermediateSnapshots: client.runID != "", + }, nil } func isLocalExecutionMode(execMode string) bool { @@ -706,8 +744,8 @@ func (b *Remote) fetchWorkspace(ctx context.Context, organization string, name s return w, nil } -// Operation implements backend.Enhanced. -func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { +// Operation implements backendrun.OperationsBackend. +func (b *Remote) Operation(ctx context.Context, op *backendrun.Operation) (*backendrun.RunningOperation, error) { w, err := b.fetchWorkspace(ctx, b.organization, op.Workspace) if err != nil { @@ -721,7 +759,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend // - Workspace configured for local operations, in which case the remote // version is meaningless; // - Forcing local operations with a remote backend, which should only - // happen in the Terraform Cloud worker, in which case the Terraform + // happen in the HCP Terraform worker, in which case the Terraform // versions by definition match. b.IgnoreVersionConflict() @@ -738,13 +776,13 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend op.Workspace = w.Name // Determine the function to call for our operation - var f func(context.Context, context.Context, *backend.Operation, *tfe.Workspace) (*tfe.Run, error) + var f func(context.Context, context.Context, *backendrun.Operation, *tfe.Workspace) (*tfe.Run, error) switch op.Type { - case backend.OperationTypePlan: + case backendrun.OperationTypePlan: f = b.opPlan - case backend.OperationTypeApply: + case backendrun.OperationTypeApply: f = b.opApply - case backend.OperationTypeRefresh: + case backendrun.OperationTypeRefresh: return nil, fmt.Errorf( "\n\nThe \"refresh\" operation is not supported when using the \"remote\" backend. " + "Use \"terraform apply -refresh-only\" instead.") @@ -759,7 +797,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend // Build our running operation // the runninCtx is only used to block until the operation returns. runningCtx, done := context.WithCancel(context.Background()) - runningOp := &backend.RunningOperation{ + runningOp := &backendrun.RunningOperation{ Context: runningCtx, PlanEmpty: true, } @@ -791,7 +829,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend } if r == nil && opErr == context.Canceled { - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure return } @@ -818,7 +856,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend } if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure } } }() @@ -827,7 +865,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend return runningOp, nil } -func (b *Remote) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { +func (b *Remote) cancel(cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error { if r.Actions.IsCancelable { // Only ask if the remote operation should be canceled // if the auto approve flag is not set. @@ -914,43 +952,53 @@ func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.D return nil } - remoteVersion, err := version.NewSemver(workspace.TerraformVersion) + remoteConstraint, err := version.NewConstraint(workspace.TerraformVersion) if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error looking up workspace", - fmt.Sprintf("Invalid Terraform version: %s", err), - )) + message := fmt.Sprintf( + "The remote workspace specified an invalid Terraform version or constraint (%s), "+ + "and it isn't possible to determine whether the local Terraform version (%s) is compatible.", + workspace.TerraformVersion, + tfversion.String(), + ) + diags = diags.Append(incompatibleWorkspaceTerraformVersion(message, b.ignoreVersionConflict)) return diags } - v014 := version.Must(version.NewSemver("0.14.0")) - if tfversion.SemVer.LessThan(v014) || remoteVersion.LessThan(v014) { - // Versions of Terraform prior to 0.14.0 will refuse to load state files - // written by a newer version of Terraform, even if it is only a patch - // level difference. As a result we require an exact match. - if tfversion.SemVer.Equal(remoteVersion) { - return diags + remoteVersion, _ := version.NewSemver(workspace.TerraformVersion) + + if remoteVersion != nil && remoteVersion.Prerelease() == "" { + v014 := version.Must(version.NewSemver("0.14.0")) + v130 := version.Must(version.NewSemver("1.3.0")) + + // Versions from 0.14 through the early 1.x series should be compatible + // (though we don't know about 1.3 yet). + if remoteVersion.GreaterThanOrEqual(v014) && remoteVersion.LessThan(v130) { + early1xCompatible, err := version.NewConstraint(fmt.Sprintf(">= 0.14.0, < %s", v130.String())) + if err != nil { + panic(err) + } + remoteConstraint = early1xCompatible + } + + // Any future new state format will require at least a minor version + // increment, so x.y.* will always be compatible with each other. + if remoteVersion.GreaterThanOrEqual(v130) { + rwvs := remoteVersion.Segments64() + if len(rwvs) >= 3 { + // ~> x.y.0 + minorVersionCompatible, err := version.NewConstraint(fmt.Sprintf("~> %d.%d.0", rwvs[0], rwvs[1])) + if err != nil { + panic(err) + } + remoteConstraint = minorVersionCompatible + } } } - if tfversion.SemVer.GreaterThanOrEqual(v014) && remoteVersion.GreaterThanOrEqual(v014) { - // Versions of Terraform after 0.14.0 should be compatible with each - // other. At the time this code was written, the only constraints we - // are aware of are: - // - // - 0.14.0 is guaranteed to be compatible with versions up to but not - // including 1.3.0 - v130 := version.Must(version.NewSemver("1.3.0")) - if tfversion.SemVer.LessThan(v130) && remoteVersion.LessThan(v130) { - return diags - } - // - Any new Terraform state version will require at least minor patch - // increment, so x.y.* will always be compatible with each other - tfvs := tfversion.SemVer.Segments64() - rwvs := remoteVersion.Segments64() - if len(tfvs) == 3 && len(rwvs) == 3 && tfvs[0] == rwvs[0] && tfvs[1] == rwvs[1] { - return diags - } + + fullTfversion := version.Must(version.NewSemver(tfversion.String())) + + if remoteConstraint.Check(fullTfversion) { + return diags } // Even if ignoring version conflicts, it may still be useful to call this @@ -981,6 +1029,19 @@ func (b *Remote) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.D return diags } +func incompatibleWorkspaceTerraformVersion(message string, ignoreVersionConflict bool) tfdiags.Diagnostic { + severity := tfdiags.Error + suggestion := ignoreRemoteVersionHelp + if ignoreVersionConflict { + severity = tfdiags.Warning + suggestion = "" + } + description := strings.TrimSpace(fmt.Sprintf("%s\n\n%s", message, suggestion)) + return tfdiags.Sourceless(severity, "Incompatible Terraform version", description) +} + +const ignoreRemoteVersionHelp = "If you're sure you want to upgrade the state, you can force Terraform to continue using the -ignore-remote-version flag. This may result in an unusable workspace." + func (b *Remote) IsLocalOperations() bool { return b.forceLocal } diff --git a/internal/backend/remote/backend_apply.go b/internal/backend/remote/backend_apply.go index ef89466a23..a0f4cdbd76 100644 --- a/internal/backend/remote/backend_apply.go +++ b/internal/backend/remote/backend_apply.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -9,13 +12,14 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/internal/backend" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) -func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Remote) opApply(stopCtx, cancelCtx context.Context, op *backendrun.Operation, w *tfe.Workspace) (*tfe.Run, error) { log.Printf("[INFO] backend/remote: starting Apply operation") var diags tfdiags.Diagnostics diff --git a/internal/backend/remote/backend_apply_test.go b/internal/backend/remote/backend_apply_test.go index c0e8aef204..d4d4faf176 100644 --- a/internal/backend/remote/backend_apply_test.go +++ b/internal/backend/remote/backend_apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -10,10 +13,13 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" @@ -26,19 +32,18 @@ import ( "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" tfversion "github.com/hashicorp/terraform/version" - "github.com/mitchellh/cli" ) -func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationApply(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() return testOperationApplyWithTimeout(t, configDir, 0) } -func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) @@ -50,12 +55,12 @@ func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time. depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) - return &backend.Operation{ + return &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanRefresh: true, StateLocker: clistate.NewLocker(timeout, stateLockerView), - Type: backend.OperationTypeApply, + Type: backendrun.OperationTypeApply, View: operationView, DependencyLocks: depLocks, }, configCleanup, done @@ -83,7 +88,7 @@ func TestRemote_applyBasic(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -131,7 +136,7 @@ func TestRemote_applyCanceled(t *testing.T) { run.Stop() <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -171,7 +176,7 @@ func TestRemote_applyWithoutPermissions(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -210,7 +215,7 @@ func TestRemote_applyWithVCS(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -243,7 +248,7 @@ func TestRemote_applyWithParallelism(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -260,7 +265,7 @@ func TestRemote_applyWithPlan(t *testing.T) { op, configCleanup, done := testOperationApply(t, "./testdata/apply") defer configCleanup() - op.PlanFile = &planfile.Reader{} + op.PlanFile = planfile.NewWrappedLocal(&planfile.Reader{}) op.Workspace = backend.DefaultStateName run, err := b.Operation(context.Background(), op) @@ -270,7 +275,7 @@ func TestRemote_applyWithPlan(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -300,7 +305,7 @@ func TestRemote_applyWithoutRefresh(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -339,7 +344,7 @@ func TestRemote_applyWithoutRefreshIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -369,7 +374,7 @@ func TestRemote_applyWithRefreshOnly(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -408,7 +413,7 @@ func TestRemote_applyWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -440,7 +445,7 @@ func TestRemote_applyWithTarget(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected apply operation to succeed") } if run.PlanEmpty { @@ -483,7 +488,7 @@ func TestRemote_applyWithTargetIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -515,7 +520,7 @@ func TestRemote_applyWithReplace(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { @@ -556,7 +561,7 @@ func TestRemote_applyWithReplaceIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -586,7 +591,7 @@ func TestRemote_applyWithVariables(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -612,7 +617,7 @@ func TestRemote_applyNoConfig(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -647,7 +652,7 @@ func TestRemote_applyNoChanges(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if !run.PlanEmpty { @@ -685,7 +690,7 @@ func TestRemote_applyNoApprove(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -725,7 +730,7 @@ func TestRemote_applyAutoApprove(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -800,7 +805,7 @@ func TestRemote_applyApprovedExternally(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -874,7 +879,7 @@ func TestRemote_applyDiscardedExternally(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -931,7 +936,7 @@ func TestRemote_applyWithAutoApply(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -987,7 +992,7 @@ func TestRemote_applyForceLocal(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1050,7 +1055,7 @@ func TestRemote_applyWorkspaceWithoutOperations(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1170,7 +1175,7 @@ func TestRemote_applyDestroy(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1216,7 +1221,7 @@ func TestRemote_applyDestroyNoConfig(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1250,7 +1255,7 @@ func TestRemote_applyPolicyPass(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1298,7 +1303,7 @@ func TestRemote_applyPolicyHardFail(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -1353,7 +1358,7 @@ func TestRemote_applyPolicySoftFail(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1400,7 +1405,7 @@ func TestRemote_applyPolicySoftFailAutoApproveSuccess(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected apply operation to success due to auto-approve") } @@ -1466,7 +1471,7 @@ func TestRemote_applyPolicySoftFailAutoApply(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1508,7 +1513,7 @@ func TestRemote_applyWithRemoteError(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if run.Result.ExitStatus() != 1 { @@ -1627,7 +1632,7 @@ func TestRemote_applyVersionCheck(t *testing.T) { if tc.wantErr != "" { // ASSERT: if the test case wants an error, check for failure // and the error message - if run.Result != backend.OperationFailure { + if run.Result != backendrun.OperationFailure { t.Fatalf("expected run to fail, but result was %#v", run.Result) } errOutput := output.Stderr() @@ -1637,7 +1642,7 @@ func TestRemote_applyVersionCheck(t *testing.T) { } else { // ASSERT: otherwise, check for success and appropriate output // based on whether the run should be local or remote - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } output := b.CLI.(*cli.MockUi).OutputWriter.String() diff --git a/internal/backend/remote/backend_common.go b/internal/backend/remote/backend_common.go index 710cdfb84f..3ba562e4fd 100644 --- a/internal/backend/remote/backend_common.go +++ b/internal/backend/remote/backend_common.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -12,7 +15,8 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/backend" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/terraform" @@ -43,7 +47,34 @@ func backoff(min, max float64, iter int) time.Duration { return time.Duration(backoff) * time.Millisecond } -func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Remote) waitForPostPlanTasks(stopCtx, cancelCtx context.Context, r *tfe.Run) error { + taskStages := make(taskStages) + result, err := b.client.Runs.ReadWithOptions(stopCtx, r.ID, &tfe.RunReadOptions{ + Include: []tfe.RunIncludeOpt{tfe.RunTaskStages}, + }) + if err == nil { + for _, t := range result.TaskStages { + if t != nil { + taskStages[t.Stage] = t + } + } + } else { + // This error would be expected for older versions of TFE that do not allow + // fetching task_stages. + if !strings.HasSuffix(err.Error(), "Invalid include parameter") { + generalError("Failed to retrieve run", err) + } + } + + if stage, ok := taskStages[tfe.PostPlan]; ok { + if err := b.waitTaskStage(stopCtx, cancelCtx, r, stage.ID); err != nil { + return err + } + } + return nil +} + +func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backendrun.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) { started := time.Now() updated := started for i := 0; ; i++ { @@ -132,7 +163,7 @@ func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Oper case tfe.RunApplied, tfe.RunCanceled, tfe.RunDiscarded, tfe.RunErrored: continue case tfe.RunPlanned: - if op.Type == backend.OperationTypePlan { + if op.Type == backendrun.OperationTypePlan { continue } } @@ -216,7 +247,7 @@ func (b *Remote) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Oper // individual variable values are invalid. That's okay because we only use this // result to hint the user to set variables a different way. It's always the // remote system's responsibility to do final validation of the input. -func (b *Remote) hasExplicitVariableValues(op *backend.Operation) bool { +func (b *Remote) hasExplicitVariableValues(op *backendrun.Operation) bool { // Load the configuration using the caller-provided configuration loader. config, _, configDiags := op.ConfigLoader.LoadConfigWithSnapshot(op.ConfigDir) if configDiags.HasErrors() { @@ -231,7 +262,7 @@ func (b *Remote) hasExplicitVariableValues(op *backend.Operation) bool { // goal here is just to make a best effort count of how many variable // values are coming from -var or -var-file CLI arguments so that we can // hint the user that those are not supported for remote operations. - variables, _ := backend.ParseVariableValues(op.Variables, config.Module.Variables) + variables, _ := backendrun.ParseVariableValues(op.Variables, config.Module.Variables) // Check for explicitly-defined (-var and -var-file) variables, which the // remote backend does not support. All other source types are okay, @@ -247,7 +278,7 @@ func (b *Remote) hasExplicitVariableValues(op *backend.Operation) bool { return false } -func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { +func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error { if r.CostEstimate == nil { return nil } @@ -303,7 +334,7 @@ func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Op b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount))) b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr))) - if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply { + if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backendrun.OperationTypeApply { b.CLI.Output("\n------------------------------------------------------------------------") } } @@ -335,14 +366,14 @@ func (b *Remote) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Op b.CLI.Output("\n------------------------------------------------------------------------") return nil case tfe.CostEstimateCanceled: - return fmt.Errorf(msgPrefix + " canceled.") + return fmt.Errorf("%s canceled.", msgPrefix) default: return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status) } } } -func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { +func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error { if b.CLI != nil { b.CLI.Output("\n------------------------------------------------------------------------\n") } @@ -407,25 +438,25 @@ func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Ope switch pc.Status { case tfe.PolicyPasses: - if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { + if (r.HasChanges && op.Type == backendrun.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { b.CLI.Output("\n------------------------------------------------------------------------") } continue case tfe.PolicyErrored: - return fmt.Errorf(msgPrefix + " errored.") + return fmt.Errorf("%s errored.", msgPrefix) case tfe.PolicyHardFailed: - return fmt.Errorf(msgPrefix + " hard failed.") + return fmt.Errorf("%s hard failed.", msgPrefix) case tfe.PolicySoftFailed: - runUrl := fmt.Sprintf(runHeader, b.hostname, b.organization, op.Workspace, r.ID) + runURL := fmt.Sprintf(runHeaderErr, b.hostname, b.organization, op.Workspace, r.ID) - if op.Type == backend.OperationTypePlan || op.UIOut == nil || op.UIIn == nil || + if op.Type == backendrun.OperationTypePlan || op.UIOut == nil || op.UIIn == nil || !pc.Actions.IsOverridable || !pc.Permissions.CanOverride { - return fmt.Errorf(msgPrefix + " soft failed.\n" + runUrl) + return fmt.Errorf("%s soft failed.\n%s", msgPrefix, runURL) } if op.AutoApprove { if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { - return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) + return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runURL), err) } } else { opts := &terraform.InputOpts{ @@ -435,17 +466,16 @@ func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Ope } err = b.confirm(stopCtx, op, opts, r, "override") if err != nil && err != errRunOverridden { - return fmt.Errorf( - fmt.Sprintf("Failed to override: %s\n%s\n", err.Error(), runUrl), - ) + return fmt.Errorf("Failed to override: %w\n%s\n", err, runURL) } if err != errRunOverridden { if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { - return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) + return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runURL), err) } } else { - b.CLI.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runUrl)) + runURL := fmt.Sprintf(runHeader, b.hostname, b.organization, op.Workspace, r.ID) + b.CLI.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runURL)) } } @@ -460,7 +490,7 @@ func (b *Remote) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Ope return nil } -func (b *Remote) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { +func (b *Remote) confirm(stopCtx context.Context, op *backendrun.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { doneCtx, cancel := context.WithCancel(stopCtx) result := make(chan error, 2) diff --git a/internal/backend/remote/backend_context.go b/internal/backend/remote/backend_context.go index 372d1cf3d0..9f85f0149d 100644 --- a/internal/backend/remote/backend_context.go +++ b/internal/backend/remote/backend_context.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -9,18 +12,20 @@ import ( tfe "github.com/hashicorp/go-tfe" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) -// Context implements backend.Local. -func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) { +// Context implements backendrun.Local. +func (b *Remote) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - ret := &backend.LocalRun{ + ret := &backendrun.LocalRun{ PlanOpts: &terraform.PlanOpts{ Mode: op.PlanMode, Targets: op.Targets, @@ -115,7 +120,7 @@ func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Fu } if tfeVariables != nil { if op.Variables == nil { - op.Variables = make(map[string]backend.UnparsedVariableValue) + op.Variables = make(map[string]backendrun.UnparsedVariableValue) } for _, v := range tfeVariables.Items { if v.Category == tfe.CategoryTerraform { @@ -130,7 +135,7 @@ func (b *Remote) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Fu } if op.Variables != nil { - variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables) + variables, varDiags := backendrun.ParseVariableValues(op.Variables, config.Module.Variables) diags = diags.Append(varDiags) if diags.HasErrors() { return nil, nil, diags @@ -183,7 +188,7 @@ func (b *Remote) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName st return remoteWorkspace.ID, nil } -func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues { +func stubAllVariables(vv map[string]backendrun.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues { ret := make(terraform.InputValues, len(decls)) for name, cfg := range decls { @@ -210,14 +215,14 @@ func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[str return ret } -// remoteStoredVariableValue is a backend.UnparsedVariableValue implementation +// remoteStoredVariableValue is a backendrun.UnparsedVariableValue implementation // that translates from the go-tfe representation of stored variables into // the Terraform Core backend representation of variables. type remoteStoredVariableValue struct { definition *tfe.Variable } -var _ backend.UnparsedVariableValue = (*remoteStoredVariableValue)(nil) +var _ backendrun.UnparsedVariableValue = (*remoteStoredVariableValue)(nil) func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -286,7 +291,7 @@ func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariablePars Value: val, // We mark these as "from input" with the rationale that entering - // variable values into the Terraform Cloud or Enterprise UI is, + // variable values into the HCP Terraform or Enterprise UI is, // roughly speaking, a similar idea to entering variable values at // the interactive CLI prompts. It's not a perfect correspondance, // but it's closer than the other options. diff --git a/internal/backend/remote/backend_context_test.go b/internal/backend/remote/backend_context_test.go index f3a133421a..7fc6b6dd7e 100644 --- a/internal/backend/remote/backend_context_test.go +++ b/internal/backend/remote/backend_context_test.go @@ -1,14 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( "context" - "github.com/hashicorp/terraform/internal/terraform" - "github.com/hashicorp/terraform/internal/tfdiags" "reflect" "testing" tfe "github.com/hashicorp/go-tfe" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -16,7 +20,8 @@ import ( "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" - "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestRemoteStoredVariableValue(t *testing.T) { @@ -86,7 +91,7 @@ func TestRemoteStoredVariableValue(t *testing.T) { "HCL computation": { // This (stored expressions containing computation) is not a case // we intentionally supported, but it became possible for remote - // operations in Terraform 0.12 (due to Terraform Cloud/Enterprise + // operations in Terraform 0.12 (due to HCP Terraform and Terraform Enterprise // just writing the HCL verbatim into generated `.tfvars` files). // We support it here for consistency, and we continue to support // it in both places for backward-compatibility. In practice, @@ -182,7 +187,7 @@ func TestRemoteContextWithVars(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() workspaceID, err := b.getRemoteWorkspaceID(context.Background(), backend.DefaultStateName) @@ -193,7 +198,7 @@ func TestRemoteContextWithVars(t *testing.T) { streams, _ := terminal.StreamsForTesting(t) view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewLocker(0, view), @@ -249,12 +254,12 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { varValue3 := "value3" tests := map[string]struct { - localVariables map[string]backend.UnparsedVariableValue + localVariables map[string]backendrun.UnparsedVariableValue remoteVariables []*tfe.VariableCreateOptions expectedVariables terraform.InputValues }{ "no local variables": { - map[string]backend.UnparsedVariableValue{}, + map[string]backendrun.UnparsedVariableValue{}, []*tfe.VariableCreateOptions{ { Key: &varName1, @@ -303,7 +308,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { }, }, "single conflicting local variable": { - map[string]backend.UnparsedVariableValue{ + map[string]backendrun.UnparsedVariableValue{ varName3: testUnparsedVariableValue(varValue3), }, []*tfe.VariableCreateOptions{ @@ -352,7 +357,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { }, }, "no conflicting local variable": { - map[string]backend.UnparsedVariableValue{ + map[string]backendrun.UnparsedVariableValue{ varName3: testUnparsedVariableValue(varValue3), }, []*tfe.VariableCreateOptions{ @@ -405,7 +410,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() workspaceID, err := b.getRemoteWorkspaceID(context.Background(), backend.DefaultStateName) @@ -416,7 +421,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { streams, _ := terminal.StreamsForTesting(t) view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewLocker(0, view), diff --git a/internal/backend/remote/backend_plan.go b/internal/backend/remote/backend_plan.go index ca74d18b64..0fdea063e6 100644 --- a/internal/backend/remote/backend_plan.go +++ b/internal/backend/remote/backend_plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -16,7 +19,8 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/internal/backend" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/tfdiags" @@ -24,7 +28,7 @@ import ( var planConfigurationVersionsPollInterval = 500 * time.Millisecond -func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backendrun.Operation, w *tfe.Workspace) (*tfe.Run, error) { log.Printf("[INFO] backend/remote: starting Plan operation") var diags tfdiags.Diagnostics @@ -66,6 +70,15 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio )) } + if op.GenerateConfigOut != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Generating configuration is not currently supported", + `The "remote" backend does not currently support generating resource configuration `+ + `as part of a plan.`, + )) + } + if b.hasExplicitVariableValues(op) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -171,10 +184,10 @@ func (b *Remote) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operatio return b.plan(stopCtx, cancelCtx, op, w) } -func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backendrun.Operation, w *tfe.Workspace) (*tfe.Run, error) { if b.CLI != nil { header := planDefaultHeader - if op.Type == backend.OperationTypeApply { + if op.Type == backendrun.OperationTypeApply { header = applyDefaultHeader } b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n")) @@ -182,7 +195,7 @@ func (b *Remote) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, configOptions := tfe.ConfigurationVersionCreateOptions{ AutoQueueRuns: tfe.Bool(false), - Speculative: tfe.Bool(op.Type == backend.OperationTypePlan), + Speculative: tfe.Bool(op.Type == backendrun.OperationTypePlan), } cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions) @@ -245,19 +258,24 @@ in order to capture the filesystem context the remote workspace expects: } } + log.Printf("[TRACE] backend/remote: starting configuration upload at %q", configDir) err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir) if err != nil { return nil, generalError("Failed to upload configuration files", err) } + log.Printf("[TRACE] backend/remote: finished configuration upload") uploaded := false for i := 0; i < 60 && !uploaded; i++ { select { case <-stopCtx.Done(): + log.Printf("[TRACE] backend/remote: deadline reached while waiting for configuration status") return nil, context.Canceled case <-cancelCtx.Done(): + log.Printf("[TRACE] backend/remote: operation cancelled while waiting for configuration status") return nil, context.Canceled case <-time.After(planConfigurationVersionsPollInterval): + log.Printf("[TRACE] backend/remote: reading configuration status") cv, err = b.client.ConfigurationVersions.Read(stopCtx, cv.ID) if err != nil { return nil, generalError("Failed to retrieve configuration version", err) @@ -274,6 +292,7 @@ in order to capture the filesystem context the remote workspace expects: "Failed to upload configuration files", errors.New("operation timed out")) } + log.Printf("[TRACE] backend/remote: configuration uploaded and ready") runOptions := tfe.RunCreateOptions{ ConfigurationVersion: cv, Refresh: tfe.Bool(op.PlanRefresh), @@ -398,6 +417,15 @@ in order to capture the filesystem context the remote workspace expects: return r, generalError("Failed to retrieve run", err) } + // Wait for post plan tasks to complete before proceeding. + // Otherwise, in the case of an apply, if they are still running + // when we check for whether the run is confirmable the CLI will + // uncermoniously exit before the user has a chance to confirm, or for an auto-apply to take place. + err = b.waitForPostPlanTasks(stopCtx, cancelCtx, r) + if err != nil { + return r, err + } + // If the run is canceled or errored, we still continue to the // cost-estimation and policy check phases to ensure we render any // results available. In the case of a hard-failed policy check, the @@ -431,8 +459,13 @@ Preparing the remote plan... ` const runHeader = ` -[reset][yellow]To view this run in a browser, visit: -https://%s/app/%s/%s/runs/%s[reset] +[reset][yellow]To view this run in a browser, visit:[reset] +[reset][yellow]https://%s/app/%s/%s/runs/%s[reset] +` + +const runHeaderErr = ` +To view this run in a browser, visit: +https://%s/app/%s/%s/runs/%s ` // The newline in this error is to make it look good in the CLI! diff --git a/internal/backend/remote/backend_plan_test.go b/internal/backend/remote/backend_plan_test.go index 3acf0796da..607b06a6ab 100644 --- a/internal/backend/remote/backend_plan_test.go +++ b/internal/backend/remote/backend_plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -10,9 +13,12 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" @@ -24,19 +30,18 @@ import ( "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" - "github.com/mitchellh/cli" ) -func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationPlan(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() return testOperationPlanWithTimeout(t, configDir, 0) } -func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) @@ -48,12 +53,12 @@ func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.D depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) - return &backend.Operation{ + return &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanRefresh: true, StateLocker: clistate.NewLocker(timeout, stateLockerView), - Type: backend.OperationTypePlan, + Type: backendrun.OperationTypePlan, View: operationView, DependencyLocks: depLocks, }, configCleanup, done @@ -75,7 +80,7 @@ func TestRemote_planBasic(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -116,7 +121,7 @@ func TestRemote_planCanceled(t *testing.T) { run.Stop() <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -143,7 +148,7 @@ func TestRemote_planLongLine(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -188,7 +193,7 @@ func TestRemote_planWithoutPermissions(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -218,7 +223,7 @@ func TestRemote_planWithParallelism(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -235,7 +240,7 @@ func TestRemote_planWithPlan(t *testing.T) { op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() - op.PlanFile = &planfile.Reader{} + op.PlanFile = planfile.NewWrappedLocal(&planfile.Reader{}) op.Workspace = backend.DefaultStateName run, err := b.Operation(context.Background(), op) @@ -245,7 +250,7 @@ func TestRemote_planWithPlan(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -275,7 +280,7 @@ func TestRemote_planWithPath(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -305,7 +310,7 @@ func TestRemote_planWithoutRefresh(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -344,7 +349,7 @@ func TestRemote_planWithoutRefreshIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -374,7 +379,7 @@ func TestRemote_planWithRefreshOnly(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -413,7 +418,7 @@ func TestRemote_planWithRefreshOnlyIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -468,7 +473,7 @@ func TestRemote_planWithTarget(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { @@ -518,7 +523,7 @@ func TestRemote_planWithTargetIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -550,7 +555,7 @@ func TestRemote_planWithReplace(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { @@ -591,7 +596,7 @@ func TestRemote_planWithReplaceIncompatibleAPIVersion(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -621,7 +626,7 @@ func TestRemote_planWithVariables(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -647,7 +652,7 @@ func TestRemote_planNoConfig(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -676,7 +681,7 @@ func TestRemote_planNoChanges(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if !run.PlanEmpty { @@ -719,7 +724,7 @@ func TestRemote_planForceLocal(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -755,7 +760,7 @@ func TestRemote_planWithoutOperationsEntitlement(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -805,7 +810,7 @@ func TestRemote_planWorkspaceWithoutOperations(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -909,7 +914,7 @@ func TestRemote_planDestroy(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -934,7 +939,7 @@ func TestRemote_planDestroyNoConfig(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -968,7 +973,7 @@ func TestRemote_planWithWorkingDirectory(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1027,7 +1032,7 @@ func TestRemote_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1059,7 +1064,7 @@ func TestRemote_planCostEstimation(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1094,7 +1099,7 @@ func TestRemote_planPolicyPass(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1129,7 +1134,7 @@ func TestRemote_planPolicyHardFail(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -1169,7 +1174,7 @@ func TestRemote_planPolicySoftFail(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -1209,7 +1214,7 @@ func TestRemote_planWithRemoteError(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if run.Result.ExitStatus() != 1 { @@ -1245,3 +1250,33 @@ func TestRemote_planOtherError(t *testing.T) { t.Fatalf("expected error message, got: %s", err.Error()) } } + +func TestRemote_planWithGenConfigOut(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan") + defer configCleanup() + + op.GenerateConfigOut = "generated.tf" + op.Workspace = backend.DefaultStateName + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + output := done(t) + if run.Result == backendrun.OperationSuccess { + t.Fatal("expected plan operation to fail") + } + if !run.PlanEmpty { + t.Fatalf("expected plan to be empty") + } + + errOutput := output.Stderr() + if !strings.Contains(errOutput, "Generating configuration is not currently supported") { + t.Fatalf("expected error about config generation, got: %v", errOutput) + } +} diff --git a/internal/backend/remote/backend_state.go b/internal/backend/remote/backend_state.go index 54cdd0aadb..0520f2862e 100644 --- a/internal/backend/remote/backend_state.go +++ b/internal/backend/remote/backend_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -6,7 +9,9 @@ import ( "crypto/md5" "encoding/base64" "encoding/json" + "errors" "fmt" + "log" tfe "github.com/hashicorp/go-tfe" @@ -26,6 +31,22 @@ type remoteClient struct { forcePush bool } +// errorUnlockFailed is used within a retry loop to identify a non-retryable +// workspace unlock error +type errorUnlockFailed struct { + innerError error +} + +func (e errorUnlockFailed) FatalError() error { + return e.innerError +} + +func (e errorUnlockFailed) Error() string { + return e.innerError.Error() +} + +var _ Fatal = errorUnlockFailed{} + // Get the remote state. func (r *remoteClient) Get() (*remote.Payload, error) { ctx := context.Background() @@ -58,32 +79,14 @@ func (r *remoteClient) Get() (*remote.Payload, error) { }, nil } -// Put the remote state. -func (r *remoteClient) Put(state []byte) error { - ctx := context.Background() - - // Read the raw state into a Terraform state. - stateFile, err := statefile.Read(bytes.NewReader(state)) - if err != nil { - return fmt.Errorf("Error reading state: %s", err) - } - - ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues) - if err != nil { - return fmt.Errorf("Error reading output values: %s", err) - } - o, err := json.Marshal(ov) - if err != nil { - return fmt.Errorf("Error converting output values to json: %s", err) - } - +func (r *remoteClient) uploadStateFallback(ctx context.Context, stateFile *statefile.File, state []byte, jsonStateOutputs []byte) error { options := tfe.StateVersionCreateOptions{ Lineage: tfe.String(stateFile.Lineage), Serial: tfe.Int64(int64(stateFile.Serial)), MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), - State: tfe.String(base64.StdEncoding.EncodeToString(state)), Force: tfe.Bool(r.forcePush), - JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(o)), + State: tfe.String(base64.StdEncoding.EncodeToString(state)), + JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(jsonStateOutputs)), } // If we have a run ID, make sure to add it to the options @@ -93,10 +96,61 @@ func (r *remoteClient) Put(state []byte) error { } // Create the new state. - _, err = r.client.StateVersions.Create(ctx, r.workspace.ID, options) + _, err := r.client.StateVersions.Create(ctx, r.workspace.ID, options) if err != nil { r.stateUploadErr = true - return fmt.Errorf("Error uploading state: %v", err) + return fmt.Errorf("error uploading state in compatibility mode: %v", err) + } + return err +} + +// Put the remote state. +func (r *remoteClient) Put(state []byte) error { + ctx := context.Background() + + // Read the raw state into a Terraform state. + stateFile, err := statefile.Read(bytes.NewReader(state)) + if err != nil { + return fmt.Errorf("error reading state: %s", err) + } + + ov, err := jsonstate.MarshalOutputs(stateFile.State.RootOutputValues) + if err != nil { + return fmt.Errorf("error reading output values: %s", err) + } + o, err := json.Marshal(ov) + if err != nil { + return fmt.Errorf("error converting output values to json: %s", err) + } + + options := tfe.StateVersionUploadOptions{ + StateVersionCreateOptions: tfe.StateVersionCreateOptions{ + Lineage: tfe.String(stateFile.Lineage), + Serial: tfe.Int64(int64(stateFile.Serial)), + MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), + Force: tfe.Bool(r.forcePush), + JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(o)), + }, + RawState: state, + } + + // If we have a run ID, make sure to add it to the options + // so the state will be properly associated with the run. + if r.runID != "" { + options.Run = &tfe.Run{ID: r.runID} + } + + // Create the new state. + // Create the new state. + _, err = r.client.StateVersions.Upload(ctx, r.workspace.ID, options) + if errors.Is(err, tfe.ErrStateVersionUploadNotSupported) { + // Create the new state with content included in the request (Terraform Enterprise v202306-1 and below) + log.Println("[INFO] Detected that state version upload is not supported. Retrying using compatibility state upload.") + return r.uploadStateFallback(ctx, stateFile, state, o) + } + if err != nil { + r.stateUploadErr = true + return fmt.Errorf("error uploading state: %v", err) } return nil @@ -106,7 +160,7 @@ func (r *remoteClient) Put(state []byte) error { func (r *remoteClient) Delete() error { err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace.Name) if err != nil && err != tfe.ErrResourceNotFound { - return fmt.Errorf("Error deleting workspace %s: %v", r.workspace.Name, err) + return fmt.Errorf("error deleting workspace %s: %v", r.workspace.Name, err) } return nil @@ -164,7 +218,20 @@ func (r *remoteClient) Unlock(id string) error { } // Unlock the workspace. - _, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID) + // Unlock the workspace. + err := RetryBackoff(ctx, func() error { + _, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID) + if err != nil { + if errors.Is(err, tfe.ErrWorkspaceLockedStateVersionStillPending) { + // This is a retryable error. + return err + } + // This will not be retried + return &errorUnlockFailed{innerError: err} + } + return nil + }) + if err != nil { lockErr.Err = err return lockErr diff --git a/internal/backend/remote/backend_state_test.go b/internal/backend/remote/backend_state_test.go index 0503936b8e..578fe6b497 100644 --- a/internal/backend/remote/backend_state_test.go +++ b/internal/backend/remote/backend_state_test.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( "bytes" "os" + "strings" "testing" "github.com/hashicorp/terraform/internal/backend" @@ -10,6 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/internal/states/statemgr" ) func TestRemoteClient_impl(t *testing.T) { @@ -38,7 +43,48 @@ func TestRemoteClient_stateLock(t *testing.T) { remote.TestRemoteLocks(t, s1.(*remote.State).Client, s2.(*remote.State).Client) } -func TestRemoteClient_withRunID(t *testing.T) { +func TestRemoteClient_Unlock_invalidID(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + s1, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + err = s1.Unlock("no") + if err == nil { + t.Fatal("expected error, got nil") + } + + if !strings.Contains(err.Error(), "does not match existing lock ID") { + t.Fatalf("expected erroor containing \"does not match existing lock ID\", got %v", err) + } +} + +func TestRemoteClient_Unlock(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + s1, err := b.StateMgr(backend.DefaultStateName) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + id, err := s1.Lock(&statemgr.LockInfo{ + ID: "test", + }) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + err = s1.Unlock(id) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } +} + +func TestRemoteClient_Put_withRunID(t *testing.T) { // Set the TFE_RUN_ID environment variable before creating the client! if err := os.Setenv("TFE_RUN_ID", cloud.GenerateID("run-")); err != nil { t.Fatalf("error setting env var TFE_RUN_ID: %v", err) diff --git a/internal/backend/remote/backend_taskStages.go b/internal/backend/remote/backend_taskStages.go new file mode 100644 index 0000000000..47ea788aa1 --- /dev/null +++ b/internal/backend/remote/backend_taskStages.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package remote + +import ( + "context" + "fmt" + + tfe "github.com/hashicorp/go-tfe" +) + +type taskStages map[tfe.Stage]*tfe.TaskStage + +const ( + taskStageBackoffMin = 4000.0 + taskStageBackoffMax = 12000.0 +) + +// waitTaskStage waits for a task stage to complete, only informs the caller if the stage has failed in some way. +func (b *Remote) waitTaskStage(stopCtx, cancelCtx context.Context, r *tfe.Run, stageID string) error { + ctx := &IntegrationContext{ + StopContext: stopCtx, + CancelContext: cancelCtx, + } + return ctx.Poll(taskStageBackoffMin, taskStageBackoffMax, func(i int) (bool, error) { + options := tfe.TaskStageReadOptions{ + Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults, tfe.PolicyEvaluationsTaskResults}, + } + stage, err := b.client.TaskStages.Read(ctx.StopContext, stageID, &options) + if err != nil { + return false, generalError("Failed to retrieve task stage", err) + } + + switch stage.Status { + case tfe.TaskStagePending: + // Waiting for it to start + return true, nil + case tfe.TaskStageRunning: + // not a terminal status so we continue to poll + return true, nil + case tfe.TaskStagePassed: + return false, nil + case tfe.TaskStageCanceled, tfe.TaskStageErrored, tfe.TaskStageFailed: + return false, fmt.Errorf("Task Stage '%s': %s.", stage.ID, stage.Status) + case tfe.TaskStageAwaitingOverride: + return false, fmt.Errorf("Task Stage '%s' awaiting override.", stage.ID) + case tfe.TaskStageUnreachable: + return false, nil + default: + return false, fmt.Errorf("Task stage '%s' has invalid status: %s", stage.ID, stage.Status) + } + }) +} diff --git a/internal/backend/remote/backend_test.go b/internal/backend/remote/backend_test.go index ca18429b92..87c3fd40ad 100644 --- a/internal/backend/remote/backend_test.go +++ b/internal/backend/remote/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -9,18 +12,19 @@ import ( tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-svchost/disco" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/tfdiags" - tfversion "github.com/hashicorp/terraform/version" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform-svchost/disco" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" backendLocal "github.com/hashicorp/terraform/internal/backend/local" + "github.com/hashicorp/terraform/internal/tfdiags" + tfversion "github.com/hashicorp/terraform/version" ) func TestRemote(t *testing.T) { - var _ backend.Enhanced = New(nil) - var _ backend.CLI = New(nil) + var _ backendrun.OperationsBackend = New(nil) + var _ backendrun.CLI = New(nil) } func TestRemote_backendDefault(t *testing.T) { @@ -262,11 +266,11 @@ func TestRemote_addAndRemoveWorkspacesDefault(t *testing.T) { t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err) } - if err := b.DeleteWorkspace(backend.DefaultStateName); err != nil { + if err := b.DeleteWorkspace(backend.DefaultStateName, true); err != nil { t.Fatalf("expected no error, got %v", err) } - if err := b.DeleteWorkspace("prod"); err != backend.ErrWorkspacesNotSupported { + if err := b.DeleteWorkspace("prod", true); err != backend.ErrWorkspacesNotSupported { t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err) } } @@ -319,11 +323,11 @@ func TestRemote_addAndRemoveWorkspacesNoDefault(t *testing.T) { t.Fatalf("expected %#+v, got %#+v", expectedWorkspaces, states) } - if err := b.DeleteWorkspace(backend.DefaultStateName); err != backend.ErrDefaultWorkspaceNotSupported { + if err := b.DeleteWorkspace(backend.DefaultStateName, true); err != backend.ErrDefaultWorkspaceNotSupported { t.Fatalf("expected error %v, got %v", backend.ErrDefaultWorkspaceNotSupported, err) } - if err := b.DeleteWorkspace(expectedA); err != nil { + if err := b.DeleteWorkspace(expectedA, true); err != nil { t.Fatal(err) } @@ -337,7 +341,7 @@ func TestRemote_addAndRemoveWorkspacesNoDefault(t *testing.T) { t.Fatalf("expected %#+v got %#+v", expectedWorkspaces, states) } - if err := b.DeleteWorkspace(expectedB); err != nil { + if err := b.DeleteWorkspace(expectedB, true); err != nil { t.Fatal(err) } @@ -662,11 +666,106 @@ func TestRemote_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) { if len(diags) != 1 { t.Fatal("expected diag, but none returned") } - if got := diags.Err().Error(); !strings.Contains(got, "Error looking up workspace: Invalid Terraform version") { + if got := diags.Err().Error(); !strings.Contains(got, "The remote workspace specified an invalid Terraform version or constraint") { t.Fatalf("unexpected error: %s", got) } } +func TestRemote_VerifyWorkspaceTerraformVersion_versionConstraint(t *testing.T) { + b, bCleanup := testBackendDefault(t) + defer bCleanup() + + // Define our test case struct + type testCase struct { + terraformVersion string + versionConstraint string + shouldSatisfy bool + prerelease string + } + + // Create a slice of test cases + testCases := []testCase{ + { + terraformVersion: "1.8.0", + versionConstraint: "> 1.9.0", + shouldSatisfy: false, + prerelease: "", + }, + { + terraformVersion: "1.10.1", + versionConstraint: "~> 1.10.0", + shouldSatisfy: true, + prerelease: "", + }, + { + terraformVersion: "1.10.0", + versionConstraint: "> 1.9.0", + shouldSatisfy: true, + prerelease: "", + }, + { + terraformVersion: "1.8.0", + versionConstraint: "~> 1.9.0", + shouldSatisfy: false, + prerelease: "", + }, + { + terraformVersion: "1.10.0", + versionConstraint: "> v1.9.4", + shouldSatisfy: false, + prerelease: "dev", + }, + { + terraformVersion: "1.10.0", + versionConstraint: "> 1.10.0", + shouldSatisfy: false, + prerelease: "dev", + }, + } + + // Save and restore the actual version. + p := tfversion.Prerelease + v := tfversion.Version + defer func() { + tfversion.Prerelease = p + tfversion.Version = v + }() + + // Now we loop through each test case, utilizing the values of each case + // to setup our test and assert accordingly. + for _, tc := range testCases { + + tfversion.Prerelease = tc.prerelease + tfversion.Version = tc.terraformVersion + + // Update the mock remote workspace Terraform version to be a version constraint string + if _, err := b.client.Workspaces.Update( + context.Background(), + b.organization, + b.workspace, + tfe.WorkspaceUpdateOptions{ + TerraformVersion: tfe.String(tc.versionConstraint), + }, + ); err != nil { + t.Fatalf("error: %v", err) + } + diags := b.VerifyWorkspaceTerraformVersion(backend.DefaultStateName) + + if tc.shouldSatisfy { + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, but got: %v", diags.Err().Error()) + } + } else { + if len(diags) == 0 { + t.Fatal("expected diagnostic, but none returned") + } + if got := diags.Err().Error(); !strings.Contains(got, "Terraform version mismatch") { + t.Fatalf("unexpected error: %s", got) + } + } + } +} + func TestRemote_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { b, bCleanup := testBackendDefault(t) defer bCleanup() @@ -722,3 +821,29 @@ func TestRemote_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { t.Errorf("wrong summary: got %s, want %s", got, wantDetail) } } + +func TestRemote_ServiceDiscoveryAliases(t *testing.T) { + s := testServer(t) + b := New(testDisco(s)) + + diag := b.Configure(cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), // Forces aliasing to test server + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "prefix": cty.NullVal(cty.String), + }), + })) + if diag.HasErrors() { + t.Fatalf("expected no diagnostic errors, got %s", diag.Err()) + } + + aliases, err := b.ServiceDiscoveryAliases() + if err != nil { + t.Fatalf("expected no errors, got %s", err) + } + if len(aliases) != 1 { + t.Fatalf("expected 1 alias but got %d", len(aliases)) + } +} diff --git a/internal/backend/remote/cli.go b/internal/backend/remote/cli.go index 926908360e..c401b1e05b 100644 --- a/internal/backend/remote/cli.go +++ b/internal/backend/remote/cli.go @@ -1,12 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" ) -// CLIInit implements backend.CLI -func (b *Remote) CLIInit(opts *backend.CLIOpts) error { - if cli, ok := b.local.(backend.CLI); ok { +// CLIInit implements backendrun.CLI +func (b *Remote) CLIInit(opts *backendrun.CLIOpts) error { + if cli, ok := b.local.(backendrun.CLI); ok { if err := cli.CLIInit(opts); err != nil { return err } diff --git a/internal/backend/remote/cloud_integration.go b/internal/backend/remote/cloud_integration.go new file mode 100644 index 0000000000..f709dc0aa7 --- /dev/null +++ b/internal/backend/remote/cloud_integration.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package remote + +import ( + "context" + "log" + "time" +) + +// IntegrationContext is a set of data that is useful when performing HCP Terraform integration operations +type IntegrationContext struct { + StopContext context.Context + CancelContext context.Context +} + +func (s *IntegrationContext) Poll(backoffMinInterval float64, backoffMaxInterval float64, every func(i int) (bool, error)) error { + for i := 0; ; i++ { + select { + case <-s.StopContext.Done(): + log.Print("IntegrationContext.Poll: StopContext.Done() called") + return s.StopContext.Err() + case <-s.CancelContext.Done(): + log.Print("IntegrationContext.Poll: CancelContext.Done() called") + return s.CancelContext.Err() + case <-time.After(backoff(backoffMinInterval, backoffMaxInterval, i)): + // blocks for a time between min and max + } + + cont, err := every(i) + if !cont { + return err + } + } +} diff --git a/internal/backend/remote/colorize.go b/internal/backend/remote/colorize.go index 97e24e32c8..9ea2fd5d5d 100644 --- a/internal/backend/remote/colorize.go +++ b/internal/backend/remote/colorize.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( diff --git a/internal/backend/remote/remote_test.go b/internal/backend/remote/remote_test.go index f4cc3c5c28..4968c67c88 100644 --- a/internal/backend/remote/remote_test.go +++ b/internal/backend/remote/remote_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( diff --git a/internal/backend/remote/retry.go b/internal/backend/remote/retry.go new file mode 100644 index 0000000000..db70f1bd62 --- /dev/null +++ b/internal/backend/remote/retry.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package remote + +import ( + "context" + "log" + "sync/atomic" + "time" +) + +// Fatal implements a RetryBackoff func return value that, if encountered, +// signals that the func should not be retried. In that case, the error +// returned by the interface method will be returned by RetryBackoff +type Fatal interface { + FatalError() error +} + +// NonRetryableError is a simple implementation of Fatal that wraps an error +type NonRetryableError struct { + InnerError error +} + +// FatalError returns the inner error, but also implements Fatal, which +// signals to RetryBackoff that a non-retryable error occurred. +func (e NonRetryableError) FatalError() error { + return e.InnerError +} + +// Error returns the inner error string +func (e NonRetryableError) Error() string { + return e.InnerError.Error() +} + +var ( + initialBackoffDelay = time.Second + maxBackoffDelay = 3 * time.Second +) + +// RetryBackoff retries function f until nil or a FatalError is returned. +// RetryBackoff only returns an error if the context is in error or if a +// FatalError was encountered. +func RetryBackoff(ctx context.Context, f func() error) error { + // doneCh signals that the routine is done and sends the last error + var doneCh = make(chan struct{}) + var errVal atomic.Value + type errWrap struct { + E error + } + + go func() { + // the retry delay between each attempt + var delay time.Duration = 0 + defer close(doneCh) + + for { + select { + case <-ctx.Done(): + return + case <-time.After(delay): + } + + err := f() + switch e := err.(type) { + case nil: + return + case Fatal: + errVal.Store(errWrap{e.FatalError()}) + return + } + + delay *= 2 + if delay == 0 { + delay = initialBackoffDelay + } + + delay = min(delay, maxBackoffDelay) + + log.Printf("[WARN] retryable error: %q, delaying for %s", err, delay) + } + }() + + // Wait until done or deadline + select { + case <-doneCh: + case <-ctx.Done(): + } + + err, hadErr := errVal.Load().(errWrap) + var lastErr error + if hadErr { + lastErr = err.E + } + + if ctx.Err() != nil { + return ctx.Err() + } + + return lastErr +} diff --git a/internal/backend/remote/testing.go b/internal/backend/remote/testing.go index 7dbb9e9b2c..6ca7ee1c7c 100644 --- a/internal/backend/remote/testing.go +++ b/internal/backend/remote/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( @@ -10,11 +13,16 @@ import ( "testing" "time" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/auth" "github.com/hashicorp/terraform-svchost/disco" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + backendLocal "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -24,10 +32,6 @@ import ( "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" - "github.com/mitchellh/cli" - "github.com/zclconf/go-cty/cty" - - backendLocal "github.com/hashicorp/terraform/internal/backend/local" ) const ( @@ -144,6 +148,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { b.client.Plans = mc.Plans b.client.PolicyChecks = mc.PolicyChecks b.client.Runs = mc.Runs + b.client.RunEvents = mc.RunEvents b.client.StateVersions = mc.StateVersions b.client.Variables = mc.Variables b.client.Workspaces = mc.Workspaces @@ -174,15 +179,17 @@ func testBackend(t *testing.T, obj cty.Value) (*Remote, func()) { return b, s.Close } -func testLocalBackend(t *testing.T, remote *Remote) backend.Enhanced { +func testLocalBackend(t *testing.T, remote *Remote) backendrun.OperationsBackend { b := backendLocal.NewWithBackend(remote) // Add a test provider to the local backend. - p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + p := backendLocal.TestLocalProvider(t, b, "null", providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "null_resource": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + }, }, }, }, @@ -308,9 +315,9 @@ func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingM }, tfdiags.Diagnostics{} } -// testVariable returns a backend.UnparsedVariableValue used for testing. -func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue { - vars := make(map[string]backend.UnparsedVariableValue, len(vs)) +// testVariable returns a backendrun.UnparsedVariableValue used for testing. +func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backendrun.UnparsedVariableValue { + vars := make(map[string]backendrun.UnparsedVariableValue, len(vs)) for _, v := range vs { vars[v] = &unparsedVariableValue{ value: v, diff --git a/internal/backend/testing.go b/internal/backend/testing.go index 844f95668d..843adad64a 100644 --- a/internal/backend/testing.go +++ b/internal/backend/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package backend import ( @@ -10,7 +13,7 @@ import ( "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configtesting" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -65,7 +68,7 @@ func TestBackendConfig(t *testing.T, b Backend, c hcl.Body) Backend { // this function will panic. func TestWrapConfig(raw map[string]interface{}) hcl.Body { obj := hcl2shim.HCL2ValueFromConfigValue(raw) - return configs.SynthBody("", obj.AsValueMap()) + return configtesting.SynthBody("", obj.AsValueMap()) } // TestBackend will test the functionality of a Backend. The backend is @@ -132,7 +135,7 @@ func TestBackendStates(t *testing.T, b Backend) { if err := foo.WriteState(fooState); err != nil { t.Fatal("error writing foo state:", err) } - if err := foo.PersistState(); err != nil { + if err := foo.PersistState(nil); err != nil { t.Fatal("error persisting foo state:", err) } @@ -160,7 +163,7 @@ func TestBackendStates(t *testing.T, b Backend) { if err := bar.WriteState(barState); err != nil { t.Fatalf("bad: %s", err) } - if err := bar.PersistState(); err != nil { + if err := bar.PersistState(nil); err != nil { t.Fatalf("bad: %s", err) } @@ -219,12 +222,12 @@ func TestBackendStates(t *testing.T, b Backend) { } // Delete some workspaces - if err := b.DeleteWorkspace("foo"); err != nil { + if err := b.DeleteWorkspace("foo", true); err != nil { t.Fatalf("err: %s", err) } // Verify the default state can't be deleted - if err := b.DeleteWorkspace(DefaultStateName); err == nil { + if err := b.DeleteWorkspace(DefaultStateName, true); err == nil { t.Fatal("expected error") } @@ -242,7 +245,7 @@ func TestBackendStates(t *testing.T, b Backend) { t.Fatalf("should be empty: %s", v) } // and delete it again - if err := b.DeleteWorkspace("foo"); err != nil { + if err := b.DeleteWorkspace("foo", true); err != nil { t.Fatalf("err: %s", err) } diff --git a/internal/builtin/providers/terraform/data_source_state.go b/internal/builtin/providers/terraform/data_source_state.go index f69a835343..37e176e6b0 100644 --- a/internal/builtin/providers/terraform/data_source_state.go +++ b/internal/builtin/providers/terraform/data_source_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -16,7 +19,7 @@ import ( func dataSourceRemoteStateGetSchema() providers.Schema { return providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "backend": { Type: cty.String, @@ -166,11 +169,8 @@ func dataSourceRemoteStateRead(d cty.Value) (cty.Value, tfdiags.Diagnostics) { newState["outputs"] = cty.EmptyObjectVal return cty.ObjectVal(newState), diags } - mod := remoteState.RootModule() - if mod != nil { // should always have a root module in any valid state - for k, os := range mod.OutputValues { - outputs[k] = os.Value - } + for k, os := range remoteState.RootOutputValues { + outputs[k] = os.Value } newState["outputs"] = cty.ObjectVal(outputs) @@ -239,7 +239,7 @@ func getBackend(cfg cty.Value) (backend.Backend, cty.Value, tfdiags.Diagnostics) return nil, cty.NilVal, diags } - // If this is the enhanced remote backend, we want to disable the version + // If this is the remote OperationsBackend, we want to disable the version // check, because this is a read-only operation if rb, ok := b.(*remote.Remote); ok { rb.IgnoreVersionConflict() diff --git a/internal/builtin/providers/terraform/data_source_state_test.go b/internal/builtin/providers/terraform/data_source_state_test.go index 1a9f514ecb..6a52c5a739 100644 --- a/internal/builtin/providers/terraform/data_source_state_test.go +++ b/internal/builtin/providers/terraform/data_source_state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,16 +8,18 @@ import ( "log" "testing" - "github.com/apparentlymart/go-dump/dump" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) func TestResource(t *testing.T) { - if err := dataSourceRemoteStateGetSchema().Block.InternalValidate(); err != nil { + if err := dataSourceRemoteStateGetSchema().Body.InternalValidate(); err != nil { t.Fatalf("err: %s", err) } } @@ -273,13 +278,19 @@ func TestState_basic(t *testing.T) { "backend": cty.StringVal("local"), "config": cty.NullVal(cty.DynamicPseudoType), }), - cty.NilVal, + cty.ObjectVal(map[string]cty.Value{ + "backend": cty.StringVal("local"), + "config": cty.NullVal(cty.DynamicPseudoType), + "defaults": cty.NullVal(cty.DynamicPseudoType), + "outputs": cty.EmptyObjectVal, + "workspace": cty.NullVal(cty.String), + }), true, }, } for name, test := range tests { t.Run(name, func(t *testing.T) { - schema := dataSourceRemoteStateGetSchema().Block + schema := dataSourceRemoteStateGetSchema().Body config, err := schema.CoerceValue(test.Config) if err != nil { t.Fatalf("unexpected error: %s", err) @@ -302,8 +313,8 @@ func TestState_basic(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - if test.Want != cty.NilVal && !test.Want.RawEquals(got) { - t.Errorf("wrong result\nconfig: %sgot: %swant: %s", dump.Value(config), dump.Value(got), dump.Value(test.Want)) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) } }) } @@ -324,7 +335,7 @@ func TestState_validation(t *testing.T) { overrideBackendFactories = nil }() - schema := dataSourceRemoteStateGetSchema().Block + schema := dataSourceRemoteStateGetSchema().Body config, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ "backend": cty.StringVal("failsconfigure"), "config": cty.EmptyObjectVal, @@ -362,7 +373,7 @@ func (b backendFailsConfigure) StateMgr(workspace string) (statemgr.Full, error) return nil, fmt.Errorf("StateMgr not implemented") } -func (b backendFailsConfigure) DeleteWorkspace(name string) error { +func (b backendFailsConfigure) DeleteWorkspace(name string, _ bool) error { return fmt.Errorf("DeleteWorkspace not implemented") } diff --git a/internal/builtin/providers/terraform/functions.go b/internal/builtin/providers/terraform/functions.go new file mode 100644 index 0000000000..49d76b5ead --- /dev/null +++ b/internal/builtin/providers/terraform/functions.go @@ -0,0 +1,177 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +var functions = map[string]func([]cty.Value) (cty.Value, error){ + "encode_tfvars": encodeTfvarsFunc, + "decode_tfvars": decodeTfvarsFunc, + "encode_expr": encodeExprFunc, +} + +func encodeTfvarsFunc(args []cty.Value) (cty.Value, error) { + // These error checks should not be hit in practice because the language + // runtime should check them before calling, so this is just for robustness + // and completeness. + if len(args) > 1 { + return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected") + } + if len(args) == 0 { + return cty.NilVal, fmt.Errorf("exactly one argument is required") + } + + v := args[0] + ty := v.Type() + + if v.IsNull() { + // Our functions schema does not say we allow null values, so we should + // not get to this error message if the caller respects the schema. + return cty.NilVal, function.NewArgErrorf(1, "cannot encode a null value in tfvars syntax") + } + if !v.IsWhollyKnown() { + return cty.UnknownVal(cty.String).RefineNotNull(), nil + } + + var keys []string + switch { + case ty.IsObjectType(): + atys := ty.AttributeTypes() + keys = make([]string, 0, len(atys)) + for key := range atys { + keys = append(keys, key) + } + case ty.IsMapType(): + keys = make([]string, 0, v.LengthInt()) + for it := v.ElementIterator(); it.Next(); { + k, _ := it.Element() + keys = append(keys, k.AsString()) + } + default: + return cty.NilVal, function.NewArgErrorf(1, "invalid value to encode: must be an object whose attribute names will become the encoded variable names") + } + sort.Strings(keys) + + f := hclwrite.NewEmptyFile() + body := f.Body() + for _, key := range keys { + if !hclsyntax.ValidIdentifier(key) { + // We can only encode valid identifiers as tfvars keys, since + // the HCL argument grammar requires them to be identifiers. + return cty.NilVal, function.NewArgErrorf(1, "invalid variable name %q: must be a valid identifier, per Terraform's rules for input variable declarations", key) + } + + // This index should not fail because we know that "key" is a valid + // index from the logic above. + v, _ := hcl.Index(v, cty.StringVal(key), nil) + body.SetAttributeValue(key, v) + } + + result := f.Bytes() + return cty.StringVal(string(result)), nil +} + +func decodeTfvarsFunc(args []cty.Value) (cty.Value, error) { + // These error checks should not be hit in practice because the language + // runtime should check them before calling, so this is just for robustness + // and completeness. + if len(args) > 1 { + return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected") + } + if len(args) == 0 { + return cty.NilVal, fmt.Errorf("exactly one argument is required") + } + if args[0].Type() != cty.String { + return cty.NilVal, fmt.Errorf("argument must be a string") + } + if args[0].IsNull() { + return cty.NilVal, fmt.Errorf("cannot decode tfvars from a null value") + } + if !args[0].IsKnown() { + // If our input isn't known then we can't even predict the result + // type, since it will be an object type decided based on which + // arguments and values we find in the string. + return cty.DynamicVal, nil + } + + // If we get here then we know that: + // - there's exactly one element in args + // - it's a string + // - it is known and non-null + // So therefore the following is guaranteed to succeed. + src := []byte(args[0].AsString()) + + // As usual when we wrap HCL stuff up in functions, we end up needing to + // stuff HCL diagnostics into plain string error messages. This produces + // a non-ideal result but is still better than hiding the HCL-provided + // diagnosis altogether. + f, hclDiags := hclsyntax.ParseConfig(src, "", hcl.InitialPos) + if hclDiags.HasErrors() { + return cty.NilVal, fmt.Errorf("invalid tfvars syntax: %s", hclDiags.Error()) + } + attrs, hclDiags := f.Body.JustAttributes() + if hclDiags.HasErrors() { + return cty.NilVal, fmt.Errorf("invalid tfvars content: %s", hclDiags.Error()) + } + retAttrs := make(map[string]cty.Value, len(attrs)) + for name, attr := range attrs { + // Evaluating the expression with no EvalContext achieves the same + // interpretation as Terraform CLI makes of .tfvars files, rejecting + // any function calls or references to symbols. + v, hclDiags := attr.Expr.Value(nil) + if hclDiags.HasErrors() { + return cty.NilVal, fmt.Errorf("invalid expression for variable %q: %s", name, hclDiags.Error()) + } + retAttrs[name] = v + } + + return cty.ObjectVal(retAttrs), nil +} + +func encodeExprFunc(args []cty.Value) (cty.Value, error) { + // These error checks should not be hit in practice because the language + // runtime should check them before calling, so this is just for robustness + // and completeness. + if len(args) > 1 { + return cty.NilVal, function.NewArgErrorf(1, "too many arguments; only one expected") + } + if len(args) == 0 { + return cty.NilVal, fmt.Errorf("exactly one argument is required") + } + + v := args[0] + if !v.IsWhollyKnown() { + ret := cty.UnknownVal(cty.String).RefineNotNull() + // For some types we can refine further due to the HCL grammar, + // as long as w eknow the value isn't null. + if !v.Range().CouldBeNull() { + ty := v.Type() + switch { + case ty.IsObjectType() || ty.IsMapType(): + ret = ret.Refine().StringPrefixFull("{").NewValue() + case ty.IsTupleType() || ty.IsListType() || ty.IsSetType(): + ret = ret.Refine().StringPrefixFull("[").NewValue() + case ty == cty.String: + ret = ret.Refine().StringPrefixFull(`"`).NewValue() + } + } + return ret, nil + } + + // This bytes.TrimSpace is to ensure that future changes to HCL, that + // might for some reason add extra spaces before the expression (!) + // can't invalidate our unknown value prefix refinements above. + src := bytes.TrimSpace(hclwrite.TokensForValue(v).Bytes()) + return cty.StringVal(string(src)), nil +} diff --git a/internal/builtin/providers/terraform/functions_test.go b/internal/builtin/providers/terraform/functions_test.go new file mode 100644 index 0000000000..f249c078ad --- /dev/null +++ b/internal/builtin/providers/terraform/functions_test.go @@ -0,0 +1,345 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/providers" +) + +func TestTfvarsencode(t *testing.T) { + tableTestFunction(t, "encode_tfvars", []functionTest{ + { + Input: cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "number": cty.NumberIntVal(5), + "bool": cty.True, + "set": cty.SetVal([]cty.Value{cty.StringVal("beep"), cty.StringVal("boop")}), + "list": cty.SetVal([]cty.Value{cty.StringVal("bleep"), cty.StringVal("bloop")}), + "tuple": cty.SetVal([]cty.Value{cty.StringVal("bibble"), cty.StringVal("wibble")}), + "map": cty.MapVal(map[string]cty.Value{"one": cty.NumberIntVal(1)}), + "object": cty.ObjectVal(map[string]cty.Value{"one": cty.NumberIntVal(1), "true": cty.True}), + "null": cty.NullVal(cty.String), + }), + Want: cty.StringVal( + `bool = true +list = ["bleep", "bloop"] +map = { + one = 1 +} +null = null +number = 5 +object = { + one = 1 + true = true +} +set = ["beep", "boop"] +string = "hello" +tuple = ["bibble", "wibble"] +`), + }, + { + Input: cty.EmptyObjectVal, + Want: cty.StringVal(``), + }, + { + Input: cty.MapVal(map[string]cty.Value{ + "one": cty.NumberIntVal(1), + "two": cty.NumberIntVal(2), + "three": cty.NumberIntVal(3), + }), + Want: cty.StringVal( + `one = 1 +three = 3 +two = 2 +`), + }, + { + Input: cty.MapValEmpty(cty.String), + Want: cty.StringVal(``), + }, + { + Input: cty.UnknownVal(cty.EmptyObject), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.UnknownVal(cty.Map(cty.String)), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.ObjectVal(map[string]cty.Value{ + "string": cty.UnknownVal(cty.String), + }), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.MapVal(map[string]cty.Value{ + "string": cty.UnknownVal(cty.String), + }), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.NullVal(cty.EmptyObject), + WantErr: `cannot encode a null value in tfvars syntax`, + }, + { + Input: cty.NullVal(cty.Map(cty.String)), + WantErr: `cannot encode a null value in tfvars syntax`, + }, + { + Input: cty.StringVal("nope"), + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.Zero, + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.False, + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.ListValEmpty(cty.String), + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.SetValEmpty(cty.String), + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.EmptyTupleVal, + WantErr: `invalid value to encode: must be an object whose attribute names will become the encoded variable names`, + }, + { + Input: cty.ObjectVal(map[string]cty.Value{ + "not valid identifier": cty.StringVal("!"), + }), + WantErr: `invalid variable name "not valid identifier": must be a valid identifier, per Terraform's rules for input variable declarations`, + }, + }) +} + +func TestTfvarsdecode(t *testing.T) { + tableTestFunction(t, "decode_tfvars", []functionTest{ + { + Input: cty.StringVal(`string = "hello" +number = 2`), + Want: cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "number": cty.NumberIntVal(2), + }), + }, + { + Input: cty.StringVal(``), + Want: cty.EmptyObjectVal, + }, + { + Input: cty.UnknownVal(cty.String), + Want: cty.UnknownVal(cty.DynamicPseudoType), + }, + { + Input: cty.NullVal(cty.String), + WantErr: `cannot decode tfvars from a null value`, + }, + { + Input: cty.StringVal(`not valid syntax`), + // This is actually not a very good diagnosis for this error, + // since we're expecting HCL arguments rather than HCL blocks, + // but that's something we'd need to address in HCL. + WantErr: `invalid tfvars syntax: :1,17-17: Invalid block definition; Either a quoted string block label or an opening brace ("{") is expected here.`, + }, + { + Input: cty.StringVal(`foo = not valid syntax`), + WantErr: `invalid tfvars syntax: :1,11-16: Missing newline after argument; An argument definition must end with a newline.`, + }, + { + Input: cty.StringVal(`foo = var.whatever`), + WantErr: `invalid expression for variable "foo": :1,7-10: Variables not allowed; Variables may not be used here.`, + }, + { + Input: cty.StringVal(`foo = whatever()`), + WantErr: `invalid expression for variable "foo": :1,7-17: Function calls not allowed; Functions may not be called here.`, + }, + }) +} + +func TestExprencode(t *testing.T) { + tableTestFunction(t, "encode_expr", []functionTest{ + { + Input: cty.StringVal("hello"), + Want: cty.StringVal(`"hello"`), + }, + { + Input: cty.StringVal("hello\nworld\n"), + Want: cty.StringVal(`"hello\nworld\n"`), + // NOTE: If HCL changes the above to be a heredoc in future (which + // would make this test fail) then our function's refinement + // that unknown strings encode with the prefix " will become + // invalid, and should be removed. + }, + { + Input: cty.StringVal("hel${lo"), + Want: cty.StringVal(`"hel$${lo"`), // Escape template interpolation sequence + }, + { + Input: cty.StringVal("hel%{lo"), + Want: cty.StringVal(`"hel%%{lo"`), // Escape template control sequence + }, + { + Input: cty.StringVal(`boop\boop`), + Want: cty.StringVal(`"boop\\boop"`), // Escape literal backslash + }, + { + Input: cty.StringVal(""), + Want: cty.StringVal(`""`), + }, + { + Input: cty.NumberIntVal(2), + Want: cty.StringVal(`2`), + }, + { + Input: cty.True, + Want: cty.StringVal(`true`), + }, + { + Input: cty.False, + Want: cty.StringVal(`false`), + }, + { + Input: cty.EmptyObjectVal, + Want: cty.StringVal(`{}`), + }, + { + Input: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(5), + "string": cty.StringVal("..."), + }), + Want: cty.StringVal(`{ + number = 5 + string = "..." +}`), + }, + { + Input: cty.MapVal(map[string]cty.Value{ + "one": cty.NumberIntVal(1), + "two": cty.NumberIntVal(2), + }), + Want: cty.StringVal(`{ + one = 1 + two = 2 +}`), + }, + { + Input: cty.EmptyTupleVal, + Want: cty.StringVal(`[]`), + }, + { + Input: cty.TupleVal([]cty.Value{ + cty.NumberIntVal(5), + cty.StringVal("..."), + }), + Want: cty.StringVal(`[5, "..."]`), + }, + { + Input: cty.SetVal([]cty.Value{ + cty.NumberIntVal(1), + cty.NumberIntVal(5), + cty.NumberIntVal(20), + cty.NumberIntVal(55), + }), + Want: cty.StringVal(`[1, 5, 20, 55]`), + }, + { + Input: cty.DynamicVal, + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.UnknownVal(cty.Number).RefineNotNull(), + Want: cty.UnknownVal(cty.String).RefineNotNull(), + }, + { + Input: cty.UnknownVal(cty.String).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`"`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.EmptyObject).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`{`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.Map(cty.String)).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`{`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.EmptyTuple).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`[`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.List(cty.String)).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`[`). + NewValue(), + }, + { + Input: cty.UnknownVal(cty.Set(cty.String)).RefineNotNull(), + Want: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefixFull(`[`). + NewValue(), + }, + }) +} + +type functionTest struct { + Input cty.Value + Want cty.Value + WantErr string +} + +func tableTestFunction(t *testing.T, functionName string, tests []functionTest) { + t.Helper() + + provider := NewProvider() + for _, test := range tests { + t.Run(test.Input.GoString(), func(t *testing.T) { + resp := provider.CallFunction(providers.CallFunctionRequest{ + FunctionName: functionName, + Arguments: []cty.Value{test.Input}, + }) + if test.WantErr != "" { + err := resp.Err + if err == nil { + t.Fatalf("unexpected success for %#v; want error\ngot: %#v", test.Input, resp.Result) + } + if err.Error() != test.WantErr { + t.Errorf("wrong error\ngot: %s\nwant: %s", err.Error(), test.WantErr) + } + return + } + if resp.Err != nil { + t.Fatalf("unexpected error: %s", resp.Err) + } + if diff := cmp.Diff(test.Want, resp.Result, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result for %#v\n%s", test.Input, diff) + } + }) + } +} diff --git a/internal/builtin/providers/terraform/provider.go b/internal/builtin/providers/terraform/provider.go index 10292f4abb..9180bd9b4b 100644 --- a/internal/builtin/providers/terraform/provider.go +++ b/internal/builtin/providers/terraform/provider.go @@ -1,20 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" + "github.com/zclconf/go-cty/cty" + + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/hashicorp/terraform/internal/providers" ) // Provider is an implementation of providers.Interface -type Provider struct { - // Provider is the schema for the provider itself. - Schema providers.Schema - - // DataSources maps the data source name to that data source's schema. - DataSources map[string]providers.Schema -} +type Provider struct{} // NewProvider returns a new terraform provider func NewProvider() providers.Interface { @@ -23,10 +23,65 @@ func NewProvider() providers.Interface { // GetSchema returns the complete schema for the provider. func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse { - return providers.GetProviderSchemaResponse{ + resp := providers.GetProviderSchemaResponse{ + Provider: providers.Schema{}, + ServerCapabilities: providers.ServerCapabilities{ + MoveResourceState: true, + }, DataSources: map[string]providers.Schema{ "terraform_remote_state": dataSourceRemoteStateGetSchema(), }, + ResourceTypes: map[string]providers.Schema{ + "terraform_data": dataStoreResourceSchema(), + }, + EphemeralResourceTypes: map[string]providers.Schema{}, + Functions: map[string]providers.FunctionDecl{ + "encode_tfvars": { + Summary: "Produce a string representation of an object using the same syntax as for `.tfvars` files", + Description: "A rarely-needed function which takes an object value and produces a string containing a description of that object using the same syntax as Terraform CLI would expect in a `.tfvars`.", + Parameters: []providers.FunctionParam{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknownValues: true, // to perform refinements + }, + }, + ReturnType: cty.String, + }, + "decode_tfvars": { + Summary: "Parse a string containing syntax like that used in a `.tfvars` file", + Description: "A rarely-needed function which takes a string containing the content of a `.tfvars` file and returns an object describing the raw variable values it defines.", + Parameters: []providers.FunctionParam{ + { + Name: "src", + Type: cty.String, + }, + }, + ReturnType: cty.DynamicPseudoType, + }, + "encode_expr": { + Summary: "Produce a string representation of an arbitrary value using Terraform expression syntax", + Description: "A rarely-needed function which takes any value and produces a string containing Terraform language expression syntax approximating that value.", + Parameters: []providers.FunctionParam{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknownValues: true, // to perform refinements + }, + }, + ReturnType: cty.String, + }, + }, + } + providers.SchemaCache.Set(tfaddr.NewProvider(tfaddr.BuiltInProviderHost, tfaddr.BuiltInProviderNamespace, "terraform"), resp) + return resp +} + +func (p *Provider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{ + "terraform_data": dataStoreResourceIdentitySchema(), + }, } } @@ -48,7 +103,7 @@ func (p *Provider) ValidateDataResourceConfig(req providers.ValidateDataResource // This should not happen if req.TypeName != "terraform_remote_state" { - res.Diagnostics.Append(fmt.Errorf("Error: unsupported data source %s", req.TypeName)) + res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("Error: unsupported data source %s", req.TypeName)) return res } @@ -73,7 +128,7 @@ func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers // This should not happen if req.TypeName != "terraform_remote_state" { - res.Diagnostics.Append(fmt.Errorf("Error: unsupported data source %s", req.TypeName)) + res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("Error: unsupported data source %s", req.TypeName)) return res } @@ -99,39 +154,114 @@ func (p *Provider) Stop() error { // instance state whose schema version is less than the one reported by the // currently-used version of the corresponding provider, and the upgraded // result is used for any further processing. -func (p *Provider) UpgradeResourceState(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { - panic("unimplemented - terraform_remote_state has no resources") +func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + return upgradeDataStoreResourceState(req) +} + +func (p *Provider) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + return upgradeDataStoreResourceIdentity(req) } // ReadResource refreshes a resource and returns its current state. -func (p *Provider) ReadResource(providers.ReadResourceRequest) providers.ReadResourceResponse { - panic("unimplemented - terraform_remote_state has no resources") +func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse { + return readDataStoreResourceState(req) } // PlanResourceChange takes the current state and proposed state of a // resource, and returns the planned final state. -func (p *Provider) PlanResourceChange(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { - panic("unimplemented - terraform_remote_state has no resources") +func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return planDataStoreResourceChange(req) } // ApplyResourceChange takes the planned state for a resource, which may // yet contain unknown computed values, and applies the changes returning // the final state. -func (p *Provider) ApplyResourceChange(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { - panic("unimplemented - terraform_remote_state has no resources") +func (p *Provider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return applyDataStoreResourceChange(req) } // ImportResourceState requests that the given resource be imported. -func (p *Provider) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { - panic("unimplemented - terraform_remote_state has no resources") +func (p *Provider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + if req.TypeName == "terraform_data" { + return importDataStore(req) + } + + panic("unimplemented: cannot import resource type " + req.TypeName) +} + +// MoveResourceState requests that the given resource be moved. +func (p *Provider) MoveResourceState(req providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + switch req.TargetTypeName { + case "terraform_data": + return moveDataStoreResourceState(req) + default: + var resp providers.MoveResourceStateResponse + + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Error: unsupported resource %s", req.TargetTypeName)) + + return resp + } } // ValidateResourceConfig is used to to validate the resource configuration values. -func (p *Provider) ValidateResourceConfig(providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { - // At this moment there is nothing to configure for the terraform provider, - // so we will happily return without taking any action - var res providers.ValidateResourceConfigResponse - return res +func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { + return validateDataStoreResourceConfig(req) +} + +func (p *Provider) ValidateEphemeralResourceConfig(req providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse { + var resp providers.ValidateEphemeralResourceConfigResponse + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unsupported ephemeral resource type %q", req.TypeName)) + return resp +} + +// OpenEphemeralResource implements providers.Interface. +func (p *Provider) OpenEphemeralResource(req providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + var resp providers.OpenEphemeralResourceResponse + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unsupported ephemeral resource type %q", req.TypeName)) + return resp +} + +// RenewEphemeralResource implements providers.Interface. +func (p *Provider) RenewEphemeralResource(req providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse { + var resp providers.RenewEphemeralResourceResponse + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unsupported ephemeral resource type %q", req.TypeName)) + return resp +} + +// CloseEphemeralResource implements providers.Interface. +func (p *Provider) CloseEphemeralResource(req providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse { + var resp providers.CloseEphemeralResourceResponse + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unsupported ephemeral resource type %q", req.TypeName)) + return resp +} + +// CallFunction would call a function contributed by this provider, but this +// provider has no functions and so this function just panics. +func (p *Provider) CallFunction(req providers.CallFunctionRequest) providers.CallFunctionResponse { + fn, ok := functions[req.FunctionName] + if !ok { + // Should not get here if the caller is behaving correctly, because + // we don't declare any functions in our schema that we don't have + // implementations for. + return providers.CallFunctionResponse{ + Err: fmt.Errorf("provider has no function named %q", req.FunctionName), + } + } + + // NOTE: We assume that none of the arguments can be marked, because we're + // expecting to be called from logic in Terraform Core that strips marks + // before calling a provider-contributed function, and then reapplies them + // afterwards. + + result, err := fn(req.Arguments) + if err != nil { + return providers.CallFunctionResponse{ + Err: err, + } + } + return providers.CallFunctionResponse{ + Result: result, + } } // Close is a noop for this provider, since it's run in-process. diff --git a/internal/builtin/providers/terraform/provider_test.go b/internal/builtin/providers/terraform/provider_test.go index 5f06e9c342..555e796b28 100644 --- a/internal/builtin/providers/terraform/provider_test.go +++ b/internal/builtin/providers/terraform/provider_test.go @@ -1,10 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "testing" + backendInit "github.com/hashicorp/terraform/internal/backend/init" + "github.com/hashicorp/terraform/internal/providers" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" ) func init() { // Initialize the backends backendInit.Init(nil) } + +func TestMoveResourceState_DataStore(t *testing.T) { + t.Parallel() + + nullResourceStateValue := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + }) + nullResourceStateJSON, err := ctyjson.Marshal(nullResourceStateValue, nullResourceStateValue.Type()) + + if err != nil { + t.Fatalf("failed to marshal null resource state: %s", err) + } + + provider := &Provider{} + req := providers.MoveResourceStateRequest{ + SourceProviderAddress: "registry.terraform.io/hashicorp/null", + SourceStateJSON: nullResourceStateJSON, + SourceTypeName: "null_resource", + TargetTypeName: "terraform_data", + } + resp := provider.MoveResourceState(req) + + if resp.Diagnostics.HasErrors() { + t.Errorf("unexpected diagnostics: %s", resp.Diagnostics.Err()) + } + + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + }) + + if !resp.TargetState.RawEquals(expectedTargetState) { + t.Errorf("expected state was:\n%#v\ngot state is:\n%#v\n", expectedTargetState, resp.TargetState) + } +} + +func TestMoveResourceState_NonExistentResource(t *testing.T) { + t.Parallel() + + provider := &Provider{} + req := providers.MoveResourceStateRequest{ + TargetTypeName: "nonexistent_resource", + } + resp := provider.MoveResourceState(req) + + if !resp.Diagnostics.HasErrors() { + t.Fatal("expected diagnostics") + } +} diff --git a/internal/builtin/providers/terraform/resource_data.go b/internal/builtin/providers/terraform/resource_data.go new file mode 100644 index 0000000000..20d3122549 --- /dev/null +++ b/internal/builtin/providers/terraform/resource_data.go @@ -0,0 +1,276 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" +) + +func dataStoreResourceSchema() providers.Schema { + return providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.DynamicPseudoType, Optional: true}, + "output": {Type: cty.DynamicPseudoType, Computed: true}, + "triggers_replace": {Type: cty.DynamicPseudoType, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, + }, + Identity: dataStoreResourceIdentitySchema().Body, + } +} + +func dataStoreResourceIdentitySchema() providers.IdentitySchema { + return providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "The unique identifier for the data store.", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + } +} + +func validateDataStoreResourceConfig(req providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) { + if req.Config.IsNull() { + return resp + } + + // Core does not currently validate computed values are not set in the + // configuration. + for _, attr := range []string{"id", "output"} { + if !req.Config.GetAttr(attr).IsNull() { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf(`%q attribute is read-only`, attr)) + } + } + return resp +} + +func upgradeDataStoreResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { + ty := dataStoreResourceSchema().Body.ImpliedType() + val, err := ctyjson.Unmarshal(req.RawStateJSON, ty) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + resp.UpgradedState = val + return resp +} + +func upgradeDataStoreResourceIdentity(providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("The builtin provider does not support provider upgrades since it has not changed the identity schema yet.")) + return resp +} + +func readDataStoreResourceState(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + resp.NewState = req.PriorState + return resp +} + +func planDataStoreResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + if req.ProposedNewState.IsNull() { + // destroy op + resp.PlannedState = req.ProposedNewState + return resp + } + + planned := req.ProposedNewState.AsValueMap() + + input := req.ProposedNewState.GetAttr("input") + trigger := req.ProposedNewState.GetAttr("triggers_replace") + + switch { + case req.PriorState.IsNull(): + // Create + // Set the id value to unknown. + planned["id"] = cty.UnknownVal(cty.String).RefineNotNull() + + // Output type must always match the input, even when it's null. + if input.IsNull() { + planned["output"] = input + } else { + planned["output"] = cty.UnknownVal(input.Type()) + } + + resp.PlannedState = cty.ObjectVal(planned) + return resp + + case !req.PriorState.GetAttr("triggers_replace").RawEquals(trigger): + // trigger changed, so we need to replace the entire instance + resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("triggers_replace")) + planned["id"] = cty.UnknownVal(cty.String).RefineNotNull() + + // We need to check the input for the replacement instance to compute a + // new output. + if input.IsNull() { + planned["output"] = input + } else { + planned["output"] = cty.UnknownVal(input.Type()) + } + + case !req.PriorState.GetAttr("input").RawEquals(input): + // only input changed, so we only need to re-compute output + planned["output"] = cty.UnknownVal(input.Type()) + } + + resp.PlannedState = cty.ObjectVal(planned) + return resp +} + +var testUUIDHook func() string + +func applyDataStoreResourceChange(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + if req.PlannedState.IsNull() { + resp.NewState = req.PlannedState + return resp + } + + newState := req.PlannedState.AsValueMap() + + if !req.PlannedState.GetAttr("output").IsKnown() { + newState["output"] = req.PlannedState.GetAttr("input") + } + + if !req.PlannedState.GetAttr("id").IsKnown() { + idString, err := uuid.GenerateUUID() + // Terraform would probably never get this far without a good random + // source, but catch the error anyway. + if err != nil { + diag := tfdiags.AttributeValue( + tfdiags.Error, + "Error generating id", + err.Error(), + cty.GetAttrPath("id"), + ) + + resp.Diagnostics = resp.Diagnostics.Append(diag) + } + + if testUUIDHook != nil { + idString = testUUIDHook() + } + + newState["id"] = cty.StringVal(idString) + } + + resp.NewState = cty.ObjectVal(newState) + + return resp +} + +// TODO: This isn't very useful even for examples, because terraform_data has +// no way to refresh the full resource value from only the import ID. This +// minimal implementation allows the import to succeed, and can be extended +// once the configuration is available during import. +func importDataStore(req providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) { + schema := dataStoreResourceSchema() + v := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal(req.ID), + }) + state, err := schema.Body.CoerceValue(v) + resp.Diagnostics = resp.Diagnostics.Append(err) + + resp.ImportedResources = []providers.ImportedResource{ + { + TypeName: req.TypeName, + State: state, + }, + } + return resp +} + +// moveDataStoreResourceState enables moving from the official null_resource +// managed resource to the terraform_data managed resource. +func moveDataStoreResourceState(req providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { + // Verify that the source provider is an official hashicorp/null provider, + // but ignore the hostname for mirrors. + if !strings.HasSuffix(req.SourceProviderAddress, "hashicorp/null") { + diag := tfdiags.Sourceless( + tfdiags.Error, + "Unsupported source provider for move operation", + "Only moving from the official hashicorp/null provider to terraform_data is supported.", + ) + resp.Diagnostics = resp.Diagnostics.Append(diag) + + return resp + } + + // Verify that the source resource type name is null_resource. + if req.SourceTypeName != "null_resource" { + diag := tfdiags.Sourceless( + tfdiags.Error, + "Unsupported source resource type for move operation", + "Only moving from the null_resource managed resource to terraform_data is supported.", + ) + resp.Diagnostics = resp.Diagnostics.Append(diag) + + return resp + } + + nullResourceSchemaType := nullResourceSchema().Body.ImpliedType() + nullResourceValue, err := ctyjson.Unmarshal(req.SourceStateJSON, nullResourceSchemaType) + + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + + return resp + } + + triggersReplace := nullResourceValue.GetAttr("triggers") + + // PlanResourceChange uses RawEquals comparison, which will show a + // difference between cty.NullVal(cty.Map(cty.String)) and + // cty.NullVal(cty.DynamicPseudoType). + if triggersReplace.IsNull() { + triggersReplace = cty.NullVal(cty.DynamicPseudoType) + } else { + // PlanResourceChange uses RawEquals comparison, which will show a + // difference between cty.MapVal(...) and cty.ObjectVal(...). Given that + // triggers is typically configured using direct configuration syntax of + // {...}, which is a cty.ObjectVal, over a map typed variable or + // explicitly type converted map, this pragmatically chooses to convert + // the triggers value to cty.ObjectVal to prevent an immediate plan + // difference for the more typical case. + triggersReplace = cty.ObjectVal(triggersReplace.AsValueMap()) + } + + schema := dataStoreResourceSchema() + v := cty.ObjectVal(map[string]cty.Value{ + "id": nullResourceValue.GetAttr("id"), + "triggers_replace": triggersReplace, + }) + + state, err := schema.Body.CoerceValue(v) + + // null_resource did not use private state, so it is unnecessary to move. + resp.Diagnostics = resp.Diagnostics.Append(err) + resp.TargetState = state + + return resp +} + +func nullResourceSchema() providers.Schema { + return providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "triggers": {Type: cty.Map(cty.String), Optional: true}, + }, + }, + } +} diff --git a/internal/builtin/providers/terraform/resource_data_test.go b/internal/builtin/providers/terraform/resource_data_test.go new file mode 100644 index 0000000000..b88b7030cc --- /dev/null +++ b/internal/builtin/providers/terraform/resource_data_test.go @@ -0,0 +1,490 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "strings" + "testing" + + "github.com/hashicorp/terraform/internal/providers" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" +) + +func TestManagedDataValidate(t *testing.T) { + cfg := map[string]cty.Value{ + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.NullVal(cty.String), + } + + // empty + req := providers.ValidateResourceConfigRequest{ + TypeName: "terraform_data", + Config: cty.ObjectVal(cfg), + } + + resp := validateDataStoreResourceConfig(req) + if resp.Diagnostics.HasErrors() { + t.Error("empty config error:", resp.Diagnostics.ErrWithWarnings()) + } + + // invalid computed values + cfg["output"] = cty.StringVal("oops") + req.Config = cty.ObjectVal(cfg) + + resp = validateDataStoreResourceConfig(req) + if !resp.Diagnostics.HasErrors() { + t.Error("expected error") + } + + msg := resp.Diagnostics.Err().Error() + if !strings.Contains(msg, "attribute is read-only") { + t.Error("unexpected error", msg) + } +} + +func TestManagedDataUpgradeState(t *testing.T) { + schema := dataStoreResourceSchema() + ty := schema.Body.ImpliedType() + + state := cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.ListVal([]cty.Value{ + cty.StringVal("a"), cty.StringVal("b"), + }), + "id": cty.StringVal("not-quite-unique"), + }) + + jsState, err := ctyjson.Marshal(state, ty) + if err != nil { + t.Fatal(err) + } + + // empty + req := providers.UpgradeResourceStateRequest{ + TypeName: "terraform_data", + RawStateJSON: jsState, + } + + resp := upgradeDataStoreResourceState(req) + if resp.Diagnostics.HasErrors() { + t.Error("upgrade state error:", resp.Diagnostics.ErrWithWarnings()) + } + + if !resp.UpgradedState.RawEquals(state) { + t.Errorf("prior state was:\n%#v\nupgraded state is:\n%#v\n", state, resp.UpgradedState) + } +} + +func TestManagedDataRead(t *testing.T) { + req := providers.ReadResourceRequest{ + TypeName: "terraform_data", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.ListVal([]cty.Value{ + cty.StringVal("a"), cty.StringVal("b"), + }), + "id": cty.StringVal("not-quite-unique"), + }), + } + + resp := readDataStoreResourceState(req) + if resp.Diagnostics.HasErrors() { + t.Fatal("unexpected error", resp.Diagnostics.ErrWithWarnings()) + } + + if !resp.NewState.RawEquals(req.PriorState) { + t.Errorf("prior state was:\n%#v\nnew state is:\n%#v\n", req.PriorState, resp.NewState) + } +} + +func TestManagedDataPlan(t *testing.T) { + schema := dataStoreResourceSchema().Body + ty := schema.ImpliedType() + + for name, tc := range map[string]struct { + prior cty.Value + proposed cty.Value + planned cty.Value + }{ + "create": { + prior: cty.NullVal(ty), + proposed: cty.ObjectVal(map[string]cty.Value{ + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.NullVal(cty.String), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, + + "create-typed-null-input": { + prior: cty.NullVal(ty), + proposed: cty.ObjectVal(map[string]cty.Value{ + "input": cty.NullVal(cty.String), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.NullVal(cty.String), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.NullVal(cty.String), + "output": cty.NullVal(cty.String), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, + + "create-output": { + prior: cty.NullVal(ty), + proposed: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.NullVal(cty.String), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.UnknownVal(cty.String), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, + + "update-input": { + prior: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + proposed: cty.ObjectVal(map[string]cty.Value{ + "input": cty.UnknownVal(cty.List(cty.String)), + "output": cty.StringVal("input"), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.UnknownVal(cty.List(cty.String)), + "output": cty.UnknownVal(cty.List(cty.String)), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + }, + + "update-trigger": { + prior: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + proposed: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.StringVal("new-value"), + "id": cty.StringVal("not-quite-unique"), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.UnknownVal(cty.String), + "triggers_replace": cty.StringVal("new-value"), + "id": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, + + "update-input-trigger": { + prior: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("value"), + }), + "id": cty.StringVal("not-quite-unique"), + }), + proposed: cty.ObjectVal(map[string]cty.Value{ + "input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "output": cty.StringVal("input"), + "triggers_replace": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("new value"), + }), + "id": cty.StringVal("not-quite-unique"), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "output": cty.UnknownVal(cty.List(cty.String)), + "triggers_replace": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("new value"), + }), + "id": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }, + } { + t.Run("plan-"+name, func(t *testing.T) { + req := providers.PlanResourceChangeRequest{ + TypeName: "terraform_data", + PriorState: tc.prior, + ProposedNewState: tc.proposed, + } + + resp := planDataStoreResourceChange(req) + if resp.Diagnostics.HasErrors() { + t.Fatal(resp.Diagnostics.ErrWithWarnings()) + } + + if !resp.PlannedState.RawEquals(tc.planned) { + t.Errorf("expected:\n%#v\ngot:\n%#v\n", tc.planned, resp.PlannedState) + } + }) + } +} + +func TestManagedDataApply(t *testing.T) { + testUUIDHook = func() string { + return "not-quite-unique" + } + defer func() { + testUUIDHook = nil + }() + + schema := dataStoreResourceSchema().Body + ty := schema.ImpliedType() + + for name, tc := range map[string]struct { + prior cty.Value + planned cty.Value + state cty.Value + }{ + "create": { + prior: cty.NullVal(ty), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.UnknownVal(cty.String), + }), + state: cty.ObjectVal(map[string]cty.Value{ + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + }, + + "create-output": { + prior: cty.NullVal(ty), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.UnknownVal(cty.String), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.UnknownVal(cty.String), + }), + state: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + }, + + "update-input": { + prior: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "output": cty.UnknownVal(cty.List(cty.String)), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + state: cty.ObjectVal(map[string]cty.Value{ + "input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "output": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + }, + + "update-trigger": { + prior: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + "id": cty.StringVal("not-quite-unique"), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.UnknownVal(cty.String), + "triggers_replace": cty.StringVal("new-value"), + "id": cty.UnknownVal(cty.String), + }), + state: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.StringVal("new-value"), + "id": cty.StringVal("not-quite-unique"), + }), + }, + + "update-input-trigger": { + prior: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("input"), + "output": cty.StringVal("input"), + "triggers_replace": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("value"), + }), + "id": cty.StringVal("not-quite-unique"), + }), + planned: cty.ObjectVal(map[string]cty.Value{ + "input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "output": cty.UnknownVal(cty.List(cty.String)), + "triggers_replace": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("new value"), + }), + "id": cty.UnknownVal(cty.String), + }), + state: cty.ObjectVal(map[string]cty.Value{ + "input": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "output": cty.ListVal([]cty.Value{cty.StringVal("new-input")}), + "triggers_replace": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("new value"), + }), + "id": cty.StringVal("not-quite-unique"), + }), + }, + } { + t.Run("apply-"+name, func(t *testing.T) { + req := providers.ApplyResourceChangeRequest{ + TypeName: "terraform_data", + PriorState: tc.prior, + PlannedState: tc.planned, + } + + resp := applyDataStoreResourceChange(req) + if resp.Diagnostics.HasErrors() { + t.Fatal(resp.Diagnostics.ErrWithWarnings()) + } + + if !resp.NewState.RawEquals(tc.state) { + t.Errorf("expected:\n%#v\ngot:\n%#v\n", tc.state, resp.NewState) + } + }) + } +} + +func TestMoveDataStoreResourceState_Id(t *testing.T) { + t.Parallel() + + nullResourceStateValue := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + "triggers": cty.NullVal(cty.Map(cty.String)), + }) + nullResourceStateJSON, err := ctyjson.Marshal(nullResourceStateValue, nullResourceStateValue.Type()) + + if err != nil { + t.Fatalf("failed to marshal null resource state: %s", err) + } + + req := providers.MoveResourceStateRequest{ + SourceProviderAddress: "registry.terraform.io/hashicorp/null", + SourceStateJSON: nullResourceStateJSON, + SourceTypeName: "null_resource", + TargetTypeName: "terraform_data", + } + resp := moveDataStoreResourceState(req) + + if resp.Diagnostics.HasErrors() { + t.Errorf("unexpected diagnostics: %s", resp.Diagnostics.Err()) + } + + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.NullVal(cty.DynamicPseudoType), + }) + + if !resp.TargetState.RawEquals(expectedTargetState) { + t.Errorf("expected state was:\n%#v\ngot state is:\n%#v\n", expectedTargetState, resp.TargetState) + } +} + +func TestMoveResourceState_SourceProviderAddress(t *testing.T) { + t.Parallel() + + req := providers.MoveResourceStateRequest{ + SourceProviderAddress: "registry.terraform.io/examplecorp/null", + } + resp := moveDataStoreResourceState(req) + + if !resp.Diagnostics.HasErrors() { + t.Fatal("expected diagnostics") + } +} + +func TestMoveResourceState_SourceTypeName(t *testing.T) { + t.Parallel() + + req := providers.MoveResourceStateRequest{ + SourceProviderAddress: "registry.terraform.io/hashicorp/null", + SourceTypeName: "null_data_source", + } + resp := moveDataStoreResourceState(req) + + if !resp.Diagnostics.HasErrors() { + t.Fatal("expected diagnostics") + } +} + +func TestMoveDataStoreResourceState_Triggers(t *testing.T) { + t.Parallel() + + nullResourceStateValue := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + "triggers": cty.MapVal(map[string]cty.Value{ + "testkey": cty.StringVal("testvalue"), + }), + }) + nullResourceStateJSON, err := ctyjson.Marshal(nullResourceStateValue, nullResourceStateValue.Type()) + + if err != nil { + t.Fatalf("failed to marshal null resource state: %s", err) + } + + req := providers.MoveResourceStateRequest{ + SourceProviderAddress: "registry.terraform.io/hashicorp/null", + SourceStateJSON: nullResourceStateJSON, + SourceTypeName: "null_resource", + TargetTypeName: "terraform_data", + } + resp := moveDataStoreResourceState(req) + + if resp.Diagnostics.HasErrors() { + t.Errorf("unexpected diagnostics: %s", resp.Diagnostics.Err()) + } + + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + "input": cty.NullVal(cty.DynamicPseudoType), + "output": cty.NullVal(cty.DynamicPseudoType), + "triggers_replace": cty.ObjectVal(map[string]cty.Value{ + "testkey": cty.StringVal("testvalue"), + }), + }) + + if !resp.TargetState.RawEquals(expectedTargetState) { + t.Errorf("expected state was:\n%#v\ngot state is:\n%#v\n", expectedTargetState, resp.TargetState) + } +} diff --git a/internal/builtin/provisioners/file/resource_provisioner.go b/internal/builtin/provisioners/file/resource_provisioner.go index 54c2e4a2b1..535a25a033 100644 --- a/internal/builtin/provisioners/file/resource_provisioner.go +++ b/internal/builtin/provisioners/file/resource_provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package file import ( @@ -145,7 +148,7 @@ func getSrc(v cty.Value) (string, bool, error) { return expansion, false, err default: - panic("source and content cannot both be null") + return "", false, errors.New("source and content cannot both be null") } } diff --git a/internal/builtin/provisioners/file/resource_provisioner_test.go b/internal/builtin/provisioners/file/resource_provisioner_test.go index c470743b34..60e88e9a80 100644 --- a/internal/builtin/provisioners/file/resource_provisioner_test.go +++ b/internal/builtin/provisioners/file/resource_provisioner_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package file import ( @@ -81,6 +84,21 @@ func TestResourceProvider_Validate_bad_no_source(t *testing.T) { } } +func TestResourceProvider_Validate_bad_null_source(t *testing.T) { + v := cty.ObjectVal(map[string]cty.Value{ + "destination": cty.StringVal("/tmp/bar"), + "source": cty.NullVal(cty.String), + }) + + resp := New().ValidateProvisionerConfig(provisioners.ValidateProvisionerConfigRequest{ + Config: v, + }) + + if !resp.Diagnostics.HasErrors() { + t.Fatal("Should have errors") + } +} + func TestResourceProvider_Validate_bad_to_many_src(t *testing.T) { v := cty.ObjectVal(map[string]cty.Value{ "source": cty.StringVal("nope"), diff --git a/internal/builtin/provisioners/local-exec/resource_provisioner.go b/internal/builtin/provisioners/local-exec/resource_provisioner.go index 5a1dc04f23..03b74cb99e 100644 --- a/internal/builtin/provisioners/local-exec/resource_provisioner.go +++ b/internal/builtin/provisioners/local-exec/resource_provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package localexec import ( @@ -57,6 +60,10 @@ func (p *provisioner) GetSchema() (resp provisioners.GetSchemaResponse) { Type: cty.Map(cty.String), Optional: true, }, + "quiet": { + Type: cty.Bool, + Optional: true, + }, }, } @@ -163,7 +170,11 @@ func (p *provisioner) ProvisionResource(req provisioners.ProvisionResourceReques go copyUIOutput(req.UIOutput, tee, copyDoneCh) // Output what we're about to run - req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs)) + if quietVal := req.Config.GetAttr("quiet"); !quietVal.IsNull() && quietVal.True() { + req.UIOutput.Output("local-exec: Executing: Suppressed by quiet=true") + } else { + req.UIOutput.Output(fmt.Sprintf("Executing: %q", cmdargs)) + } // Start the command err = cmd.Start() diff --git a/internal/builtin/provisioners/local-exec/resource_provisioner_test.go b/internal/builtin/provisioners/local-exec/resource_provisioner_test.go index d1560d48ee..86d672481a 100644 --- a/internal/builtin/provisioners/local-exec/resource_provisioner_test.go +++ b/internal/builtin/provisioners/local-exec/resource_provisioner_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package localexec import ( @@ -8,8 +11,8 @@ import ( "testing" "time" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/provisioners" - "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" ) diff --git a/internal/builtin/provisioners/remote-exec/resource_provisioner.go b/internal/builtin/provisioners/remote-exec/resource_provisioner.go index f8edfb7855..35184f2e5a 100644 --- a/internal/builtin/provisioners/remote-exec/resource_provisioner.go +++ b/internal/builtin/provisioners/remote-exec/resource_provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remoteexec import ( @@ -238,9 +241,16 @@ func runScripts(ctx context.Context, o provisioners.UIOutput, comm communicator. return err } + // The provisioner node may hang around a bit longer before it's cleaned up + // in the graph, but we can disconnect the communicator after we run the + // commands. We do still want to drop the connection if we're canceled early + // for some reason, so build a new context from the original. + cmdCtx, cancel := context.WithCancel(ctx) + defer cancel() + // Wait for the context to end and then disconnect go func() { - <-ctx.Done() + <-cmdCtx.Done() comm.Disconnect() }() diff --git a/internal/builtin/provisioners/remote-exec/resource_provisioner_test.go b/internal/builtin/provisioners/remote-exec/resource_provisioner_test.go index 549dbf3016..d09e51ac79 100644 --- a/internal/builtin/provisioners/remote-exec/resource_provisioner_test.go +++ b/internal/builtin/provisioners/remote-exec/resource_provisioner_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remoteexec import ( @@ -11,10 +14,10 @@ import ( "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/communicator" "github.com/hashicorp/terraform/internal/communicator/remote" "github.com/hashicorp/terraform/internal/provisioners" - "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" ) @@ -254,6 +257,13 @@ func TestProvisionerTimeout(t *testing.T) { if runErr != nil { t.Fatal(err) } + + // make sure provisioner disconnected after the commands were run + select { + case <-disconnected: + case <-time.After(2 * time.Second): + t.Fatal("communicator did not disconnect") + } } // Validate that Stop can Close can be called even when not provisioning. diff --git a/internal/checks/doc.go b/internal/checks/doc.go index b67aeba354..6f97878cf9 100644 --- a/internal/checks/doc.go +++ b/internal/checks/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package checks contains the models for representing various kinds of // declarative condition checks that can be defined in a Terraform module // and then evaluated and reported by Terraform Core during plan and apply diff --git a/internal/checks/state.go b/internal/checks/state.go index 2d9d7e188d..0464ce8241 100644 --- a/internal/checks/state.go +++ b/internal/checks/state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package checks import ( @@ -9,7 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" ) -// State is a container for state tracking of all of the the checks declared in +// State is a container for state tracking of all of the checks declared in // a particular Terraform configuration and their current statuses. // // A State object is mutable during plan and apply operations but should @@ -33,7 +36,7 @@ type State struct { mu sync.Mutex statuses addrs.Map[addrs.ConfigCheckable, *configCheckableState] - failureMsgs addrs.Map[addrs.Check, string] + failureMsgs addrs.Map[addrs.CheckRule, string] } // configCheckableState is an internal part of type State that represents @@ -53,7 +56,7 @@ type configCheckableState struct { // associated with object declared by this configuration construct. Since // checks are statically declared (even though the checkable objects // aren't) we can compute this only from the configuration. - checkTypes map[addrs.CheckType]int + checkTypes map[addrs.CheckRuleType]int // objects represents the set of dynamic checkable objects associated // with this configuration construct. This is initially nil to represent @@ -64,7 +67,7 @@ type configCheckableState struct { // The leaf Status values will initially be StatusUnknown // and then gradually updated by Terraform Core as it visits the // individual checkable objects and reports their status. - objects addrs.Map[addrs.Checkable, map[addrs.CheckType][]Status] + objects addrs.Map[addrs.Checkable, map[addrs.CheckRuleType][]Status] } // NOTE: For the "Report"-prefixed methods that we use to gradually update @@ -253,7 +256,7 @@ func (c *State) ObjectFailureMessages(addr addrs.Checkable) []string { for checkType, checks := range checksByType { for i, status := range checks { if status == StatusFail { - checkAddr := addrs.NewCheck(addr, checkType, i) + checkAddr := addrs.NewCheckRule(addr, checkType, i) msg := c.failureMsgs.Get(checkAddr) if msg != "" { ret = append(ret, msg) diff --git a/internal/checks/state_init.go b/internal/checks/state_init.go index 6714243b5c..1dbb575d07 100644 --- a/internal/checks/state_init.go +++ b/internal/checks/state_init.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package checks import ( @@ -30,6 +33,10 @@ func collectInitialStatuses(into addrs.Map[addrs.ConfigCheckable, *configCheckab addr := rc.Addr().InModule(moduleAddr) collectInitialStatusForResource(into, addr, rc) } + for _, rc := range cfg.Module.EphemeralResources { + addr := rc.Addr().InModule(moduleAddr) + collectInitialStatusForResource(into, addr, rc) + } for _, oc := range cfg.Module.Outputs { addr := oc.Addr().InModule(moduleAddr) @@ -42,13 +49,45 @@ func collectInitialStatuses(into addrs.Map[addrs.ConfigCheckable, *configCheckab st := &configCheckableState{} - st.checkTypes = map[addrs.CheckType]int{ + st.checkTypes = map[addrs.CheckRuleType]int{ addrs.OutputPrecondition: ct, } into.Put(addr, st) } + for _, c := range cfg.Module.Checks { + addr := c.Addr().InModule(moduleAddr) + + st := &configCheckableState{ + checkTypes: map[addrs.CheckRuleType]int{ + addrs.CheckAssertion: len(c.Asserts), + }, + } + + if c.DataResource != nil { + st.checkTypes[addrs.CheckDataResource] = 1 + } + + into.Put(addr, st) + } + + for _, v := range cfg.Module.Variables { + addr := v.Addr().InModule(moduleAddr) + + vs := len(v.Validations) + if vs == 0 { + continue + } + + st := &configCheckableState{} + st.checkTypes = map[addrs.CheckRuleType]int{ + addrs.InputValidation: vs, + } + + into.Put(addr, st) + } + // Must also visit child modules to collect everything for _, child := range cfg.Children { collectInitialStatuses(into, child) @@ -63,7 +102,7 @@ func collectInitialStatusForResource(into addrs.Map[addrs.ConfigCheckable, *conf } st := &configCheckableState{ - checkTypes: make(map[addrs.CheckType]int), + checkTypes: make(map[addrs.CheckRuleType]int), } if ct := len(rc.Preconditions); ct > 0 { diff --git a/internal/checks/state_report.go b/internal/checks/state_report.go index ccf35ac138..afe07740e2 100644 --- a/internal/checks/state_report.go +++ b/internal/checks/state_report.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package checks import ( "fmt" + "log" "github.com/hashicorp/terraform/internal/addrs" ) @@ -32,14 +36,14 @@ func (c *State) ReportCheckableObjects(configAddr addrs.ConfigCheckable, objectA // At this point we pre-populate all of the check results as StatusUnknown, // so that even if we never hear from Terraform Core again we'll still // remember that these results were all pending. - st.objects = addrs.MakeMap[addrs.Checkable, map[addrs.CheckType][]Status]() + st.objects = addrs.MakeMap[addrs.Checkable, map[addrs.CheckRuleType][]Status]() for _, objectAddr := range objectAddrs { if gotConfigAddr := objectAddr.ConfigCheckable(); !addrs.Equivalent(configAddr, gotConfigAddr) { // All of the given object addresses must belong to the specified configuration address panic(fmt.Sprintf("%s belongs to %s, not %s", objectAddr, gotConfigAddr, configAddr)) } - checks := make(map[addrs.CheckType][]Status, len(st.checkTypes)) + checks := make(map[addrs.CheckRuleType][]Status, len(st.checkTypes)) for checkType, count := range st.checkTypes { // NOTE: This is intentionally a slice of count of the zero value // of Status, which is StatusUnknown to represent that we don't @@ -47,6 +51,7 @@ func (c *State) ReportCheckableObjects(configAddr addrs.ConfigCheckable, objectA checks[checkType] = make([]Status, count) } + log.Printf("[TRACE] ReportCheckableObjects: %s -> %s", configAddr, objectAddr) st.objects.Put(objectAddr, checks) } } @@ -61,7 +66,7 @@ func (c *State) ReportCheckableObjects(configAddr addrs.ConfigCheckable, objectA // // This method will also panic if the specified check already had a known // status; each check should have its result reported only once. -func (c *State) ReportCheckResult(objectAddr addrs.Checkable, checkType addrs.CheckType, index int, status Status) { +func (c *State) ReportCheckResult(objectAddr addrs.Checkable, checkType addrs.CheckRuleType, index int, status Status) { c.mu.Lock() defer c.mu.Unlock() @@ -76,21 +81,21 @@ func (c *State) ReportCheckResult(objectAddr addrs.Checkable, checkType addrs.Ch // situations where the check condition was itself invalid, because that // should be represented by StatusError instead, and the error signalled via // diagnostics as normal. -func (c *State) ReportCheckFailure(objectAddr addrs.Checkable, checkType addrs.CheckType, index int, errorMessage string) { +func (c *State) ReportCheckFailure(objectAddr addrs.Checkable, checkType addrs.CheckRuleType, index int, errorMessage string) { c.mu.Lock() defer c.mu.Unlock() c.reportCheckResult(objectAddr, checkType, index, StatusFail) if c.failureMsgs.Elems == nil { - c.failureMsgs = addrs.MakeMap[addrs.Check, string]() + c.failureMsgs = addrs.MakeMap[addrs.CheckRule, string]() } - checkAddr := addrs.NewCheck(objectAddr, checkType, index) + checkAddr := addrs.NewCheckRule(objectAddr, checkType, index) c.failureMsgs.Put(checkAddr, errorMessage) } // reportCheckResult is shared between both ReportCheckResult and // ReportCheckFailure, and assumes its caller already holds the mutex. -func (c *State) reportCheckResult(objectAddr addrs.Checkable, checkType addrs.CheckType, index int, status Status) { +func (c *State) reportCheckResult(objectAddr addrs.Checkable, checkType addrs.CheckRuleType, index int, status Status) { configAddr := objectAddr.ConfigCheckable() st, ok := c.statuses.GetOk(configAddr) diff --git a/internal/checks/state_test.go b/internal/checks/state_test.go index 8c0f0d447f..7a0c4c5476 100644 --- a/internal/checks/state_test.go +++ b/internal/checks/state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package checks import ( @@ -5,6 +8,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/initwd" @@ -14,8 +18,8 @@ func TestChecksHappyPath(t *testing.T) { const fixtureDir = "testdata/happypath" loader, close := configload.NewLoaderForTests(t) defer close() - inst := initwd.NewModuleInstaller(loader.ModulesDir(), nil) - _, instDiags := inst.InstallModules(context.Background(), fixtureDir, true, initwd.ModuleInstallHooksImpl{}) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, nil) + _, instDiags := inst.InstallModules(context.Background(), fixtureDir, "tests", true, false, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } @@ -62,6 +66,9 @@ func TestChecksHappyPath(t *testing.T) { childOutput := addrs.OutputValue{ Name: "b", }.InModule(moduleChild) + checkBlock := addrs.Check{ + Name: "check", + }.InModule(addrs.RootModule) // First some consistency checks to make sure our configuration is the // shape we are relying on it to be. @@ -77,6 +84,9 @@ func TestChecksHappyPath(t *testing.T) { if addr := resourceNonExist; cfg.Module.ResourceByAddr(addr.Resource) != nil { t.Fatalf("configuration includes %s, which is not supposed to exist", addr) } + if addr := checkBlock; cfg.Module.Checks[addr.Check.Name] == nil { + t.Fatalf("configuration does not include %s", addr) + } ///////////////////////////////////////////////////////////////////////// @@ -109,6 +119,10 @@ func TestChecksHappyPath(t *testing.T) { if addr := resourceNonExist; checks.ConfigHasChecks(addr) { t.Errorf("checks detected for %s, even though it doesn't exist", addr) } + if addr := checkBlock; !checks.ConfigHasChecks(addr) { + t.Errorf("checks not detected for %s", addr) + missing++ + } if missing > 0 { t.Fatalf("missing some configuration objects we'd need for subsequent testing") } @@ -124,6 +138,7 @@ func TestChecksHappyPath(t *testing.T) { resourceC, rootOutput, childOutput, + checkBlock, ) gotConfigAddrs := checks.AllConfigAddrs() if diff := cmp.Diff(wantConfigAddrs, gotConfigAddrs); diff != "" { @@ -153,6 +168,7 @@ func TestChecksHappyPath(t *testing.T) { resourceInstC0 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(0)) resourceInstC1 := resourceC.Resource.Absolute(moduleChildInst).Instance(addrs.IntKey(1)) childOutputInst := childOutput.OutputValue.Absolute(moduleChildInst) + checkBlockInst := checkBlock.Check.Absolute(addrs.RootModuleInstance) checks.ReportCheckableObjects(resourceA, addrs.MakeSet[addrs.Checkable](resourceInstA)) checks.ReportCheckResult(resourceInstA, addrs.ResourcePrecondition, 0, StatusPass) @@ -172,6 +188,9 @@ func TestChecksHappyPath(t *testing.T) { checks.ReportCheckableObjects(rootOutput, addrs.MakeSet[addrs.Checkable](rootOutputInst)) checks.ReportCheckResult(rootOutputInst, addrs.OutputPrecondition, 0, StatusPass) + checks.ReportCheckableObjects(checkBlock, addrs.MakeSet[addrs.Checkable](checkBlockInst)) + checks.ReportCheckResult(checkBlockInst, addrs.CheckAssertion, 0, StatusPass) + ///////////////////////////////////////////////////////////////////////// // This "section" is simulating what we might do to report the results @@ -185,7 +204,7 @@ func TestChecksHappyPath(t *testing.T) { t.Errorf("incorrect final aggregate check status for %s: %s, but want %s", configAddr, got, want) } } - if got, want := configCount, 5; got != want { + if got, want := configCount, 6; got != want { t.Errorf("incorrect number of known config addresses %d; want %d", got, want) } } @@ -198,6 +217,7 @@ func TestChecksHappyPath(t *testing.T) { resourceInstC0, resourceInstC1, childOutputInst, + checkBlockInst, ) for _, addr := range objAddrs { if got, want := checks.ObjectCheckStatus(addr), StatusPass; got != want { diff --git a/internal/checks/status.go b/internal/checks/status.go index e95538609c..557fa05ba5 100644 --- a/internal/checks/status.go +++ b/internal/checks/status.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package checks import ( @@ -10,7 +13,7 @@ import ( // checkable object. type Status rune -//go:generate go run golang.org/x/tools/cmd/stringer -type=Status +//go:generate go tool golang.org/x/tools/cmd/stringer -type=Status const ( // StatusUnknown represents that there is not yet a conclusive result diff --git a/internal/checks/testdata/happypath/checks-happypath.tf b/internal/checks/testdata/happypath/checks-happypath.tf index 4a6ca46fca..a9cd055b7e 100644 --- a/internal/checks/testdata/happypath/checks-happypath.tf +++ b/internal/checks/testdata/happypath/checks-happypath.tf @@ -30,3 +30,10 @@ output "a" { error_message = "A has no id." } } + +check "check" { + assert { + condition = null_resource.a.id != "" + error_message = "check block: A has no id" + } +} diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index 669cc37227..97e5d845dc 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -1,27 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "context" + "errors" "fmt" "log" "net/http" "net/url" "os" + "slices" "sort" "strings" "sync" - "time" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" - "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -38,9 +44,12 @@ const ( tfeServiceID = "tfe.v2" headerSourceKey = "X-Terraform-Integration" headerSourceValue = "cloud" + genericHostname = "localterraform.com" ) -// Cloud is an implementation of EnhancedBackend in service of the Terraform Cloud/Enterprise +var ErrCloudDoesNotSupportKVTags = errors.New("your version of Terraform Enterprise does not support key-value tags. Please upgrade Terraform Enterprise to a version that supports this feature or use set type tags instead.") + +// Cloud is an implementation of backendrun.OperationsBackend in service of the HCP Terraform or Terraform Enterprise // integration for Terraform CLI. This backend is not intended to be surfaced at the user level and // is instead an implementation detail of cloud.Cloud. type Cloud struct { @@ -54,27 +63,43 @@ type Cloud struct { // Operation. See Operation for more details. ContextOpts *terraform.ContextOpts - // client is the Terraform Cloud/Enterprise API client. + // client is the HCP Terraform or Terraform Enterprise API client. client *tfe.Client - // lastRetry is set to the last time a request was retried. - lastRetry time.Time + // viewHooks implements functions integrating the tfe.Client with the CLI + // output. + viewHooks views.CloudHooks - // hostname of Terraform Cloud or Terraform Enterprise - hostname string + // Hostname of HCP Terraform or Terraform Enterprise + Hostname string - // organization is the organization that contains the target workspaces. - organization string + // Token for HCP Terraform or Terraform Enterprise + Token string + + // Organization is the Organization that contains the target workspaces. + Organization string // WorkspaceMapping contains strategies for mapping CLI workspaces in the working directory - // to remote Terraform Cloud workspaces. + // to remote HCP Terraform workspaces. WorkspaceMapping WorkspaceMapping + // ServicesHost is the full account of discovered Terraform services at the + // HCP Terraform instance. It should include at least the tfe v2 API, and + // possibly other services. + ServicesHost *disco.Host + + // appName is the name of the instance the cloud backend is currently + // configured against + appName string + // services is used for service discovery services *disco.Disco - // local allows local operations, where Terraform Cloud serves as a state storage backend. - local backend.Enhanced + // renderer is used for rendering JSON plan output and streamed logs. + renderer *jsonformat.Renderer + + // local allows local operations, where HCP Terraform serves as a state storage backend. + local backendrun.OperationsBackend // forceLocal, if true, will force the use of the local backend. forceLocal bool @@ -96,8 +121,8 @@ type Cloud struct { } var _ backend.Backend = (*Cloud)(nil) -var _ backend.Enhanced = (*Cloud)(nil) -var _ backend.Local = (*Cloud)(nil) +var _ backendrun.OperationsBackend = (*Cloud)(nil) +var _ backendrun.Local = (*Cloud)(nil) // New creates a new initialized cloud backend. func New(services *disco.Disco) *Cloud { @@ -106,7 +131,7 @@ func New(services *disco.Disco) *Cloud { } } -// ConfigSchema implements backend.Enhanced. +// ConfigSchema implements backend.Backend (which is embedded in backendrun.OperationsBackend). func (b *Cloud) ConfigSchema() *configschema.Block { return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -136,8 +161,13 @@ func (b *Cloud) ConfigSchema() *configschema.Block { Optional: true, Description: schemaDescriptionName, }, + "project": { + Type: cty.String, + Optional: true, + Description: schemaDescriptionProject, + }, "tags": { - Type: cty.Set(cty.String), + Type: cty.DynamicPseudoType, Optional: true, Description: schemaDescriptionTags, }, @@ -149,65 +179,103 @@ func (b *Cloud) ConfigSchema() *configschema.Block { } } -// PrepareConfig implements backend.Backend. +// PrepareConfig implements backend.Backend (which is embedded in backendrun.OperationsBackend). +// Per the interface contract, it should catch invalid contents in the config value and populate +// knowable default values, but must NOT consult environment variables or other knowledge +// outside the config value itself. func (b *Cloud) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if obj.IsNull() { return obj, diags } - // check if organization is specified in the config. - if val := obj.GetAttr("organization"); val.IsNull() || val.AsString() == "" { - // organization is specified in the config but is invalid, so - // we'll fallback on TF_CLOUD_ORGANIZATION - if val := os.Getenv("TF_CLOUD_ORGANIZATION"); val == "" { - diags = diags.Append(missingConfigAttributeAndEnvVar("organization", "TF_CLOUD_ORGANIZATION")) - } - } - - WorkspaceMapping := WorkspaceMapping{} + // Since this backend uses environment variables extensively, this function + // can't do very much! We do our main validity checks in resolveCloudConfig, + // which is allowed to resolve fallback values from the environment. About + // the only thing we can check for here is whether the conflicting `name` + // and `tags` attributes are both set. if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() { if val := workspaces.GetAttr("name"); !val.IsNull() { - WorkspaceMapping.Name = val.AsString() - } - if val := workspaces.GetAttr("tags"); !val.IsNull() { - err := gocty.FromCtyValue(val, &WorkspaceMapping.Tags) - if err != nil { - log.Panicf("An unxpected error occurred: %s", err) + if val := workspaces.GetAttr("tags"); !val.IsNull() { + diags = diags.Append(invalidWorkspaceConfigMisconfiguration) } } - } else { - WorkspaceMapping.Name = os.Getenv("TF_WORKSPACE") - } - - switch WorkspaceMapping.Strategy() { - // Make sure have a workspace mapping strategy present - case WorkspaceNoneStrategy: - diags = diags.Append(invalidWorkspaceConfigMissingValues) - // Make sure that a workspace name is configured. - case WorkspaceInvalidStrategy: - diags = diags.Append(invalidWorkspaceConfigMisconfiguration) } return obj, diags } -// Configure implements backend.Enhanced. +func (b *Cloud) ServiceDiscoveryAliases() ([]backendrun.HostAlias, error) { + aliasHostname, err := svchost.ForComparison(genericHostname) + if err != nil { + // This should never happen because the hostname is statically defined. + return nil, fmt.Errorf("failed to create backend alias from alias %q. The hostname is not in the correct format. This is a bug in the backend", genericHostname) + } + + targetHostname, err := svchost.ForComparison(b.Hostname) + if err != nil { + // This should never happen because the 'to' alias is the backend host, which has + // already been ev + return nil, fmt.Errorf("failed to create backend alias to target %q. The hostname is not in the correct format.", b.Hostname) + } + + return []backendrun.HostAlias{ + { + From: aliasHostname, + To: targetHostname, + }, + }, nil +} + +// Configure implements backend.Backend (which is embedded in backendrun.OperationsBackend). func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if obj.IsNull() { return diags } - diagErr := b.setConfigurationFields(obj) - if diagErr.HasErrors() { - return diagErr + // Combine environment variables and the cloud block to get the full config. + // We are now completely done with `obj`! + config, configDiags := resolveCloudConfig(obj) + diags = diags.Append(configDiags) + if diags.HasErrors() { + return diags } - // Discover the service URL to confirm that it provides the Terraform Cloud/Enterprise API - service, err := b.discover() + // Use resolved config to set fields on backend (except token, see below) + b.Hostname = config.hostname + b.Organization = config.organization + b.WorkspaceMapping = config.workspaceMapping - // Check for errors before we continue. + // Discover the service URL to confirm that it provides the Terraform + // Cloud/Enterprise API... and while we're at it, cache the full discovery + // results. + var tfcService *url.URL + var host *disco.Host + // We want to handle errors from URL normalization and service discovery in + // the same way. So we only perform each step if there wasn't a previous + // error, and use the same block to handle errors from anywhere in the + // process. + hostname, err := svchost.ForComparison(b.Hostname) + if err == nil { + host, err = b.services.Discover(hostname) + + if err == nil { + // The discovery request worked, so cache the full results. + b.ServicesHost = host + + // Find the TFE API service URL + tfcService, err = host.ServiceURL(tfeServiceID) + } else { + // Network errors from Discover() can read like non-sequiters, so we wrap em. + var serviceDiscoErr *disco.ErrServiceDiscoveryNetworkRequest + if errors.As(err, &serviceDiscoErr) { + err = fmt.Errorf("a network issue prevented cloud configuration; %w", err) + } + } + } + + // Handle any errors from URL normalization and service discovery before we continue. if err != nil { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, @@ -218,16 +286,13 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } - // First we'll retrieve the token from the configuration - var token string - if val := obj.GetAttr("token"); !val.IsNull() { - token = val.AsString() - } + // Token time. First, see if the configuration had one: + token := config.token // Get the token from the CLI Config File in the credentials section - // if no token was not set in the configuration + // if no token was set in the configuration if token == "" { - token, err = b.token() + token, err = cliConfigToken(hostname, b.services) if err != nil { diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, @@ -242,25 +307,27 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { // Return an error if we still don't have a token at this point. if token == "" { loginCommand := "terraform login" - if b.hostname != defaultHostname { - loginCommand = loginCommand + " " + b.hostname + if b.Hostname != defaultHostname { + loginCommand = loginCommand + " " + b.Hostname } diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Required token could not be found", fmt.Sprintf( "Run the following command to generate a token for %s:\n %s", - b.hostname, + b.Hostname, loginCommand, ), )) return diags } + b.Token = token + if b.client == nil { cfg := &tfe.Config{ - Address: service.String(), - BasePath: service.Path, + Address: tfcService.String(), + BasePath: tfcService.Path, Token: token, Headers: make(http.Header), RetryLogHook: b.retryLogHook, @@ -270,33 +337,40 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { cfg.Headers.Set(tfversion.Header, tfversion.Version) cfg.Headers.Set(headerSourceKey, headerSourceValue) - // Create the TFC/E API client. + // Create the HCP Terraform API client. b.client, err = tfe.NewClient(cfg) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Failed to create the Terraform Cloud/Enterprise client", + "Failed to create the HCP Terraform or Terraform Enterprise client", fmt.Sprintf( `Encountered an unexpected error while creating the `+ - `Terraform Cloud/Enterprise client: %s.`, err, + `HCP Terraform or Terraform Enterprise client: %s.`, err, ), )) return diags } } + // Read the app name header and if empty, provide a default + b.appName = b.client.AppName() + // Validate the header's value to ensure no tampering + if !isValidAppName(b.appName) { + b.appName = "HCP Terraform" + } + // Check if the organization exists by reading its entitlements. - entitlements, err := b.client.Organizations.ReadEntitlements(context.Background(), b.organization) + entitlements, err := b.client.Organizations.ReadEntitlements(context.Background(), b.Organization) if err != nil { if err == tfe.ErrResourceNotFound { err = fmt.Errorf("organization %q at host %s not found.\n\n"+ "Please ensure that the organization and hostname are correct "+ "and that your API token for %s is valid.", - b.organization, b.hostname, b.hostname) + b.Organization, b.Hostname, b.Hostname) } diags = diags.Append(tfdiags.AttributeValue( tfdiags.Error, - fmt.Sprintf("Failed to read organization %q at host %s", b.organization, b.hostname), + fmt.Sprintf("Failed to read organization %q at host %s", b.Organization, b.Hostname), fmt.Sprintf("Encountered an unexpected error while reading the "+ "organization settings: %s", err), cty.Path{cty.GetAttrStep{Name: "organization"}}, @@ -304,9 +378,10 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } + // If TF_WORKSPACE specifies a current workspace to use, make sure it's usable. if ws, ok := os.LookupEnv("TF_WORKSPACE"); ok { - if ws == b.WorkspaceMapping.Name || b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy { - diag := b.validWorkspaceEnvVar(context.Background(), b.organization, ws) + if ws == b.WorkspaceMapping.Name || b.WorkspaceMapping.IsTagsStrategy() { + diag := b.validWorkspaceEnvVar(context.Background(), b.Organization, ws) if diag != nil { diags = diags.Append(diag) return diags @@ -332,7 +407,7 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { tfdiags.Sourceless( tfdiags.Error, "Unsupported Terraform Enterprise version", - cloudIntegrationUsedInUnsupportedTFE, + fmt.Sprintf(cloudIntegrationUsedInUnsupportedTFE, b.appName), ), ) } else { @@ -347,7 +422,9 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { // Configure a local backend for when we need to run operations locally. b.local = backendLocal.NewWithBackend(b) - b.forceLocal = b.forceLocal || !entitlements.Operations + + // Determine if we are forced to use the local backend. + b.forceLocal = os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" || !entitlements.Operations // Enable retries for server errors as the backend is now fully configured. b.client.RetryServerErrors(true) @@ -355,87 +432,170 @@ func (b *Cloud) Configure(obj cty.Value) tfdiags.Diagnostics { return diags } -func (b *Cloud) setConfigurationFields(obj cty.Value) tfdiags.Diagnostics { +func (b *Cloud) AppName() string { + if isValidAppName(b.appName) { + return b.appName + } + return "HCP Terraform" +} + +// resolveCloudConfig fills in a potentially incomplete cloud config block using +// environment variables and defaults. If the returned Diagnostics are clean, +// the resulting value is a logically valid cloud config. If the Diagnostics +// contain any errors, the resolved config value is invalid and should not be +// used. Note that this function does not verify that any objects referenced in +// the config actually exist in the remote system; it only validates that the +// resulting config is internally consistent. +func resolveCloudConfig(obj cty.Value) (cloudConfig, tfdiags.Diagnostics) { + var ret cloudConfig var diags tfdiags.Diagnostics - // Get the hostname. - b.hostname = os.Getenv("TF_CLOUD_HOSTNAME") + // Get the hostname. Config beats environment. Absent means use the default + // hostname. if val := obj.GetAttr("hostname"); !val.IsNull() && val.AsString() != "" { - b.hostname = val.AsString() - } else if b.hostname == "" { - b.hostname = defaultHostname + ret.hostname = val.AsString() + log.Printf("[TRACE] cloud: using hostname %q from cloud config block", ret.hostname) + } else { + ret.hostname = os.Getenv("TF_CLOUD_HOSTNAME") + log.Printf("[TRACE] cloud: using hostname %q from TF_CLOUD_HOSTNAME variable", ret.hostname) + } + if ret.hostname == "" { + ret.hostname = defaultHostname + log.Printf("[TRACE] cloud: using default hostname %q", ret.hostname) } - // We can have two options, setting the organization via the config - // or using TF_CLOUD_ORGANIZATION. Since PrepareConfig() validates that one of these - // values must exist, we'll initially set it to the env var and override it if - // specified in the configuration. - b.organization = os.Getenv("TF_CLOUD_ORGANIZATION") - - // Check if the organization is present and valid in the config. + // Get the organization. Config beats environment. There's no default, so + // absent means error. if val := obj.GetAttr("organization"); !val.IsNull() && val.AsString() != "" { - b.organization = val.AsString() + ret.organization = val.AsString() + log.Printf("[TRACE] cloud: using organization %q from cloud config block", ret.organization) + } else { + ret.organization = os.Getenv("TF_CLOUD_ORGANIZATION") + log.Printf("[TRACE] cloud: using organization %q from TF_CLOUD_ORGANIZATION variable", ret.organization) + } + if ret.organization == "" { + diags = diags.Append(missingConfigAttributeAndEnvVar("organization", "TF_CLOUD_ORGANIZATION")) } - // Get the workspaces configuration block and retrieve the - // default workspace name. - if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() { + // Get the token. We only report what's in the config! An empty value is + // ok; later, after this function is called, Configure() can try to resolve + // per-hostname credentials from a variety of sources (including + // hostname-specific env vars). + if val := obj.GetAttr("token"); !val.IsNull() { + ret.token = val.AsString() + log.Printf("[TRACE] cloud: found token in cloud config block") + } - // PrepareConfig checks that you cannot set both of these. + // Grab any workspace/project info from the nested config object in one go, + // so it's easier to work with. + var name, project string + if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() { if val := workspaces.GetAttr("name"); !val.IsNull() { - b.WorkspaceMapping.Name = val.AsString() + name = val.AsString() + log.Printf("[TRACE] cloud: found workspace name %q in cloud config block", name) } if val := workspaces.GetAttr("tags"); !val.IsNull() { - var tags []string - err := gocty.FromCtyValue(val, &tags) - if err != nil { - log.Panicf("An unexpected error occurred: %s", err) + log.Printf("[TRACE] tags is a %q type", val.Type().FriendlyName()) + tagsAsMap := make(map[string]string) + if val.Type().IsObjectType() || val.Type().IsMapType() { + for k, v := range val.AsValueMap() { + if v.Type() != cty.String { + diags = diags.Append(errors.New("tag object values must be strings")) + return ret, diags + } + tagsAsMap[k] = v.AsString() + } + log.Printf("[TRACE] cloud: using tags %q from cloud config block", tagsAsMap) + ret.workspaceMapping.TagsAsMap = tagsAsMap + } else if val.Type().IsTupleType() || val.Type().IsSetType() { + var tagsAsSet []string + length := val.LengthInt() + if length > 0 { + it := val.ElementIterator() + for it.Next() { + _, v := it.Element() + if !v.Type().Equals(cty.String) { + diags = diags.Append(errors.New("tag elements must be strings")) + return ret, diags + } + if vs := v.AsString(); vs != "" { + tagsAsSet = append(tagsAsSet, vs) + } + } + } + + log.Printf("[TRACE] cloud: using tags %q from cloud config block", tagsAsSet) + ret.workspaceMapping.TagsAsSet = tagsAsSet + } else { + diags = diags.Append(fmt.Errorf("tags must be a set or object, not %s", val.Type().FriendlyName())) + return ret, diags } - - b.WorkspaceMapping.Tags = tags } + if val := workspaces.GetAttr("project"); !val.IsNull() { + project = val.AsString() + log.Printf("[TRACE] cloud: found project name %q in cloud config block", project) + } + } + + // Get the project. Config beats environment, and the default value is the + // empty string. + if project != "" { + ret.workspaceMapping.Project = project + log.Printf("[TRACE] cloud: using project %q from cloud config block", ret.workspaceMapping.Project) } else { - b.WorkspaceMapping.Name = os.Getenv("TF_WORKSPACE") + ret.workspaceMapping.Project = os.Getenv("TF_CLOUD_PROJECT") + log.Printf("[TRACE] cloud: using project %q from TF_CLOUD_PROJECT variable", ret.workspaceMapping.Project) } - // Determine if we are forced to use the local backend. - b.forceLocal = os.Getenv("TF_FORCE_LOCAL_BACKEND") != "" + // Get the name, and validate the WorkspaceMapping as a whole. This is the + // only real tricky one, because TF_WORKSPACE is used in places beyond + // the cloud backend config. The rules are: + // - If the config had neither `name` nor `tags`, we fall back to TF_WORKSPACE as the name. + // - If the config had `tags`, it's still legal to set TF_WORKSPACE, and it indicates + // which workspace should be *current,* but we leave Name blank in the mapping. + // This is mostly useful in CI. + // - If the config had `name`, it's NOT LEGAL to set TF_WORKSPACE, but we make + // an exception if it's the same as the specified `name` because the intent was clear. - return diags + // Start out with the name from the config (if any) + ret.workspaceMapping.Name = name + + // Then examine the combination of name + tags: + switch ret.workspaceMapping.Strategy() { + // Invalid can't really happen here because b.PrepareConfig() already + // checked for it. But, still: + case WorkspaceInvalidStrategy: + diags = diags.Append(invalidWorkspaceConfigMisconfiguration) + // If both name and TF_WORKSPACE are set, error (unless they match) + case WorkspaceNameStrategy: + if tfws, ok := os.LookupEnv("TF_WORKSPACE"); ok && tfws != ret.workspaceMapping.Name { + diags = diags.Append(invalidWorkspaceConfigNameConflict) + } else { + log.Printf("[TRACE] cloud: using workspace name %q from cloud config block", ret.workspaceMapping.Name) + } + // If config had nothing, use TF_WORKSPACE. + case WorkspaceNoneStrategy: + ret.workspaceMapping.Name = os.Getenv("TF_WORKSPACE") + log.Printf("[TRACE] cloud: using workspace name %q from TF_WORKSPACE variable", ret.workspaceMapping.Name) + // And, if config only had tags, do nothing. + } + + // If our workspace mapping is still None after all that, then we don't have + // a valid completed config! + if ret.workspaceMapping.Strategy() == WorkspaceNoneStrategy { + diags = diags.Append(invalidWorkspaceConfigMissingValues) + } + + return ret, diags } -// discover the TFC/E API service URL and version constraints. -func (b *Cloud) discover() (*url.URL, error) { - hostname, err := svchost.ForComparison(b.hostname) - if err != nil { - return nil, err - } - - host, err := b.services.Discover(hostname) - if err != nil { - return nil, err - } - - service, err := host.ServiceURL(tfeServiceID) - // Return the error, unless its a disco.ErrVersionNotSupported error. - if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil { - return nil, err - } - - return service, err -} - -// token returns the token for this host as configured in the credentials +// cliConfigToken returns the token for this host as configured in the credentials // section of the CLI Config File. If no token was configured, an empty // string will be returned instead. -func (b *Cloud) token() (string, error) { - hostname, err := svchost.ForComparison(b.hostname) +func cliConfigToken(hostname svchost.Hostname, services *disco.Disco) (string, error) { + creds, err := services.CredentialsForHost(hostname) if err != nil { - return "", err - } - creds, err := b.services.CredentialsForHost(hostname) - if err != nil { - log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err) + log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", hostname.ForDisplay(), err) return "", nil } if creds != nil { @@ -448,29 +608,14 @@ func (b *Cloud) token() (string, error) { // backend to log any connection issues to prevent data loss. func (b *Cloud) retryLogHook(attemptNum int, resp *http.Response) { if b.CLI != nil { - // Ignore the first retry to make sure any delayed output will - // be written to the console before we start logging retries. - // - // The retry logic in the TFE client will retry both rate limited - // requests and server errors, but in the cloud backend we only - // care about server errors so we ignore rate limit (429) errors. - if attemptNum == 0 || (resp != nil && resp.StatusCode == 429) { - // Reset the last retry time. - b.lastRetry = time.Now() - return - } - - if attemptNum == 1 { - b.CLI.Output(b.Colorize().Color(strings.TrimSpace(initialRetryError))) - } else { - b.CLI.Output(b.Colorize().Color(strings.TrimSpace( - fmt.Sprintf(repeatedRetryError, time.Since(b.lastRetry).Round(time.Second))))) + if output := b.viewHooks.RetryLogHook(attemptNum, resp, true); len(output) > 0 { + b.CLI.Output(b.Colorize().Color(output)) } } } -// Workspaces implements backend.Enhanced, returning a filtered list of workspace names according to -// the workspace mapping strategy configured. +// Workspaces implements backend.Backend (which is embedded in backendrun.OperationsBackend), +// returning a filtered list of workspace names according to the workspace mapping strategy configured. func (b *Cloud) Workspaces() ([]string, error) { // Create a slice to contain all the names. var names []string @@ -482,16 +627,45 @@ func (b *Cloud) Workspaces() ([]string, error) { return names, nil } - // Otherwise, multiple workspaces are being mapped. Query Terraform Cloud for all the remote + // Otherwise, multiple workspaces are being mapped. Query HCP Terraform for all the remote // workspaces by the provided mapping strategy. options := &tfe.WorkspaceListOptions{} if b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy { - taglist := strings.Join(b.WorkspaceMapping.Tags, ",") - options.Tags = taglist + options.Tags = strings.Join(b.WorkspaceMapping.TagsAsSet, ",") + } else if b.WorkspaceMapping.Strategy() == WorkspaceKVTagsStrategy { + options.TagBindings = b.WorkspaceMapping.asTFETagBindings() + + // Populate keys, too, just in case backend does not support key/value tags. + // The backend will end up applying both filters but that should always + // be the same result set anyway. + for _, tag := range options.TagBindings { + if options.Tags != "" { + options.Tags = options.Tags + "," + } + options.Tags = options.Tags + tag.Key + } + + } + log.Printf("[TRACE] cloud: Listing workspaces with tag bindings %q", b.WorkspaceMapping.DescribeTags()) + + if b.WorkspaceMapping.Project != "" { + listOpts := &tfe.ProjectListOptions{ + Name: b.WorkspaceMapping.Project, + } + projects, err := b.client.Projects.List(context.Background(), b.Organization, listOpts) + if err != nil && err != tfe.ErrResourceNotFound { + return nil, fmt.Errorf("failed to retrieve project %s: %v", listOpts.Name, err) + } + for _, p := range projects.Items { + if p.Name == b.WorkspaceMapping.Project { + options.ProjectID = p.ID + break + } + } } for { - wl, err := b.client.Workspaces.List(context.Background(), b.organization, options) + wl, err := b.client.Workspaces.List(context.Background(), b.Organization, options) if err != nil { return nil, err } @@ -515,8 +689,8 @@ func (b *Cloud) Workspaces() ([]string, error) { return names, nil } -// DeleteWorkspace implements backend.Enhanced. -func (b *Cloud) DeleteWorkspace(name string) error { +// DeleteWorkspace implements backend.Backend (which is embedded in backendrun.OperationsBackend). +func (b *Cloud) DeleteWorkspace(name string, force bool) error { if name == backend.DefaultStateName { return backend.ErrDefaultWorkspaceNotSupported } @@ -525,19 +699,21 @@ func (b *Cloud) DeleteWorkspace(name string) error { return backend.ErrWorkspacesNotSupported } - // Configure the remote workspace name. - client := &remoteClient{ - client: b.client, - organization: b.organization, - workspace: &tfe.Workspace{ - Name: name, - }, + workspace, err := b.client.Workspaces.Read(context.Background(), b.Organization, name) + if err == tfe.ErrResourceNotFound { + return nil // If the workspace does not exist, succeed } - return client.Delete() + if err != nil { + return fmt.Errorf("failed to retrieve workspace %s: %v", name, err) + } + + // Configure the remote workspace name. + State := &State{tfeClient: b.client, organization: b.Organization, workspace: workspace, enableIntermediateSnapshots: false} + return State.Delete(force) } -// StateMgr implements backend.Enhanced. +// StateMgr implements backend.Backend (which is embedded in backendrun.OperationsBackend). func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { var remoteTFVersion string @@ -549,7 +725,7 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { return nil, backend.ErrWorkspacesNotSupported } - workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, name) + workspace, err := b.client.Workspaces.Read(context.Background(), b.Organization, name) if err != nil && err != tfe.ErrResourceNotFound { return nil, fmt.Errorf("Failed to retrieve workspace %s: %v", name, err) } @@ -557,17 +733,71 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { remoteTFVersion = workspace.TerraformVersion } - if err == tfe.ErrResourceNotFound { - // Create a workspace - options := tfe.WorkspaceCreateOptions{ - Name: tfe.String(name), - Tags: b.WorkspaceMapping.tfeTags(), + var configuredProject *tfe.Project + + // Attempt to find project if configured + if b.WorkspaceMapping.Project != "" { + listOpts := &tfe.ProjectListOptions{ + Name: b.WorkspaceMapping.Project, + } + projects, err := b.client.Projects.List(context.Background(), b.Organization, listOpts) + if err != nil && err != tfe.ErrResourceNotFound { + // This is a failure to make an API request, fail to initialize + return nil, fmt.Errorf("Attempted to find configured project %s but was unable to.", b.WorkspaceMapping.Project) + } + for _, p := range projects.Items { + if p.Name == b.WorkspaceMapping.Project { + configuredProject = p + break + } } - log.Printf("[TRACE] cloud: Creating Terraform Cloud workspace %s/%s", b.organization, name) - workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options) + if configuredProject == nil { + // We were able to read project, but were unable to find the configured project + // This is not fatal as we may attempt to create the project if we need to create + // the workspace + log.Printf("[TRACE] cloud: Attempted to find configured project %s but was unable to.", b.WorkspaceMapping.Project) + } + } + + if err == tfe.ErrResourceNotFound { + // Create workspace if it was not found + + // Workspace Create Options + workspaceCreateOptions := tfe.WorkspaceCreateOptions{ + Name: tfe.String(name), + Project: configuredProject, + } + + if b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy { + workspaceCreateOptions.Tags = b.WorkspaceMapping.tfeTags() + } else if b.WorkspaceMapping.Strategy() == WorkspaceKVTagsStrategy { + workspaceCreateOptions.TagBindings = b.WorkspaceMapping.asTFETagBindings() + } + + // Create project if not exists, otherwise use it + if workspaceCreateOptions.Project == nil && b.WorkspaceMapping.Project != "" { + // If we didn't find the project, try to create it + if workspaceCreateOptions.Project == nil { + createOpts := tfe.ProjectCreateOptions{ + Name: b.WorkspaceMapping.Project, + } + // didn't find project, create it instead + log.Printf("[TRACE] cloud: Creating %s project %s/%s", b.appName, b.Organization, b.WorkspaceMapping.Project) + project, err := b.client.Projects.Create(context.Background(), b.Organization, createOpts) + if err != nil && err != tfe.ErrResourceNotFound { + return nil, fmt.Errorf("failed to create project %s: %v", b.WorkspaceMapping.Project, err) + } + configuredProject = project + workspaceCreateOptions.Project = configuredProject + } + } + + // Create a workspace + log.Printf("[TRACE] cloud: Creating %s workspace %s/%s", b.appName, b.Organization, name) + workspace, err = b.client.Workspaces.Create(context.Background(), b.Organization, workspaceCreateOptions) if err != nil { - return nil, fmt.Errorf("Error creating workspace %s: %v", name, err) + return nil, fmt.Errorf("error creating workspace %s: %v", name, err) } remoteTFVersion = workspace.TerraformVersion @@ -587,22 +817,38 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { // object to do a nicely formatted message, so we're just assuming the // issue was that the version wasn't available since that's probably what // happened. - log.Printf("[TRACE] cloud: Attempted to select version %s for TFC workspace; unavailable, so %s will be used instead.", tfversion.String(), workspace.TerraformVersion) + log.Printf("[TRACE] cloud: Attempted to select version %s for this %s workspace; unavailable, so %s will be used instead.", tfversion.String(), b.appName, workspace.TerraformVersion) if b.CLI != nil { - versionUnavailable := fmt.Sprintf(unavailableTerraformVersion, tfversion.String(), workspace.TerraformVersion) + versionUnavailable := fmt.Sprintf(unavailableTerraformVersion, tfversion.String(), b.appName, workspace.TerraformVersion) b.CLI.Output(b.Colorize().Color(versionUnavailable)) } } } - if b.workspaceTagsRequireUpdate(workspace, b.WorkspaceMapping) { - options := tfe.WorkspaceAddTagsOptions{ - Tags: b.WorkspaceMapping.tfeTags(), + tagCheck, errFromTagCheck := b.workspaceTagsRequireUpdate(context.Background(), workspace, b.WorkspaceMapping) + if tagCheck.requiresUpdate { + if errFromTagCheck != nil { + if errors.Is(errFromTagCheck, ErrCloudDoesNotSupportKVTags) { + return nil, fmt.Errorf("backend does not support key/value tags. Try using key-only tags: %w", errFromTagCheck) + } } - log.Printf("[TRACE] cloud: Adding tags for Terraform Cloud workspace %s/%s", b.organization, name) - err = b.client.Workspaces.AddTags(context.Background(), workspace.ID, options) + + log.Printf("[TRACE] cloud: Updating tags for %s workspace %s/%s to %q", b.appName, b.Organization, name, b.WorkspaceMapping.DescribeTags()) + // Always update using KV tags if possible + if !tagCheck.supportsKVTags { + options := tfe.WorkspaceAddTagsOptions{ + Tags: b.WorkspaceMapping.tfeTags(), + } + err = b.client.Workspaces.AddTags(context.Background(), workspace.ID, options) + } else { + options := tfe.WorkspaceAddTagBindingsOptions{ + TagBindings: b.WorkspaceMapping.asTFETagBindings(), + } + _, err = b.client.Workspaces.AddTagBindings(context.Background(), workspace.ID, options) + } + if err != nil { - return nil, fmt.Errorf("Error updating workspace %s: %v", name, err) + return nil, fmt.Errorf("error updating workspace %q tags: %w", name, err) } } @@ -619,22 +865,13 @@ func (b *Cloud) StateMgr(name string) (statemgr.Full, error) { } } - client := &remoteClient{ - client: b.client, - organization: b.organization, - workspace: workspace, - - // This is optionally set during Terraform Enterprise runs. - runID: os.Getenv("TFE_RUN_ID"), - } - - return NewState(client), nil + return &State{tfeClient: b.client, organization: b.Organization, workspace: workspace, enableIntermediateSnapshots: false}, nil } -// Operation implements backend.Enhanced. -func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend.RunningOperation, error) { +// Operation implements backendrun.OperationsBackend. +func (b *Cloud) Operation(ctx context.Context, op *backendrun.Operation) (*backendrun.RunningOperation, error) { // Retrieve the workspace for this operation. - w, err := b.fetchWorkspace(ctx, b.organization, op.Workspace) + w, err := b.fetchWorkspace(ctx, b.Organization, op.Workspace) if err != nil { return nil, err } @@ -645,7 +882,7 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. // - Running remotely, in which case the local version is irrelevant; // - Workspace configured for local operations, in which case the remote // version is meaningless; - // - Forcing local operations, which should only happen in the Terraform Cloud worker, in + // - Forcing local operations, which should only happen in the HCP Terraform worker, in // which case the Terraform versions by definition match. b.IgnoreVersionConflict() @@ -661,13 +898,13 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. op.Workspace = w.Name // Determine the function to call for our operation - var f func(context.Context, context.Context, *backend.Operation, *tfe.Workspace) (*tfe.Run, error) + var f func(context.Context, context.Context, *backendrun.Operation, *tfe.Workspace) (*tfe.Run, error) switch op.Type { - case backend.OperationTypePlan: + case backendrun.OperationTypePlan: f = b.opPlan - case backend.OperationTypeApply: + case backendrun.OperationTypeApply: f = b.opApply - case backend.OperationTypeRefresh: + case backendrun.OperationTypeRefresh: // The `terraform refresh` command has been deprecated in favor of `terraform apply -refresh-state`. // Rather than respond with an error telling the user to run the other command we can just run // that command instead. We will tell the user what we are doing, and then do it. @@ -680,7 +917,7 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. f = b.opApply default: return nil, fmt.Errorf( - "\n\nTerraform Cloud does not support the %q operation.", op.Type) + "\n\n%s does not support the %q operation.", b.appName, op.Type) } // Lock @@ -689,7 +926,7 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. // Build our running operation // the runninCtx is only used to block until the operation returns. runningCtx, done := context.WithCancel(context.Background()) - runningOp := &backend.RunningOperation{ + runningOp := &backendrun.RunningOperation{ Context: runningCtx, PlanEmpty: true, } @@ -720,7 +957,7 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. } if r == nil && opErr == context.Canceled { - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure return } @@ -729,7 +966,7 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. r, err := b.client.Runs.Read(cancelCtx, r.ID) if err != nil { var diags tfdiags.Diagnostics - diags = diags.Append(generalError("Failed to retrieve run", err)) + diags = diags.Append(b.generalError("Failed to retrieve run", err)) op.ReportResult(runningOp, diags) return } @@ -740,14 +977,14 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. if opErr == context.Canceled { if err := b.cancel(cancelCtx, op, r); err != nil { var diags tfdiags.Diagnostics - diags = diags.Append(generalError("Failed to retrieve run", err)) + diags = diags.Append(b.generalError("Failed to retrieve run", err)) op.ReportResult(runningOp, diags) return } } if r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { - runningOp.Result = backend.OperationFailure + runningOp.Result = backendrun.OperationFailure } } }() @@ -756,7 +993,7 @@ func (b *Cloud) Operation(ctx context.Context, op *backend.Operation) (*backend. return runningOp, nil } -func (b *Cloud) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { +func (b *Cloud) cancel(cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error { if r.Actions.IsCancelable { // Only ask if the remote operation should be canceled // if the auto approve flag is not set. @@ -767,7 +1004,7 @@ func (b *Cloud) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe. Description: "Only 'yes' will be accepted to cancel.", }) if err != nil { - return generalError("Failed asking to cancel", err) + return b.generalError("Failed asking to cancel", err) } if v != "yes" { if b.CLI != nil { @@ -785,7 +1022,7 @@ func (b *Cloud) cancel(cancelCtx context.Context, op *backend.Operation, r *tfe. // Try to cancel the remote operation. err := b.client.Runs.Cancel(cancelCtx, r.ID, tfe.RunCancelOptions{}) if err != nil { - return generalError("Failed to cancel run", err) + return b.generalError("Failed to cancel run", err) } if b.CLI != nil { b.CLI.Output(b.Colorize().Color(strings.TrimSpace(operationCanceled))) @@ -903,7 +1140,7 @@ func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Di message := fmt.Sprintf( "The local Terraform version (%s) does not meet the version requirements for remote workspace %s/%s (%s).", tfversion.String(), - b.organization, + b.Organization, workspace.Name, remoteConstraint, ) @@ -933,46 +1170,107 @@ func (b *Cloud) cliColorize() *colorstring.Colorize { } } -func (b *Cloud) workspaceTagsRequireUpdate(workspace *tfe.Workspace, workspaceMapping WorkspaceMapping) bool { - if workspaceMapping.Strategy() != WorkspaceTagsStrategy { - return false +type tagRequiresUpdateResult struct { + requiresUpdate bool + supportsKVTags bool +} + +func (b *Cloud) workspaceTagsRequireUpdate(ctx context.Context, workspace *tfe.Workspace, workspaceMapping WorkspaceMapping) (result tagRequiresUpdateResult, err error) { + result = tagRequiresUpdateResult{ + supportsKVTags: true, } - existingTags := map[string]struct{}{} - for _, t := range workspace.TagNames { - existingTags[t] = struct{}{} + // First, depending on the strategy, build a map of the tags defined in config + // so we can compare them to the actual tags on the workspace + normalizedTagMap := make(map[string]string) + if workspaceMapping.IsTagsStrategy() { + for _, b := range workspaceMapping.asTFETagBindings() { + normalizedTagMap[b.Key] = b.Value + } + } else { + // Not a tag strategy + return } - for _, tag := range workspaceMapping.Tags { - if _, ok := existingTags[tag]; !ok { - return true + // Fetch tag bindings and determine if they should be checked + bindings, err := b.client.Workspaces.ListTagBindings(ctx, workspace.ID) + if err != nil && errors.Is(err, tfe.ErrResourceNotFound) { + // By this time, the workspace should have been fetched, proving that the + // authenticated user has access to it. If the tag bindings are not found, + // it would mean that the backend does not support tag bindings. + result.supportsKVTags = false + } else if err != nil { + return + } + + err = nil +check: + // Check desired workspace tags against existing tags + for k, v := range normalizedTagMap { + log.Printf("[TRACE] cloud: Checking tag %q=%q", k, v) + if v == "" { + // Tag can exist in legacy tags or tag bindings + if !slices.Contains(workspace.TagNames, k) || (result.supportsKVTags && !slices.ContainsFunc(bindings, func(b *tfe.TagBinding) bool { + return b.Key == k + })) { + result.requiresUpdate = true + break check + } + } else if !result.supportsKVTags { + // There is a value defined, but the backend does not support tag bindings + result.requiresUpdate = true + err = ErrCloudDoesNotSupportKVTags + break check + } else { + // There is a value, so it must match a tag binding + if !slices.ContainsFunc(bindings, func(b *tfe.TagBinding) bool { + return b.Key == k && b.Value == v + }) { + result.requiresUpdate = true + break check + } } } - return false + doesOrDoesnot := "does " + if !result.requiresUpdate { + doesOrDoesnot = "does not " + } + log.Printf("[TRACE] cloud: Workspace %s %srequire tag update", workspace.Name, doesOrDoesnot) + + return } type WorkspaceMapping struct { - Name string - Tags []string + Name string + Project string + TagsAsSet []string + TagsAsMap map[string]string } type workspaceStrategy string const ( + WorkspaceKVTagsStrategy workspaceStrategy = "kvtags" WorkspaceTagsStrategy workspaceStrategy = "tags" WorkspaceNameStrategy workspaceStrategy = "name" WorkspaceNoneStrategy workspaceStrategy = "none" WorkspaceInvalidStrategy workspaceStrategy = "invalid" ) +func (wm WorkspaceMapping) IsTagsStrategy() bool { + return wm.Strategy() == WorkspaceTagsStrategy || wm.Strategy() == WorkspaceKVTagsStrategy +} + func (wm WorkspaceMapping) Strategy() workspaceStrategy { switch { - case len(wm.Tags) > 0 && wm.Name == "": + case len(wm.TagsAsMap) > 0 && wm.Name == "": + return WorkspaceKVTagsStrategy + case len(wm.TagsAsSet) > 0 && wm.Name == "": return WorkspaceTagsStrategy - case len(wm.Tags) == 0 && wm.Name != "": + case len(wm.TagsAsSet) == 0 && wm.Name != "": return WorkspaceNameStrategy - case len(wm.Tags) == 0 && wm.Name == "": + case len(wm.TagsAsSet) == 0 && wm.Name == "": return WorkspaceNoneStrategy default: // Any other combination is invalid as each strategy is mutually exclusive @@ -980,6 +1278,35 @@ func (wm WorkspaceMapping) Strategy() workspaceStrategy { } } +// DescribeTags returns a string representation of the tags in the workspace +// mapping, based on the strategy used. +func (wm WorkspaceMapping) DescribeTags() string { + result := "" + + switch wm.Strategy() { + case WorkspaceKVTagsStrategy: + for key, val := range wm.TagsAsMap { + if len(result) > 0 { + result += ", " + } + result += fmt.Sprintf("%s=%s", key, val) + } + case WorkspaceTagsStrategy: + result = strings.Join(wm.TagsAsSet, ", ") + } + + return result +} + +// cloudConfig is an intermediate type that represents the completed +// cloud block config as a plain Go value. +type cloudConfig struct { + hostname string + organization string + token string + workspaceMapping WorkspaceMapping +} + func isLocalExecutionMode(execMode string) bool { return execMode == "local" } @@ -994,14 +1321,15 @@ func (b *Cloud) fetchWorkspace(ctx context.Context, organization string, workspa case tfe.ErrResourceNotFound: return nil, fmt.Errorf( "workspace %s not found\n\n"+ - "For security, Terraform Cloud returns '404 Not Found' responses for resources\n"+ + fmt.Sprintf("For security, %s returns '404 Not Found' responses for resources\n", b.appName)+ "for resources that a user doesn't have access to, in addition to resources that\n"+ "do not exist. If the resource does exist, please check the permissions of the provided token.", workspace, ) default: err := fmt.Errorf( - "Terraform Cloud returned an unexpected error:\n\n%s", + "%s returned an unexpected error:\n\n%s", + b.appName, err, ) return nil, err @@ -1012,7 +1340,9 @@ func (b *Cloud) fetchWorkspace(ctx context.Context, organization string, workspa } // validWorkspaceEnvVar ensures we have selected a valid workspace using TF_WORKSPACE: -// First, it ensures the workspace specified by TF_WORKSPACE exists in the organization +// First, it ensures the workspace specified by TF_WORKSPACE exists in the organization. +// (This is because we deliberately DON'T implicitly create a workspace from TF_WORKSPACE, +// unlike with a workspace specified via `name`.) // Second, if tags are specified in the configuration, it ensures TF_WORKSPACE belongs to the set // of available workspaces with those given tags. func (b *Cloud) validWorkspaceEnvVar(ctx context.Context, organization, workspace string) tfdiags.Diagnostic { @@ -1021,7 +1351,7 @@ func (b *Cloud) validWorkspaceEnvVar(ctx context.Context, organization, workspac if err != nil && err != tfe.ErrResourceNotFound { return tfdiags.Sourceless( tfdiags.Error, - "Terraform Cloud returned an unexpected error", + fmt.Sprintf("%s returned an unexpected error", b.appName), err.Error(), ) } @@ -1034,47 +1364,61 @@ func (b *Cloud) validWorkspaceEnvVar(ctx context.Context, organization, workspac ) } - // if the configuration has specified tags, we need to ensure TF_WORKSPACE - // is a valid member - if b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy { - opts := &tfe.WorkspaceListOptions{} - opts.Tags = strings.Join(b.WorkspaceMapping.Tags, ",") - - for { - wl, err := b.client.Workspaces.List(ctx, b.organization, opts) - if err != nil { - return tfdiags.Sourceless( - tfdiags.Error, - "Terraform Cloud returned an unexpected error", - err.Error(), - ) - } - - for _, ws := range wl.Items { - if ws.Name == workspace { - return nil - } - } - - if wl.CurrentPage >= wl.TotalPages { - break - } - - opts.PageNumber = wl.NextPage - } - - return tfdiags.Sourceless( - tfdiags.Error, - "Invalid workspace selection", - fmt.Sprintf( - "Terraform failed to find workspace %q with the tags specified in your configuration:\n[%s]", - workspace, - strings.ReplaceAll(opts.Tags, ",", ", "), - ), - ) + // The remaining code is only concerned with tags configurations + if !b.WorkspaceMapping.IsTagsStrategy() { + return nil } - return nil + // if the configuration has specified tags, we need to ensure TF_WORKSPACE + // is a valid member + opts := &tfe.WorkspaceListOptions{} + if b.WorkspaceMapping.Strategy() == WorkspaceTagsStrategy { + opts.Tags = strings.Join(b.WorkspaceMapping.TagsAsSet, ",") + } else if b.WorkspaceMapping.Strategy() == WorkspaceKVTagsStrategy { + opts.TagBindings = make([]*tfe.TagBinding, len(b.WorkspaceMapping.TagsAsMap)) + + index := 0 + for key, val := range b.WorkspaceMapping.TagsAsMap { + opts.TagBindings[index] = &tfe.TagBinding{ + Key: key, + Value: val, + } + index += 1 + } + } + + for { + wl, err := b.client.Workspaces.List(ctx, b.Organization, opts) + if err != nil { + return tfdiags.Sourceless( + tfdiags.Error, + fmt.Sprintf("%s returned an unexpected error", b.appName), + err.Error(), + ) + } + + for _, ws := range wl.Items { + if ws.Name == workspace { + return nil + } + } + + if wl.CurrentPage >= wl.TotalPages { + break + } + + opts.PageNumber = wl.NextPage + } + + return tfdiags.Sourceless( + tfdiags.Error, + "Invalid workspace selection", + fmt.Sprintf( + "Terraform failed to find workspace %q with the tags specified in your configuration:\n[%s]", + workspace, + b.WorkspaceMapping.DescribeTags(), + ), + ) } func (wm WorkspaceMapping) tfeTags() []*tfe.Tag { @@ -1084,7 +1428,7 @@ func (wm WorkspaceMapping) tfeTags() []*tfe.Tag { return tags } - for _, tag := range wm.Tags { + for _, tag := range wm.TagsAsSet { t := tfe.Tag{Name: tag} tags = append(tags, &t) } @@ -1092,7 +1436,28 @@ func (wm WorkspaceMapping) tfeTags() []*tfe.Tag { return tags } -func generalError(msg string, err error) error { +func (wm WorkspaceMapping) asTFETagBindings() []*tfe.TagBinding { + var tagBindings []*tfe.TagBinding + + if wm.Strategy() == WorkspaceKVTagsStrategy { + tagBindings = make([]*tfe.TagBinding, len(wm.TagsAsMap)) + + index := 0 + for key, val := range wm.TagsAsMap { + tagBindings[index] = &tfe.TagBinding{Key: key, Value: val} + index += 1 + } + } else if wm.Strategy() == WorkspaceTagsStrategy { + tagBindings = make([]*tfe.TagBinding, len(wm.TagsAsSet)) + + for i, tag := range wm.TagsAsSet { + tagBindings[i] = &tfe.TagBinding{Key: tag} + } + } + return tagBindings +} + +func (b *Cloud) generalError(msg string, err error) error { var diags tfdiags.Diagnostics if urlErr, ok := err.(*url.Error); ok { @@ -1106,7 +1471,7 @@ func generalError(msg string, err error) error { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("%s: %v", msg, err), - "For security, Terraform Cloud returns '404 Not Found' responses for resources\n"+ + fmt.Sprintf("For security, %s returns '404 Not Found' responses for resources\n", b.appName)+ "for resources that a user doesn't have access to, in addition to resources that\n"+ "do not exist. If the resource does exist, please check the permissions of the provided token.", )) @@ -1115,7 +1480,7 @@ func generalError(msg string, err error) error { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, fmt.Sprintf("%s: %v", msg, err), - `Terraform Cloud returned an unexpected error. Sometimes `+ + fmt.Sprintf(`%s returned an unexpected error. Sometimes `, b.appName)+ `this is caused by network connection problems, in which case you could retry `+ `the command. If the issue persists please open a support ticket to get help `+ `resolving the problem.`, @@ -1124,17 +1489,6 @@ func generalError(msg string, err error) error { } } -// The newline in this error is to make it look good in the CLI! -const initialRetryError = ` -[reset][yellow]There was an error connecting to Terraform Cloud. Please do not exit -Terraform to prevent data loss! Trying to restore the connection... -[reset] -` - -const repeatedRetryError = ` -[reset][yellow]Still trying to restore the connection... (%s elapsed)[reset] -` - const operationCanceled = ` [reset][red]The remote operation was successfully cancelled.[reset] ` @@ -1146,12 +1500,12 @@ const operationNotCanceled = ` const refreshToApplyRefresh = `[bold][yellow]Proceeding with 'terraform apply -refresh-only -auto-approve'.[reset]` const unavailableTerraformVersion = ` -[reset][yellow]The local Terraform version (%s) is not available in Terraform Cloud, or your +[reset][yellow]The local Terraform version (%s) is not available in %s, or your organization does not have access to it. The new workspace will use %s. You can change this later in the workspace settings.[reset]` const cloudIntegrationUsedInUnsupportedTFE = ` -This version of Terraform Cloud/Enterprise does not support the state mechanism +This version of %s does not support the state mechanism attempting to be used by the platform. This should never happen. Please reach out to HashiCorp Support to resolve this issue.` @@ -1159,25 +1513,29 @@ Please reach out to HashiCorp Support to resolve this issue.` var ( workspaceConfigurationHelp = fmt.Sprintf( `The 'workspaces' block configures how Terraform CLI maps its workspaces for this single -configuration to workspaces within a Terraform Cloud organization. Two strategies are available: +configuration to workspaces within an HCP Terraform or Terraform Enterprise organization. Two strategies are available: [bold]tags[reset] - %s [bold]name[reset] - %s`, schemaDescriptionTags, schemaDescriptionName) schemaDescriptionHostname = `The Terraform Enterprise hostname to connect to. This optional argument defaults to app.terraform.io -for use with Terraform Cloud.` +for use with HCP Terraform.` schemaDescriptionOrganization = `The name of the organization containing the targeted workspace(s).` - schemaDescriptionToken = `The token used to authenticate with Terraform Cloud/Enterprise. Typically this argument should not + schemaDescriptionToken = `The token used to authenticate with HCP Terraform or Terraform Enterprise. Typically this argument should not be set, and 'terraform login' used instead; your credentials will then be fetched from your CLI configuration file or configured credential helper.` - schemaDescriptionTags = `A set of tags used to select remote Terraform Cloud workspaces to be used for this single + schemaDescriptionTags = `A set of tags used to select remote HCP Terraform or Terraform Enterprise workspaces to be used for this single configuration. New workspaces will automatically be tagged with these tag values. Generally, this is the primary and recommended strategy to use. This option conflicts with "name".` - schemaDescriptionName = `The name of a single Terraform Cloud workspace to be used with this configuration. -When configured, only the specified workspace can be used. This option conflicts with "tags".` + schemaDescriptionName = `The name of a single HCP Terraform or Terraform Enterprise workspace to be used with this configuration. +When configured, only the specified workspace can be used. This option conflicts with "tags" +and with the TF_WORKSPACE environment variable.` + + schemaDescriptionProject = `The name of an HCP Terraform or Terraform Enterpise project. Workspaces that need creating +will be created within this project.` ) diff --git a/internal/cloud/backend_apply.go b/internal/cloud/backend_apply.go index ff22dd5a90..27efb6951a 100644 --- a/internal/cloud/backend_apply.go +++ b/internal/cloud/backend_apply.go @@ -1,19 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "bufio" "context" + "encoding/json" + "fmt" "io" "log" + "strings" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/jsonformat" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) -func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backendrun.Operation, w *tfe.Workspace) (*tfe.Run, error) { log.Printf("[INFO] cloud: starting Apply operation") var diags tfdiags.Diagnostics @@ -44,17 +51,17 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Custom parallelism values are currently not supported", - `Terraform Cloud does not support setting a custom parallelism `+ - `value at this time.`, + fmt.Sprintf("%s does not support setting a custom parallelism ", b.appName)+ + "value at this time.", )) } - if op.PlanFile != nil { + if op.PlanFile.IsLocal() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Applying a saved plan is currently not supported", - `Terraform Cloud currently requires configuration to be present and `+ - `does not accept an existing saved plan as an argument at this time.`, + "Applying a saved local plan is not supported", + fmt.Sprintf("%s can apply a saved cloud plan, or create a new plan when ", b.appName)+ + `configuration is present. It cannot apply a saved local plan.`, )) } @@ -74,63 +81,126 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio return nil, diags.Err() } - // Run the plan phase. - r, err := b.plan(stopCtx, cancelCtx, op, w) + var r *tfe.Run + var err error + cp, hasSavedPlanFile := op.PlanFile.Cloud() + if hasSavedPlanFile { + log.Printf("[TRACE] Loading saved cloud plan for apply") + // Check hostname first, for a more actionable error than a generic 404 later + if cp.Hostname != b.Hostname { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Saved plan is for a different hostname", + fmt.Sprintf("The given saved plan refers to a run on %s, but the currently configured %s instance is %s.", cp.Hostname, b.appName, b.Hostname), + )) + return r, diags.Err() + } + // Fetch the run referenced in the saved plan bookmark. + r, err = b.client.Runs.ReadWithOptions(stopCtx, cp.RunID, &tfe.RunReadOptions{ + Include: []tfe.RunIncludeOpt{tfe.RunWorkspace}, + }) + + if err != nil { + return r, err + } + + if r.Workspace.ID != w.ID { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Saved plan is for a different workspace", + fmt.Sprintf("The given saved plan does not refer to a run in the current workspace (%s/%s), so it cannot currently be applied. For more details, view this run in a browser at:\n%s", w.Organization.Name, w.Name, runURL(b.Hostname, r.Workspace.Organization.Name, r.Workspace.Name, r.ID)), + )) + return r, diags.Err() + } + + if !r.Actions.IsConfirmable { + url := runURL(b.Hostname, b.Organization, op.Workspace, r.ID) + return r, unusableSavedPlanError(r.Status, url, b.appName) + } + + // Since we're not calling plan(), we need to print a run header ourselves: + if b.CLI != nil { + b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf(applySavedHeader, b.appName) + "\n"))) + b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf( + runHeader, b.Hostname, b.Organization, r.Workspace.Name, r.ID)) + "\n")) + } + } else { + log.Printf("[TRACE] Running new cloud plan for apply") + // Run the plan phase. + r, err = b.plan(stopCtx, cancelCtx, op, w) + + if err != nil { + return r, err + } + + // This check is also performed in the plan method to determine if + // the policies should be checked, but we need to check the values + // here again to determine if we are done and should return. + if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { + return r, nil + } + + // Retrieve the run to get its current status. + r, err = b.client.Runs.Read(stopCtx, r.ID) + if err != nil { + return r, b.generalError("Failed to retrieve run", err) + } + + // Return if the run cannot be confirmed. + if !op.AutoApprove && !r.Actions.IsConfirmable { + return r, nil + } + + mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove + + if mustConfirm && b.input { + opts := &terraform.InputOpts{Id: "approve"} + + if op.PlanMode == plans.DestroyMode { + opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" + opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + + "There is no undo. Only 'yes' will be accepted to confirm." + } else { + opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" + opts.Description = "Terraform will perform the actions described above.\n" + + "Only 'yes' will be accepted to approve." + } + + err = b.confirm(stopCtx, op, opts, r, "yes") + if err != nil && err != errRunApproved { + return r, err + } + } else if mustConfirm && !b.input { + return r, errApplyNeedsUIConfirmation + } else { + // If we don't need to ask for confirmation, insert a blank + // line to separate the ouputs. + if b.CLI != nil { + b.CLI.Output("") + } + } + } + + // Do the apply! + // If we have a saved plan file, we proceed to apply the run without confirmation + // regardless of the value of AutoApprove. + if (!op.AutoApprove || hasSavedPlanFile) && err != errRunApproved { + if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { + return r, b.generalError("Failed to approve the apply command", err) + } + } + + // Retrieve the run to get task stages. + // Task Stages are calculated upfront so we only need to call this once for the run. + taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID) if err != nil { return r, err } - // This check is also performed in the plan method to determine if - // the policies should be checked, but we need to check the values - // here again to determine if we are done and should return. - if !r.HasChanges || r.Status == tfe.RunCanceled || r.Status == tfe.RunErrored { - return r, nil - } - - // Retrieve the run to get its current status. - r, err = b.client.Runs.Read(stopCtx, r.ID) - if err != nil { - return r, generalError("Failed to retrieve run", err) - } - - // Return if the run cannot be confirmed. - if !op.AutoApprove && !r.Actions.IsConfirmable { - return r, nil - } - - mustConfirm := (op.UIIn != nil && op.UIOut != nil) && !op.AutoApprove - - if mustConfirm && b.input { - opts := &terraform.InputOpts{Id: "approve"} - - if op.PlanMode == plans.DestroyMode { - opts.Query = "\nDo you really want to destroy all resources in workspace \"" + op.Workspace + "\"?" - opts.Description = "Terraform will destroy all your managed infrastructure, as shown above.\n" + - "There is no undo. Only 'yes' will be accepted to confirm." - } else { - opts.Query = "\nDo you want to perform these actions in workspace \"" + op.Workspace + "\"?" - opts.Description = "Terraform will perform the actions described above.\n" + - "Only 'yes' will be accepted to approve." - } - - err = b.confirm(stopCtx, op, opts, r, "yes") - if err != nil && err != errRunApproved { + if stage, ok := taskStages[tfe.PreApply]; ok { + if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Pre-apply Tasks"); err != nil { return r, err } - } else if mustConfirm && !b.input { - return r, errApplyNeedsUIConfirmation - } else { - // If we don't need to ask for confirmation, insert a blank - // line to separate the ouputs. - if b.CLI != nil { - b.CLI.Output("") - } - } - - if !op.AutoApprove && err != errRunApproved { - if err = b.client.Runs.Apply(stopCtx, r.ID, tfe.RunApplyOptions{}); err != nil { - return r, generalError("Failed to approve the apply command", err) - } } r, err = b.waitForRun(stopCtx, cancelCtx, op, "apply", r, w) @@ -138,47 +208,129 @@ func (b *Cloud) opApply(stopCtx, cancelCtx context.Context, op *backend.Operatio return r, err } - logs, err := b.client.Applies.Logs(stopCtx, r.Apply.ID) + err = b.renderApplyLogs(stopCtx, r) if err != nil { - return r, generalError("Failed to retrieve logs", err) + return r, err + } + + return r, nil +} + +func (b *Cloud) renderApplyLogs(ctx context.Context, run *tfe.Run) error { + logs, err := b.client.Applies.Logs(ctx, run.Apply.ID) + if err != nil { + return err } - reader := bufio.NewReaderSize(logs, 64*1024) if b.CLI != nil { + reader := bufio.NewReaderSize(logs, 64*1024) skip := 0 + for next := true; next; { var l, line []byte + var err error for isPrefix := true; isPrefix; { l, isPrefix, err = reader.ReadLine() if err != nil { if err != io.EOF { - return r, generalError("Failed to read logs", err) + return b.generalError("Failed to read logs", err) } next = false } + line = append(line, l...) } - // Skip the first 3 lines to prevent duplicate output. + // Apply logs show the same Terraform info logs as shown in the plan logs + // (which contain version and os/arch information), we therefore skip to prevent duplicate output. if skip < 3 { skip++ continue } if next || len(line) > 0 { - b.CLI.Output(b.Colorize().Color(string(line))) + log := &jsonformat.JSONLog{} + if err := json.Unmarshal(line, log); err != nil { + // If we can not parse the line as JSON, we will simply + // print the line. This maintains backwards compatibility for + // users who do not wish to enable structured output in their + // workspace. + b.CLI.Output(string(line)) + continue + } + + if b.renderer != nil { + // Otherwise, we will print the log + err := b.renderer.RenderLog(log) + if err != nil { + return err + } + } } } } - return r, nil + return nil +} + +func runURL(hostname, orgName, wsName, runID string) string { + return fmt.Sprintf("https://%s/app/%s/%s/runs/%s", hostname, orgName, wsName, runID) +} + +func unusableSavedPlanError(status tfe.RunStatus, url, appName string) error { + var diags tfdiags.Diagnostics + var summary, reason string + + switch status { + case tfe.RunApplied: + summary = "Saved plan is already applied" + reason = "The given plan file was already successfully applied, and cannot be applied again." + case tfe.RunApplying, tfe.RunApplyQueued, tfe.RunConfirmed: + summary = "Saved plan is already confirmed" + reason = "The given plan file is already being applied, and cannot be applied again." + case tfe.RunCanceled: + summary = "Saved plan is canceled" + reason = fmt.Sprintf("The given plan file can no longer be applied because the run was canceled via the %s UI or API.", appName) + case tfe.RunDiscarded: + summary = "Saved plan is discarded" + reason = fmt.Sprintf("The given plan file can no longer be applied; either another run was applied first, or a user discarded it via the %s UI or API.", appName) + case tfe.RunErrored: + summary = "Saved plan is errored" + reason = "The given plan file refers to a plan that had errors and did not complete successfully. It cannot be applied." + case tfe.RunPlannedAndFinished: + // Note: planned and finished can also indicate a plan-only run, but + // terraform plan can't create a saved plan for a plan-only run, so we + // know it's no-changes in this case. + summary = "Saved plan has no changes" + reason = "The given plan file contains no changes, so it cannot be applied." + case tfe.RunPolicyOverride: + summary = "Saved plan requires policy override" + reason = "The given plan file has soft policy failures, and cannot be applied until a user with appropriate permissions overrides the policy check." + default: + summary = "Saved plan cannot be applied" + reason = fmt.Sprintf("%s cannot apply the given plan file. This may mean the plan and checks have not yet completed, or may indicate another problem.", appName) + } + + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + summary, + fmt.Sprintf("%s For more details, view this run in a browser at:\n%s", reason, url), + )) + return diags.Err() } const applyDefaultHeader = ` -[reset][yellow]Running apply in Terraform Cloud. Output will stream here. Pressing Ctrl-C +[reset][yellow]Running apply in %s. Output will stream here. Pressing Ctrl-C will cancel the remote apply if it's still pending. If the apply started it will stop streaming the logs, but will not stop the apply running remotely.[reset] Preparing the remote apply... ` + +const applySavedHeader = ` +[reset][yellow]Running apply in %s. Output will stream here. Pressing Ctrl-C +will stop streaming the logs, but will not stop the apply running remotely.[reset] + +Preparing the remote apply... +` diff --git a/internal/cloud/backend_apply_test.go b/internal/cloud/backend_apply_test.go index 80f5f20450..969b8df31a 100644 --- a/internal/cloud/backend_apply_test.go +++ b/internal/cloud/backend_apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -10,15 +13,19 @@ import ( "testing" "time" - gomock "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" mocks "github.com/hashicorp/go-tfe/mocks" version "github.com/hashicorp/go-version" + gomock "go.uber.org/mock/gomock" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/cloud/cloudplan" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" + "github.com/hashicorp/terraform/internal/command/jsonformat" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/initwd" @@ -28,19 +35,18 @@ import ( "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" tfversion "github.com/hashicorp/terraform/version" - "github.com/mitchellh/cli" ) -func testOperationApply(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationApply(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() return testOperationApplyWithTimeout(t, configDir, 0) } -func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) @@ -52,12 +58,12 @@ func testOperationApplyWithTimeout(t *testing.T, configDir string, timeout time. depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) - return &backend.Operation{ + return &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanRefresh: true, StateLocker: clistate.NewLocker(timeout, stateLockerView), - Type: backend.OperationTypeApply, + Type: backendrun.OperationTypeApply, View: operationView, DependencyLocks: depLocks, }, configCleanup, done @@ -85,7 +91,7 @@ func TestCloud_applyBasic(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -97,8 +103,8 @@ func TestCloud_applyBasic(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -114,6 +120,153 @@ func TestCloud_applyBasic(t *testing.T) { } } +func TestCloud_applyJSONBasic(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json") + defer configCleanup() + defer done(t) + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op.UIIn = input + op.UIOut = b.CLI + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatalf("expected a non-empty plan") + } + + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summary in output: %s", gotOut) + } + if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("expected apply summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} + +func TestCloud_applyJSONWithOutputs(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json-with-outputs") + defer configCleanup() + defer done(t) + + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op.UIIn = input + op.UIOut = b.CLI + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatalf("expected a non-empty plan") + } + + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } + + outp := close(t) + gotOut := outp.Stdout() + expectedSimpleOutput := `simple = [ + "some", + "list", + ]` + expectedSensitiveOutput := `secret = (sensitive value)` + expectedComplexOutput := `complex = { + keyA = { + someList = [ + 1, + 2, + 3, + ] + } + keyB = { + someBool = true + someStr = "hello" + } + }` + + if !strings.Contains(gotOut, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summary in output: %s", gotOut) + } + if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("expected apply summary in output: %s", gotOut) + } + if !strings.Contains(gotOut, "Outputs:") { + t.Fatalf("expected output header: %s", gotOut) + } + if !strings.Contains(gotOut, expectedSimpleOutput) { + t.Fatalf("expected output: %s, got: %s", expectedSimpleOutput, gotOut) + } + if !strings.Contains(gotOut, expectedSensitiveOutput) { + t.Fatalf("expected output: %s, got: %s", expectedSensitiveOutput, gotOut) + } + if !strings.Contains(gotOut, expectedComplexOutput) { + t.Fatalf("expected output: %s, got: %s", expectedComplexOutput, gotOut) + } + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} + func TestCloud_applyCanceled(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -133,7 +286,7 @@ func TestCloud_applyCanceled(t *testing.T) { run.Stop() <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -150,7 +303,7 @@ func TestCloud_applyWithoutPermissions(t *testing.T) { // Create a named workspace without permissions. w, err := b.client.Workspaces.Create( context.Background(), - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("prod"), }, @@ -173,7 +326,7 @@ func TestCloud_applyWithoutPermissions(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -190,7 +343,7 @@ func TestCloud_applyWithVCS(t *testing.T) { // Create a named workspace with a VCS. _, err := b.client.Workspaces.Create( context.Background(), - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("prod"), VCSRepo: &tfe.VCSRepoOptions{}, @@ -212,7 +365,7 @@ func TestCloud_applyWithVCS(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -245,7 +398,7 @@ func TestCloud_applyWithParallelism(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } @@ -255,14 +408,15 @@ func TestCloud_applyWithParallelism(t *testing.T) { } } -func TestCloud_applyWithPlan(t *testing.T) { +// Apply with local plan file should fail. +func TestCloud_applyWithLocalPlan(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() op, configCleanup, done := testOperationApply(t, "./testdata/apply") defer configCleanup() - op.PlanFile = &planfile.Reader{} + op.PlanFile = planfile.NewWrappedLocal(&planfile.Reader{}) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) @@ -272,7 +426,7 @@ func TestCloud_applyWithPlan(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -280,11 +434,147 @@ func TestCloud_applyWithPlan(t *testing.T) { } errOutput := output.Stderr() - if !strings.Contains(errOutput, "saved plan is currently not supported") { + if !strings.Contains(errOutput, "saved local plan is not supported") { t.Fatalf("expected a saved plan error, got: %v", errOutput) } } +// Apply with bookmark to an existing cloud plan that's in a confirmable state +// should work. +func TestCloud_applyWithCloudPlan(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json") + defer configCleanup() + defer done(t) + + op.UIOut = b.CLI + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + // Perform the plan before trying to apply it + ws, err := b.client.Workspaces.Read(context.Background(), b.Organization, b.WorkspaceMapping.Name) + if err != nil { + t.Fatalf("Couldn't read workspace: %s", err) + } + + planRun, err := b.plan(context.Background(), context.Background(), op, ws) + if err != nil { + t.Fatalf("Couldn't perform plan: %s", err) + } + + // Synthesize a cloud plan file with the plan's run ID + pf := &cloudplan.SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: planRun.ID, + Hostname: b.Hostname, + } + op.PlanFile = planfile.NewWrappedCloud(pf) + + // Start spying on the apply output (now that the plan's done) + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + // Try apply + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + output := close(t) + if run.Result != backendrun.OperationSuccess { + t.Fatal("expected apply operation to succeed") + } + if run.PlanEmpty { + t.Fatalf("expected plan to not be empty") + } + + gotOut := output.Stdout() + if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("expected apply summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} + +func TestCloud_applyAutoApprove_with_CloudPlan(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json") + defer configCleanup() + defer done(t) + + op.AutoApprove = true + op.UIOut = b.CLI + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + ws, err := b.client.Workspaces.Read(context.Background(), b.Organization, b.WorkspaceMapping.Name) + if err != nil { + t.Fatalf("Couldn't read workspace: %s", err) + } + + planRun, err := b.plan(context.Background(), context.Background(), op, ws) + if err != nil { + t.Fatalf("Couldn't perform plan: %s", err) + } + + // Synthesize a cloud plan file with the plan's run ID + pf := &cloudplan.SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: planRun.ID, + Hostname: b.Hostname, + } + op.PlanFile = planfile.NewWrappedCloud(pf) + + // Start spying on the apply output (now that the plan's done) + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + // Try apply + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + output := close(t) + if run.Result != backendrun.OperationSuccess { + t.Fatal("expected apply operation to succeed") + } + if run.PlanEmpty { + t.Fatalf("expected plan to not be empty") + } + + gotOut := output.Stdout() + if !strings.Contains(gotOut, "1 added, 0 changed, 0 destroyed") { + t.Fatalf("expected apply summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} + func TestCloud_applyWithoutRefresh(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -302,7 +592,7 @@ func TestCloud_applyWithoutRefresh(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -339,7 +629,7 @@ func TestCloud_applyWithRefreshOnly(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -378,7 +668,7 @@ func TestCloud_applyWithTarget(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected apply operation to succeed") } if run.PlanEmpty { @@ -417,7 +707,7 @@ func TestCloud_applyWithReplace(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { @@ -456,13 +746,13 @@ func TestCloud_applyWithRequiredVariables(t *testing.T) { <-run.Done() // The usual error of a required variable being missing is deferred and the operation // is successful - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } } @@ -482,7 +772,7 @@ func TestCloud_applyNoConfig(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -517,7 +807,7 @@ func TestCloud_applyNoChanges(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if !run.PlanEmpty { @@ -555,7 +845,7 @@ func TestCloud_applyNoApprove(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -603,7 +893,7 @@ func TestCloud_applyAutoApprove(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -615,8 +905,8 @@ func TestCloud_applyAutoApprove(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -654,7 +944,7 @@ func TestCloud_applyApprovedExternally(t *testing.T) { wl, err := b.client.Workspaces.List( ctx, - b.organization, + b.Organization, nil, ) if err != nil { @@ -678,7 +968,7 @@ func TestCloud_applyApprovedExternally(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -686,8 +976,8 @@ func TestCloud_applyApprovedExternally(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -728,7 +1018,7 @@ func TestCloud_applyDiscardedExternally(t *testing.T) { wl, err := b.client.Workspaces.List( ctx, - b.organization, + b.Organization, nil, ) if err != nil { @@ -752,7 +1042,7 @@ func TestCloud_applyDiscardedExternally(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -760,8 +1050,8 @@ func TestCloud_applyDiscardedExternally(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -789,7 +1079,7 @@ func TestCloud_applyWithAutoApprove(t *testing.T) { // Create a named workspace that auto applies. _, err := b.client.Workspaces.Create( context.Background(), - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("prod"), }, @@ -817,7 +1107,7 @@ func TestCloud_applyWithAutoApprove(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -829,8 +1119,8 @@ func TestCloud_applyWithAutoApprove(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -873,7 +1163,7 @@ func TestCloud_applyForceLocal(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -885,8 +1175,8 @@ func TestCloud_applyForceLocal(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -905,7 +1195,7 @@ func TestCloud_applyWorkspaceWithoutOperations(t *testing.T) { // Create a named workspace that doesn't allow operations. _, err := b.client.Workspaces.Create( ctx, - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("no-operations"), }, @@ -936,7 +1226,7 @@ func TestCloud_applyWorkspaceWithoutOperations(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -948,8 +1238,8 @@ func TestCloud_applyWorkspaceWithoutOperations(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -966,7 +1256,7 @@ func TestCloud_applyLockTimeout(t *testing.T) { ctx := context.Background() // Retrieve the workspace used to run this operation in. - w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name) + w, err := b.client.Workspaces.Read(ctx, b.Organization, b.WorkspaceMapping.Name) if err != nil { t.Fatalf("error retrieving workspace: %v", err) } @@ -1019,8 +1309,8 @@ func TestCloud_applyLockTimeout(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "Lock timeout exceeded") { t.Fatalf("expected lock timout error in output: %s", output) @@ -1056,7 +1346,7 @@ func TestCloud_applyDestroy(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1068,8 +1358,8 @@ func TestCloud_applyDestroy(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "0 to add, 0 to change, 1 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -1102,7 +1392,7 @@ func TestCloud_applyDestroyNoConfig(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1114,6 +1404,103 @@ func TestCloud_applyDestroyNoConfig(t *testing.T) { } } +func TestCloud_applyJSONWithProvisioner(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + input := testInput(t, map[string]string{ + "approve": "yes", + }) + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json-with-provisioner") + defer configCleanup() + defer done(t) + + op.UIIn = input + op.UIOut = b.CLI + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + + if run.PlanEmpty { + t.Fatalf("expected a non-empty plan") + } + + if len(input.answers) > 0 { + t.Fatalf("expected no unused answers, got: %v", input.answers) + } + + outp := close(t) + gotOut := outp.Stdout() + if !strings.Contains(gotOut, "null_resource.foo: Provisioning with 'local-exec'") { + t.Fatalf("expected provisioner local-exec start in logs: %s", gotOut) + } + + if !strings.Contains(gotOut, "null_resource.foo: (local-exec):") { + t.Fatalf("expected provisioner local-exec progress in logs: %s", gotOut) + } + + if !strings.Contains(gotOut, "Hello World!") { + t.Fatalf("expected provisioner local-exec output in logs: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after apply + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after apply: %s", err.Error()) + } +} + +func TestCloud_applyJSONWithProvisionerError(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json-with-provisioner-error") + defer configCleanup() + defer done(t) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "local-exec provisioner error") { + t.Fatalf("unexpected error in apply logs: %s", gotOut) + } +} + func TestCloud_applyPolicyPass(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -1136,7 +1523,7 @@ func TestCloud_applyPolicyPass(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1148,8 +1535,8 @@ func TestCloud_applyPolicyPass(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -1184,7 +1571,7 @@ func TestCloud_applyPolicyHardFail(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if !run.PlanEmpty { @@ -1201,8 +1588,8 @@ func TestCloud_applyPolicyHardFail(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -1239,7 +1626,7 @@ func TestCloud_applyPolicySoftFail(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1251,8 +1638,8 @@ func TestCloud_applyPolicySoftFail(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -1314,7 +1701,7 @@ func TestCloud_applyPolicySoftFailAutoApproveSuccess(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected apply operation to success due to auto-approve") } @@ -1359,7 +1746,7 @@ func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) { // Create a named workspace that auto applies. _, err := b.client.Workspaces.Create( context.Background(), - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("prod"), }, @@ -1388,7 +1775,7 @@ func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -1400,8 +1787,8 @@ func TestCloud_applyPolicySoftFailAutoApprove(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running apply in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running apply in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summery in output: %s", output) @@ -1430,7 +1817,7 @@ func TestCloud_applyWithRemoteError(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected apply operation to fail") } if run.Result.ExitStatus() != 1 { @@ -1443,6 +1830,46 @@ func TestCloud_applyWithRemoteError(t *testing.T) { } } +func TestCloud_applyJSONWithRemoteError(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationApply(t, "./testdata/apply-json-with-error") + defer configCleanup() + defer done(t) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result == backendrun.OperationSuccess { + t.Fatal("expected apply operation to fail") + } + if run.Result.ExitStatus() != 1 { + t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) + } + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "Unsupported block type") { + t.Fatalf("unexpected plan error in output: %s", gotOut) + } +} + func TestCloud_applyVersionCheck(t *testing.T) { testCases := map[string]struct { localVersion string @@ -1510,7 +1937,7 @@ func TestCloud_applyVersionCheck(t *testing.T) { // remote workspace _, err := b.client.Workspaces.Update( ctx, - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ ExecutionMode: tfe.String(tc.executionMode), @@ -1550,7 +1977,7 @@ func TestCloud_applyVersionCheck(t *testing.T) { if tc.wantErr != "" { // ASSERT: if the test case wants an error, check for failure // and the error message - if run.Result != backend.OperationFailure { + if run.Result != backendrun.OperationFailure { t.Fatalf("expected run to fail, but result was %#v", run.Result) } errOutput := output.Stderr() @@ -1560,23 +1987,23 @@ func TestCloud_applyVersionCheck(t *testing.T) { } else { // ASSERT: otherwise, check for success and appropriate output // based on whether the run should be local or remote - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } output := b.CLI.(*cli.MockUi).OutputWriter.String() - hasRemote := strings.Contains(output, "Running apply in Terraform Cloud") + hasRemote := strings.Contains(output, "Running apply in HCP Terraform") hasSummary := strings.Contains(output, "1 added, 0 changed, 0 destroyed") hasResources := run.State.HasManagedResourceInstanceObjects() if !tc.forceLocal && !isLocalExecutionMode(tc.executionMode) { if !hasRemote { - t.Errorf("missing TFC header in output: %s", output) + t.Errorf("missing HCP Terraform header in output: %s", output) } if !hasSummary { t.Errorf("expected apply summary in output: %s", output) } } else { if hasRemote { - t.Errorf("unexpected TFC header in output: %s", output) + t.Errorf("unexpected HCP Terraform header in output: %s", output) } if !hasResources { t.Errorf("expected resources in state") diff --git a/internal/cloud/backend_cli.go b/internal/cloud/backend_cli.go index 62eb834cac..c63746ec03 100644 --- a/internal/cloud/backend_cli.go +++ b/internal/cloud/backend_cli.go @@ -1,12 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/jsonformat" ) -// CLIInit implements backend.CLI -func (b *Cloud) CLIInit(opts *backend.CLIOpts) error { - if cli, ok := b.local.(backend.CLI); ok { +// CLIInit implements backendrun.CLI +func (b *Cloud) CLIInit(opts *backendrun.CLIOpts) error { + if cli, ok := b.local.(backendrun.CLI); ok { if err := cli.CLIInit(opts); err != nil { return err } @@ -17,6 +21,10 @@ func (b *Cloud) CLIInit(opts *backend.CLIOpts) error { b.ContextOpts = opts.ContextOpts b.runningInAutomation = opts.RunningInAutomation b.input = opts.Input + b.renderer = &jsonformat.Renderer{ + Streams: opts.Streams, + Colorize: opts.CLIColor, + } return nil } diff --git a/internal/cloud/backend_colorize.go b/internal/cloud/backend_colorize.go index 6fb3c98c3b..c267fb95ec 100644 --- a/internal/cloud/backend_colorize.go +++ b/internal/cloud/backend_colorize.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( diff --git a/internal/cloud/backend_common.go b/internal/cloud/backend_common.go index e03ff66665..44d96a912b 100644 --- a/internal/cloud/backend_common.go +++ b/internal/cloud/backend_common.go @@ -1,17 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "bufio" + "bytes" "context" + "encoding/json" + "errors" "fmt" "io" "math" + "net/http" + "net/url" "strconv" "strings" "time" + "github.com/hashicorp/go-retryablehttp" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/jsonapi" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/terraform" ) @@ -33,7 +45,7 @@ func backoff(min, max float64, iter int) time.Duration { return time.Duration(backoff) * time.Millisecond } -func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backendrun.Operation, opType string, r *tfe.Run, w *tfe.Workspace) (*tfe.Run, error) { started := time.Now() updated := started for i := 0; ; i++ { @@ -49,7 +61,7 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera // Retrieve the run to get its current status. r, err := b.client.Runs.Read(stopCtx, r.ID) if err != nil { - return r, generalError("Failed to retrieve run", err) + return r, b.generalError("Failed to retrieve run", err) } // Return if the run is no longer pending. @@ -78,9 +90,9 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera } // Retrieve the workspace used to run this operation in. - w, err = b.client.Workspaces.Read(stopCtx, b.organization, w.Name) + w, err = b.client.Workspaces.Read(stopCtx, b.Organization, w.Name) if err != nil { - return nil, generalError("Failed to retrieve workspace", err) + return nil, b.generalError("Failed to retrieve workspace", err) } // If the workspace is locked the run will not be queued and we can @@ -88,7 +100,7 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera if w.Locked && w.CurrentRun != nil { cr, err := b.client.Runs.Read(stopCtx, w.CurrentRun.ID) if err != nil { - return r, generalError("Failed to retrieve current run", err) + return r, b.generalError("Failed to retrieve current run", err) } if cr.Status == tfe.RunPending { b.CLI.Output(b.Colorize().Color( @@ -105,7 +117,7 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera for { rl, err := b.client.Runs.List(stopCtx, w.ID, options) if err != nil { - return r, generalError("Failed to retrieve run list", err) + return r, b.generalError("Failed to retrieve run list", err) } // Loop through all runs to calculate the workspace queue position. @@ -122,7 +134,7 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera case tfe.RunApplied, tfe.RunCanceled, tfe.RunDiscarded, tfe.RunErrored: continue case tfe.RunPlanned: - if op.Type == backend.OperationTypePlan { + if op.Type == backendrun.OperationTypePlan { continue } } @@ -158,9 +170,9 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera options := tfe.ReadRunQueueOptions{} search: for { - rq, err := b.client.Organizations.ReadRunQueue(stopCtx, b.organization, options) + rq, err := b.client.Organizations.ReadRunQueue(stopCtx, b.Organization, options) if err != nil { - return r, generalError("Failed to retrieve queue", err) + return r, b.generalError("Failed to retrieve queue", err) } // Search through all queued items to find our run. @@ -181,9 +193,9 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera } if position > 0 { - c, err := b.client.Organizations.ReadCapacity(stopCtx, b.organization) + c, err := b.client.Organizations.ReadCapacity(stopCtx, b.Organization) if err != nil { - return r, generalError("Failed to retrieve capacity", err) + return r, b.generalError("Failed to retrieve capacity", err) } b.CLI.Output(b.Colorize().Color(fmt.Sprintf( "Waiting for %d queued run(s) to finish before starting...%s", @@ -199,7 +211,7 @@ func (b *Cloud) waitForRun(stopCtx, cancelCtx context.Context, op *backend.Opera } } -func (b *Cloud) waitTaskStage(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run, stageID string, outputTitle string) error { +func (b *Cloud) waitTaskStage(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run, stageID string, outputTitle string) error { integration := &IntegrationContext{ B: b, StopContext: stopCtx, @@ -207,10 +219,10 @@ func (b *Cloud) waitTaskStage(stopCtx, cancelCtx context.Context, op *backend.Op Op: op, Run: r, } - return b.runTasks(integration, integration.BeginOutput(outputTitle), stageID) + return b.runTaskStage(integration, integration.BeginOutput(outputTitle), stageID) } -func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { +func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error { if r.CostEstimate == nil { return nil } @@ -230,7 +242,7 @@ func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Ope // Retrieve the cost estimate to get its current status. ce, err := b.client.CostEstimates.Read(stopCtx, r.CostEstimate.ID) if err != nil { - return generalError("Failed to retrieve cost estimate", err) + return b.generalError("Failed to retrieve cost estimate", err) } // If the run is canceled or errored, but the cost-estimate still has @@ -251,7 +263,7 @@ func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Ope case tfe.CostEstimateFinished: delta, err := strconv.ParseFloat(ce.DeltaMonthlyCost, 64) if err != nil { - return generalError("Unexpected error", err) + return b.generalError("Unexpected error", err) } sign := "+" @@ -266,7 +278,7 @@ func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Ope b.CLI.Output(b.Colorize().Color(fmt.Sprintf("Resources: %d of %d estimated", ce.MatchedResourcesCount, ce.ResourcesCount))) b.CLI.Output(b.Colorize().Color(fmt.Sprintf(" $%s/mo %s$%s", ce.ProposedMonthlyCost, sign, deltaRepr))) - if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backend.OperationTypeApply { + if len(r.PolicyChecks) == 0 && r.HasChanges && op.Type == backendrun.OperationTypeApply { b.CLI.Output("\n------------------------------------------------------------------------") } } @@ -298,14 +310,14 @@ func (b *Cloud) costEstimate(stopCtx, cancelCtx context.Context, op *backend.Ope b.CLI.Output("\n------------------------------------------------------------------------") return nil case tfe.CostEstimateCanceled: - return fmt.Errorf(msgPrefix + " canceled.") + return fmt.Errorf("%s canceled.", msgPrefix) default: return fmt.Errorf("Unknown or unexpected cost estimate state: %s", ce.Status) } } } -func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Operation, r *tfe.Run) error { +func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backendrun.Operation, r *tfe.Run) error { if b.CLI != nil { b.CLI.Output("\n------------------------------------------------------------------------\n") } @@ -314,14 +326,14 @@ func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Oper // return once the policy check is complete. logs, err := b.client.PolicyChecks.Logs(stopCtx, pc.ID) if err != nil { - return generalError("Failed to retrieve policy check logs", err) + return b.generalError("Failed to retrieve policy check logs", err) } reader := bufio.NewReaderSize(logs, 64*1024) // Retrieve the policy check to get its current status. pc, err := b.client.PolicyChecks.Read(stopCtx, pc.ID) if err != nil { - return generalError("Failed to retrieve policy check", err) + return b.generalError("Failed to retrieve policy check", err) } // If the run is canceled or errored, but the policy check still has @@ -355,7 +367,7 @@ func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Oper l, isPrefix, err = reader.ReadLine() if err != nil { if err != io.EOF { - return generalError("Failed to read logs", err) + return b.generalError("Failed to read logs", err) } next = false } @@ -370,25 +382,25 @@ func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Oper switch pc.Status { case tfe.PolicyPasses: - if (r.HasChanges && op.Type == backend.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { + if (r.HasChanges && op.Type == backendrun.OperationTypeApply || i < len(r.PolicyChecks)-1) && b.CLI != nil { b.CLI.Output("\n------------------------------------------------------------------------") } continue case tfe.PolicyErrored: - return fmt.Errorf(msgPrefix + " errored.") + return fmt.Errorf("%s errored.", msgPrefix) case tfe.PolicyHardFailed: - return fmt.Errorf(msgPrefix + " hard failed.") + return fmt.Errorf("%s hard failed.", msgPrefix) case tfe.PolicySoftFailed: - runUrl := fmt.Sprintf(runHeader, b.hostname, b.organization, op.Workspace, r.ID) + runURL := fmt.Sprintf(runHeaderErr, b.Hostname, b.Organization, op.Workspace, r.ID) - if op.Type == backend.OperationTypePlan || op.UIOut == nil || op.UIIn == nil || + if op.Type == backendrun.OperationTypePlan || op.UIOut == nil || op.UIIn == nil || !pc.Actions.IsOverridable || !pc.Permissions.CanOverride { - return fmt.Errorf(msgPrefix + " soft failed.\n" + runUrl) + return fmt.Errorf("%s soft failed.\n%s", msgPrefix, runURL) } if op.AutoApprove { if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { - return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) + return b.generalError(fmt.Sprintf("Failed to override policy check.\n%s", runURL), err) } } else if !b.input { return errPolicyOverrideNeedsUIConfirmation @@ -400,17 +412,16 @@ func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Oper } err = b.confirm(stopCtx, op, opts, r, "override") if err != nil && err != errRunOverridden { - return fmt.Errorf( - fmt.Sprintf("Failed to override: %s\n%s\n", err.Error(), runUrl), - ) + return fmt.Errorf("Failed to override: %w\n%s\n", err, runURL) } if err != errRunOverridden { if _, err = b.client.PolicyChecks.Override(stopCtx, pc.ID); err != nil { - return generalError(fmt.Sprintf("Failed to override policy check.\n%s", runUrl), err) + return b.generalError(fmt.Sprintf("Failed to override policy check.\n%s", runURL), err) } } else { - b.CLI.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runUrl)) + runURL := fmt.Sprintf(runHeader, b.Hostname, b.Organization, op.Workspace, r.ID) + b.CLI.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runURL)) } } @@ -425,7 +436,7 @@ func (b *Cloud) checkPolicy(stopCtx, cancelCtx context.Context, op *backend.Oper return nil } -func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { +func (b *Cloud) confirm(stopCtx context.Context, op *backendrun.Operation, opts *terraform.InputOpts, r *tfe.Run, keyword string) error { doneCtx, cancel := context.WithCancel(stopCtx) result := make(chan error, 2) @@ -444,13 +455,13 @@ func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *te // Retrieve the run again to get its current status. r, err := b.client.Runs.Read(stopCtx, r.ID) if err != nil { - result <- generalError("Failed to retrieve run", err) + result <- b.generalError("Failed to retrieve run", err) return } switch keyword { case "override": - if r.Status != tfe.RunPolicyOverride { + if r.Status != tfe.RunPolicyOverride && r.Status != tfe.RunPostPlanAwaitingDecision { if r.Status == tfe.RunDiscarded { err = errRunDiscarded } else { @@ -511,7 +522,7 @@ func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *te // Retrieve the run again to get its current status. r, err = b.client.Runs.Read(stopCtx, r.ID) if err != nil { - return generalError("Failed to retrieve run", err) + return b.generalError("Failed to retrieve run", err) } // Make sure we discard the run if possible. @@ -519,9 +530,9 @@ func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *te err = b.client.Runs.Discard(stopCtx, r.ID, tfe.RunDiscardOptions{}) if err != nil { if op.PlanMode == plans.DestroyMode { - return generalError("Failed to discard destroy", err) + return b.generalError("Failed to discard destroy", err) } - return generalError("Failed to discard apply", err) + return b.generalError("Failed to discard apply", err) } } @@ -538,3 +549,101 @@ func (b *Cloud) confirm(stopCtx context.Context, op *backend.Operation, opts *te return <-result } + +// This method will fetch the redacted plan output as a byte slice, mirroring +// the behavior of the similar client.Plans.ReadJSONOutput method. +// +// Note: Apologies for the lengthy definition, this is a result of not being +// able to mock receiver methods +var readRedactedPlan func(context.Context, url.URL, string, string) ([]byte, error) = func(ctx context.Context, baseURL url.URL, token string, planID string) ([]byte, error) { + client := retryablehttp.NewClient() + client.RetryMax = 10 + client.RetryWaitMin = 100 * time.Millisecond + client.RetryWaitMax = 400 * time.Millisecond + client.Logger = logging.HCLogger() + + u, err := baseURL.Parse(fmt.Sprintf( + "plans/%s/json-output-redacted", url.QueryEscape(planID))) + if err != nil { + return nil, err + } + + req, err := retryablehttp.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("Accept", "application/json") + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if err = checkResponseCode(resp); err != nil { + return nil, err + } + + return io.ReadAll(resp.Body) +} + +// decodeRedactedPlan unmarshals a downloaded redacted plan into a struct the +// jsonformat.Renderer expects. +func decodeRedactedPlan(jsonBytes []byte) (*jsonformat.Plan, error) { + r := bytes.NewReader(jsonBytes) + p := &jsonformat.Plan{} + if err := json.NewDecoder(r).Decode(p); err != nil { + return nil, err + } + return p, nil +} + +func checkResponseCode(r *http.Response) error { + if r.StatusCode >= 200 && r.StatusCode <= 299 { + return nil + } + + var errs []string + var err error + + switch r.StatusCode { + case 401: + return tfe.ErrUnauthorized + case 404: + return tfe.ErrResourceNotFound + } + + errs, err = decodeErrorPayload(r) + if err != nil { + return err + } + + return errors.New(strings.Join(errs, "\n")) +} + +func decodeErrorPayload(r *http.Response) ([]string, error) { + // Decode the error payload. + var errs []string + errPayload := &jsonapi.ErrorsPayload{} + err := json.NewDecoder(r.Body).Decode(errPayload) + if err != nil || len(errPayload.Errors) == 0 { + return errs, errors.New(r.Status) + } + + // Parse and format the errors. + for _, e := range errPayload.Errors { + if e.Detail == "" { + errs = append(errs, e.Title) + } else { + errs = append(errs, fmt.Sprintf("%s\n\n%s", e.Title, e.Detail)) + } + } + + return errs, nil +} + +func isValidAppName(name string) bool { + return name == "HCP Terraform" || name == "Terraform Enterprise" +} diff --git a/internal/cloud/backend_context.go b/internal/cloud/backend_context.go index a1236b3663..d3f4c2140f 100644 --- a/internal/cloud/backend_context.go +++ b/internal/cloud/backend_context.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -5,22 +8,23 @@ import ( "fmt" "log" - "github.com/hashicorp/hcl/v2" - tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) -// LocalRun implements backend.Local -func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Full, tfdiags.Diagnostics) { +// LocalRun implements backendrun.Local +func (b *Cloud) LocalRun(op *backendrun.Operation) (*backendrun.LocalRun, statemgr.Full, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - ret := &backend.LocalRun{ + ret := &backendrun.LocalRun{ PlanOpts: &terraform.PlanOpts{ Mode: op.PlanMode, Targets: op.Targets, @@ -97,16 +101,16 @@ func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful diags = diags.Append(fmt.Errorf("error finding remote workspace: %w", err)) return nil, nil, diags } - w, err := b.fetchWorkspace(context.Background(), b.organization, op.Workspace) + w, err := b.fetchWorkspace(context.Background(), b.Organization, op.Workspace) if err != nil { diags = diags.Append(fmt.Errorf("error loading workspace: %w", err)) return nil, nil, diags } if isLocalExecutionMode(w.ExecutionMode) { - log.Printf("[TRACE] skipping retrieving variables from workspace %s/%s (%s), workspace is in Local Execution mode", remoteWorkspaceName, b.organization, remoteWorkspaceID) + log.Printf("[TRACE] skipping retrieving variables from workspace %s/%s (%s), workspace is in Local Execution mode", remoteWorkspaceName, b.Organization, remoteWorkspaceID) } else { - log.Printf("[TRACE] cloud: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.organization, remoteWorkspaceID) + log.Printf("[TRACE] cloud: retrieving variables from workspace %s/%s (%s)", remoteWorkspaceName, b.Organization, remoteWorkspaceID) tfeVariables, err := b.client.Variables.List(context.Background(), remoteWorkspaceID, nil) if err != nil && err != tfe.ErrResourceNotFound { diags = diags.Append(fmt.Errorf("error loading variables: %w", err)) @@ -115,7 +119,7 @@ func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful if tfeVariables != nil { if op.Variables == nil { - op.Variables = make(map[string]backend.UnparsedVariableValue) + op.Variables = make(map[string]backendrun.UnparsedVariableValue) } for _, v := range tfeVariables.Items { @@ -131,7 +135,7 @@ func (b *Cloud) LocalRun(op *backend.Operation) (*backend.LocalRun, statemgr.Ful } if op.Variables != nil { - variables, varDiags := backend.ParseVariableValues(op.Variables, config.Module.Variables) + variables, varDiags := backendrun.ParseVariableValues(op.Variables, config.Module.Variables) diags = diags.Append(varDiags) if diags.HasErrors() { return nil, nil, diags @@ -162,8 +166,8 @@ func (b *Cloud) getRemoteWorkspaceName(localWorkspaceName string) string { func (b *Cloud) getRemoteWorkspace(ctx context.Context, localWorkspaceName string) (*tfe.Workspace, error) { remoteWorkspaceName := b.getRemoteWorkspaceName(localWorkspaceName) - log.Printf("[TRACE] cloud: looking up workspace for %s/%s", b.organization, remoteWorkspaceName) - remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.organization, remoteWorkspaceName) + log.Printf("[TRACE] cloud: looking up workspace for %s/%s", b.Organization, remoteWorkspaceName) + remoteWorkspace, err := b.client.Workspaces.Read(ctx, b.Organization, remoteWorkspaceName) if err != nil { return nil, err } @@ -180,7 +184,7 @@ func (b *Cloud) getRemoteWorkspaceID(ctx context.Context, localWorkspaceName str return remoteWorkspace.ID, nil } -func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues { +func stubAllVariables(vv map[string]backendrun.UnparsedVariableValue, decls map[string]*configs.Variable) terraform.InputValues { ret := make(terraform.InputValues, len(decls)) for name, cfg := range decls { @@ -207,14 +211,14 @@ func stubAllVariables(vv map[string]backend.UnparsedVariableValue, decls map[str return ret } -// remoteStoredVariableValue is a backend.UnparsedVariableValue implementation +// remoteStoredVariableValue is a backendrun.UnparsedVariableValue implementation // that translates from the go-tfe representation of stored variables into // the Terraform Core backend representation of variables. type remoteStoredVariableValue struct { definition *tfe.Variable } -var _ backend.UnparsedVariableValue = (*remoteStoredVariableValue)(nil) +var _ backendrun.UnparsedVariableValue = (*remoteStoredVariableValue)(nil) func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -283,7 +287,7 @@ func (v *remoteStoredVariableValue) ParseVariableValue(mode configs.VariablePars Value: val, // We mark these as "from input" with the rationale that entering - // variable values into the Terraform Cloud or Enterprise UI is, + // variable values into the HCP Terraform or Enterprise UI is, // roughly speaking, a similar idea to entering variable values at // the interactive CLI prompts. It's not a perfect correspondance, // but it's closer than the other options. diff --git a/internal/cloud/backend_context_test.go b/internal/cloud/backend_context_test.go index 635efc88b0..c9682b3e97 100644 --- a/internal/cloud/backend_context_test.go +++ b/internal/cloud/backend_context_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -6,7 +9,9 @@ import ( "testing" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/backend" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -16,7 +21,6 @@ import ( "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) func TestRemoteStoredVariableValue(t *testing.T) { @@ -86,7 +90,7 @@ func TestRemoteStoredVariableValue(t *testing.T) { "HCL computation": { // This (stored expressions containing computation) is not a case // we intentionally supported, but it became possible for remote - // operations in Terraform 0.12 (due to Terraform Cloud/Enterprise + // operations in Terraform 0.12 (due to HCP Terraform and Terraform Enterprise // just writing the HCL verbatim into generated `.tfvars` files). // We support it here for consistency, and we continue to support // it in both places for backward-compatibility. In practice, @@ -182,7 +186,7 @@ func TestRemoteContextWithVars(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() workspaceID, err := b.getRemoteWorkspaceID(context.Background(), testBackendSingleWorkspaceName) @@ -193,7 +197,7 @@ func TestRemoteContextWithVars(t *testing.T) { streams, _ := terminal.StreamsForTesting(t) view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewLocker(0, view), @@ -249,12 +253,12 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { varValue3 := "value3" tests := map[string]struct { - localVariables map[string]backend.UnparsedVariableValue + localVariables map[string]backendrun.UnparsedVariableValue remoteVariables []*tfe.VariableCreateOptions expectedVariables terraform.InputValues }{ "no local variables": { - map[string]backend.UnparsedVariableValue{}, + map[string]backendrun.UnparsedVariableValue{}, []*tfe.VariableCreateOptions{ { Key: &varName1, @@ -303,7 +307,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { }, }, "single conflicting local variable": { - map[string]backend.UnparsedVariableValue{ + map[string]backendrun.UnparsedVariableValue{ varName3: testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.StringVal(varValue3)}, }, []*tfe.VariableCreateOptions{ @@ -352,7 +356,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { }, }, "no conflicting local variable": { - map[string]backend.UnparsedVariableValue{ + map[string]backendrun.UnparsedVariableValue{ varName3: testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.StringVal(varValue3)}, }, []*tfe.VariableCreateOptions{ @@ -405,7 +409,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") defer configCleanup() workspaceID, err := b.getRemoteWorkspaceID(context.Background(), testBackendSingleWorkspaceName) @@ -416,7 +420,7 @@ func TestRemoteVariablesDoNotOverride(t *testing.T) { streams, _ := terminal.StreamsForTesting(t) view := views.NewStateLocker(arguments.ViewHuman, views.NewView(streams)) - op := &backend.Operation{ + op := &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, StateLocker: clistate.NewLocker(0, view), diff --git a/internal/cloud/backend_plan.go b/internal/cloud/backend_plan.go index 2688d65c12..8c59119225 100644 --- a/internal/cloud/backend_plan.go +++ b/internal/cloud/backend_plan.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "bufio" "context" + "encoding/json" "errors" "fmt" "io" @@ -10,19 +14,26 @@ import ( "log" "os" "path/filepath" + "strconv" "strings" "syscall" "time" tfe "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/backend" + version "github.com/hashicorp/go-version" + + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/cloud/cloudplan" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/genconfig" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/tfdiags" ) var planConfigurationVersionsPollInterval = 500 * time.Millisecond -func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backendrun.Operation, w *tfe.Workspace) (*tfe.Run, error) { log.Printf("[INFO] cloud: starting Plan operation") var diags tfdiags.Diagnostics @@ -37,12 +48,22 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation return nil, diags.Err() } + if w.VCSRepo != nil && op.PlanOutPath != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Saved plans not allowed for workspaces with a VCS connection", + "A workspace that is connected to a VCS requires the VCS-driven workflow "+ + "to ensure that the VCS remains the single source of truth.", + )) + return nil, diags.Err() + } + if b.ContextOpts != nil && b.ContextOpts.Parallelism != defaultParallelism { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Custom parallelism values are currently not supported", - `Terraform Cloud does not support setting a custom parallelism `+ - `value at this time.`, + fmt.Sprintf("%s does not support setting a custom parallelism ", b.appName)+ + "value at this time.", )) } @@ -50,17 +71,8 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Displaying a saved plan is currently not supported", - `Terraform Cloud currently requires configuration to be present and `+ - `does not accept an existing saved plan as an argument at this time.`, - )) - } - - if op.PlanOutPath != "" { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Saving a generated plan is currently not supported", - `Terraform Cloud does not support saving the generated execution `+ - `plan locally at this time.`, + fmt.Sprintf("%s currently requires configuration to be present and ", b.appName)+ + "does not accept an existing saved plan as an argument at this time.", )) } @@ -76,31 +88,58 @@ func (b *Cloud) opPlan(stopCtx, cancelCtx context.Context, op *backend.Operation )) } + if len(op.GenerateConfigOut) > 0 { + diags = diags.Append(genconfig.ValidateTargetFile(op.GenerateConfigOut)) + } + // Return if there are any errors. if diags.HasErrors() { return nil, diags.Err() } - return b.plan(stopCtx, cancelCtx, op, w) + // If the run errored, exit before checking whether to save a plan file + run, err := b.plan(stopCtx, cancelCtx, op, w) + if err != nil { + return nil, err + } + + // Save plan file if -out was specified + if op.PlanOutPath != "" { + bookmark := cloudplan.NewSavedPlanBookmark(run.ID, b.Hostname) + err = bookmark.Save(op.PlanOutPath) + if err != nil { + return nil, err + } + } + + // Everything succeded, so display next steps + op.View.PlanNextStep(op.PlanOutPath, op.GenerateConfigOut) + + return run, nil } -func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, w *tfe.Workspace) (*tfe.Run, error) { +func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backendrun.Operation, w *tfe.Workspace) (*tfe.Run, error) { if b.CLI != nil { - header := planDefaultHeader - if op.Type == backend.OperationTypeApply || op.Type == backend.OperationTypeRefresh { - header = applyDefaultHeader + header := fmt.Sprintf(planDefaultHeader, b.appName) + if op.Type == backendrun.OperationTypeApply || op.Type == backendrun.OperationTypeRefresh { + header = fmt.Sprintf(applyDefaultHeader, b.appName) } b.CLI.Output(b.Colorize().Color(strings.TrimSpace(header) + "\n")) } + // Plan-only means they ran terraform plan without -out. + provisional := op.PlanOutPath != "" + planOnly := op.Type == backendrun.OperationTypePlan && !provisional + configOptions := tfe.ConfigurationVersionCreateOptions{ AutoQueueRuns: tfe.Bool(false), - Speculative: tfe.Bool(op.Type == backend.OperationTypePlan), + Speculative: tfe.Bool(planOnly), + Provisional: tfe.Bool(provisional), } cv, err := b.client.ConfigurationVersions.Create(stopCtx, w.ID, configOptions) if err != nil { - return nil, generalError("Failed to create configuration version", err) + return nil, b.generalError("Failed to create configuration version", err) } var configDir string @@ -108,7 +147,7 @@ func (b *Cloud) plan(stopCtx, cancelCtx context.Context, op *backend.Operation, // De-normalize the configuration directory path. configDir, err = filepath.Abs(op.ConfigDir) if err != nil { - return nil, generalError( + return nil, b.generalError( "Failed to get absolute path of the configuration directory: %v", err) } @@ -146,34 +185,39 @@ in order to capture the filesystem context the remote workspace expects: // be executed when we are destroying and doesn't need the config. configDir, err = ioutil.TempDir("", "tf") if err != nil { - return nil, generalError("Failed to create temporary directory", err) + return nil, b.generalError("Failed to create temporary directory", err) } defer os.RemoveAll(configDir) // Make sure the configured working directory exists. err = os.MkdirAll(filepath.Join(configDir, w.WorkingDirectory), 0700) if err != nil { - return nil, generalError( + return nil, b.generalError( "Failed to create temporary working directory", err) } } + log.Printf("[TRACE] backend/cloud: starting configuration upload at %q", configDir) err = b.client.ConfigurationVersions.Upload(stopCtx, cv.UploadURL, configDir) if err != nil { - return nil, generalError("Failed to upload configuration files", err) + return nil, b.generalError("Failed to upload configuration files", err) } + log.Printf("[TRACE] backend/cloud: finished configuration upload") uploaded := false for i := 0; i < 60 && !uploaded; i++ { select { case <-stopCtx.Done(): + log.Printf("[TRACE] backend/cloud: deadline reached while waiting for configuration status") return nil, context.Canceled case <-cancelCtx.Done(): + log.Printf("[TRACE] backend/cloud: operation cancelled while waiting for configuration status") return nil, context.Canceled case <-time.After(planConfigurationVersionsPollInterval): + log.Printf("[TRACE] backend/cloud: reading configuration status") cv, err = b.client.ConfigurationVersions.Read(stopCtx, cv.ID) if err != nil { - return nil, generalError("Failed to retrieve configuration version", err) + return nil, b.generalError("Failed to retrieve configuration version", err) } if cv.Status == tfe.ConfigurationUploaded { @@ -183,15 +227,17 @@ in order to capture the filesystem context the remote workspace expects: } if !uploaded { - return nil, generalError( + return nil, b.generalError( "Failed to upload configuration files", errors.New("operation timed out")) } + log.Printf("[TRACE] backend/cloud: configuration uploaded and ready") runOptions := tfe.RunCreateOptions{ ConfigurationVersion: cv, Refresh: tfe.Bool(op.PlanRefresh), Workspace: w, AutoApply: tfe.Bool(op.AutoApprove), + SavePlan: tfe.Bool(op.PlanOutPath != ""), } switch op.PlanMode { @@ -205,9 +251,9 @@ in order to capture the filesystem context the remote workspace expects: // Shouldn't get here because we should update this for each new // plan mode we add, mapping it to the corresponding RunCreateOptions // field. - return nil, generalError( + return nil, b.generalError( "Invalid plan mode", - fmt.Errorf("Terraform Cloud doesn't support %s", op.PlanMode), + fmt.Errorf("%s doesn't support %s", b.appName, op.PlanMode), ) } @@ -229,13 +275,14 @@ in order to capture the filesystem context the remote workspace expects: if configDiags.HasErrors() { return nil, fmt.Errorf("error loading config with snapshot: %w", configDiags.Errs()[0]) } + variables, varDiags := ParseCloudRunVariables(op.Variables, config.Module.Variables) if varDiags.HasErrors() { return nil, varDiags.Err() } - runVariables := make([]*tfe.RunVariable, len(variables)) + runVariables := make([]*tfe.RunVariable, 0, len(variables)) for name, value := range variables { runVariables = append(runVariables, &tfe.RunVariable{ Key: name, @@ -244,9 +291,13 @@ in order to capture the filesystem context the remote workspace expects: } runOptions.Variables = runVariables + if len(op.GenerateConfigOut) > 0 { + runOptions.AllowConfigGeneration = tfe.Bool(true) + } + r, err := b.client.Runs.Create(stopCtx, runOptions) if err != nil { - return r, generalError("Failed to create run", err) + return r, b.generalError("Failed to create run", err) } // When the lock timeout is set, if the run is still pending and @@ -288,27 +339,23 @@ in order to capture the filesystem context the remote workspace expects: if b.CLI != nil { b.CLI.Output(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf( - runHeader, b.hostname, b.organization, op.Workspace, r.ID)) + "\n")) + runHeader, b.Hostname, b.Organization, op.Workspace, r.ID)) + "\n")) + } + + // Render any warnings that were raised during run creation + if err := b.renderRunWarnings(stopCtx, b.client, r.ID); err != nil { + return r, err } // Retrieve the run to get task stages. // Task Stages are calculated upfront so we only need to call this once for the run. - taskStages := make([]*tfe.TaskStage, 0) - result, err := b.client.Runs.ReadWithOptions(stopCtx, r.ID, &tfe.RunReadOptions{ - Include: []tfe.RunIncludeOpt{tfe.RunTaskStages}, - }) - if err == nil { - taskStages = result.TaskStages - } else { - // This error would be expected for older versions of TFE that do not allow - // fetching task_stages. - if !strings.HasSuffix(err.Error(), "Invalid include parameter") { - return r, generalError("Failed to retrieve run", err) - } + taskStages, err := b.runTaskStages(stopCtx, b.client, r.ID) + if err != nil { + return r, err } - if stageID := getTaskStageIDByName(taskStages, tfe.PrePlan); stageID != nil { - if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, *stageID, "Pre-plan Tasks"); err != nil { + if stage, ok := taskStages[tfe.PrePlan]; ok { + if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Pre-plan Tasks"); err != nil { return r, err } } @@ -318,37 +365,15 @@ in order to capture the filesystem context the remote workspace expects: return r, err } - logs, err := b.client.Plans.Logs(stopCtx, r.Plan.ID) + err = b.renderPlanLogs(stopCtx, op, r) if err != nil { - return r, generalError("Failed to retrieve logs", err) - } - reader := bufio.NewReaderSize(logs, 64*1024) - - if b.CLI != nil { - for next := true; next; { - var l, line []byte - - for isPrefix := true; isPrefix; { - l, isPrefix, err = reader.ReadLine() - if err != nil { - if err != io.EOF { - return r, generalError("Failed to read logs", err) - } - next = false - } - line = append(line, l...) - } - - if next || len(line) > 0 { - b.CLI.Output(b.Colorize().Color(string(line))) - } - } + return r, err } // Retrieve the run to get its current status. r, err = b.client.Runs.Read(stopCtx, r.ID) if err != nil { - return r, generalError("Failed to retrieve run", err) + return r, b.generalError("Failed to retrieve run", err) } // If the run is canceled or errored, we still continue to the @@ -357,8 +382,8 @@ in order to capture the filesystem context the remote workspace expects: // status of the run will be "errored", but there is still policy // information which should be shown. - if stageID := getTaskStageIDByName(taskStages, tfe.PostPlan); stageID != nil { - if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, *stageID, "Post-plan Tasks"); err != nil { + if stage, ok := taskStages[tfe.PostPlan]; ok { + if err := b.waitTaskStage(stopCtx, cancelCtx, op, r, stage.ID, "Post-plan Tasks"); err != nil { return r, err } } @@ -382,29 +407,250 @@ in order to capture the filesystem context the remote workspace expects: return r, nil } -func getTaskStageIDByName(stages []*tfe.TaskStage, stageName tfe.Stage) *string { - if len(stages) == 0 { - return nil - } +// AssertImportCompatible errors if the user is attempting to use configuration- +// driven import and the version of the agent or API is too low to support it. +func (b *Cloud) AssertImportCompatible(config *configs.Config) error { + // Check TFC_RUN_ID is populated, indicating we are running in a remote TFC + // execution environment. + if len(config.Module.Import) > 0 && os.Getenv("TFC_RUN_ID") != "" { + // First, check the remote API version is high enough. + currentAPIVersion, err := version.NewVersion(b.client.RemoteAPIVersion()) + if err != nil { + return fmt.Errorf("Error parsing remote API version. To proceed, please remove any import blocks from your config. Please report the following error to the Terraform team: %s", err) + } + desiredAPIVersion, _ := version.NewVersion("2.6") + if currentAPIVersion.LessThan(desiredAPIVersion) { + return fmt.Errorf("Import blocks are not supported in this version of Terraform Enterprise. Please remove any import blocks from your config or upgrade Terraform Enterprise.") + } - for _, stage := range stages { - if stage.Stage == stageName { - return &stage.ID + // Second, check the agent version is high enough. + agentEnv, isSet := os.LookupEnv("TFC_AGENT_VERSION") + if !isSet { + return fmt.Errorf("Error reading HCP Terraform Agent version. To proceed, please remove any import blocks from your config. Please report the following error to the Terraform team: TFC_AGENT_VERSION not present.") + } + currentAgentVersion, err := version.NewVersion(agentEnv) + if err != nil { + return fmt.Errorf("Error parsing HCP Terraform Agent version. To proceed, please remove any import blocks from your config. Please report the following error to the Terraform team: %s", err) + } + desiredAgentVersion, _ := version.NewVersion("1.10") + if currentAgentVersion.LessThan(desiredAgentVersion) { + return fmt.Errorf("Import blocks are not supported in this version of the HCP Terraform Agent. You are using agent version %s, but this feature requires version %s. Please remove any import blocks from your config or upgrade your agent.", currentAgentVersion, desiredAgentVersion) } } return nil } +// renderPlanLogs reads the streamed plan JSON logs and calls the JSON Plan renderer (jsonformat.RenderPlan) to +// render the plan output. The plan output is fetched from the redacted output endpoint. +func (b *Cloud) renderPlanLogs(ctx context.Context, op *backendrun.Operation, run *tfe.Run) error { + logs, err := b.client.Plans.Logs(ctx, run.Plan.ID) + if err != nil { + return err + } + + if b.CLI != nil { + reader := bufio.NewReaderSize(logs, 64*1024) + + for next := true; next; { + var l, line []byte + var err error + + for isPrefix := true; isPrefix; { + l, isPrefix, err = reader.ReadLine() + if err != nil { + if err != io.EOF { + return b.generalError("Failed to read logs", err) + } + next = false + } + + line = append(line, l...) + } + + if next || len(line) > 0 { + log := &jsonformat.JSONLog{} + if err := json.Unmarshal(line, log); err != nil { + // If we can not parse the line as JSON, we will simply + // print the line. This maintains backwards compatibility for + // users who do not wish to enable structured output in their + // workspace. + b.CLI.Output(string(line)) + continue + } + + // We will ignore plan output, change summary or outputs logs + // during the plan phase. + if log.Type == jsonformat.LogOutputs || + log.Type == jsonformat.LogChangeSummary || + log.Type == jsonformat.LogPlannedChange { + continue + } + + if b.renderer != nil { + // Otherwise, we will print the log + err := b.renderer.RenderLog(log) + if err != nil { + return err + } + } + } + } + } + + // Get the run's current status and include the workspace and plan. We will check if + // the run has errored, if structured output is enabled, and if the plan + run, err = b.client.Runs.ReadWithOptions(ctx, run.ID, &tfe.RunReadOptions{ + Include: []tfe.RunIncludeOpt{tfe.RunWorkspace, tfe.RunPlan}, + }) + if err != nil { + return err + } + + // If the run was errored, canceled, or discarded we will not resume the rest + // of this logic and attempt to render the plan, except in certain special circumstances + // where the plan errored but successfully generated configuration during an + // import operation. In that case, we need to keep going so we can load the JSON plan + // and use it to write the generated config to the specified output file. + shouldGenerateConfig := shouldGenerateConfig(op.GenerateConfigOut, run) + shouldRenderPlan := shouldRenderPlan(run) + if !shouldRenderPlan && !shouldGenerateConfig { + // We won't return an error here since we need to resume the logic that + // follows after rendering the logs (run tasks, cost estimation, etc.) + return nil + } + + // Fetch the redacted JSON plan if we need it for either rendering the plan + // or writing out generated configuration. + var redactedPlan *jsonformat.Plan + renderSRO, err := b.shouldRenderStructuredRunOutput(run) + if err != nil { + return err + } + if renderSRO || shouldGenerateConfig { + jsonBytes, err := readRedactedPlan(ctx, b.client.BaseURL(), b.Token, run.Plan.ID) + if err != nil { + return b.generalError("Failed to read JSON plan", err) + } + redactedPlan, err = decodeRedactedPlan(jsonBytes) + if err != nil { + return b.generalError("Failed to decode JSON plan", err) + } + } + + // Write any generated config before rendering the plan, so we can stop in case of errors + if shouldGenerateConfig { + diags := maybeWriteGeneratedConfig(redactedPlan, op.GenerateConfigOut) + if diags.HasErrors() { + return diags.Err() + } + } + + // Only generate the human readable output from the plan if structured run output is + // enabled. Otherwise we risk duplicate plan output since plan output may also be + // shown in the streamed logs. + if shouldRenderPlan && renderSRO { + b.renderer.RenderHumanPlan(*redactedPlan, op.PlanMode) + } + + return nil +} + +// maybeWriteGeneratedConfig attempts to write any generated configuration from the JSON plan +// to the specified output file, if generated configuration exists and the correct flag was +// passed to the plan command. +func maybeWriteGeneratedConfig(plan *jsonformat.Plan, out string) (diags tfdiags.Diagnostics) { + if genconfig.ShouldWriteConfig(out) { + diags := genconfig.ValidateTargetFile(out) + if diags.HasErrors() { + return diags + } + + var writer io.Writer + for _, c := range plan.ResourceChanges { + change := genconfig.Change{ + Addr: c.Address, + GeneratedConfig: c.Change.GeneratedConfig, + } + if c.Change.Importing != nil { + change.ImportID = c.Change.Importing.ID + } + + var moreDiags tfdiags.Diagnostics + writer, _, moreDiags = change.MaybeWriteConfig(writer, out) + if moreDiags.HasErrors() { + return diags.Append(moreDiags) + } + } + } + + return diags +} + +// shouldRenderStructuredRunOutput ensures the remote workspace has structured +// run output enabled and, if using Terraform Enterprise, ensures it is a release +// that supports enabling SRO for CLI-driven runs. The plan output will have +// already been rendered when the logs were read if this wasn't the case. +func (b *Cloud) shouldRenderStructuredRunOutput(run *tfe.Run) (bool, error) { + if b.renderer == nil || !run.Workspace.StructuredRunOutputEnabled { + return false, nil + } + + // If the cloud backend is configured against TFC, we only require that + // the workspace has structured run output enabled. + if b.client.IsCloud() && run.Workspace.StructuredRunOutputEnabled { + return true, nil + } + + // If the cloud backend is configured against TFE, ensure the release version + // supports enabling SRO for CLI runs. + if b.client.IsEnterprise() { + tfeVersion := b.client.RemoteTFEVersion() + if tfeVersion != "" { + v := strings.Split(tfeVersion[1:], "-") + releaseDate, err := strconv.Atoi(v[0]) + if err != nil { + return false, err + } + + // Any release older than 202302-1 will not support enabling SRO for + // CLI-driven runs + if releaseDate < 202302 { + return false, nil + } else if run.Workspace.StructuredRunOutputEnabled { + return true, nil + } + } + } + + // Version of TFE is unknowable + return false, nil +} + +func shouldRenderPlan(run *tfe.Run) bool { + return !(run.Status == tfe.RunErrored || run.Status == tfe.RunCanceled || + run.Status == tfe.RunDiscarded) +} + +func shouldGenerateConfig(out string, run *tfe.Run) bool { + return (run.Plan.Status == tfe.PlanErrored || run.Plan.Status == tfe.PlanFinished) && + run.Plan.GeneratedConfiguration && len(out) > 0 +} + const planDefaultHeader = ` -[reset][yellow]Running plan in Terraform Cloud. Output will stream here. Pressing Ctrl-C +[reset][yellow]Running plan in %s. Output will stream here. Pressing Ctrl-C will stop streaming the logs, but will not stop the plan running remotely.[reset] Preparing the remote plan... ` const runHeader = ` -[reset][yellow]To view this run in a browser, visit: -https://%s/app/%s/%s/runs/%s[reset] +[reset][yellow]To view this run in a browser, visit:[reset] +[reset][yellow]https://%s/app/%s/%s/runs/%s[reset] +` + +const runHeaderErr = ` +To view this run in the browser, visit: +https://%s/app/%s/%s/runs/%s ` // The newline in this error is to make it look good in the CLI! diff --git a/internal/cloud/backend_plan_test.go b/internal/cloud/backend_plan_test.go index 162a9841aa..0bf6e86e74 100644 --- a/internal/cloud/backend_plan_test.go +++ b/internal/cloud/backend_plan_test.go @@ -1,20 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "context" + "net/http" "os" "os/signal" + "path/filepath" "strings" "syscall" "testing" "time" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/cloud/cloudplan" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" + "github.com/hashicorp/terraform/internal/command/jsonformat" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/initwd" @@ -23,19 +32,18 @@ import ( "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" - "github.com/mitchellh/cli" ) -func testOperationPlan(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationPlan(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() return testOperationPlanWithTimeout(t, configDir, 0) } -func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) @@ -47,12 +55,12 @@ func testOperationPlanWithTimeout(t *testing.T, configDir string, timeout time.D depLocks := depsfile.NewLocks() depLocks.SetProviderOverridden(addrs.MustParseProviderSourceString("registry.terraform.io/hashicorp/null")) - return &backend.Operation{ + return &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanRefresh: true, StateLocker: clistate.NewLocker(timeout, stateLockerView), - Type: backend.OperationTypePlan, + Type: backendrun.OperationTypePlan, View: operationView, DependencyLocks: depLocks, }, configCleanup, done @@ -74,7 +82,7 @@ func TestCloud_planBasic(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -82,8 +90,8 @@ func TestCloud_planBasic(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -96,6 +104,52 @@ func TestCloud_planBasic(t *testing.T) { } } +func TestCloud_planJSONBasic(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan-json-basic") + defer configCleanup() + defer done(t) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatal("expected a non-empty plan") + } + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after the operation finished + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) + } +} + func TestCloud_planCanceled(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -115,7 +169,7 @@ func TestCloud_planCanceled(t *testing.T) { run.Stop() <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -142,7 +196,7 @@ func TestCloud_planLongLine(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -150,14 +204,64 @@ func TestCloud_planLongLine(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) } } +func TestCloud_planJSONFull(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan-json-full") + defer configCleanup() + defer done(t) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatal("expected a non-empty plan") + } + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "tfcoremock_simple_resource.example: Refreshing state... [id=my-simple-resource]") { + t.Fatalf("expected plan log: %s", gotOut) + } + + if !strings.Contains(gotOut, "2 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after the operation finished + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) + } +} + func TestCloud_planWithoutPermissions(t *testing.T) { b, bCleanup := testBackendWithTags(t) defer bCleanup() @@ -165,7 +269,7 @@ func TestCloud_planWithoutPermissions(t *testing.T) { // Create a named workspace without permissions. w, err := b.client.Workspaces.Create( context.Background(), - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("prod"), }, @@ -187,7 +291,7 @@ func TestCloud_planWithoutPermissions(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -217,7 +321,7 @@ func TestCloud_planWithParallelism(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } @@ -234,7 +338,7 @@ func TestCloud_planWithPlan(t *testing.T) { op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() - op.PlanFile = &planfile.Reader{} + op.PlanFile = planfile.NewWrappedLocal(&planfile.Reader{}) op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) @@ -244,7 +348,7 @@ func TestCloud_planWithPlan(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -263,8 +367,11 @@ func TestCloud_planWithPath(t *testing.T) { op, configCleanup, done := testOperationPlan(t, "./testdata/plan") defer configCleanup() + defer done(t) - op.PlanOutPath = "./testdata/plan" + tmpDir := t.TempDir() + pfPath := tmpDir + "/plan.tfplan" + op.PlanOutPath = pfPath op.Workspace = testBackendSingleWorkspaceName run, err := b.Operation(context.Background(), op) @@ -272,9 +379,82 @@ func TestCloud_planWithPath(t *testing.T) { t.Fatalf("error starting operation: %v", err) } + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatal("expected a non-empty plan") + } + + output := b.CLI.(*cli.MockUi).OutputWriter.String() + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) + } + if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summary in output: %s", output) + } + + plan, err := cloudplan.LoadSavedPlanBookmark(pfPath) + if err != nil { + t.Fatalf("error loading cloud plan file: %v", err) + } + if !strings.Contains(plan.RunID, "run-") || plan.Hostname != "app.terraform.io" { + t.Fatalf("unexpected contents in saved cloud plan: %v", plan) + } + + // We should find a run inside the mock client that has a provisional, non-speculative + // configuration version + configVersionsAPI := b.client.ConfigurationVersions.(*MockConfigurationVersions) + if got, want := len(configVersionsAPI.configVersions), 1; got != want { + t.Fatalf("wrong number of configuration versions in the mock client %d; want %d", got, want) + } + for _, configVersion := range configVersionsAPI.configVersions { + if configVersion.Provisional != true { + t.Errorf("wrong Provisional setting in the created configuration version\ngot %v, expected %v", configVersion.Provisional, true) + } + + if configVersion.Speculative != false { + t.Errorf("wrong Speculative setting in the created configuration version\ngot %v, expected %v", configVersion.Speculative, false) + } + } +} + +// It's not nice to apply rogue configs in vcs-connected workspaces, so the +// backend voluntarily declines to create a run that could be applied. +func TestCloud_planWithPathAndVCS(t *testing.T) { + b, bCleanup := testBackendWithTags(t) + defer bCleanup() + + // Create a named workspace with a VCS. + _, err := b.client.Workspaces.Create( + context.Background(), + b.Organization, + tfe.WorkspaceCreateOptions{ + Name: tfe.String("prod-vcs"), + VCSRepo: &tfe.VCSRepoOptions{}, + }, + ) + if err != nil { + t.Fatalf("error creating named workspace: %v", err) + } + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan") + defer configCleanup() + + tmpDir := t.TempDir() + pfPath := tmpDir + "/plan.tfplan" + op.PlanOutPath = pfPath + op.Workspace = "prod-vcs" + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -282,8 +462,8 @@ func TestCloud_planWithPath(t *testing.T) { } errOutput := output.Stderr() - if !strings.Contains(errOutput, "generated plan is currently not supported") { - t.Fatalf("expected a generated plan error, got: %v", errOutput) + if !strings.Contains(errOutput, "not allowed for workspaces with a VCS") { + t.Fatalf("expected a VCS error, got: %v", errOutput) } } @@ -304,7 +484,7 @@ func TestCloud_planWithoutRefresh(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -341,7 +521,7 @@ func TestCloud_planWithRefreshOnly(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -403,7 +583,7 @@ func TestCloud_planWithTarget(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { @@ -449,7 +629,7 @@ func TestCloud_planWithReplace(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } if run.PlanEmpty { @@ -488,13 +668,13 @@ func TestCloud_planWithRequiredVariables(t *testing.T) { <-run.Done() // The usual error of a required variable being missing is deferred and the operation // is successful. - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatal("expected plan operation to succeed") } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } } @@ -514,7 +694,7 @@ func TestCloud_planNoConfig(t *testing.T) { <-run.Done() output := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -543,7 +723,7 @@ func TestCloud_planNoChanges(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if !run.PlanEmpty { @@ -586,7 +766,7 @@ func TestCloud_planForceLocal(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -594,8 +774,8 @@ func TestCloud_planForceLocal(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -622,7 +802,7 @@ func TestCloud_planWithoutOperationsEntitlement(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -630,8 +810,8 @@ func TestCloud_planWithoutOperationsEntitlement(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -647,7 +827,7 @@ func TestCloud_planWorkspaceWithoutOperations(t *testing.T) { // Create a named workspace that doesn't allow operations. _, err := b.client.Workspaces.Create( ctx, - b.organization, + b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String("no-operations"), }, @@ -672,7 +852,7 @@ func TestCloud_planWorkspaceWithoutOperations(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -680,8 +860,8 @@ func TestCloud_planWorkspaceWithoutOperations(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("unexpected TFC header in output: %s", output) + if strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("unexpected HCP Terraform header in output: %s", output) } if output := done(t).Stdout(); !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -695,7 +875,7 @@ func TestCloud_planLockTimeout(t *testing.T) { ctx := context.Background() // Retrieve the workspace used to run this operation in. - w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name) + w, err := b.client.Workspaces.Read(ctx, b.Organization, b.WorkspaceMapping.Name) if err != nil { t.Fatalf("error retrieving workspace: %v", err) } @@ -748,8 +928,8 @@ func TestCloud_planLockTimeout(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "Lock timeout exceeded") { t.Fatalf("expected lock timout error in output: %s", output) @@ -776,7 +956,7 @@ func TestCloud_planDestroy(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -801,7 +981,7 @@ func TestCloud_planDestroyNoConfig(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -818,7 +998,7 @@ func TestCloud_planWithWorkingDirectory(t *testing.T) { } // Configure the workspace to use a custom working directory. - _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options) + _, err := b.client.Workspaces.Update(context.Background(), b.Organization, b.WorkspaceMapping.Name, options) if err != nil { t.Fatalf("error configuring working directory: %v", err) } @@ -835,7 +1015,7 @@ func TestCloud_planWithWorkingDirectory(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -846,8 +1026,8 @@ func TestCloud_planWithWorkingDirectory(t *testing.T) { if !strings.Contains(output, "The remote workspace is configured to work with configuration") { t.Fatalf("expected working directory warning: %s", output) } - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -863,7 +1043,7 @@ func TestCloud_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { } // Configure the workspace to use a custom working directory. - _, err := b.client.Workspaces.Update(context.Background(), b.organization, b.WorkspaceMapping.Name, options) + _, err := b.client.Workspaces.Update(context.Background(), b.Organization, b.WorkspaceMapping.Name, options) if err != nil { t.Fatalf("error configuring working directory: %v", err) } @@ -894,7 +1074,7 @@ func TestCloud_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -902,8 +1082,8 @@ func TestCloud_planWithWorkingDirectoryFromCurrentPath(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "1 to add, 0 to change, 0 to destroy") { t.Fatalf("expected plan summary in output: %s", output) @@ -926,7 +1106,7 @@ func TestCloud_planCostEstimation(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -934,8 +1114,8 @@ func TestCloud_planCostEstimation(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "Resources: 1 of 1 estimated") { t.Fatalf("expected cost estimate result in output: %s", output) @@ -961,7 +1141,7 @@ func TestCloud_planPolicyPass(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } if run.PlanEmpty { @@ -969,8 +1149,8 @@ func TestCloud_planPolicyPass(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "Sentinel Result: true") { t.Fatalf("expected policy check result in output: %s", output) @@ -996,7 +1176,7 @@ func TestCloud_planPolicyHardFail(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -1009,8 +1189,8 @@ func TestCloud_planPolicyHardFail(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "Sentinel Result: false") { t.Fatalf("expected policy check result in output: %s", output) @@ -1036,7 +1216,7 @@ func TestCloud_planPolicySoftFail(t *testing.T) { <-run.Done() viewOutput := done(t) - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if !run.PlanEmpty { @@ -1049,8 +1229,8 @@ func TestCloud_planPolicySoftFail(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "Sentinel Result: false") { t.Fatalf("expected policy check result in output: %s", output) @@ -1076,7 +1256,7 @@ func TestCloud_planWithRemoteError(t *testing.T) { } <-run.Done() - if run.Result == backend.OperationSuccess { + if run.Result == backendrun.OperationSuccess { t.Fatal("expected plan operation to fail") } if run.Result.ExitStatus() != 1 { @@ -1084,14 +1264,55 @@ func TestCloud_planWithRemoteError(t *testing.T) { } output := b.CLI.(*cli.MockUi).OutputWriter.String() - if !strings.Contains(output, "Running plan in Terraform Cloud") { - t.Fatalf("expected TFC header in output: %s", output) + if !strings.Contains(output, "Running plan in HCP Terraform") { + t.Fatalf("expected HCP Terraform header in output: %s", output) } if !strings.Contains(output, "null_resource.foo: 1 error") { t.Fatalf("expected plan error in output: %s", output) } } +func TestCloud_planJSONWithRemoteError(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + // Initialize the plan renderer + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan-json-error") + defer configCleanup() + defer done(t) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result == backendrun.OperationSuccess { + t.Fatal("expected plan operation to fail") + } + if run.Result.ExitStatus() != 1 { + t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) + } + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "Unsupported block type") { + t.Fatalf("unexpected plan error in output: %s", gotOut) + } +} + func TestCloud_planOtherError(t *testing.T) { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -1108,7 +1329,269 @@ func TestCloud_planOtherError(t *testing.T) { } if !strings.Contains(err.Error(), - "Terraform Cloud returned an unexpected error:\n\nI'm a little teacup") { + "HCP Terraform returned an unexpected error:\n\nI'm a little teacup") { t.Fatalf("expected error message, got: %s", err.Error()) } } + +func TestCloud_planImportConfigGeneration(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan-import-config-gen") + defer configCleanup() + defer done(t) + + genPath := filepath.Join(op.ConfigDir, "generated.tf") + op.GenerateConfigOut = genPath + defer os.Remove(genPath) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationSuccess { + t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) + } + if run.PlanEmpty { + t.Fatal("expected a non-empty plan") + } + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "1 to import, 0 to add, 0 to change, 0 to destroy") { + t.Fatalf("expected plan summary in output: %s", gotOut) + } + + stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) + // An error suggests that the state was not unlocked after the operation finished + if _, err := stateMgr.Lock(statemgr.NewLockInfo()); err != nil { + t.Fatalf("unexpected error locking state after successful plan: %s", err.Error()) + } + + testFileEquals(t, genPath, filepath.Join(op.ConfigDir, "generated.tf.expected")) +} + +func TestCloud_planImportGenerateInvalidConfig(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + stream, close := terminal.StreamsForTesting(t) + + b.renderer = &jsonformat.Renderer{ + Streams: stream, + Colorize: mockColorize(), + } + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan-import-config-gen-validation-error") + defer configCleanup() + defer done(t) + + genPath := filepath.Join(op.ConfigDir, "generated.tf") + op.GenerateConfigOut = genPath + defer os.Remove(genPath) + + op.Workspace = testBackendSingleWorkspaceName + + mockSROWorkspace(t, b, op.Workspace) + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + if run.Result != backendrun.OperationFailure { + t.Fatalf("expected operation to fail") + } + if run.Result.ExitStatus() != 1 { + t.Fatalf("expected exit code 1, got %d", run.Result.ExitStatus()) + } + + outp := close(t) + gotOut := outp.Stdout() + + if !strings.Contains(gotOut, "Conflicting configuration arguments") { + t.Fatalf("Expected error in output: %s", gotOut) + } + + testFileEquals(t, genPath, filepath.Join(op.ConfigDir, "generated.tf.expected")) +} + +func TestCloud_planInvalidGenConfigOutPath(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + op, configCleanup, done := testOperationPlan(t, "./testdata/plan-import-config-gen-exists") + defer configCleanup() + + genPath := filepath.Join(op.ConfigDir, "generated.tf") + op.GenerateConfigOut = genPath + + op.Workspace = testBackendSingleWorkspaceName + + run, err := b.Operation(context.Background(), op) + if err != nil { + t.Fatalf("error starting operation: %v", err) + } + + <-run.Done() + output := done(t) + if run.Result == backendrun.OperationSuccess { + t.Fatal("expected plan operation to fail") + } + + errOutput := output.Stderr() + if !strings.Contains(errOutput, "generated file already exists") { + t.Fatalf("expected configuration files error, got: %v", errOutput) + } +} + +func TestCloud_planShouldRenderSRO(t *testing.T) { + t.Run("when instance is HCP Terraform", func(t *testing.T) { + handlers := map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.5") + w.Header().Set("TFP-AppName", "HCP Terraform") + }, + } + b, bCleanup := testBackendWithHandlers(t, handlers) + t.Cleanup(bCleanup) + b.renderer = &jsonformat.Renderer{} + + t.Run("and SRO is enabled", func(t *testing.T) { + r := &tfe.Run{ + Workspace: &tfe.Workspace{ + StructuredRunOutputEnabled: true, + }, + } + assertSRORendered(t, b, r, true) + }) + + t.Run("and SRO is not enabled", func(t *testing.T) { + r := &tfe.Run{ + Workspace: &tfe.Workspace{ + StructuredRunOutputEnabled: false, + }, + } + assertSRORendered(t, b, r, false) + }) + + }) + + t.Run("when instance is TFE and version supports CLI SRO", func(t *testing.T) { + handlers := map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.5") + w.Header().Set("TFP-AppName", "Terraform Enterprise") + w.Header().Set("X-TFE-Version", "v202303-1") + }, + } + b, bCleanup := testBackendWithHandlers(t, handlers) + t.Cleanup(bCleanup) + b.renderer = &jsonformat.Renderer{} + + t.Run("and SRO is enabled", func(t *testing.T) { + r := &tfe.Run{ + Workspace: &tfe.Workspace{ + StructuredRunOutputEnabled: true, + }, + } + assertSRORendered(t, b, r, true) + }) + + t.Run("and SRO is not enabled", func(t *testing.T) { + r := &tfe.Run{ + Workspace: &tfe.Workspace{ + StructuredRunOutputEnabled: false, + }, + } + assertSRORendered(t, b, r, false) + }) + }) + + t.Run("when instance is a known unsupported TFE release", func(t *testing.T) { + handlers := map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.5") + w.Header().Set("TFP-AppName", "Terraform Enterprise") + w.Header().Set("X-TFE-Version", "v202208-1") + }, + } + b, bCleanup := testBackendWithHandlers(t, handlers) + t.Cleanup(bCleanup) + b.renderer = &jsonformat.Renderer{} + + r := &tfe.Run{ + Workspace: &tfe.Workspace{ + StructuredRunOutputEnabled: true, + }, + } + assertSRORendered(t, b, r, false) + }) + + t.Run("when instance is an unknown TFE release", func(t *testing.T) { + handlers := map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-AppName", "Terraform Enterprise") + w.Header().Set("TFP-API-Version", "2.5") + }, + } + b, bCleanup := testBackendWithHandlers(t, handlers) + t.Cleanup(bCleanup) + b.renderer = &jsonformat.Renderer{} + + r := &tfe.Run{ + Workspace: &tfe.Workspace{ + StructuredRunOutputEnabled: true, + }, + } + assertSRORendered(t, b, r, false) + }) + +} + +func assertSRORendered(t *testing.T, b *Cloud, r *tfe.Run, shouldRender bool) { + got, err := b.shouldRenderStructuredRunOutput(r) + if err != nil { + t.Fatalf("expected no error: %v", err) + } + if shouldRender != got { + t.Fatalf("expected SRO to be rendered: %t, got %t", shouldRender, got) + } +} + +func testFileEquals(t *testing.T, got, want string) { + t.Helper() + + actual, err := os.ReadFile(got) + if err != nil { + t.Fatalf("error reading %s", got) + } + + expected, err := os.ReadFile(want) + if err != nil { + t.Fatalf("error reading %s", want) + } + + if diff := cmp.Diff(string(actual), string(expected)); len(diff) > 0 { + t.Fatalf("got:\n%s\nwant:\n%s\ndiff:\n%s", actual, expected, diff) + } +} diff --git a/internal/cloud/backend_refresh_test.go b/internal/cloud/backend_refresh_test.go index 3abb935777..6f59fa9954 100644 --- a/internal/cloud/backend_refresh_test.go +++ b/internal/cloud/backend_refresh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -6,7 +9,9 @@ import ( "testing" "time" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/cli" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" @@ -14,31 +19,30 @@ import ( "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" - "github.com/mitchellh/cli" ) -func testOperationRefresh(t *testing.T, configDir string) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationRefresh(t *testing.T, configDir string) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() return testOperationRefreshWithTimeout(t, configDir, 0) } -func testOperationRefreshWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backend.Operation, func(), func(*testing.T) *terminal.TestOutput) { +func testOperationRefreshWithTimeout(t *testing.T, configDir string, timeout time.Duration) (*backendrun.Operation, func(), func(*testing.T) *terminal.TestOutput) { t.Helper() - _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir) + _, configLoader, configCleanup := initwd.MustLoadConfigForTests(t, configDir, "tests") streams, done := terminal.StreamsForTesting(t) view := views.NewView(streams) stateLockerView := views.NewStateLocker(arguments.ViewHuman, view) operationView := views.NewOperation(arguments.ViewHuman, false, view) - return &backend.Operation{ + return &backendrun.Operation{ ConfigDir: configDir, ConfigLoader: configLoader, PlanRefresh: true, StateLocker: clistate.NewLocker(timeout, stateLockerView), - Type: backend.OperationTypeRefresh, + Type: backendrun.OperationTypeRefresh, View: operationView, }, configCleanup, done } @@ -62,13 +66,13 @@ func TestCloud_refreshBasicActuallyRunsApplyRefresh(t *testing.T) { } <-run.Done() - if run.Result != backend.OperationSuccess { + if run.Result != backendrun.OperationSuccess { t.Fatalf("operation failed: %s", b.CLI.(*cli.MockUi).ErrorWriter.String()) } output := b.CLI.(*cli.MockUi).OutputWriter.String() if !strings.Contains(output, "Proceeding with 'terraform apply -refresh-only -auto-approve'") { - t.Fatalf("expected TFC header in output: %s", output) + t.Fatalf("expected HCP Terraform header in output: %s", output) } stateMgr, _ := b.StateMgr(testBackendSingleWorkspaceName) diff --git a/internal/cloud/backend_runTasks.go b/internal/cloud/backend_runTasks.go deleted file mode 100644 index 8b7e45bfd4..0000000000 --- a/internal/cloud/backend_runTasks.go +++ /dev/null @@ -1,149 +0,0 @@ -package cloud - -import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/go-tfe" -) - -type taskResultSummary struct { - unreachable bool - pending int - failed int - failedMandatory int - passed int -} - -type taskStageReadFunc func(b *Cloud, stopCtx context.Context) (*tfe.TaskStage, error) - -func summarizeTaskResults(taskResults []*tfe.TaskResult) *taskResultSummary { - var pendingCount, errCount, errMandatoryCount, passedCount int - for _, task := range taskResults { - if task.Status == "unreachable" { - return &taskResultSummary{ - unreachable: true, - } - } else if task.Status == "running" || task.Status == "pending" { - pendingCount++ - } else if task.Status == "passed" { - passedCount++ - } else { - // Everything else is a failure - errCount++ - if task.WorkspaceTaskEnforcementLevel == "mandatory" { - errMandatoryCount++ - } - } - } - - return &taskResultSummary{ - unreachable: false, - pending: pendingCount, - failed: errCount, - failedMandatory: errMandatoryCount, - passed: passedCount, - } -} - -func (b *Cloud) runTasksWithTaskResults(context *IntegrationContext, output IntegrationOutputWriter, fetchTaskStage taskStageReadFunc) error { - return context.Poll(func(i int) (bool, error) { - stage, err := fetchTaskStage(b, context.StopContext) - - if err != nil { - return false, generalError("Failed to retrieve task stage", err) - } - - summary := summarizeTaskResults(stage.TaskResults) - - if summary.unreachable { - output.Output("Skipping task results.") - output.End() - return false, nil - } - - if summary.pending > 0 { - pendingMessage := "%d tasks still pending, %d passed, %d failed ... " - message := fmt.Sprintf(pendingMessage, summary.pending, summary.passed, summary.failed) - - if i%4 == 0 { - if i > 0 { - output.OutputElapsed(message, len(pendingMessage)) // Up to 2 digits are allowed by the max message allocation - } - } - return true, nil - } - - // No more tasks pending/running. Print all the results. - - // Track the first task name that is a mandatory enforcement level breach. - var firstMandatoryTaskFailed *string = nil - - if i == 0 { - output.Output(fmt.Sprintf("All tasks completed! %d passed, %d failed", summary.passed, summary.failed)) - } else { - output.OutputElapsed(fmt.Sprintf("All tasks completed! %d passed, %d failed", summary.passed, summary.failed), 50) - } - - output.Output("") - - for _, t := range stage.TaskResults { - capitalizedStatus := string(t.Status) - capitalizedStatus = strings.ToUpper(capitalizedStatus[:1]) + capitalizedStatus[1:] - - status := "[green]" + capitalizedStatus - if t.Status != "passed" { - level := string(t.WorkspaceTaskEnforcementLevel) - level = strings.ToUpper(level[:1]) + level[1:] - status = fmt.Sprintf("[red]%s (%s)", capitalizedStatus, level) - - if t.WorkspaceTaskEnforcementLevel == "mandatory" && firstMandatoryTaskFailed == nil { - firstMandatoryTaskFailed = &t.TaskName - } - } - - title := fmt.Sprintf(`%s ⸺ %s`, t.TaskName, status) - output.SubOutput(title) - - if len(t.Message) > 0 { - output.SubOutput(fmt.Sprintf("[dim]%s", t.Message)) - } - if len(t.URL) > 0 { - output.SubOutput(fmt.Sprintf("[dim]Details: %s", t.URL)) - } - output.SubOutput("") - } - - // If a mandatory enforcement level is breached, return an error. - var taskErr error = nil - var overall string = "[green]Passed" - if firstMandatoryTaskFailed != nil { - overall = "[red]Failed" - if summary.failedMandatory > 1 { - taskErr = fmt.Errorf("the run failed because %d mandatory tasks are required to succeed", summary.failedMandatory) - } else { - taskErr = fmt.Errorf("the run failed because the run task, %s, is required to succeed", *firstMandatoryTaskFailed) - } - } else if summary.failed > 0 { // we have failures but none of them mandatory - overall = "[green]Passed with advisory failures" - } - - output.SubOutput("") - output.SubOutput("[bold]Overall Result: " + overall) - - output.End() - - return false, taskErr - }) -} - -func (b *Cloud) runTasks(ctx *IntegrationContext, output IntegrationOutputWriter, stageID string) error { - return b.runTasksWithTaskResults(ctx, output, func(b *Cloud, stopCtx context.Context) (*tfe.TaskStage, error) { - options := tfe.TaskStageReadOptions{ - Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults}, - } - - return b.client.TaskStages.Read(ctx.StopContext, stageID, &options) - }) -} diff --git a/internal/cloud/backend_run_warning.go b/internal/cloud/backend_run_warning.go new file mode 100644 index 0000000000..c37d7e0899 --- /dev/null +++ b/internal/cloud/backend_run_warning.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "fmt" + "strings" + + tfe "github.com/hashicorp/go-tfe" +) + +const ( + changedPolicyEnforcementAction = "changed_policy_enforcements" + changedTaskEnforcementAction = "changed_task_enforcements" + ignoredPolicySetAction = "ignored_policy_sets" +) + +func (b *Cloud) renderRunWarnings(ctx context.Context, client *tfe.Client, runId string) error { + if b.CLI == nil { + return nil + } + + result, err := client.RunEvents.List(ctx, runId, nil) + if err != nil { + return err + } + if result == nil { + return nil + } + + // We don't have to worry about paging as the API doesn't support it yet + for _, re := range result.Items { + switch re.Action { + case changedPolicyEnforcementAction, changedTaskEnforcementAction, ignoredPolicySetAction: + if re.Description != "" { + b.CLI.Warn(b.Colorize().Color(strings.TrimSpace(fmt.Sprintf( + runWarningHeader, re.Description)) + "\n")) + } + } + } + + return nil +} + +const runWarningHeader = ` +[reset][yellow]Warning:[reset] %s +` diff --git a/internal/cloud/backend_run_warning_test.go b/internal/cloud/backend_run_warning_test.go new file mode 100644 index 0000000000..49e22f91e1 --- /dev/null +++ b/internal/cloud/backend_run_warning_test.go @@ -0,0 +1,156 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/hashicorp/cli" + "github.com/hashicorp/go-tfe" + tfemocks "github.com/hashicorp/go-tfe/mocks" + "go.uber.org/mock/gomock" +) + +func MockAllRunEvents(t *testing.T, client *tfe.Client) (fullRunID string, emptyRunID string) { + ctrl := gomock.NewController(t) + + fullRunID = "run-full" + emptyRunID = "run-empty" + + mockRunEventsAPI := tfemocks.NewMockRunEvents(ctrl) + + emptyList := tfe.RunEventList{ + Items: []*tfe.RunEvent{}, + } + fullList := tfe.RunEventList{ + Items: []*tfe.RunEvent{ + { + Action: "created", + CreatedAt: time.Now(), + Description: "", + }, + { + Action: "changed_task_enforcements", + CreatedAt: time.Now(), + Description: "The enforcement level for task 'MockTask' was changed to 'advisory' because the run task limit was exceeded.", + }, + { + Action: "changed_policy_enforcements", + CreatedAt: time.Now(), + Description: "The enforcement level for policy 'MockPolicy' was changed to 'advisory' because the policy limit was exceeded.", + }, + { + Action: "ignored_policy_sets", + CreatedAt: time.Now(), + Description: "The policy set 'MockPolicySet' was ignored because the versioned policy set limit was exceeded.", + }, + { + Action: "queued", + CreatedAt: time.Now(), + Description: "", + }, + }, + } + // Mock Full Request + mockRunEventsAPI. + EXPECT(). + List(gomock.Any(), fullRunID, gomock.Any()). + Return(&fullList, nil). + AnyTimes() + + // Mock Full Request + mockRunEventsAPI. + EXPECT(). + List(gomock.Any(), emptyRunID, gomock.Any()). + Return(&emptyList, nil). + AnyTimes() + + // Mock a bad Read response + mockRunEventsAPI. + EXPECT(). + List(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, tfe.ErrInvalidRunID). + AnyTimes() + + // Wire up the mock interfaces + client.RunEvents = mockRunEventsAPI + return +} + +func TestRunEventWarningsAll(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + fullRunID, _ := MockAllRunEvents(t, client) + + ctx := context.TODO() + + err := b.renderRunWarnings(ctx, client, fullRunID) + if err != nil { + t.Fatalf("Expected to not error but received %s", err) + } + + output := b.CLI.(*cli.MockUi).ErrorWriter.String() + testString := "The enforcement level for task 'MockTask'" + if !strings.Contains(output, testString) { + t.Fatalf("Expected %q to contain %q but it did not", output, testString) + } + testString = "The enforcement level for policy 'MockPolicy'" + if !strings.Contains(output, testString) { + t.Fatalf("Expected %q to contain %q but it did not", output, testString) + } + testString = "The policy set 'MockPolicySet'" + if !strings.Contains(output, testString) { + t.Fatalf("Expected %q to contain %q but it did not", output, testString) + } +} + +func TestRunEventWarningsEmpty(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + _, emptyRunID := MockAllRunEvents(t, client) + + ctx := context.TODO() + + err := b.renderRunWarnings(ctx, client, emptyRunID) + if err != nil { + t.Fatalf("Expected to not error but received %s", err) + } + + output := b.CLI.(*cli.MockUi).ErrorWriter.String() + if output != "" { + t.Fatalf("Expected %q to be empty but it was not", output) + } +} + +func TestRunEventWarningsWithError(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + MockAllRunEvents(t, client) + + ctx := context.TODO() + + err := b.renderRunWarnings(ctx, client, "bad run id") + + if err == nil { + t.Error("Expected to error but did not") + } +} diff --git a/internal/cloud/backend_show.go b/internal/cloud/backend_show.go new file mode 100644 index 0000000000..44c3b31005 --- /dev/null +++ b/internal/cloud/backend_show.go @@ -0,0 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "fmt" + "strings" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/cloud/cloudplan" + "github.com/hashicorp/terraform/internal/plans" +) + +// ShowPlanForRun downloads the JSON plan output for the specified cloud run +// (either the redacted or unredacted format, per the caller's request), and +// returns it in a cloudplan.RemotePlanJSON wrapper struct (along with various +// metadata required by terraform show). It's intended for use by the terraform +// show command, in order to format and display a saved cloud plan. +func (b *Cloud) ShowPlanForRun(ctx context.Context, runID, runHostname string, redacted bool) (*cloudplan.RemotePlanJSON, error) { + var jsonBytes []byte + mode := plans.NormalMode + var opts []plans.Quality + + // Bail early if wrong hostname + if runHostname != b.Hostname { + return nil, fmt.Errorf("hostname for run (%s) does not match the configured cloud integration (%s)", runHostname, b.Hostname) + } + + // Get run and plan + r, err := b.client.Runs.ReadWithOptions(ctx, runID, &tfe.RunReadOptions{Include: []tfe.RunIncludeOpt{tfe.RunPlan, tfe.RunWorkspace}}) + if err == tfe.ErrResourceNotFound { + return nil, fmt.Errorf("couldn't read information for cloud run %s; make sure you've run `terraform login` and that you have permission to view the run", runID) + } else if err != nil { + return nil, fmt.Errorf("couldn't read information for cloud run %s: %w", runID, err) + } + + // Sort out the run mode + if r.IsDestroy { + mode = plans.DestroyMode + } else if r.RefreshOnly { + mode = plans.RefreshOnlyMode + } + + // Check that the plan actually finished + switch r.Plan.Status { + case tfe.PlanErrored: + // Errored plans might still be displayable, but we want to mention it to the renderer. + opts = append(opts, plans.Errored) + case tfe.PlanFinished: + // Good to go, but alert the renderer if it has no changes. + if !r.Plan.HasChanges { + opts = append(opts, plans.NoChanges) + } + default: + // Bail, we can't use this. + err = fmt.Errorf("can't display a cloud plan that is currently %s", r.Plan.Status) + return nil, err + } + + // Fetch the json plan! + if redacted { + jsonBytes, err = readRedactedPlan(ctx, b.client.BaseURL(), b.Token, r.Plan.ID) + } else { + jsonBytes, err = b.client.Plans.ReadJSONOutput(ctx, r.Plan.ID) + } + if err == tfe.ErrResourceNotFound { + if redacted { + return nil, fmt.Errorf("couldn't read plan data for cloud run %s; make sure you've run `terraform login` and that you have permission to view the run", runID) + } else { + return nil, fmt.Errorf("couldn't read unredacted JSON plan data for cloud run %s; make sure you've run `terraform login` and that you have admin permissions on the workspace", runID) + } + } else if err != nil { + return nil, fmt.Errorf("couldn't read plan data for cloud run %s: %w", runID, err) + } + + // Format a run header and footer + header := strings.TrimSpace(fmt.Sprintf(runHeader, b.Hostname, b.Organization, r.Workspace.Name, r.ID)) + footer := strings.TrimSpace(statusFooter(r.Status, r.Actions.IsConfirmable, r.Workspace.Locked)) + + out := &cloudplan.RemotePlanJSON{ + JSONBytes: jsonBytes, + Redacted: redacted, + Mode: mode, + Qualities: opts, + RunHeader: header, + RunFooter: footer, + } + + return out, nil +} + +func statusFooter(status tfe.RunStatus, isConfirmable, locked bool) string { + statusText := strings.ReplaceAll(string(status), "_", " ") + statusColor := "red" + statusNote := "not confirmable" + if isConfirmable { + statusColor = "green" + statusNote = "confirmable" + } + lockedColor := "green" + lockedText := "unlocked" + if locked { + lockedColor = "red" + lockedText = "locked" + } + return fmt.Sprintf(statusFooterText, statusColor, statusText, statusNote, lockedColor, lockedText) +} + +const statusFooterText = ` +[reset][%s]Run status: %s (%s)[reset] +[%s]Workspace is %s[reset] +` diff --git a/internal/cloud/backend_show_test.go b/internal/cloud/backend_show_test.go new file mode 100644 index 0000000000..dcc2b4d3e8 --- /dev/null +++ b/internal/cloud/backend_show_test.go @@ -0,0 +1,217 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "path/filepath" + "strings" + "testing" + + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/plans" +) + +// A brief discourse on the theory of testing for this feature. Doing +// `terraform show cloudplan.tfplan` relies on the correctness of the following +// behaviors: +// +// 1. HCP Terraform API returns redacted or unredacted plan JSON on request, if permission +// requirements are met and the run is in a condition where that JSON exists. +// 2. Cloud.ShowPlanForRun() makes correct API calls, calculates metadata +// properly given a tfe.Run, and returns either a cloudplan.RemotePlanJSON or an err. +// 3. The Show command instantiates Cloud backend when given a cloud planfile, +// calls .ShowPlanForRun() on it, and passes result to Display() impls. +// 4. Display() impls yield the correct output when given a cloud plan json biscuit. +// +// 1 is axiomatic and outside our domain. 3 is regrettably totally untestable +// unless we refactor the Meta command to enable stubbing out a backend factory +// or something, which seems inadvisable at this juncture. 4 is exercised over +// in internal/command/views/show_test.go. And thus, this file only cares about +// item 2. + +// 404 on run: special error message +func TestCloud_showMissingRun(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + mockSROWorkspace(t, b, testBackendSingleWorkspaceName) + + absentRunID := "run-WwwwXxxxYyyyZzzz" + _, err := b.ShowPlanForRun(context.Background(), absentRunID, "app.terraform.io", true) + if !strings.Contains(err.Error(), "terraform login") { + t.Fatalf("expected error message to suggest checking your login status, instead got: %s", err) + } +} + +// If redacted json is available but unredacted is not +func TestCloud_showMissingUnredactedJson(t *testing.T) { + b, mc, bCleanup := testBackendAndMocksWithName(t) + defer bCleanup() + mockSROWorkspace(t, b, testBackendSingleWorkspaceName) + + ctx := context.Background() + + runID, err := testCloudRunForShow(mc, "./testdata/plan-json-basic-no-unredacted", tfe.RunPlannedAndSaved, tfe.PlanFinished) + if err != nil { + t.Fatalf("failed to init test data: %s", err) + } + // Showing the human-formatted plan should still work as expected! + redacted, err := b.ShowPlanForRun(ctx, runID, "app.terraform.io", true) + if err != nil { + t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) + } + if !strings.Contains(string(redacted.JSONBytes), `"plan_format_version":`) { + t.Fatalf("show for human doesn't include expected redacted json content") + } + // Should be marked as containing changes and non-errored + canNotApply := false + errored := false + for _, opt := range redacted.Qualities { + if opt == plans.NoChanges { + canNotApply = true + } + if opt == plans.Errored { + errored = true + } + } + if canNotApply || errored { + t.Fatalf("expected neither errored nor can't-apply in opts, instead got: %#v", redacted.Qualities) + } + + // But show -json should result in a special error. + _, err = b.ShowPlanForRun(ctx, runID, "app.terraform.io", false) + if err == nil { + t.Fatalf("unexpected success: reading unredacted json without admin permissions should have errored") + } + if !strings.Contains(err.Error(), "admin") { + t.Fatalf("expected error message to suggest your permissions are wrong, instead got: %s", err) + } +} + +// If both kinds of json are available, both kinds of show should work +func TestCloud_showIncludesUnredactedJson(t *testing.T) { + b, mc, bCleanup := testBackendAndMocksWithName(t) + defer bCleanup() + mockSROWorkspace(t, b, testBackendSingleWorkspaceName) + + ctx := context.Background() + + runID, err := testCloudRunForShow(mc, "./testdata/plan-json-basic", tfe.RunPlannedAndSaved, tfe.PlanFinished) + if err != nil { + t.Fatalf("failed to init test data: %s", err) + } + // Showing the human-formatted plan should work as expected: + redacted, err := b.ShowPlanForRun(ctx, runID, "app.terraform.io", true) + if err != nil { + t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) + } + if !strings.Contains(string(redacted.JSONBytes), `"plan_format_version":`) { + t.Fatalf("show for human doesn't include expected redacted json content") + } + // Showing the external json plan format should work as expected: + unredacted, err := b.ShowPlanForRun(ctx, runID, "app.terraform.io", false) + if err != nil { + t.Fatalf("failed to show plan for robot, even though unredacted json should be present: %s", err) + } + if !strings.Contains(string(unredacted.JSONBytes), `"format_version":`) { + t.Fatalf("show for robot doesn't include expected unredacted json content") + } +} + +func TestCloud_showNoChanges(t *testing.T) { + b, mc, bCleanup := testBackendAndMocksWithName(t) + defer bCleanup() + mockSROWorkspace(t, b, testBackendSingleWorkspaceName) + + ctx := context.Background() + + runID, err := testCloudRunForShow(mc, "./testdata/plan-json-no-changes", tfe.RunPlannedAndSaved, tfe.PlanFinished) + if err != nil { + t.Fatalf("failed to init test data: %s", err) + } + // Showing the human-formatted plan should work as expected: + redacted, err := b.ShowPlanForRun(ctx, runID, "app.terraform.io", true) + if err != nil { + t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) + } + // Should be marked as no changes + canNotApply := false + for _, opt := range redacted.Qualities { + if opt == plans.NoChanges { + canNotApply = true + } + } + if !canNotApply { + t.Fatalf("expected opts to include CanNotApply, instead got: %#v", redacted.Qualities) + } +} + +func TestCloud_showFooterNotConfirmable(t *testing.T) { + b, mc, bCleanup := testBackendAndMocksWithName(t) + defer bCleanup() + mockSROWorkspace(t, b, testBackendSingleWorkspaceName) + + ctx := context.Background() + + runID, err := testCloudRunForShow(mc, "./testdata/plan-json-full", tfe.RunDiscarded, tfe.PlanFinished) + if err != nil { + t.Fatalf("failed to init test data: %s", err) + } + + // A little more custom run tweaking: + mc.Runs.Runs[runID].Actions.IsConfirmable = false + + // Showing the human-formatted plan should work as expected: + redacted, err := b.ShowPlanForRun(ctx, runID, "app.terraform.io", true) + if err != nil { + t.Fatalf("failed to show plan for human, even though redacted json should be present: %s", err) + } + + // Footer should mention that you can't apply it: + if !strings.Contains(redacted.RunFooter, "not confirmable") { + t.Fatalf("footer should call out that run isn't confirmable, instead got: %s", redacted.RunFooter) + } +} + +func testCloudRunForShow(mc *MockClient, configDir string, runStatus tfe.RunStatus, planStatus tfe.PlanStatus) (string, error) { + ctx := context.Background() + + // get workspace ID + wsID := mc.Workspaces.workspaceNames[testBackendSingleWorkspaceName].ID + // create and upload config version + cvOpts := tfe.ConfigurationVersionCreateOptions{ + AutoQueueRuns: tfe.Bool(false), + Speculative: tfe.Bool(false), + } + cv, err := mc.ConfigurationVersions.Create(ctx, wsID, cvOpts) + if err != nil { + return "", err + } + absDir, err := filepath.Abs(configDir) + if err != nil { + return "", err + } + err = mc.ConfigurationVersions.Upload(ctx, cv.UploadURL, absDir) + if err != nil { + return "", err + } + // create run + rOpts := tfe.RunCreateOptions{ + PlanOnly: tfe.Bool(false), + IsDestroy: tfe.Bool(false), + RefreshOnly: tfe.Bool(false), + ConfigurationVersion: cv, + Workspace: &tfe.Workspace{ID: wsID}, + } + r, err := mc.Runs.Create(ctx, rOpts) + if err != nil { + return "", err + } + // mess with statuses (this is what requires full access to mock client) + mc.Runs.Runs[r.ID].Status = runStatus + mc.Plans.plans[r.Plan.ID].Status = planStatus + + // return the ID + return r.ID, nil +} diff --git a/internal/cloud/backend_state.go b/internal/cloud/backend_state.go deleted file mode 100644 index f8cb9f2455..0000000000 --- a/internal/cloud/backend_state.go +++ /dev/null @@ -1,196 +0,0 @@ -package cloud - -import ( - "bytes" - "context" - "crypto/md5" - "encoding/base64" - "encoding/json" - "errors" - "fmt" - - tfe "github.com/hashicorp/go-tfe" - - "github.com/hashicorp/terraform/internal/command/jsonstate" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statefile" - "github.com/hashicorp/terraform/internal/states/statemgr" -) - -type remoteClient struct { - client *tfe.Client - lockInfo *statemgr.LockInfo - organization string - runID string - stateUploadErr bool - workspace *tfe.Workspace - forcePush bool -} - -// Get the remote state. -func (r *remoteClient) Get() (*remote.Payload, error) { - ctx := context.Background() - - sv, err := r.client.StateVersions.ReadCurrent(ctx, r.workspace.ID) - if err != nil { - if err == tfe.ErrResourceNotFound { - // If no state exists, then return nil. - return nil, nil - } - return nil, fmt.Errorf("failed to retrieve state: %w", err) - } - - state, err := r.client.StateVersions.Download(ctx, sv.DownloadURL) - if err != nil { - return nil, fmt.Errorf("failed to download state: %w", err) - } - - // If the state is empty, then return nil. - if len(state) == 0 { - return nil, nil - } - - // Get the MD5 checksum of the state. - sum := md5.Sum(state) - - return &remote.Payload{ - Data: state, - MD5: sum[:], - }, nil -} - -// Put the remote state. -func (r *remoteClient) Put(state []byte) error { - ctx := context.Background() - - // Read the raw state into a Terraform state. - stateFile, err := statefile.Read(bytes.NewReader(state)) - if err != nil { - return fmt.Errorf("failed to read state: %w", err) - } - - ov, err := jsonstate.MarshalOutputs(stateFile.State.RootModule().OutputValues) - if err != nil { - return fmt.Errorf("failed to translate outputs: %w", err) - } - o, err := json.Marshal(ov) - if err != nil { - return fmt.Errorf("failed to marshal outputs to json: %w", err) - } - - options := tfe.StateVersionCreateOptions{ - Lineage: tfe.String(stateFile.Lineage), - Serial: tfe.Int64(int64(stateFile.Serial)), - MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), - State: tfe.String(base64.StdEncoding.EncodeToString(state)), - Force: tfe.Bool(r.forcePush), - JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(o)), - } - - // If we have a run ID, make sure to add it to the options - // so the state will be properly associated with the run. - if r.runID != "" { - options.Run = &tfe.Run{ID: r.runID} - } - - // Create the new state. - _, err = r.client.StateVersions.Create(ctx, r.workspace.ID, options) - if err != nil { - r.stateUploadErr = true - return fmt.Errorf("failed to upload state: %w", err) - } - - return nil -} - -// Delete the remote state. -func (r *remoteClient) Delete() error { - err := r.client.Workspaces.Delete(context.Background(), r.organization, r.workspace.Name) - if err != nil && err != tfe.ErrResourceNotFound { - return fmt.Errorf("failed to delete workspace %s: %w", r.workspace.Name, err) - } - - return nil -} - -// EnableForcePush to allow the remote client to overwrite state -// by implementing remote.ClientForcePusher -func (r *remoteClient) EnableForcePush() { - r.forcePush = true -} - -// Lock the remote state. -func (r *remoteClient) Lock(info *statemgr.LockInfo) (string, error) { - ctx := context.Background() - - lockErr := &statemgr.LockError{Info: r.lockInfo} - - // Lock the workspace. - _, err := r.client.Workspaces.Lock(ctx, r.workspace.ID, tfe.WorkspaceLockOptions{ - Reason: tfe.String("Locked by Terraform"), - }) - if err != nil { - if err == tfe.ErrWorkspaceLocked { - lockErr.Info = info - err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, r.organization, r.workspace.Name) - } - lockErr.Err = err - return "", lockErr - } - - r.lockInfo = info - - return r.lockInfo.ID, nil -} - -// Unlock the remote state. -func (r *remoteClient) Unlock(id string) error { - ctx := context.Background() - - // We first check if there was an error while uploading the latest - // state. If so, we will not unlock the workspace to prevent any - // changes from being applied until the correct state is uploaded. - if r.stateUploadErr { - return nil - } - - lockErr := &statemgr.LockError{Info: r.lockInfo} - - // With lock info this should be treated as a normal unlock. - if r.lockInfo != nil { - // Verify the expected lock ID. - if r.lockInfo.ID != id { - lockErr.Err = errors.New("lock ID does not match existing lock") - return lockErr - } - - // Unlock the workspace. - _, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID) - if err != nil { - lockErr.Err = err - return lockErr - } - - return nil - } - - // Verify the optional force-unlock lock ID. - if r.organization+"/"+r.workspace.Name != id { - lockErr.Err = fmt.Errorf( - "lock ID %q does not match existing lock ID \"%s/%s\"", - id, - r.organization, - r.workspace.Name, - ) - return lockErr - } - - // Force unlock the workspace. - _, err := r.client.Workspaces.ForceUnlock(ctx, r.workspace.ID) - if err != nil { - lockErr.Err = err - return lockErr - } - - return nil -} diff --git a/internal/cloud/backend_state_test.go b/internal/cloud/backend_state_test.go deleted file mode 100644 index 3b9833c38e..0000000000 --- a/internal/cloud/backend_state_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package cloud - -import ( - "bytes" - "os" - "testing" - - tfe "github.com/hashicorp/go-tfe" - - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/states/remote" - "github.com/hashicorp/terraform/internal/states/statefile" -) - -func TestRemoteClient_impl(t *testing.T) { - var _ remote.Client = new(remoteClient) -} - -func TestRemoteClient(t *testing.T) { - client := testRemoteClient(t) - remote.TestClient(t, client) -} - -func TestRemoteClient_stateVersionCreated(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - raw, err := b.StateMgr(testBackendSingleWorkspaceName) - if err != nil { - t.Fatalf("error: %v", err) - } - - client := raw.(*State).Client - - err = client.Put(([]byte)(` -{ - "version": 4, - "terraform_version": "1.3.0", - "serial": 1, - "lineage": "backend-change", - "outputs": { - "foo": { - "type": "string", - "value": "bar" - } - } -}`)) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - stateVersionsAPI := b.client.StateVersions.(*MockStateVersions) - if got, want := len(stateVersionsAPI.stateVersions), 1; got != want { - t.Fatalf("wrong number of state versions in the mock client %d; want %d", got, want) - } - - var stateVersion *tfe.StateVersion - for _, sv := range stateVersionsAPI.stateVersions { - stateVersion = sv - } - - if stateVersionsAPI.outputStates[stateVersion.ID] == nil || len(stateVersionsAPI.outputStates[stateVersion.ID]) == 0 { - t.Fatal("no state version outputs in the mock client") - } -} - -func TestRemoteClient_TestRemoteLocks(t *testing.T) { - b, bCleanup := testBackendWithName(t) - defer bCleanup() - - s1, err := b.StateMgr(testBackendSingleWorkspaceName) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - s2, err := b.StateMgr(testBackendSingleWorkspaceName) - if err != nil { - t.Fatalf("expected no error, got %v", err) - } - - remote.TestRemoteLocks(t, s1.(*State).Client, s2.(*State).Client) -} - -func TestRemoteClient_withRunID(t *testing.T) { - // Set the TFE_RUN_ID environment variable before creating the client! - if err := os.Setenv("TFE_RUN_ID", GenerateID("run-")); err != nil { - t.Fatalf("error setting env var TFE_RUN_ID: %v", err) - } - - // Create a new test client. - client := testRemoteClient(t) - - // Create a new empty state. - sf := statefile.New(states.NewState(), "", 0) - var buf bytes.Buffer - statefile.Write(sf, &buf) - - // Store the new state to verify (this will be done - // by the mock that is used) that the run ID is set. - if err := client.Put(buf.Bytes()); err != nil { - t.Fatalf("expected no error, got %v", err) - } -} diff --git a/internal/cloud/backend_taskStage_policyEvaluation.go b/internal/cloud/backend_taskStage_policyEvaluation.go new file mode 100644 index 0000000000..360b3235d7 --- /dev/null +++ b/internal/cloud/backend_taskStage_policyEvaluation.go @@ -0,0 +1,168 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-tfe" +) + +type policyEvaluationSummary struct { + unreachable bool + pending int + failed int + passed int +} + +type Symbol rune + +const ( + Tick Symbol = '\u2713' + Cross Symbol = '\u00d7' + Warning Symbol = '\u24be' + Arrow Symbol = '\u2192' + DownwardArrow Symbol = '\u21b3' +) + +type policyEvaluationSummarizer struct { + finished bool + cloud *Cloud + counter int +} + +func newPolicyEvaluationSummarizer(b *Cloud, ts *tfe.TaskStage) taskStageSummarizer { + if len(ts.PolicyEvaluations) == 0 { + return nil + } + return &policyEvaluationSummarizer{ + finished: false, + cloud: b, + } +} + +func (pes *policyEvaluationSummarizer) Summarize(context *IntegrationContext, output IntegrationOutputWriter, ts *tfe.TaskStage) (bool, *string, error) { + if pes.counter == 0 { + output.Output("------------------------------------------------------------------------\n") + output.Output("[bold]Policy Evaluations\n") + pes.counter++ + } + + if pes.finished { + return false, nil, nil + } + + counts := summarizePolicyEvaluationResults(ts.PolicyEvaluations) + + if counts.pending != 0 { + pendingMessage := "Evaluating ... " + return true, &pendingMessage, nil + } + + if counts.unreachable { + output.Output("Skipping policy evaluation.") + output.End() + return false, nil, nil + } + + // Print out the summary + if err := pes.taskStageWithPolicyEvaluation(context, output, ts.PolicyEvaluations); err != nil { + return false, nil, err + } + // Mark as finished + pes.finished = true + + return false, nil, nil +} + +func summarizePolicyEvaluationResults(policyEvaluations []*tfe.PolicyEvaluation) *policyEvaluationSummary { + var pendingCount, errCount, passedCount int + for _, policyEvaluation := range policyEvaluations { + switch policyEvaluation.Status { + case "unreachable": + return &policyEvaluationSummary{ + unreachable: true, + } + case "running", "pending", "queued": + pendingCount++ + case "passed": + passedCount++ + default: + // Everything else is a failure + errCount++ + } + } + + return &policyEvaluationSummary{ + unreachable: false, + pending: pendingCount, + failed: errCount, + passed: passedCount, + } +} + +func (pes *policyEvaluationSummarizer) taskStageWithPolicyEvaluation(context *IntegrationContext, output IntegrationOutputWriter, policyEvaluation []*tfe.PolicyEvaluation) error { + var result, message, kind string + // Currently only one policy evaluation supported : OPA + for _, polEvaluation := range policyEvaluation { + if polEvaluation.PolicyKind == "opa" { + kind = "OPA" + } else { + kind = "Sentinel" + } + if polEvaluation.Status == tfe.PolicyEvaluationPassed { + message = fmt.Sprintf("[dim] This result means that all %s policies passed and the protected behavior is allowed", kind) + result = fmt.Sprintf("[green]%s", strings.ToUpper(string(tfe.PolicyEvaluationPassed))) + if polEvaluation.ResultCount.AdvisoryFailed > 0 { + result += " (with advisory)" + } + } else { + message = fmt.Sprintf("[dim] This result means that one or more %s policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules", kind) + result = fmt.Sprintf("[red]%s", strings.ToUpper(string(tfe.PolicyEvaluationFailed))) + } + + output.Output("--------------------------------\n") + output.Output(fmt.Sprintf("[bold]%s Policy Evaluation\n", kind)) + output.Output(fmt.Sprintf("[bold]%c%c Overall Result: %s", Arrow, Arrow, result)) + + output.Output(message) + + total := getPolicyCount(polEvaluation.ResultCount) + + output.Output(fmt.Sprintf("%d policies evaluated\n", total)) + + policyOutcomes, err := pes.cloud.client.PolicySetOutcomes.List(context.StopContext, polEvaluation.ID, nil) + if err != nil { + return err + } + + for i, out := range policyOutcomes.Items { + output.Output(fmt.Sprintf("%c Policy set %d: [bold]%s (%d)", Arrow, i+1, out.PolicySetName, len(out.Outcomes))) + for _, outcome := range out.Outcomes { + output.Output(fmt.Sprintf(" %c Policy name: [bold]%s", DownwardArrow, outcome.PolicyName)) + switch outcome.Status { + case "passed": + output.Output(fmt.Sprintf(" | [green][bold]%c Passed", Tick)) + case "failed": + if outcome.EnforcementLevel == tfe.EnforcementAdvisory { + output.Output(fmt.Sprintf(" | [blue][bold]%c Advisory", Warning)) + } else { + output.Output(fmt.Sprintf(" | [red][bold]%c Failed", Cross)) + } + } + if outcome.Description != "" { + output.Output(fmt.Sprintf(" | [dim]%s", outcome.Description)) + } else { + output.Output(" | [dim]No description available") + } + } + } + } + return nil +} + +func getPolicyCount(resultCount *tfe.PolicyResultCount) int { + return resultCount.AdvisoryFailed + resultCount.MandatoryFailed + resultCount.Errored + resultCount.Passed +} diff --git a/internal/cloud/backend_taskStage_policyEvaluation_test.go b/internal/cloud/backend_taskStage_policyEvaluation_test.go new file mode 100644 index 0000000000..4e5fcdbdff --- /dev/null +++ b/internal/cloud/backend_taskStage_policyEvaluation_test.go @@ -0,0 +1,189 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "strings" + "testing" + + "github.com/hashicorp/go-tfe" +) + +func TestCloud_runTaskStageWithOPAPolicyEvaluation(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + integrationContext, writer := newMockIntegrationContext(b, t) + + cases := map[string]struct { + taskStage func() *tfe.TaskStage + context *IntegrationContext + writer *testIntegrationOutput + expectedOutputs []string + isError bool + }{ + "all-succeeded": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "pol-pass", ResultCount: &tfe.PolicyResultCount{Passed: 1}, Status: "passed", PolicyKind: "opa"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]OPA Policy Evaluation\n\n│ [bold]→→ Overall Result: [green]PASSED\n│ [dim] This result means that all OPA policies passed and the protected behavior is allowed\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-passes (1)\n│ ↳ Policy name: [bold]policy-pass\n│ | [green][bold]✓ Passed\n│ | [dim]This policy will pass\n"}, + isError: false, + }, + "mandatory-failed": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "pol-fail", ResultCount: &tfe.PolicyResultCount{MandatoryFailed: 1}, Status: "failed", PolicyKind: "opa"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]→→ Overall Result: [red]FAILED\n│ [dim] This result means that one or more OPA policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-fails (1)\n│ ↳ Policy name: [bold]policy-fail\n│ | [red][bold]× Failed\n│ | [dim]This policy will fail"}, + isError: true, + }, + "advisory-failed": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{AdvisoryFailed: 1}, Status: "failed", PolicyKind: "opa"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]OPA Policy Evaluation\n\n│ [bold]→→ Overall Result: [red]FAILED\n│ [dim] This result means that one or more OPA policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-fails (1)\n│ ↳ Policy name: [bold]policy-fail\n│ | [blue][bold]Ⓘ Advisory\n│ | [dim]This policy will fail"}, + isError: false, + }, + "unreachable": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{Errored: 1}, Status: "unreachable", PolicyKind: "opa"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"Skipping policy evaluation."}, + isError: false, + }, + } + + for _, c := range cases { + c.writer.output.Reset() + trs := policyEvaluationSummarizer{ + cloud: b, + } + c.context.Poll(0, 0, func(i int) (bool, error) { + cont, _, _ := trs.Summarize(c.context, c.writer, c.taskStage()) + if cont { + return true, nil + } + + output := c.writer.output.String() + for _, expected := range c.expectedOutputs { + if !strings.Contains(output, expected) { + t.Fatalf("Expected output to contain '%s' but it was:\n\n%s", expected, output) + } + } + return false, nil + }) + } +} + +func TestCloud_runTaskStageWithSentinelPolicyEvaluation(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + integrationContext, writer := newMockIntegrationContext(b, t) + + cases := map[string]struct { + taskStage func() *tfe.TaskStage + context *IntegrationContext + writer *testIntegrationOutput + expectedOutputs []string + isError bool + }{ + "all-succeeded": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "pol-pass", ResultCount: &tfe.PolicyResultCount{Passed: 1}, Status: "passed", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]Sentinel Policy Evaluation\n\n│ [bold]→→ Overall Result: [green]PASSED\n│ [dim] This result means that all Sentinel policies passed and the protected behavior is allowed\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-passes (1)\n│ ↳ Policy name: [bold]policy-pass\n│ | [green][bold]✓ Passed\n│ | [dim]This policy will pass\n"}, + isError: false, + }, + "mandatory-failed": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "pol-fail", ResultCount: &tfe.PolicyResultCount{MandatoryFailed: 1}, Status: "failed", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]→→ Overall Result: [red]FAILED\n│ [dim] This result means that one or more Sentinel policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-fails (1)\n│ ↳ Policy name: [bold]policy-fail\n│ | [red][bold]× Failed\n│ | [dim]This policy will fail"}, + isError: true, + }, + "advisory-failed": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{AdvisoryFailed: 1}, Status: "failed", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"│ [bold]Sentinel Policy Evaluation\n\n│ [bold]→→ Overall Result: [red]FAILED\n│ [dim] This result means that one or more Sentinel policies failed. More than likely, this was due to the discovery of violations by the main rule and other sub rules\n│ 1 policies evaluated\n\n│ → Policy set 1: [bold]policy-set-that-fails (1)\n│ ↳ Policy name: [bold]policy-fail\n│ | [blue][bold]Ⓘ Advisory\n│ | [dim]This policy will fail"}, + isError: false, + }, + "unreachable": { + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.PolicyEvaluations = []*tfe.PolicyEvaluation{ + {ID: "adv-fail", ResultCount: &tfe.PolicyResultCount{Errored: 1}, Status: "unreachable", PolicyKind: "sentinel"}, + } + return ts + }, + writer: writer, + context: integrationContext, + expectedOutputs: []string{"Skipping policy evaluation."}, + isError: false, + }, + } + + for _, c := range cases { + c.writer.output.Reset() + trs := policyEvaluationSummarizer{ + cloud: b, + } + c.context.Poll(0, 0, func(i int) (bool, error) { + cont, _, _ := trs.Summarize(c.context, c.writer, c.taskStage()) + if cont { + return true, nil + } + + output := c.writer.output.String() + for _, expected := range c.expectedOutputs { + if !strings.Contains(output, expected) { + t.Fatalf("Expected output to contain '%s' but it was:\n\n%s", expected, output) + } + } + return false, nil + }) + } +} diff --git a/internal/cloud/backend_taskStage_taskResults.go b/internal/cloud/backend_taskStage_taskResults.go new file mode 100644 index 0000000000..f3c9713bb0 --- /dev/null +++ b/internal/cloud/backend_taskStage_taskResults.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-tfe" +) + +type taskResultSummary struct { + unreachable bool + pending int + failed int + failedMandatory int + passed int +} + +type taskResultSummarizer struct { + finished bool + cloud *Cloud + counter int +} + +func newTaskResultSummarizer(b *Cloud, ts *tfe.TaskStage) taskStageSummarizer { + if len(ts.TaskResults) == 0 { + return nil + } + return &taskResultSummarizer{ + finished: false, + cloud: b, + } +} + +func (trs *taskResultSummarizer) Summarize(context *IntegrationContext, output IntegrationOutputWriter, ts *tfe.TaskStage) (bool, *string, error) { + if trs.finished { + return false, nil, nil + } + trs.counter++ + + counts := summarizeTaskResults(ts.TaskResults) + + if counts.pending != 0 { + pendingMessage := "%d tasks still pending, %d passed, %d failed ... " + message := fmt.Sprintf(pendingMessage, counts.pending, counts.passed, counts.failed) + return true, &message, nil + } + if counts.unreachable { + output.Output("Skipping task results.") + output.End() + return false, nil, nil + } + + // Print out the summary + trs.runTasksWithTaskResults(output, ts.TaskResults, counts) + + // Mark as finished + trs.finished = true + + return false, nil, nil +} + +func summarizeTaskResults(taskResults []*tfe.TaskResult) *taskResultSummary { + var pendingCount, errCount, errMandatoryCount, passedCount int + for _, task := range taskResults { + if task.Status == tfe.TaskUnreachable { + return &taskResultSummary{ + unreachable: true, + } + } else if task.Status == tfe.TaskRunning || task.Status == tfe.TaskPending { + pendingCount++ + } else if task.Status == tfe.TaskPassed { + passedCount++ + } else { + // Everything else is a failure + errCount++ + if task.WorkspaceTaskEnforcementLevel == tfe.Mandatory { + errMandatoryCount++ + } + } + } + + return &taskResultSummary{ + unreachable: false, + pending: pendingCount, + failed: errCount, + failedMandatory: errMandatoryCount, + passed: passedCount, + } +} + +func (trs *taskResultSummarizer) runTasksWithTaskResults(output IntegrationOutputWriter, taskResults []*tfe.TaskResult, count *taskResultSummary) { + // Track the first task name that is a mandatory enforcement level breach. + var firstMandatoryTaskFailed *string = nil + + if trs.counter == 0 { + output.Output(fmt.Sprintf("All tasks completed! %d passed, %d failed", count.passed, count.failed)) + } else { + output.OutputElapsed(fmt.Sprintf("All tasks completed! %d passed, %d failed", count.passed, count.failed), 50) + } + + output.Output("") + + for _, t := range taskResults { + capitalizedStatus := string(t.Status) + capitalizedStatus = strings.ToUpper(capitalizedStatus[:1]) + capitalizedStatus[1:] + + status := "[green]" + capitalizedStatus + if t.Status != "passed" { + level := string(t.WorkspaceTaskEnforcementLevel) + level = strings.ToUpper(level[:1]) + level[1:] + status = fmt.Sprintf("[red]%s (%s)", capitalizedStatus, level) + + if t.WorkspaceTaskEnforcementLevel == "mandatory" && firstMandatoryTaskFailed == nil { + firstMandatoryTaskFailed = &t.TaskName + } + } + + title := fmt.Sprintf(`%s ⸺ %s`, t.TaskName, status) + output.SubOutput(title) + + if len(t.Message) > 0 { + output.SubOutput(fmt.Sprintf("[dim]%s", t.Message)) + } + if len(t.URL) > 0 { + output.SubOutput(fmt.Sprintf("[dim]Details: %s", t.URL)) + } + output.SubOutput("") + } + + // If a mandatory enforcement level is breached, return an error. + var overall string = "[green]Passed" + if firstMandatoryTaskFailed != nil { + overall = "[red]Failed" + if count.failedMandatory > 1 { + output.Output(fmt.Sprintf("[reset][bold][red]Error:[reset][bold]the run failed because %d mandatory tasks are required to succeed", count.failedMandatory)) + } else { + output.Output(fmt.Sprintf("[reset][bold][red]Error: [reset][bold]the run failed because the run task, %s, is required to succeed", *firstMandatoryTaskFailed)) + } + } else if count.failed > 0 { // we have failures but none of them mandatory + overall = "[green]Passed with advisory failures" + } + + output.SubOutput("") + output.SubOutput("[bold]Overall Result: " + overall) + + output.End() +} diff --git a/internal/cloud/backend_runTasks_test.go b/internal/cloud/backend_taskStage_taskResults_test.go similarity index 59% rename from internal/cloud/backend_runTasks_test.go rename to internal/cloud/backend_taskStage_taskResults_test.go index 0e0bddc202..244f33de70 100644 --- a/internal/cloud/backend_runTasks_test.go +++ b/internal/cloud/backend_taskStage_taskResults_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -36,7 +39,7 @@ func newMockIntegrationContext(b *Cloud, t *testing.T) (*IntegrationContext, *te ctx := context.Background() // Retrieve the workspace used to run this operation in. - w, err := b.client.Workspaces.Read(ctx, b.organization, b.WorkspaceMapping.Name) + w, err := b.client.Workspaces.Read(ctx, b.Organization, b.WorkspaceMapping.Name) if err != nil { t.Fatalf("error retrieving workspace: %v", err) } @@ -82,16 +85,20 @@ func TestCloud_runTasksWithTaskResults(t *testing.T) { integrationContext, writer := newMockIntegrationContext(b, t) cases := map[string]struct { - taskResults []*tfe.TaskResult + taskStage func() *tfe.TaskStage context *IntegrationContext writer *testIntegrationOutput expectedOutputs []string isError bool }{ "all-succeeded": { - taskResults: []*tfe.TaskResult{ - {ID: "1", TaskName: "Mandatory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "mandatory"}, - {ID: "2", TaskName: "Advisory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "advisory"}, + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.TaskResults = []*tfe.TaskResult{ + {ID: "1", TaskName: "Mandatory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "mandatory"}, + {ID: "2", TaskName: "Advisory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "advisory"}, + } + return ts }, writer: writer, context: integrationContext, @@ -99,9 +106,13 @@ func TestCloud_runTasksWithTaskResults(t *testing.T) { isError: false, }, "mandatory-failed": { - taskResults: []*tfe.TaskResult{ - {ID: "1", TaskName: "Mandatory", Message: "500 Error", Status: "failed", WorkspaceTaskEnforcementLevel: "mandatory"}, - {ID: "2", TaskName: "Advisory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "advisory"}, + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.TaskResults = []*tfe.TaskResult{ + {ID: "1", TaskName: "Mandatory", Message: "500 Error", Status: "failed", WorkspaceTaskEnforcementLevel: "mandatory"}, + {ID: "2", TaskName: "Advisory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "advisory"}, + } + return ts }, writer: writer, context: integrationContext, @@ -109,9 +120,13 @@ func TestCloud_runTasksWithTaskResults(t *testing.T) { isError: true, }, "advisory-failed": { - taskResults: []*tfe.TaskResult{ - {ID: "1", TaskName: "Mandatory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "mandatory"}, - {ID: "2", TaskName: "Advisory", Message: "500 Error", Status: "failed", WorkspaceTaskEnforcementLevel: "advisory"}, + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.TaskResults = []*tfe.TaskResult{ + {ID: "1", TaskName: "Mandatory", Message: "A-OK", Status: "passed", WorkspaceTaskEnforcementLevel: "mandatory"}, + {ID: "2", TaskName: "Advisory", Message: "500 Error", Status: "failed", WorkspaceTaskEnforcementLevel: "advisory"}, + } + return ts }, writer: writer, context: integrationContext, @@ -119,9 +134,13 @@ func TestCloud_runTasksWithTaskResults(t *testing.T) { isError: false, }, "unreachable": { - taskResults: []*tfe.TaskResult{ - {ID: "1", TaskName: "Mandatory", Message: "", Status: "unreachable", WorkspaceTaskEnforcementLevel: "mandatory"}, - {ID: "2", TaskName: "Advisory", Message: "", Status: "unreachable", WorkspaceTaskEnforcementLevel: "advisory"}, + taskStage: func() *tfe.TaskStage { + ts := &tfe.TaskStage{} + ts.TaskResults = []*tfe.TaskResult{ + {ID: "1", TaskName: "Mandatory", Message: "", Status: "unreachable", WorkspaceTaskEnforcementLevel: "mandatory"}, + {ID: "2", TaskName: "Advisory", Message: "", Status: "unreachable", WorkspaceTaskEnforcementLevel: "advisory"}, + } + return ts }, writer: writer, context: integrationContext, @@ -130,27 +149,24 @@ func TestCloud_runTasksWithTaskResults(t *testing.T) { }, } - for caseName, c := range cases { + for _, c := range cases { c.writer.output.Reset() - err := b.runTasksWithTaskResults(c.context, writer, func(b *Cloud, stopCtx context.Context) (*tfe.TaskStage, error) { - return &tfe.TaskStage{ - TaskResults: c.taskResults, - }, nil - }) - - if c.isError && err == nil { - t.Fatalf("Expected %s to be error", caseName) + trs := taskResultSummarizer{ + cloud: b, } - - if !c.isError && err != nil { - t.Errorf("Expected %s to not be error but received %s", caseName, err) - } - - output := c.writer.output.String() - for _, expected := range c.expectedOutputs { - if !strings.Contains(output, expected) { - t.Fatalf("Expected output to contain '%s' but it was:\n\n%s", expected, output) + c.context.Poll(0, 0, func(i int) (bool, error) { + cont, _, _ := trs.Summarize(c.context, c.writer, c.taskStage()) + if cont { + return true, nil } - } + + output := c.writer.output.String() + for _, expected := range c.expectedOutputs { + if !strings.Contains(output, expected) { + t.Fatalf("Expected output to contain '%s' but it was:\n\n%s", expected, output) + } + } + return false, nil + }) } } diff --git a/internal/cloud/backend_taskStages.go b/internal/cloud/backend_taskStages.go new file mode 100644 index 0000000000..8d9298680a --- /dev/null +++ b/internal/cloud/backend_taskStages.go @@ -0,0 +1,207 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "errors" + "fmt" + "strings" + + tfe "github.com/hashicorp/go-tfe" + + "github.com/hashicorp/terraform/internal/terraform" +) + +type taskStages map[tfe.Stage]*tfe.TaskStage + +const ( + taskStageBackoffMin = 4000.0 + taskStageBackoffMax = 12000.0 +) + +const taskStageHeader = ` +To view this run in a browser, visit: +https://%s/app/%s/%s/runs/%s +` + +type taskStageSummarizer interface { + // Summarize takes an IntegrationContext, IntegrationOutputWriter for + // writing output and a pointer to a tfe.TaskStage object as arguments. + // This function summarizes and outputs the results of the task stage. + // It returns a boolean which signifies whether we should continue polling + // for results, an optional message string to print while it is polling + // and an error if any. + Summarize(*IntegrationContext, IntegrationOutputWriter, *tfe.TaskStage) (bool, *string, error) +} + +func (b *Cloud) runTaskStages(ctx context.Context, client *tfe.Client, runId string) (taskStages, error) { + taskStages := make(taskStages, 0) + result, err := client.Runs.ReadWithOptions(ctx, runId, &tfe.RunReadOptions{ + Include: []tfe.RunIncludeOpt{tfe.RunTaskStages}, + }) + if err == nil { + for _, t := range result.TaskStages { + if t != nil { + taskStages[t.Stage] = t + } + } + } else { + // This error would be expected for older versions of TFE that do not allow + // fetching task_stages. + if !strings.HasSuffix(err.Error(), "Invalid include parameter") { + return taskStages, b.generalError("Failed to retrieve run", err) + } + } + + return taskStages, nil +} + +func (b *Cloud) getTaskStageWithAllOptions(ctx *IntegrationContext, stageID string) (*tfe.TaskStage, error) { + options := tfe.TaskStageReadOptions{ + Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults, tfe.PolicyEvaluationsTaskResults}, + } + stage, err := b.client.TaskStages.Read(ctx.StopContext, stageID, &options) + if err != nil { + return nil, b.generalError("Failed to retrieve task stage", err) + } else { + return stage, nil + } +} + +func (b *Cloud) runTaskStage(ctx *IntegrationContext, output IntegrationOutputWriter, stageID string) error { + var errs error + + // Create our summarizers + summarizers := make([]taskStageSummarizer, 0) + ts, err := b.getTaskStageWithAllOptions(ctx, stageID) + if err != nil { + return err + } + + if s := newTaskResultSummarizer(b, ts); s != nil { + summarizers = append(summarizers, s) + } + + if s := newPolicyEvaluationSummarizer(b, ts); s != nil { + summarizers = append(summarizers, s) + } + + return ctx.Poll(taskStageBackoffMin, taskStageBackoffMax, func(i int) (bool, error) { + options := tfe.TaskStageReadOptions{ + Include: []tfe.TaskStageIncludeOpt{tfe.TaskStageTaskResults, tfe.PolicyEvaluationsTaskResults}, + } + stage, err := b.client.TaskStages.Read(ctx.StopContext, stageID, &options) + if err != nil { + return false, b.generalError("Failed to retrieve task stage", err) + } + + switch stage.Status { + case tfe.TaskStagePending: + // Waiting for it to start + return true, nil + case tfe.TaskStageRunning: + if _, e := processSummarizers(ctx, output, stage, summarizers, errs); e != nil { + errs = e + } + // not a terminal status so we continue to poll + return true, nil + // Note: Terminal statuses need to print out one last time just in case + case tfe.TaskStagePassed: + ok, e := processSummarizers(ctx, output, stage, summarizers, errs) + if e != nil { + errs = e + } + if ok { + if b.CLI != nil { + b.CLI.Output("------------------------------------------------------------------------") + } + return true, nil + } + case tfe.TaskStageCanceled, tfe.TaskStageErrored, tfe.TaskStageFailed: + ok, e := processSummarizers(ctx, output, stage, summarizers, errs) + if e != nil { + errs = e + } + if ok { + if b.CLI != nil { + b.CLI.Output("------------------------------------------------------------------------") + } + return true, nil + } + return false, fmt.Errorf("Task Stage %s.", stage.Status) + case tfe.TaskStageAwaitingOverride: + ok, e := processSummarizers(ctx, output, stage, summarizers, errs) + if e != nil { + errs = e + } + if ok { + return true, nil + } + cont, err := b.processStageOverrides(ctx, output, stage.ID) + if err != nil { + errs = errors.Join(errs, err) + } else { + if b.CLI != nil { + b.CLI.Output("------------------------------------------------------------------------") + } + return cont, nil + } + case tfe.TaskStageUnreachable: + return false, nil + default: + return false, fmt.Errorf("Invalid Task stage status: %s ", stage.Status) + } + return false, errs + }) +} + +func processSummarizers(ctx *IntegrationContext, output IntegrationOutputWriter, stage *tfe.TaskStage, summarizers []taskStageSummarizer, errs error) (bool, error) { + for _, s := range summarizers { + cont, msg, err := s.Summarize(ctx, output, stage) + if err != nil { + errs = errors.Join(errs, err) + break + } + + if !cont { + continue + } + + // cont is true and we must continue to poll + if msg != nil { + output.OutputElapsed(*msg, len(*msg)) // Up to 2 digits are allowed by the max message allocation + } + return true, nil + } + return false, errs +} + +func (b *Cloud) processStageOverrides(context *IntegrationContext, output IntegrationOutputWriter, taskStageID string) (bool, error) { + if b.CLI != nil { + b.CLI.Output("--------------------------------\n") + b.CLI.Output(b.Colorize().Color(fmt.Sprintf("%c%c [bold]Override", Arrow, Arrow))) + } + opts := &terraform.InputOpts{ + Id: fmt.Sprintf("%c%c [bold]Override", Arrow, Arrow), + Query: "\nDo you want to override the failed policies?", + Description: "Only 'override' will be accepted to override.", + } + runURL := fmt.Sprintf(taskStageHeader, b.Hostname, b.Organization, context.Op.Workspace, context.Run.ID) + err := b.confirm(context.StopContext, context.Op, opts, context.Run, "override") + if err != nil && err != errRunOverridden { + return false, fmt.Errorf("Failed to override: %w\n%s\n", err, runURL) + } + + if err != errRunOverridden { + if _, err = b.client.TaskStages.Override(context.StopContext, taskStageID, tfe.TaskStageOverrideOptions{}); err != nil { + return false, b.generalError(fmt.Sprintf("Failed to override policy check.\n%s", runURL), err) + } else { + return true, nil + } + } else { + output.Output(fmt.Sprintf("The run needs to be manually overridden or discarded.\n%s\n", runURL)) + } + return false, nil +} diff --git a/internal/cloud/backend_taskStages_test.go b/internal/cloud/backend_taskStages_test.go new file mode 100644 index 0000000000..903db2b894 --- /dev/null +++ b/internal/cloud/backend_taskStages_test.go @@ -0,0 +1,278 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "errors" + "strings" + "testing" + + "github.com/hashicorp/go-tfe" + tfemocks "github.com/hashicorp/go-tfe/mocks" + "go.uber.org/mock/gomock" +) + +func MockAllTaskStages(t *testing.T, client *tfe.Client) (RunID string) { + ctrl := gomock.NewController(t) + + RunID = "run-all_task_stages" + + mockRunsAPI := tfemocks.NewMockRuns(ctrl) + + goodRun := tfe.Run{ + TaskStages: []*tfe.TaskStage{ + { + Stage: tfe.PrePlan, + }, + { + Stage: tfe.PostPlan, + }, + { + Stage: tfe.PreApply, + }, + }, + } + mockRunsAPI. + EXPECT(). + ReadWithOptions(gomock.Any(), RunID, gomock.Any()). + Return(&goodRun, nil). + AnyTimes() + + // Mock a bad Read response + mockRunsAPI. + EXPECT(). + ReadWithOptions(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, tfe.ErrInvalidOrg). + AnyTimes() + + // Wire up the mock interfaces + client.Runs = mockRunsAPI + return +} + +func MockPrePlanTaskStage(t *testing.T, client *tfe.Client) (RunID string) { + ctrl := gomock.NewController(t) + + RunID = "run-pre_plan_task_stage" + + mockRunsAPI := tfemocks.NewMockRuns(ctrl) + + goodRun := tfe.Run{ + TaskStages: []*tfe.TaskStage{ + { + Stage: tfe.PrePlan, + }, + }, + } + mockRunsAPI. + EXPECT(). + ReadWithOptions(gomock.Any(), RunID, gomock.Any()). + Return(&goodRun, nil). + AnyTimes() + + // Mock a bad Read response + mockRunsAPI. + EXPECT(). + ReadWithOptions(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, tfe.ErrInvalidOrg). + AnyTimes() + + // Wire up the mock interfaces + client.Runs = mockRunsAPI + return +} + +func MockTaskStageUnsupported(t *testing.T, client *tfe.Client) (RunID string) { + ctrl := gomock.NewController(t) + + RunID = "run-unsupported_task_stage" + + mockRunsAPI := tfemocks.NewMockRuns(ctrl) + + mockRunsAPI. + EXPECT(). + ReadWithOptions(gomock.Any(), RunID, gomock.Any()). + Return(nil, errors.New("Invalid include parameter")). + AnyTimes() + + mockRunsAPI. + EXPECT(). + ReadWithOptions(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, tfe.ErrInvalidOrg). + AnyTimes() + + client.Runs = mockRunsAPI + return +} + +func TestTaskStagesWithAllStages(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + runID := MockAllTaskStages(t, client) + + ctx := context.TODO() + taskStages, err := b.runTaskStages(ctx, client, runID) + + if err != nil { + t.Fatalf("Expected to not error but received %s", err) + } + + for _, stageName := range []tfe.Stage{ + tfe.PrePlan, + tfe.PostPlan, + tfe.PreApply, + } { + if stage, ok := taskStages[stageName]; ok { + if stage.Stage != stageName { + t.Errorf("Expected task stage indexed by %s to find a Task Stage with the same index, but receieved %s", stageName, stage.Stage) + } + } else { + t.Errorf("Expected task stage indexed by %s to exist, but it did not", stageName) + } + } +} + +func TestTaskStagesWithOneStage(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + runID := MockPrePlanTaskStage(t, client) + + ctx := context.TODO() + taskStages, err := b.runTaskStages(ctx, client, runID) + + if err != nil { + t.Fatalf("Expected to not error but received %s", err) + } + + if _, ok := taskStages[tfe.PrePlan]; !ok { + t.Errorf("Expected task stage indexed by %s to exist, but it did not", tfe.PrePlan) + } + + for _, stageName := range []tfe.Stage{ + tfe.PostPlan, + tfe.PreApply, + } { + if _, ok := taskStages[stageName]; ok { + t.Errorf("Expected task stage indexed by %s to not exist, but it did", stageName) + } + } +} + +func TestTaskStagesWithOldTFC(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + runID := MockTaskStageUnsupported(t, client) + + ctx := context.TODO() + taskStages, err := b.runTaskStages(ctx, client, runID) + + if err != nil { + t.Fatalf("Expected to not error but received %s", err) + } + + if len(taskStages) != 0 { + t.Errorf("Expected task stage to be empty, but found %d stages", len(taskStages)) + } +} + +func TestTaskStagesWithErrors(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + config := &tfe.Config{ + Token: "not-a-token", + } + client, _ := tfe.NewClient(config) + MockTaskStageUnsupported(t, client) + + ctx := context.TODO() + _, err := b.runTaskStages(ctx, client, "this run ID will not exist is invalid anyway") + + if err == nil { + t.Error("Expected to error but did not") + } +} + +func TestTaskStageOverride(t *testing.T) { + b, bCleanup := testBackendWithName(t) + defer bCleanup() + + integrationContext, writer := newMockIntegrationContext(b, t) + + integrationContext.Op.UIOut = b.CLI + + cases := map[string]struct { + taskStageID string + isError bool + errMsg string + input *mockInput + cont bool + }{ + "override-pass": { + taskStageID: "ts-pass", + isError: false, + input: testInput(t, map[string]string{ + "→→ [bold]Override": "override", + }), + errMsg: "", + cont: true, + }, + "override-fail": { + taskStageID: "ts-err", + isError: true, + input: testInput(t, map[string]string{ + "→→ [bold]Override": "override", + }), + errMsg: "", + cont: false, + }, + "skip-override": { + taskStageID: "ts-err", + isError: true, + errMsg: "Failed to override: Apply discarded.", + input: testInput(t, map[string]string{ + "→→ [bold]Override": "no", + }), + cont: false, + }, + } + for _, c := range cases { + integrationContext.Op.UIIn = c.input + cont, err := b.processStageOverrides(integrationContext, writer, c.taskStageID) + if c.isError { + if err == nil { + t.Fatalf("Expected to fail with some error") + } + if c.errMsg != "" { + if !strings.Contains(err.Error(), c.errMsg) { + t.Fatalf("Expected: %s, got: %s", c.errMsg, err.Error()) + } + } + + } else { + if err != nil { + t.Fatalf("Expected to pass, got err: %s", err) + } + } + if c.cont != cont { + t.Fatalf("expected polling continue: %t, got: %t", c.cont, cont) + } + } +} diff --git a/internal/cloud/backend_test.go b/internal/cloud/backend_test.go index 181bd6e17a..1b7e37342c 100644 --- a/internal/cloud/backend_test.go +++ b/internal/cloud/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -5,22 +8,25 @@ import ( "fmt" "net/http" "os" + "reflect" + "sort" "strings" "testing" tfe "github.com/hashicorp/go-tfe" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/tfdiags" - tfversion "github.com/hashicorp/terraform/version" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" backendLocal "github.com/hashicorp/terraform/internal/backend/local" + "github.com/hashicorp/terraform/internal/tfdiags" + tfversion "github.com/hashicorp/terraform/version" ) func TestCloud(t *testing.T) { - var _ backend.Enhanced = New(nil) - var _ backend.CLI = New(nil) + var _ backendrun.OperationsBackend = New(nil) + var _ backendrun.CLI = New(nil) } func TestCloud_backendWithName(t *testing.T) { @@ -40,11 +46,11 @@ func TestCloud_backendWithName(t *testing.T) { t.Fatalf("expected fetching a state which is NOT the single configured workspace to have an ErrWorkspacesNotSupported error, but got: %v", err) } - if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported { + if err := b.DeleteWorkspace(testBackendSingleWorkspaceName, true); err != backend.ErrWorkspacesNotSupported { t.Fatalf("expected deleting the single configured workspace name to result in an error, but got: %v", err) } - if err := b.DeleteWorkspace("foo"); err != backend.ErrWorkspacesNotSupported { + if err := b.DeleteWorkspace("foo", true); err != backend.ErrWorkspacesNotSupported { t.Fatalf("expected deleting a workspace which is NOT the configured workspace name to result in an error, but got: %v", err) } } @@ -73,49 +79,159 @@ func TestCloud_backendWithTags(t *testing.T) { } } +func TestCloud_backendWithKVTags(t *testing.T) { + b, bCleanup := testBackendWithKVTags(t) + defer bCleanup() + + _, err := b.client.Workspaces.Create(context.Background(), "hashicorp", tfe.WorkspaceCreateOptions{ + Name: tfe.String("ws-billing-101"), + TagBindings: []*tfe.TagBinding{ + {Key: "dept", Value: "billing"}, + {Key: "costcenter", Value: "101"}, + }, + }) + if err != nil { + t.Fatalf("error creating workspace: %s", err) + } + + workspaces, err := b.Workspaces() + if err != nil { + t.Fatalf("error: %s", err) + } + + actual := len(workspaces) + if actual != 1 { + t.Fatalf("expected 1 workspaces, got %d", actual) + } + + if workspaces[0] != "ws-billing-101" { + t.Fatalf("expected workspace name to be 'ws-billing-101', got %s", workspaces[0]) + } +} + +func TestCloud_DescribeTags(t *testing.T) { + cases := map[string]struct { + expected string + expectedOr []string + config cty.Value + }{ + "one set tag": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("org"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.SetVal([]cty.Value{ + cty.StringVal("billing"), + }), + "project": cty.NullVal(cty.String), + }), + }), + expected: "billing", + }, + "two set tags": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("org"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.SetVal([]cty.Value{ + cty.StringVal("billing"), + cty.StringVal("cc101"), + }), + "project": cty.NullVal(cty.String), + }), + }), + expected: "billing, cc101", + }, + "one kv tag": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("org"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.MapVal(map[string]cty.Value{ + "dept": cty.StringVal("billing"), + }), + "project": cty.NullVal(cty.String), + }), + }), + expected: "dept=billing", + }, + "two kv tags": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("org"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.MapVal(map[string]cty.Value{ + "dept": cty.StringVal("billing"), + "costcenter": cty.StringVal("101"), + }), + "project": cty.NullVal(cty.String), + }), + }), + expectedOr: []string{"costcenter=101, dept=billing", "dept=billing, costcenter=101"}, + }, + "no tags": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("org"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("default"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }), + expected: "", + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + b, cleanup := testUnconfiguredBackend(t) + t.Cleanup(cleanup) + + _, valDiags := b.PrepareConfig(tc.config) + if valDiags.Err() != nil { + t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) + } + + diags := b.Configure(tc.config) + if diags.Err() != nil { + t.Fatalf("%s: unexpected configure result: %v", name, diags.Err()) + } + + actual := b.WorkspaceMapping.DescribeTags() + if tc.expectedOr != nil { + for _, expected := range tc.expectedOr { + if actual == expected { + return + } + } + t.Fatalf("expected one of %v, got %q", tc.expectedOr, actual) + } + + if actual != tc.expected { + t.Fatalf("expected %q, got %q", tc.expected, actual) + } + }) + } +} + +// Test b.PrepareConfig, which actually does very little; most real validation +// happens later in resolveCloudConfig. func TestCloud_PrepareConfig(t *testing.T) { cases := map[string]struct { config cty.Value expectedErr string }{ - "null organization": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.NullVal(cty.String), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), - }), - }), - expectedErr: `Invalid or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`, - }, - "null workspace": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("org"), - "workspaces": cty.NullVal(cty.String), - }), - expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`, - }, - "workspace: empty tags, name": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("org"), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.NullVal(cty.String), - "tags": cty.NullVal(cty.Set(cty.String)), - }), - }), - expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`, - }, - "workspace: name present": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("org"), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), - }), - }), - expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`, - }, - "workspace: name and tags present": { + "workspace: name and tags conflict": { config: cty.ObjectVal(map[string]cty.Value{ "organization": cty.StringVal("org"), "workspaces": cty.ObjectVal(map[string]cty.Value{ @@ -125,10 +241,23 @@ func TestCloud_PrepareConfig(t *testing.T) { cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }), expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`, }, + "totally empty config block is ok": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.NullVal(cty.String), + "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "tags": cty.Set(cty.String), + "project": cty.String, + })), + }), + }, } for name, tc := range cases { @@ -137,87 +266,21 @@ func TestCloud_PrepareConfig(t *testing.T) { // Validate _, valDiags := b.PrepareConfig(tc.config) - if valDiags.Err() != nil && tc.expectedErr != "" { - actualErr := valDiags.Err().Error() - if !strings.Contains(actualErr, tc.expectedErr) { - t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) - } - } - } -} -func TestCloud_PrepareConfigWithEnvVars(t *testing.T) { - cases := map[string]struct { - config cty.Value - vars map[string]string - expectedErr string - }{ - "with no organization": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.NullVal(cty.String), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), - }), - }), - vars: map[string]string{ - "TF_CLOUD_ORGANIZATION": "example-org", - }, - }, - "with no organization attribute or env var": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.NullVal(cty.String), - "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), - }), - }), - vars: map[string]string{}, - expectedErr: `Invalid or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`, - }, - "null workspace": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("hashicorp"), - "workspaces": cty.NullVal(cty.String), - }), - vars: map[string]string{ - "TF_WORKSPACE": "my-workspace", - }, - }, - "organization and workspace env var": { - config: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.NullVal(cty.String), - "workspaces": cty.NullVal(cty.String), - }), - vars: map[string]string{ - "TF_CLOUD_ORGANIZATION": "hashicorp", - "TF_WORKSPACE": "my-workspace", - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - s := testServer(t) - b := New(testDisco(s)) - - for k, v := range tc.vars { - os.Setenv(k, v) - } - t.Cleanup(func() { - for k := range tc.vars { - os.Unsetenv(k) - } - }) - - _, valDiags := b.PrepareConfig(tc.config) - if valDiags.Err() != nil && tc.expectedErr != "" { + if tc.expectedErr != "" { + if valDiags.Err() == nil { + t.Fatalf("%s: expected validation error %q but didn't get one", name, tc.expectedErr) + } else { actualErr := valDiags.Err().Error() if !strings.Contains(actualErr, tc.expectedErr) { - t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) + t.Fatalf("%s: expected validation error %q but instead got %q", name, tc.expectedErr, actualErr) } } - }) + } else { + if valDiags.Err() != nil { + t.Fatalf("%s: expected validation to pass but got error %q", name, valDiags.Err().Error()) + } + } } } @@ -229,6 +292,7 @@ func TestCloud_configWithEnvVars(t *testing.T) { expectedOrganization string expectedHostname string expectedWorkspaceName string + expectedProjectName string expectedErr string }{ "with no organization specified": { @@ -237,8 +301,9 @@ func TestCloud_configWithEnvVars(t *testing.T) { "token": cty.NullVal(cty.String), "organization": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ @@ -252,8 +317,9 @@ func TestCloud_configWithEnvVars(t *testing.T) { "token": cty.NullVal(cty.String), "organization": cty.StringVal("hashicorp"), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ @@ -267,8 +333,9 @@ func TestCloud_configWithEnvVars(t *testing.T) { "token": cty.NullVal(cty.String), "organization": cty.StringVal("hashicorp"), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ @@ -282,8 +349,9 @@ func TestCloud_configWithEnvVars(t *testing.T) { "token": cty.NullVal(cty.String), "organization": cty.StringVal("hashicorp"), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ @@ -297,8 +365,9 @@ func TestCloud_configWithEnvVars(t *testing.T) { "token": cty.NullVal(cty.String), "organization": cty.StringVal("hashicorp"), "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ - "name": cty.String, - "tags": cty.Set(cty.String), + "name": cty.String, + "tags": cty.Set(cty.String), + "project": cty.String, })), }), vars: map[string]string{ @@ -312,14 +381,15 @@ func TestCloud_configWithEnvVars(t *testing.T) { "token": cty.NullVal(cty.String), "organization": cty.StringVal("mordor"), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("mt-doom"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("mt-doom"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ "TF_WORKSPACE": "shire", }, - expectedWorkspaceName: "mt-doom", + expectedErr: `conflicts with TF_WORKSPACE environment variable`, }, "env var workspace does not have specified tag": { setup: func(b *Cloud) { @@ -340,6 +410,7 @@ func TestCloud_configWithEnvVars(t *testing.T) { "tags": cty.SetVal([]cty.Value{ cty.StringVal("cloud"), }), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ @@ -371,6 +442,7 @@ func TestCloud_configWithEnvVars(t *testing.T) { "tags": cty.SetVal([]cty.Value{ cty.StringVal("hobbity"), }), + "project": cty.NullVal(cty.String), }), }), vars: map[string]string{ @@ -378,6 +450,86 @@ func TestCloud_configWithEnvVars(t *testing.T) { }, expectedWorkspaceName: "", // No error is raised, but workspace is not set }, + "project specified": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("mordor"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("mt-doom"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.StringVal("my-project"), + }), + }), + expectedWorkspaceName: "mt-doom", + expectedProjectName: "my-project", + }, + "project env var specified": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("mordor"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("mt-doom"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }), + vars: map[string]string{ + "TF_CLOUD_PROJECT": "other-project", + }, + expectedWorkspaceName: "mt-doom", + expectedProjectName: "other-project", + }, + "project and env var specified": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("mordor"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("mt-doom"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.StringVal("my-project"), + }), + }), + vars: map[string]string{ + "TF_CLOUD_PROJECT": "other-project", + }, + expectedWorkspaceName: "mt-doom", + expectedProjectName: "my-project", + }, + "workspace exists but in different project": { + setup: func(b *Cloud) { + b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("mordor"), + }) + + project, _ := b.client.Projects.Create(context.Background(), "mordor", tfe.ProjectCreateOptions{ + Name: "another-project", + }) + + b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{ + Name: tfe.String("shire"), + Project: project, + }) + }, + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("mordor"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.SetVal([]cty.Value{ + cty.StringVal("hobbity"), + }), + "project": cty.StringVal("my-project"), + }), + }), + expectedProjectName: "my-project", + // No error is raised, and workspace is still in its original + // project, but the configured project for any future workspaces + // created with `terraform workspace new` is unaffected. + }, "with everything set as env vars": { config: cty.ObjectVal(map[string]cty.Value{ "hostname": cty.NullVal(cty.String), @@ -389,10 +541,12 @@ func TestCloud_configWithEnvVars(t *testing.T) { "TF_CLOUD_ORGANIZATION": "mordor", "TF_WORKSPACE": "mt-doom", "TF_CLOUD_HOSTNAME": "mycool.tfe-host.io", + "TF_CLOUD_PROJECT": "my-project", }, expectedOrganization: "mordor", expectedWorkspaceName: "mt-doom", expectedHostname: "mycool.tfe-host.io", + expectedProjectName: "my-project", }, } @@ -426,17 +580,21 @@ func TestCloud_configWithEnvVars(t *testing.T) { t.Fatalf("%s: unexpected configure result: %v", name, diags.Err()) } - if tc.expectedOrganization != "" && tc.expectedOrganization != b.organization { - t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.organization, tc.expectedOrganization) + if tc.expectedOrganization != "" && tc.expectedOrganization != b.Organization { + t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.Organization, tc.expectedOrganization) } - if tc.expectedHostname != "" && tc.expectedHostname != b.hostname { - t.Fatalf("%s: hostname not valid: %s, expected: %s", name, b.hostname, tc.expectedHostname) + if tc.expectedHostname != "" && tc.expectedHostname != b.Hostname { + t.Fatalf("%s: hostname not valid: %s, expected: %s", name, b.Hostname, tc.expectedHostname) } if tc.expectedWorkspaceName != "" && tc.expectedWorkspaceName != b.WorkspaceMapping.Name { t.Fatalf("%s: workspace name not valid: %s, expected: %s", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName) } + + if tc.expectedProjectName != "" && tc.expectedProjectName != b.WorkspaceMapping.Project { + t.Fatalf("%s: project name not valid: %s, expected: %s", name, b.WorkspaceMapping.Project, tc.expectedProjectName) + } }) } } @@ -447,17 +605,18 @@ func TestCloud_config(t *testing.T) { confErr string valErr string }{ - "with_an_unknown_host": { + "with_a_non_tfe_host": { config: cty.ObjectVal(map[string]cty.Value{ - "hostname": cty.StringVal("nonexisting.local"), + "hostname": cty.StringVal("nontfe.local"), "organization": cty.StringVal("hashicorp"), "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), - confErr: "Failed to request discovery document", + confErr: "Host nontfe.local does not provide a tfe service", }, // localhost advertises TFE services, but has no token in the credentials "without_a_token": { @@ -466,8 +625,9 @@ func TestCloud_config(t *testing.T) { "organization": cty.StringVal("hashicorp"), "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), confErr: "terraform login localhost", @@ -484,17 +644,33 @@ func TestCloud_config(t *testing.T) { cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }), }, + "with_kv_tags": { + config: cty.ObjectVal((map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.MapVal(map[string]cty.Value{ + "dept": cty.StringVal("billing"), + }), + "project": cty.NullVal(cty.String), + }), + })), + }, "with_a_name": { config: cty.ObjectVal(map[string]cty.Value{ "hostname": cty.NullVal(cty.String), "organization": cty.StringVal("hashicorp"), "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), }, @@ -504,11 +680,12 @@ func TestCloud_config(t *testing.T) { "organization": cty.StringVal("hashicorp"), "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.NullVal(cty.String), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.NullVal(cty.String), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), - valErr: `Missing workspace mapping strategy.`, + confErr: `Missing workspace mapping strategy.`, }, "with_both_a_name_and_tags": { config: cty.ObjectVal(map[string]cty.Value{ @@ -522,32 +699,76 @@ func TestCloud_config(t *testing.T) { cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }), valErr: `Only one of workspace "tags" or "name" is allowed.`, }, + "invalid tags dynamic type": { + config: cty.ObjectVal((map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.StringVal("dept:billing"), + "project": cty.NullVal(cty.String), + }), + })), + confErr: `tags must be a set or object, not string`, + }, + "invalid tags dynamic type, object with non-string": { + config: cty.ObjectVal((map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.MapVal(map[string]cty.Value{ + "dept": cty.NumberIntVal(1), + }), + "project": cty.NullVal(cty.String), + }), + })), + confErr: `tag object values must be strings`, + }, + "invalid tags dynamic type, tuple non-string": { + config: cty.ObjectVal((map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.TupleVal([]cty.Value{cty.NumberIntVal(1)}), + "project": cty.NullVal(cty.String), + }), + })), + confErr: `tag elements must be strings`, + }, "null config": { config: cty.NullVal(cty.EmptyObject), }, } for name, tc := range cases { - b, cleanup := testUnconfiguredBackend(t) - t.Cleanup(cleanup) + t.Run(name, func(t *testing.T) { + b, cleanup := testUnconfiguredBackend(t) + t.Cleanup(cleanup) - // Validate - _, valDiags := b.PrepareConfig(tc.config) - if (valDiags.Err() != nil || tc.valErr != "") && - (valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) { - t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) - } + // Validate + _, valDiags := b.PrepareConfig(tc.config) + if (valDiags.Err() != nil || tc.valErr != "") && + (valDiags.Err() == nil || !strings.Contains(valDiags.Err().Error(), tc.valErr)) { + t.Fatalf("unexpected validation result: %v", valDiags.Err()) + } - // Configure - confDiags := b.Configure(tc.config) - if (confDiags.Err() != nil || tc.confErr != "") && - (confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) { - t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err()) - } + // Configure + confDiags := b.Configure(tc.config) + if (confDiags.Err() != nil || tc.confErr != "") && + (confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) { + t.Fatalf("unexpected configure result: %v", confDiags.Err()) + } + }) } } @@ -563,6 +784,7 @@ func TestCloud_configVerifyMinimumTFEVersion(t *testing.T) { cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }) @@ -599,6 +821,7 @@ func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) { cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }) @@ -618,7 +841,7 @@ func TestCloud_configVerifyMinimumTFEVersionInAutomation(t *testing.T) { t.Fatalf("expected configure to error") } - expected := `This version of Terraform Cloud/Enterprise does not support the state mechanism + expected := `This version of HCP Terraform does not support the state mechanism attempting to be used by the platform. This should never happen.` if !strings.Contains(confDiags.Err().Error(), expected) { t.Fatalf("expected configure to error with %q, got %q", expected, confDiags.Err().Error()) @@ -627,7 +850,7 @@ attempting to be used by the platform. This should never happen.` func TestCloud_setUnavailableTerraformVersion(t *testing.T) { // go-tfe returns an error IRL if you try to set a Terraform version that's - // not available in your TFC instance. To test this, tfe_client_mock errors if + // not available in your HCP Terraform instance. To test this, tfe_client_mock errors if // you try to set any Terraform version for this specific workspace name. workspaceName := "unavailable-terraform-version" @@ -642,19 +865,20 @@ func TestCloud_setUnavailableTerraformVersion(t *testing.T) { cty.StringVal("sometag"), }, ), + "project": cty.NullVal(cty.String), }), }) - b, bCleanup := testBackend(t, config) + b, _, bCleanup := testBackend(t, config, nil) defer bCleanup() // Make sure the workspace doesn't exist yet -- otherwise, we can't test what // happens when a workspace gets created. This is why we can't use "name" in // the backend config above, btw: if you do, testBackend() creates the default // workspace before we get a chance to do anything. - _, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) + _, err := b.client.Workspaces.Read(context.Background(), b.Organization, workspaceName) if err != tfe.ErrResourceNotFound { - t.Fatalf("the workspace we were about to try and create (%s/%s) already exists in the mocks somehow, so this test isn't trustworthy anymore", b.organization, workspaceName) + t.Fatalf("the workspace we were about to try and create (%s/%s) already exists in the mocks somehow, so this test isn't trustworthy anymore", b.Organization, workspaceName) } _, err = b.StateMgr(workspaceName) @@ -662,7 +886,7 @@ func TestCloud_setUnavailableTerraformVersion(t *testing.T) { t.Fatalf("expected no error from StateMgr, despite not being able to set remote Terraform version: %#v", err) } // Make sure the workspace was created: - workspace, err := b.client.Workspaces.Read(context.Background(), b.organization, workspaceName) + workspace, err := b.client.Workspaces.Read(context.Background(), b.Organization, workspaceName) if err != nil { t.Fatalf("b.StateMgr() didn't actually create the desired workspace") } @@ -677,159 +901,398 @@ func TestCloud_setUnavailableTerraformVersion(t *testing.T) { } } -func TestCloud_setConfigurationFields(t *testing.T) { - originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND") - +func TestCloud_resolveCloudConfig(t *testing.T) { cases := map[string]struct { - obj cty.Value - expectedHostname string - expectedOrganziation string - expectedWorkspaceName string - expectedWorkspaceTags []string - expectedForceLocal bool - setEnv func() - resetEnv func() - expectedErr string + config cty.Value + vars map[string]string + expectedResult cloudConfig + expectedErr string }{ - "with hostname set": { - obj: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("hashicorp"), - "hostname": cty.StringVal("hashicorp.com"), + "hostname/org/name/token/project all set": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.StringVal("app.staging.terraform.io"), + "organization": cty.StringVal("examplecorp"), + "token": cty.StringVal("password123"), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.StringVal("networking"), }), }), - expectedHostname: "hashicorp.com", - expectedOrganziation: "hashicorp", + expectedResult: cloudConfig{ + hostname: "app.staging.terraform.io", + organization: "examplecorp", + token: "password123", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + Project: "networking", + }, + }, }, - "with hostname not set, set to default hostname": { - obj: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("hashicorp"), + "totally empty config block": { + config: cty.ObjectVal(map[string]cty.Value{ "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.NullVal(cty.String), + "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "tags": cty.Set(cty.String), + "project": cty.String, + })), + }), + vars: map[string]string{ + "TF_CLOUD_HOSTNAME": "app.staging.terraform.io", + "TF_CLOUD_ORGANIZATION": "examplecorp", + "TF_WORKSPACE": "prod", + "TF_CLOUD_PROJECT": "networking", + }, + expectedResult: cloudConfig{ + hostname: "app.staging.terraform.io", + organization: "examplecorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + Project: "networking", + }, + }, + }, + "null organization": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.NullVal(cty.String), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), - expectedHostname: defaultHostname, - expectedOrganziation: "hashicorp", + expectedErr: `Invalid or missing required argument: "organization" must be set in the cloud configuration or as an environment variable: TF_CLOUD_ORGANIZATION.`, }, - "with workspace name set": { - obj: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("hashicorp"), - "hostname": cty.StringVal("hashicorp.com"), + "null workspace": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("org"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "tags": cty.Set(cty.String), + "project": cty.String, + })), + }), + expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`, + }, + "workspace: empty tags, name": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("org"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("prod"), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.NullVal(cty.String), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }), - expectedHostname: "hashicorp.com", - expectedOrganziation: "hashicorp", - expectedWorkspaceName: "prod", + expectedErr: `Invalid workspaces configuration: Missing workspace mapping strategy. Either workspace "tags" or "name" is required.`, }, - "with workspace tags set": { - obj: cty.ObjectVal(map[string]cty.Value{ - "organization": cty.StringVal("hashicorp"), - "hostname": cty.StringVal("hashicorp.com"), + "workspace: name and tags present": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("org"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.NullVal(cty.String), + "name": cty.StringVal("prod"), "tags": cty.SetVal( []cty.Value{ cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }), - expectedHostname: "hashicorp.com", - expectedOrganziation: "hashicorp", - expectedWorkspaceTags: []string{"billing"}, + expectedErr: `Invalid workspaces configuration: Only one of workspace "tags" or "name" is allowed.`, }, - "with force local set": { - obj: cty.ObjectVal(map[string]cty.Value{ + "organization from environment": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.NullVal(cty.String), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }), + vars: map[string]string{ + "TF_CLOUD_ORGANIZATION": "example-org", + }, + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "example-org", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + }, + }, + }, + "null workspace, TF_WORKSPACE present": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("hashicorp"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "tags": cty.Set(cty.String), + "project": cty.String, + })), + }), + vars: map[string]string{ + "TF_WORKSPACE": "my-workspace", + }, + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "hashicorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "my-workspace", + }, + }, + }, + "organization and workspace and project env var": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.NullVal(cty.String), + "workspaces": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "tags": cty.Set(cty.String), + "project": cty.String, + })), + }), + vars: map[string]string{ + "TF_CLOUD_ORGANIZATION": "hashicorp", + "TF_WORKSPACE": "my-workspace", + "TF_CLOUD_PROJECT": "example-project", + }, + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "hashicorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "my-workspace", + Project: "example-project", + }, + }, + }, + "with no project": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("examplecorp"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }), + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "examplecorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + }, + }, + }, + "with project from environment": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("examplecorp"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }), + vars: map[string]string{ + "TF_CLOUD_PROJECT": "example-project", + }, + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "examplecorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + Project: "example-project", + }, + }, + }, + "with both project env var and config value": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("examplecorp"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.StringVal("config-project"), + }), + }), + vars: map[string]string{ + "TF_CLOUD_PROJECT": "env-project", + }, + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "examplecorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + Project: "config-project", // config wins + }, + }, + }, + "with hostname not set, set to default hostname": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("hashicorp"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }), + expectedResult: cloudConfig{ + hostname: defaultHostname, + organization: "hashicorp", + token: "", + workspaceMapping: WorkspaceMapping{ + Name: "prod", + }, + }, + }, + "with workspace tags set": { + config: cty.ObjectVal(map[string]cty.Value{ "organization": cty.StringVal("hashicorp"), "hostname": cty.StringVal("hashicorp.com"), + "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ "name": cty.NullVal(cty.String), - "tags": cty.NullVal(cty.Set(cty.String)), + "tags": cty.SetVal( + []cty.Value{ + cty.StringVal("billing"), + cty.StringVal("applications"), + }, + ), + "project": cty.NullVal(cty.String), }), }), - expectedHostname: "hashicorp.com", - expectedOrganziation: "hashicorp", - setEnv: func() { - os.Setenv("TF_FORCE_LOCAL_BACKEND", "1") + expectedResult: cloudConfig{ + hostname: "hashicorp.com", + organization: "hashicorp", + token: "", + workspaceMapping: WorkspaceMapping{ + TagsAsSet: []string{"billing", "applications"}, + }, }, - resetEnv: func() { - os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv) + }, + "with kv tags set": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("hashicorp"), + "hostname": cty.StringVal("hashicorp.com"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.MapVal(map[string]cty.Value{ + "dept": cty.StringVal("billing"), + }), + "project": cty.NullVal(cty.String), + }), + }), + expectedResult: cloudConfig{ + hostname: "hashicorp.com", + organization: "hashicorp", + token: "", + workspaceMapping: WorkspaceMapping{ + TagsAsMap: map[string]string{"dept": "billing"}, + }, }, - expectedForceLocal: true, }, } for name, tc := range cases { - b := &Cloud{} - - // if `setEnv` is set, then we expect `resetEnv` to also be set - if tc.setEnv != nil { - tc.setEnv() - defer tc.resetEnv() - } - - errDiags := b.setConfigurationFields(tc.obj) - if errDiags.HasErrors() || tc.expectedErr != "" { - actualErr := errDiags.Err().Error() - if !strings.Contains(actualErr, tc.expectedErr) { - t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err()) + t.Run(name, func(t *testing.T) { + for k, v := range tc.vars { + os.Setenv(k, v) } - } + t.Cleanup(func() { + for k := range tc.vars { + os.Unsetenv(k) + } + }) - if tc.expectedHostname != "" && b.hostname != tc.expectedHostname { - t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname) - } - if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation { - t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation) - } - if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName { - t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName) - } - if len(tc.expectedWorkspaceTags) > 0 { - presentSet := make(map[string]struct{}) - for _, tag := range b.WorkspaceMapping.Tags { - presentSet[tag] = struct{}{} - } + result, diags := resolveCloudConfig(tc.config) - expectedSet := make(map[string]struct{}) - for _, tag := range tc.expectedWorkspaceTags { - expectedSet[tag] = struct{}{} - } + // Validate either expected error or result, not both + if tc.expectedErr != "" { + if diags.Err() == nil { + t.Fatalf("%s: expected validation error %q but didn't get one", name, tc.expectedErr) + } else { + actualErr := diags.Err().Error() + if !strings.Contains(actualErr, tc.expectedErr) { + t.Fatalf("%s: expected validation error %q but instead got %q", name, tc.expectedErr, actualErr) + } + } + } else { // check result + // Sort tags first to avoid flaking, since the slice order from + // a cty.Set isn't guaranteed: + sort.Strings(tc.expectedResult.workspaceMapping.TagsAsSet) + sort.Strings(result.workspaceMapping.TagsAsSet) + if !reflect.DeepEqual(tc.expectedResult, result) { + t.Fatalf("%s: expected final config of %#v but instead got %#v", name, tc.expectedResult, result) + } - var missing []string - var unexpected []string - - for _, expected := range tc.expectedWorkspaceTags { - if _, ok := presentSet[expected]; !ok { - missing = append(missing, expected) + if !reflect.DeepEqual(tc.expectedResult.workspaceMapping.asTFETagBindings(), result.workspaceMapping.asTFETagBindings()) { + t.Fatalf("%s: expected final config of %#v but instead got %#v", name, tc.expectedResult, result) } } + }) + } +} - for _, actual := range b.WorkspaceMapping.Tags { - if _, ok := expectedSet[actual]; !ok { - unexpected = append(missing, actual) - } - } +func TestCloud_forceLocalBackend(t *testing.T) { + b, cleanup := testUnconfiguredBackend(t) + t.Cleanup(cleanup) - if len(missing) > 0 { - t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing) - } + os.Setenv("TF_FORCE_LOCAL_BACKEND", "true") + t.Cleanup(func() { + os.Unsetenv("TF_FORCE_LOCAL_BACKEND") + }) - if len(unexpected) > 0 { - t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected) - } + obj := cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("hashicorp"), + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }) - } - if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal { - t.Fatalf("%s: expected force local backend to be set ", name) - } + obj, valDiags := b.PrepareConfig(obj) + if valDiags.Err() != nil { + t.Fatalf("unexpected validation result: %v", valDiags.Err()) + } + + diags := b.Configure(obj) + if diags.Err() != nil { + t.Fatalf("unexpected configure result: %v", diags.Err()) + } + + if b.forceLocal != true { + t.Fatalf("expected force local backend to be set due to env var") } } @@ -856,7 +1319,7 @@ func TestCloud_addAndRemoveWorkspacesDefault(t *testing.T) { t.Fatalf("expected no error, got %v", err) } - if err := b.DeleteWorkspace(testBackendSingleWorkspaceName); err != backend.ErrWorkspacesNotSupported { + if err := b.DeleteWorkspace(testBackendSingleWorkspaceName, true); err != backend.ErrWorkspacesNotSupported { t.Fatalf("expected error %v, got %v", backend.ErrWorkspacesNotSupported, err) } } @@ -889,7 +1352,7 @@ func TestCloud_StateMgr_versionCheck(t *testing.T) { // Terraform version if _, err := b.client.Workspaces.Update( context.Background(), - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ TerraformVersion: tfe.String(v0140.String()), @@ -906,7 +1369,7 @@ func TestCloud_StateMgr_versionCheck(t *testing.T) { // Now change the remote workspace to a different Terraform version if _, err := b.client.Workspaces.Update( context.Background(), - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ TerraformVersion: tfe.String(v0135.String()), @@ -946,7 +1409,7 @@ func TestCloud_StateMgr_versionCheckLatest(t *testing.T) { // Update the remote workspace to the pseudo-version "latest" if _, err := b.client.Workspaces.Update( context.Background(), - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ TerraformVersion: tfe.String("latest"), @@ -1015,7 +1478,7 @@ func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) { // specified remote version if _, err := b.client.Workspaces.Update( context.Background(), - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ ExecutionMode: &tc.executionMode, @@ -1066,7 +1529,7 @@ func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) { // Update the mock remote workspace Terraform version to an invalid version if _, err := b.client.Workspaces.Update( context.Background(), - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ TerraformVersion: tfe.String("1.0.cheetarah"), @@ -1114,7 +1577,7 @@ func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { // specified remote version if _, err := b.client.Workspaces.Update( context.Background(), - b.organization, + b.Organization, b.WorkspaceMapping.Name, tfe.WorkspaceUpdateOptions{ TerraformVersion: tfe.String(remote.String()), @@ -1139,3 +1602,106 @@ func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) { t.Errorf("wrong summary: got %s, want %s", got, wantDetail) } } + +func TestCloudBackend_DeleteWorkspace_SafeAndForce(t *testing.T) { + b, bCleanup := testBackendWithTags(t) + defer bCleanup() + safeDeleteWorkspaceName := "safe-delete-workspace" + forceDeleteWorkspaceName := "force-delete-workspace" + + _, err := b.StateMgr(safeDeleteWorkspaceName) + if err != nil { + t.Fatalf("error: %s", err) + } + + _, err = b.StateMgr(forceDeleteWorkspaceName) + if err != nil { + t.Fatalf("error: %s", err) + } + + // sanity check that the mock now contains two workspaces + wl, err := b.Workspaces() + if err != nil { + t.Fatalf("error fetching workspace names: %v", err) + } + if len(wl) != 2 { + t.Fatalf("expected 2 workspaced but got %d", len(wl)) + } + + c := context.Background() + safeDeleteWorkspace, err := b.client.Workspaces.Read(c, b.Organization, safeDeleteWorkspaceName) + if err != nil { + t.Fatalf("error fetching workspace: %v", err) + } + + // Lock a workspace so that it should fail to be safe deleted + _, err = b.client.Workspaces.Lock(context.Background(), safeDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")}) + if err != nil { + t.Fatalf("error locking workspace: %v", err) + } + err = b.DeleteWorkspace(safeDeleteWorkspaceName, false) + if err == nil { + t.Fatalf("workspace should have failed to safe delete") + } + + // unlock the workspace and confirm that safe-delete now works + _, err = b.client.Workspaces.Unlock(context.Background(), safeDeleteWorkspace.ID) + if err != nil { + t.Fatalf("error unlocking workspace: %v", err) + } + err = b.DeleteWorkspace(safeDeleteWorkspaceName, false) + if err != nil { + t.Fatalf("error safe deleting workspace: %v", err) + } + + // lock a workspace and then confirm that force deleting it works + forceDeleteWorkspace, err := b.client.Workspaces.Read(c, b.Organization, forceDeleteWorkspaceName) + if err != nil { + t.Fatalf("error fetching workspace: %v", err) + } + _, err = b.client.Workspaces.Lock(context.Background(), forceDeleteWorkspace.ID, tfe.WorkspaceLockOptions{Reason: tfe.String("test")}) + if err != nil { + t.Fatalf("error locking workspace: %v", err) + } + err = b.DeleteWorkspace(forceDeleteWorkspaceName, true) + if err != nil { + t.Fatalf("error force deleting workspace: %v", err) + } +} + +func TestCloudBackend_DeleteWorkspace_DoesNotExist(t *testing.T) { + b, bCleanup := testBackendWithTags(t) + defer bCleanup() + + err := b.DeleteWorkspace("non-existent-workspace", false) + if err != nil { + t.Fatalf("expected deleting a workspace which does not exist to succeed") + } +} + +func TestCloud_ServiceDiscoveryAliases(t *testing.T) { + s := testServer(t) + b := New(testDisco(s)) + + diag := b.Configure(cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), // Forces aliasing to test server + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + })) + if diag.HasErrors() { + t.Fatalf("expected no diagnostic errors, got %s", diag.Err()) + } + + aliases, err := b.ServiceDiscoveryAliases() + if err != nil { + t.Fatalf("expected no errors, got %s", err) + } + if len(aliases) != 1 { + t.Fatalf("expected 1 alias but got %d", len(aliases)) + } +} diff --git a/internal/cloud/cloud_integration.go b/internal/cloud/cloud_integration.go index 047fc57093..d4463a09a2 100644 --- a/internal/cloud/cloud_integration.go +++ b/internal/cloud/cloud_integration.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -6,13 +9,14 @@ import ( "strconv" "time" + "github.com/hashicorp/cli" "github.com/hashicorp/go-tfe" - "github.com/hashicorp/terraform/internal/backend" - "github.com/mitchellh/cli" + + "github.com/hashicorp/terraform/internal/backend/backendrun" ) // IntegrationOutputWriter is an interface used to to write output tailored for -// Terraform Cloud integrations +// HCP Terraform integrations type IntegrationOutputWriter interface { End() OutputElapsed(message string, maxMessage int) @@ -20,12 +24,12 @@ type IntegrationOutputWriter interface { SubOutput(str string) } -// IntegrationContext is a set of data that is useful when performing Terraform Cloud integration operations +// IntegrationContext is a set of data that is useful when performing HCP Terraform integration operations type IntegrationContext struct { B *Cloud StopContext context.Context CancelContext context.Context - Op *backend.Operation + Op *backendrun.Operation Run *tfe.Run } @@ -38,14 +42,14 @@ type integrationCLIOutput struct { var _ IntegrationOutputWriter = (*integrationCLIOutput)(nil) // Compile time check -func (s *IntegrationContext) Poll(every func(i int) (bool, error)) error { +func (s *IntegrationContext) Poll(backoffMinInterval float64, backoffMaxInterval float64, every func(i int) (bool, error)) error { for i := 0; ; i++ { select { case <-s.StopContext.Done(): return s.StopContext.Err() case <-s.CancelContext.Done(): return s.CancelContext.Err() - case <-time.After(backoff(backoffMin, backoffMax, i)): + case <-time.After(backoff(backoffMinInterval, backoffMaxInterval, i)): // blocks for a time between min and max } diff --git a/internal/cloud/cloud_variables.go b/internal/cloud/cloud_variables.go index af6d6afcfb..1ddc255913 100644 --- a/internal/cloud/cloud_variables.go +++ b/internal/cloud/cloud_variables.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/hashicorp/terraform/internal/backend" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -19,9 +23,9 @@ func allowedSourceType(source terraform.ValueSourceType) bool { // in order to allow callers to short circuit cloud runs that contain variable // declaration or parsing errors. The only exception is that missing required values are not // considered errors because they may be defined within the cloud workspace. -func ParseCloudRunVariables(vv map[string]backend.UnparsedVariableValue, decls map[string]*configs.Variable) (map[string]string, tfdiags.Diagnostics) { - declared, diags := backend.ParseDeclaredVariableValues(vv, decls) - _, undedeclaredDiags := backend.ParseUndeclaredVariableValues(vv, decls) +func ParseCloudRunVariables(vv map[string]backendrun.UnparsedVariableValue, decls map[string]*configs.Variable) (map[string]string, tfdiags.Diagnostics) { + declared, diags := backendrun.ParseDeclaredVariableValues(vv, decls) + _, undedeclaredDiags := backendrun.ParseUndeclaredVariableValues(vv, decls) diags = diags.Append(undedeclaredDiags) ret := make(map[string]string, len(declared)) @@ -40,3 +44,32 @@ func ParseCloudRunVariables(vv map[string]backend.UnparsedVariableValue, decls m return ret, diags } + +// ParseCloudRunTestVariables is similar to ParseCloudVariables, except it does +// not make any assumptions about variables needed by the configuration. +// +// Within a test run execution, variables can be defined inside test files and +// inside child modules as well as the main configuration and it is a lot of +// effort to track down exactly where variable definitions exist. We just accept +// all values. +func ParseCloudRunTestVariables(globals map[string]backendrun.UnparsedVariableValue) (map[string]string, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := make(map[string]string, len(globals)) + + for name, v := range globals { + variable, variableDiags := v.ParseVariableValue(configs.VariableParseLiteral) + diags = diags.Append(variableDiags) + if variableDiags.HasErrors() { + continue + } + + if !allowedSourceType(variable.SourceType) { + continue + } + + tokens := hclwrite.TokensForValue(variable.Value) + ret[name] = string(tokens.Bytes()) + } + + return ret, diags +} diff --git a/internal/cloud/cloud_variables_test.go b/internal/cloud/cloud_variables_test.go index 9780f788c1..557fef1b6a 100644 --- a/internal/cloud/cloud_variables_test.go +++ b/internal/cloud/cloud_variables_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -5,16 +8,17 @@ import ( "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/backend" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) func TestParseCloudRunVariables(t *testing.T) { t.Run("populates variables from allowed sources", func(t *testing.T) { - vv := map[string]backend.UnparsedVariableValue{ + vv := map[string]backendrun.UnparsedVariableValue{ "undeclared": testUnparsedVariableValue{source: terraform.ValueFromCLIArg, value: cty.StringVal("0")}, "declaredFromConfig": testUnparsedVariableValue{source: terraform.ValueFromConfig, value: cty.StringVal("1")}, "declaredFromNamedFileMapString": testUnparsedVariableValue{source: terraform.ValueFromNamedFile, value: cty.MapVal(map[string]cty.Value{"foo": cty.StringVal("bar")})}, diff --git a/internal/cloud/cloudplan/remote_plan_json.go b/internal/cloud/cloudplan/remote_plan_json.go new file mode 100644 index 0000000000..b1b4192527 --- /dev/null +++ b/internal/cloud/cloudplan/remote_plan_json.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplan + +import ( + "github.com/hashicorp/terraform/internal/plans" +) + +// RemotePlanJSON is a wrapper struct that associates a pre-baked JSON plan with +// several pieces of metadata that can't be derived directly from the JSON +// contents and must instead be discovered from a tfe.Run or tfe.Plan. The +// wrapper is useful for moving data between the Cloud backend (which is the +// only thing able to fetch the JSON and determine values for the metadata) and +// the command.ShowCommand and views.Show interface (which need to have all of +// this information together). +type RemotePlanJSON struct { + // The raw bytes of json we got from the API. + JSONBytes []byte + // Indicates whether the json bytes are the "redacted json plan" format, or + // the unredacted stable "external json plan" format. These formats are + // actually very different under the hood; the redacted one can be decoded + // directly into a jsonformat.Plan struct and is intended for formatting a + // plan for human consumption, while the unredacted one matches what is + // returned by the jsonplan.Marshal() function, cannot be directly decoded + // into a public type (it's actually a jsonplan.plan struct), and will + // generally be spat back out verbatim. + Redacted bool + // Normal/destroy/refresh. Required by (jsonformat.Renderer).RenderHumanPlan. + Mode plans.Mode + // Unchanged/errored. Required by (jsonformat.Renderer).RenderHumanPlan. + Qualities []plans.Quality + // A human-readable header with a link to view the associated run in the + // HCP Terraform UI. + RunHeader string + // A human-readable footer with information relevant to the likely next + // actions for this plan. + RunFooter string +} diff --git a/internal/cloud/cloudplan/saved_plan.go b/internal/cloud/cloudplan/saved_plan.go new file mode 100644 index 0000000000..ca4e0ce4ea --- /dev/null +++ b/internal/cloud/cloudplan/saved_plan.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package cloudplan + +import ( + "encoding/json" + "errors" + "io" + "os" + "strings" +) + +var ErrInvalidRemotePlanFormat = errors.New("invalid remote plan format, must be 1") +var ErrInvalidRunID = errors.New("invalid run ID") +var ErrInvalidHostname = errors.New("invalid hostname") + +type SavedPlanBookmark struct { + RemotePlanFormat int `json:"remote_plan_format"` + RunID string `json:"run_id"` + Hostname string `json:"hostname"` +} + +func NewSavedPlanBookmark(runID, hostname string) SavedPlanBookmark { + return SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: runID, + Hostname: hostname, + } +} + +func LoadSavedPlanBookmark(filepath string) (SavedPlanBookmark, error) { + bookmark := SavedPlanBookmark{} + + file, err := os.Open(filepath) + if err != nil { + return bookmark, err + } + defer file.Close() + + data, err := io.ReadAll(file) + if err != nil { + return bookmark, err + } + + err = json.Unmarshal(data, &bookmark) + if err != nil { + return bookmark, err + } + + // Note that these error cases are somewhat ambiguous, but they *likely* + // mean we're not looking at a saved plan bookmark at all. Since we're not + // certain about the format at this point, it doesn't quite make sense to + // emit a "known file type but bad" error struct the way we do over in the + // planfile and statefile packages. + if bookmark.RemotePlanFormat != 1 { + return bookmark, ErrInvalidRemotePlanFormat + } else if bookmark.Hostname == "" { + return bookmark, ErrInvalidHostname + } else if bookmark.RunID == "" || !strings.HasPrefix(bookmark.RunID, "run-") { + return bookmark, ErrInvalidRunID + } + + return bookmark, err +} + +func (s *SavedPlanBookmark) Save(filepath string) error { + data, _ := json.Marshal(s) + + err := os.WriteFile(filepath, data, 0644) + if err != nil { + return err + } + + return nil +} diff --git a/internal/cloud/cloudplan/saved_plan_test.go b/internal/cloud/cloudplan/saved_plan_test.go new file mode 100644 index 0000000000..99ce322ee4 --- /dev/null +++ b/internal/cloud/cloudplan/saved_plan_test.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplan + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" +) + +func TestCloud_loadBasic(t *testing.T) { + bookmark := SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: "run-GXfuHMkbyHccAGUg", + Hostname: "app.terraform.io", + } + + file := "./testdata/plan-bookmark/bookmark.json" + result, err := LoadSavedPlanBookmark(file) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(bookmark, result, cmp.Comparer(cty.Value.RawEquals)); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + +func TestCloud_loadCheckRunID(t *testing.T) { + // Run ID must never be empty + file := "./testdata/plan-bookmark/empty_run_id.json" + _, err := LoadSavedPlanBookmark(file) + if !errors.Is(err, ErrInvalidRunID) { + t.Fatalf("expected %s but got %s", ErrInvalidRunID, err) + } +} + +func TestCloud_loadCheckHostname(t *testing.T) { + // Hostname must never be empty + file := "./testdata/plan-bookmark/empty_hostname.json" + _, err := LoadSavedPlanBookmark(file) + if !errors.Is(err, ErrInvalidHostname) { + t.Fatalf("expected %s but got %s", ErrInvalidHostname, err) + } +} + +func TestCloud_loadCheckVersionNumberBasic(t *testing.T) { + // remote_plan_format must be set to 1 + // remote_plan_format and format version number are used interchangeably + file := "./testdata/plan-bookmark/invalid_version.json" + _, err := LoadSavedPlanBookmark(file) + if !errors.Is(err, ErrInvalidRemotePlanFormat) { + t.Fatalf("expected %s but got %s", ErrInvalidRemotePlanFormat, err) + } +} + +func TestCloud_saveWhenFileExistsBasic(t *testing.T) { + tmpDir := t.TempDir() + tmpFile, err := os.Create(filepath.Join(tmpDir, "saved-bookmark.json")) + if err != nil { + t.Fatal("File could not be created.", err) + } + defer tmpFile.Close() + + // verify the created path exists + // os.Stat() wants path to file + _, error := os.Stat(tmpFile.Name()) + if error != nil { + t.Fatal("Path to file does not exist.", error) + } else { + b := &SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: "run-GXfuHMkbyHccAGUg", + Hostname: "app.terraform.io", + } + err := b.Save(tmpFile.Name()) + if err != nil { + t.Fatal(err) + } + } +} + +func TestCloud_saveWhenFileDoesNotExistBasic(t *testing.T) { + tmpDir := t.TempDir() + b := &SavedPlanBookmark{ + RemotePlanFormat: 1, + RunID: "run-GXfuHMkbyHccAGUg", + Hostname: "app.terraform.io", + } + err := b.Save(filepath.Join(tmpDir, "create-new-file.txt")) + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/cloud/cloudplan/testdata/plan-bookmark/bookmark.json b/internal/cloud/cloudplan/testdata/plan-bookmark/bookmark.json new file mode 100644 index 0000000000..0a1c73302a --- /dev/null +++ b/internal/cloud/cloudplan/testdata/plan-bookmark/bookmark.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 1, + "run_id": "run-GXfuHMkbyHccAGUg", + "hostname": "app.terraform.io" +} diff --git a/internal/cloud/cloudplan/testdata/plan-bookmark/empty_hostname.json b/internal/cloud/cloudplan/testdata/plan-bookmark/empty_hostname.json new file mode 100644 index 0000000000..990267294f --- /dev/null +++ b/internal/cloud/cloudplan/testdata/plan-bookmark/empty_hostname.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 1, + "run_id": "run-GXfuHMkbyHccAGUg", + "hostname": "" +} diff --git a/internal/cloud/cloudplan/testdata/plan-bookmark/empty_run_id.json b/internal/cloud/cloudplan/testdata/plan-bookmark/empty_run_id.json new file mode 100644 index 0000000000..712581aeae --- /dev/null +++ b/internal/cloud/cloudplan/testdata/plan-bookmark/empty_run_id.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 1, + "run_id": "", + "hostname": "app.terraform.io" +} diff --git a/internal/cloud/cloudplan/testdata/plan-bookmark/invalid_version.json b/internal/cloud/cloudplan/testdata/plan-bookmark/invalid_version.json new file mode 100644 index 0000000000..59a89d9231 --- /dev/null +++ b/internal/cloud/cloudplan/testdata/plan-bookmark/invalid_version.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 11, + "run_id": "run-GXfuHMkbyHccAGUg", + "hostname": "app.terraform.io" +} diff --git a/internal/cloud/e2e/README.md b/internal/cloud/e2e/README.md index 7928fc3f78..c8c585f5d6 100644 --- a/internal/cloud/e2e/README.md +++ b/internal/cloud/e2e/README.md @@ -10,7 +10,7 @@ Required flags external network calls. This is needed to run these tests. Without it, the tests do not run. * `TFE_TOKEN=` and `TFE_HOSTNAME=`. The helpers -for these tests require admin access to a TFC/TFE instance. +for these tests require admin access to an HCP Terraform or Terraform Enterprise instance. * `-timeout=30m`. Some of these tests take longer than the default 10m timeout for `go test`. ### Flags @@ -19,6 +19,6 @@ for these tests require admin access to a TFC/TFE instance. * Use the `-tfoutput` flag to print the terraform output to standard out. * Use `-ldflags` to change the version Prerelease to match a version available remotely. Some behaviors rely on the exact local version Terraform -being available in TFC/TFE, and manipulating the Prerelease during build is +being available in HCP Terraform or Terraform Enterprise, and manipulating the Prerelease during build is often the only way to ensure this. [(More on `-ldflags`.)](https://www.digitalocean.com/community/tutorials/using-ldflags-to-set-version-information-for-go-applications) diff --git a/internal/cloud/e2e/apply_auto_approve_test.go b/internal/cloud/e2e/apply_auto_approve_test.go index 7866597c94..ea88de6094 100644 --- a/internal/cloud/e2e/apply_auto_approve_test.go +++ b/internal/cloud/e2e/apply_auto_approve_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -32,7 +35,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply"}, @@ -72,7 +75,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply"}, @@ -112,7 +115,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -150,7 +153,7 @@ func Test_terraform_apply_autoApprove(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, diff --git a/internal/cloud/e2e/apply_no_input_flag_test.go b/internal/cloud/e2e/apply_no_input_flag_test.go index 1836f730b3..cddcf3458c 100644 --- a/internal/cloud/e2e/apply_no_input_flag_test.go +++ b/internal/cloud/e2e/apply_no_input_flag_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -20,7 +23,7 @@ func Test_apply_no_input_flag(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-input=false"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized`, + expectedCmdOutput: `HCP Terraform has been successfully initialized`, }, { command: []string{"apply", "-input=false"}, @@ -42,7 +45,7 @@ func Test_apply_no_input_flag(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-input=false"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized`, + expectedCmdOutput: `HCP Terraform has been successfully initialized`, }, { command: []string{"apply", "-auto-approve", "-input=false"}, diff --git a/internal/cloud/e2e/backend_apply_before_init_test.go b/internal/cloud/e2e/backend_apply_before_init_test.go index 390e54a429..1740c935f0 100644 --- a/internal/cloud/e2e/backend_apply_before_init_test.go +++ b/internal/cloud/e2e/backend_apply_before_init_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -21,7 +24,7 @@ func Test_backend_apply_before_init(t *testing.T) { commands: []tfCommand{ { command: []string{"apply"}, - expectedCmdOutput: `Terraform Cloud initialization required: please run "terraform init"`, + expectedCmdOutput: `HCP Terraform initialization required: please run "terraform init"`, expectError: true, }, }, @@ -55,7 +58,7 @@ func Test_backend_apply_before_init(t *testing.T) { commands: []tfCommand{ { command: []string{"apply"}, - expectedCmdOutput: `Terraform Cloud initialization required: please run "terraform init"`, + expectedCmdOutput: `HCP Terraform initialization required: please run "terraform init"`, expectError: true, }, }, diff --git a/internal/cloud/e2e/env_variables_test.go b/internal/cloud/e2e/env_variables_test.go index 1044432bfd..d2e12955e5 100644 --- a/internal/cloud/e2e/env_variables_test.go +++ b/internal/cloud/e2e/env_variables_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -28,7 +31,7 @@ func Test_cloud_organization_env_var(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -75,7 +78,7 @@ func Test_cloud_workspace_name_env_var(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -91,7 +94,7 @@ func Test_cloud_workspace_name_env_var(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"workspace", "show"}, @@ -156,7 +159,7 @@ func Test_cloud_workspace_tags_env_var(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -172,7 +175,7 @@ func Test_cloud_workspace_tags_env_var(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"workspace", "show"}, @@ -229,7 +232,7 @@ func Test_cloud_null_config(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -245,7 +248,7 @@ func Test_cloud_null_config(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"workspace", "show"}, diff --git a/internal/cloud/e2e/helper_test.go b/internal/cloud/e2e/helper_test.go index 4a9f2ee2e7..f943a47823 100644 --- a/internal/cloud/e2e/helper_test.go +++ b/internal/cloud/e2e/helper_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -248,15 +251,15 @@ func writeMainTF(t *testing.T, block string, dir string) { f.Close() } -// The e2e tests rely on the fact that the terraform version in TFC/E is able to -// run the `cloud` configuration block, which is available in 1.1 and will -// continue to be available in later versions. So this function checks that -// there is a version that is >= 1.1. +// The e2e tests rely on the fact that the terraform version in HCP Terraform +// is able to run the `cloud` configuration block, which is available in 1.1 +// and will continue to be available in later versions. So this function checks +// that there is a version that is >= 1.1. func skipWithoutRemoteTerraformVersion(t *testing.T) { version := tfversion.Version baseVersion, err := goversion.NewVersion(version) if err != nil { - t.Fatalf(fmt.Sprintf("Error instantiating go-version for %s", version)) + t.Fatalf("Error instantiating go-version for %s", version) } opts := &tfe.AdminTerraformVersionsListOptions{ ListOptions: tfe.ListOptions{ diff --git a/internal/cloud/e2e/init_with_empty_tags_test.go b/internal/cloud/e2e/init_with_empty_tags_test.go index 016aad50cb..cb27d2addf 100644 --- a/internal/cloud/e2e/init_with_empty_tags_test.go +++ b/internal/cloud/e2e/init_with_empty_tags_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -23,7 +26,7 @@ func Test_init_with_empty_tags(t *testing.T) { command: []string{"init"}, expectedCmdOutput: `There are no workspaces with the configured tags`, userInput: []string{"emptytag-prod"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, }, }, diff --git a/internal/cloud/e2e/main_test.go b/internal/cloud/e2e/main_test.go index 44fc606395..ca7583d13f 100644 --- a/internal/cloud/e2e/main_test.go +++ b/internal/cloud/e2e/main_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( diff --git a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go index f7d1e9b501..64309f8d6d 100644 --- a/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_multi_to_tfc_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -57,7 +60,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { command: []string{"init"}, expectedCmdOutput: `Do you want to copy only your current workspace?`, userInput: []string{"yes"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -122,7 +125,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { command: []string{"init"}, expectedCmdOutput: `Do you want to copy only your current workspace?`, userInput: []string{"yes"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "list"}, @@ -188,7 +191,7 @@ func Test_migrate_multi_to_tfc_cloud_name_strategy(t *testing.T) { command: []string{"init"}, expectedCmdOutput: `Do you want to copy only your current workspace?`, userInput: []string{"yes"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "select", "default"}, @@ -283,12 +286,12 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`, + expectedCmdOutput: `HCP Terraform requires all workspaces to be given an explicit name.`, userInput: []string{"dev", "1", "app-*"}, postInputOutput: []string{ `Would you like to rename your workspaces?`, "How would you like to rename your workspaces?", - "Terraform Cloud has been successfully initialized!"}, + "HCP Terraform has been successfully initialized!"}, }, { command: []string{"workspace", "select", "app-dev"}, @@ -388,12 +391,12 @@ func Test_migrate_multi_to_tfc_cloud_tags_strategy(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud requires all workspaces to be given an explicit name.`, + expectedCmdOutput: `HCP Terraform requires all workspaces to be given an explicit name.`, userInput: []string{"dev", "1", "app-*"}, postInputOutput: []string{ `Would you like to rename your workspaces?`, "How would you like to rename your workspaces?", - "Terraform Cloud has been successfully initialized!"}, + "HCP Terraform has been successfully initialized!"}, }, { command: []string{"workspace", "select", "app-billing"}, diff --git a/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go b/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go index c48d22bfd7..c04222eed7 100644 --- a/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_remote_backend_to_tfc_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -14,7 +17,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { ctx := context.Background() cases := testCases{ - "migrate remote backend name to tfc name": { + "migrate remote backend name to HCP Terraform name": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -42,11 +45,11 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Migrating from backend "remote" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "remote" to HCP Terraform.`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -66,7 +69,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { } }, }, - "migrate remote backend name to tfc same name": { + "migrate remote backend name to HCP Terraform same name": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -94,11 +97,11 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Migrating from backend "remote" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "remote" to HCP Terraform.`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -118,7 +121,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { } }, }, - "migrate remote backend name to tfc tags": { + "migrate remote backend name to HCP Terraform tags": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -150,12 +153,12 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Migrating from backend "remote" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "remote" to HCP Terraform.`, userInput: []string{"yes", "cloud-workspace", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud requires all workspaces to be given an explicit name.`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform requires all workspaces to be given an explicit name.`, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -180,7 +183,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { } }, }, - "migrate remote backend prefix to tfc name strategy single workspace": { + "migrate remote backend prefix to HCP Terraform name strategy single workspace": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -209,11 +212,11 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Migrating from backend "remote" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "remote" to HCP Terraform.`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -233,7 +236,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { } }, }, - "migrate remote backend prefix to tfc name strategy multi workspace": { + "migrate remote backend prefix to HCP Terraform name strategy multi workspace": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -276,7 +279,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { expectedCmdOutput: `Do you want to copy only your current workspace?`, userInput: []string{"yes"}, postInputOutput: []string{ - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -315,7 +318,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { } }, }, - "migrate remote backend prefix to tfc tags strategy single workspace": { + "migrate remote backend prefix to HCP Terraform tags strategy single workspace": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -344,12 +347,12 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Migrating from backend "remote" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "remote" to HCP Terraform.`, userInput: []string{"yes", "cloud-workspace", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud requires all workspaces to be given an explicit name.`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform requires all workspaces to be given an explicit name.`, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "list"}, @@ -369,7 +372,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { } }, }, - "migrate remote backend prefix to tfc tags strategy multi workspace": { + "migrate remote backend prefix to HCP Terraform tags strategy multi workspace": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -414,7 +417,7 @@ func Test_migrate_remote_backend_single_org(t *testing.T) { command: []string{"init", "-ignore-remote-version"}, expectedCmdOutput: `Do you wish to proceed?`, userInput: []string{"yes"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -465,7 +468,7 @@ func Test_migrate_remote_backend_multi_org(t *testing.T) { ctx := context.Background() cases := testCases{ - "migrate remote backend name to tfc name": { + "migrate remote backend name to HCP Terraform name": { operations: []operationSets{ { prep: func(t *testing.T, orgName, dir string) { @@ -493,11 +496,11 @@ func Test_migrate_remote_backend_multi_org(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Migrating from backend "remote" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "remote" to HCP Terraform.`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, diff --git a/internal/cloud/e2e/migrate_state_single_to_tfc_test.go b/internal/cloud/e2e/migrate_state_single_to_tfc_test.go index cc05bf06d9..cbdb87c62b 100644 --- a/internal/cloud/e2e/migrate_state_single_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_single_to_tfc_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -42,11 +45,11 @@ func Test_migrate_single_to_tfc(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Migrating from backend "local" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "local" to HCP Terraform.`, userInput: []string{"yes", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "list"}, @@ -93,12 +96,12 @@ func Test_migrate_single_to_tfc(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Migrating from backend "local" to Terraform Cloud.`, + expectedCmdOutput: `Migrating from backend "local" to HCP Terraform.`, userInput: []string{"yes", "new-workspace", "yes"}, postInputOutput: []string{ `Should Terraform migrate your existing state?`, - `Terraform Cloud requires all workspaces to be given an explicit name.`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform requires all workspaces to be given an explicit name.`, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "list"}, diff --git a/internal/cloud/e2e/migrate_state_tfc_to_other_test.go b/internal/cloud/e2e/migrate_state_tfc_to_other_test.go index 4029ba5b49..44927defd2 100644 --- a/internal/cloud/e2e/migrate_state_tfc_to_other_test.go +++ b/internal/cloud/e2e/migrate_state_tfc_to_other_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -20,7 +23,7 @@ func Test_migrate_tfc_to_other(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, }, }, @@ -32,7 +35,7 @@ func Test_migrate_tfc_to_other(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Migrating state from Terraform Cloud to another backend is not yet implemented.`, + expectedCmdOutput: `Migrating state from HCP Terraform to another backend is not yet implemented.`, expectError: true, }, }, diff --git a/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go b/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go index d2eb521eb4..c50363b388 100644 --- a/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go +++ b/internal/cloud/e2e/migrate_state_tfc_to_tfc_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -35,7 +38,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"workspace", "show"}, @@ -60,7 +63,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -95,7 +98,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -116,7 +119,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { userInput: []string{"new-workspace"}, postInputOutput: []string{ `Terraform can create a properly tagged workspace for you now.`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, { command: []string{"workspace", "show"}, @@ -153,7 +156,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"apply", "-auto-approve"}, @@ -181,7 +184,7 @@ func Test_migrate_tfc_to_tfc_single_workspace(t *testing.T) { userInput: []string{"new-workspace"}, postInputOutput: []string{ `Terraform can create a properly tagged workspace for you now.`, - `Terraform Cloud has been successfully initialized!`}, + `HCP Terraform has been successfully initialized!`}, }, }, }, @@ -234,7 +237,7 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) { command: []string{"init"}, expectedCmdOutput: `The currently selected workspace (default) does not exist.`, userInput: []string{"1"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"apply"}, @@ -272,7 +275,7 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) { commands: []tfCommand{ { command: []string{"init", "-ignore-remote-version"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, postInputOutput: []string{`tag_val = "service"`}, }, { @@ -315,7 +318,7 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) { command: []string{"init"}, expectedCmdOutput: `The currently selected workspace (default) does not exist.`, userInput: []string{"1"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, { command: []string{"apply", "-auto-approve"}, @@ -342,7 +345,7 @@ func Test_migrate_tfc_to_tfc_multiple_workspace(t *testing.T) { command: []string{"init", "-ignore-remote-version"}, expectedCmdOutput: `There are no workspaces with the configured tags (billing)`, userInput: []string{"new-app-prod"}, - postInputOutput: []string{`Terraform Cloud has been successfully initialized!`}, + postInputOutput: []string{`HCP Terraform has been successfully initialized!`}, }, }, }, diff --git a/internal/cloud/e2e/run_variables_test.go b/internal/cloud/e2e/run_variables_test.go index ee1f66eeb6..abe0ec2383 100644 --- a/internal/cloud/e2e/run_variables_test.go +++ b/internal/cloud/e2e/run_variables_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -61,7 +64,7 @@ func Test_cloud_run_variables(t *testing.T) { commands: []tfCommand{ { command: []string{"init"}, - expectedCmdOutput: `Terraform Cloud has been successfully initialized!`, + expectedCmdOutput: `HCP Terraform has been successfully initialized!`, }, { command: []string{"plan", "-var", "foo=bar"}, diff --git a/internal/cloud/errors.go b/internal/cloud/errors.go index cf668516f3..d6279e2224 100644 --- a/internal/cloud/errors.go +++ b/internal/cloud/errors.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -35,6 +38,13 @@ var ( fmt.Sprintf("Only one of workspace \"tags\" or \"name\" is allowed.\n\n%s", workspaceConfigurationHelp), cty.Path{cty.GetAttrStep{Name: "workspaces"}}, ) + + invalidWorkspaceConfigNameConflict = tfdiags.AttributeValue( + tfdiags.Error, + "Invalid workspaces configuration", + fmt.Sprintf("Specified workspace \"name\" conflicts with TF_WORKSPACE environment variable.\n\n%s", workspaceConfigurationHelp), + cty.Path{cty.GetAttrStep{Name: "workspaces"}}, + ) ) const ignoreRemoteVersionHelp = "If you're sure you want to upgrade the state, you can force Terraform to continue using the -ignore-remote-version flag. This may result in an unusable workspace." diff --git a/internal/cloud/migration.go b/internal/cloud/migration.go index 069d1b28eb..af6dbb5fc2 100644 --- a/internal/cloud/migration.go +++ b/internal/cloud/migration.go @@ -1,8 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( + "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/configs" - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" ) // Most of the logic for migrating into and out of "cloud mode" actually lives @@ -14,7 +17,7 @@ import ( // the context of Cloud integration mode. type ConfigChangeMode rune -//go:generate go run golang.org/x/tools/cmd/stringer -type ConfigChangeMode +//go:generate go tool golang.org/x/tools/cmd/stringer -type ConfigChangeMode const ( // ConfigMigrationIn represents when the configuration calls for using @@ -45,7 +48,7 @@ const ( // the way we currently model working directory settings and config, so its // signature probably won't survive any non-trivial refactoring of how // the CLI layer thinks about backends/state storage. -func DetectConfigChangeType(wdState *legacy.BackendState, config *configs.Backend, haveLocalStates bool) ConfigChangeMode { +func DetectConfigChangeType(wdState *workdir.BackendState, config *configs.Backend, haveLocalStates bool) ConfigChangeMode { // Although externally the cloud integration isn't really a "backend", // internally we treat it a bit like one just to preserve all of our // existing interfaces that assume backends. "cloud" is the placeholder diff --git a/internal/cloud/migration_test.go b/internal/cloud/migration_test.go index f1ae0f48ec..bfc83dae1a 100644 --- a/internal/cloud/migration_test.go +++ b/internal/cloud/migration_test.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( "testing" + "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/configs" - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" ) func TestDetectConfigChangeType(t *testing.T) { @@ -98,10 +101,10 @@ func TestDetectConfigChangeType(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - var state *legacy.BackendState + var state *workdir.BackendState var config *configs.Backend if test.stateType != "" { - state = &legacy.BackendState{ + state = &workdir.BackendState{ Type: test.stateType, // everything else is irrelevant for our purposes here } diff --git a/internal/cloud/remote_test.go b/internal/cloud/remote_test.go index b0c44d60a8..0477b5012a 100644 --- a/internal/cloud/remote_test.go +++ b/internal/cloud/remote_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( diff --git a/internal/cloud/retry.go b/internal/cloud/retry.go new file mode 100644 index 0000000000..ab784f5964 --- /dev/null +++ b/internal/cloud/retry.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "log" + "sync/atomic" + "time" +) + +// Fatal implements a RetryBackoff func return value that, if encountered, +// signals that the func should not be retried. In that case, the error +// returned by the interface method will be returned by RetryBackoff +type Fatal interface { + FatalError() error +} + +// NonRetryableError is a simple implementation of Fatal that wraps an error +type NonRetryableError struct { + InnerError error +} + +// FatalError returns the inner error, but also implements Fatal, which +// signals to RetryBackoff that a non-retryable error occurred. +func (e NonRetryableError) FatalError() error { + return e.InnerError +} + +// Error returns the inner error string +func (e NonRetryableError) Error() string { + return e.InnerError.Error() +} + +var ( + initialBackoffDelay = time.Second + maxBackoffDelay = 3 * time.Second +) + +// RetryBackoff retries function f until nil or a FatalError is returned. +// RetryBackoff only returns an error if the context is in error or if a +// FatalError was encountered. +func RetryBackoff(ctx context.Context, f func() error) error { + // doneCh signals that the routine is done and sends the last error + var doneCh = make(chan struct{}) + var errVal atomic.Value + type errWrap struct { + E error + } + + go func() { + // the retry delay between each attempt + var delay time.Duration = 0 + defer close(doneCh) + + for { + select { + case <-ctx.Done(): + return + case <-time.After(delay): + } + + err := f() + switch e := err.(type) { + case nil: + return + case Fatal: + errVal.Store(errWrap{e.FatalError()}) + return + } + + delay *= 2 + if delay == 0 { + delay = initialBackoffDelay + } + + delay = min(delay, maxBackoffDelay) + + log.Printf("[WARN] retryable error: %q, delaying for %s", err, delay) + } + }() + + // Wait until done or deadline + select { + case <-doneCh: + case <-ctx.Done(): + } + + err, hadErr := errVal.Load().(errWrap) + var lastErr error + if hadErr { + lastErr = err.E + } + + if ctx.Err() != nil { + return ctx.Err() + } + + return lastErr +} diff --git a/internal/cloud/retry_test.go b/internal/cloud/retry_test.go new file mode 100644 index 0000000000..3c8c8f989a --- /dev/null +++ b/internal/cloud/retry_test.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "errors" + "testing" + "time" +) + +type fatalError struct{} + +var fe = errors.New("this was a fatal error") + +func (f fatalError) FatalError() error { + return fe +} + +func (f fatalError) Error() string { + return f.FatalError().Error() +} + +func Test_RetryBackoff_canceled(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + + cancel() + + err := RetryBackoff(ctx, func() error { + return nil + }) + + if !errors.Is(err, context.Canceled) { + t.Errorf("expected canceled error, got %q", err) + } +} + +func Test_RetryBackoff_deadline(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Millisecond)) + + defer cancel() + + err := RetryBackoff(ctx, func() error { + time.Sleep(10 * time.Millisecond) + return nil + }) + + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("expected timeout error, got %q", err) + } +} + +func Test_RetryBackoff_happy(t *testing.T) { + t.Parallel() + + err := RetryBackoff(context.Background(), func() error { + return nil + }) + + if err != nil { + t.Errorf("expected nil err, got %q", err) + } +} + +func Test_RetryBackoff_fatal(t *testing.T) { + t.Parallel() + + err := RetryBackoff(context.Background(), func() error { + return fatalError{} + }) + + if !errors.Is(fe, err) { + t.Errorf("expected fatal error, got %q", err) + } +} + +func Test_RetryBackoff_non_fatal(t *testing.T) { + t.Parallel() + + var retriedCount = 0 + + err := RetryBackoff(context.Background(), func() error { + retriedCount += 1 + if retriedCount == 2 { + return nil + } + return errors.New("retryable error") + }) + + if err != nil { + t.Errorf("expected no error, got %q", err) + } + + if retriedCount != 2 { + t.Errorf("expected 2 retries, got %d", retriedCount) + } +} diff --git a/internal/cloud/state.go b/internal/cloud/state.go index 73bea8ba5d..9fb0679f51 100644 --- a/internal/cloud/state.go +++ b/internal/cloud/state.go @@ -1,29 +1,81 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( + "bytes" "context" + "crypto/md5" + "encoding/base64" "encoding/json" "errors" "fmt" "log" + "net/http" + "os" + "strconv" "strings" + "sync" + "time" - "github.com/hashicorp/go-tfe" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" + tfe "github.com/hashicorp/go-tfe" + uuid "github.com/hashicorp/go-uuid" + + "github.com/hashicorp/terraform/internal/command/jsonstate" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" ) -// State is similar to remote State and delegates to it, except in the case of output values, -// which use a separate methodology that ensures the caller is authorized to read cloud -// workspace outputs. -type State struct { - Client *remoteClient +const ( + // HeaderSnapshotInterval is the header key that controls the snapshot interval + HeaderSnapshotInterval = "x-terraform-snapshot-interval" +) - delegate remote.State +// State implements the State interfaces in the state package to handle +// reading and writing the remote state to HCP Terraform. This State on +// its own does no local caching so every persist will go to the remote +// storage and local writes will go to memory. +type State struct { + mu sync.Mutex + + // We track two pieces of meta data in addition to the state itself: + // + // lineage - the state's unique ID + // serial - the monotonic counter of "versions" of the state + // + // Both of these (along with state) have a sister field + // that represents the values read in from an existing source. + // All three of these values are used to determine if the new + // state has changed from an existing state we read in. + lineage, readLineage string + serial, readSerial uint64 + state, readState *states.State + disableLocks bool + tfeClient *tfe.Client + organization string + workspace *tfe.Workspace + stateUploadErr bool + forcePush bool + lockInfo *statemgr.LockInfo + + // The server can optionally return an X-Terraform-Snapshot-Interval header + // in its response to the "Create State Version" operation, which specifies + // a number of seconds the server would prefer us to wait before trying + // to write a new snapshot. If this is non-zero then we'll wait at least + // this long before allowing another intermediate snapshot. This does + // not effect final snapshots after an operation, which will always + // be written to the remote API. + stateSnapshotInterval time.Duration + // If the header X-Terraform-Snapshot-Interval is present then + // we will enable snapshots + enableIntermediateSnapshots bool } var ErrStateVersionUnauthorizedUpgradeState = errors.New(strings.TrimSpace(` @@ -33,71 +85,490 @@ of authorization and therefore this error can usually be fixed by upgrading the remote state version. `)) -// Proof that cloud State is a statemgr.Persistent interface -var _ statemgr.Persistent = (*State)(nil) +var _ statemgr.Full = (*State)(nil) +var _ statemgr.Migrator = (*State)(nil) +var _ statemgr.IntermediateStateConditionalPersister = (*State)(nil) -func NewState(client *remoteClient) *State { - return &State{ - Client: client, - delegate: remote.State{Client: client}, - } -} - -// State delegates calls to read State to the remote State +// statemgr.Reader impl. func (s *State) State() *states.State { - return s.delegate.State() + s.mu.Lock() + defer s.mu.Unlock() + + return s.state.DeepCopy() } -// Lock delegates calls to lock state to the remote State -func (s *State) Lock(info *statemgr.LockInfo) (string, error) { - return s.delegate.Lock(info) +// StateForMigration is part of our implementation of statemgr.Migrator. +func (s *State) StateForMigration() *statefile.File { + s.mu.Lock() + defer s.mu.Unlock() + + return statefile.New(s.state.DeepCopy(), s.lineage, s.serial) } -// Unlock delegates calls to unlock state to the remote State -func (s *State) Unlock(id string) error { - return s.delegate.Unlock(id) +// WriteStateForMigration is part of our implementation of statemgr.Migrator. +func (s *State) WriteStateForMigration(f *statefile.File, force bool) error { + s.mu.Lock() + defer s.mu.Unlock() + + if !force { + checkFile := statefile.New(s.state, s.lineage, s.serial) + if err := statemgr.CheckValidImport(f, checkFile); err != nil { + return err + } + } + + // We create a deep copy of the state here, because the caller also has + // a reference to the given object and can potentially go on to mutate + // it after we return, but we want the snapshot at this point in time. + s.state = f.State.DeepCopy() + s.lineage = f.Lineage + s.serial = f.Serial + s.forcePush = force + + return nil } -// RefreshState delegates calls to refresh State to the remote State -func (s *State) RefreshState() error { - return s.delegate.RefreshState() +// DisableLocks turns the Lock and Unlock methods into no-ops. This is intended +// to be called during initialization of a state manager and should not be +// called after any of the statemgr.Full interface methods have been called. +func (s *State) DisableLocks() { + s.disableLocks = true } -// RefreshState delegates calls to refresh State to the remote State -func (s *State) PersistState() error { - return s.delegate.PersistState() +// StateSnapshotMeta returns the metadata from the most recently persisted +// or refreshed persistent state snapshot. +// +// This is an implementation of statemgr.PersistentMeta. +func (s *State) StateSnapshotMeta() statemgr.SnapshotMeta { + return statemgr.SnapshotMeta{ + Lineage: s.lineage, + Serial: s.serial, + } } -// WriteState delegates calls to write State to the remote State +// statemgr.Writer impl. func (s *State) WriteState(state *states.State) error { - return s.delegate.WriteState(state) + s.mu.Lock() + defer s.mu.Unlock() + + // We create a deep copy of the state here, because the caller also has + // a reference to the given object and can potentially go on to mutate + // it after we return, but we want the snapshot at this point in time. + s.state = state.DeepCopy() + s.forcePush = false + + return nil } -func (s *State) fallbackReadOutputsFromFullState() (map[string]*states.OutputValue, error) { - log.Printf("[DEBUG] falling back to reading full state") +// PersistState uploads a snapshot of the latest state as a StateVersion to HCP Terraform +func (s *State) PersistState(schemas *schemarepo.Schemas) error { + s.mu.Lock() + defer s.mu.Unlock() - if err := s.RefreshState(); err != nil { - return nil, fmt.Errorf("failed to load state: %w", err) + log.Printf("[DEBUG] cloud/state: state read serial is: %d; serial is: %d", s.readSerial, s.serial) + log.Printf("[DEBUG] cloud/state: state read lineage is: %s; lineage is: %s", s.readLineage, s.lineage) + + if s.readState != nil { + lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage + serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial + stateUnchanged := statefile.StatesMarshalEqual(s.state, s.readState) + if stateUnchanged && lineageUnchanged && serialUnchanged { + // If the state, lineage or serial haven't changed at all then we have nothing to do. + return nil + } + s.serial++ + } else { + // We might be writing a new state altogether, but before we do that + // we'll check to make sure there isn't already a snapshot present + // that we ought to be updating. + err := s.refreshState() + if err != nil { + return fmt.Errorf("failed checking for existing remote state: %s", err) + } + log.Printf("[DEBUG] cloud/state: after refresh, state read serial is: %d; serial is: %d", s.readSerial, s.serial) + log.Printf("[DEBUG] cloud/state: after refresh, state read lineage is: %s; lineage is: %s", s.readLineage, s.lineage) + + if s.lineage == "" { // indicates that no state snapshot is present yet + lineage, err := uuid.GenerateUUID() + if err != nil { + return fmt.Errorf("failed to generate initial lineage: %v", err) + } + s.lineage = lineage + s.serial++ + } } - state := s.State() - if state == nil { - // We know that there is supposed to be state (and this is not simply a new workspace - // without state) because the fallback is only invoked when outputs are present but - // detailed types are not available. - return nil, ErrStateVersionUnauthorizedUpgradeState + f := statefile.New(s.state, s.lineage, s.serial) + + var buf bytes.Buffer + err := statefile.Write(f, &buf) + if err != nil { + return err } - return state.RootModule().OutputValues, nil + var jsonState []byte + if schemas != nil { + jsonState, err = jsonstate.Marshal(f, schemas) + if err != nil { + return err + } + } + + stateFile, err := statefile.Read(bytes.NewReader(buf.Bytes())) + if err != nil { + return fmt.Errorf("failed to read state: %w", err) + } + + ov, err := jsonstate.MarshalOutputs(stateFile.State.RootOutputValues) + if err != nil { + return fmt.Errorf("failed to translate outputs: %w", err) + } + jsonStateOutputs, err := json.Marshal(ov) + if err != nil { + return fmt.Errorf("failed to marshal outputs to json: %w", err) + } + + err = s.uploadState(s.lineage, s.serial, s.forcePush, buf.Bytes(), jsonState, jsonStateOutputs) + if err != nil { + s.stateUploadErr = true + return fmt.Errorf("error uploading state: %w", err) + } + // After we've successfully persisted, what we just wrote is our new + // reference state until someone calls RefreshState again. + // We've potentially overwritten (via force) the state, lineage + // and / or serial (and serial was incremented) so we copy over all + // three fields so everything matches the new state and a subsequent + // operation would correctly detect no changes to the lineage, serial or state. + s.readState = s.state.DeepCopy() + s.readLineage = s.lineage + s.readSerial = s.serial + + return nil } -// GetRootOutputValues fetches output values from Terraform Cloud -func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { +// ShouldPersistIntermediateState implements statemgr.IntermediateStateConditionalPersister +func (s *State) ShouldPersistIntermediateState(info *statemgr.IntermediateStatePersistInfo) bool { + if info.ForcePersist { + return true + } + + // This value is controlled by a x-terraform-snapshot-interval header intercepted during + // state-versions API responses + if !s.enableIntermediateSnapshots { + return false + } + + // Our persist interval is the largest of either the caller's requested + // interval or the server's requested interval. + wantInterval := info.RequestedPersistInterval + if s.stateSnapshotInterval > wantInterval { + wantInterval = s.stateSnapshotInterval + } + + currentInterval := time.Since(info.LastPersist) + return currentInterval >= wantInterval +} + +func (s *State) uploadStateFallback(ctx context.Context, lineage string, serial uint64, isForcePush bool, state, jsonState, jsonStateOutputs []byte) error { + options := tfe.StateVersionCreateOptions{ + Lineage: tfe.String(lineage), + Serial: tfe.Int64(int64(serial)), + MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), + Force: tfe.Bool(isForcePush), + State: tfe.String(base64.StdEncoding.EncodeToString(state)), + JSONState: tfe.String(base64.StdEncoding.EncodeToString(jsonState)), + JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(jsonStateOutputs)), + } + + // If we have a run ID, make sure to add it to the options + // so the state will be properly associated with the run. + runID := os.Getenv("TFE_RUN_ID") + if runID != "" { + options.Run = &tfe.Run{ID: runID} + } + + // Create the new state. + _, err := s.tfeClient.StateVersions.Create(ctx, s.workspace.ID, options) + return err +} + +func (s *State) uploadState(lineage string, serial uint64, isForcePush bool, state, jsonState, jsonStateOutputs []byte) error { ctx := context.Background() - so, err := s.Client.client.StateVersionOutputs.ReadCurrent(ctx, s.Client.workspace.ID) + options := tfe.StateVersionUploadOptions{ + StateVersionCreateOptions: tfe.StateVersionCreateOptions{ + Lineage: tfe.String(lineage), + Serial: tfe.Int64(int64(serial)), + MD5: tfe.String(fmt.Sprintf("%x", md5.Sum(state))), + Force: tfe.Bool(isForcePush), + JSONStateOutputs: tfe.String(base64.StdEncoding.EncodeToString(jsonStateOutputs)), + }, + RawState: state, + RawJSONState: jsonState, + } + + // If we have a run ID, make sure to add it to the options + // so the state will be properly associated with the run. + runID := os.Getenv("TFE_RUN_ID") + if runID != "" { + options.StateVersionCreateOptions.Run = &tfe.Run{ID: runID} + } + + // The server is allowed to dynamically request a different time interval + // than we'd normally use, for example if it's currently under heavy load + // and needs clients to backoff for a while. + ctx = tfe.ContextWithResponseHeaderHook(ctx, s.readSnapshotIntervalHeader) + + // Create the new state. + _, err := s.tfeClient.StateVersions.Upload(ctx, s.workspace.ID, options) + if errors.Is(err, tfe.ErrStateVersionUploadNotSupported) { + // Create the new state with content included in the request (Terraform Enterprise v202306-1 and below) + log.Println("[INFO] Detected that state version upload is not supported. Retrying using compatibility state upload.") + return s.uploadStateFallback(ctx, lineage, serial, isForcePush, state, jsonState, jsonStateOutputs) + } + + return err +} + +// Lock calls the Client's Lock method if it's implemented. +func (s *State) Lock(info *statemgr.LockInfo) (string, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disableLocks { + return "", nil + } + ctx := context.Background() + + lockErr := &statemgr.LockError{Info: s.lockInfo} + + // Lock the workspace. + _, err := s.tfeClient.Workspaces.Lock(ctx, s.workspace.ID, tfe.WorkspaceLockOptions{ + Reason: tfe.String("Locked by Terraform"), + }) + if err != nil { + if err == tfe.ErrWorkspaceLocked { + lockErr.Info = info + err = fmt.Errorf("%s (lock ID: \"%s/%s\")", err, s.organization, s.workspace.Name) + } + lockErr.Err = err + return "", lockErr + } + + s.lockInfo = info + + return s.lockInfo.ID, nil +} + +// statemgr.Refresher impl. +func (s *State) RefreshState() error { + s.mu.Lock() + defer s.mu.Unlock() + return s.refreshState() +} + +// refreshState is the main implementation of RefreshState, but split out so +// that we can make internal calls to it from methods that are already holding +// the s.mu lock. +func (s *State) refreshState() error { + payload, err := s.getStatePayload() + if err != nil { + return err + } + + // no remote state is OK + if payload == nil { + s.readState = nil + s.lineage = "" + s.serial = 0 + return nil + } + + stateFile, err := statefile.Read(bytes.NewReader(payload.Data)) + if err != nil { + return err + } + + s.lineage = stateFile.Lineage + s.serial = stateFile.Serial + s.state = stateFile.State + + // Properties from the remote must be separate so we can + // track changes as lineage, serial and/or state are mutated + s.readLineage = stateFile.Lineage + s.readSerial = stateFile.Serial + s.readState = s.state.DeepCopy() + return nil +} + +func (s *State) getStatePayload() (*remote.Payload, error) { + ctx := context.Background() + + // Check the x-terraform-snapshot-interval header to see if it has a non-empty + // value which would indicate snapshots are enabled + ctx = tfe.ContextWithResponseHeaderHook(ctx, s.readSnapshotIntervalHeader) + + sv, err := s.tfeClient.StateVersions.ReadCurrent(ctx, s.workspace.ID) + if err != nil { + if err == tfe.ErrResourceNotFound { + // If no state exists, then return nil. + return nil, nil + } + return nil, fmt.Errorf("error retrieving state: %v", err) + } + + state, err := s.tfeClient.StateVersions.Download(ctx, sv.DownloadURL) + if err != nil { + return nil, fmt.Errorf("error downloading state: %v", err) + } + + // If the state is empty, then return nil. + if len(state) == 0 { + return nil, nil + } + + // Get the MD5 checksum of the state. + sum := md5.Sum(state) + + return &remote.Payload{ + Data: state, + MD5: sum[:], + }, nil +} + +type errorUnlockFailed struct { + innerError error +} + +func (e errorUnlockFailed) FatalError() error { + return e.innerError +} + +func (e errorUnlockFailed) Error() string { + return e.innerError.Error() +} + +// Unlock calls the Client's Unlock method if it's implemented. +func (s *State) Unlock(id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if s.disableLocks { + return nil + } + + ctx := context.Background() + + // We first check if there was an error while uploading the latest + // state. If so, we will not unlock the workspace to prevent any + // changes from being applied until the correct state is uploaded. + if s.stateUploadErr { + return nil + } + + lockErr := &statemgr.LockError{Info: s.lockInfo} + + // With lock info this should be treated as a normal unlock. + if s.lockInfo != nil { + // Verify the expected lock ID. + if s.lockInfo.ID != id { + lockErr.Err = fmt.Errorf("lock ID does not match existing lock") + return lockErr + } + + // Unlock the workspace. + err := RetryBackoff(ctx, func() error { + _, err := s.tfeClient.Workspaces.Unlock(ctx, s.workspace.ID) + if err != nil { + if errors.Is(err, tfe.ErrWorkspaceLockedStateVersionStillPending) { + // This is a retryable error. + return err + } + // This will not be retried + return &errorUnlockFailed{innerError: err} + } + return nil + }) + + if err != nil { + lockErr.Err = err + return lockErr + } + + return nil + } + + // Verify the optional force-unlock lock ID. + if s.organization+"/"+s.workspace.Name != id { + lockErr.Err = fmt.Errorf( + "lock ID %q does not match existing lock ID \"%s/%s\"", + id, + s.organization, + s.workspace.Name, + ) + return lockErr + } + + // Force unlock the workspace. + _, err := s.tfeClient.Workspaces.ForceUnlock(ctx, s.workspace.ID) + if err != nil { + lockErr.Err = err + return lockErr + } + + return nil +} + +// Delete the remote state. +func (s *State) Delete(force bool) error { + var err error + + isSafeDeleteSupported := s.workspace.Permissions.CanForceDelete != nil + if force || !isSafeDeleteSupported { + err = s.tfeClient.Workspaces.Delete(context.Background(), s.organization, s.workspace.Name) + } else { + err = s.tfeClient.Workspaces.SafeDelete(context.Background(), s.organization, s.workspace.Name) + } + + if err != nil && err != tfe.ErrResourceNotFound { + return fmt.Errorf("error deleting workspace %s: %v", s.workspace.Name, err) + } + + return nil +} + +// GetRootOutputValues fetches output values from HCP Terraform +func (s *State) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { + // The cloud backend initializes this value to true, but we want to implement + // some custom retry logic. This code presumes that the tfeClient doesn't need + // to be shared with other goroutines by the caller. + s.tfeClient.RetryServerErrors(false) + defer s.tfeClient.RetryServerErrors(true) + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + var so *tfe.StateVersionOutputsList + err := RetryBackoff(ctx, func() error { + var err error + so, err = s.tfeClient.StateVersionOutputs.ReadCurrent(ctx, s.workspace.ID) + + if err != nil { + if strings.Contains(err.Error(), "service unavailable") { + return err + } + return NonRetryableError{err} + } + return nil + }) if err != nil { + switch err { + case context.DeadlineExceeded: + return nil, fmt.Errorf("current outputs were not ready to be read within the deadline. Please try again") + case context.Canceled: + return nil, fmt.Errorf("canceled reading current outputs") + } return nil, fmt.Errorf("could not read state version outputs: %w", err) } @@ -109,13 +580,27 @@ func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { // with a version of terraform < 1.3.0. In this case, we'll eject completely from this // function and fall back to the old behavior of reading the entire state file, which // requires a higher level of authorization. - return s.fallbackReadOutputsFromFullState() + log.Printf("[DEBUG] falling back to reading full state") + + if err := s.RefreshState(); err != nil { + return nil, fmt.Errorf("failed to load state: %w", err) + } + + state := s.State() + if state == nil { + // We know that there is supposed to be state (and this is not simply a new workspace + // without state) because the fallback is only invoked when outputs are present but + // detailed types are not available. + return nil, ErrStateVersionUnauthorizedUpgradeState + } + + return state.RootOutputValues, nil } if output.Sensitive { // Since this is a sensitive value, the output must be requested explicitly in order to // read its value, which is assumed to be present by callers - sensitiveOutput, err := s.Client.client.StateVersionOutputs.Read(ctx, output.ID) + sensitiveOutput, err := s.tfeClient.StateVersionOutputs.Read(ctx, output.ID) if err != nil { return nil, fmt.Errorf("could not read state version output %s: %w", output.ID, err) } @@ -136,6 +621,43 @@ func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { return result, nil } +func clamp(val, min, max int64) int64 { + if val < min { + return min + } else if val > max { + return max + } + return val +} + +func (s *State) readSnapshotIntervalHeader(status int, header http.Header) { + // Only proceed if this came from tfe.v2 API + contentType := header.Get("Content-Type") + if !strings.Contains(contentType, tfe.ContentTypeJSONAPI) { + log.Printf("[TRACE] Skipping intermediate state interval because Content-Type was %q", contentType) + return + } + + intervalStr := header.Get(HeaderSnapshotInterval) + + if intervalSecs, err := strconv.ParseInt(intervalStr, 10, 64); err == nil { + // More than an hour is an unreasonable delay, so we'll just + // limit to one hour max. + intervalSecs = clamp(intervalSecs, 0, 3600) + s.stateSnapshotInterval = time.Duration(intervalSecs) * time.Second + } else { + // If the header field is either absent or invalid then we'll + // just choose zero, which effectively means that we'll just use + // the caller's requested interval instead. If the caller has no + // requested interval or it is zero, then we will disable snapshots. + s.stateSnapshotInterval = time.Duration(0) + } + + // We will only enable snapshots for intervals greater than zero + log.Printf("[TRACE] Intermediate state interval is set by header to %v", s.stateSnapshotInterval) + s.enableIntermediateSnapshots = s.stateSnapshotInterval > 0 +} + // tfeOutputToCtyValue decodes a combination of TFE output value and detailed-type to create a // cty value that is suitable for use in terraform. func tfeOutputToCtyValue(output tfe.StateVersionOutput) (cty.Value, error) { diff --git a/internal/cloud/state_test.go b/internal/cloud/state_test.go index 738ae721a4..61919e0b1e 100644 --- a/internal/cloud/state_test.go +++ b/internal/cloud/state_test.go @@ -1,11 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( + "bytes" + "context" + "io/ioutil" "testing" + "time" - "github.com/hashicorp/go-tfe" - + tfe "github.com/hashicorp/go-tfe" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/zclconf/go-cty/cty" ) func TestState_impl(t *testing.T) { @@ -27,15 +37,10 @@ func TestState_GetRootOutputValues(t *testing.T) { b, bCleanup := testBackendWithOutputs(t) defer bCleanup() - client := &remoteClient{ - client: b.client, - workspace: &tfe.Workspace{ - ID: "ws-abcd", - }, - } - - state := NewState(client) - outputs, err := state.GetRootOutputValues() + state := &State{tfeClient: b.client, organization: b.Organization, workspace: &tfe.Workspace{ + ID: "ws-abcd", + }} + outputs, err := state.GetRootOutputValues(context.Background()) if err != nil { t.Fatalf("error returned from GetRootOutputValues: %s", err) @@ -81,3 +86,339 @@ func TestState_GetRootOutputValues(t *testing.T) { } } } + +func TestState(t *testing.T) { + var buf bytes.Buffer + s := statemgr.TestFullInitialState() + sf := statefile.New(s, "stub-lineage", 2) + err := statefile.Write(sf, &buf) + if err != nil { + t.Fatalf("err: %s", err) + } + data := buf.Bytes() + + state := testCloudState(t) + + jsonState, err := ioutil.ReadFile("../command/testdata/show-json-state/sensitive-variables/output.json") + if err != nil { + t.Fatal(err) + } + + jsonStateOutputs := []byte(` +{ + "outputs": { + "foo": { + "type": "string", + "value": "bar" + } + } +}`) + + if err := state.uploadState(state.lineage, state.serial, state.forcePush, data, jsonState, jsonStateOutputs); err != nil { + t.Fatalf("put: %s", err) + } + + payload, err := state.getStatePayload() + if err != nil { + t.Fatalf("get: %s", err) + } + if !bytes.Equal(payload.Data, data) { + t.Fatalf("expected full state %q\n\ngot: %q", string(payload.Data), string(data)) + } + + if err := state.Delete(true); err != nil { + t.Fatalf("delete: %s", err) + } + + p, err := state.getStatePayload() + if err != nil { + t.Fatalf("get: %s", err) + } + if p != nil { + t.Fatalf("expected empty state, got: %q", string(p.Data)) + } +} + +func TestCloudLocks(t *testing.T) { + back, bCleanup := testBackendWithName(t) + defer bCleanup() + + a, err := back.StateMgr(testBackendSingleWorkspaceName) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + b, err := back.StateMgr(testBackendSingleWorkspaceName) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + lockerA, ok := a.(statemgr.Locker) + if !ok { + t.Fatal("client A not a statemgr.Locker") + } + + lockerB, ok := b.(statemgr.Locker) + if !ok { + t.Fatal("client B not a statemgr.Locker") + } + + infoA := statemgr.NewLockInfo() + infoA.Operation = "test" + infoA.Who = "clientA" + + infoB := statemgr.NewLockInfo() + infoB.Operation = "test" + infoB.Who = "clientB" + + lockIDA, err := lockerA.Lock(infoA) + if err != nil { + t.Fatal("unable to get initial lock:", err) + } + + _, err = lockerB.Lock(infoB) + if err == nil { + lockerA.Unlock(lockIDA) + t.Fatal("client B obtained lock while held by client A") + } + if _, ok := err.(*statemgr.LockError); !ok { + t.Errorf("expected a LockError, but was %t: %s", err, err) + } + + if err := lockerA.Unlock(lockIDA); err != nil { + t.Fatal("error unlocking client A", err) + } + + lockIDB, err := lockerB.Lock(infoB) + if err != nil { + t.Fatal("unable to obtain lock from client B") + } + + if lockIDB == lockIDA { + t.Fatalf("duplicate lock IDs: %q", lockIDB) + } + + if err = lockerB.Unlock(lockIDB); err != nil { + t.Fatal("error unlocking client B:", err) + } +} + +func TestDelete_SafeDeleteNotSupported(t *testing.T) { + state := testCloudState(t) + workspaceId := state.workspace.ID + state.workspace.Permissions.CanForceDelete = nil + state.workspace.ResourceCount = 5 + + // Typically delete(false) should safe-delete a cloud workspace, which should fail on this workspace with resources + // However, since we have set the workspace canForceDelete permission to nil, we should fall back to force delete + if err := state.Delete(false); err != nil { + t.Fatalf("delete: %s", err) + } + workspace, err := state.tfeClient.Workspaces.ReadByID(context.Background(), workspaceId) + if workspace != nil || err != tfe.ErrResourceNotFound { + t.Fatalf("workspace %s not deleted", workspaceId) + } +} + +func TestDelete_ForceDelete(t *testing.T) { + state := testCloudState(t) + workspaceId := state.workspace.ID + state.workspace.Permissions.CanForceDelete = tfe.Bool(true) + state.workspace.ResourceCount = 5 + + if err := state.Delete(true); err != nil { + t.Fatalf("delete: %s", err) + } + workspace, err := state.tfeClient.Workspaces.ReadByID(context.Background(), workspaceId) + if workspace != nil || err != tfe.ErrResourceNotFound { + t.Fatalf("workspace %s not deleted", workspaceId) + } +} + +func TestDelete_SafeDelete(t *testing.T) { + state := testCloudState(t) + workspaceId := state.workspace.ID + state.workspace.Permissions.CanForceDelete = tfe.Bool(false) + state.workspace.ResourceCount = 5 + + // safe-deleting a workspace with resources should fail + err := state.Delete(false) + if err == nil { + t.Fatalf("workspace should have failed to safe delete") + } + + // safe-deleting a workspace with resources should succeed once it has no resources + state.workspace.ResourceCount = 0 + if err = state.Delete(false); err != nil { + t.Fatalf("workspace safe-delete err: %s", err) + } + + workspace, err := state.tfeClient.Workspaces.ReadByID(context.Background(), workspaceId) + if workspace != nil || err != tfe.ErrResourceNotFound { + t.Fatalf("workspace %s not deleted", workspaceId) + } +} + +func TestState_PersistState(t *testing.T) { + t.Run("Initial PersistState", func(t *testing.T) { + cloudState := testCloudState(t) + + if cloudState.readState != nil { + t.Fatal("expected nil initial readState") + } + + err := cloudState.PersistState(nil) + if err != nil { + t.Fatalf("expected no error, got %q", err) + } + + var expectedSerial uint64 = 1 + if cloudState.readSerial != expectedSerial { + t.Fatalf("expected initial state readSerial to be %d, got %d", expectedSerial, cloudState.readSerial) + } + }) + + t.Run("Snapshot Interval Backpressure Header", func(t *testing.T) { + // The "Create a State Version" API is allowed to return a special + // HTTP response header X-Terraform-Snapshot-Interval, in which case + // we should remember the number of seconds it specifies and delay + // creating any more intermediate state snapshots for that many seconds. + + cloudState := testCloudState(t) + + if cloudState.stateSnapshotInterval != 0 { + t.Error("state manager already has a nonzero snapshot interval") + } + + if cloudState.enableIntermediateSnapshots { + t.Error("expected state manager to have disabled snapshots") + } + + // For this test we'll use a real client talking to a fake server, + // since HTTP-level concerns like headers are out of scope for the + // mock client we typically use in other tests in this package, which + // aim to abstract away HTTP altogether. + + // Didn't want to repeat myself here + for _, testCase := range []struct { + expectedInterval time.Duration + snapshotsEnabled bool + }{ + { + expectedInterval: 300 * time.Second, + snapshotsEnabled: true, + }, + { + expectedInterval: 0 * time.Second, + snapshotsEnabled: false, + }, + } { + server := testServerWithSnapshotsEnabled(t, testCase.snapshotsEnabled) + + defer server.Close() + cfg := &tfe.Config{ + Address: server.URL, + BasePath: "api", + Token: "placeholder", + } + client, err := tfe.NewClient(cfg) + if err != nil { + t.Fatal(err) + } + cloudState.tfeClient = client + + err = cloudState.RefreshState() + if err != nil { + t.Fatal(err) + } + cloudState.WriteState(states.BuildState(func(s *states.SyncState) { + s.SetOutputValue( + addrs.OutputValue{Name: "boop"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("beep"), false, + ) + })) + + err = cloudState.PersistState(nil) + if err != nil { + t.Fatal(err) + } + + // The PersistState call above should have sent a request to the test + // server and got back the x-terraform-snapshot-interval header, whose + // value should therefore now be recorded in the relevant field. + if got := cloudState.stateSnapshotInterval; got != testCase.expectedInterval { + t.Errorf("wrong state snapshot interval after PersistState\ngot: %s\nwant: %s", got, testCase.expectedInterval) + } + + if got, want := cloudState.enableIntermediateSnapshots, testCase.snapshotsEnabled; got != want { + t.Errorf("expected disable intermediate snapshots to be\ngot: %t\nwant: %t", got, want) + } + } + }) +} + +func TestState_ShouldPersistIntermediateState(t *testing.T) { + cloudState := testCloudState(t) + + testCases := []struct { + Enabled bool + LastPersist time.Time + Interval time.Duration + Expected bool + Force bool + Description string + }{ + { + Interval: 20 * time.Second, + Enabled: true, + Expected: true, + Description: "Not persisted yet", + }, + { + Interval: 20 * time.Second, + Enabled: false, + Expected: false, + Description: "Intermediate snapshots not enabled", + }, + { + Interval: 20 * time.Second, + Enabled: false, + Force: true, + Expected: true, + Description: "Force persist", + }, + { + Interval: 20 * time.Second, + LastPersist: time.Now().Add(-15 * time.Second), + Enabled: true, + Expected: false, + Description: "Last persisted 15s ago", + }, + { + Interval: 20 * time.Second, + LastPersist: time.Now().Add(-25 * time.Second), + Enabled: true, + Expected: true, + Description: "Last persisted 25s ago", + }, + { + Interval: 5 * time.Second, + LastPersist: time.Now().Add(-15 * time.Second), + Enabled: true, + Expected: true, + Description: "Last persisted 15s ago, but interval is 5s", + }, + } + + for _, testCase := range testCases { + cloudState.enableIntermediateSnapshots = testCase.Enabled + cloudState.stateSnapshotInterval = testCase.Interval + + actual := cloudState.ShouldPersistIntermediateState(&statemgr.IntermediateStatePersistInfo{ + LastPersist: testCase.LastPersist, + ForcePersist: testCase.Force, + }) + if actual != testCase.Expected { + t.Errorf("%s: expected %v but got %v", testCase.Description, testCase.Expected, actual) + } + } +} diff --git a/internal/cloud/test.go b/internal/cloud/test.go new file mode 100644 index 0000000000..3f97be0f8d --- /dev/null +++ b/internal/cloud/test.go @@ -0,0 +1,644 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "bufio" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/hashicorp/go-tfe" + tfaddr "github.com/hashicorp/terraform-registry-address" + svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/disco" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" + tfversion "github.com/hashicorp/terraform/version" +) + +// TestSuiteRunner executes any tests found in the relevant directories in TFC. +// +// It uploads the configuration and uses go-tfe to execute a . +// +// We keep this separate from Cloud, as the tests don't execute with a +// particular workspace in mind but instead with a specific module from a +// private registry. Many things within Cloud assume the existence of a +// workspace when initialising so it isn't pratical to share this for tests. +type TestSuiteRunner struct { + + // ConfigDirectory and TestingDirectory are the paths to the directory + // that contains our configuration and our testing files. + ConfigDirectory string + TestingDirectory string + + // Config is the actual loaded config. + Config *configs.Config + + Services *disco.Disco + + // Source is the private registry module we should be sending the tests + // to when they execute. + Source string + + // GlobalVariables are the variables provided by the TF_VAR_* environment + // variables and -var and -var-file flags. + GlobalVariables map[string]backendrun.UnparsedVariableValue + + // Stopped and Cancelled track whether the user requested the testing + // process to be interrupted. Stopped is a nice graceful exit, we'll still + // tidy up any state that was created and mark the tests with relevant + // `skipped` status updates. Cancelled is a hard stop right now exit, we + // won't attempt to clean up any state left hanging, and tests will just + // be left showing `pending` as the status. We will still print out the + // destroy summary diagnostics that tell the user what state has been left + // behind and needs manual clean up. + Stopped bool + Cancelled bool + + // StoppedCtx and CancelledCtx allow in progress Terraform operations to + // respond to external calls from the test command. + StoppedCtx context.Context + CancelledCtx context.Context + + // Verbose tells the runner to print out plan files during each test run. + Verbose bool + + // OperationParallelism is the limit Terraform places on total parallel operations + // during the plan or apply command within a single test run. + OperationParallelism int + + // Filters restricts which test files will be executed. + Filters []string + + // Renderer knows how to convert JSON logs retrieved from TFE back into + // human-readable. + // + // If this is nil, the runner will print the raw logs directly to Streams. + Renderer *jsonformat.Renderer + + // View and Streams provide alternate ways to output raw data to the + // user. + View views.Test + Streams *terminal.Streams + + // appName is the name of the instance this test suite runner is configured + // against. Can be "HCP Terraform" or "Terraform Enterprise" + appName string + + // clientOverride allows tests to specify the client instead of letting the + // system initialise one itself. + clientOverride *tfe.Client +} + +func (runner *TestSuiteRunner) Stop() { + runner.Stopped = true +} + +func (runner *TestSuiteRunner) IsStopped() bool { + return runner.Stopped +} + +func (runner *TestSuiteRunner) Cancel() { + runner.Cancelled = true +} + +func (runner *TestSuiteRunner) Test() (moduletest.Status, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + configDirectory, err := filepath.Abs(runner.ConfigDirectory) + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to get absolute path of the configuration directory: %v", err)) + return moduletest.Error, diags + } + + variables, variableDiags := ParseCloudRunTestVariables(runner.GlobalVariables) + diags = diags.Append(variableDiags) + if variableDiags.HasErrors() { + // Stop early if we couldn't parse the global variables. + return moduletest.Error, diags + } + + addr, err := tfaddr.ParseModuleSource(runner.Source) + if err != nil { + if parserError, ok := err.(*tfaddr.ParserError); ok { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + parserError.Summary, + parserError.Detail, + cty.Path{cty.GetAttrStep{Name: "source"}})) + } else { + diags = diags.Append(err) + } + return moduletest.Error, diags + } + + if addr.Package.Host == tfaddr.DefaultModuleRegistryHost { + // Then they've reference something from the public registry. We can't + // run tests against that in this way yet. + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Module source points to the public registry", + "HCP Terraform and Terraform Enterprise can only execute tests for modules held within private registries.", + cty.Path{cty.GetAttrStep{Name: "source"}})) + return moduletest.Error, diags + } + + id := tfe.RegistryModuleID{ + Organization: addr.Package.Namespace, + Name: addr.Package.Name, + Provider: addr.Package.TargetSystem, + Namespace: addr.Package.Namespace, + RegistryName: tfe.PrivateRegistry, + } + + client, module, clientDiags := runner.client(addr, id) + diags = diags.Append(clientDiags) + if clientDiags.HasErrors() { + return moduletest.Error, diags + } + + configurationVersion, err := client.ConfigurationVersions.CreateForRegistryModule(runner.StoppedCtx, id) + if err != nil { + diags = diags.Append(runner.generalError("Failed to create configuration version", err)) + return moduletest.Error, diags + } + + if runner.Stopped || runner.Cancelled { + return moduletest.Error, diags + } + + if err := client.ConfigurationVersions.Upload(runner.StoppedCtx, configurationVersion.UploadURL, configDirectory); err != nil { + diags = diags.Append(runner.generalError("Failed to upload configuration version", err)) + return moduletest.Error, diags + } + + if runner.Stopped || runner.Cancelled { + return moduletest.Error, diags + } + + // From here, we'll pass any cancellation signals into the test run instead + // of cancelling things locally. The reason for this is we want to make sure + // the test run tidies up any state properly. This means, we'll send the + // cancellation signals and then still wait for and process the logs. + // + // This also means that all calls to HCP Terraform will use context.Background() + // instead of the stopped or cancelled context as we want them to finish and + // the run to be cancelled by HCP Terraform properly. + + opts := tfe.TestRunCreateOptions{ + Filters: runner.Filters, + TestDirectory: tfe.String(runner.TestingDirectory), + Verbose: tfe.Bool(runner.Verbose), + Parallelism: tfe.Int(runner.OperationParallelism), + Variables: func() []*tfe.RunVariable { + runVariables := make([]*tfe.RunVariable, 0, len(variables)) + for name, value := range variables { + runVariables = append(runVariables, &tfe.RunVariable{ + Key: name, + Value: value, + }) + } + return runVariables + }(), + ConfigurationVersion: configurationVersion, + RegistryModule: module, + } + + run, err := client.TestRuns.Create(context.Background(), opts) + if err != nil { + diags = diags.Append(runner.generalError("Failed to create test run", err)) + return moduletest.Error, diags + } + + runningCtx, done := context.WithCancel(context.Background()) + + go func() { + defer logging.PanicHandler() + defer done() + + // Let's wait for the test run to start separately, so we can provide + // some nice updates while we wait. + + completed := false + started := time.Now() + updated := started + for i := 0; !completed; i++ { + run, err := client.TestRuns.Read(context.Background(), id, run.ID) + if err != nil { + diags = diags.Append(runner.generalError("Failed to retrieve test run", err)) + return // exit early + } + + if run.Status != tfe.TestRunQueued { + // We block as long as the test run is still queued. + completed = true + continue // We can render the logs now. + } + + current := time.Now() + if i == 0 || current.Sub(updated).Seconds() > 30 { + updated = current + + // TODO: Provide better updates based on queue status etc. + // We could look through the queue to find out exactly where the + // test run is and give a count down. Other stuff like that. + // For now, we'll just print a simple status updated. + + runner.View.TFCStatusUpdate(run.Status, current.Sub(started)) + } + } + + // The test run has actually started now, so let's render the logs. + + logDiags := runner.renderLogs(client, run, id) + diags = diags.Append(logDiags) + }() + + // We're doing a couple of things in the wait function. Firstly, waiting + // for the test run to actually finish. Secondly, listening for interrupt + // signals and forwarding them onto TFC. + waitDiags := runner.wait(runningCtx, client, run, id) + diags = diags.Append(waitDiags) + + if diags.HasErrors() { + return moduletest.Error, diags + } + + // Refresh the run now we know it is finished. + run, err = client.TestRuns.Read(context.Background(), id, run.ID) + if err != nil { + diags = diags.Append(runner.generalError("Failed to retrieve completed test run", err)) + return moduletest.Error, diags + } + + if run.Status != tfe.TestRunFinished { + // The only reason we'd get here without the run being finished properly + // is because the run errored outside the scope of the tests, or because + // the run was cancelled. Either way, we can just mark it has having + // errored for the purpose of our return code. + return moduletest.Error, diags + } + + // Otherwise the run has finished successfully, and we can look at the + // actual status of the test instead of the run to figure out what status we + // should return. + + switch run.TestStatus { + case tfe.TestError: + return moduletest.Error, diags + case tfe.TestFail: + return moduletest.Fail, diags + case tfe.TestPass: + return moduletest.Pass, diags + case tfe.TestPending: + return moduletest.Pending, diags + case tfe.TestSkip: + return moduletest.Skip, diags + default: + panic("found unrecognized test status: " + run.TestStatus) + } +} + +// discover the TFC/E API service URL +func discoverTfeURL(hostname svchost.Hostname, services *disco.Disco) (*url.URL, error) { + host, err := services.Discover(hostname) + if err != nil { + var serviceDiscoErr *disco.ErrServiceDiscoveryNetworkRequest + + switch { + case errors.As(err, &serviceDiscoErr): + err = fmt.Errorf("a network issue prevented cloud configuration; %w", err) + return nil, err + default: + return nil, err + } + } + + return host.ServiceURL(tfeServiceID) +} + +func (runner *TestSuiteRunner) client(addr tfaddr.Module, id tfe.RegistryModuleID) (*tfe.Client, *tfe.RegistryModule, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + var client *tfe.Client + if runner.clientOverride != nil { + client = runner.clientOverride + } else { + service, err := discoverTfeURL(addr.Package.Host, runner.Services) + if err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + strings.ToUpper(err.Error()[:1])+err.Error()[1:], + "", // no description is needed here, the error is clear + cty.Path{cty.GetAttrStep{Name: "hostname"}}, + )) + return nil, nil, diags + } + + token, err := cliConfigToken(addr.Package.Host, runner.Services) + if err != nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + strings.ToUpper(err.Error()[:1])+err.Error()[1:], + "", // no description is needed here, the error is clear + cty.Path{cty.GetAttrStep{Name: "hostname"}}, + )) + return nil, nil, diags + } + + if token == "" { + hostname := addr.Package.Host.ForDisplay() + + loginCommand := "terraform login" + if hostname != defaultHostname { + loginCommand = loginCommand + " " + hostname + } + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Required token could not be found", + fmt.Sprintf( + "Run the following command to generate a token for %s:\n %s", + hostname, + loginCommand, + ), + )) + return nil, nil, diags + } + + cfg := &tfe.Config{ + Address: service.String(), + BasePath: service.Path, + Token: token, + Headers: make(http.Header), + RetryLogHook: runner.View.TFCRetryHook, + } + + // Set the version header to the current version. + cfg.Headers.Set(tfversion.Header, tfversion.Version) + cfg.Headers.Set(headerSourceKey, headerSourceValue) + + if client, err = tfe.NewClient(cfg); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to create the HCP Terraform or Terraform Enterprise client", + fmt.Sprintf( + `Encountered an unexpected error while creating the `+ + `HCP Terraform or Terraform Enterprise client: %s.`, err, + ), + )) + return nil, nil, diags + } + } + + module, err := client.RegistryModules.Read(runner.StoppedCtx, id) + if err != nil { + // Then the module doesn't exist, and we can't run tests against it. + if err == tfe.ErrResourceNotFound { + err = fmt.Errorf("module %q was not found.\n\nPlease ensure that the organization and hostname are correct and that your API token for %s is valid.", addr.ForDisplay(), addr.Package.Host.ForDisplay()) + } + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + fmt.Sprintf("Failed to read module %q", addr.ForDisplay()), + fmt.Sprintf("Encountered an unexpected error while the module: %s", err), + cty.Path{cty.GetAttrStep{Name: "source"}})) + return client, nil, diags + } + + // Enable retries for server errors. + client.RetryServerErrors(true) + + runner.appName = client.AppName() + if isValidAppName(runner.appName) { + runner.appName = "HCP Terraform" + } + + // Aaaaand I'm done. + return client, module, diags +} + +func (runner *TestSuiteRunner) wait(ctx context.Context, client *tfe.Client, run *tfe.TestRun, moduleId tfe.RegistryModuleID) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + handleCancelled := func() { + if err := client.TestRuns.Cancel(context.Background(), moduleId, run.ID); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Could not cancel the test run", + fmt.Sprintf("Terraform could not cancel the test run, you will have to navigate to the %s console and cancel the test run manually.\n\nThe error message received when cancelling the test run was %s", client.AppName(), err))) + return + } + + // At this point we've requested a force cancel, and we know that + // Terraform locally is just going to quit after some amount of time so + // we'll just wait for that to happen or for HCP Terraform to finish, whichever + // happens first. + <-ctx.Done() + } + + handleStopped := func() { + if err := client.TestRuns.Cancel(context.Background(), moduleId, run.ID); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Could not stop the test run", + fmt.Sprintf("Terraform could not stop the test run, you will have to navigate to the %s console and cancel the test run manually.\n\nThe error message received when stopping the test run was %s", client.AppName(), err))) + return + } + + // We've request a cancel, we're happy to just wait for HCP Terraform to cancel + // the run appropriately. + select { + case <-runner.CancelledCtx.Done(): + // We got more pushy, let's force cancel. + handleCancelled() + case <-ctx.Done(): + // It finished normally after we request the cancel. Do nothing. + } + } + + select { + case <-runner.StoppedCtx.Done(): + // The StoppedCtx is passed in from the command package, which is + // listening for interrupts from the user. After the first interrupt the + // StoppedCtx is triggered. + handleStopped() + case <-ctx.Done(): + // The remote run finished normally! Do nothing. + } + + return diags +} + +func (runner *TestSuiteRunner) renderLogs(client *tfe.Client, run *tfe.TestRun, moduleId tfe.RegistryModuleID) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + logs, err := client.TestRuns.Logs(context.Background(), moduleId, run.ID) + if err != nil { + diags = diags.Append(runner.generalError("Failed to retrieve logs", err)) + return diags + } + + reader := bufio.NewReaderSize(logs, 64*1024) + + for next := true; next; { + var l, line []byte + var err error + + for isPrefix := true; isPrefix; { + l, isPrefix, err = reader.ReadLine() + if err != nil { + if err != io.EOF { + diags = diags.Append(runner.generalError("Failed to read logs", err)) + return diags + } + next = false + } + + line = append(line, l...) + } + + if next || len(line) > 0 { + + if runner.Renderer != nil { + log := jsonformat.JSONLog{} + if err := json.Unmarshal(line, &log); err != nil { + runner.Streams.Println(string(line)) // Just print the raw line so the user can still try and interpret the information. + continue + } + + // Most of the log types can be rendered with just the + // information they contain. We just pass these straight into + // the renderer. Others, however, need additional context that + // isn't available within the renderer so we process them first. + + switch log.Type { + case jsonformat.LogTestInterrupt: + interrupt := log.TestFatalInterrupt + + runner.Streams.Eprintln(format.WordWrap(log.Message, runner.Streams.Stderr.Columns())) + if len(interrupt.State) > 0 { + runner.Streams.Eprint(format.WordWrap("\nTerraform has already created the following resources from the module under test:\n", runner.Streams.Stderr.Columns())) + for _, resource := range interrupt.State { + if len(resource.DeposedKey) > 0 { + runner.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey) + } else { + runner.Streams.Eprintf(" - %s\n", resource.Instance) + } + } + } + + if len(interrupt.States) > 0 { + for run, resources := range interrupt.States { + runner.Streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform has already created the following resources for %q:\n", run), runner.Streams.Stderr.Columns())) + + for _, resource := range resources { + if len(resource.DeposedKey) > 0 { + runner.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey) + } else { + runner.Streams.Eprintf(" - %s\n", resource.Instance) + } + } + } + } + + if len(interrupt.Planned) > 0 { + module := "the module under test" + for _, run := range runner.Config.Module.Tests[log.TestFile].Runs { + if run.Name == log.TestRun && run.ConfigUnderTest != nil { + module = fmt.Sprintf("%q", run.Module.Source.String()) + } + } + + runner.Streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform was in the process of creating the following resources for %q from %s, and they may not have been destroyed:\n", log.TestRun, module), runner.Streams.Stderr.Columns())) + for _, resource := range interrupt.Planned { + runner.Streams.Eprintf(" - %s\n", resource) + } + } + + case jsonformat.LogTestPlan: + var uimode plans.Mode + for _, run := range runner.Config.Module.Tests[log.TestFile].Runs { + if run.Name == log.TestRun { + switch run.Options.Mode { + case configs.RefreshOnlyTestMode: + uimode = plans.RefreshOnlyMode + case configs.NormalTestMode: + uimode = plans.NormalMode + } + + // Don't keep searching the runs. + break + } + } + runner.Renderer.RenderHumanPlan(*log.TestPlan, uimode) + + case jsonformat.LogTestState: + runner.Renderer.RenderHumanState(*log.TestState) + + default: + // For all the rest we can just hand over to the renderer + // to handle directly. + if err := runner.Renderer.RenderLog(&log); err != nil { + runner.Streams.Println(string(line)) // Just print the raw line so the can still try and interpret the information. + continue + } + } + + } else { + runner.Streams.Println(string(line)) // If the renderer is null, it means the user just wants to see the raw JSON outputs anyway. + } + } + } + + return diags +} + +func (runner *TestSuiteRunner) generalError(msg string, err error) error { + var diags tfdiags.Diagnostics + + if urlErr, ok := err.(*url.Error); ok { + err = urlErr.Err + } + + switch err { + case context.Canceled: + return err + case tfe.ErrResourceNotFound: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + fmt.Sprintf("%s: %v", msg, err), + fmt.Sprintf("For security, %s return '404 Not Found' responses for resources\n", runner.appName)+ + "for resources that a user doesn't have access to, in addition to resources that\n"+ + "do not exist. If the resource does exist, please check the permissions of the provided token.", + )) + return diags.Err() + default: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + fmt.Sprintf("%s: %v", msg, err), + fmt.Sprintf(`%s returned an unexpected error. Sometimes `, runner.appName)+ + `this is caused by network connection problems, in which case you could retry `+ + `the command. If the issue persists please open a support ticket to get help `+ + `resolving the problem.`, + )) + return diags.Err() + } +} diff --git a/internal/cloud/test_test.go b/internal/cloud/test_test.go new file mode 100644 index 0000000000..e33cec14df --- /dev/null +++ b/internal/cloud/test_test.go @@ -0,0 +1,1008 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloud + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-tfe" + + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestTest(t *testing.T) { + + streams, done := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := done(t) + actual := output.All() + expected := `main.tftest.hcl... in progress + defaults... pass + overrides... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 2 passed, 0 failed. +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} + +func TestTest_Parallelism(t *testing.T) { + + streams, _ := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + OperationParallelism: 4, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + if mock.TestRuns.parallelism != 4 { + t.Errorf("expected parallelism to be 4 but was %d", mock.TestRuns.parallelism) + } +} + +func TestTest_JSON(t *testing.T) { + + streams, done := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: nil, // This should force the logs to render as JSON. + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := done(t) + actual := output.All() + expected := `{"@level":"info","@message":"Terraform 1.6.0-dev","@module":"terraform.ui","@timestamp":"2023-09-12T08:29:27.257413+02:00","terraform":"1.6.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 2 run blocks","@module":"terraform.ui","@timestamp":"2023-09-12T08:29:27.268731+02:00","test_abstract":{"main.tftest.hcl":["defaults","overrides"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:29:27.268889+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":" \"defaults\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T08:29:27.710541+02:00","test_run":{"path":"main.tftest.hcl","run":"defaults","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":" \"overrides\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T08:29:27.833351+02:00","test_run":{"path":"main.tftest.hcl","run":"overrides","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:29:27.833375+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:29:27.956488+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"} +{"@level":"info","@message":"Success! 2 passed, 0 failed.","@module":"terraform.ui","@timestamp":"2023-09-12T08:29:27.956510+02:00","test_summary":{"status":"pass","passed":2,"failed":0,"errored":0,"skipped":0},"type":"test_summary"} +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} + +func TestTest_Verbose(t *testing.T) { + + directory := "testdata/test-verbose" + + loader, close := configload.NewLoaderForTests(t) + defer close() + + config, configDiags := loader.LoadConfigWithTests(directory, "tests") + if configDiags.HasErrors() { + t.Fatalf("failed to load config: %v", configDiags.Error()) + } + + streams, done := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: directory, + TestingDirectory: "tests", + Config: config, + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // The test options don't actually matter, as we just retrieve whatever + // is set in the log file. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := done(t) + actual := output.All() + expected := `main.tftest.hcl... in progress + defaults... pass + +Changes to Outputs: + + input = "Hello, world!" + +You can apply this plan to save these new output values to the Terraform +state, without changing any real infrastructure. +╷ +│ Warning: Deprecated +│ +│ with data.null_data_source.values, +│ on main.tf line 7, in data "null_data_source" "values": +│ 7: data "null_data_source" "values" { +│ +│ The null_data_source was historically used to construct intermediate values +│ to re-use elsewhere in configuration, the same can now be achieved using +│ locals +╵ +╷ +│ Warning: Deprecated +│ +│ with data.null_data_source.values, +│ on main.tf line 7, in data "null_data_source" "values": +│ 7: data "null_data_source" "values" { +│ +│ The null_data_source was historically used to construct intermediate values +│ to re-use elsewhere in configuration, the same can now be achieved using +│ locals +╵ + overrides... pass +# data.null_data_source.values: +data "null_data_source" "values" { + has_computed_default = "default" + id = "static" + inputs = { + "data" = "Hello, universe!" + } + outputs = { + "data" = "Hello, universe!" + } + random = "8484833523059069761" +} + + +Outputs: + +input = "Hello, universe!" +╷ +│ Warning: Deprecated +│ +│ with data.null_data_source.values, +│ on main.tf line 7, in data "null_data_source" "values": +│ 7: data "null_data_source" "values" { +│ +│ The null_data_source was historically used to construct intermediate values +│ to re-use elsewhere in configuration, the same can now be achieved using +│ locals +╵ +╷ +│ Warning: Deprecated +│ +│ with data.null_data_source.values, +│ on main.tf line 7, in data "null_data_source" "values": +│ 7: data "null_data_source" "values" { +│ +│ The null_data_source was historically used to construct intermediate values +│ to re-use elsewhere in configuration, the same can now be achieved using +│ locals +╵ +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 2 passed, 0 failed. +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} + +func TestTest_Cancel(t *testing.T) { + + streams, outputFn := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + module, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }) + if err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + doneContext, done := context.WithCancel(context.Background()) + stopContext, stop := context.WithCancel(context.Background()) + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test-cancel", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: stopContext, + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + // We're only going to be able to finish this if the cancellation calls + // are done correctly. + mock.TestRuns.targetCancels = 1 + + var diags tfdiags.Diagnostics + go func() { + defer done() + _, diags = runner.Test() + }() + + stop() // immediately cancel + + // Wait for finish! + <-doneContext.Done() + + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := outputFn(t) + actual := output.All() + expected := `main.tftest.hcl... in progress + +Interrupt received. +Please wait for Terraform to exit or data loss may occur. +Gracefully shutting down... + + defaults... pass + overrides... skip +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 1 passed, 0 failed, 1 skipped. +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + // We want to make sure the cancel signal actually made it through. + // Luckily we can access the test runs directly in the mock client. + tr := mock.TestRuns.modules[module.ID][0] + if tr.Status != tfe.TestRunCanceled { + t.Errorf("expected test run to have been cancelled but was %s", tr.Status) + } + + if mock.TestRuns.cancels != 1 { + t.Errorf("incorrect number of cancels, expected 1 but was %d", mock.TestRuns.cancels) + } +} + +// TestTest_DelayedCancel just makes sure that if we trigger the cancellation +// during the log reading stage then it still cancels properly. +func TestTest_DelayedCancel(t *testing.T) { + + streams, outputFn := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + module, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }) + if err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + doneContext, done := context.WithCancel(context.Background()) + stopContext, stop := context.WithCancel(context.Background()) + + mock.TestRuns.delayedCancel = stop + + // We're only going to be able to finish this if the cancellation calls + // are done correctly. + mock.TestRuns.targetCancels = 1 + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test-cancel", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: stopContext, + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + var diags tfdiags.Diagnostics + go func() { + defer done() + _, diags = runner.Test() + }() + + // Wait for finish! + <-doneContext.Done() + + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := outputFn(t) + actual := output.All() + expected := `main.tftest.hcl... in progress + +Interrupt received. +Please wait for Terraform to exit or data loss may occur. +Gracefully shutting down... + + defaults... pass + overrides... skip +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 1 passed, 0 failed, 1 skipped. +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + // We want to make sure the cancel signal actually made it through. + // Luckily we can access the test runs directly in the mock client. + tr := mock.TestRuns.modules[module.ID][0] + if tr.Status != tfe.TestRunCanceled { + t.Errorf("expected test run to have been cancelled but was %s", tr.Status) + } +} + +func TestTest_ForceCancel(t *testing.T) { + + loader, close := configload.NewLoaderForTests(t) + defer close() + + config, configDiags := loader.LoadConfigWithTests("testdata/test-force-cancel", "tests") + if configDiags.HasErrors() { + t.Fatalf("failed to load config: %v", configDiags.Error()) + } + + streams, outputFn := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + module, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }) + if err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + doneContext, done := context.WithCancel(context.Background()) + stopContext, stop := context.WithCancel(context.Background()) + cancelContext, cancel := context.WithCancel(context.Background()) + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test-force-cancel", + TestingDirectory: "tests", + Config: config, + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: stopContext, + CancelledCtx: cancelContext, + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + // We're only going to be able to finish this if the cancellation calls + // are done correctly. + mock.TestRuns.targetCancels = 2 + + var diags tfdiags.Diagnostics + go func() { + defer done() + _, diags = runner.Test() + }() + + stop() + cancel() + + // Wait for finish! + <-doneContext.Done() + + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := outputFn(t) + + expectedErr := `Terraform was interrupted during test execution, and may not have performed +the expected cleanup operations. + +Terraform was in the process of creating the following resources for +"overrides" from the module under test, and they may not have been destroyed: + - time_sleep.wait_5_seconds + - tfcoremock_simple_resource.resource +` + actualErr := output.Stderr() + if diff := cmp.Diff(expectedErr, actualErr); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + actualOut := output.Stdout() + expectedOut := `main.tftest.hcl... in progress + defaults... pass + +Interrupt received. +Please wait for Terraform to exit or data loss may occur. +Gracefully shutting down... + + +Two interrupts received. Exiting immediately. Note that data loss may have occurred. + + overrides... fail +╷ +│ Error: Test interrupted +│ +│ The test operation could not be completed due to an interrupt signal. +│ Please read the remaining diagnostics carefully for any sign of failed +│ state cleanup or dangling resources. +╵ +╷ +│ Error: Create time sleep error +│ +│ with time_sleep.wait_5_seconds, +│ on main.tf line 7, in resource "time_sleep" "wait_5_seconds": +│ 7: resource "time_sleep" "wait_5_seconds" { +│ +│ Original Error: context canceled +╵ +╷ +│ Error: execution halted +│ +╵ +╷ +│ Error: execution halted +│ +╵ +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 1 passed, 1 failed. +` + + if diff := cmp.Diff(expectedOut, actualOut); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + // We want to make sure the cancel signal actually made it through. + // Luckily we can access the test runs directly in the mock client. + tr := mock.TestRuns.modules[module.ID][0] + if tr.Status != tfe.TestRunCanceled { + t.Errorf("expected test run to have been cancelled but was %s", tr.Status) + } + + if mock.TestRuns.cancels != 2 { + t.Errorf("incorrect number of cancels, expected 2 but was %d", mock.TestRuns.cancels) + } +} + +func TestTest_LongRunningTest(t *testing.T) { + + streams, done := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test-long-running", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: &jsonformat.Renderer{ + Streams: streams, + Colorize: colorize, + RunningInAutomation: false, + }, + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := done(t) + actual := output.All() + + // The long running test logs actually contain additional progress updates, + // but this test should ignore them and just show the usual output. + + expected := `main.tftest.hcl... in progress + just_go... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 1 passed, 0 failed. +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} + +func TestTest_LongRunningTestJSON(t *testing.T) { + + streams, done := terminal.StreamsForTesting(t) + view := views.NewTest(arguments.ViewHuman, views.NewView(streams)) + + colorize := mockColorize() + colorize.Disable = true + + mock := NewMockClient() + client := &tfe.Client{ + ConfigurationVersions: mock.ConfigurationVersions, + Organizations: mock.Organizations, + RegistryModules: mock.RegistryModules, + TestRuns: mock.TestRuns, + } + + if _, err := client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{ + Name: tfe.String("organisation"), + }); err != nil { + t.Fatalf("failed to create organisation: %v", err) + } + + if _, err := client.RegistryModules.Create(context.Background(), "organisation", tfe.RegistryModuleCreateOptions{ + Name: tfe.String("name"), + Provider: tfe.String("provider"), + RegistryName: "app.terraform.io", + Namespace: "organisation", + }); err != nil { + t.Fatalf("failed to create registry module: %v", err) + } + + runner := TestSuiteRunner{ + // Configuration data. + ConfigDirectory: "testdata/test-long-running", + TestingDirectory: "tests", + Config: nil, // We don't need this for this test. + Source: "app.terraform.io/organisation/name/provider", + + // Cancellation controls, we won't be doing any cancellations in this + // test. + Stopped: false, + Cancelled: false, + StoppedCtx: context.Background(), + CancelledCtx: context.Background(), + + // Test Options, empty for this test. + GlobalVariables: nil, + Verbose: false, + Filters: nil, + + // Outputs + Renderer: nil, // This should force the logs to render as JSON. + View: view, + Streams: streams, + + // Networking + Services: nil, // Don't need this when the client is overridden. + clientOverride: client, + } + + _, diags := runner.Test() + if len(diags) > 0 { + t.Errorf("found diags and expected none: %s", diags.ErrWithWarnings()) + } + + output := done(t) + actual := output.All() + + // This test should still include the progress updates as we're doing the + // JSON output. + + expected := `{"@level":"info","@message":"Terraform 1.7.0-dev","@module":"terraform.ui","@timestamp":"2023-09-28T14:57:09.175210+02:00","terraform":"1.7.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 1 run block","@module":"terraform.ui","@timestamp":"2023-09-28T14:57:09.189212+02:00","test_abstract":{"main.tftest.hcl":["just_go"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-28T14:57:09.189386+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":" \"just_go\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:09.189429+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"starting","elapsed":0},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:11.341278+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"running","elapsed":2152},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:13.343465+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"running","elapsed":4154},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:14.381552+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-28T14:57:14.381655+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":" \"just_go\"... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:14.381712+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"teardown","elapsed":0},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:16.477705+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"teardown","elapsed":2096},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-28T14:57:17.517309+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"} +{"@level":"info","@message":"Success! 1 passed, 0 failed.","@module":"terraform.ui","@timestamp":"2023-09-28T14:57:17.517494+02:00","test_summary":{"status":"pass","passed":1,"failed":0,"errored":0,"skipped":0},"type":"test_summary"} +` + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} diff --git a/internal/cloud/testdata/apply-json-with-error/main.tf b/internal/cloud/testdata/apply-json-with-error/main.tf new file mode 100644 index 0000000000..6fa9534f48 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-error/main.tf @@ -0,0 +1,5 @@ +resource "null_resource" "foo" { + triggers = { + random = "${guid()}" + } +} diff --git a/internal/cloud/testdata/apply-json-with-error/plan-redacted.json b/internal/cloud/testdata/apply-json-with-error/plan-redacted.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-error/plan-redacted.json @@ -0,0 +1 @@ +{} diff --git a/internal/cloud/testdata/apply-json-with-error/plan.log b/internal/cloud/testdata/apply-json-with-error/plan.log new file mode 100644 index 0000000000..b877f1e583 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-error/plan.log @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T12:12:25.477403-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"error","@message":"Error: Unsupported block type","@module":"terraform.ui","@timestamp":"2023-01-20T12:12:25.615995-05:00","diagnostic":{"severity":"error","summary":"Unsupported block type","detail":"Blocks of type \"triggers\" are not expected here. Did you mean to define argument \"triggers\"? If so, use the equals sign to assign it a value.","range":{"filename":"main.tf","start":{"line":2,"column":3,"byte":35},"end":{"line":2,"column":11,"byte":43}},"snippet":{"context":"resource \"null_resource\" \"foo\"","code":" triggers {","start_line":2,"highlight_start_offset":2,"highlight_end_offset":10,"values":[]}},"type":"diagnostic"} diff --git a/internal/cloud/testdata/apply-json-with-outputs/apply.log b/internal/cloud/testdata/apply-json-with-outputs/apply.log new file mode 100644 index 0000000000..fe26806895 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-outputs/apply.log @@ -0,0 +1,5 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:14.916732Z","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Creating...","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:16.390332Z","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"apply_start"} +{"@level":"info","@message":"null_resource.foo: Creation complete after 0s [id=7091618264040236234]","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:16.391654Z","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create","id_key":"id","id_value":"7091618264040236234","elapsed_seconds":0},"type":"apply_complete"} +{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:16.992073Z","changes":{"add":1,"change":0,"remove":0,"operation":"apply"},"type":"change_summary"} +{"@level":"info","@message":"Outputs: 3","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:16.992183Z","outputs":{"complex":{"sensitive":false,"type":["object",{"keyA":["object",{"someList":["tuple",["number","number","number"]]}],"keyB":["object",{"someBool":"bool","someStr":"string"}]}],"value":{"keyA":{"someList":[1,2,3]},"keyB":{"someBool":true,"someStr":"hello"}}},"secret":{"sensitive":true,"type":"string","value":"my-secret"},"simple":{"sensitive":false,"type":["tuple",["string","string"]],"value":["some","list"]}},"type":"outputs"} diff --git a/internal/cloud/testdata/apply-json-with-outputs/main.tf b/internal/cloud/testdata/apply-json-with-outputs/main.tf new file mode 100644 index 0000000000..d801668459 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-outputs/main.tf @@ -0,0 +1,22 @@ +resource "null_resource" "foo" {} + +output "simple" { + value = ["some", "list"] +} + +output "secret" { + value = "my-secret" + sensitive = true +} + +output "complex" { + value = { + keyA = { + someList = [1, 2, 3] + } + keyB = { + someBool = true + someStr = "hello" + } + } +} diff --git a/internal/cloud/testdata/apply-json-with-outputs/plan-redacted.json b/internal/cloud/testdata/apply-json-with-outputs/plan-redacted.json new file mode 100644 index 0000000000..71932744fa --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-outputs/plan-redacted.json @@ -0,0 +1,162 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": { + "complex": { + "actions": [ + "create" + ], + "before": null, + "after": { + "keyA": { + "someList": [ + 1, + 2, + 3 + ] + }, + "keyB": { + "someBool": true, + "someStr": "hello" + } + }, + "after_unknown": false, + "before_sensitive": false, + "after_sensitive": false + }, + "secret": { + "actions": [ + "create" + ], + "before": null, + "after": "8517896e47af3c9ca19a694ea0d6cc30b0dccf08598f33d93e583721fd5f3032", + "after_unknown": false, + "before_sensitive": true, + "after_sensitive": true + }, + "simple": { + "actions": [ + "create" + ], + "before": null, + "after": [ + "some", + "list" + ], + "after_unknown": false, + "before_sensitive": false, + "after_sensitive": false + } + }, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/apply-json-with-outputs/plan.log b/internal/cloud/testdata/apply-json-with-outputs/plan.log new file mode 100644 index 0000000000..357586ac3e --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-outputs/plan.log @@ -0,0 +1,6 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:02.177699Z","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:03.842915Z","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:03.842951Z","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} +{"@level":"info","@message":"Outputs: 3","@module":"terraform.ui","@timestamp":"2023-01-20T21:13:03.842965Z","outputs":{"complex":{"sensitive":false,"action":"create"},"secret":{"sensitive":true,"action":"create"},"simple":{"sensitive":false,"action":"create"}},"type":"outputs"} + + diff --git a/internal/cloud/testdata/apply-json-with-provisioner-error/apply.log b/internal/cloud/testdata/apply-json-with-provisioner-error/apply.log new file mode 100644 index 0000000000..64f949fb8e --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner-error/apply.log @@ -0,0 +1,9 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.623068-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Destroying... [id=5383176453498935794]","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.725584-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"delete","id_key":"id","id_value":"5383176453498935794"},"type":"apply_start"} +{"@level":"info","@message":"null_resource.foo: Destruction complete after 0s","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.728526-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"delete","elapsed_seconds":0},"type":"apply_complete"} +{"@level":"info","@message":"null_resource.foo: Creating...","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.745016-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"apply_start"} +{"@level":"info","@message":"null_resource.foo: Provisioning with 'local-exec'...","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.748796-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec"},"type":"provision_start"} +{"@level":"info","@message":"null_resource.foo: (local-exec): Executing: [\"/bin/sh\" \"-c\" \"exit 125\"]","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.749082-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec","output":"Executing: [\"/bin/sh\" \"-c\" \"exit 125\"]"},"type":"provision_progress"} +{"@level":"info","@message":"null_resource.foo: (local-exec) Provisioning errored","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.751770-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec"},"type":"provision_errored"} +{"@level":"info","@message":"null_resource.foo: Creation errored after 0s","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.752082-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create","elapsed_seconds":0},"type":"apply_errored"} +{"@level":"error","@message":"Error: local-exec provisioner error","@module":"terraform.ui","@timestamp":"2023-02-16T10:13:14.761681-05:00","diagnostic":{"severity":"error","summary":"local-exec provisioner error","detail":"Error running command 'exit 125': exit status 125. Output: ","address":"null_resource.foo","range":{"filename":"main.tf","start":{"line":2,"column":28,"byte":60},"end":{"line":2,"column":29,"byte":61}},"snippet":{"context":"resource \"null_resource\" \"foo\"","code":" provisioner \"local-exec\" {","start_line":2,"highlight_start_offset":27,"highlight_end_offset":28,"values":[]}},"type":"diagnostic"} diff --git a/internal/cloud/testdata/apply-json-with-provisioner-error/main.tf b/internal/cloud/testdata/apply-json-with-provisioner-error/main.tf new file mode 100644 index 0000000000..fb1ce036c0 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner-error/main.tf @@ -0,0 +1,5 @@ +resource "null_resource" "foo" { + provisioner "local-exec" { + command = "exit 125" + } +} diff --git a/internal/cloud/testdata/apply-json-with-provisioner-error/plan-redacted.json b/internal/cloud/testdata/apply-json-with-provisioner-error/plan-redacted.json new file mode 100644 index 0000000000..935799605f --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner-error/plan-redacted.json @@ -0,0 +1,116 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/apply-json-with-provisioner-error/plan.log b/internal/cloud/testdata/apply-json-with-provisioner-error/plan.log new file mode 100644 index 0000000000..26d39210b7 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner-error/plan.log @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.623068-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.822722-05:00","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.822787-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/cloud/testdata/apply-json-with-provisioner/apply.log b/internal/cloud/testdata/apply-json-with-provisioner/apply.log new file mode 100644 index 0000000000..78acd7862e --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner/apply.log @@ -0,0 +1,10 @@ +{"@level":"info","@message":"null_resource.foo: Destroying... [id=102500065134967380]","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.614616-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"delete","id_key":"id","id_value":"102500065134967380"},"type":"apply_start"} +{"@level":"info","@message":"null_resource.foo: Destruction complete after 0s","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.615777-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"delete","elapsed_seconds":0},"type":"apply_complete"} +{"@level":"info","@message":"null_resource.foo: Creating...","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.621975-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"apply_start"} +{"@level":"info","@message":"null_resource.foo: Provisioning with 'local-exec'...","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.622630-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec"},"type":"provision_start"} +{"@level":"info","@message":"null_resource.foo: (local-exec): Executing: [\"/bin/sh\" \"-c\" \"echo Hello World!\"]","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.622702-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec","output":"Executing: [\"/bin/sh\" \"-c\" \"echo Hello World!\"]"},"type":"provision_progress"} +{"@level":"info","@message":"null_resource.foo: (local-exec): Hello World!","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.623236-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec","output":"Hello World!"},"type":"provision_progress"} +{"@level":"info","@message":"null_resource.foo: (local-exec) Provisioning complete","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.623275-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"provisioner":"local-exec"},"type":"provision_complete"} +{"@level":"info","@message":"null_resource.foo: Creation complete after 0s [id=7836952171100801169]","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.623320-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create","id_key":"id","id_value":"7836952171100801169","elapsed_seconds":0},"type":"apply_complete"} +{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 1 destroyed.","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.631098-05:00","changes":{"add":1,"change":0,"remove":1,"operation":"apply"},"type":"change_summary"} +{"@level":"info","@message":"Outputs: 0","@module":"terraform.ui","@timestamp":"2023-02-16T10:15:39.631112-05:00","outputs":{},"type":"outputs"} diff --git a/internal/cloud/testdata/apply-json-with-provisioner/main.tf b/internal/cloud/testdata/apply-json-with-provisioner/main.tf new file mode 100644 index 0000000000..20bf745ad4 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner/main.tf @@ -0,0 +1,5 @@ +resource "null_resource" "foo" { + provisioner "local-exec" { + command = "echo Hello World!" + } +} diff --git a/internal/cloud/testdata/apply-json-with-provisioner/plan-redacted.json b/internal/cloud/testdata/apply-json-with-provisioner/plan-redacted.json new file mode 100644 index 0000000000..935799605f --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner/plan-redacted.json @@ -0,0 +1,116 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/apply-json-with-provisioner/plan.log b/internal/cloud/testdata/apply-json-with-provisioner/plan.log new file mode 100644 index 0000000000..26d39210b7 --- /dev/null +++ b/internal/cloud/testdata/apply-json-with-provisioner/plan.log @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.623068-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.822722-05:00","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.822787-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/cloud/testdata/apply-json/apply.log b/internal/cloud/testdata/apply-json/apply.log new file mode 100644 index 0000000000..1238b4c9ff --- /dev/null +++ b/internal/cloud/testdata/apply-json/apply.log @@ -0,0 +1,5 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.623068-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Creating...","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.874882-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"apply_start"} +{"@level":"info","@message":"null_resource.foo: Creation complete after 0s [id=3573948886993018026]","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.878389-05:00","hook":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create","id_key":"id","id_value":"3573948886993018026","elapsed_seconds":0},"type":"apply_complete"} +{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.887223-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"apply"},"type":"change_summary"} +{"@level":"info","@message":"Outputs: 0","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.887259-05:00","outputs":{},"type":"outputs"} diff --git a/internal/cloud/testdata/apply-json/main.tf b/internal/cloud/testdata/apply-json/main.tf new file mode 100644 index 0000000000..3911a2a9b2 --- /dev/null +++ b/internal/cloud/testdata/apply-json/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/internal/cloud/testdata/apply-json/plan-redacted.json b/internal/cloud/testdata/apply-json/plan-redacted.json new file mode 100644 index 0000000000..935799605f --- /dev/null +++ b/internal/cloud/testdata/apply-json/plan-redacted.json @@ -0,0 +1,116 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/apply-json/plan.log b/internal/cloud/testdata/apply-json/plan.log new file mode 100644 index 0000000000..3ac5e43fcf --- /dev/null +++ b/internal/cloud/testdata/apply-json/plan.log @@ -0,0 +1,4 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.623068-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.822722-05:00","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-20T15:50:04.822787-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} + diff --git a/internal/cloud/testdata/plan-bookmark/bookmark.json b/internal/cloud/testdata/plan-bookmark/bookmark.json new file mode 100644 index 0000000000..0a1c73302a --- /dev/null +++ b/internal/cloud/testdata/plan-bookmark/bookmark.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 1, + "run_id": "run-GXfuHMkbyHccAGUg", + "hostname": "app.terraform.io" +} diff --git a/internal/cloud/testdata/plan-import-config-gen-exists/generated.tf b/internal/cloud/testdata/plan-import-config-gen-exists/generated.tf new file mode 100644 index 0000000000..1efdb231af --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen-exists/generated.tf @@ -0,0 +1,8 @@ +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. + +# __generated__ by Terraform from "bar" +resource "terraform_data" "foo" { + input = null + triggers_replace = null +} diff --git a/internal/cloud/testdata/plan-import-config-gen-exists/main.tf b/internal/cloud/testdata/plan-import-config-gen-exists/main.tf new file mode 100644 index 0000000000..8257ac5af6 --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen-exists/main.tf @@ -0,0 +1,4 @@ +import { + id = "bar" + to = terraform_data.foo +} diff --git a/internal/cloud/testdata/plan-import-config-gen-validation-error/generated.tf.expected b/internal/cloud/testdata/plan-import-config-gen-validation-error/generated.tf.expected new file mode 100644 index 0000000000..1efdb231af --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen-validation-error/generated.tf.expected @@ -0,0 +1,8 @@ +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. + +# __generated__ by Terraform from "bar" +resource "terraform_data" "foo" { + input = null + triggers_replace = null +} diff --git a/internal/cloud/testdata/plan-import-config-gen-validation-error/main.tf b/internal/cloud/testdata/plan-import-config-gen-validation-error/main.tf new file mode 100644 index 0000000000..8257ac5af6 --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen-validation-error/main.tf @@ -0,0 +1,4 @@ +import { + id = "bar" + to = terraform_data.foo +} diff --git a/internal/cloud/testdata/plan-import-config-gen-validation-error/plan-redacted.json b/internal/cloud/testdata/plan-import-config-gen-validation-error/plan-redacted.json new file mode 100644 index 0000000000..9e24e22517 --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen-validation-error/plan-redacted.json @@ -0,0 +1,127 @@ +{ + "format_version": "1.2", + "terraform_version": "1.5.0", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "terraform_data.foo", + "mode": "managed", + "type": "terraform_data", + "name": "foo", + "provider_name": "terraform.io/builtin/terraform", + "schema_version": 0, + "values": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "terraform_data.foo", + "mode": "managed", + "type": "terraform_data", + "name": "foo", + "provider_name": "terraform.io/builtin/terraform", + "change": { + "actions": [ + "no-op" + ], + "before": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "after": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "after_unknown": {}, + "before_sensitive": {}, + "after_sensitive": {}, + "importing": { + "id": "bar" + }, + "generated_config": "resource \"terraform_data\" \"foo\" {\n input = null\n triggers_replace = null\n}" + } + } + ], + "prior_state": { + "format_version": "1.0", + "terraform_version": "1.6.0", + "values": { + "root_module": { + "resources": [ + { + "address": "terraform_data.foo", + "mode": "managed", + "type": "terraform_data", + "name": "foo", + "provider_name": "terraform.io/builtin/terraform", + "schema_version": 0, + "values": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "sensitive_values": {} + } + ] + } + } + }, + "configuration": { + "provider_config": { + "terraform": { + "name": "terraform", + "full_name": "terraform.io/builtin/terraform" + } + }, + "root_module": {} + }, + "provider_schemas": { + "terraform.io/builtin/terraform": { + "resource_schemas": { + "terraform_data": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "input": { + "type": "dynamic", + "description_kind": "plain", + "optional": true + }, + "output": { + "type": "dynamic", + "description_kind": "plain", + "computed": true + }, + "triggers_replace": { + "type": "dynamic", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + } + } + }, + "timestamp": "2023-05-30T03:34:55Z" +} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-import-config-gen-validation-error/plan.log b/internal/cloud/testdata/plan-import-config-gen-validation-error/plan.log new file mode 100644 index 0000000000..192b2b801a --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen-validation-error/plan.log @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.5.0","@module":"terraform.ui","@timestamp":"2023-05-29T21:30:07.206963-07:00","terraform":"1.5.0","type":"version","ui":"1.1"} +{"@level":"info","@message":"Plan: 0 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-05-29T21:30:08.302799-07:00","changes":{"add":0,"change":0,"import":0,"remove":0,"operation":"plan"},"type":"change_summary"} +{"@level":"error","@message":"Error: Conflicting configuration arguments","@module":"terraform.ui","@timestamp":"2023-05-29T21:30:08.302847-07:00","diagnostic":{"severity":"error","summary":"Conflicting configuration arguments","detail":"Not allowed","address":"terraform_data.foo","range":{"filename":"generated.tf","start":{"line":22,"column":33,"byte":867},"end":{"line":22,"column":35,"byte":869}}},"type":"diagnostic"} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-import-config-gen/generated.tf.expected b/internal/cloud/testdata/plan-import-config-gen/generated.tf.expected new file mode 100644 index 0000000000..1efdb231af --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen/generated.tf.expected @@ -0,0 +1,8 @@ +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. + +# __generated__ by Terraform from "bar" +resource "terraform_data" "foo" { + input = null + triggers_replace = null +} diff --git a/internal/cloud/testdata/plan-import-config-gen/main.tf b/internal/cloud/testdata/plan-import-config-gen/main.tf new file mode 100644 index 0000000000..8257ac5af6 --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen/main.tf @@ -0,0 +1,4 @@ +import { + id = "bar" + to = terraform_data.foo +} diff --git a/internal/cloud/testdata/plan-import-config-gen/plan-redacted.json b/internal/cloud/testdata/plan-import-config-gen/plan-redacted.json new file mode 100644 index 0000000000..9e24e22517 --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen/plan-redacted.json @@ -0,0 +1,127 @@ +{ + "format_version": "1.2", + "terraform_version": "1.5.0", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "terraform_data.foo", + "mode": "managed", + "type": "terraform_data", + "name": "foo", + "provider_name": "terraform.io/builtin/terraform", + "schema_version": 0, + "values": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "sensitive_values": {} + } + ] + } + }, + "resource_changes": [ + { + "address": "terraform_data.foo", + "mode": "managed", + "type": "terraform_data", + "name": "foo", + "provider_name": "terraform.io/builtin/terraform", + "change": { + "actions": [ + "no-op" + ], + "before": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "after": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "after_unknown": {}, + "before_sensitive": {}, + "after_sensitive": {}, + "importing": { + "id": "bar" + }, + "generated_config": "resource \"terraform_data\" \"foo\" {\n input = null\n triggers_replace = null\n}" + } + } + ], + "prior_state": { + "format_version": "1.0", + "terraform_version": "1.6.0", + "values": { + "root_module": { + "resources": [ + { + "address": "terraform_data.foo", + "mode": "managed", + "type": "terraform_data", + "name": "foo", + "provider_name": "terraform.io/builtin/terraform", + "schema_version": 0, + "values": { + "id": "bar", + "input": null, + "output": null, + "triggers_replace": null + }, + "sensitive_values": {} + } + ] + } + } + }, + "configuration": { + "provider_config": { + "terraform": { + "name": "terraform", + "full_name": "terraform.io/builtin/terraform" + } + }, + "root_module": {} + }, + "provider_schemas": { + "terraform.io/builtin/terraform": { + "resource_schemas": { + "terraform_data": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "input": { + "type": "dynamic", + "description_kind": "plain", + "optional": true + }, + "output": { + "type": "dynamic", + "description_kind": "plain", + "computed": true + }, + "triggers_replace": { + "type": "dynamic", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + } + } + }, + "timestamp": "2023-05-30T03:34:55Z" +} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-import-config-gen/plan.log b/internal/cloud/testdata/plan-import-config-gen/plan.log new file mode 100644 index 0000000000..2771305567 --- /dev/null +++ b/internal/cloud/testdata/plan-import-config-gen/plan.log @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.5.0","@module":"terraform.ui","@timestamp":"2023-05-29T20:30:14.113797-07:00","terraform":"1.5.0","type":"version","ui":"1.1"} +{"@level":"info","@message":"terraform_data.foo: Plan to import","@module":"terraform.ui","@timestamp":"2023-05-29T20:30:14.130354-07:00","change":{"resource":{"addr":"terraform_data.foo","module":"","resource":"terraform_data.foo","implied_provider":"terraform","resource_type":"terraform_data","resource_name":"foo","resource_key":null},"action":"import","importing":{"id":"bar"},"generated_config":"resource \"terraform_data\" \"foo\" {\n input = null\n triggers_replace = null\n}"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-05-29T20:30:14.130392-07:00","changes":{"add":0,"change":0,"import":1,"remove":0,"operation":"plan"},"type":"change_summary"} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-json-basic-no-unredacted/main.tf b/internal/cloud/testdata/plan-json-basic-no-unredacted/main.tf new file mode 100644 index 0000000000..3911a2a9b2 --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic-no-unredacted/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/internal/cloud/testdata/plan-json-basic-no-unredacted/plan-redacted.json b/internal/cloud/testdata/plan-json-basic-no-unredacted/plan-redacted.json new file mode 100644 index 0000000000..935799605f --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic-no-unredacted/plan-redacted.json @@ -0,0 +1,116 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-json-basic-no-unredacted/plan.log b/internal/cloud/testdata/plan-json-basic-no-unredacted/plan.log new file mode 100644 index 0000000000..6e7352ed44 --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic-no-unredacted/plan.log @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.409143-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605841-05:00","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605906-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/cloud/testdata/plan-json-basic/main.tf b/internal/cloud/testdata/plan-json-basic/main.tf new file mode 100644 index 0000000000..3911a2a9b2 --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/internal/cloud/testdata/plan-json-basic/plan-redacted.json b/internal/cloud/testdata/plan-json-basic/plan-redacted.json new file mode 100644 index 0000000000..935799605f --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic/plan-redacted.json @@ -0,0 +1,116 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-json-basic/plan-unredacted.json b/internal/cloud/testdata/plan-json-basic/plan-unredacted.json new file mode 100644 index 0000000000..e840d281f4 --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic/plan-unredacted.json @@ -0,0 +1 @@ +{"format_version":"1.1","terraform_version":"1.4.4","planned_values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"triggers":null},"sensitive_values":{}}]}},"resource_changes":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["create"],"before":null,"after":{"triggers":null},"after_unknown":{"id":true},"before_sensitive":false,"after_sensitive":{}}}],"configuration":{"provider_config":{"null":{"name":"null","full_name":"registry.terraform.io/hashicorp/null"}},"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}} diff --git a/internal/cloud/testdata/plan-json-basic/plan.log b/internal/cloud/testdata/plan-json-basic/plan.log new file mode 100644 index 0000000000..6e7352ed44 --- /dev/null +++ b/internal/cloud/testdata/plan-json-basic/plan.log @@ -0,0 +1,3 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.409143-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"null_resource.foo: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605841-05:00","change":{"resource":{"addr":"null_resource.foo","module":"","resource":"null_resource.foo","implied_provider":"null","resource_type":"null_resource","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605906-05:00","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/cloud/testdata/plan-json-error/main.tf b/internal/cloud/testdata/plan-json-error/main.tf new file mode 100644 index 0000000000..bc45f28f56 --- /dev/null +++ b/internal/cloud/testdata/plan-json-error/main.tf @@ -0,0 +1,5 @@ +resource "null_resource" "foo" { + triggers { + random = "${guid()}" + } +} diff --git a/internal/cloud/testdata/plan-json-error/plan-redacted.json b/internal/cloud/testdata/plan-json-error/plan-redacted.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/internal/cloud/testdata/plan-json-error/plan-redacted.json @@ -0,0 +1 @@ +{} diff --git a/internal/cloud/testdata/plan-json-error/plan.log b/internal/cloud/testdata/plan-json-error/plan.log new file mode 100644 index 0000000000..b877f1e583 --- /dev/null +++ b/internal/cloud/testdata/plan-json-error/plan.log @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-20T12:12:25.477403-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"error","@message":"Error: Unsupported block type","@module":"terraform.ui","@timestamp":"2023-01-20T12:12:25.615995-05:00","diagnostic":{"severity":"error","summary":"Unsupported block type","detail":"Blocks of type \"triggers\" are not expected here. Did you mean to define argument \"triggers\"? If so, use the equals sign to assign it a value.","range":{"filename":"main.tf","start":{"line":2,"column":3,"byte":35},"end":{"line":2,"column":11,"byte":43}},"snippet":{"context":"resource \"null_resource\" \"foo\"","code":" triggers {","start_line":2,"highlight_start_offset":2,"highlight_end_offset":10,"values":[]}},"type":"diagnostic"} diff --git a/internal/cloud/testdata/plan-json-full/main.tf b/internal/cloud/testdata/plan-json-full/main.tf new file mode 100644 index 0000000000..d6e43c5521 --- /dev/null +++ b/internal/cloud/testdata/plan-json-full/main.tf @@ -0,0 +1,82 @@ +provider "tfcoremock" {} + +# In order to generate the JSON logs contained in plan.log +# First ONLY apply tfcoremock_simple_resource.example (set the bool attribute +# to true). Make sure the complex_resource is commented out. +# Once applied, change the bool attribute to false and uncomment the complex +# resource. + +resource "tfcoremock_simple_resource" "example" { + id = "my-simple-resource" + bool = false + number = 0 + string = "Hello, world!" + float = 0 + integer = 0 +} + +resource "tfcoremock_complex_resource" "example" { + id = "my-complex-resource" + + bool = true + number = 0 + string = "Hello, world!" + float = 0 + integer = 0 + + list = [ + { + string = "list.one" + }, + { + string = "list.two" + } + ] + + set = [ + { + string = "set.one" + }, + { + string = "set.two" + } + ] + + map = { + "one" : { + string = "map.one" + }, + "two" : { + string = "map.two" + } + } + + object = { + + string = "nested object" + + object = { + string = "nested nested object" + } + } + + list_block { + string = "list_block.one" + } + + list_block { + string = "list_block.two" + } + + list_block { + string = "list_block.three" + } + + set_block { + string = "set_block.one" + } + + set_block { + string = "set_block.two" + } +} diff --git a/internal/cloud/testdata/plan-json-full/plan-redacted.json b/internal/cloud/testdata/plan-json-full/plan-redacted.json new file mode 100644 index 0000000000..3e3d9e0f86 --- /dev/null +++ b/internal/cloud/testdata/plan-json-full/plan-redacted.json @@ -0,0 +1 @@ +{"plan_format_version":"1.1","resource_drift":[{"address":"tfcoremock_simple_resource.example","mode":"managed","type":"tfcoremock_simple_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","change":{"actions":["delete"],"before":{"bool":true,"float":0,"id":"my-simple-resource","integer":0,"number":0,"string":"Hello, world!"},"after":null,"after_unknown":{},"before_sensitive":{},"after_sensitive":false}}],"resource_changes":[{"address":"tfcoremock_complex_resource.example","mode":"managed","type":"tfcoremock_complex_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","change":{"actions":["create"],"before":null,"after":{"bool":true,"float":0,"id":"my-complex-resource","integer":0,"list":[{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"list.one"},{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"list.two"}],"list_block":[{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.one"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.two"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.three"}],"map":{"one":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"map.one"},"two":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"map.two"}},"number":0,"object":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"nested nested object"},"set":null,"string":"nested object"},"set":[{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"set.one"},{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"set.two"}],"set_block":[{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"set_block.one"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"set_block.two"}],"string":"Hello, world!"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{"list":[{},{}],"list_block":[{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]}],"map":{"one":{},"two":{}},"object":{"object":{}},"set":[{},{}],"set_block":[{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]}]}}},{"address":"tfcoremock_simple_resource.example","mode":"managed","type":"tfcoremock_simple_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","change":{"actions":["create"],"before":null,"after":{"bool":false,"float":0,"id":"my-simple-resource","integer":0,"number":0,"string":"Hello, world!"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{}}}],"relevant_attributes":[],"output_changes":{},"provider_schemas":{"registry.terraform.io/hashicorp/tfcoremock":{"provider":{"version":0,"block":{"attributes":{"data_directory":{"type":"string","description":"The directory that the provider should use to read the human-readable JSON files for each requested data source. Defaults to `data.resource`.","description_kind":"markdown","optional":true},"resource_directory":{"type":"string","description":"The directory that the provider should use to write the human-readable JSON files for each managed resource. If `use_only_state` is set to `true` then this value does not matter. Defaults to `terraform.resource`.","description_kind":"markdown","optional":true},"use_only_state":{"type":"bool","description":"If set to true the provider will rely only on the Terraform state file to load managed resources and will not write anything to disk. Defaults to `false`.","description_kind":"markdown","optional":true}},"description":"The `tfcoremock` provider is intended to aid with testing the Terraform core libraries and the Terraform CLI. This provider should allow users to define all possible Terraform configurations and run them through the Terraform core platform.\n\nThe provider supplies two static resources:\n\n- `tfcoremock_simple_resource`\n- `tfcoremock_complex_resource`\n \nUsers can then define additional dynamic resources by supplying a `dynamic_resources.json` file alongside their root Terraform configuration. These dynamic resources can be used to model any Terraform configuration not covered by the provided static resources.\n\nBy default, all resources created by the provider are then converted into a human-readable JSON format and written out to the resource directory. This behaviour can be disabled by turning on the `use_only_state` flag in the provider schema (this is useful when running the provider in a HCP Terraform environment). The resource directory defaults to `terraform.resource`.\n\nAll resources supplied by the provider (including the simple and complex resource as well as any dynamic resources) are duplicated into data sources. The data sources should be supplied in the JSON format that resources are written into. The provider looks into the data directory, which defaults to `terraform.data`.\n\nFinally, all resources (and data sources) supplied by the provider have an `id` attribute that is generated if not set by the configuration. Dynamic resources cannot define an `id` attribute as the provider will create one for them. The `id` attribute is used as name of the human-readable JSON files held in the resource and data directories.","description_kind":"markdown"}},"resource_schemas":{"tfcoremock_complex_resource":{"version":0,"block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"id":{"type":"string","description_kind":"plain","optional":true,"computed":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A complex resource that contains five basic attributes, four complex attributes, and two nested blocks.\n\nThe five basic attributes are `boolean`, `number`, `string`, `float`, and `integer` (as with the `tfcoremock_simple_resource`).\n\nThe complex attributes are a `map`, a `list`, a `set`, and an `object`. The `object` type contains the same set of attributes as the schema itself, making a recursive structure. The `list`, `set` and `map` all contain objects which are also recursive. Blocks cannot go into attributes, so the complex attributes do not recurse on the block types.\n\nThe blocks are a nested `list_block` and a nested `set_block`. The blocks contain the same set of attributes and blocks as the schema itself, also making a recursive structure. Note, blocks contain both attributes and more blocks so the block types are fully recursive.\n\nThe complex and block types are nested 3 times, at the leaf level of recursion the complex attributes and blocks only contain the simple (ie. non-recursive) attributes. This prevents a potentially infinite level of recursion.","description_kind":"markdown"}},"tfcoremock_simple_resource":{"version":0,"block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"id":{"type":"string","description_kind":"plain","optional":true,"computed":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`.","description_kind":"markdown"}}},"data_source_schemas":{"tfcoremock_complex_resource":{"version":0,"block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"id":{"type":"string","description_kind":"plain","required":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"list":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"list"},"description":"A list attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"map":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"map"},"description":"A map attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"object":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"single"},"description":"An object attribute that matches the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"set":{"nested_type":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"nesting_mode":"set"},"description":"A set attribute that contains objects that match the root schema, allowing for nested collections and objects to be modelled.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"block_types":{"list_block":{"nesting_mode":"list","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A list block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}},"set_block":{"nesting_mode":"set","block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A set block that contains the same attributes and blocks as the root schema, allowing nested blocks and objects to be modelled.","description_kind":"markdown"}}},"description":"A complex resource that contains five basic attributes, four complex attributes, and two nested blocks.\n\nThe five basic attributes are `boolean`, `number`, `string`, `float`, and `integer` (as with the `tfcoremock_simple_resource`).\n\nThe complex attributes are a `map`, a `list`, a `set`, and an `object`. The `object` type contains the same set of attributes as the schema itself, making a recursive structure. The `list`, `set` and `map` all contain objects which are also recursive. Blocks cannot go into attributes, so the complex attributes do not recurse on the block types.\n\nThe blocks are a nested `list_block` and a nested `set_block`. The blocks contain the same set of attributes and blocks as the schema itself, also making a recursive structure. Note, blocks contain both attributes and more blocks so the block types are fully recursive.\n\nThe complex and block types are nested 3 times, at the leaf level of recursion the complex attributes and blocks only contain the simple (ie. non-recursive) attributes. This prevents a potentially infinite level of recursion.","description_kind":"markdown"}},"tfcoremock_simple_resource":{"version":0,"block":{"attributes":{"bool":{"type":"bool","description":"An optional boolean attribute, can be true or false.","description_kind":"markdown","optional":true},"float":{"type":"number","description":"An optional float attribute.","description_kind":"markdown","optional":true},"id":{"type":"string","description_kind":"plain","required":true},"integer":{"type":"number","description":"An optional integer attribute.","description_kind":"markdown","optional":true},"number":{"type":"number","description":"An optional number attribute, can be an integer or a float.","description_kind":"markdown","optional":true},"string":{"type":"string","description":"An optional string attribute.","description_kind":"markdown","optional":true}},"description":"A simple resource that holds optional attributes for the five basic types: `bool`, `number`, `string`, `float`, and `integer`.","description_kind":"markdown"}}}}},"provider_format_version":"1.0"} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-json-full/plan-unredacted.json b/internal/cloud/testdata/plan-json-full/plan-unredacted.json new file mode 100644 index 0000000000..2bf56bddc0 --- /dev/null +++ b/internal/cloud/testdata/plan-json-full/plan-unredacted.json @@ -0,0 +1 @@ +{"format_version":"1.1","terraform_version":"1.4.4","planned_values":{"root_module":{"resources":[{"address":"tfcoremock_complex_resource.example","mode":"managed","type":"tfcoremock_complex_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","schema_version":0,"values":{"bool":true,"float":0,"id":"my-complex-resource","integer":0,"list":[{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"list.one"},{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"list.two"}],"list_block":[{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.one"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.two"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.three"}],"map":{"one":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"map.one"},"two":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"map.two"}},"number":0,"object":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"nested nested object"},"set":null,"string":"nested object"},"set":[{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"set.one"},{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"set.two"}],"set_block":[{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"set_block.one"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"set_block.two"}],"string":"Hello, world!"},"sensitive_values":{"list":[{},{}],"list_block":[{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]}],"map":{"one":{},"two":{}},"object":{"object":{}},"set":[{},{}],"set_block":[{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]}]}},{"address":"tfcoremock_simple_resource.example","mode":"managed","type":"tfcoremock_simple_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","schema_version":0,"values":{"bool":false,"float":0,"id":"my-simple-resource","integer":0,"number":0,"string":"Hello, world!"},"sensitive_values":{}}]}},"resource_changes":[{"address":"tfcoremock_complex_resource.example","mode":"managed","type":"tfcoremock_complex_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","change":{"actions":["create"],"before":null,"after":{"bool":true,"float":0,"id":"my-complex-resource","integer":0,"list":[{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"list.one"},{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"list.two"}],"list_block":[{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.one"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.two"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"list_block.three"}],"map":{"one":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"map.one"},"two":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"map.two"}},"number":0,"object":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"nested nested object"},"set":null,"string":"nested object"},"set":[{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"set.one"},{"bool":null,"float":null,"integer":null,"list":null,"map":null,"number":null,"object":null,"set":null,"string":"set.two"}],"set_block":[{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"set_block.one"},{"bool":null,"float":null,"integer":null,"list":null,"list_block":[],"map":null,"number":null,"object":null,"set":null,"set_block":[],"string":"set_block.two"}],"string":"Hello, world!"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{"list":[{},{}],"list_block":[{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]}],"map":{"one":{},"two":{}},"object":{"object":{}},"set":[{},{}],"set_block":[{"list_block":[],"set_block":[]},{"list_block":[],"set_block":[]}]}}},{"address":"tfcoremock_simple_resource.example","mode":"managed","type":"tfcoremock_simple_resource","name":"example","provider_name":"registry.terraform.io/hashicorp/tfcoremock","change":{"actions":["create"],"before":null,"after":{"bool":false,"float":0,"id":"my-simple-resource","integer":0,"number":0,"string":"Hello, world!"},"after_unknown":{},"before_sensitive":false,"after_sensitive":{}}}],"configuration":{"provider_config":{"tfcoremock":{"name":"tfcoremock","full_name":"registry.terraform.io/hashicorp/tfcoremock"}},"root_module":{"resources":[{"address":"tfcoremock_complex_resource.example","mode":"managed","type":"tfcoremock_complex_resource","name":"example","provider_config_key":"tfcoremock","expressions":{"bool":{"constant_value":true},"float":{"constant_value":0},"id":{"constant_value":"my-complex-resource"},"integer":{"constant_value":0},"list":{"constant_value":[{"string":"list.one"},{"string":"list.two"}]},"list_block":[{"string":{"constant_value":"list_block.one"}},{"string":{"constant_value":"list_block.two"}},{"string":{"constant_value":"list_block.three"}}],"map":{"constant_value":{"one":{"string":"map.one"},"two":{"string":"map.two"}}},"number":{"constant_value":0},"object":{"constant_value":{"object":{"string":"nested nested object"},"string":"nested object"}},"set":{"constant_value":[{"string":"set.one"},{"string":"set.two"}]},"set_block":[{"string":{"constant_value":"set_block.one"}},{"string":{"constant_value":"set_block.two"}}],"string":{"constant_value":"Hello, world!"}},"schema_version":0},{"address":"tfcoremock_simple_resource.example","mode":"managed","type":"tfcoremock_simple_resource","name":"example","provider_config_key":"tfcoremock","expressions":{"bool":{"constant_value":false},"float":{"constant_value":0},"id":{"constant_value":"my-simple-resource"},"integer":{"constant_value":0},"number":{"constant_value":0},"string":{"constant_value":"Hello, world!"}},"schema_version":0}]}}} diff --git a/internal/cloud/testdata/plan-json-full/plan.log b/internal/cloud/testdata/plan-json-full/plan.log new file mode 100644 index 0000000000..59fa3cb32c --- /dev/null +++ b/internal/cloud/testdata/plan-json-full/plan.log @@ -0,0 +1,6 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-19T13:28:29.004160-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"tfcoremock_simple_resource.example: Refreshing state... [id=my-simple-resource]","@module":"terraform.ui","@timestamp":"2023-01-19T13:28:29.232274-05:00","hook":{"resource":{"addr":"tfcoremock_simple_resource.example","module":"","resource":"tfcoremock_simple_resource.example","implied_provider":"tfcoremock","resource_type":"tfcoremock_simple_resource","resource_name":"example","resource_key":null},"id_key":"id","id_value":"my-simple-resource"},"type":"refresh_start"} +{"@level":"info","@message":"tfcoremock_simple_resource.example: Refresh complete [id=my-simple-resource]","@module":"terraform.ui","@timestamp":"2023-01-19T13:28:29.232882-05:00","hook":{"resource":{"addr":"tfcoremock_simple_resource.example","module":"","resource":"tfcoremock_simple_resource.example","implied_provider":"tfcoremock","resource_type":"tfcoremock_simple_resource","resource_name":"example","resource_key":null},"id_key":"id","id_value":"my-simple-resource"},"type":"refresh_complete"} +{"@level":"info","@message":"tfcoremock_simple_resource.example: Plan to update","@module":"terraform.ui","@timestamp":"2023-01-19T13:28:29.289259-05:00","change":{"resource":{"addr":"tfcoremock_simple_resource.example","module":"","resource":"tfcoremock_simple_resource.example","implied_provider":"tfcoremock","resource_type":"tfcoremock_simple_resource","resource_name":"example","resource_key":null},"action":"update"},"type":"planned_change"} +{"@level":"info","@message":"tfcoremock_complex_resource.example: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-19T13:28:29.289320-05:00","change":{"resource":{"addr":"tfcoremock_complex_resource.example","module":"","resource":"tfcoremock_complex_resource.example","implied_provider":"tfcoremock","resource_type":"tfcoremock_complex_resource","resource_name":"example","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 1 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-19T13:28:29.289330-05:00","changes":{"add":1,"change":1,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/cloud/testdata/plan-json-no-changes/main.tf b/internal/cloud/testdata/plan-json-no-changes/main.tf new file mode 100644 index 0000000000..3911a2a9b2 --- /dev/null +++ b/internal/cloud/testdata/plan-json-no-changes/main.tf @@ -0,0 +1 @@ +resource "null_resource" "foo" {} diff --git a/internal/cloud/testdata/plan-json-no-changes/plan-redacted.json b/internal/cloud/testdata/plan-json-no-changes/plan-redacted.json new file mode 100644 index 0000000000..f107442992 --- /dev/null +++ b/internal/cloud/testdata/plan-json-no-changes/plan-redacted.json @@ -0,0 +1,118 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "no-op" + ], + "before": { + "id": "3549869958859575216", + "triggers": null + }, + "after": { + "id": "3549869958859575216", + "triggers": null + }, + "after_unknown": {}, + "before_sensitive": {}, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/cloud/testdata/plan-json-no-changes/plan-unredacted.json b/internal/cloud/testdata/plan-json-no-changes/plan-unredacted.json new file mode 100644 index 0000000000..259533f0ce --- /dev/null +++ b/internal/cloud/testdata/plan-json-no-changes/plan-unredacted.json @@ -0,0 +1 @@ +{"format_version":"1.1","terraform_version":"1.4.4","planned_values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"3549869958859575216","triggers":null},"sensitive_values":{}}]}},"resource_changes":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","change":{"actions":["no-op"],"before":{"id":"3549869958859575216","triggers":null},"after":{"id":"3549869958859575216","triggers":null},"after_unknown":{},"before_sensitive":{},"after_sensitive":{}}}],"prior_state":{"format_version":"1.0","terraform_version":"1.4.4","values":{"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"id":"3549869958859575216","triggers":null},"sensitive_values":{}}]}}},"configuration":{"provider_config":{"null":{"name":"null","full_name":"registry.terraform.io/hashicorp/null"}},"root_module":{"resources":[{"address":"null_resource.foo","mode":"managed","type":"null_resource","name":"foo","provider_config_key":"null","schema_version":0}]}}} diff --git a/internal/cloud/testdata/plan-json-no-changes/plan.log b/internal/cloud/testdata/plan-json-no-changes/plan.log new file mode 100644 index 0000000000..7b10d42020 --- /dev/null +++ b/internal/cloud/testdata/plan-json-no-changes/plan.log @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.3.7","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.409143-05:00","terraform":"1.3.7","type":"version","ui":"1.0"} +{"@level":"info","@message":"Plan: 0 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-19T10:47:27.605906-05:00","changes":{"add":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/cloud/testdata/test-cancel/main.tf b/internal/cloud/testdata/test-cancel/main.tf new file mode 100644 index 0000000000..1576a34b2a --- /dev/null +++ b/internal/cloud/testdata/test-cancel/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string + default = "Hello, world!" +} + +resource "time_sleep" "wait_5_seconds" { + create_duration = "5s" +} + +resource "tfcoremock_simple_resource" "resource" { + string = var.input + + depends_on = [ time_sleep.wait_5_seconds ] +} diff --git a/internal/cloud/testdata/test-cancel/main.tftest.hcl b/internal/cloud/testdata/test-cancel/main.tftest.hcl new file mode 100644 index 0000000000..b790a0b8ab --- /dev/null +++ b/internal/cloud/testdata/test-cancel/main.tftest.hcl @@ -0,0 +1,20 @@ + +run "defaults" { + command = plan + + assert { + condition = tfcoremock_simple_resource.resource.string == "Hello, world!" + error_message = "bad string value" + } +} + +run "overrides" { + variables { + input = "Hello, universe!" + } + + assert { + condition = tfcoremock_simple_resource.resource.string == "Hello, universe!" + error_message = "bad string value" + } +} diff --git a/internal/cloud/testdata/test-cancel/test.log b/internal/cloud/testdata/test-cancel/test.log new file mode 100644 index 0000000000..104451b48d --- /dev/null +++ b/internal/cloud/testdata/test-cancel/test.log @@ -0,0 +1,9 @@ +{"@level":"info","@message":"Terraform 1.7.0-dev","@module":"terraform.ui","@timestamp":"2023-09-12T09:24:05.419381+02:00","terraform":"1.7.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 2 run blocks","@module":"terraform.ui","@timestamp":"2023-09-12T09:24:05.440805+02:00","test_abstract":{"main.tftest.hcl":["defaults","overrides"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T09:24:05.440916+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":"\nInterrupt received.\nPlease wait for Terraform to exit or data loss may occur.\nGracefully shutting down...\n","@module":"terraform.ui","@timestamp":"2023-09-12T09:24:05.606776+02:00","type":"log"} +{"@level":"info","@message":" \"defaults\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T09:24:05.645827+02:00","test_run":{"path":"main.tftest.hcl","run":"defaults","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":" \"overrides\"... skip","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:24:05.645853+02:00","test_run":{"path":"main.tftest.hcl","run":"overrides","progress":"complete","status":"skip"},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T09:24:05.645859+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T09:24:05.645868+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"} +{"@level":"info","@message":"Success! 1 passed, 0 failed, 1 skipped.","@module":"terraform.ui","@timestamp":"2023-09-12T09:24:05.645907+02:00","test_summary":{"status":"pass","passed":1,"failed":0,"errored":0,"skipped":1},"type":"test_summary"} diff --git a/internal/cloud/testdata/test-force-cancel/main.tf b/internal/cloud/testdata/test-force-cancel/main.tf new file mode 100644 index 0000000000..1576a34b2a --- /dev/null +++ b/internal/cloud/testdata/test-force-cancel/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string + default = "Hello, world!" +} + +resource "time_sleep" "wait_5_seconds" { + create_duration = "5s" +} + +resource "tfcoremock_simple_resource" "resource" { + string = var.input + + depends_on = [ time_sleep.wait_5_seconds ] +} diff --git a/internal/cloud/testdata/test-force-cancel/main.tftest.hcl b/internal/cloud/testdata/test-force-cancel/main.tftest.hcl new file mode 100644 index 0000000000..b790a0b8ab --- /dev/null +++ b/internal/cloud/testdata/test-force-cancel/main.tftest.hcl @@ -0,0 +1,20 @@ + +run "defaults" { + command = plan + + assert { + condition = tfcoremock_simple_resource.resource.string == "Hello, world!" + error_message = "bad string value" + } +} + +run "overrides" { + variables { + input = "Hello, universe!" + } + + assert { + condition = tfcoremock_simple_resource.resource.string == "Hello, universe!" + error_message = "bad string value" + } +} diff --git a/internal/cloud/testdata/test-force-cancel/test.log b/internal/cloud/testdata/test-force-cancel/test.log new file mode 100644 index 0000000000..9956d5b9b9 --- /dev/null +++ b/internal/cloud/testdata/test-force-cancel/test.log @@ -0,0 +1,15 @@ +{"@level":"info","@message":"Terraform 1.7.0-dev","@module":"terraform.ui","@timestamp":"2023-09-12T09:25:39.749719+02:00","terraform":"1.7.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 2 run blocks","@module":"terraform.ui","@timestamp":"2023-09-12T09:25:39.771279+02:00","test_abstract":{"main.tftest.hcl":["defaults","overrides"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T09:25:39.771341+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":" \"defaults\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T09:25:39.984002+02:00","test_run":{"path":"main.tftest.hcl","run":"defaults","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"\nInterrupt received.\nPlease wait for Terraform to exit or data loss may occur.\nGracefully shutting down...\n","@module":"terraform.ui","@timestamp":"2023-09-12T09:25:41.055021+02:00","type":"log"} +{"@level":"info","@message":"\nTwo interrupts received. Exiting immediately. Note that data loss may have occurred.\n","@module":"terraform.ui","@timestamp":"2023-09-12T09:25:41.206917+02:00","type":"log"} +{"@level":"error","@message":"Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:25:41.207183+02:00","test_interrupt":{"planned":["time_sleep.wait_5_seconds","tfcoremock_simple_resource.resource"]},"type":"test_interrupt"} +{"@level":"info","@message":" \"overrides\"... fail","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:25:41.210583+02:00","test_run":{"path":"main.tftest.hcl","run":"overrides","progress":"complete","status":"error"},"type":"test_run"} +{"@level":"error","@message":"Error: Test interrupted","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:25:41.210638+02:00","diagnostic":{"severity":"error","summary":"Test interrupted","detail":"The test operation could not be completed due to an interrupt signal. Please read the remaining diagnostics carefully for any sign of failed state cleanup or dangling resources."},"type":"diagnostic"} +{"@level":"error","@message":"Error: Create time sleep error","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:25:41.211201+02:00","diagnostic":{"severity":"error","summary":"Create time sleep error","detail":"Original Error: context canceled","address":"time_sleep.wait_5_seconds","range":{"filename":"main.tf","start":{"line":7,"column":40,"byte":110},"end":{"line":7,"column":41,"byte":111}},"snippet":{"context":"resource \"time_sleep\" \"wait_5_seconds\"","code":"resource \"time_sleep\" \"wait_5_seconds\" {","start_line":7,"highlight_start_offset":39,"highlight_end_offset":40,"values":[]}},"type":"diagnostic"} +{"@level":"error","@message":"Error: execution halted","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:25:41.211224+02:00","diagnostic":{"severity":"error","summary":"execution halted","detail":""},"type":"diagnostic"} +{"@level":"error","@message":"Error: execution halted","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T09:25:41.211233+02:00","diagnostic":{"severity":"error","summary":"execution halted","detail":""},"type":"diagnostic"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T09:25:41.211533+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":"main.tftest.hcl... fail","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T09:25:41.211575+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"error"},"type":"test_file"} +{"@level":"info","@message":"Failure! 1 passed, 1 failed.","@module":"terraform.ui","@timestamp":"2023-09-12T09:25:41.211592+02:00","test_summary":{"status":"error","passed":1,"failed":0,"errored":1,"skipped":0},"type":"test_summary"} diff --git a/internal/cloud/testdata/test-long-running/main.tf b/internal/cloud/testdata/test-long-running/main.tf new file mode 100644 index 0000000000..9438a3ae57 --- /dev/null +++ b/internal/cloud/testdata/test-long-running/main.tf @@ -0,0 +1,5 @@ + +resource "time_sleep" "sleep" { + create_duration = "5s" + destroy_duration = "3s" +} diff --git a/internal/cloud/testdata/test-long-running/main.tftest.hcl b/internal/cloud/testdata/test-long-running/main.tftest.hcl new file mode 100644 index 0000000000..5080662b55 --- /dev/null +++ b/internal/cloud/testdata/test-long-running/main.tftest.hcl @@ -0,0 +1 @@ +run "just_go" {} diff --git a/internal/cloud/testdata/test-long-running/test.log b/internal/cloud/testdata/test-long-running/test.log new file mode 100644 index 0000000000..be70381d15 --- /dev/null +++ b/internal/cloud/testdata/test-long-running/test.log @@ -0,0 +1,12 @@ +{"@level":"info","@message":"Terraform 1.7.0-dev","@module":"terraform.ui","@timestamp":"2023-09-28T14:57:09.175210+02:00","terraform":"1.7.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 1 run block","@module":"terraform.ui","@timestamp":"2023-09-28T14:57:09.189212+02:00","test_abstract":{"main.tftest.hcl":["just_go"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-28T14:57:09.189386+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":" \"just_go\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:09.189429+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"starting","elapsed":0},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:11.341278+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"running","elapsed":2152},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:13.343465+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"running","elapsed":4154},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:14.381552+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-28T14:57:14.381655+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":" \"just_go\"... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:14.381712+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"teardown","elapsed":0},"type":"test_run"} +{"@level":"info","@message":" \"just_go\"... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"just_go","@timestamp":"2023-09-28T14:57:16.477705+02:00","test_run":{"path":"main.tftest.hcl","run":"just_go","progress":"teardown","elapsed":2096},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-28T14:57:17.517309+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"} +{"@level":"info","@message":"Success! 1 passed, 0 failed.","@module":"terraform.ui","@timestamp":"2023-09-28T14:57:17.517494+02:00","test_summary":{"status":"pass","passed":1,"failed":0,"errored":0,"skipped":0},"type":"test_summary"} diff --git a/internal/cloud/testdata/test-verbose/main.tf b/internal/cloud/testdata/test-verbose/main.tf new file mode 100644 index 0000000000..90127e44c5 --- /dev/null +++ b/internal/cloud/testdata/test-verbose/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string + default = "Hello, world!" +} + +data "null_data_source" "values" { + inputs = { + data = var.input + } +} + +output "input" { + value = data.null_data_source.values.outputs["data"] +} diff --git a/internal/cloud/testdata/test-verbose/main.tftest.hcl b/internal/cloud/testdata/test-verbose/main.tftest.hcl new file mode 100644 index 0000000000..c4298bfe8a --- /dev/null +++ b/internal/cloud/testdata/test-verbose/main.tftest.hcl @@ -0,0 +1,20 @@ + +run "defaults" { + command = plan + + assert { + condition = output.input == "Hello, world!" + error_message = "bad string value" + } +} + +run "overrides" { + variables { + input = "Hello, universe!" + } + + assert { + condition = output.input == "Hello, universe!" + error_message = "bad string value" + } +} diff --git a/internal/cloud/testdata/test-verbose/test.log b/internal/cloud/testdata/test-verbose/test.log new file mode 100644 index 0000000000..7d355a40c8 --- /dev/null +++ b/internal/cloud/testdata/test-verbose/test.log @@ -0,0 +1,14 @@ +{"@level":"info","@message":"Terraform 1.7.0-dev","@module":"terraform.ui","@timestamp":"2023-09-12T08:30:36.443282+02:00","terraform":"1.7.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 2 run blocks","@module":"terraform.ui","@timestamp":"2023-09-12T08:30:36.453493+02:00","test_abstract":{"main.tftest.hcl":["defaults","overrides"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:30:36.453557+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":" \"defaults\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T08:30:37.184747+02:00","test_run":{"path":"main.tftest.hcl","run":"defaults","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"-verbose flag enabled, printing plan","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T08:30:37.184891+02:00","test_plan":{"plan_format_version":"1.2","output_changes":{"input":{"actions":["create"],"before":null,"after":"Hello, world!","after_unknown":false,"before_sensitive":false,"after_sensitive":false}},"relevant_attributes":[{"resource":"data.null_data_source.values","attribute":["outputs","data"]}],"provider_format_version":"1.0","provider_schemas":{"registry.terraform.io/hashicorp/null":{"provider":{"version":0,"block":{"description_kind":"plain"}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","description":"This is set to a random value at create time.","description_kind":"plain","computed":true},"triggers":{"type":["map","string"],"description":"A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.","description_kind":"plain","optional":true}},"description":"The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.","description_kind":"plain"}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","description":"If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.","description_kind":"plain","optional":true,"computed":true},"id":{"type":"string","description":"This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.","description_kind":"plain","deprecated":true,"computed":true},"inputs":{"type":["map","string"],"description":"A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.","description_kind":"plain","optional":true},"outputs":{"type":["map","string"],"description":"After the data source is \"read\", a copy of the `inputs` map.","description_kind":"plain","computed":true},"random":{"type":"string","description":"A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.","description_kind":"plain","computed":true}},"description":"The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n","description_kind":"plain","deprecated":true}}}}}},"type":"test_plan"} +{"@level":"warn","@message":"Warning: Deprecated","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T08:30:37.185952+02:00","diagnostic":{"severity":"warning","summary":"Deprecated","detail":"The null_data_source was historically used to construct intermediate values to re-use elsewhere in configuration, the same can now be achieved using locals","address":"data.null_data_source.values","range":{"filename":"main.tf","start":{"line":7,"column":34,"byte":104},"end":{"line":7,"column":35,"byte":105}},"snippet":{"context":"data \"null_data_source\" \"values\"","code":"data \"null_data_source\" \"values\" {","start_line":7,"highlight_start_offset":33,"highlight_end_offset":34,"values":[]}},"type":"diagnostic"} +{"@level":"warn","@message":"Warning: Deprecated","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T08:30:37.186067+02:00","diagnostic":{"severity":"warning","summary":"Deprecated","detail":"The null_data_source was historically used to construct intermediate values to re-use elsewhere in configuration, the same can now be achieved using locals","address":"data.null_data_source.values","range":{"filename":"main.tf","start":{"line":7,"column":34,"byte":104},"end":{"line":7,"column":35,"byte":105}},"snippet":{"context":"data \"null_data_source\" \"values\"","code":"data \"null_data_source\" \"values\" {","start_line":7,"highlight_start_offset":33,"highlight_end_offset":34,"values":[]}},"type":"diagnostic"} +{"@level":"info","@message":" \"overrides\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T08:30:37.246268+02:00","test_run":{"path":"main.tftest.hcl","run":"overrides","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"-verbose flag enabled, printing state","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T08:30:37.246362+02:00","test_state":{"state_format_version":"1.0","root_module":{"resources":[{"address":"data.null_data_source.values","mode":"data","type":"null_data_source","name":"values","provider_name":"registry.terraform.io/hashicorp/null","schema_version":0,"values":{"has_computed_default":"default","id":"static","inputs":{"data":"Hello, universe!"},"outputs":{"data":"Hello, universe!"},"random":"8484833523059069761"},"sensitive_values":{"inputs":{},"outputs":{}}}]},"outputs":{"input":{"sensitive":false,"value":"Hello, universe!","type":"string"}},"provider_format_version":"1.0","provider_schemas":{"registry.terraform.io/hashicorp/null":{"provider":{"version":0,"block":{"description_kind":"plain"}},"resource_schemas":{"null_resource":{"version":0,"block":{"attributes":{"id":{"type":"string","description":"This is set to a random value at create time.","description_kind":"plain","computed":true},"triggers":{"type":["map","string"],"description":"A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.","description_kind":"plain","optional":true}},"description":"The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.","description_kind":"plain"}}},"data_source_schemas":{"null_data_source":{"version":0,"block":{"attributes":{"has_computed_default":{"type":"string","description":"If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.","description_kind":"plain","optional":true,"computed":true},"id":{"type":"string","description":"This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.","description_kind":"plain","deprecated":true,"computed":true},"inputs":{"type":["map","string"],"description":"A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.","description_kind":"plain","optional":true},"outputs":{"type":["map","string"],"description":"After the data source is \"read\", a copy of the `inputs` map.","description_kind":"plain","computed":true},"random":{"type":"string","description":"A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.","description_kind":"plain","computed":true}},"description":"The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n","description_kind":"plain","deprecated":true}}}}}},"type":"test_state"} +{"@level":"warn","@message":"Warning: Deprecated","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T08:30:37.247116+02:00","diagnostic":{"severity":"warning","summary":"Deprecated","detail":"The null_data_source was historically used to construct intermediate values to re-use elsewhere in configuration, the same can now be achieved using locals","address":"data.null_data_source.values","range":{"filename":"main.tf","start":{"line":7,"column":34,"byte":104},"end":{"line":7,"column":35,"byte":105}},"snippet":{"context":"data \"null_data_source\" \"values\"","code":"data \"null_data_source\" \"values\" {","start_line":7,"highlight_start_offset":33,"highlight_end_offset":34,"values":[]}},"type":"diagnostic"} +{"@level":"warn","@message":"Warning: Deprecated","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T08:30:37.247182+02:00","diagnostic":{"severity":"warning","summary":"Deprecated","detail":"The null_data_source was historically used to construct intermediate values to re-use elsewhere in configuration, the same can now be achieved using locals","address":"data.null_data_source.values","range":{"filename":"main.tf","start":{"line":7,"column":34,"byte":104},"end":{"line":7,"column":35,"byte":105}},"snippet":{"context":"data \"null_data_source\" \"values\"","code":"data \"null_data_source\" \"values\" {","start_line":7,"highlight_start_offset":33,"highlight_end_offset":34,"values":[]}},"type":"diagnostic"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:30:37.247905+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:30:37.313672+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"} +{"@level":"info","@message":"Success! 2 passed, 0 failed.","@module":"terraform.ui","@timestamp":"2023-09-12T08:30:37.313700+02:00","test_summary":{"status":"pass","passed":2,"failed":0,"errored":0,"skipped":0},"type":"test_summary"} diff --git a/internal/cloud/testdata/test/main.tf b/internal/cloud/testdata/test/main.tf new file mode 100644 index 0000000000..64d683b49e --- /dev/null +++ b/internal/cloud/testdata/test/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string + default = "Hello, world!" +} + +resource "tfcoremock_simple_resource" "resource" { + string = var.input +} diff --git a/internal/cloud/testdata/test/main.tftest.hcl b/internal/cloud/testdata/test/main.tftest.hcl new file mode 100644 index 0000000000..b790a0b8ab --- /dev/null +++ b/internal/cloud/testdata/test/main.tftest.hcl @@ -0,0 +1,20 @@ + +run "defaults" { + command = plan + + assert { + condition = tfcoremock_simple_resource.resource.string == "Hello, world!" + error_message = "bad string value" + } +} + +run "overrides" { + variables { + input = "Hello, universe!" + } + + assert { + condition = tfcoremock_simple_resource.resource.string == "Hello, universe!" + error_message = "bad string value" + } +} diff --git a/internal/cloud/testdata/test/test.log b/internal/cloud/testdata/test/test.log new file mode 100644 index 0000000000..82eef95b5c --- /dev/null +++ b/internal/cloud/testdata/test/test.log @@ -0,0 +1,8 @@ +{"@level":"info","@message":"Terraform 1.6.0-dev","@module":"terraform.ui","@timestamp":"2023-09-12T08:29:27.257413+02:00","terraform":"1.6.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Found 1 file and 2 run blocks","@module":"terraform.ui","@timestamp":"2023-09-12T08:29:27.268731+02:00","test_abstract":{"main.tftest.hcl":["defaults","overrides"]},"type":"test_abstract"} +{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:29:27.268889+02:00","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"} +{"@level":"info","@message":" \"defaults\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"defaults","@timestamp":"2023-09-12T08:29:27.710541+02:00","test_run":{"path":"main.tftest.hcl","run":"defaults","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":" \"overrides\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"overrides","@timestamp":"2023-09-12T08:29:27.833351+02:00","test_run":{"path":"main.tftest.hcl","run":"overrides","progress":"complete","status":"pass"},"type":"test_run"} +{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:29:27.833375+02:00","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"} +{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@timestamp":"2023-09-12T08:29:27.956488+02:00","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"} +{"@level":"info","@message":"Success! 2 passed, 0 failed.","@module":"terraform.ui","@timestamp":"2023-09-12T08:29:27.956510+02:00","test_summary":{"status":"pass","passed":2,"failed":0,"errored":0,"skipped":0},"type":"test_summary"} diff --git a/internal/cloud/testing.go b/internal/cloud/testing.go index cfb49cf9b3..cc3b02f313 100644 --- a/internal/cloud/testing.go +++ b/internal/cloud/testing.go @@ -1,33 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( + "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" + "net/url" "path" + "strconv" "testing" "time" + "github.com/hashicorp/cli" tfe "github.com/hashicorp/go-tfe" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/auth" "github.com/hashicorp/terraform-svchost/disco" - "github.com/mitchellh/cli" + "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states/remote" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" + "github.com/hashicorp/terraform/internal/backend/backendrun" backendLocal "github.com/hashicorp/terraform/internal/backend/local" ) @@ -41,6 +49,13 @@ var ( tfeHost: {"token": testCred}, }) testBackendSingleWorkspaceName = "app-prod" + defaultTFCPing = map[string]func(http.ResponseWriter, *http.Request){ + "/api/v2/ping": func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.5") + w.Header().Set("TFP-AppName", "HCP Terraform") + }, + } ) // mockInput is a mock implementation of terraform.UIInput. @@ -68,16 +83,22 @@ func testInput(t *testing.T, answers map[string]string) *mockInput { } func testBackendWithName(t *testing.T) (*Cloud, func()) { + b, _, c := testBackendAndMocksWithName(t) + return b, c +} + +func testBackendAndMocksWithName(t *testing.T) (*Cloud, *MockClient, func()) { obj := cty.ObjectVal(map[string]cty.Value{ "hostname": cty.NullVal(cty.String), "organization": cty.StringVal("hashicorp"), "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal(testBackendSingleWorkspaceName), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal(testBackendSingleWorkspaceName), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }) - return testBackend(t, obj) + return testBackend(t, obj, defaultTFCPing) } func testBackendWithTags(t *testing.T) (*Cloud, func()) { @@ -92,9 +113,29 @@ func testBackendWithTags(t *testing.T) (*Cloud, func()) { cty.StringVal("billing"), }, ), + "project": cty.NullVal(cty.String), }), }) - return testBackend(t, obj) + b, _, c := testBackend(t, obj, nil) + return b, c +} + +func testBackendWithKVTags(t *testing.T) (*Cloud, func()) { + obj := cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "tags": cty.MapVal(map[string]cty.Value{ + "dept": cty.StringVal("billing"), + "costcenter": cty.StringVal("101"), + }), + "project": cty.NullVal(cty.String), + }), + }) + b, _, c := testBackend(t, obj, nil) + return b, c } func testBackendNoOperations(t *testing.T) (*Cloud, func()) { @@ -103,14 +144,31 @@ func testBackendNoOperations(t *testing.T) (*Cloud, func()) { "organization": cty.StringVal("no-operations"), "token": cty.NullVal(cty.String), "workspaces": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal(testBackendSingleWorkspaceName), - "tags": cty.NullVal(cty.Set(cty.String)), + "name": cty.StringVal(testBackendSingleWorkspaceName), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), }), }) - return testBackend(t, obj) + b, _, c := testBackend(t, obj, nil) + return b, c } -func testRemoteClient(t *testing.T) remote.Client { +func testBackendWithHandlers(t *testing.T, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, func()) { + obj := cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "token": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal(testBackendSingleWorkspaceName), + "tags": cty.NullVal(cty.Set(cty.String)), + "project": cty.NullVal(cty.String), + }), + }) + b, _, c := testBackend(t, obj, handlers) + return b, c +} + +func testCloudState(t *testing.T) *State { b, bCleanup := testBackendWithName(t) defer bCleanup() @@ -119,7 +177,7 @@ func testRemoteClient(t *testing.T) remote.Client { t.Fatalf("error: %v", err) } - return raw.(*State).Client + return raw.(*State) } func testBackendWithOutputs(t *testing.T) (*Cloud, func()) { @@ -184,8 +242,13 @@ func testBackendWithOutputs(t *testing.T) (*Cloud, func()) { return b, cleanup } -func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { - s := testServer(t) +func testBackend(t *testing.T, obj cty.Value, handlers map[string]func(http.ResponseWriter, *http.Request)) (*Cloud, *MockClient, func()) { + var s *httptest.Server + if handlers != nil { + s = testServerWithHandlers(handlers) + } else { + s = testServer(t) + } b := New(testDisco(s)) // Configure the backend so the client is created. @@ -210,8 +273,11 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { b.client.CostEstimates = mc.CostEstimates b.client.Organizations = mc.Organizations b.client.Plans = mc.Plans + b.client.TaskStages = mc.TaskStages + b.client.PolicySetOutcomes = mc.PolicySetOutcomes b.client.PolicyChecks = mc.PolicyChecks b.client.Runs = mc.Runs + b.client.RunEvents = mc.RunEvents b.client.StateVersions = mc.StateVersions b.client.StateVersionOutputs = mc.StateVersionOutputs b.client.Variables = mc.Variables @@ -221,11 +287,21 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { b.local = testLocalBackend(t, b) b.input = true + baseURL, err := url.Parse("https://app.terraform.io") + if err != nil { + t.Fatalf("testBackend: failed to parse base URL for client") + } + baseURL.Path = "/api/v2/" + + readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) { + return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID) + } + ctx := context.Background() // Create the organization. - _, err := b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ - Name: tfe.String(b.organization), + _, err = b.client.Organizations.Create(ctx, tfe.OrganizationCreateOptions{ + Name: tfe.String(b.Organization), }) if err != nil { t.Fatalf("error: %v", err) @@ -233,7 +309,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { // Create the default workspace if required. if b.WorkspaceMapping.Name != "" { - _, err = b.client.Workspaces.Create(ctx, b.organization, tfe.WorkspaceCreateOptions{ + _, err = b.client.Workspaces.Create(ctx, b.Organization, tfe.WorkspaceCreateOptions{ Name: tfe.String(b.WorkspaceMapping.Name), }) if err != nil { @@ -241,7 +317,7 @@ func testBackend(t *testing.T, obj cty.Value) (*Cloud, func()) { } } - return b, s.Close + return b, mc, s.Close } // testUnconfiguredBackend is used for testing the configuration of the backend @@ -270,27 +346,42 @@ func testUnconfiguredBackend(t *testing.T) (*Cloud, func()) { b.client.CostEstimates = mc.CostEstimates b.client.Organizations = mc.Organizations b.client.Plans = mc.Plans + b.client.PolicySetOutcomes = mc.PolicySetOutcomes b.client.PolicyChecks = mc.PolicyChecks b.client.Runs = mc.Runs + b.client.RunEvents = mc.RunEvents b.client.StateVersions = mc.StateVersions + b.client.StateVersionOutputs = mc.StateVersionOutputs b.client.Variables = mc.Variables b.client.Workspaces = mc.Workspaces + baseURL, err := url.Parse("https://app.terraform.io") + if err != nil { + t.Fatalf("testBackend: failed to parse base URL for client") + } + baseURL.Path = "/api/v2/" + + readRedactedPlan = func(ctx context.Context, baseURL url.URL, token, planID string) ([]byte, error) { + return mc.RedactedPlans.Read(ctx, baseURL.Hostname(), token, planID) + } + // Set local to a local test backend. b.local = testLocalBackend(t, b) return b, s.Close } -func testLocalBackend(t *testing.T, cloud *Cloud) backend.Enhanced { +func testLocalBackend(t *testing.T, cloud *Cloud) backendrun.OperationsBackend { b := backendLocal.NewWithBackend(cloud) // Add a test provider to the local backend. - p := backendLocal.TestLocalProvider(t, b, "null", &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + p := backendLocal.TestLocalProvider(t, b, "null", providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "null_resource": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Computed: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + }, }, }, }, @@ -324,6 +415,75 @@ func testServerWithHandlers(handlers map[string]func(http.ResponseWriter, *http. return httptest.NewServer(mux) } +func testServerWithSnapshotsEnabled(t *testing.T, enabled bool) *httptest.Server { + var serverURL string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Log(r.Method, r.URL.String()) + + if r.URL.Path == "/state-json" { + t.Log("pretending to be Archivist") + fakeState := states.NewState() + fakeStateFile := statefile.New(fakeState, "boop", 1) + var buf bytes.Buffer + statefile.Write(fakeStateFile, &buf) + respBody := buf.Bytes() + w.Header().Set("content-type", "application/json") + w.Header().Set("content-length", strconv.FormatInt(int64(len(respBody)), 10)) + w.WriteHeader(http.StatusOK) + w.Write(respBody) + return + } + + if r.URL.Path == "/api/ping" { + t.Log("pretending to be Ping") + w.WriteHeader(http.StatusNoContent) + return + } + + fakeBody := map[string]any{ + "data": map[string]any{ + "type": "state-versions", + "id": GenerateID("sv-"), + "attributes": map[string]any{ + "hosted-state-download-url": serverURL + "/state-json", + "hosted-state-upload-url": serverURL + "/state-json", + }, + }, + } + fakeBodyRaw, err := json.Marshal(fakeBody) + if err != nil { + t.Fatal(err) + } + + w.Header().Set("content-type", tfe.ContentTypeJSONAPI) + w.Header().Set("content-length", strconv.FormatInt(int64(len(fakeBodyRaw)), 10)) + + switch r.Method { + case "POST": + t.Log("pretending to be Create a State Version") + if enabled { + w.Header().Set("x-terraform-snapshot-interval", "300") + } + w.WriteHeader(http.StatusAccepted) + case "GET": + t.Log("pretending to be Fetch the Current State Version for a Workspace") + if enabled { + w.Header().Set("x-terraform-snapshot-interval", "300") + } + w.WriteHeader(http.StatusOK) + case "PUT": + t.Log("pretending to be Archivist") + default: + t.Fatal("don't know what API operation this was supposed to be") + } + + w.WriteHeader(http.StatusOK) + w.Write(fakeBodyRaw) + })) + serverURL = server.URL + return server +} + // testDefaultRequestHandlers is a map of request handlers intended to be used in a request // multiplexer for a test server. A caller may use testServerWithHandlers to start a server with // this base set of routes, and override a particular route for whatever edge case is being tested. @@ -406,6 +566,30 @@ var testDefaultRequestHandlers = map[string]func(http.ResponseWriter, *http.Requ }, } +func mockColorize() *colorstring.Colorize { + colors := make(map[string]string) + for k, v := range colorstring.DefaultColors { + colors[k] = v + } + colors["purple"] = "38;5;57" + + return &colorstring.Colorize{ + Colors: colors, + Disable: false, + Reset: true, + } +} + +func mockSROWorkspace(t *testing.T, b *Cloud, workspaceName string) { + _, err := b.client.Workspaces.Update(context.Background(), "hashicorp", workspaceName, tfe.WorkspaceUpdateOptions{ + StructuredRunOutputEnabled: tfe.Bool(true), + TerraformVersion: tfe.String("1.4.0"), + }) + if err != nil { + t.Fatalf("Error enabling SRO on workspace %s: %v", workspaceName, err) + } +} + // testDisco returns a *disco.Disco mapping app.terraform.io and // localhost to a local test server. func testDisco(s *httptest.Server) *disco.Disco { @@ -417,6 +601,7 @@ func testDisco(s *httptest.Server) *disco.Disco { d.ForceHostServices(svchost.Hostname(defaultHostname), services) d.ForceHostServices(svchost.Hostname("localhost"), services) + d.ForceHostServices(svchost.Hostname("nontfe.local"), nil) return d } @@ -432,9 +617,9 @@ func (v *unparsedVariableValue) ParseVariableValue(mode configs.VariableParsingM }, tfdiags.Diagnostics{} } -// testVariable returns a backend.UnparsedVariableValue used for testing. -func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backend.UnparsedVariableValue { - vars := make(map[string]backend.UnparsedVariableValue, len(vs)) +// testVariable returns a backendrun.UnparsedVariableValue used for testing. +func testVariables(s terraform.ValueSourceType, vs ...string) map[string]backendrun.UnparsedVariableValue { + vars := make(map[string]backendrun.UnparsedVariableValue, len(vs)) for _, v := range vs { vars[v] = &unparsedVariableValue{ value: v, diff --git a/internal/cloud/tfe_client_mock.go b/internal/cloud/tfe_client_mock.go index 2ac259a2eb..5a5ccaf33b 100644 --- a/internal/cloud/tfe_client_mock.go +++ b/internal/cloud/tfe_client_mock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cloud import ( @@ -16,8 +19,8 @@ import ( "time" tfe "github.com/hashicorp/go-tfe" - "github.com/mitchellh/copystructure" + "github.com/hashicorp/terraform/internal/copy" tfversion "github.com/hashicorp/terraform/version" ) @@ -27,10 +30,17 @@ type MockClient struct { CostEstimates *MockCostEstimates Organizations *MockOrganizations Plans *MockPlans + PolicySetOutcomes *MockPolicySetOutcomes + TaskStages *MockTaskStages + RedactedPlans *MockRedactedPlans PolicyChecks *MockPolicyChecks + Projects *MockProjects + RegistryModules *MockRegistryModules Runs *MockRuns + RunEvents *MockRunEvents StateVersions *MockStateVersions StateVersionOutputs *MockStateVersionOutputs + TestRuns *MockTestRuns Variables *MockVariables Workspaces *MockWorkspaces } @@ -42,12 +52,19 @@ func NewMockClient() *MockClient { c.CostEstimates = newMockCostEstimates(c) c.Organizations = newMockOrganizations(c) c.Plans = newMockPlans(c) + c.TaskStages = newMockTaskStages(c) + c.PolicySetOutcomes = newMockPolicySetOutcomes(c) c.PolicyChecks = newMockPolicyChecks(c) + c.Projects = newMockProjects(c) + c.RegistryModules = newMockRegistryModules(c) c.Runs = newMockRuns(c) + c.RunEvents = newMockRunEvents(c) c.StateVersions = newMockStateVersions(c) c.StateVersionOutputs = newMockStateVersionOutputs(c) + c.TestRuns = newMockTestRuns(c) c.Variables = newMockVariables(c) c.Workspaces = newMockWorkspaces(c) + c.RedactedPlans = newMockRedactedPlans(c) return c } @@ -161,6 +178,8 @@ type MockConfigurationVersions struct { uploadURLs map[string]*tfe.ConfigurationVersion } +var _ tfe.ConfigurationVersions = (*MockConfigurationVersions)(nil) + func newMockConfigurationVersions(client *MockClient) *MockConfigurationVersions { return &MockConfigurationVersions{ client: client, @@ -197,6 +216,30 @@ func (m *MockConfigurationVersions) Create(ctx context.Context, workspaceID stri UploadURL: url, } + if options.Provisional != nil && *options.Provisional { + cv.Provisional = true + } + + if options.Speculative != nil && *options.Speculative { + cv.Speculative = true + } + + m.configVersions[cv.ID] = cv + m.uploadURLs[url] = cv + + return cv, nil +} + +func (m *MockConfigurationVersions) CreateForRegistryModule(ctx context.Context, moduleID tfe.RegistryModuleID) (*tfe.ConfigurationVersion, error) { + id := GenerateID("cv-") + url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) + + cv := &tfe.ConfigurationVersion{ + ID: id, + Status: tfe.ConfigurationPending, + UploadURL: url, + } + m.configVersions[cv.ID] = cv m.uploadURLs[url] = cv @@ -226,6 +269,11 @@ func (m *MockConfigurationVersions) Upload(ctx context.Context, url, path string } m.uploadPaths[cv.ID] = path cv.Status = tfe.ConfigurationUploaded + + return m.UploadTarGzip(ctx, url, nil) +} + +func (m *MockConfigurationVersions) UploadTarGzip(ctx context.Context, url string, archive io.Reader) error { return nil } @@ -237,6 +285,18 @@ func (m *MockConfigurationVersions) Download(ctx context.Context, cvID string) ( panic("not implemented") } +func (m *MockConfigurationVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error { + panic("not implemented") +} + +func (m *MockConfigurationVersions) RestoreBackingData(ctx context.Context, svID string) error { + panic("not implemented") +} + +func (m *MockConfigurationVersions) SoftDeleteBackingData(ctx context.Context, svID string) error { + panic("not implemented") +} + type MockCostEstimates struct { client *MockClient Estimations map[string]*tfe.CostEstimate @@ -319,6 +379,8 @@ func (m *MockCostEstimates) Logs(ctx context.Context, costEstimateID string) (io return bytes.NewBuffer(logs), nil } +var _ tfe.Organizations = (*MockOrganizations)(nil) + type MockOrganizations struct { client *MockClient organizations map[string]*tfe.Organization @@ -381,6 +443,10 @@ func (m *MockOrganizations) Create(ctx context.Context, options tfe.Organization } func (m *MockOrganizations) Read(ctx context.Context, name string) (*tfe.Organization, error) { + return m.ReadWithOptions(ctx, name, tfe.OrganizationReadOptions{}) +} + +func (m *MockOrganizations) ReadWithOptions(ctx context.Context, name string, options tfe.OrganizationReadOptions) (*tfe.Organization, error) { org, ok := m.organizations[name] if !ok { return nil, tfe.ErrResourceNotFound @@ -444,10 +510,80 @@ func (m *MockOrganizations) ReadRunQueue(ctx context.Context, name string, optio return rq, nil } +func (m *MockOrganizations) ReadDataRetentionPolicyChoice(ctx context.Context, organization string) (*tfe.DataRetentionPolicyChoice, error) { + panic("not implemented") +} + +func (m *MockOrganizations) DeleteDataRetentionPolicy(context.Context, string) error { + panic("not implemented") +} + +func (m *MockOrganizations) ReadDataRetentionPolicy(context.Context, string) (*tfe.DataRetentionPolicy, error) { + panic("not implemented") +} + +func (m *MockOrganizations) SetDataRetentionPolicy(ctx context.Context, organization string, options tfe.DataRetentionPolicySetOptions) (*tfe.DataRetentionPolicy, error) { + panic("not implemented") +} + +func (m *MockOrganizations) SetDataRetentionPolicyDeleteOlder(ctx context.Context, organization string, options tfe.DataRetentionPolicyDeleteOlderSetOptions) (*tfe.DataRetentionPolicyDeleteOlder, error) { + panic("not implemented") +} + +func (m *MockOrganizations) SetDataRetentionPolicyDontDelete(ctx context.Context, organization string, options tfe.DataRetentionPolicyDontDeleteSetOptions) (*tfe.DataRetentionPolicyDontDelete, error) { + panic("not implemented") +} + +type MockRedactedPlans struct { + client *MockClient + redactedPlans map[string][]byte +} + +func newMockRedactedPlans(client *MockClient) *MockRedactedPlans { + return &MockRedactedPlans{ + client: client, + redactedPlans: make(map[string][]byte), + } +} + +func (m *MockRedactedPlans) create(cvID, workspaceID, planID string) error { + w, ok := m.client.Workspaces.workspaceIDs[workspaceID] + if !ok { + return tfe.ErrResourceNotFound + } + + planPath := filepath.Join( + m.client.ConfigurationVersions.uploadPaths[cvID], + w.WorkingDirectory, + "plan-redacted.json", + ) + + redactedPlanFile, err := os.Open(planPath) + if err != nil { + return err + } + + raw, err := io.ReadAll(redactedPlanFile) + if err != nil { + return err + } + + m.redactedPlans[planID] = raw + + return nil +} + +func (m *MockRedactedPlans) Read(ctx context.Context, hostname, token, planID string) ([]byte, error) { + if p, ok := m.redactedPlans[planID]; ok { + return p, nil + } + return nil, tfe.ErrResourceNotFound +} + type MockPlans struct { client *MockClient logs map[string]string - planOutputs map[string]string + planOutputs map[string][]byte plans map[string]*tfe.Plan } @@ -455,7 +591,7 @@ func newMockPlans(client *MockClient) *MockPlans { return &MockPlans{ client: client, logs: make(map[string]string), - planOutputs: make(map[string]string), + planOutputs: make(map[string][]byte), plans: make(map[string]*tfe.Plan), } } @@ -482,6 +618,17 @@ func (m *MockPlans) create(cvID, workspaceID string) (*tfe.Plan, error) { w.WorkingDirectory, "plan.log", ) + + // Try to load unredacted json output, if it exists + outputPath := filepath.Join( + m.client.ConfigurationVersions.uploadPaths[cvID], + w.WorkingDirectory, + "plan-unredacted.json", + ) + if outBytes, err := os.ReadFile(outputPath); err == nil { + m.planOutputs[p.ID] = outBytes + } + m.plans[p.ID] = p return p, nil @@ -542,7 +689,165 @@ func (m *MockPlans) ReadJSONOutput(ctx context.Context, planID string) ([]byte, return nil, tfe.ErrResourceNotFound } - return []byte(planOutput), nil + return planOutput, nil +} + +type MockTaskStages struct { + client *MockClient +} + +func newMockTaskStages(client *MockClient) *MockTaskStages { + return &MockTaskStages{ + client: client, + } +} + +func (m *MockTaskStages) Override(ctx context.Context, taskStageID string, options tfe.TaskStageOverrideOptions) (*tfe.TaskStage, error) { + switch taskStageID { + case "ts-err": + return nil, errors.New("test error") + + default: + return nil, nil + } +} + +func (m *MockTaskStages) Read(ctx context.Context, taskStageID string, options *tfe.TaskStageReadOptions) (*tfe.TaskStage, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockTaskStages) List(ctx context.Context, runID string, options *tfe.TaskStageListOptions) (*tfe.TaskStageList, error) { + //TODO implement me + panic("implement me") +} + +type MockPolicySetOutcomes struct { + client *MockClient +} + +func newMockPolicySetOutcomes(client *MockClient) *MockPolicySetOutcomes { + return &MockPolicySetOutcomes{ + client: client, + } +} + +func (m *MockPolicySetOutcomes) List(ctx context.Context, policyEvaluationID string, options *tfe.PolicySetOutcomeListOptions) (*tfe.PolicySetOutcomeList, error) { + switch policyEvaluationID { + case "pol-pass": + return &tfe.PolicySetOutcomeList{ + Items: []*tfe.PolicySetOutcome{ + { + ID: policyEvaluationID, + Outcomes: []tfe.Outcome{ + { + EnforcementLevel: "mandatory", + Query: "data.example.rule", + Status: "passed", + PolicyName: "policy-pass", + Description: "This policy will pass", + }, + }, + Overridable: tfe.Bool(true), + Error: "", + PolicySetName: "policy-set-that-passes", + PolicySetDescription: "This policy set will always pass", + ResultCount: tfe.PolicyResultCount{ + AdvisoryFailed: 0, + MandatoryFailed: 0, + Passed: 1, + Errored: 0, + }, + }, + }, + }, nil + case "pol-fail": + return &tfe.PolicySetOutcomeList{ + Items: []*tfe.PolicySetOutcome{ + { + ID: policyEvaluationID, + Outcomes: []tfe.Outcome{ + { + EnforcementLevel: "mandatory", + Query: "data.example.rule", + Status: "failed", + PolicyName: "policy-fail", + Description: "This policy will fail", + }, + }, + Overridable: tfe.Bool(true), + Error: "", + PolicySetName: "policy-set-that-fails", + PolicySetDescription: "This policy set will always fail", + ResultCount: tfe.PolicyResultCount{ + AdvisoryFailed: 0, + MandatoryFailed: 1, + Passed: 0, + Errored: 0, + }, + }, + }, + }, nil + + case "adv-fail": + return &tfe.PolicySetOutcomeList{ + Items: []*tfe.PolicySetOutcome{ + { + ID: policyEvaluationID, + Outcomes: []tfe.Outcome{ + { + EnforcementLevel: "advisory", + Query: "data.example.rule", + Status: "failed", + PolicyName: "policy-fail", + Description: "This policy will fail", + }, + }, + Overridable: tfe.Bool(true), + Error: "", + PolicySetName: "policy-set-that-fails", + PolicySetDescription: "This policy set will always fail", + ResultCount: tfe.PolicyResultCount{ + AdvisoryFailed: 1, + MandatoryFailed: 0, + Passed: 0, + Errored: 0, + }, + }, + }, + }, nil + default: + return &tfe.PolicySetOutcomeList{ + Items: []*tfe.PolicySetOutcome{ + { + ID: policyEvaluationID, + Outcomes: []tfe.Outcome{ + { + EnforcementLevel: "mandatory", + Query: "data.example.rule", + Status: "passed", + PolicyName: "policy-pass", + Description: "This policy will pass", + }, + }, + Overridable: tfe.Bool(true), + Error: "", + PolicySetName: "policy-set-that-passes", + PolicySetDescription: "This policy set will always pass", + ResultCount: tfe.PolicyResultCount{ + AdvisoryFailed: 0, + MandatoryFailed: 0, + Passed: 1, + Errored: 0, + }, + }, + }, + }, nil + } +} + +func (m *MockPolicySetOutcomes) Read(ctx context.Context, policySetOutcomeID string) (*tfe.PolicySetOutcome, error) { + return nil, nil } type MockPolicyChecks struct { @@ -704,6 +1009,211 @@ func (m *MockPolicyChecks) Logs(ctx context.Context, policyCheckID string) (io.R return bytes.NewBuffer(logs), nil } +type MockProjects struct { + client *MockClient + projects map[string]*tfe.Project +} + +func newMockProjects(client *MockClient) *MockProjects { + return &MockProjects{ + client: client, + projects: make(map[string]*tfe.Project), + } +} + +func (m *MockProjects) Create(ctx context.Context, organization string, options tfe.ProjectCreateOptions) (*tfe.Project, error) { + id := GenerateID("prj-") + + p := &tfe.Project{ + ID: id, + Name: options.Name, + } + + m.projects[p.ID] = p + + return p, nil +} + +func (m *MockProjects) List(ctx context.Context, organization string, options *tfe.ProjectListOptions) (*tfe.ProjectList, error) { + pl := &tfe.ProjectList{} + + for _, project := range m.projects { + pc := copy.DeepCopyValue(project) + pl.Items = append(pl.Items, pc) + } + + pl.Pagination = &tfe.Pagination{ + CurrentPage: 1, + NextPage: 1, + PreviousPage: 1, + TotalPages: 1, + TotalCount: len(pl.Items), + } + + return pl, nil +} + +func (m *MockProjects) Read(ctx context.Context, projectID string) (*tfe.Project, error) { + p, ok := m.projects[projectID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + + // we must return a copy for the client + pc := copy.DeepCopyValue(p) + return pc, nil +} + +func (m *MockProjects) Update(ctx context.Context, projectID string, options tfe.ProjectUpdateOptions) (*tfe.Project, error) { + p, ok := m.projects[projectID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + + p.Name = *options.Name + + // we must return a copy for the client + pc := copy.DeepCopyValue(p) + return pc, nil +} + +func (m *MockProjects) Delete(ctx context.Context, projectID string) error { + var p *tfe.Project = nil + for _, p := range m.projects { + if p.ID == projectID { + + break + } + } + if p == nil { + return tfe.ErrResourceNotFound + } + + delete(m.projects, p.Name) + + return nil +} + +type MockRegistryModules struct { + sync.Mutex + + client *MockClient + Modules map[string]*tfe.RegistryModule + organizations map[string][]*tfe.RegistryModule +} + +var _ tfe.RegistryModules = (*MockRegistryModules)(nil) + +func newMockRegistryModules(client *MockClient) *MockRegistryModules { + return &MockRegistryModules{ + client: client, + Modules: make(map[string]*tfe.RegistryModule), + organizations: make(map[string][]*tfe.RegistryModule), + } +} + +func (m *MockRegistryModules) List(ctx context.Context, organization string, options *tfe.RegistryModuleListOptions) (*tfe.RegistryModuleList, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) Create(ctx context.Context, organization string, options tfe.RegistryModuleCreateOptions) (*tfe.RegistryModule, error) { + m.Lock() + defer m.Unlock() + + org, err := m.client.Organizations.Read(ctx, organization) + if err != nil { + return nil, err + } + + id := fmt.Sprintf("%s/%s/%s", options.Namespace, *options.Name, *options.Provider) + if _, exists := m.Modules[id]; exists { + panic("already exists") + } + + module := &tfe.RegistryModule{ + ID: id, + Name: *options.Name, + Provider: *options.Provider, + RegistryName: options.RegistryName, + Namespace: options.Namespace, + VersionStatuses: nil, + CreatedAt: time.Now().UTC().String(), + UpdatedAt: time.Now().UTC().String(), + Organization: org, + } + + m.Modules[id] = module + m.organizations[org.ExternalID] = append(m.organizations[org.ExternalID], module) + return module, nil +} + +func (m *MockRegistryModules) CreateVersion(ctx context.Context, moduleID tfe.RegistryModuleID, options tfe.RegistryModuleCreateVersionOptions) (*tfe.RegistryModuleVersion, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) CreateWithVCSConnection(ctx context.Context, options tfe.RegistryModuleCreateWithVCSConnectionOptions) (*tfe.RegistryModule, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) Read(ctx context.Context, moduleID tfe.RegistryModuleID) (*tfe.RegistryModule, error) { + m.Lock() + defer m.Unlock() + + id := fmt.Sprintf("%s/%s/%s", moduleID.Namespace, moduleID.Name, moduleID.Provider) + module, exists := m.Modules[id] + if !exists { + return nil, tfe.ErrResourceNotFound + } + return module, nil +} + +func (m *MockRegistryModules) Delete(ctx context.Context, organization string, name string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) DeleteProvider(ctx context.Context, moduleID tfe.RegistryModuleID) error { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) DeleteVersion(ctx context.Context, moduleID tfe.RegistryModuleID, version string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) DeleteByName(ctx context.Context, moduleID tfe.RegistryModuleID) error { + panic("implement me") +} + +func (m *MockRegistryModules) Update(ctx context.Context, moduleID tfe.RegistryModuleID, options tfe.RegistryModuleUpdateOptions) (*tfe.RegistryModule, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) Upload(ctx context.Context, rmv tfe.RegistryModuleVersion, path string) error { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) UploadTarGzip(ctx context.Context, url string, r io.Reader) error { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) ListCommits(ctx context.Context, moduleID tfe.RegistryModuleID) (*tfe.CommitList, error) { + //TODO implement me + panic("implement me") +} + +func (m *MockRegistryModules) ReadVersion(ctx context.Context, moduleID tfe.RegistryModuleID, version string) (*tfe.RegistryModuleVersion, error) { + //TODO implement me + panic("implement me") +} + type MockRuns struct { sync.Mutex @@ -736,11 +1246,8 @@ func (m *MockRuns) List(ctx context.Context, workspaceID string, options *tfe.Ru rl := &tfe.RunList{} for _, run := range m.workspaces[w.ID] { - rc, err := copystructure.Copy(run) - if err != nil { - panic(err) - } - rl.Items = append(rl.Items, rc.(*tfe.Run)) + rc := copy.DeepCopyValue(run) + rl.Items = append(rl.Items, rc) } rl.Pagination = &tfe.Pagination{ @@ -779,16 +1286,17 @@ func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t } r := &tfe.Run{ - ID: GenerateID("run-"), - Actions: &tfe.RunActions{IsCancelable: true}, - Apply: a, - CostEstimate: ce, - HasChanges: false, - Permissions: &tfe.RunPermissions{}, - Plan: p, - ReplaceAddrs: options.ReplaceAddrs, - Status: tfe.RunPending, - TargetAddrs: options.TargetAddrs, + ID: GenerateID("run-"), + Actions: &tfe.RunActions{IsCancelable: true}, + Apply: a, + CostEstimate: ce, + HasChanges: false, + Permissions: &tfe.RunPermissions{}, + Plan: p, + ReplaceAddrs: options.ReplaceAddrs, + Status: tfe.RunPending, + TargetAddrs: options.TargetAddrs, + AllowConfigGeneration: options.AllowConfigGeneration, } if options.Message != nil { @@ -811,6 +1319,10 @@ func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t r.RefreshOnly = *options.RefreshOnly } + if options.AllowConfigGeneration != nil && *options.AllowConfigGeneration { + r.Plan.GeneratedConfiguration = true + } + w, ok := m.client.Workspaces.workspaceIDs[options.Workspace.ID] if !ok { return nil, tfe.ErrResourceNotFound @@ -819,6 +1331,19 @@ func (m *MockRuns) Create(ctx context.Context, options tfe.RunCreateOptions) (*t w.CurrentRun = r } + r.Workspace = &tfe.Workspace{ + ID: w.ID, + StructuredRunOutputEnabled: w.StructuredRunOutputEnabled, + TerraformVersion: w.TerraformVersion, + } + + if w.StructuredRunOutputEnabled { + err := m.client.RedactedPlans.create(options.ConfigurationVersion.ID, options.Workspace.ID, p.ID) + if err != nil { + return nil, err + } + } + if m.ModifyNewRun != nil { // caller-provided callback may modify the run in-place to mimic // side-effects that a real server might take in some situations. @@ -835,7 +1360,7 @@ func (m *MockRuns) Read(ctx context.Context, runID string) (*tfe.Run, error) { return m.ReadWithOptions(ctx, runID, nil) } -func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.RunReadOptions) (*tfe.Run, error) { +func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, options *tfe.RunReadOptions) (*tfe.Run, error) { m.Lock() defer m.Unlock() @@ -859,15 +1384,23 @@ func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.Run } logs, _ := ioutil.ReadFile(m.client.Plans.logs[r.Plan.LogReadURL]) - if r.Status == tfe.RunPlanning && r.Plan.Status == tfe.PlanFinished { - if r.IsDestroy || bytes.Contains(logs, []byte("1 to add, 0 to change, 0 to destroy")) { + if (r.Status == tfe.RunPlanning || r.Status == tfe.RunPlannedAndSaved) && r.Plan.Status == tfe.PlanFinished { + hasChanges := r.IsDestroy || + bytes.Contains(logs, []byte("1 to add")) || + bytes.Contains(logs, []byte("1 to change")) || + bytes.Contains(logs, []byte("1 to import")) + if hasChanges { r.Actions.IsCancelable = false r.Actions.IsConfirmable = true r.HasChanges = true + r.Plan.HasChanges = true r.Permissions.CanApply = true } - if bytes.Contains(logs, []byte("null_resource.foo: 1 error")) { + hasError := bytes.Contains(logs, []byte("null_resource.foo: 1 error")) || + bytes.Contains(logs, []byte("Error: Unsupported block type")) || + bytes.Contains(logs, []byte("Error: Conflicting configuration arguments")) + if hasError { r.Actions.IsCancelable = false r.HasChanges = false r.Status = tfe.RunErrored @@ -875,12 +1408,22 @@ func (m *MockRuns) ReadWithOptions(ctx context.Context, runID string, _ *tfe.Run } // we must return a copy for the client - rc, err := copystructure.Copy(r) - if err != nil { - panic(err) + r = copy.DeepCopyValue(r) + + // After copying, handle includes... or at least, any includes we're known to rely on. + if options != nil { + for _, n := range options.Include { + switch n { + case tfe.RunWorkspace: + ws, ok := m.client.Workspaces.workspaceIDs[r.Workspace.ID] + if ok { + r.Workspace = ws + } + } + } } - return rc.(*tfe.Run), nil + return r, nil } func (m *MockRuns) Apply(ctx context.Context, runID string, options tfe.RunApplyOptions) error { @@ -908,6 +1451,10 @@ func (m *MockRuns) ForceCancel(ctx context.Context, runID string, options tfe.Ru panic("not implemented") } +func (m *MockRuns) ForceExecute(ctx context.Context, runID string) error { + panic("implement me") +} + func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDiscardOptions) error { m.Lock() defer m.Unlock() @@ -921,6 +1468,33 @@ func (m *MockRuns) Discard(ctx context.Context, runID string, options tfe.RunDis return nil } +type MockRunEvents struct{} + +func newMockRunEvents(_ *MockClient) *MockRunEvents { + return &MockRunEvents{} +} + +// List all the runs events of the given run. +func (m *MockRunEvents) List(ctx context.Context, runID string, options *tfe.RunEventListOptions) (*tfe.RunEventList, error) { + return &tfe.RunEventList{ + Items: []*tfe.RunEvent{}, + }, nil +} + +func (m *MockRunEvents) Read(ctx context.Context, runEventID string) (*tfe.RunEvent, error) { + return m.ReadWithOptions(ctx, runEventID, nil) +} + +func (m *MockRunEvents) ReadWithOptions(ctx context.Context, runEventID string, options *tfe.RunEventReadOptions) (*tfe.RunEvent, error) { + return &tfe.RunEvent{ + ID: GenerateID("re-"), + Action: "created", + CreatedAt: time.Now(), + }, nil +} + +var _ tfe.StateVersions = (*MockStateVersions)(nil) + type MockStateVersions struct { client *MockClient states map[string][]byte @@ -968,6 +1542,7 @@ func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, opti sv := &tfe.StateVersion{ ID: id, DownloadURL: url, + UploadURL: fmt.Sprintf("/_archivist/upload/%s", id), Serial: *options.Serial, } @@ -975,7 +1550,6 @@ func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, opti if err != nil { return nil, err } - m.states[sv.DownloadURL] = state m.outputStates[sv.ID] = []byte(*options.JSONStateOutputs) m.stateVersions[sv.ID] = sv @@ -984,6 +1558,13 @@ func (m *MockStateVersions) Create(ctx context.Context, workspaceID string, opti return sv, nil } +func (m *MockStateVersions) Upload(ctx context.Context, workspaceID string, options tfe.StateVersionUploadOptions) (*tfe.StateVersion, error) { + createOptions := options.StateVersionCreateOptions + createOptions.State = tfe.String(base64.StdEncoding.EncodeToString(options.RawState)) + + return m.Create(ctx, workspaceID, createOptions) +} + func (m *MockStateVersions) Read(ctx context.Context, svID string) (*tfe.StateVersion, error) { return m.ReadWithOptions(ctx, svID, nil) } @@ -1031,6 +1612,18 @@ func (m *MockStateVersions) ListOutputs(ctx context.Context, svID string, option panic("not implemented") } +func (s *MockStateVersions) SoftDeleteBackingData(ctx context.Context, svID string) error { + panic("not implemented") +} + +func (s *MockStateVersions) RestoreBackingData(ctx context.Context, svID string) error { + panic("not implemented") +} + +func (s *MockStateVersions) PermanentlyDeleteBackingData(ctx context.Context, svID string) error { + panic("not implemented") +} + type MockStateVersionOutputs struct { client *MockClient outputs map[string]*tfe.StateVersionOutput @@ -1074,6 +1667,207 @@ func (m *MockStateVersionOutputs) ReadCurrent(ctx context.Context, workspaceID s return svl, nil } +type MockTestRuns struct { + sync.Mutex + + client *MockClient + + // TestRuns and modules keep track of our tfe.TestRun objects. + TestRuns map[string]*tfe.TestRun + modules map[string][]*tfe.TestRun + logs map[string]string + parallelism int + + // delayedCancel allows a mock test run to cancel an operation instead of + // completing an operation. It's used + delayedCancel context.CancelFunc + cancelled bool + + // cancels counts the number of cancels that have been called. targetCancels + // tells the mock how many cancels we should receive before we let things + // finish. This is for testing the stop/cancel relationship. + cancels int + targetCancels int +} + +var _ tfe.TestRuns = (*MockTestRuns)(nil) + +func newMockTestRuns(client *MockClient) *MockTestRuns { + return &MockTestRuns{ + client: client, + TestRuns: make(map[string]*tfe.TestRun), + modules: make(map[string][]*tfe.TestRun), + logs: make(map[string]string), + cancelled: false, + } +} + +func (m *MockTestRuns) List(ctx context.Context, registryModuleId tfe.RegistryModuleID, options *tfe.TestRunListOptions) (*tfe.TestRunList, error) { + m.Lock() + defer m.Unlock() + + module, err := m.client.RegistryModules.Read(ctx, registryModuleId) + if err != nil { + return nil, err + } + + trl := &tfe.TestRunList{} + trl.Items = append(trl.Items, m.modules[module.ID]...) + trl.Pagination = &tfe.Pagination{ + CurrentPage: 1, + PreviousPage: 1, + NextPage: 1, + TotalPages: 1, + TotalCount: len(trl.Items), + } + + return trl, nil +} + +func (m *MockTestRuns) Read(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) (*tfe.TestRun, error) { + m.Lock() + defer m.Unlock() + + if tr, exists := m.TestRuns[testRunID]; exists { + + // This just simulates some natural progression, the first time a + // test run is read it'll progress from queued to running. + switch tr.Status { + case tfe.TestRunQueued: + tr.Status = tfe.TestRunRunning + } + + return tr, nil + } + return nil, tfe.ErrResourceNotFound +} + +func (m *MockTestRuns) Create(ctx context.Context, options tfe.TestRunCreateOptions) (*tfe.TestRun, error) { + m.Lock() + defer m.Unlock() + + if options.ConfigurationVersion.Status != tfe.ConfigurationUploaded { + return nil, fmt.Errorf("configuration hasn't been uploaded") + } + + id := GenerateID("testrun-") + url := fmt.Sprintf("https://app.terraform.io/_archivist/%s", id) + + tr := &tfe.TestRun{ + ID: id, + LogReadURL: url, + Status: tfe.TestRunQueued, + + ConfigurationVersion: options.ConfigurationVersion, + RegistryModule: options.RegistryModule, + } + + m.TestRuns[tr.ID] = tr + m.logs[tr.LogReadURL] = filepath.Join( + m.client.ConfigurationVersions.uploadPaths[options.ConfigurationVersion.ID], + "test.log", + ) + m.modules[tr.RegistryModule.ID] = append(m.modules[tr.RegistryModule.ID], tr) + if options.Parallelism != nil { + m.parallelism = *options.Parallelism + } + + return tr, nil +} + +func (m *MockTestRuns) Logs(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) (io.Reader, error) { + m.Lock() + defer m.Unlock() + + tr, exists := m.TestRuns[testRunID] + if !exists { + return nil, tfe.ErrResourceNotFound + } + + logfile, exists := m.logs[tr.LogReadURL] + if !exists { + return nil, tfe.ErrResourceNotFound + } + + logs, err := os.ReadFile(logfile) + if err != nil { + return nil, err + } + + done := func() (bool, error) { + m.Lock() + defer m.Unlock() + + tr, exists := m.TestRuns[testRunID] + if !exists { + return false, tfe.ErrResourceNotFound + } + + switch tr.Status { + case tfe.TestRunRunning: + + // The first time the done function is called we'll progress from + // running into finished. We may instead cancel this if the + // delayedCancel trigger is set. + + if m.delayedCancel != nil { + if !m.cancelled { + // Make sure we only trigger the cancel once. + m.delayedCancel() + m.cancelled = true + } + return false, nil + } else { + + if m.targetCancels == 0 { + // Update the status so that on the next call it thinks it's + // finished. + tr.Status = tfe.TestRunFinished + tr.TestStatus = tfe.TestPass + } + + return false, nil + } + + case tfe.TestRunFinished, tfe.TestRunCanceled: + // We're done. + return true, nil + + case tfe.TestRunQueued: + // We shouldn't call the Logs function before the test has started + // so if that happens let's just trigger a panic. + panic("shouldn't call Logs on a queued test run") + default: + panic("unrecognized test status: " + string(tr.Status)) + } + } + + return &mockLogReader{ + done: done, + logs: bytes.NewBuffer(logs), + }, nil +} + +func (m *MockTestRuns) Cancel(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) error { + m.Lock() + defer m.Unlock() + + tr, exists := m.TestRuns[testRunID] + if !exists { + return tfe.ErrResourceNotFound + } + + m.cancels++ + if m.cancels >= m.targetCancels { + tr.Status = tfe.TestRunCanceled + } + return nil +} + +func (m *MockTestRuns) ForceCancel(ctx context.Context, moduleID tfe.RegistryModuleID, testRunID string) error { + panic("not implemented, you can't force cancel a test run via the Terraform CLI") +} + type MockVariables struct { client *MockClient workspaces map[string]*tfe.VariableList @@ -1133,6 +1927,8 @@ func (m *MockVariables) Delete(ctx context.Context, workspaceID string, variable panic("not implemented") } +var _ tfe.Workspaces = (*MockWorkspaces)(nil) + type MockWorkspaces struct { client *MockClient workspaceIDs map[string]*tfe.Workspace @@ -1150,35 +1946,51 @@ func newMockWorkspaces(client *MockClient) *MockWorkspaces { func (m *MockWorkspaces) List(ctx context.Context, organization string, options *tfe.WorkspaceListOptions) (*tfe.WorkspaceList, error) { wl := &tfe.WorkspaceList{} // Get all the workspaces that match the Search value - searchValue := "" var ws []*tfe.Workspace - var tags []string + searchValue := "" + searchTags := make(map[string]string) if options != nil { if len(options.Search) > 0 { searchValue = options.Search } if len(options.Tags) > 0 { - tags = strings.Split(options.Tags, ",") + for _, tag := range strings.Split(options.Tags, ",") { + searchTags[tag] = "" + } + } + if len(options.TagBindings) > 0 { + for _, kvTag := range options.TagBindings { + searchTags[kvTag.Key] = kvTag.Value + } } } for _, w := range m.workspaceIDs { - wTags := make(map[string]struct{}) + wTags := make(map[string]string) for _, wTag := range w.Tags { - wTags[wTag.Name] = struct{}{} + wTags[wTag.Name] = "" } - if strings.Contains(w.Name, searchValue) { - tagsSatisfied := true - for _, tag := range tags { - if _, ok := wTags[tag]; !ok { + for _, kvTag := range w.TagBindings { + wTags[kvTag.Key] = kvTag.Value + } + + tagsSatisfied := true + for k, v := range searchTags { + if value, ok := wTags[k]; ok { + if value != v { tagsSatisfied = false + break } + } else { + tagsSatisfied = false + break } - if tagsSatisfied { - ws = append(ws, w) - } + } + + if strings.Contains(w.Name, searchValue) && tagsSatisfied { + ws = append(ws, w) } } @@ -1218,10 +2030,31 @@ func (m *MockWorkspaces) List(ctx context.Context, organization string, options return wl, nil } +func (m *MockWorkspaces) ListTagBindings(ctx context.Context, workspaceID string) ([]*tfe.TagBinding, error) { + for _, w := range m.workspaceIDs { + if w.ID == workspaceID { + return w.TagBindings, nil + } + } + + return nil, tfe.ErrResourceNotFound +} + +func (m *MockWorkspaces) AddTagBindings(ctx context.Context, workspaceID string, options tfe.WorkspaceAddTagBindingsOptions) ([]*tfe.TagBinding, error) { + for id, w := range m.workspaceIDs { + if id == workspaceID { + w.TagBindings = options.TagBindings + return options.TagBindings, nil + } + } + + return nil, tfe.ErrResourceNotFound +} + func (m *MockWorkspaces) Create(ctx context.Context, organization string, options tfe.WorkspaceCreateOptions) (*tfe.Workspace, error) { // for TestCloud_setUnavailableTerraformVersion if *options.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { - return nil, fmt.Errorf("requested Terraform version not available in this TFC instance") + return nil, fmt.Errorf("requested Terraform version not available in this HCP Terraform instance") } if strings.HasSuffix(*options.Name, "no-operations") { options.Operations = tfe.Bool(false) @@ -1231,14 +2064,23 @@ func (m *MockWorkspaces) Create(ctx context.Context, organization string, option options.ExecutionMode = tfe.String("remote") } w := &tfe.Workspace{ - ID: GenerateID("ws-"), - Name: *options.Name, - ExecutionMode: *options.ExecutionMode, - Operations: *options.Operations, + ID: GenerateID("ws-"), + Name: *options.Name, + ExecutionMode: *options.ExecutionMode, + Operations: *options.Operations, + StructuredRunOutputEnabled: false, Permissions: &tfe.WorkspacePermissions{ - CanQueueApply: true, - CanQueueRun: true, + CanQueueApply: true, + CanQueueRun: true, + CanForceDelete: tfe.Bool(true), }, + Organization: &tfe.Organization{ + Name: organization, + }, + TagBindings: options.TagBindings, + } + if options.Project != nil { + w.Project = options.Project } if options.AutoApply != nil { w.AutoApply = *options.AutoApply @@ -1246,11 +2088,13 @@ func (m *MockWorkspaces) Create(ctx context.Context, organization string, option if options.VCSRepo != nil { w.VCSRepo = &tfe.VCSRepo{} } + if options.TerraformVersion != nil { w.TerraformVersion = *options.TerraformVersion } else { w.TerraformVersion = tfversion.String() } + var tags []*tfe.Tag for _, tag := range options.Tags { tags = append(tags, tag) @@ -1330,10 +2174,34 @@ func (m *MockWorkspaces) UpdateByID(ctx context.Context, workspaceID string, opt return w, nil } +func (m *MockWorkspaces) ListEffectiveTagBindings(ctx context.Context, workspaceID string) ([]*tfe.EffectiveTagBinding, error) { + w, ok := m.workspaceIDs[workspaceID] + if !ok { + return nil, tfe.ErrResourceNotFound + } + var effectiveTagBindings []*tfe.EffectiveTagBinding + for _, tb := range w.TagBindings { + effectiveTagBindings = append(effectiveTagBindings, &tfe.EffectiveTagBinding{ + Key: tb.Key, + Value: tb.Value, + }) + } + return effectiveTagBindings, nil +} + +func (m *MockWorkspaces) DeleteAllTagBindings(ctx context.Context, workspaceID string) error { + w, ok := m.workspaceIDs[workspaceID] + if !ok { + return tfe.ErrResourceNotFound + } + w.TagBindings = nil + return nil +} + func updateMockWorkspaceAttributes(w *tfe.Workspace, options tfe.WorkspaceUpdateOptions) error { // for TestCloud_setUnavailableTerraformVersion if w.Name == "unavailable-terraform-version" && options.TerraformVersion != nil { - return fmt.Errorf("requested Terraform version not available in this TFC instance") + return fmt.Errorf("requested Terraform version not available in this HCP Terraform instance") } if options.Operations != nil { @@ -1351,6 +2219,11 @@ func updateMockWorkspaceAttributes(w *tfe.Workspace, options tfe.WorkspaceUpdate if options.WorkingDirectory != nil { w.WorkingDirectory = *options.WorkingDirectory } + + if options.StructuredRunOutputEnabled != nil { + w.StructuredRunOutputEnabled = *options.StructuredRunOutputEnabled + } + return nil } @@ -1370,6 +2243,41 @@ func (m *MockWorkspaces) DeleteByID(ctx context.Context, workspaceID string) err return nil } +func (m *MockWorkspaces) SafeDelete(ctx context.Context, organization, workspace string) error { + w, ok := m.client.Workspaces.workspaceNames[workspace] + + if !ok { + return tfe.ErrResourceNotFound + } + + if w.Locked { + return errors.New("cannot safe delete locked workspace") + } + + if w.ResourceCount > 0 { + return fmt.Errorf("cannot safe delete workspace with %d resources", w.ResourceCount) + } + + return m.Delete(ctx, organization, workspace) +} + +func (m *MockWorkspaces) SafeDeleteByID(ctx context.Context, workspaceID string) error { + w, ok := m.client.Workspaces.workspaceIDs[workspaceID] + if !ok { + return tfe.ErrResourceNotFound + } + + if w.Locked { + return errors.New("cannot safe delete locked workspace") + } + + if w.ResourceCount > 0 { + return fmt.Errorf("cannot safe delete workspace with %d resources", w.ResourceCount) + } + + return m.DeleteByID(ctx, workspaceID) +} + func (m *MockWorkspaces) RemoveVCSConnection(ctx context.Context, organization, workspace string) (*tfe.Workspace, error) { w, ok := m.workspaceNames[workspace] if !ok { @@ -1464,6 +2372,30 @@ func (m *MockWorkspaces) RemoveTags(ctx context.Context, workspaceID string, opt panic("not implemented") } +func (s *MockWorkspaces) ReadDataRetentionPolicy(ctx context.Context, workspaceID string) (*tfe.DataRetentionPolicy, error) { + panic("not implemented") +} + +func (s *MockWorkspaces) SetDataRetentionPolicy(ctx context.Context, workspaceID string, options tfe.DataRetentionPolicySetOptions) (*tfe.DataRetentionPolicy, error) { + panic("not implemented") +} + +func (s *MockWorkspaces) DeleteDataRetentionPolicy(ctx context.Context, workspaceID string) error { + panic("not implemented") +} + +func (s *MockWorkspaces) ReadDataRetentionPolicyChoice(ctx context.Context, workspaceID string) (*tfe.DataRetentionPolicyChoice, error) { + panic("not implemented") +} + +func (s *MockWorkspaces) SetDataRetentionPolicyDeleteOlder(ctx context.Context, workspaceID string, options tfe.DataRetentionPolicyDeleteOlderSetOptions) (*tfe.DataRetentionPolicyDeleteOlder, error) { + panic("not implemented") +} + +func (s *MockWorkspaces) SetDataRetentionPolicyDontDelete(ctx context.Context, workspaceID string, options tfe.DataRetentionPolicyDontDeleteSetOptions) (*tfe.DataRetentionPolicyDontDelete, error) { + panic("not implemented") +} + const alphanumeric = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" func GenerateID(s string) string { diff --git a/internal/cloudplugin/binary.go b/internal/cloudplugin/binary.go new file mode 100644 index 0000000000..606741924e --- /dev/null +++ b/internal/cloudplugin/binary.go @@ -0,0 +1,275 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "log" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/hashicorp/go-getter" + svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform/internal/releaseauth" +) + +// BinaryManager downloads, caches, and returns information about the +// terraform-cloudplugin binary downloaded from the specified backend. +type BinaryManager struct { + signingKey string + binaryName string + cloudPluginDataDir string + overridePath string + host svchost.Hostname + client *CloudPluginClient + goos string + arch string + ctx context.Context +} + +// Binary is a struct containing the path to an authenticated binary corresponding to +// a backend service. +type Binary struct { + Path string + ProductVersion string + ResolvedFromCache bool + ResolvedFromDevOverride bool +} + +const ( + KB = 1000 + MB = 1000 * KB +) + +// BinaryManager initializes a new BinaryManager to broker data between the +// specified directory location containing cloudplugin package data and a +// HCP Terraform backend URL. +func NewBinaryManager(ctx context.Context, cloudPluginDataDir, overridePath string, serviceURL *url.URL, goos, arch string) (*BinaryManager, error) { + client, err := NewCloudPluginClient(ctx, serviceURL) + if err != nil { + return nil, fmt.Errorf("could not initialize cloudplugin version manager: %w", err) + } + + return &BinaryManager{ + cloudPluginDataDir: cloudPluginDataDir, + overridePath: overridePath, + host: svchost.Hostname(serviceURL.Host), + client: client, + binaryName: "terraform-cloudplugin", + goos: goos, + arch: arch, + ctx: ctx, + }, nil +} + +func (v BinaryManager) binaryLocation() string { + return path.Join(v.cloudPluginDataDir, "bin", fmt.Sprintf("%s_%s", v.goos, v.arch)) +} + +func (v BinaryManager) cachedVersion(version string) *string { + binaryPath := path.Join(v.binaryLocation(), v.binaryName) + + if _, err := os.Stat(binaryPath); err != nil { + return nil + } + + // The version from the manifest must match the contents of ".version" + versionData, err := os.ReadFile(path.Join(v.binaryLocation(), ".version")) + if err != nil || strings.Trim(string(versionData), " \n\r\t") != version { + return nil + } + + return &binaryPath +} + +// Resolve fetches, authenticates, and caches a plugin binary matching the specifications +// and returns its location and version. +func (v BinaryManager) Resolve() (*Binary, error) { + if v.overridePath != "" { + log.Printf("[TRACE] Using dev override for cloudplugin binary") + return v.resolveDev() + } + return v.resolveRelease() +} + +func (v BinaryManager) resolveDev() (*Binary, error) { + return &Binary{ + Path: v.overridePath, + ProductVersion: "dev", + ResolvedFromDevOverride: true, + }, nil +} + +func (v BinaryManager) resolveRelease() (*Binary, error) { + manifest, err := v.latestManifest(v.ctx) + if err != nil { + return nil, fmt.Errorf("could not resolve cloudplugin version for host %q: %w", v.host.ForDisplay(), err) + } + + buildInfo, err := manifest.Select(v.goos, v.arch) + if err != nil { + return nil, err + } + + // Check if there's a cached binary + if cachedBinary := v.cachedVersion(manifest.Version); cachedBinary != nil { + return &Binary{ + Path: *cachedBinary, + ProductVersion: manifest.Version, + ResolvedFromCache: true, + }, nil + } + + // Download the archive + t, err := os.CreateTemp(os.TempDir(), "terraform-cloudplugin") + if err != nil { + return nil, fmt.Errorf("failed to create temp file for download: %w", err) + } + defer os.Remove(t.Name()) + + err = v.client.DownloadFile(buildInfo.URL, t) + if err != nil { + return nil, err + } + t.Close() // Close only returns an error if it's already been called + + // Authenticate the archive + err = v.verifyCloudPlugin(manifest, buildInfo, t.Name()) + if err != nil { + return nil, fmt.Errorf("could not resolve cloudplugin version %q: %w", manifest.Version, err) + } + + // Unarchive + unzip := getter.ZipDecompressor{ + FilesLimit: 1, + FileSizeLimit: 500 * MB, + } + targetPath := v.binaryLocation() + log.Printf("[TRACE] decompressing %q to %q", t.Name(), targetPath) + + err = unzip.Decompress(targetPath, t.Name(), true, 0000) + if err != nil { + return nil, fmt.Errorf("failed to decompress cloud plugin: %w", err) + } + + err = os.WriteFile(path.Join(targetPath, ".version"), []byte(manifest.Version), 0644) + if err != nil { + log.Printf("[ERROR] failed to write .version file to %q: %s", targetPath, err) + } + + return &Binary{ + Path: path.Join(targetPath, v.binaryName), + ProductVersion: manifest.Version, + ResolvedFromCache: false, + }, nil +} + +// Useful for small files that can be decoded all at once +func (v BinaryManager) downloadFileBuffer(pathOrURL string) ([]byte, error) { + buffer := bytes.Buffer{} + err := v.client.DownloadFile(pathOrURL, &buffer) + if err != nil { + return nil, err + } + + return buffer.Bytes(), err +} + +// verifyCloudPlugin authenticates the downloaded release archive +func (v BinaryManager) verifyCloudPlugin(archiveManifest *Release, info *BuildArtifact, archiveLocation string) error { + signature, err := v.downloadFileBuffer(archiveManifest.URLSHASumsSignatures[0]) + if err != nil { + return fmt.Errorf("failed to download cloudplugin SHA256SUMS signature file: %w", err) + } + sums, err := v.downloadFileBuffer(archiveManifest.URLSHASums) + if err != nil { + return fmt.Errorf("failed to download cloudplugin SHA256SUMS file: %w", err) + } + + checksums, err := releaseauth.ParseChecksums(sums) + if err != nil { + return fmt.Errorf("failed to parse cloudplugin SHA256SUMS file: %w", err) + } + + filename := path.Base(info.URL) + reportedSHA, ok := checksums[filename] + if !ok { + return fmt.Errorf("could not find checksum for file %q", filename) + } + + sigAuth := releaseauth.NewSignatureAuthentication(signature, sums) + if len(v.signingKey) > 0 { + sigAuth.PublicKey = v.signingKey + } + + all := releaseauth.AllAuthenticators( + releaseauth.NewChecksumAuthentication(reportedSHA, archiveLocation), + sigAuth, + ) + + return all.Authenticate() +} + +func (v BinaryManager) latestManifest(ctx context.Context) (*Release, error) { + manifestCacheLocation := path.Join(v.cloudPluginDataDir, v.host.String(), "manifest.json") + + // Find the manifest cache for the hostname. + data, err := os.ReadFile(manifestCacheLocation) + modTime := time.Time{} + var localManifest *Release + if err != nil { + log.Printf("[TRACE] no cloudplugin manifest cache found for host %q", v.host) + } else { + log.Printf("[TRACE] cloudplugin manifest cache found for host %q", v.host) + + localManifest, err = decodeManifest(bytes.NewBuffer(data)) + modTime = localManifest.TimestampUpdated + if err != nil { + log.Printf("[WARN] failed to decode cloudplugin manifest cache %q: %s", manifestCacheLocation, err) + } + } + + // Even though we may have a local manifest, always see if there is a newer remote manifest + result, err := v.client.FetchManifest(modTime) + // FetchManifest can return nil, nil (see below) + if err != nil { + return nil, fmt.Errorf("failed to fetch cloudplugin manifest: %w", err) + } + + // No error and no remoteManifest means the existing manifest is not modified + // and it's safe to use the local manifest + if result == nil && localManifest != nil { + result = localManifest + } else { + data, err := json.Marshal(result) + if err != nil { + return nil, fmt.Errorf("failed to dump cloudplugin manifest to JSON: %w", err) + } + + // Ensure target directory exists + if err := os.MkdirAll(filepath.Dir(manifestCacheLocation), 0755); err != nil { + return nil, fmt.Errorf("failed to create cloudplugin manifest cache directory: %w", err) + } + + output, err := os.Create(manifestCacheLocation) + if err != nil { + return nil, fmt.Errorf("failed to create cloudplugin manifest cache: %w", err) + } + + _, err = output.Write(data) + if err != nil { + return nil, fmt.Errorf("failed to write cloudplugin manifest cache: %w", err) + } + log.Printf("[TRACE] wrote cloudplugin manifest cache to %q", manifestCacheLocation) + } + + return result, nil +} diff --git a/internal/cloudplugin/binary_test.go b/internal/cloudplugin/binary_test.go new file mode 100644 index 0000000000..f14df7b8b9 --- /dev/null +++ b/internal/cloudplugin/binary_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "context" + "net/url" + "os" + "path/filepath" + "testing" +) + +func assertResolvedBinary(t *testing.T, binary *Binary, assertCached, assertOverridden bool) { + t.Helper() + + if binary == nil { + t.Fatal("expected non-nil binary") + } + + if binary.ResolvedFromCache != assertCached { + t.Errorf("expected ResolvedFromCache to be %v, got %v", assertCached, binary.ResolvedFromCache) + } + + if binary.ResolvedFromDevOverride != assertOverridden { + t.Errorf("expected ResolvedFromDevOverride to be %v, got %v", assertOverridden, binary.ResolvedFromDevOverride) + } + + info, err := os.Stat(binary.Path) + if err != nil { + t.Fatalf("expected no error when getting binary location, got %q", err) + } + + if info.IsDir() || info.Size() == 0 { + t.Fatalf("expected non-zero file at %q", binary.Path) + } + + var expectedVersion string + if assertOverridden { + expectedVersion = "dev" + } else { + expectedVersion = "0.1.0" + } + + if binary.ProductVersion != expectedVersion { // from sample manifest + t.Errorf("expected product binary %q, got %q", expectedVersion, binary.ProductVersion) + } +} + +func TestBinaryManager_Resolve(t *testing.T) { + publicKey, err := os.ReadFile("testdata/sample.public.key") + if err != nil { + t.Fatal(err) + } + + server, err := newCloudPluginManifestHTTPTestServer(t) + if err != nil { + t.Fatalf("could not create test server: %s", err) + } + defer server.Close() + + serverURL, _ := url.Parse(server.URL) + serviceURL := serverURL.JoinPath("/api/cloudplugin/v1") + + tempDir := t.TempDir() + manager, err := NewBinaryManager(context.Background(), tempDir, "", serviceURL, "darwin", "amd64") + if err != nil { + t.Fatalf("expected no err, got: %s", err) + } + manager.signingKey = string(publicKey) + manager.binaryName = "toucan.txt" // The file contained in the test archive + + binary, err := manager.Resolve() + if err != nil { + t.Fatalf("expected no err, got %s", err) + } + + assertResolvedBinary(t, binary, false, false) + + // Resolving a second time should return a cached binary + binary, err = manager.Resolve() + if err != nil { + t.Fatalf("expected no err, got %s", err) + } + + assertResolvedBinary(t, binary, true, false) + + // Change the local binary data + err = os.WriteFile(filepath.Join(filepath.Dir(binary.Path), ".version"), []byte("0.0.9"), 0644) + if err != nil { + t.Fatalf("could not write to .binary file: %s", err) + } + + binary, err = manager.Resolve() + if err != nil { + t.Fatalf("expected no err, got %s", err) + } + + assertResolvedBinary(t, binary, false, false) + + // Set a dev override + manager.overridePath = "testdata/cloudplugin-dev" + binary, err = manager.Resolve() + if err != nil { + t.Fatalf("expected no err, got %s", err) + } + + assertResolvedBinary(t, binary, false, true) +} diff --git a/internal/cloudplugin/client.go b/internal/cloudplugin/client.go new file mode 100644 index 0000000000..c26e3abe51 --- /dev/null +++ b/internal/cloudplugin/client.go @@ -0,0 +1,323 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + "time" + + "github.com/hashicorp/go-retryablehttp" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/releaseauth" +) + +var ( + defaultRequestTimeout = 60 * time.Second +) + +// SHASumsSignatures holds a list of URLs, each referring a detached signature of the release's build artifacts. +type SHASumsSignatures []string + +// BuildArtifact represents a single build artifact in a release response. +type BuildArtifact struct { + + // The hardware architecture of the build artifact + // Enum: [386 all amd64 amd64-lxc arm arm5 arm6 arm64 arm7 armelv5 armhfv6 i686 mips mips64 mipsle ppc64le s390x ui x86_64] + Arch string `json:"arch"` + + // The Operating System corresponding to the build artifact + // Enum: [archlinux centos darwin debian dragonfly freebsd linux netbsd openbsd plan9 python solaris terraform web windows] + Os string `json:"os"` + + // This build is unsupported and provided for convenience only. + Unsupported bool `json:"unsupported,omitempty"` + + // The URL where this build can be downloaded + URL string `json:"url"` +} + +// ReleaseStatus Status of the product release +// Example: {"message":"This release is supported","state":"supported"} +type ReleaseStatus struct { + + // Provides information about the most recent change; must be provided when Name="withdrawn" + Message string `json:"message,omitempty"` + + // The state name of the release + // Enum: [supported unsupported withdrawn] + State string `json:"state"` + + // The timestamp for the creation of the product release status + // Example: 2009-11-10T23:00:00Z + // Format: date-time + TimestampUpdated time.Time `json:"timestamp_updated"` +} + +// Release All metadata for a single product release +type Release struct { + // builds + Builds []*BuildArtifact `json:"builds,omitempty"` + + // A docker image name and tag for this release in the format `name`:`tag` + // Example: consul:1.10.0-beta3 + DockerNameTag string `json:"docker_name_tag,omitempty"` + + // True if and only if this product release is a prerelease. + IsPrerelease bool `json:"is_prerelease"` + + // The license class indicates how this product is licensed. + // Enum: [enterprise hcp oss] + LicenseClass string `json:"license_class"` + + // The product name + // Example: consul-enterprise + // Required: true + Name string `json:"name"` + + // Status + Status ReleaseStatus `json:"status"` + + // Timestamp at which this product release was created. + // Example: 2009-11-10T23:00:00Z + // Format: date-time + TimestampCreated time.Time `json:"timestamp_created"` + + // Timestamp when this product release was most recently updated. + // Example: 2009-11-10T23:00:00Z + // Format: date-time + TimestampUpdated time.Time `json:"timestamp_updated"` + + // URL for a blogpost announcing this release + URLBlogpost string `json:"url_blogpost,omitempty"` + + // URL for the changelog covering this release + URLChangelog string `json:"url_changelog,omitempty"` + + // The project's docker repo on Amazon ECR-Public + URLDockerRegistryDockerhub string `json:"url_docker_registry_dockerhub,omitempty"` + + // The project's docker repo on DockerHub + URLDockerRegistryEcr string `json:"url_docker_registry_ecr,omitempty"` + + // URL for the software license applicable to this release + // Required: true + URLLicense string `json:"url_license,omitempty"` + + // The project's website URL + URLProjectWebsite string `json:"url_project_website,omitempty"` + + // URL for this release's change notes + URLReleaseNotes string `json:"url_release_notes,omitempty"` + + // URL for this release's file containing checksums of all the included build artifacts + URLSHASums string `json:"url_shasums"` + + // An array of URLs, each pointing to a signature file. Each signature file is a detached signature + // of the checksums file (see field `url_shasums`). Signature files may or may not embed the signing + // key ID in the filename. + URLSHASumsSignatures SHASumsSignatures `json:"url_shasums_signatures"` + + // URL for the product's source code repository. This field is empty for + // enterprise and hcp products. + URLSourceRepository string `json:"url_source_repository,omitempty"` + + // The version of this release + // Example: 1.10.0-beta3 + // Required: true + Version string `json:"version"` +} + +// CloudPluginClient fetches and verifies release distributions of the cloudplugin +// that correspond to an upstream backend. +type CloudPluginClient struct { + serviceURL *url.URL + httpClient *retryablehttp.Client + ctx context.Context +} + +func requestLogHook(logger retryablehttp.Logger, req *http.Request, i int) { + if i > 0 { + logger.Printf("[INFO] Previous request to the remote cloud manifest failed, attempting retry.") + } +} + +func decodeManifest(data io.Reader) (*Release, error) { + var man Release + dec := json.NewDecoder(data) + if err := dec.Decode(&man); err != nil { + return nil, ErrQueryFailed{ + inner: fmt.Errorf("failed to decode response body: %w", err), + } + } + + return &man, nil +} + +// NewCloudPluginClient creates a new client for downloading and verifying +// terraform-cloudplugin archives +func NewCloudPluginClient(ctx context.Context, serviceURL *url.URL) (*CloudPluginClient, error) { + httpClient := httpclient.New() + httpClient.Timeout = defaultRequestTimeout + + retryableClient := retryablehttp.NewClient() + retryableClient.HTTPClient = httpClient + retryableClient.RetryMax = 3 + retryableClient.RequestLogHook = requestLogHook + retryableClient.Logger = logging.HCLogger() + + return &CloudPluginClient{ + httpClient: retryableClient, + serviceURL: serviceURL, + ctx: ctx, + }, nil +} + +// FetchManifest retrieves the cloudplugin manifest from HCP Terraform, +// but returns a nil manifest if a 304 response is received, depending +// on the lastModified time. +func (c CloudPluginClient) FetchManifest(lastModified time.Time) (*Release, error) { + req, _ := retryablehttp.NewRequestWithContext(c.ctx, "GET", c.serviceURL.JoinPath("manifest.json").String(), nil) + req.Header.Set("If-Modified-Since", lastModified.Format(http.TimeFormat)) + + resp, err := c.httpClient.Do(req) + if err != nil { + if errors.Is(err, context.Canceled) { + return nil, ErrRequestCanceled + } + return nil, ErrQueryFailed{ + inner: err, + } + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + manifest, err := decodeManifest(resp.Body) + if err != nil { + return nil, err + } + return manifest, nil + case http.StatusNotModified: + return nil, nil + case http.StatusNotFound: + return nil, ErrCloudPluginNotSupported + default: + return nil, ErrQueryFailed{ + inner: errors.New(resp.Status), + } + } +} + +// DownloadFile gets the URL at the specified path or URL and writes the +// contents to the specified Writer. +func (c CloudPluginClient) DownloadFile(pathOrURL string, writer io.Writer) error { + url, err := c.resolveManifestURL(pathOrURL) + if err != nil { + return err + } + req, err := retryablehttp.NewRequestWithContext(c.ctx, "GET", url.String(), nil) + if err != nil { + return fmt.Errorf("invalid URL %q was provided by the cloudplugin manifest: %w", url, err) + } + resp, err := c.httpClient.Do(req) + if err != nil { + if errors.Is(err, context.Canceled) { + return ErrRequestCanceled + } + return err + } + defer resp.Body.Close() + + switch resp.StatusCode { + case http.StatusOK: + // OK + case http.StatusNotFound: + return ErrCloudPluginNotFound + default: + return ErrQueryFailed{ + inner: errors.New(resp.Status), + } + } + + _, err = io.Copy(writer, resp.Body) + if err != nil { + return fmt.Errorf("failed to write downloaded file: %w", err) + } + + return nil +} + +func (c CloudPluginClient) resolveManifestURL(pathOrURL string) (*url.URL, error) { + if strings.HasPrefix(pathOrURL, "/") { + copy := *c.serviceURL + copy.Path = "" + return copy.JoinPath(pathOrURL), nil + } + + result, err := url.Parse(pathOrURL) + if err != nil { + return nil, fmt.Errorf("received malformed URL %q from cloudplugin manifest: %w", pathOrURL, err) + } + return result, nil +} + +// Select gets the specific build data from the Manifest for the specified OS/Architecture +func (m Release) Select(goos, arch string) (*BuildArtifact, error) { + var supported []string + var found *BuildArtifact + for _, build := range m.Builds { + key := fmt.Sprintf("%s_%s", build.Os, build.Arch) + supported = append(supported, key) + + if goos == build.Os && arch == build.Arch { + found = build + } + } + + osArchKey := fmt.Sprintf("%s_%s", goos, arch) + log.Printf("[TRACE] checking for cloudplugin archive for %s. Supported architectures: %v", osArchKey, supported) + + if found == nil { + return nil, ErrArchNotSupported + } + + return found, nil +} + +// PrimarySHASumsSignatureURL returns the URL among the URLSHASumsSignatures that matches +// the public key known by this version of terraform. It falls back to the first URL with no +// ID in the URL. +func (m Release) PrimarySHASumsSignatureURL() (string, error) { + if len(m.URLSHASumsSignatures) == 0 { + return "", fmt.Errorf("no SHA256SUMS URLs were available") + } + + findBySuffix := func(suffix string) string { + for _, url := range m.URLSHASumsSignatures { + if len(url) > len(suffix) && strings.EqualFold(suffix, url[len(url)-len(suffix):]) { + return url + } + } + return "" + } + + withKeyID := findBySuffix(fmt.Sprintf(".%s.sig", releaseauth.HashiCorpPublicKeyID)) + if withKeyID == "" { + withNoKeyID := findBySuffix("_SHA256SUMS.sig") + if withNoKeyID == "" { + return "", fmt.Errorf("no SHA256SUMS URLs matched the known public key") + } + return withNoKeyID, nil + } + return withKeyID, nil +} diff --git a/internal/cloudplugin/client_test.go b/internal/cloudplugin/client_test.go new file mode 100644 index 0000000000..f8bfad5b12 --- /dev/null +++ b/internal/cloudplugin/client_test.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" +) + +func newHTTPTestServerUnsupported(t *testing.T) (*httptest.Server, error) { + t.Helper() + + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 Not Found")) + })), nil +} + +func TestCloudPluginClient_DownloadFile(t *testing.T) { + server, err := newCloudPluginManifestHTTPTestServer(t) + if err != nil { + t.Fatalf("could not create test server: %s", err) + } + defer server.Close() + + serverURL, _ := url.Parse(server.URL) + serviceURL := serverURL.JoinPath("/api/cloudplugin/v1") + + client, err := NewCloudPluginClient(context.Background(), serviceURL) + if err != nil { + t.Fatalf("could not create test client: %s", err) + } + + t.Run("200 response", func(t *testing.T) { + buffer := bytes.Buffer{} + err := client.DownloadFile("/archives/terraform-cloudplugin_0.1.0_SHA256SUMS", &buffer) + if err != nil { + t.Fatal("expected no error") + } + if buffer.Len() == 0 { + t.Error("expected data, but got none") + } + }) + + t.Run("404 response", func(t *testing.T) { + err := client.DownloadFile("/archives/nope.zip", io.Discard) + if !errors.Is(err, ErrCloudPluginNotFound) { + t.Fatalf("expected error %q, got %q", ErrCloudPluginNotFound, err) + } + }) +} + +func TestCloudPluginClient_FetchManifest(t *testing.T) { + server, err := newCloudPluginManifestHTTPTestServer(t) + if err != nil { + t.Fatalf("could not create test server: %s", err) + } + defer server.Close() + + serverURL, _ := url.Parse(server.URL) + serviceURL := serverURL.JoinPath("/api/cloudplugin/v1") + + client, err := NewCloudPluginClient(context.Background(), serviceURL) + if err != nil { + t.Fatalf("could not create test client: %s", err) + } + + t.Run("200 response", func(t *testing.T) { + manifest, err := client.FetchManifest(time.Time{}) + if err != nil { + t.Fatal("expected no error") + } + + if manifest == nil { + t.Fatal("expected manifest") + } + + if expected := "0.1.0"; manifest.Version != expected { + t.Errorf("expected ProductVersion %q, got %q", expected, manifest.Version) + } + }) + + t.Run("304 response", func(t *testing.T) { + manifest, err := client.FetchManifest(testManifestLastModified) + if err != nil { + t.Fatal("expected no error") + } + + if manifest != nil { + t.Fatalf("expected nil manifest, got %#v", manifest) + } + }) +} + +func TestCloudPluginClient_NotSupportedByTerraformCloud(t *testing.T) { + server, err := newHTTPTestServerUnsupported(t) + if err != nil { + t.Fatalf("could not create test server: %s", err) + } + defer server.Close() + + serverURL, _ := url.Parse(server.URL) + serviceURL := serverURL.JoinPath("/api/cloudplugin/v1") + + client, err := NewCloudPluginClient(context.Background(), serviceURL) + if err != nil { + t.Fatalf("could not create test client: %s", err) + } + + _, err = client.FetchManifest(time.Time{}) + if !errors.Is(err, ErrCloudPluginNotSupported) { + t.Errorf("Expected ErrCloudPluginNotSupported, got %v", err) + } +} + +func TestRelease_PrimarySHASumsSignatureURL(t *testing.T) { + example := Release{ + URLSHASumsSignatures: []string{ + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig", + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS/72D7468F.sig", // Not quite right + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.72D7468F.sig", + }, + } + + url, err := example.PrimarySHASumsSignatureURL() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + if url != example.URLSHASumsSignatures[2] { + t.Errorf("Expected URL %q, but got %q", example.URLSHASumsSignatures[2], url) + } +} + +func TestRelease_PrimarySHASumsSignatureURL_lowercase_should_match(t *testing.T) { + example := Release{ + URLSHASumsSignatures: []string{ + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig", + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.72d7468f.sig", + }, + } + + url, err := example.PrimarySHASumsSignatureURL() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + // Not expected but technically fine since these are hex values + if url != example.URLSHASumsSignatures[1] { + t.Errorf("Expected URL %q, but got %q", example.URLSHASumsSignatures[1], url) + } +} + +func TestRelease_PrimarySHASumsSignatureURL_no_known_keys(t *testing.T) { + example := Release{ + URLSHASumsSignatures: []string{ + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig", + "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.ABCDEF012.sig", + }, + } + + url, err := example.PrimarySHASumsSignatureURL() + if err != nil { + t.Fatalf("Expected no error, got %s", err) + } + + // Returns key with no ID + if url != example.URLSHASumsSignatures[0] { + t.Errorf("Expected URL %q, but got %q", example.URLSHASumsSignatures[0], url) + } +} diff --git a/internal/cloudplugin/cloudplugin1/grpc_client.go b/internal/cloudplugin/cloudplugin1/grpc_client.go new file mode 100644 index 0000000000..c236744e26 --- /dev/null +++ b/internal/cloudplugin/cloudplugin1/grpc_client.go @@ -0,0 +1,81 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin1 + +import ( + "context" + "fmt" + "io" + "log" + + "github.com/hashicorp/terraform/internal/cloudplugin" + "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1" +) + +// GRPCCloudClient is the client interface for interacting with terraform-cloudplugin +type GRPCCloudClient struct { + client cloudproto1.CommandServiceClient + context context.Context +} + +// Proof that GRPCCloudClient fulfills the go-plugin interface +var _ cloudplugin.Cloud1 = GRPCCloudClient{} + +// Execute sends the client Execute request and waits for the plugin to return +// an exit code response before returning +func (c GRPCCloudClient) Execute(args []string, stdout, stderr io.Writer) int { + client, err := c.client.Execute(c.context, &cloudproto1.CommandRequest{ + Args: args, + }) + + if err != nil { + fmt.Fprint(stderr, err.Error()) + return 1 + } + + for { + // cloudplugin streams output as multiple CommandResponse value. Each + // value will either contain stdout bytes, stderr bytes, or an exit code. + response, err := client.Recv() + if err == io.EOF { + log.Print("[DEBUG] received EOF from cloudplugin") + break + } else if err != nil { + fmt.Fprintf(stderr, "Failed to receive command response from cloudplugin: %s", err) + return 1 + } + + if bytes := response.GetStdout(); len(bytes) > 0 { + written, err := fmt.Fprint(stdout, string(bytes)) + if err != nil { + log.Printf("[ERROR] Failed to write cloudplugin output to stdout: %s", err) + return 1 + } + if written != len(bytes) { + log.Printf("[ERROR] Wrote %d bytes to stdout but expected to write %d", written, len(bytes)) + } + } else if bytes := response.GetStderr(); len(bytes) > 0 { + written, err := fmt.Fprint(stderr, string(bytes)) + if err != nil { + log.Printf("[ERROR] Failed to write cloudplugin output to stderr: %s", err) + return 1 + } + if written != len(bytes) { + log.Printf("[ERROR] Wrote %d bytes to stdout but expected to write %d", written, len(bytes)) + } + } else { + exitCode := response.GetExitCode() + log.Printf("[TRACE] received exit code: %d", exitCode) + if exitCode < 0 || exitCode > 255 { + log.Printf("[ERROR] cloudplugin returned an invalid error code %d", exitCode) + return 255 + } + return int(exitCode) + } + } + + // This should indicate a bug in the plugin + fmt.Fprint(stderr, "cloudplugin exited without responding with an error code") + return 1 +} diff --git a/internal/cloudplugin/cloudplugin1/grpc_client_test.go b/internal/cloudplugin/cloudplugin1/grpc_client_test.go new file mode 100644 index 0000000000..2195cafcec --- /dev/null +++ b/internal/cloudplugin/cloudplugin1/grpc_client_test.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin1 + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "testing" + + "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1" + "github.com/hashicorp/terraform/internal/cloudplugin/mock_cloudproto1" + "go.uber.org/mock/gomock" +) + +var mockError = "this is a mock error" + +func testGRPCloudClient(t *testing.T, ctrl *gomock.Controller, client *mock_cloudproto1.MockCommandService_ExecuteClient, executeError error) *GRPCCloudClient { + t.Helper() + + if client != nil && executeError != nil { + t.Fatal("one of client or executeError must be nil") + } + + result := mock_cloudproto1.NewMockCommandServiceClient(ctrl) + + result.EXPECT().Execute( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(client, executeError) + + return &GRPCCloudClient{ + client: result, + context: context.Background(), + } +} + +func Test_GRPCCloudClient_ExecuteError(t *testing.T) { + ctrl := gomock.NewController(t) + gRPCClient := testGRPCloudClient(t, ctrl, nil, errors.New(mockError)) + + buffer := bytes.Buffer{} + exitCode := gRPCClient.Execute([]string{"example"}, io.Discard, &buffer) + + if exitCode != 1 { + t.Fatalf("expected exit %d, got %d", 1, exitCode) + } + + if buffer.String() != mockError { + t.Errorf("expected error %q, got %q", mockError, buffer.String()) + } +} + +func Test_GRPCCloudClient_Execute_RecvError(t *testing.T) { + ctrl := gomock.NewController(t) + executeClient := mock_cloudproto1.NewMockCommandService_ExecuteClient(ctrl) + executeClient.EXPECT().Recv().Return(nil, errors.New(mockError)) + + gRPCClient := testGRPCloudClient(t, ctrl, executeClient, nil) + + buffer := bytes.Buffer{} + exitCode := gRPCClient.Execute([]string{"example"}, io.Discard, &buffer) + + if exitCode != 1 { + t.Fatalf("expected exit %d, got %d", 1, exitCode) + } + + mockRecvError := fmt.Sprintf("Failed to receive command response from cloudplugin: %s", mockError) + + if buffer.String() != mockRecvError { + t.Errorf("expected error %q, got %q", mockRecvError, buffer.String()) + } +} + +func Test_GRPCCloudClient_Execute_Invalid_Exit(t *testing.T) { + ctrl := gomock.NewController(t) + executeClient := mock_cloudproto1.NewMockCommandService_ExecuteClient(ctrl) + + executeClient.EXPECT().Recv().Return( + &cloudproto1.CommandResponse{ + Data: &cloudproto1.CommandResponse_ExitCode{ + ExitCode: 3_000, + }, + }, nil, + ) + + gRPCClient := testGRPCloudClient(t, ctrl, executeClient, nil) + + exitCode := gRPCClient.Execute([]string{"example"}, io.Discard, io.Discard) + + if exitCode != 255 { + t.Fatalf("expected exit %q, got %q", 255, exitCode) + } +} + +func Test_GRPCCloudClient_Execute(t *testing.T) { + ctrl := gomock.NewController(t) + executeClient := mock_cloudproto1.NewMockCommandService_ExecuteClient(ctrl) + + gomock.InOrder( + executeClient.EXPECT().Recv().Return( + &cloudproto1.CommandResponse{ + Data: &cloudproto1.CommandResponse_Stdout{ + Stdout: []byte("firstcall\n"), + }, + }, nil, + ), + executeClient.EXPECT().Recv().Return( + &cloudproto1.CommandResponse{ + Data: &cloudproto1.CommandResponse_Stdout{ + Stdout: []byte("secondcall\n"), + }, + }, nil, + ), + executeClient.EXPECT().Recv().Return( + &cloudproto1.CommandResponse{ + Data: &cloudproto1.CommandResponse_ExitCode{ + ExitCode: 99, + }, + }, nil, + ), + ) + + gRPCClient := testGRPCloudClient(t, ctrl, executeClient, nil) + + stdoutBuffer := bytes.Buffer{} + exitCode := gRPCClient.Execute([]string{"example"}, &stdoutBuffer, io.Discard) + + if exitCode != 99 { + t.Fatalf("expected exit %q, got %q", 99, exitCode) + } + + if stdoutBuffer.String() != "firstcall\nsecondcall\n" { + t.Errorf("expected output %q, got %q", "firstcall\nsecondcall\n", stdoutBuffer.String()) + } +} diff --git a/internal/cloudplugin/cloudplugin1/grpc_plugin.go b/internal/cloudplugin/cloudplugin1/grpc_plugin.go new file mode 100644 index 0000000000..a515a0e10b --- /dev/null +++ b/internal/cloudplugin/cloudplugin1/grpc_plugin.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin1 + +import ( + "context" + "errors" + "net/rpc" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/terraform/internal/cloudplugin" + "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// GRPCCloudPlugin is the go-plugin implementation, but only the client +// implementation exists in this package. +type GRPCCloudPlugin struct { + plugin.GRPCPlugin + Impl cloudplugin.Cloud1 + // Any configuration metadata that the plugin executable needs in order to + // do something useful, which will be passed along via gRPC metadata headers. + Metadata metadata.MD +} + +// Server always returns an error; we're only implementing the GRPCPlugin +// interface, not the Plugin interface. +func (p *GRPCCloudPlugin) Server(*plugin.MuxBroker) (interface{}, error) { + return nil, errors.New("cloudplugin only implements gRPC clients") +} + +// Client always returns an error; we're only implementing the GRPCPlugin +// interface, not the Plugin interface. +func (p *GRPCCloudPlugin) Client(*plugin.MuxBroker, *rpc.Client) (interface{}, error) { + return nil, errors.New("cloudplugin only implements gRPC clients") +} + +// GRPCServer always returns an error; we're only implementing the client +// interface, not the server. +func (p *GRPCCloudPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + return errors.New("cloudplugin only implements gRPC clients") +} + +// GRPCClient returns a new GRPC client for interacting with the cloud plugin server. +func (p *GRPCCloudPlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + ctx = metadata.NewOutgoingContext(ctx, p.Metadata) + return &GRPCCloudClient{ + client: cloudproto1.NewCommandServiceClient(c), + context: ctx, + }, nil +} diff --git a/internal/cloudplugin/cloudproto1/cloudproto1.pb.go b/internal/cloudplugin/cloudproto1/cloudproto1.pb.go new file mode 100644 index 0000000000..9f3d368923 --- /dev/null +++ b/internal/cloudplugin/cloudproto1/cloudproto1.pb.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: cloudproto1.proto + +package cloudproto1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// CommandRequest is used to request the execution of a specific command with +// provided flags. It is the raw args from the HCP Terraform command. +type CommandRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Args []string `protobuf:"bytes,1,rep,name=args,proto3" json:"args,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommandRequest) Reset() { + *x = CommandRequest{} + mi := &file_cloudproto1_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommandRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommandRequest) ProtoMessage() {} + +func (x *CommandRequest) ProtoReflect() protoreflect.Message { + mi := &file_cloudproto1_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommandRequest.ProtoReflect.Descriptor instead. +func (*CommandRequest) Descriptor() ([]byte, []int) { + return file_cloudproto1_proto_rawDescGZIP(), []int{0} +} + +func (x *CommandRequest) GetArgs() []string { + if x != nil { + return x.Args + } + return nil +} + +// CommandResponse contains the result of the command execution, including any +// output or errors. +type CommandResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Data: + // + // *CommandResponse_ExitCode + // *CommandResponse_Stdout + // *CommandResponse_Stderr + Data isCommandResponse_Data `protobuf_oneof:"data"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CommandResponse) Reset() { + *x = CommandResponse{} + mi := &file_cloudproto1_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CommandResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CommandResponse) ProtoMessage() {} + +func (x *CommandResponse) ProtoReflect() protoreflect.Message { + mi := &file_cloudproto1_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CommandResponse.ProtoReflect.Descriptor instead. +func (*CommandResponse) Descriptor() ([]byte, []int) { + return file_cloudproto1_proto_rawDescGZIP(), []int{1} +} + +func (x *CommandResponse) GetData() isCommandResponse_Data { + if x != nil { + return x.Data + } + return nil +} + +func (x *CommandResponse) GetExitCode() int32 { + if x != nil { + if x, ok := x.Data.(*CommandResponse_ExitCode); ok { + return x.ExitCode + } + } + return 0 +} + +func (x *CommandResponse) GetStdout() []byte { + if x != nil { + if x, ok := x.Data.(*CommandResponse_Stdout); ok { + return x.Stdout + } + } + return nil +} + +func (x *CommandResponse) GetStderr() []byte { + if x != nil { + if x, ok := x.Data.(*CommandResponse_Stderr); ok { + return x.Stderr + } + } + return nil +} + +type isCommandResponse_Data interface { + isCommandResponse_Data() +} + +type CommandResponse_ExitCode struct { + ExitCode int32 `protobuf:"varint,1,opt,name=exitCode,proto3,oneof"` +} + +type CommandResponse_Stdout struct { + Stdout []byte `protobuf:"bytes,2,opt,name=stdout,proto3,oneof"` +} + +type CommandResponse_Stderr struct { + Stderr []byte `protobuf:"bytes,3,opt,name=stderr,proto3,oneof"` +} + +func (*CommandResponse_ExitCode) isCommandResponse_Data() {} + +func (*CommandResponse_Stdout) isCommandResponse_Data() {} + +func (*CommandResponse_Stderr) isCommandResponse_Data() {} + +var File_cloudproto1_proto protoreflect.FileDescriptor + +var file_cloudproto1_proto_rawDesc = string([]byte{ + 0x0a, 0x11, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x31, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x31, + 0x22, 0x24, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x22, 0x6b, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x08, 0x65, 0x78, 0x69, + 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, + 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x64, 0x6f, 0x75, + 0x74, 0x12, 0x18, 0x0a, 0x06, 0x73, 0x74, 0x64, 0x65, 0x72, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0c, 0x48, 0x00, 0x52, 0x06, 0x73, 0x74, 0x64, 0x65, 0x72, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x64, + 0x61, 0x74, 0x61, 0x32, 0x5a, 0x0a, 0x0e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x07, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, + 0x12, 0x1b, 0x2e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x31, 0x2e, 0x43, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, + 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, + 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6c, 0x6f, 0x75, 0x64, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_cloudproto1_proto_rawDescOnce sync.Once + file_cloudproto1_proto_rawDescData []byte +) + +func file_cloudproto1_proto_rawDescGZIP() []byte { + file_cloudproto1_proto_rawDescOnce.Do(func() { + file_cloudproto1_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_cloudproto1_proto_rawDesc), len(file_cloudproto1_proto_rawDesc))) + }) + return file_cloudproto1_proto_rawDescData +} + +var file_cloudproto1_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_cloudproto1_proto_goTypes = []any{ + (*CommandRequest)(nil), // 0: cloudproto1.CommandRequest + (*CommandResponse)(nil), // 1: cloudproto1.CommandResponse +} +var file_cloudproto1_proto_depIdxs = []int32{ + 0, // 0: cloudproto1.CommandService.Execute:input_type -> cloudproto1.CommandRequest + 1, // 1: cloudproto1.CommandService.Execute:output_type -> cloudproto1.CommandResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_cloudproto1_proto_init() } +func file_cloudproto1_proto_init() { + if File_cloudproto1_proto != nil { + return + } + file_cloudproto1_proto_msgTypes[1].OneofWrappers = []any{ + (*CommandResponse_ExitCode)(nil), + (*CommandResponse_Stdout)(nil), + (*CommandResponse_Stderr)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_cloudproto1_proto_rawDesc), len(file_cloudproto1_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_cloudproto1_proto_goTypes, + DependencyIndexes: file_cloudproto1_proto_depIdxs, + MessageInfos: file_cloudproto1_proto_msgTypes, + }.Build() + File_cloudproto1_proto = out.File + file_cloudproto1_proto_goTypes = nil + file_cloudproto1_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// CommandServiceClient is the client API for CommandService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type CommandServiceClient interface { + // Execute runs a specific command with the provided flags and returns the result. + Execute(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (CommandService_ExecuteClient, error) +} + +type commandServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewCommandServiceClient(cc grpc.ClientConnInterface) CommandServiceClient { + return &commandServiceClient{cc} +} + +func (c *commandServiceClient) Execute(ctx context.Context, in *CommandRequest, opts ...grpc.CallOption) (CommandService_ExecuteClient, error) { + stream, err := c.cc.NewStream(ctx, &_CommandService_serviceDesc.Streams[0], "/cloudproto1.CommandService/Execute", opts...) + if err != nil { + return nil, err + } + x := &commandServiceExecuteClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type CommandService_ExecuteClient interface { + Recv() (*CommandResponse, error) + grpc.ClientStream +} + +type commandServiceExecuteClient struct { + grpc.ClientStream +} + +func (x *commandServiceExecuteClient) Recv() (*CommandResponse, error) { + m := new(CommandResponse) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +// CommandServiceServer is the server API for CommandService service. +type CommandServiceServer interface { + // Execute runs a specific command with the provided flags and returns the result. + Execute(*CommandRequest, CommandService_ExecuteServer) error +} + +// UnimplementedCommandServiceServer can be embedded to have forward compatible implementations. +type UnimplementedCommandServiceServer struct { +} + +func (*UnimplementedCommandServiceServer) Execute(*CommandRequest, CommandService_ExecuteServer) error { + return status.Errorf(codes.Unimplemented, "method Execute not implemented") +} + +func RegisterCommandServiceServer(s *grpc.Server, srv CommandServiceServer) { + s.RegisterService(&_CommandService_serviceDesc, srv) +} + +func _CommandService_Execute_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(CommandRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(CommandServiceServer).Execute(m, &commandServiceExecuteServer{stream}) +} + +type CommandService_ExecuteServer interface { + Send(*CommandResponse) error + grpc.ServerStream +} + +type commandServiceExecuteServer struct { + grpc.ServerStream +} + +func (x *commandServiceExecuteServer) Send(m *CommandResponse) error { + return x.ServerStream.SendMsg(m) +} + +var _CommandService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "cloudproto1.CommandService", + HandlerType: (*CommandServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Execute", + Handler: _CommandService_Execute_Handler, + ServerStreams: true, + }, + }, + Metadata: "cloudproto1.proto", +} diff --git a/internal/cloudplugin/cloudproto1/cloudproto1.proto b/internal/cloudplugin/cloudproto1/cloudproto1.proto new file mode 100644 index 0000000000..8a7f11bb45 --- /dev/null +++ b/internal/cloudplugin/cloudproto1/cloudproto1.proto @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package cloudproto1; + +option go_package = "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1"; + +// CommandRequest is used to request the execution of a specific command with +// provided flags. It is the raw args from the HCP Terraform command. +message CommandRequest { + repeated string args = 1; +} + +// CommandResponse contains the result of the command execution, including any +// output or errors. +message CommandResponse { + oneof data { + int32 exitCode = 1; + bytes stdout = 2; + bytes stderr = 3; + } +} + +// PluginService defines the gRPC service to handle available commands and +// their execution. +service CommandService { + // Execute runs a specific command with the provided flags and returns the result. + rpc Execute(CommandRequest) returns (stream CommandResponse) {} +} diff --git a/internal/cloudplugin/errors.go b/internal/cloudplugin/errors.go new file mode 100644 index 0000000000..7f15bd2c6e --- /dev/null +++ b/internal/cloudplugin/errors.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "errors" + "fmt" +) + +var ( + // ErrCloudPluginNotSupported is the error returned when the upstream HCP Terraform does not + // have a manifest. + ErrCloudPluginNotSupported = errors.New("cloud plugin is not supported by the remote version of Terraform Enterprise") + + // ErrRequestCanceled is the error returned when the context was cancelled. + ErrRequestCanceled = errors.New("request was canceled") + + // ErrArchNotSupported is the error returned when the cloudplugin does not have a build for the + // current OS/Architecture. + ErrArchNotSupported = errors.New("cloud plugin is not supported by your computer architecture/operating system") + + // ErrCloudPluginNotFound is the error returned when the cloudplugin manifest points to a location + // that was does not exist. + ErrCloudPluginNotFound = errors.New("cloud plugin download was not found in the location specified in the manifest") +) + +// ErrQueryFailed is the error returned when the cloudplugin http client request fails +type ErrQueryFailed struct { + inner error +} + +// ErrCloudPluginNotVerified is the error returned when the archive authentication process fails +type ErrCloudPluginNotVerified struct { + inner error +} + +// Error returns a string representation of ErrQueryFailed +func (e ErrQueryFailed) Error() string { + return fmt.Sprintf("failed to fetch cloud plugin from HCP Terraform: %s", e.inner) +} + +// Unwrap returns the inner error of ErrQueryFailed +func (e ErrQueryFailed) Unwrap() error { + // Return the inner error. + return e.inner +} + +// Error returns the string representation of ErrCloudPluginNotVerified +func (e ErrCloudPluginNotVerified) Error() string { + return fmt.Sprintf("failed to verify cloud plugin. Ensure that the referenced plugin is the official HashiCorp distribution: %s", e.inner) +} + +// Unwrap returns the inner error of ErrCloudPluginNotVerified +func (e ErrCloudPluginNotVerified) Unwrap() error { + return e.inner +} diff --git a/internal/cloudplugin/interface.go b/internal/cloudplugin/interface.go new file mode 100644 index 0000000000..37e844d58c --- /dev/null +++ b/internal/cloudplugin/interface.go @@ -0,0 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "io" +) + +type Cloud1 interface { + Execute(args []string, stdout, stderr io.Writer) int +} diff --git a/internal/cloudplugin/mock_cloudproto1/generate.go b/internal/cloudplugin/mock_cloudproto1/generate.go new file mode 100644 index 0000000000..d1c2196522 --- /dev/null +++ b/internal/cloudplugin/mock_cloudproto1/generate.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:generate go tool go.uber.org/mock/mockgen -destination mock.go github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1 CommandServiceClient,CommandService_ExecuteClient + +package mock_cloudproto1 diff --git a/internal/cloudplugin/mock_cloudproto1/mock.go b/internal/cloudplugin/mock_cloudproto1/mock.go new file mode 100644 index 0000000000..25e6514844 --- /dev/null +++ b/internal/cloudplugin/mock_cloudproto1/mock.go @@ -0,0 +1,186 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1 (interfaces: CommandServiceClient,CommandService_ExecuteClient) +// +// Generated by this command: +// +// mockgen -destination mock.go github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1 CommandServiceClient,CommandService_ExecuteClient +// + +// Package mock_cloudproto1 is a generated GoMock package. +package mock_cloudproto1 + +import ( + context "context" + reflect "reflect" + + cloudproto1 "github.com/hashicorp/terraform/internal/cloudplugin/cloudproto1" + gomock "go.uber.org/mock/gomock" + grpc "google.golang.org/grpc" + metadata "google.golang.org/grpc/metadata" +) + +// MockCommandServiceClient is a mock of CommandServiceClient interface. +type MockCommandServiceClient struct { + ctrl *gomock.Controller + recorder *MockCommandServiceClientMockRecorder +} + +// MockCommandServiceClientMockRecorder is the mock recorder for MockCommandServiceClient. +type MockCommandServiceClientMockRecorder struct { + mock *MockCommandServiceClient +} + +// NewMockCommandServiceClient creates a new mock instance. +func NewMockCommandServiceClient(ctrl *gomock.Controller) *MockCommandServiceClient { + mock := &MockCommandServiceClient{ctrl: ctrl} + mock.recorder = &MockCommandServiceClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCommandServiceClient) EXPECT() *MockCommandServiceClientMockRecorder { + return m.recorder +} + +// Execute mocks base method. +func (m *MockCommandServiceClient) Execute(arg0 context.Context, arg1 *cloudproto1.CommandRequest, arg2 ...grpc.CallOption) (cloudproto1.CommandService_ExecuteClient, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "Execute", varargs...) + ret0, _ := ret[0].(cloudproto1.CommandService_ExecuteClient) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Execute indicates an expected call of Execute. +func (mr *MockCommandServiceClientMockRecorder) Execute(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Execute", reflect.TypeOf((*MockCommandServiceClient)(nil).Execute), varargs...) +} + +// MockCommandService_ExecuteClient is a mock of CommandService_ExecuteClient interface. +type MockCommandService_ExecuteClient struct { + ctrl *gomock.Controller + recorder *MockCommandService_ExecuteClientMockRecorder +} + +// MockCommandService_ExecuteClientMockRecorder is the mock recorder for MockCommandService_ExecuteClient. +type MockCommandService_ExecuteClientMockRecorder struct { + mock *MockCommandService_ExecuteClient +} + +// NewMockCommandService_ExecuteClient creates a new mock instance. +func NewMockCommandService_ExecuteClient(ctrl *gomock.Controller) *MockCommandService_ExecuteClient { + mock := &MockCommandService_ExecuteClient{ctrl: ctrl} + mock.recorder = &MockCommandService_ExecuteClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCommandService_ExecuteClient) EXPECT() *MockCommandService_ExecuteClientMockRecorder { + return m.recorder +} + +// CloseSend mocks base method. +func (m *MockCommandService_ExecuteClient) CloseSend() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloseSend") + ret0, _ := ret[0].(error) + return ret0 +} + +// CloseSend indicates an expected call of CloseSend. +func (mr *MockCommandService_ExecuteClientMockRecorder) CloseSend() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseSend", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).CloseSend)) +} + +// Context mocks base method. +func (m *MockCommandService_ExecuteClient) Context() context.Context { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Context") + ret0, _ := ret[0].(context.Context) + return ret0 +} + +// Context indicates an expected call of Context. +func (mr *MockCommandService_ExecuteClientMockRecorder) Context() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Context", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).Context)) +} + +// Header mocks base method. +func (m *MockCommandService_ExecuteClient) Header() (metadata.MD, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Header") + ret0, _ := ret[0].(metadata.MD) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Header indicates an expected call of Header. +func (mr *MockCommandService_ExecuteClientMockRecorder) Header() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Header", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).Header)) +} + +// Recv mocks base method. +func (m *MockCommandService_ExecuteClient) Recv() (*cloudproto1.CommandResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Recv") + ret0, _ := ret[0].(*cloudproto1.CommandResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Recv indicates an expected call of Recv. +func (mr *MockCommandService_ExecuteClientMockRecorder) Recv() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Recv", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).Recv)) +} + +// RecvMsg mocks base method. +func (m *MockCommandService_ExecuteClient) RecvMsg(arg0 any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RecvMsg", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RecvMsg indicates an expected call of RecvMsg. +func (mr *MockCommandService_ExecuteClientMockRecorder) RecvMsg(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecvMsg", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).RecvMsg), arg0) +} + +// SendMsg mocks base method. +func (m *MockCommandService_ExecuteClient) SendMsg(arg0 any) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendMsg", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendMsg indicates an expected call of SendMsg. +func (mr *MockCommandService_ExecuteClientMockRecorder) SendMsg(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMsg", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).SendMsg), arg0) +} + +// Trailer mocks base method. +func (m *MockCommandService_ExecuteClient) Trailer() metadata.MD { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Trailer") + ret0, _ := ret[0].(metadata.MD) + return ret0 +} + +// Trailer indicates an expected call of Trailer. +func (mr *MockCommandService_ExecuteClientMockRecorder) Trailer() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Trailer", reflect.TypeOf((*MockCommandService_ExecuteClient)(nil).Trailer)) +} diff --git a/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_SHA256SUMS b/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_SHA256SUMS new file mode 100644 index 0000000000..81b6b4ee8d --- /dev/null +++ b/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_SHA256SUMS @@ -0,0 +1 @@ +22db2f0c70b50cff42afd4878fea9f6848a63f1b6532bd8b64b899f574acb35d terraform-cloudplugin_0.1.0_darwin_amd64.zip diff --git a/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.sig b/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.sig new file mode 100644 index 0000000000..553e7b007e Binary files /dev/null and b/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.sig differ diff --git a/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_darwin_amd64.zip b/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_darwin_amd64.zip new file mode 100644 index 0000000000..58dfbabb21 Binary files /dev/null and b/internal/cloudplugin/testdata/archives/terraform-cloudplugin_0.1.0_darwin_amd64.zip differ diff --git a/internal/cloudplugin/testdata/cloudplugin-dev b/internal/cloudplugin/testdata/cloudplugin-dev new file mode 100644 index 0000000000..ce12f244b3 --- /dev/null +++ b/internal/cloudplugin/testdata/cloudplugin-dev @@ -0,0 +1 @@ +i have deleted the toucan diff --git a/internal/cloudplugin/testdata/sample.md b/internal/cloudplugin/testdata/sample.md new file mode 100644 index 0000000000..2bd06f9926 --- /dev/null +++ b/internal/cloudplugin/testdata/sample.md @@ -0,0 +1,11 @@ +# Package cloudplugin Test Data Signing Key + +This directory contains a private key that is only used for signing the test data, along with the public key that the package uses to verfify the signing. Here are the steps to reproduce the test data, which would be necessary if the archive and checksum changes. + +1. Import the secret key + +`gpg --import sample.private.key` + +2. Sign the sample_release SHA256SUMS file using the sample key: + +`gpg -u 200BDA882C95B80A --output archives/terraform-cloudplugin_0.1.0_SHA256SUMS.sig --detach-sig archives/terraform-cloudplugin_0.1.0_SHA256SUMS` diff --git a/internal/cloudplugin/testdata/sample.private.key b/internal/cloudplugin/testdata/sample.private.key new file mode 100644 index 0000000000..b83c2b0295 --- /dev/null +++ b/internal/cloudplugin/testdata/sample.private.key @@ -0,0 +1,106 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQcYBGTD2nkBEACzN+0KhgkfyObviYGvtWWCQfznX440nIiu0uag1lRGh6MeupOw +cPFMtSSoltSYTTkS3J6UBvTOQbPq/IAX0H3WBnUWFJpA9h87fHbpxquEeYeUQ65r +J/IRLsPFnzOl43CCSnkDuppJqYSPJc1GJYtb4O8Tq7+Af7rdZFWoPW1bo7NIriUm +MM9y3udb8Catvz5L7aYevQ001x9jP1SzpkMVWY4/TxTrE7Xjxh7Ke7MlJmIeFWHl +ja/mjukcZZAD/KwHU+mid+MZhx1CmiN36CuYBcLpP2eJTrrhYOps8kX5z1sKsIaY +4KF1yVkLGQxGS8y2M0kZXjYtNPxY1DJGveWIRzcx+7KWkavkVbhvmrMRWLkKv+55 +BV6lsLrOYFrCTRxolNOx5ZNcUbaIucqGYe8RzQz/WlPPA3TbMnOQ7OSTIqhqMzzd +s7l7S4N7UHP+ePvMy1Uzajfsb80XI9zYRrRMshHjU6huYITwIF5wWzfRVegSfYw+ +bcxr5XWLYmJ5aMA8JNOhdjpszpbZcCZPYPV6Ya6jsXoO4QxD+ivGNBbzEuB+DolW +/6GXqfuJARPx1vPIAWX2j6uVhJb5ygBahd9aPjaURwuM9SZnCTCoc0x7171R6t+g +AIs/wB9D/iHbLEMUgH5KlW+9VtJLw2afiVAeSd38axvn/ELURvpMoJ1s5wARAQAB +AA/8CZ2IXLut6qQl68J2EEjMXkLkauoqtYeTzRlooUbOimLa7W+iM+G4uIkGzfrF +rjmFGeN3U0bU7z9W2VaHGCqp+F0r/q1yoeXymAed/lNClDQhrOKSuDy86o9r69hA +9kyvwSB2F1f1e4VE+kR+G8jbjhLmkNNyo1YhtEtz+GJPUeQvCXO6b4RH5Qhbjl23 +SDQjXKyD8KXrpaLzE/6WW0siJ0JA9QRCVuMdq6T6CvARv6kBs6PUYZ4KoB/ubC/G +HHNRLCrO4ADMUOwkYD89LuuTHIVr8dq31zCVAI7pakNMs3yctwPwkhbfOB6/kG5d +b7oVDM4kml/+dR92IzKYqJ37SLpZKED8CfY5T9cOVE83TNTFPIbWsxUKtwp6hryK +I8SPPctQTcBFp0lxzUdpFwzYWz5Ss3vx2QPtK0kBmGkruL5Hkq44ji2BBl0Yd8Lp +mUkoJOIHuD6ut5fdz0HEKhSa40/4+VjzD3aEZMeZ0zhgaZnWU0N4IVXFmgGhFJ3k +h/Z8cCq8aiEIcVh2rWKaNiYZKOYAvEQh6X2fFve+Ix2qZQhn0A5KFAwe1mLD0fHR +OQw3fzk8ydg2f6XZcchMpLi+gYoLlXNagoWIJQLbtW/tVr+TwM+RNMw2bjqVLyCx +VxnE+R8RY+nahbAHUKzGKSy/tUuUonsrn9vbG/pLtSRjThkIANQWgakgA112wCH5 +kNPAYxYxpF5+dq+7NiJ6qQkA/k64K1XopDmPKh+Ix7EmopnCJGRb/+i6HaiTvk/m +01/OJmlufJRI1snKrxA74vM6Nhh8bgX5bpqqEDp1Q+74QU5pOD1RAS2jgI7DpefP +8+9B7wCjP3m/KdZrR9r/ZQ/SpCqXQ6rrvcRBP7pp5y90Uu6MtpKhSiJNOvfWovlB +x3jagSyCvW10iBGJtUZxZ5M+IQvniPK/3NDQ7whl9sEpiAYv30qP/bZ4TIl4RjhT +b62P/657moSkFi6ugA2Go2mqruJGYQRIsj2P5L1BT7LLx4YFWKgGMuiBTi/oxcME +9NTVW3kIANhTNYYieyichaa0+nPU0H+C+Ta+iWjWdHrzsgj7v/yq9Qq7ptClIXSb +tkqcy0NSjL7D0fpCJVP5O5i4cUqIYqbhWYwgsGc0vPUqxjFF2qtPtyU5oHN05Ein +8I3kfd5Q1m9egrRmMVqa+/RK9XXHmuRELH83SHYDr3kPfJaNaqcbE+MnZhJBOh41 +hjMV5RPlX6CUZgfsh9Z67yxx2am2lnabdXYOEs7FRc+U5yAQzHpKO7o6jy6yRvAF +HK5brjmazo3h6eV7j+wfhXJL83/ML7I0qvyc//Ezdze90Oua7IbZGDKsg4oK9Or8 +91mnaBqGoL8tS0DmgvmOCmJxFQy6k18IANDlKadc5IabgmRo2beAAhuGvst3I5Cq +L1GmInDxZCUYTTX0eDpIZQz02TBmHiqLJMoUPtgYKdULUyZ8y9XM+sc2U8oxZIwe +aeXX6ZwoW6/lZIfM1Pri/fXRL3H+mc3IJW9cDLmQP6qF7BDxZiIBADfqCwlUSDGf +jG8sMTjaIfXx1rrFf+4gJTdBRfd4KEthbK+T2htXtx4Rce2ju/5FNeMnfTgoGVmM +cblrynCwIqz6tScnq8pm9cvK17ZTzp7tokkDF3ljEbw+kRY+OzbFokHXDzpUlIfR +8x1gzVKFRuk+IwWInr0Y/3fkXSbxicf7N2ZD2HKyYwBUWIrH/Bh8lk+ABbQvSGFz +aGlDb3JwIFRlcnJhZm9ybSBUZXN0IDxiY3JvZnRAaGFzaGljb3JwLmNvbT6JAk4E +EwEIADgWIQQ3TTI1RMz0lxj4EdcgC9qILJW4CgUCZMPaeQIbAwULCQgHAgYVCgkI +CwIEFgIDAQIeAQIXgAAKCRAgC9qILJW4Ch6vD/9P2NavPK8lkdrdEfEL3hS4jsIa +aCwdbY7ilH2EYADtl3Kd8X0JIlZJ6HgAHKe90Va3dJJcno4W32s04/p6tsb1LW9j +mevq1hrVU3dqZn+EBmNySP5QGHAjhgJTOoHW7CZqME/l9P04NTMXWAdWENcCD0+l +hQTftr4sEHn1v9DCVdtXF4WcQvdmsoXSstdf3wiQ2a3QkWk4a46HPSKkRyz8TerX +XEy79fPRYb/675QqQPQ6FfM+Vx/CTsfEpuJGnbtpp11HfXPZsk0TSGJAcNQXMv7V +JovGfc+amqVYqQiicT8nHNUSShJrxA1TUUF6Ige+2ewDeDBhNk5EEAAEH0aZzwci +qlGX4rbZOJjkgMtbTK65agqF67H3WDXGsOhmD+l2GjwuPz0AKxOhvdxkaBz7q08t +tjCOWXdZogfcgeLGZLRHvmAkLJTHc2b40Dhe++ryEhmHp4lXNAwhJVqEFrmW1agb +P5+qS19VjbLv8s9ynxgVXr7J7wi8hAx9t1QRe9yOif2Rmo532bU4sxD/bcGwMoO5 +xsCZJh9AmV/dJGOPfPnPuF5OPncOLPeiP0xiDsBJAeQYDClG1pBwxTCmUpFb3jT9 +N/1qBjykFVX7B+cSYb06w+MHo8bfCQrvS4ifAXDOhlvpYuVpQfouES9VtD5U13np +qoaVTAj4JbhRvVbzOJ0HGARkw9p5ARAAxY+61t9N21O0mtUDcz7hVYklLce8XQye +ucC8T8Vp68vuGIfJNsELaHo6XMJ8iK42t5fb9hk8ZX2Z8A+1ZbXEor+wcGHbrpYH +5TmFNNFdbUYka9eHfu4jZajKHmb8GkBCS7gj1wfjFj0ss3zCUmI+aD/Tjy8zumbb +0ECFfKFRaNEm3qEcGAK0Suh6t+VqpCzxI+5L5w2H/mvWRqJjULz2/IkEqhSVsJDq +mUJBPpelQGzbEyynvNbRpPsvZk9nFX2SfcKS9hqwXyUVniAh2XAnx7NScDnkCArO +GSi3b3nA4Sj7124YO+5CgmAS7FNcPb69uCoYaYgtBzuzDCIcg2OJQ8gpsYzD0h2z +HKY0PAV+k0AqzfpNIFvv/pm8DoiCkeMLtpJLE9Olq9hP45SuOMc8AHrN8QVGlKhX +aNnvct40R3XlGltrVlcFFR4yyedpQWlgvybYPcK/6F0orbAX4MIqinyraUT3sUT/ +UOJjYAu+qlA7xGyI7+MDguWttkAtt3bQQ5ML7Jem8FVe1T2Ex1muOIAa7DrrrnyX +30zCNrGEEKOOfjvxVbJRpPh641LfILrl4z9STyV3GEzUYW7k+13Xu4RNFn921INg +qSv/6oaRFtethymj3/x3+nh9XsuF1cBVw/wqRcgIzx9mK4ZfVP4cgeZoHHFtxW8/ +eFJzs6qE8E0AEQEAAQAP+QFOwPSpqnVTuBdn7l5z8obLlYXcWFUbyjUSjr53mzsT +nHssEcJub3oRvq1A6M9dfGCjk5DfcchAKKDLn0bQwXpOQyeH/9xTL+kdkX54LXjC +OdgcAp4qIkYpv4hO/3t3VQn5AaChxTHIfvr/fGh6/Yy9mI/UIDDle+xUDIP05kSk +wZKlAsm01haGYseOBWacC3Od/+40X5wdaXlAh/uOpnQCMtwH2WEoKOHXhR0EyZbW +atNe8eDyE+8cEOf0feCFLn2zQVKJbrOWhGD0N4H85K7gORNZ+vvyP8GgZ6GCU91e +gdsyR1TM0xwRusThWxEh1HyQp6w136WjBqZeheKvcZ47XpyVpDpB0dzPG9joocy9 +d69lwXJpLEzoY5EV8nTn5YU7SizIItAMKOvUolHJtaAiInNkPdqMeFOfd0wIzWKF +FWa/HrpP6UQBzndKs4shNJSn14c3MeDwa5Cdbw7Gii8Ww6rtiU4ctuqjUXvsXl6P +/5rW9WPqAM/pcSFc17N8hbNWrGRprdyqpOupyFuBQsetun4k+uKVDlMfN2IdjYm7 +jiMAcoUYoWxcvpq4Dzg/mlLr8yGjoZyDqdCuzxdyPXw/BFJYnBWa4zSNLCZ8a+1b +4VKGZLLsLKzUNoJ8HvItsoxy48aiBPTnMzoEBh2yzvR76B4663pGOB4VVaxzi59J +CADYbKEp2nHcbZhOHUo+3r8g482qMp2ygPX6w0wswUiUCWENnb9vcUoEPwk6FWVN +jKU0QEd2xArNfC9K8kj/K7gDyqCnS0Tofc/yrieLWRN0fO5Z8zLq3SKMVvBHUM4Q +6zT471FUlaLMNHlk9uyTFNmK7Ti9LL3T0k1ngPGT60uWXFC8qvALbeRtjKAKjf7s +VeGlDDXbiB2m67MU9hKjyeIMDBciT3IbYTm5Z+FrEoZxWRYEOIQek4Nmp7/waOE3 +Xlk3UmED9BwwXbRliiNzfijwRPb9QZ3FKSEZM8HXK1A0OmtxV6vg7dnVYsDuaRxb +ljl/dQDW7/mpRtugH7sHB+E5CADpsBSq6i/bMU3Q3lY6JVs5FBFZyRYT2K/zC109 +lSfj5fP8TWUyiP9mSaFW6MkDoecGod1HZyA579I16JVulYDnD1A7creYncoMMofN +UJNO8tJRJchywr+829deP+IYGQuRw9lQsk+p/uTszpexMxz5ltr4dBihCv88ZpXO +kBoB+Qkwu+IIiQz3ScnpT7fvhsGZI9hqs2bg7nuvwyoGMnW7S7CZ++YIBhpEvZ3c +jQKQde+2558ldxkyXGiKuHzZ4OzLe+jjqCYiD0hfe7BEI8jXjOn97JFT2c2Ts44t +VNvyrbe80D68id5/bmYPTn5M7qYtG3iO5p9olX8onqnVr0u1B/9dZWud4PNpkUkA +z0FE6HpwsrWYFravMiSDbkTYzpl/AXu8canKguJ7lNzl575BOaA9Fsfgswr5KgVA +urXp/wpZNaX9C/VTtkuqR8t7y3z+tkiXTGA+JT5Lp5Q3QxneqLRp6XZMckfyeKcL +SELaYCseFLaap+uAkV7/sxJynD9CL5h63lIwt/GZjKi5sbRsGL/2LxKwxjNaiQW8 +akxCkw0eupbJt2JAuQWRiJ6Yl4lMAyQxzQM/7a8Jadcy+Tl//6k4aaxR+s1RPbTA +cpiCO8tHM5/4WMMSeA36UX1PlEKtxifaqbDYS5sMeSUAAW1nfYOiNu5gg+WQ8SuN +1jZV+sVOjSCJAjYEGAEIACAWIQQ3TTI1RMz0lxj4EdcgC9qILJW4CgUCZMPaeQIb +DAAKCRAgC9qILJW4CoL9D/9lSJLa941JhieE3nyhhDcG9+Y4iB8WAgRdyfG0nihW +oT2N2PcyYdStUPdRTEQavCZ4DZdH9aRSgwnL8LsVIrQDy5Hhv93a65gUY1+ADlqs +f2ojW6ssZktO5CTfsm5KLHxKv1tF1Ju50cPtJNgU/8Nzxfi7hHDTJEkSUKzwIifK +hmeS4ESXMDo2UxiFFcbxibhLoggcuksu7bwFxQZN+C0rckqBUjipKleAQZE02W1A +o7w+evb/PHomMMVSpTR3STRmK/SVXmj4Fq+t3njy4pDzOUXOC0WKrNvad6tOikHV +wS/MrHlqZhjwULGQAjrzuf1zyzioiYkKhLFaVAAk8jBivJZqYEbTbmCTZfiDK2Jz +FpDsUsc2uNHUIBPdI7rmuBjspxhp1f6eoP04vIh02hLulMg8QA+7IwavSQjcXt2d +ju+MQJeBkEXYwLsVMlpyQ0wEH3Cnj3Wwk9vEvoBRxL/rwzhcRgT6nuVRMybLHDjv +dQKTwIwulrtYGCLcjfQR4EYTUu756BgcuhQfEytd948m3sBsst+m2YZWUT6yfKDg +p95ekAjVN0zcTGbvKB4bnKKLLF85q3ir+9uyMNet2Oi/f+u6cFZbyhLT6DsfVqGp +UKSuKABCqlgH77ztGAhaKv8JDgfHszghd8KvrES7cJmV1HFE/xCEtTBx20Ll6H8f +fA== +=ILcU +-----END PGP PRIVATE KEY BLOCK----- diff --git a/internal/cloudplugin/testdata/sample.public.key b/internal/cloudplugin/testdata/sample.public.key new file mode 100644 index 0000000000..d5ae2f3bc0 --- /dev/null +++ b/internal/cloudplugin/testdata/sample.public.key @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGTD2nkBEACzN+0KhgkfyObviYGvtWWCQfznX440nIiu0uag1lRGh6MeupOw +cPFMtSSoltSYTTkS3J6UBvTOQbPq/IAX0H3WBnUWFJpA9h87fHbpxquEeYeUQ65r +J/IRLsPFnzOl43CCSnkDuppJqYSPJc1GJYtb4O8Tq7+Af7rdZFWoPW1bo7NIriUm +MM9y3udb8Catvz5L7aYevQ001x9jP1SzpkMVWY4/TxTrE7Xjxh7Ke7MlJmIeFWHl +ja/mjukcZZAD/KwHU+mid+MZhx1CmiN36CuYBcLpP2eJTrrhYOps8kX5z1sKsIaY +4KF1yVkLGQxGS8y2M0kZXjYtNPxY1DJGveWIRzcx+7KWkavkVbhvmrMRWLkKv+55 +BV6lsLrOYFrCTRxolNOx5ZNcUbaIucqGYe8RzQz/WlPPA3TbMnOQ7OSTIqhqMzzd +s7l7S4N7UHP+ePvMy1Uzajfsb80XI9zYRrRMshHjU6huYITwIF5wWzfRVegSfYw+ +bcxr5XWLYmJ5aMA8JNOhdjpszpbZcCZPYPV6Ya6jsXoO4QxD+ivGNBbzEuB+DolW +/6GXqfuJARPx1vPIAWX2j6uVhJb5ygBahd9aPjaURwuM9SZnCTCoc0x7171R6t+g +AIs/wB9D/iHbLEMUgH5KlW+9VtJLw2afiVAeSd38axvn/ELURvpMoJ1s5wARAQAB +tC9IYXNoaUNvcnAgVGVycmFmb3JtIFRlc3QgPGJjcm9mdEBoYXNoaWNvcnAuY29t +PokCTgQTAQgAOBYhBDdNMjVEzPSXGPgR1yAL2ogslbgKBQJkw9p5AhsDBQsJCAcC +BhUKCQgLAgQWAgMBAh4BAheAAAoJECAL2ogslbgKHq8P/0/Y1q88ryWR2t0R8Qve +FLiOwhpoLB1tjuKUfYRgAO2Xcp3xfQkiVknoeAAcp73RVrd0klyejhbfazTj+nq2 +xvUtb2OZ6+rWGtVTd2pmf4QGY3JI/lAYcCOGAlM6gdbsJmowT+X0/Tg1MxdYB1YQ +1wIPT6WFBN+2viwQefW/0MJV21cXhZxC92ayhdKy11/fCJDZrdCRaThrjoc9IqRH +LPxN6tdcTLv189Fhv/rvlCpA9DoV8z5XH8JOx8Sm4kadu2mnXUd9c9myTRNIYkBw +1Bcy/tUmi8Z9z5qapVipCKJxPycc1RJKEmvEDVNRQXoiB77Z7AN4MGE2TkQQAAQf +RpnPByKqUZfittk4mOSAy1tMrrlqCoXrsfdYNcaw6GYP6XYaPC4/PQArE6G93GRo +HPurTy22MI5Zd1miB9yB4sZktEe+YCQslMdzZvjQOF776vISGYeniVc0DCElWoQW +uZbVqBs/n6pLX1WNsu/yz3KfGBVevsnvCLyEDH23VBF73I6J/ZGajnfZtTizEP9t +wbAyg7nGwJkmH0CZX90kY498+c+4Xk4+dw4s96I/TGIOwEkB5BgMKUbWkHDFMKZS +kVveNP03/WoGPKQVVfsH5xJhvTrD4wejxt8JCu9LiJ8BcM6GW+li5WlB+i4RL1W0 +PlTXeemqhpVMCPgluFG9VvM4uQINBGTD2nkBEADFj7rW303bU7Sa1QNzPuFViSUt +x7xdDJ65wLxPxWnry+4Yh8k2wQtoejpcwnyIrja3l9v2GTxlfZnwD7VltcSiv7Bw +YduulgflOYU00V1tRiRr14d+7iNlqMoeZvwaQEJLuCPXB+MWPSyzfMJSYj5oP9OP +LzO6ZtvQQIV8oVFo0SbeoRwYArRK6Hq35WqkLPEj7kvnDYf+a9ZGomNQvPb8iQSq +FJWwkOqZQkE+l6VAbNsTLKe81tGk+y9mT2cVfZJ9wpL2GrBfJRWeICHZcCfHs1Jw +OeQICs4ZKLdvecDhKPvXbhg77kKCYBLsU1w9vr24KhhpiC0HO7MMIhyDY4lDyCmx +jMPSHbMcpjQ8BX6TQCrN+k0gW+/+mbwOiIKR4wu2kksT06Wr2E/jlK44xzwAes3x +BUaUqFdo2e9y3jRHdeUaW2tWVwUVHjLJ52lBaWC/Jtg9wr/oXSitsBfgwiqKfKtp +RPexRP9Q4mNgC76qUDvEbIjv4wOC5a22QC23dtBDkwvsl6bwVV7VPYTHWa44gBrs +OuuufJffTMI2sYQQo45+O/FVslGk+HrjUt8guuXjP1JPJXcYTNRhbuT7Xde7hE0W +f3bUg2CpK//qhpEW162HKaPf/Hf6eH1ey4XVwFXD/CpFyAjPH2Yrhl9U/hyB5mgc +cW3Fbz94UnOzqoTwTQARAQABiQI2BBgBCAAgFiEEN00yNUTM9JcY+BHXIAvaiCyV +uAoFAmTD2nkCGwwACgkQIAvaiCyVuAqC/Q//ZUiS2veNSYYnhN58oYQ3BvfmOIgf +FgIEXcnxtJ4oVqE9jdj3MmHUrVD3UUxEGrwmeA2XR/WkUoMJy/C7FSK0A8uR4b/d +2uuYFGNfgA5arH9qI1urLGZLTuQk37JuSix8Sr9bRdSbudHD7STYFP/Dc8X4u4Rw +0yRJElCs8CInyoZnkuBElzA6NlMYhRXG8Ym4S6IIHLpLLu28BcUGTfgtK3JKgVI4 +qSpXgEGRNNltQKO8Pnr2/zx6JjDFUqU0d0k0Ziv0lV5o+Bavrd548uKQ8zlFzgtF +iqzb2nerTopB1cEvzKx5amYY8FCxkAI687n9c8s4qImJCoSxWlQAJPIwYryWamBG +025gk2X4gyticxaQ7FLHNrjR1CAT3SO65rgY7KcYadX+nqD9OLyIdNoS7pTIPEAP +uyMGr0kI3F7dnY7vjECXgZBF2MC7FTJackNMBB9wp491sJPbxL6AUcS/68M4XEYE ++p7lUTMmyxw473UCk8CMLpa7WBgi3I30EeBGE1Lu+egYHLoUHxMrXfePJt7AbLLf +ptmGVlE+snyg4KfeXpAI1TdM3Exm7ygeG5yiiyxfOat4q/vbsjDXrdjov3/runBW +W8oS0+g7H1ahqVCkrigAQqpYB++87RgIWir/CQ4Hx7M4IXfCr6xEu3CZldRxRP8Q +hLUwcdtC5eh/H3w= +=FKJH +-----END PGP PUBLIC KEY BLOCK----- diff --git a/internal/cloudplugin/testing.go b/internal/cloudplugin/testing.go new file mode 100644 index 0000000000..84007a4107 --- /dev/null +++ b/internal/cloudplugin/testing.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cloudplugin + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" +) + +var testManifest = `{ + "builds": [ + { + "arch": "amd64", + "os": "darwin", + "url": "/archives/terraform-cloudplugin_0.1.0_darwin_amd64.zip" + } + ], + "is_prerelease": true, + "license_class": "ent", + "name": "terraform-cloudplugin", + "status": { + "state": "supported", + "timestamp_updated": "2023-07-31T15:18:20.243Z" + }, + "timestamp_created": "2023-07-31T15:18:20.243Z", + "timestamp_updated": "2023-07-31T15:18:20.243Z", + "url_changelog": "https://github.com/hashicorp/terraform-cloudplugin/blob/main/CHANGELOG.md", + "url_license": "https://github.com/hashicorp/terraform-cloudplugin/blob/main/LICENSE", + "url_project_website": "https://www.terraform.io/", + "url_shasums": "/archives/terraform-cloudplugin_0.1.0_SHA256SUMS", + "url_shasums_signatures": [ + "/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.sig", + "/archives/terraform-cloudplugin_0.1.0_SHA256SUMS.72D7468F.sig" + ], + "url_source_repository": "https://github.com/hashicorp/terraform-cloudplugin", + "version": "0.1.0" +}` + +var ( + // This is the same as timestamp_updated above + testManifestLastModified, _ = time.Parse(time.RFC3339, "2023-07-31T15:18:20Z") +) + +type testHTTPHandler struct { +} + +func (h *testHTTPHandler) Handle(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 Not Found")) + } + + switch r.URL.Path { + case "/api/cloudplugin/v1/manifest.json": + ifModifiedSince, _ := time.Parse(http.TimeFormat, r.Header.Get("If-Modified-Since")) + w.Header().Set("Last-Modified", testManifestLastModified.Format(http.TimeFormat)) + + if ifModifiedSince.Equal(testManifestLastModified) || testManifestLastModified.Before(ifModifiedSince) { + w.WriteHeader(http.StatusNotModified) + } else { + w.Write([]byte(testManifest)) + } + default: + fileToSend, err := os.Open(fmt.Sprintf("testdata/%s", r.URL.Path)) + if err == nil { + io.Copy(w, fileToSend) + return + } + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("404 Not Found")) + } +} + +func newCloudPluginManifestHTTPTestServer(t *testing.T) (*httptest.Server, error) { + t.Helper() + + handler := new(testHTTPHandler) + return httptest.NewServer(http.HandlerFunc(handler.Handle)), nil +} diff --git a/internal/collections/cmp.go b/internal/collections/cmp.go new file mode 100644 index 0000000000..93ac73e0d2 --- /dev/null +++ b/internal/collections/cmp.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import ( + "github.com/google/go-cmp/cmp" +) + +// CmpOptions is a set of options for use with the "go-cmp" module when +// comparing data structures that contain collection types from this package. +// +// Specifically, these options arrange for [Set] and [Map] values to be +// transformed into map[any]any to allow for element-based comparisons. +// +// [Set] of T values transform into a map whose keys have dynamic type +// UniqueKey[T] and whose values have dynamic type T. +// +// [Map] of K, V values transform into a map whose keys have dynamic type +// UniqueKey[K] and whose values have dynamic type MapElem[K, V]. +var CmpOptions cmp.Option + +func init() { + CmpOptions = cmp.Options([]cmp.Option{ + cmp.Transformer("collectionElementsRaw", func(v transformerForCmp) any { + return v.transformForCmp() + }), + }) +} + +// transformerForCmp is a helper interface implemented by both `Set` and `Map` +// types, to work around the fact that go-cmp does all its work with reflection +// and thus cannot rely on the static type information from the type +// parameters. +type transformerForCmp interface { + transformForCmp() any +} + +func (s Set[T]) transformForCmp() any { + ret := make(map[any]any, s.Len()) + // It's okay to access the keys here because this package is allowed to + // depend on its own implementation details. + for k, v := range s.members { + ret[k] = v + } + return ret +} + +func (m Map[K, V]) transformForCmp() any { + ret := make(map[any]any, m.Len()) + // It's okay to access the keys here because this package is allowed to + // depend on its own implementation details. + for k, v := range m.elems { + ret[k] = v + } + return ret +} diff --git a/internal/collections/doc.go b/internal/collections/doc.go new file mode 100644 index 0000000000..1d10adec93 --- /dev/null +++ b/internal/collections/doc.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package collections contains some helper types representing collections +// of values that could not normally be represented using Go's built-in +// collection types, typically because of the need to use key types that +// are not directly comparable. +// +// There have been some discussions about introducing similar functionality +// into the Go standard library. Should that happen in future then we should +// consider removing the types from this package and adapting callers to use +// the standard library equivalents instead, since there is nothing +// Terraform-specific about the implementations in this package. +package collections diff --git a/internal/collections/map.go b/internal/collections/map.go new file mode 100644 index 0000000000..04558626ee --- /dev/null +++ b/internal/collections/map.go @@ -0,0 +1,146 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import "iter" + +// Set represents an associative array from keys of type K to values of type V. +// +// A caller-provided "key function" defines how to produce a comparable unique +// key for each distinct value of type K. +// +// Map operations are not concurrency-safe. Use external locking if multiple +// goroutines might modify the map concurrently or if one goroutine might +// read a map while another is modifying it. +type Map[K, V any] struct { + elems map[UniqueKey[K]]MapElem[K, V] + key func(K) UniqueKey[K] +} + +// MapElem represents a single element of a map. +type MapElem[K, V any] struct { + K K + V V +} + +// NewMap constructs a new map whose key type knows how to calculate its own +// unique keys, by implementing [UniqueKeyer] of itself. +func NewMap[K UniqueKeyer[K], V any](elems ...MapElem[K, V]) Map[K, V] { + m := NewMapFunc[K, V](K.UniqueKey) + for _, elems := range elems { + m.Put(elems.K, elems.V) + } + return m +} + +// NewMapFunc constructs a new map with the given "map function". +// +// A valid key function must produce only values of types that can be compared +// for equality using the Go == operator, and must guarantee that each unique +// value of K has a corresponding key that uniquely identifies it. The +// implementer of the key function can decide what constitutes a +// "unique value of K", based on the meaning of type K. +// +// Type V is unconstrained by the arguments, so callers must explicitly provide +// the key and value type arguments when calling this function. +func NewMapFunc[K, V any](keyFunc func(K) UniqueKey[K]) Map[K, V] { + return Map[K, V]{ + elems: make(map[UniqueKey[K]]MapElem[K, V]), + key: keyFunc, + } +} + +// NewMapCmp constructs a new set for any comparable key type, using the +// built-in == operator as the definition of key equivalence. +// +// This is here for completeness in case it's useful when talking to a +// generalized API that operates on maps of any key type, but if your +// key type is fixed and known to be comparable then it's pointless to +// use [Map]; use Go's built-in map types instead, which will then avoid +// redundantly storing the keys twice. +func NewMapCmp[K comparable, V any]() Map[K, V] { + return NewMapFunc[K, V](cmpUniqueKeyFunc[K]) +} + +// HasKey returns true if the map has an element with the given key, or +// false otherwise. +func (m Map[K, V]) HasKey(k K) bool { + if m.key == nil { + return false // an uninitialized map has no keys + } + uniq := m.key(k) + _, exists := m.elems[uniq] + return exists +} + +// Get retrieves the value associated with the given key, or the zero value +// of V if no matching element exists in the map. +func (m Map[K, V]) Get(k K) V { + ret, _ := m.GetOk(k) + return ret +} + +// GetOk is like [Map.Get] but also returns a second boolean result reporting +// whether a matching element was present in the map. +func (m Map[K, V]) GetOk(k K) (V, bool) { + if m.key == nil { + var zero V + return zero, false // an uninitialized map has no keys + } + uniq := m.key(k) + ret, ok := m.elems[uniq] + return ret.V, ok +} + +// Put writes a new element into the map, with the given key and value. +// +// If there is already an element with an equivalent key (as determined by the +// set's "key function") then the new element replaces that existing element. +func (m Map[K, V]) Put(k K, v V) { + if m.key == nil { + panic("Put into uninitialized collections.Map") + } + uniq := m.key(k) + m.elems[uniq] = MapElem[K, V]{ + K: k, + V: v, + } +} + +// Delete removes from the map the element with the given key, or does nothing +// if there is no such element. +func (m Map[K, V]) Delete(k K) { + if m.key == nil { + panic("Delete from uninitialized collections.Map") + } + uniq := m.key(k) + delete(m.elems, uniq) +} + +// All returns an iterator over the elements of the map, in an unspecified +// order. +// +// This is intended for use in a range-over-func statement, like this: +// +// for k, v := range map.All() { +// // do something with k and/or v +// } +// +// Modifying the map during iteration causes unspecified results. Modifying +// the map concurrently with advancing the iterator causes undefined behavior +// including possible memory unsafety. +func (m Map[K, V]) All() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, elem := range m.elems { + if !yield(elem.K, elem.V) { + return + } + } + } +} + +// Len returns the number of elements in the map. +func (m Map[K, V]) Len() int { + return len(m.elems) +} diff --git a/internal/collections/map_test.go b/internal/collections/map_test.go new file mode 100644 index 0000000000..164a53cdea --- /dev/null +++ b/internal/collections/map_test.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import "testing" + +func TestMap(t *testing.T) { + m := NewMap[testingKey, string]() + + if got, want := m.Len(), 0; got != want { + t.Errorf("wrong initial number of elements\ngot: %#v\nwant: %#v", got, want) + } + + m.Put(testingKey("a"), "A") + if got, want := m.Len(), 1; got != want { + t.Errorf("wrong number of elements after adding \"a\"\ngot: %#v\nwant: %#v", got, want) + } + if got, want := m.Get(testingKey("a")), "A"; got != want { + t.Errorf("wrong value for \"a\"\ngot: %#v\nwant: %#v", got, want) + } + + m.Put(testingKey("a"), "A'") + if got, want := m.Len(), 1; got != want { + t.Errorf("wrong number of elements after re-adding \"a\"\ngot: %#v\nwant: %#v", got, want) + } + if got, want := m.Get(testingKey("a")), "A'"; got != want { + t.Errorf("wrong updated value for \"a\"\ngot: %#v\nwant: %#v", got, want) + } + + m.Delete(testingKey("a")) + if got, want := m.Len(), 0; got != want { + t.Errorf("wrong number of elements after removing \"m\"\ngot: %#v\nwant: %#v", got, want) + } +} diff --git a/internal/collections/set.go b/internal/collections/set.go new file mode 100644 index 0000000000..c50cbe70c2 --- /dev/null +++ b/internal/collections/set.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import "iter" + +// Set represents an unordered set of values of a particular type. +// +// A caller-provided "key function" defines how to produce a comparable unique +// key for each distinct value of type T. +// +// Set operations are not concurrency-safe. Use external locking if multiple +// goroutines might modify the set concurrently or if one goroutine might +// read a set while another is modifying it. +type Set[T any] struct { + members map[UniqueKey[T]]T + key func(T) UniqueKey[T] +} + +// NewSet constructs a new set whose element type knows how to calculate its own +// unique keys, by implementing [UniqueKeyer] of itself. +func NewSet[T UniqueKeyer[T]](elems ...T) Set[T] { + return NewSetFunc(T.UniqueKey, elems...) +} + +// NewSetFunc constructs a new set with the given "key function". +// +// A valid key function must produce only values of types that can be compared +// for equality using the Go == operator, and must guarantee that each unique +// value of T has a corresponding key that uniquely identifies it. The +// implementer of the key function can decide what constitutes a +// "unique value of T", based on the meaning of type T. +func NewSetFunc[T any](keyFunc func(T) UniqueKey[T], elems ...T) Set[T] { + set := Set[T]{ + members: make(map[UniqueKey[T]]T), + key: keyFunc, + } + for _, elem := range elems { + set.Add(elem) + } + return set +} + +// NewSetCmp constructs a new set for any comparable type, using the built-in +// == operator as the definition of element equivalence. +func NewSetCmp[T comparable](elems ...T) Set[T] { + return NewSetFunc(cmpUniqueKeyFunc[T], elems...) +} + +// Has returns true if the given value is present in the set, or false +// otherwise. +func (s Set[T]) Has(v T) bool { + if len(s.members) == 0 { + // We'll skip calling "s.key" in this case, so that we don't panic + // if called on an uninitialized Set. + return false + } + k := s.key(v) + _, ok := s.members[k] + return ok +} + +// Add inserts new members into the set. +// +// If any existing member of the set is considered to be equivalent to a +// given value per the rules in the set's "key function", the old value will +// be discarded and replaced by the new value. +// +// If multiple of the given arguments is considered to be equivalent then +// only the later one is retained. +func (s Set[T]) Add(vs ...T) { + for _, v := range vs { + k := s.key(v) + s.members[k] = v + } +} + +// AddAll inserts all the members of vs into the set. +// +// The behavior is the same as calling Add for each member of vs. +func (s Set[T]) AddAll(vs Set[T]) { + for v := range vs.All() { + s.Add(v) + } +} + +// Remove removes the given member from the set, or does nothing if no +// equivalent value was present. +func (s Set[T]) Remove(v T) { + k := s.key(v) + delete(s.members, k) +} + +// All returns an iterator over the elements of the set, in an unspecified +// order. +// +// The result of this function is part of the internal state of the set +// and so callers MUST NOT modify it. If a caller is using locks to ensure +// safe concurrent access then any reads of the resulting map must be +// guarded by the same lock as would be used for other methods that read +// data from the set. +// +// All returns an iterator over the elements of the set, in an unspecified +// order. +// +// for elem := range set.All() { +// // do something with elem +// } +// +// Modifying the set during iteration causes unspecified results. Modifying +// the set concurrently with advancing the iterator causes undefined behavior +// including possible memory unsafety. +func (s Set[T]) All() iter.Seq[T] { + return func(yield func(T) bool) { + for _, v := range s.members { + if !yield(v) { + return + } + } + } +} + +// Len returns the number of unique elements in the set. +func (s Set[T]) Len() int { + return len(s.members) +} diff --git a/internal/collections/set_test.go b/internal/collections/set_test.go new file mode 100644 index 0000000000..b7b4acb188 --- /dev/null +++ b/internal/collections/set_test.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import ( + "testing" +) + +func TestSet(t *testing.T) { + set := NewSet[testingKey]() + + if got, want := set.Len(), 0; got != want { + t.Errorf("wrong initial number of elements\ngot: %#v\nwant: %#v", got, want) + } + + set.Add(testingKey("a")) + if got, want := set.Len(), 1; got != want { + t.Errorf("wrong number of elements after adding \"a\"\ngot: %#v\nwant: %#v", got, want) + } + + set.Add(testingKey("a")) + set.Add(testingKey("b")) + if got, want := set.Len(), 2; got != want { + t.Errorf("wrong number of elements after re-adding \"a\" and adding \"b\"\ngot: %#v\nwant: %#v", got, want) + } + + set.Remove(testingKey("a")) + if got, want := set.Len(), 1; got != want { + t.Errorf("wrong number of elements after removing \"a\"\ngot: %#v\nwant: %#v", got, want) + } + + if got, want := set.Has(testingKey("a")), false; got != want { + t.Errorf("set still has \"a\" after removing it") + } + if got, want := set.Has(testingKey("b")), true; got != want { + t.Errorf("set doesn't have \"b\" after adding it") + } +} + +func TestSetUninit(t *testing.T) { + // An zero-value set should behave like it's empty for read-only operations. + var zeroSet Set[string] + if got, want := zeroSet.Len(), 0; got != want { + t.Errorf("wrong number of elements\ngot: %d\nwant: %d", got, want) + } + if zeroSet.Has("anything") { + // (this is really just testing that we can call Has without panicking; + // it's unlikely that this would ever fail by successfully lying about + // a particular member being present.) + t.Error("Has reported that \"anything\" is present") + } +} diff --git a/internal/collections/unique_key.go b/internal/collections/unique_key.go new file mode 100644 index 0000000000..d6caf0f3b9 --- /dev/null +++ b/internal/collections/unique_key.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +// UniqueKey represents a value that is comparable and uniquely identifies +// another value of type T. +// +// The Go type system offers no way to guarantee at compile time that +// implementations of this type are comparable, but if this interface is +// implemented by an uncomparable type then that will cause runtime panics +// when inserting elements into collection types that use unique keys. +// +// We use this to help with correctness of the unique-key-generator callbacks +// used with the collection types in this package, so help with type parameter +// inference and to raise compile-time errors if an inappropriate callback +// is used as the key generator for a particular collection. +type UniqueKey[T any] interface { + // Implementations must include an IsUniqueKey method with an empty body + // just as a compile-time assertion that they are intended to behave as + // unique keys for a particular other type. + // + // This method is never actually called by the collection types. Other + // callers could potentially call it, but it would be strange and pointless + // to do so. + IsUniqueKey(T) +} + +// A UniqueKeyer is a type that knows how to calculate a unique key itself. +type UniqueKeyer[T any] interface { + // UniqueKey returns the unique key of the reciever. + // + // A correct implementation of UniqueKey must return a distinct value + // for each unique value of T, where the uniqueness of T values is decided + // by the implementer. See [UniqueKey] for more information. + // + // Although not enforced directly by the Go type system, it doesn't make + // sense for a type to implement [UniqueKeyer] for any type other than + // itself. Such a nonsensical implementation will not be accepted by + // functions like [NewSet] and [NewMap]. + UniqueKey() UniqueKey[T] +} + +// cmpUniqueKey is an annoying little adapter used to make arbitrary +// comparable types usable with [Set] and [Map]. +// +// It just wraps a single-element array of T around the value, so it +// remains exactly as comparable as T. However, it does unfortunately +// mean redundantly storing T twice -- both as the unique key and the +// value -- in our collections. +type cmpUniqueKey[T comparable] [1]T + +func (cmpUniqueKey[T]) IsUniqueKey(T) {} + +func cmpUniqueKeyFunc[T comparable](v T) UniqueKey[T] { + return cmpUniqueKey[T]{v} +} diff --git a/internal/collections/unique_key_test.go b/internal/collections/unique_key_test.go new file mode 100644 index 0000000000..9cfabe0527 --- /dev/null +++ b/internal/collections/unique_key_test.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +type testingKey string + +// testingKey is its own UniqueKey, because it's already a comparable type +var _ UniqueKey[testingKey] = testingKey("") +var _ UniqueKeyer[testingKey] = testingKey("") + +func (k testingKey) IsUniqueKey(testingKey) {} + +// UniqueKey implements UniqueKeyer. +func (k testingKey) UniqueKey() UniqueKey[testingKey] { + return UniqueKey[testingKey](k) +} diff --git a/internal/command/apply.go b/internal/command/apply.go index 2ae8a63782..3bf441ecad 100644 --- a/internal/command/apply.go +++ b/internal/command/apply.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/plans/planfile" @@ -62,23 +65,13 @@ func (c *ApplyCommand) Run(rawArgs []string) int { } // Attempt to load the plan file, if specified - planFile, diags := c.LoadPlanFile(args.PlanPath) + planFile, loadPlanFileDiags := c.LoadPlanFile(args.PlanPath) + diags = diags.Append(loadPlanFileDiags) if diags.HasErrors() { view.Diagnostics(diags) return 1 } - // Check for invalid combination of plan file and variable overrides - if planFile != nil && !args.Vars.Empty() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Can't set variables when applying a saved plan", - "The -var and -var-file options cannot be used when applying a saved plan file, because a saved plan includes the variable values that were set when it was created.", - )) - view.Diagnostics(diags) - return 1 - } - // FIXME: the -input flag value is needed to initialize the backend and the // operation, but there is no clear path to pass this value down, so we // continue to mutate the Meta object state for now. @@ -94,7 +87,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int { // Prepare the backend, passing the plan file if present, and the // backend-specific arguments - be, beDiags := c.PrepareBackend(planFile, args.State) + be, beDiags := c.PrepareBackend(planFile, args.State, args.ViewType) diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -102,8 +95,12 @@ func (c *ApplyCommand) Run(rawArgs []string) int { } // Build the operation request - opReq, opDiags := c.OperationRequest(be, view, planFile, args.Operation, args.AutoApprove) + opReq, opDiags := c.OperationRequest(be, view, args.ViewType, planFile, args.Operation, args.AutoApprove) diags = diags.Append(opDiags) + if diags.HasErrors() { + view.Diagnostics(diags) + return 1 + } // Collect variable value and add them to the operation request diags = diags.Append(c.GatherVariables(opReq, args.Vars)) @@ -125,7 +122,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int { return 1 } - if op.Result != backend.OperationSuccess { + if op.Result != backendrun.OperationSuccess { return op.Result.ExitStatus() } @@ -134,7 +131,7 @@ func (c *ApplyCommand) Run(rawArgs []string) int { if rb, isRemoteBackend := be.(BackendWithRemoteTerraformVersion); !isRemoteBackend || rb.IsLocalOperations() { view.ResourceCount(args.State.StateOutPath) if !c.Destroy && op.State != nil { - view.Outputs(op.State.RootModule().OutputValues) + view.Outputs(op.State.RootOutputValues) } } @@ -147,8 +144,8 @@ func (c *ApplyCommand) Run(rawArgs []string) int { return 0 } -func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.Reader, tfdiags.Diagnostics) { - var planFile *planfile.Reader +func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.WrappedPlanFile, tfdiags.Diagnostics) { + var planFile *planfile.WrappedPlanFile var diags tfdiags.Diagnostics // Try to load plan if path is specified @@ -191,7 +188,7 @@ func (c *ApplyCommand) LoadPlanFile(path string) (*planfile.Reader, tfdiags.Diag return planFile, diags } -func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments.State) (backend.Enhanced, tfdiags.Diagnostics) { +func (c *ApplyCommand) PrepareBackend(planFile *planfile.WrappedPlanFile, args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // FIXME: we need to apply the state arguments to the meta object here @@ -201,20 +198,10 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments c.Meta.applyStateArguments(args) // Load the backend - var be backend.Enhanced + var be backendrun.OperationsBackend var beDiags tfdiags.Diagnostics - if planFile == nil { - backendConfig, configDiags := c.loadBackendConfig(".") - diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return nil, diags - } - - be, beDiags = c.Backend(&BackendOpts{ - Config: backendConfig, - }) - } else { - plan, err := planFile.ReadPlan() + if lp, ok := planFile.Local(); ok { + plan, err := lp.ReadPlan() if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -232,7 +219,19 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments )) return nil, diags } - be, beDiags = c.BackendForPlan(plan.Backend) + be, beDiags = c.BackendForLocalPlan(plan.Backend) + } else { + // Both new plans and saved cloud plans load their backend from config. + backendConfig, configDiags := c.loadBackendConfig(".") + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return nil, diags + } + + be, beDiags = c.Backend(&BackendOpts{ + Config: backendConfig, + ViewType: viewType, + }) } diags = diags.Append(beDiags) @@ -243,21 +242,27 @@ func (c *ApplyCommand) PrepareBackend(planFile *planfile.Reader, args *arguments } func (c *ApplyCommand) OperationRequest( - be backend.Enhanced, + be backendrun.OperationsBackend, view views.Apply, - planFile *planfile.Reader, + viewType arguments.ViewType, + planFile *planfile.WrappedPlanFile, args *arguments.Operation, autoApprove bool, -) (*backend.Operation, tfdiags.Diagnostics) { +) (*backendrun.Operation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Applying changes with dev overrides in effect could make it impossible // to switch back to a release version if the schema isn't compatible, // so we'll warn about it. - diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) + b, isRemoteBackend := be.(BackendWithRemoteTerraformVersion) + if isRemoteBackend && !b.IsLocalOperations() { + diags = diags.Append(c.providerDevOverrideRuntimeWarningsRemoteExecution()) + } else { + diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) + } // Build the operation - opReq := c.Operation(be) + opReq := c.Operation(be, viewType) opReq.AutoApprove = autoApprove opReq.ConfigDir = "." opReq.PlanMode = args.PlanMode @@ -266,8 +271,23 @@ func (c *ApplyCommand) OperationRequest( opReq.PlanRefresh = args.Refresh opReq.Targets = args.Targets opReq.ForceReplace = args.ForceReplace - opReq.Type = backend.OperationTypeApply + opReq.Type = backendrun.OperationTypeApply opReq.View = view.Operation() + opReq.StatePersistInterval = c.Meta.StatePersistInterval() + + // EXPERIMENTAL: maybe enable deferred actions + if c.AllowExperimentalFeatures { + opReq.DeferralAllowed = args.DeferralAllowed + } else if args.DeferralAllowed { + // Belated flag parse error, since we don't know about experiments + // support at actual parse time. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "The -allow-deferral flag is only valid in experimental builds of Terraform.", + )) + return nil, diags + } var err error opReq.ConfigLoader, err = c.initConfigLoader() @@ -279,7 +299,7 @@ func (c *ApplyCommand) OperationRequest( return opReq, diags } -func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics { +func (c *ApplyCommand) GatherVariables(opReq *backendrun.Operation, args *arguments.Vars) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // FIXME the arguments package currently trivially gathers variable related @@ -290,12 +310,12 @@ func (c *ApplyCommand) GatherVariables(opReq *backend.Operation, args *arguments // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + items := make([]arguments.FlagNameValue, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } - c.Meta.variableArgs = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags @@ -342,6 +362,10 @@ Options: accompanied by errors, show them in a more compact form that includes only the summary messages. + -destroy Destroy Terraform-managed infrastructure. + The command "terraform destroy" is a convenience alias + for this option. + -lock=false Don't hold a state lock during the operation. This is dangerous if others might concurrently run commands against the same workspace. @@ -361,6 +385,15 @@ Options: -state-out=path Path to write state to that is different than "-state". This can be used to preserve the old state. + + -var 'foo=bar' Set a value for one of the input variables in the root + module of the configuration. Use this option more than + once to set more than one variable. + + -var-file=filename Load variable values from the given file, in addition + to the default files terraform.tfvars and *.auto.tfvars. + Use this option more than once to include more than one + variables file. If you don't provide a saved plan file then this command will also accept all of the plan-customization options accepted by the terraform plan command. diff --git a/internal/command/apply_destroy_test.go b/internal/command/apply_destroy_test.go index 27aaff9017..f5b79cac31 100644 --- a/internal/command/apply_destroy_test.go +++ b/internal/command/apply_destroy_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,7 +9,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -45,7 +48,7 @@ func TestApply_destroy(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -428,14 +431,14 @@ func TestApply_destroyTargetedDependencies(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, "test_load_balancer": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "instances": {Type: cty.List(cty.String), Optional: true}, @@ -579,14 +582,14 @@ func TestApply_destroyTargeted(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, }, }, "test_load_balancer": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "instances": {Type: cty.List(cty.String), Optional: true}, diff --git a/internal/command/apply_test.go b/internal/command/apply_test.go index ef4484c401..24bca84665 100644 --- a/internal/command/apply_test.go +++ b/internal/command/apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -15,16 +18,18 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -289,6 +294,10 @@ func TestApply_parallelism(t *testing.T) { // to proceed in unison. beginCtx, begin := context.WithCancel(context.Background()) + // This just makes go vet happy, in reality the function will never exit if + // begin() isn't called inside ApplyResourceChangeFn. + defer begin() + // Since our mock provider has its own mutex preventing concurrent calls // to ApplyResourceChange, we need to use a number of separate providers // here. They will all have the same mock implementation function assigned @@ -296,10 +305,10 @@ func TestApply_parallelism(t *testing.T) { providerFactories := map[addrs.Provider]providers.Factory{} for i := 0; i < 10; i++ { name := fmt.Sprintf("test%d", i) - provider := &terraform.MockProvider{} + provider := &testing_provider.MockProvider{} provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ - name + "_instance": {Block: &configschema.Block{}}, + name + "_instance": {Body: &configschema.Block{}}, }, } provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { @@ -484,7 +493,7 @@ func TestApply_error(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -790,18 +799,21 @@ func TestApply_plan_remoteState(t *testing.T) { _, snap := testModuleWithSnapshot(t, "apply") backendConfig := cty.ObjectVal(map[string]cty.Value{ - "address": cty.StringVal(srv.URL), - "update_method": cty.NullVal(cty.String), - "lock_address": cty.NullVal(cty.String), - "unlock_address": cty.NullVal(cty.String), - "lock_method": cty.NullVal(cty.String), - "unlock_method": cty.NullVal(cty.String), - "username": cty.NullVal(cty.String), - "password": cty.NullVal(cty.String), - "skip_cert_verification": cty.NullVal(cty.Bool), - "retry_max": cty.NullVal(cty.String), - "retry_wait_min": cty.NullVal(cty.String), - "retry_wait_max": cty.NullVal(cty.String), + "address": cty.StringVal(srv.URL), + "update_method": cty.NullVal(cty.String), + "lock_address": cty.NullVal(cty.String), + "unlock_address": cty.NullVal(cty.String), + "lock_method": cty.NullVal(cty.String), + "unlock_method": cty.NullVal(cty.String), + "username": cty.NullVal(cty.String), + "password": cty.NullVal(cty.String), + "skip_cert_verification": cty.NullVal(cty.Bool), + "retry_max": cty.NullVal(cty.String), + "retry_wait_min": cty.NullVal(cty.String), + "retry_wait_max": cty.NullVal(cty.String), + "client_ca_certificate_pem": cty.NullVal(cty.String), + "client_certificate_pem": cty.NullVal(cty.String), + "client_private_key_pem": cty.NullVal(cty.String), }) backendConfigRaw, err := plans.NewDynamicValue(backendConfig, backendConfig.Type()) if err != nil { @@ -812,7 +824,7 @@ func TestApply_plan_remoteState(t *testing.T) { Type: "http", Config: backendConfigRaw, }, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), }) p := testProvider() @@ -848,11 +860,12 @@ func TestApply_plan_remoteState(t *testing.T) { func TestApply_planWithVarFile(t *testing.T) { varFileDir := testTempDir(t) varFilePath := filepath.Join(varFileDir, "terraform.tfvars") - if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { + if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } - planPath := applyFixturePlanFile(t) + // The value of foo is the same as in the var file + planPath := applyFixturePlanFileWithVariableValue(t, "bar") statePath := testTempFile(t) cwd, err := os.Getwd() @@ -865,6 +878,19 @@ func TestApply_planWithVarFile(t *testing.T) { defer os.Chdir(cwd) p := applyFixtureProvider() + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + } + view, done := testView(t) c := &ApplyCommand{ Meta: Meta{ @@ -893,7 +919,57 @@ func TestApply_planWithVarFile(t *testing.T) { } } -func TestApply_planVars(t *testing.T) { +func TestApply_planWithVarFileChangingVariableValue(t *testing.T) { + varFileDir := testTempDir(t) + varFilePath := filepath.Join(varFileDir, "terraform-test.tfvars") + if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + // The value of foo is different from the var file + planPath := applyFixturePlanFileWithVariableValue(t, "lorem ipsum") + statePath := testTempFile(t) + + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(varFileDir); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-state-out", statePath, + "-var-file", varFilePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code == 0 { + t.Fatalf("expected to fail, but succeeded. \n\n%s", output.All()) + } + + expectedTitle := "Can't change variable when applying a saved plan" + if !strings.Contains(output.Stderr(), expectedTitle) { + t.Fatalf("Expected stderr to contain %q, got %q", expectedTitle, output.Stderr()) + } +} + +func TestApply_planUndeclaredVars(t *testing.T) { + // This test ensures that it isn't allowed to set undeclared input variables + // when applying from a saved plan file, since in that case the variable + // values come from the saved plan file. + planPath := applyFixturePlanFile(t) statePath := testTempFile(t) @@ -918,6 +994,475 @@ func TestApply_planVars(t *testing.T) { } } +func TestApply_planWithEnvVars(t *testing.T) { + _, snap := testModuleWithSnapshot(t, "apply-output-only") + plan := testPlan(t) + + addr, diags := addrs.ParseAbsOutputValueStr("output.shadow") + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + shadowVal := mustNewDynamicValue("noot", cty.DynamicPseudoType) + plan.VariableValues = map[string]plans.DynamicValue{ + "shadow": shadowVal, + } + plan.Changes.Outputs = append(plan.Changes.Outputs, &plans.OutputChangeSrc{ + Addr: addr, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + After: shadowVal, + }, + }) + planPath := testPlanFileMatchState( + t, + snap, + states.NewState(), + plan, + statemgr.SnapshotMeta{}, + ) + + statePath := testTempFile(t) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + t.Setenv("TF_VAR_shadow", "env") + + args := []string{ + "-state", statePath, + "-no-color", + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("unexpected failure: ", output.All()) + } + + expectedWarn := "Warning: Ignoring variable when applying a saved plan\n" + if !strings.Contains(output.Stdout(), expectedWarn) { + t.Fatalf("expected warning in output, given: %q", output.Stdout()) + } +} + +// A saved plan includes a list of "apply-time variables", i.e. ephemeral +// input variables that were set during the plan, and must therefore be set +// during apply. No other variables may be set during apply. +// +// Test that an apply supplying all apply-time variables succeeds, and then test +// that supplying a declared ephemeral input variable that is *not* in the list +// of apply-time variables fails. +// +// In the fixture used for this test foo is a required ephemeral variable, whereas bar is +// an optional one. +func TestApply_planVarsEphemeral_applyTime(t *testing.T) { + for name, tc := range map[string]func(*testing.T, *ApplyCommand, string, string, func(*testing.T) *terminal.TestOutput){ + "with planfile only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + args := []string{ + "-state", statePath, + "-var", "foo=bar", + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "with planfile passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + args := []string{ + "-state", statePath, + "-var", "foo=bar", + "-var", "bar=bar", + planPath, + } + code := c.Run(args) + output := done(t) + if code == 0 { + t.Fatal("should've failed: ", output.All()) + } + }, + + "with planfile missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + args := []string{ + "-state", statePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code == 0 { + t.Fatal("should've failed: ", output.All()) + } + }, + + "with planfile passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + const planVarFile = ` +foo = "bar" +` + + // Write a tfvars file with the variable + tfVarsPath := testVarsFile(t) + err := os.WriteFile(tfVarsPath, []byte(planVarFile), 0600) + if err != nil { + t.Fatalf("Could not write vars file %e", err) + } + + args := []string{ + "-state", statePath, + "-var-file", tfVarsPath, + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "with planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + t.Setenv("TF_VAR_foo", "bar") + + args := []string{ + "-state", statePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "with planfile passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + close := testInteractiveInput(t, []string{"bar"}) + defer close() + + args := []string{ + "-state", statePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code == 0 { + // We don't support interactive inputs for apply-time variables + t.Fatal("should have failed: ", output.All()) + } + }, + + "without planfile only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + args := []string{ + "-state", statePath, + "-var", "foo=bar", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "without planfile passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + args := []string{ + "-state", statePath, + "-var", "foo=bar", + "-var", "bar=bar", + } + code := c.Run(args) + output := done(t) + + // For a combined plan & apply operation it's okay (and expected) to also be able to pass non-ephemeral variables + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "without planfile missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + args := []string{ + "-state", statePath, + } + code := c.Run(args) + output := done(t) + if code == 0 { + t.Fatal("should've failed: ", output.All()) + } + }, + + "without planfile passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + const planVarFile = ` +foo = "bar" +` + + // Write a tfvars file with the variable + tfVarsPath := testVarsFile(t) + err := os.WriteFile(tfVarsPath, []byte(planVarFile), 0600) + if err != nil { + t.Fatalf("Could not write vars file %e", err) + } + + args := []string{ + "-state", statePath, + "-var-file", tfVarsPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "without planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + t.Setenv("TF_VAR_foo", "bar") + t.Setenv("TF_VAR_unused", `{key:"val"}`) + + args := []string{ + "-state", statePath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + + "without planfile passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) { + close := testInteractiveInput(t, []string{"bar"}) + defer close() + + args := []string{ + "-state", statePath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatal("should've succeeded: ", output.All()) + } + }, + } { + t.Run(name, func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("apply-ephemeral-variable"), td) + defer testChdir(t, td)() + + _, snap := testModuleWithSnapshot(t, "apply-ephemeral-variable") + plannedVal := cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "ami": cty.StringVal("bar"), + }) + priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) + if err != nil { + t.Fatal(err) + } + plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) + if err != nil { + t.Fatal(err) + } + plan := testPlan(t) + plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: priorValRaw, + After: plannedValRaw, + }, + }) + applyTimeVariables := collections.NewSetCmp[string]() + applyTimeVariables.Add("foo") + plan.ApplyTimeVariables = applyTimeVariables + + planPath := testPlanFileMatchState( + t, + snap, + states.NewState(), + plan, + statemgr.SnapshotMeta{}, + ) + + statePath := testTempFile(t) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + tc(t, c, statePath, planPath, done) + }) + } +} + +// Variables can be passed to apply now for ephemeral usage, but we need to +// ensure that the legacy handling of undeclared variables remains intact +func TestApply_changedVars_applyTime(t *testing.T) { + t.Run("undeclared-config-var", func(t *testing.T) { + // an undeclared config variable is a warning, just like during plan + varFileDir := testTempDir(t) + varFilePath := filepath.Join(varFileDir, "terraform.tfvars") + if err := os.WriteFile(varFilePath, []byte(`undeclared = true`), 0644); err != nil { + t.Fatalf("err: %s", err) + } + + // The value of foo is not set + planPath := applyFixturePlanFile(t) + statePath := testTempFile(t) + + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(varFileDir); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-state-out", statePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All()) + } + + if !strings.Contains(output.All(), `Value for undeclared variable`) { + t.Fatalf("missing undeclared warning:\n%s", output.All()) + } + }) + + t.Run("undeclared-cli-var", func(t *testing.T) { + // an undeclared cli variable is an error, just like during plan + planPath := applyFixturePlanFile(t) + statePath := testTempFile(t) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-var", "undeclared=true", + "-state-out", statePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code != 1 { + t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All()) + } + + if !strings.Contains(output.Stderr(), `Value for undeclared variable`) { + t.Fatalf("missing undeclared warning:\n%s", output.All()) + } + }) + + t.Run("changed-cli-var", func(t *testing.T) { + planPath := applyFixturePlanFileWithVariableValue(t, "orig") + statePath := testTempFile(t) + + p := applyFixtureProvider() + view, done := testView(t) + c := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-var", "foo=new", + "-state-out", statePath, + planPath, + } + code := c.Run(args) + output := done(t) + if code != 1 { + t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All()) + } + + if !strings.Contains(output.Stderr(), `Can't change variable when applying a saved plan`) { + t.Fatalf("missing undeclared warning:\n%s", output.All()) + } + }) + + t.Run("var-file-override-auto", func(t *testing.T) { + // for this one we're going to do a full plan to make sure the variables + // can be applied consistently. The plan specifies a var file, and + // during apply we don't want to override that with the default or auto + // var files. + td := t.TempDir() + testCopyDir(t, testFixturePath("apply-vars-auto"), td) + defer testChdir(t, td)() + + p := planVarsFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-var-file", "terraform-test.tfvars", + "-out", "planfile", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr()) + } + + view, done = testView(t) + apply := &ApplyCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: new(cli.MockUi), + View: view, + }, + } + args = []string{ + "planfile", + } + code = apply.Run(args) + output = done(t) + if code != 0 { + t.Fatalf("non-zero exit %d\n\n%s", code, output.Stderr()) + } + }) +} + // we should be able to apply a plan file with no other file dependencies func TestApply_planNoModuleFiles(t *testing.T) { // temporary data directory which we can remove between commands @@ -1112,7 +1657,7 @@ func TestApply_shutdown(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "ami": {Type: cty.String, Optional: true}, }, @@ -1324,7 +1869,7 @@ func TestApply_vars(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -1386,7 +1931,7 @@ func TestApply_varFile(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -1429,7 +1974,7 @@ func TestApply_varFileDefault(t *testing.T) { defer testChdir(t, td)() varFilePath := filepath.Join(td, "terraform.tfvars") - if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { + if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil { t.Fatalf("err: %s", err) } @@ -1448,7 +1993,7 @@ func TestApply_varFileDefault(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -1509,7 +2054,7 @@ func TestApply_varFileDefaultJSON(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -1611,7 +2156,7 @@ func TestApply_backup(t *testing.T) { actual := backupState.RootModule().Resources["test_instance.foo"] expected := originalState.RootModule().Resources["test_instance.foo"] - if !cmp.Equal(actual, expected, cmpopts.EquateEmpty()) { + if !cmp.Equal(actual, expected, cmpopts.EquateEmpty(), cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{})) { t.Fatalf( "wrong aws_instance.foo state\n%s", cmp.Diff(expected, actual, cmp.Transformer("bytesAsString", func(b []byte) string { @@ -1808,7 +2353,7 @@ func TestApply_targeted(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, @@ -1915,7 +2460,7 @@ func TestApply_replace(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, @@ -2138,7 +2683,7 @@ func applyFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -2154,7 +2699,7 @@ func applyFixtureSchema() *providers.GetProviderSchemaResponse { // GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated, // with the plan/apply steps just passing through the data determined by // Terraform Core. -func applyFixtureProvider() *terraform.MockProvider { +func applyFixtureProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = applyFixtureSchema() p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { @@ -2195,7 +2740,7 @@ func applyFixturePlanFileMatchState(t *testing.T, stateMeta statemgr.SnapshotMet t.Fatal(err) } plan := testPlan(t) - plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", @@ -2220,6 +2765,53 @@ func applyFixturePlanFileMatchState(t *testing.T, stateMeta statemgr.SnapshotMet ) } +// applyFixturePlanFileWithVariableValue creates a plan file at a temporary location containing +// a single change to create the test_instance.foo and a variable value that is included in the +// "apply" test fixture, returning the location of that plan file. +func applyFixturePlanFileWithVariableValue(t *testing.T, value string) string { + _, snap := testModuleWithSnapshot(t, "apply-vars") + plannedVal := cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("bar"), + }) + priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type()) + if err != nil { + t.Fatal(err) + } + plannedValRaw, err := plans.NewDynamicValue(plannedVal, plannedVal.Type()) + if err != nil { + t.Fatal(err) + } + plan := testPlan(t) + plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: priorValRaw, + After: plannedValRaw, + }, + }) + + plan.VariableValues = map[string]plans.DynamicValue{ + "foo": mustNewDynamicValue(value, cty.DynamicPseudoType), + } + return testPlanFileMatchState( + t, + snap, + states.NewState(), + plan, + statemgr.SnapshotMeta{}, + ) +} + const applyVarFile = ` foo = "bar" ` @@ -2227,3 +2819,12 @@ foo = "bar" const applyVarFileJSON = ` { "foo": "bar" } ` + +func mustNewDynamicValue(val string, ty cty.Type) plans.DynamicValue { + realVal := cty.StringVal(val) + ret, err := plans.NewDynamicValue(realVal, ty) + if err != nil { + panic(err) + } + return ret +} diff --git a/internal/command/arguments/apply.go b/internal/command/arguments/apply.go index 4d2e676055..2965f06736 100644 --- a/internal/command/arguments/apply.go +++ b/internal/command/arguments/apply.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -54,6 +57,14 @@ func ParseApply(args []string) (*Apply, tfdiags.Diagnostics) { )) } + if apply.State.StatePath != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Deprecated flag: -state", + `Use the "path" attribute within the "local" backend to specify a file for state storage`, + )) + } + args = cmdFlags.Args() if len(args) > 0 { apply.PlanPath = args[0] diff --git a/internal/command/arguments/apply_test.go b/internal/command/arguments/apply_test.go index 8038833800..80483d62ce 100644 --- a/internal/command/arguments/apply_test.go +++ b/internal/command/arguments/apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -86,7 +89,7 @@ func TestParseApply_basicValid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseApply(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { @@ -120,7 +123,7 @@ func TestParseApply_json(t *testing.T) { got, diags := ParseApply(tc.args) if tc.wantSuccess { - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Errorf("unexpected diags: %v", diags) } } else { @@ -197,7 +200,7 @@ func TestParseApply_targets(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseApply(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { if tc.wantErr == "" { t.Fatalf("unexpected diags: %v", diags) } else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) { @@ -256,7 +259,7 @@ func TestParseApply_replace(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseApply(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { if tc.wantErr == "" { t.Fatalf("unexpected diags: %v", diags) } else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) { @@ -308,7 +311,7 @@ func TestParseApply_vars(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseApply(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { @@ -363,7 +366,7 @@ func TestParseApplyDestroy_basicValid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseApplyDestroy(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { diff --git a/internal/command/arguments/default.go b/internal/command/arguments/default.go index 4b7bb40242..9a08504d7d 100644 --- a/internal/command/arguments/default.go +++ b/internal/command/arguments/default.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( diff --git a/internal/command/arguments/extended.go b/internal/command/arguments/extended.go index e698182db9..3b3c7cc759 100644 --- a/internal/command/arguments/extended.go +++ b/internal/command/arguments/extended.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -75,6 +78,18 @@ type Operation struct { // learn a use-case for broader matching. ForceReplace []addrs.AbsResourceInstance + // DeferralAllowed enables experimental support for automatically performing + // a partial plan if some objects are not yet plannable (due to unknown + // values in count/for_each, or due to other missing dependencies that can't + // be resolved in a single plan/apply cycle). + // + // IMPORTANT: This feature should only be available when Terraform is built + // with experimental features enabled. Since extendedFlagSet can't currently + // test whether experimental features are enabled, the check needs to happen + // when _reading_ these Operation arguments and transferring values to the + // backendrun.Operation struct. + DeferralAllowed bool + // These private fields are used only temporarily during decoding. Use // method Parse to populate the exported fields from these, validating // the raw values in the process. @@ -177,13 +192,13 @@ func (o *Operation) Parse() tfdiags.Diagnostics { } // Vars describes arguments which specify non-default variable values. This -// interfce is unfortunately obscure, because the order of the CLI arguments +// interface is unfortunately obscure, because the order of the CLI arguments // determines the final value of the gathered variables. In future it might be // desirable for the arguments package to handle the gathering of variables // directly, returning a map of variable values. type Vars struct { - vars *flagNameValueSlice - varFiles *flagNameValueSlice + vars *FlagNameValueSlice + varFiles *FlagNameValueSlice } func (v *Vars) All() []FlagNameValue { @@ -220,17 +235,18 @@ func extendedFlagSet(name string, state *State, operation *Operation, vars *Vars if operation != nil { f.IntVar(&operation.Parallelism, "parallelism", DefaultParallelism, "parallelism") + f.BoolVar(&operation.DeferralAllowed, "allow-deferral", false, "allow-deferral") f.BoolVar(&operation.Refresh, "refresh", true, "refresh") f.BoolVar(&operation.destroyRaw, "destroy", false, "destroy") f.BoolVar(&operation.refreshOnlyRaw, "refresh-only", false, "refresh-only") - f.Var((*flagStringSlice)(&operation.targetsRaw), "target", "target") - f.Var((*flagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") + f.Var((*FlagStringSlice)(&operation.targetsRaw), "target", "target") + f.Var((*FlagStringSlice)(&operation.forceReplaceRaw), "replace", "replace") } // Gather all -var and -var-file arguments into one heterogenous structure // to preserve the overall order. if vars != nil { - varsFlags := newFlagNameValueSlice("-var") + varsFlags := NewFlagNameValueSlice("-var") varFilesFlags := varsFlags.Alias("-var-file") vars.vars = &varsFlags vars.varFiles = &varFilesFlags diff --git a/internal/command/arguments/flags.go b/internal/command/arguments/flags.go index ae47bc6e04..64bf18ddd9 100644 --- a/internal/command/arguments/flags.go +++ b/internal/command/arguments/flags.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -5,72 +8,68 @@ import ( "fmt" ) -// flagStringSlice is a flag.Value implementation which allows collecting +// FlagStringSlice is a flag.Value implementation which allows collecting // multiple instances of a single flag into a slice. This is used for flags // such as -target=aws_instance.foo and -var x=y. -type flagStringSlice []string +type FlagStringSlice []string -var _ flag.Value = (*flagStringSlice)(nil) +var _ flag.Value = (*FlagStringSlice)(nil) -func (v *flagStringSlice) String() string { +func (v *FlagStringSlice) String() string { return "" } -func (v *flagStringSlice) Set(raw string) error { +func (v *FlagStringSlice) Set(raw string) error { *v = append(*v, raw) return nil } -// flagNameValueSlice is a flag.Value implementation that appends raw flag +// FlagNameValueSlice is a flag.Value implementation that appends raw flag // names and values to a slice. This is used to collect a sequence of flags // with possibly different names, preserving the overall order. -// -// FIXME: this is a copy of rawFlags from command/meta_config.go, with the -// eventual aim of replacing it altogether by gathering variables in the -// arguments package. -type flagNameValueSlice struct { - flagName string - items *[]FlagNameValue +type FlagNameValueSlice struct { + FlagName string + Items *[]FlagNameValue } -var _ flag.Value = flagNameValueSlice{} +var _ flag.Value = FlagNameValueSlice{} -func newFlagNameValueSlice(flagName string) flagNameValueSlice { +func NewFlagNameValueSlice(flagName string) FlagNameValueSlice { var items []FlagNameValue - return flagNameValueSlice{ - flagName: flagName, - items: &items, + return FlagNameValueSlice{ + FlagName: flagName, + Items: &items, } } -func (f flagNameValueSlice) Empty() bool { - if f.items == nil { +func (f FlagNameValueSlice) Empty() bool { + if f.Items == nil { return true } - return len(*f.items) == 0 + return len(*f.Items) == 0 } -func (f flagNameValueSlice) AllItems() []FlagNameValue { - if f.items == nil { +func (f FlagNameValueSlice) AllItems() []FlagNameValue { + if f.Items == nil { return nil } - return *f.items + return *f.Items } -func (f flagNameValueSlice) Alias(flagName string) flagNameValueSlice { - return flagNameValueSlice{ - flagName: flagName, - items: f.items, +func (f FlagNameValueSlice) Alias(flagName string) FlagNameValueSlice { + return FlagNameValueSlice{ + FlagName: flagName, + Items: f.Items, } } -func (f flagNameValueSlice) String() string { +func (f FlagNameValueSlice) String() string { return "" } -func (f flagNameValueSlice) Set(str string) error { - *f.items = append(*f.items, FlagNameValue{ - Name: f.flagName, +func (f FlagNameValueSlice) Set(str string) error { + *f.Items = append(*f.Items, FlagNameValue{ + Name: f.FlagName, Value: str, }) return nil diff --git a/internal/command/arguments/init.go b/internal/command/arguments/init.go new file mode 100644 index 0000000000..17505a9cbc --- /dev/null +++ b/internal/command/arguments/init.go @@ -0,0 +1,159 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Init represents the command-line arguments for the init command. +type Init struct { + // FromModule identifies the module to copy into the target directory before init. + FromModule string + + // Lockfile specifies a dependency lockfile mode. + Lockfile string + + // TestDirectory is the directory containing any test files that should be + // validated alongside the main configuration. Should be relative to the + // Path. + TestsDirectory string + + // ViewType specifies which init format to use: human or JSON. + ViewType ViewType + + // Backend specifies whether to disable backend or HCP Terraform initialization. + Backend bool + + // Cloud specifies whether to disable backend or HCP Terraform initialization. + Cloud bool + + // Get specifies whether to disable downloading modules for this configuration + Get bool + + // ForceInitCopy specifies whether to suppress prompts about copying state data. + ForceInitCopy bool + + // StateLock specifies whether hold a state lock during backend migration. + StateLock bool + + // StateLockTimeout specifies the duration to wait for a state lock. + StateLockTimeout time.Duration + + // Reconfigure specifies whether to disregard any existing configuration, preventing migration of any existing state + Reconfigure bool + + // MigrateState specifies whether to attempt to copy existing state to the new backend + MigrateState bool + + // Upgrade specifies whether to upgrade modules and plugins as part of their respective installation steps + Upgrade bool + + // Json specifies whether to output in JSON format + Json bool + + // IgnoreRemoteVersion specifies whether to ignore remote and local Terraform versions compatibility + IgnoreRemoteVersion bool + + BackendConfig FlagNameValueSlice + + Vars *Vars + + // InputEnabled is used to disable interactive input for unspecified + // variable and backend config values. Default is true. + InputEnabled bool + + TargetFlags []string + + CompactWarnings bool + + PluginPath FlagStringSlice + + Args []string +} + +// ParseInit processes CLI arguments, returning an Init value and errors. +// If errors are encountered, an Init value is still returned representing +// the best effort interpretation of the arguments. +func ParseInit(args []string) (*Init, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + init := &Init{ + Vars: &Vars{}, + } + init.BackendConfig = NewFlagNameValueSlice("-backend-config") + + cmdFlags := extendedFlagSet("init", nil, nil, init.Vars) + + cmdFlags.Var((*FlagStringSlice)(&init.TargetFlags), "target", "resource to target") + cmdFlags.BoolVar(&init.InputEnabled, "input", true, "input") + cmdFlags.BoolVar(&init.CompactWarnings, "compact-warnings", false, "use compact warnings") + cmdFlags.BoolVar(&init.Backend, "backend", true, "") + cmdFlags.BoolVar(&init.Cloud, "cloud", true, "") + cmdFlags.StringVar(&init.FromModule, "from-module", "", "copy the source of the given module into the directory before init") + cmdFlags.BoolVar(&init.Get, "get", true, "") + cmdFlags.BoolVar(&init.ForceInitCopy, "force-copy", false, "suppress prompts about copying state data") + cmdFlags.BoolVar(&init.StateLock, "lock", true, "lock state") + cmdFlags.DurationVar(&init.StateLockTimeout, "lock-timeout", 0, "lock timeout") + cmdFlags.BoolVar(&init.Reconfigure, "reconfigure", false, "reconfigure") + cmdFlags.BoolVar(&init.MigrateState, "migrate-state", false, "migrate state") + cmdFlags.BoolVar(&init.Upgrade, "upgrade", false, "") + cmdFlags.StringVar(&init.Lockfile, "lockfile", "", "Set a dependency lockfile mode") + cmdFlags.BoolVar(&init.IgnoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") + cmdFlags.StringVar(&init.TestsDirectory, "test-directory", "tests", "test-directory") + cmdFlags.BoolVar(&init.Json, "json", false, "json") + cmdFlags.Var(&init.BackendConfig, "backend-config", "") + cmdFlags.Var(&init.PluginPath, "plugin-dir", "plugin directory") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + if init.MigrateState && init.Json { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "The -migrate-state and -json options are mutually-exclusive", + "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + )) + } + + if init.MigrateState && init.Reconfigure { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -migrate-state and -reconfigure options are mutually-exclusive.", + )) + } + + init.Args = cmdFlags.Args() + + backendFlagSet := FlagIsSet(cmdFlags, "backend") + cloudFlagSet := FlagIsSet(cmdFlags, "cloud") + + if backendFlagSet && cloudFlagSet { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid init options", + "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + )) + } else if backendFlagSet { + init.Cloud = init.Backend + } else if cloudFlagSet { + init.Backend = init.Cloud + } + + switch { + case init.Json: + init.ViewType = ViewJSON + default: + init.ViewType = ViewHuman + } + + return init, diags +} diff --git a/internal/command/arguments/init_test.go b/internal/command/arguments/init_test.go new file mode 100644 index 0000000000..93e13b7b62 --- /dev/null +++ b/internal/command/arguments/init_test.go @@ -0,0 +1,222 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestParseInit_basicValid(t *testing.T) { + var flagNameValue []FlagNameValue + testCases := map[string]struct { + args []string + want *Init + }{ + "with default options": { + nil, + &Init{ + FromModule: "", + Lockfile: "", + TestsDirectory: "tests", + ViewType: ViewHuman, + Backend: true, + Cloud: true, + Get: true, + ForceInitCopy: false, + StateLock: true, + StateLockTimeout: 0, + Reconfigure: false, + MigrateState: false, + Upgrade: false, + Json: false, + IgnoreRemoteVersion: false, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &flagNameValue, + }, + Vars: &Vars{}, + InputEnabled: true, + CompactWarnings: false, + TargetFlags: nil, + }, + }, + "setting multiple options": { + []string{"-backend=false", "-force-copy=true", + "-from-module=./main-dir", "-json", "-get=false", + "-lock=false", "-lock-timeout=10s", "-reconfigure=true", + "-upgrade=true", "-lockfile=readonly", "-compact-warnings=true", + "-ignore-remote-version=true", "-test-directory=./test-dir"}, + &Init{ + FromModule: "./main-dir", + Lockfile: "readonly", + TestsDirectory: "./test-dir", + ViewType: ViewJSON, + Backend: false, + Cloud: false, + Get: false, + ForceInitCopy: true, + StateLock: false, + StateLockTimeout: time.Duration(10) * time.Second, + Reconfigure: true, + MigrateState: false, + Upgrade: true, + Json: true, + IgnoreRemoteVersion: true, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &flagNameValue, + }, + Vars: &Vars{}, + InputEnabled: true, + Args: []string{}, + CompactWarnings: true, + TargetFlags: nil, + }, + }, + "with cloud option": { + []string{"-cloud=false", "-input=false", "-target=foo_bar.baz", "-backend-config", "backend.config"}, + &Init{ + FromModule: "", + Lockfile: "", + TestsDirectory: "tests", + ViewType: ViewHuman, + Backend: false, + Cloud: false, + Get: true, + ForceInitCopy: false, + StateLock: true, + StateLockTimeout: 0, + Reconfigure: false, + MigrateState: false, + Upgrade: false, + Json: false, + IgnoreRemoteVersion: false, + BackendConfig: FlagNameValueSlice{ + FlagName: "-backend-config", + Items: &[]FlagNameValue{{Name: "-backend-config", Value: "backend.config"}}, + }, + Vars: &Vars{}, + InputEnabled: false, + Args: []string{}, + CompactWarnings: false, + TargetFlags: []string{"foo_bar.baz"}, + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Vars{}) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + + if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { + t.Errorf("unexpected result\n%s", diff) + } + }) + } +} + +func TestParseInit_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + wantErr string + wantViewType ViewType + }{ + "with unsupported options": { + args: []string{"-raw"}, + wantErr: "flag provided but not defined", + wantViewType: ViewHuman, + }, + "with both -backend and -cloud options set": { + args: []string{"-backend=false", "-cloud=false"}, + wantErr: "The -backend and -cloud options are aliases of one another and mutually-exclusive in their use", + wantViewType: ViewHuman, + }, + "with both -migrate-state and -json options set": { + args: []string{"-migrate-state", "-json"}, + wantErr: "Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option.", + wantViewType: ViewJSON, + }, + "with both -migrate-state and -reconfigure options set": { + args: []string{"-migrate-state", "-reconfigure"}, + wantErr: "The -migrate-state and -reconfigure options are mutually-exclusive.", + wantViewType: ViewHuman, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) == 0 { + t.Fatal("expected diags but got none") + } + if got, want := diags.Err().Error(), tc.wantErr; !strings.Contains(got, want) { + t.Fatalf("wrong diags\n got: %s\nwant: %s", got, want) + } + if got.ViewType != tc.wantViewType { + t.Fatalf("wrong view type, got %#v, want %#v", got.ViewType, ViewHuman) + } + }) + } +} + +func TestParseInit_vars(t *testing.T) { + testCases := map[string]struct { + args []string + want []FlagNameValue + }{ + "no var flags by default": { + args: nil, + want: nil, + }, + "one var": { + args: []string{"-var", "foo=bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + }, + }, + "one var-file": { + args: []string{"-var-file", "cool.tfvars"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, + }, + }, + "ordering preserved": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseInit(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want)) + } + if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want { + t.Fatalf("expected Empty() to return %t, but was %t", want, got) + } + }) + } +} diff --git a/internal/command/arguments/modules.go b/internal/command/arguments/modules.go new file mode 100644 index 0000000000..fb625c9404 --- /dev/null +++ b/internal/command/arguments/modules.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// Modules represents the command-line arguments for the modules command +type Modules struct { + // ViewType specifies which output format to use: human, JSON, or "raw" + ViewType ViewType +} + +// ParseModules processes CLI arguments, returning a Modules value and error +// diagnostics. If there are any diagnostics present, a Modules value is still +// returned representing the best effort interpretation of the arguments. +func ParseModules(args []string) (*Modules, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var jsonOutput bool + + modules := &Modules{} + cmdFlags := defaultFlagSet("modules") + cmdFlags.BoolVar(&jsonOutput, "json", false, "json") + + if err := cmdFlags.Parse(args); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + err.Error(), + )) + } + + args = cmdFlags.Args() + if len(args) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "Expected no positional arguments", + )) + } + + switch { + case jsonOutput: + modules.ViewType = ViewJSON + default: + modules.ViewType = ViewHuman + } + + return modules, diags +} diff --git a/internal/command/arguments/modules_test.go b/internal/command/arguments/modules_test.go new file mode 100644 index 0000000000..aad6fd0b6c --- /dev/null +++ b/internal/command/arguments/modules_test.go @@ -0,0 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package arguments + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseModules_valid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Modules + }{ + "default": { + nil, + &Modules{ + ViewType: ViewHuman, + }, + }, + "json": { + []string{"-json"}, + &Modules{ + ViewType: ViewJSON, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, diags := ParseModules(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) + } + if *got != *tc.want { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + }) + } +} + +func TestParseModules_invalid(t *testing.T) { + testCases := map[string]struct { + args []string + want *Modules + wantDiags tfdiags.Diagnostics + }{ + "invalid flag": { + []string{"-sauron"}, + &Modules{ + ViewType: ViewHuman, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -sauron", + ), + }, + }, + "too many arguments": { + []string{"-json", "frodo"}, + &Modules{ + ViewType: ViewJSON, + }, + tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Too many command line arguments", + "Expected no positional arguments", + ), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got, gotDiags := ParseModules(tc.args) + if *got != *tc.want { + t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/output.go b/internal/command/arguments/output.go index debf05dd83..aaebebf997 100644 --- a/internal/command/arguments/output.go +++ b/internal/command/arguments/output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( diff --git a/internal/command/arguments/output_test.go b/internal/command/arguments/output_test.go index cb25808137..9a94e86602 100644 --- a/internal/command/arguments/output_test.go +++ b/internal/command/arguments/output_test.go @@ -1,10 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( - "reflect" "testing" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -134,9 +135,7 @@ func TestParseOutput_invalid(t *testing.T) { if *got != *tc.want { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } - if !reflect.DeepEqual(gotDiags, tc.wantDiags) { - t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags)) - } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) }) } } diff --git a/internal/command/arguments/plan.go b/internal/command/arguments/plan.go index 2300dc7a5d..d4d2a746a4 100644 --- a/internal/command/arguments/plan.go +++ b/internal/command/arguments/plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -22,6 +25,11 @@ type Plan struct { // OutPath contains an optional path to store the plan file OutPath string + // GenerateConfigPath tells Terraform that config should be generated for + // unmatched import target paths and which path the generated file should + // be written to. + GenerateConfigPath string + // ViewType specifies which output format to use ViewType ViewType } @@ -41,6 +49,7 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) { cmdFlags.BoolVar(&plan.DetailedExitCode, "detailed-exitcode", false, "detailed-exitcode") cmdFlags.BoolVar(&plan.InputEnabled, "input", true, "input") cmdFlags.StringVar(&plan.OutPath, "out", "", "out") + cmdFlags.StringVar(&plan.GenerateConfigPath, "generate-config-out", "", "generate-config-out") var json bool cmdFlags.BoolVar(&json, "json", false, "json") @@ -53,6 +62,14 @@ func ParsePlan(args []string) (*Plan, tfdiags.Diagnostics) { )) } + if plan.State.StatePath != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Deprecated flag: -state", + `Use the "path" attribute within the "local" backend to specify a file for state storage`, + )) + } + args = cmdFlags.Args() if len(args) > 0 { diff --git a/internal/command/arguments/plan_test.go b/internal/command/arguments/plan_test.go index b547d3f7ab..e7fe6f401f 100644 --- a/internal/command/arguments/plan_test.go +++ b/internal/command/arguments/plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -70,7 +73,7 @@ func TestParsePlan_basicValid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParsePlan(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } if diff := cmp.Diff(tc.want, got, cmpOpts); diff != "" { @@ -141,7 +144,7 @@ func TestParsePlan_targets(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParsePlan(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { if tc.wantErr == "" { t.Fatalf("unexpected diags: %v", diags) } else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) { @@ -193,7 +196,7 @@ func TestParsePlan_vars(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParsePlan(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { diff --git a/internal/command/arguments/refresh.go b/internal/command/arguments/refresh.go index bc08d9df4d..1bc0c6fb15 100644 --- a/internal/command/arguments/refresh.go +++ b/internal/command/arguments/refresh.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -44,6 +47,14 @@ func ParseRefresh(args []string) (*Refresh, tfdiags.Diagnostics) { )) } + if refresh.State.StatePath != "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Deprecated flag: -state", + `Use the "path" attribute within the "local" backend to specify a file for state storage`, + )) + } + args = cmdFlags.Args() if len(args) > 0 { diags = diags.Append(tfdiags.Sourceless( diff --git a/internal/command/arguments/refresh_test.go b/internal/command/arguments/refresh_test.go index 3f35053f7b..32cbb5af7c 100644 --- a/internal/command/arguments/refresh_test.go +++ b/internal/command/arguments/refresh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -39,7 +42,7 @@ func TestParseRefresh_basicValid(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseRefresh(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } // Ignore the extended arguments for simplicity @@ -114,7 +117,7 @@ func TestParseRefresh_targets(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseRefresh(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { if tc.wantErr == "" { t.Fatalf("unexpected diags: %v", diags) } else if got := diags.Err().Error(); !strings.Contains(got, tc.wantErr) { @@ -166,7 +169,7 @@ func TestParseRefresh_vars(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { got, diags := ParseRefresh(tc.args) - if len(diags) > 0 { + if len(diags) > 0 && diags.HasErrors() { t.Fatalf("unexpected diags: %v", diags) } if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { diff --git a/internal/command/arguments/show.go b/internal/command/arguments/show.go index 4d95fc1daa..cf9bfd8106 100644 --- a/internal/command/arguments/show.go +++ b/internal/command/arguments/show.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( diff --git a/internal/command/arguments/show_test.go b/internal/command/arguments/show_test.go index 5088e1a94e..922f430824 100644 --- a/internal/command/arguments/show_test.go +++ b/internal/command/arguments/show_test.go @@ -1,10 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( - "reflect" "testing" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -91,9 +92,7 @@ func TestParseShow_invalid(t *testing.T) { if *got != *tc.want { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } - if !reflect.DeepEqual(gotDiags, tc.wantDiags) { - t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags)) - } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) }) } } diff --git a/internal/command/arguments/test.go b/internal/command/arguments/test.go index 8ffbd4914d..3a11394489 100644 --- a/internal/command/arguments/test.go +++ b/internal/command/arguments/test.go @@ -1,63 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( - "flag" - "io/ioutil" - + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/tfdiags" ) -// Test represents the command line arguments for the "terraform test" command. +// Test represents the command-line arguments for the test command. type Test struct { - Output TestOutput -} + // CloudRunSource specifies the remote private module that this test run + // should execute against in a remote HCP Terraform run. + CloudRunSource string -// TestOutput represents a subset of the arguments for "terraform test" -// related to how it presents its results. That is, it's the arguments that -// are relevant to the command's view rather than its controller. -type TestOutput struct { - // If not an empty string, JUnitXMLFile gives a filename where JUnit-style - // XML test result output should be written, in addition to the normal - // output printed to the standard output and error streams. - // (The typical usage pattern for tools that can consume this file format - // is to configure them to look for a separate test result file on disk - // after running the tests.) + // Filter contains a list of test files to execute. If empty, all test files + // will be executed. + Filter []string + + // OperationParallelism is the limit Terraform places on total parallel operations + // during the plan or apply command within a single test run. + OperationParallelism int + + // TestDirectory allows the user to override the directory that the test + // command will use to discover test files, defaults to "tests". Regardless + // of the value here, test files within the configuration directory will + // always be discovered. + TestDirectory string + + // ViewType specifies which output format to use: human or JSON. + ViewType ViewType + + // JUnitXMLFile specifies an optional filename to write a JUnit XML test + // result report to, in addition to the information written to the selected + // view type. JUnitXMLFile string + + // You can specify common variables for all tests from the command line. + Vars *Vars + + // Verbose tells the test command to print out the plan either in + // human-readable format or JSON for each run step depending on the + // ViewType. + Verbose bool } -// ParseTest interprets a slice of raw command line arguments into a -// Test value. -func ParseTest(args []string) (Test, tfdiags.Diagnostics) { - var ret Test +func ParseTest(args []string) (*Test, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - // NOTE: ParseTest should still return at least a partial - // Test even on error, containing enough information for the - // command to report error diagnostics in a suitable way. - - f := flag.NewFlagSet("test", flag.ContinueOnError) - f.SetOutput(ioutil.Discard) - f.Usage = func() {} - f.StringVar(&ret.Output.JUnitXMLFile, "junit-xml", "", "Write a JUnit XML file describing the results") - - err := f.Parse(args) - if err != nil { - diags = diags.Append(err) - return ret, diags + test := Test{ + Vars: new(Vars), } - // We'll now discard all of the arguments that the flag package handled, - // and focus only on the positional arguments for the rest of the function. - args = f.Args() + var jsonOutput bool + cmdFlags := extendedFlagSet("test", nil, nil, test.Vars) + cmdFlags.Var((*FlagStringSlice)(&test.Filter), "filter", "filter") + cmdFlags.StringVar(&test.TestDirectory, "test-directory", configs.DefaultTestDirectory, "test-directory") + cmdFlags.BoolVar(&jsonOutput, "json", false, "json") + cmdFlags.StringVar(&test.JUnitXMLFile, "junit-xml", "", "junit-xml") + cmdFlags.BoolVar(&test.Verbose, "verbose", false, "verbose") + cmdFlags.IntVar(&test.OperationParallelism, "parallelism", DefaultParallelism, "parallelism") - if len(args) != 0 { + // TODO: Finalise the name of this flag. + cmdFlags.StringVar(&test.CloudRunSource, "cloud-run", "", "cloud-run") + + if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Invalid command arguments", - "The test command doesn't expect any positional command-line arguments.", - )) - return ret, diags + "Failed to parse command-line flags", + err.Error())) } - return ret, diags + if len(test.JUnitXMLFile) > 0 && len(test.CloudRunSource) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Incompatible command-line flags", + "The -junit-xml option is currently not compatible with remote test execution via the -cloud-run flag. If you are interested in JUnit XML output for remotely-executed tests please open an issue in GitHub.")) + } + + // Only set the default parallelism if this is not a cloud-run test. + // A cloud-run test will eventually run its own local test, and if the + // user still hasn't set the parallelism, that run will use the default. + if test.OperationParallelism < 1 && len(test.CloudRunSource) == 0 { + test.OperationParallelism = DefaultParallelism + } + + switch { + case jsonOutput: + test.ViewType = ViewJSON + default: + test.ViewType = ViewHuman + } + + return &test, diags } diff --git a/internal/command/arguments/test_test.go b/internal/command/arguments/test_test.go index 9a1c7fed01..dc3849ff93 100644 --- a/internal/command/arguments/test_test.go +++ b/internal/command/arguments/test_test.go @@ -1,83 +1,212 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( "testing" - "github.com/apparentlymart/go-shquot/shquot" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform/internal/tfdiags" ) -func TestParseTest(t *testing.T) { - tests := []struct { - Input []string - Want Test - WantError string +func TestParseTest_Vars(t *testing.T) { + tcs := map[string]struct { + args []string + want []FlagNameValue }{ - { - nil, - Test{ - Output: TestOutput{ - JUnitXMLFile: "", - }, - }, - ``, + "no var flags by default": { + args: nil, + want: nil, }, - { - []string{"-invalid"}, - Test{ - Output: TestOutput{ - JUnitXMLFile: "", - }, + "one var": { + args: []string{"-var", "foo=bar"}, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, }, - `flag provided but not defined: -invalid`, }, - { - []string{"-junit-xml=result.xml"}, - Test{ - Output: TestOutput{ - JUnitXMLFile: "result.xml", - }, + "one var-file": { + args: []string{"-var-file", "cool.tfvars"}, + want: []FlagNameValue{ + {Name: "-var-file", Value: "cool.tfvars"}, }, - ``, }, - { - []string{"baz"}, - Test{ - Output: TestOutput{ - JUnitXMLFile: "", - }, + "ordering preserved": { + args: []string{ + "-var", "foo=bar", + "-var-file", "cool.tfvars", + "-var", "boop=beep", + }, + want: []FlagNameValue{ + {Name: "-var", Value: "foo=bar"}, + {Name: "-var-file", Value: "cool.tfvars"}, + {Name: "-var", Value: "boop=beep"}, }, - `Invalid command arguments`, }, } - baseCmdline := []string{"terraform", "test"} - for _, test := range tests { - name := shquot.POSIXShell(append(baseCmdline, test.Input...)) + for name, tc := range tcs { t.Run(name, func(t *testing.T) { - t.Log(name) - got, diags := ParseTest(test.Input) - - if test.WantError != "" { - if len(diags) != 1 { - t.Fatalf("got %d diagnostics; want exactly 1\n%s", len(diags), diags.Err().Error()) - } - if diags[0].Severity() != tfdiags.Error { - t.Fatalf("got a warning; want an error\n%s", diags.Err().Error()) - } - if desc := diags[0].Description(); desc.Summary != test.WantError { - t.Fatalf("wrong error\ngot: %s\nwant: %s", desc.Summary, test.WantError) - } - } else { - if len(diags) != 0 { - t.Fatalf("got %d diagnostics; want none\n%s", len(diags), diags.Err().Error()) - } + got, diags := ParseTest(tc.args) + if len(diags) > 0 { + t.Fatalf("unexpected diags: %v", diags) } - - if diff := cmp.Diff(test.Want, got); diff != "" { - t.Errorf("wrong result\n%s", diff) + if vars := got.Vars.All(); !cmp.Equal(vars, tc.want) { + t.Fatalf("unexpected result\n%s", cmp.Diff(vars, tc.want)) + } + if got, want := got.Vars.Empty(), len(tc.want) == 0; got != want { + t.Fatalf("expected Empty() to return %t, but was %t", want, got) } }) } } + +func TestParseTest(t *testing.T) { + tcs := map[string]struct { + args []string + want *Test + wantDiags tfdiags.Diagnostics + }{ + "defaults": { + args: nil, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: nil, + }, + "with-filters": { + args: []string{"-filter=one.tftest.hcl", "-filter=two.tftest.hcl"}, + want: &Test{ + Filter: []string{"one.tftest.hcl", "two.tftest.hcl"}, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: nil, + }, + "json": { + args: []string{"-json"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewJSON, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: nil, + }, + "test-directory": { + args: []string{"-test-directory=other"}, + want: &Test{ + Filter: nil, + TestDirectory: "other", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: nil, + }, + "verbose": { + args: []string{"-verbose"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Verbose: true, + Vars: &Vars{}, + OperationParallelism: 10, + }, + }, + "with-parallelism-set": { + args: []string{"-parallelism=5"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 5, + }, + wantDiags: nil, + }, + "with-parallelism-0": { + args: []string{"-parallelism=0"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: nil, + }, + "cloud-with-parallelism-0": { + args: []string{"-parallelism=0", "-cloud-run=foobar"}, + want: &Test{ + CloudRunSource: "foobar", + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 0, + }, + wantDiags: nil, + }, + "unknown flag": { + args: []string{"-boop"}, + want: &Test{ + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "flag provided but not defined: -boop", + ), + }, + }, + "incompatible flags: -junit-xml and -cloud-run": { + args: []string{"-junit-xml=./output.xml", "-cloud-run=foobar"}, + want: &Test{ + CloudRunSource: "foobar", + JUnitXMLFile: "./output.xml", + Filter: nil, + TestDirectory: "tests", + ViewType: ViewHuman, + Vars: &Vars{}, + OperationParallelism: 10, + }, + wantDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Incompatible command-line flags", + "The -junit-xml option is currently not compatible with remote test execution via the -cloud-run flag. If you are interested in JUnit XML output for remotely-executed tests please open an issue in GitHub.", + ), + }, + }, + } + + cmpOpts := cmpopts.IgnoreUnexported(Operation{}, Vars{}, State{}) + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + got, diags := ParseTest(tc.args) + + if diff := cmp.Diff(tc.want, got, cmpOpts); len(diff) > 0 { + t.Errorf("diff:\n%s", diff) + } + + tfdiags.AssertDiagnosticsMatch(t, diags, tc.wantDiags) + }) + } +} diff --git a/internal/command/arguments/types.go b/internal/command/arguments/types.go index ff529361e0..4e9065359b 100644 --- a/internal/command/arguments/types.go +++ b/internal/command/arguments/types.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments // ViewType represents which view layer to use for a given command. Not all diff --git a/internal/command/arguments/validate.go b/internal/command/arguments/validate.go index daadd7ed53..7df4b2c5d9 100644 --- a/internal/command/arguments/validate.go +++ b/internal/command/arguments/validate.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( @@ -10,6 +13,15 @@ type Validate struct { // unspecified, validate will use the current directory. Path string + // TestDirectory is the directory containing any test files that should be + // validated alongside the main configuration. Should be relative to the + // Path. + TestDirectory string + + // NoTests indicates that Terraform should not validate any test files + // included with the module. + NoTests bool + // ViewType specifies which output format to use: human, JSON, or "raw". ViewType ViewType } @@ -26,6 +38,8 @@ func ParseValidate(args []string) (*Validate, tfdiags.Diagnostics) { var jsonOutput bool cmdFlags := defaultFlagSet("validate") cmdFlags.BoolVar(&jsonOutput, "json", false, "json") + cmdFlags.StringVar(&validate.TestDirectory, "test-directory", "tests", "test-directory") + cmdFlags.BoolVar(&validate.NoTests, "no-tests", false, "no-tests") if err := cmdFlags.Parse(args); err != nil { diags = diags.Append(tfdiags.Sourceless( diff --git a/internal/command/arguments/validate_test.go b/internal/command/arguments/validate_test.go index e744055507..094b61ee94 100644 --- a/internal/command/arguments/validate_test.go +++ b/internal/command/arguments/validate_test.go @@ -1,10 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( - "reflect" "testing" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -16,22 +17,42 @@ func TestParseValidate_valid(t *testing.T) { "defaults": { nil, &Validate{ - Path: ".", - ViewType: ViewHuman, + Path: ".", + TestDirectory: "tests", + ViewType: ViewHuman, }, }, "json": { []string{"-json"}, &Validate{ - Path: ".", - ViewType: ViewJSON, + Path: ".", + TestDirectory: "tests", + ViewType: ViewJSON, }, }, "path": { []string{"-json", "foo"}, &Validate{ - Path: "foo", - ViewType: ViewJSON, + Path: "foo", + TestDirectory: "tests", + ViewType: ViewJSON, + }, + }, + "test-directory": { + []string{"-test-directory", "other"}, + &Validate{ + Path: ".", + TestDirectory: "other", + ViewType: ViewHuman, + }, + }, + "no-tests": { + []string{"-no-tests"}, + &Validate{ + Path: ".", + TestDirectory: "tests", + ViewType: ViewHuman, + NoTests: true, }, }, } @@ -58,8 +79,9 @@ func TestParseValidate_invalid(t *testing.T) { "unknown flag": { []string{"-boop"}, &Validate{ - Path: ".", - ViewType: ViewHuman, + Path: ".", + TestDirectory: "tests", + ViewType: ViewHuman, }, tfdiags.Diagnostics{ tfdiags.Sourceless( @@ -72,8 +94,9 @@ func TestParseValidate_invalid(t *testing.T) { "too many arguments": { []string{"-json", "bar", "baz"}, &Validate{ - Path: "bar", - ViewType: ViewJSON, + Path: "bar", + TestDirectory: "tests", + ViewType: ViewJSON, }, tfdiags.Diagnostics{ tfdiags.Sourceless( @@ -91,9 +114,7 @@ func TestParseValidate_invalid(t *testing.T) { if *got != *tc.want { t.Fatalf("unexpected result\n got: %#v\nwant: %#v", got, tc.want) } - if !reflect.DeepEqual(gotDiags, tc.wantDiags) { - t.Errorf("wrong result\ngot: %s\nwant: %s", spew.Sdump(gotDiags), spew.Sdump(tc.wantDiags)) - } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.wantDiags) }) } } diff --git a/internal/command/arguments/view.go b/internal/command/arguments/view.go index 3d6372b6ae..9d13bb8ff3 100644 --- a/internal/command/arguments/view.go +++ b/internal/command/arguments/view.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments // View represents the global command-line arguments which configure the view. diff --git a/internal/command/arguments/view_test.go b/internal/command/arguments/view_test.go index d2e7c3f730..75583cb0fb 100644 --- a/internal/command/arguments/view_test.go +++ b/internal/command/arguments/view_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package arguments import ( diff --git a/internal/command/autocomplete.go b/internal/command/autocomplete.go index 87e765fe83..e773451277 100644 --- a/internal/command/autocomplete.go +++ b/internal/command/autocomplete.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/autocomplete_test.go b/internal/command/autocomplete_test.go index 5f0c5ef916..35e364fc9e 100644 --- a/internal/command/autocomplete_test.go +++ b/internal/command/autocomplete_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,7 +9,7 @@ import ( "reflect" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/posener/complete" ) diff --git a/internal/command/cli_ui.go b/internal/command/cli_ui.go index 7679b1ed8e..4fbfa677c2 100644 --- a/internal/command/cli_ui.go +++ b/internal/command/cli_ui.go @@ -1,13 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/mitchellh/colorstring" ) -// ColoredUi is a Ui implementation that colors its output according +// ColorizeUi is a Ui implementation that colors its output according // to the given color schemes for the given type of output. type ColorizeUi struct { Colorize *colorstring.Colorize diff --git a/internal/command/cli_ui_test.go b/internal/command/cli_ui_test.go index ac2b7d7ea8..e56d1372b0 100644 --- a/internal/command/cli_ui_test.go +++ b/internal/command/cli_ui_test.go @@ -1,9 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) func TestColorizeUi_impl(t *testing.T) { diff --git a/internal/command/cliconfig/cliconfig.go b/internal/command/cliconfig/cliconfig.go index 7f3b1aa73d..e162fe44f9 100644 --- a/internal/command/cliconfig/cliconfig.go +++ b/internal/command/cliconfig/cliconfig.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package cliconfig has the types representing and the logic to load CLI-level // configuration settings. // @@ -9,11 +12,15 @@ package cliconfig import ( + "errors" "fmt" + "io/fs" "io/ioutil" "log" + "maps" "os" "path/filepath" + "strings" "github.com/hashicorp/hcl" @@ -22,6 +29,7 @@ import ( ) const pluginCacheDirEnvVar = "TF_PLUGIN_CACHE_DIR" +const pluginCacheMayBreakLockFileEnvVar = "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE" // Config is the structure of the configuration for the Terraform CLI. // @@ -38,6 +46,17 @@ type Config struct { // avoid repeatedly re-downloading over the Internet. PluginCacheDir string `hcl:"plugin_cache_dir"` + // PluginCacheMayBreakDependencyLockFile is an interim accommodation for + // those who wish to use the Plugin Cache Dir even in cases where doing so + // will cause the dependency lock file to be incomplete. + // + // This is likely to become a silent no-op in future Terraform versions but + // is here in recognition of the fact that the dependency lock file is not + // yet a good fit for all Terraform workflows and folks in that category + // would prefer to have the plugin cache dir's behavior to take priority + // over the requirements of the dependency lock file. + PluginCacheMayBreakDependencyLockFile bool `hcl:"plugin_cache_may_break_dependency_lock_file"` + Hosts map[string]*ConfigHost `hcl:"host"` Credentials map[string]map[string]interface{} `hcl:"credentials"` @@ -89,12 +108,14 @@ func LoadConfig() (*Config, tfdiags.Diagnostics) { configVal := BuiltinConfig // copy config := &configVal - if mainFilename, err := cliConfigFile(); err == nil { + if mainFilename, mainFileDiags := cliConfigFile(); len(mainFileDiags) == 0 { if _, err := os.Stat(mainFilename); err == nil { mainConfig, mainDiags := loadConfigFile(mainFilename) diags = diags.Append(mainDiags) config = config.Merge(mainConfig) } + } else { + diags = diags.Append(mainFileDiags) } // Unless the user has specifically overridden the configuration file @@ -209,9 +230,14 @@ func loadConfigDir(path string) (*Config, tfdiags.Diagnostics) { // Any values specified in this config should override those set in the // configuration file. func EnvConfig() *Config { + env := makeEnvMap(os.Environ()) + return envConfig(env) +} + +func envConfig(env map[string]string) *Config { config := &Config{} - if envPluginCacheDir := os.Getenv(pluginCacheDirEnvVar); envPluginCacheDir != "" { + if envPluginCacheDir := env[pluginCacheDirEnvVar]; envPluginCacheDir != "" { // No Expandenv here, because expanding environment variables inside // an environment variable would be strange and seems unnecessary. // (User can expand variables into the value while setting it using @@ -219,9 +245,34 @@ func EnvConfig() *Config { config.PluginCacheDir = envPluginCacheDir } + if envMayBreak := env[pluginCacheMayBreakLockFileEnvVar]; envMayBreak != "" && envMayBreak != "0" { + // This is an environment variable analog to the + // plugin_cache_may_break_dependency_lock_file setting. If either this + // or the config file setting are enabled then it's enabled; there is + // no way to override back to false if either location sets this to + // true. + config.PluginCacheMayBreakDependencyLockFile = true + } + return config } +func makeEnvMap(environ []string) map[string]string { + if len(environ) == 0 { + return nil + } + + ret := make(map[string]string, len(environ)) + for _, entry := range environ { + eq := strings.IndexByte(entry, '=') + if eq == -1 { + continue + } + ret[entry[:eq]] = entry[eq+1:] + } + return ret +} + // Validate checks for errors in the configuration that cannot be detected // just by HCL decoding, returning any problems as diagnostics. // @@ -291,18 +342,14 @@ func (c *Config) Merge(c2 *Config) *Config { var result Config result.Providers = make(map[string]string) result.Provisioners = make(map[string]string) - for k, v := range c.Providers { - result.Providers[k] = v - } + maps.Copy(result.Providers, c.Providers) + maps.Copy(result.Provisioners, c.Provisioners) for k, v := range c2.Providers { if v1, ok := c.Providers[k]; ok { log.Printf("[INFO] Local %s provider configuration '%s' overrides '%s'", k, v, v1) } result.Providers[k] = v } - for k, v := range c.Provisioners { - result.Provisioners[k] = v - } for k, v := range c2.Provisioners { if v1, ok := c.Provisioners[k]; ok { log.Printf("[INFO] Local %s provisioner configuration '%s' overrides '%s'", k, v, v1) @@ -317,37 +364,31 @@ func (c *Config) Merge(c2 *Config) *Config { result.PluginCacheDir = c2.PluginCacheDir } + if c.PluginCacheMayBreakDependencyLockFile || c2.PluginCacheMayBreakDependencyLockFile { + // This setting saturates to "on"; once either configuration sets it, + // there is no way to override it back to off again. + result.PluginCacheMayBreakDependencyLockFile = true + } + if (len(c.Hosts) + len(c2.Hosts)) > 0 { result.Hosts = make(map[string]*ConfigHost) - for name, host := range c.Hosts { - result.Hosts[name] = host - } - for name, host := range c2.Hosts { - result.Hosts[name] = host - } + maps.Copy(result.Hosts, c.Hosts) + maps.Copy(result.Hosts, c2.Hosts) } if (len(c.Credentials) + len(c2.Credentials)) > 0 { result.Credentials = make(map[string]map[string]interface{}) - for host, creds := range c.Credentials { - result.Credentials[host] = creds - } - for host, creds := range c2.Credentials { - // We just clobber an entry from the other file right now. Will - // improve on this later using the more-robust merging behavior - // built in to HCL2. - result.Credentials[host] = creds - } + maps.Copy(result.Credentials, c.Credentials) + // We just clobber an entry from the other file right now. Will + // improve on this later using the more-robust merging behavior + // built in to HCL2. + maps.Copy(result.Credentials, c2.Credentials) } if (len(c.CredentialsHelpers) + len(c2.CredentialsHelpers)) > 0 { result.CredentialsHelpers = make(map[string]*ConfigCredentialsHelper) - for name, helper := range c.CredentialsHelpers { - result.CredentialsHelpers[name] = helper - } - for name, helper := range c2.CredentialsHelpers { - result.CredentialsHelpers[name] = helper - } + maps.Copy(result.CredentialsHelpers, c.CredentialsHelpers) + maps.Copy(result.CredentialsHelpers, c2.CredentialsHelpers) } if (len(c.ProviderInstallation) + len(c2.ProviderInstallation)) > 0 { @@ -358,7 +399,8 @@ func (c *Config) Merge(c2 *Config) *Config { return &result } -func cliConfigFile() (string, error) { +func cliConfigFile() (string, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics mustExist := true configFilePath := cliConfigFileOverride() @@ -378,15 +420,19 @@ func cliConfigFile() (string, error) { f, err := os.Open(configFilePath) if err == nil { f.Close() - return configFilePath, nil + return configFilePath, diags } - if mustExist || !os.IsNotExist(err) { - return "", err + if mustExist || !errors.Is(err, fs.ErrNotExist) { + diags = append(diags, tfdiags.Sourceless( + tfdiags.Warning, + "Unable to open CLI configuration file", + fmt.Sprintf("The CLI configuration file at %q does not exist.", configFilePath), + )) } log.Println("[DEBUG] File doesn't exist, but doesn't need to. Ignoring.") - return "", nil + return "", diags } func cliConfigFileOverride() string { diff --git a/internal/command/cliconfig/cliconfig_test.go b/internal/command/cliconfig/cliconfig_test.go index d09ffff7b7..87720fb7f0 100644 --- a/internal/command/cliconfig/cliconfig_test.go +++ b/internal/command/cliconfig/cliconfig_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cliconfig import ( @@ -8,6 +11,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/tfdiags" ) // This is the directory where our test fixtures are. @@ -31,7 +35,7 @@ func TestLoadConfig(t *testing.T) { } } -func TestLoadConfig_env(t *testing.T) { +func TestLoadConfig_envSubst(t *testing.T) { defer os.Unsetenv("TFTEST") os.Setenv("TFTEST", "hello") @@ -55,6 +59,166 @@ func TestLoadConfig_env(t *testing.T) { } } +func TestLoadConfig_non_existing_file(t *testing.T) { + tmpDir := os.TempDir() + cliTmpFile := filepath.Join(tmpDir, "dev.tfrc") + + os.Setenv("TF_CLI_CONFIG_FILE", cliTmpFile) + defer os.Unsetenv("TF_CLI_CONFIG_FILE") + + c, errs := LoadConfig() + if errs.HasErrors() || c.Validate().HasErrors() { + t.Fatalf("err: %s", errs) + } + + hasOpenFileWarn := false + for _, err := range errs { + if err.Severity() == tfdiags.Warning && err.Description().Summary == "Unable to open CLI configuration file" { + hasOpenFileWarn = true + break + } + } + + if !hasOpenFileWarn { + t.Fatal("expecting a warning message because of nonexisting CLI configuration file") + } +} + +func TestEnvConfig(t *testing.T) { + tests := map[string]struct { + env map[string]string + want *Config + }{ + "no environment variables": { + nil, + &Config{}, + }, + "TF_PLUGIN_CACHE_DIR=boop": { + map[string]string{ + "TF_PLUGIN_CACHE_DIR": "boop", + }, + &Config{ + PluginCacheDir: "boop", + }, + }, + "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=anything_except_zero": { + map[string]string{ + "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "anything_except_zero", + }, + &Config{ + PluginCacheMayBreakDependencyLockFile: true, + }, + }, + "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE=0": { + map[string]string{ + "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "0", + }, + &Config{}, + }, + "TF_PLUGIN_CACHE_DIR and TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": { + map[string]string{ + "TF_PLUGIN_CACHE_DIR": "beep", + "TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE": "1", + }, + &Config{ + PluginCacheDir: "beep", + PluginCacheMayBreakDependencyLockFile: true, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := envConfig(test.env) + want := test.want + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + +func TestMakeEnvMap(t *testing.T) { + tests := map[string]struct { + environ []string + want map[string]string + }{ + "nil": { + nil, + nil, + }, + "one": { + []string{ + "FOO=bar", + }, + map[string]string{ + "FOO": "bar", + }, + }, + "many": { + []string{ + "FOO=1", + "BAR=2", + "BAZ=3", + }, + map[string]string{ + "FOO": "1", + "BAR": "2", + "BAZ": "3", + }, + }, + "conflict": { + []string{ + "FOO=1", + "BAR=1", + "FOO=2", + }, + map[string]string{ + "BAR": "1", + "FOO": "2", // Last entry of each name wins + }, + }, + "empty_val": { + []string{ + "FOO=", + }, + map[string]string{ + "FOO": "", + }, + }, + "no_equals": { + []string{ + "FOO=bar", + "INVALID", + }, + map[string]string{ + "FOO": "bar", + }, + }, + "multi_equals": { + []string{ + "FOO=bar=baz=boop", + }, + map[string]string{ + "FOO": "bar=baz=boop", + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := makeEnvMap(test.environ) + want := test.want + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } + +} + func TestLoadConfig_hosts(t *testing.T) { got, diags := loadConfigFile(filepath.Join(fixtureDir, "hosts")) if len(diags) != 0 { @@ -284,6 +448,7 @@ func TestConfig_Merge(t *testing.T) { }, }, }, + PluginCacheMayBreakDependencyLockFile: true, } expected := &Config{ @@ -338,6 +503,7 @@ func TestConfig_Merge(t *testing.T) { }, }, }, + PluginCacheMayBreakDependencyLockFile: true, } actual := c1.Merge(c2) diff --git a/internal/command/cliconfig/config_unix.go b/internal/command/cliconfig/config_unix.go index 6dc1450b23..71bb390f2a 100644 --- a/internal/command/cliconfig/config_unix.go +++ b/internal/command/cliconfig/config_unix.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !windows // +build !windows diff --git a/internal/command/cliconfig/config_windows.go b/internal/command/cliconfig/config_windows.go index 8f232fd5b6..b8c7e9cb6f 100644 --- a/internal/command/cliconfig/config_windows.go +++ b/internal/command/cliconfig/config_windows.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build windows // +build windows diff --git a/internal/command/cliconfig/credentials.go b/internal/command/cliconfig/credentials.go index a85a1b7cd2..2ee061d2fd 100644 --- a/internal/command/cliconfig/credentials.go +++ b/internal/command/cliconfig/credentials.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cliconfig import ( diff --git a/internal/command/cliconfig/credentials_test.go b/internal/command/cliconfig/credentials_test.go index 50ed443b7d..3f9debdbac 100644 --- a/internal/command/cliconfig/credentials_test.go +++ b/internal/command/cliconfig/credentials_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cliconfig import ( diff --git a/plugins.go b/internal/command/cliconfig/plugins.go similarity index 77% rename from plugins.go rename to internal/command/cliconfig/plugins.go index be576e81ac..ad4cab536c 100644 --- a/plugins.go +++ b/internal/command/cliconfig/plugins.go @@ -1,24 +1,25 @@ -package main +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cliconfig import ( "fmt" "log" "path/filepath" "runtime" - - "github.com/hashicorp/terraform/internal/command/cliconfig" ) -// globalPluginDirs returns directories that should be searched for +// GlobalPluginDirs returns directories that should be searched for // globally-installed plugins (not specific to the current configuration). // // Earlier entries in this slice get priority over later when multiple copies // of the same plugin version are found, but newer versions always override // older versions where both satisfy the provider version constraints. -func globalPluginDirs() []string { +func GlobalPluginDirs() []string { var ret []string // Look in ~/.terraform.d/plugins/ , or its equivalent on non-UNIX - dir, err := cliconfig.ConfigDir() + dir, err := ConfigDir() if err != nil { log.Printf("[ERROR] Error finding global config directory: %s", err) } else { diff --git a/internal/command/cliconfig/provider_installation.go b/internal/command/cliconfig/provider_installation.go index 73d5349ba7..fb3d2dec27 100644 --- a/internal/command/cliconfig/provider_installation.go +++ b/internal/command/cliconfig/provider_installation.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cliconfig import ( "fmt" + "os" "path/filepath" "github.com/hashicorp/hcl" @@ -219,7 +223,7 @@ func decodeProviderInstallationFromConfig(hclFile *hclast.File) ([]*ProviderInst diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider_installation method block", - fmt.Sprintf("The dev_overrides block at at %s must appear before all other installation methods, because development overrides always have the highest priority.", methodBlock.Pos()), + fmt.Sprintf("The dev_overrides block at %s must appear before all other installation methods, because development overrides always have the highest priority.", methodBlock.Pos()), )) continue } @@ -246,11 +250,27 @@ func decodeProviderInstallationFromConfig(hclFile *hclast.File) ([]*ProviderInst diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider installation dev overrides", - fmt.Sprintf("The entry %q in %s is not a valid provider source string.", rawAddr, block.Pos()), + fmt.Sprintf("The entry %q in %s is not a valid provider source string.\n\n%s", rawAddr, block.Pos(), moreDiags.Err().Error()), )) continue } - dirPath := filepath.Clean(rawPath) + unsetEnvVars := make(map[string]bool) + interpolatedPath := os.Expand(rawPath, func(envVarName string) string { + if value, ok := os.LookupEnv(envVarName); ok { + return value + } else { + if _, reported := unsetEnvVars[envVarName]; !reported { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Interpolated environment variable not set", + fmt.Sprintf("The environment variable %s is not set or empty and can result in undesired behavior", envVarName), + )) + unsetEnvVars[envVarName] = true + } + return "" + } + }) + dirPath := filepath.Clean(interpolatedPath) devOverrides[addr] = getproviders.PackageLocalDir(dirPath) } diff --git a/internal/command/cliconfig/provider_installation_test.go b/internal/command/cliconfig/provider_installation_test.go index cd55c0b6b6..48e7d2764e 100644 --- a/internal/command/cliconfig/provider_installation_test.go +++ b/internal/command/cliconfig/provider_installation_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package cliconfig import ( diff --git a/internal/command/clistate/local_state.go b/internal/command/clistate/local_state.go index 7a0102c702..363ec1268e 100644 --- a/internal/command/clistate/local_state.go +++ b/internal/command/clistate/local_state.go @@ -1,8 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package clistate import ( - "bytes" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -11,8 +14,7 @@ import ( "sync" "time" - multierror "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/internal/legacy/terraform" + "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/states/statemgr" ) @@ -39,13 +41,13 @@ type LocalState struct { // hurt to remove file we never wrote to. created bool - state *terraform.State - readState *terraform.State + state *workdir.BackendStateFile + readState *workdir.BackendStateFile written bool } // SetState will force a specific state in-memory for this local state. -func (s *LocalState) SetState(state *terraform.State) { +func (s *LocalState) SetState(state *workdir.BackendStateFile) { s.mu.Lock() defer s.mu.Unlock() @@ -54,7 +56,7 @@ func (s *LocalState) SetState(state *terraform.State) { } // StateReader impl. -func (s *LocalState) State() *terraform.State { +func (s *LocalState) State() *workdir.BackendStateFile { return s.state.DeepCopy() } @@ -64,7 +66,7 @@ func (s *LocalState) State() *terraform.State { // the original. // // StateWriter impl. -func (s *LocalState) WriteState(state *terraform.State) error { +func (s *LocalState) WriteState(state *workdir.BackendStateFile) error { s.mu.Lock() defer s.mu.Unlock() @@ -77,13 +79,6 @@ func (s *LocalState) WriteState(state *terraform.State) error { s.state = state.DeepCopy() // don't want mutations before we actually get this written to disk - if s.readState != nil && s.state != nil { - // We don't trust callers to properly manage serials. Instead, we assume - // that a WriteState is always for the next version after what was - // most recently read. - s.state.Serial = s.readState.Serial - } - if _, err := s.stateFileOut.Seek(0, io.SeekStart); err != nil { return err } @@ -96,11 +91,12 @@ func (s *LocalState) WriteState(state *terraform.State) error { return nil } - if !s.state.MarshalEqual(s.readState) { - s.state.Serial++ + raw, err := workdir.EncodeBackendStateFile(state) + if err != nil { + return err } - - if err := terraform.WriteState(s.state, s.stateFileOut); err != nil { + _, err = s.stateFileOut.Write(raw) + if err != nil { return err } @@ -142,9 +138,8 @@ func (s *LocalState) RefreshState() error { return err } - // we need a non-nil reader for ReadState and an empty buffer works - // to return EOF immediately - reader = bytes.NewBuffer(nil) + // a nil reader means no state at all, handled below + reader = nil } else { defer f.Close() @@ -161,10 +156,16 @@ func (s *LocalState) RefreshState() error { reader = s.stateFileOut } - state, err := terraform.ReadState(reader) - // if there's no state we just assign the nil return value - if err != nil && err != terraform.ErrNoState { - return err + var state *workdir.BackendStateFile + if reader != nil { // otherwise we'll leave state as nil + raw, err := io.ReadAll(reader) + if err != nil { + return err + } + state, err = workdir.ParseBackendStateFile(raw) + if err != nil { + return err + } } s.state = state @@ -190,7 +191,7 @@ func (s *LocalState) Lock(info *statemgr.LockInfo) (string, error) { if err := s.lock(); err != nil { info, infoErr := s.lockInfo() if infoErr != nil { - err = multierror.Append(err, infoErr) + err = errors.Join(err, infoErr) } lockErr := &statemgr.LockError{ @@ -217,7 +218,7 @@ func (s *LocalState) Unlock(id string) error { idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) info, err := s.lockInfo() if err != nil { - idErr = multierror.Append(idErr, err) + idErr = errors.Join(idErr, err) } return &statemgr.LockError{ diff --git a/internal/command/clistate/local_state_lock_unix.go b/internal/command/clistate/local_state_lock_unix.go index abf6c5de62..f978da64c8 100644 --- a/internal/command/clistate/local_state_lock_unix.go +++ b/internal/command/clistate/local_state_lock_unix.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !windows // +build !windows diff --git a/internal/command/clistate/local_state_lock_windows.go b/internal/command/clistate/local_state_lock_windows.go index a51e630773..4268eae374 100644 --- a/internal/command/clistate/local_state_lock_windows.go +++ b/internal/command/clistate/local_state_lock_windows.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build windows // +build windows diff --git a/internal/command/clistate/state.go b/internal/command/clistate/state.go index a9946b6e6a..64cd574cf5 100644 --- a/internal/command/clistate/state.go +++ b/internal/command/clistate/state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package state exposes common helpers for working with state from the CLI. // // This is a separate package so that backends can use this for consistent diff --git a/internal/command/clistate/state_test.go b/internal/command/clistate/state_test.go index e0daecdd54..ae3ae972a1 100644 --- a/internal/command/clistate/state_test.go +++ b/internal/command/clistate/state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package clistate import ( diff --git a/internal/command/cloud.go b/internal/command/cloud.go new file mode 100644 index 0000000000..69f194823e --- /dev/null +++ b/internal/command/cloud.go @@ -0,0 +1,324 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "bytes" + "fmt" + "io" + "log" + "net/url" + "os" + "os/exec" + "path" + "runtime" + + "google.golang.org/grpc/metadata" + + "github.com/hashicorp/go-plugin" + "github.com/hashicorp/terraform/internal/cloud" + "github.com/hashicorp/terraform/internal/cloudplugin" + "github.com/hashicorp/terraform/internal/cloudplugin/cloudplugin1" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// CloudCommand is a Command implementation that interacts with Terraform +// Cloud for operations that are inherently planless. It delegates +// all execution to an internal plugin. +type CloudCommand struct { + Meta + // Path to the plugin server executable + pluginBinary string + // Service URL we can download plugin release binaries from + pluginService *url.URL + // Everything the plugin needs to build a client and Do Things + pluginConfig CloudPluginConfig +} + +const ( + // DefaultCloudPluginVersion is the implied protocol version, though all + // historical versions are defined explicitly. + DefaultCloudPluginVersion = 1 + + // ExitRPCError is the exit code that is returned if an plugin + // communication error occurred. + ExitRPCError = 99 + + // ExitPluginError is the exit code that is returned if the plugin + // cannot be downloaded. + ExitPluginError = 98 + + // The regular HCP Terraform API service that the go-tfe client relies on. + tfeServiceID = "tfe.v2" + // The cloud plugin release download service that the BinaryManager relies + // on to fetch the plugin. + cloudpluginServiceID = "cloudplugin.v1" +) + +var ( + // Handshake is used to verify that the plugin is the appropriate plugin for + // the client. This is not a security verification. + Handshake = plugin.HandshakeConfig{ + MagicCookieKey: "TF_CLOUDPLUGIN_MAGIC_COOKIE", + MagicCookieValue: "721fca41431b780ff3ad2623838faaa178d74c65e1cfdfe19537c31656496bf9f82d6c6707f71d81c8eed0db9043f79e56ab4582d013bc08ead14f57961461dc", + ProtocolVersion: DefaultCloudPluginVersion, + } + // CloudPluginDataDir is the name of the directory within the data directory + CloudPluginDataDir = "cloudplugin" +) + +func (c *CloudCommand) realRun(args []string, stdout, stderr io.Writer) int { + args = c.Meta.process(args) + + diags := c.initPlugin() + if diags.HasWarnings() || diags.HasErrors() { + c.View.Diagnostics(diags) + } + if diags.HasErrors() { + return ExitPluginError + } + + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: Handshake, + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + Cmd: exec.Command(c.pluginBinary), + Logger: logging.NewCloudLogger(), + VersionedPlugins: map[int]plugin.PluginSet{ + 1: { + "cloud": &cloudplugin1.GRPCCloudPlugin{ + Metadata: c.pluginConfig.ToMetadata(), + }, + }, + }, + }) + defer client.Kill() + + // Connect via RPC + rpcClient, err := client.Client() + if err != nil { + fmt.Fprintf(stderr, "Failed to create cloud plugin client: %s", err) + return ExitRPCError + } + + // Request the plugin + raw, err := rpcClient.Dispense("cloud") + if err != nil { + fmt.Fprintf(stderr, "Failed to request cloud plugin interface: %s", err) + return ExitRPCError + } + + // Proxy the request + // Note: future changes will need to determine the type of raw when + // multiple versions are possible. + cloud1, ok := raw.(cloudplugin.Cloud1) + if !ok { + c.Ui.Error("If more than one cloudplugin versions are available, they need to be added to the cloud command. This is a bug in Terraform.") + return ExitRPCError + } + return cloud1.Execute(args, stdout, stderr) +} + +// discoverAndConfigure is an implementation detail of initPlugin. It fills in the +// pluginService and pluginConfig fields on a CloudCommand struct. +func (c *CloudCommand) discoverAndConfigure() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // First, spin up a Cloud backend. (Why? bc finding the info the plugin + // needs is hard, and the Cloud backend already knows how to do it all.) + backendConfig, bConfigDiags := c.loadBackendConfig(".") + diags = diags.Append(bConfigDiags) + if diags.HasErrors() { + return diags + } + b, backendDiags := c.Backend(&BackendOpts{ + Config: backendConfig, + }) + diags = diags.Append(backendDiags) + if diags.HasErrors() { + return diags + } + cb, ok := b.(*cloud.Cloud) + if !ok { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No `cloud` block found", + "Cloud command requires that a `cloud` block be configured in the working directory", + )) + return diags + } + + // Ok sweet. First, re-use the cached service discovery info for this TFC + // instance to find our plugin service and TFE API URLs: + pluginService, err := cb.ServicesHost.ServiceURL(cloudpluginServiceID) + if err != nil { + return diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cloud plugin service not found", + err.Error(), + )) + } + c.pluginService = pluginService + + tfeService, err := cb.ServicesHost.ServiceURL(tfeServiceID) + if err != nil { + return diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "HCP Terraform API service not found", + err.Error(), + )) + } + + currentWorkspace, err := c.Workspace() + if err != nil { + // The only possible error here is "you set TF_WORKSPACE badly" + return diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Bad current workspace", + err.Error(), + )) + } + + // Now just steal everything we need so we can pass it to the plugin later. + c.pluginConfig = CloudPluginConfig{ + Address: tfeService.String(), + BasePath: tfeService.Path, + DisplayHostname: cb.Hostname, + Token: cb.Token, + Organization: cb.Organization, + CurrentWorkspace: currentWorkspace, + WorkspaceName: cb.WorkspaceMapping.Name, + WorkspaceTags: cb.WorkspaceMapping.TagsAsSet, + DefaultProjectName: cb.WorkspaceMapping.Project, + } + + return diags +} + +func (c *CloudCommand) initPlugin() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + var errorSummary = "Cloud plugin initialization error" + + // Initialization can be aborted by interruption signals + ctx, done := c.InterruptibleContext(c.CommandContext()) + defer done() + + // Discover service URLs, and build out the plugin config + diags = diags.Append(c.discoverAndConfigure()) + if diags.HasErrors() { + return diags + } + + packagesPath, err := c.initPackagesCache() + if err != nil { + return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error())) + } + + overridePath := os.Getenv("TF_CLOUD_PLUGIN_DEV_OVERRIDE") + + bm, err := cloudplugin.NewBinaryManager(ctx, packagesPath, overridePath, c.pluginService, runtime.GOOS, runtime.GOARCH) + if err != nil { + return diags.Append(tfdiags.Sourceless(tfdiags.Error, errorSummary, err.Error())) + } + + version, err := bm.Resolve() + if err != nil { + return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Cloud plugin download error", err.Error())) + } + + var cacheTraceMsg = "" + if version.ResolvedFromCache { + cacheTraceMsg = " (resolved from cache)" + } + if version.ResolvedFromDevOverride { + cacheTraceMsg = " (resolved from dev override)" + detailMsg := fmt.Sprintf("Instead of using the current released version, Terraform is loading the cloud plugin from the following location:\n\n - %s\n\nOverriding the cloud plugin location can cause unexpected behavior, and is only intended for use when developing new versions of the plugin.", version.Path) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Cloud plugin development overrides are in effect", + detailMsg, + )) + } + log.Printf("[TRACE] plugin %q binary located at %q%s", version.ProductVersion, version.Path, cacheTraceMsg) + c.pluginBinary = version.Path + return diags +} + +func (c *CloudCommand) initPackagesCache() (string, error) { + packagesPath := path.Join(c.WorkingDir.DataDir(), CloudPluginDataDir) + + if info, err := os.Stat(packagesPath); err != nil || !info.IsDir() { + log.Printf("[TRACE] initialized cloudplugin cache directory at %q", packagesPath) + err = os.MkdirAll(packagesPath, 0755) + if err != nil { + return "", fmt.Errorf("failed to initialize cloudplugin cache directory: %w", err) + } + } else { + log.Printf("[TRACE] cloudplugin cache directory found at %q", packagesPath) + } + + return packagesPath, nil +} + +// Run runs the cloud command with the given arguments. +func (c *CloudCommand) Run(args []string) int { + args = c.Meta.process(args) + return c.realRun(args, c.Meta.Streams.Stdout.File, c.Meta.Streams.Stderr.File) +} + +// Help returns help text for the cloud command. +func (c *CloudCommand) Help() string { + helpText := new(bytes.Buffer) + if exitCode := c.realRun([]string{}, helpText, io.Discard); exitCode != 0 { + return "" + } + + return helpText.String() +} + +// Synopsis returns a short summary of the cloud command. +func (c *CloudCommand) Synopsis() string { + return "Manage HCP Terraform settings and metadata" +} + +// CloudPluginConfig is everything the cloud plugin needs to know to configure a +// client and talk to HCP Terraform. +type CloudPluginConfig struct { + // Maybe someday we can use struct tags to automate grabbing these out of + // the metadata headers! And verify client-side that we're sending the right + // stuff, instead of having it all be a stringly-typed mystery ball! I want + // to believe in that distant shining day! 🌻 Meantime, these struct tags + // serve purely as docs. + Address string `md:"tfc-address"` + BasePath string `md:"tfc-base-path"` + DisplayHostname string `md:"tfc-display-hostname"` + Token string `md:"tfc-token"` + Organization string `md:"tfc-organization"` + // The actual selected workspace + CurrentWorkspace string `md:"tfc-current-workspace"` + + // The raw "WorkspaceMapping" attributes, which determine the workspaces + // that could be selected. Generally you want CurrentWorkspace instead, but + // these can potentially be useful for niche use cases. + WorkspaceName string `md:"tfc-workspace-name"` + WorkspaceTags []string `md:"tfc-workspace-tags"` + DefaultProjectName string `md:"tfc-default-project-name"` +} + +func (c CloudPluginConfig) ToMetadata() metadata.MD { + // First, do everything except tags the easy way + md := metadata.Pairs( + "tfc-address", c.Address, + "tfc-base-path", c.BasePath, + "tfc-display-hostname", c.DisplayHostname, + "tfc-token", c.Token, + "tfc-organization", c.Organization, + "tfc-current-workspace", c.CurrentWorkspace, + "tfc-workspace-name", c.WorkspaceName, + "tfc-default-project-name", c.DefaultProjectName, + ) + // Then the straggler + md["tfc-workspace-tags"] = c.WorkspaceTags + return md +} diff --git a/internal/command/cloud_test.go b/internal/command/cloud_test.go new file mode 100644 index 0000000000..7547ec8398 --- /dev/null +++ b/internal/command/cloud_test.go @@ -0,0 +1,243 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "fmt" + "io" + "log" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" + svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/auth" + "github.com/hashicorp/terraform-svchost/disco" + "github.com/hashicorp/terraform/internal/backend" + backendInit "github.com/hashicorp/terraform/internal/backend/init" + backendCloud "github.com/hashicorp/terraform/internal/cloud" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/version" + "google.golang.org/grpc/metadata" +) + +func newCloudPluginManifestHTTPTestServer(t *testing.T) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + + initialPath, _ := os.Getwd() + + mux.HandleFunc("/api/cloudplugin/v1/manifest", func(w http.ResponseWriter, r *http.Request) { + fileToSend, _ := os.Open(path.Join(initialPath, "testdata/cloud-archives/manifest.json")) + defer fileToSend.Close() + io.Copy(w, fileToSend) + }) + + // Respond to service version constraints calls. + mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, fmt.Sprintf(`{ + "service": "%s", + "product": "terraform", + "minimum": "0.1.0", + "maximum": "10.0.0" +}`, path.Base(r.URL.Path))) + }) + + // Respond to pings to get the API version header. + mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.5") + }) + + // Respond to the initial query to read the hashicorp org entitlements. + mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.api+json") + io.WriteString(w, `{ + "data": { + "id": "org-GExadygjSbKP8hsY", + "type": "entitlement-sets", + "attributes": { + "operations": true, + "private-module-registry": true, + "sentinel": true, + "state-storage": true, + "teams": true, + "vcs-integrations": true + } + } +}`) + }) + + mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/test", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.api+json") + io.WriteString(w, `{ + "data": { + "id": "ws-GExadygjSbKP8hsY", + "type": "workspaces", + "attributes": { + "name": "test", + "terraform-version": "1.5.4" + } + } +}`) + }) + + return httptest.NewServer(mux) +} + +// testDisco returns a *disco.Disco mapping app.terraform.io and +// localhost to a local test server. +func testDisco(s *httptest.Server) *disco.Disco { + host, _ := url.Parse(s.URL) + defaultHostname := "app.terraform.io" + tfeHost := svchost.Hostname(defaultHostname) + services := map[string]interface{}{ + "cloudplugin.v1": fmt.Sprintf("%s/api/cloudplugin/v1/", s.URL), + "tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL), + } + + credsSrc := auth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{ + tfeHost: {"token": "test-auth-token"}, + }) + + d := disco.NewWithCredentialsSource(credsSrc) + d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) + d.ForceHostServices(tfeHost, services) + d.ForceHostServices(svchost.Hostname(host.Host), services) + + return d +} + +func TestCloud_withBackendConfig(t *testing.T) { + t.Skip("To be converted to an e2e test") + + server := newCloudPluginManifestHTTPTestServer(t) + disco := testDisco(server) + + wd := tempWorkingDirFixture(t, "cloud-config") + defer testChdir(t, wd.RootModuleDir())() + + // Overwrite the cloud backend with the test disco + previousBackend := backendInit.Backend("cloud") + backendInit.Set("cloud", func() backend.Backend { return backendCloud.New(disco) }) + defer backendInit.Set("cloud", previousBackend) + + ui := cli.NewMockUi() + view, _ := testView(t) + + // Initialize the backend + ic := &InitCommand{ + Meta{ + Ui: ui, + View: view, + testingOverrides: metaOverridesForProvider(testProvider()), + Services: disco, + }, + } + + log.Print("[TRACE] TestCloud_withBackendConfig running: terraform init") + if code := ic.Run([]string{}); code != 0 { + t.Fatalf("init failed\n%s", ui.ErrorWriter) + } + + // Run the cloud command + ui = cli.NewMockUi() + c := &CloudCommand{ + Meta: Meta{ + Ui: ui, + testingOverrides: metaOverridesForProvider(testProvider()), + Services: disco, + WorkingDir: wd, + }, + } + + args := []string{"version"} + if code := c.Run(args); code != 0 { + t.Fatalf("expected exit 0, got %d: \n%s", code, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + expected := "HCP Terraform Plugin v0.1.0\n\n" + if output != expected { + t.Fatalf("the output did not equal the expected string:\n%s", cmp.Diff(expected, output)) + } +} + +func TestCloud_withENVConfig(t *testing.T) { + t.Skip("To be converted to an e2e test") + + server := newCloudPluginManifestHTTPTestServer(t) + disco := testDisco(server) + + wd := tempWorkingDir(t) + defer testChdir(t, wd.RootModuleDir())() + + serverURL, _ := url.Parse(server.URL) + + os.Setenv("TF_CLOUD_HOSTNAME", serverURL.Host) + defer os.Unsetenv("TF_CLOUD_HOSTNAME") + + // Run the cloud command + ui := cli.NewMockUi() + c := &CloudCommand{ + Meta: Meta{ + Ui: ui, + testingOverrides: metaOverridesForProvider(testProvider()), + Services: disco, + WorkingDir: wd, + }, + } + + args := []string{"version"} + if code := c.Run(args); code != 0 { + t.Fatalf("expected exit 0, got %d: \n%s", code, ui.ErrorWriter.String()) + } + + output := ui.OutputWriter.String() + expected := "HCP Terraform Plugin v0.1.0\n\n" + if output != expected { + t.Fatalf("the output did not equal the expected string:\n%s", cmp.Diff(expected, output)) + } +} + +func TestCloudPluginConfig_ToMetadata(t *testing.T) { + expected := metadata.Pairs( + "tfc-address", "https://app.staging.terraform.io", + "tfc-base-path", "/api/v2/", + "tfc-display-hostname", "app.staging.terraform.io", + "tfc-token", "not-a-legit-token", + "tfc-organization", "example-corp", + "tfc-current-workspace", "example-space", + "tfc-workspace-name", "example-space", + // Actually combining -name and -tags is an invalid scenario from + // Terraform's point of view, but here we're just testing that every + // field makes the trip safely if sent. + "tfc-workspace-tags", "networking", + // Duplicate is on purpose. + "tfc-workspace-tags", "platform-team", + "tfc-default-project-name", "production-services", + ) + inputStruct := CloudPluginConfig{ + Address: "https://app.staging.terraform.io", + BasePath: "/api/v2/", + DisplayHostname: "app.staging.terraform.io", + Token: "not-a-legit-token", + Organization: "example-corp", + CurrentWorkspace: "example-space", + WorkspaceName: "example-space", + WorkspaceTags: []string{"networking", "platform-team"}, + DefaultProjectName: "production-services", + } + result := inputStruct.ToMetadata() + if !reflect.DeepEqual(expected, result) { + t.Fatalf("Expected: %#v\nGot: %#v\n", expected, result) + } +} diff --git a/internal/command/command.go b/internal/command/command.go index 41748d652f..d6a2aec036 100644 --- a/internal/command/command.go +++ b/internal/command/command.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -27,6 +30,10 @@ const DefaultPluginVendorDir = "terraform.d/plugins/" + pluginMachineName // DefaultStateFilename is the default filename used for the state file. const DefaultStateFilename = "terraform.tfstate" +// DefaultStatePersistInterval is the default interval a backend should persist +// Terraform state, if applicable. Backends can set their own custom defaults. +const DefaultStatePersistInterval = 20 + // DefaultVarsFilename is the default filename used for vars const DefaultVarsFilename = "terraform.tfvars" @@ -38,7 +45,7 @@ const DefaultBackupExtension = ".backup" const DefaultParallelism = 10 // ErrUnsupportedLocalOp is the common error message shown for operations -// that require a backend.Local. +// that require a backendrun.Local. const ErrUnsupportedLocalOp = `The configured backend doesn't support this operation. The "backend" in Terraform defines how Terraform operates. The default diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 09b2bb7930..f9437c989d 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -7,7 +10,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "github.com/google/go-cmp/cmp" "io" "io/ioutil" "net/http" @@ -20,8 +22,12 @@ import ( "syscall" "testing" + "github.com/google/go-cmp/cmp" + svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" backendInit "github.com/hashicorp/terraform/internal/backend/init" backendLocal "github.com/hashicorp/terraform/internal/backend/local" @@ -34,19 +40,17 @@ import ( "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/initwd" - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" _ "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terminal" - "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/version" - "github.com/zclconf/go-cty/cty" ) // These are the directories for our test data and fixtures. @@ -82,6 +86,7 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } +// tempWorkingDir constructs a workdir.Dir object referring to a newly-created // tempWorkingDir constructs a workdir.Dir object referring to a newly-created // temporary directory. The temporary directory is automatically removed when // the test and all its subtests complete. @@ -153,8 +158,8 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths // in a JSON file so that we can load them below. - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) - _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } @@ -190,7 +195,13 @@ func testPlan(t *testing.T) *plans.Plan { Type: "local", Config: backendConfigRaw, }, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), + + // We'll default to the fake plan being both applyable and complete, + // since that's what most tests expect. Tests can override these + // back to false again afterwards if they need to. + Applyable: true, + Complete: true, } } @@ -248,6 +259,24 @@ func testPlanFileNoop(t *testing.T) string { return testPlanFile(t, snap, state, plan) } +func testFileEquals(t *testing.T, got, want string) { + t.Helper() + + actual, err := os.ReadFile(got) + if err != nil { + t.Fatalf("error reading %s", got) + } + + expected, err := os.ReadFile(want) + if err != nil { + t.Fatalf("error reading %s", want) + } + + if diff := cmp.Diff(string(actual), string(expected)); len(diff) > 0 { + t.Fatalf("got:\n%s\nwant:\n%s\ndiff:\n%s", actual, expected, diff) + } +} + func testReadPlan(t *testing.T, path string) *plans.Plan { t.Helper() @@ -293,6 +322,53 @@ func testState() *states.State { }).DeepCopy() } +func testStateWithIdentity() *states.State { + return states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + // The weird whitespace here is reflective of how this would + // get written out in a real state file, due to the indentation + // of all of the containing wrapping objects and arrays. + AttrsJSON: []byte("{\n \"id\": \"foo\"\n }"), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{}, + IdentitySchemaVersion: 0, + IdentityJSON: []byte("{\n \"id\": \"my-foo-id\"\n }"), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte("{\n \"id\": \"bar\"\n }"), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{}, + IdentitySchemaVersion: 0, + IdentityJSON: []byte("{\n \"id\": \"my-bar-id\"\n }"), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + // DeepCopy is used here to ensure our synthetic state matches exactly + // with a state that will have been copied during the command + // operation, and all fields have been copied correctly. + }).DeepCopy() +} + // writeStateForTesting is a helper that writes the given naked state to the // given writer, generating a stub *statefile.File wrapper which is then // immediately discarded. @@ -331,7 +407,10 @@ func testStateMgrCurrentLineage(mgr statemgr.Persistent) string { // // (do stuff to the state) // assertStateHasMarker(state, mark) func markStateForMatching(state *states.State, mark string) string { - state.RootModule().SetOutputValue("testing_mark", cty.StringVal(mark), false) + state.SetOutputValue( + addrs.OutputValue{Name: "testing_mark"}.Absolute(addrs.RootModuleInstance), + cty.StringVal(mark), false, + ) return mark } @@ -339,7 +418,7 @@ func markStateForMatching(state *states.State, mark string) string { // mark string previously added to the given state. If no such mark is present, // the result is an empty string. func getStateMatchingMarker(state *states.State) string { - os := state.RootModule().OutputValues["testing_mark"] + os := state.RootOutputValues["testing_mark"] if os == nil { return "" } @@ -426,7 +505,7 @@ func testStateFileWorkspaceDefault(t *testing.T, workspace string, s *states.Sta // testStateFileRemote writes the state out to the remote statefile // in the cwd. Use `testCwd` to change into a temp cwd. -func testStateFileRemote(t *testing.T, s *legacy.State) string { +func testStateFileRemote(t *testing.T, s *workdir.BackendStateFile) string { t.Helper() path := filepath.Join(DefaultDataDir, DefaultStateFilename) @@ -434,14 +513,12 @@ func testStateFileRemote(t *testing.T, s *legacy.State) string { t.Fatalf("err: %s", err) } - f, err := os.Create(path) + raw, err := workdir.EncodeBackendStateFile(s) if err != nil { - t.Fatalf("err: %s", err) + t.Fatalf("encoding backend state file: %s", err) } - defer f.Close() - - if err := legacy.WriteState(s, f); err != nil { - t.Fatalf("err: %s", err) + if err := os.WriteFile(path, raw, os.ModePerm); err != nil { + t.Fatalf("writing backend state file: %s", err) } return path @@ -465,12 +542,9 @@ func testStateRead(t *testing.T, path string) *states.State { return sf.State } -// testDataStateRead reads a "data state", which is a file format resembling +// testDataStateRead reads a backend state, which is a file format resembling // our state format v3 that is used only to track current backend settings. -// -// This old format still uses *legacy.State, but should be replaced with -// a more specialized type in a later release. -func testDataStateRead(t *testing.T, path string) *legacy.State { +func testDataStateRead(t *testing.T, path string) *workdir.BackendStateFile { t.Helper() f, err := os.Open(path) @@ -479,7 +553,12 @@ func testDataStateRead(t *testing.T, path string) *legacy.State { } defer f.Close() - s, err := legacy.ReadState(f) + raw, err := io.ReadAll(f) + if err != nil { + t.Fatalf("err: %s", err) + } + + s, err := workdir.ParseBackendStateFile(raw) if err != nil { t.Fatalf("err: %s", err) } @@ -500,8 +579,8 @@ func testStateOutput(t *testing.T, path string, expected string) { } } -func testProvider() *terraform.MockProvider { - p := new(terraform.MockProvider) +func testProvider() *testing_provider.MockProvider { + p := new(testing_provider.MockProvider) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { resp.PlannedState = req.ProposedNewState return resp @@ -521,6 +600,12 @@ func testTempFile(t *testing.T) string { return filepath.Join(testTempDir(t), "state.tfstate") } +func testVarsFile(t *testing.T) string { + t.Helper() + + return filepath.Join(testTempDir(t), "variables.tfvars") +} + func testTempDir(t *testing.T) string { t.Helper() d, err := filepath.EvalSymlinks(t.TempDir()) @@ -713,7 +798,7 @@ func testInputMap(t *testing.T, answers map[string]string) func() { // be returned about the backend configuration having changed and that // "terraform init" must be run, since the test backend config cache created // by this function contains the hash for an empty configuration. -func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *httptest.Server) { +func testBackendState(t *testing.T, s *states.State, c int) (*workdir.BackendStateFile, *httptest.Server) { t.Helper() var b64md5 string @@ -753,8 +838,8 @@ func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *htt configSchema := b.ConfigSchema() hash := backendConfig.Hash(configSchema) - state := legacy.NewState() - state.Backend = &legacy.BackendState{ + state := workdir.NewBackendStateFile() + state.Backend = &workdir.BackendState{ Type: "http", ConfigRaw: json.RawMessage(fmt.Sprintf(`{"address":%q}`, srv.URL)), Hash: uint64(hash), @@ -766,10 +851,10 @@ func testBackendState(t *testing.T, s *states.State, c int) (*legacy.State, *htt // testRemoteState is used to make a test HTTP server to return a given // state file that can be used for testing legacy remote state. // -// The return values are a *legacy.State instance that should be written -// as the "data state" (really: backend state) and the server that the -// returned data state refers to. -func testRemoteState(t *testing.T, s *states.State, c int) (*legacy.State, *httptest.Server) { +// The return values are a [workdir.BackendStateFile] instance that should be +// written as the backend state and the server that the returned data state +// refers to. +func testRemoteState(t *testing.T, s *states.State, c int) (*workdir.BackendStateFile, *httptest.Server) { t.Helper() var b64md5 string @@ -789,10 +874,10 @@ func testRemoteState(t *testing.T, s *states.State, c int) (*legacy.State, *http resp.Write(buf.Bytes()) } - retState := legacy.NewState() + retState := workdir.NewBackendStateFile() srv := httptest.NewServer(http.HandlerFunc(cb)) - b := &legacy.BackendState{ + b := &workdir.BackendState{ Type: "http", } b.SetConfig(cty.ObjectVal(map[string]cty.Value{ @@ -1058,8 +1143,8 @@ func testView(t *testing.T) (*views.View, func(*testing.T) *terminal.TestOutput) // checkGoldenReference compares the given test output with a known "golden" output log // located under the specified fixture path. // -// If any of these tests fail, please communicate with Terraform Cloud folks before resolving, -// as changes to UI output may also affect the behavior of Terraform Cloud's structured run output. +// If any of these tests fail, please communicate with HCP Terraform folks before resolving, +// as changes to UI output may also affect the behavior of HCP Terraform's structured run output. func checkGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePathName string) { t.Helper() @@ -1087,8 +1172,8 @@ func checkGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePath if len(gotLines) != len(wantLines) { t.Errorf("unexpected number of log lines: got %d, want %d\n"+ - "NOTE: This failure may indicate a UI change affecting the behavior of structured run output on TFC.\n"+ - "Please communicate with Terraform Cloud team before resolving", len(gotLines), len(wantLines)) + "NOTE: This failure may indicate a UI change affecting the behavior of structured run output on HCP Terraform.\n"+ + "Please communicate with HCP Terraform team before resolving", len(gotLines), len(wantLines)) } // Verify that the log starts with a version message @@ -1140,6 +1225,6 @@ func checkGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePath if diff := cmp.Diff(wantLineMaps, gotLineMaps); diff != "" { t.Errorf("wrong output lines\n%s\n"+ "NOTE: This failure may indicate a UI change affecting the behavior of structured run output on TFC.\n"+ - "Please communicate with Terraform Cloud team before resolving", diff) + "Please communicate with HCP Terraform team before resolving", diff) } } diff --git a/internal/command/console.go b/internal/command/console.go index 2859fbc7d5..56bf62e0c1 100644 --- a/internal/command/console.go +++ b/internal/command/console.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,25 +9,29 @@ import ( "os" "strings" + "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/repl" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - - "github.com/mitchellh/cli" ) -// ConsoleCommand is a Command implementation that applies a Terraform -// configuration and actually builds or changes infrastructure. +// ConsoleCommand is a Command implementation that starts an interactive +// console that can be used to try expressions with the current config. type ConsoleCommand struct { Meta } func (c *ConsoleCommand) Run(args []string) int { args = c.Meta.process(args) + var evalFromPlan bool cmdFlags := c.Meta.extendedFlagSet("console") cmdFlags.StringVar(&c.Meta.statePath, "state", DefaultStateFilename, "path") + cmdFlags.BoolVar(&evalFromPlan, "plan", false, "evaluate from plan") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command line flags: %s\n", err.Error())) @@ -64,7 +71,7 @@ func (c *ConsoleCommand) Run(args []string) int { } // We require a local backend - local, ok := b.(backend.Local) + local, ok := b.(backendrun.Local) if !ok { c.showDiagnostics(diags) // in case of any warnings in here c.Ui.Error(ErrUnsupportedLocalOp) @@ -75,7 +82,7 @@ func (c *ConsoleCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(b) // Build the operation - opReq := c.Operation(b) + opReq := c.Operation(b, arguments.ViewHuman) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() opReq.AllowUnsetVariables = true // we'll just evaluate them as unknown @@ -117,19 +124,27 @@ func (c *ConsoleCommand) Run(args []string) int { ErrorWriter: os.Stderr, } - evalOpts := &terraform.EvalOpts{} - if lr.PlanOpts != nil { - // the LocalRun type is built primarily to support the main operations, - // so the variable values end up in the "PlanOpts" even though we're - // not actually making a plan. - evalOpts.SetVariables = lr.PlanOpts.SetVariables - } + var scope *lang.Scope + if evalFromPlan { + var planDiags tfdiags.Diagnostics + _, scope, planDiags = lr.Core.PlanAndEval(lr.Config, lr.InputState, lr.PlanOpts) + diags = diags.Append(planDiags) + } else { + evalOpts := &terraform.EvalOpts{} + if lr.PlanOpts != nil { + // the LocalRun type is built primarily to support the main operations, + // so the variable values end up in the "PlanOpts" even though we're + // not actually making a plan. + evalOpts.SetVariables = lr.PlanOpts.SetVariables + } - // Before we can evaluate expressions, we must compute and populate any - // derived values (input variables, local values, output values) - // that are not stored in the persistent state. - scope, scopeDiags := lr.Core.Eval(lr.Config, lr.InputState, addrs.RootModuleInstance, evalOpts) - diags = diags.Append(scopeDiags) + // Before we can evaluate expressions, we must compute and populate any + // derived values (input variables, local values, output values) + // that are not stored in the persistent state. + var scopeDiags tfdiags.Diagnostics + scope, scopeDiags = lr.Core.Eval(lr.Config, lr.InputState, addrs.RootModuleInstance, evalOpts) + diags = diags.Append(scopeDiags) + } if scope == nil { // scope is nil if there are errors so bad that we can't even build a scope. // Otherwise, we'll try to eval anyway. @@ -140,14 +155,21 @@ func (c *ConsoleCommand) Run(args []string) int { // set the ConsoleMode to true so any available console-only functions included. scope.ConsoleMode = true - if diags.HasErrors() { - diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results.")) - } - // Before we become interactive we'll show any diagnostics we encountered // during initialization, and then afterwards the driver will manage any // further diagnostics itself. + if diags.HasErrors() { + // showDiagnostics is designed to always render warnings first, but + // for this command we have one special warning that should always + // appear after everything else, to increase the chances that the + // user will notice it before they become confused by an incomplete + // expression result. + c.showDiagnostics(diags) + diags = nil + diags = diags.Append(tfdiags.SimpleWarning("Due to the problems above, some expressions may produce unexpected results.")) + } c.showDiagnostics(diags) + diags = nil // IO Loop session := &repl.Session{ @@ -205,6 +227,12 @@ Options: -state=path Legacy option for the local backend only. See the local backend's documentation for more information. + -plan Create a new plan (as if running "terraform plan") and + then evaluate expressions against its planned state, + instead of evaluating against the current state. + You can use this to inspect the effects of configuration + changes that haven't been applied yet. + -var 'foo=bar' Set a variable in the Terraform configuration. This flag can be set multiple times. diff --git a/internal/command/console_interactive.go b/internal/command/console_interactive.go index 32cc3a9efc..0d6e8b6eeb 100644 --- a/internal/command/console_interactive.go +++ b/internal/command/console_interactive.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !solaris // +build !solaris @@ -10,11 +13,12 @@ import ( "fmt" "io" "os" - - "github.com/hashicorp/terraform/internal/repl" + "strings" "github.com/chzyer/readline" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" + + "github.com/hashicorp/terraform/internal/repl" ) func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int { @@ -36,20 +40,55 @@ func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int { } defer l.Close() + // TODO: Currently we're handling multi-line input largely _in spite of_ + // the readline library, because it doesn't support that. This means that + // in particular the history treats each line as a separate history entry, + // and doesn't allow editing of previous lines after the user's already + // pressed enter. + // + // Hopefully we can do better than this one day, but having some basic + // support for multi-line input is at least better than none at all: + // this is mainly helpful when pasting in expressions from elsewhere that + // already have newline characters in them, to avoid pre-editing it. + + lines := make([]string, 0, 4) for { // Read a line + if len(lines) == 0 { + l.SetPrompt("> ") + } else { + l.SetPrompt(": ") + } line, err := l.Readline() if err == readline.ErrInterrupt { - if len(line) == 0 { + if len(lines) == 0 && line == "" { break + } else if line != "" { + continue } else { + // Reset the entry buffer to start a new expression + lines = lines[:0] + ui.Output("(multi-line entry canceled)") continue } } else if err == io.EOF { break } + lines = append(lines, line) + // The following implements a heuristic for deciding if it seems likely + // that the user was intending to continue entering more expression + // characters on a subsequent line. This should get the right answer + // for any valid expression, but might get confused by invalid input. + // The user can always hit enter one more time (entering a blank line) + // to break out of a multi-line sequence and force interpretation of + // what was already entered. + if repl.ExpressionEntryCouldContinue(lines) { + continue + } - out, exit, diags := session.Handle(line) + input := strings.Join(lines, "\n") + "\n" + lines = lines[:0] // reset for next iteration + out, exit, diags := session.Handle(input) if diags.HasErrors() { c.showDiagnostics(diags) } diff --git a/internal/command/console_interactive_solaris.go b/internal/command/console_interactive_solaris.go index b6e5d4d73f..49c3468c8b 100644 --- a/internal/command/console_interactive_solaris.go +++ b/internal/command/console_interactive_solaris.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build solaris // +build solaris @@ -6,8 +9,8 @@ package command import ( "fmt" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/repl" - "github.com/mitchellh/cli" ) func (c *ConsoleCommand) modeInteractive(session *repl.Session, ui cli.Ui) int { diff --git a/internal/command/console_test.go b/internal/command/console_test.go index 13b743547e..c54a7651da 100644 --- a/internal/command/console_test.go +++ b/internal/command/console_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -7,9 +10,9 @@ import ( "strings" "testing" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" - "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" ) @@ -66,7 +69,7 @@ func TestConsole_tfvars(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -118,7 +121,7 @@ func TestConsole_unsetRequiredVars(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -172,8 +175,8 @@ func TestConsole_variables(t *testing.T) { commands := map[string]string{ "var.foo\n": "\"bar\"\n", "var.snack\n": "\"popcorn\"\n", - "var.secret_snack\n": "(sensitive)\n", - "local.snack_bar\n": "[\n \"popcorn\",\n (sensitive),\n]\n", + "var.secret_snack\n": "(sensitive value)\n", + "local.snack_bar\n": "[\n \"popcorn\",\n (sensitive value),\n]\n", } args := []string{} @@ -236,3 +239,46 @@ func TestConsole_modules(t *testing.T) { } } } + +func TestConsole_modulesPlan(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("apply"), td) + defer testChdir(t, td)() + + p := applyFixtureProvider() + ui := cli.NewMockUi() + view, _ := testView(t) + + c := &ConsoleCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + View: view, + }, + } + + commands := map[string]string{ + "test_instance.foo.ami\n": "\"bar\"\n", + } + + // The -plan option means that we'll be evaluating expressions against + // a plan constructed from this configuration, instead of against its + // (non-existent) prior state. + args := []string{"-plan"} + + for cmd, val := range commands { + var output bytes.Buffer + defer testStdinPipe(t, strings.NewReader(cmd))() + outCloser := testStdoutCapture(t, &output) + code := c.Run(args) + outCloser() + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + actual := output.String() + if output.String() != val { + t.Fatalf("bad: %q, expected %q", actual, val) + } + } +} diff --git a/internal/command/e2etest/automation_test.go b/internal/command/e2etest/automation_test.go index 02652927d7..edc5a6c47c 100644 --- a/internal/command/e2etest/automation_test.go +++ b/internal/command/e2etest/automation_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( @@ -233,7 +236,7 @@ func TestPlanOnlyInAutomation(t *testing.T) { } // Because we're running with TF_IN_AUTOMATION set, we should not see - // any mention of the the "terraform apply" command in the output. + // any mention of the "terraform apply" command in the output. if strings.Contains(stdout, "terraform apply") { t.Errorf("unwanted mention of \"terraform apply\" in plan output\n%s", stdout) } diff --git a/internal/command/e2etest/doc.go b/internal/command/e2etest/doc.go index 056a43aa3f..2e0a521e69 100644 --- a/internal/command/e2etest/doc.go +++ b/internal/command/e2etest/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package e2etest contains a set of tests that run against a real Terraform // binary, compiled on the fly at the start of the test run. // diff --git a/internal/command/e2etest/init_test.go b/internal/command/e2etest/init_test.go index 371ee0e1e9..abe2fb111d 100644 --- a/internal/command/e2etest/init_test.go +++ b/internal/command/e2etest/init_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( @@ -146,7 +149,7 @@ func TestInitProvidersLocalOnly(t *testing.T) { // not work until you remove it. // // To avoid this, we will "zero out" any existing cli config file. - tf.AddEnv("TF_CLI_CONFIG_FILE=\"\"") + tf.AddEnv("TF_CLI_CONFIG_FILE=") // Our fixture dir has a generic os_arch dir, which we need to customize // to the actual OS/arch where this test is running in order to get the @@ -371,14 +374,13 @@ func TestInitProviderNotFound(t *testing.T) { │ Could not retrieve the list of available versions for provider │ hashicorp/nonexist: provider registry registry.terraform.io does not have a │ provider named registry.terraform.io/hashicorp/nonexist -│ +│` + ` ` + ` │ All modules should specify their required_providers so that external │ consumers will get the correct providers when using a module. To see which │ modules are currently depending on hashicorp/nonexist, run the following │ command: │ terraform providers ╵ - ` if stripAnsi(stderr) != expectedErr { t.Errorf("wrong output:\n%s", cmp.Diff(stripAnsi(stderr), expectedErr)) diff --git a/internal/command/e2etest/main_test.go b/internal/command/e2etest/main_test.go index 3c9ba5a5e1..9aca3ec5a9 100644 --- a/internal/command/e2etest/main_test.go +++ b/internal/command/e2etest/main_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/make-archive.sh b/internal/command/e2etest/make-archive.sh index 227cc7f0f9..fba2c5c1c6 100755 --- a/internal/command/e2etest/make-archive.sh +++ b/internal/command/e2etest/make-archive.sh @@ -1,4 +1,7 @@ #!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + # For normal use this package can just be tested with "go test" as standard, # but this script is an alternative to allow the tests to be run somewhere diff --git a/internal/command/e2etest/module_archive_test.go b/internal/command/e2etest/module_archive_test.go index cb6a2979fd..a2b77037d3 100644 --- a/internal/command/e2etest/module_archive_test.go +++ b/internal/command/e2etest/module_archive_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/primary_test.go b/internal/command/e2etest/primary_test.go index bd570dd4ff..c7b6eaa1ee 100644 --- a/internal/command/e2etest/primary_test.go +++ b/internal/command/e2etest/primary_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( @@ -201,13 +204,13 @@ func TestPrimaryChdirOption(t *testing.T) { t.Fatalf("failed to read state file: %s", err) } - gotOutput := state.RootModule().OutputValues["cwd"] + gotOutput := state.RootOutputValues["cwd"] wantOutputValue := cty.StringVal(filepath.ToSlash(tf.Path())) // path.cwd returns the original path, because path.root is how we get the overridden path if gotOutput == nil || !wantOutputValue.RawEquals(gotOutput.Value) { t.Errorf("incorrect value for cwd output\ngot: %#v\nwant Value: %#v", gotOutput, wantOutputValue) } - gotOutput = state.RootModule().OutputValues["root"] + gotOutput = state.RootOutputValues["root"] wantOutputValue = cty.StringVal(filepath.ToSlash(tf.Path("subdir"))) // path.root is a relative path, but the text fixture uses abspath on it. if gotOutput == nil || !wantOutputValue.RawEquals(gotOutput.Value) { t.Errorf("incorrect value for root output\ngot: %#v\nwant Value: %#v", gotOutput, wantOutputValue) diff --git a/internal/command/e2etest/provider_dev_test.go b/internal/command/e2etest/provider_dev_test.go index 1f6c9fb0d9..04561d0256 100644 --- a/internal/command/e2etest/provider_dev_test.go +++ b/internal/command/e2etest/provider_dev_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/provider_plugin_test.go b/internal/command/e2etest/provider_plugin_test.go index 49fa793dc1..9d8fa3e956 100644 --- a/internal/command/e2etest/provider_plugin_test.go +++ b/internal/command/e2etest/provider_plugin_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/providers_mirror_test.go b/internal/command/e2etest/providers_mirror_test.go index f237fa6513..12552882c9 100644 --- a/internal/command/e2etest/providers_mirror_test.go +++ b/internal/command/e2etest/providers_mirror_test.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( "os" "path/filepath" "sort" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -15,59 +19,97 @@ import ( // interacts directly with Terraform Registry and the full details of that are // tricky to mock. Such a mock is _possible_, but we're using e2etest as a // compromise for now to keep these tests relatively simple. - func TestTerraformProvidersMirror(t *testing.T) { // This test reaches out to releases.hashicorp.com to download the // template and null providers, so it can only run if network access is // allowed. skipIfCannotAccessNetwork(t) - outputDir := t.TempDir() - t.Logf("creating mirror directory in %s", outputDir) + for _, test := range []struct { + name string + args []string + err string + }{ + { + name: "terraform-providers-mirror", + args: []string{"-platform=linux_amd64", "-platform=windows_386"}, + }, + { + name: "terraform-providers-mirror-with-lock-file", + args: []string{"-platform=linux_amd64", "-platform=windows_386"}, + }, + { + // should ignore lock file + name: "terraform-providers-mirror-with-broken-lock-file", + args: []string{"-platform=linux_amd64", "-platform=windows_386", "-lock-file=false"}, + }, + { + name: "terraform-providers-mirror-with-broken-lock-file", + args: []string{"-platform=linux_amd64", "-platform=windows_386", "-lock-file=true"}, + err: "Inconsistent dependency lock file", + }, + } { + t.Run(test.name, func(t *testing.T) { + outputDir := t.TempDir() + t.Logf("creating mirror directory in %s", outputDir) - fixturePath := filepath.Join("testdata", "terraform-providers-mirror") - tf := e2e.NewBinary(t, terraformBin, fixturePath) + fixturePath := filepath.Join("testdata", test.name) + tf := e2e.NewBinary(t, terraformBin, fixturePath) - stdout, stderr, err := tf.Run("providers", "mirror", "-platform=linux_amd64", "-platform=windows_386", outputDir) - if err != nil { - t.Fatalf("unexpected error: %s\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr) - } + args := []string{"providers", "mirror"} + args = append(args, test.args...) + args = append(args, outputDir) - // The test fixture includes exact version constraints for the two - // providers it depends on so that the following should remain stable. - // In the (unlikely) event that these particular versions of these - // providers are removed from the registry, this test will start to fail. - want := []string{ - "registry.terraform.io/hashicorp/null/2.1.0.json", - "registry.terraform.io/hashicorp/null/index.json", - "registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip", - "registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_windows_386.zip", - "registry.terraform.io/hashicorp/template/2.1.1.json", - "registry.terraform.io/hashicorp/template/index.json", - "registry.terraform.io/hashicorp/template/terraform-provider-template_2.1.1_linux_amd64.zip", - "registry.terraform.io/hashicorp/template/terraform-provider-template_2.1.1_windows_386.zip", - } - var got []string - walkErr := filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - if info.IsDir() { - return nil // we only care about leaf files for this test - } - relPath, err := filepath.Rel(outputDir, path) - if err != nil { - return err - } - got = append(got, filepath.ToSlash(relPath)) - return nil - }) - if walkErr != nil { - t.Fatal(walkErr) - } - sort.Strings(got) + stdout, stderr, err := tf.Run(args...) + if test.err != "" { + if !strings.Contains(stderr, test.err) { + t.Fatalf("expected error %q, got %q\n", test.err, stderr) + } + return + } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("unexpected files in result\n%s", diff) + if err != nil { + t.Fatalf("unexpected error: %s\nstdout:\n%s\nstderr:\n%s", err, stdout, stderr) + } + + // The test fixture includes exact version constraints for the two + // providers it depends on so that the following should remain stable. + // In the (unlikely) event that these particular versions of these + // providers are removed from the registry, this test will start to fail. + want := []string{ + "registry.terraform.io/hashicorp/null/2.1.0.json", + "registry.terraform.io/hashicorp/null/index.json", + "registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_linux_amd64.zip", + "registry.terraform.io/hashicorp/null/terraform-provider-null_2.1.0_windows_386.zip", + "registry.terraform.io/hashicorp/template/2.1.1.json", + "registry.terraform.io/hashicorp/template/index.json", + "registry.terraform.io/hashicorp/template/terraform-provider-template_2.1.1_linux_amd64.zip", + "registry.terraform.io/hashicorp/template/terraform-provider-template_2.1.1_windows_386.zip", + } + var got []string + walkErr := filepath.Walk(outputDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil // we only care about leaf files for this test + } + relPath, err := filepath.Rel(outputDir, path) + if err != nil { + return err + } + got = append(got, filepath.ToSlash(relPath)) + return nil + }) + if walkErr != nil { + t.Fatal(walkErr) + } + sort.Strings(got) + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("unexpected files in result\n%s", diff) + } + + }) } } diff --git a/internal/command/e2etest/providers_schema_test.go b/internal/command/e2etest/providers_schema_test.go new file mode 100644 index 0000000000..7a5d7dea8d --- /dev/null +++ b/internal/command/e2etest/providers_schema_test.go @@ -0,0 +1,265 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package e2etest + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/e2e" + "github.com/hashicorp/terraform/internal/getproviders" +) + +// TestProvidersSchema is a test for `provider schemas -json` subcommand +// which effectively tests much of the schema-related logic underneath +func TestProvidersSchema(t *testing.T) { + if !canRunGoBuild { + // We're running in a separate-build-then-run context, so we can't + // currently execute this test which depends on being able to build + // new executable at runtime. + // + // (See the comment on canRunGoBuild's declaration for more information.) + t.Skip("can't run without building a new provider executable") + } + t.Parallel() + + tf := e2e.NewBinary(t, terraformBin, "testdata/provider-plugin") + + // In order to do a decent end-to-end test for this case we will need a real + // enough provider plugin to try to run and make sure we are able to + // actually run it. Here will build the simple and simple6 (built with + // protocol v6) providers. + simple6Provider := filepath.Join(tf.WorkDir(), "terraform-provider-simple6") + simple6ProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple-v6/main", simple6Provider) + + simpleProvider := filepath.Join(tf.WorkDir(), "terraform-provider-simple") + simpleProviderExe := e2e.GoBuild("github.com/hashicorp/terraform/internal/provider-simple/main", simpleProvider) + + // Move the provider binaries into a directory that we will point terraform + // to using the -plugin-dir cli flag. + platform := getproviders.CurrentPlatform.String() + hashiDir := "cache/registry.terraform.io/hashicorp/" + if err := os.MkdirAll(tf.Path(hashiDir, "simple6/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simple6ProviderExe, tf.Path(hashiDir, "simple6/0.0.1/", platform, "terraform-provider-simple6")); err != nil { + t.Fatal(err) + } + + if err := os.MkdirAll(tf.Path(hashiDir, "simple/0.0.1/", platform), os.ModePerm); err != nil { + t.Fatal(err) + } + if err := os.Rename(simpleProviderExe, tf.Path(hashiDir, "simple/0.0.1/", platform, "terraform-provider-simple")); err != nil { + t.Fatal(err) + } + + //// INIT + _, stderr, err := tf.Run("init", "-plugin-dir=cache") + if err != nil { + t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) + } + + expectedRawOutput := `{ + "format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/simple": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, + "ephemeral_resource_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, + "resource_identity_schemas": { + "simple_resource": { + "version": 0, + "attributes": { + "id": { + "type": "string", + "required_for_import": true + } + } + } + } + }, + "registry.terraform.io/hashicorp/simple6": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, + "ephemeral_resource_schemas": { + "simple_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description_kind": "plain", + "computed": true + }, + "value": { + "type": "string", + "description_kind": "plain", + "optional": true + } + }, + "description_kind": "plain" + } + } + }, + "functions": { + "noop": { + "description": "noop takes any single argument and returns the same value", + "return_type": "dynamic", + "parameters": [ + { + "name": "noop", + "description": "any value", + "is_nullable": true, + "type": "dynamic" + } + ] + } + }, + "resource_identity_schemas": { + "simple_resource": { + "version": 0, + "attributes": { + "id": { + "type": "string", + "required_for_import": true + } + } + } + } + } + } +} +` + var expectedOutput bytes.Buffer + err = json.Compact(&expectedOutput, []byte(expectedRawOutput)) + if err != nil { + t.Fatal(err) + } + + stdout, stderr, err := tf.Run("providers", "schema", "-json") + if err != nil { + t.Fatalf("unexpected error: %s\n%s", err, stderr) + } + + var output bytes.Buffer + err = json.Compact(&output, []byte(stdout)) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(expectedOutput.String(), output.String()); diff != "" { + t.Fatalf("unexpected schema: %s\n", diff) + } +} diff --git a/internal/command/e2etest/providers_tamper_test.go b/internal/command/e2etest/providers_tamper_test.go index 0f4e831220..7a8b01a1dd 100644 --- a/internal/command/e2etest/providers_tamper_test.go +++ b/internal/command/e2etest/providers_tamper_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/provisioner_plugin_test.go b/internal/command/e2etest/provisioner_plugin_test.go index 3f5d312b21..4da61bbac9 100644 --- a/internal/command/e2etest/provisioner_plugin_test.go +++ b/internal/command/e2etest/provisioner_plugin_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/provisioner_test.go b/internal/command/e2etest/provisioner_test.go index 63d53576eb..18df32f7f9 100644 --- a/internal/command/e2etest/provisioner_test.go +++ b/internal/command/e2etest/provisioner_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/remote_state_test.go b/internal/command/e2etest/remote_state_test.go index 16b9d5a3f7..12cca47c2c 100644 --- a/internal/command/e2etest/remote_state_test.go +++ b/internal/command/e2etest/remote_state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/strip_ansi.go b/internal/command/e2etest/strip_ansi.go index 22b66bae37..7c958ceb6d 100644 --- a/internal/command/e2etest/strip_ansi.go +++ b/internal/command/e2etest/strip_ansi.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/e2etest/terraform_provider_funcs_test.go b/internal/command/e2etest/terraform_provider_funcs_test.go new file mode 100644 index 0000000000..7b6d4ac65e --- /dev/null +++ b/internal/command/e2etest/terraform_provider_funcs_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package e2etest + +import ( + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/e2e" +) + +func TestTerraformProviderFunctions(t *testing.T) { + // This test ensures that the terraform.io/builtin/terraform provider + // remains available and that its three functions are available to be + // called. This test is here because builtin providers are a bit of a + // special case in the CLI layer which could in principle get accidentally + // broken there even with deeper tests in the provider package itself + // still passing. + // + // The tests in the provider's own package are authoritative for the + // expected behavior of the functions. This test is focused on whether + // the functions can be called at all, though it does some very light + // testing of results for one specific input each. If the functions + // are intentionally changed to produce different results for those + // inputs in future then it may be appropriate to just update these + // tests to match. + + t.Parallel() + fixturePath := filepath.Join("testdata", "terraform-provider-funcs") + tf := e2e.NewBinary(t, terraformBin, fixturePath) + + //// INIT + _, stderr, err := tf.Run("init") + if err != nil { + t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) + } + + //// PLAN + _, stderr, err = tf.Run("plan", "-out=tfplan") + if err != nil { + t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) + } + + // The saved plan should include three planned output values containing + // results from our function calls. + plan, err := tf.Plan("tfplan") + if err != nil { + t.Fatalf("can't reload saved plan: %s", err) + } + + gotOutputs := make(map[string]cty.Value, 3) + for _, outputSrc := range plan.Changes.Outputs { + output, err := outputSrc.Decode() + if err != nil { + t.Fatalf("can't decode planned change for %s: %s", outputSrc.Addr, err) + } + gotOutputs[output.Addr.String()] = output.After + } + wantOutputs := map[string]cty.Value{ + "output.exprencode": cty.StringVal(`[1, 2, 3]`), + "output.tfvarsdecode": cty.ObjectVal(map[string]cty.Value{ + "baaa": cty.StringVal("🐑"), + "boop": cty.StringVal("👃"), + }), + "output.tfvarsencode": cty.StringVal(`a = "👋" +b = "🐝" +c = "👓" +`), + } + if diff := cmp.Diff(wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong output values\n%s", diff) + } +} diff --git a/internal/command/e2etest/terraform_test.go b/internal/command/e2etest/terraform_test.go new file mode 100644 index 0000000000..29b99ba5c3 --- /dev/null +++ b/internal/command/e2etest/terraform_test.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package e2etest + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/hashicorp/terraform/internal/e2e" +) + +func TestTerraformProviderData(t *testing.T) { + + fixturePath := filepath.Join("testdata", "terraform-managed-data") + tf := e2e.NewBinary(t, terraformBin, fixturePath) + + _, stderr, err := tf.Run("init", "-input=false") + if err != nil { + t.Fatalf("unexpected init error: %s\nstderr:\n%s", err, stderr) + } + + stdout, stderr, err := tf.Run("plan", "-out=tfplan", "-input=false") + if err != nil { + t.Fatalf("unexpected plan error: %s\nstderr:\n%s", err, stderr) + } + + if !strings.Contains(stdout, "4 to add, 0 to change, 0 to destroy") { + t.Errorf("incorrect plan tally; want 4 to add:\n%s", stdout) + } + + stdout, stderr, err = tf.Run("apply", "-input=false", "tfplan") + if err != nil { + t.Fatalf("unexpected apply error: %s\nstderr:\n%s", err, stderr) + } + + if !strings.Contains(stdout, "Resources: 4 added, 0 changed, 0 destroyed") { + t.Errorf("incorrect apply tally; want 4 added:\n%s", stdout) + } + + state, err := tf.LocalState() + if err != nil { + t.Fatalf("failed to read state file: %s", err) + } + + // we'll check the final output to validate the resources + d := state.RootOutputValues["d"].Value + input := d.GetAttr("input") + output := d.GetAttr("output") + if input.IsNull() { + t.Fatal("missing input from resource d") + } + if !input.RawEquals(output) { + t.Fatalf("input %#v does not equal output %#v\n", input, output) + } +} diff --git a/internal/command/e2etest/testdata/custom-provider-install-method/main.tf b/internal/command/e2etest/testdata/custom-provider-install-method/main.tf index a521cf07bc..12ef6b3a10 100644 --- a/internal/command/e2etest/testdata/custom-provider-install-method/main.tf +++ b/internal/command/e2etest/testdata/custom-provider-install-method/main.tf @@ -9,7 +9,7 @@ # # For this test in particular we're using the "vendor" directory that is # the documented way to include provider plugins directly inside a -# configuration uploaded to Terraform Cloud, but this functionality applies +# configuration uploaded to HCP Terraform, but this functionality applies # to all of the implicit local filesystem search directories. terraform { diff --git a/internal/command/e2etest/testdata/local-only-provider/main.tf b/internal/command/e2etest/testdata/local-only-provider/main.tf index a521cf07bc..12ef6b3a10 100644 --- a/internal/command/e2etest/testdata/local-only-provider/main.tf +++ b/internal/command/e2etest/testdata/local-only-provider/main.tf @@ -9,7 +9,7 @@ # # For this test in particular we're using the "vendor" directory that is # the documented way to include provider plugins directly inside a -# configuration uploaded to Terraform Cloud, but this functionality applies +# configuration uploaded to HCP Terraform, but this functionality applies # to all of the implicit local filesystem search directories. terraform { diff --git a/internal/command/e2etest/testdata/plugin-cache/.terraform.lock.hcl b/internal/command/e2etest/testdata/plugin-cache/.terraform.lock.hcl new file mode 100644 index 0000000000..a96e3e4f05 --- /dev/null +++ b/internal/command/e2etest/testdata/plugin-cache/.terraform.lock.hcl @@ -0,0 +1,14 @@ +# The global cache is only an eligible installation source if there's already +# a lock entry for the given provider and it contains at least one checksum +# that matches the cache entry. +# +# This lock file therefore matches the "not a real provider" fake executable +# under the "cache" directory, rather than the real provider from upstream, +# so that Terraform CLI will consider the cache entry as valid. + +provider "registry.terraform.io/hashicorp/template" { + version = "2.1.0" + hashes = [ + "h1:e7YvVlRZlaZJ8ED5KnH0dAg0kPL0nAU7eEoCAZ/sOos=", + ] +} diff --git a/internal/command/e2etest/testdata/terraform-managed-data/main.tf b/internal/command/e2etest/testdata/terraform-managed-data/main.tf new file mode 100644 index 0000000000..271888e6a1 --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-managed-data/main.tf @@ -0,0 +1,18 @@ +resource "terraform_data" "a" { +} + +resource "terraform_data" "b" { + input = terraform_data.a.id +} + +resource "terraform_data" "c" { + triggers_replace = terraform_data.b +} + +resource "terraform_data" "d" { + input = [ terraform_data.b, terraform_data.c ] +} + +output "d" { + value = terraform_data.d +} diff --git a/internal/command/e2etest/testdata/terraform-provider-funcs/terraform-provider-funcs.tf b/internal/command/e2etest/testdata/terraform-provider-funcs/terraform-provider-funcs.tf new file mode 100644 index 0000000000..9875e47f7b --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-provider-funcs/terraform-provider-funcs.tf @@ -0,0 +1,35 @@ +# This test fixture is here primarily just to make sure that the +# terraform.io/builtin/terraform functions remain available for use. The +# actual behavior of these functions is the responsibility of +# ./internal/builtin/providers/terraform, and so it has more detailed tests +# whereas this one is focused largely just on whether these functions are +# callable at all. + +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} + +output "tfvarsencode" { + value = provider::terraform::encode_tfvars({ + a = "👋" + b = "🐝" + c = "👓" + }) +} + +output "tfvarsdecode" { + value = provider::terraform::decode_tfvars( + <<-EOT + boop = "👃" + baaa = "🐑" + EOT + ) +} + +output "exprencode" { + value = provider::terraform::encode_expr([1, 2, 3]) +} diff --git a/internal/command/e2etest/testdata/terraform-providers-mirror-with-broken-lock-file/.terraform.lock.hcl b/internal/command/e2etest/testdata/terraform-providers-mirror-with-broken-lock-file/.terraform.lock.hcl new file mode 100644 index 0000000000..7e16d31628 --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-providers-mirror-with-broken-lock-file/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/template" { + version = "2.1.1" + constraints = "2.1.1" + hashes = [ + "h1:fBNBluCX4pWlYEw5ZyCTHB00E+3BDSe7GjRzF1ojcvU=", + "h1:x2/zuJFN/oOUpE1C1nSk4n86AA2zASOyy2BUdFYcpXw=", + "zh:05fddf3cacb607f623c2b221c3e9ab724079deca0b703b2738e9d55c10e31717", + "zh:1a250b29274f3e340ea775bf9bd57476e982bca1fb4b59343fb3126e75dfd85c", + "zh:284735b9bd0e416ec02c0844e7f4ebbd4b5744140a21606e33f16eb14640cbf1", + "zh:2e9d246094ac8a68951015d40f42145e795b31d7c84fee20fa9f997b3d428906", + "zh:65e8e73860662a0c0698c8a8d35c857302f1fe3f41947e7c048c49a541a9c7f1", + "zh:70dacd22d0c93b2000948c06ded67fa147d992a0353737438f24a61e3f956c41", + "zh:aa1a0321e79e08ffb52789ab0af3896c493d436de7396d154d09a0be7d5d50e1", + "zh:bea4c276c4df9d117f19c4266d060db9b48c865ac7a71d2e77a27866c19bfaf5", + "zh:de04cb0cb046dad184f5bb783659cf98d88c6798db038cbf5a2c3c08e853d444", + "zh:de3c45a4fa1f756aa4db3350c021d1c0f9b23640cff77e0ba4df4eeb8eae957f", + "zh:e3cf2db204f64ad4e288af00fabc6a8af13a6687aba60a7e1ce0ea215a9580b1", + "zh:f795833225207d2eee022b91d26bee18d5e518e70912dd7a1d2a0eff2cbe4f1d", + ] +} + +provider "registry.terraform.io/hashicorp/broken" { + version = "2.1.1" + constraints = "2.1.1" + hashes = [ + "h1:fBNBluCX4pWlYEw5ZyCTHB00E+3BDSe7GjRzF1ojcvU=", + "h1:x2/zuJFN/oOUpE1C1nSk4n86AA2zASOyy2BUdFYcpXw=", + "zh:05fddf3cacb607f623c2b221c3e9ab724079deca0b703b2738e9d55c10e31717", + "zh:1a250b29274f3e340ea775bf9bd57476e982bca1fb4b59343fb3126e75dfd85c", + "zh:284735b9bd0e416ec02c0844e7f4ebbd4b5744140a21606e33f16eb14640cbf1", + "zh:2e9d246094ac8a68951015d40f42145e795b31d7c84fee20fa9f997b3d428906", + "zh:65e8e73860662a0c0698c8a8d35c857302f1fe3f41947e7c048c49a541a9c7f1", + "zh:70dacd22d0c93b2000948c06ded67fa147d992a0353737438f24a61e3f956c41", + "zh:aa1a0321e79e08ffb52789ab0af3896c493d436de7396d154d09a0be7d5d50e1", + "zh:bea4c276c4df9d117f19c4266d060db9b48c865ac7a71d2e77a27866c19bfaf5", + "zh:de04cb0cb046dad184f5bb783659cf98d88c6798db038cbf5a2c3c08e853d444", + "zh:de3c45a4fa1f756aa4db3350c021d1c0f9b23640cff77e0ba4df4eeb8eae957f", + "zh:e3cf2db204f64ad4e288af00fabc6a8af13a6687aba60a7e1ce0ea215a9580b1", + "zh:f795833225207d2eee022b91d26bee18d5e518e70912dd7a1d2a0eff2cbe4f1d", + ] +} diff --git a/internal/command/e2etest/testdata/terraform-providers-mirror-with-broken-lock-file/terraform-providers-mirror.tf b/internal/command/e2etest/testdata/terraform-providers-mirror-with-broken-lock-file/terraform-providers-mirror.tf new file mode 100644 index 0000000000..4b31e03012 --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-providers-mirror-with-broken-lock-file/terraform-providers-mirror.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + template = { version = "2.1.1" } + null = { source = "hashicorp/null", version = "2.1.0" } + terraform = { source = "terraform.io/builtin/terraform" } + } +} diff --git a/internal/command/e2etest/testdata/terraform-providers-mirror-with-lock-file/.terraform.lock.hcl b/internal/command/e2etest/testdata/terraform-providers-mirror-with-lock-file/.terraform.lock.hcl new file mode 100644 index 0000000000..64d78c2e31 --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-providers-mirror-with-lock-file/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "2.1.0" + constraints = "2.1.0" + hashes = [ + "h1:J/XPKw4nOAsE0iHHqkR0oIBfchtt3pokNj4gFlHqVvk=", + "h1:uugNjv4FEabvXfifTzRCqSerdraltZR0UwXzH8QYPUQ=", + "zh:022eb9cefb72d25cb39aebf17787ae5a1a239544abae7ac11fdc2b5a464c06f8", + "zh:089aec7ba6b9843741fec84e0bc046d97d2e41a9fedbe5d77124e66227395c63", + "zh:09e9a6fe88e8d33e4656a4f3768275c0f959f4624886a3a96d250e1067afec8c", + "zh:0fa2d6a05874405eb8b2a7ececb6b7522be25642e31838d23620bf7b4f371c9d", + "zh:2a7ab2f42d86e8bd4db3cdf94287a6d91c61456b59a0ce2d0f5d6992a08b668b", + "zh:6526bfa4f547223d4a14d7bf9098a4f7177a5c886a7edc65056df1cb98f6aad9", + "zh:8e58a5a130d377e8fc0da8ad526f33738c320b19463679f7d68212c5c939bad4", + "zh:9dc5be5713fca7dbfa99e9673450aaa7216915bffbc043b30798e037a8f2c870", + "zh:ab7671e33198b718a1ae3272dcea0380f357926324f96c3be0c6ef9423ebece1", + "zh:b27db66404ea0704fb076ef26bb5b5c556a31b81a8b2302ec705a7e46d93d3e0", + "zh:bcc4a07ce1fb3bdee4ea360dd9549e099ecc2e9d80aab7f8daf54387a87a5f8e", + "zh:bf44f8693075f46ae833303fee17e0b0649c72e9347027670fa30e9fbce37fc4", + ] +} + +provider "registry.terraform.io/hashicorp/template" { + version = "2.1.1" + constraints = "2.1.1" + hashes = [ + "h1:fBNBluCX4pWlYEw5ZyCTHB00E+3BDSe7GjRzF1ojcvU=", + "h1:x2/zuJFN/oOUpE1C1nSk4n86AA2zASOyy2BUdFYcpXw=", + "zh:05fddf3cacb607f623c2b221c3e9ab724079deca0b703b2738e9d55c10e31717", + "zh:1a250b29274f3e340ea775bf9bd57476e982bca1fb4b59343fb3126e75dfd85c", + "zh:284735b9bd0e416ec02c0844e7f4ebbd4b5744140a21606e33f16eb14640cbf1", + "zh:2e9d246094ac8a68951015d40f42145e795b31d7c84fee20fa9f997b3d428906", + "zh:65e8e73860662a0c0698c8a8d35c857302f1fe3f41947e7c048c49a541a9c7f1", + "zh:70dacd22d0c93b2000948c06ded67fa147d992a0353737438f24a61e3f956c41", + "zh:aa1a0321e79e08ffb52789ab0af3896c493d436de7396d154d09a0be7d5d50e1", + "zh:bea4c276c4df9d117f19c4266d060db9b48c865ac7a71d2e77a27866c19bfaf5", + "zh:de04cb0cb046dad184f5bb783659cf98d88c6798db038cbf5a2c3c08e853d444", + "zh:de3c45a4fa1f756aa4db3350c021d1c0f9b23640cff77e0ba4df4eeb8eae957f", + "zh:e3cf2db204f64ad4e288af00fabc6a8af13a6687aba60a7e1ce0ea215a9580b1", + "zh:f795833225207d2eee022b91d26bee18d5e518e70912dd7a1d2a0eff2cbe4f1d", + ] +} diff --git a/internal/command/e2etest/testdata/terraform-providers-mirror-with-lock-file/terraform-providers-mirror.tf b/internal/command/e2etest/testdata/terraform-providers-mirror-with-lock-file/terraform-providers-mirror.tf new file mode 100644 index 0000000000..1598a27835 --- /dev/null +++ b/internal/command/e2etest/testdata/terraform-providers-mirror-with-lock-file/terraform-providers-mirror.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + template = { source = "hashicorp/template" } + null = { source = "hashicorp/null" } + terraform = { source = "terraform.io/builtin/terraform" } + } +} diff --git a/internal/command/e2etest/unmanaged_test.go b/internal/command/e2etest/unmanaged_test.go index e67e758a33..e3e2335f71 100644 --- a/internal/command/e2etest/unmanaged_test.go +++ b/internal/command/e2etest/unmanaged_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( @@ -113,6 +116,7 @@ func (p *providerServer5) ApplyResourceChange(ctx context.Context, req *proto5.A defer p.Unlock() p.applyResourceChangeCalled = true + return p.ProviderServer.ApplyResourceChange(ctx, req) } diff --git a/internal/command/e2etest/version_test.go b/internal/command/e2etest/version_test.go index 90c716aff4..847ae02c79 100644 --- a/internal/command/e2etest/version_test.go +++ b/internal/command/e2etest/version_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2etest import ( diff --git a/internal/command/flag_kv.go b/internal/command/flag_kv.go index b084c5135d..2d16ca5505 100644 --- a/internal/command/flag_kv.go +++ b/internal/command/flag_kv.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -28,16 +31,3 @@ func (v *FlagStringKV) Set(raw string) error { (*v)[key] = value return nil } - -// FlagStringSlice is a flag.Value implementation for parsing targets from the -// command line, e.g. -target=aws_instance.foo -target=aws_vpc.bar -type FlagStringSlice []string - -func (v *FlagStringSlice) String() string { - return "" -} -func (v *FlagStringSlice) Set(raw string) error { - *v = append(*v, raw) - - return nil -} diff --git a/internal/command/flag_kv_test.go b/internal/command/flag_kv_test.go index 0f39211fb7..8d37535dcb 100644 --- a/internal/command/flag_kv_test.go +++ b/internal/command/flag_kv_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/fmt.go b/internal/command/fmt.go index 64d12e796f..b7990f61c4 100644 --- a/internal/command/fmt.go +++ b/internal/command/fmt.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -11,10 +14,10 @@ import ( "path/filepath" "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/hcl/v2/hclwrite" - "github.com/mitchellh/cli" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/tfdiags" @@ -24,6 +27,15 @@ const ( stdinArg = "-" ) +var ( + fmtSupportedExts = []string{ + ".tf", + ".tfvars", + ".tftest.hcl", + ".tfmock.hcl", + } +) + // FmtCommand is a Command implementation that rewrites Terraform config // files to a canonical format and style. type FmtCommand struct { @@ -124,21 +136,31 @@ func (c *FmtCommand) fmt(paths []string, stdin io.Reader, stdout io.Writer) tfdi dirDiags := c.processDir(path, stdout) diags = diags.Append(dirDiags) } else { - switch filepath.Ext(path) { - case ".tf", ".tfvars": - f, err := os.Open(path) - if err != nil { - // Open does not produce error messages that are end-user-appropriate, - // so we'll need to simplify here. - diags = diags.Append(fmt.Errorf("Failed to read file %s", path)) - continue - } + fmtd := false + for _, ext := range fmtSupportedExts { + if strings.HasSuffix(path, ext) { + f, err := os.Open(path) + if err != nil { + // Open does not produce error messages that are end-user-appropriate, + // so we'll need to simplify here. + diags = diags.Append(fmt.Errorf("Failed to read file %s", path)) + continue + } - fileDiags := c.processFile(c.normalizePath(path), f, stdout, false) - diags = diags.Append(fileDiags) - f.Close() - default: - diags = diags.Append(fmt.Errorf("Only .tf and .tfvars files can be processed with terraform fmt")) + fileDiags := c.processFile(c.normalizePath(path), f, stdout, false) + diags = diags.Append(fileDiags) + f.Close() + + // Take note that we processed the file. + fmtd = true + + // Don't check the remaining extensions. + break + } + } + + if !fmtd { + diags = diags.Append(fmt.Errorf("Only .tf, .tfvars, and .tftest.hcl files can be processed with terraform fmt")) continue } } @@ -241,20 +263,23 @@ func (c *FmtCommand) processDir(path string, stdout io.Writer) tfdiags.Diagnosti continue } - ext := filepath.Ext(name) - switch ext { - case ".tf", ".tfvars": - f, err := os.Open(subPath) - if err != nil { - // Open does not produce error messages that are end-user-appropriate, - // so we'll need to simplify here. - diags = diags.Append(fmt.Errorf("Failed to read file %s", subPath)) - continue - } + for _, ext := range fmtSupportedExts { + if strings.HasSuffix(name, ext) { + f, err := os.Open(subPath) + if err != nil { + // Open does not produce error messages that are end-user-appropriate, + // so we'll need to simplify here. + diags = diags.Append(fmt.Errorf("Failed to read file %s", subPath)) + continue + } - fileDiags := c.processFile(c.normalizePath(subPath), f, stdout, false) - diags = diags.Append(fileDiags) - f.Close() + fileDiags := c.processFile(c.normalizePath(subPath), f, stdout, false) + diags = diags.Append(fileDiags) + f.Close() + + // Don't need to check the remaining extensions. + break + } } } @@ -525,9 +550,10 @@ func (c *FmtCommand) Help() string { helpText := ` Usage: terraform [global options] fmt [options] [target...] - Rewrites all Terraform configuration files to a canonical format. Both - configuration files (.tf) and variables files (.tfvars) are updated. - JSON files (.tf.json or .tfvars.json) are not modified. + Rewrites all Terraform configuration files to a canonical format. All + configuration files (.tf), variables files (.tfvars), and testing files + (.tftest.hcl) are updated. JSON files (.tf.json, .tfvars.json, or + .tftest.json) are not modified. By default, fmt scans the current directory for configuration files. If you provide a directory for the target argument, then fmt will scan that diff --git a/internal/command/fmt_test.go b/internal/command/fmt_test.go index bfc1f2b7e1..2f46ec0e8a 100644 --- a/internal/command/fmt_test.go +++ b/internal/command/fmt_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -10,9 +13,137 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) +func TestFmt_MockDataFiles(t *testing.T) { + const inSuffix = "_in.tfmock.hcl" + const outSuffix = "_out.tfmock.hcl" + const gotSuffix = "_got.tfmock.hcl" + entries, err := ioutil.ReadDir("testdata/tfmock-fmt") + if err != nil { + t.Fatal(err) + } + + tmpDir, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + + for _, info := range entries { + if info.IsDir() { + continue + } + filename := info.Name() + if !strings.HasSuffix(filename, inSuffix) { + continue + } + testName := filename[:len(filename)-len(inSuffix)] + t.Run(testName, func(t *testing.T) { + inFile := filepath.Join("testdata", "tfmock-fmt", testName+inSuffix) + wantFile := filepath.Join("testdata", "tfmock-fmt", testName+outSuffix) + gotFile := filepath.Join(tmpDir, testName+gotSuffix) + input, err := ioutil.ReadFile(inFile) + if err != nil { + t.Fatal(err) + } + want, err := ioutil.ReadFile(wantFile) + if err != nil { + t.Fatal(err) + } + err = ioutil.WriteFile(gotFile, input, 0700) + if err != nil { + t.Fatal(err) + } + + ui := cli.NewMockUi() + c := &FmtCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + }, + } + args := []string{gotFile} + if code := c.Run(args); code != 0 { + t.Fatalf("fmt command was unsuccessful:\n%s", ui.ErrorWriter.String()) + } + + got, err := ioutil.ReadFile(gotFile) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(string(want), string(got)); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + +func TestFmt_TestFiles(t *testing.T) { + const inSuffix = "_in.tftest.hcl" + const outSuffix = "_out.tftest.hcl" + const gotSuffix = "_got.tftest.hcl" + entries, err := ioutil.ReadDir("testdata/tftest-fmt") + if err != nil { + t.Fatal(err) + } + + tmpDir, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } + + for _, info := range entries { + if info.IsDir() { + continue + } + filename := info.Name() + if !strings.HasSuffix(filename, inSuffix) { + continue + } + testName := filename[:len(filename)-len(inSuffix)] + t.Run(testName, func(t *testing.T) { + inFile := filepath.Join("testdata", "tftest-fmt", testName+inSuffix) + wantFile := filepath.Join("testdata", "tftest-fmt", testName+outSuffix) + gotFile := filepath.Join(tmpDir, testName+gotSuffix) + input, err := ioutil.ReadFile(inFile) + if err != nil { + t.Fatal(err) + } + want, err := ioutil.ReadFile(wantFile) + if err != nil { + t.Fatal(err) + } + err = ioutil.WriteFile(gotFile, input, 0700) + if err != nil { + t.Fatal(err) + } + + ui := cli.NewMockUi() + c := &FmtCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + }, + } + args := []string{gotFile} + if code := c.Run(args); code != 0 { + t.Fatalf("fmt command was unsuccessful:\n%s", ui.ErrorWriter.String()) + } + + got, err := ioutil.ReadFile(gotFile) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(string(want), string(got)); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + func TestFmt(t *testing.T) { const inSuffix = "_in.tf" const outSuffix = "_out.tf" diff --git a/internal/command/format/diagnostic.go b/internal/command/format/diagnostic.go index 780592a08c..5915f26bdf 100644 --- a/internal/command/format/diagnostic.go +++ b/internal/command/format/diagnostic.go @@ -1,9 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package format import ( "bufio" "bytes" "fmt" + "iter" + "slices" "sort" "strings" @@ -74,7 +79,8 @@ func DiagnosticFromJSON(diag *viewsjson.Diagnostic, color *colorstring.Colorize, // be pure text that lends itself well to word-wrapping. fmt.Fprintf(&buf, color.Color("[bold]%s[reset]\n\n"), diag.Summary) - appendSourceSnippets(&buf, diag, color) + f := &snippetFormatter{&buf, diag, color} + f.write() if diag.Detail != "" { paraWidth := width - leftRuleWidth - 1 // leave room for the left rule @@ -148,7 +154,8 @@ func DiagnosticPlainFromJSON(diag *viewsjson.Diagnostic, width int) string { // be pure text that lends itself well to word-wrapping. fmt.Fprintf(&buf, "%s\n\n", diag.Summary) - appendSourceSnippets(&buf, diag, disabledColorize) + f := &snippetFormatter{&buf, diag, disabledColorize} + f.write() if diag.Detail != "" { if width > 1 { @@ -212,7 +219,17 @@ func DiagnosticWarningsCompact(diags tfdiags.Diagnostics, color *colorstring.Col return b.String() } -func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color *colorstring.Colorize) { +// snippetFormatter handles formatting diagnostic information with source snippets +type snippetFormatter struct { + buf *bytes.Buffer + diag *viewsjson.Diagnostic + color *colorstring.Colorize +} + +func (f *snippetFormatter) write() { + diag := f.diag + buf := f.buf + color := f.color if diag.Address != "" { fmt.Fprintf(buf, " with %s,\n", diag.Address) } @@ -278,14 +295,13 @@ func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color * ) } - if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) { + if len(snippet.Values) > 0 || (snippet.FunctionCall != nil && snippet.FunctionCall.Signature != nil) || snippet.TestAssertionExpr != nil { // The diagnostic may also have information about the dynamic // values of relevant variables at the point of evaluation. // This is particularly useful for expressions that get evaluated // multiple times with different values, such as blocks using // "count" and "for_each", or within "for" expressions. - values := make([]viewsjson.DiagnosticExpressionValue, len(snippet.Values)) - copy(values, snippet.Values) + values := slices.Clone(snippet.Values) sort.Slice(values, func(i, j int) bool { return values[i].Traversal < values[j].Traversal }) @@ -309,11 +325,139 @@ func appendSourceSnippets(buf *bytes.Buffer, diag *viewsjson.Diagnostic, color * } buf.WriteString(")\n") } - for _, value := range values { - fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), value.Traversal, value.Statement) + + // always print the values unless in the case of a test assertion, where we only print them if the user has requested verbose output + printValues := snippet.TestAssertionExpr == nil || snippet.TestAssertionExpr.ShowVerbose + + // The diagnostic may also have information about failures from test assertions + // in a `terraform test` run. This is useful for understanding the values that + // were being compared when the assertion failed. + // Also, we'll print a JSON diff of the two values to make it easier to see the + // differences. + if snippet.TestAssertionExpr != nil { + f.printTestDiagOutput(snippet.TestAssertionExpr) + } + + if printValues { + for _, value := range values { + // if the statement is one line, we'll just print it as is + // otherwise, we have to ensure that each line is indented correctly + // and that the first line has the traversal information + valSlice := strings.Split(value.Statement, "\n") + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]%s[reset] %s\n"), + value.Traversal, valSlice[0]) + + for _, line := range valSlice[1:] { + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) + } + } } } } buf.WriteByte('\n') } + +func (f *snippetFormatter) printTestDiagOutput(diag *viewsjson.DiagnosticTestBinaryExpr) { + buf := f.buf + color := f.color + // We only print the LHS and RHS if the user has requested verbose output + // for the test assertion. + if diag.ShowVerbose { + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]LHS[reset]:\n")) + for line := range strings.SplitSeq(diag.LHS, "\n") { + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) + } + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]RHS[reset]:\n")) + for line := range strings.SplitSeq(diag.RHS, "\n") { + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] %s\n"), line) + } + } + if diag.Warning != "" { + fmt.Fprintf(buf, color.Color(" [dark_gray]│[reset] [bold]Warning[reset]: %s\n"), diag.Warning) + } + f.printJSONDiff(diag.LHS, diag.RHS) + buf.WriteByte('\n') +} + +// printJSONDiff prints a colorized line-by-line diff of the JSON values of the LHS and RHS expressions +// in a test assertion. +// It visually distinguishes removed and added lines, helping users identify +// discrepancies between an "actual" (lhsStr) and an "expected" (rhsStr) JSON output. +func (f *snippetFormatter) printJSONDiff(lhsStr, rhsStr string) { + + buf := f.buf + color := f.color + // No visible difference in the JSON, so we'll just return + if lhsStr == rhsStr { + return + } + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [bold]Diff[reset]:\n")) + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [red][bold]--- actual[reset]\n")) + fmt.Fprint(buf, color.Color(" [dark_gray]│[reset] [green][bold]+++ expected[reset]\n")) + nextLhs, stopLhs := iter.Pull(strings.SplitSeq(lhsStr, "\n")) + nextRhs, stopRhs := iter.Pull(strings.SplitSeq(rhsStr, "\n")) + + printLine := func(prefix, line string) { + var colour string + switch prefix { + case "-": + colour = "[red]" + case "+": + colour = "[green]" + default: + } + msg := fmt.Sprintf(" [dark_gray]│[reset] %s%s[reset] %s\n", colour, prefix, line) + fmt.Fprint(buf, color.Color(msg)) + } + + // Collect differing lines separately for each side + removedLines := []string{} + addedLines := []string{} + + // Function to print collected diffs and reset buffers + printDiffs := func() { + for _, line := range removedLines { + printLine("-", line) + } + for _, line := range addedLines { + printLine("+", line) + } + removedLines = []string{} + addedLines = []string{} + } + + // We'll iterate over both sides of the expression and collect the differences + // along the way. When a match is found, we'll then print all the collected diffs + // and the matching line, and then reset the buffers. + for { + lhsLine, lhsOk := nextLhs() + rhsLine, rhsOk := nextRhs() + + if !lhsOk && !rhsOk { // Both sides are done, so we'll print the diffs and break + printDiffs() + break + } + + // If one side is done, we'll just print the remaining lines from the other side + if !lhsOk { + addedLines = append(addedLines, rhsLine) + continue + } + if !rhsOk { + removedLines = append(removedLines, lhsLine) + continue + } + + if lhsLine == rhsLine { + printDiffs() + printLine(" ", lhsLine) + } else { + removedLines = append(removedLines, lhsLine) + addedLines = append(addedLines, rhsLine) + } + } + + stopLhs() + stopRhs() +} diff --git a/internal/command/format/diagnostic_test.go b/internal/command/format/diagnostic_test.go index 95f2ed6aa1..0c21375898 100644 --- a/internal/command/format/diagnostic_test.go +++ b/internal/command/format/diagnostic_test.go @@ -1,6 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package format import ( + "bytes" + "fmt" + "regexp" "strings" "testing" @@ -260,6 +266,82 @@ func TestDiagnostic(t *testing.T) { [red]│[reset] [red]│[reset] Whatever shall we do? [red]╵[reset] +`, + }, + "error origination from failed test assertion": { + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Test assertion failed", + Detail: "LHS not equal to RHS", + Subject: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + Expression: &hclsyntax.BinaryOpExpr{ + Op: hclsyntax.OpEqual, + LHS: &hclsyntax.LiteralValueExpr{ + Val: cty.ObjectVal(map[string]cty.Value{ + "inner": cty.StringVal("str1"), + "extra": cty.StringVal("str2"), + }), + }, + RHS: &hclsyntax.LiteralValueExpr{ + Val: cty.ObjectVal(map[string]cty.Value{ + "inner": cty.StringVal("str11"), + "extra": cty.StringVal("str21"), + }), + }, + SrcRange: hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{Line: 1, Column: 6, Byte: 5}, + End: hcl.Pos{Line: 1, Column: 12, Byte: 11}, + }, + }, + EvalContext: &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "inner": cty.StringVal("str1"), + }), + "bar": cty.ObjectVal(map[string]cty.Value{ + "inner": cty.StringVal("str2"), + }), + }, + }, + // This is simulating what the test assertion expression + // type would generate on evaluation, by implementing the + // same interface it uses. + Extra: diagnosticCausedByTestFailure{true}, + }, + `[red]╷[reset] +[red]│[reset] [bold][red]Error: [reset][bold]Test assertion failed[reset] +[red]│[reset] +[red]│[reset] on test.tf line 1: +[red]│[reset] 1: test [underline]source[reset] code +[red]│[reset] [dark_gray]├────────────────[reset] +[red]│[reset] [dark_gray]│[reset] [bold]LHS[reset]: +[red]│[reset] [dark_gray]│[reset] { +[red]│[reset] [dark_gray]│[reset] "extra": "str2", +[red]│[reset] [dark_gray]│[reset] "inner": "str1" +[red]│[reset] [dark_gray]│[reset] } +[red]│[reset] [dark_gray]│[reset] [bold]RHS[reset]: +[red]│[reset] [dark_gray]│[reset] { +[red]│[reset] [dark_gray]│[reset] "extra": "str21", +[red]│[reset] [dark_gray]│[reset] "inner": "str11" +[red]│[reset] [dark_gray]│[reset] } +[red]│[reset] [dark_gray]│[reset] [bold]Diff[reset]: +[red]│[reset] [dark_gray]│[reset] [red][bold]--- actual[reset] +[red]│[reset] [dark_gray]│[reset] [green][bold]+++ expected[reset] +[red]│[reset] [dark_gray]│[reset] [reset] { +[red]│[reset] [dark_gray]│[reset] [red]-[reset] "extra": "str2", +[red]│[reset] [dark_gray]│[reset] [red]-[reset] "inner": "str1" +[red]│[reset] [dark_gray]│[reset] [green]+[reset] "extra": "str21", +[red]│[reset] [dark_gray]│[reset] [green]+[reset] "inner": "str11" +[red]│[reset] [dark_gray]│[reset] [reset] } +[red]│[reset] +[red]│[reset] +[red]│[reset] LHS not equal to RHS +[red]╵[reset] `, }, } @@ -907,6 +989,243 @@ func TestDiagnosticFromJSON_invalid(t *testing.T) { } } +func TestJsonDiff(t *testing.T) { + f := &snippetFormatter{ + buf: &bytes.Buffer{}, + color: &colorstring.Colorize{ + Reset: true, + Disable: true, + }, + } + + tests := []struct { + name string + strA string + strB string + diff string + }{ + { + name: "Basic different fields", + strA: `{ + "field1": "value1", + "field2": "value2", + "field3": "value3", + "field4": "value4" +}`, + strB: `{ + "field1": "value1", + "field2": "different", + "field3": "value3", + "field4": "value4" +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ { + │ "field1": "value1", + │ - "field2": "value2", + │ + "field2": "different", + │ "field3": "value3", + │ "field4": "value4" + │ } +`, + }, + { + name: "Unequal number of fields", + strA: `{ + "field1": "value1", + "field2": "value2", + "field3": "value3", + "extraField": "extraValue", + "field4": "value4" +}`, + strB: `{ + "field1": "value1", + "fieldX": "valueX", + "fieldY": "valueY", + "field4": "value4" +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ { + │ "field1": "value1", + │ - "field2": "value2", + │ - "field3": "value3", + │ - "extraField": "extraValue", + │ - "field4": "value4" + │ - } + │ + "fieldX": "valueX", + │ + "fieldY": "valueY", + │ + "field4": "value4" + │ + } +`, + }, + { + name: "Empty vs non-empty JSON", + strA: `{}`, + strB: `{ + "field1": "value1", + "field2": "value2" +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ - {} + │ + { + │ + "field1": "value1", + │ + "field2": "value2" + │ + } +`, + }, + { + name: "Completely different JSONs", + strA: `{ + "a": 1, + "b": 2 +}`, + strB: `{ + "c": 3, + "d": 4 +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ { + │ - "a": 1, + │ - "b": 2 + │ + "c": 3, + │ + "d": 4 + │ } +`, + }, + { + name: "Nested objects with differences", + strA: `{ + "outer": { + "inner1": "value1", + "inner2": "value2" + } +}`, + strB: `{ + "outer": { + "inner1": "changed", + "inner2": "value2" + } +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ { + │ "outer": { + │ - "inner1": "value1", + │ + "inner1": "changed", + │ "inner2": "value2" + │ } + │ } +`, + }, + { + name: "Multiple separate diff blocks", + strA: `{ + "block1": "original1", + "unchanged1": "same", + "block2": "original2", + "unchanged2": "same", + "block3": "original3" +}`, + strB: `{ + "block1": "changed1", + "unchanged1": "same", + "block2": "changed2", + "unchanged2": "same", + "block3": "changed3" +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ { + │ - "block1": "original1", + │ + "block1": "changed1", + │ "unchanged1": "same", + │ - "block2": "original2", + │ + "block2": "changed2", + │ "unchanged2": "same", + │ - "block3": "original3" + │ + "block3": "changed3" + │ } +`, + }, + { + name: "Large number of differences", + strA: `{ + "item1": "a", + "item2": "b", + "item3": "c", + "item4": "d", + "item5": "e", + "item6": "f", + "item7": "g" +}`, + strB: `{ + "item1": "a", + "item2": "B", + "item3": "C", + "item4": "D", + "item5": "e", + "item6": "F", + "item7": "g" +}`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ { + │ "item1": "a", + │ - "item2": "b", + │ - "item3": "c", + │ - "item4": "d", + │ + "item2": "B", + │ + "item3": "C", + │ + "item4": "D", + │ "item5": "e", + │ - "item6": "f", + │ + "item6": "F", + │ "item7": "g" + │ } +`, + }, + { + name: "Identical JSONs", + strA: `{"field": "value"}`, + strB: `{"field": "value"}`, + diff: ``, // No output expected for identical JSONs + }, + { + name: "simple: no matches", + strA: `1`, + strB: `2`, + diff: ` │ Diff: + │ --- actual + │ +++ expected + │ - 1 + │ + 2 +`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + f.buf.Reset() + f.printJSONDiff(test.strA, test.strB) + diff := regexp.MustCompile(`\[[^\]]+\]`).ReplaceAllString(f.buf.String(), "") + fmt.Println(diff) + if d := cmp.Diff(diff, test.diff); d != "" { + t.Errorf("diff mismatch: got %s\n, want %s\n: diff: %s\n", diff, test.diff, d) + } + }) + } +} + // fakeDiagFunctionCallExtra is a fake implementation of the interface that // HCL uses to provide "extra information" associated with diagnostics that // describe errors during a function call. @@ -943,3 +1262,17 @@ var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(true func (e diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool { return bool(e) } + +var _ tfdiags.DiagnosticExtraCausedByTestFailure = diagnosticCausedByTestFailure{} + +type diagnosticCausedByTestFailure struct { + Verbose bool +} + +func (e diagnosticCausedByTestFailure) DiagnosticCausedByTestFailure() bool { + return true +} + +func (e diagnosticCausedByTestFailure) IsTestVerboseMode() bool { + return e.Verbose +} diff --git a/internal/command/format/diff.go b/internal/command/format/diff.go deleted file mode 100644 index 0f812f5cb1..0000000000 --- a/internal/command/format/diff.go +++ /dev/null @@ -1,2036 +0,0 @@ -package format - -import ( - "bufio" - "bytes" - "fmt" - "log" - "sort" - "strings" - - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/mitchellh/colorstring" - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang/marks" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/plans/objchange" - "github.com/hashicorp/terraform/internal/states" -) - -// DiffLanguage controls the description of the resource change reasons. -type DiffLanguage rune - -//go:generate go run golang.org/x/tools/cmd/stringer -type=DiffLanguage diff.go - -const ( - // DiffLanguageProposedChange indicates that the change is one which is - // planned to be applied. - DiffLanguageProposedChange DiffLanguage = 'P' - - // DiffLanguageDetectedDrift indicates that the change is detected drift - // from the configuration. - DiffLanguageDetectedDrift DiffLanguage = 'D' -) - -// ResourceChange returns a string representation of a change to a particular -// resource, for inclusion in user-facing plan output. -// -// The resource schema must be provided along with the change so that the -// formatted change can reflect the configuration structure for the associated -// resource. -// -// If "color" is non-nil, it will be used to color the result. Otherwise, -// no color codes will be included. -func ResourceChange( - change *plans.ResourceInstanceChange, - schema *configschema.Block, - color *colorstring.Colorize, - language DiffLanguage, -) string { - addr := change.Addr - var buf bytes.Buffer - - if color == nil { - color = &colorstring.Colorize{ - Colors: colorstring.DefaultColors, - Disable: true, - Reset: false, - } - } - - dispAddr := addr.String() - if change.DeposedKey != states.NotDeposed { - dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, change.DeposedKey) - } - - switch change.Action { - case plans.Create: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be created"), dispAddr)) - case plans.Read: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be read during apply"), dispAddr)) - switch change.ActionReason { - case plans.ResourceInstanceReadBecauseConfigUnknown: - buf.WriteString("\n # (config refers to values not yet known)") - case plans.ResourceInstanceReadBecauseDependencyPending: - buf.WriteString("\n # (depends on a resource or a module with changes pending)") - } - case plans.Update: - switch language { - case DiffLanguageProposedChange: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be updated in-place"), dispAddr)) - case DiffLanguageDetectedDrift: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has changed"), dispAddr)) - default: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] update (unknown reason %s)"), dispAddr, language)) - } - case plans.CreateThenDelete, plans.DeleteThenCreate: - switch change.ActionReason { - case plans.ResourceInstanceReplaceBecauseTainted: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] is tainted, so must be [bold][red]replaced"), dispAddr)) - case plans.ResourceInstanceReplaceByRequest: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested"), dispAddr)) - case plans.ResourceInstanceReplaceByTriggers: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by"), dispAddr)) - default: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] must be [bold][red]replaced"), dispAddr)) - } - case plans.Delete: - switch language { - case DiffLanguageProposedChange: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] will be [bold][red]destroyed"), dispAddr)) - case DiffLanguageDetectedDrift: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has been deleted"), dispAddr)) - default: - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] delete (unknown reason %s)"), dispAddr, language)) - } - // We can sometimes give some additional detail about why we're - // proposing to delete. We show this as additional notes, rather than - // as additional wording in the main action statement, in an attempt - // to make the "will be destroyed" message prominent and consistent - // in all cases, for easier scanning of this often-risky action. - switch change.ActionReason { - case plans.ResourceInstanceDeleteBecauseNoResourceConfig: - buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", addr.Resource.Resource)) - case plans.ResourceInstanceDeleteBecauseNoModule: - // FIXME: Ideally we'd truncate addr.Module to reflect the earliest - // step that doesn't exist, so it's clearer which call this refers - // to, but we don't have enough information out here in the UI layer - // to decide that; only the "expander" in Terraform Core knows - // which module instance keys are actually declared. - buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", addr.Module)) - case plans.ResourceInstanceDeleteBecauseWrongRepetition: - // We have some different variations of this one - switch addr.Resource.Key.(type) { - case nil: - buf.WriteString("\n # (because resource uses count or for_each)") - case addrs.IntKey: - buf.WriteString("\n # (because resource does not use count)") - case addrs.StringKey: - buf.WriteString("\n # (because resource does not use for_each)") - } - case plans.ResourceInstanceDeleteBecauseCountIndex: - buf.WriteString(fmt.Sprintf("\n # (because index %s is out of range for count)", addr.Resource.Key)) - case plans.ResourceInstanceDeleteBecauseEachKey: - buf.WriteString(fmt.Sprintf("\n # (because key %s is not in for_each map)", addr.Resource.Key)) - } - if change.DeposedKey != states.NotDeposed { - // Some extra context about this unusual situation. - buf.WriteString(color.Color("\n # (left over from a partially-failed replacement of this instance)")) - } - case plans.NoOp: - if change.Moved() { - buf.WriteString(fmt.Sprintf(color.Color("[bold] # %s[reset] has moved to [bold]%s[reset]"), change.PrevRunAddr.String(), dispAddr)) - break - } - fallthrough - default: - // should never happen, since the above is exhaustive - buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) - } - buf.WriteString(color.Color("[reset]\n")) - - if change.Moved() && change.Action != plans.NoOp { - buf.WriteString(fmt.Sprintf(color.Color(" # [reset](moved from %s)\n"), change.PrevRunAddr.String())) - } - - if change.Moved() && change.Action == plans.NoOp { - buf.WriteString(" ") - } else { - buf.WriteString(color.Color(DiffActionSymbol(change.Action)) + " ") - } - - switch addr.Resource.Resource.Mode { - case addrs.ManagedResourceMode: - buf.WriteString(fmt.Sprintf( - "resource %q %q", - addr.Resource.Resource.Type, - addr.Resource.Resource.Name, - )) - case addrs.DataResourceMode: - buf.WriteString(fmt.Sprintf( - "data %q %q", - addr.Resource.Resource.Type, - addr.Resource.Resource.Name, - )) - default: - // should never happen, since the above is exhaustive - buf.WriteString(addr.String()) - } - - buf.WriteString(" {") - - p := blockBodyDiffPrinter{ - buf: &buf, - color: color, - action: change.Action, - requiredReplace: change.RequiredReplace, - } - - // Most commonly-used resources have nested blocks that result in us - // going at least three traversals deep while we recurse here, so we'll - // start with that much capacity and then grow as needed for deeper - // structures. - path := make(cty.Path, 0, 3) - - result := p.writeBlockBodyDiff(schema, change.Before, change.After, 6, path) - if result.bodyWritten { - buf.WriteString("\n") - buf.WriteString(strings.Repeat(" ", 4)) - } - buf.WriteString("}\n") - - return buf.String() -} - -// OutputChanges returns a string representation of a set of changes to output -// values for inclusion in user-facing plan output. -// -// If "color" is non-nil, it will be used to color the result. Otherwise, -// no color codes will be included. -func OutputChanges( - changes []*plans.OutputChangeSrc, - color *colorstring.Colorize, -) string { - var buf bytes.Buffer - p := blockBodyDiffPrinter{ - buf: &buf, - color: color, - action: plans.Update, // not actually used in this case, because we're not printing a containing block - } - - // We're going to reuse the codepath we used for printing resource block - // diffs, by pretending that the set of defined outputs are the attributes - // of some resource. It's a little forced to do this, but it gives us all - // the same formatting heuristics as we normally use for resource - // attributes. - oldVals := make(map[string]cty.Value, len(changes)) - newVals := make(map[string]cty.Value, len(changes)) - synthSchema := &configschema.Block{ - Attributes: make(map[string]*configschema.Attribute, len(changes)), - } - for _, changeSrc := range changes { - name := changeSrc.Addr.OutputValue.Name - change, err := changeSrc.Decode() - if err != nil { - // It'd be weird to get a decoding error here because that would - // suggest that Terraform itself just produced an invalid plan, and - // we don't have any good way to ignore it in this codepath, so - // we'll just log it and ignore it. - log.Printf("[ERROR] format.OutputChanges: Failed to decode planned change for output %q: %s", name, err) - continue - } - synthSchema.Attributes[name] = &configschema.Attribute{ - Type: cty.DynamicPseudoType, // output types are decided dynamically based on the given value - Optional: true, - Sensitive: change.Sensitive, - } - oldVals[name] = change.Before - newVals[name] = change.After - } - - p.writeBlockBodyDiff(synthSchema, cty.ObjectVal(oldVals), cty.ObjectVal(newVals), 2, nil) - - return buf.String() -} - -type blockBodyDiffPrinter struct { - buf *bytes.Buffer - color *colorstring.Colorize - action plans.Action - requiredReplace cty.PathSet - // verbose is set to true when using the "diff" printer to format state - verbose bool -} - -type blockBodyDiffResult struct { - bodyWritten bool - skippedAttributes int - skippedBlocks int -} - -const forcesNewResourceCaption = " [red]# forces replacement[reset]" - -// writeBlockBodyDiff writes attribute or block differences -// and returns true if any differences were found and written -func (p *blockBodyDiffPrinter) writeBlockBodyDiff(schema *configschema.Block, old, new cty.Value, indent int, path cty.Path) blockBodyDiffResult { - path = ctyEnsurePathCapacity(path, 1) - result := blockBodyDiffResult{} - - // write the attributes diff - blankBeforeBlocks := p.writeAttrsDiff(schema.Attributes, old, new, indent, path, &result) - p.writeSkippedAttr(result.skippedAttributes, indent+2) - - { - blockTypeNames := make([]string, 0, len(schema.BlockTypes)) - for name := range schema.BlockTypes { - blockTypeNames = append(blockTypeNames, name) - } - sort.Strings(blockTypeNames) - - for _, name := range blockTypeNames { - blockS := schema.BlockTypes[name] - oldVal := ctyGetAttrMaybeNull(old, name) - newVal := ctyGetAttrMaybeNull(new, name) - - result.bodyWritten = true - skippedBlocks := p.writeNestedBlockDiffs(name, blockS, oldVal, newVal, blankBeforeBlocks, indent, path) - if skippedBlocks > 0 { - result.skippedBlocks += skippedBlocks - } - - // Always include a blank for any subsequent block types. - blankBeforeBlocks = true - } - if result.skippedBlocks > 0 { - noun := "blocks" - if result.skippedBlocks == 1 { - noun = "block" - } - p.buf.WriteString("\n\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), result.skippedBlocks, noun)) - } - } - - return result -} - -func (p *blockBodyDiffPrinter) writeAttrsDiff( - attrsS map[string]*configschema.Attribute, - old, new cty.Value, - indent int, - path cty.Path, - result *blockBodyDiffResult) bool { - - attrNames := make([]string, 0, len(attrsS)) - displayAttrNames := make(map[string]string, len(attrsS)) - attrNameLen := 0 - for name := range attrsS { - oldVal := ctyGetAttrMaybeNull(old, name) - newVal := ctyGetAttrMaybeNull(new, name) - if oldVal.IsNull() && newVal.IsNull() { - // Skip attributes where both old and new values are null - // (we do this early here so that we'll do our value alignment - // based on the longest attribute name that has a change, rather - // than the longest attribute name in the full set.) - continue - } - - attrNames = append(attrNames, name) - displayAttrNames[name] = displayAttributeName(name) - if len(displayAttrNames[name]) > attrNameLen { - attrNameLen = len(displayAttrNames[name]) - } - } - sort.Strings(attrNames) - if len(attrNames) == 0 { - return false - } - - for _, name := range attrNames { - attrS := attrsS[name] - oldVal := ctyGetAttrMaybeNull(old, name) - newVal := ctyGetAttrMaybeNull(new, name) - - result.bodyWritten = true - skipped := p.writeAttrDiff(displayAttrNames[name], attrS, oldVal, newVal, attrNameLen, indent, path) - if skipped { - result.skippedAttributes++ - } - } - - return true -} - -// getPlanActionAndShow returns the action value -// and a boolean for showJustNew. In this function we -// modify the old and new values to remove any possible marks -func getPlanActionAndShow(old cty.Value, new cty.Value) (plans.Action, bool) { - var action plans.Action - showJustNew := false - switch { - case old.IsNull(): - action = plans.Create - showJustNew = true - case new.IsNull(): - action = plans.Delete - case ctyEqualWithUnknown(old, new): - action = plans.NoOp - showJustNew = true - default: - action = plans.Update - } - return action, showJustNew -} - -func (p *blockBodyDiffPrinter) writeAttrDiff(name string, attrS *configschema.Attribute, old, new cty.Value, nameLen, indent int, path cty.Path) bool { - path = append(path, cty.GetAttrStep{Name: name}) - action, showJustNew := getPlanActionAndShow(old, new) - - if action == plans.NoOp && !p.verbose && !identifyingAttribute(name, attrS) { - return true - } - - if attrS.NestedType != nil { - p.writeNestedAttrDiff(name, attrS.NestedType, old, new, nameLen, indent, path, action, showJustNew) - return false - } - - p.buf.WriteString("\n") - - p.writeSensitivityWarning(old, new, indent, action, false) - - p.buf.WriteString(strings.Repeat(" ", indent)) - p.writeActionSymbol(action) - - p.buf.WriteString(p.color.Color("[bold]")) - p.buf.WriteString(name) - p.buf.WriteString(p.color.Color("[reset]")) - p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) - p.buf.WriteString(" = ") - - if attrS.Sensitive { - p.buf.WriteString("(sensitive value)") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - } else { - switch { - case showJustNew: - p.writeValue(new, action, indent+2) - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - default: - // We show new even if it is null to emphasize the fact - // that it is being unset, since otherwise it is easy to - // misunderstand that the value is still set to the old value. - p.writeValueDiff(old, new, indent+2, path) - } - } - - return false -} - -// writeNestedAttrDiff is responsible for formatting Attributes with NestedTypes -// in the diff. -func (p *blockBodyDiffPrinter) writeNestedAttrDiff( - name string, objS *configschema.Object, old, new cty.Value, - nameLen, indent int, path cty.Path, action plans.Action, showJustNew bool) { - - p.buf.WriteString("\n") - p.writeSensitivityWarning(old, new, indent, action, false) - p.buf.WriteString(strings.Repeat(" ", indent)) - p.writeActionSymbol(action) - - p.buf.WriteString(p.color.Color("[bold]")) - p.buf.WriteString(name) - p.buf.WriteString(p.color.Color("[reset]")) - p.buf.WriteString(strings.Repeat(" ", nameLen-len(name))) - - if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) { - p.buf.WriteString(" = (sensitive value)") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - return - } - - result := &blockBodyDiffResult{} - switch objS.Nesting { - case configschema.NestingSingle: - p.buf.WriteString(" = {") - if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.writeAttrsDiff(objS.Attributes, old, new, indent+4, path, result) - p.writeSkippedAttr(result.skippedAttributes, indent+6) - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString("}") - - case configschema.NestingList: - p.buf.WriteString(" = [") - if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.buf.WriteString("\n") - - oldItems := ctyCollectionValues(old) - newItems := ctyCollectionValues(new) - // Here we intentionally preserve the index-based correspondance - // between old and new, rather than trying to detect insertions - // and removals in the list, because this more accurately reflects - // how Terraform Core and providers will understand the change, - // particularly when the nested block contains computed attributes - // that will themselves maintain correspondance by index. - - // commonLen is number of elements that exist in both lists, which - // will be presented as updates (~). Any additional items in one - // of the lists will be presented as either creates (+) or deletes (-) - // depending on which list they belong to. maxLen is the number of - // elements in that longer list. - var commonLen int - var maxLen int - // unchanged is the number of unchanged elements - var unchanged int - - switch { - case len(oldItems) < len(newItems): - commonLen = len(oldItems) - maxLen = len(newItems) - default: - commonLen = len(newItems) - maxLen = len(oldItems) - } - for i := 0; i < maxLen; i++ { - path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) - - var action plans.Action - var oldItem, newItem cty.Value - switch { - case i < commonLen: - oldItem = oldItems[i] - newItem = newItems[i] - if oldItem.RawEquals(newItem) { - action = plans.NoOp - unchanged++ - } else { - action = plans.Update - } - case i < len(oldItems): - oldItem = oldItems[i] - newItem = cty.NullVal(oldItem.Type()) - action = plans.Delete - case i < len(newItems): - newItem = newItems[i] - oldItem = cty.NullVal(newItem.Type()) - action = plans.Create - default: - action = plans.NoOp - } - - if action != plans.NoOp { - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeActionSymbol(action) - p.buf.WriteString("{") - - result := &blockBodyDiffResult{} - p.writeAttrsDiff(objS.Attributes, oldItem, newItem, indent+8, path, result) - if action == plans.Update { - p.writeSkippedAttr(result.skippedAttributes, indent+10) - } - p.buf.WriteString("\n") - - p.buf.WriteString(strings.Repeat(" ", indent+6)) - p.buf.WriteString("},\n") - } - } - p.writeSkippedElems(unchanged, indent+6) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString("]") - - if !new.IsKnown() { - p.buf.WriteString(" -> (known after apply)") - } - - case configschema.NestingSet: - oldItems := ctyCollectionValues(old) - newItems := ctyCollectionValues(new) - - var all cty.Value - if len(oldItems)+len(newItems) > 0 { - allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) - allItems = append(allItems, oldItems...) - allItems = append(allItems, newItems...) - - all = cty.SetVal(allItems) - } else { - all = cty.SetValEmpty(old.Type().ElementType()) - } - - p.buf.WriteString(" = [") - - var unchanged int - - for it := all.ElementIterator(); it.Next(); { - _, val := it.Element() - var action plans.Action - var oldValue, newValue cty.Value - switch { - case !val.IsKnown(): - action = plans.Update - newValue = val - case !new.IsKnown(): - action = plans.Delete - // the value must have come from the old set - oldValue = val - // Mark the new val as null, but the entire set will be - // displayed as "(unknown after apply)" - newValue = cty.NullVal(val.Type()) - case old.IsNull() || !old.HasElement(val).True(): - action = plans.Create - oldValue = cty.NullVal(val.Type()) - newValue = val - case new.IsNull() || !new.HasElement(val).True(): - action = plans.Delete - oldValue = val - newValue = cty.NullVal(val.Type()) - default: - action = plans.NoOp - oldValue = val - newValue = val - } - - if action == plans.NoOp { - unchanged++ - continue - } - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeActionSymbol(action) - p.buf.WriteString("{") - - if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - - path := append(path, cty.IndexStep{Key: val}) - p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+8, path, result) - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+6)) - p.buf.WriteString("},") - } - p.buf.WriteString("\n") - p.writeSkippedElems(unchanged, indent+6) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString("]") - - if !new.IsKnown() { - p.buf.WriteString(" -> (known after apply)") - } - - case configschema.NestingMap: - // For the sake of handling nested blocks, we'll treat a null map - // the same as an empty map since the config language doesn't - // distinguish these anyway. - old = ctyNullBlockMapAsEmpty(old) - new = ctyNullBlockMapAsEmpty(new) - - oldItems := old.AsValueMap() - - newItems := map[string]cty.Value{} - - if new.IsKnown() { - newItems = new.AsValueMap() - } - - allKeys := make(map[string]bool) - for k := range oldItems { - allKeys[k] = true - } - for k := range newItems { - allKeys[k] = true - } - allKeysOrder := make([]string, 0, len(allKeys)) - for k := range allKeys { - allKeysOrder = append(allKeysOrder, k) - } - sort.Strings(allKeysOrder) - - p.buf.WriteString(" = {\n") - - // unchanged tracks the number of unchanged elements - unchanged := 0 - for _, k := range allKeysOrder { - var action plans.Action - oldValue := oldItems[k] - - newValue := newItems[k] - switch { - case oldValue == cty.NilVal: - oldValue = cty.NullVal(newValue.Type()) - action = plans.Create - case newValue == cty.NilVal: - newValue = cty.NullVal(oldValue.Type()) - action = plans.Delete - case !newValue.RawEquals(oldValue): - action = plans.Update - default: - action = plans.NoOp - unchanged++ - } - - if action != plans.NoOp { - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeActionSymbol(action) - fmt.Fprintf(p.buf, "%q = {", k) - if p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1]) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - - path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) - p.writeAttrsDiff(objS.Attributes, oldValue, newValue, indent+8, path, result) - p.writeSkippedAttr(result.skippedAttributes, indent+10) - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+6)) - p.buf.WriteString("},\n") - } - } - - p.writeSkippedElems(unchanged, indent+6) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString("}") - if !new.IsKnown() { - p.buf.WriteString(" -> (known after apply)") - } - } -} - -func (p *blockBodyDiffPrinter) writeNestedBlockDiffs(name string, blockS *configschema.NestedBlock, old, new cty.Value, blankBefore bool, indent int, path cty.Path) int { - skippedBlocks := 0 - path = append(path, cty.GetAttrStep{Name: name}) - if old.IsNull() && new.IsNull() { - // Nothing to do if both old and new is null - return skippedBlocks - } - - // If either the old or the new value is marked, - // Display a special diff because it is irrelevant - // to list all obfuscated attributes as (sensitive) - if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) { - p.writeSensitiveNestedBlockDiff(name, old, new, indent, blankBefore, path) - return 0 - } - - // Where old/new are collections representing a nesting mode other than - // NestingSingle, we assume the collection value can never be unknown - // since we always produce the container for the nested objects, even if - // the objects within are computed. - - switch blockS.Nesting { - case configschema.NestingSingle, configschema.NestingGroup: - var action plans.Action - eqV := new.Equals(old) - switch { - case old.IsNull(): - action = plans.Create - case new.IsNull(): - action = plans.Delete - case !new.IsWhollyKnown() || !old.IsWhollyKnown(): - // "old" should actually always be known due to our contract - // that old values must never be unknown, but we'll allow it - // anyway to be robust. - action = plans.Update - case !eqV.IsKnown() || !eqV.True(): - action = plans.Update - } - - skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, old, new, indent, blankBefore, path) - if skipped { - return 1 - } - case configschema.NestingList: - // For the sake of handling nested blocks, we'll treat a null list - // the same as an empty list since the config language doesn't - // distinguish these anyway. - old = ctyNullBlockListAsEmpty(old) - new = ctyNullBlockListAsEmpty(new) - - oldItems := ctyCollectionValues(old) - newItems := ctyCollectionValues(new) - - // Here we intentionally preserve the index-based correspondance - // between old and new, rather than trying to detect insertions - // and removals in the list, because this more accurately reflects - // how Terraform Core and providers will understand the change, - // particularly when the nested block contains computed attributes - // that will themselves maintain correspondance by index. - - // commonLen is number of elements that exist in both lists, which - // will be presented as updates (~). Any additional items in one - // of the lists will be presented as either creates (+) or deletes (-) - // depending on which list they belong to. - var commonLen int - switch { - case len(oldItems) < len(newItems): - commonLen = len(oldItems) - default: - commonLen = len(newItems) - } - - blankBeforeInner := blankBefore - for i := 0; i < commonLen; i++ { - path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) - oldItem := oldItems[i] - newItem := newItems[i] - action := plans.Update - if oldItem.RawEquals(newItem) { - action = plans.NoOp - } - skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldItem, newItem, indent, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - for i := commonLen; i < len(oldItems); i++ { - path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) - oldItem := oldItems[i] - newItem := cty.NullVal(oldItem.Type()) - skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Delete, oldItem, newItem, indent, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - for i := commonLen; i < len(newItems); i++ { - path := append(path, cty.IndexStep{Key: cty.NumberIntVal(int64(i))}) - newItem := newItems[i] - oldItem := cty.NullVal(newItem.Type()) - skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, plans.Create, oldItem, newItem, indent, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - case configschema.NestingSet: - // For the sake of handling nested blocks, we'll treat a null set - // the same as an empty set since the config language doesn't - // distinguish these anyway. - old = ctyNullBlockSetAsEmpty(old) - new = ctyNullBlockSetAsEmpty(new) - - oldItems := ctyCollectionValues(old) - newItems := ctyCollectionValues(new) - - if (len(oldItems) + len(newItems)) == 0 { - // Nothing to do if both sets are empty - return 0 - } - - allItems := make([]cty.Value, 0, len(oldItems)+len(newItems)) - allItems = append(allItems, oldItems...) - allItems = append(allItems, newItems...) - all := cty.SetVal(allItems) - - blankBeforeInner := blankBefore - for it := all.ElementIterator(); it.Next(); { - _, val := it.Element() - var action plans.Action - var oldValue, newValue cty.Value - switch { - case !val.IsKnown(): - action = plans.Update - newValue = val - case !old.HasElement(val).True(): - action = plans.Create - oldValue = cty.NullVal(val.Type()) - newValue = val - case !new.HasElement(val).True(): - action = plans.Delete - oldValue = val - newValue = cty.NullVal(val.Type()) - default: - action = plans.NoOp - oldValue = val - newValue = val - } - path := append(path, cty.IndexStep{Key: val}) - skipped := p.writeNestedBlockDiff(name, nil, &blockS.Block, action, oldValue, newValue, indent, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - - case configschema.NestingMap: - // For the sake of handling nested blocks, we'll treat a null map - // the same as an empty map since the config language doesn't - // distinguish these anyway. - old = ctyNullBlockMapAsEmpty(old) - new = ctyNullBlockMapAsEmpty(new) - - oldItems := old.AsValueMap() - newItems := new.AsValueMap() - if (len(oldItems) + len(newItems)) == 0 { - // Nothing to do if both maps are empty - return 0 - } - - allKeys := make(map[string]bool) - for k := range oldItems { - allKeys[k] = true - } - for k := range newItems { - allKeys[k] = true - } - allKeysOrder := make([]string, 0, len(allKeys)) - for k := range allKeys { - allKeysOrder = append(allKeysOrder, k) - } - sort.Strings(allKeysOrder) - - blankBeforeInner := blankBefore - for _, k := range allKeysOrder { - var action plans.Action - oldValue := oldItems[k] - newValue := newItems[k] - switch { - case oldValue == cty.NilVal: - oldValue = cty.NullVal(newValue.Type()) - action = plans.Create - case newValue == cty.NilVal: - newValue = cty.NullVal(oldValue.Type()) - action = plans.Delete - case !newValue.RawEquals(oldValue): - action = plans.Update - default: - action = plans.NoOp - } - - path := append(path, cty.IndexStep{Key: cty.StringVal(k)}) - skipped := p.writeNestedBlockDiff(name, &k, &blockS.Block, action, oldValue, newValue, indent, blankBeforeInner, path) - if skipped { - skippedBlocks++ - } else { - blankBeforeInner = false - } - } - } - return skippedBlocks -} - -func (p *blockBodyDiffPrinter) writeSensitiveNestedBlockDiff(name string, old, new cty.Value, indent int, blankBefore bool, path cty.Path) { - var action plans.Action - switch { - case old.IsNull(): - action = plans.Create - case new.IsNull(): - action = plans.Delete - case !new.IsWhollyKnown() || !old.IsWhollyKnown(): - // "old" should actually always be known due to our contract - // that old values must never be unknown, but we'll allow it - // anyway to be robust. - action = plans.Update - case !ctyEqualValueAndMarks(old, new): - action = plans.Update - } - - if blankBefore { - p.buf.WriteRune('\n') - } - - // New line before warning printing - p.buf.WriteRune('\n') - p.writeSensitivityWarning(old, new, indent, action, true) - p.buf.WriteString(strings.Repeat(" ", indent)) - p.writeActionSymbol(action) - fmt.Fprintf(p.buf, "%s {", name) - if action != plans.NoOp && p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.buf.WriteRune('\n') - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.buf.WriteString("# At least one attribute in this block is (or was) sensitive,\n") - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.buf.WriteString("# so its contents will not be displayed.") - p.buf.WriteRune('\n') - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.buf.WriteString("}") -} - -func (p *blockBodyDiffPrinter) writeNestedBlockDiff(name string, label *string, blockS *configschema.Block, action plans.Action, old, new cty.Value, indent int, blankBefore bool, path cty.Path) bool { - if action == plans.NoOp && !p.verbose { - return true - } - - if blankBefore { - p.buf.WriteRune('\n') - } - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent)) - p.writeActionSymbol(action) - - if label != nil { - fmt.Fprintf(p.buf, "%s %q {", name, *label) - } else { - fmt.Fprintf(p.buf, "%s {", name) - } - - if action != plans.NoOp && (p.pathForcesNewResource(path) || p.pathForcesNewResource(path[:len(path)-1])) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - - result := p.writeBlockBodyDiff(blockS, old, new, indent+4, path) - if result.bodyWritten { - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - } - p.buf.WriteString("}") - - return false -} - -func (p *blockBodyDiffPrinter) writeValue(val cty.Value, action plans.Action, indent int) { - // Could check specifically for the sensitivity marker - if val.HasMark(marks.Sensitive) { - p.buf.WriteString("(sensitive)") - return - } - - if !val.IsKnown() { - p.buf.WriteString("(known after apply)") - return - } - if val.IsNull() { - p.buf.WriteString(p.color.Color("[dark_gray]null[reset]")) - return - } - - ty := val.Type() - - switch { - case ty.IsPrimitiveType(): - switch ty { - case cty.String: - { - // Special behavior for JSON strings containing array or object - src := []byte(val.AsString()) - ty, err := ctyjson.ImpliedType(src) - // check for the special case of "null", which decodes to nil, - // and just allow it to be printed out directly - if err == nil && !ty.IsPrimitiveType() && strings.TrimSpace(val.AsString()) != "null" { - jv, err := ctyjson.Unmarshal(src, ty) - if err == nil { - p.buf.WriteString("jsonencode(") - if jv.LengthInt() == 0 { - p.writeValue(jv, action, 0) - } else { - p.buf.WriteByte('\n') - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeValue(jv, action, indent+4) - p.buf.WriteByte('\n') - p.buf.WriteString(strings.Repeat(" ", indent)) - } - p.buf.WriteByte(')') - break // don't *also* do the normal behavior below - } - } - } - - if strings.Contains(val.AsString(), "\n") { - // It's a multi-line string, so we want to use the multi-line - // rendering so it'll be readable. Rather than re-implement - // that here, we'll just re-use the multi-line string diff - // printer with no changes, which ends up producing the - // result we want here. - // The path argument is nil because we don't track path - // information into strings and we know that a string can't - // have any indices or attributes that might need to be marked - // as (requires replacement), which is what that argument is for. - p.writeValueDiff(val, val, indent, nil) - break - } - - fmt.Fprintf(p.buf, "%q", val.AsString()) - case cty.Bool: - if val.True() { - p.buf.WriteString("true") - } else { - p.buf.WriteString("false") - } - case cty.Number: - bf := val.AsBigFloat() - p.buf.WriteString(bf.Text('f', -1)) - default: - // should never happen, since the above is exhaustive - fmt.Fprintf(p.buf, "%#v", val) - } - case ty.IsListType() || ty.IsSetType() || ty.IsTupleType(): - p.buf.WriteString("[") - - it := val.ElementIterator() - for it.Next() { - _, val := it.Element() - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.writeValue(val, action, indent+4) - p.buf.WriteString(",") - } - - if val.LengthInt() > 0 { - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent)) - } - p.buf.WriteString("]") - case ty.IsMapType(): - p.buf.WriteString("{") - - keyLen := 0 - for it := val.ElementIterator(); it.Next(); { - key, _ := it.Element() - if keyStr := key.AsString(); len(keyStr) > keyLen { - keyLen = len(keyStr) - } - } - - for it := val.ElementIterator(); it.Next(); { - key, val := it.Element() - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.writeValue(key, action, indent+4) - p.buf.WriteString(strings.Repeat(" ", keyLen-len(key.AsString()))) - p.buf.WriteString(" = ") - p.writeValue(val, action, indent+4) - } - - if val.LengthInt() > 0 { - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent)) - } - p.buf.WriteString("}") - case ty.IsObjectType(): - p.buf.WriteString("{") - - atys := ty.AttributeTypes() - attrNames := make([]string, 0, len(atys)) - displayAttrNames := make(map[string]string, len(atys)) - nameLen := 0 - for attrName := range atys { - attrNames = append(attrNames, attrName) - displayAttrNames[attrName] = displayAttributeName(attrName) - if len(displayAttrNames[attrName]) > nameLen { - nameLen = len(displayAttrNames[attrName]) - } - } - sort.Strings(attrNames) - - for _, attrName := range attrNames { - val := val.GetAttr(attrName) - displayAttrName := displayAttrNames[attrName] - - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.buf.WriteString(displayAttrName) - p.buf.WriteString(strings.Repeat(" ", nameLen-len(displayAttrName))) - p.buf.WriteString(" = ") - p.writeValue(val, action, indent+4) - } - - if len(attrNames) > 0 { - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent)) - } - p.buf.WriteString("}") - } -} - -func (p *blockBodyDiffPrinter) writeValueDiff(old, new cty.Value, indent int, path cty.Path) { - ty := old.Type() - typesEqual := ctyTypesEqual(ty, new.Type()) - - // We have some specialized diff implementations for certain complex - // values where it's useful to see a visualization of the diff of - // the nested elements rather than just showing the entire old and - // new values verbatim. - // However, these specialized implementations can apply only if both - // values are known and non-null. - if old.IsKnown() && new.IsKnown() && !old.IsNull() && !new.IsNull() && typesEqual { - if old.HasMark(marks.Sensitive) || new.HasMark(marks.Sensitive) { - p.buf.WriteString("(sensitive)") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - return - } - - switch { - case ty == cty.String: - // We have special behavior for both multi-line strings in general - // and for strings that can parse as JSON. For the JSON handling - // to apply, both old and new must be valid JSON. - // For single-line strings that don't parse as JSON we just fall - // out of this switch block and do the default old -> new rendering. - oldS := old.AsString() - newS := new.AsString() - - { - // Special behavior for JSON strings containing object or - // list values. - oldBytes := []byte(oldS) - newBytes := []byte(newS) - oldType, oldErr := ctyjson.ImpliedType(oldBytes) - newType, newErr := ctyjson.ImpliedType(newBytes) - if oldErr == nil && newErr == nil && !(oldType.IsPrimitiveType() && newType.IsPrimitiveType()) { - oldJV, oldErr := ctyjson.Unmarshal(oldBytes, oldType) - newJV, newErr := ctyjson.Unmarshal(newBytes, newType) - if oldErr == nil && newErr == nil { - if !oldJV.RawEquals(newJV) { // two JSON values may differ only in insignificant whitespace - p.buf.WriteString("jsonencode(") - p.buf.WriteByte('\n') - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(plans.Update) - p.writeValueDiff(oldJV, newJV, indent+4, path) - p.buf.WriteByte('\n') - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteByte(')') - } else { - // if they differ only in insignificant whitespace - // then we'll note that but still expand out the - // effective value. - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color("jsonencode( [red]# whitespace changes force replacement[reset]")) - } else { - p.buf.WriteString(p.color.Color("jsonencode( [dim]# whitespace changes[reset]")) - } - p.buf.WriteByte('\n') - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeValue(oldJV, plans.NoOp, indent+4) - p.buf.WriteByte('\n') - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteByte(')') - } - return - } - } - } - - if !strings.Contains(oldS, "\n") && !strings.Contains(newS, "\n") { - break - } - - p.buf.WriteString("<<-EOT") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.buf.WriteString("\n") - - var oldLines, newLines []cty.Value - { - r := strings.NewReader(oldS) - sc := bufio.NewScanner(r) - for sc.Scan() { - oldLines = append(oldLines, cty.StringVal(sc.Text())) - } - } - { - r := strings.NewReader(newS) - sc := bufio.NewScanner(r) - for sc.Scan() { - newLines = append(newLines, cty.StringVal(sc.Text())) - } - } - - // Optimization for strings which are exactly equal: just print - // directly without calculating the sequence diff. This makes a - // significant difference when this code path is reached via a - // writeValue call with a large multi-line string. - if oldS == newS { - for _, line := range newLines { - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.buf.WriteString(line.AsString()) - p.buf.WriteString("\n") - } - } else { - diffLines := ctySequenceDiff(oldLines, newLines) - for _, diffLine := range diffLines { - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(diffLine.Action) - - switch diffLine.Action { - case plans.NoOp, plans.Delete: - p.buf.WriteString(diffLine.Before.AsString()) - case plans.Create: - p.buf.WriteString(diffLine.After.AsString()) - default: - // Should never happen since the above covers all - // actions that ctySequenceDiff can return for strings - p.buf.WriteString(diffLine.After.AsString()) - - } - p.buf.WriteString("\n") - } - } - - p.buf.WriteString(strings.Repeat(" ", indent)) // +4 here because there's no symbol - p.buf.WriteString("EOT") - - return - - case ty.IsSetType(): - p.buf.WriteString("[") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.buf.WriteString("\n") - - var addedVals, removedVals, allVals []cty.Value - for it := old.ElementIterator(); it.Next(); { - _, val := it.Element() - allVals = append(allVals, val) - if new.HasElement(val).False() { - removedVals = append(removedVals, val) - } - } - for it := new.ElementIterator(); it.Next(); { - _, val := it.Element() - allVals = append(allVals, val) - if val.IsKnown() && old.HasElement(val).False() { - addedVals = append(addedVals, val) - } - } - - var all, added, removed cty.Value - if len(allVals) > 0 { - all = cty.SetVal(allVals) - } else { - all = cty.SetValEmpty(ty.ElementType()) - } - if len(addedVals) > 0 { - added = cty.SetVal(addedVals) - } else { - added = cty.SetValEmpty(ty.ElementType()) - } - if len(removedVals) > 0 { - removed = cty.SetVal(removedVals) - } else { - removed = cty.SetValEmpty(ty.ElementType()) - } - - suppressedElements := 0 - for it := all.ElementIterator(); it.Next(); { - _, val := it.Element() - - var action plans.Action - switch { - case !val.IsKnown(): - action = plans.Update - case added.HasElement(val).True(): - action = plans.Create - case removed.HasElement(val).True(): - action = plans.Delete - default: - action = plans.NoOp - } - - if action == plans.NoOp && !p.verbose { - suppressedElements++ - continue - } - - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.writeValue(val, action, indent+4) - p.buf.WriteString(",\n") - } - - if suppressedElements > 0 { - p.writeActionSymbol(plans.NoOp) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - noun := "elements" - if suppressedElements == 1 { - noun = "element" - } - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun)) - p.buf.WriteString("\n") - } - - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString("]") - return - case ty.IsListType() || ty.IsTupleType(): - p.buf.WriteString("[") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.buf.WriteString("\n") - - elemDiffs := ctySequenceDiff(old.AsValueSlice(), new.AsValueSlice()) - - // Maintain a stack of suppressed lines in the diff for later - // display or elision - var suppressedElements []*plans.Change - var changeShown bool - - for i := 0; i < len(elemDiffs); i++ { - if !p.verbose { - for i < len(elemDiffs) && elemDiffs[i].Action == plans.NoOp { - suppressedElements = append(suppressedElements, elemDiffs[i]) - i++ - } - } - - // If we have some suppressed elements on the stack… - if len(suppressedElements) > 0 { - // If we've just rendered a change, display the first - // element in the stack as context - if changeShown { - elemDiff := suppressedElements[0] - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeValue(elemDiff.After, elemDiff.Action, indent+4) - p.buf.WriteString(",\n") - suppressedElements = suppressedElements[1:] - } - - hidden := len(suppressedElements) - - // If we're not yet at the end of the list, capture the - // last element on the stack as context for the upcoming - // change to be rendered - var nextContextDiff *plans.Change - if hidden > 0 && i < len(elemDiffs) { - hidden-- - nextContextDiff = suppressedElements[hidden] - } - - // If there are still hidden elements, show an elision - // statement counting them - if hidden > 0 { - p.writeActionSymbol(plans.NoOp) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - noun := "elements" - if hidden == 1 { - noun = "element" - } - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), hidden, noun)) - p.buf.WriteString("\n") - } - - // Display the next context diff if it was captured above - if nextContextDiff != nil { - p.buf.WriteString(strings.Repeat(" ", indent+4)) - p.writeValue(nextContextDiff.After, nextContextDiff.Action, indent+4) - p.buf.WriteString(",\n") - } - - // Suppressed elements have now been handled so clear them again - suppressedElements = nil - } - - if i >= len(elemDiffs) { - break - } - - elemDiff := elemDiffs[i] - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(elemDiff.Action) - switch elemDiff.Action { - case plans.NoOp, plans.Delete: - p.writeValue(elemDiff.Before, elemDiff.Action, indent+4) - case plans.Update: - p.writeValueDiff(elemDiff.Before, elemDiff.After, indent+4, path) - case plans.Create: - p.writeValue(elemDiff.After, elemDiff.Action, indent+4) - default: - // Should never happen since the above covers all - // actions that ctySequenceDiff can return. - p.writeValue(elemDiff.After, elemDiff.Action, indent+4) - } - - p.buf.WriteString(",\n") - changeShown = true - } - - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString("]") - - return - - case ty.IsMapType(): - p.buf.WriteString("{") - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - p.buf.WriteString("\n") - - var allKeys []string - keyLen := 0 - for it := old.ElementIterator(); it.Next(); { - k, _ := it.Element() - keyStr := k.AsString() - allKeys = append(allKeys, keyStr) - if len(keyStr) > keyLen { - keyLen = len(keyStr) - } - } - for it := new.ElementIterator(); it.Next(); { - k, _ := it.Element() - keyStr := k.AsString() - allKeys = append(allKeys, keyStr) - if len(keyStr) > keyLen { - keyLen = len(keyStr) - } - } - - sort.Strings(allKeys) - - suppressedElements := 0 - lastK := "" - for i, k := range allKeys { - if i > 0 && lastK == k { - continue // skip duplicates (list is sorted) - } - lastK = k - - kV := cty.StringVal(k) - var action plans.Action - if old.HasIndex(kV).False() { - action = plans.Create - } else if new.HasIndex(kV).False() { - action = plans.Delete - } - - if old.HasIndex(kV).True() && new.HasIndex(kV).True() { - if ctyEqualValueAndMarks(old.Index(kV), new.Index(kV)) { - action = plans.NoOp - } else { - action = plans.Update - } - } - - if action == plans.NoOp && !p.verbose { - suppressedElements++ - continue - } - - path := append(path, cty.IndexStep{Key: kV}) - - oldV := old.Index(kV) - newV := new.Index(kV) - p.writeSensitivityWarning(oldV, newV, indent+2, action, false) - - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.writeValue(cty.StringVal(k), action, indent+4) - p.buf.WriteString(strings.Repeat(" ", keyLen-len(k))) - p.buf.WriteString(" = ") - switch action { - case plans.Create, plans.NoOp: - v := new.Index(kV) - if v.HasMark(marks.Sensitive) { - p.buf.WriteString("(sensitive)") - } else { - p.writeValue(v, action, indent+4) - } - case plans.Delete: - oldV := old.Index(kV) - newV := cty.NullVal(oldV.Type()) - p.writeValueDiff(oldV, newV, indent+4, path) - default: - if oldV.HasMark(marks.Sensitive) || newV.HasMark(marks.Sensitive) { - p.buf.WriteString("(sensitive)") - } else { - p.writeValueDiff(oldV, newV, indent+4, path) - } - } - - p.buf.WriteByte('\n') - } - - if suppressedElements > 0 { - p.writeActionSymbol(plans.NoOp) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - noun := "elements" - if suppressedElements == 1 { - noun = "element" - } - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun)) - p.buf.WriteString("\n") - } - - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString("}") - - return - case ty.IsObjectType(): - p.buf.WriteString("{") - p.buf.WriteString("\n") - - forcesNewResource := p.pathForcesNewResource(path) - - var allKeys []string - displayKeys := make(map[string]string) - keyLen := 0 - for it := old.ElementIterator(); it.Next(); { - k, _ := it.Element() - keyStr := k.AsString() - allKeys = append(allKeys, keyStr) - displayKeys[keyStr] = displayAttributeName(keyStr) - if len(displayKeys[keyStr]) > keyLen { - keyLen = len(displayKeys[keyStr]) - } - } - for it := new.ElementIterator(); it.Next(); { - k, _ := it.Element() - keyStr := k.AsString() - allKeys = append(allKeys, keyStr) - displayKeys[keyStr] = displayAttributeName(keyStr) - if len(displayKeys[keyStr]) > keyLen { - keyLen = len(displayKeys[keyStr]) - } - } - - sort.Strings(allKeys) - - suppressedElements := 0 - lastK := "" - for i, k := range allKeys { - if i > 0 && lastK == k { - continue // skip duplicates (list is sorted) - } - lastK = k - - kV := k - var action plans.Action - if !old.Type().HasAttribute(kV) { - action = plans.Create - } else if !new.Type().HasAttribute(kV) { - action = plans.Delete - } else if ctyEqualValueAndMarks(old.GetAttr(kV), new.GetAttr(kV)) { - action = plans.NoOp - } else { - action = plans.Update - } - - // TODO: If in future we have a schema associated with this - // object, we should pass the attribute's schema to - // identifyingAttribute here. - if action == plans.NoOp && !p.verbose && !identifyingAttribute(k, nil) { - suppressedElements++ - continue - } - - path := append(path, cty.GetAttrStep{Name: kV}) - - p.buf.WriteString(strings.Repeat(" ", indent+2)) - p.writeActionSymbol(action) - p.buf.WriteString(displayKeys[k]) - p.buf.WriteString(strings.Repeat(" ", keyLen-len(displayKeys[k]))) - p.buf.WriteString(" = ") - - switch action { - case plans.Create, plans.NoOp: - v := new.GetAttr(kV) - p.writeValue(v, action, indent+4) - case plans.Delete: - oldV := old.GetAttr(kV) - newV := cty.NullVal(oldV.Type()) - p.writeValueDiff(oldV, newV, indent+4, path) - default: - oldV := old.GetAttr(kV) - newV := new.GetAttr(kV) - p.writeValueDiff(oldV, newV, indent+4, path) - } - - p.buf.WriteString("\n") - } - - if suppressedElements > 0 { - p.writeActionSymbol(plans.NoOp) - p.buf.WriteString(strings.Repeat(" ", indent+2)) - noun := "elements" - if suppressedElements == 1 { - noun = "element" - } - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), suppressedElements, noun)) - p.buf.WriteString("\n") - } - - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString("}") - - if forcesNewResource { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } - return - } - } - - // In all other cases, we just show the new and old values as-is - p.writeValue(old, plans.Delete, indent) - if new.IsNull() { - p.buf.WriteString(p.color.Color(" [dark_gray]->[reset] ")) - } else { - p.buf.WriteString(p.color.Color(" [yellow]->[reset] ")) - } - - p.writeValue(new, plans.Create, indent) - if p.pathForcesNewResource(path) { - p.buf.WriteString(p.color.Color(forcesNewResourceCaption)) - } -} - -// writeActionSymbol writes a symbol to represent the given action, followed -// by a space. -// -// It only supports the actions that can be represented with a single character: -// Create, Delete, Update and NoAction. -func (p *blockBodyDiffPrinter) writeActionSymbol(action plans.Action) { - switch action { - case plans.Create: - p.buf.WriteString(p.color.Color("[green]+[reset] ")) - case plans.Delete: - p.buf.WriteString(p.color.Color("[red]-[reset] ")) - case plans.Update: - p.buf.WriteString(p.color.Color("[yellow]~[reset] ")) - case plans.NoOp: - p.buf.WriteString(" ") - default: - // Should never happen - p.buf.WriteString(p.color.Color("? ")) - } -} - -func (p *blockBodyDiffPrinter) writeSensitivityWarning(old, new cty.Value, indent int, action plans.Action, isBlock bool) { - // Dont' show this warning for create or delete - if action == plans.Create || action == plans.Delete { - return - } - - // Customize the warning based on if it is an attribute or block - diffType := "attribute value" - if isBlock { - diffType = "block" - } - - // If only attribute sensitivity is changing, clarify that the value is unchanged - var valueUnchangedSuffix string - if !isBlock { - oldUnmarked, _ := old.UnmarkDeep() - newUnmarked, _ := new.UnmarkDeep() - if oldUnmarked.RawEquals(newUnmarked) { - valueUnchangedSuffix = " The value is unchanged." - } - } - - if new.HasMark(marks.Sensitive) && !old.HasMark(marks.Sensitive) { - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will be marked as sensitive and will not\n"), diffType)) - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString(fmt.Sprintf("# display in UI output after applying this change.%s\n", valueUnchangedSuffix)) - } - - // Note if changing this attribute will change its sensitivity - if old.HasMark(marks.Sensitive) && !new.HasMark(marks.Sensitive) { - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString(fmt.Sprintf(p.color.Color("# [yellow]Warning:[reset] this %s will no longer be marked as sensitive\n"), diffType)) - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString(fmt.Sprintf("# after applying this change.%s\n", valueUnchangedSuffix)) - } -} - -func (p *blockBodyDiffPrinter) pathForcesNewResource(path cty.Path) bool { - if !p.action.IsReplace() || p.requiredReplace.Empty() { - // "requiredReplace" only applies when the instance is being replaced, - // and we should only inspect that set if it is not empty - return false - } - return p.requiredReplace.Has(path) -} - -func ctyEmptyString(value cty.Value) bool { - if !value.IsNull() && value.IsKnown() { - valueType := value.Type() - if valueType == cty.String && value.AsString() == "" { - return true - } - } - return false -} - -func ctyGetAttrMaybeNull(val cty.Value, name string) cty.Value { - attrType := val.Type().AttributeType(name) - - if val.IsNull() { - return cty.NullVal(attrType) - } - - // We treat "" as null here - // as existing SDK doesn't support null yet. - // This allows us to avoid spurious diffs - // until we introduce null to the SDK. - attrValue := val.GetAttr(name) - // If the value is marked, the ctyEmptyString function will fail - if !val.ContainsMarked() && ctyEmptyString(attrValue) { - return cty.NullVal(attrType) - } - - return attrValue -} - -func ctyCollectionValues(val cty.Value) []cty.Value { - if !val.IsKnown() || val.IsNull() { - return nil - } - - ret := make([]cty.Value, 0, val.LengthInt()) - for it := val.ElementIterator(); it.Next(); { - _, value := it.Element() - ret = append(ret, value) - } - return ret -} - -// ctySequenceDiff returns differences between given sequences of cty.Value(s) -// in the form of Create, Delete, or Update actions (for objects). -func ctySequenceDiff(old, new []cty.Value) []*plans.Change { - var ret []*plans.Change - lcs := objchange.LongestCommonSubsequence(old, new) - var oldI, newI, lcsI int - for oldI < len(old) || newI < len(new) || lcsI < len(lcs) { - // We first process items in the old and new sequences which are not - // equal to the current common sequence item. Old items are marked as - // deletions, and new items are marked as additions. - // - // There is an exception for deleted & created object items, which we - // try to render as updates where that makes sense. - for oldI < len(old) && (lcsI >= len(lcs) || !old[oldI].RawEquals(lcs[lcsI])) { - // Render this as an object update if all of these are true: - // - // - the current old item is an object; - // - there's a current new item which is also an object; - // - either there are no common items left, or the current new item - // doesn't equal the current common item. - // - // Why do we need the the last clause? If we have current items in all - // three sequences, and the current new item is equal to a common item, - // then we should just need to advance the old item list and we'll - // eventually find a common item matching both old and new. - // - // This combination of conditions allows us to render an object update - // diff instead of a combination of delete old & create new. - isObjectDiff := old[oldI].Type().IsObjectType() && newI < len(new) && new[newI].Type().IsObjectType() && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) - if isObjectDiff { - ret = append(ret, &plans.Change{ - Action: plans.Update, - Before: old[oldI], - After: new[newI], - }) - oldI++ - newI++ // we also consume the next "new" in this case - continue - } - - // Otherwise, this item is not part of the common sequence, so - // render as a deletion. - ret = append(ret, &plans.Change{ - Action: plans.Delete, - Before: old[oldI], - After: cty.NullVal(old[oldI].Type()), - }) - oldI++ - } - for newI < len(new) && (lcsI >= len(lcs) || !new[newI].RawEquals(lcs[lcsI])) { - ret = append(ret, &plans.Change{ - Action: plans.Create, - Before: cty.NullVal(new[newI].Type()), - After: new[newI], - }) - newI++ - } - - // When we've exhausted the old & new sequences of items which are not - // in the common subsequence, we render a common item and continue. - if lcsI < len(lcs) { - ret = append(ret, &plans.Change{ - Action: plans.NoOp, - Before: lcs[lcsI], - After: lcs[lcsI], - }) - - // All of our indexes advance together now, since the line - // is common to all three sequences. - lcsI++ - oldI++ - newI++ - } - } - return ret -} - -// ctyEqualValueAndMarks checks equality of two possibly-marked values, -// considering partially-unknown values and equal values with different marks -// as inequal -func ctyEqualWithUnknown(old, new cty.Value) bool { - if !old.IsWhollyKnown() || !new.IsWhollyKnown() { - return false - } - return ctyEqualValueAndMarks(old, new) -} - -// ctyEqualValueAndMarks checks equality of two possibly-marked values, -// considering equal values with different marks as inequal -func ctyEqualValueAndMarks(old, new cty.Value) bool { - oldUnmarked, oldMarks := old.UnmarkDeep() - newUnmarked, newMarks := new.UnmarkDeep() - sameValue := oldUnmarked.Equals(newUnmarked) - return sameValue.IsKnown() && sameValue.True() && oldMarks.Equal(newMarks) -} - -// ctyTypesEqual checks equality of two types more loosely -// by avoiding checks of object/tuple elements -// as we render differences on element-by-element basis anyway -func ctyTypesEqual(oldT, newT cty.Type) bool { - if oldT.IsObjectType() && newT.IsObjectType() { - return true - } - if oldT.IsTupleType() && newT.IsTupleType() { - return true - } - return oldT.Equals(newT) -} - -func ctyEnsurePathCapacity(path cty.Path, minExtra int) cty.Path { - if cap(path)-len(path) >= minExtra { - return path - } - newCap := cap(path) * 2 - if newCap < (len(path) + minExtra) { - newCap = len(path) + minExtra - } - newPath := make(cty.Path, len(path), newCap) - copy(newPath, path) - return newPath -} - -// ctyNullBlockListAsEmpty either returns the given value verbatim if it is non-nil -// or returns an empty value of a suitable type to serve as a placeholder for it. -// -// In particular, this function handles the special situation where a "list" is -// actually represented as a tuple type where nested blocks contain -// dynamically-typed values. -func ctyNullBlockListAsEmpty(in cty.Value) cty.Value { - if !in.IsNull() { - return in - } - if ty := in.Type(); ty.IsListType() { - return cty.ListValEmpty(ty.ElementType()) - } - return cty.EmptyTupleVal // must need a tuple, then -} - -// ctyNullBlockMapAsEmpty either returns the given value verbatim if it is non-nil -// or returns an empty value of a suitable type to serve as a placeholder for it. -// -// In particular, this function handles the special situation where a "map" is -// actually represented as an object type where nested blocks contain -// dynamically-typed values. -func ctyNullBlockMapAsEmpty(in cty.Value) cty.Value { - if !in.IsNull() { - return in - } - if ty := in.Type(); ty.IsMapType() { - return cty.MapValEmpty(ty.ElementType()) - } - return cty.EmptyObjectVal // must need an object, then -} - -// ctyNullBlockSetAsEmpty either returns the given value verbatim if it is non-nil -// or returns an empty value of a suitable type to serve as a placeholder for it. -func ctyNullBlockSetAsEmpty(in cty.Value) cty.Value { - if !in.IsNull() { - return in - } - // Dynamically-typed attributes are not supported inside blocks backed by - // sets, so our result here is always a set. - return cty.SetValEmpty(in.Type().ElementType()) -} - -// DiffActionSymbol returns a string that, once passed through a -// colorstring.Colorize, will produce a result that can be written -// to a terminal to produce a symbol made of three printable -// characters, possibly interspersed with VT100 color codes. -func DiffActionSymbol(action plans.Action) string { - switch action { - case plans.DeleteThenCreate: - return "[red]-[reset]/[green]+[reset]" - case plans.CreateThenDelete: - return "[green]+[reset]/[red]-[reset]" - case plans.Create: - return " [green]+[reset]" - case plans.Delete: - return " [red]-[reset]" - case plans.Read: - return " [cyan]<=[reset]" - case plans.Update: - return " [yellow]~[reset]" - default: - return " ?" - } -} - -// Extremely coarse heuristic for determining whether or not a given attribute -// name is important for identifying a resource. In the future, this may be -// replaced by a flag in the schema, but for now this is likely to be good -// enough. -func identifyingAttribute(name string, attrSchema *configschema.Attribute) bool { - return name == "id" || name == "tags" || name == "name" -} - -func (p *blockBodyDiffPrinter) writeSkippedAttr(skipped, indent int) { - if skipped > 0 { - noun := "attributes" - if skipped == 1 { - noun = "attribute" - } - p.buf.WriteString("\n") - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun)) - } -} - -func (p *blockBodyDiffPrinter) writeSkippedElems(skipped, indent int) { - if skipped > 0 { - noun := "elements" - if skipped == 1 { - noun = "element" - } - p.buf.WriteString(strings.Repeat(" ", indent)) - p.buf.WriteString(fmt.Sprintf(p.color.Color("[dark_gray]# (%d unchanged %s hidden)[reset]"), skipped, noun)) - p.buf.WriteString("\n") - } -} - -func displayAttributeName(name string) string { - if !hclsyntax.ValidIdentifier(name) { - return fmt.Sprintf("%q", name) - } - return name -} diff --git a/internal/command/format/difflanguage_string.go b/internal/command/format/difflanguage_string.go deleted file mode 100644 index 8399cddc46..0000000000 --- a/internal/command/format/difflanguage_string.go +++ /dev/null @@ -1,29 +0,0 @@ -// Code generated by "stringer -type=DiffLanguage diff.go"; DO NOT EDIT. - -package format - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[DiffLanguageProposedChange-80] - _ = x[DiffLanguageDetectedDrift-68] -} - -const ( - _DiffLanguage_name_0 = "DiffLanguageDetectedDrift" - _DiffLanguage_name_1 = "DiffLanguageProposedChange" -) - -func (i DiffLanguage) String() string { - switch { - case i == 68: - return _DiffLanguage_name_0 - case i == 80: - return _DiffLanguage_name_1 - default: - return "DiffLanguage(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/internal/command/format/format.go b/internal/command/format/format.go index aa8d7deb2a..33ed9243e5 100644 --- a/internal/command/format/format.go +++ b/internal/command/format/format.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package format contains helpers for formatting various Terraform // structures for human-readabout output. // @@ -6,3 +9,34 @@ // output formatting as much as possible so that text formats of Terraform // structures have a consistent look and feel. package format + +import "github.com/hashicorp/terraform/internal/plans" + +// DiffActionSymbol returns a string that, once passed through a +// colorstring.Colorize, will produce a result that can be written +// to a terminal to produce a symbol made of three printable +// characters, possibly interspersed with VT100 color codes. +func DiffActionSymbol(action plans.Action) string { + switch action { + case plans.DeleteThenCreate: + return "[red]-[reset]/[green]+[reset]" + case plans.CreateThenDelete: + return "[green]+[reset]/[red]-[reset]" + case plans.Create: + return " [green]+[reset]" + case plans.Delete: + return " [red]-[reset]" + case plans.Forget: + return " [red].[reset]" + case plans.CreateThenForget: + return " [green]+[reset]/[red].[reset]" + case plans.Read: + return " [cyan]<=[reset]" + case plans.Update: + return " [yellow]~[reset]" + case plans.NoOp: + return " " + default: + return " ?" + } +} diff --git a/internal/command/format/object_id.go b/internal/command/format/object_id.go index 75b427b8d4..d9036135aa 100644 --- a/internal/command/format/object_id.go +++ b/internal/command/format/object_id.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package format import ( diff --git a/internal/command/format/object_id_test.go b/internal/command/format/object_id_test.go index 2f13e1366c..edcd4f045c 100644 --- a/internal/command/format/object_id_test.go +++ b/internal/command/format/object_id_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package format import ( diff --git a/internal/command/format/state.go b/internal/command/format/state.go deleted file mode 100644 index d0db1cc3dd..0000000000 --- a/internal/command/format/state.go +++ /dev/null @@ -1,216 +0,0 @@ -package format - -import ( - "bytes" - "fmt" - "sort" - "strings" - - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/terraform" - "github.com/mitchellh/colorstring" -) - -// StateOpts are the options for formatting a state. -type StateOpts struct { - // State is the state to format. This is required. - State *states.State - - // Schemas are used to decode attributes. This is required. - Schemas *terraform.Schemas - - // Color is the colorizer. This is optional. - Color *colorstring.Colorize -} - -// State takes a state and returns a string -func State(opts *StateOpts) string { - if opts.Color == nil { - panic("colorize not given") - } - - if opts.Schemas == nil { - panic("schemas not given") - } - - s := opts.State - if len(s.Modules) == 0 { - return "The state file is empty. No resources are represented." - } - - buf := bytes.NewBufferString("[reset]") - p := blockBodyDiffPrinter{ - buf: buf, - color: opts.Color, - action: plans.NoOp, - verbose: true, - } - - // Format all the modules - for _, m := range s.Modules { - formatStateModule(p, m, opts.Schemas) - } - - // Write the outputs for the root module - m := s.RootModule() - - if m.OutputValues != nil { - if len(m.OutputValues) > 0 { - p.buf.WriteString("Outputs:\n\n") - } - - // Sort the outputs - ks := make([]string, 0, len(m.OutputValues)) - for k := range m.OutputValues { - ks = append(ks, k) - } - sort.Strings(ks) - - // Output each output k/v pair - for _, k := range ks { - v := m.OutputValues[k] - p.buf.WriteString(fmt.Sprintf("%s = ", k)) - if v.Sensitive { - p.buf.WriteString("(sensitive value)") - } else { - p.writeValue(v.Value, plans.NoOp, 0) - } - p.buf.WriteString("\n") - } - } - - trimmedOutput := strings.TrimSpace(p.buf.String()) - trimmedOutput += "[reset]" - - return opts.Color.Color(trimmedOutput) - -} - -func formatStateModule(p blockBodyDiffPrinter, m *states.Module, schemas *terraform.Schemas) { - // First get the names of all the resources so we can show them - // in alphabetical order. - names := make([]string, 0, len(m.Resources)) - for name := range m.Resources { - names = append(names, name) - } - sort.Strings(names) - - // Go through each resource and begin building up the output. - for _, key := range names { - for k, v := range m.Resources[key].Instances { - // keep these in order to keep the current object first, and - // provide deterministic output for the deposed objects - type obj struct { - header string - instance *states.ResourceInstanceObjectSrc - } - instances := []obj{} - - addr := m.Resources[key].Addr - resAddr := addr.Resource - - taintStr := "" - if v.Current != nil && v.Current.Status == 'T' { - taintStr = " (tainted)" - } - - instances = append(instances, - obj{fmt.Sprintf("# %s:%s\n", addr.Instance(k), taintStr), v.Current}) - - for dk, v := range v.Deposed { - instances = append(instances, - obj{fmt.Sprintf("# %s: (deposed object %s)\n", addr.Instance(k), dk), v}) - } - - // Sort the instances for consistent output. - // Starting the sort from the second index, so the current instance - // is always first. - sort.Slice(instances[1:], func(i, j int) bool { - return instances[i+1].header < instances[j+1].header - }) - - for _, obj := range instances { - header := obj.header - instance := obj.instance - p.buf.WriteString(header) - if instance == nil { - // this shouldn't happen, but there's nothing to do here so - // don't panic below. - continue - } - - var schema *configschema.Block - - provider := m.Resources[key].ProviderConfig.Provider - if _, exists := schemas.Providers[provider]; !exists { - // This should never happen in normal use because we should've - // loaded all of the schemas and checked things prior to this - // point. We can't return errors here, but since this is UI code - // we will try to do _something_ reasonable. - p.buf.WriteString(fmt.Sprintf("# missing schema for provider %q\n\n", provider.String())) - continue - } - - switch resAddr.Mode { - case addrs.ManagedResourceMode: - schema, _ = schemas.ResourceTypeConfig( - provider, - resAddr.Mode, - resAddr.Type, - ) - if schema == nil { - p.buf.WriteString(fmt.Sprintf( - "# missing schema for provider %q resource type %s\n\n", provider, resAddr.Type)) - continue - } - - p.buf.WriteString(fmt.Sprintf( - "resource %q %q {", - resAddr.Type, - resAddr.Name, - )) - case addrs.DataResourceMode: - schema, _ = schemas.ResourceTypeConfig( - provider, - resAddr.Mode, - resAddr.Type, - ) - if schema == nil { - p.buf.WriteString(fmt.Sprintf( - "# missing schema for provider %q data source %s\n\n", provider, resAddr.Type)) - continue - } - - p.buf.WriteString(fmt.Sprintf( - "data %q %q {", - resAddr.Type, - resAddr.Name, - )) - default: - // should never happen, since the above is exhaustive - p.buf.WriteString(resAddr.String()) - } - - val, err := instance.Decode(schema.ImpliedType()) - if err != nil { - fmt.Println(err.Error()) - break - } - - path := make(cty.Path, 0, 3) - result := p.writeBlockBodyDiff(schema, val.Value, val.Value, 2, path) - if result.bodyWritten { - p.buf.WriteString("\n") - } - - p.buf.WriteString("}\n\n") - } - } - } - p.buf.WriteString("\n") -} diff --git a/internal/command/format/trivia.go b/internal/command/format/trivia.go index b97d50b0e0..17852d33f9 100644 --- a/internal/command/format/trivia.go +++ b/internal/command/format/trivia.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package format import ( @@ -13,7 +16,7 @@ import ( // If the given colorize has colors enabled, the rule will also be given a // dark grey color to attempt to visually de-emphasize it for sighted users. // -// This is intended for printing to the UI via mitchellh/cli.UI.Output, or +// This is intended for printing to the UI via hashicorp/cli.UI.Output, or // similar, which will automatically append a trailing newline too. func HorizontalRule(color *colorstring.Colorize, width int) string { if width <= 1 { diff --git a/internal/command/get.go b/internal/command/get.go index 0f541c3b1e..f87988f32d 100644 --- a/internal/command/get.go +++ b/internal/command/get.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( + "context" "fmt" "strings" @@ -15,16 +19,22 @@ type GetCommand struct { func (c *GetCommand) Run(args []string) int { var update bool + var testsDirectory string args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("get") cmdFlags.BoolVar(&update, "update", false, "update") + cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) return 1 } + // Initialization can be aborted by interruption signals + ctx, done := c.InterruptibleContext(c.CommandContext()) + defer done() + path, err := ModulePath(cmdFlags.Args()) if err != nil { c.Ui.Error(err.Error()) @@ -33,7 +43,7 @@ func (c *GetCommand) Run(args []string) int { path = c.normalizePath(path) - abort, diags := getModules(&c.Meta, path, update) + abort, diags := getModules(ctx, &c.Meta, path, testsDirectory, update) c.showDiagnostics(diags) if abort || diags.HasErrors() { return 1 @@ -44,10 +54,10 @@ func (c *GetCommand) Run(args []string) int { func (c *GetCommand) Help() string { helpText := ` -Usage: terraform [global options] get [options] PATH +Usage: terraform [global options] get [options] - Downloads and installs modules needed for the configuration given by - PATH. + Downloads and installs modules needed for the configuration in the + current working directory. This recursively downloads all modules needed, such as modules imported by modules imported by the root and so on. If a module is @@ -60,10 +70,12 @@ Usage: terraform [global options] get [options] PATH Options: - -update Check already-downloaded modules for available updates - and install the newest versions available. + -update Check already-downloaded modules for available updates + and install the newest versions available. - -no-color Disable text coloring in the output. + -no-color Disable text coloring in the output. + + -test-directory=path Set the Terraform test directory, defaults to "tests". ` return strings.TrimSpace(helpText) @@ -73,10 +85,10 @@ func (c *GetCommand) Synopsis() string { return "Install or upgrade remote Terraform modules" } -func getModules(m *Meta, path string, upgrade bool) (abort bool, diags tfdiags.Diagnostics) { +func getModules(ctx context.Context, m *Meta, path string, testsDir string, upgrade bool) (abort bool, diags tfdiags.Diagnostics) { hooks := uiModuleInstallHooks{ Ui: m.Ui, ShowLocalPaths: true, } - return m.installModules(path, upgrade, hooks) + return m.installModules(ctx, path, testsDir, upgrade, true, hooks) } diff --git a/internal/command/get_test.go b/internal/command/get_test.go index 3f9cc300b5..0f2222cb83 100644 --- a/internal/command/get_test.go +++ b/internal/command/get_test.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) func TestGet(t *testing.T) { diff --git a/internal/command/graph.go b/internal/command/graph.go index 4fe7428049..b98f6edd4d 100644 --- a/internal/command/graph.go +++ b/internal/command/graph.go @@ -1,10 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" + "sort" "strings" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" @@ -51,7 +57,7 @@ func (c *GraphCommand) Run(args []string) int { } // Try to load plan if path is specified - var planFile *planfile.Reader + var planFile *planfile.WrappedPlanFile if planPath != "" { planFile, err = c.PlanFile(planPath) if err != nil { @@ -80,7 +86,7 @@ func (c *GraphCommand) Run(args []string) int { } // We require a local backend - local, ok := b.(backend.Local) + local, ok := b.(backendrun.Local) if !ok { c.showDiagnostics(diags) // in case of any warnings in here c.Ui.Error(ErrUnsupportedLocalOp) @@ -91,7 +97,7 @@ func (c *GraphCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(b) // Build the operation - opReq := c.Operation(b) + opReq := c.Operation(b, arguments.ViewHuman) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() opReq.PlanFile = planFile @@ -109,13 +115,26 @@ func (c *GraphCommand) Run(args []string) int { c.showDiagnostics(diags) return 1 } + lr.Core.SetGraphOpts(&terraform.ContextGraphOpts{SkipGraphValidation: drawCycles}) if graphTypeStr == "" { - switch { - case lr.Plan != nil: + if planFile == nil { + // Simple resource dependency mode: + // This is based on the plan graph but we then further reduce it down + // to just resource dependency relationships, assuming that in most + // cases the most important thing is what order we'll visit the + // resources in. + fullG, graphDiags := lr.Core.PlanGraphForUI(lr.Config, lr.InputState, plans.NormalMode) + diags = diags.Append(graphDiags) + if graphDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + g := fullG.ResourceGraph() + return c.resourceOnlyGraph(g) + } else { graphTypeStr = "apply" - default: - graphTypeStr = "plan" } } @@ -136,7 +155,7 @@ func (c *GraphCommand) Run(args []string) int { // here, though perhaps one day this should be an error. if lr.Plan == nil { plan = &plans.Plan{ - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), UIMode: plans.NormalMode, PriorState: lr.InputState, PrevRunState: lr.InputState, @@ -185,11 +204,103 @@ func (c *GraphCommand) Run(args []string) int { return 1 } - c.Ui.Output(graphStr) + _, err = c.Streams.Stdout.File.WriteString(graphStr) + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to write graph to stdout: %s", err)) + return 1 + } return 0 } +func (c *GraphCommand) resourceOnlyGraph(graph addrs.DirectedGraph[addrs.ConfigResource]) int { + out := c.Streams.Stdout.File + fmt.Fprintln(out, "digraph G {") + // Horizontal presentation is easier to read because our nodes tend + // to be much wider than they are tall. The leftmost nodes in the output + // are those Terraform would visit first. + fmt.Fprintln(out, " rankdir = \"RL\";") + fmt.Fprintln(out, " node [shape = rect, fontname = \"sans-serif\"];") + + // To help relate the output back to the configuration it came from, + // and to make the individual node labels more reasonably sized when + // deeply nested inside modules, we'll cluster the nodes together by + // the module they belong to and then show only the local resource + // address in the individual nodes. We'll accomplish that by sorting + // the nodes first by module, so we can then notice the transitions. + allAddrs := graph.AllNodes() + if len(allAddrs) == 0 { + fmt.Fprintln(out, " /* This configuration does not contain any resources. */") + fmt.Fprintln(out, " /* For a more detailed graph, try: terraform graph -type=plan */") + } + addrsOrder := make([]addrs.ConfigResource, 0, len(allAddrs)) + for _, addr := range allAddrs { + addrsOrder = append(addrsOrder, addr) + } + sort.Slice(addrsOrder, func(i, j int) bool { + iAddr, jAddr := addrsOrder[i], addrsOrder[j] + iModStr, jModStr := iAddr.Module.String(), jAddr.Module.String() + switch { + case iModStr != jModStr: + return iModStr < jModStr + default: + iRes, jRes := iAddr.Resource, jAddr.Resource + switch { + case iRes.Mode != jRes.Mode: + return iRes.Mode == addrs.DataResourceMode + case iRes.Type != jRes.Type: + return iRes.Type < jRes.Type + default: + return iRes.Name < jRes.Name + } + } + }) + + currentMod := addrs.RootModule + for _, addr := range addrsOrder { + if !addr.Module.Equal(currentMod) { + // We need a new subgraph, then. + // Experimentally it seems like nested clusters tend to make it + // hard for dot to converge on a good layout, so we'll stick with + // just one level of clusters for now but could revise later based + // on feedback. + if !currentMod.IsRoot() { + fmt.Fprintln(out, " }") + } + currentMod = addr.Module + fmt.Fprintf(out, " subgraph \"cluster_%s\" {\n", currentMod.String()) + fmt.Fprintf(out, " label = %q\n", currentMod.String()) + fmt.Fprintf(out, " fontname = %q\n", "sans-serif") + } + if currentMod.IsRoot() { + fmt.Fprintf(out, " %q [label=%q];\n", addr.String(), addr.Resource.String()) + } else { + fmt.Fprintf(out, " %q [label=%q];\n", addr.String(), addr.Resource.String()) + } + } + if !currentMod.IsRoot() { + fmt.Fprintln(out, " }") + } + + // Now we'll emit all of the edges. + // We use addrsOrder for both levels to ensure a consistent ordering between + // runs without further sorting, which means we visit more nodes than we + // really need to but this output format is only really useful for relatively + // small graphs anyway, so this should be fine. + for _, sourceAddr := range addrsOrder { + deps := graph.DirectDependenciesOf(sourceAddr) + for _, targetAddr := range addrsOrder { + if !deps.Has(targetAddr) { + continue + } + fmt.Fprintf(out, " %q -> %q;\n", sourceAddr.String(), targetAddr.String()) + } + } + + fmt.Fprintln(out, "}") + return 0 +} + func (c *GraphCommand) Help() string { helpText := ` Usage: terraform [global options] graph [options] @@ -197,6 +308,12 @@ Usage: terraform [global options] graph [options] Produces a representation of the dependency graph between different objects in the current configuration and state. + By default the graph shows a summary only of the relationships between + resources in the configuration, since those are the main objects that + have side-effects whose ordering is significant. You can generate more + detailed graphs reflecting Terraform's actual evaluation strategy + by specifying the -type=TYPE option to select an operation type. + The graph is presented in the DOT language. The typical program that can read this format is GraphViz, but many web services are also available to read this format. @@ -204,17 +321,22 @@ Usage: terraform [global options] graph [options] Options: -plan=tfplan Render graph using the specified plan file instead of the - configuration in the current directory. + configuration in the current directory. Implies -type=apply. -draw-cycles Highlight any cycles in the graph with colored edges. - This helps when diagnosing cycle errors. + This helps when diagnosing cycle errors. This option is + supported only when illustrating a real evaluation graph, + selected using the -type=TYPE option. - -type=plan Type of graph to output. Can be: plan, plan-refresh-only, - plan-destroy, or apply. By default Terraform chooses - "plan", or "apply" if you also set the -plan=... option. + -type=TYPE Type of operation graph to output. Can be: plan, + plan-refresh-only, plan-destroy, or apply. By default + Terraform just summarizes the relationships between the + resources in your configuration, without any particular + operation in mind. Full operation graphs are more detailed + but therefore often harder to read. -module-depth=n (deprecated) In prior versions of Terraform, specified the - depth of modules to show in the output. + depth of modules to show in the output. ` return strings.TrimSpace(helpText) } diff --git a/internal/command/graph_test.go b/internal/command/graph_test.go index f58f7103e9..5d03fdb0c6 100644 --- a/internal/command/graph_test.go +++ b/internal/command/graph_test.go @@ -1,39 +1,170 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( + "context" "os" "strings" "testing" - "github.com/mitchellh/cli" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terminal" ) -func TestGraph(t *testing.T) { +func TestGraph_planPhase(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("graph"), td) defer testChdir(t, td)() ui := new(cli.MockUi) + streams, closeStreams := terminal.StreamsForTesting(t) c := &GraphCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(applyFixtureProvider()), Ui: ui, + Streams: streams, }, } - args := []string{} + args := []string{"-type=plan"} if code := c.Run(args); code != 0 { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } - output := ui.OutputWriter.String() - if !strings.Contains(output, `provider[\"registry.terraform.io/hashicorp/test\"]`) { - t.Fatalf("doesn't look like digraph: %s", output) + output := closeStreams(t) + if !strings.Contains(output.Stdout(), `provider[\"registry.terraform.io/hashicorp/test\"]`) { + t.Fatalf("doesn't look like digraph:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr()) + } +} + +func TestGraph_cyclic(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("graph-cyclic"), td) + defer testChdir(t, td)() + + tests := []struct { + name string + args []string + expected string + + // The cyclic errors do not maintain a consistent order, so we can't + // predict the exact output. We'll just check that the error messages + // are present for the things we know are cyclic. + errors []string + }{ + { + name: "plan", + args: []string{"-type=plan"}, + errors: []string{`Error: Cycle: test_instance.`, + `Error: Cycle: local.`}, + }, + { + name: "plan with -draw-cycles option", + args: []string{"-draw-cycles", "-type=plan"}, + expected: `digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] provider[\"registry.terraform.io/hashicorp/test\"]" [label = "provider[\"registry.terraform.io/hashicorp/test\"]", shape = "diamond"] + "[root] test_instance.bar (expand)" [label = "test_instance.bar", shape = "box"] + "[root] test_instance.foo (expand)" [label = "test_instance.foo", shape = "box"] + "[root] local.test1 (expand)" -> "[root] local.test2 (expand)" + "[root] local.test2 (expand)" -> "[root] local.test1 (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"]" + "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" -> "[root] test_instance.bar (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" -> "[root] test_instance.foo (expand)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" + "[root] test_instance.bar (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"]" + "[root] test_instance.bar (expand)" -> "[root] test_instance.foo (expand)" [color = "red", penwidth = "2.0"] + "[root] test_instance.foo (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"]" + "[root] test_instance.foo (expand)" -> "[root] test_instance.bar (expand)" [color = "red", penwidth = "2.0"] + } +}`, + }, + { + name: "apply", + args: []string{"-type=apply"}, + // The cyclic errors do not maintain a consistent order, so we can't + // predict the exact output. We'll just check that the error messages + // are present for the things we know are cyclic. + errors: []string{`Error: Cycle: test_instance.`, + `Error: Cycle: local.`}, + }, + { + name: "apply with -draw-cycles option", + args: []string{"-draw-cycles", "-type=apply"}, + expected: `digraph { + compound = "true" + newrank = "true" + subgraph "root" { + "[root] provider[\"registry.terraform.io/hashicorp/test\"]" [label = "provider[\"registry.terraform.io/hashicorp/test\"]", shape = "diamond"] + "[root] test_instance.bar (expand)" [label = "test_instance.bar", shape = "box"] + "[root] test_instance.foo (expand)" [label = "test_instance.foo", shape = "box"] + "[root] local.test1 (expand)" -> "[root] local.test2 (expand)" + "[root] local.test2 (expand)" -> "[root] local.test1 (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"]" + "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" -> "[root] test_instance.bar (expand)" + "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" -> "[root] test_instance.foo (expand)" + "[root] root" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"] (close)" + "[root] test_instance.bar (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"]" + "[root] test_instance.bar (expand)" -> "[root] test_instance.foo (expand)" [color = "red", penwidth = "2.0"] + "[root] test_instance.foo (expand)" -> "[root] provider[\"registry.terraform.io/hashicorp/test\"]" + "[root] test_instance.foo (expand)" -> "[root] test_instance.bar (expand)" [color = "red", penwidth = "2.0"] + } +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ui := new(cli.MockUi) + streams, closeStreams := terminal.StreamsForTesting(t) + c := &GraphCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(applyFixtureProvider()), + Ui: ui, + Streams: streams, + }, + } + + code := c.Run(tt.args) + // If we expect errors, make sure they are present + if len(tt.errors) > 0 { + if code == 0 { + t.Fatalf("expected error, got none") + } + got := strings.TrimSpace(ui.ErrorWriter.String()) + for _, err := range tt.errors { + if !strings.Contains(got, err) { + t.Fatalf("expected error:\n%s\n\nactual error:\n%s", err, got) + } + } + return + } + + // If we don't expect errors, make sure the command ran successfully + if code != 0 { + t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + } + output := closeStreams(t) + if strings.TrimSpace(output.Stdout()) != strings.TrimSpace(tt.expected) { + t.Fatalf("expected dot graph to match:\n%s", cmp.Diff(output.Stdout(), tt.expected)) + } + + }) } } @@ -55,40 +186,19 @@ func TestGraph_multipleArgs(t *testing.T) { } } -func TestGraph_noArgs(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath("graph"), td) - defer testChdir(t, td)() - - ui := new(cli.MockUi) - c := &GraphCommand{ - Meta: Meta{ - testingOverrides: metaOverridesForProvider(applyFixtureProvider()), - Ui: ui, - }, - } - - args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) - } - - output := ui.OutputWriter.String() - if !strings.Contains(output, `provider[\"registry.terraform.io/hashicorp/test\"]`) { - t.Fatalf("doesn't look like digraph: %s", output) - } -} - func TestGraph_noConfig(t *testing.T) { td := t.TempDir() os.MkdirAll(td, 0755) defer testChdir(t, td)() - ui := new(cli.MockUi) + streams, closeStreams := terminal.StreamsForTesting(t) + defer closeStreams(t) + ui := cli.NewMockUi() c := &GraphCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(applyFixtureProvider()), Ui: ui, + Streams: streams, }, } @@ -100,11 +210,102 @@ func TestGraph_noConfig(t *testing.T) { } } -func TestGraph_plan(t *testing.T) { +func TestGraph_resourcesOnly(t *testing.T) { + wd := tempWorkingDirFixture(t, "graph-interesting") + defer testChdir(t, wd.RootModuleDir())() + + // The graph-interesting fixture has a child module, so we'll need to + // run the module installer just to get the working directory set up + // properly, as if the user has run "terraform init". This is really + // just building the working directory's index of module directories. + loader, cleanupLoader := configload.NewLoaderForTests(t) + t.Cleanup(cleanupLoader) + err := os.MkdirAll(".terraform/modules", 0700) + if err != nil { + t.Fatal(err) + } + inst := initwd.NewModuleInstaller(".terraform/modules", loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), ".", "tests", true, false, initwd.ModuleInstallHooksImpl{}) + if instDiags.HasErrors() { + t.Fatal(instDiags.Err()) + } + + p := testProvider() + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "foo": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "arg": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + ui := cli.NewMockUi() + streams, closeStreams := terminal.StreamsForTesting(t) + c := &GraphCommand{ + Meta: Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("foo"): providers.FactoryFixed(p), + }, + }, + Ui: ui, + Streams: streams, + }, + } + + // A "resources only" graph is the default behavior, with no extra arguments. + args := []string{} + if code := c.Run(args); code != 0 { + output := closeStreams(t) + t.Fatalf("unexpected error: \n%s", output.Stderr()) + } + + output := closeStreams(t) + gotGraph := strings.TrimSpace(output.Stdout()) + wantGraph := strings.TrimSpace(` +digraph G { + rankdir = "RL"; + node [shape = rect, fontname = "sans-serif"]; + "foo.bar" [label="foo.bar"]; + "foo.baz" [label="foo.baz"]; + "foo.boop" [label="foo.boop"]; + subgraph "cluster_module.child" { + label = "module.child" + fontname = "sans-serif" + "module.child.foo.bleep" [label="foo.bleep"]; + } + "foo.baz" -> "foo.bar"; + "foo.boop" -> "module.child.foo.bleep"; + "module.child.foo.bleep" -> "foo.bar"; +} +`) + if diff := cmp.Diff(wantGraph, gotGraph); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } +} + +func TestGraph_applyPhaseSavedPlan(t *testing.T) { testCwd(t) + emptyObj, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) + if err != nil { + t.Fatal(err) + } + + nullEmptyObj, err := plans.NewDynamicValue(cty.NullVal((cty.EmptyObject)), cty.EmptyObject) + if err != nil { + t.Fatal(err) + } + plan := &plans.Plan{ - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), } plan.Changes.Resources = append(plan.Changes.Resources, &plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ @@ -114,34 +315,33 @@ func TestGraph_plan(t *testing.T) { }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), ChangeSrc: plans.ChangeSrc{ Action: plans.Delete, - Before: plans.DynamicValue(`{}`), - After: plans.DynamicValue(`null`), + Before: emptyObj, + After: nullEmptyObj, }, ProviderAddr: addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, }) - emptyConfig, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) - if err != nil { - t.Fatal(err) - } + plan.Backend = plans.Backend{ // Doesn't actually matter since we aren't going to activate the backend // for this command anyway, but we need something here for the plan // file writer to succeed. Type: "placeholder", - Config: emptyConfig, + Config: emptyObj, } _, configSnap := testModuleWithSnapshot(t, "graph") planPath := testPlanFile(t, configSnap, states.NewState(), plan) - ui := new(cli.MockUi) + streams, closeStreams := terminal.StreamsForTesting(t) + ui := cli.NewMockUi() c := &GraphCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(applyFixtureProvider()), Ui: ui, + Streams: streams, }, } @@ -152,8 +352,8 @@ func TestGraph_plan(t *testing.T) { t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) } - output := ui.OutputWriter.String() - if !strings.Contains(output, `provider[\"registry.terraform.io/hashicorp/test\"]`) { - t.Fatalf("doesn't look like digraph: %s", output) + output := closeStreams(t) + if !strings.Contains(output.Stdout(), `provider[\"registry.terraform.io/hashicorp/test\"]`) { + t.Fatalf("doesn't look like digraph:\n%s\n\nstderr:\n%s", output.Stdout(), output.Stderr()) } } diff --git a/internal/command/helper.go b/internal/command/helper.go new file mode 100644 index 0000000000..ad0f06da3c --- /dev/null +++ b/internal/command/helper.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/cloud" +) + +const failedToLoadSchemasMessage = ` +Warning: Failed to update data for external integrations + +Terraform was unable to generate a description of the updated +state for use with external integrations in HCP Terraform or Terraform Enterprise. +Any integrations configured for this workspace which depend on +information from the state may not work correctly when using the +result of this action. + +This problem occurs when Terraform cannot read the schema for +one or more of the providers used in the state. The next successful +apply will correct the problem by re-generating the JSON description +of the state: + terraform apply +` + +func isCloudMode(b backendrun.OperationsBackend) bool { + _, ok := b.(*cloud.Cloud) + + return ok +} diff --git a/internal/command/hook_module_install.go b/internal/command/hook_module_install.go index 4afa7072c3..4046c7ce5b 100644 --- a/internal/command/hook_module_install.go +++ b/internal/command/hook_module_install.go @@ -1,33 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" + "github.com/hashicorp/cli" version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/initwd" - "github.com/mitchellh/cli" ) +type view interface { + Log(message string, params ...any) +} type uiModuleInstallHooks struct { initwd.ModuleInstallHooksImpl Ui cli.Ui ShowLocalPaths bool + View view } var _ initwd.ModuleInstallHooks = uiModuleInstallHooks{} func (h uiModuleInstallHooks) Download(modulePath, packageAddr string, v *version.Version) { if v != nil { - h.Ui.Info(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath)) + h.log(fmt.Sprintf("Downloading %s %s for %s...", packageAddr, v, modulePath)) } else { - h.Ui.Info(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath)) + h.log(fmt.Sprintf("Downloading %s for %s...", packageAddr, modulePath)) } } func (h uiModuleInstallHooks) Install(modulePath string, v *version.Version, localDir string) { if h.ShowLocalPaths { - h.Ui.Info(fmt.Sprintf("- %s in %s", modulePath, localDir)) + h.log(fmt.Sprintf("- %s in %s", modulePath, localDir)) } else { - h.Ui.Info(fmt.Sprintf("- %s", modulePath)) + h.log(fmt.Sprintf("- %s", modulePath)) + } +} + +func (h uiModuleInstallHooks) log(message string) { + switch h.View.(type) { + case view: + // there is no unformatted option for the View interface, so we need to + // pass message as a parameter to avoid double escaping % characters + h.View.Log("%s", message) + default: + h.Ui.Info(message) } } diff --git a/internal/command/import.go b/internal/command/import.go index 51d3895e73..09a9e23459 100644 --- a/internal/command/import.go +++ b/internal/command/import.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -11,7 +14,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" @@ -110,7 +113,7 @@ func (c *ImportCommand) Run(args []string) int { // This is to reduce the risk that a typo in the resource address will // import something that Terraform will want to immediately destroy on // the next plan, and generally acts as a reassurance of user intent. - targetConfig := config.DescendentForInstance(addr.Module) + targetConfig := config.DescendantForInstance(addr.Module) if targetConfig == nil { modulePath := addr.Module.String() diags = diags.Append(&hcl.Diagnostic{ @@ -170,19 +173,19 @@ func (c *ImportCommand) Run(args []string) int { return 1 } - // We require a backend.Local to build a context. + // We require a backendrun.Local to build a context. // This isn't necessarily a "local.Local" backend, which provides local // operations, however that is the only current implementation. A // "local.Local" backend also doesn't necessarily provide local state, as // that may be delegated to a "remotestate.Backend". - local, ok := b.(backend.Local) + local, ok := b.(backendrun.Local) if !ok { c.Ui.Error(ErrUnsupportedLocalOp) return 1 } // Build the operation - opReq := c.Operation(b) + opReq := c.Operation(b, arguments.ViewHuman) opReq.ConfigDir = configPath opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { @@ -232,8 +235,8 @@ func (c *ImportCommand) Run(args []string) int { newState, importDiags := lr.Core.Import(lr.Config, lr.InputState, &terraform.ImportOpts{ Targets: []*terraform.ImportTarget{ { - Addr: addr, - ID: args[1], + LegacyAddr: addr, + LegacyID: args[1], }, }, @@ -248,13 +251,21 @@ func (c *ImportCommand) Run(args []string) int { return 1 } + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + if isCloudMode(b) { + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags = c.MaybeGetSchemas(newState, nil) + diags = diags.Append(schemaDiags) + } + // Persist the final state log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) if err := state.WriteState(newState); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } - if err := state.PersistState(); err != nil { + if err := state.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } @@ -286,14 +297,6 @@ Usage: terraform [global options] import [options] ADDR ID determine the ID syntax to use. It typically matches directly to the ID that the provider uses. - The current implementation of Terraform import can only import resources - into the state. It does not generate configuration. A future version of - Terraform will also generate configuration. - - Because of this, prior to running terraform import it is necessary to write - a resource configuration block for the resource manually, to which the - imported object will be attached. - This command will not modify your infrastructure, but it will make network requests to inspect parts of your infrastructure relevant to the resource being imported. @@ -338,7 +341,7 @@ func (c *ImportCommand) Synopsis() string { } const importCommandInvalidAddressReference = `For information on valid syntax, see: -https://www.terraform.io/docs/cli/state/resource-addressing.html` +https://developer.hashicorp.com/terraform/cli/state/resource-addressing` const importCommandMissingResourceFmt = `[reset][bold][red]Error:[reset][bold] resource address %q does not exist in the configuration.[reset] diff --git a/internal/command/import_test.go b/internal/command/import_test.go index 6f1ad71b0c..f5c34dd4cc 100644 --- a/internal/command/import_test.go +++ b/internal/command/import_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -8,7 +11,7 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -47,7 +50,7 @@ func TestImport(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -101,7 +104,7 @@ func TestImport_providerConfig(t *testing.T) { } p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -109,7 +112,7 @@ func TestImport_providerConfig(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -214,7 +217,7 @@ func TestImport_remoteState(t *testing.T) { } p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -222,7 +225,7 @@ func TestImport_remoteState(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -369,7 +372,7 @@ func TestImport_providerConfigWithVar(t *testing.T) { } p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -377,7 +380,7 @@ func TestImport_providerConfigWithVar(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -449,7 +452,7 @@ func TestImport_providerConfigWithDataSource(t *testing.T) { } p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -457,7 +460,7 @@ func TestImport_providerConfigWithDataSource(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -466,7 +469,7 @@ func TestImport_providerConfigWithDataSource(t *testing.T) { }, DataSources: map[string]providers.Schema{ "test_data": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -514,7 +517,7 @@ func TestImport_providerConfigWithVarDefault(t *testing.T) { } p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -522,7 +525,7 @@ func TestImport_providerConfigWithVarDefault(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -593,7 +596,7 @@ func TestImport_providerConfigWithVarFile(t *testing.T) { } p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -601,7 +604,7 @@ func TestImport_providerConfigWithVarFile(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -751,7 +754,7 @@ func TestImportModuleVarFile(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -825,7 +828,7 @@ func TestImportModuleInputVariableEvaluation(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, diff --git a/internal/command/init.go b/internal/command/init.go index adc827fa44..4ba9448edf 100644 --- a/internal/command/init.go +++ b/internal/command/init.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "context" + "errors" "fmt" "log" "reflect" @@ -9,16 +13,19 @@ import ( "strings" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform-config-inspect/tfconfig" svchost "github.com/hashicorp/terraform-svchost" "github.com/posener/complete" "github.com/zclconf/go-cty/cty" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" backendInit "github.com/hashicorp/terraform/internal/backend/init" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/getproviders" @@ -36,49 +43,35 @@ type InitCommand struct { } func (c *InitCommand) Run(args []string) int { - var flagFromModule, flagLockfile string - var flagBackend, flagCloud, flagGet, flagUpgrade bool - var flagPluginPath FlagStringSlice - flagConfigExtra := newRawFlags("-backend-config") - + var diags tfdiags.Diagnostics args = c.Meta.process(args) - cmdFlags := c.Meta.extendedFlagSet("init") - cmdFlags.BoolVar(&flagBackend, "backend", true, "") - cmdFlags.BoolVar(&flagCloud, "cloud", true, "") - cmdFlags.Var(flagConfigExtra, "backend-config", "") - cmdFlags.StringVar(&flagFromModule, "from-module", "", "copy the source of the given module into the directory before init") - cmdFlags.BoolVar(&flagGet, "get", true, "") - cmdFlags.BoolVar(&c.forceInitCopy, "force-copy", false, "suppress prompts about copying state data") - cmdFlags.BoolVar(&c.Meta.stateLock, "lock", true, "lock state") - cmdFlags.DurationVar(&c.Meta.stateLockTimeout, "lock-timeout", 0, "lock timeout") - cmdFlags.BoolVar(&c.reconfigure, "reconfigure", false, "reconfigure") - cmdFlags.BoolVar(&c.migrateState, "migrate-state", false, "migrate state") - cmdFlags.BoolVar(&flagUpgrade, "upgrade", false, "") - cmdFlags.Var(&flagPluginPath, "plugin-dir", "plugin directory") - cmdFlags.StringVar(&flagLockfile, "lockfile", "", "Set a dependency lockfile mode") - cmdFlags.BoolVar(&c.Meta.ignoreRemoteVersion, "ignore-remote-version", false, "continue even if remote and local Terraform versions are incompatible") - cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - if err := cmdFlags.Parse(args); err != nil { + initArgs, initDiags := arguments.ParseInit(args) + + view := views.NewInit(initArgs.ViewType, c.View) + + if initDiags.HasErrors() { + diags = diags.Append(initDiags) + view.Diagnostics(diags) return 1 } - backendFlagSet := arguments.FlagIsSet(cmdFlags, "backend") - cloudFlagSet := arguments.FlagIsSet(cmdFlags, "cloud") + c.forceInitCopy = initArgs.ForceInitCopy + c.Meta.stateLock = initArgs.StateLock + c.Meta.stateLockTimeout = initArgs.StateLockTimeout + c.reconfigure = initArgs.Reconfigure + c.migrateState = initArgs.MigrateState + c.Meta.ignoreRemoteVersion = initArgs.IgnoreRemoteVersion + c.Meta.input = initArgs.InputEnabled + c.Meta.targetFlags = initArgs.TargetFlags + c.Meta.compactWarnings = initArgs.CompactWarnings - switch { - case backendFlagSet && cloudFlagSet: - c.Ui.Error("The -backend and -cloud options are aliases of one another and mutually-exclusive in their use") - return 1 - case backendFlagSet: - flagCloud = flagBackend - case cloudFlagSet: - flagBackend = flagCloud - } - - if c.migrateState && c.reconfigure { - c.Ui.Error("The -migrate-state and -reconfigure options are mutually-exclusive") - return 1 + varArgs := initArgs.Vars.All() + items := make([]arguments.FlagNameValue, len(varArgs)) + for i := range varArgs { + items[i].Name = varArgs[i].Name + items[i].Value = varArgs[i].Value } + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} // Copying the state only happens during backend migration, so setting // -force-copy implies -migrate-state @@ -86,193 +79,118 @@ func (c *InitCommand) Run(args []string) int { c.migrateState = true } - var diags tfdiags.Diagnostics - - if len(flagPluginPath) > 0 { - c.pluginPath = flagPluginPath + if len(initArgs.PluginPath) > 0 { + c.pluginPath = initArgs.PluginPath } // Validate the arg count and get the working directory - args = cmdFlags.Args() - path, err := ModulePath(args) + path, err := ModulePath(initArgs.Args) if err != nil { - c.Ui.Error(err.Error()) + diags = diags.Append(err) + view.Diagnostics(diags) return 1 } if err := c.storePluginPath(c.pluginPath); err != nil { - c.Ui.Error(fmt.Sprintf("Error saving -plugin-path values: %s", err)) + diags = diags.Append(fmt.Errorf("Error saving -plugin-dir to workspace directory: %s", err)) + view.Diagnostics(diags) return 1 } + // Initialization can be aborted by interruption signals + ctx, done := c.InterruptibleContext(c.CommandContext()) + defer done() + // This will track whether we outputted anything so that we know whether // to output a newline before the success message var header bool - if flagFromModule != "" { - src := flagFromModule + if initArgs.FromModule != "" { + src := initArgs.FromModule - empty, err := configs.IsEmptyDir(path) + empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) if err != nil { - c.Ui.Error(fmt.Sprintf("Error validating destination directory: %s", err)) + diags = diags.Append(fmt.Errorf("Error validating destination directory: %s", err)) + view.Diagnostics(diags) return 1 } if !empty { - c.Ui.Error(strings.TrimSpace(errInitCopyNotEmpty)) + diags = diags.Append(errors.New(strings.TrimSpace(errInitCopyNotEmpty))) + view.Diagnostics(diags) return 1 } - c.Ui.Output(c.Colorize().Color(fmt.Sprintf( - "[reset][bold]Copying configuration[reset] from %q...", src, - ))) + view.Output(views.CopyingConfigurationMessage, src) header = true hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: false, // since they are in a weird location for init + View: view, } - initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(path, src, hooks) + ctx, span := tracer.Start(ctx, "-from-module=...", trace.WithAttributes( + attribute.String("module_source", src), + )) + + initDirFromModuleAbort, initDirFromModuleDiags := c.initDirFromModule(ctx, path, src, hooks) diags = diags.Append(initDirFromModuleDiags) if initDirFromModuleAbort || initDirFromModuleDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) + span.SetStatus(codes.Error, "module installation failed") + span.End() return 1 } + span.End() - c.Ui.Output("") + view.Output(views.EmptyMessage) } // If our directory is empty, then we're done. We can't get or set up // the backend with an empty directory. - empty, err := configs.IsEmptyDir(path) + empty, err := configs.IsEmptyDir(path, initArgs.TestsDirectory) if err != nil { diags = diags.Append(fmt.Errorf("Error checking configuration: %s", err)) - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if empty { - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(outputInitEmpty))) + view.Output(views.OutputInitEmptyMessage) return 0 } - // For Terraform v0.12 we introduced a special loading mode where we would - // use the 0.11-syntax-compatible "earlyconfig" package as a heuristic to - // identify situations where it was likely that the user was trying to use - // 0.11-only syntax that the upgrade tool might help with. - // - // However, as the language has moved on that is no longer a suitable - // heuristic in Terraform 0.13 and later: other new additions to the - // language can cause the main loader to disagree with earlyconfig, which - // would lead us to give poor advice about how to respond. - // - // For that reason, we no longer use a different error message in that - // situation, but for now we still use both codepaths because some of our - // initialization functionality remains built around "earlyconfig" and - // so we need to still load the module via that mechanism anyway until we - // can do some more invasive refactoring here. - rootModEarly, earlyConfDiags := c.loadSingleModuleEarly(path) - // If _only_ the early loader encountered errors then that's unusual - // (it should generally be a superset of the normal loader) but we'll - // return those errors anyway since otherwise we'll probably get - // some weird behavior downstream. Errors from the early loader are - // generally not as high-quality since it has less context to work with. - if earlyConfDiags.HasErrors() { - c.Ui.Error(c.Colorize().Color(strings.TrimSpace(errInitConfigError))) - // Errors from the early loader are generally not as high-quality since - // it has less context to work with. + // Load just the root module to begin backend and module initialization + rootModEarly, earlyConfDiags := c.loadSingleModuleWithTests(path, initArgs.TestsDirectory) - // TODO: It would be nice to check the version constraints in - // rootModEarly.RequiredCore and print out a hint if the module is - // declaring that it's not compatible with this version of Terraform, - // and that may be what caused earlyconfig to fail. - diags = diags.Append(earlyConfDiags) - c.showDiagnostics(diags) - return 1 - } + // There may be parsing errors in config loading but these will be shown later _after_ + // checking for core version requirement errors. Not meeting the version requirement should + // be the first error displayed if that is an issue, but other operations are required + // before being able to check core version requirements. + if rootModEarly == nil { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError)), earlyConfDiags) + view.Diagnostics(diags) - if flagGet { - modsOutput, modsAbort, modsDiags := c.getModules(path, rootModEarly, flagUpgrade) - diags = diags.Append(modsDiags) - if modsAbort || modsDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - if modsOutput { - header = true - } - } - - // With all of the modules (hopefully) installed, we can now try to load the - // whole configuration tree. - config, confDiags := c.loadConfig(path) - // configDiags will be handled after the version constraint check, since an - // incorrect version of terraform may be producing errors for configuration - // constructs added in later versions. - - // Before we go further, we'll check to make sure none of the modules in - // the configuration declare that they don't support this Terraform - // version, so we can produce a version-related error message rather than - // potentially-confusing downstream errors. - versionDiags := terraform.CheckCoreVersionRequirements(config) - if versionDiags.HasErrors() { - c.showDiagnostics(versionDiags) - return 1 - } - - diags = diags.Append(confDiags) - if confDiags.HasErrors() { - c.Ui.Error(strings.TrimSpace(errInitConfigError)) - c.showDiagnostics(diags) return 1 } var back backend.Backend + // There may be config errors or backend init errors but these will be shown later _after_ + // checking for core version requirement errors. + var backDiags tfdiags.Diagnostics + var backendOutput bool + switch { - case flagCloud && config.Module.CloudConfig != nil: - be, backendOutput, backendDiags := c.initCloud(config.Module, flagConfigExtra) - diags = diags.Append(backendDiags) - if backendDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - if backendOutput { - header = true - } - back = be - case flagBackend: - be, backendOutput, backendDiags := c.initBackend(config.Module, flagConfigExtra) - diags = diags.Append(backendDiags) - if backendDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - if backendOutput { - header = true - } - back = be + case initArgs.Cloud && rootModEarly.CloudConfig != nil: + back, backendOutput, backDiags = c.initCloud(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) + case initArgs.Backend: + back, backendOutput, backDiags = c.initBackend(ctx, rootModEarly, initArgs.BackendConfig, initArgs.ViewType, view) default: // load the previously-stored backend config - be, backendDiags := c.Meta.backendFromState() - diags = diags.Append(backendDiags) - if backendDiags.HasErrors() { - c.showDiagnostics(diags) - return 1 - } - back = be + back, backDiags = c.Meta.backendFromState(ctx) } - - if back == nil { - // If we didn't initialize a backend then we'll try to at least - // instantiate one. This might fail if it wasn't already initialized - // by a previous run, so we must still expect that "back" may be nil - // in code that follows. - var backDiags tfdiags.Diagnostics - back, backDiags = c.Backend(&BackendOpts{Init: true}) - if backDiags.HasErrors() { - // This is fine. We'll proceed with no backend, then. - back = nil - } + if backendOutput { + header = true } var state *states.State @@ -284,28 +202,100 @@ func (c *InitCommand) Run(args []string) int { c.ignoreRemoteVersionConflict(back) workspace, err := c.Workspace() if err != nil { - c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) + diags = diags.Append(fmt.Errorf("Error selecting workspace: %s", err)) + view.Diagnostics(diags) return 1 } sMgr, err := back.StateMgr(workspace) if err != nil { - c.Ui.Error(fmt.Sprintf("Error loading state: %s", err)) + diags = diags.Append(fmt.Errorf("Error loading state: %s", err)) + view.Diagnostics(diags) return 1 } if err := sMgr.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) + diags = diags.Append(fmt.Errorf("Error refreshing state: %s", err)) + view.Diagnostics(diags) return 1 } state = sMgr.State() } + if initArgs.Get { + modsOutput, modsAbort, modsDiags := c.getModules(ctx, path, initArgs.TestsDirectory, rootModEarly, initArgs.Upgrade, view) + diags = diags.Append(modsDiags) + if modsAbort || modsDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + if modsOutput { + header = true + } + } + + // With all of the modules (hopefully) installed, we can now try to load the + // whole configuration tree. + config, confDiags := c.loadConfigWithTests(path, initArgs.TestsDirectory) + // configDiags will be handled after the version constraint check, since an + // incorrect version of terraform may be producing errors for configuration + // constructs added in later versions. + + // Before we go further, we'll check to make sure none of the modules in + // the configuration declare that they don't support this Terraform + // version, so we can produce a version-related error message rather than + // potentially-confusing downstream errors. + versionDiags := terraform.CheckCoreVersionRequirements(config) + if versionDiags.HasErrors() { + view.Diagnostics(versionDiags) + return 1 + } + + // We've passed the core version check, now we can show errors from the + // configuration and backend initialisation. + + // Now, we can check the diagnostics from the early configuration and the + // backend. + diags = diags.Append(earlyConfDiags) + diags = diags.Append(backDiags) + if earlyConfDiags.HasErrors() { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) + return 1 + } + + // Now, we can show any errors from initializing the backend, but we won't + // show the InitConfigError preamble as we didn't detect problems with + // the early configuration. + if backDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + + // If everything is ok with the core version check and backend initialization, + // show other errors from loading the full configuration tree. + diags = diags.Append(confDiags) + if confDiags.HasErrors() { + diags = diags.Append(errors.New(view.PrepareMessage(views.InitConfigError))) + view.Diagnostics(diags) + return 1 + } + + if cb, ok := back.(*cloud.Cloud); ok { + if c.RunningInAutomation { + if err := cb.AssertImportCompatible(config); err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Compatibility error", err.Error())) + view.Diagnostics(diags) + return 1 + } + } + } + // Now that we have loaded all modules, check the module tree for missing providers. - providersOutput, providersAbort, providerDiags := c.getProviders(config, state, flagUpgrade, flagPluginPath, flagLockfile) + providersOutput, providersAbort, providerDiags := c.getProviders(ctx, config, state, initArgs.Upgrade, initArgs.PluginPath, initArgs.Lockfile, view) diags = diags.Append(providerDiags) if providersAbort || providerDiags.HasErrors() { - c.showDiagnostics(diags) + view.Diagnostics(diags) return 1 } if providersOutput { @@ -315,52 +305,67 @@ func (c *InitCommand) Run(args []string) int { // If we outputted information, then we need to output a newline // so that our success message is nicely spaced out from prior text. if header { - c.Ui.Output("") + view.Output(views.EmptyMessage) } // If we accumulated any warnings along the way that weren't accompanied // by errors then we'll output them here so that the success message is // still the final thing shown. - c.showDiagnostics(diags) + view.Diagnostics(diags) _, cloud := back.(*cloud.Cloud) - output := outputInitSuccess + output := views.OutputInitSuccessMessage if cloud { - output = outputInitSuccessCloud + output = views.OutputInitSuccessCloudMessage } - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output))) + view.Output(output) if !c.RunningInAutomation { // If we're not running in an automation wrapper, give the user // some more detailed next steps that are appropriate for interactive // shell usage. - output = outputInitSuccessCLI + output = views.OutputInitSuccessCLIMessage if cloud { - output = outputInitSuccessCLICloud + output = views.OutputInitSuccessCLICloudMessage } - c.Ui.Output(c.Colorize().Color(strings.TrimSpace(output))) + view.Output(output) } return 0 } -func (c *InitCommand) getModules(path string, earlyRoot *tfconfig.Module, upgrade bool) (output bool, abort bool, diags tfdiags.Diagnostics) { - if len(earlyRoot.ModuleCalls) == 0 { +func (c *InitCommand) getModules(ctx context.Context, path, testsDir string, earlyRoot *configs.Module, upgrade bool, view views.Init) (output bool, abort bool, diags tfdiags.Diagnostics) { + testModules := false // We can also have modules buried in test files. + for _, file := range earlyRoot.Tests { + for _, run := range file.Runs { + if run.Module != nil { + testModules = true + } + } + } + + if len(earlyRoot.ModuleCalls) == 0 && !testModules { // Nothing to do return false, false, nil } + ctx, span := tracer.Start(ctx, "install modules", trace.WithAttributes( + attribute.Bool("upgrade", upgrade), + )) + defer span.End() + if upgrade { - c.Ui.Output(c.Colorize().Color("[reset][bold]Upgrading modules...")) + view.Output(views.UpgradingModulesMessage) } else { - c.Ui.Output(c.Colorize().Color("[reset][bold]Initializing modules...")) + view.Output(views.InitializingModulesMessage) } hooks := uiModuleInstallHooks{ Ui: c.Ui, ShowLocalPaths: true, + View: view, } - installAbort, installDiags := c.installModules(path, upgrade, hooks) + installAbort, installDiags := c.installModules(ctx, path, testsDir, upgrade, false, hooks) diags = diags.Append(installDiags) // At this point, installModules may have generated error diags or been @@ -383,14 +388,18 @@ func (c *InitCommand) getModules(path string, earlyRoot *tfconfig.Module, upgrad return true, installAbort, diags } -func (c *InitCommand) initCloud(root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { - c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing Terraform Cloud...")) +func (c *InitCommand) initCloud(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { + ctx, span := tracer.Start(ctx, "initialize HCP Terraform") + _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here + defer span.End() + + view.Output(views.InitializingTerraformCloudMessage) if len(extraConfig.AllItems()) != 0 { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid command-line option", - "The -backend-config=... command line option is only for state backends, and is not applicable to Terraform Cloud-based configurations.\n\nTo change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module.", + "The -backend-config=... command line option is only for state backends, and is not applicable to HCP Terraform-based configurations.\n\nTo change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module.", )) return nil, true, diags } @@ -398,8 +407,9 @@ func (c *InitCommand) initCloud(root *configs.Module, extraConfig rawFlags) (be backendConfig := root.CloudConfig.ToBackendConfig() opts := &BackendOpts{ - Config: &backendConfig, - Init: true, + Config: &backendConfig, + Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -407,8 +417,12 @@ func (c *InitCommand) initCloud(root *configs.Module, extraConfig rawFlags) (be return back, true, diags } -func (c *InitCommand) initBackend(root *configs.Module, extraConfig rawFlags) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { - c.Ui.Output(c.Colorize().Color("\n[reset][bold]Initializing the backend...")) +func (c *InitCommand) initBackend(ctx context.Context, root *configs.Module, extraConfig arguments.FlagNameValueSlice, viewType arguments.ViewType, view views.Init) (be backend.Backend, output bool, diags tfdiags.Diagnostics) { + ctx, span := tracer.Start(ctx, "initialize backend") + _ = ctx // prevent staticcheck from complaining to avoid a maintenence hazard of having the wrong ctx in scope here + defer span.End() + + view.Output(views.InitializingBackendMessage) var backendConfig *configs.Backend var backendConfigOverride hcl.Body @@ -418,7 +432,7 @@ func (c *InitCommand) initBackend(root *configs.Module, extraConfig rawFlags) (b diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unsupported backend type", - Detail: fmt.Sprintf("There is no explicit backend type named %q. To configure Terraform Cloud, declare a 'cloud' block instead.", backendType), + Detail: fmt.Sprintf("There is no explicit backend type named %q. To configure HCP Terraform, declare a 'cloud' block instead.", backendType), Subject: &root.Backend.TypeRange, }) return nil, true, diags @@ -479,6 +493,7 @@ the backend configuration is present and valid. Config: backendConfig, ConfigOverride: backendConfigOverride, Init: true, + ViewType: viewType, } back, backDiags := c.Backend(opts) @@ -488,7 +503,10 @@ the backend configuration is present and valid. // Load the complete module tree, and fetch any missing providers. // This method outputs its own Ui. -func (c *InitCommand) getProviders(config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string) (output, abort bool, diags tfdiags.Diagnostics) { +func (c *InitCommand) getProviders(ctx context.Context, config *configs.Config, state *states.State, upgrade bool, pluginDirs []string, flagLockfile string, view views.Init) (output, abort bool, diags tfdiags.Diagnostics) { + ctx, span := tracer.Start(ctx, "install providers") + defer span.End() + // Dev overrides cause the result of "terraform init" to be irrelevant for // any overridden providers, so we'll warn about it to avoid later // confusion when Terraform ends up using a different provider than the @@ -546,10 +564,6 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, log.Printf("[DEBUG] will search for provider plugins in %s", pluginDirs) } - // Installation can be aborted by interruption signals - ctx, done := c.InterruptibleContext() - defer done() - // We want to print out a nice warning if we don't manage to pull // checksums for all our providers. This is tracked via callbacks // and incomplete providers are stored here for later analysis. @@ -562,15 +576,13 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, // are shimming our vt100 output to the legacy console API on Windows. evts := &providercache.InstallerEvents{ PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { - c.Ui.Output(c.Colorize().Color( - "\n[reset][bold]Initializing provider plugins...", - )) + view.Output(views.InitializingProviderPluginMessage) }, ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { - c.Ui.Info(fmt.Sprintf("- Using previously-installed %s v%s", provider.ForDisplay(), selectedVersion)) + view.LogInitMessage(views.ProviderAlreadyInstalledMessage, provider.ForDisplay(), selectedVersion) }, BuiltInProviderAvailable: func(provider addrs.Provider) { - c.Ui.Info(fmt.Sprintf("- %s is built in to Terraform", provider.ForDisplay())) + view.LogInitMessage(views.BuiltInProviderAvailableMessage, provider.ForDisplay()) }, BuiltInProviderFailure: func(provider addrs.Provider, err error) { diags = diags.Append(tfdiags.Sourceless( @@ -581,20 +593,20 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, }, QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { if locked { - c.Ui.Info(fmt.Sprintf("- Reusing previous version of %s from the dependency lock file", provider.ForDisplay())) + view.LogInitMessage(views.ReusingPreviousVersionInfo, provider.ForDisplay()) } else { if len(versionConstraints) > 0 { - c.Ui.Info(fmt.Sprintf("- Finding %s versions matching %q...", provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints))) + view.LogInitMessage(views.FindingMatchingVersionMessage, provider.ForDisplay(), getproviders.VersionConstraintsString(versionConstraints)) } else { - c.Ui.Info(fmt.Sprintf("- Finding latest version of %s...", provider.ForDisplay())) + view.LogInitMessage(views.FindingLatestVersionMessage, provider.ForDisplay()) } } }, LinkFromCacheBegin: func(provider addrs.Provider, version getproviders.Version, cacheRoot string) { - c.Ui.Info(fmt.Sprintf("- Using %s v%s from the shared cache directory", provider.ForDisplay(), version)) + view.LogInitMessage(views.UsingProviderFromCacheDirInfo, provider.ForDisplay(), version) }, FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { - c.Ui.Info(fmt.Sprintf("- Installing %s v%s...", provider.ForDisplay(), version)) + view.LogInitMessage(views.InstallingProviderMessage, provider.ForDisplay(), version) }, QueryPackagesFailure: func(provider addrs.Provider, err error) { switch errorTy := err.(type) { @@ -653,7 +665,7 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider registry host", - fmt.Sprintf("The host %q given in in provider source address %q does not offer a Terraform provider registry that is compatible with this Terraform version, but it may be compatible with a different Terraform version.", + fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry that is compatible with this Terraform version, but it may be compatible with a different Terraform version.", errorTy.Hostname, provider.String(), ), )) @@ -662,7 +674,7 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid provider registry host", - fmt.Sprintf("The host %q given in in provider source address %q does not offer a Terraform provider registry.", + fmt.Sprintf("The host %q given in provider source address %q does not offer a Terraform provider registry.", errorTy.Hostname, provider.String(), ), )) @@ -674,11 +686,12 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, // the end, by checking ctx.Err(). default: + suggestion := fmt.Sprintf("\n\nTo see which modules are currently depending on %s and what versions are specified, run the following command:\n terraform providers", provider.ForDisplay()) diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to query available provider packages", - fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s", - provider.ForDisplay(), err, + fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s%s", + provider.ForDisplay(), err, suggestion, ), )) } @@ -791,10 +804,10 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, keyID = authResult.KeyID } if keyID != "" { - keyID = c.Colorize().Color(fmt.Sprintf(", key ID [reset][bold]%s[reset]", keyID)) + keyID = view.PrepareMessage(views.KeyID, keyID) } - c.Ui.Info(fmt.Sprintf("- Installed %s v%s (%s%s)", provider.ForDisplay(), version, authResult, keyID)) + view.LogInitMessage(views.InstalledProviderVersionInfo, provider.ForDisplay(), version, authResult, keyID) }, ProvidersLockUpdated: func(provider addrs.Provider, version getproviders.Version, localHashes []getproviders.Hash, signedHashes []getproviders.Hash, priorHashes []getproviders.Hash) { // We're going to use this opportunity to track if we have any @@ -840,9 +853,7 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, } } if thirdPartySigned { - c.Ui.Info(fmt.Sprintf("\nPartner and community providers are signed by their developers.\n" + - "If you'd like to know more about provider signing, you can read about it here:\n" + - "https://www.terraform.io/docs/cli/plugins/signing.html")) + view.LogInitMessage(views.PartnerAndCommunityProvidersMessage) } }, } @@ -851,7 +862,8 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, mode := providercache.InstallNewProvidersOnly if upgrade { if flagLockfile == "readonly" { - c.Ui.Error("The -upgrade flag conflicts with -lockfile=readonly.") + diags = diags.Append(fmt.Errorf("The -upgrade flag conflicts with -lockfile=readonly.")) + view.Diagnostics(diags) return true, true, diags } @@ -859,8 +871,8 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, } newLocks, err := inst.EnsureProviderVersions(ctx, previousLocks, reqs, mode) if ctx.Err() == context.Canceled { - c.showDiagnostics(diags) - c.Ui.Error("Provider installation was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Provider installation was canceled by an interrupt signal.")) + view.Diagnostics(diags) return true, true, diags } if err != nil { @@ -927,16 +939,9 @@ func (c *InitCommand) getProviders(config *configs.Config, state *states.State, // say a little about what the dependency lock file is, for new // users or those who are upgrading from a previous Terraform // version that didn't have dependency lock files. - c.Ui.Output(c.Colorize().Color(` -Terraform has created a lock file [bold].terraform.lock.hcl[reset] to record the provider -selections it made above. Include this file in your version control repository -so that Terraform can guarantee to make the same selections by default when -you run "terraform init" in the future.`)) + view.Output(views.LockInfo) } else { - c.Ui.Output(c.Colorize().Color(` -Terraform has made some changes to the provider dependency selections recorded -in the .terraform.lock.hcl file. Review those changes and commit them to your -version control system if they represent changes you intended to make.`)) + view.Output(views.DependenciesLockChangesInfo) } moreDiags = c.replaceLockedDependencies(newLocks) @@ -954,7 +959,7 @@ version control system if they represent changes you intended to make.`)) // // If the returned diagnostics contains errors then the returned body may be // incomplete or invalid. -func (c *InitCommand) backendConfigOverrideBody(flags rawFlags, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { +func (c *InitCommand) backendConfigOverrideBody(flags arguments.FlagNameValueSlice, schema *configschema.Block) (hcl.Body, tfdiags.Diagnostics) { items := flags.AllItems() if len(items) == 0 { return nil, nil @@ -1067,6 +1072,7 @@ func (c *InitCommand) AutocompleteFlags() complete.Flags { "-lock": completePredictBoolean, "-lock-timeout": complete.PredictAnything, "-no-color": complete.PredictNothing, + "-json": complete.PredictNothing, "-plugin-dir": complete.PredictDirs(""), "-reconfigure": complete.PredictNothing, "-migrate-state": complete.PredictNothing, @@ -1093,7 +1099,7 @@ Usage: terraform [global options] init [options] Options: - -backend=false Disable backend or Terraform Cloud initialization + -backend=false Disable backend or HCP Terraform initialization for this configuration and use what was previously initialized instead. @@ -1129,6 +1135,9 @@ Options: -no-color If specified, output won't contain any color. + -json If specified, machine readable output will be + printed in JSON format. + -plugin-dir Directory containing plugin binaries. This overrides all default search paths for plugins, and prevents the automatic installation of plugins. This flag can be used @@ -1148,12 +1157,14 @@ Options: -lockfile=MODE Set a dependency lockfile mode. Currently only "readonly" is valid. - -ignore-remote-version A rare option used for Terraform Cloud and the remote backend + -ignore-remote-version A rare option used for HCP Terraform and the remote backend only. Set this to ignore checking that the local and remote Terraform versions use compatible state representations, making an operation proceed even when there is a potential mismatch. See the documentation on configuring Terraform with - Terraform Cloud for more information. + HCP Terraform or Terraform Enterprise for more information. + + -test-directory=path Set the Terraform test directory, defaults to "tests". ` return strings.TrimSpace(helpText) @@ -1163,13 +1174,6 @@ func (c *InitCommand) Synopsis() string { return "Prepare your working directory for other commands" } -const errInitConfigError = ` -[reset]There are some problems with the configuration, described below. - -The Terraform configuration must be valid before initialization so that -Terraform can determine which modules and providers need to be installed. -` - const errInitCopyNotEmpty = ` The working directory already contains files. The -from-module option requires an empty directory into which a copy of the referenced module will be placed. @@ -1178,39 +1182,6 @@ To initialize the configuration already in this working directory, omit the -from-module option. ` -const outputInitEmpty = ` -[reset][bold]Terraform initialized in an empty directory![reset] - -The directory has no Terraform configuration files. You may begin working -with Terraform immediately by creating Terraform configuration files. -` - -const outputInitSuccess = ` -[reset][bold][green]Terraform has been successfully initialized![reset][green] -` - -const outputInitSuccessCloud = ` -[reset][bold][green]Terraform Cloud has been successfully initialized![reset][green] -` - -const outputInitSuccessCLI = `[reset][green] -You may now begin working with Terraform. Try running "terraform plan" to see -any changes that are required for your infrastructure. All Terraform commands -should now work. - -If you ever set or change modules or backend configuration for Terraform, -rerun this command to reinitialize your working directory. If you forget, other -commands will detect it and remind you to do so if necessary. -` - -const outputInitSuccessCLICloud = `[reset][green] -You may now begin working with Terraform Cloud. Try running "terraform plan" to -see any changes that are required for your infrastructure. - -If you ever set or change modules or Terraform Settings, run "terraform init" -again to reinitialize your working directory. -` - // providerProtocolTooOld is a message sent to the CLI UI if the provider's // supported protocol versions are too old for the user's version of terraform, // but a newer version of the provider is compatible. diff --git a/internal/command/init_test.go b/internal/command/init_test.go index 2609677bdf..290e3f2746 100644 --- a/internal/command/init_test.go +++ b/internal/command/init_test.go @@ -9,26 +9,50 @@ import ( "log" "os" "path/filepath" + "regexp" "strings" "testing" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" + version "github.com/hashicorp/go-version" "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/providercache" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" ) +// cleanString removes newlines, and redundant spaces. +func cleanString(s string) string { + // Replace newlines with a single space. + s = strings.ReplaceAll(s, "\n", " ") + + // Remove other special characters like \r, \t + s = strings.ReplaceAll(s, "\r", "") + s = strings.ReplaceAll(s, "\t", "") + + // Replace multiple spaces with a single space. + spaceRegex := regexp.MustCompile(`\s+`) + s = spaceRegex.ReplaceAllString(s, " ") + + // Trim any leading or trailing spaces. + s = strings.TrimSpace(s) + + return s +} + func TestInit_empty(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -36,7 +60,7 @@ func TestInit_empty(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -47,7 +71,43 @@ func TestInit_empty(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) + } + exp := views.MessageRegistry[views.OutputInitEmptyMessage].JSONValue + actual := cleanString(done(t).All()) + if !strings.Contains(actual, cleanString(exp)) { + t.Fatalf("expected output to be %q\n, got %q", exp, actual) + } +} + +func TestInit_only_test_files(t *testing.T) { + // Create a temporary working directory that has only test files and no tf configuration + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + if _, err := os.Create("main.tftest.hcl"); err != nil { + t.Fatalf("err: %s", err) + } + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", done(t).All()) + } + exp := views.MessageRegistry[views.OutputInitSuccessCLIMessage].JSONValue + actual := cleanString(done(t).All()) + if !strings.Contains(actual, cleanString(exp)) { + t.Fatalf("expected output to be %q\n, got %q", exp, actual) } } @@ -58,7 +118,7 @@ func TestInit_multipleArgs(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -72,10 +132,40 @@ func TestInit_multipleArgs(t *testing.T) { "bad", } if code := c.Run(args); code != 1 { - t.Fatalf("bad: \n%s", ui.OutputWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } +func TestInit_migrateStateAndJSON(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{ + "-migrate-state=true", + "-json=true", + } + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("error, -migrate-state and -json should be exclusive: \n%s", testOutput.All()) + } + + // Check output + checkGoldenReference(t, testOutput, "init-migrate-state-with-json") +} + func TestInit_fromModule_cwdDest(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -83,7 +173,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -96,7 +186,7 @@ func TestInit_fromModule_cwdDest(t *testing.T) { "-from-module=" + testFixturePath("init"), } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(td, "hello.tf")); err != nil { @@ -134,7 +224,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -147,7 +237,7 @@ func TestInit_fromModule_dstInSrc(t *testing.T) { "-from-module=./..", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(dir, "foo", "issue518.tf")); err != nil { @@ -162,7 +252,7 @@ func TestInit_get(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -173,16 +263,42 @@ func TestInit_get(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Check output - output := ui.OutputWriter.String() + output := done(t).Stdout() if !strings.Contains(output, "foo in foo") { t.Fatalf("doesn't look like we installed module 'foo': %s", output) } } +func TestInit_json(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-get"), td) + defer testChdir(t, td)() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", done(t).All()) + } + + // Check output + output := done(t) + checkGoldenReference(t, output, "init-get") +} + func TestInit_getUpgradeModules(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -190,7 +306,7 @@ func TestInit_getUpgradeModules(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -203,14 +319,15 @@ func TestInit_getUpgradeModules(t *testing.T) { "-get=true", "-upgrade", } - if code := c.Run(args); code != 0 { - t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("command did not complete successfully:\n%s", testOutput.Stderr()) } // Check output - output := ui.OutputWriter.String() - if !strings.Contains(output, "Upgrading modules...") { - t.Fatalf("doesn't look like get upgrade: %s", output) + if !strings.Contains(testOutput.Stdout(), "Upgrading modules...") { + t.Fatalf("doesn't look like get upgrade: %s", testOutput.Stdout()) } } @@ -221,7 +338,7 @@ func TestInit_backend(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -232,7 +349,7 @@ func TestInit_backend(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { @@ -250,7 +367,7 @@ func TestInit_backendUnset(t *testing.T) { log.Printf("[TRACE] TestInit_backendUnset: beginning first init") ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -261,12 +378,14 @@ func TestInit_backendUnset(t *testing.T) { // Init args := []string{} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } log.Printf("[TRACE] TestInit_backendUnset: first init complete") - t.Logf("First run output:\n%s", ui.OutputWriter.String()) - t.Logf("First run errors:\n%s", ui.ErrorWriter.String()) + t.Logf("First run output:\n%s", testOutput.Stdout()) + t.Logf("First run errors:\n%s", testOutput.Stderr()) if _, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename)); err != nil { t.Fatalf("err: %s", err) @@ -282,7 +401,7 @@ func TestInit_backendUnset(t *testing.T) { } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -292,12 +411,14 @@ func TestInit_backendUnset(t *testing.T) { } args := []string{"-force-copy"} - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } log.Printf("[TRACE] TestInit_backendUnset: second init complete") - t.Logf("Second run output:\n%s", ui.OutputWriter.String()) - t.Logf("Second run errors:\n%s", ui.ErrorWriter.String()) + t.Logf("Second run output:\n%s", testOutput.Stdout()) + t.Logf("Second run errors:\n%s", testOutput.Stderr()) s := testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if !s.Backend.Empty() { @@ -314,7 +435,7 @@ func TestInit_backendConfigFile(t *testing.T) { t.Run("good-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -324,7 +445,7 @@ func TestInit_backendConfigFile(t *testing.T) { } args := []string{"-backend-config", "input.config"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Read our saved backend config and verify we have our settings @@ -337,7 +458,7 @@ func TestInit_backendConfigFile(t *testing.T) { // the backend config file must not be a full terraform block t.Run("full-backend-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -349,15 +470,15 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Unsupported block type") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + if !strings.Contains(done(t).All(), "Unsupported block type") { + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) // the backend config file must match the schema for the backend t.Run("invalid-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -369,15 +490,15 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Unsupported argument") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + if !strings.Contains(done(t).All(), "Unsupported argument") { + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) // missing file is an error t.Run("missing-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -389,15 +510,15 @@ func TestInit_backendConfigFile(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("expected error, got success\n") } - if !strings.Contains(ui.ErrorWriter.String(), "Failed to read file") { - t.Fatalf("wrong error: %s", ui.ErrorWriter) + if !strings.Contains(done(t).All(), "Failed to read file") { + t.Fatalf("wrong error: %s", done(t).Stderr()) } }) // blank filename clears the backend config t.Run("blank-config-file", func(t *testing.T) { ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -407,7 +528,7 @@ func TestInit_backendConfigFile(t *testing.T) { } args := []string{"-backend-config=", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } // Read our saved backend config and verify the backend config is empty @@ -433,7 +554,7 @@ func TestInit_backendConfigFile(t *testing.T) { }, }, } - flagConfigExtra := newRawFlags("-backend-config") + flagConfigExtra := arguments.NewFlagNameValueSlice("-backend-config") flagConfigExtra.Set("input.config") _, diags := c.backendConfigOverrideBody(flagConfigExtra, schema) if len(diags) != 0 { @@ -449,7 +570,7 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -467,12 +588,13 @@ func TestInit_backendConfigFilePowershellConfusion(t *testing.T) { // result in an early exit with a diagnostic that the provided // configuration file is not a diretory. args := []string{"-backend-config=", "./input.config"} - if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(args) + output := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, output.Stderr(), output.Stdout()) } - output := ui.ErrorWriter.String() - if got, want := output, `Too many command line arguments`; !strings.Contains(got, want) { + if got, want := output.Stderr(), `Too many command line arguments`; !strings.Contains(got, want) { t.Fatalf("wrong output\ngot:\n%s\n\nwant: message containing %q", got, want) } } @@ -489,7 +611,7 @@ func TestInit_backendReconfigure(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -512,7 +634,7 @@ func TestInit_backendReconfigure(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // now run init again, changing the path. @@ -520,7 +642,7 @@ func TestInit_backendReconfigure(t *testing.T) { // Without -reconfigure, the test fails since the backend asks for input on migrating state args = []string{"-reconfigure", "-backend-config", "path=changed"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } } @@ -531,7 +653,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -542,7 +664,7 @@ func TestInit_backendConfigFileChange(t *testing.T) { args := []string{"-backend-config", "input.config", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -564,7 +686,7 @@ func TestInit_backendMigrateWhileLocked(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -595,13 +717,13 @@ func TestInit_backendMigrateWhileLocked(t *testing.T) { // Attempt to migrate args := []string{"-backend-config", "input.config", "-migrate-state", "-force-copy"} if code := c.Run(args); code == 0 { - t.Fatalf("expected nonzero exit code: %s", ui.OutputWriter.String()) + t.Fatalf("expected nonzero exit code: %s", done(t).Stdout()) } // Disabling locking should work args = []string{"-backend-config", "input.config", "-migrate-state", "-force-copy", "-lock=false"} if code := c.Run(args); code != 0 { - t.Fatalf("expected zero exit code, got %d: %s", code, ui.ErrorWriter.String()) + t.Fatalf("expected zero exit code, got %d: %s", code, done(t).Stderr()) } } @@ -612,10 +734,13 @@ func TestInit_backendConfigFileChangeWithExistingState(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) + view, _ := testView(t) + c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, }, } @@ -646,7 +771,7 @@ func TestInit_backendConfigKV(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -657,7 +782,7 @@ func TestInit_backendConfigKV(t *testing.T) { args := []string{"-backend-config", "path=hello"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -674,7 +799,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -685,7 +810,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { args := []string{"-backend-config", "path=test"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } ui = new(cli.MockUi) @@ -700,7 +825,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { // a second init should require no changes, nor should it change the backend. args = []string{"-input=false"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -716,7 +841,7 @@ func TestInit_backendConfigKVReInit(t *testing.T) { // override the -backend-config options by settings args = []string{"-input=false", "-backend-config", "", "-migrate-state"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -737,7 +862,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -748,7 +873,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { args := []string{"-input=false"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } ui = new(cli.MockUi) @@ -764,7 +889,7 @@ func TestInit_backendConfigKVReInitWithConfigDiff(t *testing.T) { // should it change the backend. args = []string{"-input=false", "-backend-config", "path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // make sure the backend is configured how we expect @@ -785,7 +910,7 @@ func TestInit_backendCli_no_config_block(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -796,10 +921,10 @@ func TestInit_backendCli_no_config_block(t *testing.T) { args := []string{"-backend-config", "path=test"} if code := c.Run(args); code != 0 { - t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 0\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "Warning: Missing backend configuration") { t.Fatal("expected missing backend block warning, got", errMsg) } @@ -824,7 +949,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -835,7 +960,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { args := []string{"-backend-config", "path=hello"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -850,7 +975,7 @@ func TestInit_backendReinitWithExtra(t *testing.T) { // init again and make sure nothing changes if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"hello","workspace_dir":null}`; got != want { @@ -868,7 +993,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -878,7 +1003,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { } if code := c.Run([]string{"-input=false"}); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // Read our saved backend config and verify we have our settings @@ -907,7 +1032,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { args := []string{"-input=false", "-backend-config=path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } state = testDataStateRead(t, filepath.Join(DefaultDataDir, DefaultStateFilename)) if got, want := normalizeJSON(t, state.Backend.ConfigRaw), `{"path":"foo","workspace_dir":null}`; got != want { @@ -921,7 +1046,7 @@ func TestInit_backendReinitConfigToExtra(t *testing.T) { func TestInit_backendCloudInvalidOptions(t *testing.T) { // There are various "terraform init" options that are only for - // traditional backends and not applicable to Terraform Cloud mode. + // traditional backends and not applicable to HCP Terraform mode. // For those, we want to return an explicit error rather than // just silently ignoring them, so that users will be aware that // Cloud mode has more of an expected "happy path" than the @@ -971,13 +1096,13 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { // certain settings of backends that tend to vary depending on // where Terraform is running, such as AWS authentication profiles // that are naturally local only to the machine where Terraform is - // running. Those needs don't apply to Terraform Cloud, because + // running. Those needs don't apply to HCP Terraform, because // the remote workspace encapsulates all of the details of how // operations and state work in that case, and so the Cloud // configuration is only about which workspaces we'll be working // with. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -986,19 +1111,18 @@ func TestInit_backendCloudInvalidOptions(t *testing.T) { } args := []string{"-backend-config=anything"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option The -backend-config=... command line option is only for state backends, and -is not applicable to Terraform Cloud-based configurations. +is not applicable to HCP Terraform-based configurations. To change the set of workspaces associated with this configuration, edit the Cloud configuration block in the root module. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1011,12 +1135,12 @@ Cloud configuration block in the root module. // skipping state migration when migrating between backends, but it // has a historical flaw that it doesn't work properly when the // initial situation is the implicit local backend with a state file - // present. The Terraform Cloud migration path has some additional + // present. The HCP Terraform migration path has some additional // steps to take care of more details automatically, and so // -reconfigure doesn't really make sense in that context, particularly // with its design bug with the handling of the implicit local backend. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1025,19 +1149,18 @@ Cloud configuration block in the root module. } args := []string{"-reconfigure"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option The -reconfigure option is for in-place reconfiguration of state backends -only, and is not needed when changing Terraform Cloud settings. +only, and is not needed when changing HCP Terraform settings. -When using Terraform Cloud, initialization automatically activates any new +When using HCP Terraform, initialization automatically activates any new Cloud configuration settings. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1047,7 +1170,7 @@ Cloud configuration settings. defer setupTempDir(t)() // We have a slightly different error message for the case where we - // seem to be trying to migrate to Terraform Cloud with existing + // seem to be trying to migrate to HCP Terraform with existing // state or explicit backend already present. if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil { @@ -1055,7 +1178,7 @@ Cloud configuration settings. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1064,16 +1187,15 @@ Cloud configuration settings. } args := []string{"-reconfigure"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option -The -reconfigure option is unsupported when migrating to Terraform Cloud, -because activating Terraform Cloud involves some additional steps. - +The -reconfigure option is unsupported when migrating to HCP Terraform, +because activating HCP Terraform involves some additional steps. ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1086,7 +1208,7 @@ because activating Terraform Cloud involves some additional steps. // and changing configuration while staying in cloud mode never migrates // state, so this special option isn't relevant. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1095,19 +1217,18 @@ because activating Terraform Cloud involves some additional steps. } args := []string{"-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option The -migrate-state option is for migration between state backends only, and -is not applicable when using Terraform Cloud. +is not applicable when using HCP Terraform. -State storage is handled automatically by Terraform Cloud and so the state +State storage is handled automatically by HCP Terraform and so the state storage location is not configurable. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1117,7 +1238,7 @@ storage location is not configurable. defer setupTempDir(t)() // We have a slightly different error message for the case where we - // seem to be trying to migrate to Terraform Cloud with existing + // seem to be trying to migrate to HCP Terraform with existing // state or explicit backend already present. if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil { @@ -1125,7 +1246,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1134,19 +1255,18 @@ storage location is not configurable. } args := []string{"-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option The -migrate-state option is for migration between state backends only, and -is not applicable when using Terraform Cloud. +is not applicable when using HCP Terraform. -Terraform Cloud migration has additional steps, configured by interactive +HCP Terraform migrations have additional steps, configured by interactive prompts. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1159,7 +1279,7 @@ prompts. // and changing configuration while staying in cloud mode never migrates // state, so this special option isn't relevant. ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1168,19 +1288,18 @@ prompts. } args := []string{"-force-copy"} if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + t.Fatalf("unexpected success\n%s", done(t).Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := done(t).Stderr() wantStderr := ` Error: Invalid command-line option The -force-copy option is for migration between state backends only, and is -not applicable when using Terraform Cloud. +not applicable when using HCP Terraform. -State storage is handled automatically by Terraform Cloud and so the state +State storage is handled automatically by HCP Terraform and so the state storage location is not configurable. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1190,7 +1309,7 @@ storage location is not configurable. defer setupTempDir(t)() // We have a slightly different error message for the case where we - // seem to be trying to migrate to Terraform Cloud with existing + // seem to be trying to migrate to HCP Terraform with existing // state or explicit backend already present. if err := os.WriteFile("terraform.tfstate", fakeStateBytes, 0644); err != nil { @@ -1198,7 +1317,7 @@ storage location is not configurable. } ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ Ui: ui, @@ -1206,20 +1325,21 @@ storage location is not configurable. }, } args := []string{"-force-copy"} - if code := c.Run(args); code == 0 { - t.Fatalf("unexpected success\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("unexpected success\n%s", testOutput.Stdout()) } - gotStderr := ui.ErrorWriter.String() + gotStderr := testOutput.Stderr() wantStderr := ` Error: Invalid command-line option The -force-copy option is for migration between state backends only, and is -not applicable when using Terraform Cloud. +not applicable when using HCP Terraform. -Terraform Cloud migration has additional steps, configured by interactive +HCP Terraform migrations have additional steps, configured by interactive prompts. - ` if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { t.Errorf("wrong error output\n%s", diff) @@ -1235,7 +1355,7 @@ func TestInit_inputFalse(t *testing.T) { defer testChdir(t, td)() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -1246,7 +1366,7 @@ func TestInit_inputFalse(t *testing.T) { args := []string{"-input=false", "-backend-config=path=foo"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // write different states for foo and bar @@ -1282,10 +1402,10 @@ func TestInit_inputFalse(t *testing.T) { args = []string{"-input=false", "-backend-config=path=bar", "-migrate-state"} if code := c.Run(args); code == 0 { - t.Fatal("init should have failed", ui.OutputWriter) + t.Fatal("init should have failed", done(t).Stdout()) } - errMsg := ui.ErrorWriter.String() + errMsg := done(t).All() if !strings.Contains(errMsg, "interactive input is disabled") { t.Fatal("expected input disabled error, got", errMsg) } @@ -1302,7 +1422,7 @@ func TestInit_inputFalse(t *testing.T) { // A missing input=false should abort rather than loop infinitely args = []string{"-backend-config=path=baz"} if code := c.Run(args); code == 0 { - t.Fatal("init should have failed", ui.OutputWriter) + t.Fatal("init should have failed", done(t).Stdout()) } } @@ -1314,7 +1434,7 @@ func TestInit_getProvider(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "exact": {"1.2.3"}, @@ -1339,7 +1459,7 @@ func TestInit_getProvider(t *testing.T) { "-backend=false", // should be possible to install plugins without backend init } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // check that we got the providers for our config @@ -1393,18 +1513,20 @@ func TestInit_getProvider(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m.Ui = ui m.View = view c := &InitCommand{ Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatal("expected error, got:", ui.OutputWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatal("expected error, got:", testOutput.Stdout()) } - errMsg := ui.ErrorWriter.String() + errMsg := testOutput.Stderr() if !strings.Contains(errMsg, "Unsupported state file format") { t.Fatal("unexpected error:", errMsg) } @@ -1419,7 +1541,7 @@ func TestInit_getProviderSource(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ // looking for an exact version "acme/alpha": {"1.2.3"}, @@ -1443,7 +1565,7 @@ func TestInit_getProviderSource(t *testing.T) { "-backend=false", // should be possible to install plugins without backend init } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } // check that we got the providers for our config @@ -1469,7 +1591,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "acme/alpha": {"1.2.3"}, }) @@ -1484,9 +1606,10 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { c := &InitCommand{ Meta: m, } - - if code := c.Run(nil); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(nil) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } // Expect this diagnostic output @@ -1494,7 +1617,7 @@ func TestInit_getProviderLegacyFromState(t *testing.T) { "Invalid legacy provider address", "You must complete the Terraform 0.13 upgrade process", } - got := ui.ErrorWriter.String() + got := testOutput.All() for _, want := range wants { if !strings.Contains(got, want) { t.Fatalf("expected output to contain %q, got:\n\n%s", want, got) @@ -1510,7 +1633,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { overrides := metaOverridesForProvider(testProvider()) ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) // create a provider source which allows installing an invalid package addr := addrs.MustParseProviderSourceString("invalid/package") @@ -1542,8 +1665,10 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { args := []string{ "-backend=false", // should be possible to install plugins without backend init } - if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 1 { + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, testOutput.Stderr(), testOutput.Stdout()) } // invalid provider should be installed @@ -1556,7 +1681,7 @@ func TestInit_getProviderInvalidPackage(t *testing.T) { "Failed to install provider", "could not find executable file starting with terraform-provider-package", } - got := ui.ErrorWriter.String() + got := testOutput.All() for _, wantError := range wantErrors { if !strings.Contains(got, wantError) { t.Fatalf("missing error:\nwant: %q\ngot:\n%s", wantError, got) @@ -1587,7 +1712,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ Ui: ui, View: view, @@ -1601,8 +1726,10 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { args := []string{ "-backend=false", // should be possible to install plugins without backend init } - if code := c.Run(args); code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected error, got output: \n%s", testOutput.Stdout()) } // foo should be installed @@ -1617,7 +1744,7 @@ func TestInit_getProviderDetectedLegacy(t *testing.T) { } // error output is the main focus of this test - errOutput := ui.ErrorWriter.String() + errOutput := testOutput.All() errors := []string{ "Failed to query available provider packages", "Could not retrieve the list of available versions", @@ -1645,7 +1772,7 @@ func TestInit_providerSource(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1658,11 +1785,12 @@ func TestInit_providerSource(t *testing.T) { } args := []string{} - - if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) } - if strings.Contains(ui.OutputWriter.String(), "Terraform has initialized, but configuration upgrades may be needed") { + if strings.Contains(testOutput.Stdout(), "Terraform has initialized, but configuration upgrades may be needed") { t.Fatalf("unexpected \"configuration upgrade\" warning in output") } @@ -1731,10 +1859,10 @@ func TestInit_providerSource(t *testing.T) { t.Errorf("wrong version selections after upgrade\n%s", diff) } - if got, want := ui.OutputWriter.String(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { + if got, want := testOutput.Stdout(), "Installed hashicorp/test v1.2.3 (verified checksum)"; !strings.Contains(got, want) { t.Fatalf("unexpected output: %s\nexpected to include %q", got, want) } - if got, want := ui.ErrorWriter.String(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { + if got, want := testOutput.All(), "\n - hashicorp/source\n - hashicorp/test\n - hashicorp/test-beta"; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1753,7 +1881,7 @@ func TestInit_cancelModules(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1766,12 +1894,13 @@ func TestInit_cancelModules(t *testing.T) { } args := []string{} - - if code := c.Run(args); code == 0 { - t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded; wanted error\n%s", testOutput.Stdout()) } - if got, want := ui.ErrorWriter.String(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { + if got, want := testOutput.Stderr(), `Module installation was canceled by an interrupt signal`; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1795,7 +1924,7 @@ func TestInit_cancelProviders(t *testing.T) { close(shutdownCh) ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1809,15 +1938,16 @@ func TestInit_cancelProviders(t *testing.T) { } args := []string{} - - if code := c.Run(args); code == 0 { - t.Fatalf("succeeded; wanted error\n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded; wanted error\n%s", testOutput.All()) } // Currently the first operation that is cancelable is provider // installation, so our error message comes from there. If we // make the earlier steps cancelable in future then it'd be // expected for this particular message to change. - if got, want := ui.ErrorWriter.String(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { + if got, want := testOutput.Stderr(), `Provider installation was canceled by an interrupt signal`; !strings.Contains(got, want) { t.Fatalf("wrong error message\nshould contain: %s\ngot:\n%s", want, got) } } @@ -1839,7 +1969,7 @@ func TestInit_getUpgradePlugins(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1860,7 +1990,7 @@ func TestInit_getUpgradePlugins(t *testing.T) { "-upgrade=true", } if code := c.Run(args); code != 0 { - t.Fatalf("command did not complete successfully:\n%s", ui.ErrorWriter.String()) + t.Fatalf("command did not complete successfully:\n%s", done(t).All()) } cacheDir := m.providerLocalCacheDir() @@ -1964,7 +2094,7 @@ func TestInit_getProviderMissing(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -1977,12 +2107,14 @@ func TestInit_getProviderMissing(t *testing.T) { } args := []string{} - if code := c.Run(args); code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected error, got output: \n%s", testOutput.Stdout()) } - if !strings.Contains(ui.ErrorWriter.String(), "no available releases match") { - t.Fatalf("unexpected error output: %s", ui.ErrorWriter) + if !strings.Contains(testOutput.All(), "no available releases match") { + t.Fatalf("unexpected error output: %s", testOutput.Stderr()) } } @@ -1993,7 +2125,7 @@ func TestInit_checkRequiredVersion(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2004,9 +2136,9 @@ func TestInit_checkRequiredVersion(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `required_version = "~> 0.9.0"`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2024,7 +2156,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2035,9 +2167,9 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2048,7 +2180,7 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { defer testChdir(t, td)() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2059,9 +2191,9 @@ func TestInit_checkRequiredVersionFirst(t *testing.T) { args := []string{} if code := c.Run(args); code != 1 { - t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, ui.ErrorWriter.String(), ui.OutputWriter.String()) + t.Fatalf("got exit status %d; want 1\nstderr:\n%s\n\nstdout:\n%s", code, done(t).Stderr(), done(t).Stdout()) } - errStr := ui.ErrorWriter.String() + errStr := done(t).All() if !strings.Contains(errStr, `Unsupported Terraform Core version`) { t.Fatalf("output should point to unmet version constraint, but is:\n\n%s", errStr) } @@ -2082,7 +2214,7 @@ func TestInit_providerLockFile(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2096,7 +2228,7 @@ func TestInit_providerLockFile(t *testing.T) { args := []string{} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } lockFile := ".terraform.lock.hcl" @@ -2127,7 +2259,7 @@ provider "registry.terraform.io/hashicorp/test" { // succeeds, to ensure that we don't try to rewrite an unchanged lock file os.Chmod(".", 0555) if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).All()) } } @@ -2267,9 +2399,11 @@ provider "registry.terraform.io/hashicorp/test" { defer close() ui := new(cli.MockUi) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -2285,10 +2419,10 @@ provider "registry.terraform.io/hashicorp/test" { code := c.Run(tc.args) if tc.ok && code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) + t.Fatalf("bad: \n%s", done(t).Stderr()) } if !tc.ok && code == 0 { - t.Fatalf("expected error, got output: \n%s", ui.OutputWriter.String()) + t.Fatalf("expected error, got output: \n%s", done(t).Stdout()) } buf, err := ioutil.ReadFile(lockFile) @@ -2313,7 +2447,7 @@ func TestInit_pluginDirReset(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) c := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(testProvider()), @@ -2334,7 +2468,7 @@ func TestInit_pluginDirReset(t *testing.T) { // run once and save the -plugin-dir args := []string{"-plugin-dir", "a"} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } pluginDirs, err := c.loadPluginPath() @@ -2359,7 +2493,7 @@ func TestInit_pluginDirReset(t *testing.T) { // make sure we remove the plugin-dir record args = []string{"-plugin-dir="} if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } pluginDirs, err = c.loadPluginPath() @@ -2383,7 +2517,7 @@ func TestInit_pluginDirProviders(t *testing.T) { defer close() ui := new(cli.MockUi) - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2424,7 +2558,7 @@ func TestInit_pluginDirProviders(t *testing.T) { "-plugin-dir", "c", } if code := c.Run(args); code != 0 { - t.Fatalf("bad: \n%s", ui.ErrorWriter) + t.Fatalf("bad: \n%s", done(t).Stderr()) } locks, err := m.lockedDependencies() @@ -2484,7 +2618,7 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2522,15 +2656,17 @@ func TestInit_pluginDirProvidersDoesNotGet(t *testing.T) { "-plugin-dir", "a", "-plugin-dir", "b", } - if code := c.Run(args); code == 0 { + code := c.Run(args) + testOutput := done(t) + if code == 0 { // should have been an error - t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", ui.OutputWriter, ui.ErrorWriter) + t.Fatalf("succeeded; want error\nstdout:\n%s\nstderr\n%s", testOutput.Stdout(), testOutput.Stderr()) } // The error output should mention the "between" provider but should not // mention either the "exact" or "greater-than" provider, because the // latter two are available via the -plugin-dir directories. - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "hashicorp/between"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the 'between' provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2557,7 +2693,7 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2570,11 +2706,13 @@ func TestInit_pluginDirWithBuiltIn(t *testing.T) { } args := []string{"-plugin-dir", "./"} - if code := c.Run(args); code != 0 { - t.Fatalf("error: %s", ui.ErrorWriter) + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("error: %s", testOutput.Stderr()) } - outputStr := ui.OutputWriter.String() + outputStr := testOutput.Stdout() if subStr := "terraform.io/builtin/terraform is built in to Terraform"; !strings.Contains(outputStr, subStr) { t.Errorf("output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, outputStr) } @@ -2595,7 +2733,7 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { defer close() ui := cli.NewMockUi() - view, _ := testView(t) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: ui, @@ -2607,11 +2745,13 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { Meta: m, } - if code := c.Run(nil); code == 0 { - t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", ui.OutputWriter, ui.ErrorWriter) + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) } - errStr := ui.ErrorWriter.String() + errStr := testOutput.Stderr() if subStr := "Cannot use terraform.io/builtin/terraform: built-in"; !strings.Contains(errStr, subStr) { t.Errorf("error output should mention the terraform provider\nwant substr: %s\ngot:\n%s", subStr, errStr) } @@ -2620,6 +2760,350 @@ func TestInit_invalidBuiltInProviders(t *testing.T) { } } +func TestInit_invalidSyntaxNoBackend(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-syntax-invalid-no-backend"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, done := testView(t) + m := Meta{ + Ui: ui, + View: view, + } + + c := &InitCommand{ + Meta: m, + } + + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) + } + + errStr := testOutput.Stderr() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + } + if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) + } +} + +func TestInit_invalidSyntaxWithBackend(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-syntax-invalid-with-backend"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, done := testView(t) + m := Meta{ + Ui: ui, + View: view, + } + + c := &InitCommand{ + Meta: m, + } + + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) + } + + errStr := testOutput.Stderr() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + } + if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should mention the syntax problem\nwant substr: %s\ngot:\n%s", subStr, errStr) + } +} + +func TestInit_invalidSyntaxInvalidBackend(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-syntax-invalid-backend-invalid"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, done := testView(t) + m := Meta{ + Ui: ui, + View: view, + } + + c := &InitCommand{ + Meta: m, + } + + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) + } + + errStr := testOutput.Stderr() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + } + if subStr := "Error: Unsupported block type"; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should mention syntax errors\nwant substr: %s\ngot:\n%s", subStr, errStr) + } + if subStr := "Error: Unsupported backend type"; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should mention the invalid backend\nwant substr: %s\ngot:\n%s", subStr, errStr) + } +} + +func TestInit_invalidSyntaxBackendAttribute(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-syntax-invalid-backend-attribute-invalid"), td) + defer testChdir(t, td)() + + ui := cli.NewMockUi() + view, done := testView(t) + m := Meta{ + Ui: ui, + View: view, + } + + c := &InitCommand{ + Meta: m, + } + + code := c.Run(nil) + testOutput := done(t) + if code == 0 { + t.Fatalf("succeeded, but was expecting error\nstdout:\n%s\nstderr:\n%s", testOutput.Stdout(), testOutput.Stderr()) + } + + errStr := testOutput.All() + if subStr := "Terraform encountered problems during initialisation, including problems\nwith the configuration, described below."; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should include preamble\nwant substr: %s\ngot:\n%s", subStr, errStr) + } + if subStr := "Error: Invalid character"; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should mention the invalid character\nwant substr: %s\ngot:\n%s", subStr, errStr) + } + if subStr := "Error: Invalid expression"; !strings.Contains(errStr, subStr) { + t.Errorf("Error output should mention the invalid expression\nwant substr: %s\ngot:\n%s", subStr, errStr) + } +} + +func TestInit_testsWithExternalProviders(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-tests-external-providers"), td) + defer testChdir(t, td)() + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/testing": {"1.0.0"}, + "testing/configure": {"1.0.0"}, + }) + defer close() + + hashicorpTestingProviderAddress := addrs.NewDefaultProvider("testing") + hashicorpTestingProvider := new(testing_provider.MockProvider) + testingConfigureProviderAddress := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "testing", "configure") + testingConfigureProvider := new(testing_provider.MockProvider) + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + hashicorpTestingProviderAddress: providers.FactoryFixed(hashicorpTestingProvider), + testingConfigureProviderAddress: providers.FactoryFixed(testingConfigureProvider), + }, + }, + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + var args []string + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", done(t).All()) + } +} + +func TestInit_tests(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-tests"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: \n%s", done(t).Stderr()) + } +} + +func TestInit_testsWithProvider(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-tests-with-provider"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{} + code := c.Run(args) + testOutput := done(t) + if code == 0 { + t.Fatalf("expected failure but got: \n%s", testOutput.All()) + } + + got := testOutput.Stderr() + want := ` +Error: Failed to query available provider packages + +Could not retrieve the list of available versions for provider +hashicorp/test: no available releases match the given constraints 1.0.1, +1.0.2 + +To see which modules are currently depending on hashicorp/test and what +versions are specified, run the following command: + terraform providers +` + if diff := cmp.Diff(got, want); len(diff) > 0 { + t.Fatalf("wrong error message: \ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } +} + +func TestInit_testsWithOverriddenInvalidRequiredProviders(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-overrides-and-duplicates"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{} + code := c.Run(args) // just make sure it doesn't crash. + if code != 1 { + t.Fatalf("expected failure but got: \n%s", done(t).All()) + } +} + +func TestInit_testsWithInvalidRequiredProviders(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-duplicates"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{} + code := c.Run(args) // just make sure it doesn't crash. + if code != 1 { + t.Fatalf("expected failure but got: \n%s", done(t).All()) + } +} + +func TestInit_testsWithModule(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("init-with-tests-with-module"), td) + defer testChdir(t, td)() + + provider := applyFixtureProvider() // We just want the types from this provider. + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "hashicorp/test": {"1.0.0"}, + }) + defer close() + + ui := new(cli.MockUi) + view, done := testView(t) + c := &InitCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider), + Ui: ui, + View: view, + ProviderSource: providerSource, + }, + } + + args := []string{} + code := c.Run(args) + testOutput := done(t) + if code != 0 { + t.Fatalf("bad: \n%s", testOutput.All()) + } + + // Check output + output := testOutput.Stdout() + if !strings.Contains(output, "test.main.setup in setup") { + t.Fatalf("doesn't look like we installed the test module': %s", output) + } +} + // newMockProviderSource is a helper to succinctly construct a mock provider // source that contains a set of packages matching the given provider versions // that are available for installation (from temporary local files). diff --git a/internal/command/jsonchecks/checks.go b/internal/command/jsonchecks/checks.go index 892dcba325..9692e7a739 100644 --- a/internal/command/jsonchecks/checks.go +++ b/internal/command/jsonchecks/checks.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonchecks import ( @@ -70,8 +73,6 @@ func MarshalCheckStates(results *states.CheckResults) []byte { // object in the configuration even if Terraform Core encountered an error // before being able to determine the dynamic instances of the checkable object. type checkResultStatic struct { - ExperimentalNote experimentalNote `json:"//"` - // Address is the address of the checkable object this result relates to. Address staticObjectAddr `json:"address"` @@ -116,9 +117,3 @@ type checkProblem struct { // intentionally an object to allow us to add other data over time, such // as the source location where the failing condition was defined. } - -type experimentalNote struct{} - -func (n experimentalNote) MarshalJSON() ([]byte, error) { - return []byte(`"EXPERIMENTAL: see docs for details"`), nil -} diff --git a/internal/command/jsonchecks/checks_test.go b/internal/command/jsonchecks/checks_test.go index 6e0f52da4f..4b631801ca 100644 --- a/internal/command/jsonchecks/checks_test.go +++ b/internal/command/jsonchecks/checks_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonchecks import ( @@ -36,6 +39,8 @@ func TestMarshalCheckStates(t *testing.T) { outputAInstAddr := addrs.Checkable(addrs.OutputValue{Name: "a"}.Absolute(addrs.RootModuleInstance)) outputBAddr := addrs.ConfigCheckable(addrs.OutputValue{Name: "b"}.InModule(moduleChildAddr.Module())) outputBInstAddr := addrs.Checkable(addrs.OutputValue{Name: "b"}.Absolute(moduleChildAddr)) + checkBlockAAddr := addrs.ConfigCheckable(addrs.Check{Name: "a"}.InModule(addrs.RootModule)) + checkBlockAInstAddr := addrs.Checkable(addrs.Check{Name: "a"}.Absolute(addrs.RootModuleInstance)) tests := map[string]struct { Input *states.CheckResults @@ -90,11 +95,42 @@ func TestMarshalCheckStates(t *testing.T) { }), ), }), + addrs.MakeMapElem(checkBlockAAddr, &states.CheckResultAggregate{ + Status: checks.StatusFail, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem(checkBlockAInstAddr, &states.CheckResultObject{ + Status: checks.StatusFail, + FailureMessages: []string{ + "Couldn't reverse the polarity.", + }, + }), + ), + }), ), }, []any{ map[string]any{ - "//": "EXPERIMENTAL: see docs for details", + "address": map[string]any{ + "kind": "check", + "to_display": "check.a", + "name": "a", + }, + "instances": []any{ + map[string]any{ + "address": map[string]any{ + "to_display": `check.a`, + }, + "problems": []any{ + map[string]any{ + "message": "Couldn't reverse the polarity.", + }, + }, + "status": "fail", + }, + }, + "status": "fail", + }, + map[string]any{ "address": map[string]any{ "kind": "output_value", "module": "module.child", @@ -118,7 +154,6 @@ func TestMarshalCheckStates(t *testing.T) { "status": "fail", }, map[string]any{ - "//": "EXPERIMENTAL: see docs for details", "address": map[string]any{ "kind": "resource", "mode": "managed", @@ -144,7 +179,6 @@ func TestMarshalCheckStates(t *testing.T) { "status": "fail", }, map[string]any{ - "//": "EXPERIMENTAL: see docs for details", "address": map[string]any{ "kind": "output_value", "name": "a", @@ -161,7 +195,6 @@ func TestMarshalCheckStates(t *testing.T) { "status": "fail", }, map[string]any{ - "//": "EXPERIMENTAL: see docs for details", "address": map[string]any{ "kind": "resource", "mode": "managed", diff --git a/internal/command/jsonchecks/doc.go b/internal/command/jsonchecks/doc.go index c495befb89..540b991455 100644 --- a/internal/command/jsonchecks/doc.go +++ b/internal/command/jsonchecks/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package jsonchecks implements the common JSON representation of check // results/statuses that we use across both the JSON plan and JSON state // representations. diff --git a/internal/command/jsonchecks/objects.go b/internal/command/jsonchecks/objects.go index d7a5014fee..b2a0c94e12 100644 --- a/internal/command/jsonchecks/objects.go +++ b/internal/command/jsonchecks/objects.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonchecks import ( @@ -45,6 +48,28 @@ func makeStaticObjectAddr(addr addrs.ConfigCheckable) staticObjectAddr { if !addr.Module.IsRoot() { ret["module"] = addr.Module.String() } + case addrs.ConfigCheck: + if kind := addr.CheckableKind(); kind != addrs.CheckableCheck { + // Something has gone very wrong + panic(fmt.Sprintf("%T has CheckableKind %s", addr, kind)) + } + + ret["kind"] = "check" + ret["name"] = addr.Check.Name + if !addr.Module.IsRoot() { + ret["module"] = addr.Module.String() + } + case addrs.ConfigInputVariable: + if kind := addr.CheckableKind(); kind != addrs.CheckableInputVariable { + // Something has gone very wrong + panic(fmt.Sprintf("%T has CheckableKind %s", addr, kind)) + } + + ret["kind"] = "var" + ret["name"] = addr.Variable.Name + if !addr.Module.IsRoot() { + ret["module"] = addr.Module.String() + } default: panic(fmt.Sprintf("unsupported ConfigCheckable implementation %T", addr)) } @@ -71,6 +96,14 @@ func makeDynamicObjectAddr(addr addrs.Checkable) dynamicObjectAddr { if !addr.Module.IsRoot() { ret["module"] = addr.Module.String() } + case addrs.AbsCheck: + if !addr.Module.IsRoot() { + ret["module"] = addr.Module.String() + } + case addrs.AbsInputVariableInstance: + if !addr.Module.IsRoot() { + ret["module"] = addr.Module.String() + } default: panic(fmt.Sprintf("unsupported Checkable implementation %T", addr)) } diff --git a/internal/command/jsonchecks/status.go b/internal/command/jsonchecks/status.go index f55194aeb0..bf590d240e 100644 --- a/internal/command/jsonchecks/status.go +++ b/internal/command/jsonchecks/status.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonchecks import ( diff --git a/internal/command/jsonconfig/config.go b/internal/command/jsonconfig/config.go index f744c91086..372f64ce76 100644 --- a/internal/command/jsonconfig/config.go +++ b/internal/command/jsonconfig/config.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonconfig import ( @@ -11,7 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/terraform" ) @@ -176,7 +179,7 @@ func marshalProviderConfigs( // version argument) and more accurate (as it reflects the full set of // constraints, in case there are multiple). if vc, ok := reqs[providerFqn]; ok { - p.VersionConstraint = getproviders.VersionConstraintsString(vc) + p.VersionConstraint = providerreqs.VersionConstraintsString(vc) } key := opaqueProviderKey(k, c.Path.String()) @@ -205,7 +208,7 @@ func marshalProviderConfigs( } if vc, ok := reqs[pr.Type]; ok { - p.VersionConstraint = getproviders.VersionConstraintsString(vc) + p.VersionConstraint = providerreqs.VersionConstraintsString(vc) } m[key] = p @@ -228,7 +231,7 @@ func marshalProviderConfigs( } if vc, ok := reqs[pr.Type]; ok { - p.VersionConstraint = getproviders.VersionConstraintsString(vc) + p.VersionConstraint = providerreqs.VersionConstraintsString(vc) } if c.Parent != nil { @@ -411,6 +414,7 @@ func marshalModuleCall(c *configs.Config, mc *configs.ModuleCall, schemas *terra schema.Attributes = make(map[string]*configschema.Attribute) for _, variable := range c.Module.Variables { schema.Attributes[variable.Name] = &configschema.Attribute{ + Type: cty.DynamicPseudoType, Required: variable.Default == cty.NilVal, } } @@ -468,17 +472,17 @@ func marshalResources(resources map[string]*configs.Resource, schemas *terraform } } - schema, schemaVer := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( v.Provider, v.Mode, v.Type, ) - if schema == nil { + if schema.Body == nil { return nil, fmt.Errorf("no schema found for %s (in provider %s)", v.Addr().String(), v.Provider) } - r.SchemaVersion = schemaVer + r.SchemaVersion = uint64(schema.Version) - r.Expressions = marshalExpressions(v.Config, schema) + r.Expressions = marshalExpressions(v.Config, schema.Body) // Managed is populated only for Mode = addrs.ManagedResourceMode if v.Managed != nil && len(v.Managed.Provisioners) > 0 { @@ -516,9 +520,9 @@ func marshalResources(resources map[string]*configs.Resource, schemas *terraform return rs, nil } -// Flatten all resource provider keys in a module and its descendents, such +// Flatten all resource provider keys in a module and its descendants, such // that any resources from providers using a configuration passed through the -// module call have a direct refernce to that provider configuration. +// module call have a direct reference to that provider configuration. func normalizeModuleProviderKeys(m *module, pcs map[string]providerConfig) { for i, r := range m.Resources { if pc, exists := pcs[r.ProviderConfigKey]; exists { diff --git a/internal/command/jsonconfig/config_test.go b/internal/command/jsonconfig/config_test.go index 69aeae3f03..5bcd51a13a 100644 --- a/internal/command/jsonconfig/config_test.go +++ b/internal/command/jsonconfig/config_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonconfig import ( diff --git a/internal/command/jsonconfig/doc.go b/internal/command/jsonconfig/doc.go index 28324a5787..4f8e025899 100644 --- a/internal/command/jsonconfig/doc.go +++ b/internal/command/jsonconfig/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package jsonconfig implements methods for outputting a configuration snapshot // in machine-readable json format package jsonconfig diff --git a/internal/command/jsonconfig/expression.go b/internal/command/jsonconfig/expression.go index fa443fc3ea..e32f087560 100644 --- a/internal/command/jsonconfig/expression.go +++ b/internal/command/jsonconfig/expression.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonconfig import ( @@ -7,12 +10,13 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang" - "github.com/hashicorp/terraform/internal/lang/blocktoattr" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/blocktoattr" + "github.com/hashicorp/terraform/internal/lang/langrefs" ) // expression represents any unparsed expression @@ -44,7 +48,7 @@ func marshalExpression(ex hcl.Expression) expression { ret.ConstantValue = valJSON } - refs, _ := lang.ReferencesInExpr(ex) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, ex) if len(refs) > 0 { var varString []string for _, ref := range refs { diff --git a/internal/command/jsonconfig/expression_test.go b/internal/command/jsonconfig/expression_test.go index 58af11dda5..2fb394f15a 100644 --- a/internal/command/jsonconfig/expression_test.go +++ b/internal/command/jsonconfig/expression_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonconfig import ( diff --git a/internal/command/jsonformat/README.md b/internal/command/jsonformat/README.md new file mode 100644 index 0000000000..dfbb420fda --- /dev/null +++ b/internal/command/jsonformat/README.md @@ -0,0 +1,244 @@ +# jsonformat + +This package contains functionality around formatting and displaying the JSON +structured output produced by adding the `-json` flag to various Terraform +commands. + +## Terraform Structured Plan Renderer + +As of January 2023, this package contains only a single structure: the +`Renderer`. + +The renderer accepts the JSON structured output produced by the +`terraform show -json` command and writes it in a human-readable +format. + +Implementation details and decisions for the `Renderer` are discussed in the +following sections. + +### Implementation + +There are two subpackages within the `jsonformat` renderer package. The `differ` +package compares the `before` and `after` values of the given plan and produces +`Diff` objects from the `computed` package. + +This approach is aimed at ensuring the process by which the plan difference is +calculated is separated from the rendering itself. In this way it should be +possible to modify the rendering or add new renderer formats without being +concerned with the complex diff calculations. + +#### The `differ` package + +The `differ` package operates on `Change` objects. These are produced from +`jsonplan.Change` objects (which are produced by the `terraform show` command). +Each `jsonplan.Change` object represents a single resource within the overall +Terraform configuration. + +The `differ` package will iterate through the `Change` objects and produce a +single `Diff` that represents a processed summary of the changes described by +the `Change`. You will see that the produced changes are nested so a change to a +list attribute will contain a slice of changes, this is discussed in the +"[The computed package](#the-computed-package)" section. + +##### The `Change` object + +The `Change` objects contain raw Golang representations of JSON objects (generic +`interface{}` fields). These are produced by parsing the `json.RawMessage` +objects within the provided changes. + +The fields the differ cares about from the provided changes are: + +- `Before`: The value before the proposed change. +- `After`: The value after the proposed change. +- `Unknown`: If the value is being computed during the change. +- `BeforeSensitive`: If the value was sensitive before the change. +- `AfterSensitive`: If the value is sensitive after the change. +- `ReplacePaths`: If the change is causing the overall resource to be replaced. + +In addition, the changes define two additional meta fields that they set and +manipulate internally: + +- `BeforeExplicit`: If the value in `Before` is explicit or an implied result due to a change elsewhere. +- `AfterExplicit`: If the value in `After` is explicit or an implied result due to a change elsewhere. + +The actual concrete type of each of the generic fields is determined by the +overall schema. The changes are also recursive, this means as we iterate through +the `Change` we create relevant child values based on the schema for the given +resource. + +For example, the initial change is always a `block` type which means the +`Before` and `After` values will actually be `map[string]interface{}` types +mapping each attribute and block to their relevant values. The +`Unknown`, `BeforeSensitive`, `AfterSensitive` values will all be either a +`map[string]interface{}` which maps each attribute or nested block to their +unknown and sensitive status, or it could simply be a `boolean` which generally +means the entire block and all children are sensitive or computed. + +In total, a `Change` can represent the following types: + +- `Attribute` + - `map`: Values will typically be `map[string]interface{}`. + - `list`: Values will typically be `[]interface{}`. + - `set`: Values will typically be `[]interface{}`. + - `object`: Values will typically be `map[string]interface{}`. + - `tuple`: Values will typically be `[]interface{}`. + - `bool`: Values will typically be a `bool`. + - `number`: Values will typically be a `float64`. + - `string`: Values will typically be a `string`. +- `Block`: Values will typically be `map[string]interface{}`, but they can be + split between nested blocks and attributes. +- `Output` + - Outputs are interesting as we don't have a schema for them, as such they + can be any JSON type. + - We also use the Output type to represent dynamic attributes, since in both + cases we work out the type based on the JSON representation instead of the + schema. + +The `ReplacePaths` field is unique in that it's value doesn't actually change +based on the schema - it's always a slice of index slices. An index in this +context will either be an integer pointing to a child of a set or a list or a +string pointing to the child of a map, object or block. As we iterate through +the value we manipulate the outer slice to remove child slices where the index +doesn't match and propagate paths that do match onto the children. + +*Quick note on explicit vs implicit:* In practice, it is only possible to get +implicit changes when you manipulate a collection. That is to say child values +of a modified collection will insert `nil` entries into the relevant before +or after fields of their child changes to represent their values being deleted +or created. It is also possible for users to explicitly put null values into +their collections, and this behaviour is different to deleting an item in the +collection. With the `BeforeExplicit` and `AfterExplicit` values we can tell the +difference between whether this value was removed from a collection or this +value was set to null in a collection. + +*Quick note on the go-cty Value and Type objects:* The `Before` and `After` +fields are actually go-cty values, but we cannot convert them directly because +of the HCP Terraform redacted endpoint. The redacted endpoint turns sensitive +values into strings regardless of their types. Because of this, we cannot just +do a direct conversion using the ctyjson package. We would have to iterate +through the schema first, find the sensitive values and their mapped types, +update the types inside the schema to strings, and then go back and do the +overall conversion. This isn't including any of the more complicated parts +around what happens if something was sensitive before and isn't sensitive after +or vice versa. This would mean the type would need to change between the before +and after value. It is in fact just easier to iterate through the values as +generic JSON interfaces, and obfuscate the sensitive values as we never need to +print them anyway. + +##### Iterating through changes + +The `differ` package will recursively create child `Change` objects for the +complex objects. + +There are two key subtypes of a `Change`: `SliceChange` and `MapChange`. +`SliceChange` values are used by list, set, and tuple attributes. `MapChange` +values are used by map and object attributes, and blocks. For what it is worth +outputs and dynamic types can end up using both, but they're kind of special as +the processing for dynamic types works out the type from the JSON struct and +then just passes it into the relevant real types for actual processing. + +The two subtypes implement `GetChild` functions that retrieve a child change +for a relevant index (`int` for slice, `string` for map). These functions build +an entirely populated `Change` object, and the package will then recursively +compute the change for the child (and all other children). When a complex change +has all the children changes, it then passes that into the relevant complex +diff type. + +#### The `computed` package + +A computed `Diff` should contain all the relevant information it needs to render +itself. + +The `Diff` itself contains the action (eg. `Create`, `Delete`, `Update`), and +whether this change is causing the overall resource to be replaced (read from +the `ReplacePaths` field discussed in the previous section). The actual content +of the diffs is passed directly into the internal renderer field. The internal +renderer is then an implementation that knows the actual content of the changes +and what they represent. + +For example to instantiate a diff resulting from updating a list of +primitives: + +```go + listDiff := computed.NewDiff(renderers.List([]computed.Diff{ + computed.NewDiff(renderers.Primitive(0.0, 0.0, cty.Number), plans.NoOp, false), + computed.NewDiff(renderers.Primitive(1.0, nil, cty.Number), plans.Delete, false), + computed.NewDiff(renderers.Primitive(nil, 4.0, cty.Number), plans.Create, false), + computed.NewDiff(renderers.Primitive(2.0, 2.0, cty.Number), plans.NoOp, false) + }, plans.Update, false)) +``` + +##### The `RenderHuman` function + +Currently, there is only one way to render a change, and it is implemented via +the `RenderHuman` function. In the future, there may be additional rendering +capabilities, but for now the `RenderHuman` function just passes the call +directly onto the internal renderer. + +Rendering the above diff with: `listDiff.RenderHuman(0, RenderOpts{})` would +produce: + +```text +[ + 0, + - 1 -> null, + + 4, + 2, +] +``` + +Note, the render function itself doesn't print out metadata about its own change +(eg. there's no `~` symbol in front of the opening bracket). The expectation is +that parent changes control how child changes are rendered, so are responsible +for deciding on their opening indentation, whether they have a key (as in maps, +objects, and blocks), or how the action symbol is displayed. + +In the above example, the primitive renderer would print out only `1 -> null` +while the surrounding list renderer is providing the indentation, the symbol and +the line ending commas. + +##### Implementing new diff types + +To implement a new diff type, you must implement the internal Renderer +functionality. To do this you create a new implementation of the +`computed.DiffRenderer`, make sure it accepts all the data you need, and +implement the `RenderHuman` function (and any other additional render functions +that may exist). + +Some changes publish warnings that should be displayed alongside them. +If your new change has no warnings you can use the `NoWarningsRenderer` to avoid +implementing the additional `Warnings` function. + +If/when new Renderer types are implemented, additional `Render` like functions +will be added. You should implement all of these with your new change type. + +##### Implementing new renderer types for changes + +As of January 2023, there is only a single type of renderer (the human-readable) +renderer. As such, the `Diff` structure provides a single `RenderHuman` +function. + +To implement a new renderer: + +1. Add a new render function onto the internal `DiffRenderer` interface. +2. Add a new render function onto the `Diff` struct that passes the call onto + the internal renderer. +3. Implement the new function on all the existing internal interfaces. + +Since each internal renderer contains all the information it needs to provide +change information about itself, your new Render function should pass in +anything it needs. + +### New types of Renderer + +In the future, we may wish to add in different kinds of renderer, such as a +compact renderer, or an interactive renderer. To do this, you'll need to modify +the Renderer struct or create a new type of Renderer. + +The logic around creating the `Diff` structures will be shared (ie. calling +into the differ package should be consistent across renderers). But when it +comes to rendering the changes, I'd expect the `Diff` structures to implement +additional functions that allow them to internally organise the data as required +and return a relevant object. For the existing human-readable renderer that is +simply a string, but for a future interactive renderer it might be a model from +an MVC pattern. diff --git a/internal/command/jsonformat/collections/action.go b/internal/command/jsonformat/collections/action.go new file mode 100644 index 0000000000..b7878453c6 --- /dev/null +++ b/internal/command/jsonformat/collections/action.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import "github.com/hashicorp/terraform/internal/plans" + +// CompareActions will compare current and next, and return plans.Update if they +// are different, and current if they are the same. +func CompareActions(current, next plans.Action) plans.Action { + if next == plans.NoOp { + return current + } + + if current != next { + return plans.Update + } + return current +} diff --git a/internal/command/jsonformat/collections/map.go b/internal/command/jsonformat/collections/map.go new file mode 100644 index 0000000000..9ec96ff3ce --- /dev/null +++ b/internal/command/jsonformat/collections/map.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +type ProcessKey func(key string) computed.Diff + +func TransformMap[Input any](before, after map[string]Input, keys []string, process ProcessKey) (map[string]computed.Diff, plans.Action) { + current := plans.NoOp + if before != nil && after == nil { + current = plans.Delete + } + if before == nil && after != nil { + current = plans.Create + } + + elements := make(map[string]computed.Diff) + for _, key := range keys { + elements[key] = process(key) + current = CompareActions(current, elements[key].Action) + } + + return elements, current +} diff --git a/internal/command/jsonformat/collections/slice.go b/internal/command/jsonformat/collections/slice.go new file mode 100644 index 0000000000..7e1fa2f910 --- /dev/null +++ b/internal/command/jsonformat/collections/slice.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package collections + +import ( + "reflect" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/objchange" +) + +type TransformIndices func(before, after int) computed.Diff +type ProcessIndices func(before, after int) +type IsObjType[Input any] func(input Input) bool + +func TransformSlice[Input any](before, after []Input, process TransformIndices, isObjType IsObjType[Input]) ([]computed.Diff, plans.Action) { + current := plans.NoOp + if before != nil && after == nil { + current = plans.Delete + } + if before == nil && after != nil { + current = plans.Create + } + + var elements []computed.Diff + ProcessSlice(before, after, func(before, after int) { + element := process(before, after) + elements = append(elements, element) + current = CompareActions(current, element.Action) + }, isObjType) + return elements, current +} + +func ProcessSlice[Input any](before, after []Input, process ProcessIndices, isObjType IsObjType[Input]) { + // If before and after are the same length and is not a reordering + // we want to compare elements on an individual basis + if len(before) == len(after) && !isReorder(before, after) { + for ix := range before { + process(ix, ix) + } + return + } + + lcs := objchange.LongestCommonSubsequence(before, after, func(before, after Input) bool { + return reflect.DeepEqual(before, after) + }) + + var beforeIx, afterIx, lcsIx int + for beforeIx < len(before) || afterIx < len(after) || lcsIx < len(lcs) { + // Step through all the before values until we hit the next item in the + // longest common subsequence. We are going to just say that all of + // these have been deleted. + for beforeIx < len(before) && (lcsIx >= len(lcs) || !reflect.DeepEqual(before[beforeIx], lcs[lcsIx])) { + isObjectDiff := isObjType(before[beforeIx]) && afterIx < len(after) && isObjType(after[afterIx]) && (lcsIx >= len(lcs) || !reflect.DeepEqual(after[afterIx], lcs[lcsIx])) + if isObjectDiff { + process(beforeIx, afterIx) + beforeIx++ + afterIx++ + continue + } + + process(beforeIx, len(after)) + beforeIx++ + } + + // Now, step through all the after values until hit the next item in the + // LCS. We are going to say that all of these have been created. + for afterIx < len(after) && (lcsIx >= len(lcs) || !reflect.DeepEqual(after[afterIx], lcs[lcsIx])) { + process(len(before), afterIx) + afterIx++ + } + + // Finally, add the item in common as unchanged. + if lcsIx < len(lcs) { + process(beforeIx, afterIx) + beforeIx++ + afterIx++ + lcsIx++ + } + } +} + +// isReorder returns true if every item of before can be found in after +func isReorder[Input any](before, after []Input) bool { + // To be a reorder the length needs to be the same + if len(before) != len(after) { + return false + } + + for _, b := range before { + hasMatch := false + for _, a := range after { + if reflect.DeepEqual(b, a) { + // Match found, no need to search anymore + hasMatch = true + break + } + } + if !hasMatch { + return false + } + } + + return true +} diff --git a/internal/command/jsonformat/computed/diff.go b/internal/command/jsonformat/computed/diff.go new file mode 100644 index 0000000000..31f23c566c --- /dev/null +++ b/internal/command/jsonformat/computed/diff.go @@ -0,0 +1,133 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package computed + +import ( + "github.com/mitchellh/colorstring" + + "github.com/hashicorp/terraform/internal/plans" +) + +// Diff captures the computed diff for a single block, element or attribute. +// +// It essentially merges common functionality across all types of changes, +// namely the replace logic and the action / change type. Any remaining +// behaviour can be offloaded to the renderer which will be unique for the +// various change types (eg. maps, objects, lists, blocks, primitives, etc.). +type Diff struct { + // Renderer captures the uncommon functionality across the different kinds + // of changes. Each type of change (lists, blocks, sets, etc.) will have a + // unique renderer. + Renderer DiffRenderer + + // Action is the action described by this change (such as create, delete, + // update, etc.). + Action plans.Action + + // Replace tells the Change that it should add the `# forces replacement` + // suffix. + // + // Every single change could potentially add this suffix, so we embed it in + // the change as common functionality instead of in the specific renderers. + Replace bool +} + +// NewDiff creates a new Diff object with the provided renderer, action and +// replace context. +func NewDiff(renderer DiffRenderer, action plans.Action, replace bool) Diff { + return Diff{ + Renderer: renderer, + Action: action, + Replace: replace, + } +} + +// RenderHuman prints the Change into a human-readable string referencing the +// specified RenderOpts. +// +// If the returned string is a single line, then indent should be ignored. +// +// If the return string is multiple lines, then indent should be used to offset +// the beginning of all lines but the first by the specified amount. +func (diff Diff) RenderHuman(indent int, opts RenderHumanOpts) string { + return diff.Renderer.RenderHuman(diff, indent, opts) +} + +// WarningsHuman returns a list of strings that should be rendered as warnings +// before a given change is rendered. +// +// As with the RenderHuman function, the indent should only be applied on +// multiline warnings and on the second and following lines. +func (diff Diff) WarningsHuman(indent int, opts RenderHumanOpts) []string { + return diff.Renderer.WarningsHuman(diff, indent, opts) +} + +type DiffRenderer interface { + RenderHuman(diff Diff, indent int, opts RenderHumanOpts) string + WarningsHuman(diff Diff, indent int, opts RenderHumanOpts) []string +} + +// RenderHumanOpts contains options that can control how the human render +// function of the DiffRenderer will function. +type RenderHumanOpts struct { + Colorize *colorstring.Colorize + + // OverrideNullSuffix tells the Renderer not to display the `-> null` suffix + // that is normally displayed when an element, attribute, or block is + // deleted. + OverrideNullSuffix bool + + // ForceForcesReplacement tells the Renderer to display the + // `# forces replacement` suffix, even if a diff doesn't have the Replace + // field set. + // + // Some renderers (like the Set renderer) don't display the suffix + // themselves but force their child diffs to display it instead. + ForceForcesReplacement bool + + // ForbidForcesReplacement is the opposite of ForceForcesReplacement. It + // tells the Renderer to not display the '# forces replacement' suffix, even + // if a diff does have the Replace field set. + // + // Some renderers (like the Unknown renderer) want to capture the + // forceReplacement setting at their level instead of within the children. + ForbidForcesReplacement bool + + // ShowUnchangedChildren instructs the Renderer to render all children of a + // given complex change, instead of hiding unchanged items and compressing + // them into a single line. + ShowUnchangedChildren bool + + // HideDiffActionSymbols tells the renderer not to show the '+'/'-' symbols + // and to skip the places where the symbols would result in an offset. + HideDiffActionSymbols bool +} + +// NewRenderHumanOpts creates a new RenderHumanOpts struct with the required +// fields set. +func NewRenderHumanOpts(colorize *colorstring.Colorize) RenderHumanOpts { + return RenderHumanOpts{ + Colorize: colorize, + } +} + +// Clone returns a new RenderOpts object, that matches the original but can be +// edited without changing the original. +func (opts RenderHumanOpts) Clone() RenderHumanOpts { + return RenderHumanOpts{ + Colorize: opts.Colorize, + + OverrideNullSuffix: opts.OverrideNullSuffix, + ShowUnchangedChildren: opts.ShowUnchangedChildren, + HideDiffActionSymbols: opts.HideDiffActionSymbols, + + // ForceForcesReplacement and ForbidForcesReplacement are special cases + // in that they don't cascade. So each diff should decide independently + // whether it's direct children should override their internal Replace + // logic, instead of an ancestor making the switch and affecting the + // entire tree. + ForceForcesReplacement: false, + ForbidForcesReplacement: false, + } +} diff --git a/internal/command/jsonformat/computed/doc.go b/internal/command/jsonformat/computed/doc.go new file mode 100644 index 0000000000..1d52d0d050 --- /dev/null +++ b/internal/command/jsonformat/computed/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package computed contains types that represent the computed diffs for +// Terraform blocks, attributes, and outputs. +// +// Each Diff struct is made up of a renderer, an action, and a boolean +// describing the diff. The renderer internally holds child diffs or concrete +// values that allow it to know how to render the diff appropriately. +package computed diff --git a/internal/command/jsonformat/computed/renderers/block.go b/internal/command/jsonformat/computed/renderers/block.go new file mode 100644 index 0000000000..8b297ea1c3 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/block.go @@ -0,0 +1,197 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + + "github.com/hashicorp/terraform/internal/plans" +) + +var ( + _ computed.DiffRenderer = (*blockRenderer)(nil) + + importantAttributes = []string{ + "id", + "name", + "tags", + } +) + +func importantAttribute(attr string) bool { + for _, attribute := range importantAttributes { + if attribute == attr { + return true + } + } + return false +} + +func Block(attributes map[string]computed.Diff, blocks Blocks) computed.DiffRenderer { + return &blockRenderer{ + attributes: attributes, + blocks: blocks, + } +} + +type blockRenderer struct { + NoWarningsRenderer + + attributes map[string]computed.Diff + blocks Blocks +} + +func (renderer blockRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + if len(renderer.attributes) == 0 && len(renderer.blocks.GetAllKeys()) == 0 { + return fmt.Sprintf("{}%s", forcesReplacement(diff.Replace, opts)) + } + + unchangedAttributes := 0 + unchangedBlocks := 0 + + maximumAttributeKeyLen := 0 + var attributeKeys []string + escapedAttributeKeys := make(map[string]string) + for key := range renderer.attributes { + attributeKeys = append(attributeKeys, key) + escapedKey := EnsureValidAttributeName(key) + escapedAttributeKeys[key] = escapedKey + if maximumAttributeKeyLen < len(escapedKey) { + maximumAttributeKeyLen = len(escapedKey) + } + } + sort.Strings(attributeKeys) + + importantAttributeOpts := opts.Clone() + importantAttributeOpts.ShowUnchangedChildren = true + + attributeOpts := opts.Clone() + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts))) + for _, key := range attributeKeys { + attribute := renderer.attributes[key] + if importantAttribute(key) { + + // Always display the important attributes. + for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, importantAttributeOpts), maximumAttributeKeyLen, key, attribute.RenderHuman(indent+1, importantAttributeOpts))) + continue + } + if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren { + unchangedAttributes++ + continue + } + + for _, warning := range attribute.WarningsHuman(indent+1, opts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, attributeOpts), maximumAttributeKeyLen, escapedAttributeKeys[key], attribute.RenderHuman(indent+1, attributeOpts))) + } + + if unchangedAttributes > 0 { + buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("attribute", unchangedAttributes, opts))) + } + + blockKeys := renderer.blocks.GetAllKeys() + for _, key := range blockKeys { + + foundChangedBlock := false + renderBlock := func(diff computed.Diff, mapKey string, opts computed.RenderHumanOpts) { + + creatingSensitiveValue := diff.Action == plans.Create && renderer.blocks.AfterSensitiveBlocks[key] + deletingSensitiveValue := diff.Action == plans.Delete && renderer.blocks.BeforeSensitiveBlocks[key] + modifyingSensitiveValue := (diff.Action == plans.Update || diff.Action == plans.NoOp) && (renderer.blocks.AfterSensitiveBlocks[key] || renderer.blocks.BeforeSensitiveBlocks[key]) + + if creatingSensitiveValue || deletingSensitiveValue || modifyingSensitiveValue { + // Intercept the renderer here if the sensitive data was set + // across all the blocks instead of individually. + action := diff.Action + if diff.Action == plans.NoOp && renderer.blocks.BeforeSensitiveBlocks[key] != renderer.blocks.AfterSensitiveBlocks[key] { + action = plans.Update + } + + diff = computed.NewDiff(SensitiveBlock(diff, renderer.blocks.BeforeSensitiveBlocks[key], renderer.blocks.AfterSensitiveBlocks[key]), action, diff.Replace) + } + + if diff.Action == plans.NoOp && !opts.ShowUnchangedChildren { + unchangedBlocks++ + return + } + + if !foundChangedBlock && len(renderer.attributes) > 0 { + // We always want to put an extra new line between the + // attributes and blocks, and between groups of blocks. + buf.WriteString("\n") + foundChangedBlock = true + } + + // If the force replacement metadata was set for every entry in the + // block we need to override that here. Our child blocks will only + // know about the replace function if it was set on them + // specifically, and not if it was set for all the blocks. + blockOpts := opts.Clone() + blockOpts.ForceForcesReplacement = renderer.blocks.ReplaceBlocks[key] + + for _, warning := range diff.WarningsHuman(indent+1, blockOpts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%s%s %s\n", formatIndent(indent+1), writeDiffActionSymbol(diff.Action, blockOpts), EnsureValidAttributeName(key), mapKey, diff.RenderHuman(indent+1, blockOpts))) + + } + + switch { + case renderer.blocks.IsSingleBlock(key): + renderBlock(renderer.blocks.SingleBlocks[key], "", opts) + case renderer.blocks.IsMapBlock(key): + var keys []string + for key := range renderer.blocks.MapBlocks[key] { + keys = append(keys, key) + } + sort.Strings(keys) + + if renderer.blocks.UnknownBlocks[key] { + renderBlock(computed.NewDiff(Unknown(computed.Diff{}), diff.Action, false), "", opts) + } + + for _, innerKey := range keys { + renderBlock(renderer.blocks.MapBlocks[key][innerKey], fmt.Sprintf(" %q", innerKey), opts) + } + case renderer.blocks.IsSetBlock(key): + + setOpts := opts.Clone() + setOpts.ForceForcesReplacement = diff.Replace + + if renderer.blocks.UnknownBlocks[key] { + renderBlock(computed.NewDiff(Unknown(computed.Diff{}), diff.Action, false), "", opts) + } + + for _, block := range renderer.blocks.SetBlocks[key] { + renderBlock(block, "", opts) + } + case renderer.blocks.IsListBlock(key): + + if renderer.blocks.UnknownBlocks[key] { + renderBlock(computed.NewDiff(Unknown(computed.Diff{}), diff.Action, false), "", opts) + } + + for _, block := range renderer.blocks.ListBlocks[key] { + renderBlock(block, "", opts) + } + } + } + + if unchangedBlocks > 0 { + buf.WriteString(fmt.Sprintf("\n%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("block", unchangedBlocks, opts))) + } + + buf.WriteString(fmt.Sprintf("%s%s}", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts))) + return buf.String() +} diff --git a/internal/command/jsonformat/computed/renderers/blocks.go b/internal/command/jsonformat/computed/renderers/blocks.go new file mode 100644 index 0000000000..a251cecde5 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/blocks.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "sort" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" +) + +// Blocks is a helper struct for collating the different kinds of blocks in a +// simple way for rendering. +type Blocks struct { + SingleBlocks map[string]computed.Diff + ListBlocks map[string][]computed.Diff + SetBlocks map[string][]computed.Diff + MapBlocks map[string]map[string]computed.Diff + + // ReplaceBlocks and Before/AfterSensitiveBlocks carry forward the + // information about an entire group of blocks (eg. if all the blocks for a + // given list block are sensitive that isn't captured in the individual + // blocks as they are processed independently). These maps allow the + // renderer to check the metadata on the overall groups and respond + // accordingly. + + ReplaceBlocks map[string]bool + BeforeSensitiveBlocks map[string]bool + AfterSensitiveBlocks map[string]bool + UnknownBlocks map[string]bool +} + +func (blocks *Blocks) GetAllKeys() []string { + var keys []string + for key := range blocks.SingleBlocks { + keys = append(keys, key) + } + for key := range blocks.ListBlocks { + keys = append(keys, key) + } + for key := range blocks.SetBlocks { + keys = append(keys, key) + } + for key := range blocks.MapBlocks { + keys = append(keys, key) + } + sort.Strings(keys) + return keys +} + +func (blocks *Blocks) IsSingleBlock(key string) bool { + _, ok := blocks.SingleBlocks[key] + return ok +} + +func (blocks *Blocks) IsListBlock(key string) bool { + _, ok := blocks.ListBlocks[key] + return ok +} + +func (blocks *Blocks) IsMapBlock(key string) bool { + _, ok := blocks.MapBlocks[key] + return ok +} + +func (blocks *Blocks) IsSetBlock(key string) bool { + _, ok := blocks.SetBlocks[key] + return ok +} + +func (blocks *Blocks) AddSingleBlock(key string, diff computed.Diff, replace, beforeSensitive, afterSensitive, unknown bool) { + blocks.SingleBlocks[key] = diff + blocks.ReplaceBlocks[key] = replace + blocks.BeforeSensitiveBlocks[key] = beforeSensitive + blocks.AfterSensitiveBlocks[key] = afterSensitive + blocks.UnknownBlocks[key] = unknown +} + +func (blocks *Blocks) AddAllListBlock(key string, diffs []computed.Diff, replace, beforeSensitive, afterSensitive, unknown bool) { + blocks.ListBlocks[key] = diffs + blocks.ReplaceBlocks[key] = replace + blocks.BeforeSensitiveBlocks[key] = beforeSensitive + blocks.AfterSensitiveBlocks[key] = afterSensitive + blocks.UnknownBlocks[key] = unknown +} + +func (blocks *Blocks) AddAllSetBlock(key string, diffs []computed.Diff, replace, beforeSensitive, afterSensitive, unknown bool) { + blocks.SetBlocks[key] = diffs + blocks.ReplaceBlocks[key] = replace + blocks.BeforeSensitiveBlocks[key] = beforeSensitive + blocks.AfterSensitiveBlocks[key] = afterSensitive + blocks.UnknownBlocks[key] = unknown +} + +func (blocks *Blocks) AddAllMapBlocks(key string, diffs map[string]computed.Diff, replace, beforeSensitive, afterSensitive, unknown bool) { + blocks.MapBlocks[key] = diffs + blocks.ReplaceBlocks[key] = replace + blocks.BeforeSensitiveBlocks[key] = beforeSensitive + blocks.AfterSensitiveBlocks[key] = afterSensitive + blocks.UnknownBlocks[key] = unknown +} diff --git a/internal/command/jsonformat/computed/renderers/json.go b/internal/command/jsonformat/computed/renderers/json.go new file mode 100644 index 0000000000..01c072d8eb --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/json.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/jsondiff" + "github.com/hashicorp/terraform/internal/plans" +) + +// RendererJsonOpts creates a jsondiff.JsonOpts object that returns the correct +// embedded renderers for each JSON type. +// +// We need to define this in our renderers package in order to avoid cycles, and +// to help with reuse between the output processing in the differs package, and +// our JSON string rendering here. +func RendererJsonOpts() jsondiff.JsonOpts { + return jsondiff.JsonOpts{ + Primitive: func(before, after interface{}, ctype cty.Type, action plans.Action) computed.Diff { + return computed.NewDiff(Primitive(before, after, ctype), action, false) + }, + Object: func(elements map[string]computed.Diff, action plans.Action) computed.Diff { + return computed.NewDiff(Object(elements), action, false) + }, + Array: func(elements []computed.Diff, action plans.Action) computed.Diff { + return computed.NewDiff(List(elements), action, false) + }, + Unknown: func(diff computed.Diff, action plans.Action) computed.Diff { + return computed.NewDiff(Unknown(diff), action, false) + }, + Sensitive: func(diff computed.Diff, beforeSensitive bool, afterSensitive bool, action plans.Action) computed.Diff { + return computed.NewDiff(Sensitive(diff, beforeSensitive, afterSensitive), action, false) + }, + TypeChange: func(before, after computed.Diff, action plans.Action) computed.Diff { + return computed.NewDiff(TypeChange(before, after), action, false) + }, + } +} diff --git a/internal/command/jsonformat/computed/renderers/list.go b/internal/command/jsonformat/computed/renderers/list.go new file mode 100644 index 0000000000..d7044bc261 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/list.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "bytes" + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*listRenderer)(nil) + +func List(elements []computed.Diff) computed.DiffRenderer { + return &listRenderer{ + displayContext: true, + elements: elements, + } +} + +func NestedList(elements []computed.Diff) computed.DiffRenderer { + return &listRenderer{ + elements: elements, + } +} + +type listRenderer struct { + NoWarningsRenderer + + displayContext bool + elements []computed.Diff +} + +func (renderer listRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + if len(renderer.elements) == 0 { + return fmt.Sprintf("[]%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) + } + + elementOpts := opts.Clone() + elementOpts.OverrideNullSuffix = true + + unchangedElementOpts := opts.Clone() + unchangedElementOpts.ShowUnchangedChildren = true + + var unchangedElements []computed.Diff + + // renderNext tells the renderer to print out the next element in the list + // whatever state it is in. So, even if a change is a NoOp we will still + // print it out if the last change we processed wants us to. + renderNext := false + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("[%s\n", forcesReplacement(diff.Replace, opts))) + for _, element := range renderer.elements { + if element.Action == plans.NoOp && !renderNext && !opts.ShowUnchangedChildren { + unchangedElements = append(unchangedElements, element) + continue + } + renderNext = false + + opts := elementOpts + + // If we want to display the context around this change, we want to + // render the change immediately before this change in the list, and the + // change immediately after in the list, even if both these changes are + // NoOps. This will give the user reading the diff some context as to + // where in the list these changes are being made, as order matters. + if renderer.displayContext { + // If our list of unchanged elements contains more than one entry + // we'll print out a count of the number of unchanged elements that + // we skipped. Note, this is the length of the unchanged elements + // minus 1 as the most recent unchanged element will be printed out + // in full. + if len(unchangedElements) > 1 { + buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", len(unchangedElements)-1, opts))) + } + // If our list of unchanged elements contains at least one entry, + // we're going to print out the most recent change in full. That's + // what happens here. + if len(unchangedElements) > 0 { + lastElement := unchangedElements[len(unchangedElements)-1] + buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(lastElement.Action, unchangedElementOpts), lastElement.RenderHuman(indent+1, unchangedElementOpts))) + } + // We now reset the unchanged elements list, we've printed out a + // count of all the elements we skipped so we start counting from + // scratch again. This means that if we process a run of changed + // elements, they won't all start printing out summaries of every + // change that happened previously. + unchangedElements = nil + + if element.Action == plans.NoOp { + // If this is a NoOp action then we're going to render it below + // so we need to just override the opts we're going to use to + // make sure we use the unchanged opts. + opts = unchangedElementOpts + } else { + // As we also want to render the element immediately after any + // changes, we make a note here to say we should render the next + // change whatever it is. But, we only want to render the next + // change if the current change isn't a NoOp. If the current change + // is a NoOp then it was told to print by the last change and we + // don't want to cascade and print all changes from now on. + renderNext = true + } + } + + for _, warning := range element.WarningsHuman(indent+1, opts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, opts), element.RenderHuman(indent+1, opts))) + } + + // If we were not displaying any context alongside our changes then the + // unchangedElements list will contain every unchanged element, and we'll + // print that out as we do with every other collection. + // + // If we were displaying context, then this will contain any unchanged + // elements since our last change, so we should also print it out. + if len(unchangedElements) > 0 { + buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", len(unchangedElements), opts))) + } + + buf.WriteString(fmt.Sprintf("%s%s]%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts))) + return buf.String() +} diff --git a/internal/command/jsonformat/computed/renderers/map.go b/internal/command/jsonformat/computed/renderers/map.go new file mode 100644 index 0000000000..1c822e087d --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/map.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*mapRenderer)(nil) + +func Map(elements map[string]computed.Diff) computed.DiffRenderer { + return &mapRenderer{ + elements: elements, + alignKeys: true, + } +} + +func NestedMap(elements map[string]computed.Diff) computed.DiffRenderer { + return &mapRenderer{ + elements: elements, + overrideNullSuffix: true, + overrideForcesReplacement: true, + } +} + +type mapRenderer struct { + NoWarningsRenderer + + elements map[string]computed.Diff + + overrideNullSuffix bool + overrideForcesReplacement bool + alignKeys bool +} + +func (renderer mapRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + forcesReplacementSelf := diff.Replace && !renderer.overrideForcesReplacement + forcesReplacementChildren := diff.Replace && renderer.overrideForcesReplacement + + if len(renderer.elements) == 0 { + return fmt.Sprintf("{}%s%s", nullSuffix(diff.Action, opts), forcesReplacement(forcesReplacementSelf, opts)) + } + + // Sort the map elements by key, so we have a deterministic ordering in + // the output. + var keys []string + + // We need to make sure the keys are capable of rendering properly. + escapedKeys := make(map[string]string) + + maximumKeyLen := 0 + for key := range renderer.elements { + keys = append(keys, key) + + escapedKey := hclEscapeString(key) + escapedKeys[key] = escapedKey + if maximumKeyLen < len(escapedKey) { + maximumKeyLen = len(escapedKey) + } + } + sort.Strings(keys) + + unchangedElements := 0 + + elementOpts := opts.Clone() + elementOpts.OverrideNullSuffix = diff.Action == plans.Delete || renderer.overrideNullSuffix + elementOpts.ForceForcesReplacement = forcesReplacementChildren + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(forcesReplacementSelf, opts))) + for _, key := range keys { + element := renderer.elements[key] + + if element.Action == plans.NoOp && !opts.ShowUnchangedChildren { + // Don't render NoOp operations when we are compact display. + unchangedElements++ + continue + } + + for _, warning := range element.WarningsHuman(indent+1, opts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + // Only show commas between elements for objects. + comma := "" + if _, ok := element.Renderer.(*objectRenderer); ok { + comma = "," + } + + if renderer.alignKeys { + buf.WriteString(fmt.Sprintf("%s%s%-*s = %s%s\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, elementOpts), maximumKeyLen, escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma)) + } else { + buf.WriteString(fmt.Sprintf("%s%s%s = %s%s\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, elementOpts), escapedKeys[key], element.RenderHuman(indent+1, elementOpts), comma)) + } + + } + + if unchangedElements > 0 { + buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", unchangedElements, opts))) + } + + buf.WriteString(fmt.Sprintf("%s%s}%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts))) + return buf.String() +} diff --git a/internal/command/jsonformat/computed/renderers/object.go b/internal/command/jsonformat/computed/renderers/object.go new file mode 100644 index 0000000000..587c1e3c95 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/object.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "bytes" + "fmt" + "sort" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*objectRenderer)(nil) + +func Object(attributes map[string]computed.Diff) computed.DiffRenderer { + return &objectRenderer{ + attributes: attributes, + overrideNullSuffix: true, + } +} + +func NestedObject(attributes map[string]computed.Diff) computed.DiffRenderer { + return &objectRenderer{ + attributes: attributes, + overrideNullSuffix: false, + } +} + +type objectRenderer struct { + NoWarningsRenderer + + attributes map[string]computed.Diff + overrideNullSuffix bool +} + +func (renderer objectRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + if len(renderer.attributes) == 0 { + return fmt.Sprintf("{}%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) + } + + attributeOpts := opts.Clone() + attributeOpts.OverrideNullSuffix = renderer.overrideNullSuffix + + // We need to keep track of our keys in two ways. The first is the order in + // which we will display them. The second is a mapping to their safely + // escaped equivalent. + + maximumKeyLen := 0 + var keys []string + escapedKeys := make(map[string]string) + for key := range renderer.attributes { + keys = append(keys, key) + escapedKey := EnsureValidAttributeName(key) + escapedKeys[key] = escapedKey + if maximumKeyLen < len(escapedKey) { + maximumKeyLen = len(escapedKey) + } + } + sort.Strings(keys) + + unchangedAttributes := 0 + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("{%s\n", forcesReplacement(diff.Replace, opts))) + for _, key := range keys { + attribute := renderer.attributes[key] + + if importantAttribute(key) { + importantAttributeOpts := attributeOpts.Clone() + importantAttributeOpts.ShowUnchangedChildren = true + + for _, warning := range attribute.WarningsHuman(indent+1, importantAttributeOpts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, importantAttributeOpts), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, importantAttributeOpts))) + continue + } + + if attribute.Action == plans.NoOp && !opts.ShowUnchangedChildren { + // Don't render NoOp operations when we are compact display. + unchangedAttributes++ + continue + } + + for _, warning := range attribute.WarningsHuman(indent+1, opts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%-*s = %s\n", formatIndent(indent+1), writeDiffActionSymbol(attribute.Action, attributeOpts), maximumKeyLen, escapedKeys[key], attribute.RenderHuman(indent+1, attributeOpts))) + } + + if unchangedAttributes > 0 { + buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("attribute", unchangedAttributes, opts))) + } + + buf.WriteString(fmt.Sprintf("%s%s}%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts))) + return buf.String() +} diff --git a/internal/command/jsonformat/computed/renderers/primitive.go b/internal/command/jsonformat/computed/renderers/primitive.go new file mode 100644 index 0000000000..67a9ae920e --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/primitive.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*primitiveRenderer)(nil) + +func Primitive(before, after interface{}, ctype cty.Type) computed.DiffRenderer { + return &primitiveRenderer{ + before: before, + after: after, + ctype: ctype, + } +} + +type primitiveRenderer struct { + NoWarningsRenderer + + before interface{} + after interface{} + ctype cty.Type +} + +func (renderer primitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + if renderer.ctype == cty.String { + return renderer.renderStringDiff(diff, indent, opts) + } + + beforeValue := renderPrimitiveValue(renderer.before, renderer.ctype, opts) + afterValue := renderPrimitiveValue(renderer.after, renderer.ctype, opts) + + switch diff.Action { + case plans.Create: + return fmt.Sprintf("%s%s", afterValue, forcesReplacement(diff.Replace, opts)) + case plans.Delete: + return fmt.Sprintf("%s%s%s", beforeValue, nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) + case plans.NoOp: + return fmt.Sprintf("%s%s", beforeValue, forcesReplacement(diff.Replace, opts)) + default: + return fmt.Sprintf("%s %s %s%s", beforeValue, opts.Colorize.Color("[yellow]->[reset]"), afterValue, forcesReplacement(diff.Replace, opts)) + } +} + +func renderPrimitiveValue(value interface{}, t cty.Type, opts computed.RenderHumanOpts) string { + if value == nil { + return opts.Colorize.Color("[dark_gray]null[reset]") + } + + switch { + case t == cty.Bool: + if value.(bool) { + return "true" + } + return "false" + case t == cty.Number: + number := value.(json.Number) + return number.String() + default: + panic("unrecognized primitive type: " + t.FriendlyName()) + } +} + +func (renderer primitiveRenderer) renderStringDiff(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + + // We process multiline strings at the end of the switch statement. + var lines []string + + switch diff.Action { + case plans.Create, plans.NoOp: + str := evaluatePrimitiveString(renderer.after, opts) + + if str.Json != nil { + if diff.Action == plans.NoOp { + return renderer.renderStringDiffAsJson(diff, indent, opts, str, str) + } else { + return renderer.renderStringDiffAsJson(diff, indent, opts, evaluatedString{}, str) + } + } + + if !str.IsMultiline { + return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts)) + } + + // We are creating a single multiline string, so let's split by the new + // line character. While we are doing this, we are going to insert our + // indents and make sure each line is formatted correctly. + lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\n") + + // We now just need to do the same for the first entry in lines, because + // we split on the new line characters which won't have been at the + // beginning of the first line. + lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0]) + case plans.Delete: + str := evaluatePrimitiveString(renderer.before, opts) + if str.IsNull { + // We don't put the null suffix (-> null) here because the final + // render or null -> null would look silly. + return fmt.Sprintf("%s%s", str.RenderSimple(), forcesReplacement(diff.Replace, opts)) + } + + if str.Json != nil { + return renderer.renderStringDiffAsJson(diff, indent, opts, str, evaluatedString{}) + } + + if !str.IsMultiline { + return fmt.Sprintf("%s%s%s", str.RenderSimple(), nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) + } + + // We are creating a single multiline string, so let's split by the new + // line character. While we are doing this, we are going to insert our + // indents and make sure each line is formatted correctly. + lines = strings.Split(strings.ReplaceAll(str.String, "\n", fmt.Sprintf("\n%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts))), "\n") + + // We now just need to do the same for the first entry in lines, because + // we split on the new line characters which won't have been at the + // beginning of the first line. + lines[0] = fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), lines[0]) + default: + beforeString := evaluatePrimitiveString(renderer.before, opts) + afterString := evaluatePrimitiveString(renderer.after, opts) + + if beforeString.Json != nil && afterString.Json != nil { + return renderer.renderStringDiffAsJson(diff, indent, opts, beforeString, afterString) + } + + if beforeString.Json != nil || afterString.Json != nil { + // This means one of the strings is JSON and one isn't. We're going + // to be a little inefficient here, but we can just reuse another + // renderer for this so let's keep it simple. + return computed.NewDiff( + TypeChange( + computed.NewDiff(Primitive(renderer.before, nil, cty.String), plans.Delete, false), + computed.NewDiff(Primitive(nil, renderer.after, cty.String), plans.Create, false)), + diff.Action, + diff.Replace).RenderHuman(indent, opts) + } + + if !beforeString.IsMultiline && !afterString.IsMultiline { + return fmt.Sprintf("%s %s %s%s", beforeString.RenderSimple(), opts.Colorize.Color("[yellow]->[reset]"), afterString.RenderSimple(), forcesReplacement(diff.Replace, opts)) + } + + beforeLines := strings.Split(beforeString.String, "\n") + afterLines := strings.Split(afterString.String, "\n") + + processIndices := func(beforeIx, afterIx int) { + if beforeIx < 0 || beforeIx >= len(beforeLines) { + lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Create, opts), afterLines[afterIx])) + return + } + + if afterIx < 0 || afterIx >= len(afterLines) { + lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Delete, opts), beforeLines[beforeIx])) + return + } + + if beforeLines[beforeIx] != afterLines[afterIx] { + lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Delete, opts), beforeLines[beforeIx])) + lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.Create, opts), afterLines[afterIx])) + return + } + + lines = append(lines, fmt.Sprintf("%s%s%s", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), beforeLines[beforeIx])) + } + isObjType := func(_ string) bool { + return false + } + + collections.ProcessSlice(beforeLines, afterLines, processIndices, isObjType) + } + + // We return early if we find non-multiline strings or JSON strings, so we + // know here that we just render the lines slice properly. + return fmt.Sprintf("<<-EOT%s\n%s\n%s%sEOT%s", + forcesReplacement(diff.Replace, opts), + strings.Join(lines, "\n"), + formatIndent(indent), + writeDiffActionSymbol(plans.NoOp, opts), + nullSuffix(diff.Action, opts)) +} + +func (renderer primitiveRenderer) renderStringDiffAsJson(diff computed.Diff, indent int, opts computed.RenderHumanOpts, before evaluatedString, after evaluatedString) string { + jsonDiff := RendererJsonOpts().Transform(structured.Change{ + BeforeExplicit: diff.Action != plans.Create, + AfterExplicit: diff.Action != plans.Delete, + Before: before.Json, + After: after.Json, + Unknown: false, + BeforeSensitive: false, + AfterSensitive: false, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }) + + action := diff.Action + + jsonOpts := opts.Clone() + jsonOpts.OverrideNullSuffix = true + + var whitespace, replace string + if jsonDiff.Action == plans.NoOp && diff.Action == plans.Update { + // Then this means we are rendering a whitespace only change. The JSON + // differ will have ignored the whitespace changes so that makes the + // diff we are about to print out very confusing without extra + // explanation. + if diff.Replace { + whitespace = " # whitespace changes force replacement" + } else { + whitespace = " # whitespace changes" + } + + // Because we'd be showing no changes otherwise: + jsonOpts.ShowUnchangedChildren = true + + // Whitespace changes should not appear as if edited. + action = plans.NoOp + } else { + // We only show the replace suffix if we didn't print something out + // about whitespace changes. + replace = forcesReplacement(diff.Replace, opts) + } + + renderedJsonDiff := jsonDiff.RenderHuman(indent+1, jsonOpts) + + if diff.Action == plans.Create || diff.Action == plans.Delete { + // We don't display the '+' or '-' symbols on the JSON diffs, we should + // still display the '~' for an update action though. + action = plans.NoOp + } + + if strings.Contains(renderedJsonDiff, "\n") { + return fmt.Sprintf("jsonencode(%s\n%s%s%s%s\n%s%s)%s", whitespace, formatIndent(indent+1), writeDiffActionSymbol(action, opts), renderedJsonDiff, replace, formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts)) + } + return fmt.Sprintf("jsonencode(%s)%s%s", renderedJsonDiff, whitespace, replace) +} diff --git a/internal/command/jsonformat/computed/renderers/renderer_test.go b/internal/command/jsonformat/computed/renderers/renderer_test.go new file mode 100644 index 0000000000..ab699572f5 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/renderer_test.go @@ -0,0 +1,2246 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "encoding/json" + "strings" + "testing" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + + "github.com/google/go-cmp/cmp" + "github.com/mitchellh/colorstring" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/plans" +) + +func TestRenderers_Human(t *testing.T) { + colorize := colorstring.Colorize{ + Colors: colorstring.DefaultColors, + Disable: true, + } + + tcs := map[string]struct { + diff computed.Diff + expected string + opts computed.RenderHumanOpts + }{ + // We're using the string "null" in these tests to demonstrate the + // difference between rendering an actual string and rendering a null + // value. + "primitive_create_string": { + diff: computed.Diff{ + Renderer: Primitive(nil, "null", cty.String), + Action: plans.Create, + }, + expected: "\"null\"", + }, + "primitive_delete_string": { + diff: computed.Diff{ + Renderer: Primitive("null", nil, cty.String), + Action: plans.Delete, + }, + expected: "\"null\" -> null", + }, + "primitive_update_string_to_null": { + diff: computed.Diff{ + Renderer: Primitive("null", nil, cty.String), + Action: plans.Update, + }, + expected: "\"null\" -> null", + }, + "primitive_update_string_from_null": { + diff: computed.Diff{ + Renderer: Primitive(nil, "null", cty.String), + Action: plans.Update, + }, + expected: "null -> \"null\"", + }, + "primitive_update_multiline_string_to_null": { + diff: computed.Diff{ + Renderer: Primitive("nu\nll", nil, cty.String), + Action: plans.Update, + }, + expected: ` +<<-EOT + - nu + - ll + + null + EOT +`, + }, + "primitive_update_multiline_string_from_null": { + diff: computed.Diff{ + Renderer: Primitive(nil, "nu\nll", cty.String), + Action: plans.Update, + }, + expected: ` +<<-EOT + - null + + nu + + ll + EOT +`, + }, + "primitive_update_json_string_to_null": { + diff: computed.Diff{ + Renderer: Primitive("[null]", nil, cty.String), + Action: plans.Update, + }, + expected: ` +jsonencode( + [ + - null, + ] + ) -> null +`, + }, + "primitive_update_json_string_from_null": { + diff: computed.Diff{ + Renderer: Primitive(nil, "[null]", cty.String), + Action: plans.Update, + }, + expected: ` +null -> jsonencode( + [ + + null, + ] + ) +`, + }, + "primitive_create_fake_json": { + diff: computed.Diff{ + Renderer: Primitive(nil, "[\"hello\"] and some more", cty.String), + Action: plans.Create, + }, + expected: `"[\"hello\"] and some more"`, + }, + "primitive_create_null_string": { + diff: computed.Diff{ + Renderer: Primitive(nil, nil, cty.String), + Action: plans.Create, + }, + expected: "null", + }, + "primitive_delete_null_string": { + diff: computed.Diff{ + Renderer: Primitive(nil, nil, cty.String), + Action: plans.Delete, + }, + expected: "null", + }, + "primitive_create": { + diff: computed.Diff{ + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + expected: "1", + }, + "primitive_delete": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + expected: "1 -> null", + }, + "primitive_delete_override": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + opts: computed.RenderHumanOpts{OverrideNullSuffix: true}, + expected: "1", + }, + "primitive_update_to_null": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Update, + }, + expected: "1 -> null", + }, + "primitive_update_from_null": { + diff: computed.Diff{ + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Update, + }, + expected: "null -> 1", + }, + "primitive_update": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + expected: "0 -> 1", + }, + "primitive_update_long_float": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("123456789"), json.Number("987654321"), cty.Number), + Action: plans.Update, + }, + expected: "123456789 -> 987654321", + }, + "primitive_update_long_float_with_decimals": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("1.23456789"), json.Number("9.87654321"), cty.Number), + Action: plans.Update, + }, + expected: "1.23456789 -> 9.87654321", + }, + "primitive_create_very_long_float": { + diff: computed.Diff{ + Renderer: Primitive(nil, json.Number("9223372036854773256"), cty.Number), + Action: plans.Create, + }, + expected: "9223372036854773256", + }, + "primitive_update_replace": { + diff: computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + Replace: true, + }, + expected: "0 -> 1 # forces replacement", + }, + "primitive_multiline_string_create": { + diff: computed.Diff{ + Renderer: Primitive(nil, "hello\nworld", cty.String), + Action: plans.Create, + }, + expected: ` +<<-EOT + hello + world + EOT +`, + }, + "primitive_multiline_string_delete": { + diff: computed.Diff{ + Renderer: Primitive("hello\nworld", nil, cty.String), + Action: plans.Delete, + }, + expected: ` +<<-EOT + hello + world + EOT -> null +`, + }, + "primitive_multiline_string_update": { + diff: computed.Diff{ + Renderer: Primitive("hello\nold\nworld", "hello\nnew\nworld", cty.String), + Action: plans.Update, + }, + expected: ` +<<-EOT + hello + - old + + new + world + EOT +`, + }, + "primitive_json_string_create": { + diff: computed.Diff{ + Renderer: Primitive(nil, "{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", cty.String), + Action: plans.Create, + }, + expected: ` +jsonencode( + { + + key_one = "value_one" + + key_two = "value_two" + } + ) +`, + }, + "primitive_json_string_delete": { + diff: computed.Diff{ + Renderer: Primitive("{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", nil, cty.String), + Action: plans.Delete, + }, + expected: ` +jsonencode( + { + - key_one = "value_one" + - key_two = "value_two" + } + ) -> null +`, + }, + "primitive_json_string_update": { + diff: computed.Diff{ + Renderer: Primitive("{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", "{\"key_one\": \"value_one\",\"key_two\":\"value_two\",\"key_three\":\"value_three\"}", cty.String), + Action: plans.Update, + }, + expected: ` +jsonencode( + ~ { + + key_three = "value_three" + # (2 unchanged attributes hidden) + } + ) +`, + }, + "primitive_json_explicit_nulls": { + diff: computed.Diff{ + Renderer: Primitive("{\"key_one\":\"value_one\",\"key_two\":\"value_two\"}", "{\"key_one\":null}", cty.String), + Action: plans.Update, + }, + expected: ` +jsonencode( + ~ { + ~ key_one = "value_one" -> null + - key_two = "value_two" + } + ) +`, + }, + "primitive_fake_json_string_update": { + diff: computed.Diff{ + // This isn't valid JSON, our renderer should be okay with it. + Renderer: Primitive("{\"key_one\": \"value_one\",\"key_two\":\"value_two\"", "{\"key_one\": \"value_one\",\"key_two\":\"value_two\",\"key_three\":\"value_three\"", cty.String), + Action: plans.Update, + }, + expected: "\"{\\\"key_one\\\": \\\"value_one\\\",\\\"key_two\\\":\\\"value_two\\\"\" -> \"{\\\"key_one\\\": \\\"value_one\\\",\\\"key_two\\\":\\\"value_two\\\",\\\"key_three\\\":\\\"value_three\\\"\"", + }, + "primitive_multiline_to_json_update": { + diff: computed.Diff{ + Renderer: Primitive("hello\nworld", "{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", cty.String), + Action: plans.Update, + }, + expected: ` +<<-EOT + hello + world + EOT -> jsonencode( + { + + key_one = "value_one" + + key_two = "value_two" + } + ) +`, + }, + "primitive_json_to_multiline_update": { + diff: computed.Diff{ + Renderer: Primitive("{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", "hello\nworld", cty.String), + Action: plans.Update, + }, + expected: ` +jsonencode( + { + - key_one = "value_one" + - key_two = "value_two" + } + ) -> <<-EOT + hello + world + EOT +`, + }, + "primitive_json_to_string_update": { + diff: computed.Diff{ + Renderer: Primitive("{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", "hello world", cty.String), + Action: plans.Update, + }, + expected: ` +jsonencode( + { + - key_one = "value_one" + - key_two = "value_two" + } + ) -> "hello world" +`, + }, + "primitive_string_to_json_update": { + diff: computed.Diff{ + Renderer: Primitive("hello world", "{\"key_one\": \"value_one\",\"key_two\":\"value_two\"}", cty.String), + Action: plans.Update, + }, + expected: ` +"hello world" -> jsonencode( + { + + key_one = "value_one" + + key_two = "value_two" + } + ) +`, + }, + "primitive_multi_to_single_update": { + diff: computed.Diff{ + Renderer: Primitive("hello\nworld", "hello world", cty.String), + Action: plans.Update, + }, + expected: ` +<<-EOT + - hello + - world + + hello world + EOT +`, + }, + "primitive_single_to_multi_update": { + diff: computed.Diff{ + Renderer: Primitive("hello world", "hello\nworld", cty.String), + Action: plans.Update, + }, + expected: ` +<<-EOT + - hello world + + hello + + world + EOT +`, + }, + "sensitive_update": { + diff: computed.Diff{ + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, true, true), + Action: plans.Update, + }, + expected: "(sensitive value)", + }, + "sensitive_update_replace": { + diff: computed.Diff{ + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + Replace: true, + }, true, true), + Action: plans.Update, + Replace: true, + }, + expected: "(sensitive value) # forces replacement", + }, + "computed_create": { + diff: computed.Diff{ + Renderer: Unknown(computed.Diff{}), + Action: plans.Create, + }, + expected: "(known after apply)", + }, + "computed_update": { + diff: computed.Diff{ + Renderer: Unknown(computed.Diff{ + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }), + Action: plans.Update, + }, + expected: "0 -> (known after apply)", + }, + "computed_update_from_null": { + diff: computed.Diff{ + Renderer: Unknown(computed.Diff{}), + Action: plans.Update, + }, + expected: "(known after apply)", + }, + "computed_create_forces_replacement": { + diff: computed.Diff{ + Renderer: Unknown(computed.Diff{}), + Action: plans.Create, + Replace: true, + }, + expected: "(known after apply) # forces replacement", + }, + "computed_update_forces_replacement": { + diff: computed.Diff{ + Renderer: Unknown(computed.Diff{ + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }), + Action: plans.Update, + Replace: true, + }, + expected: "0 -> (known after apply) # forces replacement", + }, + "object_created": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{}), + Action: plans.Create, + }, + expected: "{}", + }, + "object_created_with_attributes": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(nil, json.Number("0"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Create, + }, + expected: ` +{ + + attribute_one = 0 + } +`, + }, + "object_deleted": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{}), + Action: plans.Delete, + }, + expected: "{} -> null", + }, + "object_deleted_with_attributes": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Delete, + }, + expected: ` +{ + - attribute_one = 0 + } -> null +`, + }, + "nested_object_deleted": { + diff: computed.Diff{ + Renderer: NestedObject(map[string]computed.Diff{}), + Action: plans.Delete, + }, + expected: "{} -> null", + }, + "nested_object_deleted_with_attributes": { + diff: computed.Diff{ + Renderer: NestedObject(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Delete, + }, + expected: ` +{ + - attribute_one = 0 -> null + } -> null +`, + }, + "object_create_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(nil, json.Number("0"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + attribute_one = 0 + } +`, + }, + "object_update_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ attribute_one = 0 -> 1 + } +`, + }, + "object_update_attribute_forces_replacement": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + Replace: true, + }, + expected: ` +{ # forces replacement + ~ attribute_one = 0 -> 1 + } +`, + }, + "object_delete_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + - attribute_one = 0 + } +`, + }, + "object_ignore_unchanged_attributes": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + "attribute_two": { + Renderer: Primitive(json.Number("0"), json.Number("0"), cty.Number), + Action: plans.NoOp, + }, + "attribute_three": { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ attribute_one = 0 -> 1 + + attribute_three = 1 + # (1 unchanged attribute hidden) + } +`, + }, + "object_create_sensitive_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, false, true), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + attribute_one = (sensitive value) + } +`, + }, + "object_update_sensitive_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, true, true), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ attribute_one = (sensitive value) + } +`, + }, + "object_delete_sensitive_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, true, false), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + - attribute_one = (sensitive value) + } +`, + }, + "object_create_computed_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Unknown(computed.Diff{Renderer: nil}), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + attribute_one = (known after apply) + } +`, + }, + "object_update_computed_attribute": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Unknown(computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ attribute_one = 1 -> (known after apply) + } +`, + }, + "object_escapes_attribute_keys": { + diff: computed.Diff{ + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "attribute:two": { + Renderer: Primitive(json.Number("2"), json.Number("3"), cty.Number), + Action: plans.Update, + }, + "attribute_six": { + Renderer: Primitive(json.Number("3"), json.Number("4"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "attribute:two" = 2 -> 3 + ~ attribute_one = 1 -> 2 + ~ attribute_six = 3 -> 4 + } +`, + }, + "map_create_empty": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{}), + Action: plans.Create, + }, + expected: "{}", + }, + "map_create": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive(nil, "new", cty.String), + Action: plans.Create, + }, + }), + Action: plans.Create, + }, + expected: ` +{ + + "element_one" = "new" + } +`, + }, + "map_delete_empty": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{}), + Action: plans.Delete, + }, + expected: "{} -> null", + }, + "map_delete": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive("old", nil, cty.String), + Action: plans.Delete, + }, + }), + Action: plans.Delete, + }, + expected: ` +{ + - "element_one" = "old" + } -> null +`, + }, + "map_create_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive(nil, "new", cty.String), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + "element_one" = "new" + } +`, + }, + "map_update_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive("old", "new", cty.String), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "element_one" = "old" -> "new" + } +`, + }, + "map_delete_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive("old", nil, cty.String), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + - "element_one" = "old" -> null + } +`, + }, + "map_update_forces_replacement": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive("old", "new", cty.String), + Action: plans.Update, + }, + }), + Action: plans.Update, + Replace: true, + }, + expected: ` +{ # forces replacement + ~ "element_one" = "old" -> "new" + } +`, + }, + "map_ignore_unchanged_elements": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive(nil, "new", cty.String), + Action: plans.Create, + }, + "element_two": { + Renderer: Primitive("old", "old", cty.String), + Action: plans.NoOp, + }, + "element_three": { + Renderer: Primitive("old", "new", cty.String), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + "element_one" = "new" + ~ "element_three" = "old" -> "new" + # (1 unchanged element hidden) + } +`, + }, + "map_create_sensitive_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, false, true), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + "element_one" = (sensitive value) + } +`, + }, + "map_update_sensitive_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, true, true), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "element_one" = (sensitive value) + } +`, + }, + "map_update_sensitive_element_status": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("0"), cty.Number), + Action: plans.NoOp, + }, true, false), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + # Warning: this attribute value will no longer be marked as sensitive + # after applying this change. The value is unchanged. + ~ "element_one" = (sensitive value) + } +`, + }, + "map_delete_sensitive_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, true, false), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + - "element_one" = (sensitive value) -> null + } +`, + }, + "map_create_computed_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Unknown(computed.Diff{}), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + "element_one" = (known after apply) + } +`, + }, + "map_update_computed_element": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Unknown(computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "element_one" = 1 -> (known after apply) + } +`, + }, + "map_aligns_key": { + diff: computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "element_two": { + Renderer: Primitive(json.Number("1"), json.Number("3"), cty.Number), + Action: plans.Update, + }, + "element_three": { + Renderer: Primitive(json.Number("1"), json.Number("4"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "element_one" = 1 -> 2 + ~ "element_three" = 1 -> 4 + ~ "element_two" = 1 -> 3 + } +`, + }, + "nested_map_does_not_align_keys": { + diff: computed.Diff{ + Renderer: NestedMap(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "element_two": { + Renderer: Primitive(json.Number("1"), json.Number("3"), cty.Number), + Action: plans.Update, + }, + "element_three": { + Renderer: Primitive(json.Number("1"), json.Number("4"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "element_one" = 1 -> 2 + ~ "element_three" = 1 -> 4 + ~ "element_two" = 1 -> 3 + } +`, + }, + "list_create_empty": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{}), + Action: plans.Create, + }, + expected: "[]", + }, + "list_create": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Create, + }, + expected: ` +[ + + 1, + ] +`, + }, + "list_delete_empty": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{}), + Action: plans.Delete, + }, + expected: "[] -> null", + }, + "list_delete": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Delete, + }, + expected: ` +[ + - 1, + ] -> null +`, + }, + "list_create_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + + 1, + ] +`, + }, + "list_update_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ 0 -> 1, + ] +`, + }, + "list_replace_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + - 0, + + 1, + ] +`, + }, + "list_delete_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + - 0, + ] +`, + }, + "list_update_forces_replacement": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + Replace: true, + }, + expected: ` +[ # forces replacement + ~ 0 -> 1, + ] +`, + }, + "list_update_ignores_unchanged": { + diff: computed.Diff{ + Renderer: NestedList([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("0"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("1"), json.Number("1"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("2"), json.Number("5"), cty.Number), + Action: plans.Update, + }, + { + Renderer: Primitive(json.Number("3"), json.Number("3"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("4"), json.Number("4"), cty.Number), + Action: plans.NoOp, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ 2 -> 5, + # (4 unchanged elements hidden) + ] +`, + }, + "list_update_ignored_unchanged_with_context": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("0"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("1"), json.Number("1"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("2"), json.Number("5"), cty.Number), + Action: plans.Update, + }, + { + Renderer: Primitive(json.Number("3"), json.Number("3"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("4"), json.Number("4"), cty.Number), + Action: plans.NoOp, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + # (1 unchanged element hidden) + 1, + ~ 2 -> 5, + 3, + # (1 unchanged element hidden) + ] +`, + }, + "list_create_sensitive_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, false, true), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + + (sensitive value), + ] +`, + }, + "list_delete_sensitive_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, true, false), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + - (sensitive value), + ] +`, + }, + "list_update_sensitive_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, true, true), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ (sensitive value), + ] +`, + }, + "list_update_sensitive_element_status": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("1"), json.Number("1"), cty.Number), + Action: plans.NoOp, + }, false, true), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + # Warning: this attribute value will be marked as sensitive and will not + # display in UI output after applying this change. The value is unchanged. + ~ (sensitive value), + ] +`, + }, + "list_create_computed_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Unknown(computed.Diff{}), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + + (known after apply), + ] +`, + }, + "list_update_computed_element": { + diff: computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Unknown(computed.Diff{ + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ 0 -> (known after apply), + ] +`, + }, + "set_create_empty": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{}), + Action: plans.Create, + }, + expected: "[]", + }, + "set_create": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Create, + }, + expected: ` +[ + + 1, + ] +`, + }, + "set_delete_empty": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{}), + Action: plans.Delete, + }, + expected: "[] -> null", + }, + "set_delete": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Delete, + }, + expected: ` +[ + - 1, + ] -> null +`, + }, + "set_create_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + + 1, + ] +`, + }, + "set_update_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ 0 -> 1, + ] +`, + }, + "set_replace_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + - 0, + + 1, + ] +`, + }, + "set_delete_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + - 0, + ] +`, + }, + "set_update_forces_replacement": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + Replace: true, + }, + expected: ` +[ # forces replacement + ~ 0 -> 1, + ] +`, + }, + "nested_set_update_forces_replacement": { + diff: computed.Diff{ + Renderer: NestedSet([]computed.Diff{ + { + Renderer: Object(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + }), + Action: plans.Update, + Replace: true, + }, + expected: ` +[ + ~ { # forces replacement + ~ attribute_one = 0 -> 1 + }, + ] +`, + }, + "set_update_ignores_unchanged": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Primitive(json.Number("0"), json.Number("0"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("1"), json.Number("1"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("2"), json.Number("5"), cty.Number), + Action: plans.Update, + }, + { + Renderer: Primitive(json.Number("3"), json.Number("3"), cty.Number), + Action: plans.NoOp, + }, + { + Renderer: Primitive(json.Number("4"), json.Number("4"), cty.Number), + Action: plans.NoOp, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ 2 -> 5, + # (4 unchanged elements hidden) + ] +`, + }, + "set_create_sensitive_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, false, true), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + + (sensitive value), + ] +`, + }, + "set_delete_sensitive_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, false, true), + Action: plans.Delete, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + - (sensitive value), + ] +`, + }, + "set_update_sensitive_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("0"), json.Number("1"), cty.Number), + Action: plans.Update, + }, true, true), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ (sensitive value), + ] +`, + }, + "set_update_sensitive_element_status": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Sensitive(computed.Diff{ + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, false, true), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + # Warning: this attribute value will be marked as sensitive and will not + # display in UI output after applying this change. + ~ (sensitive value), + ] +`, + }, + "set_create_computed_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Unknown(computed.Diff{}), + Action: plans.Create, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + + (known after apply), + ] +`, + }, + "set_update_computed_element": { + diff: computed.Diff{ + Renderer: Set([]computed.Diff{ + { + Renderer: Unknown(computed.Diff{ + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }), + Action: plans.Update, + }, + }), + Action: plans.Update, + }, + expected: ` +[ + ~ 0 -> (known after apply), + ] +`, + }, + "create_empty_block": { + diff: computed.Diff{ + Renderer: Block(nil, Blocks{}), + Action: plans.Create, + }, + expected: "{}", + }, + "create_populated_block": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "root", cty.String), + Action: plans.Create, + }, + "boolean": { + Renderer: Primitive(nil, true, cty.Bool), + Action: plans.Create, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "one", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Create, + }, + "nested_block_two": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "two", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Create, + }, + }, + }), + Action: plans.Create, + }, + expected: ` +{ + + boolean = true + + string = "root" + + + nested_block { + + string = "one" + } + + + nested_block_two { + + string = "two" + } + }`, + }, + "update_empty_block": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "root", cty.String), + Action: plans.Create, + }, + "boolean": { + Renderer: Primitive(nil, true, cty.Bool), + Action: plans.Create, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block": { + + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "one", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Create, + }, + "nested_block_two": { + + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "two", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Create, + }, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + + boolean = true + + string = "root" + + + nested_block { + + string = "one" + } + + + nested_block_two { + + string = "two" + } + }`, + }, + "update_populated_block": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "root", cty.String), + Action: plans.Create, + }, + "boolean": { + Renderer: Primitive(false, true, cty.Bool), + Action: plans.Update, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "one", cty.String), + Action: plans.NoOp, + }, + }, Blocks{}), + Action: plans.NoOp, + }, + "nested_block_two": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive(nil, "two", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Create, + }, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ boolean = false -> true + + string = "root" + + + nested_block_two { + + string = "two" + } + + # (1 unchanged block hidden) + }`, + }, + "clear_populated_block": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("root", nil, cty.String), + Action: plans.Delete, + }, + "boolean": { + Renderer: Primitive(true, nil, cty.Bool), + Action: plans.Delete, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("one", nil, cty.String), + Action: plans.Delete, + }, + }, Blocks{}), + Action: plans.Delete, + }, + "nested_block_two": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("two", nil, cty.String), + Action: plans.Delete, + }, + }, Blocks{}), + Action: plans.Delete, + }, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + - boolean = true -> null + - string = "root" -> null + + - nested_block { + - string = "one" -> null + } + + - nested_block_two { + - string = "two" -> null + } + }`, + }, + "delete_populated_block": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("root", nil, cty.String), + Action: plans.Delete, + }, + "boolean": { + Renderer: Primitive(true, nil, cty.Bool), + Action: plans.Delete, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("one", nil, cty.String), + Action: plans.Delete, + }, + }, Blocks{}), + Action: plans.Delete, + }, + "nested_block_two": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("two", nil, cty.String), + Action: plans.Delete, + }, + }, Blocks{}), + Action: plans.Delete, + }, + }, + }), + Action: plans.Delete, + }, + expected: ` +{ + - boolean = true -> null + - string = "root" -> null + + - nested_block { + - string = "one" -> null + } + + - nested_block_two { + - string = "two" -> null + } + }`, + }, + "list_block_update": { + diff: computed.Diff{ + Renderer: Block( + nil, + Blocks{ + ListBlocks: map[string][]computed.Diff{ + "list_blocks": { + { + Renderer: Block(map[string]computed.Diff{ + "number": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "string": { + Renderer: Primitive(nil, "new", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Update, + }, + { + Renderer: Block(map[string]computed.Diff{ + "number": { + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + "string": { + Renderer: Primitive("old", "new", cty.String), + Action: plans.Update, + }, + }, Blocks{}), + Action: plans.Update, + }, + }, + }, + }), + }, + expected: ` +{ + ~ list_blocks { + ~ number = 1 -> 2 + + string = "new" + } + ~ list_blocks { + - number = 1 -> null + ~ string = "old" -> "new" + } + }`, + }, + "set_block_update": { + diff: computed.Diff{ + Renderer: Block( + nil, + Blocks{ + SetBlocks: map[string][]computed.Diff{ + "set_blocks": { + { + Renderer: Block(map[string]computed.Diff{ + "number": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "string": { + Renderer: Primitive(nil, "new", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Update, + }, + { + Renderer: Block(map[string]computed.Diff{ + "number": { + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + "string": { + Renderer: Primitive("old", "new", cty.String), + Action: plans.Update, + }, + }, Blocks{}), + Action: plans.Update, + }, + }, + }, + }), + }, + expected: ` +{ + ~ set_blocks { + ~ number = 1 -> 2 + + string = "new" + } + ~ set_blocks { + - number = 1 -> null + ~ string = "old" -> "new" + } + }`, + }, + "map_block_update": { + diff: computed.Diff{ + Renderer: Block( + nil, + Blocks{ + MapBlocks: map[string]map[string]computed.Diff{ + "list_blocks": { + "key_one": { + Renderer: Block(map[string]computed.Diff{ + "number": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "string": { + Renderer: Primitive(nil, "new", cty.String), + Action: plans.Create, + }, + }, Blocks{}), + Action: plans.Update, + }, + "key:two": { + Renderer: Block(map[string]computed.Diff{ + "number": { + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + "string": { + Renderer: Primitive("old", "new", cty.String), + Action: plans.Update, + }, + }, Blocks{}), + Action: plans.Update, + }, + }, + }, + }), + }, + expected: ` +{ + ~ list_blocks "key:two" { + - number = 1 -> null + ~ string = "old" -> "new" + } + ~ list_blocks "key_one" { + ~ number = 1 -> 2 + + string = "new" + } + } +`, + }, + "sensitive_block": { + diff: computed.Diff{ + Renderer: SensitiveBlock(computed.Diff{ + Renderer: Block(nil, Blocks{}), + Action: plans.NoOp, + }, true, true), + Action: plans.Update, + }, + expected: ` +{ + # At least one attribute in this block is (or was) sensitive, + # so its contents will not be displayed. + } +`, + }, + "delete_empty_block": { + diff: computed.Diff{ + Renderer: Block(nil, Blocks{}), + Action: plans.Delete, + }, + expected: "{}", + }, + "block_escapes_keys": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "attribute_one": { + Renderer: Primitive(json.Number("1"), json.Number("2"), cty.Number), + Action: plans.Update, + }, + "attribute:two": { + Renderer: Primitive(json.Number("2"), json.Number("3"), cty.Number), + Action: plans.Update, + }, + "attribute_six": { + Renderer: Primitive(json.Number("3"), json.Number("4"), cty.Number), + Action: plans.Update, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block:one": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("one", "four", cty.String), + Action: plans.Update, + }, + }, Blocks{}), + Action: plans.Update, + }, + "nested_block_two": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("two", "three", cty.String), + Action: plans.Update, + }, + }, Blocks{}), + Action: plans.Update, + }, + }, + }), + Action: plans.Update, + }, + expected: ` +{ + ~ "attribute:two" = 2 -> 3 + ~ attribute_one = 1 -> 2 + ~ attribute_six = 3 -> 4 + + ~ "nested_block:one" { + ~ string = "one" -> "four" + } + + ~ nested_block_two { + ~ string = "two" -> "three" + } + }`, + }, + "block_always_includes_important_attributes": { + diff: computed.Diff{ + Renderer: Block(map[string]computed.Diff{ + "id": { + Renderer: Primitive("root", "root", cty.String), + Action: plans.NoOp, + }, + "boolean": { + Renderer: Primitive(false, false, cty.Bool), + Action: plans.NoOp, + }, + }, Blocks{ + SingleBlocks: map[string]computed.Diff{ + "nested_block": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("one", "one", cty.String), + Action: plans.NoOp, + }, + }, Blocks{}), + Action: plans.NoOp, + }, + "nested_block_two": { + Renderer: Block(map[string]computed.Diff{ + "string": { + Renderer: Primitive("two", "two", cty.String), + Action: plans.NoOp, + }, + }, Blocks{}), + Action: plans.NoOp, + }, + }, + }), + Action: plans.NoOp, + }, + expected: ` +{ + id = "root" + # (1 unchanged attribute hidden) + + # (2 unchanged blocks hidden) + }`, + }, + "output_map_to_list": { + diff: computed.Diff{ + Renderer: TypeChange(computed.Diff{ + Renderer: Map(map[string]computed.Diff{ + "element_one": { + Renderer: Primitive(json.Number("0"), nil, cty.Number), + Action: plans.Delete, + }, + "element_two": { + Renderer: Primitive(json.Number("1"), nil, cty.Number), + Action: plans.Delete, + }, + }), + Action: plans.Delete, + }, computed.Diff{ + Renderer: List([]computed.Diff{ + { + Renderer: Primitive(nil, json.Number("0"), cty.Number), + Action: plans.Create, + }, + { + Renderer: Primitive(nil, json.Number("1"), cty.Number), + Action: plans.Create, + }, + }), + Action: plans.Create, + }), + }, + expected: ` +{ + - "element_one" = 0 + - "element_two" = 1 + } -> [ + + 0, + + 1, + ] +`, + }, + "json_string_no_symbols": { + diff: computed.Diff{ + Renderer: Primitive("{\"key\":\"value\"}", "{\"key\":\"value\"}", cty.String), + Action: plans.NoOp, + }, + opts: computed.RenderHumanOpts{ + HideDiffActionSymbols: true, + ShowUnchangedChildren: true, + }, + expected: ` +jsonencode( + { + key = "value" + } +) +`, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + opts := tc.opts.Clone() + opts.Colorize = &colorize + + expected := strings.TrimSpace(tc.expected) + actual := tc.diff.RenderHuman(0, opts) + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Fatalf("\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", expected, actual, diff) + } + }) + } +} diff --git a/internal/command/jsonformat/computed/renderers/sensitive.go b/internal/command/jsonformat/computed/renderers/sensitive.go new file mode 100644 index 0000000000..4799bad08c --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/sensitive.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*sensitiveRenderer)(nil) + +func Sensitive(change computed.Diff, beforeSensitive, afterSensitive bool) computed.DiffRenderer { + return &sensitiveRenderer{ + inner: change, + beforeSensitive: beforeSensitive, + afterSensitive: afterSensitive, + } +} + +type sensitiveRenderer struct { + inner computed.Diff + + beforeSensitive bool + afterSensitive bool +} + +func (renderer sensitiveRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + return fmt.Sprintf("(sensitive value)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) +} + +func (renderer sensitiveRenderer) WarningsHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) []string { + if (renderer.beforeSensitive == renderer.afterSensitive) || renderer.inner.Action == plans.Create || renderer.inner.Action == plans.Delete { + // Only display warnings for sensitive values if they are changing from + // being sensitive or to being sensitive and if they are not being + // destroyed or created. + return []string{} + } + + var warning string + if renderer.beforeSensitive { + warning = opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this attribute value will no longer be marked as sensitive\n%s # after applying this change.", formatIndent(indent))) + } else { + warning = opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this attribute value will be marked as sensitive and will not\n%s # display in UI output after applying this change.", formatIndent(indent))) + } + + if renderer.inner.Action == plans.NoOp { + return []string{fmt.Sprintf("%s The value is unchanged.", warning)} + } + return []string{warning} +} diff --git a/internal/command/jsonformat/computed/renderers/sensitive_block.go b/internal/command/jsonformat/computed/renderers/sensitive_block.go new file mode 100644 index 0000000000..1484f6b80a --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/sensitive_block.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +func SensitiveBlock(diff computed.Diff, beforeSensitive, afterSensitive bool) computed.DiffRenderer { + return &sensitiveBlockRenderer{ + inner: diff, + beforeSensitive: beforeSensitive, + afterSensitive: afterSensitive, + } +} + +type sensitiveBlockRenderer struct { + inner computed.Diff + + afterSensitive bool + beforeSensitive bool +} + +func (renderer sensitiveBlockRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + cachedLinePrefix := fmt.Sprintf("%s%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts)) + return fmt.Sprintf("{%s\n%s # At least one attribute in this block is (or was) sensitive,\n%s # so its contents will not be displayed.\n%s}", + forcesReplacement(diff.Replace, opts), cachedLinePrefix, cachedLinePrefix, cachedLinePrefix) +} + +func (renderer sensitiveBlockRenderer) WarningsHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) []string { + if (renderer.beforeSensitive == renderer.afterSensitive) || renderer.inner.Action == plans.Create || renderer.inner.Action == plans.Delete { + // Only display warnings for sensitive values if they are changing from + // being sensitive or to being sensitive and if they are not being + // destroyed or created. + return []string{} + } + + if renderer.beforeSensitive { + return []string{opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will no longer be marked as sensitive\n%s # after applying this change.", formatIndent(indent)))} + } else { + return []string{opts.Colorize.Color(fmt.Sprintf(" # [yellow]Warning[reset]: this block will be marked as sensitive and will not\n%s # display in UI output after applying this change.", formatIndent(indent)))} + } +} diff --git a/internal/command/jsonformat/computed/renderers/set.go b/internal/command/jsonformat/computed/renderers/set.go new file mode 100644 index 0000000000..d34dcb18f7 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/set.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "bytes" + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*setRenderer)(nil) + +func Set(elements []computed.Diff) computed.DiffRenderer { + return &setRenderer{ + elements: elements, + } +} + +func NestedSet(elements []computed.Diff) computed.DiffRenderer { + return &setRenderer{ + elements: elements, + overrideForcesReplacement: true, + } +} + +type setRenderer struct { + NoWarningsRenderer + + elements []computed.Diff + + overrideForcesReplacement bool +} + +func (renderer setRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + // Sets are a bit finicky, nested sets don't render the forces replacement + // suffix themselves, but push it onto their children. So if we are + // overriding the forces replacement setting, we set it to true for children + // and false for ourselves. + displayForcesReplacementInSelf := diff.Replace && !renderer.overrideForcesReplacement + displayForcesReplacementInChildren := diff.Replace && renderer.overrideForcesReplacement + + if len(renderer.elements) == 0 { + return fmt.Sprintf("[]%s%s", nullSuffix(diff.Action, opts), forcesReplacement(displayForcesReplacementInSelf, opts)) + } + + elementOpts := opts.Clone() + elementOpts.OverrideNullSuffix = true + elementOpts.ForceForcesReplacement = displayForcesReplacementInChildren + + unchangedElements := 0 + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("[%s\n", forcesReplacement(displayForcesReplacementInSelf, opts))) + for _, element := range renderer.elements { + if element.Action == plans.NoOp && !opts.ShowUnchangedChildren { + unchangedElements++ + continue + } + + for _, warning := range element.WarningsHuman(indent+1, opts) { + buf.WriteString(fmt.Sprintf("%s%s\n", formatIndent(indent+1), warning)) + } + buf.WriteString(fmt.Sprintf("%s%s%s,\n", formatIndent(indent+1), writeDiffActionSymbol(element.Action, elementOpts), element.RenderHuman(indent+1, elementOpts))) + } + + if unchangedElements > 0 { + buf.WriteString(fmt.Sprintf("%s%s%s\n", formatIndent(indent+1), writeDiffActionSymbol(plans.NoOp, opts), unchanged("element", unchangedElements, opts))) + } + + buf.WriteString(fmt.Sprintf("%s%s]%s", formatIndent(indent), writeDiffActionSymbol(plans.NoOp, opts), nullSuffix(diff.Action, opts))) + return buf.String() +} diff --git a/internal/command/jsonformat/computed/renderers/string.go b/internal/command/jsonformat/computed/renderers/string.go new file mode 100644 index 0000000000..9d5ed899f8 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/string.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" +) + +type evaluatedString struct { + String string + Json interface{} + + IsMultiline bool + IsNull bool +} + +func evaluatePrimitiveString(value interface{}, opts computed.RenderHumanOpts) evaluatedString { + if value == nil { + return evaluatedString{ + String: opts.Colorize.Color("[dark_gray]null[reset]"), + IsNull: true, + } + } + + str := value.(string) + + if strings.HasPrefix(str, "{") || strings.HasPrefix(str, "[") { + jv, err := structured.ParseJson(strings.NewReader(str)) + if err == nil { + return evaluatedString{ + String: str, + Json: jv, + } + } + } + + if strings.Contains(str, "\n") { + return evaluatedString{ + String: strings.TrimSpace(str), + IsMultiline: true, + } + } + + return evaluatedString{ + String: str, + } +} + +func (e evaluatedString) RenderSimple() string { + if e.IsNull { + return e.String + } + return fmt.Sprintf("%q", e.String) +} diff --git a/internal/command/jsonformat/computed/renderers/testing.go b/internal/command/jsonformat/computed/renderers/testing.go new file mode 100644 index 0000000000..273dcea1fb --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/testing.go @@ -0,0 +1,321 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "sort" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +type ValidateDiffFunction func(t *testing.T, diff computed.Diff) + +func validateDiff(t *testing.T, diff computed.Diff, expectedAction plans.Action, expectedReplace bool) { + if diff.Replace != expectedReplace || diff.Action != expectedAction { + t.Errorf("\nreplace:\n\texpected:%t\n\tactual:%t\naction:\n\texpected:%s\n\tactual:%s", expectedReplace, diff.Replace, expectedAction, diff.Action) + } +} + +func ValidatePrimitive(before, after interface{}, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + primitive, ok := diff.Renderer.(*primitiveRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + beforeDiff := cmp.Diff(primitive.before, before) + afterDiff := cmp.Diff(primitive.after, after) + + if len(beforeDiff) > 0 || len(afterDiff) > 0 { + t.Errorf("before diff: (%s), after diff: (%s)", beforeDiff, afterDiff) + } + } +} + +func ValidateObject(attributes map[string]ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + object, ok := diff.Renderer.(*objectRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + if !object.overrideNullSuffix { + t.Errorf("created the wrong type of object renderer") + } + + validateMapType(t, object.attributes, attributes) + } +} + +func ValidateNestedObject(attributes map[string]ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + object, ok := diff.Renderer.(*objectRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + if object.overrideNullSuffix { + t.Errorf("created the wrong type of object renderer") + } + + validateMapType(t, object.attributes, attributes) + } +} + +func ValidateMap(elements map[string]ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + m, ok := diff.Renderer.(*mapRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + validateMapType(t, m.elements, elements) + } +} + +func validateMapType(t *testing.T, actual map[string]computed.Diff, expected map[string]ValidateDiffFunction) { + validateKeys(t, actual, expected) + + for key, expected := range expected { + if actual, ok := actual[key]; ok { + expected(t, actual) + } + } +} + +func validateKeys[C, V any](t *testing.T, actual map[string]C, expected map[string]V) { + if len(actual) != len(expected) { + + var actualAttributes []string + var expectedAttributes []string + + for key := range actual { + actualAttributes = append(actualAttributes, key) + } + for key := range expected { + expectedAttributes = append(expectedAttributes, key) + } + + sort.Strings(actualAttributes) + sort.Strings(expectedAttributes) + + if diff := cmp.Diff(actualAttributes, expectedAttributes); len(diff) > 0 { + t.Errorf("actual and expected attributes did not match: %s", diff) + } + } +} + +func ValidateList(elements []ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + list, ok := diff.Renderer.(*listRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + if !list.displayContext { + t.Errorf("created the wrong type of list renderer") + } + + validateSliceType(t, list.elements, elements) + } +} + +func ValidateNestedList(elements []ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + list, ok := diff.Renderer.(*listRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + if list.displayContext { + t.Errorf("created the wrong type of list renderer") + } + + validateSliceType(t, list.elements, elements) + } +} + +func ValidateSet(elements []ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + set, ok := diff.Renderer.(*setRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + validateSliceType(t, set.elements, elements) + } +} + +func validateSliceType(t *testing.T, actual []computed.Diff, expected []ValidateDiffFunction) { + if len(actual) != len(expected) { + t.Errorf("expected %d elements but found %d elements", len(expected), len(actual)) + return + } + + for ix := 0; ix < len(expected); ix++ { + expected[ix](t, actual[ix]) + } +} + +func ValidateBlock( + attributes map[string]ValidateDiffFunction, + singleBlocks map[string]ValidateDiffFunction, + listBlocks map[string][]ValidateDiffFunction, + mapBlocks map[string]map[string]ValidateDiffFunction, + setBlocks map[string][]ValidateDiffFunction, + action plans.Action, + replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + block, ok := diff.Renderer.(*blockRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + validateKeys(t, block.attributes, attributes) + validateKeys(t, block.blocks.SingleBlocks, singleBlocks) + validateKeys(t, block.blocks.ListBlocks, listBlocks) + validateKeys(t, block.blocks.MapBlocks, mapBlocks) + validateKeys(t, block.blocks.SetBlocks, setBlocks) + + for key, expected := range attributes { + if actual, ok := block.attributes[key]; ok { + expected(t, actual) + } + } + + for key, expected := range singleBlocks { + expected(t, block.blocks.SingleBlocks[key]) + } + + for key, expected := range listBlocks { + if actual, ok := block.blocks.ListBlocks[key]; ok { + if len(actual) != len(expected) { + t.Errorf("expected %d blocks within %s but found %d elements", len(expected), key, len(actual)) + } + for ix := range expected { + expected[ix](t, actual[ix]) + } + } + } + + for key, expected := range setBlocks { + if actual, ok := block.blocks.SetBlocks[key]; ok { + if len(actual) != len(expected) { + t.Errorf("expected %d blocks within %s but found %d elements", len(expected), key, len(actual)) + } + for ix := range expected { + expected[ix](t, actual[ix]) + } + } + } + + for key, expected := range setBlocks { + if actual, ok := block.blocks.SetBlocks[key]; ok { + if len(actual) != len(expected) { + t.Errorf("expected %d blocks within %s but found %d elements", len(expected), key, len(actual)) + } + for ix := range expected { + expected[ix](t, actual[ix]) + } + } + } + + for key, expected := range mapBlocks { + if actual, ok := block.blocks.MapBlocks[key]; ok { + if len(actual) != len(expected) { + t.Errorf("expected %d blocks within %s but found %d elements", len(expected), key, len(actual)) + } + for dKey := range expected { + expected[dKey](t, actual[dKey]) + } + } + } + } +} + +func ValidateTypeChange(before, after ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + typeChange, ok := diff.Renderer.(*typeChangeRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + before(t, typeChange.before) + after(t, typeChange.after) + } +} + +func ValidateSensitive(inner ValidateDiffFunction, beforeSensitive, afterSensitive bool, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + sensitive, ok := diff.Renderer.(*sensitiveRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + if beforeSensitive != sensitive.beforeSensitive || afterSensitive != sensitive.afterSensitive { + t.Errorf("before or after sensitive values don't match:\n\texpected; before: %t after: %t\n\tactual; before: %t, after: %t", beforeSensitive, afterSensitive, sensitive.beforeSensitive, sensitive.afterSensitive) + } + + inner(t, sensitive.inner) + } +} + +func ValidateUnknown(before ValidateDiffFunction, action plans.Action, replace bool) ValidateDiffFunction { + return func(t *testing.T, diff computed.Diff) { + validateDiff(t, diff, action, replace) + + unknown, ok := diff.Renderer.(*unknownRenderer) + if !ok { + t.Errorf("invalid renderer type: %T", diff.Renderer) + return + } + + if before == nil { + if unknown.before.Renderer != nil { + t.Errorf("did not expect a before renderer, but found one") + } + return + } + + if unknown.before.Renderer == nil { + t.Errorf("expected a before renderer, but found none") + } + + before(t, unknown.before) + } +} diff --git a/internal/command/jsonformat/computed/renderers/type_change.go b/internal/command/jsonformat/computed/renderers/type_change.go new file mode 100644 index 0000000000..0d014f19f3 --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/type_change.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" +) + +var _ computed.DiffRenderer = (*typeChangeRenderer)(nil) + +func TypeChange(before, after computed.Diff) computed.DiffRenderer { + return &typeChangeRenderer{ + before: before, + after: after, + } +} + +type typeChangeRenderer struct { + NoWarningsRenderer + + before computed.Diff + after computed.Diff +} + +func (renderer typeChangeRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + opts.OverrideNullSuffix = true // Never render null suffix for children of type changes. + return fmt.Sprintf("%s %s %s", renderer.before.RenderHuman(indent, opts), opts.Colorize.Color("[yellow]->[reset]"), renderer.after.RenderHuman(indent, opts)) +} diff --git a/internal/command/jsonformat/computed/renderers/unknown.go b/internal/command/jsonformat/computed/renderers/unknown.go new file mode 100644 index 0000000000..b50f058d5d --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/unknown.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + + "github.com/hashicorp/terraform/internal/plans" +) + +var _ computed.DiffRenderer = (*unknownRenderer)(nil) + +func Unknown(before computed.Diff) computed.DiffRenderer { + return &unknownRenderer{ + before: before, + } +} + +type unknownRenderer struct { + NoWarningsRenderer + + before computed.Diff +} + +func (renderer unknownRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + + // the before renderer can be nil and not a create action when the provider + // previously returned a null value for the computed attribute and is now + // declaring they will recompute it as part of the next update. + + if diff.Action == plans.Create || renderer.before.Renderer == nil { + return fmt.Sprintf("(known after apply)%s", forcesReplacement(diff.Replace, opts)) + } + + beforeOpts := opts.Clone() + // Never render null suffix for children of unknown changes. + beforeOpts.OverrideNullSuffix = true + if diff.Replace { + // If we're displaying forces replacement for the overall unknown + // change, then do not display it for the before specifically. + beforeOpts.ForbidForcesReplacement = true + } + return fmt.Sprintf("%s -> (known after apply)%s", renderer.before.RenderHuman(indent, beforeOpts), forcesReplacement(diff.Replace, opts)) +} diff --git a/internal/command/jsonformat/computed/renderers/util.go b/internal/command/jsonformat/computed/renderers/util.go new file mode 100644 index 0000000000..b1d29c884c --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/util.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + "strings" + + "github.com/hashicorp/terraform/internal/command/format" + + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +// NoWarningsRenderer defines a Warnings function that returns an empty list of +// warnings. This can be used by other renderers to ensure we don't see lots of +// repeats of this empty function. +type NoWarningsRenderer struct{} + +// WarningsHuman returns an empty slice, as the name NoWarningsRenderer suggests. +func (render NoWarningsRenderer) WarningsHuman(_ computed.Diff, _ int, _ computed.RenderHumanOpts) []string { + return nil +} + +// nullSuffix returns the `-> null` suffix if the change is a delete action, and +// it has not been overridden. +func nullSuffix(action plans.Action, opts computed.RenderHumanOpts) string { + if !opts.OverrideNullSuffix && action == plans.Delete { + return opts.Colorize.Color(" [dark_gray]-> null[reset]") + } + return "" +} + +// forcesReplacement returns the `# forces replacement` suffix if this change is +// driving the entire resource to be replaced. +func forcesReplacement(replace bool, opts computed.RenderHumanOpts) string { + if (replace || opts.ForceForcesReplacement) && !opts.ForbidForcesReplacement { + return opts.Colorize.Color(" [red]# forces replacement[reset]") + } + return "" +} + +// indent returns whitespace that is the required length for the specified +// indent. +func formatIndent(indent int) string { + return strings.Repeat(" ", indent) +} + +// unchanged prints out a description saying how many of 'keyword' have been +// hidden because they are unchanged or noop actions. +func unchanged(keyword string, count int, opts computed.RenderHumanOpts) string { + if count == 1 { + return opts.Colorize.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %s hidden)[reset]", count, keyword)) + } + return opts.Colorize.Color(fmt.Sprintf("[dark_gray]# (%d unchanged %ss hidden)[reset]", count, keyword)) +} + +// EnsureValidAttributeName checks if `name` contains any HCL syntax and calls +// and returns hclEscapeString. +func EnsureValidAttributeName(name string) string { + if !hclsyntax.ValidIdentifier(name) { + return hclEscapeString(name) + } + return name +} + +// hclEscapeString formats the input string into a format that is safe for +// rendering within HCL. +// +// Note, this function doesn't actually do a very good job of this currently. We +// need to expose some internal functions from HCL in a future version and call +// them from here. For now, just use "%q" formatting. +func hclEscapeString(str string) string { + // TODO: Replace this with more complete HCL logic instead of the simple + // go workaround. + return fmt.Sprintf("%q", str) +} + +// writeDiffActionSymbol writes out the symbols for the associated action, and +// handles localized colorization of the symbol as well as indenting the symbol +// to be 4 spaces wide. +// +// If the opts has HideDiffActionSymbols set then this function returns an empty +// string. +func writeDiffActionSymbol(action plans.Action, opts computed.RenderHumanOpts) string { + if opts.HideDiffActionSymbols { + return "" + } + return fmt.Sprintf("%s ", opts.Colorize.Color(format.DiffActionSymbol(action))) +} diff --git a/internal/command/jsonformat/computed/renderers/write_only.go b/internal/command/jsonformat/computed/renderers/write_only.go new file mode 100644 index 0000000000..e3f3cd95fd --- /dev/null +++ b/internal/command/jsonformat/computed/renderers/write_only.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package renderers + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" +) + +var _ computed.DiffRenderer = (*writeOnlyRenderer)(nil) + +func WriteOnly(sensitive bool) computed.DiffRenderer { + return &writeOnlyRenderer{ + sensitive, + } +} + +type writeOnlyRenderer struct { + sensitive bool +} + +func (renderer writeOnlyRenderer) RenderHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) string { + if renderer.sensitive { + return fmt.Sprintf("(sensitive, write-only attribute)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) + } + return fmt.Sprintf("(write-only attribute)%s%s", nullSuffix(diff.Action, opts), forcesReplacement(diff.Replace, opts)) +} + +func (renderer writeOnlyRenderer) WarningsHuman(diff computed.Diff, indent int, opts computed.RenderHumanOpts) []string { + return []string{} +} diff --git a/internal/command/jsonformat/diff.go b/internal/command/jsonformat/diff.go new file mode 100644 index 0000000000..402a414d84 --- /dev/null +++ b/internal/command/jsonformat/diff.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/differ" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/plans" +) + +func precomputeDiffs(plan Plan, mode plans.Mode) diffs { + diffs := diffs{ + outputs: make(map[string]computed.Diff), + } + + for _, drift := range plan.ResourceDrift { + + var relevantAttrs attribute_path.Matcher + if mode == plans.RefreshOnlyMode { + // For a refresh only plan, we show all the drift. + relevantAttrs = attribute_path.AlwaysMatcher() + } else { + matcher := attribute_path.Empty(true) + + // Otherwise we only want to show the drift changes that are + // relevant. + for _, attr := range plan.RelevantAttributes { + if len(attr.Resource) == 0 || attr.Resource == drift.Address { + matcher = attribute_path.AppendSingle(matcher, attr.Attr) + } + } + + if len(matcher.Paths) > 0 { + relevantAttrs = matcher + } + } + + if relevantAttrs == nil { + // If we couldn't build a relevant attribute matcher, then we are + // not going to show anything for this drift. + continue + } + + schema := plan.getSchema(drift) + change := structured.FromJsonChange(drift.Change, relevantAttrs) + diffs.drift = append(diffs.drift, diff{ + change: drift, + diff: differ.ComputeDiffForBlock(change, schema.Block), + }) + } + + for _, change := range plan.ResourceChanges { + schema := plan.getSchema(change) + structuredChange := structured.FromJsonChange(change.Change, attribute_path.AlwaysMatcher()) + diffs.changes = append(diffs.changes, diff{ + change: change, + diff: differ.ComputeDiffForBlock(structuredChange, schema.Block), + }) + } + + for _, change := range plan.DeferredChanges { + schema := plan.getSchema(change.ResourceChange) + structuredChange := structured.FromJsonChange(change.ResourceChange.Change, attribute_path.AlwaysMatcher()) + diffs.deferred = append(diffs.deferred, deferredDiff{ + reason: change.Reason, + diff: diff{ + change: change.ResourceChange, + diff: differ.ComputeDiffForBlock(structuredChange, schema.Block), + }, + }) + } + + for key, output := range plan.OutputChanges { + change := structured.FromJsonChange(output, attribute_path.AlwaysMatcher()) + diffs.outputs[key] = differ.ComputeDiffForOutput(change) + } + + return diffs +} + +type diffs struct { + drift []diff + changes []diff + deferred []deferredDiff + outputs map[string]computed.Diff +} + +func (d diffs) Empty() bool { + for _, change := range d.changes { + if change.diff.Action != plans.NoOp || change.Moved() { + return false + } + } + + for _, output := range d.outputs { + if output.Action != plans.NoOp { + return false + } + } + + return true +} + +type diff struct { + change jsonplan.ResourceChange + diff computed.Diff +} + +func (d diff) Moved() bool { + return len(d.change.PreviousAddress) > 0 && d.change.PreviousAddress != d.change.Address +} + +func (d diff) Importing() bool { + return d.change.Change.Importing != nil +} + +type deferredDiff struct { + diff diff + reason string +} diff --git a/internal/command/jsonformat/differ/attribute.go b/internal/command/jsonformat/differ/attribute.go new file mode 100644 index 0000000000..763f00333e --- /dev/null +++ b/internal/command/jsonformat/differ/attribute.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/plans" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + + "github.com/hashicorp/terraform/internal/command/jsonprovider" +) + +func ComputeDiffForAttribute(change structured.Change, attribute *jsonprovider.Attribute) computed.Diff { + if attribute.AttributeNestedType != nil { + return computeDiffForNestedAttribute(change, attribute.AttributeNestedType) + } + + return ComputeDiffForType(change, unmarshalAttribute(attribute)) +} + +func computeDiffForNestedAttribute(change structured.Change, nested *jsonprovider.NestedType) computed.Diff { + if sensitive, ok := checkForSensitiveNestedAttribute(change, nested); ok { + return sensitive + } + + if computed, ok := checkForUnknownNestedAttribute(change, nested); ok { + return computed + } + + switch NestingMode(nested.NestingMode) { + case nestingModeSingle, nestingModeGroup: + return computeAttributeDiffAsNestedObject(change, nested.Attributes) + case nestingModeMap: + return computeAttributeDiffAsNestedMap(change, nested.Attributes) + case nestingModeList: + return computeAttributeDiffAsNestedList(change, nested.Attributes) + case nestingModeSet: + return computeAttributeDiffAsNestedSet(change, nested.Attributes) + default: + panic("unrecognized nesting mode: " + nested.NestingMode) + } +} + +func computeDiffForWriteOnlyAttribute(change structured.Change, blockAction plans.Action) computed.Diff { + renderer := renderers.WriteOnly(change.IsBeforeSensitive() || change.IsAfterSensitive()) + replacePathMatches := change.ReplacePaths.Matches() + // Write-only diffs should always copy the behavior of the block they are in, except for updates + // since we don't want them to be always highlighted. + if blockAction == plans.Update { + return computed.NewDiff(renderer, plans.NoOp, replacePathMatches) + } + return computed.NewDiff(renderer, blockAction, replacePathMatches) + +} + +func ComputeDiffForType(change structured.Change, ctype cty.Type) computed.Diff { + if !change.NonLegacySchema { + // Empty strings in blocks should be considered null, because the legacy + // SDK can't always differentiate between null and empty strings and may + // return either. + if before, ok := change.Before.(string); ok && len(before) == 0 { + change.Before = nil + } + if after, ok := change.After.(string); ok && len(after) == 0 { + change.After = nil + } + } + + if sensitive, ok := checkForSensitiveType(change, ctype); ok { + return sensitive + } + + if computed, ok := checkForUnknownType(change, ctype); ok { + return computed + } + + switch { + case ctype == cty.NilType, ctype == cty.DynamicPseudoType: + // Forward nil or dynamic types over to be processed as outputs. + // There is nothing particularly special about the way outputs are + // processed that make this unsafe, we could just as easily call this + // function computeChangeForDynamicValues(), but external callers will + // only be in this situation when processing outputs so this function + // is named for their benefit. + return ComputeDiffForOutput(change) + case ctype.IsPrimitiveType(): + return computeAttributeDiffAsPrimitive(change, ctype) + case ctype.IsObjectType(): + return computeAttributeDiffAsObject(change, ctype.AttributeTypes()) + case ctype.IsMapType(): + return computeAttributeDiffAsMap(change, ctype.ElementType()) + case ctype.IsListType(): + return computeAttributeDiffAsList(change, ctype.ElementType()) + case ctype.IsTupleType(): + return computeAttributeDiffAsTuple(change, ctype.TupleElementTypes()) + case ctype.IsSetType(): + return computeAttributeDiffAsSet(change, ctype.ElementType()) + default: + panic("unrecognized type: " + ctype.FriendlyName()) + } +} + +func unmarshalAttribute(attribute *jsonprovider.Attribute) cty.Type { + ctyType, err := ctyjson.UnmarshalType(attribute.AttributeType) + if err != nil { + panic("could not unmarshal attribute type: " + err.Error()) + } + return ctyType +} diff --git a/internal/command/jsonformat/differ/block.go b/internal/command/jsonformat/differ/block.go new file mode 100644 index 0000000000..edafc7f4fb --- /dev/null +++ b/internal/command/jsonformat/differ/block.go @@ -0,0 +1,165 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +func ComputeDiffForBlock(change structured.Change, block *jsonprovider.Block) computed.Diff { + if sensitive, ok := checkForSensitiveBlock(change, block); ok { + return sensitive + } + + if unknown, ok := checkForUnknownBlock(change, block); ok { + return unknown + } + + // NonLegacyValue is only ever switched from false to true, since the + // behavior would be for the entire resource. + change.NonLegacySchema = change.NonLegacySchema || containsNonLegacyFeatures(block) + + current := change.GetDefaultActionForIteration() + + blockValue := change.AsMap() + + attributes := make(map[string]computed.Diff) + for key, attr := range block.Attributes { + if attr.WriteOnly { + continue + } + + childValue := blockValue.GetChild(key) + + if !childValue.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + childValue = childValue.AsNoOp() + } + + // Always treat changes to blocks as implicit. + childValue.BeforeExplicit = false + childValue.AfterExplicit = false + + childChange := ComputeDiffForAttribute(childValue, attr) + if childChange.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil { + // Don't record nil values at all in blocks except if they are write-only. + continue + } + + attributes[key] = childChange + current = collections.CompareActions(current, childChange.Action) + } + + blocks := renderers.Blocks{ + ReplaceBlocks: make(map[string]bool), + BeforeSensitiveBlocks: make(map[string]bool), + AfterSensitiveBlocks: make(map[string]bool), + UnknownBlocks: make(map[string]bool), + SingleBlocks: make(map[string]computed.Diff), + ListBlocks: make(map[string][]computed.Diff), + SetBlocks: make(map[string][]computed.Diff), + MapBlocks: make(map[string]map[string]computed.Diff), + } + + for key, blockType := range block.BlockTypes { + childValue := blockValue.GetChild(key) + + if !childValue.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + childValue = childValue.AsNoOp() + } + + beforeSensitive := childValue.IsBeforeSensitive() + afterSensitive := childValue.IsAfterSensitive() + forcesReplacement := childValue.ReplacePaths.Matches() + unknown := childValue.IsUnknown() + + switch NestingMode(blockType.NestingMode) { + case nestingModeSet: + diffs, action := computeBlockDiffsAsSet(childValue, blockType.Block) + if action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { + // Don't record nil values in blocks. + continue + } + blocks.AddAllSetBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive, unknown) + current = collections.CompareActions(current, action) + case nestingModeList: + diffs, action := computeBlockDiffsAsList(childValue, blockType.Block) + if action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { + // Don't record nil values in blocks. + continue + } + blocks.AddAllListBlock(key, diffs, forcesReplacement, beforeSensitive, afterSensitive, unknown) + current = collections.CompareActions(current, action) + case nestingModeMap: + diffs, action := computeBlockDiffsAsMap(childValue, blockType.Block) + if action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { + // Don't record nil values in blocks. + continue + } + blocks.AddAllMapBlocks(key, diffs, forcesReplacement, beforeSensitive, afterSensitive, unknown) + current = collections.CompareActions(current, action) + case nestingModeSingle, nestingModeGroup: + diff := ComputeDiffForBlock(childValue, blockType.Block) + if diff.Action == plans.NoOp && childValue.Before == nil && childValue.After == nil && !unknown { + // Don't record nil values in blocks. + continue + } + blocks.AddSingleBlock(key, diff, forcesReplacement, beforeSensitive, afterSensitive, unknown) + current = collections.CompareActions(current, diff.Action) + default: + panic("unrecognized nesting mode: " + blockType.NestingMode) + } + } + + for name, attr := range block.Attributes { + if attr.WriteOnly { + attributes[name] = computeDiffForWriteOnlyAttribute(change, current) + } + } + + return computed.NewDiff(renderers.Block(attributes, blocks), current, change.ReplacePaths.Matches()) +} + +// containsNonLegacyFeatures checks for features not supported by the legacy +// SDK, so that we can skip the empty string -> null fixup for them. +func containsNonLegacyFeatures(block *jsonprovider.Block) bool { + for _, blockType := range block.BlockTypes { + switch NestingMode(blockType.NestingMode) { + case nestingModeMap, nestingModeGroup: + // these block types were not possible in the SDK + return true + } + } + + for _, attribute := range block.Attributes { + //nested object types were not possible in the SDK + if attribute.AttributeNestedType != nil { + return true + } + + ty := unmarshalAttribute(attribute) + // these types were not possible in the SDK + switch { + case ty.HasDynamicTypes(): + return true + case ty.IsTupleType() || ty.IsObjectType(): + return true + case ty.IsCollectionType(): + // Nested collections were not really supported, but could be + // generated with string types (though we conservatively limit this + // to primitive types) + ety := ty.ElementType() + if ety.IsCollectionType() && !ety.ElementType().IsPrimitiveType() { + return true + } + } + } + return false +} diff --git a/internal/command/jsonformat/differ/differ.go b/internal/command/jsonformat/differ/differ.go new file mode 100644 index 0000000000..8e72a3c71f --- /dev/null +++ b/internal/command/jsonformat/differ/differ.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" +) + +// asDiff is a helper function to abstract away some simple and common +// functionality when converting a renderer into a concrete diff. +func asDiff(change structured.Change, renderer computed.DiffRenderer) computed.Diff { + return computed.NewDiff(renderer, change.CalculateAction(), change.ReplacePaths.Matches()) +} diff --git a/internal/command/jsonformat/differ/differ_test.go b/internal/command/jsonformat/differ/differ_test.go new file mode 100644 index 0000000000..9a2d09940e --- /dev/null +++ b/internal/command/jsonformat/differ/differ_test.go @@ -0,0 +1,3004 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +type SetDiff struct { + Before SetDiffEntry + After SetDiffEntry +} + +type SetDiffEntry struct { + SingleDiff renderers.ValidateDiffFunction + ObjectDiff map[string]renderers.ValidateDiffFunction + + Replace bool + Action plans.Action +} + +func (entry SetDiffEntry) Validate(obj func(attributes map[string]renderers.ValidateDiffFunction, action plans.Action, replace bool) renderers.ValidateDiffFunction) renderers.ValidateDiffFunction { + if entry.SingleDiff != nil { + return entry.SingleDiff + } + + return obj(entry.ObjectDiff, entry.Action, entry.Replace) +} + +func TestValue_SimpleBlocks(t *testing.T) { + // Most of the other test functions wrap the test cases in various + // collections or blocks. This function just very simply lets you specify + // individual test cases within blocks for some simple tests. + + tcs := map[string]struct { + input structured.Change + block *jsonprovider.Block + validate renderers.ValidateDiffFunction + }{ + "delete_with_null_sensitive_value": { + input: structured.Change{ + Before: map[string]interface{}{ + "normal_attribute": "some value", + }, + After: nil, + BeforeSensitive: map[string]interface{}{ + "sensitive_attribute": true, + }, + AfterSensitive: false, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "normal_attribute": { + AttributeType: unmarshalType(t, cty.String), + }, + "sensitive_attribute": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "normal_attribute": renderers.ValidatePrimitive("some value", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Delete, false), + }, + "create_with_null_sensitive_value": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "normal_attribute": "some value", + }, + BeforeSensitive: map[string]interface{}{ + "sensitive_attribute": true, + }, + AfterSensitive: false, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "normal_attribute": { + AttributeType: unmarshalType(t, cty.String), + }, + "sensitive_attribute": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "normal_attribute": renderers.ValidatePrimitive(nil, "some value", plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, + "create_with_unknown_block": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "normal_attribute": "some value", + }, + Unknown: map[string]any{ + "nested": true, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "normal_attribute": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + BlockTypes: map[string]*jsonprovider.BlockType{ + "nested": { + NestingMode: "single", + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attr": { + AttributeType: unmarshalType(t, cty.String), + Optional: true, + }, + }, + }, + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "normal_attribute": renderers.ValidatePrimitive(nil, "some value", plans.Create, false), + }, map[string]renderers.ValidateDiffFunction{ + "nested": renderers.ValidateUnknown(nil, plans.Create, false), + }, nil, nil, nil, plans.Create, false)}, + } + for name, tc := range tcs { + // Set some default values + if tc.input.ReplacePaths == nil { + tc.input.ReplacePaths = &attribute_path.PathMatcher{} + } + + if tc.input.RelevantAttributes == nil { + tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() + } + + t.Run(name, func(t *testing.T) { + tc.validate(t, ComputeDiffForBlock(tc.input, tc.block)) + }) + } +} + +func TestValue_ObjectAttributes(t *testing.T) { + // This function holds a range of test cases creating, deleting and editing + // objects. It is built in such a way that it can automatically test these + // operations on objects both directly and nested, as well as within all + // types of collections. + + tcs := map[string]struct { + input structured.Change + attributes map[string]cty.Type + validateSingleDiff renderers.ValidateDiffFunction + validateObject renderers.ValidateDiffFunction + validateNestedObject renderers.ValidateDiffFunction + validateDiffs map[string]renderers.ValidateDiffFunction + validateList renderers.ValidateDiffFunction + validateReplace bool + validateAction plans.Action + // Sets break changes out differently to the other collections, so they + // have their own entry. + validateSetDiffs *SetDiff + }{ + "create": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "attribute_one": "new", + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + validateAction: plans.Create, + validateReplace: false, + }, + "delete": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: nil, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + validateAction: plans.Delete, + validateReplace: false, + }, + "create_sensitive": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "attribute_one": "new", + }, + AfterSensitive: true, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateSingleDiff: renderers.ValidateSensitive(renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, plans.Create, false), + false, + true, + plans.Create, + false), + validateNestedObject: renderers.ValidateSensitive(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, plans.Create, false), + false, + true, + plans.Create, + false), + }, + "delete_sensitive": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + BeforeSensitive: true, + After: nil, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateSingleDiff: renderers.ValidateSensitive(renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, plans.Delete, false), true, false, plans.Delete, false), + validateNestedObject: renderers.ValidateSensitive(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, plans.Delete, false), true, false, plans.Delete, false), + }, + "create_unknown": { + input: structured.Change{ + Before: nil, + After: nil, + Unknown: true, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateSingleDiff: renderers.ValidateUnknown(nil, plans.Create, false), + }, + "update_unknown": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: nil, + Unknown: true, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateObject: renderers.ValidateUnknown(renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, plans.Delete, false), plans.Update, false), + validateNestedObject: renderers.ValidateUnknown(renderers.ValidateNestedObject(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false), + }, plans.Update, false), plans.Update, false), + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + SingleDiff: renderers.ValidateUnknown(nil, plans.Create, false), + }, + }, + }, + "create_attribute": { + input: structured.Change{ + Before: map[string]interface{}{}, + After: map[string]interface{}{ + "attribute_one": "new", + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "create_attribute_from_explicit_null": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": nil, + }, + After: map[string]interface{}{ + "attribute_one": "new", + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "delete_attribute": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: map[string]interface{}{}, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: nil, + Action: plans.Create, + Replace: false, + }, + }, + }, + "delete_attribute_to_explicit_null": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: map[string]interface{}{ + "attribute_one": nil, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: nil, + Action: plans.Create, + Replace: false, + }, + }, + }, + "update_attribute": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: map[string]interface{}{ + "attribute_one": "new", + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "create_sensitive_attribute": { + input: structured.Change{ + Before: map[string]interface{}{}, + After: map[string]interface{}{ + "attribute_one": "new", + }, + AfterSensitive: map[string]interface{}{ + "attribute_one": true, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: nil, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "delete_sensitive_attribute": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + BeforeSensitive: map[string]interface{}{ + "attribute_one": true, + }, + After: map[string]interface{}{}, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: nil, + Action: plans.Create, + Replace: false, + }, + }, + }, + "update_sensitive_attribute": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + BeforeSensitive: map[string]interface{}{ + "attribute_one": true, + }, + After: map[string]interface{}{ + "attribute_one": "new", + }, + AfterSensitive: map[string]interface{}{ + "attribute_one": true, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", "new", plans.Update, false), true, true, plans.Update, false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "create_computed_attribute": { + input: structured.Change{ + Before: map[string]interface{}{}, + After: map[string]interface{}{}, + Unknown: map[string]interface{}{ + "attribute_one": true, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateUnknown(nil, plans.Create, false), + }, + validateAction: plans.Update, + validateReplace: false, + }, + "update_computed_attribute": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: map[string]interface{}{}, + Unknown: map[string]interface{}{ + "attribute_one": true, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateUnknown( + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + plans.Update, + false), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidateUnknown(nil, plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "ignores_unset_fields": { + input: structured.Change{ + Before: map[string]interface{}{}, + After: map[string]interface{}{}, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{}, + validateAction: plans.NoOp, + validateReplace: false, + }, + "update_replace_self": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: map[string]interface{}{ + "attribute_one": "new", + }, + ReplacePaths: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + {}, + }, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, + validateAction: plans.Update, + validateReplace: true, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: true, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + Action: plans.Create, + Replace: true, + }, + }, + }, + "update_replace_attribute": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old", + }, + After: map[string]interface{}{ + "attribute_one": "new", + }, + ReplacePaths: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + {"attribute_one"}, + }, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, true), + }, + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, true), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, true), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + "update_includes_relevant_attributes": { + input: structured.Change{ + Before: map[string]interface{}{ + "attribute_one": "old_one", + "attribute_two": "old_two", + }, + After: map[string]interface{}{ + "attribute_one": "new_one", + "attribute_two": "new_two", + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + {"attribute_one"}, + }, + }, + }, + attributes: map[string]cty.Type{ + "attribute_one": cty.String, + "attribute_two": cty.String, + }, + validateDiffs: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), + "attribute_two": renderers.ValidatePrimitive("old_two", "old_two", plans.NoOp, false), + }, + validateList: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + // Lists are a bit special, and in this case is actually + // going to ignore the relevant attributes. This is + // deliberate. See the comments in list.go for an + // explanation. + "attribute_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), + "attribute_two": renderers.ValidatePrimitive("old_two", "new_two", plans.Update, false), + }, plans.Update, false), + }, plans.Update, false), + validateAction: plans.Update, + validateReplace: false, + validateSetDiffs: &SetDiff{ + Before: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + "attribute_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, + Action: plans.Delete, + Replace: false, + }, + After: SetDiffEntry{ + ObjectDiff: map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + "attribute_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, + Action: plans.Create, + Replace: false, + }, + }, + }, + } + + for name, tmp := range tcs { + tc := tmp + + // Let's set some default values on the input. + if tc.input.RelevantAttributes == nil { + tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() + } + if tc.input.ReplacePaths == nil { + tc.input.ReplacePaths = &attribute_path.PathMatcher{} + } + + collectionDefaultAction := plans.Update + if name == "ignores_unset_fields" { + // Special case for this test, as it is the only one that doesn't + // have the collection types return an update. + collectionDefaultAction = plans.NoOp + } + + t.Run(name, func(t *testing.T) { + t.Run("object", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Object(tc.attributes)), + } + + if tc.validateObject != nil { + tc.validateObject(t, ComputeDiffForAttribute(tc.input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute)) + return + } + + validate := renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace) + validate(t, ComputeDiffForAttribute(tc.input, attribute)) + }) + + t.Run("map", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.Object(tc.attributes))), + } + + input := wrapChangeInMap(tc.input) + + if tc.validateObject != nil { + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": tc.validateObject, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": tc.validateSingleDiff, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + + t.Run("list", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.Object(tc.attributes))), + } + + input := wrapChangeInSlice(tc.input) + + if tc.validateList != nil { + tc.validateList(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateObject != nil { + validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ + tc.validateObject, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ + tc.validateSingleDiff, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + + t.Run("set", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.Object(tc.attributes))), + } + + input := wrapChangeInSlice(tc.input) + + if tc.validateSetDiffs != nil { + validate := renderers.ValidateSet(func() []renderers.ValidateDiffFunction { + var ret []renderers.ValidateDiffFunction + ret = append(ret, tc.validateSetDiffs.Before.Validate(renderers.ValidateObject)) + ret = append(ret, tc.validateSetDiffs.After.Validate(renderers.ValidateObject)) + return ret + }(), collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateObject != nil { + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + tc.validateObject, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + tc.validateSingleDiff, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + renderers.ValidateObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + }) + + t.Run(fmt.Sprintf("nested_%s", name), func(t *testing.T) { + t.Run("object", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeNestedType: &jsonprovider.NestedType{ + Attributes: func() map[string]*jsonprovider.Attribute { + attributes := make(map[string]*jsonprovider.Attribute) + for key, attribute := range tc.attributes { + attributes[key] = &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, attribute), + } + } + return attributes + }(), + NestingMode: "single", + }, + } + + if tc.validateNestedObject != nil { + tc.validateNestedObject(t, ComputeDiffForAttribute(tc.input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + tc.validateSingleDiff(t, ComputeDiffForAttribute(tc.input, attribute)) + return + } + + validate := renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace) + validate(t, ComputeDiffForAttribute(tc.input, attribute)) + }) + + t.Run("map", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeNestedType: &jsonprovider.NestedType{ + Attributes: func() map[string]*jsonprovider.Attribute { + attributes := make(map[string]*jsonprovider.Attribute) + for key, attribute := range tc.attributes { + attributes[key] = &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, attribute), + } + } + return attributes + }(), + NestingMode: "map", + }, + } + + input := wrapChangeInMap(tc.input) + + if tc.validateNestedObject != nil { + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": tc.validateNestedObject, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": tc.validateSingleDiff, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + + t.Run("list", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeNestedType: &jsonprovider.NestedType{ + Attributes: func() map[string]*jsonprovider.Attribute { + attributes := make(map[string]*jsonprovider.Attribute) + for key, attribute := range tc.attributes { + attributes[key] = &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, attribute), + } + } + return attributes + }(), + NestingMode: "list", + }, + } + + input := wrapChangeInSlice(tc.input) + + if tc.validateNestedObject != nil { + validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{ + tc.validateNestedObject, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{ + tc.validateSingleDiff, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateNestedList([]renderers.ValidateDiffFunction{ + renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + + t.Run("set", func(t *testing.T) { + attribute := &jsonprovider.Attribute{ + AttributeNestedType: &jsonprovider.NestedType{ + Attributes: func() map[string]*jsonprovider.Attribute { + attributes := make(map[string]*jsonprovider.Attribute) + for key, attribute := range tc.attributes { + attributes[key] = &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, attribute), + } + } + return attributes + }(), + NestingMode: "set", + }, + } + + input := wrapChangeInSlice(tc.input) + + if tc.validateSetDiffs != nil { + validate := renderers.ValidateSet(func() []renderers.ValidateDiffFunction { + var ret []renderers.ValidateDiffFunction + ret = append(ret, tc.validateSetDiffs.Before.Validate(renderers.ValidateNestedObject)) + ret = append(ret, tc.validateSetDiffs.After.Validate(renderers.ValidateNestedObject)) + return ret + }(), collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateNestedObject != nil { + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + tc.validateNestedObject, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + if tc.validateSingleDiff != nil { + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + tc.validateSingleDiff, + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + renderers.ValidateNestedObject(tc.validateDiffs, tc.validateAction, tc.validateReplace), + }, collectionDefaultAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + }) + } +} + +func TestValue_BlockAttributesAndNestedBlocks(t *testing.T) { + // This function tests manipulating simple attributes and blocks within + // blocks. It automatically tests these operations within the contexts of + // different block types. + + tcs := map[string]struct { + before interface{} + after interface{} + block *jsonprovider.Block + validate renderers.ValidateDiffFunction + validateSet []renderers.ValidateDiffFunction + }{ + "create_attribute": { + before: map[string]interface{}{}, + after: map[string]interface{}{ + "attribute_one": "new", + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, nil, nil, nil, nil, plans.Update, false), + validateSet: []renderers.ValidateDiffFunction{ + renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Delete, false), + renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, + }, + "update_attribute": { + before: map[string]interface{}{ + "attribute_one": "old", + }, + after: map[string]interface{}{ + "attribute_one": "new", + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + validateSet: []renderers.ValidateDiffFunction{ + renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Delete, false), + renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, + }, + "delete_attribute": { + before: map[string]interface{}{ + "attribute_one": "old", + }, + after: map[string]interface{}{}, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Update, false), + validateSet: []renderers.ValidateDiffFunction{ + renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Delete, false), + renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Create, false), + }, + }, + "create_block": { + before: map[string]interface{}{}, + after: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "new", + }, + }, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_one": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "single", + }, + }, + }, + validate: renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, nil, nil, nil, plans.Update, false), + validateSet: []renderers.ValidateDiffFunction{ + renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Delete, false), + renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, nil, nil, nil, plans.Create, false), + }, + }, + "update_block": { + before: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "old", + }, + }, + after: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "new", + }, + }, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_one": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "single", + }, + }, + }, + validate: renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, nil, nil, nil, plans.Update, false), + validateSet: []renderers.ValidateDiffFunction{ + renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Delete, false), + }, nil, nil, nil, plans.Delete, false), + renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, nil, nil, nil, plans.Create, false), + }, + }, + "delete_block": { + before: map[string]interface{}{ + "block_one": map[string]interface{}{ + "attribute_one": "old", + }, + }, + after: map[string]interface{}{}, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_one": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "attribute_one": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "single", + }, + }, + }, + validate: renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Delete, false), + }, nil, nil, nil, plans.Update, false), + validateSet: []renderers.ValidateDiffFunction{ + renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_one": renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "attribute_one": renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, nil, nil, nil, nil, plans.Delete, false), + }, nil, nil, nil, plans.Delete, false), + renderers.ValidateBlock(nil, nil, nil, nil, nil, plans.Create, false), + }, + }, + } + for name, tmp := range tcs { + tc := tmp + + t.Run(name, func(t *testing.T) { + t.Run("single", func(t *testing.T) { + input := structured.Change{ + Before: map[string]interface{}{ + "block_type": tc.before, + }, + After: map[string]interface{}{ + "block_type": tc.after, + }, + ReplacePaths: &attribute_path.PathMatcher{}, + RelevantAttributes: attribute_path.AlwaysMatcher(), + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "single", + }, + }, + } + + validate := renderers.ValidateBlock(nil, map[string]renderers.ValidateDiffFunction{ + "block_type": tc.validate, + }, nil, nil, nil, plans.Update, false) + validate(t, ComputeDiffForBlock(input, block)) + }) + t.Run("map", func(t *testing.T) { + input := structured.Change{ + Before: map[string]interface{}{ + "block_type": map[string]interface{}{ + "one": tc.before, + }, + }, + After: map[string]interface{}{ + "block_type": map[string]interface{}{ + "one": tc.after, + }, + }, + ReplacePaths: &attribute_path.PathMatcher{}, + RelevantAttributes: attribute_path.AlwaysMatcher(), + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "map", + }, + }, + } + + validate := renderers.ValidateBlock(nil, nil, nil, map[string]map[string]renderers.ValidateDiffFunction{ + "block_type": { + "one": tc.validate, + }, + }, nil, plans.Update, false) + validate(t, ComputeDiffForBlock(input, block)) + }) + t.Run("list", func(t *testing.T) { + input := structured.Change{ + Before: map[string]interface{}{ + "block_type": []interface{}{ + tc.before, + }, + }, + After: map[string]interface{}{ + "block_type": []interface{}{ + tc.after, + }, + }, + ReplacePaths: &attribute_path.PathMatcher{}, + RelevantAttributes: attribute_path.AlwaysMatcher(), + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "list", + }, + }, + } + + validate := renderers.ValidateBlock(nil, nil, map[string][]renderers.ValidateDiffFunction{ + "block_type": { + tc.validate, + }, + }, nil, nil, plans.Update, false) + validate(t, ComputeDiffForBlock(input, block)) + }) + t.Run("set", func(t *testing.T) { + input := structured.Change{ + Before: map[string]interface{}{ + "block_type": []interface{}{ + tc.before, + }, + }, + After: map[string]interface{}{ + "block_type": []interface{}{ + tc.after, + }, + }, + ReplacePaths: &attribute_path.PathMatcher{}, + RelevantAttributes: attribute_path.AlwaysMatcher(), + } + + block := &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "block_type": { + Block: tc.block, + NestingMode: "set", + }, + }, + } + + validate := renderers.ValidateBlock(nil, nil, nil, nil, map[string][]renderers.ValidateDiffFunction{ + "block_type": func() []renderers.ValidateDiffFunction { + if tc.validateSet != nil { + return tc.validateSet + } + return []renderers.ValidateDiffFunction{tc.validate} + }(), + }, plans.Update, false) + validate(t, ComputeDiffForBlock(input, block)) + }) + }) + } +} + +func TestValue_Outputs(t *testing.T) { + tcs := map[string]struct { + input structured.Change + validateDiff renderers.ValidateDiffFunction + }{ + "primitive_create": { + input: structured.Change{ + Before: nil, + After: "new", + }, + validateDiff: renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + "object_create": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateDiff: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + "element_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, plans.Create, false), + }, + "list_create": { + input: structured.Change{ + Before: nil, + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, plans.Create, false), + }, + "primitive_update": { + input: structured.Change{ + Before: "old", + After: "new", + }, + validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, + "object_update": { + input: structured.Change{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateDiff: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), + "element_two": renderers.ValidatePrimitive("old_two", "new_two", plans.Update, false), + }, plans.Update, false), + }, + "list_update": { + input: structured.Change{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), + renderers.ValidatePrimitive("old_two", "new_two", plans.Update, false), + }, plans.Update, false), + }, + "primitive_delete": { + input: structured.Change{ + Before: "old", + After: nil, + }, + validateDiff: renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + "object_delete": { + input: structured.Change{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: nil, + }, + validateDiff: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + "element_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, plans.Delete, false), + }, + "list_delete": { + input: structured.Change{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: nil, + }, + validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, plans.Delete, false), + }, + "primitive_to_list": { + input: structured.Change{ + Before: "old", + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + "primitive_to_object": { + input: structured.Change{ + Before: "old", + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + "element_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + "list_to_primitive": { + input: structured.Change{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: "new", + }, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, plans.Delete, false), + renderers.ValidatePrimitive(nil, "new", plans.Create, false), + plans.Update, false), + }, + "list_to_object": { + input: structured.Change{ + Before: []interface{}{ + "old_one", + "old_two", + }, + After: map[string]interface{}{ + "element_one": "new_one", + "element_two": "new_two", + }, + }, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, plans.Delete, false), + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + "element_two": renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + "object_to_primitive": { + input: structured.Change{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: "new", + }, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + "element_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, plans.Delete, false), + renderers.ValidatePrimitive(nil, "new", plans.Create, false), + plans.Update, false), + }, + "object_to_list": { + input: structured.Change{ + Before: map[string]interface{}{ + "element_one": "old_one", + "element_two": "old_two", + }, + After: []interface{}{ + "new_one", + "new_two", + }, + }, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive("old_one", nil, plans.Delete, false), + "element_two": renderers.ValidatePrimitive("old_two", nil, plans.Delete, false), + }, plans.Delete, false), + renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, "new_one", plans.Create, false), + renderers.ValidatePrimitive(nil, "new_two", plans.Create, false), + }, plans.Create, false), plans.Update, false), + }, + } + + for name, tc := range tcs { + + // Let's set some default values on the input. + if tc.input.RelevantAttributes == nil { + tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() + } + if tc.input.ReplacePaths == nil { + tc.input.ReplacePaths = &attribute_path.PathMatcher{} + } + + t.Run(name, func(t *testing.T) { + tc.validateDiff(t, ComputeDiffForOutput(tc.input)) + }) + } +} + +func TestValue_PrimitiveAttributes(t *testing.T) { + // This function tests manipulating primitives: creating, deleting and + // updating. It also automatically tests these operations within the + // contexts of collections. + + tcs := map[string]struct { + input structured.Change + attribute cty.Type + validateDiff renderers.ValidateDiffFunction + validateSliceDiffs []renderers.ValidateDiffFunction // Lists are special in some cases. + validateSetDiffs []renderers.ValidateDiffFunction // Sets are special in some cases. + }{ + "primitive_create": { + input: structured.Change{ + After: "new", + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + "primitive_delete": { + input: structured.Change{ + Before: "old", + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive("old", nil, plans.Delete, false), + }, + "primitive_update": { + input: structured.Change{ + Before: "old", + After: "new", + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + }, + "primitive_set_explicit_null": { + input: structured.Change{ + Before: "old", + After: nil, + AfterExplicit: true, + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive("old", nil, plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, nil, plans.Create, false), + }, + }, + "primitive_unset_explicit_null": { + input: structured.Change{ + BeforeExplicit: true, + Before: nil, + After: "new", + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive(nil, "new", plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, "new", plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + }, + "primitive_create_sensitive": { + input: structured.Change{ + Before: nil, + After: "new", + AfterSensitive: true, + }, + attribute: cty.String, + validateDiff: renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), + }, + "primitive_delete_sensitive": { + input: structured.Change{ + Before: "old", + BeforeSensitive: true, + After: nil, + }, + attribute: cty.String, + validateDiff: renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), + }, + "primitive_update_sensitive": { + input: structured.Change{ + Before: "old", + BeforeSensitive: true, + After: "new", + AfterSensitive: true, + }, + attribute: cty.String, + validateDiff: renderers.ValidateSensitive(renderers.ValidatePrimitive("old", "new", plans.Update, false), true, true, plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidateSensitive(renderers.ValidatePrimitive("old", "new", plans.Update, false), true, true, plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidateSensitive(renderers.ValidatePrimitive("old", nil, plans.Delete, false), true, false, plans.Delete, false), + renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "new", plans.Create, false), false, true, plans.Create, false), + }, + }, + "primitive_create_computed": { + input: structured.Change{ + Before: nil, + After: nil, + Unknown: true, + }, + attribute: cty.String, + validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), + }, + "primitive_update_computed": { + input: structured.Change{ + Before: "old", + After: nil, + Unknown: true, + }, + attribute: cty.String, + validateDiff: renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidateUnknown(renderers.ValidatePrimitive("old", nil, plans.Delete, false), plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidateUnknown(nil, plans.Create, false), + }, + }, + "primitive_update_replace": { + input: structured.Change{ + Before: "old", + After: "new", + ReplacePaths: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + {}, // An empty path suggests replace should be true. + }, + }, + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, true), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", "new", plans.Update, true), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Delete, true), + renderers.ValidatePrimitive(nil, "new", plans.Create, true), + }, + }, + "noop": { + input: structured.Change{ + Before: "old", + After: "old", + }, + attribute: cty.String, + validateDiff: renderers.ValidatePrimitive("old", "old", plans.NoOp, false), + }, + "dynamic": { + input: structured.Change{ + Before: "old", + After: "new", + }, + attribute: cty.DynamicPseudoType, + validateDiff: renderers.ValidatePrimitive("old", "new", plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", "new", plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, "new", plans.Create, false), + }, + }, + "dynamic_type_change": { + input: structured.Change{ + Before: "old", + After: json.Number("4"), + }, + attribute: cty.DynamicPseudoType, + validateDiff: renderers.ValidateTypeChange( + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, json.Number("4"), plans.Create, false), + plans.Update, false), + validateSliceDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidateTypeChange( + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, json.Number("4"), plans.Create, false), + plans.Update, false), + }, + validateSetDiffs: []renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("old", nil, plans.Delete, false), + renderers.ValidatePrimitive(nil, json.Number("4"), plans.Create, false), + }, + }, + } + for name, tmp := range tcs { + tc := tmp + + // Let's set some default values on the input. + if tc.input.RelevantAttributes == nil { + tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() + } + if tc.input.ReplacePaths == nil { + tc.input.ReplacePaths = &attribute_path.PathMatcher{} + } + + defaultCollectionsAction := plans.Update + if name == "noop" { + defaultCollectionsAction = plans.NoOp + } + + t.Run(name, func(t *testing.T) { + t.Run("direct", func(t *testing.T) { + tc.validateDiff(t, ComputeDiffForAttribute(tc.input, &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, tc.attribute), + })) + }) + + t.Run("map", func(t *testing.T) { + input := wrapChangeInMap(tc.input) + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(tc.attribute)), + } + + validate := renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": tc.validateDiff, + }, defaultCollectionsAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + + t.Run("list", func(t *testing.T) { + input := wrapChangeInSlice(tc.input) + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(tc.attribute)), + } + + if tc.validateSliceDiffs != nil { + validate := renderers.ValidateList(tc.validateSliceDiffs, defaultCollectionsAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateList([]renderers.ValidateDiffFunction{ + tc.validateDiff, + }, defaultCollectionsAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + + t.Run("set", func(t *testing.T) { + input := wrapChangeInSlice(tc.input) + attribute := &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(tc.attribute)), + } + + if tc.validateSliceDiffs != nil { + validate := renderers.ValidateSet(tc.validateSetDiffs, defaultCollectionsAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + return + } + + validate := renderers.ValidateSet([]renderers.ValidateDiffFunction{ + tc.validateDiff, + }, defaultCollectionsAction, false) + validate(t, ComputeDiffForAttribute(input, attribute)) + }) + }) + } +} + +func TestValue_CollectionAttributes(t *testing.T) { + // This function tests creating and deleting collections. Note, it does not + // generally cover editing collections except in special cases as editing + // collections is handled automatically by other functions. + tcs := map[string]struct { + input structured.Change + attribute *jsonprovider.Attribute + validateDiff renderers.ValidateDiffFunction + }{ + "map_create_empty": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateMap(nil, plans.Create, false), + }, + "map_create_populated": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "element_one": "one", + "element_two": "two", + }, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive(nil, "one", plans.Create, false), + "element_two": renderers.ValidatePrimitive(nil, "two", plans.Create, false), + }, plans.Create, false), + }, + "map_delete_empty": { + input: structured.Change{ + Before: map[string]interface{}{}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateMap(nil, plans.Delete, false), + }, + "map_delete_populated": { + input: structured.Change{ + Before: map[string]interface{}{ + "element_one": "one", + "element_two": "two", + }, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element_one": renderers.ValidatePrimitive("one", nil, plans.Delete, false), + "element_two": renderers.ValidatePrimitive("two", nil, plans.Delete, false), + }, plans.Delete, false), + }, + "map_create_sensitive": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateMap(nil, plans.Create, false), false, true, plans.Create, false), + }, + "map_update_sensitive": { + input: structured.Change{ + Before: map[string]interface{}{ + "element": "one", + }, + BeforeSensitive: true, + After: map[string]interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "element": renderers.ValidatePrimitive("one", nil, plans.Delete, false), + }, plans.Update, false), true, true, plans.Update, false), + }, + "map_delete_sensitive": { + input: structured.Change{ + Before: map[string]interface{}{}, + BeforeSensitive: true, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateMap(nil, plans.Delete, false), true, false, plans.Delete, false), + }, + "map_create_unknown": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), + }, + "map_update_unknown": { + input: structured.Change{ + Before: map[string]interface{}{}, + After: map[string]interface{}{ + "element": "one", + }, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + validateDiff: renderers.ValidateUnknown(renderers.ValidateMap(nil, plans.Delete, false), plans.Update, false), + }, + "list_create_empty": { + input: structured.Change{ + Before: nil, + After: []interface{}{}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateList(nil, plans.Create, false), + }, + "list_create_populated": { + input: structured.Change{ + Before: nil, + After: []interface{}{"one", "two"}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, "one", plans.Create, false), + renderers.ValidatePrimitive(nil, "two", plans.Create, false), + }, plans.Create, false), + }, + "list_delete_empty": { + input: structured.Change{ + Before: []interface{}{}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateList(nil, plans.Delete, false), + }, + "list_delete_populated": { + input: structured.Change{ + Before: []interface{}{"one", "two"}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("one", nil, plans.Delete, false), + renderers.ValidatePrimitive("two", nil, plans.Delete, false), + }, plans.Delete, false), + }, + "list_create_sensitive": { + input: structured.Change{ + Before: nil, + After: []interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateList(nil, plans.Create, false), false, true, plans.Create, false), + }, + "list_update_sensitive": { + input: structured.Change{ + Before: []interface{}{"one"}, + BeforeSensitive: true, + After: []interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("one", nil, plans.Delete, false), + }, plans.Update, false), true, true, plans.Update, false), + }, + "list_delete_sensitive": { + input: structured.Change{ + Before: []interface{}{}, + BeforeSensitive: true, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateList(nil, plans.Delete, false), true, false, plans.Delete, false), + }, + "list_create_unknown": { + input: structured.Change{ + Before: nil, + After: []interface{}{}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), + }, + "list_update_unknown": { + input: structured.Change{ + Before: []interface{}{}, + After: []interface{}{"one"}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.List(cty.String)), + }, + validateDiff: renderers.ValidateUnknown(renderers.ValidateList(nil, plans.Delete, false), plans.Update, false), + }, + "set_create_empty": { + input: structured.Change{ + Before: nil, + After: []interface{}{}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSet(nil, plans.Create, false), + }, + "set_create_populated": { + input: structured.Change{ + Before: nil, + After: []interface{}{"one", "two"}, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSet([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(nil, "one", plans.Create, false), + renderers.ValidatePrimitive(nil, "two", plans.Create, false), + }, plans.Create, false), + }, + "set_delete_empty": { + input: structured.Change{ + Before: []interface{}{}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSet(nil, plans.Delete, false), + }, + "set_delete_populated": { + input: structured.Change{ + Before: []interface{}{"one", "two"}, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSet([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("one", nil, plans.Delete, false), + renderers.ValidatePrimitive("two", nil, plans.Delete, false), + }, plans.Delete, false), + }, + "set_create_sensitive": { + input: structured.Change{ + Before: nil, + After: []interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateSet(nil, plans.Create, false), false, true, plans.Create, false), + }, + "set_update_sensitive": { + input: structured.Change{ + Before: []interface{}{"one"}, + BeforeSensitive: true, + After: []interface{}{}, + AfterSensitive: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateSet([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("one", nil, plans.Delete, false), + }, plans.Update, false), true, true, plans.Update, false), + }, + "set_delete_sensitive": { + input: structured.Change{ + Before: []interface{}{}, + BeforeSensitive: true, + After: nil, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateSensitive(renderers.ValidateSet(nil, plans.Delete, false), true, false, plans.Delete, false), + }, + "set_create_unknown": { + input: structured.Change{ + Before: nil, + After: []interface{}{}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateUnknown(nil, plans.Create, false), + }, + "set_update_unknown": { + input: structured.Change{ + Before: []interface{}{}, + After: []interface{}{"one"}, + Unknown: true, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Set(cty.String)), + }, + validateDiff: renderers.ValidateUnknown(renderers.ValidateSet(nil, plans.Delete, false), plans.Update, false), + }, + "tuple_primitive": { + input: structured.Change{ + Before: []interface{}{ + "one", + json.Number("2"), + "three", + }, + After: []interface{}{ + "one", + json.Number("4"), + "three", + }, + }, + attribute: &jsonprovider.Attribute{ + AttributeType: unmarshalType(t, cty.Tuple([]cty.Type{cty.String, cty.Number, cty.String})), + }, + validateDiff: renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive("one", "one", plans.NoOp, false), + renderers.ValidatePrimitive(json.Number("2"), json.Number("4"), plans.Update, false), + renderers.ValidatePrimitive("three", "three", plans.NoOp, false), + }, plans.Update, false), + }, + } + + for name, tc := range tcs { + + // Let's set some default values on the input. + if tc.input.RelevantAttributes == nil { + tc.input.RelevantAttributes = attribute_path.AlwaysMatcher() + } + if tc.input.ReplacePaths == nil { + tc.input.ReplacePaths = &attribute_path.PathMatcher{} + } + + t.Run(name, func(t *testing.T) { + tc.validateDiff(t, ComputeDiffForAttribute(tc.input, tc.attribute)) + }) + } +} + +func TestRelevantAttributes(t *testing.T) { + tcs := map[string]struct { + input structured.Change + block *jsonprovider.Block + validate renderers.ValidateDiffFunction + }{ + "simple_attributes": { + input: structured.Change{ + Before: map[string]interface{}{ + "id": "old_id", + "ignore": "doesn't matter", + }, + After: map[string]interface{}{ + "id": "new_id", + "ignore": "doesn't matter but modified", + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + { + "id", + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "id": { + AttributeType: unmarshalType(t, cty.String), + }, + "ignore": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "id": renderers.ValidatePrimitive("old_id", "new_id", plans.Update, false), + "ignore": renderers.ValidatePrimitive("doesn't matter", "doesn't matter", plans.NoOp, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + "nested_attributes": { + input: structured.Change{ + Before: map[string]interface{}{ + "list_block": []interface{}{ + map[string]interface{}{ + "id": "old_one", + }, + map[string]interface{}{ + "id": "ignored", + }, + }, + }, + After: map[string]interface{}{ + "list_block": []interface{}{ + map[string]interface{}{ + "id": "new_one", + }, + map[string]interface{}{ + "id": "ignored_but_changed", + }, + }, + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + { + "list_block", + float64(0), + "id", + }, + }, + }, + }, + block: &jsonprovider.Block{ + BlockTypes: map[string]*jsonprovider.BlockType{ + "list_block": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "id": { + AttributeType: unmarshalType(t, cty.String), + }, + }, + }, + NestingMode: "list", + }, + }, + }, + validate: renderers.ValidateBlock(nil, nil, map[string][]renderers.ValidateDiffFunction{ + "list_block": { + renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "id": renderers.ValidatePrimitive("old_one", "new_one", plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "id": renderers.ValidatePrimitive("ignored", "ignored", plans.NoOp, false), + }, nil, nil, nil, nil, plans.NoOp, false), + }, + }, nil, nil, plans.Update, false), + }, + "nested_attributes_in_object": { + input: structured.Change{ + Before: map[string]interface{}{ + "object": map[string]interface{}{ + "id": "old_id", + }, + }, + After: map[string]interface{}{ + "object": map[string]interface{}{ + "id": "new_id", + }, + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Propagate: true, + Paths: [][]interface{}{ + { + "object", // Even though we just specify object, it should now include every below object as well. + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "object": { + AttributeType: unmarshalType(t, cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "id": renderers.ValidatePrimitive("old_id", "new_id", plans.Update, false), + }, plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + "elements_in_list": { + input: structured.Change{ + Before: map[string]interface{}{ + "list": []interface{}{ + json.Number("0"), json.Number("1"), json.Number("2"), json.Number("3"), json.Number("4"), + }, + }, + After: map[string]interface{}{ + "list": []interface{}{ + json.Number("0"), json.Number("5"), json.Number("6"), json.Number("7"), json.Number("4"), + }, + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ // The list is actually just going to ignore this. + { + "list", + float64(0), + }, + { + "list", + float64(2), + }, + { + "list", + float64(4), + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "list": { + AttributeType: unmarshalType(t, cty.List(cty.Number)), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + // The list validator below just ignores our relevant + // attributes. This is deliberate. + "list": renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(json.Number("0"), json.Number("0"), plans.NoOp, false), + renderers.ValidatePrimitive(json.Number("1"), json.Number("5"), plans.Update, false), + renderers.ValidatePrimitive(json.Number("2"), json.Number("6"), plans.Update, false), + renderers.ValidatePrimitive(json.Number("3"), json.Number("7"), plans.Update, false), + renderers.ValidatePrimitive(json.Number("4"), json.Number("4"), plans.NoOp, false), + }, plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + "elements_in_map": { + input: structured.Change{ + Before: map[string]interface{}{ + "map": map[string]interface{}{ + "key_one": "value_one", + "key_two": "value_two", + "key_three": "value_three", + }, + }, + After: map[string]interface{}{ + "map": map[string]interface{}{ + "key_one": "value_three", + "key_two": "value_seven", + "key_four": "value_four", + }, + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Paths: [][]interface{}{ + { + "map", + "key_one", + }, + { + "map", + "key_three", + }, + { + "map", + "key_four", + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "map": { + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "map": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "key_one": renderers.ValidatePrimitive("value_one", "value_three", plans.Update, false), + "key_two": renderers.ValidatePrimitive("value_two", "value_two", plans.NoOp, false), + "key_three": renderers.ValidatePrimitive("value_three", nil, plans.Delete, false), + "key_four": renderers.ValidatePrimitive(nil, "value_four", plans.Create, false), + }, plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + "elements_in_set": { + input: structured.Change{ + Before: map[string]interface{}{ + "set": []interface{}{ + json.Number("0"), json.Number("1"), json.Number("2"), json.Number("3"), json.Number("4"), + }, + }, + After: map[string]interface{}{ + "set": []interface{}{ + json.Number("0"), json.Number("2"), json.Number("4"), json.Number("5"), json.Number("6"), + }, + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Propagate: true, + Paths: [][]interface{}{ + { + "set", + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "set": { + AttributeType: unmarshalType(t, cty.Set(cty.Number)), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "set": renderers.ValidateSet([]renderers.ValidateDiffFunction{ + renderers.ValidatePrimitive(json.Number("0"), json.Number("0"), plans.NoOp, false), + renderers.ValidatePrimitive(json.Number("1"), nil, plans.Delete, false), + renderers.ValidatePrimitive(json.Number("2"), json.Number("2"), plans.NoOp, false), + renderers.ValidatePrimitive(json.Number("3"), nil, plans.Delete, false), + renderers.ValidatePrimitive(json.Number("4"), json.Number("4"), plans.NoOp, false), + renderers.ValidatePrimitive(nil, json.Number("5"), plans.Create, false), + renderers.ValidatePrimitive(nil, json.Number("6"), plans.Create, false), + }, plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + "dynamic_types": { + input: structured.Change{ + Before: map[string]interface{}{ + "dynamic_nested_type": map[string]interface{}{ + "nested_id": "nomatch", + "nested_object": map[string]interface{}{ + "nested_nested_id": "matched", + }, + }, + "dynamic_nested_type_match": map[string]interface{}{ + "nested_id": "allmatch", + "nested_object": map[string]interface{}{ + "nested_nested_id": "allmatch", + }, + }, + }, + After: map[string]interface{}{ + "dynamic_nested_type": map[string]interface{}{ + "nested_id": "nomatch_changed", + "nested_object": map[string]interface{}{ + "nested_nested_id": "matched", + }, + }, + "dynamic_nested_type_match": map[string]interface{}{ + "nested_id": "allmatch", + "nested_object": map[string]interface{}{ + "nested_nested_id": "allmatch", + }, + }, + }, + RelevantAttributes: &attribute_path.PathMatcher{ + Propagate: true, + Paths: [][]interface{}{ + { + "dynamic_nested_type", + "nested_object", + "nested_nested_id", + }, + { + "dynamic_nested_type_match", + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "dynamic_nested_type": { + AttributeType: unmarshalType(t, cty.DynamicPseudoType), + }, + "dynamic_nested_type_match": { + AttributeType: unmarshalType(t, cty.DynamicPseudoType), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "dynamic_nested_type": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "nested_id": renderers.ValidatePrimitive("nomatch", "nomatch", plans.NoOp, false), + "nested_object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "nested_nested_id": renderers.ValidatePrimitive("matched", "matched", plans.NoOp, false), + }, plans.NoOp, false), + }, plans.NoOp, false), + "dynamic_nested_type_match": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "nested_id": renderers.ValidatePrimitive("allmatch", "allmatch", plans.NoOp, false), + "nested_object": renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "nested_nested_id": renderers.ValidatePrimitive("allmatch", "allmatch", plans.NoOp, false), + }, plans.NoOp, false), + }, plans.NoOp, false), + }, nil, nil, nil, nil, plans.NoOp, false), + }, + } + for name, tc := range tcs { + if tc.input.ReplacePaths == nil { + tc.input.ReplacePaths = &attribute_path.PathMatcher{} + } + t.Run(name, func(t *testing.T) { + tc.validate(t, ComputeDiffForBlock(tc.input, tc.block)) + }) + } +} + +func TestDynamicPseudoType(t *testing.T) { + tcs := map[string]struct { + input structured.Change + validate renderers.ValidateDiffFunction + }{ + "after_sensitive_in_dynamic_type": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "key": "value", + }, + Unknown: false, + BeforeSensitive: false, + AfterSensitive: map[string]interface{}{ + "key": true, + }, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "key": renderers.ValidateSensitive(renderers.ValidatePrimitive(nil, "value", plans.Create, false), false, true, plans.Create, false), + }, plans.Create, false), + }, + "before_sensitive_in_dynamic_type": { + input: structured.Change{ + Before: map[string]interface{}{ + "key": "value", + }, + After: nil, + Unknown: false, + BeforeSensitive: map[string]interface{}{ + "key": true, + }, + AfterSensitive: false, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "key": renderers.ValidateSensitive(renderers.ValidatePrimitive("value", nil, plans.Delete, false), true, false, plans.Delete, false), + }, plans.Delete, false), + }, + "sensitive_in_dynamic_type": { + input: structured.Change{ + Before: map[string]interface{}{ + "key": "before", + }, + After: map[string]interface{}{ + "key": "after", + }, + Unknown: false, + BeforeSensitive: map[string]interface{}{ + "key": true, + }, + AfterSensitive: map[string]interface{}{ + "key": true, + }, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "key": renderers.ValidateSensitive(renderers.ValidatePrimitive("before", "after", plans.Update, false), true, true, plans.Update, false), + }, plans.Update, false), + }, + "create_unknown_in_dynamic_type": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{}, + Unknown: map[string]interface{}{ + "key": true, + }, + BeforeSensitive: false, + AfterSensitive: false, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "key": renderers.ValidateUnknown(nil, plans.Create, false), + }, plans.Create, false), + }, + "update_unknown_in_dynamic_type": { + input: structured.Change{ + Before: map[string]interface{}{ + "key": "before", + }, + After: map[string]interface{}{}, + Unknown: map[string]interface{}{ + "key": true, + }, + BeforeSensitive: false, + AfterSensitive: false, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + validate: renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "key": renderers.ValidateUnknown(renderers.ValidatePrimitive("before", nil, plans.Delete, false), plans.Update, false), + }, plans.Update, false), + }, + } + for key, tc := range tcs { + t.Run(key, func(t *testing.T) { + tc.validate(t, ComputeDiffForType(tc.input, cty.DynamicPseudoType)) + }) + } +} + +func TestSpecificCases(t *testing.T) { + // This is a special test that can contain any combination of individual + // cases and will execute against them. For testing/fixing specific issues + // you can generally put the test case in here. + tcs := map[string]struct { + input structured.Change + block *jsonprovider.Block + validate renderers.ValidateDiffFunction + }{ + "issues/33016/unknown": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "triggers": map[string]interface{}{}, + }, + Unknown: map[string]interface{}{ + "id": true, + "triggers": map[string]interface{}{ + "rotation": true, + }, + }, + BeforeSensitive: false, + AfterSensitive: map[string]interface{}{ + "triggers": map[string]interface{}{}, + }, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "id": { + AttributeType: unmarshalType(t, cty.String), + }, + "triggers": { + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "id": renderers.ValidateUnknown(nil, plans.Create, false), + "triggers": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "rotation": renderers.ValidateUnknown(nil, plans.Create, false), + }, plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, + "issues/33016/null": { + input: structured.Change{ + Before: nil, + After: map[string]interface{}{ + "triggers": map[string]interface{}{ + "rotation": nil, + }, + }, + Unknown: map[string]interface{}{ + "id": true, + "triggers": map[string]interface{}{}, + }, + BeforeSensitive: false, + AfterSensitive: map[string]interface{}{ + "triggers": map[string]interface{}{}, + }, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "id": { + AttributeType: unmarshalType(t, cty.String), + }, + "triggers": { + AttributeType: unmarshalType(t, cty.Map(cty.String)), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "id": renderers.ValidateUnknown(nil, plans.Create, false), + "triggers": renderers.ValidateMap(map[string]renderers.ValidateDiffFunction{ + "rotation": renderers.ValidatePrimitive(nil, nil, plans.Create, false), + }, plans.Create, false), + }, nil, nil, nil, nil, plans.Create, false), + }, + + // The following tests are from issue 33472. Basically Terraform allows + // callers to treat numbers as strings in references and expects us + // to coerce the strings into numbers. For example the following are + // equivalent. + // - test_resource.resource.list[0].attribute + // - test_resource.resource.list["0"].attribute + // + // We need our attribute_path package (used within the ReplacePaths and + // RelevantAttributes fields) to handle coercing strings into numbers + // when it's expected. + + "issues/33472/expected": { + input: structured.Change{ + Before: map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "number": json.Number("-1"), + }, + }, + }, + After: map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "number": json.Number("2"), + }, + }, + }, + Unknown: false, + BeforeSensitive: false, + AfterSensitive: false, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: &attribute_path.PathMatcher{ + Propagate: true, + Paths: [][]interface{}{ + { + "list", + 0.0, // This is normal and expected so easy case. + "number", + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "list": { + AttributeType: unmarshalType(t, cty.List(cty.Object(map[string]cty.Type{ + "number": cty.Number, + }))), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "list": renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "number": renderers.ValidatePrimitive(json.Number("-1"), json.Number("2"), plans.Update, false), + }, plans.Update, false), + }, plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + + "issues/33472/coerce": { + input: structured.Change{ + Before: map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "number": json.Number("-1"), + }, + }, + }, + After: map[string]interface{}{ + "list": []interface{}{ + map[string]interface{}{ + "number": json.Number("2"), + }, + }, + }, + Unknown: false, + BeforeSensitive: false, + AfterSensitive: false, + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: &attribute_path.PathMatcher{ + Propagate: true, + Paths: [][]interface{}{ + { + "list", + "0", // Difficult but allowed, we need to handle this. + "number", + }, + }, + }, + }, + block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "list": { + AttributeType: unmarshalType(t, cty.List(cty.Object(map[string]cty.Type{ + "number": cty.Number, + }))), + }, + }, + }, + validate: renderers.ValidateBlock(map[string]renderers.ValidateDiffFunction{ + "list": renderers.ValidateList([]renderers.ValidateDiffFunction{ + renderers.ValidateObject(map[string]renderers.ValidateDiffFunction{ + "number": renderers.ValidatePrimitive(json.Number("-1"), json.Number("2"), plans.Update, false), + }, plans.Update, false), + }, plans.Update, false), + }, nil, nil, nil, nil, plans.Update, false), + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + tc.validate(t, ComputeDiffForBlock(tc.input, tc.block)) + }) + } +} + +// unmarshalType converts a cty.Type into a json.RawMessage understood by the +// schema. It also lets the testing framework handle any errors to keep the API +// clean. +func unmarshalType(t *testing.T, ctyType cty.Type) json.RawMessage { + msg, err := ctyjson.MarshalType(ctyType) + if err != nil { + t.Fatalf("invalid type: %s", ctyType.FriendlyName()) + } + return msg +} + +// wrapChangeInSlice does the same as wrapChangeInMap, except it wraps it into a +// slice internally. +func wrapChangeInSlice(input structured.Change) structured.Change { + return wrapChange(input, float64(0), func(value interface{}, unknown interface{}, explicit bool) interface{} { + switch value.(type) { + case nil: + if set, ok := unknown.(bool); (set && ok) || explicit { + return []interface{}{nil} + + } + return []interface{}{} + default: + return []interface{}{value} + } + }) +} + +// wrapChangeInMap access a single structured.Change and returns a new +// structured.Change that represents a map with a single element. That single +// element is the input value. +func wrapChangeInMap(input structured.Change) structured.Change { + return wrapChange(input, "element", func(value interface{}, unknown interface{}, explicit bool) interface{} { + switch value.(type) { + case nil: + if set, ok := unknown.(bool); (set && ok) || explicit { + return map[string]interface{}{ + "element": nil, + } + } + return map[string]interface{}{} + default: + return map[string]interface{}{ + "element": value, + } + } + }) +} + +func wrapChange(input structured.Change, step interface{}, wrap func(interface{}, interface{}, bool) interface{}) structured.Change { + + replacePaths := &attribute_path.PathMatcher{} + for _, path := range input.ReplacePaths.(*attribute_path.PathMatcher).Paths { + var updated []interface{} + updated = append(updated, step) + updated = append(updated, path...) + replacePaths.Paths = append(replacePaths.Paths, updated) + } + + // relevantAttributes usually default to AlwaysMatcher, which means we can + // just ignore it. But if we have had some paths specified we need to wrap + // those as well. + relevantAttributes := input.RelevantAttributes + if concrete, ok := relevantAttributes.(*attribute_path.PathMatcher); ok { + + newRelevantAttributes := &attribute_path.PathMatcher{} + for _, path := range concrete.Paths { + var updated []interface{} + updated = append(updated, step) + updated = append(updated, path...) + newRelevantAttributes.Paths = append(newRelevantAttributes.Paths, updated) + } + relevantAttributes = newRelevantAttributes + } + + return structured.Change{ + Before: wrap(input.Before, nil, input.BeforeExplicit), + After: wrap(input.After, input.Unknown, input.AfterExplicit), + Unknown: wrap(input.Unknown, nil, false), + BeforeSensitive: wrap(input.BeforeSensitive, nil, false), + AfterSensitive: wrap(input.AfterSensitive, nil, false), + ReplacePaths: replacePaths, + RelevantAttributes: relevantAttributes, + } +} diff --git a/internal/command/jsonformat/differ/list.go b/internal/command/jsonformat/differ/list.go new file mode 100644 index 0000000000..78d003a743 --- /dev/null +++ b/internal/command/jsonformat/differ/list.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +func computeAttributeDiffAsList(change structured.Change, elementType cty.Type) computed.Diff { + sliceValue := change.AsSlice() + + processIndices := func(beforeIx, afterIx int) computed.Diff { + value := sliceValue.GetChild(beforeIx, afterIx) + + // It's actually really difficult to render the diffs when some indices + // within a slice are relevant and others aren't. To make this simpler + // we just treat all children of a relevant list or set as also + // relevant. + // + // Interestingly the terraform plan builder also agrees with this, and + // never sets relevant attributes beneath lists or sets. We're just + // going to enforce this logic here as well. If the collection is + // relevant (decided elsewhere), then every element in the collection is + // also relevant. To be clear, in practice even if we didn't do the + // following explicitly the effect would be the same. It's just nicer + // for us to be clear about the behaviour we expect. + // + // What makes this difficult is the fact that the beforeIx and afterIx + // can be different, and it's quite difficult to work out which one is + // the relevant one. For nested lists, block lists, and tuples it's much + // easier because we always process the same indices in the before and + // after. + value.RelevantAttributes = attribute_path.AlwaysMatcher() + + return ComputeDiffForType(value, elementType) + } + + isObjType := func(_ interface{}) bool { + return elementType.IsObjectType() + } + + elements, current := collections.TransformSlice(sliceValue.Before, sliceValue.After, processIndices, isObjType) + return computed.NewDiff(renderers.List(elements), current, change.ReplacePaths.Matches()) +} + +func computeAttributeDiffAsNestedList(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff { + var elements []computed.Diff + current := change.GetDefaultActionForIteration() + processNestedList(change, func(value structured.Change) { + element := computeDiffForNestedAttribute(value, &jsonprovider.NestedType{ + Attributes: attributes, + NestingMode: "single", + }) + elements = append(elements, element) + current = collections.CompareActions(current, element.Action) + }) + return computed.NewDiff(renderers.NestedList(elements), current, change.ReplacePaths.Matches()) +} + +func computeBlockDiffsAsList(change structured.Change, block *jsonprovider.Block) ([]computed.Diff, plans.Action) { + var elements []computed.Diff + current := change.GetDefaultActionForIteration() + processNestedList(change, func(value structured.Change) { + element := ComputeDiffForBlock(value, block) + elements = append(elements, element) + current = collections.CompareActions(current, element.Action) + }) + return elements, current +} + +func processNestedList(change structured.Change, process func(value structured.Change)) { + sliceValue := change.AsSlice() + for ix := 0; ix < len(sliceValue.Before) || ix < len(sliceValue.After); ix++ { + value := sliceValue.GetChild(ix, ix) + if !value.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + value = value.AsNoOp() + } + process(value) + } +} diff --git a/internal/command/jsonformat/differ/map.go b/internal/command/jsonformat/differ/map.go new file mode 100644 index 0000000000..0c3dc12bf0 --- /dev/null +++ b/internal/command/jsonformat/differ/map.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +func computeAttributeDiffAsMap(change structured.Change, elementType cty.Type) computed.Diff { + mapValue := change.AsMap() + elements, current := collections.TransformMap(mapValue.Before, mapValue.After, mapValue.AllKeys(), func(key string) computed.Diff { + value := mapValue.GetChild(key) + if !value.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + value = value.AsNoOp() + } + return ComputeDiffForType(value, elementType) + }) + return computed.NewDiff(renderers.Map(elements), current, change.ReplacePaths.Matches()) +} + +func computeAttributeDiffAsNestedMap(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff { + mapValue := change.AsMap() + elements, current := collections.TransformMap(mapValue.Before, mapValue.After, mapValue.ExplicitKeys(), func(key string) computed.Diff { + value := mapValue.GetChild(key) + if !value.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + value = value.AsNoOp() + } + return computeDiffForNestedAttribute(value, &jsonprovider.NestedType{ + Attributes: attributes, + NestingMode: "single", + }) + }) + return computed.NewDiff(renderers.NestedMap(elements), current, change.ReplacePaths.Matches()) +} + +func computeBlockDiffsAsMap(change structured.Change, block *jsonprovider.Block) (map[string]computed.Diff, plans.Action) { + mapValue := change.AsMap() + return collections.TransformMap(mapValue.Before, mapValue.After, mapValue.ExplicitKeys(), func(key string) computed.Diff { + value := mapValue.GetChild(key) + if !value.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + value = value.AsNoOp() + } + return ComputeDiffForBlock(value, block) + }) +} diff --git a/internal/command/jsonformat/differ/object.go b/internal/command/jsonformat/differ/object.go new file mode 100644 index 0000000000..6ca44c59e0 --- /dev/null +++ b/internal/command/jsonformat/differ/object.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +func computeAttributeDiffAsObject(change structured.Change, attributes map[string]cty.Type) computed.Diff { + attributeDiffs, action := processObject(change, attributes, nil, func(value structured.Change, ctype cty.Type, _ plans.Action) computed.Diff { + return ComputeDiffForType(value, ctype) + }) + return computed.NewDiff(renderers.Object(attributeDiffs), action, change.ReplacePaths.Matches()) +} + +func computeAttributeDiffAsNestedObject(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff { + + otherAttributes := make(map[string]*jsonprovider.Attribute) + writeOnlyAttributes := make(map[string]*jsonprovider.Attribute) + for key, attr := range attributes { + if attr.WriteOnly { + writeOnlyAttributes[key] = attr + } else { + otherAttributes[key] = attr + } + } + + attributeDiffs, action := processObject(change, otherAttributes, writeOnlyAttributes, func(value structured.Change, attribute *jsonprovider.Attribute, currentAction plans.Action) computed.Diff { + if attribute.WriteOnly { + return computeDiffForWriteOnlyAttribute(value, currentAction) + } + return ComputeDiffForAttribute(value, attribute) + }) + return computed.NewDiff(renderers.NestedObject(attributeDiffs), action, change.ReplacePaths.Matches()) +} + +// processObject steps through the children of value as if it is an object and +// calls out to the provided computeDiff function once it has collated the +// diffs for each child attribute. +// +// We have to make this generic as attributes and nested objects process either +// cty.Type or jsonprovider.Attribute children respectively. And we want to +// reuse as much code as possible. +// +// Also, as it generic we cannot make this function a method on Change as you +// can't create generic methods on structs. Instead, we make this a generic +// function that receives the value as an argument. +func processObject[T any](v structured.Change, attributes map[string]T, writeOnlyAttributes map[string]T, computeDiff func(structured.Change, T, plans.Action) computed.Diff) (map[string]computed.Diff, plans.Action) { + attributeDiffs := make(map[string]computed.Diff) + mapValue := v.AsMap() + + currentAction := v.GetDefaultActionForIteration() + for key, attribute := range attributes { + attributeValue := mapValue.GetChild(key) + + if !attributeValue.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + attributeValue = attributeValue.AsNoOp() + } + + // We always assume changes to object are implicit. + attributeValue.BeforeExplicit = false + attributeValue.AfterExplicit = false + + attributeDiff := computeDiff(attributeValue, attribute, currentAction) + if attributeDiff.Action == plans.NoOp && attributeValue.Before == nil && attributeValue.After == nil { + // We skip attributes of objects that are null both before and + // after. We don't even count these as unchanged attributes. + continue + } + attributeDiffs[key] = attributeDiff + currentAction = collections.CompareActions(currentAction, attributeDiff.Action) + } + for key, attribute := range writeOnlyAttributes { + attributeValue := mapValue.GetChild(key) + + if !attributeValue.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + attributeValue = attributeValue.AsNoOp() + } + + // We always assume changes to object are implicit. + attributeValue.BeforeExplicit = false + attributeValue.AfterExplicit = false + + attributeDiff := computeDiff(attributeValue, attribute, currentAction) + attributeDiffs[key] = attributeDiff + } + + return attributeDiffs, currentAction +} diff --git a/internal/command/jsonformat/differ/output.go b/internal/command/jsonformat/differ/output.go new file mode 100644 index 0000000000..07b44d27f4 --- /dev/null +++ b/internal/command/jsonformat/differ/output.go @@ -0,0 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" +) + +func ComputeDiffForOutput(change structured.Change) computed.Diff { + if sensitive, ok := checkForSensitiveType(change, cty.DynamicPseudoType); ok { + return sensitive + } + + if unknown, ok := checkForUnknownType(change, cty.DynamicPseudoType); ok { + return unknown + } + + jsonOpts := renderers.RendererJsonOpts() + return jsonOpts.Transform(change) +} diff --git a/internal/command/jsonformat/differ/primitive.go b/internal/command/jsonformat/differ/primitive.go new file mode 100644 index 0000000000..be97f5a597 --- /dev/null +++ b/internal/command/jsonformat/differ/primitive.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" +) + +func computeAttributeDiffAsPrimitive(change structured.Change, ctype cty.Type) computed.Diff { + return asDiff(change, renderers.Primitive(change.Before, change.After, ctype)) +} diff --git a/internal/command/jsonformat/differ/sensitive.go b/internal/command/jsonformat/differ/sensitive.go new file mode 100644 index 0000000000..ac241d5414 --- /dev/null +++ b/internal/command/jsonformat/differ/sensitive.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +type CreateSensitiveRenderer func(computed.Diff, bool, bool) computed.DiffRenderer + +func checkForSensitiveType(change structured.Change, ctype cty.Type) (computed.Diff, bool) { + return change.CheckForSensitive( + func(value structured.Change) computed.Diff { + return ComputeDiffForType(value, ctype) + }, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff { + return computed.NewDiff(renderers.Sensitive(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.Matches()) + }, + ) +} + +func checkForSensitiveNestedAttribute(change structured.Change, attribute *jsonprovider.NestedType) (computed.Diff, bool) { + return change.CheckForSensitive( + func(value structured.Change) computed.Diff { + return computeDiffForNestedAttribute(value, attribute) + }, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff { + return computed.NewDiff(renderers.Sensitive(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.Matches()) + }, + ) +} + +func checkForSensitiveBlock(change structured.Change, block *jsonprovider.Block) (computed.Diff, bool) { + return change.CheckForSensitive( + func(value structured.Change) computed.Diff { + return ComputeDiffForBlock(value, block) + }, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff { + return computed.NewDiff(renderers.SensitiveBlock(inner, beforeSensitive, afterSensitive), action, change.ReplacePaths.Matches()) + }, + ) +} diff --git a/internal/command/jsonformat/differ/set.go b/internal/command/jsonformat/differ/set.go new file mode 100644 index 0000000000..13ae568981 --- /dev/null +++ b/internal/command/jsonformat/differ/set.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "reflect" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/plans" +) + +func computeAttributeDiffAsSet(change structured.Change, elementType cty.Type) computed.Diff { + var elements []computed.Diff + current := change.GetDefaultActionForIteration() + processSet(change, func(value structured.Change) { + element := ComputeDiffForType(value, elementType) + elements = append(elements, element) + current = collections.CompareActions(current, element.Action) + }) + return computed.NewDiff(renderers.Set(elements), current, change.ReplacePaths.Matches()) +} + +func computeAttributeDiffAsNestedSet(change structured.Change, attributes map[string]*jsonprovider.Attribute) computed.Diff { + var elements []computed.Diff + current := change.GetDefaultActionForIteration() + processSet(change, func(value structured.Change) { + element := computeDiffForNestedAttribute(value, &jsonprovider.NestedType{ + Attributes: attributes, + NestingMode: "single", + }) + elements = append(elements, element) + current = collections.CompareActions(current, element.Action) + }) + return computed.NewDiff(renderers.NestedSet(elements), current, change.ReplacePaths.Matches()) +} + +func computeBlockDiffsAsSet(change structured.Change, block *jsonprovider.Block) ([]computed.Diff, plans.Action) { + var elements []computed.Diff + current := change.GetDefaultActionForIteration() + processSet(change, func(value structured.Change) { + element := ComputeDiffForBlock(value, block) + elements = append(elements, element) + current = collections.CompareActions(current, element.Action) + }) + return elements, current +} + +func processSet(change structured.Change, process func(value structured.Change)) { + sliceValue := change.AsSlice() + + foundInBefore := make(map[int]int) + foundInAfter := make(map[int]int) + + // O(n^2) operation here to find matching pairs in the set, so we can make + // the display look pretty. There might be a better way to do this, so look + // here for potential optimisations. + + for ix := 0; ix < len(sliceValue.Before); ix++ { + matched := false + for jx := 0; jx < len(sliceValue.After); jx++ { + if _, ok := foundInAfter[jx]; ok { + // We've already found a match for this after value. + continue + } + + child := sliceValue.GetChild(ix, jx) + if reflect.DeepEqual(child.Before, child.After) && child.IsBeforeSensitive() == child.IsAfterSensitive() && !child.IsUnknown() { + matched = true + foundInBefore[ix] = jx + foundInAfter[jx] = ix + } + } + + if !matched { + foundInBefore[ix] = -1 + } + } + + clearRelevantStatus := func(change structured.Change) structured.Change { + // It's actually really difficult to render the diffs when some indices + // within a slice are relevant and others aren't. To make this simpler + // we just treat all children of a relevant list or set as also + // relevant. + // + // Interestingly the terraform plan builder also agrees with this, and + // never sets relevant attributes beneath lists or sets. We're just + // going to enforce this logic here as well. If the collection is + // relevant (decided elsewhere), then every element in the collection is + // also relevant. To be clear, in practice even if we didn't do the + // following explicitly the effect would be the same. It's just nicer + // for us to be clear about the behaviour we expect. + // + // What makes this difficult is the fact that the beforeIx and afterIx + // can be different, and it's quite difficult to work out which one is + // the relevant one. For nested lists, block lists, and tuples it's much + // easier because we always process the same indices in the before and + // after. + change.RelevantAttributes = attribute_path.AlwaysMatcher() + return change + } + + // Now everything in before should be a key in foundInBefore and a value + // in foundInAfter. If a key is mapped to -1 in foundInBefore it means it + // does not have an equivalent in foundInAfter and so has been deleted. + // Everything in foundInAfter has a matching value in foundInBefore, but + // some values in after may not be in foundInAfter. This means these values + // are newly created. + + for ix := 0; ix < len(sliceValue.Before); ix++ { + if jx := foundInBefore[ix]; jx >= 0 { + child := clearRelevantStatus(sliceValue.GetChild(ix, jx)) + process(child) + continue + } + child := clearRelevantStatus(sliceValue.GetChild(ix, len(sliceValue.After))) + process(child) + } + + for jx := 0; jx < len(sliceValue.After); jx++ { + if _, ok := foundInAfter[jx]; ok { + // Then this value was handled in the previous for loop. + continue + } + child := clearRelevantStatus(sliceValue.GetChild(len(sliceValue.Before), jx)) + process(child) + } +} diff --git a/internal/command/jsonformat/differ/tuple.go b/internal/command/jsonformat/differ/tuple.go new file mode 100644 index 0000000000..700fea8f4d --- /dev/null +++ b/internal/command/jsonformat/differ/tuple.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" +) + +func computeAttributeDiffAsTuple(change structured.Change, elementTypes []cty.Type) computed.Diff { + var elements []computed.Diff + current := change.GetDefaultActionForIteration() + sliceValue := change.AsSlice() + for ix, elementType := range elementTypes { + childValue := sliceValue.GetChild(ix, ix) + if !childValue.RelevantAttributes.MatchesPartial() { + // Mark non-relevant attributes as unchanged. + childValue = childValue.AsNoOp() + } + element := ComputeDiffForType(childValue, elementType) + elements = append(elements, element) + current = collections.CompareActions(current, element.Action) + } + return computed.NewDiff(renderers.List(elements), current, change.ReplacePaths.Matches()) +} diff --git a/internal/command/jsonformat/differ/types.go b/internal/command/jsonformat/differ/types.go new file mode 100644 index 0000000000..374c5c1163 --- /dev/null +++ b/internal/command/jsonformat/differ/types.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +// NestingMode is a wrapper around a string type to describe the various +// different kinds of nesting modes that can be applied to nested blocks and +// objects. +type NestingMode string + +const ( + nestingModeSet NestingMode = "set" + nestingModeList NestingMode = "list" + nestingModeMap NestingMode = "map" + nestingModeSingle NestingMode = "single" + nestingModeGroup NestingMode = "group" +) diff --git a/internal/command/jsonformat/differ/unknown.go b/internal/command/jsonformat/differ/unknown.go new file mode 100644 index 0000000000..3e1bdcd727 --- /dev/null +++ b/internal/command/jsonformat/differ/unknown.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package differ + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonprovider" +) + +func checkForUnknownType(change structured.Change, ctype cty.Type) (computed.Diff, bool) { + return change.CheckForUnknown( + false, + processUnknown, + createProcessUnknownWithBefore(func(value structured.Change) computed.Diff { + return ComputeDiffForType(value, ctype) + })) +} + +func checkForUnknownNestedAttribute(change structured.Change, attribute *jsonprovider.NestedType) (computed.Diff, bool) { + + // We want our child attributes to show up as computed instead of deleted. + // Let's populate that here. + childUnknown := make(map[string]interface{}) + for key := range attribute.Attributes { + childUnknown[key] = true + } + + return change.CheckForUnknown( + childUnknown, + processUnknown, + createProcessUnknownWithBefore(func(value structured.Change) computed.Diff { + return computeDiffForNestedAttribute(value, attribute) + })) +} + +func checkForUnknownBlock(change structured.Change, block *jsonprovider.Block) (computed.Diff, bool) { + + // We want our child attributes to show up as computed instead of deleted. + // Let's populate that here. + childUnknown := make(map[string]interface{}) + for key := range block.Attributes { + childUnknown[key] = true + } + + return change.CheckForUnknown( + childUnknown, + processUnknown, + createProcessUnknownWithBefore(func(value structured.Change) computed.Diff { + return ComputeDiffForBlock(value, block) + })) +} + +func processUnknown(current structured.Change) computed.Diff { + return asDiff(current, renderers.Unknown(computed.Diff{})) +} + +func createProcessUnknownWithBefore(computeDiff func(value structured.Change) computed.Diff) structured.ProcessUnknownWithBefore { + return func(current structured.Change, before structured.Change) computed.Diff { + return asDiff(current, renderers.Unknown(computeDiff(before))) + } +} diff --git a/internal/command/jsonformat/jsondiff/diff.go b/internal/command/jsonformat/jsondiff/diff.go new file mode 100644 index 0000000000..8401b3575b --- /dev/null +++ b/internal/command/jsonformat/jsondiff/diff.go @@ -0,0 +1,151 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsondiff + +import ( + "reflect" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/command/jsonformat/collections" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/plans" +) + +type TransformPrimitiveJson func(before, after interface{}, ctype cty.Type, action plans.Action) computed.Diff +type TransformObjectJson func(map[string]computed.Diff, plans.Action) computed.Diff +type TransformArrayJson func([]computed.Diff, plans.Action) computed.Diff +type TransformUnknownJson func(computed.Diff, plans.Action) computed.Diff +type TransformSensitiveJson func(computed.Diff, bool, bool, plans.Action) computed.Diff +type TransformTypeChangeJson func(before, after computed.Diff, action plans.Action) computed.Diff + +// JsonOpts defines the external callback functions that callers should +// implement to process the supplied diffs. +type JsonOpts struct { + Primitive TransformPrimitiveJson + Object TransformObjectJson + Array TransformArrayJson + Unknown TransformUnknownJson + Sensitive TransformSensitiveJson + TypeChange TransformTypeChangeJson +} + +// Transform accepts a generic before and after value that is assumed to be JSON +// formatted and transforms it into a computed.Diff, using the callbacks +// supplied in the JsonOpts class. +func (opts JsonOpts) Transform(change structured.Change) computed.Diff { + if sensitive, ok := opts.processSensitive(change); ok { + return sensitive + } + + if unknown, ok := opts.processUnknown(change); ok { + return unknown + } + + beforeType := GetType(change.Before) + afterType := GetType(change.After) + + deleted := afterType == Null && !change.AfterExplicit + created := beforeType == Null && !change.BeforeExplicit + + if beforeType == afterType || (created || deleted) { + targetType := beforeType + if targetType == Null { + targetType = afterType + } + return opts.processUpdate(change, targetType) + } + + b := opts.processUpdate(change.AsDelete(), beforeType) + a := opts.processUpdate(change.AsCreate(), afterType) + return opts.TypeChange(b, a, plans.Update) +} + +func (opts JsonOpts) processUpdate(change structured.Change, jtype Type) computed.Diff { + switch jtype { + case Null: + return opts.processPrimitive(change, cty.NilType) + case Bool: + return opts.processPrimitive(change, cty.Bool) + case String: + return opts.processPrimitive(change, cty.String) + case Number: + return opts.processPrimitive(change, cty.Number) + case Object: + return opts.processObject(change.AsMap()) + case Array: + return opts.processArray(change.AsSlice()) + default: + panic("unrecognized json type: " + jtype) + } +} + +func (opts JsonOpts) processPrimitive(change structured.Change, ctype cty.Type) computed.Diff { + beforeMissing := change.Before == nil && !change.BeforeExplicit + afterMissing := change.After == nil && !change.AfterExplicit + + var action plans.Action + switch { + case beforeMissing && !afterMissing: + action = plans.Create + case !beforeMissing && afterMissing: + action = plans.Delete + case reflect.DeepEqual(change.Before, change.After): + action = plans.NoOp + default: + action = plans.Update + } + + return opts.Primitive(change.Before, change.After, ctype, action) +} + +func (opts JsonOpts) processArray(change structured.ChangeSlice) computed.Diff { + processIndices := func(beforeIx, afterIx int) computed.Diff { + // It's actually really difficult to render the diffs when some indices + // within a list are relevant and others aren't. To make this simpler + // we just treat all children of a relevant list as also relevant, so we + // ignore the relevant attributes field. + // + // Interestingly the terraform plan builder also agrees with this, and + // never sets relevant attributes beneath lists or sets. We're just + // going to enforce this logic here as well. If the list is relevant + // (decided elsewhere), then every element in the list is also relevant. + return opts.Transform(change.GetChild(beforeIx, afterIx)) + } + + isObjType := func(value interface{}) bool { + return GetType(value) == Object + } + + return opts.Array(collections.TransformSlice(change.Before, change.After, processIndices, isObjType)) +} + +func (opts JsonOpts) processObject(change structured.ChangeMap) computed.Diff { + return opts.Object(collections.TransformMap(change.Before, change.After, change.AllKeys(), func(key string) computed.Diff { + child := change.GetChild(key) + if !child.RelevantAttributes.MatchesPartial() { + child = child.AsNoOp() + } + + return opts.Transform(child) + })) +} + +func (opts JsonOpts) processUnknown(change structured.Change) (computed.Diff, bool) { + return change.CheckForUnknown( + false, + func(current structured.Change) computed.Diff { + return opts.Unknown(computed.Diff{}, plans.Create) + }, func(current structured.Change, before structured.Change) computed.Diff { + return opts.Unknown(opts.Transform(before), plans.Update) + }, + ) +} + +func (opts JsonOpts) processSensitive(change structured.Change) (computed.Diff, bool) { + return change.CheckForSensitive(opts.Transform, func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff { + return opts.Sensitive(inner, beforeSensitive, afterSensitive, action) + }) +} diff --git a/internal/command/jsonformat/jsondiff/types.go b/internal/command/jsonformat/jsondiff/types.go new file mode 100644 index 0000000000..52d959e7c7 --- /dev/null +++ b/internal/command/jsonformat/jsondiff/types.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsondiff + +import ( + "encoding/json" + "fmt" +) + +type Type string + +const ( + Number Type = "number" + Object Type = "object" + Array Type = "array" + Bool Type = "bool" + String Type = "string" + Null Type = "null" +) + +func GetType(value interface{}) Type { + switch value.(type) { + case []interface{}: + return Array + case json.Number: + return Number + case string: + return String + case bool: + return Bool + case nil: + return Null + case map[string]interface{}: + return Object + default: + panic(fmt.Sprintf("unrecognized json type %T", value)) + } +} diff --git a/internal/command/jsonformat/plan.go b/internal/command/jsonformat/plan.go new file mode 100644 index 0000000000..7eb54899d1 --- /dev/null +++ b/internal/command/jsonformat/plan.go @@ -0,0 +1,616 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat + +import ( + "bytes" + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed/renderers" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" + "github.com/hashicorp/terraform/internal/plans" +) + +const ( + detectedDrift string = "drift" + proposedChange string = "change" +) + +type Plan struct { + PlanFormatVersion string `json:"plan_format_version"` + OutputChanges map[string]jsonplan.Change `json:"output_changes,omitempty"` + ResourceChanges []jsonplan.ResourceChange `json:"resource_changes,omitempty"` + ResourceDrift []jsonplan.ResourceChange `json:"resource_drift,omitempty"` + RelevantAttributes []jsonplan.ResourceAttr `json:"relevant_attributes,omitempty"` + DeferredChanges []jsonplan.DeferredResourceChange `json:"deferred_changes,omitempty"` + + ProviderFormatVersion string `json:"provider_format_version"` + ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas,omitempty"` +} + +func (plan Plan) getSchema(change jsonplan.ResourceChange) *jsonprovider.Schema { + switch change.Mode { + case jsonstate.ManagedResourceMode: + return plan.ProviderSchemas[change.ProviderName].ResourceSchemas[change.Type] + case jsonstate.DataResourceMode: + return plan.ProviderSchemas[change.ProviderName].DataSourceSchemas[change.Type] + default: + panic("found unrecognized resource mode: " + change.Mode) + } +} + +func (plan Plan) renderHuman(renderer Renderer, mode plans.Mode, opts ...plans.Quality) { + checkOpts := func(target plans.Quality) bool { + for _, opt := range opts { + if opt == target { + return true + } + } + return false + } + + diffs := precomputeDiffs(plan, mode) + haveRefreshChanges := renderHumanDiffDrift(renderer, diffs, mode) + + willPrintResourceChanges := false + counts := make(map[plans.Action]int) + importingCount := 0 + var changes []diff + for _, diff := range diffs.changes { + action := jsonplan.UnmarshalActions(diff.change.Change.Actions) + if action == plans.NoOp && !diff.Moved() && !diff.Importing() { + // Don't show anything for NoOp changes. + continue + } + if action == plans.Delete && diff.change.Mode != jsonstate.ManagedResourceMode { + // Don't render anything for deleted data sources. + continue + } + + changes = append(changes, diff) + + if diff.Importing() { + importingCount++ + } + + // Don't count move-only changes + if action != plans.NoOp { + willPrintResourceChanges = true + counts[action]++ + } + } + + // Precompute the outputs early, so we can make a decision about whether we + // display the "there are no changes messages". + outputs := renderHumanDiffOutputs(renderer, diffs.outputs) + + if len(changes) == 0 && len(outputs) == 0 { + // If we didn't find any changes to report at all then this is a + // "No changes" plan. How we'll present this depends on whether + // the plan is "applyable" and, if so, whether it had refresh changes + // that we already would've presented above. + + if checkOpts(plans.Errored) { + if haveRefreshChanges { + renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) + renderer.Streams.Println() + } + renderer.Streams.Print( + renderer.Colorize.Color("\n[reset][bold][red]Planning failed.[reset][bold] Terraform encountered an error while generating this plan.[reset]\n\n"), + ) + } else if len(diffs.deferred) > 0 { + // We had no current changes, but deferred changes + if haveRefreshChanges { + renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) + renderer.Streams.Println("") + } + renderer.Streams.Print( + renderer.Colorize.Color("\n[reset][bold][green]No current changes.[reset][bold] This plan requires another plan to be applied first.[reset]\n\n"), + ) + } else { + switch mode { + case plans.RefreshOnlyMode: + if haveRefreshChanges { + // We already generated a sufficient prompt about what will + // happen if applying this change above, so we don't need to + // say anything more. + return + } + + renderer.Streams.Print(renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n")) + renderer.Streams.Println(format.WordWrap( + "Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.", + renderer.Streams.Stdout.Columns())) + case plans.DestroyMode: + if haveRefreshChanges { + renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) + fmt.Fprintln(renderer.Streams.Stdout.File) + } + renderer.Streams.Print(renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n")) + renderer.Streams.Println(format.WordWrap( + "Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.", + renderer.Streams.Stdout.Columns())) + default: + if haveRefreshChanges { + renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) + renderer.Streams.Println("") + } + renderer.Streams.Print( + renderer.Colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"), + ) + + if haveRefreshChanges { + if !checkOpts(plans.NoChanges) { + // In this case, applying this plan will not change any + // remote objects but _will_ update the state to match what + // we detected during refresh, so we'll reassure the user + // about that. + renderer.Streams.Println(format.WordWrap( + "Your configuration already matches the changes detected above, so applying this plan will only update the state to include the changes detected above and won't change any real infrastructure.", + renderer.Streams.Stdout.Columns(), + )) + } else { + // In this case we detected changes during refresh but this isn't + // a planning mode where we consider those to be applyable. The + // user must re-run in refresh-only mode in order to update the + // state to match the upstream changes. + suggestion := "." + if !renderer.RunningInAutomation { + // The normal message includes a specific command line to run. + suggestion = ":\n terraform apply -refresh-only" + } + renderer.Streams.Println(format.WordWrap( + "Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan"+suggestion, + renderer.Streams.Stdout.Columns(), + )) + } + return + } + + // If we get down here then we're just in the simple situation where + // the plan isn't applyable at all. + renderer.Streams.Println(format.WordWrap( + "Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.", + renderer.Streams.Stdout.Columns(), + )) + } + } + } + + if haveRefreshChanges { + renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) + renderer.Streams.Println() + } + + haveDeferredChanges := renderHumanDeferredChanges(renderer, diffs, mode) + if haveDeferredChanges { + renderer.Streams.Print(format.HorizontalRule(renderer.Colorize, renderer.Streams.Stdout.Columns())) + renderer.Streams.Println() + } + + if willPrintResourceChanges { + renderer.Streams.Println(format.WordWrap( + "\nTerraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:", + renderer.Streams.Stdout.Columns())) + if counts[plans.Create] > 0 { + renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Create))) + } + if counts[plans.Update] > 0 { + renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Update))) + } + if counts[plans.Delete] > 0 { + renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Delete))) + } + if counts[plans.DeleteThenCreate] > 0 { + renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.DeleteThenCreate))) + } + if counts[plans.CreateThenDelete] > 0 { + renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.CreateThenDelete))) + } + if counts[plans.Read] > 0 { + renderer.Streams.Println(renderer.Colorize.Color(actionDescription(plans.Read))) + } + } + + if len(changes) > 0 { + if checkOpts(plans.Errored) { + renderer.Streams.Printf("\nTerraform planned the following actions, but then encountered a problem:\n") + } else { + renderer.Streams.Printf("\nTerraform will perform the following actions:\n") + } + + for _, change := range changes { + diff, render := renderHumanDiff(renderer, change, proposedChange) + if render { + fmt.Fprintln(renderer.Streams.Stdout.File) + renderer.Streams.Println(diff) + } + } + + if importingCount > 0 { + renderer.Streams.Printf( + renderer.Colorize.Color("\n[bold]Plan:[reset] %d to import, %d to add, %d to change, %d to destroy.\n"), + importingCount, + counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete], + counts[plans.Update], + counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete]) + } else { + renderer.Streams.Printf( + renderer.Colorize.Color("\n[bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"), + counts[plans.Create]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete], + counts[plans.Update], + counts[plans.Delete]+counts[plans.DeleteThenCreate]+counts[plans.CreateThenDelete]) + } + } + + if len(outputs) > 0 { + renderer.Streams.Print("\nChanges to Outputs:\n") + renderer.Streams.Printf("%s\n", outputs) + + if len(counts) == 0 { + // If we have output changes but not resource changes then we + // won't have output any indication about the changes at all yet, + // so we need some extra context about what it would mean to + // apply a change that _only_ includes output changes. + renderer.Streams.Println(format.WordWrap( + "\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.", + renderer.Streams.Stdout.Columns())) + } + } +} + +func renderHumanDiffOutputs(renderer Renderer, outputs map[string]computed.Diff) string { + var rendered []string + + var keys []string + escapedKeys := make(map[string]string) + var escapedKeyMaxLen int + for key := range outputs { + escapedKey := renderers.EnsureValidAttributeName(key) + keys = append(keys, key) + escapedKeys[key] = escapedKey + if len(escapedKey) > escapedKeyMaxLen { + escapedKeyMaxLen = len(escapedKey) + } + } + sort.Strings(keys) + + for _, key := range keys { + output := outputs[key] + if output.Action != plans.NoOp { + rendered = append(rendered, fmt.Sprintf("%s %-*s = %s", renderer.Colorize.Color(format.DiffActionSymbol(output.Action)), escapedKeyMaxLen, escapedKeys[key], output.RenderHuman(0, computed.NewRenderHumanOpts(renderer.Colorize)))) + } + } + return strings.Join(rendered, "\n") +} + +func renderHumanDiffDrift(renderer Renderer, diffs diffs, mode plans.Mode) bool { + var drs []diff + + // In refresh-only mode, we show all resources marked as drifted, + // including those which have moved without other changes. In other plan + // modes, move-only changes will be rendered in the planned changes, so + // we skip them here. + + if mode == plans.RefreshOnlyMode { + drs = diffs.drift + } else { + for _, dr := range diffs.drift { + if dr.diff.Action != plans.NoOp { + drs = append(drs, dr) + } + } + } + + if len(drs) == 0 { + return false + } + + // If the overall plan is empty, and it's not a refresh only plan then we + // won't show any drift changes. + if diffs.Empty() && mode != plans.RefreshOnlyMode { + return false + } + + renderer.Streams.Print(renderer.Colorize.Color("\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform\n")) + renderer.Streams.Println() + renderer.Streams.Print(format.WordWrap( + "Terraform detected the following changes made outside of Terraform since the last \"terraform apply\" which may have affected this plan:\n", + renderer.Streams.Stdout.Columns())) + + for _, drift := range drs { + diff, render := renderHumanDiff(renderer, drift, detectedDrift) + if render { + renderer.Streams.Println() + renderer.Streams.Println(diff) + } + } + + switch mode { + case plans.RefreshOnlyMode: + renderer.Streams.Println(format.WordWrap( + "\n\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", + renderer.Streams.Stdout.Columns(), + )) + default: + renderer.Streams.Println(format.WordWrap( + "\n\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", + renderer.Streams.Stdout.Columns(), + )) + } + + return true +} + +func renderHumanDeferredChanges(renderer Renderer, diffs diffs, mode plans.Mode) bool { + if len(diffs.deferred) == 0 { + return false + } + + renderer.Streams.Print(renderer.Colorize.Color("\n[bold][cyan]Note:[reset][bold] This is a partial plan, parts can only be known in the next plan / apply cycle.\n")) + renderer.Streams.Println() + + for _, deferred := range diffs.deferred { + diff, render := renderHumanDeferredDiff(renderer, deferred) + if render { + renderer.Streams.Println() + renderer.Streams.Println(diff) + } + } + return true +} + +func renderHumanDiff(renderer Renderer, diff diff, cause string) (string, bool) { + + // Internally, our computed diffs can't tell the difference between a + // replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple + // update action. So, at the top most level we rely on the action provided + // by the plan itself instead of what we compute. Nested attributes and + // blocks however don't have the replace type of actions, so we can trust + // the computed actions of these. + + action := jsonplan.UnmarshalActions(diff.change.Change.Actions) + if action == plans.NoOp && !diff.Moved() && !diff.Importing() { + // Skip resource changes that have nothing interesting to say. + return "", false + } + + var buf bytes.Buffer + buf.WriteString(renderer.Colorize.Color(resourceChangeComment(diff.change, action, cause))) + + opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts.ShowUnchangedChildren = diff.Importing() + + buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(diff.change), diff.diff.RenderHuman(0, opts))) + return buf.String(), true +} + +func renderHumanDeferredDiff(renderer Renderer, deferred deferredDiff) (string, bool) { + + // Internally, our computed diffs can't tell the difference between a + // replace action (eg. CreateThenDestroy, DestroyThenCreate) and a simple + // update action. So, at the top most level we rely on the action provided + // by the plan itself instead of what we compute. Nested attributes and + // blocks however don't have the replace type of actions, so we can trust + // the computed actions of these. + action := jsonplan.UnmarshalActions(deferred.diff.change.Change.Actions) + if action == plans.NoOp && !deferred.diff.Moved() && !deferred.diff.Importing() { + // Skip resource changes that have nothing interesting to say. + return "", false + } + + var buf bytes.Buffer + var explanation string + switch deferred.reason { + // TODO: Add other cases + case jsonplan.DeferredReasonInstanceCountUnknown: + explanation = "because the number of resource instances is unknown" + case jsonplan.DeferredReasonResourceConfigUnknown: + explanation = "because the resource configuration is unknown" + case jsonplan.DeferredReasonProviderConfigUnknown: + explanation = "because the provider configuration is unknown" + case jsonplan.DeferredReasonDeferredPrereq: + explanation = "because a prerequisite for this resource is deferred" + case jsonplan.DeferredReasonAbsentPrereq: + explanation = "because a prerequisite for this resource has not yet been created" + default: + explanation = "for an unknown reason" + } + + buf.WriteString(renderer.Colorize.Color(fmt.Sprintf("[bold] # %s[reset] was deferred\n", deferred.diff.change.Address))) + buf.WriteString(renderer.Colorize.Color(fmt.Sprintf(" #[reset] (%s)\n", explanation))) + + opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts.ShowUnchangedChildren = deferred.diff.Importing() + + buf.WriteString(fmt.Sprintf("%s %s %s", renderer.Colorize.Color(format.DiffActionSymbol(action)), resourceChangeHeader(deferred.diff.change), deferred.diff.diff.RenderHuman(0, opts))) + return buf.String(), true +} + +func resourceChangeComment(resource jsonplan.ResourceChange, action plans.Action, changeCause string) string { + var buf bytes.Buffer + + dispAddr := resource.Address + if len(resource.Deposed) != 0 { + dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, resource.Deposed) + } + + var printedMoved bool + var printedImported bool + + switch action { + case plans.Create: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be created", dispAddr)) + case plans.Read: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be read during apply", dispAddr)) + switch resource.ActionReason { + case jsonplan.ResourceInstanceReadBecauseConfigUnknown: + buf.WriteString("\n # (config refers to values not yet known)") + case jsonplan.ResourceInstanceReadBecauseDependencyPending: + buf.WriteString("\n # (depends on a resource or a module with changes pending)") + case jsonplan.ResourceInstanceReadBecauseCheckNested: + buf.WriteString("\n # (config will be reloaded to verify a check block)") + } + case plans.Update: + switch changeCause { + case proposedChange: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be updated in-place", dispAddr)) + case detectedDrift: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has changed", dispAddr)) + default: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] update (unknown reason %s)", dispAddr, changeCause)) + } + case plans.CreateThenDelete, plans.DeleteThenCreate: + switch resource.ActionReason { + case jsonplan.ResourceInstanceReplaceBecauseTainted: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] is tainted, so must be [bold][red]replaced[reset]", dispAddr)) + case jsonplan.ResourceInstanceReplaceByRequest: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset], as requested", dispAddr)) + case jsonplan.ResourceInstanceReplaceByTriggers: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]replaced[reset] due to changes in replace_triggered_by", dispAddr)) + default: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] must be [bold][red]replaced[reset]", dispAddr)) + } + case plans.CreateThenForget: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] must be replaced, but the existing object will not be destroyed", dispAddr)) + buf.WriteString("\n # (destroy = false is set in the configuration)") + case plans.Forget: + if len(resource.Deposed) > 0 { + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be removed from Terraform state, but [bold][red]will not be destroyed[reset]", dispAddr)) + buf.WriteString("\n[bold] # (left over from a partially-failed replacement of this instance)") + } else { + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will no longer be managed by Terraform, but [bold][red]will not be destroyed[reset]", dispAddr)) + } + buf.WriteString("\n # (destroy = false is set in the configuration)") + case plans.Delete: + switch changeCause { + case proposedChange: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be [bold][red]destroyed[reset]", dispAddr)) + case detectedDrift: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has been deleted", dispAddr)) + default: + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] delete (unknown reason %s)", dispAddr, changeCause)) + } + // We can sometimes give some additional detail about why we're + // proposing to delete. We show this as additional notes, rather than + // as additional wording in the main action statement, in an attempt + // to make the "will be destroyed" message prominent and consistent + // in all cases, for easier scanning of this often-risky action. + switch resource.ActionReason { + case jsonplan.ResourceInstanceDeleteBecauseNoResourceConfig: + buf.WriteString(fmt.Sprintf("\n # (because %s.%s is not in configuration)", resource.Type, resource.Name)) + case jsonplan.ResourceInstanceDeleteBecauseNoMoveTarget: + buf.WriteString(fmt.Sprintf("\n # (because %s was moved to %s, which is not in configuration)", resource.PreviousAddress, resource.Address)) + case jsonplan.ResourceInstanceDeleteBecauseNoModule: + // FIXME: Ideally we'd truncate addr.Module to reflect the earliest + // step that doesn't exist, so it's clearer which call this refers + // to, but we don't have enough information out here in the UI layer + // to decide that; only the "expander" in Terraform Core knows + // which module instance keys are actually declared. + buf.WriteString(fmt.Sprintf("\n # (because %s is not in configuration)", resource.ModuleAddress)) + case jsonplan.ResourceInstanceDeleteBecauseWrongRepetition: + var index interface{} + if resource.Index != nil { + if err := json.Unmarshal(resource.Index, &index); err != nil { + panic(err) + } + } + + // We have some different variations of this one + switch index.(type) { + case nil: + buf.WriteString("\n # (because resource uses count or for_each)") + case float64: + buf.WriteString("\n # (because resource does not use count)") + case string: + buf.WriteString("\n # (because resource does not use for_each)") + } + case jsonplan.ResourceInstanceDeleteBecauseCountIndex: + buf.WriteString(fmt.Sprintf("\n # (because index [%s] is out of range for count)", resource.Index)) + case jsonplan.ResourceInstanceDeleteBecauseEachKey: + buf.WriteString(fmt.Sprintf("\n # (because key [%s] is not in for_each map)", resource.Index)) + } + if len(resource.Deposed) != 0 { + // Some extra context about this unusual situation. + buf.WriteString("\n # (left over from a partially-failed replacement of this instance)") + } + case plans.NoOp: + if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address { + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] has moved to [bold]%s[reset]", resource.PreviousAddress, dispAddr)) + printedMoved = true + break + } + if resource.Change.Importing != nil { + buf.WriteString(fmt.Sprintf("[bold] # %s[reset] will be imported", dispAddr)) + if len(resource.Change.GeneratedConfig) > 0 { + buf.WriteString("\n #[reset] (config will be generated)") + } + printedImported = true + break + } + fallthrough + default: + // should never happen, since the above is exhaustive + buf.WriteString(fmt.Sprintf("%s has an action the plan renderer doesn't support (this is a bug)", dispAddr)) + } + buf.WriteString("\n") + + if len(resource.PreviousAddress) > 0 && resource.PreviousAddress != resource.Address && !printedMoved { + buf.WriteString(fmt.Sprintf(" # [reset](moved from %s)\n", resource.PreviousAddress)) + } + if resource.Change.Importing != nil && !printedImported { + // We want to make this as forward compatible as possible, and we know + // the ID may be removed from the Importing metadata in favour of + // something else. + // As Importing metadata is loaded from a JSON struct, the effect of it + // being removed in the future will mean this renderer will receive it + // as an empty string + if len(resource.Change.Importing.ID) > 0 { + buf.WriteString(fmt.Sprintf(" # [reset](imported from \"%s\")\n", resource.Change.Importing.ID)) + } else { + // This means we're trying to render a plan from a future version + // and we didn't get given the ID. So we'll do our best. + buf.WriteString(" # [reset](will be imported first)\n") + } + } + if resource.Change.Importing != nil && (action == plans.CreateThenDelete || action == plans.DeleteThenCreate) { + buf.WriteString(" # [reset][yellow]Warning: this will destroy the imported resource[reset]\n") + } + + return buf.String() +} + +func resourceChangeHeader(change jsonplan.ResourceChange) string { + mode := "resource" + if change.Mode != jsonstate.ManagedResourceMode { + mode = "data" + } + return fmt.Sprintf("%s \"%s\" \"%s\"", mode, change.Type, change.Name) +} + +func actionDescription(action plans.Action) string { + switch action { + case plans.Create: + return " [green]+[reset] create" + case plans.Delete: + return " [red]-[reset] destroy" + case plans.Update: + return " [yellow]~[reset] update in-place" + case plans.CreateThenDelete: + return "[green]+[reset]/[red]-[reset] create replacement and then destroy" + case plans.DeleteThenCreate: + return "[red]-[reset]/[green]+[reset] destroy and then create replacement" + case plans.Read: + return " [cyan]<=[reset] read (data resources)" + default: + panic(fmt.Sprintf("unrecognized change type: %s", action.String())) + } +} diff --git a/internal/command/format/diff_test.go b/internal/command/jsonformat/plan_test.go similarity index 66% rename from internal/command/format/diff_test.go rename to internal/command/jsonformat/plan_test.go index 56e2eafeaf..47a7476899 100644 --- a/internal/command/format/diff_test.go +++ b/internal/command/jsonformat/plan_test.go @@ -1,19 +1,505 @@ -package format +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat import ( + "encoding/json" "fmt" "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang/marks" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" "github.com/mitchellh/colorstring" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/jsonformat/differ" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/terraform" ) +func TestRenderHuman_EmptyPlan(t *testing.T) { + color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} + streams, done := terminal.StreamsForTesting(t) + + plan := Plan{} + + renderer := Renderer{Colorize: color, Streams: streams} + plan.renderHuman(renderer, plans.NormalMode) + + want := ` +No changes. Your infrastructure matches the configuration. + +Terraform has compared your real infrastructure against your configuration +and found no differences, so no changes are needed. +` + + got := done(t).Stdout() + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } +} + +func TestRenderHuman_DeferredPlan(t *testing.T) { + color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} + streams, done := terminal.StreamsForTesting(t) + + plan := Plan{ + DeferredChanges: []jsonplan.DeferredResourceChange{ + { + Reason: jsonplan.DeferredReasonDeferredPrereq, + ResourceChange: jsonplan.ResourceChange{ + Address: "aws_instance.foo", + Mode: "managed", + Type: "aws_instance", + Name: "foo", + IndexUnknown: true, + ProviderName: "aws", + Change: jsonplan.Change{ + Actions: []string{"update"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + }, + }, + }, + }, + ProviderSchemas: map[string]*jsonprovider.Provider{ + "aws": { + ResourceSchemas: map[string]*jsonprovider.Schema{ + "aws_instance": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "id": { + AttributeType: marshalJson(t, "string"), + }, + "ami": { + AttributeType: marshalJson(t, "string"), + }, + }, + }, + }, + }, + }, + }, + } + + renderer := Renderer{Colorize: color, Streams: streams} + plan.renderHuman(renderer, plans.NormalMode) + + want := ` +No current changes. This plan requires another plan to be applied first. + + +Note: This is a partial plan, parts can only be known in the next plan / apply cycle. + + + # aws_instance.foo was deferred + # (because a prerequisite for this resource is deferred) + ~ resource "aws_instance" "foo" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + } + +───────────────────────────────────────────────────────────────────────────── +` + + got := done(t).Stdout() + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } +} + +func TestRenderHuman_EmptyOutputs(t *testing.T) { + color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} + streams, done := terminal.StreamsForTesting(t) + + outputVal, _ := json.Marshal("some-text") + plan := Plan{ + OutputChanges: map[string]jsonplan.Change{ + "a_string": { + Actions: []string{"no-op"}, + Before: outputVal, + After: outputVal, + }, + }, + } + + renderer := Renderer{Colorize: color, Streams: streams} + plan.renderHuman(renderer, plans.NormalMode) + + want := ` +No changes. Your infrastructure matches the configuration. + +Terraform has compared your real infrastructure against your configuration +and found no differences, so no changes are needed. +` + + got := done(t).Stdout() + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } +} + +func TestRenderHuman_Imports(t *testing.T) { + color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} + + schemas := map[string]*jsonprovider.Provider{ + "test": { + ResourceSchemas: map[string]*jsonprovider.Schema{ + "test_resource": { + Block: &jsonprovider.Block{ + Attributes: map[string]*jsonprovider.Attribute{ + "id": { + AttributeType: marshalJson(t, "string"), + }, + "value": { + AttributeType: marshalJson(t, "string"), + }, + }, + }, + }, + }, + }, + } + + tcs := map[string]struct { + plan Plan + output string + }{ + "simple_import": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.resource", + Mode: "managed", + Type: "test_resource", + Name: "resource", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"no-op"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + Importing: &jsonplan.Importing{ + ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + }, + }, + }, + }, + }, + output: ` +Terraform will perform the following actions: + + # test_resource.resource will be imported + resource "test_resource" "resource" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + value = "Hello, World!" + } + +Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. +`, + }, + "simple_import_with_generated_config": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.resource", + Mode: "managed", + Type: "test_resource", + Name: "resource", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"no-op"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + Importing: &jsonplan.Importing{ + ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + }, + GeneratedConfig: `resource "test_resource" "resource" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + value = "Hello, World!" +}`, + }, + }, + }, + }, + output: ` +Terraform will perform the following actions: + + # test_resource.resource will be imported + # (config will be generated) + resource "test_resource" "resource" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + value = "Hello, World!" + } + +Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. +`, + }, + "import_and_move": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.after", + PreviousAddress: "test_resource.before", + Mode: "managed", + Type: "test_resource", + Name: "after", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"no-op"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + Importing: &jsonplan.Importing{ + ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + }, + }, + }, + }, + }, + output: ` +Terraform will perform the following actions: + + # test_resource.before has moved to test_resource.after + # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") + resource "test_resource" "after" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + value = "Hello, World!" + } + +Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. +`, + }, + "import_move_and_update": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.after", + PreviousAddress: "test_resource.before", + Mode: "managed", + Type: "test_resource", + Name: "after", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"update"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, Universe!", + }), + Importing: &jsonplan.Importing{ + ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + }, + }, + }, + }, + }, + output: ` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # test_resource.after will be updated in-place + # (moved from test_resource.before) + # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") + ~ resource "test_resource" "after" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + ~ value = "Hello, World!" -> "Hello, Universe!" + } + +Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. +`, + }, + "import_and_update": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.resource", + Mode: "managed", + Type: "test_resource", + Name: "resource", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"update"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, Universe!", + }), + Importing: &jsonplan.Importing{ + ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + }, + }, + }, + }, + }, + output: ` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # test_resource.resource will be updated in-place + # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") + ~ resource "test_resource" "resource" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + ~ value = "Hello, World!" -> "Hello, Universe!" + } + +Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. +`, + }, + "import_and_update_with_no_id": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.resource", + Mode: "managed", + Type: "test_resource", + Name: "resource", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"update"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, Universe!", + }), + Importing: &jsonplan.Importing{}, + }, + }, + }, + }, + output: ` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # test_resource.resource will be updated in-place + # (will be imported first) + ~ resource "test_resource" "resource" { + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" + ~ value = "Hello, World!" -> "Hello, Universe!" + } + +Plan: 1 to import, 0 to add, 1 to change, 0 to destroy. +`, + }, + "import_and_replace": { + plan: Plan{ + ResourceChanges: []jsonplan.ResourceChange{ + { + Address: "test_resource.resource", + Mode: "managed", + Type: "test_resource", + Name: "resource", + ProviderName: "test", + Change: jsonplan.Change{ + Actions: []string{"create", "delete"}, + Before: marshalJson(t, map[string]interface{}{ + "id": "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + "value": "Hello, World!", + }), + After: marshalJson(t, map[string]interface{}{ + "id": "9794FB1F-7260-442F-830C-F2D450E90CE3", + "value": "Hello, World!", + }), + ReplacePaths: marshalJson(t, [][]string{{"id"}}), + Importing: &jsonplan.Importing{ + ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E", + }, + }, + ActionReason: "", + }, + }, + }, + output: ` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: ++/- create replacement and then destroy + +Terraform will perform the following actions: + + # test_resource.resource must be replaced + # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E") + # Warning: this will destroy the imported resource ++/- resource "test_resource" "resource" { + ~ id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" -> "9794FB1F-7260-442F-830C-F2D450E90CE3" # forces replacement + value = "Hello, World!" + } + +Plan: 1 to import, 1 to add, 0 to change, 1 to destroy. +`, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + plan := tc.plan + plan.PlanFormatVersion = jsonplan.FormatVersion + plan.ProviderFormatVersion = jsonprovider.FormatVersion + plan.ProviderSchemas = schemas + + renderer := Renderer{ + Colorize: color, + Streams: streams, + } + plan.renderHuman(renderer, plans.NormalMode) + + got := done(t).Stdout() + want := tc.output + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } + }) + } +} + func TestResourceChange_primitiveTypes(t *testing.T) { testCases := map[string]testCase{ "creation": { @@ -32,8 +518,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + id = (known after apply) - } -`, + }`, }, "creation (null string)": { Action: plans.Create, @@ -51,8 +536,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + string = "null" - } -`, + }`, }, "creation (null string with extra whitespace)": { Action: plans.Create, @@ -70,8 +554,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { + string = "null " - } -`, + }`, }, "creation (object with quoted keys)": { Action: plans.Create, @@ -98,8 +581,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { + "quoted:key" = "some-value" + unquoted = "value" } - } -`, + }`, }, "deletion": { Action: plans.Delete, @@ -117,8 +599,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" -> null - } -`, + }`, }, "deletion of deposed object": { Action: plans.Delete, @@ -138,8 +619,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { # (left over from a partially-failed replacement of this instance) - resource "test_instance" "example" { - id = "i-02ae66f368e8518a9" -> null - } -`, + }`, }, "deletion (empty string)": { Action: plans.Delete, @@ -158,9 +638,79 @@ func TestResourceChange_primitiveTypes(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - - id = "i-02ae66f368e8518a9" -> null - } -`, + - id = "i-02ae66f368e8518a9" -> null + # (1 unchanged attribute hidden) + }`, + }, + "forget": { + Action: plans.Forget, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-123"), + }), + After: cty.NullVal(cty.EmptyObject), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will no longer be managed by Terraform, but will not be destroyed + # (destroy = false is set in the configuration) + . resource "test_instance" "example" { + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + }`, + }, + "forget (deposed)": { + Action: plans.Forget, + Mode: addrs.ManagedResourceMode, + DeposedKey: states.DeposedKey("adios"), + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + }), + After: cty.NullVal(cty.EmptyObject), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + }, + }, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example (deposed object adios) will be removed from Terraform state, but will not be destroyed + # (left over from a partially-failed replacement of this instance) + # (destroy = false is set in the configuration) + . resource "test_instance" "example" { + id = "i-02ae66f368e8518a9" + }`, + }, + "create-then-forget": { + Action: plans.CreateThenForget, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02999999999999999"), + "ami": cty.StringVal("ami-AFTER"), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(cty.Path{ + cty.GetAttrStep{Name: "ami"}, + }), + ExpectedOutput: ` # test_instance.example must be replaced, but the existing object will not be destroyed + # (destroy = false is set in the configuration) + +/. resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement + ~ id = "i-02ae66f368e8518a9" -> "i-02999999999999999" + }`, }, "string in-place update": { Action: plans.Update, @@ -184,8 +734,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" - } -`, + }`, }, "update with quoted key": { Action: plans.Update, @@ -213,8 +762,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { id = "i-02ae66f368e8518a9" ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com" # (1 unchanged attribute hidden) - } -`, + }`, }, "string force-new update": { Action: plans.DeleteThenCreate, @@ -241,8 +789,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement id = "i-02ae66f368e8518a9" - } -`, + }`, }, "string in-place update (null values)": { Action: plans.Update, @@ -251,26 +798,96 @@ func TestResourceChange_primitiveTypes(t *testing.T) { "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-BEFORE"), "unchanged": cty.NullVal(cty.String), + "empty": cty.StringVal(""), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("i-02ae66f368e8518a9"), "ami": cty.StringVal("ami-AFTER"), "unchanged": cty.NullVal(cty.String), + "empty": cty.NullVal(cty.String), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, "unchanged": {Type: cty.String, Optional: true}, + "empty": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + }`, + }, + "string update (non-legacy)": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "from_null": cty.NullVal(cty.String), + "to_null": cty.StringVal(""), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "from_null": cty.StringVal(""), + "to_null": cty.NullVal(cty.String), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "from_null": {Type: cty.DynamicPseudoType, Optional: true}, + "to_null": {Type: cty.String, Optional: true}, + }, + }, + RequiredReplace: cty.NewPathSet(), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + + from_null = "" + id = "i-02ae66f368e8518a9" + - to_null = "" -> null + }`, + }, + "string update (non-legacy nested object)": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "obj": cty.ObjectVal(map[string]cty.Value{ + "from_null": cty.NullVal(cty.String), + "to_null": cty.StringVal(""), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "obj": cty.ObjectVal(map[string]cty.Value{ + "from_null": cty.StringVal(""), + "to_null": cty.NullVal(cty.String), + }), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "obj": {NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "from_null": {Type: cty.DynamicPseudoType, Optional: true}, + "to_null": {Type: cty.String, Optional: true}, + }, + }}, }, }, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { - ~ ami = "ami-BEFORE" -> "ami-AFTER" id = "i-02ae66f368e8518a9" - } -`, + ~ obj = { + + from_null = "" + - to_null = "" -> null + } + }`, }, "in-place update of multi-line string field": { Action: plans.Update, @@ -281,8 +898,7 @@ func TestResourceChange_primitiveTypes(t *testing.T) { long multi-line string -field -`), +field`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), @@ -290,8 +906,7 @@ field extremely long multi-line string -field -`), +field`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -311,8 +926,7 @@ field string field EOT - } -`, + }`, }, "addition of multi-line string field": { Action: plans.Update, @@ -324,8 +938,7 @@ field After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "more_lines": cty.StringVal(`original -new line -`), +new line`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -341,22 +954,19 @@ new line original new line EOT - } -`, + }`, }, "force-new update of multi-line string field": { Action: plans.DeleteThenCreate, Mode: addrs.ManagedResourceMode, Before: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("i-02ae66f368e8518a9"), - "more_lines": cty.StringVal(`original -`), + "id": cty.StringVal("i-02ae66f368e8518a9"), + "more_lines": cty.StringVal(`original`), }), After: cty.ObjectVal(map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "more_lines": cty.StringVal(`original -new line -`), +new line`), }), Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -374,8 +984,7 @@ new line original + new line EOT - } -`, + }`, }, // Sensitive @@ -416,8 +1025,7 @@ new line } + id = (known after apply) + password = (sensitive value) - } -`, + }`, }, "update with equal sensitive field": { Action: plans.Update, @@ -445,8 +1053,7 @@ new line ~ id = "blah" -> (known after apply) ~ str = "before" -> "after" # (1 unchanged attribute hidden) - } -`, + }`, }, // tainted objects @@ -475,8 +1082,7 @@ new line -/+ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement ~ id = "i-02ae66f368e8518a9" -> (known after apply) - } -`, + }`, }, "force replacement with empty before value": { Action: plans.DeleteThenCreate, @@ -503,8 +1109,7 @@ new line -/+ resource "test_instance" "example" { + forced = "example" # forces replacement name = "name" - } -`, + }`, }, "force replacement with empty before value legacy": { Action: plans.DeleteThenCreate, @@ -531,8 +1136,7 @@ new line -/+ resource "test_instance" "example" { + forced = "example" # forces replacement name = "name" - } -`, + }`, }, "read during apply because of unknown configuration": { Action: plans.Read, @@ -553,8 +1157,7 @@ new line # (config refers to values not yet known) <= data "test_instance" "example" { name = "name" - } -`, + }`, }, "read during apply because of pending changes to upstream dependency": { Action: plans.Read, @@ -575,8 +1178,7 @@ new line # (depends on a resource or a module with changes pending) <= data "test_instance" "example" { name = "name" - } -`, + }`, }, "read during apply for unspecified reason": { Action: plans.Read, @@ -595,8 +1197,7 @@ new line ExpectedOutput: ` # data.test_instance.example will be read during apply <= data "test_instance" "example" { name = "name" - } -`, + }`, }, "show all identifying attributes even if unchanged": { Action: plans.Update, @@ -641,8 +1242,7 @@ new line "name" = "bob" } # (2 unchanged attributes hidden) - } -`, + }`, }, } @@ -687,8 +1287,7 @@ func TestResourceChange_JSON(t *testing.T) { + str = "value" } ) - } -`, + }`, }, "in-place update of object": { Action: plans.Update, @@ -714,12 +1313,11 @@ func TestResourceChange_JSON(t *testing.T) { ~ json_field = jsonencode( ~ { + bbb = "new_value" - - ccc = 5 -> null - # (1 unchanged element hidden) + - ccc = 5 + # (1 unchanged attribute hidden) } ) - } -`, + }`, }, "in-place update of object with quoted keys": { Action: plans.Update, @@ -745,12 +1343,11 @@ func TestResourceChange_JSON(t *testing.T) { ~ json_field = jsonencode( ~ { + "b:bb" = "new_value" - - "c:c" = "old_value" -> null - # (1 unchanged element hidden) + - "c:c" = "old_value" + # (1 unchanged attribute hidden) } ) - } -`, + }`, }, "in-place update (from empty tuple)": { Action: plans.Update, @@ -780,8 +1377,7 @@ func TestResourceChange_JSON(t *testing.T) { ] } ) - } -`, + }`, }, "in-place update (to empty tuple)": { Action: plans.Update, @@ -811,8 +1407,7 @@ func TestResourceChange_JSON(t *testing.T) { ] } ) - } -`, + }`, }, "in-place update (tuple of different types)": { Action: plans.Update, @@ -846,8 +1441,7 @@ func TestResourceChange_JSON(t *testing.T) { ] } ) - } -`, + }`, }, "force-new update": { Action: plans.DeleteThenCreate, @@ -876,11 +1470,10 @@ func TestResourceChange_JSON(t *testing.T) { ~ json_field = jsonencode( ~ { + bbb = "new_value" - # (1 unchanged element hidden) + # (1 unchanged attribute hidden) } # forces replacement ) - } -`, + }`, }, "in-place update (whitespace change)": { Action: plans.Update, @@ -910,8 +1503,7 @@ func TestResourceChange_JSON(t *testing.T) { bbb = "another" } ) - } -`, + }`, }, "force-new update (whitespace change)": { Action: plans.DeleteThenCreate, @@ -944,8 +1536,7 @@ func TestResourceChange_JSON(t *testing.T) { bbb = "another" } ) - } -`, + }`, }, "creation (empty)": { Action: plans.Create, @@ -966,8 +1557,7 @@ func TestResourceChange_JSON(t *testing.T) { + resource "test_instance" "example" { + id = (known after apply) + json_field = jsonencode({}) - } -`, + }`, }, "JSON list item removal": { Action: plans.Update, @@ -997,8 +1587,7 @@ func TestResourceChange_JSON(t *testing.T) { - "third", ] ) - } -`, + }`, }, "JSON list item addition": { Action: plans.Update, @@ -1028,8 +1617,7 @@ func TestResourceChange_JSON(t *testing.T) { + "third", ] ) - } -`, + }`, }, "JSON list object addition": { Action: plans.Update, @@ -1055,11 +1643,10 @@ func TestResourceChange_JSON(t *testing.T) { ~ json_field = jsonencode( ~ { + second = "222" - # (1 unchanged element hidden) + # (1 unchanged attribute hidden) } ) - } -`, + }`, }, "JSON object with nested list": { Action: plans.Update, @@ -1094,8 +1681,7 @@ func TestResourceChange_JSON(t *testing.T) { ] } ) - } -`, + }`, }, "JSON list of objects - adding item": { Action: plans.Update, @@ -1128,8 +1714,7 @@ func TestResourceChange_JSON(t *testing.T) { }, ] ) - } -`, + }`, }, "JSON list of objects - removing item": { Action: plans.Update, @@ -1165,8 +1750,7 @@ func TestResourceChange_JSON(t *testing.T) { }, ] ) - } -`, + }`, }, "JSON object with list of objects": { Action: plans.Update, @@ -1201,8 +1785,7 @@ func TestResourceChange_JSON(t *testing.T) { ] } ) - } -`, + }`, }, "JSON object double nested lists": { Action: plans.Update, @@ -1237,8 +1820,7 @@ func TestResourceChange_JSON(t *testing.T) { ] } ) - } -`, + }`, }, "in-place update from object to tuple": { Action: plans.Update, @@ -1276,8 +1858,7 @@ func TestResourceChange_JSON(t *testing.T) { + "something", ] ) - } -`, + }`, }, } runTestCases(t, testCases) @@ -1362,8 +1943,7 @@ func TestResourceChange_listObject(t *testing.T) { }, ] ~ id = "i-02ae66f368e8518a9" -> (known after apply) - } -`, + }`, }, } runTestCases(t, testCases) @@ -1401,8 +1981,7 @@ func TestResourceChange_primitiveList(t *testing.T) { + "new-element", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - first addition": { Action: plans.Update, @@ -1434,8 +2013,7 @@ func TestResourceChange_primitiveList(t *testing.T) { + "new-element", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - insertion": { Action: plans.Update, @@ -1482,8 +2060,7 @@ func TestResourceChange_primitiveList(t *testing.T) { # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, "force-new update - insertion": { Action: plans.DeleteThenCreate, @@ -1525,8 +2102,7 @@ func TestResourceChange_primitiveList(t *testing.T) { "cccc", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - deletion": { Action: plans.Update, @@ -1570,8 +2146,7 @@ func TestResourceChange_primitiveList(t *testing.T) { # (1 unchanged element hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, "creation - empty list": { Action: plans.Create, @@ -1595,8 +2170,7 @@ func TestResourceChange_primitiveList(t *testing.T) { + ami = "ami-STATIC" + id = (known after apply) + list_field = [] - } -`, + }`, }, "in-place update - full to empty": { Action: plans.Update, @@ -1632,8 +2206,7 @@ func TestResourceChange_primitiveList(t *testing.T) { - "cccc", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - null to empty": { Action: plans.Update, @@ -1661,8 +2234,7 @@ func TestResourceChange_primitiveList(t *testing.T) { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + list_field = [] # (1 unchanged attribute hidden) - } -`, + }`, }, "update to unknown element": { Action: plans.Update, @@ -1698,13 +2270,11 @@ func TestResourceChange_primitiveList(t *testing.T) { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ list_field = [ "aaaa", - - "bbbb", - + (known after apply), + ~ "bbbb" -> (known after apply), "cccc", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "update - two new unknown elements": { Action: plans.Update, @@ -1752,8 +2322,7 @@ func TestResourceChange_primitiveList(t *testing.T) { # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, } runTestCases(t, testCases) @@ -1797,13 +2366,11 @@ func TestResourceChange_primitiveTuple(t *testing.T) { ~ tuple_field = [ # (1 unchanged element hidden) "bbbb", - - "dddd", - + "cccc", + ~ "dddd" -> "cccc", "eeee", # (1 unchanged element hidden) ] - } -`, + }`, }, } runTestCases(t, testCases) @@ -1841,8 +2408,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { + "new-element", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - first insertion": { Action: plans.Update, @@ -1874,8 +2440,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { + "new-element", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - insertion": { Action: plans.Update, @@ -1913,8 +2478,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, "force-new update - insertion": { Action: plans.DeleteThenCreate, @@ -1955,8 +2519,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { # (2 unchanged elements hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - deletion": { Action: plans.Update, @@ -1994,8 +2557,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { # (1 unchanged element hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, "creation - empty set": { Action: plans.Create, @@ -2019,8 +2581,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { + ami = "ami-STATIC" + id = (known after apply) + set_field = [] - } -`, + }`, }, "in-place update - full to empty set": { Action: plans.Update, @@ -2054,8 +2615,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { - "bbbb", ] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - null to empty set": { Action: plans.Update, @@ -2083,8 +2643,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { ~ id = "i-02ae66f368e8518a9" -> (known after apply) + set_field = [] # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update to unknown": { Action: plans.Update, @@ -2118,8 +2677,7 @@ func TestResourceChange_primitiveSet(t *testing.T) { - "bbbb", ] -> (known after apply) # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update to unknown element": { Action: plans.Update, @@ -2153,12 +2711,11 @@ func TestResourceChange_primitiveSet(t *testing.T) { ~ id = "i-02ae66f368e8518a9" -> (known after apply) ~ set_field = [ - "bbbb", - ~ (known after apply), + + (known after apply), # (1 unchanged element hidden) ] # (1 unchanged attribute hidden) - } -`, + }`, }, } runTestCases(t, testCases) @@ -2198,8 +2755,7 @@ func TestResourceChange_map(t *testing.T) { + "new-key" = "new-element" } # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - first insertion": { Action: plans.Update, @@ -2233,8 +2789,7 @@ func TestResourceChange_map(t *testing.T) { + "new-key" = "new-element" } # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - insertion": { Action: plans.Update, @@ -2274,8 +2829,7 @@ func TestResourceChange_map(t *testing.T) { # (2 unchanged elements hidden) } # (1 unchanged attribute hidden) - } -`, + }`, }, "force-new update - insertion": { Action: plans.DeleteThenCreate, @@ -2316,8 +2870,7 @@ func TestResourceChange_map(t *testing.T) { # (2 unchanged elements hidden) } # (1 unchanged attribute hidden) - } -`, + }`, }, "in-place update - deletion": { Action: plans.Update, @@ -2355,8 +2908,7 @@ func TestResourceChange_map(t *testing.T) { # (1 unchanged element hidden) } # (1 unchanged attribute hidden) - } -`, + }`, }, "creation - empty": { Action: plans.Create, @@ -2380,8 +2932,7 @@ func TestResourceChange_map(t *testing.T) { + ami = "ami-STATIC" + id = (known after apply) + map_field = {} - } -`, + }`, }, "update to unknown element": { Action: plans.Update, @@ -2420,8 +2971,7 @@ func TestResourceChange_map(t *testing.T) { # (2 unchanged elements hidden) } # (1 unchanged attribute hidden) - } -`, + }`, }, } runTestCases(t, testCases) @@ -2471,8 +3021,7 @@ func TestResourceChange_nestedList(t *testing.T) { # (1 unchanged attribute hidden) # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - creation": { Action: plans.Update, @@ -2515,8 +3064,7 @@ func TestResourceChange_nestedList(t *testing.T) { id = "i-02ae66f368e8518a9" + root_block_device {} - } -`, + }`, }, "in-place update - first insertion": { Action: plans.Update, @@ -2562,8 +3110,7 @@ func TestResourceChange_nestedList(t *testing.T) { + root_block_device { + volume_type = "gp2" } - } -`, + }`, }, "in-place update - insertion": { Action: plans.Update, @@ -2626,8 +3173,7 @@ func TestResourceChange_nestedList(t *testing.T) { + new_field = "new_value" # (1 unchanged attribute hidden) } - } -`, + }`, }, "force-new update (inside blocks)": { Action: plans.DeleteThenCreate, @@ -2690,8 +3236,7 @@ func TestResourceChange_nestedList(t *testing.T) { ~ root_block_device { ~ volume_type = "gp2" -> "different" # forces replacement } - } -`, + }`, }, "force-new update (whole block)": { Action: plans.DeleteThenCreate, @@ -2746,8 +3291,7 @@ func TestResourceChange_nestedList(t *testing.T) { ~ root_block_device { # forces replacement ~ volume_type = "gp2" -> "different" } - } -`, + }`, }, "in-place update - deletion": { Action: plans.Update, @@ -2794,8 +3338,7 @@ func TestResourceChange_nestedList(t *testing.T) { - root_block_device { - volume_type = "gp2" -> null } - } -`, + }`, }, "with dynamically-typed attribute": { Action: plans.Update, @@ -2834,8 +3377,7 @@ func TestResourceChange_nestedList(t *testing.T) { + block { + attr = true } - } -`, + }`, }, "in-place sequence update - deletion": { Action: plans.Update, @@ -2876,8 +3418,7 @@ func TestResourceChange_nestedList(t *testing.T) { ~ list { ~ attr = "y" -> "z" } - } -`, + }`, }, "in-place update - unknown": { Action: plans.Update, @@ -2926,8 +3467,156 @@ func TestResourceChange_nestedList(t *testing.T) { id = "i-02ae66f368e8518a9" # (1 unchanged block hidden) - } -`, + }`, + }, + "in-place create - unknown block": { + Action: plans.Create, + Mode: addrs.ManagedResourceMode, + Before: cty.NullVal(testSchemaPlus(configschema.NestingList).ImpliedType()), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingList), + ExpectedOutput: ` # test_instance.example will be created + + resource "test_instance" "example" { + + ami = "ami-AFTER" + + disks = [ + + { + + mount_point = "/var/diska" + + size = "50GB" + }, + ] + + id = "i-02ae66f368e8518a9" + + + root_block_device (known after apply) + }`, + }, + "in-place update - unknown block": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingList), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device (known after apply) + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp1" -> null + } + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp2" -> null + } + }`, + }, + "in-place update - unknown block with replace": { + Action: plans.CreateThenDelete, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet( + cty.GetAttrPath("root_block_device").IndexInt(0).GetAttr("volume_type"), + cty.GetAttrPath("root_block_device").IndexInt(1).GetAttr("volume_type"), + ), + Schema: testSchemaPlus(configschema.NestingList), + ExpectedOutput: ` # test_instance.example must be replaced ++/- resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device (known after apply) + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp1" -> null # forces replacement + } + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp2" -> null # forces replacement + } + }`, }, "in-place update - modification": { Action: plans.Update, @@ -2999,8 +3688,7 @@ func TestResourceChange_nestedList(t *testing.T) { id = "i-02ae66f368e8518a9" # (1 unchanged block hidden) - } -`, + }`, }, } runTestCases(t, testCases) @@ -3037,11 +3725,8 @@ func TestResourceChange_nestedSet(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "disks"}}, }, RequiredReplace: cty.NewPathSet(), Schema: testSchema(configschema.NestingSet), @@ -3054,8 +3739,7 @@ func TestResourceChange_nestedSet(t *testing.T) { + root_block_device { + volume_type = "gp2" } - } -`, + }`, }, "in-place update - creation": { Action: plans.Update, @@ -3101,8 +3785,7 @@ func TestResourceChange_nestedSet(t *testing.T) { + root_block_device { + volume_type = "gp2" } - } -`, + }`, }, "in-place update - creation - sensitive set": { Action: plans.Update, @@ -3133,11 +3816,8 @@ func TestResourceChange_nestedSet(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("disks"), }, RequiredReplace: cty.NewPathSet(), Schema: testSchema(configschema.NestingSet), @@ -3152,8 +3832,7 @@ func TestResourceChange_nestedSet(t *testing.T) { + root_block_device { + volume_type = "gp2" } - } -`, + }`, }, "in-place update - marking set sensitive": { Action: plans.Update, @@ -3184,11 +3863,8 @@ func TestResourceChange_nestedSet(t *testing.T) { "volume_type": cty.String, })), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("disks"), }, RequiredReplace: cty.NewPathSet(), Schema: testSchema(configschema.NestingSet), @@ -3199,8 +3875,7 @@ func TestResourceChange_nestedSet(t *testing.T) { # display in UI output after applying this change. The value is unchanged. ~ disks = (sensitive value) id = "i-02ae66f368e8518a9" - } -`, + }`, }, "in-place update - insertion": { Action: plans.Update, @@ -3251,26 +3926,25 @@ func TestResourceChange_nestedSet(t *testing.T) { ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" ~ disks = [ + - { + - mount_point = "/var/diska" -> null + }, + { + mount_point = "/var/diska" + size = "50GB" }, - - { - - mount_point = "/var/diska" -> null - }, # (1 unchanged element hidden) ] id = "i-02ae66f368e8518a9" + - root_block_device { + - volume_type = "gp2" -> null + } + root_block_device { + new_field = "new_value" + volume_type = "gp2" } - - root_block_device { - - volume_type = "gp2" -> null - } - } -`, + }`, }, "force-new update (whole block)": { Action: plans.DeleteThenCreate, @@ -3326,14 +4000,13 @@ func TestResourceChange_nestedSet(t *testing.T) { ] id = "i-02ae66f368e8518a9" - + root_block_device { # forces replacement - + volume_type = "different" - } - root_block_device { # forces replacement - volume_type = "gp2" -> null } - } -`, + + root_block_device { # forces replacement + + volume_type = "different" + } + }`, }, "in-place update - deletion": { Action: plans.Update, @@ -3383,8 +4056,7 @@ func TestResourceChange_nestedSet(t *testing.T) { - new_field = "new_value" -> null - volume_type = "gp2" -> null } - } -`, + }`, }, "in-place update - empty nested sets": { Action: plans.Update, @@ -3416,11 +4088,9 @@ func TestResourceChange_nestedSet(t *testing.T) { ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { ~ ami = "ami-BEFORE" -> "ami-AFTER" - + disks = [ - ] + + disks = [] id = "i-02ae66f368e8518a9" - } -`, + }`, }, "in-place update - null insertion": { Action: plans.Update, @@ -3468,15 +4138,14 @@ func TestResourceChange_nestedSet(t *testing.T) { ] id = "i-02ae66f368e8518a9" + - root_block_device { + - volume_type = "gp2" -> null + } + root_block_device { + new_field = "new_value" + volume_type = "gp2" } - - root_block_device { - - volume_type = "gp2" -> null - } - } -`, + }`, }, "in-place update - unknown": { Action: plans.Update, @@ -3525,8 +4194,156 @@ func TestResourceChange_nestedSet(t *testing.T) { id = "i-02ae66f368e8518a9" # (1 unchanged block hidden) - } -`, + }`, + }, + "in-place create - unknown block": { + Action: plans.Create, + Mode: addrs.ManagedResourceMode, + Before: cty.NullVal(testSchemaPlus(configschema.NestingSet).ImpliedType()), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be created + + resource "test_instance" "example" { + + ami = "ami-AFTER" + + disks = [ + + { + + mount_point = "/var/diska" + + size = "50GB" + }, + ] + + id = "i-02ae66f368e8518a9" + + + root_block_device (known after apply) + }`, + }, + "in-place update - unknown block": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device (known after apply) + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp1" -> null + } + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp2" -> null + } + }`, + }, + "in-place update - unknown block with replace": { + Action: plans.CreateThenDelete, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet( + cty.GetAttrPath("root_block_device").IndexInt(0).GetAttr("volume_type"), + cty.GetAttrPath("root_block_device").IndexInt(1).GetAttr("volume_type"), + ), + Schema: testSchemaPlus(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example must be replaced ++/- resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device (known after apply) + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp1" -> null # forces replacement + } + - root_block_device { + - new_field = "new_value" -> null + - volume_type = "gp2" -> null # forces replacement + } + }`, }, } runTestCases(t, testCases) @@ -3578,8 +4395,7 @@ func TestResourceChange_nestedMap(t *testing.T) { + root_block_device "a" { + volume_type = "gp2" } - } -`, + }`, }, "in-place update - creation": { Action: plans.Update, @@ -3625,8 +4441,7 @@ func TestResourceChange_nestedMap(t *testing.T) { + root_block_device "a" { + volume_type = "gp2" } - } -`, + }`, }, "in-place update - change attr": { Action: plans.Update, @@ -3680,8 +4495,7 @@ func TestResourceChange_nestedMap(t *testing.T) { + new_field = "new_value" # (1 unchanged attribute hidden) } - } -`, + }`, }, "in-place update - insertion": { Action: plans.Update, @@ -3746,8 +4560,7 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (1 unchanged block hidden) - } -`, + }`, }, "force-new update (whole block)": { Action: plans.DeleteThenCreate, @@ -3812,8 +4625,7 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - deletion": { Action: plans.Update, @@ -3863,8 +4675,7 @@ func TestResourceChange_nestedMap(t *testing.T) { - new_field = "new_value" -> null - volume_type = "gp2" -> null } - } -`, + }`, }, "in-place update - unknown": { Action: plans.Update, @@ -3913,8 +4724,156 @@ func TestResourceChange_nestedMap(t *testing.T) { id = "i-02ae66f368e8518a9" # (1 unchanged block hidden) - } -`, + }`, + }, + "in-place create - unknown block": { + Action: plans.Create, + Mode: addrs.ManagedResourceMode, + Before: cty.NullVal(testSchemaPlus(configschema.NestingMap).ImpliedType()), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.MapVal(map[string]cty.Value{ + "diska": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example will be created + + resource "test_instance" "example" { + + ami = "ami-AFTER" + + disks = { + + "diska" = { + + mount_point = "/var/diska" + + size = "50GB" + }, + } + + id = "i-02ae66f368e8518a9" + + + root_block_device (known after apply) + }`, + }, + "in-place update - unknown block": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.MapVal(map[string]cty.Value{ + "diska": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.MapVal(map[string]cty.Value{ + "gp1": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + "gp2": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.MapVal(map[string]cty.Value{ + "diska": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device (known after apply) + - root_block_device "gp1" { + - new_field = "new_value" -> null + - volume_type = "gp1" -> null + } + - root_block_device "gp2" { + - new_field = "new_value" -> null + - volume_type = "gp2" -> null + } + }`, + }, + "in-place update - unknown block with replace": { + Action: plans.CreateThenDelete, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.MapVal(map[string]cty.Value{ + "diska": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.MapVal(map[string]cty.Value{ + "gp1": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + "gp2": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.MapVal(map[string]cty.Value{ + "diska": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + "root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet( + cty.GetAttrPath("root_block_device").IndexString("gp1").GetAttr("volume_type"), + cty.GetAttrPath("root_block_device").IndexString("gp2").GetAttr("volume_type"), + ), + Schema: testSchemaPlus(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example must be replaced ++/- resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device (known after apply) + - root_block_device "gp1" { + - new_field = "new_value" -> null + - volume_type = "gp1" -> null # forces replacement + } + - root_block_device "gp2" { + - new_field = "new_value" -> null + - volume_type = "gp2" -> null # forces replacement + } + }`, }, "in-place update - insertion sensitive": { Action: plans.Update, @@ -3949,14 +4908,8 @@ func TestResourceChange_nestedMap(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "disks"}, - cty.IndexStep{Key: cty.StringVal("disk_a")}, - cty.GetAttrStep{Name: "mount_point"}, - }, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("disks").IndexString("disk_a").GetAttr("mount_point"), }, RequiredReplace: cty.NewPathSet(), Schema: testSchemaPlus(configschema.NestingMap), @@ -3965,15 +4918,14 @@ func TestResourceChange_nestedMap(t *testing.T) { ~ ami = "ami-BEFORE" -> "ami-AFTER" ~ disks = { + "disk_a" = { - + mount_point = (sensitive) + + mount_point = (sensitive value) + size = "50GB" }, } id = "i-02ae66f368e8518a9" # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - multiple unchanged blocks": { Action: plans.Update, @@ -4023,8 +4975,7 @@ func TestResourceChange_nestedMap(t *testing.T) { # (1 unchanged attribute hidden) # (2 unchanged blocks hidden) - } -`, + }`, }, "in-place update - multiple blocks first changed": { Action: plans.Update, @@ -4078,8 +5029,7 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - multiple blocks second changed": { Action: plans.Update, @@ -4133,8 +5083,7 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - multiple blocks changed": { Action: plans.Update, @@ -4189,8 +5138,7 @@ func TestResourceChange_nestedMap(t *testing.T) { ~ root_block_device "b" { ~ volume_type = "gp2" -> "gp3" } - } -`, + }`, }, "in-place update - multiple different unchanged blocks": { Action: plans.Update, @@ -4244,8 +5192,7 @@ func TestResourceChange_nestedMap(t *testing.T) { # (1 unchanged attribute hidden) # (2 unchanged blocks hidden) - } -`, + }`, }, "in-place update - multiple different blocks first changed": { Action: plans.Update, @@ -4303,8 +5250,7 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - multiple different blocks second changed": { Action: plans.Update, @@ -4362,8 +5308,7 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (1 unchanged block hidden) - } -`, + }`, }, "in-place update - multiple different blocks changed": { Action: plans.Update, @@ -4423,8 +5368,7 @@ func TestResourceChange_nestedMap(t *testing.T) { ~ root_block_device "a" { ~ volume_type = "gp2" -> "gp3" } - } -`, + }`, }, "in-place update - mixed blocks unchanged": { Action: plans.Update, @@ -4490,8 +5434,7 @@ func TestResourceChange_nestedMap(t *testing.T) { # (1 unchanged attribute hidden) # (4 unchanged blocks hidden) - } -`, + }`, }, "in-place update - mixed blocks changed": { Action: plans.Update, @@ -4565,8 +5508,937 @@ func TestResourceChange_nestedMap(t *testing.T) { } # (2 unchanged blocks hidden) - } -`, + }`, + }, + } + runTestCases(t, testCases) +} + +func TestResourceChange_nestedSingle(t *testing.T) { + testCases := map[string]testCase{ + "in-place update - equal": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchema(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + # (1 unchanged block hidden) + }`, + }, + "in-place update - creation": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + "disk": cty.NullVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.NullVal(cty.String), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchema(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + + disk = { + + mount_point = "/var/diska" + + size = "50GB" + } + id = "i-02ae66f368e8518a9" + + + root_block_device {} + }`, + }, + "force-new update (inside blocks)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diskb"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("different"), + }), + }), + RequiredReplace: cty.NewPathSet( + cty.Path{ + cty.GetAttrStep{Name: "root_block_device"}, + cty.GetAttrStep{Name: "volume_type"}, + }, + cty.Path{ + cty.GetAttrStep{Name: "disk"}, + cty.GetAttrStep{Name: "mount_point"}, + }, + ), + Schema: testSchema(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disk = { + ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement + # (1 unchanged attribute hidden) + } + id = "i-02ae66f368e8518a9" + + ~ root_block_device { + ~ volume_type = "gp2" -> "different" # forces replacement + } + }`, + }, + "force-new update (whole block)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diskb"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("different"), + }), + }), + RequiredReplace: cty.NewPathSet( + cty.Path{cty.GetAttrStep{Name: "root_block_device"}}, + cty.Path{cty.GetAttrStep{Name: "disk"}}, + ), + Schema: testSchema(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disk = { # forces replacement + ~ mount_point = "/var/diska" -> "/var/diskb" + # (1 unchanged attribute hidden) + } + id = "i-02ae66f368e8518a9" + + ~ root_block_device { # forces replacement + ~ volume_type = "gp2" -> "different" + } + }`, + }, + "in-place update - deletion": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + "disk": cty.NullVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchema(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + - disk = { + - mount_point = "/var/diska" -> null + - size = "50GB" -> null + } -> null + id = "i-02ae66f368e8518a9" + + - root_block_device { + - volume_type = "gp2" -> null + } + }`, + }, + "with dynamically-typed attribute": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "block": cty.NullVal(cty.Object(map[string]cty.Type{ + "attr": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "attr": {Type: cty.DynamicPseudoType, Optional: true}, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + + block { + + attr = "foo" + } + }`, + }, + "in-place update - unknown": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disk = { + ~ mount_point = "/var/diska" -> (known after apply) + ~ size = "50GB" -> (known after apply) + } -> (known after apply) + id = "i-02ae66f368e8518a9" + + # (1 unchanged block hidden) + }`, + }, + "in-place create - unknown block": { + Action: plans.Create, + Mode: addrs.ManagedResourceMode, + Before: cty.NullVal(testSchemaPlus(configschema.NestingSingle).ImpliedType()), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + })), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be created + + resource "test_instance" "example" { + + ami = "ami-AFTER" + + disk = { + + mount_point = "/var/diska" + + size = "50GB" + } + + id = "i-02ae66f368e8518a9" + + + root_block_device (known after apply) + }`, + }, + "in-place update - unknown block": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + })), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device { + ~ new_field = "new_value" -> (known after apply) + ~ volume_type = "gp1" -> (known after apply) + } -> (known after apply) + }`, + }, + "in-place update - unknown block with replace": { + Action: plans.CreateThenDelete, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp1"), + "new_field": cty.StringVal("new_value"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + "new_field": cty.String, + })), + }), + RequiredReplace: cty.NewPathSet( + cty.GetAttrPath("root_block_device").GetAttr("volume_type"), + ), + Schema: testSchemaPlus(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example must be replaced ++/- resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + ~ root_block_device { + ~ new_field = "new_value" -> (known after apply) + ~ volume_type = "gp1" -> (known after apply) # forces replacement + } -> (known after apply) + }`, + }, + "in-place update - modification": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disk": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("25GB"), + }), + "root_block_device": cty.ObjectVal(map[string]cty.Value{ + "volume_type": cty.StringVal("gp2"), + "new_field": cty.StringVal("new_value"), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaPlus(configschema.NestingSingle), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disk = { + ~ size = "50GB" -> "25GB" + # (1 unchanged attribute hidden) + } + id = "i-02ae66f368e8518a9" + + # (1 unchanged block hidden) + }`, + }, + } + runTestCases(t, testCases) +} + +func TestResourceChange_nestedMapSensitiveSchema(t *testing.T) { + testCases := map[string]testCase{ + "creation from null": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "ami": cty.NullVal(cty.String), + "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.MapVal(map[string]cty.Value{ + "disk_a": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + + ami = "ami-AFTER" + + disks = (sensitive value) + + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.MapVal(map[string]cty.Value{ + "disk_a": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) + id = "i-02ae66f368e8518a9" + }`, + }, + "force-new update (whole block)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.MapVal(map[string]cty.Value{ + "disk_a": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.MapVal(map[string]cty.Value{ + "disk_a": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("100GB"), + }), + }), + }), + RequiredReplace: cty.NewPathSet( + cty.Path{cty.GetAttrStep{Name: "disks"}}, + ), + Schema: testSchemaSensitive(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) # forces replacement + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update - deletion": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.MapVal(map[string]cty.Value{ + "disk_a": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + - disks = (sensitive value) -> null + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update - unknown": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.MapVal(map[string]cty.Value{ + "disk_a": cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingMap), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) + id = "i-02ae66f368e8518a9" + }`, + }, + } + runTestCases(t, testCases) +} + +func TestResourceChange_nestedListSensitiveSchema(t *testing.T) { + testCases := map[string]testCase{ + "creation from null": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "ami": cty.NullVal(cty.String), + "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingList), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + + ami = "ami-AFTER" + + disks = (sensitive value) + + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingList), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) + id = "i-02ae66f368e8518a9" + }`, + }, + "force-new update (whole block)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("100GB"), + }), + }), + }), + RequiredReplace: cty.NewPathSet( + cty.Path{cty.GetAttrStep{Name: "disks"}}, + ), + Schema: testSchemaSensitive(configschema.NestingList), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) # forces replacement + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update - deletion": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingList), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + - disks = (sensitive value) -> null + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update - unknown": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingList), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) + id = "i-02ae66f368e8518a9" + }`, + }, + } + runTestCases(t, testCases) +} + +func TestResourceChange_nestedSetSensitiveSchema(t *testing.T) { + testCases := map[string]testCase{ + "creation from null": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "ami": cty.NullVal(cty.String), + "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + + ami = "ami-AFTER" + + disks = (sensitive value) + + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.NullVal(cty.String), + }), + }), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) + id = "i-02ae66f368e8518a9" + }`, + }, + "force-new update (whole block)": { + Action: plans.DeleteThenCreate, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("100GB"), + }), + }), + }), + RequiredReplace: cty.NewPathSet( + cty.Path{cty.GetAttrStep{Name: "disks"}}, + ), + Schema: testSchemaSensitive(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example must be replaced +-/+ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) # forces replacement + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update - deletion": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + - disks = (sensitive value) -> null + id = "i-02ae66f368e8518a9" + }`, + }, + "in-place update - unknown": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-BEFORE"), + "disks": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "mount_point": cty.StringVal("/var/diska"), + "size": cty.StringVal("50GB"), + }), + }), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "ami": cty.StringVal("ami-AFTER"), + "disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.String, + }))), + }), + RequiredReplace: cty.NewPathSet(), + Schema: testSchemaSensitive(configschema.NestingSet), + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ ami = "ami-BEFORE" -> "ami-AFTER" + ~ disks = (sensitive value) + id = "i-02ae66f368e8518a9" + }`, }, } runTestCases(t, testCases) @@ -4587,8 +6459,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be destroyed - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because of wrong repetition mode (NoKey)": { Action: plans.Delete, @@ -4601,8 +6472,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be destroyed # (because resource uses count or for_each) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because of wrong repetition mode (IntKey)": { Action: plans.Delete, @@ -4615,8 +6485,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example[1] will be destroyed # (because resource does not use count) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because of wrong repetition mode (StringKey)": { Action: plans.Delete, @@ -4629,8 +6498,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example["a"] will be destroyed # (because resource does not use for_each) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because no resource configuration": { Action: plans.Delete, @@ -4643,8 +6511,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # module.foo.test_instance.example will be destroyed # (because test_instance.example is not in configuration) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because no module": { Action: plans.Delete, @@ -4657,8 +6524,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # module.foo[1].test_instance.example will be destroyed # (because module.foo[1] is not in configuration) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because out of range for count": { Action: plans.Delete, @@ -4671,8 +6537,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example[1] will be destroyed # (because index [1] is out of range for count) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "delete because out of range for for_each": { Action: plans.Delete, @@ -4685,8 +6550,7 @@ func TestResourceChange_actionReason(t *testing.T) { RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example["boop"] will be destroyed # (because key ["boop"] is not in for_each map) - - resource "test_instance" "example" {} -`, + - resource "test_instance" "example" {}`, }, "replace for no particular reason (delete first)": { Action: plans.DeleteThenCreate, @@ -4697,8 +6561,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" {} -`, +-/+ resource "test_instance" "example" {}`, }, "replace for no particular reason (create first)": { Action: plans.CreateThenDelete, @@ -4709,8 +6572,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example must be replaced -+/- resource "test_instance" "example" {} -`, ++/- resource "test_instance" "example" {}`, }, "replace by request (delete first)": { Action: plans.DeleteThenCreate, @@ -4721,8 +6583,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be replaced, as requested --/+ resource "test_instance" "example" {} -`, +-/+ resource "test_instance" "example" {}`, }, "replace by request (create first)": { Action: plans.CreateThenDelete, @@ -4733,8 +6594,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example will be replaced, as requested -+/- resource "test_instance" "example" {} -`, ++/- resource "test_instance" "example" {}`, }, "replace because tainted (delete first)": { Action: plans.DeleteThenCreate, @@ -4745,8 +6605,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example is tainted, so must be replaced --/+ resource "test_instance" "example" {} -`, +-/+ resource "test_instance" "example" {}`, }, "replace because tainted (create first)": { Action: plans.CreateThenDelete, @@ -4757,8 +6616,7 @@ func TestResourceChange_actionReason(t *testing.T) { Schema: emptySchema, RequiredReplace: cty.NewPathSet(), ExpectedOutput: ` # test_instance.example is tainted, so must be replaced -+/- resource "test_instance" "example" {} -`, ++/- resource "test_instance" "example" {}`, }, "replace because cannot update (delete first)": { Action: plans.DeleteThenCreate, @@ -4772,8 +6630,7 @@ func TestResourceChange_actionReason(t *testing.T) { // typically appears inline as a "# forces replacement" comment. // (not shown here) ExpectedOutput: ` # test_instance.example must be replaced --/+ resource "test_instance" "example" {} -`, +-/+ resource "test_instance" "example" {}`, }, "replace because cannot update (create first)": { Action: plans.CreateThenDelete, @@ -4787,8 +6644,7 @@ func TestResourceChange_actionReason(t *testing.T) { // typically appears inline as a "# forces replacement" comment. // (not shown here) ExpectedOutput: ` # test_instance.example must be replaced -+/- resource "test_instance" "example" {} -`, ++/- resource "test_instance" "example" {}`, }, } @@ -4830,32 +6686,14 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - // Nested blocks/sets will mark the whole set/block as sensitive - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + // Nested blocks/sets will mark the whole set/block as sensitive + cty.Path{cty.GetAttrStep{Name: "nested_block_list"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -4889,18 +6727,18 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, ExpectedOutput: ` # test_instance.example will be created + resource "test_instance" "example" { - + ami = (sensitive) + + ami = (sensitive value) + id = "i-02ae66f368e8518a9" + list_field = [ + "hello", - + (sensitive), + + (sensitive value), + "!", ] + map_key = { + "breakfast" = 800 - + "dinner" = (sensitive) + + "dinner" = (sensitive value) } - + map_whole = (sensitive) + + map_whole = (sensitive value) + nested_block_list { # At least one attribute in this block is (or was) sensitive, @@ -4911,8 +6749,7 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + }`, }, "in-place update - before sensitive": { Action: plans.Update, @@ -4975,39 +6812,15 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "special"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "special"}}, + cty.Path{cty.GetAttrStep{Name: "some_number"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -5043,29 +6856,30 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { ~ resource "test_instance" "example" { # Warning: this attribute value will no longer be marked as sensitive # after applying this change. - ~ ami = (sensitive) + ~ ami = (sensitive value) id = "i-02ae66f368e8518a9" ~ list_field = [ # (1 unchanged element hidden) "friends", - - (sensitive), - + ".", + # Warning: this attribute value will no longer be marked as sensitive + # after applying this change. + ~ (sensitive value), ] ~ map_key = { # Warning: this attribute value will no longer be marked as sensitive # after applying this change. - ~ "dinner" = (sensitive) + ~ "dinner" = (sensitive value) # (1 unchanged element hidden) } # Warning: this attribute value will no longer be marked as sensitive # after applying this change. - ~ map_whole = (sensitive) + ~ map_whole = (sensitive value) # Warning: this attribute value will no longer be marked as sensitive # after applying this change. - ~ some_number = (sensitive) + ~ some_number = (sensitive value) # Warning: this attribute value will no longer be marked as sensitive # after applying this change. - ~ special = (sensitive) + ~ special = (sensitive value) # Warning: this block will no longer be marked as sensitive # after applying this change. @@ -5074,14 +6888,14 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { # so its contents will not be displayed. } - # Warning: this block will no longer be marked as sensitive - # after applying this change. - ~ nested_block_set { + - nested_block_set { # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + + nested_block_set { + + an_attr = "changed" + } + }`, }, "in-place update - after sensitive": { Action: plans.Update, @@ -5122,27 +6936,12 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { "an_attr": cty.StringVal("changed"), }), }), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_single"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -5167,19 +6966,20 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { ~ resource "test_instance" "example" { id = "i-02ae66f368e8518a9" ~ list_field = [ - - "hello", - + (sensitive), + # Warning: this attribute value will be marked as sensitive and will not + # display in UI output after applying this change. + ~ (sensitive value), "friends", ] ~ map_key = { ~ "breakfast" = 800 -> 700 # Warning: this attribute value will be marked as sensitive and will not # display in UI output after applying this change. - ~ "dinner" = (sensitive) + ~ "dinner" = (sensitive value) } # Warning: this attribute value will be marked as sensitive and will not # display in UI output after applying this change. - ~ map_whole = (sensitive) + ~ map_whole = (sensitive value) # Warning: this block will be marked as sensitive and will not # display in UI output after applying this change. @@ -5187,8 +6987,7 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + }`, }, "in-place update - both sensitive": { Action: plans.Update, @@ -5235,49 +7034,19 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, }, - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_map"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -5301,25 +7070,23 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, ExpectedOutput: ` # test_instance.example will be updated in-place ~ resource "test_instance" "example" { - ~ ami = (sensitive) + ~ ami = (sensitive value) id = "i-02ae66f368e8518a9" ~ list_field = [ - - (sensitive), - + (sensitive), + ~ (sensitive value), "friends", ] ~ map_key = { - ~ "dinner" = (sensitive) + ~ "dinner" = (sensitive value) # (1 unchanged element hidden) } - ~ map_whole = (sensitive) + ~ map_whole = (sensitive value) - ~ nested_block_map { + ~ nested_block_map "foo" { # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + }`, }, "in-place update - value unchanged, sensitivity changes": { Action: plans.Update, @@ -5382,39 +7149,15 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "special"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "some_number"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "special"}}, + cty.Path{cty.GetAttrStep{Name: "some_number"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -5450,29 +7193,30 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { ~ resource "test_instance" "example" { # Warning: this attribute value will no longer be marked as sensitive # after applying this change. The value is unchanged. - ~ ami = (sensitive) + ~ ami = (sensitive value) id = "i-02ae66f368e8518a9" ~ list_field = [ # (1 unchanged element hidden) "friends", - - (sensitive), - + "!", + # Warning: this attribute value will no longer be marked as sensitive + # after applying this change. The value is unchanged. + ~ (sensitive value), ] ~ map_key = { # Warning: this attribute value will no longer be marked as sensitive # after applying this change. The value is unchanged. - ~ "dinner" = (sensitive) + ~ "dinner" = (sensitive value) # (1 unchanged element hidden) } # Warning: this attribute value will no longer be marked as sensitive # after applying this change. The value is unchanged. - ~ map_whole = (sensitive) + ~ map_whole = (sensitive value) # Warning: this attribute value will no longer be marked as sensitive # after applying this change. The value is unchanged. - ~ some_number = (sensitive) + ~ some_number = (sensitive value) # Warning: this attribute value will no longer be marked as sensitive # after applying this change. The value is unchanged. - ~ special = (sensitive) + ~ special = (sensitive value) # Warning: this block will no longer be marked as sensitive # after applying this change. @@ -5487,8 +7231,7 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + }`, }, "deletion": { Action: plans.Delete, @@ -5522,31 +7265,13 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), After: cty.NullVal(cty.EmptyObject), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "ami"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "map_whole"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.Path{cty.GetAttrStep{Name: "ami"}}, + cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}}, + cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}}, + cty.Path{cty.GetAttrStep{Name: "map_whole"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block"}}, + cty.Path{cty.GetAttrStep{Name: "nested_block_set"}}, }, RequiredReplace: cty.NewPathSet(), Schema: &configschema.Block{ @@ -5571,24 +7296,23 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }, ExpectedOutput: ` # test_instance.example will be destroyed - resource "test_instance" "example" { - - ami = (sensitive) -> null + - ami = (sensitive value) -> null - id = "i-02ae66f368e8518a9" -> null - list_field = [ - "hello", - - (sensitive), + - (sensitive value), ] -> null - map_key = { - "breakfast" = 800 - - "dinner" = (sensitive) + - "dinner" = (sensitive value) } -> null - - map_whole = (sensitive) -> null + - map_whole = (sensitive value) -> null - nested_block_set { # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + }`, }, "update with sensitive value forcing replacement": { Action: plans.DeleteThenCreate, @@ -5611,25 +7335,13 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { }), }), }), - BeforeValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("ami"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nested_block_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + BeforeSensitivePaths: []cty.Path{ + cty.GetAttrPath("ami"), + cty.GetAttrPath("nested_block_set"), }, - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("ami"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, - { - Path: cty.GetAttrPath("nested_block_set"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("ami"), + cty.GetAttrPath("nested_block_set"), }, Schema: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -5653,15 +7365,18 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { ), ExpectedOutput: ` # test_instance.example must be replaced -/+ resource "test_instance" "example" { - ~ ami = (sensitive) # forces replacement + ~ ami = (sensitive value) # forces replacement id = "i-02ae66f368e8518a9" - ~ nested_block_set { # forces replacement + - nested_block_set { # forces replacement # At least one attribute in this block is (or was) sensitive, # so its contents will not be displayed. } - } -`, + + nested_block_set { # forces replacement + # At least one attribute in this block is (or was) sensitive, + # so its contents will not be displayed. + } + }`, }, "update with sensitive attribute forcing replacement": { Action: plans.DeleteThenCreate, @@ -5687,8 +7402,7 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { -/+ resource "test_instance" "example" { ~ ami = (sensitive value) # forces replacement id = "i-02ae66f368e8518a9" - } -`, + }`, }, "update with sensitive nested type attribute forcing replacement": { Action: plans.DeleteThenCreate, @@ -5732,8 +7446,278 @@ func TestResourceChange_sensitiveVariable(t *testing.T) { # (1 unchanged attribute hidden) } id = "i-02ae66f368e8518a9" - } -`, + }`, + }, + } + runTestCases(t, testCases) +} + +func TestResourceChange_writeOnlyAttributes(t *testing.T) { + testCases := map[string]testCase{ + "create": { + Action: plans.Create, + Mode: addrs.ManagedResourceMode, + Before: cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "write_only": cty.String, + "conn_info": cty.Object(map[string]cty.Type{ + "user": cty.String, + "write_only_set_password": cty.String, + }), + "block": cty.List(cty.Object(map[string]cty.Type{ + "user": cty.String, + "block_set_password": cty.String, + })), + })), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "write_only": cty.NullVal(cty.String), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "write_only_set_password": cty.NullVal(cty.String), + }), + "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("this-is-not-secret"), + "block_set_password": cty.NullVal(cty.String), + })}), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "write_only": {Type: cty.String, WriteOnly: true}, + "conn_info": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + }, + ExpectedOutput: ` # test_instance.example will be created + + resource "test_instance" "example" { + + conn_info = { + + user = "not-secret" + + write_only_set_password = (sensitive, write-only attribute) + } + + id = "i-02ae66f368e8518a9" + + write_only = (write-only attribute) + + + block { + + block_set_password = (write-only attribute) + + user = "this-is-not-secret" + } + }`, + }, + "update attribute": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "write_only": cty.NullVal(cty.String), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "write_only_set_password": cty.NullVal(cty.String), + }), + "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-so-secret"), + "block_set_password": cty.NullVal(cty.String), + })}), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a10"), + "write_only": cty.NullVal(cty.String), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "write_only_set_password": cty.NullVal(cty.String), + }), + "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-so-secret"), + "block_set_password": cty.NullVal(cty.String), + })}), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "write_only": {Type: cty.String, WriteOnly: true}, + "conn_info": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + }, + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + ~ id = "i-02ae66f368e8518a9" -> "i-02ae66f368e8518a10" + # (2 unchanged attributes hidden) + + # (1 unchanged block hidden) + }`, + }, + "update - delete block": { + Action: plans.Update, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "write_only": cty.NullVal(cty.String), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "write_only_set_password": cty.NullVal(cty.String), + }), + "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "block_set_password": cty.NullVal(cty.String), + })}), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "write_only": cty.NullVal(cty.String), + "conn_info": cty.NullVal(cty.Object(map[string]cty.Type{ + "user": cty.String, + "write_only_set_password": cty.String, + })), + "block": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "user": cty.String, + "block_set_password": cty.String, + }))), + }), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "write_only": {Type: cty.String, WriteOnly: true}, + "conn_info": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + }, + ExpectedOutput: ` # test_instance.example will be updated in-place + ~ resource "test_instance" "example" { + - conn_info = { + - user = "not-secret" -> null + - write_only_set_password = (sensitive, write-only attribute) -> null + } -> null + id = "i-02ae66f368e8518a9" + # (1 unchanged attribute hidden) + + - block { + - block_set_password = (write-only attribute) -> null + - user = "not-secret" -> null + } + }`, + }, + "delete": { + Action: plans.Delete, + Mode: addrs.ManagedResourceMode, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-02ae66f368e8518a9"), + "write_only": cty.NullVal(cty.String), + "conn_info": cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "write_only_set_password": cty.NullVal(cty.String), + }), + "block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "user": cty.StringVal("not-secret"), + "block_set_password": cty.NullVal(cty.String), + })}), + }), + After: cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "write_only": cty.String, + "conn_info": cty.Object(map[string]cty.Type{ + "user": cty.String, + "write_only_set_password": cty.String, + }), + "block": cty.List(cty.Object(map[string]cty.Type{ + "user": cty.String, + "block_set_password": cty.String, + })), + })), + Schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "write_only": {Type: cty.String, WriteOnly: true}, + "conn_info": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "user": {Type: cty.String, Optional: true}, + "block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true}, + }, + }, + }, + }, + }, + ExpectedOutput: ` # test_instance.example will be destroyed + - resource "test_instance" "example" { + - conn_info = { + - user = "not-secret" -> null + - write_only_set_password = (sensitive, write-only attribute) -> null + } -> null + - id = "i-02ae66f368e8518a9" -> null + - write_only = (write-only attribute) -> null + + - block { + - block_set_password = (write-only attribute) -> null + - user = "not-secret" -> null + } + }`, }, } runTestCases(t, testCases) @@ -5775,8 +7759,7 @@ func TestResourceChange_moved(t *testing.T) { ~ bar = "baz" -> "boop" id = "12345" # (1 unchanged attribute hidden) - } -`, + }`, }, "moved without changes": { PrevRunAddr: prevRunAddr, @@ -5804,8 +7787,7 @@ func TestResourceChange_moved(t *testing.T) { resource "test_instance" "example" { id = "12345" # (2 unchanged attributes hidden) - } -`, + }`, }, } @@ -5813,20 +7795,20 @@ func TestResourceChange_moved(t *testing.T) { } type testCase struct { - Action plans.Action - ActionReason plans.ResourceInstanceChangeActionReason - ModuleInst addrs.ModuleInstance - Mode addrs.ResourceMode - InstanceKey addrs.InstanceKey - DeposedKey states.DeposedKey - Before cty.Value - BeforeValMarks []cty.PathValueMarks - AfterValMarks []cty.PathValueMarks - After cty.Value - Schema *configschema.Block - RequiredReplace cty.PathSet - ExpectedOutput string - PrevRunAddr addrs.AbsResourceInstance + Action plans.Action + ActionReason plans.ResourceInstanceChangeActionReason + ModuleInst addrs.ModuleInstance + Mode addrs.ResourceMode + InstanceKey addrs.InstanceKey + DeposedKey states.DeposedKey + Before cty.Value + BeforeSensitivePaths []cty.Path + After cty.Value + AfterSensitivePaths []cty.Path + Schema *configschema.Block + RequiredReplace cty.PathSet + ExpectedOutput string + PrevRunAddr addrs.AbsResourceInstance } func runTestCases(t *testing.T, testCases map[string]testCase) { @@ -5865,7 +7847,25 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { prevRunAddr = addr } - change := &plans.ResourceInstanceChange{ + beforeDynamicValue, err := plans.NewDynamicValue(beforeVal, ty) + if err != nil { + t.Fatalf("failed to create dynamic before value: %s", err) + } + + afterDynamicValue, err := plans.NewDynamicValue(afterVal, ty) + if err != nil { + t.Fatalf("failed to create dynamic after value: %s", err) + } + + src := &plans.ResourceInstanceChangeSrc{ + ChangeSrc: plans.ChangeSrc{ + Action: tc.Action, + Before: beforeDynamicValue, + BeforeSensitivePaths: tc.BeforeSensitivePaths, + After: afterDynamicValue, + AfterSensitivePaths: tc.AfterSensitivePaths, + }, + Addr: addr, PrevRunAddr: prevRunAddr, DeposedKey: tc.DeposedKey, @@ -5873,18 +7873,42 @@ func runTestCases(t *testing.T, testCases map[string]testCase) { Provider: addrs.NewDefaultProvider("test"), Module: addrs.RootModule, }, - Change: plans.Change{ - Action: tc.Action, - Before: beforeVal.MarkWithPaths(tc.BeforeValMarks), - After: afterVal.MarkWithPaths(tc.AfterValMarks), - }, ActionReason: tc.ActionReason, RequiredReplace: tc.RequiredReplace, } - output := ResourceChange(change, tc.Schema, color, DiffLanguageProposedChange) + tfschemas := &terraform.Schemas{ + Providers: map[addrs.Provider]providers.ProviderSchema{ + src.ProviderAddr.Provider: { + ResourceTypes: map[string]providers.Schema{ + src.Addr.Resource.Resource.Type: { + Body: tc.Schema, + }, + }, + DataSources: map[string]providers.Schema{ + src.Addr.Resource.Resource.Type: { + Body: tc.Schema, + }, + }, + }, + }, + } + jsonchanges, err := jsonplan.MarshalResourceChanges([]*plans.ResourceInstanceChangeSrc{src}, tfschemas) + if err != nil { + t.Errorf("failed to marshal resource changes: %s", err) + return + } + + jsonschemas := jsonprovider.MarshalForRenderer(tfschemas) + change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher()) + renderer := Renderer{Colorize: color} + diff := diff{ + change: jsonchanges[0], + diff: differ.ComputeDiffForBlock(change, jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block), + } + output, _ := renderHumanDiff(renderer, diff, proposedChange) if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" { - t.Errorf("wrong output\n%s", diff) + t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.ExpectedOutput, output, diff) } }) } @@ -5906,8 +7930,7 @@ func TestOutputChanges(t *testing.T) { false, ), }, - ` - + foo = "bar"`, + ` + foo = "bar"`, }, "removed output": { []*plans.OutputChangeSrc{ @@ -5918,8 +7941,7 @@ func TestOutputChanges(t *testing.T) { false, ), }, - ` - - foo = "bar" -> null`, + ` - foo = "bar" -> null`, }, "single string change": { []*plans.OutputChangeSrc{ @@ -5930,8 +7952,7 @@ func TestOutputChanges(t *testing.T) { false, ), }, - ` - ~ foo = "bar" -> "baz"`, + ` ~ foo = "bar" -> "baz"`, }, "element added to list": { []*plans.OutputChangeSrc{ @@ -5953,8 +7974,7 @@ func TestOutputChanges(t *testing.T) { false, ), }, - ` - ~ foo = [ + ` ~ foo = [ # (1 unchanged element hidden) "beta", + "gamma", @@ -5983,8 +8003,7 @@ func TestOutputChanges(t *testing.T) { false, ), }, - ` - ~ a = 1 -> 2 + ` ~ a = 1 -> 2 ~ b = (sensitive value) ~ c = false -> true`, }, @@ -5992,7 +8011,21 @@ func TestOutputChanges(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - output := OutputChanges(tc.changes, color) + changes := &plans.ChangesSrc{ + Outputs: tc.changes, + } + + outputs, err := jsonplan.MarshalOutputChanges(changes) + if err != nil { + t.Fatalf("failed to marshal output changes") + } + + renderer := Renderer{Colorize: color} + diffs := precomputeDiffs(Plan{ + OutputChanges: outputs, + }, plans.NormalMode) + + output := renderHumanDiffOutputs(renderer, diffs.outputs) if output != tc.output { t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output) } @@ -6000,6 +8033,238 @@ func TestOutputChanges(t *testing.T) { } } +func TestResourceChange_deferredActions(t *testing.T) { + color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} + providerAddr := addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + } + testCases := map[string]struct { + changes []*plans.DeferredResourceInstanceChange + output string + }{ + "deferred create action": { + changes: []*plans.DeferredResourceInstanceChange{ + { + DeferredReason: providers.DeferredReasonAbsentPrereq, + Change: &plans.ResourceInstanceChange{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: providerAddr, + Change: plans.Change{ + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "ami": cty.StringVal("bar"), + "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.Number, + })), + "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + }, + }, + }, + }, + output: ` # test_instance.instance was deferred + # (because a prerequisite for this resource has not yet been created) + + resource "test_instance" "instance" { + + ami = "bar" + + disk = (known after apply) + + id = (known after apply) + + + root_block_device (known after apply) + }`, + }, + + "deferred create action unknown for_each": { + changes: []*plans.DeferredResourceInstanceChange{ + { + DeferredReason: providers.DeferredReasonInstanceCountUnknown, + Change: &plans.ResourceInstanceChange{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }.Instance(addrs.WildcardKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: providerAddr, + Change: plans.Change{ + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "ami": cty.StringVal("bar"), + "disk": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.Number, + })), + "root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + }, + }, + }, + }, + output: ` # test_instance.instance[*] was deferred + # (because the number of resource instances is unknown) + + resource "test_instance" "instance" { + + ami = "bar" + + disk = (known after apply) + + id = (known after apply) + + + root_block_device (known after apply) + }`, + }, + + "deferred update action": { + changes: []*plans.DeferredResourceInstanceChange{ + { + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + Change: &plans.ResourceInstanceChange{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: providerAddr, + Change: plans.Change{ + Action: plans.Update, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "ami": cty.StringVal("bar"), + "disk": cty.NullVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.Number, + })), + "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "ami": cty.StringVal("baz"), + "disk": cty.NullVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.Number, + })), + "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + }, + }, + }, + }, + output: ` # test_instance.instance was deferred + # (because the provider configuration is unknown) + ~ resource "test_instance" "instance" { + ~ ami = "bar" -> "baz" + id = "foo" + }`, + }, + + "deferred destroy action": { + changes: []*plans.DeferredResourceInstanceChange{ + { + DeferredReason: providers.DeferredReasonResourceConfigUnknown, + Change: &plans.ResourceInstanceChange{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: providerAddr, + Change: plans.Change{ + Action: plans.Update, + Before: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "ami": cty.StringVal("bar"), + "disk": cty.NullVal(cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.Number, + })), + "root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + })), + }), + After: cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "ami": cty.String, + "disk": cty.Object(map[string]cty.Type{ + "mount_point": cty.String, + "size": cty.Number, + }), + "root_block_device": cty.Object(map[string]cty.Type{ + "volume_type": cty.String, + }), + })), + }, + }, + }, + }, + output: ` # test_instance.instance was deferred + # (because the resource configuration is unknown) + ~ resource "test_instance" "instance" { + - ami = "bar" -> null + - id = "foo" -> null + }`, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + blockSchema := testSchema(configschema.NestingSingle) + fullSchema := &terraform.Schemas{ + Providers: map[addrs.Provider]providers.ProviderSchema{ + providerAddr.Provider: { + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Body: blockSchema, + }, + }, + }, + }, + } + var changes []*plans.DeferredResourceInstanceChangeSrc + for _, change := range tc.changes { + changeSrc, err := change.Encode(providers.Schema{ + Body: blockSchema, + }) + if err != nil { + t.Fatalf("Failed to encode change: %s", err) + } + changes = append(changes, changeSrc) + } + + deferredChanges, err := jsonplan.MarshalDeferredResourceChanges(changes, fullSchema) + if err != nil { + t.Fatalf("failed to marshal deferred changes: %s", err) + } + + renderer := Renderer{Colorize: color} + jsonschemas := jsonprovider.MarshalForRenderer(fullSchema) + diffs := precomputeDiffs(Plan{ + DeferredChanges: deferredChanges, + ProviderSchemas: jsonschemas, + }, plans.NormalMode) + + // TODO: Add diffing for outputs + // TODO: Make sure it's true and either make it a single entity in the test case or deal with a list here + output, _ := renderHumanDeferredDiff(renderer, diffs.deferred[0]) + if diff := cmp.Diff(tc.output, output); len(diff) > 0 { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", output, tc.output, diff) + } + }) + } +} + func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc { addr := addrs.AbsOutputValue{ OutputValue: addrs.OutputValue{Name: name}, @@ -6023,11 +8288,16 @@ func outputChange(name string, before, after cty.Value, sensitive bool) *plans.O // A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block func testSchema(nesting configschema.NestingMode) *configschema.Block { + var diskKey = "disks" + if nesting == configschema.NestingSingle { + diskKey = "disk" + } + return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, - "disks": { + diskKey: { NestedType: &configschema.Object{ Attributes: map[string]*configschema.Attribute{ "mount_point": {Type: cty.String, Optional: true}, @@ -6054,6 +8324,27 @@ func testSchema(nesting configschema.NestingMode) *configschema.Block { } } +// A basic test schema using a configurable NestingMode for one (NestedType) +// attribute marked sensitive. +func testSchemaSensitive(nesting configschema.NestingMode) *configschema.Block { + return &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "disks": { + Sensitive: true, + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "mount_point": {Type: cty.String, Optional: true}, + "size": {Type: cty.String, Optional: true}, + }, + Nesting: nesting, + }, + }, + }, + } +} + func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block { return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -6100,11 +8391,16 @@ func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Bl // similar to testSchema with the addition of a "new_field" block func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { + var diskKey = "disks" + if nesting == configschema.NestingSingle { + diskKey = "disk" + } + return &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, - "disks": { + diskKey: { NestedType: &configschema.Object{ Attributes: map[string]*configschema.Attribute{ "mount_point": {Type: cty.String, Optional: true}, @@ -6135,3 +8431,11 @@ func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block { }, } } + +func marshalJson(t *testing.T, data interface{}) json.RawMessage { + result, err := json.Marshal(data) + if err != nil { + t.Fatalf("failed to marshal json: %v", err) + } + return result +} diff --git a/internal/command/jsonformat/renderer.go b/internal/command/jsonformat/renderer.go new file mode 100644 index 0000000000..a3463ac8c5 --- /dev/null +++ b/internal/command/jsonformat/renderer.go @@ -0,0 +1,299 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat + +import ( + "fmt" + "strconv" + + "github.com/mitchellh/colorstring" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/differ" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" + viewsjson "github.com/hashicorp/terraform/internal/command/views/json" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/terminal" +) + +type JSONLogType string + +type JSONLog struct { + Message string `json:"@message"` + Type JSONLogType `json:"type"` + Diagnostic *viewsjson.Diagnostic `json:"diagnostic"` + Outputs viewsjson.Outputs `json:"outputs"` + Hook map[string]interface{} `json:"hook"` + + // Special fields for test messages. + + TestRun string `json:"@testrun,omitempty"` + TestFile string `json:"@testfile,omitempty"` + + TestFileStatus *viewsjson.TestFileStatus `json:"test_file,omitempty"` + TestRunStatus *viewsjson.TestRunStatus `json:"test_run,omitempty"` + TestFileCleanup *viewsjson.TestFileCleanup `json:"test_cleanup,omitempty"` + TestSuiteSummary *viewsjson.TestSuiteSummary `json:"test_summary,omitempty"` + TestFatalInterrupt *viewsjson.TestFatalInterrupt `json:"test_interrupt,omitempty"` + TestState *State `json:"test_state,omitempty"` + TestPlan *Plan `json:"test_plan,omitempty"` +} + +const ( + LogApplyComplete JSONLogType = "apply_complete" + LogApplyErrored JSONLogType = "apply_errored" + LogApplyStart JSONLogType = "apply_start" + LogChangeSummary JSONLogType = "change_summary" + LogDiagnostic JSONLogType = "diagnostic" + LogPlannedChange JSONLogType = "planned_change" + LogProvisionComplete JSONLogType = "provision_complete" + LogProvisionErrored JSONLogType = "provision_errored" + LogProvisionProgress JSONLogType = "provision_progress" + LogProvisionStart JSONLogType = "provision_start" + LogOutputs JSONLogType = "outputs" + LogRefreshComplete JSONLogType = "refresh_complete" + LogRefreshStart JSONLogType = "refresh_start" + LogResourceDrift JSONLogType = "resource_drift" + LogVersion JSONLogType = "version" + + // Ephemeral operation messages + LogEphemeralOpStart JSONLogType = "ephemeral_op_start" + LogEphemeralOpComplete JSONLogType = "ephemeral_op_complete" + LogEphemeralOpErrored JSONLogType = "ephemeral_op_errored" + + // Test Messages + LogTestAbstract JSONLogType = "test_abstract" + LogTestFile JSONLogType = "test_file" + LogTestRun JSONLogType = "test_run" + LogTestPlan JSONLogType = "test_plan" + LogTestState JSONLogType = "test_state" + LogTestSummary JSONLogType = "test_summary" + LogTestCleanup JSONLogType = "test_cleanup" + LogTestInterrupt JSONLogType = "test_interrupt" + LogTestStatus JSONLogType = "test_status" + LogTestRetry JSONLogType = "test_retry" +) + +func incompatibleVersions(localVersion, remoteVersion string) bool { + var parsedLocal, parsedRemote float64 + var err error + + if parsedLocal, err = strconv.ParseFloat(localVersion, 64); err != nil { + return false + } + if parsedRemote, err = strconv.ParseFloat(remoteVersion, 64); err != nil { + return false + } + + // If the local version is less than the remote version then the remote + // version might contain things the local version doesn't know about, so + // we're going to say they are incompatible. + // + // So far, we have built the renderer and the json packages to be backwards + // compatible so if the local version is greater than the remote version + // then that is okay, we'll still render a complete and correct plan. + // + // Note, this might change in the future. For example, if we introduce a + // new major version in one of the formats the renderer may no longer be + // backward compatible. + return parsedLocal < parsedRemote +} + +type Renderer struct { + Streams *terminal.Streams + Colorize *colorstring.Colorize + + RunningInAutomation bool +} + +func (renderer Renderer) RenderHumanPlan(plan Plan, mode plans.Mode, opts ...plans.Quality) { + if incompatibleVersions(jsonplan.FormatVersion, plan.PlanFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, plan.ProviderFormatVersion) { + renderer.Streams.Println(format.WordWrap( + renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This plan was generated using a different version of Terraform, the diff presented here may be missing representations of recent features."), + renderer.Streams.Stdout.Columns())) + } + + plan.renderHuman(renderer, mode, opts...) +} + +func (renderer Renderer) RenderHumanState(state State) { + if incompatibleVersions(jsonstate.FormatVersion, state.StateFormatVersion) || incompatibleVersions(jsonprovider.FormatVersion, state.ProviderFormatVersion) { + renderer.Streams.Println(format.WordWrap( + renderer.Colorize.Color("\n[bold][red]Warning:[reset][bold] This state was retrieved using a different version of Terraform, the state presented here maybe missing representations of recent features."), + renderer.Streams.Stdout.Columns())) + } + + if state.Empty() { + renderer.Streams.Println("The state file is empty. No resources are represented.") + return + } + + opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts.ShowUnchangedChildren = true + opts.HideDiffActionSymbols = true + + state.renderHumanStateModule(renderer, state.RootModule, opts, true) + state.renderHumanStateOutputs(renderer, opts) +} + +func (renderer Renderer) RenderLog(log *JSONLog) error { + switch log.Type { + case LogRefreshComplete, + LogVersion, + LogPlannedChange, + LogProvisionComplete, + LogProvisionErrored, + LogApplyErrored, + LogEphemeralOpErrored, + LogTestAbstract, + LogTestStatus, + LogTestRetry, + LogTestPlan, + LogTestState, + LogTestInterrupt: + // We won't display these types of logs + return nil + + case LogApplyStart, LogApplyComplete, LogRefreshStart, LogProvisionStart, LogResourceDrift, LogEphemeralOpStart, LogEphemeralOpComplete: + msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s[reset]"), log.Message) + renderer.Streams.Println(msg) + + case LogDiagnostic: + diag := format.DiagnosticFromJSON(log.Diagnostic, renderer.Colorize, 78) + renderer.Streams.Print(diag) + + case LogOutputs: + if len(log.Outputs) > 0 { + renderer.Streams.Println(renderer.Colorize.Color("[bold][green]Outputs:[reset]")) + for name, output := range log.Outputs { + change := structured.FromJsonViewsOutput(output) + ctype, err := ctyjson.UnmarshalType(output.Type) + if err != nil { + return err + } + + opts := computed.NewRenderHumanOpts(renderer.Colorize) + opts.ShowUnchangedChildren = true + + outputDiff := differ.ComputeDiffForType(change, ctype) + outputStr := outputDiff.RenderHuman(0, opts) + + msg := fmt.Sprintf("%s = %s", name, outputStr) + renderer.Streams.Println(msg) + } + } + + case LogProvisionProgress: + provisioner := log.Hook["provisioner"].(string) + output := log.Hook["output"].(string) + resource := log.Hook["resource"].(map[string]interface{}) + resourceAddr := resource["addr"].(string) + + msg := fmt.Sprintf(renderer.Colorize.Color("[bold]%s: (%s):[reset] %s"), + resourceAddr, provisioner, output) + renderer.Streams.Println(msg) + + case LogChangeSummary: + // Normally, we will only render the apply change summary since the renderer + // generates a plan change summary for us + msg := fmt.Sprintf(renderer.Colorize.Color("[bold][green]%s[reset]"), log.Message) + renderer.Streams.Println("\n" + msg + "\n") + + case LogTestFile: + status := log.TestFileStatus + + var msg string + switch status.Progress { + case "starting": + msg = fmt.Sprintf(renderer.Colorize.Color("%s... [light_gray]in progress[reset]"), status.Path) + case "teardown": + msg = fmt.Sprintf(renderer.Colorize.Color("%s... [light_gray]tearing down[reset]"), status.Path) + case "complete": + switch status.Status { + case "error", "fail": + msg = fmt.Sprintf(renderer.Colorize.Color("%s... [red]fail[reset]"), status.Path) + case "pass": + msg = fmt.Sprintf(renderer.Colorize.Color("%s... [green]pass[reset]"), status.Path) + case "skip", "pending": + msg = fmt.Sprintf(renderer.Colorize.Color("%s... [light_gray]%s[reset]"), status.Path, string(status.Status)) + } + case "running": + // Don't print anything for the running status. + break + } + + renderer.Streams.Println(msg) + + case LogTestRun: + status := log.TestRunStatus + + if status.Progress != "complete" { + // Don't print anything for status updates, we only report when the + // run is actually finished. + break + } + + var msg string + switch status.Status { + case "error", "fail": + msg = fmt.Sprintf(renderer.Colorize.Color(" %s... [red]fail[reset]"), status.Run) + case "pass": + msg = fmt.Sprintf(renderer.Colorize.Color(" %s... [green]pass[reset]"), status.Run) + case "skip", "pending": + msg = fmt.Sprintf(renderer.Colorize.Color(" %s... [light_gray]%s[reset]"), status.Run, string(status.Status)) + } + + renderer.Streams.Println(msg) + + case LogTestSummary: + renderer.Streams.Println() // We start our summary with a line break. + + summary := log.TestSuiteSummary + + switch summary.Status { + case "pending", "skip": + renderer.Streams.Print("Executed 0 tests") + if summary.Skipped > 0 { + renderer.Streams.Printf(", %d skipped.\n", summary.Skipped) + } else { + renderer.Streams.Println(".") + } + return nil + case "pass": + renderer.Streams.Print(renderer.Colorize.Color("[green]Success![reset] ")) + case "fail", "error": + renderer.Streams.Print(renderer.Colorize.Color("[red]Failure![reset] ")) + } + + renderer.Streams.Printf("%d passed, %d failed", summary.Passed, summary.Failed+summary.Errored) + if summary.Skipped > 0 { + renderer.Streams.Printf(", %d skipped.\n", summary.Skipped) + } else { + renderer.Streams.Println(".") + } + + case LogTestCleanup: + cleanup := log.TestFileCleanup + + renderer.Streams.Eprintln(format.WordWrap(log.Message, renderer.Streams.Stderr.Columns())) + for _, resource := range cleanup.FailedResources { + if len(resource.DeposedKey) > 0 { + renderer.Streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey) + } else { + renderer.Streams.Eprintf(" - %s\n", resource.Instance) + } + } + + default: + // If the log type is not a known log type, we will just print the log message + renderer.Streams.Println(log.Message) + } + + return nil +} diff --git a/internal/command/jsonformat/renderer_test.go b/internal/command/jsonformat/renderer_test.go new file mode 100644 index 0000000000..ffa0260705 --- /dev/null +++ b/internal/command/jsonformat/renderer_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" +) + +func TestIncompatibleVersions(t *testing.T) { + tcs := map[string]struct { + local string + remote string + expected bool + }{ + "matching": { + local: "1.1", + remote: "1.1", + expected: false, + }, + "local_latest": { + local: "1.2", + remote: "1.1", + expected: false, + }, + "local_earliest": { + local: "1.1", + remote: "1.2", + expected: true, + }, + "parses_state_version": { + local: jsonstate.FormatVersion, + remote: jsonstate.FormatVersion, + expected: false, + }, + "parses_provider_version": { + local: jsonprovider.FormatVersion, + remote: jsonprovider.FormatVersion, + expected: false, + }, + "parses_plan_version": { + local: jsonplan.FormatVersion, + remote: jsonplan.FormatVersion, + expected: false, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + actual := incompatibleVersions(tc.local, tc.remote) + if actual != tc.expected { + t.Errorf("expected %t but found %t", tc.expected, actual) + } + }) + } +} diff --git a/internal/command/jsonformat/state.go b/internal/command/jsonformat/state.go new file mode 100644 index 0000000000..04db2b24e5 --- /dev/null +++ b/internal/command/jsonformat/state.go @@ -0,0 +1,105 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat + +import ( + "maps" + "slices" + + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/command/jsonformat/differ" + "github.com/hashicorp/terraform/internal/command/jsonformat/structured" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" +) + +type State struct { + StateFormatVersion string `json:"state_format_version"` + RootModule jsonstate.Module `json:"root_module,omitempty"` + RootModuleOutputs map[string]jsonstate.Output `json:"outputs,omitempty"` + + ProviderFormatVersion string `json:"provider_format_version"` + ProviderSchemas map[string]*jsonprovider.Provider `json:"provider_schemas,omitempty"` +} + +func (state State) Empty() bool { + return len(state.RootModuleOutputs) == 0 && len(state.RootModule.Resources) == 0 && len(state.RootModule.ChildModules) == 0 +} + +func (state State) GetSchema(resource jsonstate.Resource) *jsonprovider.Schema { + switch resource.Mode { + case jsonstate.ManagedResourceMode: + return state.ProviderSchemas[resource.ProviderName].ResourceSchemas[resource.Type] + case jsonstate.DataResourceMode: + return state.ProviderSchemas[resource.ProviderName].DataSourceSchemas[resource.Type] + default: + panic("found unrecognized resource mode: " + resource.Mode) + } +} + +func (state State) renderHumanStateModule(renderer Renderer, module jsonstate.Module, opts computed.RenderHumanOpts, first bool) { + if len(module.Resources) > 0 && !first { + renderer.Streams.Println() + } + + for _, resource := range module.Resources { + + if !first { + renderer.Streams.Println() + } + + if first { + first = false + } + + if len(resource.DeposedKey) > 0 { + renderer.Streams.Printf("# %s: (deposed object %s)", resource.Address, resource.DeposedKey) + } else if resource.Tainted { + renderer.Streams.Printf("# %s: (tainted)", resource.Address) + } else { + renderer.Streams.Printf("# %s:", resource.Address) + } + + renderer.Streams.Println() + + schema := state.GetSchema(resource) + switch resource.Mode { + case jsonstate.ManagedResourceMode: + change := structured.FromJsonResource(resource) + renderer.Streams.Printf("resource %q %q %s", resource.Type, resource.Name, differ.ComputeDiffForBlock(change, schema.Block).RenderHuman(0, opts)) + case jsonstate.DataResourceMode: + change := structured.FromJsonResource(resource) + renderer.Streams.Printf("data %q %q %s", resource.Type, resource.Name, differ.ComputeDiffForBlock(change, schema.Block).RenderHuman(0, opts)) + default: + panic("found unrecognized resource mode: " + resource.Mode) + } + + renderer.Streams.Println() + } + + for _, child := range module.ChildModules { + state.renderHumanStateModule(renderer, child, opts, first) + } +} + +func (state State) renderHumanStateOutputs(renderer Renderer, opts computed.RenderHumanOpts) { + if len(state.RootModuleOutputs) > 0 { + renderer.Streams.Printf("\n\nOutputs:\n\n") + + for _, key := range slices.Sorted(maps.Keys(state.RootModuleOutputs)) { + output := state.RootModuleOutputs[key] + change := structured.FromJsonOutput(output) + ctype, err := ctyjson.UnmarshalType(output.Type) + if err != nil { + // We can actually do this without the type, so even if we fail + // to work out the type let's just render this anyway. + renderer.Streams.Printf("%s = %s\n", key, differ.ComputeDiffForOutput(change).RenderHuman(0, opts)) + } else { + renderer.Streams.Printf("%s = %s\n", key, differ.ComputeDiffForType(change, ctype).RenderHuman(0, opts)) + } + } + } +} diff --git a/internal/command/format/state_test.go b/internal/command/jsonformat/state_test.go similarity index 67% rename from internal/command/format/state_test.go rename to internal/command/jsonformat/state_test.go index d83c6eaf97..f4f5c3a234 100644 --- a/internal/command/format/state_test.go +++ b/internal/command/jsonformat/state_test.go @@ -1,87 +1,106 @@ -package format +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonformat import ( "fmt" "testing" + "github.com/google/go-cmp/cmp" + "github.com/mitchellh/colorstring" + + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/internal/terminal" + + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" - "github.com/zclconf/go-cty/cty" ) func TestState(t *testing.T) { + color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true} + tests := []struct { - State *StateOpts - Want string + State *states.State + Schemas *terraform.Schemas + Want string }{ - { - &StateOpts{ - State: &states.State{}, - Color: disabledColorize, - Schemas: &terraform.Schemas{}, - }, - "The state file is empty. No resources are represented.", + 0: { + State: &states.State{}, + Schemas: &terraform.Schemas{}, + Want: "The state file is empty. No resources are represented.\n", }, - { - &StateOpts{ - State: basicState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - basicStateOutput, + 1: { + State: basicState(t), + Schemas: testSchemas(), + Want: basicStateOutput, }, - { - &StateOpts{ - State: nestedState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - nestedStateOutput, + 2: { + State: nestedState(t), + Schemas: testSchemas(), + Want: nestedStateOutput, }, - { - &StateOpts{ - State: deposedState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - deposedNestedStateOutput, + 3: { + State: deposedState(t), + Schemas: testSchemas(), + Want: deposedNestedStateOutput, }, - { - &StateOpts{ - State: onlyDeposedState(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - onlyDeposedOutput, + 4: { + State: onlyDeposedState(t), + Schemas: testSchemas(), + Want: onlyDeposedOutput, }, - { - &StateOpts{ - State: stateWithMoreOutputs(t), - Color: disabledColorize, - Schemas: testSchemas(), - }, - stateWithMoreOutputsOutput, + 5: { + State: stateWithMoreOutputs(t), + Schemas: testSchemas(), + Want: stateWithMoreOutputsOutput, }, } for i, tt := range tests { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - got := State(tt.State) - if got != tt.Want { - t.Errorf( - "wrong result\ninput: %v\ngot: \n%q\nwant: \n%q", - tt.State.State, got, tt.Want, - ) + + root, outputs, err := jsonstate.MarshalForRenderer(&statefile.File{ + State: tt.State, + }, tt.Schemas) + + if err != nil { + t.Errorf("found err: %v", err) + return + } + + streams, done := terminal.StreamsForTesting(t) + renderer := Renderer{ + Colorize: color, + Streams: streams, + } + + renderer.RenderHumanState(State{ + StateFormatVersion: jsonstate.FormatVersion, + RootModule: root, + RootModuleOutputs: outputs, + ProviderFormatVersion: jsonprovider.FormatVersion, + ProviderSchemas: jsonprovider.MarshalForRenderer(tt.Schemas), + }) + + result := done(t).All() + if diff := cmp.Diff(result, tt.Want); diff != "" { + t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tt.Want, result, diff) } }) } } -func testProvider() *terraform.MockProvider { - p := new(terraform.MockProvider) +func testProvider() *testing_provider.MockProvider { + p := new(testing_provider.MockProvider) p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } @@ -94,7 +113,7 @@ func testProvider() *terraform.MockProvider { func testProviderSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Optional: true}, }, @@ -102,7 +121,7 @@ func testProviderSchema() *providers.GetProviderSchemaResponse { }, ResourceTypes: map[string]providers.Schema{ "test_resource": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "foo": {Type: cty.String, Optional: true}, @@ -124,7 +143,7 @@ func testProviderSchema() *providers.GetProviderSchemaResponse { }, DataSources: map[string]providers.Schema{ "test_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "compute": {Type: cty.String, Optional: true}, "value": {Type: cty.String, Computed: true}, @@ -138,8 +157,8 @@ func testProviderSchema() *providers.GetProviderSchemaResponse { func testSchemas() *terraform.Schemas { provider := testProvider() return &terraform.Schemas{ - Providers: map[addrs.Provider]*terraform.ProviderSchema{ - addrs.NewDefaultProvider("test"): provider.ProviderSchema(), + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.NewDefaultProvider("test"): provider.GetProviderSchema(), }, } } @@ -157,7 +176,8 @@ resource "test_resource" "baz" { Outputs: -bar = "bar value"` +bar = "bar value" +` const nestedStateOutput = `# test_resource.baz[0]: resource "test_resource" "baz" { @@ -166,7 +186,8 @@ resource "test_resource" "baz" { nested { value = "42" } -}` +} +` const deposedNestedStateOutput = `# test_resource.baz[0]: resource "test_resource" "baz" { @@ -184,10 +205,10 @@ resource "test_resource" "baz" { nested { value = "42" } -}` +} +` -const onlyDeposedOutput = `# test_resource.baz[0]: -# test_resource.baz[0]: (deposed object 1234) +const onlyDeposedOutput = `# test_resource.baz[0]: (deposed object 1234) resource "test_resource" "baz" { woozles = "confuzles" @@ -203,7 +224,8 @@ resource "test_resource" "baz" { nested { value = "42" } -}` +} +` const stateWithMoreOutputsOutput = `# test_resource.baz[0]: resource "test_resource" "baz" { @@ -220,7 +242,8 @@ map_var = { "second" = "bar" } sensitive_var = (sensitive value) -string_var = "string value"` +string_var = "string value" +` func basicState(t *testing.T) *states.State { state := states.NewState() @@ -230,8 +253,10 @@ func basicState(t *testing.T) *states.State { t.Errorf("root module is nil; want valid object") } - rootModule.SetLocalValue("foo", cty.StringVal("foo value")) - rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar value"), false, + ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, @@ -240,7 +265,7 @@ func basicState(t *testing.T) *states.State { }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ @@ -256,7 +281,7 @@ func basicState(t *testing.T) *states.State { }.Instance(addrs.NoKey), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"compute":"sure"}`), }, addrs.AbsProviderConfig{ @@ -275,14 +300,30 @@ func stateWithMoreOutputs(t *testing.T) *states.State { t.Errorf("root module is nil; want valid object") } - rootModule.SetOutputValue("string_var", cty.StringVal("string value"), false) - rootModule.SetOutputValue("int_var", cty.NumberIntVal(42), false) - rootModule.SetOutputValue("bool_var", cty.BoolVal(true), false) - rootModule.SetOutputValue("sensitive_var", cty.StringVal("secret!!!"), true) - rootModule.SetOutputValue("map_var", cty.MapVal(map[string]cty.Value{ - "first": cty.StringVal("foo"), - "second": cty.StringVal("bar"), - }), false) + state.SetOutputValue( + addrs.OutputValue{Name: "string_var"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("string value"), false, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "int_var"}.Absolute(addrs.RootModuleInstance), + cty.NumberIntVal(42), false, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "bool_var"}.Absolute(addrs.RootModuleInstance), + cty.True, false, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "sensitive_var"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("secret!!!"), true, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "map_var"}.Absolute(addrs.RootModuleInstance), + cty.MapVal(map[string]cty.Value{ + "first": cty.StringVal("foo"), + "second": cty.StringVal("bar"), + }), + false, + ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ @@ -292,7 +333,7 @@ func stateWithMoreOutputs(t *testing.T) *states.State { }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"woozles":"confuzles"}`), }, addrs.AbsProviderConfig{ @@ -319,7 +360,7 @@ func nestedState(t *testing.T) *states.State { }.Instance(addrs.IntKey(0)), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), }, addrs.AbsProviderConfig{ @@ -342,7 +383,7 @@ func deposedState(t *testing.T) *states.State { states.DeposedKey("1234"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), }, addrs.AbsProviderConfig{ @@ -371,7 +412,7 @@ func onlyDeposedState(t *testing.T) *states.State { states.DeposedKey("1234"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), }, addrs.AbsProviderConfig{ @@ -388,7 +429,7 @@ func onlyDeposedState(t *testing.T) *states.State { states.DeposedKey("5678"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - SchemaVersion: 1, + SchemaVersion: 0, AttrsJSON: []byte(`{"woozles":"confuzles","nested": [{"value": "42"}]}`), }, addrs.AbsProviderConfig{ diff --git a/internal/command/jsonformat/structured/attribute_path/matcher.go b/internal/command/jsonformat/structured/attribute_path/matcher.go new file mode 100644 index 0000000000..d5d3c8cbbd --- /dev/null +++ b/internal/command/jsonformat/structured/attribute_path/matcher.go @@ -0,0 +1,232 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package attribute_path + +import ( + "encoding/json" + "fmt" + "strconv" +) + +// Matcher provides an interface for stepping through changes following an +// attribute path. +// +// GetChildWithKey and GetChildWithIndex will check if any of the internal paths +// match the provided key or index, and return a new Matcher that will match +// that children or potentially it's children. +// +// The caller of the above functions is required to know whether the next value +// in the path is a list type or an object type and call the relevant function, +// otherwise these functions will crash/panic. +// +// The Matches function returns true if the paths you have traversed until now +// ends. +type Matcher interface { + // Matches returns true if we have reached the end of a path and found an + // exact match. + Matches() bool + + // MatchesPartial returns true if the current attribute is part of a path + // but not necessarily at the end of the path. + MatchesPartial() bool + + GetChildWithKey(key string) Matcher + GetChildWithIndex(index int) Matcher +} + +// Parse accepts a json.RawMessage and outputs a formatted Matcher object. +// +// Parse expects the message to be a JSON array of JSON arrays containing +// strings and floats. This function happily accepts a null input representing +// none of the changes in this resource are causing a replacement. The propagate +// argument tells the matcher to propagate any matches to the matched attributes +// children. +// +// In general, this function is designed to accept messages that have been +// produced by the lossy cty.Paths conversion functions within the jsonplan +// package. There is nothing particularly special about that conversion process +// though, it just produces the nested JSON arrays described above. +func Parse(message json.RawMessage, propagate bool) Matcher { + matcher := &PathMatcher{ + Propagate: propagate, + } + if message == nil { + return matcher + } + + if err := json.Unmarshal(message, &matcher.Paths); err != nil { + panic("failed to unmarshal attribute paths: " + err.Error()) + } + + return matcher +} + +// Empty returns an empty PathMatcher that will by default match nothing. +// +// We give direct access to the PathMatcher struct so a matcher can be built +// in parts with the Append and AppendSingle functions. +func Empty(propagate bool) *PathMatcher { + return &PathMatcher{ + Propagate: propagate, + } +} + +// Append accepts an existing PathMatcher and returns a new one that attaches +// all the paths from message with the existing paths. +// +// The new PathMatcher is created fresh, and the existing one is unchanged. +func Append(matcher *PathMatcher, message json.RawMessage) *PathMatcher { + var values [][]interface{} + if err := json.Unmarshal(message, &values); err != nil { + panic("failed to unmarshal attribute paths: " + err.Error()) + } + + return &PathMatcher{ + Propagate: matcher.Propagate, + Paths: append(matcher.Paths, values...), + } +} + +// AppendSingle accepts an existing PathMatcher and returns a new one that +// attaches the single path from message with the existing paths. +// +// The new PathMatcher is created fresh, and the existing one is unchanged. +func AppendSingle(matcher *PathMatcher, message json.RawMessage) *PathMatcher { + var values []interface{} + if err := json.Unmarshal(message, &values); err != nil { + panic("failed to unmarshal attribute paths: " + err.Error()) + } + + return &PathMatcher{ + Propagate: matcher.Propagate, + Paths: append(matcher.Paths, values), + } +} + +// PathMatcher contains a slice of paths that represent paths through the values +// to relevant/tracked attributes. +type PathMatcher struct { + // We represent our internal paths as a [][]interface{} as the cty.Paths + // conversion process is lossy. Since the type information is lost there + // is no (easy) way to reproduce the original cty.Paths object. Instead, + // we simply rely on the external callers to know the type information and + // call the correct GetChild function. + Paths [][]interface{} + + // Propagate tells the matcher that it should propagate any matches it finds + // onto the children of that match. + Propagate bool +} + +func (p *PathMatcher) Matches() bool { + for _, path := range p.Paths { + if len(path) == 0 { + return true + } + } + return false +} + +func (p *PathMatcher) MatchesPartial() bool { + return len(p.Paths) > 0 +} + +func (p *PathMatcher) GetChildWithKey(key string) Matcher { + child := &PathMatcher{ + Propagate: p.Propagate, + } + for _, path := range p.Paths { + if len(path) == 0 { + // This means that the current value matched, but not necessarily + // it's child. + + if p.Propagate { + // If propagate is true, then our child match our matches + child.Paths = append(child.Paths, path) + } + + // If not we would simply drop this path from our set of paths but + // either way we just continue. + continue + } + + if path[0].(string) == key { + child.Paths = append(child.Paths, path[1:]) + } + } + return child +} + +func (p *PathMatcher) GetChildWithIndex(index int) Matcher { + child := &PathMatcher{ + Propagate: p.Propagate, + } + for _, path := range p.Paths { + if len(path) == 0 { + // This means that the current value matched, but not necessarily + // it's child. + + if p.Propagate { + // If propagate is true, then our child match our matches + child.Paths = append(child.Paths, path) + } + + // If not we would simply drop this path from our set of paths but + // either way we just continue. + continue + } + + // Terraform actually allows user to provide strings into indexes as + // long as the string can be interpreted into a number. For example, the + // following are equivalent and we need to support them. + // - test_resource.resource.list[0].attribute + // - test_resource.resource.list["0"].attribute + // + // Note, that Terraform will raise a validation error if the string + // can't be coerced into a number, so we will panic here if anything + // goes wrong safe in the knowledge the validation should stop this from + // happening. + + switch val := path[0].(type) { + case float64: + if int(path[0].(float64)) == index { + child.Paths = append(child.Paths, path[1:]) + } + case string: + f, err := strconv.ParseFloat(val, 64) + if err != nil { + panic(fmt.Errorf("found invalid type within path (%v:%T), the validation shouldn't have allowed this to happen; this is a bug in Terraform, please report it", val, val)) + } + if int(f) == index { + child.Paths = append(child.Paths, path[1:]) + } + default: + panic(fmt.Errorf("found invalid type within path (%v:%T), the validation shouldn't have allowed this to happen; this is a bug in Terraform, please report it", val, val)) + } + } + return child +} + +// AlwaysMatcher returns a matcher that will always match all paths. +func AlwaysMatcher() Matcher { + return &alwaysMatcher{} +} + +type alwaysMatcher struct{} + +func (a *alwaysMatcher) Matches() bool { + return true +} + +func (a *alwaysMatcher) MatchesPartial() bool { + return true +} + +func (a *alwaysMatcher) GetChildWithKey(_ string) Matcher { + return a +} + +func (a *alwaysMatcher) GetChildWithIndex(_ int) Matcher { + return a +} diff --git a/internal/command/jsonformat/structured/attribute_path/matcher_test.go b/internal/command/jsonformat/structured/attribute_path/matcher_test.go new file mode 100644 index 0000000000..ce69b8715b --- /dev/null +++ b/internal/command/jsonformat/structured/attribute_path/matcher_test.go @@ -0,0 +1,256 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package attribute_path + +import "testing" + +func TestPathMatcher_FollowsPath(t *testing.T) { + var matcher Matcher + + matcher = &PathMatcher{ + Paths: [][]interface{}{ + { + float64(0), + "key", + float64(0), + }, + }, + } + + if matcher.Matches() { + t.Errorf("should not have exact matched at base level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at base level") + } + + matcher = matcher.GetChildWithIndex(0) + + if matcher.Matches() { + t.Errorf("should not have exact matched at first level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at first level") + } + + matcher = matcher.GetChildWithKey("key") + + if matcher.Matches() { + t.Errorf("should not have exact matched at second level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at second level") + } + + matcher = matcher.GetChildWithIndex(0) + + if !matcher.Matches() { + t.Errorf("should have exact matched at leaf level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at leaf level") + } +} +func TestPathMatcher_Propagates(t *testing.T) { + var matcher Matcher + + matcher = &PathMatcher{ + Paths: [][]interface{}{ + { + float64(0), + "key", + }, + }, + Propagate: true, + } + + if matcher.Matches() { + t.Errorf("should not have exact matched at base level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at base level") + } + + matcher = matcher.GetChildWithIndex(0) + + if matcher.Matches() { + t.Errorf("should not have exact matched at first level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at first level") + } + + matcher = matcher.GetChildWithKey("key") + + if !matcher.Matches() { + t.Errorf("should have exact matched at second level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at second level") + } + + matcher = matcher.GetChildWithIndex(0) + + if !matcher.Matches() { + t.Errorf("should have exact matched at leaf level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at leaf level") + } +} +func TestPathMatcher_DoesNotPropagate(t *testing.T) { + var matcher Matcher + + matcher = &PathMatcher{ + Paths: [][]interface{}{ + { + float64(0), + "key", + }, + }, + } + + if matcher.Matches() { + t.Errorf("should not have exact matched at base level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at base level") + } + + matcher = matcher.GetChildWithIndex(0) + + if matcher.Matches() { + t.Errorf("should not have exact matched at first level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at first level") + } + + matcher = matcher.GetChildWithKey("key") + + if !matcher.Matches() { + t.Errorf("should have exact matched at second level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at second level") + } + + matcher = matcher.GetChildWithIndex(0) + + if matcher.Matches() { + t.Errorf("should not have exact matched at leaf level") + } + if matcher.MatchesPartial() { + t.Errorf("should not have partial matched at leaf level") + } +} + +func TestPathMatcher_BreaksPath(t *testing.T) { + var matcher Matcher + + matcher = &PathMatcher{ + Paths: [][]interface{}{ + { + float64(0), + "key", + float64(0), + }, + }, + } + + if matcher.Matches() { + t.Errorf("should not have exact matched at base level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at base level") + } + + matcher = matcher.GetChildWithIndex(0) + + if matcher.Matches() { + t.Errorf("should not have exact matched at first level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at first level") + } + + matcher = matcher.GetChildWithKey("invalid") + + if matcher.Matches() { + t.Errorf("should not have exact matched at second level") + } + if matcher.MatchesPartial() { + t.Errorf("should not have partial matched at second level") + + } +} + +func TestPathMatcher_MultiplePaths(t *testing.T) { + var matcher Matcher + + matcher = &PathMatcher{ + Paths: [][]interface{}{ + { + float64(0), + "key", + float64(0), + }, + { + float64(0), + "key", + float64(1), + }, + }, + } + + if matcher.Matches() { + t.Errorf("should not have exact matched at base level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at base level") + } + + matcher = matcher.GetChildWithIndex(0) + + if matcher.Matches() { + t.Errorf("should not have exact matched at first level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at first level") + } + + matcher = matcher.GetChildWithKey("key") + + if matcher.Matches() { + t.Errorf("should not have exact matched at second level") + } + if !matcher.MatchesPartial() { + t.Errorf("should have partial matched at second level") + } + + validZero := matcher.GetChildWithIndex(0) + validOne := matcher.GetChildWithIndex(1) + invalid := matcher.GetChildWithIndex(2) + + if !validZero.Matches() { + t.Errorf("should have exact matched at leaf level") + } + if !validZero.MatchesPartial() { + t.Errorf("should have partial matched at leaf level") + } + + if !validOne.Matches() { + t.Errorf("should have exact matched at leaf level") + } + if !validOne.MatchesPartial() { + t.Errorf("should have partial matched at leaf level") + } + + if invalid.Matches() { + t.Errorf("should not have exact matched at leaf level") + } + if invalid.MatchesPartial() { + t.Errorf("should not have partial matched at leaf level") + } +} diff --git a/internal/command/jsonformat/structured/change.go b/internal/command/jsonformat/structured/change.go new file mode 100644 index 0000000000..f258ac30d1 --- /dev/null +++ b/internal/command/jsonformat/structured/change.go @@ -0,0 +1,318 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package structured + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "reflect" + + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonstate" + viewsjson "github.com/hashicorp/terraform/internal/command/views/json" + "github.com/hashicorp/terraform/internal/plans" +) + +// Change contains the unmarshalled generic interface{} types that are output by +// the JSON functions in the various json packages (such as jsonplan and +// jsonprovider). +// +// A Change can be converted into a computed.Diff, ready for rendering, with the +// ComputeDiffForAttribute, ComputeDiffForOutput, and ComputeDiffForBlock +// functions. +// +// The Before and After fields are actually go-cty values, but we cannot convert +// them directly because of the HCP Terraform redacted endpoint. The redacted +// endpoint turns sensitive values into strings regardless of their types. +// Because of this, we cannot just do a direct conversion using the ctyjson +// package. We would have to iterate through the schema first, find the +// sensitive values and their mapped types, update the types inside the schema +// to strings, and then go back and do the overall conversion. This isn't +// including any of the more complicated parts around what happens if something +// was sensitive before and isn't sensitive after or vice versa. This would mean +// the type would need to change between the before and after value. It is in +// fact just easier to iterate through the values as generic JSON interfaces. +type Change struct { + + // BeforeExplicit matches AfterExplicit except references the Before value. + BeforeExplicit bool + + // AfterExplicit refers to whether the After value is explicit or + // implicit. It is explicit if it has been specified by the user, and + // implicit if it has been set as a consequence of other changes. + // + // For example, explicitly setting a value to null in a list should result + // in After being null and AfterExplicit being true. In comparison, + // removing an element from a list should also result in After being null + // and AfterExplicit being false. Without the explicit information our + // functions would not be able to tell the difference between these two + // cases. + AfterExplicit bool + + // Before contains the value before the proposed change. + // + // The type of the value should be informed by the schema and cast + // appropriately when needed. + Before interface{} + + // After contains the value after the proposed change. + // + // The type of the value should be informed by the schema and cast + // appropriately when needed. + After interface{} + + // Unknown describes whether the After value is known or unknown at the time + // of the plan. In practice, this means the after value should be rendered + // simply as `(known after apply)`. + // + // The concrete value could be a boolean describing whether the entirety of + // the After value is unknown, or it could be a list or a map depending on + // the schema describing whether specific elements or attributes within the + // value are unknown. + Unknown interface{} + + // BeforeSensitive matches Unknown, but references whether the Before value + // is sensitive. + BeforeSensitive interface{} + + // AfterSensitive matches Unknown, but references whether the After value is + // sensitive. + AfterSensitive interface{} + + // ReplacePaths contains a set of paths that point to attributes/elements + // that are causing the overall resource to be replaced rather than simply + // updated. + ReplacePaths attribute_path.Matcher + + // RelevantAttributes contains a set of paths that point attributes/elements + // that we should display. Any element/attribute not matched by this Matcher + // should be skipped. + RelevantAttributes attribute_path.Matcher + + // NonLegacySchema must only be used when rendering the change to the CLI, + // and is otherwise ignored. This flag is set when we can be sure that the + // change originated from a resource which is not using the legacy SDK, so + // we don't need to hide changes between empty and null strings. + // NonLegacySchema is only switched to true by the renderer, because that is + // where we have most of the schema information to detect the condition. + NonLegacySchema bool +} + +// FromJsonChange unmarshals the raw []byte values in the jsonplan.Change +// structs into generic interface{} types that can be reasoned about. +func FromJsonChange(change jsonplan.Change, relevantAttributes attribute_path.Matcher) Change { + ret := Change{ + Before: unmarshalGeneric(change.Before), + After: unmarshalGeneric(change.After), + Unknown: unmarshalGeneric(change.AfterUnknown), + BeforeSensitive: unmarshalGeneric(change.BeforeSensitive), + AfterSensitive: unmarshalGeneric(change.AfterSensitive), + ReplacePaths: attribute_path.Parse(change.ReplacePaths, false), + RelevantAttributes: relevantAttributes, + } + + // A forget-only action (i.e. ["forget"], not ["create", "forget"]) + // should be represented as a no-op, so it does not look like we are + // proposing to delete the resource. + if len(change.Actions) == 1 && change.Actions[0] == "forget" { + ret = ret.AsNoOp() + } + + return ret +} + +// FromJsonResource unmarshals the raw values in the jsonstate.Resource structs +// into generic interface{} types that can be reasoned about. +func FromJsonResource(resource jsonstate.Resource) Change { + return Change{ + // We model resource formatting as NoOps. + Before: unwrapAttributeValues(resource.AttributeValues), + After: unwrapAttributeValues(resource.AttributeValues), + + // We have some sensitive values, but we don't have any unknown values. + Unknown: false, + BeforeSensitive: unmarshalGeneric(resource.SensitiveValues), + AfterSensitive: unmarshalGeneric(resource.SensitiveValues), + + // We don't display replacement data for resources, and all attributes + // are relevant. + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + } +} + +// FromJsonOutput unmarshals the raw values in the jsonstate.Output structs into +// generic interface{} types that can be reasoned about. +func FromJsonOutput(output jsonstate.Output) Change { + return Change{ + // We model resource formatting as NoOps. + Before: unmarshalGeneric(output.Value), + After: unmarshalGeneric(output.Value), + + // We have some sensitive values, but we don't have any unknown values. + Unknown: false, + BeforeSensitive: output.Sensitive, + AfterSensitive: output.Sensitive, + + // We don't display replacement data for resources, and all attributes + // are relevant. + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + } +} + +// FromJsonViewsOutput unmarshals the raw values in the viewsjson.Output structs into +// generic interface{} types that can be reasoned about. +func FromJsonViewsOutput(output viewsjson.Output) Change { + return Change{ + // We model resource formatting as NoOps. + Before: unmarshalGeneric(output.Value), + After: unmarshalGeneric(output.Value), + + // We have some sensitive values, but we don't have any unknown values. + Unknown: false, + BeforeSensitive: output.Sensitive, + AfterSensitive: output.Sensitive, + + // We don't display replacement data for resources, and all attributes + // are relevant. + ReplacePaths: attribute_path.Empty(false), + RelevantAttributes: attribute_path.AlwaysMatcher(), + } +} + +// CalculateAction does a very simple analysis to make the best guess at the +// action this change describes. For complex types such as objects, maps, lists, +// or sets it is likely more efficient to work out the action directly instead +// of relying on this function. +func (change Change) CalculateAction() plans.Action { + if (change.Before == nil && !change.BeforeExplicit) && (change.After != nil || change.AfterExplicit) { + return plans.Create + } + if (change.After == nil && !change.AfterExplicit) && (change.Before != nil || change.BeforeExplicit) { + return plans.Delete + } + + if reflect.DeepEqual(change.Before, change.After) && change.AfterExplicit == change.BeforeExplicit && change.IsAfterSensitive() == change.IsBeforeSensitive() { + return plans.NoOp + } + + return plans.Update +} + +// GetDefaultActionForIteration is used to guess what the change could be for +// complex attributes (collections and objects) and blocks. +// +// You can't really tell the difference between a NoOp and an Update just by +// looking at the attribute itself as you need to inspect the children. +// +// This function returns a Delete or a Create action if the before or after +// values were null, and returns a NoOp for all other cases. It should be used +// in conjunction with compareActions to calculate the actual action based on +// the actions of the children. +func (change Change) GetDefaultActionForIteration() plans.Action { + if change.Before == nil && change.After == nil { + return plans.NoOp + } + + if change.Before == nil { + return plans.Create + } + if change.After == nil { + return plans.Delete + } + return plans.NoOp +} + +// AsNoOp returns the current change as if it is a NoOp operation. +// +// Basically it replaces all the after values with the before values. +func (change Change) AsNoOp() Change { + return Change{ + BeforeExplicit: change.BeforeExplicit, + AfterExplicit: change.BeforeExplicit, + Before: change.Before, + After: change.Before, + Unknown: false, + BeforeSensitive: change.BeforeSensitive, + AfterSensitive: change.BeforeSensitive, + ReplacePaths: change.ReplacePaths, + RelevantAttributes: change.RelevantAttributes, + } +} + +// AsDelete returns the current change as if it is a Delete operation. +// +// Basically it replaces all the after values with nil or false. +func (change Change) AsDelete() Change { + return Change{ + BeforeExplicit: change.BeforeExplicit, + AfterExplicit: false, + Before: change.Before, + After: nil, + Unknown: nil, + BeforeSensitive: change.BeforeSensitive, + AfterSensitive: nil, + ReplacePaths: change.ReplacePaths, + RelevantAttributes: change.RelevantAttributes, + } +} + +// AsCreate returns the current change as if it is a Create operation. +// +// Basically it replaces all the before values with nil or false. +func (change Change) AsCreate() Change { + return Change{ + BeforeExplicit: false, + AfterExplicit: change.AfterExplicit, + Before: nil, + After: change.After, + Unknown: change.Unknown, + BeforeSensitive: nil, + AfterSensitive: change.AfterSensitive, + ReplacePaths: change.ReplacePaths, + RelevantAttributes: change.RelevantAttributes, + } +} + +func unmarshalGeneric(raw json.RawMessage) interface{} { + if raw == nil { + return nil + } + + out, err := ParseJson(bytes.NewReader(raw)) + if err != nil { + panic("unrecognized json type: " + err.Error()) + } + return out +} + +func unwrapAttributeValues(values jsonstate.AttributeValues) map[string]interface{} { + out := make(map[string]interface{}) + for key, value := range values { + out[key] = unmarshalGeneric(value) + } + return out +} + +func ParseJson(reader io.Reader) (interface{}, error) { + decoder := json.NewDecoder(reader) + decoder.UseNumber() + + var jv interface{} + if err := decoder.Decode(&jv); err != nil { + return nil, err + } + + // The JSON decoder should have consumed the entire input stream, so + // we should be at EOF now. + if token, err := decoder.Token(); err != io.EOF { + return nil, fmt.Errorf("unexpected token after valid JSON: %v", token) + } + + return jv, nil +} diff --git a/internal/command/jsonformat/structured/doc.go b/internal/command/jsonformat/structured/doc.go new file mode 100644 index 0000000000..78467319f1 --- /dev/null +++ b/internal/command/jsonformat/structured/doc.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package structured contains the structured representation of the JSON changes +// returned by the jsonplan package. +// +// Placing these in a dedicated package allows for greater reuse across the +// various type of renderers. +package structured diff --git a/internal/command/jsonformat/structured/map.go b/internal/command/jsonformat/structured/map.go new file mode 100644 index 0000000000..6d507b625a --- /dev/null +++ b/internal/command/jsonformat/structured/map.go @@ -0,0 +1,169 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package structured + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" +) + +// ChangeMap is a Change that represents a Map or an Object type, and has +// converted the relevant interfaces into maps for easier access. +type ChangeMap struct { + // Before contains the value before the proposed change. + Before map[string]interface{} + + // After contains the value after the proposed change. + After map[string]interface{} + + // Unknown contains the unknown status of any elements/attributes of this + // map/object. + Unknown map[string]interface{} + + // BeforeSensitive contains the before sensitive status of any + // elements/attributes of this map/object. + BeforeSensitive map[string]interface{} + + // AfterSensitive contains the after sensitive status of any + // elements/attributes of this map/object. + AfterSensitive map[string]interface{} + + // ReplacePaths matches the same attributes in Change exactly. + ReplacePaths attribute_path.Matcher + + // RelevantAttributes matches the same attributes in Change exactly. + RelevantAttributes attribute_path.Matcher + + // this reflects the parent NonLegacyValue, so that any behavior is + // automatically inherited into child changes. + nonLegacySchema bool +} + +// AsMap converts the Change into an object or map representation by converting +// the internal Before, After, Unknown, BeforeSensitive, and AfterSensitive +// data structures into generic maps. +func (change Change) AsMap() ChangeMap { + return ChangeMap{ + Before: genericToMap(change.Before), + After: genericToMap(change.After), + Unknown: genericToMap(change.Unknown), + BeforeSensitive: genericToMap(change.BeforeSensitive), + AfterSensitive: genericToMap(change.AfterSensitive), + ReplacePaths: change.ReplacePaths, + RelevantAttributes: change.RelevantAttributes, + nonLegacySchema: change.NonLegacySchema, + } +} + +// GetChild safely packages up a Change object for the given child, handling +// all the cases where the data might be null or a static boolean. +func (m ChangeMap) GetChild(key string) Change { + before, beforeExplicit := getFromGenericMap(m.Before, key) + after, afterExplicit := getFromGenericMap(m.After, key) + unknown, _ := getFromGenericMap(m.Unknown, key) + beforeSensitive, _ := getFromGenericMap(m.BeforeSensitive, key) + afterSensitive, _ := getFromGenericMap(m.AfterSensitive, key) + + return Change{ + BeforeExplicit: beforeExplicit, + AfterExplicit: afterExplicit, + Before: before, + After: after, + Unknown: unknown, + BeforeSensitive: beforeSensitive, + AfterSensitive: afterSensitive, + ReplacePaths: m.ReplacePaths.GetChildWithKey(key), + RelevantAttributes: m.RelevantAttributes.GetChildWithKey(key), + NonLegacySchema: m.nonLegacySchema, + } +} + +// ExplicitKeys returns the keys in the Before and After, as opposed to AllKeys +// which also includes keys from the additional meta structures (like the +// sensitive and unknown values). +// +// This function is useful for processing nested attributes and repeated blocks +// where the unknown and sensitive structs contain information about the actual +// attributes, while the before and after structs hold the actual nested values. +func (m ChangeMap) ExplicitKeys() []string { + keys := make(map[string]bool) + for before := range m.Before { + if _, ok := keys[before]; ok { + continue + } + keys[before] = true + } + for after := range m.After { + if _, ok := keys[after]; ok { + continue + } + keys[after] = true + } + + var dedupedKeys []string + for key := range keys { + dedupedKeys = append(dedupedKeys, key) + } + return dedupedKeys +} + +// AllKeys returns all the possible keys for this map. The keys for the map are +// potentially hidden and spread across multiple internal data structures and +// so this function conveniently packages them up. +func (m ChangeMap) AllKeys() []string { + keys := make(map[string]bool) + for before := range m.Before { + if _, ok := keys[before]; ok { + continue + } + keys[before] = true + } + for after := range m.After { + if _, ok := keys[after]; ok { + continue + } + keys[after] = true + } + for unknown := range m.Unknown { + if _, ok := keys[unknown]; ok { + continue + } + keys[unknown] = true + } + for sensitive := range m.AfterSensitive { + if _, ok := keys[sensitive]; ok { + continue + } + keys[sensitive] = true + } + for sensitive := range m.BeforeSensitive { + if _, ok := keys[sensitive]; ok { + continue + } + keys[sensitive] = true + } + + var dedupedKeys []string + for key := range keys { + dedupedKeys = append(dedupedKeys, key) + } + return dedupedKeys +} + +func getFromGenericMap(generic map[string]interface{}, key string) (interface{}, bool) { + if generic == nil { + return nil, false + } + + if child, ok := generic[key]; ok { + return child, ok + } + return nil, false +} + +func genericToMap(generic interface{}) map[string]interface{} { + if concrete, ok := generic.(map[string]interface{}); ok { + return concrete + } + return nil +} diff --git a/internal/command/jsonformat/structured/sensitive.go b/internal/command/jsonformat/structured/sensitive.go new file mode 100644 index 0000000000..b37bf4ef5d --- /dev/null +++ b/internal/command/jsonformat/structured/sensitive.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package structured + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" + "github.com/hashicorp/terraform/internal/plans" +) + +type ProcessSensitiveInner func(change Change) computed.Diff +type CreateSensitiveDiff func(inner computed.Diff, beforeSensitive, afterSensitive bool, action plans.Action) computed.Diff + +func (change Change) IsBeforeSensitive() bool { + if sensitive, ok := change.BeforeSensitive.(bool); ok { + return sensitive + } + return false +} + +func (change Change) IsAfterSensitive() bool { + if sensitive, ok := change.AfterSensitive.(bool); ok { + return sensitive + } + return false +} + +// CheckForSensitive is a helper function that handles all common functionality +// for processing a sensitive value. +// +// It returns the computed sensitive diff and true if this value was sensitive +// and needs to be rendered as such, otherwise it returns the second return +// value as false and the first value can be discarded. +// +// The actual processing of sensitive values happens within the +// ProcessSensitiveInner and CreateSensitiveDiff functions. Callers should +// implement these functions as appropriate when using this function. +// +// The ProcessSensitiveInner function should simply return a computed.Diff for +// the provided Change. The provided Change will be the same as the original +// change but with the sensitive metadata removed. The new inner diff is then +// passed into the actual CreateSensitiveDiff function which should return the +// actual sensitive diff. +// +// We include the inner change into the sensitive diff as a way to let the +// sensitive renderer have as much information as possible, while still letting +// it do the actual rendering. +func (change Change) CheckForSensitive(processInner ProcessSensitiveInner, createDiff CreateSensitiveDiff) (computed.Diff, bool) { + beforeSensitive := change.IsBeforeSensitive() + afterSensitive := change.IsAfterSensitive() + + if !beforeSensitive && !afterSensitive { + return computed.Diff{}, false + } + + // We are still going to give the change the contents of the actual change. + // So we create a new Change with everything matching the current value, + // except for the sensitivity. + // + // The change can choose what to do with this information, in most cases + // it will just be ignored in favour of printing `(sensitive value)`. + + value := Change{ + BeforeExplicit: change.BeforeExplicit, + AfterExplicit: change.AfterExplicit, + Before: change.Before, + After: change.After, + Unknown: change.Unknown, + BeforeSensitive: false, + AfterSensitive: false, + ReplacePaths: change.ReplacePaths, + RelevantAttributes: change.RelevantAttributes, + } + + inner := processInner(value) + + action := inner.Action + sensitiveStatusChanged := beforeSensitive != afterSensitive + + // nullNoOp is a stronger NoOp, where not only is there no change happening + // but the before and after values are not explicitly set and are both + // null. This will override even the sensitive state changing. + nullNoOp := change.Before == nil && !change.BeforeExplicit && change.After == nil && !change.AfterExplicit + + if action == plans.NoOp && sensitiveStatusChanged && !nullNoOp { + // Let's override this, since it means the sensitive status has changed + // rather than the actual content of the value. + action = plans.Update + } + + return createDiff(inner, beforeSensitive, afterSensitive, action), true +} diff --git a/internal/command/jsonformat/structured/slice.go b/internal/command/jsonformat/structured/slice.go new file mode 100644 index 0000000000..1df1a5fec5 --- /dev/null +++ b/internal/command/jsonformat/structured/slice.go @@ -0,0 +1,94 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package structured + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path" +) + +// ChangeSlice is a Change that represents a Tuple, Set, or List type, and has +// converted the relevant interfaces into slices for easier access. +type ChangeSlice struct { + // Before contains the value before the proposed change. + Before []interface{} + + // After contains the value after the proposed change. + After []interface{} + + // Unknown contains the unknown status of any elements of this list/set. + Unknown []interface{} + + // BeforeSensitive contains the before sensitive status of any elements of + //this list/set. + BeforeSensitive []interface{} + + // AfterSensitive contains the after sensitive status of any elements of + //this list/set. + AfterSensitive []interface{} + + // ReplacePaths matches the same attributes in Change exactly. + ReplacePaths attribute_path.Matcher + + // RelevantAttributes matches the same attributes in Change exactly. + RelevantAttributes attribute_path.Matcher +} + +// AsSlice converts the Change into a slice representation by converting the +// internal Before, After, Unknown, BeforeSensitive, and AfterSensitive data +// structures into generic slices. +func (change Change) AsSlice() ChangeSlice { + return ChangeSlice{ + Before: genericToSlice(change.Before), + After: genericToSlice(change.After), + Unknown: genericToSlice(change.Unknown), + BeforeSensitive: genericToSlice(change.BeforeSensitive), + AfterSensitive: genericToSlice(change.AfterSensitive), + ReplacePaths: change.ReplacePaths, + RelevantAttributes: change.RelevantAttributes, + } +} + +// GetChild safely packages up a Change object for the given child, handling +// all the cases where the data might be null or a static boolean. +func (s ChangeSlice) GetChild(beforeIx, afterIx int) Change { + before, beforeExplicit := getFromGenericSlice(s.Before, beforeIx) + after, afterExplicit := getFromGenericSlice(s.After, afterIx) + unknown, _ := getFromGenericSlice(s.Unknown, afterIx) + beforeSensitive, _ := getFromGenericSlice(s.BeforeSensitive, beforeIx) + afterSensitive, _ := getFromGenericSlice(s.AfterSensitive, afterIx) + + mostRelevantIx := beforeIx + if beforeIx < 0 || beforeIx >= len(s.Before) { + mostRelevantIx = afterIx + } + + return Change{ + BeforeExplicit: beforeExplicit, + AfterExplicit: afterExplicit, + Before: before, + After: after, + Unknown: unknown, + BeforeSensitive: beforeSensitive, + AfterSensitive: afterSensitive, + ReplacePaths: s.ReplacePaths.GetChildWithIndex(mostRelevantIx), + RelevantAttributes: s.RelevantAttributes.GetChildWithIndex(mostRelevantIx), + } +} + +func getFromGenericSlice(generic []interface{}, ix int) (interface{}, bool) { + if generic == nil { + return nil, false + } + if ix < 0 || ix >= len(generic) { + return nil, false + } + return generic[ix], true +} + +func genericToSlice(generic interface{}) []interface{} { + if concrete, ok := generic.([]interface{}); ok { + return concrete + } + return nil +} diff --git a/internal/command/jsonformat/structured/unknown.go b/internal/command/jsonformat/structured/unknown.go new file mode 100644 index 0000000000..c05bc87e70 --- /dev/null +++ b/internal/command/jsonformat/structured/unknown.go @@ -0,0 +1,65 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package structured + +import ( + "github.com/hashicorp/terraform/internal/command/jsonformat/computed" +) + +type ProcessUnknown func(current Change) computed.Diff +type ProcessUnknownWithBefore func(current Change, before Change) computed.Diff + +func (change Change) IsUnknown() bool { + if unknown, ok := change.Unknown.(bool); ok { + return unknown + } + return false +} + +// CheckForUnknown is a helper function that handles all common functionality +// for processing an unknown value. +// +// It returns the computed unknown diff and true if this value was unknown and +// needs to be rendered as such, otherwise it returns the second return value as +// false and the first return value should be discarded. +// +// The actual processing of unknown values happens in the ProcessUnknown and +// ProcessUnknownWithBefore functions. If a value is unknown and is being +// created, the ProcessUnknown function is called and the caller should decide +// how to create the unknown value. If a value is being updated the +// ProcessUnknownWithBefore function is called and the function provides the +// before value as if it is being deleted for the caller to handle. Note that +// values being deleted will never be marked as unknown so this case isn't +// handled. +// +// The childUnknown argument is meant to allow callers with extra information +// about the type being processed to provide a list of known children that might +// not be present in the before or after values. These values will be propagated +// as the unknown values in the before value should it be needed. +func (change Change) CheckForUnknown(childUnknown interface{}, process ProcessUnknown, processBefore ProcessUnknownWithBefore) (computed.Diff, bool) { + unknown := change.IsUnknown() + + if !unknown { + return computed.Diff{}, false + } + + // No matter what we do here, we want to treat the after value as explicit. + // This is because it is going to be null in the value, and we don't want + // the functions in this package to assume this means it has been deleted. + change.AfterExplicit = true + + if change.Before == nil { + return process(change), true + } + + // If we get here, then we have a before value. We're going to model a + // delete operation and our renderer later can render the overall change + // accurately. + before := change.AsDelete() + + // We also let our callers override the unknown values in any before, this + // is the renderers can display them as being computed instead of deleted. + before.Unknown = childUnknown + return processBefore(change, before), true +} diff --git a/internal/command/jsonfunction/function.go b/internal/command/jsonfunction/function.go new file mode 100644 index 0000000000..83cc59c8e7 --- /dev/null +++ b/internal/command/jsonfunction/function.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonfunction + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// FormatVersion represents the version of the json format and will be +// incremented for any change to this format that requires changes to a +// consuming parser. +// +// Any changes to this version should also consider compatibility in the +// jsonprovider package versioning as well, as that functionality is also +// reliant on this package. +const FormatVersion = "1.0" + +// functions is the top-level object returned when exporting function signatures +type functions struct { + FormatVersion string `json:"format_version"` + Signatures map[string]*FunctionSignature `json:"function_signatures,omitempty"` +} + +// FunctionSignature represents a function signature. +type FunctionSignature struct { + // Description is an optional human-readable description + // of the function + Description string `json:"description,omitempty"` + + // Summary is the optional shortened description of the function + Summary string `json:"summary,omitempty"` + + // DeprecationMessage is an optional message that indicates that the + // function should be considered deprecated and what actions should be + // performed by the practitioner to handle the deprecation. + DeprecationMessage string `json:"deprecation_message,omitempty"` + + // ReturnTypes is the ctyjson representation of the function's + // return types based on supplying all parameters using + // dynamic types. Functions can have dynamic return types. + ReturnType cty.Type `json:"return_type"` + + // Parameters describes the function's fixed positional parameters. + Parameters []*parameter `json:"parameters,omitempty"` + + // VariadicParameter describes the function's variadic + // parameters, if any are supported. + VariadicParameter *parameter `json:"variadic_parameter,omitempty"` +} + +func newFunctions() *functions { + signatures := make(map[string]*FunctionSignature) + return &functions{ + FormatVersion: FormatVersion, + Signatures: signatures, + } +} + +func MarshalProviderFunctions(f map[string]providers.FunctionDecl) map[string]*FunctionSignature { + if f == nil { + return nil + } + + result := make(map[string]*FunctionSignature, len(f)) + + for name, v := range f { + result[name] = marshalProviderFunction(v) + } + + return result +} + +func Marshal(f map[string]function.Function) ([]byte, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + signatures := newFunctions() + + for name, v := range f { + if name == "can" || name == "core::can" { + signatures.Signatures[name] = marshalCan(v) + } else if name == "try" || name == "core::try" { + signatures.Signatures[name] = marshalTry(v) + } else if name == "templatestring" || name == "core::templatestring" { + signatures.Signatures[name] = marshalTemplatestring(v) + } else { + signature, err := marshalFunction(v) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + fmt.Sprintf("Failed to serialize function %q", name), + err.Error(), + )) + } + signatures.Signatures[name] = signature + } + } + + if diags.HasErrors() { + return nil, diags + } + + ret, err := json.Marshal(signatures) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to serialize functions", + err.Error(), + )) + return nil, diags + } + return ret, nil +} + +func marshalFunction(f function.Function) (*FunctionSignature, error) { + var err error + var vp *parameter + if f.VarParam() != nil { + vp = marshalParameter(f.VarParam()) + } + + var p []*parameter + if len(f.Params()) > 0 { + p = marshalParameters(f.Params()) + } + + r, err := getReturnType(f) + if err != nil { + return nil, err + } + + return &FunctionSignature{ + Description: f.Description(), + ReturnType: r, + Parameters: p, + VariadicParameter: vp, + }, nil +} + +func marshalProviderFunction(f providers.FunctionDecl) *FunctionSignature { + var vp *parameter + if f.VariadicParameter != nil { + vp = marshalProviderParameter(*f.VariadicParameter) + } + + var p []*parameter + if len(f.Parameters) > 0 { + p = marshalProviderParameters(f.Parameters) + } + + return &FunctionSignature{ + Description: f.Description, + Summary: f.Summary, + DeprecationMessage: f.DeprecationMessage, + ReturnType: f.ReturnType, + Parameters: p, + VariadicParameter: vp, + } +} + +// marshalTry returns a static function signature for the try function. +// We need this exception because the function implementation uses capsule +// types that we can't marshal. +func marshalTry(try function.Function) *FunctionSignature { + return &FunctionSignature{ + Description: try.Description(), + ReturnType: cty.DynamicPseudoType, + VariadicParameter: ¶meter{ + Name: try.VarParam().Name, + Description: try.VarParam().Description, + IsNullable: try.VarParam().AllowNull, + Type: cty.DynamicPseudoType, + }, + } +} + +// marshalCan returns a static function signature for the can function. +// We need this exception because the function implementation uses capsule +// types that we can't marshal. +func marshalCan(can function.Function) *FunctionSignature { + return &FunctionSignature{ + Description: can.Description(), + ReturnType: cty.Bool, + Parameters: []*parameter{ + { + Name: can.Params()[0].Name, + Description: can.Params()[0].Description, + IsNullable: can.Params()[0].AllowNull, + Type: cty.DynamicPseudoType, + }, + }, + } +} + +// marshalTemplatestring returns a static function signature for the +// templatestring function. +// We need this exception because the function implementation uses capsule +// types that we can't marshal. +func marshalTemplatestring(templatestring function.Function) *FunctionSignature { + return &FunctionSignature{ + Description: templatestring.Description(), + ReturnType: cty.String, + Parameters: []*parameter{ + { + Name: templatestring.Params()[0].Name, + Description: templatestring.Params()[0].Description, + IsNullable: templatestring.Params()[0].AllowNull, + Type: cty.String, + }, + { + Name: templatestring.Params()[1].Name, + Description: templatestring.Params()[1].Description, + IsNullable: templatestring.Params()[1].AllowNull, + Type: cty.DynamicPseudoType, + }, + }, + } +} diff --git a/internal/command/jsonfunction/function_test.go b/internal/command/jsonfunction/function_test.go new file mode 100644 index 0000000000..fcdca4eb46 --- /dev/null +++ b/internal/command/jsonfunction/function_test.go @@ -0,0 +1,217 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonfunction + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/providers" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +func TestMarshal(t *testing.T) { + tests := []struct { + Name string + Functions map[string]function.Function + ProviderFunctions map[string]providers.FunctionDecl + Want string + WantErr string + }{ + { + Name: "minimal function", + Functions: map[string]function.Function{ + "fun": function.New(&function.Spec{ + Type: function.StaticReturnType(cty.Bool), + }), + }, + ProviderFunctions: map[string]providers.FunctionDecl{ + "fun": { + ReturnType: cty.Bool, + }, + }, + Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":"bool"}}}`, + }, + { + Name: "function with description", + Functions: map[string]function.Function{ + "fun": function.New(&function.Spec{ + Description: "`timestamp` returns a UTC timestamp string.", + Type: function.StaticReturnType(cty.String), + }), + }, + ProviderFunctions: map[string]providers.FunctionDecl{ + "fun": { + Description: "`timestamp` returns a UTC timestamp string.", + ReturnType: cty.String, + }, + }, + Want: "{\"format_version\":\"1.0\",\"function_signatures\":{\"fun\":{\"description\":\"`timestamp` returns a UTC timestamp string.\",\"return_type\":\"string\"}}}", + }, + { + Name: "function with parameters", + Functions: map[string]function.Function{ + "fun": function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "timestamp", + Description: "timestamp text", + Type: cty.String, + }, + { + Name: "duration", + Description: "duration text", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + }), + }, + ProviderFunctions: map[string]providers.FunctionDecl{ + "fun": { + Parameters: []providers.FunctionParam{ + { + Name: "timestamp", + Description: "timestamp text", + Type: cty.String, + }, + { + Name: "duration", + Description: "duration text", + Type: cty.String, + }, + }, + ReturnType: cty.String, + }, + }, + Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":"string","parameters":[{"name":"timestamp","description":"timestamp text","type":"string"},{"name":"duration","description":"duration text","type":"string"}]}}}`, + }, + { + Name: "function with variadic parameter", + Functions: map[string]function.Function{ + "fun": function.New(&function.Spec{ + VarParam: &function.Parameter{ + Name: "default", + Description: "default description", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + AllowMarked: true, + }, + Type: function.StaticReturnType(cty.DynamicPseudoType), + }), + }, + ProviderFunctions: map[string]providers.FunctionDecl{ + "fun": { + VariadicParameter: &providers.FunctionParam{ + Name: "default", + Description: "default description", + Type: cty.DynamicPseudoType, + AllowUnknownValues: true, + AllowNullValue: true, + }, + ReturnType: cty.DynamicPseudoType, + }, + }, + Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":"dynamic","variadic_parameter":{"name":"default","description":"default description","is_nullable":true,"type":"dynamic"}}}}`, + }, + { + Name: "function with list types", + Functions: map[string]function.Function{ + "fun": function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.List(cty.String), + }, + }, + Type: function.StaticReturnType(cty.List(cty.String)), + }), + }, + ProviderFunctions: map[string]providers.FunctionDecl{ + "fun": { + Parameters: []providers.FunctionParam{ + { + Name: "list", + Type: cty.List(cty.String), + }, + }, + ReturnType: cty.List(cty.String), + }, + }, + Want: `{"format_version":"1.0","function_signatures":{"fun":{"return_type":["list","string"],"parameters":[{"name":"list","type":["list","string"]}]}}}`, + }, + { + Name: "returns diagnostics on failure", + Functions: map[string]function.Function{ + "fun": function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: func(args []cty.Value) (ret cty.Type, err error) { + return cty.DynamicPseudoType, fmt.Errorf("error") + }, + }), + }, + WantErr: "Failed to serialize function \"fun\": error", + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d-%s", i, test.Name), func(t *testing.T) { + got, diags := Marshal(test.Functions) + if test.WantErr != "" { + if !diags.HasErrors() { + t.Fatal("expected error, got none") + } + if diags.Err().Error() != test.WantErr { + t.Fatalf("expected error %q, got %q", test.WantErr, diags.Err()) + } + } else { + if diags.HasErrors() { + t.Fatal(diags) + } + + if diff := cmp.Diff(test.Want, string(got)); diff != "" { + t.Fatalf("mismatch of function signature: %s", diff) + } + } + + if test.ProviderFunctions != nil { + // Provider functions should marshal identically to cty + // functions, without the wrapping object. + got := MarshalProviderFunctions(test.ProviderFunctions) + + gotBytes, err := json.Marshal(got) + + if err != nil { + // these should never error + t.Fatal("Marshal of ProviderFunctions failed:", err) + } + + var want functions + + err = json.Unmarshal([]byte(test.Want), &want) + + if err != nil { + // these should never error + t.Fatal("Unmarshal of Want failed:", err) + } + + wantBytes, err := json.Marshal(want.Signatures) + + if err != nil { + // these should never error + t.Fatal("Marshal of Want.Signatures failed:", err) + } + + if diff := cmp.Diff(string(wantBytes), string(gotBytes)); diff != "" { + t.Fatalf("mismatch of function signature: %s", diff) + } + } + }) + } +} diff --git a/internal/command/jsonfunction/parameter.go b/internal/command/jsonfunction/parameter.go new file mode 100644 index 0000000000..147f20a4e0 --- /dev/null +++ b/internal/command/jsonfunction/parameter.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonfunction + +import ( + "github.com/hashicorp/terraform/internal/providers" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// parameter represents a parameter to a function. +type parameter struct { + // Name is an optional name for the argument. + Name string `json:"name,omitempty"` + + // Description is an optional human-readable description + // of the argument + Description string `json:"description,omitempty"` + + // IsNullable is true if null is acceptable value for the argument + IsNullable bool `json:"is_nullable,omitempty"` + + // A type that any argument for this parameter must conform to. + Type cty.Type `json:"type"` +} + +func marshalParameter(p *function.Parameter) *parameter { + if p == nil { + return ¶meter{} + } + + return ¶meter{ + Name: p.Name, + Description: p.Description, + IsNullable: p.AllowNull, + Type: p.Type, + } +} + +func marshalParameters(parameters []function.Parameter) []*parameter { + ret := make([]*parameter, len(parameters)) + for k, p := range parameters { + ret[k] = marshalParameter(&p) + } + return ret +} + +func marshalProviderParameter(p providers.FunctionParam) *parameter { + return ¶meter{ + Name: p.Name, + Description: p.Description, + IsNullable: p.AllowNullValue, + Type: p.Type, + } +} + +func marshalProviderParameters(parameters []providers.FunctionParam) []*parameter { + ret := make([]*parameter, len(parameters)) + for k, p := range parameters { + ret[k] = marshalProviderParameter(p) + } + return ret +} diff --git a/internal/command/jsonfunction/parameter_test.go b/internal/command/jsonfunction/parameter_test.go new file mode 100644 index 0000000000..ff04c3abb8 --- /dev/null +++ b/internal/command/jsonfunction/parameter_test.go @@ -0,0 +1,67 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonfunction + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +func TestMarshalParameter(t *testing.T) { + tests := []struct { + Name string + Input *function.Parameter + Want *parameter + }{ + { + "call with nil", + nil, + ¶meter{}, + }, + { + "parameter with description", + &function.Parameter{ + Name: "timestamp", + Description: "`timestamp` returns a UTC timestamp string in [RFC 3339]", + Type: cty.String, + }, + ¶meter{ + Name: "timestamp", + Description: "`timestamp` returns a UTC timestamp string in [RFC 3339]", + Type: cty.String, + }, + }, + { + "parameter with additional properties", + &function.Parameter{ + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }, + ¶meter{ + Name: "value", + Type: cty.DynamicPseudoType, + IsNullable: true, + }, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d-%s", i, test.Name), func(t *testing.T) { + got := marshalParameter(test.Input) + + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("mismatch of parameter signature: %s", diff) + } + }) + } +} diff --git a/internal/command/jsonfunction/return_type.go b/internal/command/jsonfunction/return_type.go new file mode 100644 index 0000000000..ac02f1f7bb --- /dev/null +++ b/internal/command/jsonfunction/return_type.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package jsonfunction + +import ( + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +func getReturnType(f function.Function) (cty.Type, error) { + args := make([]cty.Type, 0) + for _, param := range f.Params() { + args = append(args, param.Type) + } + if f.VarParam() != nil { + args = append(args, f.VarParam().Type) + } + + return f.ReturnType(args) +} diff --git a/internal/command/jsonplan/condition.go b/internal/command/jsonplan/condition.go deleted file mode 100644 index dc7c4faf6d..0000000000 --- a/internal/command/jsonplan/condition.go +++ /dev/null @@ -1,44 +0,0 @@ -package jsonplan - -// conditionResult is the representation of an evaluated condition block. -// -// This no longer really fits how Terraform is modelling checks -- we're now -// treating check status as a whole-object thing rather than an individual -// condition thing -- but we've preserved this for now to remain as compatible -// as possible with the interface we'd experimentally-implemented but not -// documented in the Terraform v1.2 release, before we'd really solidified the -// use-cases for checks outside of just making a single plan and apply -// operation fail with an error. -type conditionResult struct { - // This is a weird "pseudo-comment" noting that we're deprecating this - // not-previously-documented, experimental representation of conditions - // in favor of the "checks" property which better fits Terraform Core's - // modelling of checks. - DeprecationNotice conditionResultDeprecationNotice `json:"//"` - - // Address is the absolute address of the condition's containing object. - Address string `json:"address,omitempty"` - - // Type is the condition block type, and is one of ResourcePrecondition, - // ResourcePostcondition, or OutputPrecondition. - Type string `json:"condition_type,omitempty"` - - // Result is true if the condition succeeds, and false if it fails or is - // known only at apply time. - Result bool `json:"result"` - - // Unknown is true if the condition can only be evaluated at apply time. - Unknown bool `json:"unknown"` - - // ErrorMessage is the custom error for a failing condition. It is only - // present if the condition fails. - ErrorMessage string `json:"error_message,omitempty"` -} - -type conditionResultDeprecationNotice struct{} - -func (n conditionResultDeprecationNotice) MarshalJSON() ([]byte, error) { - return conditionResultDeprecationNoticeJSON, nil -} - -var conditionResultDeprecationNoticeJSON = []byte(`"This previously-experimental representation of conditions is deprecated and will be removed in Terraform v1.4. Use the 'checks' property instead."`) diff --git a/internal/command/jsonplan/doc.go b/internal/command/jsonplan/doc.go index db1f3fb0cd..22966dc524 100644 --- a/internal/command/jsonplan/doc.go +++ b/internal/command/jsonplan/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package jsonplan implements methods for outputting a plan in a // machine-readable json format package jsonplan diff --git a/internal/command/jsonplan/module.go b/internal/command/jsonplan/module.go index a122e77da4..68ea66344a 100644 --- a/internal/command/jsonplan/module.go +++ b/internal/command/jsonplan/module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonplan // module is the representation of a module in state. This can be the root diff --git a/internal/command/jsonplan/plan.go b/internal/command/jsonplan/plan.go index 048d68ea71..16bd9d07e7 100644 --- a/internal/command/jsonplan/plan.go +++ b/internal/command/jsonplan/plan.go @@ -1,20 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonplan import ( "encoding/json" "fmt" "sort" + "strings" + "time" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/command/jsonchecks" "github.com/hashicorp/terraform/internal/command/jsonconfig" "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/terraform" @@ -24,9 +30,32 @@ import ( // FormatVersion represents the version of the json format and will be // incremented for any change to this format that requires changes to a // consuming parser. -const FormatVersion = "1.1" +const ( + FormatVersion = "1.2" -// Plan is the top-level representation of the json format of a plan. It includes + ResourceInstanceReplaceBecauseCannotUpdate = "replace_because_cannot_update" + ResourceInstanceReplaceBecauseTainted = "replace_because_tainted" + ResourceInstanceReplaceByRequest = "replace_by_request" + ResourceInstanceReplaceByTriggers = "replace_by_triggers" + ResourceInstanceDeleteBecauseNoResourceConfig = "delete_because_no_resource_config" + ResourceInstanceDeleteBecauseWrongRepetition = "delete_because_wrong_repetition" + ResourceInstanceDeleteBecauseCountIndex = "delete_because_count_index" + ResourceInstanceDeleteBecauseEachKey = "delete_because_each_key" + ResourceInstanceDeleteBecauseNoModule = "delete_because_no_module" + ResourceInstanceDeleteBecauseNoMoveTarget = "delete_because_no_move_target" + ResourceInstanceReadBecauseConfigUnknown = "read_because_config_unknown" + ResourceInstanceReadBecauseDependencyPending = "read_because_dependency_pending" + ResourceInstanceReadBecauseCheckNested = "read_because_check_nested" + + DeferredReasonUnknown = "unknown" + DeferredReasonInstanceCountUnknown = "instance_count_unknown" + DeferredReasonResourceConfigUnknown = "resource_config_unknown" + DeferredReasonProviderConfigUnknown = "provider_config_unknown" + DeferredReasonDeferredPrereq = "deferred_prereq" + DeferredReasonAbsentPrereq = "absent_prereq" +) + +// plan is the top-level representation of the json format of a plan. It includes // the complete config and current state. type plan struct { FormatVersion string `json:"format_version,omitempty"` @@ -35,14 +64,18 @@ type plan struct { PlannedValues stateValues `json:"planned_values,omitempty"` // ResourceDrift and ResourceChanges are sorted in a user-friendly order // that is undefined at this time, but consistent. - ResourceDrift []resourceChange `json:"resource_drift,omitempty"` - ResourceChanges []resourceChange `json:"resource_changes,omitempty"` - OutputChanges map[string]change `json:"output_changes,omitempty"` - PriorState json.RawMessage `json:"prior_state,omitempty"` - Config json.RawMessage `json:"configuration,omitempty"` - RelevantAttributes []resourceAttr `json:"relevant_attributes,omitempty"` - Conditions []conditionResult `json:"condition_results,omitempty"` - Checks json.RawMessage `json:"checks,omitempty"` + ResourceDrift []ResourceChange `json:"resource_drift,omitempty"` + ResourceChanges []ResourceChange `json:"resource_changes,omitempty"` + DeferredChanges []DeferredResourceChange `json:"deferred_changes,omitempty"` + OutputChanges map[string]Change `json:"output_changes,omitempty"` + PriorState json.RawMessage `json:"prior_state,omitempty"` + Config json.RawMessage `json:"configuration,omitempty"` + RelevantAttributes []ResourceAttr `json:"relevant_attributes,omitempty"` + Checks json.RawMessage `json:"checks,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Applyable bool `json:"applyable"` + Complete bool `json:"complete"` + Errored bool `json:"errored"` } func newPlan() *plan { @@ -51,35 +84,38 @@ func newPlan() *plan { } } -// resourceAttr contains the address and attribute of an external for the +// ResourceAttr contains the address and attribute of an external for the // RelevantAttributes in the plan. -type resourceAttr struct { +type ResourceAttr struct { Resource string `json:"resource"` Attr json.RawMessage `json:"attribute"` } // Change is the representation of a proposed change for an object. -type change struct { +type Change struct { // Actions are the actions that will be taken on the object selected by the // properties below. Valid actions values are: // ["no-op"] // ["create"] // ["read"] // ["update"] - // ["delete", "create"] - // ["create", "delete"] + // ["delete", "create"] (replace) + // ["create", "delete"] (replace) // ["delete"] - // The two "replace" actions are represented in this way to allow callers to - // e.g. just scan the list for "delete" to recognize all three situations - // where the object will be deleted, allowing for any new deletion - // combinations that might be added in future. + // ["forget"] + // ["create", "forget"] (replace) + // The three "replace" actions are represented in this way to allow callers + // to, e.g., just scan the list for "delete" to recognize all three + // situations where the object will be deleted, allowing for any new + // deletion combinations that might be added in future. Actions []string `json:"actions,omitempty"` // Before and After are representations of the object value both before and - // after the action. For ["create"] and ["delete"] actions, either "before" - // or "after" is unset (respectively). For ["no-op"], the before and after - // values are identical. The "after" value will be incomplete if there are - // values within it that won't be known until after apply. + // after the action. For ["create"] and ["delete"]/["forget"] actions, + // either "before" or "after" is unset (respectively). For ["no-op"], the + // before and after values are identical. The "after" value will be + // incomplete if there are values within it that won't be known until after + // apply. Before json.RawMessage `json:"before,omitempty"` After json.RawMessage `json:"after,omitempty"` @@ -105,6 +141,42 @@ type change struct { // consists of one or more steps, each of which will be a number or a // string. ReplacePaths json.RawMessage `json:"replace_paths,omitempty"` + + // Importing contains the import metadata about this operation. If importing + // is present (ie. not null) then the change is an import operation in + // addition to anything mentioned in the actions field. The actual contents + // of the Importing struct is subject to change, so downstream consumers + // should treat any values in here as strictly optional. + Importing *Importing `json:"importing,omitempty"` + + // GeneratedConfig contains any HCL config generated for this resource + // during planning as a string. + // + // If this is populated, then Importing should also be populated but this + // might change in the future. However, not all Importing changes will + // contain generated config. + GeneratedConfig string `json:"generated_config,omitempty"` + + // BeforeIdentity and AfterIdentity are representations of the resource + // identity value both before and after the action. + BeforeIdentity json.RawMessage `json:"before_identity,omitempty"` + AfterIdentity json.RawMessage `json:"after_identity,omitempty"` +} + +// Importing is a nested object for the resource import metadata. +type Importing struct { + // The original ID of this resource used to target it as part of planned + // import operation. + ID string `json:"id,omitempty"` + + // Unknown indicates the ID was unknown at the time of planning. This + // would have led to the overall change being deferred, as such this should + // only be true when processing changes from the deferred changes list. + Unknown bool `json:"unknown,omitempty"` + + // The identity can be used instead of the ID to target the resource as part + // of the planned import operation. + Identity json.RawMessage `json:"identity,omitempty"` } type output struct { @@ -121,6 +193,54 @@ type variable struct { Value json.RawMessage `json:"value,omitempty"` } +// MarshalForRenderer returns the pre-json encoding changes of the requested +// plan, in a format available to the structured renderer. +// +// This function does a small part of the Marshal function, as it only returns +// the part of the plan required by the jsonformat.Plan renderer. +func MarshalForRenderer( + p *plans.Plan, + schemas *terraform.Schemas, +) (map[string]Change, []ResourceChange, []ResourceChange, []ResourceAttr, error) { + output := newPlan() + + var err error + if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil { + return nil, nil, nil, nil, err + } + + if output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas); err != nil { + return nil, nil, nil, nil, err + } + + if len(p.DriftedResources) > 0 { + // In refresh-only mode, we render all resources marked as drifted, + // including those which have moved without other changes. In other plan + // modes, move-only changes will be included in the planned changes, so + // we skip them here. + var driftedResources []*plans.ResourceInstanceChangeSrc + if p.UIMode == plans.RefreshOnlyMode { + driftedResources = p.DriftedResources + } else { + for _, dr := range p.DriftedResources { + if dr.Action != plans.NoOp { + driftedResources = append(driftedResources, dr) + } + } + } + output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas) + if err != nil { + return nil, nil, nil, nil, err + } + } + + if err := output.marshalRelevantAttrs(p); err != nil { + return nil, nil, nil, nil, err + } + + return output.OutputChanges, output.ResourceChanges, output.ResourceDrift, output.RelevantAttributes, nil +} + // Marshal returns the json encoding of a terraform plan. func Marshal( config *configs.Config, @@ -130,6 +250,10 @@ func Marshal( ) ([]byte, error) { output := newPlan() output.TerraformVersion = version.String() + output.Timestamp = p.Timestamp.Format(time.RFC3339) + output.Applyable = p.Applyable + output.Complete = p.Complete + output.Errored = p.Errored err := output.marshalPlanVariables(p.VariableValues, config.Module.Variables) if err != nil { @@ -158,7 +282,7 @@ func Marshal( } } } - output.ResourceDrift, err = output.marshalResourceChanges(driftedResources, schemas) + output.ResourceDrift, err = MarshalResourceChanges(driftedResources, schemas) if err != nil { return nil, fmt.Errorf("error in marshaling resource drift: %s", err) } @@ -170,22 +294,22 @@ func Marshal( // output.ResourceChanges if p.Changes != nil { - output.ResourceChanges, err = output.marshalResourceChanges(p.Changes.Resources, schemas) + output.ResourceChanges, err = MarshalResourceChanges(p.Changes.Resources, schemas) if err != nil { return nil, fmt.Errorf("error in marshaling resource changes: %s", err) } } - // output.OutputChanges - err = output.marshalOutputChanges(p.Changes) - if err != nil { - return nil, fmt.Errorf("error in marshaling output changes: %s", err) + if p.DeferredResources != nil { + output.DeferredChanges, err = MarshalDeferredResourceChanges(p.DeferredResources, schemas) + if err != nil { + return nil, fmt.Errorf("error in marshaling deferred resource changes: %s", err) + } } - // output.Conditions (deprecated in favor of Checks, below) - err = output.marshalCheckResults(p.Checks) - if err != nil { - return nil, fmt.Errorf("error in marshaling check results: %s", err) + // output.OutputChanges + if output.OutputChanges, err = MarshalOutputChanges(p.Changes); err != nil { + return nil, fmt.Errorf("error in marshaling output changes: %s", err) } // output.Checks @@ -207,8 +331,7 @@ func Marshal( return nil, fmt.Errorf("error marshaling config: %s", err) } - ret, err := json.Marshal(output) - return ret, err + return json.Marshal(output) } func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls map[string]*configs.Variable) error { @@ -265,18 +388,26 @@ func (p *plan) marshalPlanVariables(vars map[string]plans.DynamicValue, decls ma return nil } -func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]resourceChange, error) { - var ret []resourceChange +// MarshalResourceChanges converts the provided internal representation of +// ResourceInstanceChangeSrc objects into the public structured JSON changes. +// +// This function is referenced directly from the structured renderer tests, to +// ensure parity between the renderers. It probably shouldn't be used anywhere +// else. +func MarshalResourceChanges(resources []*plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]ResourceChange, error) { + var ret []ResourceChange - for _, rc := range resources { - var r resourceChange - addr := rc.Addr - r.Address = addr.String() - if !addr.Equal(rc.PrevRunAddr) { - r.PreviousAddress = rc.PrevRunAddr.String() + var sortedResources []*plans.ResourceInstanceChangeSrc + sortedResources = append(sortedResources, resources...) + sort.Slice(sortedResources, func(i, j int) bool { + if !sortedResources[i].Addr.Equal(sortedResources[j].Addr) { + return sortedResources[i].Addr.Less(sortedResources[j].Addr) } + return sortedResources[i].DeposedKey < sortedResources[j].DeposedKey + }) - dataSource := addr.Resource.Resource.Mode == addrs.DataResourceMode + for _, rc := range sortedResources { + dataSource := rc.Addr.Resource.Resource.Mode == addrs.DataResourceMode // We create "delete" actions for data resources so we can clean up // their entries in state, but this is an implementation detail that // users shouldn't see. @@ -284,16 +415,297 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS continue } - schema, _ := schemas.ResourceTypeConfig( - rc.ProviderAddr.Provider, - addr.Resource.Resource.Mode, - addr.Resource.Resource.Type, - ) - if schema == nil { - return nil, fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider) + r, err := marshalResourceChange(rc, schemas) + if err != nil { + return nil, err + } + ret = append(ret, r) + } + + return ret, nil +} + +func marshalResourceChange(rc *plans.ResourceInstanceChangeSrc, schemas *terraform.Schemas) (ResourceChange, error) { + var r ResourceChange + addr := rc.Addr + r.Address = addr.String() + if !addr.Equal(rc.PrevRunAddr) { + r.PreviousAddress = rc.PrevRunAddr.String() + } + + schema := schemas.ResourceTypeConfig( + rc.ProviderAddr.Provider, + addr.Resource.Resource.Mode, + addr.Resource.Resource.Type, + ) + if schema.Body == nil { + return r, fmt.Errorf("no schema found for %s (in provider %s)", r.Address, rc.ProviderAddr.Provider) + } + + changeV, err := rc.Decode(schema) + if err != nil { + return r, err + } + // We drop the marks from the change, as decoding is only an + // intermediate step to re-encode the values as json + changeV.Before, _ = changeV.Before.UnmarkDeep() + changeV.After, _ = changeV.After.UnmarkDeep() + + var before, after []byte + var beforeSensitive, afterSensitive []byte + var afterUnknown cty.Value + + if changeV.Before != cty.NilVal { + before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) + if err != nil { + return r, err + } + sensitivePaths := rc.BeforeSensitivePaths + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(changeV.Before, nil)...) + bs := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.Before, marks.Sensitive, sensitivePaths)) + beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) + if err != nil { + return r, err + } + } + if changeV.After != cty.NilVal { + if changeV.After.IsWhollyKnown() { + after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) + if err != nil { + return r, err + } + afterUnknown = cty.EmptyObjectVal + } else { + filteredAfter := omitUnknowns(changeV.After) + if filteredAfter.IsNull() { + after = nil + } else { + after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) + if err != nil { + return r, err + } + } + afterUnknown = unknownAsBool(changeV.After) + } + sensitivePaths := rc.AfterSensitivePaths + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(changeV.After, nil)...) + as := jsonstate.SensitiveAsBool(marks.MarkPaths(changeV.After, marks.Sensitive, sensitivePaths)) + afterSensitive, err = ctyjson.Marshal(as, as.Type()) + if err != nil { + return r, err + } + } + + a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) + if err != nil { + return r, err + } + replacePaths, err := encodePaths(rc.RequiredReplace) + if err != nil { + return r, err + } + + var importing *Importing + if rc.Importing != nil { + if rc.Importing.Unknown { + importing = &Importing{Unknown: true} + } else { + if rc.Importing.ID != "" { + importing = &Importing{ID: rc.Importing.ID} + } else { + identity, err := rc.Importing.Identity.Decode(schema.Identity.ImpliedType()) + if err != nil { + return r, err + } + rawIdentity, err := ctyjson.Marshal(identity, identity.Type()) + if err != nil { + return r, err + } + + importing = &Importing{ + Identity: json.RawMessage(rawIdentity), + } + } + } + } + + var beforeIdentity, afterIdentity []byte + if schema.Identity != nil && rc.BeforeIdentity != nil { + identity, err := rc.BeforeIdentity.Decode(schema.Identity.ImpliedType()) + if err != nil { + return r, err + } + beforeIdentity, err = ctyjson.Marshal(identity, identity.Type()) + if err != nil { + return r, err + } + } + if schema.Identity != nil && rc.AfterIdentity != nil { + identity, err := rc.AfterIdentity.Decode(schema.Identity.ImpliedType()) + if err != nil { + return r, err + } + afterIdentity, err = ctyjson.Marshal(identity, identity.Type()) + if err != nil { + return r, err + } + } + + r.Change = Change{ + Actions: actionString(rc.Action.String()), + Before: json.RawMessage(before), + After: json.RawMessage(after), + AfterUnknown: a, + BeforeSensitive: json.RawMessage(beforeSensitive), + AfterSensitive: json.RawMessage(afterSensitive), + ReplacePaths: replacePaths, + Importing: importing, + GeneratedConfig: rc.GeneratedConfig, + BeforeIdentity: json.RawMessage(beforeIdentity), + AfterIdentity: json.RawMessage(afterIdentity), + } + + if rc.DeposedKey != states.NotDeposed { + r.Deposed = rc.DeposedKey.String() + } + + key := addr.Resource.Key + if key != nil { + if key == addrs.WildcardKey { + // The wildcard key should only be set for a deferred instance. + r.IndexUnknown = true + } else { + value := key.Value() + if r.Index, err = ctyjson.Marshal(value, value.Type()); err != nil { + return r, err + } + } + } + + switch addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + r.Mode = jsonstate.ManagedResourceMode + case addrs.DataResourceMode: + r.Mode = jsonstate.DataResourceMode + default: + return r, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String()) + } + r.ModuleAddress = addr.Module.String() + r.Name = addr.Resource.Resource.Name + r.Type = addr.Resource.Resource.Type + r.ProviderName = rc.ProviderAddr.Provider.String() + + switch rc.ActionReason { + case plans.ResourceInstanceChangeNoReason: + r.ActionReason = "" // will be omitted in output + case plans.ResourceInstanceReplaceBecauseCannotUpdate: + r.ActionReason = ResourceInstanceReplaceBecauseCannotUpdate + case plans.ResourceInstanceReplaceBecauseTainted: + r.ActionReason = ResourceInstanceReplaceBecauseTainted + case plans.ResourceInstanceReplaceByRequest: + r.ActionReason = ResourceInstanceReplaceByRequest + case plans.ResourceInstanceReplaceByTriggers: + r.ActionReason = ResourceInstanceReplaceByTriggers + case plans.ResourceInstanceDeleteBecauseNoResourceConfig: + r.ActionReason = ResourceInstanceDeleteBecauseNoResourceConfig + case plans.ResourceInstanceDeleteBecauseWrongRepetition: + r.ActionReason = ResourceInstanceDeleteBecauseWrongRepetition + case plans.ResourceInstanceDeleteBecauseCountIndex: + r.ActionReason = ResourceInstanceDeleteBecauseCountIndex + case plans.ResourceInstanceDeleteBecauseEachKey: + r.ActionReason = ResourceInstanceDeleteBecauseEachKey + case plans.ResourceInstanceDeleteBecauseNoModule: + r.ActionReason = ResourceInstanceDeleteBecauseNoModule + case plans.ResourceInstanceDeleteBecauseNoMoveTarget: + r.ActionReason = ResourceInstanceDeleteBecauseNoMoveTarget + case plans.ResourceInstanceReadBecauseConfigUnknown: + r.ActionReason = ResourceInstanceReadBecauseConfigUnknown + case plans.ResourceInstanceReadBecauseDependencyPending: + r.ActionReason = ResourceInstanceReadBecauseDependencyPending + case plans.ResourceInstanceReadBecauseCheckNested: + r.ActionReason = ResourceInstanceReadBecauseCheckNested + default: + return r, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) + } + + return r, nil +} + +// MarshalDeferredResourceChanges converts the provided internal representation +// of DeferredResourceInstanceChangeSrc objects into the public structured JSON +// changes. +// This is public to make testing easier. +func MarshalDeferredResourceChanges(resources []*plans.DeferredResourceInstanceChangeSrc, schemas *terraform.Schemas) ([]DeferredResourceChange, error) { + var ret []DeferredResourceChange + + var sortedResources []*plans.DeferredResourceInstanceChangeSrc + sortedResources = append(sortedResources, resources...) + sort.Slice(sortedResources, func(i, j int) bool { + if !sortedResources[i].ChangeSrc.Addr.Equal(sortedResources[j].ChangeSrc.Addr) { + return sortedResources[i].ChangeSrc.Addr.Less(sortedResources[j].ChangeSrc.Addr) + } + return sortedResources[i].ChangeSrc.DeposedKey < sortedResources[j].ChangeSrc.DeposedKey + }) + + for _, rc := range sortedResources { + change, err := marshalResourceChange(rc.ChangeSrc, schemas) + if err != nil { + return nil, err } - changeV, err := rc.Decode(schema.ImpliedType()) + deferredChange := DeferredResourceChange{ + ResourceChange: change, + } + + switch rc.DeferredReason { + case providers.DeferredReasonInstanceCountUnknown: + deferredChange.Reason = DeferredReasonInstanceCountUnknown + case providers.DeferredReasonResourceConfigUnknown: + deferredChange.Reason = DeferredReasonResourceConfigUnknown + case providers.DeferredReasonProviderConfigUnknown: + deferredChange.Reason = DeferredReasonProviderConfigUnknown + case providers.DeferredReasonAbsentPrereq: + deferredChange.Reason = DeferredReasonAbsentPrereq + case providers.DeferredReasonDeferredPrereq: + deferredChange.Reason = DeferredReasonDeferredPrereq + default: + // If we find a reason we don't know about, we'll just mark it as + // unknown. This is a bit of a safety net to ensure that we don't + // break if new reasons are introduced in future versions of the + // provider protocol. + deferredChange.Reason = DeferredReasonUnknown + } + + ret = append(ret, deferredChange) + } + + return ret, nil +} + +// MarshalOutputChanges converts the provided internal representation of +// Changes objects into the structured JSON representation. +// +// This function is referenced directly from the structured renderer tests, to +// ensure parity between the renderers. It probably shouldn't be used anywhere +// else. +func MarshalOutputChanges(changes *plans.ChangesSrc) (map[string]Change, error) { + if changes == nil { + // Nothing to do! + return nil, nil + } + + outputChanges := make(map[string]Change, len(changes.Outputs)) + for _, oc := range changes.Outputs { + + // Skip output changes that are not from the root module. + // These are automatically stripped from plans that are written to disk + // elsewhere, we just need to duplicate the logic here in case anyone + // is converting this plan directly from memory. + if !oc.Addr.Module.IsRoot() { + continue + } + + changeV, err := oc.Decode() if err != nil { return nil, err } @@ -303,7 +715,6 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS changeV.After, _ = changeV.After.UnmarkDeep() var before, after []byte - var beforeSensitive, afterSensitive []byte var afterUnknown cty.Value if changeV.Before != cty.NilVal { @@ -311,15 +722,6 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS if err != nil { return nil, err } - marks := rc.BeforeValMarks - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(changeV.Before, nil)...) - } - bs := jsonstate.SensitiveAsBool(changeV.Before.MarkWithPaths(marks)) - beforeSensitive, err = ctyjson.Marshal(bs, bs.Type()) - if err != nil { - return nil, err - } } if changeV.After != cty.NilVal { if changeV.After.IsWhollyKnown() { @@ -327,143 +729,6 @@ func (p *plan) marshalResourceChanges(resources []*plans.ResourceInstanceChangeS if err != nil { return nil, err } - afterUnknown = cty.EmptyObjectVal - } else { - filteredAfter := omitUnknowns(changeV.After) - if filteredAfter.IsNull() { - after = nil - } else { - after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) - if err != nil { - return nil, err - } - } - afterUnknown = unknownAsBool(changeV.After) - } - marks := rc.AfterValMarks - if schema.ContainsSensitive() { - marks = append(marks, schema.ValueMarks(changeV.After, nil)...) - } - as := jsonstate.SensitiveAsBool(changeV.After.MarkWithPaths(marks)) - afterSensitive, err = ctyjson.Marshal(as, as.Type()) - if err != nil { - return nil, err - } - } - - a, err := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) - if err != nil { - return nil, err - } - replacePaths, err := encodePaths(rc.RequiredReplace) - if err != nil { - return nil, err - } - - r.Change = change{ - Actions: actionString(rc.Action.String()), - Before: json.RawMessage(before), - After: json.RawMessage(after), - AfterUnknown: a, - BeforeSensitive: json.RawMessage(beforeSensitive), - AfterSensitive: json.RawMessage(afterSensitive), - ReplacePaths: replacePaths, - } - - if rc.DeposedKey != states.NotDeposed { - r.Deposed = rc.DeposedKey.String() - } - - key := addr.Resource.Key - if key != nil { - r.Index = key - } - - switch addr.Resource.Resource.Mode { - case addrs.ManagedResourceMode: - r.Mode = "managed" - case addrs.DataResourceMode: - r.Mode = "data" - default: - return nil, fmt.Errorf("resource %s has an unsupported mode %s", r.Address, addr.Resource.Resource.Mode.String()) - } - r.ModuleAddress = addr.Module.String() - r.Name = addr.Resource.Resource.Name - r.Type = addr.Resource.Resource.Type - r.ProviderName = rc.ProviderAddr.Provider.String() - - switch rc.ActionReason { - case plans.ResourceInstanceChangeNoReason: - r.ActionReason = "" // will be omitted in output - case plans.ResourceInstanceReplaceBecauseCannotUpdate: - r.ActionReason = "replace_because_cannot_update" - case plans.ResourceInstanceReplaceBecauseTainted: - r.ActionReason = "replace_because_tainted" - case plans.ResourceInstanceReplaceByRequest: - r.ActionReason = "replace_by_request" - case plans.ResourceInstanceReplaceByTriggers: - r.ActionReason = "replace_by_triggers" - case plans.ResourceInstanceDeleteBecauseNoResourceConfig: - r.ActionReason = "delete_because_no_resource_config" - case plans.ResourceInstanceDeleteBecauseWrongRepetition: - r.ActionReason = "delete_because_wrong_repetition" - case plans.ResourceInstanceDeleteBecauseCountIndex: - r.ActionReason = "delete_because_count_index" - case plans.ResourceInstanceDeleteBecauseEachKey: - r.ActionReason = "delete_because_each_key" - case plans.ResourceInstanceDeleteBecauseNoModule: - r.ActionReason = "delete_because_no_module" - case plans.ResourceInstanceReadBecauseConfigUnknown: - r.ActionReason = "read_because_config_unknown" - case plans.ResourceInstanceReadBecauseDependencyPending: - r.ActionReason = "read_because_dependency_pending" - default: - return nil, fmt.Errorf("resource %s has an unsupported action reason %s", r.Address, rc.ActionReason) - } - - ret = append(ret, r) - - } - - sort.Slice(ret, func(i, j int) bool { - return ret[i].Address < ret[j].Address - }) - - return ret, nil -} - -func (p *plan) marshalOutputChanges(changes *plans.Changes) error { - if changes == nil { - // Nothing to do! - return nil - } - - p.OutputChanges = make(map[string]change, len(changes.Outputs)) - for _, oc := range changes.Outputs { - changeV, err := oc.Decode() - if err != nil { - return err - } - // We drop the marks from the change, as decoding is only an - // intermediate step to re-encode the values as json - changeV.Before, _ = changeV.Before.UnmarkDeep() - changeV.After, _ = changeV.After.UnmarkDeep() - - var before, after []byte - var afterUnknown cty.Value - - if changeV.Before != cty.NilVal { - before, err = ctyjson.Marshal(changeV.Before, changeV.Before.Type()) - if err != nil { - return err - } - } - if changeV.After != cty.NilVal { - if changeV.After.IsWhollyKnown() { - after, err = ctyjson.Marshal(changeV.After, changeV.After.Type()) - if err != nil { - return err - } afterUnknown = cty.False } else { filteredAfter := omitUnknowns(changeV.After) @@ -472,7 +737,7 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error { } else { after, err = ctyjson.Marshal(filteredAfter, filteredAfter.Type()) if err != nil { - return err + return nil, err } } afterUnknown = unknownAsBool(changeV.After) @@ -489,111 +754,31 @@ func (p *plan) marshalOutputChanges(changes *plans.Changes) error { } sensitive, err := ctyjson.Marshal(outputSensitive, outputSensitive.Type()) if err != nil { - return err + return nil, err } a, _ := ctyjson.Marshal(afterUnknown, afterUnknown.Type()) - c := change{ + c := Change{ Actions: actionString(oc.Action.String()), Before: json.RawMessage(before), After: json.RawMessage(after), AfterUnknown: a, BeforeSensitive: json.RawMessage(sensitive), AfterSensitive: json.RawMessage(sensitive), + + // Just to be explicit, outputs cannot be imported so this is always + // nil. + Importing: nil, } - p.OutputChanges[oc.Addr.OutputValue.Name] = c + outputChanges[oc.Addr.OutputValue.Name] = c } - return nil + return outputChanges, nil } -func (p *plan) marshalCheckResults(results *states.CheckResults) error { - if results == nil { - return nil - } - - // For the moment this is still producing the flat structure from - // the initial release of preconditions/postconditions in Terraform v1.2. - // This therefore discards the aggregate information about any configuration - // objects that might end up with zero instances declared. - // We'll need to think about what we want to do here in order to expose - // the full check details while hopefully also remaining compatible with - // what we previously documented. - - for _, configElem := range results.ConfigResults.Elems { - for _, objectElem := range configElem.Value.ObjectResults.Elems { - objectAddr := objectElem.Key - result := objectElem.Value - - var boolResult, unknown bool - switch result.Status { - case checks.StatusPass: - boolResult = true - case checks.StatusFail: - boolResult = false - case checks.StatusError: - boolResult = false - case checks.StatusUnknown: - unknown = true - } - - // We need to export one of the previously-documented condition - // types here even though we're no longer actually representing - // individual checks, so we'll fib a bit and just report a - // fixed string depending on the object type. Note that this - // means we'll report that a resource postcondition failed even - // if it was actually a precondition, which is non-ideal but - // hopefully we replace this with an object-first data structure - // in the near future. - fakeCheckType := "Condition" - switch objectAddr.(type) { - case addrs.AbsResourceInstance: - fakeCheckType = "ResourcePostcondition" - case addrs.AbsOutputValue: - fakeCheckType = "OutputPrecondition" - } - - // NOTE: Our original design for this data structure exposed - // each declared check individually, but checks don't really - // have durable addresses between runs so we've now simplified - // the model to say that it's entire objects that pass or fail, - // via the combination of all of their checks. - // - // The public data structure for this was built around the - // original design and so we approximate that here by - // generating only a single "condition" per object in most cases, - // but will generate one for each error message if we're - // reporting a failure and we have at least one message. - if result.Status == checks.StatusFail && len(result.FailureMessages) != 0 { - for _, msg := range result.FailureMessages { - p.Conditions = append(p.Conditions, conditionResult{ - Address: objectAddr.String(), - Type: fakeCheckType, - Result: boolResult, - Unknown: unknown, - ErrorMessage: msg, - }) - } - } else { - p.Conditions = append(p.Conditions, conditionResult{ - Address: objectAddr.String(), - Type: fakeCheckType, - Result: boolResult, - Unknown: unknown, - }) - } - } - } - - sort.Slice(p.Conditions, func(i, j int) bool { - return p.Conditions[i].Address < p.Conditions[j].Address - }) - return nil -} - -func (p *plan) marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) error { +func (p *plan) marshalPlannedValues(changes *plans.ChangesSrc, schemas *terraform.Schemas) error { // marshal the planned changes into a module plan, err := marshalPlannedValues(changes, schemas) if err != nil { @@ -619,8 +804,17 @@ func (p *plan) marshalRelevantAttrs(plan *plans.Plan) error { return err } - p.RelevantAttributes = append(p.RelevantAttributes, resourceAttr{addr, path}) + p.RelevantAttributes = append(p.RelevantAttributes, ResourceAttr{addr, path}) } + + // we want our outputs to be deterministic, so we'll sort the attributes + // here. The order of the attributes is not important, as long as it is + // stable. + + sort.SliceStable(p.RelevantAttributes, func(i, j int) bool { + return strings.Compare(fmt.Sprintf("%#v", plan.RelevantAttributes[i]), fmt.Sprintf("%#v", plan.RelevantAttributes[j])) < 0 + }) + return nil } @@ -771,11 +965,51 @@ func actionString(action string) []string { return []string{"read"} case action == "DeleteThenCreate": return []string{"delete", "create"} + case action == "Forget": + return []string{"forget"} + case action == "CreateThenForget": + return []string{"create", "forget"} default: return []string{action} } } +// UnmarshalActions reverses the actionString function. +func UnmarshalActions(actions []string) plans.Action { + if len(actions) == 2 { + if actions[0] == "create" && actions[1] == "delete" { + return plans.CreateThenDelete + } + + if actions[0] == "delete" && actions[1] == "create" { + return plans.DeleteThenCreate + } + + if actions[0] == "create" && actions[1] == "forget" { + return plans.CreateThenForget + } + } + + if len(actions) == 1 { + switch actions[0] { + case "create": + return plans.Create + case "delete": + return plans.Delete + case "update": + return plans.Update + case "read": + return plans.Read + case "forget": + return plans.Forget + case "no-op": + return plans.NoOp + } + } + + panic("unrecognized action slice: " + strings.Join(actions, ", ")) +} + // encodePaths lossily encodes a cty.PathSet into an array of arrays of step // values, such as: // @@ -787,7 +1021,7 @@ func actionString(action string) []string { // indexes. // // JavaScript (or similar dynamic language) consumers of these values can -// iterate over the the steps starting from the root object to reach the +// iterate over the steps starting from the root object to reach the // value that each path is describing. func encodePaths(pathSet cty.PathSet) (json.RawMessage, error) { if pathSet.Empty() { diff --git a/internal/command/jsonplan/plan_test.go b/internal/command/jsonplan/plan_test.go index ef5b6cda24..0d7fc49cc9 100644 --- a/internal/command/jsonplan/plan_test.go +++ b/internal/command/jsonplan/plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonplan import ( @@ -7,6 +10,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" ) func TestOmitUnknowns(t *testing.T) { @@ -318,6 +324,97 @@ func TestEncodePaths(t *testing.T) { } } +func TestOutputs(t *testing.T) { + root := addrs.RootModuleInstance + + child, diags := addrs.ParseModuleInstanceStr("module.child") + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + + tests := map[string]struct { + changes *plans.ChangesSrc + expected map[string]Change + }{ + "copies all outputs": { + changes: &plans.ChangesSrc{ + Outputs: []*plans.OutputChangeSrc{ + { + Addr: root.OutputValue("first"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: root.OutputValue("second"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + }, + }, + expected: map[string]Change{ + "first": { + Actions: []string{"create"}, + Before: json.RawMessage("null"), + After: json.RawMessage("null"), + AfterUnknown: json.RawMessage("false"), + BeforeSensitive: json.RawMessage("false"), + AfterSensitive: json.RawMessage("false"), + }, + "second": { + Actions: []string{"create"}, + Before: json.RawMessage("null"), + After: json.RawMessage("null"), + AfterUnknown: json.RawMessage("false"), + BeforeSensitive: json.RawMessage("false"), + AfterSensitive: json.RawMessage("false"), + }, + }, + }, + "skips non root modules": { + changes: &plans.ChangesSrc{ + Outputs: []*plans.OutputChangeSrc{ + { + Addr: root.OutputValue("first"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: child.OutputValue("second"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + }, + }, + expected: map[string]Change{ + "first": { + Actions: []string{"create"}, + Before: json.RawMessage("null"), + After: json.RawMessage("null"), + AfterUnknown: json.RawMessage("false"), + BeforeSensitive: json.RawMessage("false"), + AfterSensitive: json.RawMessage("false"), + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + changes, err := MarshalOutputChanges(test.changes) + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + + if !cmp.Equal(changes, test.expected) { + t.Errorf("wrong result:\n %v\n", cmp.Diff(changes, test.expected)) + } + }) + } +} + func deepObjectValue(depth int) cty.Value { v := cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("a"), diff --git a/internal/command/jsonplan/resource.go b/internal/command/jsonplan/resource.go index 1e737a6266..f5e6bff617 100644 --- a/internal/command/jsonplan/resource.go +++ b/internal/command/jsonplan/resource.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonplan import ( @@ -6,7 +9,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" ) -// Resource is the representation of a resource in the json plan +// resource is the representation of a resource in the json plan type resource struct { // Address is the absolute resource address Address string `json:"address,omitempty"` @@ -39,12 +42,22 @@ type resource struct { // SensitiveValues is similar to AttributeValues, but with all sensitive // values replaced with true, and all non-sensitive leaf values omitted. SensitiveValues json.RawMessage `json:"sensitive_values,omitempty"` + + // The version of the resource identity schema the "identity" property + // conforms to. + // It's a pointer, because it should be optional, but also 0 is a valid + // schema version. + IdentitySchemaVersion *uint64 `json:"identity_schema_version,omitempty"` + + // The JSON representation of the resource identity, whose structure + // depends on the resource identity schema. + IdentityValues attributeValues `json:"identity,omitempty"` } -// resourceChange is a description of an individual change action that Terraform +// ResourceChange is a description of an individual change action that Terraform // plans to use to move from the prior state to a new state matching the // configuration. -type resourceChange struct { +type ResourceChange struct { // Address is the absolute resource address Address string `json:"address,omitempty"` @@ -67,10 +80,11 @@ type resourceChange struct { // "managed" or "data" Mode string `json:"mode,omitempty"` - Type string `json:"type,omitempty"` - Name string `json:"name,omitempty"` - Index addrs.InstanceKey `json:"index,omitempty"` - ProviderName string `json:"provider_name,omitempty"` + Type string `json:"type,omitempty"` + Name string `json:"name,omitempty"` + Index json.RawMessage `json:"index,omitempty"` + IndexUnknown bool `json:"index_unknown,omitempty"` + ProviderName string `json:"provider_name,omitempty"` // "deposed", if set, indicates that this action applies to a "deposed" // object of the given instance rather than to its "current" object. Omitted @@ -78,7 +92,7 @@ type resourceChange struct { Deposed string `json:"deposed,omitempty"` // Change describes the change that will be made to this object - Change change `json:"change,omitempty"` + Change Change `json:"change,omitempty"` // ActionReason is a keyword representing some optional extra context // for why the actions in Change.Actions were chosen. @@ -90,3 +104,13 @@ type resourceChange struct { // and treat them as an unspecified reason. ActionReason string `json:"action_reason,omitempty"` } + +// DeferredResourceChange is a description of a resource change that has been +// deferred for some reason. +type DeferredResourceChange struct { + // Reason is the reason why this resource change was deferred. + Reason string `json:"reason"` + + // Change contains any information we have about the deferred change. + ResourceChange ResourceChange `json:"resource_change"` +} diff --git a/internal/command/jsonplan/values.go b/internal/command/jsonplan/values.go index f727f8a1d4..af12ef5acd 100644 --- a/internal/command/jsonplan/values.go +++ b/internal/command/jsonplan/values.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonplan import ( @@ -10,7 +13,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/jsonstate" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" @@ -23,11 +25,11 @@ type stateValues struct { RootModule module `json:"root_module,omitempty"` } -// attributeValues is the JSON representation of the attribute values of the +// AttributeValues is the JSON representation of the attribute values of the // resource, whose structure depends on the resource type schema. type attributeValues map[string]interface{} -func marshalAttributeValues(value cty.Value, schema *configschema.Block) attributeValues { +func marshalAttributeValues(value cty.Value) attributeValues { if value == cty.NilVal || value.IsNull() { return nil } @@ -44,7 +46,7 @@ func marshalAttributeValues(value cty.Value, schema *configschema.Block) attribu // marshalPlannedOutputs takes a list of changes and returns a map of output // values -func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) { +func marshalPlannedOutputs(changes *plans.ChangesSrc) (map[string]output, error) { if changes.Outputs == nil { // No changes - we're done here! return nil, nil @@ -90,7 +92,7 @@ func marshalPlannedOutputs(changes *plans.Changes) (map[string]output, error) { } -func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (module, error) { +func marshalPlannedValues(changes *plans.ChangesSrc, schemas *terraform.Schemas) (module, error) { var ret module // build two maps: @@ -163,7 +165,7 @@ func marshalPlannedValues(changes *plans.Changes, schemas *terraform.Schemas) (m } // marshalPlanResources -func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) { +func marshalPlanResources(changes *plans.ChangesSrc, ris []addrs.AbsResourceInstance, schemas *terraform.Schemas) ([]resource, error) { var ret []resource for _, ri := range ris { @@ -192,16 +194,16 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc ) } - schema, schemaVer := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( r.ProviderAddr.Provider, r.Addr.Resource.Resource.Mode, resource.Type, ) - if schema == nil { + if schema.Body == nil { return nil, fmt.Errorf("no schema found for %s", r.Addr.String()) } - resource.SchemaVersion = schemaVer - changeV, err := r.Decode(schema.ImpliedType()) + resource.SchemaVersion = uint64(schema.Version) + changeV, err := r.Decode(schema) if err != nil { return nil, err } @@ -217,10 +219,10 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc if changeV.After != cty.NilVal { if changeV.After.IsWhollyKnown() { - resource.AttributeValues = marshalAttributeValues(changeV.After, schema) + resource.AttributeValues = marshalAttributeValues(changeV.After) } else { knowns := omitUnknowns(changeV.After) - resource.AttributeValues = marshalAttributeValues(knowns, schema) + resource.AttributeValues = marshalAttributeValues(knowns) } } @@ -231,6 +233,12 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc } resource.SensitiveValues = v + if schema.Identity != nil && !changeV.AfterIdentity.IsNull() { + identityVersion := uint64(schema.IdentityVersion) + resource.IdentitySchemaVersion = &identityVersion + resource.IdentityValues = marshalAttributeValues(changeV.AfterIdentity) + } + ret = append(ret, resource) } @@ -244,7 +252,7 @@ func marshalPlanResources(changes *plans.Changes, ris []addrs.AbsResourceInstanc // marshalPlanModules iterates over a list of modules to recursively describe // the full module tree. func marshalPlanModules( - changes *plans.Changes, + changes *plans.ChangesSrc, schemas *terraform.Schemas, childModules []addrs.ModuleInstance, moduleMap map[string][]addrs.ModuleInstance, diff --git a/internal/command/jsonplan/values_test.go b/internal/command/jsonplan/values_test.go index 30b22429ae..9be7fe2fe5 100644 --- a/internal/command/jsonplan/values_test.go +++ b/internal/command/jsonplan/values_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonplan import ( @@ -5,13 +8,19 @@ import ( "reflect" "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terraform" - "github.com/zclconf/go-cty/cty" ) +func ptrOf[T any](v T) *T { + return &v +} + func TestMarshalAttributeValues(t *testing.T) { tests := []struct { Attr cty.Value @@ -100,7 +109,7 @@ func TestMarshalAttributeValues(t *testing.T) { } for _, test := range tests { - got := marshalAttributeValues(test.Attr, test.Schema) + got := marshalAttributeValues(test.Attr) eq := reflect.DeepEqual(got, test.Want) if !eq { t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want) @@ -112,17 +121,17 @@ func TestMarshalPlannedOutputs(t *testing.T) { after, _ := plans.NewDynamicValue(cty.StringVal("after"), cty.DynamicPseudoType) tests := []struct { - Changes *plans.Changes + Changes *plans.ChangesSrc Want map[string]output Err bool }{ { - &plans.Changes{}, + &plans.ChangesSrc{}, nil, false, }, { - &plans.Changes{ + &plans.ChangesSrc{ Outputs: []*plans.OutputChangeSrc{ { Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), @@ -144,7 +153,7 @@ func TestMarshalPlannedOutputs(t *testing.T) { false, }, { // Delete action - &plans.Changes{ + &plans.ChangesSrc{ Outputs: []*plans.OutputChangeSrc{ { Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), @@ -180,11 +189,13 @@ func TestMarshalPlannedOutputs(t *testing.T) { func TestMarshalPlanResources(t *testing.T) { tests := map[string]struct { - Action plans.Action - Before cty.Value - After cty.Value - Want []resource - Err bool + Action plans.Action + Before cty.Value + After cty.Value + Want []resource + Err bool + BeforeIdentity cty.Value + AfterIdentity cty.Value }{ "create with unknowns": { Action: plans.Create, @@ -252,6 +263,37 @@ func TestMarshalPlanResources(t *testing.T) { }}, Err: false, }, + "with identity": { + Action: plans.Create, + Before: cty.NullVal(cty.EmptyObject), + After: cty.ObjectVal(map[string]cty.Value{ + "woozles": cty.StringVal("woo"), + "foozles": cty.NullVal(cty.String), + }), + BeforeIdentity: cty.NullVal(cty.EmptyObject), + AfterIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("someId"), + }), + Want: []resource{{ + Address: "test_thing.example", + Mode: "managed", + Type: "test_thing", + Name: "example", + Index: addrs.InstanceKey(nil), + ProviderName: "registry.terraform.io/hashicorp/test", + SchemaVersion: 1, + AttributeValues: attributeValues{ + "woozles": json.RawMessage(`"woo"`), + "foozles": json.RawMessage(`null`), + }, + SensitiveValues: json.RawMessage("{}"), + IdentitySchemaVersion: ptrOf[uint64](2), + IdentityValues: attributeValues{ + "id": json.RawMessage(`"someId"`), + }, + }}, + Err: false, + }, } for name, test := range tests { @@ -265,7 +307,24 @@ func TestMarshalPlanResources(t *testing.T) { if err != nil { t.Fatal(err) } - testChange := &plans.Changes{ + + var beforeIdentity, afterIdentity plans.DynamicValue + if !test.BeforeIdentity.IsNull() { + var err error + beforeIdentity, err = plans.NewDynamicValue(test.BeforeIdentity, test.BeforeIdentity.Type()) + if err != nil { + t.Fatal(err) + } + } + if !test.AfterIdentity.IsNull() { + var err error + afterIdentity, err = plans.NewDynamicValue(test.AfterIdentity, test.AfterIdentity.Type()) + if err != nil { + t.Fatal(err) + } + } + + testChange := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ @@ -278,9 +337,11 @@ func TestMarshalPlanResources(t *testing.T) { Module: addrs.RootModule, }, ChangeSrc: plans.ChangeSrc{ - Action: test.Action, - Before: before, - After: after, + Action: test.Action, + Before: before, + After: after, + BeforeIdentity: beforeIdentity, + AfterIdentity: afterIdentity, }, }, }, @@ -311,7 +372,7 @@ func TestMarshalPlanValuesNoopDeposed(t *testing.T) { if err != nil { t.Fatal(err) } - testChange := &plans.Changes{ + testChange := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ @@ -341,19 +402,26 @@ func TestMarshalPlanValuesNoopDeposed(t *testing.T) { func testSchemas() *terraform.Schemas { return &terraform.Schemas{ - Providers: map[addrs.Provider]*terraform.ProviderSchema{ - addrs.NewDefaultProvider("test"): &terraform.ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.NewDefaultProvider("test"): providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_thing": { - Attributes: map[string]*configschema.Attribute{ - "woozles": {Type: cty.String, Optional: true, Computed: true}, - "foozles": {Type: cty.String, Optional: true}, + Version: 1, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "woozles": {Type: cty.String, Optional: true, Computed: true}, + "foozles": {Type: cty.String, Optional: true}, + }, + }, + IdentityVersion: 2, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + }, + Nesting: configschema.NestingSingle, }, }, }, - ResourceTypeSchemaVersions: map[string]uint64{ - "test_thing": 1, - }, }, }, } diff --git a/internal/command/jsonprovider/attribute.go b/internal/command/jsonprovider/attribute.go index 9425cd9e58..a242bffb81 100644 --- a/internal/command/jsonprovider/attribute.go +++ b/internal/command/jsonprovider/attribute.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( @@ -7,9 +10,9 @@ import ( "github.com/zclconf/go-cty/cty" ) -type attribute struct { +type Attribute struct { AttributeType json.RawMessage `json:"type,omitempty"` - AttributeNestedType *nestedType `json:"nested_type,omitempty"` + AttributeNestedType *NestedType `json:"nested_type,omitempty"` Description string `json:"description,omitempty"` DescriptionKind string `json:"description_kind,omitempty"` Deprecated bool `json:"deprecated,omitempty"` @@ -17,10 +20,11 @@ type attribute struct { Optional bool `json:"optional,omitempty"` Computed bool `json:"computed,omitempty"` Sensitive bool `json:"sensitive,omitempty"` + WriteOnly bool `json:"write_only,omitempty"` } -type nestedType struct { - Attributes map[string]*attribute `json:"attributes,omitempty"` +type NestedType struct { + Attributes map[string]*Attribute `json:"attributes,omitempty"` NestingMode string `json:"nesting_mode,omitempty"` } @@ -33,8 +37,8 @@ func marshalStringKind(sk configschema.StringKind) string { } } -func marshalAttribute(attr *configschema.Attribute) *attribute { - ret := &attribute{ +func marshalAttribute(attr *configschema.Attribute) *Attribute { + ret := &Attribute{ Description: attr.Description, DescriptionKind: marshalStringKind(attr.DescriptionKind), Required: attr.Required, @@ -42,6 +46,7 @@ func marshalAttribute(attr *configschema.Attribute) *attribute { Computed: attr.Computed, Sensitive: attr.Sensitive, Deprecated: attr.Deprecated, + WriteOnly: attr.WriteOnly, } // we're not concerned about errors because at this point the schema has @@ -52,10 +57,10 @@ func marshalAttribute(attr *configschema.Attribute) *attribute { } if attr.NestedType != nil { - nestedTy := nestedType{ + nestedTy := NestedType{ NestingMode: nestingModeString(attr.NestedType.Nesting), } - attrs := make(map[string]*attribute, len(attr.NestedType.Attributes)) + attrs := make(map[string]*Attribute, len(attr.NestedType.Attributes)) for k, attr := range attr.NestedType.Attributes { attrs[k] = marshalAttribute(attr) } @@ -65,3 +70,27 @@ func marshalAttribute(attr *configschema.Attribute) *attribute { return ret } + +type IdentityAttribute struct { + IdentityType json.RawMessage `json:"type,omitempty"` + Description string `json:"description,omitempty"` + RequiredForImport bool `json:"required_for_import,omitempty"` + OptionalForImport bool `json:"optional_for_import,omitempty"` +} + +func marshalIdentityAttribute(attr *configschema.Attribute) *IdentityAttribute { + ret := &IdentityAttribute{ + Description: attr.Description, + RequiredForImport: attr.Required, + OptionalForImport: attr.Optional, + } + + // we're not concerned about errors because at this point the schema has + // already been checked and re-checked. + if attr.Type != cty.NilType { + attrTy, _ := attr.Type.MarshalJSON() + ret.IdentityType = attrTy + } + + return ret +} diff --git a/internal/command/jsonprovider/attribute_test.go b/internal/command/jsonprovider/attribute_test.go index a2502eadfc..e3a747f3db 100644 --- a/internal/command/jsonprovider/attribute_test.go +++ b/internal/command/jsonprovider/attribute_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( @@ -13,11 +16,11 @@ import ( func TestMarshalAttribute(t *testing.T) { tests := []struct { Input *configschema.Attribute - Want *attribute + Want *Attribute }{ { &configschema.Attribute{Type: cty.String, Optional: true, Computed: true}, - &attribute{ + &Attribute{ AttributeType: json.RawMessage(`"string"`), Optional: true, Computed: true, @@ -25,11 +28,12 @@ func TestMarshalAttribute(t *testing.T) { }, }, { // collection types look a little odd. - &configschema.Attribute{Type: cty.Map(cty.String), Optional: true, Computed: true}, - &attribute{ + &configschema.Attribute{Type: cty.Map(cty.String), Optional: true, Computed: true, WriteOnly: true}, + &Attribute{ AttributeType: json.RawMessage(`["map","string"]`), Optional: true, Computed: true, + WriteOnly: true, DescriptionKind: "plain", }, }, @@ -42,3 +46,33 @@ func TestMarshalAttribute(t *testing.T) { } } } + +func TestMarshalIdentityAttribute(t *testing.T) { + tests := []struct { + Input *configschema.Attribute + Want *IdentityAttribute + }{ + { + &configschema.Attribute{Type: cty.String, Optional: true}, + &IdentityAttribute{ + IdentityType: json.RawMessage(`"string"`), + OptionalForImport: true, + }, + }, + { // collection types look a little odd. + &configschema.Attribute{Type: cty.List(cty.String), Required: true}, + &IdentityAttribute{ + IdentityType: json.RawMessage(`["list","string"]`), + RequiredForImport: true, + }, + }, + } + + for _, test := range tests { + got := marshalIdentityAttribute(test.Input) + if !cmp.Equal(got, test.Want) { + t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, test.Want)) + } + } + +} diff --git a/internal/command/jsonprovider/block.go b/internal/command/jsonprovider/block.go index ebb145a4e6..a583e3fc33 100644 --- a/internal/command/jsonprovider/block.go +++ b/internal/command/jsonprovider/block.go @@ -1,29 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( "github.com/hashicorp/terraform/internal/configs/configschema" ) -type block struct { - Attributes map[string]*attribute `json:"attributes,omitempty"` - BlockTypes map[string]*blockType `json:"block_types,omitempty"` +type Block struct { + Attributes map[string]*Attribute `json:"attributes,omitempty"` + BlockTypes map[string]*BlockType `json:"block_types,omitempty"` Description string `json:"description,omitempty"` DescriptionKind string `json:"description_kind,omitempty"` Deprecated bool `json:"deprecated,omitempty"` } -type blockType struct { +type BlockType struct { NestingMode string `json:"nesting_mode,omitempty"` - Block *block `json:"block,omitempty"` + Block *Block `json:"block,omitempty"` MinItems uint64 `json:"min_items,omitempty"` MaxItems uint64 `json:"max_items,omitempty"` } -func marshalBlockTypes(nestedBlock *configschema.NestedBlock) *blockType { +func marshalBlockTypes(nestedBlock *configschema.NestedBlock) *BlockType { if nestedBlock == nil { - return &blockType{} + return &BlockType{} } - ret := &blockType{ + ret := &BlockType{ Block: marshalBlock(&nestedBlock.Block), MinItems: uint64(nestedBlock.MinItems), MaxItems: uint64(nestedBlock.MaxItems), @@ -32,19 +35,19 @@ func marshalBlockTypes(nestedBlock *configschema.NestedBlock) *blockType { return ret } -func marshalBlock(configBlock *configschema.Block) *block { +func marshalBlock(configBlock *configschema.Block) *Block { if configBlock == nil { - return &block{} + return &Block{} } - ret := block{ + ret := Block{ Deprecated: configBlock.Deprecated, Description: configBlock.Description, DescriptionKind: marshalStringKind(configBlock.DescriptionKind), } if len(configBlock.Attributes) > 0 { - attrs := make(map[string]*attribute, len(configBlock.Attributes)) + attrs := make(map[string]*Attribute, len(configBlock.Attributes)) for k, attr := range configBlock.Attributes { attrs[k] = marshalAttribute(attr) } @@ -52,7 +55,7 @@ func marshalBlock(configBlock *configschema.Block) *block { } if len(configBlock.BlockTypes) > 0 { - blockTypes := make(map[string]*blockType, len(configBlock.BlockTypes)) + blockTypes := make(map[string]*BlockType, len(configBlock.BlockTypes)) for k, bt := range configBlock.BlockTypes { blockTypes[k] = marshalBlockTypes(bt) } diff --git a/internal/command/jsonprovider/block_test.go b/internal/command/jsonprovider/block_test.go index 93197fb84b..0dc46cd9a7 100644 --- a/internal/command/jsonprovider/block_test.go +++ b/internal/command/jsonprovider/block_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( @@ -13,11 +16,11 @@ import ( func TestMarshalBlock(t *testing.T) { tests := []struct { Input *configschema.Block - Want *block + Want *Block }{ { nil, - &block{}, + &Block{}, }, { Input: &configschema.Block{ @@ -37,16 +40,16 @@ func TestMarshalBlock(t *testing.T) { }, }, }, - Want: &block{ - Attributes: map[string]*attribute{ + Want: &Block{ + Attributes: map[string]*Attribute{ "ami": {AttributeType: json.RawMessage(`"string"`), Optional: true, DescriptionKind: "plain"}, "id": {AttributeType: json.RawMessage(`"string"`), Optional: true, Computed: true, DescriptionKind: "plain"}, }, - BlockTypes: map[string]*blockType{ + BlockTypes: map[string]*BlockType{ "network_interface": { NestingMode: "list", - Block: &block{ - Attributes: map[string]*attribute{ + Block: &Block{ + Attributes: map[string]*Attribute{ "description": {AttributeType: json.RawMessage(`"string"`), Optional: true, DescriptionKind: "plain"}, "device_index": {AttributeType: json.RawMessage(`"string"`), Optional: true, DescriptionKind: "plain"}, }, diff --git a/internal/command/jsonprovider/doc.go b/internal/command/jsonprovider/doc.go index f7be0ade92..a63c5a3962 100644 --- a/internal/command/jsonprovider/doc.go +++ b/internal/command/jsonprovider/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package jsonprovider contains types and functions to marshal terraform // provider schemas into a json formatted output. package jsonprovider diff --git a/internal/command/jsonprovider/provider.go b/internal/command/jsonprovider/provider.go index 4487db4987..e87c022063 100644 --- a/internal/command/jsonprovider/provider.go +++ b/internal/command/jsonprovider/provider.go @@ -1,8 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( "encoding/json" + "github.com/hashicorp/terraform/internal/command/jsonfunction" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terraform" ) @@ -11,60 +16,55 @@ import ( // consuming parser. const FormatVersion = "1.0" -// providers is the top-level object returned when exporting provider schemas -type providers struct { +// Providers is the top-level object returned when exporting provider schemas +type Providers struct { FormatVersion string `json:"format_version"` Schemas map[string]*Provider `json:"provider_schemas,omitempty"` } type Provider struct { - Provider *schema `json:"provider,omitempty"` - ResourceSchemas map[string]*schema `json:"resource_schemas,omitempty"` - DataSourceSchemas map[string]*schema `json:"data_source_schemas,omitempty"` + Provider *Schema `json:"provider,omitempty"` + ResourceSchemas map[string]*Schema `json:"resource_schemas,omitempty"` + DataSourceSchemas map[string]*Schema `json:"data_source_schemas,omitempty"` + EphemeralResourceSchemas map[string]*Schema `json:"ephemeral_resource_schemas,omitempty"` + Functions map[string]*jsonfunction.FunctionSignature `json:"functions,omitempty"` + ResourceIdentitySchemas map[string]*IdentitySchema `json:"resource_identity_schemas,omitempty"` } -func newProviders() *providers { +func newProviders() *Providers { schemas := make(map[string]*Provider) - return &providers{ + return &Providers{ FormatVersion: FormatVersion, Schemas: schemas, } } +// MarshalForRenderer converts the provided internation representation of the +// schema into the public structured JSON versions. +// +// This is a format that can be read by the structured plan renderer. +func MarshalForRenderer(s *terraform.Schemas) map[string]*Provider { + schemas := make(map[string]*Provider, len(s.Providers)) + for k, v := range s.Providers { + schemas[k.String()] = marshalProvider(v) + } + return schemas +} + func Marshal(s *terraform.Schemas) ([]byte, error) { providers := newProviders() - - for k, v := range s.Providers { - providers.Schemas[k.String()] = marshalProvider(v) - } - + providers.Schemas = MarshalForRenderer(s) ret, err := json.Marshal(providers) return ret, err } -func marshalProvider(tps *terraform.ProviderSchema) *Provider { - if tps == nil { - return &Provider{} - } - - var ps *schema - var rs, ds map[string]*schema - - if tps.Provider != nil { - ps = marshalSchema(tps.Provider) - } - - if tps.ResourceTypes != nil { - rs = marshalSchemas(tps.ResourceTypes, tps.ResourceTypeSchemaVersions) - } - - if tps.DataSources != nil { - ds = marshalSchemas(tps.DataSources, tps.ResourceTypeSchemaVersions) - } - +func marshalProvider(tps providers.ProviderSchema) *Provider { return &Provider{ - Provider: ps, - ResourceSchemas: rs, - DataSourceSchemas: ds, + Provider: marshalSchema(tps.Provider), + ResourceSchemas: marshalSchemas(tps.ResourceTypes), + DataSourceSchemas: marshalSchemas(tps.DataSources), + EphemeralResourceSchemas: marshalSchemas(tps.EphemeralResourceTypes), + Functions: jsonfunction.MarshalProviderFunctions(tps.Functions), + ResourceIdentitySchemas: marshalIdentitySchemas(tps.ResourceTypes), } } diff --git a/internal/command/jsonprovider/provider_test.go b/internal/command/jsonprovider/provider_test.go index 32e8ebce0f..162370c537 100644 --- a/internal/command/jsonprovider/provider_test.go +++ b/internal/command/jsonprovider/provider_test.go @@ -1,31 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( "encoding/json" + "fmt" "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/providers" ) +var cmpOpts = cmpopts.IgnoreUnexported(Provider{}) + func TestMarshalProvider(t *testing.T) { tests := []struct { - Input *terraform.ProviderSchema + Input providers.ProviderSchema Want *Provider }{ { - nil, - &Provider{}, + providers.ProviderSchema{}, + &Provider{ + Provider: &Schema{}, + ResourceSchemas: map[string]*Schema{}, + DataSourceSchemas: map[string]*Schema{}, + EphemeralResourceSchemas: map[string]*Schema{}, + ResourceIdentitySchemas: map[string]*IdentitySchema{}, + }, }, { testProvider(), &Provider{ - Provider: &schema{ - Block: &block{ - Attributes: map[string]*attribute{ + Provider: &Schema{ + Block: &Block{ + Attributes: map[string]*Attribute{ "region": { AttributeType: json.RawMessage(`"string"`), Required: true, @@ -35,11 +48,11 @@ func TestMarshalProvider(t *testing.T) { DescriptionKind: "plain", }, }, - ResourceSchemas: map[string]*schema{ + ResourceSchemas: map[string]*Schema{ "test_instance": { Version: 42, - Block: &block{ - Attributes: map[string]*attribute{ + Block: &Block{ + Attributes: map[string]*Attribute{ "id": { AttributeType: json.RawMessage(`"string"`), Optional: true, @@ -52,9 +65,9 @@ func TestMarshalProvider(t *testing.T) { DescriptionKind: "plain", }, "volumes": { - AttributeNestedType: &nestedType{ + AttributeNestedType: &NestedType{ NestingMode: "list", - Attributes: map[string]*attribute{ + Attributes: map[string]*Attribute{ "size": { AttributeType: json.RawMessage(`"string"`), Required: true, @@ -71,10 +84,10 @@ func TestMarshalProvider(t *testing.T) { DescriptionKind: "plain", }, }, - BlockTypes: map[string]*blockType{ + BlockTypes: map[string]*BlockType{ "network_interface": { - Block: &block{ - Attributes: map[string]*attribute{ + Block: &Block{ + Attributes: map[string]*Attribute{ "device_index": { AttributeType: json.RawMessage(`"string"`), Optional: true, @@ -95,11 +108,11 @@ func TestMarshalProvider(t *testing.T) { }, }, }, - DataSourceSchemas: map[string]*schema{ + DataSourceSchemas: map[string]*Schema{ "test_data_source": { Version: 3, - Block: &block{ - Attributes: map[string]*attribute{ + Block: &Block{ + Attributes: map[string]*Attribute{ "id": { AttributeType: json.RawMessage(`"string"`), Optional: true, @@ -112,10 +125,10 @@ func TestMarshalProvider(t *testing.T) { DescriptionKind: "plain", }, }, - BlockTypes: map[string]*blockType{ + BlockTypes: map[string]*BlockType{ "network_interface": { - Block: &block{ - Attributes: map[string]*attribute{ + Block: &Block{ + Attributes: map[string]*Attribute{ "device_index": { AttributeType: json.RawMessage(`"string"`), Optional: true, @@ -136,77 +149,173 @@ func TestMarshalProvider(t *testing.T) { }, }, }, + EphemeralResourceSchemas: map[string]*Schema{ + "test_eph_instance": { + Block: &Block{ + Attributes: map[string]*Attribute{ + "id": { + AttributeType: json.RawMessage(`"string"`), + Optional: true, + Computed: true, + DescriptionKind: "plain", + }, + "ami": { + AttributeType: json.RawMessage(`"string"`), + Optional: true, + DescriptionKind: "plain", + }, + "volumes": { + AttributeNestedType: &NestedType{ + NestingMode: "list", + Attributes: map[string]*Attribute{ + "size": { + AttributeType: json.RawMessage(`"string"`), + Required: true, + DescriptionKind: "plain", + }, + "mount_point": { + AttributeType: json.RawMessage(`"string"`), + Required: true, + DescriptionKind: "plain", + }, + }, + }, + Optional: true, + DescriptionKind: "plain", + }, + }, + BlockTypes: map[string]*BlockType{ + "network_interface": { + Block: &Block{ + Attributes: map[string]*Attribute{ + "device_index": { + AttributeType: json.RawMessage(`"string"`), + Optional: true, + DescriptionKind: "plain", + }, + "description": { + AttributeType: json.RawMessage(`"string"`), + Optional: true, + DescriptionKind: "plain", + }, + }, + DescriptionKind: "plain", + }, + NestingMode: "list", + }, + }, + DescriptionKind: "plain", + }, + }, + }, + ResourceIdentitySchemas: map[string]*IdentitySchema{}, }, }, } - for _, test := range tests { - got := marshalProvider(test.Input) - if !cmp.Equal(got, test.Want) { - t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, test.Want)) - } + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + got := marshalProvider(test.Input) + if diff := cmp.Diff(test.Want, got, cmpOpts); diff != "" { + t.Fatalf("wrong result:\n %s\n", diff) + } + }) } } -func testProvider() *terraform.ProviderSchema { - return &terraform.ProviderSchema{ - Provider: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "region": {Type: cty.String, Required: true}, +func testProvider() providers.ProviderSchema { + return providers.ProviderSchema{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": {Type: cty.String, Required: true}, + }, }, }, - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - "volumes": { - Optional: true, - NestedType: &configschema.Object{ + Version: 42, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "volumes": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "size": {Type: cty.String, Required: true}, + "mount_point": {Type: cty.String, Required: true}, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "network_interface": { Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "size": {Type: cty.String, Required: true}, - "mount_point": {Type: cty.String, Required: true}, - }, - }, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "network_interface": { - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "device_index": {Type: cty.String, Optional: true}, - "description": {Type: cty.String, Optional: true}, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "device_index": {Type: cty.String, Optional: true}, + "description": {Type: cty.String, Optional: true}, + }, }, }, }, }, }, }, - DataSources: map[string]*configschema.Block{ + DataSources: map[string]providers.Schema{ "test_data_source": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "ami": {Type: cty.String, Optional: true}, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "network_interface": { - Nesting: configschema.NestingList, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "device_index": {Type: cty.String, Optional: true}, - "description": {Type: cty.String, Optional: true}, + Version: 3, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "network_interface": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "device_index": {Type: cty.String, Optional: true}, + "description": {Type: cty.String, Optional: true}, + }, }, }, }, }, }, }, - - ResourceTypeSchemaVersions: map[string]uint64{ - "test_instance": 42, - "test_data_source": 3, + EphemeralResourceTypes: map[string]providers.Schema{ + "test_eph_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "ami": {Type: cty.String, Optional: true}, + "volumes": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "size": {Type: cty.String, Required: true}, + "mount_point": {Type: cty.String, Required: true}, + }, + }, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "network_interface": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "device_index": {Type: cty.String, Optional: true}, + "description": {Type: cty.String, Optional: true}, + }, + }, + }, + }, + }, + }, }, } } diff --git a/internal/command/jsonprovider/schema.go b/internal/command/jsonprovider/schema.go index 5a1465b8a7..22c516aab0 100644 --- a/internal/command/jsonprovider/schema.go +++ b/internal/command/jsonprovider/schema.go @@ -1,38 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" ) -type schema struct { +type Schema struct { Version uint64 `json:"version"` - Block *block `json:"block,omitempty"` + Block *Block `json:"block,omitempty"` } // marshalSchema is a convenience wrapper around mashalBlock. Schema version // should be set by the caller. -func marshalSchema(block *configschema.Block) *schema { - if block == nil { - return &schema{} +func marshalSchema(schema providers.Schema) *Schema { + if schema.Body == nil { + return &Schema{} } - var ret schema - ret.Block = marshalBlock(block) + var ret Schema + ret.Block = marshalBlock(schema.Body) + ret.Version = uint64(schema.Version) return &ret } -func marshalSchemas(blocks map[string]*configschema.Block, rVersions map[string]uint64) map[string]*schema { - if blocks == nil { - return map[string]*schema{} +func marshalSchemas(schemas map[string]providers.Schema) map[string]*Schema { + if schemas == nil { + return map[string]*Schema{} } - ret := make(map[string]*schema, len(blocks)) - for k, v := range blocks { + ret := make(map[string]*Schema, len(schemas)) + for k, v := range schemas { ret[k] = marshalSchema(v) - version, ok := rVersions[k] - if ok { - ret[k].Version = version - } } return ret } + +type IdentitySchema struct { + Version uint64 `json:"version"` + Attributes map[string]*IdentityAttribute `json:"attributes,omitempty"` +} + +func marshalIdentitySchema(schema providers.Schema) *IdentitySchema { + var ret IdentitySchema + ret.Version = uint64(schema.IdentityVersion) + ret.Attributes = make(map[string]*IdentityAttribute, len(schema.Identity.Attributes)) + + for k, v := range schema.Identity.Attributes { + ret.Attributes[k] = marshalIdentityAttribute(v) + } + + return &ret +} + +func marshalIdentitySchemas(schemas map[string]providers.Schema) map[string]*IdentitySchema { + if schemas == nil { + return map[string]*IdentitySchema{} + } + + ret := make(map[string]*IdentitySchema, len(schemas)) + for k, v := range schemas { + if v.Identity != nil { + ret[k] = marshalIdentitySchema(v) + } + } + + return ret +} diff --git a/internal/command/jsonprovider/schema_test.go b/internal/command/jsonprovider/schema_test.go index 737a8d74f8..fc83b7e93a 100644 --- a/internal/command/jsonprovider/schema_test.go +++ b/internal/command/jsonprovider/schema_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonprovider import ( @@ -5,24 +8,22 @@ import ( "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" ) func TestMarshalSchemas(t *testing.T) { tests := []struct { - Input map[string]*configschema.Block - Versions map[string]uint64 - Want map[string]*schema + Input map[string]providers.Schema + Want map[string]*Schema }{ { nil, - map[string]uint64{}, - map[string]*schema{}, + map[string]*Schema{}, }, } for _, test := range tests { - got := marshalSchemas(test.Input, test.Versions) + got := marshalSchemas(test.Input) if !cmp.Equal(got, test.Want) { t.Fatalf("wrong result:\n %v\n", cmp.Diff(got, test.Want)) } @@ -31,12 +32,12 @@ func TestMarshalSchemas(t *testing.T) { func TestMarshalSchema(t *testing.T) { tests := map[string]struct { - Input *configschema.Block - Want *schema + Input providers.Schema + Want *Schema }{ "nil_block": { - nil, - &schema{}, + providers.Schema{}, + &Schema{}, }, } diff --git a/internal/command/jsonstate/doc.go b/internal/command/jsonstate/doc.go index 54773d09c3..ba9558e9e5 100644 --- a/internal/command/jsonstate/doc.go +++ b/internal/command/jsonstate/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package jsonstate implements methods for outputting a state in a // machine-readable json format package jsonstate diff --git a/internal/command/jsonstate/state.go b/internal/command/jsonstate/state.go index bdecc06940..52e93dfa58 100644 --- a/internal/command/jsonstate/state.go +++ b/internal/command/jsonstate/state.go @@ -1,8 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonstate import ( "encoding/json" "fmt" + "maps" + "slices" "sort" "github.com/zclconf/go-cty/cty" @@ -14,12 +19,18 @@ import ( "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" ) -// FormatVersion represents the version of the json format and will be -// incremented for any change to this format that requires changes to a -// consuming parser. -const FormatVersion = "1.0" +const ( + // FormatVersion represents the version of the json format and will be + // incremented for any change to this format that requires changes to a + // consuming parser. + FormatVersion = "1.0" + + ManagedResourceMode = "managed" + DataResourceMode = "data" +) // state is the top-level representation of the json format of a terraform // state. @@ -33,33 +44,33 @@ type state struct { // stateValues is the common representation of resolved values for both the prior // state (which is always complete) and the planned new state. type stateValues struct { - Outputs map[string]output `json:"outputs,omitempty"` - RootModule module `json:"root_module,omitempty"` + Outputs map[string]Output `json:"outputs,omitempty"` + RootModule Module `json:"root_module,omitempty"` } -type output struct { +type Output struct { Sensitive bool `json:"sensitive"` Value json.RawMessage `json:"value,omitempty"` Type json.RawMessage `json:"type,omitempty"` } -// module is the representation of a module in state. This can be the root module +// Module is the representation of a module in state. This can be the root module // or a child module -type module struct { +type Module struct { // Resources are sorted in a user-friendly order that is undefined at this // time, but consistent. - Resources []resource `json:"resources,omitempty"` + Resources []Resource `json:"resources,omitempty"` // Address is the absolute module address, omitted for the root module Address string `json:"address,omitempty"` // Each module object can optionally have its own nested "child_modules", // recursively describing the full module tree. - ChildModules []module `json:"child_modules,omitempty"` + ChildModules []Module `json:"child_modules,omitempty"` } // Resource is the representation of a resource in the state. -type resource struct { +type Resource struct { // Address is the absolute resource address Address string `json:"address,omitempty"` @@ -70,7 +81,7 @@ type resource struct { Name string `json:"name,omitempty"` // Index is omitted for a resource not using `count` or `for_each`. - Index addrs.InstanceKey `json:"index,omitempty"` + Index json.RawMessage `json:"index,omitempty"` // ProviderName allows the property "type" to be interpreted unambiguously // in the unusual situation where a provider offers a resource type whose @@ -86,7 +97,7 @@ type resource struct { // resource, whose structure depends on the resource type schema. Any // unknown values are omitted or set to null, making them indistinguishable // from absent values. - AttributeValues attributeValues `json:"values,omitempty"` + AttributeValues AttributeValues `json:"values,omitempty"` // SensitiveValues is similar to AttributeValues, but with all sensitive // values replaced with true, and all non-sensitive leaf values omitted. @@ -101,21 +112,38 @@ type resource struct { // Deposed is set if the resource is deposed in terraform state. DeposedKey string `json:"deposed_key,omitempty"` + + // The version of the resource identity schema the "identity" property + // conforms to. + // It's a pointer, because it should be optional, but also 0 is a valid + // schema version. + IdentitySchemaVersion *uint64 `json:"identity_schema_version,omitempty"` + + // The JSON representation of the resource identity, whose structure + // depends on the resource identity schema. + IdentityValues IdentityValues `json:"identity,omitempty"` } -// attributeValues is the JSON representation of the attribute values of the +// AttributeValues is the JSON representation of the attribute values of the // resource, whose structure depends on the resource type schema. -type attributeValues map[string]interface{} +type AttributeValues map[string]json.RawMessage -func marshalAttributeValues(value cty.Value) attributeValues { +// IdentityValues is the JSON representation of the identity values of the +// resource, whose structure depends on the resource identity schema. +type IdentityValues map[string]json.RawMessage + +func marshalAttributeValues(value cty.Value) (unmarkedVal cty.Value, marshalledVals AttributeValues, sensitivePaths []cty.Path, err error) { // unmark our value to show all values - value, _ = value.UnmarkDeep() - - if value == cty.NilVal || value.IsNull() { - return nil + value, sensitivePaths, err = unmarkValueForMarshaling(value) + if err != nil { + return cty.NilVal, nil, nil, err } - ret := make(attributeValues) + if value == cty.NilVal || value.IsNull() { + return value, nil, nil, nil + } + + ret := make(AttributeValues) it := value.ElementIterator() for it.Next() { @@ -123,7 +151,22 @@ func marshalAttributeValues(value cty.Value) attributeValues { vJSON, _ := ctyjson.Marshal(v, v.Type()) ret[k.AsString()] = json.RawMessage(vJSON) } - return ret + return value, ret, sensitivePaths, nil +} + +func marshalIdentityValues(value cty.Value) (IdentityValues, error) { + if value == cty.NilVal || value.IsNull() { + return nil, nil + } + + ret := make(IdentityValues) + it := value.ElementIterator() + for it.Next() { + k, v := it.Element() + vJSON, _ := ctyjson.Marshal(v, v.Type()) + ret[k.AsString()] = json.RawMessage(vJSON) + } + return ret, nil } // newState() returns a minimally-initialized state @@ -133,6 +176,27 @@ func newState() *state { } } +// MarshalForRenderer returns the pre-json encoding changes of the state, in a +// format available to the structured renderer. +func MarshalForRenderer(sf *statefile.File, schemas *terraform.Schemas) (Module, map[string]Output, error) { + if sf.State.Modules == nil { + // Empty state case. + return Module{}, nil, nil + } + + outputs, err := MarshalOutputs(sf.State.RootOutputValues) + if err != nil { + return Module{}, nil, err + } + + root, err := marshalRootModule(sf.State, schemas) + if err != nil { + return Module{}, nil, err + } + + return root, outputs, err +} + // Marshal returns the json encoding of a terraform state. func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) { output := newState() @@ -145,13 +209,11 @@ func Marshal(sf *statefile.File, schemas *terraform.Schemas) ([]byte, error) { if sf.TerraformVersion != nil { output.TerraformVersion = sf.TerraformVersion.String() } - // output.StateValues err := output.marshalStateValues(sf.State, schemas) if err != nil { return nil, err } - // output.Checks if sf.State.CheckResults != nil && sf.State.CheckResults.ConfigResults.Len() > 0 { output.Checks = jsonchecks.MarshalCheckStates(sf.State.CheckResults) @@ -166,7 +228,7 @@ func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.S var err error // only marshal the root module outputs - sv.Outputs, err = MarshalOutputs(s.RootModule().OutputValues) + sv.Outputs, err = MarshalOutputs(s.RootOutputValues) if err != nil { return err } @@ -181,14 +243,14 @@ func (jsonstate *state) marshalStateValues(s *states.State, schemas *terraform.S return nil } -// MarshalOutputs translates a map of states.OutputValue to a map of jsonstate.output, +// MarshalOutputs translates a map of states.OutputValue to a map of jsonstate.Output, // which are defined for json encoding. -func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]output, error) { +func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]Output, error) { if outputs == nil { return nil, nil } - ret := make(map[string]output) + ret := make(map[string]Output) for k, v := range outputs { ty := v.Value.Type() ov, err := ctyjson.Marshal(v.Value, ty) @@ -199,7 +261,7 @@ func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]output, if err != nil { return ret, err } - ret[k] = output{ + ret[k] = Output{ Value: ov, Type: ot, Sensitive: v.Sensitive, @@ -209,8 +271,8 @@ func MarshalOutputs(outputs map[string]*states.OutputValue) (map[string]output, return ret, nil } -func marshalRootModule(s *states.State, schemas *terraform.Schemas) (module, error) { - var ret module +func marshalRootModule(s *states.State, schemas *terraform.Schemas) (Module, error) { + var ret Module var err error ret.Address = "" @@ -259,11 +321,11 @@ func marshalModules( schemas *terraform.Schemas, modules []addrs.ModuleInstance, moduleMap map[string][]addrs.ModuleInstance, -) ([]module, error) { - var ret []module +) ([]Module, error) { + var ret []Module for _, child := range modules { // cm for child module, naming things is hard. - cm := module{Address: child.String()} + cm := Module{Address: child.String()} // the module may be resourceless and contain only submodules, it will then be nil here stateMod := s.Module(child) @@ -294,27 +356,53 @@ func marshalModules( return ret, nil } -func marshalResources(resources map[string]*states.Resource, module addrs.ModuleInstance, schemas *terraform.Schemas) ([]resource, error) { - var ret []resource +func marshalResources(resources map[string]*states.Resource, module addrs.ModuleInstance, schemas *terraform.Schemas) ([]Resource, error) { + var ret []Resource + var sortedResources []*states.Resource for _, r := range resources { - for k, ri := range r.Instances { + sortedResources = append(sortedResources, r) + } + sort.Slice(sortedResources, func(i, j int) bool { + return sortedResources[i].Addr.Less(sortedResources[j].Addr) + }) + + for _, r := range sortedResources { + + var sortedKeys []addrs.InstanceKey + for k := range r.Instances { + sortedKeys = append(sortedKeys, k) + } + sort.Slice(sortedKeys, func(i, j int) bool { + return addrs.InstanceKeyLess(sortedKeys[i], sortedKeys[j]) + }) + + for _, k := range sortedKeys { + ri := r.Instances[k] + + var err error resAddr := r.Addr.Resource - current := resource{ + current := Resource{ Address: r.Addr.Instance(k).String(), - Index: k, Type: resAddr.Type, Name: resAddr.Name, ProviderName: r.ProviderConfig.Provider.String(), } + if k != nil { + index := k.Value() + if current.Index, err = ctyjson.Marshal(index, index.Type()); err != nil { + return nil, err + } + } + switch resAddr.Mode { case addrs.ManagedResourceMode: - current.Mode = "managed" + current.Mode = ManagedResourceMode case addrs.DataResourceMode: - current.Mode = "data" + current.Mode = DataResourceMode default: return ret, fmt.Errorf("resource %s has an unsupported mode %s", resAddr.String(), @@ -322,7 +410,7 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module ) } - schema, version := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( r.ProviderConfig.Provider, resAddr.Mode, resAddr.Type, @@ -330,29 +418,53 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module // It is possible that the only instance is deposed if ri.Current != nil { - if version != ri.Current.SchemaVersion { - return nil, fmt.Errorf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, resAddr, version) + if schema.Version != int64(ri.Current.SchemaVersion) { + return nil, fmt.Errorf("schema version %d for %s in state does not match version %d from the provider", ri.Current.SchemaVersion, resAddr, schema.Version) } current.SchemaVersion = ri.Current.SchemaVersion - if schema == nil { + if schema.Body == nil { return nil, fmt.Errorf("no schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider) } - riObj, err := ri.Current.Decode(schema.ImpliedType()) + + // Check if we have an identity in the state + if ri.Current.IdentityJSON != nil { + if schema.IdentityVersion != int64(ri.Current.IdentitySchemaVersion) { + return nil, fmt.Errorf("resource identity schema version %d for %s in state does not match version %d from the provider", ri.Current.IdentitySchemaVersion, resAddr, schema.IdentityVersion) + } + + if schema.Identity == nil { + return nil, fmt.Errorf("no resource identity schema found for %s (in provider %s)", resAddr.String(), r.ProviderConfig.Provider) + } + + current.IdentitySchemaVersion = &ri.Current.IdentitySchemaVersion + } + + riObj, err := ri.Current.Decode(schema) if err != nil { return nil, err } - current.AttributeValues = marshalAttributeValues(riObj.Value) - - s := SensitiveAsBool(riObj.Value) + var value cty.Value + var sensitivePaths []cty.Path + value, current.AttributeValues, sensitivePaths, err = marshalAttributeValues(riObj.Value) + if err != nil { + return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) + } + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(value, nil)...) + s := SensitiveAsBool(marks.MarkPaths(value, marks.Sensitive, sensitivePaths)) v, err := ctyjson.Marshal(s, s.Type()) if err != nil { return nil, err } current.SensitiveValues = v + current.IdentityValues, err = marshalIdentityValues(riObj.Identity) + if err != nil { + return nil, fmt.Errorf("preparing identity values for %s: %w", current.Address, err) + } + if len(riObj.Dependencies) > 0 { dependencies := make([]string, len(riObj.Dependencies)) for i, v := range riObj.Dependencies { @@ -367,9 +479,11 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module ret = append(ret, current) } - for deposedKey, rios := range ri.Deposed { + for _, deposedKey := range slices.Sorted(maps.Keys(ri.Deposed)) { + rios := ri.Deposed[states.DeposedKey(deposedKey)] + // copy the base fields from the current instance - deposed := resource{ + deposed := Resource{ Address: current.Address, Type: current.Type, Name: current.Name, @@ -378,20 +492,30 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module Index: current.Index, } - riObj, err := rios.Decode(schema.ImpliedType()) + riObj, err := rios.Decode(schema) if err != nil { return nil, err } - deposed.AttributeValues = marshalAttributeValues(riObj.Value) - - s := SensitiveAsBool(riObj.Value) + var value cty.Value + var sensitivePaths []cty.Path + value, deposed.AttributeValues, sensitivePaths, err = marshalAttributeValues(riObj.Value) + if err != nil { + return nil, fmt.Errorf("preparing attribute values for %s: %w", current.Address, err) + } + sensitivePaths = append(sensitivePaths, schema.Body.SensitivePaths(value, nil)...) + s := SensitiveAsBool(marks.MarkPaths(value, marks.Sensitive, sensitivePaths)) v, err := ctyjson.Marshal(s, s.Type()) if err != nil { return nil, err } deposed.SensitiveValues = v + deposed.IdentityValues, err = marshalIdentityValues(riObj.Identity) + if err != nil { + return nil, fmt.Errorf("preparing identity values for %s: %w", current.Address, err) + } + if len(riObj.Dependencies) > 0 { dependencies := make([]string, len(riObj.Dependencies)) for i, v := range riObj.Dependencies { @@ -403,16 +527,12 @@ func marshalResources(resources map[string]*states.Resource, module addrs.Module if riObj.Status == states.ObjectTainted { deposed.Tainted = true } - deposed.DeposedKey = deposedKey.String() + deposed.DeposedKey = string(deposedKey) ret = append(ret, deposed) } } } - sort.Slice(ret, func(i, j int) bool { - return ret[i].Address < ret[j].Address - }) - return ret, nil } @@ -487,3 +607,24 @@ func SensitiveAsBool(val cty.Value) cty.Value { panic(fmt.Sprintf("sensitiveAsBool cannot handle %#v", val)) } } + +// unmarkValueForMarshaling takes a value that possibly contains marked values +// and returns an equal value without markings along with the separated mark +// metadata that should be presented alongside the value in another JSON +// property. +// +// This function only accepts the marks that are valid to persist, and so will +// return an error if other marks are present. Marks that this package doesn't +// know how to store must be dealt with somehow by a caller -- presumably by +// replacing each marked value with some sort of storage placeholder. +func unmarkValueForMarshaling(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { + val, pvms := v.UnmarkDeepWithPaths() + sensitivePaths, otherMarks := marks.PathsWithMark(pvms, marks.Sensitive) + if len(otherMarks) != 0 { + return cty.NilVal, nil, fmt.Errorf( + "%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)", + tfdiags.FormatCtyPath(otherMarks[0].Path), otherMarks[0].Marks, + ) + } + return val, sensitivePaths, err +} diff --git a/internal/command/jsonstate/state_test.go b/internal/command/jsonstate/state_test.go index c0cd2e81c3..f94ba37fd6 100644 --- a/internal/command/jsonstate/state_test.go +++ b/internal/command/jsonstate/state_test.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package jsonstate import ( "encoding/json" + "fmt" "reflect" "testing" @@ -11,14 +15,19 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" ) +func ptrOf[T any](v T) *T { + return &v +} + func TestMarshalOutputs(t *testing.T) { tests := []struct { Outputs map[string]*states.OutputValue - Want map[string]output + Want map[string]Output Err bool }{ { @@ -33,7 +42,7 @@ func TestMarshalOutputs(t *testing.T) { Value: cty.StringVal("sekret"), }, }, - map[string]output{ + map[string]Output{ "test": { Sensitive: true, Value: json.RawMessage(`"sekret"`), @@ -49,7 +58,7 @@ func TestMarshalOutputs(t *testing.T) { Value: cty.StringVal("not_so_sekret"), }, }, - map[string]output{ + map[string]Output{ "test": { Sensitive: false, Value: json.RawMessage(`"not_so_sekret"`), @@ -76,7 +85,7 @@ func TestMarshalOutputs(t *testing.T) { }), }, }, - map[string]output{ + map[string]Output{ "mapstring": { Sensitive: false, Value: json.RawMessage(`{"beep":"boop"}`), @@ -110,28 +119,33 @@ func TestMarshalOutputs(t *testing.T) { func TestMarshalAttributeValues(t *testing.T) { tests := []struct { - Attr cty.Value - Want attributeValues + Attr cty.Value + Want AttributeValues + WantSensitivePaths []cty.Path }{ { cty.NilVal, nil, + nil, }, { cty.NullVal(cty.String), nil, + nil, }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), }), - attributeValues{"foo": json.RawMessage(`"bar"`)}, + AttributeValues{"foo": json.RawMessage(`"bar"`)}, + nil, }, { cty.ObjectVal(map[string]cty.Value{ "foo": cty.NullVal(cty.String), }), - attributeValues{"foo": json.RawMessage(`null`)}, + AttributeValues{"foo": json.RawMessage(`null`)}, + nil, }, { cty.ObjectVal(map[string]cty.Value{ @@ -143,12 +157,13 @@ func TestMarshalAttributeValues(t *testing.T) { cty.StringVal("moon"), }), }), - attributeValues{ + AttributeValues{ "bar": json.RawMessage(`{"hello":"world"}`), "baz": json.RawMessage(`["goodnight","moon"]`), }, + nil, }, - // Marked values + // Sensitive values { cty.ObjectVal(map[string]cty.Value{ "bar": cty.MapVal(map[string]cty.Value{ @@ -159,20 +174,47 @@ func TestMarshalAttributeValues(t *testing.T) { cty.StringVal("moon").Mark(marks.Sensitive), }), }), - attributeValues{ + AttributeValues{ "bar": json.RawMessage(`{"hello":"world"}`), "baz": json.RawMessage(`["goodnight","moon"]`), }, + []cty.Path{ + cty.GetAttrPath("baz").IndexInt(1), + }, }, } for _, test := range tests { - got := marshalAttributeValues(test.Attr) - eq := reflect.DeepEqual(got, test.Want) - if !eq { - t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want) - } + t.Run(fmt.Sprintf("%#v", test.Attr), func(t *testing.T) { + val, got, sensitivePaths, err := marshalAttributeValues(test.Attr) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v\n", got, test.Want) + } + if !reflect.DeepEqual(sensitivePaths, test.WantSensitivePaths) { + t.Errorf("wrong marks\ngot: %#v\nwant: %#v\n", sensitivePaths, test.WantSensitivePaths) + } + if _, marks := val.Unmark(); len(marks) != 0 { + t.Errorf("returned value still has marks; should have been unmarked\n%#v", marks) + } + }) } + + t.Run("reject unsupported marks", func(t *testing.T) { + _, _, _, err := marshalAttributeValues(cty.ObjectVal(map[string]cty.Value{ + "disallowed": cty.StringVal("a").Mark("unsupported"), + })) + if err == nil { + t.Fatalf("unexpected success; want error") + } + got := err.Error() + want := `.disallowed: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)` + if got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + }) } func TestMarshalResources(t *testing.T) { @@ -180,7 +222,7 @@ func TestMarshalResources(t *testing.T) { tests := map[string]struct { Resources map[string]*states.Resource Schemas *terraform.Schemas - Want []resource + Want []Resource Err bool }{ "nil": { @@ -214,19 +256,61 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.InstanceKey(nil), + Index: nil, ProviderName: "registry.terraform.io/hashicorp/test", - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, - SensitiveValues: json.RawMessage("{}"), + SensitiveValues: json.RawMessage("{\"foozles\":true}"), + }, + }, + false, + }, + "single resource_with_sensitive": { + map[string]*states.Resource{ + "test_thing.baz": { + Addr: addrs.AbsResource{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "bar", + }, + }, + Instances: map[addrs.InstanceKey]*states.ResourceInstance{ + addrs.NoKey: { + Current: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles"}`), + }, + }, + }, + ProviderConfig: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + }, + }, + testSchemas(), + []Resource{ + { + Address: "test_thing.bar", + Mode: "managed", + Type: "test_thing", + Name: "bar", + Index: nil, + ProviderName: "registry.terraform.io/hashicorp/test", + AttributeValues: AttributeValues{ + "foozles": json.RawMessage(`"sensuzles"`), + "woozles": json.RawMessage(`"confuzles"`), + }, + SensitiveValues: json.RawMessage("{\"foozles\":true}"), }, }, false, @@ -246,9 +330,8 @@ func TestMarshalResources(t *testing.T) { Current: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"foozles":"confuzles"}`), - AttrSensitivePaths: []cty.PathValueMarks{{ - Path: cty.Path{cty.GetAttrStep{Name: "foozles"}}, - Marks: cty.NewValueMarks(marks.Sensitive)}, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("foozles"), }, }, }, @@ -260,15 +343,15 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.InstanceKey(nil), + Index: nil, ProviderName: "registry.terraform.io/hashicorp/test", - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`"confuzles"`), "woozles": json.RawMessage(`null`), }, @@ -331,19 +414,19 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_thing.bar[0]", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.IntKey(0), + Index: json.RawMessage(`0`), ProviderName: "registry.terraform.io/hashicorp/test", - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, - SensitiveValues: json.RawMessage("{}"), + SensitiveValues: json.RawMessage("{\"foozles\":true}"), }, }, false, @@ -373,19 +456,19 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_thing.bar[\"rockhopper\"]", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.StringKey("rockhopper"), + Index: json.RawMessage(`"rockhopper"`), ProviderName: "registry.terraform.io/hashicorp/test", - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, - SensitiveValues: json.RawMessage("{}"), + SensitiveValues: json.RawMessage("{\"foozles\":true}"), }, }, false, @@ -417,20 +500,20 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.InstanceKey(nil), + Index: nil, ProviderName: "registry.terraform.io/hashicorp/test", DeposedKey: deposedKey.String(), - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, - SensitiveValues: json.RawMessage("{}"), + SensitiveValues: json.RawMessage("{\"foozles\":true}"), }, }, false, @@ -466,33 +549,33 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.InstanceKey(nil), + Index: nil, ProviderName: "registry.terraform.io/hashicorp/test", - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, - SensitiveValues: json.RawMessage("{}"), + SensitiveValues: json.RawMessage("{\"foozles\":true}"), }, { Address: "test_thing.bar", Mode: "managed", Type: "test_thing", Name: "bar", - Index: addrs.InstanceKey(nil), + Index: nil, ProviderName: "registry.terraform.io/hashicorp/test", DeposedKey: deposedKey.String(), - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "foozles": json.RawMessage(`null`), "woozles": json.RawMessage(`"confuzles"`), }, - SensitiveValues: json.RawMessage("{}"), + SensitiveValues: json.RawMessage("{\"foozles\":true}"), }, }, false, @@ -512,9 +595,8 @@ func TestMarshalResources(t *testing.T) { Current: &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"data":{"woozles":"confuzles"}}`), - AttrSensitivePaths: []cty.PathValueMarks{{ - Path: cty.Path{cty.GetAttrStep{Name: "data"}}, - Marks: cty.NewValueMarks(marks.Sensitive)}, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("data"), }, }, }, @@ -526,15 +608,15 @@ func TestMarshalResources(t *testing.T) { }, }, testSchemas(), - []resource{ + []Resource{ { Address: "test_map_attr.bar", Mode: "managed", Type: "test_map_attr", Name: "bar", - Index: addrs.InstanceKey(nil), + Index: nil, ProviderName: "registry.terraform.io/hashicorp/test", - AttributeValues: attributeValues{ + AttributeValues: AttributeValues{ "data": json.RawMessage(`{"woozles":"confuzles"}`), }, SensitiveValues: json.RawMessage(`{"data":true}`), @@ -542,6 +624,115 @@ func TestMarshalResources(t *testing.T) { }, false, }, + "single resource with identity": { + map[string]*states.Resource{ + "test_identity.bar": { + Addr: addrs.AbsResource{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_identity", + Name: "bar", + }, + }, + Instances: map[addrs.InstanceKey]*states.ResourceInstance{ + addrs.NoKey: { + Current: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles","name":"bar"}`), + IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`), + }, + }, + }, + ProviderConfig: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + }, + }, + testSchemas(), + []Resource{ + { + Address: "test_identity.bar", + Mode: "managed", + Type: "test_identity", + Name: "bar", + Index: nil, + ProviderName: "registry.terraform.io/hashicorp/test", + AttributeValues: AttributeValues{ + "name": json.RawMessage(`"bar"`), + "foozles": json.RawMessage(`"sensuzles"`), + "woozles": json.RawMessage(`"confuzles"`), + }, + SensitiveValues: json.RawMessage("{\"foozles\":true}"), + IdentityValues: IdentityValues{ + "name": json.RawMessage(`"bar"`), + "foozles": json.RawMessage(`"sensuzles"`), + }, + IdentitySchemaVersion: ptrOf[uint64](0), + }, + }, + false, + }, + "single resource wrong identity schema": { + map[string]*states.Resource{ + "test_identity.bar": { + Addr: addrs.AbsResource{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_identity", + Name: "bar", + }, + }, + Instances: map[addrs.InstanceKey]*states.ResourceInstance{ + addrs.NoKey: { + Current: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles","name":"bar"}`), + IdentitySchemaVersion: 1, + IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`), + }, + }, + }, + ProviderConfig: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + }, + }, + testSchemas(), + nil, + true, + }, + "single resource missing identity schema": { + map[string]*states.Resource{ + "test_thing.bar": { + Addr: addrs.AbsResource{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "bar", + }, + }, + Instances: map[addrs.InstanceKey]*states.ResourceInstance{ + addrs.NoKey: { + Current: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"woozles":"confuzles","foozles":"sensuzles"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"foozles":"sensuzles","name":"bar"}`), + }, + }, + }, + ProviderConfig: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + }, + }, + testSchemas(), + nil, + true, + }, } for name, test := range tests { @@ -762,25 +953,47 @@ func TestMarshalModules_parent_no_resources(t *testing.T) { func testSchemas() *terraform.Schemas { return &terraform.Schemas{ - Providers: map[addrs.Provider]*terraform.ProviderSchema{ + Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.NewDefaultProvider("test"): { - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_thing": { - Attributes: map[string]*configschema.Attribute{ - "woozles": {Type: cty.String, Optional: true, Computed: true}, - "foozles": {Type: cty.String, Optional: true, Sensitive: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "woozles": {Type: cty.String, Optional: true, Computed: true}, + "foozles": {Type: cty.String, Optional: true, Sensitive: true}, + }, }, }, "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "foo": {Type: cty.String, Optional: true}, - "bar": {Type: cty.String, Optional: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "foo": {Type: cty.String, Optional: true}, + "bar": {Type: cty.String, Optional: true}, + }, }, }, "test_map_attr": { - Attributes: map[string]*configschema.Attribute{ - "data": {Type: cty.Map(cty.String), Optional: true, Computed: true, Sensitive: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "data": {Type: cty.Map(cty.String), Optional: true, Computed: true, Sensitive: true}, + }, + }, + }, + "test_identity": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Required: true}, + "woozles": {Type: cty.String, Optional: true, Computed: true}, + "foozles": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Required: true}, + "foozles": {Type: cty.String, Optional: true}, + }, + Nesting: configschema.NestingSingle, }, }, }, diff --git a/internal/command/junit/junit.go b/internal/command/junit/junit.go new file mode 100644 index 0000000000..c62c51d581 --- /dev/null +++ b/internal/command/junit/junit.go @@ -0,0 +1,338 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package junit + +import ( + "bytes" + "encoding/xml" + "fmt" + "maps" + "os" + "slices" + "strconv" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestJUnitXMLFile produces a JUnit XML file at the conclusion of a test +// run, summarizing the outcome of the test in a form that can then be +// interpreted by tools which render JUnit XML result reports. +// +// The de-facto convention for JUnit XML is for it to be emitted as a separate +// file as a complement to human-oriented output, rather than _instead of_ +// human-oriented output. To meet that expectation the method [TestJUnitXMLFile.Save] +// should be called at the same time as the test's view reaches its "Conclusion" event. +// If that event isn't reached for any reason then no file should be created at +// all, which JUnit XML-consuming tools tend to expect as an outcome of a +// catastrophically-errored test suite. +// +// TestJUnitXMLFile implements the JUnit interface, which allows creation of a local +// file that contains a description of a completed test suite. It is intended only +// for use in conjunction with a View that provides the streaming output of ongoing +// testing events. + +type TestJUnitXMLFile struct { + filename string + + // A config loader is required to access sources, which are used with diagnostics to create XML content + configLoader *configload.Loader + + // A pointer to the containing test suite runner is needed to monitor details like the command being stopped + testSuiteRunner moduletest.TestSuiteRunner +} + +type JUnit interface { + Save(*moduletest.Suite) tfdiags.Diagnostics +} + +var _ JUnit = (*TestJUnitXMLFile)(nil) + +// NewTestJUnitXML returns a [Test] implementation that will, when asked to +// report "conclusion", write a JUnit XML report to the given filename. +// +// If the file already exists then this view will silently overwrite it at the +// point of being asked to write a conclusion. Otherwise it will create the +// file at that time. If creating or overwriting the file fails, a subsequent +// call to method Err will return information about the problem. +func NewTestJUnitXMLFile(filename string, configLoader *configload.Loader, testSuiteRunner moduletest.TestSuiteRunner) *TestJUnitXMLFile { + return &TestJUnitXMLFile{ + filename: filename, + configLoader: configLoader, + testSuiteRunner: testSuiteRunner, + } +} + +// Save takes in a test suite, generates JUnit XML summarising the test results, +// and saves the content to the filename specified by user +func (v *TestJUnitXMLFile) Save(suite *moduletest.Suite) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // Prepare XML content + sources := v.configLoader.Parser().Sources() + xmlSrc, err := junitXMLTestReport(suite, v.testSuiteRunner.IsStopped(), sources) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error generating JUnit XML test output", + Detail: err.Error(), + }) + return diags + } + + // Save XML to the specified path + saveDiags := v.save(xmlSrc) + diags = append(diags, saveDiags...) + + return diags + +} + +func (v *TestJUnitXMLFile) save(xmlSrc []byte) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + err := os.WriteFile(v.filename, xmlSrc, 0660) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("error saving JUnit XML to file %q", v.filename), + Detail: err.Error(), + }) + return diags + } + + return nil +} + +type withMessage struct { + Message string `xml:"message,attr,omitempty"` + Body string `xml:",cdata"` +} + +type testCase struct { + Name string `xml:"name,attr"` + Classname string `xml:"classname,attr"` + Skipped *withMessage `xml:"skipped,omitempty"` + Failure *withMessage `xml:"failure,omitempty"` + Error *withMessage `xml:"error,omitempty"` + Stderr *withMessage `xml:"system-err,omitempty"` + + // RunTime is the time spent executing the run associated + // with this test case, in seconds with the fractional component + // representing partial seconds. + // + // We assume here that it's not practically possible for an + // execution to take literally zero fractional seconds at + // the accuracy we're using here (nanoseconds converted into + // floating point seconds) and so use zero to represent + // "not known", and thus omit that case. (In practice many + // JUnit XML consumers treat the absense of this attribute + // as zero anyway.) + RunTime float64 `xml:"time,attr,omitempty"` + Timestamp string `xml:"timestamp,attr,omitempty"` +} + +func junitXMLTestReport(suite *moduletest.Suite, suiteRunnerStopped bool, sources map[string][]byte) ([]byte, error) { + var buf bytes.Buffer + enc := xml.NewEncoder(&buf) + enc.EncodeToken(xml.ProcInst{ + Target: "xml", + Inst: []byte(`version="1.0" encoding="UTF-8"`), + }) + enc.Indent("", " ") + + // Some common element/attribute names we'll use repeatedly below. + suitesName := xml.Name{Local: "testsuites"} + suiteName := xml.Name{Local: "testsuite"} + caseName := xml.Name{Local: "testcase"} + nameName := xml.Name{Local: "name"} + testsName := xml.Name{Local: "tests"} + skippedName := xml.Name{Local: "skipped"} + failuresName := xml.Name{Local: "failures"} + errorsName := xml.Name{Local: "errors"} + + enc.EncodeToken(xml.StartElement{Name: suitesName}) + + // Sort the file names to ensure consistent ordering in XML + for _, name := range slices.Sorted(maps.Keys(suite.Files)) { + file := suite.Files[name] + // Each test file is modelled as a "test suite". + + // First we'll count the number of tests and number of failures/errors + // for the suite-level summary. + totalTests := len(file.Runs) + totalFails := 0 + totalErrs := 0 + totalSkipped := 0 + for _, run := range file.Runs { + switch run.Status { + case moduletest.Skip: + totalSkipped++ + case moduletest.Fail: + totalFails++ + case moduletest.Error: + totalErrs++ + } + } + enc.EncodeToken(xml.StartElement{ + Name: suiteName, + Attr: []xml.Attr{ + {Name: nameName, Value: file.Name}, + {Name: testsName, Value: strconv.Itoa(totalTests)}, + {Name: skippedName, Value: strconv.Itoa(totalSkipped)}, + {Name: failuresName, Value: strconv.Itoa(totalFails)}, + {Name: errorsName, Value: strconv.Itoa(totalErrs)}, + }, + }) + + for i, run := range file.Runs { + // Each run is a "test case". + + testCase := testCase{ + Name: run.Name, + + // We treat the test scenario filename as the "class name", + // implying that the run name is the "method name", just + // because that seems to inspire more useful rendering in + // some consumers of JUnit XML that were designed for + // Java-shaped languages. + Classname: file.Name, + } + if execMeta := run.ExecutionMeta; execMeta != nil { + testCase.RunTime = execMeta.Duration.Seconds() + testCase.Timestamp = execMeta.StartTimestamp() + } + + // Depending on run status, add either of: "skipped", "failure", or "error" elements + switch run.Status { + case moduletest.Skip: + testCase.Skipped = skipDetails(i, file, suiteRunnerStopped) + + case moduletest.Fail: + // When the test fails we only use error diags that originate from failing assertions + var failedAssertions tfdiags.Diagnostics + for _, d := range run.Diagnostics { + if tfdiags.DiagnosticCausedByTestFailure(d) { + failedAssertions = failedAssertions.Append(d) + } + } + + testCase.Failure = &withMessage{ + Message: failureMessage(failedAssertions, len(run.Config.CheckRules)), + Body: getDiagString(failedAssertions, sources), + } + + case moduletest.Error: + // When the test errors we use all diags with Error severity + var errDiags tfdiags.Diagnostics + for _, d := range run.Diagnostics { + if d.Severity() == tfdiags.Error { + errDiags = errDiags.Append(d) + } + } + + testCase.Error = &withMessage{ + Message: "Encountered an error", + Body: getDiagString(errDiags, sources), + } + } + + // Determine if there are diagnostics left unused by the switch block above + // that should be included in the "system-err" element + if len(run.Diagnostics) > 0 { + var systemErrDiags tfdiags.Diagnostics + + if run.Status == moduletest.Error && run.Diagnostics.HasWarnings() { + // If the test case errored, then all Error diags are in the "error" element + // Therefore we'd only need to include warnings in "system-err" + systemErrDiags = run.Diagnostics.Warnings() + } + + if run.Status != moduletest.Error { + // If a test hasn't errored then we need to find all diagnostics that aren't due + // to a failing assertion in a test (these are already displayed in the "failure" element) + + // Collect diags not due to failed assertions, both errors and warnings + for _, d := range run.Diagnostics { + if !tfdiags.DiagnosticCausedByTestFailure(d) { + systemErrDiags = systemErrDiags.Append(d) + } + } + } + + if len(systemErrDiags) > 0 { + testCase.Stderr = &withMessage{ + Body: getDiagString(systemErrDiags, sources), + } + } + } + + enc.EncodeElement(&testCase, xml.StartElement{ + Name: caseName, + }) + } + + enc.EncodeToken(xml.EndElement{Name: suiteName}) + } + enc.EncodeToken(xml.EndElement{Name: suitesName}) + enc.Close() + return buf.Bytes(), nil +} + +func failureMessage(failedAssertions tfdiags.Diagnostics, checkCount int) string { + if len(failedAssertions) == 0 { + return "" + } + + if len(failedAssertions) == 1 { + // Slightly different format if only single assertion failure + return fmt.Sprintf("%d of %d assertions failed: %s", len(failedAssertions), checkCount, failedAssertions[0].Description().Detail) + } + + // Handle multiple assertion failures + return fmt.Sprintf("%d of %d assertions failed, including: %s", len(failedAssertions), checkCount, failedAssertions[0].Description().Detail) +} + +// skipDetails checks data about the test suite, file and runs to determine why a given run was skipped +// Test can be skipped due to: +// 1. terraform test recieving an interrupt from users; all unstarted tests will be skipped +// 2. A previous run in a file has failed, causing subsequent run blocks to be skipped +// The returned value is used to set content in the "skipped" element +func skipDetails(runIndex int, file *moduletest.File, suiteStopped bool) *withMessage { + if suiteStopped { + // Test suite experienced an interrupt + // This block only handles graceful Stop interrupts, as Cancel interrupts will prevent a JUnit file being produced at all + return &withMessage{ + Message: "Testcase skipped due to an interrupt", + Body: "Terraform received an interrupt and stopped gracefully. This caused all remaining testcases to be skipped", + } + } + + if file.Status == moduletest.Error { + // Overall test file marked as errored in the context of a skipped test means tests have been skipped after + // a previous test/run blocks has errored out + for i := runIndex; i >= 0; i-- { + if file.Runs[i].Status == moduletest.Error { + // Skipped due to error in previous run within the file + return &withMessage{ + Message: "Testcase skipped due to a previous testcase error", + Body: fmt.Sprintf("Previous testcase %q ended in error, which caused the remaining tests in the file to be skipped", file.Runs[i].Name), + } + } + } + } + + // Unhandled case: This results in with no attributes or body + return &withMessage{} +} + +func getDiagString(diags tfdiags.Diagnostics, sources map[string][]byte) string { + var diagsStr strings.Builder + for _, d := range diags { + diagsStr.WriteString(format.DiagnosticPlain(d, sources, 80)) + } + return diagsStr.String() +} diff --git a/internal/command/junit/junit_internal_test.go b/internal/command/junit/junit_internal_test.go new file mode 100644 index 0000000000..19cffe4959 --- /dev/null +++ b/internal/command/junit/junit_internal_test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package junit + +import ( + "bytes" + "fmt" + "os" + "testing" +) + +func Test_TestJUnitXMLFile_save(t *testing.T) { + + cases := map[string]struct { + filename string + expectError bool + }{ + "can save output to the specified filename": { + filename: func() string { + td := t.TempDir() + return fmt.Sprintf("%s/output.xml", td) + }(), + }, + "returns an error when given a filename that isn't absolute or relative": { + filename: "~/output.xml", + expectError: true, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + j := TestJUnitXMLFile{ + filename: tc.filename, + } + + xml := []byte(` + + + +`) + + diags := j.save(xml) + + if diags.HasErrors() { + if !tc.expectError { + t.Fatalf("got unexpected error: %s", diags.Err()) + } + // return early if testing error case + return + } + + if !diags.HasErrors() && tc.expectError { + t.Fatalf("expected an error but got none") + } + + fileContent, err := os.ReadFile(tc.filename) + if err != nil { + t.Fatalf("unexpected error opening file") + } + + if !bytes.Equal(fileContent, xml) { + t.Fatalf("wanted XML:\n%s\n got XML:\n%s\n", string(xml), string(fileContent)) + } + }) + } +} diff --git a/internal/command/junit/junit_test.go b/internal/command/junit/junit_test.go new file mode 100644 index 0000000000..956571e561 --- /dev/null +++ b/internal/command/junit/junit_test.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package junit_test + +import ( + "bytes" + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform/internal/backend/local" + "github.com/hashicorp/terraform/internal/command/junit" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/moduletest" +) + +// This test cannot access sources when contructing output for XML files. Due to this, the majority of testing +// for TestJUnitXMLFile is in internal/command/test_test.go +// In the junit package we can write some limited tests about XML output as long as there are no errors and/or +// failing tests in the test. +func Test_TestJUnitXMLFile_Save(t *testing.T) { + + cases := map[string]struct { + filename string + runner *local.TestSuiteRunner + suite moduletest.Suite + expectedOuput []byte + expectError bool + }{ + " element can explain when skip is due to the runner being stopped by an interrupt": { + filename: "output.xml", + runner: &local.TestSuiteRunner{ + Stopped: true, + }, + suite: moduletest.Suite{ + Status: moduletest.Skip, + Files: map[string]*moduletest.File{ + "file1.tftest.hcl": { + Name: "file1.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "my_test", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + expectedOuput: []byte(` + + + + + +`), + }, + " element can explain when skip is due to the previously errored runs/testcases in the file": { + filename: "output.xml", + runner: &local.TestSuiteRunner{}, + suite: moduletest.Suite{ + Status: moduletest.Error, + Files: map[string]*moduletest.File{ + "file1.tftest.hcl": { + Name: "file1.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "my_test_1", + Status: moduletest.Error, + }, + { + Name: "my_test_2", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + expectedOuput: []byte(` + + + + + + + + +`), + }, + " element is present without additional details when contextual data is not available": { + filename: "output.xml", + runner: &local.TestSuiteRunner{ + // No data about being stopped + }, + suite: moduletest.Suite{ + Status: moduletest.Pending, + Files: map[string]*moduletest.File{ + "file1.tftest.hcl": { + Name: "file1.tftest.hcl", + Status: moduletest.Pending, + Runs: []*moduletest.Run{ + { + Name: "my_test", + Status: moduletest.Skip, // Only run present is skipped, no previous errors + }, + }, + }, + }, + }, + expectedOuput: []byte(` + + + + + +`), + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + // Setup test + td := t.TempDir() + path := fmt.Sprintf("%s/%s", td, tc.filename) + + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + + j := junit.NewTestJUnitXMLFile(path, loader, tc.runner) + + // Process data & save file + j.Save(&tc.suite) + + // Assertions + actualOut, err := os.ReadFile(path) + if err != nil { + t.Fatalf("error opening XML file: %s", err) + } + + if !bytes.Equal(actualOut, tc.expectedOuput) { + t.Fatalf("expected output:\n%s\ngot:\n%s", tc.expectedOuput, actualOut) + } + }) + } + +} diff --git a/internal/command/login.go b/internal/command/login.go index 6b1d8bddd4..9322cc00a3 100644 --- a/internal/command/login.go +++ b/internal/command/login.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -186,8 +189,8 @@ func (c *LoginCommand) Run(args []string) int { // We prefer an OAuth code grant if the server supports it. oauthToken, tokenDiags = c.interactiveGetTokenByCode(hostname, credsCtx, clientConfig) case clientConfig.SupportedGrantTypes.Has(disco.OAuthOwnerPasswordGrant) && hostname == svchost.Hostname("app.terraform.io"): - // The password grant type is allowed only for Terraform Cloud SaaS. - // Note this case is purely theoretical at this point, as TFC currently uses + // The password grant type is allowed only for HCP Terraform SaaS. + // Note this case is purely theoretical at this point, as HCP Terraform currently uses // its own bespoke login protocol (tfe) oauthToken, tokenDiags = c.interactiveGetTokenByPassword(hostname, credsCtx, clientConfig) default: @@ -225,7 +228,7 @@ func (c *LoginCommand) Run(args []string) int { } c.Ui.Output("\n---------------------------------------------------------------------------------\n") - if hostname == "app.terraform.io" { // Terraform Cloud + if hostname == "app.terraform.io" { // HCP Terraform var motd struct { Message string `json:"msg"` Errors []interface{} `json:"errors"` @@ -313,12 +316,12 @@ func (c *LoginCommand) outputDefaultTFELoginSuccess(dispHostname string) { func (c *LoginCommand) outputDefaultTFCLoginSuccess() { c.Ui.Output(c.Colorize().Color(strings.TrimSpace(` -[green][bold]Success![reset] [bold]Logged in to Terraform Cloud[reset] +[green][bold]Success![reset] [bold]Logged in to HCP Terraform[reset] ` + "\n"))) } func (c *LoginCommand) logMOTDError(err error) { - log.Printf("[TRACE] login: An error occurred attempting to fetch a message of the day for Terraform Cloud: %s", err) + log.Printf("[TRACE] login: An error occurred attempting to fetch a message of the day for HCP Terraform: %s", err) } // Help implements cli.Command. @@ -341,7 +344,7 @@ Usage: terraform [global options] login [hostname] automatic login, and saves it in a credentials file in your home directory. If no hostname is provided, the default hostname is app.terraform.io, to - log in to Terraform Cloud. + log in to HCP Terraform. If not overridden by credentials helper settings in the CLI configuration, the credentials will be written to the following local file: diff --git a/internal/command/login_test.go b/internal/command/login_test.go index b612b7bbed..2e68eb4017 100644 --- a/internal/command/login_test.go +++ b/internal/command/login_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -7,7 +10,7 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" svchost "github.com/hashicorp/terraform-svchost" "github.com/hashicorp/terraform-svchost/disco" @@ -71,7 +74,7 @@ func TestLogin(t *testing.T) { }, }) svcs.ForceHostServices(svchost.Hostname("app.terraform.io"), map[string]interface{}{ - // This represents Terraform Cloud, which does not yet support the + // This represents HCP Terraform, which does not yet support the // login API, but does support its own bespoke tokens API. "tfe.v2": ts.URL + "/api/v2", "tfe.v2.1": ts.URL + "/api/v2", @@ -121,7 +124,7 @@ func TestLogin(t *testing.T) { if got, want := creds.Token(), "good-token"; got != want { t.Errorf("wrong token %q; want %q", got, want) } - if got, want := ui.OutputWriter.String(), "Welcome to Terraform Cloud!"; !strings.Contains(got, want) { + if got, want := ui.OutputWriter.String(), "Welcome to HCP Terraform!"; !strings.Contains(got, want) { t.Errorf("expected output to contain %q, but was:\n%s", want, got) } })) diff --git a/internal/command/logout.go b/internal/command/logout.go index 904ccc5b06..f068fa3fee 100644 --- a/internal/command/logout.go +++ b/internal/command/logout.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/logout_test.go b/internal/command/logout_test.go index 6f2511ee74..464a7653e9 100644 --- a/internal/command/logout_test.go +++ b/internal/command/logout_test.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "path/filepath" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" svchost "github.com/hashicorp/terraform-svchost" svcauth "github.com/hashicorp/terraform-svchost/auth" diff --git a/internal/command/meta.go b/internal/command/meta.go index 594292f1b9..f7b7313409 100644 --- a/internal/command/meta.go +++ b/internal/command/meta.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -8,30 +11,33 @@ import ( "fmt" "io/ioutil" "log" + "maps" "os" "path/filepath" "strconv" "strings" "time" + "github.com/hashicorp/cli" plugin "github.com/hashicorp/go-plugin" "github.com/hashicorp/terraform-svchost/disco" - "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/format" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/command/webbrowser" "github.com/hashicorp/terraform/internal/command/workdir" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/getproviders" - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -101,6 +107,22 @@ type Meta struct { // into the given directory. PluginCacheDir string + // PluginCacheMayBreakDependencyLockFile is a temporary CLI configuration-based + // opt out for the behavior of only using the plugin cache dir if its + // contents match checksums recorded in the dependency lock file. + // + // This is an accommodation for those who currently essentially ignore the + // dependency lock file -- treating it only as transient working directory + // state -- and therefore don't care if the plugin cache dir causes the + // checksums inside to only be sufficient for the computer where Terraform + // is currently running. + // + // We intend to remove this exception again (making the CLI configuration + // setting a silent no-op) in future once we've improved the dependency + // lock file mechanism so that it's usable for everyone and there are no + // longer any compelling reasons for folks to not lock their dependencies. + PluginCacheMayBreakDependencyLockFile bool + // ProviderSource allows determining the available versions of a provider // and determines where a distribution package for a particular // provider version can be obtained. @@ -110,6 +132,15 @@ type Meta struct { // web browser. BrowserLauncher webbrowser.Launcher + // A context.Context provided by the caller -- typically "package main" -- + // which might be carrying telemetry-related metadata and so should be + // used when creating downstream traces, etc. + // + // This isn't guaranteed to be set, so use [Meta.CommandContext] to + // safely create a context for the entire execution of a command, which + // will be connected to this parent context if it's present. + CallerContext context.Context + // When this channel is closed, the command will be cancelled. ShutdownCh <-chan struct{} @@ -171,10 +202,10 @@ type Meta struct { configLoader *configload.Loader // backendState is the currently active backend state - backendState *legacy.BackendState + backendState *workdir.BackendState // Variables for the context (private) - variableArgs rawFlags + variableArgs arguments.FlagNameValueSlice input bool // Targets for this context (private) @@ -263,9 +294,7 @@ func (m *Meta) StateOutPath() string { // Colorize returns the colorization structure for a command. func (m *Meta) Colorize() *colorstring.Colorize { colors := make(map[string]string) - for k, v := range colorstring.DefaultColors { - colors[k] = v - } + maps.Copy(colors, colorstring.DefaultColors) colors["purple"] = "38;5;57" return &colorstring.Colorize{ @@ -372,12 +401,23 @@ func (m *Meta) StdinPiped() bool { // InterruptibleContext returns a context.Context that will be cancelled // if the process is interrupted by a platform-specific interrupt signal. // +// The typical way to use this is to pass the result of [Meta.CommandContext] +// as the base context, but that's appropriate only if the interruptible +// context is being created directly inside the "Run" method of a particular +// command, to create a context representing the entire remaining runtime of +// that command: +// // As usual with cancelable contexts, the caller must always call the given // cancel function once all operations are complete in order to make sure // that the context resources will still be freed even if there is no // interruption. -func (m *Meta) InterruptibleContext() (context.Context, context.CancelFunc) { - base := context.Background() +// +// // This example is only for when using this function very early in +// // the "Run" method of a Command implementation. If you already have +// // an active context, pass that in as base instead. +// ctx, done := c.InterruptibleContext(c.CommandContext()) +// defer done() +func (m *Meta) InterruptibleContext(base context.Context) (context.Context, context.CancelFunc) { if m.ShutdownCh == nil { // If we're running in a unit testing context without a shutdown // channel populated then we'll return an uncancelable channel. @@ -396,6 +436,27 @@ func (m *Meta) InterruptibleContext() (context.Context, context.CancelFunc) { return ctx, cancel } +// CommandContext returns the "root context" to use in the main Run function +// of a command. +// +// This method is just a substitute for passing a context directly to the +// "Run" method of a command, which we can't do because that API is owned by +// hashicorp/cli rather than by Terraform. Use this only in situations +// comparable to the context having been passed in as an argument to Run. +// +// If the caller (e.g. "package main") provided a context when it instantiated +// the Meta then the returned context will inherit all of its values, deadlines, +// etc. If the caller did not provide a context then the result is an inert +// background context ready to be passed to other functions. +func (m *Meta) CommandContext() context.Context { + if m.CallerContext == nil { + return context.Background() + } + // We just return the caller context directly for now, since we don't + // have anything to add to it. + return m.CallerContext +} + // RunOperation executes the given operation on the given backend, blocking // until that operation completes or is interrupted, and then returns // the RunningOperation object representing the completed or @@ -405,7 +466,7 @@ func (m *Meta) InterruptibleContext() (context.Context, context.CancelFunc) { // If the operation runs to completion then no error is returned even if the // operation itself is unsuccessful. Use the "Result" field of the // returned operation object to recognize operation-level failure. -func (m *Meta) RunOperation(b backend.Enhanced, opReq *backend.Operation) (*backend.RunningOperation, error) { +func (m *Meta) RunOperation(b backendrun.OperationsBackend, opReq *backendrun.Operation) (*backendrun.RunningOperation, error) { if opReq.View == nil { panic("RunOperation called with nil View") } @@ -517,11 +578,11 @@ func (m *Meta) extendedFlagSet(n string) *flag.FlagSet { f := m.defaultFlagSet(n) f.BoolVar(&m.input, "input", true, "input") - f.Var((*FlagStringSlice)(&m.targetFlags), "target", "resource to target") + f.Var((*arguments.FlagStringSlice)(&m.targetFlags), "target", "resource to target") f.BoolVar(&m.compactWarnings, "compact-warnings", false, "use compact warnings") - if m.variableArgs.items == nil { - m.variableArgs = newRawFlags("-var") + if m.variableArgs.Items == nil { + m.variableArgs = arguments.NewFlagNameValueSlice("-var") } varValues := m.variableArgs.Alias("-var") varFiles := m.variableArgs.Alias("-var-file") @@ -674,6 +735,28 @@ func (m *Meta) showDiagnostics(vals ...interface{}) { } } +const ( + // StatePersistIntervalEnvVar is the environment variable that can be set + // to control the interval at which Terraform persists state. The interval + // itself defaults to 20 seconds. + StatePersistIntervalEnvVar = "TF_STATE_PERSIST_INTERVAL" +) + +// StatePersistInterval returns the configured interval that Terraform should +// persist statefiles to the desired backend. Backends may choose to override +// the default value. +func (m *Meta) StatePersistInterval() int { + if val, ok := os.LookupEnv(StatePersistIntervalEnvVar); ok { + if interval, err := strconv.Atoi(val); err == nil && interval > DefaultStatePersistInterval { + // The user-specified interval must be greater than the default minimum + return interval + } else if err != nil { + log.Printf("[ERROR] Can't parse state persist interval %q of environment variable %q", val, StatePersistIntervalEnvVar) + } + } + return DefaultStatePersistInterval +} + // WorkspaceNameEnvVar is the name of the environment variable that can be used // to set the name of the Terraform workspace, overriding the workspace chosen // by `terraform workspace select`. @@ -779,3 +862,48 @@ func (m *Meta) checkRequiredVersion() tfdiags.Diagnostics { return nil } + +// MaybeGetSchemas attempts to load and return the schemas +// If there is not enough information to return the schemas, +// it could potentially return nil without errors. It is the +// responsibility of the caller to handle the lack of schema +// information accordingly +func (c *Meta) MaybeGetSchemas(state *states.State, config *configs.Config) (*terraform.Schemas, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + path, err := os.Getwd() + if err != nil { + diags.Append(tfdiags.SimpleWarning(failedToLoadSchemasMessage)) + return nil, diags + } + + if config == nil { + config, diags = c.loadConfig(path) + if diags.HasErrors() { + diags.Append(tfdiags.SimpleWarning(failedToLoadSchemasMessage)) + return nil, diags + } + } + + if config != nil || state != nil { + opts, err := c.contextOpts() + if err != nil { + diags = diags.Append(err) + return nil, diags + } + tfCtx, ctxDiags := terraform.NewContext(opts) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + return nil, diags + } + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags := tfCtx.Schemas(config, state) + diags = diags.Append(schemaDiags) + if schemaDiags.HasErrors() { + return nil, diags + } + return schemas, diags + + } + return nil, diags +} diff --git a/internal/command/meta_backend.go b/internal/command/meta_backend.go index 43e49152cf..d4e0234a81 100644 --- a/internal/command/meta_backend.go +++ b/internal/command/meta_backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command // This file contains all the Backend-related function calls on Meta, @@ -16,22 +19,23 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + backendInit "github.com/hashicorp/terraform/internal/backend/init" + backendLocal "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/command/workdir" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" - - backendInit "github.com/hashicorp/terraform/internal/backend/init" - backendLocal "github.com/hashicorp/terraform/internal/backend/local" - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" ) // BackendOpts are the options used to initialize a backend.Backend. @@ -53,6 +57,10 @@ type BackendOpts struct { // ForceLocal will force a purely local backend, including state. // You probably don't want to set this. ForceLocal bool + + // ViewType will set console output format for the + // initialization operation (JSON or human-readable). + ViewType arguments.ViewType } // BackendWithRemoteTerraformVersion is a shared interface between the 'remote' and 'cloud' backends @@ -81,7 +89,7 @@ type BackendWithRemoteTerraformVersion interface { // A side-effect of this method is the population of m.backendState, recording // the final resolved backend configuration after dealing with overrides from // the "terraform init" command line, etc. -func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics) { +func (m *Meta) Backend(opts *BackendOpts) (backendrun.OperationsBackend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // If no opts are set, then initialize @@ -144,7 +152,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics cliOpts.Validation = true // If the backend supports CLI initialization, do it. - if cli, ok := b.(backend.CLI); ok { + if cli, ok := b.(backendrun.CLI); ok { if err := cli.CLIInit(cliOpts); err != nil { diags = diags.Append(fmt.Errorf( "Error initializing backend %T: %s\n\n"+ @@ -157,7 +165,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics // If the result of loading the backend is an enhanced backend, // then return that as-is. This works even if b == nil (it will be !ok). - if enhanced, ok := b.(backend.Enhanced); ok { + if enhanced, ok := b.(backendrun.OperationsBackend); ok { log.Printf("[TRACE] Meta.Backend: backend %T supports operations", b) return enhanced, nil } @@ -194,7 +202,7 @@ func (m *Meta) Backend(opts *BackendOpts) (backend.Enhanced, tfdiags.Diagnostics // with inside backendFromConfig, because we still need that codepath // to be able to recognize the lack of a config as distinct from // explicitly setting local until we do some more refactoring here. - m.backendState = &legacy.BackendState{ + m.backendState = &workdir.BackendState{ Type: "local", ConfigRaw: json.RawMessage("{}"), } @@ -222,7 +230,7 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ Id: "create-workspace", Query: "\n[reset][bold][yellow]No workspaces found.[reset]", - Description: fmt.Sprintf(inputCloudInitCreateWorkspace, strings.Join(c.WorkspaceMapping.Tags, ", ")), + Description: fmt.Sprintf(inputCloudInitCreateWorkspace, c.WorkspaceMapping.DescribeTags()), }) if err != nil { return fmt.Errorf("Couldn't create initial workspace: %w", err) @@ -231,10 +239,10 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { if name == "" { return fmt.Errorf("Couldn't create initial workspace: no name provided") } - log.Printf("[TRACE] Meta.selectWorkspace: selecting the new TFC workspace requested by the user (%s)", name) + log.Printf("[TRACE] Meta.selectWorkspace: selecting the new HCP Terraform workspace requested by the user (%s)", name) return m.SetWorkspace(name) } else { - return fmt.Errorf(strings.TrimSpace(errBackendNoExistingWorkspaces)) + return errors.New(strings.TrimSpace(errBackendNoExistingWorkspaces)) } } @@ -284,17 +292,17 @@ func (m *Meta) selectWorkspace(b backend.Backend) error { } workspace = workspaces[idx-1] - log.Printf("[TRACE] Meta.selectWorkspace: setting the current workpace according to user selection (%s)", workspace) + log.Printf("[TRACE] Meta.selectWorkspace: setting the current workspace according to user selection (%s)", workspace) return m.SetWorkspace(workspace) } -// BackendForPlan is similar to Backend, but uses backend settings that were +// BackendForLocalPlan is similar to Backend, but uses backend settings that were // stored in a plan. // // The current workspace name is also stored as part of the plan, and so this // method will check that it matches the currently-selected workspace name // and produce error diagnostics if not. -func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags.Diagnostics) { +func (m *Meta) BackendForLocalPlan(settings plans.Backend) (backendrun.OperationsBackend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics f := backendInit.Backend(settings.Type) @@ -303,7 +311,7 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags return nil, diags } b := f() - log.Printf("[TRACE] Meta.BackendForPlan: instantiated backend of type %T", b) + log.Printf("[TRACE] Meta.BackendForLocalPlan: instantiated backend of type %T", b) schema := b.ConfigSchema() configVal, err := settings.Config.Decode(schema.ImpliedType()) @@ -325,7 +333,7 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags } // If the backend supports CLI initialization, do it. - if cli, ok := b.(backend.CLI); ok { + if cli, ok := b.(backendrun.CLI); ok { cliOpts, err := m.backendCLIOpts() if err != nil { diags = diags.Append(err) @@ -343,14 +351,18 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags // If the result of loading the backend is an enhanced backend, // then return that as-is. This works even if b == nil (it will be !ok). - if enhanced, ok := b.(backend.Enhanced); ok { + if enhanced, ok := b.(backendrun.OperationsBackend); ok { log.Printf("[TRACE] Meta.BackendForPlan: backend %T supports operations", b) + if err := m.setupEnhancedBackendAliases(enhanced); err != nil { + diags = diags.Append(err) + return nil, diags + } return enhanced, nil } // Otherwise, we'll wrap our state-only remote backend in the local backend // to cause any operations to be run locally. - log.Printf("[TRACE] Meta.Backend: backend %T does not support operations, so wrapping it in a local backend", b) + log.Printf("[TRACE] Meta.BackendForLocalPlan: backend %T does not support operations, so wrapping it in a local backend", b) cliOpts, err := m.backendCLIOpts() if err != nil { diags = diags.Append(err) @@ -366,14 +378,14 @@ func (m *Meta) BackendForPlan(settings plans.Backend) (backend.Enhanced, tfdiags return local, diags } -// backendCLIOpts returns a backend.CLIOpts object that should be passed to +// backendCLIOpts returns a backendrun.CLIOpts object that should be passed to // a backend that supports local CLI operations. -func (m *Meta) backendCLIOpts() (*backend.CLIOpts, error) { +func (m *Meta) backendCLIOpts() (*backendrun.CLIOpts, error) { contextOpts, err := m.contextOpts() if contextOpts == nil && err != nil { return nil, err } - return &backend.CLIOpts{ + return &backendrun.CLIOpts{ CLI: m.Ui, CLIColor: m.Colorize(), Streams: m.Streams, @@ -386,12 +398,12 @@ func (m *Meta) backendCLIOpts() (*backend.CLIOpts, error) { }, err } -// Operation initializes a new backend.Operation struct. +// Operation initializes a new backendrun.Operation struct. // // This prepares the operation. After calling this, the caller is expected // to modify fields of the operation such as Sequence to specify what will // be called. -func (m *Meta) Operation(b backend.Backend) *backend.Operation { +func (m *Meta) Operation(b backend.Backend, vt arguments.ViewType) *backendrun.Operation { schema := b.ConfigSchema() workspace, err := m.Workspace() if err != nil { @@ -411,7 +423,7 @@ func (m *Meta) Operation(b backend.Backend) *backend.Operation { stateLocker := clistate.NewNoopLocker() if m.stateLock { - view := views.NewStateLocker(arguments.ViewHuman, m.View) + view := views.NewStateLocker(vt, m.View) stateLocker = clistate.NewLocker(m.stateLockTimeout, view) } @@ -425,7 +437,7 @@ func (m *Meta) Operation(b backend.Backend) *backend.Operation { log.Printf("[WARN] Failed to load dependency locks while preparing backend operation (ignored): %s", diags.Err().Error()) } - return &backend.Operation{ + return &backendrun.Operation{ PlanOutBackend: planOutBackend, Targets: m.targets, UIIn: m.UIInput(), @@ -521,9 +533,9 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di // ------------------------------------------------------------------------ // For historical reasons, current backend configuration for a working - // directory is kept in a *state-like* file, using the legacy state - // structures in the Terraform package. It is not actually a Terraform - // state, and so only the "backend" portion of it is actually used. + // directory is kept in a *state-like* file, using a subset of the oldstate + // snapshot version 3. It is not actually a Terraform state, and so only + // the "backend" portion of it is actually used. // // The remainder of this code often confusingly refers to this as a "state", // so it's unfortunately important to remember that this is not actually @@ -550,7 +562,7 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di s := sMgr.State() if s == nil { log.Printf("[TRACE] Meta.Backend: backend has not previously been initialized in this working directory") - s = legacy.NewState() + s = workdir.NewBackendStateFile() } else if s.Backend != nil { log.Printf("[TRACE] Meta.Backend: working directory was previously initialized for %q backend", s.Backend.Type) } else { @@ -573,17 +585,6 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di } }() - if !s.Remote.Empty() { - // Legacy remote state is no longer supported. User must first - // migrate with Terraform 0.11 or earlier. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Legacy remote state not supported", - "This working directory is configured for legacy remote state, which is no longer supported from Terraform v0.12 onwards. To migrate this environment, first run \"terraform init\" under a Terraform 0.11 release, and then upgrade Terraform again.", - )) - return nil, diags - } - // This switch statement covers all the different combinations of // configuring new backends, updating previously-configured backends, etc. switch { @@ -611,17 +612,17 @@ func (m *Meta) backendFromConfig(opts *BackendOpts) (backend.Backend, tfdiags.Di return nil, diags } - return m.backend_c_r_S(c, cHash, sMgr, true) + return m.backend_c_r_S(c, cHash, sMgr, true, opts) // Configuring a backend for the first time or -reconfigure flag was used case c != nil && s.Backend.Empty(): log.Printf("[TRACE] Meta.Backend: moving from default local state only to %q backend", c.Type) if !opts.Init { if c.Type == "cloud" { - initReason := "Initial configuration of Terraform Cloud" + initReason := "Initial configuration of HCP Terraform or Terraform Enterprise" diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Terraform Cloud initialization required: please run \"terraform init\"", + "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), )) } else { @@ -719,11 +720,11 @@ func (m *Meta) determineInitReason(previousBackendType string, currentBackendTyp initReason := "" switch cloudMode { case cloud.ConfigMigrationIn: - initReason = fmt.Sprintf("Changed from backend %q to Terraform Cloud", previousBackendType) + initReason = fmt.Sprintf("Changed from backend %q to HCP Terraform", previousBackendType) case cloud.ConfigMigrationOut: - initReason = fmt.Sprintf("Changed from Terraform Cloud to backend %q", previousBackendType) + initReason = fmt.Sprintf("Changed from HCP Terraform to backend %q", previousBackendType) case cloud.ConfigChangeInPlace: - initReason = "Terraform Cloud configuration block has changed" + initReason = "HCP Terraform configuration block has changed" default: switch { case previousBackendType != currentBackendType: @@ -738,13 +739,13 @@ func (m *Meta) determineInitReason(previousBackendType string, currentBackendTyp case cloud.ConfigChangeInPlace: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Terraform Cloud initialization required: please run \"terraform init\"", + "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), )) case cloud.ConfigMigrationIn: diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Terraform Cloud initialization required: please run \"terraform init\"", + "HCP Terraform or Terraform Enterprise initialization required: please run \"terraform init\"", fmt.Sprintf(strings.TrimSpace(errBackendInitCloud), initReason), )) default: @@ -759,10 +760,10 @@ func (m *Meta) determineInitReason(previousBackendType string, currentBackendTyp } // backendFromState returns the initialized (not configured) backend directly -// from the state. This should be used only when a user runs `terraform init -// -backend=false`. This function returns a local backend if there is no state -// or no backend configured. -func (m *Meta) backendFromState() (backend.Backend, tfdiags.Diagnostics) { +// from the backend state. This should be used only when a user runs +// `terraform init -backend=false`. This function returns a local backend if +// there is no backend state or no backend configured. +func (m *Meta) backendFromState(_ context.Context) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Get the path to where we store a local cache of backend configuration // if we're using a remote backend. This may not yet exist which means @@ -824,6 +825,17 @@ func (m *Meta) backendFromState() (backend.Backend, tfdiags.Diagnostics) { return nil, diags } + // If the result of loading the backend is an enhanced backend, + // then set up enhanced backend service aliases. + if enhanced, ok := b.(backendrun.OperationsBackend); ok { + log.Printf("[TRACE] Meta.BackendForPlan: backend %T supports operations", b) + + if err := m.setupEnhancedBackendAliases(enhanced); err != nil { + diags = diags.Append(err) + return nil, diags + } + } + return b, diags } @@ -845,9 +857,18 @@ func (m *Meta) backendFromState() (backend.Backend, tfdiags.Diagnostics) { //------------------------------------------------------------------- // Unconfiguring a backend (moving from backend => local). -func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool) (backend.Backend, tfdiags.Diagnostics) { +func (m *Meta) backend_c_r_S( + c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + vt := arguments.ViewJSON + // Set default viewtype if none was set as the StateLocker needs to know exactly + // what viewType we want to have. + if opts == nil || opts.ViewType != vt { + vt = arguments.ViewHuman + } + s := sMgr.State() cloudMode := cloud.DetectConfigChangeType(s.Backend, c, false) @@ -860,7 +881,7 @@ func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *clistate.Local backendType := s.Backend.Type if cloudMode == cloud.ConfigMigrationOut { - m.Ui.Output("Migrating from Terraform Cloud to local state.") + m.Ui.Output("Migrating from HCP Terraform or Terraform Enterprise to local state.") } else { m.Ui.Output(fmt.Sprintf(strings.TrimSpace(outputBackendMigrateLocal), s.Backend.Type)) } @@ -885,6 +906,7 @@ func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *clistate.Local DestinationType: "local", Source: b, Destination: localB, + ViewType: vt, }) if err != nil { diags = diags.Append(err) @@ -916,6 +938,13 @@ func (m *Meta) backend_c_r_S(c *configs.Backend, cHash int, sMgr *clistate.Local func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.LocalState, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + vt := arguments.ViewJSON + // Set default viewtype if none was set as the StateLocker needs to know exactly + // what viewType we want to have. + if opts == nil || opts.ViewType != vt { + vt = arguments.ViewHuman + } + // Grab a purely local backend to get the local state if it exists localB, localBDiags := m.Backend(&BackendOpts{ForceLocal: true, Init: true}) if localBDiags.HasErrors() { @@ -970,6 +999,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local DestinationType: c.Type, Source: localB, Destination: b, + ViewType: vt, }) if err != nil { diags = diags.Append(err) @@ -998,7 +1028,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local diags = diags.Append(fmt.Errorf(errBackendMigrateLocalDelete, err)) return nil, diags } - if err := localState.PersistState(); err != nil { + if err := localState.PersistState(nil); err != nil { diags = diags.Append(fmt.Errorf(errBackendMigrateLocalDelete, err)) return nil, diags } @@ -1007,7 +1037,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local } if m.stateLock { - view := views.NewStateLocker(arguments.ViewHuman, m.View) + view := views.NewStateLocker(vt, m.View) stateLocker := clistate.NewLocker(m.stateLockTimeout, view) if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) @@ -1025,9 +1055,9 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local // Store the metadata in our saved state location s := sMgr.State() if s == nil { - s = legacy.NewState() + s = workdir.NewBackendStateFile() } - s.Backend = &legacy.BackendState{ + s.Backend = &workdir.BackendState{ Type: c.Type, ConfigRaw: json.RawMessage(configJSON), Hash: uint64(cHash), @@ -1063,7 +1093,7 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local return nil, diags } - // By now the backend is successfully configured. If using Terraform Cloud, the success + // By now the backend is successfully configured. If using HCP Terraform, the success // message is handled as part of the final init message if _, ok := b.(*cloud.Cloud); !ok { m.Ui.Output(m.Colorize().Color(fmt.Sprintf( @@ -1077,6 +1107,13 @@ func (m *Meta) backend_C_r_s(c *configs.Backend, cHash int, sMgr *clistate.Local func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clistate.LocalState, output bool, opts *BackendOpts) (backend.Backend, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + vt := arguments.ViewJSON + // Set default viewtype if none was set as the StateLocker needs to know exactly + // what viewType we want to have. + if opts == nil || opts.ViewType != vt { + vt = arguments.ViewHuman + } + // Get the old state s := sMgr.State() @@ -1090,11 +1127,11 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista // Notify the user switch cloudMode { case cloud.ConfigChangeInPlace: - m.Ui.Output("Terraform Cloud configuration has changed.") + m.Ui.Output("HCP Terraform configuration has changed.") case cloud.ConfigMigrationIn: - m.Ui.Output(fmt.Sprintf("Migrating from backend %q to Terraform Cloud.", s.Backend.Type)) + m.Ui.Output(fmt.Sprintf("Migrating from backend %q to HCP Terraform.", s.Backend.Type)) case cloud.ConfigMigrationOut: - m.Ui.Output(fmt.Sprintf("Migrating from Terraform Cloud to backend %q.", c.Type)) + m.Ui.Output(fmt.Sprintf("Migrating from HCP Terraform to backend %q.", c.Type)) default: if s.Backend.Type != c.Type { output := fmt.Sprintf(outputBackendMigrateChange, s.Backend.Type, c.Type) @@ -1116,9 +1153,9 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista return nil, diags } - // If this is a migration into, out of, or irrelevant to Terraform Cloud + // If this is a migration into, out of, or irrelevant to HCP Terraform // mode then we will do state migration here. Otherwise, we just update - // the working directory initialization directly, because Terraform Cloud + // the working directory initialization directly, because HCP Terraform // doesn't have configurable state storage anyway -- we're only changing // which workspaces are relevant to this configuration, not where their // state lives. @@ -1136,6 +1173,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista DestinationType: c.Type, Source: oldB, Destination: b, + ViewType: vt, }) if err != nil { diags = diags.Append(err) @@ -1143,7 +1181,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista } if m.stateLock { - view := views.NewStateLocker(arguments.ViewHuman, m.View) + view := views.NewStateLocker(vt, m.View) stateLocker := clistate.NewLocker(m.stateLockTimeout, view) if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil { diags = diags.Append(fmt.Errorf("Error locking state: %s", err)) @@ -1162,9 +1200,9 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista // Update the backend state s = sMgr.State() if s == nil { - s = legacy.NewState() + s = workdir.NewBackendStateFile() } - s.Backend = &legacy.BackendState{ + s.Backend = &workdir.BackendState{ Type: c.Type, ConfigRaw: json.RawMessage(configJSON), Hash: uint64(cHash), @@ -1188,7 +1226,7 @@ func (m *Meta) backend_C_r_S_changed(c *configs.Backend, cHash int, sMgr *clista } if output { - // By now the backend is successfully configured. If using Terraform Cloud, the success + // By now the backend is successfully configured. If using HCP Terraform, the success // message is handled as part of the final init message if _, ok := b.(*cloud.Cloud); !ok { m.Ui.Output(m.Colorize().Color(fmt.Sprintf( @@ -1244,6 +1282,17 @@ func (m *Meta) savedBackend(sMgr *clistate.LocalState) (backend.Backend, tfdiags return nil, diags } + // If the result of loading the backend is an enhanced backend, + // then set up enhanced backend service aliases. + if enhanced, ok := b.(backendrun.OperationsBackend); ok { + log.Printf("[TRACE] Meta.BackendForPlan: backend %T supports operations", b) + + if err := m.setupEnhancedBackendAliases(enhanced); err != nil { + diags = diags.Append(err) + return nil, diags + } + } + return b, diags } @@ -1276,7 +1325,7 @@ func (m *Meta) updateSavedBackendHash(cHash int, sMgr *clistate.LocalState) tfdi // this function will conservatively assume that migration is required, // expecting that the migration code will subsequently deal with the same // errors. -func (m *Meta) backendConfigNeedsMigration(c *configs.Backend, s *legacy.BackendState) bool { +func (m *Meta) backendConfigNeedsMigration(c *configs.Backend, s *workdir.BackendState) bool { if s == nil || s.Empty() { log.Print("[TRACE] backendConfigNeedsMigration: no cached config, so migration is required") return true @@ -1339,6 +1388,14 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V return nil, cty.NilVal, diags } + if !configVal.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unknown values within backend definition", + "The `terraform` configuration block should contain only concrete and static values. Another diagnostic should contain more information about which part of the configuration is problematic.")) + return nil, cty.NilVal, diags + } + // TODO: test if m.Input() { var err error @@ -1364,9 +1421,36 @@ func (m *Meta) backendInitFromConfig(c *configs.Backend) (backend.Backend, cty.V configureDiags := b.Configure(newVal) diags = diags.Append(configureDiags.InConfigBody(c.Config, "")) + // If the result of loading the backend is an enhanced backend, + // then set up enhanced backend service aliases. + if enhanced, ok := b.(backendrun.OperationsBackend); ok { + log.Printf("[TRACE] Meta.BackendForPlan: backend %T supports operations", b) + if err := m.setupEnhancedBackendAliases(enhanced); err != nil { + diags = diags.Append(err) + return nil, cty.NilVal, diags + } + } + return b, configVal, diags } +// Helper method to get aliases from the enhanced backend and alias them +// in the Meta service discovery. It's unfortunate that the Meta backend +// is modifying the service discovery at this level, but the owner +// of the service discovery pointer does not have easy access to the backend. +func (m *Meta) setupEnhancedBackendAliases(b backendrun.OperationsBackend) error { + // Set up the service discovery aliases specified by the enhanced backend. + serviceAliases, err := b.ServiceDiscoveryAliases() + if err != nil { + return err + } + + for _, alias := range serviceAliases { + m.Services.Alias(alias.From, alias.To) + } + return nil +} + // Helper method to ignore remote/cloud backend version conflicts. Only call this // for commands which cannot accidentally upgrade remote state files. func (m *Meta) ignoreRemoteVersionConflict(b backend.Backend) { @@ -1405,19 +1489,19 @@ func (m *Meta) remoteVersionCheck(b backend.Backend, workspace string) tfdiags.D func (m *Meta) assertSupportedCloudInitOptions(mode cloud.ConfigChangeMode) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if mode.InvolvesCloud() { - log.Printf("[TRACE] Meta.Backend: Terraform Cloud mode initialization type: %s", mode) + log.Printf("[TRACE] Meta.Backend: HCP Terraform or Terraform Enterprise mode initialization type: %s", mode) if m.reconfigure { if mode.IsCloudMigration() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid command-line option", - "The -reconfigure option is unsupported when migrating to Terraform Cloud, because activating Terraform Cloud involves some additional steps.", + "The -reconfigure option is unsupported when migrating to HCP Terraform, because activating HCP Terraform involves some additional steps.", )) } else { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid command-line option", - "The -reconfigure option is for in-place reconfiguration of state backends only, and is not needed when changing Terraform Cloud settings.\n\nWhen using Terraform Cloud, initialization automatically activates any new Cloud configuration settings.", + "The -reconfigure option is for in-place reconfiguration of state backends only, and is not needed when changing HCP Terraform settings.\n\nWhen using HCP Terraform, initialization automatically activates any new Cloud configuration settings.", )) } } @@ -1434,13 +1518,13 @@ func (m *Meta) assertSupportedCloudInitOptions(mode cloud.ConfigChangeMode) tfdi diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid command-line option", - fmt.Sprintf("The %s option is for migration between state backends only, and is not applicable when using Terraform Cloud.\n\nTerraform Cloud migration has additional steps, configured by interactive prompts.", name), + fmt.Sprintf("The %s option is for migration between state backends only, and is not applicable when using HCP Terraform.\n\nHCP Terraform migrations have additional steps, configured by interactive prompts.", name), )) } else { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid command-line option", - fmt.Sprintf("The %s option is for migration between state backends only, and is not applicable when using Terraform Cloud.\n\nState storage is handled automatically by Terraform Cloud and so the state storage location is not configurable.", name), + fmt.Sprintf("The %s option is for migration between state backends only, and is not applicable when using HCP Terraform.\n\nState storage is handled automatically by HCP Terraform and so the state storage location is not configurable.", name), )) } } @@ -1534,7 +1618,7 @@ configuration or state have been made. const errBackendInitCloud = ` Reason: %s. -Changes to the Terraform Cloud configuration block require reinitialization, to discover any changes to the available workspaces. +Changes to the HCP Terraform configuration block require reinitialization, to discover any changes to the available workspaces. To re-initialize, run: terraform init @@ -1568,11 +1652,11 @@ has changed. Terraform will now check for existing state in the backends. const inputCloudInitCreateWorkspace = ` There are no workspaces with the configured tags (%s) -in your Terraform Cloud organization. To finish initializing, Terraform needs at +in your HCP Terraform organization. To finish initializing, Terraform needs at least one workspace available. Terraform can create a properly tagged workspace for you now. Please enter a -name to create a new Terraform Cloud workspace. +name to create a new HCP Terraform workspace. ` const successBackendUnset = ` diff --git a/internal/command/meta_backend_migrate.go b/internal/command/meta_backend_migrate.go index 8cd011eff0..ce9da101b4 100644 --- a/internal/command/meta_backend_migrate.go +++ b/internal/command/meta_backend_migrate.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,7 +8,6 @@ import ( "context" "errors" "fmt" - "io/ioutil" "log" "os" "path/filepath" @@ -26,6 +28,7 @@ import ( type backendMigrateOpts struct { SourceType, DestinationType string Source, Destination backend.Backend + ViewType arguments.ViewType // Fields below are set internally when migrate is called @@ -69,7 +72,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { opts.force = m.forceInitCopy // Disregard remote Terraform version for the state source backend. If it's a - // Terraform Cloud remote backend, we don't care about the remote version, + // HCP Terraform remote backend, we don't care about the remote version, // as we are migrating away and will not break a remote workspace. m.ignoreRemoteVersionConflict(opts.Source) @@ -78,7 +81,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { m.ignoreRemoteVersionConflict(opts.Destination) } else { // Check the remote Terraform version for the state destination backend. If - // it's a Terraform Cloud remote backend, we want to ensure that we don't + // it's an HCP Terraform remote backend, we want to ensure that we don't // break the workspace by uploading an incompatible state file. for _, workspace := range destinationWorkspaces { diags := m.remoteVersionCheck(opts.Destination, workspace) @@ -88,7 +91,7 @@ func (m *Meta) backendMigrateState(opts *backendMigrateOpts) error { } // If there are no specified destination workspaces, perform a remote // backend version check with the default workspace. - // Ensure that we are not dealing with Terraform Cloud migrations, as it + // Ensure that we are not dealing with HCP Terraform migrations, as it // does not support the default name. if len(destinationWorkspaces) == 0 && !destinationTFC { diags := m.remoteVersionCheck(opts.Destination, backend.DefaultStateName) @@ -339,8 +342,13 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { if m.stateLock { lockCtx := context.Background() - - view := views.NewStateLocker(arguments.ViewHuman, m.View) + vt := arguments.ViewJSON + // Set default viewtype if none was set as the StateLocker needs to know exactly + // what viewType we want to have. + if opts == nil || opts.ViewType != vt { + vt = arguments.ViewHuman + } + view := views.NewStateLocker(vt, m.View) locker := clistate.NewLocker(m.stateLockTimeout, view) lockerSource := locker.WithContext(lockCtx) @@ -438,7 +446,11 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), opts.SourceType, opts.DestinationType, err) } - if err := destinationState.PersistState(); err != nil { + // The backend is currently handled before providers are installed during init, + // so requiring schemas here could lead to a catch-22 where it requires some manual + // intervention to proceed far enough for provider installation. To avoid this, + // when migrating to HCP Terraform backend, the initial JSON varient of state won't be generated and stored. + if err := destinationState.PersistState(nil); err != nil { return fmt.Errorf(strings.TrimSpace(errBackendStateCopy), opts.SourceType, opts.DestinationType, err) } @@ -450,10 +462,14 @@ func (m *Meta) backendMigrateState_s_s(opts *backendMigrateOpts) error { func (m *Meta) backendMigrateEmptyConfirm(source, destination statemgr.Full, opts *backendMigrateOpts) (bool, error) { var inputOpts *terraform.InputOpts if opts.DestinationType == "cloud" { + appName := "HCP Terraform" + if cloudBackend, ok := opts.Destination.(*cloud.Cloud); ok { + appName = cloudBackend.AppName() + } inputOpts = &terraform.InputOpts{ Id: "backend-migrate-copy-to-empty-cloud", - Query: "Do you want to copy existing state to Terraform Cloud?", - Description: fmt.Sprintf(strings.TrimSpace(inputBackendMigrateEmptyCloud), opts.SourceType), + Query: "Do you want to copy existing state to HCP Terraform?", + Description: fmt.Sprintf(strings.TrimSpace(inputBackendMigrateEmptyCloud), opts.SourceType, appName), } } else { inputOpts = &terraform.InputOpts{ @@ -475,7 +491,7 @@ func (m *Meta) backendMigrateNonEmptyConfirm( destination := destinationState.State() // Save both to a temporary - td, err := ioutil.TempDir("", "terraform") + td, err := os.MkdirTemp("", "terraform") if err != nil { return false, fmt.Errorf("Error creating temporary directory: %s", err) } @@ -500,12 +516,16 @@ func (m *Meta) backendMigrateNonEmptyConfirm( // Ask for confirmation var inputOpts *terraform.InputOpts if opts.DestinationType == "cloud" { + appName := "HCP Terraform" + if cloudBackend, ok := opts.Destination.(*cloud.Cloud); ok { + appName = cloudBackend.AppName() + } inputOpts = &terraform.InputOpts{ Id: "backend-migrate-to-tfc", - Query: "Do you want to copy existing state to Terraform Cloud?", + Query: "Do you want to copy existing state to HCP Terraform?", Description: fmt.Sprintf( strings.TrimSpace(inputBackendMigrateNonEmptyCloud), - opts.SourceType, sourcePath, destinationPath), + opts.SourceType, sourcePath, destinationPath, appName), } } else { inputOpts = &terraform.InputOpts{ @@ -552,23 +572,22 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { return err } - // from TFC to non-TFC backend + // from HCP Terraform to non-TFC backend if sourceTFC && !destinationTFC { - // From Terraform Cloud to another backend. This is not yet implemented, and - // we recommend people to use the TFC API. - return fmt.Errorf(strings.TrimSpace(errTFCMigrateNotYetImplemented)) + // From HCP Terraform to another backend. This is not yet implemented, and + // we recommend people to use the HCP Terraform API. + return errors.New(strings.TrimSpace(errTFCMigrateNotYetImplemented)) } // Everything below, by the above two conditionals, now assumes that the - // destination is always Terraform Cloud (TFC). - + // destination is always HCP Terraform. sourceSingle := sourceSingleState || (len(sourceWorkspaces) == 1) if sourceSingle { if cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy { // If we know the name via WorkspaceNameStrategy, then set the // destinationWorkspace to the new Name and skip the user prompt. Here the // destinationWorkspace is not set to `default` thereby we will create it - // in TFC if it does not exist. + // in HCP Terraform if it does not exist. opts.destinationWorkspace = cloudBackendDestination.WorkspaceMapping.Name } @@ -611,7 +630,7 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { return m.backendMigrateState_s_s(opts) } - destinationTagsStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceTagsStrategy + destinationTagsStrategy := cloudBackendDestination.WorkspaceMapping.IsTagsStrategy() destinationNameStrategy := cloudBackendDestination.WorkspaceMapping.Strategy() == cloud.WorkspaceNameStrategy multiSource := !sourceSingleState && len(sourceWorkspaces) > 1 @@ -646,7 +665,7 @@ func (m *Meta) backendMigrateTFC(opts *backendMigrateOpts) error { return nil } -// migrates a multi-state backend to Terraform Cloud +// migrates a multi-state backend to HCP Terraform func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspaces []string) error { log.Print("[TRACE] backendMigrateState: migrating all named workspaces") @@ -688,7 +707,7 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa } } - // Fetch the pattern that will be used to rename the workspaces for Terraform Cloud. + // Fetch the pattern that will be used to rename the workspaces for HCP Terraform or Terraform Enterprise. // // * For the general case, this will be a pattern provided by the user. // @@ -696,9 +715,9 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa // instead 'migrate' the workspaces using a pattern based on the old prefix+name, // not allowing a user to accidentally input the wrong pattern to line up with // what the the remote backend was already using before (which presumably already - // meets the naming considerations for Terraform Cloud). + // meets the naming considerations for HCP Terraform). // In other words, this is a fast-track migration path from the remote backend, retaining - // how things already are in Terraform Cloud with no user intervention needed. + // how things already are in HCP Terraform with no user intervention needed. pattern := "" if remoteBackend, ok := opts.Source.(*remote.Remote); ok { if err := m.promptRemotePrefixToCloudTagsMigration(opts); err != nil { @@ -709,7 +728,14 @@ func (m *Meta) backendMigrateState_S_TFC(opts *backendMigrateOpts, sourceWorkspa } if pattern == "" { - pattern, err = m.promptMultiStateMigrationPattern(opts.SourceType) + var appName string + if cloudBackend, ok := opts.Destination.(*cloud.Cloud); ok { + appName = cloudBackend.AppName() + } else { + appName = "HCP Terraform" + } + + pattern, err = m.promptMultiStateMigrationPattern(opts.SourceType, appName) if err != nil { return err } @@ -795,10 +821,15 @@ func (m *Meta) promptSingleToCloudSingleStateMigration(opts *backendMigrateOpts) migrate := opts.force if !migrate { var err error + appName := "HCP Terraform" + if cloudBackend, ok := opts.Destination.(*cloud.Cloud); ok { + appName = cloudBackend.AppName() + } + migrate, err = m.confirm(&terraform.InputOpts{ Id: "backend-migrate-state-single-to-cloud-single", Query: "Do you wish to proceed?", - Description: strings.TrimSpace(tfcInputBackendMigrateStateSingleToCloudSingle), + Description: fmt.Sprintf(strings.TrimSpace(tfcInputBackendMigrateStateSingleToCloudSingle), appName), }) if err != nil { return false, fmt.Errorf("Error asking for state migration action: %s", err) @@ -816,10 +847,14 @@ func (m *Meta) promptRemotePrefixToCloudTagsMigration(opts *backendMigrateOpts) migrate := opts.force if !migrate { var err error + appName := "HCP Terraform" + if cloudBackend, ok := opts.Destination.(*cloud.Cloud); ok { + appName = cloudBackend.AppName() + } migrate, err = m.confirm(&terraform.InputOpts{ Id: "backend-migrate-remote-multistate-to-cloud", Query: "Do you wish to proceed?", - Description: strings.TrimSpace(tfcInputBackendMigrateRemoteMultiToCloud), + Description: fmt.Sprintf(strings.TrimSpace(tfcInputBackendMigrateRemoteMultiToCloud), appName), }) if err != nil { return fmt.Errorf("Error asking for state migration action: %s", err) @@ -842,13 +877,17 @@ func (m *Meta) promptMultiToSingleCloudMigration(opts *backendMigrateOpts) error migrate := opts.force if !migrate { var err error + appName := "HCP Terraform" + if cloudBackend, ok := opts.Destination.(*cloud.Cloud); ok { + appName = cloudBackend.AppName() + } // Ask the user if they want to migrate their existing remote state migrate, err = m.confirm(&terraform.InputOpts{ Id: "backend-migrate-multistate-to-single", Query: "Do you want to copy only your current workspace?", Description: fmt.Sprintf( strings.TrimSpace(tfcInputBackendMigrateMultiToSingle), - opts.SourceType, opts.destinationWorkspace), + opts.SourceType, opts.destinationWorkspace, appName), }) if err != nil { return fmt.Errorf("Error asking for state migration action: %s", err) @@ -870,7 +909,7 @@ func (m *Meta) promptNewWorkspaceName(destinationType string) (string, error) { log.Print("[TRACE] backendMigrateState: can't prompt for input, so aborting migration") return "", errors.New(strings.TrimSpace(errInteractiveInputDisabled)) } - message = `[reset][bold][yellow]Terraform Cloud requires all workspaces to be given an explicit name.[reset]` + message = `[reset][bold][yellow]HCP Terraform and Terraform Enterprise require all workspaces to be given an explicit name.[reset]` } name, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ Id: "new-state-name", @@ -884,13 +923,13 @@ func (m *Meta) promptNewWorkspaceName(destinationType string) (string, error) { return name, nil } -func (m *Meta) promptMultiStateMigrationPattern(sourceType string) (string, error) { +func (m *Meta) promptMultiStateMigrationPattern(sourceType string, appName string) (string, error) { // This is not the first prompt a user would be presented with in the migration to TFC, so no // guard on m.input is needed here. renameWorkspaces, err := m.UIInput().Input(context.Background(), &terraform.InputOpts{ Id: "backend-migrate-multistate-to-tfc", Query: fmt.Sprintf("[reset][bold][yellow]%s[reset]", "Would you like to rename your workspaces?"), - Description: fmt.Sprintf(strings.TrimSpace(tfcInputBackendMigrateMultiToMulti), sourceType), + Description: fmt.Sprintf(strings.TrimSpace(tfcInputBackendMigrateMultiToMulti), sourceType, appName), }) if err != nil { return "", fmt.Errorf("Error asking for state migration action: %s", err) @@ -960,7 +999,7 @@ This will attempt to copy (with permission) all workspaces again. ` const errBackendStateCopy = ` -Error copying state from the previous %q backend to the newly configured +Error copying state from the previous %q backend to the newly configured %q backend: %s @@ -969,9 +1008,10 @@ the error above and try again. ` const errTFCMigrateNotYetImplemented = ` -Migrating state from Terraform Cloud to another backend is not yet implemented. +Migrating state from HCP Terraform or Terraform Enterprise to another backend is not +yet implemented. -Please use the API to do this: https://www.terraform.io/docs/cloud/api/state-versions.html +Please use the API to do this: https://developer.hashicorp.com/terraform/cloud-docs/api-docs/state-versions ` const errInteractiveInputDisabled = ` @@ -988,23 +1028,26 @@ For example, if a workspace is currently named 'prod', the pattern 'app-*' would 'app-prod' for a new workspace name; 'app-*-region1' would yield 'app-prod-region1'. ` +// Done const tfcInputBackendMigrateMultiToMulti = ` Unlike typical Terraform workspaces representing an environment associated with a particular -configuration (e.g. production, staging, development), Terraform Cloud workspaces are named uniquely -across all configurations used within an organization. A typical strategy to start with is --- (e.g. networking-prod-us-east, networking-staging-us-east). +configuration (e.g. production, staging, development), HCP Terraform and Terraform Enterprise +workspaces are named uniquely across all configurations used within an organization. A typical +strategy to start with is -- (e.g. networking-prod-us-east, +networking-staging-us-east). -For more information on workspace naming, see https://www.terraform.io/docs/cloud/workspaces/naming.html +For more information on workspace naming, see https://developer.hashicorp.com/terraform/cloud-docs/workspaces/create -When migrating existing workspaces from the backend %[1]q to Terraform Cloud, would you like to -rename your workspaces? Enter 1 or 2. +When migrating existing workspaces from the backend %[1]q to %[2]s, +would you like to rename your workspaces? Enter 1 or 2. 1. Yes, I'd like to rename all workspaces according to a pattern I will provide. 2. No, I would not like to rename my workspaces. Migrate them as currently named. ` +// Done const tfcInputBackendMigrateMultiToSingle = ` -The previous backend %[1]q has multiple workspaces, but Terraform Cloud has +The previous backend %[1]q has multiple workspaces, but %[3]s has been configured to use a single workspace (%[2]q). By continuing, you will only migrate your current workspace. If you wish to migrate all workspaces from the previous backend, you may cancel this operation and use the 'tags' @@ -1013,28 +1056,31 @@ strategy in your workspace configuration block instead. Enter "yes" to proceed or "no" to cancel. ` +// Done const tfcInputBackendMigrateStateSingleToCloudSingle = ` -As part of migrating to Terraform Cloud, Terraform can optionally copy your -current workspace state to the configured Terraform Cloud workspace. +As part of migrating to %[1]s, Terraform can optionally copy +your current workspace state to the configured %[1]s workspace. Answer "yes" to copy the latest state snapshot to the configured -Terraform Cloud workspace. +%[1]s workspace. Answer "no" to ignore the existing state and just activate the configured -Terraform Cloud workspace with its existing state, if any. +%[1]s workspace with its existing state, if any. Should Terraform migrate your existing state? ` +// Done const tfcInputBackendMigrateRemoteMultiToCloud = ` When migrating from the 'remote' backend to Terraform's native integration -with Terraform Cloud, Terraform will automatically create or use existing -workspaces based on the previous backend configuration's 'prefix' value. +with %[1]s, Terraform will automatically +create or use existing workspaces based on the previous backend configuration's +'prefix' value. When the migration is complete, workspace names in Terraform will match the -fully qualified Terraform Cloud workspace name. If necessary, the workspace -tags configured in the 'cloud' option block will be added to the associated -Terraform Cloud workspaces. +fully qualified %[1]s workspace name. If necessary, the workspace +tags configured in the 'cloud' option block will be added to the associated +%[1]s workspaces. Enter "yes" to proceed or "no" to cancel. ` @@ -1046,9 +1092,10 @@ configured %[2]q backend. Do you want to copy this state to the new %[2]q backend? Enter "yes" to copy and "no" to start with an empty state. ` +// Done const inputBackendMigrateEmptyCloud = ` -Pre-existing state was found while migrating the previous %q backend to Terraform Cloud. -No existing state was found in Terraform Cloud. Do you want to copy this state to Terraform Cloud? +Pre-existing state was found while migrating the previous %[1]q backend to %[2]s. +No existing state was found in %[2]s. Do you want to copy this state to %[2]s? Enter "yes" to copy and "no" to start with an empty state. ` @@ -1066,17 +1113,18 @@ Enter "yes" to copy and "no" to start with the existing state in the newly configured %[2]q backend. ` +// Done const inputBackendMigrateNonEmptyCloud = ` Pre-existing state was found while migrating the previous %q backend to -Terraform Cloud. An existing non-empty state already exists in Terraform Cloud. +%[4]s. An existing non-empty state already exists in %[4]s. The two states have been saved to temporary files that will be removed after responding to this query. Previous (type %[1]q): %[2]s -New (Terraform Cloud): %[3]s +New (%[4]s): %[3]s -Do you want to overwrite the state in Terraform Cloud with the previous state? -Enter "yes" to copy and "no" to start with the existing state in Terraform Cloud. +Do you want to overwrite the state in %[4]s with the previous state? +Enter "yes" to copy and "no" to start with the existing state in %. ` const inputBackendMigrateMultiToSingle = ` diff --git a/internal/command/meta_backend_migrate_test.go b/internal/command/meta_backend_migrate_test.go index 24e5d9df9e..ae546f805f 100644 --- a/internal/command/meta_backend_migrate_test.go +++ b/internal/command/meta_backend_migrate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -48,7 +51,7 @@ func TestBackendMigrate_promptMultiStatePattern(t *testing.T) { } sourceType := "cloud" - _, err := m.promptMultiStateMigrationPattern(sourceType) + _, err := m.promptMultiStateMigrationPattern(sourceType, "HCP Terraform") if tc.expectedErr == "" && err != nil { t.Fatalf("expected error to be nil, but was %s", err.Error()) } diff --git a/internal/command/meta_backend_test.go b/internal/command/meta_backend_test.go index 111e136cee..e0a08750cc 100644 --- a/internal/command/meta_backend_test.go +++ b/internal/command/meta_backend_test.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( + "context" "io/ioutil" "os" "path/filepath" @@ -9,6 +13,7 @@ import ( "strings" "testing" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/configs" @@ -17,7 +22,6 @@ import ( "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/mitchellh/cli" "github.com/zclconf/go-cty/cty" backendInit "github.com/hashicorp/terraform/internal/backend/init" @@ -45,7 +49,7 @@ func TestMetaBackend_emptyDir(t *testing.T) { t.Fatalf("unexpected error: %s", err) } s.WriteState(testState()) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -132,9 +136,12 @@ func TestMetaBackend_emptyWithDefaultState(t *testing.T) { // Write some state next := testState() - next.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false) + next.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) s.WriteState(next) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -205,7 +212,7 @@ func TestMetaBackend_emptyWithExplicitState(t *testing.T) { next := testState() markStateForMatching(next, "bar") // just any change so it shows as different than before s.WriteState(next) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -265,7 +272,7 @@ func TestMetaBackend_configureNew(t *testing.T) { mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -339,7 +346,7 @@ func TestMetaBackend_configureNewWithState(t *testing.T) { state = states.NewState() mark := markStateForMatching(state, "changing") - if err := statemgr.WriteAndPersist(s, state); err != nil { + if err := statemgr.WriteAndPersist(s, state, nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -505,7 +512,7 @@ func TestMetaBackend_configureNewWithStateExisting(t *testing.T) { mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -576,7 +583,7 @@ func TestMetaBackend_configureNewWithStateExistingNoMigrate(t *testing.T) { state = states.NewState() mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -695,7 +702,7 @@ func TestMetaBackend_configuredChange(t *testing.T) { mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1448,7 +1455,7 @@ func TestMetaBackend_configuredUnset(t *testing.T) { // Write some state s.WriteState(testState()) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1506,7 +1513,7 @@ func TestMetaBackend_configuredUnsetCopy(t *testing.T) { // Write some state s.WriteState(testState()) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1546,7 +1553,7 @@ func TestMetaBackend_planLocal(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.BackendForPlan(backendConfig) + b, diags := m.BackendForLocalPlan(backendConfig) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1585,7 +1592,7 @@ func TestMetaBackend_planLocal(t *testing.T) { mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1647,7 +1654,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { m.stateOutPath = statePath // Get the backend - b, diags := m.BackendForPlan(plannedBackend) + b, diags := m.BackendForLocalPlan(plannedBackend) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1686,7 +1693,7 @@ func TestMetaBackend_planLocalStatePath(t *testing.T) { mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1736,7 +1743,7 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { m := testMetaBackend(t, nil) // Get the backend - b, diags := m.BackendForPlan(backendConfig) + b, diags := m.BackendForLocalPlan(backendConfig) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1773,7 +1780,7 @@ func TestMetaBackend_planLocalMatch(t *testing.T) { mark := markStateForMatching(state, "changing") s.WriteState(state) - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("unexpected error: %s", err) } @@ -1858,7 +1865,10 @@ func TestMetaBackend_localDoesNotDeleteLocal(t *testing.T) { // // create our local state orig := states.NewState() - orig.Module(addrs.RootModuleInstance).SetOutputValue("foo", cty.StringVal("bar"), false) + orig.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) testStateFileDefault(t, orig) m := testMetaBackend(t, nil) @@ -1936,7 +1946,7 @@ func TestBackendFromState(t *testing.T) { // them to match just for this test. wd.OverrideDataDir(".") - stateBackend, diags := m.backendFromState() + stateBackend, diags := m.backendFromState(context.Background()) if diags.HasErrors() { t.Fatal(diags.Err()) } diff --git a/internal/command/meta_config.go b/internal/command/meta_config.go index 1df46d0493..922518f7ab 100644 --- a/internal/command/meta_config.go +++ b/internal/command/meta_config.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,17 +12,18 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform-config-inspect/tfconfig" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/earlyconfig" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" ) // normalizePath normalizes a given path so that it is, if possible, relative @@ -32,7 +36,7 @@ func (m *Meta) normalizePath(path string) string { } // loadConfig reads a configuration from the given directory, which should -// contain a root module and have already have any required descendent modules +// contain a root module and have already have any required descendant modules // installed. func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -49,6 +53,23 @@ func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) return config, diags } +// loadConfigWithTests matches loadConfig, except it also loads any test files +// into the config alongside the main configuration. +func (m *Meta) loadConfigWithTests(rootDir, testDir string) (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + rootDir = m.normalizePath(rootDir) + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + config, hclDiags := loader.LoadConfigWithTests(rootDir, testDir) + diags = diags.Append(hclDiags) + return config, diags +} + // loadSingleModule reads configuration from the given directory and returns // a description of that module only, without attempting to assemble a module // tree for referenced child modules. @@ -72,28 +93,20 @@ func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostic return module, diags } -// loadSingleModuleEarly is a variant of loadSingleModule that uses the special -// "early config" loader that is more forgiving of unexpected constructs and -// legacy syntax. -// -// Early-loaded config is not registered in the source code cache, so -// diagnostics produced from it may render without source code snippets. In -// practice this is not a big concern because the early config loader also -// cannot generate detailed source locations, so it prefers to produce -// diagnostics without explicit source location information and instead includes -// approximate locations in the message text. -// -// Most callers should use loadConfig. This method exists to support early -// initialization use-cases where the root module must be inspected in order -// to determine what else needs to be installed before the full configuration -// can be used. -func (m *Meta) loadSingleModuleEarly(dir string) (*tfconfig.Module, tfdiags.Diagnostics) { +// loadSingleModuleWithTests matches loadSingleModule except it also loads any +// tests for the target module. +func (m *Meta) loadSingleModuleWithTests(dir string, testDir string) (*configs.Module, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics dir = m.normalizePath(dir) - module, moreDiags := earlyconfig.LoadModule(dir) - diags = diags.Append(moreDiags) + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return nil, diags + } + module, hclDiags := loader.Parser().LoadConfigDirWithTests(dir, testDir) + diags = diags.Append(hclDiags) return module, diags } @@ -164,13 +177,16 @@ func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) { } // installModules reads a root module from the given directory and attempts -// recursively to install all of its descendent modules. +// recursively to install all of its descendant modules. // // The given hooks object will be notified of installation progress, which // can then be relayed to the end-user. The uiModuleInstallHooks type in // this package has a reasonable implementation for displaying notifications // via a provided cli.Ui. -func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) { +func (m *Meta) installModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) { + ctx, span := tracer.Start(ctx, "install modules") + defer span.End() + rootDir = m.normalizePath(rootDir) err := os.MkdirAll(m.modulesDir(), os.ModePerm) @@ -179,18 +195,20 @@ func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleI return true, diags } - inst := m.moduleInstaller() + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return true, diags + } - // Installation can be aborted by interruption signals - ctx, done := m.InterruptibleContext() - defer done() + inst := initwd.NewModuleInstaller(m.modulesDir(), loader, m.registryClient()) - _, moreDiags := inst.InstallModules(ctx, rootDir, upgrade, hooks) + _, moreDiags := inst.InstallModules(ctx, rootDir, testsDir, upgrade, installErrsOnly, hooks) diags = diags.Append(moreDiags) if ctx.Err() == context.Canceled { m.showDiagnostics(diags) - m.Ui.Error("Module installation was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Module installation was canceled by an interrupt signal.")) return true, diags } @@ -206,17 +224,24 @@ func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleI // can then be relayed to the end-user. The uiModuleInstallHooks type in // this package has a reasonable implementation for displaying notifications // via a provided cli.Ui. -func (m *Meta) initDirFromModule(targetDir string, addr string, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) { - // Installation can be aborted by interruption signals - ctx, done := m.InterruptibleContext() - defer done() +func (m *Meta) initDirFromModule(ctx context.Context, targetDir string, addr string, hooks initwd.ModuleInstallHooks) (abort bool, diags tfdiags.Diagnostics) { + ctx, span := tracer.Start(ctx, "initialize directory from module", trace.WithAttributes( + attribute.String("source_addr", addr), + )) + defer span.End() + + loader, err := m.initConfigLoader() + if err != nil { + diags = diags.Append(err) + return true, diags + } targetDir = m.normalizePath(targetDir) - moreDiags := initwd.DirFromModule(ctx, targetDir, m.modulesDir(), addr, m.registryClient(), hooks) + moreDiags := initwd.DirFromModule(ctx, loader, targetDir, m.modulesDir(), addr, m.registryClient(), hooks) diags = diags.Append(moreDiags) if ctx.Err() == context.Canceled { m.showDiagnostics(diags) - m.Ui.Error("Module initialization was canceled by an interrupt signal.") + diags = diags.Append(fmt.Errorf("Module initialization was canceled by an interrupt signal.")) return true, diags } return false, diags @@ -343,13 +368,6 @@ func (m *Meta) initConfigLoader() (*configload.Loader, error) { return m.configLoader, nil } -// moduleInstaller instantiates and returns a module installer for use by -// "terraform init" (directly or indirectly). -func (m *Meta) moduleInstaller() *initwd.ModuleInstaller { - reg := m.registryClient() - return initwd.NewModuleInstaller(m.modulesDir(), reg) -} - // registryClient instantiates and returns a new Terraform Registry client. func (m *Meta) registryClient() *registry.Client { return registry.NewClient(m.Services, nil) @@ -393,60 +411,3 @@ func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty. return val, diags } } - -// rawFlags is a flag.Value implementation that just appends raw flag -// names and values to a slice. -type rawFlags struct { - flagName string - items *[]rawFlag -} - -func newRawFlags(flagName string) rawFlags { - var items []rawFlag - return rawFlags{ - flagName: flagName, - items: &items, - } -} - -func (f rawFlags) Empty() bool { - if f.items == nil { - return true - } - return len(*f.items) == 0 -} - -func (f rawFlags) AllItems() []rawFlag { - if f.items == nil { - return nil - } - return *f.items -} - -func (f rawFlags) Alias(flagName string) rawFlags { - return rawFlags{ - flagName: flagName, - items: f.items, - } -} - -func (f rawFlags) String() string { - return "" -} - -func (f rawFlags) Set(str string) error { - *f.items = append(*f.items, rawFlag{ - Name: f.flagName, - Value: str, - }) - return nil -} - -type rawFlag struct { - Name string - Value string -} - -func (f rawFlag) String() string { - return fmt.Sprintf("%s=%q", f.Name, f.Value) -} diff --git a/internal/command/meta_dependencies.go b/internal/command/meta_dependencies.go index 1b0cb97f8d..e7f213e43d 100644 --- a/internal/command/meta_dependencies.go +++ b/internal/command/meta_dependencies.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/meta_new.go b/internal/command/meta_new.go index b89760a4c0..f6bb98eb3f 100644 --- a/internal/command/meta_new.go +++ b/internal/command/meta_new.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -24,14 +27,15 @@ func (m *Meta) Input() bool { return true } -// PlanFile returns a reader for the plan file at the given path. +// PlanFile loads the plan file at the given path, which might be either a local +// or cloud plan. // // If the return value and error are both nil, the given path exists but seems // to be a configuration directory instead. // // Error will be non-nil if path refers to something which looks like a plan // file and loading the file fails. -func (m *Meta) PlanFile(path string) (*planfile.Reader, error) { +func (m *Meta) PlanFile(path string) (*planfile.WrappedPlanFile, error) { fi, err := os.Stat(path) if err != nil { return nil, err @@ -42,5 +46,5 @@ func (m *Meta) PlanFile(path string) (*planfile.Reader, error) { return nil, nil } - return planfile.Open(path) + return planfile.OpenWrapped(path) } diff --git a/internal/command/meta_providers.go b/internal/command/meta_providers.go index c406e4745f..1448980683 100644 --- a/internal/command/meta_providers.go +++ b/internal/command/meta_providers.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -15,7 +18,6 @@ import ( terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/logging" - "github.com/hashicorp/terraform/internal/moduletest" tfplugin "github.com/hashicorp/terraform/internal/plugin" tfplugin6 "github.com/hashicorp/terraform/internal/plugin6" "github.com/hashicorp/terraform/internal/providercache" @@ -63,6 +65,7 @@ func (m *Meta) providerInstallerCustomSource(source getproviders.Source) *provid inst := providercache.NewInstaller(targetDir, source) if globalCacheDir != nil { inst.SetGlobalCacheDir(globalCacheDir) + inst.SetGlobalCacheDirMayBreakDependencyLockFile(m.PluginCacheMayBreakDependencyLockFile) } var builtinProviderTypes []string for ty := range m.internalProviders() { @@ -214,6 +217,35 @@ func (m *Meta) providerDevOverrideRuntimeWarnings() tfdiags.Diagnostics { } } +// providerDevOverrideRuntimeWarningsRemoteExecution returns a diagnostics that contains at +// least one warning if and only if there is at least one provider development +// override in effect. If not, the result is always empty. The result never +// contains error diagnostics. +// +// Certain commands when executing remotely can use this to include a warning that any provider overrides +// are only locally configured, meaning the remote operation won't be affected by them. +// +// See providerDevOverrideRuntimeWarnings for runtime warnings specific to local execution +func (m *Meta) providerDevOverrideRuntimeWarningsRemoteExecution() tfdiags.Diagnostics { + if len(m.ProviderDevOverrides) == 0 { + return nil + } + var detailMsg strings.Builder + detailMsg.WriteString("The following provider development overrides are set in the CLI configuration:\n") + for addr, path := range m.ProviderDevOverrides { + detailMsg.WriteString(fmt.Sprintf(" - %s in %s\n", addr.ForDisplay(), path)) + } + detailMsg.WriteString("\nProvider development overrides are only configured locally and the remote operation won't be affected by them") + + return tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Provider development overrides are in effect", + detailMsg.String(), + ), + } +} + // providerFactories uses the selections made previously by an installer in // the local cache directory (m.providerLocalCacheDir) to produce a map // from provider addresses to factory functions to create instances of @@ -337,9 +369,6 @@ func (m *Meta) internalProviders() map[string]providers.Factory { "terraform": func() (providers.Interface, error) { return terraformProvider.NewProvider(), nil }, - "test": func() (providers.Interface, error) { - return moduletest.NewProvider(), nil - }, } } @@ -378,18 +407,26 @@ func providerFactory(meta *providercache.CachedProvider) providers.Factory { // store the client so that the plugin can kill the child process protoVer := client.NegotiatedVersion() - switch protoVer { - case 5: - p := raw.(*tfplugin.GRPCProvider) - p.PluginClient = client - return p, nil - case 6: - p := raw.(*tfplugin6.GRPCProvider) - p.PluginClient = client - return p, nil - default: - panic("unsupported protocol version") - } + return finalizeFactoryPlugin(raw, protoVer, meta.Provider, client), nil + } +} + +// finalizeFactoryPlugin completes the setup of a plugin dispensed by the rpc +// client to be returned by the plugin factory. +func finalizeFactoryPlugin(rawPlugin any, protoVersion int, addr addrs.Provider, client *plugin.Client) providers.Interface { + switch protoVersion { + case 5: + p := rawPlugin.(*tfplugin.GRPCProvider) + p.PluginClient = client + p.Addr = addr + return p + case 6: + p := rawPlugin.(*tfplugin6.GRPCProvider) + p.PluginClient = client + p.Addr = addr + return p + default: + panic("unsupported protocol version") } } @@ -458,13 +495,9 @@ func unmanagedProviderFactory(provider addrs.Provider, reattach *plugin.Reattach // go-plugin), so client.NegotiatedVersion() always returns 0. We // assume that an unmanaged provider reporting protocol version 0 is // actually using proto v5 for backwards compatibility. - p := raw.(*tfplugin.GRPCProvider) - p.PluginClient = client - return p, nil + return finalizeFactoryPlugin(raw, 5, provider, client), nil case 6: - p := raw.(*tfplugin6.GRPCProvider) - p.PluginClient = client - return p, nil + return finalizeFactoryPlugin(raw, 6, provider, client), nil default: return nil, fmt.Errorf("unsupported protocol version %d", protoVer) } diff --git a/internal/command/meta_test.go b/internal/command/meta_test.go index b8b6c83372..4b448f77a7 100644 --- a/internal/command/meta_test.go +++ b/internal/command/meta_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -11,10 +14,10 @@ import ( "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/terraform" - "github.com/mitchellh/cli" ) func TestMetaColorize(t *testing.T) { @@ -216,6 +219,60 @@ func TestMeta_Env(t *testing.T) { } } +func TestMeta_StatePersistInterval(t *testing.T) { + m := new(Meta) + t.Run("when the env var is not defined", func(t *testing.T) { + interval := m.StatePersistInterval() + if interval != DefaultStatePersistInterval { + t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval) + } + }) + t.Run("with valid interval greater than the default", func(t *testing.T) { + os.Setenv(StatePersistIntervalEnvVar, "25") + t.Cleanup(func() { + os.Unsetenv(StatePersistIntervalEnvVar) + }) + + interval := m.StatePersistInterval() + if interval != 25 { + t.Fatalf("expected state persist interval to be 25, got: %d", interval) + } + }) + t.Run("with a valid interval less than the default", func(t *testing.T) { + os.Setenv(StatePersistIntervalEnvVar, "10") + t.Cleanup(func() { + os.Unsetenv(StatePersistIntervalEnvVar) + }) + + interval := m.StatePersistInterval() + if interval != DefaultStatePersistInterval { + t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval) + } + }) + t.Run("with invalid integer interval", func(t *testing.T) { + os.Setenv(StatePersistIntervalEnvVar, "foo") + t.Cleanup(func() { + os.Unsetenv(StatePersistIntervalEnvVar) + }) + + interval := m.StatePersistInterval() + if interval != DefaultStatePersistInterval { + t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval) + } + }) + t.Run("with negative integer interval", func(t *testing.T) { + os.Setenv(StatePersistIntervalEnvVar, "-10") + t.Cleanup(func() { + os.Unsetenv(StatePersistIntervalEnvVar) + }) + + interval := m.StatePersistInterval() + if interval != DefaultStatePersistInterval { + t.Fatalf("expected state persist interval to be %d, got: %d", DefaultStatePersistInterval, interval) + } + }) +} + func TestMeta_Workspace_override(t *testing.T) { defer func(value string) { os.Setenv(WorkspaceNameEnvVar, value) diff --git a/internal/command/meta_vars.go b/internal/command/meta_vars.go index f082daa0c7..9ca10e41aa 100644 --- a/internal/command/meta_vars.go +++ b/internal/command/meta_vars.go @@ -1,15 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "io/ioutil" "os" + "path/filepath" "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" hcljson "github.com/hashicorp/hcl/v2/json" - "github.com/hashicorp/terraform/internal/backend" + + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" @@ -19,16 +24,77 @@ import ( // for root module input variables. const VarEnvPrefix = "TF_VAR_" -// collectVariableValues inspects the various places that root module input variable +// collectVariableValuesForTests inspects the various places that test // values can come from and constructs a map ready to be passed to the -// backend as part of a backend.Operation. +// backend as part of a backendrun.Operation. // // This method returns diagnostics relating to the collection of the values, // but the values themselves may produce additional diagnostics when finally // parsed. -func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue, tfdiags.Diagnostics) { +func (m *Meta) collectVariableValuesForTests(testsFilePath string) (map[string]backendrun.UnparsedVariableValue, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - ret := map[string]backend.UnparsedVariableValue{} + ret := map[string]backendrun.UnparsedVariableValue{} + + // We collect the variables from the ./tests directory + // there is no other need to process environmental variables + // as this is done via collectVariableValues function + if testsFilePath == "" { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Missing test directory", + "The test directory was unspecified when it should always be set. This is a bug in Terraform - please report it.")) + return ret, diags + } + + // Firstly we collect variables from .tfvars file + testVarsFilename := filepath.Join(testsFilePath, DefaultVarsFilename) + if _, err := os.Stat(testVarsFilename); err == nil { + moreDiags := m.addVarsFromFile(testVarsFilename, terraform.ValueFromAutoFile, ret) + diags = diags.Append(moreDiags) + + } + + // Then we collect variables from .tfvars.json file + const defaultVarsFilenameJSON = DefaultVarsFilename + ".json" + testVarsFilenameJSON := filepath.Join(testsFilePath, defaultVarsFilenameJSON) + + if _, err := os.Stat(testVarsFilenameJSON); err == nil { + moreDiags := m.addVarsFromFile(testVarsFilenameJSON, terraform.ValueFromAutoFile, ret) + diags = diags.Append(moreDiags) + } + + // Also, load any variables from the *.auto.tfvars files. + if infos, err := os.ReadDir(testsFilePath); err == nil { + for _, info := range infos { + if info.IsDir() { + continue + } + + if !isAutoVarFile(info.Name()) { + continue + } + + moreDiags := m.addVarsFromFile(filepath.Join(testsFilePath, info.Name()), terraform.ValueFromAutoFile, ret) + diags = diags.Append(moreDiags) + } + } + + // Also, no need to additionally process variables from command line, + // as this is also done via collectVariableValues + + return ret, diags +} + +// collectVariableValues inspects the various places that root module input variable +// values can come from and constructs a map ready to be passed to the +// backend as part of a backendrun.Operation. +// +// This method returns diagnostics relating to the collection of the values, +// but the values themselves may produce additional diagnostics when finally +// parsed. +func (m *Meta) collectVariableValues() (map[string]backendrun.UnparsedVariableValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := map[string]backendrun.UnparsedVariableValue{} // First we'll deal with environment variables, since they have the lowest // precedence. @@ -84,13 +150,13 @@ func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue // Finally we process values given explicitly on the command line, either // as individual literal settings or as additional files to read. - for _, rawFlag := range m.variableArgs.AllItems() { - switch rawFlag.Name { + for _, flagNameValue := range m.variableArgs.AllItems() { + switch flagNameValue.Name { case "-var": // Value should be in the form "name=value", where value is a // raw string whose interpretation will depend on the variable's // parsing mode. - raw := rawFlag.Value + raw := flagNameValue.Value eq := strings.Index(raw, "=") if eq == -1 { diags = diags.Append(tfdiags.Sourceless( @@ -117,20 +183,20 @@ func (m *Meta) collectVariableValues() (map[string]backend.UnparsedVariableValue } case "-var-file": - moreDiags := m.addVarsFromFile(rawFlag.Value, terraform.ValueFromNamedFile, ret) + moreDiags := m.addVarsFromFile(flagNameValue.Value, terraform.ValueFromNamedFile, ret) diags = diags.Append(moreDiags) default: // Should never happen; always a bug in the code that built up // the contents of m.variableArgs. - diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", rawFlag.Name)) + diags = diags.Append(fmt.Errorf("unsupported variable option name %q (this is a bug in Terraform)", flagNameValue.Name)) } } return ret, diags } -func (m *Meta) addVarsFromFile(filename string, sourceType terraform.ValueSourceType, to map[string]backend.UnparsedVariableValue) tfdiags.Diagnostics { +func (m *Meta) addVarsFromFile(filename string, sourceType terraform.ValueSourceType, to map[string]backendrun.UnparsedVariableValue) tfdiags.Diagnostics { var diags tfdiags.Diagnostics src, err := ioutil.ReadFile(filename) @@ -220,7 +286,7 @@ func (m *Meta) addVarsFromFile(filename string, sourceType terraform.ValueSource return diags } -// unparsedVariableValueLiteral is a backend.UnparsedVariableValue +// unparsedVariableValueLiteral is a backendrun.UnparsedVariableValue // implementation that was actually already parsed (!). This is // intended to deal with expressions inside "tfvars" files. type unparsedVariableValueExpression struct { @@ -242,7 +308,7 @@ func (v unparsedVariableValueExpression) ParseVariableValue(mode configs.Variabl }, diags } -// unparsedVariableValueString is a backend.UnparsedVariableValue +// unparsedVariableValueString is a backendrun.UnparsedVariableValue // implementation that parses its value from a string. This can be used // to deal with values given directly on the command line and via environment // variables. diff --git a/internal/command/metadata_command.go b/internal/command/metadata_command.go new file mode 100644 index 0000000000..b4fa2522b2 --- /dev/null +++ b/internal/command/metadata_command.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "strings" + + "github.com/hashicorp/cli" +) + +// MetadataCommand is a Command implementation that just shows help for +// the subcommands nested below it. +type MetadataCommand struct { + Meta +} + +func (c *MetadataCommand) Run(args []string) int { + return cli.RunResultHelp +} + +func (c *MetadataCommand) Help() string { + helpText := ` +Usage: terraform [global options] metadata [options] [args] + + This command has subcommands for metadata related purposes. + +` + return strings.TrimSpace(helpText) +} + +func (c *MetadataCommand) Synopsis() string { + return "Metadata related commands" +} diff --git a/internal/command/metadata_functions.go b/internal/command/metadata_functions.go new file mode 100644 index 0000000000..0b5ba14f88 --- /dev/null +++ b/internal/command/metadata_functions.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/command/jsonfunction" + "github.com/hashicorp/terraform/internal/lang" + "github.com/zclconf/go-cty/cty/function" +) + +var ( + ignoredFunctions = []string{"map", "list", "core::map", "core::list"} +) + +// MetadataFunctionsCommand is a Command implementation that prints out information +// about the available functions in Terraform. +type MetadataFunctionsCommand struct { + Meta +} + +func (c *MetadataFunctionsCommand) Help() string { + return metadataFunctionsCommandHelp +} + +func (c *MetadataFunctionsCommand) Synopsis() string { + return "Show signatures and descriptions for the available functions" +} + +func (c *MetadataFunctionsCommand) Run(args []string) int { + args = c.Meta.process(args) + cmdFlags := c.Meta.defaultFlagSet("metadata functions") + var jsonOutput bool + cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output") + + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } + if err := cmdFlags.Parse(args); err != nil { + c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + return 1 + } + + if !jsonOutput { + c.Ui.Error( + "The `terraform metadata functions` command requires the `-json` flag.\n") + cmdFlags.Usage() + return 1 + } + + scope := &lang.Scope{} + funcs := scope.Functions() + filteredFuncs := make(map[string]function.Function) + for k, v := range funcs { + if isIgnoredFunction(k) { + continue + } + filteredFuncs[k] = v + } + + jsonFunctions, marshalDiags := jsonfunction.Marshal(filteredFuncs) + if marshalDiags.HasErrors() { + c.showDiagnostics(marshalDiags) + return 1 + } + c.Ui.Output(string(jsonFunctions)) + + return 0 +} + +const metadataFunctionsCommandHelp = ` +Usage: terraform [global options] metadata functions -json + + Prints out a json representation of the available function signatures. +` + +func isIgnoredFunction(name string) bool { + for _, i := range ignoredFunctions { + if i == name { + return true + } + } + return false +} diff --git a/internal/command/metadata_functions_test.go b/internal/command/metadata_functions_test.go new file mode 100644 index 0000000000..f5d0bdc24a --- /dev/null +++ b/internal/command/metadata_functions_test.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "encoding/json" + "testing" + + "github.com/hashicorp/cli" +) + +func TestMetadataFunctions_error(t *testing.T) { + ui := new(cli.MockUi) + c := &MetadataFunctionsCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + // This test will always error because it's missing the -json flag + if code := c.Run(nil); code != 1 { + t.Fatalf("expected error, got:\n%s", ui.OutputWriter.String()) + } +} + +func TestMetadataFunctions_output(t *testing.T) { + ui := new(cli.MockUi) + m := Meta{Ui: ui} + c := &MetadataFunctionsCommand{Meta: m} + + if code := c.Run([]string{"-json"}); code != 0 { + t.Fatalf("wrong exit status %d; want 0\nstderr: %s", code, ui.ErrorWriter.String()) + } + + var got functions + gotString := ui.OutputWriter.String() + err := json.Unmarshal([]byte(gotString), &got) + if err != nil { + t.Fatal(err) + } + + if len(got.Signatures) < 100 { + t.Fatalf("expected at least 100 function signatures, got %d", len(got.Signatures)) + } + + // check if one particular stable function is correct + gotMax, ok := got.Signatures["max"] + wantMax := "{\"description\":\"`max` takes one or more numbers and returns the greatest number from the set.\",\"return_type\":\"number\",\"variadic_parameter\":{\"name\":\"numbers\",\"type\":\"number\"}}" + if !ok { + t.Fatal(`missing function signature for "max"`) + } + if string(gotMax) != wantMax { + t.Fatalf("wrong function signature for \"max\":\ngot: %q\nwant: %q", gotMax, wantMax) + } + + stderr := ui.ErrorWriter.String() + if stderr != "" { + t.Fatalf("expected empty stderr, got:\n%s", stderr) + } + + // test that ignored functions are not part of the json + for _, v := range ignoredFunctions { + _, ok := got.Signatures[v] + if ok { + t.Fatalf("found ignored function %q inside output", v) + } + } +} + +type functions struct { + FormatVersion string `json:"format_version"` + Signatures map[string]json.RawMessage `json:"function_signatures,omitempty"` +} diff --git a/internal/command/modules.go b/internal/command/modules.go new file mode 100644 index 0000000000..19eab321fa --- /dev/null +++ b/internal/command/modules.go @@ -0,0 +1,132 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/modsdir" + "github.com/hashicorp/terraform/internal/moduleref" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ModulesCommand is a Command implementation that prints out information +// about the modules declared by the current configuration. +type ModulesCommand struct { + Meta + viewType arguments.ViewType +} + +func (c *ModulesCommand) Help() string { + return modulesCommandHelp +} + +func (c *ModulesCommand) Synopsis() string { + return "Show all declared modules in a working directory" +} + +func (c *ModulesCommand) Run(rawArgs []string) int { + // Parse global view arguments + rawArgs = c.Meta.process(rawArgs) + common, rawArgs := arguments.ParseView(rawArgs) + c.View.Configure(common) + + // Parse command specific flags + args, diags := arguments.ParseModules(rawArgs) + if diags.HasErrors() { + c.View.Diagnostics(diags) + c.View.HelpPrompt("modules") + return 1 + } + c.viewType = args.ViewType + + // Set up the command's view + view := views.NewModules(c.viewType, c.View) + + rootModPath, err := ModulePath([]string{}) + if err != nil { + diags = diags.Append(err) + view.Diagnostics(diags) + return 1 + } + + // Read the root module path so we can then traverse the tree + rootModEarly, earlyConfDiags := c.loadSingleModule(rootModPath) + if rootModEarly == nil { + diags = diags.Append(errors.New("root module not found. Please run terraform init"), earlyConfDiags) + view.Diagnostics(diags) + return 1 + } + + config, confDiags := c.loadConfig(rootModPath) + // Here we check if there are any uninstalled dependencies + versionDiags := terraform.CheckCoreVersionRequirements(config) + if versionDiags.HasErrors() { + view.Diagnostics(versionDiags) + return 1 + } + + diags = diags.Append(earlyConfDiags) + if earlyConfDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + + diags = diags.Append(confDiags) + if confDiags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + + // Fetch the module manifest + internalManifest, diags := c.internalManifest() + if diags.HasErrors() { + view.Diagnostics(diags) + return 1 + } + + // Create a module reference resolver + resolver := moduleref.NewResolver(internalManifest) + + // Crawl the Terraform config and find entries with references + manifestWithRef := resolver.Resolve(config) + + // Render the new manifest with references + return view.Display(*manifestWithRef) +} + +// internalManifest will use the configuration loader to refresh and load the +// internal manifest. +func (c *ModulesCommand) internalManifest() (modsdir.Manifest, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + loader, err := c.initConfigLoader() + if err != nil { + diags = diags.Append(fmt.Errorf("Failed to initialize config loader: %w", err)) + return nil, diags + } + + if err = loader.RefreshModules(); err != nil { + diags = diags.Append(fmt.Errorf("Failed to refresh module manifest: %w", err)) + return nil, diags + } + + return loader.ModuleManifest(), diags +} + +const modulesCommandHelp = ` +Usage: terraform [global options] modules [options] + + Prints out a list of all declared Terraform modules and their resolved versions + in a Terraform working directory. + +Options: + + -json If specified, output declared Terraform modules and + their resolved versions in a machine-readable format. +` diff --git a/internal/command/modules_test.go b/internal/command/modules_test.go new file mode 100644 index 0000000000..431105fa5d --- /dev/null +++ b/internal/command/modules_test.go @@ -0,0 +1,207 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "encoding/json" + "os" + "reflect" + "runtime" + "sort" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/moduleref" +) + +func TestModules_noJsonFlag(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(dir, 0755) + testCopyDir(t, testFixturePath("modules-nested-dependencies"), dir) + ui := new(cli.MockUi) + view, done := testView(t) + defer testChdir(t, dir)() + + cmd := &ModulesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + code := cmd.Run(args) + if code != 0 { + t.Fatalf("Got a non-zero exit code: %d\n", code) + } + + actual := done(t).All() + + expectedOutputHuman := ` +Modules declared by configuration: +. +├── "other"[./mods/other] +└── "test"[./mods/test] + └── "test2"[./test2] + └── "test3"[./test3] + +` + if runtime.GOOS == "windows" { + expectedOutputHuman = ` +Modules declared by configuration: +. +├── "other"[.\mods\other] +└── "test"[.\mods\test] + └── "test2"[.\test2] + └── "test3"[.\test3] + +` + } + + if diff := cmp.Diff(expectedOutputHuman, actual); diff != "" { + t.Fatalf("unexpected output:\n%s\n", diff) + } +} + +func TestModules_noJsonFlag_noModules(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(dir, 0755) + ui := new(cli.MockUi) + view, done := testView(t) + defer testChdir(t, dir)() + + cmd := &ModulesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{} + code := cmd.Run(args) + if code != 0 { + t.Fatalf("Got a non-zero exit code: %d\n", code) + } + + actual := done(t).All() + + if diff := cmp.Diff("No modules found in configuration.\n", actual); diff != "" { + t.Fatalf("unexpected output (-want +got):\n%s", diff) + } +} + +func TestModules_fullCmd(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(dir, 0755) + testCopyDir(t, testFixturePath("modules-nested-dependencies"), dir) + + ui := new(cli.MockUi) + view, done := testView(t) + defer testChdir(t, dir)() + + cmd := &ModulesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-json"} + code := cmd.Run(args) + if code != 0 { + t.Fatalf("Got a non-zero exit code: %d\n", code) + } + + output := done(t).All() + compareJSONOutput(t, output, expectedOutputJSON) +} + +func TestModules_fullCmd_unreferencedEntries(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(dir, 0755) + testCopyDir(t, testFixturePath("modules-unreferenced-entries"), dir) + + ui := new(cli.MockUi) + view, done := testView(t) + defer testChdir(t, dir)() + + cmd := &ModulesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-json"} + code := cmd.Run(args) + if code != 0 { + t.Fatalf("Got a non-zero exit code: %d\n", code) + } + output := done(t).All() + compareJSONOutput(t, output, expectedOutputJSON) +} + +func TestModules_uninstalledModules(t *testing.T) { + dir := t.TempDir() + os.MkdirAll(dir, 0755) + testCopyDir(t, testFixturePath("modules-uninstalled-entries"), dir) + + ui := new(cli.MockUi) + view, done := testView(t) + defer testChdir(t, dir)() + + cmd := &ModulesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(testProvider()), + Ui: ui, + View: view, + }, + } + + args := []string{"-json"} + code := cmd.Run(args) + if code == 0 { + t.Fatal("Expected a non-zero exit code\n") + } + output := done(t).All() + if !strings.Contains(output, "Module not installed") { + t.Fatalf("expected to see a `not installed` error message: %s\n", output) + } + + if !strings.Contains(output, `Run "terraform init"`) { + t.Fatalf("expected error message to ask user to run terraform init: %s\n", output) + } +} + +func compareJSONOutput(t *testing.T, got string, want string) { + var expected, actual moduleref.Manifest + + if err := json.Unmarshal([]byte(got), &actual); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %v", err) + } + + if err := json.Unmarshal([]byte(want), &expected); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %v", err) + } + + sort.Slice(actual.Records, func(i, j int) bool { + return actual.Records[i].Key < actual.Records[j].Key + }) + sort.Slice(expected.Records, func(i, j int) bool { + return expected.Records[i].Key < expected.Records[j].Key + }) + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("unexpected output, got: %s\n, want:%s\n", got, want) + } +} + +var expectedOutputJSON = `{"format_version":"1.0","modules":[{"key":"test","source":"./mods/test","version":""},{"key":"test2","source":"./test2","version":""},{"key":"test3","source":"./test3","version":""},{"key":"other","source":"./mods/other","version":""}]}` diff --git a/internal/command/output.go b/internal/command/output.go index 0bcde54e8f..9b4bf9d7c3 100644 --- a/internal/command/output.go +++ b/internal/command/output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -66,6 +69,10 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu return nil, diags } + // Command can be aborted by interruption signals + ctx, done := c.InterruptibleContext(c.CommandContext()) + defer done() + // This is a read-only command c.ignoreRemoteVersionConflict(b) @@ -82,7 +89,7 @@ func (c *OutputCommand) Outputs(statePath string) (map[string]*states.OutputValu return nil, diags } - output, err := stateStore.GetRootOutputValues() + output, err := stateStore.GetRootOutputValues(ctx) if err != nil { return nil, diags.Append(err) } diff --git a/internal/command/output_test.go b/internal/command/output_test.go index d3d742c59b..28bbc6d1f2 100644 --- a/internal/command/output_test.go +++ b/internal/command/output_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/plan.go b/internal/command/plan.go index d5ffedbff6..a477b4061a 100644 --- a/internal/command/plan.go +++ b/internal/command/plan.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/tfdiags" @@ -61,10 +64,14 @@ func (c *PlanCommand) Run(rawArgs []string) int { // object state for now. c.Meta.parallelism = args.Operation.Parallelism - diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) - // Prepare the backend with the backend-specific arguments - be, beDiags := c.PrepareBackend(args.State) + be, beDiags := c.PrepareBackend(args.State, args.ViewType) + b, isRemoteBackend := be.(BackendWithRemoteTerraformVersion) + if isRemoteBackend && !b.IsLocalOperations() { + diags = diags.Append(c.providerDevOverrideRuntimeWarningsRemoteExecution()) + } else { + diags = diags.Append(c.providerDevOverrideRuntimeWarnings()) + } diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -72,7 +79,7 @@ func (c *PlanCommand) Run(rawArgs []string) int { } // Build the operation request - opReq, opDiags := c.OperationRequest(be, view, args.Operation, args.OutPath) + opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation, args.OutPath, args.GenerateConfigPath) diags = diags.Append(opDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -100,7 +107,7 @@ func (c *PlanCommand) Run(rawArgs []string) int { return 1 } - if op.Result != backend.OperationSuccess { + if op.Result != backendrun.OperationSuccess { return op.Result.ExitStatus() } if args.DetailedExitCode && !op.PlanEmpty { @@ -110,7 +117,7 @@ func (c *PlanCommand) Run(rawArgs []string) int { return op.Result.ExitStatus() } -func (c *PlanCommand) PrepareBackend(args *arguments.State) (backend.Enhanced, tfdiags.Diagnostics) { +func (c *PlanCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) { // FIXME: we need to apply the state arguments to the meta object here // because they are later used when initializing the backend. Carving a // path to pass these arguments to the functions that need them is @@ -124,7 +131,8 @@ func (c *PlanCommand) PrepareBackend(args *arguments.State) (backend.Enhanced, t // Load the backend be, beDiags := c.Backend(&BackendOpts{ - Config: backendConfig, + Config: backendConfig, + ViewType: viewType, }) diags = diags.Append(beDiags) if beDiags.HasErrors() { @@ -135,25 +143,42 @@ func (c *PlanCommand) PrepareBackend(args *arguments.State) (backend.Enhanced, t } func (c *PlanCommand) OperationRequest( - be backend.Enhanced, + be backendrun.OperationsBackend, view views.Plan, + viewType arguments.ViewType, args *arguments.Operation, planOutPath string, -) (*backend.Operation, tfdiags.Diagnostics) { + generateConfigOut string, +) (*backendrun.Operation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Build the operation - opReq := c.Operation(be) + opReq := c.Operation(be, viewType) opReq.ConfigDir = "." opReq.PlanMode = args.PlanMode opReq.Hooks = view.Hooks() opReq.PlanRefresh = args.Refresh opReq.PlanOutPath = planOutPath + opReq.GenerateConfigOut = generateConfigOut opReq.Targets = args.Targets opReq.ForceReplace = args.ForceReplace - opReq.Type = backend.OperationTypePlan + opReq.Type = backendrun.OperationTypePlan opReq.View = view.Operation() + // EXPERIMENTAL: maybe enable deferred actions + if c.AllowExperimentalFeatures { + opReq.DeferralAllowed = args.DeferralAllowed + } else if args.DeferralAllowed { + // Belated flag parse error, since we don't know about experiments + // support at actual parse time. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "The -allow-deferral flag is only valid in experimental builds of Terraform.", + )) + return nil, diags + } + var err error opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { @@ -164,7 +189,7 @@ func (c *PlanCommand) OperationRequest( return opReq, diags } -func (c *PlanCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics { +func (c *PlanCommand) GatherVariables(opReq *backendrun.Operation, args *arguments.Vars) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // FIXME the arguments package currently trivially gathers variable related @@ -175,12 +200,12 @@ func (c *PlanCommand) GatherVariables(opReq *backend.Operation, args *arguments. // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + items := make([]arguments.FlagNameValue, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } - c.Meta.variableArgs = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags @@ -240,33 +265,42 @@ Plan Customization Options: Other Options: - -compact-warnings If Terraform produces any warnings that are not - accompanied by errors, shows them in a more compact form - that includes only the summary messages. + -compact-warnings If Terraform produces any warnings that are not + accompanied by errors, shows them in a more compact + form that includes only the summary messages. - -detailed-exitcode Return detailed exit codes when the command exits. This - will change the meaning of exit codes to: - 0 - Succeeded, diff is empty (no changes) - 1 - Errored - 2 - Succeeded, there is a diff + -detailed-exitcode Return detailed exit codes when the command exits. + This will change the meaning of exit codes to: + 0 - Succeeded, diff is empty (no changes) + 1 - Errored + 2 - Succeeded, there is a diff - -input=true Ask for input for variables if not directly set. + -generate-config-out=path (Experimental) If import blocks are present in + configuration, instructs Terraform to generate HCL + for any imported resources not already present. The + configuration is written to a new file at PATH, + which must not already exist. Terraform may still + attempt to write configuration if the plan errors. - -lock=false Don't hold a state lock during the operation. This is - dangerous if others might concurrently run commands - against the same workspace. + -input=true Ask for input for variables if not directly set. - -lock-timeout=0s Duration to retry a state lock. + -lock=false Don't hold a state lock during the operation. This + is dangerous if others might concurrently run + commands against the same workspace. - -no-color If specified, output won't contain any color. + -lock-timeout=0s Duration to retry a state lock. - -out=path Write a plan file to the given path. This can be used as - input to the "apply" command. + -no-color If specified, output won't contain any color. - -parallelism=n Limit the number of concurrent operations. Defaults to 10. + -out=path Write a plan file to the given path. This can be + used as input to the "apply" command. - -state=statefile A legacy option used for the local backend only. See the - local backend's documentation for more information. + -parallelism=n Limit the number of concurrent operations. Defaults + to 10. + + -state=statefile A legacy option used for the local backend only. + See the local backend's documentation for more + information. ` return strings.TrimSpace(helpText) } diff --git a/internal/command/plan_test.go b/internal/command/plan_test.go index 9e145e6bf3..2bcaa4f155 100644 --- a/internal/command/plan_test.go +++ b/internal/command/plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -18,11 +21,12 @@ import ( "github.com/hashicorp/terraform/internal/addrs" backendinit "github.com/hashicorp/terraform/internal/backend/init" + "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -183,12 +187,53 @@ func TestPlan_noState(t *testing.T) { // Verify that the provider was called with the existing state actual := p.PlanResourceChangeRequest.PriorState - expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Block.ImpliedType()) + expected := cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Body.ImpliedType()) if !expected.RawEquals(actual) { t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) } } +func TestPlan_generatedConfigPath(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("plan-import-config-gen"), td) + defer testChdir(t, td)() + + genPath := filepath.Join(td, "generated.tf") + + p := planFixtureProvider() + view, done := testView(t) + + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_instance", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + Private: nil, + }, + }, + } + + args := []string{ + "-generate-config-out", genPath, + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + + testFileEquals(t, genPath, filepath.Join(td, "generated.tf.expected")) +} + func TestPlan_outPath(t *testing.T) { td := t.TempDir() testCopyDir(t, testFixturePath("plan"), td) @@ -275,6 +320,54 @@ func TestPlan_outPathNoChange(t *testing.T) { } } +func TestPlan_outPathWithError(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath("plan-fail-condition"), td) + defer testChdir(t, td)() + + outPath := filepath.Join(td, "test.plan") + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ + PlannedState: cty.NullVal(cty.EmptyObject), + } + + args := []string{ + "-out", outPath, + } + code := c.Run(args) + output := done(t) + if code == 0 { + t.Fatal("expected non-zero exit status", output) + } + + plan := testReadPlan(t, outPath) // will call t.Fatal itself if the file cannot be read + if !plan.Errored { + t.Fatal("plan should be marked with Errored") + } + + if plan.Checks == nil { + t.Fatal("plan contains no checks") + } + + // the checks should only contain one failure + results := plan.Checks.ConfigResults.Elements() + if len(results) != 1 { + t.Fatal("incorrect number of check results", len(results)) + } + if results[0].Value.Status != checks.StatusFail { + t.Errorf("incorrect status, got %s", results[0].Value.Status) + } +} + // When using "-out" with a backend, the plan should encode the backend config func TestPlan_outBackend(t *testing.T) { // Create a temporary working directory that is empty @@ -310,7 +403,7 @@ func TestPlan_outBackend(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -379,7 +472,7 @@ func TestPlan_outBackend(t *testing.T) { func TestPlan_refreshFalse(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() - testCopyDir(t, testFixturePath("plan"), td) + testCopyDir(t, testFixturePath("plan-existing-state"), td) defer testChdir(t, td)() p := planFixtureProvider() @@ -405,6 +498,71 @@ func TestPlan_refreshFalse(t *testing.T) { } } +func TestPlan_refreshTrue(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("plan-existing-state"), td) + defer testChdir(t, td)() + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-refresh=true", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + + if !p.ReadResourceCalled { + t.Fatalf("ReadResource should have been called") + } +} + +// A consumer relies on the fact that running +// terraform plan -refresh=false -refresh=true gives the same result as +// terraform plan -refresh=true. +// While the flag logic itself is handled by the stdlib flags package (and code +// in main() that is tested elsewhere), we verify the overall plan command +// behaviour here in case we accidentally break this with additional logic. +func TestPlan_refreshFalseRefreshTrue(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("plan-existing-state"), td) + defer testChdir(t, td)() + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-refresh=false", + "-refresh=true", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + + if !p.ReadResourceCalled { + t.Fatal("ReadResource should have been called") + } +} + func TestPlan_state(t *testing.T) { // Create a temporary working directory that is empty td := t.TempDir() @@ -437,10 +595,10 @@ func TestPlan_state(t *testing.T) { expected := cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar"), "ami": cty.NullVal(cty.String), - "network_interface": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{ "device_index": cty.String, "description": cty.String, - }))), + })), }) if !expected.RawEquals(actual) { t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) @@ -479,10 +637,10 @@ func TestPlan_stateDefault(t *testing.T) { expected := cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("bar"), "ami": cty.NullVal(cty.String), - "network_interface": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "network_interface": cty.ListValEmpty(cty.Object(map[string]cty.Type{ "device_index": cty.String, "description": cty.String, - }))), + })), }) if !expected.RawEquals(actual) { t.Fatalf("wrong prior state\ngot: %#v\nwant: %#v", actual, expected) @@ -502,7 +660,7 @@ func TestPlan_validate(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -673,7 +831,7 @@ func TestPlan_providerArgumentUnset(t *testing.T) { // override the planFixtureProvider schema to include a required provider argument p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Required: true}, }, @@ -681,7 +839,7 @@ func TestPlan_providerArgumentUnset(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true, Computed: true}, @@ -702,7 +860,7 @@ func TestPlan_providerArgumentUnset(t *testing.T) { }, DataSources: map[string]providers.Schema{ "test_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -752,7 +910,7 @@ func TestPlan_providerConfigMerge(t *testing.T) { // override the planFixtureProvider schema to include a required provider argument and a nested block p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Required: true}, "url": {Type: cty.String, Required: true}, @@ -772,7 +930,7 @@ func TestPlan_providerConfigMerge(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, @@ -1047,7 +1205,7 @@ func TestPlan_shutdown(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "ami": {Type: cty.String, Optional: true}, }, @@ -1104,7 +1262,7 @@ func TestPlan_targeted(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, @@ -1209,7 +1367,7 @@ func TestPlan_replace(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, @@ -1271,6 +1429,10 @@ func TestPlan_parallelism(t *testing.T) { // to proceed in unison. beginCtx, begin := context.WithCancel(context.Background()) + // This just makes go vet happy, in reality the function will never exit if + // begin() isn't called inside ApplyResourceChangeFn. + defer begin() + // Since our mock provider has its own mutex preventing concurrent calls // to ApplyResourceChange, we need to use a number of separate providers // here. They will all have the same mock implementation function assigned @@ -1278,10 +1440,10 @@ func TestPlan_parallelism(t *testing.T) { providerFactories := map[addrs.Provider]providers.Factory{} for i := 0; i < 10; i++ { name := fmt.Sprintf("test%d", i) - provider := &terraform.MockProvider{} + provider := &testing_provider.MockProvider{} provider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ - name + "_instance": {Block: &configschema.Block{}}, + name + "_instance": {Body: &configschema.Block{}}, }, } provider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { @@ -1432,7 +1594,7 @@ func planFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -1453,7 +1615,7 @@ func planFixtureSchema() *providers.GetProviderSchemaResponse { }, DataSources: map[string]providers.Schema{ "test_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -1474,7 +1636,7 @@ func planFixtureSchema() *providers.GetProviderSchemaResponse { // operation with the configuration in testdata/plan. This mock has // GetSchemaResponse and PlanResourceChangeFn populated, with the plan // step just passing through the new object proposed by Terraform Core. -func planFixtureProvider() *terraform.MockProvider { +func planFixtureProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = planFixtureSchema() p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { @@ -1500,7 +1662,7 @@ func planVarsFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "value": {Type: cty.String, Optional: true}, @@ -1515,7 +1677,7 @@ func planVarsFixtureSchema() *providers.GetProviderSchemaResponse { // operation with the configuration in testdata/plan-vars. This mock has // GetSchemaResponse and PlanResourceChangeFn populated, with the plan // step just passing through the new object proposed by Terraform Core. -func planVarsFixtureProvider() *terraform.MockProvider { +func planVarsFixtureProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = planVarsFixtureSchema() p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { @@ -1537,7 +1699,7 @@ func planVarsFixtureProvider() *terraform.MockProvider { // planFixtureProvider returns a mock provider that is configured for basic // operation with the configuration in testdata/plan. This mock has // GetSchemaResponse and PlanResourceChangeFn populated, returning 3 warnings. -func planWarningsFixtureProvider() *terraform.MockProvider { +func planWarningsFixtureProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = planFixtureSchema() p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { diff --git a/internal/command/plugins.go b/internal/command/plugins.go index 7467b09db9..99e48fd5eb 100644 --- a/internal/command/plugins.go +++ b/internal/command/plugins.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/plugins_lock.go b/internal/command/plugins_lock.go index 03f11b71d2..445542df87 100644 --- a/internal/command/plugins_lock.go +++ b/internal/command/plugins_lock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/plugins_lock_test.go b/internal/command/plugins_lock_test.go index 730346e462..97ffa4ca73 100644 --- a/internal/command/plugins_lock_test.go +++ b/internal/command/plugins_lock_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/plugins_test.go b/internal/command/plugins_test.go index 015a47d364..7b1edfbf2b 100644 --- a/internal/command/plugins_test.go +++ b/internal/command/plugins_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/providers.go b/internal/command/providers.go index c55a91774a..d0695e23c8 100644 --- a/internal/command/providers.go +++ b/internal/command/providers.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "path/filepath" + "strings" "github.com/xlab/treeprint" @@ -26,8 +30,11 @@ func (c *ProvidersCommand) Synopsis() string { } func (c *ProvidersCommand) Run(args []string) int { + var testsDirectory string + args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers") + cmdFlags.StringVar(&testsDirectory, "test-directory", "tests", "test-directory") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) @@ -42,7 +49,7 @@ func (c *ProvidersCommand) Run(args []string) int { var diags tfdiags.Diagnostics - empty, err := configs.IsEmptyDir(configPath) + empty, err := configs.IsEmptyDir(configPath, testsDirectory) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -66,7 +73,7 @@ func (c *ProvidersCommand) Run(args []string) int { return 1 } - config, configDiags := c.loadConfig(configPath) + config, configDiags := c.loadConfigWithTests(configPath, testsDirectory) diags = diags.Append(configDiags) if configDiags.HasErrors() { c.showDiagnostics(diags) @@ -143,6 +150,24 @@ func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.M } tree.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr)) } + for name, testNode := range node.Tests { + name = strings.TrimSuffix(name, ".tftest.hcl") + name = strings.ReplaceAll(name, "/", ".") + branch := tree.AddBranch(fmt.Sprintf("test.%s", name)) + + for fqn, dep := range testNode.Requirements { + versionsStr := getproviders.VersionConstraintsString(dep) + if versionsStr != "" { + versionsStr = " " + versionsStr + } + branch.AddNode(fmt.Sprintf("provider[%s]%s", fqn.String(), versionsStr)) + } + + for _, run := range testNode.Runs { + branch := branch.AddBranch(fmt.Sprintf("run.%s", run.Name)) + c.populateTreeNode(branch, run) + } + } for name, childNode := range node.Children { branch := tree.AddBranch(fmt.Sprintf("module.%s", name)) c.populateTreeNode(branch, childNode) @@ -150,7 +175,7 @@ func (c *ProvidersCommand) populateTreeNode(tree treeprint.Tree, node *configs.M } const providersCommandHelp = ` -Usage: terraform [global options] providers [DIR] +Usage: terraform [global options] providers [options] [DIR] Prints out a tree of modules in the referenced configuration annotated with their provider requirements. @@ -158,4 +183,8 @@ Usage: terraform [global options] providers [DIR] This provides an overview of all of the provider requirements across all referenced modules, as an aid to understanding why particular provider plugins are needed and why particular versions are selected. + +Options: + + -test-directory=path Set the Terraform test directory, defaults to "tests". ` diff --git a/internal/command/providers_lock.go b/internal/command/providers_lock.go index 7dcf20db11..2571bbe33a 100644 --- a/internal/command/providers_lock.go +++ b/internal/command/providers_lock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -7,6 +10,7 @@ import ( "os" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/providercache" @@ -37,12 +41,14 @@ func (c *ProvidersLockCommand) Synopsis() string { func (c *ProvidersLockCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers lock") - var optPlatforms FlagStringSlice + var optPlatforms arguments.FlagStringSlice var fsMirrorDir string var netMirrorURL string + cmdFlags.Var(&optPlatforms, "platform", "target platform") cmdFlags.StringVar(&fsMirrorDir, "fs-mirror", "", "filesystem mirror directory") cmdFlags.StringVar(&netMirrorURL, "net-mirror", "", "network mirror base URL") + pluginCache := cmdFlags.Bool("enable-plugin-cache", false, "") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) @@ -82,6 +88,10 @@ func (c *ProvidersLockCommand) Run(args []string) int { } } + // Installation steps can be cancelled by SIGINT and similar. + ctx, done := c.InterruptibleContext(c.CommandContext()) + defer done() + // Unlike other commands, this command ignores the installation methods // selected in the CLI configuration and instead chooses an installation // method based on CLI options. @@ -182,8 +192,6 @@ func (c *ProvidersLockCommand) Run(args []string) int { // merge all of the generated locks together at the end. updatedLocks := map[getproviders.Platform]*depsfile.Locks{} selectedVersions := map[addrs.Provider]getproviders.Version{} - ctx, cancel := c.InterruptibleContext() - defer cancel() for _, platform := range platforms { tempDir, err := ioutil.TempDir("", "terraform-providers-lock") if err != nil { @@ -241,6 +249,13 @@ func (c *ProvidersLockCommand) Run(args []string) int { dir := providercache.NewDirWithPlatform(tempDir, platform) installer := providercache.NewInstaller(dir, source) + // Use global plugin cache for extra speed if present and flag is set + globalCacheDir := c.providerGlobalCacheDir() + if *pluginCache && globalCacheDir != nil { + installer.SetGlobalCacheDir(globalCacheDir.WithPlatform(platform)) + installer.SetGlobalCacheDirMayBreakDependencyLockFile(c.PluginCacheMayBreakDependencyLockFile) + } + newLocks, err := installer.EnsureProviderVersions(ctx, oldLocks, reqs, providercache.InstallNewProvidersForce) if err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -365,38 +380,42 @@ Usage: terraform [global options] providers lock [options] [providers...] Options: - -fs-mirror=dir Consult the given filesystem mirror directory instead - of the origin registry for each of the given providers. + -fs-mirror=dir Consult the given filesystem mirror directory instead + of the origin registry for each of the given providers. - This would be necessary to generate lock file entries for - a provider that is available only via a mirror, and not - published in an upstream registry. In this case, the set - of valid checksums will be limited only to what Terraform - can learn from the data in the mirror directory. + This would be necessary to generate lock file entries for + a provider that is available only via a mirror, and not + published in an upstream registry. In this case, the set + of valid checksums will be limited only to what Terraform + can learn from the data in the mirror directory. - -net-mirror=url Consult the given network mirror (given as a base URL) - instead of the origin registry for each of the given - providers. + -net-mirror=url Consult the given network mirror (given as a base URL) + instead of the origin registry for each of the given + providers. - This would be necessary to generate lock file entries for - a provider that is available only via a mirror, and not - published in an upstream registry. In this case, the set - of valid checksums will be limited only to what Terraform - can learn from the data in the mirror indices. + This would be necessary to generate lock file entries for + a provider that is available only via a mirror, and not + published in an upstream registry. In this case, the set + of valid checksums will be limited only to what Terraform + can learn from the data in the mirror indices. - -platform=os_arch Choose a target platform to request package checksums - for. + -platform=os_arch Choose a target platform to request package checksums + for. - By default Terraform will request package checksums - suitable only for the platform where you run this - command. Use this option multiple times to include - checksums for multiple target systems. + By default Terraform will request package checksums + suitable only for the platform where you run this + command. Use this option multiple times to include + checksums for multiple target systems. - Target names consist of an operating system and a CPU - architecture. For example, "linux_amd64" selects the - Linux operating system running on an AMD64 or x86_64 - CPU. Each provider is available only for a limited - set of target platforms. + Target names consist of an operating system and a CPU + architecture. For example, "linux_amd64" selects the + Linux operating system running on an AMD64 or x86_64 + CPU. Each provider is available only for a limited + set of target platforms. + + -enable-plugin-cache Enable the usage of the globally configured plugin cache. + This will speed up the locking process, but the providers + wont be loaded from an authoritative source. ` } diff --git a/internal/command/providers_lock_test.go b/internal/command/providers_lock_test.go index 5ba792b7e6..dba0650cf9 100644 --- a/internal/command/providers_lock_test.go +++ b/internal/command/providers_lock_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -8,10 +11,10 @@ import ( "strings" "testing" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" - "github.com/mitchellh/cli" ) func TestProvidersLock(t *testing.T) { diff --git a/internal/command/providers_mirror.go b/internal/command/providers_mirror.go index 5890ed8d99..d2292742f1 100644 --- a/internal/command/providers_mirror.go +++ b/internal/command/providers_mirror.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -10,6 +13,7 @@ import ( "github.com/apparentlymart/go-versions/versions" "github.com/hashicorp/go-getter" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/getproviders" "github.com/hashicorp/terraform/internal/httpclient" "github.com/hashicorp/terraform/internal/tfdiags" @@ -30,8 +34,13 @@ func (c *ProvidersMirrorCommand) Synopsis() string { func (c *ProvidersMirrorCommand) Run(args []string) int { args = c.Meta.process(args) cmdFlags := c.Meta.defaultFlagSet("providers mirror") - var optPlatforms FlagStringSlice + + var optPlatforms arguments.FlagStringSlice cmdFlags.Var(&optPlatforms, "platform", "target platform") + + var optLockFile bool + cmdFlags.BoolVar(&optLockFile, "lock-file", true, "use lock file") + cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) @@ -71,11 +80,30 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { } } + // Installation steps can be cancelled by SIGINT and similar. + ctx, done := c.InterruptibleContext(c.CommandContext()) + defer done() + config, confDiags := c.loadConfig(".") diags = diags.Append(confDiags) reqs, moreDiags := config.ProviderRequirements() diags = diags.Append(moreDiags) + // Read lock file + lockedDeps, lockedDepsDiags := c.Meta.lockedDependencies() + diags = diags.Append(lockedDepsDiags) + + // If lock file is present, validate it against configuration + if !lockedDeps.Empty() && optLockFile { + if errs := config.VerifyDependencySelections(lockedDeps); len(errs) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Inconsistent dependency lock file", + fmt.Sprintf("To update the locked dependency selections to match a changed configuration, run:\n terraform init -upgrade\n got:%v", errs), + )) + } + } + // If we have any error diagnostics already then we won't proceed further. if diags.HasErrors() { c.showDiagnostics(diags) @@ -114,8 +142,6 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { // infrequently to update a mirror, so it doesn't need to optimize away // fetches of packages that might already be present. - ctx, cancel := c.InterruptibleContext() - defer cancel() for provider, constraints := range reqs { if provider.IsBuiltIn() { c.Ui.Output(fmt.Sprintf("- Skipping %s because it is built in to Terraform CLI", provider.ForDisplay())) @@ -140,7 +166,10 @@ func (c *ProvidersMirrorCommand) Run(args []string) int { continue } selected := candidates.Newest() - if len(constraintsStr) > 0 { + if !lockedDeps.Empty() && optLockFile { + selected = lockedDeps.Provider(provider).Version() + c.Ui.Output(fmt.Sprintf(" - Selected v%s to match dependency lock file", selected.String())) + } else if len(constraintsStr) > 0 { c.Ui.Output(fmt.Sprintf(" - Selected v%s to meet constraints %s", selected.String(), constraintsStr)) } else { c.Ui.Output(fmt.Sprintf(" - Selected v%s with no constraints", selected.String())) @@ -355,5 +384,10 @@ Options: Linux operating system running on an AMD64 or x86_64 CPU. Each provider is available only for a limited set of target platforms. + + -lock-file=false Ignore the provider lock file when fetching providers. + By default the mirror command will use the version info + in the lock file if the configuration directory has been + previously initialized. ` } diff --git a/internal/command/providers_mirror_test.go b/internal/command/providers_mirror_test.go index 073dd6f308..68637e65a1 100644 --- a/internal/command/providers_mirror_test.go +++ b/internal/command/providers_mirror_test.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) // More thorough tests for providers mirror can be found in the e2etest diff --git a/internal/command/providers_schema.go b/internal/command/providers_schema.go index b4d61ec764..807ad027e9 100644 --- a/internal/command/providers_schema.go +++ b/internal/command/providers_schema.go @@ -1,10 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "os" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/jsonprovider" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -60,7 +64,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { } // We require a local backend - local, ok := b.(backend.Local) + local, ok := b.(backendrun.Local) if !ok { c.showDiagnostics(diags) // in case of any warnings in here c.Ui.Error(ErrUnsupportedLocalOp) @@ -78,7 +82,7 @@ func (c *ProvidersSchemaCommand) Run(args []string) int { } // Build the operation - opReq := c.Operation(b) + opReq := c.Operation(b, arguments.ViewJSON) opReq.ConfigDir = cwd opReq.ConfigLoader, err = c.initConfigLoader() opReq.AllowUnsetVariables = true diff --git a/internal/command/providers_schema_test.go b/internal/command/providers_schema_test.go index 9aa8f810c7..47713035bf 100644 --- a/internal/command/providers_schema_test.go +++ b/internal/command/providers_schema_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,11 +12,12 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/terraform" - "github.com/mitchellh/cli" - "github.com/zclconf/go-cty/cty" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" ) func TestProvidersSchema_error(t *testing.T) { @@ -55,9 +59,11 @@ func TestProvidersSchema_output(t *testing.T) { p := providersSchemaFixtureProvider() ui := new(cli.MockUi) + view, done := testView(t) m := Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, } @@ -66,12 +72,9 @@ func TestProvidersSchema_output(t *testing.T) { Meta: m, } if code := ic.Run([]string{}); code != 0 { - t.Fatalf("init failed\n%s", ui.ErrorWriter) + t.Fatalf("init failed\n%s", done(t).Stderr()) } - // flush the init output from the mock ui - ui.OutputWriter.Reset() - // `terraform provider schemas` command pc := &ProvidersSchemaCommand{Meta: m} if code := pc.Run([]string{"-json"}); code != 0 { @@ -113,7 +116,7 @@ type providerSchema struct { // testProvider returns a mock provider that is configured for basic // operation with the configuration in testdata/providers-schema. -func providersSchemaFixtureProvider() *terraform.MockProvider { +func providersSchemaFixtureProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = providersSchemaFixtureSchema() return p @@ -124,7 +127,7 @@ func providersSchemaFixtureProvider() *terraform.MockProvider { func providersSchemaFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Optional: true}, }, @@ -132,7 +135,7 @@ func providersSchemaFixtureSchema() *providers.GetProviderSchemaResponse { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, diff --git a/internal/command/providers_test.go b/internal/command/providers_test.go index 9d9bc9b6b9..8335d92120 100644 --- a/internal/command/providers_test.go +++ b/internal/command/providers_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,7 +8,7 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) func TestProviders(t *testing.T) { @@ -81,6 +84,7 @@ func TestProviders_modules(t *testing.T) { // first run init with mock provider sources to install the module initUi := new(cli.MockUi) + view, _ := testView(t) providerSource, close := newMockProviderSource(t, map[string][]string{ "foo": {"1.0.0"}, "bar": {"2.0.0"}, @@ -90,6 +94,7 @@ func TestProviders_modules(t *testing.T) { m := Meta{ testingOverrides: metaOverridesForProvider(testProvider()), Ui: initUi, + View: view, ProviderSource: providerSource, } ic := &InitCommand{ @@ -163,3 +168,38 @@ func TestProviders_state(t *testing.T) { } } } + +func TestProviders_tests(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + if err := os.Chdir(testFixturePath("providers/tests")); err != nil { + t.Fatalf("err: %s", err) + } + defer os.Chdir(cwd) + + ui := new(cli.MockUi) + c := &ProvidersCommand{ + Meta: Meta{ + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + wantOutput := []string{ + "test.main", + "provider[registry.terraform.io/hashicorp/bar]", + } + + output := ui.OutputWriter.String() + for _, want := range wantOutput { + if !strings.Contains(output, want) { + t.Errorf("output missing %s:\n%s", want, output) + } + } +} diff --git a/internal/command/push.go b/internal/command/push.go index ee1544926d..1fe054e2a2 100644 --- a/internal/command/push.go +++ b/internal/command/push.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/refresh.go b/internal/command/refresh.go index 546d7f0fbd..4c43d6d580 100644 --- a/internal/command/refresh.go +++ b/internal/command/refresh.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/tfdiags" @@ -64,7 +67,7 @@ func (c *RefreshCommand) Run(rawArgs []string) int { c.Meta.parallelism = args.Operation.Parallelism // Prepare the backend with the backend-specific arguments - be, beDiags := c.PrepareBackend(args.State) + be, beDiags := c.PrepareBackend(args.State, args.ViewType) diags = diags.Append(beDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -72,7 +75,7 @@ func (c *RefreshCommand) Run(rawArgs []string) int { } // Build the operation request - opReq, opDiags := c.OperationRequest(be, view, args.Operation) + opReq, opDiags := c.OperationRequest(be, view, args.ViewType, args.Operation) diags = diags.Append(opDiags) if diags.HasErrors() { view.Diagnostics(diags) @@ -101,13 +104,13 @@ func (c *RefreshCommand) Run(rawArgs []string) int { } if op.State != nil { - view.Outputs(op.State.RootModule().OutputValues) + view.Outputs(op.State.RootOutputValues) } return op.Result.ExitStatus() } -func (c *RefreshCommand) PrepareBackend(args *arguments.State) (backend.Enhanced, tfdiags.Diagnostics) { +func (c *RefreshCommand) PrepareBackend(args *arguments.State, viewType arguments.ViewType) (backendrun.OperationsBackend, tfdiags.Diagnostics) { // FIXME: we need to apply the state arguments to the meta object here // because they are later used when initializing the backend. Carving a // path to pass these arguments to the functions that need them is @@ -121,7 +124,8 @@ func (c *RefreshCommand) PrepareBackend(args *arguments.State) (backend.Enhanced // Load the backend be, beDiags := c.Backend(&BackendOpts{ - Config: backendConfig, + Config: backendConfig, + ViewType: viewType, }) diags = diags.Append(beDiags) if beDiags.HasErrors() { @@ -131,18 +135,32 @@ func (c *RefreshCommand) PrepareBackend(args *arguments.State) (backend.Enhanced return be, diags } -func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refresh, args *arguments.Operation, -) (*backend.Operation, tfdiags.Diagnostics) { +func (c *RefreshCommand) OperationRequest(be backendrun.OperationsBackend, view views.Refresh, viewType arguments.ViewType, args *arguments.Operation, +) (*backendrun.Operation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics // Build the operation - opReq := c.Operation(be) + opReq := c.Operation(be, viewType) opReq.ConfigDir = "." opReq.Hooks = view.Hooks() opReq.Targets = args.Targets - opReq.Type = backend.OperationTypeRefresh + opReq.Type = backendrun.OperationTypeRefresh opReq.View = view.Operation() + // EXPERIMENTAL: maybe enable deferred actions + if c.AllowExperimentalFeatures { + opReq.DeferralAllowed = args.DeferralAllowed + } else if args.DeferralAllowed { + // Belated flag parse error, since we don't know about experiments + // support at actual parse time. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to parse command-line flags", + "The -allow-deferral flag is only valid in experimental builds of Terraform.", + )) + return nil, diags + } + var err error opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { @@ -153,7 +171,7 @@ func (c *RefreshCommand) OperationRequest(be backend.Enhanced, view views.Refres return opReq, diags } -func (c *RefreshCommand) GatherVariables(opReq *backend.Operation, args *arguments.Vars) tfdiags.Diagnostics { +func (c *RefreshCommand) GatherVariables(opReq *backendrun.Operation, args *arguments.Vars) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // FIXME the arguments package currently trivially gathers variable related @@ -164,12 +182,12 @@ func (c *RefreshCommand) GatherVariables(opReq *backend.Operation, args *argumen // package directly, removing this shim layer. varArgs := args.All() - items := make([]rawFlag, len(varArgs)) + items := make([]arguments.FlagNameValue, len(varArgs)) for i := range varArgs { items[i].Name = varArgs[i].Name items[i].Value = varArgs[i].Value } - c.Meta.variableArgs = rawFlags{items: &items} + c.Meta.variableArgs = arguments.FlagNameValueSlice{Items: &items} opReq.Variables, diags = c.collectVariableValues() return diags diff --git a/internal/command/refresh_test.go b/internal/command/refresh_test.go index 1598bbd299..9373efd7c6 100644 --- a/internal/command/refresh_test.go +++ b/internal/command/refresh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -13,7 +16,7 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" @@ -526,7 +529,7 @@ func TestRefresh_varsUnset(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -556,7 +559,7 @@ func TestRefresh_backup(t *testing.T) { statePath := testStateFile(t, state) // Output path - outf, err := ioutil.TempFile(td, "tf") + outf, err := os.CreateTemp(td, "tf") if err != nil { t.Fatalf("err: %s", err) } @@ -571,7 +574,7 @@ func TestRefresh_backup(t *testing.T) { } // Backup path - backupf, err := ioutil.TempFile(td, "tf") + backupf, err := os.CreateTemp(td, "tf") if err != nil { t.Fatalf("err: %s", err) } @@ -726,7 +729,7 @@ func TestRefresh_displaysOutputs(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -766,7 +769,7 @@ func TestRefresh_targeted(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, @@ -924,7 +927,7 @@ func refreshFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -941,7 +944,7 @@ func refreshFixtureSchema() *providers.GetProviderSchemaResponse { func refreshVarFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -949,7 +952,7 @@ func refreshVarFixtureSchema() *providers.GetProviderSchemaResponse { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, }, diff --git a/internal/command/show.go b/internal/command/show.go index 123b86536e..9e624a915d 100644 --- a/internal/command/show.go +++ b/internal/command/show.go @@ -1,11 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( + "context" + "errors" "fmt" "os" "strings" "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/cloud" + "github.com/hashicorp/terraform/internal/cloud/cloudplan" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs" @@ -17,10 +24,29 @@ import ( "github.com/hashicorp/terraform/internal/tfdiags" ) +// Many of the methods we get data from can emit special error types if they're +// pretty sure about the file type but still can't use it. But they can't all do +// that! So, we have to do a couple ourselves if we want to preserve that data. +type errUnusableDataMisc struct { + inner error + kind string +} + +func errUnusable(err error, kind string) *errUnusableDataMisc { + return &errUnusableDataMisc{inner: err, kind: kind} +} +func (e *errUnusableDataMisc) Error() string { + return e.inner.Error() +} +func (e *errUnusableDataMisc) Unwrap() error { + return e.inner +} + // ShowCommand is a Command implementation that reads and outputs the // contents of a Terraform plan or state file. type ShowCommand struct { Meta + viewType arguments.ViewType } func (c *ShowCommand) Run(rawArgs []string) int { @@ -35,6 +61,7 @@ func (c *ShowCommand) Run(rawArgs []string) int { c.View.HelpPrompt("show") return 1 } + c.viewType = args.ViewType // Set up view view := views.NewShow(args.ViewType, c.View) @@ -48,7 +75,7 @@ func (c *ShowCommand) Run(rawArgs []string) int { } // Get the data we need to display - plan, stateFile, config, schemas, showDiags := c.show(args.Path) + plan, jsonPlan, stateFile, config, schemas, showDiags := c.show(args.Path) diags = diags.Append(showDiags) if showDiags.HasErrors() { view.Diagnostics(diags) @@ -56,7 +83,7 @@ func (c *ShowCommand) Run(rawArgs []string) int { } // Display the data - return view.Display(config, plan, stateFile, schemas) + return view.Display(config, plan, jsonPlan, stateFile, schemas) } func (c *ShowCommand) Help() string { @@ -80,9 +107,10 @@ func (c *ShowCommand) Synopsis() string { return "Show the current state or a saved plan" } -func (c *ShowCommand) show(path string) (*plans.Plan, *statefile.File, *configs.Config, *terraform.Schemas, tfdiags.Diagnostics) { +func (c *ShowCommand) show(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, *terraform.Schemas, tfdiags.Diagnostics) { var diags, showDiags tfdiags.Diagnostics var plan *plans.Plan + var jsonPlan *cloudplan.RemotePlanJSON var stateFile *statefile.File var config *configs.Config var schemas *terraform.Schemas @@ -93,7 +121,7 @@ func (c *ShowCommand) show(path string) (*plans.Plan, *statefile.File, *configs. stateFile, showDiags = c.showFromLatestStateSnapshot() diags = diags.Append(showDiags) if showDiags.HasErrors() { - return plan, stateFile, config, schemas, diags + return plan, jsonPlan, stateFile, config, schemas, diags } } @@ -101,34 +129,22 @@ func (c *ShowCommand) show(path string) (*plans.Plan, *statefile.File, *configs. // so try to load the argument as a plan file first. // If that fails, try to load it as a statefile. if path != "" { - plan, stateFile, config, showDiags = c.showFromPath(path) + plan, jsonPlan, stateFile, config, showDiags = c.showFromPath(path) diags = diags.Append(showDiags) if showDiags.HasErrors() { - return plan, stateFile, config, schemas, diags + return plan, jsonPlan, stateFile, config, schemas, diags } } // Get schemas, if possible if config != nil || stateFile != nil { - opts, err := c.contextOpts() - if err != nil { - diags = diags.Append(err) - return plan, stateFile, config, schemas, diags - } - tfCtx, ctxDiags := terraform.NewContext(opts) - diags = diags.Append(ctxDiags) - if ctxDiags.HasErrors() { - return plan, stateFile, config, schemas, diags - } - var schemaDiags tfdiags.Diagnostics - schemas, schemaDiags = tfCtx.Schemas(config, stateFile.State) - diags = diags.Append(schemaDiags) - if schemaDiags.HasErrors() { - return plan, stateFile, config, schemas, diags + schemas, diags = c.MaybeGetSchemas(stateFile.State, config) + if diags.HasErrors() { + return plan, jsonPlan, stateFile, config, schemas, diags } } - return plan, stateFile, config, schemas, diags + return plan, jsonPlan, stateFile, config, schemas, diags } func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -158,42 +174,130 @@ func (c *ShowCommand) showFromLatestStateSnapshot() (*statefile.File, tfdiags.Di return stateFile, diags } -func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config, tfdiags.Diagnostics) { +func (c *ShowCommand) showFromPath(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var planErr, stateErr error var plan *plans.Plan + var jsonPlan *cloudplan.RemotePlanJSON var stateFile *statefile.File var config *configs.Config - // Try to get the plan file and associated data from - // the path argument. If that fails, try to get the - // statefile from the path argument. - plan, stateFile, config, planErr = getPlanFromPath(path) + // Path might be a local plan file, a bookmark to a saved cloud plan, or a + // state file. First, try to get a plan and associated data from a local + // plan file. If that fails, try to get a json plan from the path argument. + // If that fails, try to get the statefile from the path argument. + plan, jsonPlan, stateFile, config, planErr = c.getPlanFromPath(path) if planErr != nil { stateFile, stateErr = getStateFromPath(path) if stateErr != nil { - diags = diags.Append( - tfdiags.Sourceless( - tfdiags.Error, - "Failed to read the given file as a state or plan file", - fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr), - ), - ) - return nil, nil, nil, diags + // To avoid spamming the user with irrelevant errors, first check to + // see if one of our errors happens to know for a fact what file + // type we were dealing with. If so, then we can ignore the other + // ones (which are likely to be something unhelpful like "not a + // valid zip file"). If not, we can fall back to dumping whatever + // we've got. + var unLocal *planfile.ErrUnusableLocalPlan + var unState *statefile.ErrUnusableState + var unMisc *errUnusableDataMisc + if errors.As(planErr, &unLocal) { + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Couldn't show local plan", + fmt.Sprintf("Plan read error: %s", unLocal), + ), + ) + } else if errors.As(planErr, &unMisc) { + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + fmt.Sprintf("Couldn't show %s", unMisc.kind), + fmt.Sprintf("Plan read error: %s", unMisc), + ), + ) + } else if errors.As(stateErr, &unState) { + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Couldn't show state file", + fmt.Sprintf("Plan read error: %s", unState), + ), + ) + } else if errors.As(stateErr, &unMisc) { + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + fmt.Sprintf("Couldn't show %s", unMisc.kind), + fmt.Sprintf("Plan read error: %s", unMisc), + ), + ) + } else { + // Ok, give up and show the really big error + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Failed to read the given file as a state or plan file", + fmt.Sprintf("State read error: %s\n\nPlan read error: %s", stateErr, planErr), + ), + ) + } + + return nil, nil, nil, nil, diags } } - return plan, stateFile, config, diags + return plan, jsonPlan, stateFile, config, diags } -// getPlanFromPath returns a plan, statefile, and config if the user-supplied -// path points to a plan file. If both plan and error are nil, the path is likely -// a directory. An error could suggest that the given path points to a statefile. -func getPlanFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config, error) { - planReader, err := planfile.Open(path) +// getPlanFromPath returns a plan, json plan, statefile, and config if the +// user-supplied path points to either a local or cloud plan file. Note that +// some of the return values will be nil no matter what; local plan files do not +// yield a json plan, and cloud plans do not yield real plan/state/config +// structs. An error generally suggests that the given path is either a +// directory or a statefile. +func (c *ShowCommand) getPlanFromPath(path string) (*plans.Plan, *cloudplan.RemotePlanJSON, *statefile.File, *configs.Config, error) { + var err error + var plan *plans.Plan + var jsonPlan *cloudplan.RemotePlanJSON + var stateFile *statefile.File + var config *configs.Config + + pf, err := planfile.OpenWrapped(path) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } + if lp, ok := pf.Local(); ok { + plan, stateFile, config, err = getDataFromPlanfileReader(lp) + } else if cp, ok := pf.Cloud(); ok { + redacted := c.viewType != arguments.ViewJSON + jsonPlan, err = c.getDataFromCloudPlan(cp, redacted) + } + + return plan, jsonPlan, stateFile, config, err +} + +func (c *ShowCommand) getDataFromCloudPlan(plan *cloudplan.SavedPlanBookmark, redacted bool) (*cloudplan.RemotePlanJSON, error) { + // Set up the backend + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + return nil, errUnusable(backendDiags.Err(), "cloud plan") + } + // Cloud plans only work if we're cloud. + cl, ok := b.(*cloud.Cloud) + if !ok { + errMessage := fmt.Sprintf("can't show a saved cloud plan unless the current root module is connected to %s", cl.AppName()) + return nil, errUnusable(errors.New(errMessage), "cloud plan") + } + + result, err := cl.ShowPlanForRun(context.Background(), plan.RunID, plan.Hostname, redacted) + if err != nil { + err = errUnusable(err, "cloud plan") + } + return result, err +} + +// getDataFromPlanfileReader returns a plan, statefile, and config, extracted from a local plan file. +func getDataFromPlanfileReader(planReader *planfile.Reader) (*plans.Plan, *statefile.File, *configs.Config, error) { // Get plan plan, err := planReader.ReadPlan() if err != nil { @@ -209,7 +313,7 @@ func getPlanFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config // Get config config, diags := planReader.ReadConfig() if diags.HasErrors() { - return nil, nil, nil, diags.Err() + return nil, nil, nil, errUnusable(diags.Err(), "local plan") } return plan, stateFile, config, err @@ -219,14 +323,14 @@ func getPlanFromPath(path string) (*plans.Plan, *statefile.File, *configs.Config func getStateFromPath(path string) (*statefile.File, error) { file, err := os.Open(path) if err != nil { - return nil, fmt.Errorf("Error loading statefile: %s", err) + return nil, fmt.Errorf("Error loading statefile: %w", err) } defer file.Close() var stateFile *statefile.File stateFile, err = statefile.Read(file) if err != nil { - return nil, fmt.Errorf("Error reading %s as a statefile: %s", path, err) + return nil, fmt.Errorf("Error reading %s as a statefile: %w", path, err) } return stateFile, nil } @@ -236,12 +340,12 @@ func getStateFromBackend(b backend.Backend, workspace string) (*statefile.File, // Get the state store for the given workspace stateStore, err := b.StateMgr(workspace) if err != nil { - return nil, fmt.Errorf("Failed to load state manager: %s", err) + return nil, fmt.Errorf("Failed to load state manager: %w", err) } // Refresh the state store with the latest state snapshot from persistent storage if err := stateStore.RefreshState(); err != nil { - return nil, fmt.Errorf("Failed to load state: %s", err) + return nil, fmt.Errorf("Failed to load state: %w", err) } // Get the latest state snapshot and return it diff --git a/internal/command/show_test.go b/internal/command/show_test.go index 6e0103e261..9059226b87 100644 --- a/internal/command/show_test.go +++ b/internal/command/show_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,16 +12,17 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/version" - "github.com/mitchellh/cli" - "github.com/zclconf/go-cty/cty" ) func TestShow_badArgs(t *testing.T) { @@ -76,7 +80,7 @@ func TestShow_noArgsWithState(t *testing.T) { view, done := testView(t) c := &ShowCommand{ Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), + testingOverrides: metaOverridesForProvider(showFixtureProvider()), View: view, }, } @@ -105,7 +109,7 @@ func TestShow_argsWithState(t *testing.T) { view, done := testView(t) c := &ShowCommand{ Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), + testingOverrides: metaOverridesForProvider(showFixtureProvider()), View: view, }, } @@ -153,7 +157,7 @@ func TestShow_argsWithStateAliasedProvider(t *testing.T) { view, done := testView(t) c := &ShowCommand{ Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), + testingOverrides: metaOverridesForProvider(showFixtureProvider()), View: view, }, } @@ -198,9 +202,13 @@ func TestShow_argsPlanFileDoesNotExist(t *testing.T) { } got := output.Stderr() - want := `Plan read error: open doesNotExist.tfplan:` - if !strings.Contains(got, want) { - t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want) + want1 := `Plan read error: couldn't load the provided path` + want2 := `open doesNotExist.tfplan: no such file or directory` + if !strings.Contains(got, want1) { + t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want1) + } + if !strings.Contains(got, want2) { + t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want2) } } @@ -253,9 +261,13 @@ func TestShow_json_argsPlanFileDoesNotExist(t *testing.T) { } got := output.Stderr() - want := `Plan read error: open doesNotExist.tfplan:` - if !strings.Contains(got, want) { - t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want) + want1 := `Plan read error: couldn't load the provided path` + want2 := `open doesNotExist.tfplan: no such file or directory` + if !strings.Contains(got, want1) { + t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want1) + } + if !strings.Contains(got, want2) { + t.Errorf("unexpected output\ngot: %s\nwant:\n%s", got, want2) } } @@ -365,7 +377,7 @@ func TestShow_planWithForceReplaceChange(t *testing.T) { t.Fatal(err) } plan := testPlan(t) - plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", @@ -418,7 +430,43 @@ func TestShow_planWithForceReplaceChange(t *testing.T) { if !strings.Contains(got, want) { t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) } +} +func TestShow_planErrored(t *testing.T) { + _, snap := testModuleWithSnapshot(t, "show") + plan := testPlan(t) + plan.Errored = true + planFilePath := testPlanFile( + t, + snap, + states.NewState(), + plan, + ) + + view, done := testView(t) + c := &ShowCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(showFixtureProvider()), + View: view, + }, + } + + args := []string{ + planFilePath, + "-no-color", + } + code := c.Run(args) + output := done(t) + + if code != 0 { + t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, output.Stderr()) + } + + got := output.Stdout() + want := `Planning failed. Terraform encountered an error while generating this plan.` + if !strings.Contains(got, want) { + t.Fatalf("unexpected output\ngot: %s\nwant: %s", got, want) + } } func TestShow_plan_json(t *testing.T) { @@ -447,13 +495,23 @@ func TestShow_plan_json(t *testing.T) { func TestShow_state(t *testing.T) { originalState := testState() + originalState.SetOutputValue( + addrs.OutputValue{Name: "test"}.Absolute(addrs.RootModuleInstance), + cty.ObjectVal(map[string]cty.Value{ + "attr": cty.NullVal(cty.DynamicPseudoType), + "null": cty.NullVal(cty.String), + "list": cty.ListVal([]cty.Value{cty.NullVal(cty.Number)}), + }), + false, + ) + statePath := testStateFile(t, originalState) defer os.RemoveAll(filepath.Dir(statePath)) view, done := testView(t) c := &ShowCommand{ Meta: Meta{ - testingOverrides: metaOverridesForProvider(testProvider()), + testingOverrides: metaOverridesForProvider(showFixtureProvider()), View: view, }, } @@ -500,10 +558,12 @@ func TestShow_json_output(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -515,6 +575,20 @@ func TestShow_json_output(t *testing.T) { t.Fatalf("init failed\n%s", ui.ErrorWriter) } + // read expected output + wantFile, err := os.Open("output.json") + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + defer wantFile.Close() + byteValue, err := ioutil.ReadAll(wantFile) + if err != nil { + t.Fatalf("unexpected err: %s", err) + } + + var want plan + json.Unmarshal([]byte(byteValue), &want) + // plan planView, planDone := testView(t) pc := &PlanCommand{ @@ -532,8 +606,15 @@ func TestShow_json_output(t *testing.T) { code := pc.Run(args) planOutput := planDone(t) - if code != 0 { - t.Fatalf("unexpected exit status %d; want 0\ngot: %s", code, planOutput.Stderr()) + var wantedCode int + if want.Errored { + wantedCode = 1 + } else { + wantedCode = 0 + } + + if code != wantedCode { + t.Fatalf("unexpected exit status %d; want %d\ngot: %s", code, wantedCode, planOutput.Stderr()) } // show @@ -559,22 +640,11 @@ func TestShow_json_output(t *testing.T) { } // compare view output to wanted output - var got, want plan + var got plan gotString := showOutput.Stdout() json.Unmarshal([]byte(gotString), &got) - wantFile, err := os.Open("output.json") - if err != nil { - t.Fatalf("unexpected err: %s", err) - } - defer wantFile.Close() - byteValue, err := ioutil.ReadAll(wantFile) - if err != nil { - t.Fatalf("unexpected err: %s", err) - } - json.Unmarshal([]byte(byteValue), &want) - // Disregard format version to reduce needless test fixture churn want.FormatVersion = got.FormatVersion @@ -598,10 +668,12 @@ func TestShow_json_output_sensitive(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -691,10 +763,12 @@ func TestShow_json_output_conditions_refresh_only(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -800,10 +874,12 @@ func TestShow_json_output_state(t *testing.T) { // init ui := new(cli.MockUi) + view, _ := testView(t) ic := &InitCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), Ui: ui, + View: view, ProviderSource: providerSource, }, } @@ -939,7 +1015,7 @@ func TestShow_corruptStatefile(t *testing.T) { func showFixtureSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Optional: true}, }, @@ -947,7 +1023,7 @@ func showFixtureSchema() *providers.GetProviderSchemaResponse { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -964,7 +1040,7 @@ func showFixtureSchema() *providers.GetProviderSchemaResponse { func showFixtureSensitiveSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": {Type: cty.String, Optional: true}, }, @@ -972,7 +1048,7 @@ func showFixtureSensitiveSchema() *providers.GetProviderSchemaResponse { }, ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "ami": {Type: cty.String, Optional: true}, @@ -989,7 +1065,7 @@ func showFixtureSensitiveSchema() *providers.GetProviderSchemaResponse { // GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated, // with the plan/apply steps just passing through the data determined by // Terraform Core. -func showFixtureProvider() *terraform.MockProvider { +func showFixtureProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = showFixtureSchema() p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { @@ -1052,7 +1128,7 @@ func showFixtureProvider() *terraform.MockProvider { // GetSchemaResponse, PlanResourceChangeFn, and ApplyResourceChangeFn populated, // with the plan/apply steps just passing through the data determined by // Terraform Core. It also has a sensitive attribute in the provider schema. -func showFixtureSensitiveProvider() *terraform.MockProvider { +func showFixtureSensitiveProvider() *testing_provider.MockProvider { p := testProvider() p.GetProviderSchemaResponse = showFixtureSensitiveSchema() p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { @@ -1103,7 +1179,7 @@ func showFixturePlanFile(t *testing.T, action plans.Action) string { t.Fatal(err) } plan := testPlan(t) - plan.Changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ Addr: addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", @@ -1140,6 +1216,9 @@ type plan struct { OutputChanges map[string]interface{} `json:"output_changes,omitempty"` PriorState priorState `json:"prior_state,omitempty"` Config map[string]interface{} `json:"configuration,omitempty"` + Applyable bool `json:"applyable"` + Complete bool `json:"complete"` + Errored bool `json:"errored"` } type priorState struct { diff --git a/internal/command/state_command.go b/internal/command/state_command.go index 5e7915b67b..5ef4cec8a3 100644 --- a/internal/command/state_command.go +++ b/internal/command/state_command.go @@ -1,9 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "strings" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) // StateCommand is a Command implementation that just shows help for diff --git a/internal/command/state_identities.go b/internal/command/state_identities.go new file mode 100644 index 0000000000..9e1879e788 --- /dev/null +++ b/internal/command/state_identities.go @@ -0,0 +1,157 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateIdentitiesCommand is a Command implementation that lists the resource identities +// within a state file. +type StateIdentitiesCommand struct { + Meta + StateMeta +} + +func (c *StateIdentitiesCommand) Run(args []string) int { + args = c.Meta.process(args) + var statePath string + var jsonOutput bool + cmdFlags := c.Meta.defaultFlagSet("state identities") + cmdFlags.StringVar(&statePath, "state", "", "path") + cmdFlags.BoolVar(&jsonOutput, "json", false, "produce JSON output") + lookupId := cmdFlags.String("id", "", "Restrict output to paths with a resource having the specified ID.") + if err := cmdFlags.Parse(args); err != nil { + c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + return cli.RunResultHelp + } + args = cmdFlags.Args() + + if !jsonOutput { + c.Ui.Error( + "The `terraform state identities` command requires the `-json` flag.\n") + cmdFlags.Usage() + return 1 + } + + if statePath != "" { + c.Meta.statePath = statePath + } + + // Load the backend + b, backendDiags := c.Backend(nil) + if backendDiags.HasErrors() { + c.showDiagnostics(backendDiags) + return 1 + } + + // This is a read-only command + c.ignoreRemoteVersionConflict(b) + + // Get the state + env, err := c.Workspace() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) + return 1 + } + stateMgr, err := b.StateMgr(env) + if err != nil { + c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) + return 1 + } + if err := stateMgr.RefreshState(); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err)) + return 1 + } + + state := stateMgr.State() + if state == nil { + c.Ui.Error(errStateNotFound) + return 1 + } + + var addrs []addrs.AbsResourceInstance + var diags tfdiags.Diagnostics + if len(args) == 0 { + addrs, diags = c.lookupAllResourceInstanceAddrs(state) + } else { + addrs, diags = c.lookupResourceInstanceAddrs(state, args...) + } + if diags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + output := make(map[string]any) + for _, addr := range addrs { + // If the resource exists but identity is nil, skip it, as it is not required to be present + if is := state.ResourceInstance(addr); is != nil && is.Current.IdentityJSON != nil { + if *lookupId == "" || *lookupId == states.LegacyInstanceObjectID(is.Current) { + var rawIdentity map[string]any + if err := json.Unmarshal(is.Current.IdentityJSON, &rawIdentity); err != nil { + c.Ui.Error(fmt.Sprintf("Failed to unmarshal identity JSON: %s", err)) + return 1 + } + output[addr.String()] = rawIdentity + } + } + } + + outputJSON, err := json.MarshalIndent(output, "", " ") + if err != nil { + c.Ui.Error(fmt.Sprintf("Failed to marshal output JSON: %s", err)) + return 1 + } + + c.Ui.Output(string(outputJSON)) + c.showDiagnostics(diags) + + return 0 +} + +func (c *StateIdentitiesCommand) Help() string { + helpText := ` +Usage: terraform [global options] state identities [options] -json [address...] + + List the json format of the identities of resources in the Terraform state. + + This command lists the identities of resource instances in the Terraform state in json format. + The address argument can be used to filter the instances by resource or module. If + no pattern is given, identities for all resource instances are listed. + + The addresses must either be module addresses or absolute resource + addresses, such as: + aws_instance.example + module.example + module.example.module.child + module.example.aws_instance.example + + An error will be returned if any of the resources or modules given as + filter addresses do not exist in the state. + +Options: + + -state=statefile Path to a Terraform state file to use to look + up Terraform-managed resources. By default, Terraform + will consult the state of the currently-selected + workspace. + + -id=ID Filters the results to include only instances whose + resource types have an attribute named "id" whose value + equals the given id string. + +` + return strings.TrimSpace(helpText) +} + +func (c *StateIdentitiesCommand) Synopsis() string { + return "List the identities of resources in the state" +} diff --git a/internal/command/state_identities_test.go b/internal/command/state_identities_test.go new file mode 100644 index 0000000000..1653f3baee --- /dev/null +++ b/internal/command/state_identities_test.go @@ -0,0 +1,424 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "encoding/json" + "os" + "path/filepath" + "reflect" + "testing" + + "github.com/hashicorp/cli" +) + +func TestStateIdentities(t *testing.T) { + state := testStateWithIdentity() + statePath := testStateFile(t, state) + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-json", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that outputs were displayed + expected := `{ + "test_instance.foo": {"id": "my-foo-id"}, + "test_instance.bar": {"id": "my-bar-id"} + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } +} + +func TestStateIdentitiesWithNoIdentityInfo(t *testing.T) { + state := testState() + statePath := testStateFile(t, state) + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-json", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that an empty output is displayed with no error + expected := `{}` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } +} + +func TestStateIdentitiesFilterByID(t *testing.T) { + state := testStateWithIdentity() + statePath := testStateFile(t, state) + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-json", + "-id", "foo", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that outputs were displayed + expected := `{ + "test_instance.foo": {"id": "my-foo-id"} + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } +} + +func TestStateIdentitiesWithNonExistentID(t *testing.T) { + state := testState() + statePath := testStateFile(t, state) + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + "-json", + "-id", "baz", + } + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that output is empty + if ui.OutputWriter != nil { + actual := ui.OutputWriter.String() + if actual != "{}\n" { + t.Fatalf("Expected an empty output but got: %q", actual) + } + } +} + +func TestStateIdentitiesWithNoJsonFlag(t *testing.T) { + state := testState() + statePath := testStateFile(t, state) + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{ + "-state", statePath, + } + // Should return an error because the -json flag is required + if code := c.Run(args); code != 1 { + t.Fatalf("expected error: \n%s", ui.OutputWriter.String()) + } +} + +func TestStateIdentities_backendDefaultState(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-identities-backend-default"), td) + defer testChdir(t, td)() + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{"-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + } + + // Test that outputs were displayed + expected := `{ + "null_resource.a": { + "project": "my-project", + "role": "roles/viewer", + "member": "user:peter@example.com" + } + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } +} + +func TestStateIdentities_backendOverrideState(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-identities-backend-default"), td) + + // Rename the state file to a custom name to simulate a custom state file + err := os.Rename(filepath.Join(td, "terraform.tfstate"), filepath.Join(td, "custom.tfstate")) + if err != nil { + t.Fatalf("Failed to rename state file: %s", err) + } + defer testChdir(t, td)() + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + // Run the command with a custom state file + args := []string{"-state=custom.tfstate", "-json"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } + + // Test that outputs were displayed + expected := `{ + "null_resource.a": { + "project": "my-project", + "role": "roles/viewer", + "member": "user:peter@example.com" + } + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } +} + +func TestStateIdentities_noState(t *testing.T) { + testCwd(t) + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + args := []string{} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d", code) + } +} + +func TestStateIdentities_modules(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("state-identities-nested-modules"), td) + defer testChdir(t, td)() + + p := testProvider() + ui := cli.NewMockUi() + c := &StateIdentitiesCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + Ui: ui, + }, + } + + t.Run("list resources in module and submodules", func(t *testing.T) { + args := []string{"-json", "module.nest"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } + + // resources in the module and any submodules should be included in the outputs + expected := `{ + "module.nest.test_instance.nest": { + "project": "my-project-nest", + "role": "roles/viewer-nest" + }, + "module.nest.module.subnest.test_instance.subnest": { + "project": "my-project-subnest", + "role": "roles/viewer-subnest" + } + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } + }) + + t.Run("submodule has resources only", func(t *testing.T) { + // now get the state for a module that has no resources, only another nested module + ui.OutputWriter.Reset() + args := []string{"-json", "module.nonexist"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d", code) + } + expected := `{ + "module.nonexist.module.child.test_instance.child": { + "project": "my-project-child", + "role": "roles/viewer-child" + } + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } + }) + + t.Run("expanded module", func(t *testing.T) { + // finally get the state for a module with an index + ui.OutputWriter.Reset() + args := []string{"-json", "module.count"} + if code := c.Run(args); code != 0 { + t.Fatalf("bad: %d: %s", code, ui.ErrorWriter.String()) + } + expected := `{ + "module.count[0].test_instance.count": { + "project": "my-project-count-0", + "role": "roles/viewer-count-0" + }, + "module.count[1].test_instance.count": { + "project": "my-project-count-1", + "role": "roles/viewer-count-1" + } + }` + actual := ui.OutputWriter.String() + + // Normalize JSON strings + var expectedJSON, actualJSON map[string]interface{} + if err := json.Unmarshal([]byte(expected), &expectedJSON); err != nil { + t.Fatalf("Failed to unmarshal expected JSON: %s", err) + } + if err := json.Unmarshal([]byte(actual), &actualJSON); err != nil { + t.Fatalf("Failed to unmarshal actual JSON: %s", err) + } + + if !reflect.DeepEqual(expectedJSON, actualJSON) { + t.Fatalf("Expected:\n%q\n\nTo equal: %q", expected, actual) + } + }) + + t.Run("completely nonexistent module", func(t *testing.T) { + // finally get the state for a module with an index + ui.OutputWriter.Reset() + args := []string{"-json", "module.notevenalittlebit"} + if code := c.Run(args); code != 1 { + t.Fatalf("bad: %d: %s", code, ui.OutputWriter.String()) + } + }) + +} diff --git a/internal/command/state_list.go b/internal/command/state_list.go index 54358b28d7..02fd6b2ab9 100644 --- a/internal/command/state_list.go +++ b/internal/command/state_list.go @@ -1,13 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" ) // StateListCommand is a Command implementation that lists the resources diff --git a/internal/command/state_list_test.go b/internal/command/state_list_test.go index 0b75051e38..fc3dadcd9a 100644 --- a/internal/command/state_list_test.go +++ b/internal/command/state_list_test.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) func TestStateList(t *testing.T) { diff --git a/internal/command/state_meta.go b/internal/command/state_meta.go index 17959f5ff9..0b970ddb3d 100644 --- a/internal/command/state_meta.go +++ b/internal/command/state_meta.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -102,7 +105,7 @@ func (c *StateMeta) lookupResourceInstanceAddr(state *states.State, allowMissing switch addr := targetAddr.(type) { case addrs.ModuleInstance: // Matches all instances within the indicated module and all of its - // descendent modules. + // descendant modules. // found is used to identify cases where the selected module has no // resources, but one or more of its submodules does. diff --git a/internal/command/state_mv.go b/internal/command/state_mv.go index 949f6c4b45..7ce5d0bf02 100644 --- a/internal/command/state_mv.go +++ b/internal/command/state_mv.go @@ -1,20 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" + "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/backendrun" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" ) -// StateMvCommand is a Command implementation that shows a single resource. +// StateMvCommand is a Command implementation that changes bindings +// in Terraform state so that existing remote objects bind to new resource instances. type StateMvCommand struct { StateMeta } @@ -71,7 +77,7 @@ func (c *StateMvCommand) Run(args []string) int { // If currentBackend is nil and diags didn't have errors, // this means we have an implicit local backend - _, isLocalBackend := currentBackend.(backend.Local) + _, isLocalBackend := currentBackend.(backendrun.Local) if currentBackend != nil && !isLocalBackend { diags = diags.Append( tfdiags.Sourceless( @@ -385,12 +391,27 @@ func (c *StateMvCommand) Run(args []string) int { return 0 // This is as far as we go in dry-run mode } + b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + if isCloudMode(b) { + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags = c.MaybeGetSchemas(stateTo, nil) + diags = diags.Append(schemaDiags) + } + // Write the new state if err := stateToMgr.WriteState(stateTo); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } - if err := stateToMgr.PersistState(); err != nil { + if err := stateToMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } @@ -401,7 +422,7 @@ func (c *StateMvCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } - if err := stateFromMgr.PersistState(); err != nil { + if err := stateFromMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } diff --git a/internal/command/state_mv_test.go b/internal/command/state_mv_test.go index 00f871f880..0dcec8f62b 100644 --- a/internal/command/state_mv_test.go +++ b/internal/command/state_mv_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -8,7 +11,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" diff --git a/internal/command/state_pull.go b/internal/command/state_pull.go index 8872cec65c..c67919a38a 100644 --- a/internal/command/state_pull.go +++ b/internal/command/state_pull.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,7 +12,8 @@ import ( "github.com/hashicorp/terraform/internal/states/statemgr" ) -// StatePullCommand is a Command implementation that shows a single resource. +// StatePullCommand is a Command implementation that allows downloading +// and outputting state information from remote state. type StatePullCommand struct { Meta StateMeta diff --git a/internal/command/state_pull_test.go b/internal/command/state_pull_test.go index 233bed25c0..0a589b0a93 100644 --- a/internal/command/state_pull_test.go +++ b/internal/command/state_pull_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,7 +9,7 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) func TestStatePull(t *testing.T) { diff --git a/internal/command/state_push.go b/internal/command/state_push.go index 0b863740c5..6f7ae6e3d6 100644 --- a/internal/command/state_push.go +++ b/internal/command/state_push.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,15 +9,18 @@ import ( "os" "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/mitchellh/cli" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" ) -// StatePushCommand is a Command implementation that shows a single resource. +// StatePushCommand is a Command implementation that allows +// pushing a local state to a remote location. type StatePushCommand struct { Meta StateMeta @@ -126,15 +132,24 @@ func (c *StatePushCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) return 1 } + + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + var diags tfdiags.Diagnostics + if isCloudMode(b) { + schemas, diags = c.MaybeGetSchemas(srcStateFile.State, nil) + } + if err := stateMgr.WriteState(srcStateFile.State); err != nil { c.Ui.Error(fmt.Sprintf("Failed to write state: %s", err)) return 1 } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf("Failed to persist state: %s", err)) return 1 } + c.showDiagnostics(diags) return 0 } diff --git a/internal/command/state_push_test.go b/internal/command/state_push_test.go index e30010bb9e..57cbb7df40 100644 --- a/internal/command/state_push_test.go +++ b/internal/command/state_push_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,10 +8,10 @@ import ( "strings" "testing" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" "github.com/hashicorp/terraform/internal/states" - "github.com/mitchellh/cli" ) func TestStatePush_empty(t *testing.T) { @@ -267,7 +270,7 @@ func TestStatePush_forceRemoteState(t *testing.T) { if err := sMgr.WriteState(states.NewState()); err != nil { t.Fatal(err) } - if err := sMgr.PersistState(); err != nil { + if err := sMgr.PersistState(nil); err != nil { t.Fatal(err) } diff --git a/internal/command/state_replace_provider.go b/internal/command/state_replace_provider.go index ec5347a769..53796adedc 100644 --- a/internal/command/state_replace_provider.go +++ b/internal/command/state_replace_provider.go @@ -1,16 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" ) // StateReplaceProviderCommand is a Command implementation that allows users @@ -160,16 +164,32 @@ func (c *StateReplaceProviderCommand) Run(args []string) int { resource.ProviderConfig.Provider = to } + b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + if isCloudMode(b) { + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags = c.MaybeGetSchemas(state, nil) + diags = diags.Append(schemaDiags) + } + // Write the updated state if err := stateMgr.WriteState(state); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } + c.showDiagnostics(diags) c.Ui.Output(fmt.Sprintf("\nSuccessfully replaced provider for %d resources.", len(willReplace))) return 0 } diff --git a/internal/command/state_replace_provider_test.go b/internal/command/state_replace_provider_test.go index 9c86cf7797..b3397e2fce 100644 --- a/internal/command/state_replace_provider_test.go +++ b/internal/command/state_replace_provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,7 +9,7 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" diff --git a/internal/command/state_rm.go b/internal/command/state_rm.go index f126c5f5a5..0ea230663e 100644 --- a/internal/command/state_rm.go +++ b/internal/command/state_rm.go @@ -1,18 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" ) -// StateRmCommand is a Command implementation that shows a single resource. +// StateRmCommand is a Command implementation that removes +// a single resource from the state. type StateRmCommand struct { StateMeta } @@ -110,11 +115,26 @@ func (c *StateRmCommand) Run(args []string) int { return 0 // This is as far as we go in dry-run mode } + b, backendDiags := c.Backend(nil) + diags = diags.Append(backendDiags) + if backendDiags.HasErrors() { + c.showDiagnostics(diags) + return 1 + } + + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + if isCloudMode(b) { + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags = c.MaybeGetSchemas(state, nil) + diags = diags.Append(schemaDiags) + } + if err := stateMgr.WriteState(state); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf(errStateRmPersist, err)) return 1 } diff --git a/internal/command/state_rm_test.go b/internal/command/state_rm_test.go index 1b58a59677..5afcf60b24 100644 --- a/internal/command/state_rm_test.go +++ b/internal/command/state_rm_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,7 +9,7 @@ import ( "strings" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" diff --git a/internal/command/state_show.go b/internal/command/state_show.go index 7ee86624df..ec50449d8c 100644 --- a/internal/command/state_show.go +++ b/internal/command/state_show.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,11 +8,16 @@ import ( "os" "strings" + "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/backend" - "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/states" - "github.com/mitchellh/cli" + "github.com/hashicorp/terraform/internal/states/statefile" ) // StateShowCommand is a Command implementation that shows a single resource. @@ -23,19 +31,19 @@ func (c *StateShowCommand) Run(args []string) int { cmdFlags := c.Meta.defaultFlagSet("state show") cmdFlags.StringVar(&c.Meta.statePath, "state", "", "path") if err := cmdFlags.Parse(args); err != nil { - c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) + c.Streams.Eprintf("Error parsing command-line flags: %s\n", err.Error()) return 1 } args = cmdFlags.Args() if len(args) != 1 { - c.Ui.Error("Exactly one argument expected.\n") + c.Streams.Eprint("Exactly one argument expected.\n") return cli.RunResultHelp } // Check for user-supplied plugin path var err error if c.pluginPath, err = c.loadPluginPath(); err != nil { - c.Ui.Error(fmt.Sprintf("Error loading plugin path: %s", err)) + c.Streams.Eprintf("Error loading plugin path: %\n", err) return 1 } @@ -47,9 +55,9 @@ func (c *StateShowCommand) Run(args []string) int { } // We require a local backend - local, ok := b.(backend.Local) + local, ok := b.(backendrun.Local) if !ok { - c.Ui.Error(ErrUnsupportedLocalOp) + c.Streams.Eprint(ErrUnsupportedLocalOp) return 1 } @@ -59,67 +67,67 @@ func (c *StateShowCommand) Run(args []string) int { // Check if the address can be parsed addr, addrDiags := addrs.ParseAbsResourceInstanceStr(args[0]) if addrDiags.HasErrors() { - c.Ui.Error(fmt.Sprintf(errParsingAddress, args[0])) + c.Streams.Eprintln(fmt.Sprintf(errParsingAddress, args[0])) return 1 } // We expect the config dir to always be the cwd cwd, err := os.Getwd() if err != nil { - c.Ui.Error(fmt.Sprintf("Error getting cwd: %s", err)) + c.Streams.Eprintf("Error getting cwd: %s\n", err) return 1 } // Build the operation (required to get the schemas) - opReq := c.Operation(b) + opReq := c.Operation(b, arguments.ViewHuman) opReq.AllowUnsetVariables = true opReq.ConfigDir = cwd opReq.ConfigLoader, err = c.initConfigLoader() if err != nil { - c.Ui.Error(fmt.Sprintf("Error initializing config loader: %s", err)) + c.Streams.Eprintf("Error initializing config loader: %s\n", err) return 1 } // Get the context (required to get the schemas) lr, _, ctxDiags := local.LocalRun(opReq) if ctxDiags.HasErrors() { - c.showDiagnostics(ctxDiags) + c.View.Diagnostics(ctxDiags) return 1 } // Get the schemas from the context schemas, diags := lr.Core.Schemas(lr.Config, lr.InputState) if diags.HasErrors() { - c.showDiagnostics(diags) + c.View.Diagnostics(diags) return 1 } // Get the state env, err := c.Workspace() if err != nil { - c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) + c.Streams.Eprintf("Error selecting workspace: %s\n", err) return 1 } stateMgr, err := b.StateMgr(env) if err != nil { - c.Ui.Error(fmt.Sprintf(errStateLoadingState, err)) + c.Streams.Eprintln(fmt.Sprintf(errStateLoadingState, err)) return 1 } if err := stateMgr.RefreshState(); err != nil { - c.Ui.Error(fmt.Sprintf("Failed to refresh state: %s", err)) + c.Streams.Eprintf("Failed to refresh state: %s\n", err) return 1 } state := stateMgr.State() if state == nil { - c.Ui.Error(errStateNotFound) + c.Streams.Eprintln(errStateNotFound) return 1 } is := state.ResourceInstance(addr) if !is.HasCurrent() { - c.Ui.Error(errNoInstanceFound) + c.Streams.Eprintln(errNoInstanceFound) return 1 } @@ -137,13 +145,26 @@ func (c *StateShowCommand) Run(args []string) int { absPc, ) - output := format.State(&format.StateOpts{ - State: singleInstance, - Color: c.Colorize(), - Schemas: schemas, - }) - c.Ui.Output(output[strings.Index(output, "#"):]) + root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(singleInstance, "", 0), schemas) + if err != nil { + c.Streams.Eprintf("Failed to marshal state to json: %s", err) + } + jstate := jsonformat.State{ + StateFormatVersion: jsonstate.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + RootModule: root, + RootModuleOutputs: outputs, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + } + + renderer := jsonformat.Renderer{ + Streams: c.Streams, + Colorize: c.Colorize(), + RunningInAutomation: c.RunningInAutomation, + } + + renderer.RenderHumanState(jstate) return 0 } diff --git a/internal/command/state_show_test.go b/internal/command/state_show_test.go index 3da87c0eca..eb12c5db43 100644 --- a/internal/command/state_show_test.go +++ b/internal/command/state_show_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -8,7 +11,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" - "github.com/mitchellh/cli" + "github.com/hashicorp/terraform/internal/terminal" "github.com/zclconf/go-cty/cty" ) @@ -36,7 +39,7 @@ func TestStateShow(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "foo": {Type: cty.String, Optional: true}, @@ -47,11 +50,11 @@ func TestStateShow(t *testing.T) { }, } - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -59,13 +62,15 @@ func TestStateShow(t *testing.T) { "-state", statePath, "test_instance.foo", } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Test that outputs were displayed expected := strings.TrimSpace(testStateShowOutput) + "\n" - actual := ui.OutputWriter.String() + actual := output.Stdout() if actual != expected { t.Fatalf("Expected:\n%q\n\nTo equal:\n%q", actual, expected) } @@ -111,7 +116,7 @@ func TestStateShow_multi(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "foo": {Type: cty.String, Optional: true}, @@ -122,11 +127,11 @@ func TestStateShow_multi(t *testing.T) { }, } - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -134,13 +139,15 @@ func TestStateShow_multi(t *testing.T) { "-state", statePath, "test_instance.foo", } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Test that outputs were displayed expected := strings.TrimSpace(testStateShowOutput) + "\n" - actual := ui.OutputWriter.String() + actual := output.Stdout() if actual != expected { t.Fatalf("Expected:\n%q\n\nTo equal:\n%q", actual, expected) } @@ -150,11 +157,11 @@ func TestStateShow_noState(t *testing.T) { testCwd(t) p := testProvider() - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -164,8 +171,9 @@ func TestStateShow_noState(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("bad: %d", code) } - if !strings.Contains(ui.ErrorWriter.String(), "No state file was found!") { - t.Fatalf("expected a no state file error, got: %s", ui.ErrorWriter.String()) + output := done(t) + if !strings.Contains(output.Stderr(), "No state file was found!") { + t.Fatalf("expected a no state file error, got: %s", output.Stderr()) } } @@ -174,11 +182,11 @@ func TestStateShow_emptyState(t *testing.T) { statePath := testStateFile(t, state) p := testProvider() - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: metaOverridesForProvider(p), - Ui: ui, + Streams: streams, }, } @@ -189,8 +197,9 @@ func TestStateShow_emptyState(t *testing.T) { if code := c.Run(args); code != 1 { t.Fatalf("bad: %d", code) } - if !strings.Contains(ui.ErrorWriter.String(), "No instance found for the given address!") { - t.Fatalf("expected a no instance found error, got: %s", ui.ErrorWriter.String()) + output := done(t) + if !strings.Contains(output.Stderr(), "No instance found for the given address!") { + t.Fatalf("expected a no instance found error, got: %s", output.Stderr()) } } @@ -218,7 +227,7 @@ func TestStateShow_configured_provider(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true, Computed: true}, "foo": {Type: cty.String, Optional: true}, @@ -229,7 +238,7 @@ func TestStateShow_configured_provider(t *testing.T) { }, } - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) c := &StateShowCommand{ Meta: Meta{ testingOverrides: &testingOverrides{ @@ -237,7 +246,7 @@ func TestStateShow_configured_provider(t *testing.T) { addrs.NewDefaultProvider("test-beta"): providers.FactoryFixed(p), }, }, - Ui: ui, + Streams: streams, }, } @@ -245,13 +254,15 @@ func TestStateShow_configured_provider(t *testing.T) { "-state", statePath, "test_instance.foo", } - if code := c.Run(args); code != 0 { - t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String()) + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) } // Test that outputs were displayed expected := strings.TrimSpace(testStateShowOutput) + "\n" - actual := ui.OutputWriter.String() + actual := output.Stdout() if actual != expected { t.Fatalf("Expected:\n%q\n\nTo equal:\n%q", actual, expected) } diff --git a/internal/command/state_test.go b/internal/command/state_test.go index cd2e830125..de99f6fb27 100644 --- a/internal/command/state_test.go +++ b/internal/command/state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/taint.go b/internal/command/taint.go index 0c5a499f2e..1efbc32986 100644 --- a/internal/command/taint.go +++ b/internal/command/taint.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -125,6 +129,14 @@ func (c *TaintCommand) Run(args []string) int { return 1 } + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + if isCloudMode(b) { + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags = c.MaybeGetSchemas(state, nil) + diags = diags.Append(schemaDiags) + } + ss := state.SyncWrapper() // Get the resource and instance we're going to taint @@ -171,11 +183,12 @@ func (c *TaintCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } + c.showDiagnostics(diags) c.Ui.Output(fmt.Sprintf("Resource instance %s has been marked as tainted.", addr)) return 0 } diff --git a/internal/command/taint_test.go b/internal/command/taint_test.go index 001d477082..364cb84e56 100644 --- a/internal/command/taint_test.go +++ b/internal/command/taint_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,7 +9,7 @@ import ( "testing" "github.com/google/go-cmp/cmp" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" diff --git a/internal/command/telemetry.go b/internal/command/telemetry.go new file mode 100644 index 0000000000..ba6a2c92ad --- /dev/null +++ b/internal/command/telemetry.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform/internal/command") +} diff --git a/internal/command/test.go b/internal/command/test.go index 1f18689f1b..8b236fe779 100644 --- a/internal/command/test.go +++ b/internal/command/test.go @@ -1,730 +1,302 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "context" - "fmt" - "io/ioutil" - "log" - "os" "path/filepath" "strings" + "time" - ctyjson "github.com/zclconf/go-cty/cty/json" - - "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend/local" + "github.com/hashicorp/terraform/internal/cloud" "github.com/hashicorp/terraform/internal/command/arguments" - "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/junit" "github.com/hashicorp/terraform/internal/command/views" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configload" - "github.com/hashicorp/terraform/internal/depsfile" - "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/moduletest" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/providercache" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) -// TestCommand is the implementation of "terraform test". type TestCommand struct { Meta } -func (c *TestCommand) Run(rawArgs []string) int { - // Parse and apply global view arguments - common, rawArgs := arguments.ParseView(rawArgs) - c.View.Configure(common) - - args, diags := arguments.ParseTest(rawArgs) - view := views.NewTest(c.View, args.Output) - if diags.HasErrors() { - view.Diagnostics(diags) - return 1 - } - - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - `The "terraform test" command is experimental`, - "We'd like to invite adventurous module authors to write integration tests for their modules using this command, but all of the behaviors of this command are currently experimental and may change based on feedback.\n\nFor more information on the testing experiment, including ongoing research goals and avenues for feedback, see:\n https://www.terraform.io/docs/language/modules/testing-experiment.html", - )) - - ctx, cancel := c.InterruptibleContext() - defer cancel() - - results, moreDiags := c.run(ctx, args) - diags = diags.Append(moreDiags) - - initFailed := diags.HasErrors() - view.Diagnostics(diags) - diags = view.Results(results) - resultsFailed := diags.HasErrors() - view.Diagnostics(diags) // possible additional errors from saving the results - - var testsFailed bool - for _, suite := range results { - for _, component := range suite.Components { - for _, assertion := range component.Assertions { - if !assertion.Outcome.SuiteCanPass() { - testsFailed = true - } - } - } - } - - // Lots of things can possibly have failed - if initFailed || resultsFailed || testsFailed { - return 1 - } - return 0 -} - -func (c *TestCommand) run(ctx context.Context, args arguments.Test) (results map[string]*moduletest.Suite, diags tfdiags.Diagnostics) { - suiteNames, err := c.collectSuiteNames() - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error while searching for test configurations", - fmt.Sprintf("While attempting to scan the 'tests' subdirectory for potential test configurations, Terraform encountered an error: %s.", err), - )) - return nil, diags - } - - ret := make(map[string]*moduletest.Suite, len(suiteNames)) - for _, suiteName := range suiteNames { - if ctx.Err() != nil { - // If the context has already failed in some way then we'll - // halt early and report whatever's already happened. - break - } - suite, moreDiags := c.runSuite(ctx, suiteName) - diags = diags.Append(moreDiags) - ret[suiteName] = suite - } - - return ret, diags -} - -func (c *TestCommand) runSuite(ctx context.Context, suiteName string) (*moduletest.Suite, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - ret := moduletest.Suite{ - Name: suiteName, - Components: map[string]*moduletest.Component{}, - } - - // In order to make this initial round of "terraform test" pretty self - // contained while it's experimental, it's largely just mimicking what - // would happen when running the main Terraform workflow commands, which - // comes at the expense of a few irritants that we'll hopefully resolve - // in future iterations as the design solidifies: - // - We need to install remote modules separately for each of the - // test suites, because we don't have any sense of a shared cache - // of modules that multiple configurations can refer to at once. - // - We _do_ have a sense of a cache of remote providers, but it's fixed - // at being specifically a two-level cache (global vs. directory-specific) - // and so we can't easily capture a third level of "all of the test suites - // for this module" that sits between the two. Consequently, we need to - // dynamically choose between creating a directory-specific "global" - // cache or using the user's existing global cache, to avoid any - // situation were we'd be re-downloading the same providers for every - // one of the test suites. - // - We need to do something a bit horrid in order to have our test - // provider instance persist between the plan and apply steps, because - // normally that is the exact opposite of what we want. - // The above notes are here mainly as an aid to someone who might be - // planning a subsequent phase of this R&D effort, to help distinguish - // between things we're doing here because they are valuable vs. things - // we're doing just to make it work without doing any disruptive - // refactoring. - - suiteDirs, moreDiags := c.prepareSuiteDir(ctx, suiteName) - diags = diags.Append(moreDiags) - if diags.HasErrors() { - // Generate a special failure representing the test initialization - // having failed, since we therefore won'tbe able to run the actual - // tests defined inside. - ret.Components["(init)"] = &moduletest.Component{ - Assertions: map[string]*moduletest.Assertion{ - "(init)": { - Outcome: moduletest.Error, - Description: "terraform init", - Message: "failed to install test suite dependencies", - Diagnostics: diags, - }, - }, - } - return &ret, nil - } - - // When we run the suite itself, we collect up diagnostics associated - // with individual components, so ret.Components may or may not contain - // failed/errored components after runTestSuite returns. - var finalState *states.State - ret.Components, finalState = c.runTestSuite(ctx, suiteDirs) - - // Regardless of the success or failure of the test suite, if there are - // any objects left in the state then we'll generate a top-level error - // about each one to minimize the chance of the user failing to notice - // that there are leftover objects that might continue to cost money - // unless manually deleted. - for _, ms := range finalState.Modules { - for _, rs := range ms.Resources { - for instanceKey, is := range rs.Instances { - var objs []*states.ResourceInstanceObjectSrc - if is.Current != nil { - objs = append(objs, is.Current) - } - for _, obj := range is.Deposed { - objs = append(objs, obj) - } - for _, obj := range objs { - // Unfortunately we don't have provider schemas out here - // and so we're limited in what we can achieve with these - // ResourceInstanceObjectSrc values, but we can try some - // heuristicy things to try to give some useful information - // in common cases. - var k, v string - if ty, err := ctyjson.ImpliedType(obj.AttrsJSON); err == nil { - if approxV, err := ctyjson.Unmarshal(obj.AttrsJSON, ty); err == nil { - k, v = format.ObjectValueIDOrName(approxV) - } - } - - var detail string - if k != "" { - // We can be more specific if we were able to infer - // an identifying attribute for this object. - detail = fmt.Sprintf( - "Due to errors during destroy, test suite %q has left behind an object for %s, with the following identity:\n %s = %q\n\nYou will need to delete this object manually in the remote system, or else it may have an ongoing cost.", - suiteName, - rs.Addr.Instance(instanceKey), - k, v, - ) - } else { - // If our heuristics for finding a suitable identifier - // failed then unfortunately we must be more vague. - // (We can't just print the entire object, because it - // might be overly large and it might contain sensitive - // values.) - detail = fmt.Sprintf( - "Due to errors during destroy, test suite %q has left behind an object for %s. You will need to delete this object manually in the remote system, or else it may have an ongoing cost.", - suiteName, - rs.Addr.Instance(instanceKey), - ) - } - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to clean up after tests", - detail, - )) - } - } - } - } - - return &ret, diags -} - -func (c *TestCommand) prepareSuiteDir(ctx context.Context, suiteName string) (testCommandSuiteDirs, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - configDir := filepath.Join("tests", suiteName) - log.Printf("[TRACE] terraform test: Prepare directory for suite %q in %s", suiteName, configDir) - - suiteDirs := testCommandSuiteDirs{ - SuiteName: suiteName, - ConfigDir: configDir, - } - - // Before we can run a test suite we need to make sure that we have all of - // its dependencies available, so the following is essentially an - // abbreviated form of what happens during "terraform init", with some - // extra trickery in places. - - // First, module installation. This will include linking in the module - // under test, but also includes grabbing the dependencies of that module - // if it has any. - suiteDirs.ModulesDir = filepath.Join(configDir, ".terraform", "modules") - os.MkdirAll(suiteDirs.ModulesDir, 0755) // if this fails then we'll ignore it and let InstallModules below fail instead - reg := c.registryClient() - moduleInst := initwd.NewModuleInstaller(suiteDirs.ModulesDir, reg) - _, moreDiags := moduleInst.InstallModules(ctx, configDir, true, nil) - diags = diags.Append(moreDiags) - if diags.HasErrors() { - return suiteDirs, diags - } - - // The installer puts the files in a suitable place on disk, but we - // still need to actually load the configuration. We need to do this - // with a separate config loader because the Meta.configLoader instance - // is intended for interacting with the current working directory, not - // with the test suite subdirectories. - loader, err := configload.NewLoader(&configload.Config{ - ModulesDir: suiteDirs.ModulesDir, - Services: c.Services, - }) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to create test configuration loader", - fmt.Sprintf("Failed to prepare loader for test configuration %s: %s.", configDir, err), - )) - return suiteDirs, diags - } - cfg, hclDiags := loader.LoadConfig(configDir) - diags = diags.Append(hclDiags) - if diags.HasErrors() { - return suiteDirs, diags - } - suiteDirs.Config = cfg - - // With the full configuration tree available, we can now install - // the necessary providers. We'll use a separate local cache directory - // here, because the test configuration might have additional requirements - // compared to the module itself. - suiteDirs.ProvidersDir = filepath.Join(configDir, ".terraform", "providers") - os.MkdirAll(suiteDirs.ProvidersDir, 0755) // if this fails then we'll ignore it and operations below fail instead - localCacheDir := providercache.NewDir(suiteDirs.ProvidersDir) - providerInst := c.providerInstaller().Clone(localCacheDir) - if !providerInst.HasGlobalCacheDir() { - // If the user already configured a global cache directory then we'll - // just use it for caching the test providers too, because then we - // can potentially reuse cache entries they already have. However, - // if they didn't configure one then we'll still establish one locally - // in the working directory, which we'll then share across all tests - // to avoid downloading the same providers repeatedly. - cachePath := filepath.Join(c.DataDir(), "testing-providers") // note this is _not_ under the suite dir - err := os.MkdirAll(cachePath, 0755) - // If we were unable to create the directory for any reason then we'll - // just proceed without a cache, at the expense of repeated downloads. - // (With that said, later installing might end up failing for the - // same reason anyway...) - if err == nil || os.IsExist(err) { - cacheDir := providercache.NewDir(cachePath) - providerInst.SetGlobalCacheDir(cacheDir) - } - } - reqs, hclDiags := cfg.ProviderRequirements() - diags = diags.Append(hclDiags) - if diags.HasErrors() { - return suiteDirs, diags - } - - // For test suites we only retain the "locks" in memory for the duration - // for one run, just to make sure that we use the same providers when we - // eventually run the test suite. - locks := depsfile.NewLocks() - evts := &providercache.InstallerEvents{ - QueryPackagesFailure: func(provider addrs.Provider, err error) { - if err != nil && addrs.IsDefaultProvider(provider) && provider.Type == "test" { - // This is some additional context for the failure error - // we'll generate afterwards. Not the most ideal UX but - // good enough for this prototype implementation, to help - // hint about the special builtin provider we use here. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - "Probably-unintended reference to \"hashicorp/test\" provider", - "For the purposes of this experimental implementation of module test suites, you must use the built-in test provider terraform.io/builtin/test, which requires an explicit required_providers declaration.", - )) - } - }, - } - ctx = evts.OnContext(ctx) - locks, err = providerInst.EnsureProviderVersions(ctx, locks, reqs, providercache.InstallUpgrades) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to install required providers", - fmt.Sprintf("Couldn't install necessary providers for test configuration %s: %s.", configDir, err), - )) - return suiteDirs, diags - } - suiteDirs.ProviderLocks = locks - suiteDirs.ProviderCache = localCacheDir - - return suiteDirs, diags -} - -func (c *TestCommand) runTestSuite(ctx context.Context, suiteDirs testCommandSuiteDirs) (map[string]*moduletest.Component, *states.State) { - log.Printf("[TRACE] terraform test: Run test suite %q", suiteDirs.SuiteName) - - ret := make(map[string]*moduletest.Component) - - // To collect test results we'll use an instance of the special "test" - // provider, which records the intention to make a test assertion during - // planning and then hopefully updates that to an actual assertion result - // during apply, unless an apply error causes the graph walk to exit early. - // For this to work correctly, we must ensure we're using the same provider - // instance for both plan and apply. - testProvider := moduletest.NewProvider() - - // synthError is a helper to return early with a synthetic failing - // component, for problems that prevent us from even discovering what an - // appropriate component and assertion name might be. - state := states.NewState() - synthError := func(name string, desc string, msg string, diags tfdiags.Diagnostics) (map[string]*moduletest.Component, *states.State) { - key := "(" + name + ")" // parens ensure this can't conflict with an actual component/assertion key - ret[key] = &moduletest.Component{ - Assertions: map[string]*moduletest.Assertion{ - key: { - Outcome: moduletest.Error, - Description: desc, - Message: msg, - Diagnostics: diags, - }, - }, - } - return ret, state - } - - // NOTE: This function intentionally deviates from the usual pattern of - // gradually appending more diagnostics to the same diags, because - // here we're associating each set of diagnostics with the specific - // operation it belongs to. - - providerFactories, diags := c.testSuiteProviders(suiteDirs, testProvider) - if diags.HasErrors() { - // It should be unusual to get in here, because testSuiteProviders - // should rely only on things guaranteed by prepareSuiteDir, but - // since we're doing external I/O here there is always the risk that - // the filesystem changes or fails between setting up and using the - // providers. - return synthError( - "init", - "terraform init", - "failed to resolve the required providers", - diags, - ) - } - - plan, diags := c.testSuitePlan(ctx, suiteDirs, providerFactories) - if diags.HasErrors() { - // It should be unusual to get in here, because testSuitePlan - // should rely only on things guaranteed by prepareSuiteDir, but - // since we're doing external I/O here there is always the risk that - // the filesystem changes or fails between setting up and using the - // providers. - return synthError( - "plan", - "terraform plan", - "failed to create a plan", - diags, - ) - } - - // Now we'll apply the plan. Once we try to apply, we might've created - // real remote objects, and so we must try to run destroy even if the - // apply returns errors, and we must return whatever state we end up - // with so the caller can generate additional loud errors if anything - // is left in it. - - state, diags = c.testSuiteApply(ctx, plan, suiteDirs, providerFactories) - if diags.HasErrors() { - // We don't return here, unlike the others above, because we want to - // continue to the destroy below even if there are apply errors. - synthError( - "apply", - "terraform apply", - "failed to apply the created plan", - diags, - ) - } - - // By the time we get here, the test provider will have gathered up all - // of the planned assertions and the final results for any assertions that - // were not blocked by an error. This also resets the provider so that - // the destroy operation below won't get tripped up on stale results. - ret = testProvider.Reset() - - state, diags = c.testSuiteDestroy(ctx, state, suiteDirs, providerFactories) - if diags.HasErrors() { - synthError( - "destroy", - "terraform destroy", - "failed to destroy objects created during test (NOTE: leftover remote objects may still exist)", - diags, - ) - } - - return ret, state -} - -func (c *TestCommand) testSuiteProviders(suiteDirs testCommandSuiteDirs, testProvider *moduletest.Provider) (map[addrs.Provider]providers.Factory, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - ret := make(map[addrs.Provider]providers.Factory) - - // We can safely use the internal providers returned by Meta here because - // the built-in provider versions can never vary based on the configuration - // and thus we don't need to worry about potential version differences - // between main module and test suite modules. - for name, factory := range c.internalProviders() { - ret[addrs.NewBuiltInProvider(name)] = factory - } - - // For the remaining non-builtin providers, we'll just take whatever we - // recorded earlier in the in-memory-only "lock file". All of these should - // typically still be available because we would've only just installed - // them, but this could fail if e.g. the filesystem has been somehow - // damaged in the meantime. - for provider, lock := range suiteDirs.ProviderLocks.AllProviders() { - version := lock.Version() - cached := suiteDirs.ProviderCache.ProviderVersion(provider, version) - if cached == nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Required provider not found", - fmt.Sprintf("Although installation previously succeeded for %s v%s, it no longer seems to be present in the cache directory.", provider.ForDisplay(), version.String()), - )) - continue // potentially collect up multiple errors - } - - // NOTE: We don't consider the checksums for test suite dependencies, - // because we're creating a fresh "lock file" each time we run anyway - // and so they wouldn't actually guarantee anything useful. - - ret[provider] = providerFactory(cached) - } - - // We'll replace the test provider instance with the one our caller - // provided, so it'll be able to interrogate the test results directly. - ret[addrs.NewBuiltInProvider("test")] = func() (providers.Interface, error) { - return testProvider, nil - } - - return ret, diags -} - -type testSuiteRunContext struct { - Core *terraform.Context - - PlanMode plans.Mode - Config *configs.Config - InputState *states.State - Changes *plans.Changes -} - -func (c *TestCommand) testSuiteContext(suiteDirs testCommandSuiteDirs, providerFactories map[addrs.Provider]providers.Factory, state *states.State, plan *plans.Plan, destroy bool) (*testSuiteRunContext, tfdiags.Diagnostics) { - var changes *plans.Changes - if plan != nil { - changes = plan.Changes - } - - planMode := plans.NormalMode - if destroy { - planMode = plans.DestroyMode - } - - tfCtx, diags := terraform.NewContext(&terraform.ContextOpts{ - Providers: providerFactories, - - // We just use the provisioners from the main Meta here, because - // unlike providers provisioner plugins are not automatically - // installable anyway, and so we'll need to hunt for them in the same - // legacy way that normal Terraform operations do. - Provisioners: c.provisionerFactories(), - - Meta: &terraform.ContextMeta{ - Env: "test_" + suiteDirs.SuiteName, - }, - }) - if diags.HasErrors() { - return nil, diags - } - return &testSuiteRunContext{ - Core: tfCtx, - - PlanMode: planMode, - Config: suiteDirs.Config, - InputState: state, - Changes: changes, - }, diags -} - -func (c *TestCommand) testSuitePlan(ctx context.Context, suiteDirs testCommandSuiteDirs, providerFactories map[addrs.Provider]providers.Factory) (*plans.Plan, tfdiags.Diagnostics) { - log.Printf("[TRACE] terraform test: create plan for suite %q", suiteDirs.SuiteName) - runCtx, diags := c.testSuiteContext(suiteDirs, providerFactories, nil, nil, false) - if diags.HasErrors() { - return nil, diags - } - - // We'll also validate as part of planning, to ensure that the test - // configuration would pass "terraform validate". This is actually - // largely redundant with the runCtx.Core.Plan call below, but was - // included here originally because Plan did _originally_ assume that - // an earlier Validate had already passed, but now does its own - // validation work as (mostly) a superset of validate. - moreDiags := runCtx.Core.Validate(runCtx.Config) - diags = diags.Append(moreDiags) - if diags.HasErrors() { - return nil, diags - } - - plan, moreDiags := runCtx.Core.Plan( - runCtx.Config, runCtx.InputState, &terraform.PlanOpts{Mode: runCtx.PlanMode}, - ) - diags = diags.Append(moreDiags) - return plan, diags -} - -func (c *TestCommand) testSuiteApply(ctx context.Context, plan *plans.Plan, suiteDirs testCommandSuiteDirs, providerFactories map[addrs.Provider]providers.Factory) (*states.State, tfdiags.Diagnostics) { - log.Printf("[TRACE] terraform test: apply plan for suite %q", suiteDirs.SuiteName) - runCtx, diags := c.testSuiteContext(suiteDirs, providerFactories, nil, plan, false) - if diags.HasErrors() { - // To make things easier on the caller, we'll return a valid empty - // state even in this case. - return states.NewState(), diags - } - - state, moreDiags := runCtx.Core.Apply(plan, runCtx.Config) - diags = diags.Append(moreDiags) - return state, diags -} - -func (c *TestCommand) testSuiteDestroy(ctx context.Context, state *states.State, suiteDirs testCommandSuiteDirs, providerFactories map[addrs.Provider]providers.Factory) (*states.State, tfdiags.Diagnostics) { - log.Printf("[TRACE] terraform test: plan to destroy any existing objects for suite %q", suiteDirs.SuiteName) - runCtx, diags := c.testSuiteContext(suiteDirs, providerFactories, state, nil, true) - if diags.HasErrors() { - return state, diags - } - - plan, moreDiags := runCtx.Core.Plan( - runCtx.Config, runCtx.InputState, &terraform.PlanOpts{Mode: runCtx.PlanMode}, - ) - diags = diags.Append(moreDiags) - if diags.HasErrors() { - return state, diags - } - - log.Printf("[TRACE] terraform test: apply the plan to destroy any existing objects for suite %q", suiteDirs.SuiteName) - runCtx, moreDiags = c.testSuiteContext(suiteDirs, providerFactories, state, plan, true) - diags = diags.Append(moreDiags) - if diags.HasErrors() { - return state, diags - } - - state, moreDiags = runCtx.Core.Apply(plan, runCtx.Config) - diags = diags.Append(moreDiags) - return state, diags -} - -func (c *TestCommand) collectSuiteNames() ([]string, error) { - items, err := ioutil.ReadDir("tests") - if err != nil { - if os.IsNotExist(err) { - return nil, nil - } - return nil, err - } - - ret := make([]string, 0, len(items)) - for _, item := range items { - if !item.IsDir() { - continue - } - name := item.Name() - suitePath := filepath.Join("tests", name) - tfFiles, err := filepath.Glob(filepath.Join(suitePath, "*.tf")) - if err != nil { - // We'll just ignore it and treat it like a dir with no .tf files - tfFiles = nil - } - tfJSONFiles, err := filepath.Glob(filepath.Join(suitePath, "*.tf.json")) - if err != nil { - // We'll just ignore it and treat it like a dir with no .tf.json files - tfJSONFiles = nil - } - if (len(tfFiles) + len(tfJSONFiles)) == 0 { - // Not a test suite, then. - continue - } - ret = append(ret, name) - } - - return ret, nil -} - func (c *TestCommand) Help() string { helpText := ` -Usage: terraform test [options] +Usage: terraform [global options] test [options] - This is an experimental command to help with automated integration - testing of shared modules. The usage and behavior of this command is - likely to change in breaking ways in subsequent releases, as we - are currently using this command primarily for research purposes. + Executes automated integration tests against the current Terraform + configuration. - In its current experimental form, "test" will look under the current - working directory for a subdirectory called "tests", and then within - that directory search for one or more subdirectories that contain - ".tf" or ".tf.json" files. For any that it finds, it will perform - Terraform operations similar to the following sequence of commands - in each of those directories: - terraform validate - terraform apply - terraform destroy + Terraform will search for .tftest.hcl files within the current configuration + and testing directories. Terraform will then execute the testing run blocks + within any testing files in order, and verify conditional checks and + assertions against the created infrastructure. - The test configurations should not declare any input variables and - should at least contain a call to the module being tested, which - will always be available at the path ../.. due to the expected - filesystem layout. - - The tests are considered to be successful if all of the above steps - succeed. - - Test configurations may optionally include uses of the special - built-in test provider terraform.io/builtin/test, which allows - writing explicit test assertions which must also all pass in order - for the test run to be considered successful. - - This initial implementation is intended as a minimally-viable - product to use for further research and experimentation, and in - particular it currently lacks the following capabilities that we - expect to consider in later iterations, based on feedback: - - Testing of subsequent updates to existing infrastructure, - where currently it only supports initial creation and - then destruction. - - Testing top-level modules that are intended to be used for - "real" environments, which typically have hard-coded values - that don't permit creating a separate "copy" for testing. - - Some sort of support for unit test runs that don't interact - with remote systems at all, e.g. for use in checking pull - requests from untrusted contributors. - - In the meantime, we'd like to hear feedback from module authors - who have tried writing some experimental tests for their modules - about what sorts of tests you were able to write, what sorts of - tests you weren't able to write, and any tests that you were - able to write but that were difficult to model in some way. + This command creates real infrastructure and will attempt to clean up the + testing infrastructure on completion. Monitor the output carefully to ensure + this cleanup process is successful. Options: - -compact-warnings Use a more compact representation for warnings, if - this command produces only warnings and no errors. + -cloud-run=source If specified, Terraform will execute this test run + remotely using HCP Terraform or Terraform Enterprise. + You must specify the source of a module registered in + a private module registry as the argument to this flag. + This allows Terraform to associate the cloud run with + the correct HCP Terraform or Terraform Enterprise module + and organization. - -junit-xml=FILE In addition to the usual output, also write test - results to the given file path in JUnit XML format. - This format is commonly supported by CI systems, and - they typically expect to be given a filename to search - for in the test workspace after the test run finishes. + -filter=testfile If specified, Terraform will only execute the test files + specified by this flag. You can use this option multiple + times to execute more than one test file. - -no-color Don't include virtual terminal formatting sequences in - the output. + -json If specified, machine readable output will be printed in + JSON format + + -no-color If specified, output won't contain any color. + + -parallelism=n Limit the number of concurrent operations within the + plan/apply operation of a test run. Defaults to 10. + + -test-directory=path Set the Terraform test directory, defaults to "tests". + + -var 'foo=bar' Set a value for one of the input variables in the root + module of the configuration. Use this option more than + once to set more than one variable. + + -var-file=filename Load variable values from the given file, in addition + to the default files terraform.tfvars and *.auto.tfvars. + Use this option more than once to include more than one + variables file. + + -verbose Print the plan or state for each test run block as it + executes. ` return strings.TrimSpace(helpText) } func (c *TestCommand) Synopsis() string { - return "Experimental support for module integration testing" + return "Execute integration tests for Terraform modules" } -type testCommandSuiteDirs struct { - SuiteName string +func (c *TestCommand) Run(rawArgs []string) int { + var diags tfdiags.Diagnostics - ConfigDir string - ModulesDir string - ProvidersDir string + common, rawArgs := arguments.ParseView(rawArgs) + c.View.Configure(common) - Config *configs.Config - ProviderCache *providercache.Dir - ProviderLocks *depsfile.Locks + // Since we build the colorizer for the cloud runner outside the views + // package we need to propagate our no-color setting manually. Once the + // cloud package is fully migrated over to the new streams IO we should be + // able to remove this. + c.Meta.color = !common.NoColor + c.Meta.Color = c.Meta.color + + args, diags := arguments.ParseTest(rawArgs) + if diags.HasErrors() { + c.View.Diagnostics(diags) + c.View.HelpPrompt("test") + return 1 + } + c.Meta.parallelism = args.OperationParallelism + + view := views.NewTest(args.ViewType, c.View) + + // The specified testing directory must be a relative path, and it must + // point to a directory that is a descendant of the configuration directory. + if !filepath.IsLocal(args.TestDirectory) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid testing directory", + "The testing directory must be a relative path pointing to a directory local to the configuration directory.")) + + view.Diagnostics(nil, nil, diags) + return 1 + } + + config, configDiags := c.loadConfigWithTests(".", args.TestDirectory) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + view.Diagnostics(nil, nil, diags) + return 1 + } + + // Users can also specify variables via the command line, so we'll parse + // all that here. + var items []arguments.FlagNameValue + for _, variable := range args.Vars.All() { + items = append(items, arguments.FlagNameValue{ + Name: variable.Name, + Value: variable.Value, + }) + } + c.variableArgs = arguments.FlagNameValueSlice{Items: &items} + + // Collect variables for "terraform test" + testVariables, variableDiags := c.collectVariableValuesForTests(args.TestDirectory) + diags = diags.Append(variableDiags) + + variables, variableDiags := c.collectVariableValues() + diags = diags.Append(variableDiags) + if variableDiags.HasErrors() { + view.Diagnostics(nil, nil, diags) + return 1 + } + + opts, err := c.contextOpts() + if err != nil { + diags = diags.Append(err) + view.Diagnostics(nil, nil, diags) + return 1 + } + + // Print out all the diagnostics we have from the setup. These will just be + // warnings, and we want them out of the way before we start the actual + // testing. + view.Diagnostics(nil, nil, diags) + + // We have two levels of interrupt here. A 'stop' and a 'cancel'. A 'stop' + // is a soft request to stop. We'll finish the current test, do the tidy up, + // but then skip all remaining tests and run blocks. A 'cancel' is a hard + // request to stop now. We'll cancel the current operation immediately + // even if it's a delete operation, and we won't clean up any infrastructure + // if we're halfway through a test. We'll print details explaining what was + // stopped so the user can do their best to recover from it. + + runningCtx, done := context.WithCancel(context.Background()) + stopCtx, stop := context.WithCancel(runningCtx) + cancelCtx, cancel := context.WithCancel(context.Background()) + + var runner moduletest.TestSuiteRunner + if len(args.CloudRunSource) > 0 { + + var renderer *jsonformat.Renderer + if args.ViewType == arguments.ViewHuman { + // We only set the renderer if we want Human-readable output. + // Otherwise, we just let the runner echo whatever data it receives + // back from the agent anyway. + renderer = &jsonformat.Renderer{ + Streams: c.Streams, + Colorize: c.Colorize(), + RunningInAutomation: c.RunningInAutomation, + } + } + + runner = &cloud.TestSuiteRunner{ + ConfigDirectory: ".", // Always loading from the current directory. + TestingDirectory: args.TestDirectory, + Config: config, + Services: c.Services, + Source: args.CloudRunSource, + GlobalVariables: variables, + Stopped: false, + Cancelled: false, + StoppedCtx: stopCtx, + CancelledCtx: cancelCtx, + Verbose: args.Verbose, + OperationParallelism: args.OperationParallelism, + Filters: args.Filter, + Renderer: renderer, + View: view, + Streams: c.Streams, + } + } else { + localRunner := &local.TestSuiteRunner{ + Config: config, + // The GlobalVariables are loaded from the + // main configuration directory + // The GlobalTestVariables are loaded from the + // test directory + GlobalVariables: variables, + GlobalTestVariables: testVariables, + TestingDirectory: args.TestDirectory, + Opts: opts, + View: view, + Stopped: false, + Cancelled: false, + StoppedCtx: stopCtx, + CancelledCtx: cancelCtx, + Filter: args.Filter, + Verbose: args.Verbose, + } + + // JUnit output is only compatible with local test execution + if args.JUnitXMLFile != "" { + // Make sure TestCommand's calls loadConfigWithTests before this code, so configLoader is not nil + localRunner.JUnit = junit.NewTestJUnitXMLFile(args.JUnitXMLFile, c.configLoader, localRunner) + } + + runner = localRunner + } + + var testDiags tfdiags.Diagnostics + var status moduletest.Status + + go func() { + defer logging.PanicHandler() + defer done() + defer stop() + defer cancel() + + status, testDiags = runner.Test() + }() + + // Wait for the operation to complete, or for an interrupt to occur. + select { + case <-c.ShutdownCh: + // Nice request to be cancelled. + + view.Interrupted() + runner.Stop() + stop() + + select { + case <-c.ShutdownCh: + // The user pressed it again, now we have to get it to stop as + // fast as possible. + + view.FatalInterrupt() + runner.Cancel() + cancel() + + waitTime := 5 * time.Second + if len(args.CloudRunSource) > 0 { + // We wait longer for cloud runs because the agent should force + // kill the remote job after 5 seconds (as defined above). + // + // This can take longer as the remote agent doesn't receive the + // interrupt immediately. So for cloud runs, we'll wait a minute + // which should give the remote process enough to receive the + // signal, process it, and exit. + // + // If after a minute, the job still hasn't finished then we + // assume something else has gone wrong and we'll just have to + // live with the consequences. + waitTime = time.Minute + } + + // We'll wait 5 seconds for this operation to finish now, regardless + // of whether it finishes successfully or not. + select { + case <-runningCtx.Done(): + case <-time.After(waitTime): + } + + case <-runningCtx.Done(): + // The application finished nicely after the request was stopped. + } + case <-runningCtx.Done(): + // tests finished normally with no interrupts. + } + + view.Diagnostics(nil, nil, testDiags) + + if status != moduletest.Pass { + return 1 + } + return 0 } diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 17ae6da67a..d3ec9f0552 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -1,163 +1,3480 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "bytes" - "io/ioutil" + "encoding/json" + "fmt" + "os" + "path" + "regexp" + "runtime" "strings" "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + testing_command "github.com/hashicorp/terraform/internal/command/testing" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terminal" ) -// These are the main tests for the "terraform test" command. -func TestTest(t *testing.T) { - t.Run("passes", func(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath("test-passes"), td) - defer testChdir(t, td)() - - streams, close := terminal.StreamsForTesting(t) - cmd := &TestCommand{ - Meta: Meta{ - Streams: streams, - View: views.NewView(streams), +func TestTest_Runs(t *testing.T) { + tcs := map[string]struct { + override string + args []string + envVars map[string]string + expectedOut []string + expectedErr []string + expectedResourceCount int + code int + initCode int + skip bool + description string + }{ + "simple_pass": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "top-dir-only-test-files": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "top-dir-only-nested-test-files": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "simple_pass_nested": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "simple_pass_nested_alternate": { + args: []string{"-test-directory", "other"}, + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "simple_pass_very_nested": { + args: []string{"-test-directory", "tests/subdir"}, + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "simple_pass_cmd_parallel": { + override: "simple_pass", + args: []string{"-parallelism", "1"}, + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + description: "simple_pass with parallelism set to 1", + }, + "simple_pass_very_nested_alternate": { + override: "simple_pass_very_nested", + args: []string{"-test-directory", "./tests/subdir"}, + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "simple_pass_bad_test_directory": { + override: "simple_pass", + args: []string{"-test-directory", "../tests"}, + expectedErr: []string{"Invalid testing directory"}, + code: 1, + }, + "simple_pass_bad_test_directory_abs": { + override: "simple_pass", + args: []string{"-test-directory", "/home/username/config/tests"}, + expectedErr: []string{"Invalid testing directory"}, + code: 1, + }, + "pass_with_locals": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "pass_with_outputs": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "pass_with_variables": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "plan_then_apply": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "expect_failures_checks": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "expect_failures_inputs": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "expect_failures_resources": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "multiple_files": { + expectedOut: []string{"2 passed, 0 failed"}, + code: 0, + }, + "multiple_files_with_filter": { + override: "multiple_files", + args: []string{"-filter=one.tftest.hcl"}, + expectedOut: []string{"1 passed, 0 failed"}, + code: 0, + }, + "no_state": { + expectedOut: []string{"0 passed, 1 failed"}, + expectedErr: []string{"No value for required variable"}, + description: "the run apply fails, causing it to produce a nil state.", + code: 1, + }, + "variables": { + expectedOut: []string{"2 passed, 0 failed"}, + code: 0, + }, + "variables_overridden": { + override: "variables", + args: []string{"-var=input=foo"}, + expectedOut: []string{"1 passed, 1 failed"}, + expectedErr: []string{`invalid value`}, + code: 1, + }, + "simple_fail": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"invalid value", `│ - "bar" + │ + "zap"`}, + code: 1, + }, + "custom_condition_checks": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"this really should fail"}, + code: 1, + }, + "custom_condition_inputs": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"this should definitely fail"}, + code: 1, + }, + "custom_condition_outputs": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"this should fail"}, + code: 1, + }, + "custom_condition_resources": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"this really should fail"}, + code: 1, + }, + "no_providers_in_main": { + expectedOut: []string{"1 passed, 0 failed"}, + code: 0, + }, + "default_variables": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "undefined_variables": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "shared_state": { + expectedOut: []string{"8 passed, 0 failed."}, + code: 0, + }, + "shared_state_object": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "variable_references": { + expectedOut: []string{"2 passed, 0 failed."}, + args: []string{"-var=global=\"triple\""}, + code: 0, + }, + "unreferenced_global_variable": { + override: "variable_references", + expectedOut: []string{"2 passed, 0 failed."}, + // The other variable shouldn't pass validation, but it won't be + // referenced anywhere so should just be ignored. + args: []string{"-var=global=\"triple\"", "-var=other=bad"}, + code: 0, + }, + "variables_types": { + expectedOut: []string{"1 passed, 0 failed."}, + args: []string{"-var=number_input=0", "-var=string_input=Hello, world!", "-var=list_input=[\"Hello\",\"world\"]"}, + code: 0, + }, + "null-outputs": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "destroy_fail": { + expectedOut: []string{"3 passed, 0 failed."}, + expectedErr: []string{`Terraform left the following resources in state`}, + code: 1, + expectedResourceCount: 4, + }, + "default_optional_values": { + expectedOut: []string{"4 passed, 0 failed."}, + code: 0, + }, + "tfvars_in_test_dir": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "auto_tfvars_in_test_dir": { + override: "tfvars_in_test_dir", + args: []string{"-test-directory=alternate"}, + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "functions_available": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "provider-functions-available": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "mocking": { + expectedOut: []string{"10 passed, 0 failed."}, + code: 0, + }, + "mocking-invalid": { + expectedErr: []string{ + "Invalid outputs attribute", + "The override_during attribute must be a value of plan or apply.", }, - } - exitStatus := cmd.Run([]string{"-junit-xml=junit.xml", "-no-color"}) - outp := close(t) - if got, want := exitStatus, 0; got != want { - t.Fatalf("wrong exit status %d; want %d\nstderr:\n%s", got, want, outp.Stderr()) - } - - gotStdout := strings.TrimSpace(outp.Stdout()) - wantStdout := strings.TrimSpace(` -Warning: The "terraform test" command is experimental - -We'd like to invite adventurous module authors to write integration tests for -their modules using this command, but all of the behaviors of this command -are currently experimental and may change based on feedback. - -For more information on the testing experiment, including ongoing research -goals and avenues for feedback, see: - https://www.terraform.io/docs/language/modules/testing-experiment.html -`) - if diff := cmp.Diff(wantStdout, gotStdout); diff != "" { - t.Errorf("wrong stdout\n%s", diff) - } - - gotStderr := strings.TrimSpace(outp.Stderr()) - wantStderr := strings.TrimSpace(` -Success! All of the test assertions passed. -`) - if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { - t.Errorf("wrong stderr\n%s", diff) - } - - gotXMLSrc, err := ioutil.ReadFile("junit.xml") - if err != nil { - t.Fatal(err) - } - gotXML := string(bytes.TrimSpace(gotXMLSrc)) - wantXML := strings.TrimSpace(` - - 0 - 0 - 1 - - hello - 1 - 0 - 0 - 0 - - output - foo - - - -`) - if diff := cmp.Diff(wantXML, gotXML); diff != "" { - t.Errorf("wrong JUnit XML\n%s", diff) - } - }) - t.Run("fails", func(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath("test-fails"), td) - defer testChdir(t, td)() - - streams, close := terminal.StreamsForTesting(t) - cmd := &TestCommand{ - Meta: Meta{ - Streams: streams, - View: views.NewView(streams), + initCode: 1, + }, + "mocking-error": { + expectedErr: []string{ + "Unknown condition value", + "plan_mocked_overridden.tftest.hcl", + "test_resource.primary[0].id", + "plan_mocked_provider.tftest.hcl", + "test_resource.secondary[0].id", }, - } - exitStatus := cmd.Run([]string{"-junit-xml=junit.xml", "-no-color"}) - outp := close(t) - if got, want := exitStatus, 1; got != want { - t.Fatalf("wrong exit status %d; want %d\nstderr:\n%s", got, want, outp.Stderr()) - } + code: 1, + }, + "dangling_data_block": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "skip_destroy_on_empty": { + expectedOut: []string{"3 passed, 0 failed."}, + code: 0, + }, + "empty_module_with_output": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "global_var_refs": { + expectedOut: []string{"1 passed, 2 failed."}, + expectedErr: []string{"The input variable \"env_var_input\" is not available to the current context", "The input variable \"setup\" is not available to the current context"}, + code: 1, + }, + "global_var_ref_in_suite_var": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "env-vars": { + expectedOut: []string{"1 passed, 0 failed."}, + envVars: map[string]string{ + "TF_VAR_input": "foo", + }, + code: 0, + }, + "env-vars-in-module": { + expectedOut: []string{"2 passed, 0 failed."}, + envVars: map[string]string{ + "TF_VAR_input": "foo", + }, + code: 0, + }, + "ephemeral_input": { + expectedOut: []string{"2 passed, 0 failed."}, + code: 0, + }, + "ephemeral_input_with_error": { + expectedOut: []string{"Error message refers to ephemeral values", "1 passed, 1 failed."}, + expectedErr: []string{"Test assertion failed", + `│ - "(ephemeral value)" + │ + "bar"`}, + code: 1, + }, + "ephemeral_resource": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"Ephemeral resource instance has expired", "Ephemeral resources cannot be asserted"}, + code: 1, + }, + "with_state_key": { + expectedOut: []string{"3 passed, 1 failed."}, + expectedErr: []string{"Test assertion failed", "resource renamed without moved block"}, + code: 1, + }, + "unapplyable-plan": { + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"Cannot apply non-applyable plan"}, + code: 1, + }, + "write-only-attributes": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "write-only-attributes-mocked": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + "write-only-attributes-overridden": { + expectedOut: []string{"1 passed, 0 failed."}, + code: 0, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + if tc.skip { + t.Skip() + } - gotStdout := strings.TrimSpace(outp.Stdout()) - wantStdout := strings.TrimSpace(` -Warning: The "terraform test" command is experimental + for k, v := range tc.envVars { + os.Setenv(k, v) + } + defer func() { + for k := range tc.envVars { + os.Unsetenv(k) + } + }() -We'd like to invite adventurous module authors to write integration tests for -their modules using this command, but all of the behaviors of this command -are currently experimental and may change based on feedback. + file := name + if len(tc.override) > 0 { + file = tc.override + } -For more information on the testing experiment, including ongoing research -goals and avenues for feedback, see: - https://www.terraform.io/docs/language/modules/testing-experiment.html -`) - if diff := cmp.Diff(wantStdout, gotStdout); diff != "" { - t.Errorf("wrong stdout\n%s", diff) - } + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", file)), td) + defer testChdir(t, td)() - gotStderr := strings.TrimSpace(outp.Stderr()) - wantStderr := strings.TrimSpace(` -─── Failed: hello.foo.output (output "foo" value) ─────────────────────────── -wrong value - got: "foo value boop" - want: "foo not boop" + provider := testing_command.NewProvider(nil) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() -───────────────────────────────────────────────────────────────────────────── -`) - if diff := cmp.Diff(wantStderr, gotStderr); diff != "" { - t.Errorf("wrong stderr\n%s", diff) - } + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) - gotXMLSrc, err := ioutil.ReadFile("junit.xml") - if err != nil { - t.Fatal(err) - } - gotXML := string(bytes.TrimSpace(gotXMLSrc)) - wantXML := strings.TrimSpace(` - - 0 - 1 - 1 - - hello - 1 - 0 - 0 - 1 - - output - foo - - wrong value got: "foo value boop" want: "foo not boop" - - - - -`) - if diff := cmp.Diff(wantXML, gotXML); diff != "" { - t.Errorf("wrong JUnit XML\n%s", diff) - } + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != tc.initCode { + output := done(t) + t.Fatalf("expected status code %d but got %d: %s", tc.initCode, code, output.All()) + } + + if tc.initCode > 0 { + // Then we don't expect the init step to succeed. So we'll check + // the init output for our expected error messages and outputs. + output := done(t).All() + stdout, stderr := output, output + + if len(tc.expectedOut) > 0 { + for _, expectedOut := range tc.expectedOut { + if !strings.Contains(stdout, expectedOut) { + t.Errorf("output didn't contain expected string:\n\n%s", stdout) + } + } + } + + if len(tc.expectedErr) > 0 { + for _, expectedErr := range tc.expectedErr { + if !strings.Contains(stderr, expectedErr) { + t.Errorf("error didn't contain expected string:\n\n%s", stderr) + } + } + } else if stderr != "" { + t.Errorf("unexpected stderr output\n%s", stderr) + } + + // If `terraform init` failed, then we don't expect that + // `terraform test` will have run at all, so we can just return + // here. + return + } + + // discard the output from the init command + done(t) + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run(append(tc.args, "-no-color")) + output := done(t) + + if code != tc.code { + t.Errorf("expected status code %d but got %d:\n\n%s", tc.code, code, output.All()) + } + + if len(tc.expectedOut) > 0 { + for _, expectedOut := range tc.expectedOut { + if !strings.Contains(output.Stdout(), expectedOut) { + t.Errorf("output didn't contain expected string:\n\n%s", output.Stdout()) + } + } + } + + if len(tc.expectedErr) > 0 { + for _, expectedErr := range tc.expectedErr { + if !strings.Contains(output.Stderr(), expectedErr) { + t.Errorf("error didn't contain expected string:\n\n%s", output.Stderr()) + } + } + } else if output.Stderr() != "" { + t.Errorf("unexpected stderr output\n%s", output.Stderr()) + } + + if provider.ResourceCount() != tc.expectedResourceCount { + t.Errorf("should have left %d resources on completion but left %v", tc.expectedResourceCount, provider.ResourceString()) + } + }) + } +} + +func TestTest_Interrupt(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "with_interrupt")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + interrupt := make(chan struct{}) + provider.Interrupt = interrupt + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + ShutdownCh: interrupt, + }, + } + + c.Run(nil) + output := done(t).All() + + if !strings.Contains(output, "Interrupt received") { + t.Errorf("output didn't produce the right output:\n\n%s", output) + } + + if provider.ResourceCount() > 0 { + // we asked for a nice stop in this one, so it should still have tidied everything up. + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_DestroyFail(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "destroy_fail")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, }) + defer close() + + view, done := testView(t) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + ProviderSource: providerSource, + } + + init := &InitCommand{Meta: meta} + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code %d but got %d: %s", 0, code, output.All()) + } + + interrupt := make(chan struct{}) + provider.Interrupt = interrupt + view, done = testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + ShutdownCh: interrupt, + }, + } + + c.Run([]string{"-no-color"}) + output := done(t) + err := output.Stderr() + + cleanupMessage := `main.tftest.hcl... in progress + run "setup"... pass + run "single"... pass + run "double"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 3 passed, 0 failed. +` + + cleanupErr := `Terraform encountered an error destroying resources created while executing +main.tftest.hcl/double. + +Error: Failed to destroy resource + +destroy_fail is set to true + +Error: Failed to destroy resource + +destroy_fail is set to true + +Terraform left the following resources in state after executing +main.tftest.hcl/double, and they need to be cleaned up manually: + - test_resource.another + - test_resource.resource +Terraform encountered an error destroying resources created while executing +main.tftest.hcl/single. + +Error: Failed to destroy resource + +destroy_fail is set to true + +Error: Failed to destroy resource + +destroy_fail is set to true + +Terraform left the following resources in state after executing +main.tftest.hcl/single, and they need to be cleaned up manually: + - test_resource.another + - test_resource.resource +` + + // It's really important that the above message is printed, so we're testing + // for it specifically and making sure it contains all the resources. + if diff := cmp.Diff(cleanupErr, err); diff != "" { + t.Errorf("expected err to be %s\n\nbut got %s\n\n diff:%s\n", cleanupErr, err, diff) + } + if diff := cmp.Diff(cleanupMessage, output.Stdout()); diff != "" { + t.Errorf("expected output to be %s\n\nbut got %s\n\n diff:%s\n", cleanupMessage, output.Stdout(), diff) + } + + // This time the test command shouldn't have cleaned up the resource because + // the destroy failed. + if provider.ResourceCount() != 4 { + t.Errorf("should not have deleted all resources on completion but only has %v", provider.ResourceString()) + } +} + +func TestTest_SharedState_Order(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "shared_state")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code %d but got %d: %s", 9, code, output.All()) + } + + c := &TestCommand{ + Meta: meta, + } + + c.Run(nil) + output := done(t).All() + + // Split the log into lines + lines := strings.Split(output, "\n") + + var arr []string + for _, line := range lines { + if strings.Contains(line, "run \"") && strings.Contains(line, "\x1b[32mpass") { + arr = append(arr, line) + } + } + + // Ensure the order of the tests is correct. Even though they share no state, + // the order should be sequential. + expectedOrder := []string{ + // main.tftest.hcl + "run \"setup\"", + "run \"test\"", + + // no-shared-state.tftest.hcl + "run \"setup\"", + "run \"test_a\"", + "run \"test_b\"", + "run \"test_c\"", + "run \"test_d\"", + "run \"test_e\"", + } + + for i, line := range expectedOrder { + if !strings.Contains(arr[i], line) { + t.Errorf("unexpected test order: expected %q, got %q", line, arr[i]) + } + } +} + +func TestTest_Parallel_Divided_Order(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "parallel_divided")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code %d but got %d: %s", 9, code, output.All()) + } + + c := &TestCommand{ + Meta: meta, + } + + c.Run(nil) + output := done(t).All() + + // Split the log into lines + lines := strings.Split(output, "\n") + + // Find the positions of the tests in the log output + var mainFirstIndex, mainSecondIndex, mainThirdIndex, mainFourthIndex, mainFifthIndex, mainSixthIndex int + for i, line := range lines { + if strings.Contains(line, "run \"main_first\"") { + mainFirstIndex = i + } else if strings.Contains(line, "run \"main_second\"") { + mainSecondIndex = i + } else if strings.Contains(line, "run \"main_third\"") { + mainThirdIndex = i + } else if strings.Contains(line, "run \"main_fourth\"") { + mainFourthIndex = i + } else if strings.Contains(line, "run \"main_fifth\"") { + mainFifthIndex = i + } else if strings.Contains(line, "run \"main_sixth\"") { + mainSixthIndex = i + } + } + if mainFirstIndex == 0 || mainSecondIndex == 0 || mainThirdIndex == 0 || mainFourthIndex == 0 || mainFifthIndex == 0 || mainSixthIndex == 0 { + t.Fatalf("one or more tests not found in the log output") + } + + // Ensure the order of the tests is correct. The runs before main_fourth can execute in parallel. + if mainFirstIndex > mainFourthIndex || mainSecondIndex > mainFourthIndex || mainThirdIndex > mainFourthIndex { + t.Errorf("main_first, main_second, or main_third appears after main_fourth in the log output") + } + + // Ensure main_fifth and main_sixth do not execute before main_fourth + if mainFifthIndex < mainFourthIndex { + t.Errorf("main_fifth appears before main_fourth in the log output") + } + if mainSixthIndex < mainFourthIndex { + t.Errorf("main_sixth appears before main_fourth in the log output") + } +} + +func TestTest_Parallel(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "parallel")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code %d but got %d: %s", 9, code, output.All()) + } + + c := &TestCommand{ + Meta: meta, + } + + c.Run(nil) + output := done(t).All() + + if !strings.Contains(output, "40 passed, 0 failed") { + t.Errorf("output didn't produce the right output:\n\n%s", output) + } + + // Split the log into lines + lines := strings.Split(output, "\n") + + // Find the positions of "test_d", "test_c", "test_setup" in the log output + var testDIndex, testCIndex, testSetupIndex int + for i, line := range lines { + if strings.Contains(line, "run \"setup\"") { + testSetupIndex = i + } else if strings.Contains(line, "run \"test_d\"") { + testDIndex = i + } else if strings.Contains(line, "run \"test_c\"") { + testCIndex = i + } + } + if testDIndex == 0 || testCIndex == 0 || testSetupIndex == 0 { + t.Fatalf("test_d, test_c, or test_setup not found in the log output") + } + + // Ensure "test_d" appears before "test_c", because test_d has no dependencies, + // and would therefore run in parallel to much earlier tests which test_c depends on. + if testDIndex > testCIndex { + t.Errorf("test_d appears after test_c in the log output") + } + + // Ensure "test_d" appears after "test_setup", because they have the same state key + if testDIndex < testSetupIndex { + t.Errorf("test_d appears before test_setup in the log output") + } +} + +func TestTest_InterruptSkipsRemaining(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "with_interrupt_and_additional_file")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + interrupt := make(chan struct{}) + provider.Interrupt = interrupt + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + ShutdownCh: interrupt, + }, + } + + c.Run([]string{"-no-color"}) + output := done(t).All() + + if !strings.Contains(output, "skip_me.tftest.hcl... skip") { + t.Errorf("output didn't produce the right output:\n\n%s", output) + } + + if provider.ResourceCount() > 0 { + // we asked for a nice stop in this one, so it should still have tidied everything up. + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_DoubleInterrupt(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "with_double_interrupt")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + interrupt := make(chan struct{}) + provider.Interrupt = interrupt + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + ShutdownCh: interrupt, + }, + } + + c.Run(nil) + output := done(t).All() + + if !strings.Contains(output, "Two interrupts received") { + t.Errorf("output didn't produce the right output:\n\n%s", output) + } + + cleanupMessage := `Terraform was interrupted while executing main.tftest.hcl, and may not have +performed the expected cleanup operations. + +Terraform has already created the following resources from the module under +test: + - test_resource.primary + - test_resource.secondary + - test_resource.tertiary` + + // It's really important that the above message is printed, so we're testing + // for it specifically and making sure it contains all the resources. + if !strings.Contains(output, cleanupMessage) { + t.Errorf("output didn't produce the right output:\n\n%s", output) + } + + // This time the test command shouldn't have cleaned up the resource because + // of the hard interrupt. + if provider.ResourceCount() != 3 { + // we asked for a nice stop in this one, so it should still have tidied everything up. + t.Errorf("should not have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_ProviderAlias(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "with_provider_alias")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + output := done(t) + + if code := init.Run([]string{"-no-color"}); code != 0 { + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + command := &TestCommand{ + Meta: meta, + } + + code := command.Run([]string{"-no-color"}) + output = done(t) + + printedOutput := false + + if code != 0 { + printedOutput = true + t.Errorf("expected status code 0 but got %d: %s", code, output.All()) + } + + if provider.ResourceCount() > 0 { + if !printedOutput { + t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All()) + } else { + t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString()) + } + } +} + +func TestTest_ComplexCondition(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "complex_condition")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{"test": {"1.0.0"}}) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + output := done(t) + + if code := init.Run([]string{"-no-color"}); code != 0 { + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + command := &TestCommand{ + Meta: meta, + } + + code := command.Run([]string{"-no-color"}) + output = done(t) + + printedOutput := false + + if code != 1 { + printedOutput = true + t.Errorf("expected status code 1 but got %d: %s", code, output.All()) + } + + expectedOut := `main.tftest.hcl... in progress + run "validate_diff_types"... fail + run "validate_output"... fail + run "validate_complex_output"... fail + run "validate_complex_output_sensitive"... fail + run "validate_complex_output_pass"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 1 passed, 4 failed. +` + + expectedErr := ` +Error: Test assertion failed + + on main.tftest.hcl line 37, in run "validate_diff_types": + 37: condition = var.tr1 == var.tr2 + ├──────────────── + │ Warning: LHS and RHS values are of different types + + +expected to fail + +Error: Test assertion failed + + on main.tftest.hcl line 44, in run "validate_output": + 44: condition = output.foo == var.foo + ├──────────────── + │ Diff: + │ --- actual + │ +++ expected + │ { + │ - "bar": "notbaz", + │ + "bar": "baz", + │ "matches": "matches", + │ - "qux": "quux", + │ - "xuq": "xuq" + │ + "qux": "qux", + │ + "xuq": "nope" + │ } + + +expected to fail due to different values + +Error: Test assertion failed + + on main.tftest.hcl line 52, in run "validate_complex_output": + 52: condition = output.complex == var.bar + ├──────────────── + │ Warning: LHS and RHS values are of different types + │ Diff: + │ --- actual + │ +++ expected + │ { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ - "qux": "quux" + │ + "qux": "qux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + + +expected to fail + +Error: Test assertion failed + + on main.tftest.hcl line 60, in run "validate_complex_output_sensitive": + 60: condition = output.complex == output.complex_sensitive + ├──────────────── + │ Diff: + │ --- actual + │ +++ expected + │ - { + │ - "root": [ + │ - { + │ - "bar": [ + │ - 1 + │ - ], + │ - "qux": "quux" + │ - }, + │ - { + │ - "bar": [ + │ - 2 + │ - ], + │ - "qux": "quux" + │ - } + │ - ] + │ - } + │ + "(sensitive value)" + + +expected to fail +` + if diff := cmp.Diff(output.Stdout(), expectedOut); len(diff) > 0 { + t.Errorf("\nexpected: \n%s\ngot: %s\ndiff: %s", expectedOut, output.All(), diff) + } + if diff := cmp.Diff(output.Stderr(), expectedErr); len(diff) > 0 { + t.Errorf("\nexpected stderr: \n%s\ngot: %s\ndiff: %s", expectedErr, output.Stderr(), diff) + } + + if provider.ResourceCount() > 0 { + if !printedOutput { + t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All()) + } else { + t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString()) + } + } +} + +func TestTest_ComplexConditionVerbose(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "complex_condition")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{"test": {"1.0.0"}}) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + output := done(t) + + if code := init.Run([]string{"-no-color"}); code != 0 { + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + command := &TestCommand{ + Meta: meta, + } + + code := command.Run([]string{"-no-color", "-verbose"}) + output = done(t) + + printedOutput := false + + if code != 1 { + printedOutput = true + t.Errorf("expected status code 1 but got %d: %s", code, output.All()) + } + + expectedErr := ` +Error: Test assertion failed + + on main.tftest.hcl line 37, in run "validate_diff_types": + 37: condition = var.tr1 == var.tr2 + ├──────────────── + │ LHS: + │ { + │ "iops": null, + │ "size": 60 + │ } + │ RHS: + │ { + │ "iops": null, + │ "size": 60 + │ } + │ Warning: LHS and RHS values are of different types + + │ var.tr1 is { + │ "iops": null, + │ "size": 60 + │ } + │ var.tr2 is { + │ "iops": null, + │ "size": 60 + │ } + +expected to fail + +Error: Test assertion failed + + on main.tftest.hcl line 44, in run "validate_output": + 44: condition = output.foo == var.foo + ├──────────────── + │ LHS: + │ { + │ "bar": "notbaz", + │ "matches": "matches", + │ "qux": "quux", + │ "xuq": "xuq" + │ } + │ RHS: + │ { + │ "bar": "baz", + │ "matches": "matches", + │ "qux": "qux", + │ "xuq": "nope" + │ } + │ Diff: + │ --- actual + │ +++ expected + │ { + │ - "bar": "notbaz", + │ + "bar": "baz", + │ "matches": "matches", + │ - "qux": "quux", + │ - "xuq": "xuq" + │ + "qux": "qux", + │ + "xuq": "nope" + │ } + + │ output.foo is { + │ "bar": "notbaz", + │ "matches": "matches", + │ "qux": "quux", + │ "xuq": "xuq" + │ } + │ var.foo is { + │ "bar": "baz", + │ "matches": "matches", + │ "qux": "qux", + │ "xuq": "nope" + │ } + +expected to fail due to different values + +Error: Test assertion failed + + on main.tftest.hcl line 52, in run "validate_complex_output": + 52: condition = output.complex == var.bar + ├──────────────── + │ LHS: + │ { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ "qux": "quux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + │ RHS: + │ { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ "qux": "qux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + │ Warning: LHS and RHS values are of different types + │ Diff: + │ --- actual + │ +++ expected + │ { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ - "qux": "quux" + │ + "qux": "qux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + + │ output.complex is { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ "qux": "quux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + │ var.bar is { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ "qux": "qux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + +expected to fail + +Error: Test assertion failed + + on main.tftest.hcl line 60, in run "validate_complex_output_sensitive": + 60: condition = output.complex == output.complex_sensitive + ├──────────────── + │ LHS: + │ { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ "qux": "quux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + │ RHS: + │ "(sensitive value)" + │ Diff: + │ --- actual + │ +++ expected + │ - { + │ - "root": [ + │ - { + │ - "bar": [ + │ - 1 + │ - ], + │ - "qux": "quux" + │ - }, + │ - { + │ - "bar": [ + │ - 2 + │ - ], + │ - "qux": "quux" + │ - } + │ - ] + │ - } + │ + "(sensitive value)" + + │ output.complex is { + │ "root": [ + │ { + │ "bar": [ + │ 1 + │ ], + │ "qux": "quux" + │ }, + │ { + │ "bar": [ + │ 2 + │ ], + │ "qux": "quux" + │ } + │ ] + │ } + │ output.complex_sensitive is "(sensitive value)" + +expected to fail +` + outputs := []string{ + "main.tftest.hcl... in progress", + " run \"validate_diff_types\"... fail", + " run \"validate_output\"... fail", + " run \"validate_complex_output\"... fail", + " run \"validate_complex_output_sensitive\"... fail", + " run \"validate_complex_output_pass\"... pass", + "main.tftest.hcl... tearing down", + "main.tftest.hcl... fail", + "Failure! 1 passed, 4 failed.", + } + stdout := output.Stdout() + for _, expected := range outputs { + if !strings.Contains(stdout, expected) { + t.Errorf("output didn't contain expected output %q", expected) + } + } + + if diff := cmp.Diff(output.Stderr(), expectedErr); len(diff) > 0 { + t.Errorf("\nexpected stderr: \n%s\ngot: %s\ndiff: %s", expectedErr, output.Stderr(), diff) + } + + if provider.ResourceCount() > 0 { + if !printedOutput { + t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All()) + } else { + t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString()) + } + } +} + +func TestTest_ModuleDependencies(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "with_setup_module")), td) + defer testChdir(t, td)() + + // Our two providers will share a common set of values to make things + // easier. + store := &testing_command.ResourceStore{ + Data: make(map[string]cty.Value), + } + + // We set it up so the module provider will update the data sources + // available to the core mock provider. + test := testing_command.NewProvider(store) + setup := testing_command.NewProvider(store) + + test.SetDataPrefix("data") + test.SetResourcePrefix("resource") + + // Let's make the setup provider write into the data for test provider. + setup.SetResourcePrefix("data") + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + "setup": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: &testingOverrides{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): providers.FactoryFixed(test.Provider), + addrs.NewDefaultProvider("setup"): providers.FactoryFixed(setup.Provider), + }, + }, + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + command := &TestCommand{ + Meta: meta, + } + + code := command.Run(nil) + output := done(t) + + printedOutput := false + + if code != 0 { + printedOutput = true + t.Errorf("expected status code 0 but got %d: %s", code, output.All()) + } + + if test.ResourceCount() > 0 { + if !printedOutput { + printedOutput = true + t.Errorf("should have deleted all resources on completion but left %s\n\n%s", test.ResourceString(), output.All()) + } else { + t.Errorf("should have deleted all resources on completion but left %s", test.ResourceString()) + } + } + + if setup.ResourceCount() > 0 { + if !printedOutput { + t.Errorf("should have deleted all resources on completion but left %s\n\n%s", setup.ResourceString(), output.All()) + } else { + t.Errorf("should have deleted all resources on completion but left %s", setup.ResourceString()) + } + } +} + +func TestTest_CatchesErrorsBeforeDestroy(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "invalid_default_state")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 1 { + t.Errorf("expected status code 0 but got %d", code) + } + + expectedOut := `main.tftest.hcl... in progress + run "test"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +` + + expectedErr := ` +Error: No value for required variable + + on main.tf line 2: + 2: variable "input" { + +The module under test for run block "test" has a required variable "input" +with no set value. Use a -var or -var-file command line argument or add this +variable into a "variables" block within the test file or run block. +` + + actualOut := output.Stdout() + actualErr := output.Stderr() + + if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 { + t.Errorf("std out didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("std err didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_Verbose(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "plan_then_apply")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-verbose", "-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "validate_test_resource"... pass + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # test_resource.foo will be created + + resource "test_resource" "foo" { + + destroy_fail = (known after apply) + + id = "constant_value" + + value = "bar" + + write_only = (write-only attribute) + } + +Plan: 1 to add, 0 to change, 0 to destroy. + + run "apply_test_resource"... pass + +# test_resource.foo: +resource "test_resource" "foo" { + destroy_fail = false + id = "constant_value" + value = "bar" + write_only = (write-only attribute) +} + +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 2 passed, 0 failed. +` + + actual := output.All() + + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_ValidatesBeforeExecution(t *testing.T) { + tcs := map[string]struct { + expectedOut string + expectedErr string + }{ + "invalid": { + expectedOut: `main.tftest.hcl... in progress + run "invalid"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +`, + expectedErr: ` +Error: Invalid ` + "`expect_failures`" + ` reference + + on main.tftest.hcl line 5, in run "invalid": + 5: local.my_value, + +You cannot expect failures from local.my_value. You can only expect failures +from checkable objects such as input variables, output values, check blocks, +managed resources and data sources. +`, + }, + "invalid-module": { + expectedOut: `main.tftest.hcl... in progress + run "invalid"... fail + run "test"... skip +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed, 1 skipped. +`, + expectedErr: ` +Error: Reference to undeclared input variable + + on setup/main.tf line 3, in resource "test_resource" "setup": + 3: value = var.not_real // Oh no! + +An input variable with the name "not_real" has not been declared. This +variable can be declared with a variable "not_real" {} block. +`, + }, + "missing-provider": { + expectedOut: `main.tftest.hcl... in progress + run "passes_validation"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +`, + expectedErr: ` +Error: Provider configuration not present + +To work with test_resource.secondary its original provider configuration at +provider["registry.terraform.io/hashicorp/test"].secondary is required, but +it has been removed. This occurs when a provider configuration is removed +while objects created by that provider still exist in the state. Re-add the +provider configuration to destroy test_resource.secondary, after which you +can remove the provider configuration again. +`, + }, + "missing-provider-in-run-block": { + expectedOut: `main.tftest.hcl... in progress + run "passes_validation"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +`, + expectedErr: ` +Error: Provider configuration not present + +To work with test_resource.secondary its original provider configuration at +provider["registry.terraform.io/hashicorp/test"].secondary is required, but +it has been removed. This occurs when a provider configuration is removed +while objects created by that provider still exist in the state. Re-add the +provider configuration to destroy test_resource.secondary, after which you +can remove the provider configuration again. +`, + }, + "missing-provider-definition-in-file": { + expectedOut: `main.tftest.hcl... in progress + run "passes_validation"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +`, + expectedErr: ` +Error: Missing provider definition for test + + on main.tftest.hcl line 12, in run "passes_validation": + 12: test = test + +This provider block references a provider definition that does not exist. +`, + }, + "missing-provider-in-test-module": { + expectedOut: `main.tftest.hcl... in progress + run "passes_validation_primary"... pass + run "passes_validation_secondary"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 1 passed, 1 failed. +`, + expectedErr: ` +Error: Provider configuration not present + +To work with test_resource.secondary its original provider configuration at +provider["registry.terraform.io/hashicorp/test"].secondary is required, but +it has been removed. This occurs when a provider configuration is removed +while objects created by that provider still exist in the state. Re-add the +provider configuration to destroy test_resource.secondary, after which you +can remove the provider configuration again. +`, + }, + } + + for file, tc := range tcs { + t.Run(file, func(t *testing.T) { + + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", file)), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 1 { + t.Errorf("expected status code 1 but got %d", code) + } + + actualOut, expectedOut := output.Stdout(), tc.expectedOut + actualErr, expectedErr := output.Stderr(), tc.expectedErr + + if !strings.Contains(actualOut, expectedOut) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expectedOut, actualOut) + } + + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("error didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } + }) + } +} + +func TestTest_NestedSetupModules(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "with_nested_setup_modules")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + } + + command := &TestCommand{ + Meta: meta, + } + + code := command.Run(nil) + output := done(t) + + printedOutput := false + + if code != 0 { + printedOutput = true + t.Errorf("expected status code 0 but got %d: %s", code, output.All()) + } + + if provider.ResourceCount() > 0 { + if !printedOutput { + t.Errorf("should have deleted all resources on completion but left %s\n\n%s", provider.ResourceString(), output.All()) + } else { + t.Errorf("should have deleted all resources on completion but left %s", provider.ResourceString()) + } + } +} + +func TestTest_StatePropagation(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "state_propagation")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-verbose", "-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "initial_apply_example"... pass + +# test_resource.module_resource: +resource "test_resource" "module_resource" { + destroy_fail = false + id = "df6h8as9" + value = "start" + write_only = (write-only attribute) +} + + run "initial_apply"... pass + +# test_resource.resource: +resource "test_resource" "resource" { + destroy_fail = false + id = "598318e0" + value = "start" + write_only = (write-only attribute) +} + + run "plan_second_example"... pass + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # test_resource.second_module_resource will be created + + resource "test_resource" "second_module_resource" { + + destroy_fail = (known after apply) + + id = "b6a1d8cb" + + value = "start" + + write_only = (write-only attribute) + } + +Plan: 1 to add, 0 to change, 0 to destroy. + + run "plan_update"... pass + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # test_resource.resource will be updated in-place + ~ resource "test_resource" "resource" { + id = "598318e0" + ~ value = "start" -> "update" + # (2 unchanged attributes hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + + run "plan_update_example"... pass + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # test_resource.module_resource will be updated in-place + ~ resource "test_resource" "module_resource" { + id = "df6h8as9" + ~ value = "start" -> "update" + # (2 unchanged attributes hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 5 passed, 0 failed. +` + + actual := output.All() + + if diff := cmp.Diff(expected, actual); diff != "" { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_OnlyExternalModules(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "only_modules")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "first"... pass + run "second"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 2 passed, 0 failed. +` + + actual := output.Stdout() + + if !strings.Contains(actual, expected) { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s", expected, actual) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_PartialUpdates(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "partial_updates")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "first"... pass + +Warning: Resource targeting is in effect + +You are creating a plan with the -target option, which means that the result +of this plan may not represent all of the changes requested by the current +configuration. + +The -target option is not for routine use, and is provided only for +exceptional situations such as recovering from errors or mistakes, or when +Terraform specifically suggests to use it as part of an error message. + +Warning: Applied changes may be incomplete + +The plan was created with the -target option in effect, so some changes +requested in the configuration may have been ignored and the output values +may not be fully updated. Run the following command to verify that no other +changes are pending: + terraform plan + +Note that the -target option is not suitable for routine use, and is provided +only for exceptional situations such as recovering from errors or mistakes, +or when Terraform specifically suggests to use it as part of an error +message. + + run "second"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 2 passed, 0 failed. +` + + actual := output.All() + + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +// There should not be warnings in clean-up +func TestTest_InvalidWarningsInCleanup(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "invalid-cleanup-warnings")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "test"... pass + +Warning: Value for undeclared variable + + on main.tftest.hcl line 6, in run "test": + 6: validation = "Hello, world!" + +The module under test does not declare a variable named "validation", but it +is declared in run block "test". + +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 1 passed, 0 failed. +` + + actual := output.All() + + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_BadReferences(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "bad-references")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code == 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expectedOut := `main.tftest.hcl... in progress +main.tftest.hcl... tearing down +main.tftest.hcl... fail +providers.tftest.hcl... in progress + run "test"... fail +providers.tftest.hcl... tearing down +providers.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +` + actualOut := output.Stdout() + if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + expectedErr := ` +Error: Reference to unavailable variable + + on main.tftest.hcl line 15, in run "test": + 15: input_one = var.notreal + +The input variable "notreal" is not available to the current run block. You +can only reference variables defined at the file or global levels. + +Error: Reference to unavailable run block + + on main.tftest.hcl line 16, in run "test": + 16: input_two = run.finalise.response + +The run block "finalise" has not executed yet. You can only reference run +blocks that are in the same test file and will execute before the current run +block. + +Error: Reference to unknown run block + + on main.tftest.hcl line 17, in run "test": + 17: input_three = run.madeup.response + +The run block "madeup" does not exist within this test file. You can only +reference run blocks that are in the same test file and will execute before +the current run block. + +Error: Reference to unavailable variable + + on providers.tftest.hcl line 3, in provider "test": + 3: resource_prefix = var.default + +The input variable "default" is not available to the current provider +configuration. You can only reference variables defined at the file or global +levels. +` + actualErr := output.Stderr() + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_UndefinedVariables(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "variables_undefined_in_config")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code == 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expectedOut := `main.tftest.hcl... in progress + run "test"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +` + actualOut := output.Stdout() + if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + expectedErr := ` +Error: Reference to undeclared input variable + + on main.tf line 2, in resource "test_resource" "foo": + 2: value = var.input + +An input variable with the name "input" has not been declared. This variable +can be declared with a variable "input" {} block. +` + actualErr := output.Stderr() + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_VariablesInProviders(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "provider_vars")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "test"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 1 passed, 0 failed. +` + actual := output.All() + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_ExpectedFailuresDuringPlanning(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "expected_failures_during_planning")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code == 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expectedOut := `check.tftest.hcl... in progress + run "check_passes"... pass +check.tftest.hcl... tearing down +check.tftest.hcl... pass +input.tftest.hcl... in progress + run "input_failure"... fail + +Warning: Expected failure while planning + +A custom condition within var.input failed during the planning stage and +prevented the requested apply operation. While this was an expected failure, +the apply operation could not be executed and so the overall test case will +be marked as a failure and the original diagnostic included in the test +report. + + run "no_run"... skip +input.tftest.hcl... tearing down +input.tftest.hcl... fail +output.tftest.hcl... in progress + run "output_failure"... fail + +Warning: Expected failure while planning + + on output.tftest.hcl line 13, in run "output_failure": + 13: output.output, + +A custom condition within output.output failed during the planning stage and +prevented the requested apply operation. While this was an expected failure, +the apply operation could not be executed and so the overall test case will +be marked as a failure and the original diagnostic included in the test +report. + +output.tftest.hcl... tearing down +output.tftest.hcl... fail +resource.tftest.hcl... in progress + run "resource_failure"... fail + +Warning: Expected failure while planning + +A custom condition within test_resource.resource failed during the planning +stage and prevented the requested apply operation. While this was an expected +failure, the apply operation could not be executed and so the overall test +case will be marked as a failure and the original diagnostic included in the +test report. + +resource.tftest.hcl... tearing down +resource.tftest.hcl... fail + +Failure! 1 passed, 3 failed, 1 skipped. +` + actualOut := output.Stdout() + if diff := cmp.Diff(expectedOut, actualOut); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + expectedErr := ` +Error: Invalid value for variable + + on input.tftest.hcl line 5, in run "input_failure": + 5: input = "bcd" + ├──────────────── + │ var.input is "bcd" + +input must contain the character 'a' + +This was checked by the validation rule at main.tf:5,3-13. + +Error: Module output value precondition failed + + on main.tf line 33, in output "output": + 33: condition = strcontains(test_resource.resource.value, "d") + ├──────────────── + │ test_resource.resource.value is "abc" + +input must contain the character 'd' + +Error: Resource postcondition failed + + on main.tf line 16, in resource "test_resource" "resource": + 16: condition = strcontains(self.value, "b") + ├──────────────── + │ self.value is "acd" + +input must contain the character 'b' +` + actualErr := output.Stderr() + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_MissingExpectedFailuresDuringApply(t *testing.T) { + // Test asserting that the test run fails, but not errors out, when expected failures are not present during apply. + // This lets subsequent runs continue to execute and the file to be marked as failed. + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "expect_failures_during_apply")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code == 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expectedOut := `main.tftest.hcl... in progress + run "test"... fail + run "follow-up"... pass + +Warning: Value for undeclared variable + + on main.tftest.hcl line 16, in run "follow-up": + 16: input = "does not matter" + +The module under test does not declare a variable named "input", but it is +declared in run block "follow-up". + +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 1 passed, 1 failed. +` + actualOut := output.Stdout() + if diff := cmp.Diff(expectedOut, actualOut); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + expectedErr := ` +Error: Missing expected failure + + on main.tftest.hcl line 7, in run "test": + 7: output.output + +The checkable object, output.output, was expected to report an error but did +not. +` + actualErr := output.Stderr() + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_UnknownAndNulls(t *testing.T) { + + tcs := map[string]struct { + code int + stdout string + stderr string + }{ + "null_value_in_assert": { + code: 1, + stdout: `main.tftest.hcl... in progress + run "first"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +`, + stderr: ` +Error: Test assertion failed + + on main.tftest.hcl line 8, in run "first": + 8: condition = test_resource.resource.value == output.null_output + ├──────────────── + │ Warning: LHS and RHS values are of different types + │ Diff: + │ --- actual + │ +++ expected + │ - "bar" + │ + null + + +this is always going to fail +`, + }, + "null_value_in_vars": { + code: 1, + stdout: `fail.tftest.hcl... in progress + run "first"... pass + run "second"... fail +fail.tftest.hcl... tearing down +fail.tftest.hcl... fail +pass.tftest.hcl... in progress + run "first"... pass + run "second"... pass +pass.tftest.hcl... tearing down +pass.tftest.hcl... pass + +Failure! 3 passed, 1 failed. +`, + stderr: ` +Error: Required variable not set + + on fail.tftest.hcl line 11, in run "second": + 11: interesting_input = run.first.null_output + +The given value is not suitable for var.interesting_input defined at +main.tf:7,1-29: required variable may not be set to null. +`, + }, + "unknown_value_in_assert": { + code: 1, + stdout: `main.tftest.hcl... in progress + run "one"... pass + run "two"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 1 passed, 1 failed. +`, + stderr: fmt.Sprintf(` +Error: Unknown condition value + + on main.tftest.hcl line 8, in run "two": + 8: condition = output.destroy_fail == run.one.destroy_fail + ├──────────────── + │ output.destroy_fail is false + +Condition expression could not be evaluated at this time. This means you have +executed a %s block with %s and one of the values your +condition depended on is not known until after the plan has been applied. +Either remove this value from your condition, or execute an %s command +from this %s block. Alternatively, if there is an override for this value, +you can make it available during the plan phase by setting %s in the %s block. +`, "`run`", "`command = plan`", "`apply`", "`run`", "`override_during =\nplan`", "`override_`"), + }, + "unknown_value_in_vars": { + code: 1, + stdout: `main.tftest.hcl... in progress + run "one"... pass + run "two"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 1 passed, 1 failed. +`, + stderr: ` +Error: Reference to unknown value + + on main.tftest.hcl line 8, in run "two": + 8: destroy_fail = run.one.destroy_fail + +The value for run.one.destroy_fail is unknown. Run block "one" is executing a +"plan" operation, and the specified output value is only known after apply. +`, + }, + "nested_unknown_values": { + code: 1, + stdout: `main.tftest.hcl... in progress + run "first"... pass + run "second"... pass + run "third"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 2 passed, 1 failed. +`, + stderr: ` +Error: Reference to unknown value + + on main.tftest.hcl line 31, in run "third": + 31: input = run.second + +The value for run.second is unknown. Run block "second" is executing a "plan" +operation, and the specified output value is only known after apply. +`, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", name)), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != tc.code { + t.Errorf("expected return code %d but got %d", tc.code, code) + } + + expectedOut := tc.stdout + actualOut := output.Stdout() + if diff := cmp.Diff(expectedOut, actualOut); len(diff) > 0 { + t.Errorf("unexpected output\n\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + expectedErr := tc.stderr + actualErr := output.Stderr() + if diff := cmp.Diff(expectedErr, actualErr); len(diff) > 0 { + t.Errorf("unexpected output\n\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + }) + } } + +func TestTest_SensitiveInputValues(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "sensitive_input_values")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color", "-verbose"}) + output := done(t) + + if code != 1 { + t.Errorf("expected status code 1 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "setup"... pass + + + +Outputs: + +password = (sensitive value) + + run "test"... pass + +# test_resource.resource: +resource "test_resource" "resource" { + destroy_fail = false + id = "9ddca5a9" + value = (sensitive value) + write_only = (write-only attribute) +} + + +Outputs: + +password = (sensitive value) + + run "test_failed"... fail + +# test_resource.resource: +resource "test_resource" "resource" { + destroy_fail = false + id = "9ddca5a9" + value = (sensitive value) + write_only = (write-only attribute) +} + + +Outputs: + +password = (sensitive value) + +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 2 passed, 1 failed. +` + + expectedErr := ` +Error: Test assertion failed + + on main.tftest.hcl line 27, in run "test_failed": + 27: condition = var.complex == { + 28: foo = "bar" + 29: baz = test_resource.resource.id + 30: } + ├──────────────── + │ LHS: + │ { + │ "baz": "(sensitive value)", + │ "foo": "bar" + │ } + │ RHS: + │ { + │ "baz": "9ddca5a9", + │ "foo": "bar" + │ } + │ Diff: + │ --- actual + │ +++ expected + │ { + │ - "baz": "(sensitive value)", + │ + "baz": "9ddca5a9", + │ "foo": "bar" + │ } + + │ test_resource.resource.id is "9ddca5a9" + │ var.complex is { + │ "baz": "(sensitive value)", + │ "foo": "bar" + │ } + +expected to fail +` + + actual := output.Stdout() + + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if diff := cmp.Diff(output.Stderr(), expectedErr); len(diff) > 0 { + t.Errorf("stderr didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, output.Stderr(), diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +// This test takes around 10 seconds to complete, as we're testing the progress +// updates that are printed every 2 seconds. Sorry! +func TestTest_LongRunningTest(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "long_running")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + actual := output.All() + expected := `main.tftest.hcl... in progress + run "test"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 1 passed, 0 failed. +` + + if code != 0 { + t.Errorf("expected return code %d but got %d", 0, code) + } + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("unexpected output\n\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} + +// This test takes around 10 seconds to complete, as we're testing the progress +// updates that are printed every 2 seconds. Sorry! +func TestTest_LongRunningTestJSON(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "long_running")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + code := c.Run([]string{"-json"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + actual := output.All() + var messages []string + for ix, line := range strings.Split(actual, "\n") { + if len(line) == 0 { + // Skip empty lines. + continue + } + + if ix == 0 { + // skip the first one, it's version information + continue + } + + var obj map[string]interface{} + + if err := json.Unmarshal([]byte(line), &obj); err != nil { + t.Errorf("failed to unmarshal returned line: %s", line) + continue + } + + // Remove the timestamp as it changes every time. + delete(obj, "@timestamp") + + if obj["type"].(string) == "test_run" { + // Then we need to delete the `elapsed` field from within the run + // as it'll cause flaky tests. + + run := obj["test_run"].(map[string]interface{}) + if run["progress"].(string) != "complete" { + delete(run, "elapsed") + } + } + + message, err := json.Marshal(obj) + if err != nil { + t.Errorf("failed to remarshal returned line: %s", line) + continue + } + + messages = append(messages, string(message)) + } + + expected := []string{ + `{"@level":"info","@message":"Found 1 file and 1 run block","@module":"terraform.ui","test_abstract":{"main.tftest.hcl":["test"]},"type":"test_abstract"}`, + `{"@level":"info","@message":"main.tftest.hcl... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","test_file":{"path":"main.tftest.hcl","progress":"starting"},"type":"test_file"}`, + `{"@level":"info","@message":" \"test\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"test","test_run":{"path":"main.tftest.hcl","progress":"starting","run":"test"},"type":"test_run"}`, + `{"@level":"info","@message":" \"test\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"test","test_run":{"path":"main.tftest.hcl","progress":"running","run":"test"},"type":"test_run"}`, + `{"@level":"info","@message":" \"test\"... in progress","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"test","test_run":{"path":"main.tftest.hcl","progress":"running","run":"test"},"type":"test_run"}`, + `{"@level":"info","@message":" \"test\"... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"test","test_run":{"path":"main.tftest.hcl","progress":"complete","run":"test","status":"pass"},"type":"test_run"}`, + `{"@level":"info","@message":"main.tftest.hcl... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","test_file":{"path":"main.tftest.hcl","progress":"teardown"},"type":"test_file"}`, + `{"@level":"info","@message":" \"test\"... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"test","test_run":{"path":"main.tftest.hcl","progress":"teardown","run":"test"},"type":"test_run"}`, + `{"@level":"info","@message":" \"test\"... tearing down","@module":"terraform.ui","@testfile":"main.tftest.hcl","@testrun":"test","test_run":{"path":"main.tftest.hcl","progress":"teardown","run":"test"},"type":"test_run"}`, + `{"@level":"info","@message":"main.tftest.hcl... pass","@module":"terraform.ui","@testfile":"main.tftest.hcl","test_file":{"path":"main.tftest.hcl","progress":"complete","status":"pass"},"type":"test_file"}`, + `{"@level":"info","@message":"Success! 1 passed, 0 failed.","@module":"terraform.ui","test_summary":{"errored":0,"failed":0,"passed":1,"skipped":0,"status":"pass"},"type":"test_summary"}`, + } + + if code != 0 { + t.Errorf("expected return code %d but got %d", 0, code) + } + + if diff := cmp.Diff(expected, messages); len(diff) > 0 { + t.Errorf("unexpected output\n\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", strings.Join(expected, "\n"), strings.Join(messages, "\n"), diff) + } +} + +func TestTest_InvalidOverrides(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "invalid-overrides")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "setup"... pass + +Warning: Invalid override target + + on main.tftest.hcl line 39, in run "setup": + 39: target = test_resource.absent_five + +The override target test_resource.absent_five does not exist within the +configuration under test. This could indicate a typo in the target address or +an unnecessary override. + + run "test"... pass + +Warning: Invalid override target + + on main.tftest.hcl line 45, in run "test": + 45: target = module.setup.test_resource.absent_six + +The override target module.setup.test_resource.absent_six does not exist +within the configuration under test. This could indicate a typo in the target +address or an unnecessary override. + +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Warning: Invalid override target + + on main.tftest.hcl line 4, in mock_provider "test": + 4: target = test_resource.absent_one + +The override target test_resource.absent_one does not exist within the +configuration under test. This could indicate a typo in the target address or +an unnecessary override. + +(and 3 more similar warnings elsewhere) + +Success! 2 passed, 0 failed. +` + + actual := output.All() + + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_InvalidConfig(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "invalid_config")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != 1 { + t.Errorf("expected status code ! but got %d", code) + } + + expectedOut := `main.tftest.hcl... in progress + run "test"... fail +main.tftest.hcl... tearing down +main.tftest.hcl... fail + +Failure! 0 passed, 1 failed. +` + expectedErr := ` +Error: Failed to load plugin schemas + +Error while loading schemas for plugin components: Failed to obtain provider +schema: Could not load the schema for provider +registry.terraform.io/hashicorp/test: failed to instantiate provider +"registry.terraform.io/hashicorp/test" to obtain schema: fork/exec +.terraform/providers/registry.terraform.io/hashicorp/test/1.0.0/%s/terraform-provider-test_1.0.0: +permission denied.. +` + expectedErr = fmt.Sprintf(expectedErr, runtime.GOOS+"_"+runtime.GOARCH) + out := output.Stdout() + err := output.Stderr() + + if diff := cmp.Diff(out, expectedOut); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, out, diff) + } + if diff := cmp.Diff(err, expectedErr); len(diff) > 0 { + t.Errorf("error didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, err, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_RunBlocksInProviders(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "provider_runs")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + test := &TestCommand{ + Meta: meta, + } + + code := test.Run([]string{"-no-color"}) + output := done(t) + + if code != 0 { + t.Errorf("expected status code 0 but got %d", code) + } + + expected := `main.tftest.hcl... in progress + run "setup"... pass + run "main"... pass +main.tftest.hcl... tearing down +main.tftest.hcl... pass + +Success! 2 passed, 0 failed. +` + actual := output.All() + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_RunBlocksInProviders_BadReferences(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "provider_runs_invalid")), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + test := &TestCommand{ + Meta: meta, + } + + code := test.Run([]string{"-no-color"}) + output := done(t) + + if code != 1 { + t.Errorf("expected status code 1 but got %d", code) + } + + expectedOut := `missing_run_block.tftest.hcl... in progress + run "main"... fail +missing_run_block.tftest.hcl... tearing down +missing_run_block.tftest.hcl... fail +unavailable_run_block.tftest.hcl... in progress + run "main"... fail +unavailable_run_block.tftest.hcl... tearing down +unavailable_run_block.tftest.hcl... fail +unused_provider.tftest.hcl... in progress + run "main"... pass +unused_provider.tftest.hcl... tearing down +unused_provider.tftest.hcl... pass + +Failure! 1 passed, 2 failed. +` + actualOut := output.Stdout() + if diff := cmp.Diff(actualOut, expectedOut); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedOut, actualOut, diff) + } + + expectedErr := ` +Error: Reference to unknown run block + + on missing_run_block.tftest.hcl line 2, in provider "test": + 2: resource_prefix = run.missing.resource_directory + +The run block "missing" does not exist within this test file. You can only +reference run blocks that are in the same test file and will execute before +the provider is required. + +Error: Reference to unavailable run block + + on unavailable_run_block.tftest.hcl line 2, in provider "test": + 2: resource_prefix = run.main.resource_directory + +The run block "main" has not executed yet. You can only reference run blocks +that are in the same test file and will execute before the provider is +required. +` + actualErr := output.Stderr() + if diff := cmp.Diff(actualErr, expectedErr); len(diff) > 0 { + t.Errorf("output didn't match expected:\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", expectedErr, actualErr, diff) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } +} + +func TestTest_JUnitOutput(t *testing.T) { + + tcs := map[string]struct { + path string + code int + wantFilename string + }{ + "can create XML for a single file with 1 pass, 1 fail": { + path: "junit-output/1pass-1fail", + wantFilename: "expected-output.xml", + code: 1, // Test failure + }, + "can create XML for multiple files with 1 pass each": { + path: "junit-output/multiple-files", + wantFilename: "expected-output.xml", + code: 0, + }, + "can display a test run's errors under the equivalent test case element": { + path: "junit-output/missing-provider", + wantFilename: "expected-output.xml", + code: 1, // Test error + }, + } + + for tn, tc := range tcs { + t.Run(tn, func(t *testing.T) { + // Setup test + td := t.TempDir() + testPath := path.Join("test", tc.path) + testCopyDir(t, testFixturePath(testPath), td) + defer testChdir(t, td)() + + provider := testing_command.NewProvider(nil) + view, done := testView(t) + + c := &TestCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + // Run command with -junit-xml=./output.xml flag + outputFile := fmt.Sprintf("%s/output.xml", td) + code := c.Run([]string{fmt.Sprintf("-junit-xml=%s", outputFile), "-no-color"}) + done(t) + + // Assertions + if code != tc.code { + t.Errorf("expected status code %d but got %d", tc.code, code) + } + + actualOut, err := os.ReadFile(outputFile) + if err != nil { + t.Fatalf("error opening XML file: %s", err) + } + expectedOutputFile := fmt.Sprintf("%s/%s", td, tc.wantFilename) + expectedOutput, err := os.ReadFile(expectedOutputFile) + if err != nil { + t.Fatalf("error opening XML file: %s", err) + } + + // actual output will include timestamps and test duration data, which isn't deterministic; redact it for comparison + timeRegexp := regexp.MustCompile(`time="[^"]+"`) + actualOut = timeRegexp.ReplaceAll(actualOut, []byte("time=\"TIME_REDACTED\"")) + timestampRegexp := regexp.MustCompile(`timestamp="[^"]+"`) + actualOut = timestampRegexp.ReplaceAll(actualOut, []byte("timestamp=\"TIMESTAMP_REDACTED\"")) + + if !bytes.Equal(actualOut, expectedOutput) { + t.Fatalf("wanted XML:\n%s\n got XML:\n%s\ndiff:%s\n", string(expectedOutput), string(actualOut), cmp.Diff(expectedOutput, actualOut)) + } + + if provider.ResourceCount() > 0 { + t.Errorf("should have deleted all resources on completion but left %v", provider.ResourceString()) + } + }) + } +} diff --git a/internal/command/testdata/apply-ephemeral-variable/main.tf b/internal/command/testdata/apply-ephemeral-variable/main.tf new file mode 100644 index 0000000000..7961a3b9a1 --- /dev/null +++ b/internal/command/testdata/apply-ephemeral-variable/main.tf @@ -0,0 +1,19 @@ +variable "foo" { + type = string + ephemeral = true +} + +variable "bar" { + type = string + default = null + ephemeral = true +} + +variable "unused" { + type = map(string) + default = null +} + +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/internal/command/testdata/apply-output-only/main.tf b/internal/command/testdata/apply-output-only/main.tf new file mode 100644 index 0000000000..af93614d23 --- /dev/null +++ b/internal/command/testdata/apply-output-only/main.tf @@ -0,0 +1,7 @@ +variable "shadow" { + type = string +} + +output "foo" { + value = var.shadow +} diff --git a/internal/command/testdata/apply-vars-auto/main.tf b/internal/command/testdata/apply-vars-auto/main.tf new file mode 100644 index 0000000000..1d6da85c3d --- /dev/null +++ b/internal/command/testdata/apply-vars-auto/main.tf @@ -0,0 +1,5 @@ +variable "foo" {} + +resource "test_instance" "foo" { + value = var.foo +} diff --git a/internal/command/testdata/apply-vars-auto/terraform-test.tfvars b/internal/command/testdata/apply-vars-auto/terraform-test.tfvars new file mode 100644 index 0000000000..5abc475eb9 --- /dev/null +++ b/internal/command/testdata/apply-vars-auto/terraform-test.tfvars @@ -0,0 +1 @@ +foo = "bar" diff --git a/internal/command/testdata/apply-vars-auto/terraform.tfvars b/internal/command/testdata/apply-vars-auto/terraform.tfvars new file mode 100644 index 0000000000..d622baea7a --- /dev/null +++ b/internal/command/testdata/apply-vars-auto/terraform.tfvars @@ -0,0 +1 @@ +foo = "auto" diff --git a/internal/command/testdata/apply/output.jsonlog b/internal/command/testdata/apply/output.jsonlog index 806a091fdd..a742e943da 100644 --- a/internal/command/testdata/apply/output.jsonlog +++ b/internal/command/testdata/apply/output.jsonlog @@ -1,7 +1,8 @@ {"@level":"info","@message":"Terraform 0.15.0-dev","@module":"terraform.ui","terraform":"0.15.0-dev","type":"version","ui":"0.1.0"} +{"@level":"warn","@message":"Warning: Deprecated flag: -state","@module":"terraform.ui","diagnostic":{"detail":"Use the \"path\" attribute within the \"local\" backend to specify a file for state storage","severity":"warning","summary":"Deprecated flag: -state"},"type":"diagnostic"} {"@level":"info","@message":"test_instance.foo: Plan to create","@module":"terraform.ui","change":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} -{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"import":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} {"@level":"info","@message":"test_instance.foo: Creating...","@module":"terraform.ui","hook":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"apply_start"} {"@level":"info","@message":"test_instance.foo: Creation complete after 0s","@module":"terraform.ui","hook":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create","elapsed_seconds":0},"type":"apply_complete"} -{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"apply"},"type":"change_summary"} +{"@level":"info","@message":"Apply complete! Resources: 1 added, 0 changed, 0 destroyed.","@module":"terraform.ui","changes":{"add":1,"import":0,"change":0,"remove":0,"operation":"apply"},"type":"change_summary"} {"@level":"info","@message":"Outputs: 0","@module":"terraform.ui","outputs":{},"type":"outputs"} diff --git a/internal/command/testdata/cloud-archives/manifest.json b/internal/command/testdata/cloud-archives/manifest.json new file mode 100644 index 0000000000..7173d6f46b --- /dev/null +++ b/internal/command/testdata/cloud-archives/manifest.json @@ -0,0 +1,51 @@ +{ + "plugin_version": "0.1.0", + "archives": { + "darwin_amd64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_darwin_amd64.zip", + "sha256sum": "bb611bb4c082fec9943d3c315fcd7cacd7dabce43fbf79b8e6b451bb4e54096d" + }, + "darwin_arm64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_darwin_arm64.zip", + "sha256sum": "295bf15c2af01d18ce7832d6d357667119e4b14eb8fd2454d506b23ed7825652" + }, + "freebsd_386": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_freebsd_386.zip", + "sha256sum": "b0744b9c8c0eb7ea61824728c302d0fd4fda4bb841fb6b3e701ef9eb10adbc39" + }, + "freebsd_amd64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_freebsd_amd64.zip", + "sha256sum": "8fc967d1402c5106fb0ca1b084b7edd2b11fd8d7c2225f5cd05584a56e0b2a16" + }, + "linux_386": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_linux_386.zip", + "sha256sum": "2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d" + }, + "linux_amd64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_linux_amd64.zip", + "sha256sum": "c877c8cebf76209c2c7d427d31e328212cd4716fdd8b6677939fd2a01e06a2d0" + }, + "linux_arm": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_linux_arm.zip", + "sha256sum": "97ff8fe4e2e853c9ea54605305732e5b16437045230a2df21f410e36edcfe7bd" + }, + "linux_arm64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_linux_arm64.zip", + "sha256sum": "d415a1c39b9ec79bd00efe72d0bf14e557833b6c1ce9898f223a7dd22abd0241" + }, + "solaris_amd64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_solaris_amd64.zip", + "sha256sum": "0f33a13eca612d1b3cda959d655a1535d69bcc1195dee37407c667c12c4900b5" + }, + "windows_386": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_windows_386.zip", + "sha256sum": "a6d572e5064e1b1cf8b0b4e64bc058dc630313c95e975b44e0540f231655d31c" + }, + "windows_amd64": { + "url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_windows_amd64.zip", + "sha256sum": "2aaceed12ebdf25d21f9953a09c328bd8892f5a5bd5382bd502f054478f56998" + } + }, + "sha256sums_url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS", + "sha256sums_signature_url": "https://releases.hashicorp.com/terraform-cloudplugin/0.1.0-prototype/terraform-cloudplugin_0.1.0-prototype_SHA256SUMS.sig" +} diff --git a/internal/command/testdata/cloud-config/main.tf b/internal/command/testdata/cloud-config/main.tf new file mode 100644 index 0000000000..5038febaf5 --- /dev/null +++ b/internal/command/testdata/cloud-config/main.tf @@ -0,0 +1,9 @@ +terraform { + cloud { + organization = "hashicorp" + + workspaces { + name = "test" + } + } +} diff --git a/internal/command/testdata/graph-cyclic/main.tf b/internal/command/testdata/graph-cyclic/main.tf new file mode 100644 index 0000000000..7866759c7d --- /dev/null +++ b/internal/command/testdata/graph-cyclic/main.tf @@ -0,0 +1,12 @@ +locals { + test1 = local.test2 + test2 = local.test1 +} + +resource "test_instance" "foo" { + ami = resource.test_instance.bar.ami +} + +resource "test_instance" "bar" { + ami = resource.test_instance.foo.ami +} diff --git a/internal/command/testdata/graph-interesting/child/graph-interesting-child.tf b/internal/command/testdata/graph-interesting/child/graph-interesting-child.tf new file mode 100644 index 0000000000..31f2b15f07 --- /dev/null +++ b/internal/command/testdata/graph-interesting/child/graph-interesting-child.tf @@ -0,0 +1,12 @@ + +variable "in" { + type = string +} + +resource "foo" "bleep" { + arg = var.in +} + +output "out" { + value = foo.bleep.arg +} diff --git a/internal/command/testdata/graph-interesting/graph-interesting.tf b/internal/command/testdata/graph-interesting/graph-interesting.tf new file mode 100644 index 0000000000..91b5db1ba4 --- /dev/null +++ b/internal/command/testdata/graph-interesting/graph-interesting.tf @@ -0,0 +1,20 @@ +resource "foo" "bar" { +} + +locals { + foo_bar_baz = foo.bar.baz +} + +resource "foo" "baz" { + arg = local.foo_bar_baz +} + +module "child" { + source = "./child" + + in = local.foo_bar_baz +} + +resource "foo" "boop" { + arg = module.child.out +} diff --git a/internal/command/testdata/init-cloud-simple/init-cloud-simple.tf b/internal/command/testdata/init-cloud-simple/init-cloud-simple.tf index 2493abe6b0..a021371bf8 100644 --- a/internal/command/testdata/init-cloud-simple/init-cloud-simple.tf +++ b/internal/command/testdata/init-cloud-simple/init-cloud-simple.tf @@ -1,6 +1,6 @@ -# This is a simple configuration with Terraform Cloud mode minimally +# This is a simple configuration with HCP Terraform mode minimally # activated, but it's suitable only for testing things that we can exercise -# without actually accessing Terraform Cloud, such as checking of invalid +# without actually accessing HCP Terraform, such as checking of invalid # command-line options to "terraform init". terraform { diff --git a/internal/command/testdata/init-get/output.jsonlog b/internal/command/testdata/init-get/output.jsonlog new file mode 100644 index 0000000000..88acf532fd --- /dev/null +++ b/internal/command/testdata/init-get/output.jsonlog @@ -0,0 +1,7 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"info","@message":"Initializing the backend...","@module":"terraform.ui","message_code": "initializing_backend_message","type":"init_output"} +{"@level":"info","@message":"Initializing modules...","@module":"terraform.ui","message_code": "initializing_modules_message","type":"init_output"} +{"@level":"info","@message":"- foo in foo","@module":"terraform.ui","type":"log"} +{"@level":"info","@message":"Initializing provider plugins...","@module":"terraform.ui","message_code": "initializing_provider_plugin_message","type":"init_output"} +{"@level":"info","@message":"Terraform has been successfully initialized!","@module":"terraform.ui","message_code": "output_init_success_message","type":"init_output"} +{"@level":"info","@message":"You may now begin working with Terraform. Try running \"terraform plan\" to see\nany changes that are required for your infrastructure. All Terraform commands\nshould now work.\n\nIf you ever set or change modules or backend configuration for Terraform,\nrerun this command to reinitialize your working directory. If you forget, other\ncommands will detect it and remind you to do so if necessary.","@module":"terraform.ui","message_code": "output_init_success_cli_message","type":"init_output"} diff --git a/internal/command/testdata/init-migrate-state-with-json/hello.tf b/internal/command/testdata/init-migrate-state-with-json/hello.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/command/testdata/init-migrate-state-with-json/output.jsonlog b/internal/command/testdata/init-migrate-state-with-json/output.jsonlog new file mode 100644 index 0000000000..1f52cb38de --- /dev/null +++ b/internal/command/testdata/init-migrate-state-with-json/output.jsonlog @@ -0,0 +1,2 @@ +{"@level":"info","@message":"Terraform 1.9.0-dev","@module":"terraform.ui","terraform":"1.9.0-dev","type":"version","ui":"1.2"} +{"@level":"error","@message":"Error: The -migrate-state and -json options are mutually-exclusive","@module":"terraform.ui","diagnostic":{"severity":"error","summary":"The -migrate-state and -json options are mutually-exclusive","detail":"Terraform cannot ask for interactive approval when -json is set. To use the -migrate-state option, disable the -json option."},"type":"diagnostic"} diff --git a/internal/command/testdata/init-syntax-invalid-backend-attribute-invalid/main.tf b/internal/command/testdata/init-syntax-invalid-backend-attribute-invalid/main.tf new file mode 100644 index 0000000000..8e50cf0b0e --- /dev/null +++ b/internal/command/testdata/init-syntax-invalid-backend-attribute-invalid/main.tf @@ -0,0 +1,10 @@ + +terraform { + backend "local" { + path = $invalid + } +} + +variable "input" { + type = string +} diff --git a/internal/command/testdata/init-syntax-invalid-backend-invalid/main.tf b/internal/command/testdata/init-syntax-invalid-backend-invalid/main.tf new file mode 100644 index 0000000000..4fb3f33692 --- /dev/null +++ b/internal/command/testdata/init-syntax-invalid-backend-invalid/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "nonexistent" {} +} + +bad_block { +} + diff --git a/internal/command/testdata/init-syntax-invalid-no-backend/main.tf b/internal/command/testdata/init-syntax-invalid-no-backend/main.tf new file mode 100644 index 0000000000..5f1451d26c --- /dev/null +++ b/internal/command/testdata/init-syntax-invalid-no-backend/main.tf @@ -0,0 +1,3 @@ +bad_block { +} + diff --git a/internal/command/testdata/init-syntax-invalid-with-backend/main.tf b/internal/command/testdata/init-syntax-invalid-with-backend/main.tf new file mode 100644 index 0000000000..2ea4406cc1 --- /dev/null +++ b/internal/command/testdata/init-syntax-invalid-with-backend/main.tf @@ -0,0 +1,7 @@ +terraform { + backend "local" {} +} + +bad_block { +} + diff --git a/internal/command/testdata/init-with-duplicates/primary.tf b/internal/command/testdata/init-with-duplicates/primary.tf new file mode 100644 index 0000000000..fcc6047614 --- /dev/null +++ b/internal/command/testdata/init-with-duplicates/primary.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } +} + +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/internal/command/testdata/init-with-duplicates/secondary.tf b/internal/command/testdata/init-with-duplicates/secondary.tf new file mode 100644 index 0000000000..e11209619b --- /dev/null +++ b/internal/command/testdata/init-with-duplicates/secondary.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + { // This typo is deliberate, we want to test that the parser can handle it. + } +} + +resource "test_instance" "bar" { + ami = "foo" +} diff --git a/internal/command/testdata/init-with-overrides-and-duplicates/primary.tf b/internal/command/testdata/init-with-overrides-and-duplicates/primary.tf new file mode 100644 index 0000000000..fcc6047614 --- /dev/null +++ b/internal/command/testdata/init-with-overrides-and-duplicates/primary.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } +} + +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/internal/command/testdata/init-with-overrides-and-duplicates/secondary.tf b/internal/command/testdata/init-with-overrides-and-duplicates/secondary.tf new file mode 100644 index 0000000000..2ed0d7feeb --- /dev/null +++ b/internal/command/testdata/init-with-overrides-and-duplicates/secondary.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + } + } +} + +resource "test_instance" "bar" { + ami = "foo" +} diff --git a/internal/command/testdata/init-with-overrides-and-duplicates/secondary_override.tf b/internal/command/testdata/init-with-overrides-and-duplicates/secondary_override.tf new file mode 100644 index 0000000000..3c269f3732 --- /dev/null +++ b/internal/command/testdata/init-with-overrides-and-duplicates/secondary_override.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.0" + { // This typo is deliberate, we want to test that the parser can handle it. + } +} + +resource "test_instance" "bar" { + ami = "override" +} diff --git a/internal/command/testdata/init-with-tests-external-providers/main.tf b/internal/command/testdata/init-with-tests-external-providers/main.tf new file mode 100644 index 0000000000..0bc133ee12 --- /dev/null +++ b/internal/command/testdata/init-with-tests-external-providers/main.tf @@ -0,0 +1,3 @@ +resource "testing_instance" "baz" { + ami = "baz" +} diff --git a/internal/command/testdata/init-with-tests-external-providers/main.tftest.hcl b/internal/command/testdata/init-with-tests-external-providers/main.tftest.hcl new file mode 100644 index 0000000000..3ba7e33205 --- /dev/null +++ b/internal/command/testdata/init-with-tests-external-providers/main.tftest.hcl @@ -0,0 +1,25 @@ + +// configure is not a "hashicorp" provider, so it won't be able to load +// this using the default behaviour. Terraform will need to look into the setup +// module to find the provider configuration. +provider "configure" {} + +// testing is a "hashicorp" provider, so it can load this using the defaults +// even though not required provider block providers a definition for it. +provider "testing" {} + +run "setup" { + module { + source = "./setup" + } + + providers = { + configure = configure + } +} + +run "test" { + providers = { + testing = testing + } +} diff --git a/internal/command/testdata/init-with-tests-external-providers/setup/main.tf b/internal/command/testdata/init-with-tests-external-providers/setup/main.tf new file mode 100644 index 0000000000..607cfe131a --- /dev/null +++ b/internal/command/testdata/init-with-tests-external-providers/setup/main.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + configure = { + source = "testing/configure" + } + } +} + +resource "configure_instance" "baz" { + ami = "baz" +} diff --git a/internal/command/testdata/init-with-tests-with-module/main.tf b/internal/command/testdata/init-with-tests-with-module/main.tf new file mode 100644 index 0000000000..2b976525ac --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-module/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/internal/command/testdata/init-with-tests-with-module/main.tftest.hcl b/internal/command/testdata/init-with-tests-with-module/main.tftest.hcl new file mode 100644 index 0000000000..8b0776e1a7 --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-module/main.tftest.hcl @@ -0,0 +1,12 @@ +run "setup" { + module { + source = "./setup" + } +} + +run "test" { + assert { + condition = test_instance.foo.ami == "bar" + error_message = "incorrect value" + } +} diff --git a/internal/command/testdata/init-with-tests-with-module/setup/main.tf b/internal/command/testdata/init-with-tests-with-module/setup/main.tf new file mode 100644 index 0000000000..f1017765c5 --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-module/setup/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "baz" { + ami = "baz" +} diff --git a/internal/command/testdata/init-with-tests-with-provider/main.tf b/internal/command/testdata/init-with-tests-with-provider/main.tf new file mode 100644 index 0000000000..fe11fd160e --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-provider/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.2" + } + } +} + +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/internal/command/testdata/init-with-tests-with-provider/main.tftest.hcl b/internal/command/testdata/init-with-tests-with-provider/main.tftest.hcl new file mode 100644 index 0000000000..8b0776e1a7 --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-provider/main.tftest.hcl @@ -0,0 +1,12 @@ +run "setup" { + module { + source = "./setup" + } +} + +run "test" { + assert { + condition = test_instance.foo.ami == "bar" + error_message = "incorrect value" + } +} diff --git a/internal/command/testdata/init-with-tests-with-provider/setup/main.tf b/internal/command/testdata/init-with-tests-with-provider/setup/main.tf new file mode 100644 index 0000000000..b0d3436f4d --- /dev/null +++ b/internal/command/testdata/init-with-tests-with-provider/setup/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + version = "1.0.1" + } + } +} + +resource "test_instance" "baz" { + ami = "baz" +} diff --git a/internal/command/testdata/init-with-tests/main.tf b/internal/command/testdata/init-with-tests/main.tf new file mode 100644 index 0000000000..2b976525ac --- /dev/null +++ b/internal/command/testdata/init-with-tests/main.tf @@ -0,0 +1,3 @@ +resource "test_instance" "foo" { + ami = "bar" +} diff --git a/internal/command/testdata/init-with-tests/main.tftest.hcl b/internal/command/testdata/init-with-tests/main.tftest.hcl new file mode 100644 index 0000000000..b68725876b --- /dev/null +++ b/internal/command/testdata/init-with-tests/main.tftest.hcl @@ -0,0 +1,6 @@ +run "test" { + assert { + condition = test_instance.foo.ami == "bar" + error_message = "incorrect value" + } +} diff --git a/internal/command/testdata/login-tfe-server/tfeserver.go b/internal/command/testdata/login-tfe-server/tfeserver.go index 111c0712cc..1d90fc1e0b 100644 --- a/internal/command/testdata/login-tfe-server/tfeserver.go +++ b/internal/command/testdata/login-tfe-server/tfeserver.go @@ -11,14 +11,14 @@ import ( const ( goodToken = "good-token" accountDetails = `{"data":{"id":"user-abc123","type":"users","attributes":{"username":"testuser","email":"testuser@example.com"}}}` - MOTD = `{"msg":"Welcome to Terraform Cloud!"}` + MOTD = `{"msg":"Welcome to HCP Terraform!"}` ) // Handler is an implementation of net/http.Handler that provides a stub // TFE API server implementation with the following endpoints: // -// /ping - API existence endpoint -// /account/details - current user endpoint +// /ping - API existence endpoint +// /account/details - current user endpoint var Handler http.Handler type handler struct{} diff --git a/internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json b/internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json new file mode 100644 index 0000000000..a4f71c7b2b --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/.terraform/modules/modules.json @@ -0,0 +1,29 @@ +{ + "Modules": [ + { + "Key": "", + "Source": "", + "Dir": "." + }, + { + "Key": "other", + "Source": "./mods/other", + "Dir": "mods/other" + }, + { + "Key": "test", + "Source": "./mods/test", + "Dir": "mods/test" + }, + { + "Key": "test.test2", + "Source": "./test2", + "Dir": "mods/test/test2" + }, + { + "Key": "test.test2.test3", + "Source": "./test3", + "Dir": "mods/test/test2/test3" + } + ] +} \ No newline at end of file diff --git a/internal/command/testdata/modules-nested-dependencies/main.tf b/internal/command/testdata/modules-nested-dependencies/main.tf new file mode 100644 index 0000000000..77bf763519 --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/main.tf @@ -0,0 +1,7 @@ +module "test" { + source = "./mods/test" +} + +module "other" { + source = "./mods/other" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/other/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/other/main.tf new file mode 100644 index 0000000000..f059e25f9e --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/other/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "test" { +} +output "myoutput" { + value = "bar" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/test/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/test/main.tf new file mode 100644 index 0000000000..9e17562a61 --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/test/main.tf @@ -0,0 +1,3 @@ +module "test2" { + source = "./test2" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf new file mode 100644 index 0000000000..ecbfa4be19 --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/main.tf @@ -0,0 +1,3 @@ +module "test3" { + source = "./test3" +} diff --git a/internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf new file mode 100644 index 0000000000..f059e25f9e --- /dev/null +++ b/internal/command/testdata/modules-nested-dependencies/mods/test/test2/test3/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "test" { +} +output "myoutput" { + value = "bar" +} diff --git a/internal/command/testdata/modules-nested-dependencies/terraform.tfstate b/internal/command/testdata/modules-nested-dependencies/terraform.tfstate new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/command/testdata/modules-uninstalled-entries/.terraform/modules/modules.json b/internal/command/testdata/modules-uninstalled-entries/.terraform/modules/modules.json new file mode 100644 index 0000000000..b812559fd0 --- /dev/null +++ b/internal/command/testdata/modules-uninstalled-entries/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"child","Source":"./child","Dir":"child"},{"Key":"count_child","Source":"./child","Dir":"child"}]} \ No newline at end of file diff --git a/internal/command/testdata/modules-uninstalled-entries/child/main.tf b/internal/command/testdata/modules-uninstalled-entries/child/main.tf new file mode 100644 index 0000000000..f059e25f9e --- /dev/null +++ b/internal/command/testdata/modules-uninstalled-entries/child/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "test" { +} +output "myoutput" { + value = "bar" +} diff --git a/internal/command/testdata/modules-uninstalled-entries/main.tf b/internal/command/testdata/modules-uninstalled-entries/main.tf new file mode 100644 index 0000000000..fb13eb506a --- /dev/null +++ b/internal/command/testdata/modules-uninstalled-entries/main.tf @@ -0,0 +1,15 @@ +locals { + foo = 3 +} + +module "child" { + source = "./child" +} +module "count_child" { + count = 1 + source = "./child" +} + +module "registry_mod" { + source = "foo/bar/barfoo" +} diff --git a/internal/command/testdata/modules-uninstalled-entries/terraform.tfstate b/internal/command/testdata/modules-uninstalled-entries/terraform.tfstate new file mode 100644 index 0000000000..36ff20d906 --- /dev/null +++ b/internal/command/testdata/modules-uninstalled-entries/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 4, + "terraform_version": "0.13.0", + "serial": 7, + "lineage": "9cb740e3-d64d-e53e-a8e4-99b9bcacf24b", + "outputs": {}, + "resources": [ + { + "module": "module.child", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": {} + } + ] + } + ] + } + \ No newline at end of file diff --git a/internal/command/testdata/modules-unreferenced-entries/.terraform/modules/modules.json b/internal/command/testdata/modules-unreferenced-entries/.terraform/modules/modules.json new file mode 100644 index 0000000000..7ee59d1a66 --- /dev/null +++ b/internal/command/testdata/modules-unreferenced-entries/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"child","Source":"./child","Dir":"child"},{"Key":"count_child","Source":"./child","Dir":"child"},{"Key":"old_count_child","Source":"./child","Dir":"child"}]} diff --git a/internal/command/testdata/modules-unreferenced-entries/child/main.tf b/internal/command/testdata/modules-unreferenced-entries/child/main.tf new file mode 100644 index 0000000000..f059e25f9e --- /dev/null +++ b/internal/command/testdata/modules-unreferenced-entries/child/main.tf @@ -0,0 +1,5 @@ +resource "test_instance" "test" { +} +output "myoutput" { + value = "bar" +} diff --git a/internal/command/testdata/modules-unreferenced-entries/main.tf b/internal/command/testdata/modules-unreferenced-entries/main.tf new file mode 100644 index 0000000000..4802b5639c --- /dev/null +++ b/internal/command/testdata/modules-unreferenced-entries/main.tf @@ -0,0 +1,11 @@ +locals { + foo = 3 +} + +module "child" { + source = "./child" +} +module "count_child" { + count = 1 + source = "./child" +} \ No newline at end of file diff --git a/internal/command/testdata/modules-unreferenced-entries/terraform.tfstate b/internal/command/testdata/modules-unreferenced-entries/terraform.tfstate new file mode 100644 index 0000000000..36ff20d906 --- /dev/null +++ b/internal/command/testdata/modules-unreferenced-entries/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 4, + "terraform_version": "0.13.0", + "serial": 7, + "lineage": "9cb740e3-d64d-e53e-a8e4-99b9bcacf24b", + "outputs": {}, + "resources": [ + { + "module": "module.child", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": {} + } + ] + } + ] + } + \ No newline at end of file diff --git a/internal/command/testdata/plan-existing-state/main.tf b/internal/command/testdata/plan-existing-state/main.tf new file mode 100644 index 0000000000..7b30915731 --- /dev/null +++ b/internal/command/testdata/plan-existing-state/main.tf @@ -0,0 +1,13 @@ +resource "test_instance" "foo" { + ami = "bar" + + # This is here because at some point it caused a test failure + network_interface { + device_index = 0 + description = "Main network interface" + } +} + +data "test_data_source" "a" { + id = "zzzzz" +} diff --git a/internal/command/testdata/plan-existing-state/terraform.tfstate b/internal/command/testdata/plan-existing-state/terraform.tfstate new file mode 100644 index 0000000000..e81bd9b869 --- /dev/null +++ b/internal/command/testdata/plan-existing-state/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 4, + "terraform_version": "1.6.0", + "serial": 1, + "lineage": "d496625c-bde2-aebc-f5f4-ebbf54eabed2", + "outputs": {}, + "resources": [ + { + "module": "module.child", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": {} + } + ] + } + ], + "check_results": null +} diff --git a/internal/command/testdata/plan-fail-condition/main.tf b/internal/command/testdata/plan-fail-condition/main.tf new file mode 100644 index 0000000000..5cee0ffb5b --- /dev/null +++ b/internal/command/testdata/plan-fail-condition/main.tf @@ -0,0 +1,15 @@ +locals { + ami = "bar" +} + +resource "test_instance" "foo" { + ami = local.ami + + lifecycle { + precondition { + // failing condition + condition = local.ami != "bar" + error_message = "ami is bar" + } + } +} diff --git a/internal/command/testdata/plan-import-config-gen/generated.tf.expected b/internal/command/testdata/plan-import-config-gen/generated.tf.expected new file mode 100644 index 0000000000..c864b2a600 --- /dev/null +++ b/internal/command/testdata/plan-import-config-gen/generated.tf.expected @@ -0,0 +1,7 @@ +# __generated__ by Terraform +# Please review these resources and move them into your main configuration files. + +# __generated__ by Terraform from "bar" +resource "test_instance" "foo" { + ami = null +} diff --git a/internal/command/testdata/plan-import-config-gen/main.tf b/internal/command/testdata/plan-import-config-gen/main.tf new file mode 100644 index 0000000000..ea89e71b0f --- /dev/null +++ b/internal/command/testdata/plan-import-config-gen/main.tf @@ -0,0 +1,4 @@ +import { + id = "bar" + to = test_instance.foo +} diff --git a/internal/command/testdata/plan/output.jsonlog b/internal/command/testdata/plan/output.jsonlog index d823fbf29c..7f42c6eca5 100644 --- a/internal/command/testdata/plan/output.jsonlog +++ b/internal/command/testdata/plan/output.jsonlog @@ -2,4 +2,4 @@ {"@level":"info","@message":"data.test_data_source.a: Refreshing...","@module":"terraform.ui","hook":{"resource":{"addr":"data.test_data_source.a","module":"","resource":"data.test_data_source.a","implied_provider":"test","resource_type":"test_data_source","resource_name":"a","resource_key":null},"action":"read"},"type":"apply_start"} {"@level":"info","@message":"data.test_data_source.a: Refresh complete after 0s [id=zzzzz]","@module":"terraform.ui","hook":{"resource":{"addr":"data.test_data_source.a","module":"","resource":"data.test_data_source.a","implied_provider":"test","resource_type":"test_data_source","resource_name":"a","resource_key":null},"action":"read","id_key":"id","id_value":"zzzzz","elapsed_seconds":0},"type":"apply_complete"} {"@level":"info","@message":"test_instance.foo: Plan to create","@module":"terraform.ui","change":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} -{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"import":0,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"} diff --git a/internal/command/testdata/providers/tests/main.tf b/internal/command/testdata/providers/tests/main.tf new file mode 100644 index 0000000000..f2388a802f --- /dev/null +++ b/internal/command/testdata/providers/tests/main.tf @@ -0,0 +1,3 @@ +resource "bar_instance" "test" { + +} diff --git a/internal/command/testdata/providers/tests/main.tftest.hcl b/internal/command/testdata/providers/tests/main.tftest.hcl new file mode 100644 index 0000000000..17b46b3340 --- /dev/null +++ b/internal/command/testdata/providers/tests/main.tftest.hcl @@ -0,0 +1,5 @@ +// This won't actually show up in the providers list, as nothing is actually +// using it. +provider "foo" { + +} diff --git a/internal/command/testdata/push-backend-new/main.tf b/internal/command/testdata/push-backend-new/main.tf deleted file mode 100644 index 68a49b44a5..0000000000 --- a/internal/command/testdata/push-backend-new/main.tf +++ /dev/null @@ -1,5 +0,0 @@ -terraform { - backend "inmem" {} -} - -atlas { name = "hello" } diff --git a/internal/command/testdata/show-json-sensitive/output.json b/internal/command/testdata/show-json-sensitive/output.json index 156b12f3e3..a4c2257e40 100644 --- a/internal/command/testdata/show-json-sensitive/output.json +++ b/internal/command/testdata/show-json-sensitive/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "bar" @@ -28,7 +30,8 @@ "password": "secret" }, "sensitive_values": { - "ami": true + "ami": true, + "password": true } }, { @@ -44,7 +47,8 @@ "password": "secret" }, "sensitive_values": { - "ami": true + "ami": true, + "password": true } }, { @@ -60,7 +64,8 @@ "password": "secret" }, "sensitive_values": { - "ami": true + "ami": true, + "password": true } } ] diff --git a/internal/command/testdata/show-json/basic-create/output.json b/internal/command/testdata/show-json/basic-create/output.json index ed348c3ece..b17f419454 100644 --- a/internal/command/testdata/show-json/basic-create/output.json +++ b/internal/command/testdata/show-json/basic-create/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "bar" diff --git a/internal/command/testdata/show-json/basic-delete/output.json b/internal/command/testdata/show-json/basic-delete/output.json index e24ff07774..9ae556bf93 100644 --- a/internal/command/testdata/show-json/basic-delete/output.json +++ b/internal/command/testdata/show-json/basic-delete/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "bar" diff --git a/internal/command/testdata/show-json/basic-update/output.json b/internal/command/testdata/show-json/basic-update/output.json index 05927e47fb..4e08f72fea 100644 --- a/internal/command/testdata/show-json/basic-update/output.json +++ b/internal/command/testdata/show-json/basic-update/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": false, + "complete": true, "variables": { "test_var": { "value": "bar" diff --git a/internal/command/testdata/show-json/conditions/for-refresh.tfstate b/internal/command/testdata/show-json/conditions/for-refresh.tfstate index 1b0b10196f..ec436cef0b 100644 --- a/internal/command/testdata/show-json/conditions/for-refresh.tfstate +++ b/internal/command/testdata/show-json/conditions/for-refresh.tfstate @@ -1,39 +1,55 @@ { - "version": 4, - "terraform_version": "1.2.0-dev", - "serial": 1, - "lineage": "no", - "outputs": {}, - "resources": [ + "version": 4, + "terraform_version": "1.2.0-dev", + "serial": 1, + "lineage": "no", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "test_instance", + "name": "foo", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ { - "mode": "managed", - "type": "test_instance", - "name": "foo", - "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "ami": "ami-test", - "id": "placeholder" - } - } - ] - }, - { - "mode": "managed", - "type": "test_instance", - "name": "bar", - "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", - "instances": [ - { - "schema_version": 0, - "attributes": { - "ami": "ami-test", - "id": "placeheld" - } - } + "schema_version": 0, + "attributes": { + "ami": "ami-test", + "id": "placeholder" + }, + "sensitive_attributes": [ + [ + { + "type": "get_attr", + "value": "password" + } ] + ] } - ] + ] + }, + { + "mode": "managed", + "type": "test_instance", + "name": "bar", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "ami": "ami-test", + "id": "placeheld" + }, + "sensitive_attributes": [ + [ + { + "type": "get_attr", + "value": "password" + } + ] + ] + } + ] + } + ] } diff --git a/internal/command/testdata/show-json/conditions/output-refresh-only.json b/internal/command/testdata/show-json/conditions/output-refresh-only.json index 291bbd773e..ab1c66f362 100644 --- a/internal/command/testdata/show-json/conditions/output-refresh-only.json +++ b/internal/command/testdata/show-json/conditions/output-refresh-only.json @@ -1,6 +1,8 @@ { - "format_version": "1.1", - "terraform_version": "1.2.0-dev", + "format_version": "1.2", + "terraform_version": "1.8.0-dev", + "applyable": true, + "complete": true, "variables": { "ami": { "value": "bad-ami" @@ -33,13 +35,13 @@ }, "prior_state": { "format_version": "1.0", - "terraform_version": "1.1.0", + "terraform_version": "1.8.0", "values": { "outputs": { "foo_id": { "sensitive": false, - "type": "string", - "value": "placeholder" + "value": "placeholder", + "type": "string" } }, "root_module": { @@ -56,7 +58,9 @@ "id": "placeheld", "password": null }, - "sensitive_values": {} + "sensitive_values": { + "password": true + } }, { "address": "test_instance.foo", @@ -70,7 +74,9 @@ "id": "placeholder", "password": null }, - "sensitive_values": {} + "sensitive_values": { + "password": true + } } ] } @@ -142,26 +148,70 @@ ] } ], - "condition_results": [ + "checks": [ { - "address": "output.foo_id", - "condition_type": "OutputPrecondition", - "result": true, - "unknown": false + "address": { + "kind": "output_value", + "name": "foo_id", + "to_display": "output.foo_id" + }, + "status": "pass", + "instances": [ + { + "address": { + "to_display": "output.foo_id" + }, + "status": "pass" + } + ] }, { - "address": "test_instance.bar", - "condition_type": "ResourcePostcondition", - "result": false, - "unknown": false, - "error_message": "Resource ID is unacceptably short (9 characters)." + "address": { + "kind": "resource", + "mode": "managed", + "name": "bar", + "to_display": "test_instance.bar", + "type": "test_instance" + }, + "status": "fail", + "instances": [ + { + "address": { + "to_display": "test_instance.bar" + }, + "status": "fail", + "problems": [ + { + "message": "Resource ID is unacceptably short (9 characters)." + } + ] + } + ] }, { - "address": "test_instance.foo", - "condition_type": "ResourcePrecondition", - "result": false, - "unknown": false, - "error_message": "Invalid AMI ID: must start with \"ami-\"." + "address": { + "kind": "resource", + "mode": "managed", + "name": "foo", + "to_display": "test_instance.foo", + "type": "test_instance" + }, + "status": "fail", + "instances": [ + { + "address": { + "to_display": "test_instance.foo" + }, + "status": "fail", + "problems": [ + { + "message": "Invalid AMI ID: must start with \"ami-\"." + } + ] + } + ] } - ] + ], + "timestamp": "2024-01-24T18:33:05Z", + "errored": false } diff --git a/internal/command/testdata/show-json/conditions/output.json b/internal/command/testdata/show-json/conditions/output.json index 1d13887b26..148f3f2b59 100644 --- a/internal/command/testdata/show-json/conditions/output.json +++ b/internal/command/testdata/show-json/conditions/output.json @@ -1,6 +1,8 @@ { "format_version": "1.1", "terraform_version": "1.2.0-dev", + "applyable": true, + "complete": true, "variables": { "ami": { "value": "ami-test" diff --git a/internal/command/testdata/show-json/drift/output.json b/internal/command/testdata/show-json/drift/output.json index 08029bf37e..946b90e760 100644 --- a/internal/command/testdata/show-json/drift/output.json +++ b/internal/command/testdata/show-json/drift/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ diff --git a/internal/command/testdata/show-json/module-depends-on/output.json b/internal/command/testdata/show-json/module-depends-on/output.json index c76c762659..59f4a1bce9 100644 --- a/internal/command/testdata/show-json/module-depends-on/output.json +++ b/internal/command/testdata/show-json/module-depends-on/output.json @@ -1,6 +1,8 @@ { "format_version": "1.0", "terraform_version": "0.13.1-dev", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ diff --git a/internal/command/testdata/show-json/modules/output.json b/internal/command/testdata/show-json/modules/output.json index ceb9c1cf01..96a5f490a6 100644 --- a/internal/command/testdata/show-json/modules/output.json +++ b/internal/command/testdata/show-json/modules/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "planned_values": { "outputs": { "test": { diff --git a/internal/command/testdata/show-json/moved-drift/output.json b/internal/command/testdata/show-json/moved-drift/output.json index db3ac21c25..c50d6bc1d7 100644 --- a/internal/command/testdata/show-json/moved-drift/output.json +++ b/internal/command/testdata/show-json/moved-drift/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ diff --git a/internal/command/testdata/show-json/moved/output.json b/internal/command/testdata/show-json/moved/output.json index d8b9f2a245..60ddb7b90c 100644 --- a/internal/command/testdata/show-json/moved/output.json +++ b/internal/command/testdata/show-json/moved/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ diff --git a/internal/command/testdata/show-json/multi-resource-update/output.json b/internal/command/testdata/show-json/multi-resource-update/output.json index 31581378ca..4612cd2ee9 100644 --- a/internal/command/testdata/show-json/multi-resource-update/output.json +++ b/internal/command/testdata/show-json/multi-resource-update/output.json @@ -1,6 +1,8 @@ { "format_version": "1.0", "terraform_version": "0.13.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "bar" diff --git a/internal/command/testdata/show-json/nested-modules/output.json b/internal/command/testdata/show-json/nested-modules/output.json index cf1ab978c9..f96a24484d 100644 --- a/internal/command/testdata/show-json/nested-modules/output.json +++ b/internal/command/testdata/show-json/nested-modules/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "child_modules": [ diff --git a/internal/command/testdata/show-json/plan-error/main.tf b/internal/command/testdata/show-json/plan-error/main.tf new file mode 100644 index 0000000000..c26c1a0aa4 --- /dev/null +++ b/internal/command/testdata/show-json/plan-error/main.tf @@ -0,0 +1,15 @@ +locals { + ami = "bar" +} + +resource "test_instance" "test" { + ami = local.ami + + lifecycle { + precondition { + // failing condition + condition = local.ami != "bar" + error_message = "ami is bar" + } + } +} \ No newline at end of file diff --git a/internal/command/testdata/show-json/plan-error/output.json b/internal/command/testdata/show-json/plan-error/output.json new file mode 100644 index 0000000000..dc0f0c33c4 --- /dev/null +++ b/internal/command/testdata/show-json/plan-error/output.json @@ -0,0 +1,37 @@ +{ + "format_version": "1.2", + "applyable": false, + "complete": false, + "planned_values": { + "root_module": {} + }, + "prior_state": {}, + "configuration": { + "provider_config": { + "test": { + "full_name": "registry.terraform.io/hashicorp/test", + "name": "test" + } + }, + "root_module": { + "resources": [ + { + "address": "test_instance.test", + "expressions": { + "ami": { + "references": [ + "local.ami" + ] + } + }, + "mode": "managed", + "name": "test", + "provider_config_key": "test", + "schema_version": 0, + "type": "test_instance" + } + ] + } + }, + "errored": true +} \ No newline at end of file diff --git a/internal/command/testdata/show-json/provider-aliasing-conflict/output.json b/internal/command/testdata/show-json/provider-aliasing-conflict/output.json index 6b7ee48c8d..b516a4a564 100644 --- a/internal/command/testdata/show-json/provider-aliasing-conflict/output.json +++ b/internal/command/testdata/show-json/provider-aliasing-conflict/output.json @@ -1,6 +1,8 @@ { "format_version": "1.0", "terraform_version": "1.1.0-dev", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ @@ -39,6 +41,27 @@ } }, "resource_changes": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider_name": "registry.terraform.io/hashicorp/test", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "ami": "foo" + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, { "address": "module.child.test_instance.test", "module_address": "module.child", @@ -60,27 +83,6 @@ "before_sensitive": false, "after_sensitive": {} } - }, - { - "address": "test_instance.test", - "mode": "managed", - "type": "test_instance", - "name": "test", - "provider_name": "registry.terraform.io/hashicorp/test", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "ami": "foo" - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } } ], "configuration": { diff --git a/internal/command/testdata/show-json/provider-aliasing-default/output.json b/internal/command/testdata/show-json/provider-aliasing-default/output.json index d6b1706437..f2639da510 100644 --- a/internal/command/testdata/show-json/provider-aliasing-default/output.json +++ b/internal/command/testdata/show-json/provider-aliasing-default/output.json @@ -1,6 +1,8 @@ { "format_version": "1.0", "terraform_version": "1.1.0-dev", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ @@ -75,6 +77,49 @@ } }, "resource_changes": [ + { + "address": "test_instance.test", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider_name": "registry.terraform.io/hashicorp/test", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "ami": "foo" + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "module.child.test_instance.test", + "module_address": "module.child", + "mode": "managed", + "type": "test_instance", + "name": "test", + "provider_name": "registry.terraform.io/hashicorp/test", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "ami": "bar" + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, { "address": "module.child.module.no_requirements.test_instance.test", "module_address": "module.child.module.no_requirements", @@ -118,49 +163,6 @@ "before_sensitive": false, "after_sensitive": {} } - }, - { - "address": "module.child.test_instance.test", - "module_address": "module.child", - "mode": "managed", - "type": "test_instance", - "name": "test", - "provider_name": "registry.terraform.io/hashicorp/test", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "ami": "bar" - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } - }, - { - "address": "test_instance.test", - "mode": "managed", - "type": "test_instance", - "name": "test", - "provider_name": "registry.terraform.io/hashicorp/test", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "ami": "foo" - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } } ], "configuration": { diff --git a/internal/command/testdata/show-json/provider-aliasing/output.json b/internal/command/testdata/show-json/provider-aliasing/output.json index 187141c9cf..9f5675036e 100755 --- a/internal/command/testdata/show-json/provider-aliasing/output.json +++ b/internal/command/testdata/show-json/provider-aliasing/output.json @@ -1,6 +1,8 @@ { "format_version": "1.0", "terraform_version": "1.1.0-dev", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ @@ -155,11 +157,10 @@ }, "resource_changes": [ { - "address": "module.child.module.grandchild.test_instance.test_alternate", - "module_address": "module.child.module.grandchild", + "address": "test_instance.test", "mode": "managed", "type": "test_instance", - "name": "test_alternate", + "name": "test", "provider_name": "registry.terraform.io/hashicorp/test", "change": { "actions": [ @@ -167,7 +168,7 @@ ], "before": null, "after": { - "ami": "secondary" + "ami": "foo" }, "after_unknown": { "id": true @@ -177,11 +178,10 @@ } }, { - "address": "module.child.module.grandchild.test_instance.test_main", - "module_address": "module.child.module.grandchild", + "address": "test_instance.test_backup", "mode": "managed", "type": "test_instance", - "name": "test_main", + "name": "test_backup", "provider_name": "registry.terraform.io/hashicorp/test", "change": { "actions": [ @@ -189,7 +189,7 @@ ], "before": null, "after": { - "ami": "main" + "ami": "foo-backup" }, "after_unknown": { "id": true @@ -242,50 +242,6 @@ "after_sensitive": {} } }, - { - "address": "module.sibling.module.grandchild.test_instance.test_alternate", - "module_address": "module.sibling.module.grandchild", - "mode": "managed", - "type": "test_instance", - "name": "test_alternate", - "provider_name": "registry.terraform.io/hashicorp/test", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "ami": "secondary" - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } - }, - { - "address": "module.sibling.module.grandchild.test_instance.test_main", - "module_address": "module.sibling.module.grandchild", - "mode": "managed", - "type": "test_instance", - "name": "test_main", - "provider_name": "registry.terraform.io/hashicorp/test", - "change": { - "actions": [ - "create" - ], - "before": null, - "after": { - "ami": "main" - }, - "after_unknown": { - "id": true - }, - "before_sensitive": false, - "after_sensitive": {} - } - }, { "address": "module.sibling.test_instance.test_primary", "module_address": "module.sibling", @@ -331,10 +287,11 @@ } }, { - "address": "test_instance.test", + "address": "module.child.module.grandchild.test_instance.test_alternate", + "module_address": "module.child.module.grandchild", "mode": "managed", "type": "test_instance", - "name": "test", + "name": "test_alternate", "provider_name": "registry.terraform.io/hashicorp/test", "change": { "actions": [ @@ -342,7 +299,7 @@ ], "before": null, "after": { - "ami": "foo" + "ami": "secondary" }, "after_unknown": { "id": true @@ -352,10 +309,11 @@ } }, { - "address": "test_instance.test_backup", + "address": "module.child.module.grandchild.test_instance.test_main", + "module_address": "module.child.module.grandchild", "mode": "managed", "type": "test_instance", - "name": "test_backup", + "name": "test_main", "provider_name": "registry.terraform.io/hashicorp/test", "change": { "actions": [ @@ -363,7 +321,51 @@ ], "before": null, "after": { - "ami": "foo-backup" + "ami": "main" + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "module.sibling.module.grandchild.test_instance.test_alternate", + "module_address": "module.sibling.module.grandchild", + "mode": "managed", + "type": "test_instance", + "name": "test_alternate", + "provider_name": "registry.terraform.io/hashicorp/test", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "ami": "secondary" + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + }, + { + "address": "module.sibling.module.grandchild.test_instance.test_main", + "module_address": "module.sibling.module.grandchild", + "mode": "managed", + "type": "test_instance", + "name": "test_main", + "provider_name": "registry.terraform.io/hashicorp/test", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "ami": "main" }, "after_unknown": { "id": true diff --git a/internal/command/testdata/show-json/provider-version-no-config/output.json b/internal/command/testdata/show-json/provider-version-no-config/output.json index aae0caca44..d1002c7f0a 100644 --- a/internal/command/testdata/show-json/provider-version-no-config/output.json +++ b/internal/command/testdata/show-json/provider-version-no-config/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "bar" diff --git a/internal/command/testdata/show-json/provider-version/output.json b/internal/command/testdata/show-json/provider-version/output.json index 111ece4830..98b90f9c78 100644 --- a/internal/command/testdata/show-json/provider-version/output.json +++ b/internal/command/testdata/show-json/provider-version/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "bar" diff --git a/internal/command/testdata/show-json/requires-replace/output.json b/internal/command/testdata/show-json/requires-replace/output.json index 1eb37ea1cf..675536fedb 100644 --- a/internal/command/testdata/show-json/requires-replace/output.json +++ b/internal/command/testdata/show-json/requires-replace/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "planned_values": { "root_module": { "resources": [ diff --git a/internal/command/testdata/show-json/sensitive-values/output.json b/internal/command/testdata/show-json/sensitive-values/output.json index fcdc976913..2d89ef583a 100644 --- a/internal/command/testdata/show-json/sensitive-values/output.json +++ b/internal/command/testdata/show-json/sensitive-values/output.json @@ -1,5 +1,7 @@ { "format_version": "1.0", + "applyable": true, + "complete": true, "variables": { "test_var": { "value": "boop" diff --git a/internal/command/testdata/show-json/unknown-output/output.json b/internal/command/testdata/show-json/unknown-output/output.json index 8a52b8dc57..dddc049fc4 100644 --- a/internal/command/testdata/show-json/unknown-output/output.json +++ b/internal/command/testdata/show-json/unknown-output/output.json @@ -1,6 +1,8 @@ { "format_version": "1.1", "terraform_version": "1.3.0-dev", + "applyable": true, + "complete": true, "planned_values": { "outputs": { "bar": { diff --git a/internal/command/testdata/state-identities-backend-default/.terraform/terraform.tfstate b/internal/command/testdata/state-identities-backend-default/.terraform/terraform.tfstate new file mode 100644 index 0000000000..44ed4f6726 --- /dev/null +++ b/internal/command/testdata/state-identities-backend-default/.terraform/terraform.tfstate @@ -0,0 +1,23 @@ +{ + "version": 3, + "serial": 0, + "lineage": "666f9301-7e65-4b19-ae23-71184bb19b03", + "backend": { + "type": "local", + "config": { + "path": null, + "workspace_dir": null + }, + "hash": 666019178 + }, + "modules": [ + { + "path": [ + "root" + ], + "outputs": {}, + "resources": {}, + "depends_on": [] + } + ] +} diff --git a/internal/command/testdata/state-identities-backend-default/main.tf b/internal/command/testdata/state-identities-backend-default/main.tf new file mode 100644 index 0000000000..7f62e0e197 --- /dev/null +++ b/internal/command/testdata/state-identities-backend-default/main.tf @@ -0,0 +1,4 @@ +terraform { + backend "local" { + } +} diff --git a/internal/command/testdata/state-identities-backend-default/terraform.tfstate b/internal/command/testdata/state-identities-backend-default/terraform.tfstate new file mode 100644 index 0000000000..bfebdbd135 --- /dev/null +++ b/internal/command/testdata/state-identities-backend-default/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "0.12.0", + "serial": 7, + "lineage": "configuredUnchanged", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "a", + "provider": "provider[\"registry.terraform.io/-/null\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "8521602373864259745", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project", + "role": "roles/viewer", + "member": "user:peter@example.com" + } + } + ] + } + ] +} diff --git a/internal/command/testdata/state-identities-nested-modules/terraform.tfstate b/internal/command/testdata/state-identities-nested-modules/terraform.tfstate new file mode 100644 index 0000000000..81d8837eb7 --- /dev/null +++ b/internal/command/testdata/state-identities-nested-modules/terraform.tfstate @@ -0,0 +1,133 @@ +{ + "version": 4, + "terraform_version": "0.15.0", + "serial": 8, + "lineage": "00bfda35-ad61-ec8d-c013-14b0320bc416", + "resources": [ + { + "mode": "managed", + "type": "test_instance", + "name": "root", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "root", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project-root", + "role": "roles/viewer-root" + } + } + ] + }, + { + "module": "module.nest", + "mode": "managed", + "type": "test_instance", + "name": "nest", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "ami": "nested", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project-nest", + "role": "roles/viewer-nest" + } + } + ] + }, + { + "module": "module.nest.module.subnest", + "mode": "managed", + "type": "test_instance", + "name": "subnest", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "subnested", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project-subnest", + "role": "roles/viewer-subnest" + } + } + ] + }, + { + "module": "module.nonexist.module.child", + "mode": "managed", + "type": "test_instance", + "name": "child", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "child", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project-child", + "role": "roles/viewer-child" + } + } + ] + }, + { + "module": "module.count[0]", + "mode": "managed", + "type": "test_instance", + "name": "count", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "zero", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project-count-0", + "role": "roles/viewer-count-0" + } + } + ] + }, + { + "module": "module.count[1]", + "mode": "managed", + "type": "test_instance", + "name": "count", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "one", + "triggers": null + }, + "identity_schema_version": 0, + "identity": { + "project": "my-project-count-1", + "role": "roles/viewer-count-1" + } + } + ] + } + ] +} diff --git a/internal/command/testdata/test-fails/test-fails.tf b/internal/command/testdata/test-fails/test-fails.tf deleted file mode 100644 index d618fb9d85..0000000000 --- a/internal/command/testdata/test-fails/test-fails.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "input" { - type = string -} - -output "foo" { - value = "foo value ${var.input}" -} diff --git a/internal/command/testdata/test-fails/tests/hello/hello.tf b/internal/command/testdata/test-fails/tests/hello/hello.tf deleted file mode 100644 index 6fbf8ee696..0000000000 --- a/internal/command/testdata/test-fails/tests/hello/hello.tf +++ /dev/null @@ -1,23 +0,0 @@ -terraform { - required_providers { - test = { - source = "terraform.io/builtin/test" - } - } -} - -module "main" { - source = "../.." - - input = "boop" -} - -resource "test_assertions" "foo" { - component = "foo" - - equal "output" { - description = "output \"foo\" value" - got = module.main.foo - want = "foo not boop" - } -} diff --git a/internal/command/testdata/test-passes/test-passes.tf b/internal/command/testdata/test-passes/test-passes.tf deleted file mode 100644 index d618fb9d85..0000000000 --- a/internal/command/testdata/test-passes/test-passes.tf +++ /dev/null @@ -1,7 +0,0 @@ -variable "input" { - type = string -} - -output "foo" { - value = "foo value ${var.input}" -} diff --git a/internal/command/testdata/test-passes/tests/hello/hello.tf b/internal/command/testdata/test-passes/tests/hello/hello.tf deleted file mode 100644 index 2129f43df1..0000000000 --- a/internal/command/testdata/test-passes/tests/hello/hello.tf +++ /dev/null @@ -1,23 +0,0 @@ -terraform { - required_providers { - test = { - source = "terraform.io/builtin/test" - } - } -} - -module "main" { - source = "../.." - - input = "boop" -} - -resource "test_assertions" "foo" { - component = "foo" - - equal "output" { - description = "output \"foo\" value" - got = module.main.foo - want = "foo value boop" - } -} diff --git a/internal/command/testdata/test/bad-references/main.tf b/internal/command/testdata/test/bad-references/main.tf new file mode 100644 index 0000000000..49b5720c2d --- /dev/null +++ b/internal/command/testdata/test/bad-references/main.tf @@ -0,0 +1,16 @@ + +variable "input_one" { + type = string +} + +variable "input_two" { + type = string +} + +resource "test_resource" "resource" { + value = "${var.input_one} - ${var.input_two}" +} + +output "response" { + value = test_resource.resource.value +} diff --git a/internal/command/testdata/test/bad-references/main.tftest.hcl b/internal/command/testdata/test/bad-references/main.tftest.hcl new file mode 100644 index 0000000000..d7b0e453c4 --- /dev/null +++ b/internal/command/testdata/test/bad-references/main.tftest.hcl @@ -0,0 +1,26 @@ + +variables { + default = "double" +} + +run "setup" { + variables { + input_one = var.default + input_two = var.default + } +} + +run "test" { + variables { + input_one = var.notreal + input_two = run.finalise.response + input_three = run.madeup.response + } +} + +run "finalise" { + variables { + input_one = var.default + input_two = var.default + } +} diff --git a/internal/command/testdata/test/bad-references/providers.tftest.hcl b/internal/command/testdata/test/bad-references/providers.tftest.hcl new file mode 100644 index 0000000000..ee784c12a4 --- /dev/null +++ b/internal/command/testdata/test/bad-references/providers.tftest.hcl @@ -0,0 +1,6 @@ + +provider "test" { + resource_prefix = var.default +} + +run "test" {} diff --git a/internal/command/testdata/test/complex_condition/main.tf b/internal/command/testdata/test/complex_condition/main.tf new file mode 100644 index 0000000000..2f61115647 --- /dev/null +++ b/internal/command/testdata/test/complex_condition/main.tf @@ -0,0 +1,58 @@ +resource "test_resource" "foo" { + value = "bar" +} + +output "foo" { + value = { + bar = "notbaz" + qux = "quux" + matches = "matches" + xuq = "xuq" + } +} + +variable "sample" { + type = list(object({ + bar = tuple([number]) + qux = string + })) + + default = [ { + bar = [1] + qux = "quux" + }, + { + bar = [2] + qux = "quux" + }] +} + +variable "sample_sensitive" { + sensitive = true + type = list(object({ + bar = tuple([number]) + qux = string + })) + + default = [ { + bar = [1] + qux = "quux" + }, + { + bar = [2] + qux = "quux_sensitive" + }] +} + +output "complex" { + value = { + root = var.sample + } +} + +output "complex_sensitive" { + sensitive = true + value = { + root = var.sample_sensitive + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/complex_condition/main.tftest.hcl b/internal/command/testdata/test/complex_condition/main.tftest.hcl new file mode 100644 index 0000000000..b279319431 --- /dev/null +++ b/internal/command/testdata/test/complex_condition/main.tftest.hcl @@ -0,0 +1,70 @@ + +variables { + + foo = { + bar = "baz", + qux = "qux", + matches = "matches", + xuq = "nope" + } + + bar = { + root = [{ + bar = [1] + qux = "qux" + }, + { + bar = [2] + qux = "quux" + }] + } +} + +run "validate_diff_types" { +// the compared values are of different types, but have the same +// visual representation in the terminal. + variables { + tr1 = { + "iops" = tonumber(null) + "size" = 60 +} + tr2 = { + iops = null + size = 60 +} + } + assert { + condition = var.tr1 == var.tr2 + error_message = "expected to fail" + } +} + +run "validate_output" { + assert { + condition = output.foo == var.foo + error_message = "expected to fail due to different values" + } +} + +run "validate_complex_output" { + assert { + // just a more complex value comparison + condition = output.complex == var.bar + error_message = "expected to fail" + } +} + +run "validate_complex_output_sensitive" { + // the rhs is sensitive + assert { + condition = output.complex == output.complex_sensitive + error_message = "expected to fail" + } +} + +run "validate_complex_output_pass" { + assert { + condition = output.complex != var.foo + error_message = "should pass" + } +} diff --git a/internal/command/testdata/test/custom_condition_checks/main.tf b/internal/command/testdata/test/custom_condition_checks/main.tf new file mode 100644 index 0000000000..d010847f85 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_checks/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} + +check "expected_to_fail" { + assert { + condition = test_resource.resource.value != var.input + error_message = "this really should fail" + } +} diff --git a/internal/command/testdata/test/custom_condition_checks/main.tftest.hcl b/internal/command/testdata/test/custom_condition_checks/main.tftest.hcl new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_checks/main.tftest.hcl @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/custom_condition_inputs/main.tf b/internal/command/testdata/test/custom_condition_inputs/main.tf new file mode 100644 index 0000000000..027c8e8fdf --- /dev/null +++ b/internal/command/testdata/test/custom_condition_inputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string + + validation { + condition = var.input == "something very specific" + error_message = "this should definitely fail" + } +} + +resource "test_resource" "resource" { + value = var.input +} diff --git a/internal/command/testdata/test/custom_condition_inputs/main.tftest.hcl b/internal/command/testdata/test/custom_condition_inputs/main.tftest.hcl new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_inputs/main.tftest.hcl @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/custom_condition_outputs/main.tf b/internal/command/testdata/test/custom_condition_outputs/main.tf new file mode 100644 index 0000000000..af05c486a0 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_outputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string +} + +output "output" { + value = var.input + + precondition { + condition = var.input == "something incredibly specific" + error_message = "this should fail" + } +} diff --git a/internal/command/testdata/test/custom_condition_outputs/main.tftest.hcl b/internal/command/testdata/test/custom_condition_outputs/main.tftest.hcl new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_outputs/main.tftest.hcl @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/custom_condition_resources/main.tf b/internal/command/testdata/test/custom_condition_resources/main.tf new file mode 100644 index 0000000000..0b12d8cda0 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_resources/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input + + lifecycle { + postcondition { + condition = self.value != var.input + error_message = "this really should fail" + } + } +} diff --git a/internal/command/testdata/test/custom_condition_resources/main.tftest.hcl b/internal/command/testdata/test/custom_condition_resources/main.tftest.hcl new file mode 100644 index 0000000000..d3ead1fe15 --- /dev/null +++ b/internal/command/testdata/test/custom_condition_resources/main.tftest.hcl @@ -0,0 +1,5 @@ +variables { + input = "some value" +} + +run "test" {} diff --git a/internal/command/testdata/test/dangling_data_block/main.tf b/internal/command/testdata/test/dangling_data_block/main.tf new file mode 100644 index 0000000000..d4abb421fc --- /dev/null +++ b/internal/command/testdata/test/dangling_data_block/main.tf @@ -0,0 +1,11 @@ +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} + +output "id" { + value = test_resource.resource.id +} diff --git a/internal/command/testdata/test/dangling_data_block/testing/verify/main.tf b/internal/command/testdata/test/dangling_data_block/testing/verify/main.tf new file mode 100644 index 0000000000..1afe6fc56a --- /dev/null +++ b/internal/command/testdata/test/dangling_data_block/testing/verify/main.tf @@ -0,0 +1,11 @@ +variable "id" { + type = string +} + +data "test_data_source" "resource" { + id = var.id +} + +output "value" { + value = data.test_data_source.resource.value +} diff --git a/internal/command/testdata/test/dangling_data_block/tests/main.tftest.hcl b/internal/command/testdata/test/dangling_data_block/tests/main.tftest.hcl new file mode 100644 index 0000000000..bcb80c8f19 --- /dev/null +++ b/internal/command/testdata/test/dangling_data_block/tests/main.tftest.hcl @@ -0,0 +1,15 @@ +run "test" { + variables { + input = "Hello, world!" + } +} + +run "verify" { + module { + source = "./testing/verify" + } + + variables { + id = run.test.id + } +} diff --git a/internal/command/testdata/test/default_optional_values/main.tf b/internal/command/testdata/test/default_optional_values/main.tf new file mode 100644 index 0000000000..bdf5d5cca9 --- /dev/null +++ b/internal/command/testdata/test/default_optional_values/main.tf @@ -0,0 +1,26 @@ + +variable "input" { + + type = object({ + required = string + optional = optional(string) + default = optional(string, "default") + }) + + default = { + required = "required" + } + +} + +resource "test_resource" "resource" { + value = var.input.default +} + +output "computed" { + value = test_resource.resource.value +} + +output "input" { + value = var.input +} diff --git a/internal/command/testdata/test/default_optional_values/main.tftest.hcl b/internal/command/testdata/test/default_optional_values/main.tftest.hcl new file mode 100644 index 0000000000..fbce68962b --- /dev/null +++ b/internal/command/testdata/test/default_optional_values/main.tftest.hcl @@ -0,0 +1,47 @@ + +run "stacked" { + variables { + input = { + required = "required" + optional = "optional" + default = "overridden" + } + } + + assert { + condition = output.computed == "overridden" + error_message = "did not override default value" + } +} + +run "defaults" { + assert { + condition = output.computed == "default" + error_message = "didn't set default value" + } +} + +run "default_matches_last_output" { + assert { + condition = var.input == run.defaults.input + error_message = "output of last should match input of this" + } +} + +run "custom_defined_apply_defaults" { + variables { + input = { + required = "required" + } + } + + assert { + condition = output.computed == "default" + error_message = "didn't set default value" + } + + assert { + condition = var.input == run.defaults.input + error_message = "output of last should match input of this" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/default_variables/main.tf b/internal/command/testdata/test/default_variables/main.tf new file mode 100644 index 0000000000..ce7d9e83d8 --- /dev/null +++ b/internal/command/testdata/test/default_variables/main.tf @@ -0,0 +1,5 @@ + +variable "input" { + type = string + default = "Hello, world!" +} diff --git a/internal/command/testdata/test/default_variables/main.tftest.hcl b/internal/command/testdata/test/default_variables/main.tftest.hcl new file mode 100644 index 0000000000..a6292d0923 --- /dev/null +++ b/internal/command/testdata/test/default_variables/main.tftest.hcl @@ -0,0 +1,7 @@ + +run "applies_defaults" { + assert { + condition = var.input == "Hello, world!" + error_message = "should have applied default value" + } +} diff --git a/internal/command/testdata/test/destroy_fail/main.tf b/internal/command/testdata/test/destroy_fail/main.tf new file mode 100644 index 0000000000..4ee97ff639 --- /dev/null +++ b/internal/command/testdata/test/destroy_fail/main.tf @@ -0,0 +1,10 @@ + +resource "test_resource" "resource" { + value = "Hello, world!" + destroy_fail = true +} + +resource "test_resource" "another" { + value = "Hello, world!" + destroy_fail = true +} \ No newline at end of file diff --git a/internal/command/testdata/test/destroy_fail/main.tftest.hcl b/internal/command/testdata/test/destroy_fail/main.tftest.hcl new file mode 100644 index 0000000000..b5443eb9c1 --- /dev/null +++ b/internal/command/testdata/test/destroy_fail/main.tftest.hcl @@ -0,0 +1,12 @@ + +run "setup" { + module { + source = "./setup" + } +} + +run "single" {} + +run "double" { + state_key = "double" +} diff --git a/internal/command/testdata/test/destroy_fail/setup/main.tf b/internal/command/testdata/test/destroy_fail/setup/main.tf new file mode 100644 index 0000000000..6936d477f5 --- /dev/null +++ b/internal/command/testdata/test/destroy_fail/setup/main.tf @@ -0,0 +1 @@ +resource "test_resource" "resource" {} \ No newline at end of file diff --git a/internal/command/testdata/test/empty_module_with_output/empty/main.tf b/internal/command/testdata/test/empty_module_with_output/empty/main.tf new file mode 100644 index 0000000000..e16d2b13c3 --- /dev/null +++ b/internal/command/testdata/test/empty_module_with_output/empty/main.tf @@ -0,0 +1,3 @@ +output "value" { + value = "Hello, World!" +} diff --git a/internal/command/testdata/test/empty_module_with_output/main.tf b/internal/command/testdata/test/empty_module_with_output/main.tf new file mode 100644 index 0000000000..0fb7b668e9 --- /dev/null +++ b/internal/command/testdata/test/empty_module_with_output/main.tf @@ -0,0 +1,3 @@ +module "empty" { + source = "./empty" +} diff --git a/internal/command/testdata/test/empty_module_with_output/main.tftest.hcl b/internal/command/testdata/test/empty_module_with_output/main.tftest.hcl new file mode 100644 index 0000000000..22100ef2f7 --- /dev/null +++ b/internal/command/testdata/test/empty_module_with_output/main.tftest.hcl @@ -0,0 +1,6 @@ +run "empty" { + assert { + condition = module.empty.value == "Hello, World!" + error_message = "wrong output value" + } +} diff --git a/internal/command/testdata/test/env-vars-in-module/main.tf b/internal/command/testdata/test/env-vars-in-module/main.tf new file mode 100644 index 0000000000..13d283df92 --- /dev/null +++ b/internal/command/testdata/test/env-vars-in-module/main.tf @@ -0,0 +1 @@ +resource "test_resource" "resource" {} diff --git a/internal/command/testdata/test/env-vars-in-module/main.tftest.hcl b/internal/command/testdata/test/env-vars-in-module/main.tftest.hcl new file mode 100644 index 0000000000..8b33650bb2 --- /dev/null +++ b/internal/command/testdata/test/env-vars-in-module/main.tftest.hcl @@ -0,0 +1,7 @@ +run "module" { + module { + source = "./mod" + } +} + +run "test" {} diff --git a/internal/command/testdata/test/env-vars-in-module/mod/main.tf b/internal/command/testdata/test/env-vars-in-module/mod/main.tf new file mode 100644 index 0000000000..4d5816c339 --- /dev/null +++ b/internal/command/testdata/test/env-vars-in-module/mod/main.tf @@ -0,0 +1,5 @@ +variable "input" {} + +output "value" { + value = var.input +} diff --git a/internal/command/testdata/test/env-vars/main.tf b/internal/command/testdata/test/env-vars/main.tf new file mode 100644 index 0000000000..97c6896727 --- /dev/null +++ b/internal/command/testdata/test/env-vars/main.tf @@ -0,0 +1,5 @@ +variable "input" {} + +resource "test_resource" "resource" { + value = var.input +} diff --git a/internal/command/testdata/test/env-vars/main.tftest.hcl b/internal/command/testdata/test/env-vars/main.tftest.hcl new file mode 100644 index 0000000000..eb3013a877 --- /dev/null +++ b/internal/command/testdata/test/env-vars/main.tftest.hcl @@ -0,0 +1 @@ +run "test" {} diff --git a/internal/command/testdata/test/ephemeral_input/main.tf b/internal/command/testdata/test/ephemeral_input/main.tf new file mode 100644 index 0000000000..a0c5391a0d --- /dev/null +++ b/internal/command/testdata/test/ephemeral_input/main.tf @@ -0,0 +1,7 @@ +variable "foo" { + ephemeral = true + type = string +} +output "value" { + value = "Hello, World!" +} diff --git a/internal/command/testdata/test/ephemeral_input/main.tftest.hcl b/internal/command/testdata/test/ephemeral_input/main.tftest.hcl new file mode 100644 index 0000000000..6e5411b315 --- /dev/null +++ b/internal/command/testdata/test/ephemeral_input/main.tftest.hcl @@ -0,0 +1,19 @@ +run "validate_ephemeral_input" { + variables { + foo = "bar" + } + assert { + condition = var.foo == "bar" + error_message = "Should be accessible" + } +} + +run "validate_ephemeral_input_is_ephemeral" { + variables { + foo = "bar" + } + assert { + condition = ephemeralasnull(var.foo) == null + error_message = "Should be ephemeral" + } +} diff --git a/internal/command/testdata/test/ephemeral_input_with_error/main.tf b/internal/command/testdata/test/ephemeral_input_with_error/main.tf new file mode 100644 index 0000000000..a0c5391a0d --- /dev/null +++ b/internal/command/testdata/test/ephemeral_input_with_error/main.tf @@ -0,0 +1,7 @@ +variable "foo" { + ephemeral = true + type = string +} +output "value" { + value = "Hello, World!" +} diff --git a/internal/command/testdata/test/ephemeral_input_with_error/main.tftest.hcl b/internal/command/testdata/test/ephemeral_input_with_error/main.tftest.hcl new file mode 100644 index 0000000000..55f075019e --- /dev/null +++ b/internal/command/testdata/test/ephemeral_input_with_error/main.tftest.hcl @@ -0,0 +1,19 @@ +run "validate_ephemeral_input" { + variables { + foo = "baz" + } + assert { + condition = var.foo == "bar" + error_message = "Expecting this to fail, real value is: ${var.foo}" + } +} + +run "validate_ephemeral_input_is_ephemeral" { + variables { + foo = "bar" + } + assert { + condition = ephemeralasnull(var.foo) == null + error_message = "Should be ephemeral" + } +} diff --git a/internal/command/testdata/test/ephemeral_resource/main.tf b/internal/command/testdata/test/ephemeral_resource/main.tf new file mode 100644 index 0000000000..d09098251b --- /dev/null +++ b/internal/command/testdata/test/ephemeral_resource/main.tf @@ -0,0 +1,2 @@ +ephemeral "test_ephemeral_resource" "data" { +} diff --git a/internal/command/testdata/test/ephemeral_resource/main.tftest.hcl b/internal/command/testdata/test/ephemeral_resource/main.tftest.hcl new file mode 100644 index 0000000000..7e38302731 --- /dev/null +++ b/internal/command/testdata/test/ephemeral_resource/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_ephemeral_resource" { + assert { + condition = ephemeral.test_ephemeral_resource.data.value == "bar" + error_message = "We expect this to fail since ephemeral resources should be closed when this is evaluated" + } +} diff --git a/internal/command/testdata/test/expect_failures_checks/main.tf b/internal/command/testdata/test/expect_failures_checks/main.tf new file mode 100644 index 0000000000..d010847f85 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_checks/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} + +check "expected_to_fail" { + assert { + condition = test_resource.resource.value != var.input + error_message = "this really should fail" + } +} diff --git a/internal/command/testdata/test/expect_failures_checks/main.tftest.hcl b/internal/command/testdata/test/expect_failures_checks/main.tftest.hcl new file mode 100644 index 0000000000..bcac23b653 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_checks/main.tftest.hcl @@ -0,0 +1,9 @@ +variables { + input = "some value" +} + +run "test" { + expect_failures = [ + check.expected_to_fail + ] +} diff --git a/internal/command/testdata/test/expect_failures_during_apply/main.tf b/internal/command/testdata/test/expect_failures_during_apply/main.tf new file mode 100644 index 0000000000..acc1ca24d4 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_during_apply/main.tf @@ -0,0 +1,13 @@ + +locals { + input = uuid() # using UUID to ensure that plan phase will return an unknown value +} + +output "output" { + value = local.input + + precondition { + condition = local.input != "" + error_message = "this should not fail during the apply phase" + } +} diff --git a/internal/command/testdata/test/expect_failures_during_apply/main.tftest.hcl b/internal/command/testdata/test/expect_failures_during_apply/main.tftest.hcl new file mode 100644 index 0000000000..a8039de9b4 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_during_apply/main.tftest.hcl @@ -0,0 +1,18 @@ +run "test" { + + command = apply + +// We are expecting the output to fail during apply, but it will not, so the test will fail. + expect_failures = [ + output.output + ] +} + +// this should still run +run "follow-up" { + command = apply + + variables { + input = "does not matter" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/expect_failures_inputs/main.tf b/internal/command/testdata/test/expect_failures_inputs/main.tf new file mode 100644 index 0000000000..027c8e8fdf --- /dev/null +++ b/internal/command/testdata/test/expect_failures_inputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string + + validation { + condition = var.input == "something very specific" + error_message = "this should definitely fail" + } +} + +resource "test_resource" "resource" { + value = var.input +} diff --git a/internal/command/testdata/test/expect_failures_inputs/main.tftest.hcl b/internal/command/testdata/test/expect_failures_inputs/main.tftest.hcl new file mode 100644 index 0000000000..ec603112b9 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_inputs/main.tftest.hcl @@ -0,0 +1,11 @@ +variables { + input = "some value" +} + +run "test" { + command = plan + + expect_failures = [ + var.input + ] +} diff --git a/internal/command/testdata/test/expect_failures_outputs/main.tf b/internal/command/testdata/test/expect_failures_outputs/main.tf new file mode 100644 index 0000000000..af05c486a0 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_outputs/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string +} + +output "output" { + value = var.input + + precondition { + condition = var.input == "something incredibly specific" + error_message = "this should fail" + } +} diff --git a/internal/command/testdata/test/expect_failures_outputs/main.tftest.hcl b/internal/command/testdata/test/expect_failures_outputs/main.tftest.hcl new file mode 100644 index 0000000000..49b2f16973 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_outputs/main.tftest.hcl @@ -0,0 +1,12 @@ +variables { + input = "some value" +} + +run "test" { + + command = plan + + expect_failures = [ + output.output + ] +} diff --git a/internal/command/testdata/test/expect_failures_resources/main.tf b/internal/command/testdata/test/expect_failures_resources/main.tf new file mode 100644 index 0000000000..0b12d8cda0 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_resources/main.tf @@ -0,0 +1,15 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input + + lifecycle { + postcondition { + condition = self.value != var.input + error_message = "this really should fail" + } + } +} diff --git a/internal/command/testdata/test/expect_failures_resources/main.tftest.hcl b/internal/command/testdata/test/expect_failures_resources/main.tftest.hcl new file mode 100644 index 0000000000..899469dd88 --- /dev/null +++ b/internal/command/testdata/test/expect_failures_resources/main.tftest.hcl @@ -0,0 +1,11 @@ +variables { + input = "some value" +} + +run "test" { + command = plan + + expect_failures = [ + test_resource.resource + ] +} diff --git a/internal/command/testdata/test/expected_failures_during_planning/check.tftest.hcl b/internal/command/testdata/test/expected_failures_during_planning/check.tftest.hcl new file mode 100644 index 0000000000..0b39cf981e --- /dev/null +++ b/internal/command/testdata/test/expected_failures_during_planning/check.tftest.hcl @@ -0,0 +1,16 @@ + +run "check_passes" { + + variables { + input = "abd" + } + + # Checks are a little different, as they only produce warnings. So in this + # case we actually expect the whole run block to be fine. It'll produce + # warnings during the plan, still execute the apply operation, and then + # validate the check block failed during the apply stage. + expect_failures = [ + check.cchar, + ] + +} diff --git a/internal/command/testdata/test/expected_failures_during_planning/input.tftest.hcl b/internal/command/testdata/test/expected_failures_during_planning/input.tftest.hcl new file mode 100644 index 0000000000..7d92070f88 --- /dev/null +++ b/internal/command/testdata/test/expected_failures_during_planning/input.tftest.hcl @@ -0,0 +1,30 @@ + +run "input_failure" { + + variables { + input = "bcd" + } + + # While we do expect var.input to fail, we are asking this run block to + # execute an apply operation. It can't do that because our custom condition + # fails during the planning stage as well. Our test is going to make sure we + # add the helpful warning diagnostic explaining this. + expect_failures = [ + var.input, + ] + +} + + +// This should not run because the previous run block is expected to error, thus +// terminating the test file. +run "no_run" { + + variables { + input = "abc" + } + assert { + condition = var.input == "abc" + error_message = "should not run" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/expected_failures_during_planning/main.tf b/internal/command/testdata/test/expected_failures_during_planning/main.tf new file mode 100644 index 0000000000..2af33660f6 --- /dev/null +++ b/internal/command/testdata/test/expected_failures_during_planning/main.tf @@ -0,0 +1,37 @@ + +variable "input" { + type = string + + validation { + condition = strcontains(var.input, "a") + error_message = "input must contain the character 'a'" + } +} + +resource "test_resource" "resource" { + value = var.input + + lifecycle { + postcondition { + condition = strcontains(self.value, "b") + error_message = "input must contain the character 'b'" + } + } +} + +check "cchar" { + assert { + condition = strcontains(test_resource.resource.value, "c") + error_message = "input must contain the character 'c'" + } +} + +output "output" { + value = test_resource.resource.value + + precondition { + condition = strcontains(test_resource.resource.value, "d") + error_message = "input must contain the character 'd'" + } +} + diff --git a/internal/command/testdata/test/expected_failures_during_planning/output.tftest.hcl b/internal/command/testdata/test/expected_failures_during_planning/output.tftest.hcl new file mode 100644 index 0000000000..22463207c5 --- /dev/null +++ b/internal/command/testdata/test/expected_failures_during_planning/output.tftest.hcl @@ -0,0 +1,16 @@ + +run "output_failure" { + + variables { + input = "abc" + } + + # While we do expect output.output to fail, we are asking this run block to + # execute an apply operation. It can't do that because our custom condition + # fails during the planning stage as well. Our test is going to make sure we + # add the helpful warning diagnostic explaining this. + expect_failures = [ + output.output, + ] + +} diff --git a/internal/command/testdata/test/expected_failures_during_planning/resource.tftest.hcl b/internal/command/testdata/test/expected_failures_during_planning/resource.tftest.hcl new file mode 100644 index 0000000000..61c76655d4 --- /dev/null +++ b/internal/command/testdata/test/expected_failures_during_planning/resource.tftest.hcl @@ -0,0 +1,16 @@ + +run "resource_failure" { + + variables { + input = "acd" + } + + # While we do expect test_resource.resource to fail, we are asking this run + # block to execute an apply operation. It can't do that because our custom + # condition fails during the planning stage as well. Our test is going to make + # sure we add the helpful warning diagnostic explaining this. + expect_failures = [ + test_resource.resource, + ] + +} diff --git a/internal/command/testdata/test/functions_available/alternate.tftest.hcl b/internal/command/testdata/test/functions_available/alternate.tftest.hcl new file mode 100644 index 0000000000..29a2f83986 --- /dev/null +++ b/internal/command/testdata/test/functions_available/alternate.tftest.hcl @@ -0,0 +1,10 @@ +variables { + input = jsonencode({key:"value"}) +} + +run "test" { + assert { + condition = jsondecode(test_resource.resource.value).key == "value" + error_message = "wrong value" + } +} diff --git a/internal/command/testdata/test/functions_available/main.tf b/internal/command/testdata/test/functions_available/main.tf new file mode 100644 index 0000000000..32434a0311 --- /dev/null +++ b/internal/command/testdata/test/functions_available/main.tf @@ -0,0 +1,8 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} \ No newline at end of file diff --git a/internal/command/testdata/test/functions_available/main.tftest.hcl b/internal/command/testdata/test/functions_available/main.tftest.hcl new file mode 100644 index 0000000000..2df2131d4a --- /dev/null +++ b/internal/command/testdata/test/functions_available/main.tftest.hcl @@ -0,0 +1,11 @@ + +run "test" { + variables { + input = jsonencode({key:"value"}) + } + + assert { + condition = jsondecode(test_resource.resource.value).key == "value" + error_message = "wrong value" + } +} diff --git a/internal/command/testdata/test/global_var_ref_in_suite_var/main.tf b/internal/command/testdata/test/global_var_ref_in_suite_var/main.tf new file mode 100644 index 0000000000..5bcb244a1b --- /dev/null +++ b/internal/command/testdata/test/global_var_ref_in_suite_var/main.tf @@ -0,0 +1,10 @@ +variable "input" { + default = null + type = object({ + organization_name = string + }) +} + +output "value" { + value = var.input +} diff --git a/internal/command/testdata/test/global_var_ref_in_suite_var/run_block_output.tftest.hcl b/internal/command/testdata/test/global_var_ref_in_suite_var/run_block_output.tftest.hcl new file mode 100644 index 0000000000..38bf5750e1 --- /dev/null +++ b/internal/command/testdata/test/global_var_ref_in_suite_var/run_block_output.tftest.hcl @@ -0,0 +1,13 @@ + +variables { + input = { + organization_name = var.org_name + } +} + +run "execute" { + assert { + condition = output.value.organization_name == "my-org" + error_message = "bad output value" + } +} diff --git a/internal/command/testdata/test/global_var_ref_in_suite_var/terraform.tfvars b/internal/command/testdata/test/global_var_ref_in_suite_var/terraform.tfvars new file mode 100644 index 0000000000..724a50aa15 --- /dev/null +++ b/internal/command/testdata/test/global_var_ref_in_suite_var/terraform.tfvars @@ -0,0 +1 @@ +org_name = "my-org" diff --git a/internal/command/testdata/test/global_var_refs/environment_variable.tftest.hcl b/internal/command/testdata/test/global_var_refs/environment_variable.tftest.hcl new file mode 100644 index 0000000000..de4195c7eb --- /dev/null +++ b/internal/command/testdata/test/global_var_refs/environment_variable.tftest.hcl @@ -0,0 +1,6 @@ + +variables { + input = var.env_var_input +} + +run "execute" {} diff --git a/internal/command/testdata/test/global_var_refs/main.tf b/internal/command/testdata/test/global_var_refs/main.tf new file mode 100644 index 0000000000..af99659240 --- /dev/null +++ b/internal/command/testdata/test/global_var_refs/main.tf @@ -0,0 +1,7 @@ +variable "input" { + type = string +} + +output "value" { + value = var.input +} diff --git a/internal/command/testdata/test/global_var_refs/run_block_output.tftest.hcl b/internal/command/testdata/test/global_var_refs/run_block_output.tftest.hcl new file mode 100644 index 0000000000..d3b71602af --- /dev/null +++ b/internal/command/testdata/test/global_var_refs/run_block_output.tftest.hcl @@ -0,0 +1,17 @@ + +variables { + input = var.setup.value +} + +run "setup" { + variables { + input = "hello" + } +} + +run "execute" { + assert { + condition = output.value == "hello" + error_message = "bad output value" + } +} diff --git a/internal/command/testdata/test/invalid-cleanup-warnings/main.tf b/internal/command/testdata/test/invalid-cleanup-warnings/main.tf new file mode 100644 index 0000000000..25878f33d4 --- /dev/null +++ b/internal/command/testdata/test/invalid-cleanup-warnings/main.tf @@ -0,0 +1,8 @@ +# main.tf + +variable "input" {} + +resource "test_resource" "resource" { + value = var.input +} + diff --git a/internal/command/testdata/test/invalid-cleanup-warnings/main.tftest.hcl b/internal/command/testdata/test/invalid-cleanup-warnings/main.tftest.hcl new file mode 100644 index 0000000000..2f778e2704 --- /dev/null +++ b/internal/command/testdata/test/invalid-cleanup-warnings/main.tftest.hcl @@ -0,0 +1,13 @@ +# main.tftest.hcl + +run "test" { + variables { + input = "Hello, world!" + validation = "Hello, world!" + } + assert { + condition = test_resource.resource.value == "Hello, world!" + error_message = "bad!" + } +} + diff --git a/internal/command/testdata/test/invalid-module/main.tf b/internal/command/testdata/test/invalid-module/main.tf new file mode 100644 index 0000000000..0b62a12f69 --- /dev/null +++ b/internal/command/testdata/test/invalid-module/main.tf @@ -0,0 +1,8 @@ + +locals { + my_value = "Hello, world!" +} + +resource "test_resource" "example" { + value = local.my_value +} diff --git a/internal/command/testdata/test/invalid-module/main.tftest.hcl b/internal/command/testdata/test/invalid-module/main.tftest.hcl new file mode 100644 index 0000000000..4d5a1efa0a --- /dev/null +++ b/internal/command/testdata/test/invalid-module/main.tftest.hcl @@ -0,0 +1,8 @@ + +run "invalid" { + module { + source = "./setup" + } +} + +run "test" {} diff --git a/internal/command/testdata/test/invalid-module/setup/main.tf b/internal/command/testdata/test/invalid-module/setup/main.tf new file mode 100644 index 0000000000..2573bf34d4 --- /dev/null +++ b/internal/command/testdata/test/invalid-module/setup/main.tf @@ -0,0 +1,4 @@ + +resource "test_resource" "setup" { + value = var.not_real // Oh no! +} diff --git a/internal/command/testdata/test/invalid-overrides/main.tf b/internal/command/testdata/test/invalid-overrides/main.tf new file mode 100644 index 0000000000..5c21823e72 --- /dev/null +++ b/internal/command/testdata/test/invalid-overrides/main.tf @@ -0,0 +1,6 @@ + +resource "test_resource" "resource" {} + +module "setup" { + source = "./setup" +} diff --git a/internal/command/testdata/test/invalid-overrides/main.tftest.hcl b/internal/command/testdata/test/invalid-overrides/main.tftest.hcl new file mode 100644 index 0000000000..c6317c7a9a --- /dev/null +++ b/internal/command/testdata/test/invalid-overrides/main.tftest.hcl @@ -0,0 +1,47 @@ + +mock_provider "test" { + override_resource { + target = test_resource.absent_one + } +} + +override_resource { + target = test_resource.absent_two +} + +override_resource { + target = module.setup.test_resource.absent_three +} + +override_module { + target = module.absent_four +} + +override_resource { + // This one only exists in the main configuration, but not the setup + // configuration. We shouldn't see a warning for this. + target = module.setup.test_resource.child_resource +} + +override_resource { + // This is the reverse, only exists if you load the setup module directly. + // We shouldn't see a warning for this even though it's not in the main + // configuration. + target = test_resource.child_resource +} + +run "setup" { + module { + source = "./setup" + } + + override_resource { + target = test_resource.absent_five + } +} + +run "test" { + override_resource { + target = module.setup.test_resource.absent_six + } +} diff --git a/internal/command/testdata/test/invalid-overrides/setup/main.tf b/internal/command/testdata/test/invalid-overrides/setup/main.tf new file mode 100644 index 0000000000..42d4148b29 --- /dev/null +++ b/internal/command/testdata/test/invalid-overrides/setup/main.tf @@ -0,0 +1,2 @@ + +resource "test_resource" "child_resource" {} diff --git a/internal/command/testdata/test/invalid/main.tf b/internal/command/testdata/test/invalid/main.tf new file mode 100644 index 0000000000..0b62a12f69 --- /dev/null +++ b/internal/command/testdata/test/invalid/main.tf @@ -0,0 +1,8 @@ + +locals { + my_value = "Hello, world!" +} + +resource "test_resource" "example" { + value = local.my_value +} diff --git a/internal/command/testdata/test/invalid/main.tftest.hcl b/internal/command/testdata/test/invalid/main.tftest.hcl new file mode 100644 index 0000000000..336f52b56e --- /dev/null +++ b/internal/command/testdata/test/invalid/main.tftest.hcl @@ -0,0 +1,8 @@ + +run "invalid" { + + expect_failures = [ + local.my_value, + ] + +} diff --git a/internal/command/testdata/test/invalid_config/main.tf b/internal/command/testdata/test/invalid_config/main.tf new file mode 100644 index 0000000000..65dd87f156 --- /dev/null +++ b/internal/command/testdata/test/invalid_config/main.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +resource "test_resource" "foo" { + nein = "foo" +} diff --git a/internal/command/testdata/test/invalid_config/main.tftest.hcl b/internal/command/testdata/test/invalid_config/main.tftest.hcl new file mode 100644 index 0000000000..d3995cae55 --- /dev/null +++ b/internal/command/testdata/test/invalid_config/main.tftest.hcl @@ -0,0 +1,2 @@ + +run "test" {} diff --git a/internal/command/testdata/test/invalid_default_state/main.tf b/internal/command/testdata/test/invalid_default_state/main.tf new file mode 100644 index 0000000000..5cbdbbedcf --- /dev/null +++ b/internal/command/testdata/test/invalid_default_state/main.tf @@ -0,0 +1,8 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + value = var.input +} diff --git a/internal/command/testdata/test/invalid_default_state/main.tftest.hcl b/internal/command/testdata/test/invalid_default_state/main.tftest.hcl new file mode 100644 index 0000000000..2b9dbba21d --- /dev/null +++ b/internal/command/testdata/test/invalid_default_state/main.tftest.hcl @@ -0,0 +1,6 @@ +run "test" { + assert { + condition = test_resource.resource.value == "Hello, world!" + error_message = "wrong condition" + } +} diff --git a/internal/command/testdata/test/junit-output/1pass-1fail/expected-output.xml b/internal/command/testdata/test/junit-output/1pass-1fail/expected-output.xml new file mode 100644 index 0000000000..eace09d9dd --- /dev/null +++ b/internal/command/testdata/test/junit-output/1pass-1fail/expected-output.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/internal/command/testdata/test/junit-output/1pass-1fail/main.tf b/internal/command/testdata/test/junit-output/1pass-1fail/main.tf new file mode 100644 index 0000000000..5e2e17ac2b --- /dev/null +++ b/internal/command/testdata/test/junit-output/1pass-1fail/main.tf @@ -0,0 +1,3 @@ +locals { + number = 10 +} diff --git a/internal/command/testdata/test/junit-output/1pass-1fail/main.tftest.hcl b/internal/command/testdata/test/junit-output/1pass-1fail/main.tftest.hcl new file mode 100644 index 0000000000..e62d28e32a --- /dev/null +++ b/internal/command/testdata/test/junit-output/1pass-1fail/main.tftest.hcl @@ -0,0 +1,21 @@ +run "failing_assertion" { + assert { + condition = local.number == 10 + error_message = "assertion 1 should pass" + } + assert { + condition = local.number < 0 + error_message = "local variable 'number' has a value greater than zero, so assertion 2 will fail" + } + assert { + condition = local.number == 10 + error_message = "assertion 3 should pass" + } +} + +run "passing_assertion" { + assert { + condition = local.number > 0 + error_message = "local variable 'number' has a value greater than zero, so this assertion will pass" + } +} diff --git a/internal/command/testdata/test/junit-output/missing-provider/expected-output.xml b/internal/command/testdata/test/junit-output/missing-provider/expected-output.xml new file mode 100644 index 0000000000..dc7eb93faf --- /dev/null +++ b/internal/command/testdata/test/junit-output/missing-provider/expected-output.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/internal/command/testdata/test/junit-output/missing-provider/main.tf b/internal/command/testdata/test/junit-output/missing-provider/main.tf new file mode 100644 index 0000000000..b7217a3ea2 --- /dev/null +++ b/internal/command/testdata/test/junit-output/missing-provider/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [test.secondary] + } + } +} + +resource "test_resource" "primary" { + value = "foo" +} + +resource "test_resource" "secondary" { + provider = test.secondary + value = "bar" +} diff --git a/internal/command/testdata/test/junit-output/missing-provider/main.tftest.hcl b/internal/command/testdata/test/junit-output/missing-provider/main.tftest.hcl new file mode 100644 index 0000000000..43e8952ec7 --- /dev/null +++ b/internal/command/testdata/test/junit-output/missing-provider/main.tftest.hcl @@ -0,0 +1,13 @@ +provider "test" {} + +run "passes_validation" { + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + + assert { + condition = test_resource.secondary.value == "bar" + error_message = "secondary contains invalid value" + } +} diff --git a/internal/command/testdata/test/junit-output/multiple-files/expected-output.xml b/internal/command/testdata/test/junit-output/multiple-files/expected-output.xml new file mode 100644 index 0000000000..1e6a924545 --- /dev/null +++ b/internal/command/testdata/test/junit-output/multiple-files/expected-output.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/internal/command/testdata/test/junit-output/multiple-files/main.tf b/internal/command/testdata/test/junit-output/multiple-files/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/junit-output/multiple-files/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/junit-output/multiple-files/one.tftest.hcl b/internal/command/testdata/test/junit-output/multiple-files/one.tftest.hcl new file mode 100644 index 0000000000..66bf87c39d --- /dev/null +++ b/internal/command/testdata/test/junit-output/multiple-files/one.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/junit-output/multiple-files/two.tftest.hcl b/internal/command/testdata/test/junit-output/multiple-files/two.tftest.hcl new file mode 100644 index 0000000000..66bf87c39d --- /dev/null +++ b/internal/command/testdata/test/junit-output/multiple-files/two.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/long_running/main.tf b/internal/command/testdata/test/long_running/main.tf new file mode 100644 index 0000000000..3613cbd124 --- /dev/null +++ b/internal/command/testdata/test/long_running/main.tf @@ -0,0 +1,4 @@ +resource "test_resource" "foo" { + create_wait_seconds = 5 + destroy_wait_seconds = 3 +} diff --git a/internal/command/testdata/test/long_running/main.tftest.hcl b/internal/command/testdata/test/long_running/main.tftest.hcl new file mode 100644 index 0000000000..d3995cae55 --- /dev/null +++ b/internal/command/testdata/test/long_running/main.tftest.hcl @@ -0,0 +1,2 @@ + +run "test" {} diff --git a/internal/command/testdata/test/missing-provider-definition-in-file/main.tf b/internal/command/testdata/test/missing-provider-definition-in-file/main.tf new file mode 100644 index 0000000000..32a4e744bc --- /dev/null +++ b/internal/command/testdata/test/missing-provider-definition-in-file/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [ test.secondary, test ] + } + } +} + +resource "test_resource" "primary" { + value = "foo" +} + +resource "test_resource" "secondary" { + provider = test.secondary + value = "bar" +} diff --git a/internal/command/testdata/test/missing-provider-definition-in-file/main.tftest.hcl b/internal/command/testdata/test/missing-provider-definition-in-file/main.tftest.hcl new file mode 100644 index 0000000000..68a5f2beba --- /dev/null +++ b/internal/command/testdata/test/missing-provider-definition-in-file/main.tftest.hcl @@ -0,0 +1,24 @@ + +# provider "test" {} + +# provider "test" { +# alias = "secondary" +# } + +run "passes_validation" { + +// references a provider that is not defined in the test file + providers = { + test = test + } + + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + + assert { + condition = test_resource.secondary.value == "bar" + error_message = "secondary contains invalid value" + } +} diff --git a/internal/command/testdata/test/missing-provider-in-run-block/main.tf b/internal/command/testdata/test/missing-provider-in-run-block/main.tf new file mode 100644 index 0000000000..ded2238470 --- /dev/null +++ b/internal/command/testdata/test/missing-provider-in-run-block/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [ test.secondary ] + } + } +} + +resource "test_resource" "primary" { + value = "foo" +} + +resource "test_resource" "secondary" { + provider = test.secondary + value = "bar" +} diff --git a/internal/command/testdata/test/missing-provider-in-run-block/main.tftest.hcl b/internal/command/testdata/test/missing-provider-in-run-block/main.tftest.hcl new file mode 100644 index 0000000000..ec840ccba7 --- /dev/null +++ b/internal/command/testdata/test/missing-provider-in-run-block/main.tftest.hcl @@ -0,0 +1,23 @@ + +provider "test" {} + +provider "test" { + alias = "secondary" +} + +run "passes_validation" { + + providers = { + test = test + } + + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + + assert { + condition = test_resource.secondary.value == "bar" + error_message = "secondary contains invalid value" + } +} diff --git a/internal/command/testdata/test/missing-provider-in-test-module/main.tf b/internal/command/testdata/test/missing-provider-in-test-module/main.tf new file mode 100644 index 0000000000..de4efc04cb --- /dev/null +++ b/internal/command/testdata/test/missing-provider-in-test-module/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "primary" { + value = "foo" +} diff --git a/internal/command/testdata/test/missing-provider-in-test-module/main.tftest.hcl b/internal/command/testdata/test/missing-provider-in-test-module/main.tftest.hcl new file mode 100644 index 0000000000..98dd73ae23 --- /dev/null +++ b/internal/command/testdata/test/missing-provider-in-test-module/main.tftest.hcl @@ -0,0 +1,40 @@ + +provider "test" {} + +provider "test" { + alias = "secondary" +} + +run "passes_validation_primary" { + + providers = { + test = test + } + + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + +} + +run "passes_validation_secondary" { + + providers = { + test = test + } + + module { + source = "./setup" + } + + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + + assert { + condition = test_resource.secondary.value == "bar" + error_message = "secondary contains invalid value" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/missing-provider-in-test-module/setup/main.tf b/internal/command/testdata/test/missing-provider-in-test-module/setup/main.tf new file mode 100644 index 0000000000..ded2238470 --- /dev/null +++ b/internal/command/testdata/test/missing-provider-in-test-module/setup/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [ test.secondary ] + } + } +} + +resource "test_resource" "primary" { + value = "foo" +} + +resource "test_resource" "secondary" { + provider = test.secondary + value = "bar" +} diff --git a/internal/command/testdata/test/missing-provider/main.tf b/internal/command/testdata/test/missing-provider/main.tf new file mode 100644 index 0000000000..ded2238470 --- /dev/null +++ b/internal/command/testdata/test/missing-provider/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [ test.secondary ] + } + } +} + +resource "test_resource" "primary" { + value = "foo" +} + +resource "test_resource" "secondary" { + provider = test.secondary + value = "bar" +} diff --git a/internal/command/testdata/test/missing-provider/main.tftest.hcl b/internal/command/testdata/test/missing-provider/main.tftest.hcl new file mode 100644 index 0000000000..4901cdc802 --- /dev/null +++ b/internal/command/testdata/test/missing-provider/main.tftest.hcl @@ -0,0 +1,14 @@ + +provider "test" {} + +run "passes_validation" { + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + + assert { + condition = test_resource.secondary.value == "bar" + error_message = "secondary contains invalid value" + } +} diff --git a/internal/command/testdata/test/mocking-error/child/main.tf b/internal/command/testdata/test/mocking-error/child/main.tf new file mode 100644 index 0000000000..2ef4e4d979 --- /dev/null +++ b/internal/command/testdata/test/mocking-error/child/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [test.primary, test.secondary] + } + } +} + +variable "instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +output "primary" { + value = test_resource.primary +} + +output "secondary" { + value = test_resource.secondary +} diff --git a/internal/command/testdata/test/mocking-error/main.tf b/internal/command/testdata/test/mocking-error/main.tf new file mode 100644 index 0000000000..49506e06c3 --- /dev/null +++ b/internal/command/testdata/test/mocking-error/main.tf @@ -0,0 +1,46 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +provider "test" { + alias = "primary" +} + +provider "test" { + alias = "secondary" +} + +variable "instances" { + type = number +} + +variable "child_instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +module "child" { + count = var.instances + + source = "./child" + + providers = { + test.primary = test.primary + test.secondary = test.secondary + } + + instances = var.child_instances +} diff --git a/internal/command/testdata/test/mocking-error/tests/plan_mocked_overridden.tftest.hcl b/internal/command/testdata/test/mocking-error/tests/plan_mocked_overridden.tftest.hcl new file mode 100644 index 0000000000..5003cab765 --- /dev/null +++ b/internal/command/testdata/test/mocking-error/tests/plan_mocked_overridden.tftest.hcl @@ -0,0 +1,34 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } + + override_resource { + target = test_resource.primary + values = { + id = "bbbb" + } + } +} + +variables { + instances = 1 + child_instances = 1 +} + +// This test will fail because the plan command does not use the +// overridden values for computed properties, +// making the left-hand side of the condition unknown. +run "test" { + command = plan + + assert { + condition = test_resource.primary[0].id == "bbbb" + error_message = "plan should not have the overridden value" + } + +} diff --git a/internal/command/testdata/test/mocking-error/tests/plan_mocked_provider.tftest.hcl b/internal/command/testdata/test/mocking-error/tests/plan_mocked_provider.tftest.hcl new file mode 100644 index 0000000000..ed387c5be8 --- /dev/null +++ b/internal/command/testdata/test/mocking-error/tests/plan_mocked_provider.tftest.hcl @@ -0,0 +1,25 @@ +mock_provider "test" { + alias = "secondary" + + mock_resource "test_resource" { + defaults = { + id = "ffff" + } + } +} + + +variables { + instances = 2 + child_instances = 1 +} + +run "test" { + command = plan + + assert { + condition = test_resource.secondary[0].id == "ffff" + error_message = "plan should use the mocked provider value when override_during is plan" + } + +} diff --git a/internal/command/testdata/test/mocking-invalid/child/main.tf b/internal/command/testdata/test/mocking-invalid/child/main.tf new file mode 100644 index 0000000000..2ef4e4d979 --- /dev/null +++ b/internal/command/testdata/test/mocking-invalid/child/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [test.primary, test.secondary] + } + } +} + +variable "instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +output "primary" { + value = test_resource.primary +} + +output "secondary" { + value = test_resource.secondary +} diff --git a/internal/command/testdata/test/mocking-invalid/main.tf b/internal/command/testdata/test/mocking-invalid/main.tf new file mode 100644 index 0000000000..49506e06c3 --- /dev/null +++ b/internal/command/testdata/test/mocking-invalid/main.tf @@ -0,0 +1,46 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +provider "test" { + alias = "primary" +} + +provider "test" { + alias = "secondary" +} + +variable "instances" { + type = number +} + +variable "child_instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +module "child" { + count = var.instances + + source = "./child" + + providers = { + test.primary = test.primary + test.secondary = test.secondary + } + + instances = var.child_instances +} diff --git a/internal/command/testdata/test/mocking-invalid/tests/module_mocked_invalid_type.tftest.hcl b/internal/command/testdata/test/mocking-invalid/tests/module_mocked_invalid_type.tftest.hcl new file mode 100644 index 0000000000..ee4730f89b --- /dev/null +++ b/internal/command/testdata/test/mocking-invalid/tests/module_mocked_invalid_type.tftest.hcl @@ -0,0 +1,13 @@ +override_module { + target = module.child[1] + outputs = "should be an object" +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + # We won't even execute this, as the configuration isn't valid. +} diff --git a/internal/command/testdata/test/mocking-invalid/tests/override_computed_invalid_boolean.tftest.hcl b/internal/command/testdata/test/mocking-invalid/tests/override_computed_invalid_boolean.tftest.hcl new file mode 100644 index 0000000000..ce346c6d54 --- /dev/null +++ b/internal/command/testdata/test/mocking-invalid/tests/override_computed_invalid_boolean.tftest.hcl @@ -0,0 +1,30 @@ +mock_provider "test" { + alias = "primary" + override_during = baz // This should either be plan or apply, therefore this test should fail + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } + + override_resource { + target = test_resource.primary + values = { + id = "bbbb" + } + } +} + +variables { + instances = 1 + child_instances = 1 +} + +run "test" { + + assert { + condition = test_resource.primary[0].id == "bbbb" + error_message = "mock not applied" + } +} diff --git a/internal/command/testdata/test/mocking/child/main.tf b/internal/command/testdata/test/mocking/child/main.tf new file mode 100644 index 0000000000..2ef4e4d979 --- /dev/null +++ b/internal/command/testdata/test/mocking/child/main.tf @@ -0,0 +1,30 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [test.primary, test.secondary] + } + } +} + +variable "instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +output "primary" { + value = test_resource.primary +} + +output "secondary" { + value = test_resource.secondary +} diff --git a/internal/command/testdata/test/mocking/main.tf b/internal/command/testdata/test/mocking/main.tf new file mode 100644 index 0000000000..49506e06c3 --- /dev/null +++ b/internal/command/testdata/test/mocking/main.tf @@ -0,0 +1,46 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +provider "test" { + alias = "primary" +} + +provider "test" { + alias = "secondary" +} + +variable "instances" { + type = number +} + +variable "child_instances" { + type = number +} + +resource "test_resource" "primary" { + provider = test.primary + count = var.instances +} + +resource "test_resource" "secondary" { + provider = test.secondary + count = var.instances +} + +module "child" { + count = var.instances + + source = "./child" + + providers = { + test.primary = test.primary + test.secondary = test.secondary + } + + instances = var.child_instances +} diff --git a/internal/command/testdata/test/mocking/tests/mocks/data.tfmock.hcl b/internal/command/testdata/test/mocking/tests/mocks/data.tfmock.hcl new file mode 100644 index 0000000000..92e87c2968 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/mocks/data.tfmock.hcl @@ -0,0 +1,6 @@ +mock_resource "test_resource" { + override_during = plan + defaults = { + id = "aaaa" + } +} diff --git a/internal/command/testdata/test/mocking/tests/module_empty_outputs.tftest.hcl b/internal/command/testdata/test/mocking/tests/module_empty_outputs.tftest.hcl new file mode 100644 index 0000000000..6553ee3f37 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/module_empty_outputs.tftest.hcl @@ -0,0 +1,12 @@ +override_module { + target = module.child[1] +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + # Just want to make sure things don't crash with missing `outputs` attribute. +} diff --git a/internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl new file mode 100644 index 0000000000..f1f96e934b --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/module_mocked.tftest.hcl @@ -0,0 +1,44 @@ +override_module { + target = module.child[1] + outputs = { + primary = [ + { + id = "bbbb" + } + ] + secondary = [ + { + id = "cccc" + } + ] + } +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + + assert { + condition = module.child[0].primary[0].id != "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[1].primary[0].id == "bbbb" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[1].secondary[0].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[2].secondary[0].id != "cccc" + error_message = "wrongly applied mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl b/internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl new file mode 100644 index 0000000000..26114fb1df --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/module_mocked_overridden.tftest.hcl @@ -0,0 +1,70 @@ +override_module { + target = module.child + outputs = { + primary = [ + { + id = "bbbb" + } + ] + secondary = [ + { + id = "cccc" + } + ] + } +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + + override_module { + target = module.child[1] + outputs = { + primary = [ + { + id = "aaaa" + } + ] + secondary = [ + { + id = "dddd" + } + ] + } + } + + assert { + condition = module.child[0].primary[0].id == "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[0].secondary[0].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[2].primary[0].id == "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[2].secondary[0].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[1].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[1].secondary[0].id == "dddd" + error_message = "did not apply mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl b/internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl new file mode 100644 index 0000000000..f814dc79e7 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/no_mocks.tftest.hcl @@ -0,0 +1,7 @@ + +variables { + instances = 1 + child_instances = 0 +} + +run "test" {} diff --git a/internal/command/testdata/test/mocking/tests/plan_mocked_overridden.tftest.hcl b/internal/command/testdata/test/mocking/tests/plan_mocked_overridden.tftest.hcl new file mode 100644 index 0000000000..f5dd295096 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/plan_mocked_overridden.tftest.hcl @@ -0,0 +1,32 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } + + override_resource { + target = test_resource.primary + override_during = plan + values = { + id = "bbbb" + } + } +} + +variables { + instances = 1 + child_instances = 1 +} + +run "test" { + command = plan + + assert { + condition = test_resource.primary[0].id == "bbbb" + error_message = "plan should override the value when override_during is plan" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/plan_mocked_provider.tftest.hcl b/internal/command/testdata/test/mocking/tests/plan_mocked_provider.tftest.hcl new file mode 100644 index 0000000000..c47755d4b5 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/plan_mocked_provider.tftest.hcl @@ -0,0 +1,26 @@ +mock_provider "test" { + alias = "secondary" + override_during = plan + + mock_resource "test_resource" { + defaults = { + id = "ffff" + } + } +} + + +variables { + instances = 2 + child_instances = 1 +} + +run "test" { + command = plan + + assert { + condition = test_resource.secondary[0].id == "ffff" + error_message = "plan should use the mocked provider value when override_during is plan" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/plan_mocked_provider_overridden.tftest.hcl b/internal/command/testdata/test/mocking/tests/plan_mocked_provider_overridden.tftest.hcl new file mode 100644 index 0000000000..9c553deda2 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/plan_mocked_provider_overridden.tftest.hcl @@ -0,0 +1,55 @@ +mock_provider "test" { + alias = "primary" + override_during = plan + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } + + override_resource { + target = test_resource.primary + values = { + id = "bbbb" + } + } + + override_resource { + target = test_resource.primary[1] + override_during = apply // this should take precedence over the provider-level override_during + values = { + id = "bbbb" + } + } +} + + +override_resource { + target = test_resource.secondary[0] + override_during = plan + values = { + id = "ssss" + } +} + + +variables { + instances = 2 + child_instances = 1 +} + +run "test" { + command = plan + + assert { + condition = test_resource.primary[0].id == "bbbb" + error_message = "plan should override the value when override_during is plan" + } + + assert { + condition = test_resource.secondary[0].id == "ssss" + error_message = "plan should override the value when override_during is plan" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl new file mode 100644 index 0000000000..e9ea31a880 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/primary_mocked.tftest.hcl @@ -0,0 +1,39 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } +} + +variables { + instances = 1 + child_instances = 1 +} + + +run "test" { + + assert { + condition = test_resource.primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[0].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.secondary[0].id != "aaaa" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[0].secondary[0].id != "aaaa" + error_message = "wrongly applied mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/primary_mocked_external.tftest.hcl b/internal/command/testdata/test/mocking/tests/primary_mocked_external.tftest.hcl new file mode 100644 index 0000000000..5c3735bb2b --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/primary_mocked_external.tftest.hcl @@ -0,0 +1,47 @@ +mock_provider "test" { + alias = "primary" + + source ="./tests/mocks" +} + +mock_provider "test" { + alias = "secondary" + + mock_resource "test_resource" { + override_during = plan + defaults = { + id = "bbbb" + } + } +} + +variables { + instances = 1 + child_instances = 1 +} + + +run "test" { + command = plan + + assert { + condition = test_resource.primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[0].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.secondary[0].id == "bbbb" + error_message = "wrongly applied mocks" + } + + assert { + condition = module.child[0].secondary[0].id == "bbbb" + error_message = "wrongly applied mocks" + } + +} diff --git a/internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl b/internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl new file mode 100644 index 0000000000..62fca3e29c --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/primary_mocked_overridden.tftest.hcl @@ -0,0 +1,64 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + } + } + + override_resource { + target = test_resource.primary + values = { + id = "bbbb" + } + } +} + +variables { + instances = 3 + child_instances = 1 +} + +run "test" { + + override_resource { + target = test_resource.primary[1] + values = { + id = "cccc" + } + } + + assert { + condition = test_resource.primary[0].id == "bbbb" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.primary[1].id == "cccc" + error_message = "did not apply mocks" + } + + assert { + condition = test_resource.primary[2].id == "bbbb" + error_message = "did not apply mocks" + } + + assert { + condition = module.child[0].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + // Override should not affect the other instances + condition = !contains(["aaaa", "cccc"], test_resource.secondary[0].id) + error_message = "override from another instance affected this instance" + } + + assert { + // Provider Override should propagate to the child module + condition = module.child[0].primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + +} diff --git a/internal/command/testdata/test/multiple_files/main.tf b/internal/command/testdata/test/multiple_files/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/multiple_files/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/multiple_files/one.tftest.hcl b/internal/command/testdata/test/multiple_files/one.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/multiple_files/one.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/multiple_files/two.tftest.hcl b/internal/command/testdata/test/multiple_files/two.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/multiple_files/two.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/nested_unknown_values/main.tf b/internal/command/testdata/test/nested_unknown_values/main.tf new file mode 100644 index 0000000000..6c7977cff7 --- /dev/null +++ b/internal/command/testdata/test/nested_unknown_values/main.tf @@ -0,0 +1,19 @@ + +variable "input" { + type = object({ + one = string, + two = string, + }) +} + +resource "test_resource" "resource" { + value = var.input.two +} + +output "one" { + value = test_resource.resource.id +} + +output "two" { + value = test_resource.resource.value +} diff --git a/internal/command/testdata/test/nested_unknown_values/main.tftest.hcl b/internal/command/testdata/test/nested_unknown_values/main.tftest.hcl new file mode 100644 index 0000000000..1a63ad93a6 --- /dev/null +++ b/internal/command/testdata/test/nested_unknown_values/main.tftest.hcl @@ -0,0 +1,33 @@ + +run "first" { + + command = plan + + variables { + input = { + one = "one" + two = "two" + } + } +} + +run "second" { + + command = plan + + variables { + input = { + # This should be okay, as run.first.one is unknown but we're not + # referencing it directly. + one = "one" + two = run.first.two + } + } +} + +run "third" { + variables { + # This should fail as one of the values in run.second is unknown. + input = run.second + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/no_providers_in_main/main.tf b/internal/command/testdata/test/no_providers_in_main/main.tf new file mode 100644 index 0000000000..fe07905e58 --- /dev/null +++ b/internal/command/testdata/test/no_providers_in_main/main.tf @@ -0,0 +1,19 @@ + +terraform { + required_providers { + test = { + source = "hashicorp/test" + configuration_aliases = [test.primary, test.secondary] + } + } +} + +resource "test_resource" "primary" { + provider = test.primary + value = "foo" +} + +resource "test_resource" "secondary" { + provider = test.secondary + value = "bar" +} diff --git a/internal/command/testdata/test/no_providers_in_main/main.tftest.hcl b/internal/command/testdata/test/no_providers_in_main/main.tftest.hcl new file mode 100644 index 0000000000..6d888d177c --- /dev/null +++ b/internal/command/testdata/test/no_providers_in_main/main.tftest.hcl @@ -0,0 +1,20 @@ + +provider "test" { + alias = "primary" +} + +provider "test" { + alias = "secondary" +} + +run "passes_validation" { + assert { + condition = test_resource.primary.value == "foo" + error_message = "primary contains invalid value" + } + + assert { + condition = test_resource.secondary.value == "bar" + error_message = "secondary contains invalid value" + } +} diff --git a/internal/command/testdata/test/no_state/main.tf b/internal/command/testdata/test/no_state/main.tf new file mode 100644 index 0000000000..4a8ea3c797 --- /dev/null +++ b/internal/command/testdata/test/no_state/main.tf @@ -0,0 +1,14 @@ + +variable "input" { + type = number +} + +variable "input2" { + type = number + ephemeral = true + default = 0 +} + +output "output" { + value = var.input > 5 ? var.input : null +} diff --git a/internal/command/testdata/test/no_state/main.tftest.hcl b/internal/command/testdata/test/no_state/main.tftest.hcl new file mode 100644 index 0000000000..bb175c177e --- /dev/null +++ b/internal/command/testdata/test/no_state/main.tftest.hcl @@ -0,0 +1,13 @@ + +run "first" { + variables { + input = 2 + } + +// var.input2 is ephemeral, and this would cause it to not be set during the plan phase, +// but will be set to default during the apply phase. This leads to the apply update being null + assert { + condition = output.output == var.input2 + error_message = "output should have been null" + } +} diff --git a/internal/command/testdata/test/null-outputs/main.tf b/internal/command/testdata/test/null-outputs/main.tf new file mode 100644 index 0000000000..83bff5c54b --- /dev/null +++ b/internal/command/testdata/test/null-outputs/main.tf @@ -0,0 +1,8 @@ + +variable "input" { + type = number +} + +output "output" { + value = var.input > 5 ? var.input : null +} diff --git a/internal/command/testdata/test/null-outputs/main.tftest.hcl b/internal/command/testdata/test/null-outputs/main.tftest.hcl new file mode 100644 index 0000000000..f8b8072438 --- /dev/null +++ b/internal/command/testdata/test/null-outputs/main.tftest.hcl @@ -0,0 +1,22 @@ + +run "first" { + variables { + input = 2 + } + + assert { + condition = output.output == null + error_message = "output should have been null" + } +} + +run "second" { + variables { + input = 8 + } + + assert { + condition = output.output == 8 + error_message = "output should have been 8" + } +} diff --git a/internal/command/testdata/test/null_value_in_assert/main.tf b/internal/command/testdata/test/null_value_in_assert/main.tf new file mode 100644 index 0000000000..aac3d54110 --- /dev/null +++ b/internal/command/testdata/test/null_value_in_assert/main.tf @@ -0,0 +1,17 @@ + +variable "null_input" { + type = string + default = null +} + +variable "interesting_input" { + type = string +} + +resource "test_resource" "resource" { + value = var.interesting_input +} + +output "null_output" { + value = var.null_input +} diff --git a/internal/command/testdata/test/null_value_in_assert/main.tftest.hcl b/internal/command/testdata/test/null_value_in_assert/main.tftest.hcl new file mode 100644 index 0000000000..83b26d3814 --- /dev/null +++ b/internal/command/testdata/test/null_value_in_assert/main.tftest.hcl @@ -0,0 +1,16 @@ + +run "first" { + variables { + interesting_input = "bar" + } + + assert { + condition = test_resource.resource.value == output.null_output + error_message = "this is always going to fail" + } + + assert { + condition = var.null_input == output.null_output + error_message = "this should pass" + } +} diff --git a/internal/command/testdata/test/null_value_in_vars/fail.tftest.hcl b/internal/command/testdata/test/null_value_in_vars/fail.tftest.hcl new file mode 100644 index 0000000000..24b7bef410 --- /dev/null +++ b/internal/command/testdata/test/null_value_in_vars/fail.tftest.hcl @@ -0,0 +1,13 @@ + +run "first" { + variables { + interesting_input = "bar" + } +} + +run "second" { + variables { + // It shouldn't let this happen. + interesting_input = run.first.null_output + } +} diff --git a/internal/command/testdata/test/null_value_in_vars/main.tf b/internal/command/testdata/test/null_value_in_vars/main.tf new file mode 100644 index 0000000000..8d80485053 --- /dev/null +++ b/internal/command/testdata/test/null_value_in_vars/main.tf @@ -0,0 +1,18 @@ + +variable "null_input" { + type = string + default = null +} + +variable "interesting_input" { + type = string + nullable = false +} + +resource "test_resource" "resource" { + value = var.interesting_input +} + +output "null_output" { + value = var.null_input +} diff --git a/internal/command/testdata/test/null_value_in_vars/pass.tftest.hcl b/internal/command/testdata/test/null_value_in_vars/pass.tftest.hcl new file mode 100644 index 0000000000..3568b2c2f4 --- /dev/null +++ b/internal/command/testdata/test/null_value_in_vars/pass.tftest.hcl @@ -0,0 +1,18 @@ + +run "first" { + variables { + interesting_input = "bar" + } +} + +run "second" { + variables { + interesting_input = "bar" + null_input = run.first.null_output + } + + assert { + condition = output.null_output == run.first.null_output + error_message = "should have passed" + } +} diff --git a/internal/command/testdata/test/only_modules/example/main.tf b/internal/command/testdata/test/only_modules/example/main.tf new file mode 100644 index 0000000000..3e6809599c --- /dev/null +++ b/internal/command/testdata/test/only_modules/example/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string +} + +resource "test_resource" "module_resource" { + id = "df6h8as9" + value = var.input +} diff --git a/internal/command/testdata/test/only_modules/main.tf b/internal/command/testdata/test/only_modules/main.tf new file mode 100644 index 0000000000..0cad6cfbbc --- /dev/null +++ b/internal/command/testdata/test/only_modules/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + id = "598318e0" + value = var.input +} diff --git a/internal/command/testdata/test/only_modules/main.tftest.hcl b/internal/command/testdata/test/only_modules/main.tftest.hcl new file mode 100644 index 0000000000..480e8bb93e --- /dev/null +++ b/internal/command/testdata/test/only_modules/main.tftest.hcl @@ -0,0 +1,23 @@ +# This is an example test file from a use case requested by a user. We only +# refer to alternate modules and not the main configuration. This means we +# shouldn't have to provide any data for the main configuration. + +run "first" { + module { + source = "./example" + } + + variables { + input = "start" + } +} + +run "second" { + module { + source = "./example" + } + + variables { + input = "update" + } +} diff --git a/internal/command/testdata/test/parallel/main.tf b/internal/command/testdata/test/parallel/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/parallel/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/parallel/main.tftest.hcl b/internal/command/testdata/test/parallel/main.tftest.hcl new file mode 100644 index 0000000000..f78376839b --- /dev/null +++ b/internal/command/testdata/test/parallel/main.tftest.hcl @@ -0,0 +1,91 @@ +test { + // This would set the parallel flag to true in all runs + parallel = true +} + +variables { + foo = "foo" +} + + +run "main_first" { + state_key = "start" + module { + source = "./setup" + } + + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "bad" + } +} + +run "main_second" { + variables { + input = run.main_first.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.main_first.value == var.foo + error_message = "triple bad" + } +} + +run "main_third" { + variables { + input = run.main_second.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.main_first.value == var.foo + error_message = "triple bad" + } +} + +run "main_fourth" { + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +// The satisfies all the conditions to run in parallel, but the parallel flag is set to false, +// so it should run in sequence +run "main_fifth" { + state_key = "start" + parallel = false + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +// Expected order: +// - run [main_first] +// - run [main_second] +// - run [main_third] +// - run [main_fourth] +// - run [main_fifth] + diff --git a/internal/command/testdata/test/parallel/parallel.tftest.hcl b/internal/command/testdata/test/parallel/parallel.tftest.hcl new file mode 100644 index 0000000000..61fc678218 --- /dev/null +++ b/internal/command/testdata/test/parallel/parallel.tftest.hcl @@ -0,0 +1,524 @@ +// To run in parallel, sequential runs must have different state keys, and not depend on each other +// NotDepends: true +// DiffStateKey: true +test { + // This would set the parallel flag to true in all runs + parallel = true +} + +variables { + foo = "foo" +} + + +run "setup" { + state_key = "start" + module { + source = "./setup" + } + + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "bad" + } +} + +// Depends on previous run, but has different state key, so would not run in parallel +// NotDepends: false +// DiffStateKey: true +run "test_a" { + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.setup.value == var.foo + error_message = "triple bad" + } +} + +// Depends on previous run, and has same state key, so would not run in parallel +// NotDepends: false +// DiffStateKey: false +run "test_b" { + variables { + input = run.test_a.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.setup.value == var.foo + error_message = "triple bad" + } +} + +// Does not depend on previous run, and has same state key, so would not run in parallel +// NotDepends: true +// DiffStateKey: false +run "test_c" { + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +// Does not depend on previous run, and has different state key, so would run in parallel +// NotDepends: true +// DiffStateKey: true +// However, it has a state key that is the same as run.setup, so it should wait for that run, and +// thus may run in parallel with test_a +run "test_d" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_1" { + state_key = "state_foo" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_1" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_2" { + state_key = "state_bar" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_2" + } +} + +// NotDepends: true +// DiffStateKey: false +run "test_3" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_3" + } +} + +// NotDepends: false +// DiffStateKey: false +run "test_4" { + state_key = "start" + variables { + input = run.test_2.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_4" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_5" { + state_key = "state_baz" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_5" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_6" { + state_key = "state_qux" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_6" + } +} + +// NotDepends: true +// DiffStateKey: false +run "test_7" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_7" + } +} + +// NotDepends: false +// DiffStateKey: false +run "test_8" { + state_key = "start" + variables { + input = run.test_6.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_8" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_9" { + state_key = "state_foo" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_9" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_10" { + state_key = "state_bar" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_10" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_11" { + state_key = "state_foo" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_11" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_12" { + state_key = "state_bar" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_12" + } +} + +// NotDepends: true +// DiffStateKey: false +run "test_13" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_13" + } +} + +// NotDepends: false +// DiffStateKey: false +run "test_14" { + state_key = "start" + variables { + input = run.test_12.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_14" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_15" { + state_key = "state_baz" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_15" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_16" { + state_key = "state_qux" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_16" + } +} + +// NotDepends: true +// DiffStateKey: false +run "test_17" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_17" + } +} + +// NotDepends: false +// DiffStateKey: false +run "test_18" { + state_key = "start" + variables { + input = run.test_16.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_18" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_19" { + state_key = "state_foo" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_19" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_20" { + state_key = "state_bar" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_20" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_21" { + state_key = "state_foo" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_21" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_22" { + state_key = "state_bar" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_22" + } +} + +// NotDepends: true +// DiffStateKey: false +run "test_23" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_23" + } +} + +// NotDepends: false +// DiffStateKey: false +run "test_24" { + state_key = "start" + variables { + input = run.test_22.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_24" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_25" { + state_key = "state_baz" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_25" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_26" { + state_key = "state_qux" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_26" + } +} + +// NotDepends: true +// DiffStateKey: false +run "test_27" { + state_key = "start" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_27" + } +} + +// NotDepends: false +// DiffStateKey: false +run "test_28" { + state_key = "start" + variables { + input = run.test_26.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_28" + } +} + +// NotDepends: true +// DiffStateKey: true +run "test_29" { + state_key = "state_foo" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "error in test_29" + } +} + +// NotDepends: false +// DiffStateKey: true +run "test_30" { + state_key = "state_bar" + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "error in test_30" + } +} + +// Expected order: +// - run [setup] +// - run [test_a, test_d] +// - run [test_b] +// - run [test_c] + diff --git a/internal/command/testdata/test/parallel/setup/main.tf b/internal/command/testdata/test/parallel/setup/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/parallel/setup/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/parallel_divided/main.tf b/internal/command/testdata/test/parallel_divided/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/parallel_divided/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/parallel_divided/main.tftest.hcl b/internal/command/testdata/test/parallel_divided/main.tftest.hcl new file mode 100644 index 0000000000..977bf9068a --- /dev/null +++ b/internal/command/testdata/test/parallel_divided/main.tftest.hcl @@ -0,0 +1,96 @@ +test { + // This would set the parallel flag to true in all runs + parallel = true +} + +variables { + foo = "foo" +} + + +run "main_first" { + state_key = "start" + module { + source = "./setup" + } + + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "bad" + } +} + +run "main_second" { + variables { + input = run.main_first.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.main_first.value == var.foo + error_message = "triple bad" + } +} + +run "main_third" { + state_key = "uniq_3" + variables { + input = run.main_second.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.main_first.value == var.foo + error_message = "triple bad" + } +} + +run "main_fourth" { + parallel = false // effectively dividing the parallelizable group into two + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +// Because of the division caused by main_fourth, main_fifth and main_sixth would run in parallel, +// but would only run after main_fourth has completed. +run "main_fifth" { + state_key = "uniq_5" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +run "main_sixth" { + state_key = "uniq_6" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/parallel_divided/setup/main.tf b/internal/command/testdata/test/parallel_divided/setup/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/parallel_divided/setup/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/partial_updates/main.tf b/internal/command/testdata/test/partial_updates/main.tf new file mode 100644 index 0000000000..0da3bb1ff0 --- /dev/null +++ b/internal/command/testdata/test/partial_updates/main.tf @@ -0,0 +1,15 @@ + +resource "test_resource" "resource" {} + +locals { + follow = { + (test_resource.resource.id): "follow" + } +} + +resource "test_resource" "follow" { + for_each = local.follow + + id = each.key + value = each.value +} diff --git a/internal/command/testdata/test/partial_updates/main.tftest.hcl b/internal/command/testdata/test/partial_updates/main.tftest.hcl new file mode 100644 index 0000000000..4698ccaaf1 --- /dev/null +++ b/internal/command/testdata/test/partial_updates/main.tftest.hcl @@ -0,0 +1,10 @@ + +run "first" { + plan_options { + target = [ + test_resource.resource, + ] + } +} + +run "second" {} \ No newline at end of file diff --git a/internal/command/testdata/test/pass_with_locals/main.tf b/internal/command/testdata/test/pass_with_locals/main.tf new file mode 100644 index 0000000000..d398a7b8fa --- /dev/null +++ b/internal/command/testdata/test/pass_with_locals/main.tf @@ -0,0 +1,7 @@ +resource "test_resource" "foo" { + value = "bar" +} + +locals { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/pass_with_locals/main.tftest.hcl b/internal/command/testdata/test/pass_with_locals/main.tftest.hcl new file mode 100644 index 0000000000..396eb3021a --- /dev/null +++ b/internal/command/testdata/test/pass_with_locals/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = local.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/pass_with_outputs/main.tf b/internal/command/testdata/test/pass_with_outputs/main.tf new file mode 100644 index 0000000000..7354f944a1 --- /dev/null +++ b/internal/command/testdata/test/pass_with_outputs/main.tf @@ -0,0 +1,7 @@ +resource "test_resource" "foo" { + value = "bar" +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/pass_with_outputs/main.tftest.hcl b/internal/command/testdata/test/pass_with_outputs/main.tftest.hcl new file mode 100644 index 0000000000..bdf84aa556 --- /dev/null +++ b/internal/command/testdata/test/pass_with_outputs/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = output.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/pass_with_variables/main.tf b/internal/command/testdata/test/pass_with_variables/main.tf new file mode 100644 index 0000000000..3d98070d87 --- /dev/null +++ b/internal/command/testdata/test/pass_with_variables/main.tf @@ -0,0 +1,7 @@ +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} diff --git a/internal/command/testdata/test/pass_with_variables/main.tftest.hcl b/internal/command/testdata/test/pass_with_variables/main.tftest.hcl new file mode 100644 index 0000000000..a46894fb18 --- /dev/null +++ b/internal/command/testdata/test/pass_with_variables/main.tftest.hcl @@ -0,0 +1,21 @@ +variables { + input = "bar" +} + +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} + +run "apply_test_resource" { + variables { + input = "zap" + } + + assert { + condition = test_resource.foo.value == "zap" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/plan_then_apply/main.tf b/internal/command/testdata/test/plan_then_apply/main.tf new file mode 100644 index 0000000000..52edae3fd5 --- /dev/null +++ b/internal/command/testdata/test/plan_then_apply/main.tf @@ -0,0 +1,4 @@ +resource "test_resource" "foo" { + id = "constant_value" + value = "bar" +} diff --git a/internal/command/testdata/test/plan_then_apply/main.tftest.hcl b/internal/command/testdata/test/plan_then_apply/main.tftest.hcl new file mode 100644 index 0000000000..67754835dd --- /dev/null +++ b/internal/command/testdata/test/plan_then_apply/main.tftest.hcl @@ -0,0 +1,16 @@ +run "validate_test_resource" { + + command = plan + + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} + +run "apply_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/provider-functions-available/main.tf b/internal/command/testdata/test/provider-functions-available/main.tf new file mode 100644 index 0000000000..f6535cd4dc --- /dev/null +++ b/internal/command/testdata/test/provider-functions-available/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + test = { + source = "test" + } + } +} + + +output "value" { + value = provider::test::is_true(true) +} diff --git a/internal/command/testdata/test/provider-functions-available/main.tftest.hcl b/internal/command/testdata/test/provider-functions-available/main.tftest.hcl new file mode 100644 index 0000000000..2f965e3cda --- /dev/null +++ b/internal/command/testdata/test/provider-functions-available/main.tftest.hcl @@ -0,0 +1,7 @@ + +run "test" { + assert { + condition = provider::test::is_true(output.value) + error_message = "bad response" + } +} diff --git a/internal/command/testdata/test/provider_runs/main.tf b/internal/command/testdata/test/provider_runs/main.tf new file mode 100644 index 0000000000..50a07d62f0 --- /dev/null +++ b/internal/command/testdata/test/provider_runs/main.tf @@ -0,0 +1 @@ +resource "test_resource" "foo" {} diff --git a/internal/command/testdata/test/provider_runs/main.tftest.hcl b/internal/command/testdata/test/provider_runs/main.tftest.hcl new file mode 100644 index 0000000000..2dd8c53663 --- /dev/null +++ b/internal/command/testdata/test/provider_runs/main.tftest.hcl @@ -0,0 +1,24 @@ +variables { + resource_directory = "resources" +} + +provider "test" { + alias = "setup" + resource_prefix = var.resource_directory +} + +run "setup" { + module { + source = "./setup" + } + + providers = { + test = test.setup + } +} + +provider "test" { + resource_prefix = run.setup.resource_directory +} + +run "main" {} diff --git a/internal/command/testdata/test/provider_runs/setup/main.tf b/internal/command/testdata/test/provider_runs/setup/main.tf new file mode 100644 index 0000000000..25d3e57e96 --- /dev/null +++ b/internal/command/testdata/test/provider_runs/setup/main.tf @@ -0,0 +1,11 @@ +variable "resource_directory" { + type = string +} + +resource "test_resource" "foo" { + value = var.resource_directory +} + +output "resource_directory" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/provider_runs_invalid/main.tf b/internal/command/testdata/test/provider_runs_invalid/main.tf new file mode 100644 index 0000000000..25d3e57e96 --- /dev/null +++ b/internal/command/testdata/test/provider_runs_invalid/main.tf @@ -0,0 +1,11 @@ +variable "resource_directory" { + type = string +} + +resource "test_resource" "foo" { + value = var.resource_directory +} + +output "resource_directory" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/provider_runs_invalid/missing_run_block.tftest.hcl b/internal/command/testdata/test/provider_runs_invalid/missing_run_block.tftest.hcl new file mode 100644 index 0000000000..bd88563d17 --- /dev/null +++ b/internal/command/testdata/test/provider_runs_invalid/missing_run_block.tftest.hcl @@ -0,0 +1,9 @@ +provider "test" { + resource_prefix = run.missing.resource_directory +} + +run "main" { + variables { + resource_directory = "resource" + } +} diff --git a/internal/command/testdata/test/provider_runs_invalid/unavailable_run_block.tftest.hcl b/internal/command/testdata/test/provider_runs_invalid/unavailable_run_block.tftest.hcl new file mode 100644 index 0000000000..ec39cd278a --- /dev/null +++ b/internal/command/testdata/test/provider_runs_invalid/unavailable_run_block.tftest.hcl @@ -0,0 +1,9 @@ +provider "test" { + resource_prefix = run.main.resource_directory +} + +run "main" { + variables { + resource_directory = "resource" + } +} diff --git a/internal/command/testdata/test/provider_runs_invalid/unused_provider.tftest.hcl b/internal/command/testdata/test/provider_runs_invalid/unused_provider.tftest.hcl new file mode 100644 index 0000000000..c0a4dec3bf --- /dev/null +++ b/internal/command/testdata/test/provider_runs_invalid/unused_provider.tftest.hcl @@ -0,0 +1,17 @@ +provider "test" { + resource_prefix = run.main.resource_directory +} + +provider "test" { + alias = "usable" +} + +run "main" { + providers = { + test = test.usable + } + + variables { + resource_directory = "resource" + } +} diff --git a/internal/command/testdata/test/provider_vars/main.tf b/internal/command/testdata/test/provider_vars/main.tf new file mode 100644 index 0000000000..8968be12da --- /dev/null +++ b/internal/command/testdata/test/provider_vars/main.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +resource "test_resource" "foo" {} diff --git a/internal/command/testdata/test/provider_vars/main.tftest.hcl b/internal/command/testdata/test/provider_vars/main.tftest.hcl new file mode 100644 index 0000000000..2f06972d60 --- /dev/null +++ b/internal/command/testdata/test/provider_vars/main.tftest.hcl @@ -0,0 +1,10 @@ + +variables { + resource_directory = "my-resource-dir" +} + +provider "test" { + resource_prefix = var.resource_directory +} + +run "test" {} diff --git a/internal/command/testdata/test/sensitive_input_values/main.tf b/internal/command/testdata/test/sensitive_input_values/main.tf new file mode 100644 index 0000000000..14e85cd4ae --- /dev/null +++ b/internal/command/testdata/test/sensitive_input_values/main.tf @@ -0,0 +1,13 @@ +variable "password" { + type = string +} + +resource "test_resource" "resource" { + id = "9ddca5a9" + value = var.password +} + +output "password" { + value = var.password + sensitive = true +} diff --git a/internal/command/testdata/test/sensitive_input_values/main.tftest.hcl b/internal/command/testdata/test/sensitive_input_values/main.tftest.hcl new file mode 100644 index 0000000000..3f6f372a6e --- /dev/null +++ b/internal/command/testdata/test/sensitive_input_values/main.tftest.hcl @@ -0,0 +1,33 @@ +run "setup" { + variables { + password = "password" + } + + module { + source = "./setup" + } +} + +run "test" { + variables { + password = run.setup.password + } +} + +run "test_failed" { + variables { + password = run.setup.password + complex = { + foo = "bar" + baz = run.test.password + } + } + + assert { + condition = var.complex == { + foo = "bar" + baz = test_resource.resource.id + } + error_message = "expected to fail" + } +} diff --git a/internal/command/testdata/test/sensitive_input_values/setup/main.tf b/internal/command/testdata/test/sensitive_input_values/setup/main.tf new file mode 100644 index 0000000000..0b0576edb8 --- /dev/null +++ b/internal/command/testdata/test/sensitive_input_values/setup/main.tf @@ -0,0 +1,9 @@ +variable "password" { + sensitive = true + type = string +} + +output "password" { + value = var.password + sensitive = true +} diff --git a/internal/command/testdata/test/shared_state/main.tf b/internal/command/testdata/test/shared_state/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/shared_state/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/shared_state/main.tftest.hcl b/internal/command/testdata/test/shared_state/main.tftest.hcl new file mode 100644 index 0000000000..9ee5ee20ef --- /dev/null +++ b/internal/command/testdata/test/shared_state/main.tftest.hcl @@ -0,0 +1,37 @@ + +variables { + foo = "foo" +} + + +run "setup" { + module { + source = "./setup" + } + + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "bad" + } +} + +run "test" { + + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.setup.value == var.foo + error_message = "triple bad" + } +} diff --git a/internal/command/testdata/test/shared_state/no-shared-state.tftest.hcl b/internal/command/testdata/test/shared_state/no-shared-state.tftest.hcl new file mode 100644 index 0000000000..3287103c97 --- /dev/null +++ b/internal/command/testdata/test/shared_state/no-shared-state.tftest.hcl @@ -0,0 +1,91 @@ +variables { + foo = "foo" +} + + +run "setup" { + state_key = "setup" + module { + source = "./setup" + } + + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "bad" + } +} + +run "test_a" { + state_key = "test_a" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.setup.value == var.foo + error_message = "triple bad" + } +} + +run "test_b" { + state_key = "test_b" + variables { + input = run.test_a.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.setup.value == var.foo + error_message = "triple bad" + } +} + +run "test_c" { + state_key = "test_c" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + + +run "test_d" { + state_key = "test_d" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} + +run "test_e" { + state_key = "test_e" + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/shared_state/setup/main.tf b/internal/command/testdata/test/shared_state/setup/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/shared_state/setup/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/shared_state_object/main.tf b/internal/command/testdata/test/shared_state_object/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/shared_state_object/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/shared_state_object/main.tftest.hcl b/internal/command/testdata/test/shared_state_object/main.tftest.hcl new file mode 100644 index 0000000000..3fb3e975f9 --- /dev/null +++ b/internal/command/testdata/test/shared_state_object/main.tftest.hcl @@ -0,0 +1,37 @@ + +variables { + foo = "foo" +} + + +run "setup" { + module { + source = "./setup" + } + + variables { + input = "foo" + } + + assert { + condition = output.value == var.foo + error_message = "bad" + } +} + +run "test" { + + variables { + input = run.setup.value + } + + assert { + condition = output.value == var.foo + error_message = "double bad" + } + + assert { + condition = run.setup == { value : "foo" } + error_message = "triple bad" + } +} diff --git a/internal/command/testdata/test/shared_state_object/setup/main.tf b/internal/command/testdata/test/shared_state_object/setup/main.tf new file mode 100644 index 0000000000..ada34cf2db --- /dev/null +++ b/internal/command/testdata/test/shared_state_object/setup/main.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = string +} + +resource "test_resource" "foo" { + value = var.input +} + +output "value" { + value = test_resource.foo.value +} diff --git a/internal/command/testdata/test/simple_fail/main.tf b/internal/command/testdata/test/simple_fail/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/simple_fail/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/simple_fail/main.tftest.hcl b/internal/command/testdata/test/simple_fail/main.tftest.hcl new file mode 100644 index 0000000000..319a217673 --- /dev/null +++ b/internal/command/testdata/test/simple_fail/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "zap" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/simple_pass/main.tf b/internal/command/testdata/test/simple_pass/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/simple_pass/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/simple_pass/main.tftest.hcl b/internal/command/testdata/test/simple_pass/main.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/simple_pass/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/simple_pass_nested/main.tf b/internal/command/testdata/test/simple_pass_nested/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/simple_pass_nested/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/simple_pass_nested/tests/main.tftest.hcl b/internal/command/testdata/test/simple_pass_nested/tests/main.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/simple_pass_nested/tests/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/simple_pass_nested_alternate/main.tf b/internal/command/testdata/test/simple_pass_nested_alternate/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/simple_pass_nested_alternate/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/simple_pass_nested_alternate/other/main.tftest.hcl b/internal/command/testdata/test/simple_pass_nested_alternate/other/main.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/simple_pass_nested_alternate/other/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/simple_pass_very_nested/main.tf b/internal/command/testdata/test/simple_pass_very_nested/main.tf new file mode 100644 index 0000000000..41cc84e5c4 --- /dev/null +++ b/internal/command/testdata/test/simple_pass_very_nested/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = "bar" +} diff --git a/internal/command/testdata/test/simple_pass_very_nested/tests/subdir/main.tftest.hcl b/internal/command/testdata/test/simple_pass_very_nested/tests/subdir/main.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/simple_pass_very_nested/tests/subdir/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/skip_destroy_on_empty/main.tf b/internal/command/testdata/test/skip_destroy_on_empty/main.tf new file mode 100644 index 0000000000..ac048846b7 --- /dev/null +++ b/internal/command/testdata/test/skip_destroy_on_empty/main.tf @@ -0,0 +1,6 @@ + +resource "test_resource" "resource" {} + +output "id" { + value = test_resource.resource.id +} diff --git a/internal/command/testdata/test/skip_destroy_on_empty/main.tftest.hcl b/internal/command/testdata/test/skip_destroy_on_empty/main.tftest.hcl new file mode 100644 index 0000000000..f3ddd4625e --- /dev/null +++ b/internal/command/testdata/test/skip_destroy_on_empty/main.tftest.hcl @@ -0,0 +1,14 @@ + +run "test" {} + +run "verify" { + module { + source = "./verify" + } + + variables { + id = run.test.id + } +} + +run "test_two" {} \ No newline at end of file diff --git a/internal/command/testdata/test/skip_destroy_on_empty/verify/main.tf b/internal/command/testdata/test/skip_destroy_on_empty/verify/main.tf new file mode 100644 index 0000000000..cf49deff83 --- /dev/null +++ b/internal/command/testdata/test/skip_destroy_on_empty/verify/main.tf @@ -0,0 +1,8 @@ + +variable "id" { + type = string +} + +data "test_data_source" "resource" { + id = var.id +} \ No newline at end of file diff --git a/internal/command/testdata/test/state_propagation/example/main.tf b/internal/command/testdata/test/state_propagation/example/main.tf new file mode 100644 index 0000000000..3e6809599c --- /dev/null +++ b/internal/command/testdata/test/state_propagation/example/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string +} + +resource "test_resource" "module_resource" { + id = "df6h8as9" + value = var.input +} diff --git a/internal/command/testdata/test/state_propagation/main.tf b/internal/command/testdata/test/state_propagation/main.tf new file mode 100644 index 0000000000..0cad6cfbbc --- /dev/null +++ b/internal/command/testdata/test/state_propagation/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + id = "598318e0" + value = var.input +} diff --git a/internal/command/testdata/test/state_propagation/main.tftest.hcl b/internal/command/testdata/test/state_propagation/main.tftest.hcl new file mode 100644 index 0000000000..e4fca75561 --- /dev/null +++ b/internal/command/testdata/test/state_propagation/main.tftest.hcl @@ -0,0 +1,54 @@ +# Our test will run this in verbose mode and we should see the plan output for +# the second run block showing the resource being updated as the state should +# be propagated from the first one to the second one. +# +# We also interweave alternate modules to test the handling of multiple states +# within the file. + +run "initial_apply_example" { + module { + source = "./example" + } + + variables { + input = "start" + } +} + +run "initial_apply" { + variables { + input = "start" + } +} + +run "plan_second_example" { + command = plan + + module { + source = "./second_example" + } + + variables { + input = "start" + } +} + +run "plan_update" { + command = plan + + variables { + input = "update" + } +} + +run "plan_update_example" { + command = plan + + module { + source = "./example" + } + + variables { + input = "update" + } +} diff --git a/internal/command/testdata/test/state_propagation/second_example/main.tf b/internal/command/testdata/test/state_propagation/second_example/main.tf new file mode 100644 index 0000000000..f61e0cf2e7 --- /dev/null +++ b/internal/command/testdata/test/state_propagation/second_example/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string +} + +resource "test_resource" "second_module_resource" { + id = "b6a1d8cb" + value = var.input +} diff --git a/internal/command/testdata/test/tfvars_in_test_dir/alternate/main.tftest.hcl b/internal/command/testdata/test/tfvars_in_test_dir/alternate/main.tftest.hcl new file mode 100644 index 0000000000..392d6eb83d --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/alternate/main.tftest.hcl @@ -0,0 +1,13 @@ +run "primary" { + assert { + condition = var.foo == var.test_foo + error_message = "Expected: ${var.test_foo}, Actual: ${var.foo}" + } +} + +run "secondary" { + assert { + condition = var.fooJSON == var.test_foo_json + error_message = "Expected: ${var.test_foo_json}, Actual: ${var.fooJSON}" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/tfvars_in_test_dir/alternate/vars.auto.tfvars b/internal/command/testdata/test/tfvars_in_test_dir/alternate/vars.auto.tfvars new file mode 100644 index 0000000000..ef80b12b91 --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/alternate/vars.auto.tfvars @@ -0,0 +1,3 @@ +foo = "foo_tfvars_value" +test_foo = "foo_tfvars_value" +test_foo_json = "foo_json_tfvars_value" \ No newline at end of file diff --git a/internal/command/testdata/test/tfvars_in_test_dir/alternate/vars.auto.tfvars.json b/internal/command/testdata/test/tfvars_in_test_dir/alternate/vars.auto.tfvars.json new file mode 100644 index 0000000000..eff1f347ff --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/alternate/vars.auto.tfvars.json @@ -0,0 +1,3 @@ +{ + "fooJSON": "foo_json_tfvars_value" +} \ No newline at end of file diff --git a/internal/command/testdata/test/tfvars_in_test_dir/main.tf b/internal/command/testdata/test/tfvars_in_test_dir/main.tf new file mode 100644 index 0000000000..fb298d41ba --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/main.tf @@ -0,0 +1,17 @@ +variable "foo" { + description = "This is test variable" + default = "def_value" +} + +variable "fooJSON" { + description = "This is test variable" + default = "def_value" +} + +output "out_foo" { + value = var.foo +} + +output "out_fooJSON" { + value = var.fooJSON +} diff --git a/internal/command/testdata/test/tfvars_in_test_dir/tests/main.tftest.hcl b/internal/command/testdata/test/tfvars_in_test_dir/tests/main.tftest.hcl new file mode 100644 index 0000000000..392d6eb83d --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/tests/main.tftest.hcl @@ -0,0 +1,13 @@ +run "primary" { + assert { + condition = var.foo == var.test_foo + error_message = "Expected: ${var.test_foo}, Actual: ${var.foo}" + } +} + +run "secondary" { + assert { + condition = var.fooJSON == var.test_foo_json + error_message = "Expected: ${var.test_foo_json}, Actual: ${var.fooJSON}" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/tfvars_in_test_dir/tests/terraform.tfvars b/internal/command/testdata/test/tfvars_in_test_dir/tests/terraform.tfvars new file mode 100644 index 0000000000..ef80b12b91 --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/tests/terraform.tfvars @@ -0,0 +1,3 @@ +foo = "foo_tfvars_value" +test_foo = "foo_tfvars_value" +test_foo_json = "foo_json_tfvars_value" \ No newline at end of file diff --git a/internal/command/testdata/test/tfvars_in_test_dir/tests/terraform.tfvars.json b/internal/command/testdata/test/tfvars_in_test_dir/tests/terraform.tfvars.json new file mode 100644 index 0000000000..eff1f347ff --- /dev/null +++ b/internal/command/testdata/test/tfvars_in_test_dir/tests/terraform.tfvars.json @@ -0,0 +1,3 @@ +{ + "fooJSON": "foo_json_tfvars_value" +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf b/internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf new file mode 100644 index 0000000000..8e891884ea --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-nested-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl b/internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl new file mode 100644 index 0000000000..387693d859 --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-nested-test-files/tests/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf b/internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf new file mode 100644 index 0000000000..8e891884ea --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl b/internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl new file mode 100644 index 0000000000..387693d859 --- /dev/null +++ b/internal/command/testdata/test/top-dir-only-test-files/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/unapplyable-plan/main.tf b/internal/command/testdata/test/unapplyable-plan/main.tf new file mode 100644 index 0000000000..36bc69bcdb --- /dev/null +++ b/internal/command/testdata/test/unapplyable-plan/main.tf @@ -0,0 +1,8 @@ + +resource "test_resource" "example" { + value = "bar" +} + +output "value" { + value = test_resource.example.value +} diff --git a/internal/command/testdata/test/unapplyable-plan/main.tftest.hcl b/internal/command/testdata/test/unapplyable-plan/main.tftest.hcl new file mode 100644 index 0000000000..8e52386ac9 --- /dev/null +++ b/internal/command/testdata/test/unapplyable-plan/main.tftest.hcl @@ -0,0 +1,11 @@ + +run "test" { + command = apply + plan_options { + mode = refresh-only + } + assert { + condition = test_resource.example.value == "bar" + error_message = "wrong value" + } +} diff --git a/internal/command/testdata/test/undefined_variables/main.tf b/internal/command/testdata/test/undefined_variables/main.tf new file mode 100644 index 0000000000..ce7d9e83d8 --- /dev/null +++ b/internal/command/testdata/test/undefined_variables/main.tf @@ -0,0 +1,5 @@ + +variable "input" { + type = string + default = "Hello, world!" +} diff --git a/internal/command/testdata/test/undefined_variables/main.tftest.hcl b/internal/command/testdata/test/undefined_variables/main.tftest.hcl new file mode 100644 index 0000000000..d3009c37bc --- /dev/null +++ b/internal/command/testdata/test/undefined_variables/main.tftest.hcl @@ -0,0 +1,13 @@ + +variables { + # config_free isn't defined in the config, but we'll + # still let users refer to it within the assertions. + config_free = "Hello, world!" +} + +run "applies_defaults" { + assert { + condition = var.input == var.config_free + error_message = "should have applied default value" + } +} diff --git a/internal/command/testdata/test/unknown_value_in_assert/main.tf b/internal/command/testdata/test/unknown_value_in_assert/main.tf new file mode 100644 index 0000000000..f269501272 --- /dev/null +++ b/internal/command/testdata/test/unknown_value_in_assert/main.tf @@ -0,0 +1,14 @@ + +variable "destroy_fail" { + type = bool + default = null + nullable = true +} + +resource "test_resource" "resource" { + destroy_fail = var.destroy_fail +} + +output "destroy_fail" { + value = test_resource.resource.destroy_fail +} diff --git a/internal/command/testdata/test/unknown_value_in_assert/main.tftest.hcl b/internal/command/testdata/test/unknown_value_in_assert/main.tftest.hcl new file mode 100644 index 0000000000..5bf5dcfed2 --- /dev/null +++ b/internal/command/testdata/test/unknown_value_in_assert/main.tftest.hcl @@ -0,0 +1,11 @@ + +run "one" { + command = plan +} + +run "two" { + assert { + condition = output.destroy_fail == run.one.destroy_fail + error_message = "should fail" + } +} diff --git a/internal/command/testdata/test/unknown_value_in_vars/main.tf b/internal/command/testdata/test/unknown_value_in_vars/main.tf new file mode 100644 index 0000000000..f269501272 --- /dev/null +++ b/internal/command/testdata/test/unknown_value_in_vars/main.tf @@ -0,0 +1,14 @@ + +variable "destroy_fail" { + type = bool + default = null + nullable = true +} + +resource "test_resource" "resource" { + destroy_fail = var.destroy_fail +} + +output "destroy_fail" { + value = test_resource.resource.destroy_fail +} diff --git a/internal/command/testdata/test/unknown_value_in_vars/main.tftest.hcl b/internal/command/testdata/test/unknown_value_in_vars/main.tftest.hcl new file mode 100644 index 0000000000..2103aaebe1 --- /dev/null +++ b/internal/command/testdata/test/unknown_value_in_vars/main.tftest.hcl @@ -0,0 +1,10 @@ + +run "one" { + command = plan +} + +run "two" { + variables { + destroy_fail = run.one.destroy_fail + } +} diff --git a/internal/command/testdata/test/variable_references/main.tf b/internal/command/testdata/test/variable_references/main.tf new file mode 100644 index 0000000000..0ada3b9031 --- /dev/null +++ b/internal/command/testdata/test/variable_references/main.tf @@ -0,0 +1,12 @@ + +variable "input_one" { + type = string +} + +variable "input_two" { + type = string +} + +resource "test_resource" "resource" { + value = "${var.input_one} - ${var.input_two}" +} diff --git a/internal/command/testdata/test/variable_references/main.tftest.hcl b/internal/command/testdata/test/variable_references/main.tftest.hcl new file mode 100644 index 0000000000..9607e6adc8 --- /dev/null +++ b/internal/command/testdata/test/variable_references/main.tftest.hcl @@ -0,0 +1,27 @@ +variables { + default = "double" +} + +run "primary" { + variables { + input_one = var.default + input_two = var.default + } + + assert { + condition = test_resource.resource.value == "${var.default} - ${var.input_two}" + error_message = "bad concatenation" + } +} + +run "secondary" { + variables { + input_one = var.default + input_two = var.global # This test requires this passed in as a global var. + } + + assert { + condition = test_resource.resource.value == "double - ${var.global}" + error_message = "bad concatenation" + } +} diff --git a/internal/command/testdata/test/variables/main.tf b/internal/command/testdata/test/variables/main.tf new file mode 100644 index 0000000000..bf4c24b14d --- /dev/null +++ b/internal/command/testdata/test/variables/main.tf @@ -0,0 +1,8 @@ +variable "input" { + type = string + default = "bar" +} + +resource "test_resource" "foo" { + value = var.input +} diff --git a/internal/command/testdata/test/variables/main.tftest.hcl b/internal/command/testdata/test/variables/main.tftest.hcl new file mode 100644 index 0000000000..6feaf3cc5c --- /dev/null +++ b/internal/command/testdata/test/variables/main.tftest.hcl @@ -0,0 +1,6 @@ +run "validate_test_resource" { + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/variables/set_variables.tftest.hcl b/internal/command/testdata/test/variables/set_variables.tftest.hcl new file mode 100644 index 0000000000..7f6a9cee65 --- /dev/null +++ b/internal/command/testdata/test/variables/set_variables.tftest.hcl @@ -0,0 +1,10 @@ +run "validate_test_resource" { + variables { + input = "bar" + } + + assert { + condition = test_resource.foo.value == "bar" + error_message = "invalid value" + } +} diff --git a/internal/command/testdata/test/variables_types/main.tf b/internal/command/testdata/test/variables_types/main.tf new file mode 100644 index 0000000000..11b7370d22 --- /dev/null +++ b/internal/command/testdata/test/variables_types/main.tf @@ -0,0 +1,11 @@ +variable "string_input" { + type = string +} + +variable "number_input" { + type = number +} + +variable "list_input" { + type = list(string) +} diff --git a/internal/command/testdata/test/variables_types/main.tftest.hcl b/internal/command/testdata/test/variables_types/main.tftest.hcl new file mode 100644 index 0000000000..ff6757179c --- /dev/null +++ b/internal/command/testdata/test/variables_types/main.tftest.hcl @@ -0,0 +1,20 @@ +run "variables" { + + # This run block requires the following variables to have been defined as + # command line arguments. + + assert { + condition = var.number_input == 0 + error_message = "bad number value" + } + + assert { + condition = var.string_input == "Hello, world!" + error_message = "bad string value" + } + + assert { + condition = var.list_input == tolist(["Hello", "world"]) + error_message = "bad list value" + } +} diff --git a/internal/command/testdata/test/variables_undefined_in_config/main.tf b/internal/command/testdata/test/variables_undefined_in_config/main.tf new file mode 100644 index 0000000000..2160d8edec --- /dev/null +++ b/internal/command/testdata/test/variables_undefined_in_config/main.tf @@ -0,0 +1,3 @@ +resource "test_resource" "foo" { + value = var.input +} diff --git a/internal/command/testdata/test/variables_undefined_in_config/main.tftest.hcl b/internal/command/testdata/test/variables_undefined_in_config/main.tftest.hcl new file mode 100644 index 0000000000..144f85601a --- /dev/null +++ b/internal/command/testdata/test/variables_undefined_in_config/main.tftest.hcl @@ -0,0 +1,10 @@ +run "test" { + variables { + input = "value" + } + + assert { + condition = test_resource.foo.value == "value" + error_message = "bad value" + } +} diff --git a/internal/command/testdata/test/with_double_interrupt/main.tf b/internal/command/testdata/test/with_double_interrupt/main.tf new file mode 100644 index 0000000000..92846111d7 --- /dev/null +++ b/internal/command/testdata/test/with_double_interrupt/main.tf @@ -0,0 +1,25 @@ + +variable "interrupts" { + type = number +} + +resource "test_resource" "primary" { + value = "primary" +} + +resource "test_resource" "secondary" { + value = "secondary" + interrupt_count = var.interrupts + + depends_on = [ + test_resource.primary + ] +} + +resource "test_resource" "tertiary" { + value = "tertiary" + + depends_on = [ + test_resource.secondary + ] +} diff --git a/internal/command/testdata/test/with_double_interrupt/main.tftest.hcl b/internal/command/testdata/test/with_double_interrupt/main.tftest.hcl new file mode 100644 index 0000000000..4743976709 --- /dev/null +++ b/internal/command/testdata/test/with_double_interrupt/main.tftest.hcl @@ -0,0 +1,17 @@ +variables { + interrupts = 0 +} + +run "primary" { + +} + +run "secondary" { + variables { + interrupts = 2 + } +} + +run "tertiary" { + +} diff --git a/internal/command/testdata/test/with_interrupt/main.tf b/internal/command/testdata/test/with_interrupt/main.tf new file mode 100644 index 0000000000..92846111d7 --- /dev/null +++ b/internal/command/testdata/test/with_interrupt/main.tf @@ -0,0 +1,25 @@ + +variable "interrupts" { + type = number +} + +resource "test_resource" "primary" { + value = "primary" +} + +resource "test_resource" "secondary" { + value = "secondary" + interrupt_count = var.interrupts + + depends_on = [ + test_resource.primary + ] +} + +resource "test_resource" "tertiary" { + value = "tertiary" + + depends_on = [ + test_resource.secondary + ] +} diff --git a/internal/command/testdata/test/with_interrupt/main.tftest.hcl b/internal/command/testdata/test/with_interrupt/main.tftest.hcl new file mode 100644 index 0000000000..e2260f96a7 --- /dev/null +++ b/internal/command/testdata/test/with_interrupt/main.tftest.hcl @@ -0,0 +1,17 @@ +variables { + interrupts = 0 +} + +run "primary" { + +} + +run "secondary" { + variables { + interrupts = 1 + } +} + +run "tertiary" { + +} diff --git a/internal/command/testdata/test/with_interrupt_and_additional_file/main.tf b/internal/command/testdata/test/with_interrupt_and_additional_file/main.tf new file mode 100644 index 0000000000..92846111d7 --- /dev/null +++ b/internal/command/testdata/test/with_interrupt_and_additional_file/main.tf @@ -0,0 +1,25 @@ + +variable "interrupts" { + type = number +} + +resource "test_resource" "primary" { + value = "primary" +} + +resource "test_resource" "secondary" { + value = "secondary" + interrupt_count = var.interrupts + + depends_on = [ + test_resource.primary + ] +} + +resource "test_resource" "tertiary" { + value = "tertiary" + + depends_on = [ + test_resource.secondary + ] +} diff --git a/internal/command/testdata/test/with_interrupt_and_additional_file/main.tftest.hcl b/internal/command/testdata/test/with_interrupt_and_additional_file/main.tftest.hcl new file mode 100644 index 0000000000..e2260f96a7 --- /dev/null +++ b/internal/command/testdata/test/with_interrupt_and_additional_file/main.tftest.hcl @@ -0,0 +1,17 @@ +variables { + interrupts = 0 +} + +run "primary" { + +} + +run "secondary" { + variables { + interrupts = 1 + } +} + +run "tertiary" { + +} diff --git a/internal/command/testdata/test/with_interrupt_and_additional_file/skip_me.tftest.hcl b/internal/command/testdata/test/with_interrupt_and_additional_file/skip_me.tftest.hcl new file mode 100644 index 0000000000..e3f14446b4 --- /dev/null +++ b/internal/command/testdata/test/with_interrupt_and_additional_file/skip_me.tftest.hcl @@ -0,0 +1,9 @@ +variables { + interrupts = 0 +} + +run "primary" {} + +run "secondary" {} + +run "tertiary" {} diff --git a/internal/command/testdata/test/with_nested_setup_modules/main.tf b/internal/command/testdata/test/with_nested_setup_modules/main.tf new file mode 100644 index 0000000000..f304def8b1 --- /dev/null +++ b/internal/command/testdata/test/with_nested_setup_modules/main.tf @@ -0,0 +1,2 @@ + +resource "test_resource" "resource" {} diff --git a/internal/command/testdata/test/with_nested_setup_modules/main.tftest.hcl b/internal/command/testdata/test/with_nested_setup_modules/main.tftest.hcl new file mode 100644 index 0000000000..3172b8c6dc --- /dev/null +++ b/internal/command/testdata/test/with_nested_setup_modules/main.tftest.hcl @@ -0,0 +1,14 @@ +variables { + value = "Hello, world!" +} + +run "load_module" { + module { + source = "./setup" + } + + assert { + condition = output.value == "Hello, world!" + error_message = "invalid value" + } +} \ No newline at end of file diff --git a/internal/command/testdata/test/with_nested_setup_modules/setup/main.tf b/internal/command/testdata/test/with_nested_setup_modules/setup/main.tf new file mode 100644 index 0000000000..54e468b7db --- /dev/null +++ b/internal/command/testdata/test/with_nested_setup_modules/setup/main.tf @@ -0,0 +1,14 @@ + +variable "value" { + type = string +} + +module "child" { + source = "./other" + + value = var.value +} + +output "value" { + value = module.child.value +} diff --git a/internal/command/testdata/test/with_nested_setup_modules/setup/other/main.tf b/internal/command/testdata/test/with_nested_setup_modules/setup/other/main.tf new file mode 100644 index 0000000000..e1f6e52f3f --- /dev/null +++ b/internal/command/testdata/test/with_nested_setup_modules/setup/other/main.tf @@ -0,0 +1,12 @@ + +variable "value" { + type = string +} + +resource "test_resource" "resource" { + value = var.value +} + +output "value" { + value = test_resource.resource.value +} diff --git a/internal/command/testdata/test/with_provider_alias/main.tf b/internal/command/testdata/test/with_provider_alias/main.tf new file mode 100644 index 0000000000..3b60dc01cc --- /dev/null +++ b/internal/command/testdata/test/with_provider_alias/main.tf @@ -0,0 +1,12 @@ + +variable "managed_id" { + type = string +} + +data "test_data_source" "managed_data" { + id = var.managed_id +} + +resource "test_resource" "created" { + value = data.test_data_source.managed_data.value +} diff --git a/internal/command/testdata/test/with_provider_alias/main.tftest.hcl b/internal/command/testdata/test/with_provider_alias/main.tftest.hcl new file mode 100644 index 0000000000..7529a5ef42 --- /dev/null +++ b/internal/command/testdata/test/with_provider_alias/main.tftest.hcl @@ -0,0 +1,37 @@ +provider "test" { + data_prefix = "data" + resource_prefix = "resource" +} + +provider "test" { + alias = "setup" + + # The setup provider will write into the main providers data sources. + resource_prefix = "data" +} + +variables { + managed_id = "B853C121" +} + +run "setup" { + module { + source = "./setup" + } + + variables { + value = "Hello, world!" + id = "B853C121" + } + + providers = { + test = test.setup + } +} + +run "test" { + assert { + condition = test_resource.created.value == "Hello, world!" + error_message = "bad value" + } +} diff --git a/internal/command/testdata/test/with_provider_alias/setup/main.tf b/internal/command/testdata/test/with_provider_alias/setup/main.tf new file mode 100644 index 0000000000..03a9936262 --- /dev/null +++ b/internal/command/testdata/test/with_provider_alias/setup/main.tf @@ -0,0 +1,12 @@ +variable "value" { + type = string +} + +variable "id" { + type = string +} + +resource "test_resource" "managed" { + id = var.id + value = var.value +} diff --git a/internal/command/testdata/test/with_setup_module/main.tf b/internal/command/testdata/test/with_setup_module/main.tf new file mode 100644 index 0000000000..3b60dc01cc --- /dev/null +++ b/internal/command/testdata/test/with_setup_module/main.tf @@ -0,0 +1,12 @@ + +variable "managed_id" { + type = string +} + +data "test_data_source" "managed_data" { + id = var.managed_id +} + +resource "test_resource" "created" { + value = data.test_data_source.managed_data.value +} diff --git a/internal/command/testdata/test/with_setup_module/main.tftest.hcl b/internal/command/testdata/test/with_setup_module/main.tftest.hcl new file mode 100644 index 0000000000..1f2a6a94aa --- /dev/null +++ b/internal/command/testdata/test/with_setup_module/main.tftest.hcl @@ -0,0 +1,21 @@ +variables { + managed_id = "B853C121" +} + +run "setup" { + module { + source = "./setup" + } + + variables { + value = "Hello, world!" + id = "B853C121" + } +} + +run "test" { + assert { + condition = test_resource.created.value == "Hello, world!" + error_message = "bad value" + } +} diff --git a/internal/command/testdata/test/with_setup_module/setup/main.tf b/internal/command/testdata/test/with_setup_module/setup/main.tf new file mode 100644 index 0000000000..49056bbea7 --- /dev/null +++ b/internal/command/testdata/test/with_setup_module/setup/main.tf @@ -0,0 +1,13 @@ +variable "value" { + type = string +} + +variable "id" { + type = string +} + +resource "test_resource" "managed" { + provider = setup + id = var.id + value = var.value +} diff --git a/internal/command/testdata/test/with_state_key/breaking_change.tftest.hcl b/internal/command/testdata/test/with_state_key/breaking_change.tftest.hcl new file mode 100644 index 0000000000..6457888261 --- /dev/null +++ b/internal/command/testdata/test/with_state_key/breaking_change.tftest.hcl @@ -0,0 +1,14 @@ +run "old_version" { + state_key = "test1" +} + +run "new_code" { + state_key = "test1" + module { + source = "./breaking_change" + } + assert { + condition = test_resource.renamed_without_move.id == run.old_version.test_id + error_message = "resource renamed without moved block" + } +} diff --git a/internal/command/testdata/test/with_state_key/breaking_change/main.tf b/internal/command/testdata/test/with_state_key/breaking_change/main.tf new file mode 100644 index 0000000000..9cc88f8ca9 --- /dev/null +++ b/internal/command/testdata/test/with_state_key/breaking_change/main.tf @@ -0,0 +1,7 @@ +resource "test_resource" "renamed_without_move" { + value = "test" +} + +output "test_id" { + value = test_resource.renamed_without_move.id +} diff --git a/internal/command/testdata/test/with_state_key/main.tf b/internal/command/testdata/test/with_state_key/main.tf new file mode 100644 index 0000000000..828cab6ebf --- /dev/null +++ b/internal/command/testdata/test/with_state_key/main.tf @@ -0,0 +1,11 @@ +resource "test_resource" "test_id_moved" { +} + +output "test_id" { + value = test_resource.test_id_moved.id +} + +moved { + from = test_resource.test_id + to = test_resource.test_id_moved +} diff --git a/internal/command/testdata/test/with_state_key/moved.tftest.hcl b/internal/command/testdata/test/with_state_key/moved.tftest.hcl new file mode 100644 index 0000000000..a8749c3b6a --- /dev/null +++ b/internal/command/testdata/test/with_state_key/moved.tftest.hcl @@ -0,0 +1,14 @@ +run "old_version" { + state_key = "test1" + module { + source = "./old_version" + } +} + +run "new_code" { + state_key = "test1" + assert { + condition = test_resource.test_id_moved.id == run.old_version.test_id + error_message = "ressource_id differed" + } +} diff --git a/internal/command/testdata/test/with_state_key/old_version/main.tf b/internal/command/testdata/test/with_state_key/old_version/main.tf new file mode 100644 index 0000000000..5591c77444 --- /dev/null +++ b/internal/command/testdata/test/with_state_key/old_version/main.tf @@ -0,0 +1,7 @@ +resource "test_resource" "test_id" { + value = "test" +} + +output "test_id" { + value = test_resource.test_id.id +} diff --git a/internal/command/testdata/test/write-only-attributes-mocked/main.tf b/internal/command/testdata/test/write-only-attributes-mocked/main.tf new file mode 100644 index 0000000000..3f80d47a4a --- /dev/null +++ b/internal/command/testdata/test/write-only-attributes-mocked/main.tf @@ -0,0 +1,14 @@ + +variable "input" { + type = string +} + +data "test_data_source" "datasource" { + id = "resource" + write_only = var.input +} + +resource "test_resource" "resource" { + value = data.test_data_source.datasource.value + write_only = var.input +} diff --git a/internal/command/testdata/test/write-only-attributes-mocked/main.tftest.hcl b/internal/command/testdata/test/write-only-attributes-mocked/main.tftest.hcl new file mode 100644 index 0000000000..5efe8eabc1 --- /dev/null +++ b/internal/command/testdata/test/write-only-attributes-mocked/main.tftest.hcl @@ -0,0 +1,36 @@ + +mock_provider "test" { + mock_resource "test_resource" { + defaults = { + id = "resource" + } + } + + mock_data "test_data_source" { + defaults = { + value = "hello" + } + } +} + +run "test" { + variables { + input = "input" + } + + assert { + condition = data.test_data_source.datasource.value == "hello" + error_message = "wrong value" + } + + assert { + condition = test_resource.resource.value == "hello" + error_message = "wrong value" + } + + assert { + condition = test_resource.resource.id == "resource" + error_message = "wrong value" + } + +} diff --git a/internal/command/testdata/test/write-only-attributes-overridden/main.tf b/internal/command/testdata/test/write-only-attributes-overridden/main.tf new file mode 100644 index 0000000000..3f80d47a4a --- /dev/null +++ b/internal/command/testdata/test/write-only-attributes-overridden/main.tf @@ -0,0 +1,14 @@ + +variable "input" { + type = string +} + +data "test_data_source" "datasource" { + id = "resource" + write_only = var.input +} + +resource "test_resource" "resource" { + value = data.test_data_source.datasource.value + write_only = var.input +} diff --git a/internal/command/testdata/test/write-only-attributes-overridden/main.tftest.hcl b/internal/command/testdata/test/write-only-attributes-overridden/main.tftest.hcl new file mode 100644 index 0000000000..203f5fb7ef --- /dev/null +++ b/internal/command/testdata/test/write-only-attributes-overridden/main.tftest.hcl @@ -0,0 +1,38 @@ + +provider "test" {} + +override_resource { + target = test_resource.resource + values = { + id = "resource" + } +} + +override_data { + target = data.test_data_source.datasource + values = { + value = "hello" + } +} + +run "test" { + variables { + input = "input" + } + + assert { + condition = data.test_data_source.datasource.value == "hello" + error_message = "wrong value" + } + + assert { + condition = test_resource.resource.value == "hello" + error_message = "wrong value" + } + + assert { + condition = test_resource.resource.id == "resource" + error_message = "wrong value" + } + +} diff --git a/internal/command/testdata/test/write-only-attributes/main.tf b/internal/command/testdata/test/write-only-attributes/main.tf new file mode 100644 index 0000000000..82b012575a --- /dev/null +++ b/internal/command/testdata/test/write-only-attributes/main.tf @@ -0,0 +1,9 @@ + +variable "input" { + type = string +} + +resource "test_resource" "resource" { + id = "resource" + write_only = var.input +} diff --git a/internal/command/testdata/test/write-only-attributes/main.tftest.hcl b/internal/command/testdata/test/write-only-attributes/main.tftest.hcl new file mode 100644 index 0000000000..15764bae83 --- /dev/null +++ b/internal/command/testdata/test/write-only-attributes/main.tftest.hcl @@ -0,0 +1,8 @@ + +provider "test" {} + +run "test" { + variables { + input = "input" + } +} diff --git a/internal/command/testdata/tfmock-fmt/data_in.tfmock.hcl b/internal/command/testdata/tfmock-fmt/data_in.tfmock.hcl new file mode 100644 index 0000000000..3a41fbbfdc --- /dev/null +++ b/internal/command/testdata/tfmock-fmt/data_in.tfmock.hcl @@ -0,0 +1,22 @@ +mock_data "aws_availability_zones" { + defaults = { + names = [ +"us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ] + } +} + +override_data { +target = data.aws_subnets.private_subnets + values = { + ids = ["subnet-a", + "subnet-b", + "subnet-c" + ] + } +} diff --git a/internal/command/testdata/tfmock-fmt/data_out.tfmock.hcl b/internal/command/testdata/tfmock-fmt/data_out.tfmock.hcl new file mode 100644 index 0000000000..0727268d29 --- /dev/null +++ b/internal/command/testdata/tfmock-fmt/data_out.tfmock.hcl @@ -0,0 +1,22 @@ +mock_data "aws_availability_zones" { + defaults = { + names = [ + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f" + ] + } +} + +override_data { + target = data.aws_subnets.private_subnets + values = { + ids = ["subnet-a", + "subnet-b", + "subnet-c" + ] + } +} diff --git a/internal/command/testdata/tfmock-fmt/resource_in.tfmock.hcl b/internal/command/testdata/tfmock-fmt/resource_in.tfmock.hcl new file mode 100644 index 0000000000..3cf0ab68c0 --- /dev/null +++ b/internal/command/testdata/tfmock-fmt/resource_in.tfmock.hcl @@ -0,0 +1,9 @@ +mock_resource "aws_s3_bucket" { + defaults = { + arn = "arn:aws:s3:::name"} +} + +override_resource { + target = aws_launch_template.vm + values = {id = "lt-xyz"} +} diff --git a/internal/command/testdata/tfmock-fmt/resource_out.tfmock.hcl b/internal/command/testdata/tfmock-fmt/resource_out.tfmock.hcl new file mode 100644 index 0000000000..2dcf61f1c1 --- /dev/null +++ b/internal/command/testdata/tfmock-fmt/resource_out.tfmock.hcl @@ -0,0 +1,9 @@ +mock_resource "aws_s3_bucket" { + defaults = { + arn = "arn:aws:s3:::name" } +} + +override_resource { + target = aws_launch_template.vm + values = { id = "lt-xyz" } +} diff --git a/internal/command/testdata/tftest-fmt/main_in.tftest.hcl b/internal/command/testdata/tftest-fmt/main_in.tftest.hcl new file mode 100644 index 0000000000..c8c5fb17c7 --- /dev/null +++ b/internal/command/testdata/tftest-fmt/main_in.tftest.hcl @@ -0,0 +1,18 @@ + +variables { + first = "value" +second = "value" +} + +run "some_run_block" { + command=plan + + plan_options={ + refresh=false + } + + assert { + condition = var.input == 12 + error_message = "something" + } +} diff --git a/internal/command/testdata/tftest-fmt/main_out.tftest.hcl b/internal/command/testdata/tftest-fmt/main_out.tftest.hcl new file mode 100644 index 0000000000..c6de0ddd73 --- /dev/null +++ b/internal/command/testdata/tftest-fmt/main_out.tftest.hcl @@ -0,0 +1,18 @@ + +variables { + first = "value" + second = "value" +} + +run "some_run_block" { + command = plan + + plan_options = { + refresh = false + } + + assert { + condition = var.input == 12 + error_message = "something" + } +} diff --git a/internal/command/testdata/validate-invalid/duplicate_import_targets/main.tf b/internal/command/testdata/validate-invalid/duplicate_import_targets/main.tf new file mode 100644 index 0000000000..3c663bd105 --- /dev/null +++ b/internal/command/testdata/validate-invalid/duplicate_import_targets/main.tf @@ -0,0 +1,12 @@ +resource "aws_instance" "web" { +} + +import { + to = aws_instance.web + id = "test" +} + +import { + to = aws_instance.web + id = "test2" +} diff --git a/internal/command/testdata/validate-invalid/duplicate_import_targets/output.json b/internal/command/testdata/validate-invalid/duplicate_import_targets/output.json new file mode 100644 index 0000000000..091b5d8274 --- /dev/null +++ b/internal/command/testdata/validate-invalid/duplicate_import_targets/output.json @@ -0,0 +1,34 @@ +{ + "format_version": "1.0", + "valid": false, + "error_count": 1, + "warning_count": 0, + "diagnostics": [ + { + "severity": "error", + "summary": "Duplicate import configuration for \"aws_instance.web\"", + "detail": "An import block for the resource \"aws_instance.web\" was already declared at testdata/validate-invalid/duplicate_import_targets/main.tf:4,1-7. A resource can have only one import block.", + "range": { + "filename": "testdata/validate-invalid/duplicate_import_targets/main.tf", + "start": { + "line": 10, + "column": 8, + "byte": 101 + }, + "end": { + "line": 10, + "column": 24, + "byte": 117 + } + }, + "snippet": { + "context": "import", + "code": " to = aws_instance.web", + "start_line": 10, + "highlight_start_offset": 7, + "highlight_end_offset": 23, + "values": [] + } + } + ] +} diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go new file mode 100644 index 0000000000..6d83407cb6 --- /dev/null +++ b/internal/command/testing/test_provider.go @@ -0,0 +1,433 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package testing + +import ( + "fmt" + "path" + "strings" + "sync" + "time" + + "github.com/hashicorp/go-uuid" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + ProviderSchema = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "data_prefix": {Type: cty.String, Optional: true}, + "resource_prefix": {Type: cty.String, Optional: true}, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "interrupt_count": {Type: cty.Number, Optional: true}, + "destroy_fail": {Type: cty.Bool, Optional: true, Computed: true}, + "create_wait_seconds": {Type: cty.Number, Optional: true}, + "destroy_wait_seconds": {Type: cty.Number, Optional: true}, + "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_data_source": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "value": {Type: cty.String, Computed: true}, + "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + + // We never actually reference these values from a data + // source, but we have tests that use the same cty.Value + // to represent a test_resource and a test_data_source + // so the schemas have to match. + + "interrupt_count": {Type: cty.Number, Computed: true}, + "destroy_fail": {Type: cty.Bool, Computed: true}, + "create_wait_seconds": {Type: cty.Number, Computed: true}, + "destroy_wait_seconds": {Type: cty.Number, Computed: true}, + }, + }, + }, + }, + EphemeralResourceTypes: map[string]providers.Schema{ + "test_ephemeral_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + Functions: map[string]providers.FunctionDecl{ + "is_true": { + Parameters: []providers.FunctionParam{ + { + Name: "input", + Type: cty.Bool, + AllowNullValue: false, + AllowUnknownValues: false, + }, + }, + ReturnType: cty.Bool, + }, + }, + } +) + +// TestProvider is a wrapper around terraform.MockProvider that defines dynamic +// schemas, and keeps track of the resources and data sources that it contains. +type TestProvider struct { + Provider *testing.MockProvider + + data, resource cty.Value + + Interrupt chan<- struct{} + + Store *ResourceStore +} + +// NewProvider creates a new TestProvider for use in tests. +// +// If you provide an empty or nil *ResourceStore argument this is equivalent to the provider +// not having provisioned any remote objects prior to the test's events. +// +// If you provide a *ResourceStore containing values, those cty.Values represent remote objects +// that the provider has 'already' provisioned and can return information about immediately in a test. +func NewProvider(store *ResourceStore) *TestProvider { + if store == nil { + store = &ResourceStore{ + Data: make(map[string]cty.Value), + } + } + + provider := &TestProvider{ + Provider: new(testing.MockProvider), + Store: store, + } + + provider.Provider.GetProviderSchemaResponse = ProviderSchema + provider.Provider.ConfigureProviderFn = provider.ConfigureProvider + provider.Provider.PlanResourceChangeFn = provider.PlanResourceChange + provider.Provider.ApplyResourceChangeFn = provider.ApplyResourceChange + provider.Provider.ReadResourceFn = provider.ReadResource + provider.Provider.ReadDataSourceFn = provider.ReadDataSource + provider.Provider.CallFunctionFn = provider.CallFunction + provider.Provider.OpenEphemeralResourceFn = provider.OpenEphemeralResource + provider.Provider.CloseEphemeralResourceFn = provider.CloseEphemeralResource + + return provider +} + +func (provider *TestProvider) DataPrefix() string { + var prefix string + if !provider.data.IsNull() && provider.data.IsKnown() { + prefix = provider.data.AsString() + } + return prefix +} + +func (provider *TestProvider) SetDataPrefix(prefix string) { + provider.data = cty.StringVal(prefix) +} + +func (provider *TestProvider) GetDataKey(id string) string { + if !provider.data.IsNull() && provider.data.IsKnown() { + return path.Join(provider.data.AsString(), id) + } + return id +} + +func (provider *TestProvider) ResourcePrefix() string { + var prefix string + if !provider.resource.IsNull() && provider.resource.IsKnown() { + prefix = provider.resource.AsString() + } + return prefix +} + +func (provider *TestProvider) SetResourcePrefix(prefix string) { + provider.resource = cty.StringVal(prefix) +} + +func (provider *TestProvider) GetResourceKey(id string) string { + if !provider.resource.IsNull() && provider.resource.IsKnown() { + return path.Join(provider.resource.AsString(), id) + } + return id +} + +func (provider *TestProvider) ResourceString() string { + return provider.string(provider.ResourcePrefix()) +} + +func (provider *TestProvider) ResourceCount() int { + return provider.count(provider.ResourcePrefix()) +} + +func (provider *TestProvider) DataSourceString() string { + return provider.string(provider.DataPrefix()) +} + +func (provider *TestProvider) DataSourceCount() int { + return provider.count(provider.DataPrefix()) +} + +func (provider *TestProvider) count(prefix string) int { + provider.Store.mutex.RLock() + defer provider.Store.mutex.RUnlock() + + if len(prefix) == 0 { + return len(provider.Store.Data) + } + + count := 0 + for key := range provider.Store.Data { + if strings.HasPrefix(key, prefix) { + count++ + } + } + return count +} + +func (provider *TestProvider) string(prefix string) string { + provider.Store.mutex.RLock() + defer provider.Store.mutex.RUnlock() + + var keys []string + for key := range provider.Store.Data { + if strings.HasPrefix(key, prefix) { + keys = append(keys, key) + } + } + return strings.Join(keys, ", ") +} + +func (provider *TestProvider) ConfigureProvider(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + provider.resource = request.Config.GetAttr("resource_prefix") + provider.data = request.Config.GetAttr("data_prefix") + return providers.ConfigureProviderResponse{} +} + +func (provider *TestProvider) PlanResourceChange(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + if request.ProposedNewState.IsNull() { + // Then this is a delete operation. + return providers.PlanResourceChangeResponse{ + PlannedState: request.ProposedNewState, + } + } + + resource := request.ProposedNewState + if id := resource.GetAttr("id"); !id.IsKnown() || id.IsNull() { + vals := resource.AsValueMap() + vals["id"] = cty.UnknownVal(cty.String) + resource = cty.ObjectVal(vals) + } + + if destroyFail := resource.GetAttr("destroy_fail"); !destroyFail.IsKnown() || destroyFail.IsNull() { + vals := resource.AsValueMap() + vals["destroy_fail"] = cty.UnknownVal(cty.Bool) + resource = cty.ObjectVal(vals) + } + + if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() { + vals := resource.AsValueMap() + vals["write_only"] = cty.NullVal(cty.String) + resource = cty.ObjectVal(vals) + } + + return providers.PlanResourceChangeResponse{ + PlannedState: resource, + } +} + +func (provider *TestProvider) ApplyResourceChange(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + if request.PlannedState.IsNull() { + // Then this is a delete operation. + + if destroyFail := request.PriorState.GetAttr("destroy_fail"); destroyFail.IsKnown() && destroyFail.True() { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to destroy resource", "destroy_fail is set to true")) + return providers.ApplyResourceChangeResponse{ + Diagnostics: diags, + } + } + + if wait := request.PriorState.GetAttr("destroy_wait_seconds"); !wait.IsNull() && wait.IsKnown() { + waitTime, _ := wait.AsBigFloat().Int64() + time.Sleep(time.Second * time.Duration(waitTime)) + } + + provider.Store.Delete(provider.GetResourceKey(request.PriorState.GetAttr("id").AsString())) + return providers.ApplyResourceChangeResponse{ + NewState: request.PlannedState, + } + } + + resource := request.PlannedState + id := resource.GetAttr("id") + if !id.IsKnown() { + val, err := uuid.GenerateUUID() + if err != nil { + panic(fmt.Errorf("failed to generate uuid: %v", err)) + } + + id = cty.StringVal(val) + + vals := resource.AsValueMap() + vals["id"] = id + resource = cty.ObjectVal(vals) + } + + if interrupts := resource.GetAttr("interrupt_count"); !interrupts.IsNull() && interrupts.IsKnown() && provider.Interrupt != nil { + count, _ := interrupts.AsBigFloat().Int64() + for ix := 0; ix < int(count); ix++ { + provider.Interrupt <- struct{}{} + } + + // Wait for a second to make sure the interrupts are processed by + // Terraform before the provider finishes. This is an attempt to ensure + // the output of any tests that rely on this behaviour is deterministic. + time.Sleep(time.Second) + } + + if wait := resource.GetAttr("create_wait_seconds"); !wait.IsNull() && wait.IsKnown() { + waitTime, _ := wait.AsBigFloat().Int64() + time.Sleep(time.Second * time.Duration(waitTime)) + } + + if destroyFail := resource.GetAttr("destroy_fail"); !destroyFail.IsKnown() { + vals := resource.AsValueMap() + vals["destroy_fail"] = cty.False + resource = cty.ObjectVal(vals) + } + + provider.Store.Put(provider.GetResourceKey(id.AsString()), resource) + return providers.ApplyResourceChangeResponse{ + NewState: resource, + } +} + +func (provider *TestProvider) ReadResource(request providers.ReadResourceRequest) providers.ReadResourceResponse { + var diags tfdiags.Diagnostics + + id := request.PriorState.GetAttr("id").AsString() + resource := provider.Store.Get(provider.GetResourceKey(id)) + if resource == cty.NilVal { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id))) + } + + return providers.ReadResourceResponse{ + NewState: resource, + Diagnostics: diags, + } +} + +func (provider *TestProvider) ReadDataSource(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + var diags tfdiags.Diagnostics + + id := request.Config.GetAttr("id").AsString() + resource := provider.Store.Get(provider.GetDataKey(id)) + if resource == cty.NilVal { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id))) + } + + if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() { + vals := resource.AsValueMap() + vals["write_only"] = cty.NullVal(cty.String) + resource = cty.ObjectVal(vals) + } + + return providers.ReadDataSourceResponse{ + State: resource, + Diagnostics: diags, + } +} + +func (provider *TestProvider) CallFunction(request providers.CallFunctionRequest) providers.CallFunctionResponse { + switch request.FunctionName { + case "is_true": + return providers.CallFunctionResponse{ + Result: request.Arguments[0], + } + default: + return providers.CallFunctionResponse{ + Err: fmt.Errorf("unknown function %q", request.FunctionName), + } + } +} + +func (provider *TestProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + resp.Result = cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("bar"), + }) + return resp +} + +func (provider *TestProvider) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { + return resp +} + +// ResourceStore manages a set of cty.Value resources that can be shared between +// TestProvider providers. +// +// A ResourceStore represents the remote objects that a test provider is managing. +// For example, when the test provider gets a ReadResource request it will search +// the store for a resource with a matching ID. See (*TestProvider).ReadResource. +type ResourceStore struct { + mutex sync.RWMutex + + Data map[string]cty.Value +} + +func (store *ResourceStore) Delete(key string) cty.Value { + store.mutex.Lock() + defer store.mutex.Unlock() + + if resource, ok := store.Data[key]; ok { + delete(store.Data, key) + return resource + } + return cty.NilVal +} + +func (store *ResourceStore) Get(key string) cty.Value { + store.mutex.RLock() + defer store.mutex.RUnlock() + + return store.get(key) +} + +func (store *ResourceStore) Put(key string, resource cty.Value) cty.Value { + store.mutex.Lock() + defer store.mutex.Unlock() + + old := store.get(key) + store.Data[key] = resource + return old +} + +func (store *ResourceStore) get(key string) cty.Value { + if resource, ok := store.Data[key]; ok { + return resource + } + return cty.NilVal +} diff --git a/internal/command/ui_input.go b/internal/command/ui_input.go index 071982dec2..89cce0701a 100644 --- a/internal/command/ui_input.go +++ b/internal/command/ui_input.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/ui_input_test.go b/internal/command/ui_input_test.go index d08cb0a245..92a2975b77 100644 --- a/internal/command/ui_input_test.go +++ b/internal/command/ui_input_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/unlock.go b/internal/command/unlock.go index 1b09b28863..86a6481aa2 100644 --- a/internal/command/unlock.go +++ b/internal/command/unlock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -7,9 +10,9 @@ import ( "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" ) // UnlockCommand is a cli.Command implementation that manually unlocks @@ -65,6 +68,9 @@ func (c *UnlockCommand) Run(args []string) int { return 1 } + // This is a read-only operation with respect to the state contents + c.ignoreRemoteVersionConflict(b) + env, err := c.Workspace() if err != nil { c.Ui.Error(fmt.Sprintf("Error selecting workspace: %s", err)) @@ -88,7 +94,7 @@ func (c *UnlockCommand) Run(args []string) int { desc := "Terraform will remove the lock on the remote state.\n" + "This will allow local Terraform commands to modify this state, even though it\n" + - "may be still be in use. Only 'yes' will be accepted to confirm." + "may still be in use. Only 'yes' will be accepted to confirm." v, err := c.UIInput().Input(context.Background(), &terraform.InputOpts{ Id: "force-unlock", diff --git a/internal/command/unlock_test.go b/internal/command/unlock_test.go index 9822b38426..52c77480b4 100644 --- a/internal/command/unlock_test.go +++ b/internal/command/unlock_test.go @@ -1,13 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "os" "testing" - "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" + "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" + "github.com/hashicorp/terraform/internal/command/workdir" ) // Since we can't unlock a local state file, just test that calling unlock @@ -20,12 +23,12 @@ func TestUnlock(t *testing.T) { // Write the legacy state statePath := DefaultStateFilename { - f, err := os.Create(statePath) + emptyStateFile := workdir.NewBackendStateFile() + emptyStateFileRaw, err := workdir.EncodeBackendStateFile(emptyStateFile) if err != nil { - t.Fatalf("err: %s", err) + t.Fatal(err) } - err = legacy.WriteState(legacy.NewState(), f) - f.Close() + err = os.WriteFile(statePath, emptyStateFileRaw, os.ModePerm) if err != nil { t.Fatalf("err: %s", err) } diff --git a/internal/command/untaint.go b/internal/command/untaint.go index ba290a8a47..2e139bc983 100644 --- a/internal/command/untaint.go +++ b/internal/command/untaint.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,6 +12,7 @@ import ( "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -163,6 +167,15 @@ func (c *UntaintCommand) Run(args []string) int { c.showDiagnostics(diags) return 1 } + + // Get schemas, if possible, before writing state + var schemas *terraform.Schemas + if isCloudMode(b) { + var schemaDiags tfdiags.Diagnostics + schemas, schemaDiags = c.MaybeGetSchemas(state, nil) + diags = diags.Append(schemaDiags) + } + obj.Status = states.ObjectReady ss.SetResourceInstanceCurrent(addr, obj, rs.ProviderConfig) @@ -170,11 +183,12 @@ func (c *UntaintCommand) Run(args []string) int { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } - if err := stateMgr.PersistState(); err != nil { + if err := stateMgr.PersistState(schemas); err != nil { c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) return 1 } + c.showDiagnostics(diags) c.Ui.Output(fmt.Sprintf("Resource instance %s has been successfully untainted.", addr)) return 0 } diff --git a/internal/command/untaint_test.go b/internal/command/untaint_test.go index cc193125e4..ae008287d5 100644 --- a/internal/command/untaint_test.go +++ b/internal/command/untaint_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,9 +9,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" - "github.com/mitchellh/cli" ) func TestUntaint(t *testing.T) { diff --git a/internal/command/validate.go b/internal/command/validate.go index 110fcec8c3..a574f00c0f 100644 --- a/internal/command/validate.go +++ b/internal/command/validate.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,8 +8,10 @@ import ( "path/filepath" "strings" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -49,7 +54,7 @@ func (c *ValidateCommand) Run(rawArgs []string) int { return view.Results(diags) } - validateDiags := c.validate(dir) + validateDiags := c.validate(dir, args.TestDirectory, args.NoTests) diags = diags.Append(validateDiags) // Validating with dev overrides in effect means that the result might @@ -61,30 +66,84 @@ func (c *ValidateCommand) Run(rawArgs []string) int { return view.Results(diags) } -func (c *ValidateCommand) validate(dir string) tfdiags.Diagnostics { +func (c *ValidateCommand) validate(dir, testDir string, noTests bool) tfdiags.Diagnostics { var diags tfdiags.Diagnostics + var cfg *configs.Config - cfg, cfgDiags := c.loadConfig(dir) - diags = diags.Append(cfgDiags) - + if noTests { + cfg, diags = c.loadConfig(dir) + } else { + cfg, diags = c.loadConfigWithTests(dir, testDir) + } if diags.HasErrors() { return diags } - opts, err := c.contextOpts() - if err != nil { - diags = diags.Append(err) + validate := func(cfg *configs.Config) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + opts, err := c.contextOpts() + if err != nil { + diags = diags.Append(err) + return diags + } + + tfCtx, ctxDiags := terraform.NewContext(opts) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + return diags + } + + return diags.Append(tfCtx.Validate(cfg, nil)) + } + + diags = diags.Append(validate(cfg)) + + if noTests { return diags } - tfCtx, ctxDiags := terraform.NewContext(opts) - diags = diags.Append(ctxDiags) - if ctxDiags.HasErrors() { - return diags + validatedModules := make(map[string]bool) + + // We'll also do a quick validation of the Terraform test files. These live + // outside the Terraform graph so we have to do this separately. + for _, file := range cfg.Module.Tests { + + // The file validation only returns warnings so we'll just add them + // without checking anything about them. + diags = diags.Append(file.Validate(cfg)) + + for _, run := range file.Runs { + + if run.Module != nil { + // Then we can also validate the referenced modules, but we are + // only going to do this is if they are local modules. + // + // Basically, local testing modules are something the user can + // reasonably go and fix. If it's a module being downloaded from + // the registry, the expectation is that the author of the + // module should have ran `terraform validate` themselves. + if _, ok := run.Module.Source.(addrs.ModuleSourceLocal); ok { + + if validated := validatedModules[run.Module.Source.String()]; !validated { + + // Since we can reference the same module twice, let's + // not validate the same thing multiple times. + + validatedModules[run.Module.Source.String()] = true + diags = diags.Append(validate(run.ConfigUnderTest)) + } + + } + + diags = diags.Append(run.Validate(run.ConfigUnderTest)) + } else { + diags = diags.Append(run.Validate(cfg)) + } + + } } - validateDiags := tfCtx.Validate(cfg) - diags = diags.Append(validateDiags) return diags } @@ -120,11 +179,15 @@ Usage: terraform [global options] validate [options] Options: - -json Produce output in a machine-readable JSON format, suitable for - use in text editor integrations and other automated systems. - Always disables color. + -json Produce output in a machine-readable JSON format, + suitable for use in text editor integrations and other + automated systems. Always disables color. - -no-color If specified, output won't contain any color. + -no-color If specified, output won't contain any color. + + -no-tests If specified, Terraform will not validate test files. + + -test-directory=path Set the Terraform test directory, defaults to "tests". ` return strings.TrimSpace(helpText) } diff --git a/internal/command/validate_test.go b/internal/command/validate_test.go index 969e79683f..be379ed08d 100644 --- a/internal/command/validate_test.go +++ b/internal/command/validate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -9,8 +12,11 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" "github.com/zclconf/go-cty/cty" + testing_command "github.com/hashicorp/terraform/internal/command/testing" + "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/terminal" @@ -22,7 +28,7 @@ func setupTest(t *testing.T, fixturepath string, args ...string) (*terminal.Test p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "ami": {Type: cty.String, Optional: true}, }, @@ -147,6 +153,17 @@ func TestSameResourceMultipleTimesShouldFail(t *testing.T) { } } +func TestSameImportTargetMultipleTimesShouldFail(t *testing.T) { + output, code := setupTest(t, "validate-invalid/duplicate_import_targets") + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) + } + wantError := `Error: Duplicate import configuration for "aws_instance.web"` + if !strings.Contains(output.Stderr(), wantError) { + t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) + } +} + func TestOutputWithoutValueShouldFail(t *testing.T) { output, code := setupTest(t, "validate-invalid/outputs") if code != 1 { @@ -203,6 +220,173 @@ func TestMissingDefinedVar(t *testing.T) { } } +func TestValidateWithInvalidTestFile(t *testing.T) { + + // We're reusing some testing configs that were written for testing the + // test command here, so we have to initalise things slightly differently + // to the other tests. + + view, done := testView(t) + provider := testing_command.NewProvider(nil) + c := &ValidateCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + View: view, + }, + } + + var args []string + args = append(args, "-no-color") + args = append(args, testFixturePath("test/invalid")) + + code := c.Run(args) + output := done(t) + + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) + } + + wantError := "Error: Invalid `expect_failures` reference" + if !strings.Contains(output.Stderr(), wantError) { + t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) + } +} + +func TestValidateWithInvalidTestModule(t *testing.T) { + + // We're reusing some testing configs that were written for testing the + // test command here, so we have to initalise things slightly differently + // to the other tests. + + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "invalid-module")), td) + defer testChdir(t, td)() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + t.Fatalf("expected status code 0 but got %d: %s", code, ui.ErrorWriter) + } + + c := &ValidateCommand{ + Meta: meta, + } + + var args []string + args = append(args, "-no-color") + + code := c.Run(args) + output := done(t) + + if code != 1 { + t.Fatalf("Should have failed: %d\n\n%s", code, output.Stderr()) + } + + wantError := "Error: Reference to undeclared input variable" + if !strings.Contains(output.Stderr(), wantError) { + t.Fatalf("Missing error string %q\n\n'%s'", wantError, output.Stderr()) + } +} + +func TestValidateWithInvalidOverrides(t *testing.T) { + + // We're reusing some testing configs that were written for testing the + // test command here, so we have to initalise things slightly differently + // to the other tests. + + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "invalid-overrides")), td) + defer testChdir(t, td)() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + provider := testing_command.NewProvider(nil) + + providerSource, close := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer close() + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + output := done(t) + if code := init.Run(nil); code != 0 { + t.Fatalf("expected status code 0 but got %d: %s", code, output.All()) + } + + // reset streams for next command + streams, done = terminal.StreamsForTesting(t) + meta.View = views.NewView(streams) + meta.Streams = streams + + c := &ValidateCommand{ + Meta: meta, + } + + var args []string + args = append(args, "-no-color") + + code := c.Run(args) + output = done(t) + + if code != 0 { + t.Errorf("Should have passed: %d\n\n%s", code, output.Stderr()) + } + + actual := output.All() + expected := ` +Warning: Invalid override target + + on main.tftest.hcl line 4, in mock_provider "test": + 4: target = test_resource.absent_one + +The override target test_resource.absent_one does not exist within the +configuration under test. This could indicate a typo in the target address or +an unnecessary override. + +(and 5 more similar warnings elsewhere) +Success! The configuration is valid, but there were some validation warnings +as shown above. + +` + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } +} + func TestValidate_json(t *testing.T) { tests := []struct { path string @@ -215,6 +399,7 @@ func TestValidate_json(t *testing.T) { {"validate-invalid/multiple_providers", false}, {"validate-invalid/multiple_modules", false}, {"validate-invalid/multiple_resources", false}, + {"validate-invalid/duplicate_import_targets", false}, {"validate-invalid/outputs", false}, {"validate-invalid/incorrectmodulename", false}, {"validate-invalid/interpolation", false}, diff --git a/internal/command/version.go b/internal/command/version.go index 7fef59202d..418febf991 100644 --- a/internal/command/version.go +++ b/internal/command/version.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -158,7 +161,7 @@ func (c *VersionCommand) Run(args []string) int { if outdated { c.Ui.Output(fmt.Sprintf( "\nYour version of Terraform is out of date! The latest version\n"+ - "is %s. You can update by downloading from https://www.terraform.io/downloads.html", + "is %s. You can update by downloading from https://developer.hashicorp.com/terraform/install", latest)) } diff --git a/internal/command/version_test.go b/internal/command/version_test.go index 3ec5a4b863..5a65849616 100644 --- a/internal/command/version_test.go +++ b/internal/command/version_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,10 +8,10 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" "github.com/hashicorp/terraform/internal/getproviders" - "github.com/mitchellh/cli" ) func TestVersionCommand_implements(t *testing.T) { @@ -103,7 +106,7 @@ func TestVersion_outdated(t *testing.T) { } actual := strings.TrimSpace(ui.OutputWriter.String()) - expected := "Terraform v4.5.6\non aros_riscv64\n\nYour version of Terraform is out of date! The latest version\nis 4.5.7. You can update by downloading from https://www.terraform.io/downloads.html" + expected := "Terraform v4.5.6\non aros_riscv64\n\nYour version of Terraform is out of date! The latest version\nis 4.5.7. You can update by downloading from https://developer.hashicorp.com/terraform/install" if actual != expected { t.Fatalf("wrong output\ngot: %#v\nwant: %#v", actual, expected) } diff --git a/internal/command/views/apply.go b/internal/command/views/apply.go index ec07f6ad9a..590b5945ed 100644 --- a/internal/command/views/apply.go +++ b/internal/command/views/apply.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -63,6 +66,14 @@ func (v *ApplyHuman) ResourceCount(stateOutPath string) { v.view.colorize.Color("[reset][bold][green]\nDestroy complete! Resources: %d destroyed.\n"), v.countHook.Removed, ) + } else if v.countHook.Imported > 0 { + v.view.streams.Printf( + v.view.colorize.Color("[reset][bold][green]\nApply complete! Resources: %d imported, %d added, %d changed, %d destroyed.\n"), + v.countHook.Imported, + v.countHook.Added, + v.countHook.Changed, + v.countHook.Removed, + ) } else { v.view.streams.Printf( v.view.colorize.Color("[reset][bold][green]\nApply complete! Resources: %d added, %d changed, %d destroyed.\n"), @@ -130,6 +141,7 @@ func (v *ApplyJSON) ResourceCount(stateOutPath string) { Add: v.countHook.Added, Change: v.countHook.Changed, Remove: v.countHook.Removed, + Import: v.countHook.Imported, Operation: operation, }) } diff --git a/internal/command/views/apply_test.go b/internal/command/views/apply_test.go index b16242ed63..4f376f5eb3 100644 --- a/internal/command/views/apply_test.go +++ b/internal/command/views/apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -100,16 +103,24 @@ func TestApplyHuman_help(t *testing.T) { // Hooks and ResourceCount are tangled up and easiest to test together. func TestApply_resourceCount(t *testing.T) { testCases := map[string]struct { - destroy bool - want string + destroy bool + want string + importing bool }{ "apply": { false, "Apply complete! Resources: 1 added, 2 changed, 3 destroyed.", + false, }, "destroy": { true, "Destroy complete! Resources: 3 destroyed.", + false, + }, + "import": { + false, + "Apply complete! Resources: 1 imported, 1 added, 2 changed, 3 destroyed.", + true, }, } @@ -138,6 +149,10 @@ func TestApply_resourceCount(t *testing.T) { count.Changed = 2 count.Removed = 3 + if tc.importing { + count.Imported = 1 + } + v.ResourceCount("") got := done(t).Stdout() @@ -246,7 +261,6 @@ func TestApplyJSON_outputs(t *testing.T) { }, "password": map[string]interface{}{ "sensitive": true, - "value": "horse-battery", "type": "string", }, }, diff --git a/internal/command/views/cloud.go b/internal/command/views/cloud.go new file mode 100644 index 0000000000..4842360507 --- /dev/null +++ b/internal/command/views/cloud.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + "net/http" + "strings" + "time" +) + +// CloudHooks provides functions that help with integrating directly into +// the go-tfe tfe.Client struct. +type CloudHooks struct { + // lastRetry is set to the last time a request was retried. + lastRetry time.Time +} + +// RetryLogHook returns a string providing an update about a request from the +// client being retried. +// +// If colorize is true, then the value returned by this function should be +// processed by a colorizer. +func (hooks *CloudHooks) RetryLogHook(attemptNum int, resp *http.Response, colorize bool) string { + // Ignore the first retry to make sure any delayed output will + // be written to the console before we start logging retries. + // + // The retry logic in the TFE client will retry both rate limited + // requests and server errors, but in the cloud backend we only + // care about server errors so we ignore rate limit (429) errors. + if attemptNum == 0 || (resp != nil && resp.StatusCode == 429) { + hooks.lastRetry = time.Now() + return "" + } + + var msg string + if attemptNum == 1 { + msg = initialRetryError + } else { + msg = fmt.Sprintf(repeatedRetryError, time.Since(hooks.lastRetry).Round(time.Second)) + } + + if colorize { + return strings.TrimSpace(fmt.Sprintf("[reset][yellow]%s[reset]", msg)) + } + return strings.TrimSpace(msg) +} + +// The newline in this error is to make it look good in the CLI! +const initialRetryError = ` +There was an error connecting to HCP Terraform. Please do not exit +Terraform to prevent data loss! Trying to restore the connection... +` + +const repeatedRetryError = "Still trying to restore the connection... (%s elapsed)" diff --git a/internal/command/views/hook_count.go b/internal/command/views/hook_count.go index 054c9da38d..f4c4e2b7f8 100644 --- a/internal/command/views/hook_count.go +++ b/internal/command/views/hook_count.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -7,16 +10,16 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" ) // countHook is a hook that counts the number of resources // added, removed, changed during the course of an apply. type countHook struct { - Added int - Changed int - Removed int + Added int + Changed int + Removed int + Imported int ToAdd int ToChange int @@ -39,9 +42,10 @@ func (h *countHook) Reset() { h.Added = 0 h.Changed = 0 h.Removed = 0 + h.Imported = 0 } -func (h *countHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { +func (h *countHook) PreApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { h.Lock() defer h.Unlock() @@ -49,17 +53,17 @@ func (h *countHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generati h.pending = make(map[string]plans.Action) } - h.pending[addr.String()] = action + h.pending[id.Addr.String()] = action return terraform.HookActionContinue, nil } -func (h *countHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) { +func (h *countHook) PostApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (terraform.HookAction, error) { h.Lock() defer h.Unlock() if h.pending != nil { - pendingKey := addr.String() + pendingKey := id.Addr.String() if action, ok := h.pending[pendingKey]; ok { delete(h.pending, pendingKey) @@ -82,12 +86,12 @@ func (h *countHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generat return terraform.HookActionContinue, nil } -func (h *countHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { +func (h *countHook) PostDiff(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { h.Lock() defer h.Unlock() // We don't count anything for data resources - if addr.Resource.Resource.Mode == addrs.DataResourceMode { + if id.Addr.Resource.Resource.Mode == addrs.DataResourceMode { return terraform.HookActionContinue, nil } @@ -104,3 +108,11 @@ func (h *countHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generati return terraform.HookActionContinue, nil } + +func (h *countHook) PostApplyImport(id terraform.HookResourceIdentity, importing plans.ImportingSrc) (terraform.HookAction, error) { + h.Lock() + defer h.Unlock() + + h.Imported++ + return terraform.HookActionContinue, nil +} diff --git a/internal/command/views/hook_count_test.go b/internal/command/views/hook_count_test.go index 3cf51d4dd4..f8297d1ff7 100644 --- a/internal/command/views/hook_count_test.go +++ b/internal/command/views/hook_count_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -14,6 +17,17 @@ import ( legacy "github.com/hashicorp/terraform/internal/legacy/terraform" ) +func testCountHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResourceIdentity { + return terraform.HookResourceIdentity{ + Addr: addr, + ProviderAddr: addrs.Provider{ + Type: "test", + Namespace: "hashicorp", + Hostname: "example.com", + }, + } +} + func TestCountHook_impl(t *testing.T) { var _ terraform.Hook = new(countHook) } @@ -22,7 +36,7 @@ func TestCountHookPostDiff_DestroyDeposed(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "lorem": &legacy.InstanceDiff{DestroyDeposed: true}, + "lorem": {DestroyDeposed: true}, } for k := range resources { @@ -32,7 +46,7 @@ func TestCountHookPostDiff_DestroyDeposed(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.DeposedKey("deadbeef"), plans.Delete, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), states.DeposedKey("deadbeef"), plans.Delete, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -50,10 +64,10 @@ func TestCountHookPostDiff_DestroyOnly(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "foo": &legacy.InstanceDiff{Destroy: true}, - "bar": &legacy.InstanceDiff{Destroy: true}, - "lorem": &legacy.InstanceDiff{Destroy: true}, - "ipsum": &legacy.InstanceDiff{Destroy: true}, + "foo": {Destroy: true}, + "bar": {Destroy: true}, + "lorem": {Destroy: true}, + "ipsum": {Destroy: true}, } for k := range resources { @@ -63,7 +77,7 @@ func TestCountHookPostDiff_DestroyOnly(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.CurrentGen, plans.Delete, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), addrs.NotDeposed, plans.Delete, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -81,19 +95,19 @@ func TestCountHookPostDiff_AddOnly(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "foo": &legacy.InstanceDiff{ + "foo": { Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{RequiresNew: true}, + "foo": {RequiresNew: true}, }, }, - "bar": &legacy.InstanceDiff{ + "bar": { Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{RequiresNew: true}, + "foo": {RequiresNew: true}, }, }, - "lorem": &legacy.InstanceDiff{ + "lorem": { Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{RequiresNew: true}, + "foo": {RequiresNew: true}, }, }, } @@ -105,7 +119,7 @@ func TestCountHookPostDiff_AddOnly(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.CurrentGen, plans.Create, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), addrs.NotDeposed, plans.Create, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -123,22 +137,22 @@ func TestCountHookPostDiff_ChangeOnly(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "foo": &legacy.InstanceDiff{ + "foo": { Destroy: false, Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{}, + "foo": {}, }, }, - "bar": &legacy.InstanceDiff{ + "bar": { Destroy: false, Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{}, + "foo": {}, }, }, - "lorem": &legacy.InstanceDiff{ + "lorem": { Destroy: false, Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{}, + "foo": {}, }, }, } @@ -150,7 +164,7 @@ func TestCountHookPostDiff_ChangeOnly(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.CurrentGen, plans.Update, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), addrs.NotDeposed, plans.Update, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -181,7 +195,7 @@ func TestCountHookPostDiff_Mixed(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), addrs.NotDeposed, a, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -200,10 +214,10 @@ func TestCountHookPostDiff_NoChange(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "foo": &legacy.InstanceDiff{}, - "bar": &legacy.InstanceDiff{}, - "lorem": &legacy.InstanceDiff{}, - "ipsum": &legacy.InstanceDiff{}, + "foo": {}, + "bar": {}, + "lorem": {}, + "ipsum": {}, } for k := range resources { @@ -213,7 +227,7 @@ func TestCountHookPostDiff_NoChange(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.CurrentGen, plans.NoOp, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), addrs.NotDeposed, plans.NoOp, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -245,7 +259,7 @@ func TestCountHookPostDiff_DataSource(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PostDiff(addr, states.CurrentGen, a, cty.DynamicVal, cty.DynamicVal) + h.PostDiff(testCountHookResourceID(addr), addrs.NotDeposed, a, cty.DynamicVal, cty.DynamicVal) } expected := new(countHook) @@ -264,22 +278,22 @@ func TestCountHookApply_ChangeOnly(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "foo": &legacy.InstanceDiff{ + "foo": { Destroy: false, Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{}, + "foo": {}, }, }, - "bar": &legacy.InstanceDiff{ + "bar": { Destroy: false, Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{}, + "foo": {}, }, }, - "lorem": &legacy.InstanceDiff{ + "lorem": { Destroy: false, Attributes: map[string]*legacy.ResourceAttrDiff{ - "foo": &legacy.ResourceAttrDiff{}, + "foo": {}, }, }, } @@ -291,8 +305,8 @@ func TestCountHookApply_ChangeOnly(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PreApply(addr, states.CurrentGen, plans.Update, cty.DynamicVal, cty.DynamicVal) - h.PostApply(addr, states.CurrentGen, cty.DynamicVal, nil) + h.PreApply(testCountHookResourceID(addr), addrs.NotDeposed, plans.Update, cty.DynamicVal, cty.DynamicVal) + h.PostApply(testCountHookResourceID(addr), addrs.NotDeposed, cty.DynamicVal, nil) } expected := &countHook{pending: make(map[string]plans.Action)} @@ -309,10 +323,10 @@ func TestCountHookApply_DestroyOnly(t *testing.T) { h := new(countHook) resources := map[string]*legacy.InstanceDiff{ - "foo": &legacy.InstanceDiff{Destroy: true}, - "bar": &legacy.InstanceDiff{Destroy: true}, - "lorem": &legacy.InstanceDiff{Destroy: true}, - "ipsum": &legacy.InstanceDiff{Destroy: true}, + "foo": {Destroy: true}, + "bar": {Destroy: true}, + "lorem": {Destroy: true}, + "ipsum": {Destroy: true}, } for k := range resources { @@ -322,8 +336,8 @@ func TestCountHookApply_DestroyOnly(t *testing.T) { Name: k, }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - h.PreApply(addr, states.CurrentGen, plans.Delete, cty.DynamicVal, cty.DynamicVal) - h.PostApply(addr, states.CurrentGen, cty.DynamicVal, nil) + h.PreApply(testCountHookResourceID(addr), addrs.NotDeposed, plans.Delete, cty.DynamicVal, cty.DynamicVal) + h.PostApply(testCountHookResourceID(addr), addrs.NotDeposed, cty.DynamicVal, nil) } expected := &countHook{pending: make(map[string]plans.Action)} diff --git a/internal/command/views/hook_json.go b/internal/command/views/hook_json.go index 38e24de39d..4d05849bd9 100644 --- a/internal/command/views/hook_json.go +++ b/internal/command/views/hook_json.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -13,19 +16,16 @@ import ( "github.com/hashicorp/terraform/internal/command/format" "github.com/hashicorp/terraform/internal/command/views/json" "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" ) -// How long to wait between sending heartbeat/progress messages -const heartbeatInterval = 10 * time.Second - func newJSONHook(view *JSONView) *jsonHook { return &jsonHook{ - view: view, - applying: make(map[string]applyProgress), - timeNow: time.Now, - timeAfter: time.After, + view: view, + resourceProgress: make(map[string]resourceProgress), + timeNow: time.Now, + timeAfter: time.After, + periodicUiTimer: defaultPeriodicUiTimer, } } @@ -34,24 +34,26 @@ type jsonHook struct { view *JSONView - // Concurrent map of resource addresses to allow the sequence of pre-apply, - // progress, and post-apply messages to share data about the resource - applying map[string]applyProgress - applyingLock sync.Mutex + // Concurrent map of resource addresses to allow tracking + // progress, and post-action messages to share data about the resource + resourceProgress map[string]resourceProgress + resourceProgressMu sync.Mutex // Mockable functions for testing the progress timer goroutine timeNow func() time.Time timeAfter func(time.Duration) <-chan time.Time + + periodicUiTimer time.Duration } var _ terraform.Hook = (*jsonHook)(nil) -type applyProgress struct { +type resourceProgress struct { addr addrs.AbsResourceInstance action plans.Action start time.Time - // done is used for post-apply to stop the progress goroutine + // done is used for post-action to stop the progress goroutine done chan struct{} // heartbeatDone is used to allow tests to safely wait for the progress @@ -59,22 +61,22 @@ type applyProgress struct { heartbeatDone chan struct{} } -func (h *jsonHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { +func (h *jsonHook) PreApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { if action != plans.NoOp { idKey, idValue := format.ObjectValueIDOrName(priorState) - h.view.Hook(json.NewApplyStart(addr, action, idKey, idValue)) + h.view.Hook(json.NewApplyStart(id.Addr, action, idKey, idValue)) } - progress := applyProgress{ - addr: addr, + progress := resourceProgress{ + addr: id.Addr, action: action, start: h.timeNow().Round(time.Second), done: make(chan struct{}), heartbeatDone: make(chan struct{}), } - h.applyingLock.Lock() - h.applying[addr.String()] = progress - h.applyingLock.Unlock() + h.resourceProgressMu.Lock() + h.resourceProgress[id.Addr.String()] = progress + h.resourceProgressMu.Unlock() if action != plans.NoOp { go h.applyingHeartbeat(progress) @@ -82,13 +84,13 @@ func (h *jsonHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generatio return terraform.HookActionContinue, nil } -func (h *jsonHook) applyingHeartbeat(progress applyProgress) { +func (h *jsonHook) applyingHeartbeat(progress resourceProgress) { defer close(progress.heartbeatDone) for { select { case <-progress.done: return - case <-h.timeAfter(heartbeatInterval): + case <-h.timeAfter(h.periodicUiTimer): } elapsed := h.timeNow().Round(time.Second).Sub(progress.start) @@ -96,15 +98,15 @@ func (h *jsonHook) applyingHeartbeat(progress applyProgress) { } } -func (h *jsonHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (terraform.HookAction, error) { - key := addr.String() - h.applyingLock.Lock() - progress := h.applying[key] +func (h *jsonHook) PostApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (terraform.HookAction, error) { + key := id.Addr.String() + h.resourceProgressMu.Lock() + progress := h.resourceProgress[key] if progress.done != nil { close(progress.done) } - delete(h.applying, key) - h.applyingLock.Unlock() + delete(h.resourceProgress, key) + h.resourceProgressMu.Unlock() if progress.action == plans.NoOp { return terraform.HookActionContinue, nil @@ -116,50 +118,116 @@ func (h *jsonHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generati // Errors are collected and displayed post-apply, so no need to // re-render them here. Instead just signal that this resource failed // to apply. - h.view.Hook(json.NewApplyErrored(addr, progress.action, elapsed)) + h.view.Hook(json.NewApplyErrored(id.Addr, progress.action, elapsed)) } else { idKey, idValue := format.ObjectValueID(newState) - h.view.Hook(json.NewApplyComplete(addr, progress.action, idKey, idValue, elapsed)) + h.view.Hook(json.NewApplyComplete(id.Addr, progress.action, idKey, idValue, elapsed)) } return terraform.HookActionContinue, nil } -func (h *jsonHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) { - h.view.Hook(json.NewProvisionStart(addr, typeName)) +func (h *jsonHook) PreProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string) (terraform.HookAction, error) { + h.view.Hook(json.NewProvisionStart(id.Addr, typeName)) return terraform.HookActionContinue, nil } -func (h *jsonHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (terraform.HookAction, error) { +func (h *jsonHook) PostProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string, err error) (terraform.HookAction, error) { if err != nil { // Errors are collected and displayed post-apply, so no need to // re-render them here. Instead just signal that this provisioner step // failed. - h.view.Hook(json.NewProvisionErrored(addr, typeName)) + h.view.Hook(json.NewProvisionErrored(id.Addr, typeName)) } else { - h.view.Hook(json.NewProvisionComplete(addr, typeName)) + h.view.Hook(json.NewProvisionComplete(id.Addr, typeName)) } return terraform.HookActionContinue, nil } -func (h *jsonHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) { +func (h *jsonHook) ProvisionOutput(id terraform.HookResourceIdentity, typeName string, msg string) { s := bufio.NewScanner(strings.NewReader(msg)) s.Split(scanLines) for s.Scan() { line := strings.TrimRightFunc(s.Text(), unicode.IsSpace) if line != "" { - h.view.Hook(json.NewProvisionProgress(addr, typeName, line)) + h.view.Hook(json.NewProvisionProgress(id.Addr, typeName, line)) } } } -func (h *jsonHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) { +func (h *jsonHook) PreRefresh(id terraform.HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (terraform.HookAction, error) { idKey, idValue := format.ObjectValueID(priorState) - h.view.Hook(json.NewRefreshStart(addr, idKey, idValue)) + h.view.Hook(json.NewRefreshStart(id.Addr, idKey, idValue)) return terraform.HookActionContinue, nil } -func (h *jsonHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (terraform.HookAction, error) { +func (h *jsonHook) PostRefresh(id terraform.HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (terraform.HookAction, error) { idKey, idValue := format.ObjectValueID(newState) - h.view.Hook(json.NewRefreshComplete(addr, idKey, idValue)) + h.view.Hook(json.NewRefreshComplete(id.Addr, idKey, idValue)) + return terraform.HookActionContinue, nil +} + +func (h *jsonHook) PreEphemeralOp(id terraform.HookResourceIdentity, action plans.Action) (terraform.HookAction, error) { + // this uses the same plans.Read action as a data source to indicate that + // the ephemeral resource can't be processed until apply, so there is no + // progress hook + if action == plans.Read { + return terraform.HookActionContinue, nil + } + + h.view.Hook(json.NewEphemeralOpStart(id.Addr, action)) + progress := resourceProgress{ + addr: id.Addr, + action: action, + start: h.timeNow().Round(time.Second), + done: make(chan struct{}), + heartbeatDone: make(chan struct{}), + } + h.resourceProgressMu.Lock() + h.resourceProgress[id.Addr.String()] = progress + h.resourceProgressMu.Unlock() + + go h.ephemeralOpHeartbeat(progress) + + return terraform.HookActionContinue, nil +} + +func (h *jsonHook) ephemeralOpHeartbeat(progress resourceProgress) { + defer close(progress.heartbeatDone) + for { + select { + case <-progress.done: + return + case <-h.timeAfter(h.periodicUiTimer): + } + + elapsed := h.timeNow().Round(time.Second).Sub(progress.start) + h.view.Hook(json.NewEphemeralOpProgress(progress.addr, progress.action, elapsed)) + } +} + +func (h *jsonHook) PostEphemeralOp(id terraform.HookResourceIdentity, action plans.Action, opErr error) (terraform.HookAction, error) { + key := id.Addr.String() + h.resourceProgressMu.Lock() + progress := h.resourceProgress[key] + if progress.done != nil { + close(progress.done) + } + delete(h.resourceProgress, key) + h.resourceProgressMu.Unlock() + + if progress.action == plans.NoOp { + return terraform.HookActionContinue, nil + } + + elapsed := h.timeNow().Round(time.Second).Sub(progress.start) + + if opErr != nil { + // Errors are collected and displayed post-operation, so no need to + // re-render them here. Instead just signal that this operation failed. + h.view.Hook(json.NewEphemeralOpErrored(id.Addr, progress.action, elapsed)) + } else { + h.view.Hook(json.NewEphemeralOpComplete(id.Addr, progress.action, elapsed)) + } + return terraform.HookActionContinue, nil } diff --git a/internal/command/views/hook_json_test.go b/internal/command/views/hook_json_test.go index cb1cbc920b..1c2569dd7e 100644 --- a/internal/command/views/hook_json_test.go +++ b/internal/command/views/hook_json_test.go @@ -1,19 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( + "errors" "fmt" + "strings" "sync" "testing" "time" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" - "github.com/zclconf/go-cty/cty" ) +func testJSONHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResourceIdentity { + return terraform.HookResourceIdentity{ + Addr: addr, + ProviderAddr: addrs.Provider{ + Type: "test", + Namespace: "hashicorp", + Hostname: "example.com", + }, + } +} + // Test a sequence of hooks associated with creating a resource func TestJSONHook_create(t *testing.T) { streams, done := terminal.StreamsForTesting(t) @@ -46,15 +62,15 @@ func TestJSONHook_create(t *testing.T) { }), }) - action, err := hook.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState) + action, err := hook.PreApply(testJSONHookResourceID(addr), addrs.NotDeposed, plans.Create, priorState, plannedNewState) testHookReturnValues(t, action, err) - action, err = hook.PreProvisionInstanceStep(addr, "local-exec") + action, err = hook.PreProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec") testHookReturnValues(t, action, err) - hook.ProvisionOutput(addr, "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`) + hook.ProvisionOutput(testJSONHookResourceID(addr), "local-exec", `Executing: ["/bin/sh" "-c" "touch /etc/motd"]`) - action, err = hook.PostProvisionInstanceStep(addr, "local-exec", nil) + action, err = hook.PostProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec", nil) testHookReturnValues(t, action, err) // Travel 10s into the future, notify the progress goroutine, and sleep @@ -63,7 +79,7 @@ func TestJSONHook_create(t *testing.T) { now = now.Add(10 * time.Second) after <- now nowMu.Unlock() - time.Sleep(1 * time.Millisecond) + time.Sleep(10 * time.Millisecond) // Travel 10s into the future, notify the progress goroutine, and sleep // briefly to allow it to execute @@ -71,24 +87,24 @@ func TestJSONHook_create(t *testing.T) { now = now.Add(10 * time.Second) after <- now nowMu.Unlock() - time.Sleep(1 * time.Millisecond) + time.Sleep(10 * time.Millisecond) // Travel 2s into the future. We have arrived! nowMu.Lock() now = now.Add(2 * time.Second) nowMu.Unlock() - action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, nil) + action, err = hook.PostApply(testJSONHookResourceID(addr), addrs.NotDeposed, plannedNewState, nil) testHookReturnValues(t, action, err) // Shut down the progress goroutine if still active - hook.applyingLock.Lock() - for key, progress := range hook.applying { + hook.resourceProgressMu.Lock() + for key, progress := range hook.resourceProgress { close(progress.done) <-progress.heartbeatDone - delete(hook.applying, key) + delete(hook.resourceProgress, key) } - hook.applyingLock.Unlock() + hook.resourceProgressMu.Unlock() wantResource := map[string]interface{}{ "addr": string("test_instance.boop"), @@ -201,25 +217,25 @@ func TestJSONHook_errors(t *testing.T) { }), }) - action, err := hook.PreApply(addr, states.CurrentGen, plans.Delete, priorState, plannedNewState) + action, err := hook.PreApply(testJSONHookResourceID(addr), addrs.NotDeposed, plans.Delete, priorState, plannedNewState) testHookReturnValues(t, action, err) provisionError := fmt.Errorf("provisioner didn't want to") - action, err = hook.PostProvisionInstanceStep(addr, "local-exec", provisionError) + action, err = hook.PostProvisionInstanceStep(testJSONHookResourceID(addr), "local-exec", provisionError) testHookReturnValues(t, action, err) applyError := fmt.Errorf("provider was sad") - action, err = hook.PostApply(addr, states.CurrentGen, plannedNewState, applyError) + action, err = hook.PostApply(testJSONHookResourceID(addr), addrs.NotDeposed, plannedNewState, applyError) testHookReturnValues(t, action, err) // Shut down the progress goroutine - hook.applyingLock.Lock() - for key, progress := range hook.applying { + hook.resourceProgressMu.Lock() + for key, progress := range hook.resourceProgress { close(progress.done) <-progress.heartbeatDone - delete(hook.applying, key) + delete(hook.resourceProgress, key) } - hook.applyingLock.Unlock() + hook.resourceProgressMu.Unlock() wantResource := map[string]interface{}{ "addr": string("test_instance.boop"), @@ -283,10 +299,10 @@ func TestJSONHook_refresh(t *testing.T) { }), }) - action, err := hook.PreRefresh(addr, states.CurrentGen, state) + action, err := hook.PreRefresh(testJSONHookResourceID(addr), addrs.NotDeposed, state) testHookReturnValues(t, action, err) - action, err = hook.PostRefresh(addr, states.CurrentGen, state, state) + action, err = hook.PostRefresh(testJSONHookResourceID(addr), addrs.NotDeposed, state, state) testHookReturnValues(t, action, err) wantResource := map[string]interface{}{ @@ -326,6 +342,231 @@ func TestJSONHook_refresh(t *testing.T) { testJSONViewOutputEquals(t, done(t).Stdout(), want) } +func TestJSONHook_EphemeralOp(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + hook := newJSONHook(NewJSONView(NewView(streams))) + + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "boop", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open) + testHookReturnValues(t, action, err) + + action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, nil) + testHookReturnValues(t, action, err) + + want := []map[string]interface{}{ + { + "@level": "info", + "@message": "test_instance.boop: Opening...", + "@module": "terraform.ui", + "type": "ephemeral_op_start", + "hook": map[string]interface{}{ + "action": string("open"), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + { + "@level": "info", + "@message": "test_instance.boop: Opening complete after 0s", + "@module": "terraform.ui", + "type": "ephemeral_op_complete", + "hook": map[string]interface{}{ + "action": string("open"), + "elapsed_seconds": float64(0), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + } + + testJSONViewOutputEquals(t, done(t).Stdout(), want) +} + +func TestJSONHook_EphemeralOp_progress(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + hook := newJSONHook(NewJSONView(NewView(streams))) + hook.periodicUiTimer = 1 * time.Second + + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "boop", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open) + testHookReturnValues(t, action, err) + + time.Sleep(2005 * time.Millisecond) + + action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, nil) + testHookReturnValues(t, action, err) + + want := []map[string]interface{}{ + { + "@level": "info", + "@message": "test_instance.boop: Opening...", + "@module": "terraform.ui", + "type": "ephemeral_op_start", + "hook": map[string]interface{}{ + "action": string("open"), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + { + "@level": "info", + "@message": "test_instance.boop: Still opening... [1s elapsed]", + "@module": "terraform.ui", + "type": "ephemeral_op_progress", + "hook": map[string]interface{}{ + "action": string("open"), + "elapsed_seconds": float64(1), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + { + "@level": "info", + "@message": "test_instance.boop: Still opening... [2s elapsed]", + "@module": "terraform.ui", + "type": "ephemeral_op_progress", + "hook": map[string]interface{}{ + "action": string("open"), + "elapsed_seconds": float64(2), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + { + "@level": "info", + "@message": "test_instance.boop: Opening complete after 2s", + "@module": "terraform.ui", + "type": "ephemeral_op_complete", + "hook": map[string]interface{}{ + "action": string("open"), + "elapsed_seconds": float64(2), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + } + + stdout := done(t).Stdout() + + // time.Sleep can take longer than declared time + // so we only test the first lines we expect to see after sleeping + lines := strings.SplitN(stdout, "\n", 4) + firstLines := strings.Join(lines[:4], "\n") + + testJSONViewOutputEquals(t, firstLines, want) +} + +func TestJSONHook_EphemeralOp_error(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + hook := newJSONHook(NewJSONView(NewView(streams))) + + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "boop", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + action, err := hook.PreEphemeralOp(testJSONHookResourceID(addr), plans.Open) + testHookReturnValues(t, action, err) + + action, err = hook.PostEphemeralOp(testJSONHookResourceID(addr), plans.Open, errors.New("test error")) + testHookReturnValues(t, action, err) + + want := []map[string]interface{}{ + { + "@level": "info", + "@message": "test_instance.boop: Opening...", + "@module": "terraform.ui", + "type": "ephemeral_op_start", + "hook": map[string]interface{}{ + "action": string("open"), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + { + "@level": "info", + "@message": "test_instance.boop: Opening errored after 0s", + "@module": "terraform.ui", + "type": "ephemeral_op_errored", + "hook": map[string]interface{}{ + "action": string("open"), + "elapsed_seconds": float64(0), + "resource": map[string]interface{}{ + "addr": string("test_instance.boop"), + "implied_provider": string("test"), + "module": string(""), + "resource": string("test_instance.boop"), + "resource_key": nil, + "resource_name": string("boop"), + "resource_type": string("test_instance"), + }, + }, + }, + } + + testJSONViewOutputEquals(t, done(t).Stdout(), want) +} + func testHookReturnValues(t *testing.T, action terraform.HookAction, err error) { t.Helper() diff --git a/internal/command/views/hook_ui.go b/internal/command/views/hook_ui.go index 2c5c0f5704..053be90aaf 100644 --- a/internal/command/views/hook_ui.go +++ b/internal/command/views/hook_ui.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -15,10 +18,11 @@ import ( "github.com/hashicorp/terraform/internal/command/format" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" ) +// How long to wait between sending heartbeat/progress messages const defaultPeriodicUiTimer = 10 * time.Second const maxIdLen = 80 @@ -46,10 +50,15 @@ var _ terraform.Hook = (*UiHook)(nil) // uiResourceState tracks the state of a single resource type uiResourceState struct { - DispAddr string - IDKey, IDValue string - Op uiResourceOp - Start time.Time + // Address represents resource address + Address string + // IDKey represents name of the identifyable attribute (e.g. "id" or "name") + IDKey string + // IDValue represents the ID + IDValue string + + Op uiResourceOp + Start time.Time DoneCh chan struct{} // To be used for cancellation @@ -66,12 +75,15 @@ const ( uiResourceDestroy uiResourceRead uiResourceNoOp + uiResourceOpen + uiResourceRenew + uiResourceClose ) -func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { - dispAddr := addr.String() - if gen != states.CurrentGen { - dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, gen) +func (h *UiHook) PreApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { + dispAddr := id.Addr.String() + if dk != addrs.NotDeposed { + dispAddr = fmt.Sprintf("%s (deposed object %s)", dispAddr, dk) } var operation string @@ -118,15 +130,15 @@ func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, )) } - key := addr.String() + key := id.Addr.String() uiState := uiResourceState{ - DispAddr: key, - IDKey: idKey, - IDValue: idValue, - Op: op, - Start: time.Now().Round(time.Second), - DoneCh: make(chan struct{}), - done: make(chan struct{}), + Address: key, + IDKey: idKey, + IDValue: idValue, + Op: op, + Start: time.Now().Round(time.Second), + DoneCh: make(chan struct{}), + done: make(chan struct{}), } h.resourcesLock.Lock() @@ -135,13 +147,13 @@ func (h *UiHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, // Start goroutine that shows progress if op != uiResourceNoOp { - go h.stillApplying(uiState) + go h.stillRunning(uiState) } return terraform.HookActionContinue, nil } -func (h *UiHook) stillApplying(state uiResourceState) { +func (h *UiHook) stillRunning(state uiResourceState) { defer close(state.done) for { select { @@ -162,6 +174,12 @@ func (h *UiHook) stillApplying(state uiResourceState) { msg = "Still creating..." case uiResourceRead: msg = "Still reading..." + case uiResourceOpen: + msg = "Still opening..." + case uiResourceRenew: + msg = "Still renewing..." + case uiResourceClose: + msg = "Still closing..." case uiResourceUnknown: return } @@ -171,26 +189,31 @@ func (h *UiHook) stillApplying(state uiResourceState) { idSuffix = fmt.Sprintf("%s=%s, ", state.IDKey, truncateId(state.IDValue, maxIdLen)) } + elapsed := time.Now().Round(time.Second).Sub(state.Start) + minutes := int(elapsed.Seconds()) / 60 + seconds := int(elapsed.Seconds()) % 60 + h.println(fmt.Sprintf( - h.view.colorize.Color("[reset][bold]%s: %s [%s%s elapsed][reset]"), - state.DispAddr, + h.view.colorize.Color("[reset][bold]%s: %s [%s%02dm%02ds elapsed][reset]"), + state.Address, msg, idSuffix, - time.Now().Round(time.Second).Sub(state.Start), + minutes, + seconds, )) } } -func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, applyerr error) (terraform.HookAction, error) { - id := addr.String() +func (h *UiHook) PostApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, applyerr error) (terraform.HookAction, error) { + addr := id.Addr.String() h.resourcesLock.Lock() - state := h.resources[id] + state := h.resources[addr] if state.DoneCh != nil { close(state.DoneCh) } - delete(h.resources, id) + delete(h.resources, addr) h.resourcesLock.Unlock() var stateIdSuffix string @@ -220,9 +243,9 @@ func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation return terraform.HookActionContinue, nil } - addrStr := addr.String() - if depKey, ok := gen.(states.DeposedKey); ok { - addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, depKey) + addrStr := id.Addr.String() + if dk != addrs.NotDeposed { + addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, dk) } colorized := fmt.Sprintf( @@ -234,20 +257,20 @@ func (h *UiHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation return terraform.HookActionContinue, nil } -func (h *UiHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (terraform.HookAction, error) { +func (h *UiHook) PreProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string) (terraform.HookAction, error) { h.println(fmt.Sprintf( h.view.colorize.Color("[reset][bold]%s: Provisioning with '%s'...[reset]"), - addr, typeName, + id.Addr, typeName, )) return terraform.HookActionContinue, nil } -func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, msg string) { +func (h *UiHook) ProvisionOutput(id terraform.HookResourceIdentity, typeName string, msg string) { var buf bytes.Buffer prefix := fmt.Sprintf( h.view.colorize.Color("[reset][bold]%s (%s):[reset] "), - addr, typeName, + id.Addr, typeName, ) s := bufio.NewScanner(strings.NewReader(msg)) s.Split(scanLines) @@ -261,15 +284,15 @@ func (h *UiHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string h.println(strings.TrimSpace(buf.String())) } -func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (terraform.HookAction, error) { +func (h *UiHook) PreRefresh(id terraform.HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (terraform.HookAction, error) { var stateIdSuffix string if k, v := format.ObjectValueID(priorState); k != "" && v != "" { stateIdSuffix = fmt.Sprintf(" [%s=%s]", k, v) } - addrStr := addr.String() - if depKey, ok := gen.(states.DeposedKey); ok { - addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, depKey) + addrStr := id.Addr.String() + if dk != addrs.NotDeposed { + addrStr = fmt.Sprintf("%s (deposed object %s)", addrStr, dk) } h.println(fmt.Sprintf( @@ -278,18 +301,18 @@ func (h *UiHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generatio return terraform.HookActionContinue, nil } -func (h *UiHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (terraform.HookAction, error) { +func (h *UiHook) PreImportState(id terraform.HookResourceIdentity, importID string) (terraform.HookAction, error) { h.println(fmt.Sprintf( h.view.colorize.Color("[reset][bold]%s: Importing from ID %q..."), - addr, importID, + id.Addr, importID, )) return terraform.HookActionContinue, nil } -func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (terraform.HookAction, error) { +func (h *UiHook) PostImportState(id terraform.HookResourceIdentity, imported []providers.ImportedResource) (terraform.HookAction, error) { h.println(fmt.Sprintf( h.view.colorize.Color("[reset][bold][green]%s: Import prepared!"), - addr, + id.Addr, )) for _, s := range imported { h.println(fmt.Sprintf( @@ -301,6 +324,132 @@ func (h *UiHook) PostImportState(addr addrs.AbsResourceInstance, imported []prov return terraform.HookActionContinue, nil } +func (h *UiHook) PrePlanImport(id terraform.HookResourceIdentity, importTarget cty.Value) (terraform.HookAction, error) { + if importTarget.Type().IsObjectType() { + h.println(fmt.Sprintf( + h.view.colorize.Color("[reset][bold]%s: Preparing import... [identity=%s]"), + id.Addr, tfdiags.ObjectToString(importTarget), + )) + } else { + h.println(fmt.Sprintf( + h.view.colorize.Color("[reset][bold]%s: Preparing import... [id=%s]"), + id.Addr, importTarget.AsString(), + )) + + } + + return terraform.HookActionContinue, nil +} + +func (h *UiHook) PreApplyImport(id terraform.HookResourceIdentity, importing plans.ImportingSrc) (terraform.HookAction, error) { + h.println(fmt.Sprintf( + h.view.colorize.Color("[reset][bold]%s: Importing... [id=%s]"), + id.Addr, importing.ID, + )) + + return terraform.HookActionContinue, nil +} + +func (h *UiHook) PostApplyImport(id terraform.HookResourceIdentity, importing plans.ImportingSrc) (terraform.HookAction, error) { + h.println(fmt.Sprintf( + h.view.colorize.Color("[reset][bold]%s: Import complete [id=%s]"), + id.Addr, importing.ID, + )) + + return terraform.HookActionContinue, nil +} + +func (h *UiHook) PreEphemeralOp(rId terraform.HookResourceIdentity, action plans.Action) (terraform.HookAction, error) { + key := rId.Addr.String() + + var operation string + var op uiResourceOp + switch action { + case plans.Read: + // FIXME: this uses the same semantics as data sources, where "read" + // means deferred until apply, but because data sources don't implement + // hooks, and the meaning of Read is overloaded, we can't rely on any + // existing hooks + operation = "Configuration unknown, deferring..." + case plans.Open: + operation = "Opening..." + op = uiResourceOpen + case plans.Renew: + operation = "Renewing..." + op = uiResourceRenew + case plans.Close: + operation = "Closing..." + op = uiResourceClose + default: + // We don't expect any other actions in here, so anything else is a + // bug in the caller but we'll ignore it in order to be robust. + h.println(fmt.Sprintf("(Unknown action %s for %s)", action, key)) + return terraform.HookActionContinue, nil + } + + h.println(fmt.Sprintf( + h.view.colorize.Color("[reset][bold]%s: %s"), + rId.Addr, operation, + )) + + if action == plans.Read { + return terraform.HookActionContinue, nil + } + + uiState := uiResourceState{ + Address: key, + Op: op, + Start: time.Now().Round(time.Second), + DoneCh: make(chan struct{}), + done: make(chan struct{}), + } + + h.resourcesLock.Lock() + h.resources[key] = uiState + h.resourcesLock.Unlock() + + go h.stillRunning(uiState) + + return terraform.HookActionContinue, nil +} + +func (h *UiHook) PostEphemeralOp(rId terraform.HookResourceIdentity, action plans.Action, opErr error) (terraform.HookAction, error) { + addr := rId.Addr.String() + h.resourcesLock.Lock() + state := h.resources[addr] + if state.DoneCh != nil { + close(state.DoneCh) + } + delete(h.resources, addr) + h.resourcesLock.Unlock() + + elapsedTime := time.Now().Round(time.Second).Sub(state.Start) + + var msg string + switch state.Op { + case uiResourceOpen: + msg = "Opening complete" + case uiResourceRenew: + msg = "Renewal complete" + case uiResourceClose: + msg = "Closing complete" + case uiResourceUnknown: + return terraform.HookActionContinue, nil + } + + if opErr != nil { + // Errors are collected and printed in ApplyCommand, no need to duplicate + return terraform.HookActionContinue, nil + } + + h.println(fmt.Sprintf( + h.view.colorize.Color("[reset][bold]%s: %s after %s"), + rId.Addr, msg, elapsedTime, + )) + + return terraform.HookActionContinue, nil +} + // Wrap calls to the view so that concurrent calls do not interleave println. func (h *UiHook) println(s string) { h.viewLock.Lock() diff --git a/internal/command/views/hook_ui_test.go b/internal/command/views/hook_ui_test.go index bbde3b6866..08b3b81bf9 100644 --- a/internal/command/views/hook_ui_test.go +++ b/internal/command/views/hook_ui_test.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( + "errors" "fmt" "regexp" "testing" @@ -19,6 +23,17 @@ import ( "github.com/hashicorp/terraform/internal/terraform" ) +func testUiHookResourceID(addr addrs.AbsResourceInstance) terraform.HookResourceIdentity { + return terraform.HookResourceIdentity{ + Addr: addr, + ProviderAddr: addrs.Provider{ + Type: "test", + Namespace: "hashicorp", + Hostname: "example.com", + }, + } +} + // Test the PreApply hook for creating a new resource func TestUiHookPreApply_create(t *testing.T) { streams, done := terminal.StreamsForTesting(t) @@ -48,7 +63,7 @@ func TestUiHookPreApply_create(t *testing.T) { }), }) - action, err := h.PreApply(addr, states.CurrentGen, plans.Create, priorState, plannedNewState) + action, err := h.PreApply(testUiHookResourceID(addr), addrs.NotDeposed, plans.Create, priorState, plannedNewState) if err != nil { t.Fatal(err) } @@ -106,7 +121,7 @@ func TestUiHookPreApply_periodicTimer(t *testing.T) { }), }) - action, err := h.PreApply(addr, states.CurrentGen, plans.Update, priorState, plannedNewState) + action, err := h.PreApply(testUiHookResourceID(addr), addrs.NotDeposed, plans.Update, priorState, plannedNewState) if err != nil { t.Fatal(err) } @@ -114,7 +129,7 @@ func TestUiHookPreApply_periodicTimer(t *testing.T) { t.Fatalf("Expected hook to continue, given: %#v", action) } - time.Sleep(3100 * time.Millisecond) + time.Sleep(3005 * time.Millisecond) // stop the background writer uiState := h.resources[addr.String()] @@ -122,13 +137,14 @@ func TestUiHookPreApply_periodicTimer(t *testing.T) { <-uiState.done expectedOutput := `test_instance.foo: Modifying... [id=test] -test_instance.foo: Still modifying... [id=test, 1s elapsed] -test_instance.foo: Still modifying... [id=test, 2s elapsed] -test_instance.foo: Still modifying... [id=test, 3s elapsed] +test_instance.foo: Still modifying... [id=test, 00m01s elapsed] +test_instance.foo: Still modifying... [id=test, 00m02s elapsed] +test_instance.foo: Still modifying... [id=test, 00m03s elapsed] ` result := done(t) output := result.Stdout() - if output != expectedOutput { + // we do not test for equality because time.Sleep can take longer than declared time + if !strings.HasPrefix(output, expectedOutput) { t.Fatalf("Output didn't match.\nExpected: %q\nGiven: %q", expectedOutput, output) } @@ -170,7 +186,7 @@ func TestUiHookPreApply_destroy(t *testing.T) { })) key := states.NewDeposedKey() - action, err := h.PreApply(addr, key, plans.Delete, priorState, plannedNewState) + action, err := h.PreApply(testUiHookResourceID(addr), key, plans.Delete, priorState, plannedNewState) if err != nil { t.Fatal(err) } @@ -221,7 +237,7 @@ func TestUiHookPostApply_colorInterpolation(t *testing.T) { "id": cty.StringVal("[blue]"), }) - action, err := h.PostApply(addr, states.CurrentGen, newState, nil) + action, err := h.PostApply(testUiHookResourceID(addr), addrs.NotDeposed, newState, nil) if err != nil { t.Fatal(err) } @@ -274,7 +290,7 @@ func TestUiHookPostApply_emptyState(t *testing.T) { "names": cty.List(cty.String), })) - action, err := h.PostApply(addr, states.CurrentGen, newState, nil) + action, err := h.PostApply(testUiHookResourceID(addr), addrs.NotDeposed, newState, nil) if err != nil { t.Fatal(err) } @@ -296,7 +312,7 @@ func TestUiHookPostApply_emptyState(t *testing.T) { } } -func TestPreProvisionInstanceStep(t *testing.T) { +func TestUiHookPreProvisionInstanceStep(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) h := NewUiHook(view) @@ -307,7 +323,7 @@ func TestPreProvisionInstanceStep(t *testing.T) { Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - action, err := h.PreProvisionInstanceStep(addr, "local-exec") + action, err := h.PreProvisionInstanceStep(testUiHookResourceID(addr), "local-exec") if err != nil { t.Fatal(err) } @@ -323,7 +339,7 @@ func TestPreProvisionInstanceStep(t *testing.T) { // Test ProvisionOutput, including lots of edge cases for the output // whitespace/line ending logic. -func TestProvisionOutput(t *testing.T) { +func TestUiHookProvisionOutput(t *testing.T) { addr := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", @@ -394,7 +410,7 @@ test_instance.foo (winrm): bar view := NewView(streams) h := NewUiHook(view) - h.ProvisionOutput(addr, tc.provisioner, tc.input) + h.ProvisionOutput(testUiHookResourceID(addr), tc.provisioner, tc.input) result := done(t) if got := result.Stdout(); got != tc.wantOutput { @@ -406,7 +422,7 @@ test_instance.foo (winrm): bar // Test the PreRefresh hook in the normal path where the resource exists with // an ID key and value in the state. -func TestPreRefresh(t *testing.T) { +func TestUiHookPreRefresh(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) h := NewUiHook(view) @@ -422,7 +438,7 @@ func TestPreRefresh(t *testing.T) { "bar": cty.ListValEmpty(cty.String), }) - action, err := h.PreRefresh(addr, states.CurrentGen, priorState) + action, err := h.PreRefresh(testUiHookResourceID(addr), addrs.NotDeposed, priorState) if err != nil { t.Fatal(err) @@ -439,7 +455,7 @@ func TestPreRefresh(t *testing.T) { // Test that PreRefresh still works if no ID key and value can be determined // from state. -func TestPreRefresh_noID(t *testing.T) { +func TestUiHookPreRefresh_noID(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) h := NewUiHook(view) @@ -454,7 +470,7 @@ func TestPreRefresh_noID(t *testing.T) { "bar": cty.ListValEmpty(cty.String), }) - action, err := h.PreRefresh(addr, states.CurrentGen, priorState) + action, err := h.PreRefresh(testUiHookResourceID(addr), addrs.NotDeposed, priorState) if err != nil { t.Fatal(err) @@ -470,7 +486,7 @@ func TestPreRefresh_noID(t *testing.T) { } // Test the very simple PreImportState hook. -func TestPreImportState(t *testing.T) { +func TestUiHookPreImportState(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) h := NewUiHook(view) @@ -481,7 +497,7 @@ func TestPreImportState(t *testing.T) { Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - action, err := h.PreImportState(addr, "test") + action, err := h.PreImportState(testUiHookResourceID(addr), "test") if err != nil { t.Fatal(err) @@ -499,7 +515,7 @@ func TestPreImportState(t *testing.T) { // Test the PostImportState UI hook. Again, this hook behaviour seems odd to // me (see below), so please don't consider these tests as justification for // keeping this behaviour. -func TestPostImportState(t *testing.T) { +func TestUiHookPostImportState(t *testing.T) { streams, done := terminal.StreamsForTesting(t) view := NewView(streams) h := NewUiHook(view) @@ -529,7 +545,7 @@ func TestPostImportState(t *testing.T) { }, } - action, err := h.PostImportState(addr, imported) + action, err := h.PostImportState(testUiHookResourceID(addr), imported) if err != nil { t.Fatal(err) @@ -548,6 +564,126 @@ func TestPostImportState(t *testing.T) { } } +func TestUiHookEphemeralOp(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + h := NewUiHook(view) + + addr := addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + action, err := h.PreEphemeralOp(testUiHookResourceID(addr), plans.Close) + if err != nil { + t.Fatal(err) + } + if action != terraform.HookActionContinue { + t.Fatalf("Expected hook to continue, given: %#v", action) + } + + action, err = h.PostEphemeralOp(testUiHookResourceID(addr), plans.Close, nil) + if err != nil { + t.Fatal(err) + } + if action != terraform.HookActionContinue { + t.Fatalf("Expected hook to continue, given: %#v", action) + } + result := done(t) + + want := `ephemeral.test_instance.foo: Closing... +ephemeral.test_instance.foo: Closing complete after 0s +` + if got := result.Stdout(); got != want { + t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) + } +} + +func TestUiHookEphemeralOp_progress(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + h := NewUiHook(view) + h.periodicUiTimer = 1 * time.Second + + addr := addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + action, err := h.PreEphemeralOp(testUiHookResourceID(addr), plans.Open) + if err != nil { + t.Fatal(err) + } + if action != terraform.HookActionContinue { + t.Fatalf("Expected hook to continue, given: %#v", action) + } + + start := time.Now() + time.Sleep(2005 * time.Millisecond) + elapsed := time.Since(start).Round(time.Second) + + action, err = h.PostEphemeralOp(testUiHookResourceID(addr), plans.Open, nil) + if err != nil { + t.Fatal(err) + } + if action != terraform.HookActionContinue { + t.Fatalf("Expected hook to continue, given: %#v", action) + } + + result := done(t) + stdout := result.Stdout() + + // we do not test for equality because time.Sleep can take longer than declared time + wantPrefix := `ephemeral.test_instance.foo: Opening... +ephemeral.test_instance.foo: Still opening... [00m01s elapsed] +ephemeral.test_instance.foo: Still opening... [00m02s elapsed]` + if !strings.HasPrefix(stdout, wantPrefix) { + t.Fatalf("unexpected prefix\n got: %q\nwant: %q", stdout, wantPrefix) + } + wantSuffix := fmt.Sprintf(`ephemeral.test_instance.foo: Opening complete after %s +`, elapsed) + if !strings.HasSuffix(stdout, wantSuffix) { + t.Fatalf("unexpected prefix\n got: %q\nwant: %q", stdout, wantSuffix) + } +} + +func TestUiHookEphemeralOp_error(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewView(streams) + h := NewUiHook(view) + + addr := addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + action, err := h.PreEphemeralOp(testUiHookResourceID(addr), plans.Close) + if err != nil { + t.Fatal(err) + } + if action != terraform.HookActionContinue { + t.Fatalf("Expected hook to continue, given: %#v", action) + } + + action, err = h.PostEphemeralOp(testUiHookResourceID(addr), plans.Close, errors.New("test error")) + if err != nil { + t.Fatal(err) + } + if action != terraform.HookActionContinue { + t.Fatalf("Expected hook to continue, given: %#v", action) + } + result := done(t) + + want := `ephemeral.test_instance.foo: Closing... +` + if got := result.Stdout(); got != want { + t.Fatalf("unexpected output\n got: %q\nwant: %q", got, want) + } +} + func TestTruncateId(t *testing.T) { testCases := []struct { Input string diff --git a/internal/command/views/init.go b/internal/command/views/init.go new file mode 100644 index 0000000000..cd91daa428 --- /dev/null +++ b/internal/command/views/init.go @@ -0,0 +1,378 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// The Init view is used for the init command. +type Init interface { + Diagnostics(diags tfdiags.Diagnostics) + Output(messageCode InitMessageCode, params ...any) + LogInitMessage(messageCode InitMessageCode, params ...any) + Log(message string, params ...any) + PrepareMessage(messageCode InitMessageCode, params ...any) string +} + +// NewInit returns Init implementation for the given ViewType. +func NewInit(vt arguments.ViewType, view *View) Init { + switch vt { + case arguments.ViewJSON: + return &InitJSON{ + view: NewJSONView(view), + } + case arguments.ViewHuman: + return &InitHuman{ + view: view, + } + default: + panic(fmt.Sprintf("unknown view type %v", vt)) + } +} + +// The InitHuman implementation renders human-readable text logs, suitable for +// a scrolling terminal. +type InitHuman struct { + view *View +} + +var _ Init = (*InitHuman)(nil) + +func (v *InitHuman) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} + +func (v *InitHuman) Output(messageCode InitMessageCode, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +func (v *InitHuman) LogInitMessage(messageCode InitMessageCode, params ...any) { + v.view.streams.Println(v.PrepareMessage(messageCode, params...)) +} + +// this implements log method for use by interfaces that need to log generic string messages, e.g used for logging in hook_module_install.go +func (v *InitHuman) Log(message string, params ...any) { + v.view.streams.Println(strings.TrimSpace(fmt.Sprintf(message, params...))) +} + +func (v *InitHuman) PrepareMessage(messageCode InitMessageCode, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return string(messageCode) + } + + if message.HumanValue == "" { + // no need to apply colorization if the message is empty + return message.HumanValue + } + + return v.view.colorize.Color(strings.TrimSpace(fmt.Sprintf(message.HumanValue, params...))) +} + +// The InitJSON implementation renders streaming JSON logs, suitable for +// integrating with other software. +type InitJSON struct { + view *JSONView +} + +var _ Init = (*InitJSON)(nil) + +func (v *InitJSON) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} + +func (v *InitJSON) Output(messageCode InitMessageCode, params ...any) { + // don't add empty messages to json output + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + + current_timestamp := time.Now().UTC().Format(time.RFC3339) + json_data := map[string]string{ + "@level": "info", + "@message": preppedMessage, + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "init_output", + "message_code": string(messageCode), + } + + init_output, _ := json.Marshal(json_data) + v.view.view.streams.Println(string(init_output)) +} + +func (v *InitJSON) LogInitMessage(messageCode InitMessageCode, params ...any) { + preppedMessage := v.PrepareMessage(messageCode, params...) + if preppedMessage == "" { + return + } + + v.view.Log(preppedMessage) +} + +// this implements log method for use by services that need to log generic string messages, e.g usage logging in hook_module_install.go +func (v *InitJSON) Log(message string, params ...any) { + v.view.Log(strings.TrimSpace(fmt.Sprintf(message, params...))) +} + +func (v *InitJSON) PrepareMessage(messageCode InitMessageCode, params ...any) string { + message, ok := MessageRegistry[messageCode] + if !ok { + // display the message code as fallback if not found in the message registry + return string(messageCode) + } + + return strings.TrimSpace(fmt.Sprintf(message.JSONValue, params...)) +} + +// InitMessage represents a message string in both json and human decorated text format. +type InitMessage struct { + HumanValue string + JSONValue string +} + +var MessageRegistry map[InitMessageCode]InitMessage = map[InitMessageCode]InitMessage{ + "copying_configuration_message": { + HumanValue: "[reset][bold]Copying configuration[reset] from %q...", + JSONValue: "Copying configuration from %q...", + }, + "output_init_empty_message": { + HumanValue: outputInitEmpty, + JSONValue: outputInitEmptyJSON, + }, + "output_init_success_message": { + HumanValue: outputInitSuccess, + JSONValue: outputInitSuccessJSON, + }, + "output_init_success_cloud_message": { + HumanValue: outputInitSuccessCloud, + JSONValue: outputInitSuccessCloudJSON, + }, + "output_init_success_cli_message": { + HumanValue: outputInitSuccessCLI, + JSONValue: outputInitSuccessCLI_JSON, + }, + "output_init_success_cli_cloud_message": { + HumanValue: outputInitSuccessCLICloud, + JSONValue: outputInitSuccessCLICloudJSON, + }, + "upgrading_modules_message": { + HumanValue: "[reset][bold]Upgrading modules...", + JSONValue: "Upgrading modules...", + }, + "initializing_modules_message": { + HumanValue: "[reset][bold]Initializing modules...", + JSONValue: "Initializing modules...", + }, + "initializing_terraform_cloud_message": { + HumanValue: "\n[reset][bold]Initializing HCP Terraform...", + JSONValue: "Initializing HCP Terraform...", + }, + "initializing_backend_message": { + HumanValue: "\n[reset][bold]Initializing the backend...", + JSONValue: "Initializing the backend...", + }, + "initializing_provider_plugin_message": { + HumanValue: "\n[reset][bold]Initializing provider plugins...", + JSONValue: "Initializing provider plugins...", + }, + "dependencies_lock_changes_info": { + HumanValue: dependenciesLockChangesInfo, + JSONValue: dependenciesLockChangesInfo, + }, + "lock_info": { + HumanValue: previousLockInfoHuman, + JSONValue: previousLockInfoJSON, + }, + "provider_already_installed_message": { + HumanValue: "- Using previously-installed %s v%s", + JSONValue: "%s v%s: Using previously-installed provider version", + }, + "built_in_provider_available_message": { + HumanValue: "- %s is built in to Terraform", + JSONValue: "%s is built in to Terraform", + }, + "reusing_previous_version_info": { + HumanValue: "- Reusing previous version of %s from the dependency lock file", + JSONValue: "%s: Reusing previous version from the dependency lock file", + }, + "finding_matching_version_message": { + HumanValue: "- Finding %s versions matching %q...", + JSONValue: "Finding matching versions for provider: %s, version_constraint: %q", + }, + "finding_latest_version_message": { + HumanValue: "- Finding latest version of %s...", + JSONValue: "%s: Finding latest version...", + }, + "using_provider_from_cache_dir_info": { + HumanValue: "- Using %s v%s from the shared cache directory", + JSONValue: "%s v%s: Using from the shared cache directory", + }, + "installing_provider_message": { + HumanValue: "- Installing %s v%s...", + JSONValue: "Installing provider version: %s v%s...", + }, + "key_id": { + HumanValue: ", key ID [reset][bold]%s[reset]", + JSONValue: "key_id: %s", + }, + "installed_provider_version_info": { + HumanValue: "- Installed %s v%s (%s%s)", + JSONValue: "Installed provider version: %s v%s (%s%s)", + }, + "partner_and_community_providers_message": { + HumanValue: partnerAndCommunityProvidersInfo, + JSONValue: partnerAndCommunityProvidersInfo, + }, + "init_config_error": { + HumanValue: errInitConfigError, + JSONValue: errInitConfigErrorJSON, + }, + "empty_message": { + HumanValue: "", + JSONValue: "", + }, +} + +type InitMessageCode string + +const ( + CopyingConfigurationMessage InitMessageCode = "copying_configuration_message" + EmptyMessage InitMessageCode = "empty_message" + OutputInitEmptyMessage InitMessageCode = "output_init_empty_message" + OutputInitSuccessMessage InitMessageCode = "output_init_success_message" + OutputInitSuccessCloudMessage InitMessageCode = "output_init_success_cloud_message" + OutputInitSuccessCLIMessage InitMessageCode = "output_init_success_cli_message" + OutputInitSuccessCLICloudMessage InitMessageCode = "output_init_success_cli_cloud_message" + UpgradingModulesMessage InitMessageCode = "upgrading_modules_message" + InitializingTerraformCloudMessage InitMessageCode = "initializing_terraform_cloud_message" + InitializingModulesMessage InitMessageCode = "initializing_modules_message" + InitializingBackendMessage InitMessageCode = "initializing_backend_message" + InitializingProviderPluginMessage InitMessageCode = "initializing_provider_plugin_message" + LockInfo InitMessageCode = "lock_info" + DependenciesLockChangesInfo InitMessageCode = "dependencies_lock_changes_info" + ProviderAlreadyInstalledMessage InitMessageCode = "provider_already_installed_message" + BuiltInProviderAvailableMessage InitMessageCode = "built_in_provider_available_message" + ReusingPreviousVersionInfo InitMessageCode = "reusing_previous_version_info" + FindingMatchingVersionMessage InitMessageCode = "finding_matching_version_message" + FindingLatestVersionMessage InitMessageCode = "finding_latest_version_message" + UsingProviderFromCacheDirInfo InitMessageCode = "using_provider_from_cache_dir_info" + InstallingProviderMessage InitMessageCode = "installing_provider_message" + KeyID InitMessageCode = "key_id" + InstalledProviderVersionInfo InitMessageCode = "installed_provider_version_info" + PartnerAndCommunityProvidersMessage InitMessageCode = "partner_and_community_providers_message" + InitConfigError InitMessageCode = "init_config_error" +) + +const outputInitEmpty = ` +[reset][bold]Terraform initialized in an empty directory![reset] + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitEmptyJSON = ` +Terraform initialized in an empty directory! + +The directory has no Terraform configuration files. You may begin working +with Terraform immediately by creating Terraform configuration files. +` + +const outputInitSuccess = ` +[reset][bold][green]Terraform has been successfully initialized![reset][green] +` + +const outputInitSuccessJSON = ` +Terraform has been successfully initialized! +` + +const outputInitSuccessCloud = ` +[reset][bold][green]HCP Terraform has been successfully initialized![reset][green] +` + +const outputInitSuccessCloudJSON = ` +HCP Terraform has been successfully initialized! +` + +const outputInitSuccessCLI = `[reset][green] +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +` + +const outputInitSuccessCLI_JSON = ` +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +` + +const outputInitSuccessCLICloud = `[reset][green] +You may now begin working with HCP Terraform. Try running "terraform plan" to +see any changes that are required for your infrastructure. + +If you ever set or change modules or Terraform Settings, run "terraform init" +again to reinitialize your working directory. +` + +const outputInitSuccessCLICloudJSON = ` +You may now begin working with HCP Terraform. Try running "terraform plan" to +see any changes that are required for your infrastructure. + +If you ever set or change modules or Terraform Settings, run "terraform init" +again to reinitialize your working directory. +` + +const previousLockInfoHuman = ` +Terraform has created a lock file [bold].terraform.lock.hcl[reset] to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future.` + +const previousLockInfoJSON = ` +Terraform has created a lock file .terraform.lock.hcl to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future.` + +const dependenciesLockChangesInfo = ` +Terraform has made some changes to the provider dependency selections recorded +in the .terraform.lock.hcl file. Review those changes and commit them to your +version control system if they represent changes you intended to make.` + +const partnerAndCommunityProvidersInfo = "\nPartner and community providers are signed by their developers.\n" + + "If you'd like to know more about provider signing, you can read about it here:\n" + + "https://developer.hashicorp.com/terraform/cli/plugins/signing" + +const errInitConfigError = ` +[reset]Terraform encountered problems during initialisation, including problems +with the configuration, described below. + +The Terraform configuration must be valid before initialization so that +Terraform can determine which modules and providers need to be installed. +` + +const errInitConfigErrorJSON = ` +Terraform encountered problems during initialisation, including problems +with the configuration, described below. + +The Terraform configuration must be valid before initialization so that +Terraform can determine which modules and providers need to be installed. +` diff --git a/internal/command/views/init_test.go b/internal/command/views/init_test.go new file mode 100644 index 0000000000..5017d71477 --- /dev/null +++ b/internal/command/views/init_test.go @@ -0,0 +1,329 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" + tfversion "github.com/hashicorp/terraform/version" +) + +func TestNewInit_jsonViewDiagnostics(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + diags := getTestDiags(t) + newInit.Diagnostics(diags) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "error", + "@message": "Error: Error selecting workspace", + "@module": "terraform.ui", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Error selecting workspace", + "detail": "Workspace random_pet does not exist", + }, + "type": "diagnostic", + }, + { + "@level": "error", + "@message": "Error: Unsupported backend type", + "@module": "terraform.ui", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Unsupported backend type", + "detail": "There is no explicit backend type named fake backend.", + }, + "type": "diagnostic", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) +} + +func TestNewInit_humanViewDiagnostics(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + diags := getTestDiags(t) + newInit.Diagnostics(diags) + + actual := done(t).All() + expected := "\nError: Error selecting workspace\n\nWorkspace random_pet does not exist\n\nError: Unsupported backend type\n\nThere is no explicit backend type named fake backend.\n" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } +} + +func TestNewInit_unsupportedViewDiagnostics(t *testing.T) { + defer func() { + r := recover() + if r == nil { + t.Fatalf("should panic with unsupported view type raw") + } else if r != "unknown view type raw" { + t.Fatalf("unexpected panic message: %v", r) + } + }() + + streams, done := terminal.StreamsForTesting(t) + defer done(t) + + NewInit(arguments.ViewRaw, NewView(streams).SetRunningInAutomation(true)) +} + +func getTestDiags(t *testing.T) tfdiags.Diagnostics { + t.Helper() + + var diags tfdiags.Diagnostics + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Error selecting workspace", + "Workspace random_pet does not exist", + ), + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported backend type", + Detail: "There is no explicit backend type named fake backend.", + Subject: nil, + }, + ) + + return diags +} + +func TestNewInit_jsonViewOutput(t *testing.T) { + t.Run("no param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + newInit.Output(InitializingProviderPluginMessage) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": "Initializing provider plugins...", + "message_code": "initializing_provider_plugin_message", + "@module": "terraform.ui", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) + + t.Run("single param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + packageName := "hashicorp/aws" + newInit.Output(FindingLatestVersionMessage, packageName) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": fmt.Sprintf("%s: Finding latest version...", packageName), + "@module": "terraform.ui", + "message_code": "finding_latest_version_message", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) + + t.Run("variable length params", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + var packageName, packageVersion = "hashicorp/aws", "3.0.0" + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": fmt.Sprintf("%s v%s: Using previously-installed provider version", packageName, packageVersion), + "@module": "terraform.ui", + "message_code": "provider_already_installed_message", + "type": "init_output", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) + }) +} + +func TestNewInit_jsonViewLog(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + newInit.LogInitMessage(InitializingProviderPluginMessage) + + version := tfversion.String() + want := []map[string]interface{}{ + { + "@level": "info", + "@message": fmt.Sprintf("Terraform %s", version), + "@module": "terraform.ui", + "terraform": version, + "type": "version", + "ui": JSON_UI_VERSION, + }, + { + "@level": "info", + "@message": "Initializing provider plugins...", + "@module": "terraform.ui", + "type": "log", + }, + } + + actual := done(t).Stdout() + testJSONViewOutputEqualsFull(t, actual, want) +} + +func TestNewInit_jsonViewPrepareMessage(t *testing.T) { + t.Run("existing message code", func(t *testing.T) { + streams, _ := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewJSON, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitJSON); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + want := "Initializing modules..." + + actual := newInit.PrepareMessage(InitializingModulesMessage) + if !cmp.Equal(want, actual) { + t.Errorf("unexpected output: %s", cmp.Diff(want, actual)) + } + }) +} + +func TestNewInit_humanViewOutput(t *testing.T) { + t.Run("no param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + newInit.Output(InitializingProviderPluginMessage) + + actual := done(t).All() + expected := "Initializing provider plugins..." + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) + + t.Run("single param", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + packageName := "hashicorp/aws" + newInit.Output(FindingLatestVersionMessage, packageName) + + actual := done(t).All() + expected := "Finding latest version of hashicorp/aws" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) + + t.Run("variable length params", func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + + newInit := NewInit(arguments.ViewHuman, NewView(streams).SetRunningInAutomation(true)) + if _, ok := newInit.(*InitHuman); !ok { + t.Fatalf("unexpected return type %t", newInit) + } + + var packageName, packageVersion = "hashicorp/aws", "3.0.0" + newInit.Output(ProviderAlreadyInstalledMessage, packageName, packageVersion) + + actual := done(t).All() + expected := "- Using previously-installed hashicorp/aws v3.0.0" + if !strings.Contains(actual, expected) { + t.Fatalf("expected output to contain: %s, but got %s", expected, actual) + } + }) +} diff --git a/internal/command/views/json/change.go b/internal/command/views/json/change.go index 21188bfaf5..32c0f529e4 100644 --- a/internal/command/views/json/change.go +++ b/internal/command/views/json/change.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( @@ -8,10 +11,24 @@ import ( func NewResourceInstanceChange(change *plans.ResourceInstanceChangeSrc) *ResourceInstanceChange { c := &ResourceInstanceChange{ - Resource: newResourceAddr(change.Addr), - Action: changeAction(change.Action), - Reason: changeReason(change.ActionReason), + Resource: newResourceAddr(change.Addr), + Action: changeAction(change.Action), + Reason: changeReason(change.ActionReason), + GeneratedConfig: change.GeneratedConfig, } + + // The order here matters, we want the moved action to take precedence over + // the import action. We're basically taking "the most recent action" as the + // primary action in the streamed logs. That is to say, that if a resource + // is imported and then moved in a single operation then the change for that + // resource will be reported as ActionMove while the Importing flag will + // still be set to true. + // + // Since both the moved and imported actions only overwrite a NoOp this + // behaviour is consistent across the other actions as well. Something that + // is imported and then updated, or moved and then updated, will have the + // ActionUpdate as the recognised action for the change. + if !change.Addr.Equal(change.PrevRunAddr) { if c.Action == ActionNoOp { c.Action = ActionMove @@ -19,6 +36,12 @@ func NewResourceInstanceChange(change *plans.ResourceInstanceChangeSrc) *Resourc pr := newResourceAddr(change.PrevRunAddr) c.PreviousResource = &pr } + if change.Importing != nil { + if c.Action == ActionNoOp { + c.Action = ActionImport + } + c.Importing = &Importing{ID: change.Importing.ID} + } return c } @@ -28,6 +51,8 @@ type ResourceInstanceChange struct { PreviousResource *ResourceAddr `json:"previous_resource,omitempty"` Action ChangeAction `json:"action"` Reason ChangeReason `json:"reason,omitempty"` + Importing *Importing `json:"importing,omitempty"` + GeneratedConfig string `json:"generated_config,omitempty"` } func (c *ResourceInstanceChange) String() string { @@ -39,11 +64,20 @@ type ChangeAction string const ( ActionNoOp ChangeAction = "noop" ActionMove ChangeAction = "move" + ActionForget ChangeAction = "remove" ActionCreate ChangeAction = "create" ActionRead ChangeAction = "read" ActionUpdate ChangeAction = "update" ActionReplace ChangeAction = "replace" ActionDelete ChangeAction = "delete" + ActionImport ChangeAction = "import" + + // While ephemeral resources do not represent a change + // or participate in the plan in the same way as the above + // we declare them here for convenience in helper functions. + ActionOpen ChangeAction = "open" + ActionRenew ChangeAction = "renew" + ActionClose ChangeAction = "close" ) func changeAction(action plans.Action) ChangeAction { @@ -56,10 +90,18 @@ func changeAction(action plans.Action) ChangeAction { return ActionRead case plans.Update: return ActionUpdate - case plans.DeleteThenCreate, plans.CreateThenDelete: + case plans.DeleteThenCreate, plans.CreateThenDelete, plans.CreateThenForget: return ActionReplace case plans.Delete: return ActionDelete + case plans.Forget: + return ActionForget + case plans.Open: + return ActionOpen + case plans.Renew: + return ActionRenew + case plans.Close: + return ActionClose default: return ActionNoOp } @@ -80,8 +122,10 @@ const ( ReasonDeleteBecauseCountIndex ChangeReason = "delete_because_count_index" ReasonDeleteBecauseEachKey ChangeReason = "delete_because_each_key" ReasonDeleteBecauseNoModule ChangeReason = "delete_because_no_module" + ReasonDeleteBecauseNoMoveTarget ChangeReason = "delete_because_no_move_target" ReasonReadBecauseConfigUnknown ChangeReason = "read_because_config_unknown" ReasonReadBecauseDependencyPending ChangeReason = "read_because_dependency_pending" + ReasonReadBecauseCheckNested ChangeReason = "read_because_check_nested" ) func changeReason(reason plans.ResourceInstanceChangeActionReason) ChangeReason { @@ -108,8 +152,12 @@ func changeReason(reason plans.ResourceInstanceChangeActionReason) ChangeReason return ReasonDeleteBecauseNoModule case plans.ResourceInstanceReadBecauseConfigUnknown: return ReasonReadBecauseConfigUnknown + case plans.ResourceInstanceDeleteBecauseNoMoveTarget: + return ReasonDeleteBecauseNoMoveTarget case plans.ResourceInstanceReadBecauseDependencyPending: return ReasonReadBecauseDependencyPending + case plans.ResourceInstanceReadBecauseCheckNested: + return ReasonReadBecauseCheckNested default: // This should never happen, but there's no good way to guarantee // exhaustive handling of the enum, so a generic fall back is better diff --git a/internal/command/views/json/change_summary.go b/internal/command/views/json/change_summary.go index 2b87f62e25..84ace7790a 100644 --- a/internal/command/views/json/change_summary.go +++ b/internal/command/views/json/change_summary.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import "fmt" @@ -13,20 +16,27 @@ const ( type ChangeSummary struct { Add int `json:"add"` Change int `json:"change"` + Import int `json:"import"` Remove int `json:"remove"` Operation Operation `json:"operation"` } // The summary strings for apply and plan are accidentally a public interface -// used by Terraform Cloud and Terraform Enterprise, so the exact formats of +// used by HCP Terraform and Terraform Enterprise, so the exact formats of // these strings are important. func (cs *ChangeSummary) String() string { switch cs.Operation { case OperationApplied: + if cs.Import > 0 { + return fmt.Sprintf("Apply complete! Resources: %d imported, %d added, %d changed, %d destroyed.", cs.Import, cs.Add, cs.Change, cs.Remove) + } return fmt.Sprintf("Apply complete! Resources: %d added, %d changed, %d destroyed.", cs.Add, cs.Change, cs.Remove) case OperationDestroyed: return fmt.Sprintf("Destroy complete! Resources: %d destroyed.", cs.Remove) case OperationPlanned: + if cs.Import > 0 { + return fmt.Sprintf("Plan: %d to import, %d to add, %d to change, %d to destroy.", cs.Import, cs.Add, cs.Change, cs.Remove) + } return fmt.Sprintf("Plan: %d to add, %d to change, %d to destroy.", cs.Add, cs.Change, cs.Remove) default: return fmt.Sprintf("%s: %d add, %d change, %d destroy", cs.Operation, cs.Add, cs.Change, cs.Remove) diff --git a/internal/command/views/json/diagnostic.go b/internal/command/views/json/diagnostic.go index 1175792c72..90f8459c23 100644 --- a/internal/command/views/json/diagnostic.go +++ b/internal/command/views/json/diagnostic.go @@ -1,8 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( "bufio" - "bytes" "fmt" "sort" "strings" @@ -11,9 +13,10 @@ import ( "github.com/hashicorp/hcl/v2/hcled" "github.com/hashicorp/hcl/v2/hclparse" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // These severities map to the tfdiags.Severity values, plus an explicit @@ -100,6 +103,11 @@ type DiagnosticSnippet struct { // FunctionCall is information about a function call whose failure is // being reported by this diagnostic, if any. FunctionCall *DiagnosticFunctionCall `json:"function_call,omitempty"` + + // TestAssertionExpr is information derived from a diagnostic that is caused + // by a failed run assertion. This field is only populated when the assertion + // is a binary expression, i.e `a operand b``. + TestAssertionExpr *DiagnosticTestBinaryExpr `json:"test_assertion_expr,omitempty"` } // DiagnosticExpressionValue represents an HCL traversal string (e.g. @@ -126,6 +134,17 @@ type DiagnosticFunctionCall struct { Signature *Function `json:"signature,omitempty"` } +// DiagnosticTestBinaryExpr represents a failed test assertion diagnostic +// caused by a binary expression. It includes the left-hand side (LHS) and +// right-hand side (RHS) values of the binary expression, as well as a warning +// message if there is a potential issue with the values being compared. +type DiagnosticTestBinaryExpr struct { + LHS string `json:"lhs"` + RHS string `json:"rhs"` + Warning string `json:"warning"` + ShowVerbose bool `json:"show_verbose"` +} + // NewDiagnostic takes a tfdiags.Diagnostic and a map of configuration sources, // and returns a Diagnostic struct. func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnostic { @@ -272,7 +291,9 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost values := make([]DiagnosticExpressionValue, 0, len(vars)) seen := make(map[string]struct{}, len(vars)) includeUnknown := tfdiags.DiagnosticCausedByUnknown(diag) + includeEphemeral := tfdiags.DiagnosticCausedByEphemeral(diag) includeSensitive := tfdiags.DiagnosticCausedBySensitive(diag) + testDiag := tfdiags.ExtraInfo[tfdiags.DiagnosticExtraCausedByTestFailure](diag) Traversals: for _, traversal := range vars { for len(traversal) > 1 { @@ -285,14 +306,57 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost continue } - traversalStr := traversalStr(traversal) + traversalStr := tfdiags.TraversalStr(traversal) if _, exists := seen[traversalStr]; exists { continue Traversals // don't show duplicates when the same variable is referenced multiple times } value := DiagnosticExpressionValue{ Traversal: traversalStr, } + + // If the diagnostic is caused by a failed run assertion, + // we'll redact sensitive and ephemeral values within traversals, but format + // the values in a more human-readable way than the general case. + // If the value is unknown, we'll leave it to the general case to handle. + if testDiag != nil && val.IsKnown() { + valBuf, err := tfdiags.FormatValueStr(val) + if err != nil { + panic(err) + } + value.Statement = fmt.Sprintf("is %s", valBuf) + values = append(values, value) + seen[traversalStr] = struct{}{} + continue Traversals + } + + // We'll skip any value that has a mark that we don't + // know how to handle, because in that case we can't + // know what that mark is intended to represent and so + // must be conservative. + _, valMarks := val.Unmark() + for mark := range valMarks { + switch mark { + case marks.Sensitive, marks.Ephemeral: + // These are handled below + continue + default: + // All other marks are unhandled, so we'll + // skip this traversal entirely. + continue Traversals + } + } switch { + case val.HasMark(marks.Sensitive) && val.HasMark(marks.Ephemeral): + // We only mention the combination of sensitive and ephemeral + // values if the diagnostic we're rendering is explicitly + // marked as being caused by sensitive and ephemeral values, + // because otherwise readers tend to be misled into thinking the error + // is caused by the sensitive value even when it isn't. + if !includeSensitive || !includeEphemeral { + continue Traversals + } + + value.Statement = "has an ephemeral, sensitive value" case val.HasMark(marks.Sensitive): // We only mention a sensitive value if the diagnostic // we're rendering is explicitly marked as being @@ -306,6 +370,11 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost // in order to minimize the chance of giving away // whatever was sensitive about it. value.Statement = "has a sensitive value" + case val.HasMark(marks.Ephemeral): + if !includeEphemeral { + continue Traversals + } + value.Statement = "has an ephemeral value" case !val.IsKnown(): // We'll avoid saying anything about unknown or // "known after apply" unless the diagnostic is @@ -315,7 +384,27 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost // unknown value even when it isn't. if ty := val.Type(); ty != cty.DynamicPseudoType { if includeUnknown { - value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName()) + switch { + case ty.IsCollectionType(): + valRng := val.Range() + minLen := valRng.LengthLowerBound() + maxLen := valRng.LengthUpperBound() + const maxLimit = 1024 // (upper limit is just an arbitrary value to avoid showing distracting large numbers in the UI) + switch { + case minLen == maxLen: + value.Statement = fmt.Sprintf("is a %s of length %d, known only after apply", ty.FriendlyName(), minLen) + case minLen != 0 && maxLen <= maxLimit: + value.Statement = fmt.Sprintf("is a %s with between %d and %d elements, known only after apply", ty.FriendlyName(), minLen, maxLen) + case minLen != 0: + value.Statement = fmt.Sprintf("is a %s with at least %d elements, known only after apply", ty.FriendlyName(), minLen) + case maxLen <= maxLimit: + value.Statement = fmt.Sprintf("is a %s with up to %d elements, known only after apply", ty.FriendlyName(), maxLen) + default: + value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName()) + } + default: + value.Statement = fmt.Sprintf("is a %s, known only after apply", ty.FriendlyName()) + } } else { value.Statement = fmt.Sprintf("is a %s", ty.FriendlyName()) } @@ -326,7 +415,7 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost value.Statement = "will be known only after apply" } default: - value.Statement = fmt.Sprintf("is %s", compactValueStr(val)) + value.Statement = fmt.Sprintf("is %s", tfdiags.CompactValueStr(val)) } values = append(values, value) seen[traversalStr] = struct{}{} @@ -335,6 +424,7 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost sort.Slice(values, func(i, j int) bool { return values[i].Traversal < values[j].Traversal }) + diagnostic.Snippet.Values = values if callInfo := tfdiags.ExtraInfo[hclsyntax.FunctionCallDiagExtra](diag); callInfo != nil && callInfo.CalledFunctionName() != "" { @@ -352,6 +442,13 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost diagnostic.Snippet.FunctionCall = callInfo } + if testDiag != nil { + // If the test assertion is a binary expression, we'll include the human-readable + // formatted LHS and RHS values in the diagnostic snippet. + diagnostic.Snippet.TestAssertionExpr = formatRunBinaryDiag(ctx, fromExpr.Expression) + diagnostic.Snippet.TestAssertionExpr.ShowVerbose = testDiag.IsTestVerboseMode() + } + } } @@ -360,6 +457,36 @@ func NewDiagnostic(diag tfdiags.Diagnostic, sources map[string][]byte) *Diagnost return diagnostic } +// formatRunBinaryDiag formats the binary expression that caused the failed run diagnostic. +// The LHS and RHS values are formatted in a more human-readable way, redacting +// sensitive and ephemeral values only for the exact values that hold the mark(s). +func formatRunBinaryDiag(ctx *hcl.EvalContext, expr hcl.Expression) *DiagnosticTestBinaryExpr { + bExpr, ok := expr.(*hclsyntax.BinaryOpExpr) + if !ok { + return nil + } + // The expression has already been evaluated and failed, so we can ignore the diags here. + lhs, _ := bExpr.LHS.Value(ctx) + rhs, _ := bExpr.RHS.Value(ctx) + + lhsStr, err := tfdiags.FormatValueStr(lhs) + if err != nil { + panic(err) + } + rhsStr, err := tfdiags.FormatValueStr(rhs) + if err != nil { + panic(err) + } + + ret := &DiagnosticTestBinaryExpr{LHS: lhsStr, RHS: rhsStr} + + // The types do not match. We don't diff them. + if !lhs.Type().Equals(rhs.Type()) { + ret.Warning = "LHS and RHS values are of different types" + } + return ret +} + func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) { filename := rng.Filename offset := rng.Start.Byte @@ -381,110 +508,3 @@ func parseRange(src []byte, rng hcl.Range) (*hcl.File, int) { return file, offset } - -// compactValueStr produces a compact, single-line summary of a given value -// that is suitable for display in the UI. -// -// For primitives it returns a full representation, while for more complex -// types it instead summarizes the type, size, etc to produce something -// that is hopefully still somewhat useful but not as verbose as a rendering -// of the entire data structure. -func compactValueStr(val cty.Value) string { - // This is a specialized subset of value rendering tailored to producing - // helpful but concise messages in diagnostics. It is not comprehensive - // nor intended to be used for other purposes. - - if val.HasMark(marks.Sensitive) { - // We check this in here just to make sure, but note that the caller - // of compactValueStr ought to have already checked this and skipped - // calling into compactValueStr anyway, so this shouldn't actually - // be reachable. - return "(sensitive value)" - } - - // WARNING: We've only checked that the value isn't sensitive _shallowly_ - // here, and so we must never show any element values from complex types - // in here. However, it's fine to show map keys and attribute names because - // those are never sensitive in isolation: the entire value would be - // sensitive in that case. - - ty := val.Type() - switch { - case val.IsNull(): - return "null" - case !val.IsKnown(): - // Should never happen here because we should filter before we get - // in here, but we'll do something reasonable rather than panic. - return "(not yet known)" - case ty == cty.Bool: - if val.True() { - return "true" - } - return "false" - case ty == cty.Number: - bf := val.AsBigFloat() - return bf.Text('g', 10) - case ty == cty.String: - // Go string syntax is not exactly the same as HCL native string syntax, - // but we'll accept the minor edge-cases where this is different here - // for now, just to get something reasonable here. - return fmt.Sprintf("%q", val.AsString()) - case ty.IsCollectionType() || ty.IsTupleType(): - l := val.LengthInt() - switch l { - case 0: - return "empty " + ty.FriendlyName() - case 1: - return ty.FriendlyName() + " with 1 element" - default: - return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l) - } - case ty.IsObjectType(): - atys := ty.AttributeTypes() - l := len(atys) - switch l { - case 0: - return "object with no attributes" - case 1: - var name string - for k := range atys { - name = k - } - return fmt.Sprintf("object with 1 attribute %q", name) - default: - return fmt.Sprintf("object with %d attributes", l) - } - default: - return ty.FriendlyName() - } -} - -// traversalStr produces a representation of an HCL traversal that is compact, -// resembles HCL native syntax, and is suitable for display in the UI. -func traversalStr(traversal hcl.Traversal) string { - // This is a specialized subset of traversal rendering tailored to - // producing helpful contextual messages in diagnostics. It is not - // comprehensive nor intended to be used for other purposes. - - var buf bytes.Buffer - for _, step := range traversal { - switch tStep := step.(type) { - case hcl.TraverseRoot: - buf.WriteString(tStep.Name) - case hcl.TraverseAttr: - buf.WriteByte('.') - buf.WriteString(tStep.Name) - case hcl.TraverseIndex: - buf.WriteByte('[') - if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() { - buf.WriteString(compactValueStr(tStep.Key)) - } else { - // We'll just use a placeholder for more complex values, - // since otherwise our result could grow ridiculously long. - buf.WriteString("...") - } - buf.WriteByte(']') - } - } - return buf.String() -} diff --git a/internal/command/views/json/diagnostic_test.go b/internal/command/views/json/diagnostic_test.go index 422dade9b3..f4a32ca89e 100644 --- a/internal/command/views/json/diagnostic_test.go +++ b/internal/command/views/json/diagnostic_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( diff --git a/internal/command/views/json/function.go b/internal/command/views/json/function.go index f36986dae0..6bdf8d048d 100644 --- a/internal/command/views/json/function.go +++ b/internal/command/views/json/function.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( diff --git a/internal/command/views/json/function_test.go b/internal/command/views/json/function_test.go index 48d4a13233..acc7563475 100644 --- a/internal/command/views/json/function_test.go +++ b/internal/command/views/json/function_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( diff --git a/internal/command/views/json/hook.go b/internal/command/views/json/hook.go index 142a4d1fd1..d4fc8aa036 100644 --- a/internal/command/views/json/hook.go +++ b/internal/command/views/json/hook.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( @@ -13,22 +16,23 @@ type Hook interface { String() string } -// ApplyStart: triggered by PreApply hook -type applyStart struct { +// operationStart: triggered by Pre{Apply,EphemeralOp} hook +type operationStart struct { Resource ResourceAddr `json:"resource"` Action ChangeAction `json:"action"` IDKey string `json:"id_key,omitempty"` IDValue string `json:"id_value,omitempty"` actionVerb string + msgType MessageType } -var _ Hook = (*applyStart)(nil) +var _ Hook = (*operationStart)(nil) -func (h *applyStart) HookType() MessageType { - return MessageApplyStart +func (h *operationStart) HookType() MessageType { + return h.msgType } -func (h *applyStart) String() string { +func (h *operationStart) String() string { var id string if h.IDKey != "" && h.IDValue != "" { id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) @@ -37,49 +41,74 @@ func (h *applyStart) String() string { } func NewApplyStart(addr addrs.AbsResourceInstance, action plans.Action, idKey string, idValue string) Hook { - hook := &applyStart{ + hook := &operationStart{ Resource: newResourceAddr(addr), Action: changeAction(action), IDKey: idKey, IDValue: idValue, actionVerb: startActionVerb(action), + msgType: MessageApplyStart, } return hook } -// ApplyProgress: currently triggered by a timer started on PreApply. In +func NewEphemeralOpStart(addr addrs.AbsResourceInstance, action plans.Action) Hook { + hook := &operationStart{ + Resource: newResourceAddr(addr), + Action: changeAction(action), + actionVerb: startActionVerb(action), + msgType: MessageEphemeralOpStart, + } + + return hook +} + +// operationProgress: currently triggered by a timer started on Pre{Apply,EphemeralOp}. In // future, this might also be triggered by provider progress reporting. -type applyProgress struct { +type operationProgress struct { Resource ResourceAddr `json:"resource"` Action ChangeAction `json:"action"` Elapsed float64 `json:"elapsed_seconds"` actionVerb string elapsed time.Duration + msgType MessageType } -var _ Hook = (*applyProgress)(nil) +var _ Hook = (*operationProgress)(nil) -func (h *applyProgress) HookType() MessageType { - return MessageApplyProgress +func (h *operationProgress) HookType() MessageType { + return h.msgType } -func (h *applyProgress) String() string { +func (h *operationProgress) String() string { return fmt.Sprintf("%s: Still %s... [%s elapsed]", h.Resource.Addr, h.actionVerb, h.elapsed) } func NewApplyProgress(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { - return &applyProgress{ + return &operationProgress{ Resource: newResourceAddr(addr), Action: changeAction(action), Elapsed: elapsed.Seconds(), actionVerb: progressActionVerb(action), elapsed: elapsed, + msgType: MessageApplyProgress, } } -// ApplyComplete: triggered by PostApply hook -type applyComplete struct { +func NewEphemeralOpProgress(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { + return &operationProgress{ + Resource: newResourceAddr(addr), + Action: changeAction(action), + Elapsed: elapsed.Seconds(), + actionVerb: progressActionVerb(action), + elapsed: elapsed, + msgType: MessageEphemeralOpProgress, + } +} + +// operationComplete: triggered by PostApply hook +type operationComplete struct { Resource ResourceAddr `json:"resource"` Action ChangeAction `json:"action"` IDKey string `json:"id_key,omitempty"` @@ -87,15 +116,16 @@ type applyComplete struct { Elapsed float64 `json:"elapsed_seconds"` actionNoun string elapsed time.Duration + msgType MessageType } -var _ Hook = (*applyComplete)(nil) +var _ Hook = (*operationComplete)(nil) -func (h *applyComplete) HookType() MessageType { - return MessageApplyComplete +func (h *operationComplete) HookType() MessageType { + return h.msgType } -func (h *applyComplete) String() string { +func (h *operationComplete) String() string { var id string if h.IDKey != "" && h.IDValue != "" { id = fmt.Sprintf(" [%s=%s]", h.IDKey, h.IDValue) @@ -104,7 +134,7 @@ func (h *applyComplete) String() string { } func NewApplyComplete(addr addrs.AbsResourceInstance, action plans.Action, idKey, idValue string, elapsed time.Duration) Hook { - return &applyComplete{ + return &operationComplete{ Resource: newResourceAddr(addr), Action: changeAction(action), IDKey: idKey, @@ -112,36 +142,61 @@ func NewApplyComplete(addr addrs.AbsResourceInstance, action plans.Action, idKey Elapsed: elapsed.Seconds(), actionNoun: actionNoun(action), elapsed: elapsed, + msgType: MessageApplyComplete, } } -// ApplyErrored: triggered by PostApply hook on failure. This will be followed -// by diagnostics when the apply finishes. -type applyErrored struct { - Resource ResourceAddr `json:"resource"` - Action ChangeAction `json:"action"` - Elapsed float64 `json:"elapsed_seconds"` - actionNoun string - elapsed time.Duration -} - -var _ Hook = (*applyErrored)(nil) - -func (h *applyErrored) HookType() MessageType { - return MessageApplyErrored -} - -func (h *applyErrored) String() string { - return fmt.Sprintf("%s: %s errored after %s", h.Resource.Addr, h.actionNoun, h.elapsed) -} - -func NewApplyErrored(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { - return &applyErrored{ +func NewEphemeralOpComplete(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { + return &operationComplete{ Resource: newResourceAddr(addr), Action: changeAction(action), Elapsed: elapsed.Seconds(), actionNoun: actionNoun(action), elapsed: elapsed, + msgType: MessageEphemeralOpComplete, + } +} + +// operationErrored: triggered by PostApply hook on failure. This will be followed +// by diagnostics when the apply finishes. +type operationErrored struct { + Resource ResourceAddr `json:"resource"` + Action ChangeAction `json:"action"` + Elapsed float64 `json:"elapsed_seconds"` + actionNoun string + elapsed time.Duration + msgType MessageType +} + +var _ Hook = (*operationErrored)(nil) + +func (h *operationErrored) HookType() MessageType { + return h.msgType +} + +func (h *operationErrored) String() string { + return fmt.Sprintf("%s: %s errored after %s", h.Resource.Addr, h.actionNoun, h.elapsed) +} + +func NewApplyErrored(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { + return &operationErrored{ + Resource: newResourceAddr(addr), + Action: changeAction(action), + Elapsed: elapsed.Seconds(), + actionNoun: actionNoun(action), + elapsed: elapsed, + msgType: MessageApplyErrored, + } +} + +func NewEphemeralOpErrored(addr addrs.AbsResourceInstance, action plans.Action, elapsed time.Duration) Hook { + return &operationErrored{ + Resource: newResourceAddr(addr), + Action: changeAction(action), + Elapsed: elapsed.Seconds(), + actionNoun: actionNoun(action), + elapsed: elapsed, + msgType: MessageEphemeralOpErrored, } } @@ -310,10 +365,18 @@ func startActionVerb(action plans.Action) string { return "Destroying" case plans.Read: return "Refreshing" - case plans.CreateThenDelete, plans.DeleteThenCreate: + case plans.CreateThenDelete, plans.DeleteThenCreate, plans.CreateThenForget: // This is not currently possible to reach, as we receive separate // passes for create and delete return "Replacing" + case plans.Forget: + return "Removing" + case plans.Open: + return "Opening" + case plans.Renew: + return "Renewing" + case plans.Close: + return "Closing" case plans.NoOp: // This should never be possible: a no-op planned change should not // be applied. We'll fall back to "Applying". @@ -336,10 +399,21 @@ func progressActionVerb(action plans.Action) string { return "destroying" case plans.Read: return "refreshing" - case plans.CreateThenDelete, plans.DeleteThenCreate: + case plans.CreateThenDelete, plans.CreateThenForget, plans.DeleteThenCreate: // This is not currently possible to reach, as we receive separate // passes for create and delete return "replacing" + case plans.Open: + return "opening" + case plans.Renew: + return "renewing" + case plans.Close: + return "closing" + case plans.Forget: + // Removing a resource from state should not take very long. Fall back + // to "applying" just in case, since the terminology "forgetting" is + // meant to be internal to Terraform. + fallthrough case plans.NoOp: // This should never be possible: a no-op planned change should not // be applied. We'll fall back to "applying". @@ -350,7 +424,7 @@ func progressActionVerb(action plans.Action) string { } // Convert the subset of plans.Action values we expect to receive into a -// noun for the applyComplete and applyErrored hook messages. This will be +// noun for the operationComplete and operationErrored hook messages. This will be // combined into a phrase like "Creation complete after 1m4s". func actionNoun(action plans.Action) string { switch action { @@ -362,10 +436,18 @@ func actionNoun(action plans.Action) string { return "Destruction" case plans.Read: return "Refresh" - case plans.CreateThenDelete, plans.DeleteThenCreate: + case plans.CreateThenDelete, plans.DeleteThenCreate, plans.CreateThenForget: // This is not currently possible to reach, as we receive separate // passes for create and delete return "Replacement" + case plans.Forget: + return "Removal" + case plans.Open: + return "Opening" + case plans.Renew: + return "Renewal" + case plans.Close: + return "Closing" case plans.NoOp: // This should never be possible: a no-op planned change should not // be applied. We'll fall back to "Apply". diff --git a/internal/command/views/json/importing.go b/internal/command/views/json/importing.go new file mode 100644 index 0000000000..deadf3e477 --- /dev/null +++ b/internal/command/views/json/importing.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package json + +// Importing contains metadata about a resource change that includes an import +// action. +// +// Every field in here should be treated as optional as future versions do not +// make a guarantee that they will retain the format of this change. +// +// Consumers should be capable of rendering/parsing the Importing struct even +// if it does not have the ID field set. +type Importing struct { + ID string `json:"id,omitempty"` +} diff --git a/internal/command/views/json/message_types.go b/internal/command/views/json/message_types.go index 5d19705d0c..3c3ec299bb 100644 --- a/internal/command/views/json/message_types.go +++ b/internal/command/views/json/message_types.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json type MessageType string @@ -25,4 +28,22 @@ const ( MessageProvisionErrored MessageType = "provision_errored" MessageRefreshStart MessageType = "refresh_start" MessageRefreshComplete MessageType = "refresh_complete" + + // Ephemeral operation messages + MessageEphemeralOpStart MessageType = "ephemeral_op_start" + MessageEphemeralOpProgress MessageType = "ephemeral_op_progress" + MessageEphemeralOpComplete MessageType = "ephemeral_op_complete" + MessageEphemeralOpErrored MessageType = "ephemeral_op_errored" + + // Test messages + MessageTestAbstract MessageType = "test_abstract" + MessageTestFile MessageType = "test_file" + MessageTestRun MessageType = "test_run" + MessageTestPlan MessageType = "test_plan" + MessageTestState MessageType = "test_state" + MessageTestSummary MessageType = "test_summary" + MessageTestCleanup MessageType = "test_cleanup" + MessageTestInterrupt MessageType = "test_interrupt" + MessageTestStatus MessageType = "test_status" + MessageTestRetry MessageType = "test_retry" ) diff --git a/internal/command/views/json/output.go b/internal/command/views/json/output.go index 05070984af..7a194859bb 100644 --- a/internal/command/views/json/output.go +++ b/internal/command/views/json/output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( @@ -42,10 +45,15 @@ func OutputsFromMap(outputValues map[string]*states.OutputValue) (Outputs, tfdia return nil, diags } + var redactedValue json.RawMessage + if !ov.Sensitive { + redactedValue = json.RawMessage(value) + } + outputs[name] = Output{ Sensitive: ov.Sensitive, Type: json.RawMessage(valueType), - Value: json.RawMessage(value), + Value: redactedValue, } } diff --git a/internal/command/views/json/output_test.go b/internal/command/views/json/output_test.go index e3e9495b8c..e90422b2d2 100644 --- a/internal/command/views/json/output_test.go +++ b/internal/command/views/json/output_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( @@ -52,12 +55,10 @@ func TestOutputsFromMap(t *testing.T) { "beep": { Sensitive: true, Type: json.RawMessage(`"string"`), - Value: json.RawMessage(`"horse-battery"`), }, "blorp": { Sensitive: true, Type: json.RawMessage(`["object",{"a":["object",{"b":["object",{"c":"string"}]}]}]`), - Value: json.RawMessage(`{"a":{"b":{"c":"oh, hi"}}}`), }, "honk": { Sensitive: false, diff --git a/internal/command/views/json/resource_addr.go b/internal/command/views/json/resource_addr.go index 27ff502a2c..e21332bf81 100644 --- a/internal/command/views/json/resource_addr.go +++ b/internal/command/views/json/resource_addr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package json import ( diff --git a/internal/command/views/json/test.go b/internal/command/views/json/test.go new file mode 100644 index 0000000000..1afe4e4c05 --- /dev/null +++ b/internal/command/views/json/test.go @@ -0,0 +1,66 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package json + +import ( + "strings" + + "github.com/hashicorp/terraform/internal/moduletest" +) + +type TestSuiteAbstract map[string][]string + +type TestStatus string + +type TestProgress string + +type TestFileStatus struct { + Path string `json:"path"` + Progress TestProgress `json:"progress"` + Status TestStatus `json:"status,omitempty"` +} + +type TestRunStatus struct { + Path string `json:"path"` + Run string `json:"run"` + Progress TestProgress `json:"progress"` + Elapsed *int64 `json:"elapsed,omitempty"` + Status TestStatus `json:"status,omitempty"` +} + +type TestSuiteSummary struct { + Status TestStatus `json:"status"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Errored int `json:"errored"` + Skipped int `json:"skipped"` +} + +type TestFileCleanup struct { + FailedResources []TestFailedResource `json:"failed_resources"` +} + +type TestFailedResource struct { + Instance string `json:"instance"` + DeposedKey string `json:"deposed_key,omitempty"` +} + +type TestFatalInterrupt struct { + State []TestFailedResource `json:"state,omitempty"` + States map[string][]TestFailedResource `json:"states,omitempty"` + Planned []string `json:"planned,omitempty"` +} + +type TestStatusUpdate struct { + Status string `json:"status"` + Duration float64 `json:"duration"` +} + +func ToTestStatus(status moduletest.Status) TestStatus { + return TestStatus(strings.ToLower(status.String())) +} + +func ToTestProgress(progress moduletest.Progress) TestProgress { + return TestProgress(strings.ToLower(progress.String())) +} diff --git a/internal/command/views/json_view.go b/internal/command/views/json_view.go index f92036d5c0..085d0e703b 100644 --- a/internal/command/views/json_view.go +++ b/internal/command/views/json_view.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -5,6 +8,7 @@ import ( "fmt" "github.com/hashicorp/go-hclog" + "github.com/hashicorp/terraform/internal/command/views/json" "github.com/hashicorp/terraform/internal/tfdiags" tfversion "github.com/hashicorp/terraform/version" @@ -13,7 +17,7 @@ import ( // This version describes the schema of JSON UI messages. This version must be // updated after making any changes to this view, the jsonHook, or any of the // command/views/json package. -const JSON_UI_VERSION = "1.0" +const JSON_UI_VERSION = "1.2" func NewJSONView(view *View) *JSONView { log := hclog.New(&hclog.LoggerOptions{ @@ -66,23 +70,19 @@ func (v *JSONView) StateDump(state string) { ) } -func (v *JSONView) Diagnostics(diags tfdiags.Diagnostics) { +func (v *JSONView) Diagnostics(diags tfdiags.Diagnostics, metadata ...interface{}) { sources := v.view.configSources() for _, diag := range diags { diagnostic := json.NewDiagnostic(diag, sources) + + args := []interface{}{"type", json.MessageDiagnostic, "diagnostic", diagnostic} + args = append(args, metadata...) + switch diag.Severity() { case tfdiags.Warning: - v.log.Warn( - fmt.Sprintf("Warning: %s", diag.Description().Summary), - "type", json.MessageDiagnostic, - "diagnostic", diagnostic, - ) + v.log.Warn(fmt.Sprintf("Warning: %s", diag.Description().Summary), args...) default: - v.log.Error( - fmt.Sprintf("Error: %s", diag.Description().Summary), - "type", json.MessageDiagnostic, - "diagnostic", diagnostic, - ) + v.log.Error(fmt.Sprintf("Error: %s", diag.Description().Summary), args...) } } } diff --git a/internal/command/views/json_view_test.go b/internal/command/views/json_view_test.go index 6bb5c49132..ac12410f71 100644 --- a/internal/command/views/json_view_test.go +++ b/internal/command/views/json_view_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -8,6 +11,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" viewsjson "github.com/hashicorp/terraform/internal/command/views/json" "github.com/hashicorp/terraform/internal/plans" @@ -101,6 +105,53 @@ func TestJSONView_Diagnostics(t *testing.T) { testJSONViewOutputEquals(t, done(t).Stdout(), want) } +func TestJSONView_DiagnosticsWithMetadata(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + jv := NewJSONView(NewView(streams)) + + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + `Improper use of "less"`, + `You probably mean "10 buckets or fewer"`, + )) + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unusually stripey cat detected", + "Are you sure this random_pet isn't a cheetah?", + )) + + jv.Diagnostics(diags, "@meta", "extra_info") + + want := []map[string]interface{}{ + { + "@level": "warn", + "@message": `Warning: Improper use of "less"`, + "@module": "terraform.ui", + "type": "diagnostic", + "diagnostic": map[string]interface{}{ + "severity": "warning", + "summary": `Improper use of "less"`, + "detail": `You probably mean "10 buckets or fewer"`, + }, + "@meta": "extra_info", + }, + { + "@level": "error", + "@message": "Error: Unusually stripey cat detected", + "@module": "terraform.ui", + "type": "diagnostic", + "diagnostic": map[string]interface{}{ + "severity": "error", + "summary": "Unusually stripey cat detected", + "detail": "Are you sure this random_pet isn't a cheetah?", + }, + "@meta": "extra_info", + }, + } + testJSONViewOutputEquals(t, done(t).Stdout(), want) +} + func TestJSONView_PlannedChange(t *testing.T) { streams, done := terminal.StreamsForTesting(t) jv := NewJSONView(NewView(streams)) @@ -202,6 +253,7 @@ func TestJSONView_ChangeSummary(t *testing.T) { "type": "change_summary", "changes": map[string]interface{}{ "add": float64(1), + "import": float64(0), "change": float64(2), "remove": float64(3), "operation": "apply", @@ -211,6 +263,36 @@ func TestJSONView_ChangeSummary(t *testing.T) { testJSONViewOutputEquals(t, done(t).Stdout(), want) } +func TestJSONView_ChangeSummaryWithImport(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + jv := NewJSONView(NewView(streams)) + + jv.ChangeSummary(&viewsjson.ChangeSummary{ + Add: 1, + Change: 2, + Remove: 3, + Import: 1, + Operation: viewsjson.OperationApplied, + }) + + want := []map[string]interface{}{ + { + "@level": "info", + "@message": "Apply complete! Resources: 1 imported, 1 added, 2 changed, 3 destroyed.", + "@module": "terraform.ui", + "type": "change_summary", + "changes": map[string]interface{}{ + "add": float64(1), + "change": float64(2), + "remove": float64(3), + "import": float64(1), + "operation": "apply", + }, + }, + } + testJSONViewOutputEquals(t, done(t).Stdout(), want) +} + func TestJSONView_Hook(t *testing.T) { streams, done := terminal.StreamsForTesting(t) jv := NewJSONView(NewView(streams)) @@ -295,7 +377,7 @@ func TestJSONView_Outputs(t *testing.T) { // against a slice of structs representing the desired log messages. It // verifies that the output of JSONView is in JSON log format, one message per // line. -func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}) { +func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string]interface{}, options ...cmp.Option) { t.Helper() // Remove final trailing newline @@ -328,12 +410,12 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string delete(gotStruct, "@timestamp") // Verify the timestamp format - if _, err := time.Parse("2006-01-02T15:04:05.000000Z07:00", timestamp.(string)); err != nil { + if _, err := time.Parse(time.RFC3339, timestamp.(string)); err != nil { t.Errorf("error parsing timestamp on line %d: %s", i, err) } } - if !cmp.Equal(wantStruct, gotStruct) { + if !cmp.Equal(wantStruct, gotStruct, options...) { t.Errorf("unexpected output on line %d:\n%s", i, cmp.Diff(wantStruct, gotStruct)) } } @@ -341,7 +423,7 @@ func testJSONViewOutputEqualsFull(t *testing.T, output string, want []map[string // testJSONViewOutputEquals skips the first line of output, since it ought to // be a version message that we don't care about for most of our tests. -func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}) { +func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]interface{}, options ...cmp.Option) { t.Helper() // Remove up to the first newline @@ -349,5 +431,5 @@ func testJSONViewOutputEquals(t *testing.T, output string, want []map[string]int if index >= 0 { output = output[index+1:] } - testJSONViewOutputEqualsFull(t, output, want) + testJSONViewOutputEqualsFull(t, output, want, options...) } diff --git a/internal/command/views/modules.go b/internal/command/views/modules.go new file mode 100644 index 0000000000..8fa3cd8a19 --- /dev/null +++ b/internal/command/views/modules.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package views + +import ( + encJson "encoding/json" + "fmt" + "sort" + + "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/moduleref" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/xlab/treeprint" +) + +type Modules interface { + // Display renders the list of module entries. + Display(manifest moduleref.Manifest) int + + // Diagnostics renders early diagnostics, resulting from argument parsing. + Diagnostics(diags tfdiags.Diagnostics) +} + +func NewModules(vt arguments.ViewType, view *View) Modules { + switch vt { + case arguments.ViewJSON: + return &ModulesJSON{view: view} + case arguments.ViewHuman: + return &ModulesHuman{view: view} + default: + panic(fmt.Sprintf("unknown view type %v", vt)) + } +} + +type ModulesHuman struct { + view *View +} + +var _ Modules = (*ModulesHuman)(nil) + +func (v *ModulesHuman) Display(manifest moduleref.Manifest) int { + if len(manifest.Records) == 0 { + v.view.streams.Println("No modules found in configuration.") + return 0 + } + printRoot := treeprint.New() + + // ensure output is deterministic + sort.Sort(manifest.Records) + + populateTreeNode(printRoot, &moduleref.Record{ + Children: manifest.Records, + }) + + v.view.streams.Println(fmt.Sprintf("\nModules declared by configuration:\n%s", printRoot.String())) + return 0 +} + +func populateTreeNode(tree treeprint.Tree, node *moduleref.Record) { + for _, childNode := range node.Children { + item := fmt.Sprintf("\"%s\"[%s]", childNode.Key, childNode.Source.String()) + if childNode.Version != nil { + item += fmt.Sprintf(" %s", childNode.Version) + // Avoid rendering the version constraint if an exact version is given i.e. 'version = "1.2.3"' + if childNode.VersionConstraints != nil && childNode.VersionConstraints.String() != childNode.Version.String() { + item += fmt.Sprintf(" (%s)", childNode.VersionConstraints.String()) + } + } + branch := tree.AddBranch(item) + populateTreeNode(branch, childNode) + } +} + +func (v *ModulesHuman) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} + +type ModulesJSON struct { + view *View +} + +var _ Modules = (*ModulesHuman)(nil) + +func (v *ModulesJSON) Display(manifest moduleref.Manifest) int { + var bytes []byte + var err error + + flattenedManifest := flattenManifest(manifest) + if bytes, err = encJson.Marshal(flattenedManifest); err != nil { + v.view.streams.Eprintf("error marshalling manifest: %v", err) + return 1 + } + + v.view.streams.Println(string(bytes)) + return 0 +} + +// FlattenManifest returns the nested contents of a moduleref.Manifest in +// a flattened format with the VersionConstraints and Children attributes +// ommited for the purposes of the json format of the modules command +func flattenManifest(m moduleref.Manifest) map[string]interface{} { + var flatten func(records []*moduleref.Record) + var recordList []map[string]string + flatten = func(records []*moduleref.Record) { + for _, record := range records { + if record.Version != nil { + recordList = append(recordList, map[string]string{ + "key": record.Key, + "source": record.Source.String(), + "version": record.Version.String(), + }) + } else { + recordList = append(recordList, map[string]string{ + "key": record.Key, + "source": record.Source.String(), + "version": "", + }) + } + + if len(record.Children) > 0 { + flatten(record.Children) + } + } + } + + flatten(m.Records) + ret := map[string]interface{}{ + "format_version": m.FormatVersion, + "modules": recordList, + } + return ret +} + +func (v *ModulesJSON) Diagnostics(diags tfdiags.Diagnostics) { + v.view.Diagnostics(diags) +} diff --git a/internal/command/views/operation.go b/internal/command/views/operation.go index 01daedc39b..46350b320f 100644 --- a/internal/command/views/operation.go +++ b/internal/command/views/operation.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -8,6 +11,9 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" "github.com/hashicorp/terraform/internal/command/views/json" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states/statefile" @@ -25,7 +31,7 @@ type Operation interface { PlannedChange(change *plans.ResourceInstanceChangeSrc) Plan(plan *plans.Plan, schemas *terraform.Schemas) - PlanNextStep(planPath string) + PlanNextStep(planPath string, genConfigPath string) Diagnostics(diags tfdiags.Diagnostics) } @@ -86,7 +92,42 @@ func (v *OperationHuman) EmergencyDumpState(stateFile *statefile.File) error { } func (v *OperationHuman) Plan(plan *plans.Plan, schemas *terraform.Schemas) { - renderPlan(plan, schemas, v.view) + outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas) + if err != nil { + v.view.streams.Eprintf("Failed to marshal plan to json: %s", err) + return + } + + renderer := jsonformat.Renderer{ + Colorize: v.view.colorize, + Streams: v.view.streams, + RunningInAutomation: v.inAutomation, + } + + jplan := jsonformat.Plan{ + PlanFormatVersion: jsonplan.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + OutputChanges: outputs, + ResourceChanges: changed, + ResourceDrift: drift, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + RelevantAttributes: attrs, + } + + // Side load some data that we can't extract from the JSON plan. + var opts []plans.Quality + if plan.Errored { + opts = append(opts, plans.Errored) + } else if !plan.Applyable { + // FIXME: There might be other reasons for "non-applyable" in future, + // so maybe we should check plan.Changes.IsEmpty here and use a more + // generic fallback message if the plan doesn't seem to be empty. + // There are currently no other cases though, so we'll keep it + // simple for now. + opts = append(opts, plans.NoChanges) + } + + renderer.RenderHumanPlan(jplan, plan.UIMode, opts...) } func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) { @@ -98,20 +139,33 @@ func (v *OperationHuman) PlannedChange(change *plans.ResourceInstanceChangeSrc) // PlanNextStep gives the user some next-steps, unless we're running in an // automation tool which is presumed to provide its own UI for further actions. -func (v *OperationHuman) PlanNextStep(planPath string) { +func (v *OperationHuman) PlanNextStep(planPath string, genConfigPath string) { if v.inAutomation { return } v.view.outputHorizRule() + if genConfigPath != "" { + v.view.streams.Println( + format.WordWrap( + "\n"+strings.TrimSpace(fmt.Sprintf(planHeaderGenConfig, genConfigPath)), + v.view.outputColumns(), + )) + } + if planPath == "" { - v.view.streams.Print( - "\n" + strings.TrimSpace(format.WordWrap(planHeaderNoOutput, v.view.outputColumns())) + "\n", + v.view.streams.Println( + format.WordWrap( + "\n"+strings.TrimSpace(planHeaderNoOutput), + v.view.outputColumns(), + ), ) } else { - v.view.streams.Printf( - "\n"+strings.TrimSpace(format.WordWrap(planHeaderYesOutput, v.view.outputColumns()))+"\n", - planPath, planPath, + v.view.streams.Println( + format.WordWrap( + "\n"+strings.TrimSpace(fmt.Sprintf(planHeaderYesOutput, planPath, planPath)), + v.view.outputColumns(), + ), ) } } @@ -178,6 +232,11 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) { // Avoid rendering data sources on deletion continue } + + if change.Importing != nil { + cs.Import++ + } + switch change.Action { case plans.Create: cs.Add++ @@ -190,7 +249,7 @@ func (v *OperationJSON) Plan(plan *plans.Plan, schemas *terraform.Schemas) { cs.Remove++ } - if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) { + if change.Action != plans.NoOp || !change.Addr.Equal(change.PrevRunAddr) || change.Importing != nil { v.view.PlannedChange(json.NewResourceInstanceChange(change)) } } @@ -219,7 +278,7 @@ func (v *OperationJSON) PlannedChange(change *plans.ResourceInstanceChangeSrc) { // PlanNextStep does nothing for the JSON view as it is a hook for user-facing // output only applicable to human-readable UI. -func (v *OperationJSON) PlanNextStep(planPath string) { +func (v *OperationJSON) PlanNextStep(planPath string, genConfigPath string) { } func (v *OperationJSON) Diagnostics(diags tfdiags.Diagnostics) { @@ -246,3 +305,7 @@ Saved the plan to: %s To perform exactly these actions, run the following command to apply: terraform apply %q ` + +const planHeaderGenConfig = ` +Terraform has generated configuration and written it to %s. Please review the configuration and edit it as necessary before adding it to version control. +` diff --git a/internal/command/views/operation_test.go b/internal/command/views/operation_test.go index 9b0323a276..e3a933b7c3 100644 --- a/internal/command/views/operation_test.go +++ b/internal/command/views/operation_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -85,7 +88,7 @@ func TestOperation_planNoChanges(t *testing.T) { func(schemas *terraform.Schemas) *plans.Plan { return &plans.Plan{ UIMode: plans.NormalMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), } }, "no differences, so no changes are needed.", @@ -94,7 +97,7 @@ func TestOperation_planNoChanges(t *testing.T) { func(schemas *terraform.Schemas) *plans.Plan { return &plans.Plan{ UIMode: plans.RefreshOnlyMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), } }, "Terraform has checked that the real remote objects still match", @@ -103,7 +106,7 @@ func TestOperation_planNoChanges(t *testing.T) { func(schemas *terraform.Schemas) *plans.Plan { return &plans.Plan{ UIMode: plans.DestroyMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), } }, "No objects need to be destroyed.", @@ -115,12 +118,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "somewhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addr, @@ -136,14 +139,14 @@ func TestOperation_planNoChanges(t *testing.T) { }), }, } - rcs, err := rc.Encode(ty) + rcs, err := rc.Encode(schema) if err != nil { panic(err) } drs := []*plans.ResourceInstanceChangeSrc{rcs} return &plans.Plan{ UIMode: plans.NormalMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), DriftedResources: drs, } }, @@ -156,12 +159,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "somewhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addr, @@ -177,12 +180,12 @@ func TestOperation_planNoChanges(t *testing.T) { }), }, } - rcs, err := rc.Encode(ty) + rcs, err := rc.Encode(schema) if err != nil { panic(err) } drs := []*plans.ResourceInstanceChangeSrc{rcs} - changes := plans.NewChanges() + changes := plans.NewChangesSrc() changes.Resources = drs return &plans.Plan{ UIMode: plans.NormalMode, @@ -203,12 +206,12 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "somewhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addr, @@ -224,14 +227,14 @@ func TestOperation_planNoChanges(t *testing.T) { }), }, } - rcs, err := rc.Encode(ty) + rcs, err := rc.Encode(schema) if err != nil { panic(err) } drs := []*plans.ResourceInstanceChangeSrc{rcs} return &plans.Plan{ UIMode: plans.RefreshOnlyMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), DriftedResources: drs, } }, @@ -249,12 +252,11 @@ func TestOperation_planNoChanges(t *testing.T) { Type: "test_resource", Name: "anywhere", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( addrs.NewDefaultProvider("test"), addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - ty := schema.ImpliedType() rc := &plans.ResourceInstanceChange{ Addr: addr, PrevRunAddr: addrPrev, @@ -273,14 +275,14 @@ func TestOperation_planNoChanges(t *testing.T) { }), }, } - rcs, err := rc.Encode(ty) + rcs, err := rc.Encode(schema) if err != nil { panic(err) } drs := []*plans.ResourceInstanceChangeSrc{rcs} return &plans.Plan{ UIMode: plans.RefreshOnlyMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), DriftedResources: drs, } }, @@ -290,7 +292,7 @@ func TestOperation_planNoChanges(t *testing.T) { func(schemas *terraform.Schemas) *plans.Plan { return &plans.Plan{ UIMode: plans.DestroyMode, - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), PrevRunState: states.BuildState(func(state *states.SyncState) { state.SetResourceInstanceCurrent( addrs.Resource{ @@ -356,6 +358,78 @@ Plan: 1 to add, 0 to change, 0 to destroy. } } +func TestOperation_planWithDatasource(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + v := NewOperation(arguments.ViewHuman, true, NewView(streams)) + + plan := testPlanWithDatasource(t) + schemas := testSchemas() + v.Plan(plan, schemas) + + want := ` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + <= read (data resources) + +Terraform will perform the following actions: + + # data.test_data_source.bar will be read during apply + <= data "test_data_source" "bar" { + + bar = "foo" + + id = "C6743020-40BD-4591-81E6-CD08494341D3" + } + + # test_resource.foo will be created + + resource "test_resource" "foo" { + + foo = "bar" + + id = (known after apply) + } + +Plan: 1 to add, 0 to change, 0 to destroy. +` + + if got := done(t).Stdout(); got != want { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want) + } +} + +func TestOperation_planWithDatasourceAndDrift(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + v := NewOperation(arguments.ViewHuman, true, NewView(streams)) + + plan := testPlanWithDatasource(t) + schemas := testSchemas() + v.Plan(plan, schemas) + + want := ` +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + <= read (data resources) + +Terraform will perform the following actions: + + # data.test_data_source.bar will be read during apply + <= data "test_data_source" "bar" { + + bar = "foo" + + id = "C6743020-40BD-4591-81E6-CD08494341D3" + } + + # test_resource.foo will be created + + resource "test_resource" "foo" { + + foo = "bar" + + id = (known after apply) + } + +Plan: 1 to add, 0 to change, 0 to destroy. +` + + if got := done(t).Stdout(); got != want { + t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s", got, want) + } +} + func TestOperation_planNextStep(t *testing.T) { testCases := map[string]struct { path string @@ -375,7 +449,7 @@ func TestOperation_planNextStep(t *testing.T) { streams, done := terminal.StreamsForTesting(t) v := NewOperation(arguments.ViewHuman, false, NewView(streams)) - v.PlanNextStep(tc.path) + v.PlanNextStep(tc.path, "") if got := done(t).Stdout(); !strings.Contains(got, tc.want) { t.Errorf("wrong result\ngot: %q\nwant: %q", got, tc.want) @@ -390,7 +464,7 @@ func TestOperation_planNextStepInAutomation(t *testing.T) { streams, done := terminal.StreamsForTesting(t) v := NewOperation(arguments.ViewHuman, true, NewView(streams)) - v.PlanNextStep("") + v.PlanNextStep("", "") if got := done(t).Stdout(); got != "" { t.Errorf("unexpected output\ngot: %q", got) @@ -488,7 +562,7 @@ func TestOperationJSON_planNoChanges(t *testing.T) { v := &OperationJSON{view: NewJSONView(NewView(streams))} plan := &plans.Plan{ - Changes: plans.NewChanges(), + Changes: plans.NewChangesSrc(), } v.Plan(plan, nil) @@ -501,6 +575,7 @@ func TestOperationJSON_planNoChanges(t *testing.T) { "changes": map[string]interface{}{ "operation": "plan", "add": float64(0), + "import": float64(0), "change": float64(0), "remove": float64(0), }, @@ -524,7 +599,7 @@ func TestOperationJSON_plan(t *testing.T) { derp := addrs.Resource{Mode: addrs.DataResourceMode, Type: "test_source", Name: "derp"} plan := &plans.Plan{ - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), @@ -668,6 +743,7 @@ func TestOperationJSON_plan(t *testing.T) { "changes": map[string]interface{}{ "operation": "plan", "add": float64(3), + "import": float64(0), "change": float64(1), "remove": float64(3), }, @@ -677,6 +753,153 @@ func TestOperationJSON_plan(t *testing.T) { testJSONViewOutputEquals(t, done(t).Stdout(), want) } +func TestOperationJSON_planWithImport(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + v := &OperationJSON{view: NewJSONView(NewView(streams))} + + root := addrs.RootModuleInstance + vpc, diags := addrs.ParseModuleInstanceStr("module.vpc") + if len(diags) > 0 { + t.Fatal(diags.Err()) + } + boop := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "boop"} + beep := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "beep"} + + plan := &plans.Plan{ + Changes: &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), + PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(vpc), + ChangeSrc: plans.ChangeSrc{Action: plans.NoOp, Importing: &plans.ImportingSrc{ID: "DECD6D77"}}, + }, + { + Addr: boop.Instance(addrs.IntKey(1)).Absolute(vpc), + PrevRunAddr: boop.Instance(addrs.IntKey(1)).Absolute(vpc), + ChangeSrc: plans.ChangeSrc{Action: plans.Delete, Importing: &plans.ImportingSrc{ID: "DECD6D77"}}, + }, + { + Addr: boop.Instance(addrs.IntKey(0)).Absolute(root), + PrevRunAddr: boop.Instance(addrs.IntKey(0)).Absolute(root), + ChangeSrc: plans.ChangeSrc{Action: plans.CreateThenDelete, Importing: &plans.ImportingSrc{ID: "DECD6D77"}}, + }, + { + Addr: beep.Instance(addrs.NoKey).Absolute(root), + PrevRunAddr: beep.Instance(addrs.NoKey).Absolute(root), + ChangeSrc: plans.ChangeSrc{Action: plans.Update, Importing: &plans.ImportingSrc{ID: "DECD6D77"}}, + }, + }, + }, + } + v.Plan(plan, testSchemas()) + + want := []map[string]interface{}{ + // Simple import + { + "@level": "info", + "@message": "module.vpc.test_resource.boop[0]: Plan to import", + "@module": "terraform.ui", + "type": "planned_change", + "change": map[string]interface{}{ + "action": "import", + "resource": map[string]interface{}{ + "addr": `module.vpc.test_resource.boop[0]`, + "implied_provider": "test", + "module": "module.vpc", + "resource": `test_resource.boop[0]`, + "resource_key": float64(0), + "resource_name": "boop", + "resource_type": "test_resource", + }, + "importing": map[string]interface{}{ + "id": "DECD6D77", + }, + }, + }, + // Delete after importing + { + "@level": "info", + "@message": "module.vpc.test_resource.boop[1]: Plan to delete", + "@module": "terraform.ui", + "type": "planned_change", + "change": map[string]interface{}{ + "action": "delete", + "resource": map[string]interface{}{ + "addr": `module.vpc.test_resource.boop[1]`, + "implied_provider": "test", + "module": "module.vpc", + "resource": `test_resource.boop[1]`, + "resource_key": float64(1), + "resource_name": "boop", + "resource_type": "test_resource", + }, + "importing": map[string]interface{}{ + "id": "DECD6D77", + }, + }, + }, + // Create-then-delete after importing. + { + "@level": "info", + "@message": "test_resource.boop[0]: Plan to replace", + "@module": "terraform.ui", + "type": "planned_change", + "change": map[string]interface{}{ + "action": "replace", + "resource": map[string]interface{}{ + "addr": `test_resource.boop[0]`, + "implied_provider": "test", + "module": "", + "resource": `test_resource.boop[0]`, + "resource_key": float64(0), + "resource_name": "boop", + "resource_type": "test_resource", + }, + "importing": map[string]interface{}{ + "id": "DECD6D77", + }, + }, + }, + // Update after importing + { + "@level": "info", + "@message": "test_resource.beep: Plan to update", + "@module": "terraform.ui", + "type": "planned_change", + "change": map[string]interface{}{ + "action": "update", + "resource": map[string]interface{}{ + "addr": `test_resource.beep`, + "implied_provider": "test", + "module": "", + "resource": `test_resource.beep`, + "resource_key": nil, + "resource_name": "beep", + "resource_type": "test_resource", + }, + "importing": map[string]interface{}{ + "id": "DECD6D77", + }, + }, + }, + { + "@level": "info", + "@message": "Plan: 4 to import, 1 to add, 1 to change, 2 to destroy.", + "@module": "terraform.ui", + "type": "change_summary", + "changes": map[string]interface{}{ + "operation": "plan", + "add": float64(1), + "import": float64(4), + "change": float64(1), + "remove": float64(2), + }, + }, + } + + testJSONViewOutputEquals(t, done(t).Stdout(), want) +} + func TestOperationJSON_planDriftWithMove(t *testing.T) { streams, done := terminal.StreamsForTesting(t) v := &OperationJSON{view: NewJSONView(NewView(streams))} @@ -689,7 +912,7 @@ func TestOperationJSON_planDriftWithMove(t *testing.T) { plan := &plans.Plan{ UIMode: plans.NormalMode, - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: honk.Instance(addrs.StringKey("bonk")).Absolute(root), @@ -804,6 +1027,7 @@ func TestOperationJSON_planDriftWithMove(t *testing.T) { "changes": map[string]interface{}{ "operation": "plan", "add": float64(0), + "import": float64(0), "change": float64(0), "remove": float64(0), }, @@ -825,7 +1049,7 @@ func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) { plan := &plans.Plan{ UIMode: plans.RefreshOnlyMode, - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{}, }, DriftedResources: []*plans.ResourceInstanceChangeSrc{ @@ -934,6 +1158,7 @@ func TestOperationJSON_planDriftWithMoveRefreshOnly(t *testing.T) { "changes": map[string]interface{}{ "operation": "plan", "add": float64(0), + "import": float64(0), "change": float64(0), "remove": float64(0), }, @@ -950,7 +1175,7 @@ func TestOperationJSON_planOutputChanges(t *testing.T) { root := addrs.RootModuleInstance plan := &plans.Plan{ - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{}, Outputs: []*plans.OutputChangeSrc{ { @@ -993,6 +1218,7 @@ func TestOperationJSON_planOutputChanges(t *testing.T) { "changes": map[string]interface{}{ "operation": "plan", "add": float64(0), + "import": float64(0), "change": float64(0), "remove": float64(0), }, diff --git a/internal/command/views/output.go b/internal/command/views/output.go index 6545aaceec..c7fdee2712 100644 --- a/internal/command/views/output.go +++ b/internal/command/views/output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( diff --git a/internal/command/views/output_test.go b/internal/command/views/output_test.go index 3307778e67..cb59b6937c 100644 --- a/internal/command/views/output_test.go +++ b/internal/command/views/output_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( diff --git a/internal/command/views/plan.go b/internal/command/views/plan.go index 8bb47a7fb2..fc7dbad4b4 100644 --- a/internal/command/views/plan.go +++ b/internal/command/views/plan.go @@ -1,20 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( - "bytes" "fmt" - "sort" - "strings" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" - "github.com/hashicorp/terraform/internal/command/format" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang/globalref" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -97,468 +89,3 @@ func (v *PlanJSON) Diagnostics(diags tfdiags.Diagnostics) { func (v *PlanJSON) HelpPrompt() { } - -// The plan renderer is used by the Operation view (for plan and apply -// commands) and the Show view (for the show command). -func renderPlan(plan *plans.Plan, schemas *terraform.Schemas, view *View) { - haveRefreshChanges := renderChangesDetectedByRefresh(plan, schemas, view) - - counts := map[plans.Action]int{} - var rChanges []*plans.ResourceInstanceChangeSrc - for _, change := range plan.Changes.Resources { - if change.Action == plans.NoOp && !change.Moved() { - continue // We don't show anything for no-op changes - } - if change.Action == plans.Delete && change.Addr.Resource.Resource.Mode == addrs.DataResourceMode { - // Avoid rendering data sources on deletion - continue - } - - rChanges = append(rChanges, change) - - // Don't count move-only changes - if change.Action != plans.NoOp { - counts[change.Action]++ - } - } - var changedRootModuleOutputs []*plans.OutputChangeSrc - for _, output := range plan.Changes.Outputs { - if !output.Addr.Module.IsRoot() { - continue - } - if output.ChangeSrc.Action == plans.NoOp { - continue - } - changedRootModuleOutputs = append(changedRootModuleOutputs, output) - } - - if len(rChanges) == 0 && len(changedRootModuleOutputs) == 0 { - // If we didn't find any changes to report at all then this is a - // "No changes" plan. How we'll present this depends on whether - // the plan is "applyable" and, if so, whether it had refresh changes - // that we already would've presented above. - - switch plan.UIMode { - case plans.RefreshOnlyMode: - if haveRefreshChanges { - // We already generated a sufficient prompt about what will - // happen if applying this change above, so we don't need to - // say anything more. - return - } - - view.streams.Print( - view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure still matches the configuration.[reset]\n\n"), - ) - view.streams.Println(format.WordWrap( - "Terraform has checked that the real remote objects still match the result of your most recent changes, and found no differences.", - view.outputColumns(), - )) - - case plans.DestroyMode: - if haveRefreshChanges { - view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) - view.streams.Println("") - } - view.streams.Print( - view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] No objects need to be destroyed.[reset]\n\n"), - ) - view.streams.Println(format.WordWrap( - "Either you have not created any objects yet or the existing objects were already deleted outside of Terraform.", - view.outputColumns(), - )) - - default: - if haveRefreshChanges { - view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) - view.streams.Println("") - } - view.streams.Print( - view.colorize.Color("\n[reset][bold][green]No changes.[reset][bold] Your infrastructure matches the configuration.[reset]\n\n"), - ) - - if haveRefreshChanges { - if plan.CanApply() { - // In this case, applying this plan will not change any - // remote objects but _will_ update the state to match what - // we detected during refresh, so we'll reassure the user - // about that. - view.streams.Println(format.WordWrap( - "Your configuration already matches the changes detected above, so applying this plan will only update the state to include the changes detected above and won't change any real infrastructure.", - view.outputColumns(), - )) - } else { - // In this case we detected changes during refresh but this isn't - // a planning mode where we consider those to be applyable. The - // user must re-run in refresh-only mode in order to update the - // state to match the upstream changes. - suggestion := "." - if !view.runningInAutomation { - // The normal message includes a specific command line to run. - suggestion = ":\n terraform apply -refresh-only" - } - view.streams.Println(format.WordWrap( - "Your configuration already matches the changes detected above. If you'd like to update the Terraform state to match, create and apply a refresh-only plan"+suggestion, - view.outputColumns(), - )) - } - return - } - - // If we get down here then we're just in the simple situation where - // the plan isn't applyable at all. - view.streams.Println(format.WordWrap( - "Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.", - view.outputColumns(), - )) - } - return - } - if haveRefreshChanges { - view.streams.Print(format.HorizontalRule(view.colorize, view.outputColumns())) - view.streams.Println("") - } - - if len(counts) > 0 { - headerBuf := &bytes.Buffer{} - fmt.Fprintf(headerBuf, "\n%s\n", strings.TrimSpace(format.WordWrap(planHeaderIntro, view.outputColumns()))) - if counts[plans.Create] > 0 { - fmt.Fprintf(headerBuf, "%s create\n", format.DiffActionSymbol(plans.Create)) - } - if counts[plans.Update] > 0 { - fmt.Fprintf(headerBuf, "%s update in-place\n", format.DiffActionSymbol(plans.Update)) - } - if counts[plans.Delete] > 0 { - fmt.Fprintf(headerBuf, "%s destroy\n", format.DiffActionSymbol(plans.Delete)) - } - if counts[plans.DeleteThenCreate] > 0 { - fmt.Fprintf(headerBuf, "%s destroy and then create replacement\n", format.DiffActionSymbol(plans.DeleteThenCreate)) - } - if counts[plans.CreateThenDelete] > 0 { - fmt.Fprintf(headerBuf, "%s create replacement and then destroy\n", format.DiffActionSymbol(plans.CreateThenDelete)) - } - if counts[plans.Read] > 0 { - fmt.Fprintf(headerBuf, "%s read (data resources)\n", format.DiffActionSymbol(plans.Read)) - } - - view.streams.Print(view.colorize.Color(headerBuf.String())) - } - - if len(rChanges) > 0 { - view.streams.Printf("\nTerraform will perform the following actions:\n\n") - - // Note: we're modifying the backing slice of this plan object in-place - // here. The ordering of resource changes in a plan is not significant, - // but we can only do this safely here because we can assume that nobody - // is concurrently modifying our changes while we're trying to print it. - sort.Slice(rChanges, func(i, j int) bool { - iA := rChanges[i].Addr - jA := rChanges[j].Addr - if iA.String() == jA.String() { - return rChanges[i].DeposedKey < rChanges[j].DeposedKey - } - return iA.Less(jA) - }) - - for _, rcs := range rChanges { - if rcs.Action == plans.NoOp && !rcs.Moved() { - continue - } - - providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) - if providerSchema == nil { - // Should never happen - view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr) - continue - } - rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) - if rSchema == nil { - // Should never happen - view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr) - continue - } - - view.streams.Println(format.ResourceChange( - decodeChange(rcs, rSchema), - rSchema, - view.colorize, - format.DiffLanguageProposedChange, - )) - } - - // stats is similar to counts above, but: - // - it considers only resource changes - // - it simplifies "replace" into both a create and a delete - stats := map[plans.Action]int{} - for _, change := range rChanges { - switch change.Action { - case plans.CreateThenDelete, plans.DeleteThenCreate: - stats[plans.Create]++ - stats[plans.Delete]++ - default: - stats[change.Action]++ - } - } - view.streams.Printf( - view.colorize.Color("[reset][bold]Plan:[reset] %d to add, %d to change, %d to destroy.\n"), - stats[plans.Create], stats[plans.Update], stats[plans.Delete], - ) - } - - // If there is at least one planned change to the root module outputs - // then we'll render a summary of those too. - if len(changedRootModuleOutputs) > 0 { - view.streams.Println( - view.colorize.Color("[reset]\n[bold]Changes to Outputs:[reset]") + - format.OutputChanges(changedRootModuleOutputs, view.colorize), - ) - - if len(counts) == 0 { - // If we have output changes but not resource changes then we - // won't have output any indication about the changes at all yet, - // so we need some extra context about what it would mean to - // apply a change that _only_ includes output changes. - view.streams.Println(format.WordWrap( - "\nYou can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.", - view.outputColumns(), - )) - } - } -} - -// renderChangesDetectedByRefresh is a part of renderPlan that generates -// the note about changes detected by refresh (sometimes considered as "drift"). -// -// It will only generate output if there's at least one difference detected. -// Otherwise, it will produce nothing at all. To help the caller recognize -// those two situations incase subsequent output must accommodate it, -// renderChangesDetectedByRefresh returns true if it produced at least one -// line of output, and guarantees to always produce whole lines terminated -// by newline characters. -func renderChangesDetectedByRefresh(plan *plans.Plan, schemas *terraform.Schemas, view *View) (rendered bool) { - // If this is not a refresh-only plan, we will need to filter out any - // non-relevant changes to reduce plan output. - relevant := make(map[string]bool) - for _, r := range plan.RelevantAttributes { - relevant[r.Resource.String()] = true - } - - var changes []*plans.ResourceInstanceChange - for _, rcs := range plan.DriftedResources { - providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) - if providerSchema == nil { - // Should never happen - view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr) - continue - } - rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) - if rSchema == nil { - // Should never happen - view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr) - continue - } - - changes = append(changes, decodeChange(rcs, rSchema)) - } - - // In refresh-only mode, we show all resources marked as drifted, - // including those which have moved without other changes. In other plan - // modes, move-only changes will be rendered in the planned changes, so - // we skip them here. - var drs []*plans.ResourceInstanceChange - if plan.UIMode == plans.RefreshOnlyMode { - drs = changes - } else { - for _, dr := range changes { - change := filterRefreshChange(dr, plan.RelevantAttributes) - if change.Action != plans.NoOp { - dr.Change = change - drs = append(drs, dr) - } - } - } - - if len(drs) == 0 { - return false - } - - // In an empty plan, we don't show any outside changes, because nothing in - // the plan could have been affected by those changes. If a user wants to - // see all external changes, then a refresh-only plan should be executed - // instead. - if plan.Changes.Empty() && plan.UIMode != plans.RefreshOnlyMode { - return false - } - - view.streams.Print( - view.colorize.Color("[reset]\n[bold][cyan]Note:[reset][bold] Objects have changed outside of Terraform[reset]\n\n"), - ) - view.streams.Print(format.WordWrap( - "Terraform detected the following changes made outside of Terraform since the last \"terraform apply\" which may have affected this plan:\n\n", - view.outputColumns(), - )) - - // Note: we're modifying the backing slice of this plan object in-place - // here. The ordering of resource changes in a plan is not significant, - // but we can only do this safely here because we can assume that nobody - // is concurrently modifying our changes while we're trying to print it. - sort.Slice(drs, func(i, j int) bool { - iA := drs[i].Addr - jA := drs[j].Addr - if iA.String() == jA.String() { - return drs[i].DeposedKey < drs[j].DeposedKey - } - return iA.Less(jA) - }) - - for _, rcs := range drs { - providerSchema := schemas.ProviderSchema(rcs.ProviderAddr.Provider) - if providerSchema == nil { - // Should never happen - view.streams.Printf("(schema missing for %s)\n\n", rcs.ProviderAddr) - continue - } - rSchema, _ := providerSchema.SchemaForResourceAddr(rcs.Addr.Resource.Resource) - if rSchema == nil { - // Should never happen - view.streams.Printf("(schema missing for %s)\n\n", rcs.Addr) - continue - } - - view.streams.Println(format.ResourceChange( - rcs, - rSchema, - view.colorize, - format.DiffLanguageDetectedDrift, - )) - } - - switch plan.UIMode { - case plans.RefreshOnlyMode: - view.streams.Println(format.WordWrap( - "\nThis is a refresh-only plan, so Terraform will not take any actions to undo these. If you were expecting these changes then you can apply this plan to record the updated values in the Terraform state without changing any remote objects.", - view.outputColumns(), - )) - default: - view.streams.Println(format.WordWrap( - "\nUnless you have made equivalent changes to your configuration, or ignored the relevant attributes using ignore_changes, the following plan may include actions to undo or respond to these changes.", - view.outputColumns(), - )) - } - - return true -} - -// Filter individual resource changes for display based on the attributes which -// may have contributed to the plan as a whole. In order to continue to use the -// existing diff renderer, we are going to create a fake change for display, -// only showing the attributes we're interested in. -// The resulting change will be a NoOp if it has nothing relevant to the plan. -func filterRefreshChange(change *plans.ResourceInstanceChange, contributing []globalref.ResourceAttr) plans.Change { - - if change.Action == plans.NoOp { - return change.Change - } - - var relevantAttrs []cty.Path - resAddr := change.Addr - - for _, attr := range contributing { - if !resAddr.ContainingResource().Equal(attr.Resource.ContainingResource()) { - continue - } - - // If the contributing address has no instance key, then the - // contributing reference applies to all instances. - if attr.Resource.Resource.Key == addrs.NoKey || resAddr.Equal(attr.Resource) { - relevantAttrs = append(relevantAttrs, attr.Attr) - } - } - - // If no attributes are relevant in this resource, then we can turn this - // onto a NoOp change for display. - if len(relevantAttrs) == 0 { - return plans.Change{ - Action: plans.NoOp, - Before: change.Before, - After: change.Before, - } - } - - // We have some attributes in this change which were marked as relevant, so - // we are going to take the Before value and add in only those attributes - // from the After value which may have contributed to the plan. - - // If the types don't match because the schema is dynamic, we may not be - // able to apply the paths to the new values. - // if we encounter a path that does not apply correctly and the types do - // not match overall, just assume we need the entire value. - isDynamic := !change.Before.Type().Equals(change.After.Type()) - failedApply := false - - before := change.Before - after, _ := cty.Transform(before, func(path cty.Path, v cty.Value) (cty.Value, error) { - for i, attrPath := range relevantAttrs { - // We match prefix in case we are traversing any null or dynamic - // values and enter in via a shorter path. The traversal is - // depth-first, so we will always hit the longest match first. - if attrPath.HasPrefix(path) { - // remove the path from further consideration - relevantAttrs = append(relevantAttrs[:i], relevantAttrs[i+1:]...) - - applied, err := path.Apply(change.After) - if err != nil { - failedApply = true - // Assume the types match for now, and failure to apply is - // because a parent value is null. If there were dynamic - // types we'll just restore the entire value. - return cty.NullVal(v.Type()), nil - } - - return applied, err - } - } - return v, nil - }) - - // A contributing attribute path did not match the after value type in some - // way, so restore the entire change. - if isDynamic && failedApply { - after = change.After - } - - action := change.Action - if before.RawEquals(after) { - action = plans.NoOp - } - - return plans.Change{ - Action: action, - Before: before, - After: after, - } -} - -func decodeChange(change *plans.ResourceInstanceChangeSrc, schema *configschema.Block) *plans.ResourceInstanceChange { - changeV, err := change.Decode(schema.ImpliedType()) - if err != nil { - // Should never happen in here, since we've already been through - // loads of layers of encode/decode of the planned changes before now. - panic(fmt.Sprintf("failed to decode plan for %s while rendering diff: %s", change.Addr, err)) - } - - // We currently have an opt-out that permits the legacy SDK to return values - // that defy our usual conventions around handling of nesting blocks. To - // avoid the rendering code from needing to handle all of these, we'll - // normalize first. - // (Ideally we'd do this as part of the SDK opt-out implementation in core, - // but we've added it here for now to reduce risk of unexpected impacts - // on other code in core.) - changeV.Change.Before = objchange.NormalizeObjectFromLegacySDK(changeV.Change.Before, schema) - changeV.Change.After = objchange.NormalizeObjectFromLegacySDK(changeV.Change.After, schema) - return changeV -} - -const planHeaderIntro = ` -Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: -` diff --git a/internal/command/views/plan_test.go b/internal/command/views/plan_test.go index 65e277595f..8a77a2acf5 100644 --- a/internal/command/views/plan_test.go +++ b/internal/command/views/plan_test.go @@ -1,17 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang/globalref" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/internal/terraform" - "github.com/zclconf/go-cty/cty" ) // Ensure that the correct view type and in-automation settings propagate to the @@ -63,14 +67,14 @@ func testPlan(t *testing.T) *plans.Plan { t.Fatal(err) } - changes := plans.NewChanges() + changes := plans.NewChangesSrc() addr := addrs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ Addr: addr, PrevRunAddr: addr, ProviderAddr: addrs.AbsProviderConfig{ @@ -85,21 +89,62 @@ func testPlan(t *testing.T) *plans.Plan { }) return &plans.Plan{ - Changes: changes, + Changes: changes, + Applyable: true, + Complete: true, } } +func testPlanWithDatasource(t *testing.T) *plans.Plan { + plan := testPlan(t) + + addr := addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test_data_source", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + dataVal := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("C6743020-40BD-4591-81E6-CD08494341D3"), + "bar": cty.StringVal("foo"), + }) + priorValRaw, err := plans.NewDynamicValue(cty.NullVal(dataVal.Type()), dataVal.Type()) + if err != nil { + t.Fatal(err) + } + plannedValRaw, err := plans.NewDynamicValue(dataVal, dataVal.Type()) + if err != nil { + t.Fatal(err) + } + + plan.Changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + Addr: addr, + PrevRunAddr: addr, + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Read, + Before: priorValRaw, + After: plannedValRaw, + }, + }) + + return plan +} + func testSchemas() *terraform.Schemas { provider := testProvider() return &terraform.Schemas{ - Providers: map[addrs.Provider]*terraform.ProviderSchema{ - addrs.NewDefaultProvider("test"): provider.ProviderSchema(), + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.NewDefaultProvider("test"): provider.GetProviderSchema(), }, } } -func testProvider() *terraform.MockProvider { - p := new(terraform.MockProvider) +func testProvider() *testing_provider.MockProvider { + p := new(testing_provider.MockProvider) p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } @@ -112,11 +157,11 @@ func testProvider() *terraform.MockProvider { func testProviderSchema() *providers.GetProviderSchemaResponse { return &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{}, + Body: &configschema.Block{}, }, ResourceTypes: map[string]providers.Schema{ "test_resource": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "foo": {Type: cty.String, Optional: true}, @@ -124,270 +169,15 @@ func testProviderSchema() *providers.GetProviderSchemaResponse { }, }, }, - } -} - -func TestFilterRefreshChange(t *testing.T) { - tests := map[string]struct { - paths []cty.Path - before, after, expected cty.Value - }{ - "attr was null": { - // nested attr was null - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("attr_null_before").GetAttr("b"), - }, - before: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "attr_null_before": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("old"), - "b": cty.NullVal(cty.String), - }), - }), - }), - after: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "attr_null_before": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - }), - }), - expected: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "attr_null_before": cty.ObjectVal(map[string]cty.Value{ - // we old picked the change in b - "a": cty.StringVal("old"), - "b": cty.StringVal("new"), - }), - }), - }), - }, - "object was null": { - // nested object attrs were null - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("obj_null_before").GetAttr("b"), - }, - before: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "obj_null_before": cty.NullVal(cty.Object(map[string]cty.Type{ - "a": cty.String, - "b": cty.String, - })), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("old"), - }), - }), - }), - after: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "obj_null_before": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("new"), - }), - }), - }), - expected: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "obj_null_before": cty.ObjectVal(map[string]cty.Value{ - // optimally "a" would be null, but we need to take the - // entire object since it was null before. - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("old"), - }), - }), - }), - }, - "object becomes null": { - // nested object attr becoming null - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("obj_null_after").GetAttr("a"), - }, - before: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "obj_null_after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("old"), - "b": cty.StringVal("old"), - }), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("old"), - }), - }), - }), - after: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "obj_null_after": cty.NullVal(cty.Object(map[string]cty.Type{ - "a": cty.String, - "b": cty.String, - })), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("new"), - }), - }), - }), - expected: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "obj_null_after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.NullVal(cty.String), - "b": cty.StringVal("old"), - }), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("old"), - }), - }), - }), - }, - "dynamic adding values": { - // dynamic gaining values - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), - }, - before: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.DynamicVal, - }), - after: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - // the entire attr object is taken here because there is - // nothing to compare within the before value - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("new"), - }), - }), - }), - expected: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - // "other" is picked up here too this time, because we need - // to take the entire dynamic "attr" value - "other": cty.ObjectVal(map[string]cty.Value{ - "o": cty.StringVal("new"), - }), - }), - }), - }, - "whole object becomes null": { - // whole object becomes null - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), - }, - before: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("old"), - "b": cty.StringVal("old"), - }), - }), - }), - after: cty.NullVal(cty.Object(map[string]cty.Type{ - "attr": cty.DynamicPseudoType, - })), - // since we have a dynamic type we have to take the entire object - // because the paths may not apply between versions. - expected: cty.NullVal(cty.Object(map[string]cty.Type{ - "attr": cty.DynamicPseudoType, - })), - }, - "whole object was null": { - // whole object was null - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("after").GetAttr("a"), - }, - before: cty.NullVal(cty.Object(map[string]cty.Type{ - "attr": cty.DynamicPseudoType, - })), - after: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - }), - }), - expected: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - }), - }), - }, - "restructured dynamic": { - // dynamic value changing structure significantly - paths: []cty.Path{ - cty.GetAttrPath("attr").GetAttr("list").IndexInt(1).GetAttr("a"), - }, - before: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("old"), - }), - }), - }), - }), - after: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - }), - }), - // the path does not apply at all to the new object, so we must - // take all the changes - expected: cty.ObjectVal(map[string]cty.Value{ - "attr": cty.ObjectVal(map[string]cty.Value{ - "after": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("new"), - "b": cty.StringVal("new"), - }), - }), - }), - }, - } - - for k, tc := range tests { - t.Run(k, func(t *testing.T) { - addr, diags := addrs.ParseAbsResourceInstanceStr("test_resource.a") - if diags != nil { - t.Fatal(diags.ErrWithWarnings()) - } - - change := &plans.ResourceInstanceChange{ - Addr: addr, - Change: plans.Change{ - Before: tc.before, - After: tc.after, - Action: plans.Update, + DataSources: map[string]providers.Schema{ + "test_data_source": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "bar": {Type: cty.String, Optional: true}, + }, }, - } - - var contributing []globalref.ResourceAttr - for _, p := range tc.paths { - contributing = append(contributing, globalref.ResourceAttr{ - Resource: addr, - Attr: p, - }) - } - - res := filterRefreshChange(change, contributing) - if !res.After.RawEquals(tc.expected) { - t.Errorf("\nexpected: %#v\ngot: %#v\n", tc.expected, res.After) - } - }) + }, + }, } } diff --git a/internal/command/views/refresh.go b/internal/command/views/refresh.go index c670fd2d27..1db57c06b1 100644 --- a/internal/command/views/refresh.go +++ b/internal/command/views/refresh.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( diff --git a/internal/command/views/refresh_test.go b/internal/command/views/refresh_test.go index 75dbcd6c4d..59f5b28ae3 100644 --- a/internal/command/views/refresh_test.go +++ b/internal/command/views/refresh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( @@ -98,7 +101,6 @@ func TestRefreshJSON_outputs(t *testing.T) { }, "password": map[string]interface{}{ "sensitive": true, - "value": "horse-battery", "type": "string", }, }, diff --git a/internal/command/views/show.go b/internal/command/views/show.go index 1ab16c2d51..e5ab06d097 100644 --- a/internal/command/views/show.go +++ b/internal/command/views/show.go @@ -1,10 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( + "bytes" + "encoding/json" "fmt" + + "github.com/hashicorp/terraform/internal/cloud/cloudplan" "github.com/hashicorp/terraform/internal/command/arguments" - "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat" "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" "github.com/hashicorp/terraform/internal/command/jsonstate" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/plans" @@ -15,7 +23,7 @@ import ( type Show interface { // Display renders the plan, if it is available. If plan is nil, it renders the statefile. - Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int + Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *terraform.Schemas) int // Diagnostics renders early diagnostics, resulting from argument parsing. Diagnostics(diags tfdiags.Diagnostics) @@ -38,20 +46,76 @@ type ShowHuman struct { var _ Show = (*ShowHuman)(nil) -func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int { - if plan != nil { - renderPlan(plan, schemas, v.view) +func (v *ShowHuman) Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *terraform.Schemas) int { + renderer := jsonformat.Renderer{ + Colorize: v.view.colorize, + Streams: v.view.streams, + RunningInAutomation: v.view.runningInAutomation, + } + + // Prefer to display a pre-built JSON plan, if we got one; then, fall back + // to building one ourselves. + if planJSON != nil { + if !planJSON.Redacted { + v.view.streams.Eprintf("Didn't get renderable JSON plan format for human display") + return 1 + } + // The redacted json plan format can be decoded into a jsonformat.Plan + p := jsonformat.Plan{} + r := bytes.NewReader(planJSON.JSONBytes) + if err := json.NewDecoder(r).Decode(&p); err != nil { + v.view.streams.Eprintf("Couldn't decode renderable JSON plan format: %s", err) + } + + v.view.streams.Print(v.view.colorize.Color(planJSON.RunHeader + "\n")) + renderer.RenderHumanPlan(p, planJSON.Mode, planJSON.Qualities...) + v.view.streams.Print(v.view.colorize.Color("\n" + planJSON.RunFooter + "\n")) + } else if plan != nil { + outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(plan, schemas) + if err != nil { + v.view.streams.Eprintf("Failed to marshal plan to json: %s", err) + return 1 + } + + jplan := jsonformat.Plan{ + PlanFormatVersion: jsonplan.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + OutputChanges: outputs, + ResourceChanges: changed, + ResourceDrift: drift, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + RelevantAttributes: attrs, + } + + var opts []plans.Quality + if plan.Errored { + opts = append(opts, plans.Errored) + } else if !plan.Applyable { + opts = append(opts, plans.NoChanges) + } + + renderer.RenderHumanPlan(jplan, plan.UIMode, opts...) } else { if stateFile == nil { v.view.streams.Println("No state.") return 0 } - v.view.streams.Println(format.State(&format.StateOpts{ - State: stateFile.State, - Color: v.view.colorize, - Schemas: schemas, - })) + root, outputs, err := jsonstate.MarshalForRenderer(stateFile, schemas) + if err != nil { + v.view.streams.Eprintf("Failed to marshal state to json: %s", err) + return 1 + } + + jstate := jsonformat.State{ + StateFormatVersion: jsonstate.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + RootModule: root, + RootModuleOutputs: outputs, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + } + + renderer.RenderHumanState(jstate) } return 0 } @@ -66,15 +130,23 @@ type ShowJSON struct { var _ Show = (*ShowJSON)(nil) -func (v *ShowJSON) Display(config *configs.Config, plan *plans.Plan, stateFile *statefile.File, schemas *terraform.Schemas) int { - if plan != nil { - jsonPlan, err := jsonplan.Marshal(config, plan, stateFile, schemas) +func (v *ShowJSON) Display(config *configs.Config, plan *plans.Plan, planJSON *cloudplan.RemotePlanJSON, stateFile *statefile.File, schemas *terraform.Schemas) int { + // Prefer to display a pre-built JSON plan, if we got one; then, fall back + // to building one ourselves. + if planJSON != nil { + if planJSON.Redacted { + v.view.streams.Eprintf("Didn't get external JSON plan format") + return 1 + } + v.view.streams.Println(string(planJSON.JSONBytes)) + } else if plan != nil { + planJSON, err := jsonplan.Marshal(config, plan, stateFile, schemas) if err != nil { v.view.streams.Eprintf("Failed to marshal plan to json: %s", err) return 1 } - v.view.streams.Println(string(jsonPlan)) + v.view.streams.Println(string(planJSON)) } else { // It is possible that there is neither state nor a plan. // That's ok, we'll just return an empty object. diff --git a/internal/command/views/show_test.go b/internal/command/views/show_test.go index fe69130d98..b1bcc4a3d3 100644 --- a/internal/command/views/show_test.go +++ b/internal/command/views/show_test.go @@ -1,15 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( "encoding/json" + "os" "strings" "testing" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/cloud/cloudplan" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/terminal" @@ -19,8 +25,14 @@ import ( ) func TestShowHuman(t *testing.T) { + redactedPath := "./testdata/plans/redacted-plan.json" + redactedPlanJson, err := os.ReadFile(redactedPath) + if err != nil { + t.Fatalf("couldn't read json plan test data at %s for showing a cloud plan. Did the file get moved?", redactedPath) + } testCases := map[string]struct { plan *plans.Plan + jsonPlan *cloudplan.RemotePlanJSON stateFile *statefile.File schemas *terraform.Schemas wantExact bool @@ -29,11 +41,28 @@ func TestShowHuman(t *testing.T) { "plan file": { testPlan(t), nil, + nil, testSchemas(), false, "# test_resource.foo will be created", }, + "cloud plan file": { + nil, + &cloudplan.RemotePlanJSON{ + JSONBytes: redactedPlanJson, + Redacted: true, + Mode: plans.NormalMode, + Qualities: []plans.Quality{}, + RunHeader: "[reset][yellow]To view this run in a browser, visit:\nhttps://app.terraform.io/app/example_org/example_workspace/runs/run-run-bugsBUGSbugsBUGS[reset]", + RunFooter: "[reset][green]Run status: planned and saved (confirmable)[reset]\n[green]Workspace is unlocked[reset]", + }, + nil, + nil, + false, + "# null_resource.foo will be created", + }, "statefile": { + nil, nil, &statefile.File{ Serial: 0, @@ -45,6 +74,7 @@ func TestShowHuman(t *testing.T) { "# test_resource.foo:", }, "empty statefile": { + nil, nil, &statefile.File{ Serial: 0, @@ -53,12 +83,13 @@ func TestShowHuman(t *testing.T) { }, testSchemas(), true, - "\n", + "The state file is empty. No resources are represented.\n", }, "nothing": { nil, nil, nil, + nil, true, "No state.\n", }, @@ -70,7 +101,7 @@ func TestShowHuman(t *testing.T) { view.Configure(&arguments.View{NoColor: true}) v := NewShow(arguments.ViewHuman, view) - code := v.Display(nil, testCase.plan, testCase.stateFile, testCase.schemas) + code := v.Display(nil, testCase.plan, testCase.jsonPlan, testCase.stateFile, testCase.schemas) if code != 0 { t.Errorf("expected 0 return code, got %d", code) } @@ -86,15 +117,35 @@ func TestShowHuman(t *testing.T) { } func TestShowJSON(t *testing.T) { + unredactedPath := "../testdata/show-json/basic-create/output.json" + unredactedPlanJson, err := os.ReadFile(unredactedPath) + if err != nil { + t.Fatalf("couldn't read json plan test data at %s for showing a cloud plan. Did the file get moved?", unredactedPath) + } testCases := map[string]struct { plan *plans.Plan + jsonPlan *cloudplan.RemotePlanJSON stateFile *statefile.File }{ "plan file": { testPlan(t), nil, + nil, + }, + "cloud plan file": { + nil, + &cloudplan.RemotePlanJSON{ + JSONBytes: unredactedPlanJson, + Redacted: false, + Mode: plans.NormalMode, + Qualities: []plans.Quality{}, + RunHeader: "[reset][yellow]To view this run in a browser, visit:\nhttps://app.terraform.io/app/example_org/example_workspace/runs/run-run-bugsBUGSbugsBUGS[reset]", + RunFooter: "[reset][green]Run status: planned and saved (confirmable)[reset]\n[green]Workspace is unlocked[reset]", + }, + nil, }, "statefile": { + nil, nil, &statefile.File{ Serial: 0, @@ -103,6 +154,7 @@ func TestShowJSON(t *testing.T) { }, }, "empty statefile": { + nil, nil, &statefile.File{ Serial: 0, @@ -113,10 +165,11 @@ func TestShowJSON(t *testing.T) { "nothing": { nil, nil, + nil, }, } - config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show") + config, _, configCleanup := initwd.MustLoadConfigForTests(t, "./testdata/show", "tests") defer configCleanup() for name, testCase := range testCases { @@ -127,13 +180,15 @@ func TestShowJSON(t *testing.T) { v := NewShow(arguments.ViewJSON, view) schemas := &terraform.Schemas{ - Providers: map[addrs.Provider]*terraform.ProviderSchema{ + Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.NewDefaultProvider("test"): { - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_resource": { - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Optional: true, Computed: true}, - "foo": {Type: cty.String, Optional: true}, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "foo": {Type: cty.String, Optional: true}, + }, }, }, }, @@ -141,7 +196,7 @@ func TestShowJSON(t *testing.T) { }, } - code := v.Display(config, testCase.plan, testCase.stateFile, schemas) + code := v.Display(config, testCase.plan, testCase.jsonPlan, testCase.stateFile, schemas) if code != 0 { t.Errorf("expected 0 return code, got %d", code) diff --git a/internal/command/views/state_locker.go b/internal/command/views/state_locker.go index df3d51b0f9..575e6a1415 100644 --- a/internal/command/views/state_locker.go +++ b/internal/command/views/state_locker.go @@ -1,7 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( + "encoding/json" "fmt" + "time" "github.com/hashicorp/terraform/internal/command/arguments" ) @@ -18,6 +23,8 @@ func NewStateLocker(vt arguments.ViewType, view *View) StateLocker { switch vt { case arguments.ViewHuman: return &StateLockerHuman{view: view} + case arguments.ViewJSON: + return &StateLockerJSON{view: view} default: panic(fmt.Sprintf("unknown view type %v", vt)) } @@ -30,6 +37,7 @@ type StateLockerHuman struct { } var _ StateLocker = (*StateLockerHuman)(nil) +var _ StateLocker = (*StateLockerJSON)(nil) func (v *StateLockerHuman) Locking() { v.view.streams.Println("Acquiring state lock. This may take a few moments...") @@ -38,3 +46,37 @@ func (v *StateLockerHuman) Locking() { func (v *StateLockerHuman) Unlocking() { v.view.streams.Println("Releasing state lock. This may take a few moments...") } + +// StateLockerJSON is an implementation of StateLocker which prints the state lock status +// to a terminal in machine-readable JSON form. +type StateLockerJSON struct { + view *View +} + +func (v *StateLockerJSON) Locking() { + current_timestamp := time.Now().Format(time.RFC3339) + + json_data := map[string]string{ + "@level": "info", + "@message": "Acquiring state lock. This may take a few moments...", + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "state_lock_acquire"} + + lock_info_message, _ := json.Marshal(json_data) + v.view.streams.Println(string(lock_info_message)) +} + +func (v *StateLockerJSON) Unlocking() { + current_timestamp := time.Now().Format(time.RFC3339) + + json_data := map[string]string{ + "@level": "info", + "@message": "Releasing state lock. This may take a few moments...", + "@module": "terraform.ui", + "@timestamp": current_timestamp, + "type": "state_lock_release"} + + lock_info_message, _ := json.Marshal(json_data) + v.view.streams.Println(string(lock_info_message)) +} diff --git a/internal/command/views/test.go b/internal/command/views/test.go index 18c32c747b..6f8135c2e0 100644 --- a/internal/command/views/test.go +++ b/internal/command/views/test.go @@ -1,373 +1,762 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( - "encoding/xml" + "bytes" "fmt" - "io/ioutil" - "sort" - "strings" + "net/http" + "time" + "github.com/hashicorp/go-tfe" + "github.com/mitchellh/colorstring" + + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/format" + "github.com/hashicorp/terraform/internal/command/jsonformat" + "github.com/hashicorp/terraform/internal/command/jsonplan" + "github.com/hashicorp/terraform/internal/command/jsonprovider" + "github.com/hashicorp/terraform/internal/command/jsonstate" + "github.com/hashicorp/terraform/internal/command/views/json" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/moduletest" - "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/colorstring" ) -// Test is the view interface for the "terraform test" command. +// Test renders outputs for test executions. type Test interface { - // Results presents the given test results. - Results(map[string]*moduletest.Suite) tfdiags.Diagnostics + // Abstract should print an early summary of the tests that will be + // executed. This will be called before the tests have been executed so + // the status for everything within suite will be test.Pending. + // + // This should be used to state what is going to be tested. + Abstract(suite *moduletest.Suite) - // Diagnostics is for reporting warnings or errors that occurred with the - // mechanics of running tests. For this command in particular, some - // errors are considered to be test failures rather than mechanism failures, - // and so those will be reported via Results rather than via Diagnostics. - Diagnostics(tfdiags.Diagnostics) + // Conclusion should print out a summary of the tests including their + // completed status. + Conclusion(suite *moduletest.Suite) + + // File prints out the summary for an entire test file. + File(file *moduletest.File, progress moduletest.Progress) + + // Run prints out the summary for a single test run block. + Run(run *moduletest.Run, file *moduletest.File, progress moduletest.Progress, elapsed int64) + + // DestroySummary prints out the summary of the destroy step of each test + // file. If everything goes well, this should be empty. + DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) + + // Diagnostics prints out the provided diagnostics. + Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) + + // Interrupted prints out a message stating that an interrupt has been + // received and testing will stop. + Interrupted() + + // FatalInterrupt prints out a message stating that a hard interrupt has + // been received and testing will stop and cleanup will be skipped. + FatalInterrupt() + + // FatalInterruptSummary prints out the resources that were held in state + // and were being created at the time the FatalInterrupt was received. + // + // This will typically be called in place of DestroySummary, as there is no + // guarantee that this function will be called during a FatalInterrupt. In + // addition, this function prints additional details about the current + // operation alongside the current state as the state will be missing newly + // created resources that also need to be handled manually. + FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, states map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) + + // TFCStatusUpdate prints a reassuring update, letting users know the latest + // status of their ongoing remote test run. + TFCStatusUpdate(status tfe.TestRunStatus, elapsed time.Duration) + + // TFCRetryHook prints an update if a request failed and is being retried. + TFCRetryHook(attemptNum int, resp *http.Response) } -// NewTest returns an implementation of Test configured to respect the -// settings described in the given arguments. -func NewTest(base *View, args arguments.TestOutput) Test { - return &testHuman{ - streams: base.streams, - showDiagnostics: base.Diagnostics, - colorize: base.colorize, - junitXMLFile: args.JUnitXMLFile, +func NewTest(vt arguments.ViewType, view *View) Test { + switch vt { + case arguments.ViewJSON: + return &TestJSON{ + view: NewJSONView(view), + } + case arguments.ViewHuman: + return &TestHuman{ + view: view, + } + default: + panic(fmt.Sprintf("unknown view type %v", vt)) } } -type testHuman struct { - // This is the subset of functionality we need from the base view. - streams *terminal.Streams - showDiagnostics func(diags tfdiags.Diagnostics) - colorize *colorstring.Colorize +type TestHuman struct { + CloudHooks - // If junitXMLFile is not empty then results will be written to - // the given file path in addition to the usual output. - junitXMLFile string + view *View } -func (v *testHuman) Results(results map[string]*moduletest.Suite) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics +var _ Test = (*TestHuman)(nil) - // FIXME: Due to how this prototype command evolved concurrently with - // establishing the idea of command views, the handling of JUnit output - // as part of the "human" view rather than as a separate view in its - // own right is a little odd and awkward. We should refactor this - // prior to making "terraform test" a real supported command to make - // it be structured more like the other commands that use the views - // package. +func (t *TestHuman) Abstract(_ *moduletest.Suite) { + // Do nothing, we don't print an abstract for the human view. +} - v.humanResults(results) +func (t *TestHuman) Conclusion(suite *moduletest.Suite) { + t.view.streams.Println() - if v.junitXMLFile != "" { - moreDiags := v.junitXMLResults(results, v.junitXMLFile) - diags = diags.Append(moreDiags) + counts := make(map[moduletest.Status]int) + for _, file := range suite.Files { + for _, run := range file.Runs { + count := counts[run.Status] + counts[run.Status] = count + 1 + } } - return diags -} - -func (v *testHuman) Diagnostics(diags tfdiags.Diagnostics) { - if len(diags) == 0 { + if suite.Status <= moduletest.Skip { + // Then no tests. + t.view.streams.Print("Executed 0 tests") + if counts[moduletest.Skip] > 0 { + t.view.streams.Printf(", %d skipped.\n", counts[moduletest.Skip]) + } else { + t.view.streams.Println(".") + } return } - v.showDiagnostics(diags) + + if suite.Status == moduletest.Pass { + t.view.streams.Print(t.view.colorize.Color("[green]Success![reset]")) + } else { + t.view.streams.Print(t.view.colorize.Color("[red]Failure![reset]")) + } + + t.view.streams.Printf(" %d passed, %d failed", counts[moduletest.Pass], counts[moduletest.Fail]+counts[moduletest.Error]) + if counts[moduletest.Skip] > 0 { + t.view.streams.Printf(", %d skipped.\n", counts[moduletest.Skip]) + } else { + t.view.streams.Println(".") + } } -func (v *testHuman) humanResults(results map[string]*moduletest.Suite) { - failCount := 0 - width := v.streams.Stderr.Columns() - - suiteNames := make([]string, 0, len(results)) - for suiteName := range results { - suiteNames = append(suiteNames, suiteName) +func (t *TestHuman) File(file *moduletest.File, progress moduletest.Progress) { + switch progress { + case moduletest.Starting, moduletest.Running: + t.view.streams.Printf(t.view.colorize.Color("%s... [light_gray]in progress[reset]\n"), file.Name) + case moduletest.TearDown: + t.view.streams.Printf(t.view.colorize.Color("%s... [light_gray]tearing down[reset]\n"), file.Name) + case moduletest.Complete: + t.view.streams.Printf("%s... %s\n", file.Name, colorizeTestStatus(file.Status, t.view.colorize)) + t.Diagnostics(nil, file, file.Diagnostics) + default: + panic("unrecognized test progress: " + progress.String()) } - sort.Strings(suiteNames) - for _, suiteName := range suiteNames { - suite := results[suiteName] +} - componentNames := make([]string, 0, len(suite.Components)) - for componentName := range suite.Components { - componentNames = append(componentNames, componentName) +func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File, progress moduletest.Progress, _ int64) { + switch progress { + case moduletest.Starting, moduletest.Running, moduletest.TearDown: + return // We don't print progress updates in human mode + case moduletest.Complete: + // Do nothing, the rest of the function handles this. + default: + panic("unrecognized test progress: " + progress.String()) + } + + t.view.streams.Printf(" run %q... %s\n", run.Name, colorizeTestStatus(run.Status, t.view.colorize)) + + if run.Verbose != nil { + // We're going to be more verbose about what we print, here's the plan + // or the state depending on the type of run we did. + + schemas := &terraform.Schemas{ + Providers: run.Verbose.Providers, + Provisioners: run.Verbose.Provisioners, } - for _, componentName := range componentNames { - component := suite.Components[componentName] - assertionNames := make([]string, 0, len(component.Assertions)) - for assertionName := range component.Assertions { - assertionNames = append(assertionNames, assertionName) + renderer := jsonformat.Renderer{ + Streams: t.view.streams, + Colorize: t.view.colorize, + RunningInAutomation: t.view.runningInAutomation, + } + + if run.Config.Command == configs.ApplyTestCommand { + // Then we'll print the state. + root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas) + if err != nil { + run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to render test state", + fmt.Sprintf("Terraform could not marshal the state for display: %v", err))) + } else { + state := jsonformat.State{ + StateFormatVersion: jsonstate.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + RootModule: root, + RootModuleOutputs: outputs, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + } + + t.view.streams.Println() // Separate the state from any previous statements. + renderer.RenderHumanState(state) + t.view.streams.Println() // Separate the state from any future statements. } - sort.Strings(assertionNames) - - for _, assertionName := range assertionNames { - assertion := component.Assertions[assertionName] - - fullName := fmt.Sprintf("%s.%s.%s", suiteName, componentName, assertionName) - if strings.HasPrefix(componentName, "(") { - // parenthesis-prefixed components are placeholders that - // the test harness generates to represent problems that - // prevented checking any assertions at all, so we'll - // just hide them and show the suite name. - fullName = suiteName - } - headingExtra := fmt.Sprintf("%s (%s)", fullName, assertion.Description) - - switch assertion.Outcome { - case moduletest.Failed: - // Failed means that the assertion was successfully - // excecuted but that the assertion condition didn't hold. - v.eprintRuleHeading("yellow", "Failed", headingExtra) - - case moduletest.Error: - // Error means that the system encountered an unexpected - // error when trying to evaluate the assertion. - v.eprintRuleHeading("red", "Error", headingExtra) - - default: - // We don't do anything for moduletest.Passed or - // moduletest.Skipped. Perhaps in future we'll offer a - // -verbose option to include information about those. - continue - } - failCount++ - - if len(assertion.Message) > 0 { - dispMsg := format.WordWrap(assertion.Message, width) - v.streams.Eprintln(dispMsg) - } - if len(assertion.Diagnostics) > 0 { - // We'll do our own writing of the diagnostics in this - // case, rather than using v.Diagnostics, because we - // specifically want all of these diagnostics to go to - // Stderr along with all of the other output we've - // generated. - for _, diag := range assertion.Diagnostics { - diagStr := format.Diagnostic(diag, nil, v.colorize, width) - v.streams.Eprint(diagStr) - } - } - } - } - } - - if failCount > 0 { - // If we've printed at least one failure then we'll have printed at - // least one horizontal rule across the terminal, and so we'll balance - // that with another horizontal rule. - if width > 1 { - rule := strings.Repeat("─", width-1) - v.streams.Eprintln(v.colorize.Color("[dark_gray]" + rule)) - } - } - - if failCount == 0 { - if len(results) > 0 { - // This is not actually an error, but it's convenient if all of our - // result output goes to the same stream for when this is running in - // automation that might be gathering this output via a pipe. - v.streams.Eprint(v.colorize.Color("[bold][green]Success![reset] All of the test assertions passed.\n\n")) } else { - v.streams.Eprint(v.colorize.Color("[bold][yellow]No tests defined.[reset] This module doesn't have any test suites to run.\n\n")) - } - } - - // Try to flush any buffering that might be happening. (This isn't always - // successful, depending on what sort of fd Stderr is connected to.) - v.streams.Stderr.File.Sync() -} - -func (v *testHuman) junitXMLResults(results map[string]*moduletest.Suite, filename string) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - // "JUnit XML" is a file format that has become a de-facto standard for - // test reporting tools but that is not formally specified anywhere, and - // so each producer and consumer implementation unfortunately tends to - // differ in certain ways from others. - // With that in mind, this is a best effort sort of thing aimed at being - // broadly compatible with various consumers, but it's likely that - // some consumers will present these results better than others. - // This implementation is based mainly on the pseudo-specification of the - // format curated here, based on the Jenkins parser implementation: - // https://llg.cubic.org/docs/junit/ - - // An "Outcome" represents one of the various XML elements allowed inside - // a testcase element to indicate the test outcome. - type Outcome struct { - Message string `xml:"message,omitempty"` - } - - // TestCase represents an individual test case as part of a suite. Note - // that a JUnit XML incorporates both the "component" and "assertion" - // levels of our model: we pretend that component is a class name and - // assertion is a method name in order to match with the Java-flavored - // expectations of JUnit XML, which are hopefully close enough to get - // a test result rendering that's useful to humans. - type TestCase struct { - AssertionName string `xml:"name"` - ComponentName string `xml:"classname"` - - // These fields represent the different outcomes of a TestCase. Only one - // of these should be populated in each TestCase; this awkward - // structure is just to make this play nicely with encoding/xml's - // expecatations. - Skipped *Outcome `xml:"skipped,omitempty"` - Error *Outcome `xml:"error,omitempty"` - Failure *Outcome `xml:"failure,omitempty"` - - Stderr string `xml:"system-out,omitempty"` - } - - // TestSuite represents an individual test suite, of potentially many - // in a JUnit XML document. - type TestSuite struct { - Name string `xml:"name"` - TotalCount int `xml:"tests"` - SkippedCount int `xml:"skipped"` - ErrorCount int `xml:"errors"` - FailureCount int `xml:"failures"` - Cases []*TestCase `xml:"testcase"` - } - - // TestSuites represents the root element of the XML document. - type TestSuites struct { - XMLName struct{} `xml:"testsuites"` - ErrorCount int `xml:"errors"` - FailureCount int `xml:"failures"` - TotalCount int `xml:"tests"` - Suites []*TestSuite `xml:"testsuite"` - } - - xmlSuites := TestSuites{} - suiteNames := make([]string, 0, len(results)) - for suiteName := range results { - suiteNames = append(suiteNames, suiteName) - } - sort.Strings(suiteNames) - for _, suiteName := range suiteNames { - suite := results[suiteName] - - xmlSuite := &TestSuite{ - Name: suiteName, - } - xmlSuites.Suites = append(xmlSuites.Suites, xmlSuite) - - componentNames := make([]string, 0, len(suite.Components)) - for componentName := range suite.Components { - componentNames = append(componentNames, componentName) - } - for _, componentName := range componentNames { - component := suite.Components[componentName] - - assertionNames := make([]string, 0, len(component.Assertions)) - for assertionName := range component.Assertions { - assertionNames = append(assertionNames, assertionName) - } - sort.Strings(assertionNames) - - for _, assertionName := range assertionNames { - assertion := component.Assertions[assertionName] - xmlSuites.TotalCount++ - xmlSuite.TotalCount++ - - xmlCase := &TestCase{ - ComponentName: componentName, - AssertionName: assertionName, - } - xmlSuite.Cases = append(xmlSuite.Cases, xmlCase) - - switch assertion.Outcome { - case moduletest.Pending: - // We represent "pending" cases -- cases blocked by - // upstream errors -- as if they were "skipped" in JUnit - // terms, because we didn't actually check them and so - // can't say whether they succeeded or not. - xmlSuite.SkippedCount++ - xmlCase.Skipped = &Outcome{ - Message: assertion.Message, - } - case moduletest.Failed: - xmlSuites.FailureCount++ - xmlSuite.FailureCount++ - xmlCase.Failure = &Outcome{ - Message: assertion.Message, - } - case moduletest.Error: - xmlSuites.ErrorCount++ - xmlSuite.ErrorCount++ - xmlCase.Error = &Outcome{ - Message: assertion.Message, - } - - // We'll also include the diagnostics in the "stderr" - // portion of the output, so they'll hopefully be visible - // in a test log viewer in JUnit-XML-Consuming CI systems. - var buf strings.Builder - for _, diag := range assertion.Diagnostics { - diagStr := format.DiagnosticPlain(diag, nil, 68) - buf.WriteString(diagStr) - } - xmlCase.Stderr = buf.String() + // We'll print the plan. + outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(run.Verbose.Plan, schemas) + if err != nil { + run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to render test plan", + fmt.Sprintf("Terraform could not marshal the plan for display: %v", err))) + } else { + plan := jsonformat.Plan{ + PlanFormatVersion: jsonplan.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + OutputChanges: outputs, + ResourceChanges: changed, + ResourceDrift: drift, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + RelevantAttributes: attrs, } + var opts []plans.Quality + if run.Verbose.Plan.Errored { + opts = append(opts, plans.Errored) + } else if !run.Verbose.Plan.Applyable { + opts = append(opts, plans.NoChanges) + } + + renderer.RenderHumanPlan(plan, run.Verbose.Plan.UIMode, opts...) + t.view.streams.Println() // Separate the plan from any future statements. } } } - xmlOut, err := xml.MarshalIndent(&xmlSuites, "", " ") - if err != nil { - // If marshalling fails then that's a bug in the code above, - // because we should always be producing a value that is - // accepted by encoding/xml. - panic(fmt.Sprintf("invalid values to marshal as JUnit XML: %s", err)) + // Finally we'll print out a summary of the diagnostics from the run. + t.Diagnostics(run, file, run.Diagnostics) + + var warnings bool + for _, diag := range run.Diagnostics { + switch diag.Severity() { + case tfdiags.Error: + // do nothing + case tfdiags.Warning: + warnings = true + } + + if warnings { + // We only care about checking if we printed any warnings in the + // previous output. + break + } } - err = ioutil.WriteFile(filename, xmlOut, 0644) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to write JUnit XML file", - fmt.Sprintf( - "Could not create %s to record the test results in JUnit XML format: %s.", - filename, - err, - ), - )) + if warnings { + // warnings are printed to stdout, so we'll put a new line into stdout + // to separate any future statements info statements. + t.view.streams.Println() } - - return diags } -func (v *testHuman) eprintRuleHeading(color, prefix, extra string) { - const lineCell string = "─" - textLen := len(prefix) + len(": ") + len(extra) - spacingLen := 2 - leftLineLen := 3 - - rightLineLen := 0 - width := v.streams.Stderr.Columns() - if (textLen + spacingLen + leftLineLen) < (width - 1) { - // (we allow an extra column at the end because some terminals can't - // print in the final column without wrapping to the next line) - rightLineLen = width - (textLen + spacingLen + leftLineLen) - 1 +func (t *TestHuman) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) { + identifier := file.Name + if run != nil { + identifier = fmt.Sprintf("%s/%s", identifier, run.Name) } - colorCode := "[" + color + "]" - - // We'll prepare what we're going to print in memory first, so that we can - // send it all to stderr in one write in case other programs are also - // concurrently trying to write to the terminal for some reason. - var buf strings.Builder - buf.WriteString(v.colorize.Color(colorCode + strings.Repeat(lineCell, leftLineLen))) - buf.WriteByte(' ') - buf.WriteString(v.colorize.Color("[bold]" + colorCode + prefix + ":")) - buf.WriteByte(' ') - buf.WriteString(extra) - if rightLineLen > 0 { - buf.WriteByte(' ') - buf.WriteString(v.colorize.Color(colorCode + strings.Repeat(lineCell, rightLineLen))) + if diags.HasErrors() { + t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("Terraform encountered an error destroying resources created while executing %s.\n", identifier), t.view.errorColumns())) + } + t.Diagnostics(run, file, diags) + + if state.HasManagedResourceInstanceObjects() { + // FIXME: This message says "resources" but this is actually a list + // of resource instance objects. + t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform left the following resources in state after executing %s, and they need to be cleaned up manually:\n", identifier), t.view.errorColumns())) + for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { + if resource.DeposedKey != states.NotDeposed { + t.view.streams.Eprintf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) + continue + } + t.view.streams.Eprintf(" - %s\n", resource.ResourceInstance) + } + } +} + +func (t *TestHuman) Diagnostics(_ *moduletest.Run, _ *moduletest.File, diags tfdiags.Diagnostics) { + t.view.Diagnostics(diags) +} + +func (t *TestHuman) Interrupted() { + t.view.streams.Eprintln(format.WordWrap(interrupted, t.view.errorColumns())) +} + +func (t *TestHuman) FatalInterrupt() { + t.view.streams.Eprintln(format.WordWrap(fatalInterrupt, t.view.errorColumns())) +} + +func (t *TestHuman) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) { + t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform was interrupted while executing %s, and may not have performed the expected cleanup operations.\n", file.Name), t.view.errorColumns())) + + // Print out the main state first, this is the state that isn't associated + // with a run block. + if state, exists := existingStates[nil]; exists && !state.Empty() { + t.view.streams.Eprint(format.WordWrap("\nTerraform has already created the following resources from the module under test:\n", t.view.errorColumns())) + for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { + if resource.DeposedKey != states.NotDeposed { + t.view.streams.Eprintf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) + continue + } + t.view.streams.Eprintf(" - %s\n", resource.ResourceInstance) + } + } + + // Then print out the other states in order. + for _, run := range file.Runs { + state, exists := existingStates[run] + if !exists || state.Empty() { + continue + } + + t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform has already created the following resources for %q from %q:\n", run.Name, run.Config.Module.Source), t.view.errorColumns())) + for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { + if resource.DeposedKey != states.NotDeposed { + t.view.streams.Eprintf(" - %s (%s)\n", resource.ResourceInstance, resource.DeposedKey) + continue + } + t.view.streams.Eprintf(" - %s\n", resource.ResourceInstance) + } + } + + if len(created) == 0 { + // No planned changes, so we won't print anything. + return + } + + var resources []string + for _, change := range created { + resources = append(resources, change.Addr.String()) + } + + if len(resources) > 0 { + module := "the module under test" + if run.Config.ConfigUnderTest != nil { + module = fmt.Sprintf("%q", run.Config.Module.Source.String()) + } + + t.view.streams.Eprint(format.WordWrap(fmt.Sprintf("\nTerraform was in the process of creating the following resources for %q from %s, and they may not have been destroyed:\n", run.Name, module), t.view.errorColumns())) + for _, resource := range resources { + t.view.streams.Eprintf(" - %s\n", resource) + } + } +} + +func (t *TestHuman) TFCStatusUpdate(status tfe.TestRunStatus, elapsed time.Duration) { + switch status { + case tfe.TestRunQueued: + t.view.streams.Printf("Waiting for the tests to start... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) + case tfe.TestRunRunning: + t.view.streams.Printf("Waiting for the tests to complete... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) + } +} + +func (t *TestHuman) TFCRetryHook(attemptNum int, resp *http.Response) { + t.view.streams.Println(t.view.colorize.Color(t.RetryLogHook(attemptNum, resp, true))) +} + +type TestJSON struct { + CloudHooks + + view *JSONView +} + +var _ Test = (*TestJSON)(nil) + +func (t *TestJSON) Abstract(suite *moduletest.Suite) { + var fileCount, runCount int + + abstract := json.TestSuiteAbstract{} + for name, file := range suite.Files { + fileCount++ + var runs []string + for _, run := range file.Runs { + runCount++ + runs = append(runs, run.Name) + } + abstract[name] = runs + } + + files := "files" + runs := "run blocks" + + if fileCount == 1 { + files = "file" + } + + if runCount == 1 { + runs = "run block" + } + + t.view.log.Info( + fmt.Sprintf("Found %d %s and %d %s", fileCount, files, runCount, runs), + "type", json.MessageTestAbstract, + json.MessageTestAbstract, abstract) +} + +func (t *TestJSON) Conclusion(suite *moduletest.Suite) { + summary := json.TestSuiteSummary{ + Status: json.ToTestStatus(suite.Status), + } + for _, file := range suite.Files { + for _, run := range file.Runs { + switch run.Status { + case moduletest.Skip: + summary.Skipped++ + case moduletest.Pass: + summary.Passed++ + case moduletest.Error: + summary.Errored++ + case moduletest.Fail: + summary.Failed++ + } + } + } + + var message bytes.Buffer + if suite.Status <= moduletest.Skip { + // Then no tests. + message.WriteString("Executed 0 tests") + if summary.Skipped > 0 { + message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped)) + } else { + message.WriteString(".") + } + } else { + if suite.Status == moduletest.Pass { + message.WriteString("Success!") + } else { + message.WriteString("Failure!") + } + + message.WriteString(fmt.Sprintf(" %d passed, %d failed", summary.Passed, summary.Failed+summary.Errored)) + if summary.Skipped > 0 { + message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped)) + } else { + message.WriteString(".") + } + } + + t.view.log.Info( + message.String(), + "type", json.MessageTestSummary, + json.MessageTestSummary, summary) +} + +func (t *TestJSON) File(file *moduletest.File, progress moduletest.Progress) { + switch progress { + case moduletest.Starting, moduletest.Running: + t.view.log.Info( + fmt.Sprintf("%s... in progress", file.Name), + "type", json.MessageTestFile, + json.MessageTestFile, json.TestFileStatus{ + Path: file.Name, + Progress: json.ToTestProgress(moduletest.Starting), + }, + "@testfile", file.Name) + case moduletest.TearDown: + t.view.log.Info( + fmt.Sprintf("%s... tearing down", file.Name), + "type", json.MessageTestFile, + json.MessageTestFile, json.TestFileStatus{ + Path: file.Name, + Progress: json.ToTestProgress(moduletest.TearDown), + }, + "@testfile", file.Name) + case moduletest.Complete: + t.view.log.Info( + fmt.Sprintf("%s... %s", file.Name, testStatus(file.Status)), + "type", json.MessageTestFile, + json.MessageTestFile, json.TestFileStatus{ + Path: file.Name, + Progress: json.ToTestProgress(moduletest.Complete), + Status: json.ToTestStatus(file.Status), + }, + "@testfile", file.Name) + t.Diagnostics(nil, file, file.Diagnostics) + default: + panic("unrecognized test progress: " + progress.String()) + } +} + +func (t *TestJSON) Run(run *moduletest.Run, file *moduletest.File, progress moduletest.Progress, elapsed int64) { + switch progress { + case moduletest.Starting, moduletest.Running: + t.view.log.Info( + fmt.Sprintf(" %q... in progress", run.Name), + "type", json.MessageTestRun, + json.MessageTestRun, json.TestRunStatus{ + Path: file.Name, + Run: run.Name, + Progress: json.ToTestProgress(progress), + Elapsed: &elapsed, + }, + "@testfile", file.Name, + "@testrun", run.Name) + return + case moduletest.TearDown: + t.view.log.Info( + fmt.Sprintf(" %q... tearing down", run.Name), + "type", json.MessageTestRun, + json.MessageTestRun, json.TestRunStatus{ + Path: file.Name, + Run: run.Name, + Progress: json.ToTestProgress(progress), + Elapsed: &elapsed, + }, + "@testfile", file.Name, + "@testrun", run.Name) + return + case moduletest.Complete: + // Do nothing, the rest of the function handles this case. + default: + panic("unrecognized test progress: " + progress.String()) + } + + t.view.log.Info( + fmt.Sprintf(" %q... %s", run.Name, testStatus(run.Status)), + "type", json.MessageTestRun, + json.MessageTestRun, json.TestRunStatus{ + Path: file.Name, + Run: run.Name, + Progress: json.ToTestProgress(progress), + Status: json.ToTestStatus(run.Status)}, + "@testfile", file.Name, + "@testrun", run.Name) + + if run.Verbose != nil { + + schemas := &terraform.Schemas{ + Providers: run.Verbose.Providers, + Provisioners: run.Verbose.Provisioners, + } + + if run.Config.Command == configs.ApplyTestCommand { + // Then we'll print the state. + root, outputs, err := jsonstate.MarshalForRenderer(statefile.New(run.Verbose.State, file.Name, uint64(run.Index)), schemas) + if err != nil { + run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to render test state", + fmt.Sprintf("Terraform could not marshal the state for display: %v", err))) + } else { + state := jsonformat.State{ + StateFormatVersion: jsonstate.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + RootModule: root, + RootModuleOutputs: outputs, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + } + + t.view.log.Info( + "-verbose flag enabled, printing state", + "type", json.MessageTestState, + json.MessageTestState, state, + "@testfile", file.Name, + "@testrun", run.Name) + } + } else { + outputs, changed, drift, attrs, err := jsonplan.MarshalForRenderer(run.Verbose.Plan, schemas) + if err != nil { + run.Diagnostics = run.Diagnostics.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to render test plan", + fmt.Sprintf("Terraform could not marshal the plan for display: %v", err))) + } else { + plan := jsonformat.Plan{ + PlanFormatVersion: jsonplan.FormatVersion, + ProviderFormatVersion: jsonprovider.FormatVersion, + OutputChanges: outputs, + ResourceChanges: changed, + ResourceDrift: drift, + ProviderSchemas: jsonprovider.MarshalForRenderer(schemas), + RelevantAttributes: attrs, + } + + t.view.log.Info( + "-verbose flag enabled, printing plan", + "type", json.MessageTestPlan, + json.MessageTestPlan, plan, + "@testfile", file.Name, + "@testrun", run.Name) + } + } + } + + t.Diagnostics(run, file, run.Diagnostics) +} + +func (t *TestJSON) DestroySummary(diags tfdiags.Diagnostics, run *moduletest.Run, file *moduletest.File, state *states.State) { + if state.HasManagedResourceInstanceObjects() { + cleanup := json.TestFileCleanup{} + for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { + cleanup.FailedResources = append(cleanup.FailedResources, json.TestFailedResource{ + Instance: resource.ResourceInstance.String(), + DeposedKey: resource.DeposedKey.String(), + }) + } + + if run != nil { + t.view.log.Error( + fmt.Sprintf("Terraform left some resources in state after executing %s/%s, they need to be cleaned up manually.", file.Name, run.Name), + "type", json.MessageTestCleanup, + json.MessageTestCleanup, cleanup, + "@testfile", file.Name, + "@testrun", run.Name) + } else { + t.view.log.Error( + fmt.Sprintf("Terraform left some resources in state after executing %s, they need to be cleaned up manually.", file.Name), + "type", json.MessageTestCleanup, + json.MessageTestCleanup, cleanup, + "@testfile", file.Name) + } + + } + + t.Diagnostics(run, file, diags) +} + +func (t *TestJSON) Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) { + var metadata []interface{} + if file != nil { + metadata = append(metadata, "@testfile", file.Name) + } + if run != nil { + metadata = append(metadata, "@testrun", run.Name) + } + t.view.Diagnostics(diags, metadata...) +} + +func (t *TestJSON) Interrupted() { + t.view.Log(interrupted) +} + +func (t *TestJSON) FatalInterrupt() { + t.view.Log(fatalInterrupt) +} + +func (t *TestJSON) FatalInterruptSummary(run *moduletest.Run, file *moduletest.File, existingStates map[*moduletest.Run]*states.State, created []*plans.ResourceInstanceChangeSrc) { + + message := json.TestFatalInterrupt{ + States: make(map[string][]json.TestFailedResource), + } + + for run, state := range existingStates { + if state.Empty() { + continue + } + + var resources []json.TestFailedResource + for _, resource := range addrs.SetSortedNatural(state.AllManagedResourceInstanceObjectAddrs()) { + resources = append(resources, json.TestFailedResource{ + Instance: resource.ResourceInstance.String(), + DeposedKey: resource.DeposedKey.String(), + }) + } + + if run == nil { + message.State = resources + } else { + message.States[run.Name] = resources + } + } + + if len(created) > 0 { + for _, change := range created { + message.Planned = append(message.Planned, change.Addr.String()) + } + } + + if len(message.States) == 0 && len(message.State) == 0 && len(message.Planned) == 0 { + // Then we don't have any information to share with the user. + return + } + + if run != nil { + t.view.log.Error( + "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.", + "type", json.MessageTestInterrupt, + json.MessageTestInterrupt, message, + "@testfile", file.Name, + "@testrun", run.Name) + } else { + t.view.log.Error( + "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.", + "type", json.MessageTestInterrupt, + json.MessageTestInterrupt, message, + "@testfile", file.Name) + } +} + +func (t *TestJSON) TFCStatusUpdate(status tfe.TestRunStatus, elapsed time.Duration) { + var message string + switch status { + case tfe.TestRunQueued: + message = fmt.Sprintf("Waiting for the tests to start... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) + case tfe.TestRunRunning: + message = fmt.Sprintf("Waiting for the tests to complete... (%s elapsed)\n", elapsed.Truncate(30*time.Second)) + default: + // Don't care about updates for other statuses. + return + } + + t.view.log.Info( + message, + "type", json.MessageTestStatus, + json.MessageTestStatus, json.TestStatusUpdate{ + Status: string(status), + Duration: elapsed.Seconds(), + }) +} + +func (t *TestJSON) TFCRetryHook(attemptNum int, resp *http.Response) { + t.view.log.Error( + t.RetryLogHook(attemptNum, resp, false), + "type", json.MessageTestRetry, + ) +} + +func colorizeTestStatus(status moduletest.Status, color *colorstring.Colorize) string { + switch status { + case moduletest.Error, moduletest.Fail: + return color.Color("[red]fail[reset]") + case moduletest.Pass: + return color.Color("[green]pass[reset]") + case moduletest.Skip: + return color.Color("[light_gray]skip[reset]") + case moduletest.Pending: + return color.Color("[light_gray]pending[reset]") + default: + panic("unrecognized status: " + status.String()) + } +} + +func testStatus(status moduletest.Status) string { + switch status { + case moduletest.Error, moduletest.Fail: + return "fail" + case moduletest.Pass: + return "pass" + case moduletest.Skip: + return "skip" + case moduletest.Pending: + return "pending" + default: + panic("unrecognized status: " + status.String()) } - v.streams.Eprintln(buf.String()) } diff --git a/internal/command/views/test_test.go b/internal/command/views/test_test.go index 6acd889e85..2d75449f7b 100644 --- a/internal/command/views/test_test.go +++ b/internal/command/views/test_test.go @@ -1,32 +1,3290 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/command/arguments" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/terminal" + "github.com/hashicorp/terraform/internal/tfdiags" ) -func TestTest(t *testing.T) { - streams, close := terminal.StreamsForTesting(t) - baseView := NewView(streams) - view := NewTest(baseView, arguments.TestOutput{ - JUnitXMLFile: "", - }) +func TestTestHuman_Conclusion(t *testing.T) { + tcs := map[string]struct { + Suite *moduletest.Suite + Expected string + }{ + "no tests": { + Suite: &moduletest.Suite{}, + Expected: "\nExecuted 0 tests.\n", + }, - results := map[string]*moduletest.Suite{} - view.Results(results) + "only skipped tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Skip, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + Expected: "\nExecuted 0 tests, 6 skipped.\n", + }, - output := close(t) - gotOutput := strings.TrimSpace(output.All()) - wantOutput := `No tests defined. This module doesn't have any test suites to run.` - if gotOutput != wantOutput { - t.Errorf("wrong output\ngot:\n%s\nwant:\n%s", gotOutput, wantOutput) + "only passed tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Pass, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + }, + }, + Expected: "\nSuccess! 6 passed, 0 failed.\n", + }, + + "passed and skipped tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Pass, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + }, + }, + Expected: "\nSuccess! 4 passed, 0 failed, 2 skipped.\n", + }, + + "only failed tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Fail, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + }, + }, + Expected: "\nFailure! 0 passed, 6 failed.\n", + }, + + "failed and skipped tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Fail, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + Expected: "\nFailure! 0 passed, 4 failed, 2 skipped.\n", + }, + + "failed, passed and skipped tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Fail, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + }, + }, + Expected: "\nFailure! 2 passed, 2 failed, 2 skipped.\n", + }, + + "failed and errored tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Error, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Error, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Error, + }, + { + Name: "test_three", + Status: moduletest.Error, + }, + }, + }, + }, + }, + Expected: "\nFailure! 0 passed, 6 failed.\n", + }, + + "failed, errored, passed, and skipped tests": { + Suite: &moduletest.Suite{ + Status: moduletest.Error, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Error, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + Expected: "\nFailure! 2 passed, 2 failed, 2 skipped.\n", + }, } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { - // TODO: Test more at this layer. For now, the main UI output tests for - // the "terraform test" command are in the command package as part of - // the overall command tests. + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewHuman, NewView(streams)) + + view.Conclusion(tc.Suite) + + actual := done(t).Stdout() + expected := tc.Expected + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Fatalf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + }) + } +} + +func TestTestHuman_File(t *testing.T) { + tcs := map[string]struct { + File *moduletest.File + Progress moduletest.Progress + Expected string + }{ + "pass": { + File: &moduletest.File{Name: "main.tf", Status: moduletest.Pass}, + Progress: moduletest.Complete, + Expected: "main.tf... pass\n", + }, + + "pending": { + File: &moduletest.File{Name: "main.tf", Status: moduletest.Pending}, + Progress: moduletest.Complete, + Expected: "main.tf... pending\n", + }, + + "skip": { + File: &moduletest.File{Name: "main.tf", Status: moduletest.Skip}, + Progress: moduletest.Complete, + Expected: "main.tf... skip\n", + }, + + "fail": { + File: &moduletest.File{Name: "main.tf", Status: moduletest.Fail}, + Progress: moduletest.Complete, + Expected: "main.tf... fail\n", + }, + + "error": { + File: &moduletest.File{Name: "main.tf", Status: moduletest.Error}, + Progress: moduletest.Complete, + Expected: "main.tf... fail\n", + }, + "starting": { + File: &moduletest.File{Name: "main.tftest.hcl", Status: moduletest.Pending}, + Progress: moduletest.Starting, + Expected: "main.tftest.hcl... in progress\n", + }, + "tear_down": { + File: &moduletest.File{Name: "main.tftest.hcl", Status: moduletest.Pending}, + Progress: moduletest.TearDown, + Expected: "main.tftest.hcl... tearing down\n", + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewHuman, NewView(streams)) + + view.File(tc.File, tc.Progress) + + actual := done(t).Stdout() + expected := tc.Expected + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Fatalf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + }) + } +} + +func TestTestHuman_Run(t *testing.T) { + tcs := map[string]struct { + Run *moduletest.Run + Progress moduletest.Progress + StdOut string + StdErr string + }{ + "pass": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... pass\n", + }, + + "pass_with_diags": { + Run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Pass, + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Warning, "a warning occurred", "some warning happened during this test")}, + }, + Progress: moduletest.Complete, + StdOut: ` run "run_block"... pass + +Warning: a warning occurred + +some warning happened during this test + +`, + }, + + "pending": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pending}, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... pending\n", + }, + + "skip": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Skip}, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... skip\n", + }, + + "fail": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Fail}, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... fail\n", + }, + + "fail_with_diags": { + Run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Fail, + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "a comparison failed", "details details details"), + tfdiags.Sourceless(tfdiags.Error, "a second comparison failed", "other details"), + }, + }, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... fail\n", + StdErr: ` +Error: a comparison failed + +details details details + +Error: a second comparison failed + +other details +`, + }, + + "error": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Error}, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... fail\n", + }, + + "error_with_diags": { + Run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Error, + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "an error occurred", "something bad happened during this test")}, + }, + Progress: moduletest.Complete, + StdOut: " run \"run_block\"... fail\n", + StdErr: ` +Error: an error occurred + +something bad happened during this test +`, + }, + "verbose_plan": { + Run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Pass, + Config: &configs.TestRun{ + Command: configs.PlanTestCommand, + }, + Verbose: &moduletest.Verbose{ + Plan: &plans.Plan{ + Changes: &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "creating", + }, + }, + }, + PrevRunAddr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "creating", + }, + }, + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + After: dynamicValue( + t, + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + }), + cty.Object(map[string]cty.Type{ + "value": cty.String, + })), + }, + }, + }, + }, + }, + State: states.NewState(), // empty state + Config: &configs.Config{}, + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }: { + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Progress: moduletest.Complete, + StdOut: ` run "run_block"... pass + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # test_resource.creating will be created + + resource "test_resource" "creating" { + + value = "Hello, world!" + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +`, + }, + "verbose_apply": { + Run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Pass, + Config: &configs.TestRun{ + Command: configs.ApplyTestCommand, + }, + Verbose: &moduletest.Verbose{ + Plan: &plans.Plan{}, // empty plan + State: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "creating", + }, + }, + }, + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"foobar"}`), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }, + }) + }), + Config: &configs.Config{}, + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }: { + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Progress: moduletest.Complete, + StdOut: ` run "run_block"... pass + +# test_resource.creating: +resource "test_resource" "creating" { + value = "foobar" +} + +`, + }, + // These next three tests should print nothing, as we only report on + // progress complete. + "progress_starting": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + Progress: moduletest.Starting, + }, + "progress_running": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + Progress: moduletest.Running, + }, + "progress_teardown": { + Run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + Progress: moduletest.TearDown, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + file := &moduletest.File{ + Name: "main.tftest.hcl", + } + + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewHuman, NewView(streams)) + + view.Run(tc.Run, file, tc.Progress, 0) + + output := done(t) + actual, expected := output.Stdout(), tc.StdOut + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + actual, expected = output.Stderr(), tc.StdErr + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + }) + } +} + +func TestTestHuman_DestroySummary(t *testing.T) { + tcs := map[string]struct { + diags tfdiags.Diagnostics + run *moduletest.Run + file *moduletest.File + state *states.State + stdout string + stderr string + }{ + "empty": { + diags: nil, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.NewState(), + }, + "empty_state_only_warnings": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.NewState(), + stdout: ` +Warning: first warning + +some thing not very bad happened + +Warning: second warning + +some thing not very bad happened again +`, + }, + "empty_state_with_errors": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), + tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.NewState(), + stdout: ` +Warning: first warning + +some thing not very bad happened + +Warning: second warning + +some thing not very bad happened again +`, + stderr: `Terraform encountered an error destroying resources created while executing +main.tftest.hcl. + +Error: first error + +this time it is very bad +`, + }, + "error_from_run": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), + }, + run: &moduletest.Run{Name: "run_block"}, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.NewState(), + stderr: `Terraform encountered an error destroying resources created while executing +main.tftest.hcl/run_block. + +Error: first error + +this time it is very bad +`, + }, + "state_only_warnings": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceDeposed( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + "0fcb640a", + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + stdout: ` +Warning: first warning + +some thing not very bad happened + +Warning: second warning + +some thing not very bad happened again +`, + stderr: ` +Terraform left the following resources in state after executing +main.tftest.hcl, and they need to be cleaned up manually: + - test.bar + - test.bar (0fcb640a) + - test.foo +`, + }, + "state_with_errors": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "some thing not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "some thing not very bad happened again"), + tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceDeposed( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + "0fcb640a", + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + stdout: ` +Warning: first warning + +some thing not very bad happened + +Warning: second warning + +some thing not very bad happened again +`, + stderr: `Terraform encountered an error destroying resources created while executing +main.tftest.hcl. + +Error: first error + +this time it is very bad + +Terraform left the following resources in state after executing +main.tftest.hcl, and they need to be cleaned up manually: + - test.bar + - test.bar (0fcb640a) + - test.foo +`, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewHuman, NewView(streams)) + + view.DestroySummary(tc.diags, tc.run, tc.file, tc.state) + + output := done(t) + actual, expected := output.Stdout(), tc.stdout + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + + actual, expected = output.Stderr(), tc.stderr + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + }) + } +} + +func TestTestHuman_FatalInterruptSummary(t *testing.T) { + tcs := map[string]struct { + states map[*moduletest.Run]*states.State + run *moduletest.Run + created []*plans.ResourceInstanceChangeSrc + want string + }{ + "no_state_only_plan": { + states: make(map[*moduletest.Run]*states.State), + run: &moduletest.Run{ + Config: &configs.TestRun{}, + Name: "run_block", + }, + created: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + }, + want: ` +Terraform was interrupted while executing main.tftest.hcl, and may not have +performed the expected cleanup operations. + +Terraform was in the process of creating the following resources for +"run_block" from the module under test, and they may not have been destroyed: + - test_instance.one + - test_instance.two +`, + }, + "file_state_no_plan": { + states: map[*moduletest.Run]*states.State{ + nil: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + }, + created: nil, + want: ` +Terraform was interrupted while executing main.tftest.hcl, and may not have +performed the expected cleanup operations. + +Terraform has already created the following resources from the module under +test: + - test_instance.one + - test_instance.two +`, + }, + "run_states_no_plan": { + states: map[*moduletest.Run]*states.State{ + &moduletest.Run{ + Name: "setup_block", + Config: &configs.TestRun{ + Module: &configs.TestRunModuleCall{ + Source: addrs.ModuleSourceLocal("../setup"), + }, + }, + }: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + }, + created: nil, + want: ` +Terraform was interrupted while executing main.tftest.hcl, and may not have +performed the expected cleanup operations. + +Terraform has already created the following resources for "setup_block" from +"../setup": + - test_instance.one + - test_instance.two +`, + }, + "all_states_with_plan": { + states: map[*moduletest.Run]*states.State{ + &moduletest.Run{ + Name: "setup_block", + Config: &configs.TestRun{ + Module: &configs.TestRunModuleCall{ + Source: addrs.ModuleSourceLocal("../setup"), + }, + }, + }: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "setup_one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "setup_two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + nil: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + }, + created: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "new_one", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "new_two", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + }, + run: &moduletest.Run{ + Config: &configs.TestRun{}, + Name: "run_block", + }, + want: ` +Terraform was interrupted while executing main.tftest.hcl, and may not have +performed the expected cleanup operations. + +Terraform has already created the following resources from the module under +test: + - test_instance.one + - test_instance.two + +Terraform has already created the following resources for "setup_block" from +"../setup": + - test_instance.setup_one + - test_instance.setup_two + +Terraform was in the process of creating the following resources for +"run_block" from the module under test, and they may not have been destroyed: + - test_instance.new_one + - test_instance.new_two +`, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewHuman, NewView(streams)) + + file := &moduletest.File{ + Name: "main.tftest.hcl", + Runs: func() []*moduletest.Run { + var runs []*moduletest.Run + for run := range tc.states { + if run != nil { + runs = append(runs, run) + } + } + return runs + }(), + } + + view.FatalInterruptSummary(tc.run, file, tc.states, tc.created) + actual, expected := done(t).Stderr(), tc.want + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, actual, diff) + } + }) + } +} + +func TestTestJSON_Abstract(t *testing.T) { + tcs := map[string]struct { + suite *moduletest.Suite + want []map[string]interface{} + }{ + "single": { + suite: &moduletest.Suite{ + Files: map[string]*moduletest.File{ + "main.tftest.hcl": { + Runs: []*moduletest.Run{ + { + Name: "setup", + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Found 1 file and 1 run block", + "@module": "terraform.ui", + "test_abstract": map[string]interface{}{ + "main.tftest.hcl": []interface{}{ + "setup", + }, + }, + "type": "test_abstract", + }, + }, + }, + "plural": { + suite: &moduletest.Suite{ + Files: map[string]*moduletest.File{ + "main.tftest.hcl": { + Runs: []*moduletest.Run{ + { + Name: "setup", + }, + { + Name: "test", + }, + }, + }, + "other.tftest.hcl": { + Runs: []*moduletest.Run{ + { + Name: "test", + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Found 2 files and 3 run blocks", + "@module": "terraform.ui", + "test_abstract": map[string]interface{}{ + "main.tftest.hcl": []interface{}{ + "setup", + "test", + }, + "other.tftest.hcl": []interface{}{ + "test", + }, + }, + "type": "test_abstract", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewJSON, NewView(streams)) + + view.Abstract(tc.suite) + testJSONViewOutputEquals(t, done(t).All(), tc.want) + }) + } +} + +func TestTestJSON_Conclusion(t *testing.T) { + tcs := map[string]struct { + suite *moduletest.Suite + want []map[string]interface{} + }{ + "no tests": { + suite: &moduletest.Suite{}, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Executed 0 tests.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "pending", + "errored": 0.0, + "failed": 0.0, + "passed": 0.0, + "skipped": 0.0, + }, + "type": "test_summary", + }, + }, + }, + + "only skipped tests": { + suite: &moduletest.Suite{ + Status: moduletest.Skip, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Skip, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Executed 0 tests, 6 skipped.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "skip", + "errored": 0.0, + "failed": 0.0, + "passed": 0.0, + "skipped": 6.0, + }, + "type": "test_summary", + }, + }, + }, + + "only passed tests": { + suite: &moduletest.Suite{ + Status: moduletest.Pass, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Success! 6 passed, 0 failed.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "pass", + "errored": 0.0, + "failed": 0.0, + "passed": 6.0, + "skipped": 0.0, + }, + "type": "test_summary", + }, + }, + }, + + "passed and skipped tests": { + suite: &moduletest.Suite{ + Status: moduletest.Pass, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Pass, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Success! 4 passed, 0 failed, 2 skipped.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "pass", + "errored": 0.0, + "failed": 0.0, + "passed": 4.0, + "skipped": 2.0, + }, + "type": "test_summary", + }, + }, + }, + + "only failed tests": { + suite: &moduletest.Suite{ + Status: moduletest.Fail, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Failure! 0 passed, 6 failed.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "fail", + "errored": 0.0, + "failed": 6.0, + "passed": 0.0, + "skipped": 0.0, + }, + "type": "test_summary", + }, + }, + }, + + "failed and skipped tests": { + suite: &moduletest.Suite{ + Status: moduletest.Fail, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Failure! 0 passed, 4 failed, 2 skipped.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "fail", + "errored": 0.0, + "failed": 4.0, + "passed": 0.0, + "skipped": 2.0, + }, + "type": "test_summary", + }, + }, + }, + + "failed, passed and skipped tests": { + suite: &moduletest.Suite{ + Status: moduletest.Fail, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Skip, + }, + { + Name: "test_two", + Status: moduletest.Fail, + }, + { + Name: "test_three", + Status: moduletest.Pass, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Failure! 2 passed, 2 failed, 2 skipped.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "fail", + "errored": 0.0, + "failed": 2.0, + "passed": 2.0, + "skipped": 2.0, + }, + "type": "test_summary", + }, + }, + }, + + "failed and errored tests": { + suite: &moduletest.Suite{ + Status: moduletest.Error, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Error, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Fail, + }, + { + Name: "test_two", + Status: moduletest.Error, + }, + { + Name: "test_three", + Status: moduletest.Error, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Failure! 0 passed, 6 failed.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "error", + "errored": 3.0, + "failed": 3.0, + "passed": 0.0, + "skipped": 0.0, + }, + "type": "test_summary", + }, + }, + }, + + "failed, errored, passed, and skipped tests": { + suite: &moduletest.Suite{ + Status: moduletest.Error, + Files: map[string]*moduletest.File{ + "descriptive_test_name.tftest.hcl": { + Name: "descriptive_test_name.tftest.hcl", + Status: moduletest.Fail, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Pass, + }, + { + Name: "test_two", + Status: moduletest.Pass, + }, + { + Name: "test_three", + Status: moduletest.Fail, + }, + }, + }, + "other_descriptive_test_name.tftest.hcl": { + Name: "other_descriptive_test_name.tftest.hcl", + Status: moduletest.Error, + Runs: []*moduletest.Run{ + { + Name: "test_one", + Status: moduletest.Error, + }, + { + Name: "test_two", + Status: moduletest.Skip, + }, + { + Name: "test_three", + Status: moduletest.Skip, + }, + }, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "Failure! 2 passed, 2 failed, 2 skipped.", + "@module": "terraform.ui", + "test_summary": map[string]interface{}{ + "status": "error", + "errored": 1.0, + "failed": 1.0, + "passed": 2.0, + "skipped": 2.0, + }, + "type": "test_summary", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewJSON, NewView(streams)) + + view.Conclusion(tc.suite) + testJSONViewOutputEquals(t, done(t).All(), tc.want) + }) + } +} + +func TestTestJSON_DestroySummary(t *testing.T) { + tcs := map[string]struct { + file *moduletest.File + run *moduletest.Run + state *states.State + diags tfdiags.Diagnostics + want []map[string]interface{} + }{ + "empty_state_only_warnings": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.NewState(), + want: []map[string]interface{}{ + { + "@level": "warn", + "@message": "Warning: first warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened", + "severity": "warning", + "summary": "first warning", + }, + "type": "diagnostic", + }, + { + "@level": "warn", + "@message": "Warning: second warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened again", + "severity": "warning", + "summary": "second warning", + }, + "type": "diagnostic", + }, + }, + }, + "empty_state_with_errors": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), + tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.NewState(), + want: []map[string]interface{}{ + { + "@level": "warn", + "@message": "Warning: first warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened", + "severity": "warning", + "summary": "first warning", + }, + "type": "diagnostic", + }, + { + "@level": "warn", + "@message": "Warning: second warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened again", + "severity": "warning", + "summary": "second warning", + }, + "type": "diagnostic", + }, + { + "@level": "error", + "@message": "Error: first error", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "this time it is very bad", + "severity": "error", + "summary": "first error", + }, + "type": "diagnostic", + }, + }, + }, + "state_from_run": { + file: &moduletest.File{Name: "main.tftest.hcl"}, + run: &moduletest.Run{Name: "run_block"}, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform left some resources in state after executing main.tftest.hcl/run_block, they need to be cleaned up manually.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_cleanup": map[string]interface{}{ + "failed_resources": []interface{}{ + map[string]interface{}{ + "instance": "test.foo", + }, + }, + }, + "type": "test_cleanup", + }, + }, + }, + "state_only_warnings": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceDeposed( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + "0fcb640a", + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform left some resources in state after executing main.tftest.hcl, they need to be cleaned up manually.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "test_cleanup": map[string]interface{}{ + "failed_resources": []interface{}{ + map[string]interface{}{ + "instance": "test.bar", + }, + map[string]interface{}{ + "instance": "test.bar", + "deposed_key": "0fcb640a", + }, + map[string]interface{}{ + "instance": "test.foo", + }, + }, + }, + "type": "test_cleanup", + }, + { + "@level": "warn", + "@message": "Warning: first warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened", + "severity": "warning", + "summary": "first warning", + }, + "type": "diagnostic", + }, + { + "@level": "warn", + "@message": "Warning: second warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened again", + "severity": "warning", + "summary": "second warning", + }, + "type": "diagnostic", + }, + }, + }, + "state_with_errors": { + diags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "first warning", "something not very bad happened"), + tfdiags.Sourceless(tfdiags.Warning, "second warning", "something not very bad happened again"), + tfdiags.Sourceless(tfdiags.Error, "first error", "this time it is very bad"), + }, + file: &moduletest.File{Name: "main.tftest.hcl"}, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + state.SetResourceInstanceDeposed( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + "0fcb640a", + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform left some resources in state after executing main.tftest.hcl, they need to be cleaned up manually.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "test_cleanup": map[string]interface{}{ + "failed_resources": []interface{}{ + map[string]interface{}{ + "instance": "test.bar", + }, + map[string]interface{}{ + "instance": "test.bar", + "deposed_key": "0fcb640a", + }, + map[string]interface{}{ + "instance": "test.foo", + }, + }, + }, + "type": "test_cleanup", + }, + { + "@level": "warn", + "@message": "Warning: first warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened", + "severity": "warning", + "summary": "first warning", + }, + "type": "diagnostic", + }, + { + "@level": "warn", + "@message": "Warning: second warning", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "something not very bad happened again", + "severity": "warning", + "summary": "second warning", + }, + "type": "diagnostic", + }, + { + "@level": "error", + "@message": "Error: first error", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "diagnostic": map[string]interface{}{ + "detail": "this time it is very bad", + "severity": "error", + "summary": "first error", + }, + "type": "diagnostic", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewJSON, NewView(streams)) + + view.DestroySummary(tc.diags, tc.run, tc.file, tc.state) + testJSONViewOutputEquals(t, done(t).All(), tc.want) + }) + } +} + +func TestTestJSON_File(t *testing.T) { + tcs := map[string]struct { + file *moduletest.File + progress moduletest.Progress + want []map[string]interface{} + }{ + "pass": { + file: &moduletest.File{Name: "main.tf", Status: moduletest.Pass}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tf... pass", + "@module": "terraform.ui", + "@testfile": "main.tf", + "test_file": map[string]interface{}{ + "path": "main.tf", + "progress": "complete", + "status": "pass", + }, + "type": "test_file", + }, + }, + }, + + "pending": { + file: &moduletest.File{Name: "main.tf", Status: moduletest.Pending}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tf... pending", + "@module": "terraform.ui", + "@testfile": "main.tf", + "test_file": map[string]interface{}{ + "path": "main.tf", + "progress": "complete", + "status": "pending", + }, + "type": "test_file", + }, + }, + }, + + "skip": { + file: &moduletest.File{Name: "main.tf", Status: moduletest.Skip}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tf... skip", + "@module": "terraform.ui", + "@testfile": "main.tf", + "test_file": map[string]interface{}{ + "path": "main.tf", + "progress": "complete", + "status": "skip", + }, + "type": "test_file", + }, + }, + }, + + "fail": { + file: &moduletest.File{Name: "main.tf", Status: moduletest.Fail}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tf... fail", + "@module": "terraform.ui", + "@testfile": "main.tf", + "test_file": map[string]interface{}{ + "path": "main.tf", + "progress": "complete", + "status": "fail", + }, + "type": "test_file", + }, + }, + }, + + "error": { + file: &moduletest.File{Name: "main.tf", Status: moduletest.Error}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tf... fail", + "@module": "terraform.ui", + "@testfile": "main.tf", + "test_file": map[string]interface{}{ + "path": "main.tf", + "progress": "complete", + "status": "error", + }, + "type": "test_file", + }, + }, + }, + + "starting": { + file: &moduletest.File{Name: "main.tftest.hcl", Status: moduletest.Pending}, + progress: moduletest.Starting, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tftest.hcl... in progress", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "test_file": map[string]interface{}{ + "path": "main.tftest.hcl", + "progress": "starting", + }, + "type": "test_file", + }, + }, + }, + + "tear_down": { + file: &moduletest.File{Name: "main.tftest.hcl", Status: moduletest.Pending}, + progress: moduletest.TearDown, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": "main.tftest.hcl... tearing down", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "test_file": map[string]interface{}{ + "path": "main.tftest.hcl", + "progress": "teardown", + }, + "type": "test_file", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewJSON, NewView(streams)) + + view.File(tc.file, tc.progress) + testJSONViewOutputEquals(t, done(t).All(), tc.want) + }) + } +} + +func TestTestJSON_Run(t *testing.T) { + tcs := map[string]struct { + run *moduletest.Run + progress moduletest.Progress + elapsed int64 + want []map[string]interface{} + }{ + "starting": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + progress: moduletest.Starting, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... in progress", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "starting", + "elapsed": float64(0), + }, + "type": "test_run", + }, + }, + }, + + "running": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + progress: moduletest.Running, + elapsed: 2024, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... in progress", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "running", + "elapsed": float64(2024), + }, + "type": "test_run", + }, + }, + }, + + "teardown": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + progress: moduletest.TearDown, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... tearing down", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "teardown", + "elapsed": float64(0), + }, + "type": "test_run", + }, + }, + }, + + "pass": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Pass}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... pass", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "pass", + }, + "type": "test_run", + }, + }, + }, + + "pass_with_diags": { + run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Pass, + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Warning, "a warning occurred", "some warning happened during this test")}, + }, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... pass", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "pass", + }, + "type": "test_run", + }, + { + "@level": "warn", + "@message": "Warning: a warning occurred", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "diagnostic": map[string]interface{}{ + "detail": "some warning happened during this test", + "severity": "warning", + "summary": "a warning occurred", + }, + "type": "diagnostic", + }, + }, + }, + + "pending": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Pending}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... pending", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "pending", + }, + "type": "test_run", + }, + }, + }, + + "skip": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Skip}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... skip", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "skip", + }, + "type": "test_run", + }, + }, + }, + + "fail": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Fail}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... fail", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "fail", + }, + "type": "test_run", + }, + }, + }, + + "fail_with_diags": { + run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Fail, + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "a comparison failed", "details details details"), + tfdiags.Sourceless(tfdiags.Error, "a second comparison failed", "other details"), + }, + }, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... fail", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "fail", + }, + "type": "test_run", + }, + { + "@level": "error", + "@message": "Error: a comparison failed", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "diagnostic": map[string]interface{}{ + "detail": "details details details", + "severity": "error", + "summary": "a comparison failed", + }, + "type": "diagnostic", + }, + { + "@level": "error", + "@message": "Error: a second comparison failed", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "diagnostic": map[string]interface{}{ + "detail": "other details", + "severity": "error", + "summary": "a second comparison failed", + }, + "type": "diagnostic", + }, + }, + }, + + "error": { + run: &moduletest.Run{Name: "run_block", Status: moduletest.Error}, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... fail", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "error", + }, + "type": "test_run", + }, + }, + }, + + "error_with_diags": { + run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Error, + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "an error occurred", "something bad happened during this test")}, + }, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... fail", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "error", + }, + "type": "test_run", + }, + { + "@level": "error", + "@message": "Error: an error occurred", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "diagnostic": map[string]interface{}{ + "detail": "something bad happened during this test", + "severity": "error", + "summary": "an error occurred", + }, + "type": "diagnostic", + }, + }, + }, + + "verbose_plan": { + run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Pass, + Config: &configs.TestRun{ + Command: configs.PlanTestCommand, + }, + Verbose: &moduletest.Verbose{ + Plan: &plans.Plan{ + Changes: &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "creating", + }, + }, + }, + PrevRunAddr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "creating", + }, + }, + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + After: dynamicValue( + t, + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("foobar"), + }), + cty.Object(map[string]cty.Type{ + "value": cty.String, + })), + }, + }, + }, + }, + }, + State: states.NewState(), // empty state + Config: &configs.Config{ + Module: &configs.Module{ + ProviderRequirements: &configs.RequiredProviders{}, + }, + }, + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }: { + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + }, + }, + }, + }, + }, + }, + }, + }, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... pass", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "pass", + }, + "type": "test_run", + }, + { + "@level": "info", + "@message": "-verbose flag enabled, printing plan", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_plan": map[string]interface{}{ + "plan_format_version": "1.2", + "provider_format_version": "1.0", + "resource_changes": []interface{}{ + map[string]interface{}{ + "address": "test_resource.creating", + "change": map[string]interface{}{ + "actions": []interface{}{"create"}, + "after": map[string]interface{}{ + "value": "foobar", + }, + "after_sensitive": map[string]interface{}{}, + "after_unknown": map[string]interface{}{}, + "before": nil, + "before_sensitive": false, + }, + "mode": "managed", + "name": "creating", + "provider_name": "registry.terraform.io/hashicorp/test", + "type": "test_resource", + }, + }, + "provider_schemas": map[string]interface{}{ + "registry.terraform.io/hashicorp/test": map[string]interface{}{ + "provider": map[string]interface{}{ + "version": 0.0, + }, + "resource_schemas": map[string]interface{}{ + "test_resource": map[string]interface{}{ + "block": map[string]interface{}{ + "attributes": map[string]interface{}{ + "value": map[string]interface{}{ + "description_kind": "plain", + "type": "string", + }, + }, + "description_kind": "plain", + }, + "version": 0.0, + }, + }, + }, + }, + }, + "type": "test_plan", + }, + }, + }, + + "verbose_apply": { + run: &moduletest.Run{ + Name: "run_block", + Status: moduletest.Pass, + Config: &configs.TestRun{ + Command: configs.ApplyTestCommand, + }, + Verbose: &moduletest.Verbose{ + Plan: &plans.Plan{}, // empty plan + State: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "creating", + }, + }, + }, + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"foobar"}`), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }, + }) + }), + Config: &configs.Config{ + Module: &configs.Module{}, + }, + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "hashicorp", + Type: "test", + }: { + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + }, + }, + }, + }, + }, + }, + }, + }, + progress: moduletest.Complete, + want: []map[string]interface{}{ + { + "@level": "info", + "@message": " \"run_block\"... pass", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_run": map[string]interface{}{ + "path": "main.tftest.hcl", + "run": "run_block", + "progress": "complete", + "status": "pass", + }, + "type": "test_run", + }, + { + "@level": "info", + "@message": "-verbose flag enabled, printing state", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_state": map[string]interface{}{ + "state_format_version": "1.0", + "provider_format_version": "1.0", + "root_module": map[string]interface{}{ + "resources": []interface{}{ + map[string]interface{}{ + "address": "test_resource.creating", + "mode": "managed", + "name": "creating", + "provider_name": "registry.terraform.io/hashicorp/test", + "schema_version": 0.0, + "sensitive_values": map[string]interface{}{}, + "type": "test_resource", + "values": map[string]interface{}{ + "value": "foobar", + }, + }, + }, + }, + "provider_schemas": map[string]interface{}{ + "registry.terraform.io/hashicorp/test": map[string]interface{}{ + "provider": map[string]interface{}{ + "version": 0.0, + }, + "resource_schemas": map[string]interface{}{ + "test_resource": map[string]interface{}{ + "block": map[string]interface{}{ + "attributes": map[string]interface{}{ + "value": map[string]interface{}{ + "description_kind": "plain", + "type": "string", + }, + }, + "description_kind": "plain", + }, + "version": 0.0, + }, + }, + }, + }, + }, + "type": "test_state", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewJSON, NewView(streams)) + + file := &moduletest.File{Name: "main.tftest.hcl"} + + view.Run(tc.run, file, tc.progress, tc.elapsed) + testJSONViewOutputEquals(t, done(t).All(), tc.want, cmp.FilterPath(func(path cmp.Path) bool { + return strings.Contains(path.Last().String(), "version") || strings.Contains(path.Last().String(), "timestamp") + }, cmp.Ignore())) + }) + } +} + +func TestTestJSON_FatalInterruptSummary(t *testing.T) { + tcs := map[string]struct { + states map[*moduletest.Run]*states.State + changes []*plans.ResourceInstanceChangeSrc + want []map[string]interface{} + }{ + "no_state_only_plan": { + states: make(map[*moduletest.Run]*states.State), + changes: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_interrupt": map[string]interface{}{ + "planned": []interface{}{ + "test_instance.one", + "test_instance.two", + }, + }, + "type": "test_interrupt", + }, + }, + }, + "file_state_no_plan": { + states: map[*moduletest.Run]*states.State{ + nil: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + }, + changes: nil, + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_interrupt": map[string]interface{}{ + "state": []interface{}{ + map[string]interface{}{ + "instance": "test_instance.one", + }, + map[string]interface{}{ + "instance": "test_instance.two", + }, + }, + }, + "type": "test_interrupt", + }, + }, + }, + "run_states_no_plan": { + states: map[*moduletest.Run]*states.State{ + &moduletest.Run{Name: "setup_block"}: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + }, + changes: nil, + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_interrupt": map[string]interface{}{ + "states": map[string]interface{}{ + "setup_block": []interface{}{ + map[string]interface{}{ + "instance": "test_instance.one", + }, + map[string]interface{}{ + "instance": "test_instance.two", + }, + }, + }, + }, + "type": "test_interrupt", + }, + }, + }, + "all_states_with_plan": { + states: map[*moduletest.Run]*states.State{ + &moduletest.Run{Name: "setup_block"}: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "setup_one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "setup_two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + nil: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "one", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + + state.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "two", + }, + }, + }, + &states.ResourceInstanceObjectSrc{}, + addrs.AbsProviderConfig{}) + }), + }, + changes: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "new_one", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "new_two", + }, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + }, + want: []map[string]interface{}{ + { + "@level": "error", + "@message": "Terraform was interrupted during test execution, and may not have performed the expected cleanup operations.", + "@module": "terraform.ui", + "@testfile": "main.tftest.hcl", + "@testrun": "run_block", + "test_interrupt": map[string]interface{}{ + "state": []interface{}{ + map[string]interface{}{ + "instance": "test_instance.one", + }, + map[string]interface{}{ + "instance": "test_instance.two", + }, + }, + "states": map[string]interface{}{ + "setup_block": []interface{}{ + map[string]interface{}{ + "instance": "test_instance.setup_one", + }, + map[string]interface{}{ + "instance": "test_instance.setup_two", + }, + }, + }, + "planned": []interface{}{ + "test_instance.new_one", + "test_instance.new_two", + }, + }, + "type": "test_interrupt", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + streams, done := terminal.StreamsForTesting(t) + view := NewTest(arguments.ViewJSON, NewView(streams)) + + file := &moduletest.File{Name: "main.tftest.hcl"} + run := &moduletest.Run{Name: "run_block"} + + view.FatalInterruptSummary(run, file, tc.states, tc.changes) + testJSONViewOutputEquals(t, done(t).All(), tc.want) + }) + } +} + +func dynamicValue(t *testing.T, value cty.Value, typ cty.Type) plans.DynamicValue { + d, err := plans.NewDynamicValue(value, typ) + if err != nil { + t.Fatalf("failed to create dynamic value: %s", err) + } + return d } diff --git a/internal/command/views/testdata/plans/redacted-plan.json b/internal/command/views/testdata/plans/redacted-plan.json new file mode 100644 index 0000000000..935799605f --- /dev/null +++ b/internal/command/views/testdata/plans/redacted-plan.json @@ -0,0 +1,116 @@ +{ + "plan_format_version": "1.1", + "resource_drift": [], + "resource_changes": [ + { + "address": "null_resource.foo", + "mode": "managed", + "type": "null_resource", + "name": "foo", + "provider_name": "registry.terraform.io/hashicorp/null", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "triggers": null + }, + "after_unknown": { + "id": true + }, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "relevant_attributes": [], + "output_changes": {}, + "provider_schemas": { + "registry.terraform.io/hashicorp/null": { + "provider": { + "version": 0, + "block": { + "description_kind": "plain" + } + }, + "resource_schemas": { + "null_resource": { + "version": 0, + "block": { + "attributes": { + "id": { + "type": "string", + "description": "This is set to a random value at create time.", + "description_kind": "plain", + "computed": true + }, + "triggers": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that, when changed, will force the null resource to be replaced, re-running any associated provisioners.", + "description_kind": "plain", + "optional": true + } + }, + "description": "The `null_resource` resource implements the standard resource lifecycle but takes no further action.\n\nThe `triggers` argument allows specifying an arbitrary set of values that, when changed, will cause the resource to be replaced.", + "description_kind": "plain" + } + } + }, + "data_source_schemas": { + "null_data_source": { + "version": 0, + "block": { + "attributes": { + "has_computed_default": { + "type": "string", + "description": "If set, its literal value will be stored and returned. If not, its value defaults to `\"default\"`. This argument exists primarily for testing and has little practical use.", + "description_kind": "plain", + "optional": true, + "computed": true + }, + "id": { + "type": "string", + "description": "This attribute is only present for some legacy compatibility issues and should not be used. It will be removed in a future version.", + "description_kind": "plain", + "deprecated": true, + "computed": true + }, + "inputs": { + "type": [ + "map", + "string" + ], + "description": "A map of arbitrary strings that is copied into the `outputs` attribute, and accessible directly for interpolation.", + "description_kind": "plain", + "optional": true + }, + "outputs": { + "type": [ + "map", + "string" + ], + "description": "After the data source is \"read\", a copy of the `inputs` map.", + "description_kind": "plain", + "computed": true + }, + "random": { + "type": "string", + "description": "A random value. This is primarily for testing and has little practical use; prefer the [hashicorp/random provider](https://registry.terraform.io/providers/hashicorp/random) for more practical random number use-cases.", + "description_kind": "plain", + "computed": true + } + }, + "description": "The `null_data_source` data source implements the standard data source lifecycle but does not\ninteract with any external APIs.\n\nHistorically, the `null_data_source` was typically used to construct intermediate values to re-use elsewhere in configuration. The\nsame can now be achieved using [locals](https://developer.hashicorp.com/terraform/language/values/locals).\n", + "description_kind": "plain", + "deprecated": true + } + } + } + } + }, + "provider_format_version": "1.0" +} \ No newline at end of file diff --git a/internal/command/views/validate.go b/internal/command/views/validate.go index 08ce913f82..dee57179fc 100644 --- a/internal/command/views/validate.go +++ b/internal/command/views/validate.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( diff --git a/internal/command/views/validate_test.go b/internal/command/views/validate_test.go index 6545c3b314..8ccb85c022 100644 --- a/internal/command/views/validate_test.go +++ b/internal/command/views/validate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( diff --git a/internal/command/views/view.go b/internal/command/views/view.go index 206ead7fd5..807e1fcb12 100644 --- a/internal/command/views/view.go +++ b/internal/command/views/view.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package views import ( diff --git a/internal/command/webbrowser/native.go b/internal/command/webbrowser/native.go index 4e8281ce13..9ab15a363e 100644 --- a/internal/command/webbrowser/native.go +++ b/internal/command/webbrowser/native.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package webbrowser import ( diff --git a/internal/command/webbrowser/webbrowser.go b/internal/command/webbrowser/webbrowser.go index 8931ec5172..6e4e40c6d1 100644 --- a/internal/command/webbrowser/webbrowser.go +++ b/internal/command/webbrowser/webbrowser.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package webbrowser // Launcher is an object that knows how to open a given URL in a new tab in diff --git a/internal/command/workdir/backend_state.go b/internal/command/workdir/backend_state.go new file mode 100644 index 0000000000..128bdba58e --- /dev/null +++ b/internal/command/workdir/backend_state.go @@ -0,0 +1,212 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package workdir + +import ( + "encoding/json" + "fmt" + + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/version" +) + +// BackendStateFile describes the overall structure of the file format used +// to track a working directory's active backend. +// +// The main interesting part of this is the [BackendStateFile.Backend] field, +// but [BackendStateFile.Version] is also important to make sure that the +// current Terraform CLI version will be able to understand the file. +type BackendStateFile struct { + // Don't access this directly. It's here only for use during serialization + // and deserialization of backend state file contents. + Version int `json:"version"` + + // TFVersion is the version of Terraform that wrote this state. This is + // really just for debugging purposes; we don't currently vary behavior + // based on this field. + TFVersion string `json:"terraform_version,omitempty"` + + // Backend tracks the configuration for the backend in use with + // this state. This is used to track any changes in the backend + // configuration. + Backend *BackendState `json:"backend,omitempty"` + + // This is here just so we can sniff for the unlikely-but-possible + // situation that someone is trying to use modern Terraform with a + // directory that was most recently used with Terraform v0.8, before + // there was any concept of backends. Don't access this field. + Remote *struct{} `json:"remote,omitempty"` +} + +// NewBackendStateFile returns a new [BackendStateFile] object that initially +// has no backend configured. +// +// Callers should then mutate [BackendStateFile.Backend] in the result to +// specify the explicit backend in use, if any. +func NewBackendStateFile() *BackendStateFile { + return &BackendStateFile{ + // NOTE: We don't populate Version or TFVersion here because we + // always clobber those when encoding a state file in + // [EncodeBackendStateFile]. + } +} + +// ParseBackendStateFile tries to decode the given byte slice as the backend +// state file format. +// +// Returns an error if the content is not valid syntax, or if the file is +// of an unsupported format version. +// +// This does not immediately decode the embedded backend config, and so +// it's possible that a subsequent call to [BackendState.Config] will +// return further errors even if this call succeeds. +func ParseBackendStateFile(src []byte) (*BackendStateFile, error) { + // To avoid any weird collisions with as-yet-unknown future versions of + // the format, we'll do a first pass of decoding just the "version" + // property, and then decode the rest only if we find the version number + // that we're expecting. + type VersionSniff struct { + Version int `json:"version"` + TFVersion string `json:"terraform_version,omitempty"` + } + var versionSniff VersionSniff + err := json.Unmarshal(src, &versionSniff) + if err != nil { + return nil, fmt.Errorf("invalid syntax: %w", err) + } + if versionSniff.Version == 0 { + // This could either mean that it's explicitly "version": 0 or that + // the version property is missing. We'll assume the latter here + // because state snapshot version 0 was an encoding/gob binary format + // rather than a JSON format and so it would be very weird for + // that to show up in a JSON file. + return nil, fmt.Errorf("invalid syntax: no format version number") + } + if versionSniff.Version != 3 { + return nil, fmt.Errorf("unsupported backend state version %d; you may need to use Terraform CLI v%s to work in this directory", versionSniff.Version, versionSniff.TFVersion) + } + + // If we get here then we can be sure that this file at least _thinks_ + // it's format version 3. + var stateFile BackendStateFile + err = json.Unmarshal(src, &stateFile) + if err != nil { + return nil, fmt.Errorf("invalid syntax: %w", err) + } + if stateFile.Backend == nil && stateFile.Remote != nil { + // It's very unlikely to get here, but one way it could happen is + // if this working directory was most recently used with Terraform v0.8 + // or earlier, which didn't yet include the concept of backends. + // This error message assumes that's the case. + return nil, fmt.Errorf("this working directory uses legacy remote state and so must first be upgraded using Terraform v0.9") + } + + return &stateFile, nil +} + +func EncodeBackendStateFile(f *BackendStateFile) ([]byte, error) { + f.Version = 3 // we only support version 3 + f.TFVersion = version.SemVer.String() + return json.MarshalIndent(f, "", " ") +} + +func (f *BackendStateFile) DeepCopy() *BackendStateFile { + if f == nil { + return nil + } + ret := &BackendStateFile{ + Version: f.Version, + TFVersion: f.TFVersion, + Backend: f.Backend.DeepCopy(), + } + if f.Remote != nil { + // This shouldn't ever be present in an object held by a caller since + // we'd return an error about it during load, but we'll set it anyway + // just to minimize surprise. + ret.Remote = &struct{}{} + } + return ret +} + +// BackendState describes the physical storage format for the backend state +// in a working directory, and provides the lowest-level API for decoding it. +type BackendState struct { + Type string `json:"type"` // Backend type + ConfigRaw json.RawMessage `json:"config"` // Backend raw config + Hash uint64 `json:"hash"` // Hash of portion of configuration from config files +} + +// Empty returns true if there is no active backend. +// +// In practice this typically means that the working directory is using the +// implied local backend, but that decision is made by the caller. +func (s *BackendState) Empty() bool { + return s == nil || s.Type == "" +} + +// Config decodes the type-specific configuration object using the provided +// schema and returns the result as a cty.Value. +// +// An error is returned if the stored configuration does not conform to the +// given schema, or is otherwise invalid. +func (s *BackendState) Config(schema *configschema.Block) (cty.Value, error) { + ty := schema.ImpliedType() + if s == nil { + return cty.NullVal(ty), nil + } + return ctyjson.Unmarshal(s.ConfigRaw, ty) +} + +// SetConfig replaces (in-place) the type-specific configuration object using +// the provided value and associated schema. +// +// An error is returned if the given value does not conform to the implied +// type of the schema. +func (s *BackendState) SetConfig(val cty.Value, schema *configschema.Block) error { + ty := schema.ImpliedType() + buf, err := ctyjson.Marshal(val, ty) + if err != nil { + return err + } + s.ConfigRaw = buf + return nil +} + +// ForPlan produces an alternative representation of the reciever that is +// suitable for storing in a plan. The current workspace must additionally +// be provided, to be stored alongside the backend configuration. +// +// The backend configuration schema is required in order to properly +// encode the backend-specific configuration settings. +func (s *BackendState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { + if s == nil { + return nil, nil + } + + configVal, err := s.Config(schema) + if err != nil { + return nil, fmt.Errorf("failed to decode backend config: %w", err) + } + return plans.NewBackend(s.Type, configVal, schema, workspaceName) +} + +func (s *BackendState) DeepCopy() *BackendState { + if s == nil { + return nil + } + ret := &BackendState{ + Type: s.Type, + Hash: s.Hash, + } + + if s.ConfigRaw != nil { + ret.ConfigRaw = make([]byte, len(s.ConfigRaw)) + copy(ret.ConfigRaw, s.ConfigRaw) + } + return ret +} diff --git a/internal/command/workdir/backend_state_test.go b/internal/command/workdir/backend_state_test.go new file mode 100644 index 0000000000..f2e9675a62 --- /dev/null +++ b/internal/command/workdir/backend_state_test.go @@ -0,0 +1,143 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package workdir + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +func TestParseBackendStateFile(t *testing.T) { + tests := map[string]struct { + Input string + Want *BackendStateFile + WantErr string + }{ + "empty": { + Input: ``, + WantErr: `invalid syntax: unexpected end of JSON input`, + }, + "empty but valid JSON syntax": { + Input: `{}`, + WantErr: `invalid syntax: no format version number`, + }, + "older version": { + Input: `{ + "version": 2, + "terraform_version": "0.3.0" + }`, + WantErr: `unsupported backend state version 2; you may need to use Terraform CLI v0.3.0 to work in this directory`, + }, + "newer version": { + Input: `{ + "version": 4, + "terraform_version": "54.23.9" + }`, + WantErr: `unsupported backend state version 4; you may need to use Terraform CLI v54.23.9 to work in this directory`, + }, + "legacy remote state is active": { + Input: `{ + "version": 3, + "terraform_version": "0.8.0", + "remote": { + "anything": "goes" + } + }`, + WantErr: `this working directory uses legacy remote state and so must first be upgraded using Terraform v0.9`, + }, + "active backend": { + Input: `{ + "version": 3, + "terraform_version": "0.8.0", + "backend": { + "type": "treasure_chest_buried_on_a_remote_island", + "config": {} + } + }`, + Want: &BackendStateFile{ + Version: 3, + TFVersion: "0.8.0", + Backend: &BackendState{ + Type: "treasure_chest_buried_on_a_remote_island", + ConfigRaw: json.RawMessage("{}"), + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := ParseBackendStateFile([]byte(test.Input)) + + if test.WantErr != "" { + if err == nil { + t.Fatalf("unexpected success\nwant error: %s", test.WantErr) + } + if got, want := err.Error(), test.WantErr; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(test.Want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + +func ParseBackendStateConfig(t *testing.T) { + // This test only really covers the happy path because Config/SetConfig is + // largely just a thin wrapper around configschema's "ImpliedType" and + // cty's json unmarshal/marshal and both of those are well-tested elsewhere. + + s := &BackendState{ + Type: "whatever", + ConfigRaw: []byte(`{ + "foo": "bar" + }`), + } + + schema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Optional: true, + }, + }, + } + got, err := s.Config(schema) + want := cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + err = s.SetConfig(cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("baz"), + }), schema) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + gotRaw := s.ConfigRaw + wantRaw := []byte(`{"foo":"baz"}`) + if !bytes.Equal(wantRaw, gotRaw) { + t.Errorf("wrong raw config after encode\ngot: %s\nwant: %s", gotRaw, wantRaw) + } +} diff --git a/internal/command/workdir/dir.go b/internal/command/workdir/dir.go index 1af5b8ed0c..c8b9fdd05e 100644 --- a/internal/command/workdir/dir.go +++ b/internal/command/workdir/dir.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package workdir import ( diff --git a/internal/command/workdir/doc.go b/internal/command/workdir/doc.go index d645e4f09d..010f57e6d0 100644 --- a/internal/command/workdir/doc.go +++ b/internal/command/workdir/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package workdir models the various local artifacts and state we keep inside // a Terraform "working directory". // diff --git a/internal/command/workdir/normalize_path.go b/internal/command/workdir/normalize_path.go index de4a6a274b..ef5bc89d42 100644 --- a/internal/command/workdir/normalize_path.go +++ b/internal/command/workdir/normalize_path.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package workdir import ( diff --git a/internal/command/workdir/plugin_dirs.go b/internal/command/workdir/plugin_dirs.go index 017b0ffc16..d72c0cb11f 100644 --- a/internal/command/workdir/plugin_dirs.go +++ b/internal/command/workdir/plugin_dirs.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package workdir import ( diff --git a/internal/command/workdir/plugin_dirs_test.go b/internal/command/workdir/plugin_dirs_test.go index 614ff8585d..ebd1ed6830 100644 --- a/internal/command/workdir/plugin_dirs_test.go +++ b/internal/command/workdir/plugin_dirs_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package workdir import ( diff --git a/internal/command/workspace_command.go b/internal/command/workspace_command.go index a0f5f542ac..a895af53d9 100644 --- a/internal/command/workspace_command.go +++ b/internal/command/workspace_command.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "net/url" "strings" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) // WorkspaceCommand is a Command Implementation that manipulates workspaces, @@ -21,8 +24,7 @@ func (c *WorkspaceCommand) Run(args []string) int { cmdFlags := c.Meta.extendedFlagSet("workspace") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } - c.Ui.Output(c.Help()) - return 0 + return cli.RunResultHelp } func (c *WorkspaceCommand) Help() string { @@ -68,7 +70,8 @@ const ( envDoesNotExist = ` Workspace %q doesn't exist. -You can create this workspace with the "new" subcommand.` +You can create this workspace with the "new" subcommand +or include the "-or-create" flag with the "select" subcommand.` envChanged = `[reset][green]Switched to workspace %q.` diff --git a/internal/command/workspace_command_test.go b/internal/command/workspace_command_test.go index 35dd090d0c..ede8bbff8d 100644 --- a/internal/command/workspace_command_test.go +++ b/internal/command/workspace_command_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -7,15 +10,15 @@ import ( "strings" "testing" + "github.com/hashicorp/cli" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/backend" "github.com/hashicorp/terraform/internal/backend/local" "github.com/hashicorp/terraform/internal/backend/remote-state/inmem" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" - "github.com/mitchellh/cli" - - legacy "github.com/hashicorp/terraform/internal/legacy/terraform" ) func TestWorkspace_createAndChange(t *testing.T) { @@ -381,20 +384,30 @@ func TestWorkspace_deleteWithState(t *testing.T) { } // create a non-empty state - originalState := &legacy.State{ - Modules: []*legacy.ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*legacy.ResourceState{ - "test_instance.foo": { + originalState := states.BuildState(func(ss *states.SyncState) { + ss.SetResourceInstanceCurrent( + addrs.AbsResourceInstance{ + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, Type: "test_instance", - Primary: &legacy.InstanceState{ - ID: "bar", - }, + Name: "foo", }, }, }, - }, + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte("{}"), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewBuiltInProvider("test"), + }, + ) + }) + originalStateFile := &statefile.File{ + Serial: 1, + Lineage: "whatever", + State: originalState, } f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate")) @@ -402,7 +415,7 @@ func TestWorkspace_deleteWithState(t *testing.T) { t.Fatal(err) } defer f.Close() - if err := legacy.WriteState(originalState, f); err != nil { + if err := statefile.Write(originalStateFile, f); err != nil { t.Fatal(err) } @@ -435,3 +448,31 @@ func TestWorkspace_deleteWithState(t *testing.T) { t.Fatal("env 'test' still exists!") } } + +func TestWorkspace_selectWithOrCreate(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + os.MkdirAll(td, 0755) + defer testChdir(t, td)() + + selectCmd := &WorkspaceSelectCommand{} + + current, _ := selectCmd.Workspace() + if current != backend.DefaultStateName { + t.Fatal("current workspace should be 'default'") + } + + args := []string{"-or-create", "test"} + ui := new(cli.MockUi) + view, _ := testView(t) + selectCmd.Meta = Meta{Ui: ui, View: view} + if code := selectCmd.Run(args); code != 0 { + t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) + } + + current, _ = selectCmd.Workspace() + if current != "test" { + t.Fatalf("current workspace should be 'test', got %q", current) + } + +} diff --git a/internal/command/workspace_delete.go b/internal/command/workspace_delete.go index 013db3966c..625124e9eb 100644 --- a/internal/command/workspace_delete.go +++ b/internal/command/workspace_delete.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -5,12 +8,12 @@ import ( "strings" "time" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -131,11 +134,11 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { // We'll collect a list of what's being managed here as extra context // for the message. var buf strings.Builder - for _, obj := range stateMgr.State().AllResourceInstanceObjectAddrs() { + for _, obj := range stateMgr.State().AllManagedResourceInstanceObjectAddrs() { if obj.DeposedKey == states.NotDeposed { - fmt.Fprintf(&buf, "\n - %s", obj.Instance.String()) + fmt.Fprintf(&buf, "\n - %s", obj.ResourceInstance.String()) } else { - fmt.Fprintf(&buf, "\n - %s (deposed object %s)", obj.Instance.String(), obj.DeposedKey) + fmt.Fprintf(&buf, "\n - %s (deposed object %s)", obj.ResourceInstance.String(), obj.DeposedKey) } } @@ -165,7 +168,7 @@ func (c *WorkspaceDeleteCommand) Run(args []string) int { // be delegated from the Backend to the State itself. stateLocker.Unlock() - err = b.DeleteWorkspace(workspace) + err = b.DeleteWorkspace(workspace, force) if err != nil { c.Ui.Error(err.Error()) return 1 @@ -210,7 +213,9 @@ Usage: terraform [global options] workspace delete [OPTIONS] NAME Options: - -force Remove even a non-empty workspace. + -force Remove a workspace even if it is managing resources. + Terraform can no longer track or manage the workspace's + infrastructure. -lock=false Don't hold a state lock during the operation. This is dangerous if others might concurrently run commands diff --git a/internal/command/workspace_list.go b/internal/command/workspace_list.go index 7b43bc3462..be3d650e41 100644 --- a/internal/command/workspace_list.go +++ b/internal/command/workspace_list.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/command/workspace_new.go b/internal/command/workspace_new.go index cd28e69867..f4a4c153f2 100644 --- a/internal/command/workspace_new.go +++ b/internal/command/workspace_new.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( @@ -6,12 +9,12 @@ import ( "strings" "time" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/command/arguments" "github.com/hashicorp/terraform/internal/command/clistate" "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -156,7 +159,7 @@ func (c *WorkspaceNewCommand) Run(args []string) int { c.Ui.Error(err.Error()) return 1 } - err = stateMgr.PersistState() + err = stateMgr.PersistState(nil) if err != nil { c.Ui.Error(err.Error()) return 1 diff --git a/internal/command/workspace_select.go b/internal/command/workspace_select.go index e257b59d7b..26038e980d 100644 --- a/internal/command/workspace_select.go +++ b/internal/command/workspace_select.go @@ -1,11 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( "fmt" "strings" + "github.com/hashicorp/cli" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -18,7 +21,9 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { args = c.Meta.process(args) envCommandShowWarning(c.Ui, c.LegacyName) + var orCreate bool cmdFlags := c.Meta.defaultFlagSet("workspace select") + cmdFlags.BoolVar(&orCreate, "or-create", false, "create workspace if it does not exist") cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } if err := cmdFlags.Parse(args); err != nil { c.Ui.Error(fmt.Sprintf("Error parsing command-line flags: %s\n", err.Error())) @@ -95,9 +100,20 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { } } + var newState bool + if !found { - c.Ui.Error(fmt.Sprintf(envDoesNotExist, name)) - return 1 + if orCreate { + _, err = b.StateMgr(name) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + newState = true + } else { + c.Ui.Error(fmt.Sprintf(envDoesNotExist, name)) + return 1 + } } err = c.SetWorkspace(name) @@ -106,11 +122,16 @@ func (c *WorkspaceSelectCommand) Run(args []string) int { return 1 } - c.Ui.Output( - c.Colorize().Color( - fmt.Sprintf(envChanged, name), - ), - ) + if newState { + c.Ui.Output(c.Colorize().Color(fmt.Sprintf( + strings.TrimSpace(envCreated), name))) + } else { + c.Ui.Output( + c.Colorize().Color( + fmt.Sprintf(envChanged, name), + ), + ) + } return 0 } @@ -132,6 +153,10 @@ Usage: terraform [global options] workspace select NAME Select a different Terraform workspace. +Options: + + -or-create=false Create the Terraform workspace if it doesn't exist. + ` return strings.TrimSpace(helpText) } diff --git a/internal/command/workspace_show.go b/internal/command/workspace_show.go index f1f372e862..3a56527120 100644 --- a/internal/command/workspace_show.go +++ b/internal/command/workspace_show.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package command import ( diff --git a/internal/communicator/communicator.go b/internal/communicator/communicator.go index 5b754b34f3..6ea7196742 100644 --- a/internal/communicator/communicator.go +++ b/internal/communicator/communicator.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package communicator import ( diff --git a/internal/communicator/communicator_mock.go b/internal/communicator/communicator_mock.go index b60edec197..513ab74b0c 100644 --- a/internal/communicator/communicator_mock.go +++ b/internal/communicator/communicator_mock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package communicator import ( diff --git a/internal/communicator/communicator_test.go b/internal/communicator/communicator_test.go index 7401bc46cb..081d27939d 100644 --- a/internal/communicator/communicator_test.go +++ b/internal/communicator/communicator_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package communicator import ( diff --git a/internal/communicator/remote/command.go b/internal/communicator/remote/command.go index 4c90368ec8..327b2a4bf8 100644 --- a/internal/communicator/remote/command.go +++ b/internal/communicator/remote/command.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( diff --git a/internal/communicator/remote/command_test.go b/internal/communicator/remote/command_test.go index fbe5b64eba..c501c14ca2 100644 --- a/internal/communicator/remote/command_test.go +++ b/internal/communicator/remote/command_test.go @@ -1 +1,4 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote diff --git a/internal/communicator/shared/shared.go b/internal/communicator/shared/shared.go index 5990807a78..68ee53366e 100644 --- a/internal/communicator/shared/shared.go +++ b/internal/communicator/shared/shared.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package shared import ( diff --git a/internal/communicator/shared/shared_test.go b/internal/communicator/shared/shared_test.go index 575e5f78de..5a5399f680 100644 --- a/internal/communicator/shared/shared_test.go +++ b/internal/communicator/shared/shared_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package shared import ( diff --git a/internal/communicator/ssh/communicator.go b/internal/communicator/ssh/communicator.go index c6af68839e..1cec681bde 100644 --- a/internal/communicator/ssh/communicator.go +++ b/internal/communicator/ssh/communicator.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( @@ -418,7 +421,7 @@ func (c *Communicator) Upload(path string, input io.Reader) error { switch src := input.(type) { case *os.File: fi, err := src.Stat() - if err != nil { + if err == nil { size = fi.Size() } case *bytes.Buffer: @@ -641,7 +644,13 @@ func checkSCPStatus(r *bufio.Reader) error { return nil } +var testUploadSizeHook func(size int64) + func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader, size int64) error { + if testUploadSizeHook != nil { + testUploadSizeHook(size) + } + if size == 0 { // Create a temporary file where we can copy the contents of the src // so that we can determine the length, since SCP is length-prefixed. diff --git a/internal/communicator/ssh/communicator_test.go b/internal/communicator/ssh/communicator_test.go index 8d7db99967..77ad0bdc1d 100644 --- a/internal/communicator/ssh/communicator_test.go +++ b/internal/communicator/ssh/communicator_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !race // +build !race @@ -577,10 +580,28 @@ func TestAccUploadFile(t *testing.T) { } tmpDir := t.TempDir() + source, err := os.CreateTemp(tmpDir, "tempfile.in") + if err != nil { + t.Fatal(err) + } + + content := "this is the file content" + if _, err := source.WriteString(content); err != nil { + t.Fatal(err) + } + source.Seek(0, io.SeekStart) - content := []byte("this is the file content") - source := bytes.NewReader(content) tmpFile := filepath.Join(tmpDir, "tempFile.out") + + testUploadSizeHook = func(size int64) { + if size != int64(len(content)) { + t.Errorf("expected %d bytes, got %d\n", len(content), size) + } + } + defer func() { + testUploadSizeHook = nil + }() + err = c.Upload(tmpFile, source) if err != nil { t.Fatalf("error uploading file: %s", err) @@ -591,7 +612,7 @@ func TestAccUploadFile(t *testing.T) { t.Fatal(err) } - if !bytes.Equal(data, content) { + if string(data) != content { t.Fatalf("bad: %s", data) } } diff --git a/internal/communicator/ssh/http_proxy.go b/internal/communicator/ssh/http_proxy.go index 883dada50a..b175b6d530 100644 --- a/internal/communicator/ssh/http_proxy.go +++ b/internal/communicator/ssh/http_proxy.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( @@ -97,7 +100,6 @@ func (p *proxyDialer) Dial(network, addr string) (net.Conn, error) { res, err := http.ReadResponse(bufio.NewReader(c), req) if err != nil { - res.Body.Close() c.Close() return nil, err } diff --git a/internal/communicator/ssh/password.go b/internal/communicator/ssh/password.go index 8b32c8d4cd..aa6c7d25c4 100644 --- a/internal/communicator/ssh/password.go +++ b/internal/communicator/ssh/password.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( diff --git a/internal/communicator/ssh/password_test.go b/internal/communicator/ssh/password_test.go index 219669e4bd..9f8f461dec 100644 --- a/internal/communicator/ssh/password_test.go +++ b/internal/communicator/ssh/password_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( diff --git a/internal/communicator/ssh/provisioner.go b/internal/communicator/ssh/provisioner.go index b98ee9f5d2..f2f458684a 100644 --- a/internal/communicator/ssh/provisioner.go +++ b/internal/communicator/ssh/provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( diff --git a/internal/communicator/ssh/provisioner_test.go b/internal/communicator/ssh/provisioner_test.go index 1ee0cf8aa3..47daf37256 100644 --- a/internal/communicator/ssh/provisioner_test.go +++ b/internal/communicator/ssh/provisioner_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( diff --git a/internal/communicator/ssh/ssh_test.go b/internal/communicator/ssh/ssh_test.go index 85164be3e2..d9051149e5 100644 --- a/internal/communicator/ssh/ssh_test.go +++ b/internal/communicator/ssh/ssh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package ssh import ( diff --git a/internal/communicator/winrm/communicator.go b/internal/communicator/winrm/communicator.go index 302ccec8eb..8634da631e 100644 --- a/internal/communicator/winrm/communicator.go +++ b/internal/communicator/winrm/communicator.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package winrm import ( diff --git a/internal/communicator/winrm/communicator_test.go b/internal/communicator/winrm/communicator_test.go index bc1de8e309..7f561d51d6 100644 --- a/internal/communicator/winrm/communicator_test.go +++ b/internal/communicator/winrm/communicator_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package winrm import ( diff --git a/internal/communicator/winrm/provisioner.go b/internal/communicator/winrm/provisioner.go index 3843c9d00e..a5c2f0f278 100644 --- a/internal/communicator/winrm/provisioner.go +++ b/internal/communicator/winrm/provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package winrm import ( diff --git a/internal/communicator/winrm/provisioner_test.go b/internal/communicator/winrm/provisioner_test.go index 50718aa86d..1fed73f95a 100644 --- a/internal/communicator/winrm/provisioner_test.go +++ b/internal/communicator/winrm/provisioner_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package winrm import ( diff --git a/internal/configs/action.go b/internal/configs/action.go new file mode 100644 index 0000000000..8aa361be7d --- /dev/null +++ b/internal/configs/action.go @@ -0,0 +1,277 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// Action represents an "action" block inside a configuration +type Action struct { + Name string + Type string + Config hcl.Body + Count hcl.Expression + ForEach hcl.Expression + DependsOn []hcl.Traversal + + ProviderConfigRef *ProviderConfigRef + Provider addrs.Provider + + DeclRange hcl.Range + TypeRange hcl.Range +} + +// ActionTrigger represents a configured "action_trigger" inside the lifecycle +// block of a managed resource. +type ActionTrigger struct { + Condition hcl.Expression + Events []ActionTriggerEvent + Actions []ActionRef + + DeclRange hcl.Range +} + +// ActionTriggerEvent is an enum for valid values for events for action +// triggers. +type ActionTriggerEvent int + +//go:generate go tool golang.org/x/tools/cmd/stringer -type ActionTriggerEvent + +const ( + BeforeCreate ActionTriggerEvent = iota + AfterCreate + BeforeUpdate + AfterUpdate + BeforeDestroy + AfterDestroy +) + +// ActionRef represents a reference to a configured Action +// copypasta of providerconfigref; not sure what's needed. +type ActionRef struct { + Traversal hcl.Traversal + Range hcl.Range +} + +func decodeActionTriggerBlock(block *hcl.Block) (*ActionTrigger, hcl.Diagnostics) { + var diags hcl.Diagnostics + a := &ActionTrigger{ + Events: []ActionTriggerEvent{}, + Actions: []ActionRef{}, + Condition: nil, + } + + content, bodyDiags := block.Body.Content(actionTriggerSchema) + diags = append(diags, bodyDiags...) + + if attr, exists := content.Attributes["condition"]; exists { + a.Condition = attr.Expr + } + + // this is parsing events like expressions, so it's angry when there's quotes + // needs to parse strings: + // Quoted references are deprecated; In this context, references are expected literally rather than in quotes. + if attr, exists := content.Attributes["events"]; exists { + exprs, ediags := hcl.ExprList(attr.Expr) + diags = append(diags, ediags...) + + events := []ActionTriggerEvent{} + + for _, expr := range exprs { + var event ActionTriggerEvent + switch hcl.ExprAsKeyword(expr) { + case "before_create": + event = BeforeCreate + case "after_create": + event = AfterCreate + case "before_update": + event = BeforeUpdate + case "after_update": + event = AfterUpdate + case "before_destroy": + event = BeforeDestroy + case "after_destroy": + event = AfterDestroy + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Invalid \"event\" value %s", hcl.ExprAsKeyword(expr)), + Detail: "The \"event\" argument supports the following values: before_create, after_create, before_update, after_update, before_destroy, after_destroy.", + Subject: expr.Range().Ptr(), + }) + } + events = append(events, event) + } + a.Events = events + } + + if attr, exists := content.Attributes["actions"]; exists { + exprs, ediags := hcl.ExprList(attr.Expr) + diags = append(diags, ediags...) + actions := []ActionRef{} + for _, expr := range exprs { + traversal, travDiags := hcl.AbsTraversalForExpr(expr) + diags = append(diags, travDiags...) + // verify that the traversal is an action + if traversal.RootName() != "action" { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid actions argument inside action_triggers", + Detail: "action_triggers.actions accepts a list of one or more actions", + Subject: block.DefRange.Ptr(), + }) + } + if len(traversal) != 0 { + actionRef := ActionRef{ + Traversal: traversal, + Range: expr.Range(), + } + actions = append(actions, actionRef) + } + } + a.Actions = actions + } + + if len(a.Actions) == 0 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No actions specified", + Detail: "At least one action must be specified for an action_trigger.", + Subject: block.DefRange.Ptr(), + }) + } + + if len(a.Events) == 0 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No events specified", + Detail: "At least one event must be specified for an action_trigger.", + Subject: block.DefRange.Ptr(), + }) + } + return a, diags +} + +func decodeActionBlock(block *hcl.Block) (*Action, hcl.Diagnostics) { + var diags hcl.Diagnostics + a := &Action{ + Type: block.Labels[0], + Name: block.Labels[1], + DeclRange: block.DefRange, + TypeRange: block.LabelRanges[0], + } + + if !hclsyntax.ValidIdentifier(a.Type) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid action type name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + if !hclsyntax.ValidIdentifier(a.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid action name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[1], + }) + } + + content, remain, moreDiags := block.Body.PartialContent(actionBlockSchema) + diags = append(diags, moreDiags...) + a.Config = remain + + if attr, exists := content.Attributes["count"]; exists { + a.Count = attr.Expr + } + + if attr, exists := content.Attributes["for_each"]; exists { + a.ForEach = attr.Expr + // Cannot have count and for_each on the same action block + if a.Count != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid combination of "count" and "for_each"`, + Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used.`, + Subject: &attr.NameRange, + }) + } + } + + if attr, exists := content.Attributes["provider"]; exists { + var providerDiags hcl.Diagnostics + a.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") + diags = append(diags, providerDiags...) + } + + if attr, exists := content.Attributes["depends_on"]; exists { + deps, depsDiags := DecodeDependsOn(attr) + diags = append(diags, depsDiags...) + a.DependsOn = append(a.DependsOn, deps...) + } + + return a, diags +} + +// actionBlockSchema is the schema for an action type within terraform. +var actionBlockSchema = &hcl.BodySchema{ + Attributes: commonResourceAttributes, +} + +var actionTriggerSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "events", + Required: true, + }, + { + Name: "condition", + Required: false, + }, + { + Name: "actions", + Required: true, + }, + }, +} + +func (a *Action) moduleUniqueKey() string { + return a.Addr().String() +} + +// Addr returns a resource address for the receiver that is relative to the +// resource's containing module. +func (a *Action) Addr() addrs.Action { + return addrs.Action{ + Type: a.Type, + Name: a.Name, + } +} + +// ProviderConfigAddr returns the address for the provider configuration that +// should be used for this action. This function returns a default provider +// config addr if an explicit "provider" argument was not provided. +func (a *Action) ProviderConfigAddr() addrs.LocalProviderConfig { + if a.ProviderConfigRef == nil { + // If no specific "provider" argument is given, we want to look up the + // provider config where the local name matches the implied provider + // from the resource type. This may be different from the resource's + // provider type. + return addrs.LocalProviderConfig{ + LocalName: a.Addr().ImpliedProvider(), + } + } + + return addrs.LocalProviderConfig{ + LocalName: a.ProviderConfigRef.Name, + Alias: a.ProviderConfigRef.Alias, + } +} diff --git a/internal/configs/actiontriggerevent_string.go b/internal/configs/actiontriggerevent_string.go new file mode 100644 index 0000000000..67eba05a93 --- /dev/null +++ b/internal/configs/actiontriggerevent_string.go @@ -0,0 +1,28 @@ +// Code generated by "stringer -type ActionTriggerEvent"; DO NOT EDIT. + +package configs + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[BeforeCreate-0] + _ = x[AfterCreate-1] + _ = x[BeforeUpdate-2] + _ = x[AfterUpdate-3] + _ = x[BeforeDestroy-4] + _ = x[AfterDestroy-5] +} + +const _ActionTriggerEvent_name = "BeforeCreateAfterCreateBeforeUpdateAfterUpdateBeforeDestroyAfterDestroy" + +var _ActionTriggerEvent_index = [...]uint8{0, 12, 23, 35, 46, 59, 71} + +func (i ActionTriggerEvent) String() string { + if i < 0 || i >= ActionTriggerEvent(len(_ActionTriggerEvent_index)-1) { + return "ActionTriggerEvent(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _ActionTriggerEvent_name[_ActionTriggerEvent_index[i]:_ActionTriggerEvent_index[i+1]] +} diff --git a/internal/configs/backend.go b/internal/configs/backend.go index 4bf968e6ad..e580f4b6c8 100644 --- a/internal/configs/backend.go +++ b/internal/configs/backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/checks.go b/internal/configs/checks.go index 417dff45ee..0cf31ab16e 100644 --- a/internal/configs/checks.go +++ b/internal/configs/checks.go @@ -1,11 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" ) // CheckRule represents a configuration-defined validation rule, precondition, @@ -48,7 +53,7 @@ func (cr *CheckRule) validateSelfReferences(checkType string, addr addrs.Resourc if expr == nil { continue } - refs, _ := lang.References(expr.Variables()) + refs, _ := langrefs.References(addrs.ParseRef, expr.Variables()) for _, ref := range refs { var refAddr addrs.Resource @@ -139,3 +144,117 @@ var checkRuleBlockSchema = &hcl.BodySchema{ }, }, } + +// Check represents a configuration defined check block. +// +// A check block contains 0-1 data blocks, and 0-n assert blocks. The check +// block will load the data block, and execute the assert blocks as check rules +// during the plan and apply Terraform operations. +type Check struct { + Name string + + DataResource *Resource + Asserts []*CheckRule + + DeclRange hcl.Range +} + +func (c Check) Addr() addrs.Check { + return addrs.Check{ + Name: c.Name, + } +} + +func (c Check) Accessible(addr addrs.Referenceable) bool { + if check, ok := addr.(addrs.Check); ok { + return check.Equal(c.Addr()) + } + return false +} + +func decodeCheckBlock(block *hcl.Block, override bool) (*Check, hcl.Diagnostics) { + var diags hcl.Diagnostics + + check := &Check{ + Name: block.Labels[0], + DeclRange: block.DefRange, + } + + if override { + // For now we'll just forbid overriding check blocks, to simplify + // the initial design. If we can find a clear use-case for overriding + // checks in override files and there's a way to define it that + // isn't confusing then we could relax this. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't override check blocks", + Detail: "Override files cannot override check blocks.", + Subject: check.DeclRange.Ptr(), + }) + return check, diags + } + + content, moreDiags := block.Body.Content(checkBlockSchema) + diags = append(diags, moreDiags...) + + if !hclsyntax.ValidIdentifier(check.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid check block name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + + for _, block := range content.Blocks { + switch block.Type { + case "data": + + if check.DataResource != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple data resource blocks", + Detail: fmt.Sprintf("This check block already has a data resource defined at %s.", check.DataResource.DeclRange.Ptr()), + Subject: block.DefRange.Ptr(), + }) + continue + } + + data, moreDiags := decodeDataBlock(block, override, true) + diags = append(diags, moreDiags...) + if !moreDiags.HasErrors() { + // Connect this data block back up to this check block. + data.Container = check + + // Finally, save the data block. + check.DataResource = data + } + case "assert": + assert, moreDiags := decodeCheckRuleBlock(block, override) + diags = append(diags, moreDiags...) + if !moreDiags.HasErrors() { + check.Asserts = append(check.Asserts, assert) + } + default: + panic(fmt.Sprintf("unhandled check nested block %q", block.Type)) + } + } + + if len(check.Asserts) == 0 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Zero assert blocks", + Detail: "Check blocks must have at least one assert block.", + Subject: check.DeclRange.Ptr(), + }) + } + + return check, diags +} + +var checkBlockSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: "data", LabelNames: []string{"type", "name"}}, + {Type: "assert"}, + }, +} diff --git a/internal/configs/cloud.go b/internal/configs/cloud.go index 1ed6482e16..c4b82de186 100644 --- a/internal/configs/cloud.go +++ b/internal/configs/cloud.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/compat_shim.go b/internal/configs/compat_shim.go index 79360201e5..6857f29031 100644 --- a/internal/configs/compat_shim.go +++ b/internal/configs/compat_shim.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/config.go b/internal/configs/config.go index f38d3cd85d..ce07d965c0 100644 --- a/internal/configs/config.go +++ b/internal/configs/config.go @@ -1,21 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "fmt" "log" + "maps" + "slices" "sort" + "strings" version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" ) // A Config is a node in the tree of modules within a configuration. // // The module tree is constructed by following ModuleCall instances recursively -// through the root module transitively into descendent modules. +// through the root module transitively into descendant modules. // // A module tree described in *this* package represents the static tree // represented by configuration. During evaluation a static ModuleNode may @@ -88,8 +95,16 @@ type ModuleRequirements struct { Name string SourceAddr addrs.ModuleSource SourceDir string - Requirements getproviders.Requirements + Requirements providerreqs.Requirements Children map[string]*ModuleRequirements + Tests map[string]*TestFileModuleRequirements +} + +// TestFileModuleRequirements maps the runs for a given test file to the module +// requirements for that run block. +type TestFileModuleRequirements struct { + Requirements providerreqs.Requirements + Runs map[string]*ModuleRequirements } // NewEmptyConfig constructs a single-node configuration tree with an empty @@ -123,17 +138,12 @@ func (c *Config) Depth() int { func (c *Config) DeepEach(cb func(c *Config)) { cb(c) - names := make([]string, 0, len(c.Children)) - for name := range c.Children { - names = append(names, name) - } - - for _, name := range names { - c.Children[name].DeepEach(cb) + for _, ch := range c.Children { + ch.DeepEach(cb) } } -// AllModules returns a slice of all the receiver and all of its descendent +// AllModules returns a slice of all the receiver and all of its descendant // nodes in the module tree, in the same order they would be visited by // DeepEach. func (c *Config) AllModules() []*Config { @@ -144,14 +154,14 @@ func (c *Config) AllModules() []*Config { return ret } -// Descendent returns the descendent config that has the given path beneath +// Descendant returns the descendant config that has the given path beneath // the receiver, or nil if there is no such module. // // The path traverses the static module tree, prior to any expansion to handle // count and for_each arguments. // // An empty path will just return the receiver, and is therefore pointless. -func (c *Config) Descendent(path addrs.Module) *Config { +func (c *Config) Descendant(path addrs.Module) *Config { current := c for _, name := range path { current = current.Children[name] @@ -162,13 +172,13 @@ func (c *Config) Descendent(path addrs.Module) *Config { return current } -// DescendentForInstance is like Descendent except that it accepts a path +// DescendantForInstance is like Descendant except that it accepts a path // to a particular module instance in the dynamic module graph, returning // the node from the static module graph that corresponds to it. // // All instances created by a particular module call share the same // configuration, so the keys within the given path are disregarded. -func (c *Config) DescendentForInstance(path addrs.ModuleInstance) *Config { +func (c *Config) DescendantForInstance(path addrs.ModuleInstance) *Config { current := c for _, step := range path { current = current.Children[step.Name] @@ -179,6 +189,46 @@ func (c *Config) DescendentForInstance(path addrs.ModuleInstance) *Config { return current } +// TargetExists returns true if it's possible for the provided target to exist +// within the configuration. +// +// This doesn't consider instance expansion, so we're only making sure the +// target could exist if the instance expansion expands correctly. +func (c *Config) TargetExists(target addrs.Targetable) bool { + switch target.AddrType() { + case addrs.ConfigResourceAddrType: + addr := target.(addrs.ConfigResource) + module := c.Descendant(addr.Module) + if module != nil { + return module.Module.ResourceByAddr(addr.Resource) != nil + } else { + return false + } + case addrs.AbsResourceInstanceAddrType: + addr := target.(addrs.AbsResourceInstance) + module := c.DescendantForInstance(addr.Module) + if module != nil { + return module.Module.ResourceByAddr(addr.Resource.Resource) != nil + } else { + return false + } + case addrs.AbsResourceAddrType: + addr := target.(addrs.AbsResource) + module := c.DescendantForInstance(addr.Module) + if module != nil { + return module.Module.ResourceByAddr(addr.Resource) != nil + } else { + return false + } + case addrs.ModuleAddrType: + return c.Descendant(target.(addrs.Module)) != nil + case addrs.ModuleInstanceAddrType: + return c.DescendantForInstance(target.(addrs.ModuleInstance)) != nil + default: + panic(fmt.Errorf("unrecognized targetable type: %d", target.AddrType())) + } +} + // EntersNewPackage returns true if this call is to an external module, either // directly via a remote source address or indirectly via a registry source // address. @@ -244,14 +294,14 @@ func (c *Config) VerifyDependencySelections(depLocks *depsfile.Locks) []error { lock = depLocks.Provider(providerAddr) } if lock == nil { - log.Printf("[TRACE] Config.VerifyDependencySelections: provider %s has no lock file entry to satisfy %q", providerAddr, getproviders.VersionConstraintsString(constraints)) + log.Printf("[TRACE] Config.VerifyDependencySelections: provider %s has no lock file entry to satisfy %q", providerAddr, providerreqs.VersionConstraintsString(constraints)) errs = append(errs, fmt.Errorf("provider %s: required by this configuration but no version is selected", providerAddr)) continue } selectedVersion := lock.Version() - allowedVersions := getproviders.MeetingConstraints(constraints) - log.Printf("[TRACE] Config.VerifyDependencySelections: provider %s has %s to satisfy %q", providerAddr, selectedVersion.String(), getproviders.VersionConstraintsString(constraints)) + allowedVersions := providerreqs.MeetingConstraints(constraints) + log.Printf("[TRACE] Config.VerifyDependencySelections: provider %s has %s to satisfy %q", providerAddr, selectedVersion.String(), providerreqs.VersionConstraintsString(constraints)) if !allowedVersions.Has(selectedVersion) { // The most likely cause of this is that the author of a module // has changed its constraints, but this could also happen in @@ -260,8 +310,8 @@ func (c *Config) VerifyDependencySelections(depLocks *depsfile.Locks) []error { // distinguish those cases here in order to avoid the more // specific error message potentially being a red herring in // the edge-cases. - currentConstraints := getproviders.VersionConstraintsString(constraints) - lockedConstraints := getproviders.VersionConstraintsString(lock.VersionConstraints()) + currentConstraints := providerreqs.VersionConstraintsString(constraints) + lockedConstraints := providerreqs.VersionConstraintsString(lock.VersionConstraints()) switch { case currentConstraints != lockedConstraints: errs = append(errs, fmt.Errorf("provider %s: locked version selection %s doesn't match the updated version constraints %q", providerAddr, selectedVersion.String(), currentConstraints)) @@ -287,9 +337,18 @@ func (c *Config) VerifyDependencySelections(depLocks *depsfile.Locks) []error { // // If the returned diagnostics includes errors then the resulting Requirements // may be incomplete. -func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnostics) { - reqs := make(getproviders.Requirements) - diags := c.addProviderRequirements(reqs, true) +func (c *Config) ProviderRequirements() (providerreqs.Requirements, hcl.Diagnostics) { + reqs := make(providerreqs.Requirements) + diags := c.addProviderRequirements(reqs, true, true) + + return reqs, diags +} + +// ProviderRequirementsConfigOnly searches the full tree of configuration +// files for all providers. This function does not consider any test files. +func (c *Config) ProviderRequirementsConfigOnly() (providerreqs.Requirements, hcl.Diagnostics) { + reqs := make(providerreqs.Requirements) + diags := c.addProviderRequirements(reqs, true, false) return reqs, diags } @@ -299,9 +358,9 @@ func (c *Config) ProviderRequirements() (getproviders.Requirements, hcl.Diagnost // // If the returned diagnostics includes errors then the resulting Requirements // may be incomplete. -func (c *Config) ProviderRequirementsShallow() (getproviders.Requirements, hcl.Diagnostics) { - reqs := make(getproviders.Requirements) - diags := c.addProviderRequirements(reqs, false) +func (c *Config) ProviderRequirementsShallow() (providerreqs.Requirements, hcl.Diagnostics) { + reqs := make(providerreqs.Requirements) + diags := c.addProviderRequirements(reqs, false, true) return reqs, diags } @@ -313,8 +372,8 @@ func (c *Config) ProviderRequirementsShallow() (getproviders.Requirements, hcl.D // If the returned diagnostics includes errors then the resulting Requirements // may be incomplete. func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagnostics) { - reqs := make(getproviders.Requirements) - diags := c.addProviderRequirements(reqs, false) + reqs := make(providerreqs.Requirements) + diags := c.addProviderRequirements(reqs, false, false) children := make(map[string]*ModuleRequirements) for name, child := range c.Children { @@ -324,11 +383,33 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno diags = append(diags, childDiags...) } + tests := make(map[string]*TestFileModuleRequirements) + for name, test := range c.Module.Tests { + testReqs := &TestFileModuleRequirements{ + Requirements: make(providerreqs.Requirements), + Runs: make(map[string]*ModuleRequirements), + } + + for _, run := range test.Runs { + if run.ConfigUnderTest == nil { + continue + } + + runReqs, runDiags := run.ConfigUnderTest.ProviderRequirementsByModule() + runReqs.Name = run.Name + testReqs.Runs[run.Name] = runReqs + diags = append(diags, runDiags...) + } + + tests[name] = testReqs + } + ret := &ModuleRequirements{ SourceAddr: c.SourceAddr, SourceDir: c.Module.SourceDir, Requirements: reqs, Children: children, + Tests: tests, } return ret, diags @@ -338,7 +419,7 @@ func (c *Config) ProviderRequirementsByModule() (*ModuleRequirements, hcl.Diagno // implementation, gradually mutating a shared requirements object to // eventually return. If the recurse argument is true, the requirements will // include all descendant modules; otherwise, only the specified module. -func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse bool) hcl.Diagnostics { +func (c *Config) addProviderRequirements(reqs providerreqs.Requirements, recurse, tests bool) hcl.Diagnostics { var diags hcl.Diagnostics // First we'll deal with the requirements directly in _our_ module... @@ -356,7 +437,7 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse // don't exactly agree in practice 🙄 so this might produce new errors. // TODO: Use the new parser throughout this package so we can get the // better error messages it produces in more situations. - constraints, err := getproviders.ParseVersionConstraints(providerReqs.Requirement.Required.String()) + constraints, err := providerreqs.ParseVersionConstraints(providerReqs.Requirement.Required.String()) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -383,6 +464,7 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse } reqs[fqn] = nil } + for _, rc := range c.Module.DataResources { fqn := rc.Provider if _, exists := reqs[fqn]; exists { @@ -392,40 +474,115 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse reqs[fqn] = nil } + for _, rc := range c.Module.EphemeralResources { + fqn := rc.Provider + if _, exists := reqs[fqn]; exists { + // Explicit dependency already present + continue + } + reqs[fqn] = nil + } + + // Import blocks that are generating config may have a custom provider + // meta-argument. Like the provider meta-argument used in resource blocks, + // we use this opportunity to load any implicit providers. + // + // We'll also use this to validate that import blocks and targeted resource + // blocks agree on which provider they should be using. If they don't agree, + // this will be because the user has written explicit provider arguments + // that don't agree and we'll get them to fix it. + for _, i := range c.Module.Import { + if len(i.ToResource.Module) > 0 { + // All provider information for imports into modules should come + // from the module block, so we don't need to load anything for + // import targets within modules. + continue + } + + if target, exists := c.Module.ManagedResources[i.ToResource.Resource.String()]; exists { + // This means the information about the provider for this import + // should come from the resource block itself and not the import + // block. + // + // In general, we say that you shouldn't set the provider attribute + // on import blocks in this case. But to make config generation + // easier, we will say that if it is set in both places and it's the + // same then that is okay. + + if i.ProviderConfigRef != nil { + if target.ProviderConfigRef == nil { + // This means we have a provider specified in the import + // block and not in the resource block. This isn't the right + // way round so let's consider this a failure. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import provider argument", + Detail: "The provider argument can only be specified in import blocks that will generate configuration.\n\nUse the provider argument in the target resource block to configure the provider for a resource with explicit provider configuration.", + Subject: i.ProviderDeclRange.Ptr(), + }) + continue + } + + if i.ProviderConfigRef.Name != target.ProviderConfigRef.Name || i.ProviderConfigRef.Alias != target.ProviderConfigRef.Alias { + // This means we have a provider specified in both the + // import block and the resource block, and they disagree. + // This is bad as Terraform now has different instructions + // about which provider to use. + // + // The general guidance is that only the resource should be + // specifying the provider as the import block provider + // attribute is just for generating config. So, let's just + // tell the user to only set the provider argument in the + // resource. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import provider argument", + Detail: "The provider argument can only be specified in import blocks that will generate configuration.\n\nUse the provider argument in the target resource block to configure the provider for a resource with explicit provider configuration.", + Subject: i.ProviderDeclRange.Ptr(), + }) + continue + } + } + + // All the provider information should come from the target resource + // which has already been processed, so skip the rest of this + // processing. + continue + } + + // Otherwise we are generating config for the resource being imported, + // so all the provider information must come from this import block. + fqn := i.Provider + if _, exists := reqs[fqn]; exists { + // Explicit dependency already present + continue + } + reqs[fqn] = nil + } + // "provider" block can also contain version constraints for _, provider := range c.Module.ProviderConfigs { - fqn := c.Module.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: provider.Name}) - if _, ok := reqs[fqn]; !ok { - // We'll at least have an unconstrained dependency then, but might - // add to this in the loop below. - reqs[fqn] = nil - } - if provider.Version.Required != nil { - // The model of version constraints in this package is still the - // old one using a different upstream module to represent versions, - // so we'll need to shim that out here for now. The two parsers - // don't exactly agree in practice 🙄 so this might produce new errors. - // TODO: Use the new parser throughout this package so we can get the - // better error messages it produces in more situations. - constraints, err := getproviders.ParseVersionConstraints(provider.Version.Required.String()) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid version constraint", - // The errors returned by ParseVersionConstraint already include - // the section of input that was incorrect, so we don't need to - // include that here. - Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()), - Subject: provider.Version.DeclRange.Ptr(), - }) + moreDiags := c.addProviderRequirementsFromProviderBlock(reqs, provider) + diags = append(diags, moreDiags...) + } + + // We may have provider blocks and required_providers set in some testing + // files. + if tests && recurse { + for _, file := range c.Module.Tests { + // Then we'll also look for requirements in testing modules. + for _, run := range file.Runs { + if run.ConfigUnderTest != nil { + moreDiags := run.ConfigUnderTest.addProviderRequirements(reqs, true, false) + diags = append(diags, moreDiags...) + } } - reqs[fqn] = append(reqs[fqn], constraints...) } } if recurse { for _, childConfig := range c.Children { - moreDiags := childConfig.addProviderRequirements(reqs, true) + moreDiags := childConfig.addProviderRequirements(reqs, true, false) diags = append(diags, moreDiags...) } } @@ -433,10 +590,44 @@ func (c *Config) addProviderRequirements(reqs getproviders.Requirements, recurse return diags } +func (c *Config) addProviderRequirementsFromProviderBlock(reqs providerreqs.Requirements, provider *Provider) hcl.Diagnostics { + var diags hcl.Diagnostics + + fqn := c.Module.ProviderForLocalConfig(addrs.LocalProviderConfig{LocalName: provider.Name}) + if _, ok := reqs[fqn]; !ok { + // We'll at least have an unconstrained dependency then, but might + // add to this in the loop below. + reqs[fqn] = nil + } + if provider.Version.Required != nil { + // The model of version constraints in this package is still the + // old one using a different upstream module to represent versions, + // so we'll need to shim that out here for now. The two parsers + // don't exactly agree in practice 🙄 so this might produce new errors. + // TODO: Use the new parser throughout this package so we can get the + // better error messages it produces in more situations. + constraints, err := providerreqs.ParseVersionConstraints(provider.Version.Required.String()) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + // The errors returned by ParseVersionConstraint already include + // the section of input that was incorrect, so we don't need to + // include that here. + Detail: fmt.Sprintf("Incorrect version constraint syntax: %s.", err.Error()), + Subject: provider.Version.DeclRange.Ptr(), + }) + } + reqs[fqn] = append(reqs[fqn], constraints...) + } + + return diags +} + // resolveProviderTypes walks through the providers in the module and ensures // the true types are assigned based on the provider requirements for the // module. -func (c *Config) resolveProviderTypes() { +func (c *Config) resolveProviderTypes() map[string]addrs.Provider { for _, child := range c.Children { child.resolveProviderTypes() } @@ -478,6 +669,147 @@ func (c *Config) resolveProviderTypes() { } } } + + return providers +} + +// resolveProviderTypesForTests matches resolveProviderTypes except it uses +// the information from resolveProviderTypes to resolve the provider types for +// providers defined within the configs test files. +func (c *Config) resolveProviderTypesForTests(providers map[string]addrs.Provider) { + + for _, test := range c.Module.Tests { + + // testProviders contains the configuration blocks for all the providers + // defined by this test file. It is keyed by the name of the provider + // and the values are a slice of provider configurations which contains + // all the definitions of a named provider of which there can be + // multiple because of aliases. + testProviders := make(map[string][]*Provider) + for _, provider := range test.Providers { + testProviders[provider.Name] = append(testProviders[provider.Name], provider) + } + + // matchedProviders maps the names of providers from testProviders to + // the provider type we have identified for them so far. If during the + // course of resolving the types we find a run block is attempting to + // reuse a provider that has already been assigned a different type, + // then this is an error that we can raise now. + matchedProviders := make(map[string]addrs.Provider) + + // First, we primarily draw our provider types from the main + // configuration under test. The providers for the main configuration + // are provided to us in the argument. + + // We've now set provider types for all the providers required by the + // main configuration. But we can have modules with their own required + // providers referenced by the run blocks. We also have passed provider + // configs that can affect the types of providers when the names don't + // match, so we'll do that here. + + for _, run := range test.Runs { + + // If this run block is executing against our main configuration, we + // want to use the external providers passed in. If we are executing + // against a different module then we need to resolve the provider + // types for that first, and then use those providers. + providers := providers + if run.ConfigUnderTest != nil { + providers = run.ConfigUnderTest.resolveProviderTypes() + } + + // We now check to see what providers this run block is actually + // using, and we can then assign types back to the + + if len(run.Providers) > 0 { + // This provider is only using the subset of providers specified + // within the provider block. + + for _, p := range run.Providers { + addr, exists := providers[p.InChild.Name] + if !exists { + // If this provider wasn't explicitly defined in the + // target module, then we'll set it to the default. + addr = addrs.NewDefaultProvider(p.InChild.Name) + } + + // The child type is always just derived from the providers + // within the config this run block is using. + p.InChild.providerType = addr + + // If we have previously assigned a type to the provider + // for the parent reference, then we use that for the + // parent type. + if addr, exists := matchedProviders[p.InParent.Name]; exists { + p.InParent.providerType = addr + continue + } + + // Otherwise, we'll define the parent type based on the + // child and reference that backwards. + p.InParent.providerType = p.InChild.providerType + + if aliases, exists := testProviders[p.InParent.Name]; exists { + matchedProviders[p.InParent.Name] = p.InParent.providerType + for _, alias := range aliases { + alias.providerType = p.InParent.providerType + } + } + } + + } else { + // This provider is going to load all the providers it can using + // simple name matching. + + for name, addr := range providers { + + if _, exists := matchedProviders[name]; exists { + // Then we've already handled providers of this type + // previously. + continue + } + + if aliases, exists := testProviders[name]; exists { + // Then this provider has been defined within our test + // config. Let's give it the appropriate type. + matchedProviders[name] = addr + for _, alias := range aliases { + alias.providerType = addr + } + + continue + } + + // If we get here then it means we don't actually have a + // provider block for this provider name within our test + // file. This is fine, it just means we don't have to do + // anything and the test will use the default provider for + // that name. + + } + } + + } + + // Now, we've analysed all the test runs for this file. If any providers + // have not been claimed then we'll just give them the default provider + // for their name. + for name, aliases := range testProviders { + if _, exists := matchedProviders[name]; exists { + // Then this provider has a type already. + continue + } + + addr := addrs.NewDefaultProvider(name) + matchedProviders[name] = addr + + for _, alias := range aliases { + alias.providerType = addr + } + } + + } + } // ProviderTypes returns the FQNs of each distinct provider type referenced @@ -492,12 +824,8 @@ func (c *Config) ProviderTypes() []addrs.Provider { // Ignore diagnostics here because they relate to version constraints reqs, _ := c.ProviderRequirements() - ret := make([]addrs.Provider, 0, len(reqs)) - for k := range reqs { - ret = append(ret, k) - } - sort.Slice(ret, func(i, j int) bool { - return ret[i].String() < ret[j].String() + ret := slices.SortedFunc(maps.Keys(reqs), func(i, j addrs.Provider) int { + return strings.Compare(i.String(), j.String()) }) return ret } @@ -520,9 +848,9 @@ func (c *Config) ResolveAbsProviderAddr(addr addrs.ProviderConfig, inModule addr return addr case addrs.LocalProviderConfig: - // Find the descendent Config that contains the module that this + // Find the descendant Config that contains the module that this // local config belongs to. - mc := c.Descendent(inModule) + mc := c.Descendant(inModule) if mc == nil { panic(fmt.Sprintf("ResolveAbsProviderAddr with non-existent module %s", inModule.String())) } @@ -555,3 +883,177 @@ func (c *Config) ProviderForConfigAddr(addr addrs.LocalProviderConfig) addrs.Pro } return c.ResolveAbsProviderAddr(addr, addrs.RootModule).Provider } + +// RequiredProviderConfig represents a provider configuration that is required +// by a module, either explicitly or implicitly. +// +// An explicit provider means the LocalName within the addrs.LocalProviderConfig +// was defined directly within the configuration via a required_providers block +// instead of implied due to the name of a resource or data block. +// +// This helps callers of the EffectiveRequiredProviderConfigs function tailor +// error messages around implied or explicit provider types. +type RequiredProviderConfig struct { + Local addrs.LocalProviderConfig + Explicit bool +} + +// EffectiveRequiredProviderConfigs returns a set of all of the provider +// configurations this config's direct module expects to have passed in +// (explicitly or implicitly) by its caller. This method only makes sense +// to call on the object representing the root module. +// +// This includes both provider configurations declared explicitly using +// configuration_aliases in the required_providers block _and_ configurations +// that are implied to be required by declaring something that belongs to +// an configuration for a provider even when there is no such declaration +// inside the module itself. +// +// Terraform Core treats root modules differently than downstream modules in +// that it will implicitly create empty provider configurations for any provider +// config addresses that are implied in the configuration but not explicitly +// configured. This function assumes those implied empty configurations don't +// exist and so therefore any provider configuration without an explicit +// "provider" block is a required provider config. In practice that means that +// the answer is appropriate for downstream modules but not for root modules, +// unless a root module is being used in a context where it is treated as if +// a shared module, such as when directly testing a shared module or when +// using a shared module as the root of the module tree of a stack component. +// +// This function assumes that the configuration is valid. It may produce under- +// or over-constrained results if called on an invalid configuration. +func (c *Config) EffectiveRequiredProviderConfigs() addrs.Map[addrs.RootProviderConfig, RequiredProviderConfig] { + // The Terraform language has accumulated so many different ways to imply + // the need for a provider configuration that answering this is quite a + // complicated process that ends up potentially needing to visit the + // entire subtree of modules even though we're only actually answering + // about the current node's requirements. In the happy explicit case we + // can avoid any recursion, but that case is rare in practice. + + if c == nil { + return addrs.MakeMap[addrs.RootProviderConfig, RequiredProviderConfig]() + } + + // We'll start by visiting all of the "provider" blocks in the module and + // figuring out which provider configuration address they each declare. Any + // configuration addresses we find here cannot be "required" provider + // configs because the module instantiates them itself. + selfConfigured := addrs.MakeSet[addrs.RootProviderConfig]() + for _, pc := range c.Module.ProviderConfigs { + localAddr := pc.Addr() + sourceAddr := c.Module.ProviderForLocalConfig(localAddr) + selfConfigured.Add(addrs.RootProviderConfig{ + Provider: sourceAddr, + Alias: localAddr.Alias, + }) + } + ret := addrs.MakeMap[addrs.RootProviderConfig, RequiredProviderConfig]() + + // maybePut looks up the default local provider for the given root provider. + maybePut := func(addr addrs.RootProviderConfig) { + localName := c.Module.LocalNameForProvider(addr.Provider) + localAddr := addrs.LocalProviderConfig{ + LocalName: localName, + Alias: addr.Alias, + } + if !selfConfigured.Has(addr) && !ret.Has(addr) { + ret.Put(addr, RequiredProviderConfig{ + Local: localAddr, + + // Since we look at the required providers first below, and only + // the required providers can set explicit local names, this + // will always be false as the map entry will already have been + // set if this would be true. + Explicit: false, + }) + } + } + + // maybePutLocal looks up the default provider for the given local provider + // address. + maybePutLocal := func(localAddr addrs.LocalProviderConfig, explicit bool) { + // Caution: this function is only correct to use for LocalProviderConfig + // in the _current_ module c.Module. It will produce incorrect results + // if used for addresses from any child module. + addr := addrs.RootProviderConfig{ + Provider: c.Module.ProviderForLocalConfig(localAddr), + Alias: localAddr.Alias, + } + if !selfConfigured.Has(addr) && !ret.Has(addr) { + ret.Put(addr, RequiredProviderConfig{ + Local: localAddr, + Explicit: explicit, + }) + } + } + + if c.Module.ProviderRequirements != nil { + for _, req := range c.Module.ProviderRequirements.RequiredProviders { + for _, addr := range req.Aliases { + // The RequiredProviders block always produces explicit provider + // names. + maybePutLocal(addr, true) + } + } + } + for _, rc := range c.Module.ManagedResources { + maybePutLocal(rc.ProviderConfigAddr(), false) + } + for _, rc := range c.Module.DataResources { + maybePutLocal(rc.ProviderConfigAddr(), false) + } + for _, ic := range c.Module.Import { + if ic.ProviderConfigRef != nil { + maybePutLocal(addrs.LocalProviderConfig{ + LocalName: ic.ProviderConfigRef.Name, + Alias: ic.ProviderConfigRef.Alias, + }, false) + } else { + maybePut(addrs.RootProviderConfig{ + Provider: ic.Provider, + }) + } + } + for _, mc := range c.Module.ModuleCalls { + for _, pp := range mc.Providers { + maybePutLocal(pp.InParent.Addr(), false) + } + // If there aren't any explicitly-passed providers then + // the module implicitly requires a default configuration + // for each provider the child module mentions, since + // that would get implicitly passed into the child by + // Terraform Core. + // (We don't need to visit the child module at all if + // the call has an explicit "providers" argument, because + // we require that to be exhaustive when present.) + if len(mc.Providers) == 0 { + child := c.Children[mc.Name] + childReqs := child.EffectiveRequiredProviderConfigs() + for _, childReq := range childReqs.Keys() { + if childReq.Alias != "" { + continue // only default provider configs are eligible for this implicit treatment + } + // We must reinterpret the child address to appear as + // if written in its parent (our current module). + maybePut(addrs.RootProviderConfig{ + Provider: childReq.Provider, + }) + } + } + } + + return ret +} + +func (c *Config) CheckCoreVersionRequirements() hcl.Diagnostics { + var diags hcl.Diagnostics + + diags = diags.Extend(c.Module.CheckCoreVersionRequirements(c.Path, c.SourceAddr)) + + for _, c := range c.Children { + childDiags := c.CheckCoreVersionRequirements() + diags = diags.Extend(childDiags) + } + + return diags +} diff --git a/internal/configs/config_build.go b/internal/configs/config_build.go index 4e2dddaa1b..3f3378a2d0 100644 --- a/internal/configs/config_build.go +++ b/internal/configs/config_build.go @@ -1,41 +1,148 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( - "sort" + "fmt" + "maps" + "path" + "slices" + "strings" version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" ) // BuildConfig constructs a Config from a root module by loading all of its -// descendent modules via the given ModuleWalker. +// descendant modules via the given ModuleWalker. This function also side loads +// and installs any mock data files needed by the testing framework via the +// MockDataLoader. // // The result is a module tree that has so far only had basic module- and // file-level invariants validated. If the returned diagnostics contains errors, // the returned module tree may be incomplete but can still be used carefully // for static analysis. -func BuildConfig(root *Module, walker ModuleWalker) (*Config, hcl.Diagnostics) { +func BuildConfig(root *Module, walker ModuleWalker, loader MockDataLoader) (*Config, hcl.Diagnostics) { var diags hcl.Diagnostics cfg := &Config{ Module: root, } cfg.Root = cfg // Root module is self-referential. cfg.Children, diags = buildChildModules(cfg, walker) + diags = append(diags, buildTestModules(cfg, walker)...) // Skip provider resolution if there are any errors, since the provider // configurations themselves may not be valid. if !diags.HasErrors() { // Now that the config is built, we can connect the provider names to all // the known types for validation. - cfg.resolveProviderTypes() + providers := cfg.resolveProviderTypes() + cfg.resolveProviderTypesForTests(providers) } diags = append(diags, validateProviderConfigs(nil, cfg, nil)...) + diags = append(diags, validateProviderConfigsForTests(cfg)...) + + // Final step, let's side load any external mock data into our test files. + diags = append(diags, installMockDataFiles(cfg, loader)...) return cfg, diags } +func installMockDataFiles(root *Config, loader MockDataLoader) hcl.Diagnostics { + var diags hcl.Diagnostics + + for _, file := range root.Module.Tests { + for _, provider := range file.Providers { + if !provider.Mock { + // Don't try and process non-mocked providers. + continue + } + + data, dataDiags := loader.LoadMockData(provider) + diags = append(diags, dataDiags...) + if data != nil { + // If we loaded some data, then merge the new data into the old + // data. In this case we expect and accept collisions, so we + // don't want the merge function warning us about them. + diags = append(diags, provider.MockData.Merge(data, true)...) + } + } + } + + return diags +} + +func buildTestModules(root *Config, walker ModuleWalker) hcl.Diagnostics { + var diags hcl.Diagnostics + + for name, file := range root.Module.Tests { + for _, run := range file.Runs { + if run.Module == nil { + continue + } + + // We want to make sure the path for the testing modules are unique + // so we create a dedicated path for them. + // + // Some examples: + // - file: main.tftest.hcl, run: setup - test.main.setup + // - file: tests/main.tftest.hcl, run: setup - test.tests.main.setup + + dir := path.Dir(name) + base := path.Base(name) + + path := addrs.Module{} + path = append(path, "test") + if dir != "." { + path = append(path, strings.Split(dir, "/")...) + } + path = append(path, strings.TrimSuffix(base, ".tftest.hcl"), run.Name) + + req := ModuleRequest{ + Name: run.Name, + Path: path, + SourceAddr: run.Module.Source, + SourceAddrRange: run.Module.SourceDeclRange, + VersionConstraint: run.Module.Version, + Parent: root, + CallRange: run.Module.DeclRange, + } + + cfg, modDiags := loadModule(root, &req, walker) + diags = append(diags, modDiags...) + + if cfg != nil { + // To get the loader to work, we need to set a bunch of values + // (like the name, path, and parent) as if the module was being + // loaded as a child of the root config. + // + // In actuality, when this is executed it will be as if the + // module was the root. So, we'll post-process some things to + // get it to behave as expected later. + + // First, update the main module for this test run to behave as + // if it is the root module. + cfg.Parent = nil + + // Then we need to update the paths for this config and all + // children, so they think they are all relative to the root + // module we just created. + rebaseChildModule(cfg, cfg) + + // Finally, link the new config back into our test run so + // it can be retrieved later. + run.ConfigUnderTest = cfg + } + } + } + + return diags +} + func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, hcl.Diagnostics) { var diags hcl.Diagnostics ret := map[string]*Config{} @@ -44,17 +151,10 @@ func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, // We'll sort the calls by their local names so that they'll appear in a // predictable order in any logging that's produced during the walk. - callNames := make([]string, 0, len(calls)) - for k := range calls { - callNames = append(callNames, k) - } - sort.Strings(callNames) - - for _, callName := range callNames { + for _, callName := range slices.Sorted(maps.Keys(calls)) { call := calls[callName] - path := make([]string, len(parent.Path)+1) - copy(path, parent.Path) - path[len(path)-1] = call.Name + path := slices.Clone(parent.Path) + path = append(path, call.Name) req := ModuleRequest{ Name: call.Name, @@ -65,45 +165,88 @@ func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, Parent: parent, CallRange: call.DeclRange, } - - mod, ver, modDiags := walker.LoadModule(&req) + child, modDiags := loadModule(parent.Root, &req, walker) diags = append(diags, modDiags...) - if mod == nil { - // nil can be returned if the source address was invalid and so - // nothing could be loaded whatsoever. LoadModule should've - // returned at least one error diagnostic in that case. + if child == nil { + // This means an error occurred, there should be diagnostics within + // modDiags for this. continue } - child := &Config{ - Parent: parent, - Root: parent.Root, - Path: path, - Module: mod, - CallRange: call.DeclRange, - SourceAddr: call.SourceAddr, - SourceAddrRange: call.SourceAddrRange, - Version: ver, - } - - child.Children, modDiags = buildChildModules(child, walker) - diags = append(diags, modDiags...) - - if mod.Backend != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Backend configuration ignored", - Detail: "Any selected backend applies to the entire configuration, so Terraform expects provider configurations only in the root module.\n\nThis is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing purposes, but this backend configuration block will have no effect.", - Subject: mod.Backend.DeclRange.Ptr(), - }) - } - ret[call.Name] = child } return ret, diags } +func loadModule(root *Config, req *ModuleRequest, walker ModuleWalker) (*Config, hcl.Diagnostics) { + var diags hcl.Diagnostics + + mod, ver, modDiags := walker.LoadModule(req) + diags = append(diags, modDiags...) + if mod == nil { + // nil can be returned if the source address was invalid and so + // nothing could be loaded whatsoever. LoadModule should've + // returned at least one error diagnostic in that case. + return nil, diags + } + + cfg := &Config{ + Parent: req.Parent, + Root: root, + Path: req.Path, + Module: mod, + CallRange: req.CallRange, + SourceAddr: req.SourceAddr, + SourceAddrRange: req.SourceAddrRange, + Version: ver, + } + + cfg.Children, modDiags = buildChildModules(cfg, walker) + diags = append(diags, modDiags...) + + if mod.Backend != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Backend configuration ignored", + Detail: "Any selected backend applies to the entire configuration, so Terraform expects provider configurations only in the root module.\n\nThis is a warning rather than an error because it's sometimes convenient to temporarily call a root module as a child module for testing purposes, but this backend configuration block will have no effect.", + Subject: mod.Backend.DeclRange.Ptr(), + }) + } + + if len(mod.Import) > 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import configuration", + Detail: fmt.Sprintf("An import block was detected in %q. Import blocks are only allowed in the root module.", cfg.Path), + Subject: mod.Import[0].DeclRange.Ptr(), + }) + } + + return cfg, diags +} + +// rebaseChildModule updates cfg to make it act as if root is the base of the +// module tree. +// +// This is used for modules loaded directly from test files. In order to load +// them properly, and reuse the code for loading modules from normal +// configuration files, we pretend they are children of the main configuration +// object. Later, when it comes time for them to execute they will act as if +// they are the root module directly. +// +// This function updates cfg so that it treats the provided root as the actual +// root of this module tree. It then recurses into all the child modules and +// does the same for them. +func rebaseChildModule(cfg *Config, root *Config) { + for _, child := range cfg.Children { + rebaseChildModule(child, root) + } + + cfg.Path = cfg.Path[len(root.Path):] + cfg.Root = root +} + // A ModuleWalker knows how to find and load a child module given details about // the module to be loaded and a reference to its partially-loaded parent // Config. @@ -198,3 +341,21 @@ func init() { } }) } + +// MockDataLoader provides an interface similar to loading modules, except it loads +// and returns MockData objects for the testing framework to consume. +type MockDataLoader interface { + // LoadMockData accepts a path to a local directory that should contain a + // set of .tfmock.hcl files that contain mock data that can be consumed by + // a mock provider within the tewting framework. + LoadMockData(provider *Provider) (*MockData, hcl.Diagnostics) +} + +// MockDataLoaderFunc is an implementation of MockDataLoader that wraps a +// callback function, for more convenient use of that interface. +type MockDataLoaderFunc func(provider *Provider) (*MockData, hcl.Diagnostics) + +// LoadMockData implements MockDataLoader. +func (f MockDataLoaderFunc) LoadMockData(provider *Provider) (*MockData, hcl.Diagnostics) { + return f(provider) +} diff --git a/internal/configs/config_build_test.go b/internal/configs/config_build_test.go index 274a5cd01b..2966d13bfb 100644 --- a/internal/configs/config_build_test.go +++ b/internal/configs/config_build_test.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "fmt" "io/ioutil" + "path" "path/filepath" "reflect" "sort" @@ -10,6 +14,7 @@ import ( "testing" "github.com/davecgh/go-spew/spew" + "github.com/zclconf/go-cty/cty" version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" @@ -36,8 +41,11 @@ func TestBuildConfig(t *testing.T) { version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI)) versionI++ return mod, version, diags - }, - )) + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) assertNoDiagnostics(t, diags) if cfg == nil { t.Fatal("got nil config; want non-nil") @@ -92,8 +100,11 @@ func TestBuildConfigDiags(t *testing.T) { version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI)) versionI++ return mod, version, diags - }, - )) + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) wantDiag := `testdata/nested-errors/child_c/child_c.tf:5,1-8: ` + `Unsupported block type; Blocks of type "invalid" are not expected here.` @@ -135,8 +146,11 @@ func TestBuildConfigChildModuleBackend(t *testing.T) { mod, diags := parser.LoadConfigDir(sourcePath) version, _ := version.NewVersion("1.0.0") return mod, version, diags - }, - )) + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) assertDiagnosticSummary(t, diags, "Backend configuration ignored") @@ -169,7 +183,7 @@ func TestBuildConfigInvalidModules(t *testing.T) { parser := NewParser(nil) path := filepath.Join(testDir, name) - mod, diags := parser.LoadConfigDir(path) + mod, diags := parser.LoadConfigDirWithTests(path, "tests") if diags.HasErrors() { // these tests should only trigger errors that are caught in // the config loader. @@ -210,8 +224,11 @@ func TestBuildConfigInvalidModules(t *testing.T) { mod, diags := parser.LoadConfigDir(sourcePath) version, _ := version.NewVersion("1.0.0") return mod, version, diags - }, - )) + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) // we can make this less repetitive later if we want for _, msg := range expectedErrs { @@ -279,3 +296,208 @@ func TestBuildConfigInvalidModules(t *testing.T) { }) } } + +func TestBuildConfig_WithMockDataSources(t *testing.T) { + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-mock-sources", "tests") + assertNoDiagnostics(t, diags) + assertNoDiagnostics(t, diags) + if mod == nil { + t.Fatal("got nil root module; want non-nil") + } + + cfg, diags := BuildConfig(mod, nil, MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + sourcePath := filepath.Join("testdata/valid-modules/with-mock-sources", provider.MockDataExternalSource) + return parser.LoadMockDataDir(sourcePath, provider.MockDataDuringPlan, hcl.Range{}) + })) + assertNoDiagnostics(t, diags) + if cfg == nil { + t.Fatal("got nil config; want non-nil") + } + + provider := cfg.Module.Tests["main.tftest.hcl"].Providers["aws"] + + if len(provider.MockData.MockDataSources) != 1 { + t.Errorf("expected to load 1 mock data source but loaded %d", len(provider.MockData.MockDataSources)) + } + if len(provider.MockData.MockResources) != 1 { + t.Errorf("expected to load 1 mock resource but loaded %d", len(provider.MockData.MockResources)) + } + if provider.MockData.Overrides.Len() != 1 { + t.Errorf("expected to load 1 override but loaded %d", provider.MockData.Overrides.Len()) + } +} + +func TestBuildConfig_WithMockDataSourcesInline(t *testing.T) { + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-mock-sources-inline", "tests") + assertNoDiagnostics(t, diags) + assertNoDiagnostics(t, diags) + if mod == nil { + t.Fatal("got nil root module; want non-nil") + } + + cfg, diags := BuildConfig(mod, nil, MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + sourcePath := filepath.Join("testdata/valid-modules/with-mock-sources-inline", provider.MockDataExternalSource) + return parser.LoadMockDataDir(sourcePath, provider.MockDataDuringPlan, hcl.Range{}) + })) + assertNoDiagnostics(t, diags) + if cfg == nil { + t.Fatal("got nil config; want non-nil") + } + + provider := cfg.Module.Tests["main.tftest.hcl"].Providers["aws"] + + // This time we want to check that the mock data defined inline took + // precedence over the mock data defined in the data files. + defaults := provider.MockData.MockResources["aws_s3_bucket"].Defaults + expected := cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("aws:s3:::bucket"), + }) + + if !defaults.RawEquals(expected) { + t.Errorf("expected: %s\nactual: %s", expected.GoString(), defaults.GoString()) + } +} + +func TestBuildConfig_WithNestedTestModules(t *testing.T) { + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-nested-module", "tests") + assertNoDiagnostics(t, diags) + if mod == nil { + t.Fatal("got nil root module; want non-nil") + } + + cfg, diags := BuildConfig(mod, ModuleWalkerFunc( + func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { + + // Bit of a hack to get the test working, but we know all the source + // addresses in this test are locals, so we can just treat them as + // paths in the filesystem. + + addr := req.SourceAddr.String() + current := req.Parent + for current.SourceAddr != nil { + addr = path.Join(current.SourceAddr.String(), addr) + current = current.Parent + } + sourcePath := filepath.Join("testdata/valid-modules/with-tests-nested-module", addr) + + mod, diags := parser.LoadConfigDir(sourcePath) + version, _ := version.NewVersion("1.0.0") + return mod, version, diags + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) + assertNoDiagnostics(t, diags) + if cfg == nil { + t.Fatal("got nil config; want non-nil") + } + + // We should have loaded our test case, and one of the test runs should + // have loaded an alternate module. + + if len(cfg.Module.Tests) != 1 { + t.Fatalf("expected exactly one test case but found %d", len(cfg.Module.Tests)) + } + + test := cfg.Module.Tests["main.tftest.hcl"] + if len(test.Runs) != 1 { + t.Fatalf("expected two test runs but found %d", len(test.Runs)) + } + + run := test.Runs[0] + if run.ConfigUnderTest == nil { + t.Fatalf("the first test run should have loaded config but did not") + } + + if run.ConfigUnderTest.Parent != nil { + t.Errorf("config under test should not have a parent") + } + + if run.ConfigUnderTest.Root != run.ConfigUnderTest { + t.Errorf("config under test root should be itself") + } + + if len(run.ConfigUnderTest.Path) > 0 { + t.Errorf("config under test path should be the root module") + } + + // We should also have loaded a single child underneath the config under + // test, and it should have valid paths. + + child := run.ConfigUnderTest.Children["child"] + + if child.Parent != run.ConfigUnderTest { + t.Errorf("child should point back to root") + } + + if len(child.Path) != 1 || child.Path[0] != "child" { + t.Errorf("child should have rebased against virtual root") + } + + if child.Root != run.ConfigUnderTest { + t.Errorf("child root should be main config under test") + } +} + +func TestBuildConfig_WithTestModule(t *testing.T) { + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests-module", "tests") + assertNoDiagnostics(t, diags) + if mod == nil { + t.Fatal("got nil root module; want non-nil") + } + + cfg, diags := BuildConfig(mod, ModuleWalkerFunc( + func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { + // For the sake of this test we're going to just treat our + // SourceAddr as a path relative to our fixture directory. + // A "real" implementation of ModuleWalker should accept the + // various different source address syntaxes Terraform supports. + sourcePath := filepath.Join("testdata/valid-modules/with-tests-module", req.SourceAddr.String()) + + mod, diags := parser.LoadConfigDir(sourcePath) + version, _ := version.NewVersion("1.0.0") + return mod, version, diags + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) + assertNoDiagnostics(t, diags) + if cfg == nil { + t.Fatal("got nil config; want non-nil") + } + + // We should have loaded our test case, and one of the test runs should + // have loaded an alternate module. + + if len(cfg.Module.Tests) != 1 { + t.Fatalf("expected exactly one test case but found %d", len(cfg.Module.Tests)) + } + + test := cfg.Module.Tests["main.tftest.hcl"] + if len(test.Runs) != 2 { + t.Fatalf("expected two test runs but found %d", len(test.Runs)) + } + + run := test.Runs[0] + if run.ConfigUnderTest == nil { + t.Fatalf("the first test run should have loaded config but did not") + } + + if run.ConfigUnderTest.Parent != nil { + t.Errorf("config under test should not have a parent") + } + + if run.ConfigUnderTest.Root != run.ConfigUnderTest { + t.Errorf("config under test root should be itself") + } + + if len(run.ConfigUnderTest.Path) > 0 { + t.Errorf("config under test path should be the root module") + } +} diff --git a/internal/configs/config_test.go b/internal/configs/config_test.go index b5360278df..9d81d12cc9 100644 --- a/internal/configs/config_test.go +++ b/internal/configs/config_test.go @@ -1,19 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( + "os" "testing" "github.com/go-test/deep" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/zclconf/go-cty/cty" - version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2/hclsyntax" svchost "github.com/hashicorp/terraform-svchost" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/depsfile" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + + _ "github.com/hashicorp/terraform/internal/logging" ) func TestConfigProviderTypes(t *testing.T) { @@ -31,6 +37,7 @@ func TestConfigProviderTypes(t *testing.T) { got = cfg.ProviderTypes() want := []addrs.Provider{ addrs.NewDefaultProvider("aws"), + addrs.NewDefaultProvider("local"), addrs.NewDefaultProvider("null"), addrs.NewDefaultProvider("template"), addrs.NewDefaultProvider("test"), @@ -141,12 +148,12 @@ func TestConfigProviderRequirements(t *testing.T) { got, diags := cfg.ProviderRequirements() assertNoDiagnostics(t, diags) - want := getproviders.Requirements{ + want := providerreqs.Requirements{ // the nullProvider constraints from the two modules are merged - nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), - randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), - tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), - configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"), + nullProvider: providerreqs.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), + randomProvider: providerreqs.MustParseVersionConstraints("~> 1.2.0"), + tlsProvider: providerreqs.MustParseVersionConstraints("~> 3.0"), + configuredProvider: providerreqs.MustParseVersionConstraints("~> 1.4"), impliedProvider: nil, happycloudProvider: nil, terraformProvider: nil, @@ -158,6 +165,37 @@ func TestConfigProviderRequirements(t *testing.T) { } } +func TestConfigProviderRequirementsInclTests(t *testing.T) { + cfg, diags := testNestedModuleConfigFromDirWithTests(t, "testdata/provider-reqs-with-tests") + assertDiagnosticCount(t, diags, 0) + + tlsProvider := addrs.NewProvider( + addrs.DefaultProviderRegistryHost, + "hashicorp", "tls", + ) + nullProvider := addrs.NewDefaultProvider("null") + randomProvider := addrs.NewDefaultProvider("random") + impliedProvider := addrs.NewDefaultProvider("implied") + terraformProvider := addrs.NewBuiltInProvider("terraform") + configuredProvider := addrs.NewDefaultProvider("configured") + + got, diags := cfg.ProviderRequirements() + assertNoDiagnostics(t, diags) + want := providerreqs.Requirements{ + // the nullProvider constraints from the two modules are merged + nullProvider: providerreqs.MustParseVersionConstraints("~> 2.0.0"), + randomProvider: providerreqs.MustParseVersionConstraints("~> 1.2.0"), + tlsProvider: providerreqs.MustParseVersionConstraints("~> 3.0"), + configuredProvider: nil, + impliedProvider: nil, + terraformProvider: nil, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + func TestConfigProviderRequirementsDuplicate(t *testing.T) { _, diags := testNestedModuleConfigFromDir(t, "testdata/duplicate-local-name") assertDiagnosticCount(t, diags, 3) @@ -185,12 +223,12 @@ func TestConfigProviderRequirementsShallow(t *testing.T) { got, diags := cfg.ProviderRequirementsShallow() assertNoDiagnostics(t, diags) - want := getproviders.Requirements{ + want := providerreqs.Requirements{ // the nullProvider constraint is only from the root module - nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"), - randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), - tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), - configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"), + nullProvider: providerreqs.MustParseVersionConstraints("~> 2.0.0"), + randomProvider: providerreqs.MustParseVersionConstraints("~> 1.2.0"), + tlsProvider: providerreqs.MustParseVersionConstraints("~> 3.0"), + configuredProvider: providerreqs.MustParseVersionConstraints("~> 1.4"), impliedProvider: nil, terraformProvider: nil, } @@ -200,6 +238,30 @@ func TestConfigProviderRequirementsShallow(t *testing.T) { } } +func TestConfigProviderRequirementsShallowInclTests(t *testing.T) { + cfg, diags := testNestedModuleConfigFromDirWithTests(t, "testdata/provider-reqs-with-tests") + assertDiagnosticCount(t, diags, 0) + + tlsProvider := addrs.NewProvider( + addrs.DefaultProviderRegistryHost, + "hashicorp", "tls", + ) + impliedProvider := addrs.NewDefaultProvider("implied") + terraformProvider := addrs.NewBuiltInProvider("terraform") + + got, diags := cfg.ProviderRequirementsShallow() + assertNoDiagnostics(t, diags) + want := providerreqs.Requirements{ + tlsProvider: providerreqs.MustParseVersionConstraints("~> 3.0"), + impliedProvider: nil, + terraformProvider: nil, + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + func TestConfigProviderRequirementsByModule(t *testing.T) { cfg, diags := testNestedModuleConfigFromDir(t, "testdata/provider-reqs") // TODO: Version Constraint Deprecation. @@ -230,12 +292,12 @@ func TestConfigProviderRequirementsByModule(t *testing.T) { Name: "", SourceAddr: nil, SourceDir: "testdata/provider-reqs", - Requirements: getproviders.Requirements{ + Requirements: providerreqs.Requirements{ // Only the root module's version is present here - nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0"), - randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), - tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), - configuredProvider: getproviders.MustParseVersionConstraints("~> 1.4"), + nullProvider: providerreqs.MustParseVersionConstraints("~> 2.0.0"), + randomProvider: providerreqs.MustParseVersionConstraints("~> 1.2.0"), + tlsProvider: providerreqs.MustParseVersionConstraints("~> 3.0"), + configuredProvider: providerreqs.MustParseVersionConstraints("~> 1.4"), impliedProvider: nil, terraformProvider: nil, }, @@ -244,8 +306,8 @@ func TestConfigProviderRequirementsByModule(t *testing.T) { Name: "kinder", SourceAddr: addrs.ModuleSourceLocal("./child"), SourceDir: "testdata/provider-reqs/child", - Requirements: getproviders.Requirements{ - nullProvider: getproviders.MustParseVersionConstraints("= 2.0.1"), + Requirements: providerreqs.Requirements{ + nullProvider: providerreqs.MustParseVersionConstraints("= 2.0.1"), happycloudProvider: nil, }, Children: map[string]*ModuleRequirements{ @@ -253,10 +315,67 @@ func TestConfigProviderRequirementsByModule(t *testing.T) { Name: "nested", SourceAddr: addrs.ModuleSourceLocal("./grandchild"), SourceDir: "testdata/provider-reqs/child/grandchild", - Requirements: getproviders.Requirements{ + Requirements: providerreqs.Requirements{ grandchildProvider: nil, }, Children: map[string]*ModuleRequirements{}, + Tests: make(map[string]*TestFileModuleRequirements), + }, + }, + Tests: make(map[string]*TestFileModuleRequirements), + }, + }, + Tests: make(map[string]*TestFileModuleRequirements), + } + + ignore := cmpopts.IgnoreUnexported(version.Constraint{}, cty.Value{}, hclsyntax.Body{}) + if diff := cmp.Diff(want, got, ignore); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + +func TestConfigProviderRequirementsByModuleInclTests(t *testing.T) { + cfg, diags := testNestedModuleConfigFromDirWithTests(t, "testdata/provider-reqs-with-tests") + assertDiagnosticCount(t, diags, 0) + + tlsProvider := addrs.NewProvider( + addrs.DefaultProviderRegistryHost, + "hashicorp", "tls", + ) + nullProvider := addrs.NewDefaultProvider("null") + randomProvider := addrs.NewDefaultProvider("random") + impliedProvider := addrs.NewDefaultProvider("implied") + terraformProvider := addrs.NewBuiltInProvider("terraform") + configuredProvider := addrs.NewDefaultProvider("configured") + + got, diags := cfg.ProviderRequirementsByModule() + assertNoDiagnostics(t, diags) + want := &ModuleRequirements{ + Name: "", + SourceAddr: nil, + SourceDir: "testdata/provider-reqs-with-tests", + Requirements: providerreqs.Requirements{ + // Only the root module's version is present here + tlsProvider: providerreqs.MustParseVersionConstraints("~> 3.0"), + impliedProvider: nil, + terraformProvider: nil, + }, + Children: make(map[string]*ModuleRequirements), + Tests: map[string]*TestFileModuleRequirements{ + "provider-reqs-root.tftest.hcl": { + Requirements: providerreqs.Requirements{}, + Runs: map[string]*ModuleRequirements{ + "setup": { + Name: "setup", + SourceAddr: addrs.ModuleSourceLocal("./setup"), + SourceDir: "testdata/provider-reqs-with-tests/setup", + Requirements: providerreqs.Requirements{ + nullProvider: providerreqs.MustParseVersionConstraints("~> 2.0.0"), + randomProvider: providerreqs.MustParseVersionConstraints("~> 1.2.0"), + configuredProvider: nil, + }, + Children: make(map[string]*ModuleRequirements), + Tests: make(map[string]*TestFileModuleRequirements), }, }, }, @@ -312,25 +431,25 @@ func TestVerifyDependencySelections(t *testing.T) { }, "suitable locks": { func(locks *depsfile.Locks) { - locks.SetProvider(configuredProvider, getproviders.MustParseVersion("1.4.0"), nil, nil) - locks.SetProvider(grandchildProvider, getproviders.MustParseVersion("0.1.0"), nil, nil) - locks.SetProvider(impliedProvider, getproviders.MustParseVersion("0.2.0"), nil, nil) - locks.SetProvider(nullProvider, getproviders.MustParseVersion("2.0.1"), nil, nil) - locks.SetProvider(randomProvider, getproviders.MustParseVersion("1.2.2"), nil, nil) - locks.SetProvider(tlsProvider, getproviders.MustParseVersion("3.0.1"), nil, nil) - locks.SetProvider(happycloudProvider, getproviders.MustParseVersion("0.0.1"), nil, nil) + locks.SetProvider(configuredProvider, providerreqs.MustParseVersion("1.4.0"), nil, nil) + locks.SetProvider(grandchildProvider, providerreqs.MustParseVersion("0.1.0"), nil, nil) + locks.SetProvider(impliedProvider, providerreqs.MustParseVersion("0.2.0"), nil, nil) + locks.SetProvider(nullProvider, providerreqs.MustParseVersion("2.0.1"), nil, nil) + locks.SetProvider(randomProvider, providerreqs.MustParseVersion("1.2.2"), nil, nil) + locks.SetProvider(tlsProvider, providerreqs.MustParseVersion("3.0.1"), nil, nil) + locks.SetProvider(happycloudProvider, providerreqs.MustParseVersion("0.0.1"), nil, nil) }, nil, }, "null provider constraints changed": { func(locks *depsfile.Locks) { - locks.SetProvider(configuredProvider, getproviders.MustParseVersion("1.4.0"), nil, nil) - locks.SetProvider(grandchildProvider, getproviders.MustParseVersion("0.1.0"), nil, nil) - locks.SetProvider(impliedProvider, getproviders.MustParseVersion("0.2.0"), nil, nil) - locks.SetProvider(nullProvider, getproviders.MustParseVersion("3.0.0"), nil, nil) - locks.SetProvider(randomProvider, getproviders.MustParseVersion("1.2.2"), nil, nil) - locks.SetProvider(tlsProvider, getproviders.MustParseVersion("3.0.1"), nil, nil) - locks.SetProvider(happycloudProvider, getproviders.MustParseVersion("0.0.1"), nil, nil) + locks.SetProvider(configuredProvider, providerreqs.MustParseVersion("1.4.0"), nil, nil) + locks.SetProvider(grandchildProvider, providerreqs.MustParseVersion("0.1.0"), nil, nil) + locks.SetProvider(impliedProvider, providerreqs.MustParseVersion("0.2.0"), nil, nil) + locks.SetProvider(nullProvider, providerreqs.MustParseVersion("3.0.0"), nil, nil) + locks.SetProvider(randomProvider, providerreqs.MustParseVersion("1.2.2"), nil, nil) + locks.SetProvider(tlsProvider, providerreqs.MustParseVersion("3.0.1"), nil, nil) + locks.SetProvider(happycloudProvider, providerreqs.MustParseVersion("0.0.1"), nil, nil) }, []string{ `provider registry.terraform.io/hashicorp/null: locked version selection 3.0.0 doesn't match the updated version constraints "~> 2.0.0, 2.0.1"`, @@ -341,14 +460,14 @@ func TestVerifyDependencySelections(t *testing.T) { // In this case, we set the lock file version constraints to // match the configuration, and so our error message changes // to not assume the configuration changed anymore. - locks.SetProvider(nullProvider, getproviders.MustParseVersion("3.0.0"), getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), nil) + locks.SetProvider(nullProvider, providerreqs.MustParseVersion("3.0.0"), providerreqs.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), nil) - locks.SetProvider(configuredProvider, getproviders.MustParseVersion("1.4.0"), nil, nil) - locks.SetProvider(grandchildProvider, getproviders.MustParseVersion("0.1.0"), nil, nil) - locks.SetProvider(impliedProvider, getproviders.MustParseVersion("0.2.0"), nil, nil) - locks.SetProvider(randomProvider, getproviders.MustParseVersion("1.2.2"), nil, nil) - locks.SetProvider(tlsProvider, getproviders.MustParseVersion("3.0.1"), nil, nil) - locks.SetProvider(happycloudProvider, getproviders.MustParseVersion("0.0.1"), nil, nil) + locks.SetProvider(configuredProvider, providerreqs.MustParseVersion("1.4.0"), nil, nil) + locks.SetProvider(grandchildProvider, providerreqs.MustParseVersion("0.1.0"), nil, nil) + locks.SetProvider(impliedProvider, providerreqs.MustParseVersion("0.2.0"), nil, nil) + locks.SetProvider(randomProvider, providerreqs.MustParseVersion("1.2.2"), nil, nil) + locks.SetProvider(tlsProvider, providerreqs.MustParseVersion("3.0.1"), nil, nil) + locks.SetProvider(happycloudProvider, providerreqs.MustParseVersion("0.0.1"), nil, nil) }, []string{ `provider registry.terraform.io/hashicorp/null: version constraints "~> 2.0.0, 2.0.1" don't match the locked version selection 3.0.0`, @@ -413,9 +532,51 @@ func TestConfigAddProviderRequirements(t *testing.T) { cfg, diags := testModuleConfigFromFile("testdata/valid-files/providers-explicit-implied.tf") assertNoDiagnostics(t, diags) - reqs := getproviders.Requirements{ + reqs := providerreqs.Requirements{ addrs.NewDefaultProvider("null"): nil, } - diags = cfg.addProviderRequirements(reqs, true) + diags = cfg.addProviderRequirements(reqs, true, false) assertNoDiagnostics(t, diags) } + +func TestConfigImportProviderClashesWithModules(t *testing.T) { + src, err := os.ReadFile("testdata/invalid-import-files/import-and-module-clash.tf") + if err != nil { + t.Fatal(err) + } + + parser := testParser(map[string]string{ + "main.tf": string(src), + }) + + _, diags := parser.LoadConfigFile("main.tf") + assertExactDiagnostics(t, diags, []string{ + `main.tf:9,3-19: Invalid import provider argument; The provider argument can only be specified in import blocks that will generate configuration. + +Use the providers argument within the module block to configure providers for all resources within a module, including imported resources.`, + }) +} + +func TestConfigImportProviderClashesWithResources(t *testing.T) { + cfg, diags := testModuleConfigFromFile("testdata/invalid-import-files/import-and-resource-clash.tf") + assertNoDiagnostics(t, diags) + + diags = cfg.addProviderRequirements(providerreqs.Requirements{}, true, false) + assertExactDiagnostics(t, diags, []string{ + `testdata/invalid-import-files/import-and-resource-clash.tf:9,3-19: Invalid import provider argument; The provider argument can only be specified in import blocks that will generate configuration. + +Use the provider argument in the target resource block to configure the provider for a resource with explicit provider configuration.`, + }) +} + +func TestConfigImportProviderWithNoResourceProvider(t *testing.T) { + cfg, diags := testModuleConfigFromFile("testdata/invalid-import-files/import-and-no-resource.tf") + assertNoDiagnostics(t, diags) + + diags = cfg.addProviderRequirements(providerreqs.Requirements{}, true, false) + assertExactDiagnostics(t, diags, []string{ + `testdata/invalid-import-files/import-and-no-resource.tf:5,3-19: Invalid import provider argument; The provider argument can only be specified in import blocks that will generate configuration. + +Use the provider argument in the target resource block to configure the provider for a resource with explicit provider configuration.`, + }) +} diff --git a/internal/configs/configload/copy_dir.go b/internal/configs/configload/copy_dir.go index 840a7aa975..ff7abdb941 100644 --- a/internal/configs/configload/copy_dir.go +++ b/internal/configs/configload/copy_dir.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( @@ -27,11 +30,17 @@ func copyDir(dst, src string) error { // destination with the path without the src on it. dstPath := filepath.Join(dst, path[len(src):]) - // we don't want to try and copy the same file over itself. - if eq, err := sameFile(path, dstPath); eq { + // Call os.Stat on dstPath to obtain os.FileInfo since os.SameFile + // requires FileInfo objects for comparison. + dstInfo, err := os.Stat(dstPath) + if err != nil { + if !os.IsNotExist(err) { + return err + } + } else if os.SameFile(info, dstInfo) { + // The destination file exists and is the same as the source file; + // skip copying. return nil - } else if err != nil { - return err } // If we have a directory, make that subdirectory, then continue @@ -83,33 +92,3 @@ func copyDir(dst, src string) error { return filepath.Walk(src, walkFn) } - -// sameFile tried to determine if to paths are the same file. -// If the paths don't match, we lookup the inode on supported systems. -func sameFile(a, b string) (bool, error) { - if a == b { - return true, nil - } - - aIno, err := inode(a) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - - bIno, err := inode(b) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - - if aIno > 0 && aIno == bIno { - return true, nil - } - - return false, nil -} diff --git a/internal/configs/configload/copy_dir_test.go b/internal/configs/configload/copy_dir_test.go index b7cc80495a..fa71a709ed 100644 --- a/internal/configs/configload/copy_dir_test.go +++ b/internal/configs/configload/copy_dir_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( diff --git a/internal/configs/configload/doc.go b/internal/configs/configload/doc.go index 8b615f9026..96ba285f5d 100644 --- a/internal/configs/configload/doc.go +++ b/internal/configs/configload/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package configload knows how to install modules into the .terraform/modules // directory and to load modules from those installed locations. It is used // in conjunction with the LoadConfig function in the parent package. diff --git a/internal/configs/configload/inode.go b/internal/configs/configload/inode.go deleted file mode 100644 index cbd93204c1..0000000000 --- a/internal/configs/configload/inode.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build linux || darwin || openbsd || netbsd || solaris || dragonfly -// +build linux darwin openbsd netbsd solaris dragonfly - -package configload - -import ( - "fmt" - "os" - "syscall" -) - -// lookup the inode of a file on posix systems -func inode(path string) (uint64, error) { - stat, err := os.Stat(path) - if err != nil { - return 0, err - } - if st, ok := stat.Sys().(*syscall.Stat_t); ok { - return st.Ino, nil - } - return 0, fmt.Errorf("could not determine file inode") -} diff --git a/internal/configs/configload/inode_freebsd.go b/internal/configs/configload/inode_freebsd.go deleted file mode 100644 index cefd72ca55..0000000000 --- a/internal/configs/configload/inode_freebsd.go +++ /dev/null @@ -1,22 +0,0 @@ -//go:build freebsd -// +build freebsd - -package configload - -import ( - "fmt" - "os" - "syscall" -) - -// lookup the inode of a file on posix systems -func inode(path string) (uint64, error) { - stat, err := os.Stat(path) - if err != nil { - return 0, err - } - if st, ok := stat.Sys().(*syscall.Stat_t); ok { - return uint64(st.Ino), nil - } - return 0, fmt.Errorf("could not determine file inode") -} diff --git a/internal/configs/configload/inode_windows.go b/internal/configs/configload/inode_windows.go deleted file mode 100644 index be26679a84..0000000000 --- a/internal/configs/configload/inode_windows.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build windows -// +build windows - -package configload - -// no syscall.Stat_t on windows, return 0 for inodes -func inode(path string) (uint64, error) { - return 0, nil -} diff --git a/internal/configs/configload/loader.go b/internal/configs/configload/loader.go index 0f2481d3f6..07d68b30dd 100644 --- a/internal/configs/configload/loader.go +++ b/internal/configs/configload/loader.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( @@ -6,6 +9,7 @@ import ( "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/modsdir" "github.com/hashicorp/terraform/internal/registry" "github.com/spf13/afero" ) @@ -20,7 +24,7 @@ type Loader struct { // parser is used to read configuration parser *configs.Parser - // modules is used to install and locate descendent modules that are + // modules is used to install and locate descendant modules that are // referenced (directly or indirectly) from the root module. modules moduleMgr } @@ -28,7 +32,7 @@ type Loader struct { // Config is used with NewLoader to specify configuration arguments for the // loader. type Config struct { - // ModulesDir is a path to a directory where descendent modules are + // ModulesDir is a path to a directory where descendant modules are // (or should be) installed. (This is usually the // .terraform/modules directory, in the common case where this package // is being loaded from the main Terraform CLI package.) @@ -104,6 +108,10 @@ func (l *Loader) Parser() *configs.Parser { return l.parser } +func (l *Loader) ModuleManifest() modsdir.Manifest { + return l.modules.manifest +} + // Sources returns the source code cache for the underlying parser of this // loader. This is a shorthand for l.Parser().Sources(). func (l *Loader) Sources() map[string][]byte { @@ -161,3 +169,10 @@ func (l *Loader) ImportSourcesFromSnapshot(snap *Snapshot) { func (l *Loader) AllowLanguageExperiments(allowed bool) { l.parser.AllowLanguageExperiments(allowed) } + +// AllowsLanguageExperiments returns the value most recently passed to +// [Loader.AllowLanguageExperiments], or false if that method has not been +// called on this object. +func (l *Loader) AllowsLanguageExperiments() bool { + return l.parser.AllowsLanguageExperiments() +} diff --git a/internal/configs/configload/loader_load.go b/internal/configs/configload/loader_load.go index 1ca26814a2..6a9c1f92fb 100644 --- a/internal/configs/configload/loader_load.go +++ b/internal/configs/configload/loader_load.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( @@ -5,12 +8,13 @@ import ( version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/configs" ) // LoadConfig reads the Terraform module in the given directory and uses it as the // root module to build the static module tree that represents a configuration, -// assuming that all required descendent modules have already been installed. +// assuming that all required descendant modules have already been installed. // // If error diagnostics are returned, the returned configuration may be either // nil or incomplete. In the latter case, cautious static analysis is possible @@ -19,7 +23,16 @@ import ( // LoadConfig performs the basic syntax and uniqueness validations that are // required to process the individual modules func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) { - rootMod, diags := l.parser.LoadConfigDir(rootDir) + return l.loadConfig(l.parser.LoadConfigDir(rootDir)) +} + +// LoadConfigWithTests matches LoadConfig, except the configs.Config contains +// any relevant .tftest.hcl files. +func (l *Loader) LoadConfigWithTests(rootDir string, testDir string) (*configs.Config, hcl.Diagnostics) { + return l.loadConfig(l.parser.LoadConfigDirWithTests(rootDir, testDir)) +} + +func (l *Loader) loadConfig(rootMod *configs.Module, diags hcl.Diagnostics) (*configs.Config, hcl.Diagnostics) { if rootMod == nil || diags.HasErrors() { // Ensure we return any parsed modules here so that required_version // constraints can be verified even when encountering errors. @@ -30,12 +43,24 @@ func (l *Loader) LoadConfig(rootDir string) (*configs.Config, hcl.Diagnostics) { return cfg, diags } - cfg, cDiags := configs.BuildConfig(rootMod, configs.ModuleWalkerFunc(l.moduleWalkerLoad)) + cfg, cDiags := configs.BuildConfig(rootMod, configs.ModuleWalkerFunc(l.moduleWalkerLoad), configs.MockDataLoaderFunc(l.LoadExternalMockData)) diags = append(diags, cDiags...) return cfg, diags } +// LoadExternalMockData reads the external mock data files for the given +// provider, if they are present. +func (l *Loader) LoadExternalMockData(provider *configs.Provider) (*configs.MockData, hcl.Diagnostics) { + if len(provider.MockDataExternalSource) == 0 { + // We have no external mock data, so don't do anything. + return nil, nil + } + + // Otherwise, just hand this off to the parser to handle. + return l.parser.LoadMockDataDir(provider.MockDataExternalSource, provider.MockDataDuringPlan, provider.DeclRange) +} + // moduleWalkerLoad is a configs.ModuleWalkerFunc for loading modules that // are presumed to have already been installed. func (l *Loader) moduleWalkerLoad(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) { diff --git a/internal/configs/configload/loader_load_test.go b/internal/configs/configload/loader_load_test.go index d285cc9f16..606adf46ee 100644 --- a/internal/configs/configload/loader_load_test.go +++ b/internal/configs/configload/loader_load_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( diff --git a/internal/configs/configload/loader_snapshot.go b/internal/configs/configload/loader_snapshot.go index 915665f833..d8260cddd7 100644 --- a/internal/configs/configload/loader_snapshot.go +++ b/internal/configs/configload/loader_snapshot.go @@ -1,18 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( "fmt" "io" + "maps" "os" "path/filepath" - "sort" + "slices" "time" version "github.com/hashicorp/go-version" "github.com/hashicorp/hcl/v2" + "github.com/spf13/afero" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/modsdir" - "github.com/spf13/afero" ) // LoadConfigWithSnapshot is a variant of LoadConfig that also simultaneously @@ -28,7 +33,7 @@ func (l *Loader) LoadConfigWithSnapshot(rootDir string) (*configs.Config, *Snaps Modules: map[string]*SnapshotModule{}, } walker := l.makeModuleWalkerSnapshot(snap) - cfg, cDiags := configs.BuildConfig(rootMod, walker) + cfg, cDiags := configs.BuildConfig(rootMod, walker, configs.MockDataLoaderFunc(l.LoadExternalMockData)) diags = append(diags, cDiags...) addDiags := l.addModuleToSnapshot(snap, "", rootDir, "", nil) @@ -253,11 +258,7 @@ func (fs snapshotFS) Open(name string) (afero.File, error) { modDir := filepath.Clean(candidate.Dir) if modDir == directDir { // We've matched the module directory itself - filenames := make([]string, 0, len(candidate.Files)) - for n := range candidate.Files { - filenames = append(filenames, n) - } - sort.Strings(filenames) + filenames := slices.Sorted(maps.Keys(candidate.Files)) return &snapshotDir{ filenames: filenames, }, nil @@ -325,6 +326,10 @@ func (fs snapshotFS) Chmod(name string, mode os.FileMode) error { return fmt.Errorf("cannot set file mode inside configuration snapshot") } +func (snapshotFS) Chown(name string, uid int, gid int) error { + return fmt.Errorf("cannot set file owner inside configuration snapshot") +} + func (fs snapshotFS) Chtimes(name string, atime, mtime time.Time) error { return fmt.Errorf("cannot set file times inside configuration snapshot") } diff --git a/internal/configs/configload/loader_snapshot_test.go b/internal/configs/configload/loader_snapshot_test.go index cf1b9b26f7..be3f1ee263 100644 --- a/internal/configs/configload/loader_snapshot_test.go +++ b/internal/configs/configload/loader_snapshot_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( @@ -42,7 +45,7 @@ func TestLoadConfigWithSnapshot(t *testing.T) { problems := deep.Equal(wantModuleDirs, gotModuleDirs) for _, problem := range problems { - t.Errorf(problem) + t.Error(problem) } if len(problems) > 0 { return diff --git a/internal/configs/configload/loader_test.go b/internal/configs/configload/loader_test.go index 396a449b41..c11c279fc6 100644 --- a/internal/configs/configload/loader_test.go +++ b/internal/configs/configload/loader_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( diff --git a/internal/configs/configload/module_mgr.go b/internal/configs/configload/module_mgr.go index bf8d067e6d..238e8d7799 100644 --- a/internal/configs/configload/module_mgr.go +++ b/internal/configs/configload/module_mgr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( @@ -20,7 +23,7 @@ type moduleMgr struct { // abstraction and will always write into the "real" filesystem. CanInstall bool - // Dir is the path where descendent modules are (or will be) installed. + // Dir is the path where descendant modules are (or will be) installed. Dir string // Services is a service discovery client that will be used to find diff --git a/internal/configs/configload/testing.go b/internal/configs/configload/testing.go index 86ca9d10b7..bc90b2ef3c 100644 --- a/internal/configs/configload/testing.go +++ b/internal/configs/configload/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configload import ( @@ -17,7 +20,7 @@ import ( // In the case of any errors, t.Fatal (or similar) will be called to halt // execution of the test, so the calling test does not need to handle errors // itself. -func NewLoaderForTests(t *testing.T) (*Loader, func()) { +func NewLoaderForTests(t testing.TB) (*Loader, func()) { t.Helper() modulesDir, err := ioutil.TempDir("", "tf-configs") diff --git a/internal/configs/configschema/coerce_value.go b/internal/configs/configschema/coerce_value.go index 66804c3752..4e3263df43 100644 --- a/internal/configs/configschema/coerce_value.go +++ b/internal/configs/configschema/coerce_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -64,8 +67,10 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { val = in.GetAttr(name) case attrS.Computed || attrS.Optional: val = cty.NullVal(attrType) - default: + case attrS.Required: return cty.UnknownVal(impliedType), path.NewErrorf("attribute %q is required", name) + default: + return cty.UnknownVal(impliedType), path.NewErrorf("attribute %q has none of required, optional, or computed set", name) } val, err := convert.Convert(val, attrConvType) @@ -105,13 +110,15 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { continue } + coll, marks := coll.Unmark() + if !coll.CanIterateElements() { return cty.UnknownVal(impliedType), path.NewErrorf("must be a list") } l := coll.LengthInt() if l == 0 { - attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) + attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()).WithMarks(marks) continue } elems := make([]cty.Value, 0, l) @@ -127,7 +134,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { elems = append(elems, val) } } - attrs[typeName] = cty.ListVal(elems) + attrs[typeName] = cty.ListVal(elems).WithMarks(marks) default: attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) } @@ -145,14 +152,15 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { attrs[typeName] = cty.UnknownVal(cty.Set(blockS.ImpliedType())) continue } + coll, marks := coll.Unmark() if !coll.CanIterateElements() { - return cty.UnknownVal(impliedType), path.NewErrorf("must be a set") + return cty.UnknownVal(impliedType), path.NewErrorf("cannot iterate over %#v", coll) } l := coll.LengthInt() if l == 0 { - attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) + attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()).WithMarks(marks) continue } elems := make([]cty.Value, 0, l) @@ -168,7 +176,7 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { elems = append(elems, val) } } - attrs[typeName] = cty.SetVal(elems) + attrs[typeName] = cty.SetVal(elems).WithMarks(marks) default: attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) } @@ -186,13 +194,14 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { attrs[typeName] = cty.UnknownVal(cty.Map(blockS.ImpliedType())) continue } + coll, marks := coll.Unmark() if !coll.CanIterateElements() { return cty.UnknownVal(impliedType), path.NewErrorf("must be a map") } l := coll.LengthInt() if l == 0 { - attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) + attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()).WithMarks(marks) continue } elems := make(map[string]cty.Value) @@ -215,24 +224,14 @@ func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { // If the attribute values here contain any DynamicPseudoTypes, // the concrete type must be an object. useObject := false - switch { - case coll.Type().IsObjectType(): + if coll.Type().IsObjectType() || blockS.ImpliedType().HasDynamicTypes() { useObject = true - default: - // It's possible that we were given a map, and need to coerce it to an object - ety := coll.Type().ElementType() - for _, v := range elems { - if !v.Type().Equals(ety) { - useObject = true - break - } - } } if useObject { - attrs[typeName] = cty.ObjectVal(elems) + attrs[typeName] = cty.ObjectVal(elems).WithMarks(marks) } else { - attrs[typeName] = cty.MapVal(elems) + attrs[typeName] = cty.MapVal(elems).WithMarks(marks) } default: attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) diff --git a/internal/configs/configschema/coerce_value_test.go b/internal/configs/configschema/coerce_value_test.go index 37f81b7698..697724c6c1 100644 --- a/internal/configs/configschema/coerce_value_test.go +++ b/internal/configs/configschema/coerce_value_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -443,6 +446,20 @@ func TestCoerceValue(t *testing.T) { }), ``, }, + "omitted attribute requirements": { + &Block{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + }, + }, + }, + cty.EmptyObjectVal, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.UnknownVal(cty.String), + }), + `attribute "foo" has none of required, optional, or computed set`, + }, "dynamic value attributes": { &Block{ BlockTypes: map[string]*NestedBlock{ diff --git a/internal/configs/configschema/copy.go b/internal/configs/configschema/copy.go new file mode 100644 index 0000000000..ce4845607b --- /dev/null +++ b/internal/configs/configschema/copy.go @@ -0,0 +1,71 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configschema + +// DeepCopy returns a deep copy of the schema. +func (b *Block) DeepCopy() *Block { + block := &Block{ + Description: b.Description, + DescriptionKind: b.DescriptionKind, + Deprecated: b.Deprecated, + } + + if b.Attributes != nil { + block.Attributes = make(map[string]*Attribute, len(b.Attributes)) + } + for name, a := range b.Attributes { + block.Attributes[name] = a.DeepCopy() + } + + if b.BlockTypes != nil { + block.BlockTypes = make(map[string]*NestedBlock, len(b.BlockTypes)) + } + for name, bt := range b.BlockTypes { + inner := bt.Block.DeepCopy() + block.BlockTypes[name] = &NestedBlock{ + Block: *inner, + Nesting: bt.Nesting, + MinItems: bt.MinItems, + MaxItems: bt.MaxItems, + } + } + + return block +} + +// DeepCopy returns a deep copy of the schema. +func (a *Attribute) DeepCopy() *Attribute { + attr := &Attribute{ + Type: a.Type, + Description: a.Description, + DescriptionKind: a.DescriptionKind, + Deprecated: a.Deprecated, + Required: a.Required, + Computed: a.Computed, + Optional: a.Optional, + Sensitive: a.Sensitive, + + // NestedType is not copied here because it will be copied + // separately if it is set. + NestedType: nil, + } + if a.NestedType != nil { + attr.NestedType = a.NestedType.DeepCopy() + } + return attr +} + +// DeepCopy returns a deep copy of the schema. +func (o *Object) DeepCopy() *Object { + object := &Object{ + Nesting: o.Nesting, + } + if o.Attributes != nil { + object.Attributes = make(map[string]*Attribute, len(o.Attributes)) + for name, a := range o.Attributes { + object.Attributes[name] = a.DeepCopy() + } + } + return object +} diff --git a/internal/configs/configschema/decoder_spec.go b/internal/configs/configschema/decoder_spec.go index d2d6616dd8..5fc6e77131 100644 --- a/internal/configs/configschema/decoder_spec.go +++ b/internal/configs/configschema/decoder_spec.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -181,11 +184,11 @@ func (b *Block) DecoderSpec() hcldec.Spec { } func (a *Attribute) decoderSpec(name string) hcldec.Spec { - ret := &hcldec.AttrSpec{Name: name} - if a == nil { - return ret + if a == nil || (a.Type == cty.NilType && a.NestedType == nil) { + panic("Invalid attribute schema: schema is nil.") } + ret := &hcldec.AttrSpec{Name: name} if a.NestedType != nil { if a.Type != cty.NilType { panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.") diff --git a/internal/configs/configschema/decoder_spec_test.go b/internal/configs/configschema/decoder_spec_test.go index 12fdee7613..cd588ddc69 100644 --- a/internal/configs/configschema/decoder_spec_test.go +++ b/internal/configs/configschema/decoder_spec_test.go @@ -1,16 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( "sort" "testing" - "github.com/apparentlymart/go-dump/dump" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) @@ -407,9 +409,9 @@ func TestBlockDecoderSpec(t *testing.T) { } } - if !got.RawEquals(test.Want) { - t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) - t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want)) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Logf("implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) + t.Errorf("wrong result\n%s", diff) } // Double-check that we're producing consistent results for DecoderSpec @@ -442,18 +444,6 @@ func TestAttributeDecoderSpec(t *testing.T) { Want cty.Value DiagCount int }{ - "empty": { - &Attribute{}, - hcl.EmptyBody(), - cty.NilVal, - 0, - }, - "nil": { - nil, - hcl.EmptyBody(), - cty.NilVal, - 0, - }, "optional string (null)": { &Attribute{ Type: cty.String, @@ -861,9 +851,9 @@ func TestAttributeDecoderSpec(t *testing.T) { } } - if !got.RawEquals(test.Want) { - t.Logf("[INFO] implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) - t.Errorf("wrong result\ngot: %s\nwant: %s", dump.Value(got), dump.Value(test.Want)) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Logf("implied schema is %s", spew.Sdump(hcldec.ImpliedSchema(spec))) + t.Errorf("wrong result\n%s", diff) } }) } @@ -875,17 +865,33 @@ func TestAttributeDecoderSpec(t *testing.T) { // be removed when InternalValidate() is extended to validate Attribute specs // (and is used). See the #FIXME in decoderSpec. func TestAttributeDecoderSpec_panic(t *testing.T) { - attrS := &Attribute{ - Type: cty.Object(map[string]cty.Type{ - "nested_attribute": cty.String, - }), - NestedType: &Object{}, - Optional: true, + tests := map[string]struct { + Schema *Attribute + }{ + "empty": { + Schema: &Attribute{}, + }, + "nil": { + Schema: nil, + }, + "nested_attribute": { + Schema: &Attribute{ + Type: cty.Object(map[string]cty.Type{ + "nested_attribute": cty.String, + }), + NestedType: &Object{}, + Optional: true, + }, + }, } - defer func() { recover() }() - attrS.decoderSpec("attr") - t.Errorf("expected panic") + for name, test := range tests { + t.Run(name, func(t *testing.T) { + defer func() { recover() }() + test.Schema.decoderSpec("attr") + t.Errorf("expected panic") + }) + } } func TestListOptionalAttrsFromObject(t *testing.T) { diff --git a/internal/configs/configschema/doc.go b/internal/configs/configschema/doc.go index caf8d730c1..867b47a58c 100644 --- a/internal/configs/configschema/doc.go +++ b/internal/configs/configschema/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package configschema contains types for describing the expected structure // of a configuration block whose shape is not known until runtime. // diff --git a/internal/configs/configschema/empty_value.go b/internal/configs/configschema/empty_value.go index 89e1f612f9..72d3e2931c 100644 --- a/internal/configs/configschema/empty_value.go +++ b/internal/configs/configschema/empty_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( diff --git a/internal/configs/configschema/empty_value_test.go b/internal/configs/configschema/empty_value_test.go index a9d8e30749..8ec6cda489 100644 --- a/internal/configs/configschema/empty_value_test.go +++ b/internal/configs/configschema/empty_value_test.go @@ -1,11 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( "fmt" "testing" - "github.com/apparentlymart/go-dump/dump" "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" ) @@ -162,8 +166,8 @@ func TestBlockEmptyValue(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) { got := test.Schema.EmptyValue() - if !test.Want.RawEquals(got) { - t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), dump.Value(got), dump.Value(test.Want)) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\nschema: %s\ndiff:\n%s", spew.Sdump(test.Schema), diff) } }) } @@ -249,8 +253,8 @@ func TestAttributeEmptyValue(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%#v", test.Schema), func(t *testing.T) { got := test.Schema.EmptyValue() - if !test.Want.RawEquals(got) { - t.Errorf("wrong result\nschema: %s\ngot: %s\nwant: %s", spew.Sdump(test.Schema), dump.Value(got), dump.Value(test.Want)) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\nschema: %s\ndiff:\n%s", spew.Sdump(test.Schema), diff) } }) } diff --git a/internal/configs/configschema/filter.go b/internal/configs/configschema/filter.go new file mode 100644 index 0000000000..3d05558f49 --- /dev/null +++ b/internal/configs/configschema/filter.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configschema + +import "github.com/zclconf/go-cty/cty" + +type FilterT[T any] func(cty.Path, T) bool + +var ( + FilterReadOnlyAttribute = func(path cty.Path, attribute *Attribute) bool { + return attribute.Computed && !attribute.Optional + } + + FilterHelperSchemaIdAttribute = func(path cty.Path, attribute *Attribute) bool { + if path.Equals(cty.GetAttrPath("id")) && attribute.Computed && attribute.Optional { + return true + } + return false + } + + FilterDeprecatedAttribute = func(path cty.Path, attribute *Attribute) bool { + return attribute.Deprecated + } + + FilterDeprecatedBlock = func(path cty.Path, block *NestedBlock) bool { + return block.Deprecated + } +) + +func FilterOr[T any](filters ...FilterT[T]) FilterT[T] { + return func(path cty.Path, value T) bool { + for _, f := range filters { + if f(path, value) { + return true + } + } + return false + } +} + +func (b *Block) Filter(filterAttribute FilterT[*Attribute], filterBlock FilterT[*NestedBlock]) *Block { + return b.filter(nil, filterAttribute, filterBlock) +} + +func (b *Block) filter(path cty.Path, filterAttribute FilterT[*Attribute], filterBlock FilterT[*NestedBlock]) *Block { + ret := &Block{ + Description: b.Description, + DescriptionKind: b.DescriptionKind, + Deprecated: b.Deprecated, + } + + if b.Attributes != nil { + ret.Attributes = make(map[string]*Attribute, len(b.Attributes)) + } + for name, attrS := range b.Attributes { + path := path.GetAttr(name) + if filterAttribute == nil || !filterAttribute(path, attrS) { + attr := *attrS + if attrS.NestedType != nil { + attr.NestedType = filterNestedType(attrS.NestedType, path, filterAttribute) + } + ret.Attributes[name] = &attr + } + } + + if b.BlockTypes != nil { + ret.BlockTypes = make(map[string]*NestedBlock, len(b.BlockTypes)) + } + for name, blockS := range b.BlockTypes { + path := path.GetAttr(name) + if filterBlock == nil || !filterBlock(path, blockS) { + block := blockS.filter(path, filterAttribute, filterBlock) + ret.BlockTypes[name] = &NestedBlock{ + Block: *block, + Nesting: blockS.Nesting, + MinItems: blockS.MinItems, + MaxItems: blockS.MaxItems, + } + } + } + + return ret +} + +func filterNestedType(obj *Object, path cty.Path, filterAttribute FilterT[*Attribute]) *Object { + if obj == nil { + return nil + } + + ret := &Object{ + Attributes: map[string]*Attribute{}, + Nesting: obj.Nesting, + } + + for name, attrS := range obj.Attributes { + path := path.GetAttr(name) + if filterAttribute == nil || !filterAttribute(path, attrS) { + attr := *attrS + if attrS.NestedType != nil { + attr.NestedType = filterNestedType(attrS.NestedType, path, filterAttribute) + } + ret.Attributes[name] = &attr + } + } + + return ret +} diff --git a/internal/configs/configschema/filter_test.go b/internal/configs/configschema/filter_test.go new file mode 100644 index 0000000000..979204cbfa --- /dev/null +++ b/internal/configs/configschema/filter_test.go @@ -0,0 +1,294 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configschema + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +func TestFilter(t *testing.T) { + testCases := map[string]struct { + schema *Block + filterAttribute FilterT[*Attribute] + filterBlock FilterT[*NestedBlock] + want *Block + }{ + "empty": { + schema: &Block{}, + filterAttribute: FilterDeprecatedAttribute, + filterBlock: FilterDeprecatedBlock, + want: &Block{}, + }, + "noop": { + schema: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Required: true, + }, + }, + BlockTypes: map[string]*NestedBlock{ + "list": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + filterAttribute: nil, + filterBlock: nil, + want: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Required: true, + }, + }, + BlockTypes: map[string]*NestedBlock{ + "list": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + "filter_deprecated": { + schema: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "deprecated_string": { + Type: cty.String, + Deprecated: true, + }, + "nested": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + }, + "deprecated_string": { + Type: cty.String, + Deprecated: true, + }, + }, + Nesting: NestingList, + }, + }, + }, + + BlockTypes: map[string]*NestedBlock{ + "list": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + }, + Deprecated: true, + }, + }, + }, + }, + filterAttribute: FilterDeprecatedAttribute, + filterBlock: FilterDeprecatedBlock, + want: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "nested": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + }, + }, + Nesting: NestingList, + }, + }, + }, + }, + }, + "filter_read_only": { + schema: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "read_only_string": { + Type: cty.String, + Computed: true, + }, + "nested": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "read_only_string": { + Type: cty.String, + Computed: true, + }, + "deeply_nested": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "number": { + Type: cty.Number, + Required: true, + }, + "read_only_number": { + Type: cty.Number, + Computed: true, + }, + }, + Nesting: NestingList, + }, + }, + }, + Nesting: NestingList, + }, + }, + "missing_attributes": { + NestedType: &Object{ + Nesting: NestingList, + }, + Computed: true, + }, + }, + + BlockTypes: map[string]*NestedBlock{ + "list": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "read_only_string": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + filterAttribute: FilterReadOnlyAttribute, + filterBlock: nil, + want: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "nested": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + "deeply_nested": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "number": { + Type: cty.Number, + Required: true, + }, + }, + Nesting: NestingList, + }, + }, + }, + Nesting: NestingList, + }, + }, + }, + BlockTypes: map[string]*NestedBlock{ + "list": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + "filter_optional_computed_id": { + schema: &Block{ + Attributes: map[string]*Attribute{ + "id": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "string": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + filterAttribute: FilterHelperSchemaIdAttribute, + filterBlock: nil, + want: &Block{ + Attributes: map[string]*Attribute{ + "string": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + original := tc.schema.DeepCopy() + + got := tc.schema.Filter(tc.filterAttribute, tc.filterBlock) + if !cmp.Equal(got, tc.want, cmp.Comparer(cty.Type.Equals), cmpopts.EquateEmpty()) { + t.Error(cmp.Diff(got, tc.want, cmp.Comparer(cty.Type.Equals), cmpopts.EquateEmpty())) + } + + // We shouldn't have edited the original schema. + if !cmp.Equal(tc.schema, original, cmp.Comparer(cty.Type.Equals), cmpopts.EquateEmpty()) { + t.Errorf("the original schema was edited when it shouldn't have been: %s", cmp.Diff(tc.schema, original, cmp.Comparer(cty.Type.Equals), cmpopts.EquateEmpty())) + } + }) + } +} diff --git a/internal/configs/configschema/implied_type.go b/internal/configs/configschema/implied_type.go index 9de5073f35..c94c6591ff 100644 --- a/internal/configs/configschema/implied_type.go +++ b/internal/configs/configschema/implied_type.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -33,7 +36,7 @@ func (b *Block) specType() cty.Type { } // ContainsSensitive returns true if any of the attributes of the receiving -// block or any of its descendent blocks are marked as sensitive. +// block or any of its descendant blocks are marked as sensitive. // // Blocks themselves cannot be sensitive as a whole -- sensitivity is a // per-attribute idea -- but sometimes we want to include a whole object @@ -56,6 +59,29 @@ func (b *Block) ContainsSensitive() bool { return false } +// ContainsWriteOnly returns true if any of the attributes of the receiving +// block or any of its descendant blocks are considered write only +// based on the declarations in the schema. +// +// Blocks themselves cannot be write only as a whole -- write only is a +// per-attribute idea. +func (b *Block) ContainsWriteOnly() bool { + for _, attrS := range b.Attributes { + if attrS.WriteOnly { + return true + } + if attrS.NestedType != nil && attrS.NestedType.ContainsWriteOnly() { + return true + } + } + for _, blockS := range b.BlockTypes { + if blockS.ContainsWriteOnly() { + return true + } + } + return false +} + // ImpliedType returns the cty.Type that would result from decoding a Block's // ImpliedType and getting the resulting AttributeType. // @@ -81,6 +107,14 @@ func (o *Object) ImpliedType() cty.Type { return o.specType().WithoutOptionalAttributesDeep() } +// ConfigType returns a cty.Type that can be used to decode a configuration +// object using the receiving block schema. +// +// ConfigType will preserve optional attributes +func (o *Object) ConfigType() cty.Type { + return o.specType() +} + // specType returns the cty.Type used for decoding a NestedType Attribute using // the receiving block schema. func (o *Object) specType() cty.Type { @@ -131,3 +165,17 @@ func (o *Object) ContainsSensitive() bool { } return false } + +// ContainsWriteOnly returns true if any of the attributes of the receiving +// Object are considered write only based on the declarations in the schema. +func (o *Object) ContainsWriteOnly() bool { + for _, attrS := range o.Attributes { + if attrS.WriteOnly { + return true + } + if attrS.NestedType != nil && attrS.NestedType.ContainsWriteOnly() { + return true + } + } + return false +} diff --git a/internal/configs/configschema/implied_type_test.go b/internal/configs/configschema/implied_type_test.go index 6e3c0de722..7dbb611d38 100644 --- a/internal/configs/configschema/implied_type_test.go +++ b/internal/configs/configschema/implied_type_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -215,6 +218,69 @@ func TestBlockContainsSensitive(t *testing.T) { } }) } +} + +func TestBlockContainsWriteOnly(t *testing.T) { + tests := map[string]struct { + Schema *Block + Want bool + }{ + "object contains write only": { + &Block{ + Attributes: map[string]*Attribute{ + "wo": {WriteOnly: true}, + }, + }, + true, + }, + "no write only attrs": { + &Block{ + Attributes: map[string]*Attribute{ + "not_wo": {}, + }, + }, + false, + }, + "nested object contains write only": { + &Block{ + Attributes: map[string]*Attribute{ + "nested": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "wo": {WriteOnly: true}, + }, + }, + }, + }, + }, + true, + }, + "nested obj, no write only attrs": { + &Block{ + Attributes: map[string]*Attribute{ + "nested": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "public": {}, + }, + }, + }, + }, + }, + false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := test.Schema.ContainsWriteOnly() + if got != test.Want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } } @@ -461,6 +527,101 @@ func TestObjectContainsSensitive(t *testing.T) { } +func TestObjectContainsWriteOnly(t *testing.T) { + tests := map[string]struct { + Schema *Object + Want bool + }{ + "object contains write only": { + &Object{ + Attributes: map[string]*Attribute{ + "wo": {WriteOnly: true}, + }, + }, + true, + }, + "no write only attrs": { + &Object{ + Attributes: map[string]*Attribute{ + "not_wo": {}, + }, + }, + false, + }, + "nested object contains write only": { + &Object{ + Attributes: map[string]*Attribute{ + "nested": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "wo": {WriteOnly: true}, + }, + }, + }, + }, + }, + true, + }, + "nested obj, no write only attrs": { + &Object{ + Attributes: map[string]*Attribute{ + "nested": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "public": {}, + }, + }, + }, + }, + }, + false, + }, + "several nested objects, one contains write only": { + &Object{ + Attributes: map[string]*Attribute{ + "alpha": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "not_wo": {}, + }, + }, + }, + "beta": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "wo": {WriteOnly: true}, + }, + }, + }, + "gamma": { + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "not_wo": {}, + }, + }, + }, + }, + }, + true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := test.Schema.ContainsWriteOnly() + if got != test.Want { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } + +} + // Nested attribute should return optional object attributes for decoding. func TestObjectSpecType(t *testing.T) { tests := map[string]struct { diff --git a/internal/configs/configschema/internal_validate.go b/internal/configs/configschema/internal_validate.go index b113adfecc..d2343c74b0 100644 --- a/internal/configs/configschema/internal_validate.go +++ b/internal/configs/configschema/internal_validate.go @@ -1,12 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( + "errors" "fmt" "regexp" "github.com/zclconf/go-cty/cty" - - multierror "github.com/hashicorp/go-multierror" ) var validName = regexp.MustCompile(`^[a-z0-9_]+$`) @@ -25,47 +27,47 @@ func (b *Block) InternalValidate() error { } func (b *Block) internalValidate(prefix string) error { - var multiErr *multierror.Error + var multiErr error for name, attrS := range b.Attributes { if attrS == nil { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: attribute schema is nil", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: attribute schema is nil", prefix, name)) continue } - multiErr = multierror.Append(multiErr, attrS.internalValidate(name, prefix)) + multiErr = errors.Join(multiErr, attrS.internalValidate(name, prefix)) } for name, blockS := range b.BlockTypes { if blockS == nil { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: block schema is nil", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: block schema is nil", prefix, name)) continue } if _, isAttr := b.Attributes[name]; isAttr { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: name defined as both attribute and child block type", prefix, name)) } else if !validName.MatchString(name) { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name)) } if blockS.MinItems < 0 || blockS.MaxItems < 0 { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be greater than zero", prefix, name)) } switch blockS.Nesting { case NestingSingle: switch { case blockS.MinItems != blockS.MaxItems: - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must match in NestingSingle mode", prefix, name)) case blockS.MinItems < 0 || blockS.MinItems > 1: - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must be set to either 0 or 1 in NestingSingle mode", prefix, name)) } case NestingGroup: if blockS.MinItems != 0 || blockS.MaxItems != 0 { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems cannot be used in NestingGroup mode", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems cannot be used in NestingGroup mode", prefix, name)) } case NestingList, NestingSet: if blockS.MinItems > blockS.MaxItems && blockS.MaxItems != 0 { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: MinItems must be less than or equal to MaxItems in %s mode", prefix, name, blockS.Nesting)) } if blockS.Nesting == NestingSet { ety := blockS.Block.ImpliedType() @@ -73,22 +75,28 @@ func (b *Block) internalValidate(prefix string) error { // This is not permitted because the HCL (cty) set implementation // needs to know the exact type of set elements in order to // properly hash them, and so can't support mixed types. - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name)) + } + if blockS.Block.ContainsWriteOnly() { + // This is not permitted because any marks within sets will + // be hoisted up the outer set value, so only the set itself + // can be WriteOnly. + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: NestingSet blocks may not contain WriteOnly attributes", prefix, name)) } } case NestingMap: if blockS.MinItems != 0 || blockS.MaxItems != 0 { - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: MinItems and MaxItems must both be 0 in NestingMap mode", prefix, name)) } default: - multiErr = multierror.Append(multiErr, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting)) + multiErr = errors.Join(multiErr, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, blockS.Nesting)) } subPrefix := prefix + name + "." - multiErr = multierror.Append(multiErr, blockS.Block.internalValidate(subPrefix)) + multiErr = errors.Join(multiErr, blockS.Block.internalValidate(subPrefix)) } - return multiErr.ErrorOrNil() + return multiErr } // InternalValidate returns an error if the receiving attribute and its child @@ -102,30 +110,30 @@ func (a *Attribute) InternalValidate(name string) error { } func (a *Attribute) internalValidate(name, prefix string) error { - var err *multierror.Error + var err error /* FIXME: this validation breaks certain existing providers and cannot be enforced without coordination. if !validName.MatchString(name) { - err = multierror.Append(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: name may contain only lowercase letters, digits and underscores", prefix, name)) } */ if !a.Optional && !a.Required && !a.Computed { - err = multierror.Append(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: must set Optional, Required or Computed", prefix, name)) } if a.Optional && a.Required { - err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: cannot set both Optional and Required", prefix, name)) } if a.Computed && a.Required { - err = multierror.Append(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: cannot set both Computed and Required", prefix, name)) } if a.Type == cty.NilType && a.NestedType == nil { - err = multierror.Append(err, fmt.Errorf("%s%s: either Type or NestedType must be defined", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: either Type or NestedType must be defined", prefix, name)) } if a.Type != cty.NilType { if a.NestedType != nil { - err = multierror.Append(fmt.Errorf("%s: Type and NestedType cannot both be set", name)) + err = errors.Join(fmt.Errorf("%s: Type and NestedType cannot both be set", name)) } } @@ -140,20 +148,40 @@ func (a *Attribute) internalValidate(name, prefix string) error { // This is not permitted because the HCL (cty) set implementation // needs to know the exact type of set elements in order to // properly hash them, and so can't support mixed types. - err = multierror.Append(err, fmt.Errorf("%s%s: NestingSet blocks may not contain attributes of cty.DynamicPseudoType", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: NestingSet attributes may not contain attributes of cty.DynamicPseudoType", prefix, name)) + } + if a.NestedType.ContainsWriteOnly() { + // This is not permitted because any marks within sets will + // be hoisted up the outer set value, so only the set itself + // can be WriteOnly. + err = errors.Join(err, fmt.Errorf("%s%s: NestingSet attributes may not contain WriteOnly attributes", prefix, name)) } } default: - err = multierror.Append(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, a.NestedType.Nesting)) + err = errors.Join(err, fmt.Errorf("%s%s: invalid nesting mode %s", prefix, name, a.NestedType.Nesting)) } for name, attrS := range a.NestedType.Attributes { if attrS == nil { - err = multierror.Append(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name)) + err = errors.Join(err, fmt.Errorf("%s%s: attribute schema is nil", prefix, name)) continue } - err = multierror.Append(err, attrS.internalValidate(name, prefix)) + err = errors.Join(err, attrS.internalValidate(name, prefix)) } } - return err.ErrorOrNil() + return err +} + +func (o *Object) InternalValidate() error { + var err error + + for name, attrS := range o.Attributes { + if attrS == nil { + err = errors.Join(err, fmt.Errorf("%s: attribute schema is nil", name)) + continue + } + err = errors.Join(err, attrS.internalValidate(name, "")) + } + + return err } diff --git a/internal/configs/configschema/internal_validate_test.go b/internal/configs/configschema/internal_validate_test.go index 3be461d448..f0be0cebc1 100644 --- a/internal/configs/configschema/internal_validate_test.go +++ b/internal/configs/configschema/internal_validate_test.go @@ -1,11 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( "testing" "github.com/zclconf/go-cty/cty" - - multierror "github.com/hashicorp/go-multierror" ) func TestBlockInternalValidate(t *testing.T) { @@ -264,11 +265,50 @@ func TestBlockInternalValidate(t *testing.T) { }, []string{"bad: block schema is nil"}, }, + "nested set block with write-only attribute": { + &Block{ + BlockTypes: map[string]*NestedBlock{ + "bad": { + Nesting: NestingSet, + Block: Block{ + Attributes: map[string]*Attribute{ + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + []string{"bad: NestingSet blocks may not contain WriteOnly attributes"}, + }, + "nested set attributes with write-only attribute": { + &Block{ + Attributes: map[string]*Attribute{ + "bad": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + Nesting: NestingSet, + }, + Optional: true, + }, + }, + }, + []string{"bad: NestingSet attributes may not contain WriteOnly attributes"}, + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { - errs := multierrorErrors(test.Block.InternalValidate()) + errs := joinedErrors(test.Block.InternalValidate()) if got, want := len(errs), len(test.Errs); got != want { t.Errorf("wrong number of errors %d; want %d", got, want) for _, err := range errs { @@ -287,16 +327,111 @@ func TestBlockInternalValidate(t *testing.T) { } } -func multierrorErrors(err error) []error { - // A function like this should really be part of the multierror package... +func TestObjectInternalValidate(t *testing.T) { + tests := map[string]struct { + Object *Object + Errs []string + }{ + "empty": { + &Object{}, + []string{}, + }, + "valid": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + Required: true, + }, + "bar": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: NestingSingle, + }, + []string{}, + }, + "nil": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": nil, + }, + Nesting: NestingSingle, + }, + []string{"foo: attribute schema is nil"}, + }, + "attribute with no flags set": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + }, + }, + Nesting: NestingSingle, + }, + []string{"foo: must set Optional, Required or Computed"}, + }, + "attribute required and optional": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Type: cty.String, + Required: true, + Optional: true, + }, + }, + Nesting: NestingSingle, + }, + []string{"foo: cannot set both Optional and Required"}, + }, + "attribute with missing type": { + &Object{ + Attributes: map[string]*Attribute{ + "foo": { + Optional: true, + }, + }, + Nesting: NestingSingle, + }, + []string{"foo: either Type or NestedType must be defined"}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + errs := joinedErrors(test.Object.InternalValidate()) + if got, want := len(errs), len(test.Errs); got != want { + t.Errorf("wrong number of errors %d; want %d", got, want) + for _, err := range errs { + t.Logf("- %s", err.Error()) + } + } else { + if len(errs) > 0 { + for i := range errs { + if errs[i].Error() != test.Errs[i] { + t.Errorf("wrong error: got %s, want %s", errs[i].Error(), test.Errs[i]) + } + } + } + } + }) + } +} + +func joinedErrors(err error) []error { if err == nil { return nil } + // This interface is implemented by the result of errors.Join when + // multiple errors are present. + type Unwrapper interface { + Unwrap() []error + } switch terr := err.(type) { - case *multierror.Error: - return terr.Errors + case Unwrapper: + return terr.Unwrap() default: return []error{err} } diff --git a/internal/configs/configschema/marks.go b/internal/configs/configschema/marks.go index c581b187b9..263913e128 100644 --- a/internal/configs/configschema/marks.go +++ b/internal/configs/configschema/marks.go @@ -1,38 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( "fmt" + "slices" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) -// ValueMarks returns a set of path value marks for a given value and path, -// based on the sensitive flag for each attribute within the schema. Nested -// blocks are descended (if present in the given value). -func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { - var pvm []cty.PathValueMarks +// WARNING: SensitivePaths must exactly mirror the WriteOnlyPaths method, since +// they both use the same process just for different attribute types. Any fixes +// here must be made in WriteOnlyPaths, and vice versa. + +// SensitivePaths returns a set of paths into the given value that should +// be marked as sensitive based on the static declarations in the schema. +func (b *Block) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path { + var ret []cty.Path + + // A block as a whole cannot be sensitive, so nothing to return + if val.IsNull() || !val.IsKnown() { + return ret + } - // We can mark attributes as sensitive even if the value is null for name, attrS := range b.Attributes { if attrS.Sensitive { - // Create a copy of the path, with this step added, to add to our PathValueMarks slice - attrPath := make(cty.Path, len(path), len(path)+1) - copy(attrPath, path) - attrPath = append(path, cty.GetAttrStep{Name: name}) - pvm = append(pvm, cty.PathValueMarks{ - Path: attrPath, - Marks: cty.NewValueMarks(marks.Sensitive), - }) + attrPath := slices.Concat(basePath, cty.GetAttrPath(name)) + ret = append(ret, attrPath) } } - // If the value is null, no other marks are possible - if val.IsNull() { - return pvm - } - - // Extract marks for nested attribute type values + // Extract paths for marks from nested attribute type values for name, attrS := range b.Attributes { // If the attribute has no nested type, or the nested type doesn't // contain any sensitive attributes, skip inspecting it @@ -41,14 +40,11 @@ func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { } // Create a copy of the path, with this step added, to add to our PathValueMarks slice - attrPath := make(cty.Path, len(path), len(path)+1) - copy(attrPath, path) - attrPath = append(path, cty.GetAttrStep{Name: name}) - - pvm = append(pvm, attrS.NestedType.ValueMarks(val.GetAttr(name), attrPath)...) + attrPath := slices.Concat(basePath, cty.GetAttrPath(name)) + ret = append(ret, attrS.NestedType.SensitivePaths(val.GetAttr(name), attrPath)...) } - // Extract marks for nested blocks + // Extract paths for marks from nested blocks for name, blockS := range b.BlockTypes { // If our block doesn't contain any sensitive attributes, skip inspecting it if !blockS.Block.ContainsSensitive() { @@ -61,34 +57,35 @@ func (b *Block) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { } // Create a copy of the path, with this step added, to add to our PathValueMarks slice - blockPath := make(cty.Path, len(path), len(path)+1) - copy(blockPath, path) - blockPath = append(path, cty.GetAttrStep{Name: name}) + blockPath := slices.Concat(basePath, cty.GetAttrPath(name)) switch blockS.Nesting { case NestingSingle, NestingGroup: - pvm = append(pvm, blockS.Block.ValueMarks(blockV, blockPath)...) + ret = append(ret, blockS.Block.SensitivePaths(blockV, blockPath)...) case NestingList, NestingMap, NestingSet: + blockV, _ = blockV.Unmark() // peel off one level of marking so we can iterate for it := blockV.ElementIterator(); it.Next(); { idx, blockEV := it.Element() - morePaths := blockS.Block.ValueMarks(blockEV, append(blockPath, cty.IndexStep{Key: idx})) - pvm = append(pvm, morePaths...) + // Create a copy of the path, with this block instance's index + // step added, to add to our PathValueMarks slice + blockInstancePath := slices.Concat(blockPath, cty.IndexPath(idx)) + morePaths := blockS.Block.SensitivePaths(blockEV, blockInstancePath) + ret = append(ret, morePaths...) } default: panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) } } - return pvm + return ret } -// ValueMarks returns a set of path value marks for a given value and path, -// based on the sensitive flag for each attribute within the nested attribute. -// Attributes with nested types are descended (if present in the given value). -func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { - var pvm []cty.PathValueMarks +// SensitivePaths returns a set of paths into the given value that should be +// marked as sensitive based on the static declarations in the schema. +func (o *Object) SensitivePaths(val cty.Value, basePath cty.Path) []cty.Path { + var ret []cty.Path if val.IsNull() || !val.IsKnown() { - return pvm + return ret } for name, attrS := range o.Attributes { @@ -100,24 +97,20 @@ func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { switch o.Nesting { case NestingSingle, NestingGroup: // Create a path to this attribute - attrPath := make(cty.Path, len(path), len(path)+1) - copy(attrPath, path) - attrPath = append(path, cty.GetAttrStep{Name: name}) + attrPath := slices.Concat(basePath, cty.GetAttrPath(name)) if attrS.Sensitive { // If the entire attribute is sensitive, mark it so - pvm = append(pvm, cty.PathValueMarks{ - Path: attrPath, - Marks: cty.NewValueMarks(marks.Sensitive), - }) + ret = append(ret, attrPath) } else { // The attribute has a nested type which contains sensitive // attributes, so recurse - pvm = append(pvm, attrS.NestedType.ValueMarks(val.GetAttr(name), attrPath)...) + ret = append(ret, attrS.NestedType.SensitivePaths(val.GetAttr(name), attrPath)...) } case NestingList, NestingMap, NestingSet: // For nested attribute types which have a non-single nesting mode, // we add path value marks for each element of the collection + val, _ = val.Unmark() // peel off one level of marking so we can iterate for it := val.ElementIterator(); it.Next(); { idx, attrEV := it.Element() attrV := attrEV.GetAttr(name) @@ -127,25 +120,18 @@ func (o *Object) ValueMarks(val cty.Value, path cty.Path) []cty.PathValueMarks { // of the loops: index into the collection, then the contained // attribute name. This is because we have one type // representing multiple collection elements. - attrPath := make(cty.Path, len(path), len(path)+2) - copy(attrPath, path) - attrPath = append(path, cty.IndexStep{Key: idx}, cty.GetAttrStep{Name: name}) + attrPath := slices.Concat(basePath, cty.IndexPath(idx).GetAttr(name)) if attrS.Sensitive { // If the entire attribute is sensitive, mark it so - pvm = append(pvm, cty.PathValueMarks{ - Path: attrPath, - Marks: cty.NewValueMarks(marks.Sensitive), - }) + ret = append(ret, attrPath) } else { - // The attribute has a nested type which contains sensitive - // attributes, so recurse - pvm = append(pvm, attrS.NestedType.ValueMarks(attrV, attrPath)...) + ret = append(ret, attrS.NestedType.SensitivePaths(attrV, attrPath)...) } } default: panic(fmt.Sprintf("unsupported nesting mode %s", attrS.NestedType.Nesting)) } } - return pvm + return ret } diff --git a/internal/configs/configschema/marks_test.go b/internal/configs/configschema/marks_test.go index 2077e5e805..780be9f85c 100644 --- a/internal/configs/configschema/marks_test.go +++ b/internal/configs/configschema/marks_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -32,6 +35,18 @@ func TestBlockValueMarks(t *testing.T) { Nesting: NestingList, }, }, + "nested_sensitive": { + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "boop": { + Type: cty.String, + }, + }, + Nesting: NestingList, + }, + Sensitive: true, + Optional: true, + }, }, BlockTypes: map[string]*NestedBlock{ @@ -73,6 +88,9 @@ func TestBlockValueMarks(t *testing.T) { "boop": cty.String, "honk": cty.String, }))), + "nested_sensitive": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + }))), "list": cty.UnknownVal(schema.BlockTypes["list"].ImpliedType()), }), cty.ObjectVal(map[string]cty.Value{ @@ -82,6 +100,9 @@ func TestBlockValueMarks(t *testing.T) { "boop": cty.String, "honk": cty.String, }))), + "nested_sensitive": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + }))).Mark(marks.Sensitive), "list": cty.UnknownVal(schema.BlockTypes["list"].ImpliedType()), }), }, @@ -93,6 +114,10 @@ func TestBlockValueMarks(t *testing.T) { "boop": cty.String, "honk": cty.String, }))), + "nested_sensitive": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + "honk": cty.String, + }))), "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "sensitive": cty.UnknownVal(cty.String), @@ -111,6 +136,9 @@ func TestBlockValueMarks(t *testing.T) { "boop": cty.String, "honk": cty.String, }))), + "nested_sensitive": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + }))).Mark(marks.Sensitive), "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "sensitive": cty.UnknownVal(cty.String).Mark(marks.Sensitive), @@ -141,6 +169,9 @@ func TestBlockValueMarks(t *testing.T) { "honk": cty.UnknownVal(cty.String), }), }), + "nested_sensitive": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + }))), "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ "sensitive": cty.String, "unsensitive": cty.String, @@ -163,6 +194,9 @@ func TestBlockValueMarks(t *testing.T) { "honk": cty.UnknownVal(cty.String).Mark(marks.Sensitive), }), }), + "nested_sensitive": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + }))).Mark(marks.Sensitive), "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ "sensitive": cty.String, "unsensitive": cty.String, @@ -173,9 +207,19 @@ func TestBlockValueMarks(t *testing.T) { for name, tc := range testCases { t.Run(name, func(t *testing.T) { - got := tc.given.MarkWithPaths(schema.ValueMarks(tc.given, nil)) - if !got.RawEquals(tc.expect) { - t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expect, got) + given, err := schema.CoerceValue(tc.given) + if err != nil { + t.Fatal(err) + } + expect, err := schema.CoerceValue(tc.expect) + if err != nil { + t.Fatal(err) + } + + sensitivePaths := schema.SensitivePaths(given, nil) + got := marks.MarkPaths(given, marks.Sensitive, sensitivePaths) + if !expect.RawEquals(got) { + t.Fatalf("\nexpected: %#v\ngot: %#v\n", expect, got) } }) } diff --git a/internal/configs/configschema/none_required.go b/internal/configs/configschema/none_required.go index 0be3b8fa35..de2cda6c5e 100644 --- a/internal/configs/configschema/none_required.go +++ b/internal/configs/configschema/none_required.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema // NoneRequired returns a deep copy of the receiver with any required diff --git a/internal/configs/configschema/path.go b/internal/configs/configschema/path.go index d3584d3a2f..2760eecbd8 100644 --- a/internal/configs/configschema/path.go +++ b/internal/configs/configschema/path.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( diff --git a/internal/configs/configschema/path_test.go b/internal/configs/configschema/path_test.go index 1ca7cb41a8..bfd887f1bd 100644 --- a/internal/configs/configschema/path_test.go +++ b/internal/configs/configschema/path_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -29,6 +32,24 @@ func TestAttributeByPath(t *testing.T) { }, }, }, + "a4": { + Description: "a3", + NestedType: &Object{ + Nesting: NestingMap, + Attributes: map[string]*Attribute{ + "nt1": {Description: "nt1"}, + "nt2": { + Description: "nt2", + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "deeply_nested": {Description: "deeply_nested"}, + }, + }, + }, + }, + }, + }, }, BlockTypes: map[string]*NestedBlock{ "b1": { @@ -94,6 +115,11 @@ func TestAttributeByPath(t *testing.T) { "missing", false, }, + { + cty.GetAttrPath("a3").IndexInt(1).GetAttr("nt2").GetAttr("deeply_nested"), + "deeply_nested", + true, + }, { cty.GetAttrPath("b1"), "block", @@ -130,6 +156,11 @@ func TestAttributeByPath(t *testing.T) { "a9", true, }, + { + cty.GetAttrPath("a4").IndexString("test").GetAttr("nt2").GetAttr("deeply_nested"), + "deeply_nested", + true, + }, } { t.Run(tc.attrDescription, func(t *testing.T) { attr := schema.AttributeByPath(tc.path) diff --git a/internal/configs/configschema/schema.go b/internal/configs/configschema/schema.go index 9ecc71e54d..0e5d387c30 100644 --- a/internal/configs/configschema/schema.go +++ b/internal/configs/configschema/schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( @@ -75,6 +78,10 @@ type Attribute struct { Sensitive bool Deprecated bool + + // WriteOnly, if set to true, indicates that the attribute is not presisted + // in the state. + WriteOnly bool } // Object represents the embedding of a structural object inside an Attribute. @@ -117,7 +124,7 @@ type NestingMode int // Object represents the embedding of a NestedBl -//go:generate go run golang.org/x/tools/cmd/stringer -type=NestingMode +//go:generate go tool golang.org/x/tools/cmd/stringer -type=NestingMode const ( nestingModeInvalid NestingMode = iota diff --git a/internal/configs/configschema/validate_traversal.go b/internal/configs/configschema/validate_traversal.go index 8320c9de36..9178768a91 100644 --- a/internal/configs/configschema/validate_traversal.go +++ b/internal/configs/configschema/validate_traversal.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( diff --git a/internal/configs/configschema/validate_traversal_test.go b/internal/configs/configschema/validate_traversal_test.go index f399148811..80107ab884 100644 --- a/internal/configs/configschema/validate_traversal_test.go +++ b/internal/configs/configschema/validate_traversal_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configschema import ( diff --git a/internal/configs/configschema/write_only.go b/internal/configs/configschema/write_only.go new file mode 100644 index 0000000000..b9e89c92ad --- /dev/null +++ b/internal/configs/configschema/write_only.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configschema + +import ( + "fmt" + "slices" + + "github.com/zclconf/go-cty/cty" +) + +// WARNING: WriteOnlyPaths must exactly mirror the SensitivePaths method, since +// they both use the same process just for different attribute types. Any fixes +// here must be made in SensitivePaths, and vice versa. + +// WriteOnlyPaths returns a set of paths into the given value that should +// be marked as write-only based on the static declarations in the schema. +func (b *Block) WriteOnlyPaths(val cty.Value, basePath cty.Path) []cty.Path { + var ret []cty.Path + + // a block cannot be write-only, so nothing to return + if val.IsNull() || !val.IsKnown() { + return ret + } + + for name, attrS := range b.Attributes { + if attrS.WriteOnly { + attrPath := slices.Concat(basePath, cty.GetAttrPath(name)) + ret = append(ret, attrPath) + } + } + + // Extract paths for marks from nested attribute type values + for name, attrS := range b.Attributes { + // If the attribute has no nested type, or the nested type doesn't + // contain any write-only attributes, skip inspecting it + if attrS.NestedType == nil || !attrS.NestedType.ContainsWriteOnly() { + continue + } + + // Create a copy of the path, with this step added, to add to our PathValueMarks slice + attrPath := slices.Concat(basePath, cty.GetAttrPath(name)) + ret = append(ret, attrS.NestedType.writeOnlyPaths(val.GetAttr(name), attrPath)...) + } + + // Extract paths for marks from nested blocks + for name, blockS := range b.BlockTypes { + // If our block doesn't contain any write-only attributes, skip inspecting it + if !blockS.Block.ContainsWriteOnly() { + continue + } + + blockV := val.GetAttr(name) + if blockV.IsNull() || !blockV.IsKnown() { + continue + } + + // Create a copy of the path, with this step added, to add to our PathValueMarks slice + blockPath := slices.Concat(basePath, cty.GetAttrPath(name)) + + switch blockS.Nesting { + case NestingSingle, NestingGroup: + ret = append(ret, blockS.Block.WriteOnlyPaths(blockV, blockPath)...) + case NestingList, NestingMap, NestingSet: + blockV, _ = blockV.Unmark() // peel off one level of marking so we can iterate + for it := blockV.ElementIterator(); it.Next(); { + idx, blockEV := it.Element() + // Create a copy of the path, with this block instance's index + // step added, to add to our PathValueMarks slice + blockInstancePath := slices.Concat(blockPath, cty.IndexPath(idx)) + morePaths := blockS.Block.WriteOnlyPaths(blockEV, blockInstancePath) + ret = append(ret, morePaths...) + } + default: + panic(fmt.Sprintf("unsupported nesting mode %s", blockS.Nesting)) + } + } + return ret +} + +// writeOnlyPaths returns a set of paths into the given value that should be +// marked as write-only based on the static declarations in the schema. +func (o *Object) writeOnlyPaths(val cty.Value, basePath cty.Path) []cty.Path { + var ret []cty.Path + + if val.IsNull() || !val.IsKnown() { + return ret + } + + for name, attrS := range o.Attributes { + // Skip attributes which can never produce write-only path value marks + if !attrS.WriteOnly && (attrS.NestedType == nil || !attrS.NestedType.ContainsWriteOnly()) { + continue + } + + switch o.Nesting { + case NestingSingle, NestingGroup: + // Create a path to this attribute + attrPath := slices.Concat(basePath, cty.GetAttrPath(name)) + if attrS.WriteOnly { + // If the entire attribute is write-only, mark it so + ret = append(ret, attrPath) + } else { + // The attribute has a nested type which contains write-only + // attributes, so recurse + ret = append(ret, attrS.NestedType.writeOnlyPaths(val.GetAttr(name), attrPath)...) + } + case NestingList, NestingMap, NestingSet: + // For nested attribute types which have a non-single nesting mode, + // we add path value marks for each element of the collection + val, _ = val.Unmark() // peel off one level of marking so we can iterate + for it := val.ElementIterator(); it.Next(); { + idx, attrEV := it.Element() + attrV := attrEV.GetAttr(name) + + // Create a path to this element of the attribute's collection. Note + // that the path is extended in opposite order to the iteration order + // of the loops: index into the collection, then the contained + // attribute name. This is because we have one type + // representing multiple collection elements. + attrPath := slices.Concat(basePath, cty.IndexPath(idx).GetAttr(name)) + + if attrS.WriteOnly { + // If the entire attribute is write-only, mark it so + ret = append(ret, attrPath) + } else { + ret = append(ret, attrS.NestedType.writeOnlyPaths(attrV, attrPath)...) + } + } + default: + panic(fmt.Sprintf("unsupported nesting mode %s", o.Nesting)) + } + } + return ret +} diff --git a/internal/configs/configschema/write_only_test.go b/internal/configs/configschema/write_only_test.go new file mode 100644 index 0000000000..206185de15 --- /dev/null +++ b/internal/configs/configschema/write_only_test.go @@ -0,0 +1,289 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configschema + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestBlock_WriteOnlyPaths(t *testing.T) { + schema := &Block{ + Attributes: map[string]*Attribute{ + "not_wo": { + Type: cty.String, + Optional: true, + }, + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + "nested": { + Optional: true, + NestedType: &Object{ + Attributes: map[string]*Attribute{ + "boop": { + Type: cty.String, + Optional: true, + }, + "honk": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + Nesting: NestingList, + }, + }, + "single": { + Optional: true, + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "not_wo": { + Optional: true, + Type: cty.String, + }, + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + "nested_single": { + Optional: true, + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "not_wo": { + Optional: true, + Type: cty.String, + }, + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "single_wo": { + Optional: true, + WriteOnly: true, + NestedType: &Object{ + Nesting: NestingSingle, + Attributes: map[string]*Attribute{ + "not_wo": { + Optional: true, + Type: cty.String, + }, + }, + }, + }, + }, + }, + }, + }, + + BlockTypes: map[string]*NestedBlock{ + "list": { + Nesting: NestingList, + Block: Block{ + Attributes: map[string]*Attribute{ + "not_wo": { + Type: cty.String, + Optional: true, + }, + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + "single_block": { + Nesting: NestingSingle, + Block: Block{ + Attributes: map[string]*Attribute{ + "not_wo": { + Type: cty.String, + Optional: true, + }, + "wo": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }}, + } + + testCases := map[string]struct { + value cty.Value + expected []cty.Path + }{ + "unknown value": { + cty.UnknownVal(schema.ImpliedType()), + []cty.Path{}, + }, + "null object": { + cty.NullVal(schema.ImpliedType()), + []cty.Path{}, + }, + "object with unknown attributes and blocks": { + cty.ObjectVal(map[string]cty.Value{ + "wo": cty.UnknownVal(cty.String), + "not_wo": cty.UnknownVal(cty.String), + "nested": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "boop": cty.String, + "honk": cty.String, + }))), + "list": cty.UnknownVal(schema.BlockTypes["list"].ImpliedType()), + }), + []cty.Path{ + {cty.GetAttrStep{Name: "wo"}}, + }, + }, + "object with block value": { + cty.ObjectVal(map[string]cty.Value{ + "wo": cty.NullVal(cty.String), + "not_wo": cty.UnknownVal(cty.String), + "list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "wo": cty.UnknownVal(cty.String), + "not_wo": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "wo": cty.NullVal(cty.String), + "not_wo": cty.NullVal(cty.String), + }), + }), + }), + []cty.Path{ + {cty.GetAttrStep{Name: "wo"}}, + {cty.GetAttrStep{Name: "list"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "wo"}}, + {cty.GetAttrStep{Name: "list"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "wo"}}, + }, + }, + "object with known values and nested attribute": { + cty.ObjectVal(map[string]cty.Value{ + "wo": cty.StringVal("foo"), + "not_wo": cty.StringVal("bar"), + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "boop": cty.StringVal("foo"), + "honk": cty.StringVal("bar"), + }), + cty.ObjectVal(map[string]cty.Value{ + "boop": cty.NullVal(cty.String), + "honk": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "boop": cty.UnknownVal(cty.String), + "honk": cty.UnknownVal(cty.String), + }), + }), + "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "not_wo": cty.String, + "wo": cty.String, + }))), + }), + []cty.Path{ + {cty.GetAttrStep{Name: "wo"}}, + {cty.GetAttrStep{Name: "nested"}, cty.IndexStep{Key: cty.NumberIntVal(0)}, cty.GetAttrStep{Name: "honk"}}, + {cty.GetAttrStep{Name: "nested"}, cty.IndexStep{Key: cty.NumberIntVal(1)}, cty.GetAttrStep{Name: "honk"}}, + {cty.GetAttrStep{Name: "nested"}, cty.IndexStep{Key: cty.NumberIntVal(2)}, cty.GetAttrStep{Name: "honk"}}, + }, + }, + "object with single nested block and attribute": { + cty.ObjectVal(map[string]cty.Value{ + "wo": cty.StringVal("foo"), + "not_wo": cty.StringVal("bar"), + "single": cty.ObjectVal(map[string]cty.Value{ + "not_wo": cty.StringVal("foo"), + "wo": cty.StringVal("bar"), + }), + "single_block": cty.ObjectVal(map[string]cty.Value{ + "not_wo": cty.StringVal("test"), + "wo": cty.StringVal("secret"), + }), + }), + []cty.Path{ + {cty.GetAttrStep{Name: "wo"}}, + {cty.GetAttrStep{Name: "single"}, cty.GetAttrStep{Name: "wo"}}, + cty.GetAttrPath("single").GetAttr(("single_wo")), + {cty.GetAttrStep{Name: "single_block"}, cty.GetAttrStep{Name: "wo"}}, + }, + }, + "object with doubly single-nested attribute": { + cty.ObjectVal(map[string]cty.Value{ + "single": cty.ObjectVal(map[string]cty.Value{ + "not_wo": cty.StringVal("foo"), + "wo": cty.NullVal(cty.String), + "nested_single": cty.ObjectVal(map[string]cty.Value{ + "not_wo": cty.StringVal("foo"), + "wo": cty.NullVal(cty.String), + }), + }), + }), + []cty.Path{ + {cty.GetAttrStep{Name: "wo"}}, + {cty.GetAttrStep{Name: "single"}, cty.GetAttrStep{Name: "wo"}}, + cty.GetAttrPath("single").GetAttr(("single_wo")), + {cty.GetAttrStep{Name: "single"}, cty.GetAttrStep{Name: "nested_single"}, cty.GetAttrStep{Name: "wo"}}, + }, + }, + "single nested write-only attr": { + cty.ObjectVal(map[string]cty.Value{ + "single": cty.ObjectVal(map[string]cty.Value{ + "single_wo": cty.ObjectVal(map[string]cty.Value{ + "not_wo": cty.StringVal("foo").Mark("test"), + }), + }), + }), + []cty.Path{ + cty.GetAttrPath("wo"), + cty.GetAttrPath("single").GetAttr(("wo")), + cty.GetAttrPath("single").GetAttr(("single_wo")), + }, + }, + "single nested null write-only attr": { + cty.ObjectVal(map[string]cty.Value{ + "single": cty.NullVal(cty.Object(map[string]cty.Type{ + "not_wo": cty.String, + "wo": cty.String, + "nested_single": cty.Object(map[string]cty.Type{ + "not_wo": cty.String, + "wo": cty.String, + }), + "single_wo": cty.Object(map[string]cty.Type{ + "not_wo": cty.String, + }), + })), + }), + []cty.Path{ + {cty.GetAttrStep{Name: "wo"}}, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + if err := schema.InternalValidate(); err != nil { + t.Fatal(err) + } + val, err := schema.CoerceValue(tc.value) + if err != nil { + t.Fatal(err) + } + woPaths := schema.WriteOnlyPaths(val, nil) + if !cty.NewPathSet(tc.expected...).Equal(cty.NewPathSet(woPaths...)) { + t.Fatalf("\nexpected: %#v\ngot: %#v\n", tc.expected, woPaths) + } + }) + } +} diff --git a/internal/configs/synth_body.go b/internal/configs/configtesting/synth_body.go similarity index 97% rename from internal/configs/synth_body.go rename to internal/configs/configtesting/synth_body.go index cd914e5dbc..39e61a1553 100644 --- a/internal/configs/synth_body.go +++ b/internal/configs/configtesting/synth_body.go @@ -1,4 +1,7 @@ -package configs +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configtesting import ( "fmt" diff --git a/internal/configs/synth_body_test.go b/internal/configs/configtesting/synth_body_test.go similarity index 93% rename from internal/configs/synth_body_test.go rename to internal/configs/configtesting/synth_body_test.go index 8ca46a00a7..5c708bb42d 100644 --- a/internal/configs/synth_body_test.go +++ b/internal/configs/configtesting/synth_body_test.go @@ -1,4 +1,7 @@ -package configs +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configtesting import ( "testing" diff --git a/internal/configs/container.go b/internal/configs/container.go new file mode 100644 index 0000000000..ebfda0e2f3 --- /dev/null +++ b/internal/configs/container.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import "github.com/hashicorp/terraform/internal/addrs" + +// Container provides an interface for scoped resources. +// +// Any resources contained within a Container should not be accessible from +// outside the container. +type Container interface { + // Accessible should return true if the resource specified by addr can + // reference other items within this Container. + // + // Typically, that means that addr will either be the container itself or + // something within the container. + Accessible(addr addrs.Referenceable) bool +} diff --git a/internal/configs/depends_on.go b/internal/configs/depends_on.go index 036c2d6c30..9e3d23ff03 100644 --- a/internal/configs/depends_on.go +++ b/internal/configs/depends_on.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "github.com/hashicorp/hcl/v2" ) -func decodeDependsOn(attr *hcl.Attribute) ([]hcl.Traversal, hcl.Diagnostics) { +func DecodeDependsOn(attr *hcl.Attribute) ([]hcl.Traversal, hcl.Diagnostics) { var ret []hcl.Traversal exprs, diags := hcl.ExprList(attr.Expr) diff --git a/internal/configs/doc.go b/internal/configs/doc.go index f01eb79f40..e17d1d6ddb 100644 --- a/internal/configs/doc.go +++ b/internal/configs/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package configs contains types that represent Terraform configurations and // the different elements thereof. // @@ -14,6 +17,6 @@ // // The Parser type is the main entry-point into this package. The LoadConfigDir // method can be used to load a single module directory, and then a full -// configuration (including any descendent modules) can be produced using +// configuration (including any descendant modules) can be produced using // the top-level BuildConfig method. package configs diff --git a/internal/configs/escaping_blocks_test.go b/internal/configs/escaping_blocks_test.go index 7996d8b77e..d61a965abc 100644 --- a/internal/configs/escaping_blocks_test.go +++ b/internal/configs/escaping_blocks_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/experiments.go b/internal/configs/experiments.go index 1d1a36b020..46d8155c7c 100644 --- a/internal/configs/experiments.go +++ b/internal/configs/experiments.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -85,24 +88,6 @@ func sniffActiveExperiments(body hcl.Body, allowed bool) (experiments.Set, hcl.D exps, expDiags := decodeExperimentsAttr(attr) - // Because we concluded this particular experiment in the same - // release as we made experiments alpha-releases-only, we need to - // treat it as special to avoid masking the "experiment has concluded" - // error with the more general "experiments are not available at all" - // error. Note that this experiment is marked as concluded so this - // only "allows" showing the different error message that it is - // concluded, and does not allow actually using the experiment outside - // of an alpha. - // NOTE: We should be able to remove this special exception a release - // or two after v1.3 when folks have had a chance to notice that the - // experiment has concluded and update their modules accordingly. - // When we do so, we might also consider changing decodeExperimentsAttr - // to _not_ include concluded experiments in the returned set, since - // we're doing that right now only to make this condition work. - if exps.Has(experiments.ModuleVariableOptionalAttrs) && len(exps) == 1 { - allowed = true - } - if allowed { diags = append(diags, expDiags...) if !expDiags.HasErrors() { @@ -153,15 +138,6 @@ func decodeExperimentsAttr(attr *hcl.Attribute) (experiments.Set, hcl.Diagnostic Subject: expr.Range().Ptr(), }) case experiments.ConcludedError: - // As a special case we still include the optional attributes - // experiment if it's present, because our caller treats that - // as special. See the comment in sniffActiveExperiments for - // more information, and remove this special case here one the - // special case up there is also removed. - if kw == "module_variable_optional_attrs" { - ret.Add(experiments.ModuleVariableOptionalAttrs) - } - diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Experiment has concluded", @@ -231,6 +207,5 @@ func checkModuleExperiments(m *Module) hcl.Diagnostics { } } */ - return diags } diff --git a/internal/configs/experiments_test.go b/internal/configs/experiments_test.go index 7d1b9dc4e3..7d0c456784 100644 --- a/internal/configs/experiments_test.go +++ b/internal/configs/experiments_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/hcl2shim/flatmap.go b/internal/configs/hcl2shim/flatmap.go index bb4228d98c..c78cb06d5f 100644 --- a/internal/configs/hcl2shim/flatmap.go +++ b/internal/configs/hcl2shim/flatmap.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/flatmap_test.go b/internal/configs/hcl2shim/flatmap_test.go index e2ae8a70fc..c144aedf0c 100644 --- a/internal/configs/hcl2shim/flatmap_test.go +++ b/internal/configs/hcl2shim/flatmap_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/paths.go b/internal/configs/hcl2shim/paths.go index 3403c026bf..5a4c605e01 100644 --- a/internal/configs/hcl2shim/paths.go +++ b/internal/configs/hcl2shim/paths.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/paths_test.go b/internal/configs/hcl2shim/paths_test.go index ab166e6f8c..8c463e7c8b 100644 --- a/internal/configs/hcl2shim/paths_test.go +++ b/internal/configs/hcl2shim/paths_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/single_attr_body.go b/internal/configs/hcl2shim/single_attr_body.go index 68f48da8f3..68c41a7530 100644 --- a/internal/configs/hcl2shim/single_attr_body.go +++ b/internal/configs/hcl2shim/single_attr_body.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/values.go b/internal/configs/hcl2shim/values.go index 7b0e09607f..50424642d4 100644 --- a/internal/configs/hcl2shim/values.go +++ b/internal/configs/hcl2shim/values.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/values_equiv.go b/internal/configs/hcl2shim/values_equiv.go index 92f0213d72..649d15009e 100644 --- a/internal/configs/hcl2shim/values_equiv.go +++ b/internal/configs/hcl2shim/values_equiv.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/values_equiv_test.go b/internal/configs/hcl2shim/values_equiv_test.go index aff01a9e33..dff02004a2 100644 --- a/internal/configs/hcl2shim/values_equiv_test.go +++ b/internal/configs/hcl2shim/values_equiv_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/hcl2shim/values_test.go b/internal/configs/hcl2shim/values_test.go index 4bc816dd9b..66856bf9cc 100644 --- a/internal/configs/hcl2shim/values_test.go +++ b/internal/configs/hcl2shim/values_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hcl2shim import ( diff --git a/internal/configs/import.go b/internal/configs/import.go new file mode 100644 index 0000000000..f7efcef3a4 --- /dev/null +++ b/internal/configs/import.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + hcljson "github.com/hashicorp/hcl/v2/json" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +type Import struct { + ID hcl.Expression + + Identity hcl.Expression + + To hcl.Expression + // The To address may not be resolvable immediately if it contains dynamic + // index expressions, so we will extract the ConfigResource address and + // store it here for reference. + ToResource addrs.ConfigResource + + ForEach hcl.Expression + + ProviderConfigRef *ProviderConfigRef + Provider addrs.Provider + + DeclRange hcl.Range + ProviderDeclRange hcl.Range +} + +func decodeImportBlock(block *hcl.Block) (*Import, hcl.Diagnostics) { + var diags hcl.Diagnostics + imp := &Import{ + DeclRange: block.DefRange, + } + + content, moreDiags := block.Body.Content(importBlockSchema) + diags = append(diags, moreDiags...) + + if attr, exists := content.Attributes["id"]; exists { + imp.ID = attr.Expr + } + + if attr, exists := content.Attributes["identity"]; exists { + imp.Identity = attr.Expr + } + + if attr, exists := content.Attributes["to"]; exists { + toExpr, jsDiags := unwrapJSONRefExpr(attr.Expr) + diags = diags.Extend(jsDiags) + if diags.HasErrors() { + return imp, diags + } + + imp.To = toExpr + + addr, toDiags := parseConfigResourceFromExpression(imp.To) + diags = diags.Extend(toDiags.ToHCL()) + + if addr.Resource.Mode != addrs.ManagedResourceMode { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import address", + Detail: "Only managed resources can be imported.", + Subject: attr.Range.Ptr(), + }) + } + + imp.ToResource = addr + } + + if attr, exists := content.Attributes["for_each"]; exists { + imp.ForEach = attr.Expr + } + + if attr, exists := content.Attributes["provider"]; exists { + if len(imp.ToResource.Module) > 0 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import provider argument", + Detail: "The provider argument can only be specified in import blocks that will generate configuration.\n\nUse the providers argument within the module block to configure providers for all resources within a module, including imported resources.", + Subject: attr.Range.Ptr(), + }) + } + + var providerDiags hcl.Diagnostics + imp.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") + imp.ProviderDeclRange = attr.Range + diags = append(diags, providerDiags...) + } + + if imp.ID == nil && imp.Identity == nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import block", + Detail: "At least one of 'id' or 'identity' must be specified.", + Subject: block.DefRange.Ptr(), + }) + } + + if imp.ID != nil && imp.Identity != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import block", + Detail: "Only one of 'id' or 'identity' can be specified.", + Subject: block.DefRange.Ptr(), + }) + } + + return imp, diags +} + +var importBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "provider", + }, + { + Name: "for_each", + }, + { + Name: "id", + }, + { + Name: "to", + Required: true, + }, + { + Name: "identity", + }, + }, +} + +// parseResourceInstanceFromExpression takes an arbitrary expression +// representing a resource instance, and parses out the static ConfigResource +// skipping an variable index expressions. This is used to connect an import +// block's "to" to the configuration address before the full instance +// expressions are evaluated. +func parseConfigResourceFromExpression(expr hcl.Expression) (addrs.ConfigResource, tfdiags.Diagnostics) { + traversal, hcdiags := exprToResourceTraversal(expr) + if hcdiags.HasErrors() { + return addrs.ConfigResource{}, tfdiags.Diagnostics(nil).Append(hcdiags) + } + + addr, diags := addrs.ParseAbsResourceInstance(traversal) + if diags.HasErrors() { + return addrs.ConfigResource{}, diags + } + + return addr.ConfigResource(), diags +} + +// unwrapJSONRefExpr takes a string expression from a JSON configuration, +// and re-evaluates the string as HCL. If the expression is not JSON, the +// original expression is returned directly. +func unwrapJSONRefExpr(expr hcl.Expression) (hcl.Expression, hcl.Diagnostics) { + if !hcljson.IsJSONExpression(expr) { + return expr, nil + } + + // We can abuse the hcl json api and rely on the fact that calling + // Value on a json expression with no EvalContext will return the + // raw string. We can then parse that as normal hcl syntax, and + // continue with the decoding. + v, diags := expr.Value(nil) + if diags.HasErrors() { + return nil, diags + } + + // the JSON representation can only be a string + if v.Type() != cty.String { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference expression", + Detail: "A single reference string is required.", + Subject: expr.Range().Ptr(), + }) + + return nil, diags + } + + rng := expr.Range() + expr, ds := hclsyntax.ParseExpression([]byte(v.AsString()), rng.Filename, rng.Start) + diags = diags.Extend(ds) + return expr, diags +} + +// exprToResourceTraversal is used to parse the import block's to expression, +// which must be a resource instance, but may contain limited variables with +// index expressions. Since we only need the ConfigResource to connect the +// import to the configuration, we skip any index expressions. +func exprToResourceTraversal(expr hcl.Expression) (hcl.Traversal, hcl.Diagnostics) { + var trav hcl.Traversal + var diags hcl.Diagnostics + + switch e := expr.(type) { + case *hclsyntax.RelativeTraversalExpr: + t, d := exprToResourceTraversal(e.Source) + diags = diags.Extend(d) + trav = append(trav, t...) + trav = append(trav, e.Traversal...) + + case *hclsyntax.ScopeTraversalExpr: + // a static reference, we can just append the traversal + trav = append(trav, e.Traversal...) + + case *hclsyntax.IndexExpr: + // Get the collection from the index expression, we don't need the + // index for a ConfigResource + t, d := exprToResourceTraversal(e.Collection) + diags = diags.Extend(d) + if diags.HasErrors() { + return nil, diags + } + trav = append(trav, t...) + + default: + // if we don't recognise the expression type (which means we are likely + // dealing with a test mock), try and interpret this as an absolute + // traversal + t, d := hcl.AbsTraversalForExpr(e) + diags = diags.Extend(d) + trav = append(trav, t...) + } + + return trav, diags +} + +// parseImportToStatic attempts to parse the To address of an import block +// statically to get the resource address. This returns false when the address +// cannot be parsed, which is usually a result of dynamic index expressions +// using for_each. +func parseImportToStatic(expr hcl.Expression) (addrs.AbsResourceInstance, bool) { + // we may have a nil expression in some error cases, which we can just + // false to avoid the parsing + if expr == nil { + return addrs.AbsResourceInstance{}, false + } + + var toDiags tfdiags.Diagnostics + traversal, hd := hcl.AbsTraversalForExpr(expr) + toDiags = toDiags.Append(hd) + to, td := addrs.ParseAbsResourceInstance(traversal) + toDiags = toDiags.Append(td) + return to, !toDiags.HasErrors() +} diff --git a/internal/configs/import_test.go b/internal/configs/import_test.go new file mode 100644 index 0000000000..891aed2680 --- /dev/null +++ b/internal/configs/import_test.go @@ -0,0 +1,282 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "fmt" + "reflect" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/zclconf/go-cty/cty" +) + +func TestParseConfigResourceFromExpression(t *testing.T) { + mustExpr := func(expr hcl.Expression, diags hcl.Diagnostics) hcl.Expression { + if diags != nil { + panic(diags.Error()) + } + return expr + } + + tests := []struct { + expr hcl.Expression + expect addrs.ConfigResource + }{ + { + mustExpr(hclsyntax.ParseExpression([]byte("test_instance.bar"), "my_traversal", hcl.Pos{})), + mustAbsResourceInstanceAddr("test_instance.bar").ConfigResource(), + }, + + // parsing should skip the each.key variable + { + mustExpr(hclsyntax.ParseExpression([]byte("test_instance.bar[each.key]"), "my_traversal", hcl.Pos{})), + mustAbsResourceInstanceAddr("test_instance.bar").ConfigResource(), + }, + + // nested modules must work too + { + mustExpr(hclsyntax.ParseExpression([]byte("module.foo[each.key].test_instance.bar[each.key]"), "my_traversal", hcl.Pos{})), + mustAbsResourceInstanceAddr("module.foo.test_instance.bar").ConfigResource(), + }, + } + + for i, tc := range tests { + t.Run(fmt.Sprintf("%d-%s", i, tc.expect), func(t *testing.T) { + + got, diags := parseConfigResourceFromExpression(tc.expr) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + if !got.Equal(tc.expect) { + t.Fatalf("got %s, want %s", got, tc.expect) + } + }) + } +} + +func TestImportBlock_decode(t *testing.T) { + blockRange := hcl.Range{ + Filename: "mock.tf", + Start: hcl.Pos{Line: 3, Column: 12, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 19, Byte: 34}, + } + + foo_str_expr := hcltest.MockExprLiteral(cty.StringVal("foo")) + id_obj_expr := hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + })) + bar_expr := hcltest.MockExprTraversalSrc("test_instance.bar") + + bar_index_expr := hcltest.MockExprTraversalSrc("test_instance.bar[\"one\"]") + + mod_bar_expr := hcltest.MockExprTraversalSrc("module.bar.test_instance.bar") + + tests := map[string]struct { + input *hcl.Block + want *Import + err string + }{ + "success": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "id": { + Name: "id", + Expr: foo_str_expr, + }, + "to": { + Name: "to", + Expr: bar_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ToResource: mustAbsResourceInstanceAddr("test_instance.bar").ConfigResource(), + ID: foo_str_expr, + DeclRange: blockRange, + }, + ``, + }, + "indexed resources": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "id": { + Name: "id", + Expr: foo_str_expr, + }, + "to": { + Name: "to", + Expr: bar_index_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ToResource: mustAbsResourceInstanceAddr("test_instance.bar[\"one\"]").ConfigResource(), + ID: foo_str_expr, + DeclRange: blockRange, + }, + ``, + }, + "resource inside module": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "id": { + Name: "id", + Expr: foo_str_expr, + }, + "to": { + Name: "to", + Expr: mod_bar_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ToResource: mustAbsResourceInstanceAddr("module.bar.test_instance.bar").ConfigResource(), + ID: foo_str_expr, + DeclRange: blockRange, + }, + ``, + }, + "error: missing id or identity argument": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "to": { + Name: "to", + Expr: bar_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ToResource: mustAbsResourceInstanceAddr("test_instance.bar").ConfigResource(), + DeclRange: blockRange, + }, + "Invalid import block", + }, + "error: id and identity argument": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "id": { + Name: "id", + Expr: foo_str_expr, + }, + "identity": { + Name: "identity", + Expr: id_obj_expr, + }, + "to": { + Name: "to", + Expr: bar_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ToResource: mustAbsResourceInstanceAddr("test_instance.bar").ConfigResource(), + DeclRange: blockRange, + }, + "Invalid import block", + }, + "error: missing to argument": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "id": { + Name: "id", + Expr: foo_str_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ID: foo_str_expr, + DeclRange: blockRange, + }, + "Missing required argument", + }, + "error: data source": { + &hcl.Block{ + Type: "import", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "id": { + Name: "id", + Expr: foo_str_expr, + }, + "to": { + Name: "to", + Expr: hcltest.MockExprTraversalSrc("data.test_instance.bar"), + }, + }, + }), + DefRange: blockRange, + }, + &Import{ + ID: foo_str_expr, + DeclRange: blockRange, + }, + "Invalid import address", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, diags := decodeImportBlock(test.input) + + if diags.HasErrors() { + if test.err == "" { + t.Fatalf("unexpected error: %s", diags.Errs()) + } + if gotErr := diags[0].Summary; gotErr != test.err { + t.Errorf("wrong error, got %q, want %q", gotErr, test.err) + } + } else if test.err != "" { + t.Fatal("expected error") + } + + if diags.HasErrors() { + return + } + + if !got.ToResource.Equal(test.want.ToResource) { + t.Errorf("expected resource %q got %q", test.want.ToResource, got.ToResource) + } + + if !reflect.DeepEqual(got.ID, test.want.ID) { + t.Errorf("expected ID %q got %q", test.want.ID, got.ID) + } + }) + } +} + +func mustAbsResourceInstanceAddr(str string) addrs.AbsResourceInstance { + addr, diags := addrs.ParseAbsResourceInstanceStr(str) + if diags.HasErrors() { + panic(fmt.Sprintf("invalid absolute resource instance address: %s", diags.Err())) + } + return addr +} diff --git a/internal/configs/mock_provider.go b/internal/configs/mock_provider.go new file mode 100644 index 0000000000..97118a9b23 --- /dev/null +++ b/internal/configs/mock_provider.go @@ -0,0 +1,541 @@ +package configs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + // When this attribute is set to plan, the values specified in the override + // block will be used for computed attributes even when planning. It defaults + // to apply, meaning that the values will only be used during apply. + overrideDuringCommand = "override_during" +) + +func decodeMockProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, config, moreDiags := block.Body.PartialContent(mockProviderSchema) + diags = append(diags, moreDiags...) + + name := block.Labels[0] + nameDiags := checkProviderNameNormalized(name, block.DefRange) + diags = append(diags, nameDiags...) + if nameDiags.HasErrors() { + // If the name is invalid then we mustn't produce a result because + // downstream could try to use it as a provider type and then crash. + return nil, diags + } + + provider := &Provider{ + Name: name, + NameRange: block.LabelRanges[0], + DeclRange: block.DefRange, + + // Mock providers shouldn't need any additional data. + Config: hcl.EmptyBody(), + + // Mark this provider as being mocked. + Mock: true, + } + + if attr, exists := content.Attributes["alias"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias) + diags = append(diags, valDiags...) + provider.AliasRange = attr.Expr.Range().Ptr() + + if !hclsyntax.ValidIdentifier(provider.Alias) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration alias", + Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail), + }) + } + } + + useForPlan, useForPlanDiags := useForPlan(content, false) + diags = append(diags, useForPlanDiags...) + provider.MockDataDuringPlan = useForPlan + + var dataDiags hcl.Diagnostics + provider.MockData, dataDiags = decodeMockDataBody(config, useForPlan, MockProviderOverrideSource) + diags = append(diags, dataDiags...) + + if attr, exists := content.Attributes["source"]; exists { + sourceDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.MockDataExternalSource) + diags = append(diags, sourceDiags...) + } + + return provider, diags +} + +func useForPlan(content *hcl.BodyContent, def bool) (bool, hcl.Diagnostics) { + var diags hcl.Diagnostics + if attr, exists := content.Attributes[overrideDuringCommand]; exists { + switch hcl.ExprAsKeyword(attr.Expr) { + case "plan": + return true, diags + case "apply": + return false, diags + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Invalid %s value", overrideDuringCommand), + Detail: fmt.Sprintf("The %s attribute must be a value of plan or apply.", overrideDuringCommand), + Subject: attr.Range.Ptr(), + }) + return def, diags + } + } + return def, diags +} + +// MockData packages up all the available mock and override data available to +// a mocked provider. +type MockData struct { + MockResources map[string]*MockResource + MockDataSources map[string]*MockResource + Overrides addrs.Map[addrs.Targetable, *Override] +} + +// Merge will merge the target MockData object into the current MockData. +// +// If skipCollisions is true, then Merge will simply ignore any entries within +// other that clash with entries already in data. If skipCollisions is false, +// then we will create diagnostics for each duplicate resource. +func (data *MockData) Merge(other *MockData, skipCollisions bool) (diags hcl.Diagnostics) { + if other == nil { + return diags + } + + for name, resource := range other.MockResources { + current, exists := data.MockResources[name] + if !exists { + data.MockResources[name] = resource + continue + } + + if skipCollisions { + continue + } + + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate mock resource block", + Detail: fmt.Sprintf("A mock_resource %q block already exists at %s.", name, current.Range), + Subject: resource.TypeRange.Ptr(), + }) + } + for name, datasource := range other.MockDataSources { + current, exists := data.MockDataSources[name] + if !exists { + data.MockDataSources[name] = datasource + continue + } + + if skipCollisions { + continue + } + + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate mock resource block", + Detail: fmt.Sprintf("A mock_data %q block already exists at %s.", name, current.Range), + Subject: datasource.TypeRange.Ptr(), + }) + } + for _, elem := range other.Overrides.Elems { + target, override := elem.Key, elem.Value + + current, exists := data.Overrides.GetOk(target) + if !exists { + data.Overrides.Put(target, override) + continue + } + + if skipCollisions { + continue + } + + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override block", + Detail: fmt.Sprintf("An override block for %s already exists at %s.", target, current.Range), + Subject: override.Range.Ptr(), + }) + } + return diags +} + +// MockResource maps a resource or data source type and name to a set of values +// for that resource. +type MockResource struct { + Mode addrs.ResourceMode + Type string + + Defaults cty.Value + + // UseForPlan is true if the values should be computed during the planning + // phase. + UseForPlan bool + + Range hcl.Range + TypeRange hcl.Range + DefaultsRange hcl.Range +} + +type OverrideSource int + +const ( + UnknownOverrideSource OverrideSource = iota + RunBlockOverrideSource + TestFileOverrideSource + MockProviderOverrideSource + MockDataFileOverrideSource +) + +// Override targets a specific module, resource or data source with a set of +// replacement values that should be used in place of whatever the underlying +// provider would normally do. +type Override struct { + Target *addrs.Target + Values cty.Value + + // UseForPlan is true if the values should be computed during the planning + // phase. + UseForPlan bool + + // Source tells us where this Override was defined. + Source OverrideSource + + Range hcl.Range + TypeRange hcl.Range + TargetRange hcl.Range + ValuesRange hcl.Range +} + +func decodeMockDataBody(body hcl.Body, useForPlanDefault bool, source OverrideSource) (*MockData, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := body.Content(mockDataSchema) + diags = append(diags, contentDiags...) + + data := &MockData{ + MockResources: make(map[string]*MockResource), + MockDataSources: make(map[string]*MockResource), + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + } + + for _, block := range content.Blocks { + switch block.Type { + case "mock_resource", "mock_data": + resource, resourceDiags := decodeMockResourceBlock(block, useForPlanDefault) + diags = append(diags, resourceDiags...) + + if resource != nil { + switch resource.Mode { + case addrs.ManagedResourceMode: + if previous, ok := data.MockResources[resource.Type]; ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate mock_resource block", + Detail: fmt.Sprintf("A mock_resource block for %s has already been defined at %s.", resource.Type, previous.Range), + Subject: resource.TypeRange.Ptr(), + }) + continue + } + data.MockResources[resource.Type] = resource + case addrs.DataResourceMode: + if previous, ok := data.MockDataSources[resource.Type]; ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate mock_data block", + Detail: fmt.Sprintf("A mock_data block for %s has already been defined at %s.", resource.Type, previous.Range), + Subject: resource.TypeRange.Ptr(), + }) + continue + } + data.MockDataSources[resource.Type] = resource + } + } + case "override_resource": + override, overrideDiags := decodeOverrideResourceBlock(block, useForPlanDefault, source) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := data.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_resource block", + Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + data.Overrides.Put(subject, override) + } + case "override_data": + override, overrideDiags := decodeOverrideDataBlock(block, useForPlanDefault, source) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := data.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_data block", + Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + data.Overrides.Put(subject, override) + } + } + } + + return data, diags +} + +func decodeMockResourceBlock(block *hcl.Block, useForPlanDefault bool) (*MockResource, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(mockResourceSchema) + diags = append(diags, contentDiags...) + + resource := &MockResource{ + Type: block.Labels[0], + Range: block.DefRange, + TypeRange: block.LabelRanges[0], + } + + switch block.Type { + case "mock_resource": + resource.Mode = addrs.ManagedResourceMode + case "mock_data": + resource.Mode = addrs.DataResourceMode + } + + if defaults, exists := content.Attributes["defaults"]; exists { + var defaultDiags hcl.Diagnostics + resource.DefaultsRange = defaults.Range + resource.Defaults, defaultDiags = defaults.Expr.Value(nil) + diags = append(diags, defaultDiags...) + } else { + // It's fine if we don't have any defaults, just means we'll generate + // values for everything ourselves. + resource.Defaults = cty.NilVal + } + + useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault) + diags = append(diags, useForPlanDiags...) + resource.UseForPlan = useForPlan + + return resource, diags +} + +func decodeOverrideModuleBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) { + override, diags := decodeOverrideBlock(block, "outputs", "override_module", useForPlanDefault, source) + + if override.Target != nil { + switch override.Target.Subject.AddrType() { + case addrs.ModuleAddrType, addrs.ModuleInstanceAddrType: + // Do nothing, we're good here. + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target modules from override_module blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + } + + return override, diags +} + +func decodeOverrideResourceBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) { + override, diags := decodeOverrideBlock(block, "values", "override_resource", useForPlanDefault, source) + + if override.Target != nil { + var mode addrs.ResourceMode + + switch override.Target.Subject.AddrType() { + case addrs.AbsResourceInstanceAddrType: + subject := override.Target.Subject.(addrs.AbsResourceInstance) + mode = subject.Resource.Resource.Mode + case addrs.AbsResourceAddrType: + subject := override.Target.Subject.(addrs.AbsResource) + mode = subject.Resource.Mode + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + + if mode != addrs.ManagedResourceMode { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target resources from override_resource blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + } + + return override, diags +} + +func decodeOverrideDataBlock(block *hcl.Block, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) { + override, diags := decodeOverrideBlock(block, "values", "override_data", useForPlanDefault, source) + + if override.Target != nil { + var mode addrs.ResourceMode + + switch override.Target.Subject.AddrType() { + case addrs.AbsResourceInstanceAddrType: + subject := override.Target.Subject.(addrs.AbsResourceInstance) + mode = subject.Resource.Resource.Mode + case addrs.AbsResourceAddrType: + subject := override.Target.Subject.(addrs.AbsResource) + mode = subject.Resource.Mode + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + + if mode != addrs.DataResourceMode { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid override target", + Detail: fmt.Sprintf("You can only target data sources from override_data blocks, not %s.", override.Target.Subject), + Subject: override.TargetRange.Ptr(), + }) + return nil, diags + } + } + + return override, diags +} + +func decodeOverrideBlock(block *hcl.Block, attributeName string, blockName string, useForPlanDefault bool, source OverrideSource) (*Override, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "target"}, + {Name: overrideDuringCommand}, + {Name: attributeName}, + }, + }) + diags = append(diags, contentDiags...) + + override := &Override{ + Source: source, + Range: block.DefRange, + TypeRange: block.TypeRange, + } + + if target, exists := content.Attributes["target"]; exists { + override.TargetRange = target.Range + traversal, traversalDiags := hcl.AbsTraversalForExpr(target.Expr) + diags = append(diags, traversalDiags...) + if traversal != nil { + var targetDiags tfdiags.Diagnostics + override.Target, targetDiags = addrs.ParseTarget(traversal) + diags = append(diags, targetDiags.ToHCL()...) + } + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing target attribute", + Detail: fmt.Sprintf("%s blocks must specify a target address.", blockName), + Subject: override.Range.Ptr(), + }) + } + + if attribute, exists := content.Attributes[attributeName]; exists { + var valueDiags hcl.Diagnostics + override.ValuesRange = attribute.Range + override.Values, valueDiags = attribute.Expr.Value(nil) + diags = append(diags, valueDiags...) + } else { + // It's fine if we don't have any values, just means we'll generate + // values for everything ourselves. We set this to an empty object so + // it's equivalent to `values = {}` which makes later processing easier. + override.Values = cty.EmptyObjectVal + } + + useForPlan, useForPlanDiags := useForPlan(content, useForPlanDefault) + diags = append(diags, useForPlanDiags...) + override.UseForPlan = useForPlan + + if !override.Values.Type().IsObjectType() { + + var attributePreposition string + switch attributeName { + case "outputs": + attributePreposition = "an" + default: + attributePreposition = "a" + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Invalid %s attribute", attributeName), + Detail: fmt.Sprintf("%s blocks must specify %s %s attribute that is an object.", blockName, attributePreposition, attributeName), + Subject: override.ValuesRange.Ptr(), + }) + } + + return override, diags +} + +var mockProviderSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "alias", + }, + { + Name: "source", + }, + { + Name: overrideDuringCommand, + }, + }, +} + +var mockDataSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: "mock_resource", LabelNames: []string{"type"}}, + {Type: "mock_data", LabelNames: []string{"type"}}, + {Type: "override_resource"}, + {Type: "override_data"}, + }, +} + +var mockResourceSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "defaults"}, + {Name: overrideDuringCommand}, + }, +} diff --git a/internal/configs/mock_provider_test.go b/internal/configs/mock_provider_test.go new file mode 100644 index 0000000000..67700f4fd8 --- /dev/null +++ b/internal/configs/mock_provider_test.go @@ -0,0 +1,585 @@ +package configs + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" +) + +func TestMockData_Merge(t *testing.T) { + + tcs := map[string]struct { + current *MockData + target *MockData + result *MockData + }{ + "empty_target": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: &MockData{ + MockResources: map[string]*MockResource{}, + MockDataSources: map[string]*MockResource{}, + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + }, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + }, + "nil_target": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: nil, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + }, + "all_collisions": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("target"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("target"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("target")), + ), + }, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + }, + "no_collisions": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource_two", + Defaults: cty.StringVal("target"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source_two", + Defaults: cty.StringVal("target"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_other_resource", cty.StringVal("target")), + ), + }, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + "test_resource_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource_two", + Defaults: cty.StringVal("target"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + "test_data_source_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source_two", + Defaults: cty.StringVal("target"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + makeOverride(t, "test_resource.my_other_resource", cty.StringVal("target")), + ), + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + diags := tc.current.Merge(tc.target, true) + validateMockData(t, tc.current, tc.result) + + var details []string + for _, diag := range diags { + details = append(details, diag.Detail) + } + if len(details) > 0 { + t.Errorf("expected no diags but found [%s]", strings.Join(details, ", ")) + } + + }) + } +} + +func TestMockData_MergeWithCollisions(t *testing.T) { + + tcs := map[string]struct { + current *MockData + target *MockData + result *MockData + diags []string + }{ + "empty_target": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: &MockData{ + MockResources: map[string]*MockResource{}, + MockDataSources: map[string]*MockResource{}, + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + }, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + }, + "nil_target": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: nil, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + }, + "all_collisions": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("target"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("target"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("target")), + ), + }, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + diags: []string{ + "A mock_resource \"test_resource\" block already exists at :0,0-0.", + "A mock_data \"test_data_source\" block already exists at :0,0-0.", + "An override block for test_resource.my_resource already exists at :0,0-0.", + }, + }, + "no_collisions": { + current: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + ), + }, + target: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource_two", + Defaults: cty.StringVal("target"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source_two", + Defaults: cty.StringVal("target"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_other_resource", cty.StringVal("target")), + ), + }, + result: &MockData{ + MockResources: map[string]*MockResource{ + "test_resource": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Defaults: cty.StringVal("current"), + }, + "test_resource_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_resource_two", + Defaults: cty.StringVal("target"), + }, + }, + MockDataSources: map[string]*MockResource{ + "test_data_source": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source", + Defaults: cty.StringVal("current"), + }, + "test_data_source_two": { + Mode: addrs.ManagedResourceMode, + Type: "test_data_source_two", + Defaults: cty.StringVal("target"), + }, + }, + Overrides: addrs.MakeMap[addrs.Targetable, *Override]( + makeOverride(t, "test_resource.my_resource", cty.StringVal("current")), + makeOverride(t, "test_resource.my_other_resource", cty.StringVal("target")), + ), + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + diags := tc.current.Merge(tc.target, false) + validateMockData(t, tc.current, tc.result) + + var details []string + for _, diag := range diags { + details = append(details, diag.Detail) + } + if diff := cmp.Diff(tc.diags, details); len(diff) > 0 { + t.Error(diff) + } + + }) + } +} + +func validateMockData(t *testing.T, actual, expected *MockData) { + + // Validate mock resources. + + for key, actual := range actual.MockResources { + expected, exists := expected.MockResources[key] + if !exists { + t.Errorf("actual mock resources contained %s but expected mock resources did not", key) + continue + } + + validateValues(t, key, actual.Defaults, expected.Defaults) + } + + for key := range expected.MockResources { + _, exists := actual.MockResources[key] + if !exists { + t.Errorf("expected mock resources contained %s but actual mock resources did not", key) + } + } + + // Validate mock data sources. + + for key, actual := range actual.MockDataSources { + expected, exists := expected.MockDataSources[key] + if !exists { + t.Errorf("actual mock data sources contained %s but expected mock data sources did not", key) + continue + } + + validateValues(t, key, actual.Defaults, expected.Defaults) + } + + for key := range expected.MockDataSources { + _, exists := actual.MockDataSources[key] + if !exists { + t.Errorf("expected mock data sources contained %s but actual mock data sources did not", key) + } + } + + // Validate the overrides. + + for _, elem := range actual.Overrides.Elems { + key, actual := elem.Key, elem.Value + + expected, exists := expected.Overrides.GetOk(key) + if !exists { + t.Errorf("actual overrides contained %s but expected overrides did not", key) + continue + } + + validateValues(t, key.String(), actual.Values, expected.Values) + } + + for _, elem := range expected.Overrides.Elems { + key := elem.Key + + if actual.Overrides.Has(key) { + continue + } + + t.Errorf("expected overrides contained %s but actual overrides did not", key) + } +} + +func validateValues(t *testing.T, key string, actual, expected cty.Value) { + if !actual.RawEquals(expected) { + t.Errorf("for %s\n\tactual: %s\n\texpected: %s", key, actual, expected) + } +} + +func makeOverride(t *testing.T, target string, values cty.Value) addrs.MapElem[addrs.Targetable, *Override] { + addr, diags := addrs.ParseTargetStr(target) + if diags.HasErrors() { + t.Fatalf("failed to parse target: %s", diags) + } + + return addrs.MapElem[addrs.Targetable, *Override]{ + Key: addr.Subject, + Value: &Override{ + Target: addr, + Values: values, + }, + } +} diff --git a/internal/configs/module.go b/internal/configs/module.go index c2088b9fde..71043ef5d7 100644 --- a/internal/configs/module.go +++ b/internal/configs/module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -7,6 +10,8 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" + + tfversion "github.com/hashicorp/terraform/version" ) // Module is a container for a set of configuration constructs that are @@ -41,10 +46,18 @@ type Module struct { ModuleCalls map[string]*ModuleCall - ManagedResources map[string]*Resource - DataResources map[string]*Resource + ManagedResources map[string]*Resource + DataResources map[string]*Resource + EphemeralResources map[string]*Resource + Actions map[string]*Action - Moved []*Moved + Moved []*Moved + Removed []*Removed + Import []*Import + + Checks map[string]*Check + + Tests map[string]*TestFile } // File describes the contents of a single configuration file. @@ -75,10 +88,26 @@ type File struct { ModuleCalls []*ModuleCall - ManagedResources []*Resource - DataResources []*Resource + ManagedResources []*Resource + DataResources []*Resource + EphemeralResources []*Resource - Moved []*Moved + Moved []*Moved + Removed []*Removed + Import []*Import + + Checks []*Check + Actions []*Action +} + +// NewModuleWithTests matches NewModule except it will also load in the provided +// test files. +func NewModuleWithTests(primaryFiles, overrideFiles []*File, testFiles map[string]*TestFile) (*Module, hcl.Diagnostics) { + mod, diags := NewModule(primaryFiles, overrideFiles) + if mod != nil { + mod.Tests = testFiles + } + return mod, diags } // NewModule takes a list of primary files and a list of override files and @@ -99,8 +128,12 @@ func NewModule(primaryFiles, overrideFiles []*File) (*Module, hcl.Diagnostics) { Outputs: map[string]*Output{}, ModuleCalls: map[string]*ModuleCall{}, ManagedResources: map[string]*Resource{}, + EphemeralResources: map[string]*Resource{}, DataResources: map[string]*Resource{}, + Checks: map[string]*Check{}, ProviderMetas: map[addrs.Provider]*ProviderMeta{}, + Tests: map[string]*TestFile{}, + Actions: map[string]*Action{}, } // Process the required_providers blocks first, to ensure that all @@ -165,6 +198,8 @@ func (m *Module) ResourceByAddr(addr addrs.Resource) *Resource { return m.ManagedResources[key] case addrs.DataResourceMode: return m.DataResources[key] + case addrs.EphemeralResourceMode: + return m.EphemeralResources[key] default: return nil } @@ -196,8 +231,8 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { if m.CloudConfig != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Duplicate Terraform Cloud configurations", - Detail: fmt.Sprintf("A module may have only one 'cloud' block configuring Terraform Cloud. Terraform Cloud was previously configured at %s.", m.CloudConfig.DeclRange), + Summary: "Duplicate HCP Terraform configurations", + Detail: fmt.Sprintf("A module may have only one 'cloud' block configuring HCP Terraform or Terraform Enterprise. The 'cloud' block was previously configured at %s.", m.CloudConfig.DeclRange), Subject: &c.DeclRange, }) continue @@ -209,8 +244,8 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { if m.Backend != nil && m.CloudConfig != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Both a backend and Terraform Cloud configuration are present", - Detail: fmt.Sprintf("A module may declare either one 'cloud' block configuring Terraform Cloud OR one 'backend' block configuring a state backend. Terraform Cloud is configured at %s; a backend is configured at %s. Remove the backend block to configure Terraform Cloud.", m.CloudConfig.DeclRange, m.Backend.DeclRange), + Summary: "Both a backend and cloud configuration are present", + Detail: fmt.Sprintf("A module may declare either one 'cloud' block OR one 'backend' block configuring a state backend. The 'cloud' block is configured at %s; a backend is configured at %s. Remove the backend block to configure HCP Terraform or Terraform Enteprise.", m.CloudConfig.DeclRange, m.Backend.DeclRange), Subject: &m.Backend.DeclRange, }) } @@ -328,6 +363,9 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { } } + // Data sources can either be defined at the module root level, or within a + // single check block. We'll merge the data sources from both into the + // single module level DataResources map. for _, r := range file.DataResources { key := r.moduleUniqueKey() if existing, exists := m.DataResources[key]; exists { @@ -340,7 +378,66 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { continue } m.DataResources[key] = r + } + for _, r := range file.EphemeralResources { + key := r.moduleUniqueKey() + if existing, exists := m.EphemeralResources[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicate ephemeral %q configuration", existing.Type), + Detail: fmt.Sprintf("A %s ephemeral resource named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Type, existing.Name, existing.DeclRange), + Subject: &r.DeclRange, + }) + continue + } + m.EphemeralResources[key] = r + + // set the provider FQN for the resource + if r.ProviderConfigRef != nil { + r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) + } else { + // an invalid resource name (for e.g. "null resource" instead of + // "null_resource") can cause a panic down the line in addrs: + // https://github.com/hashicorp/terraform/issues/25560 + implied, err := addrs.ParseProviderPart(r.Addr().ImpliedProvider()) + if err == nil { + r.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + // We don't return a diagnostic because the invalid resource name + // will already have been caught. + } + } + + for _, c := range file.Checks { + if c.DataResource != nil { + key := c.DataResource.moduleUniqueKey() + if existing, exists := m.DataResources[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicate data %q configuration", existing.Type), + Detail: fmt.Sprintf("A %s data resource named %q was already declared at %s. Resource names must be unique per type in each module, including within check blocks.", existing.Type, existing.Name, existing.DeclRange), + Subject: &c.DataResource.DeclRange, + }) + continue + } + m.DataResources[key] = c.DataResource + } + + if existing, exists := m.Checks[c.Name]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicate check %q configuration", existing.Name), + Detail: fmt.Sprintf("A check block named %q was already declared at %s. Check blocks must be unique within each module.", existing.Name, existing.DeclRange), + Subject: &c.DeclRange, + }) + continue + } + m.Checks[c.Name] = c + } + + // Handle the provider associations for all data resources together. + for _, r := range m.DataResources { // set the provider FQN for the resource if r.ProviderConfigRef != nil { r.Provider = m.ProviderForLocalConfig(r.ProviderConfigAddr()) @@ -357,11 +454,75 @@ func (m *Module) appendFile(file *File) hcl.Diagnostics { } } - // "Moved" blocks just append, because they are all independent - // of one another at this level. (We handle any references between - // them at runtime.) + // "Moved" blocks just append, because they are all independent of one + // another at this level. (We handle any references between them at + // runtime.) m.Moved = append(m.Moved, file.Moved...) + m.Removed = append(m.Removed, file.Removed...) + + for _, i := range file.Import { + iTo, iToOK := parseImportToStatic(i.To) + for _, mi := range m.Import { + // Try to detect duplicate import targets. We need to see if the to + // address can be parsed statically. + miTo, miToOK := parseImportToStatic(mi.To) + if iToOK && miToOK && iTo.Equal(miTo) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicate import configuration for %q", i.ToResource), + Detail: fmt.Sprintf("An import block for the resource %q was already declared at %s. A resource can have only one import block.", i.ToResource, mi.DeclRange), + Subject: i.To.Range().Ptr(), + }) + } + } + + if i.ProviderConfigRef != nil { + i.Provider = m.ProviderForLocalConfig(addrs.LocalProviderConfig{ + LocalName: i.ProviderConfigRef.Name, + Alias: i.ProviderConfigRef.Alias, + }) + } else { + implied, err := addrs.ParseProviderPart(i.ToResource.Resource.ImpliedProvider()) + if err == nil { + i.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + // We don't return a diagnostic because the invalid resource name + // will already have been caught. + } + + m.Import = append(m.Import, i) + } + + for _, a := range file.Actions { + key := a.moduleUniqueKey() + if existing, exists := m.Actions[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Duplicate action %q configuration", existing.Type), + Detail: fmt.Sprintf("An action named %q was already declared at %s. Resource names must be unique per type in each module.", existing.Name, existing.DeclRange), + Subject: &a.DeclRange, + }) + continue + } + m.Actions[key] = a + + // set the provider FQN for the action + if a.ProviderConfigRef != nil { + a.Provider = m.ProviderForLocalConfig(a.ProviderConfigAddr()) + } else { + // an invalid resource name (for e.g. "null resource" instead of + // "null_resource") can cause a panic down the line in addrs: + // https://github.com/hashicorp/terraform/issues/25560 + implied, err := addrs.ParseProviderPart(a.Addr().ImpliedProvider()) + if err == nil { + a.Provider = m.ImpliedProviderForUnqualifiedType(implied) + } + // We don't return a diagnostic because the invalid resource name + // will already have been caught. + } + } + return diags } @@ -403,8 +564,8 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics { // though it can override cloud/backend blocks from _other_ files. diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Duplicate Terraform Cloud configurations", - Detail: fmt.Sprintf("A module may have only one 'cloud' block configuring Terraform Cloud. Terraform Cloud was previously configured at %s.", file.CloudConfigs[0].DeclRange), + Summary: "Duplicate HCP Terraform configurations", + Detail: fmt.Sprintf("A module may have only one 'cloud' block configuring HCP Terraform or Terraform Enterprise. The 'cloud' block was previously configured at %s.", file.CloudConfigs[0].DeclRange), Subject: &file.CloudConfigs[1].DeclRange, }) } @@ -543,10 +704,35 @@ func (m *Module) mergeFile(file *File) hcl.Diagnostics { }) } + for _, m := range file.Import { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot override 'import' blocks", + Detail: "Import blocks can appear only in normal files, not in override files.", + Subject: m.DeclRange.Ptr(), + }) + } + + for _, a := range file.Actions { + key := a.moduleUniqueKey() + existing, exists := m.Actions[key] + if !exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing resource to override", + Detail: fmt.Sprintf("There is no action named %q. An override file can only override a resource block defined in a primary configuration file.", a.Name), + Subject: &a.DeclRange, + }) + continue + } + mergeDiags := existing.merge(a, m.ProviderRequirements.RequiredProviders) + diags = append(diags, mergeDiags...) + } + return diags } -// gatherProviderLocalNames is a helper function that populatesA a map of +// gatherProviderLocalNames is a helper function that populates a map of // provider FQNs -> provider local names. This information is useful for // user-facing output, which should include both the FQN and LocalName. It must // only be populated after the module has been parsed. @@ -589,3 +775,59 @@ func (m *Module) ImpliedProviderForUnqualifiedType(pType string) addrs.Provider } return addrs.ImpliedProviderForUnqualifiedType(pType) } + +func (m *Module) CheckCoreVersionRequirements(path addrs.Module, sourceAddr addrs.ModuleSource) hcl.Diagnostics { + var diags hcl.Diagnostics + + for _, constraint := range m.CoreVersionConstraints { + // Before checking if the constraints are met, check that we are not using any prerelease fields as these + // are not currently supported. + var prereleaseDiags hcl.Diagnostics + for _, required := range constraint.Required { + if required.Prerelease() { + prereleaseDiags = prereleaseDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid required_version constraint", + Detail: fmt.Sprintf( + "Prerelease version constraints are not supported: %s. Remove the prerelease information from the constraint. Prerelease versions of terraform will match constraints using their version core only.", + required.String()), + Subject: constraint.DeclRange.Ptr(), + }) + } + } + + if len(prereleaseDiags) > 0 { + // There were some prerelease fields in the constraints. Don't check the constraints as they will + // fail, and populate the diagnostics for these constraints with the prerelease diagnostics. + diags = diags.Extend(prereleaseDiags) + continue + } + + if !constraint.Required.Check(tfversion.SemVer) { + switch { + case len(path) == 0: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported Terraform Core version", + Detail: fmt.Sprintf( + "This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", + tfversion.String(), + ), + Subject: constraint.DeclRange.Ptr(), + }) + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported Terraform Core version", + Detail: fmt.Sprintf( + "Module %s (from %s) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", + path, sourceAddr, tfversion.String(), + ), + Subject: constraint.DeclRange.Ptr(), + }) + } + } + } + + return diags +} diff --git a/internal/configs/module_call.go b/internal/configs/module_call.go index 3ec42ec01a..50b9289d90 100644 --- a/internal/configs/module_call.go +++ b/internal/configs/module_call.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -6,8 +9,9 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getmodules" + "github.com/hashicorp/terraform/internal/getmodules/moduleaddrs" ) // ModuleCall represents a "module" block in a module or file. @@ -76,9 +80,9 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno var addr addrs.ModuleSource var err error if haveVersionArg { - addr, err = addrs.ParseModuleSourceRegistry(mc.SourceAddrRaw) + addr, err = moduleaddrs.ParseModuleSourceRegistry(mc.SourceAddrRaw) } else { - addr, err = addrs.ParseModuleSource(mc.SourceAddrRaw) + addr, err = moduleaddrs.ParseModuleSource(mc.SourceAddrRaw) } mc.SourceAddr = addr if err != nil { @@ -96,7 +100,7 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno // though, mostly related to remote package sub-paths and local // paths. switch err := err.(type) { - case *getmodules.MaybeRelativePathErr: + case *moduleaddrs.MaybeRelativePathErr: diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid module source address", @@ -148,42 +152,15 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno } if attr, exists := content.Attributes["depends_on"]; exists { - deps, depsDiags := decodeDependsOn(attr) + deps, depsDiags := DecodeDependsOn(attr) diags = append(diags, depsDiags...) mc.DependsOn = append(mc.DependsOn, deps...) } if attr, exists := content.Attributes["providers"]; exists { - seen := make(map[string]hcl.Range) - pairs, pDiags := hcl.ExprMap(attr.Expr) - diags = append(diags, pDiags...) - for _, pair := range pairs { - key, keyDiags := decodeProviderConfigRef(pair.Key, "providers") - diags = append(diags, keyDiags...) - value, valueDiags := decodeProviderConfigRef(pair.Value, "providers") - diags = append(diags, valueDiags...) - if keyDiags.HasErrors() || valueDiags.HasErrors() { - continue - } - - matchKey := key.String() - if prev, exists := seen[matchKey]; exists { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Duplicate provider address", - Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev), - Subject: pair.Value.Range().Ptr(), - }) - continue - } - - rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range()) - seen[matchKey] = rng - mc.Providers = append(mc.Providers, PassedProviderConfig{ - InChild: key, - InParent: value, - }) - } + providers, providerDiags := decodePassedProviderConfigs(attr) + diags = append(diags, providerDiags...) + mc.Providers = append(mc.Providers, providers...) } var seenEscapeBlock *hcl.Block @@ -243,6 +220,43 @@ type PassedProviderConfig struct { InParent *ProviderConfigRef } +func decodePassedProviderConfigs(attr *hcl.Attribute) ([]PassedProviderConfig, hcl.Diagnostics) { + var diags hcl.Diagnostics + var providers []PassedProviderConfig + + seen := make(map[string]hcl.Range) + pairs, pDiags := hcl.ExprMap(attr.Expr) + diags = append(diags, pDiags...) + for _, pair := range pairs { + key, keyDiags := decodeProviderConfigRef(pair.Key, "providers") + diags = append(diags, keyDiags...) + value, valueDiags := decodeProviderConfigRef(pair.Value, "providers") + diags = append(diags, valueDiags...) + if keyDiags.HasErrors() || valueDiags.HasErrors() { + continue + } + + matchKey := key.String() + if prev, exists := seen[matchKey]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider address", + Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev), + Subject: pair.Value.Range().Ptr(), + }) + continue + } + + rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range()) + seen[matchKey] = rng + providers = append(providers, PassedProviderConfig{ + InChild: key, + InParent: value, + }) + } + return providers, diags +} + var moduleBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { diff --git a/internal/configs/module_call_test.go b/internal/configs/module_call_test.go index af2269dca0..5f64f65506 100644 --- a/internal/configs/module_call_test.go +++ b/internal/configs/module_call_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -6,7 +9,9 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getmodules/moduleaddrs" ) func TestLoadModuleCall(t *testing.T) { @@ -178,7 +183,7 @@ func TestModuleSourceAddrEntersNewPackage(t *testing.T) { for _, test := range tests { t.Run(test.Addr, func(t *testing.T) { - addr, err := addrs.ParseModuleSource(test.Addr) + addr, err := moduleaddrs.ParseModuleSource(test.Addr) if err != nil { t.Fatalf("parsing failed for %q: %s", test.Addr, err) } diff --git a/internal/configs/module_merge.go b/internal/configs/module_merge.go index 2381a88d11..f856f1abed 100644 --- a/internal/configs/module_merge.go +++ b/internal/configs/module_merge.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -46,6 +49,10 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics { v.Sensitive = ov.Sensitive v.SensitiveSet = ov.SensitiveSet } + if ov.EphemeralSet { + v.Ephemeral = ov.Ephemeral + v.EphemeralSet = ov.EphemeralSet + } if ov.Default != cty.NilVal { v.Default = ov.Default } @@ -70,7 +77,7 @@ func (v *Variable) merge(ov *Variable) hcl.Diagnostics { // but in particular might be user-observable in the edge case where the // literal value in config could've been converted to the overridden type // constraint but the converted value cannot. In practice, this situation - // should be rare since most of our conversions are interchangable. + // should be rare since most of our conversions are interchangeable. if v.Default != cty.NilVal { val, err := convert.Convert(v.Default, v.ConstraintType) if err != nil { @@ -145,6 +152,10 @@ func (o *Output) merge(oo *Output) hcl.Diagnostics { o.Sensitive = oo.Sensitive o.SensitiveSet = oo.SensitiveSet } + if oo.EphemeralSet { + o.Ephemeral = oo.Ephemeral + o.EphemeralSet = oo.EphemeralSet + } // We don't allow depends_on to be overridden because that is likely to // cause confusing misbehavior. @@ -190,12 +201,12 @@ func (mc *ModuleCall) merge(omc *ModuleCall) hcl.Diagnostics { // We don't allow depends_on to be overridden because that is likely to // cause confusing misbehavior. - if len(mc.DependsOn) != 0 { + if len(omc.DependsOn) != 0 { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unsupported override", Detail: "The depends_on argument may not be overridden.", - Subject: mc.DependsOn[0].SourceRange().Ptr(), // the first item is the closest range we have + Subject: omc.DependsOn[0].SourceRange().Ptr(), // the first item is the closest range we have }) } @@ -252,6 +263,9 @@ func (r *Resource) merge(or *Resource, rps map[string]*RequiredProvider) hcl.Dia if len(or.Managed.Provisioners) != 0 { r.Managed.Provisioners = or.Managed.Provisioners } + if len(or.Managed.ActionTriggers) != 0 { + r.Managed.ActionTriggers = or.Managed.ActionTriggers + } } r.Config = MergeBodies(r.Config, or.Config) @@ -269,3 +283,39 @@ func (r *Resource) merge(or *Resource, rps map[string]*RequiredProvider) hcl.Dia return diags } + +// Actions +func (a *Action) merge(oa *Action, rps map[string]*RequiredProvider) hcl.Diagnostics { + var diags hcl.Diagnostics + + if oa.Count != nil { + a.Count = oa.Count + } + if oa.ForEach != nil { + a.ForEach = oa.ForEach + } + + if oa.ProviderConfigRef != nil { + a.ProviderConfigRef = oa.ProviderConfigRef + if existing, exists := rps[oa.ProviderConfigRef.Name]; exists { + a.Provider = existing.Type + } else { + a.Provider = addrs.ImpliedProviderForUnqualifiedType(a.ProviderConfigRef.Name) + } + } + + a.Config = MergeBodies(a.Config, oa.Config) + + // We don't allow depends_on to be overridden because that is likely to + // cause confusing misbehavior. + if len(oa.DependsOn) != 0 { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported override", + Detail: "The depends_on argument may not be overridden.", + Subject: oa.DependsOn[0].SourceRange().Ptr(), // the first item is the closest range we have + }) + } + + return diags +} diff --git a/internal/configs/module_merge_body.go b/internal/configs/module_merge_body.go index 6ae64a2a9f..91e07c447e 100644 --- a/internal/configs/module_merge_body.go +++ b/internal/configs/module_merge_body.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/module_merge_test.go b/internal/configs/module_merge_test.go index b5d7fb368c..9a9f6b1b06 100644 --- a/internal/configs/module_merge_test.go +++ b/internal/configs/module_merge_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -6,8 +9,9 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" - "github.com/hashicorp/terraform/internal/addrs" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" ) func TestModuleOverrideVariable(t *testing.T) { @@ -117,6 +121,26 @@ func TestModuleOverrideModule(t *testing.T) { Byte: 17, }, }, + DependsOn: []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "null_resource", + SrcRange: hcl.Range{ + Filename: "testdata/valid-modules/override-module/primary.tf", + Start: hcl.Pos{Line: 11, Column: 17, Byte: 149}, + End: hcl.Pos{Line: 11, Column: 30, Byte: 162}, + }, + }, + hcl.TraverseAttr{ + Name: "test", + SrcRange: hcl.Range{ + Filename: "testdata/valid-modules/override-module/primary.tf", + Start: hcl.Pos{Line: 11, Column: 30, Byte: 162}, + End: hcl.Pos{Line: 11, Column: 35, Byte: 167}, + }, + }, + }, + }, Providers: []PassedProviderConfig{ { InChild: &ProviderConfigRef{ @@ -330,3 +354,70 @@ func TestModuleOverrideIgnoreAllChanges(t *testing.T) { t.Fatalf("wrong result: expected r.Managed.IgnoreAllChanges to be true") } } + +// This tests the override behavior of action blocks and action_triggers inside resources. +func TestModuleOverride_action_and_trigger(t *testing.T) { + mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-action-and-trigger") + assertNoDiagnostics(t, diags) + + if len(mod.Actions) != 2 { + t.Fatalf("wrong number of actions: %d\n", len(mod.Actions)) + } + + // verify that the action has attr foo = baz (override) + got := mod.Actions["action.test_action.test"] + want := &Action{ + Name: "test", + Type: "test_action", + Config: nil, + Count: nil, + ForEach: nil, + DependsOn: nil, + ProviderConfigRef: nil, + Provider: addrs.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "test"), + DeclRange: hcl.Range{ + Filename: "testdata/valid-modules/override-action-and-trigger/main.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + TypeRange: hcl.Range{ + Filename: "testdata/valid-modules/override-action-and-trigger/main.tf", + Start: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + End: hcl.Pos{Line: 1, Column: 21, Byte: 20}, + }, + } + + // We're going to extract and nil out our hcl.Body here because DeepEqual + // is not a useful way to assert on that. + gotConfig := got.Config + got.Config = nil + + assertResultDeepEqual(t, got, want) + + // now to check that config + type content struct { + Foo *string `hcl:"foo"` + } + var gotArgs content + diags = gohcl.DecodeBody(gotConfig, nil, &gotArgs) + assertNoDiagnostics(t, diags) + + wantArgs := content{ + Foo: stringPtr("baz"), + } + assertResultDeepEqual(t, gotArgs, wantArgs) + + if _, exists := mod.ManagedResources["test_instance.test"]; !exists { + t.Fatalf("no resource 'test_instance.test'") + } + if len(mod.ManagedResources) != 1 { + t.Fatalf("wrong number of managed resources in result %d; want 1", len(mod.ManagedResources)) + } + + r := mod.ManagedResources["test_instance.test"].Managed + assertResultDeepEqual(t, len(r.ActionTriggers), 1) + + // verify the resource action trigger event changed + at := mod.ManagedResources["test_instance.test"].Managed.ActionTriggers[0] + assertResultDeepEqual(t, at.Events, []ActionTriggerEvent{AfterDestroy}) +} diff --git a/internal/configs/module_test.go b/internal/configs/module_test.go index 5a74dda3e3..13e75f6ffe 100644 --- a/internal/configs/module_test.go +++ b/internal/configs/module_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -408,7 +411,7 @@ func TestModule_cloud_override(t *testing.T) { func TestModule_cloud_duplicate_overrides(t *testing.T) { _, diags := testModuleFromDir("testdata/invalid-modules/override-cloud-duplicates") - want := `Duplicate Terraform Cloud configurations` + want := `Duplicate HCP Terraform configurations` if got := diags.Error(); !strings.Contains(got, want) { t.Fatalf("expected module error to contain %q\nerror was:\n%s", want, got) } diff --git a/internal/configs/moved.go b/internal/configs/moved.go index 5cfbd5dfb0..ccc143b9d7 100644 --- a/internal/configs/moved.go +++ b/internal/configs/moved.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/moved_test.go b/internal/configs/moved_test.go index 433525d28c..54599a3410 100644 --- a/internal/configs/moved_test.go +++ b/internal/configs/moved_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/named_values.go b/internal/configs/named_values.go index bb0698071e..c9c92e45e0 100644 --- a/internal/configs/named_values.go +++ b/internal/configs/named_values.go @@ -1,16 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "fmt" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/typeexpr" + "github.com/hashicorp/terraform/internal/tfdiags" ) // A consistent detail message for all "not a valid identifier" diagnostics. @@ -32,9 +36,11 @@ type Variable struct { ParsingMode VariableParsingMode Validations []*CheckRule Sensitive bool + Ephemeral bool DescriptionSet bool SensitiveSet bool + EphemeralSet bool // Nullable indicates that null is a valid value for this variable. Setting // Nullable to false means that the module can expect this variable to @@ -117,6 +123,12 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno v.SensitiveSet = true } + if attr, exists := content.Attributes["ephemeral"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Ephemeral) + diags = append(diags, valDiags...) + v.EphemeralSet = true + } + if attr, exists := content.Attributes["nullable"]; exists { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Nullable) diags = append(diags, valDiags...) @@ -140,8 +152,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno if v.ConstraintType != cty.NilType { var err error // If the type constraint has defaults, we must apply those - // defaults to the variable default value before type conversion. - if v.TypeDefaults != nil { + // defaults to the variable default value before type conversion, + // unless the default value is null. Null is excluded from the + // type default application process as a special case, to allow + // nullable variables to have a null default value. + if v.TypeDefaults != nil && !val.IsNull() { val = v.TypeDefaults.Apply(val) } val, err = convert.Convert(val, v.ConstraintType) @@ -149,8 +164,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid default value for variable", - Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), - Subject: attr.Expr.Range().Ptr(), + Detail: fmt.Sprintf( + "This default value is not compatible with the variable's type constraint: %s.", + tfdiags.FormatError(err), + ), + Subject: attr.Expr.Range().Ptr(), }) val = cty.DynamicVal } @@ -172,10 +190,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno switch block.Type { case "validation": - vv, moreDiags := decodeVariableValidationBlock(v.Name, block, override) + vv, moreDiags := decodeCheckRuleBlock(block, override) diags = append(diags, moreDiags...) - v.Validations = append(v.Validations, vv) + diags = append(diags, checkVariableValidationBlock(v.Name, vv)...) + v.Validations = append(v.Validations, vv) default: // The above cases should be exhaustive for all block types // defined in variableBlockSchema @@ -318,72 +337,6 @@ func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnosti } } -// decodeVariableValidationBlock is a wrapper around decodeCheckRuleBlock -// that imposes the additional rule that the condition expression can refer -// only to an input variable of the given name. -func decodeVariableValidationBlock(varName string, block *hcl.Block, override bool) (*CheckRule, hcl.Diagnostics) { - vv, diags := decodeCheckRuleBlock(block, override) - if vv.Condition != nil { - // The validation condition can only refer to the variable itself, - // to ensure that the variable declaration can't create additional - // edges in the dependency graph. - goodRefs := 0 - for _, traversal := range vv.Condition.Variables() { - ref, moreDiags := addrs.ParseRef(traversal) - if !moreDiags.HasErrors() { - if addr, ok := ref.Subject.(addrs.InputVariable); ok { - if addr.Name == varName { - goodRefs++ - continue // Reference is valid - } - } - } - // If we fall out here then the reference is invalid. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid reference in variable validation", - Detail: fmt.Sprintf("The condition for variable %q can only refer to the variable itself, using var.%s.", varName, varName), - Subject: traversal.SourceRange().Ptr(), - }) - } - if goodRefs < 1 { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid variable validation condition", - Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName), - Subject: vv.Condition.Range().Ptr(), - }) - } - } - - if vv.ErrorMessage != nil { - // The same applies to the validation error message, except that - // references are not required. A string literal is a valid error - // message. - goodRefs := 0 - for _, traversal := range vv.ErrorMessage.Variables() { - ref, moreDiags := addrs.ParseRef(traversal) - if !moreDiags.HasErrors() { - if addr, ok := ref.Subject.(addrs.InputVariable); ok { - if addr.Name == varName { - goodRefs++ - continue // Reference is valid - } - } - } - // If we fall out here then the reference is invalid. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid reference in variable validation", - Detail: fmt.Sprintf("The error message for variable %q can only refer to the variable itself, using var.%s.", varName, varName), - Subject: traversal.SourceRange().Ptr(), - }) - } - } - - return vv, diags -} - // Output represents an "output" block in a module or file. type Output struct { Name string @@ -391,11 +344,13 @@ type Output struct { Expr hcl.Expression DependsOn []hcl.Traversal Sensitive bool + Ephemeral bool Preconditions []*CheckRule DescriptionSet bool SensitiveSet bool + EphemeralSet bool DeclRange hcl.Range } @@ -441,8 +396,14 @@ func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostic o.SensitiveSet = true } + if attr, exists := content.Attributes["ephemeral"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Ephemeral) + diags = append(diags, valDiags...) + o.EphemeralSet = true + } + if attr, exists := content.Attributes["depends_on"]; exists { - deps, depsDiags := decodeDependsOn(attr) + deps, depsDiags := DecodeDependsOn(attr) diags = append(diags, depsDiags...) o.DependsOn = append(o.DependsOn, deps...) } @@ -532,6 +493,9 @@ var variableBlockSchema = &hcl.BodySchema{ { Name: "sensitive", }, + { + Name: "ephemeral", + }, { Name: "nullable", }, @@ -558,9 +522,38 @@ var outputBlockSchema = &hcl.BodySchema{ { Name: "sensitive", }, + { + Name: "ephemeral", + }, }, Blocks: []hcl.BlockHeaderSchema{ {Type: "precondition"}, {Type: "postcondition"}, }, } + +func checkVariableValidationBlock(varName string, vv *CheckRule) hcl.Diagnostics { + var diags hcl.Diagnostics + + if vv.Condition != nil { + // The validation condition must include a reference to the variable itself + for _, traversal := range vv.Condition.Variables() { + ref, moreDiags := addrs.ParseRef(traversal) + if !moreDiags.HasErrors() { + if addr, ok := ref.Subject.(addrs.InputVariable); ok { + if addr.Name == varName { + return nil + } + } + } + } + + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid variable validation condition", + Detail: fmt.Sprintf("The condition for variable %q must refer to var.%s in order to test incoming values.", varName, varName), + Subject: vv.Condition.Range().Ptr(), + }) + } + return nil +} diff --git a/internal/configs/named_values_test.go b/internal/configs/named_values_test.go new file mode 100644 index 0000000000..0626157c03 --- /dev/null +++ b/internal/configs/named_values_test.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func TestVariableInvalidDefault(t *testing.T) { + src := ` + variable foo { + type = map(object({ + foo = bool + })) + + default = { + "thingy" = { + foo = "string where bool is expected" + } + } + } + ` + + hclF, diags := hclsyntax.ParseConfig([]byte(src), "test.tf", hcl.InitialPos) + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + + _, diags = parseConfigFile(hclF.Body, nil, false, false) + if !diags.HasErrors() { + t.Fatal("unexpected success; want error") + } + + for _, diag := range diags { + if diag.Severity != hcl.DiagError { + continue + } + if diag.Summary != "Invalid default value for variable" { + t.Errorf("unexpected diagnostic summary: %q", diag.Summary) + continue + } + if got, want := diag.Detail, `This default value is not compatible with the variable's type constraint: ["thingy"].foo: a bool is required.`; got != want { + t.Errorf("wrong diagnostic detault\ngot: %s\nwant: %s", got, want) + } + } +} diff --git a/internal/configs/parser.go b/internal/configs/parser.go index 5a4b81078a..91e1325df8 100644 --- a/internal/configs/parser.go +++ b/internal/configs/parser.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -118,3 +121,10 @@ func (p *Parser) ForceFileSource(filename string, src []byte) { func (p *Parser) AllowLanguageExperiments(allowed bool) { p.allowExperiments = allowed } + +// AllowsLanguageExperiments returns the value most recently passed to +// [Parser.AllowLanguageExperiments], or false if that method has not been +// called on this object. +func (p *Parser) AllowsLanguageExperiments() bool { + return p.allowExperiments +} diff --git a/internal/configs/parser_config.go b/internal/configs/parser_config.go index 2e08580b5b..f7ffcfcfa2 100644 --- a/internal/configs/parser_config.go +++ b/internal/configs/parser_config.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -29,13 +32,48 @@ func (p *Parser) LoadConfigFileOverride(path string) (*File, hcl.Diagnostics) { return p.loadConfigFile(path, true) } -func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnostics) { - +// LoadTestFile reads the file at the given path and parses it as a Terraform +// test file. +// +// It references the same LoadHCLFile as LoadConfigFile, so inherits the same +// syntax selection behaviours. +func (p *Parser) LoadTestFile(path string) (*TestFile, hcl.Diagnostics) { body, diags := p.LoadHCLFile(path) if body == nil { return nil, diags } + test, testDiags := loadTestFile(body) + diags = append(diags, testDiags...) + return test, diags +} + +// LoadMockDataFile reads the file at the given path and parses it as a +// Terraform mock data file. +// +// It references the same LoadHCLFile as LoadConfigFile, so inherits the same +// syntax selection behaviours. +func (p *Parser) LoadMockDataFile(path string, useForPlanDefault bool) (*MockData, hcl.Diagnostics) { + body, diags := p.LoadHCLFile(path) + if body == nil { + return nil, diags + } + + data, dataDiags := decodeMockDataBody(body, useForPlanDefault, MockDataFileOverrideSource) + diags = append(diags, dataDiags...) + return data, diags +} + +func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnostics) { + body, diags := p.LoadHCLFile(path) + if body == nil { + return nil, diags + } + + return parseConfigFile(body, diags, override, p.allowExperiments) +} + +func parseConfigFile(body hcl.Body, diags hcl.Diagnostics, override, allowExperiments bool) (*File, hcl.Diagnostics) { file := &File{} var reqDiags hcl.Diagnostics @@ -45,7 +83,7 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost // We'll load the experiments first because other decoding logic in the // loop below might depend on these experiments. var expDiags hcl.Diagnostics - file.ActiveExperiments, expDiags = sniffActiveExperiments(body, p.allowExperiments) + file.ActiveExperiments, expDiags = sniffActiveExperiments(body, allowExperiments) diags = append(diags, expDiags...) content, contentDiags := body.Content(configFileSchema) @@ -82,7 +120,9 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost case "required_providers": reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock) diags = append(diags, reqsDiags...) - file.RequiredProviders = append(file.RequiredProviders, reqs) + if reqs != nil { + file.RequiredProviders = append(file.RequiredProviders, reqs) + } case "provider_meta": providerCfg, cfgDiags := decodeProviderMetaBlock(innerBlock) @@ -109,7 +149,7 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost }) case "provider": - cfg, cfgDiags := decodeProviderBlock(block) + cfg, cfgDiags := decodeProviderBlock(block, false) diags = append(diags, cfgDiags...) if cfg != nil { file.ProviderConfigs = append(file.ProviderConfigs, cfg) @@ -142,19 +182,26 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost } case "resource": - cfg, cfgDiags := decodeResourceBlock(block, override) + cfg, cfgDiags := decodeResourceBlock(block, override, allowExperiments) diags = append(diags, cfgDiags...) if cfg != nil { file.ManagedResources = append(file.ManagedResources, cfg) } case "data": - cfg, cfgDiags := decodeDataBlock(block, override) + cfg, cfgDiags := decodeDataBlock(block, override, false) diags = append(diags, cfgDiags...) if cfg != nil { file.DataResources = append(file.DataResources, cfg) } + case "ephemeral": + cfg, cfgDiags := decodeEphemeralBlock(block, override) + diags = append(diags, cfgDiags...) + if cfg != nil { + file.EphemeralResources = append(file.EphemeralResources, cfg) + } + case "moved": cfg, cfgDiags := decodeMovedBlock(block) diags = append(diags, cfgDiags...) @@ -162,6 +209,36 @@ func (p *Parser) loadConfigFile(path string, override bool) (*File, hcl.Diagnost file.Moved = append(file.Moved, cfg) } + case "removed": + cfg, cfgDiags := decodeRemovedBlock(block) + diags = append(diags, cfgDiags...) + if cfg != nil { + file.Removed = append(file.Removed, cfg) + } + + case "import": + cfg, cfgDiags := decodeImportBlock(block) + diags = append(diags, cfgDiags...) + if cfg != nil { + file.Import = append(file.Import, cfg) + } + + case "check": + cfg, cfgDiags := decodeCheckBlock(block, override) + diags = append(diags, cfgDiags...) + if cfg != nil { + file.Checks = append(file.Checks, cfg) + } + + case "action": + if allowExperiments { + cfg, cfgDiags := decodeActionBlock(block) + diags = append(diags, cfgDiags...) + if cfg != nil { + file.Actions = append(file.Actions, cfg) + } + } + default: // Should never happen because the above cases should be exhaustive // for all block type names in our schema. @@ -249,9 +326,27 @@ var configFileSchema = &hcl.BodySchema{ Type: "data", LabelNames: []string{"type", "name"}, }, + { + Type: "ephemeral", + LabelNames: []string{"type", "name"}, + }, + { + Type: "action", + LabelNames: []string{"type", "name"}, + }, { Type: "moved", }, + { + Type: "removed", + }, + { + Type: "import", + }, + { + Type: "check", + LabelNames: []string{"name"}, + }, }, } diff --git a/internal/configs/parser_config_dir.go b/internal/configs/parser_config_dir.go index 2923af93a0..59d4d9d100 100644 --- a/internal/configs/parser_config_dir.go +++ b/internal/configs/parser_config_dir.go @@ -1,14 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "fmt" "os" + "path" "path/filepath" "strings" "github.com/hashicorp/hcl/v2" ) +const ( + DefaultTestDirectory = "tests" +) + // LoadConfigDir reads the .tf and .tf.json files in the given directory // as config files (using LoadConfigFile) and then combines these files into // a single Module. @@ -29,7 +37,7 @@ import ( // .tf files are parsed using the HCL native syntax while .tf.json files are // parsed using the HCL JSON syntax. func (p *Parser) LoadConfigDir(path string) (*Module, hcl.Diagnostics) { - primaryPaths, overridePaths, diags := p.dirFiles(path) + primaryPaths, overridePaths, _, diags := p.dirFiles(path, "") if diags.HasErrors() { return nil, diags } @@ -47,20 +55,97 @@ func (p *Parser) LoadConfigDir(path string) (*Module, hcl.Diagnostics) { return mod, diags } +// LoadConfigDirWithTests matches LoadConfigDir, but the return Module also +// contains any relevant .tftest.hcl files. +func (p *Parser) LoadConfigDirWithTests(path string, testDirectory string) (*Module, hcl.Diagnostics) { + primaryPaths, overridePaths, testPaths, diags := p.dirFiles(path, testDirectory) + if diags.HasErrors() { + return nil, diags + } + + primary, fDiags := p.loadFiles(primaryPaths, false) + diags = append(diags, fDiags...) + override, fDiags := p.loadFiles(overridePaths, true) + diags = append(diags, fDiags...) + tests, fDiags := p.loadTestFiles(path, testPaths) + diags = append(diags, fDiags...) + + mod, modDiags := NewModuleWithTests(primary, override, tests) + diags = append(diags, modDiags...) + + mod.SourceDir = path + + return mod, diags +} + +func (p *Parser) LoadMockDataDir(dir string, useForPlanDefault bool, source hcl.Range) (*MockData, hcl.Diagnostics) { + var diags hcl.Diagnostics + + infos, err := p.fs.ReadDir(dir) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to read mock data directory", + Detail: fmt.Sprintf("Mock data directory %s does not exist or cannot be read.", dir), + Subject: source.Ptr(), + }) + return nil, diags + } + + var files []string + for _, info := range infos { + if info.IsDir() { + // We only care about terraform configuration files. + continue + } + + name := info.Name() + if !(strings.HasSuffix(name, ".tfmock.hcl") || strings.HasSuffix(name, ".tfmock.json")) { + continue + } + + if IsIgnoredFile(name) { + continue + } + + files = append(files, filepath.Join(dir, name)) + } + + var data *MockData + for _, file := range files { + current, currentDiags := p.LoadMockDataFile(file, useForPlanDefault) + diags = append(diags, currentDiags...) + if data != nil { + diags = append(diags, data.Merge(current, false)...) + continue + } + data = current + } + return data, diags +} + // ConfigDirFiles returns lists of the primary and override files configuration // files in the given directory. // // If the given directory does not exist or cannot be read, error diagnostics // are returned. If errors are returned, the resulting lists may be incomplete. func (p Parser) ConfigDirFiles(dir string) (primary, override []string, diags hcl.Diagnostics) { - return p.dirFiles(dir) + primary, override, _, diags = p.dirFiles(dir, "") + return primary, override, diags +} + +// ConfigDirFilesWithTests matches ConfigDirFiles except it also returns the +// paths to any test files within the module. +func (p Parser) ConfigDirFilesWithTests(dir string, testDirectory string) (primary, override, tests []string, diags hcl.Diagnostics) { + return p.dirFiles(dir, testDirectory) } // IsConfigDir determines whether the given path refers to a directory that // exists and contains at least one Terraform config file (with a .tf or -// .tf.json extension.) +// .tf.json extension.). Note, we explicitely exclude checking for tests here +// as tests must live alongside actual .tf config files. func (p *Parser) IsConfigDir(path string) bool { - primaryPaths, overridePaths, _ := p.dirFiles(path) + primaryPaths, overridePaths, _, _ := p.dirFiles(path, "") return (len(primaryPaths) + len(overridePaths)) > 0 } @@ -85,7 +170,66 @@ func (p *Parser) loadFiles(paths []string, override bool) ([]*File, hcl.Diagnost return files, diags } -func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Diagnostics) { +// dirFiles finds Terraform configuration files within dir, splitting them into +// primary and override files based on the filename. +// +// If testsDir is not empty, dirFiles will also retrieve Terraform testing files +// both directly within dir and within testsDir as a subdirectory of dir. In +// this way, testsDir acts both as a direction to retrieve test files within the +// main direction and as the location for additional test files. +func (p *Parser) dirFiles(dir string, testsDir string) (primary, override, tests []string, diags hcl.Diagnostics) { + includeTests := len(testsDir) > 0 + + if includeTests { + testPath := path.Join(dir, testsDir) + + infos, err := p.fs.ReadDir(testPath) + if err != nil { + // Then we couldn't read from the testing directory for some reason. + + if os.IsNotExist(err) { + // Then this means the testing directory did not exist. + // We won't actually stop loading the rest of the configuration + // for this, we will add a warning to explain to the user why + // test files weren't processed but leave it at that. + if testsDir != DefaultTestDirectory { + // We'll only add the warning if a directory other than the + // default has been requested. If the user is just loading + // the default directory then we have no expectation that + // it should actually exist. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Test directory does not exist", + Detail: fmt.Sprintf("Requested test directory %s does not exist.", testPath), + }) + } + } else { + // Then there is some other reason we couldn't load. We will + // treat this as a full error. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to read test directory", + Detail: fmt.Sprintf("Test directory %s could not be read: %v.", testPath, err), + }) + + // We'll also stop loading the rest of the config for this. + return + } + + } else { + for _, testInfo := range infos { + if testInfo.IsDir() || IsIgnoredFile(testInfo.Name()) { + continue + } + + if strings.HasSuffix(testInfo.Name(), ".tftest.hcl") || strings.HasSuffix(testInfo.Name(), ".tftest.json") { + tests = append(tests, filepath.Join(testPath, testInfo.Name())) + } + } + } + + } + infos, err := p.fs.ReadDir(dir) if err != nil { diags = append(diags, &hcl.Diagnostic{ @@ -98,7 +242,7 @@ func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Dia for _, info := range infos { if info.IsDir() { - // We only care about files + // We only care about terraform configuration files. continue } @@ -108,6 +252,13 @@ func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Dia continue } + if ext == ".tftest.hcl" || ext == ".tftest.json" { + if includeTests { + tests = append(tests, filepath.Join(dir, name)) + } + continue + } + baseName := name[:len(name)-len(ext)] // strip extension isOverride := baseName == "override" || strings.HasSuffix(baseName, "_override") @@ -122,6 +273,32 @@ func (p *Parser) dirFiles(dir string) (primary, override []string, diags hcl.Dia return } +func (p *Parser) loadTestFiles(basePath string, paths []string) (map[string]*TestFile, hcl.Diagnostics) { + var diags hcl.Diagnostics + + tfs := make(map[string]*TestFile) + for _, path := range paths { + tf, fDiags := p.LoadTestFile(path) + diags = append(diags, fDiags...) + if tf != nil { + // We index test files relative to the module they are testing, so + // the key is the relative path between basePath and path. + relPath, err := filepath.Rel(basePath, path) + if err != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Failed to calculate relative path", + Detail: fmt.Sprintf("Terraform could not calculate the relative path for test file %s and it has been skipped: %s", path, err), + }) + continue + } + tfs[relPath] = tf + } + } + + return tfs, diags +} + // fileExt returns the Terraform configuration extension of the given // path, or a blank string if it is not a recognized extension. func fileExt(path string) string { @@ -129,6 +306,10 @@ func fileExt(path string) string { return ".tf" } else if strings.HasSuffix(path, ".tf.json") { return ".tf.json" + } else if strings.HasSuffix(path, ".tftest.hcl") { + return ".tftest.hcl" + } else if strings.HasSuffix(path, ".tftest.json") { + return ".tftest.json" } else { return "" } @@ -143,21 +324,21 @@ func IsIgnoredFile(name string) bool { } // IsEmptyDir returns true if the given filesystem path contains no Terraform -// configuration files. +// configuration or test files. // // Unlike the methods of the Parser type, this function always consults the // real filesystem, and thus it isn't appropriate to use when working with // configuration loaded from a plan file. -func IsEmptyDir(path string) (bool, error) { +func IsEmptyDir(path, testDir string) (bool, error) { if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { return true, nil } p := NewParser(nil) - fs, os, diags := p.dirFiles(path) + fs, os, tests, diags := p.dirFiles(path, testDir) if diags.HasErrors() { return false, diags } - return len(fs) == 0 && len(os) == 0, nil + return len(fs) == 0 && len(os) == 0 && len(tests) == 0, nil } diff --git a/internal/configs/parser_config_dir_test.go b/internal/configs/parser_config_dir_test.go index da2f23c9fa..53d2145024 100644 --- a/internal/configs/parser_config_dir_test.go +++ b/internal/configs/parser_config_dir_test.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "fmt" "io/ioutil" + "os" "path/filepath" "testing" @@ -70,6 +74,12 @@ func TestParserLoadConfigDirSuccess(t *testing.T) { if mod.SourceDir != path { t.Errorf("wrong SourceDir value %q; want %s", mod.SourceDir, path) } + + if len(mod.Tests) > 0 { + // We only load tests when requested, and we didn't request this + // time. + t.Errorf("should not have loaded tests, but found %d", len(mod.Tests)) + } }) } @@ -104,6 +114,152 @@ func TestParserLoadConfigDirSuccess(t *testing.T) { } +func TestParserLoadConfigDirWithTests(t *testing.T) { + directories := []string{ + "testdata/valid-modules/with-tests", + "testdata/valid-modules/with-tests-expect-failures", + "testdata/valid-modules/with-tests-nested", + "testdata/valid-modules/with-tests-very-nested", + "testdata/valid-modules/with-tests-json", + "testdata/valid-modules/with-mocks", + } + + for _, directory := range directories { + t.Run(directory, func(t *testing.T) { + + testDirectory := DefaultTestDirectory + if directory == "testdata/valid-modules/with-tests-very-nested" { + testDirectory = "very/nested" + } + + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests(directory, testDirectory) + if len(diags) > 0 { // We don't want any warnings or errors. + t.Errorf("unexpected diagnostics") + for _, diag := range diags { + t.Logf("- %s", diag) + } + } + + if len(mod.Tests) != 2 { + t.Errorf("incorrect number of test files found: %d", len(mod.Tests)) + } + }) + } +} + +func TestParserLoadTestFiles_Invalid(t *testing.T) { + + tcs := map[string][]string{ + "duplicate_data_overrides": { + "duplicate_data_overrides.tftest.hcl:7,3-16: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:2,3-16.", + "duplicate_data_overrides.tftest.hcl:18,1-14: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:13,1-14.", + "duplicate_data_overrides.tftest.hcl:29,3-16: Duplicate override_data block; An override_data block targeting data.aws_instance.test has already been defined at duplicate_data_overrides.tftest.hcl:24,3-16.", + }, + "duplicate_mixed_providers": { + "duplicate_mixed_providers.tftest.hcl:3,1-20: Duplicate provider block; A provider for aws is already defined at duplicate_mixed_providers.tftest.hcl:1,10-15.", + "duplicate_mixed_providers.tftest.hcl:9,1-20: Duplicate provider block; A provider for aws.test is already defined at duplicate_mixed_providers.tftest.hcl:5,10-15.", + }, + "duplicate_mock_data_sources": { + "duplicate_mock_data_sources.tftest.hcl:7,13-27: Duplicate mock_data block; A mock_data block for aws_instance has already been defined at duplicate_mock_data_sources.tftest.hcl:3,3-27.", + }, + "duplicate_mock_providers": { + "duplicate_mock_providers.tftest.hcl:3,1-20: Duplicate provider block; A provider for aws is already defined at duplicate_mock_providers.tftest.hcl:1,15-20.", + "duplicate_mock_providers.tftest.hcl:9,1-20: Duplicate provider block; A provider for aws.test is already defined at duplicate_mock_providers.tftest.hcl:5,15-20.", + }, + "duplicate_mock_resources": { + "duplicate_mock_resources.tftest.hcl:7,17-31: Duplicate mock_resource block; A mock_resource block for aws_instance has already been defined at duplicate_mock_resources.tftest.hcl:3,3-31.", + }, + "duplicate_module_overrides": { + "duplicate_module_overrides.tftest.hcl:7,1-16: Duplicate override_module block; An override_module block targeting module.child has already been defined at duplicate_module_overrides.tftest.hcl:2,1-16.", + "duplicate_module_overrides.tftest.hcl:18,3-18: Duplicate override_module block; An override_module block targeting module.child has already been defined at duplicate_module_overrides.tftest.hcl:13,3-18.", + }, + "duplicate_providers": { + "duplicate_providers.tftest.hcl:3,1-15: Duplicate provider block; A provider for aws is already defined at duplicate_providers.tftest.hcl:1,10-15.", + "duplicate_providers.tftest.hcl:9,1-15: Duplicate provider block; A provider for aws.test is already defined at duplicate_providers.tftest.hcl:5,10-15.", + }, + "duplicate_resource_overrides": { + "duplicate_resource_overrides.tftest.hcl:7,3-20: Duplicate override_resource block; An override_resource block targeting aws_instance.test has already been defined at duplicate_resource_overrides.tftest.hcl:2,3-20.", + "duplicate_resource_overrides.tftest.hcl:18,1-18: Duplicate override_resource block; An override_resource block targeting aws_instance.test has already been defined at duplicate_resource_overrides.tftest.hcl:13,1-18.", + "duplicate_resource_overrides.tftest.hcl:29,3-20: Duplicate override_resource block; An override_resource block targeting aws_instance.test has already been defined at duplicate_resource_overrides.tftest.hcl:24,3-20.", + }, + "invalid_data_override": { + "invalid_data_override.tftest.hcl:6,1-14: Missing target attribute; override_data blocks must specify a target address.", + }, + "invalid_data_override_target": { + "invalid_data_override_target.tftest.hcl:8,3-24: Invalid override target; You can only target data sources from override_data blocks, not module.child.", + "invalid_data_override_target.tftest.hcl:3,3-31: Invalid override target; You can only target data sources from override_data blocks, not aws_instance.target.", + }, + "invalid_mock_data_sources": { + "invalid_mock_data_sources.tftest.hcl:7,13-16: Variables not allowed; Variables may not be used here.", + }, + "invalid_mock_resources": { + "invalid_mock_resources.tftest.hcl:7,13-16: Variables not allowed; Variables may not be used here.", + }, + "invalid_module_override": { + "invalid_module_override.tftest.hcl:5,1-16: Missing target attribute; override_module blocks must specify a target address.", + "invalid_module_override.tftest.hcl:11,3-9: Unsupported argument; An argument named \"values\" is not expected here.", + }, + "invalid_module_override_target": { + "invalid_module_override_target.tftest.hcl:3,3-31: Invalid override target; You can only target modules from override_module blocks, not aws_instance.target.", + "invalid_module_override_target.tftest.hcl:8,3-36: Invalid override target; You can only target modules from override_module blocks, not data.aws_instance.target.", + }, + "invalid_resource_override": { + "invalid_resource_override.tftest.hcl:6,1-18: Missing target attribute; override_resource blocks must specify a target address.", + }, + "invalid_resource_override_target": { + "invalid_resource_override_target.tftest.hcl:3,3-36: Invalid override target; You can only target resources from override_resource blocks, not data.aws_instance.target.", + "invalid_resource_override_target.tftest.hcl:8,3-24: Invalid override target; You can only target resources from override_resource blocks, not module.child.", + }, + "duplicate_file_config": { + "duplicate_file_config.tftest.hcl:3,1-5: Multiple \"test\" blocks; This test file already has a \"test\" block defined at duplicate_file_config.tftest.hcl:1,1-5.", + "duplicate_file_config.tftest.hcl:5,1-5: Multiple \"test\" blocks; This test file already has a \"test\" block defined at duplicate_file_config.tftest.hcl:1,1-5.", + }, + } + + for name, expected := range tcs { + t.Run(name, func(t *testing.T) { + src, err := os.ReadFile(fmt.Sprintf("testdata/invalid-test-files/%s.tftest.hcl", name)) + if err != nil { + t.Fatal(err) + } + + parser := testParser(map[string]string{ + fmt.Sprintf("%s.tftest.hcl", name): string(src), + }) + + _, actual := parser.LoadTestFile(fmt.Sprintf("%s.tftest.hcl", name)) + assertExactDiagnostics(t, actual, expected) + }) + } +} + +func TestParserLoadConfigDirWithTests_ReturnsWarnings(t *testing.T) { + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests("testdata/valid-modules/with-tests", "not_real") + if len(diags) != 1 { + t.Errorf("expected exactly 1 diagnostic, but found %d", len(diags)) + } else { + if diags[0].Severity != hcl.DiagWarning { + t.Errorf("expected warning severity but found %d", diags[0].Severity) + } + + if diags[0].Summary != "Test directory does not exist" { + t.Errorf("expected summary to be \"Test directory does not exist\" but was \"%s\"", diags[0].Summary) + } + + if diags[0].Detail != "Requested test directory testdata/valid-modules/with-tests/not_real does not exist." { + t.Errorf("expected detail to be \"Requested test directory testdata/valid-modules/with-tests/not_real does not exist.\" but was \"%s\"", diags[0].Detail) + } + } + + // Despite the warning, should still have loaded the tests in the + // configuration directory. + if len(mod.Tests) != 2 { + t.Errorf("incorrect number of test files found: %d", len(mod.Tests)) + } +} + // TestParseLoadConfigDirFailure is a simple test that just verifies that // a number of test configuration directories (in testdata/invalid-modules) // produce diagnostics when parsed. @@ -127,7 +283,7 @@ func TestParserLoadConfigDirFailure(t *testing.T) { parser := NewParser(nil) path := filepath.Join("testdata/invalid-modules", name) - _, diags := parser.LoadConfigDir(path) + _, diags := parser.LoadConfigDirWithTests(path, "tests") if !diags.HasErrors() { t.Errorf("no errors; want at least one") for _, diag := range diags { @@ -169,7 +325,7 @@ func TestParserLoadConfigDirFailure(t *testing.T) { } func TestIsEmptyDir(t *testing.T) { - val, err := IsEmptyDir(filepath.Join("testdata", "valid-files")) + val, err := IsEmptyDir(filepath.Join("testdata", "valid-files"), "") if err != nil { t.Fatalf("err: %s", err) } @@ -179,7 +335,7 @@ func TestIsEmptyDir(t *testing.T) { } func TestIsEmptyDir_noExist(t *testing.T) { - val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope")) + val, err := IsEmptyDir(filepath.Join("testdata", "nopenopenope"), "") if err != nil { t.Fatalf("err: %s", err) } @@ -188,8 +344,8 @@ func TestIsEmptyDir_noExist(t *testing.T) { } } -func TestIsEmptyDir_noConfigs(t *testing.T) { - val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty")) +func TestIsEmptyDir_noConfigsAndTests(t *testing.T) { + val, err := IsEmptyDir(filepath.Join("testdata", "dir-empty"), "") if err != nil { t.Fatalf("err: %s", err) } @@ -197,3 +353,26 @@ func TestIsEmptyDir_noConfigs(t *testing.T) { t.Fatal("should be empty") } } + +func TestIsEmptyDir_noConfigsButHasTests(t *testing.T) { + // The top directory has no configs, but it contains test files + val, err := IsEmptyDir(filepath.Join("testdata", "only-test-files"), "tests") + if err != nil { + t.Fatalf("err: %s", err) + } + if val { + t.Fatal("should not be empty") + } +} + +func TestIsEmptyDir_nestedTestsOnly(t *testing.T) { + // The top directory has no configs and no test files, but the nested + // directory has test files + val, err := IsEmptyDir(filepath.Join("testdata", "only-nested-test-files"), "tests") + if err != nil { + t.Fatalf("err: %s", err) + } + if val { + t.Fatal("should not be empty") + } +} diff --git a/internal/configs/parser_config_test.go b/internal/configs/parser_config_test.go index e1244ade12..e85bfa2da2 100644 --- a/internal/configs/parser_config_test.go +++ b/internal/configs/parser_config_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -189,18 +192,27 @@ func TestParserLoadConfigFileWarning(t *testing.T) { sc := bufio.NewScanner(bytes.NewReader(src)) wantWarnings := make(map[int]string) lineNum := 1 + allowExperiments := false for sc.Scan() { lineText := sc.Text() if idx := strings.Index(lineText, marker); idx != -1 { summaryText := lineText[idx+len(marker):] wantWarnings[lineNum] = summaryText } + if lineText == "# ALLOW-LANGUAGE-EXPERIMENTS" { + allowExperiments = true + } lineNum++ } parser := testParser(map[string]string{ name: string(src), }) + // Some inputs use a special comment to request that they be + // permitted to use language experiments. We typically use that + // to test that the experiment opt-in is working and is causing + // the expected "you are using experimental features" warning. + parser.AllowLanguageExperiments(allowExperiments) _, diags := parser.LoadConfigFile(name) if diags.HasErrors() { diff --git a/internal/configs/parser_test.go b/internal/configs/parser_test.go index 4eb558d1c6..2a018533aa 100644 --- a/internal/configs/parser_test.go +++ b/internal/configs/parser_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -45,7 +48,7 @@ func testModuleConfigFromFile(filename string) (*Config, hcl.Diagnostics) { f, diags := parser.LoadConfigFile(filename) mod, modDiags := NewModule([]*File{f}, nil) diags = append(diags, modDiags...) - cfg, moreDiags := BuildConfig(mod, nil) + cfg, moreDiags := BuildConfig(mod, nil, nil) return cfg, append(diags, moreDiags...) } @@ -56,15 +59,41 @@ func testModuleFromDir(path string) (*Module, hcl.Diagnostics) { return parser.LoadConfigDir(path) } +// testModuleFromDirWithExperiments reads configuration from the given directory +// path as a module and returns it. The parser is configured to allow language +// experiments. This is a helper for use in unit tests. +func testModuleFromDirWithExperiments(path string) (*Module, hcl.Diagnostics) { + parser := NewParser(nil) + parser.AllowLanguageExperiments(true) + return parser.LoadConfigDir(path) +} + // testModuleFromDir reads configuration from the given directory path as a // module and returns its configuration. This is a helper for use in unit tests. func testModuleConfigFromDir(path string) (*Config, hcl.Diagnostics) { parser := NewParser(nil) mod, diags := parser.LoadConfigDir(path) - cfg, moreDiags := BuildConfig(mod, nil) + cfg, moreDiags := BuildConfig(mod, nil, nil) return cfg, append(diags, moreDiags...) } +// testNestedModuleConfigFromDirWithTests matches testNestedModuleConfigFromDir +// except it also loads any test files within the directory. +func testNestedModuleConfigFromDirWithTests(t *testing.T, path string) (*Config, hcl.Diagnostics) { + t.Helper() + + parser := NewParser(nil) + mod, diags := parser.LoadConfigDirWithTests(path, "tests") + if mod == nil { + t.Fatal("got nil root module; want non-nil") + } + + cfg, nestedDiags := buildNestedModuleConfig(mod, path, parser) + + diags = append(diags, nestedDiags...) + return cfg, diags +} + // testNestedModuleConfigFromDir reads configuration from the given directory path as // a module with (optional) submodules and returns its configuration. This is a // helper for use in unit tests. @@ -77,8 +106,15 @@ func testNestedModuleConfigFromDir(t *testing.T, path string) (*Config, hcl.Diag t.Fatal("got nil root module; want non-nil") } + cfg, nestedDiags := buildNestedModuleConfig(mod, path, parser) + + diags = append(diags, nestedDiags...) + return cfg, diags +} + +func buildNestedModuleConfig(mod *Module, path string, parser *Parser) (*Config, hcl.Diagnostics) { versionI := 0 - cfg, nestedDiags := BuildConfig(mod, ModuleWalkerFunc( + return BuildConfig(mod, ModuleWalkerFunc( func(req *ModuleRequest) (*Module, *version.Version, hcl.Diagnostics) { // For the sake of this test we're going to just treat our // SourceAddr as a path relative to the calling module. @@ -98,11 +134,11 @@ func testNestedModuleConfigFromDir(t *testing.T, path string) (*Config, hcl.Diag version, _ := version.NewVersion(fmt.Sprintf("1.0.%d", versionI)) versionI++ return mod, version, diags - }, - )) - - diags = append(diags, nestedDiags...) - return cfg, diags + }), + MockDataLoaderFunc(func(provider *Provider) (*MockData, hcl.Diagnostics) { + return nil, nil + }), + ) } func assertNoDiagnostics(t *testing.T, diags hcl.Diagnostics) bool { diff --git a/internal/configs/parser_values.go b/internal/configs/parser_values.go deleted file mode 100644 index 10d98e5b09..0000000000 --- a/internal/configs/parser_values.go +++ /dev/null @@ -1,43 +0,0 @@ -package configs - -import ( - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" -) - -// LoadValuesFile reads the file at the given path and parses it as a "values -// file", which is an HCL config file whose top-level attributes are treated -// as arbitrary key.value pairs. -// -// If the file cannot be read -- for example, if it does not exist -- then -// a nil map will be returned along with error diagnostics. Callers may wish -// to disregard the returned diagnostics in this case and instead generate -// their own error message(s) with additional context. -// -// If the returned diagnostics has errors when a non-nil map is returned -// then the map may be incomplete but should be valid enough for careful -// static analysis. -// -// This method wraps LoadHCLFile, and so it inherits the syntax selection -// behaviors documented for that method. -func (p *Parser) LoadValuesFile(path string) (map[string]cty.Value, hcl.Diagnostics) { - body, diags := p.LoadHCLFile(path) - if body == nil { - return nil, diags - } - - vals := make(map[string]cty.Value) - attrs, attrDiags := body.JustAttributes() - diags = append(diags, attrDiags...) - if attrs == nil { - return vals, diags - } - - for name, attr := range attrs { - val, valDiags := attr.Expr.Value(nil) - diags = append(diags, valDiags...) - vals[name] = val - } - - return vals, diags -} diff --git a/internal/configs/parser_values_test.go b/internal/configs/parser_values_test.go deleted file mode 100644 index b26901bfd6..0000000000 --- a/internal/configs/parser_values_test.go +++ /dev/null @@ -1,113 +0,0 @@ -package configs - -import ( - "testing" - - "github.com/zclconf/go-cty/cty" -) - -func TestParserLoadValuesFile(t *testing.T) { - tests := map[string]struct { - Source string - Want map[string]cty.Value - DiagCount int - }{ - "empty.tfvars": { - "", - map[string]cty.Value{}, - 0, - }, - "empty.json": { - "{}", - map[string]cty.Value{}, - 0, - }, - "zerolen.json": { - "", - map[string]cty.Value{}, - 2, // syntax error and missing root object - }, - "one-number.tfvars": { - "foo = 1\n", - map[string]cty.Value{ - "foo": cty.NumberIntVal(1), - }, - 0, - }, - "one-number.tfvars.json": { - `{"foo": 1}`, - map[string]cty.Value{ - "foo": cty.NumberIntVal(1), - }, - 0, - }, - "two-bools.tfvars": { - "foo = true\nbar = false\n", - map[string]cty.Value{ - "foo": cty.True, - "bar": cty.False, - }, - 0, - }, - "two-bools.tfvars.json": { - `{"foo": true, "bar": false}`, - map[string]cty.Value{ - "foo": cty.True, - "bar": cty.False, - }, - 0, - }, - "invalid-syntax.tfvars": { - "foo bar baz\n", - map[string]cty.Value{}, - 2, // invalid block definition, and unexpected foo block (the latter due to parser recovery behavior) - }, - "block.tfvars": { - "foo = true\ninvalid {\n}\n", - map[string]cty.Value{ - "foo": cty.True, - }, - 1, // blocks are not allowed - }, - "variables.tfvars": { - "baz = true\nfoo = var.baz\n", - map[string]cty.Value{ - "baz": cty.True, - "foo": cty.DynamicVal, - }, - 1, // variables cannot be referenced here - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - p := testParser(map[string]string{ - name: test.Source, - }) - got, diags := p.LoadValuesFile(name) - if len(diags) != test.DiagCount { - t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.DiagCount) - for _, diag := range diags { - t.Logf("- %s", diag) - } - } - - if len(got) != len(test.Want) { - t.Errorf("wrong number of result keys %d; want %d", len(got), len(test.Want)) - } - - for name, gotVal := range got { - wantVal := test.Want[name] - if wantVal == cty.NilVal { - t.Errorf("unexpected result key %q", name) - continue - } - - if !gotVal.RawEquals(wantVal) { - t.Errorf("wrong value for %q\ngot: %#v\nwant: %#v", name, gotVal, wantVal) - continue - } - } - }) - } -} diff --git a/internal/configs/provider.go b/internal/configs/provider.go index 6ed24f63af..f3ebfeb7de 100644 --- a/internal/configs/provider.go +++ b/internal/configs/provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -32,9 +35,23 @@ type Provider struct { // export this so providers don't need to be re-resolved. // This same field is also added to the ProviderConfigRef struct. providerType addrs.Provider + + // Mock and MockData declare this provider as a "mock_provider", which means + // it should use the data in MockData instead of actually initialising the + // provider. MockDataDuringPlan tells the provider that, by default, it + // should generate values during the planning stage instead of waiting for + // the apply stage. + Mock bool + MockDataDuringPlan bool + MockData *MockData + + // MockDataExternalSource is a file path pointing to the external data + // file for a mock provider. An empty string indicates all data should be + // loaded inline. + MockDataExternalSource string } -func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { +func decodeProviderBlock(block *hcl.Block, testFile bool) (*Provider, hcl.Diagnostics) { var diags hcl.Diagnostics content, config, moreDiags := block.Body.PartialContent(providerBlockSchema) @@ -57,6 +74,10 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { NameRange: block.LabelRanges[0], Config: config, DeclRange: block.DefRange, + + // We'll just explicitly mark real providers as not being mocks even + // though this is the default. + Mock: false, } if attr, exists := content.Attributes["alias"]; exists { @@ -74,15 +95,24 @@ func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) { } if attr, exists := content.Attributes["version"]; exists { - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Version constraints inside provider configuration blocks are deprecated", - Detail: "Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of Terraform. To silence this warning, move the provider version constraint into the required_providers block.", - Subject: attr.Expr.Range().Ptr(), - }) - var versionDiags hcl.Diagnostics - provider.Version, versionDiags = decodeVersionConstraint(attr) - diags = append(diags, versionDiags...) + if testFile { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Version constraints are not allowed in test files", + Detail: "Version constraints inside provider configuration blocks are not allowed in test files. To silence this error, move the provider version constraint into the required_providers block of the configuration that uses this provider.", + Subject: attr.Expr.Range().Ptr(), + }) + } else { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Version constraints inside provider configuration blocks are deprecated", + Detail: "Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is now deprecated and will be removed in a future version of Terraform. To silence this warning, move the provider version constraint into the required_providers block.", + Subject: attr.Expr.Range().Ptr(), + }) + var versionDiags hcl.Diagnostics + provider.Version, versionDiags = decodeVersionConstraint(attr) + diags = append(diags, versionDiags...) + } } // Reserved attribute names diff --git a/internal/configs/provider_meta.go b/internal/configs/provider_meta.go index d1e817e7ed..ba2d2fd41e 100644 --- a/internal/configs/provider_meta.go +++ b/internal/configs/provider_meta.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import "github.com/hashicorp/hcl/v2" @@ -26,7 +29,11 @@ func decodeProviderMetaBlock(block *hcl.Block) (*ProviderMeta, hcl.Diagnostics) } // verify that the local name is already localized or produce an error. - diags = append(diags, checkProviderNameNormalized(block.Labels[0], block.DefRange)...) + nameDiags := checkProviderNameNormalized(block.Labels[0], block.DefRange) + diags = append(diags, nameDiags...) + if nameDiags.HasErrors() { + return nil, diags + } return &ProviderMeta{ Provider: block.Labels[0], diff --git a/internal/configs/provider_requirements.go b/internal/configs/provider_requirements.go index c982c1a37c..f4bea7f8f3 100644 --- a/internal/configs/provider_requirements.go +++ b/internal/configs/provider_requirements.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/provider_requirements_test.go b/internal/configs/provider_requirements_test.go index 8d00f61902..39d4b69a35 100644 --- a/internal/configs/provider_requirements_test.go +++ b/internal/configs/provider_requirements_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/provider_test.go b/internal/configs/provider_test.go index 65924f085f..1bee5159e4 100644 --- a/internal/configs/provider_test.go +++ b/internal/configs/provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/configs/provider_validation.go b/internal/configs/provider_validation.go index 0cf7378d36..f21d78b098 100644 --- a/internal/configs/provider_validation.go +++ b/internal/configs/provider_validation.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -6,9 +9,268 @@ import ( "strings" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" ) +// validateProviderConfigsForTests performs the same role as +// validateProviderConfigs except it validates the providers configured within +// test files. +// +// To do this is calls out to validateProviderConfigs for each run block that +// has ConfigUnderTest set. +// +// In addition, for each run block that executes against the main config it +// validates the providers the run block wants to use match the providers +// specified in the main configuration. It does this without reaching out to +// validateProviderConfigs because the main configuration has already been +// validated, and we don't want to redo all the work that happens in that +// function. So, we only validate the providers our test files define match +// the providers required by the main configuration. +// +// This function does some fairly controversial conversions into structures +// expected by validateProviderConfigs but since we're just using it for +// validation we'll still get the correct error messages, and we can make the +// declaration ranges line up sensibly so we'll even get good diagnostics. +func validateProviderConfigsForTests(cfg *Config) (diags hcl.Diagnostics) { + + for name, test := range cfg.Module.Tests { + for _, run := range test.Runs { + + if run.ConfigUnderTest == nil { + // Then we're calling out to the main configuration under test. + // + // We just need to make sure that the providers we are setting + // actually match the providers in the configuration. The main + // configuration has already been validated, so we don't need to + // do the whole thing again. + + if len(run.Providers) > 0 { + // This is the easy case, we can just validate that the + // provider types match. + for _, provider := range run.Providers { + + parentType, childType := provider.InParent.providerType, provider.InChild.providerType + if parentType.IsZero() { + parentType = addrs.NewDefaultProvider(provider.InParent.Name) + } + if childType.IsZero() { + childType = addrs.NewDefaultProvider(provider.InChild.Name) + } + + if !childType.Equals(parentType) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider type mismatch", + Detail: fmt.Sprintf( + "The local name %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types.", + provider.InParent.Name, name, parentType, provider.InChild.Name, childType, provider.InParent.Name, name), + Subject: provider.InParent.NameRange.Ptr(), + }) + } + } + + // Skip to the next file, we only need to verify the types + // specified here. + continue + } + + // Otherwise, we need to verify that the providers required by + // the configuration match the types defined by our test file. + + for _, requirement := range cfg.Module.ProviderRequirements.RequiredProviders { + if provider, exists := test.Providers[requirement.Name]; exists { + + providerType := provider.providerType + if providerType.IsZero() { + providerType = addrs.NewDefaultProvider(provider.Name) + } + + if !providerType.Equals(requirement.Type) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider type mismatch", + Detail: fmt.Sprintf( + "The provider %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types.", + provider.moduleUniqueKey(), name, providerType, requirement.Name, requirement.Type, provider.moduleUniqueKey(), name), + Subject: provider.DeclRange.Ptr(), + }) + } + } + + for _, alias := range requirement.Aliases { + if provider, exists := test.Providers[alias.StringCompact()]; exists { + + providerType := provider.providerType + if providerType.IsZero() { + providerType = addrs.NewDefaultProvider(provider.Name) + } + + if !providerType.Equals(requirement.Type) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider type mismatch", + Detail: fmt.Sprintf( + "The provider %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types.", + provider.moduleUniqueKey(), name, providerType, alias.StringCompact(), requirement.Type, provider.moduleUniqueKey(), name), + Subject: provider.DeclRange.Ptr(), + }) + } + } + } + } + + for _, provider := range cfg.Module.ProviderConfigs { + + providerType := provider.providerType + if providerType.IsZero() { + providerType = addrs.NewDefaultProvider(provider.Name) + } + + if testProvider, exists := test.Providers[provider.moduleUniqueKey()]; exists { + + testProviderType := testProvider.providerType + if testProviderType.IsZero() { + testProviderType = addrs.NewDefaultProvider(testProvider.Name) + } + + if !providerType.Equals(testProviderType) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider type mismatch", + Detail: fmt.Sprintf( + "The provider %q in %s represents provider %q, but %q in the root module represents %q.\n\nThis means the provider definition for %q within %s has been referenced by multiple run blocks and assigned to different provider types.", + testProvider.moduleUniqueKey(), name, testProviderType, provider.moduleUniqueKey(), providerType, testProvider.moduleUniqueKey(), name), + Subject: testProvider.DeclRange.Ptr(), + }) + } + } + } + + } else { + // Then we're executing another module. We'll just call out to + // validateProviderConfigs and let it do the whole thing. + + providers := run.Providers + if len(providers) == 0 { + // If the test run didn't provide us a subset of providers + // to use, we'll build our own. This is so that we can fit + // into the schema expected by validateProviderConfigs. + + matchedProviders := make(map[string]PassedProviderConfig) + + // We'll go over all the requirements in the module first + // and see if we have defined any providers for that + // requirement. If we have, then we'll take not of that. + + for _, requirement := range cfg.Module.ProviderRequirements.RequiredProviders { + + if provider, exists := test.Providers[requirement.Name]; exists { + matchedProviders[requirement.Name] = PassedProviderConfig{ + InChild: &ProviderConfigRef{ + Name: requirement.Name, + NameRange: requirement.DeclRange, + providerType: requirement.Type, + }, + InParent: &ProviderConfigRef{ + Name: provider.Name, + NameRange: provider.NameRange, + Alias: provider.Alias, + AliasRange: provider.AliasRange, + providerType: provider.providerType, + }, + } + } + + // Also, remember to check for any aliases the module + // expects. + + for _, alias := range requirement.Aliases { + key := alias.StringCompact() + + if provider, exists := test.Providers[key]; exists { + matchedProviders[key] = PassedProviderConfig{ + InChild: &ProviderConfigRef{ + Name: requirement.Name, + NameRange: requirement.DeclRange, + Alias: alias.Alias, + AliasRange: requirement.DeclRange.Ptr(), + providerType: requirement.Type, + }, + InParent: &ProviderConfigRef{ + Name: provider.Name, + NameRange: provider.NameRange, + Alias: provider.Alias, + AliasRange: provider.AliasRange, + providerType: provider.providerType, + }, + } + } + + } + + } + + // Next, we'll look at any providers the module has defined + // directly. If we have an equivalent provider in the test + // file then we'll add that in to override it. If the module + // has both built a required providers block and a provider + // block for the same provider, we'll overwrite the one we + // made for the requirement provider. We get more precise + // DeclRange objects from provider blocks so it makes for + // better error messages to use these. + + for _, provider := range cfg.Module.ProviderConfigs { + key := provider.moduleUniqueKey() + + if testProvider, exists := test.Providers[key]; exists { + matchedProviders[key] = PassedProviderConfig{ + InChild: &ProviderConfigRef{ + Name: provider.Name, + NameRange: provider.DeclRange, + Alias: provider.Alias, + AliasRange: provider.DeclRange.Ptr(), + providerType: provider.providerType, + }, + InParent: &ProviderConfigRef{ + Name: testProvider.Name, + NameRange: testProvider.NameRange, + Alias: testProvider.Alias, + AliasRange: testProvider.AliasRange, + providerType: testProvider.providerType, + }, + } + } + } + + // Last thing to do here is add them into the actual + // providers list that is going into the module call below. + for _, provider := range matchedProviders { + providers = append(providers, provider) + } + + } + + // Let's make a little fake module call that we can use to call + // into validateProviderConfigs. + mc := &ModuleCall{ + Name: run.Name, + SourceAddr: run.Module.Source, + SourceAddrRange: run.Module.SourceDeclRange, + SourceSet: true, + Version: run.Module.Version, + Providers: providers, + DeclRange: run.Module.DeclRange, + } + + diags = append(diags, validateProviderConfigs(mc, run.ConfigUnderTest, nil)...) + } + } + } + + return diags +} + // validateProviderConfigs walks the full configuration tree from the root // module outward, static validation rules to the various combinations of // provider configuration, required_providers values, and module call providers @@ -20,13 +282,20 @@ import ( // however will generate an error if a suitable provider configuration is not // passed in through the module call. // -// The call argument is the ModuleCall for the provided Config cfg. The +// The parentCall argument is the ModuleCall for the provided Config cfg. The // noProviderConfigRange argument is passed down the call stack, indicating // that the module call, or a parent module call, has used a feature (at the // specified source location) that precludes providers from being configured at // all within the module. +// +// Set parentCall to nil when analyzing the root module. In that case the +// given configuration is allowed to require passed-in provider configurations +// without that being an error at this layer, although Terraform Core itself +// will raise an error if asked to plan such a configuration without the caller +// passing in suitable pre-configured providers to use. func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConfigRange *hcl.Range) (diags hcl.Diagnostics) { mod := cfg.Module + analyzingRootModule := (parentCall == nil) for name, child := range cfg.Children { mc := mod.ModuleCalls[name] @@ -304,25 +573,30 @@ func validateProviderConfigs(parentCall *ModuleCall, cfg *Config, noProviderConf } // A declared alias requires either a matching configuration within the - // module, or one must be passed in. - for name, providerAddr := range configAliases { - _, confOk := configured[name] - _, passedOk := passedIn[name] + // module, or one must be passed in, unless we're analyzing the root + // module. For the root module it's up to Terraform Core to check if + // it's being given the required provider configurations as part of the + // options when creating a plan. + if !analyzingRootModule { + for name, providerAddr := range configAliases { + _, confOk := configured[name] + _, passedOk := passedIn[name] - if confOk || passedOk { - continue + if confOk || passedOk { + continue + } + + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: fmt.Sprintf( + "The child module requires an additional configuration for provider %s, with the local name %q.\n\nRefer to the module's documentation to understand the intended purpose of this additional provider configuration, and then add an entry for %s in the \"providers\" meta-argument in the module block to choose which provider configuration the module should use for that purpose.", + providerAddr.Provider.ForDisplay(), name, + name, + ), + Subject: &parentCall.DeclRange, + }) } - - diags = append(diags, &hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing required provider configuration", - Detail: fmt.Sprintf( - "The child module requires an additional configuration for provider %s, with the local name %q.\n\nRefer to the module's documentation to understand the intended purpose of this additional provider configuration, and then add an entry for %s in the \"providers\" meta-argument in the module block to choose which provider configuration the module should use for that purpose.", - providerAddr.Provider.ForDisplay(), name, - name, - ), - Subject: &parentCall.DeclRange, - }) } // You cannot pass in a provider that cannot be used diff --git a/internal/configs/provisioner.go b/internal/configs/provisioner.go index 4de8bf6f47..36213fde87 100644 --- a/internal/configs/provisioner.go +++ b/internal/configs/provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -203,7 +206,7 @@ type Connection struct { // ProvisionerWhen is an enum for valid values for when to run provisioners. type ProvisionerWhen int -//go:generate go run golang.org/x/tools/cmd/stringer -type ProvisionerWhen +//go:generate go tool golang.org/x/tools/cmd/stringer -type ProvisionerWhen const ( ProvisionerWhenInvalid ProvisionerWhen = iota @@ -215,7 +218,7 @@ const ( // for provisioners. type ProvisionerOnFailure int -//go:generate go run golang.org/x/tools/cmd/stringer -type ProvisionerOnFailure +//go:generate go tool golang.org/x/tools/cmd/stringer -type ProvisionerOnFailure const ( ProvisionerOnFailureInvalid ProvisionerOnFailure = iota diff --git a/internal/configs/removed.go b/internal/configs/removed.go new file mode 100644 index 0000000000..dadaee17ba --- /dev/null +++ b/internal/configs/removed.go @@ -0,0 +1,157 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" +) + +// Removed describes the contents of a "removed" block in configuration. +type Removed struct { + // From is the address of the configuration object being removed. + From *addrs.RemoveTarget + + // Destroy indicates that the resource should be destroyed, not just removed + // from state. Defaults to true. + Destroy bool + + // Managed captures a number of metadata fields that are applicable only + // for managed resources, and not for other resource modes. + // + // "removed" blocks support only a subset of the fields in [ManagedResource]. + Managed *ManagedResource + + DeclRange hcl.Range +} + +func decodeRemovedBlock(block *hcl.Block) (*Removed, hcl.Diagnostics) { + var diags hcl.Diagnostics + removed := &Removed{ + DeclRange: block.DefRange, + } + + content, moreDiags := block.Body.Content(removedBlockSchema) + diags = append(diags, moreDiags...) + + var targetKind addrs.RemoveTargetKind + var resourceMode addrs.ResourceMode // only valid if targetKind is addrs.RemoveTargetResource + if attr, exists := content.Attributes["from"]; exists { + from, traversalDiags := hcl.AbsTraversalForExpr(attr.Expr) + diags = append(diags, traversalDiags...) + if !traversalDiags.HasErrors() { + from, fromDiags := addrs.ParseRemoveTarget(from) + diags = append(diags, fromDiags.ToHCL()...) + removed.From = from + if removed.From != nil { + targetKind = removed.From.ObjectKind() + if targetKind == addrs.RemoveTargetResource { + resourceMode = removed.From.RelSubject.(addrs.ConfigResource).Resource.Mode + } + } + } + } + + removed.Destroy = true + if resourceMode == addrs.ManagedResourceMode { + removed.Managed = &ManagedResource{} + } + + var seenConnection *hcl.Block + for _, block := range content.Blocks { + switch block.Type { + case "lifecycle": + lcContent, lcDiags := block.Body.Content(removedLifecycleBlockSchema) + diags = append(diags, lcDiags...) + + if attr, exists := lcContent.Attributes["destroy"]; exists { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &removed.Destroy) + diags = append(diags, valDiags...) + } + + case "connection": + if removed.Managed == nil { + // target is not a managed resource, then + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid connection block", + Detail: "Provisioner connection configuration is valid only when a removed block targets a managed resource.", + Subject: &block.DefRange, + }) + continue + } + + if seenConnection != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate connection block", + Detail: fmt.Sprintf("This \"removed\" block already has a connection block at %s.", seenConnection.DefRange), + Subject: &block.DefRange, + }) + continue + } + seenConnection = block + + removed.Managed.Connection = &Connection{ + Config: block.Body, + DeclRange: block.DefRange, + } + + case "provisioner": + if removed.Managed == nil { + // target is not a managed resource, then + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provisioner block", + Detail: "Provisioners are valid only when a removed block targets a managed resource.", + Subject: &block.DefRange, + }) + continue + } + + pv, pvDiags := decodeProvisionerBlock(block) + diags = append(diags, pvDiags...) + if pv != nil { + removed.Managed.Provisioners = append(removed.Managed.Provisioners, pv) + + if pv.When != ProvisionerWhenDestroy { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provisioner block", + Detail: "Only destroy-time provisioners are valid in \"removed\" blocks. To declare a destroy-time provisioner, use:\n when = destroy", + Subject: &block.DefRange, + }) + } + } + } + } + + return removed, diags +} + +var removedBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "from", + Required: true, + }, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "lifecycle"}, + {Type: "connection"}, + {Type: "provisioner", LabelNames: []string{"type"}}, + }, +} + +var removedLifecycleBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "destroy", + }, + }, +} diff --git a/internal/configs/removed_test.go b/internal/configs/removed_test.go new file mode 100644 index 0000000000..dc43225b8f --- /dev/null +++ b/internal/configs/removed_test.go @@ -0,0 +1,490 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/hashicorp/terraform/internal/addrs" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" +) + +func TestRemovedBlock_decode(t *testing.T) { + blockRange := hcl.Range{ + Filename: "mock.tf", + Start: hcl.Pos{Line: 3, Column: 12, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 19, Byte: 34}, + } + + foo_expr := hcltest.MockExprTraversalSrc("test_instance.foo") + foo_index_expr := hcltest.MockExprTraversalSrc("test_instance.foo[1]") + mod_foo_expr := hcltest.MockExprTraversalSrc("module.foo") + mod_foo_index_expr := hcltest.MockExprTraversalSrc("module.foo[1]") + + tests := map[string]struct { + input *hcl.Block + want *Removed + err string + }{ + "destroy true": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "lifecycle", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "destroy": { + Name: "destroy", + Expr: hcltest.MockExprLiteral(cty.BoolVal(true)), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{}, + DeclRange: blockRange, + }, + ``, + }, + "destroy false": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "lifecycle", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "destroy": { + Name: "destroy", + Expr: hcltest.MockExprLiteral(cty.BoolVal(false)), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: false, + Managed: &ManagedResource{}, + DeclRange: blockRange, + }, + ``, + }, + "provisioner when = destroy": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "provisioner", + Labels: []string{"remote-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "when": { + Name: "when", + Expr: hcltest.MockExprTraversalSrc("destroy"), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{ + Provisioners: []*Provisioner{ + { + Type: "remote-exec", + Config: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{}, + Blocks: hcl.Blocks{}, + }), + When: ProvisionerWhenDestroy, + OnFailure: ProvisionerOnFailureFail, + }, + }, + }, + DeclRange: blockRange, + }, + ``, + }, + "provisioner when = create": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "provisioner", + Labels: []string{"local-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "when": { + Name: "when", + Expr: hcltest.MockExprTraversalSrc("create"), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{ + Provisioners: []*Provisioner{ + { + Type: "local-exec", + Config: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{}, + Blocks: hcl.Blocks{}, + }), + When: ProvisionerWhenCreate, + OnFailure: ProvisionerOnFailureFail, + }, + }, + }, + DeclRange: blockRange, + }, + `Invalid provisioner block`, + }, + "provisioner no when": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "connection", + Body: hcltest.MockBody(&hcl.BodyContent{}), + }, + &hcl.Block{ + Type: "provisioner", + Labels: []string{"local-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{}), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{ + Connection: &Connection{ + Config: hcltest.MockBody(&hcl.BodyContent{}), + }, + Provisioners: []*Provisioner{ + { + Type: "local-exec", + Config: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{}, + Blocks: hcl.Blocks{}, + }), + When: ProvisionerWhenCreate, + OnFailure: ProvisionerOnFailureFail, + }, + }, + }, + DeclRange: blockRange, + }, + `Invalid provisioner block`, + }, + "modules": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: mod_foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "lifecycle", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "destroy": { + Name: "destroy", + Expr: hcltest.MockExprLiteral(cty.BoolVal(true)), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(mod_foo_expr), + Destroy: true, + DeclRange: blockRange, + }, + ``, + }, + "provisioner for module": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: mod_foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "provisioner", + Labels: []string{"local-exec"}, + LabelRanges: []hcl.Range{{}}, + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "when": { + Name: "when", + Expr: hcltest.MockExprTraversalSrc("destroy"), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(mod_foo_expr), + Destroy: true, + DeclRange: blockRange, + }, + `Invalid provisioner block`, + }, + "connection for module": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: mod_foo_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "connection", + Body: hcltest.MockBody(&hcl.BodyContent{}), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(mod_foo_expr), + Destroy: true, + DeclRange: blockRange, + }, + `Invalid connection block`, + }, + // KEM Unspecified behaviour + "no lifecycle block": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_expr, + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: mustRemoveEndpointFromExpr(foo_expr), + Destroy: true, + Managed: &ManagedResource{}, + DeclRange: blockRange, + }, + ``, + }, + "error: missing argument": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "lifecycle", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "destroy": { + Name: "destroy", + Expr: hcltest.MockExprLiteral(cty.BoolVal(true)), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + Destroy: true, + DeclRange: blockRange, + }, + "Missing required argument", + }, + "error: indexed resource instance": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: foo_index_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "lifecycle", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "destroy": { + Name: "destroy", + Expr: hcltest.MockExprLiteral(cty.BoolVal(true)), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: nil, + Destroy: true, + DeclRange: blockRange, + }, + `Resource instance keys not allowed`, + }, + "error: indexed module instance": { + &hcl.Block{ + Type: "removed", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "from": { + Name: "from", + Expr: mod_foo_index_expr, + }, + }, + Blocks: hcl.Blocks{ + &hcl.Block{ + Type: "lifecycle", + Body: hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "destroy": { + Name: "destroy", + Expr: hcltest.MockExprLiteral(cty.BoolVal(true)), + }, + }, + }), + }, + }, + }), + DefRange: blockRange, + }, + &Removed{ + From: nil, + Destroy: true, + DeclRange: blockRange, + }, + `Module instance keys not allowed`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, diags := decodeRemovedBlock(test.input) + + if diags.HasErrors() { + if test.err == "" { + t.Fatalf("unexpected error: %s", diags.Errs()) + } + if gotErr := diags[0].Summary; gotErr != test.err { + t.Errorf("wrong error, got %q, want %q", gotErr, test.err) + } + } else if test.err != "" { + t.Fatal("expected error") + } + + if !cmp.Equal(got, test.want, cmp.AllowUnexported(addrs.MoveEndpoint{})) { + t.Fatalf("wrong result: %s", cmp.Diff(got, test.want)) + } + }) + } +} + +func mustRemoveEndpointFromExpr(expr hcl.Expression) *addrs.RemoveTarget { + traversal, hcldiags := hcl.AbsTraversalForExpr(expr) + if hcldiags.HasErrors() { + panic(hcldiags.Errs()) + } + + ep, diags := addrs.ParseRemoveTarget(traversal) + if diags.HasErrors() { + panic(diags.Err()) + } + + return ep +} diff --git a/internal/configs/resource.go b/internal/configs/resource.go index 1f67c6c40f..12334d5b4e 100644 --- a/internal/configs/resource.go +++ b/internal/configs/resource.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( @@ -6,10 +9,9 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" - hcljson "github.com/hashicorp/hcl/v2/json" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -37,14 +39,22 @@ type Resource struct { // For all other resource modes, this field is nil. Managed *ManagedResource + // Container links a scoped resource back up to the resources that contains + // it. This field is referenced during static analysis to check whether any + // references are also made from within the same container. + // + // If this is nil, then this resource is essentially public. + Container Container + DeclRange hcl.Range TypeRange hcl.Range } // ManagedResource represents a "resource" block in a module or file. type ManagedResource struct { - Connection *Connection - Provisioners []*Provisioner + Connection *Connection + Provisioners []*Provisioner + ActionTriggers []*ActionTrigger CreateBeforeDestroy bool PreventDestroy bool @@ -95,7 +105,7 @@ func (r *Resource) HasCustomConditions() bool { return len(r.Postconditions) != 0 || len(r.Preconditions) != 0 } -func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) { +func decodeResourceBlock(block *hcl.Block, override bool, allowExperiments bool) (*Resource, hcl.Diagnostics) { var diags hcl.Diagnostics r := &Resource{ Mode: addrs.ManagedResourceMode, @@ -106,7 +116,7 @@ func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagno Managed: &ManagedResource{}, } - content, remain, moreDiags := block.Body.PartialContent(resourceBlockSchema) + content, remain, moreDiags := block.Body.PartialContent(ResourceBlockSchema) diags = append(diags, moreDiags...) r.Config = remain @@ -151,7 +161,7 @@ func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagno } if attr, exists := content.Attributes["depends_on"]; exists { - deps, depsDiags := decodeDependsOn(attr) + deps, depsDiags := DecodeDependsOn(attr) diags = append(diags, depsDiags...) r.DependsOn = append(r.DependsOn, deps...) } @@ -272,6 +282,17 @@ func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagno case "postcondition": r.Postconditions = append(r.Postconditions, cr) } + + // decoded, but not yet used! + case "action_trigger": + if allowExperiments { + at, atDiags := decodeActionTriggerBlock(block) + diags = append(diags, atDiags...) + if at != nil { + r.Managed.ActionTriggers = append(r.Managed.ActionTriggers, at) + } + } + default: // The cases above should be exhaustive for all block types // defined in the lifecycle schema, so this shouldn't happen. @@ -349,24 +370,24 @@ func decodeResourceBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagno return r, diags } -func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) { +func decodeEphemeralBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostics) { var diags hcl.Diagnostics r := &Resource{ - Mode: addrs.DataResourceMode, + Mode: addrs.EphemeralResourceMode, Type: block.Labels[0], Name: block.Labels[1], DeclRange: block.DefRange, TypeRange: block.LabelRanges[0], } - content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema) + content, remain, moreDiags := block.Body.PartialContent(ephemeralBlockSchema) diags = append(diags, moreDiags...) r.Config = remain if !hclsyntax.ValidIdentifier(r.Type) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Invalid data source name", + Summary: "Invalid ephemeral resource type", Detail: badIdentifierDetail, Subject: &block.LabelRanges[0], }) @@ -374,7 +395,7 @@ func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostic if !hclsyntax.ValidIdentifier(r.Name) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: "Invalid data resource name", + Summary: "Invalid ephemeral resource name", Detail: badIdentifierDetail, Subject: &block.LabelRanges[1], }) @@ -386,7 +407,7 @@ func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostic if attr, exists := content.Attributes["for_each"]; exists { r.ForEach = attr.Expr - // Cannot have count and for_each on the same data block + // Cannot have count and for_each on the same ephemeral block if r.Count != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -404,7 +425,7 @@ func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostic } if attr, exists := content.Attributes["depends_on"]; exists { - deps, depsDiags := decodeDependsOn(attr) + deps, depsDiags := DecodeDependsOn(attr) diags = append(diags, depsDiags...) r.DependsOn = append(r.DependsOn, deps...) } @@ -449,6 +470,182 @@ func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostic lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) diags = append(diags, lcDiags...) + // All of the attributes defined for resource lifecycle are for + // managed resources only, so we can emit a common error message + // for any given attributes that HCL accepted. + for name, attr := range lcContent.Attributes { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid ephemeral resource lifecycle argument", + Detail: fmt.Sprintf("The lifecycle argument %q is defined only for managed resources (\"resource\" blocks), and is not valid for ephemeral resources.", name), + Subject: attr.NameRange.Ptr(), + }) + } + + for _, block := range lcContent.Blocks { + switch block.Type { + case "precondition", "postcondition": + cr, moreDiags := decodeCheckRuleBlock(block, override) + diags = append(diags, moreDiags...) + + moreDiags = cr.validateSelfReferences(block.Type, r.Addr()) + diags = append(diags, moreDiags...) + + switch block.Type { + case "precondition": + r.Preconditions = append(r.Preconditions, cr) + case "postcondition": + r.Postconditions = append(r.Postconditions, cr) + } + default: + // The cases above should be exhaustive for all block types + // defined in the lifecycle schema, so this shouldn't happen. + panic(fmt.Sprintf("unexpected lifecycle sub-block type %q", block.Type)) + } + } + + default: + // Any other block types are ones we're reserving for future use, + // but don't have any defined meaning today. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reserved block type name in ephemeral block", + Detail: fmt.Sprintf("The block type name %q is reserved for use by Terraform in a future version.", block.Type), + Subject: block.TypeRange.Ptr(), + }) + } + } + + return r, diags +} + +func decodeDataBlock(block *hcl.Block, override, nested bool) (*Resource, hcl.Diagnostics) { + var diags hcl.Diagnostics + r := &Resource{ + Mode: addrs.DataResourceMode, + Type: block.Labels[0], + Name: block.Labels[1], + DeclRange: block.DefRange, + TypeRange: block.LabelRanges[0], + } + + content, remain, moreDiags := block.Body.PartialContent(dataBlockSchema) + diags = append(diags, moreDiags...) + r.Config = remain + + if !hclsyntax.ValidIdentifier(r.Type) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid data source name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[0], + }) + } + if !hclsyntax.ValidIdentifier(r.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid data resource name", + Detail: badIdentifierDetail, + Subject: &block.LabelRanges[1], + }) + } + + if attr, exists := content.Attributes["count"]; exists && !nested { + r.Count = attr.Expr + } else if exists && nested { + // We don't allow count attributes in nested data blocks. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "count" attribute`, + Detail: `The "count" and "for_each" meta-arguments are not supported within nested data blocks.`, + Subject: &attr.NameRange, + }) + } + + if attr, exists := content.Attributes["for_each"]; exists && !nested { + r.ForEach = attr.Expr + // Cannot have count and for_each on the same data block + if r.Count != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid combination of "count" and "for_each"`, + Detail: `The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be created.`, + Subject: &attr.NameRange, + }) + } + } else if exists && nested { + // We don't allow for_each attributes in nested data blocks. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "for_each" attribute`, + Detail: `The "count" and "for_each" meta-arguments are not supported within nested data blocks.`, + Subject: &attr.NameRange, + }) + } + + if attr, exists := content.Attributes["provider"]; exists { + var providerDiags hcl.Diagnostics + r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") + diags = append(diags, providerDiags...) + } + + if attr, exists := content.Attributes["depends_on"]; exists { + deps, depsDiags := DecodeDependsOn(attr) + diags = append(diags, depsDiags...) + r.DependsOn = append(r.DependsOn, deps...) + } + + var seenEscapeBlock *hcl.Block + var seenLifecycle *hcl.Block + for _, block := range content.Blocks { + switch block.Type { + + case "_": + if seenEscapeBlock != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate escaping block", + Detail: fmt.Sprintf( + "The special block type \"_\" can be used to force particular arguments to be interpreted as resource-type-specific rather than as meta-arguments, but each data block can have only one such block. The first escaping block was at %s.", + seenEscapeBlock.DefRange, + ), + Subject: &block.DefRange, + }) + continue + } + seenEscapeBlock = block + + // When there's an escaping block its content merges with the + // existing config we extracted earlier, so later decoding + // will see a blend of both. + r.Config = hcl.MergeBodies([]hcl.Body{r.Config, block.Body}) + + case "lifecycle": + if nested { + // We don't allow lifecycle arguments in nested data blocks, + // the lifecycle is managed by the parent block. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid lifecycle block", + Detail: `Nested data blocks do not support "lifecycle" blocks as the lifecycle is managed by the containing block.`, + Subject: block.DefRange.Ptr(), + }) + } + + if seenLifecycle != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate lifecycle block", + Detail: fmt.Sprintf("This resource already has a lifecycle block at %s.", seenLifecycle.DefRange), + Subject: block.DefRange.Ptr(), + }) + continue + } + seenLifecycle = block + + lcContent, lcDiags := block.Body.Content(resourceLifecycleBlockSchema) + diags = append(diags, lcDiags...) + // All of the attributes defined for resource lifecycle are for // managed resources only, so we can emit a common error message // for any given attributes that HCL accepted. @@ -502,36 +699,27 @@ func decodeDataBlock(block *hcl.Block, override bool) (*Resource, hcl.Diagnostic // replace_triggered_by expressions, ensuring they only contains references to // a single resource, and the only extra variables are count.index or each.key. func decodeReplaceTriggeredBy(expr hcl.Expression) ([]hcl.Expression, hcl.Diagnostics) { - // Since we are manually parsing the replace_triggered_by argument, we - // need to specially handle json configs, in which case the values will - // be json strings rather than hcl. To simplify parsing however we will - // decode the individual list elements, rather than the entire expression. - isJSON := hcljson.IsJSONExpression(expr) - exprs, diags := hcl.ExprList(expr) + if diags.HasErrors() { + return nil, diags + } for i, expr := range exprs { - if isJSON { - // We can abuse the hcl json api and rely on the fact that calling - // Value on a json expression with no EvalContext will return the - // raw string. We can then parse that as normal hcl syntax, and - // continue with the decoding. - v, ds := expr.Value(nil) - diags = diags.Extend(ds) - if diags.HasErrors() { - continue - } - - expr, ds = hclsyntax.ParseExpression([]byte(v.AsString()), "", expr.Range().Start) - diags = diags.Extend(ds) - if diags.HasErrors() { - continue - } - // make sure to swap out the expression we're returning too - exprs[i] = expr + // Since we are manually parsing the replace_triggered_by argument, we + // need to specially handle json configs, in which case the values will + // be json strings rather than hcl. To simplify parsing however we will + // decode the individual list elements, rather than the entire + // expression. + var jsDiags hcl.Diagnostics + expr, jsDiags = unwrapJSONRefExpr(expr) + diags = diags.Extend(jsDiags) + if diags.HasErrors() { + continue } + // re-assign the value in case it was replaced by a json expression + exprs[i] = expr - refs, refDiags := lang.ReferencesInExpr(expr) + refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, expr) for _, diag := range refDiags { severity := hcl.DiagError if diag.Severity() == tfdiags.Warning { @@ -731,7 +919,12 @@ var commonResourceAttributes = []hcl.AttributeSchema{ }, } -var resourceBlockSchema = &hcl.BodySchema{ +// ResourceBlockSchema is the schema for a resource or data resource type within +// Terraform. +// +// This schema is public as it is required elsewhere in order to validate and +// use generated config. +var ResourceBlockSchema = &hcl.BodySchema{ Attributes: commonResourceAttributes, Blocks: []hcl.BlockHeaderSchema{ {Type: "locals"}, // reserved for future use @@ -751,6 +944,15 @@ var dataBlockSchema = &hcl.BodySchema{ }, } +var ephemeralBlockSchema = &hcl.BodySchema{ + Attributes: commonResourceAttributes, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "lifecycle"}, + {Type: "locals"}, // reserved for future use + {Type: "_"}, // meta-argument escaping block + }, +} + var resourceLifecycleBlockSchema = &hcl.BodySchema{ // We tell HCL that these elements are all valid for both "resource" // and "data" lifecycle blocks, but the rules are actually more restrictive @@ -773,5 +975,6 @@ var resourceLifecycleBlockSchema = &hcl.BodySchema{ Blocks: []hcl.BlockHeaderSchema{ {Type: "precondition"}, {Type: "postcondition"}, + {Type: "action_trigger"}, }, } diff --git a/internal/configs/source_bundle_parser.go b/internal/configs/source_bundle_parser.go new file mode 100644 index 0000000000..edac0cf014 --- /dev/null +++ b/internal/configs/source_bundle_parser.go @@ -0,0 +1,237 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclparse" +) + +// SourceBundleParser is the main interface to read configuration files and +// other related files from a source bundle. This is a subset of the +// functionality implemented by [Parser], specifically ignoring tftest files, +// which are not relevant for now. +type SourceBundleParser struct { + sources *sourcebundle.Bundle + p *hclparse.Parser + + // allowExperiments controls whether we will allow modules to opt in to + // experimental language features. In main code this will be set only + // for alpha releases and some development builds. Test code must decide + // for itself whether to enable it so that tests can cover both the + // allowed and not-allowed situations. + allowExperiments bool +} + +// NewSourceBundleParser creates a new [SourceBundleParser] for the given +// source bundle. +func NewSourceBundleParser(sources *sourcebundle.Bundle) *SourceBundleParser { + return &SourceBundleParser{ + sources: sources, + p: hclparse.NewParser(), + } +} + +// LoadConfigDir is the primary public entry point for [SourceBundleParser], +// and is similar to [Parser.LoadConfigDir]. It reads the .tf and .tf.json +// files at the given source address as config files, and combines these into a +// single [Module]. +func (p *SourceBundleParser) LoadConfigDir(source sourceaddrs.FinalSource) (*Module, hcl.Diagnostics) { + primarySources, overrideSources, diags := p.dirSources(source) + if diags.HasErrors() { + return nil, diags + } + + primary, fDiags := p.loadSources(primarySources, false) + diags = append(diags, fDiags...) + override, fDiags := p.loadSources(overrideSources, true) + diags = append(diags, fDiags...) + + mod, modDiags := NewModule(primary, override) + diags = append(diags, modDiags...) + + sourceDir, err := p.sources.LocalPathForSource(source) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot find configuration source code", + Detail: fmt.Sprintf("Failed to load %s from the pre-installed source packages: %s. This is a bug in Terraform - please report it.", source, err), + }) + return nil, diags + } + mod.SourceDir = sourceDir + + return mod, diags +} + +// IsConfigDir is used to detect directories which have no config files, so +// that we can return useful early diagnostics when a given root module source +// address points at a directory which is not Terraform module. +func (p *SourceBundleParser) IsConfigDir(source sourceaddrs.FinalSource) bool { + primaryPaths, overridePaths, _ := p.dirSources(source) + return (len(primaryPaths) + len(overridePaths)) > 0 +} + +// Bundle returns the source bundle that this parser is reading from. +func (p *SourceBundleParser) Bundle() *sourcebundle.Bundle { + return p.sources +} + +func (p *SourceBundleParser) dirSources(source sourceaddrs.FinalSource) (primary, override []sourceaddrs.FinalSource, diags hcl.Diagnostics) { + localDir, err := p.sources.LocalPathForSource(source) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot find configuration source code", + Detail: fmt.Sprintf("Failed to load %s from the pre-installed source packages: %s.", source, err), + }) + return + } + + allEntries, err := os.ReadDir(localDir) + if err != nil { + if os.IsNotExist(err) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing Terraform configuration", + Detail: fmt.Sprintf("There is no Terraform configuration directory at %s.", source), + }) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot read Terraform configuration", + // In this case the error message from the Go standard library + // is likely to disclose the real local directory name + // from the source bundle, but that's okay because it may + // sometimes help with debugging. + Detail: fmt.Sprintf("Error while reading the cached snapshot of %s: %s.", source, err), + }) + } + return + } + + for _, entry := range allEntries { + if entry.IsDir() { + continue + } + + name := entry.Name() + ext := fileExt(name) + if ext == "" || IsIgnoredFile(name) { + continue + } + + if ext == ".tftest.hcl" || ext == ".tftest.json" { + continue + } + + baseName := name[:len(name)-len(ext)] // strip extension + isOverride := baseName == "override" || strings.HasSuffix(baseName, "_override") + + asLocalSourcePath := "./" + filepath.Base(name) + relSource, err := sourceaddrs.ParseLocalSource(asLocalSourcePath) + if err != nil { + // If we get here then it's a bug in how we constructed the + // path above, not invalid user input. + panic(fmt.Sprintf("constructed invalid relative source path: %s", err)) + } + fileSourceAddr, err := sourceaddrs.ResolveRelativeFinalSource(source, relSource) + if err != nil { + // If we get here then it's a bug in how we constructed the + // path above, not invalid user input. + panic(fmt.Sprintf("constructed invalid relative source path: %s", err)) + } + + if isOverride { + override = append(override, fileSourceAddr) + } else { + primary = append(primary, fileSourceAddr) + } + } + + return +} + +func (p *SourceBundleParser) loadSources(sources []sourceaddrs.FinalSource, override bool) ([]*File, hcl.Diagnostics) { + var files []*File + var diags hcl.Diagnostics + + for _, path := range sources { + f, fDiags := p.loadConfigFile(path, override) + diags = append(diags, fDiags...) + if f != nil { + files = append(files, f) + } + } + + return files, diags +} + +func (p *SourceBundleParser) loadConfigFile(source sourceaddrs.FinalSource, override bool) (*File, hcl.Diagnostics) { + var diags hcl.Diagnostics + path, err := p.sources.LocalPathForSource(source) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot find configuration source code", + Detail: fmt.Sprintf("Failed to load %s from the pre-installed source packages: %s.", source, err), + }) + return nil, diags + } + + src, err := os.ReadFile(path) + if err != nil { + return nil, hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Failed to read file", + Detail: fmt.Sprintf("The file %q could not be read.", path), + }, + } + } + + // NOTE: this synthetic filename is intentionally a string rendering of the + // file's source address, which in many cases is _not_ a path name. We use + // the full source address in order to allow later consumers of diagnostics + // to look up the configuration file from the source bundle. We use this in + // the filename field of the diagnostic source to achieve this. + syntheticFilename := source.String() + + var file *hcl.File + var fdiags hcl.Diagnostics + switch { + case strings.HasSuffix(path, ".json"): + file, fdiags = p.p.ParseJSON(src, syntheticFilename) + default: + file, fdiags = p.p.ParseHCL(src, syntheticFilename) + } + diags = append(diags, fdiags...) + + body := hcl.EmptyBody() + if file != nil && file.Body != nil { + body = file.Body + } + + return parseConfigFile(body, diags, override, p.allowExperiments) +} + +// AllowLanguageExperiments specifies whether subsequent LoadConfigFile (and +// similar) calls will allow opting in to experimental language features. +// +// If this method is never called for a particular parser, the default behavior +// is to disallow language experiments. +// +// Main code should set this only for alpha or development builds. Test code +// is responsible for deciding for itself whether and how to call this +// method. +func (p *SourceBundleParser) AllowLanguageExperiments(allowed bool) { + p.allowExperiments = allowed +} diff --git a/internal/configs/test_file.go b/internal/configs/test_file.go new file mode 100644 index 0000000000..fb2cdce6c6 --- /dev/null +++ b/internal/configs/test_file.go @@ -0,0 +1,959 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getmodules/moduleaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestCommand represents the Terraform a given run block will execute, plan +// or apply. Defaults to apply. +type TestCommand rune + +// TestMode represents the plan mode that Terraform will use for a given run +// block, normal or refresh-only. Defaults to normal. +type TestMode rune + +const ( + // ApplyTestCommand causes the run block to execute a Terraform apply + // operation. + ApplyTestCommand TestCommand = 0 + + // PlanTestCommand causes the run block to execute a Terraform plan + // operation. + PlanTestCommand TestCommand = 'P' + + // NormalTestMode causes the run block to execute in plans.NormalMode. + NormalTestMode TestMode = 0 + + // RefreshOnlyTestMode causes the run block to execute in + // plans.RefreshOnlyMode. + RefreshOnlyTestMode TestMode = 'R' +) + +// TestFile represents a single test file within a `terraform test` execution. +// +// A test file is made up of a sequential list of run blocks, each designating +// a command to execute and a series of validations to check after the command. +type TestFile struct { + // Variables defines a set of global variable definitions that should be set + // for every run block within the test file. + Variables map[string]hcl.Expression + + // Providers defines a set of providers that are available to run blocks + // within this test file. + // + // Some or all of these providers may be mocked providers. + // + // If empty, tests should use the default providers for the module under + // test. + Providers map[string]*Provider + + // Overrides contains any specific overrides that should be applied for this + // test outside any mock providers. + Overrides addrs.Map[addrs.Targetable, *Override] + + // Runs defines the sequential list of run blocks that should be executed in + // order. + Runs []*TestRun + + Config *TestFileConfig + + VariablesDeclRange hcl.Range +} + +// TestFileConfig represents the configuration block within a test file. +type TestFileConfig struct { + // Parallel: Indicates if test runs should be executed in parallel. + Parallel bool + + DeclRange hcl.Range +} + +// TestRun represents a single run block within a test file. +// +// Each run block represents a single Terraform command to be executed and a set +// of validations to run after the command. +type TestRun struct { + Name string + + // Command is the Terraform command to execute. + // + // One of ['apply', 'plan']. + Command TestCommand + + // Options contains the embedded plan options that will affect the given + // Command. These should map to the options documented here: + // - https://developer.hashicorp.com/terraform/cli/commands/plan#planning-options + // + // Note, that the Variables are a top level concept and not embedded within + // the options despite being listed as plan options in the documentation. + Options *TestRunOptions + + // Variables defines a set of variable definitions for this command. + // + // Any variables specified locally that clash with the global variables will + // take precedence over the global definition. + Variables map[string]hcl.Expression + + // Overrides contains any specific overrides that should be applied for this + // run block only outside any mock providers or overrides from the file. + Overrides addrs.Map[addrs.Targetable, *Override] + + // Providers specifies the set of providers that should be loaded into the + // module for this run block. + // + // Providers specified here must be configured in one of the provider blocks + // for this file. If empty, the run block will load the default providers + // for the module under test. + Providers []PassedProviderConfig + + // CheckRules defines the list of assertions/validations that should be + // checked by this run block. + CheckRules []*CheckRule + + // Module defines an address of another module that should be loaded and + // executed as part of this run block instead of the module under test. + // + // In the initial version of the testing framework we will only support + // loading alternate modules from local directories or the registry. + Module *TestRunModuleCall + + // ConfigUnderTest describes the configuration this run block should execute + // against. + // + // In typical cases, this will be null and the config under test is the + // configuration within the directory the terraform test command is + // executing within. However, when Module is set the config under test is + // whichever config is defined by Module. This field is then set during the + // configuration load process and should be used when the test is executed. + ConfigUnderTest *Config + + // File is a reference to the parent TestFile that contains this run block. + File *TestFile + + // ExpectFailures should be a list of checkable objects that are expected + // to report a failure from their custom conditions as part of this test + // run. + ExpectFailures []hcl.Traversal + + // StateKey when given, will be used to identify the state file to use for + // this test run. If not provided, the state key is derived from the + // configuration under test. + StateKey string + + // Parallel: Indicates if the test run should be executed in parallel. + // This, in combination with the state key, will determine if the test run + // will be executed in parallel with other test runs. + Parallel bool + + NameDeclRange hcl.Range + VariablesDeclRange hcl.Range + DeclRange hcl.Range +} + +// Validate does a very simple and cursory check across the test file to look +// for simple issues we can highlight early on. +// +// This function only returns warnings in the diagnostics. Callers of this +// function usually do not validate the returned diagnostics as a result. If +// you change this function, make sure to update the callers as well. +func (file *TestFile) Validate(config *Config) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + for _, provider := range file.Providers { + if !provider.Mock { + continue + } + + for _, elem := range provider.MockData.Overrides.Elems { + if elem.Value.Source != MockProviderOverrideSource { + // Only check overrides that are defined directly within the + // mock provider block of this file. This is a safety check + // against any override blocks loaded from a dedicated data + // file, for these we won't raise warnings if they target + // resources that don't exist. + continue + } + + if !file.canTarget(config, elem.Key) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Invalid override target", + Detail: fmt.Sprintf("The override target %s does not exist within the configuration under test. This could indicate a typo in the target address or an unnecessary override.", elem.Key), + Subject: elem.Value.TargetRange.Ptr(), + }) + } + } + } + + for _, elem := range file.Overrides.Elems { + if !file.canTarget(config, elem.Key) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Invalid override target", + Detail: fmt.Sprintf("The override target %s does not exist within the configuration under test. This could indicate a typo in the target address or an unnecessary override.", elem.Key), + Subject: elem.Value.TargetRange.Ptr(), + }) + } + } + + return diags +} + +// canTarget is a helper function, that just checks all the available +// configurations to make sure at least one contains the specified target. +func (file *TestFile) canTarget(config *Config, target addrs.Targetable) bool { + // If the target is in the main configuration, then easy. + if config.TargetExists(target) { + return true + } + + // But, we could be targeting something in configuration loaded by one of + // the run blocks. + for _, run := range file.Runs { + if run.Module != nil { + if run.ConfigUnderTest.TargetExists(target) { + return true + } + } + } + + return false +} + +// Validate does a very simple and cursory check across the run block to look +// for simple issues we can highlight early on. +func (run *TestRun) Validate(config *Config) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // First, we want to make sure all the ExpectFailure references are the + // correct kind of reference. + for _, traversal := range run.ExpectFailures { + + reference, refDiags := addrs.ParseRefFromTestingScope(traversal) + diags = diags.Append(refDiags) + if refDiags.HasErrors() { + continue + } + + switch reference.Subject.(type) { + // You can only reference outputs, inputs, checks, and resources. + case addrs.OutputValue, addrs.InputVariable, addrs.Check, addrs.ResourceInstance, addrs.Resource: + // Do nothing, these are okay! + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid `expect_failures` reference", + Detail: fmt.Sprintf("You cannot expect failures from %s. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", reference.Subject.String()), + Subject: reference.SourceRange.ToHCL().Ptr(), + }) + } + + } + + // All the overrides defined within a run block should target an existing + // configuration block. + for _, elem := range run.Overrides.Elems { + if !config.TargetExists(elem.Key) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Invalid override target", + Detail: fmt.Sprintf("The override target %s does not exist within the configuration under test. This could indicate a typo in the target address or an unnecessary override.", elem.Key), + Subject: elem.Value.TargetRange.Ptr(), + }) + } + } + + // All the providers defined within a run block should target an existing + // provider block within the test file. + for _, ref := range run.Providers { + _, ok := run.File.Providers[ref.InParent.String()] + if !ok { + // Then this reference was invalid as we didn't have the + // specified provider in the parent. This should have been + // caught earlier in validation anyway so is unlikely to happen. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Missing provider definition for %s", ref.InParent.String()), + Detail: "This provider block references a provider definition that does not exist.", + Subject: ref.InParent.NameRange.Ptr(), + }) + } + } + + return diags +} + +// TestRunModuleCall specifies which module should be executed by a given run +// block. +type TestRunModuleCall struct { + // Source is the source of the module to test. + Source addrs.ModuleSource + + // Version is the version of the module to load from the registry. + Version VersionConstraint + + DeclRange hcl.Range + SourceDeclRange hcl.Range +} + +// TestRunOptions contains the plan options for a given run block. +type TestRunOptions struct { + // Mode is the planning mode to run in. One of ['normal', 'refresh-only']. + Mode TestMode + + // Refresh is analogous to the -refresh=false Terraform plan option. + Refresh bool + + // Replace is analogous to the -replace=ADDRESS Terraform plan option. + Replace []hcl.Traversal + + // Target is analogous to the -target=ADDRESS Terraform plan option. + Target []hcl.Traversal + + DeclRange hcl.Range +} + +func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) { + var diags hcl.Diagnostics + tf := &TestFile{ + Providers: make(map[string]*Provider), + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + } + + // we need to retrieve the file config block first, because the run blocks + // may depend on some of its settings. + configContent, remain, contentDiags := body.PartialContent(&hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{{Type: "test"}}, + }) + diags = append(diags, contentDiags...) + + var cDiags hcl.Diagnostics + tf.Config, cDiags = decodeFileConfigBlock(configContent) + diags = append(diags, cDiags...) + if diags.HasErrors() { + return nil, diags + } + + content, contentDiags := remain.Content(testFileSchema) + diags = append(diags, contentDiags...) + + runBlockNames := make(map[string]hcl.Range) + + for _, block := range content.Blocks { + switch block.Type { + case "run": + run, runDiags := decodeTestRunBlock(block, tf) + diags = append(diags, runDiags...) + if !runDiags.HasErrors() { + tf.Runs = append(tf.Runs, run) + } + + if rng, exists := runBlockNames[run.Name]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate \"run\" block names", + Detail: fmt.Sprintf("This test file already has a run named %s block defined at %s.", run.Name, rng), + Subject: block.DefRange.Ptr(), + }) + continue + } + runBlockNames[run.Name] = run.DeclRange + + case "variables": + if tf.Variables != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple \"variables\" blocks", + Detail: fmt.Sprintf("This test file already has a variables block defined at %s.", tf.VariablesDeclRange), + Subject: block.DefRange.Ptr(), + }) + continue + } + + tf.Variables = make(map[string]hcl.Expression) + tf.VariablesDeclRange = block.DefRange + + vars, varsDiags := block.Body.JustAttributes() + diags = append(diags, varsDiags...) + for _, v := range vars { + tf.Variables[v.Name] = v.Expr + } + case "provider": + provider, providerDiags := decodeProviderBlock(block, true) + diags = append(diags, providerDiags...) + if provider != nil { + key := provider.moduleUniqueKey() + if previous, exists := tf.Providers[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider block", + Detail: fmt.Sprintf("A provider for %s is already defined at %s.", key, previous.NameRange), + Subject: provider.DeclRange.Ptr(), + }) + continue + } + tf.Providers[key] = provider + } + case "mock_provider": + provider, providerDiags := decodeMockProviderBlock(block) + diags = append(diags, providerDiags...) + if provider != nil { + key := provider.moduleUniqueKey() + if previous, exists := tf.Providers[key]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider block", + Detail: fmt.Sprintf("A provider for %s is already defined at %s.", key, previous.NameRange), + Subject: provider.DeclRange.Ptr(), + }) + continue + } + tf.Providers[key] = provider + } + case "override_resource": + override, overrideDiags := decodeOverrideResourceBlock(block, false, TestFileOverrideSource) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := tf.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_resource block", + Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + tf.Overrides.Put(subject, override) + } + case "override_data": + override, overrideDiags := decodeOverrideDataBlock(block, false, TestFileOverrideSource) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := tf.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_data block", + Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + tf.Overrides.Put(subject, override) + } + case "override_module": + override, overrideDiags := decodeOverrideModuleBlock(block, false, TestFileOverrideSource) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := tf.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_module block", + Detail: fmt.Sprintf("An override_module block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + tf.Overrides.Put(subject, override) + } + } + } + + return tf, diags +} + +func decodeFileConfigBlock(fileContent *hcl.BodyContent) (*TestFileConfig, hcl.Diagnostics) { + var diags hcl.Diagnostics + + // The "test" block is optional, so we just return a nil config if it doesn't exist. + if len(fileContent.Blocks) == 0 { + return nil, diags + } + + block := fileContent.Blocks[0] + for _, other := range fileContent.Blocks[1:] { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple \"test\" blocks", + Detail: fmt.Sprintf(`This test file already has a "test" block defined at %s.`, block.DefRange), + Subject: other.DefRange.Ptr(), + }) + } + if diags.HasErrors() { + return nil, diags + } + + ret := &TestFileConfig{DeclRange: block.DefRange} + + content, contentDiags := block.Body.Content(testFileConfigBlockSchema) + diags = append(diags, contentDiags...) + if content == nil { + return ret, diags + } + + if attr, exists := content.Attributes["parallel"]; exists { + rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Parallel) + diags = append(diags, rawDiags...) + } + + return ret, diags +} + +func decodeTestRunBlock(block *hcl.Block, file *TestFile) (*TestRun, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(testRunBlockSchema) + diags = append(diags, contentDiags...) + + r := TestRun{ + Overrides: addrs.MakeMap[addrs.Targetable, *Override](), + File: file, + Name: block.Labels[0], + NameDeclRange: block.LabelRanges[0], + DeclRange: block.DefRange, + Parallel: file.Config != nil && file.Config.Parallel, + } + + if !hclsyntax.ValidIdentifier(r.Name) { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid run block name", + Detail: badIdentifierDetail, + Subject: r.NameDeclRange.Ptr(), + }) + } + + for _, block := range content.Blocks { + switch block.Type { + case "assert": + cr, crDiags := decodeCheckRuleBlock(block, false) + diags = append(diags, crDiags...) + if !crDiags.HasErrors() { + r.CheckRules = append(r.CheckRules, cr) + } + case "plan_options": + if r.Options != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple \"plan_options\" blocks", + Detail: fmt.Sprintf("This run block already has a plan_options block defined at %s.", r.Options.DeclRange), + Subject: block.DefRange.Ptr(), + }) + continue + } + + opts, optsDiags := decodeTestRunOptionsBlock(block) + diags = append(diags, optsDiags...) + if !optsDiags.HasErrors() { + r.Options = opts + } + case "variables": + if r.Variables != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple \"variables\" blocks", + Detail: fmt.Sprintf("This run block already has a variables block defined at %s.", r.VariablesDeclRange), + Subject: block.DefRange.Ptr(), + }) + continue + } + + r.Variables = make(map[string]hcl.Expression) + r.VariablesDeclRange = block.DefRange + + vars, varsDiags := block.Body.JustAttributes() + diags = append(diags, varsDiags...) + for _, v := range vars { + r.Variables[v.Name] = v.Expr + } + case "module": + if r.Module != nil { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple \"module\" blocks", + Detail: fmt.Sprintf("This run block already has a module block defined at %s.", r.Module.DeclRange), + Subject: block.DefRange.Ptr(), + }) + } + + module, moduleDiags := decodeTestRunModuleBlock(block) + diags = append(diags, moduleDiags...) + if !moduleDiags.HasErrors() { + r.Module = module + } + case "override_resource": + override, overrideDiags := decodeOverrideResourceBlock(block, false, RunBlockOverrideSource) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := r.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_resource block", + Detail: fmt.Sprintf("An override_resource block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + r.Overrides.Put(subject, override) + } + case "override_data": + override, overrideDiags := decodeOverrideDataBlock(block, false, RunBlockOverrideSource) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := r.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_data block", + Detail: fmt.Sprintf("An override_data block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + r.Overrides.Put(subject, override) + } + case "override_module": + override, overrideDiags := decodeOverrideModuleBlock(block, false, RunBlockOverrideSource) + diags = append(diags, overrideDiags...) + + if override != nil && override.Target != nil { + subject := override.Target.Subject + if previous, ok := r.Overrides.GetOk(subject); ok { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate override_module block", + Detail: fmt.Sprintf("An override_module block targeting %s has already been defined at %s.", subject, previous.Range), + Subject: override.Range.Ptr(), + }) + continue + } + r.Overrides.Put(subject, override) + } + } + } + + if r.Variables == nil { + // There is no distinction between a nil map of variables or an empty + // map, but we can avoid any potential nil pointer exceptions by just + // creating an empty map. + r.Variables = make(map[string]hcl.Expression) + } + + if r.Options == nil { + // Create an options with default values if the user didn't specify + // anything. + r.Options = &TestRunOptions{ + Mode: NormalTestMode, + Refresh: true, + } + } + + if attr, exists := content.Attributes["command"]; exists { + switch hcl.ExprAsKeyword(attr.Expr) { + case "apply": + r.Command = ApplyTestCommand + case "plan": + r.Command = PlanTestCommand + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid \"command\" keyword", + Detail: "The \"command\" argument requires one of the following keywords without quotes: apply or plan.", + Subject: attr.Expr.Range().Ptr(), + }) + } + } else { + r.Command = ApplyTestCommand // Default to apply + } + + if attr, exists := content.Attributes["providers"]; exists { + providers, providerDiags := decodePassedProviderConfigs(attr) + diags = append(diags, providerDiags...) + r.Providers = append(r.Providers, providers...) + } + + if attr, exists := content.Attributes["expect_failures"]; exists { + failures, failDiags := DecodeDependsOn(attr) + diags = append(diags, failDiags...) + r.ExpectFailures = failures + } + + if attr, exists := content.Attributes["state_key"]; exists { + rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.StateKey) + diags = append(diags, rawDiags...) + } + + if attr, exists := content.Attributes["parallel"]; exists { + rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &r.Parallel) + diags = append(diags, rawDiags...) + } + + return &r, diags +} + +func decodeTestRunModuleBlock(block *hcl.Block) (*TestRunModuleCall, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(testRunModuleBlockSchema) + diags = append(diags, contentDiags...) + + module := TestRunModuleCall{ + DeclRange: block.DefRange, + } + + haveVersionArg := false + if attr, exists := content.Attributes["version"]; exists { + var versionDiags hcl.Diagnostics + module.Version, versionDiags = decodeVersionConstraint(attr) + diags = append(diags, versionDiags...) + haveVersionArg = true + } + + if attr, exists := content.Attributes["source"]; exists { + module.SourceDeclRange = attr.Range + + var raw string + rawDiags := gohcl.DecodeExpression(attr.Expr, nil, &raw) + diags = append(diags, rawDiags...) + if !rawDiags.HasErrors() { + var err error + if haveVersionArg { + module.Source, err = moduleaddrs.ParseModuleSourceRegistry(raw) + } else { + module.Source, err = moduleaddrs.ParseModuleSource(raw) + } + if err != nil { + // NOTE: We leave mc.SourceAddr as nil for any situation where the + // source attribute is invalid, so any code which tries to carefully + // use the partial result of a failed config decode must be + // resilient to that. + module.Source = nil + + // NOTE: In practice it's actually very unlikely to end up here, + // because our source address parser can turn just about any string + // into some sort of remote package address, and so for most errors + // we'll detect them only during module installation. There are + // still a _few_ purely-syntax errors we can catch at parsing time, + // though, mostly related to remote package sub-paths and local + // paths. + switch err := err.(type) { + case *moduleaddrs.MaybeRelativePathErr: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid module source address", + Detail: fmt.Sprintf( + "Terraform failed to determine your intended installation method for remote module package %q.\n\nIf you intended this as a path relative to the current module, use \"./%s\" instead. The \"./\" prefix indicates that the address is a relative filesystem path.", + err.Addr, err.Addr, + ), + Subject: module.SourceDeclRange.Ptr(), + }) + default: + if haveVersionArg { + // In this case we'll include some extra context that + // we assumed a registry source address due to the + // version argument. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid registry module source address", + Detail: fmt.Sprintf("Failed to parse module registry address: %s.\n\nTerraform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", err), + Subject: module.SourceDeclRange.Ptr(), + }) + } else { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid module source address", + Detail: fmt.Sprintf("Failed to parse module source address: %s.", err), + Subject: module.SourceDeclRange.Ptr(), + }) + } + } + } + + switch module.Source.(type) { + case addrs.ModuleSourceRemote: + // We only support local or registry modules when loading + // modules directly from alternate sources during a test + // execution. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid module source address", + Detail: "Only local or registry module sources are currently supported from within test run blocks.", + Subject: module.SourceDeclRange.Ptr(), + }) + } + } + } else { + // Must have a source attribute. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing \"source\" attribute for module block", + Detail: "You must specify a source attribute when executing alternate modules during test executions.", + Subject: module.DeclRange.Ptr(), + }) + } + + return &module, diags +} + +func decodeTestRunOptionsBlock(block *hcl.Block) (*TestRunOptions, hcl.Diagnostics) { + var diags hcl.Diagnostics + + content, contentDiags := block.Body.Content(testRunOptionsBlockSchema) + diags = append(diags, contentDiags...) + + opts := TestRunOptions{ + DeclRange: block.DefRange, + } + + if attr, exists := content.Attributes["mode"]; exists { + switch hcl.ExprAsKeyword(attr.Expr) { + case "refresh-only": + opts.Mode = RefreshOnlyTestMode + case "normal": + opts.Mode = NormalTestMode + default: + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid \"mode\" keyword", + Detail: "The \"mode\" argument requires one of the following keywords without quotes: normal or refresh-only", + Subject: attr.Expr.Range().Ptr(), + }) + } + } else { + opts.Mode = NormalTestMode // Default to normal + } + + if attr, exists := content.Attributes["refresh"]; exists { + diags = append(diags, gohcl.DecodeExpression(attr.Expr, nil, &opts.Refresh)...) + } else { + // Defaults to true. + opts.Refresh = true + } + + if attr, exists := content.Attributes["replace"]; exists { + reps, repsDiags := DecodeDependsOn(attr) + diags = append(diags, repsDiags...) + opts.Replace = reps + } + + if attr, exists := content.Attributes["target"]; exists { + tars, tarsDiags := DecodeDependsOn(attr) + diags = append(diags, tarsDiags...) + opts.Target = tars + } + + if !opts.Refresh && opts.Mode == RefreshOnlyTestMode { + // These options are incompatible. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Incompatible plan options", + Detail: "The \"refresh\" option cannot be set to false when running a test in \"refresh-only\" mode.", + Subject: content.Attributes["refresh"].Range.Ptr(), + }) + } + + return &opts, diags +} + +var testFileSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "run", + LabelNames: []string{"name"}, + }, + { + Type: "provider", + LabelNames: []string{"name"}, + }, + { + Type: "mock_provider", + LabelNames: []string{"name"}, + }, + { + Type: "variables", + }, + { + Type: "override_resource", + }, + { + Type: "override_data", + }, + { + Type: "override_module", + }, + }, +} + +var testFileConfigBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "parallel"}, + }, +} + +var testRunBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "command"}, + {Name: "providers"}, + {Name: "expect_failures"}, + {Name: "state_key"}, + {Name: "parallel"}, + }, + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "plan_options", + }, + { + Type: "assert", + }, + { + Type: "variables", + }, + { + Type: "module", + }, + { + Type: "override_resource", + }, + { + Type: "override_data", + }, + { + Type: "override_module", + }, + }, +} + +var testRunOptionsBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "mode"}, + {Name: "refresh"}, + {Name: "replace"}, + {Name: "target"}, + }, +} + +var testRunModuleBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "source"}, + {Name: "version"}, + }, +} diff --git a/internal/configs/test_file_test.go b/internal/configs/test_file_test.go new file mode 100644 index 0000000000..749af9bd72 --- /dev/null +++ b/internal/configs/test_file_test.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package configs + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +func TestTestRun_Validate(t *testing.T) { + tcs := map[string]struct { + expectedFailures []string + diagnostic string + }{ + "empty": {}, + "supports_expected": { + expectedFailures: []string{ + "check.expected_check", + "var.expected_var", + "output.expected_output", + "test_resource.resource", + "resource.test_resource.resource", + "data.test_resource.resource", + }, + }, + "count": { + expectedFailures: []string{ + "count.index", + }, + diagnostic: "You cannot expect failures from count.index. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", + }, + "foreach": { + expectedFailures: []string{ + "each.key", + }, + diagnostic: "You cannot expect failures from each.key. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", + }, + "local": { + expectedFailures: []string{ + "local.value", + }, + diagnostic: "You cannot expect failures from local.value. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", + }, + "module": { + expectedFailures: []string{ + "module.my_module", + }, + diagnostic: "You cannot expect failures from module.my_module. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", + }, + "path": { + expectedFailures: []string{ + "path.walk", + }, + diagnostic: "You cannot expect failures from path.walk. You can only expect failures from checkable objects such as input variables, output values, check blocks, managed resources and data sources.", + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + run := &TestRun{} + for _, addr := range tc.expectedFailures { + run.ExpectFailures = append(run.ExpectFailures, parseTraversal(t, addr)) + } + + diags := run.Validate(nil) + + if len(diags) > 1 { + t.Fatalf("too many diags: %d", len(diags)) + } + + if len(tc.diagnostic) == 0 { + if len(diags) != 0 { + t.Fatalf("expected no diags but got: %s", diags[0].Description().Detail) + } + + return + } + + if diff := cmp.Diff(tc.diagnostic, diags[0].Description().Detail); len(diff) > 0 { + t.Fatalf("unexpected diff:\n%s", diff) + } + }) + } +} + +func parseTraversal(t *testing.T, addr string) hcl.Traversal { + t.Helper() + + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(addr), "", hcl.InitialPos) + if diags.HasErrors() { + t.Fatalf("invalid address: %s", diags.Error()) + } + return traversal +} diff --git a/internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf b/internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf new file mode 100644 index 0000000000..bb8cb139d1 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/import-in-child-module/child/main.tf @@ -0,0 +1,6 @@ +resource "aws_instance" "web" {} + +import { + to = aws_instance.web + id = "test" +} \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/import-in-child-module/errors b/internal/configs/testdata/config-diagnostics/import-in-child-module/errors new file mode 100644 index 0000000000..b0a5ac4fc1 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/import-in-child-module/errors @@ -0,0 +1 @@ +import-in-child-module/child/main.tf:3,1-7: Invalid import configuration; An import block was detected in "module.child". Import blocks are only allowed in the root module. \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf b/internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf new file mode 100644 index 0000000000..3133e57b93 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/import-in-child-module/root.tf @@ -0,0 +1,10 @@ +resource "aws_instance" "web" {} + +import { + to = aws_instance.web + id = "test" +} + +module "child" { + source = "./child" +} \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/errors b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/errors new file mode 100644 index 0000000000..99fe7fab37 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/errors @@ -0,0 +1,3 @@ +testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tftest.hcl:2,1-15: Provider type mismatch; The provider "foo" in main.tftest.hcl represents provider "registry.terraform.io/hashicorp/bar", but "foo" in the root module represents "registry.terraform.io/hashicorp/foo".\n\nThis means the provider definition for "foo" within main.tftest.hcl, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types. +testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tftest.hcl:4,1-15: Provider type mismatch; The provider "foo.bar" in main.tftest.hcl represents provider "registry.terraform.io/hashicorp/bar", but "foo.bar" in the root module represents "registry.terraform.io/hashicorp/foo".\n\nThis means the provider definition for "foo.bar" within main.tftest.hcl, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types. +testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tftest.hcl:8,1-15: Provider type mismatch; The provider "bar" in main.tftest.hcl represents provider "registry.terraform.io/hashicorp/foo", but "bar" in the root module represents "registry.terraform.io/hashicorp/bar".\n\nThis means the provider definition for "bar" within main.tftest.hcl has been referenced by multiple run blocks and assigned to different provider types. \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tf b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tf new file mode 100644 index 0000000000..3b15ce42c7 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + foo = { + source = "hashicorp/foo" + configuration_aliases = [foo.bar] + } + } +} + +provider "bar" {} + +resource "foo_resource" "resource" {} + +resource "bar_resource" "resource" {} diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tftest.hcl b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tftest.hcl new file mode 100644 index 0000000000..4e93da6dfd --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/main.tftest.hcl @@ -0,0 +1,18 @@ + +provider "foo" {} + +provider "foo" { + alias = "bar" +} + +provider "bar" {} + +run "setup_module" { + + module { + source = "./setup" + } + +} + +run "main_module" {} diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/setup/main.tf b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/setup/main.tf new file mode 100644 index 0000000000..beb7059325 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch-with-module/setup/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + foo = { + source = "hashicorp/bar" + configuration_aliases = [ foo.bar ] + } + bar = { + source = "hashicorp/foo" + } + } +} + +resource "foo_resource" "resource" {} + +resource "bar_resource" "resource" {} diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/errors b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/errors new file mode 100644 index 0000000000..50024d044c --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/errors @@ -0,0 +1 @@ +testdata/config-diagnostics/tests-provider-mismatch/main.tftest.hcl:27,11-14: Provider type mismatch; The local name "bar" in main.tftest.hcl represents provider "registry.terraform.io/hashicorp/bar", but "foo" in the root module represents "registry.terraform.io/hashicorp/foo".\n\nThis means the provider definition for "bar" within main.tftest.hcl, or other provider definitions with the same name, have been referenced by multiple run blocks and assigned to different provider types. \ No newline at end of file diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/main.tf b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/main.tf new file mode 100644 index 0000000000..3b15ce42c7 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + foo = { + source = "hashicorp/foo" + configuration_aliases = [foo.bar] + } + } +} + +provider "bar" {} + +resource "foo_resource" "resource" {} + +resource "bar_resource" "resource" {} diff --git a/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/main.tftest.hcl b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/main.tftest.hcl new file mode 100644 index 0000000000..adf7a650e0 --- /dev/null +++ b/internal/configs/testdata/config-diagnostics/tests-provider-mismatch/main.tftest.hcl @@ -0,0 +1,32 @@ + +provider "foo" {} + +provider "foo" { + alias = "bar" +} + +provider "bar" { + alias = "foo" +} + +run "default_should_be_fine" {} + +run "bit_complicated_still_okay" { + + providers = { + foo = foo + foo.bar = foo.bar + bar = bar.foo + } + +} + +run "mismatched_foo_direct" { + + providers = { + foo = bar // bad! + foo.bar = foo.bar + bar = bar.foo + } + +} diff --git a/internal/configs/testdata/invalid-files/ephemeral-invalid-lifecycle.tf b/internal/configs/testdata/invalid-files/ephemeral-invalid-lifecycle.tf new file mode 100644 index 0000000000..db67b13856 --- /dev/null +++ b/internal/configs/testdata/invalid-files/ephemeral-invalid-lifecycle.tf @@ -0,0 +1,5 @@ +ephemeral "test_resource" "test" { + lifecycle { + create_before_destroy = true + } +} \ No newline at end of file diff --git a/internal/configs/testdata/invalid-files/ephemeral-invalid-name.tf b/internal/configs/testdata/invalid-files/ephemeral-invalid-name.tf new file mode 100644 index 0000000000..8e2904ce25 --- /dev/null +++ b/internal/configs/testdata/invalid-files/ephemeral-invalid-name.tf @@ -0,0 +1,2 @@ +ephemeral "test resource" "nope" { +} \ No newline at end of file diff --git a/internal/configs/testdata/invalid-files/import-for-each.tf b/internal/configs/testdata/invalid-files/import-for-each.tf new file mode 100644 index 0000000000..622d25ba34 --- /dev/null +++ b/internal/configs/testdata/invalid-files/import-for-each.tf @@ -0,0 +1,9 @@ +import { + for_each = ["a", "b"] + to = invalid[each.value] + id = each.value +} + +resource "test_resource" "test" { + for_each = toset(["a", "b"]) +} diff --git a/internal/configs/testdata/invalid-files/import-for-each.tf.json b/internal/configs/testdata/invalid-files/import-for-each.tf.json new file mode 100644 index 0000000000..041060d250 --- /dev/null +++ b/internal/configs/testdata/invalid-files/import-for-each.tf.json @@ -0,0 +1,14 @@ +{ + "import": { + "for_each": "[\"a\", \"b\"]", + "to": ["test_resource.test[wrong]"], + "id": "${each.value}" + }, + "resource": { + "test_resource": { + "test": { + "for_each": {"a":"a","b":"b"} + } + } + } +} diff --git a/internal/configs/testdata/invalid-files/resource-rtb.tf.json b/internal/configs/testdata/invalid-files/resource-rtb.tf.json new file mode 100644 index 0000000000..a2389b2cc7 --- /dev/null +++ b/internal/configs/testdata/invalid-files/resource-rtb.tf.json @@ -0,0 +1,18 @@ +{ + "resource": { + "test_object": { + "a": { + "count": 1, + "test_string": "new" + }, + "b": { + "count": 1, + "lifecycle": { + "replace_triggered_by": [ + {"test_object.a[count.index].test_string":"nope"} + ] + } + } + } + } +} diff --git a/internal/configs/testdata/invalid-files/variable-validation-condition-noself.tf b/internal/configs/testdata/invalid-files/variable-validation-condition-noself.tf new file mode 100644 index 0000000000..3571a58ab1 --- /dev/null +++ b/internal/configs/testdata/invalid-files/variable-validation-condition-noself.tf @@ -0,0 +1,10 @@ +locals { + something = "else" +} + +variable "validation" { + validation { + condition = local.something == "else" + error_message = "Something else." + } +} diff --git a/internal/configs/testdata/invalid-import-files/import-and-module-clash.tf b/internal/configs/testdata/invalid-import-files/import-and-module-clash.tf new file mode 100644 index 0000000000..3bd56565b4 --- /dev/null +++ b/internal/configs/testdata/invalid-import-files/import-and-module-clash.tf @@ -0,0 +1,12 @@ + +module "importable_resource" { + source = "../valid-modules/importable-resource" +} + +provider "local" {} + +import { + provider = local + id = "foo/bar" + to = module.importable_resource.local_file.foo +} diff --git a/internal/configs/testdata/invalid-import-files/import-and-no-resource.tf b/internal/configs/testdata/invalid-import-files/import-and-no-resource.tf new file mode 100644 index 0000000000..0bb2b0121a --- /dev/null +++ b/internal/configs/testdata/invalid-import-files/import-and-no-resource.tf @@ -0,0 +1,10 @@ + +provider "local" {} + +import { + provider = local + id = "foo/bar" + to = local_file.foo_bar +} + +resource "local_file" "foo_bar" {} diff --git a/internal/configs/testdata/invalid-import-files/import-and-resource-clash.tf b/internal/configs/testdata/invalid-import-files/import-and-resource-clash.tf new file mode 100644 index 0000000000..c22b1d1a0d --- /dev/null +++ b/internal/configs/testdata/invalid-import-files/import-and-resource-clash.tf @@ -0,0 +1,16 @@ + +provider "local" {} + +provider "local" { + alias = "alternate" +} + +import { + provider = local + id = "foo/bar" + to = local_file.foo_bar +} + +resource "local_file" "foo_bar" { + provider = local.alternate +} diff --git a/internal/configs/testdata/invalid-modules/tests-duplicate-run-blocks/main.tf b/internal/configs/testdata/invalid-modules/tests-duplicate-run-blocks/main.tf new file mode 100644 index 0000000000..2c5e585478 --- /dev/null +++ b/internal/configs/testdata/invalid-modules/tests-duplicate-run-blocks/main.tf @@ -0,0 +1,8 @@ + +resource "aws_instance" "web" { + ami = "ami-1234" + security_groups = [ + "foo", + "bar", + ] +} diff --git a/internal/configs/testdata/invalid-modules/tests-duplicate-run-blocks/main.tftest.hcl b/internal/configs/testdata/invalid-modules/tests-duplicate-run-blocks/main.tftest.hcl new file mode 100644 index 0000000000..4fcd39d42b --- /dev/null +++ b/internal/configs/testdata/invalid-modules/tests-duplicate-run-blocks/main.tftest.hcl @@ -0,0 +1,4 @@ + +run "same" {} + +run "same" {} diff --git a/internal/configs/testdata/invalid-modules/tests-invalid-run-blocks/main.tf b/internal/configs/testdata/invalid-modules/tests-invalid-run-blocks/main.tf new file mode 100644 index 0000000000..2c5e585478 --- /dev/null +++ b/internal/configs/testdata/invalid-modules/tests-invalid-run-blocks/main.tf @@ -0,0 +1,8 @@ + +resource "aws_instance" "web" { + ami = "ami-1234" + security_groups = [ + "foo", + "bar", + ] +} diff --git a/internal/configs/testdata/invalid-modules/tests-invalid-run-blocks/main.tftest.hcl b/internal/configs/testdata/invalid-modules/tests-invalid-run-blocks/main.tftest.hcl new file mode 100644 index 0000000000..16bd35eb92 --- /dev/null +++ b/internal/configs/testdata/invalid-modules/tests-invalid-run-blocks/main.tftest.hcl @@ -0,0 +1,2 @@ + +run "contains spaces" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_data_overrides.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_data_overrides.tftest.hcl new file mode 100644 index 0000000000..f77b788d96 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_data_overrides.tftest.hcl @@ -0,0 +1,33 @@ +mock_provider "aws" { + override_data { + target = data.aws_instance.test + values = {} + } + + override_data { + target = data.aws_instance.test + values = {} + } +} + +override_data { + target = data.aws_instance.test + values = {} +} + +override_data { + target = data.aws_instance.test + values = {} +} + +run "test" { + override_data { + target = data.aws_instance.test + values = {} + } + + override_data { + target = data.aws_instance.test + values = {} + } +} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_file_config.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_file_config.tftest.hcl new file mode 100644 index 0000000000..a5cc35159f --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_file_config.tftest.hcl @@ -0,0 +1,13 @@ +test {} + +test {} + +test {} + +provider "aws" {} + +provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mixed_providers.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mixed_providers.tftest.hcl new file mode 100644 index 0000000000..b319143f20 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mixed_providers.tftest.hcl @@ -0,0 +1,13 @@ +provider "aws" {} + +mock_provider "aws" {} + +provider "aws" { + alias = "test" +} + +mock_provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mock_data_sources.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mock_data_sources.tftest.hcl new file mode 100644 index 0000000000..8ff1685093 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mock_data_sources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_data "aws_instance" { + defaults = {} + } + + mock_data "aws_instance" { + defaults = {} + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mock_providers.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mock_providers.tftest.hcl new file mode 100644 index 0000000000..0ac2ae2bfe --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mock_providers.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" {} + +mock_provider "aws" {} + +mock_provider "aws" { + alias = "test" +} + +mock_provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_mock_resources.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_mock_resources.tftest.hcl new file mode 100644 index 0000000000..d95a5b1c50 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_mock_resources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_resource "aws_instance" { + defaults = {} + } + + mock_resource "aws_instance" { + defaults = {} + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_module_overrides.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_module_overrides.tftest.hcl new file mode 100644 index 0000000000..176d3e5f5f --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_module_overrides.tftest.hcl @@ -0,0 +1,22 @@ + +override_module { + target = module.child + outputs = {} +} + +override_module { + target = module.child + outputs = {} +} + +run "test" { + override_module { + target = module.child + outputs = {} + } + + override_module { + target = module.child + outputs = {} + } +} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_providers.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_providers.tftest.hcl new file mode 100644 index 0000000000..a0dac23dfa --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_providers.tftest.hcl @@ -0,0 +1,13 @@ +provider "aws" {} + +provider "aws" {} + +provider "aws" { + alias = "test" +} + +provider "aws" { + alias = "test" +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/duplicate_resource_overrides.tftest.hcl b/internal/configs/testdata/invalid-test-files/duplicate_resource_overrides.tftest.hcl new file mode 100644 index 0000000000..1494449197 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/duplicate_resource_overrides.tftest.hcl @@ -0,0 +1,33 @@ +mock_provider "aws" { + override_resource { + target = aws_instance.test + values = {} + } + + override_resource { + target = aws_instance.test + values = {} + } +} + +override_resource { + target = aws_instance.test + values = {} +} + +override_resource { + target = aws_instance.test + values = {} +} + +run "test" { + override_resource { + target = aws_instance.test + values = {} + } + + override_resource { + target = aws_instance.test + values = {} + } +} diff --git a/internal/configs/testdata/invalid-test-files/invalid_data_override.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_data_override.tftest.hcl new file mode 100644 index 0000000000..3196ce49d9 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_data_override.tftest.hcl @@ -0,0 +1,10 @@ + +override_data { + target = data.aws_instance.target +} + +override_data { + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_data_override_target.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_data_override_target.tftest.hcl new file mode 100644 index 0000000000..f3172477fd --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_data_override_target.tftest.hcl @@ -0,0 +1,12 @@ + +override_data { + target = aws_instance.target + values = {} +} + +override_data { + target = module.child + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_mock_data_sources.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_mock_data_sources.tftest.hcl new file mode 100644 index 0000000000..e45371de8d --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_mock_data_sources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_data "aws_instance" {} + + mock_data "aws_ami_instance" { + defaults = { + ami = var.ami + } + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_mock_resources.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_mock_resources.tftest.hcl new file mode 100644 index 0000000000..936e57dc96 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_mock_resources.tftest.hcl @@ -0,0 +1,13 @@ +mock_provider "aws" { + + mock_resource "aws_instance" {} + + mock_resource "aws_ami_instance" { + defaults = { + ami = var.ami + } + } + +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_module_override.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_module_override.tftest.hcl new file mode 100644 index 0000000000..7562d08796 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_module_override.tftest.hcl @@ -0,0 +1,14 @@ +override_module { + target = module.child +} + +override_module { + outputs = {} +} + +override_module { + target = module.other + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_module_override_target.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_module_override_target.tftest.hcl new file mode 100644 index 0000000000..ed5081bc24 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_module_override_target.tftest.hcl @@ -0,0 +1,12 @@ + +override_module { + target = aws_instance.target + outputs = {} +} + +override_module { + target = data.aws_instance.target + outputs = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_resource_override.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_resource_override.tftest.hcl new file mode 100644 index 0000000000..2dd0f6f6b9 --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_resource_override.tftest.hcl @@ -0,0 +1,10 @@ + +override_resource { + target = aws_instance.target +} + +override_resource { + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/invalid-test-files/invalid_resource_override_target.tftest.hcl b/internal/configs/testdata/invalid-test-files/invalid_resource_override_target.tftest.hcl new file mode 100644 index 0000000000..ca3e5a317a --- /dev/null +++ b/internal/configs/testdata/invalid-test-files/invalid_resource_override_target.tftest.hcl @@ -0,0 +1,12 @@ + +override_resource { + target = data.aws_instance.target + values = {} +} + +override_resource { + target = module.child + values = {} +} + +run "test" {} diff --git a/internal/configs/testdata/only-nested-test-files/fixtures/main.tf b/internal/configs/testdata/only-nested-test-files/fixtures/main.tf new file mode 100644 index 0000000000..8e891884ea --- /dev/null +++ b/internal/configs/testdata/only-nested-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl b/internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl new file mode 100644 index 0000000000..387693d859 --- /dev/null +++ b/internal/configs/testdata/only-nested-test-files/tests/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/configs/testdata/only-test-files/fixtures/main.tf b/internal/configs/testdata/only-test-files/fixtures/main.tf new file mode 100644 index 0000000000..8e891884ea --- /dev/null +++ b/internal/configs/testdata/only-test-files/fixtures/main.tf @@ -0,0 +1,8 @@ +variable "sample" { + type = bool + default = true +} + +output "name" { + value = var.sample +} \ No newline at end of file diff --git a/internal/configs/testdata/only-test-files/main.tftest.hcl b/internal/configs/testdata/only-test-files/main.tftest.hcl new file mode 100644 index 0000000000..387693d859 --- /dev/null +++ b/internal/configs/testdata/only-test-files/main.tftest.hcl @@ -0,0 +1,9 @@ +run "foo" { + module { + source = "./fixtures" + } + assert { + condition = output.name == true + error_message = "foo" + } +} \ No newline at end of file diff --git a/internal/earlyconfig/testdata/provider-reqs/provider-reqs-root.tf b/internal/configs/testdata/provider-reqs-with-tests/provider-reqs-root.tf similarity index 50% rename from internal/earlyconfig/testdata/provider-reqs/provider-reqs-root.tf rename to internal/configs/testdata/provider-reqs-with-tests/provider-reqs-root.tf index 7325a26d15..b072ef7bab 100644 --- a/internal/earlyconfig/testdata/provider-reqs/provider-reqs-root.tf +++ b/internal/configs/testdata/provider-reqs-with-tests/provider-reqs-root.tf @@ -1,9 +1,5 @@ terraform { required_providers { - null = "~> 2.0.0" - random = { - version = "~> 1.2.0" - } tls = { source = "hashicorp/tls" version = "~> 3.0" @@ -16,6 +12,9 @@ terraform { resource "implied_foo" "bar" { } -module "child" { - source = "./child" +# There is no provider in required_providers called "terraform", but for +# this name in particular we imply terraform.io/builtin/terraform instead, +# to avoid selecting the now-unmaintained +# registry.terraform.io/hashicorp/terraform. +data "terraform_remote_state" "bar" { } diff --git a/internal/configs/testdata/provider-reqs-with-tests/provider-reqs-root.tftest.hcl b/internal/configs/testdata/provider-reqs-with-tests/provider-reqs-root.tftest.hcl new file mode 100644 index 0000000000..5f52a23a70 --- /dev/null +++ b/internal/configs/testdata/provider-reqs-with-tests/provider-reqs-root.tftest.hcl @@ -0,0 +1,9 @@ +# There is no provider in required_providers called "configured", so we won't +# have a version constraint for it. +provider "configured" {} + +run "setup" { + module { + source = "./setup" + } +} diff --git a/internal/configs/testdata/provider-reqs-with-tests/setup/setup.tf b/internal/configs/testdata/provider-reqs-with-tests/setup/setup.tf new file mode 100644 index 0000000000..ce7f37e4d8 --- /dev/null +++ b/internal/configs/testdata/provider-reqs-with-tests/setup/setup.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + null = "~> 2.0.0" + random = { + version = "~> 1.2.0" + } + } +} + +resource "configured_resource" "resource" {} diff --git a/internal/configs/testdata/valid-files/actions.tf b/internal/configs/testdata/valid-files/actions.tf new file mode 100644 index 0000000000..9e24e222a6 --- /dev/null +++ b/internal/configs/testdata/valid-files/actions.tf @@ -0,0 +1,34 @@ +action "provider_reboot" "powercycle" { + method = "biggest_hammer" // drop it in the ocean, i presume +} + +resource "aws_security_group" "firewall" { + lifecycle { + create_before_destroy = true + prevent_destroy = true + ignore_changes = [ + description, + ] + action_trigger { + events = [after_create, after_update] + condition = has_changed(self.environment) + actions = [action.provider_reboot.powercycle] + } + } + + connection { + host = "127.0.0.1" + } + + provisioner "local-exec" { + command = "echo hello" + + connection { + host = "10.1.2.1" + } + } + + provisioner "local-exec" { + command = "echo hello" + } +} diff --git a/internal/configs/testdata/valid-files/actions.tf.json b/internal/configs/testdata/valid-files/actions.tf.json new file mode 100644 index 0000000000..3ae44c582b --- /dev/null +++ b/internal/configs/testdata/valid-files/actions.tf.json @@ -0,0 +1,28 @@ +{ + "action": { + "test_action": { + "a": {} + } + }, + "resource": { + "test_object": { + "a": { + "count": 1, + "test_string": "new" + }, + "b": { + "count": 1, + "lifecycle": { + "replace_triggered_by": [ + "test_object.a[count.index].test_string" + ], + "action_trigger": { + "events": ["before_destroy"], + "actions": ["action.test_action.a"] + } + } + } + } + } + } + \ No newline at end of file diff --git a/internal/configs/testdata/valid-files/ephemeral_inputs_outputs.tf b/internal/configs/testdata/valid-files/ephemeral_inputs_outputs.tf new file mode 100644 index 0000000000..9b93d7df42 --- /dev/null +++ b/internal/configs/testdata/valid-files/ephemeral_inputs_outputs.tf @@ -0,0 +1,8 @@ +variable "in" { + ephemeral = true +} + +output "out" { + ephemeral = true + value = var.in +} diff --git a/internal/configs/testdata/valid-files/import-for-each.tf b/internal/configs/testdata/valid-files/import-for-each.tf new file mode 100644 index 0000000000..365fa34624 --- /dev/null +++ b/internal/configs/testdata/valid-files/import-for-each.tf @@ -0,0 +1,9 @@ +import { + for_each = ["a", "b"] + to = test_resource.test[each.value] + id = each.value +} + +resource "test_resource" "test" { + for_each = toset(["a", "b"]) +} diff --git a/internal/configs/testdata/valid-files/import-for-each.tf.json b/internal/configs/testdata/valid-files/import-for-each.tf.json new file mode 100644 index 0000000000..ef1be1f5a9 --- /dev/null +++ b/internal/configs/testdata/valid-files/import-for-each.tf.json @@ -0,0 +1,14 @@ +{ + "import": { + "for_each": "[\"a\", \"b\"]", + "to": "test_resource.test[each.value]", + "id": "${each.value}" + }, + "resource": { + "test_resource": { + "test": { + "for_each": {"a":"a","b":"b"} + } + } + } +} diff --git a/internal/configs/testdata/valid-files/object-optional-attrs.tf b/internal/configs/testdata/valid-files/object-optional-attrs.tf new file mode 100644 index 0000000000..8b7fda9a74 --- /dev/null +++ b/internal/configs/testdata/valid-files/object-optional-attrs.tf @@ -0,0 +1,38 @@ +variable "a" { + type = object({ + foo = optional(string) + bar = optional(bool, true) + }) +} + +variable "b" { + type = list( + object({ + foo = optional(string) + }) + ) +} + +variable "c" { + type = set( + object({ + foo = optional(string) + }) + ) +} + +variable "d" { + type = map( + object({ + foo = optional(string) + }) + ) +} + +variable "e" { + type = object({ + foo = string + bar = optional(bool, true) + }) + default = null +} diff --git a/internal/configs/testdata/valid-files/providers-explicit-implied.tf b/internal/configs/testdata/valid-files/providers-explicit-implied.tf index 778b0aa539..49c063a1e2 100644 --- a/internal/configs/testdata/valid-files/providers-explicit-implied.tf +++ b/internal/configs/testdata/valid-files/providers-explicit-implied.tf @@ -14,6 +14,17 @@ resource "null_resource" "foo" { } +import { + id = "directory/filename" + to = local_file.foo +} + +import { + provider = template.foo + id = "directory/foo_filename" + to = local_file.bar +} + terraform { required_providers { test = { diff --git a/internal/configs/testdata/valid-files/resources.tf b/internal/configs/testdata/valid-files/resources.tf index aab038ea8c..6b88b0a1ae 100644 --- a/internal/configs/testdata/valid-files/resources.tf +++ b/internal/configs/testdata/valid-files/resources.tf @@ -47,3 +47,12 @@ resource "aws_instance" "depends" { replace_triggered_by = [ aws_instance.web[1], aws_security_group.firewall.id ] } } + +ephemeral "aws_connect" "tunnel" { +} + +ephemeral "aws_secret" "auth" { + for_each = local.auths + input = each.value + depends_on = [aws_instance.depends] +} diff --git a/internal/configs/testdata/invalid-files/variable-validation-condition-badref.tf b/internal/configs/testdata/valid-files/variable-validation-condition-crossref.tf similarity index 53% rename from internal/configs/testdata/invalid-files/variable-validation-condition-badref.tf rename to internal/configs/testdata/valid-files/variable-validation-condition-crossref.tf index 9b9e935767..adfdd7c31e 100644 --- a/internal/configs/testdata/invalid-files/variable-validation-condition-badref.tf +++ b/internal/configs/testdata/valid-files/variable-validation-condition-crossref.tf @@ -5,7 +5,7 @@ locals { variable "validation" { validation { - condition = local.foo == var.validation # ERROR: Invalid reference in variable validation + condition = local.foo == var.validation error_message = "Must be five." } } @@ -13,6 +13,6 @@ variable "validation" { variable "validation_error_expression" { validation { condition = var.validation_error_expression != 1 - error_message = "Cannot equal ${local.foo}." # ERROR: Invalid reference in variable validation + error_message = "Cannot equal ${local.foo}." } } diff --git a/internal/configs/testdata/valid-modules/importable-resource/main.tf b/internal/configs/testdata/valid-modules/importable-resource/main.tf new file mode 100644 index 0000000000..3bd2dcb5c5 --- /dev/null +++ b/internal/configs/testdata/valid-modules/importable-resource/main.tf @@ -0,0 +1,2 @@ + +resource "local_file" "foo" {} diff --git a/internal/configs/testdata/valid-modules/importable-resource/providers.tf b/internal/configs/testdata/valid-modules/importable-resource/providers.tf new file mode 100644 index 0000000000..4124dcaee0 --- /dev/null +++ b/internal/configs/testdata/valid-modules/importable-resource/providers.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + local = { + source = "hashicorp/local" + } + } +} diff --git a/internal/configs/testdata/valid-modules/override-action-and-trigger/main.tf b/internal/configs/testdata/valid-modules/override-action-and-trigger/main.tf new file mode 100644 index 0000000000..47c5f083ad --- /dev/null +++ b/internal/configs/testdata/valid-modules/override-action-and-trigger/main.tf @@ -0,0 +1,16 @@ +action "test_action" "test" { + foo = "bar" +} + +action "test_action" "unchanged" { + foo = "bar" +} + +resource "test_instance" "test" { + lifecycle { + action_trigger { + events = [after_create, after_update] + actions = [action.test_action.dosomething] + } + } +} \ No newline at end of file diff --git a/internal/configs/testdata/valid-modules/override-action-and-trigger/main_override.tf b/internal/configs/testdata/valid-modules/override-action-and-trigger/main_override.tf new file mode 100644 index 0000000000..913674bf35 --- /dev/null +++ b/internal/configs/testdata/valid-modules/override-action-and-trigger/main_override.tf @@ -0,0 +1,12 @@ +action "test_action" "test" { + foo = "baz" +} + +resource "test_instance" "test" { + lifecycle { + action_trigger { + events = [after_destroy] + actions = [action.test_action.dosomething] + } + } +} \ No newline at end of file diff --git a/internal/configs/testdata/valid-modules/override-module/primary.tf b/internal/configs/testdata/valid-modules/override-module/primary.tf index a676a00eed..01a870da88 100644 --- a/internal/configs/testdata/valid-modules/override-module/primary.tf +++ b/internal/configs/testdata/valid-modules/override-module/primary.tf @@ -8,4 +8,6 @@ module "example" { providers = { test = test.foo } + depends_on = [null_resource.test] } +resource "null_resource" "test" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources-inline/main.tf b/internal/configs/testdata/valid-modules/with-mock-sources-inline/main.tf new file mode 100644 index 0000000000..47246b4b66 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources-inline/main.tf @@ -0,0 +1,2 @@ + +resource "aws_s3_bucket" "my_bucket" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources-inline/main.tftest.hcl b/internal/configs/testdata/valid-modules/with-mock-sources-inline/main.tftest.hcl new file mode 100644 index 0000000000..103b046153 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources-inline/main.tftest.hcl @@ -0,0 +1,12 @@ + +mock_provider "aws" { + source = "./testing/aws" + + mock_resource "aws_s3_bucket" { + defaults = { + arn = "aws:s3:::bucket" + } + } +} + +run "test" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/.ignored.tfmock.hcl b/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/.ignored.tfmock.hcl new file mode 100644 index 0000000000..14da2c15f2 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/.ignored.tfmock.hcl @@ -0,0 +1,9 @@ +# If read, this file should cause issues. But, it should be ignored. + +mock_resource "aws_s3_bucket" {} + +mock_data "aws_s3_bucket" {} + +override_resource { + target = aws_s3_bucket.my_bucket +} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/data.tfmock.hcl b/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/data.tfmock.hcl new file mode 100644 index 0000000000..def19ed3f6 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/data.tfmock.hcl @@ -0,0 +1 @@ +mock_data "aws_s3_bucket" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/resource.tfmock.hcl b/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/resource.tfmock.hcl new file mode 100644 index 0000000000..503afc3c2d --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources-inline/testing/aws/resource.tfmock.hcl @@ -0,0 +1,5 @@ +mock_resource "aws_s3_bucket" {} + +override_resource { + target = aws_s3_bucket.my_bucket +} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources/main.tf b/internal/configs/testdata/valid-modules/with-mock-sources/main.tf new file mode 100644 index 0000000000..47246b4b66 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources/main.tf @@ -0,0 +1,2 @@ + +resource "aws_s3_bucket" "my_bucket" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources/main.tftest.hcl b/internal/configs/testdata/valid-modules/with-mock-sources/main.tftest.hcl new file mode 100644 index 0000000000..c592039b12 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources/main.tftest.hcl @@ -0,0 +1,6 @@ + +mock_provider "aws" { + source = "./testing/aws" +} + +run "test" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/.ignored.tfmock.hcl b/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/.ignored.tfmock.hcl new file mode 100644 index 0000000000..14da2c15f2 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/.ignored.tfmock.hcl @@ -0,0 +1,9 @@ +# If read, this file should cause issues. But, it should be ignored. + +mock_resource "aws_s3_bucket" {} + +mock_data "aws_s3_bucket" {} + +override_resource { + target = aws_s3_bucket.my_bucket +} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/data.tfmock.hcl b/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/data.tfmock.hcl new file mode 100644 index 0000000000..def19ed3f6 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/data.tfmock.hcl @@ -0,0 +1 @@ +mock_data "aws_s3_bucket" {} diff --git a/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/resource.tfmock.hcl b/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/resource.tfmock.hcl new file mode 100644 index 0000000000..503afc3c2d --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mock-sources/testing/aws/resource.tfmock.hcl @@ -0,0 +1,5 @@ +mock_resource "aws_s3_bucket" {} + +override_resource { + target = aws_s3_bucket.my_bucket +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/child/main.tf b/internal/configs/testdata/valid-modules/with-mocks/child/main.tf new file mode 100644 index 0000000000..1ceca4ccdc --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/child/main.tf @@ -0,0 +1,8 @@ + +output "string" { + value = "Hello, world!" +} + +output "number" { + value = 0 +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/main.tf b/internal/configs/testdata/valid-modules/with-mocks/main.tf new file mode 100644 index 0000000000..572c350aab --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} + +resource "aws_instance" "first" {} + +resource "aws_instance" "second" {} + +resource "aws_instance" "third" {} + +data "aws_secretsmanager_secret" "creds" {} + +module "child" { + source = "./child" +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/test_case_one.tftest.hcl b/internal/configs/testdata/valid-modules/with-mocks/test_case_one.tftest.hcl new file mode 100644 index 0000000000..5df5f5950b --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/test_case_one.tftest.hcl @@ -0,0 +1,48 @@ + +mock_provider "aws" { + + mock_resource "aws_instance" { + defaults = { + arn = "aws:instance" + } + } + + mock_data "aws_secretsmanager_secret" {} + + override_resource { + target = aws_instance.second + values = {} + } + + override_data { + target = data.aws_secretsmanager_secret.creds + values = { + arn = "aws:secretsmanager" + } + } +} + +override_module { + target = module.child + outputs = { + string = "testfile" + number = -1 + } +} + +run "test" { + override_resource { + target = aws_instance.first + values = { + arn = "aws:instance:first" + } + } + + override_module { + target = module.child + outputs = { + string = "testrun" + number = -1 + } + } +} diff --git a/internal/configs/testdata/valid-modules/with-mocks/test_case_two.tftest.hcl b/internal/configs/testdata/valid-modules/with-mocks/test_case_two.tftest.hcl new file mode 100644 index 0000000000..9133e6aeb3 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-mocks/test_case_two.tftest.hcl @@ -0,0 +1,25 @@ + +provider "aws" {} + +override_data { + target = data.aws_secretsmanager_secret.creds + values = { + arn = "aws:secretsmanager" + } +} + +run "test" { + override_resource { + target = aws_instance.first + values = { + arn = "aws:instance:first" + } + } + + override_data { + target = data.aws_secretsmanager_secret.creds + values = { + arn = "aws:secretsmanager" + } + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-expect-failures/main.tf b/internal/configs/testdata/valid-modules/with-tests-expect-failures/main.tf new file mode 100644 index 0000000000..b54c29ba6f --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-expect-failures/main.tf @@ -0,0 +1,13 @@ + +variable "input" { + type = string +} + + +resource "foo_resource" "a" { + value = var.input +} + +output "output" { + value = foo_resource.a.value +} diff --git a/internal/configs/testdata/valid-modules/with-tests-expect-failures/test_case_one.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-expect-failures/test_case_one.tftest.hcl new file mode 100644 index 0000000000..23bcde5d57 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-expect-failures/test_case_one.tftest.hcl @@ -0,0 +1,10 @@ +variables { + input = "default" +} + +run "test_run_one" { + expect_failures = [ + input.input, + output.output, + ] +} diff --git a/internal/configs/testdata/valid-modules/with-tests-expect-failures/test_case_two.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-expect-failures/test_case_two.tftest.hcl new file mode 100644 index 0000000000..31b1322e89 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-expect-failures/test_case_two.tftest.hcl @@ -0,0 +1,9 @@ +variables { + input = "default" +} + +run "test_run_one" { + expect_failures = [ + foo_resource.a, + ] +} diff --git a/internal/configs/testdata/valid-modules/with-tests-json/main.tf.json b/internal/configs/testdata/valid-modules/with-tests-json/main.tf.json new file mode 100644 index 0000000000..03624c1463 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-json/main.tf.json @@ -0,0 +1,17 @@ +{ + "variable": { + "input": { + "type": "string" + } + }, + "resource": { + "foo_resource": { + "a": { + "value": "${var.input}" + } + }, + "bar_resource": { + "c": {} + } + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-json/test_case_two.tftest.json b/internal/configs/testdata/valid-modules/with-tests-json/test_case_two.tftest.json new file mode 100644 index 0000000000..43e677db64 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-json/test_case_two.tftest.json @@ -0,0 +1,45 @@ +{ + "run": { + "test_run_one": { + "variables": { + "input": "test_run_one" + }, + "assert": [ + { + "condition": "${foo_resource.a.value} == test_run_one", + "error_message": "invalid value" + } + ] + }, + "test_run_two": { + "plan_options": { + "mode": "refresh-only" + }, + "variables": { + "input": "test_run_two" + }, + "assert": [ + { + "condition": "${foo_resource.a.value} == test_run_one", + "error_message": "invalid value" + } + ] + }, + "test_run_three": { + "variables": { + "input": "test_run_three" + }, + "plan_options": { + "replace": [ + "bar_resource.c" + ] + }, + "assert": [ + { + "condition": "${foo_resource.a.value} == test_run_three", + "error_message": "invalid value" + } + ] + } + } +} \ No newline at end of file diff --git a/internal/configs/testdata/valid-modules/with-tests-json/tests/test_case_one.tftest.json b/internal/configs/testdata/valid-modules/with-tests-json/tests/test_case_one.tftest.json new file mode 100644 index 0000000000..934173aca3 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-json/tests/test_case_one.tftest.json @@ -0,0 +1,32 @@ +{ + "variables": { + "input": "default" + }, + "run": { + "test_run_one": { + "command": "plan", + "plan_options": { + "target": [ + "foo_resource.a" + ] + }, + "assert": [ + { + "condition": "${foo_resource.a.value} == default", + "error_message": "invalid value" + } + ] + }, + "test_run_two": { + "variables": { + "input": "custom" + }, + "assert": [ + { + "condition": "${foo_resource.a.value} == custom", + "error_message": "invalid value" + } + ] + } + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-module/main.tf b/internal/configs/testdata/valid-modules/with-tests-module/main.tf new file mode 100644 index 0000000000..3b60dc01cc --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-module/main.tf @@ -0,0 +1,12 @@ + +variable "managed_id" { + type = string +} + +data "test_data_source" "managed_data" { + id = var.managed_id +} + +resource "test_resource" "created" { + value = data.test_data_source.managed_data.value +} diff --git a/internal/configs/testdata/valid-modules/with-tests-module/main.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-module/main.tftest.hcl new file mode 100644 index 0000000000..1f2a6a94aa --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-module/main.tftest.hcl @@ -0,0 +1,21 @@ +variables { + managed_id = "B853C121" +} + +run "setup" { + module { + source = "./setup" + } + + variables { + value = "Hello, world!" + id = "B853C121" + } +} + +run "test" { + assert { + condition = test_resource.created.value == "Hello, world!" + error_message = "bad value" + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-module/setup/main.tf b/internal/configs/testdata/valid-modules/with-tests-module/setup/main.tf new file mode 100644 index 0000000000..49056bbea7 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-module/setup/main.tf @@ -0,0 +1,13 @@ +variable "value" { + type = string +} + +variable "id" { + type = string +} + +resource "test_resource" "managed" { + provider = setup + id = var.id + value = var.value +} diff --git a/internal/configs/testdata/valid-modules/with-tests-nested-module/main.tf b/internal/configs/testdata/valid-modules/with-tests-nested-module/main.tf new file mode 100644 index 0000000000..f304def8b1 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested-module/main.tf @@ -0,0 +1,2 @@ + +resource "test_resource" "resource" {} diff --git a/internal/configs/testdata/valid-modules/with-tests-nested-module/main.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-nested-module/main.tftest.hcl new file mode 100644 index 0000000000..3172b8c6dc --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested-module/main.tftest.hcl @@ -0,0 +1,14 @@ +variables { + value = "Hello, world!" +} + +run "load_module" { + module { + source = "./setup" + } + + assert { + condition = output.value == "Hello, world!" + error_message = "invalid value" + } +} \ No newline at end of file diff --git a/internal/configs/testdata/valid-modules/with-tests-nested-module/setup/main.tf b/internal/configs/testdata/valid-modules/with-tests-nested-module/setup/main.tf new file mode 100644 index 0000000000..54e468b7db --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested-module/setup/main.tf @@ -0,0 +1,14 @@ + +variable "value" { + type = string +} + +module "child" { + source = "./other" + + value = var.value +} + +output "value" { + value = module.child.value +} diff --git a/internal/configs/testdata/valid-modules/with-tests-nested-module/setup/other/main.tf b/internal/configs/testdata/valid-modules/with-tests-nested-module/setup/other/main.tf new file mode 100644 index 0000000000..e1f6e52f3f --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested-module/setup/other/main.tf @@ -0,0 +1,12 @@ + +variable "value" { + type = string +} + +resource "test_resource" "resource" { + value = var.value +} + +output "value" { + value = test_resource.resource.value +} diff --git a/internal/configs/testdata/valid-modules/with-tests-nested/main.tf b/internal/configs/testdata/valid-modules/with-tests-nested/main.tf new file mode 100644 index 0000000000..b84d4f3c41 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested/main.tf @@ -0,0 +1,11 @@ + +variable "input" { + type = string +} + + +resource "foo_resource" "a" { + value = var.input +} + +resource "bar_resource" "c" {} diff --git a/internal/configs/testdata/valid-modules/with-tests-nested/tests/test_case_one.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-nested/tests/test_case_one.tftest.hcl new file mode 100644 index 0000000000..01ef5dff05 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested/tests/test_case_one.tftest.hcl @@ -0,0 +1,31 @@ +variables { + input = "default" +} + +# test_run_one runs a partial plan +run "test_run_one" { + command = plan + + plan_options { + target = [ + foo_resource.a + ] + } + + assert { + condition = foo_resource.a.value == "default" + error_message = "invalid value" + } +} + +# test_run_two does a complete apply operation +run "test_run_two" { + variables { + input = "custom" + } + + assert { + condition = foo_resource.a.value == "custom" + error_message = "invalid value" + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-nested/tests/test_case_two.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-nested/tests/test_case_two.tftest.hcl new file mode 100644 index 0000000000..b2be9172b6 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-nested/tests/test_case_two.tftest.hcl @@ -0,0 +1,46 @@ +# test_run_one does a complete apply +run "test_run_one" { + variables { + input = "test_run_one" + } + + assert { + condition = foo_resource.a.value == "test_run_one" + error_message = "invalid value" + } +} + +# test_run_two does a refresh only apply +run "test_run_two" { + plan_options { + mode = refresh-only + } + + variables { + input = "test_run_two" + } + + assert { + # value shouldn't change, as we're doing a refresh-only apply. + condition = foo_resource.a.value == "test_run_one" + error_message = "invalid value" + } +} + +# test_run_three does an apply with a replace operation +run "test_run_three" { + variables { + input = "test_run_three" + } + + plan_options { + replace = [ + bar_resource.c + ] + } + + assert { + condition = foo_resource.a.value == "test_run_three" + error_message = "invalid value" + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-very-nested/main.tf b/internal/configs/testdata/valid-modules/with-tests-very-nested/main.tf new file mode 100644 index 0000000000..b84d4f3c41 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-very-nested/main.tf @@ -0,0 +1,11 @@ + +variable "input" { + type = string +} + + +resource "foo_resource" "a" { + value = var.input +} + +resource "bar_resource" "c" {} diff --git a/internal/configs/testdata/valid-modules/with-tests-very-nested/very/nested/test_case_one.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-very-nested/very/nested/test_case_one.tftest.hcl new file mode 100644 index 0000000000..01ef5dff05 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-very-nested/very/nested/test_case_one.tftest.hcl @@ -0,0 +1,31 @@ +variables { + input = "default" +} + +# test_run_one runs a partial plan +run "test_run_one" { + command = plan + + plan_options { + target = [ + foo_resource.a + ] + } + + assert { + condition = foo_resource.a.value == "default" + error_message = "invalid value" + } +} + +# test_run_two does a complete apply operation +run "test_run_two" { + variables { + input = "custom" + } + + assert { + condition = foo_resource.a.value == "custom" + error_message = "invalid value" + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests-very-nested/very/nested/test_case_two.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests-very-nested/very/nested/test_case_two.tftest.hcl new file mode 100644 index 0000000000..b2be9172b6 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests-very-nested/very/nested/test_case_two.tftest.hcl @@ -0,0 +1,46 @@ +# test_run_one does a complete apply +run "test_run_one" { + variables { + input = "test_run_one" + } + + assert { + condition = foo_resource.a.value == "test_run_one" + error_message = "invalid value" + } +} + +# test_run_two does a refresh only apply +run "test_run_two" { + plan_options { + mode = refresh-only + } + + variables { + input = "test_run_two" + } + + assert { + # value shouldn't change, as we're doing a refresh-only apply. + condition = foo_resource.a.value == "test_run_one" + error_message = "invalid value" + } +} + +# test_run_three does an apply with a replace operation +run "test_run_three" { + variables { + input = "test_run_three" + } + + plan_options { + replace = [ + bar_resource.c + ] + } + + assert { + condition = foo_resource.a.value == "test_run_three" + error_message = "invalid value" + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests/main.tf b/internal/configs/testdata/valid-modules/with-tests/main.tf new file mode 100644 index 0000000000..b84d4f3c41 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests/main.tf @@ -0,0 +1,11 @@ + +variable "input" { + type = string +} + + +resource "foo_resource" "a" { + value = var.input +} + +resource "bar_resource" "c" {} diff --git a/internal/configs/testdata/valid-modules/with-tests/test_case_one.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests/test_case_one.tftest.hcl new file mode 100644 index 0000000000..01ef5dff05 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests/test_case_one.tftest.hcl @@ -0,0 +1,31 @@ +variables { + input = "default" +} + +# test_run_one runs a partial plan +run "test_run_one" { + command = plan + + plan_options { + target = [ + foo_resource.a + ] + } + + assert { + condition = foo_resource.a.value == "default" + error_message = "invalid value" + } +} + +# test_run_two does a complete apply operation +run "test_run_two" { + variables { + input = "custom" + } + + assert { + condition = foo_resource.a.value == "custom" + error_message = "invalid value" + } +} diff --git a/internal/configs/testdata/valid-modules/with-tests/test_case_two.tftest.hcl b/internal/configs/testdata/valid-modules/with-tests/test_case_two.tftest.hcl new file mode 100644 index 0000000000..3799198646 --- /dev/null +++ b/internal/configs/testdata/valid-modules/with-tests/test_case_two.tftest.hcl @@ -0,0 +1,48 @@ +test {} + +# test_run_one does a complete apply +run "test_run_one" { + variables { + input = "test_run_one" + } + + assert { + condition = foo_resource.a.value == "test_run_one" + error_message = "invalid value" + } +} + +# test_run_two does a refresh only apply +run "test_run_two" { + plan_options { + mode = refresh-only + } + + variables { + input = "test_run_two" + } + + assert { + # value shouldn't change, as we're doing a refresh-only apply. + condition = foo_resource.a.value == "test_run_one" + error_message = "invalid value" + } +} + +# test_run_three does an apply with a replace operation +run "test_run_three" { + variables { + input = "test_run_three" + } + + plan_options { + replace = [ + bar_resource.c + ] + } + + assert { + condition = foo_resource.a.value == "test_run_three" + error_message = "invalid value" + } +} diff --git a/internal/configs/util.go b/internal/configs/util.go index e135546fb7..5315ac1a0f 100644 --- a/internal/configs/util.go +++ b/internal/configs/util.go @@ -1,8 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configtesting" ) // exprIsNativeQuotedString determines whether the given expression looks like @@ -61,3 +67,13 @@ func schemaWithDynamic(schema *hcl.BodySchema) *hcl.BodySchema { return ret } + +// SynthBody is a forwarding alias for the old location of +// [configtesting.SynthBody]. +// +// Using the new name is preferable because it might avoid you needing to +// import this entire configs package, if you didn't have some other +// use for it anyway. +func SynthBody(filename string, values map[string]cty.Value) hcl.Body { + return configtesting.SynthBody(filename, values) +} diff --git a/internal/configs/variable_type_hint.go b/internal/configs/variable_type_hint.go index 9a0597bac5..7ec6ea3cfa 100644 --- a/internal/configs/variable_type_hint.go +++ b/internal/configs/variable_type_hint.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs // VariableTypeHint is an enumeration used for the Variable.TypeHint field, @@ -19,13 +22,13 @@ package configs // - TypeHintMap requires a type that could be converted to an object type VariableTypeHint rune -//go:generate go run golang.org/x/tools/cmd/stringer -type VariableTypeHint +//go:generate go tool golang.org/x/tools/cmd/stringer -type VariableTypeHint // TypeHintNone indicates the absence of a type hint. Values specified in // ambiguous contexts will be treated as literal strings, as if TypeHintString // were selected, but no runtime value checks will be applied. This is reasonable // type hint for a module that is never intended to be used at the top-level -// of a configuration, since descendent modules never receive values from +// of a configuration, since descendant modules never receive values from // ambiguous contexts. const TypeHintNone VariableTypeHint = 0 diff --git a/internal/configs/version_constraint.go b/internal/configs/version_constraint.go index 0f541dc711..d4b93481b2 100644 --- a/internal/configs/version_constraint.go +++ b/internal/configs/version_constraint.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package configs import ( diff --git a/internal/copy/copy_dir.go b/internal/copy/copy_dir.go index f76470fdbc..4170984b56 100644 --- a/internal/copy/copy_dir.go +++ b/internal/copy/copy_dir.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package copy import ( diff --git a/internal/copy/copy_dir_test.go b/internal/copy/copy_dir_test.go index 244bcbb4e0..c389307244 100644 --- a/internal/copy/copy_dir_test.go +++ b/internal/copy/copy_dir_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package copy import ( diff --git a/internal/copy/copy_file.go b/internal/copy/copy_file.go index d4833bc0e8..bbbd490981 100644 --- a/internal/copy/copy_file.go +++ b/internal/copy/copy_file.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package copy import ( @@ -5,8 +8,6 @@ import ( "os" ) -// From: https://gist.github.com/m4ng0squ4sh/92462b38df26839a3ca324697c8cba04 - // CopyFile copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all it's contents will be replaced by the contents diff --git a/internal/copy/copy_value.go b/internal/copy/copy_value.go new file mode 100644 index 0000000000..767c3871f5 --- /dev/null +++ b/internal/copy/copy_value.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package copy + +import ( + "reflect" +) + +// DeepCopyValue produces a deep copy of the given value, where the result +// ideally shares no mutable memory with the given value. +// +// There are some limitations on what's possible, however: +// - This package can't write into an unexported field of a struct, so those +// will be ignored entirely and thus left as their zero values in the +// result. +// - If the given structure contains function pointers then their closures +// might refer to shared memory that this function cannot copy. If they +// refer to memory that's also included in the data structure outside of +// the function pointer then the two will be disconnected in the result. +// - It isn't really meaningful to "copy" a channel since it's a +// synchronization primitive rather than a data structure, so the result +// will share the same channels as the input. +// - Copying other library-based synchronization primitives like [sync.Mutex] +// doesn't really make sense either, although this function doesn't +// understand what they are and so the result is undefined. If your +// synchronization primitive has a "ready to use" zero value then it _might_ +// be acceptable to store it in an unexported field and thus have it be +// zeroed in the result, but at that point you're probably better off +// writing a specialized deepcopy function so that you can actually use +// the synchronization primitive to prevent data races during copying. +// - The uintptr and [unsafe.Pointer] types might well refer to some shared +// memory, but don't give any information about how to copy that memory +// and so those are just preserved verbatim, making the result point to +// the same memory as the input. +// - Broadly, this function needs special handling for each different kind +// of value in Go, and so if a later version of Go has introduced a new kind +// of value then this function might not support it yet. That might cause +// this function to panic, or to ignore part of the structure, or otherwise +// misbehave. +// +// This is intended as a relatively simple utility for straightforward cases, +// primarily for use in contrived situations like unit tests. +// +// It intentionally does not offer any customization; if you need to do +// something special then it's better to just write your own simple direct code +// than to pull in all of this reflection trickery. Even if you don't need to do +// something special it's probably still better to just write some +// straightforward code that directly describes the behavior you're intending, +// so that the Go compiler can help you and so you don't force future +// maintainers to understand all of this metaprogramming if something goes +// wrong. Seriously... don't use this function. +func DeepCopyValue[T any](v T) T { + // We use type parameters in the signature to make usage more convenient + // for the caller (no type assertions required) but we actually do all + // our internal work in the realm of package reflect. + input := reflect.ValueOf(&v).Elem() // if T is an interface type then input is the interface value, not the value inside it + ty := reflect.TypeFor[T]() // likewise, if T is interface type then this is the static interface type, not the dynamic type + result := deepCopyValue(input, ty) + return result.Interface().(T) +} + +func deepCopyValue(v reflect.Value, ty reflect.Type) reflect.Value { + switch ty.Kind() { + // A large subset of the kinds don't point to mutable sharable memory, + // or don't refer to something we can possibly copy, and so we can just + // return them directly without any extra work. + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String, reflect.UnsafePointer, reflect.Func, reflect.Chan: + return v + + case reflect.Array: + return deepCopyArray(v, ty) + case reflect.Interface: + return deepCopyInterface(v, ty) + case reflect.Map: + return deepCopyMap(v, ty) + case reflect.Pointer: + return deepCopyPointer(v, ty) + case reflect.Slice: + return deepCopySlice(v, ty) + case reflect.Struct: + return deepCopyStruct(v, ty) + + default: + panic("unsupported type kind " + ty.Kind().String()) + } +} + +func deepCopyArray(v reflect.Value, ty reflect.Type) reflect.Value { + // Copying an array really means allocating a new array and then + // copying each of the elements from the source. + ret := reflect.New(ty).Elem() + for i := range ret.Len() { + newElemV := deepCopyValue(v.Index(i), ty.Elem()) + ret.Index(i).Set(newElemV) + } + return ret +} + +func deepCopyInterface(v reflect.Value, ty reflect.Type) reflect.Value { + if v.IsNil() { + return v + } + // An interface value is not directly mutable itself, but the value + // inside it might be and so we'll copy that and then wrap the result + // in a new interface value of the same type. + ret := reflect.New(ty).Elem() + dynV := deepCopyValue(v.Elem(), v.Elem().Type()) + ret.Set(dynV) + return ret +} + +func deepCopyMap(v reflect.Value, ty reflect.Type) reflect.Value { + if v.IsNil() { + return v + } + ret := reflect.MakeMap(ty) + for iter := v.MapRange(); iter.Next(); { + // We don't copy the key because Go does not allow any mutably-aliasable + // types as map keys. (That would make it very easy to corrupt the + // internals of the map, after all!) + k := iter.Key() + v := deepCopyValue(iter.Value(), ty.Elem()) + ret.SetMapIndex(k, v) + } + return ret +} + +func deepCopyPointer(v reflect.Value, ty reflect.Type) reflect.Value { + if v.IsNil() { + return v + } + // We copy a pointer by copying what it refers to and then returning + // a pointer to that copy. + newTarget := deepCopyValue(v.Elem(), ty.Elem()) + return newTarget.Addr() +} + +func deepCopySlice(v reflect.Value, ty reflect.Type) reflect.Value { + if v.IsNil() { + return v + } + // Copying a slice really means copying the part of its backing array + // that in could potentially observe. In particular, it's possible to + // expand the view of the backing array up to the slice's capacity, + // so we need to copy the entire capacity even if the length is + // currently shorter to ensure that the result is truly equivalent. + length := v.Len() + capacity := v.Cap() + + // This exposes any elements that are between length and capacity. + fullView := v.Slice3(0, capacity, capacity) + // Making a slice also allocates a new backing array for it. + ret := reflect.MakeSlice(ty, capacity, capacity) + for i := range capacity { + ret.Index(i).Set(fullView.Index(i)) + } + + // We must restore the original length before we return. + return ret.Slice(0, length) +} + +func deepCopyStruct(v reflect.Value, ty reflect.Type) reflect.Value { + // To copy a struct we must copy each exported field one by one. + // We can't assign to unexported fields and so we just leave those + // unset in the new value. + ret := reflect.New(ty).Elem() + for i := range ty.NumField() { + fieldRet := ret.Field(i) + if !fieldRet.CanSet() { + // Presumably it's an unexported field, so we can't do anything + // with it and must leave it zeroed. + continue + } + newVal := deepCopyValue(v.Field(i), ty.Field(i).Type) + fieldRet.Set(newVal) + } + return ret +} diff --git a/internal/copy/copy_value_test.go b/internal/copy/copy_value_test.go new file mode 100644 index 0000000000..e83af305c9 --- /dev/null +++ b/internal/copy/copy_value_test.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package copy + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" +) + +func TestCopyValue(t *testing.T) { + t.Run("pointer to something that needs copying", func(t *testing.T) { + // To test this we need to point to something that actually gets + // deep copied, because the pointer _itself_ is just a number, + // not mutably-aliased memory. (If the pointee is not something + // that can be mutably aliased then the result would just match the + // input, because no copying is needed.) + type V struct { + S string + } + input := &V{"hello"} + result := testDeepCopyValueLogged(t, input) + if input == result { + t.Errorf("result pointer matches input pointer") + } + if input.S != "hello" { + t.Errorf("input was modified before we modified it") + } + result.S = "goodbye" + if input.S != "hello" { + t.Errorf("modifying result also modified input") + } + }) + t.Run("pointer to something that doesn't need copying", func(t *testing.T) { + // Strings are immutable and so we don't deep-copy them. Therefore + // a pointer to a string doesn't get modified during copy either. + s := "hello" + input := &s + result := testDeepCopyValueLogged(t, input) + if input != result { + t.Errorf("result pointer does not match input pointer") + } + }) + t.Run("pointer that is nil", func(t *testing.T) { + var input *int + result := testDeepCopyValueLogged(t, input) + if result != nil { + t.Errorf("result is not nil") + } + }) + t.Run("slice", func(t *testing.T) { + arr := [...]rune{'a', 'b', 'c', 'd'} + input := arr[0:2:4] // ab is in length, cd is hidden in extra capacity + result := testDeepCopyValueLogged(t, input) + if &input[0] == &result[0] { + t.Errorf("result shares backing array with input") + } + if got := len(result); got != 2 { + t.Fatalf("result has incorrect length %d", got) + } + if got := cap(result); got != 4 { + t.Fatalf("result has incorrect capacity %d", got) + } + // We'll expand the slices so we can view the excess capacity too + fullInput := input[0:4] + fullResult := result[0:4] + want := []rune{'a', 'b', 'c', 'd'} + if diff := cmp.Diff(want, fullInput); diff != "" { + t.Errorf("input was modified\n%s", diff) + } + if diff := cmp.Diff(want, fullResult); diff != "" { + t.Errorf("incorrect result\n%s", diff) + } + }) + t.Run("slice that is nil", func(t *testing.T) { + var input []int + result := testDeepCopyValueLogged(t, input) + if result != nil { + t.Errorf("result is not nil") + } + }) + t.Run("array", func(t *testing.T) { + // Arrays are passed by value anyway, so deep copying one really + // means deep copying anything they refer to that might contain + // mutably-aliased data. We'll use slices as the victims here; + // their backing arrays should be copied and thus the result + // should have different slices but with the same content. + input := [...][]rune{ + {'a', 'b'}, + {'c', 'd'}, + } + result := testDeepCopyValueLogged(t, input) + if &result[0][0] == &input[0][0] { + t.Errorf("first element of result shares backing array with input") + } + if &result[1][0] == &input[1][0] { + t.Errorf("second element of result shares backing array with input") + } + want := [...][]rune{ + {'a', 'b'}, + {'c', 'd'}, + } + if diff := cmp.Diff(want, result); diff != "" { + t.Errorf("incorrect result\n%s", diff) + } + }) + t.Run("map", func(t *testing.T) { + // Maps are a bit tricky to test because they are an address-based + // data structure but the addresses of the internals are intentionally + // not exposed. Therefore we'll test this indirectly by making a + // map, copying it, and then modifying the copy. That should leave + // the original unchanged, if the copy was performed correctly. + input := map[string]string{"greeting": "hello"} + result := testDeepCopyValueLogged(t, input) + if len(input) != 1 { + t.Errorf("input length changed before we did any modifying") + } + if input["greeting"] != "hello" { + t.Errorf("input element changed before we did any modifying") + } + if len(result) != 1 { + t.Errorf("result length changed before we did any modifying") + } + if result["greeting"] != "hello" { + t.Errorf("result element changed before we did any modifying") + } + result["greeting"] = "hallo" + if input["greeting"] != "hello" { + t.Errorf("input element changed when we modified result") + } + }) + t.Run("map that is nil", func(t *testing.T) { + var input map[string]string + result := testDeepCopyValueLogged(t, input) + if result != nil { + t.Errorf("result is not nil") + } + }) + t.Run("struct", func(t *testing.T) { + type S struct { + Exported string + unexported string + } + input := S{ + Exported: "beep", + unexported: "boop", + } + result := testDeepCopyValueLogged(t, input) + if result.Exported != "beep" { + t.Errorf("Exported field has wrong result") + } + if result.unexported != "" { + t.Errorf("unexported field got populated (should have been left as zero value)") + } + }) + t.Run("interface", func(t *testing.T) { + // We'll create an interface that contains a pointer to something + // mutable, and then mutate it after copy to make sure that the + // two values can change independently. + type B struct { + S string + } + type A struct { + B *B + } + inputInner := &A{ + &B{"hello"}, + } + input := any(inputInner) // an interface value wrapping inputInner + result := testDeepCopyValueLogged(t, input) + if resultInner, ok := result.(*A); !ok { + t.Fatalf("result contains %T, not %T", result, resultInner) + } + if result.(*A) == input.(*A) { + t.Error("result has same address as input") + } + if result.(*A).B == input.(*A).B { + t.Error("result.b has same address as input") + } + if input.(*A).B.S != "hello" { + t.Errorf("input was modified before we modified it") + } + result.(*A).B.S = "goodbye" + if input.(*A).B.S != "hello" { + t.Errorf("modifying result also modified input") + } + }) +} + +func testDeepCopyValueLogged[T any](t *testing.T, input T) T { + t.Helper() + t.Logf("input: %s", spew.Sdump(input)) + result := DeepCopyValue(input) + t.Logf("result: %s", spew.Sdump(result)) + return result +} diff --git a/internal/dag/dag.go b/internal/dag/dag.go index f5268e76f0..dfb9ef9570 100644 --- a/internal/dag/dag.go +++ b/internal/dag/dag.go @@ -1,13 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( + "errors" "fmt" "sort" "strings" "github.com/hashicorp/terraform/internal/tfdiags" - - "github.com/hashicorp/go-multierror" ) // AcyclicGraph is a specialization of Graph that cannot have cycles. @@ -28,34 +30,145 @@ func (g *AcyclicGraph) DirectedGraph() Grapher { // Returns a Set that includes every Vertex yielded by walking down from the // provided starting Vertex v. -func (g *AcyclicGraph) Ancestors(v Vertex) (Set, error) { +func (g *AcyclicGraph) Ancestors(vs ...Vertex) Set { s := make(Set) memoFunc := func(v Vertex, d int) error { s.Add(v) return nil } - if err := g.DepthFirstWalk(g.downEdgesNoCopy(v), memoFunc); err != nil { - return nil, err + start := make(Set) + for _, v := range vs { + for _, dep := range g.downEdgesNoCopy(v) { + start.Add(dep) + } } - return s, nil + if err := g.DepthFirstWalk(start, memoFunc); err != nil { + return nil + } + + return s } -// Returns a Set that includes every Vertex yielded by walking up from the -// provided starting Vertex v. -func (g *AcyclicGraph) Descendents(v Vertex) (Set, error) { +// FirstAncestorsWith returns a Set that includes every Vertex yielded by +// walking down from the provided starting Vertex v, and stopping each branch when +// match returns true. This will return the set of all first ancestors +// encountered which match some criteria. +func (g *AcyclicGraph) FirstAncestorsWith(v Vertex, match func(Vertex) bool) Set { + s := make(Set) + searchFunc := func(v Vertex, d int) error { + if match(v) { + s.Add(v) + return errStopWalkBranch + } + + return nil + } + + start := make(Set) + for _, dep := range g.downEdgesNoCopy(v) { + start.Add(dep) + } + + // our memoFunc doesn't return an error + g.DepthFirstWalk(start, searchFunc) + + return s +} + +// MatchAncestor returns true if the given match function returns true for any +// descendants of the given Vertex. +func (g *AcyclicGraph) MatchAncestor(v Vertex, match func(Vertex) bool) bool { + var ret bool + matchFunc := func(v Vertex, d int) error { + if match(v) { + ret = true + return errStopWalk + } + + return nil + } + + start := make(Set) + for _, dep := range g.downEdgesNoCopy(v) { + start.Add(dep) + } + + // our memoFunc doesn't return an error + g.DepthFirstWalk(start, matchFunc) + + return ret +} + +// Descendants returns a Set that includes every Vertex yielded by walking up +// from the provided starting Vertex v. +func (g *AcyclicGraph) Descendants(v Vertex) Set { s := make(Set) memoFunc := func(v Vertex, d int) error { s.Add(v) return nil } - if err := g.ReverseDepthFirstWalk(g.upEdgesNoCopy(v), memoFunc); err != nil { - return nil, err + start := make(Set) + for _, dep := range g.upEdgesNoCopy(v) { + start.Add(dep) } - return s, nil + // our memoFunc doesn't return an error + g.ReverseDepthFirstWalk(start, memoFunc) + + return s +} + +// FirstDescendantsWith returns a Set that includes every Vertex yielded by +// walking up from the provided starting Vertex v, and stopping each branch when +// match returns true. This will return the set of all first descendants +// encountered which match some criteria. +func (g *AcyclicGraph) FirstDescendantsWith(v Vertex, match func(Vertex) bool) Set { + s := make(Set) + searchFunc := func(v Vertex, d int) error { + if match(v) { + s.Add(v) + return errStopWalkBranch + } + + return nil + } + + start := make(Set) + for _, dep := range g.upEdgesNoCopy(v) { + start.Add(dep) + } + + // our memoFunc doesn't return an error + g.ReverseDepthFirstWalk(start, searchFunc) + + return s +} + +// MatchDescendant returns true if the given match function returns true for any +// descendants of the given Vertex. +func (g *AcyclicGraph) MatchDescendant(v Vertex, match func(Vertex) bool) bool { + var ret bool + matchFunc := func(v Vertex, d int) error { + if match(v) { + ret = true + return errStopWalk + } + + return nil + } + + start := make(Set) + for _, dep := range g.upEdgesNoCopy(v) { + start.Add(dep) + } + + // our memoFunc doesn't return an error + g.ReverseDepthFirstWalk(start, matchFunc) + + return ret } // Root returns the root of the DAG, or an error. @@ -128,7 +241,7 @@ func (g *AcyclicGraph) Validate() error { cycleStr[j] = VertexName(vertex) } - err = multierror.Append(err, fmt.Errorf( + err = errors.Join(err, fmt.Errorf( "Cycle: %s", strings.Join(cycleStr, ", "))) } } @@ -136,7 +249,7 @@ func (g *AcyclicGraph) Validate() error { // Look for cycles to self for _, e := range g.Edges() { if e.Source() == e.Target() { - err = multierror.Append(err, fmt.Errorf( + err = errors.Join(err, fmt.Errorf( "Self reference: %s", VertexName(e.Source()))) } } @@ -179,16 +292,18 @@ type vertexAtDepth struct { Depth int } -// TopologicalOrder returns a topological sort of the given graph. The nodes -// are not sorted, and any valid order may be returned. This function will -// panic if it encounters a cycle. +// TopologicalOrder returns a topological sort of the given graph, with source +// vertices ordered before the targets of their edges. The nodes are not sorted, +// and any valid order may be returned. This function will panic if it +// encounters a cycle. func (g *AcyclicGraph) TopologicalOrder() []Vertex { return g.topoOrder(upOrder) } -// ReverseTopologicalOrder returns a topological sort of the given graph, -// following each edge in reverse. The nodes are not sorted, and any valid -// order may be returned. This function will panic if it encounters a cycle. +// ReverseTopologicalOrder returns a topological sort of the given graph, with +// target vertices ordered before the sources of their edges. The nodes are not +// sorted, and any valid order may be returned. This function will panic if it +// encounters a cycle. func (g *AcyclicGraph) ReverseTopologicalOrder() []Vertex { return g.topoOrder(downOrder) } @@ -251,6 +366,16 @@ const ( upOrder ) +var ( + // stopWalkBranch halts the descent in the current branch of the walk + // without adding any more edges from the current vertex, and continues with + // the next vertex already added to the queue. + errStopWalkBranch = errors.New("stop walk branch") + + // stopWalk halts the entire walk. + errStopWalk = errors.New("stop walk") +) + // DepthFirstWalk does a depth-first walk of the graph starting from // the vertices in start. func (g *AcyclicGraph) DepthFirstWalk(start Set, f DepthWalkFunc) error { @@ -317,6 +442,12 @@ func (g *AcyclicGraph) walk(order walkType, test bool, start Set, f DepthWalkFun // Visit the current node if err := f(current.Vertex, current.Depth); err != nil { + switch err { + case errStopWalk: + return nil + case errStopWalkBranch: + continue + } return err } diff --git a/internal/dag/dag_test.go b/internal/dag/dag_test.go index 5b273938e8..43c8c5d2c4 100644 --- a/internal/dag/dag_test.go +++ b/internal/dag/dag_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( @@ -238,10 +241,7 @@ func TestAcyclicGraphAncestors(t *testing.T) { g.Connect(BasicEdge(3, 4)) g.Connect(BasicEdge(4, 5)) - actual, err := g.Ancestors(2) - if err != nil { - t.Fatalf("err: %#v", err) - } + actual := g.Ancestors(2) expected := []Vertex{3, 4, 5} @@ -256,7 +256,7 @@ func TestAcyclicGraphAncestors(t *testing.T) { } } -func TestAcyclicGraphDescendents(t *testing.T) { +func TestAcyclicGraphDescendants(t *testing.T) { var g AcyclicGraph g.Add(1) g.Add(2) @@ -269,10 +269,7 @@ func TestAcyclicGraphDescendents(t *testing.T) { g.Connect(BasicEdge(3, 4)) g.Connect(BasicEdge(4, 5)) - actual, err := g.Descendents(2) - if err != nil { - t.Fatalf("err: %#v", err) - } + actual := g.Descendants(2) expected := []Vertex{0, 1} @@ -287,6 +284,108 @@ func TestAcyclicGraphDescendents(t *testing.T) { } } +func TestAcyclicGraphFindDescendants(t *testing.T) { + var g AcyclicGraph + g.Add(0) + g.Add(1) + g.Add(2) + g.Add(3) + g.Add(4) + g.Add(5) + g.Add(6) + g.Connect(BasicEdge(0, 1)) + g.Connect(BasicEdge(1, 2)) + g.Connect(BasicEdge(2, 6)) + g.Connect(BasicEdge(3, 4)) + g.Connect(BasicEdge(4, 5)) + g.Connect(BasicEdge(5, 6)) + + actual := g.FirstDescendantsWith(6, func(v Vertex) bool { + // looking for first odd descendants + return v.(int)%2 != 0 + }) + + expected := make(Set) + expected.Add(1) + expected.Add(5) + + if expected.Intersection(actual).Len() != expected.Len() { + t.Fatalf("expected %#v, got %#v\n", expected, actual) + } + + foundOne := g.MatchDescendant(6, func(v Vertex) bool { + return v.(int) == 1 + }) + if !foundOne { + t.Fatal("did not match 1 in the graph") + } + + foundSix := g.MatchDescendant(6, func(v Vertex) bool { + return v.(int) == 6 + }) + if foundSix { + t.Fatal("6 should not be a descendant of itself") + } + + foundTen := g.MatchDescendant(6, func(v Vertex) bool { + return v.(int) == 10 + }) + if foundTen { + t.Fatal("10 is not in the graph at all") + } +} + +func TestAcyclicGraphFindAncestors(t *testing.T) { + var g AcyclicGraph + g.Add(0) + g.Add(1) + g.Add(2) + g.Add(3) + g.Add(4) + g.Add(5) + g.Add(6) + g.Connect(BasicEdge(1, 0)) + g.Connect(BasicEdge(2, 1)) + g.Connect(BasicEdge(6, 2)) + g.Connect(BasicEdge(4, 3)) + g.Connect(BasicEdge(5, 4)) + g.Connect(BasicEdge(6, 5)) + + actual := g.FirstAncestorsWith(6, func(v Vertex) bool { + // looking for first odd ancestors + return v.(int)%2 != 0 + }) + + expected := make(Set) + expected.Add(1) + expected.Add(5) + + if expected.Intersection(actual).Len() != expected.Len() { + t.Fatalf("expected %#v, got %#v\n", expected, actual) + } + + foundOne := g.MatchAncestor(6, func(v Vertex) bool { + return v.(int) == 1 + }) + if !foundOne { + t.Fatal("did not match 1 in the graph") + } + + foundSix := g.MatchAncestor(6, func(v Vertex) bool { + return v.(int) == 6 + }) + if foundSix { + t.Fatal("6 should not be a descendant of itself") + } + + foundTen := g.MatchAncestor(6, func(v Vertex) bool { + return v.(int) == 10 + }) + if foundTen { + t.Fatal("10 is not in the graph at all") + } +} + func TestAcyclicGraphWalk(t *testing.T) { var g AcyclicGraph g.Add(1) @@ -403,10 +502,7 @@ func BenchmarkDAG(b *testing.B) { b.StartTimer() // Find dependencies for every node for _, v := range g.Vertices() { - _, err := g.Ancestors(v) - if err != nil { - b.Fatal(err) - } + _ = g.Ancestors(v) } // reduce the final graph diff --git a/internal/dag/dot.go b/internal/dag/dot.go index 7e6d2af3b1..af9d5c8680 100644 --- a/internal/dag/dot.go +++ b/internal/dag/dot.go @@ -1,8 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( "bytes" "fmt" + "maps" + "slices" "sort" "strings" ) @@ -94,13 +99,8 @@ func (v *marshalVertex) dot(g *marshalGraph, opts *DotOpts) []byte { return []byte{} } - newAttrs := make(map[string]string) - for k, v := range attrs { - newAttrs[k] = v - } - for k, v := range node.Attrs { - newAttrs[k] = v - } + newAttrs := maps.Clone(attrs) + maps.Copy(newAttrs, node.Attrs) name = node.Name attrs = newAttrs @@ -174,7 +174,7 @@ func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) { w.Write(v.dot(g, opts)) } - var dotEdges []string + dotEdges := make(map[string]string) if opts.DrawCycles { for _, c := range g.Cycles { @@ -200,20 +200,22 @@ func (g *marshalGraph) writeBody(opts *DotOpts, w *indentWriter) { Attrs: make(map[string]string), } - dotEdges = append(dotEdges, cycleDot(e, g)) + dotEdges[e.Name] = cycleDot(e, g) src = tgt } } } for _, e := range g.Edges { - dotEdges = append(dotEdges, e.dot(g)) + // only add the edge if it's not been added as part of a cycle + // or if there are duplicates. + if _, ok := dotEdges[e.Name]; !ok { + dotEdges[e.Name] = e.dot(g) + } } - // srot these again to match the old output - sort.Strings(dotEdges) - - for _, e := range dotEdges { + // sort these again to match the old output + for _, e := range slices.Sorted(maps.Values(dotEdges)) { w.WriteString(e + "\n") } diff --git a/internal/dag/dot_test.go b/internal/dag/dot_test.go index 1c28ae68a1..8b7074d60b 100644 --- a/internal/dag/dot_test.go +++ b/internal/dag/dot_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( diff --git a/internal/dag/edge.go b/internal/dag/edge.go index 8c78924bbd..d97b91c0db 100644 --- a/internal/dag/edge.go +++ b/internal/dag/edge.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag // Edge represents an edge in the graph, with a source and target vertex. diff --git a/internal/dag/edge_test.go b/internal/dag/edge_test.go index 67d2cbed4c..a768dd97de 100644 --- a/internal/dag/edge_test.go +++ b/internal/dag/edge_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( diff --git a/internal/dag/graph.go b/internal/dag/graph.go index 222ac07869..0d66db82e6 100644 --- a/internal/dag/graph.go +++ b/internal/dag/graph.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( @@ -166,28 +169,29 @@ func (g *Graph) RemoveEdge(edge Edge) { } } -// UpEdges returns the vertices connected to the outward edges from the source -// Vertex v. +// UpEdges returns the vertices that are *sources* of edges that target the +// destination Vertex v. func (g *Graph) UpEdges(v Vertex) Set { return g.upEdgesNoCopy(v).Copy() } -// DownEdges returns the vertices connected from the inward edges to Vertex v. +// DownEdges returns the vertices that are *targets* of edges that originate +// from the source Vertex v. func (g *Graph) DownEdges(v Vertex) Set { return g.downEdgesNoCopy(v).Copy() } -// downEdgesNoCopy returns the outward edges from the source Vertex v as a Set. -// This Set is the same as used internally bu the Graph to prevent a copy, and -// must not be modified by the caller. +// downEdgesNoCopy returns the vertices targeted by edges from the source Vertex +// v as a Set. This Set is the same as used internally by the Graph to prevent a +// copy, and must not be modified by the caller. func (g *Graph) downEdgesNoCopy(v Vertex) Set { g.init() return g.downEdges[hashcode(v)] } -// upEdgesNoCopy returns the inward edges to the destination Vertex v as a Set. -// This Set is the same as used internally bu the Graph to prevent a copy, and -// must not be modified by the caller. +// upEdgesNoCopy returns the vertices that are sources of edges targeting the +// destination Vertex v as a Set. This Set is the same as used internally by the +// Graph to prevent a copy, and must not be modified by the caller. func (g *Graph) upEdgesNoCopy(v Vertex) Set { g.init() return g.upEdges[hashcode(v)] @@ -230,6 +234,28 @@ func (g *Graph) Connect(edge Edge) { s.Add(source) } +// Subsume imports all of the nodes and edges from the given graph into the +// reciever, leaving the given graph unchanged. +// +// If any of the nodes in the given graph are already present in the reciever +// then the existing node will be retained and any new edges from the given +// graph will be connected with it. +// +// If the given graph has edges in common with the reciever then they will be +// ignored, because each pair of nodes can only be connected once. +func (g *Graph) Subsume(other *Graph) { + // We're using Set.Filter just as a "visit each element" here, so we're + // not doing anything with the result (which will always be empty). + other.vertices.Filter(func(i interface{}) bool { + g.Add(i) + return false + }) + other.edges.Filter(func(i interface{}) bool { + g.Connect(i.(Edge)) + return false + }) +} + // String outputs some human-friendly output for the graph structure. func (g *Graph) StringWithNodeTypes() string { var buf bytes.Buffer diff --git a/internal/dag/graph_test.go b/internal/dag/graph_test.go index 76c47641da..48755fae91 100644 --- a/internal/dag/graph_test.go +++ b/internal/dag/graph_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( diff --git a/internal/dag/marshal.go b/internal/dag/marshal.go index a78032ad36..8439c3ae1e 100644 --- a/internal/dag/marshal.go +++ b/internal/dag/marshal.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( @@ -141,7 +144,7 @@ func newMarshalGraph(name string, g *Graph) *marshalGraph { sort.Sort(edges(mg.Edges)) - for _, c := range (&AcyclicGraph{*g}).Cycles() { + for _, c := range (&AcyclicGraph{Graph: *g}).Cycles() { var cycle []*marshalVertex for _, v := range c { mv := newMarshalVertex(v) diff --git a/internal/dag/marshal_test.go b/internal/dag/marshal_test.go index 79f1c648ea..eb4f87718d 100644 --- a/internal/dag/marshal_test.go +++ b/internal/dag/marshal_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( diff --git a/internal/dag/set.go b/internal/dag/set.go index 390415e316..145cf31327 100644 --- a/internal/dag/set.go +++ b/internal/dag/set.go @@ -1,5 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag +import ( + "iter" + "maps" +) + // Set is a set data structure. type Set map[interface{}]interface{} @@ -89,25 +97,18 @@ func (s Set) Len() int { return len(s) } -// List returns the list of set elements. -func (s Set) List() []interface{} { - if s == nil { - return nil +// List returns the sequence of set elements. +func (s Set) List() iter.Seq[any] { + return func(yield func(any) bool) { + for _, v := range s { + if !yield(v) { + return + } + } } - - r := make([]interface{}, 0, len(s)) - for _, v := range s { - r = append(r, v) - } - - return r } // Copy returns a shallow copy of the set. func (s Set) Copy() Set { - c := make(Set, len(s)) - for k, v := range s { - c[k] = v - } - return c + return maps.Clone(s) } diff --git a/internal/dag/set_test.go b/internal/dag/set_test.go index 721f2467dc..277991d5fb 100644 --- a/internal/dag/set_test.go +++ b/internal/dag/set_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( diff --git a/internal/dag/tarjan.go b/internal/dag/tarjan.go index fb4d4a7732..b488bf9478 100644 --- a/internal/dag/tarjan.go +++ b/internal/dag/tarjan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag // StronglyConnected returns the list of strongly connected components @@ -55,13 +58,6 @@ func stronglyConnected(acct *sccAcct, g *Graph, v Vertex) int { return minIdx } -func min(a, b int) int { - if a <= b { - return a - } - return b -} - // sccAcct is used ot pass around accounting information for // the StronglyConnectedComponents algorithm type sccAcct struct { diff --git a/internal/dag/tarjan_test.go b/internal/dag/tarjan_test.go index cdebcb3bba..4e68d5bf77 100644 --- a/internal/dag/tarjan_test.go +++ b/internal/dag/tarjan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( diff --git a/internal/dag/walk.go b/internal/dag/walk.go index ff8afeac7c..836047c1dd 100644 --- a/internal/dag/walk.go +++ b/internal/dag/walk.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( @@ -73,6 +76,15 @@ func (w *Walker) init() { } } +// NewWalker creates a new walker with the given callback function. +func NewWalker(cb WalkFunc, opts ...func(*Walker)) *Walker { + w := &Walker{Callback: cb} + for _, opt := range opts { + opt(w) + } + return w +} + type walkerVertex struct { // These should only be set once on initialization and never written again. // They are not protected by a lock since they don't need to be since diff --git a/internal/dag/walk_test.go b/internal/dag/walk_test.go index fc5844e2e1..ef2353fd96 100644 --- a/internal/dag/walk_test.go +++ b/internal/dag/walk_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package dag import ( @@ -19,7 +22,7 @@ func TestWalker_basic(t *testing.T) { // Run it a bunch of times since it is timing dependent for i := 0; i < 50; i++ { var order []interface{} - w := &Walker{Callback: walkCbRecord(&order)} + w := NewWalker(walkCbRecord(&order)) w.Update(&g) // Wait @@ -44,7 +47,7 @@ func TestWalker_updateNilGraph(t *testing.T) { // Run it a bunch of times since it is timing dependent for i := 0; i < 50; i++ { var order []interface{} - w := &Walker{Callback: walkCbRecord(&order)} + w := NewWalker(walkCbRecord(&order)) w.Update(&g) w.Update(nil) @@ -80,7 +83,7 @@ func TestWalker_error(t *testing.T) { return recordF(v) } - w := &Walker{Callback: cb} + w := NewWalker(cb) w.Update(&g) // Wait @@ -107,7 +110,6 @@ func TestWalker_newVertex(t *testing.T) { done2 := make(chan int) // Build a callback that notifies us when 2 has been walked - var w *Walker cb := func(v Vertex) tfdiags.Diagnostics { if v == 2 { defer close(done2) @@ -116,7 +118,7 @@ func TestWalker_newVertex(t *testing.T) { } // Add the initial vertices - w = &Walker{Callback: cb} + w := NewWalker(cb) w.Update(&g) // if 2 has been visited, the walk is complete so far @@ -152,7 +154,7 @@ func TestWalker_removeVertex(t *testing.T) { var order []interface{} recordF := walkCbRecord(&order) - var w *Walker + w := NewWalker(nil) cb := func(v Vertex) tfdiags.Diagnostics { if v == 1 { g.Remove(2) @@ -163,7 +165,7 @@ func TestWalker_removeVertex(t *testing.T) { } // Add the initial vertices - w = &Walker{Callback: cb} + w.Callback = cb w.Update(&g) // Wait @@ -188,7 +190,7 @@ func TestWalker_newEdge(t *testing.T) { var order []interface{} recordF := walkCbRecord(&order) - var w *Walker + w := NewWalker(nil) cb := func(v Vertex) tfdiags.Diagnostics { // record where we are first, otherwise the Updated vertex may get // walked before the first visit. @@ -203,7 +205,7 @@ func TestWalker_newEdge(t *testing.T) { } // Add the initial vertices - w = &Walker{Callback: cb} + w.Callback = cb w.Update(&g) // Wait @@ -238,7 +240,7 @@ func TestWalker_removeEdge(t *testing.T) { // forcing 2 before 3 via the callback (and not the graph). If // 2 cannot execute before 3 (edge removal is non-functional), then // this test will timeout. - var w *Walker + w := NewWalker(nil) gateCh := make(chan struct{}) cb := func(v Vertex) tfdiags.Diagnostics { t.Logf("visit vertex %#v", v) @@ -271,7 +273,7 @@ func TestWalker_removeEdge(t *testing.T) { } // Add the initial vertices - w = &Walker{Callback: cb} + w.Callback = cb w.Update(&g) // Wait diff --git a/internal/depsfile/doc.go b/internal/depsfile/doc.go index a0f25b910c..d00a958511 100644 --- a/internal/depsfile/doc.go +++ b/internal/depsfile/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package depsfile contains the logic for reading and writing Terraform's // dependency lock and development override configuration files. // diff --git a/internal/depsfile/locks.go b/internal/depsfile/locks.go index 4997c4858f..9b3bcffc10 100644 --- a/internal/depsfile/locks.go +++ b/internal/depsfile/locks.go @@ -1,11 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package depsfile import ( "fmt" + "slices" "sort" + "maps" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" ) // Locks is the top-level type representing the information retained in a @@ -66,11 +72,7 @@ func (l *Locks) Provider(addr addrs.Provider) *ProviderLock { func (l *Locks) AllProviders() map[addrs.Provider]*ProviderLock { // We return a copy of our internal map so that future calls to // SetProvider won't modify the map we're returning, or vice-versa. - ret := make(map[addrs.Provider]*ProviderLock, len(l.providers)) - for k, v := range l.providers { - ret[k] = v - } - return ret + return maps.Clone(l.providers) } // SetProvider creates a new lock or replaces the existing lock for the given @@ -88,7 +90,7 @@ func (l *Locks) AllProviders() map[addrs.Provider]*ProviderLock { // non-lockable provider address then this function will panic. Use // function ProviderIsLockable to determine whether a particular provider // should participate in the version locking mechanism. -func (l *Locks) SetProvider(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock { +func (l *Locks) SetProvider(addr addrs.Provider, version providerreqs.Version, constraints providerreqs.VersionConstraints, hashes []providerreqs.Hash) *ProviderLock { if !ProviderIsLockable(addr) { panic(fmt.Sprintf("Locks.SetProvider with non-lockable provider %s", addr)) } @@ -172,7 +174,7 @@ func (l *Locks) SetSameOverriddenProviders(other *Locks) { // non-lockable provider address then this function will panic. Use // function ProviderIsLockable to determine whether a particular provider // should participate in the version locking mechanism. -func NewProviderLock(addr addrs.Provider, version getproviders.Version, constraints getproviders.VersionConstraints, hashes []getproviders.Hash) *ProviderLock { +func NewProviderLock(addr addrs.Provider, version providerreqs.Version, constraints providerreqs.VersionConstraints, hashes []providerreqs.Hash) *ProviderLock { if !ProviderIsLockable(addr) { panic(fmt.Sprintf("Locks.NewProviderLock with non-lockable provider %s", addr)) } @@ -193,7 +195,7 @@ func NewProviderLock(addr addrs.Provider, version getproviders.Version, constrai // assumes that we already sorted the items, which means that any duplicates // will be consecutive in the sequence. dedupeHashes := hashes[:0] - prevHash := getproviders.NilHash + prevHash := providerreqs.NilHash for _, hash := range hashes { if hash != prevHash { dedupeHashes = append(dedupeHashes, hash) @@ -312,11 +314,7 @@ func (l *Locks) Empty() bool { func (l *Locks) DeepCopy() *Locks { ret := NewLocks() for addr, lock := range l.providers { - var hashes []getproviders.Hash - if len(lock.hashes) > 0 { - hashes = make([]getproviders.Hash, len(lock.hashes)) - copy(hashes, lock.hashes) - } + hashes := slices.Clone(lock.hashes) ret.SetProvider(addr, lock.version, lock.versionConstraints, hashes) } return ret @@ -334,8 +332,8 @@ type ProviderLock struct { // constraint but the previous selection still remains valid. // "version" is therefore authoritative, while "versionConstraints" is // just for a UI hint and not used to make any real decisions. - version getproviders.Version - versionConstraints getproviders.VersionConstraints + version providerreqs.Version + versionConstraints providerreqs.VersionConstraints // hashes contains zero or more hashes of packages or package contents // for the package associated with the selected version across all of @@ -347,7 +345,7 @@ type ProviderLock struct { // "h1:" for the first hash format version. Other hash versions following // this scheme may come later. These versioned hash schemes are implemented // in the getproviders package; for example, "h1:" is implemented in - // getproviders.HashV1 . + // providerreqs.HashV1 . // // There is also a legacy hash format which is just a lowercase-hex-encoded // SHA256 hash of the official upstream .zip file for the selected version. @@ -364,7 +362,7 @@ type ProviderLock struct { // means we can only populate the hash for the current platform, and so // it won't be possible to verify a subsequent installation of the same // provider on a different platform. - hashes []getproviders.Hash + hashes []providerreqs.Hash } // Provider returns the address of the provider this lock applies to. @@ -373,7 +371,7 @@ func (l *ProviderLock) Provider() addrs.Provider { } // Version returns the currently-selected version for the corresponding provider. -func (l *ProviderLock) Version() getproviders.Version { +func (l *ProviderLock) Version() providerreqs.Version { return l.version } @@ -385,7 +383,7 @@ func (l *ProviderLock) Version() getproviders.Version { // configuration have changed since a selection was made, and thus hint to the // user that they may need to run terraform init -upgrade to apply the new // constraints. -func (l *ProviderLock) VersionConstraints() getproviders.VersionConstraints { +func (l *ProviderLock) VersionConstraints() providerreqs.VersionConstraints { return l.versionConstraints } @@ -399,7 +397,7 @@ func (l *ProviderLock) VersionConstraints() getproviders.VersionConstraints { // of which must match in order for verification to be considered successful. // // Do not modify the backing array of the returned slice. -func (l *ProviderLock) AllHashes() []getproviders.Hash { +func (l *ProviderLock) AllHashes() []providerreqs.Hash { return l.hashes } @@ -434,6 +432,6 @@ func (l *ProviderLock) ContainsAll(target *ProviderLock) bool { // // At least one of the given hashes must match for a package to be considered // valud. -func (l *ProviderLock) PreferredHashes() []getproviders.Hash { - return getproviders.PreferredHashes(l.hashes) +func (l *ProviderLock) PreferredHashes() []providerreqs.Hash { + return providerreqs.PreferredHashes(l.hashes) } diff --git a/internal/depsfile/locks_file.go b/internal/depsfile/locks_file.go index e619e06703..a6b3962103 100644 --- a/internal/depsfile/locks_file.go +++ b/internal/depsfile/locks_file.go @@ -1,7 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package depsfile import ( "fmt" + "maps" + "slices" "sort" "github.com/hashicorp/hcl/v2" @@ -12,7 +17,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/replacefile" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" @@ -135,10 +140,7 @@ func SaveLocksToBytes(locks *Locks) ([]byte, tfdiags.Diagnostics) { }, }) - providers := make([]addrs.Provider, 0, len(locks.providers)) - for provider := range locks.providers { - providers = append(providers, provider) - } + providers := slices.Collect(maps.Keys(locks.providers)) sort.Slice(providers, func(i, j int) bool { return providers[i].LessThan(providers[j]) }) @@ -149,7 +151,7 @@ func SaveLocksToBytes(locks *Locks) ([]byte, tfdiags.Diagnostics) { block := rootBody.AppendNewBlock("provider", []string{lock.addr.String()}) body := block.Body() body.SetAttributeValue("version", cty.StringVal(lock.version.String())) - if constraintsStr := getproviders.VersionConstraintsString(lock.versionConstraints); constraintsStr != "" { + if constraintsStr := providerreqs.VersionConstraintsString(lock.versionConstraints); constraintsStr != "" { body.SetAttributeValue("constraints", cty.StringVal(constraintsStr)) } if len(lock.hashes) != 0 { @@ -315,12 +317,12 @@ func decodeProviderLockFromHCL(block *hcl.Block) (*ProviderLock, tfdiags.Diagnos return ret, diags } -func decodeProviderVersionArgument(provider addrs.Provider, attr *hcl.Attribute) (getproviders.Version, tfdiags.Diagnostics) { +func decodeProviderVersionArgument(provider addrs.Provider, attr *hcl.Attribute) (providerreqs.Version, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if attr == nil { // It's not okay to omit this argument, but the caller should already // have generated diagnostics about that. - return getproviders.UnspecifiedVersion, diags + return providerreqs.UnspecifiedVersion, diags } expr := attr.Expr @@ -328,7 +330,7 @@ func decodeProviderVersionArgument(provider addrs.Provider, attr *hcl.Attribute) hclDiags := gohcl.DecodeExpression(expr, nil, &raw) diags = diags.Append(hclDiags) if hclDiags.HasErrors() { - return getproviders.UnspecifiedVersion, diags + return providerreqs.UnspecifiedVersion, diags } if raw == nil { diags = diags.Append(&hcl.Diagnostic{ @@ -337,9 +339,9 @@ func decodeProviderVersionArgument(provider addrs.Provider, attr *hcl.Attribute) Detail: "A provider lock block must contain a \"version\" argument.", Subject: expr.Range().Ptr(), // the range for a missing argument's expression is the body's missing item range }) - return getproviders.UnspecifiedVersion, diags + return providerreqs.UnspecifiedVersion, diags } - version, err := getproviders.ParseVersion(*raw) + version, err := providerreqs.ParseVersion(*raw) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -361,7 +363,7 @@ func decodeProviderVersionArgument(provider addrs.Provider, attr *hcl.Attribute) return version, diags } -func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl.Attribute) (getproviders.VersionConstraints, tfdiags.Diagnostics) { +func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl.Attribute) (providerreqs.VersionConstraints, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if attr == nil { // It's okay to omit this argument. @@ -375,7 +377,7 @@ func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl if hclDiags.HasErrors() { return nil, diags } - constraints, err := getproviders.ParseVersionConstraints(raw) + constraints, err := providerreqs.ParseVersionConstraints(raw) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -384,7 +386,7 @@ func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl Subject: expr.Range().Ptr(), }) } - if canon := getproviders.VersionConstraintsString(constraints); canon != raw { + if canon := providerreqs.VersionConstraintsString(constraints); canon != raw { // Canonical forms are required in the lock file, to reduce the risk // that a file diff will show changes that are entirely cosmetic. diags = diags.Append(&hcl.Diagnostic{ @@ -398,7 +400,7 @@ func decodeProviderVersionConstraintsArgument(provider addrs.Provider, attr *hcl return constraints, diags } -func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) ([]getproviders.Hash, tfdiags.Diagnostics) { +func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) ([]providerreqs.Hash, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if attr == nil { // It's okay to omit this argument. @@ -425,7 +427,7 @@ func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) return nil, diags } - ret := make([]getproviders.Hash, 0, len(hashExprs)) + ret := make([]providerreqs.Hash, 0, len(hashExprs)) for _, hashExpr := range hashExprs { var raw string hclDiags := gohcl.DecodeExpression(hashExpr, nil, &raw) @@ -434,7 +436,7 @@ func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) continue } - hash, err := getproviders.ParseHash(raw) + hash, err := providerreqs.ParseHash(raw) if err != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, @@ -451,7 +453,7 @@ func decodeProviderHashesArgument(provider addrs.Provider, attr *hcl.Attribute) return ret, diags } -func encodeHashSetTokens(hashes []getproviders.Hash) hclwrite.Tokens { +func encodeHashSetTokens(hashes []providerreqs.Hash) hclwrite.Tokens { // We'll generate the source code in a low-level way here (direct // token manipulation) because it's desirable to maintain exactly // the layout implemented here so that diffs against the locks diff --git a/internal/depsfile/locks_file_test.go b/internal/depsfile/locks_file_test.go index 632aa71c7a..0cf9b5ab56 100644 --- a/internal/depsfile/locks_file_test.go +++ b/internal/depsfile/locks_file_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package depsfile import ( @@ -9,8 +12,9 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -113,7 +117,7 @@ func TestLoadLocksFromFile(t *testing.T) { if got, want := lock.Version().String(), "1.0.0"; got != want { t.Errorf("wrong version\ngot: %s\nwant: %s", got, want) } - if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ""; got != want { + if got, want := providerreqs.VersionConstraintsString(lock.VersionConstraints()), ""; got != want { t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) } if got, want := len(lock.hashes), 0; got != want { @@ -127,7 +131,7 @@ func TestLoadLocksFromFile(t *testing.T) { if got, want := lock.Version().String(), "1.2.0"; got != want { t.Errorf("wrong version\ngot: %s\nwant: %s", got, want) } - if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), "~> 1.2"; got != want { + if got, want := providerreqs.VersionConstraintsString(lock.VersionConstraints()), "~> 1.2"; got != want { t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) } if got, want := len(lock.hashes), 0; got != want { @@ -141,13 +145,13 @@ func TestLoadLocksFromFile(t *testing.T) { if got, want := lock.Version().String(), "3.0.10"; got != want { t.Errorf("wrong version\ngot: %s\nwant: %s", got, want) } - if got, want := getproviders.VersionConstraintsString(lock.VersionConstraints()), ">= 3.0.2"; got != want { + if got, want := providerreqs.VersionConstraintsString(lock.VersionConstraints()), ">= 3.0.2"; got != want { t.Errorf("wrong version constraints\ngot: %s\nwant: %s", got, want) } - wantHashes := []getproviders.Hash{ - getproviders.MustParseHash("test:placeholder-hash-1"), - getproviders.MustParseHash("test:placeholder-hash-2"), - getproviders.MustParseHash("test:placeholder-hash-3"), + wantHashes := []providerreqs.Hash{ + providerreqs.MustParseHash("test:placeholder-hash-1"), + providerreqs.MustParseHash("test:placeholder-hash-2"), + providerreqs.MustParseHash("test:placeholder-hash-3"), } if diff := cmp.Diff(wantHashes, lock.hashes); diff != "" { t.Errorf("wrong hashes\n%s", diff) @@ -205,15 +209,15 @@ func TestSaveLocksToFile(t *testing.T) { barProvider := addrs.MustParseProviderSourceString("test/bar") bazProvider := addrs.MustParseProviderSourceString("test/baz") booProvider := addrs.MustParseProviderSourceString("test/boo") - oneDotOh := getproviders.MustParseVersion("1.0.0") - oneDotTwo := getproviders.MustParseVersion("1.2.0") - atLeastOneDotOh := getproviders.MustParseVersionConstraints(">= 1.0.0") - pessimisticOneDotOh := getproviders.MustParseVersionConstraints("~> 1") - abbreviatedOneDotTwo := getproviders.MustParseVersionConstraints("1.2") - hashes := []getproviders.Hash{ - getproviders.MustParseHash("test:cccccccccccccccccccccccccccccccccccccccccccccccc"), - getproviders.MustParseHash("test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), - getproviders.MustParseHash("test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), + oneDotOh := providerreqs.MustParseVersion("1.0.0") + oneDotTwo := providerreqs.MustParseVersion("1.2.0") + atLeastOneDotOh := providerreqs.MustParseVersionConstraints(">= 1.0.0") + pessimisticOneDotOh := providerreqs.MustParseVersionConstraints("~> 1") + abbreviatedOneDotTwo := providerreqs.MustParseVersionConstraints("1.2") + hashes := []providerreqs.Hash{ + providerreqs.MustParseHash("test:cccccccccccccccccccccccccccccccccccccccccccccccc"), + providerreqs.MustParseHash("test:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"), + providerreqs.MustParseHash("test:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), } locks.SetProvider(fooProvider, oneDotOh, atLeastOneDotOh, hashes) locks.SetProvider(barProvider, oneDotTwo, pessimisticOneDotOh, nil) @@ -230,7 +234,7 @@ func TestSaveLocksToFile(t *testing.T) { fileInfo, err := os.Stat(filename) if err != nil { - t.Fatalf(err.Error()) + t.Fatal(err.Error()) } if mode := fileInfo.Mode(); mode&0111 != 0 { t.Fatalf("Expected lock file to be non-executable: %o", mode) @@ -238,7 +242,7 @@ func TestSaveLocksToFile(t *testing.T) { gotContentBytes, err := ioutil.ReadFile(filename) if err != nil { - t.Fatalf(err.Error()) + t.Fatal(err.Error()) } gotContent := string(gotContentBytes) wantContent := `# This file is maintained automatically by "terraform init". diff --git a/internal/depsfile/locks_test.go b/internal/depsfile/locks_test.go index ce84d2e2e0..59e2e6afba 100644 --- a/internal/depsfile/locks_test.go +++ b/internal/depsfile/locks_test.go @@ -1,22 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package depsfile import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" ) func TestLocksEqual(t *testing.T) { boopProvider := addrs.NewDefaultProvider("boop") - v2 := getproviders.MustParseVersion("2.0.0") - v2LocalBuild := getproviders.MustParseVersion("2.0.0+awesomecorp.1") - v2GtConstraints := getproviders.MustParseVersionConstraints(">= 2.0.0") - v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0") - hash1 := getproviders.HashScheme("test").New("1") - hash2 := getproviders.HashScheme("test").New("2") - hash3 := getproviders.HashScheme("test").New("3") + v2 := providerreqs.MustParseVersion("2.0.0") + v2LocalBuild := providerreqs.MustParseVersion("2.0.0+awesomecorp.1") + v2GtConstraints := providerreqs.MustParseVersionConstraints(">= 2.0.0") + v2EqConstraints := providerreqs.MustParseVersionConstraints("2.0.0") + hash1 := providerreqs.HashScheme("test").New("1") + hash2 := providerreqs.HashScheme("test").New("2") + hash3 := providerreqs.HashScheme("test").New("3") equalBothWays := func(t *testing.T, a, b *Locks) { t.Helper() @@ -66,7 +70,7 @@ func TestLocksEqual(t *testing.T) { t.Run("both have boop provider with same version and same hashes", func(t *testing.T) { a := NewLocks() b := NewLocks() - hashes := []getproviders.Hash{hash1, hash2, hash3} + hashes := []providerreqs.Hash{hash1, hash2, hash3} a.SetProvider(boopProvider, v2, v2EqConstraints, hashes) b.SetProvider(boopProvider, v2, v2EqConstraints, hashes) equalBothWays(t, a, b) @@ -74,8 +78,8 @@ func TestLocksEqual(t *testing.T) { t.Run("both have boop provider with same version but different hashes", func(t *testing.T) { a := NewLocks() b := NewLocks() - hashesA := []getproviders.Hash{hash1, hash2} - hashesB := []getproviders.Hash{hash1, hash3} + hashesA := []providerreqs.Hash{hash1, hash2} + hashesB := []providerreqs.Hash{hash1, hash3} a.SetProvider(boopProvider, v2, v2EqConstraints, hashesA) b.SetProvider(boopProvider, v2, v2EqConstraints, hashesB) nonEqualBothWays(t, a, b) @@ -84,13 +88,13 @@ func TestLocksEqual(t *testing.T) { func TestLocksEqualProviderAddress(t *testing.T) { boopProvider := addrs.NewDefaultProvider("boop") - v2 := getproviders.MustParseVersion("2.0.0") - v2LocalBuild := getproviders.MustParseVersion("2.0.0+awesomecorp.1") - v2GtConstraints := getproviders.MustParseVersionConstraints(">= 2.0.0") - v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0") - hash1 := getproviders.HashScheme("test").New("1") - hash2 := getproviders.HashScheme("test").New("2") - hash3 := getproviders.HashScheme("test").New("3") + v2 := providerreqs.MustParseVersion("2.0.0") + v2LocalBuild := providerreqs.MustParseVersion("2.0.0+awesomecorp.1") + v2GtConstraints := providerreqs.MustParseVersionConstraints(">= 2.0.0") + v2EqConstraints := providerreqs.MustParseVersionConstraints("2.0.0") + hash1 := providerreqs.HashScheme("test").New("1") + hash2 := providerreqs.HashScheme("test").New("2") + hash3 := providerreqs.HashScheme("test").New("3") equalProviderAddressBothWays := func(t *testing.T, a, b *Locks) { t.Helper() @@ -132,8 +136,8 @@ func TestLocksEqualProviderAddress(t *testing.T) { t.Run("both have boop provider with same version but different hashes", func(t *testing.T) { a := NewLocks() b := NewLocks() - hashesA := []getproviders.Hash{hash1, hash2} - hashesB := []getproviders.Hash{hash1, hash3} + hashesA := []providerreqs.Hash{hash1, hash2} + hashesB := []providerreqs.Hash{hash1, hash3} a.SetProvider(boopProvider, v2, v2EqConstraints, hashesA) b.SetProvider(boopProvider, v2, v2EqConstraints, hashesB) equalProviderAddressBothWays(t, a, b) @@ -143,17 +147,17 @@ func TestLocksEqualProviderAddress(t *testing.T) { func TestLocksProviderSetRemove(t *testing.T) { beepProvider := addrs.NewDefaultProvider("beep") boopProvider := addrs.NewDefaultProvider("boop") - v2 := getproviders.MustParseVersion("2.0.0") - v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0") - v2GtConstraints := getproviders.MustParseVersionConstraints(">= 2.0.0") - hash := getproviders.HashScheme("test").New("1") + v2 := providerreqs.MustParseVersion("2.0.0") + v2EqConstraints := providerreqs.MustParseVersionConstraints("2.0.0") + v2GtConstraints := providerreqs.MustParseVersionConstraints(">= 2.0.0") + hash := providerreqs.HashScheme("test").New("1") locks := NewLocks() if got, want := len(locks.AllProviders()), 0; got != want { t.Fatalf("fresh locks object already has providers") } - locks.SetProvider(boopProvider, v2, v2EqConstraints, []getproviders.Hash{hash}) + locks.SetProvider(boopProvider, v2, v2EqConstraints, []providerreqs.Hash{hash}) { got := locks.AllProviders() want := map[addrs.Provider]*ProviderLock{ @@ -161,7 +165,7 @@ func TestLocksProviderSetRemove(t *testing.T) { addr: boopProvider, version: v2, versionConstraints: v2EqConstraints, - hashes: []getproviders.Hash{hash}, + hashes: []providerreqs.Hash{hash}, }, } if diff := cmp.Diff(want, got, ProviderLockComparer); diff != "" { @@ -169,7 +173,7 @@ func TestLocksProviderSetRemove(t *testing.T) { } } - locks.SetProvider(beepProvider, v2, v2GtConstraints, []getproviders.Hash{hash}) + locks.SetProvider(beepProvider, v2, v2GtConstraints, []providerreqs.Hash{hash}) { got := locks.AllProviders() want := map[addrs.Provider]*ProviderLock{ @@ -177,13 +181,13 @@ func TestLocksProviderSetRemove(t *testing.T) { addr: boopProvider, version: v2, versionConstraints: v2EqConstraints, - hashes: []getproviders.Hash{hash}, + hashes: []providerreqs.Hash{hash}, }, beepProvider: { addr: beepProvider, version: v2, versionConstraints: v2GtConstraints, - hashes: []getproviders.Hash{hash}, + hashes: []providerreqs.Hash{hash}, }, } if diff := cmp.Diff(want, got, ProviderLockComparer); diff != "" { @@ -199,7 +203,7 @@ func TestLocksProviderSetRemove(t *testing.T) { addr: beepProvider, version: v2, versionConstraints: v2GtConstraints, - hashes: []getproviders.Hash{hash}, + hashes: []providerreqs.Hash{hash}, }, } if diff := cmp.Diff(want, got, ProviderLockComparer); diff != "" { @@ -219,17 +223,17 @@ func TestLocksProviderSetRemove(t *testing.T) { func TestProviderLockContainsAll(t *testing.T) { provider := addrs.NewDefaultProvider("provider") - v2 := getproviders.MustParseVersion("2.0.0") - v2EqConstraints := getproviders.MustParseVersionConstraints("2.0.0") + v2 := providerreqs.MustParseVersion("2.0.0") + v2EqConstraints := providerreqs.MustParseVersionConstraints("2.0.0") t.Run("non-symmetric", func(t *testing.T) { - target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + target := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "K43RHM2klOoywtyW", "swJPXfuCNhJsTM5c", }) - original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + original := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "1ZAChGWUMWn4zmIk", "K43RHM2klOoywtyW", @@ -247,13 +251,13 @@ func TestProviderLockContainsAll(t *testing.T) { }) t.Run("symmetric", func(t *testing.T) { - target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + target := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "K43RHM2klOoywtyW", "swJPXfuCNhJsTM5c", }) - original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + original := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "K43RHM2klOoywtyW", "swJPXfuCNhJsTM5c", @@ -268,7 +272,7 @@ func TestProviderLockContainsAll(t *testing.T) { }) t.Run("edge case - null", func(t *testing.T) { - original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + original := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "K43RHM2klOoywtyW", "swJPXfuCNhJsTM5c", @@ -280,13 +284,13 @@ func TestProviderLockContainsAll(t *testing.T) { }) t.Run("edge case - empty", func(t *testing.T) { - original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + original := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "K43RHM2klOoywtyW", "swJPXfuCNhJsTM5c", }) - target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{}) + target := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{}) if !original.ContainsAll(target) { t.Fatalf("orginal should report true on empty") @@ -294,9 +298,9 @@ func TestProviderLockContainsAll(t *testing.T) { }) t.Run("edge case - original empty", func(t *testing.T) { - original := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{}) + original := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{}) - target := NewProviderLock(provider, v2, v2EqConstraints, []getproviders.Hash{ + target := NewProviderLock(provider, v2, v2EqConstraints, []providerreqs.Hash{ "9r3i9a9QmASqMnQM", "K43RHM2klOoywtyW", "swJPXfuCNhJsTM5c", diff --git a/internal/depsfile/paths.go b/internal/depsfile/paths.go index 252f67e208..54c2895d57 100644 --- a/internal/depsfile/paths.go +++ b/internal/depsfile/paths.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package depsfile // LockFilePath is the path, relative to a configuration's root module diff --git a/internal/depsfile/testing.go b/internal/depsfile/testing.go index cf8965be82..651296a364 100644 --- a/internal/depsfile/testing.go +++ b/internal/depsfile/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package depsfile import ( diff --git a/internal/didyoumean/name_suggestion.go b/internal/didyoumean/name_suggestion.go index 54899bc652..83612a593c 100644 --- a/internal/didyoumean/name_suggestion.go +++ b/internal/didyoumean/name_suggestion.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package didyoumean import ( diff --git a/internal/didyoumean/name_suggestion_test.go b/internal/didyoumean/name_suggestion_test.go index 756cb4b697..1a44dbdc09 100644 --- a/internal/didyoumean/name_suggestion_test.go +++ b/internal/didyoumean/name_suggestion_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package didyoumean import ( diff --git a/internal/e2e/e2e.go b/internal/e2e/e2e.go index e1ede0e460..77cc86a129 100644 --- a/internal/e2e/e2e.go +++ b/internal/e2e/e2e.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package e2e import ( @@ -150,8 +153,8 @@ func (b *binary) Run(args ...string) (stdout, stderr string, err error) { // Path returns a file path within the temporary working directory by // appending the given arguments as path segments. func (b *binary) Path(parts ...string) string { - args := make([]string, len(parts)+1) - args[0] = b.workDir + args := make([]string, 0, len(parts)+1) + args = append(args, b.workDir) args = append(args, parts...) return filepath.Join(args...) } diff --git a/internal/earlyconfig/config.go b/internal/earlyconfig/config.go deleted file mode 100644 index 86d93c27ba..0000000000 --- a/internal/earlyconfig/config.go +++ /dev/null @@ -1,210 +0,0 @@ -package earlyconfig - -import ( - "fmt" - "sort" - - version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" - "github.com/hashicorp/terraform/internal/moduledeps" - "github.com/hashicorp/terraform/internal/plugin/discovery" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// A Config is a node in the tree of modules within a configuration. -// -// The module tree is constructed by following ModuleCall instances recursively -// through the root module transitively into descendent modules. -type Config struct { - // RootModule points to the Config for the root module within the same - // module tree as this module. If this module _is_ the root module then - // this is self-referential. - Root *Config - - // ParentModule points to the Config for the module that directly calls - // this module. If this is the root module then this field is nil. - Parent *Config - - // Path is a sequence of module logical names that traverse from the root - // module to this config. Path is empty for the root module. - // - // This should only be used to display paths to the end-user in rare cases - // where we are talking about the static module tree, before module calls - // have been resolved. In most cases, an addrs.ModuleInstance describing - // a node in the dynamic module tree is better, since it will then include - // any keys resulting from evaluating "count" and "for_each" arguments. - Path addrs.Module - - // ChildModules points to the Config for each of the direct child modules - // called from this module. The keys in this map match the keys in - // Module.ModuleCalls. - Children map[string]*Config - - // Module points to the object describing the configuration for the - // various elements (variables, resources, etc) defined by this module. - Module *tfconfig.Module - - // CallPos is the source position for the header of the module block that - // requested this module. - // - // This field is meaningless for the root module, where its contents are undefined. - CallPos tfconfig.SourcePos - - // SourceAddr is the source address that the referenced module was requested - // from, as specified in configuration. - // - // This field is meaningless for the root module, where its contents are undefined. - SourceAddr addrs.ModuleSource - - // Version is the specific version that was selected for this module, - // based on version constraints given in configuration. - // - // This field is nil if the module was loaded from a non-registry source, - // since versions are not supported for other sources. - // - // This field is meaningless for the root module, where it will always - // be nil. - Version *version.Version -} - -// ProviderRequirements searches the full tree of modules under the receiver -// for both explicit and implicit dependencies on providers. -// -// The result is a full manifest of all of the providers that must be available -// in order to work with the receiving configuration. -// -// If the returned diagnostics includes errors then the resulting Requirements -// may be incomplete. -func (c *Config) ProviderRequirements() (getproviders.Requirements, tfdiags.Diagnostics) { - reqs := make(getproviders.Requirements) - diags := c.addProviderRequirements(reqs) - return reqs, diags -} - -// addProviderRequirements is the main part of the ProviderRequirements -// implementation, gradually mutating a shared requirements object to -// eventually return. -func (c *Config) addProviderRequirements(reqs getproviders.Requirements) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - // First we'll deal with the requirements directly in _our_ module... - for localName, providerReqs := range c.Module.RequiredProviders { - var fqn addrs.Provider - if source := providerReqs.Source; source != "" { - addr, moreDiags := addrs.ParseProviderSourceString(source) - if moreDiags.HasErrors() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid provider source address", - fmt.Sprintf("Invalid source %q for provider %q in %s", source, localName, c.Path), - )) - continue - } - fqn = addr - } - if fqn.IsZero() { - fqn = addrs.ImpliedProviderForUnqualifiedType(localName) - } - if _, ok := reqs[fqn]; !ok { - // We'll at least have an unconstrained dependency then, but might - // add to this in the loop below. - reqs[fqn] = nil - } - for _, constraintsStr := range providerReqs.VersionConstraints { - if constraintsStr != "" { - constraints, err := getproviders.ParseVersionConstraints(constraintsStr) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid provider version constraint", - fmt.Sprintf("Provider %q in %s has invalid version constraint %q: %s.", localName, c.Path, constraintsStr, err), - )) - continue - } - reqs[fqn] = append(reqs[fqn], constraints...) - } - } - } - - // ...and now we'll recursively visit all of the child modules to merge - // in their requirements too. - for _, childConfig := range c.Children { - moreDiags := childConfig.addProviderRequirements(reqs) - diags = diags.Append(moreDiags) - } - - return diags -} - -// ProviderDependencies is a deprecated variant of ProviderRequirements which -// uses the moduledeps models for representation. This is preserved to allow -// a gradual transition over to ProviderRequirements, but note that its -// support for fully-qualified provider addresses has some idiosyncracies. -func (c *Config) ProviderDependencies() (*moduledeps.Module, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - var name string - if len(c.Path) > 0 { - name = c.Path[len(c.Path)-1] - } - - ret := &moduledeps.Module{ - Name: name, - } - - providers := make(moduledeps.Providers) - for name, reqs := range c.Module.RequiredProviders { - var fqn addrs.Provider - if source := reqs.Source; source != "" { - addr, parseDiags := addrs.ParseProviderSourceString(source) - if parseDiags.HasErrors() { - diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ - Severity: tfconfig.DiagError, - Summary: "Invalid provider source", - Detail: fmt.Sprintf("Invalid source %q for provider", name), - })) - continue - } - fqn = addr - } - if fqn.IsZero() { - fqn = addrs.NewDefaultProvider(name) - } - var constraints version.Constraints - for _, reqStr := range reqs.VersionConstraints { - if reqStr != "" { - constraint, err := version.NewConstraint(reqStr) - if err != nil { - diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ - Severity: tfconfig.DiagError, - Summary: "Invalid provider version constraint", - Detail: fmt.Sprintf("Invalid version constraint %q for provider %s.", reqStr, fqn.String()), - })) - continue - } - constraints = append(constraints, constraint...) - } - } - providers[fqn] = moduledeps.ProviderDependency{ - Constraints: discovery.NewConstraints(constraints), - Reason: moduledeps.ProviderDependencyExplicit, - } - } - ret.Providers = providers - - childNames := make([]string, 0, len(c.Children)) - for name := range c.Children { - childNames = append(childNames, name) - } - sort.Strings(childNames) - - for _, name := range childNames { - child, childDiags := c.Children[name].ProviderDependencies() - ret.Children = append(ret.Children, child) - diags = diags.Append(childDiags) - } - - return ret, diags -} diff --git a/internal/earlyconfig/config_build.go b/internal/earlyconfig/config_build.go deleted file mode 100644 index dd84cf9ccc..0000000000 --- a/internal/earlyconfig/config_build.go +++ /dev/null @@ -1,173 +0,0 @@ -package earlyconfig - -import ( - "fmt" - "sort" - "strings" - - version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// BuildConfig constructs a Config from a root module by loading all of its -// descendent modules via the given ModuleWalker. -func BuildConfig(root *tfconfig.Module, walker ModuleWalker) (*Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - cfg := &Config{ - Module: root, - } - cfg.Root = cfg // Root module is self-referential. - cfg.Children, diags = buildChildModules(cfg, walker) - return cfg, diags -} - -func buildChildModules(parent *Config, walker ModuleWalker) (map[string]*Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - ret := map[string]*Config{} - calls := parent.Module.ModuleCalls - - // We'll sort the calls by their local names so that they'll appear in a - // predictable order in any logging that's produced during the walk. - callNames := make([]string, 0, len(calls)) - for k := range calls { - callNames = append(callNames, k) - } - sort.Strings(callNames) - - for _, callName := range callNames { - call := calls[callName] - path := make([]string, len(parent.Path)+1) - copy(path, parent.Path) - path[len(path)-1] = call.Name - - var vc version.Constraints - haveVersionArg := false - if strings.TrimSpace(call.Version) != "" { - haveVersionArg = true - - var err error - vc, err = version.NewConstraint(call.Version) - if err != nil { - diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ - Severity: tfconfig.DiagError, - Summary: "Invalid version constraint", - Detail: fmt.Sprintf("Module %q (declared at %s line %d) has invalid version constraint %q: %s.", callName, call.Pos.Filename, call.Pos.Line, call.Version, err), - })) - continue - } - } - - var sourceAddr addrs.ModuleSource - var err error - if haveVersionArg { - sourceAddr, err = addrs.ParseModuleSourceRegistry(call.Source) - } else { - sourceAddr, err = addrs.ParseModuleSource(call.Source) - } - if err != nil { - if haveVersionArg { - diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ - Severity: tfconfig.DiagError, - Summary: "Invalid registry module source address", - Detail: fmt.Sprintf("Module %q (declared at %s line %d) has invalid source address %q: %s.\n\nTerraform assumed that you intended a module registry source address because you also set the argument \"version\", which applies only to registry modules.", callName, call.Pos.Filename, call.Pos.Line, call.Source, err), - })) - } else { - diags = diags.Append(wrapDiagnostic(tfconfig.Diagnostic{ - Severity: tfconfig.DiagError, - Summary: "Invalid module source address", - Detail: fmt.Sprintf("Module %q (declared at %s line %d) has invalid source address %q: %s.", callName, call.Pos.Filename, call.Pos.Line, call.Source, err), - })) - } - // If we didn't have a valid source address then we can't continue - // down the module tree with this one. - continue - } - - req := ModuleRequest{ - Name: call.Name, - Path: path, - SourceAddr: sourceAddr, - VersionConstraints: vc, - Parent: parent, - CallPos: call.Pos, - } - - mod, ver, modDiags := walker.LoadModule(&req) - diags = append(diags, modDiags...) - if mod == nil { - // nil can be returned if the source address was invalid and so - // nothing could be loaded whatsoever. LoadModule should've - // returned at least one error diagnostic in that case. - continue - } - - child := &Config{ - Parent: parent, - Root: parent.Root, - Path: path, - Module: mod, - CallPos: call.Pos, - SourceAddr: sourceAddr, - Version: ver, - } - - child.Children, modDiags = buildChildModules(child, walker) - diags = diags.Append(modDiags) - - ret[call.Name] = child - } - - return ret, diags -} - -// ModuleRequest is used as part of the ModuleWalker interface used with -// function BuildConfig. -type ModuleRequest struct { - // Name is the "logical name" of the module call within configuration. - // This is provided in case the name is used as part of a storage key - // for the module, but implementations must otherwise treat it as an - // opaque string. It is guaranteed to have already been validated as an - // HCL identifier and UTF-8 encoded. - Name string - - // Path is a list of logical names that traverse from the root module to - // this module. This can be used, for example, to form a lookup key for - // each distinct module call in a configuration, allowing for multiple - // calls with the same name at different points in the tree. - Path addrs.Module - - // SourceAddr is the source address string provided by the user in - // configuration. - SourceAddr addrs.ModuleSource - - // VersionConstraint is the version constraint applied to the module in - // configuration. - VersionConstraints version.Constraints - - // Parent is the partially-constructed module tree node that the loaded - // module will be added to. Callers may refer to any field of this - // structure except Children, which is still under construction when - // ModuleRequest objects are created and thus has undefined content. - // The main reason this is provided is so that full module paths can - // be constructed for uniqueness. - Parent *Config - - // CallRange is the source position for the header of the "module" block - // in configuration that prompted this request. - CallPos tfconfig.SourcePos -} - -// ModuleWalker is an interface used with BuildConfig. -type ModuleWalker interface { - LoadModule(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) -} - -// ModuleWalkerFunc is an implementation of ModuleWalker that directly wraps -// a callback function, for more convenient use of that interface. -type ModuleWalkerFunc func(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) - -func (f ModuleWalkerFunc) LoadModule(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { - return f(req) -} diff --git a/internal/earlyconfig/config_test.go b/internal/earlyconfig/config_test.go deleted file mode 100644 index 21aa71beea..0000000000 --- a/internal/earlyconfig/config_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package earlyconfig - -import ( - "log" - "path/filepath" - "testing" - - "github.com/google/go-cmp/cmp" - version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" - svchost "github.com/hashicorp/terraform-svchost" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -func TestConfigProviderRequirements(t *testing.T) { - cfg := testConfig(t, "testdata/provider-reqs") - - impliedProvider := addrs.NewProvider( - addrs.DefaultProviderRegistryHost, - "hashicorp", "implied", - ) - nullProvider := addrs.NewProvider( - addrs.DefaultProviderRegistryHost, - "hashicorp", "null", - ) - randomProvider := addrs.NewProvider( - addrs.DefaultProviderRegistryHost, - "hashicorp", "random", - ) - tlsProvider := addrs.NewProvider( - addrs.DefaultProviderRegistryHost, - "hashicorp", "tls", - ) - happycloudProvider := addrs.NewProvider( - svchost.Hostname("tf.example.com"), - "awesomecorp", "happycloud", - ) - - got, diags := cfg.ProviderRequirements() - if diags.HasErrors() { - t.Fatalf("unexpected diagnostics: %s", diags.Err().Error()) - } - want := getproviders.Requirements{ - // the nullProvider constraints from the two modules are merged - nullProvider: getproviders.MustParseVersionConstraints("~> 2.0.0, 2.0.1"), - randomProvider: getproviders.MustParseVersionConstraints("~> 1.2.0"), - tlsProvider: getproviders.MustParseVersionConstraints("~> 3.0"), - impliedProvider: nil, - happycloudProvider: nil, - } - - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("wrong result\n%s", diff) - } -} - -func testConfig(t *testing.T, baseDir string) *Config { - rootMod, diags := LoadModule(baseDir) - if diags.HasErrors() { - t.Fatalf("unexpected diagnostics: %s", diags.Err().Error()) - } - - cfg, diags := BuildConfig(rootMod, ModuleWalkerFunc(testModuleWalkerFunc)) - if diags.HasErrors() { - t.Fatalf("unexpected diagnostics: %s", diags.Err().Error()) - } - - return cfg -} - -// testModuleWalkerFunc is a simple implementation of ModuleWalkerFunc that -// only understands how to resolve relative filesystem paths, using source -// location information from the call. -func testModuleWalkerFunc(req *ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { - callFilename := req.CallPos.Filename - sourcePath := req.SourceAddr.String() - finalPath := filepath.Join(filepath.Dir(callFilename), sourcePath) - log.Printf("[TRACE] %s in %s -> %s", sourcePath, callFilename, finalPath) - - newMod, diags := LoadModule(finalPath) - return newMod, version.Must(version.NewVersion("0.0.0")), diags -} diff --git a/internal/earlyconfig/diagnostics.go b/internal/earlyconfig/diagnostics.go deleted file mode 100644 index 15adad5638..0000000000 --- a/internal/earlyconfig/diagnostics.go +++ /dev/null @@ -1,82 +0,0 @@ -package earlyconfig - -import ( - "fmt" - - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -func wrapDiagnostics(diags tfconfig.Diagnostics) tfdiags.Diagnostics { - ret := make(tfdiags.Diagnostics, len(diags)) - for i, diag := range diags { - ret[i] = wrapDiagnostic(diag) - } - return ret -} - -func wrapDiagnostic(diag tfconfig.Diagnostic) tfdiags.Diagnostic { - return wrappedDiagnostic{ - d: diag, - } -} - -type wrappedDiagnostic struct { - d tfconfig.Diagnostic -} - -func (d wrappedDiagnostic) Severity() tfdiags.Severity { - switch d.d.Severity { - case tfconfig.DiagError: - return tfdiags.Error - case tfconfig.DiagWarning: - return tfdiags.Warning - default: - // Should never happen since there are no other severities - return 0 - } -} - -func (d wrappedDiagnostic) Description() tfdiags.Description { - // Since the inspect library doesn't produce precise source locations, - // we include the position information as part of the error message text. - // See the comment inside method "Source" for more information. - switch { - case d.d.Pos == nil: - return tfdiags.Description{ - Summary: d.d.Summary, - Detail: d.d.Detail, - } - case d.d.Detail != "": - return tfdiags.Description{ - Summary: d.d.Summary, - Detail: fmt.Sprintf("On %s line %d: %s", d.d.Pos.Filename, d.d.Pos.Line, d.d.Detail), - } - default: - return tfdiags.Description{ - Summary: fmt.Sprintf("%s (on %s line %d)", d.d.Summary, d.d.Pos.Filename, d.d.Pos.Line), - } - } -} - -func (d wrappedDiagnostic) Source() tfdiags.Source { - // Since the inspect library is constrained by the lowest common denominator - // between legacy HCL and modern HCL, it only returns ranges at whole-line - // granularity, and that isn't sufficient to populate a tfdiags.Source - // and so we'll just omit ranges altogether and include the line number in - // the Description text. - // - // Callers that want to return nicer errors should consider reacting to - // earlyconfig errors by attempting a follow-up parse with the normal - // config loader, which can produce more precise source location - // information. - return tfdiags.Source{} -} - -func (d wrappedDiagnostic) FromExpr() *tfdiags.FromExpr { - return nil -} - -func (d wrappedDiagnostic) ExtraInfo() interface{} { - return nil -} diff --git a/internal/earlyconfig/doc.go b/internal/earlyconfig/doc.go deleted file mode 100644 index a9cf10f37c..0000000000 --- a/internal/earlyconfig/doc.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package earlyconfig is a specialized alternative to the top-level "configs" -// package that does only shallow processing of configuration and is therefore -// able to be much more liberal than the full config loader in what it accepts. -// -// In particular, it can accept both current and legacy HCL syntax, and it -// ignores top-level blocks that it doesn't recognize. These two characteristics -// make this package ideal for dependency-checking use-cases so that we are -// more likely to be able to return an error message about an explicit -// incompatibility than to return a less-actionable message about a construct -// not being supported. -// -// However, its liberal approach also means it should be used sparingly. It -// exists primarily for "terraform init", so that it is able to detect -// incompatibilities more robustly when installing dependencies. For most -// other use-cases, use the "configs" and "configs/configload" packages. -// -// Package earlyconfig is a wrapper around the terraform-config-inspect -// codebase, adding to it just some helper functionality for Terraform's own -// use-cases. -package earlyconfig diff --git a/internal/earlyconfig/module.go b/internal/earlyconfig/module.go deleted file mode 100644 index e4edba0e05..0000000000 --- a/internal/earlyconfig/module.go +++ /dev/null @@ -1,13 +0,0 @@ -package earlyconfig - -import ( - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// LoadModule loads some top-level metadata for the module in the given -// directory. -func LoadModule(dir string) (*tfconfig.Module, tfdiags.Diagnostics) { - mod, diags := tfconfig.LoadModule(dir) - return mod, wrapDiagnostics(diags) -} diff --git a/internal/earlyconfig/testdata/provider-reqs/child/provider-reqs-child.tf b/internal/earlyconfig/testdata/provider-reqs/child/provider-reqs-child.tf deleted file mode 100644 index ff03ded90e..0000000000 --- a/internal/earlyconfig/testdata/provider-reqs/child/provider-reqs-child.tf +++ /dev/null @@ -1,11 +0,0 @@ -terraform { - required_providers { - cloud = { - source = "tf.example.com/awesomecorp/happycloud" - } - null = { - # This should merge with the null provider constraint in the root module - version = "2.0.1" - } - } -} diff --git a/internal/experiments/doc.go b/internal/experiments/doc.go index 5538d739c9..0e671be50b 100644 --- a/internal/experiments/doc.go +++ b/internal/experiments/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package experiments contains the models and logic for opt-in experiments // that can be activated for a particular Terraform module. // diff --git a/internal/experiments/errors.go b/internal/experiments/errors.go index a1fdc6f5c4..b6d59778c5 100644 --- a/internal/experiments/errors.go +++ b/internal/experiments/errors.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package experiments import ( diff --git a/internal/experiments/experiment.go b/internal/experiments/experiment.go index 41787c679a..db7a660d93 100644 --- a/internal/experiments/experiment.go +++ b/internal/experiments/experiment.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package experiments // Experiment represents a particular experiment, which can be activated @@ -14,19 +17,29 @@ type Experiment string // identifier so that it can be specified in configuration. const ( VariableValidation = Experiment("variable_validation") + VariableValidationCrossRef = Experiment("variable_validation_crossref") ModuleVariableOptionalAttrs = Experiment("module_variable_optional_attrs") SuppressProviderSensitiveAttrs = Experiment("provider_sensitive_attrs") + TemplateStringFunc = Experiment("template_string_func") ConfigDrivenMove = Experiment("config_driven_move") PreconditionsPostconditions = Experiment("preconditions_postconditions") + EphemeralValues = Experiment("ephemeral_values") + UnknownInstances = Experiment("unknown_instances") + Actions = Experiment("actions") ) func init() { // Each experiment constant defined above must be registered here as either // a current or a concluded experiment. + registerCurrentExperiment(Actions) + registerConcludedExperiment(UnknownInstances, "Unknown instances are being rolled into a larger feature for deferring unready resources and modules.") registerConcludedExperiment(VariableValidation, "Custom variable validation can now be used by default, without enabling an experiment.") + registerConcludedExperiment(VariableValidationCrossRef, "Input variable validation rules may now refer to other objects in the same module without enabling any experiment.") registerConcludedExperiment(SuppressProviderSensitiveAttrs, "Provider-defined sensitive attributes are now redacted by default, without enabling an experiment.") + registerConcludedExperiment(TemplateStringFunc, "The templatestring function can now be used without enabling an experiment.") registerConcludedExperiment(ConfigDrivenMove, "Declarations of moved resource instances using \"moved\" blocks can now be used by default, without enabling an experiment.") registerConcludedExperiment(PreconditionsPostconditions, "Condition blocks can now be used by default, without enabling an experiment.") + registerConcludedExperiment(EphemeralValues, "Ephemeral values can now be used by default, without enabling an experiment.") registerConcludedExperiment(ModuleVariableOptionalAttrs, "The final feature corresponding to this experiment differs from the experimental form and is available in the Terraform language from Terraform v1.3.0 onwards.") } @@ -92,7 +105,7 @@ var currentExperiments = make(Set) // Members of this map are registered in the init function above. var concludedExperiments = make(map[Experiment]string) -//lint:ignore U1000 No experiments are active +//lint:ignore U1000 It is okay if no experiments are active func registerCurrentExperiment(exp Experiment) { currentExperiments.Add(exp) } diff --git a/internal/experiments/set.go b/internal/experiments/set.go index 8247e212b5..89bce6c7c3 100644 --- a/internal/experiments/set.go +++ b/internal/experiments/set.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package experiments // Set is a collection of experiments where every experiment is either a member diff --git a/internal/experiments/testing.go b/internal/experiments/testing.go index 414933782d..c2fd935af7 100644 --- a/internal/experiments/testing.go +++ b/internal/experiments/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package experiments import ( diff --git a/internal/genconfig/doc.go b/internal/genconfig/doc.go new file mode 100644 index 0000000000..36b69a0872 --- /dev/null +++ b/internal/genconfig/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package genconfig implements config generation from provided state values. +package genconfig diff --git a/internal/genconfig/generate_config.go b/internal/genconfig/generate_config.go new file mode 100644 index 0000000000..1faf9939e7 --- /dev/null +++ b/internal/genconfig/generate_config.go @@ -0,0 +1,567 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package genconfig + +import ( + "encoding/json" + "fmt" + "maps" + "slices" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// GenerateResourceContents generates HCL configuration code for the provided +// resource and state value. +// +// If you want to generate actual valid Terraform code you should follow this +// call up with a call to WrapResourceContents, which will place a Terraform +// resource header around the attributes and blocks returned by this function. +func GenerateResourceContents(addr addrs.AbsResourceInstance, + schema *configschema.Block, + pc addrs.LocalProviderConfig, + stateVal cty.Value) (string, tfdiags.Diagnostics) { + var buf strings.Builder + + var diags tfdiags.Diagnostics + + if pc.LocalName != addr.Resource.Resource.ImpliedProvider() || pc.Alias != "" { + buf.WriteString(strings.Repeat(" ", 2)) + buf.WriteString(fmt.Sprintf("provider = %s\n", pc.StringCompact())) + } + + if stateVal.RawEquals(cty.NilVal) { + diags = diags.Append(writeConfigAttributes(addr, &buf, schema.Attributes, 2)) + diags = diags.Append(writeConfigBlocks(addr, &buf, schema.BlockTypes, 2)) + } else { + diags = diags.Append(writeConfigAttributesFromExisting(addr, &buf, stateVal, schema.Attributes, 2)) + diags = diags.Append(writeConfigBlocksFromExisting(addr, &buf, stateVal, schema.BlockTypes, 2)) + } + + // The output better be valid HCL which can be parsed and formatted. + formatted := hclwrite.Format([]byte(buf.String())) + return string(formatted), diags +} + +func WrapResourceContents(addr addrs.AbsResourceInstance, config string) string { + var buf strings.Builder + + buf.WriteString(fmt.Sprintf("resource %q %q {\n", addr.Resource.Resource.Type, addr.Resource.Resource.Name)) + buf.WriteString(config) + buf.WriteString("}") + + // The output better be valid HCL which can be parsed and formatted. + formatted := hclwrite.Format([]byte(buf.String())) + return string(formatted) +} + +func writeConfigAttributes(addr addrs.AbsResourceInstance, buf *strings.Builder, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if len(attrs) == 0 { + return diags + } + + // Get a list of sorted attribute names so the output will be consistent between runs. + for _, name := range slices.Sorted(maps.Keys(attrs)) { + attrS := attrs[name] + if attrS.NestedType != nil { + diags = diags.Append(writeConfigNestedTypeAttribute(addr, buf, name, attrS, indent)) + continue + } + if attrS.Required { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = ", name)) + tok := hclwrite.TokensForValue(attrS.EmptyValue()) + if _, err := tok.WriteTo(buf); err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Skipped part of config generation", + Detail: fmt.Sprintf("Could not create attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted.", name, addr), + Extra: err, + }) + continue + } + writeAttrTypeConstraint(buf, attrS) + } else if attrS.Optional { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = ", name)) + tok := hclwrite.TokensForValue(attrS.EmptyValue()) + if _, err := tok.WriteTo(buf); err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Skipped part of config generation", + Detail: fmt.Sprintf("Could not create attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted.", name, addr), + Extra: err, + }) + continue + } + writeAttrTypeConstraint(buf, attrS) + } + } + return diags +} + +func writeConfigAttributesFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, attrs map[string]*configschema.Attribute, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + if len(attrs) == 0 { + return diags + } + + // Sort attribute names so the output will be consistent between runs. + for _, name := range slices.Sorted(maps.Keys(attrs)) { + attrS := attrs[name] + if attrS.NestedType != nil { + writeConfigNestedTypeAttributeFromExisting(addr, buf, name, attrS, stateVal, indent) + continue + } + + // Exclude computed-only attributes + if attrS.Required || attrS.Optional { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = ", name)) + + var val cty.Value + if !stateVal.IsNull() && stateVal.Type().HasAttribute(name) { + val = stateVal.GetAttr(name) + } else { + val = attrS.EmptyValue() + } + if val.Type() == cty.String { + // Before we inspect the string, take off any marks. + unmarked, marks := val.Unmark() + + // SHAMELESS HACK: If we have "" for an optional value, assume + // it is actually null, due to the legacy SDK. + if !unmarked.IsNull() && attrS.Optional && len(unmarked.AsString()) == 0 { + unmarked = attrS.EmptyValue() + } + + // Before we carry on, add the marks back. + val = unmarked.WithMarks(marks) + } + if attrS.Sensitive || val.IsMarked() { + buf.WriteString("null # sensitive") + } else { + // If the value is a string storing a JSON value we want to represent it in a terraform native way + // and encapsulate it in `jsonencode` as it is the idiomatic representation + if val.IsKnown() && !val.IsNull() && val.Type() == cty.String && json.Valid([]byte(val.AsString())) { + var ctyValue ctyjson.SimpleJSONValue + err := ctyValue.UnmarshalJSON([]byte(val.AsString())) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Failed to parse JSON", + Detail: fmt.Sprintf("Could not parse JSON value of attribute %s in %s when generating import configuration. The plan will likely report the missing attribute as being deleted. This is most likely a bug in Terraform, please report it.", name, addr), + Extra: err, + }) + continue + } + + // Lone deserializable primitive types are valid json, but should be treated as strings + if ctyValue.Type().IsPrimitiveType() { + if d := writeTokens(val, buf); d != nil { + diags = diags.Append(d) + continue + } + } else { + buf.WriteString("jsonencode(") + + if d := writeTokens(ctyValue.Value, buf); d != nil { + diags = diags.Append(d) + continue + } + + buf.WriteString(")") + } + } else { + if d := writeTokens(val, buf); d != nil { + diags = diags.Append(d) + continue + } + } + } + + buf.WriteString("\n") + } + } + return diags +} + +func writeTokens(val cty.Value, buf *strings.Builder) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + tok := hclwrite.TokensForValue(val) + if _, err := tok.WriteTo(buf); err != nil { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Skipped part of config generation", + Detail: "Could not create attribute in import configuration. The plan will likely report the missing attribute as being deleted.", + Extra: err, + }) + } + return diags +} + +func writeConfigBlocks(addr addrs.AbsResourceInstance, buf *strings.Builder, blocks map[string]*configschema.NestedBlock, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if len(blocks) == 0 { + return diags + } + + // Get a list of sorted block names so the output will be consistent between runs. + for _, name := range slices.Sorted(maps.Keys(blocks)) { + blockS := blocks[name] + diags = diags.Append(writeConfigNestedBlock(addr, buf, name, blockS, indent)) + } + return diags +} + +func writeConfigNestedBlock(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + switch schema.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s {", name)) + writeBlockTypeConstraint(buf, schema) + diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) + diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2)) + buf.WriteString("}\n") + return diags + case configschema.NestingList, configschema.NestingSet: + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s {", name)) + writeBlockTypeConstraint(buf, schema) + diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) + diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2)) + buf.WriteString("}\n") + return diags + case configschema.NestingMap: + buf.WriteString(strings.Repeat(" ", indent)) + // we use an arbitrary placeholder key (block label) "key" + buf.WriteString(fmt.Sprintf("%s \"key\" {", name)) + writeBlockTypeConstraint(buf, schema) + diags = diags.Append(writeConfigAttributes(addr, buf, schema.Attributes, indent+2)) + diags = diags.Append(writeConfigBlocks(addr, buf, schema.BlockTypes, indent+2)) + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("}\n") + return diags + default: + // This should not happen, the above should be exhaustive. + panic(fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String())) + } +} + +func writeConfigNestedTypeAttribute(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = ", name)) + + switch schema.NestedType.Nesting { + case configschema.NestingSingle: + buf.WriteString("{") + writeAttrTypeConstraint(buf, schema) + diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+2)) + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("}\n") + return diags + case configschema.NestingList, configschema.NestingSet: + buf.WriteString("[{") + writeAttrTypeConstraint(buf, schema) + diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+2)) + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("}]\n") + return diags + case configschema.NestingMap: + buf.WriteString("{") + writeAttrTypeConstraint(buf, schema) + buf.WriteString(strings.Repeat(" ", indent+2)) + // we use an arbitrary placeholder key "key" + buf.WriteString("key = {\n") + diags = diags.Append(writeConfigAttributes(addr, buf, schema.NestedType.Attributes, indent+4)) + buf.WriteString(strings.Repeat(" ", indent+2)) + buf.WriteString("}\n") + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("}\n") + return diags + default: + // This should not happen, the above should be exhaustive. + panic(fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String())) + } +} + +func writeConfigBlocksFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, stateVal cty.Value, blocks map[string]*configschema.NestedBlock, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if len(blocks) == 0 { + return diags + } + + // Sort block names so the output will be consistent between runs. + for _, name := range slices.Sorted(maps.Keys(blocks)) { + blockS := blocks[name] + // This shouldn't happen in real usage; state always has all values (set + // to null as needed), but it protects against panics in tests (and any + // really weird and unlikely cases). + if !stateVal.Type().HasAttribute(name) { + continue + } + blockVal := stateVal.GetAttr(name) + diags = diags.Append(writeConfigNestedBlockFromExisting(addr, buf, name, blockS, blockVal, indent)) + } + + return diags +} + +func writeConfigNestedTypeAttributeFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.Attribute, stateVal cty.Value, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + switch schema.NestedType.Nesting { + case configschema.NestingSingle: + if schema.Sensitive || stateVal.IsMarked() { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = {} # sensitive\n", name)) + return diags + } + + // This shouldn't happen in real usage; state always has all values (set + // to null as needed), but it protects against panics in tests (and any + // really weird and unlikely cases). + if !stateVal.Type().HasAttribute(name) { + return diags + } + nestedVal := stateVal.GetAttr(name) + + if nestedVal.IsNull() { + // There is a difference between a null object, and an object with + // no attributes. + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = null\n", name)) + return diags + } + + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = {\n", name)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, nestedVal, schema.NestedType.Attributes, indent+2)) + buf.WriteString("}\n") + return diags + + case configschema.NestingList, configschema.NestingSet: + + if schema.Sensitive || stateVal.IsMarked() { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = [] # sensitive\n", name)) + return diags + } + + listVals := ctyCollectionValues(stateVal.GetAttr(name)) + if listVals == nil { + // There is a difference between an empty list and a null list + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = null\n", name)) + return diags + } + + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = [\n", name)) + for i := range listVals { + buf.WriteString(strings.Repeat(" ", indent+2)) + + // The entire element is marked. + if listVals[i].IsMarked() { + buf.WriteString("{}, # sensitive\n") + continue + } + + buf.WriteString("{\n") + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.NestedType.Attributes, indent+4)) + buf.WriteString(strings.Repeat(" ", indent+2)) + buf.WriteString("},\n") + } + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("]\n") + return diags + + case configschema.NestingMap: + if schema.Sensitive || stateVal.IsMarked() { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = {} # sensitive\n", name)) + return diags + } + + attr := stateVal.GetAttr(name) + if attr.IsNull() { + // There is a difference between an empty map and a null map. + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = null\n", name)) + return diags + } + + vals := attr.AsValueMap() + + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s = {\n", name)) + for _, key := range slices.Sorted(maps.Keys(vals)) { + buf.WriteString(strings.Repeat(" ", indent+2)) + buf.WriteString(fmt.Sprintf("%s = {", hclEscapeString(key))) + + // This entire value is marked + if vals[key].IsMarked() { + buf.WriteString("} # sensitive\n") + continue + } + + buf.WriteString("\n") + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.NestedType.Attributes, indent+4)) + buf.WriteString(strings.Repeat(" ", indent+2)) + buf.WriteString("}\n") + } + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("}\n") + return diags + + default: + // This should not happen, the above should be exhaustive. + panic(fmt.Errorf("unsupported NestingMode %s", schema.NestedType.Nesting.String())) + } +} + +func writeConfigNestedBlockFromExisting(addr addrs.AbsResourceInstance, buf *strings.Builder, name string, schema *configschema.NestedBlock, stateVal cty.Value, indent int) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + switch schema.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + if stateVal.IsNull() { + return diags + } + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s {", name)) + + // If the entire value is marked, don't print any nested attributes + if stateVal.IsMarked() { + buf.WriteString("} # sensitive\n") + return diags + } + buf.WriteString("\n") + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, stateVal, schema.Attributes, indent+2)) + diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, stateVal, schema.BlockTypes, indent+2)) + buf.WriteString("}\n") + return diags + case configschema.NestingList, configschema.NestingSet: + if stateVal.IsMarked() { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name)) + return diags + } + listVals := ctyCollectionValues(stateVal) + for i := range listVals { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s {\n", name)) + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, listVals[i], schema.Attributes, indent+2)) + diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, listVals[i], schema.BlockTypes, indent+2)) + buf.WriteString("}\n") + } + return diags + case configschema.NestingMap: + // If the entire value is marked, don't print any nested attributes + if stateVal.IsMarked() { + buf.WriteString(fmt.Sprintf("%s {} # sensitive\n", name)) + return diags + } + + vals := stateVal.AsValueMap() + for _, key := range slices.Sorted(maps.Keys(vals)) { + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(fmt.Sprintf("%s %q {", name, key)) + // This entire map element is marked + if vals[key].IsMarked() { + buf.WriteString("} # sensitive\n") + return diags + } + buf.WriteString("\n") + diags = diags.Append(writeConfigAttributesFromExisting(addr, buf, vals[key], schema.Attributes, indent+2)) + diags = diags.Append(writeConfigBlocksFromExisting(addr, buf, vals[key], schema.BlockTypes, indent+2)) + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString("}\n") + } + return diags + default: + // This should not happen, the above should be exhaustive. + panic(fmt.Errorf("unsupported NestingMode %s", schema.Nesting.String())) + } +} + +func writeAttrTypeConstraint(buf *strings.Builder, schema *configschema.Attribute) { + if schema.Required { + buf.WriteString(" # REQUIRED ") + } else { + buf.WriteString(" # OPTIONAL ") + } + + if schema.NestedType != nil { + buf.WriteString(fmt.Sprintf("%s\n", schema.NestedType.ImpliedType().FriendlyName())) + } else { + buf.WriteString(fmt.Sprintf("%s\n", schema.Type.FriendlyName())) + } +} + +func writeBlockTypeConstraint(buf *strings.Builder, schema *configschema.NestedBlock) { + if schema.MinItems > 0 { + buf.WriteString(" # REQUIRED block\n") + } else { + buf.WriteString(" # OPTIONAL block\n") + } +} + +// copied from command/format/diff +func ctyCollectionValues(val cty.Value) []cty.Value { + if !val.IsKnown() || val.IsNull() { + return nil + } + + var len int + if val.IsMarked() { + val, _ = val.Unmark() + len = val.LengthInt() + } else { + len = val.LengthInt() + } + + ret := make([]cty.Value, 0, len) + for it := val.ElementIterator(); it.Next(); { + _, value := it.Element() + ret = append(ret, value) + } + + return ret +} + +// hclEscapeString formats the input string into a format that is safe for +// rendering within HCL. +// +// Note, this function doesn't actually do a very good job of this currently. We +// need to expose some internal functions from HCL in a future version and call +// them from here. For now, just use "%q" formatting. +// +// Note, the similar function in jsonformat/computed/renderers/map.go is doing +// something similar. +func hclEscapeString(str string) string { + // TODO: Replace this with more complete HCL logic instead of the simple + // go workaround. + if !hclsyntax.ValidIdentifier(str) { + return fmt.Sprintf("%q", str) + } + return str +} diff --git a/internal/genconfig/generate_config_test.go b/internal/genconfig/generate_config_test.go new file mode 100644 index 0000000000..87d3fb778f --- /dev/null +++ b/internal/genconfig/generate_config_test.go @@ -0,0 +1,848 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package genconfig + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" +) + +func TestConfigGeneration(t *testing.T) { + tcs := map[string]struct { + schema *configschema.Block + addr addrs.AbsResourceInstance + provider addrs.LocalProviderConfig + value cty.Value + expected string + }{ + "simple_resource": { + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "list_block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.NilVal, + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = null # OPTIONAL string + list_block { # OPTIONAL block + nested_value = null # OPTIONAL string + } +}`, + }, + "simple_resource_with_state": { + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "list_block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value": cty.StringVal("Hello, world!"), + "list_block": cty.ObjectVal(map[string]cty.Value{ + "nested_value": cty.StringVal("Hello, solar system!"), + }), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = "Hello, world!" + list_block { + nested_value = "Hello, solar system!" + } +}`, + }, + "simple_resource_with_partial_state": { + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "list_block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "list_block": cty.ObjectVal(map[string]cty.Value{ + "nested_value": cty.StringVal("Hello, solar system!"), + }), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = null + list_block { + nested_value = "Hello, solar system!" + } +}`, + }, + "simple_resource_with_alternate_provider": { + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "list_block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "mock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value": cty.StringVal("Hello, world!"), + "list_block": cty.ObjectVal(map[string]cty.Value{ + "nested_value": cty.StringVal("Hello, solar system!"), + }), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + provider = mock + value = "Hello, world!" + list_block { + nested_value = "Hello, solar system!" + } +}`, + }, + "simple_resource_with_aliased_provider": { + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "list_block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + Alias: "alternate", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value": cty.StringVal("Hello, world!"), + "list_block": cty.ObjectVal(map[string]cty.Value{ + "nested_value": cty.StringVal("Hello, solar system!"), + }), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + provider = tfcoremock.alternate + value = "Hello, world!" + list_block { + nested_value = "Hello, solar system!" + } +}`, + }, + "resource_with_nulls": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "single": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{}, + Nesting: configschema.NestingSingle, + }, + Required: true, + }, + "list": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingList, + }, + Required: true, + }, + "map": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingMap, + }, + Required: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "nested_single": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + // No configschema.NestingGroup example for this test, because this block type can never be null in state. + "nested_list": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + "nested_set": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + "nested_map": { + Nesting: configschema.NestingMap, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "single": cty.NullVal(cty.Object(map[string]cty.Type{})), + "list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + }))), + "map": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + }))), + "nested_single": cty.NullVal(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + })), + "nested_list": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + })), + "nested_set": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + })), + "nested_map": cty.MapValEmpty(cty.Object(map[string]cty.Type{ + "nested_id": cty.String, + })), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + list = null + map = null + single = null +}`, + }, + "simple_resource_with_stringified_json_object": { + schema: &configschema.Block{ + // BlockTypes: map[string]*configschema.NestedBlock{}, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value": cty.StringVal(`{ "0Hello": "World", "And": ["Solar", "System"], "ready": true }`), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = jsonencode({ + "0Hello" = "World" + And = ["Solar", "System"] + ready = true + }) +}`, + }, + "simple_resource_with_stringified_json_array": { + schema: &configschema.Block{ + // BlockTypes: map[string]*configschema.NestedBlock{}, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value": cty.StringVal(`["Hello", "World"]`), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = jsonencode(["Hello", "World"]) +}`, + }, + "simple_resource_with_json_primitive_strings": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value_string_number": { + Type: cty.String, + Optional: true, + }, + "value_string_bool": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value_string_number": cty.StringVal("42"), + "value_string_bool": cty.StringVal("true"), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value_string_bool = "true" + value_string_number = "42" +}`, + }, + "simple_resource_with_malformed_json": { + schema: &configschema.Block{ + // BlockTypes: map[string]*configschema.NestedBlock{}, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: nil, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_simple_resource", + Name: "empty", + }, + Key: nil, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("D2320658"), + "value": cty.StringVal(`["Hello", "World"`), + }), + expected: ` +resource "tfcoremock_simple_resource" "empty" { + value = "[\"Hello\", \"World\"" +}`, + }, + // Just try all the simple values with sensitive marks. + "sensitive_values": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "string": sensitiveAttribute(cty.String), + "empty_string": sensitiveAttribute(cty.String), + "number": sensitiveAttribute(cty.Number), + "bool": sensitiveAttribute(cty.Bool), + "object": sensitiveAttribute(cty.Object(map[string]cty.Type{ + "nested": cty.String, + })), + "list": sensitiveAttribute(cty.List(cty.String)), + "map": sensitiveAttribute(cty.Map(cty.String)), + "set": sensitiveAttribute(cty.Set(cty.String)), + }, + }, + addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "tfcoremock_sensitive_values", + Name: "values", + }, + Key: addrs.NoKey, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "tfcoremock", + }, + value: cty.ObjectVal(map[string]cty.Value{ + // Values that are sensitive will now be marked as such + "string": cty.StringVal("Hello, world!").Mark(marks.Sensitive), + "empty_string": cty.StringVal("").Mark(marks.Sensitive), + "number": cty.NumberIntVal(42).Mark(marks.Sensitive), + "bool": cty.True.Mark(marks.Sensitive), + "object": cty.ObjectVal(map[string]cty.Value{ + "nested": cty.StringVal("Hello, solar system!"), + }).Mark(marks.Sensitive), + "list": cty.ListVal([]cty.Value{ + cty.StringVal("Hello, world!"), + }).Mark(marks.Sensitive), + "map": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("Hello, world!"), + }).Mark(marks.Sensitive), + "set": cty.SetVal([]cty.Value{ + cty.StringVal("Hello, world!"), + }).Mark(marks.Sensitive), + }), + expected: ` +resource "tfcoremock_sensitive_values" "values" { + bool = null # sensitive + empty_string = null # sensitive + list = null # sensitive + map = null # sensitive + number = null # sensitive + object = null # sensitive + set = null # sensitive + string = null # sensitive +}`, + }, + "simple_map_with_whitespace_in_keys": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "map": { + Type: cty.Map(cty.String), + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "resource", + }, + Key: addrs.NoKey, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "testing", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "key with spaces": cty.StringVal("spaces"), + "key_with_underscores": cty.StringVal("underscores"), + }), + }), + expected: `resource "testing_resource" "resource" { + map = { + "key with spaces" = "spaces" + key_with_underscores = "underscores" + } +}`, + }, + "nested_map_with_whitespace_in_keys": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "map": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingMap, + }, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "resource", + }, + Key: addrs.NoKey, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "testing", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "key with spaces": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("spaces"), + }), + "key_with_underscores": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("underscores"), + }), + }), + }), + expected: `resource "testing_resource" "resource" { + map = { + "key with spaces" = { + value = "spaces" + } + key_with_underscores = { + value = "underscores" + } + } +}`, + }, + "simple_map_with_periods_in_keys": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "map": { + Type: cty.Map(cty.String), + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "resource", + }, + Key: addrs.NoKey, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "testing", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "key.with.periods": cty.StringVal("periods"), + "key_with_underscores": cty.StringVal("underscores"), + }), + }), + expected: `resource "testing_resource" "resource" { + map = { + "key.with.periods" = "periods" + key_with_underscores = "underscores" + } +}`, + }, + "nested_map_with_periods_in_keys": { + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "map": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + Nesting: configschema.NestingMap, + }, + Optional: true, + }, + }, + }, + addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "resource", + }, + Key: addrs.NoKey, + }, + }, + provider: addrs.LocalProviderConfig{ + LocalName: "testing", + }, + value: cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "key.with.periods": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("periods"), + }), + "key_with_underscores": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("underscores"), + }), + }), + }), + expected: `resource "testing_resource" "resource" { + map = { + "key.with.periods" = { + value = "periods" + } + key_with_underscores = { + value = "underscores" + } + } +}`, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + err := tc.schema.InternalValidate() + if err != nil { + t.Fatalf("schema failed InternalValidate: %s", err) + } + contents, diags := GenerateResourceContents(tc.addr, tc.schema, tc.provider, tc.value) + if len(diags) > 0 { + t.Errorf("expected no diagnostics but found %s", diags) + } + + got := WrapResourceContents(tc.addr, contents) + want := strings.TrimSpace(tc.expected) + if diff := cmp.Diff(got, want); len(diff) > 0 { + t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } + }) + } +} + +func sensitiveAttribute(t cty.Type) *configschema.Attribute { + return &configschema.Attribute{ + Type: t, + Optional: true, + Sensitive: true, + } +} diff --git a/internal/genconfig/generate_config_write.go b/internal/genconfig/generate_config_write.go new file mode 100644 index 0000000000..870086aa1f --- /dev/null +++ b/internal/genconfig/generate_config_write.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package genconfig + +import ( + "fmt" + "io" + "os" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func ShouldWriteConfig(out string) bool { + // No specified out file, so don't write anything. + return len(out) != 0 +} + +func ValidateTargetFile(out string) (diags tfdiags.Diagnostics) { + if _, err := os.Stat(out); !os.IsNotExist(err) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Target generated file already exists", + "Terraform can only write generated config into a new file. Either choose a different target location or move all existing configuration out of the target file, delete it and try again.")) + + } + return diags +} + +type Change struct { + Addr string + ImportID string + GeneratedConfig string +} + +func (c *Change) MaybeWriteConfig(writer io.Writer, out string) (io.Writer, bool, tfdiags.Diagnostics) { + var wroteConfig bool + var diags tfdiags.Diagnostics + if len(c.GeneratedConfig) > 0 { + if writer == nil { + // Lazily create the generated file, in case we have no + // generated config to create. + if w, err := os.Create(out); err != nil { + if os.IsPermission(err) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to create target generated file", + fmt.Sprintf("Terraform did not have permission to create the generated file (%s) in the target directory. Please modify permissions over the target directory, and try again.", out))) + return nil, false, diags + } + + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to create target generated file", + fmt.Sprintf("Terraform could not create the generated file (%s) in the target directory: %v. Depending on the error message, this may be a bug in Terraform itself. If so, please report it!", out, err))) + return nil, false, diags + } else { + writer = w + } + + header := "# __generated__ by Terraform\n# Please review these resources and move them into your main configuration files.\n" + // Missing the header from the file, isn't the end of the world + // so if this did return an error, then we will just ignore it. + _, _ = writer.Write([]byte(header)) + } + + header := "\n# __generated__ by Terraform" + if len(c.ImportID) > 0 { + header += fmt.Sprintf(" from %q", c.ImportID) + } + header += "\n" + if _, err := writer.Write([]byte(fmt.Sprintf("%s%s\n", header, c.GeneratedConfig))); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to save generated config", + fmt.Sprintf("Terraform encountered an error while writing generated config: %v. The config for %s must be created manually before applying. Depending on the error message, this may be a bug in Terraform itself. If so, please report it!", err, c.Addr))) + } + wroteConfig = true + } + + return writer, wroteConfig, diags +} diff --git a/internal/getmodules/doc.go b/internal/getmodules/doc.go index 9c72998429..dec6c5ace5 100644 --- a/internal/getmodules/doc.go +++ b/internal/getmodules/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package getmodules contains the low-level functionality for fetching // remote module packages. It's essentially just a thin wrapper around // go-getter. diff --git a/internal/getmodules/getter.go b/internal/getmodules/getter.go index 82ea599afc..98c3598c5c 100644 --- a/internal/getmodules/getter.go +++ b/internal/getmodules/getter.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getmodules import ( @@ -11,12 +14,19 @@ import ( "github.com/hashicorp/terraform/internal/copy" ) -// We configure our own go-getter detector and getter sets here, because -// the set of sources we support is part of Terraform's documentation and -// so we don't want any new sources introduced in go-getter to sneak in here -// and work even though they aren't documented. This also insulates us from -// any meddling that might be done by other go-getter callers linked into our -// executable. +// We configure our own go-getter getter set here, because the set of sources +// we support is part of Terraform's documentation and so we don't want any +// new sources introduced in go-getter to sneak in here and work even though +// they aren't documented. This also insulates us from any meddling that might +// be done by other go-getter callers linked into our executable. +// +// We don't use go-getter's own detectors because Terraform needs to be in +// control of its own source address syntax due to it being covered by our +// Terraform v1.x compatibility promises. However, we do still follow at least +// what go-getter would've done at some point in the past that now represent's +// Terraform's compatibility contract, and arrange for the result to be +// something that should be consumable just by the set of getters defined +// below. // // Note that over time we've found go-getter's design to be not wholly fit // for Terraform's purposes in various ways, and so we're continuing to use @@ -28,56 +38,44 @@ import ( // in this package which call into go-getter for more information on what // tradeoffs we're making here. -var goGetterDetectors = []getter.Detector{ - new(getter.GitHubDetector), - new(getter.GitDetector), - - // Because historically BitBucket supported both Git and Mercurial - // repositories but used the same repository URL syntax for both, - // this detector takes the unusual step of actually reaching out - // to the BitBucket API to recognize the repository type. That - // means there's the possibility of an outgoing network request - // inside what is otherwise normally just a local string manipulation - // operation, but we continue to accept this for now. - // - // Perhaps a future version of go-getter will remove the check now - // that BitBucket only supports Git anyway. Aside from this historical - // exception, we should avoid adding any new detectors that make network - // requests in here, and limit ourselves only to ones that can operate - // entirely through local string manipulation. - new(getter.BitBucketDetector), - - new(getter.GCSDetector), - new(getter.S3Detector), - new(fileDetector), -} - var goGetterNoDetectors = []getter.Detector{} var goGetterDecompressors = map[string]getter.Decompressor{ - "bz2": new(getter.Bzip2Decompressor), - "gz": new(getter.GzipDecompressor), - "xz": new(getter.XzDecompressor), - "zip": new(getter.ZipDecompressor), - + // Bzip2 + "bz2": new(getter.Bzip2Decompressor), + "tbz2": new(getter.TarBzip2Decompressor), "tar.bz2": new(getter.TarBzip2Decompressor), "tar.tbz2": new(getter.TarBzip2Decompressor), + // Gzip + "gz": new(getter.GzipDecompressor), "tar.gz": new(getter.TarGzipDecompressor), "tgz": new(getter.TarGzipDecompressor), + // Xz + "xz": new(getter.XzDecompressor), "tar.xz": new(getter.TarXzDecompressor), "txz": new(getter.TarXzDecompressor), + + // Zip + "zip": new(getter.ZipDecompressor), } var goGetterGetters = map[string]getter.Getter{ - "file": new(getter.FileGetter), - "gcs": new(getter.GCSGetter), - "git": new(getter.GitGetter), - "hg": new(getter.HgGetter), - "s3": new(getter.S3Getter), + // Protocol-based getters "http": getterHTTPGetter, "https": getterHTTPGetter, + + // Cloud storage getters + "gcs": new(getter.GCSGetter), + "s3": new(getter.S3Getter), + + // Version control getters + "git": new(getter.GitGetter), + "hg": new(getter.HgGetter), + + // Local and file-based getters + "file": new(getter.FileGetter), } var getterHTTPClient = cleanhttp.DefaultClient() diff --git a/internal/getmodules/installer.go b/internal/getmodules/installer.go index 657c0e73a0..20836409ea 100644 --- a/internal/getmodules/installer.go +++ b/internal/getmodules/installer.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getmodules import ( diff --git a/internal/getmodules/file_detector.go b/internal/getmodules/moduleaddrs/detect_abs_filepath.go similarity index 57% rename from internal/getmodules/file_detector.go rename to internal/getmodules/moduleaddrs/detect_abs_filepath.go index b28e2cdc97..6abf9c2e2f 100644 --- a/internal/getmodules/file_detector.go +++ b/internal/getmodules/moduleaddrs/detect_abs_filepath.go @@ -1,4 +1,7 @@ -package getmodules +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs import ( "fmt" @@ -6,19 +9,18 @@ import ( "runtime" ) -// fileDetector is a replacement for go-getter's own file detector which -// better meets Terraform's needs: specifically, it rejects relative filesystem -// paths with a somewhat-decent error message. +// detectAbsFilePath detects strings that seem like they are trying to be +// file paths. // -// This is a replacement for some historical hackery we did where we tried to -// avoid calling into go-getter altogether in this situation. This is, -// therefore, a copy of getter.FileDetector but with the "not absolute path" -// case replaced with a similar result as Terraform's old heuristic would've -// returned: a custom error type that the caller can react to in order to -// produce a hint error message if desired. -type fileDetector struct{} - -func (d *fileDetector) Detect(src, pwd string) (string, bool, error) { +// If the path is absolute then it's transformed into a file:// URL. If it's +// relative then we return an error of type *MaybeRelativePathErr, so that +// the caller can return a special error message to diagnose that the author +// should have written a local source address if they wanted to use a relative +// path. +// +// This should always be the last detector, because unless the input is an +// empty string this will always claim everything it's given. +func detectAbsFilePath(src string) (string, bool, error) { if len(src) == 0 { return "", false, nil } @@ -27,21 +29,17 @@ func (d *fileDetector) Detect(src, pwd string) (string, bool, error) { return "", true, &MaybeRelativePathErr{src} } - return fmtFileURL(src), true, nil -} - -func fmtFileURL(path string) string { if runtime.GOOS == "windows" { // Make sure we're using "/" on Windows. URLs are "/"-based. - path = filepath.ToSlash(path) - return fmt.Sprintf("file://%s", path) + src = filepath.ToSlash(src) + return fmt.Sprintf("file://%s", src), true, nil } // Make sure that we don't start with "/" since we add that below. - if path[0] == '/' { - path = path[1:] + if src[0] == '/' { + src = src[1:] } - return fmt.Sprintf("file:///%s", path) + return fmt.Sprintf("file:///%s", src), true, nil } // MaybeRelativePathErr is the error type returned by NormalizePackageAddress diff --git a/internal/getmodules/moduleaddrs/detect_gcs.go b/internal/getmodules/moduleaddrs/detect_gcs.go new file mode 100644 index 0000000000..a7c915d6f2 --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_gcs.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "fmt" + "net/url" + "strings" +) + +// detectGCS detects strings that seem like schemeless references to +// Google Cloud Storage and translates them into URLs for the "gcs" getter. +func detectGCS(src string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.Contains(src, "googleapis.com/") { + parts := strings.Split(src, "/") + if len(parts) < 5 { + return "", false, fmt.Errorf( + "URL is not a valid GCS URL") + } + version := parts[2] + bucket := parts[3] + object := strings.Join(parts[4:], "/") + + url, err := url.Parse(fmt.Sprintf("https://www.googleapis.com/storage/%s/%s/%s", + version, bucket, object)) + if err != nil { + return "", false, fmt.Errorf("error parsing GCS URL: %s", err) + } + + return "gcs::" + url.String(), true, nil + } + + return "", false, nil +} diff --git a/internal/getmodules/moduleaddrs/detect_gcs_test.go b/internal/getmodules/moduleaddrs/detect_gcs_test.go new file mode 100644 index 0000000000..90e173ff36 --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_gcs_test.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "testing" +) + +func TestDetectGCS(t *testing.T) { + tableTestDetectorFuncs(t, []struct { + Input string + Output string + }{ + { + "www.googleapis.com/storage/v1/bucket/foo", + "gcs::https://www.googleapis.com/storage/v1/bucket/foo", + }, + { + "www.googleapis.com/storage/v1/bucket/foo/bar", + "gcs::https://www.googleapis.com/storage/v1/bucket/foo/bar", + }, + { + "www.googleapis.com/storage/v1/foo/bar.baz", + "gcs::https://www.googleapis.com/storage/v1/foo/bar.baz", + }, + }) +} diff --git a/internal/getmodules/moduleaddrs/detect_git.go b/internal/getmodules/moduleaddrs/detect_git.go new file mode 100644 index 0000000000..21e9a6f7c1 --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_git.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +// detectGit translates Git SSH URLs into normal-shaped URLs. +func detectGit(src string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + u, err := detectSSH(src) + if err != nil { + return "", true, err + } + if u == nil { + return "", false, nil + } + + // We require the username to be "git" to assume that this is a Git URL + if u.User.Username() != "git" { + return "", false, nil + } + + return "git::" + u.String(), true, nil +} + +// detectGitHub detects shorthand schemeless references to github.com and +// translates them into git HTTP source addresses. +func detectGitHub(src string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.HasPrefix(src, "github.com/") { + src, rawQuery, _ := strings.Cut(src, "?") + + parts := strings.Split(src, "/") + if len(parts) < 3 { + return "", false, fmt.Errorf( + "GitHub URLs should be github.com/username/repo") + } + + urlStr := fmt.Sprintf("https://%s", strings.Join(parts[:3], "/")) + url, err := url.Parse(urlStr) + if err != nil { + return "", true, fmt.Errorf("error parsing GitHub URL: %s", err) + } + url.RawQuery = rawQuery + + if !strings.HasSuffix(url.Path, ".git") { + url.Path += ".git" + } + + if len(parts) > 3 { + url.Path += "//" + strings.Join(parts[3:], "/") + } + + return "git::" + url.String(), true, nil + } + + return "", false, nil +} + +// detectBitBucket detects shorthand schemeless references to bitbucket.org and +// translates them into git HTTP source addresses. +func detectBitBucket(src string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.HasPrefix(src, "bitbucket.org/") { + u, err := url.Parse("https://" + src) + if err != nil { + return "", true, fmt.Errorf("error parsing BitBucket URL: %s", err) + } + + // NOTE: A long, long time ago bitbucket.org repositories could + // potentially be either Git or Mercurial repositories and we would've + // needed to make an API call here to know which to generate. + // + // Thankfully BitBucket now only supports Git, and so we can just + // assume all bitbucket.org strings are trying to refer to Git + // repositories. + + if !strings.HasSuffix(u.Path, ".git") { + u.Path += ".git" + } + + return "git::" + u.String(), true, nil + } + + return "", false, nil +} + +// sshPattern matches SCP-like SSH patterns (user@host:path) +var sshPattern = regexp.MustCompile("^(?:([^@]+)@)?([^:]+):/?(.+)$") + +// detectSSH determines if the src string matches an SSH-like URL and +// converts it into a net.URL. This returns nil if the string doesn't match +// the SSH pattern. +func detectSSH(src string) (*url.URL, error) { + matched := sshPattern.FindStringSubmatch(src) + if matched == nil { + return nil, nil + } + + user := matched[1] + host := matched[2] + path := matched[3] + qidx := strings.Index(path, "?") + if qidx == -1 { + qidx = len(path) + } + + var u url.URL + u.Scheme = "ssh" + u.User = url.User(user) + u.Host = host + u.Path = path[0:qidx] + if qidx < len(path) { + q, err := url.ParseQuery(path[qidx+1:]) + if err != nil { + return nil, fmt.Errorf("error parsing Git SSH URL: %s", err) + } + u.RawQuery = q.Encode() + } + + return &u, nil +} diff --git a/internal/getmodules/moduleaddrs/detect_git_test.go b/internal/getmodules/moduleaddrs/detect_git_test.go new file mode 100644 index 0000000000..d78a3a0e23 --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_git_test.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "testing" +) + +func TestDetectGit(t *testing.T) { + tableTestDetectorFuncs(t, []struct { + Input string + Output string + }{ + { + "git@github.com:hashicorp/foo.git", + "git::ssh://git@github.com/hashicorp/foo.git", + }, + { + "git@github.com:org/project.git?ref=test-branch", + "git::ssh://git@github.com/org/project.git?ref=test-branch", + }, + { + "git@github.com:hashicorp/foo.git//bar", + "git::ssh://git@github.com/hashicorp/foo.git//bar", + }, + { + "git@github.com:hashicorp/foo.git?foo=bar", + "git::ssh://git@github.com/hashicorp/foo.git?foo=bar", + }, + { + "git@github.xyz.com:org/project.git", + "git::ssh://git@github.xyz.com/org/project.git", + }, + { + "git@github.xyz.com:org/project.git?ref=test-branch", + "git::ssh://git@github.xyz.com/org/project.git?ref=test-branch", + }, + { + "git@github.xyz.com:org/project.git//module/a", + "git::ssh://git@github.xyz.com/org/project.git//module/a", + }, + { + "git@github.xyz.com:org/project.git//module/a?ref=test-branch", + "git::ssh://git@github.xyz.com/org/project.git//module/a?ref=test-branch", + }, + { + // Already in the canonical form, so no rewriting required + // When the ssh: protocol is used explicitly, we recognize it as + // URL form rather than SCP-like form, so the part after the colon + // is a port number, not part of the path. + "git::ssh://git@git.example.com:2222/hashicorp/foo.git", + "git::ssh://git@git.example.com:2222/hashicorp/foo.git", + }, + }) +} + +func TestDetectGitHub(t *testing.T) { + tableTestDetectorFuncs(t, []struct { + Input string + Output string + }{ + {"github.com/hashicorp/foo", "git::https://github.com/hashicorp/foo.git"}, + {"github.com/hashicorp/foo.git", "git::https://github.com/hashicorp/foo.git"}, + { + "github.com/hashicorp/foo/bar", + "git::https://github.com/hashicorp/foo.git//bar", + }, + { + "github.com/hashicorp/foo?foo=bar", + "git::https://github.com/hashicorp/foo.git?foo=bar", + }, + { + "github.com/hashicorp/foo.git?foo=bar", + "git::https://github.com/hashicorp/foo.git?foo=bar", + }, + { + "github.com/hashicorp/foo.git?foo=bar/baz", + "git::https://github.com/hashicorp/foo.git?foo=bar/baz", + }, + }) +} + +func TestDetectBitBucket(t *testing.T) { + tableTestDetectorFuncs(t, []struct { + Input string + Output string + }{ + // HTTP + { + "bitbucket.org/hashicorp/tf-test-git", + "git::https://bitbucket.org/hashicorp/tf-test-git.git", + }, + { + "bitbucket.org/hashicorp/tf-test-git.git", + "git::https://bitbucket.org/hashicorp/tf-test-git.git", + }, + }) +} diff --git a/internal/getmodules/moduleaddrs/detect_remote_shorthands.go b/internal/getmodules/moduleaddrs/detect_remote_shorthands.go new file mode 100644 index 0000000000..7764df7e14 --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_remote_shorthands.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "fmt" + "net/url" + "path/filepath" + "regexp" +) + +var forcedRegexp = regexp.MustCompile(`^([A-Za-z0-9]+)::(.+)$`) + +// This is the minimal set of detectors we need for backward compatibility. +// +// Do not add any new detectors. All new source types should use the canonical +// source address syntax. +var detectors = []func(src string) (string, bool, error){ + detectGitHub, + detectGit, + detectBitBucket, + detectGCS, + detectS3, + detectAbsFilePath, +} + +// detectRemoteSourceShorthands recognizes several non-URL strings that +// Terraform historically accepted as shorthands for module source addresses, +// and converts them each into something more reasonable that specifies +// both a source type and a fully-qualified URL. +func detectRemoteSourceShorthands(src string) (string, error) { + getForce, getSrc := getForcedSourceType(src) + + // Separate out the subdir if there is one, we don't pass that to detect + getSrc, subDir := SplitPackageSubdir(getSrc) + + u, err := url.Parse(getSrc) + if err == nil && u.Scheme != "" { + // Valid URL + return src, nil + } + + for _, d := range detectors { + result, ok, err := d(getSrc) + if err != nil { + return "", err + } + if !ok { + continue + } + + var detectForce string + detectForce, result = getForcedSourceType(result) + result, detectSubdir := SplitPackageSubdir(result) + + // If we have a subdir from the detection, then prepend it to our + // requested subdir. + if detectSubdir != "" { + if subDir != "" { + subDir = filepath.Join(detectSubdir, subDir) + } else { + subDir = detectSubdir + } + } + + if subDir != "" { + u, err := url.Parse(result) + if err != nil { + return "", fmt.Errorf("Error parsing URL: %s", err) + } + u.Path += "//" + subDir + + // a subdir may contain wildcards, but in order to support them we + // have to ensure the path isn't escaped. + u.RawPath = u.Path + + result = u.String() + } + + // Preserve the forced getter if it exists. We try to use the + // original set force first, followed by any force set by the + // detector. + if getForce != "" { + result = fmt.Sprintf("%s::%s", getForce, result) + } else if detectForce != "" { + result = fmt.Sprintf("%s::%s", detectForce, result) + } + + return result, nil + } + + return "", fmt.Errorf("invalid source address: %s", src) +} + +func getForcedSourceType(src string) (string, string) { + var forced string + if ms := forcedRegexp.FindStringSubmatch(src); ms != nil { + forced = ms[1] + src = ms[2] + } + + return forced, src +} diff --git a/internal/getmodules/moduleaddrs/detect_remote_shorthands_test.go b/internal/getmodules/moduleaddrs/detect_remote_shorthands_test.go new file mode 100644 index 0000000000..2049a7c25c --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_remote_shorthands_test.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import "testing" + +// The actual tests for this live in the other detect_*_test.go files, but +// this file contains helpers that all of those tests share. + +func tableTestDetectorFuncs(t *testing.T, cases []struct{ Input, Output string }) { + t.Helper() + + for _, tc := range cases { + t.Run(tc.Input, func(t *testing.T) { + output, err := detectRemoteSourceShorthands(tc.Input) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if output != tc.Output { + t.Errorf("wrong result\ninput: %s\ngot: %s\nwant: %s", tc.Input, output, tc.Output) + } + }) + } +} diff --git a/internal/getmodules/moduleaddrs/detect_s3.go b/internal/getmodules/moduleaddrs/detect_s3.go new file mode 100644 index 0000000000..cd539aead9 --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_s3.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "fmt" + "net/url" + "strings" +) + +// detectS3 detects strings that seem like schemeless references to +// Amazon S3 and translates them into URLs for the "s3" getter. +func detectS3(src string) (string, bool, error) { + if len(src) == 0 { + return "", false, nil + } + + if strings.Contains(src, ".amazonaws.com/") { + parts := strings.Split(src, "/") + if len(parts) < 2 { + return "", false, fmt.Errorf( + "URL is not a valid S3 URL") + } + + hostParts := strings.Split(parts[0], ".") + if len(hostParts) == 3 { + return detectS3PathStyle(hostParts[0], parts[1:]) + } else if len(hostParts) == 4 { + return detectS3OldVhostStyle(hostParts[1], hostParts[0], parts[1:]) + } else if len(hostParts) == 5 && hostParts[1] == "s3" { + return detectS3NewVhostStyle(hostParts[2], hostParts[0], parts[1:]) + } else { + return "", false, fmt.Errorf( + "URL is not a valid S3 URL") + } + } + + return "", false, nil +} + +func detectS3PathStyle(region string, parts []string) (string, bool, error) { + urlStr := fmt.Sprintf("https://%s.amazonaws.com/%s", region, strings.Join(parts, "/")) + url, err := url.Parse(urlStr) + if err != nil { + return "", false, fmt.Errorf("error parsing S3 URL: %s", err) + } + + return "s3::" + url.String(), true, nil +} + +func detectS3OldVhostStyle(region, bucket string, parts []string) (string, bool, error) { + urlStr := fmt.Sprintf("https://%s.amazonaws.com/%s/%s", region, bucket, strings.Join(parts, "/")) + url, err := url.Parse(urlStr) + if err != nil { + return "", false, fmt.Errorf("error parsing S3 URL: %s", err) + } + + return "s3::" + url.String(), true, nil +} + +func detectS3NewVhostStyle(region, bucket string, parts []string) (string, bool, error) { + urlStr := fmt.Sprintf("https://s3.%s.amazonaws.com/%s/%s", region, bucket, strings.Join(parts, "/")) + url, err := url.Parse(urlStr) + if err != nil { + return "", false, fmt.Errorf("error parsing S3 URL: %s", err) + } + + return "s3::" + url.String(), true, nil +} diff --git a/internal/getmodules/moduleaddrs/detect_s3_test.go b/internal/getmodules/moduleaddrs/detect_s3_test.go new file mode 100644 index 0000000000..a41be74cba --- /dev/null +++ b/internal/getmodules/moduleaddrs/detect_s3_test.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "testing" +) + +func TestDetectS3(t *testing.T) { + tableTestDetectorFuncs(t, []struct { + Input string + Output string + }{ + // Virtual hosted style + { + "bucket.s3.amazonaws.com/foo", + "s3::https://s3.amazonaws.com/bucket/foo", + }, + { + "bucket.s3.amazonaws.com/foo/bar", + "s3::https://s3.amazonaws.com/bucket/foo/bar", + }, + { + "bucket.s3.amazonaws.com/foo/bar.baz", + "s3::https://s3.amazonaws.com/bucket/foo/bar.baz", + }, + { + "bucket.s3-eu-west-1.amazonaws.com/foo", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo", + }, + { + "bucket.s3-eu-west-1.amazonaws.com/foo/bar", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo/bar", + }, + { + "bucket.s3-eu-west-1.amazonaws.com/foo/bar.baz", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo/bar.baz", + }, + // 5 parts Virtual hosted-style + { + "bucket.s3.eu-west-1.amazonaws.com/foo/bar.baz", + "s3::https://s3.eu-west-1.amazonaws.com/bucket/foo/bar.baz", + }, + // Path style + { + "s3.amazonaws.com/bucket/foo", + "s3::https://s3.amazonaws.com/bucket/foo", + }, + { + "s3.amazonaws.com/bucket/foo/bar", + "s3::https://s3.amazonaws.com/bucket/foo/bar", + }, + { + "s3.amazonaws.com/bucket/foo/bar.baz", + "s3::https://s3.amazonaws.com/bucket/foo/bar.baz", + }, + { + "s3-eu-west-1.amazonaws.com/bucket/foo", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo", + }, + { + "s3-eu-west-1.amazonaws.com/bucket/foo/bar", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo/bar", + }, + { + "s3-eu-west-1.amazonaws.com/bucket/foo/bar.baz", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo/bar.baz", + }, + // Misc tests + { + "s3-eu-west-1.amazonaws.com/bucket/foo/bar.baz?version=1234", + "s3::https://s3-eu-west-1.amazonaws.com/bucket/foo/bar.baz?version=1234", + }, + }) +} diff --git a/internal/getmodules/package.go b/internal/getmodules/moduleaddrs/package.go similarity index 53% rename from internal/getmodules/package.go rename to internal/getmodules/moduleaddrs/package.go index a46065623a..ae6dcec018 100644 --- a/internal/getmodules/package.go +++ b/internal/getmodules/moduleaddrs/package.go @@ -1,39 +1,29 @@ -package getmodules +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 -import ( - getter "github.com/hashicorp/go-getter" -) +package moduleaddrs -// NormalizePackageAddress uses the go-getter "detector" functionality in -// order to turn a user-supplied source address into a normalized address -// which always includes a prefix naming a protocol to fetch with and may -// also include a transformed/normalized version of the protocol-specific -// source address included afterward. +// NormalizePackageAddress turns a user-supplied source address into a +// normalized address which always includes a prefix naming a protocol to fetch +// with and may also include a transformed/normalized version of the +// protocol-specific source address included afterward. // -// This is part of the implementation of addrs.ParseModulePackage and of -// addrs.ParseModuleSource, so for most callers it'd be better to call +// This is part of the implementation of [ParseModulePackage] and of +// [ParseModuleSource], so for most callers it'd be better to call // one of those other functions instead. The addrs package can potentially // perform other processing in addition to just the go-getter detection. // // Note that this function expects to recieve only a package address, not // a full source address that might also include a subdirectory portion. // The caller must trim off any subdirectory portion using -// getmodules.SplitPackageSubdir before calling this function, passing in +// [SplitPackageSubdir] before calling this function, passing in // just the packageAddr return value, or the result will be incorrect. // -// The detectors in go-getter can potentially introduce their own -// package subdirectory portions. If that happens then this function will +// The normalization process can potentially introduce its own package +// subdirectory portions. If that happens then this function will // return the subdirectory portion as a non-empty subDir return value, // which the caller must then use as a prefix for any subDir it already // extracted from the user's given package address. -// -// Some of go-getter's detectors make outgoing HTTP requests, and so -// the behavior of this function may depend on the network connectivity -// of the system where Terraform is running. However, most of the getters -// we use are local-only, and so HTTP requests are only for some ambiguous -// edge-cases, such as the BitBucket detector which has a mechanism to -// detect whether to use Git or Mercurial, because earlier versions of -// BitBucket used to support both. func NormalizePackageAddress(given string) (packageAddr, subDir string, err error) { // Because we're passing go-getter no base directory here, the file // detector will return an error if the user entered a relative filesystem @@ -54,13 +44,8 @@ func NormalizePackageAddress(given string) (packageAddr, subDir string, err erro // reasons, and we treat them as remote packages even though "downloading" // them just means a recursive copy of the source directory tree.) - result, err := getter.Detect(given, "", goGetterDetectors) + result, err := detectRemoteSourceShorthands(given) if err != nil { - // NOTE: go-getter's error messages are of very inconsistent quality - // and many are not suitable for an end-user audience, but they are all - // just strings and so we can't really do any sort of post-processing - // to improve them and thus we just accept some bad error messages for - // now. return "", "", err } diff --git a/internal/getmodules/moduleaddrs/source_parsing.go b/internal/getmodules/moduleaddrs/source_parsing.go new file mode 100644 index 0000000000..0289c969a9 --- /dev/null +++ b/internal/getmodules/moduleaddrs/source_parsing.go @@ -0,0 +1,217 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs + +import ( + "fmt" + "path" + "strings" + + tfaddr "github.com/hashicorp/terraform-registry-address" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// We have some of the module address parsers in here, rather than in +// package addrs, because right now our remote source address normalization +// is inextricably tied to the external go-getter library, which means any +// package that calls these functions must indirectly depend on go-getter. +// +// Package addrs is imported from almost everywhere, so any dependency it +// has becomes an indirect dependency of everything else. Only a few callers +// actually need to parse module source addresses, so it's pragmatic to have +// just those callers import this package, whereas packages that only need +// to work with addresses that were already parsed -- or don't need to interact +// with module source addresses _at all_ -- can avoid indirectly depending +// on go-getter and all of its various third-party dependencies. + +var moduleSourceLocalPrefixes = []string{ + "./", + "../", + ".\\", + "..\\", +} + +// ParseModuleSource parses a module source address as given in the "source" +// argument inside a "module" block in the configuration. +// +// For historical reasons this syntax is a bit overloaded, supporting three +// different address types: +// - Local paths starting with either ./ or ../, which are special because +// Terraform considers them to belong to the same "package" as the caller. +// - Module registry addresses, given as either NAMESPACE/NAME/SYSTEM or +// HOST/NAMESPACE/NAME/SYSTEM, in which case the remote registry serves +// as an indirection over the third address type that follows. +// - Various URL-like and other heuristically-recognized strings which +// we currently delegate to the external library go-getter. +// +// There is some ambiguity between the module registry addresses and go-getter's +// very liberal heuristics and so this particular function will typically treat +// an invalid registry address as some other sort of remote source address +// rather than returning an error. If you know that you're expecting a +// registry address in particular, use [ParseModuleSourceRegistry] instead, which +// can therefore expose more detailed error messages about registry address +// parsing in particular. +func ParseModuleSource(raw string) (addrs.ModuleSource, error) { + if isModuleSourceLocal(raw) { + localAddr, err := parseModuleSourceLocal(raw) + if err != nil { + // This is to make sure we really return a nil ModuleSource in + // this case, rather than an interface containing the zero + // value of ModuleSourceLocal. + return nil, err + } + return localAddr, nil + } + + // For historical reasons, whether an address is a registry + // address is defined only by whether it can be successfully + // parsed as one, and anything else must fall through to be + // parsed as a direct remote source, where go-getter might + // then recognize it as a filesystem path. This is odd + // but matches behavior we've had since Terraform v0.10 which + // existing modules may be relying on. + // (Notice that this means that there's never any path where + // the registry source parse error gets returned to the caller, + // which is annoying but has been true for many releases + // without it posing a serious problem in practice.) + if ret, err := ParseModuleSourceRegistry(raw); err == nil { + return ret, nil + } + + // If we get down here then we treat everything else as a + // remote address. In practice there's very little that + // go-getter doesn't consider invalid input, so even invalid + // nonsense will probably interpreted as _something_ here + // and then fail during installation instead. We can't + // really improve this situation for historical reasons. + remoteAddr, err := parseModuleSourceRemote(raw) + if err != nil { + // This is to make sure we really return a nil ModuleSource in + // this case, rather than an interface containing the zero + // value of ModuleSourceRemote. + return nil, err + } + return remoteAddr, nil +} + +func parseModuleSourceLocal(raw string) (addrs.ModuleSourceLocal, error) { + // As long as we have a suitable prefix (detected by ParseModuleSource) + // there is no failure case for local paths: we just use the "path" + // package's cleaning logic to remove any redundant "./" and "../" + // sequences and any duplicate slashes and accept whatever that + // produces. + + // Although using backslashes (Windows-style) is non-idiomatic, we do + // allow it and just normalize it away, so the rest of Terraform will + // only see the forward-slash form. + if strings.Contains(raw, `\`) { + // Note: We use string replacement rather than filepath.ToSlash + // here because the filepath package behavior varies by current + // platform, but we want to interpret configured paths the same + // across all platforms: these are virtual paths within a module + // package, not physical filesystem paths. + raw = strings.ReplaceAll(raw, `\`, "/") + } + + // Note that we could've historically blocked using "//" in a path here + // in order to avoid confusion with the subdir syntax in remote addresses, + // but we historically just treated that as the same as a single slash + // and so we continue to do that now for compatibility. Clean strips those + // out and reduces them to just a single slash. + clean := path.Clean(raw) + + // However, we do need to keep a single "./" on the front if it isn't + // a "../" path, or else it would be ambigous with the registry address + // syntax. + if !strings.HasPrefix(clean, "../") { + clean = "./" + clean + } + + return addrs.ModuleSourceLocal(clean), nil +} + +func isModuleSourceLocal(raw string) bool { + for _, prefix := range moduleSourceLocalPrefixes { + if strings.HasPrefix(raw, prefix) { + return true + } + } + return false +} + +// ParseModuleSourceRegistry is a variant of ParseModuleSource which only +// accepts module registry addresses, and will reject any other address type. +// +// Use this instead of ParseModuleSource if you know from some other surrounding +// context that an address is intended to be a registry address rather than +// some other address type, which will then allow for better error reporting +// due to the additional information about user intent. +func ParseModuleSourceRegistry(raw string) (addrs.ModuleSource, error) { + // Before we delegate to the "real" function we'll just make sure this + // doesn't look like a local source address, so we can return a better + // error message for that situation. + if isModuleSourceLocal(raw) { + return addrs.ModuleSourceRegistry{}, fmt.Errorf("can't use local directory %q as a module registry address", raw) + } + + src, err := tfaddr.ParseModuleSource(raw) + if err != nil { + return nil, err + } + return addrs.ModuleSourceRegistry{ + Package: src.Package, + Subdir: src.Subdir, + }, nil +} + +func parseModuleSourceRemote(raw string) (addrs.ModuleSourceRemote, error) { + var subDir string + raw, subDir = SplitPackageSubdir(raw) + if strings.HasPrefix(subDir, "../") { + return addrs.ModuleSourceRemote{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir) + } + + // A remote source address is really just a go-getter address resulting + // from go-getter's "detect" phase, which adds on the prefix specifying + // which protocol it should use and possibly also adjusts the + // protocol-specific part into different syntax. + // + // Note that for historical reasons this can potentially do network + // requests in order to disambiguate certain address types, although + // that's a legacy thing that is only for some specific, less-commonly-used + // address types. Most just do local string manipulation. We should + // aim to remove the network requests over time, if possible. + norm, moreSubDir, err := NormalizePackageAddress(raw) + if err != nil { + // We must pass through the returned error directly here because + // the getmodules package has some special error types it uses + // for certain cases where the UI layer might want to include a + // more helpful error message. + return addrs.ModuleSourceRemote{}, err + } + + if moreSubDir != "" { + switch { + case subDir != "": + // The detector's own subdir goes first, because the + // subdir we were given is conceptually relative to + // the subdirectory that we just detected. + subDir = path.Join(moreSubDir, subDir) + default: + subDir = path.Clean(moreSubDir) + } + if strings.HasPrefix(subDir, "../") { + // This would suggest a bug in a go-getter detector, but + // we'll catch it anyway to avoid doing something confusing + // downstream. + return addrs.ModuleSourceRemote{}, fmt.Errorf("detected subdirectory path %q of %q leads outside of the module package", subDir, norm) + } + } + + return addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage(norm), + Subdir: subDir, + }, nil +} diff --git a/internal/addrs/module_source_test.go b/internal/getmodules/moduleaddrs/source_parsing_test.go similarity index 72% rename from internal/addrs/module_source_test.go rename to internal/getmodules/moduleaddrs/source_parsing_test.go index d6b5626ec6..fae56599d1 100644 --- a/internal/addrs/module_source_test.go +++ b/internal/getmodules/moduleaddrs/source_parsing_test.go @@ -1,65 +1,70 @@ -package addrs +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs import ( "testing" "github.com/google/go-cmp/cmp" svchost "github.com/hashicorp/terraform-svchost" + + "github.com/hashicorp/terraform/internal/addrs" ) func TestParseModuleSource(t *testing.T) { tests := map[string]struct { input string - want ModuleSource + want addrs.ModuleSource wantErr string }{ // Local paths "local in subdirectory": { input: "./child", - want: ModuleSourceLocal("./child"), + want: addrs.ModuleSourceLocal("./child"), }, "local in subdirectory non-normalized": { input: "./nope/../child", - want: ModuleSourceLocal("./child"), + want: addrs.ModuleSourceLocal("./child"), }, "local in sibling directory": { input: "../sibling", - want: ModuleSourceLocal("../sibling"), + want: addrs.ModuleSourceLocal("../sibling"), }, "local in sibling directory non-normalized": { input: "./nope/../../sibling", - want: ModuleSourceLocal("../sibling"), + want: addrs.ModuleSourceLocal("../sibling"), }, "Windows-style local in subdirectory": { input: `.\child`, - want: ModuleSourceLocal("./child"), + want: addrs.ModuleSourceLocal("./child"), }, "Windows-style local in subdirectory non-normalized": { input: `.\nope\..\child`, - want: ModuleSourceLocal("./child"), + want: addrs.ModuleSourceLocal("./child"), }, "Windows-style local in sibling directory": { input: `..\sibling`, - want: ModuleSourceLocal("../sibling"), + want: addrs.ModuleSourceLocal("../sibling"), }, "Windows-style local in sibling directory non-normalized": { input: `.\nope\..\..\sibling`, - want: ModuleSourceLocal("../sibling"), + want: addrs.ModuleSourceLocal("../sibling"), }, "an abominable mix of different slashes": { input: `./nope\nope/why\./please\don't`, - want: ModuleSourceLocal("./nope/nope/why/please/don't"), + want: addrs.ModuleSourceLocal("./nope/nope/why/please/don't"), }, // Registry addresses - // (NOTE: There is another test function TestParseModuleSourceRegistry + // (NOTE: There is another test function TestParseaddrs.ModuleSourceRegistry // which tests this situation more exhaustively, so this is just a // token set of cases to see that we are indeed calling into the // registry address parser when appropriate.) "main registry implied": { input: "hashicorp/subnets/cidr", - want: ModuleSourceRegistry{ - Package: ModuleRegistryPackage{ + want: addrs.ModuleSourceRegistry{ + Package: addrs.ModuleRegistryPackage{ Host: svchost.Hostname("registry.terraform.io"), Namespace: "hashicorp", Name: "subnets", @@ -70,8 +75,8 @@ func TestParseModuleSource(t *testing.T) { }, "main registry implied, subdir": { input: "hashicorp/subnets/cidr//examples/foo", - want: ModuleSourceRegistry{ - Package: ModuleRegistryPackage{ + want: addrs.ModuleSourceRegistry{ + Package: addrs.ModuleRegistryPackage{ Host: svchost.Hostname("registry.terraform.io"), Namespace: "hashicorp", Name: "subnets", @@ -91,8 +96,8 @@ func TestParseModuleSource(t *testing.T) { }, "custom registry": { input: "example.com/awesomecorp/network/happycloud", - want: ModuleSourceRegistry{ - Package: ModuleRegistryPackage{ + want: addrs.ModuleSourceRegistry{ + Package: addrs.ModuleRegistryPackage{ Host: svchost.Hostname("example.com"), Namespace: "awesomecorp", Name: "network", @@ -103,8 +108,8 @@ func TestParseModuleSource(t *testing.T) { }, "custom registry, subdir": { input: "example.com/awesomecorp/network/happycloud//examples/foo", - want: ModuleSourceRegistry{ - Package: ModuleRegistryPackage{ + want: addrs.ModuleSourceRegistry{ + Package: addrs.ModuleRegistryPackage{ Host: svchost.Hostname("example.com"), Namespace: "awesomecorp", Name: "network", @@ -117,68 +122,75 @@ func TestParseModuleSource(t *testing.T) { // Remote package addresses "github.com shorthand": { input: "github.com/hashicorp/terraform-cidr-subnets", - want: ModuleSourceRemote{ - Package: ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), }, }, "github.com shorthand, subdir": { input: "github.com/hashicorp/terraform-cidr-subnets//example/foo", - want: ModuleSourceRemote{ - Package: ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), Subdir: "example/foo", }, }, "git protocol, URL-style": { input: "git://example.com/code/baz.git", - want: ModuleSourceRemote{ - Package: ModulePackage("git://example.com/code/baz.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git://example.com/code/baz.git"), }, }, "git protocol, URL-style, subdir": { input: "git://example.com/code/baz.git//bleep/bloop", - want: ModuleSourceRemote{ - Package: ModulePackage("git://example.com/code/baz.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git://example.com/code/baz.git"), Subdir: "bleep/bloop", }, }, "git over HTTPS, URL-style": { input: "git::https://example.com/code/baz.git", - want: ModuleSourceRemote{ - Package: ModulePackage("git::https://example.com/code/baz.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::https://example.com/code/baz.git"), }, }, "git over HTTPS, URL-style, subdir": { input: "git::https://example.com/code/baz.git//bleep/bloop", - want: ModuleSourceRemote{ - Package: ModulePackage("git::https://example.com/code/baz.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::https://example.com/code/baz.git"), + Subdir: "bleep/bloop", + }, + }, + "git over HTTPS, URL-style, subdir, query parameters": { + input: "git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah", + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::https://example.com/code/baz.git?otherthing=blah"), Subdir: "bleep/bloop", }, }, "git over SSH, URL-style": { input: "git::ssh://git@example.com/code/baz.git", - want: ModuleSourceRemote{ - Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), }, }, "git over SSH, URL-style, subdir": { input: "git::ssh://git@example.com/code/baz.git//bleep/bloop", - want: ModuleSourceRemote{ - Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), Subdir: "bleep/bloop", }, }, "git over SSH, scp-style": { input: "git::git@example.com:code/baz.git", - want: ModuleSourceRemote{ + want: addrs.ModuleSourceRemote{ // Normalized to URL-style - Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), + Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), }, }, "git over SSH, scp-style, subdir": { input: "git::git@example.com:code/baz.git//bleep/bloop", - want: ModuleSourceRemote{ + want: addrs.ModuleSourceRemote{ // Normalized to URL-style - Package: ModulePackage("git::ssh://git@example.com/code/baz.git"), + Package: addrs.ModulePackage("git::ssh://git@example.com/code/baz.git"), Subdir: "bleep/bloop", }, }, @@ -189,73 +201,73 @@ func TestParseModuleSource(t *testing.T) { "Google Cloud Storage bucket implied, path prefix": { input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", - want: ModuleSourceRemote{ - Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), }, }, "Google Cloud Storage bucket, path prefix": { input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", - want: ModuleSourceRemote{ - Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), }, }, "Google Cloud Storage bucket implied, archive object": { input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", - want: ModuleSourceRemote{ - Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), }, }, "Google Cloud Storage bucket, archive object": { input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", - want: ModuleSourceRemote{ - Package: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), }, }, "Amazon S3 bucket implied, archive object": { input: "s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", - want: ModuleSourceRemote{ - Package: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), }, }, "Amazon S3 bucket, archive object": { input: "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", - want: ModuleSourceRemote{ - Package: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), }, }, "HTTP URL": { input: "http://example.com/module", - want: ModuleSourceRemote{ - Package: ModulePackage("http://example.com/module"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("http://example.com/module"), }, }, "HTTPS URL": { input: "https://example.com/module", - want: ModuleSourceRemote{ - Package: ModulePackage("https://example.com/module"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("https://example.com/module"), }, }, "HTTPS URL, archive file": { input: "https://example.com/module.zip", - want: ModuleSourceRemote{ - Package: ModulePackage("https://example.com/module.zip"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("https://example.com/module.zip"), }, }, "HTTPS URL, forced archive file": { input: "https://example.com/module?archive=tar", - want: ModuleSourceRemote{ - Package: ModulePackage("https://example.com/module?archive=tar"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("https://example.com/module?archive=tar"), }, }, "HTTPS URL, forced archive file and checksum": { input: "https://example.com/module?archive=tar&checksum=blah", - want: ModuleSourceRemote{ + want: addrs.ModuleSourceRemote{ // The query string only actually gets processed when we finally // do the get, so "checksum=blah" is accepted as valid up // at this parsing layer. - Package: ModulePackage("https://example.com/module?archive=tar&checksum=blah"), + Package: addrs.ModulePackage("https://example.com/module?archive=tar&checksum=blah"), }, }, @@ -265,8 +277,8 @@ func TestParseModuleSource(t *testing.T) { // high-level steps to work with these, even though "downloading" // is replaced by a deep filesystem copy instead. input: "/tmp/foo/example", - want: ModuleSourceRemote{ - Package: ModulePackage("file:///tmp/foo/example"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("file:///tmp/foo/example"), }, }, "absolute filesystem path, subdir": { @@ -276,8 +288,8 @@ func TestParseModuleSource(t *testing.T) { // of that subtree, and so they can use the usual subdir // syntax to move the package root higher in the real filesystem. input: "/tmp/foo//example", - want: ModuleSourceRemote{ - Package: ModulePackage("file:///tmp/foo"), + want: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("file:///tmp/foo"), Subdir: "example", }, }, @@ -306,11 +318,11 @@ func TestParseModuleSource(t *testing.T) { "go-getter will accept all sorts of garbage": { input: "dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg", - want: ModuleSourceRemote{ + want: addrs.ModuleSourceRemote{ // Unfortunately go-getter doesn't actually reject a totally // invalid address like this until getting time, as long as // it looks somewhat like a URL. - Package: ModulePackage("dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg"), + Package: addrs.ModulePackage("dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg"), }, }, } @@ -343,11 +355,11 @@ func TestParseModuleSource(t *testing.T) { func TestModuleSourceRemoteFromRegistry(t *testing.T) { t.Run("both have subdir", func(t *testing.T) { - remote := ModuleSourceRemote{ - Package: ModulePackage("boop"), + remote := addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("boop"), Subdir: "foo", } - registry := ModuleSourceRegistry{ + registry := addrs.ModuleSourceRegistry{ Subdir: "bar", } gotAddr := remote.FromRegistry(registry) @@ -362,11 +374,11 @@ func TestModuleSourceRemoteFromRegistry(t *testing.T) { } }) t.Run("only remote has subdir", func(t *testing.T) { - remote := ModuleSourceRemote{ - Package: ModulePackage("boop"), + remote := addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("boop"), Subdir: "foo", } - registry := ModuleSourceRegistry{ + registry := addrs.ModuleSourceRegistry{ Subdir: "", } gotAddr := remote.FromRegistry(registry) @@ -381,11 +393,11 @@ func TestModuleSourceRemoteFromRegistry(t *testing.T) { } }) t.Run("only registry has subdir", func(t *testing.T) { - remote := ModuleSourceRemote{ - Package: ModulePackage("boop"), + remote := addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("boop"), Subdir: "", } - registry := ModuleSourceRegistry{ + registry := addrs.ModuleSourceRegistry{ Subdir: "bar", } gotAddr := remote.FromRegistry(registry) @@ -401,16 +413,66 @@ func TestModuleSourceRemoteFromRegistry(t *testing.T) { }) } +func TestParseModuleSourceRemote(t *testing.T) { + + tests := map[string]struct { + input string + wantString string + wantForDisplay string + wantErr string + }{ + "git over HTTPS, URL-style, query parameters": { + // Query parameters should be correctly appended after the Package + input: `git::https://example.com/code/baz.git?otherthing=blah`, + wantString: `git::https://example.com/code/baz.git?otherthing=blah`, + wantForDisplay: `git::https://example.com/code/baz.git?otherthing=blah`, + }, + "git over HTTPS, URL-style, subdir, query parameters": { + // Query parameters should be correctly appended after the Package and Subdir + input: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, + wantString: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, + wantForDisplay: `git::https://example.com/code/baz.git//bleep/bloop?otherthing=blah`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + remote, err := parseModuleSourceRemote(test.input) + + if test.wantErr != "" { + switch { + case err == nil: + t.Errorf("unexpected success\nwant error: %s", test.wantErr) + case err.Error() != test.wantErr: + t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + if got, want := remote.String(), test.wantString; got != want { + t.Errorf("wrong String() result\ngot: %s\nwant: %s", got, want) + } + if got, want := remote.ForDisplay(), test.wantForDisplay; got != want { + t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) + } + }) + } +} + func TestParseModuleSourceRegistry(t *testing.T) { - // We test parseModuleSourceRegistry alone here, in addition to testing - // it indirectly as part of TestParseModuleSource, because general + // We test ParseModuleSourceRegistry alone here, in addition to testing + // it indirectly as part of TestParseaddrs.ModuleSource, because general // module parsing unfortunately eats all of the error situations from // registry passing by falling back to trying for a direct remote package // address. // Historical note: These test cases were originally derived from the // ones in the old internal/registry/regsrc package that the - // ModuleSourceRegistry type is replacing. That package had the notion + // addrs.ModuleSourceRegistry type is replacing. That package had the notion // of "normalized" addresses as separate from the original user input, // but this new implementation doesn't try to preserve the original // user input at all, and so the main string output is always normalized. @@ -554,7 +616,7 @@ func TestParseModuleSourceRegistry(t *testing.T) { t.Fatalf("unexpected error: %s", err.Error()) } - addr, ok := addrI.(ModuleSourceRegistry) + addr, ok := addrI.(addrs.ModuleSourceRegistry) if !ok { t.Fatalf("wrong address type %T; want %T", addrI, addr) } diff --git a/internal/getmodules/subdir.go b/internal/getmodules/moduleaddrs/subdir.go similarity index 56% rename from internal/getmodules/subdir.go rename to internal/getmodules/moduleaddrs/subdir.go index 38d398f7b0..2139ca354e 100644 --- a/internal/getmodules/subdir.go +++ b/internal/getmodules/moduleaddrs/subdir.go @@ -1,9 +1,13 @@ -package getmodules +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleaddrs import ( + "fmt" "path" - - getter "github.com/hashicorp/go-getter" + "path/filepath" + "strings" ) // SplitPackageSubdir detects whether the given address string has a @@ -20,24 +24,48 @@ import ( // remote module package address and thus can contribute its own // additions to the final subdirectory selection. func SplitPackageSubdir(given string) (packageAddr, subDir string) { - // We delegate this mostly to go-getter, because older Terraform - // versions just used go-getter directly and so we need to preserve - // its various quirks for compatibility reasons. - // - // However, note that in Terraform we _always_ split off the subdirectory - // portion and handle it within Terraform-level code, _never_ passing - // a subdirectory portion down into go-getter's own Get function, because - // Terraform's ability to refer between local paths inside the same - // package depends on Terraform itself always being aware of where the - // package's root directory ended up on disk, and always needs the - // package installed wholesale. - packageAddr, subDir = getter.SourceDirSubdir(given) + packageAddr, subDir = splitPackageSubdirRaw(given) if subDir != "" { subDir = path.Clean(subDir) } return packageAddr, subDir } +func splitPackageSubdirRaw(src string) (packageAddr, subDir string) { + // URL might contains another url in query parameters + stop := len(src) + if idx := strings.Index(src, "?"); idx > -1 { + stop = idx + } + + // Calculate an offset to avoid accidentally marking the scheme + // as the dir. + var offset int + if idx := strings.Index(src[:stop], "://"); idx > -1 { + offset = idx + 3 + } + + // First see if we even have an explicit subdir + idx := strings.Index(src[offset:stop], "//") + if idx == -1 { + return src, "" + } + + idx += offset + subdir := src[idx+2:] + src = src[:idx] + + // Next, check if we have query parameters and push them onto the + // URL. + if idx = strings.Index(subdir, "?"); idx > -1 { + query := subdir[idx:] + subdir = subdir[:idx] + src += query + } + + return src, subdir +} + // ExpandSubdirGlobs handles a subdir string that might contain glob syntax, // turning it into a concrete subdirectory path by referring to the actual // files on disk in the given directory which we assume contains the content @@ -50,8 +78,20 @@ func SplitPackageSubdir(given string) (packageAddr, subDir string) { // will then expand into the single subdirectory found inside instDir, or // return an error if the result would be ambiguous. func ExpandSubdirGlobs(instDir string, subDir string) (string, error) { - // We just delegate this entirely to go-getter, because older Terraform - // versions just used go-getter directly and so we need to preserve - // its various quirks for compatibility reasons. - return getter.SubdirGlob(instDir, subDir) + pattern := filepath.Join(instDir, subDir) + + matches, err := filepath.Glob(pattern) + if err != nil { + return "", err + } + + if len(matches) == 0 { + return "", fmt.Errorf("subdir %q not found", subDir) + } + + if len(matches) > 1 { + return "", fmt.Errorf("subdir %q matches multiple paths", subDir) + } + + return matches[0], nil } diff --git a/internal/getproviders/didyoumean.go b/internal/getproviders/didyoumean.go index e31ba20e41..e6f2b76727 100644 --- a/internal/getproviders/didyoumean.go +++ b/internal/getproviders/didyoumean.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/didyoumean_test.go b/internal/getproviders/didyoumean_test.go index 18804a9a6f..fbff1ff06a 100644 --- a/internal/getproviders/didyoumean_test.go +++ b/internal/getproviders/didyoumean_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/doc.go b/internal/getproviders/doc.go index a39aa1dda0..1478249db4 100644 --- a/internal/getproviders/doc.go +++ b/internal/getproviders/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package getproviders is the lowest-level provider automatic installation // functionality. It can answer questions about what providers and provider // versions are available in a registry, and it can retrieve the URL for diff --git a/internal/getproviders/errors.go b/internal/getproviders/errors.go index 7d2720c0f8..c0f81a27df 100644 --- a/internal/getproviders/errors.go +++ b/internal/getproviders/errors.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/filesystem_mirror_source.go b/internal/getproviders/filesystem_mirror_source.go index 118aff208f..48da19b70d 100644 --- a/internal/getproviders/filesystem_mirror_source.go +++ b/internal/getproviders/filesystem_mirror_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/filesystem_mirror_source_test.go b/internal/getproviders/filesystem_mirror_source_test.go index f498b81cc4..0a36b4a404 100644 --- a/internal/getproviders/filesystem_mirror_source_test.go +++ b/internal/getproviders/filesystem_mirror_source_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/filesystem_search.go b/internal/getproviders/filesystem_search.go index 2ae727293f..f9a07f92ab 100644 --- a/internal/getproviders/filesystem_search.go +++ b/internal/getproviders/filesystem_search.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/filesystem_search_test.go b/internal/getproviders/filesystem_search_test.go index 37ced6ff59..1452524799 100644 --- a/internal/getproviders/filesystem_search_test.go +++ b/internal/getproviders/filesystem_search_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/hanging_source.go b/internal/getproviders/hanging_source.go index 388b617013..7fac4d542b 100644 --- a/internal/getproviders/hanging_source.go +++ b/internal/getproviders/hanging_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/hash.go b/internal/getproviders/hash.go index 2a8adfced4..e18fe21f10 100644 --- a/internal/getproviders/hash.go +++ b/internal/getproviders/hash.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( @@ -6,8 +9,8 @@ import ( "io" "os" "path/filepath" - "strings" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "golang.org/x/mod/sumdb/dirhash" ) @@ -25,11 +28,11 @@ import ( // conversion. Instead, use either the HashScheme.New method on one of the // HashScheme contents (for a hash of a particular scheme) or the ParseHash // function (if hashes of any scheme are acceptable). -type Hash string +type Hash = providerreqs.Hash // NilHash is the zero value of Hash. It isn't a valid hash, so all of its // methods will panic. -const NilHash = Hash("") +const NilHash = providerreqs.NilHash // ParseHash parses the string representation of a Hash into a Hash value. // @@ -46,89 +49,25 @@ const NilHash = Hash("") // If this function returns an error then the returned Hash is invalid and // must not be used. func ParseHash(s string) (Hash, error) { - colon := strings.Index(s, ":") - if colon < 1 { // 1 because a zero-length scheme is not allowed - return NilHash, fmt.Errorf("hash string must start with a scheme keyword followed by a colon") - } - return Hash(s), nil + return providerreqs.ParseHash(s) } // MustParseHash is a wrapper around ParseHash that panics if it returns an // error. func MustParseHash(s string) Hash { - hash, err := ParseHash(s) - if err != nil { - panic(err.Error()) - } - return hash -} - -// Scheme returns the scheme of the recieving hash. If the receiver is not -// using valid syntax then this method will panic. -func (h Hash) Scheme() HashScheme { - colon := strings.Index(string(h), ":") - if colon < 0 { - panic(fmt.Sprintf("invalid hash string %q", h)) - } - return HashScheme(h[:colon+1]) -} - -// HasScheme returns true if the given scheme matches the receiver's scheme, -// or false otherwise. -// -// If the receiver is not using valid syntax then this method will panic. -func (h Hash) HasScheme(want HashScheme) bool { - return h.Scheme() == want -} - -// Value returns the scheme-specific value from the recieving hash. The -// meaning of this value depends on the scheme. -// -// If the receiver is not using valid syntax then this method will panic. -func (h Hash) Value() string { - colon := strings.Index(string(h), ":") - if colon < 0 { - panic(fmt.Sprintf("invalid hash string %q", h)) - } - return string(h[colon+1:]) -} - -// String returns a string representation of the receiving hash. -func (h Hash) String() string { - return string(h) -} - -// GoString returns a Go syntax representation of the receiving hash. -// -// This is here primarily to help with producing descriptive test failure -// output; these results are not particularly useful at runtime. -func (h Hash) GoString() string { - if h == NilHash { - return "getproviders.NilHash" - } - switch scheme := h.Scheme(); scheme { - case HashScheme1: - return fmt.Sprintf("getproviders.HashScheme1.New(%q)", h.Value()) - case HashSchemeZip: - return fmt.Sprintf("getproviders.HashSchemeZip.New(%q)", h.Value()) - default: - // This fallback is for when we encounter lock files or API responses - // with hash schemes that the current version of Terraform isn't - // familiar with. They were presumably introduced in a later version. - return fmt.Sprintf("getproviders.HashScheme(%q).New(%q)", scheme, h.Value()) - } + return providerreqs.MustParseHash(s) } // HashScheme is an enumeration of schemes that are allowed for values of type // Hash. -type HashScheme string +type HashScheme = providerreqs.HashScheme const ( // HashScheme1 is the scheme identifier for the first hash scheme. // // Use HashV1 (or one of its wrapper functions) to calculate hashes with // this scheme. - HashScheme1 HashScheme = HashScheme("h1:") + HashScheme1 HashScheme = providerreqs.HashScheme1 // HashSchemeZip is the scheme identifier for the legacy hash scheme that // applies to distribution archives (.zip files) rather than package @@ -136,18 +75,9 @@ const ( // distribution .zip file, not an extracted directory. // // Use PackageHashLegacyZipSHA to calculate hashes with this scheme. - HashSchemeZip HashScheme = HashScheme("zh:") + HashSchemeZip HashScheme = providerreqs.HashSchemeZip ) -// New creates a new Hash value with the receiver as its scheme and the given -// raw string as its value. -// -// It's the caller's responsibility to make sure that the given value makes -// sense for the selected scheme. -func (hs HashScheme) New(value string) Hash { - return Hash(string(hs) + value) -} - // PackageHash computes a hash of the contents of the package at the given // location, using whichever hash algorithm is the current default. // @@ -160,7 +90,7 @@ func (hs HashScheme) New(value string) Hash { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageHash(loc PackageLocation) (Hash, error) { +func PackageHash(loc PackageLocation) (providerreqs.Hash, error) { return PackageHashV1(loc) } @@ -178,7 +108,7 @@ func PackageHash(loc PackageLocation) (Hash, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageMatchesHash(loc PackageLocation, want Hash) (bool, error) { +func PackageMatchesHash(loc PackageLocation, want providerreqs.Hash) (bool, error) { switch want.Scheme() { case HashScheme1: got, err := PackageHashV1(loc) @@ -213,17 +143,17 @@ func PackageMatchesHash(loc PackageLocation, want Hash) (bool, error) { // types PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageMatchesAnyHash(loc PackageLocation, allowed []Hash) (bool, error) { +func PackageMatchesAnyHash(loc PackageLocation, allowed []providerreqs.Hash) (bool, error) { // It's likely that we'll have multiple hashes of the same scheme in // the "allowed" set, in which case we'll avoid repeatedly re-reading the // given package by caching its result for each of the two // currently-supported hash formats. These will be NilHash until we // encounter the first hash of the corresponding scheme. - var v1Hash, zipHash Hash + var v1Hash, zipHash providerreqs.Hash for _, want := range allowed { switch want.Scheme() { - case HashScheme1: - if v1Hash == NilHash { + case providerreqs.HashScheme1: + if v1Hash == providerreqs.NilHash { got, err := PackageHashV1(loc) if err != nil { return false, err @@ -233,13 +163,13 @@ func PackageMatchesAnyHash(loc PackageLocation, allowed []Hash) (bool, error) { if v1Hash == want { return true, nil } - case HashSchemeZip: + case providerreqs.HashSchemeZip: archiveLoc, ok := loc.(PackageLocalArchive) if !ok { // A zip hash can never match an unpacked directory continue } - if zipHash == NilHash { + if zipHash == providerreqs.NilHash { got, err := PackageHashLegacyZipSHA(archiveLoc) if err != nil { return false, err @@ -265,24 +195,8 @@ func PackageMatchesAnyHash(loc PackageLocation, allowed []Hash) (bool, error) { // format. If PreferredHash returns a non-empty string then it will be one // of the hash strings in "given", and that hash is the one that must pass // verification in order for a package to be considered valid. -func PreferredHashes(given []Hash) []Hash { - // For now this is just filtering for the two hash formats we support, - // both of which are considered equally "preferred". If we introduce - // a new scheme like "h2:" in future then, depending on the characteristics - // of that new version, it might make sense to rework this function so - // that it only returns "h1:" hashes if the input has no "h2:" hashes, - // so that h2: is preferred when possible and h1: is only a fallback for - // interacting with older systems that haven't been updated with the new - // scheme yet. - - var ret []Hash - for _, hash := range given { - switch hash.Scheme() { - case HashScheme1, HashSchemeZip: - ret = append(ret, hash) - } - } - return ret +func PreferredHashes(given []providerreqs.Hash) []providerreqs.Hash { + return providerreqs.PreferredHashes(given) } // PackageHashLegacyZipSHA implements the old provider package hashing scheme @@ -297,7 +211,7 @@ func PreferredHashes(given []Hash) []Hash { // // Because this hashing scheme uses the official provider .zip file as its // input, it accepts only PackageLocalArchive locations. -func PackageHashLegacyZipSHA(loc PackageLocalArchive) (Hash, error) { +func PackageHashLegacyZipSHA(loc PackageLocalArchive) (providerreqs.Hash, error) { archivePath, err := filepath.EvalSymlinks(string(loc)) if err != nil { return "", err @@ -324,8 +238,8 @@ func PackageHashLegacyZipSHA(loc PackageLocalArchive) (Hash, error) { // // This just adds the "zh:" prefix and encodes the string in hex, so that the // result is in the same format as PackageHashLegacyZipSHA. -func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) Hash { - return HashSchemeZip.New(fmt.Sprintf("%x", sum[:])) +func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) providerreqs.Hash { + return providerreqs.HashSchemeZip.New(fmt.Sprintf("%x", sum[:])) } // PackageHashV1 computes a hash of the contents of the package at the given @@ -346,7 +260,7 @@ func HashLegacyZipSHAFromSHA(sum [sha256.Size]byte) Hash { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func PackageHashV1(loc PackageLocation) (Hash, error) { +func PackageHashV1(loc PackageLocation) (providerreqs.Hash, error) { // Our HashV1 is really just the Go Modules hash version 1, which is // sufficient for our needs and already well-used for identity of // Go Modules distribution packages. It is also blocked from incompatible @@ -407,7 +321,7 @@ func PackageHashV1(loc PackageLocation) (Hash, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func (m PackageMeta) Hash() (Hash, error) { +func (m PackageMeta) Hash() (providerreqs.Hash, error) { return PackageHash(m.Location) } @@ -421,7 +335,7 @@ func (m PackageMeta) Hash() (Hash, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func (m PackageMeta) MatchesHash(want Hash) (bool, error) { +func (m PackageMeta) MatchesHash(want providerreqs.Hash) (bool, error) { return PackageMatchesHash(m.Location, want) } @@ -431,7 +345,7 @@ func (m PackageMeta) MatchesHash(want Hash) (bool, error) { // If it cannot read from the given location, MatchesHash returns an error. // Unlike the signular MatchesHash, MatchesAnyHash considers an unsupported // hash format to be a successful non-match. -func (m PackageMeta) MatchesAnyHash(acceptable []Hash) (bool, error) { +func (m PackageMeta) MatchesAnyHash(acceptable []providerreqs.Hash) (bool, error) { return PackageMatchesAnyHash(m.Location, acceptable) } @@ -446,6 +360,6 @@ func (m PackageMeta) MatchesAnyHash(acceptable []Hash) (bool, error) { // PackageLocalDir and PackageLocalArchive, because it needs to access the // contents of the indicated package in order to compute the hash. If given // a non-local location this function will always return an error. -func (m PackageMeta) HashV1() (Hash, error) { +func (m PackageMeta) HashV1() (providerreqs.Hash, error) { return PackageHashV1(m.Location) } diff --git a/internal/getproviders/http_mirror_source.go b/internal/getproviders/http_mirror_source.go index 82f890a763..6e838aee35 100644 --- a/internal/getproviders/http_mirror_source.go +++ b/internal/getproviders/http_mirror_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/http_mirror_source_test.go b/internal/getproviders/http_mirror_source_test.go index 3bf8a004aa..32ec618c60 100644 --- a/internal/getproviders/http_mirror_source_test.go +++ b/internal/getproviders/http_mirror_source_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/memoize_source.go b/internal/getproviders/memoize_source.go index 2930d5a18d..bbd400ce87 100644 --- a/internal/getproviders/memoize_source.go +++ b/internal/getproviders/memoize_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/memoize_source_test.go b/internal/getproviders/memoize_source_test.go index 006602b345..30489eca65 100644 --- a/internal/getproviders/memoize_source_test.go +++ b/internal/getproviders/memoize_source_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/multi_source.go b/internal/getproviders/multi_source.go index bcec76e8ff..287622f1de 100644 --- a/internal/getproviders/multi_source.go +++ b/internal/getproviders/multi_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/multi_source_test.go b/internal/getproviders/multi_source_test.go index f78fb519c5..1311b83742 100644 --- a/internal/getproviders/multi_source_test.go +++ b/internal/getproviders/multi_source_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/package_authentication.go b/internal/getproviders/package_authentication.go index 9f135ec11f..c12251869d 100644 --- a/internal/getproviders/package_authentication.go +++ b/internal/getproviders/package_authentication.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( @@ -6,15 +9,14 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "io" "log" "strings" - // TODO: replace crypto/openpgp since it is deprecated - // https://github.com/golang/go/issues/44226 - //lint:file-ignore SA1019 openpgp is deprecated but there are no good alternatives yet - "golang.org/x/crypto/openpgp" - openpgpArmor "golang.org/x/crypto/openpgp/armor" - openpgpErrors "golang.org/x/crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp" + openpgpArmor "github.com/ProtonMail/go-crypto/openpgp/armor" + openpgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors" + "github.com/ProtonMail/go-crypto/openpgp/packet" ) type packageAuthenticationResult int @@ -412,7 +414,7 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( if err != nil { return nil, fmt.Errorf("error creating HashiCorp keyring: %s", err) } - _, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) + _, err = s.checkDetachedSignature(hashicorpKeyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature), nil) if err == nil { return &PackageAuthenticationResult{result: officialProvider, KeyID: keyID}, nil } @@ -435,7 +437,7 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( return nil, fmt.Errorf("error decoding trust signature: %s", err) } - _, err = openpgp.CheckDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body) + _, err = s.checkDetachedSignature(hashicorpPartnersKeyring, authorKey.Body, trustSignature.Body, nil) if err != nil { return nil, fmt.Errorf("error verifying trust signature: %s", err) } @@ -448,6 +450,27 @@ func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) ( return &PackageAuthenticationResult{result: communityProvider, KeyID: keyID}, nil } +func (s signatureAuthentication) checkDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader, config *packet.Config) (*openpgp.Entity, error) { + entity, err := openpgp.CheckDetachedSignature(keyring, signed, signature, config) + // FIXME: it's not clear what should be done with provider signing key + // expiration. This check reverts the validation behavior to match that of + // the original x/crypto/openpgp package. + // + // We don't force providers to update keys for older releases, so they may + // have since expired. We are validating the original signature however, + // which was vouched for by the registry. The openpgp code always checks + // signature details last, so we know if we have ErrKeyExpired all other + // validation already passed. This is also checked in findSigningKey while + // iterating over the possible signers. + if err == openpgpErrors.ErrKeyExpired { + for id := range entity.Identities { + log.Printf("[WARN] expired openpgp key from %s\n", id) + } + err = nil + } + return entity, err +} + func (s signatureAuthentication) AcceptableHashes() []Hash { // This is a bit of an abstraction leak because signatureAuthentication // otherwise just treats the document as an opaque blob that's been @@ -506,9 +529,9 @@ func (s signatureAuthentication) findSigningKey() (*SigningKey, string, error) { return nil, "", fmt.Errorf("error decoding signing key: %s", err) } - entity, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature)) + entity, err := s.checkDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature), nil) - // If the signature issuer does not match the the key, keep trying the + // If the signature issuer does not match the key, keep trying the // rest of the provided keys. if err == openpgpErrors.ErrUnknownIssuer { continue diff --git a/internal/getproviders/package_authentication_test.go b/internal/getproviders/package_authentication_test.go index 45052b75d2..59bcef4453 100644 --- a/internal/getproviders/package_authentication_test.go +++ b/internal/getproviders/package_authentication_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( @@ -5,17 +8,19 @@ import ( "encoding/base64" "errors" "fmt" + "os" "strings" "testing" "github.com/google/go-cmp/cmp" - // TODO: replace crypto/openpgp since it is deprecated - // https://github.com/golang/go/issues/44226 - //lint:file-ignore SA1019 openpgp is deprecated but there are no good alternatives yet - "golang.org/x/crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp" ) +func TestMain(m *testing.M) { + os.Exit(m.Run()) +} + func TestPackageAuthenticationResult(t *testing.T) { tests := []struct { result *PackageAuthenticationResult @@ -490,7 +495,7 @@ func TestSignatureAuthentication_failure(t *testing.T) { TrustSignature: testOtherKeyTrustSignatureArmor, }, }, - "error verifying trust signature: openpgp: invalid signature: hash tag doesn't match", + "error verifying trust signature: openpgp: invalid signature: RSA verification failure", }, } @@ -579,6 +584,22 @@ G7Zdrci1KEd943HhzDCsUFz4gJwbvUyiAYb2ddndpUBkYwCB/XrHWPOSnGxHgZoo =mYqJ -----END PGP PUBLIC KEY BLOCK-----` +// testAuthorEccKeyArmor uses Curve 25519 and has test key ID D01ED5C4BB1ED36A014B0D376540DDA046E5E135 +const testAuthorEccKeyArmor = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEY1B7+hYJKwYBBAHaRw8BAQdAFRDpASP+iDY+QotOBP9DF5CfuhSBD8Dl0hSG +D7plEsO0M1RlcnJhZm9ybSBUZXN0aW5nIDx0ZXJyYWZvcm0rdGVzdGluZ0BoYXNo +aWNvcnAuY29tPoiTBBMWCgA7FiEE0B7VxLse02oBSw03ZUDdoEbl4TUFAmNQe/oC +GwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcCF4AACgkQZUDdoEbl4TWhwwD+N/BR +pR9NhRFDm+JRhA3saKmpTSRo9yJnr6tRlumE4KQA/A2cOCDeezf6t3SXltoYUKIt +EYmbLxgMDlffVkFyC8IMuDgEY1B7+hIKKwYBBAGXVQEFAQEHQJ7frE76Le1qI1Go +dfrVIzEgAcYjDW6T01/V95wgqPIuAwEIB4h4BBgWCgAgFiEE0B7VxLse02oBSw03 +ZUDdoEbl4TUFAmNQe/oCGwwACgkQZUDdoEbl4TWvsAD/YSQAigAH5hq4OmK4gs0J +O74RFokGZzbPtoIvutb8eYoA/1QxxyqE/8A4Z21azYEO0j563LRa8SkZcB5UPDy3 +7ngJ +=Xb0o +-----END PGP PUBLIC KEY BLOCK-----` + // testAuthorKeyTrustSignatureArmor is a trust signature of the data in // testAuthorKeyArmor signed with HashicorpPartnersKey. const testAuthorKeyTrustSignatureArmor = `-----BEGIN PGP SIGNATURE----- @@ -698,6 +719,11 @@ func TestEntityString(t *testing.T) { nil, "", }, + { + "testAuthorEccKeyArmor", + testReadArmoredEntity(t, testAuthorEccKeyArmor), + "6540DDA046E5E135 Terraform Testing ", + }, { "testAuthorKeyArmor", testReadArmoredEntity(t, testAuthorKeyArmor), diff --git a/internal/getproviders/providerreqs/hash.go b/internal/getproviders/providerreqs/hash.go new file mode 100644 index 0000000000..41d6c36478 --- /dev/null +++ b/internal/getproviders/providerreqs/hash.go @@ -0,0 +1,174 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providerreqs + +import ( + "fmt" + "strings" +) + +// Hash is a specially-formatted string representing a checksum of a package +// or the contents of the package. +// +// A Hash string is always starts with a scheme, which is a short series of +// alphanumeric characters followed by a colon, and then the remainder of the +// string has a different meaning depending on the scheme prefix. +// +// The currently-valid schemes are defined as the constants of type HashScheme +// in this package. +// +// Callers outside of this package must not create Hash values via direct +// conversion. Instead, use either the HashScheme.New method on one of the +// HashScheme contents (for a hash of a particular scheme) or the ParseHash +// function (if hashes of any scheme are acceptable). +type Hash string + +// NilHash is the zero value of Hash. It isn't a valid hash, so all of its +// methods will panic. +const NilHash = Hash("") + +// ParseHash parses the string representation of a Hash into a Hash value. +// +// A particular version of Terraform only supports a fixed set of hash schemes, +// but this function intentionally allows unrecognized schemes so that we can +// silently ignore other schemes that may be introduced in the future. For +// that reason, the Scheme method of the returned Hash may return a value that +// isn't in one of the HashScheme constants in this package. +// +// This function doesn't verify that the value portion of the given hash makes +// sense for the given scheme. Invalid values are just considered to not match +// any packages. +// +// If this function returns an error then the returned Hash is invalid and +// must not be used. +func ParseHash(s string) (Hash, error) { + colon := strings.Index(s, ":") + if colon < 1 { // 1 because a zero-length scheme is not allowed + return NilHash, fmt.Errorf("hash string must start with a scheme keyword followed by a colon") + } + return Hash(s), nil +} + +// MustParseHash is a wrapper around ParseHash that panics if it returns an +// error. +func MustParseHash(s string) Hash { + hash, err := ParseHash(s) + if err != nil { + panic(err.Error()) + } + return hash +} + +// Scheme returns the scheme of the recieving hash. If the receiver is not +// using valid syntax then this method will panic. +func (h Hash) Scheme() HashScheme { + colon := strings.Index(string(h), ":") + if colon < 0 { + panic(fmt.Sprintf("invalid hash string %q", h)) + } + return HashScheme(h[:colon+1]) +} + +// HasScheme returns true if the given scheme matches the receiver's scheme, +// or false otherwise. +// +// If the receiver is not using valid syntax then this method will panic. +func (h Hash) HasScheme(want HashScheme) bool { + return h.Scheme() == want +} + +// Value returns the scheme-specific value from the recieving hash. The +// meaning of this value depends on the scheme. +// +// If the receiver is not using valid syntax then this method will panic. +func (h Hash) Value() string { + colon := strings.Index(string(h), ":") + if colon < 0 { + panic(fmt.Sprintf("invalid hash string %q", h)) + } + return string(h[colon+1:]) +} + +// String returns a string representation of the receiving hash. +func (h Hash) String() string { + return string(h) +} + +// GoString returns a Go syntax representation of the receiving hash. +// +// This is here primarily to help with producing descriptive test failure +// output; these results are not particularly useful at runtime. +func (h Hash) GoString() string { + if h == NilHash { + return "getproviders.NilHash" + } + switch scheme := h.Scheme(); scheme { + case HashScheme1: + return fmt.Sprintf("getproviders.HashScheme1.New(%q)", h.Value()) + case HashSchemeZip: + return fmt.Sprintf("getproviders.HashSchemeZip.New(%q)", h.Value()) + default: + // This fallback is for when we encounter lock files or API responses + // with hash schemes that the current version of Terraform isn't + // familiar with. They were presumably introduced in a later version. + return fmt.Sprintf("getproviders.HashScheme(%q).New(%q)", scheme, h.Value()) + } +} + +// HashScheme is an enumeration of schemes that are allowed for values of type +// Hash. +type HashScheme string + +const ( + // HashScheme1 is the scheme identifier for the first hash scheme. + // + // Use HashV1 (or one of its wrapper functions) to calculate hashes with + // this scheme. + HashScheme1 HashScheme = HashScheme("h1:") + + // HashSchemeZip is the scheme identifier for the legacy hash scheme that + // applies to distribution archives (.zip files) rather than package + // contents, and can therefore only be verified against the original + // distribution .zip file, not an extracted directory. + // + // Use PackageHashLegacyZipSHA to calculate hashes with this scheme. + HashSchemeZip HashScheme = HashScheme("zh:") +) + +// New creates a new Hash value with the receiver as its scheme and the given +// raw string as its value. +// +// It's the caller's responsibility to make sure that the given value makes +// sense for the selected scheme. +func (hs HashScheme) New(value string) Hash { + return Hash(string(hs) + value) +} + +// PreferredHashes examines all of the given hash strings and returns the one +// that the current version of Terraform considers to provide the strongest +// verification. +// +// Returns an empty string if none of the given hashes are of a supported +// format. If PreferredHash returns a non-empty string then it will be one +// of the hash strings in "given", and that hash is the one that must pass +// verification in order for a package to be considered valid. +func PreferredHashes(given []Hash) []Hash { + // For now this is just filtering for the two hash formats we support, + // both of which are considered equally "preferred". If we introduce + // a new scheme like "h2:" in future then, depending on the characteristics + // of that new version, it might make sense to rework this function so + // that it only returns "h1:" hashes if the input has no "h2:" hashes, + // so that h2: is preferred when possible and h1: is only a fallback for + // interacting with older systems that haven't been updated with the new + // scheme yet. + + var ret []Hash + for _, hash := range given { + switch hash.Scheme() { + case HashScheme1, HashSchemeZip: + ret = append(ret, hash) + } + } + return ret +} diff --git a/internal/getproviders/hash_test.go b/internal/getproviders/providerreqs/hash_test.go similarity index 94% rename from internal/getproviders/hash_test.go rename to internal/getproviders/providerreqs/hash_test.go index de7873498a..9c7d4cddbe 100644 --- a/internal/getproviders/hash_test.go +++ b/internal/getproviders/providerreqs/hash_test.go @@ -1,4 +1,7 @@ -package getproviders +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providerreqs import ( "testing" diff --git a/internal/getproviders/providerreqs/version.go b/internal/getproviders/providerreqs/version.go new file mode 100644 index 0000000000..579eceb9ac --- /dev/null +++ b/internal/getproviders/providerreqs/version.go @@ -0,0 +1,298 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package providerreqs contains types we use to talk about provider +// requirements. +// +// This is separated from the parent directory package getproviders because +// lots of Terraform packages need to talk about provider requirements but +// very few actually need to perform provider plugin installation, and so +// this separate package avoids the need for every package that talks about +// provider requirements to also indirectly depend on all of the external +// modules used for provider installation. +package providerreqs + +import ( + "fmt" + "sort" + "strings" + + "github.com/apparentlymart/go-versions/versions" + "github.com/apparentlymart/go-versions/versions/constraints" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// Version represents a particular single version of a provider. +type Version = versions.Version + +// UnspecifiedVersion is the zero value of Version, representing the absense +// of a version number. +var UnspecifiedVersion Version = versions.Unspecified + +// VersionList represents a list of versions. It is a []Version with some +// extra methods for convenient filtering. +type VersionList = versions.List + +// VersionSet represents a set of versions, usually describing the acceptable +// versions that can be selected under a particular version constraint provided +// by the end-user. +type VersionSet = versions.Set + +// VersionConstraints represents a set of version constraints, which can +// define the membership of a VersionSet by exclusion. +type VersionConstraints = constraints.IntersectionSpec + +// Warnings represents a list of warnings returned by a Registry source. +type Warnings = []string + +// Requirements gathers together requirements for many different providers +// into a single data structure, as a convenient way to represent the full +// set of requirements for a particular configuration or state or both. +// +// If an entry in a Requirements has a zero-length VersionConstraints then +// that indicates that the provider is required but that any version is +// acceptable. That's different than a provider being absent from the map +// altogether, which means that it is not required at all. +type Requirements map[addrs.Provider]VersionConstraints + +// Merge takes the requirements in the receiever and the requirements in the +// other given value and produces a new set of requirements that combines +// all of the requirements of both. +// +// The resulting requirements will permit only selections that both of the +// source requirements would've allowed. +func (r Requirements) Merge(other Requirements) Requirements { + ret := make(Requirements) + for addr, constraints := range r { + ret[addr] = constraints + } + for addr, constraints := range other { + ret[addr] = append(ret[addr], constraints...) + } + return ret +} + +// Selections gathers together version selections for many different providers. +// +// This is the result of provider installation: a specific version selected +// for each provider given in the requested Requirements, selected based on +// the given version constraints. +type Selections map[addrs.Provider]Version + +// ParseVersion parses a "semver"-style version string into a Version value, +// which is the version syntax we use for provider versions. +func ParseVersion(str string) (Version, error) { + return versions.ParseVersion(str) +} + +// MustParseVersion is a variant of ParseVersion that panics if it encounters +// an error while parsing. +func MustParseVersion(str string) Version { + ret, err := ParseVersion(str) + if err != nil { + panic(err) + } + return ret +} + +// ParseVersionConstraints parses a "Ruby-like" version constraint string +// into a VersionConstraints value. +func ParseVersionConstraints(str string) (VersionConstraints, error) { + return constraints.ParseRubyStyleMulti(str) +} + +// MustParseVersionConstraints is a variant of ParseVersionConstraints that +// panics if it encounters an error while parsing. +func MustParseVersionConstraints(str string) VersionConstraints { + ret, err := ParseVersionConstraints(str) + if err != nil { + panic(err) + } + return ret +} + +// MeetingConstraints returns a version set that contains all of the versions +// that meet the given constraints, specified using the Spec type from the +// constraints package. +func MeetingConstraints(vc VersionConstraints) VersionSet { + return versions.MeetingConstraints(vc) +} + +// VersionConstraintsString returns a canonical string representation of +// a VersionConstraints value. +func VersionConstraintsString(spec VersionConstraints) string { + // (we have our own function for this because the upstream versions + // library prefers to use npm/cargo-style constraint syntax, but + // Terraform prefers Ruby-like. Maybe we can upstream a "RubyLikeString") + // function to do this later, but having this in here avoids blocking on + // that and this is the sort of thing that is unlikely to need ongoing + // maintenance because the version constraint syntax is unlikely to change.) + // + // ParseVersionConstraints allows some variations for convenience, but the + // return value from this function serves as the normalized form of a + // particular version constraint, which is the form we require in dependency + // lock files. Therefore the canonical forms produced here are a compatibility + // constraint for the dependency lock file parser. + + if len(spec) == 0 { + return "" + } + + // VersionConstraints values are typically assembled by combining together + // the version constraints from many separate declarations throughout + // a configuration, across many modules. As a consequence, they typically + // contain duplicates and the terms inside are in no particular order. + // For our canonical representation we'll both deduplicate the items + // and sort them into a consistent order. + sels := make(map[constraints.SelectionSpec]struct{}) + for _, sel := range spec { + // The parser allows writing abbreviated version (such as 2) which + // end up being represented in memory with trailing unconstrained parts + // (for example 2.*.*). For the purpose of serialization with Ruby + // style syntax, these unconstrained parts can all be represented as 0 + // with no loss of meaning, so we make that conversion here. Doing so + // allows us to deduplicate equivalent constraints, such as >= 2.0 and + // >= 2.0.0. + normalizedSel := constraints.SelectionSpec{ + Operator: sel.Operator, + Boundary: sel.Boundary.ConstrainToZero(), + } + sels[normalizedSel] = struct{}{} + } + selsOrder := make([]constraints.SelectionSpec, 0, len(sels)) + for sel := range sels { + selsOrder = append(selsOrder, sel) + } + sort.Slice(selsOrder, func(i, j int) bool { + is, js := selsOrder[i], selsOrder[j] + boundaryCmp := versionSelectionBoundaryCompare(is.Boundary, js.Boundary) + if boundaryCmp == 0 { + // The operator is the decider, then. + return versionSelectionOperatorLess(is.Operator, js.Operator) + } + return boundaryCmp < 0 + }) + + var b strings.Builder + for i, sel := range selsOrder { + if i > 0 { + b.WriteString(", ") + } + switch sel.Operator { + case constraints.OpGreaterThan: + b.WriteString("> ") + case constraints.OpLessThan: + b.WriteString("< ") + case constraints.OpGreaterThanOrEqual: + b.WriteString(">= ") + case constraints.OpGreaterThanOrEqualPatchOnly, constraints.OpGreaterThanOrEqualMinorOnly: + // These two differ in how the version is written, not in the symbol. + b.WriteString("~> ") + case constraints.OpLessThanOrEqual: + b.WriteString("<= ") + case constraints.OpEqual: + b.WriteString("") + case constraints.OpNotEqual: + b.WriteString("!= ") + default: + // The above covers all of the operators we support during + // parsing, so we should not get here. + b.WriteString("??? ") + } + + // We use a different constraint operator to distinguish between the + // two types of pessimistic constraint: minor-only and patch-only. For + // minor-only constraints, we always want to display only the major and + // minor version components, so we special-case that operator below. + // + // One final edge case is a minor-only constraint specified with only + // the major version, such as ~> 2. We treat this the same as ~> 2.0, + // because a major-only pessimistic constraint does not exist: it is + // logically identical to >= 2.0.0. + if sel.Operator == constraints.OpGreaterThanOrEqualMinorOnly { + // The minor-pessimistic syntax uses only two version components. + fmt.Fprintf(&b, "%s.%s", sel.Boundary.Major, sel.Boundary.Minor) + } else { + fmt.Fprintf(&b, "%s.%s.%s", sel.Boundary.Major, sel.Boundary.Minor, sel.Boundary.Patch) + } + if sel.Boundary.Prerelease != "" { + b.WriteString("-" + sel.Boundary.Prerelease) + } + if sel.Boundary.Metadata != "" { + b.WriteString("+" + sel.Boundary.Metadata) + } + } + return b.String() +} + +// Our sort for selection operators is somewhat arbitrary and mainly motivated +// by consistency rather than meaning, but this ordering does at least try +// to make it so "simple" constraint sets will appear how a human might +// typically write them, with the lower bounds first and the upper bounds +// last. Weird mixtures of different sorts of constraints will likely seem +// less intuitive, but they'd be unintuitive no matter the ordering. +var versionSelectionsBoundaryPriority = map[constraints.SelectionOp]int{ + // We skip zero here so that if we end up seeing an invalid + // operator (which the string function would render as "???") + // then it will have index zero and thus appear first. + constraints.OpGreaterThan: 1, + constraints.OpGreaterThanOrEqual: 2, + constraints.OpEqual: 3, + constraints.OpGreaterThanOrEqualPatchOnly: 4, + constraints.OpGreaterThanOrEqualMinorOnly: 5, + constraints.OpLessThanOrEqual: 6, + constraints.OpLessThan: 7, + constraints.OpNotEqual: 8, +} + +func versionSelectionOperatorLess(i, j constraints.SelectionOp) bool { + iPrio := versionSelectionsBoundaryPriority[i] + jPrio := versionSelectionsBoundaryPriority[j] + return iPrio < jPrio +} + +func versionSelectionBoundaryCompare(i, j constraints.VersionSpec) int { + // In the Ruby-style constraint syntax, unconstrained parts appear + // only for omitted portions of a version string, like writing + // "2" instead of "2.0.0". For sorting purposes we'll just + // consider those as zero, which also matches how we serialize them + // to strings. + i, j = i.ConstrainToZero(), j.ConstrainToZero() + + // Once we've removed any unconstrained parts, we can safely + // convert to our main Version type so we can use its ordering. + iv := Version{ + Major: i.Major.Num, + Minor: i.Minor.Num, + Patch: i.Patch.Num, + Prerelease: versions.VersionExtra(i.Prerelease), + Metadata: versions.VersionExtra(i.Metadata), + } + jv := Version{ + Major: j.Major.Num, + Minor: j.Minor.Num, + Patch: j.Patch.Num, + Prerelease: versions.VersionExtra(j.Prerelease), + Metadata: versions.VersionExtra(j.Metadata), + } + if iv.Same(jv) { + // Although build metadata doesn't normally weigh in to + // precedence choices, we'll use it for our visual + // ordering just because we need to pick _some_ order. + switch { + case iv.Metadata.Raw() == jv.Metadata.Raw(): + return 0 + case iv.Metadata.LessThan(jv.Metadata): + return -1 + default: + return 1 // greater, by elimination + } + } + switch { + case iv.LessThan(jv): + return -1 + default: + return 1 // greater, by elimination + } +} diff --git a/internal/getproviders/providerreqs/version_test.go b/internal/getproviders/providerreqs/version_test.go new file mode 100644 index 0000000000..f84fa2be29 --- /dev/null +++ b/internal/getproviders/providerreqs/version_test.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providerreqs + +import ( + "testing" +) + +func TestVersionConstraintsString(t *testing.T) { + testCases := map[string]struct { + spec VersionConstraints + want string + }{ + "exact": { + MustParseVersionConstraints("1.2.3"), + "1.2.3", + }, + "prerelease": { + MustParseVersionConstraints("1.2.3-beta"), + "1.2.3-beta", + }, + "metadata": { + MustParseVersionConstraints("1.2.3+foo.bar"), + "1.2.3+foo.bar", + }, + "prerelease and metadata": { + MustParseVersionConstraints("1.2.3-beta+foo.bar"), + "1.2.3-beta+foo.bar", + }, + "major only": { + MustParseVersionConstraints(">= 3"), + ">= 3.0.0", + }, + "major only with pessimistic operator": { + MustParseVersionConstraints("~> 3"), + "~> 3.0", + }, + "pessimistic minor": { + MustParseVersionConstraints("~> 3.0"), + "~> 3.0", + }, + "pessimistic patch": { + MustParseVersionConstraints("~> 3.0.0"), + "~> 3.0.0", + }, + "other operators": { + MustParseVersionConstraints("> 1.0.0, < 1.0.0, >= 1.0.0, <= 1.0.0, != 1.0.0"), + "> 1.0.0, >= 1.0.0, <= 1.0.0, < 1.0.0, != 1.0.0", + }, + "multiple": { + MustParseVersionConstraints(">= 3.0, < 4.0"), + ">= 3.0.0, < 4.0.0", + }, + "duplicates removed": { + MustParseVersionConstraints(">= 1.2.3, 1.2.3, ~> 1.2, 1.2.3"), + "~> 1.2, >= 1.2.3, 1.2.3", + }, + "equivalent duplicates removed": { + MustParseVersionConstraints(">= 2.68, >= 2.68.0"), + ">= 2.68.0", + }, + "consistent ordering, exhaustive": { + // This weird jumble is just to exercise the different sort + // ordering codepaths. Hopefully nothing quite this horrific + // shows up often in practice. + MustParseVersionConstraints("< 1.2.3, <= 1.2.3, != 1.2.3, 1.2.3+local.2, 1.2.3+local.1, = 1.2.4, = 1.2.3, > 2, > 1.2.3, >= 1.2.3, ~> 1.2.3, ~> 1.2"), + "~> 1.2, > 1.2.3, >= 1.2.3, 1.2.3, ~> 1.2.3, <= 1.2.3, < 1.2.3, != 1.2.3, 1.2.3+local.1, 1.2.3+local.2, 1.2.4, > 2.0.0", + }, + "consistent ordering, more typical": { + // This one is aiming to simulate a common situation where + // various different modules express compatible constraints + // but some modules are more constrained than others. The + // combined results here can be kinda confusing, but hopefully + // ordering them consistently makes them a _little_ easier to read. + MustParseVersionConstraints("~> 1.2, >= 1.2, 1.2.4"), + ">= 1.2.0, ~> 1.2, 1.2.4", + }, + "consistent ordering, disjoint": { + // One situation where our presentation of version constraints is + // particularly important is when a user accidentally ends up with + // disjoint constraints that can therefore never match. In that + // case, our ordering should hopefully make it easier to determine + // that the constraints are disjoint, as a first step to debugging, + // by showing > or >= constrains sorted after < or <= constraints. + MustParseVersionConstraints(">= 2, >= 1.2, < 1.3"), + ">= 1.2.0, < 1.3.0, >= 2.0.0", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + got := VersionConstraintsString(tc.spec) + + if got != tc.want { + t.Errorf("wrong\n got: %q\nwant: %q", got, tc.want) + } + }) + } +} diff --git a/internal/getproviders/public_keys.go b/internal/getproviders/public_keys.go index 7426564545..1d4ecba1a5 100644 --- a/internal/getproviders/public_keys.go +++ b/internal/getproviders/public_keys.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders // HashicorpPublicKey is the HashiCorp public key, also available at diff --git a/internal/getproviders/registry_client.go b/internal/getproviders/registry_client.go index 5da2a83ca2..82ab2ed873 100644 --- a/internal/getproviders/registry_client.go +++ b/internal/getproviders/registry_client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/registry_client_test.go b/internal/getproviders/registry_client_test.go index 85fe00aa8f..73d96ff4af 100644 --- a/internal/getproviders/registry_client_test.go +++ b/internal/getproviders/registry_client_test.go @@ -1,9 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( "context" "encoding/json" - "fmt" "log" "net/http" "net/http/httptest" @@ -446,8 +448,6 @@ func TestFindClosestProtocolCompatibleVersion(t *testing.T) { t.Fatalf("wrong error\ngot: \nwant: %s", test.wantErr) } - fmt.Printf("Got: %s, Want: %s\n", got, test.wantSuggestion) - if !got.Same(test.wantSuggestion) { t.Fatalf("wrong result\ngot: %s\nwant: %s", got.String(), test.wantSuggestion.String()) } diff --git a/internal/getproviders/registry_source.go b/internal/getproviders/registry_source.go index e48e043f71..c001a89a8d 100644 --- a/internal/getproviders/registry_source.go +++ b/internal/getproviders/registry_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/registry_source_test.go b/internal/getproviders/registry_source_test.go index d55d1fff1a..2759995810 100644 --- a/internal/getproviders/registry_source_test.go +++ b/internal/getproviders/registry_source_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/source.go b/internal/getproviders/source.go index b8543d8efd..7e286189fe 100644 --- a/internal/getproviders/source.go +++ b/internal/getproviders/source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( diff --git a/internal/getproviders/types.go b/internal/getproviders/types.go index 28b1913d67..acf040edc3 100644 --- a/internal/getproviders/types.go +++ b/internal/getproviders/types.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders import ( @@ -6,34 +9,32 @@ import ( "sort" "strings" - "github.com/apparentlymart/go-versions/versions" - "github.com/apparentlymart/go-versions/versions/constraints" - "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" ) // Version represents a particular single version of a provider. -type Version = versions.Version +type Version = providerreqs.Version // UnspecifiedVersion is the zero value of Version, representing the absense // of a version number. -var UnspecifiedVersion Version = versions.Unspecified +var UnspecifiedVersion Version = providerreqs.UnspecifiedVersion // VersionList represents a list of versions. It is a []Version with some // extra methods for convenient filtering. -type VersionList = versions.List +type VersionList = providerreqs.VersionList // VersionSet represents a set of versions, usually describing the acceptable // versions that can be selected under a particular version constraint provided // by the end-user. -type VersionSet = versions.Set +type VersionSet = providerreqs.VersionSet // VersionConstraints represents a set of version constraints, which can // define the membership of a VersionSet by exclusion. -type VersionConstraints = constraints.IntersectionSpec +type VersionConstraints = providerreqs.VersionConstraints // Warnings represents a list of warnings returned by a Registry source. -type Warnings = []string +type Warnings = providerreqs.Warnings // Requirements gathers together requirements for many different providers // into a single data structure, as a convenient way to represent the full @@ -43,69 +44,44 @@ type Warnings = []string // that indicates that the provider is required but that any version is // acceptable. That's different than a provider being absent from the map // altogether, which means that it is not required at all. -type Requirements map[addrs.Provider]VersionConstraints - -// Merge takes the requirements in the receiever and the requirements in the -// other given value and produces a new set of requirements that combines -// all of the requirements of both. -// -// The resulting requirements will permit only selections that both of the -// source requirements would've allowed. -func (r Requirements) Merge(other Requirements) Requirements { - ret := make(Requirements) - for addr, constraints := range r { - ret[addr] = constraints - } - for addr, constraints := range other { - ret[addr] = append(ret[addr], constraints...) - } - return ret -} +type Requirements = providerreqs.Requirements // Selections gathers together version selections for many different providers. // // This is the result of provider installation: a specific version selected // for each provider given in the requested Requirements, selected based on // the given version constraints. -type Selections map[addrs.Provider]Version +type Selections = providerreqs.Selections // ParseVersion parses a "semver"-style version string into a Version value, // which is the version syntax we use for provider versions. func ParseVersion(str string) (Version, error) { - return versions.ParseVersion(str) + return providerreqs.ParseVersion(str) } // MustParseVersion is a variant of ParseVersion that panics if it encounters // an error while parsing. func MustParseVersion(str string) Version { - ret, err := ParseVersion(str) - if err != nil { - panic(err) - } - return ret + return providerreqs.MustParseVersion(str) } // ParseVersionConstraints parses a "Ruby-like" version constraint string // into a VersionConstraints value. func ParseVersionConstraints(str string) (VersionConstraints, error) { - return constraints.ParseRubyStyleMulti(str) + return providerreqs.ParseVersionConstraints(str) } // MustParseVersionConstraints is a variant of ParseVersionConstraints that // panics if it encounters an error while parsing. func MustParseVersionConstraints(str string) VersionConstraints { - ret, err := ParseVersionConstraints(str) - if err != nil { - panic(err) - } - return ret + return providerreqs.MustParseVersionConstraints(str) } // MeetingConstraints returns a version set that contains all of the versions // that meet the given constraints, specified using the Spec type from the // constraints package. func MeetingConstraints(vc VersionConstraints) VersionSet { - return versions.MeetingConstraints(vc) + return providerreqs.MeetingConstraints(vc) } // Platform represents a target platform that a provider is or might be @@ -382,177 +358,5 @@ func (l PackageMetaList) FilterProviderPlatformExactVersion(provider addrs.Provi // VersionConstraintsString returns a canonical string representation of // a VersionConstraints value. func VersionConstraintsString(spec VersionConstraints) string { - // (we have our own function for this because the upstream versions - // library prefers to use npm/cargo-style constraint syntax, but - // Terraform prefers Ruby-like. Maybe we can upstream a "RubyLikeString") - // function to do this later, but having this in here avoids blocking on - // that and this is the sort of thing that is unlikely to need ongoing - // maintenance because the version constraint syntax is unlikely to change.) - // - // ParseVersionConstraints allows some variations for convenience, but the - // return value from this function serves as the normalized form of a - // particular version constraint, which is the form we require in dependency - // lock files. Therefore the canonical forms produced here are a compatibility - // constraint for the dependency lock file parser. - - if len(spec) == 0 { - return "" - } - - // VersionConstraints values are typically assembled by combining together - // the version constraints from many separate declarations throughout - // a configuration, across many modules. As a consequence, they typically - // contain duplicates and the terms inside are in no particular order. - // For our canonical representation we'll both deduplicate the items - // and sort them into a consistent order. - sels := make(map[constraints.SelectionSpec]struct{}) - for _, sel := range spec { - // The parser allows writing abbreviated version (such as 2) which - // end up being represented in memory with trailing unconstrained parts - // (for example 2.*.*). For the purpose of serialization with Ruby - // style syntax, these unconstrained parts can all be represented as 0 - // with no loss of meaning, so we make that conversion here. Doing so - // allows us to deduplicate equivalent constraints, such as >= 2.0 and - // >= 2.0.0. - normalizedSel := constraints.SelectionSpec{ - Operator: sel.Operator, - Boundary: sel.Boundary.ConstrainToZero(), - } - sels[normalizedSel] = struct{}{} - } - selsOrder := make([]constraints.SelectionSpec, 0, len(sels)) - for sel := range sels { - selsOrder = append(selsOrder, sel) - } - sort.Slice(selsOrder, func(i, j int) bool { - is, js := selsOrder[i], selsOrder[j] - boundaryCmp := versionSelectionBoundaryCompare(is.Boundary, js.Boundary) - if boundaryCmp == 0 { - // The operator is the decider, then. - return versionSelectionOperatorLess(is.Operator, js.Operator) - } - return boundaryCmp < 0 - }) - - var b strings.Builder - for i, sel := range selsOrder { - if i > 0 { - b.WriteString(", ") - } - switch sel.Operator { - case constraints.OpGreaterThan: - b.WriteString("> ") - case constraints.OpLessThan: - b.WriteString("< ") - case constraints.OpGreaterThanOrEqual: - b.WriteString(">= ") - case constraints.OpGreaterThanOrEqualPatchOnly, constraints.OpGreaterThanOrEqualMinorOnly: - // These two differ in how the version is written, not in the symbol. - b.WriteString("~> ") - case constraints.OpLessThanOrEqual: - b.WriteString("<= ") - case constraints.OpEqual: - b.WriteString("") - case constraints.OpNotEqual: - b.WriteString("!= ") - default: - // The above covers all of the operators we support during - // parsing, so we should not get here. - b.WriteString("??? ") - } - - // We use a different constraint operator to distinguish between the - // two types of pessimistic constraint: minor-only and patch-only. For - // minor-only constraints, we always want to display only the major and - // minor version components, so we special-case that operator below. - // - // One final edge case is a minor-only constraint specified with only - // the major version, such as ~> 2. We treat this the same as ~> 2.0, - // because a major-only pessimistic constraint does not exist: it is - // logically identical to >= 2.0.0. - if sel.Operator == constraints.OpGreaterThanOrEqualMinorOnly { - // The minor-pessimistic syntax uses only two version components. - fmt.Fprintf(&b, "%s.%s", sel.Boundary.Major, sel.Boundary.Minor) - } else { - fmt.Fprintf(&b, "%s.%s.%s", sel.Boundary.Major, sel.Boundary.Minor, sel.Boundary.Patch) - } - if sel.Boundary.Prerelease != "" { - b.WriteString("-" + sel.Boundary.Prerelease) - } - if sel.Boundary.Metadata != "" { - b.WriteString("+" + sel.Boundary.Metadata) - } - } - return b.String() -} - -// Our sort for selection operators is somewhat arbitrary and mainly motivated -// by consistency rather than meaning, but this ordering does at least try -// to make it so "simple" constraint sets will appear how a human might -// typically write them, with the lower bounds first and the upper bounds -// last. Weird mixtures of different sorts of constraints will likely seem -// less intuitive, but they'd be unintuitive no matter the ordering. -var versionSelectionsBoundaryPriority = map[constraints.SelectionOp]int{ - // We skip zero here so that if we end up seeing an invalid - // operator (which the string function would render as "???") - // then it will have index zero and thus appear first. - constraints.OpGreaterThan: 1, - constraints.OpGreaterThanOrEqual: 2, - constraints.OpEqual: 3, - constraints.OpGreaterThanOrEqualPatchOnly: 4, - constraints.OpGreaterThanOrEqualMinorOnly: 5, - constraints.OpLessThanOrEqual: 6, - constraints.OpLessThan: 7, - constraints.OpNotEqual: 8, -} - -func versionSelectionOperatorLess(i, j constraints.SelectionOp) bool { - iPrio := versionSelectionsBoundaryPriority[i] - jPrio := versionSelectionsBoundaryPriority[j] - return iPrio < jPrio -} - -func versionSelectionBoundaryCompare(i, j constraints.VersionSpec) int { - // In the Ruby-style constraint syntax, unconstrained parts appear - // only for omitted portions of a version string, like writing - // "2" instead of "2.0.0". For sorting purposes we'll just - // consider those as zero, which also matches how we serialize them - // to strings. - i, j = i.ConstrainToZero(), j.ConstrainToZero() - - // Once we've removed any unconstrained parts, we can safely - // convert to our main Version type so we can use its ordering. - iv := Version{ - Major: i.Major.Num, - Minor: i.Minor.Num, - Patch: i.Patch.Num, - Prerelease: versions.VersionExtra(i.Prerelease), - Metadata: versions.VersionExtra(i.Metadata), - } - jv := Version{ - Major: j.Major.Num, - Minor: j.Minor.Num, - Patch: j.Patch.Num, - Prerelease: versions.VersionExtra(j.Prerelease), - Metadata: versions.VersionExtra(j.Metadata), - } - if iv.Same(jv) { - // Although build metadata doesn't normally weigh in to - // precedence choices, we'll use it for our visual - // ordering just because we need to pick _some_ order. - switch { - case iv.Metadata.Raw() == jv.Metadata.Raw(): - return 0 - case iv.Metadata.LessThan(jv.Metadata): - return -1 - default: - return 1 // greater, by elimination - } - } - switch { - case iv.LessThan(jv): - return -1 - default: - return 1 // greater, by elimination - } + return providerreqs.VersionConstraintsString(spec) } diff --git a/internal/getproviders/types_test.go b/internal/getproviders/types_test.go index 0008793a1f..386adb474f 100644 --- a/internal/getproviders/types_test.go +++ b/internal/getproviders/types_test.go @@ -1,99 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package getproviders -import ( - "testing" -) - -func TestVersionConstraintsString(t *testing.T) { - testCases := map[string]struct { - spec VersionConstraints - want string - }{ - "exact": { - MustParseVersionConstraints("1.2.3"), - "1.2.3", - }, - "prerelease": { - MustParseVersionConstraints("1.2.3-beta"), - "1.2.3-beta", - }, - "metadata": { - MustParseVersionConstraints("1.2.3+foo.bar"), - "1.2.3+foo.bar", - }, - "prerelease and metadata": { - MustParseVersionConstraints("1.2.3-beta+foo.bar"), - "1.2.3-beta+foo.bar", - }, - "major only": { - MustParseVersionConstraints(">= 3"), - ">= 3.0.0", - }, - "major only with pessimistic operator": { - MustParseVersionConstraints("~> 3"), - "~> 3.0", - }, - "pessimistic minor": { - MustParseVersionConstraints("~> 3.0"), - "~> 3.0", - }, - "pessimistic patch": { - MustParseVersionConstraints("~> 3.0.0"), - "~> 3.0.0", - }, - "other operators": { - MustParseVersionConstraints("> 1.0.0, < 1.0.0, >= 1.0.0, <= 1.0.0, != 1.0.0"), - "> 1.0.0, >= 1.0.0, <= 1.0.0, < 1.0.0, != 1.0.0", - }, - "multiple": { - MustParseVersionConstraints(">= 3.0, < 4.0"), - ">= 3.0.0, < 4.0.0", - }, - "duplicates removed": { - MustParseVersionConstraints(">= 1.2.3, 1.2.3, ~> 1.2, 1.2.3"), - "~> 1.2, >= 1.2.3, 1.2.3", - }, - "equivalent duplicates removed": { - MustParseVersionConstraints(">= 2.68, >= 2.68.0"), - ">= 2.68.0", - }, - "consistent ordering, exhaustive": { - // This weird jumble is just to exercise the different sort - // ordering codepaths. Hopefully nothing quite this horrific - // shows up often in practice. - MustParseVersionConstraints("< 1.2.3, <= 1.2.3, != 1.2.3, 1.2.3+local.2, 1.2.3+local.1, = 1.2.4, = 1.2.3, > 2, > 1.2.3, >= 1.2.3, ~> 1.2.3, ~> 1.2"), - "~> 1.2, > 1.2.3, >= 1.2.3, 1.2.3, ~> 1.2.3, <= 1.2.3, < 1.2.3, != 1.2.3, 1.2.3+local.1, 1.2.3+local.2, 1.2.4, > 2.0.0", - }, - "consistent ordering, more typical": { - // This one is aiming to simulate a common situation where - // various different modules express compatible constraints - // but some modules are more constrained than others. The - // combined results here can be kinda confusing, but hopefully - // ordering them consistently makes them a _little_ easier to read. - MustParseVersionConstraints("~> 1.2, >= 1.2, 1.2.4"), - ">= 1.2.0, ~> 1.2, 1.2.4", - }, - "consistent ordering, disjoint": { - // One situation where our presentation of version constraints is - // particularly important is when a user accidentally ends up with - // disjoint constraints that can therefore never match. In that - // case, our ordering should hopefully make it easier to determine - // that the constraints are disjoint, as a first step to debugging, - // by showing > or >= constrains sorted after < or <= constraints. - MustParseVersionConstraints(">= 2, >= 1.2, < 1.3"), - ">= 1.2.0, < 1.3.0, >= 2.0.0", - }, - } - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - got := VersionConstraintsString(tc.spec) - - if got != tc.want { - t.Errorf("wrong\n got: %q\nwant: %q", got, tc.want) - } - }) - } -} +import "testing" func TestParsePlatform(t *testing.T) { tests := []struct { diff --git a/internal/grpcwrap/provider.go b/internal/grpcwrap/provider.go index 170cea6388..162b9d380d 100644 --- a/internal/grpcwrap/provider.go +++ b/internal/grpcwrap/provider.go @@ -1,66 +1,95 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package grpcwrap import ( "context" + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "github.com/hashicorp/terraform/internal/plugin/convert" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfplugin5" - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/zclconf/go-cty/cty/msgpack" ) -// New wraps a providers.Interface to implement a grpc ProviderServer. +// Provider wraps a providers.Interface to implement a grpc ProviderServer. // This is useful for creating a test binary out of an internal provider // implementation. func Provider(p providers.Interface) tfplugin5.ProviderServer { return &provider{ - provider: p, - schema: p.GetProviderSchema(), + provider: p, + schema: p.GetProviderSchema(), + identitySchemas: p.GetResourceIdentitySchemas(), } } type provider struct { - provider providers.Interface - schema providers.GetProviderSchemaResponse + provider providers.Interface + schema providers.GetProviderSchemaResponse + identitySchemas providers.GetResourceIdentitySchemasResponse +} + +func (p *provider) GetMetadata(_ context.Context, req *tfplugin5.GetMetadata_Request) (*tfplugin5.GetMetadata_Response, error) { + return nil, status.Error(codes.Unimplemented, "GetMetadata is not implemented by core") } func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema_Request) (*tfplugin5.GetProviderSchema_Response, error) { resp := &tfplugin5.GetProviderSchema_Response{ - ResourceSchemas: make(map[string]*tfplugin5.Schema), - DataSourceSchemas: make(map[string]*tfplugin5.Schema), + ResourceSchemas: make(map[string]*tfplugin5.Schema), + DataSourceSchemas: make(map[string]*tfplugin5.Schema), + EphemeralResourceSchemas: make(map[string]*tfplugin5.Schema), } resp.Provider = &tfplugin5.Schema{ Block: &tfplugin5.Schema_Block{}, } - if p.schema.Provider.Block != nil { - resp.Provider.Block = convert.ConfigSchemaToProto(p.schema.Provider.Block) + if p.schema.Provider.Body != nil { + resp.Provider.Block = convert.ConfigSchemaToProto(p.schema.Provider.Body) } resp.ProviderMeta = &tfplugin5.Schema{ Block: &tfplugin5.Schema_Block{}, } - if p.schema.ProviderMeta.Block != nil { - resp.ProviderMeta.Block = convert.ConfigSchemaToProto(p.schema.ProviderMeta.Block) + if p.schema.ProviderMeta.Body != nil { + resp.ProviderMeta.Block = convert.ConfigSchemaToProto(p.schema.ProviderMeta.Body) } for typ, res := range p.schema.ResourceTypes { resp.ResourceSchemas[typ] = &tfplugin5.Schema{ Version: res.Version, - Block: convert.ConfigSchemaToProto(res.Block), + Block: convert.ConfigSchemaToProto(res.Body), } } for typ, dat := range p.schema.DataSources { resp.DataSourceSchemas[typ] = &tfplugin5.Schema{ - Version: dat.Version, - Block: convert.ConfigSchemaToProto(dat.Block), + Version: int64(dat.Version), + Block: convert.ConfigSchemaToProto(dat.Body), } } + for typ, dat := range p.schema.EphemeralResourceTypes { + resp.EphemeralResourceSchemas[typ] = &tfplugin5.Schema{ + Version: int64(dat.Version), + Block: convert.ConfigSchemaToProto(dat.Body), + } + } + if decls, err := convert.FunctionDeclsToProto(p.schema.Functions); err == nil { + resp.Functions = decls + } else { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } - resp.ServerCapabilities = &tfplugin5.GetProviderSchema_ServerCapabilities{ - PlanDestroy: p.schema.ServerCapabilities.PlanDestroy, + resp.ServerCapabilities = &tfplugin5.ServerCapabilities{ + GetProviderSchemaOptional: p.schema.ServerCapabilities.GetProviderSchemaOptional, + PlanDestroy: p.schema.ServerCapabilities.PlanDestroy, + MoveResourceState: p.schema.ServerCapabilities.MoveResourceState, } // include any diagnostics from the original GetSchema call @@ -71,7 +100,7 @@ func (p *provider) GetSchema(_ context.Context, req *tfplugin5.GetProviderSchema func (p *provider) PrepareProviderConfig(_ context.Context, req *tfplugin5.PrepareProviderConfig_Request) (*tfplugin5.PrepareProviderConfig_Response, error) { resp := &tfplugin5.PrepareProviderConfig_Response{} - ty := p.schema.Provider.Block.ImpliedType() + ty := p.schema.Provider.Body.ImpliedType() configVal, err := decodeDynamicValue(req.Config, ty) if err != nil { @@ -90,7 +119,7 @@ func (p *provider) PrepareProviderConfig(_ context.Context, req *tfplugin5.Prepa func (p *provider) ValidateResourceTypeConfig(_ context.Context, req *tfplugin5.ValidateResourceTypeConfig_Request) (*tfplugin5.ValidateResourceTypeConfig_Response, error) { resp := &tfplugin5.ValidateResourceTypeConfig_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + ty := p.schema.ResourceTypes[req.TypeName].Body.ImpliedType() configVal, err := decodeDynamicValue(req.Config, ty) if err != nil { @@ -101,6 +130,10 @@ func (p *provider) ValidateResourceTypeConfig(_ context.Context, req *tfplugin5. validateResp := p.provider.ValidateResourceConfig(providers.ValidateResourceConfigRequest{ TypeName: req.TypeName, Config: configVal, + ClientCapabilities: providers.ClientCapabilities{ + DeferralAllowed: true, + WriteOnlyAttributesAllowed: true, + }, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics) @@ -109,7 +142,7 @@ func (p *provider) ValidateResourceTypeConfig(_ context.Context, req *tfplugin5. func (p *provider) ValidateDataSourceConfig(_ context.Context, req *tfplugin5.ValidateDataSourceConfig_Request) (*tfplugin5.ValidateDataSourceConfig_Response, error) { resp := &tfplugin5.ValidateDataSourceConfig_Response{} - ty := p.schema.DataSources[req.TypeName].Block.ImpliedType() + ty := p.schema.DataSources[req.TypeName].Body.ImpliedType() configVal, err := decodeDynamicValue(req.Config, ty) if err != nil { @@ -126,9 +159,28 @@ func (p *provider) ValidateDataSourceConfig(_ context.Context, req *tfplugin5.Va return resp, nil } +func (p *provider) ValidateEphemeralResourceConfig(_ context.Context, req *tfplugin5.ValidateEphemeralResourceConfig_Request) (*tfplugin5.ValidateEphemeralResourceConfig_Response, error) { + resp := &tfplugin5.ValidateEphemeralResourceConfig_Response{} + ty := p.schema.DataSources[req.TypeName].Body.ImpliedType() + + configVal, err := decodeDynamicValue(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + validateResp := p.provider.ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest{ + TypeName: req.TypeName, + Config: configVal, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics) + return resp, nil +} + func (p *provider) UpgradeResourceState(_ context.Context, req *tfplugin5.UpgradeResourceState_Request) (*tfplugin5.UpgradeResourceState_Response, error) { resp := &tfplugin5.UpgradeResourceState_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + ty := p.schema.ResourceTypes[req.TypeName].Body.ImpliedType() upgradeResp := p.provider.UpgradeResourceState(providers.UpgradeResourceStateRequest{ TypeName: req.TypeName, @@ -154,7 +206,7 @@ func (p *provider) UpgradeResourceState(_ context.Context, req *tfplugin5.Upgrad func (p *provider) Configure(_ context.Context, req *tfplugin5.Configure_Request) (*tfplugin5.Configure_Response, error) { resp := &tfplugin5.Configure_Response{} - ty := p.schema.Provider.Block.ImpliedType() + ty := p.schema.Provider.Body.ImpliedType() configVal, err := decodeDynamicValue(req.Config, ty) if err != nil { @@ -173,7 +225,8 @@ func (p *provider) Configure(_ context.Context, req *tfplugin5.Configure_Request func (p *provider) ReadResource(_ context.Context, req *tfplugin5.ReadResource_Request) (*tfplugin5.ReadResource_Response, error) { resp := &tfplugin5.ReadResource_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + resSchema := p.schema.ResourceTypes[req.TypeName] + ty := resSchema.Body.ImpliedType() stateVal, err := decodeDynamicValue(req.CurrentState, ty) if err != nil { @@ -181,18 +234,31 @@ func (p *provider) ReadResource(_ context.Context, req *tfplugin5.ReadResource_R return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + var currentIdentity cty.Value + if req.CurrentIdentity != nil && req.CurrentIdentity.IdentityData != nil { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + currentIdentity, err = decodeDynamicValue(req.CurrentIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + readResp := p.provider.ReadResource(providers.ReadResourceRequest{ - TypeName: req.TypeName, - PriorState: stateVal, - Private: req.Private, - ProviderMeta: metaVal, + TypeName: req.TypeName, + PriorState: stateVal, + Private: req.Private, + ProviderMeta: metaVal, + CurrentIdentity: currentIdentity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, readResp.Diagnostics) if readResp.Diagnostics.HasErrors() { @@ -207,12 +273,27 @@ func (p *provider) ReadResource(_ context.Context, req *tfplugin5.ReadResource_R } resp.NewState = dv + if !readResp.Identity.IsNull() { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + identity, err := encodeDynamicValue(readResp.Identity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.NewIdentity = &tfplugin5.ResourceIdentityData{ + IdentityData: identity, + } + } return resp, nil } func (p *provider) PlanResourceChange(_ context.Context, req *tfplugin5.PlanResourceChange_Request) (*tfplugin5.PlanResourceChange_Response, error) { resp := &tfplugin5.PlanResourceChange_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + resSchema := p.schema.ResourceTypes[req.TypeName] + ty := resSchema.Body.ImpliedType() priorStateVal, err := decodeDynamicValue(req.PriorState, ty) if err != nil { @@ -232,13 +313,27 @@ func (p *provider) PlanResourceChange(_ context.Context, req *tfplugin5.PlanReso return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + var priorIdentity cty.Value + if req.PriorIdentity != nil && req.PriorIdentity.IdentityData != nil { + + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + priorIdentity, err = decodeDynamicValue(req.PriorIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + planResp := p.provider.PlanResourceChange(providers.PlanResourceChangeRequest{ TypeName: req.TypeName, PriorState: priorStateVal, @@ -246,6 +341,7 @@ func (p *provider) PlanResourceChange(_ context.Context, req *tfplugin5.PlanReso Config: configVal, PriorPrivate: req.PriorPrivate, ProviderMeta: metaVal, + PriorIdentity: priorIdentity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, planResp.Diagnostics) if planResp.Diagnostics.HasErrors() { @@ -264,12 +360,30 @@ func (p *provider) PlanResourceChange(_ context.Context, req *tfplugin5.PlanReso resp.RequiresReplace = append(resp.RequiresReplace, convert.PathToAttributePath(path)) } + if !planResp.PlannedIdentity.IsNull() { + + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + plannedIdentity, err := encodeDynamicValue(planResp.PlannedIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + resp.PlannedIdentity = &tfplugin5.ResourceIdentityData{ + IdentityData: plannedIdentity, + } + } + return resp, nil } func (p *provider) ApplyResourceChange(_ context.Context, req *tfplugin5.ApplyResourceChange_Request) (*tfplugin5.ApplyResourceChange_Response, error) { resp := &tfplugin5.ApplyResourceChange_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + resSchema := p.schema.ResourceTypes[req.TypeName] + ty := resSchema.Body.ImpliedType() priorStateVal, err := decodeDynamicValue(req.PriorState, ty) if err != nil { @@ -289,20 +403,34 @@ func (p *provider) ApplyResourceChange(_ context.Context, req *tfplugin5.ApplyRe return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + var plannedIdentity cty.Value + if req.PlannedIdentity != nil && req.PlannedIdentity.IdentityData != nil { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + plannedIdentity, err = decodeDynamicValue(req.PlannedIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + applyResp := p.provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ - TypeName: req.TypeName, - PriorState: priorStateVal, - PlannedState: plannedStateVal, - Config: configVal, - PlannedPrivate: req.PlannedPrivate, - ProviderMeta: metaVal, + TypeName: req.TypeName, + PriorState: priorStateVal, + PlannedState: plannedStateVal, + Config: configVal, + PlannedPrivate: req.PlannedPrivate, + ProviderMeta: metaVal, + PlannedIdentity: plannedIdentity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, applyResp.Diagnostics) @@ -310,38 +438,135 @@ func (p *provider) ApplyResourceChange(_ context.Context, req *tfplugin5.ApplyRe return resp, nil } resp.Private = applyResp.Private - resp.NewState, err = encodeDynamicValue(applyResp.NewState, ty) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + if !applyResp.NewIdentity.IsNull() { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + newIdentity, err := encodeDynamicValue(applyResp.NewIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.NewIdentity = &tfplugin5.ResourceIdentityData{ + IdentityData: newIdentity, + } + } + return resp, nil } func (p *provider) ImportResourceState(_ context.Context, req *tfplugin5.ImportResourceState_Request) (*tfplugin5.ImportResourceState_Response, error) { resp := &tfplugin5.ImportResourceState_Response{} + var identity cty.Value + var err error + if req.Identity != nil && req.Identity.IdentityData != nil { + resSchema := p.schema.ResourceTypes[req.TypeName] + + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + identity, err = decodeDynamicValue(req.Identity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } importResp := p.provider.ImportResourceState(providers.ImportResourceStateRequest{ TypeName: req.TypeName, ID: req.Id, + Identity: identity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, importResp.Diagnostics) for _, res := range importResp.ImportedResources { - ty := p.schema.ResourceTypes[res.TypeName].Block.ImpliedType() + importSchema := p.schema.ResourceTypes[res.TypeName] + ty := importSchema.Body.ImpliedType() state, err := encodeDynamicValue(res.State, ty) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) continue } - resp.ImportedResources = append(resp.ImportedResources, &tfplugin5.ImportResourceState_ImportedResource{ + resource := &tfplugin5.ImportResourceState_ImportedResource{ TypeName: res.TypeName, State: state, Private: res.Private, - }) + } + + if !res.Identity.IsNull() { + if importSchema.Identity == nil { + return nil, fmt.Errorf("identity schema not found for type %s", res.TypeName) + } + identity, err := encodeDynamicValue(res.Identity, importSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + continue + } + resource.Identity = &tfplugin5.ResourceIdentityData{ + IdentityData: identity, + } + } + + resp.ImportedResources = append(resp.ImportedResources, resource) + } + + return resp, nil +} + +func (p *provider) MoveResourceState(_ context.Context, request *tfplugin5.MoveResourceState_Request) (*tfplugin5.MoveResourceState_Response, error) { + resp := &tfplugin5.MoveResourceState_Response{} + + var sourceIdentity []byte + var err error + if request.SourceIdentity != nil && len(request.SourceIdentity.Json) > 0 { + sourceIdentity = request.SourceIdentity.Json + } + + moveResp := p.provider.MoveResourceState(providers.MoveResourceStateRequest{ + SourceProviderAddress: request.SourceProviderAddress, + SourceTypeName: request.SourceTypeName, + SourceSchemaVersion: request.SourceSchemaVersion, + SourceStateJSON: request.SourceState.Json, + SourcePrivate: request.SourcePrivate, + SourceIdentity: sourceIdentity, + TargetTypeName: request.TargetTypeName, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, moveResp.Diagnostics) + if moveResp.Diagnostics.HasErrors() { + return resp, nil + } + + targetSchema := p.schema.ResourceTypes[request.TargetTypeName] + targetType := targetSchema.Body.ImpliedType() + targetState, err := encodeDynamicValue(moveResp.TargetState, targetType) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.TargetState = targetState + resp.TargetPrivate = moveResp.TargetPrivate + + if !moveResp.TargetIdentity.IsNull() { + if targetSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", request.TargetTypeName) + } + targetIdentity, err := encodeDynamicValue(moveResp.TargetIdentity, targetSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.TargetIdentity = &tfplugin5.ResourceIdentityData{ + IdentityData: targetIdentity, + } } return resp, nil @@ -349,7 +574,7 @@ func (p *provider) ImportResourceState(_ context.Context, req *tfplugin5.ImportR func (p *provider) ReadDataSource(_ context.Context, req *tfplugin5.ReadDataSource_Request) (*tfplugin5.ReadDataSource_Response, error) { resp := &tfplugin5.ReadDataSource_Response{} - ty := p.schema.DataSources[req.TypeName].Block.ImpliedType() + ty := p.schema.DataSources[req.TypeName].Body.ImpliedType() configVal, err := decodeDynamicValue(req.Config, ty) if err != nil { @@ -357,7 +582,7 @@ func (p *provider) ReadDataSource(_ context.Context, req *tfplugin5.ReadDataSour return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) @@ -383,6 +608,134 @@ func (p *provider) ReadDataSource(_ context.Context, req *tfplugin5.ReadDataSour return resp, nil } +func (p *provider) OpenEphemeralResource(_ context.Context, req *tfplugin5.OpenEphemeralResource_Request) (*tfplugin5.OpenEphemeralResource_Response, error) { + panic("unimplemented") +} + +func (p *provider) RenewEphemeralResource(_ context.Context, req *tfplugin5.RenewEphemeralResource_Request) (*tfplugin5.RenewEphemeralResource_Response, error) { + panic("unimplemented") +} + +func (p *provider) CloseEphemeralResource(_ context.Context, req *tfplugin5.CloseEphemeralResource_Request) (*tfplugin5.CloseEphemeralResource_Response, error) { + panic("unimplemented") +} + +func (p *provider) GetFunctions(context.Context, *tfplugin5.GetFunctions_Request) (*tfplugin5.GetFunctions_Response, error) { + panic("unimplemented") +} + +func (p *provider) CallFunction(_ context.Context, req *tfplugin5.CallFunction_Request) (*tfplugin5.CallFunction_Response, error) { + var err error + resp := &tfplugin5.CallFunction_Response{} + + funcSchema := p.schema.Functions[req.Name] + + var args []cty.Value + if len(req.Arguments) != 0 { + args = make([]cty.Value, len(req.Arguments)) + for i, rawArg := range req.Arguments { + idx := int64(i) + + var argTy cty.Type + if i < len(funcSchema.Parameters) { + argTy = funcSchema.Parameters[i].Type + } else { + if funcSchema.VariadicParameter == nil { + + resp.Error = &tfplugin5.FunctionError{ + Text: "too many arguments for non-variadic function", + FunctionArgument: &idx, + } + return resp, nil + } + argTy = funcSchema.VariadicParameter.Type + } + + argVal, err := decodeDynamicValue(rawArg, argTy) + if err != nil { + resp.Error = &tfplugin5.FunctionError{ + Text: err.Error(), + FunctionArgument: &idx, + } + return resp, nil + } + + args[i] = argVal + } + } + + callResp := p.provider.CallFunction(providers.CallFunctionRequest{ + FunctionName: req.Name, + Arguments: args, + }) + + if callResp.Err != nil { + resp.Error = &tfplugin5.FunctionError{ + Text: callResp.Err.Error(), + } + + if argErr, ok := callResp.Err.(function.ArgError); ok { + idx := int64(argErr.Index) + resp.Error.FunctionArgument = &idx + } + + return resp, nil + } + + resp.Result, err = encodeDynamicValue(callResp.Result, funcSchema.ReturnType) + if err != nil { + resp.Error = &tfplugin5.FunctionError{ + Text: err.Error(), + } + + return resp, nil + } + + return resp, nil +} + +func (p *provider) GetResourceIdentitySchemas(_ context.Context, req *tfplugin5.GetResourceIdentitySchemas_Request) (*tfplugin5.GetResourceIdentitySchemas_Response, error) { + resp := &tfplugin5.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*tfplugin5.ResourceIdentitySchema{}, + Diagnostics: []*tfplugin5.Diagnostic{}, + } + + for name, schema := range p.identitySchemas.IdentityTypes { + resp.IdentitySchemas[name] = convert.ResourceIdentitySchemaToProto(schema) + } + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, p.identitySchemas.Diagnostics) + return resp, nil +} + +func (p *provider) UpgradeResourceIdentity(_ context.Context, req *tfplugin5.UpgradeResourceIdentity_Request) (*tfplugin5.UpgradeResourceIdentity_Response, error) { + resp := &tfplugin5.UpgradeResourceIdentity_Response{} + resource, ok := p.schema.ResourceTypes[req.TypeName] + if !ok { + return nil, fmt.Errorf("resource identity schema not found for type %q", req.TypeName) + } + ty := resource.Identity.ImpliedType() + upgradeResp := p.provider.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: req.TypeName, + Version: req.Version, + RawIdentityJSON: req.RawIdentity.Json, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, upgradeResp.Diagnostics) + if upgradeResp.Diagnostics.HasErrors() { + return resp, nil + } + + dv, err := encodeDynamicValue(upgradeResp.UpgradedIdentity, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.UpgradedIdentity = &tfplugin5.ResourceIdentityData{ + IdentityData: dv, + } + return resp, nil +} + func (p *provider) Stop(context.Context, *tfplugin5.Stop_Request) (*tfplugin5.Stop_Response, error) { resp := &tfplugin5.Stop_Response{} err := p.provider.Stop() diff --git a/internal/grpcwrap/provider6.go b/internal/grpcwrap/provider6.go index af287d0f54..0fe69c2744 100644 --- a/internal/grpcwrap/provider6.go +++ b/internal/grpcwrap/provider6.go @@ -1,14 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package grpcwrap import ( "context" + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/hashicorp/terraform/internal/plugin6/convert" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfplugin6" - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/zclconf/go-cty/cty/msgpack" ) // New wraps a providers.Interface to implement a grpc ProviderServer using @@ -16,51 +25,73 @@ import ( // internal provider implementation. func Provider6(p providers.Interface) tfplugin6.ProviderServer { return &provider6{ - provider: p, - schema: p.GetProviderSchema(), + provider: p, + schema: p.GetProviderSchema(), + identitySchemas: p.GetResourceIdentitySchemas(), } } type provider6 struct { - provider providers.Interface - schema providers.GetProviderSchemaResponse + provider providers.Interface + schema providers.GetProviderSchemaResponse + identitySchemas providers.GetResourceIdentitySchemasResponse +} + +func (p *provider6) GetMetadata(_ context.Context, req *tfplugin6.GetMetadata_Request) (*tfplugin6.GetMetadata_Response, error) { + return nil, status.Error(codes.Unimplemented, "GetMetadata is not implemented by core") } func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProviderSchema_Request) (*tfplugin6.GetProviderSchema_Response, error) { resp := &tfplugin6.GetProviderSchema_Response{ - ResourceSchemas: make(map[string]*tfplugin6.Schema), - DataSourceSchemas: make(map[string]*tfplugin6.Schema), + ResourceSchemas: make(map[string]*tfplugin6.Schema), + DataSourceSchemas: make(map[string]*tfplugin6.Schema), + EphemeralResourceSchemas: make(map[string]*tfplugin6.Schema), + Functions: make(map[string]*tfplugin6.Function), } resp.Provider = &tfplugin6.Schema{ Block: &tfplugin6.Schema_Block{}, } - if p.schema.Provider.Block != nil { - resp.Provider.Block = convert.ConfigSchemaToProto(p.schema.Provider.Block) + if p.schema.Provider.Body != nil { + resp.Provider.Block = convert.ConfigSchemaToProto(p.schema.Provider.Body) } resp.ProviderMeta = &tfplugin6.Schema{ Block: &tfplugin6.Schema_Block{}, } - if p.schema.ProviderMeta.Block != nil { - resp.ProviderMeta.Block = convert.ConfigSchemaToProto(p.schema.ProviderMeta.Block) + if p.schema.ProviderMeta.Body != nil { + resp.ProviderMeta.Block = convert.ConfigSchemaToProto(p.schema.ProviderMeta.Body) } for typ, res := range p.schema.ResourceTypes { resp.ResourceSchemas[typ] = &tfplugin6.Schema{ Version: res.Version, - Block: convert.ConfigSchemaToProto(res.Block), + Block: convert.ConfigSchemaToProto(res.Body), } } for typ, dat := range p.schema.DataSources { resp.DataSourceSchemas[typ] = &tfplugin6.Schema{ - Version: dat.Version, - Block: convert.ConfigSchemaToProto(dat.Block), + Version: int64(dat.Version), + Block: convert.ConfigSchemaToProto(dat.Body), } } + for typ, dat := range p.schema.EphemeralResourceTypes { + resp.EphemeralResourceSchemas[typ] = &tfplugin6.Schema{ + Version: int64(dat.Version), + Block: convert.ConfigSchemaToProto(dat.Body), + } + } + if decls, err := convert.FunctionDeclsToProto(p.schema.Functions); err == nil { + resp.Functions = decls + } else { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } - resp.ServerCapabilities = &tfplugin6.GetProviderSchema_ServerCapabilities{ - PlanDestroy: p.schema.ServerCapabilities.PlanDestroy, + resp.ServerCapabilities = &tfplugin6.ServerCapabilities{ + GetProviderSchemaOptional: p.schema.ServerCapabilities.GetProviderSchemaOptional, + PlanDestroy: p.schema.ServerCapabilities.PlanDestroy, + MoveResourceState: p.schema.ServerCapabilities.MoveResourceState, } // include any diagnostics from the original GetSchema call @@ -71,7 +102,7 @@ func (p *provider6) GetProviderSchema(_ context.Context, req *tfplugin6.GetProvi func (p *provider6) ValidateProviderConfig(_ context.Context, req *tfplugin6.ValidateProviderConfig_Request) (*tfplugin6.ValidateProviderConfig_Response, error) { resp := &tfplugin6.ValidateProviderConfig_Response{} - ty := p.schema.Provider.Block.ImpliedType() + ty := p.schema.Provider.Body.ImpliedType() configVal, err := decodeDynamicValue6(req.Config, ty) if err != nil { @@ -90,7 +121,7 @@ func (p *provider6) ValidateProviderConfig(_ context.Context, req *tfplugin6.Val func (p *provider6) ValidateResourceConfig(_ context.Context, req *tfplugin6.ValidateResourceConfig_Request) (*tfplugin6.ValidateResourceConfig_Response, error) { resp := &tfplugin6.ValidateResourceConfig_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + ty := p.schema.ResourceTypes[req.TypeName].Body.ImpliedType() configVal, err := decodeDynamicValue6(req.Config, ty) if err != nil { @@ -109,7 +140,7 @@ func (p *provider6) ValidateResourceConfig(_ context.Context, req *tfplugin6.Val func (p *provider6) ValidateDataResourceConfig(_ context.Context, req *tfplugin6.ValidateDataResourceConfig_Request) (*tfplugin6.ValidateDataResourceConfig_Response, error) { resp := &tfplugin6.ValidateDataResourceConfig_Response{} - ty := p.schema.DataSources[req.TypeName].Block.ImpliedType() + ty := p.schema.DataSources[req.TypeName].Body.ImpliedType() configVal, err := decodeDynamicValue6(req.Config, ty) if err != nil { @@ -126,9 +157,28 @@ func (p *provider6) ValidateDataResourceConfig(_ context.Context, req *tfplugin6 return resp, nil } +func (p *provider6) ValidateEphemeralResourceConfig(_ context.Context, req *tfplugin6.ValidateEphemeralResourceConfig_Request) (*tfplugin6.ValidateEphemeralResourceConfig_Response, error) { + resp := &tfplugin6.ValidateEphemeralResourceConfig_Response{} + ty := p.schema.DataSources[req.TypeName].Body.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + validateResp := p.provider.ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest{ + TypeName: req.TypeName, + Config: configVal, + }) + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, validateResp.Diagnostics) + return resp, nil +} + func (p *provider6) UpgradeResourceState(_ context.Context, req *tfplugin6.UpgradeResourceState_Request) (*tfplugin6.UpgradeResourceState_Response, error) { resp := &tfplugin6.UpgradeResourceState_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + ty := p.schema.ResourceTypes[req.TypeName].Body.ImpliedType() upgradeResp := p.provider.UpgradeResourceState(providers.UpgradeResourceStateRequest{ TypeName: req.TypeName, @@ -154,7 +204,7 @@ func (p *provider6) UpgradeResourceState(_ context.Context, req *tfplugin6.Upgra func (p *provider6) ConfigureProvider(_ context.Context, req *tfplugin6.ConfigureProvider_Request) (*tfplugin6.ConfigureProvider_Response, error) { resp := &tfplugin6.ConfigureProvider_Response{} - ty := p.schema.Provider.Block.ImpliedType() + ty := p.schema.Provider.Body.ImpliedType() configVal, err := decodeDynamicValue6(req.Config, ty) if err != nil { @@ -173,7 +223,8 @@ func (p *provider6) ConfigureProvider(_ context.Context, req *tfplugin6.Configur func (p *provider6) ReadResource(_ context.Context, req *tfplugin6.ReadResource_Request) (*tfplugin6.ReadResource_Response, error) { resp := &tfplugin6.ReadResource_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + resSchema := p.schema.ResourceTypes[req.TypeName] + ty := resSchema.Body.ImpliedType() stateVal, err := decodeDynamicValue6(req.CurrentState, ty) if err != nil { @@ -181,18 +232,32 @@ func (p *provider6) ReadResource(_ context.Context, req *tfplugin6.ReadResource_ return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + var currentIdentity cty.Value + if req.CurrentIdentity != nil && req.CurrentIdentity.IdentityData != nil { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + currentIdentity, err = decodeDynamicValue6(req.CurrentIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + readResp := p.provider.ReadResource(providers.ReadResourceRequest{ - TypeName: req.TypeName, - PriorState: stateVal, - Private: req.Private, - ProviderMeta: metaVal, + TypeName: req.TypeName, + PriorState: stateVal, + Private: req.Private, + ProviderMeta: metaVal, + CurrentIdentity: currentIdentity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, readResp.Diagnostics) if readResp.Diagnostics.HasErrors() { @@ -207,12 +272,27 @@ func (p *provider6) ReadResource(_ context.Context, req *tfplugin6.ReadResource_ } resp.NewState = dv + if !readResp.Identity.IsNull() { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + newIdentity, err := encodeDynamicValue6(readResp.Identity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.NewIdentity = &tfplugin6.ResourceIdentityData{ + IdentityData: newIdentity, + } + } + return resp, nil } func (p *provider6) PlanResourceChange(_ context.Context, req *tfplugin6.PlanResourceChange_Request) (*tfplugin6.PlanResourceChange_Response, error) { resp := &tfplugin6.PlanResourceChange_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + resSchema := p.schema.ResourceTypes[req.TypeName] + ty := resSchema.Body.ImpliedType() priorStateVal, err := decodeDynamicValue6(req.PriorState, ty) if err != nil { @@ -232,13 +312,26 @@ func (p *provider6) PlanResourceChange(_ context.Context, req *tfplugin6.PlanRes return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + var priorIdentity cty.Value + if req.PriorIdentity != nil && req.PriorIdentity.IdentityData != nil { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + priorIdentity, err = decodeDynamicValue6(req.PriorIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + planResp := p.provider.PlanResourceChange(providers.PlanResourceChangeRequest{ TypeName: req.TypeName, PriorState: priorStateVal, @@ -246,6 +339,7 @@ func (p *provider6) PlanResourceChange(_ context.Context, req *tfplugin6.PlanRes Config: configVal, PriorPrivate: req.PriorPrivate, ProviderMeta: metaVal, + PriorIdentity: priorIdentity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, planResp.Diagnostics) if planResp.Diagnostics.HasErrors() { @@ -264,12 +358,29 @@ func (p *provider6) PlanResourceChange(_ context.Context, req *tfplugin6.PlanRes resp.RequiresReplace = append(resp.RequiresReplace, convert.PathToAttributePath(path)) } + if !planResp.PlannedIdentity.IsNull() { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + plannedIdentityVal, err := encodeDynamicValue6(planResp.PlannedIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + resp.PlannedIdentity = &tfplugin6.ResourceIdentityData{ + IdentityData: plannedIdentityVal, + } + } + return resp, nil } func (p *provider6) ApplyResourceChange(_ context.Context, req *tfplugin6.ApplyResourceChange_Request) (*tfplugin6.ApplyResourceChange_Response, error) { resp := &tfplugin6.ApplyResourceChange_Response{} - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + resSchema := p.schema.ResourceTypes[req.TypeName] + ty := resSchema.Body.ImpliedType() priorStateVal, err := decodeDynamicValue6(req.PriorState, ty) if err != nil { @@ -289,20 +400,34 @@ func (p *provider6) ApplyResourceChange(_ context.Context, req *tfplugin6.ApplyR return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) return resp, nil } + var plannedIdentity cty.Value + if req.PlannedIdentity != nil && req.PlannedIdentity.IdentityData != nil { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + + plannedIdentity, err = decodeDynamicValue6(req.PlannedIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + applyResp := p.provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ - TypeName: req.TypeName, - PriorState: priorStateVal, - PlannedState: plannedStateVal, - Config: configVal, - PlannedPrivate: req.PlannedPrivate, - ProviderMeta: metaVal, + TypeName: req.TypeName, + PriorState: priorStateVal, + PlannedState: plannedStateVal, + Config: configVal, + PlannedPrivate: req.PlannedPrivate, + ProviderMeta: metaVal, + PlannedIdentity: plannedIdentity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, applyResp.Diagnostics) @@ -317,39 +442,138 @@ func (p *provider6) ApplyResourceChange(_ context.Context, req *tfplugin6.ApplyR return resp, nil } + if !applyResp.NewIdentity.IsNull() { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + newIdentity, err := encodeDynamicValue6(applyResp.NewIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.NewIdentity = &tfplugin6.ResourceIdentityData{ + IdentityData: newIdentity, + } + } + return resp, nil } func (p *provider6) ImportResourceState(_ context.Context, req *tfplugin6.ImportResourceState_Request) (*tfplugin6.ImportResourceState_Response, error) { resp := &tfplugin6.ImportResourceState_Response{} + resSchema := p.schema.ResourceTypes[req.TypeName] + + var identity cty.Value + var err error + if req.Identity != nil && req.Identity.IdentityData != nil { + if resSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", req.TypeName) + } + identity, err = decodeDynamicValue6(req.Identity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + } + importResp := p.provider.ImportResourceState(providers.ImportResourceStateRequest{ TypeName: req.TypeName, ID: req.Id, + Identity: identity, }) resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, importResp.Diagnostics) for _, res := range importResp.ImportedResources { - ty := p.schema.ResourceTypes[res.TypeName].Block.ImpliedType() + importSchema := p.schema.ResourceTypes[res.TypeName] + ty := importSchema.Body.ImpliedType() state, err := encodeDynamicValue6(res.State, ty) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) continue } - - resp.ImportedResources = append(resp.ImportedResources, &tfplugin6.ImportResourceState_ImportedResource{ + importedResource := &tfplugin6.ImportResourceState_ImportedResource{ TypeName: res.TypeName, State: state, Private: res.Private, - }) + } + if !res.Identity.IsNull() { + if importSchema.Identity == nil { + return nil, fmt.Errorf("identity schema not found for type %s", res.TypeName) + } + + identity, err := encodeDynamicValue6(res.Identity, importSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + continue + } + + importedResource.Identity = &tfplugin6.ResourceIdentityData{ + IdentityData: identity, + } + } + + resp.ImportedResources = append(resp.ImportedResources, importedResource) } return resp, nil } +func (p *provider6) MoveResourceState(_ context.Context, request *tfplugin6.MoveResourceState_Request) (*tfplugin6.MoveResourceState_Response, error) { + resp := &tfplugin6.MoveResourceState_Response{} + + var sourceIdentity []byte + var err error + if request.SourceIdentity != nil && len(request.SourceIdentity.Json) > 0 { + sourceIdentity = request.SourceIdentity.Json + } + + moveResp := p.provider.MoveResourceState(providers.MoveResourceStateRequest{ + SourceProviderAddress: request.SourceProviderAddress, + SourceTypeName: request.SourceTypeName, + SourceSchemaVersion: request.SourceSchemaVersion, + SourceStateJSON: request.SourceState.Json, + SourcePrivate: request.SourcePrivate, + TargetTypeName: request.TargetTypeName, + SourceIdentity: sourceIdentity, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, moveResp.Diagnostics) + if moveResp.Diagnostics.HasErrors() { + return resp, nil + } + + targetSchema := p.schema.ResourceTypes[request.TargetTypeName] + targetType := targetSchema.Body.ImpliedType() + targetState, err := encodeDynamicValue6(moveResp.TargetState, targetType) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + if !moveResp.TargetIdentity.IsNull() { + if targetSchema.Identity == nil { + return resp, fmt.Errorf("identity schema not found for type %s", request.TargetTypeName) + } + + targetIdentity, err := encodeDynamicValue6(moveResp.TargetIdentity, targetSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + resp.TargetIdentity = &tfplugin6.ResourceIdentityData{ + IdentityData: targetIdentity, + } + } + + resp.TargetState = targetState + resp.TargetPrivate = moveResp.TargetPrivate + return resp, nil +} + func (p *provider6) ReadDataSource(_ context.Context, req *tfplugin6.ReadDataSource_Request) (*tfplugin6.ReadDataSource_Response, error) { resp := &tfplugin6.ReadDataSource_Response{} - ty := p.schema.DataSources[req.TypeName].Block.ImpliedType() + ty := p.schema.DataSources[req.TypeName].Body.ImpliedType() configVal, err := decodeDynamicValue6(req.Config, ty) if err != nil { @@ -357,7 +581,7 @@ func (p *provider6) ReadDataSource(_ context.Context, req *tfplugin6.ReadDataSou return resp, nil } - metaTy := p.schema.ProviderMeta.Block.ImpliedType() + metaTy := p.schema.ProviderMeta.Body.ImpliedType() metaVal, err := decodeDynamicValue6(req.ProviderMeta, metaTy) if err != nil { resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) @@ -383,6 +607,183 @@ func (p *provider6) ReadDataSource(_ context.Context, req *tfplugin6.ReadDataSou return resp, nil } +func (p *provider6) OpenEphemeralResource(_ context.Context, req *tfplugin6.OpenEphemeralResource_Request) (*tfplugin6.OpenEphemeralResource_Response, error) { + resp := &tfplugin6.OpenEphemeralResource_Response{} + ty := p.schema.EphemeralResourceTypes[req.TypeName].Body.ImpliedType() + + configVal, err := decodeDynamicValue6(req.Config, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + openResp := p.provider.OpenEphemeralResource(providers.OpenEphemeralResourceRequest{ + TypeName: req.TypeName, + Config: configVal, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, openResp.Diagnostics) + if openResp.Diagnostics.HasErrors() { + return resp, nil + } + + resp.Result, err = encodeDynamicValue6(openResp.Result, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + resp.Private = openResp.Private + resp.RenewAt = timestamppb.New(openResp.RenewAt) + + return resp, nil +} + +func (p *provider6) RenewEphemeralResource(_ context.Context, req *tfplugin6.RenewEphemeralResource_Request) (*tfplugin6.RenewEphemeralResource_Response, error) { + resp := &tfplugin6.RenewEphemeralResource_Response{} + renewResp := p.provider.RenewEphemeralResource(providers.RenewEphemeralResourceRequest{ + TypeName: req.TypeName, + Private: req.Private, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, renewResp.Diagnostics) + if renewResp.Diagnostics.HasErrors() { + return resp, nil + } + + resp.Private = renewResp.Private + resp.RenewAt = timestamppb.New(renewResp.RenewAt) + return resp, nil +} + +func (p *provider6) CloseEphemeralResource(_ context.Context, req *tfplugin6.CloseEphemeralResource_Request) (*tfplugin6.CloseEphemeralResource_Response, error) { + resp := &tfplugin6.CloseEphemeralResource_Response{} + closeResp := p.provider.CloseEphemeralResource(providers.CloseEphemeralResourceRequest{ + TypeName: req.TypeName, + Private: req.Private, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, closeResp.Diagnostics) + if closeResp.Diagnostics.HasErrors() { + return resp, nil + } + + return resp, nil +} + +func (p *provider6) GetFunctions(context.Context, *tfplugin6.GetFunctions_Request) (*tfplugin6.GetFunctions_Response, error) { + panic("unimplemented") +} + +func (p *provider6) CallFunction(_ context.Context, req *tfplugin6.CallFunction_Request) (*tfplugin6.CallFunction_Response, error) { + var err error + resp := &tfplugin6.CallFunction_Response{} + + funcSchema := p.schema.Functions[req.Name] + + var args []cty.Value + if len(req.Arguments) != 0 { + args = make([]cty.Value, len(req.Arguments)) + for i, rawArg := range req.Arguments { + idx := int64(i) + + var argTy cty.Type + if i < len(funcSchema.Parameters) { + argTy = funcSchema.Parameters[i].Type + } else { + if funcSchema.VariadicParameter == nil { + resp.Error = &tfplugin6.FunctionError{ + Text: "too many arguments for non-variadic function", + FunctionArgument: &idx, + } + return resp, nil + } + argTy = funcSchema.VariadicParameter.Type + } + + argVal, err := decodeDynamicValue6(rawArg, argTy) + if err != nil { + resp.Error = &tfplugin6.FunctionError{ + Text: err.Error(), + FunctionArgument: &idx, + } + return resp, nil + } + + args[i] = argVal + } + } + + callResp := p.provider.CallFunction(providers.CallFunctionRequest{ + FunctionName: req.Name, + Arguments: args, + }) + if callResp.Err != nil { + resp.Error = &tfplugin6.FunctionError{ + Text: callResp.Err.Error(), + } + + if argErr, ok := callResp.Err.(function.ArgError); ok { + idx := int64(argErr.Index) + resp.Error.FunctionArgument = &idx + } + + return resp, nil + } + + resp.Result, err = encodeDynamicValue6(callResp.Result, funcSchema.ReturnType) + if err != nil { + resp.Error = &tfplugin6.FunctionError{ + Text: err.Error(), + } + + return resp, nil + } + + return resp, nil +} + +func (p *provider6) GetResourceIdentitySchemas(_ context.Context, req *tfplugin6.GetResourceIdentitySchemas_Request) (*tfplugin6.GetResourceIdentitySchemas_Response, error) { + resp := &tfplugin6.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*tfplugin6.ResourceIdentitySchema{}, + Diagnostics: []*tfplugin6.Diagnostic{}, + } + + for name, schema := range p.identitySchemas.IdentityTypes { + resp.IdentitySchemas[name] = convert.ResourceIdentitySchemaToProto(schema) + } + + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, p.identitySchemas.Diagnostics) + return resp, nil +} + +func (p *provider6) UpgradeResourceIdentity(_ context.Context, req *tfplugin6.UpgradeResourceIdentity_Request) (*tfplugin6.UpgradeResourceIdentity_Response, error) { + resp := &tfplugin6.UpgradeResourceIdentity_Response{} + resource, ok := p.identitySchemas.IdentityTypes[req.TypeName] + if !ok { + return nil, fmt.Errorf("resource identity schema not found for type %q", req.TypeName) + } + ty := resource.Body.ImpliedType() + + upgradeResp := p.provider.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: req.TypeName, + Version: req.Version, + RawIdentityJSON: req.RawIdentity.Json, + }) + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, upgradeResp.Diagnostics) + + if upgradeResp.Diagnostics.HasErrors() { + return resp, nil + } + + dv, err := encodeDynamicValue6(upgradeResp.UpgradedIdentity, ty) + if err != nil { + resp.Diagnostics = convert.AppendProtoDiag(resp.Diagnostics, err) + return resp, nil + } + + resp.UpgradedIdentity = &tfplugin6.ResourceIdentityData{ + IdentityData: dv, + } + return resp, nil +} + func (p *provider6) StopProvider(context.Context, *tfplugin6.StopProvider_Request) (*tfplugin6.StopProvider_Response, error) { resp := &tfplugin6.StopProvider_Response{} err := p.provider.Stop() diff --git a/internal/grpcwrap/provisioner.go b/internal/grpcwrap/provisioner.go index ef265248a6..f813f99dd8 100644 --- a/internal/grpcwrap/provisioner.go +++ b/internal/grpcwrap/provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package grpcwrap import ( diff --git a/internal/helper/slowmessage/slowmessage.go b/internal/helper/slowmessage/slowmessage.go index e4e1471061..6b9f6e5eeb 100644 --- a/internal/helper/slowmessage/slowmessage.go +++ b/internal/helper/slowmessage/slowmessage.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package slowmessage import ( diff --git a/internal/helper/slowmessage/slowmessage_test.go b/internal/helper/slowmessage/slowmessage_test.go index 32658aaccc..4bea94c500 100644 --- a/internal/helper/slowmessage/slowmessage_test.go +++ b/internal/helper/slowmessage/slowmessage_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package slowmessage import ( diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index bb06beb470..7dcdfdd004 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package httpclient import ( diff --git a/internal/httpclient/client_test.go b/internal/httpclient/client_test.go index e3f97c299d..9c262eeb9f 100644 --- a/internal/httpclient/client_test.go +++ b/internal/httpclient/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package httpclient import ( diff --git a/internal/httpclient/useragent.go b/internal/httpclient/useragent.go index d6aba31d40..45b715fd8c 100644 --- a/internal/httpclient/useragent.go +++ b/internal/httpclient/useragent.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package httpclient import ( diff --git a/internal/httpclient/useragent_test.go b/internal/httpclient/useragent_test.go index f5aa47c5fb..c93b3bbd95 100644 --- a/internal/httpclient/useragent_test.go +++ b/internal/httpclient/useragent_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package httpclient import ( diff --git a/internal/initwd/doc.go b/internal/initwd/doc.go index b9d938dbb0..9d56a4ac39 100644 --- a/internal/initwd/doc.go +++ b/internal/initwd/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package initwd contains various helper functions used by the "terraform init" // command to initialize a working directory. // diff --git a/internal/initwd/from_module.go b/internal/initwd/from_module.go index 38a4e49b5c..7d54943dd1 100644 --- a/internal/initwd/from_module.go +++ b/internal/initwd/from_module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package initwd import ( @@ -10,24 +13,27 @@ import ( "sort" "strings" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/copy" - "github.com/hashicorp/terraform/internal/earlyconfig" "github.com/hashicorp/terraform/internal/getmodules" + "github.com/hashicorp/terraform/internal/getmodules/moduleaddrs" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" "github.com/hashicorp/terraform/internal/modsdir" "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/tfdiags" ) const initFromModuleRootCallName = "root" +const initFromModuleRootFilename = "
" const initFromModuleRootKeyPrefix = initFromModuleRootCallName + "." // DirFromModule populates the given directory (which must exist and be // empty) with the contents of the module at the given source address. // -// It does this by installing the given module and all of its descendent +// It does this by installing the given module and all of its descendant // modules in a temporary root directory and then copying the installed // files into suitable locations. As a consequence, any diagnostics it // generates will reveal the location of this temporary directory to the @@ -37,12 +43,13 @@ const initFromModuleRootKeyPrefix = initFromModuleRootCallName + "." // installation proceeds in a manner identical to normal module installation. // // If the given source address specifies a sub-directory of the given -// package then only the sub-directory and its descendents will be copied +// package then only the sub-directory and its descendants will be copied // into the given root directory, which will cause any relative module // references using ../ from that module to be unresolvable. Error diagnostics // are produced in that case, to prompt the user to rewrite the source strings // to be absolute references to the original remote module. -func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, reg *registry.Client, hooks ModuleInstallHooks) tfdiags.Diagnostics { +func DirFromModule(ctx context.Context, loader *configload.Loader, rootDir, modulesDir, sourceAddrStr string, reg *registry.Client, hooks ModuleInstallHooks) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics // The way this function works is pretty ugly, but we accept it because @@ -87,8 +94,8 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, } instDir := filepath.Join(rootDir, ".terraform/init-from-module") - inst := NewModuleInstaller(instDir, reg) - log.Printf("[DEBUG] installing modules in %s to initialize working directory from %q", instDir, sourceAddr) + inst := NewModuleInstaller(instDir, loader, reg) + log.Printf("[DEBUG] installing modules in %s to initialize working directory from %q", instDir, sourceAddrStr) os.RemoveAll(instDir) // if this fails then we'll fail on MkdirAll below too err := os.MkdirAll(instDir, os.ModePerm) if err != nil { @@ -103,12 +110,6 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, instManifest := make(modsdir.Manifest) retManifest := make(modsdir.Manifest) - fakeFilename := fmt.Sprintf("-from-module=%q", sourceAddr) - fakePos := tfconfig.SourcePos{ - Filename: fakeFilename, - Line: 1, - } - // -from-module allows relative paths but it's different than a normal // module address where it'd be resolved relative to the module call // (which is synthetic, here.) To address this, we'll just patch up any @@ -117,25 +118,38 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, // that the result will be "downloaded" with go-getter (copied from the // source location), rather than just recorded as a relative path. { - maybePath := filepath.ToSlash(sourceAddr) + maybePath := filepath.ToSlash(sourceAddrStr) if maybePath == "." || strings.HasPrefix(maybePath, "./") || strings.HasPrefix(maybePath, "../") { if wd, err := os.Getwd(); err == nil { - sourceAddr = filepath.Join(wd, sourceAddr) - log.Printf("[TRACE] -from-module relative path rewritten to absolute path %s", sourceAddr) + sourceAddrStr = filepath.Join(wd, sourceAddrStr) + log.Printf("[TRACE] -from-module relative path rewritten to absolute path %s", sourceAddrStr) } } } // Now we need to create an artificial root module that will seed our // installation process. - fakeRootModule := &tfconfig.Module{ - ModuleCalls: map[string]*tfconfig.ModuleCall{ + sourceAddr, err := moduleaddrs.ParseModuleSource(sourceAddrStr) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid module source address", + fmt.Sprintf("Failed to parse module source address: %s", err), + )) + } + fakeRootModule := &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{ initFromModuleRootCallName: { - Name: initFromModuleRootCallName, - Source: sourceAddr, - Pos: fakePos, + Name: initFromModuleRootCallName, + SourceAddr: sourceAddr, + DeclRange: hcl.Range{ + Filename: initFromModuleRootFilename, + Start: hcl.InitialPos, + End: hcl.InitialPos, + }, }, }, + ProviderRequirements: &configs.RequiredProviders{}, } // wrapHooks filters hook notifications to only include Download calls @@ -144,11 +158,18 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, wrapHooks := installHooksInitDir{ Wrapped: hooks, } + // Create a manifest record for the root module. This will be used if + // there are any relative-pathed modules in the root. + instManifest[""] = modsdir.Record{ + Key: "", + Dir: rootDir, + } fetcher := getmodules.NewPackageFetcher() - _, instDiags := inst.installDescendentModules(ctx, fakeRootModule, rootDir, instManifest, true, wrapHooks, fetcher) - diags = append(diags, instDiags...) - if instDiags.HasErrors() { - return diags + + walker := inst.moduleInstallWalker(ctx, instManifest, true, wrapHooks, fetcher) + _, cDiags := inst.installDescendantModules(fakeRootModule, instManifest, walker, true) + if cDiags.HasErrors() { + return diags.Append(cDiags) } // If all of that succeeded then we'll now migrate what was installed @@ -181,7 +202,7 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Failed to copy root module", - fmt.Sprintf("Error copying root module %q from %s to %s: %s.", sourceAddr, record.Dir, rootDir, err), + fmt.Sprintf("Error copying root module %q from %s to %s: %s.", sourceAddrStr, record.Dir, rootDir, err), )) continue } @@ -191,12 +212,12 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, // and must thus be rewritten to be absolute addresses again. // For now we can't do this rewriting automatically, but we'll // generate an error to help the user do it manually. - mod, _ := earlyconfig.LoadModule(rootDir) // ignore diagnostics since we're just doing value-add here anyway + mod, _ := loader.Parser().LoadConfigDir(rootDir) // ignore diagnostics since we're just doing value-add here anyway if mod != nil { for _, mc := range mod.ModuleCalls { - if pathTraversesUp(mc.Source) { - packageAddr, givenSubdir := getmodules.SplitPackageSubdir(sourceAddr) - newSubdir := filepath.Join(givenSubdir, mc.Source) + if pathTraversesUp(mc.SourceAddrRaw) { + packageAddr, givenSubdir := moduleaddrs.SplitPackageSubdir(sourceAddrStr) + newSubdir := filepath.Join(givenSubdir, mc.SourceAddrRaw) if pathTraversesUp(newSubdir) { // This should never happen in any reasonable // configuration since this suggests a path that @@ -214,7 +235,7 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Root module references parent directory", - fmt.Sprintf("The requested module %q refers to a module via its parent directory. To use this as a new root module this source string must be rewritten as a remote source address, such as %q.", sourceAddr, newAddr), + fmt.Sprintf("The requested module %q refers to a module via its parent directory. To use this as a new root module this source string must be rewritten as a remote source address, such as %q.", sourceAddrStr, newAddr), )) continue } @@ -231,7 +252,7 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, if !strings.HasPrefix(record.Key, initFromModuleRootKeyPrefix) { // Ignore the *real* root module, whose key is empty, since // we're only interested in the module named "root" and its - // descendents. + // descendants. continue } @@ -331,7 +352,7 @@ func DirFromModule(ctx context.Context, rootDir, modulesDir, sourceAddr string, if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Failed to copy descendent module", + "Failed to copy descendant module", fmt.Sprintf("Error copying module %q from %s to %s: %s.", newKey, tempPath, rootDir, err), )) continue diff --git a/internal/initwd/from_module_test.go b/internal/initwd/from_module_test.go index 9714ed3465..5d2209e628 100644 --- a/internal/initwd/from_module_test.go +++ b/internal/initwd/from_module_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package initwd import ( @@ -38,14 +41,16 @@ func TestDirFromModule_registry(t *testing.T) { hooks := &testInstallHooks{} reg := registry.NewClient(nil, nil) - diags := DirFromModule(context.Background(), dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks) - assertNoDiagnostics(t, diags) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, dir, modsDir, "hashicorp/module-installer-acctest/aws//examples/main", reg, hooks) + tfdiags.AssertNoDiagnostics(t, diags) v := version.Must(version.NewVersion("0.0.2")) wantCalls := []testInstallHookCall{ // The module specified to populate the root directory is not mentioned - // here, because the hook mechanism is defined to talk about descendent + // here, because the hook mechanism is defined to talk about descendant // modules only and so a caller to InitDirFromModule is expected to // produce its own user-facing announcement about the root module being // installed. @@ -93,7 +98,7 @@ func TestDirFromModule_registry(t *testing.T) { t.Fatalf("wrong installer calls\n%s", diff) } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modsDir, }) if err != nil { @@ -103,9 +108,7 @@ func TestDirFromModule_registry(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) { - return - } + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) wantTraces := map[string]string{ "": "in example", @@ -154,8 +157,10 @@ func TestDirFromModule_submodules(t *testing.T) { } modInstallDir := filepath.Join(dir, ".terraform/modules") - diags := DirFromModule(context.Background(), dir, modInstallDir, fromModuleDir, nil, hooks) - assertNoDiagnostics(t, diags) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, dir, modInstallDir, fromModuleDir, nil, hooks) + tfdiags.AssertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ { Name: "Install", @@ -173,7 +178,7 @@ func TestDirFromModule_submodules(t *testing.T) { return } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modInstallDir, }) if err != nil { @@ -183,9 +188,8 @@ func TestDirFromModule_submodules(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) { - return - } + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + wantTraces := map[string]string{ "": "in root module", "child_a": "in child_a module", @@ -205,6 +209,38 @@ func TestDirFromModule_submodules(t *testing.T) { assertResultDeepEqual(t, gotTraces, wantTraces) } +// submodulesWithProvider is identical to above, except that the configuration +// would fail to load for some reason. We still want the module to be installed +// for use cases like testing or CDKTF, and will only emit warnings for config +// errors. +func TestDirFromModule_submodulesWithProvider(t *testing.T) { + fixtureDir := filepath.Clean("testdata/empty") + fromModuleDir, err := filepath.Abs("./testdata/local-module-missing-provider") + if err != nil { + t.Fatal(err) + } + + tmpDir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + dir, err := filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Error(err) + } + modInstallDir := filepath.Join(dir, ".terraform/modules") + + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, dir, modInstallDir, fromModuleDir, nil, hooks) + + for _, d := range diags { + if d.Severity() != tfdiags.Warning { + t.Errorf("expected warning, got %v", diags.Err()) + } + } +} + // TestDirFromModule_rel_submodules is similar to the test above, but the // from-module is relative to the install dir ("../"): // https://github.com/hashicorp/terraform/issues/23010 @@ -246,8 +282,10 @@ func TestDirFromModule_rel_submodules(t *testing.T) { modInstallDir := ".terraform/modules" sourceDir := "../local-modules" - diags := DirFromModule(context.Background(), ".", modInstallDir, sourceDir, nil, hooks) - assertNoDiagnostics(t, diags) + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + diags := DirFromModule(context.Background(), loader, ".", modInstallDir, sourceDir, nil, hooks) + tfdiags.AssertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ { Name: "Install", @@ -265,7 +303,7 @@ func TestDirFromModule_rel_submodules(t *testing.T) { return } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modInstallDir, }) if err != nil { @@ -275,9 +313,8 @@ func TestDirFromModule_rel_submodules(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - if assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) { - return - } + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + wantTraces := map[string]string{ "": "in root module", "child_a": "in child_a module", diff --git a/internal/initwd/load_config.go b/internal/initwd/load_config.go deleted file mode 100644 index 6dc032ba17..0000000000 --- a/internal/initwd/load_config.go +++ /dev/null @@ -1,56 +0,0 @@ -package initwd - -import ( - "fmt" - - version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" - "github.com/hashicorp/terraform/internal/earlyconfig" - "github.com/hashicorp/terraform/internal/modsdir" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// LoadConfig loads a full configuration tree that has previously had all of -// its dependent modules installed to the given modulesDir using a -// ModuleInstaller. -// -// This uses the early configuration loader and thus only reads top-level -// metadata from the modules in the configuration. Most callers should use -// the configs/configload package to fully load a configuration. -func LoadConfig(rootDir, modulesDir string) (*earlyconfig.Config, tfdiags.Diagnostics) { - rootMod, diags := earlyconfig.LoadModule(rootDir) - if rootMod == nil { - return nil, diags - } - - manifest, err := modsdir.ReadManifestSnapshotForDir(modulesDir) - if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to read module manifest", - fmt.Sprintf("Terraform failed to read its manifest of locally-cached modules: %s.", err), - )) - return nil, diags - } - - return earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( - func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - key := manifest.ModuleKey(req.Path) - record, exists := manifest[key] - if !exists { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module not installed", - fmt.Sprintf("Module %s is not yet installed. Run \"terraform init\" to install all modules required by this configuration.", req.Path.String()), - )) - return nil, nil, diags - } - - mod, mDiags := earlyconfig.LoadModule(record.Dir) - diags = diags.Append(mDiags) - return mod, record.Version, diags - }, - )) -} diff --git a/internal/initwd/module_install.go b/internal/initwd/module_install.go index adc5dec5ec..9672d14227 100644 --- a/internal/initwd/module_install.go +++ b/internal/initwd/module_install.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package initwd import ( @@ -10,11 +13,16 @@ import ( "path/filepath" "strings" + "github.com/apparentlymart/go-versions/versions" version "github.com/hashicorp/go-version" - "github.com/hashicorp/terraform-config-inspect/tfconfig" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/earlyconfig" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/getmodules" + "github.com/hashicorp/terraform/internal/getmodules/moduleaddrs" "github.com/hashicorp/terraform/internal/modsdir" "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/registry/regsrc" @@ -24,6 +32,7 @@ import ( type ModuleInstaller struct { modsDir string + loader *configload.Loader reg *registry.Client // The keys in moduleVersions are resolved and trimmed registry source @@ -40,9 +49,10 @@ type moduleVersion struct { version string } -func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { +func NewModuleInstaller(modsDir string, loader *configload.Loader, reg *registry.Client) *ModuleInstaller { return &ModuleInstaller{ modsDir: modsDir, + loader: loader, reg: reg, registryPackageVersions: make(map[addrs.ModuleRegistryPackage]*response.ModuleVersions), registryPackageSources: make(map[moduleVersion]addrs.ModuleSourceRemote), @@ -69,6 +79,12 @@ func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { // needs to replace a directory that is already present with a newly-extracted // package. // +// installErrsOnly installs modules but converts validation errors from +// building the configuration after installation to warnings. This is used by +// commands like `get` or `init -from-module` where the established behavior +// was only to install the requested module, and extra validation can break +// compatibility. +// // If the returned diagnostics contains errors then the module installation // may have wholly or partially completed. Modules must be loaded in order // to find their dependencies, so this function does many of the same checks @@ -77,12 +93,23 @@ func NewModuleInstaller(modsDir string, reg *registry.Client) *ModuleInstaller { // If successful (the returned diagnostics contains no errors) then the // first return value is the early configuration tree that was constructed by // the installation process. -func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, upgrade bool, hooks ModuleInstallHooks) (*earlyconfig.Config, tfdiags.Diagnostics) { +func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir, testsDir string, upgrade, installErrsOnly bool, hooks ModuleInstallHooks) (*configs.Config, tfdiags.Diagnostics) { log.Printf("[TRACE] ModuleInstaller: installing child modules for %s into %s", rootDir, i.modsDir) + var diags tfdiags.Diagnostics - rootMod, diags := earlyconfig.LoadModule(rootDir) + rootMod, mDiags := i.loader.Parser().LoadConfigDirWithTests(rootDir, testsDir) if rootMod == nil { + // We drop the diagnostics here because we only want to report module + // loading errors after checking the core version constraints, which we + // can only do if the module can be at least partially loaded. return nil, diags + } else if vDiags := rootMod.CheckCoreVersionRequirements(nil, nil); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Append(vDiags) + } else { + diags = diags.Append(mDiags) } manifest, err := modsdir.ReadManifestSnapshotForDir(i.modsDir) @@ -96,14 +123,6 @@ func (i *ModuleInstaller) InstallModules(ctx context.Context, rootDir string, up } fetcher := getmodules.NewPackageFetcher() - cfg, instDiags := i.installDescendentModules(ctx, rootMod, rootDir, manifest, upgrade, hooks, fetcher) - diags = append(diags, instDiags...) - - return cfg, diags -} - -func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod *tfconfig.Module, rootDir string, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*earlyconfig.Config, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics if hooks == nil { // Use our no-op implementation as a placeholder @@ -116,9 +135,41 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod Key: "", Dir: rootDir, } + walker := i.moduleInstallWalker(ctx, manifest, upgrade, hooks, fetcher) - cfg, cDiags := earlyconfig.BuildConfig(rootMod, earlyconfig.ModuleWalkerFunc( - func(req *earlyconfig.ModuleRequest) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { + cfg, instDiags := i.installDescendantModules(rootMod, manifest, walker, installErrsOnly) + diags = append(diags, instDiags...) + + return cfg, diags +} + +func (i *ModuleInstaller) moduleInstallWalker(ctx context.Context, manifest modsdir.Manifest, upgrade bool, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) configs.ModuleWalker { + return configs.ModuleWalkerFunc( + func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) { + var diags hcl.Diagnostics + + if req.SourceAddr == nil { + // If the parent module failed to parse the module source + // address, we can't load it here. Return nothing as the parent + // module's diagnostics should explain this. + return nil, nil, diags + } + + if req.Name == "" { + // An empty string for a module instance name breaks our + // manifest map, which uses that to indicate the root module. + // Because we descend into modules which have errors, we need + // to look out for this case, but the config loader's + // diagnostics will report the error later. + return nil, nil, diags + } + + if !hclsyntax.ValidIdentifier(req.Name) { + // A module with an invalid name shouldn't be installed at all. This is + // mostly a concern for remote modules, since we need to be able to convert + // the name to a valid path. + return nil, nil, diags + } key := manifest.ModuleKey(req.Path) instPath := i.packageInstallPath(req.Path) @@ -137,8 +188,8 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod case record.SourceAddr != req.SourceAddr.String(): log.Printf("[TRACE] ModuleInstaller: %s source address has changed from %q to %q", key, record.SourceAddr, req.SourceAddr) replace = true - case record.Version != nil && !req.VersionConstraints.Check(record.Version): - log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraints) + case record.Version != nil && !req.VersionConstraint.Required.Check(record.Version): + log.Printf("[TRACE] ModuleInstaller: %s version %s no longer compatible with constraints %s", key, record.Version, req.VersionConstraint.Required) replace = true } } @@ -151,7 +202,7 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod log.Printf("[TRACE] ModuleInstaller: discarding previous record of %s prior to reinstall", key) } delete(manifest, key) - // Deleting a module invalidates all of its descendent modules too. + // Deleting a module invalidates all of its descendant modules too. keyPrefix := key + "." for subKey := range manifest { if strings.HasPrefix(subKey, keyPrefix) { @@ -172,14 +223,14 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod err := os.RemoveAll(instPath) if err != nil && !os.IsNotExist(err) { log.Printf("[TRACE] ModuleInstaller: failed to remove %s: %s", key, err) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to remove local module cache", - fmt.Sprintf( + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to remove local module cache", + Detail: fmt.Sprintf( "Terraform tried to remove %s in order to reinstall this module, but encountered an error: %s", instPath, err, ), - )) + }) return nil, nil, diags } } else { @@ -188,8 +239,19 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod // keep our existing record. info, err := os.Stat(record.Dir) if err == nil && info.IsDir() { - mod, mDiags := earlyconfig.LoadModule(record.Dir) - diags = diags.Append(mDiags) + mod, mDiags := i.loader.Parser().LoadConfigDir(record.Dir) + if mod == nil { + // nil indicates an unreadable module, which should never happen, + // so we return the full loader diagnostics here. + diags = diags.Extend(mDiags) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) + } else { + diags = diags.Extend(mDiags) + } log.Printf("[TRACE] ModuleInstaller: Module installer: %s %s already installed in %s", key, record.Version, record.Dir) return mod, record.Version, diags @@ -226,10 +288,49 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod // of addrs.ModuleSource. panic(fmt.Sprintf("unsupported module source address %#v", addr)) } - }, - )) - diags = append(diags, cDiags...) + ) +} + +func (i *ModuleInstaller) installDescendantModules(rootMod *configs.Module, manifest modsdir.Manifest, installWalker configs.ModuleWalker, installErrsOnly bool) (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // When attempting to initialize the current directory with a module + // source, some use cases may want to ignore configuration errors from the + // building of the entire configuration structure, but we still need to + // capture installation errors. Because the actual module installation + // happens in the ModuleWalkFunc callback while building the config, we + // need to create a closure to capture the installation diagnostics + // separately. + var instDiags hcl.Diagnostics + walker := installWalker + if installErrsOnly { + walker = configs.ModuleWalkerFunc(func(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) { + mod, version, diags := installWalker.LoadModule(req) + instDiags = instDiags.Extend(diags) + return mod, version, diags + }) + } + + cfg, cDiags := configs.BuildConfig(rootMod, walker, configs.MockDataLoaderFunc(i.loader.LoadExternalMockData)) + diags = diags.Append(cDiags) + if installErrsOnly { + // We can't continue if there was an error during installation, but + // return all diagnostics in case there happens to be anything else + // useful when debugging the problem. Any instDiags will be included in + // diags already. + if instDiags.HasErrors() { + return cfg, diags + } + + // If there are any errors here, they must be only from building the + // config structures. We don't want to block initialization at this + // point, so convert these into warnings. Any actual errors in the + // configuration will be raised as soon as the config is loaded again. + // We continue below because writing the manifest is required to finish + // module installation. + diags = tfdiags.OverrideAll(diags, tfdiags.Warning, nil) + } err := manifest.WriteSnapshotToDir(i.modsDir) if err != nil { @@ -243,8 +344,8 @@ func (i *ModuleInstaller) installDescendentModules(ctx context.Context, rootMod return cfg, diags } -func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*tfconfig.Module, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics +func (i *ModuleInstaller) installLocalModule(req *configs.ModuleRequest, key string, manifest modsdir.Manifest, hooks ModuleInstallHooks) (*configs.Module, hcl.Diagnostics) { + var diags hcl.Diagnostics parentKey := manifest.ModuleKey(req.Parent.Path) parentRecord, recorded := manifest[parentKey] @@ -253,12 +354,13 @@ func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key panic(fmt.Errorf("missing manifest record for parent module %s", parentKey)) } - if len(req.VersionConstraints) != 0 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid version constraint", - fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + if len(req.VersionConstraint.Required) != 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it has a relative local path.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) } // For local sources we don't actually need to modify the @@ -270,25 +372,31 @@ func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key // it is possible that the local directory is a symlink newDir, err := filepath.EvalSymlinks(newDir) if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("Unable to evaluate directory symlink: %s", err.Error()), + }) } - mod, mDiags := earlyconfig.LoadModule(newDir) + // Finally we are ready to try actually loading the module. + mod, mDiags := i.loader.Parser().LoadConfigDir(newDir) if mod == nil { // nil indicates missing or unreadable directory, so we'll // discard the returned diags and return a more specific // error message here. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read for module %q at %s:%d.", newDir, req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + }) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) } else { - diags = diags.Append(mDiags) + diags = diags.Extend(mDiags) } // Note the local location in our manifest. @@ -303,8 +411,8 @@ func (i *ModuleInstaller) installLocalModule(req *earlyconfig.ModuleRequest, key return mod, diags } -func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyconfig.ModuleRequest, key string, instPath string, addr addrs.ModuleSourceRegistry, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*tfconfig.Module, *version.Version, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics +func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *configs.ModuleRequest, key string, instPath string, addr addrs.ModuleSourceRegistry, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Module, *version.Version, hcl.Diagnostics) { + var diags hcl.Diagnostics hostname := addr.Package.Host reg := i.reg @@ -329,23 +437,25 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc resp, err = reg.ModuleVersions(ctx, regsrcAddr) if err != nil { if registry.IsModuleNotFound(err) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module not found", - fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module not found", + Detail: fmt.Sprintf("Module %q (from %s:%d) cannot be found in the module registry at %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, hostname), + Subject: req.CallRange.Ptr(), + }) } else if errors.Is(err, context.Canceled) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module installation was interrupted", - fmt.Sprintf("Received interrupt signal while retrieving available versions for module %q.", req.Name), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module installation was interrupted", + Detail: fmt.Sprintf("Received interrupt signal while retrieving available versions for module %q.", req.Name), + }) } else { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error accessing remote module registry", - fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, hostname, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error accessing remote module registry", + Detail: fmt.Sprintf("Failed to retrieve available versions for module %q (%s:%d) from %s: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, hostname, err), + Subject: req.CallRange.Ptr(), + }) } return nil, nil, diags } @@ -359,11 +469,12 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc if len(resp.Modules) < 1 { // Should never happen, but since this is a remote service that may // be implemented by third-parties we will handle it gracefully. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid response from remote module registry", - fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid response from remote module registry", + Detail: fmt.Sprintf("The registry at %s returned an invalid response when Terraform requested available versions for module %q (%s:%d).", hostname, req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } @@ -377,26 +488,63 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc // Should never happen if the registry server is compliant with // the protocol, but we'll warn if not to assist someone who // might be developing a module registry server. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - "Invalid response from remote module registry", - fmt.Sprintf("The registry at %s returned an invalid version string %q for module %q (%s:%d), which Terraform ignored.", hostname, mv.Version, req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Invalid response from remote module registry", + Detail: fmt.Sprintf("The registry at %s returned an invalid version string %q for module %q (%s:%d), which Terraform ignored.", hostname, mv.Version, req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) continue } // If we've found a pre-release version then we'll ignore it unless // it was exactly requested. - if v.Prerelease() != "" && req.VersionConstraints.String() != v.String() { - log.Printf("[TRACE] ModuleInstaller: %s ignoring %s because it is a pre-release and was not requested exactly", key, v) - continue + // + // The prerelease checking will be handled by a different library for + // 2 reasons. First, this other library automatically includes the + // "prerelease versions must be exactly requested" behaviour that we are + // looking for. Second, this other library is used to handle all version + // constraints for the provider logic and this is the first step to + // making the module and provider version logic match. + if v.Prerelease() != "" { + // At this point all versions published by the module with + // prerelease metadata will be checked. Users may not have even + // requested this prerelease so don't print lots of unnecessary # + // warnings. + acceptableVersions, err := versions.MeetingConstraintsString(req.VersionConstraint.Required.String()) + if err != nil { + log.Printf("[WARN] ModuleInstaller: %s ignoring %s because the version constraints (%s) could not be parsed: %s", key, v, req.VersionConstraint.Required.String(), err.Error()) + continue + } + + // Validate the version is also readable by the other versions + // library. + version, err := versions.ParseVersion(v.String()) + if err != nil { + log.Printf("[WARN] ModuleInstaller: %s ignoring %s because the version (%s) reported by the module could not be parsed: %s", key, v, v.String(), err.Error()) + continue + } + + // Finally, check if the prerelease is acceptable to version. As + // highlighted previously, we go through all of this because the + // apparentlymart/go-versions library handles prerelease constraints + // in the apporach we want to. + if !acceptableVersions.Has(version) { + log.Printf("[TRACE] ModuleInstaller: %s ignoring %s because it is a pre-release and was not requested exactly", key, v) + continue + } + + // If we reach here, it means this prerelease version was exactly + // requested according to the extra constraints of this library. + // We fall through and allow the other library to also validate it + // for consistency. } if latestVersion == nil || v.GreaterThan(latestVersion) { latestVersion = v } - if req.VersionConstraints.Check(v) { + if req.VersionConstraint.Required.Check(v) { if latestMatch == nil || v.GreaterThan(latestMatch) { latestMatch = v } @@ -404,20 +552,22 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc } if latestVersion == nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module has no versions", - fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallPos.Filename, req.CallPos.Line, hostname), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module has no versions", + Detail: fmt.Sprintf("Module %q (%s:%d) has no versions available on %s.", addr, req.CallRange.Filename, req.CallRange.Start.Line, hostname), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } if latestMatch == nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unresolvable module version constraint", - fmt.Sprintf("There is no available version of module %q (%s:%d) which matches the given version constraint. The newest available version is %s.", addr, req.CallPos.Filename, req.CallPos.Line, latestVersion), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unresolvable module version constraint", + Detail: fmt.Sprintf("There is no available version of module %q (%s:%d) which matches the given version constraint. The newest available version is %s.", addr, req.CallRange.Filename, req.CallRange.Start.Line, latestVersion), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } @@ -434,20 +584,20 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc realAddrRaw, err := reg.ModuleLocation(ctx, regsrcAddr, latestMatch.String()) if err != nil { log.Printf("[ERROR] %s from %s %s: %s", key, addr, latestMatch, err) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Error accessing remote module registry", - fmt.Sprintf("Failed to retrieve a download URL for %s %s from %s: %s", addr, latestMatch, hostname, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error accessing remote module registry", + Detail: fmt.Sprintf("Failed to retrieve a download URL for %s %s from %s: %s", addr, latestMatch, hostname, err), + }) return nil, nil, diags } - realAddr, err := addrs.ParseModuleSource(realAddrRaw) + realAddr, err := moduleaddrs.ParseModuleSource(realAddrRaw) if err != nil { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid package location from module registry", - fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: %s.", hostname, realAddrRaw, addr, latestMatch, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid package location from module registry", + Detail: fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: %s.", hostname, realAddrRaw, addr, latestMatch, err), + }) return nil, nil, diags } switch realAddr := realAddr.(type) { @@ -458,11 +608,11 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc case addrs.ModuleSourceRemote: i.registryPackageSources[moduleAddr] = realAddr default: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid package location from module registry", - fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: must be a direct remote package address.", hostname, realAddrRaw, addr, latestMatch), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid package location from module registry", + Detail: fmt.Sprintf("Module registry %s returned invalid source location %q for %s %s: must be a direct remote package address.", hostname, realAddrRaw, addr, latestMatch), + }) return nil, nil, diags } } @@ -473,11 +623,11 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc err := fetcher.FetchPackage(ctx, instPath, dlAddr.Package.String()) if errors.Is(err, context.Canceled) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module download was interrupted", - fmt.Sprintf("Interrupt signal received when downloading module %s.", addr), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module download was interrupted", + Detail: fmt.Sprintf("Interrupt signal received when downloading module %s.", addr), + }) return nil, nil, diags } if err != nil { @@ -486,11 +636,12 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc // we have no way to recognize any specific errors to improve them // and masking the error entirely would hide valuable diagnostic // information from the user. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to download module", - fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallPos.Filename, req.CallPos.Line, dlAddr, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to download module", + Detail: fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, dlAddr, err), + Subject: req.CallRange.Ptr(), + }) return nil, nil, diags } @@ -506,20 +657,54 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc log.Printf("[TRACE] ModuleInstaller: %s should now be at %s", key, modDir) // Finally we are ready to try actually loading the module. - mod, mDiags := earlyconfig.LoadModule(modDir) + mod, mDiags := i.loader.Parser().LoadConfigDir(modDir) if mod == nil { - // nil indicates missing or unreadable directory, so we'll - // discard the returned diags and return a more specific - // error message here. For registry modules this actually - // indicates a bug in the code above, since it's not the - // user's responsibility to create the directory in this case. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), - )) + // a nil module indicates a missing or unreadable directory, typically + // this would indicate that Terraform has done something wrong. + // However, if the subDir is not empty then it is possible that the + // module was properly downloaded but the user is trying to read a + // subdirectory that doesn't exist. In this case, it's not a problem + // with Terraform. + if len(subDir) > 0 { + // Let's make this error message as precise as possible. + _, instErr := os.Stat(instPath) + _, subErr := os.Stat(modDir) + if instErr == nil && os.IsNotExist(subErr) { + // Then the root directory the module was downloaded to could + // be loaded fine, but the subdirectory does not exist. This + // definitely means the user is trying to read a subdirectory + // that doesn't exist. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module subdirectory", + Detail: fmt.Sprintf("The directory %s does not exist. The target submodule %s does not exist within the target module.", modDir, subDir), + }) + } else { + // There's something else gone wrong here, so we'll report it + // as a bug in Terraform. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), + }) + } + } else { + // If there is no subDir, then somehow the module was downloaded but + // could not be read even at the root directory it was downloaded into. + // This is definitely something that Terraform is doing wrong. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), + }) + } + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) } else { - diags = append(diags, mDiags...) + diags = diags.Extend(mDiags) } // Note the local location in our manifest. @@ -535,20 +720,21 @@ func (i *ModuleInstaller) installRegistryModule(ctx context.Context, req *earlyc return mod, latestMatch, diags } -func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *earlyconfig.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*tfconfig.Module, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics +func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *configs.ModuleRequest, key string, instPath string, manifest modsdir.Manifest, hooks ModuleInstallHooks, fetcher *getmodules.PackageFetcher) (*configs.Module, hcl.Diagnostics) { + var diags hcl.Diagnostics // Report up to the caller that we're about to start downloading. addr := req.SourceAddr.(addrs.ModuleSourceRemote) packageAddr := addr.Package hooks.Download(key, packageAddr.String(), nil) - if len(req.VersionConstraints) != 0 { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Invalid version constraint", - fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it doesn't come from a module registry.", req.Name, req.CallPos.Filename, req.CallPos.Line), - )) + if len(req.VersionConstraint.Required) != 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: fmt.Sprintf("Cannot apply a version constraint to module %q (at %s:%d) because it doesn't come from a module registry.", req.Name, req.CallRange.Filename, req.CallRange.Start.Line), + Subject: req.CallRange.Ptr(), + }) return nil, diags } @@ -556,59 +742,70 @@ func (i *ModuleInstaller) installGoGetterModule(ctx context.Context, req *earlyc if err != nil { // go-getter generates a poor error for an invalid relative path, so // we'll detect that case and generate a better one. - if _, ok := err.(*getmodules.MaybeRelativePathErr); ok { + if _, ok := err.(*moduleaddrs.MaybeRelativePathErr); ok { log.Printf( "[TRACE] ModuleInstaller: %s looks like a local path but is missing ./ or ../", req.SourceAddr, ) - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Module not found", - fmt.Sprintf( + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module not found", + Detail: fmt.Sprintf( "The module address %q could not be resolved.\n\n"+ "If you intended this as a path relative to the current "+ "module, use \"./%s\" instead. The \"./\" prefix "+ "indicates that the address is a relative filesystem path.", req.SourceAddr, req.SourceAddr, ), - )) + }) } else { // Errors returned by go-getter have very inconsistent quality as // end-user error messages, but for now we're accepting that because // we have no way to recognize any specific errors to improve them // and masking the error entirely would hide valuable diagnostic // information from the user. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Failed to download module", - fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallPos.Filename, req.CallPos.Line, packageAddr, err), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to download module", + Detail: fmt.Sprintf("Could not download module %q (%s:%d) source code from %q: %s", req.Name, req.CallRange.Filename, req.CallRange.Start.Line, packageAddr, err), + Subject: req.CallRange.Ptr(), + }) } return nil, diags } - modDir, err := getmodules.ExpandSubdirGlobs(instPath, addr.Subdir) + modDir, err := moduleaddrs.ExpandSubdirGlobs(instPath, addr.Subdir) if err != nil { - diags = diags.Append(err) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to expand subdir globs", + Detail: err.Error(), + }) return nil, diags } log.Printf("[TRACE] ModuleInstaller: %s %q was downloaded to %s", key, addr, modDir) - mod, mDiags := earlyconfig.LoadModule(modDir) + // Finally we are ready to try actually loading the module. + mod, mDiags := i.loader.Parser().LoadConfigDir(modDir) if mod == nil { // nil indicates missing or unreadable directory, so we'll // discard the returned diags and return a more specific // error message here. For go-getter modules this actually // indicates a bug in the code above, since it's not the // user's responsibility to create the directory in this case. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Unreadable module directory", - fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), - )) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unreadable module directory", + Detail: fmt.Sprintf("The directory %s could not be read. This is a bug in Terraform and should be reported.", modDir), + }) + } else if vDiags := mod.CheckCoreVersionRequirements(req.Path, req.SourceAddr); vDiags.HasErrors() { + // If the core version requirements are not met, we drop any other + // diagnostics, as they may reflect language changes from future + // Terraform versions. + diags = diags.Extend(vDiags) } else { - diags = append(diags, mDiags...) + diags = diags.Extend(mDiags) } // Note the local location in our manifest. @@ -636,7 +833,7 @@ func (i *ModuleInstaller) packageInstallPath(modulePath addrs.Module) string { // // This function's behavior is only reasonable for errors returned from the // ModuleInstaller.installLocalModule function. -func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags.Diagnostics) tfdiags.Diagnostics { +func maybeImproveLocalInstallError(req *configs.ModuleRequest, diags hcl.Diagnostics) hcl.Diagnostics { if !diags.HasErrors() { return diags } @@ -671,7 +868,7 @@ func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags Path: req.Path, SourceAddr: req.SourceAddr, }) - current := req.Parent // an earlyconfig.Config where Children isn't populated yet + current := req.Parent // a configs.Config where Children isn't populated yet for { if current == nil || current.SourceAddr == nil { // We've reached the root module, in which case we aren't @@ -715,11 +912,11 @@ func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags if !strings.HasPrefix(nextPath, prefix) { // ESCAPED! escapeeAddr := step.Path.String() - var newDiags tfdiags.Diagnostics + var newDiags hcl.Diagnostics // First we'll copy over any non-error diagnostics from the source diags for _, diag := range diags { - if diag.Severity() != tfdiags.Error { + if diag.Severity != hcl.DiagError { newDiags = newDiags.Append(diag) } } @@ -734,14 +931,14 @@ func maybeImproveLocalInstallError(req *earlyconfig.ModuleRequest, diags tfdiags // about it. suggestion = "\n\nTerraform treats absolute filesystem paths as external modules which establish a new module package. To treat this directory as part of the same package as its caller, use a local path starting with either \"./\" or \"../\"." } - newDiags = newDiags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Local module path escapes module package", - fmt.Sprintf( + newDiags = newDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Local module path escapes module package", + Detail: fmt.Sprintf( "The given source directory for %s would be outside of its containing package %q. Local source addresses starting with \"../\" must stay within the same package that the calling module belongs to.%s", escapeeAddr, packageAddr, suggestion, ), - )) + }) return newDiags } diff --git a/internal/initwd/module_install_hooks.go b/internal/initwd/module_install_hooks.go index 817a6dc832..3fbdfdcf8d 100644 --- a/internal/initwd/module_install_hooks.go +++ b/internal/initwd/module_install_hooks.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package initwd import ( diff --git a/internal/initwd/module_install_test.go b/internal/initwd/module_install_test.go index b05c5619f6..27a097bcb5 100644 --- a/internal/initwd/module_install_test.go +++ b/internal/initwd/module_install_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package initwd import ( @@ -16,6 +19,7 @@ import ( "github.com/google/go-cmp/cmp" version "github.com/hashicorp/go-version" svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" @@ -39,9 +43,11 @@ func TestModuleInstaller(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) - assertNoDiagnostics(t, diags) + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) + tfdiags.AssertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ { @@ -72,7 +78,7 @@ func TestModuleInstaller(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) wantTraces := map[string]string{ "": "in root module", @@ -100,8 +106,11 @@ func TestModuleInstaller_error(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) if !diags.HasErrors() { t.Fatal("expected error") @@ -110,6 +119,48 @@ func TestModuleInstaller_error(t *testing.T) { } } +func TestModuleInstaller_emptyModuleName(t *testing.T) { + fixtureDir := filepath.Clean("testdata/empty-module-name") + dir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) + + if !diags.HasErrors() { + t.Fatal("expected error") + } else { + assertDiagnosticSummary(t, diags, "Invalid module instance name") + } +} + +func TestModuleInstaller_invalidModuleName(t *testing.T) { + fixtureDir := filepath.Clean("testdata/invalid-module-name") + dir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) + + if !diags.HasErrors() { + t.Fatal("expected error") + } else { + assertDiagnosticSummary(t, diags, "Invalid module instance name") + } +} + func TestModuleInstaller_packageEscapeError(t *testing.T) { fixtureDir := filepath.Clean("testdata/load-module-package-escape") dir, done := tempChdir(t, fixtureDir) @@ -135,8 +186,11 @@ func TestModuleInstaller_packageEscapeError(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) if !diags.HasErrors() { t.Fatal("expected error") @@ -170,14 +224,71 @@ func TestModuleInstaller_explicitPackageBoundary(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) if diags.HasErrors() { t.Fatalf("unexpected errors\n%s", diags.Err().Error()) } } +func TestModuleInstaller_ExactMatchPrerelease(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") + } + + fixtureDir := filepath.Clean("testdata/prerelease-version-constraint-match") + dir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) + + if diags.HasErrors() { + t.Fatalf("found unexpected errors: %s", diags.Err()) + } + + if !cfg.Children["acctest_exact"].Version.Equal(version.Must(version.NewVersion("v0.0.3-alpha.1"))) { + t.Fatalf("expected version %s but found version %s", "v0.0.3-alpha.1", cfg.Version.String()) + } +} + +func TestModuleInstaller_PartialMatchPrerelease(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") + } + + fixtureDir := filepath.Clean("testdata/prerelease-version-constraint") + dir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + cfg, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) + + if diags.HasErrors() { + t.Fatalf("found unexpected errors: %s", diags.Err()) + } + + if !cfg.Children["acctest_partial"].Version.Equal(version.Must(version.NewVersion("v0.0.2"))) { + t.Fatalf("expected version %s but found version %s", "v0.0.2", cfg.Version.String()) + } +} + func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) { fixtureDir := filepath.Clean("testdata/invalid-version-constraint") dir, done := tempChdir(t, fixtureDir) @@ -186,8 +297,11 @@ func TestModuleInstaller_invalid_version_constraint_error(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) if !diags.HasErrors() { t.Fatal("expected error") @@ -209,8 +323,11 @@ func TestModuleInstaller_invalidVersionConstraintGetter(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) if !diags.HasErrors() { t.Fatal("expected error") @@ -232,8 +349,11 @@ func TestModuleInstaller_invalidVersionConstraintLocal(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) if !diags.HasErrors() { t.Fatal("expected error") @@ -255,9 +375,12 @@ func TestModuleInstaller_symlink(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, nil) - _, diags := inst.InstallModules(context.Background(), ".", false, hooks) - assertNoDiagnostics(t, diags) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) + tfdiags.AssertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ { @@ -288,7 +411,7 @@ func TestModuleInstaller_symlink(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) wantTraces := map[string]string{ "": "in root module", @@ -308,6 +431,45 @@ func TestModuleInstaller_symlink(t *testing.T) { assertResultDeepEqual(t, gotTraces, wantTraces) } +func TestLoaderInstallModules_invalidRegistry(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") + } + + fixtureDir := filepath.Clean("testdata/invalid-registry-modules") + tmpDir, done := tempChdir(t, fixtureDir) + // the module installer runs filepath.EvalSymlinks() on the destination + // directory before copying files, and the resultant directory is what is + // returned by the install hooks. Without this, tests could fail on machines + // where the default temp dir was a symlink. + dir, err := filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Error(err) + } + + defer done() + + hooks := &testInstallHooks{} + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) + + if !diags.HasErrors() { + t.Fatal("expected error") + } else { + tfdiags.AssertDiagnosticCount(t, diags, 1) + assertDiagnosticSummary(t, diags, "Unreadable module subdirectory") + + // the diagnostic should specifically call out the submodule that failed + if !strings.Contains(diags[0].Description().Detail, "target submodule modules/child_c") { + t.Errorf("unmatched error detail") + } + } +} + func TestLoaderInstallModules_registry(t *testing.T) { if os.Getenv("TF_ACC") == "" { t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") @@ -328,9 +490,12 @@ func TestLoaderInstallModules_registry(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) - _, diags := inst.InstallModules(context.Background(), dir, false, hooks) - assertNoDiagnostics(t, diags) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) + tfdiags.AssertNoDiagnostics(t, diags) v := version.Must(version.NewVersion("0.0.1")) @@ -434,7 +599,7 @@ func TestLoaderInstallModules_registry(t *testing.T) { t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modulesDir, }) if err != nil { @@ -444,7 +609,7 @@ func TestLoaderInstallModules_registry(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) wantTraces := map[string]string{ "": "in local caller for registry-modules", @@ -488,9 +653,12 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { hooks := &testInstallHooks{} modulesDir := filepath.Join(dir, ".terraform/modules") - inst := NewModuleInstaller(modulesDir, registry.NewClient(nil, nil)) - _, diags := inst.InstallModules(context.Background(), dir, false, hooks) - assertNoDiagnostics(t, diags) + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) + tfdiags.AssertNoDiagnostics(t, diags) wantCalls := []testInstallHookCall{ // the configuration builder visits each level of calls in lexicographical @@ -561,7 +729,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { t.Fatalf("wrong installer calls\n%s", diff) } - loader, err := configload.NewLoader(&configload.Config{ + loader, err = configload.NewLoader(&configload.Config{ ModulesDir: modulesDir, }) if err != nil { @@ -571,7 +739,7 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { // Make sure the configuration is loadable now. // (This ensures that correct information is recorded in the manifest.) config, loadDiags := loader.LoadConfig(".") - assertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) wantTraces := map[string]string{ "": "in local caller for go-getter-modules", @@ -596,6 +764,159 @@ func TestLoaderInstallModules_goGetter(t *testing.T) { } +func TestModuleInstaller_fromTests(t *testing.T) { + fixtureDir := filepath.Clean("testdata/local-module-from-test") + dir, done := tempChdir(t, fixtureDir) + defer done() + + hooks := &testInstallHooks{} + + modulesDir := filepath.Join(dir, ".terraform/modules") + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, nil) + _, diags := inst.InstallModules(context.Background(), ".", "tests", false, false, hooks) + tfdiags.AssertNoDiagnostics(t, diags) + + wantCalls := []testInstallHookCall{ + { + Name: "Install", + ModuleAddr: "test.tests.main.setup", + PackageAddr: "", + LocalPath: "setup", + }, + } + + if assertResultDeepEqual(t, hooks.Calls, wantCalls) { + return + } + + loader, err := configload.NewLoader(&configload.Config{ + ModulesDir: modulesDir, + }) + if err != nil { + t.Fatal(err) + } + + // Make sure the configuration is loadable now. + // (This ensures that correct information is recorded in the manifest.) + config, loadDiags := loader.LoadConfigWithTests(".", "tests") + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + + if config.Module.Tests["tests/main.tftest.hcl"].Runs[0].ConfigUnderTest == nil { + t.Fatalf("should have loaded config into the relevant run block but did not") + } +} + +func TestLoadInstallModules_registryFromTest(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("this test accesses registry.terraform.io and github.com; set TF_ACC=1 to run it") + } + + fixtureDir := filepath.Clean("testdata/registry-module-from-test") + tmpDir, done := tempChdir(t, fixtureDir) + // the module installer runs filepath.EvalSymlinks() on the destination + // directory before copying files, and the resultant directory is what is + // returned by the install hooks. Without this, tests could fail on machines + // where the default temp dir was a symlink. + dir, err := filepath.EvalSymlinks(tmpDir) + if err != nil { + t.Error(err) + } + + defer done() + + hooks := &testInstallHooks{} + modulesDir := filepath.Join(dir, ".terraform/modules") + + loader, close := configload.NewLoaderForTests(t) + defer close() + inst := NewModuleInstaller(modulesDir, loader, registry.NewClient(nil, nil)) + _, diags := inst.InstallModules(context.Background(), dir, "tests", false, false, hooks) + tfdiags.AssertNoDiagnostics(t, diags) + + v := version.Must(version.NewVersion("0.0.1")) + wantCalls := []testInstallHookCall{ + // the configuration builder visits each level of calls in lexicographical + // order by name, so the following list is kept in the same order. + + // setup access acctest directly. + { + Name: "Download", + ModuleAddr: "test.main.setup", + PackageAddr: "registry.terraform.io/hashicorp/module-installer-acctest/aws", // intentionally excludes the subdir because we're downloading the whole package here + Version: v, + }, + { + Name: "Install", + ModuleAddr: "test.main.setup", + Version: v, + // NOTE: This local path and the other paths derived from it below + // can vary depending on how the registry is implemented. At the + // time of writing this test, registry.terraform.io returns + // git repository source addresses and so this path refers to the + // root of the git clone, but historically the registry referred + // to GitHub-provided tar archives which meant that there was an + // extra level of subdirectory here for the typical directory + // nesting in tar archives, which would've been reflected as + // an extra segment on this path. If this test fails due to an + // additional path segment in future, then a change to the upstream + // registry might be the root cause. + LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup"), + }, + + // main.tftest.hcl.setup.child_a + // (no download because it's a relative path inside acctest_child_a) + { + Name: "Install", + ModuleAddr: "test.main.setup.child_a", + LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_a"), + }, + + // main.tftest.hcl.setup.child_a.child_b + // (no download because it's a relative path inside main.tftest.hcl.setup.child_a) + { + Name: "Install", + ModuleAddr: "test.main.setup.child_a.child_b", + LocalPath: filepath.Join(dir, ".terraform/modules/test.main.setup/modules/child_b"), + }, + } + + if diff := cmp.Diff(wantCalls, hooks.Calls); diff != "" { + t.Fatalf("wrong installer calls\n%s", diff) + } + + //check that the registry reponses were cached + packageAddr := addrs.ModuleRegistryPackage{ + Host: svchost.Hostname("registry.terraform.io"), + Namespace: "hashicorp", + Name: "module-installer-acctest", + TargetSystem: "aws", + } + if _, ok := inst.registryPackageVersions[packageAddr]; !ok { + t.Errorf("module versions cache was not populated\ngot: %s\nwant: key hashicorp/module-installer-acctest/aws", spew.Sdump(inst.registryPackageVersions)) + } + if _, ok := inst.registryPackageSources[moduleVersion{module: packageAddr, version: "0.0.1"}]; !ok { + t.Errorf("module download url cache was not populated\ngot: %s", spew.Sdump(inst.registryPackageSources)) + } + + loader, err = configload.NewLoader(&configload.Config{ + ModulesDir: modulesDir, + }) + if err != nil { + t.Fatal(err) + } + + // Make sure the configuration is loadable now. + // (This ensures that correct information is recorded in the manifest.) + config, loadDiags := loader.LoadConfigWithTests(".", "tests") + tfdiags.AssertNoDiagnostics(t, tfdiags.Diagnostics{}.Append(loadDiags)) + + if config.Module.Tests["main.tftest.hcl"].Runs[0].ConfigUnderTest == nil { + t.Fatalf("should have loaded config into the relevant run block but did not") + } +} + type testInstallHooks struct { Calls []testInstallHookCall } @@ -676,35 +997,19 @@ func tempChdir(t *testing.T, sourceDir string) (string, func()) { } } -func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) bool { - t.Helper() - return assertDiagnosticCount(t, diags, 0) -} - -func assertDiagnosticCount(t *testing.T, diags tfdiags.Diagnostics, want int) bool { - t.Helper() - if len(diags) != 0 { - t.Errorf("wrong number of diagnostics %d; want %d", len(diags), want) - for _, diag := range diags { - t.Logf("- %#v", diag) - } - return true - } - return false -} - func assertDiagnosticSummary(t *testing.T, diags tfdiags.Diagnostics, want string) bool { t.Helper() for _, diag := range diags { if diag.Description().Summary == want { + t.Logf("matching diagnostic detail %q", diag.Description().Detail) return false } } t.Errorf("missing diagnostic summary %q", want) for _, diag := range diags { - t.Logf("- %#v", diag) + t.Logf("- %#v", diag.Description().Summary) } return true } diff --git a/internal/initwd/testdata/empty-module-name/child/main.tf b/internal/initwd/testdata/empty-module-name/child/main.tf new file mode 100644 index 0000000000..6187fa659d --- /dev/null +++ b/internal/initwd/testdata/empty-module-name/child/main.tf @@ -0,0 +1,3 @@ +output "boop" { + value = "beep" +} diff --git a/internal/initwd/testdata/empty-module-name/main.tf b/internal/initwd/testdata/empty-module-name/main.tf new file mode 100644 index 0000000000..45add55b6f --- /dev/null +++ b/internal/initwd/testdata/empty-module-name/main.tf @@ -0,0 +1,3 @@ +module "" { + source = "./child" +} diff --git a/internal/initwd/testdata/invalid-module-name/child/main.tf b/internal/initwd/testdata/invalid-module-name/child/main.tf new file mode 100644 index 0000000000..6187fa659d --- /dev/null +++ b/internal/initwd/testdata/invalid-module-name/child/main.tf @@ -0,0 +1,3 @@ +output "boop" { + value = "beep" +} diff --git a/internal/initwd/testdata/invalid-module-name/main.tf b/internal/initwd/testdata/invalid-module-name/main.tf new file mode 100644 index 0000000000..316afe474c --- /dev/null +++ b/internal/initwd/testdata/invalid-module-name/main.tf @@ -0,0 +1,3 @@ +module "../invalid" { + source = "./child" +} diff --git a/internal/initwd/testdata/invalid-registry-modules/.gitignore b/internal/initwd/testdata/invalid-registry-modules/.gitignore new file mode 100644 index 0000000000..6e0db03a8b --- /dev/null +++ b/internal/initwd/testdata/invalid-registry-modules/.gitignore @@ -0,0 +1 @@ +.terraform/* diff --git a/internal/initwd/testdata/invalid-registry-modules/root.tf b/internal/initwd/testdata/invalid-registry-modules/root.tf new file mode 100644 index 0000000000..856910f45d --- /dev/null +++ b/internal/initwd/testdata/invalid-registry-modules/root.tf @@ -0,0 +1,28 @@ +# This fixture indirectly depends on a github repo at: +# https://github.com/hashicorp/terraform-aws-module-installer-acctest +# ...and expects its v0.0.1 tag to be pointing at the following commit: +# d676ab2559d4e0621d59e3c3c4cbb33958ac4608 +# +# This repository is accessed indirectly via: +# https://registry.terraform.io/modules/hashicorp/module-installer-acctest/aws/0.0.1 +# +# Since the tag's id is included in a downloaded archive, it is expected to +# have the following id: +# 853d03855b3290a3ca491d4c3a7684572dd42237 +# (this particular assumption is encoded in the tests that use this fixture) + + +variable "v" { + description = "in local caller for registry-modules" + default = "" +} + +// see ../registry-modules/root.tf for more info, and for valid usages to the +// acceptance test module + +// this sub module is not available in the registry, we should see an error +// message in the output +module "acctest_child_c" { + source = "hashicorp/module-installer-acctest/aws//modules/child_c" + version = "0.0.1" +} diff --git a/internal/initwd/testdata/load-module-package-prefix/package-prefix.tf b/internal/initwd/testdata/load-module-package-prefix/package-prefix.tf index 08d5ced602..8ba0d28f35 100644 --- a/internal/initwd/testdata/load-module-package-prefix/package-prefix.tf +++ b/internal/initwd/testdata/load-module-package-prefix/package-prefix.tf @@ -8,7 +8,7 @@ module "child" { # # Note that we're intentionally using the special // delimiter to # tell Terraform that it should treat the "package" directory as a - # whole as a module package, with all of its descendents "downloaded" + # whole as a module package, with all of its descendants "downloaded" # (copied) together into ./.terraform/modules/child so that child # can refer to ../grandchild successfully. source = "%%BASE%%/package//child" diff --git a/internal/initwd/testdata/local-module-from-test/main.tf b/internal/initwd/testdata/local-module-from-test/main.tf new file mode 100644 index 0000000000..4263e1f121 --- /dev/null +++ b/internal/initwd/testdata/local-module-from-test/main.tf @@ -0,0 +1,2 @@ +# Keep this empty, we just want to make sure the test file loads the setup +# module. \ No newline at end of file diff --git a/internal/initwd/testdata/local-module-from-test/setup/main.tf b/internal/initwd/testdata/local-module-from-test/setup/main.tf new file mode 100644 index 0000000000..6eac7e720a --- /dev/null +++ b/internal/initwd/testdata/local-module-from-test/setup/main.tf @@ -0,0 +1,4 @@ +variable "v" { + description = "in setup module" + default = "" +} diff --git a/internal/initwd/testdata/local-module-from-test/tests/main.tftest.hcl b/internal/initwd/testdata/local-module-from-test/tests/main.tftest.hcl new file mode 100644 index 0000000000..e9a479f24f --- /dev/null +++ b/internal/initwd/testdata/local-module-from-test/tests/main.tftest.hcl @@ -0,0 +1,5 @@ +run "setup" { + module { + source = "./setup" + } +} diff --git a/internal/initwd/testdata/local-module-missing-provider/main.tf b/internal/initwd/testdata/local-module-missing-provider/main.tf new file mode 100644 index 0000000000..0ef917dc7c --- /dev/null +++ b/internal/initwd/testdata/local-module-missing-provider/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + foo = { + source = "hashicorp/foo" + // since this module declares an alias with no config, it is not valid as + // a root module. + configuration_aliases = [ foo.alternate ] + } + } +} + +resource "foo_instance" "bam" { + provider = foo.alternate +} diff --git a/internal/initwd/testdata/prerelease-version-constraint-match/root.tf b/internal/initwd/testdata/prerelease-version-constraint-match/root.tf new file mode 100644 index 0000000000..b68baf770e --- /dev/null +++ b/internal/initwd/testdata/prerelease-version-constraint-match/root.tf @@ -0,0 +1,7 @@ +# We expect this test to download the requested version because it is an exact +# match for a prerelease version. + +module "acctest_exact" { + source = "hashicorp/module-installer-acctest/aws" + version = "=0.0.3-alpha.1" +} diff --git a/internal/initwd/testdata/prerelease-version-constraint/root.tf b/internal/initwd/testdata/prerelease-version-constraint/root.tf new file mode 100644 index 0000000000..8ff3dd68da --- /dev/null +++ b/internal/initwd/testdata/prerelease-version-constraint/root.tf @@ -0,0 +1,8 @@ +# We expect this test to download the version 0.0.2, the one before the +# specified version even with the equality because the specified version is a +# prerelease. + +module "acctest_partial" { + source = "hashicorp/module-installer-acctest/aws" + version = "<=0.0.3-alpha.1" +} diff --git a/internal/initwd/testdata/registry-module-from-test/main.tf b/internal/initwd/testdata/registry-module-from-test/main.tf new file mode 100644 index 0000000000..69d0720bc0 --- /dev/null +++ b/internal/initwd/testdata/registry-module-from-test/main.tf @@ -0,0 +1,2 @@ +# Deliberately empty, we just want to make sure the module is loaded from the +# tests. \ No newline at end of file diff --git a/internal/initwd/testdata/registry-module-from-test/main.tftest.hcl b/internal/initwd/testdata/registry-module-from-test/main.tftest.hcl new file mode 100644 index 0000000000..da19708133 --- /dev/null +++ b/internal/initwd/testdata/registry-module-from-test/main.tftest.hcl @@ -0,0 +1,8 @@ +run "setup" { + # We have a dedicated repo for this test module. + # See ../registry-modules/root.tf for more info. + module { + source = "hashicorp/module-installer-acctest/aws" + version = "0.0.1" + } +} diff --git a/internal/initwd/testing.go b/internal/initwd/testing.go index 406718159c..87fa8c4766 100644 --- a/internal/initwd/testing.go +++ b/internal/initwd/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package initwd import ( @@ -28,15 +31,15 @@ import ( // As with NewLoaderForTests, a cleanup function is returned which must be // called before the test completes in order to remove the temporary // modules directory. -func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configload.Loader, func(), tfdiags.Diagnostics) { +func LoadConfigForTests(t *testing.T, rootDir string, testsDir string) (*configs.Config, *configload.Loader, func(), tfdiags.Diagnostics) { t.Helper() var diags tfdiags.Diagnostics loader, cleanup := configload.NewLoaderForTests(t) - inst := NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) + inst := NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) - _, moreDiags := inst.InstallModules(context.Background(), rootDir, true, ModuleInstallHooksImpl{}) + _, moreDiags := inst.InstallModules(context.Background(), rootDir, testsDir, true, false, ModuleInstallHooksImpl{}) diags = diags.Append(moreDiags) if diags.HasErrors() { cleanup() @@ -62,10 +65,10 @@ func LoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configl // This is useful for concisely writing tests that don't expect errors at // all. For tests that expect errors and need to assert against them, use // LoadConfigForTests instead. -func MustLoadConfigForTests(t *testing.T, rootDir string) (*configs.Config, *configload.Loader, func()) { +func MustLoadConfigForTests(t *testing.T, rootDir, testsDir string) (*configs.Config, *configload.Loader, func()) { t.Helper() - config, loader, cleanup, diags := LoadConfigForTests(t, rootDir) + config, loader, cleanup, diags := LoadConfigForTests(t, rootDir, testsDir) if diags.HasErrors() { cleanup() t.Fatal(diags.Err()) diff --git a/internal/instances/expander.go b/internal/instances/expander.go index 2c912c897d..a8f440560a 100644 --- a/internal/instances/expander.go +++ b/internal/instances/expander.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package instances import ( @@ -6,6 +9,8 @@ import ( "sync" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/zclconf/go-cty/cty" ) @@ -41,9 +46,9 @@ type Expander struct { } // NewExpander initializes and returns a new Expander, empty and ready to use. -func NewExpander() *Expander { +func NewExpander(overrides *mocking.Overrides) *Expander { return &Expander{ - exps: newExpanderModule(), + exps: newExpanderModule(overrides), } } @@ -59,6 +64,13 @@ func (e *Expander) SetModuleCount(parentAddr addrs.ModuleInstance, callAddr addr e.setModuleExpansion(parentAddr, callAddr, expansionCount(count)) } +// SetModuleCountUnknown records that the given module call inside the given +// parent module instance uses the "count" repetition argument but its value +// is not yet known. +func (e *Expander) SetModuleCountUnknown(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall) { + e.setModuleExpansion(parentAddr, callAddr, expansionDeferredIntKey) +} + // SetModuleForEach records that the given module call inside the given parent // module instance uses the "for_each" repetition argument, with the given // map value. @@ -70,6 +82,13 @@ func (e *Expander) SetModuleForEach(parentAddr addrs.ModuleInstance, callAddr ad e.setModuleExpansion(parentAddr, callAddr, expansionForEach(mapping)) } +// SetModuleForEachUnknown records that the given module call inside the given +// parent module instance uses the "for_each" repetition argument, but its +// map keys are not yet known. +func (e *Expander) SetModuleForEachUnknown(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall) { + e.setModuleExpansion(parentAddr, callAddr, expansionDeferredStringKey) +} + // SetResourceSingle records that the given resource inside the given module // does not use any repetition arguments and is therefore a singleton. func (e *Expander) SetResourceSingle(moduleAddr addrs.ModuleInstance, resourceAddr addrs.Resource) { @@ -82,6 +101,12 @@ func (e *Expander) SetResourceCount(moduleAddr addrs.ModuleInstance, resourceAdd e.setResourceExpansion(moduleAddr, resourceAddr, expansionCount(count)) } +// SetResourceCountUnknown records that the given resource inside the given +// module uses the "count" repetition argument but its value isn't yet known. +func (e *Expander) SetResourceCountUnknown(moduleAddr addrs.ModuleInstance, resourceAddr addrs.Resource) { + e.setResourceExpansion(moduleAddr, resourceAddr, expansionDeferredIntKey) +} + // SetResourceForEach records that the given resource inside the given module // uses the "for_each" repetition argument, with the given map value. // @@ -92,20 +117,74 @@ func (e *Expander) SetResourceForEach(moduleAddr addrs.ModuleInstance, resourceA e.setResourceExpansion(moduleAddr, resourceAddr, expansionForEach(mapping)) } +// SetResourceForEachUnknown records that the given resource inside the given +// module uses the "for_each" repetition argument, but the map keys aren't +// known yet. +func (e *Expander) SetResourceForEachUnknown(moduleAddr addrs.ModuleInstance, resourceAddr addrs.Resource) { + e.setResourceExpansion(moduleAddr, resourceAddr, expansionDeferredStringKey) +} + // ExpandModule finds the exhaustive set of module instances resulting from // the expansion of the given module and all of its ancestor modules. // +// If any involved module calls have an as-yet-unknown set of instance keys +// then the result includes only the known instance addresses, if any. +// // All of the modules on the path to the identified module must already have // had their expansion registered using one of the SetModule* methods before // calling, or this method will panic. -func (e *Expander) ExpandModule(addr addrs.Module) []addrs.ModuleInstance { - return e.expandModule(addr, false) +// +// Any overridden modules will not be included in the result here. +func (e *Expander) ExpandModule(addr addrs.Module, includeDirectOverrides bool) []addrs.ModuleInstance { + return e.expandModule(addr, false, includeDirectOverrides) } -// expandModule allows skipping unexpanded module addresses by setting skipUnknown to true. +// ExpandAbsModuleCall is similar to [Expander.ExpandModule] except that it +// filters the result to include only the instances that belong to the +// given module call instance, and therefore returns just instance keys +// since the rest of the module address is implied by the given argument. +// +// For example, passing an address representing module.a["foo"].module.b +// would include only instances under module.a["foo"], and disregard instances +// under other dynamic paths like module.a["bar"]. +// +// If the requested module call has an unknown expansion (e.g. because it +// had an unknown value for count or for_each) then the second result is +// false and the other results are meaningless. If the second return value is +// true, then the set of module instances is complete, and all of the instances +// have instance keys matching the returned keytype. +// +// The instances are returned in the typical sort order for the returned +// key type: integer keys are sorted numerically, and string keys are sorted +// lexically. +func (e *Expander) ExpandAbsModuleCall(addr addrs.AbsModuleCall) (keyType addrs.InstanceKeyType, insts []addrs.InstanceKey, known bool) { + e.mu.RLock() + defer e.mu.RUnlock() + + expParent, ok := e.findModule(addr.Module) + if !ok { + // This module call lives under an unknown-expansion prefix, so we + // cannot answer this question. + return addrs.NoKeyType, nil, false + } + + expCall, ok := expParent.moduleCalls[addr.Call] + if !ok { + // This indicates a bug, since we should've calculated the expansions + // (even if unknown) before any caller asks for the results. + panic(fmt.Sprintf("no expansion has been registered for %s", addr.String())) + } + keyType, instKeys, deferred := expCall.instanceKeys() + if deferred { + return addrs.NoKeyType, nil, false + } + return keyType, instKeys, true +} + +// expandModule allows skipping unexpanded module addresses by setting skipUnregistered to true. // This is used by instances.Set, which is only concerned with the expanded // instances, and should not panic when looking up unknown addresses. -func (e *Expander) expandModule(addr addrs.Module, skipUnknown bool) []addrs.ModuleInstance { +func (e *Expander) expandModule(addr addrs.Module, skipUnregistered, includeDirectOverrides bool) []addrs.ModuleInstance { if len(addr) == 0 { // Root module is always a singleton. return singletonRootModule @@ -120,13 +199,42 @@ func (e *Expander) expandModule(addr addrs.Module, skipUnknown bool) []addrs.Mod // (moduleInstances does plenty of allocations itself, so the benefit of // pre-allocating this is marginal but it's not hard to do.) parentAddr := make(addrs.ModuleInstance, 0, 4) - ret := e.exps.moduleInstances(addr, parentAddr, skipUnknown) + ret := e.exps.moduleInstances(addr, parentAddr, skipUnregistered, includeDirectOverrides) sort.SliceStable(ret, func(i, j int) bool { return ret[i].Less(ret[j]) }) return ret } +// UnknownModuleInstances finds a set of patterns that collectively cover +// all of the possible module instance addresses that could appear for the +// given module once all of the intermediate module expansions are fully known. +// +// This imprecisely describes what's omitted from the [Expander.ExpandModule] +// result whenever there's an as-yet-unknown call expansion somewhere in the +// module path. +// +// Note that an [addrs.PartialExpandedModule] value is effectively an infinite +// set of [addrs.ModuleInstance] values itself, so the result could be +// considered as the union of all of those sets but we return it as a set of +// sets because the inner sets are of infinite size while the outer set is +// finite. +func (e *Expander) UnknownModuleInstances(addr addrs.Module, includeDirectOverrides bool) addrs.Set[addrs.PartialExpandedModule] { + if len(addr) == 0 { + // The root module is always "expanded" because it's always a singleton, + // so we have nothing to return in that case. + return nil + } + + e.mu.RLock() + defer e.mu.RUnlock() + + ret := addrs.MakeSet[addrs.PartialExpandedModule]() + parentAddr := make(addrs.ModuleInstance, 0, 4) + e.exps.partialExpandedModuleInstances(addr, parentAddr, includeDirectOverrides, ret) + return ret +} + // GetDeepestExistingModuleInstance is a funny specialized function for // determining how many steps we can traverse through the given module instance // address before encountering an undeclared instance of a declared module. @@ -165,6 +273,10 @@ func (e *Expander) GetDeepestExistingModuleInstance(given addrs.ModuleInstance) // ExpandModuleResource finds the exhaustive set of resource instances resulting from // the expansion of the given resource and all of its containing modules. // +// If any involved module calls or resources have an as-yet-unknown set of +// instance keys then the result includes only the known instance addresses, +// if any. +// // All of the modules on the path to the identified resource and the resource // itself must already have had their expansion registered using one of the // SetModule*/SetResource* methods before calling, or this method will panic. @@ -211,6 +323,31 @@ func (e *Expander) ExpandResource(resourceAddr addrs.AbsResource) []addrs.AbsRes return ret } +// UnknownResourceInstances finds a set of patterns that collectively cover +// all of the possible resource instance addresses that could appear for the +// given static resource once all of the intermediate module expansions are +// fully known. +// +// This imprecisely describes what's omitted from the [Expander.ExpandResource] +// and [Expander.ExpandModuleResource] results whenever there's an +// as-yet-unknown expansion somewhere in the module path or in the resource +// itself. +// +// Note that an [addrs.PartialExpandedResource] value is effectively an infinite +// set of [addrs.AbsResourceInstance] values itself, so the result could be +// considered as the union of all of those sets but we return it as a set of +// sets because the inner sets are of infinite size while the outer set is +// finite. +func (e *Expander) UnknownResourceInstances(resourceAddr addrs.ConfigResource) addrs.Set[addrs.PartialExpandedResource] { + e.mu.RLock() + defer e.mu.RUnlock() + + ret := addrs.MakeSet[addrs.PartialExpandedResource]() + parentModuleAddr := make(addrs.ModuleInstance, 0, 4) + e.exps.partialExpandedResourceInstances(resourceAddr.Module, resourceAddr.Resource, parentModuleAddr, ret) + return ret +} + // GetModuleInstanceRepetitionData returns an object describing the values // that should be available for each.key, each.value, and count.index within // the call block for the given module instance. @@ -223,7 +360,12 @@ func (e *Expander) GetModuleInstanceRepetitionData(addr addrs.ModuleInstance) Re e.mu.RLock() defer e.mu.RUnlock() - parentMod := e.findModule(addr[:len(addr)-1]) + parentMod, known := e.findModule(addr[:len(addr)-1]) + if !known { + // If we're nested inside something unexpanded then we don't even + // know what type of expansion we're doing. + return TotallyUnknownRepetitionData + } lastStep := addr[len(addr)-1] exp, ok := parentMod.moduleCalls[addrs.ModuleCall{Name: lastStep.Name}] if !ok { @@ -232,6 +374,34 @@ func (e *Expander) GetModuleInstanceRepetitionData(addr addrs.ModuleInstance) Re return exp.repetitionData(lastStep.InstanceKey) } +// GetModuleCallInstanceKeys determines the child instance keys for one specific +// instance of a module call. +// +// keyType describes the expected type of all keys in knownKeys, which typically +// also implies what data type would be used to describe the full set of +// instances: [addrs.IntKeyType] as a list or tuple, [addrs.StringKeyType] as +// a map or object, and [addrs.NoKeyType] as just a single value. +// +// If unknownKeys is true then there might be additional keys that we can't know +// yet because the call's expansion isn't known. +func (e *Expander) GetModuleCallInstanceKeys(addr addrs.AbsModuleCall) (keyType addrs.InstanceKeyType, knownKeys []addrs.InstanceKey, unknownKeys bool) { + e.mu.RLock() + defer e.mu.RUnlock() + + parentMod, known := e.findModule(addr.Module) + if !known { + // If we're nested inside something unexpanded then we don't even + // know yet what kind of instance key to expect. (The caller might + // be able to infer this itself using configuration info, though.) + return addrs.UnknownKeyType, nil, true + } + exp, ok := parentMod.moduleCalls[addr.Call] + if !ok { + panic(fmt.Sprintf("no expansion has been registered for %s", addr)) + } + return exp.instanceKeys() +} + // GetResourceInstanceRepetitionData returns an object describing the values // that should be available for each.key, each.value, and count.index within // the definition block for the given resource instance. @@ -239,7 +409,12 @@ func (e *Expander) GetResourceInstanceRepetitionData(addr addrs.AbsResourceInsta e.mu.RLock() defer e.mu.RUnlock() - parentMod := e.findModule(addr.Module) + parentMod, known := e.findModule(addr.Module) + if !known { + // If we're nested inside something unexpanded then we don't even + // know what type of expansion we're doing. + return TotallyUnknownRepetitionData + } exp, ok := parentMod.resources[addr.Resource.Resource] if !ok { panic(fmt.Sprintf("no expansion has been registered for %s", addr.ContainingResource())) @@ -247,6 +422,34 @@ func (e *Expander) GetResourceInstanceRepetitionData(addr addrs.AbsResourceInsta return exp.repetitionData(addr.Resource.Key) } +// ResourceInstanceKeys determines the child instance keys for one specific +// instance of a resource. +// +// keyType describes the expected type of all keys in knownKeys, which typically +// also implies what data type would be used to describe the full set of +// instances: [addrs.IntKeyType] as a list or tuple, [addrs.StringKeyType] as +// a map or object, and [addrs.NoKeyType] as just a single value. +// +// If unknownKeys is true then there might be additional keys that we can't know +// yet because the call's expansion isn't known. +func (e *Expander) ResourceInstanceKeys(addr addrs.AbsResource) (keyType addrs.InstanceKeyType, knownKeys []addrs.InstanceKey, unknownKeys bool) { + e.mu.RLock() + defer e.mu.RUnlock() + + parentMod, known := e.findModule(addr.Module) + if !known { + // If we're nested inside something unexpanded then we don't even + // know yet what kind of instance key to expect. (The caller might + // be able to infer this itself using configuration info, though.) + return addrs.UnknownKeyType, nil, true + } + exp, ok := parentMod.resources[addr.Resource] + if !ok { + panic(fmt.Sprintf("no expansion has been registered for %s", addr)) + } + return exp.instanceKeys() +} + // AllInstances returns a set of all of the module and resource instances known // to the expander. // @@ -259,11 +462,14 @@ func (e *Expander) AllInstances() Set { return Set{e} } -func (e *Expander) findModule(moduleInstAddr addrs.ModuleInstance) *expanderModule { +func (e *Expander) findModule(moduleInstAddr addrs.ModuleInstance) (expMod *expanderModule, known bool) { // We expect that all of the modules on the path to our module instance // should already have expansions registered. mod := e.exps for i, step := range moduleInstAddr { + if expansionIsDeferred(mod.moduleCalls[addrs.ModuleCall{Name: step.Name}]) { + return nil, false + } next, ok := mod.childInstances[step] if !ok { // Top-down ordering of registration is part of the contract of @@ -272,22 +478,28 @@ func (e *Expander) findModule(moduleInstAddr addrs.ModuleInstance) *expanderModu } mod = next } - return mod + return mod, true } func (e *Expander) setModuleExpansion(parentAddr addrs.ModuleInstance, callAddr addrs.ModuleCall, exp expansion) { e.mu.Lock() defer e.mu.Unlock() - mod := e.findModule(parentAddr) + mod, known := e.findModule(parentAddr) + if !known { + panic(fmt.Sprintf("can't register expansion for call in %s beneath unexpanded parent", parentAddr)) + } if _, exists := mod.moduleCalls[callAddr]; exists { panic(fmt.Sprintf("expansion already registered for %s", parentAddr.Child(callAddr.Name, addrs.NoKey))) } - // We'll also pre-register the child instances so that later calls can - // populate them as the caller traverses the configuration tree. - for _, key := range exp.instanceKeys() { - step := addrs.ModuleInstanceStep{Name: callAddr.Name, InstanceKey: key} - mod.childInstances[step] = newExpanderModule() + if !expansionIsDeferred(exp) { + // We'll also pre-register the child instances so that later calls can + // populate them as the caller traverses the configuration tree. + _, knownKeys, _ := exp.instanceKeys() + for _, key := range knownKeys { + step := addrs.ModuleInstanceStep{Name: callAddr.Name, InstanceKey: key} + mod.childInstances[step] = newExpanderModule(e.exps.overrides) + } } mod.moduleCalls[callAddr] = exp } @@ -296,7 +508,10 @@ func (e *Expander) setResourceExpansion(parentAddr addrs.ModuleInstance, resourc e.mu.Lock() defer e.mu.Unlock() - mod := e.findModule(parentAddr) + mod, known := e.findModule(parentAddr) + if !known { + panic(fmt.Sprintf("can't register expansion in %s where path includes unknown expansion", parentAddr)) + } if _, exists := mod.resources[resourceAddr]; exists { panic(fmt.Sprintf("expansion already registered for %s", resourceAddr.Absolute(parentAddr))) } @@ -339,26 +554,41 @@ type expanderModule struct { moduleCalls map[addrs.ModuleCall]expansion resources map[addrs.Resource]expansion childInstances map[addrs.ModuleInstanceStep]*expanderModule + + // overrides ensures that any overridden modules instances will not be + // returned as options for expansion. A nil overrides indicates there are + // no overrides and we're not operating within the testing framework. + overrides *mocking.Overrides } -func newExpanderModule() *expanderModule { +func newExpanderModule(overrides *mocking.Overrides) *expanderModule { return &expanderModule{ moduleCalls: make(map[addrs.ModuleCall]expansion), resources: make(map[addrs.Resource]expansion), childInstances: make(map[addrs.ModuleInstanceStep]*expanderModule), + overrides: overrides, } } var singletonRootModule = []addrs.ModuleInstance{addrs.RootModuleInstance} // if moduleInstances is being used to lookup known instances after all -// expansions have been done, set skipUnknown to true which allows addrs which -// may not have been seen to return with no instances rather than panicking. -func (m *expanderModule) moduleInstances(addr addrs.Module, parentAddr addrs.ModuleInstance, skipUnknown bool) []addrs.ModuleInstance { +// expansions have been done, set skipUnregistered to true which allows addrs +// which may not have been seen to return with no instances rather than +// panicking. +func (m *expanderModule) moduleInstances(addr addrs.Module, parentAddr addrs.ModuleInstance, skipUnregistered, includeDirectOverrides bool) []addrs.ModuleInstance { callName := addr[0] + + // If the parent module is overridden then this module should not be + // expanded. Note, we don't check includeDirectOverrides because if the + // parent module is overridden then this module isn't "directly" overridden. + if _, overridden := m.overrides.GetModuleOverride(parentAddr); overridden { + return nil + } + exp, ok := m.moduleCalls[addrs.ModuleCall{Name: callName}] if !ok { - if skipUnknown { + if skipUnregistered { return nil } // This is a bug in the caller, because it should always register @@ -366,6 +596,11 @@ func (m *expanderModule) moduleInstances(addr addrs.Module, parentAddr addrs.Mod // expansion of it. panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Child(callName, addrs.NoKey))) } + if expansionIsDeferred(exp) { + // We don't yet have enough information to determine the instance + // addresses for this module. + return nil + } var ret []addrs.ModuleInstance @@ -376,14 +611,26 @@ func (m *expanderModule) moduleInstances(addr addrs.Module, parentAddr addrs.Mod continue } instAddr := append(parentAddr, step) - ret = append(ret, inst.moduleInstances(addr[1:], instAddr, skipUnknown)...) + ret = append(ret, inst.moduleInstances(addr[1:], instAddr, skipUnregistered, includeDirectOverrides)...) } return ret } + if _, overridden := m.overrides.GetModuleOverride(parentAddr.Child(callName, addrs.NoKey)); !includeDirectOverrides && overridden { + // Then all the potential instances of this module have been + // overridden so we don't want to do any expansion for them. + return ret + } + // Otherwise, we'll use the expansion from the final step to produce // a sequence of addresses under this prefix. - for _, k := range exp.instanceKeys() { + _, knownKeys, _ := exp.instanceKeys() + for _, k := range knownKeys { + if _, overridden := m.overrides.GetModuleOverride(parentAddr.Child(callName, k)); !includeDirectOverrides && overridden { + // This specific instance is overridden, so we won't return it. + continue + } + // We're reusing the buffer under parentAddr as we recurse through // the structure, so we need to copy it here to produce a final // immutable slice to return. @@ -395,6 +642,54 @@ func (m *expanderModule) moduleInstances(addr addrs.Module, parentAddr addrs.Mod return ret } +func (m *expanderModule) partialExpandedModuleInstances(addr addrs.Module, parentAddr addrs.ModuleInstance, includeDirectOverrides bool, into addrs.Set[addrs.PartialExpandedModule]) { + callName := addr[0] + + // If the parent module is overridden then this module should not be + // expanded. Note, we don't check includeDirectOverrides because if the + // parent module is overridden then this module isn't "directly" overridden. + if _, overridden := m.overrides.GetModuleOverride(parentAddr); overridden { + return + } + + exp, ok := m.moduleCalls[addrs.ModuleCall{Name: callName}] + if !ok { + // This is a bug in the caller, because it should always register + // expansions for an object and all of its ancestors before requesting + // expansion of it. + panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Child(callName, addrs.NoKey))) + } + if expansionIsDeferred(exp) { + if _, overridden := m.overrides.GetModuleOverride(parentAddr.Child(callName, addrs.NoKey)); !includeDirectOverrides && overridden { + // Then all the potential instances of this module have been + // overridden so we don't want to do any expansion for them. + return + } + + // We've found a deferred expansion, so we're done searching this + // subtree and can just treat the whole of "addr" as unexpanded + // calls. + retAddr := parentAddr.UnexpandedChild(addrs.ModuleCall{Name: callName}) + for _, step := range addr[1:] { + retAddr = retAddr.Child(addrs.ModuleCall{Name: step}) + } + into.Add(retAddr) + return + } + + // If this step already has everything expanded then we need to + // search inside it to see if it has any unexpanded descendants. + if len(addr) > 1 { + for step, inst := range m.childInstances { + if step.Name != callName { + continue + } + instAddr := append(parentAddr, step) + inst.partialExpandedModuleInstances(addr[1:], instAddr, includeDirectOverrides, into) + } + } +} + func (m *expanderModule) moduleResourceInstances(moduleAddr addrs.Module, resourceAddr addrs.Resource, parentAddr addrs.ModuleInstance) []addrs.AbsResourceInstance { if len(moduleAddr) > 0 { var ret []addrs.AbsResourceInstance @@ -402,11 +697,14 @@ func (m *expanderModule) moduleResourceInstances(moduleAddr addrs.Module, resour // then iterate resource expansions in the context of each module // path leading to them. callName := moduleAddr[0] - if _, ok := m.moduleCalls[addrs.ModuleCall{Name: callName}]; !ok { + if exp, ok := m.moduleCalls[addrs.ModuleCall{Name: callName}]; !ok { // This is a bug in the caller, because it should always register // expansions for an object and all of its ancestors before requesting // expansion of it. panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Child(callName, addrs.NoKey))) + } else if expansionIsDeferred(exp) { + // We don't yet have any known instance addresses, then. + return nil } for step, inst := range m.childInstances { @@ -457,14 +755,81 @@ func (m *expanderModule) resourceInstances(moduleAddr addrs.ModuleInstance, reso return m.onlyResourceInstances(resourceAddr, parentAddr) } +func (m *expanderModule) partialExpandedResourceInstances(moduleAddr addrs.Module, resourceAddr addrs.Resource, parentAddr addrs.ModuleInstance, into addrs.Set[addrs.PartialExpandedResource]) { + // The idea here is to recursively walk along the module path until we + // either encounter a module call whose expansion isn't known yet or we + // run out of module steps. If we make it all the way to the end of the + // module path without encountering anything then that just leaves the + // resource expansion, which itself might be either known or unknown. + + switch { + case len(moduleAddr) > 0: + callName := moduleAddr[0] + callAddr := addrs.ModuleCall{Name: callName} + exp, ok := m.moduleCalls[callAddr] + if !ok { + // This is a bug in the caller, because it should always register + // expansions for an object and all of its ancestors before requesting + // expansion of it. + panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Child(callName, addrs.NoKey))) + } + if expansionIsDeferred(exp) { + // We've found a module call with an unknown expansion so this is + // as far as we can go and the rest of the module path has + // unknown expansion. + retMod := parentAddr.UnexpandedChild(callAddr) + for _, stepName := range moduleAddr[1:] { + retMod = retMod.Child(addrs.ModuleCall{Name: stepName}) + } + ret := retMod.Resource(resourceAddr) + into.Add(ret) + return + } + + // If we get here then we can continue exploring all of the known + // instances of this current module call. + for step, inst := range m.childInstances { + if step.Name != callName { + continue + } + instAddr := parentAddr.Child(step.Name, step.InstanceKey) + inst.partialExpandedResourceInstances(moduleAddr[1:], resourceAddr, instAddr, into) + } + + default: + // If we've run out of module address steps then the only remaining + // question is whether the resource's own expansion is known. + exp, ok := m.resources[resourceAddr] + if !ok { + // This is a bug in the caller, because it should always register + // expansions for an object and all of its ancestors before requesting + // expansion of it. + panic(fmt.Sprintf("no expansion has been registered for %s", parentAddr.Resource(resourceAddr.Mode, resourceAddr.Type, resourceAddr.Name))) + } + if expansionIsDeferred(exp) { + ret := parentAddr.UnexpandedResource(resourceAddr) + into.Add(ret) + return + } + // If the expansion isn't deferred then there's nothing to do here, + // because the instances of this resource would appear in the + // resourceInstances method results instead. + } +} + func (m *expanderModule) onlyResourceInstances(resourceAddr addrs.Resource, parentAddr addrs.ModuleInstance) []addrs.AbsResourceInstance { var ret []addrs.AbsResourceInstance exp, ok := m.resources[resourceAddr] if !ok { panic(fmt.Sprintf("no expansion has been registered for %s", resourceAddr.Absolute(parentAddr))) } + if expansionIsDeferred(exp) { + // We don't yet have enough information to determine the instance addresses. + return nil + } - for _, k := range exp.instanceKeys() { + _, knownKeys, _ := exp.instanceKeys() + for _, k := range knownKeys { // We're reusing the buffer under parentAddr as we recurse through // the structure, so we need to copy it here to produce a final // immutable slice to return. @@ -509,7 +874,8 @@ func (m *expanderModule) knowsResourceInstance(want addrs.AbsResourceInstance) b if resourceExp == nil { return false } - for _, key := range resourceExp.instanceKeys() { + _, knownKeys, _ := resourceExp.instanceKeys() + for _, key := range knownKeys { if key == want.Resource.Key { return true } diff --git a/internal/instances/expander_test.go b/internal/instances/expander_test.go index 2e98328892..40db28ef0d 100644 --- a/internal/instances/expander_test.go +++ b/internal/instances/expander_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package instances import ( @@ -6,11 +9,158 @@ import ( "testing" "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/moduletest/mocking" ) +func TestExpanderWithOverrides(t *testing.T) { + + mustModuleInstance := func(t *testing.T, s string) addrs.ModuleInstance { + if len(s) == 0 { + return addrs.RootModuleInstance + } + + addr, diags := addrs.ParseModuleInstanceStr(s) + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err()) + } + return addr + } + + tcs := map[string]struct { + // Hook to install chosen overrides. + overrides mocking.InitLocalOverrides + + // Hook to initialise the expander with the desired state. + expander func(*Expander) + + // The target module instance to inspect. + target string + + // Set to true to include overrides in the result. + includeOverrides bool + + // The expected result. + wantModules []addrs.ModuleInstance + + // The expected result for partial modules. + wantPartials map[string]bool + }{ + "root module": { + wantModules: singletonRootModule, + wantPartials: make(map[string]bool), + }, + "instanced child module not overridden": { + expander: func(expander *Expander) { + expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) + }, + target: "module.double", + wantModules: []addrs.ModuleInstance{ + mustModuleInstance(t, "module.double[0]"), + mustModuleInstance(t, "module.double[1]"), + }, + wantPartials: make(map[string]bool), + }, + "instanced child module single instance overridden": { + overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance(t, "module.double[0]"), &configs.Override{}) + }, + expander: func(expander *Expander) { + expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) + }, + target: "module.double", + wantModules: []addrs.ModuleInstance{ + mustModuleInstance(t, "module.double[1]"), + }, + wantPartials: make(map[string]bool), + }, + "instanced child module single instance overridden includes overrides": { + overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance(t, "module.double[0]"), &configs.Override{}) + }, + expander: func(expander *Expander) { + expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) + }, + target: "module.double", + includeOverrides: true, + wantModules: []addrs.ModuleInstance{ + mustModuleInstance(t, "module.double[0]"), + mustModuleInstance(t, "module.double[1]"), + }, + wantPartials: make(map[string]bool), + }, + "deeply nested child module with parent overridden": { + overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance(t, "module.double[0]"), &configs.Override{}) + }, + expander: func(expander *Expander) { + expander.SetModuleCount(addrs.RootModuleInstance, addrs.ModuleCall{Name: "double"}, 2) + expander.SetModuleSingle(mustModuleInstance(t, "module.double[1]"), addrs.ModuleCall{Name: "single"}) + }, + target: "module.double.module.single", + wantModules: []addrs.ModuleInstance{mustModuleInstance(t, "module.double[1].module.single")}, + wantPartials: make(map[string]bool), + }, + "unknown child module overridden by instanced module": { + overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance(t, "module.unknown[0]"), &configs.Override{}) + }, + expander: func(expander *Expander) { + expander.SetModuleCountUnknown(addrs.RootModuleInstance, addrs.ModuleCall{Name: "unknown"}) + }, + target: "module.unknown", + wantPartials: map[string]bool{ + "module.unknown[*]": true, + }, + }, + "unknown child module overridden by instanced module includes overrides": { + overrides: func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance(t, "module.unknown"), &configs.Override{}) + }, + expander: func(expander *Expander) { + expander.SetModuleCountUnknown(addrs.RootModuleInstance, addrs.ModuleCall{Name: "unknown"}) + }, + target: "module.unknown", + wantPartials: make(map[string]bool), // This time it's empty, as we overrode all instances. + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + overrides := mocking.OverridesForTesting(nil, tc.overrides) + + expander := NewExpander(overrides) + if tc.expander != nil { + tc.expander(expander) + } + + target := mustModuleInstance(t, tc.target).Module() + + gotModules := expander.ExpandModule(target, tc.includeOverrides) + gotPartials := expander.UnknownModuleInstances(target, tc.includeOverrides) + + if diff := cmp.Diff(tc.wantModules, gotModules); len(diff) > 0 { + t.Errorf("wrong result\n%s", diff) + } + + // Convert the gotPartials into strings to make cmp.Diff work. + gotPartialsStr := make(map[string]bool, len(gotPartials)) + for _, partial := range gotPartials { + gotPartialsStr[partial.String()] = true + } + + if diff := cmp.Diff(tc.wantPartials, gotPartialsStr, ctydebug.CmpOptions); len(diff) > 0 { + t.Errorf("wrong result\n%s", diff) + } + }) + } + +} + func TestExpander(t *testing.T) { // Some module and resource addresses and values we'll use repeatedly below. singleModuleAddr := addrs.ModuleCall{Name: "single"} @@ -67,7 +217,7 @@ func TestExpander(t *testing.T) { // - resource test.single with no count or for_each // - resource test.count2 with count = 2 - ex := NewExpander() + ex := NewExpander(nil) // We don't register the root module, because it's always implied to exist. // @@ -123,7 +273,7 @@ func TestExpander(t *testing.T) { t.Run("root module", func(t *testing.T) { // Requesting expansion of the root module doesn't really mean anything // since it's always a singleton, but for consistency it should work. - got := ex.ExpandModule(addrs.RootModule) + got := ex.ExpandModule(addrs.RootModule, false) want := []addrs.ModuleInstance{addrs.RootModuleInstance} if diff := cmp.Diff(want, got); diff != "" { t.Errorf("wrong result\n%s", diff) @@ -178,7 +328,7 @@ func TestExpander(t *testing.T) { } }) t.Run("module single", func(t *testing.T) { - got := ex.ExpandModule(addrs.RootModule.Child("single")) + got := ex.ExpandModule(addrs.RootModule.Child("single"), false) want := []addrs.ModuleInstance{ mustModuleInstanceAddr(`module.single`), } @@ -245,7 +395,7 @@ func TestExpander(t *testing.T) { } }) t.Run("module count2", func(t *testing.T) { - got := ex.ExpandModule(mustModuleAddr(`count2`)) + got := ex.ExpandModule(mustModuleAddr(`count2`), false) want := []addrs.ModuleInstance{ mustModuleInstanceAddr(`module.count2[0]`), mustModuleInstanceAddr(`module.count2[1]`), @@ -283,7 +433,7 @@ func TestExpander(t *testing.T) { } }) t.Run("module count2 module count2", func(t *testing.T) { - got := ex.ExpandModule(mustModuleAddr(`count2.count2`)) + got := ex.ExpandModule(mustModuleAddr(`count2.count2`), false) want := []addrs.ModuleInstance{ mustModuleInstanceAddr(`module.count2[0].module.count2[0]`), mustModuleInstanceAddr(`module.count2[0].module.count2[1]`), @@ -294,6 +444,24 @@ func TestExpander(t *testing.T) { t.Errorf("wrong result\n%s", diff) } }) + t.Run("module count2[0] module count2 instances", func(t *testing.T) { + instAddr := mustModuleInstanceAddr(`module.count2[0].module.count2[0]`) + callAddr := instAddr.AbsCall() // discards the final [0] instance key from the above + keyType, got, known := ex.ExpandAbsModuleCall(callAddr) + if !known { + t.Fatal("expansion unknown; want known") + } + if keyType != addrs.IntKeyType { + t.Fatalf("wrong key type %#v; want %#v", keyType, addrs.IntKeyType) + } + want := []addrs.InstanceKey{ + addrs.IntKey(0), + addrs.IntKey(1), + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) t.Run("module count2 module count2 GetDeepestExistingModuleInstance", func(t *testing.T) { t.Run("first step invalid", func(t *testing.T) { got := ex.GetDeepestExistingModuleInstance(mustModuleInstanceAddr(`module.count2["nope"].module.count2[0]`)) @@ -356,7 +524,7 @@ func TestExpander(t *testing.T) { } }) t.Run("module count0", func(t *testing.T) { - got := ex.ExpandModule(mustModuleAddr(`count0`)) + got := ex.ExpandModule(mustModuleAddr(`count0`), false) want := []addrs.ModuleInstance(nil) if diff := cmp.Diff(want, got); diff != "" { t.Errorf("wrong result\n%s", diff) @@ -376,7 +544,7 @@ func TestExpander(t *testing.T) { } }) t.Run("module for_each", func(t *testing.T) { - got := ex.ExpandModule(mustModuleAddr(`for_each`)) + got := ex.ExpandModule(mustModuleAddr(`for_each`), false) want := []addrs.ModuleInstance{ mustModuleInstanceAddr(`module.for_each["a"]`), mustModuleInstanceAddr(`module.for_each["b"]`), @@ -496,6 +664,163 @@ func TestExpander(t *testing.T) { }) } +func TestExpanderWithUnknowns(t *testing.T) { + t.Run("resource in root module with unknown for_each", func(t *testing.T) { + resourceAddr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + } + ex := NewExpander(nil) + ex.SetResourceForEachUnknown(addrs.RootModuleInstance, resourceAddr) + + got := ex.ExpandModuleResource(addrs.RootModule, resourceAddr) + if len(got) != 0 { + t.Errorf("unexpected known addresses: %#v", got) + } + }) + t.Run("resource in root module with unknown count", func(t *testing.T) { + resourceAddr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + } + ex := NewExpander(nil) + ex.SetResourceCountUnknown(addrs.RootModuleInstance, resourceAddr) + + got := ex.ExpandModuleResource(addrs.RootModule, resourceAddr) + if len(got) != 0 { + t.Errorf("unexpected known addresses: %#v", got) + } + }) + t.Run("module with unknown for_each", func(t *testing.T) { + moduleCallAddr := addrs.ModuleCall{Name: "foo"} + ex := NewExpander(nil) + ex.SetModuleForEachUnknown(addrs.RootModuleInstance, moduleCallAddr) + + got := ex.ExpandModule(addrs.Module{moduleCallAddr.Name}, false) + if len(got) != 0 { + t.Errorf("unexpected known addresses: %#v", got) + } + + gotUnknown := ex.UnknownModuleInstances(addrs.Module{moduleCallAddr.Name}, false) + if len(gotUnknown) != 1 { + t.Errorf("unexpected unknown addresses: %#v", gotUnknown) + } + wantUnknownCall := addrs.RootModuleInstance.UnexpandedChild(moduleCallAddr) + if !gotUnknown.Has(wantUnknownCall) { + t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) + } + }) + t.Run("module with unknown count", func(t *testing.T) { + moduleCallAddr := addrs.ModuleCall{Name: "foo"} + ex := NewExpander(nil) + ex.SetModuleCountUnknown(addrs.RootModuleInstance, moduleCallAddr) + + gotKnown := ex.ExpandModule(addrs.Module{moduleCallAddr.Name}, false) + if len(gotKnown) != 0 { + t.Errorf("unexpected known addresses: %#v", gotKnown) + } + + gotUnknown := ex.UnknownModuleInstances(addrs.Module{moduleCallAddr.Name}, false) + if len(gotUnknown) != 1 { + t.Errorf("unexpected unknown addresses: %#v", gotUnknown) + } + wantUnknownCall := addrs.RootModuleInstance.UnexpandedChild(moduleCallAddr) + if !gotUnknown.Has(wantUnknownCall) { + t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) + } + }) + t.Run("nested module with unknown count", func(t *testing.T) { + moduleCallAddr1 := addrs.ModuleCall{Name: "foo"} + moduleCallAddr2 := addrs.ModuleCall{Name: "bar"} + module1 := addrs.RootModule.Child(moduleCallAddr1.Name) + module2 := module1.Child(moduleCallAddr2.Name) + module1Inst0 := addrs.RootModuleInstance.Child("foo", addrs.IntKey(0)) + module1Inst1 := addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)) + module1Inst2 := addrs.RootModuleInstance.Child("foo", addrs.IntKey(2)) + ex := NewExpander(nil) + ex.SetModuleCount(addrs.RootModuleInstance, moduleCallAddr1, 3) + ex.SetModuleCountUnknown(module1Inst0, moduleCallAddr2) + ex.SetModuleCount(module1Inst1, moduleCallAddr2, 1) + ex.SetModuleCountUnknown(module1Inst2, moduleCallAddr2) + + // We'll also put some resources inside module.foo[1].module.bar[0] + // so that we can test requesting unknown resource instance sets. + resourceAddrKnownExp := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "known_expansion", + } + resourceAddrUnknownExp := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "unknown_expansion", + } + module1Inst1Module2Inst0 := module1Inst1.Child("bar", addrs.IntKey(0)) + ex.SetResourceCount(module1Inst1Module2Inst0, resourceAddrKnownExp, 2) + ex.SetResourceCountUnknown(module1Inst1Module2Inst0, resourceAddrUnknownExp) + + module2Call := addrs.AbsModuleCall{ + Module: module1Inst0, + Call: moduleCallAddr2, + } + _, _, instsKnown := ex.ExpandAbsModuleCall(module2Call) + if instsKnown { + t.Fatalf("instances of %s are known; should be unknown", module2Call.String()) + } + + gotKnown := ex.ExpandModule(module2, false) + wantKnown := []addrs.ModuleInstance{ + module1Inst1.Child("bar", addrs.IntKey(0)), + } + if diff := cmp.Diff(wantKnown, gotKnown); diff != "" { + t.Errorf("unexpected known addresses\n%s", diff) + } + + gotUnknown := ex.UnknownModuleInstances(module2, false) + if len(gotUnknown) != 2 { + t.Errorf("unexpected unknown addresses: %#v", gotUnknown) + } + if wantUnknownCall := module1Inst0.UnexpandedChild(moduleCallAddr2); !gotUnknown.Has(wantUnknownCall) { + t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) + } + if unwantUnknownCall := module1Inst1.UnexpandedChild(moduleCallAddr2); gotUnknown.Has(unwantUnknownCall) { + t.Errorf("unknown should not have %s, but does", unwantUnknownCall) + } + if wantUnknownCall := module1Inst2.UnexpandedChild(moduleCallAddr2); !gotUnknown.Has(wantUnknownCall) { + t.Errorf("unknown should have %s, but it doesn't", wantUnknownCall) + } + + gotKnownResource := ex.ExpandResource(module1Inst1Module2Inst0.Resource( + resourceAddrKnownExp.Mode, resourceAddrKnownExp.Type, resourceAddrKnownExp.Name, + )) + wantKnownResource := []addrs.AbsResourceInstance{ + mustAbsResourceInstanceAddr("module.foo[1].module.bar[0].test.known_expansion[0]"), + mustAbsResourceInstanceAddr("module.foo[1].module.bar[0].test.known_expansion[1]"), + } + if diff := cmp.Diff(wantKnownResource, gotKnownResource); diff != "" { + t.Errorf("unexpected known addresses\n%s", diff) + } + + gotUnknownResource := ex.UnknownResourceInstances(module2.Resource( + resourceAddrUnknownExp.Mode, resourceAddrUnknownExp.Type, resourceAddrUnknownExp.Name, + )) + if len(gotUnknownResource) != 3 { + t.Errorf("unexpected unknown addresses: %#v", gotUnknownResource) + } + if wantResInst := module1Inst0.UnexpandedChild(moduleCallAddr2).Resource(resourceAddrUnknownExp); !gotUnknownResource.Has(wantResInst) { + t.Errorf("unknown should have %s, but it doesn't", wantResInst) + } + if wantResInst := module1Inst1Module2Inst0.UnexpandedResource(resourceAddrUnknownExp); !gotUnknownResource.Has(wantResInst) { + t.Errorf("unknown should have %s, but it doesn't", wantResInst) + } + if wantResInst := module1Inst2.UnexpandedChild(moduleCallAddr2).Resource(resourceAddrUnknownExp); !gotUnknownResource.Has(wantResInst) { + t.Errorf("unknown should have %s, but it doesn't", wantResInst) + } + }) +} + func mustAbsResourceInstanceAddr(str string) addrs.AbsResourceInstance { addr, diags := addrs.ParseAbsResourceInstanceStr(str) if diags.HasErrors() { diff --git a/internal/instances/expansion_mode.go b/internal/instances/expansion_mode.go index 1183e3c768..797e40dcc4 100644 --- a/internal/instances/expansion_mode.go +++ b/internal/instances/expansion_mode.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package instances import ( @@ -13,7 +16,19 @@ import ( // ways expansion can operate depending on how repetition is configured for // an object. type expansion interface { - instanceKeys() []addrs.InstanceKey + // instanceKeys determines the full set of instance keys for whatever + // object this expansion is associated with, or indicates that we + // can't yet know what they are (using keysUnknown=true). + // + // if keysUnknown is true then the "keys" result is likely to be incomplete, + // meaning that there might be other instance keys that we've not yet + // calculated, but we know that they will be of type keyType. + // + // If len(keys) == 0 when keysUnknown is true then we don't yet know _any_ + // instance keys for this object. keyType might be [addrs.UnknownKeyType] + // if we don't even have enough information to predict what type of + // keys we will be using for this object. + instanceKeys() (keyType addrs.InstanceKeyType, keys []addrs.InstanceKey, keysUnknown bool) repetitionData(addrs.InstanceKey) RepetitionData } @@ -26,8 +41,8 @@ type expansionSingle uintptr var singleKeys = []addrs.InstanceKey{addrs.NoKey} var expansionSingleVal expansionSingle -func (e expansionSingle) instanceKeys() []addrs.InstanceKey { - return singleKeys +func (e expansionSingle) instanceKeys() (addrs.InstanceKeyType, []addrs.InstanceKey, bool) { + return addrs.NoKeyType, singleKeys, false } func (e expansionSingle) repetitionData(key addrs.InstanceKey) RepetitionData { @@ -40,12 +55,12 @@ func (e expansionSingle) repetitionData(key addrs.InstanceKey) RepetitionData { // expansionCount is the expansion corresponding to the "count" argument. type expansionCount int -func (e expansionCount) instanceKeys() []addrs.InstanceKey { +func (e expansionCount) instanceKeys() (addrs.InstanceKeyType, []addrs.InstanceKey, bool) { ret := make([]addrs.InstanceKey, int(e)) for i := range ret { ret[i] = addrs.IntKey(i) } - return ret + return addrs.IntKeyType, ret, false } func (e expansionCount) repetitionData(key addrs.InstanceKey) RepetitionData { @@ -61,7 +76,7 @@ func (e expansionCount) repetitionData(key addrs.InstanceKey) RepetitionData { // expansionForEach is the expansion corresponding to the "for_each" argument. type expansionForEach map[string]cty.Value -func (e expansionForEach) instanceKeys() []addrs.InstanceKey { +func (e expansionForEach) instanceKeys() (addrs.InstanceKeyType, []addrs.InstanceKey, bool) { ret := make([]addrs.InstanceKey, 0, len(e)) for k := range e { ret = append(ret, addrs.StringKey(k)) @@ -69,7 +84,7 @@ func (e expansionForEach) instanceKeys() []addrs.InstanceKey { sort.Slice(ret, func(i, j int) bool { return ret[i].(addrs.StringKey) < ret[j].(addrs.StringKey) }) - return ret + return addrs.StringKeyType, ret, false } func (e expansionForEach) repetitionData(key addrs.InstanceKey) RepetitionData { @@ -83,3 +98,27 @@ func (e expansionForEach) repetitionData(key addrs.InstanceKey) RepetitionData { EachValue: v, } } + +// expansionDeferred is a special expansion which represents that we don't +// yet have enough information to calculate the expansion of a particular +// object. +// +// [expansionDeferredIntKey] and [expansionDeferredStringKey] are the only +// valid values of this type. +type expansionDeferred rune + +const expansionDeferredIntKey = expansionDeferred(addrs.IntKeyType) +const expansionDeferredStringKey = expansionDeferred(addrs.StringKeyType) + +func (e expansionDeferred) instanceKeys() (addrs.InstanceKeyType, []addrs.InstanceKey, bool) { + return addrs.InstanceKeyType(e), nil, true +} + +func (e expansionDeferred) repetitionData(key addrs.InstanceKey) RepetitionData { + panic("no known instances for object with deferred expansion") +} + +func expansionIsDeferred(exp expansion) bool { + _, ret := exp.(expansionDeferred) + return ret +} diff --git a/internal/instances/instance_key_data.go b/internal/instances/instance_key_data.go index 9ada5253ce..8744d71123 100644 --- a/internal/instances/instance_key_data.go +++ b/internal/instances/instance_key_data.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package instances import ( @@ -26,3 +29,47 @@ type RepetitionData struct { // or cty.Number if not nil. EachKey, EachValue cty.Value } + +// TotallyUnknownRepetitionData is a [RepetitionData] value for situations +// where don't even know yet what type of repetition will be used. +var TotallyUnknownRepetitionData = RepetitionData{ + CountIndex: cty.UnknownVal(cty.Number), + EachKey: cty.UnknownVal(cty.String), + EachValue: cty.DynamicVal, +} + +// UnknownCountRepetitionData is a suitable [RepetitionData] value to use when +// evaluating the configuration of an object which has a count argument that +// is currently unknown. +var UnknownCountRepetitionData = RepetitionData{ + CountIndex: cty.UnknownVal(cty.Number), +} + +// UnknownForEachRepetitionData generates a suitable [RepetitionData] value to +// use when evaluating the configuration of an object whose for_each argument +// is currently unknown. +// +// forEachType should be the type constraint of the unknown for_each argument +// value. This should be of a type that is valid to use in for_each, but +// if not then this will just return a very general RepetitionData that would +// be suitable (but less specific) for any valid for_each value. +func UnknownForEachRepetitionData(forEachType cty.Type) RepetitionData { + switch { + case forEachType.IsMapType(): + return RepetitionData{ + EachKey: cty.UnknownVal(cty.String), + EachValue: cty.UnknownVal(forEachType.ElementType()), + } + case forEachType.IsSetType() && forEachType.ElementType().Equals(cty.String): + return RepetitionData{ + EachKey: cty.UnknownVal(cty.String), + EachValue: cty.UnknownVal(cty.String), + } + default: + // We know that each.key is always a string, at least. + return RepetitionData{ + EachKey: cty.UnknownVal(cty.String), + EachValue: cty.DynamicVal, + } + } +} diff --git a/internal/instances/set.go b/internal/instances/set.go index 701a2d27e0..1b182860f8 100644 --- a/internal/instances/set.go +++ b/internal/instances/set.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package instances import ( @@ -46,6 +49,6 @@ func (s Set) HasResource(want addrs.AbsResource) bool { // If there are multiple module calls in the path that have repetition enabled // then the result is the full expansion of all combinations of all of their // declared instance keys. -func (s Set) InstancesForModule(modAddr addrs.Module) []addrs.ModuleInstance { - return s.exp.expandModule(modAddr, true) +func (s Set) InstancesForModule(modAddr addrs.Module, includeDirectOverrides bool) []addrs.ModuleInstance { + return s.exp.expandModule(modAddr, true, includeDirectOverrides) } diff --git a/internal/instances/set_test.go b/internal/instances/set_test.go index e255cef1b8..f8ca16bad7 100644 --- a/internal/instances/set_test.go +++ b/internal/instances/set_test.go @@ -1,14 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package instances import ( "testing" - "github.com/hashicorp/terraform/internal/addrs" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" ) func TestSet(t *testing.T) { - exp := NewExpander() + exp := NewExpander(nil) // The following constructs the following imaginary module/resource tree: // - root module @@ -205,7 +209,7 @@ func TestSet(t *testing.T) { } // ensure we can lookup non-existent addrs in a set without panic - if set.InstancesForModule(addrs.RootModule.Child("missing")) != nil { + if set.InstancesForModule(addrs.RootModule.Child("missing"), false) != nil { t.Error("unexpected instances from missing module") } } diff --git a/internal/ipaddr/doc.go b/internal/ipaddr/doc.go index 68d79c3c2d..c95ac881b2 100644 --- a/internal/ipaddr/doc.go +++ b/internal/ipaddr/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package ipaddr is a fork of a subset of the Go standard "net" package which // retains parsing behaviors from Go 1.16 or earlier. // diff --git a/internal/lang/blocktoattr/doc.go b/internal/lang/blocktoattr/doc.go index 8f89909c6f..858f54f909 100644 --- a/internal/lang/blocktoattr/doc.go +++ b/internal/lang/blocktoattr/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package blocktoattr includes some helper functions that can perform // preprocessing on a HCL body where a configschema.Block schema is available // in order to allow list and set attributes defined in the schema to be diff --git a/internal/lang/blocktoattr/fixup.go b/internal/lang/blocktoattr/fixup.go index 5d05a86f2f..6a6e0ef03d 100644 --- a/internal/lang/blocktoattr/fixup.go +++ b/internal/lang/blocktoattr/fixup.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package blocktoattr import ( @@ -204,7 +207,7 @@ type fixupBlocksExpr struct { func (e *fixupBlocksExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { // In order to produce a suitable value for our expression we need to - // now decode the whole descendent block structure under each of our block + // now decode the whole descendant block structure under each of our block // bodies. // // That requires us to do something rather strange: we must construct a diff --git a/internal/lang/blocktoattr/fixup_bench_test.go b/internal/lang/blocktoattr/fixup_bench_test.go index 518fcfd0fb..a6aeffd1de 100644 --- a/internal/lang/blocktoattr/fixup_bench_test.go +++ b/internal/lang/blocktoattr/fixup_bench_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package blocktoattr import ( diff --git a/internal/lang/blocktoattr/fixup_test.go b/internal/lang/blocktoattr/fixup_test.go index 36ab48041c..ce8ee8abaa 100644 --- a/internal/lang/blocktoattr/fixup_test.go +++ b/internal/lang/blocktoattr/fixup_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package blocktoattr import ( diff --git a/internal/lang/blocktoattr/schema.go b/internal/lang/blocktoattr/schema.go index f704a04870..4ee88ec339 100644 --- a/internal/lang/blocktoattr/schema.go +++ b/internal/lang/blocktoattr/schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package blocktoattr import ( diff --git a/internal/lang/blocktoattr/variables.go b/internal/lang/blocktoattr/variables.go index 92d5931607..f05f1e8aab 100644 --- a/internal/lang/blocktoattr/variables.go +++ b/internal/lang/blocktoattr/variables.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package blocktoattr import ( diff --git a/internal/lang/blocktoattr/variables_test.go b/internal/lang/blocktoattr/variables_test.go index 94076b75c2..e2a4d6f52b 100644 --- a/internal/lang/blocktoattr/variables_test.go +++ b/internal/lang/blocktoattr/variables_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package blocktoattr import ( diff --git a/internal/lang/checks.go b/internal/lang/checks.go new file mode 100644 index 0000000000..9ad8d91e04 --- /dev/null +++ b/internal/lang/checks.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package lang + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" +) + +// EvalCheckErrorMessage makes a best effort to evaluate the given expression, +// as an error message string as we'd expect for an error_message argument +// inside a validation/condition/check block. +// +// It will either return a non-empty message string or it'll return diagnostics +// with either errors or warnings that explain why the given expression isn't +// acceptable. +func EvalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext, ruleAddr *addrs.CheckRule) (string, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + val, hclDiags := expr.Value(hclCtx) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return "", diags + } + + val, err := convert.Convert(val, cty.String) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid error message", + Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)), + Subject: expr.Range().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + return "", diags + } + if !val.IsKnown() { + return "", diags + } + if val.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid error message", + Detail: "Unsuitable value for error message: must not be null.", + Subject: expr.Range().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + return "", diags + } + + val, valMarks := val.Unmark() + if _, sensitive := valMarks[marks.Sensitive]; sensitive { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Error message refers to sensitive values", + Detail: `The error expression used to explain this condition refers to sensitive values, so Terraform will not display the resulting message. + +You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`, + Subject: expr.Range().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + return "", diags + } + + if _, ephemeral := valMarks[marks.Ephemeral]; ephemeral { + var extra interface{} + if ruleAddr != nil { + extra = &addrs.CheckRuleDiagnosticExtra{ + CheckRule: *ruleAddr, + } + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Error message refers to ephemeral values", + Detail: `The error expression used to explain this condition refers to ephemeral values, so Terraform will not display the resulting message. + +You can correct this by removing references to ephemeral values, or by using the ephemeralasnull() function on the references to not reveal ephemeral data.`, + Subject: expr.Range().Ptr(), + Extra: extra, + }) + return "", diags + } + + // NOTE: We've discarded any other marks the string might have been carrying, + // aside from the sensitive mark. + + return strings.TrimSpace(val.AsString()), diags +} diff --git a/internal/lang/data.go b/internal/lang/data.go index 710fccedc8..fe2c205a3e 100644 --- a/internal/lang/data.go +++ b/internal/lang/data.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // Data is an interface whose implementations can provide cty.Value @@ -20,7 +24,7 @@ import ( // cases where it's not possible to even determine a suitable result type, // cty.DynamicVal is returned along with errors describing the problem. type Data interface { - StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics + StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics GetCountAttr(addrs.CountAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) GetForEachAttr(addrs.ForEachAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) @@ -30,4 +34,7 @@ type Data interface { GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) GetInputVariable(addrs.InputVariable, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) + GetOutput(addrs.OutputValue, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) + GetCheckBlock(addrs.Check, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) + GetRunBlock(addrs.Run, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) } diff --git a/internal/lang/data_test.go b/internal/lang/data_test.go index e86a856183..398bc8b757 100644 --- a/internal/lang/data_test.go +++ b/internal/lang/data_test.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) type dataForTests struct { @@ -11,15 +15,18 @@ type dataForTests struct { ForEachAttrs map[string]cty.Value Resources map[string]cty.Value LocalValues map[string]cty.Value + OutputValues map[string]cty.Value Modules map[string]cty.Value PathAttrs map[string]cty.Value TerraformAttrs map[string]cty.Value InputVariables map[string]cty.Value + CheckBlocks map[string]cty.Value + RunBlocks map[string]cty.Value } var _ Data = &dataForTests{} -func (d *dataForTests) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics { +func (d *dataForTests) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { return nil // does nothing in this stub implementation } @@ -60,3 +67,15 @@ func (d *dataForTests) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) func (d *dataForTests) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { return d.TerraformAttrs[addr.Name], nil } + +func (d *dataForTests) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.OutputValues[addr.Name], nil +} + +func (d *dataForTests) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.CheckBlocks[addr.Name], nil +} + +func (d *dataForTests) GetRunBlock(addr addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.RunBlocks[addr.Name], nil +} diff --git a/internal/lang/doc.go b/internal/lang/doc.go index af5c5cac0d..2ff87b0533 100644 --- a/internal/lang/doc.go +++ b/internal/lang/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package lang deals with the runtime aspects of Terraform's configuration // language, with concerns such as expression evaluation. It is closely related // to sibling package "configs", which is responsible for configuration diff --git a/internal/lang/ephemeral/doc.go b/internal/lang/ephemeral/doc.go new file mode 100644 index 0000000000..015440fc6b --- /dev/null +++ b/internal/lang/ephemeral/doc.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package ephemeral contains helper functions for working with values that +// might have ephemeral parts. +// +// "Ephemeral" in this context means that a value is preserved only in memory +// for no longer than the duration of a single Terraform phase, and is not +// persisted as part of longer-lived artifacts such as state snapshots and +// saved plan files. Because ephemeral values cannot be persisted, they can +// be used only as part of the configuration of objects that are ephemeral +// themselves, such as provider configurations and provisioners. +package ephemeral diff --git a/internal/lang/ephemeral/marshal.go b/internal/lang/ephemeral/marshal.go new file mode 100644 index 0000000000..28f9cb8bad --- /dev/null +++ b/internal/lang/ephemeral/marshal.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +// RemoveEphemeralValues takes a value that possibly contains ephemeral +// values and returns an equal value without ephemeral values. If an attribute contains +// an ephemeral value it will be set to null. +func RemoveEphemeralValues(value cty.Value) cty.Value { + // We currently have no error case, so we can ignore the error + val, _ := cty.Transform(value, func(p cty.Path, v cty.Value) (cty.Value, error) { + _, givenMarks := v.Unmark() + if _, isEphemeral := givenMarks[marks.Ephemeral]; isEphemeral { + // We'll strip the ephemeral mark but retain any other marks + // that might be present on the input. + delete(givenMarks, marks.Ephemeral) + if !v.IsKnown() { + // If the source value is unknown then we must leave it + // unknown because its final type might be more precise + // than the associated type constraint and returning a + // typed null could therefore over-promise on what the + // final result type will be. + // We're deliberately constructing a fresh unknown value + // here, rather than returning the one we were given, + // because we need to discard any refinements that the + // unknown value might be carrying that definitely won't + // be honored when we force the final result to be null. + return cty.UnknownVal(v.Type()).WithMarks(givenMarks), nil + } + return cty.NullVal(v.Type()).WithMarks(givenMarks), nil + } + return v, nil + }) + return val +} diff --git a/internal/lang/ephemeral/marshal_test.go b/internal/lang/ephemeral/marshal_test.go new file mode 100644 index 0000000000..f61e38d110 --- /dev/null +++ b/internal/lang/ephemeral/marshal_test.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +func TestEphemeral_removeEphemeralValues(t *testing.T) { + for name, tc := range map[string]struct { + input cty.Value + want cty.Value + }{ + "empty case": { + input: cty.NullVal(cty.DynamicPseudoType), + want: cty.NullVal(cty.DynamicPseudoType), + }, + "ephemeral marks case": { + input: cty.ObjectVal(map[string]cty.Value{ + "ephemeral": cty.StringVal("ephemeral_value").Mark(marks.Ephemeral), + "normal": cty.StringVal("normal_value"), + }), + want: cty.ObjectVal(map[string]cty.Value{ + "ephemeral": cty.NullVal(cty.String), + "normal": cty.StringVal("normal_value"), + }), + }, + "sensitive marks case": { + input: cty.ObjectVal(map[string]cty.Value{ + "sensitive": cty.StringVal("sensitive_value").Mark(marks.Sensitive), + "normal": cty.StringVal("normal_value"), + }), + want: cty.ObjectVal(map[string]cty.Value{ + "sensitive": cty.StringVal("sensitive_value").Mark(marks.Sensitive), + "normal": cty.StringVal("normal_value"), + }), + }, + "sensitive and ephemeral marks case": { + input: cty.ObjectVal(map[string]cty.Value{ + "sensitive_and_ephemeral": cty.StringVal("sensitive_and_ephemeral_value").Mark(marks.Sensitive).Mark(marks.Ephemeral), + "normal": cty.StringVal("normal_value"), + }), + want: cty.ObjectVal(map[string]cty.Value{ + "sensitive_and_ephemeral": cty.NullVal(cty.String).Mark(marks.Sensitive), + "normal": cty.StringVal("normal_value"), + }), + }, + } { + t.Run(name, func(t *testing.T) { + got := RemoveEphemeralValues(tc.input) + + if !got.RawEquals(tc.want) { + t.Errorf("got %#v, want %#v", got, tc.want) + } + }) + } +} diff --git a/internal/lang/ephemeral/paths.go b/internal/lang/ephemeral/paths.go new file mode 100644 index 0000000000..3fc48d0d2a --- /dev/null +++ b/internal/lang/ephemeral/paths.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/lang/marks" +) + +// EphemeralValuePaths returns the paths within the given value that are +// marked as ephemeral, if any. +func EphemeralValuePaths(v cty.Value) []cty.Path { + _, pvms := v.UnmarkDeepWithPaths() + ret, _ := marks.PathsWithMark(pvms, marks.Ephemeral) + return ret +} diff --git a/internal/lang/ephemeral/paths_test.go b/internal/lang/ephemeral/paths_test.go new file mode 100644 index 0000000000..c865d8dee0 --- /dev/null +++ b/internal/lang/ephemeral/paths_test.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +func TestEphemeralValuePaths(t *testing.T) { + // This test is intentionally not a thorough wringing of all possible cases + // because EphemeralValuePaths is really just a thing wrapper around a + // more general function in package marks, and that function already has + // its own tests. That also in turn wraps a more-general-again function in + // upstream cty that also has its own tests. + v := cty.ObjectVal(map[string]cty.Value{ + "unmarked": cty.StringVal("unmarked"), + "sensitive": cty.StringVal("sensitive").Mark(marks.Sensitive), + "ephemeral": cty.StringVal("ephemeral").Mark(marks.Ephemeral), + "both": cty.StringVal("both").Mark(marks.Ephemeral).Mark(marks.Sensitive), + "nested": cty.ListVal([]cty.Value{ + cty.StringVal("unmarked"), + cty.StringVal("sensitive").Mark(marks.Sensitive), + cty.StringVal("ephemeral").Mark(marks.Ephemeral), + cty.StringVal("both").Mark(marks.Ephemeral).Mark(marks.Sensitive), + }), + }) + got := cty.NewPathSet(EphemeralValuePaths(v)...) + want := cty.NewPathSet( + cty.GetAttrPath("ephemeral"), + cty.GetAttrPath("both"), + cty.GetAttrPath("nested").IndexInt(2), + cty.GetAttrPath("nested").IndexInt(3), + ) + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} diff --git a/internal/lang/ephemeral/strip.go b/internal/lang/ephemeral/strip.go new file mode 100644 index 0000000000..cd6f41acb3 --- /dev/null +++ b/internal/lang/ephemeral/strip.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +// StripWriteOnlyAttributes converts all the write-only attributes in value to +// null values. +func StripWriteOnlyAttributes(value cty.Value, schema *configschema.Block) cty.Value { + // writeOnlyTransformer never returns errors, so we don't need to detect + // them here. + updated, _ := cty.TransformWithTransformer(value, &writeOnlyTransformer{ + schema: schema, + }) + return updated +} + +var _ cty.Transformer = (*writeOnlyTransformer)(nil) + +type writeOnlyTransformer struct { + schema *configschema.Block +} + +func (w *writeOnlyTransformer) Enter(path cty.Path, value cty.Value) (cty.Value, error) { + attr := w.schema.AttributeByPath(path) + if attr == nil { + return value, nil + } + + if attr.WriteOnly { + value, marks := value.Unmark() + return cty.NullVal(value.Type()).WithMarks(marks), nil + } + + return value, nil +} + +func (w *writeOnlyTransformer) Exit(_ cty.Path, value cty.Value) (cty.Value, error) { + return value, nil // no changes +} diff --git a/internal/lang/ephemeral/strip_test.go b/internal/lang/ephemeral/strip_test.go new file mode 100644 index 0000000000..c7562ed024 --- /dev/null +++ b/internal/lang/ephemeral/strip_test.go @@ -0,0 +1,193 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" +) + +func TestStripWriteOnlyAttributes(t *testing.T) { + tcs := map[string]struct { + val cty.Value + schema *configschema.Block + want cty.Value + }{ + "primitive": { + val: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + WriteOnly: true, + }, + }, + }, + want: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + }, + "complex": { + val: cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + }, + Nesting: configschema.NestingSingle, + }, + WriteOnly: true, + }, + }, + }, + want: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.Object(map[string]cty.Type{ + "value": cty.String, + })), + }), + }, + "nested in object": { + val: cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + WriteOnly: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + }, + want: cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + }), + }, + "nested in list": { + val: cty.ObjectVal(map[string]cty.Value{ + "value": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + WriteOnly: true, + }, + }, + Nesting: configschema.NestingList, + }, + }, + }, + }, + want: cty.ObjectVal(map[string]cty.Value{ + "value": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + }), + }), + }, + "nested in map": { + val: cty.ObjectVal(map[string]cty.Value{ + "value": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + WriteOnly: true, + }, + }, + Nesting: configschema.NestingMap, + }, + }, + }, + }, + want: cty.ObjectVal(map[string]cty.Value{ + "value": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + }), + }), + }, + "preserves marks": { + val: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("value"), + }).Mark(marks.Sensitive), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + WriteOnly: true, + }, + }, + }, + want: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }).Mark(marks.Sensitive), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + got := StripWriteOnlyAttributes(tc.val, tc.schema) + if diff := cmp.Diff(got, tc.want, ctydebug.CmpOptions); len(diff) > 0 { + t.Errorf("got diff:\n%s", diff) + } + }) + } +} diff --git a/internal/lang/ephemeral/validate.go b/internal/lang/ephemeral/validate.go new file mode 100644 index 0000000000..3556651f94 --- /dev/null +++ b/internal/lang/ephemeral/validate.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ValidateWriteOnlyAttributes identifies all instances of write-only paths that contain non-null values +// and returns a diagnostic for each instance +func ValidateWriteOnlyAttributes(summary string, detail func(cty.Path) string, newVal cty.Value, schema *configschema.Block) (diags tfdiags.Diagnostics) { + writeOnlyPaths, err := nonNullWriteOnlyPaths(newVal, schema) + if err != nil { + return diags.Append(tfdiags.Sourceless( + tfdiags.Error, + summary, + fmt.Sprintf("Error validating write-only attributes: %s.", err), + )) + } + if len(writeOnlyPaths) != 0 { + for _, p := range writeOnlyPaths { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + summary, + detail(p), + )) + } + } + return diags +} + +// nonNullWriteOnlyPaths returns a list of paths to attributes that are write-only +// and non-null in the given value. +func nonNullWriteOnlyPaths(val cty.Value, schema *configschema.Block) ([]cty.Path, error) { + if schema == nil { + panic("nonNullWriteOnlyPaths called wih nil schema") + } + var paths []cty.Path + + for _, path := range schema.WriteOnlyPaths(val, nil) { + // Note that path.Apply will fail if the path traverses a set, but ephemeral + // values won't work in a set anyway, and they are prohibited by the + // plugin framework. + v, err := path.Apply(val) + if err != nil { + return nil, err + } + if !v.IsNull() { + paths = append(paths, path) + } + + } + + return paths, nil +} diff --git a/internal/lang/ephemeral/validate_test.go b/internal/lang/ephemeral/validate_test.go new file mode 100644 index 0000000000..89bfdc6c59 --- /dev/null +++ b/internal/lang/ephemeral/validate_test.go @@ -0,0 +1,165 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +func TestNonNullWriteOnlyPaths(t *testing.T) { + for name, tc := range map[string]struct { + val cty.Value + schema *configschema.Block + + expectedPaths []cty.Path + }{ + "no write-only attributes": { + val: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-abc123"), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + }, + }, + }, + }, + + "write-only attribute with null value": { + val: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + + "write-only attribute with non-null value": { + val: cty.ObjectVal(map[string]cty.Value{ + "valid": cty.NullVal(cty.String), + "id": cty.StringVal("i-abc123"), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "valid": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + "id": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + expectedPaths: []cty.Path{cty.GetAttrPath("id")}, + }, + + "write-only attributes in blocks": { + val: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "valid-write-only": cty.NullVal(cty.String), + "valid": cty.StringVal("valid"), + "id": cty.StringVal("i-abc123"), + "bar": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "valid-write-only": cty.NullVal(cty.String), + "valid": cty.StringVal("valid"), + "id": cty.StringVal("i-abc123"), + }), + cty.ObjectVal(map[string]cty.Value{ + "valid-write-only": cty.NullVal(cty.String), + "valid": cty.StringVal("valid"), + "id": cty.StringVal("i-xyz123"), + }), + }), + }), + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "valid-write-only": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + "valid": { + Type: cty.String, + Optional: true, + }, + "id": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "bar": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "valid-write-only": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + "valid": { + Type: cty.String, + Optional: true, + }, + "id": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + expectedPaths: []cty.Path{ + cty.GetAttrPath("foo").Index(cty.NumberIntVal(0)).GetAttr("id"), + cty.GetAttrPath("foo").Index(cty.NumberIntVal(0)).GetAttr("bar").Index(cty.NumberIntVal(0)).GetAttr("id"), + cty.GetAttrPath("foo").Index(cty.NumberIntVal(0)).GetAttr("bar").Index(cty.NumberIntVal(1)).GetAttr("id"), + }, + }, + } { + t.Run(name, func(t *testing.T) { + paths, err := nonNullWriteOnlyPaths(tc.val, tc.schema) + if err != nil { + t.Fatal(err) + } + + if len(paths) != len(tc.expectedPaths) { + t.Fatalf("expected %d write-only paths, got %d", len(tc.expectedPaths), len(paths)) + } + + for i, path := range paths { + if !path.Equals(tc.expectedPaths[i]) { + t.Fatalf("expected path %#v, got %#v", tc.expectedPaths[i], path) + } + } + }) + } +} diff --git a/internal/lang/eval.go b/internal/lang/eval.go index 5c82392bcc..e654e2e123 100644 --- a/internal/lang/eval.go +++ b/internal/lang/eval.go @@ -1,18 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( "fmt" + "log" + "strings" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/ext/dynblock" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "maps" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang/blocktoattr" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" ) // ExpandBlock expands any "dynamic" blocks present in the given body. The @@ -25,7 +35,7 @@ func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body spec := schema.DecoderSpec() traversals := dynblock.ExpandVariablesHCLDec(body, spec) - refs, diags := References(traversals) + refs, diags := langrefs.References(s.ParseRef, traversals) ctx, ctxDiags := s.EvalContext(refs) diags = diags.Append(ctxDiags) @@ -46,7 +56,7 @@ func (s *Scope) ExpandBlock(body hcl.Body, schema *configschema.Block) (hcl.Body func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { spec := schema.DecoderSpec() - refs, diags := ReferencesInBlock(body, schema) + refs, diags := langrefs.ReferencesInBlock(s.ParseRef, body, schema) ctx, ctxDiags := s.EvalContext(refs) diags = diags.Append(ctxDiags) @@ -65,7 +75,7 @@ func (s *Scope) EvalBlock(body hcl.Body, schema *configschema.Block) (cty.Value, body = blocktoattr.FixUpBlockAttrs(body, schema) val, evalDiags := hcldec.Decode(body, spec, ctx) - diags = diags.Append(evalDiags) + diags = diags.Append(checkForUnknownFunctionDiags(evalDiags)) return val, diags } @@ -93,7 +103,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem }) } - refs, refDiags := References(hcldec.Variables(body, spec)) + refs, refDiags := langrefs.References(s.ParseRef, hcldec.Variables(body, spec)) diags = diags.Append(refDiags) terraformAttrs := map[string]cty.Value{} @@ -143,7 +153,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem } val, decDiags := hcldec.Decode(body, schema.DecoderSpec(), ctx) - diags = diags.Append(decDiags) + diags = diags.Append(checkForUnknownFunctionDiags(decDiags)) return val, diags } @@ -158,7 +168,7 @@ func (s *Scope) EvalSelfBlock(body hcl.Body, self cty.Value, schema *configschem // If the returned diagnostics contains errors then the result may be // incomplete, but will always be of the requested type. func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) { - refs, diags := ReferencesInExpr(expr) + refs, diags := langrefs.ReferencesInExpr(s.ParseRef, expr) ctx, ctxDiags := s.EvalContext(refs) diags = diags.Append(ctxDiags) @@ -169,7 +179,7 @@ func (s *Scope) EvalExpr(expr hcl.Expression, wantType cty.Type) (cty.Value, tfd } val, evalDiags := expr.Value(ctx) - diags = diags.Append(evalDiags) + diags = diags.Append(checkForUnknownFunctionDiags(evalDiags)) if wantType != cty.DynamicPseudoType { var convErr error @@ -259,7 +269,7 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl // First we'll do static validation of the references. This catches things // early that might otherwise not get caught due to unknown values being // present in the scope during planning. - staticDiags := s.Data.StaticValidateReferences(refs, selfAddr) + staticDiags := s.Data.StaticValidateReferences(refs, selfAddr, s.SourceAddr) diags = diags.Append(staticDiags) if staticDiags.HasErrors() { return ctx, diags @@ -275,13 +285,17 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl // that's redundant in the process of populating our values map. dataResources := map[string]map[string]cty.Value{} managedResources := map[string]map[string]cty.Value{} + ephemeralResources := map[string]map[string]cty.Value{} wholeModules := map[string]cty.Value{} inputVariables := map[string]cty.Value{} localValues := map[string]cty.Value{} + outputValues := map[string]cty.Value{} pathAttrs := map[string]cty.Value{} terraformAttrs := map[string]cty.Value{} countAttrs := map[string]cty.Value{} forEachAttrs := map[string]cty.Value{} + checkBlocks := map[string]cty.Value{} + runBlocks := map[string]cty.Value{} var self cty.Value for _, ref := range refs { @@ -354,6 +368,8 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl into = managedResources case addrs.DataResourceMode: into = dataResources + case addrs.EphemeralResourceMode: + into = ephemeralResources default: panic(fmt.Errorf("unsupported ResourceMode %s", subj.Mode)) } @@ -402,6 +418,21 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl diags = diags.Append(valDiags) forEachAttrs[subj.Name] = val + case addrs.OutputValue: + val, valDiags := normalizeRefValue(s.Data.GetOutput(subj, rng)) + diags = diags.Append(valDiags) + outputValues[subj.Name] = val + + case addrs.Check: + val, valDiags := normalizeRefValue(s.Data.GetCheckBlock(subj, rng)) + diags = diags.Append(valDiags) + checkBlocks[subj.Name] = val + + case addrs.Run: + val, valDiags := normalizeRefValue(s.Data.GetRunBlock(subj, rng)) + diags = diags.Append(valDiags) + runBlocks[subj.Name] = val + default: // Should never happen panic(fmt.Errorf("Scope.buildEvalContext cannot handle address type %T", rawSubj)) @@ -413,11 +444,11 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl // traversal, but we also expose them under "resource" as an escaping // technique if we add a reserved name in a future language edition which // conflicts with someone's existing provider. - for k, v := range buildResourceObjects(managedResources) { - vals[k] = v - } - vals["resource"] = cty.ObjectVal(buildResourceObjects(managedResources)) + builtManagedResources := buildResourceObjects(managedResources) + maps.Copy(vals, builtManagedResources) + vals["resource"] = cty.ObjectVal(builtManagedResources) + vals["ephemeral"] = cty.ObjectVal(buildResourceObjects(ephemeralResources)) vals["data"] = cty.ObjectVal(buildResourceObjects(dataResources)) vals["module"] = cty.ObjectVal(wholeModules) vals["var"] = cty.ObjectVal(inputVariables) @@ -426,6 +457,22 @@ func (s *Scope) evalContext(refs []*addrs.Reference, selfAddr addrs.Referenceabl vals["terraform"] = cty.ObjectVal(terraformAttrs) vals["count"] = cty.ObjectVal(countAttrs) vals["each"] = cty.ObjectVal(forEachAttrs) + + // Checks, outputs, and run blocks are conditionally included in the + // available scope, so we'll only write out their values if we actually have + // something for them. + if len(checkBlocks) > 0 { + vals["check"] = cty.ObjectVal(checkBlocks) + } + + if len(outputValues) > 0 { + vals["output"] = cty.ObjectVal(outputValues) + } + + if len(runBlocks) > 0 { + vals["run"] = cty.ObjectVal(runBlocks) + } + if self != cty.NilVal { vals["self"] = self } @@ -451,3 +498,87 @@ func normalizeRefValue(val cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfd } return val, diags } + +// checkForUnknownFunctionDiags inspects the diagnostics for errors from unknown +// function calls, and tailors the messages to better suit Terraform. We now +// have multiple namespaces where functions may be declared, and it's up to the +// user to have properly configured the module to populate the provider +// namespace. The generic unknown function diagnostic from hcl does not direct +// the user on how to remedy the situation in Terraform, and we can give more +// useful information in a few Terraform specific cases here. +func checkForUnknownFunctionDiags(diags hcl.Diagnostics) hcl.Diagnostics { + for _, d := range diags { + extra, ok := hcl.DiagnosticExtra[hclsyntax.FunctionCallUnknownDiagExtra](d) + if !ok { + continue + } + name := extra.CalledFunctionName() + namespace := extra.CalledFunctionNamespace() + namespaceParts := strings.Split(namespace, "::") + if len(namespaceParts) < 2 { + // no namespace (namespace includes ::, so will have at least 2 + // parts), but check if there is a matching name in a provider + // namspace. + if d.EvalContext == nil { + continue + } + + for funcName := range d.EvalContext.Functions { + if strings.HasSuffix(funcName, "::"+name) { + d.Detail = fmt.Sprintf("%s Did you mean %q?", d.Detail, funcName) + break + } + } + continue + } + + // the diagnostic isn't really shared with anything, and copying would + // still retain the internal pointers, so we're going to modify the + // diagnostic in-place if we want to change the output. Log the original + // diagnostic for debugging purposes in case we overwrite something + // potentially useful in the future from hcl. + log.Printf("[ERROR] UnknownFunctionCall: %s", d.Error()) + d.Summary = "Unknown provider function" + + if namespaceParts[0] != "provider" { + // help if the user is skipping the provider:: prefix before the + // provider name. + d.Detail = fmt.Sprintf(`The function namespace %q is not valid. Provider function calls must use the "provider::" namespace prefix.`, namespaceParts[0]) + continue + } + + if namespaceParts[1] == "" { + // missing provider name entirely + d.Detail = `The function call must include the provider name after the "provider::" prefix.` + continue + } + + if d.EvalContext == nil { + // There's no eval context for some reason, so we can't inspect the + // available functions. + d.Detail = fmt.Sprintf(`There is no function named "%s%s".`, namespace, name) + continue + } + + otherProviderFuncs := false + for funcName := range d.EvalContext.Functions { + // there are other functions in this provider namespace, so it must + // have been included in the configuration, and we can be clear that + // this a function which the provider does not support. + if strings.HasPrefix(funcName, namespace) { + otherProviderFuncs = true + break + } + } + if otherProviderFuncs { + d.Detail = fmt.Sprintf("The function %q is not available from the provider %q.", name, namespaceParts[1]) + continue + } + + // no other functions exist for this provider, so hint that the user may + // need to include it in the configuration. + d.Detail = fmt.Sprintf(`There is no function named "%s%s". Ensure that provider name %q is declared in this module's required_providers block, and that this provider offers a function named %q.`, namespace, name, namespaceParts[1], name) + } + + return diags +} diff --git a/internal/lang/eval_test.go b/internal/lang/eval_test.go index 37e9e54a0c..3d84360f5f 100644 --- a/internal/lang/eval_test.go +++ b/internal/lang/eval_test.go @@ -1,13 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( "bytes" "encoding/json" + "fmt" "testing" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -32,6 +37,9 @@ func TestScopeEvalContext(t *testing.T) { "data.null_data_source.foo": cty.ObjectVal(map[string]cty.Value{ "attr": cty.StringVal("bar"), }), + "ephemeral.null_secret.foo": cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("ephemeral"), + }), "null_resource.multi": cty.TupleVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "attr": cty.StringVal("multi0"), @@ -70,51 +78,70 @@ func TestScopeEvalContext(t *testing.T) { InputVariables: map[string]cty.Value{ "baz": cty.StringVal("boop"), }, + OutputValues: map[string]cty.Value{ + "rootoutput0": cty.StringVal("rootbar0"), + "rootoutput1": cty.StringVal("rootbar1"), + }, + CheckBlocks: map[string]cty.Value{ + "check0": cty.ObjectVal(map[string]cty.Value{ + "status": cty.StringVal("pass"), + }), + "check1": cty.ObjectVal(map[string]cty.Value{ + "status": cty.StringVal("fail"), + }), + }, + RunBlocks: map[string]cty.Value{ + "zero": cty.ObjectVal(map[string]cty.Value{ + "run0output0": cty.StringVal("run0bar0"), + "run0output1": cty.StringVal("run0bar1"), + }), + }, } tests := []struct { - Expr string - Want map[string]cty.Value + Expr string + Want map[string]cty.Value + TestingOnly bool }{ { - `12`, - map[string]cty.Value{}, + Expr: `12`, + Want: map[string]cty.Value{}, }, { - `count.index`, - map[string]cty.Value{ + Expr: `count.index`, + Want: map[string]cty.Value{ "count": cty.ObjectVal(map[string]cty.Value{ "index": cty.NumberIntVal(0), }), }, }, { - `each.key`, - map[string]cty.Value{ + Expr: `each.key`, + Want: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "key": cty.StringVal("a"), }), }, }, { - `each.value`, - map[string]cty.Value{ + Expr: `each.value`, + Want: map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "value": cty.NumberIntVal(1), }), }, }, { - `local.foo`, - map[string]cty.Value{ + Expr: `local.foo`, + Want: map[string]cty.Value{ "local": cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("bar"), }), }, }, { - `null_resource.foo`, - map[string]cty.Value{ + Expr: `null_resource.foo`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "attr": cty.StringVal("bar"), @@ -130,8 +157,8 @@ func TestScopeEvalContext(t *testing.T) { }, }, { - `null_resource.foo.attr`, - map[string]cty.Value{ + Expr: `null_resource.foo.attr`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "attr": cty.StringVal("bar"), @@ -147,8 +174,8 @@ func TestScopeEvalContext(t *testing.T) { }, }, { - `null_resource.multi`, - map[string]cty.Value{ + Expr: `null_resource.multi`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "multi": cty.TupleVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -175,8 +202,8 @@ func TestScopeEvalContext(t *testing.T) { }, { // at this level, all instance references return the entire resource - `null_resource.multi[1]`, - map[string]cty.Value{ + Expr: `null_resource.multi[1]`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "multi": cty.TupleVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -203,8 +230,8 @@ func TestScopeEvalContext(t *testing.T) { }, { // at this level, all instance references return the entire resource - `null_resource.each["each1"]`, - map[string]cty.Value{ + Expr: `null_resource.each["each1"]`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "each0": cty.ObjectVal(map[string]cty.Value{ @@ -231,8 +258,8 @@ func TestScopeEvalContext(t *testing.T) { }, { // at this level, all instance references return the entire resource - `null_resource.each["each1"].attr`, - map[string]cty.Value{ + Expr: `null_resource.each["each1"].attr`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "each": cty.ObjectVal(map[string]cty.Value{ "each0": cty.ObjectVal(map[string]cty.Value{ @@ -258,8 +285,8 @@ func TestScopeEvalContext(t *testing.T) { }, }, { - `foo(null_resource.multi, null_resource.multi[1])`, - map[string]cty.Value{ + Expr: `foo(null_resource.multi, null_resource.multi[1])`, + Want: map[string]cty.Value{ "null_resource": cty.ObjectVal(map[string]cty.Value{ "multi": cty.TupleVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -285,8 +312,8 @@ func TestScopeEvalContext(t *testing.T) { }, }, { - `data.null_data_source.foo`, - map[string]cty.Value{ + Expr: `data.null_data_source.foo`, + Want: map[string]cty.Value{ "data": cty.ObjectVal(map[string]cty.Value{ "null_data_source": cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ @@ -297,8 +324,20 @@ func TestScopeEvalContext(t *testing.T) { }, }, { - `module.foo`, - map[string]cty.Value{ + Expr: `ephemeral.null_secret.foo`, + Want: map[string]cty.Value{ + "ephemeral": cty.ObjectVal(map[string]cty.Value{ + "null_secret": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("ephemeral"), + }), + }), + }), + }, + }, + { + Expr: `module.foo`, + Want: map[string]cty.Value{ "module": cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "output0": cty.StringVal("bar0"), @@ -309,8 +348,8 @@ func TestScopeEvalContext(t *testing.T) { }, // any module reference returns the entire module { - `module.foo.output1`, - map[string]cty.Value{ + Expr: `module.foo.output1`, + Want: map[string]cty.Value{ "module": cty.ObjectVal(map[string]cty.Value{ "foo": cty.ObjectVal(map[string]cty.Value{ "output0": cty.StringVal("bar0"), @@ -320,97 +359,158 @@ func TestScopeEvalContext(t *testing.T) { }, }, { - `path.module`, - map[string]cty.Value{ + Expr: `path.module`, + Want: map[string]cty.Value{ "path": cty.ObjectVal(map[string]cty.Value{ "module": cty.StringVal("foo/bar"), }), }, }, { - `self.baz`, - map[string]cty.Value{ + Expr: `self.baz`, + Want: map[string]cty.Value{ "self": cty.ObjectVal(map[string]cty.Value{ "attr": cty.StringVal("multi1"), }), }, }, { - `terraform.workspace`, - map[string]cty.Value{ + Expr: `terraform.workspace`, + Want: map[string]cty.Value{ "terraform": cty.ObjectVal(map[string]cty.Value{ "workspace": cty.StringVal("default"), }), }, }, { - `var.baz`, - map[string]cty.Value{ + Expr: `var.baz`, + Want: map[string]cty.Value{ "var": cty.ObjectVal(map[string]cty.Value{ "baz": cty.StringVal("boop"), }), }, }, + { + Expr: "run.zero", + Want: map[string]cty.Value{ + "run": cty.ObjectVal(map[string]cty.Value{ + "zero": cty.ObjectVal(map[string]cty.Value{ + "run0output0": cty.StringVal("run0bar0"), + "run0output1": cty.StringVal("run0bar1"), + }), + }), + }, + TestingOnly: true, + }, + { + Expr: "run.zero.run0output0", + Want: map[string]cty.Value{ + "run": cty.ObjectVal(map[string]cty.Value{ + "zero": cty.ObjectVal(map[string]cty.Value{ + "run0output0": cty.StringVal("run0bar0"), + "run0output1": cty.StringVal("run0bar1"), + }), + }), + }, + TestingOnly: true, + }, + { + Expr: "output.rootoutput0", + Want: map[string]cty.Value{ + "output": cty.ObjectVal(map[string]cty.Value{ + "rootoutput0": cty.StringVal("rootbar0"), + }), + }, + TestingOnly: true, + }, + { + Expr: "check.check0", + Want: map[string]cty.Value{ + "check": cty.ObjectVal(map[string]cty.Value{ + "check0": cty.ObjectVal(map[string]cty.Value{ + "status": cty.StringVal("pass"), + }), + }), + }, + TestingOnly: true, + }, + } + + exec := func(t *testing.T, parseRef langrefs.ParseRef, test struct { + Expr string + Want map[string]cty.Value + TestingOnly bool + }) { + expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1}) + if len(parseDiags) != 0 { + t.Errorf("unexpected diagnostics during parse") + for _, diag := range parseDiags { + t.Errorf("- %s", diag) + } + return + } + + refs, refsDiags := langrefs.ReferencesInExpr(parseRef, expr) + if refsDiags.HasErrors() { + t.Fatal(refsDiags.Err()) + } + + scope := &Scope{ + Data: data, + ParseRef: parseRef, + + // "self" will just be an arbitrary one of the several resource + // instances we have in our test dataset. + SelfAddr: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "null_resource", + Name: "multi", + }, + Key: addrs.IntKey(1), + }, + } + ctx, ctxDiags := scope.EvalContext(refs) + if ctxDiags.HasErrors() { + t.Fatal(ctxDiags.Err()) + } + + // For easier test assertions we'll just remove any top-level + // empty objects from our variables map. + for k, v := range ctx.Variables { + if v.RawEquals(cty.EmptyObjectVal) { + delete(ctx.Variables, k) + } + } + + gotVal := cty.ObjectVal(ctx.Variables) + wantVal := cty.ObjectVal(test.Want) + + if !gotVal.RawEquals(wantVal) { + // We'll JSON-ize our values here just so it's easier to + // read them in the assertion output. + gotJSON := formattedJSONValue(gotVal) + wantJSON := formattedJSONValue(wantVal) + + t.Errorf( + "wrong result\nexpr: %s\ngot: %s\nwant: %s", + test.Expr, gotJSON, wantJSON, + ) + } } for _, test := range tests { - t.Run(test.Expr, func(t *testing.T) { - expr, parseDiags := hclsyntax.ParseExpression([]byte(test.Expr), "", hcl.Pos{Line: 1, Column: 1}) - if len(parseDiags) != 0 { - t.Errorf("unexpected diagnostics during parse") - for _, diag := range parseDiags { - t.Errorf("- %s", diag) - } - return - } - refs, refsDiags := ReferencesInExpr(expr) - if refsDiags.HasErrors() { - t.Fatal(refsDiags.Err()) - } + if !test.TestingOnly { + t.Run(test.Expr, func(t *testing.T) { + exec(t, addrs.ParseRef, test) + }) + } - scope := &Scope{ - Data: data, - - // "self" will just be an arbitrary one of the several resource - // instances we have in our test dataset. - SelfAddr: addrs.ResourceInstance{ - Resource: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "multi", - }, - Key: addrs.IntKey(1), - }, - } - ctx, ctxDiags := scope.EvalContext(refs) - if ctxDiags.HasErrors() { - t.Fatal(ctxDiags.Err()) - } - - // For easier test assertions we'll just remove any top-level - // empty objects from our variables map. - for k, v := range ctx.Variables { - if v.RawEquals(cty.EmptyObjectVal) { - delete(ctx.Variables, k) - } - } - - gotVal := cty.ObjectVal(ctx.Variables) - wantVal := cty.ObjectVal(test.Want) - - if !gotVal.RawEquals(wantVal) { - // We'll JSON-ize our values here just so it's easier to - // read them in the assertion output. - gotJSON := formattedJSONValue(gotVal) - wantJSON := formattedJSONValue(wantVal) - - t.Errorf( - "wrong result\nexpr: %s\ngot: %s\nwant: %s", - test.Expr, gotJSON, wantJSON, - ) - } + t.Run(fmt.Sprintf("%s-testing", test.Expr), func(t *testing.T) { + exec(t, addrs.ParseRefFromTestingScope, test) }) + } } @@ -677,7 +777,8 @@ func TestScopeExpandEvalBlock(t *testing.T) { body := file.Body scope := &Scope{ - Data: data, + Data: data, + ParseRef: addrs.ParseRef, } body, expandDiags := scope.ExpandBlock(body, schema) @@ -823,7 +924,8 @@ func TestScopeEvalSelfBlock(t *testing.T) { body := file.Body scope := &Scope{ - Data: data, + Data: data, + ParseRef: addrs.ParseRef, } gotVal, ctxDiags := scope.EvalSelfBlock(body, test.Self, schema, test.KeyData) diff --git a/internal/lang/format/format.go b/internal/lang/format/format.go new file mode 100644 index 0000000000..cbca4f834c --- /dev/null +++ b/internal/lang/format/format.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/zclconf/go-cty/cty" +) + +// CtyPath is a helper function to produce a user-friendly string +// representation of a cty.Path. The result uses a syntax similar to the +// HCL expression language in the hope of it being familiar to users. +func CtyPath(path cty.Path) string { + var buf strings.Builder + for _, step := range path { + switch ts := step.(type) { + case cty.GetAttrStep: + fmt.Fprintf(&buf, ".%s", ts.Name) + case cty.IndexStep: + buf.WriteByte('[') + key := ts.Key + keyTy := key.Type() + switch { + case key.IsNull(): + buf.WriteString("null") + case !key.IsKnown(): + buf.WriteString("(not yet known)") + case keyTy == cty.Number: + bf := key.AsBigFloat() + buf.WriteString(bf.Text('g', -1)) + case keyTy == cty.String: + buf.WriteString(strconv.Quote(key.AsString())) + default: + buf.WriteString("...") + } + buf.WriteByte(']') + } + } + return buf.String() +} + +// ErrorDiag is a helper function to produce a user-friendly string +// representation of certain special error types that we might want to +// include in diagnostic messages. +func ErrorDiag(err error) string { + perr, ok := err.(cty.PathError) + if !ok || len(perr.Path) == 0 { + return err.Error() + } + + return fmt.Sprintf("%s: %s", CtyPath(perr.Path), perr.Error()) +} + +// ErrorDiagPrefixed is like Error except that it presents any path +// information after the given prefix string, which is assumed to contain +// an HCL syntax representation of the value that errors are relative to. +func ErrorDiagPrefixed(err error, prefix string) string { + perr, ok := err.(cty.PathError) + if !ok || len(perr.Path) == 0 { + return fmt.Sprintf("%s: %s", prefix, err.Error()) + } + + return fmt.Sprintf("%s%s: %s", prefix, CtyPath(perr.Path), perr.Error()) +} diff --git a/internal/lang/funcs/cidr.go b/internal/lang/funcs/cidr.go index bf878b50cb..3b20d5d83a 100644 --- a/internal/lang/funcs/cidr.go +++ b/internal/lang/funcs/cidr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -24,7 +27,8 @@ var CidrHostFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var hostNum *big.Int if err := gocty.FromCtyValue(args[1], &hostNum); err != nil { @@ -53,7 +57,8 @@ var CidrNetmaskFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { _, network, err := ipaddr.ParseCIDR(args[0].AsString()) if err != nil { @@ -85,7 +90,8 @@ var CidrSubnetFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var newbits int if err := gocty.FromCtyValue(args[1], &newbits); err != nil { @@ -123,7 +129,8 @@ var CidrSubnetsFunc = function.New(&function.Spec{ Name: "newbits", Type: cty.Number, }, - Type: function.StaticReturnType(cty.List(cty.String)), + Type: function.StaticReturnType(cty.List(cty.String)), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { _, network, err := ipaddr.ParseCIDR(args[0].AsString()) if err != nil { diff --git a/internal/lang/funcs/cidr_test.go b/internal/lang/funcs/cidr_test.go index 5d8a960589..2845a0ab32 100644 --- a/internal/lang/funcs/cidr_test.go +++ b/internal/lang/funcs/cidr_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( diff --git a/internal/lang/funcs/collection.go b/internal/lang/funcs/collection.go index 82a9deee9f..fd41620230 100644 --- a/internal/lang/funcs/collection.go +++ b/internal/lang/funcs/collection.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -32,10 +35,12 @@ var LengthFunc = function.New(&function.Spec{ return cty.Number, errors.New("argument must be a string, a collection type, or a structural type") } }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { coll := args[0] collTy := args[0].Type() marks := coll.Marks() + switch { case collTy == cty.DynamicPseudoType: return cty.UnknownVal(cty.Number).WithMarks(marks), nil @@ -68,7 +73,8 @@ var AllTrueFunc = function.New(&function.Spec{ Type: cty.List(cty.Bool), }, }, - Type: function.StaticReturnType(cty.Bool), + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { result := cty.True for it := args[0].ElementIterator(); it.Next(); { @@ -97,7 +103,8 @@ var AnyTrueFunc = function.New(&function.Spec{ Type: cty.List(cty.Bool), }, }, - Type: function.StaticReturnType(cty.Bool), + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { result := cty.False var hasUnknown bool @@ -146,6 +153,7 @@ var CoalesceFunc = function.New(&function.Spec{ } return retType, nil }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { for _, argVal := range args { // We already know this will succeed because of the checks in our Type func above @@ -178,7 +186,8 @@ var IndexFunc = function.New(&function.Spec{ Type: cty.DynamicPseudoType, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) { return cty.NilVal, errors.New("argument must be a list or tuple") @@ -214,14 +223,16 @@ var IndexFunc = function.New(&function.Spec{ var LookupFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "inputMap", - Type: cty.DynamicPseudoType, - AllowMarked: true, + Name: "inputMap", + Type: cty.DynamicPseudoType, + AllowMarked: true, + AllowUnknown: true, }, { - Name: "key", - Type: cty.String, - AllowMarked: true, + Name: "key", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, }, }, VarParam: &function.Parameter{ @@ -268,7 +279,7 @@ var LookupFunc = function.New(&function.Spec{ } }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - var defaultVal cty.Value + defaultVal := cty.NullVal(retType) defaultValueSet := false if len(args) == 3 { @@ -289,12 +300,13 @@ var LookupFunc = function.New(&function.Spec{ if len(keyMarks) > 0 { markses = append(markses, keyMarks) } - lookupKey := keyVal.AsString() - if !mapVar.IsKnown() { + if !(mapVar.IsKnown() && keyVal.IsKnown()) { return cty.UnknownVal(retType).WithMarks(markses...), nil } + lookupKey := keyVal.AsString() + if mapVar.Type().IsObjectType() { if mapVar.Type().HasAttribute(lookupKey) { return mapVar.GetAttr(lookupKey).WithMarks(markses...), nil @@ -343,6 +355,7 @@ var MatchkeysFunc = function.New(&function.Spec{ // the return type is based on args[0] (values) return args[0].Type(), nil }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !args[0].IsKnown() { return cty.UnknownVal(cty.List(retType.ElementType())), nil @@ -486,7 +499,8 @@ var SumFunc = function.New(&function.Spec{ Type: cty.DynamicPseudoType, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { if !args[0].CanIterateElements() { @@ -501,7 +515,7 @@ var SumFunc = function.New(&function.Spec{ ty := args[0].Type() if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() { - return cty.NilVal, function.NewArgErrorf(0, fmt.Sprintf("argument must be list, set, or tuple. Received %s", ty.FriendlyName())) + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple. Received %s", ty.FriendlyName()) } if !args[0].IsWhollyKnown() { @@ -555,7 +569,8 @@ var TransposeFunc = function.New(&function.Spec{ Type: cty.Map(cty.List(cty.String)), }, }, - Type: function.StaticReturnType(cty.Map(cty.List(cty.String))), + Type: function.StaticReturnType(cty.Map(cty.List(cty.String))), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { inputMap := args[0] if !inputMap.IsWhollyKnown() { @@ -567,8 +582,14 @@ var TransposeFunc = function.New(&function.Spec{ for it := inputMap.ElementIterator(); it.Next(); { inKey, inVal := it.Element() + if inVal.IsNull() { + return cty.MapValEmpty(cty.List(cty.String)), errors.New("input must not contain null list") + } for iter := inVal.ElementIterator(); iter.Next(); { _, val := iter.Element() + if val.IsNull() { + return cty.MapValEmpty(cty.List(cty.String)), errors.New("input list must not contain null string") + } if !val.Type().Equals(cty.String) { return cty.MapValEmpty(cty.List(cty.String)), errors.New("input must be a map of lists of strings") } diff --git a/internal/lang/funcs/collection_test.go b/internal/lang/funcs/collection_test.go index d470f357ed..e7fc9ab980 100644 --- a/internal/lang/funcs/collection_test.go +++ b/internal/lang/funcs/collection_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -68,11 +71,15 @@ func TestLength(t *testing.T) { }, { cty.UnknownVal(cty.List(cty.Bool)), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).Refine(). + NotNull(). + NumberRangeLowerBound(cty.Zero, true). + NumberRangeUpperBound(cty.NumberIntVal(math.MaxInt), true). + NewValue(), }, { cty.DynamicVal, - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), }, { cty.StringVal("hello"), @@ -117,11 +124,10 @@ func TestLength(t *testing.T) { }, { cty.UnknownVal(cty.String), - cty.UnknownVal(cty.Number), - }, - { - cty.DynamicVal, - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).Refine(). + NotNull(). + NumberRangeLowerBound(cty.Zero, true). + NewValue(), }, { // Marked collections return a marked length cty.ListVal([]cty.Value{ @@ -163,6 +169,10 @@ func TestLength(t *testing.T) { }).Mark("secret"), cty.NumberIntVal(3).Mark("secret"), }, + { // Marked objects return a marked length + cty.UnknownVal(cty.String).Mark("secret"), + cty.UnknownVal(cty.Number).Refine().NotNull().NumberRangeLowerBound(cty.NumberIntVal(0), true).NewValue().Mark("secret"), + }, { // Marks on object attribute values do not propagate cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello").Mark("a"), @@ -226,7 +236,7 @@ func TestAllTrue(t *testing.T) { }, { cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}), - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { @@ -234,12 +244,12 @@ func TestAllTrue(t *testing.T) { cty.UnknownVal(cty.Bool), cty.UnknownVal(cty.Bool), }), - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { cty.UnknownVal(cty.List(cty.Bool)), - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { @@ -307,7 +317,7 @@ func TestAnyTrue(t *testing.T) { }, { cty.ListVal([]cty.Value{cty.UnknownVal(cty.Bool)}), - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { @@ -315,7 +325,7 @@ func TestAnyTrue(t *testing.T) { cty.UnknownVal(cty.Bool), cty.False, }), - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { @@ -328,7 +338,7 @@ func TestAnyTrue(t *testing.T) { }, { cty.UnknownVal(cty.List(cty.Bool)), - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { @@ -406,17 +416,17 @@ func TestCoalesce(t *testing.T) { }, { []cty.Value{cty.UnknownVal(cty.Bool), cty.True}, - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { []cty.Value{cty.UnknownVal(cty.Bool), cty.StringVal("hello")}, - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), false, }, { []cty.Value{cty.DynamicVal, cty.True}, - cty.UnknownVal(cty.Bool), + cty.UnknownVal(cty.Bool).RefineNotNull(), false, }, { @@ -878,6 +888,15 @@ func TestLookup(t *testing.T) { cty.StringVal("beep").Mark("a"), false, }, + { // propagate marks from unknown map + []cty.Value{ + cty.UnknownVal(cty.Map(cty.String)).Mark("a"), + cty.StringVal("boop").Mark("b"), + cty.StringVal("nope"), + }, + cty.UnknownVal(cty.String).Mark("a").Mark("b"), + false, + }, } for _, test := range tests { @@ -1062,7 +1081,7 @@ func TestMatchkeys(t *testing.T) { cty.ListVal([]cty.Value{ cty.StringVal("ref1"), }), - cty.UnknownVal(cty.List(cty.String)), + cty.UnknownVal(cty.List(cty.String)).RefineNotNull(), false, }, { // different types that can be unified @@ -1526,7 +1545,7 @@ func TestSum(t *testing.T) { cty.StringVal("b"), cty.StringVal("c"), }), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), "argument must be list, set, or tuple of number values", }, { @@ -1580,7 +1599,7 @@ func TestSum(t *testing.T) { cty.StringVal("a"), cty.NumberIntVal(38), }), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), "argument must be list, set, or tuple of number values", }, { @@ -1600,17 +1619,17 @@ func TestSum(t *testing.T) { }, { cty.UnknownVal(cty.Number), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), "", }, { cty.UnknownVal(cty.List(cty.Number)), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), "", }, { // known list containing unknown values cty.ListVal([]cty.Value{cty.UnknownVal(cty.Number)}), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), "", }, { // numbers too large to represent as float64 @@ -1704,7 +1723,7 @@ func TestTranspose(t *testing.T) { cty.MapVal(map[string]cty.Value{ "key1": cty.UnknownVal(cty.List(cty.String)), }), - cty.UnknownVal(cty.Map(cty.List(cty.String))), + cty.UnknownVal(cty.Map(cty.List(cty.String))).RefineNotNull(), false, }, { // bad map - empty value @@ -1814,6 +1833,37 @@ func TestTranspose(t *testing.T) { }).WithMarks(cty.NewValueMarks("beep", "boop", "bloop")), false, }, + { + cty.NullVal(cty.Map(cty.List(cty.String))), + cty.NilVal, + true, + }, + { + cty.MapVal(map[string]cty.Value{ + "test": cty.NullVal(cty.List(cty.String)), + }), + cty.NilVal, + true, + }, + { + cty.MapVal(map[string]cty.Value{ + "test": cty.ListVal([]cty.Value{cty.NullVal(cty.String)}), + }), + cty.NilVal, + true, + }, + { + cty.UnknownVal(cty.Map(cty.List(cty.String))), + cty.UnknownVal(cty.Map(cty.List(cty.String))).RefineNotNull(), + false, + }, + { + cty.MapVal(map[string]cty.Value{ + "test": cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}), + }), + cty.UnknownVal(cty.Map(cty.List(cty.String))).RefineNotNull(), + false, + }, } for _, test := range tests { diff --git a/internal/lang/funcs/conversion.go b/internal/lang/funcs/conversion.go index 721606226e..7b42bcd722 100644 --- a/internal/lang/funcs/conversion.go +++ b/internal/lang/funcs/conversion.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( "strconv" + "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/lang/types" "github.com/zclconf/go-cty/cty" @@ -34,6 +38,7 @@ func MakeToFunc(wantTy cty.Type) function.Function { AllowNull: true, AllowMarked: true, AllowDynamicType: true, + AllowUnknown: true, }, }, Type: func(args []cty.Value) (cty.Type, error) { @@ -58,8 +63,10 @@ func MakeToFunc(wantTy cty.Type) function.Function { return wantTy, nil }, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - // We didn't set "AllowUnknown" on our argument, so it is guaranteed - // to be known here but may still be null. + if !args[0].IsKnown() { + return cty.UnknownVal(retType).WithSameMarks(args[0]), nil + } + ret, err := convert.Convert(args[0], retType) if err != nil { val, _ := args[0].UnmarkDeep() @@ -96,6 +103,38 @@ func MakeToFunc(wantTy cty.Type) function.Function { }) } +// EphemeralAsNullFunc is a cty function that takes a value of any type and +// returns a similar value with any ephemeral-marked values anywhere in the +// structure replaced with a null value of the same type that is not marked +// as ephemeral. +// +// This is intended as a convenience for returning the non-ephemeral parts of +// a partially-ephemeral data structure through an output value that isn't +// ephemeral itself. +var EphemeralAsNullFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowDynamicType: true, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + // This function always preserves the type of the given argument. + return args[0].Type(), nil + }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return ephemeral.RemoveEphemeralValues(args[0]), nil + }, +}) + +func EphemeralAsNull(input cty.Value) (cty.Value, error) { + return EphemeralAsNullFunc.Call([]cty.Value{input}) +} + // TypeFunc returns an encapsulated value containing its argument's type. This // value is marked to allow us to limit the use of this function at the moment // to only a few supported use cases. diff --git a/internal/lang/funcs/conversion_test.go b/internal/lang/funcs/conversion_test.go index 9c3e7e9f74..885275b6cb 100644 --- a/internal/lang/funcs/conversion_test.go +++ b/internal/lang/funcs/conversion_test.go @@ -1,11 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( "fmt" "testing" - "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/lang/marks" ) func TestTo(t *testing.T) { @@ -175,6 +181,24 @@ func TestTo(t *testing.T) { cty.DynamicVal, `incompatible object type for conversion: attribute "foo" is required`, }, + { + cty.UnknownVal(cty.Object(map[string]cty.Type{"foo": cty.String})).Mark(marks.Ephemeral).Mark("boop"), + cty.Map(cty.String), + cty.UnknownVal(cty.Map(cty.String)).Mark(marks.Ephemeral).Mark("boop"), + ``, + }, + { + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("hello"), + "bar": cty.StringVal("world").Mark("beep"), + }).Mark("boop"), + cty.Map(cty.String), + cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("hello"), + "bar": cty.StringVal("world").Mark("beep"), + }).Mark("boop"), + ``, + }, } for _, test := range tests { @@ -200,3 +224,146 @@ func TestTo(t *testing.T) { }) } } + +func TestEphemeralAsNull(t *testing.T) { + tests := []struct { + Input cty.Value + Want cty.Value + }{ + // Simple cases + { + cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral), + cty.NullVal(cty.String), + }, + { + cty.StringVal("hello"), + cty.StringVal("hello"), + }, + { + // Unknown values stay unknown because an unknown value with + // an imprecise type constraint is allowed to take on a more + // precise type in later phases, but known values (even if null) + // should not. We do know that the final known result definitely + // won't be ephemeral, though. + cty.UnknownVal(cty.String).Mark(marks.Ephemeral), + cty.UnknownVal(cty.String), + }, + { + cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String), + }, + { + // Unknown value refinements should be discarded when unmarking, + // both because we know our final value is going to be null + // anyway and because an ephemeral value is not required to + // have consistent refinements between the plan and apply phases. + cty.UnknownVal(cty.String).RefineNotNull().Mark(marks.Ephemeral), + cty.UnknownVal(cty.String), + }, + { + // Refinements must be preserved for non-ephemeral values, though. + cty.UnknownVal(cty.String).RefineNotNull(), + cty.UnknownVal(cty.String).RefineNotNull(), + }, + + // Should preserve other marks in all cases + { + cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral).Mark(marks.Sensitive), + cty.NullVal(cty.String).Mark(marks.Sensitive), + }, + { + cty.StringVal("hello").Mark(marks.Sensitive), + cty.StringVal("hello").Mark(marks.Sensitive), + }, + { + cty.UnknownVal(cty.String).Mark(marks.Ephemeral).Mark(marks.Sensitive), + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + }, + { + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + }, + { + cty.UnknownVal(cty.String).RefineNotNull().Mark(marks.Ephemeral).Mark(marks.Sensitive), + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + }, + { + cty.UnknownVal(cty.String).RefineNotNull().Mark(marks.Sensitive), + cty.UnknownVal(cty.String).RefineNotNull().Mark(marks.Sensitive), + }, + + // Nested ephemeral values + { + cty.ListVal([]cty.Value{ + cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral), + cty.StringVal("hello"), + }), + cty.ListVal([]cty.Value{ + cty.NullVal(cty.String), + cty.StringVal("hello"), + }), + }, + { + cty.TupleVal([]cty.Value{ + cty.True, + cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral), + cty.StringVal("hello"), + }), + cty.TupleVal([]cty.Value{ + cty.True, + cty.NullVal(cty.String), + cty.StringVal("hello"), + }), + }, + { + // Sets can't actually preserve individual element marks, so + // this gets treated as the entire set being ephemeral. + // (That's true of the input value, despite how it's written here, + // not just the result value; cty.SetVal does the simplification + // itself during the construction of the value.) + cty.SetVal([]cty.Value{ + cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral), + cty.StringVal("hello"), + }), + cty.NullVal(cty.Set(cty.String)), + }, + { + cty.MapVal(map[string]cty.Value{ + "addr": cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral), + "greet": cty.StringVal("hello"), + }), + cty.MapVal(map[string]cty.Value{ + "addr": cty.NullVal(cty.String), + "greet": cty.StringVal("hello"), + }), + }, + { + cty.ObjectVal(map[string]cty.Value{ + "addr": cty.StringVal("127.0.0.1:12654").Mark(marks.Ephemeral), + "greet": cty.StringVal("hello").Mark(marks.Sensitive), + "happy": cty.True, + "both": cty.NumberIntVal(2).WithMarks(cty.NewValueMarks(marks.Sensitive, marks.Ephemeral)), + }), + cty.ObjectVal(map[string]cty.Value{ + "addr": cty.NullVal(cty.String), + "greet": cty.StringVal("hello").Mark(marks.Sensitive), + "happy": cty.True, + "both": cty.NullVal(cty.Number).Mark(marks.Sensitive), + }), + }, + } + + for _, test := range tests { + t.Run(test.Input.GoString(), func(t *testing.T) { + got, err := EphemeralAsNull(test.Input) + if err != nil { + // This function is supposed to be infallible + t.Fatal(err) + } + + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} diff --git a/internal/lang/funcs/crypto.go b/internal/lang/funcs/crypto.go index 7c4ba4ada3..359d0316de 100644 --- a/internal/lang/funcs/crypto.go +++ b/internal/lang/funcs/crypto.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -9,6 +12,7 @@ import ( "encoding/asn1" "encoding/base64" "encoding/hex" + "errors" "fmt" "hash" "io" @@ -24,8 +28,9 @@ import ( ) var UUIDFunc = function.New(&function.Spec{ - Params: []function.Parameter{}, - Type: function.StaticReturnType(cty.String), + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { result, err := uuid.GenerateUUID() if err != nil { @@ -46,7 +51,8 @@ var UUIDV5Func = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var namespace uuidv5.UUID switch { @@ -74,8 +80,8 @@ var Base64Sha256Func = makeStringHashFunction(sha256.New, base64.StdEncoding.Enc // MakeFileBase64Sha256Func constructs a function that is like Base64Sha256Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileBase64Sha256Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha256.New, base64.StdEncoding.EncodeToString) +func MakeFileBase64Sha256Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha256.New, base64.StdEncoding.EncodeToString, wrap) } // Base64Sha512Func constructs a function that computes the SHA256 hash of a given string @@ -84,8 +90,8 @@ var Base64Sha512Func = makeStringHashFunction(sha512.New, base64.StdEncoding.Enc // MakeFileBase64Sha512Func constructs a function that is like Base64Sha512Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileBase64Sha512Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha512.New, base64.StdEncoding.EncodeToString) +func MakeFileBase64Sha512Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha512.New, base64.StdEncoding.EncodeToString, wrap) } // BcryptFunc constructs a function that computes a hash of the given string using the Blowfish cipher. @@ -100,7 +106,8 @@ var BcryptFunc = function.New(&function.Spec{ Name: "cost", Type: cty.Number, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { defaultCost := 10 @@ -131,8 +138,8 @@ var Md5Func = makeStringHashFunction(md5.New, hex.EncodeToString) // MakeFileMd5Func constructs a function that is like Md5Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileMd5Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, md5.New, hex.EncodeToString) +func MakeFileMd5Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, md5.New, hex.EncodeToString, wrap) } // RsaDecryptFunc constructs a function that decrypts an RSA-encrypted ciphertext. @@ -147,7 +154,8 @@ var RsaDecryptFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { s := args[0].AsString() key := args[1].AsString() @@ -168,7 +176,7 @@ var RsaDecryptFunc = function.New(&function.Spec{ default: errStr = fmt.Sprintf("invalid private key: %s", e) } - return cty.UnknownVal(cty.String), function.NewArgErrorf(1, errStr) + return cty.UnknownVal(cty.String), function.NewArgError(1, errors.New(errStr)) } privateKey, ok := rawKey.(*rsa.PrivateKey) if !ok { @@ -190,8 +198,8 @@ var Sha1Func = makeStringHashFunction(sha1.New, hex.EncodeToString) // MakeFileSha1Func constructs a function that is like Sha1Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileSha1Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha1.New, hex.EncodeToString) +func MakeFileSha1Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha1.New, hex.EncodeToString, wrap) } // Sha256Func contructs a function that computes the SHA256 hash of a given string @@ -200,8 +208,8 @@ var Sha256Func = makeStringHashFunction(sha256.New, hex.EncodeToString) // MakeFileSha256Func constructs a function that is like Sha256Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileSha256Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha256.New, hex.EncodeToString) +func MakeFileSha256Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha256.New, hex.EncodeToString, wrap) } // Sha512Func contructs a function that computes the SHA512 hash of a given string @@ -210,8 +218,8 @@ var Sha512Func = makeStringHashFunction(sha512.New, hex.EncodeToString) // MakeFileSha512Func constructs a function that is like Sha512Func but reads the // contents of a file rather than hashing a given literal string. -func MakeFileSha512Func(baseDir string) function.Function { - return makeFileHashFunction(baseDir, sha512.New, hex.EncodeToString) +func MakeFileSha512Func(baseDir string, wrap ImplWrapper) function.Function { + return makeFileHashFunction(baseDir, sha512.New, hex.EncodeToString, wrap) } func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) function.Function { @@ -222,7 +230,8 @@ func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) functi Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { s := args[0].AsString() h := hf() @@ -233,7 +242,7 @@ func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) functi }) } -func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) string) function.Function { +func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) string, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { @@ -241,8 +250,9 @@ func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), - Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: wrap(func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { path := args[0].AsString() f, err := openFile(baseDir, path) if err != nil { @@ -257,7 +267,7 @@ func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) } rv := enc(h.Sum(nil)) return cty.StringVal(rv), nil - }, + }), }) } diff --git a/internal/lang/funcs/crypto_test.go b/internal/lang/funcs/crypto_test.go index 2797777438..c7a14bb743 100644 --- a/internal/lang/funcs/crypto_test.go +++ b/internal/lang/funcs/crypto_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -144,7 +147,7 @@ func TestFileBase64Sha256(t *testing.T) { }, } - fileSHA256 := MakeFileBase64Sha256Func(".") + fileSHA256 := MakeFileBase64Sha256Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filebase64sha256(%#v)", test.Path), func(t *testing.T) { @@ -225,7 +228,7 @@ func TestFileBase64Sha512(t *testing.T) { }, } - fileSHA512 := MakeFileBase64Sha512Func(".") + fileSHA512 := MakeFileBase64Sha512Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filebase64sha512(%#v)", test.Path), func(t *testing.T) { @@ -343,7 +346,7 @@ func TestFileMD5(t *testing.T) { }, } - fileMD5 := MakeFileMd5Func(".") + fileMD5 := MakeFileMd5Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filemd5(%#v)", test.Path), func(t *testing.T) { @@ -500,7 +503,7 @@ func TestFileSHA1(t *testing.T) { }, } - fileSHA1 := MakeFileSha1Func(".") + fileSHA1 := MakeFileSha1Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filesha1(%#v)", test.Path), func(t *testing.T) { @@ -578,7 +581,7 @@ func TestFileSHA256(t *testing.T) { }, } - fileSHA256 := MakeFileSha256Func(".") + fileSHA256 := MakeFileSha256Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filesha256(%#v)", test.Path), func(t *testing.T) { @@ -656,7 +659,7 @@ func TestFileSHA512(t *testing.T) { }, } - fileSHA512 := MakeFileSha512Func(".") + fileSHA512 := MakeFileSha512Func(".", noopWrapper) for _, test := range tests { t.Run(fmt.Sprintf("filesha512(%#v)", test.Path), func(t *testing.T) { diff --git a/internal/lang/funcs/datetime.go b/internal/lang/funcs/datetime.go index fbd7c0b27c..a8e5ae6872 100644 --- a/internal/lang/funcs/datetime.go +++ b/internal/lang/funcs/datetime.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -10,13 +13,29 @@ import ( // TimestampFunc constructs a function that returns a string representation of the current date and time. var TimestampFunc = function.New(&function.Spec{ - Params: []function.Parameter{}, - Type: function.StaticReturnType(cty.String), + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil }, }) +// MakeStaticTimestampFunc constructs a function that returns a string +// representation of the date and time specified by the provided argument. +func MakeStaticTimestampFunc(static time.Time) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + if static.IsZero() { + return cty.UnknownVal(cty.String), nil + } + return cty.StringVal(static.Format(time.RFC3339)), nil + }, + }) +} + // TimeAddFunc constructs a function that adds a duration to a timestamp, returning a new timestamp. var TimeAddFunc = function.New(&function.Spec{ Params: []function.Parameter{ @@ -29,7 +48,8 @@ var TimeAddFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { ts, err := parseTimestamp(args[0].AsString()) if err != nil { @@ -56,7 +76,8 @@ var TimeCmpFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { tsA, err := parseTimestamp(args[0].AsString()) if err != nil { diff --git a/internal/lang/funcs/datetime_test.go b/internal/lang/funcs/datetime_test.go index f20e59bfae..a4c3726013 100644 --- a/internal/lang/funcs/datetime_test.go +++ b/internal/lang/funcs/datetime_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -53,13 +56,13 @@ func TestTimeadd(t *testing.T) { { // Invalid format timestamp cty.StringVal("2017-11-22"), cty.StringVal("-1h"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), true, }, { // Invalid format duration (day is not supported by ParseDuration) cty.StringVal("2017-11-22T00:00:00Z"), cty.StringVal("1d"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), true, }, } @@ -129,31 +132,31 @@ func TestTimeCmp(t *testing.T) { { cty.StringVal("2017-11-22T00:00:00Z"), cty.StringVal("bloop"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), `not a valid RFC3339 timestamp: cannot use "bloop" as year`, }, { cty.StringVal("2017-11-22 00:00:00Z"), cty.StringVal("2017-11-22T00:00:00Z"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), `not a valid RFC3339 timestamp: missing required time introducer 'T'`, }, { cty.StringVal("2017-11-22T00:00:00Z"), cty.UnknownVal(cty.String), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), ``, }, { cty.UnknownVal(cty.String), cty.StringVal("2017-11-22T00:00:00Z"), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), ``, }, { cty.UnknownVal(cty.String), cty.UnknownVal(cty.String), - cty.UnknownVal(cty.Number), + cty.UnknownVal(cty.Number).RefineNotNull(), ``, }, } diff --git a/internal/lang/funcs/descriptions.go b/internal/lang/funcs/descriptions.go new file mode 100644 index 0000000000..86b70ccb2a --- /dev/null +++ b/internal/lang/funcs/descriptions.go @@ -0,0 +1,563 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package funcs + +import ( + "github.com/zclconf/go-cty/cty/function" +) + +type descriptionEntry struct { + // Description is a description for the function. + Description string + + // ParamDescription argument must match the number of parameters of the + // function. If the function has a VarParam then that counts as one + // parameter. The given descriptions will be assigned in order starting + // with the positional arguments in their declared order, followed by the + // variadic parameter if any. + ParamDescription []string +} + +// DescriptionList is a consolidated list containing all descriptions for all +// functions available within Terraform. A function's description should point +// to the matching entry in this list. +// +// We keep this as a single list, so we can quickly review descriptions within +// a single file and copy the whole list to other projects, like +// terraform-schema. +var DescriptionList = map[string]descriptionEntry{ + "abs": { + Description: "`abs` returns the absolute value of the given number. In other words, if the number is zero or positive then it is returned as-is, but if it is negative then it is multiplied by -1 to make it positive before returning it.", + ParamDescription: []string{""}, + }, + "abspath": { + Description: "`abspath` takes a string containing a filesystem path and converts it to an absolute path. That is, if the path is not absolute, it will be joined with the current working directory.", + ParamDescription: []string{""}, + }, + "alltrue": { + Description: "`alltrue` returns `true` if all elements in a given collection are `true` or `\"true\"`. It also returns `true` if the collection is empty.", + ParamDescription: []string{""}, + }, + "anytrue": { + Description: "`anytrue` returns `true` if any element in a given collection is `true` or `\"true\"`. It also returns `false` if the collection is empty.", + ParamDescription: []string{""}, + }, + "base64decode": { + Description: "`base64decode` takes a string containing a Base64 character sequence and returns the original string.", + ParamDescription: []string{""}, + }, + "base64encode": { + Description: "`base64encode` applies Base64 encoding to a string.", + ParamDescription: []string{""}, + }, + "base64gzip": { + Description: "`base64gzip` compresses a string with gzip and then encodes the result in Base64 encoding.", + ParamDescription: []string{""}, + }, + "base64sha256": { + Description: "`base64sha256` computes the SHA256 hash of a given string and encodes it with Base64. This is not equivalent to `base64encode(sha256(\"test\"))` since `sha256()` returns hexadecimal representation.", + ParamDescription: []string{""}, + }, + "base64sha512": { + Description: "`base64sha512` computes the SHA512 hash of a given string and encodes it with Base64. This is not equivalent to `base64encode(sha512(\"test\"))` since `sha512()` returns hexadecimal representation.", + ParamDescription: []string{""}, + }, + "basename": { + Description: "`basename` takes a string containing a filesystem path and removes all except the last portion from it.", + ParamDescription: []string{""}, + }, + "bcrypt": { + Description: "`bcrypt` computes a hash of the given string using the Blowfish cipher, returning a string in [the _Modular Crypt Format_](https://passlib.readthedocs.io/en/stable/modular_crypt_format.html) usually expected in the shadow password file on many Unix systems.", + ParamDescription: []string{ + "", + "The `cost` argument is optional and will default to 10 if unspecified.", + }, + }, + "can": { + Description: "`can` evaluates the given expression and returns a boolean value indicating whether the expression produced a result without any errors.", + ParamDescription: []string{""}, + }, + "ceil": { + Description: "`ceil` returns the closest whole number that is greater than or equal to the given value, which may be a fraction.", + ParamDescription: []string{""}, + }, + "chomp": { + Description: "`chomp` removes newline characters at the end of a string.", + ParamDescription: []string{""}, + }, + "chunklist": { + Description: "`chunklist` splits a single list into fixed-size chunks, returning a list of lists.", + ParamDescription: []string{ + "", + "The maximum length of each chunk. All but the last element of the result is guaranteed to be of exactly this size.", + }, + }, + "cidrhost": { + Description: "`cidrhost` calculates a full host IP address for a given host number within a given IP network address prefix.", + ParamDescription: []string{ + "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).", + "`hostnum` is a whole number that can be represented as a binary integer with no more than the number of digits remaining in the address after the given prefix.", + }, + }, + "cidrnetmask": { + Description: "`cidrnetmask` converts an IPv4 address prefix given in CIDR notation into a subnet mask address.", + ParamDescription: []string{ + "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).", + }, + }, + "cidrsubnet": { + Description: "`cidrsubnet` calculates a subnet address within given IP network address prefix.", + ParamDescription: []string{ + "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).", + "`newbits` is the number of additional bits with which to extend the prefix.", + "`netnum` is a whole number that can be represented as a binary integer with no more than `newbits` binary digits, which will be used to populate the additional bits added to the prefix."}, + }, + "cidrsubnets": { + Description: "`cidrsubnets` calculates a sequence of consecutive IP address ranges within a particular CIDR prefix.", + ParamDescription: []string{ + "`prefix` must be given in CIDR notation, as defined in [RFC 4632 section 3.1](https://tools.ietf.org/html/rfc4632#section-3.1).", + "", + }, + }, + "coalesce": { + Description: "`coalesce` takes any number of arguments and returns the first one that isn't null or an empty string.", + ParamDescription: []string{""}, + }, + "coalescelist": { + Description: "`coalescelist` takes any number of list arguments and returns the first one that isn't empty.", + ParamDescription: []string{ + "List or tuple values to test in the given order.", + }, + }, + "compact": { + Description: "`compact` takes a list of strings and returns a new list with any empty string elements removed.", + ParamDescription: []string{""}, + }, + "concat": { + Description: "`concat` takes two or more lists and combines them into a single list.", + ParamDescription: []string{""}, + }, + "contains": { + Description: "`contains` determines whether a given list or set contains a given single value as one of its elements.", + ParamDescription: []string{"", ""}, + }, + "csvdecode": { + Description: "`csvdecode` decodes a string containing CSV-formatted data and produces a list of maps representing that data.", + ParamDescription: []string{""}, + }, + "dirname": { + Description: "`dirname` takes a string containing a filesystem path and removes the last portion from it.", + ParamDescription: []string{""}, + }, + "distinct": { + Description: "`distinct` takes a list and returns a new list with any duplicate elements removed.", + ParamDescription: []string{""}, + }, + "element": { + Description: "`element` retrieves a single element from a list.", + ParamDescription: []string{"", ""}, + }, + "endswith": { + Description: "`endswith` takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix.", + ParamDescription: []string{"", ""}, + }, + "ephemeralasnull": { + Description: "`ephemeralasnull` takes a value of any type and returns a similar value of the same type with any ephemeral values replaced with non-ephemeral null values and all non-ephemeral values preserved.", + ParamDescription: []string{""}, + }, + "file": { + Description: "`file` reads the contents of a file at the given path and returns them as a string.", + ParamDescription: []string{""}, + }, + "filebase64": { + Description: "`filebase64` reads the contents of a file at the given path and returns them as a base64-encoded string.", + ParamDescription: []string{""}, + }, + "filebase64sha256": { + Description: "`filebase64sha256` is a variant of `base64sha256` that hashes the contents of a given file rather than a literal string.", + ParamDescription: []string{""}, + }, + "filebase64sha512": { + Description: "`filebase64sha512` is a variant of `base64sha512` that hashes the contents of a given file rather than a literal string.", + ParamDescription: []string{""}, + }, + "fileexists": { + Description: "`fileexists` determines whether a file exists at a given path.", + ParamDescription: []string{""}, + }, + "filemd5": { + Description: "`filemd5` is a variant of `md5` that hashes the contents of a given file rather than a literal string.", + ParamDescription: []string{""}, + }, + "fileset": { + Description: "`fileset` enumerates a set of regular file names given a path and pattern. The path is automatically removed from the resulting set of file names and any result still containing path separators always returns forward slash (`/`) as the path separator for cross-system compatibility.", + ParamDescription: []string{"", ""}, + }, + "filesha1": { + Description: "`filesha1` is a variant of `sha1` that hashes the contents of a given file rather than a literal string.", + ParamDescription: []string{""}, + }, + "filesha256": { + Description: "`filesha256` is a variant of `sha256` that hashes the contents of a given file rather than a literal string.", + ParamDescription: []string{""}, + }, + "filesha512": { + Description: "`filesha512` is a variant of `sha512` that hashes the contents of a given file rather than a literal string.", + ParamDescription: []string{""}, + }, + "flatten": { + Description: "`flatten` takes a list and replaces any elements that are lists with a flattened sequence of the list contents.", + ParamDescription: []string{""}, + }, + "floor": { + Description: "`floor` returns the closest whole number that is less than or equal to the given value, which may be a fraction.", + ParamDescription: []string{""}, + }, + "format": { + Description: "The `format` function produces a string by formatting a number of other values according to a specification string. It is similar to the `printf` function in C, and other similar functions in other programming languages.", + ParamDescription: []string{"", ""}, + }, + "formatdate": { + Description: "`formatdate` converts a timestamp into a different time format.", + ParamDescription: []string{"", ""}, + }, + "formatlist": { + Description: "`formatlist` produces a list of strings by formatting a number of other values according to a specification string.", + ParamDescription: []string{"", ""}, + }, + "indent": { + Description: "`indent` adds a given number of spaces to the beginnings of all but the first line in a given multi-line string.", + ParamDescription: []string{ + "Number of spaces to add after each newline character.", + "", + }, + }, + "index": { + Description: "`index` finds the element index for a given value in a list.", + ParamDescription: []string{"", ""}, + }, + "issensitive": { + Description: "`issensitive` takes a value and returns a boolean indicating if the value is sensitive.", + ParamDescription: []string{""}, + }, + "join": { + Description: "`join` produces a string by concatenating together all elements of a given list of strings with the given delimiter.", + ParamDescription: []string{ + "Delimiter to insert between the given strings.", + "One or more lists of strings to join.", + }, + }, + "jsondecode": { + Description: "`jsondecode` interprets a given string as JSON, returning a representation of the result of decoding that string.", + ParamDescription: []string{""}, + }, + "jsonencode": { + Description: "`jsonencode` encodes a given value to a string using JSON syntax.", + ParamDescription: []string{""}, + }, + "keys": { + Description: "`keys` takes a map and returns a list containing the keys from that map.", + ParamDescription: []string{ + "The map to extract keys from. May instead be an object-typed value, in which case the result is a tuple of the object attributes.", + }, + }, + "length": { + Description: "`length` determines the length of a given list, map, or string.", + ParamDescription: []string{""}, + }, + "list": { + Description: "The `list` function is no longer available. Prior to Terraform v0.12 it was the only available syntax for writing a literal list inside an expression, but Terraform v0.12 introduced a new first-class syntax.", + ParamDescription: []string{""}, + }, + "log": { + Description: "`log` returns the logarithm of a given number in a given base.", + ParamDescription: []string{"", ""}, + }, + "lookup": { + Description: "`lookup` retrieves the value of a single element from a map, given its key. If the given key does not exist, the given default value is returned instead.", + ParamDescription: []string{"", "", ""}, + }, + "lower": { + Description: "`lower` converts all cased letters in the given string to lowercase.", + ParamDescription: []string{""}, + }, + "map": { + Description: "The `map` function is no longer available. Prior to Terraform v0.12 it was the only available syntax for writing a literal map inside an expression, but Terraform v0.12 introduced a new first-class syntax.", + ParamDescription: []string{""}, + }, + "matchkeys": { + Description: "`matchkeys` constructs a new list by taking a subset of elements from one list whose indexes match the corresponding indexes of values in another list.", + ParamDescription: []string{"", "", ""}, + }, + "max": { + Description: "`max` takes one or more numbers and returns the greatest number from the set.", + ParamDescription: []string{""}, + }, + "md5": { + Description: "`md5` computes the MD5 hash of a given string and encodes it with hexadecimal digits.", + ParamDescription: []string{""}, + }, + "merge": { + Description: "`merge` takes an arbitrary number of maps or objects, and returns a single map or object that contains a merged set of elements from all arguments.", + ParamDescription: []string{""}, + }, + "min": { + Description: "`min` takes one or more numbers and returns the smallest number from the set.", + ParamDescription: []string{""}, + }, + "nonsensitive": { + Description: "`nonsensitive` takes a sensitive value and returns a copy of that value with the sensitive marking removed, thereby exposing the sensitive value.", + ParamDescription: []string{""}, + }, + "one": { + Description: "`one` takes a list, set, or tuple value with either zero or one elements. If the collection is empty, `one` returns `null`. Otherwise, `one` returns the first element. If there are two or more elements then `one` will return an error.", + ParamDescription: []string{""}, + }, + "parseint": { + Description: "`parseint` parses the given string as a representation of an integer in the specified base and returns the resulting number. The base must be between 2 and 62 inclusive.", + ParamDescription: []string{"", ""}, + }, + "pathexpand": { + Description: "`pathexpand` takes a filesystem path that might begin with a `~` segment, and if so it replaces that segment with the current user's home directory path.", + ParamDescription: []string{""}, + }, + "pow": { + Description: "`pow` calculates an exponent, by raising its first argument to the power of the second argument.", + ParamDescription: []string{"", ""}, + }, + "range": { + Description: "`range` generates a list of numbers using a start value, a limit value, and a step value.", + ParamDescription: []string{""}, + }, + "regex": { + Description: "`regex` applies a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) to a string and returns the matching substrings.", + ParamDescription: []string{"", ""}, + }, + "regexall": { + Description: "`regexall` applies a [regular expression](https://en.wikipedia.org/wiki/Regular_expression) to a string and returns a list of all matches.", + ParamDescription: []string{"", ""}, + }, + "replace": { + Description: "`replace` searches a given string for another given substring, and replaces each occurrence with a given replacement string.", + ParamDescription: []string{"", "", ""}, + }, + "reverse": { + Description: "`reverse` takes a sequence and produces a new sequence of the same length with all of the same elements as the given sequence but in reverse order.", + ParamDescription: []string{""}, + }, + "rsadecrypt": { + Description: "`rsadecrypt` decrypts an RSA-encrypted ciphertext, returning the corresponding cleartext.", + ParamDescription: []string{"", ""}, + }, + "sensitive": { + Description: "`sensitive` takes any value and returns a copy of it marked so that Terraform will treat it as sensitive, with the same meaning and behavior as for [sensitive input variables](/terraform/language/values/variables#suppressing-values-in-cli-output).", + ParamDescription: []string{""}, + }, + "setintersection": { + Description: "The `setintersection` function takes multiple sets and produces a single set containing only the elements that all of the given sets have in common. In other words, it computes the [intersection](https://en.wikipedia.org/wiki/Intersection_\\(set_theory\\)) of the sets.", + ParamDescription: []string{"", ""}, + }, + "setproduct": { + Description: "The `setproduct` function finds all of the possible combinations of elements from all of the given sets by computing the [Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product).", + ParamDescription: []string{ + "The sets to consider. Also accepts lists and tuples, and if all arguments are of list or tuple type then the result will preserve the input ordering", + }, + }, + "setsubtract": { + Description: "The `setsubtract` function returns a new set containing the elements from the first set that are not present in the second set. In other words, it computes the [relative complement](https://en.wikipedia.org/wiki/Complement_\\(set_theory\\)#Relative_complement) of the second set.", + ParamDescription: []string{"", ""}, + }, + "setunion": { + Description: "The `setunion` function takes multiple sets and produces a single set containing the elements from all of the given sets. In other words, it computes the [union](https://en.wikipedia.org/wiki/Union_\\(set_theory\\)) of the sets.", + ParamDescription: []string{"", ""}, + }, + "sha1": { + Description: "`sha1` computes the SHA1 hash of a given string and encodes it with hexadecimal digits.", + ParamDescription: []string{""}, + }, + "sha256": { + Description: "`sha256` computes the SHA256 hash of a given string and encodes it with hexadecimal digits.", + ParamDescription: []string{""}, + }, + "sha512": { + Description: "`sha512` computes the SHA512 hash of a given string and encodes it with hexadecimal digits.", + ParamDescription: []string{""}, + }, + "signum": { + Description: "`signum` determines the sign of a number, returning a number between -1 and 1 to represent the sign.", + ParamDescription: []string{""}, + }, + "slice": { + Description: "`slice` extracts some consecutive elements from within a list.", + ParamDescription: []string{"", "", ""}, + }, + "sort": { + Description: "`sort` takes a list of strings and returns a new list with those strings sorted lexicographically.", + ParamDescription: []string{""}, + }, + "split": { + Description: "`split` produces a list by dividing a given string at all occurrences of a given separator.", + ParamDescription: []string{"", ""}, + }, + "startswith": { + Description: "`startswith` takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix.", + ParamDescription: []string{"", ""}, + }, + "strcontains": { + Description: "`strcontains` takes two values: a string to check and an expected substring. The function returns true if the string has the substring contained within it.", + ParamDescription: []string{"", ""}, + }, + "strrev": { + Description: "`strrev` reverses the characters in a string. Note that the characters are treated as _Unicode characters_ (in technical terms, Unicode [grapheme cluster boundaries](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) are respected).", + ParamDescription: []string{""}, + }, + "substr": { + Description: "`substr` extracts a substring from a given string by offset and (maximum) length.", + ParamDescription: []string{"", "", ""}, + }, + "sum": { + Description: "`sum` takes a list or set of numbers and returns the sum of those numbers.", + ParamDescription: []string{""}, + }, + "templatefile": { + Description: "`templatefile` reads the file at the given path and renders its content as a template using a supplied set of template variables.", + ParamDescription: []string{"", ""}, + }, + "templatestring": { + Description: "`templatestring` takes a string from elsewhere in the module and renders its content as a template using a supplied set of template variables.", + ParamDescription: []string{ + "A simple reference to a string value containing the template source code.", + "Object of variables to expose in the template scope.", + }, + }, + "textdecodebase64": { + Description: "`textdecodebase64` function decodes a string that was previously Base64-encoded, and then interprets the result as characters in a specified character encoding.", + ParamDescription: []string{"", ""}, + }, + "textencodebase64": { + Description: "`textencodebase64` encodes the unicode characters in a given string using a specified character encoding, returning the result base64 encoded because Terraform language strings are always sequences of unicode characters.", + ParamDescription: []string{"", ""}, + }, + "timeadd": { + Description: "`timeadd` adds a duration to a timestamp, returning a new timestamp.", + ParamDescription: []string{"", ""}, + }, + "timecmp": { + Description: "`timecmp` compares two timestamps and returns a number that represents the ordering of the instants those timestamps represent.", + ParamDescription: []string{"", ""}, + }, + "timestamp": { + Description: "`timestamp` returns a UTC timestamp string in [RFC 3339](https://tools.ietf.org/html/rfc3339) format.", + ParamDescription: []string{}, + }, + "plantimestamp": { + Description: "`plantimestamp` returns a UTC timestamp string in [RFC 3339](https://tools.ietf.org/html/rfc3339) format, fixed to a constant time representing the time of the plan.", + ParamDescription: []string{}, + }, + "title": { + Description: "`title` converts the first letter of each word in the given string to uppercase.", + ParamDescription: []string{""}, + }, + "tobool": { + Description: "`tobool` converts its argument to a boolean value.", + ParamDescription: []string{""}, + }, + "tolist": { + Description: "`tolist` converts its argument to a list value.", + ParamDescription: []string{""}, + }, + "tomap": { + Description: "`tomap` converts its argument to a map value.", + ParamDescription: []string{""}, + }, + "tonumber": { + Description: "`tonumber` converts its argument to a number value.", + ParamDescription: []string{""}, + }, + "toset": { + Description: "`toset` converts its argument to a set value.", + ParamDescription: []string{""}, + }, + "tostring": { + Description: "`tostring` converts its argument to a string value.", + ParamDescription: []string{""}, + }, + "transpose": { + Description: "`transpose` takes a map of lists of strings and swaps the keys and values to produce a new map of lists of strings.", + ParamDescription: []string{""}, + }, + "trim": { + Description: "`trim` removes the specified set of characters from the start and end of the given string.", + ParamDescription: []string{ + "", + "A string containing all of the characters to trim. Each character is taken separately, so the order of characters is insignificant.", + }, + }, + "trimprefix": { + Description: "`trimprefix` removes the specified prefix from the start of the given string. If the string does not start with the prefix, the string is returned unchanged.", + ParamDescription: []string{"", ""}, + }, + "trimspace": { + Description: "`trimspace` removes any space characters from the start and end of the given string.", + ParamDescription: []string{""}, + }, + "trimsuffix": { + Description: "`trimsuffix` removes the specified suffix from the end of the given string.", + ParamDescription: []string{"", ""}, + }, + "try": { + Description: "`try` evaluates all of its argument expressions in turn and returns the result of the first one that does not produce any errors.", + ParamDescription: []string{""}, + }, + "type": { + Description: "`type` returns the type of a given value.", + ParamDescription: []string{""}, + }, + "upper": { + Description: "`upper` converts all cased letters in the given string to uppercase.", + ParamDescription: []string{""}, + }, + "urlencode": { + Description: "`urlencode` applies URL encoding to a given string.", + ParamDescription: []string{""}, + }, + "uuid": { + Description: "`uuid` generates a unique identifier string.", + ParamDescription: []string{}, + }, + "uuidv5": { + Description: "`uuidv5` generates a _name-based_ UUID, as described in [RFC 4122 section 4.3](https://tools.ietf.org/html/rfc4122#section-4.3), also known as a \"version 5\" UUID.", + ParamDescription: []string{"", ""}, + }, + "values": { + Description: "`values` takes a map and returns a list containing the values of the elements in that map.", + ParamDescription: []string{""}, + }, + "yamldecode": { + Description: "`yamldecode` parses a string as a subset of YAML, and produces a representation of its value.", + ParamDescription: []string{""}, + }, + "yamlencode": { + Description: "`yamlencode` encodes a given value to a string using [YAML 1.2](https://yaml.org/spec/1.2/spec.html) block syntax.", + ParamDescription: []string{""}, + }, + "zipmap": { + Description: "`zipmap` constructs a map from a list of keys and a corresponding list of values.", + ParamDescription: []string{"", ""}, + }, +} + +// WithDescription looks up the description for a given function and uses +// go-cty's WithNewDescriptions to replace the function's description and +// parameter descriptions. +func WithDescription(name string, f function.Function) function.Function { + desc, ok := DescriptionList[name] + if !ok { + return f + } + + // Will panic if ParamDescription doesn't match the number of parameters + // the function expects + return f.WithNewDescriptions(desc.Description, desc.ParamDescription) +} diff --git a/internal/lang/funcs/encoding.go b/internal/lang/funcs/encoding.go index 2e67ebc8bc..f92d7df486 100644 --- a/internal/lang/funcs/encoding.go +++ b/internal/lang/funcs/encoding.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -18,14 +21,20 @@ import ( var Base64DecodeFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "str", - Type: cty.String, - AllowMarked: true, + Name: "str", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { str, strMarks := args[0].Unmark() + if !str.IsKnown() { + return cty.UnknownVal(cty.String).WithMarks(strMarks), nil + } + s := str.AsString() sDec, err := base64.StdEncoding.DecodeString(s) if err != nil { @@ -47,7 +56,8 @@ var Base64EncodeFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil }, @@ -65,7 +75,8 @@ var TextEncodeBase64Func = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) if err != nil || encoding == nil { @@ -108,7 +119,8 @@ var TextDecodeBase64Func = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) if err != nil || encoding == nil { @@ -151,7 +163,8 @@ var Base64GzipFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { s := args[0].AsString() @@ -178,7 +191,8 @@ var URLEncodeFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(url.QueryEscape(args[0].AsString())), nil }, diff --git a/internal/lang/funcs/encoding_test.go b/internal/lang/funcs/encoding_test.go index 2e05784e82..43f03ecc7c 100644 --- a/internal/lang/funcs/encoding_test.go +++ b/internal/lang/funcs/encoding_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -34,6 +37,12 @@ func TestBase64Decode(t *testing.T) { cty.UnknownVal(cty.String), true, }, + // unknown marked + { + cty.UnknownVal(cty.String).Mark("a").Mark("b"), + cty.UnknownVal(cty.String).RefineNotNull().Mark("a").Mark("b"), + false, + }, } for _, test := range tests { @@ -232,25 +241,25 @@ func TestBase64TextEncode(t *testing.T) { { cty.StringVal("abc123!?$*&()'-=@~"), cty.StringVal("NOT-EXISTS"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), `"NOT-EXISTS" is not a supported IANA encoding name or alias in this Terraform version`, }, { cty.StringVal("🤔"), cty.StringVal("cp437"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), `the given string contains characters that cannot be represented in IBM437`, }, { cty.UnknownVal(cty.String), cty.StringVal("windows-1250"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), ``, }, { cty.StringVal("hello world"), cty.UnknownVal(cty.String), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), ``, }, } @@ -306,13 +315,13 @@ func TestBase64TextDecode(t *testing.T) { { cty.StringVal("doesn't matter"), cty.StringVal("NOT-EXISTS"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), `"NOT-EXISTS" is not a supported IANA encoding name or alias in this Terraform version`, }, { cty.StringVal(""), cty.StringVal("cp437"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), `the given value is has an invalid base64 symbol at offset 0`, }, { @@ -324,13 +333,13 @@ func TestBase64TextDecode(t *testing.T) { { cty.UnknownVal(cty.String), cty.StringVal("windows-1250"), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), ``, }, { cty.StringVal("YQBiAGMAMQAyADMAIQA/ACQAKgAmACgAKQAnAC0APQBAAH4A"), cty.UnknownVal(cty.String), - cty.UnknownVal(cty.String), + cty.UnknownVal(cty.String).RefineNotNull(), ``, }, } diff --git a/internal/lang/funcs/filesystem.go b/internal/lang/funcs/filesystem.go index e5de7907c4..02ddcc6923 100644 --- a/internal/lang/funcs/filesystem.go +++ b/internal/lang/funcs/filesystem.go @@ -1,9 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( "encoding/base64" "fmt" - "io/ioutil" + "io" "os" "path/filepath" "unicode/utf8" @@ -14,23 +17,32 @@ import ( homedir "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" + + "github.com/hashicorp/terraform/internal/collections" ) // MakeFileFunc constructs a function that takes a file path and returns the // contents of that file, either directly as a string (where valid UTF-8 is // required) or as a string containing base64 bytes. -func MakeFileFunc(baseDir string, encBase64 bool) function.Function { +func MakeFileFunc(baseDir string, encBase64 bool, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "path", - Type: cty.String, - AllowMarked: true, + Name: "path", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, }, }, - Type: function.StaticReturnType(cty.String), - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() + + if !pathArg.IsKnown() { + return cty.UnknownVal(cty.String).WithMarks(pathMarks), nil + } + path := pathArg.AsString() src, err := readFileBytes(baseDir, path, pathMarks) if err != nil { @@ -48,7 +60,7 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { } return cty.StringVal(string(src)).WithMarks(pathMarks), nil } - }, + }), }) } @@ -65,95 +77,41 @@ func MakeFileFunc(baseDir string, encBase64 bool) function.Function { // As a special exception, a referenced template file may not recursively call // the templatefile function, since that would risk the same file being // included into itself indefinitely. -func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Function) function.Function { - - params := []function.Parameter{ - { - Name: "path", - Type: cty.String, - AllowMarked: true, - }, - { - Name: "vars", - Type: cty.DynamicPseudoType, - }, - } - - loadTmpl := func(fn string, marks cty.ValueMarks) (hcl.Expression, error) { +func MakeTemplateFileFunc(baseDir string, funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]), wrap ImplWrapper) function.Function { + loadTmpl := func(fn string, marks cty.ValueMarks) (hcl.Expression, cty.ValueMarks, error) { // We re-use File here to ensure the same filename interpretation // as it does, along with its other safety checks. tmplVal, err := File(baseDir, cty.StringVal(fn).WithMarks(marks)) if err != nil { - return nil, err + return nil, nil, err } + tmplVal, marks = tmplVal.Unmark() expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { - return nil, diags + return nil, nil, diags } - return expr, nil + return expr, marks, nil } - renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) { - if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) { - return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time - } - - ctx := &hcl.EvalContext{ - Variables: varsVal.AsValueMap(), - } - - // We require all of the variables to be valid HCL identifiers, because - // otherwise there would be no way to refer to them in the template - // anyway. Rejecting this here gives better feedback to the user - // than a syntax error somewhere in the template itself. - for n := range ctx.Variables { - if !hclsyntax.ValidIdentifier(n) { - // This error message intentionally doesn't describe _all_ of - // the different permutations that are technically valid as an - // HCL identifier, but rather focuses on what we might - // consider to be an "idiomatic" variable name. - return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n) - } - } - - // We'll pre-check references in the template here so we can give a - // more specialized error message than HCL would by default, so it's - // clearer that this problem is coming from a templatefile call. - for _, traversal := range expr.Variables() { - root := traversal.RootName() - if _, ok := ctx.Variables[root]; !ok { - return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange()) - } - } - - givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems - funcs := make(map[string]function.Function, len(givenFuncs)) - for name, fn := range givenFuncs { - if name == "templatefile" { - // We stub this one out to prevent recursive calls. - funcs[name] = function.New(&function.Spec{ - Params: params, - Type: func(args []cty.Value) (cty.Type, error) { - return cty.NilType, fmt.Errorf("cannot recursively call templatefile from inside templatefile call") - }, - }) - continue - } - funcs[name] = fn - } - ctx.Functions = funcs - - val, diags := expr.Value(ctx) - if diags.HasErrors() { - return cty.DynamicVal, diags - } - return val, nil - } + renderTmpl := makeRenderTemplateFunc(funcsCb, true) return function.New(&function.Spec{ - Params: params, + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, + }, + { + Name: "vars", + Type: cty.DynamicPseudoType, + AllowMarked: true, + AllowUnknown: true, + }, + }, Type: func(args []cty.Value) (cty.Type, error) { if !(args[0].IsKnown() && args[1].IsKnown()) { return cty.DynamicPseudoType, nil @@ -164,43 +122,57 @@ func MakeTemplateFileFunc(baseDir string, funcsCb func() map[string]function.Fun // return any type. pathArg, pathMarks := args[0].Unmark() - expr, err := loadTmpl(pathArg.AsString(), pathMarks) + expr, _, err := loadTmpl(pathArg.AsString(), pathMarks) if err != nil { return cty.DynamicPseudoType, err } + vars, _ := args[1].UnmarkDeep() // This is safe even if args[1] contains unknowns because the HCL // template renderer itself knows how to short-circuit those. - val, err := renderTmpl(expr, args[1]) + val, err := renderTmpl(expr, vars) return val.Type(), err }, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() - expr, err := loadTmpl(pathArg.AsString(), pathMarks) + + vars, varsMarks := args[1].UnmarkDeep() + + if !pathArg.IsKnown() || !vars.IsKnown() { + return cty.UnknownVal(retType).WithMarks(pathMarks, varsMarks), nil + } + + expr, tmplMarks, err := loadTmpl(pathArg.AsString(), pathMarks) if err != nil { return cty.DynamicVal, err } - result, err := renderTmpl(expr, args[1]) - return result.WithMarks(pathMarks), err - }, + result, err := renderTmpl(expr, vars) + return result.WithMarks(tmplMarks, varsMarks), err + }), }) - } // MakeFileExistsFunc constructs a function that takes a path // and determines whether a file exists at that path -func MakeFileExistsFunc(baseDir string) function.Function { +func MakeFileExistsFunc(baseDir string, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "path", - Type: cty.String, - AllowMarked: true, + Name: "path", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, }, }, - Type: function.StaticReturnType(cty.Bool), - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() + + if !pathArg.IsKnown() { + return cty.UnknownVal(cty.Bool).WithMarks(pathMarks), nil + } + path := pathArg.AsString() path, err := homedir.Expand(path) if err != nil { @@ -250,33 +222,40 @@ func MakeFileExistsFunc(baseDir string) function.Function { } return cty.False, err - }, + }), }) } // MakeFileSetFunc constructs a function that takes a glob pattern // and enumerates a file set from that pattern -func MakeFileSetFunc(baseDir string) function.Function { +func MakeFileSetFunc(baseDir string, wrap ImplWrapper) function.Function { return function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "path", - Type: cty.String, - AllowMarked: true, + Name: "path", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, }, { - Name: "pattern", - Type: cty.String, - AllowMarked: true, + Name: "pattern", + Type: cty.String, + AllowMarked: true, + AllowUnknown: true, }, }, - Type: function.StaticReturnType(cty.Set(cty.String)), - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + Type: function.StaticReturnType(cty.Set(cty.String)), + RefineResult: refineNotNull, + Impl: wrap(func(args []cty.Value, retType cty.Type) (cty.Value, error) { pathArg, pathMarks := args[0].Unmark() - path := pathArg.AsString() patternArg, patternMarks := args[1].Unmark() - pattern := patternArg.AsString() + if !pathArg.IsKnown() || !patternArg.IsKnown() { + return cty.UnknownVal(retType).WithMarks(pathMarks, patternMarks), nil + } + + path := pathArg.AsString() + pattern := patternArg.AsString() marks := []cty.ValueMarks{pathMarks, patternMarks} if !filepath.IsAbs(path) { @@ -324,7 +303,7 @@ func MakeFileSetFunc(baseDir string) function.Function { } return cty.SetVal(matchVals).WithMarks(marks...), nil - }, + }), }) } @@ -337,7 +316,8 @@ var BasenameFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(filepath.Base(args[0].AsString())), nil }, @@ -352,7 +332,8 @@ var DirnameFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { return cty.StringVal(filepath.Dir(args[0].AsString())), nil }, @@ -366,7 +347,8 @@ var AbsPathFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { absPath, err := filepath.Abs(args[0].AsString()) return cty.StringVal(filepath.ToSlash(absPath)), err @@ -381,7 +363,8 @@ var PathExpandFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { homePath, err := homedir.Expand(args[0].AsString()) @@ -416,7 +399,7 @@ func readFileBytes(baseDir, path string, marks cty.ValueMarks) ([]byte, error) { } defer f.Close() - src, err := ioutil.ReadAll(f) + src, err := io.ReadAll(f) if err != nil { return nil, fmt.Errorf("failed to read file: %w", err) } @@ -432,7 +415,7 @@ func readFileBytes(baseDir, path string, marks cty.ValueMarks) ([]byte, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func File(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileFunc(baseDir, false) + fn := MakeFileFunc(baseDir, false, noopWrapper) return fn.Call([]cty.Value{path}) } @@ -442,7 +425,7 @@ func File(baseDir string, path cty.Value) (cty.Value, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileExists(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileExistsFunc(baseDir) + fn := MakeFileExistsFunc(baseDir, noopWrapper) return fn.Call([]cty.Value{path}) } @@ -452,7 +435,7 @@ func FileExists(baseDir string, path cty.Value) (cty.Value, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) { - fn := MakeFileSetFunc(baseDir) + fn := MakeFileSetFunc(baseDir, noopWrapper) return fn.Call([]cty.Value{path, pattern}) } @@ -464,7 +447,7 @@ func FileSet(baseDir string, path, pattern cty.Value) (cty.Value, error) { // directory, so this wrapper takes a base directory string and uses it to // construct the underlying function before calling it. func FileBase64(baseDir string, path cty.Value) (cty.Value, error) { - fn := MakeFileFunc(baseDir, true) + fn := MakeFileFunc(baseDir, true, noopWrapper) return fn.Call([]cty.Value{path}) } @@ -498,3 +481,12 @@ func Dirname(path cty.Value) (cty.Value, error) { func Pathexpand(path cty.Value) (cty.Value, error) { return PathExpandFunc.Call([]cty.Value{path}) } + +// ImplWrapper allows us to pass in a wrapper function to inject behavior into +// function implementations, because we don't have access to the function.Spec +// from the returned function.Function +type ImplWrapper func(function.ImplFunc) function.ImplFunc + +func noopWrapper(fn function.ImplFunc) function.ImplFunc { + return fn +} diff --git a/internal/lang/funcs/filesystem_test.go b/internal/lang/funcs/filesystem_test.go index 037137ae64..8291ef9790 100644 --- a/internal/lang/funcs/filesystem_test.go +++ b/internal/lang/funcs/filesystem_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -6,11 +9,13 @@ import ( "path/filepath" "testing" - "github.com/hashicorp/terraform/internal/lang/marks" homedir "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang/marks" ) func TestFile(t *testing.T) { @@ -24,6 +29,11 @@ func TestFile(t *testing.T) { cty.StringVal("Hello World"), ``, }, + { + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + cty.UnknownVal(cty.String).RefineNotNull().Mark(marks.Sensitive), + ``, + }, { cty.StringVal("testdata/icon.png"), cty.NilVal, @@ -146,7 +156,13 @@ func TestTemplateFile(t *testing.T) { cty.StringVal("testdata/recursive.tmpl"), cty.MapValEmpty(cty.String), cty.NilVal, - `testdata/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside templatefile call.`, + `testdata/recursive.tmpl:1,3-16: Error in function call; Call to function "templatefile" failed: cannot recursively call templatefile from inside another template function.`, + }, + { + cty.StringVal("testdata/recursive_namespaced.tmpl"), + cty.MapValEmpty(cty.String), + cty.NilVal, + `testdata/recursive_namespaced.tmpl:1,3-22: Error in function call; Call to function "core::templatefile" failed: cannot recursively call templatefile from inside another template function.`, }, { cty.StringVal("testdata/list.tmpl"), @@ -176,14 +192,68 @@ func TestTemplateFile(t *testing.T) { cty.True, // since this template contains only an interpolation, its true value shines through ``, }, + { + // If the template filename is sensitive then we also treat the + // rendered result as sensitive, because the rendered result + // is likely to imply which filename was used. + // (Sensitive filenames seem pretty unlikely, but if they do + // crop up then we should handle them consistently with our + // usual sensitivity rules.) + cty.StringVal("testdata/hello.txt").Mark(marks.Sensitive), + cty.EmptyObjectVal, + cty.StringVal("Hello World").Mark(marks.Sensitive), + ``, + }, + { + cty.StringVal("testdata/list.tmpl").Mark("path"), + cty.ObjectVal(map[string]cty.Value{ + "list": cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b").Mark("var"), + cty.StringVal("c"), + }).Mark("vars"), + }), + cty.StringVal("- a\n- b\n- c\n").Mark("path").Mark("var").Mark("vars"), + ``, + }, + { + cty.StringVal("testdata/list.tmpl").Mark("path"), + cty.UnknownVal(cty.Map(cty.String)), + cty.DynamicVal.Mark("path"), + ``, + }, + { + cty.StringVal("testdata/list.tmpl").Mark("path"), + cty.ObjectVal(map[string]cty.Value{ + "list": cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.UnknownVal(cty.String).Mark("var"), + cty.StringVal("c"), + }), + }), + cty.UnknownVal(cty.String).RefineNotNull().Mark("path").Mark("var"), + ``, + }, + { + cty.UnknownVal(cty.String).Mark("path"), + cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("value").Mark("var"), + }), + cty.DynamicVal.Mark("path").Mark("var"), + ``, + }, } - templateFileFn := MakeTemplateFileFunc(".", func() map[string]function.Function { - return map[string]function.Function{ - "join": stdlib.JoinFunc, - "templatefile": MakeFileFunc(".", false), // just a placeholder, since templatefile itself overrides this - } - }) + funcs := map[string]function.Function{ + "join": stdlib.JoinFunc, + "core::join": stdlib.JoinFunc, + } + funcsFunc := func() (funcTable map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]) { + return funcs, collections.NewSetCmp[string](), collections.NewSetCmp[string]("templatefile") + } + templateFileFn := MakeTemplateFileFunc(".", funcsFunc, noopWrapper) + funcs["templatefile"] = templateFileFn + funcs["core::templatefile"] = templateFileFn for _, test := range tests { t.Run(fmt.Sprintf("TemplateFile(%#v, %#v)", test.Path, test.Vars), func(t *testing.T) { @@ -250,6 +320,11 @@ func TestFileExists(t *testing.T) { cty.BoolVal(false), `failed to stat (sensitive value)`, }, + { + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + cty.UnknownVal(cty.Bool).RefineNotNull().Mark(marks.Sensitive), + ``, + }, } // Ensure "unreadable" directory cannot be listed during the test run @@ -482,6 +557,18 @@ func TestFileSet(t *testing.T) { }), ``, }, + { + cty.StringVal("testdata"), + cty.UnknownVal(cty.String), + cty.UnknownVal(cty.Set(cty.String)).RefineNotNull(), + ``, + }, + { + cty.StringVal("testdata"), + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + cty.UnknownVal(cty.Set(cty.String)).RefineNotNull().Mark(marks.Sensitive), + ``, + }, } for _, test := range tests { diff --git a/internal/lang/funcs/funcs_test.go b/internal/lang/funcs/funcs_test.go new file mode 100644 index 0000000000..8aeb897555 --- /dev/null +++ b/internal/lang/funcs/funcs_test.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package funcs + +import _ "github.com/hashicorp/terraform/internal/logging" diff --git a/internal/lang/funcs/number.go b/internal/lang/funcs/number.go index d958706102..08774e3d83 100644 --- a/internal/lang/funcs/number.go +++ b/internal/lang/funcs/number.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -21,7 +24,8 @@ var LogFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var num float64 if err := gocty.FromCtyValue(args[0], &num); err != nil { @@ -49,7 +53,8 @@ var PowFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var num float64 if err := gocty.FromCtyValue(args[0], &num); err != nil { @@ -74,7 +79,8 @@ var SignumFunc = function.New(&function.Spec{ Type: cty.Number, }, }, - Type: function.StaticReturnType(cty.Number), + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { var num int if err := gocty.FromCtyValue(args[0], &num); err != nil { @@ -95,14 +101,16 @@ var SignumFunc = function.New(&function.Spec{ var ParseIntFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "number", - Type: cty.DynamicPseudoType, - AllowMarked: true, + Name: "number", + Type: cty.DynamicPseudoType, + AllowMarked: true, + AllowUnknown: true, }, { - Name: "base", - Type: cty.Number, - AllowMarked: true, + Name: "base", + Type: cty.Number, + AllowMarked: true, + AllowUnknown: true, }, }, @@ -112,6 +120,7 @@ var ParseIntFunc = function.New(&function.Spec{ } return cty.Number, nil }, + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { var numstr string @@ -119,11 +128,16 @@ var ParseIntFunc = function.New(&function.Spec{ var err error numArg, numMarks := args[0].Unmark() + baseArg, baseMarks := args[1].Unmark() + + if !numArg.IsKnown() || !baseArg.IsKnown() { + return cty.UnknownVal(retType).WithMarks(numMarks, baseMarks), nil + } + if err = gocty.FromCtyValue(numArg, &numstr); err != nil { return cty.UnknownVal(cty.String), function.NewArgError(0, err) } - baseArg, baseMarks := args[1].Unmark() if err = gocty.FromCtyValue(baseArg, &base); err != nil { return cty.UnknownVal(cty.Number), function.NewArgError(1, err) } diff --git a/internal/lang/funcs/number_test.go b/internal/lang/funcs/number_test.go index 6caf19af18..83ca3f53d0 100644 --- a/internal/lang/funcs/number_test.go +++ b/internal/lang/funcs/number_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -214,6 +217,12 @@ func TestParseInt(t *testing.T) { cty.NumberIntVal(128).Mark(marks.Sensitive), ``, }, + { + cty.StringVal("128").Mark(marks.Sensitive), + cty.UnknownVal(cty.Number).Mark(marks.Sensitive), + cty.UnknownVal(cty.Number).RefineNotNull().Mark(marks.Sensitive), + ``, + }, { cty.StringVal("128").Mark("boop"), cty.NumberIntVal(10).Mark(marks.Sensitive), diff --git a/internal/lang/funcs/redact.go b/internal/lang/funcs/redact.go index bbec3f0a1b..1cb023dfea 100644 --- a/internal/lang/funcs/redact.go +++ b/internal/lang/funcs/redact.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( diff --git a/internal/lang/funcs/redact_test.go b/internal/lang/funcs/redact_test.go index e378d5f5af..5f67c46532 100644 --- a/internal/lang/funcs/redact_test.go +++ b/internal/lang/funcs/redact_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( diff --git a/internal/lang/funcs/refinements.go b/internal/lang/funcs/refinements.go new file mode 100644 index 0000000000..5ebdaadd5a --- /dev/null +++ b/internal/lang/funcs/refinements.go @@ -0,0 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package funcs + +import ( + "github.com/zclconf/go-cty/cty" +) + +func refineNotNull(b *cty.RefinementBuilder) *cty.RefinementBuilder { + return b.NotNull() +} diff --git a/internal/lang/funcs/sensitive.go b/internal/lang/funcs/sensitive.go index 1ce0774a33..4910e01fbd 100644 --- a/internal/lang/funcs/sensitive.go +++ b/internal/lang/funcs/sensitive.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -25,8 +28,7 @@ var SensitiveFunc = function.New(&function.Spec{ return args[0].Type(), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - val, _ := args[0].Unmark() - return val.Mark(marks.Sensitive), nil + return args[0].Mark(marks.Sensitive), nil }, }) @@ -49,15 +51,36 @@ var NonsensitiveFunc = function.New(&function.Spec{ return args[0].Type(), nil }, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { - if args[0].IsKnown() && !args[0].HasMark(marks.Sensitive) { - return cty.DynamicVal, function.NewArgErrorf(0, "the given value is not sensitive, so this call is redundant") - } v, m := args[0].Unmark() delete(m, marks.Sensitive) // remove the sensitive marking return v.WithMarks(m), nil }, }) +var IssensitiveFunc = function.New(&function.Spec{ + Params: []function.Parameter{{ + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }}, + Type: func(args []cty.Value) (cty.Type, error) { + return cty.Bool, nil + }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + switch v := args[0]; { + case v.HasMark(marks.Sensitive): + return cty.True, nil + case !v.IsKnown(): + return cty.UnknownVal(cty.Bool), nil + default: + return cty.False, nil + } + }, +}) + func Sensitive(v cty.Value) (cty.Value, error) { return SensitiveFunc.Call([]cty.Value{v}) } @@ -65,3 +88,7 @@ func Sensitive(v cty.Value) (cty.Value, error) { func Nonsensitive(v cty.Value) (cty.Value, error) { return NonsensitiveFunc.Call([]cty.Value{v}) } + +func Issensitive(v cty.Value) (cty.Value, error) { + return IssensitiveFunc.Call([]cty.Value{v}) +} diff --git a/internal/lang/funcs/sensitive_test.go b/internal/lang/funcs/sensitive_test.go index 2d0120e8e7..bd86252f8c 100644 --- a/internal/lang/funcs/sensitive_test.go +++ b/internal/lang/funcs/sensitive_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( @@ -42,14 +45,6 @@ func TestSensitive(t *testing.T) { cty.NumberIntVal(1).Mark(marks.Sensitive), ``, }, - { - // A value with some non-standard mark gets "fixed" to be marked - // with the standard "sensitive" mark. (This situation occurring - // would imply an inconsistency/bug elsewhere, so we're just - // being robust about it here.) - cty.NumberIntVal(1).Mark("bloop"), - ``, - }, { // A value deep already marked is allowed and stays marked, // _and_ we'll also mark the outer collection as sensitive. @@ -78,17 +73,7 @@ func TestSensitive(t *testing.T) { t.Errorf("result is not marked sensitive") } - gotRaw, gotMarks := got.Unmark() - if len(gotMarks) != 1 { - // We're only expecting to have the "sensitive" mark we checked - // above. Any others are an error, even if they happen to - // appear alongside "sensitive". (We might change this rule - // if someday we decide to use marks for some additional - // unrelated thing in Terraform, but currently we assume that - // _all_ marks imply sensitive, and so returning any other - // marks would be confusing.) - t.Errorf("extraneous marks %#v", gotMarks) - } + gotRaw, _ := got.Unmark() // Disregarding shallow marks, the result should have the same // effective value as the input. @@ -127,16 +112,16 @@ func TestNonsensitive(t *testing.T) { ``, }, - // Passing a value that is already non-sensitive is an error, - // because this function should always be used with specific - // intention, not just as a "make everything visible" hammer. + // Passing a value that is already non-sensitive is not an error, + // as this function may be used with specific to ensure that all + // values are indeed non-sensitive { cty.NumberIntVal(1), - `the given value is not sensitive, so this call is redundant`, + ``, }, { cty.NullVal(cty.String), - `the given value is not sensitive, so this call is redundant`, + ``, }, // Unknown values may become sensitive once they are known, so we @@ -177,3 +162,75 @@ func TestNonsensitive(t *testing.T) { }) } } + +func TestIssensitive(t *testing.T) { + tests := []struct { + Input cty.Value + Sensitive cty.Value + WantErr string + }{ + { + cty.NumberIntVal(1).Mark(marks.Sensitive), + cty.True, + ``, + }, + { + cty.NumberIntVal(1), + cty.False, + ``, + }, + { + cty.DynamicVal.Mark(marks.Sensitive), + cty.True, + ``, + }, + { + cty.UnknownVal(cty.String).Mark(marks.Sensitive), + cty.True, + ``, + }, + { + cty.NullVal(cty.EmptyObject).Mark(marks.Sensitive), + cty.True, + ``, + }, + { + cty.NullVal(cty.String), + cty.False, + ``, + }, + { + cty.DynamicVal, + cty.UnknownVal(cty.Bool), + ``, + }, + { + cty.UnknownVal(cty.String), + cty.UnknownVal(cty.Bool), + ``, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("issensitive(%#v)", test.Input), func(t *testing.T) { + got, err := Issensitive(test.Input) + + if test.WantErr != "" { + if err == nil { + t.Fatal("succeeded; want error") + } + if got, want := err.Error(), test.WantErr; got != want { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Sensitive) { + t.Errorf("wrong result \ngot: %#v\nwant: %#v", got, test.Sensitive) + } + }) + } + +} diff --git a/internal/lang/funcs/string.go b/internal/lang/funcs/string.go index 9ef709c7fb..932c36da77 100644 --- a/internal/lang/funcs/string.go +++ b/internal/lang/funcs/string.go @@ -1,11 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( + "fmt" "regexp" "strings" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/customdecode" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/function" + + "github.com/hashicorp/terraform/internal/collections" ) // StartsWithFunc constructs a function that checks if a string starts with @@ -13,19 +23,43 @@ import ( var StartsWithFunc = function.New(&function.Spec{ Params: []function.Parameter{ { - Name: "str", - Type: cty.String, + Name: "str", + Type: cty.String, + AllowUnknown: true, }, { Name: "prefix", Type: cty.String, }, }, - Type: function.StaticReturnType(cty.Bool), + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - str := args[0].AsString() prefix := args[1].AsString() + if !args[0].IsKnown() { + // If the unknown value has a known prefix then we might be + // able to still produce a known result. + if prefix == "" { + // The empty string is a prefix of any string. + return cty.True, nil + } + if knownPrefix := args[0].Range().StringPrefix(); knownPrefix != "" { + if strings.HasPrefix(knownPrefix, prefix) { + return cty.True, nil + } + if len(knownPrefix) >= len(prefix) { + // If the prefix we're testing is no longer than the known + // prefix and it didn't match then the full string with + // that same prefix can't match either. + return cty.False, nil + } + } + return cty.UnknownVal(cty.Bool), nil + } + + str := args[0].AsString() + if strings.HasPrefix(str, prefix) { return cty.True, nil } @@ -47,7 +81,8 @@ var EndsWithFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.Bool), + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { str := args[0].AsString() suffix := args[1].AsString() @@ -77,7 +112,8 @@ var ReplaceFunc = function.New(&function.Spec{ Type: cty.String, }, }, - Type: function.StaticReturnType(cty.String), + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { str := args[0].AsString() substr := args[1].AsString() @@ -98,8 +134,319 @@ var ReplaceFunc = function.New(&function.Spec{ }, }) +// StrContainsFunc searches a given string for another given substring, +// if found the function returns true, otherwise returns false. +var StrContainsFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + str := args[0].AsString() + substr := args[1].AsString() + + if strings.Contains(str, substr) { + return cty.True, nil + } + + return cty.False, nil + }, +}) + +// TemplateStringFunc renders a template presented either as a literal string +// or as a reference to a string from elsewhere. +func MakeTemplateStringFunc(funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string])) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "template", + Type: customdecode.ExpressionClosureType, + }, + { + Name: "vars", + Type: cty.DynamicPseudoType, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + templateClosure := customdecode.ExpressionClosureFromVal(args[0]) + varsVal := args[1] + + // Our historical experience with the hashicorp/template provider's + // template_file data source tells us that situations where authors + // must write a string template that generates a string template + // cause all sorts of confusion, because the same syntax ends up + // being evaluated in two different contexts with different variables + // in scope, and new authors tend to be attracted to a function + // named "template" and so miss that the language has built-in + // support for inline template expressions. + // + // As a compromise to try to meet the (relatively unusual) use-cases + // where dynamic template fetching is needed without creating an + // attractive nuisance for those who would be better off just writing + // a plain inline template, this function imposes constraints on how + // the template argument may be provided and thus allows us + // to return slightly more helpful error messages. + // + // The only valid way to provide the template argument is as a + // simple, direct reference to some other value in scope that is + // of type string: + // templatestring(local.greeting_template, { name = "Alex" }) + // + // Those with more unusual desires, such as dynamically generating + // a template at runtime by trying to concatenate template chunks + // together, can still do such things by placing the template + // construction expression in a separate local value and then passing + // that local value to the template argument. But the restriction is + // intended to intentionally add an extra "roadbump" so that + // anyone who mistakenly thinks they need templatestring to render + // an inline template (a common mistake for new authors with + // template_file) will hopefully hit this roadblock and refer to + // the function documentation to learn about the other options that + // are probably more suitable for what they need. + switch expr := templateClosure.Expression.(type) { + case *hclsyntax.TemplateWrapExpr: + // This situation occurs when someone writes an interpolation-only + // expression as was required in Terraform v0.11 and earlier. + // Because older versions of Terraform required this and this + // habit has been sticky for some authors, we'll return a + // special error message. + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to treat the inner expression as template syntax, write the reference expression directly without any template interpolation syntax", + ) + case *hclsyntax.TemplateExpr: + // This is the more general case of someone trying to write + // an inline template as the argument. In this case we'll + // distinguish between an entirely-literal template, which + // probably suggests someone was trying to escape their template + // for the function to consume, vs. a template with other + // sequences that suggests someone was just trying to write + // an inline template and so probably doesn't need to call + // this function at all. + literal := true + if len(expr.Parts) != 1 { + literal = false + } else if _, ok := expr.Parts[0].(*hclsyntax.LiteralValueExpr); !ok { + literal = false + } + if literal { + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead", + ) + } else { + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression", + ) + } + default: + if !isValidTemplateStringExpr(expr) { + // Someone who really does want to construct a template dynamically + // can factor out that construction into a local value and refer + // to it in the templatestring call, but it's not really feasible + // to explain that clearly in a short error message so we'll deal + // with that option on the function's documentation page instead, + // where we can show a full example. + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template expression: must be a direct reference to a single string from elsewhere, containing valid Terraform template syntax", + ) + } + } + + templateVal, diags := templateClosure.Value() + if diags.HasErrors() { + // With the constraints we imposed above the possible errors + // here are pretty limited: it must be some kind of invalid + // traversal. As usual HCL diagnostics don't make for very + // good function errors but we've already filtered out many + // common reasons for error here, so we should get here pretty + // rarely. + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template expression: %s", + diags.Error(), + ) + } + if !templateVal.IsKnown() { + // We'll need to wait until we actually know what the template is. + return cty.UnknownVal(retType), nil + } + if templateVal.Type() != cty.String || templateVal.IsNull() { + // We're being a little stricter than usual here and requiring + // exactly a string, rather than just anything that can convert + // to one. This is because the stringification of a number or + // boolean value cannot be a useful template (it wouldn't have + // any template sequences in it) and so far more likely to be + // a mistake than actually intentional. + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template value: a string is required", + ) + } + templateVal, templateMarks := templateVal.Unmark() + templateStr := templateVal.AsString() + expr, diags := hclsyntax.ParseTemplate([]byte(templateStr), "", hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + return cty.UnknownVal(retType), function.NewArgErrorf( + 0, "invalid template: %s", + diags.Error(), + ) + } + + render := makeRenderTemplateFunc(funcsCb, false) + retVal, err := render(expr, varsVal) + if err != nil { + return cty.UnknownVal(retType), err + } + retVal, err = convert.Convert(retVal, cty.String) + if err != nil { + return cty.UnknownVal(retType), fmt.Errorf("invalid template result: %s", err) + } + return retVal.WithMarks(templateMarks), nil + }, + }) +} + +func makeRenderTemplateFunc(funcsCb func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]), allowFS bool) func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) { + return func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) { + if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) { + return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time + } + + ctx := &hcl.EvalContext{ + Variables: varsVal.AsValueMap(), + } + + // We require all of the variables to be valid HCL identifiers, because + // otherwise there would be no way to refer to them in the template + // anyway. Rejecting this here gives better feedback to the user + // than a syntax error somewhere in the template itself. + for n := range ctx.Variables { + if !hclsyntax.ValidIdentifier(n) { + // This error message intentionally doesn't describe _all_ of + // the different permutations that are technically valid as an + // HCL identifier, but rather focuses on what we might + // consider to be an "idiomatic" variable name. + return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n) + } + } + + // We'll pre-check references in the template here so we can give a + // more specialized error message than HCL would by default, so it's + // clearer that this problem is coming from a templatefile call. + for _, traversal := range expr.Variables() { + root := traversal.RootName() + if _, ok := ctx.Variables[root]; !ok { + return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange()) + } + } + + givenFuncs, fsFuncs, templateFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems + funcs := make(map[string]function.Function, len(givenFuncs)) + for name, fn := range givenFuncs { + plainName := strings.TrimPrefix(name, "core::") + switch { + case templateFuncs.Has(plainName): + funcs[name] = function.New(&function.Spec{ + Params: fn.Params(), + VarParam: fn.VarParam(), + Type: func(args []cty.Value) (cty.Type, error) { + return cty.NilType, fmt.Errorf("cannot recursively call %s from inside another template function", plainName) + }, + }) + case !allowFS && fsFuncs.Has(plainName): + // Note: for now this assumes that allowFS is false only for + // the templatestring function, and so mentions that name + // directly in the error message. + funcs[name] = function.New(&function.Spec{ + Params: fn.Params(), + VarParam: fn.VarParam(), + Type: func(args []cty.Value) (cty.Type, error) { + return cty.NilType, fmt.Errorf("cannot use filesystem access functions like %s in templatestring templates; consider passing the function result as a template variable instead", plainName) + }, + }) + default: + funcs[name] = fn + } + } + ctx.Functions = funcs + + val, diags := expr.Value(ctx) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + if val.IsNull() { + return cty.DynamicVal, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Template result is null", + Detail: "The result of the template is null, which is not a valid result for a templatestring call.", + Subject: expr.Range().Ptr(), + } + } + return val, nil + } +} + +func isValidTemplateStringExpr(expr hcl.Expression) bool { + // Our goal with this heuristic is to be as permissive as possible with + // allowing things that authors might try to use as references to a + // template string defined elsewhere, while rejecting complex expressions + // that seem like they might be trying to construct templates dynamically + // or might have resulted from a misunderstanding that "templatestring" is + // the only way to render a template, because someone hasn't learned + // about template expressions yet. + // + // This is here only to give better feedback to folks who seem to be using + // templatestring for something other than what it's intended for, and not + // to block dynamic template generation altogether. Authors who have a + // genuine need for dynamic template generation can always assert that to + // Terraform by factoring out their dynamic generation into a local value + // and referring to it; this rule is just a little speedbump to prompt + // the author to consider whether there's a better way to solve their + // problem, as opposed to just using the first solution they found. + switch expr := expr.(type) { + case *hclsyntax.ScopeTraversalExpr: + // A simple static reference from the current scope is always valid. + return true + + case *hclsyntax.RelativeTraversalExpr: + // Relative traversals are allowed as long as they begin from + // something that would otherwise be allowed. + return isValidTemplateStringExpr(expr.Source) + + case *hclsyntax.IndexExpr: + // Index expressions are allowed as long as the collection is + // also specified using an expression that conforms to these rules. + // The key operand is intentionally unconstrained because that + // is a rule for how to select an element, and so doesn't represent + // a source from which the template string is being retrieved. + return isValidTemplateStringExpr(expr.Collection) + + case *hclsyntax.SplatExpr: + // Splat expressions would be weird to use because they'd typically + // return a tuple and that wouldn't be valid as a template string, + // but we allow it here (as long as the operand would otherwise have + // been allowed) because then we'll let the type mismatch error + // show through, and that's likely a more helpful error message. + return isValidTemplateStringExpr(expr.Source) + + default: + // Nothing else is allowed. + return false + } +} + // Replace searches a given string for another given substring, // and replaces all occurences with a given replacement string. func Replace(str, substr, replace cty.Value) (cty.Value, error) { return ReplaceFunc.Call([]cty.Value{str, substr, replace}) } + +func StrContains(str, substr cty.Value) (cty.Value, error) { + return StrContainsFunc.Call([]cty.Value{str, substr}) +} diff --git a/internal/lang/funcs/string_test.go b/internal/lang/funcs/string_test.go index 7b44a27624..83b602aa15 100644 --- a/internal/lang/funcs/string_test.go +++ b/internal/lang/funcs/string_test.go @@ -1,10 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package funcs import ( "fmt" + "strings" "testing" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/customdecode" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + + "github.com/hashicorp/terraform/internal/collections" ) func TestReplace(t *testing.T) { @@ -71,3 +81,556 @@ func TestReplace(t *testing.T) { }) } } + +func TestStrContains(t *testing.T) { + tests := []struct { + String cty.Value + Substr cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("hello"), + cty.StringVal("hel"), + cty.BoolVal(true), + false, + }, + { + cty.StringVal("hello"), + cty.StringVal("lo"), + cty.BoolVal(true), + false, + }, + { + cty.StringVal("hello1"), + cty.StringVal("1"), + cty.BoolVal(true), + false, + }, + { + cty.StringVal("hello1"), + cty.StringVal("heo"), + cty.BoolVal(false), + false, + }, + { + cty.StringVal("hello1"), + cty.NumberIntVal(1), + cty.UnknownVal(cty.Bool), + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("includes(%#v, %#v)", test.String, test.Substr), func(t *testing.T) { + got, err := StrContains(test.String, test.Substr) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestStartsWith(t *testing.T) { + tests := []struct { + String, Prefix cty.Value + Want cty.Value + WantError string + }{ + { + cty.StringVal("hello world"), + cty.StringVal("hello"), + cty.True, + ``, + }, + { + cty.StringVal("hey world"), + cty.StringVal("hello"), + cty.False, + ``, + }, + { + cty.StringVal(""), + cty.StringVal(""), + cty.True, + ``, + }, + { + cty.StringVal("a"), + cty.StringVal(""), + cty.True, + ``, + }, + { + cty.StringVal(""), + cty.StringVal("a"), + cty.False, + ``, + }, + { + cty.UnknownVal(cty.String), + cty.StringVal("a"), + cty.UnknownVal(cty.Bool).RefineNotNull(), + ``, + }, + { + cty.UnknownVal(cty.String), + cty.StringVal(""), + cty.True, + ``, + }, + { + cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(), + cty.StringVal(""), + cty.True, + ``, + }, + { + cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(), + cty.StringVal("a"), + cty.False, + ``, + }, + { + cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(), + cty.StringVal("ht"), + cty.True, + ``, + }, + { + cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(), + cty.StringVal("https:"), + cty.True, + ``, + }, + { + cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(), + cty.StringVal("https-"), + cty.False, + ``, + }, + { + cty.UnknownVal(cty.String).Refine().StringPrefix("https:").NewValue(), + cty.StringVal("https://"), + cty.UnknownVal(cty.Bool).RefineNotNull(), + ``, + }, + { + // Unicode combining characters edge-case: we match the prefix + // in terms of unicode code units rather than grapheme clusters, + // which is inconsistent with our string processing elsewhere but + // would be a breaking change to fix that bug now. + cty.StringVal("\U0001f937\u200d\u2642"), // "Man Shrugging" is encoded as "Person Shrugging" followed by zero-width joiner and then the masculine gender presentation modifier + cty.StringVal("\U0001f937"), // Just the "Person Shrugging" character without any modifiers + cty.True, + ``, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("StartsWith(%#v, %#v)", test.String, test.Prefix), func(t *testing.T) { + got, err := StartsWithFunc.Call([]cty.Value{test.String, test.Prefix}) + + if test.WantError != "" { + gotErr := fmt.Sprintf("%s", err) + if gotErr != test.WantError { + t.Errorf("wrong error\ngot: %s\nwant: %s", gotErr, test.WantError) + } + return + } else if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !got.RawEquals(test.Want) { + t.Errorf( + "wrong result\nstring: %#v\nprefix: %#v\ngot: %#v\nwant: %#v", + test.String, test.Prefix, got, test.Want, + ) + } + }) + } +} + +func TestTemplateString(t *testing.T) { + // This function has some special restrictions on what syntax is valid + // in its first argument, so we'll test this one using HCL expressions + // as the inputs, rather than direct cty values as we do for most other + // functions in this package. + tests := []struct { + templateExpr string + exprScope map[string]cty.Value + vars cty.Value + want cty.Value + wantErr string + }{ + { // a single string interpolation that evaluates to null should fail + `template`, + map[string]cty.Value{ + "template": cty.StringVal(`${test}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }), + cty.NilVal, + `:1,1-8: Template result is null; The result of the template is null, which is not a valid result for a templatestring call.`, + }, + { // a single string interpolation that evaluates to unknown should not fail + `template`, + map[string]cty.Value{ + "template": cty.StringVal(`${test}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "test": cty.UnknownVal(cty.String), + }), + cty.UnknownVal(cty.String).RefineNotNull(), + ``, + }, + { + `template`, + map[string]cty.Value{ + "template": cty.StringVal(`it's ${a}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + cty.StringVal(`it's a value`), + ``, + }, + { + `template`, + map[string]cty.Value{ + "template": cty.StringVal(`${a}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.True, + }), + // The special treatment of a template with only a single + // interpolation sequence does not apply to templatestring, because + // we're expecting to be evaluating templates fetched dynamically + // from somewhere else and want to avoid callers needing to deal + // with anything other than string results. + cty.StringVal(`true`), + ``, + }, + { + `template`, + map[string]cty.Value{ + "template": cty.StringVal(`${a}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.EmptyTupleVal, + }), + // The special treatment of a template with only a single + // interpolation sequence does not apply to templatestring, because + // we're expecting to be evaluating templates fetched dynamically + // from somewhere else and want to avoid callers needing to deal + // with anything other than string results. + cty.NilVal, + `invalid template result: string required`, + }, + { + `data.whatever.whatever["foo"].result`, + map[string]cty.Value{ + "data": cty.ObjectVal(map[string]cty.Value{ + "whatever": cty.ObjectVal(map[string]cty.Value{ + "whatever": cty.MapVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "result": cty.StringVal("it's ${a}"), + }), + }), + }), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + cty.StringVal(`it's a value`), + ``, + }, + { + `data.whatever.whatever[each.key].result`, + map[string]cty.Value{ + "data": cty.ObjectVal(map[string]cty.Value{ + "whatever": cty.ObjectVal(map[string]cty.Value{ + "whatever": cty.MapVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "result": cty.StringVal("it's ${a}"), + }), + }), + }), + }), + "each": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("foo"), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + cty.StringVal(`it's a value`), + ``, + }, + { + `data.whatever.whatever[*].result`, + map[string]cty.Value{ + "data": cty.ObjectVal(map[string]cty.Value{ + "whatever": cty.ObjectVal(map[string]cty.Value{ + "whatever": cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "result": cty.StringVal("it's ${a}"), + }), + }), + }), + }), + "each": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("foo"), + }), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a value"), + }), + cty.NilVal, + // We have an intentional hole in our heuristic for whether the + // first argument is a suitable expression which permits splat + // expressions just so that we can return the type mismatch error + // from the result not being a string, instead of the more general + // error about it not being a supported expression type. + `invalid template value: a string is required`, + }, + { + `"can't write $${not_allowed}"`, + map[string]cty.Value{}, + cty.ObjectVal(map[string]cty.Value{ + "not_allowed": cty.StringVal("a literal template"), + }), + cty.NilVal, + `invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`, + }, + { + `"can't write ${not_allowed}"`, + map[string]cty.Value{}, + cty.ObjectVal(map[string]cty.Value{ + "not_allowed": cty.StringVal("a literal template"), + }), + cty.NilVal, + `invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`, + }, + { + `"can't write %%{for x in things}a literal template%%{endfor}"`, + map[string]cty.Value{}, + cty.ObjectVal(map[string]cty.Value{ + "things": cty.ListVal([]cty.Value{cty.True}), + }), + cty.NilVal, + `invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere, and so does not support providing a literal template; consider using a template string expression instead`, + }, + { + `"can't write %{for x in things}a literal template%{endfor}"`, + map[string]cty.Value{}, + cty.ObjectVal(map[string]cty.Value{ + "things": cty.ListVal([]cty.Value{cty.True}), + }), + cty.NilVal, + `invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to render an inline template, consider using a plain template string expression`, + }, + { + `"${not_allowed}"`, + map[string]cty.Value{}, + cty.ObjectVal(map[string]cty.Value{ + "not allowed": cty.StringVal("an interp-only template"), + }), + cty.NilVal, + `invalid template expression: templatestring is only for rendering templates retrieved dynamically from elsewhere; to treat the inner expression as template syntax, write the reference expression directly without any template interpolation syntax`, + }, + { + `1 + 1`, + map[string]cty.Value{}, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + `invalid template expression: must be a direct reference to a single string from elsewhere, containing valid Terraform template syntax`, + }, + { + `not_a_string`, + map[string]cty.Value{ + "not_a_string": cty.True, + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + `invalid template value: a string is required`, + }, + { + `with_lower`, + map[string]cty.Value{ + "with_lower": cty.StringVal(`it's ${lower(a)}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("A VALUE"), + }), + cty.StringVal("it's a value"), + ``, + }, + { + `with_core_lower`, + map[string]cty.Value{ + "with_core_lower": cty.StringVal(`it's ${core::lower(a)}`), + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("A VALUE"), + }), + cty.StringVal("it's a value"), + ``, + }, + { + `with_fsfunc`, + map[string]cty.Value{ + "with_fsfunc": cty.StringVal(`it's ${fsfunc()}`), + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + `:1,8-15: Error in function call; Call to function "fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`, + }, + { + `with_core_fsfunc`, + map[string]cty.Value{ + "with_core_fsfunc": cty.StringVal(`it's ${core::fsfunc()}`), + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + `:1,8-21: Error in function call; Call to function "core::fsfunc" failed: cannot use filesystem access functions like fsfunc in templatestring templates; consider passing the function result as a template variable instead.`, + }, + { + `with_templatefunc`, + map[string]cty.Value{ + "with_templatefunc": cty.StringVal(`it's ${templatefunc()}`), + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + `:1,8-21: Error in function call; Call to function "templatefunc" failed: cannot recursively call templatefunc from inside another template function.`, + }, + { + `with_core_templatefunc`, + map[string]cty.Value{ + "with_core_templatefunc": cty.StringVal(`it's ${core::templatefunc()}`), + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + `:1,8-27: Error in function call; Call to function "core::templatefunc" failed: cannot recursively call templatefunc from inside another template function.`, + }, + { + `with_fstemplatefunc`, + map[string]cty.Value{ + "with_fstemplatefunc": cty.StringVal(`it's ${fstemplatefunc()}`), + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + // The template function error takes priority over the filesystem + // function error if calling a function that's in both categories. + `:1,8-23: Error in function call; Call to function "fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`, + }, + { + `with_core_fstemplatefunc`, + map[string]cty.Value{ + "with_core_fstemplatefunc": cty.StringVal(`it's ${core::fstemplatefunc()}`), + }, + cty.ObjectVal(map[string]cty.Value{}), + cty.NilVal, + // The template function error takes priority over the filesystem + // function error if calling a function that's in both categories. + `:1,8-29: Error in function call; Call to function "core::fstemplatefunc" failed: cannot recursively call fstemplatefunc from inside another template function.`, + }, + } + + funcToTest := MakeTemplateStringFunc(func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]) { + // These are the functions available for use inside the nested template + // evaluation context. These are here only to test that we can call + // functions and that the template/filesystem functions get blocked + // with suitable error messages. This is not a realistic set of + // functions that would be available in a real call. + funcs = map[string]function.Function{ + "lower": function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + s := args[0].AsString() + return cty.StringVal(strings.ToLower(s)), nil + }, + }), + "fsfunc": function.New(&function.Spec{ + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.UnknownVal(retType), fmt.Errorf("should not be able to call fsfunc") + }, + }), + "templatefunc": function.New(&function.Spec{ + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.UnknownVal(retType), fmt.Errorf("should not be able to call templatefunc") + }, + }), + "fstemplatefunc": function.New(&function.Spec{ + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.UnknownVal(retType), fmt.Errorf("should not be able to call fstemplatefunc") + }, + }), + } + funcs["core::lower"] = funcs["lower"] + funcs["core::fsfunc"] = funcs["fsfunc"] + funcs["core::templatefunc"] = funcs["templatefunc"] + funcs["core::fstemplatefunc"] = funcs["fstemplatefunc"] + return funcs, collections.NewSetCmp("fsfunc", "fstemplatefunc"), collections.NewSetCmp("templatefunc", "fstemplatefunc") + }) + + for _, test := range tests { + t.Run(test.templateExpr, func(t *testing.T) { + // The following mimics what HCL itself would do when preparing + // the first argument to this function, since the parameter + // uses the special "expression closure type" which causes + // HCL to delay evaluation of the expression and let the + // function handle it directly itself. + expr, diags := hclsyntax.ParseExpression([]byte(test.templateExpr), "", hcl.InitialPos) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Error()) + } + exprClosure := &customdecode.ExpressionClosure{ + Expression: expr, + EvalContext: &hcl.EvalContext{ + Variables: test.exprScope, + }, + } + exprClosureVal := customdecode.ExpressionClosureVal(exprClosure) + + got, gotErr := funcToTest.Call([]cty.Value{exprClosureVal, test.vars}) + + if test.wantErr != "" { + if gotErr == nil { + t.Fatalf("unexpected success\ngot: %#v\nwant error: %s", got, test.wantErr) + } + if got, want := gotErr.Error(), test.wantErr; got != want { + t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) + } + return + } + if gotErr != nil { + t.Errorf("unexpected error: %s", gotErr.Error()) + } + if !test.want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) + } + }) + } +} diff --git a/internal/lang/funcs/testdata/recursive_namespaced.tmpl b/internal/lang/funcs/testdata/recursive_namespaced.tmpl new file mode 100644 index 0000000000..f346bfdaab --- /dev/null +++ b/internal/lang/funcs/testdata/recursive_namespaced.tmpl @@ -0,0 +1 @@ +${core::templatefile("recursive_namespaced.tmpl", {})} \ No newline at end of file diff --git a/internal/lang/function_results.go b/internal/lang/function_results.go new file mode 100644 index 0000000000..6d4f97e700 --- /dev/null +++ b/internal/lang/function_results.go @@ -0,0 +1,134 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package lang + +import ( + "crypto/sha256" + "fmt" + "io" + "log" + "sync" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/zclconf/go-cty/cty" +) + +type priorResultHash struct { + hash [sha256.Size]byte + // when the result was from a current run, we keep a record of the result + // value to aid in debugging. Results stored in the plan will only have the + // hash to avoid bloating the plan with what could be many very large + // values. + value cty.Value +} + +type FunctionResults struct { + mu sync.Mutex + // results stores the prior result from a function call, keyed by + // the hash of the function name and arguments. + results map[[sha256.Size]byte]priorResultHash +} + +// NewFunctionResultsTable initializes a mapping of function calls to prior +// results used to validate function calls. The hashes argument is an +// optional slice of prior result hashes used to preload the cache. +func NewFunctionResultsTable(hashes []FunctionResultHash) *FunctionResults { + res := &FunctionResults{ + results: make(map[[sha256.Size]byte]priorResultHash), + } + + res.insertHashes(hashes) + return res +} + +// CheckPrior compares the function call against any cached results, and returns +// an error if the result does not match a prior call. +func (f *FunctionResults) CheckPrior(name string, args []cty.Value, result cty.Value) error { + return f.CheckPriorProvider(addrs.Provider{}, name, args, result) +} + +// CheckPriorProvider compares the provider function call against any cached +// results, and returns an error if the result does not match a prior call. +func (f *FunctionResults) CheckPriorProvider(provider addrs.Provider, name string, args []cty.Value, result cty.Value) error { + argSum := sha256.New() + + if !provider.IsZero() { + io.WriteString(argSum, provider.String()+"|") + } + io.WriteString(argSum, name) + + for _, arg := range args { + // cty.Values have a Hash method, but it is not collision resistant. We + // are going to rely on the GoString formatting instead, which gives + // detailed results for all values. + io.WriteString(argSum, "|"+arg.GoString()) + } + + f.mu.Lock() + defer f.mu.Unlock() + + argHash := [sha256.Size]byte(argSum.Sum(nil)) + resHash := sha256.Sum256([]byte(result.GoString())) + + res, ok := f.results[argHash] + if !ok { + f.results[argHash] = priorResultHash{ + hash: resHash, + value: result, + } + return nil + } + + if resHash != res.hash { + provPrefix := "" + if !provider.IsZero() { + provPrefix = fmt.Sprintf("provider %s ", provider) + } + // Log the args for debugging in case the hcl context is + // insufficient. The error should be adequate most of the time, and + // could already be quite long, so we don't want to add all + // arguments too. + log.Printf("[ERROR] %sfunction %s returned an inconsistent result with args: %#v\n", provPrefix, name, args) + // The hcl package will add the necessary context around the error in + // the diagnostic, but we add the differing results when we can. + if res.value != cty.NilVal { + return fmt.Errorf("function returned an inconsistent result,\nwas: %#v,\nnow: %#v", res.value, result) + } + return fmt.Errorf("function returned an inconsistent result") + } + + return nil +} + +// insertHashes insert key-value pairs to the functionResults map. This is used +// to preload stored values before any Verify calls are made. +func (f *FunctionResults) insertHashes(hashes []FunctionResultHash) { + f.mu.Lock() + defer f.mu.Unlock() + + for _, res := range hashes { + f.results[[sha256.Size]byte(res.Key)] = priorResultHash{ + hash: [sha256.Size]byte(res.Result), + } + } +} + +// FunctionResultHash contains the key and result hash values from a prior function +// call. +type FunctionResultHash struct { + Key []byte + Result []byte +} + +// copy the hash values into a struct which can be recorded in the plan. +func (f *FunctionResults) GetHashes() []FunctionResultHash { + f.mu.Lock() + defer f.mu.Unlock() + + var res []FunctionResultHash + for k, r := range f.results { + res = append(res, FunctionResultHash{Key: k[:], Result: r.hash[:]}) + } + return res +} diff --git a/internal/lang/function_results_test.go b/internal/lang/function_results_test.go new file mode 100644 index 0000000000..da5715738b --- /dev/null +++ b/internal/lang/function_results_test.go @@ -0,0 +1,187 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package lang + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/zclconf/go-cty/cty" + + // set the correct global logger for tests + _ "github.com/hashicorp/terraform/internal/logging" +) + +func TestFunctionCache(t *testing.T) { + testAddr := addrs.NewDefaultProvider("test") + + type testCall struct { + provider addrs.Provider + name string + args []cty.Value + result cty.Value + } + + tests := []struct { + first, second testCall + expectErr bool + }{ + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.True, + }, + second: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.True, + }, + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.True, + }, + second: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.False, + }, + // result changed from true => false + expectErr: true, + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.UnknownVal(cty.Bool), + }, + second: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.False, + }, + // result changed from unknown => false + expectErr: true, + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.True, + }, + second: testCall{ + provider: addrs.NewDefaultProvider("fake"), + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.False, + }, + // OK because provider changed + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.True, + }, + second: testCall{ + provider: testAddr, + name: "func", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.False, + }, + // OK because function name changed + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok")}, + result: cty.True, + }, + second: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.StringVal("ok"), cty.StringVal("ok")}, + result: cty.False, + }, + // OK because args changed + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.ObjectVal(map[string]cty.Value{ + "attr": cty.NumberIntVal(1), + })}, + result: cty.True, + }, + second: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.ObjectVal(map[string]cty.Value{ + "attr": cty.NumberIntVal(2), + })}, + result: cty.False, + }, + // OK because args changed + }, + { + first: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.UnknownVal(cty.Object(map[string]cty.Type{ + "attr": cty.Number, + }))}, + result: cty.UnknownVal(cty.Bool), + }, + second: testCall{ + provider: testAddr, + name: "fun", + args: []cty.Value{cty.ObjectVal(map[string]cty.Value{ + "attr": cty.NumberIntVal(2), + })}, + result: cty.False, + }, + // OK because args changed from unknown to known + }, + } + + for i, test := range tests { + t.Run(fmt.Sprint(i), func(t *testing.T) { + results := NewFunctionResultsTable(nil) + err := results.CheckPriorProvider(test.first.provider, test.first.name, test.first.args, test.first.result) + if err != nil { + t.Fatal("error on first call!", err) + } + + err = results.CheckPriorProvider(test.second.provider, test.second.name, test.second.args, test.second.result) + + if err != nil && !test.expectErr { + t.Fatal(err) + } + + // reload the data to ensure we validate identically + newResults := NewFunctionResultsTable(results.GetHashes()) + + originalErr := err != nil + reloadedErr := newResults.CheckPriorProvider(test.second.provider, test.second.name, test.second.args, test.second.result) != nil + + if originalErr != reloadedErr { + t.Fatalf("original check returned err:%t, reloaded check returned err:%t", originalErr, reloadedErr) + } + }) + } +} diff --git a/internal/lang/functions.go b/internal/lang/functions.go index 3fc5b02c1d..7f944a32a6 100644 --- a/internal/lang/functions.go +++ b/internal/lang/functions.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( @@ -9,6 +12,7 @@ import ( "github.com/zclconf/go-cty/cty/function" "github.com/zclconf/go-cty/cty/function/stdlib" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/lang/funcs" ) @@ -19,19 +23,44 @@ var impureFunctions = []string{ "uuid", } +// filesystemFunctions are the functions that allow interacting with arbitrary +// paths in the local filesystem, and which can therefore have their results +// vary based on something other than their arguments, and might allow template +// rendering to expose details about the system where Terraform is running. +var filesystemFunctions = collections.NewSetCmp[string]( + "file", + "fileexists", + "fileset", + "filebase64", + "filebase64sha256", + "filebase64sha512", + "filemd5", + "filesha1", + "filesha256", + "filesha512", + "templatefile", +) + +// templateFunctions are functions that render nested templates. These are +// callable from module code but not from within the templates they are +// rendering. +var templateFunctions = collections.NewSetCmp[string]( + "templatefile", + "templatestring", +) + // Functions returns the set of functions that should be used to when evaluating // expressions in the receiving scope. func (s *Scope) Functions() map[string]function.Function { s.funcsLock.Lock() if s.funcs == nil { - // Some of our functions are just directly the cty stdlib functions. - // Others are implemented in the subdirectory "funcs" here in this - // repository. New functions should generally start out their lives - // in the "funcs" directory and potentially graduate to cty stdlib - // later if the functionality seems to be something domain-agnostic - // that would be useful to all applications using cty functions. + s.funcs = baseFunctions(s.BaseDir) - s.funcs = map[string]function.Function{ + // If you're adding something here, please consider whether it meets + // the criteria for either or both of the sets [filesystemFunctions] + // and [templateFunctions] and add it there if so, to ensure that + // functions relying on those classifications will behave correctly. + coreFuncs := map[string]function.Function{ "abs": stdlib.AbsoluteFunc, "abspath": funcs.AbsPathFunc, "alltrue": funcs.AllTrueFunc, @@ -60,17 +89,18 @@ func (s *Scope) Functions() map[string]function.Function { "distinct": stdlib.DistinctFunc, "element": stdlib.ElementFunc, "endswith": funcs.EndsWithFunc, + "ephemeralasnull": funcs.EphemeralAsNullFunc, "chunklist": stdlib.ChunklistFunc, - "file": funcs.MakeFileFunc(s.BaseDir, false), - "fileexists": funcs.MakeFileExistsFunc(s.BaseDir), - "fileset": funcs.MakeFileSetFunc(s.BaseDir), - "filebase64": funcs.MakeFileFunc(s.BaseDir, true), - "filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir), - "filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir), - "filemd5": funcs.MakeFileMd5Func(s.BaseDir), - "filesha1": funcs.MakeFileSha1Func(s.BaseDir), - "filesha256": funcs.MakeFileSha256Func(s.BaseDir), - "filesha512": funcs.MakeFileSha512Func(s.BaseDir), + "file": funcs.MakeFileFunc(s.BaseDir, false, immutableResults("file", s.FunctionResults)), + "fileexists": funcs.MakeFileExistsFunc(s.BaseDir, immutableResults("fileexists", s.FunctionResults)), + "fileset": funcs.MakeFileSetFunc(s.BaseDir, immutableResults("fileset", s.FunctionResults)), + "filebase64": funcs.MakeFileFunc(s.BaseDir, true, immutableResults("filebase64", s.FunctionResults)), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(s.BaseDir, immutableResults("filebase64sha256", s.FunctionResults)), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(s.BaseDir, immutableResults("filebase64sha512", s.FunctionResults)), + "filemd5": funcs.MakeFileMd5Func(s.BaseDir, immutableResults("filemd5", s.FunctionResults)), + "filesha1": funcs.MakeFileSha1Func(s.BaseDir, immutableResults("filesha1", s.FunctionResults)), + "filesha256": funcs.MakeFileSha256Func(s.BaseDir, immutableResults("filesha256", s.FunctionResults)), + "filesha512": funcs.MakeFileSha512Func(s.BaseDir, immutableResults("filesha512", s.FunctionResults)), "flatten": stdlib.FlattenFunc, "floor": stdlib.FloorFunc, "format": stdlib.FormatFunc, @@ -105,6 +135,7 @@ func (s *Scope) Functions() map[string]function.Function { "rsadecrypt": funcs.RsaDecryptFunc, "sensitive": funcs.SensitiveFunc, "nonsensitive": funcs.NonsensitiveFunc, + "issensitive": funcs.IssensitiveFunc, "setintersection": stdlib.SetIntersectionFunc, "setproduct": stdlib.SetProductFunc, "setsubtract": stdlib.SetSubtractFunc, @@ -117,6 +148,7 @@ func (s *Scope) Functions() map[string]function.Function { "sort": stdlib.SortFunc, "split": stdlib.SplitFunc, "startswith": funcs.StartsWithFunc, + "strcontains": funcs.StrContainsFunc, "strrev": stdlib.ReverseFunc, "substr": stdlib.SubstrFunc, "sum": funcs.SumFunc, @@ -148,22 +180,56 @@ func (s *Scope) Functions() map[string]function.Function { "zipmap": stdlib.ZipmapFunc, } - s.funcs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, func() map[string]function.Function { - // The templatefile function prevents recursive calls to itself - // by copying this map and overwriting the "templatefile" entry. - return s.funcs - }) + // Our two template-rendering functions want to be able to call + // all of the other functions themselves, but we pass them indirectly + // via a callback to avoid chicken/egg problems while initializing + // the functions table. + funcsFunc := func() (funcs map[string]function.Function, fsFuncs collections.Set[string], templateFuncs collections.Set[string]) { + // The templatefile and templatestring functions prevent recursive + // calls to themselves and each other by copying this map and + // overwriting the relevant entries. + return s.funcs, filesystemFunctions, templateFunctions + } + coreFuncs["templatefile"] = funcs.MakeTemplateFileFunc(s.BaseDir, funcsFunc, immutableResults("templatefile", s.FunctionResults)) + coreFuncs["templatestring"] = funcs.MakeTemplateStringFunc(funcsFunc) if s.ConsoleMode { // The type function is only available in terraform console. - s.funcs["type"] = funcs.TypeFunc + coreFuncs["type"] = funcs.TypeFunc + } + + if !s.ConsoleMode { + // The plantimestamp function doesn't make sense in the terraform + // console. + coreFuncs["plantimestamp"] = funcs.MakeStaticTimestampFunc(s.PlanTimestamp) } if s.PureOnly { // Force our few impure functions to return unknown so that we // can defer evaluating them until a later pass. for _, name := range impureFunctions { - s.funcs[name] = function.Unpredictable(s.funcs[name]) + coreFuncs[name] = function.Unpredictable(coreFuncs[name]) + } + } + + // All of the built-in functions are also available under the "core::" + // namespace, to distinguish from the "provider::" and "module::" + // namespaces that can serve as external extension points. + s.funcs = make(map[string]function.Function, len(coreFuncs)*2) + for name, fn := range coreFuncs { + fn = funcs.WithDescription(name, fn) + s.funcs[name] = fn + s.funcs["core::"+name] = fn + } + + // We'll also bring in any external functions that the caller provided + // when constructing this scope. For now, that's just + // provider-contributed functions, under a "provider::NAME::" namespace + // where NAME is the local name of the provider in the current module. + for providerLocalName, funcs := range s.ExternalFuncs.Provider { + for funcName, fn := range funcs { + name := fmt.Sprintf("provider::%s::%s", providerLocalName, funcName) + s.funcs[name] = fn } } } @@ -172,6 +238,167 @@ func (s *Scope) Functions() map[string]function.Function { return s.funcs } +// TestingFunctions returns the set of functions available to the testing +// framework. Generally, the testing framework doesn't have access to a specific +// state or plan when executing these functions so some of the functions +// available normally are not available during tests. +func TestingFunctions() map[string]function.Function { + // The baseDir is always the current directory during the tests. + fs := baseFunctions(".") + + // Add a description to each function and parameter based on the + // contents of descriptionList. + // One must create a matching description entry whenever a new + // function is introduced. + for name, f := range fs { + fs[name] = funcs.WithDescription(name, f) + } + + return fs +} + +// baseFunctions loads the set of functions that are used in both the testing +// framework and the main Terraform operations. +func baseFunctions(baseDir string) map[string]function.Function { + // Some of our functions are just directly the cty stdlib functions. + // Others are implemented in the subdirectory "funcs" here in this + // repository. New functions should generally start out their lives + // in the "funcs" directory and potentially graduate to cty stdlib + // later if the functionality seems to be something domain-agnostic + // that would be useful to all applications using cty functions. + // + // If you're adding something here, please consider whether it meets + // the criteria for either or both of the sets [filesystemFunctions] + // and [templateFunctions] and add it there if so, to ensure that + // functions relying on those classifications will behave correctly. + fs := map[string]function.Function{ + "abs": stdlib.AbsoluteFunc, + "abspath": funcs.AbsPathFunc, + "alltrue": funcs.AllTrueFunc, + "anytrue": funcs.AnyTrueFunc, + "basename": funcs.BasenameFunc, + "base64decode": funcs.Base64DecodeFunc, + "base64encode": funcs.Base64EncodeFunc, + "base64gzip": funcs.Base64GzipFunc, + "base64sha256": funcs.Base64Sha256Func, + "base64sha512": funcs.Base64Sha512Func, + "bcrypt": funcs.BcryptFunc, + "can": tryfunc.CanFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "cidrhost": funcs.CidrHostFunc, + "cidrnetmask": funcs.CidrNetmaskFunc, + "cidrsubnet": funcs.CidrSubnetFunc, + "cidrsubnets": funcs.CidrSubnetsFunc, + "coalesce": funcs.CoalesceFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "contains": stdlib.ContainsFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "dirname": funcs.DirnameFunc, + "distinct": stdlib.DistinctFunc, + "element": stdlib.ElementFunc, + "endswith": funcs.EndsWithFunc, + "chunklist": stdlib.ChunklistFunc, + "file": funcs.MakeFileFunc(baseDir, false, noopWrapper), + "fileexists": funcs.MakeFileExistsFunc(baseDir, noopWrapper), + "fileset": funcs.MakeFileSetFunc(baseDir, noopWrapper), + "filebase64": funcs.MakeFileFunc(baseDir, true, noopWrapper), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(baseDir, noopWrapper), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(baseDir, noopWrapper), + "filemd5": funcs.MakeFileMd5Func(baseDir, noopWrapper), + "filesha1": funcs.MakeFileSha1Func(baseDir, noopWrapper), + "filesha256": funcs.MakeFileSha256Func(baseDir, noopWrapper), + "filesha512": funcs.MakeFileSha512Func(baseDir, noopWrapper), + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "format": stdlib.FormatFunc, + "formatdate": stdlib.FormatDateFunc, + "formatlist": stdlib.FormatListFunc, + "indent": stdlib.IndentFunc, + "index": funcs.IndexFunc, // stdlib.IndexFunc is not compatible + "join": stdlib.JoinFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "length": funcs.LengthFunc, + "list": funcs.ListFunc, + "log": stdlib.LogFunc, + "lookup": funcs.LookupFunc, + "lower": stdlib.LowerFunc, + "map": funcs.MapFunc, + "matchkeys": funcs.MatchkeysFunc, + "max": stdlib.MaxFunc, + "md5": funcs.Md5Func, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "one": funcs.OneFunc, + "parseint": stdlib.ParseIntFunc, + "pathexpand": funcs.PathExpandFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "regex": stdlib.RegexFunc, + "regexall": stdlib.RegexAllFunc, + "replace": funcs.ReplaceFunc, + "reverse": stdlib.ReverseListFunc, + "rsadecrypt": funcs.RsaDecryptFunc, + "sensitive": funcs.SensitiveFunc, + "nonsensitive": funcs.NonsensitiveFunc, + "issensitive": funcs.IssensitiveFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setsubtract": stdlib.SetSubtractFunc, + "setunion": stdlib.SetUnionFunc, + "sha1": funcs.Sha1Func, + "sha256": funcs.Sha256Func, + "sha512": funcs.Sha512Func, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "startswith": funcs.StartsWithFunc, + "strcontains": funcs.StrContainsFunc, + "strrev": stdlib.ReverseFunc, + "substr": stdlib.SubstrFunc, + "sum": funcs.SumFunc, + "textdecodebase64": funcs.TextDecodeBase64Func, + "textencodebase64": funcs.TextEncodeBase64Func, + "timestamp": funcs.TimestampFunc, + "timeadd": stdlib.TimeAddFunc, + "timecmp": funcs.TimeCmpFunc, + "title": stdlib.TitleFunc, + "tostring": funcs.MakeToFunc(cty.String), + "tonumber": funcs.MakeToFunc(cty.Number), + "tobool": funcs.MakeToFunc(cty.Bool), + "toset": funcs.MakeToFunc(cty.Set(cty.DynamicPseudoType)), + "tolist": funcs.MakeToFunc(cty.List(cty.DynamicPseudoType)), + "tomap": funcs.MakeToFunc(cty.Map(cty.DynamicPseudoType)), + "transpose": funcs.TransposeFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "try": tryfunc.TryFunc, + "upper": stdlib.UpperFunc, + "urlencode": funcs.URLEncodeFunc, + "uuid": funcs.UUIDFunc, + "uuidv5": funcs.UUIDV5Func, + "values": stdlib.ValuesFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, + "zipmap": stdlib.ZipmapFunc, + } + + fs["templatefile"] = funcs.MakeTemplateFileFunc(baseDir, func() (map[string]function.Function, collections.Set[string], collections.Set[string]) { + // The templatefile function prevents recursive calls to itself + // by copying this map and overwriting the "templatefile" entry. + return fs, filesystemFunctions, templateFunctions + }, noopWrapper) + + return fs +} + // experimentalFunction checks whether the given experiment is enabled for // the recieving scope. If so, it will return the given function verbatim. // If not, it will return a placeholder function that just returns an @@ -202,3 +429,45 @@ func (s *Scope) experimentalFunction(experiment experiments.Experiment, fn funct }, }) } + +// ExternalFuncs represents functions defined by extension components outside +// of Terraform Core. +// +// This package expects the caller to provide ready-to-use function.Function +// instances for each function, which themselves perform whatever adaptations +// are necessary to translate a call into a form suitable for the external +// component that's contributing the function, and to translate the results +// to conform to the expected function return value conventions. +type ExternalFuncs struct { + Provider map[string]map[string]function.Function +} + +// immutableResults is a wrapper for cty function implementations which may +// otherwise not return consistent results because they depends on data outside +// of Terraform. Due to the fact that the cty functions are a concrete type, and +// the implementation is hidden within a private struct field, we need to pass +// along these closures to get the data to the actual call site. +func immutableResults(name string, priorResults *FunctionResults) func(fn function.ImplFunc) function.ImplFunc { + if priorResults == nil { + return func(fn function.ImplFunc) function.ImplFunc { + return fn + } + } + return func(fn function.ImplFunc) function.ImplFunc { + return func(args []cty.Value, retType cty.Type) (cty.Value, error) { + res, err := fn(args, retType) + if err != nil { + return res, err + } + err = priorResults.CheckPrior(name, args, res) + if err != nil { + return cty.UnknownVal(retType), err + } + return res, err + } + } +} + +func noopWrapper(fn function.ImplFunc) function.ImplFunc { + return fn +} diff --git a/internal/lang/functions_descriptions_test.go b/internal/lang/functions_descriptions_test.go new file mode 100644 index 0000000000..ba8fa3dcdd --- /dev/null +++ b/internal/lang/functions_descriptions_test.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package lang + +import ( + "testing" +) + +func TestFunctionDescriptions(t *testing.T) { + scope := &Scope{ + ConsoleMode: true, + } + for name, fn := range scope.Functions() { + if fn.Description() == "" { + t.Errorf("missing DescriptionList entry for function %q", name) + } + } +} diff --git a/internal/lang/functions_test.go b/internal/lang/functions_test.go index 84811d4019..7ccbd1afb5 100644 --- a/internal/lang/functions_test.go +++ b/internal/lang/functions_test.go @@ -1,17 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( "fmt" "os" "path/filepath" + "strings" "testing" + "time" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/lang/marks" homedir "github.com/mitchellh/go-homedir" "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/function/stdlib" ) // TestFunctions tests that functions are callable through the functionality @@ -355,6 +363,17 @@ func TestFunctions(t *testing.T) { }, }, + "ephemeralasnull": { + { + `ephemeralasnull(local.ephemeral)`, + cty.NullVal(cty.String), + }, + { + `ephemeralasnull("not ephemeral")`, + cty.StringVal("not ephemeral"), + }, + }, + "file": { { `file("hello.txt")`, @@ -508,6 +527,13 @@ func TestFunctions(t *testing.T) { }, }, + "issensitive": { + { + `issensitive(1)`, + cty.False, + }, + }, + "join": { { `join(" ", ["Hello", "World"])`, @@ -669,6 +695,13 @@ func TestFunctions(t *testing.T) { }, }, + "plantimestamp": { + { + `plantimestamp()`, + cty.StringVal("2004-04-25T15:00:00Z"), + }, + }, + "pow": { { `pow(1,0)`, @@ -898,6 +931,17 @@ func TestFunctions(t *testing.T) { }, }, + "strcontains": { + { + `strcontains("hello", "llo")`, + cty.BoolVal(true), + }, + { + `strcontains("hello", "a")`, + cty.BoolVal(false), + }, + }, + "strrev": { { `strrev("hello world")`, @@ -938,6 +982,25 @@ func TestFunctions(t *testing.T) { `templatefile("hello.tmpl", {name = "Jodie"})`, cty.StringVal("Hello, Jodie!"), }, + { + `core::templatefile("hello.tmpl", {name = "Namespaced Jodie"})`, + cty.StringVal("Hello, Namespaced Jodie!"), + }, + }, + + "templatestring": { + { + `templatestring(local.greeting_template, { + name = "Arthur" +})`, + cty.StringVal("Hello, Arthur!"), + }, + { + `core::templatestring(local.greeting_template, { + name = "Namespaced Arthur" +})`, + cty.StringVal("Hello, Namespaced Arthur!"), + }, }, "timeadd": { @@ -1078,6 +1141,10 @@ func TestFunctions(t *testing.T) { `upper("hello")`, cty.StringVal("HELLO"), }, + { + `core::upper("hello")`, + cty.StringVal("HELLO"), + }, }, "urlencode": { @@ -1131,6 +1198,10 @@ func TestFunctions(t *testing.T) { "key": cty.StringVal("0ba"), }), }, + { + `yamldecode("~")`, + cty.NullVal(cty.DynamicPseudoType), + }, }, "yamlencode": { @@ -1158,16 +1229,40 @@ func TestFunctions(t *testing.T) { }), }, }, + // External function dispatching tests. These ones are only here to + // test that dispatching to externally-declared functions works + // _at all_, using just some placeholder functions declared in the + // test code below. + "provider::foo::upper": { + { + `provider::foo::upper("hello")`, + cty.StringVal("HELLO"), + }, + }, } experimentalFuncs := map[string]experiments.Experiment{} - experimentalFuncs["defaults"] = experiments.ModuleVariableOptionalAttrs + + // We'll also register a few "external functions" so that we can + // verify that registering these works. The functions actually + // available in a real module will be determined dynamically by + // Terraform core based on declarations in that module, so here + // we're just aiming to test whether dispatching to these works + // at all, not to test that any particular functions work. + externalFuncs := ExternalFuncs{ + Provider: map[string]map[string]function.Function{ + "foo": { + "upper": stdlib.UpperFunc, + }, + }, + } t.Run("all functions are tested", func(t *testing.T) { data := &dataForTests{} // no variables available; we only need literals here scope := &Scope{ - Data: data, - BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + Data: data, + BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + ExternalFuncs: externalFuncs, } // Check that there is at least one test case for each function, omitting @@ -1181,6 +1276,11 @@ func TestFunctions(t *testing.T) { delete(allFunctions, impureFunc) } for f := range scope.Functions() { + if strings.Contains(f, "::") { + // Only non-namespaced functions are absolutely required to + // have at least one test. (Others _may_ have tests.) + continue + } if _, ok := tests[f]; !ok { t.Errorf("Missing test for function %s\n", f) } @@ -1204,8 +1304,9 @@ func TestFunctions(t *testing.T) { t.Run(testName, func(t *testing.T) { data := &dataForTests{} // no variables available; we only need literals here scope := &Scope{ - Data: data, - BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + Data: data, + BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + ExternalFuncs: externalFuncs, } expr, parseDiags := hclsyntax.ParseExpression([]byte(test.src), "test.hcl", hcl.Pos{Line: 1, Column: 1}) @@ -1236,10 +1337,18 @@ func TestFunctions(t *testing.T) { for _, test := range funcTests { t.Run(test.src, func(t *testing.T) { - data := &dataForTests{} // no variables available; we only need literals here + data := &dataForTests{ + LocalValues: map[string]cty.Value{ + "greeting_template": cty.StringVal("Hello, ${name}!"), + "ephemeral": cty.StringVal("ephemeral").Mark(marks.Ephemeral), + }, + } scope := &Scope{ - Data: data, - BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + Data: data, + ParseRef: addrs.ParseRef, + BaseDir: "./testdata/functions-test", // for the functions that read from the filesystem + PlanTimestamp: time.Date(2004, 04, 25, 15, 00, 00, 000, time.UTC), + ExternalFuncs: externalFuncs, } prepareScope(t, scope) @@ -1268,6 +1377,26 @@ func TestFunctions(t *testing.T) { } } +func TestPlanTimeStampUnknown(t *testing.T) { + // plantimestamp should return an unknown if there is no timestamp, which + // happens during validation + expr, parseDiags := hclsyntax.ParseExpression([]byte("plantimestamp()"), "test.hcl", hcl.Pos{Line: 1, Column: 1}) + if parseDiags.HasErrors() { + t.Fatal(parseDiags) + } + + scope := &Scope{} + got, diags := scope.EvalExpr(expr, cty.DynamicPseudoType) + if diags.HasErrors() { + t.Fatal(diags.Err()) + + } + + if got.IsKnown() { + t.Fatalf("plantimestamp() should be unknown, got %#v\n", got) + } +} + const ( CipherBase64 = "eczGaDhXDbOFRZGhjx2etVzWbRqWDlmq0bvNt284JHVbwCgObiuyX9uV0LSAMY707IEgMkExJqXmsB4OWKxvB7epRB9G/3+F+pcrQpODlDuL9oDUAsa65zEpYF0Wbn7Oh7nrMQncyUPpyr9WUlALl0gRWytOA23S+y5joa4M34KFpawFgoqTu/2EEH4Xl1zo+0fy73fEto+nfkUY+meuyGZ1nUx/+DljP7ZqxHBFSlLODmtuTMdswUbHbXbWneW51D7Jm7xB8nSdiA2JQNK5+Sg5x8aNfgvFTt/m2w2+qpsyFa5Wjeu6fZmXSl840CA07aXbk9vN4I81WmJyblD/ZA==" PrivateKey = ` diff --git a/internal/lang/globalref/analyzer.go b/internal/lang/globalref/analyzer.go index 7a24d781ef..90a66cbaae 100644 --- a/internal/lang/globalref/analyzer.go +++ b/internal/lang/globalref/analyzer.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( @@ -34,7 +37,7 @@ import ( // the Analyzer contains caches derived from data in the configuration tree. type Analyzer struct { cfg *configs.Config - providerSchemas map[addrs.Provider]*providers.Schemas + providerSchemas map[addrs.Provider]providers.ProviderSchema } // NewAnalyzer constructs a new analyzer bound to the given configuration and @@ -45,7 +48,7 @@ type Analyzer struct { // The given provider schemas must cover at least all of the providers used // in the given configuration. If not then analysis results will be silently // incomplete for any decision that requires checking schema. -func NewAnalyzer(cfg *configs.Config, providerSchemas map[addrs.Provider]*providers.Schemas) *Analyzer { +func NewAnalyzer(cfg *configs.Config, providerSchemas map[addrs.Provider]providers.ProviderSchema) *Analyzer { if !cfg.Path.IsRoot() { panic(fmt.Sprintf("constructing an Analyzer with non-root module %s", cfg.Path)) } @@ -60,7 +63,7 @@ func NewAnalyzer(cfg *configs.Config, providerSchemas map[addrs.Provider]*provid // ModuleConfig retrieves a module configuration from the configuration the // analyzer belongs to, or nil if there is no module with the given address. func (a *Analyzer) ModuleConfig(addr addrs.ModuleInstance) *configs.Module { - modCfg := a.cfg.DescendentForInstance(addr) + modCfg := a.cfg.DescendantForInstance(addr) if modCfg == nil { return nil } diff --git a/internal/lang/globalref/analyzer_contributing_resources.go b/internal/lang/globalref/analyzer_contributing_resources.go index 4024bafd0c..1f4d78c261 100644 --- a/internal/lang/globalref/analyzer_contributing_resources.go +++ b/internal/lang/globalref/analyzer_contributing_resources.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( diff --git a/internal/lang/globalref/analyzer_contributing_resources_test.go b/internal/lang/globalref/analyzer_contributing_resources_test.go index 79c441c431..158d6ac276 100644 --- a/internal/lang/globalref/analyzer_contributing_resources_test.go +++ b/internal/lang/globalref/analyzer_contributing_resources_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( diff --git a/internal/lang/globalref/analyzer_meta_references.go b/internal/lang/globalref/analyzer_meta_references.go index 9a2bb89920..b81a1a1755 100644 --- a/internal/lang/globalref/analyzer_meta_references.go +++ b/internal/lang/globalref/analyzer_meta_references.go @@ -1,13 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/gocty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/langrefs" ) // MetaReferences inspects the configuration to find the references contained @@ -114,7 +118,7 @@ func (a *Analyzer) metaReferencesInputVariable(calleeAddr addrs.ModuleInstance, if attr == nil { return nil } - refs, _ := lang.ReferencesInExpr(attr.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, attr.Expr) return absoluteRefs(callerAddr, refs) } @@ -134,7 +138,7 @@ func (a *Analyzer) metaReferencesOutputValue(callerAddr addrs.ModuleInstance, ad // We don't check for errors here because we'll make a best effort to // analyze whatever partial result HCL is able to extract. - refs, _ := lang.ReferencesInExpr(oc.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, oc.Expr) return absoluteRefs(calleeAddr, refs) } @@ -151,7 +155,7 @@ func (a *Analyzer) metaReferencesLocalValue(moduleAddr addrs.ModuleInstance, add // We don't check for errors here because we'll make a best effort to // analyze whatever partial result HCL is able to extract. - refs, _ := lang.ReferencesInExpr(local.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, local.Expr) return absoluteRefs(moduleAddr, refs) } @@ -196,13 +200,13 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc // available. In invalid cases we might be dealing with partial information, // and so the schema might be nil so we won't be able to return reference // information for this particular situation. - providerSchema := a.providerSchemas[rc.Provider] - if providerSchema == nil { + providerSchema, ok := a.providerSchemas[rc.Provider] + if !ok { return nil } - resourceTypeSchema, _ := providerSchema.SchemaForResourceAddr(addr.Resource) - if resourceTypeSchema == nil { + resourceTypeSchema := providerSchema.SchemaForResourceAddr(addr.Resource) + if resourceTypeSchema.Body == nil { return nil } @@ -230,11 +234,11 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc // Caller must also update "schema" if necessary. } traverseInBlock := func(name string) ([]hcl.Body, []hcl.Expression) { - if attr := schema.Attributes[name]; attr != nil { + if attr := schema.Body.Attributes[name]; attr != nil { // When we reach a specific attribute we can't traverse any deeper, because attributes are the leaves of the schema. - schema = nil + schema.Body = nil return traverseAttr(bodies, name) - } else if blockType := schema.BlockTypes[name]; blockType != nil { + } else if blockType := schema.Body.BlockTypes[name]; blockType != nil { // We need to take a different action here depending on // the nesting mode of the block type. Some require us // to traverse in two steps in order to select a specific @@ -244,7 +248,7 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc case configschema.NestingSingle, configschema.NestingGroup: // There should be only zero or one blocks of this // type, so we can traverse in only one step. - schema = &blockType.Block + schema.Body = &blockType.Block return traverseNestedBlockSingle(bodies, name) case configschema.NestingMap, configschema.NestingList, configschema.NestingSet: steppingThrough = blockType @@ -254,14 +258,14 @@ func (a *Analyzer) metaReferencesResourceInstance(moduleAddr addrs.ModuleInstanc // we add something new in future we'll bail out // here and conservatively return everything under // the current traversal point. - schema = nil + schema.Body = nil return nil, nil } } // We'll get here if the given name isn't in the schema at all. If so, // there's nothing else to be done here. - schema = nil + schema.Body = nil return nil, nil } Steps: @@ -277,7 +281,7 @@ Steps: // a specific attribute) and so we'll stop early, assuming that // any remaining steps are traversals into an attribute expression // result. - if schema == nil { + if schema.Body == nil { break } @@ -295,10 +299,10 @@ Steps: continue } nextStep(traverseNestedBlockMap(bodies, steppingThroughType, step.Name)) - schema = &steppingThrough.Block + schema.Body = &steppingThrough.Block default: nextStep(traverseInBlock(step.Name)) - if schema == nil { + if schema.Body == nil { // traverseInBlock determined that we've traversed as // deep as we can with reference to schema, so we'll // stop here and just process whatever's selected. @@ -316,7 +320,7 @@ Steps: continue } nextStep(traverseNestedBlockMap(bodies, steppingThroughType, keyVal.AsString())) - schema = &steppingThrough.Block + schema.Body = &steppingThrough.Block case configschema.NestingList: idxVal, err := convert.Convert(step.Key, cty.Number) if err != nil { // Invalid traversal, so can't have any refs @@ -330,7 +334,7 @@ Steps: continue } nextStep(traverseNestedBlockList(bodies, steppingThroughType, idx)) - schema = &steppingThrough.Block + schema.Body = &steppingThrough.Block default: // Note that NestingSet ends up in here because we don't // actually allow traversing into set-backed block types, @@ -348,7 +352,7 @@ Steps: continue } nextStep(traverseInBlock(nameVal.AsString())) - if schema == nil { + if schema.Body == nil { // traverseInBlock determined that we've traversed as // deep as we can with reference to schema, so we'll // stop here and just process whatever's selected. @@ -385,12 +389,12 @@ Steps: var refs []*addrs.Reference for _, expr := range exprs { - moreRefs, _ := lang.ReferencesInExpr(expr) + moreRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr) refs = append(refs, moreRefs...) } - if schema != nil { + if schema.Body != nil { for _, body := range bodies { - moreRefs, _ := lang.ReferencesInBlock(body, schema) + moreRefs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, body, schema.Body) refs = append(refs, moreRefs...) } } diff --git a/internal/lang/globalref/analyzer_meta_references_shortcuts.go b/internal/lang/globalref/analyzer_meta_references_shortcuts.go index 580e99b360..18ea32b502 100644 --- a/internal/lang/globalref/analyzer_meta_references_shortcuts.go +++ b/internal/lang/globalref/analyzer_meta_references_shortcuts.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( "fmt" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" ) // ReferencesFromOutputValue returns all of the direct references from the @@ -19,7 +22,7 @@ func (a *Analyzer) ReferencesFromOutputValue(addr addrs.AbsOutputValue) []Refere if oc == nil { return nil } - refs, _ := lang.ReferencesInExpr(oc.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, oc.Expr) return absoluteRefs(addr.Module, refs) } @@ -35,11 +38,11 @@ func (a *Analyzer) ReferencesFromOutputValue(addr addrs.AbsOutputValue) []Refere // Analyzer.ReferencesFromResourceRepetition to get that same result directly. func (a *Analyzer) ReferencesFromResourceInstance(addr addrs.AbsResourceInstance) []Reference { // Using MetaReferences for this is kinda overkill, since - // lang.ReferencesInBlock would be sufficient really, but + // langrefs.ReferencesInBlock would be sufficient really, but // this ensures we keep consistent in how we build the // resulting absolute references and otherwise aside from // some extra overhead this call boils down to a call to - // lang.ReferencesInBlock anyway. + // langrefs.ReferencesInBlock anyway. fakeRef := Reference{ ContainerAddr: addr.Module, LocalRef: &addrs.Reference{ @@ -76,10 +79,10 @@ func (a *Analyzer) ReferencesFromResourceRepetition(addr addrs.AbsResource) []Re switch { case rc.ForEach != nil: - refs, _ := lang.ReferencesInExpr(rc.ForEach) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, rc.ForEach) return absoluteRefs(addr.Module, refs) case rc.Count != nil: - refs, _ := lang.ReferencesInExpr(rc.Count) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, rc.Count) return absoluteRefs(addr.Module, refs) default: return nil diff --git a/internal/lang/globalref/analyzer_meta_references_test.go b/internal/lang/globalref/analyzer_meta_references_test.go index c693890cf6..b17433de16 100644 --- a/internal/lang/globalref/analyzer_meta_references_test.go +++ b/internal/lang/globalref/analyzer_meta_references_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( diff --git a/internal/lang/globalref/analyzer_test.go b/internal/lang/globalref/analyzer_test.go index 0a66217e7d..39c2439516 100644 --- a/internal/lang/globalref/analyzer_test.go +++ b/internal/lang/globalref/analyzer_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( @@ -5,13 +8,14 @@ import ( "path/filepath" "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/registry" - "github.com/zclconf/go-cty/cty" ) func testAnalyzer(t *testing.T, fixtureName string) *Analyzer { @@ -20,8 +24,8 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer { loader, cleanup := configload.NewLoaderForTests(t) defer cleanup() - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) - _, instDiags := inst.InstallModules(context.Background(), configDir, true, initwd.ModuleInstallHooksImpl{}) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), configDir, "tests", true, false, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatalf("unexpected module installation errors: %s", instDiags.Err().Error()) } @@ -83,13 +87,17 @@ func testAnalyzer(t *testing.T, fixtureName string) *Analyzer { }, }, } - schemas := map[addrs.Provider]*providers.Schemas{ + schemas := map[addrs.Provider]providers.ProviderSchema{ addrs.MustParseProviderSourceString("hashicorp/test"): { - ResourceTypes: map[string]*configschema.Block{ - "test_thing": resourceTypeSchema, + ResourceTypes: map[string]providers.Schema{ + "test_thing": { + Body: resourceTypeSchema, + }, }, - DataSources: map[string]*configschema.Block{ - "test_thing": resourceTypeSchema, + DataSources: map[string]providers.Schema{ + "test_thing": { + Body: resourceTypeSchema, + }, }, }, } diff --git a/internal/lang/globalref/doc.go b/internal/lang/globalref/doc.go index 133a9e7f2a..f4857a8b89 100644 --- a/internal/lang/globalref/doc.go +++ b/internal/lang/globalref/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package globalref is home to some analysis algorithms that aim to answer // questions about references between objects and object attributes across // an entire configuration. diff --git a/internal/lang/globalref/reference.go b/internal/lang/globalref/reference.go index d47cecfa70..62f80d9acb 100644 --- a/internal/lang/globalref/reference.go +++ b/internal/lang/globalref/reference.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package globalref import ( diff --git a/internal/lang/references.go b/internal/lang/langrefs/references.go similarity index 77% rename from internal/lang/references.go rename to internal/lang/langrefs/references.go index 7f41b09b61..92c01869df 100644 --- a/internal/lang/references.go +++ b/internal/lang/langrefs/references.go @@ -1,13 +1,21 @@ -package lang +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package langrefs import ( "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/blocktoattr" "github.com/hashicorp/terraform/internal/tfdiags" ) +// ParseRef describes the signature of a function that can attempt to raise +// a raw HCL traversal into a reference. +type ParseRef func(traversal hcl.Traversal) (*addrs.Reference, tfdiags.Diagnostics) + // References finds all of the references in the given set of traversals, // returning diagnostics if any of the traversals cannot be interpreted as a // reference. @@ -21,7 +29,7 @@ import ( // incomplete or invalid. Otherwise, the returned slice has one reference per // given traversal, though it is not guaranteed that the references will // appear in the same order as the given traversals. -func References(traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnostics) { +func References(parseRef ParseRef, traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnostics) { if len(traversals) == 0 { return nil, nil } @@ -30,7 +38,7 @@ func References(traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnos refs := make([]*addrs.Reference, 0, len(traversals)) for _, traversal := range traversals { - ref, refDiags := addrs.ParseRef(traversal) + ref, refDiags := parseRef(traversal) diags = diags.Append(refDiags) if ref == nil { continue @@ -47,7 +55,7 @@ func References(traversals []hcl.Traversal) ([]*addrs.Reference, tfdiags.Diagnos // // A block schema must be provided so that this function can determine where in // the body variables are expected. -func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Reference, tfdiags.Diagnostics) { +func ReferencesInBlock(parseRef ParseRef, body hcl.Body, schema *configschema.Block) ([]*addrs.Reference, tfdiags.Diagnostics) { if body == nil { return nil, nil } @@ -66,16 +74,16 @@ func ReferencesInBlock(body hcl.Body, schema *configschema.Block) ([]*addrs.Refe // in a better position to test this due to having mock providers etc // available. traversals := blocktoattr.ExpandedVariables(body, schema) - return References(traversals) + return References(parseRef, traversals) } // ReferencesInExpr is a helper wrapper around References that first searches // the given expression for traversals, before converting those traversals // to references. -func ReferencesInExpr(expr hcl.Expression) ([]*addrs.Reference, tfdiags.Diagnostics) { +func ReferencesInExpr(parseRef ParseRef, expr hcl.Expression) ([]*addrs.Reference, tfdiags.Diagnostics) { if expr == nil { return nil, nil } traversals := expr.Variables() - return References(traversals) + return References(parseRef, traversals) } diff --git a/internal/lang/marks/marks.go b/internal/lang/marks/marks.go index 78bb527329..7ba2752d22 100644 --- a/internal/lang/marks/marks.go +++ b/internal/lang/marks/marks.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package marks import ( @@ -36,6 +39,14 @@ func Contains(val cty.Value, mark valueMark) bool { // Terraform. const Sensitive = valueMark("Sensitive") +// Ephemeral indicates that a value exists only in memory during a single +// phase, and thus cannot persist between phases or between rounds. +// +// Ephemeral values can be used only in locations that don't require Terraform +// to persist them as part of artifacts such as state snapshots or saved plan +// files. +const Ephemeral = valueMark("Ephemeral") + // TypeType is used to indicate that the value contains a representation of // another value's type. This is part of the implementation of the console-only // `type` function. diff --git a/internal/lang/marks/paths.go b/internal/lang/marks/paths.go new file mode 100644 index 0000000000..0bb81ae43b --- /dev/null +++ b/internal/lang/marks/paths.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package marks + +import ( + "sort" + "strings" + + "github.com/hashicorp/terraform/internal/lang/format" + "github.com/zclconf/go-cty/cty" +) + +// PathsWithMark produces a list of paths identified as having a specified +// mark in a given set of [cty.PathValueMarks] that presumably resulted from +// deeply-unmarking a [cty.Value]. +// +// This is for situations where a subsystem needs to give special treatment +// to one specific mark value, as opposed to just handling all marks +// generically as cty operations would. The second return value is a +// subset of the given [cty.PathValueMarks] values which contained marks +// other than the one requested, so that a caller that can't preserve other +// marks at all can more easily return an error explaining that. +func PathsWithMark(pvms []cty.PathValueMarks, wantMark any) (withWanted []cty.Path, withOthers []cty.PathValueMarks) { + if len(pvms) == 0 { + // No-allocations path for the common case where there are no marks at all. + return nil, nil + } + + for _, pvm := range pvms { + if _, ok := pvm.Marks[wantMark]; ok { + withWanted = append(withWanted, pvm.Path) + } + + for mark := range pvm.Marks { + if mark != wantMark { + withOthers = append(withOthers, pvm) + // only add a path with unwanted marks a single time + break + } + } + } + + return withWanted, withOthers +} + +// RemoveAll take a series of PathValueMarks and removes the unwanted mark from +// all paths. Paths with no remaining marks will be removed entirely. The +// PathValuesMarks passed in are not cloned, and RemoveAll will modify the +// original values, so the prior set of marks should not be retained for use. +func RemoveAll(pvms []cty.PathValueMarks, remove any) []cty.PathValueMarks { + if len(pvms) == 0 { + // No-allocations path for the common case where there are no marks at all. + return nil + } + + var res []cty.PathValueMarks + + for _, pvm := range pvms { + delete(pvm.Marks, remove) + if len(pvm.Marks) > 0 { + res = append(res, pvm) + } + } + + return res +} + +// MarkPaths transforms the given value by marking each of the given paths +// with the given mark value. +func MarkPaths(val cty.Value, mark any, paths []cty.Path) cty.Value { + if len(paths) == 0 { + // No-allocations path for the common case where there are no marked paths at all. + return val + } + + // For now we'll use cty's slightly lower-level function to achieve this + // result. This is a little inefficient due to an additional dynamic + // allocation for the intermediate data structure, so if that becomes + // a problem in practice then we may wish to write a more direct + // implementation here. + markses := make([]cty.PathValueMarks, len(paths)) + marks := cty.NewValueMarks(mark) + for i, path := range paths { + markses[i] = cty.PathValueMarks{ + Path: path, + Marks: marks, + } + } + return val.MarkWithPaths(markses) +} + +// MarksEqual compares 2 unordered sets of PathValue marks for equality, with +// the comparison using the cty.PathValueMarks.Equal method. +func MarksEqual(a, b []cty.PathValueMarks) bool { + if len(a) == 0 && len(b) == 0 { + return true + } + + if len(a) != len(b) { + return false + } + + less := func(s []cty.PathValueMarks) func(i, j int) bool { + return func(i, j int) bool { + cmp := strings.Compare(format.CtyPath(s[i].Path), format.CtyPath(s[j].Path)) + + switch { + case cmp < 0: + return true + case cmp > 0: + return false + } + // the sort only needs to be consistent, so use the GoString format + // to get a comparable value + return s[i].Marks.GoString() < s[j].Marks.GoString() + } + } + + sort.Slice(a, less(a)) + sort.Slice(b, less(b)) + + for i := 0; i < len(a); i++ { + if !a[i].Equal(b[i]) { + return false + } + } + + return true +} diff --git a/internal/lang/marks/paths_test.go b/internal/lang/marks/paths_test.go new file mode 100644 index 0000000000..f6adf437e3 --- /dev/null +++ b/internal/lang/marks/paths_test.go @@ -0,0 +1,249 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package marks + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestPathsWithMark(t *testing.T) { + input := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("sensitive"), + Marks: cty.NewValueMarks("sensitive"), + }, + { + Path: cty.GetAttrPath("other"), + Marks: cty.NewValueMarks("other"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks("sensitive", "other"), + }, + { + Path: cty.GetAttrPath("neither"), + Marks: cty.NewValueMarks("x", "y"), + }, + } + + gotPaths, gotOthers := PathsWithMark(input, "sensitive") + wantPaths := []cty.Path{ + cty.GetAttrPath("sensitive"), + cty.GetAttrPath("both"), + } + wantOthers := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("other"), + Marks: cty.NewValueMarks("other"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks("sensitive", "other"), + // Note that this intentionally preserves the fact that the + // attribute was both sensitive _and_ had another mark, since + // that gives the caller the most possible information to + // potentially handle this combination in a special way in + // an error message, or whatever. It also conveniently avoids + // allocating a new mark set, which is nice. + }, + { + Path: cty.GetAttrPath("neither"), + Marks: cty.NewValueMarks("x", "y"), + }, + } + + if diff := cmp.Diff(wantPaths, gotPaths, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong matched paths\n%s", diff) + } + if diff := cmp.Diff(wantOthers, gotOthers, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong set of entries with other marks\n%s", diff) + } +} + +func TestRemoveAll(t *testing.T) { + input := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("sensitive"), + Marks: cty.NewValueMarks("sensitive"), + }, + { + Path: cty.GetAttrPath("other"), + Marks: cty.NewValueMarks("other"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks("sensitive", "other"), + }, + } + + want := []cty.PathValueMarks{ + { + Path: cty.GetAttrPath("sensitive"), + Marks: cty.NewValueMarks("sensitive"), + }, + { + Path: cty.GetAttrPath("both"), + Marks: cty.NewValueMarks("sensitive"), + }, + } + + got := RemoveAll(input, "other") + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong matched paths\n%s", diff) + } +} + +func TestMarkPaths(t *testing.T) { + value := cty.ObjectVal(map[string]cty.Value{ + "s": cty.StringVal(".s"), + "l": cty.ListVal([]cty.Value{ + cty.StringVal(".l[0]"), + cty.StringVal(".l[1]"), + }), + "m": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal(`.m["a"]`), + "b": cty.StringVal(`.m["b"]`), + }), + "o": cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal(".o.a"), + "b": cty.StringVal(".o.b"), + }), + "t": cty.TupleVal([]cty.Value{ + cty.StringVal(`.t[0]`), + cty.StringVal(`.t[1]`), + }), + }) + sensitivePaths := []cty.Path{ + cty.GetAttrPath("s"), + cty.GetAttrPath("l").IndexInt(1), + cty.GetAttrPath("m").IndexString("a"), + cty.GetAttrPath("o").GetAttr("b"), + cty.GetAttrPath("t").IndexInt(0), + } + got := MarkPaths(value, Sensitive, sensitivePaths) + want := cty.ObjectVal(map[string]cty.Value{ + "s": cty.StringVal(".s").Mark(Sensitive), + "l": cty.ListVal([]cty.Value{ + cty.StringVal(".l[0]"), + cty.StringVal(".l[1]").Mark(Sensitive), + }), + "m": cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal(`.m["a"]`).Mark(Sensitive), + "b": cty.StringVal(`.m["b"]`), + }), + "o": cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal(".o.a"), + "b": cty.StringVal(".o.b").Mark(Sensitive), + }), + "t": cty.TupleVal([]cty.Value{ + cty.StringVal(`.t[0]`).Mark(Sensitive), + cty.StringVal(`.t[1]`), + }), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + +func TestMarksEqual(t *testing.T) { + for i, tc := range []struct { + a, b []cty.PathValueMarks + equal bool + }{ + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + true, + }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "A"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + false, + }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + {Path: cty.Path{cty.GetAttrStep{Name: "b"}}, Marks: cty.NewValueMarks(Sensitive)}, + {Path: cty.Path{cty.GetAttrStep{Name: "c"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "b"}}, Marks: cty.NewValueMarks(Sensitive)}, + {Path: cty.Path{cty.GetAttrStep{Name: "c"}}, Marks: cty.NewValueMarks(Sensitive)}, + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + true, + }, + { + []cty.PathValueMarks{ + { + Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}}, + Marks: cty.NewValueMarks(Sensitive), + }, + { + Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "c"}}, + Marks: cty.NewValueMarks(Sensitive), + }, + }, + []cty.PathValueMarks{ + { + Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "c"}}, + Marks: cty.NewValueMarks(Sensitive), + }, + { + Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}}, + Marks: cty.NewValueMarks(Sensitive), + }, + }, + true, + }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "b"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + false, + }, + { + nil, + nil, + true, + }, + { + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + nil, + false, + }, + { + nil, + []cty.PathValueMarks{ + {Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(Sensitive)}, + }, + false, + }, + } { + t.Run(fmt.Sprint(i), func(t *testing.T) { + if MarksEqual(tc.a, tc.b) != tc.equal { + t.Fatalf("MarksEqual(\n%#v,\n%#v,\n) != %t\n", tc.a, tc.b, tc.equal) + } + }) + } +} diff --git a/internal/lang/scope.go b/internal/lang/scope.go index 6c229e25d9..9c7cb4666a 100644 --- a/internal/lang/scope.go +++ b/internal/lang/scope.go @@ -1,12 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package lang import ( "sync" + "time" "github.com/zclconf/go-cty/cty/function" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/experiments" + "github.com/hashicorp/terraform/internal/lang/langrefs" ) // Scope is the main type in this package, allowing dynamic evaluation of @@ -16,10 +21,26 @@ type Scope struct { // Data is used to resolve references in expressions. Data Data + // ParseRef is a function that the scope uses to extract references from + // a hcl.Traversal. This controls the type of references the scope currently + // supports. As an example, the testing scope can reference outputs directly + // while the main Terraform context scope can not. This means that this + // function for the testing scope will happily return outputs, while the + // main context scope would fail if a user attempts to reference an output. + ParseRef langrefs.ParseRef + // SelfAddr is the address that the "self" object should be an alias of, // or nil if the "self" object should not be available at all. SelfAddr addrs.Referenceable + // SourceAddr is the address of the source item for the scope. This will + // affect any scoped resources that can be accessed from within this scope. + // + // If nil, access is assumed to be at the module level. So, in practice this + // only needs to be set for items that should be able to access something + // hidden in their own scope. + SourceAddr addrs.Referenceable + // BaseDir is the base directory used by any interpolation functions that // accept filesystem paths as arguments. BaseDir string @@ -30,6 +51,18 @@ type Scope struct { // then differ during apply. PureOnly bool + // ExternalFuncs specifies optional additional functions contributed by + // components outside of Terraform Core. + // + // Do not modify anything this field refers to after constructing a + // Scope value; Scope methods may derive and cache other data from + // this data structure and will assume this entire structure is immutable. + ExternalFuncs ExternalFuncs + + // FunctionResults stores the results from possibly impure functions to + // check for consistency between plan and apply. + FunctionResults *FunctionResults + funcs map[string]function.Function funcsLock sync.Mutex @@ -41,6 +74,10 @@ type Scope struct { // ConsoleMode can be set to true to request any console-only functions are // included in this scope. ConsoleMode bool + + // PlanTimestamp is a timestamp representing when the plan was made. It will + // either have been generated during this operation or read from the plan. + PlanTimestamp time.Time } // SetActiveExperiments allows a caller to declare that a set of experiments diff --git a/internal/lang/types/type_type.go b/internal/lang/types/type_type.go index 14edf5ece2..323544b6d4 100644 --- a/internal/lang/types/type_type.go +++ b/internal/lang/types/type_type.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package types import ( diff --git a/internal/lang/types/types.go b/internal/lang/types/types.go index 69355d90af..856a79ba0b 100644 --- a/internal/lang/types/types.go +++ b/internal/lang/types/types.go @@ -1,2 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package types contains non-standard cty types used only within Terraform. package types diff --git a/internal/legacy/go.mod b/internal/legacy/go.mod new file mode 100644 index 0000000000..3fd3098fb7 --- /dev/null +++ b/internal/legacy/go.mod @@ -0,0 +1,41 @@ +module github.com/hashicorp/terraform/internal/legacy + +replace github.com/hashicorp/terraform => ../.. + +go 1.24.2 + +require ( + github.com/davecgh/go-spew v1.1.1 + github.com/google/go-cmp v0.7.0 + github.com/hashicorp/terraform v0.0.0-00010101000000-000000000000 + github.com/mitchellh/copystructure v1.2.0 + github.com/mitchellh/mapstructure v1.5.0 + github.com/mitchellh/reflectwalk v1.0.2 + github.com/zclconf/go-cty v1.16.2 +) + +require ( + github.com/agext/levenshtein v1.2.3 // indirect + github.com/apparentlymart/go-cidr v1.1.0 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/apparentlymart/go-versions v1.0.2 // indirect + github.com/bmatcuk/doublestar v1.1.5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/hashicorp/go-slug v0.16.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 // indirect + github.com/hashicorp/terraform-registry-address v0.2.4 // indirect + github.com/hashicorp/terraform-svchost v0.1.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/zclconf/go-cty-yaml v1.1.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/mod v0.24.0 // indirect + golang.org/x/net v0.38.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.31.0 // indirect +) diff --git a/internal/legacy/go.sum b/internal/legacy/go.sum new file mode 100644 index 0000000000..5ac74b77ad --- /dev/null +++ b/internal/legacy/go.sum @@ -0,0 +1,509 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/apparentlymart/go-versions v1.0.2 h1:n5Gg9YvSLK8Zzpy743J7abh2jt7z7ammOQ0oTd/5oA4= +github.com/apparentlymart/go-versions v1.0.2/go.mod h1:YF5j7IQtrOAOnsGkniupEA5bfCjzd7i14yu0shZavyM= +github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk= +github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +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= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +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-slug v0.16.3 h1:pe0PMwz2UWN1168QksdW/d7u057itB2gY568iF0E2Ns= +github.com/hashicorp/go-slug v0.16.3/go.mod h1:THWVTAXwJEinbsp4/bBRcmbaO5EYNLTqxbG4tZ3gCYQ= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +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/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2 h1:JP8y98OtHTujECs4s/HxlKc5yql/RlC99Dt1Iz4R+lM= +github.com/hashicorp/hcl/v2 v2.23.1-0.20250203194505-ba0759438da2/go.mod h1:k+HgkLpoWu9OS81sy4j1XKDXaWm/rLysG33v5ibdDnc= +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-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/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +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= +github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0= +github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +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-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/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/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/internal/legacy/helper/acctest/acctest.go b/internal/legacy/helper/acctest/acctest.go deleted file mode 100644 index 9d31031a47..0000000000 --- a/internal/legacy/helper/acctest/acctest.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package acctest contains for Terraform Acceptance Tests -package acctest diff --git a/internal/legacy/helper/acctest/random.go b/internal/legacy/helper/acctest/random.go deleted file mode 100644 index 258e4db70f..0000000000 --- a/internal/legacy/helper/acctest/random.go +++ /dev/null @@ -1,176 +0,0 @@ -package acctest - -import ( - "bytes" - crand "crypto/rand" - "crypto/rsa" - "crypto/x509" - "crypto/x509/pkix" - "encoding/pem" - "fmt" - "math/big" - "math/rand" - "net" - "strings" - "time" - - "golang.org/x/crypto/ssh" - - "github.com/apparentlymart/go-cidr/cidr" -) - -func init() { - rand.Seed(time.Now().UTC().UnixNano()) -} - -// Helpers for generating random tidbits for use in identifiers to prevent -// collisions in acceptance tests. - -// RandInt generates a random integer -func RandInt() int { - return rand.New(rand.NewSource(time.Now().UnixNano())).Int() -} - -// RandomWithPrefix is used to generate a unique name with a prefix, for -// randomizing names in acceptance tests -func RandomWithPrefix(name string) string { - return fmt.Sprintf("%s-%d", name, rand.New(rand.NewSource(time.Now().UnixNano())).Int()) -} - -func RandIntRange(min int, max int) int { - source := rand.New(rand.NewSource(time.Now().UnixNano())) - rangeMax := max - min - - return int(source.Int31n(int32(rangeMax))) -} - -// RandString generates a random alphanumeric string of the length specified -func RandString(strlen int) string { - return RandStringFromCharSet(strlen, CharSetAlphaNum) -} - -// RandStringFromCharSet generates a random string by selecting characters from -// the charset provided -func RandStringFromCharSet(strlen int, charSet string) string { - result := make([]byte, strlen) - for i := 0; i < strlen; i++ { - result[i] = charSet[rand.Intn(len(charSet))] - } - return string(result) -} - -// RandSSHKeyPair generates a public and private SSH key pair. The public key is -// returned in OpenSSH format, and the private key is PEM encoded. -func RandSSHKeyPair(comment string) (string, string, error) { - privateKey, privateKeyPEM, err := genPrivateKey() - if err != nil { - return "", "", err - } - - publicKey, err := ssh.NewPublicKey(&privateKey.PublicKey) - if err != nil { - return "", "", err - } - keyMaterial := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(publicKey))) - return fmt.Sprintf("%s %s", keyMaterial, comment), privateKeyPEM, nil -} - -// RandTLSCert generates a self-signed TLS certificate with a newly created -// private key, and returns both the cert and the private key PEM encoded. -func RandTLSCert(orgName string) (string, string, error) { - template := &x509.Certificate{ - SerialNumber: big.NewInt(int64(RandInt())), - Subject: pkix.Name{ - Organization: []string{orgName}, - }, - NotBefore: time.Now(), - NotAfter: time.Now().Add(24 * time.Hour), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, - BasicConstraintsValid: true, - } - - privateKey, privateKeyPEM, err := genPrivateKey() - if err != nil { - return "", "", err - } - - cert, err := x509.CreateCertificate(crand.Reader, template, template, &privateKey.PublicKey, privateKey) - if err != nil { - return "", "", err - } - - certPEM, err := pemEncode(cert, "CERTIFICATE") - if err != nil { - return "", "", err - } - - return certPEM, privateKeyPEM, nil -} - -// RandIpAddress returns a random IP address in the specified CIDR block. -// The prefix length must be less than 31. -func RandIpAddress(s string) (string, error) { - _, network, err := net.ParseCIDR(s) - if err != nil { - return "", err - } - - firstIp, lastIp := cidr.AddressRange(network) - first := &big.Int{} - first.SetBytes([]byte(firstIp)) - last := &big.Int{} - last.SetBytes([]byte(lastIp)) - r := &big.Int{} - r.Sub(last, first) - if len := r.BitLen(); len > 31 { - return "", fmt.Errorf("CIDR range is too large: %d", len) - } - - max := int(r.Int64()) - if max == 0 { - // panic: invalid argument to Int31n - return firstIp.String(), nil - } - - host, err := cidr.Host(network, RandIntRange(0, max)) - if err != nil { - return "", err - } - - return host.String(), nil -} - -func genPrivateKey() (*rsa.PrivateKey, string, error) { - privateKey, err := rsa.GenerateKey(crand.Reader, 1024) - if err != nil { - return nil, "", err - } - - privateKeyPEM, err := pemEncode(x509.MarshalPKCS1PrivateKey(privateKey), "RSA PRIVATE KEY") - if err != nil { - return nil, "", err - } - - return privateKey, privateKeyPEM, nil -} - -func pemEncode(b []byte, block string) (string, error) { - var buf bytes.Buffer - pb := &pem.Block{Type: block, Bytes: b} - if err := pem.Encode(&buf, pb); err != nil { - return "", err - } - - return buf.String(), nil -} - -const ( - // CharSetAlphaNum is the alphanumeric character set for use with - // RandStringFromCharSet - CharSetAlphaNum = "abcdefghijklmnopqrstuvwxyz012346789" - - // CharSetAlpha is the alphabetical character set for use with - // RandStringFromCharSet - CharSetAlpha = "abcdefghijklmnopqrstuvwxyz" -) diff --git a/internal/legacy/helper/acctest/random_test.go b/internal/legacy/helper/acctest/random_test.go deleted file mode 100644 index 2ac592e5f3..0000000000 --- a/internal/legacy/helper/acctest/random_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package acctest - -import ( - "regexp" - "testing" -) - -func TestRandIpAddress(t *testing.T) { - testCases := []struct { - s string - expected *regexp.Regexp - expectedErr string - }{ - { - s: "1.1.1.1/32", - expected: regexp.MustCompile(`^1\.1\.1\.1$`), - }, - { - s: "10.0.0.0/8", - expected: regexp.MustCompile(`^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$`), - }, - { - s: "0.0.0.0/0", - expectedErr: "CIDR range is too large: 32", - }, - { - s: "449d:e5f1:14b1:ddf3:8525:7e9e:4a0d:4a82/128", - expected: regexp.MustCompile(`^449d:e5f1:14b1:ddf3:8525:7e9e:4a0d:4a82$`), - }, - { - s: "2001:db8::/112", - expected: regexp.MustCompile(`^2001:db8::[[:xdigit:]]{1,4}$`), - }, - { - s: "2001:db8::/64", - expectedErr: "CIDR range is too large: 64", - }, - { - s: "abcdefg", - expectedErr: "invalid CIDR address: abcdefg", - }, - } - - for i, tc := range testCases { - v, err := RandIpAddress(tc.s) - if err != nil { - msg := err.Error() - if tc.expectedErr == "" { - t.Fatalf("expected test case %d to succeed but got error %q, ", i, msg) - } - if msg != tc.expectedErr { - t.Fatalf("expected test case %d to fail with %q but got %q", i, tc.expectedErr, msg) - } - } else if !tc.expected.MatchString(v) { - t.Fatalf("expected test case %d to return %q but got %q", i, tc.expected, v) - } - } -} diff --git a/internal/legacy/helper/acctest/remotetests.go b/internal/legacy/helper/acctest/remotetests.go deleted file mode 100644 index 87c60b8be4..0000000000 --- a/internal/legacy/helper/acctest/remotetests.go +++ /dev/null @@ -1,27 +0,0 @@ -package acctest - -import ( - "net/http" - "os" - "testing" -) - -// SkipRemoteTestsEnvVar is an environment variable that can be set by a user -// running the tests in an environment with limited network connectivity. By -// default, tests requiring internet connectivity make an effort to skip if no -// internet is available, but in some cases the smoke test will pass even -// though the test should still be skipped. -const SkipRemoteTestsEnvVar = "TF_SKIP_REMOTE_TESTS" - -// RemoteTestPrecheck is meant to be run by any unit test that requires -// outbound internet connectivity. The test will be skipped if it's -// unavailable. -func RemoteTestPrecheck(t *testing.T) { - if os.Getenv(SkipRemoteTestsEnvVar) != "" { - t.Skipf("skipping test, %s was set", SkipRemoteTestsEnvVar) - } - - if _, err := http.Get("http://google.com"); err != nil { - t.Skipf("skipping, internet seems to not be available: %s", err) - } -} diff --git a/internal/legacy/helper/hashcode/hashcode.go b/internal/legacy/helper/hashcode/hashcode.go index a3faed380c..6e18b52575 100644 --- a/internal/legacy/helper/hashcode/hashcode.go +++ b/internal/legacy/helper/hashcode/hashcode.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hashcode import ( diff --git a/internal/legacy/helper/hashcode/hashcode_test.go b/internal/legacy/helper/hashcode/hashcode_test.go index 4b90431bab..2f9d1f6650 100644 --- a/internal/legacy/helper/hashcode/hashcode_test.go +++ b/internal/legacy/helper/hashcode/hashcode_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package hashcode import ( diff --git a/internal/legacy/helper/schema/backend.go b/internal/legacy/helper/schema/backend.go index 7bd9426abe..68255d29a1 100644 --- a/internal/legacy/helper/schema/backend.go +++ b/internal/legacy/helper/schema/backend.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/backend_test.go b/internal/legacy/helper/schema/backend_test.go index 8b0336fe0e..49b0933b3a 100644 --- a/internal/legacy/helper/schema/backend_test.go +++ b/internal/legacy/helper/schema/backend_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/core_schema.go b/internal/legacy/helper/schema/core_schema.go index da9c502da0..5ca7bd04d3 100644 --- a/internal/legacy/helper/schema/core_schema.go +++ b/internal/legacy/helper/schema/core_schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/core_schema_test.go b/internal/legacy/helper/schema/core_schema_test.go index 84649c8bec..a86b57bb8f 100644 --- a/internal/legacy/helper/schema/core_schema_test.go +++ b/internal/legacy/helper/schema/core_schema_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/data_source_resource_shim.go b/internal/legacy/helper/schema/data_source_resource_shim.go index 8d93750aed..c7e51b6c05 100644 --- a/internal/legacy/helper/schema/data_source_resource_shim.go +++ b/internal/legacy/helper/schema/data_source_resource_shim.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/doc.go b/internal/legacy/helper/schema/doc.go index f1a0e86da4..90e845909f 100644 --- a/internal/legacy/helper/schema/doc.go +++ b/internal/legacy/helper/schema/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package schema is a legacy package that used to represent the SDK, which is now its own // library external to Terraform Core https://github.com/hashicorp/terraform-plugin-sdk // Some of it is still used by Terraform's remote state backends, but this entire diff --git a/internal/legacy/helper/schema/equal.go b/internal/legacy/helper/schema/equal.go index d5e20e0388..f713ec464d 100644 --- a/internal/legacy/helper/schema/equal.go +++ b/internal/legacy/helper/schema/equal.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema // Equal is an interface that checks for deep equality between two objects. diff --git a/internal/legacy/helper/schema/field_reader.go b/internal/legacy/helper/schema/field_reader.go index 2a66a068fb..781aa27d59 100644 --- a/internal/legacy/helper/schema/field_reader.go +++ b/internal/legacy/helper/schema/field_reader.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_config.go b/internal/legacy/helper/schema/field_reader_config.go index f4a43d1fce..15b848fc5f 100644 --- a/internal/legacy/helper/schema/field_reader_config.go +++ b/internal/legacy/helper/schema/field_reader_config.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_config_test.go b/internal/legacy/helper/schema/field_reader_config_test.go index 7a22f3ce16..c700905d02 100644 --- a/internal/legacy/helper/schema/field_reader_config_test.go +++ b/internal/legacy/helper/schema/field_reader_config_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_diff.go b/internal/legacy/helper/schema/field_reader_diff.go index 84ebe272e0..671491d7c5 100644 --- a/internal/legacy/helper/schema/field_reader_diff.go +++ b/internal/legacy/helper/schema/field_reader_diff.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_diff_test.go b/internal/legacy/helper/schema/field_reader_diff_test.go index 1f6fa7da17..dff8bd132c 100644 --- a/internal/legacy/helper/schema/field_reader_diff_test.go +++ b/internal/legacy/helper/schema/field_reader_diff_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_map.go b/internal/legacy/helper/schema/field_reader_map.go index 53f73b71bb..05d5c6870e 100644 --- a/internal/legacy/helper/schema/field_reader_map.go +++ b/internal/legacy/helper/schema/field_reader_map.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_map_test.go b/internal/legacy/helper/schema/field_reader_map_test.go index 2723674a32..b1f12ff3b1 100644 --- a/internal/legacy/helper/schema/field_reader_map_test.go +++ b/internal/legacy/helper/schema/field_reader_map_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_multi.go b/internal/legacy/helper/schema/field_reader_multi.go index 89ad3a86f2..42c229dcb2 100644 --- a/internal/legacy/helper/schema/field_reader_multi.go +++ b/internal/legacy/helper/schema/field_reader_multi.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_multi_test.go b/internal/legacy/helper/schema/field_reader_multi_test.go index 7410335f68..ceabbd5fb8 100644 --- a/internal/legacy/helper/schema/field_reader_multi_test.go +++ b/internal/legacy/helper/schema/field_reader_multi_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_reader_test.go b/internal/legacy/helper/schema/field_reader_test.go index 2c62eb0a8a..ebf8f84673 100644 --- a/internal/legacy/helper/schema/field_reader_test.go +++ b/internal/legacy/helper/schema/field_reader_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_writer.go b/internal/legacy/helper/schema/field_writer.go index 9abc41b54f..62f4e661c0 100644 --- a/internal/legacy/helper/schema/field_writer.go +++ b/internal/legacy/helper/schema/field_writer.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema // FieldWriters are responsible for writing fields by address into diff --git a/internal/legacy/helper/schema/field_writer_map.go b/internal/legacy/helper/schema/field_writer_map.go index fca3bab0a9..b13f045692 100644 --- a/internal/legacy/helper/schema/field_writer_map.go +++ b/internal/legacy/helper/schema/field_writer_map.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/field_writer_map_test.go b/internal/legacy/helper/schema/field_writer_map_test.go index d1a7932aad..7ae1b648cb 100644 --- a/internal/legacy/helper/schema/field_writer_map_test.go +++ b/internal/legacy/helper/schema/field_writer_map_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/provider.go b/internal/legacy/helper/schema/provider.go deleted file mode 100644 index c64c2e3827..0000000000 --- a/internal/legacy/helper/schema/provider.go +++ /dev/null @@ -1,477 +0,0 @@ -package schema - -import ( - "context" - "errors" - "fmt" - "sort" - "sync" - - multierror "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/legacy/terraform" -) - -var ReservedProviderFields = []string{ - "alias", - "version", -} - -// Provider represents a resource provider in Terraform, and properly -// implements all of the ResourceProvider API. -// -// By defining a schema for the configuration of the provider, the -// map of supporting resources, and a configuration function, the schema -// framework takes over and handles all the provider operations for you. -// -// After defining the provider structure, it is unlikely that you'll require any -// of the methods on Provider itself. -type Provider struct { - // Schema is the schema for the configuration of this provider. If this - // provider has no configuration, this can be omitted. - // - // The keys of this map are the configuration keys, and the value is - // the schema describing the value of the configuration. - Schema map[string]*Schema - - // ResourcesMap is the list of available resources that this provider - // can manage, along with their Resource structure defining their - // own schemas and CRUD operations. - // - // Provider automatically handles routing operations such as Apply, - // Diff, etc. to the proper resource. - ResourcesMap map[string]*Resource - - // DataSourcesMap is the collection of available data sources that - // this provider implements, with a Resource instance defining - // the schema and Read operation of each. - // - // Resource instances for data sources must have a Read function - // and must *not* implement Create, Update or Delete. - DataSourcesMap map[string]*Resource - - // ProviderMetaSchema is the schema for the configuration of the meta - // information for this provider. If this provider has no meta info, - // this can be omitted. This functionality is currently experimental - // and subject to change or break without warning; it should only be - // used by providers that are collaborating on its use with the - // Terraform team. - ProviderMetaSchema map[string]*Schema - - // ConfigureFunc is a function for configuring the provider. If the - // provider doesn't need to be configured, this can be omitted. - // - // See the ConfigureFunc documentation for more information. - ConfigureFunc ConfigureFunc - - // MetaReset is called by TestReset to reset any state stored in the meta - // interface. This is especially important if the StopContext is stored by - // the provider. - MetaReset func() error - - meta interface{} - - // a mutex is required because TestReset can directly replace the stopCtx - stopMu sync.Mutex - stopCtx context.Context - stopCtxCancel context.CancelFunc - stopOnce sync.Once - - TerraformVersion string -} - -// ConfigureFunc is the function used to configure a Provider. -// -// The interface{} value returned by this function is stored and passed into -// the subsequent resources as the meta parameter. This return value is -// usually used to pass along a configured API client, a configuration -// structure, etc. -type ConfigureFunc func(*ResourceData) (interface{}, error) - -// InternalValidate should be called to validate the structure -// of the provider. -// -// This should be called in a unit test for any provider to verify -// before release that a provider is properly configured for use with -// this library. -func (p *Provider) InternalValidate() error { - if p == nil { - return errors.New("provider is nil") - } - - var validationErrors error - sm := schemaMap(p.Schema) - if err := sm.InternalValidate(sm); err != nil { - validationErrors = multierror.Append(validationErrors, err) - } - - // Provider-specific checks - for k, _ := range sm { - if isReservedProviderFieldName(k) { - return fmt.Errorf("%s is a reserved field name for a provider", k) - } - } - - for k, r := range p.ResourcesMap { - if err := r.InternalValidate(nil, true); err != nil { - validationErrors = multierror.Append(validationErrors, fmt.Errorf("resource %s: %s", k, err)) - } - } - - for k, r := range p.DataSourcesMap { - if err := r.InternalValidate(nil, false); err != nil { - validationErrors = multierror.Append(validationErrors, fmt.Errorf("data source %s: %s", k, err)) - } - } - - return validationErrors -} - -func isReservedProviderFieldName(name string) bool { - for _, reservedName := range ReservedProviderFields { - if name == reservedName { - return true - } - } - return false -} - -// Meta returns the metadata associated with this provider that was -// returned by the Configure call. It will be nil until Configure is called. -func (p *Provider) Meta() interface{} { - return p.meta -} - -// SetMeta can be used to forcefully set the Meta object of the provider. -// Note that if Configure is called the return value will override anything -// set here. -func (p *Provider) SetMeta(v interface{}) { - p.meta = v -} - -// Stopped reports whether the provider has been stopped or not. -func (p *Provider) Stopped() bool { - ctx := p.StopContext() - select { - case <-ctx.Done(): - return true - default: - return false - } -} - -// StopCh returns a channel that is closed once the provider is stopped. -func (p *Provider) StopContext() context.Context { - p.stopOnce.Do(p.stopInit) - - p.stopMu.Lock() - defer p.stopMu.Unlock() - - return p.stopCtx -} - -func (p *Provider) stopInit() { - p.stopMu.Lock() - defer p.stopMu.Unlock() - - p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background()) -} - -// Stop implementation of terraform.ResourceProvider interface. -func (p *Provider) Stop() error { - p.stopOnce.Do(p.stopInit) - - p.stopMu.Lock() - defer p.stopMu.Unlock() - - p.stopCtxCancel() - return nil -} - -// TestReset resets any state stored in the Provider, and will call TestReset -// on Meta if it implements the TestProvider interface. -// This may be used to reset the schema.Provider at the start of a test, and is -// automatically called by resource.Test. -func (p *Provider) TestReset() error { - p.stopInit() - if p.MetaReset != nil { - return p.MetaReset() - } - return nil -} - -// GetSchema implementation of terraform.ResourceProvider interface -func (p *Provider) GetSchema(req *terraform.ProviderSchemaRequest) (*terraform.ProviderSchema, error) { - resourceTypes := map[string]*configschema.Block{} - dataSources := map[string]*configschema.Block{} - - for _, name := range req.ResourceTypes { - if r, exists := p.ResourcesMap[name]; exists { - resourceTypes[name] = r.CoreConfigSchema() - } - } - for _, name := range req.DataSources { - if r, exists := p.DataSourcesMap[name]; exists { - dataSources[name] = r.CoreConfigSchema() - } - } - - return &terraform.ProviderSchema{ - Provider: schemaMap(p.Schema).CoreConfigSchema(), - ResourceTypes: resourceTypes, - DataSources: dataSources, - }, nil -} - -// Input implementation of terraform.ResourceProvider interface. -func (p *Provider) Input( - input terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - return schemaMap(p.Schema).Input(input, c) -} - -// Validate implementation of terraform.ResourceProvider interface. -func (p *Provider) Validate(c *terraform.ResourceConfig) ([]string, []error) { - if err := p.InternalValidate(); err != nil { - return nil, []error{fmt.Errorf( - "Internal validation of the provider failed! This is always a bug\n"+ - "with the provider itself, and not a user issue. Please report\n"+ - "this bug:\n\n%s", err)} - } - - return schemaMap(p.Schema).Validate(c) -} - -// ValidateResource implementation of terraform.ResourceProvider interface. -func (p *Provider) ValidateResource( - t string, c *terraform.ResourceConfig) ([]string, []error) { - r, ok := p.ResourcesMap[t] - if !ok { - return nil, []error{fmt.Errorf( - "Provider doesn't support resource: %s", t)} - } - - return r.Validate(c) -} - -// Configure implementation of terraform.ResourceProvider interface. -func (p *Provider) Configure(c *terraform.ResourceConfig) error { - // No configuration - if p.ConfigureFunc == nil { - return nil - } - - sm := schemaMap(p.Schema) - - // Get a ResourceData for this configuration. To do this, we actually - // generate an intermediary "diff" although that is never exposed. - diff, err := sm.Diff(nil, c, nil, p.meta, true) - if err != nil { - return err - } - - data, err := sm.Data(nil, diff) - if err != nil { - return err - } - - meta, err := p.ConfigureFunc(data) - if err != nil { - return err - } - - p.meta = meta - return nil -} - -// Apply implementation of terraform.ResourceProvider interface. -func (p *Provider) Apply( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - d *terraform.InstanceDiff) (*terraform.InstanceState, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.Apply(s, d, p.meta) -} - -// Diff implementation of terraform.ResourceProvider interface. -func (p *Provider) Diff( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.Diff(s, c, p.meta) -} - -// SimpleDiff is used by the new protocol wrappers to get a diff that doesn't -// attempt to calculate ignore_changes. -func (p *Provider) SimpleDiff( - info *terraform.InstanceInfo, - s *terraform.InstanceState, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.simpleDiff(s, c, p.meta) -} - -// Refresh implementation of terraform.ResourceProvider interface. -func (p *Provider) Refresh( - info *terraform.InstanceInfo, - s *terraform.InstanceState) (*terraform.InstanceState, error) { - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - return r.Refresh(s, p.meta) -} - -// Resources implementation of terraform.ResourceProvider interface. -func (p *Provider) Resources() []terraform.ResourceType { - keys := make([]string, 0, len(p.ResourcesMap)) - for k := range p.ResourcesMap { - keys = append(keys, k) - } - sort.Strings(keys) - - result := make([]terraform.ResourceType, 0, len(keys)) - for _, k := range keys { - resource := p.ResourcesMap[k] - - // This isn't really possible (it'd fail InternalValidate), but - // we do it anyways to avoid a panic. - if resource == nil { - resource = &Resource{} - } - - result = append(result, terraform.ResourceType{ - Name: k, - Importable: resource.Importer != nil, - - // Indicates that a provider is compiled against a new enough - // version of core to support the GetSchema method. - SchemaAvailable: true, - }) - } - - return result -} - -func (p *Provider) ImportState( - info *terraform.InstanceInfo, - id string) ([]*terraform.InstanceState, error) { - // Find the resource - r, ok := p.ResourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown resource type: %s", info.Type) - } - - // If it doesn't support import, error - if r.Importer == nil { - return nil, fmt.Errorf("resource %s doesn't support import", info.Type) - } - - // Create the data - data := r.Data(nil) - data.SetId(id) - data.SetType(info.Type) - - // Call the import function - results := []*ResourceData{data} - if r.Importer.State != nil { - var err error - results, err = r.Importer.State(data, p.meta) - if err != nil { - return nil, err - } - } - - // Convert the results to InstanceState values and return it - states := make([]*terraform.InstanceState, len(results)) - for i, r := range results { - states[i] = r.State() - } - - // Verify that all are non-nil. If there are any nil the error - // isn't obvious so we circumvent that with a friendlier error. - for _, s := range states { - if s == nil { - return nil, fmt.Errorf( - "nil entry in ImportState results. This is always a bug with\n" + - "the resource that is being imported. Please report this as\n" + - "a bug to Terraform.") - } - } - - return states, nil -} - -// ValidateDataSource implementation of terraform.ResourceProvider interface. -func (p *Provider) ValidateDataSource( - t string, c *terraform.ResourceConfig) ([]string, []error) { - r, ok := p.DataSourcesMap[t] - if !ok { - return nil, []error{fmt.Errorf( - "Provider doesn't support data source: %s", t)} - } - - return r.Validate(c) -} - -// ReadDataDiff implementation of terraform.ResourceProvider interface. -func (p *Provider) ReadDataDiff( - info *terraform.InstanceInfo, - c *terraform.ResourceConfig) (*terraform.InstanceDiff, error) { - - r, ok := p.DataSourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown data source: %s", info.Type) - } - - return r.Diff(nil, c, p.meta) -} - -// RefreshData implementation of terraform.ResourceProvider interface. -func (p *Provider) ReadDataApply( - info *terraform.InstanceInfo, - d *terraform.InstanceDiff) (*terraform.InstanceState, error) { - - r, ok := p.DataSourcesMap[info.Type] - if !ok { - return nil, fmt.Errorf("unknown data source: %s", info.Type) - } - - return r.ReadDataApply(d, p.meta) -} - -// DataSources implementation of terraform.ResourceProvider interface. -func (p *Provider) DataSources() []terraform.DataSource { - keys := make([]string, 0, len(p.DataSourcesMap)) - for k, _ := range p.DataSourcesMap { - keys = append(keys, k) - } - sort.Strings(keys) - - result := make([]terraform.DataSource, 0, len(keys)) - for _, k := range keys { - result = append(result, terraform.DataSource{ - Name: k, - - // Indicates that a provider is compiled against a new enough - // version of core to support the GetSchema method. - SchemaAvailable: true, - }) - } - - return result -} diff --git a/internal/legacy/helper/schema/provider_test.go b/internal/legacy/helper/schema/provider_test.go deleted file mode 100644 index 1b176bc0d2..0000000000 --- a/internal/legacy/helper/schema/provider_test.go +++ /dev/null @@ -1,620 +0,0 @@ -package schema - -import ( - "fmt" - "reflect" - "strings" - "testing" - "time" - - "github.com/google/go-cmp/cmp" - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/legacy/terraform" -) - -func TestProvider_impl(t *testing.T) { - var _ terraform.ResourceProvider = new(Provider) -} - -func TestProviderGetSchema(t *testing.T) { - // This functionality is already broadly tested in core_schema_test.go, - // so this is just to ensure that the call passes through correctly. - p := &Provider{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Required: true, - }, - }, - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Schema: map[string]*Schema{ - "bar": { - Type: TypeString, - Required: true, - }, - }, - }, - }, - DataSourcesMap: map[string]*Resource{ - "baz": &Resource{ - Schema: map[string]*Schema{ - "bur": { - Type: TypeString, - Required: true, - }, - }, - }, - }, - } - - want := &terraform.ProviderSchema{ - Provider: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": &configschema.Attribute{ - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }, - ResourceTypes: map[string]*configschema.Block{ - "foo": testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bar": &configschema.Attribute{ - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - DataSources: map[string]*configschema.Block{ - "baz": testResource(&configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "bur": &configschema.Attribute{ - Type: cty.String, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{}, - }), - }, - } - got, err := p.GetSchema(&terraform.ProviderSchemaRequest{ - ResourceTypes: []string{"foo", "bar"}, - DataSources: []string{"baz", "bar"}, - }) - if err != nil { - t.Fatalf("unexpected error %s", err) - } - - if !cmp.Equal(got, want, equateEmpty, typeComparer) { - t.Error("wrong result:\n", cmp.Diff(got, want, equateEmpty, typeComparer)) - } -} - -func TestProviderConfigure(t *testing.T) { - cases := []struct { - P *Provider - Config map[string]interface{} - Err bool - }{ - { - P: &Provider{}, - Config: nil, - Err: false, - }, - - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ConfigureFunc: func(d *ResourceData) (interface{}, error) { - if d.Get("foo").(int) == 42 { - return nil, nil - } - - return nil, fmt.Errorf("nope") - }, - }, - Config: map[string]interface{}{ - "foo": 42, - }, - Err: false, - }, - - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ConfigureFunc: func(d *ResourceData) (interface{}, error) { - if d.Get("foo").(int) == 42 { - return nil, nil - } - - return nil, fmt.Errorf("nope") - }, - }, - Config: map[string]interface{}{ - "foo": 52, - }, - Err: true, - }, - } - - for i, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - err := tc.P.Configure(c) - if err != nil != tc.Err { - t.Fatalf("%d: %s", i, err) - } - } -} - -func TestProviderResources(t *testing.T) { - cases := []struct { - P *Provider - Result []terraform.ResourceType - }{ - { - P: &Provider{}, - Result: []terraform.ResourceType{}, - }, - - { - P: &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": nil, - "bar": nil, - }, - }, - Result: []terraform.ResourceType{ - terraform.ResourceType{Name: "bar", SchemaAvailable: true}, - terraform.ResourceType{Name: "foo", SchemaAvailable: true}, - }, - }, - - { - P: &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": nil, - "bar": &Resource{Importer: &ResourceImporter{}}, - "baz": nil, - }, - }, - Result: []terraform.ResourceType{ - terraform.ResourceType{Name: "bar", Importable: true, SchemaAvailable: true}, - terraform.ResourceType{Name: "baz", SchemaAvailable: true}, - terraform.ResourceType{Name: "foo", SchemaAvailable: true}, - }, - }, - } - - for i, tc := range cases { - actual := tc.P.Resources() - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d: %#v", i, actual) - } - } -} - -func TestProviderDataSources(t *testing.T) { - cases := []struct { - P *Provider - Result []terraform.DataSource - }{ - { - P: &Provider{}, - Result: []terraform.DataSource{}, - }, - - { - P: &Provider{ - DataSourcesMap: map[string]*Resource{ - "foo": nil, - "bar": nil, - }, - }, - Result: []terraform.DataSource{ - terraform.DataSource{Name: "bar", SchemaAvailable: true}, - terraform.DataSource{Name: "foo", SchemaAvailable: true}, - }, - }, - } - - for i, tc := range cases { - actual := tc.P.DataSources() - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d: got %#v; want %#v", i, actual, tc.Result) - } - } -} - -func TestProviderValidate(t *testing.T) { - cases := []struct { - P *Provider - Config map[string]interface{} - Err bool - }{ - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": &Schema{}, - }, - }, - Config: nil, - Err: true, - }, - } - - for i, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - _, es := tc.P.Validate(c) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v", i, es) - } - } -} - -func TestProviderDiff_legacyTimeoutType(t *testing.T) { - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "blah": &Resource{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(10 * time.Minute), - }, - }, - }, - } - - invalidCfg := map[string]interface{}{ - "foo": 42, - "timeouts": []interface{}{ - map[string]interface{}{ - "create": "40m", - }, - }, - } - ic := terraform.NewResourceConfigRaw(invalidCfg) - _, err := p.Diff( - &terraform.InstanceInfo{ - Type: "blah", - }, - nil, - ic, - ) - if err != nil { - t.Fatal(err) - } -} - -func TestProviderDiff_timeoutInvalidValue(t *testing.T) { - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "blah": &Resource{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeInt, - Optional: true, - }, - }, - Timeouts: &ResourceTimeout{ - Create: DefaultTimeout(10 * time.Minute), - }, - }, - }, - } - - invalidCfg := map[string]interface{}{ - "foo": 42, - "timeouts": map[string]interface{}{ - "create": "invalid", - }, - } - ic := terraform.NewResourceConfigRaw(invalidCfg) - _, err := p.Diff( - &terraform.InstanceInfo{ - Type: "blah", - }, - nil, - ic, - ) - if err == nil { - t.Fatal("Expected provider.Diff to fail with invalid timeout value") - } - expectedErrMsg := `time: invalid duration "invalid"` - if !strings.Contains(err.Error(), expectedErrMsg) { - t.Fatalf("Unexpected error message: %q\nExpected message to contain %q", - err.Error(), - expectedErrMsg) - } -} - -func TestProviderValidateResource(t *testing.T) { - cases := []struct { - P *Provider - Type string - Config map[string]interface{} - Err bool - }{ - { - P: &Provider{}, - Type: "foo", - Config: nil, - Err: true, - }, - - { - P: &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{}, - }, - }, - Type: "foo", - Config: nil, - Err: false, - }, - } - - for i, tc := range cases { - c := terraform.NewResourceConfigRaw(tc.Config) - _, es := tc.P.ValidateResource(tc.Type, c) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v", i, es) - } - } -} - -func TestProviderImportState_default(t *testing.T) { - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Importer: &ResourceImporter{}, - }, - }, - } - - states, err := p.ImportState(&terraform.InstanceInfo{ - Type: "foo", - }, "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if len(states) != 1 { - t.Fatalf("bad: %#v", states) - } - if states[0].ID != "bar" { - t.Fatalf("bad: %#v", states) - } -} - -func TestProviderImportState_setsId(t *testing.T) { - var val string - stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) { - val = d.Id() - return []*ResourceData{d}, nil - } - - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Importer: &ResourceImporter{ - State: stateFunc, - }, - }, - }, - } - - _, err := p.ImportState(&terraform.InstanceInfo{ - Type: "foo", - }, "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if val != "bar" { - t.Fatal("should set id") - } -} - -func TestProviderImportState_setsType(t *testing.T) { - var tVal string - stateFunc := func(d *ResourceData, meta interface{}) ([]*ResourceData, error) { - d.SetId("foo") - tVal = d.State().Ephemeral.Type - return []*ResourceData{d}, nil - } - - p := &Provider{ - ResourcesMap: map[string]*Resource{ - "foo": &Resource{ - Importer: &ResourceImporter{ - State: stateFunc, - }, - }, - }, - } - - _, err := p.ImportState(&terraform.InstanceInfo{ - Type: "foo", - }, "bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if tVal != "foo" { - t.Fatal("should set type") - } -} - -func TestProviderMeta(t *testing.T) { - p := new(Provider) - if v := p.Meta(); v != nil { - t.Fatalf("bad: %#v", v) - } - - expected := 42 - p.SetMeta(42) - if v := p.Meta(); !reflect.DeepEqual(v, expected) { - t.Fatalf("bad: %#v", v) - } -} - -func TestProviderStop(t *testing.T) { - var p Provider - - if p.Stopped() { - t.Fatal("should not be stopped") - } - - // Verify stopch blocks - ch := p.StopContext().Done() - select { - case <-ch: - t.Fatal("should not be stopped") - case <-time.After(10 * time.Millisecond): - } - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - // Verify - if !p.Stopped() { - t.Fatal("should be stopped") - } - - select { - case <-ch: - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} - -func TestProviderStop_stopFirst(t *testing.T) { - var p Provider - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - // Verify - if !p.Stopped() { - t.Fatal("should be stopped") - } - - select { - case <-p.StopContext().Done(): - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} - -func TestProviderReset(t *testing.T) { - var p Provider - stopCtx := p.StopContext() - p.MetaReset = func() error { - stopCtx = p.StopContext() - return nil - } - - // cancel the current context - p.Stop() - - if err := p.TestReset(); err != nil { - t.Fatal(err) - } - - // the first context should have been replaced - if err := stopCtx.Err(); err != nil { - t.Fatal(err) - } - - // we should not get a canceled context here either - if err := p.StopContext().Err(); err != nil { - t.Fatal(err) - } -} - -func TestProvider_InternalValidate(t *testing.T) { - cases := []struct { - P *Provider - ExpectedErr error - }{ - { - P: &Provider{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeBool, - Optional: true, - }, - }, - }, - ExpectedErr: nil, - }, - { // Reserved resource fields should be allowed in provider block - P: &Provider{ - Schema: map[string]*Schema{ - "provisioner": { - Type: TypeString, - Optional: true, - }, - "count": { - Type: TypeInt, - Optional: true, - }, - }, - }, - ExpectedErr: nil, - }, - { // Reserved provider fields should not be allowed - P: &Provider{ - Schema: map[string]*Schema{ - "alias": { - Type: TypeString, - Optional: true, - }, - }, - }, - ExpectedErr: fmt.Errorf("%s is a reserved field name for a provider", "alias"), - }, - } - - for i, tc := range cases { - err := tc.P.InternalValidate() - if tc.ExpectedErr == nil { - if err != nil { - t.Fatalf("%d: Error returned (expected no error): %s", i, err) - } - continue - } - if tc.ExpectedErr != nil && err == nil { - t.Fatalf("%d: Expected error (%s), but no error returned", i, tc.ExpectedErr) - } - if err.Error() != tc.ExpectedErr.Error() { - t.Fatalf("%d: Errors don't match. Expected: %#v Given: %#v", i, tc.ExpectedErr, err) - } - } -} diff --git a/internal/legacy/helper/schema/provisioner.go b/internal/legacy/helper/schema/provisioner.go deleted file mode 100644 index 5c2cce2b0b..0000000000 --- a/internal/legacy/helper/schema/provisioner.go +++ /dev/null @@ -1,205 +0,0 @@ -package schema - -import ( - "context" - "errors" - "fmt" - "sync" - - "github.com/hashicorp/go-multierror" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/legacy/terraform" -) - -// Provisioner represents a resource provisioner in Terraform and properly -// implements all of the ResourceProvisioner API. -// -// This higher level structure makes it much easier to implement a new or -// custom provisioner for Terraform. -// -// The function callbacks for this structure are all passed a context object. -// This context object has a number of pre-defined values that can be accessed -// via the global functions defined in context.go. -type Provisioner struct { - // ConnSchema is the schema for the connection settings for this - // provisioner. - // - // The keys of this map are the configuration keys, and the value is - // the schema describing the value of the configuration. - // - // NOTE: The value of connection keys can only be strings for now. - ConnSchema map[string]*Schema - - // Schema is the schema for the usage of this provisioner. - // - // The keys of this map are the configuration keys, and the value is - // the schema describing the value of the configuration. - Schema map[string]*Schema - - // ApplyFunc is the function for executing the provisioner. This is required. - // It is given a context. See the Provisioner struct docs for more - // information. - ApplyFunc func(ctx context.Context) error - - // ValidateFunc is a function for extended validation. This is optional - // and should be used when individual field validation is not enough. - ValidateFunc func(*terraform.ResourceConfig) ([]string, []error) - - stopCtx context.Context - stopCtxCancel context.CancelFunc - stopOnce sync.Once -} - -// Keys that can be used to access data in the context parameters for -// Provisioners. -var ( - connDataInvalid = contextKey("data invalid") - - // This returns a *ResourceData for the connection information. - // Guaranteed to never be nil. - ProvConnDataKey = contextKey("provider conn data") - - // This returns a *ResourceData for the config information. - // Guaranteed to never be nil. - ProvConfigDataKey = contextKey("provider config data") - - // This returns a terraform.UIOutput. Guaranteed to never be nil. - ProvOutputKey = contextKey("provider output") - - // This returns the raw InstanceState passed to Apply. Guaranteed to - // be set, but may be nil. - ProvRawStateKey = contextKey("provider raw state") -) - -// InternalValidate should be called to validate the structure -// of the provisioner. -// -// This should be called in a unit test to verify before release that this -// structure is properly configured for use. -func (p *Provisioner) InternalValidate() error { - if p == nil { - return errors.New("provisioner is nil") - } - - var validationErrors error - { - sm := schemaMap(p.ConnSchema) - if err := sm.InternalValidate(sm); err != nil { - validationErrors = multierror.Append(validationErrors, err) - } - } - - { - sm := schemaMap(p.Schema) - if err := sm.InternalValidate(sm); err != nil { - validationErrors = multierror.Append(validationErrors, err) - } - } - - if p.ApplyFunc == nil { - validationErrors = multierror.Append(validationErrors, fmt.Errorf( - "ApplyFunc must not be nil")) - } - - return validationErrors -} - -// StopContext returns a context that checks whether a provisioner is stopped. -func (p *Provisioner) StopContext() context.Context { - p.stopOnce.Do(p.stopInit) - return p.stopCtx -} - -func (p *Provisioner) stopInit() { - p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background()) -} - -// Stop implementation of terraform.ResourceProvisioner interface. -func (p *Provisioner) Stop() error { - p.stopOnce.Do(p.stopInit) - p.stopCtxCancel() - return nil -} - -// GetConfigSchema implementation of terraform.ResourceProvisioner interface. -func (p *Provisioner) GetConfigSchema() (*configschema.Block, error) { - return schemaMap(p.Schema).CoreConfigSchema(), nil -} - -// Apply implementation of terraform.ResourceProvisioner interface. -func (p *Provisioner) Apply( - o terraform.UIOutput, - s *terraform.InstanceState, - c *terraform.ResourceConfig) error { - var connData, configData *ResourceData - - { - // We first need to turn the connection information into a - // terraform.ResourceConfig so that we can use that type to more - // easily build a ResourceData structure. We do this by simply treating - // the conn info as configuration input. - raw := make(map[string]interface{}) - if s != nil { - for k, v := range s.Ephemeral.ConnInfo { - raw[k] = v - } - } - - c := terraform.NewResourceConfigRaw(raw) - sm := schemaMap(p.ConnSchema) - diff, err := sm.Diff(nil, c, nil, nil, true) - if err != nil { - return err - } - connData, err = sm.Data(nil, diff) - if err != nil { - return err - } - } - - { - // Build the configuration data. Doing this requires making a "diff" - // even though that's never used. We use that just to get the correct types. - configMap := schemaMap(p.Schema) - diff, err := configMap.Diff(nil, c, nil, nil, true) - if err != nil { - return err - } - configData, err = configMap.Data(nil, diff) - if err != nil { - return err - } - } - - // Build the context and call the function - ctx := p.StopContext() - ctx = context.WithValue(ctx, ProvConnDataKey, connData) - ctx = context.WithValue(ctx, ProvConfigDataKey, configData) - ctx = context.WithValue(ctx, ProvOutputKey, o) - ctx = context.WithValue(ctx, ProvRawStateKey, s) - return p.ApplyFunc(ctx) -} - -// Validate implements the terraform.ResourceProvisioner interface. -func (p *Provisioner) Validate(c *terraform.ResourceConfig) (ws []string, es []error) { - if err := p.InternalValidate(); err != nil { - return nil, []error{fmt.Errorf( - "Internal validation of the provisioner failed! This is always a bug\n"+ - "with the provisioner itself, and not a user issue. Please report\n"+ - "this bug:\n\n%s", err)} - } - - if p.Schema != nil { - w, e := schemaMap(p.Schema).Validate(c) - ws = append(ws, w...) - es = append(es, e...) - } - - if p.ValidateFunc != nil { - w, e := p.ValidateFunc(c) - ws = append(ws, w...) - es = append(es, e...) - } - - return ws, es -} diff --git a/internal/legacy/helper/schema/provisioner_test.go b/internal/legacy/helper/schema/provisioner_test.go deleted file mode 100644 index 228dacd72c..0000000000 --- a/internal/legacy/helper/schema/provisioner_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package schema - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" - - "github.com/hashicorp/terraform/internal/legacy/terraform" -) - -func TestProvisioner_impl(t *testing.T) { - var _ terraform.ResourceProvisioner = new(Provisioner) -} - -func noopApply(ctx context.Context) error { - return nil -} - -func TestProvisionerValidate(t *testing.T) { - cases := []struct { - Name string - P *Provisioner - Config map[string]interface{} - Err bool - Warns []string - }{ - { - Name: "No ApplyFunc", - P: &Provisioner{}, - Config: nil, - Err: true, - }, - { - Name: "Incorrect schema", - P: &Provisioner{ - Schema: map[string]*Schema{ - "foo": {}, - }, - ApplyFunc: noopApply, - }, - Config: nil, - Err: true, - }, - { - "Basic required field", - &Provisioner{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - ApplyFunc: noopApply, - }, - nil, - true, - nil, - }, - - { - "Basic required field set", - &Provisioner{ - Schema: map[string]*Schema{ - "foo": &Schema{ - Required: true, - Type: TypeString, - }, - }, - ApplyFunc: noopApply, - }, - map[string]interface{}{ - "foo": "bar", - }, - false, - nil, - }, - { - Name: "Warning from property validation", - P: &Provisioner{ - Schema: map[string]*Schema{ - "foo": { - Type: TypeString, - Optional: true, - ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { - ws = append(ws, "Simple warning from property validation") - return - }, - }, - }, - ApplyFunc: noopApply, - }, - Config: map[string]interface{}{ - "foo": "", - }, - Err: false, - Warns: []string{"Simple warning from property validation"}, - }, - { - Name: "No schema", - P: &Provisioner{ - Schema: nil, - ApplyFunc: noopApply, - }, - Config: nil, - Err: false, - }, - { - Name: "Warning from provisioner ValidateFunc", - P: &Provisioner{ - Schema: nil, - ApplyFunc: noopApply, - ValidateFunc: func(*terraform.ResourceConfig) (ws []string, errors []error) { - ws = append(ws, "Simple warning from provisioner ValidateFunc") - return - }, - }, - Config: nil, - Err: false, - Warns: []string{"Simple warning from provisioner ValidateFunc"}, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - ws, es := tc.P.Validate(c) - if len(es) > 0 != tc.Err { - t.Fatalf("%d: %#v %s", i, es, es) - } - if (tc.Warns != nil || len(ws) != 0) && !reflect.DeepEqual(ws, tc.Warns) { - t.Fatalf("%d: warnings mismatch, actual: %#v", i, ws) - } - }) - } -} - -func TestProvisionerApply(t *testing.T) { - cases := []struct { - Name string - P *Provisioner - Conn map[string]string - Config map[string]interface{} - Err bool - }{ - { - "Basic config", - &Provisioner{ - ConnSchema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - cd := ctx.Value(ProvConnDataKey).(*ResourceData) - d := ctx.Value(ProvConfigDataKey).(*ResourceData) - if d.Get("foo").(int) != 42 { - return fmt.Errorf("bad config data") - } - if cd.Get("foo").(string) != "bar" { - return fmt.Errorf("bad conn data") - } - - return nil - }, - }, - map[string]string{ - "foo": "bar", - }, - map[string]interface{}{ - "foo": 42, - }, - false, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - c := terraform.NewResourceConfigRaw(tc.Config) - - state := &terraform.InstanceState{ - Ephemeral: terraform.EphemeralState{ - ConnInfo: tc.Conn, - }, - } - - err := tc.P.Apply(nil, state, c) - if err != nil != tc.Err { - t.Fatalf("%d: %s", i, err) - } - }) - } -} - -func TestProvisionerApply_nilState(t *testing.T) { - p := &Provisioner{ - ConnSchema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - return nil - }, - } - - conf := map[string]interface{}{ - "foo": 42, - } - - c := terraform.NewResourceConfigRaw(conf) - err := p.Apply(nil, nil, c) - if err != nil { - t.Fatalf("err: %s", err) - } -} - -func TestProvisionerStop(t *testing.T) { - var p Provisioner - - // Verify stopch blocks - ch := p.StopContext().Done() - select { - case <-ch: - t.Fatal("should not be stopped") - case <-time.After(10 * time.Millisecond): - } - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - select { - case <-ch: - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} - -func TestProvisionerStop_apply(t *testing.T) { - p := &Provisioner{ - ConnSchema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Schema: map[string]*Schema{ - "foo": &Schema{ - Type: TypeInt, - Optional: true, - }, - }, - - ApplyFunc: func(ctx context.Context) error { - <-ctx.Done() - return nil - }, - } - - conn := map[string]string{ - "foo": "bar", - } - - conf := map[string]interface{}{ - "foo": 42, - } - - c := terraform.NewResourceConfigRaw(conf) - state := &terraform.InstanceState{ - Ephemeral: terraform.EphemeralState{ - ConnInfo: conn, - }, - } - - // Run the apply in a goroutine - doneCh := make(chan struct{}) - go func() { - p.Apply(nil, state, c) - close(doneCh) - }() - - // Should block - select { - case <-doneCh: - t.Fatal("should not be done") - case <-time.After(10 * time.Millisecond): - } - - // Stop! - p.Stop() - - select { - case <-doneCh: - case <-time.After(10 * time.Millisecond): - t.Fatal("should be done") - } -} - -func TestProvisionerStop_stopFirst(t *testing.T) { - var p Provisioner - - // Stop it - if err := p.Stop(); err != nil { - t.Fatalf("err: %s", err) - } - - select { - case <-p.StopContext().Done(): - case <-time.After(10 * time.Millisecond): - t.Fatal("should be stopped") - } -} diff --git a/internal/legacy/helper/schema/resource.go b/internal/legacy/helper/schema/resource.go index 28fa54e38c..6ca395b320 100644 --- a/internal/legacy/helper/schema/resource.go +++ b/internal/legacy/helper/schema/resource.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_data.go b/internal/legacy/helper/schema/resource_data.go index 3a61e34932..b728e3545a 100644 --- a/internal/legacy/helper/schema/resource_data.go +++ b/internal/legacy/helper/schema/resource_data.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_data_get_source.go b/internal/legacy/helper/schema/resource_data_get_source.go index 8bfb079be6..76affb83c3 100644 --- a/internal/legacy/helper/schema/resource_data_get_source.go +++ b/internal/legacy/helper/schema/resource_data_get_source.go @@ -1,6 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema -//go:generate go run golang.org/x/tools/cmd/stringer -type=getSource resource_data_get_source.go +//go:generate go tool golang.org/x/tools/cmd/stringer -type=getSource resource_data_get_source.go // getSource represents the level we want to get for a value (internally). // Any source less than or equal to the level will be loaded (whichever diff --git a/internal/legacy/helper/schema/resource_data_test.go b/internal/legacy/helper/schema/resource_data_test.go index 22ad45b6b8..1115fb8772 100644 --- a/internal/legacy/helper/schema/resource_data_test.go +++ b/internal/legacy/helper/schema/resource_data_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_diff.go b/internal/legacy/helper/schema/resource_diff.go index 72d4711eb2..69052db1e1 100644 --- a/internal/legacy/helper/schema/resource_diff.go +++ b/internal/legacy/helper/schema/resource_diff.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_diff_test.go b/internal/legacy/helper/schema/resource_diff_test.go index 9737177147..64bc4f769f 100644 --- a/internal/legacy/helper/schema/resource_diff_test.go +++ b/internal/legacy/helper/schema/resource_diff_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_importer.go b/internal/legacy/helper/schema/resource_importer.go index 5dada3caf3..ded31060a5 100644 --- a/internal/legacy/helper/schema/resource_importer.go +++ b/internal/legacy/helper/schema/resource_importer.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema // ResourceImporter defines how a resource is imported in Terraform. This diff --git a/internal/legacy/helper/schema/resource_test.go b/internal/legacy/helper/schema/resource_test.go index 47f508d81b..6e3d8b4f2e 100644 --- a/internal/legacy/helper/schema/resource_test.go +++ b/internal/legacy/helper/schema/resource_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_timeout.go b/internal/legacy/helper/schema/resource_timeout.go index df033d4c45..c212b509a7 100644 --- a/internal/legacy/helper/schema/resource_timeout.go +++ b/internal/legacy/helper/schema/resource_timeout.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/resource_timeout_test.go b/internal/legacy/helper/schema/resource_timeout_test.go index f5091755b4..73271d52fc 100644 --- a/internal/legacy/helper/schema/resource_timeout_test.go +++ b/internal/legacy/helper/schema/resource_timeout_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/schema.go b/internal/legacy/helper/schema/schema.go index 99657c4660..0db20d8308 100644 --- a/internal/legacy/helper/schema/schema.go +++ b/internal/legacy/helper/schema/schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // schema is a high-level framework for easily writing new providers // for Terraform. Usage of schema is recommended over attempting to write // to the low-level plugin interfaces manually. @@ -12,12 +15,10 @@ package schema import ( - "context" "fmt" "os" "reflect" "regexp" - "sort" "strconv" "strings" "sync" @@ -610,68 +611,6 @@ func (m schemaMap) Diff( return result, nil } -// Input implements the terraform.ResourceProvider method by asking -// for input for required configuration keys that don't have a value. -func (m schemaMap) Input( - input terraform.UIInput, - c *terraform.ResourceConfig) (*terraform.ResourceConfig, error) { - keys := make([]string, 0, len(m)) - for k, _ := range m { - keys = append(keys, k) - } - sort.Strings(keys) - - for _, k := range keys { - v := m[k] - - // Skip things that don't require config, if that is even valid - // for a provider schema. - // Required XOR Optional must always be true to validate, so we only - // need to check one. - if v.Optional { - continue - } - - // Deprecated fields should never prompt - if v.Deprecated != "" { - continue - } - - // Skip things that have a value of some sort already - if _, ok := c.Raw[k]; ok { - continue - } - - // Skip if it has a default value - defaultValue, err := v.DefaultValue() - if err != nil { - return nil, fmt.Errorf("%s: error loading default: %s", k, err) - } - if defaultValue != nil { - continue - } - - var value interface{} - switch v.Type { - case TypeBool, TypeInt, TypeFloat, TypeSet, TypeList: - continue - case TypeString: - value, err = m.inputString(input, k, v) - default: - panic(fmt.Sprintf("Unknown type for input: %#v", v.Type)) - } - - if err != nil { - return nil, fmt.Errorf( - "%s: %s", k, err) - } - - c.Config[k] = value - } - - return c, nil -} - // Validate validates the configuration against this schema mapping. func (m schemaMap) Validate(c *terraform.ResourceConfig) ([]string, []error) { return m.validateObject("", m, c) @@ -1319,20 +1258,6 @@ func (m schemaMap) diffString( return nil } -func (m schemaMap) inputString( - input terraform.UIInput, - k string, - schema *Schema) (interface{}, error) { - result, err := input.Input(context.Background(), &terraform.InputOpts{ - Id: k, - Query: k, - Description: schema.Description, - Default: schema.InputDefault, - }) - - return result, err -} - func (m schemaMap) validate( k string, schema *Schema, diff --git a/internal/legacy/helper/schema/schema_test.go b/internal/legacy/helper/schema/schema_test.go index dcc2008e72..c9b5e6d143 100644 --- a/internal/legacy/helper/schema/schema_test.go +++ b/internal/legacy/helper/schema/schema_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( @@ -3179,229 +3182,6 @@ func TestSchemaMap_Diff(t *testing.T) { } } -func TestSchemaMap_Input(t *testing.T) { - cases := map[string]struct { - Schema map[string]*Schema - Config map[string]interface{} - Input map[string]string - Result map[string]interface{} - Err bool - }{ - /* - * String decode - */ - - "no input on optional field with no config": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Input: map[string]string{}, - Result: map[string]interface{}{}, - Err: false, - }, - - "input ignored when config has a value": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Optional: true, - }, - }, - - Config: map[string]interface{}{ - "availability_zone": "bar", - }, - - Input: map[string]string{ - "availability_zone": "foo", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input ignored when schema has a default": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Default: "foo", - Optional: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input ignored when default function returns a value": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return "foo", nil - }, - Optional: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input ignored when default function returns an empty string": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Default: "", - Optional: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{}, - - Err: false, - }, - - "input used when default function returns nil": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return nil, nil - }, - Required: true, - }, - }, - - Input: map[string]string{ - "availability_zone": "bar", - }, - - Result: map[string]interface{}{ - "availability_zone": "bar", - }, - - Err: false, - }, - - "input not used when optional default function returns nil": { - Schema: map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - DefaultFunc: func() (interface{}, error) { - return nil, nil - }, - Optional: true, - }, - }, - - Input: map[string]string{}, - Result: map[string]interface{}{}, - Err: false, - }, - } - - for i, tc := range cases { - if tc.Config == nil { - tc.Config = make(map[string]interface{}) - } - - input := new(terraform.MockUIInput) - input.InputReturnMap = tc.Input - - rc := terraform.NewResourceConfigRaw(tc.Config) - rc.Config = make(map[string]interface{}) - - actual, err := schemaMap(tc.Schema).Input(input, rc) - if err != nil != tc.Err { - t.Fatalf("#%v err: %s", i, err) - } - - if !reflect.DeepEqual(tc.Result, actual.Config) { - t.Fatalf("#%v: bad:\n\ngot: %#v\nexpected: %#v", i, actual.Config, tc.Result) - } - } -} - -func TestSchemaMap_InputDefault(t *testing.T) { - emptyConfig := make(map[string]interface{}) - rc := terraform.NewResourceConfigRaw(emptyConfig) - rc.Config = make(map[string]interface{}) - - input := new(terraform.MockUIInput) - input.InputFn = func(opts *terraform.InputOpts) (string, error) { - t.Fatalf("InputFn should not be called on: %#v", opts) - return "", nil - } - - schema := map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Default: "foo", - Optional: true, - }, - } - actual, err := schemaMap(schema).Input(input, rc) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := map[string]interface{}{} - - if !reflect.DeepEqual(expected, actual.Config) { - t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected) - } -} - -func TestSchemaMap_InputDeprecated(t *testing.T) { - emptyConfig := make(map[string]interface{}) - rc := terraform.NewResourceConfigRaw(emptyConfig) - rc.Config = make(map[string]interface{}) - - input := new(terraform.MockUIInput) - input.InputFn = func(opts *terraform.InputOpts) (string, error) { - t.Fatalf("InputFn should not be called on: %#v", opts) - return "", nil - } - - schema := map[string]*Schema{ - "availability_zone": &Schema{ - Type: TypeString, - Deprecated: "long gone", - Optional: true, - }, - } - actual, err := schemaMap(schema).Input(input, rc) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := map[string]interface{}{} - - if !reflect.DeepEqual(expected, actual.Config) { - t.Fatalf("got: %#v\nexpected: %#v", actual.Config, expected) - } -} - func TestSchemaMap_InternalValidate(t *testing.T) { cases := map[string]struct { In map[string]*Schema diff --git a/internal/legacy/helper/schema/serialize.go b/internal/legacy/helper/schema/serialize.go index fe6d7504c7..c3ae6ba750 100644 --- a/internal/legacy/helper/schema/serialize.go +++ b/internal/legacy/helper/schema/serialize.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/serialize_test.go b/internal/legacy/helper/schema/serialize_test.go index 55afb1528f..44fbe52a81 100644 --- a/internal/legacy/helper/schema/serialize_test.go +++ b/internal/legacy/helper/schema/serialize_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/set.go b/internal/legacy/helper/schema/set.go index b44035c7c5..5e4d9d1ab0 100644 --- a/internal/legacy/helper/schema/set.go +++ b/internal/legacy/helper/schema/set.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/set_test.go b/internal/legacy/helper/schema/set_test.go index edeeb37a6e..b8b62c883d 100644 --- a/internal/legacy/helper/schema/set_test.go +++ b/internal/legacy/helper/schema/set_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/shims.go b/internal/legacy/helper/schema/shims.go index 3f9e2e9ffb..babfcdefab 100644 --- a/internal/legacy/helper/schema/shims.go +++ b/internal/legacy/helper/schema/shims.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/shims_test.go b/internal/legacy/helper/schema/shims_test.go index 91e24069dd..19e65ac0cc 100644 --- a/internal/legacy/helper/schema/shims_test.go +++ b/internal/legacy/helper/schema/shims_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( @@ -33,15 +36,15 @@ func testApplyDiff(t *testing.T, testSchema := providers.Schema{ Version: int64(resource.SchemaVersion), - Block: resourceSchemaToBlock(resource.Schema), + Body: resourceSchemaToBlock(resource.Schema), } - stateVal, err := StateValueFromInstanceState(state, testSchema.Block.ImpliedType()) + stateVal, err := StateValueFromInstanceState(state, testSchema.Body.ImpliedType()) if err != nil { t.Fatal(err) } - newState, err := ApplyDiff(stateVal, diff, testSchema.Block) + newState, err := ApplyDiff(stateVal, diff, testSchema.Body) if err != nil { t.Fatal(err) } @@ -69,13 +72,13 @@ func testApplyDiff(t *testing.T, // Resource.Meta will be hanlded separately, so it's OK that we lose the // timeout values here. - expectedState, err := StateValueFromInstanceState(expected, testSchema.Block.ImpliedType()) + expectedState, err := StateValueFromInstanceState(expected, testSchema.Body.ImpliedType()) if err != nil { t.Fatal(err) } if !cmp.Equal(expectedState, newState, equateEmpty, typeComparer, valueComparer) { - t.Fatalf(cmp.Diff(expectedState, newState, equateEmpty, typeComparer, valueComparer)) + t.Fatal(cmp.Diff(expectedState, newState, equateEmpty, typeComparer, valueComparer)) } } @@ -331,15 +334,15 @@ func TestShimResourceDiff_Timeout_diff(t *testing.T) { testSchema := providers.Schema{ Version: int64(r.SchemaVersion), - Block: resourceSchemaToBlock(r.Schema), + Body: resourceSchemaToBlock(r.Schema), } - initialVal, err := StateValueFromInstanceState(createdState, testSchema.Block.ImpliedType()) + initialVal, err := StateValueFromInstanceState(createdState, testSchema.Body.ImpliedType()) if err != nil { t.Fatal(err) } - appliedVal, err := StateValueFromInstanceState(applied, testSchema.Block.ImpliedType()) + appliedVal, err := StateValueFromInstanceState(applied, testSchema.Body.ImpliedType()) if err != nil { t.Fatal(err) } diff --git a/internal/legacy/helper/schema/testing.go b/internal/legacy/helper/schema/testing.go index 3b328a87c4..418b4b9296 100644 --- a/internal/legacy/helper/schema/testing.go +++ b/internal/legacy/helper/schema/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema import ( diff --git a/internal/legacy/helper/schema/valuetype.go b/internal/legacy/helper/schema/valuetype.go index 0f65d692f0..0741d96ed7 100644 --- a/internal/legacy/helper/schema/valuetype.go +++ b/internal/legacy/helper/schema/valuetype.go @@ -1,6 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package schema -//go:generate go run golang.org/x/tools/cmd/stringer -type=ValueType valuetype.go +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ValueType valuetype.go // ValueType is an enum of the type that can be represented by a schema. type ValueType int diff --git a/internal/legacy/terraform/context_components.go b/internal/legacy/terraform/context_components.go deleted file mode 100644 index 31494efb2f..0000000000 --- a/internal/legacy/terraform/context_components.go +++ /dev/null @@ -1,65 +0,0 @@ -package terraform - -import ( - "fmt" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/provisioners" -) - -// contextComponentFactory is the interface that Context uses -// to initialize various components such as providers and provisioners. -// This factory gets more information than the raw maps using to initialize -// a Context. This information is used for debugging. -type contextComponentFactory interface { - // ResourceProvider creates a new ResourceProvider with the given type. - ResourceProvider(typ addrs.Provider) (providers.Interface, error) - ResourceProviders() []string - - // ResourceProvisioner creates a new ResourceProvisioner with the given - // type. - ResourceProvisioner(typ string) (provisioners.Interface, error) - ResourceProvisioners() []string -} - -// basicComponentFactory just calls a factory from a map directly. -type basicComponentFactory struct { - providers map[addrs.Provider]providers.Factory - provisioners map[string]ProvisionerFactory -} - -func (c *basicComponentFactory) ResourceProviders() []string { - var result []string - for k := range c.providers { - result = append(result, k.String()) - } - return result -} - -func (c *basicComponentFactory) ResourceProvisioners() []string { - var result []string - for k := range c.provisioners { - result = append(result, k) - } - - return result -} - -func (c *basicComponentFactory) ResourceProvider(typ addrs.Provider) (providers.Interface, error) { - f, ok := c.providers[typ] - if !ok { - return nil, fmt.Errorf("unknown provider %q", typ.String()) - } - - return f() -} - -func (c *basicComponentFactory) ResourceProvisioner(typ string) (provisioners.Interface, error) { - f, ok := c.provisioners[typ] - if !ok { - return nil, fmt.Errorf("unknown provisioner %q", typ) - } - - return f() -} diff --git a/internal/legacy/terraform/diff.go b/internal/legacy/terraform/diff.go index 77bea32589..0c414001bd 100644 --- a/internal/legacy/terraform/diff.go +++ b/internal/legacy/terraform/diff.go @@ -1,8 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "bufio" - "bytes" "fmt" "log" "reflect" @@ -12,7 +13,6 @@ import ( "strings" "sync" - "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/zclconf/go-cty/cty" @@ -41,356 +41,6 @@ const ( // multiVal matches the index key to a flatmapped set, list or map var multiVal = regexp.MustCompile(`\.(#|%)$`) -// Diff tracks the changes that are necessary to apply a configuration -// to an existing infrastructure. -type Diff struct { - // Modules contains all the modules that have a diff - Modules []*ModuleDiff -} - -// Prune cleans out unused structures in the diff without affecting -// the behavior of the diff at all. -// -// This is not safe to call concurrently. This is safe to call on a -// nil Diff. -func (d *Diff) Prune() { - if d == nil { - return - } - - // Prune all empty modules - newModules := make([]*ModuleDiff, 0, len(d.Modules)) - for _, m := range d.Modules { - // If the module isn't empty, we keep it - if !m.Empty() { - newModules = append(newModules, m) - } - } - if len(newModules) == 0 { - newModules = nil - } - d.Modules = newModules -} - -// AddModule adds the module with the given path to the diff. -// -// This should be the preferred method to add module diffs since it -// allows us to optimize lookups later as well as control sorting. -func (d *Diff) AddModule(path addrs.ModuleInstance) *ModuleDiff { - // Lower the new-style address into a legacy-style address. - // This requires that none of the steps have instance keys, which is - // true for all addresses at the time of implementing this because - // "count" and "for_each" are not yet implemented for modules. - legacyPath := make([]string, len(path)) - for i, step := range path { - if step.InstanceKey != addrs.NoKey { - // FIXME: Once the rest of Terraform is ready to use count and - // for_each, remove all of this and just write the addrs.ModuleInstance - // value itself into the ModuleState. - panic("diff cannot represent modules with count or for_each keys") - } - - legacyPath[i] = step.Name - } - - m := &ModuleDiff{Path: legacyPath} - m.init() - d.Modules = append(d.Modules, m) - return m -} - -// ModuleByPath is used to lookup the module diff for the given path. -// This should be the preferred lookup mechanism as it allows for future -// lookup optimizations. -func (d *Diff) ModuleByPath(path addrs.ModuleInstance) *ModuleDiff { - if d == nil { - return nil - } - for _, mod := range d.Modules { - if mod.Path == nil { - panic("missing module path") - } - modPath := normalizeModulePath(mod.Path) - if modPath.String() == path.String() { - return mod - } - } - return nil -} - -// RootModule returns the ModuleState for the root module -func (d *Diff) RootModule() *ModuleDiff { - root := d.ModuleByPath(addrs.RootModuleInstance) - if root == nil { - panic("missing root module") - } - return root -} - -// Empty returns true if the diff has no changes. -func (d *Diff) Empty() bool { - if d == nil { - return true - } - - for _, m := range d.Modules { - if !m.Empty() { - return false - } - } - - return true -} - -// Equal compares two diffs for exact equality. -// -// This is different from the Same comparison that is supported which -// checks for operation equality taking into account computed values. Equal -// instead checks for exact equality. -func (d *Diff) Equal(d2 *Diff) bool { - // If one is nil, they must both be nil - if d == nil || d2 == nil { - return d == d2 - } - - // Sort the modules - sort.Sort(moduleDiffSort(d.Modules)) - sort.Sort(moduleDiffSort(d2.Modules)) - - // Copy since we have to modify the module destroy flag to false so - // we don't compare that. TODO: delete this when we get rid of the - // destroy flag on modules. - dCopy := d.DeepCopy() - d2Copy := d2.DeepCopy() - for _, m := range dCopy.Modules { - m.Destroy = false - } - for _, m := range d2Copy.Modules { - m.Destroy = false - } - - // Use DeepEqual - return reflect.DeepEqual(dCopy, d2Copy) -} - -// DeepCopy performs a deep copy of all parts of the Diff, making the -// resulting Diff safe to use without modifying this one. -func (d *Diff) DeepCopy() *Diff { - copy, err := copystructure.Config{Lock: true}.Copy(d) - if err != nil { - panic(err) - } - - return copy.(*Diff) -} - -func (d *Diff) String() string { - var buf bytes.Buffer - - keys := make([]string, 0, len(d.Modules)) - lookup := make(map[string]*ModuleDiff) - for _, m := range d.Modules { - addr := normalizeModulePath(m.Path) - key := addr.String() - keys = append(keys, key) - lookup[key] = m - } - sort.Strings(keys) - - for _, key := range keys { - m := lookup[key] - mStr := m.String() - - // If we're the root module, we just write the output directly. - if reflect.DeepEqual(m.Path, rootModulePath) { - buf.WriteString(mStr + "\n") - continue - } - - buf.WriteString(fmt.Sprintf("%s:\n", key)) - - s := bufio.NewScanner(strings.NewReader(mStr)) - for s.Scan() { - buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) - } - } - - return strings.TrimSpace(buf.String()) -} - -func (d *Diff) init() { - if d.Modules == nil { - rootDiff := &ModuleDiff{Path: rootModulePath} - d.Modules = []*ModuleDiff{rootDiff} - } - for _, m := range d.Modules { - m.init() - } -} - -// ModuleDiff tracks the differences between resources to apply within -// a single module. -type ModuleDiff struct { - Path []string - Resources map[string]*InstanceDiff - Destroy bool // Set only by the destroy plan -} - -func (d *ModuleDiff) init() { - if d.Resources == nil { - d.Resources = make(map[string]*InstanceDiff) - } - for _, r := range d.Resources { - r.init() - } -} - -// ChangeType returns the type of changes that the diff for this -// module includes. -// -// At a module level, this will only be DiffNone, DiffUpdate, DiffDestroy, or -// DiffCreate. If an instance within the module has a DiffDestroyCreate -// then this will register as a DiffCreate for a module. -func (d *ModuleDiff) ChangeType() DiffChangeType { - result := DiffNone - for _, r := range d.Resources { - change := r.ChangeType() - switch change { - case DiffCreate, DiffDestroy: - if result == DiffNone { - result = change - } - case DiffDestroyCreate, DiffUpdate: - result = DiffUpdate - } - } - - return result -} - -// Empty returns true if the diff has no changes within this module. -func (d *ModuleDiff) Empty() bool { - if d.Destroy { - return false - } - - if len(d.Resources) == 0 { - return true - } - - for _, rd := range d.Resources { - if !rd.Empty() { - return false - } - } - - return true -} - -// Instances returns the instance diffs for the id given. This can return -// multiple instance diffs if there are counts within the resource. -func (d *ModuleDiff) Instances(id string) []*InstanceDiff { - var result []*InstanceDiff - for k, diff := range d.Resources { - if k == id || strings.HasPrefix(k, id+".") { - if !diff.Empty() { - result = append(result, diff) - } - } - } - - return result -} - -// IsRoot says whether or not this module diff is for the root module. -func (d *ModuleDiff) IsRoot() bool { - return reflect.DeepEqual(d.Path, rootModulePath) -} - -// String outputs the diff in a long but command-line friendly output -// format that users can read to quickly inspect a diff. -func (d *ModuleDiff) String() string { - var buf bytes.Buffer - - names := make([]string, 0, len(d.Resources)) - for name, _ := range d.Resources { - names = append(names, name) - } - sort.Strings(names) - - for _, name := range names { - rdiff := d.Resources[name] - - crud := "UPDATE" - switch { - case rdiff.RequiresNew() && (rdiff.GetDestroy() || rdiff.GetDestroyTainted()): - crud = "DESTROY/CREATE" - case rdiff.GetDestroy() || rdiff.GetDestroyDeposed(): - crud = "DESTROY" - case rdiff.RequiresNew(): - crud = "CREATE" - } - - extra := "" - if !rdiff.GetDestroy() && rdiff.GetDestroyDeposed() { - extra = " (deposed only)" - } - - buf.WriteString(fmt.Sprintf( - "%s: %s%s\n", - crud, - name, - extra)) - - keyLen := 0 - rdiffAttrs := rdiff.CopyAttributes() - keys := make([]string, 0, len(rdiffAttrs)) - for key, _ := range rdiffAttrs { - if key == "id" { - continue - } - - keys = append(keys, key) - if len(key) > keyLen { - keyLen = len(key) - } - } - sort.Strings(keys) - - for _, attrK := range keys { - attrDiff, _ := rdiff.GetAttribute(attrK) - - v := attrDiff.New - u := attrDiff.Old - if attrDiff.NewComputed { - v = "" - } - - if attrDiff.Sensitive { - u = "" - v = "" - } - - updateMsg := "" - if attrDiff.RequiresNew { - updateMsg = " (forces new resource)" - } else if attrDiff.Sensitive { - updateMsg = " (attribute changed)" - } - - buf.WriteString(fmt.Sprintf( - " %s:%s %#v => %#v%s\n", - attrK, - strings.Repeat(" ", keyLen-len(attrK)), - u, - v, - updateMsg)) - } - } - - return buf.String() -} - // InstanceDiff is the diff of a resource from some state to another. type InstanceDiff struct { mu sync.Mutex @@ -1431,21 +1081,3 @@ func (d *InstanceDiff) Same(d2 *InstanceDiff) (bool, string) { return true, "" } - -// moduleDiffSort implements sort.Interface to sort module diffs by path. -type moduleDiffSort []*ModuleDiff - -func (s moduleDiffSort) Len() int { return len(s) } -func (s moduleDiffSort) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s moduleDiffSort) Less(i, j int) bool { - a := s[i] - b := s[j] - - // If the lengths are different, then the shorter one always wins - if len(a.Path) != len(b.Path) { - return len(a.Path) < len(b.Path) - } - - // Otherwise, compare lexically - return strings.Join(a.Path, ".") < strings.Join(b.Path, ".") -} diff --git a/internal/legacy/terraform/diff_test.go b/internal/legacy/terraform/diff_test.go index 5388eb44eb..3748ef1845 100644 --- a/internal/legacy/terraform/diff_test.go +++ b/internal/legacy/terraform/diff_test.go @@ -1,335 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" - "reflect" "strconv" - "strings" "testing" - - "github.com/hashicorp/terraform/internal/addrs" ) -func TestDiffEmpty(t *testing.T) { - var diff *Diff - if !diff.Empty() { - t.Fatal("should be empty") - } - - diff = new(Diff) - if !diff.Empty() { - t.Fatal("should be empty") - } - - mod := diff.AddModule(addrs.RootModuleInstance) - mod.Resources["nodeA"] = &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - }, - } - - if diff.Empty() { - t.Fatal("should not be empty") - } -} - -func TestDiffEmpty_taintedIsNotEmpty(t *testing.T) { - diff := new(Diff) - - mod := diff.AddModule(addrs.RootModuleInstance) - mod.Resources["nodeA"] = &InstanceDiff{ - DestroyTainted: true, - } - - if diff.Empty() { - t.Fatal("should not be empty, since DestroyTainted was set") - } -} - -func TestDiffEqual(t *testing.T) { - cases := map[string]struct { - D1, D2 *Diff - Equal bool - }{ - "nil": { - nil, - new(Diff), - false, - }, - - "empty": { - new(Diff), - new(Diff), - true, - }, - - "different module order": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}}, - &ModuleDiff{Path: []string{"root", "bar"}}, - }, - }, - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "bar"}}, - &ModuleDiff{Path: []string{"root", "foo"}}, - }, - }, - true, - }, - - "different module diff destroys": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, - }, - }, - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: false}, - }, - }, - true, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - actual := tc.D1.Equal(tc.D2) - if actual != tc.Equal { - t.Fatalf("expected: %v\n\n%#v\n\n%#v", tc.Equal, tc.D1, tc.D2) - } - }) - } -} - -func TestDiffPrune(t *testing.T) { - cases := map[string]struct { - D1, D2 *Diff - }{ - "nil": { - nil, - nil, - }, - - "empty": { - new(Diff), - new(Diff), - }, - - "empty module": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}}, - }, - }, - &Diff{}, - }, - - "destroy module": { - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, - }, - }, - &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{Path: []string{"root", "foo"}, Destroy: true}, - }, - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - tc.D1.Prune() - if !tc.D1.Equal(tc.D2) { - t.Fatalf("bad:\n\n%#v\n\n%#v", tc.D1, tc.D2) - } - }) - } -} - -func TestModuleDiff_ChangeType(t *testing.T) { - cases := []struct { - Diff *ModuleDiff - Result DiffChangeType - }{ - { - &ModuleDiff{}, - DiffNone, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{Destroy: true}, - }, - }, - DiffDestroy, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - }, - }, - }, - }, - }, - DiffUpdate, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - }, - }, - DiffCreate, - }, - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": &InstanceDiff{ - Destroy: true, - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "bar", - RequiresNew: true, - }, - }, - }, - }, - }, - DiffUpdate, - }, - } - - for i, tc := range cases { - actual := tc.Diff.ChangeType() - if actual != tc.Result { - t.Fatalf("%d: %#v", i, actual) - } - } -} - -func TestDiff_DeepCopy(t *testing.T) { - cases := map[string]*Diff{ - "empty": &Diff{}, - - "basic diff": &Diff{ - Modules: []*ModuleDiff{ - &ModuleDiff{ - Path: []string{"root"}, - Resources: map[string]*InstanceDiff{ - "aws_instance.foo": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "num": &ResourceAttrDiff{ - Old: "0", - New: "2", - }, - }, - }, - }, - }, - }, - }, - } - - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - dup := tc.DeepCopy() - if !reflect.DeepEqual(dup, tc) { - t.Fatalf("\n%#v\n\n%#v", dup, tc) - } - }) - } -} - -func TestModuleDiff_Empty(t *testing.T) { - diff := new(ModuleDiff) - if !diff.Empty() { - t.Fatal("should be empty") - } - - diff.Resources = map[string]*InstanceDiff{ - "nodeA": &InstanceDiff{}, - } - - if !diff.Empty() { - t.Fatal("should be empty") - } - - diff.Resources["nodeA"].Attributes = map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - } - - if diff.Empty() { - t.Fatal("should not be empty") - } - - diff.Resources["nodeA"].Attributes = nil - diff.Resources["nodeA"].Destroy = true - - if diff.Empty() { - t.Fatal("should not be empty") - } -} - -func TestModuleDiff_String(t *testing.T) { - diff := &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "nodeA": &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - }, - "bar": &ResourceAttrDiff{ - Old: "foo", - NewComputed: true, - }, - "longfoo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - RequiresNew: true, - }, - "secretfoo": &ResourceAttrDiff{ - Old: "foo", - New: "bar", - Sensitive: true, - }, - }, - }, - }, - } - - actual := strings.TrimSpace(diff.String()) - expected := strings.TrimSpace(moduleDiffStrBasic) - if actual != expected { - t.Fatalf("bad:\n%s", actual) - } -} - func TestInstanceDiff_ChangeType(t *testing.T) { cases := []struct { Diff *InstanceDiff @@ -434,68 +113,6 @@ func TestInstanceDiff_Empty(t *testing.T) { } } -func TestModuleDiff_Instances(t *testing.T) { - yesDiff := &InstanceDiff{Destroy: true} - noDiff := &InstanceDiff{Destroy: true, DestroyTainted: true} - - cases := []struct { - Diff *ModuleDiff - Id string - Result []*InstanceDiff - }{ - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": yesDiff, - "bar": noDiff, - }, - }, - "foo", - []*InstanceDiff{ - yesDiff, - }, - }, - - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": yesDiff, - "foo.0": yesDiff, - "bar": noDiff, - }, - }, - "foo", - []*InstanceDiff{ - yesDiff, - yesDiff, - }, - }, - - { - &ModuleDiff{ - Resources: map[string]*InstanceDiff{ - "foo": yesDiff, - "foo.0": yesDiff, - "foo_bar": noDiff, - "bar": noDiff, - }, - }, - "foo", - []*InstanceDiff{ - yesDiff, - yesDiff, - }, - }, - } - - for i, tc := range cases { - actual := tc.Diff.Instances(tc.Id) - if !reflect.DeepEqual(actual, tc.Result) { - t.Fatalf("%d: %#v", i, actual) - } - } -} - func TestInstanceDiff_RequiresNew(t *testing.T) { rd := &InstanceDiff{ Attributes: map[string]*ResourceAttrDiff{ @@ -1207,14 +824,6 @@ func TestInstanceDiffSame(t *testing.T) { } } -const moduleDiffStrBasic = ` -CREATE: nodeA - bar: "foo" => "" - foo: "foo" => "bar" - longfoo: "foo" => "bar" (forces new resource) - secretfoo: "" => "" (attribute changed) -` - func TestCountFlatmapContainerValues(t *testing.T) { for i, tc := range []struct { attrs map[string]string diff --git a/internal/legacy/terraform/features.go b/internal/legacy/terraform/features.go deleted file mode 100644 index 97c77bdbd0..0000000000 --- a/internal/legacy/terraform/features.go +++ /dev/null @@ -1,7 +0,0 @@ -package terraform - -import "os" - -// This file holds feature flags for the next release - -var flagWarnOutputErrors = os.Getenv("TF_WARN_OUTPUT_ERRORS") != "" diff --git a/internal/legacy/terraform/instancetype.go b/internal/legacy/terraform/instancetype.go deleted file mode 100644 index 375a8638a8..0000000000 --- a/internal/legacy/terraform/instancetype.go +++ /dev/null @@ -1,13 +0,0 @@ -package terraform - -//go:generate go run golang.org/x/tools/cmd/stringer -type=InstanceType instancetype.go - -// InstanceType is an enum of the various types of instances store in the State -type InstanceType int - -const ( - TypeInvalid InstanceType = iota - TypePrimary - TypeTainted - TypeDeposed -) diff --git a/internal/legacy/terraform/instancetype_string.go b/internal/legacy/terraform/instancetype_string.go deleted file mode 100644 index 95b7a9802e..0000000000 --- a/internal/legacy/terraform/instancetype_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=InstanceType instancetype.go"; DO NOT EDIT. - -package terraform - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[TypeInvalid-0] - _ = x[TypePrimary-1] - _ = x[TypeTainted-2] - _ = x[TypeDeposed-3] -} - -const _InstanceType_name = "TypeInvalidTypePrimaryTypeTaintedTypeDeposed" - -var _InstanceType_index = [...]uint8{0, 11, 22, 33, 44} - -func (i InstanceType) String() string { - if i < 0 || i >= InstanceType(len(_InstanceType_index)-1) { - return "InstanceType(" + strconv.FormatInt(int64(i), 10) + ")" - } - return _InstanceType_name[_InstanceType_index[i]:_InstanceType_index[i+1]] -} diff --git a/internal/legacy/terraform/provider_mock.go b/internal/legacy/terraform/provider_mock.go deleted file mode 100644 index abdba43246..0000000000 --- a/internal/legacy/terraform/provider_mock.go +++ /dev/null @@ -1,363 +0,0 @@ -package terraform - -import ( - "encoding/json" - "sync" - - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" - - "github.com/hashicorp/terraform/internal/configs/hcl2shim" - "github.com/hashicorp/terraform/internal/providers" -) - -var _ providers.Interface = (*MockProvider)(nil) - -// MockProvider implements providers.Interface but mocks out all the -// calls for testing purposes. -type MockProvider struct { - sync.Mutex - - // Anything you want, in case you need to store extra data with the mock. - Meta interface{} - - GetSchemaCalled bool - GetSchemaReturn *ProviderSchema // This is using ProviderSchema directly rather than providers.GetProviderSchemaResponse for compatibility with old tests - - ValidateProviderConfigCalled bool - ValidateProviderConfigResponse providers.ValidateProviderConfigResponse - ValidateProviderConfigRequest providers.ValidateProviderConfigRequest - ValidateProviderConfigFn func(providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse - - ValidateResourceConfigCalled bool - ValidateResourceConfigTypeName string - ValidateResourceConfigResponse providers.ValidateResourceConfigResponse - ValidateResourceConfigRequest providers.ValidateResourceConfigRequest - ValidateResourceConfigFn func(providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse - - ValidateDataResourceConfigCalled bool - ValidateDataResourceConfigTypeName string - ValidateDataResourceConfigResponse providers.ValidateDataResourceConfigResponse - ValidateDataResourceConfigRequest providers.ValidateDataResourceConfigRequest - ValidateDataResourceConfigFn func(providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse - - UpgradeResourceStateCalled bool - UpgradeResourceStateTypeName string - UpgradeResourceStateResponse providers.UpgradeResourceStateResponse - UpgradeResourceStateRequest providers.UpgradeResourceStateRequest - UpgradeResourceStateFn func(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse - - ConfigureProviderCalled bool - ConfigureProviderResponse providers.ConfigureProviderResponse - ConfigureProviderRequest providers.ConfigureProviderRequest - ConfigureProviderFn func(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse - - StopCalled bool - StopFn func() error - StopResponse error - - ReadResourceCalled bool - ReadResourceResponse providers.ReadResourceResponse - ReadResourceRequest providers.ReadResourceRequest - ReadResourceFn func(providers.ReadResourceRequest) providers.ReadResourceResponse - - PlanResourceChangeCalled bool - PlanResourceChangeResponse providers.PlanResourceChangeResponse - PlanResourceChangeRequest providers.PlanResourceChangeRequest - PlanResourceChangeFn func(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse - - ApplyResourceChangeCalled bool - ApplyResourceChangeResponse providers.ApplyResourceChangeResponse - ApplyResourceChangeRequest providers.ApplyResourceChangeRequest - ApplyResourceChangeFn func(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse - - ImportResourceStateCalled bool - ImportResourceStateResponse providers.ImportResourceStateResponse - ImportResourceStateRequest providers.ImportResourceStateRequest - ImportResourceStateFn func(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse - // Legacy return type for existing tests, which will be shimmed into an - // ImportResourceStateResponse if set - ImportStateReturn []*InstanceState - - ReadDataSourceCalled bool - ReadDataSourceResponse providers.ReadDataSourceResponse - ReadDataSourceRequest providers.ReadDataSourceRequest - ReadDataSourceFn func(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse - - CloseCalled bool - CloseError error -} - -func (p *MockProvider) GetProviderSchema() providers.GetProviderSchemaResponse { - p.Lock() - defer p.Unlock() - p.GetSchemaCalled = true - return p.getSchema() -} - -func (p *MockProvider) getSchema() providers.GetProviderSchemaResponse { - // This version of getSchema doesn't do any locking, so it's suitable to - // call from other methods of this mock as long as they are already - // holding the lock. - - ret := providers.GetProviderSchemaResponse{ - Provider: providers.Schema{}, - DataSources: map[string]providers.Schema{}, - ResourceTypes: map[string]providers.Schema{}, - } - if p.GetSchemaReturn != nil { - ret.Provider.Block = p.GetSchemaReturn.Provider - ret.ProviderMeta.Block = p.GetSchemaReturn.ProviderMeta - for n, s := range p.GetSchemaReturn.DataSources { - ret.DataSources[n] = providers.Schema{ - Block: s, - } - } - for n, s := range p.GetSchemaReturn.ResourceTypes { - ret.ResourceTypes[n] = providers.Schema{ - Version: int64(p.GetSchemaReturn.ResourceTypeSchemaVersions[n]), - Block: s, - } - } - } - - return ret -} - -func (p *MockProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { - p.Lock() - defer p.Unlock() - - p.ValidateProviderConfigCalled = true - p.ValidateProviderConfigRequest = r - if p.ValidateProviderConfigFn != nil { - return p.ValidateProviderConfigFn(r) - } - return p.ValidateProviderConfigResponse -} - -func (p *MockProvider) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { - p.Lock() - defer p.Unlock() - - p.ValidateResourceConfigCalled = true - p.ValidateResourceConfigRequest = r - - if p.ValidateResourceConfigFn != nil { - return p.ValidateResourceConfigFn(r) - } - - return p.ValidateResourceConfigResponse -} - -func (p *MockProvider) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { - p.Lock() - defer p.Unlock() - - p.ValidateDataResourceConfigCalled = true - p.ValidateDataResourceConfigRequest = r - - if p.ValidateDataResourceConfigFn != nil { - return p.ValidateDataResourceConfigFn(r) - } - - return p.ValidateDataResourceConfigResponse -} - -func (p *MockProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { - p.Lock() - defer p.Unlock() - - schemas := p.getSchema() - schema := schemas.ResourceTypes[r.TypeName] - schemaType := schema.Block.ImpliedType() - - p.UpgradeResourceStateCalled = true - p.UpgradeResourceStateRequest = r - - if p.UpgradeResourceStateFn != nil { - return p.UpgradeResourceStateFn(r) - } - - resp := p.UpgradeResourceStateResponse - - if resp.UpgradedState == cty.NilVal { - switch { - case r.RawStateFlatmap != nil: - v, err := hcl2shim.HCL2ValueFromFlatmap(r.RawStateFlatmap, schemaType) - if err != nil { - resp.Diagnostics = resp.Diagnostics.Append(err) - return resp - } - resp.UpgradedState = v - case len(r.RawStateJSON) > 0: - v, err := ctyjson.Unmarshal(r.RawStateJSON, schemaType) - - if err != nil { - resp.Diagnostics = resp.Diagnostics.Append(err) - return resp - } - resp.UpgradedState = v - } - } - return resp -} - -func (p *MockProvider) ConfigureProvider(r providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { - p.Lock() - defer p.Unlock() - - p.ConfigureProviderCalled = true - p.ConfigureProviderRequest = r - - if p.ConfigureProviderFn != nil { - return p.ConfigureProviderFn(r) - } - - return p.ConfigureProviderResponse -} - -func (p *MockProvider) Stop() error { - // We intentionally don't lock in this one because the whole point of this - // method is to be called concurrently with another operation that can - // be cancelled. The provider itself is responsible for handling - // any concurrency concerns in this case. - - p.StopCalled = true - if p.StopFn != nil { - return p.StopFn() - } - - return p.StopResponse -} - -func (p *MockProvider) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse { - p.Lock() - defer p.Unlock() - - p.ReadResourceCalled = true - p.ReadResourceRequest = r - - if p.ReadResourceFn != nil { - return p.ReadResourceFn(r) - } - - resp := p.ReadResourceResponse - if resp.NewState != cty.NilVal { - // make sure the NewState fits the schema - // This isn't always the case for the existing tests - newState, err := p.GetSchemaReturn.ResourceTypes[r.TypeName].CoerceValue(resp.NewState) - if err != nil { - panic(err) - } - resp.NewState = newState - return resp - } - - // just return the same state we received - resp.NewState = r.PriorState - return resp -} - -func (p *MockProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { - p.Lock() - defer p.Unlock() - - p.PlanResourceChangeCalled = true - p.PlanResourceChangeRequest = r - - if p.PlanResourceChangeFn != nil { - return p.PlanResourceChangeFn(r) - } - - return p.PlanResourceChangeResponse -} - -func (p *MockProvider) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { - p.Lock() - p.ApplyResourceChangeCalled = true - p.ApplyResourceChangeRequest = r - p.Unlock() - - if p.ApplyResourceChangeFn != nil { - return p.ApplyResourceChangeFn(r) - } - - return p.ApplyResourceChangeResponse -} - -func (p *MockProvider) ImportResourceState(r providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { - p.Lock() - defer p.Unlock() - - if p.ImportStateReturn != nil { - for _, is := range p.ImportStateReturn { - if is.Attributes == nil { - is.Attributes = make(map[string]string) - } - is.Attributes["id"] = is.ID - - typeName := is.Ephemeral.Type - // Use the requested type if the resource has no type of it's own. - // We still return the empty type, which will error, but this prevents a panic. - if typeName == "" { - typeName = r.TypeName - } - - schema := p.GetSchemaReturn.ResourceTypes[typeName] - if schema == nil { - panic("no schema found for " + typeName) - } - - private, err := json.Marshal(is.Meta) - if err != nil { - panic(err) - } - - state, err := hcl2shim.HCL2ValueFromFlatmap(is.Attributes, schema.ImpliedType()) - if err != nil { - panic(err) - } - - state, err = schema.CoerceValue(state) - if err != nil { - panic(err) - } - - p.ImportResourceStateResponse.ImportedResources = append( - p.ImportResourceStateResponse.ImportedResources, - providers.ImportedResource{ - TypeName: is.Ephemeral.Type, - State: state, - Private: private, - }) - } - } - - p.ImportResourceStateCalled = true - p.ImportResourceStateRequest = r - if p.ImportResourceStateFn != nil { - return p.ImportResourceStateFn(r) - } - - return p.ImportResourceStateResponse -} - -func (p *MockProvider) ReadDataSource(r providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { - p.Lock() - defer p.Unlock() - - p.ReadDataSourceCalled = true - p.ReadDataSourceRequest = r - - if p.ReadDataSourceFn != nil { - return p.ReadDataSourceFn(r) - } - - return p.ReadDataSourceResponse -} - -func (p *MockProvider) Close() error { - p.CloseCalled = true - return p.CloseError -} diff --git a/internal/legacy/terraform/provisioner_mock.go b/internal/legacy/terraform/provisioner_mock.go deleted file mode 100644 index fe76157a2d..0000000000 --- a/internal/legacy/terraform/provisioner_mock.go +++ /dev/null @@ -1,104 +0,0 @@ -package terraform - -import ( - "sync" - - "github.com/hashicorp/terraform/internal/provisioners" -) - -var _ provisioners.Interface = (*MockProvisioner)(nil) - -// MockProvisioner implements provisioners.Interface but mocks out all the -// calls for testing purposes. -type MockProvisioner struct { - sync.Mutex - // Anything you want, in case you need to store extra data with the mock. - Meta interface{} - - GetSchemaCalled bool - GetSchemaResponse provisioners.GetSchemaResponse - - ValidateProvisionerConfigCalled bool - ValidateProvisionerConfigRequest provisioners.ValidateProvisionerConfigRequest - ValidateProvisionerConfigResponse provisioners.ValidateProvisionerConfigResponse - ValidateProvisionerConfigFn func(provisioners.ValidateProvisionerConfigRequest) provisioners.ValidateProvisionerConfigResponse - - ProvisionResourceCalled bool - ProvisionResourceRequest provisioners.ProvisionResourceRequest - ProvisionResourceResponse provisioners.ProvisionResourceResponse - ProvisionResourceFn func(provisioners.ProvisionResourceRequest) provisioners.ProvisionResourceResponse - - StopCalled bool - StopResponse error - StopFn func() error - - CloseCalled bool - CloseResponse error - CloseFn func() error -} - -func (p *MockProvisioner) GetSchema() provisioners.GetSchemaResponse { - p.Lock() - defer p.Unlock() - - p.GetSchemaCalled = true - return p.getSchema() -} - -// getSchema is the implementation of GetSchema, which can be called from other -// methods on MockProvisioner that may already be holding the lock. -func (p *MockProvisioner) getSchema() provisioners.GetSchemaResponse { - return p.GetSchemaResponse -} - -func (p *MockProvisioner) ValidateProvisionerConfig(r provisioners.ValidateProvisionerConfigRequest) provisioners.ValidateProvisionerConfigResponse { - p.Lock() - defer p.Unlock() - - p.ValidateProvisionerConfigCalled = true - p.ValidateProvisionerConfigRequest = r - if p.ValidateProvisionerConfigFn != nil { - return p.ValidateProvisionerConfigFn(r) - } - return p.ValidateProvisionerConfigResponse -} - -func (p *MockProvisioner) ProvisionResource(r provisioners.ProvisionResourceRequest) provisioners.ProvisionResourceResponse { - p.Lock() - defer p.Unlock() - - p.ProvisionResourceCalled = true - p.ProvisionResourceRequest = r - if p.ProvisionResourceFn != nil { - fn := p.ProvisionResourceFn - return fn(r) - } - - return p.ProvisionResourceResponse -} - -func (p *MockProvisioner) Stop() error { - // We intentionally don't lock in this one because the whole point of this - // method is to be called concurrently with another operation that can - // be cancelled. The provisioner itself is responsible for handling - // any concurrency concerns in this case. - - p.StopCalled = true - if p.StopFn != nil { - return p.StopFn() - } - - return p.StopResponse -} - -func (p *MockProvisioner) Close() error { - p.Lock() - defer p.Unlock() - - p.CloseCalled = true - if p.CloseFn != nil { - return p.CloseFn() - } - - return p.CloseResponse -} diff --git a/internal/legacy/terraform/resource.go b/internal/legacy/terraform/resource.go index ddec8f828a..1a3d690375 100644 --- a/internal/legacy/terraform/resource.go +++ b/internal/legacy/terraform/resource.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -16,57 +19,6 @@ import ( "github.com/hashicorp/terraform/internal/configs/hcl2shim" ) -// Resource is a legacy way to identify a particular resource instance. -// -// New code should use addrs.ResourceInstance instead. This is still here -// only for codepaths that haven't been updated yet. -type Resource struct { - // These are all used by the new EvalNode stuff. - Name string - Type string - CountIndex int - - // These aren't really used anymore anywhere, but we keep them around - // since we haven't done a proper cleanup yet. - Id string - Info *InstanceInfo - Config *ResourceConfig - Dependencies []string - Diff *InstanceDiff - Provider ResourceProvider - State *InstanceState - Flags ResourceFlag -} - -// NewResource constructs a legacy Resource object from an -// addrs.ResourceInstance value. -// -// This is provided to shim to old codepaths that haven't been updated away -// from this type yet. Since this old type is not able to represent instances -// that have string keys, this function will panic if given a resource address -// that has a string key. -func NewResource(addr addrs.ResourceInstance) *Resource { - ret := &Resource{ - Name: addr.Resource.Name, - Type: addr.Resource.Type, - } - - if addr.Key != addrs.NoKey { - switch tk := addr.Key.(type) { - case addrs.IntKey: - ret.CountIndex = int(tk) - default: - panic(fmt.Errorf("resource instance with key %#v is not supported", addr.Key)) - } - } - - return ret -} - -// ResourceKind specifies what kind of instance we're working with, whether -// its a primary instance, a tainted instance, or an orphan. -type ResourceFlag byte - // InstanceInfo is used to hold information about the instance and/or // resource being modified. type InstanceInfo struct { @@ -137,46 +89,6 @@ func NewInstanceInfo(addr addrs.AbsResourceInstance) *InstanceInfo { } } -// ResourceAddress returns the address of the resource that the receiver is describing. -func (i *InstanceInfo) ResourceAddress() *ResourceAddress { - // GROSS: for tainted and deposed instances, their status gets appended - // to i.Id to create a unique id for the graph node. Historically these - // ids were displayed to the user, so it's designed to be human-readable: - // "aws_instance.bar.0 (deposed #0)" - // - // So here we detect such suffixes and try to interpret them back to - // their original meaning so we can then produce a ResourceAddress - // with a suitable InstanceType. - id := i.Id - instanceType := TypeInvalid - if idx := strings.Index(id, " ("); idx != -1 { - remain := id[idx:] - id = id[:idx] - - switch { - case strings.Contains(remain, "tainted"): - instanceType = TypeTainted - case strings.Contains(remain, "deposed"): - instanceType = TypeDeposed - } - } - - addr, err := parseResourceAddressInternal(id) - if err != nil { - // should never happen, since that would indicate a bug in the - // code that constructed this InstanceInfo. - panic(fmt.Errorf("InstanceInfo has invalid Id %s", id)) - } - if len(i.ModulePath) > 1 { - addr.Path = i.ModulePath[1:] // trim off "root" prefix, which is implied - } - if instanceType != TypeInvalid { - addr.InstanceTypeSet = true - addr.InstanceType = instanceType - } - return addr -} - // ResourceConfig is a legacy type that was formerly used to represent // interpolatable configuration blocks. It is now only used to shim to old // APIs that still use this type, via NewResourceConfigShimmed. diff --git a/internal/legacy/terraform/resource_address.go b/internal/legacy/terraform/resource_address.go deleted file mode 100644 index 9ab24f9db5..0000000000 --- a/internal/legacy/terraform/resource_address.go +++ /dev/null @@ -1,620 +0,0 @@ -package terraform - -import ( - "fmt" - "reflect" - "regexp" - "strconv" - "strings" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" -) - -// ResourceAddress is a way of identifying an individual resource (or, -// eventually, a subset of resources) within the state. It is used for Targets. -type ResourceAddress struct { - // Addresses a resource falling somewhere in the module path - // When specified alone, addresses all resources within a module path - Path []string - - // Addresses a specific resource that occurs in a list - Index int - - InstanceType InstanceType - InstanceTypeSet bool - Name string - Type string - Mode ResourceMode // significant only if InstanceTypeSet -} - -// Copy returns a copy of this ResourceAddress -func (r *ResourceAddress) Copy() *ResourceAddress { - if r == nil { - return nil - } - - n := &ResourceAddress{ - Path: make([]string, 0, len(r.Path)), - Index: r.Index, - InstanceType: r.InstanceType, - Name: r.Name, - Type: r.Type, - Mode: r.Mode, - } - - n.Path = append(n.Path, r.Path...) - - return n -} - -// String outputs the address that parses into this address. -func (r *ResourceAddress) String() string { - var result []string - for _, p := range r.Path { - result = append(result, "module", p) - } - - switch r.Mode { - case ManagedResourceMode: - // nothing to do - case DataResourceMode: - result = append(result, "data") - default: - panic(fmt.Errorf("unsupported resource mode %s", r.Mode)) - } - - if r.Type != "" { - result = append(result, r.Type) - } - - if r.Name != "" { - name := r.Name - if r.InstanceTypeSet { - switch r.InstanceType { - case TypePrimary: - name += ".primary" - case TypeDeposed: - name += ".deposed" - case TypeTainted: - name += ".tainted" - } - } - - if r.Index >= 0 { - name += fmt.Sprintf("[%d]", r.Index) - } - result = append(result, name) - } - - return strings.Join(result, ".") -} - -// HasResourceSpec returns true if the address has a resource spec, as -// defined in the documentation: -// -// https://www.terraform.io/docs/cli/state/resource-addressing.html -// -// In particular, this returns false if the address contains only -// a module path, thus addressing the entire module. -func (r *ResourceAddress) HasResourceSpec() bool { - return r.Type != "" && r.Name != "" -} - -// WholeModuleAddress returns the resource address that refers to all -// resources in the same module as the receiver address. -func (r *ResourceAddress) WholeModuleAddress() *ResourceAddress { - return &ResourceAddress{ - Path: r.Path, - Index: -1, - InstanceTypeSet: false, - } -} - -// MatchesResourceConfig returns true if the receiver matches the given -// configuration resource within the given _static_ module path. Note that -// the module path in a resource address is a _dynamic_ module path, and -// multiple dynamic resource paths may map to a single static path if -// count and for_each are in use on module calls. -// -// Since resource configuration blocks represent all of the instances of -// a multi-instance resource, the index of the address (if any) is not -// considered. -func (r *ResourceAddress) MatchesResourceConfig(path addrs.Module, rc *configs.Resource) bool { - if r.HasResourceSpec() { - // FIXME: Some ugliness while we are between worlds. Functionality - // in "addrs" should eventually replace this ResourceAddress idea - // completely, but for now we'll need to translate to the old - // way of representing resource modes. - switch r.Mode { - case ManagedResourceMode: - if rc.Mode != addrs.ManagedResourceMode { - return false - } - case DataResourceMode: - if rc.Mode != addrs.DataResourceMode { - return false - } - } - if r.Type != rc.Type || r.Name != rc.Name { - return false - } - } - - addrPath := r.Path - - // normalize - if len(addrPath) == 0 { - addrPath = nil - } - if len(path) == 0 { - path = nil - } - rawPath := []string(path) - return reflect.DeepEqual(addrPath, rawPath) -} - -// stateId returns the ID that this resource should be entered with -// in the state. This is also used for diffs. In the future, we'd like to -// move away from this string field so I don't export this. -func (r *ResourceAddress) stateId() string { - result := fmt.Sprintf("%s.%s", r.Type, r.Name) - switch r.Mode { - case ManagedResourceMode: - // Done - case DataResourceMode: - result = fmt.Sprintf("data.%s", result) - default: - panic(fmt.Errorf("unknown resource mode: %s", r.Mode)) - } - if r.Index >= 0 { - result += fmt.Sprintf(".%d", r.Index) - } - - return result -} - -// parseResourceAddressInternal parses the somewhat bespoke resource -// identifier used in states and diffs, such as "instance.name.0". -func parseResourceAddressInternal(s string) (*ResourceAddress, error) { - // Split based on ".". Every resource address should have at least two - // elements (type and name). - parts := strings.Split(s, ".") - if len(parts) < 2 || len(parts) > 4 { - return nil, fmt.Errorf("Invalid internal resource address format: %s", s) - } - - // Data resource if we have at least 3 parts and the first one is data - mode := ManagedResourceMode - if len(parts) > 2 && parts[0] == "data" { - mode = DataResourceMode - parts = parts[1:] - } - - // If we're not a data resource and we have more than 3, then it is an error - if len(parts) > 3 && mode != DataResourceMode { - return nil, fmt.Errorf("Invalid internal resource address format: %s", s) - } - - // Build the parts of the resource address that are guaranteed to exist - addr := &ResourceAddress{ - Type: parts[0], - Name: parts[1], - Index: -1, - InstanceType: TypePrimary, - Mode: mode, - } - - // If we have more parts, then we have an index. Parse that. - if len(parts) > 2 { - idx, err := strconv.ParseInt(parts[2], 0, 0) - if err != nil { - return nil, fmt.Errorf("Error parsing resource address %q: %s", s, err) - } - - addr.Index = int(idx) - } - - return addr, nil -} - -func ParseResourceAddress(s string) (*ResourceAddress, error) { - matches, err := tokenizeResourceAddress(s) - if err != nil { - return nil, err - } - mode := ManagedResourceMode - if matches["data_prefix"] != "" { - mode = DataResourceMode - } - resourceIndex, err := ParseResourceIndex(matches["index"]) - if err != nil { - return nil, err - } - instanceType, err := ParseInstanceType(matches["instance_type"]) - if err != nil { - return nil, err - } - path := ParseResourcePath(matches["path"]) - - // not allowed to say "data." without a type following - if mode == DataResourceMode && matches["type"] == "" { - return nil, fmt.Errorf( - "invalid resource address %q: must target specific data instance", - s, - ) - } - - return &ResourceAddress{ - Path: path, - Index: resourceIndex, - InstanceType: instanceType, - InstanceTypeSet: matches["instance_type"] != "", - Name: matches["name"], - Type: matches["type"], - Mode: mode, - }, nil -} - -// ParseResourceAddressForInstanceDiff creates a ResourceAddress for a -// resource name as described in a module diff. -// -// For historical reasons a different addressing format is used in this -// context. The internal format should not be shown in the UI and instead -// this function should be used to translate to a ResourceAddress and -// then, where appropriate, use the String method to produce a canonical -// resource address string for display in the UI. -// -// The given path slice must be empty (or nil) for the root module, and -// otherwise consist of a sequence of module names traversing down into -// the module tree. If a non-nil path is provided, the caller must not -// modify its underlying array after passing it to this function. -func ParseResourceAddressForInstanceDiff(path []string, key string) (*ResourceAddress, error) { - addr, err := parseResourceAddressInternal(key) - if err != nil { - return nil, err - } - addr.Path = path - return addr, nil -} - -// NewLegacyResourceAddress creates a ResourceAddress from a new-style -// addrs.AbsResource value. -// -// This is provided for shimming purposes so that we can still easily call into -// older functions that expect the ResourceAddress type. -func NewLegacyResourceAddress(addr addrs.AbsResource) *ResourceAddress { - ret := &ResourceAddress{ - Type: addr.Resource.Type, - Name: addr.Resource.Name, - } - - switch addr.Resource.Mode { - case addrs.ManagedResourceMode: - ret.Mode = ManagedResourceMode - case addrs.DataResourceMode: - ret.Mode = DataResourceMode - default: - panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Mode)) - } - - path := make([]string, len(addr.Module)) - for i, step := range addr.Module { - if step.InstanceKey != addrs.NoKey { - // At the time of writing this can't happen because we don't - // ket generate keyed module instances. This legacy codepath must - // be removed before we can support "count" and "for_each" for - // modules. - panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey)) - } - - path[i] = step.Name - } - ret.Path = path - ret.Index = -1 - - return ret -} - -// NewLegacyResourceInstanceAddress creates a ResourceAddress from a new-style -// addrs.AbsResource value. -// -// This is provided for shimming purposes so that we can still easily call into -// older functions that expect the ResourceAddress type. -func NewLegacyResourceInstanceAddress(addr addrs.AbsResourceInstance) *ResourceAddress { - ret := &ResourceAddress{ - Type: addr.Resource.Resource.Type, - Name: addr.Resource.Resource.Name, - } - - switch addr.Resource.Resource.Mode { - case addrs.ManagedResourceMode: - ret.Mode = ManagedResourceMode - case addrs.DataResourceMode: - ret.Mode = DataResourceMode - default: - panic(fmt.Errorf("cannot shim %s to legacy ResourceMode value", addr.Resource.Resource.Mode)) - } - - path := make([]string, len(addr.Module)) - for i, step := range addr.Module { - if step.InstanceKey != addrs.NoKey { - // At the time of writing this can't happen because we don't - // ket generate keyed module instances. This legacy codepath must - // be removed before we can support "count" and "for_each" for - // modules. - panic(fmt.Errorf("cannot shim module instance step with key %#v to legacy ResourceAddress.Path", step.InstanceKey)) - } - - path[i] = step.Name - } - ret.Path = path - - if addr.Resource.Key == addrs.NoKey { - ret.Index = -1 - } else if ik, ok := addr.Resource.Key.(addrs.IntKey); ok { - ret.Index = int(ik) - } else if _, ok := addr.Resource.Key.(addrs.StringKey); ok { - ret.Index = -1 - } else { - panic(fmt.Errorf("cannot shim resource instance with key %#v to legacy ResourceAddress.Index", addr.Resource.Key)) - } - - return ret -} - -// AbsResourceInstanceAddr converts the receiver, a legacy resource address, to -// the new resource address type addrs.AbsResourceInstance. -// -// This method can be used only on an address that has a resource specification. -// It will panic if called on a module-path-only ResourceAddress. Use -// method HasResourceSpec to check before calling, in contexts where it is -// unclear. -// -// addrs.AbsResourceInstance does not represent the "tainted" and "deposed" -// states, and so if these are present on the receiver then they are discarded. -// -// This is provided for shimming purposes so that we can easily adapt functions -// that are returning the legacy ResourceAddress type, for situations where -// the new type is required. -func (addr *ResourceAddress) AbsResourceInstanceAddr() addrs.AbsResourceInstance { - if !addr.HasResourceSpec() { - panic("AbsResourceInstanceAddr called on ResourceAddress with no resource spec") - } - - ret := addrs.AbsResourceInstance{ - Module: addr.ModuleInstanceAddr(), - Resource: addrs.ResourceInstance{ - Resource: addrs.Resource{ - Type: addr.Type, - Name: addr.Name, - }, - }, - } - - switch addr.Mode { - case ManagedResourceMode: - ret.Resource.Resource.Mode = addrs.ManagedResourceMode - case DataResourceMode: - ret.Resource.Resource.Mode = addrs.DataResourceMode - default: - panic(fmt.Errorf("cannot shim %s to addrs.ResourceMode value", addr.Mode)) - } - - if addr.Index != -1 { - ret.Resource.Key = addrs.IntKey(addr.Index) - } - - return ret -} - -// ModuleInstanceAddr returns the module path portion of the receiver as a -// addrs.ModuleInstance value. -func (addr *ResourceAddress) ModuleInstanceAddr() addrs.ModuleInstance { - path := make(addrs.ModuleInstance, len(addr.Path)) - for i, name := range addr.Path { - path[i] = addrs.ModuleInstanceStep{Name: name} - } - return path -} - -// Contains returns true if and only if the given node is contained within -// the receiver. -// -// Containment is defined in terms of the module and resource hierarchy: -// a resource is contained within its module and any ancestor modules, -// an indexed resource instance is contained with the unindexed resource, etc. -func (addr *ResourceAddress) Contains(other *ResourceAddress) bool { - ourPath := addr.Path - givenPath := other.Path - if len(givenPath) < len(ourPath) { - return false - } - for i := range ourPath { - if ourPath[i] != givenPath[i] { - return false - } - } - - // If the receiver is a whole-module address then the path prefix - // matching is all we need. - if !addr.HasResourceSpec() { - return true - } - - if addr.Type != other.Type || addr.Name != other.Name || addr.Mode != other.Mode { - return false - } - - if addr.Index != -1 && addr.Index != other.Index { - return false - } - - if addr.InstanceTypeSet && (addr.InstanceTypeSet != other.InstanceTypeSet || addr.InstanceType != other.InstanceType) { - return false - } - - return true -} - -// Equals returns true if the receiver matches the given address. -// -// The name of this method is a misnomer, since it doesn't test for exact -// equality. Instead, it tests that the _specified_ parts of each -// address match, treating any unspecified parts as wildcards. -// -// See also Contains, which takes a more hierarchical approach to comparing -// addresses. -func (addr *ResourceAddress) Equals(raw interface{}) bool { - other, ok := raw.(*ResourceAddress) - if !ok { - return false - } - - pathMatch := len(addr.Path) == 0 && len(other.Path) == 0 || - reflect.DeepEqual(addr.Path, other.Path) - - indexMatch := addr.Index == -1 || - other.Index == -1 || - addr.Index == other.Index - - nameMatch := addr.Name == "" || - other.Name == "" || - addr.Name == other.Name - - typeMatch := addr.Type == "" || - other.Type == "" || - addr.Type == other.Type - - // mode is significant only when type is set - modeMatch := addr.Type == "" || - other.Type == "" || - addr.Mode == other.Mode - - return pathMatch && - indexMatch && - addr.InstanceType == other.InstanceType && - nameMatch && - typeMatch && - modeMatch -} - -// Less returns true if and only if the receiver should be sorted before -// the given address when presenting a list of resource addresses to -// an end-user. -// -// This sort uses lexicographic sorting for most components, but uses -// numeric sort for indices, thus causing index 10 to sort after -// index 9, rather than after index 1. -func (addr *ResourceAddress) Less(other *ResourceAddress) bool { - - switch { - - case len(addr.Path) != len(other.Path): - return len(addr.Path) < len(other.Path) - - case !reflect.DeepEqual(addr.Path, other.Path): - // If the two paths are the same length but don't match, we'll just - // cheat and compare the string forms since it's easier than - // comparing all of the path segments in turn, and lexicographic - // comparison is correct for the module path portion. - addrStr := addr.String() - otherStr := other.String() - return addrStr < otherStr - - case addr.Mode != other.Mode: - return addr.Mode == DataResourceMode - - case addr.Type != other.Type: - return addr.Type < other.Type - - case addr.Name != other.Name: - return addr.Name < other.Name - - case addr.Index != other.Index: - // Since "Index" is -1 for an un-indexed address, this also conveniently - // sorts unindexed addresses before indexed ones, should they both - // appear for some reason. - return addr.Index < other.Index - - case addr.InstanceTypeSet != other.InstanceTypeSet: - return !addr.InstanceTypeSet - - case addr.InstanceType != other.InstanceType: - // InstanceType is actually an enum, so this is just an arbitrary - // sort based on the enum numeric values, and thus not particularly - // meaningful. - return addr.InstanceType < other.InstanceType - - default: - return false - - } -} - -func ParseResourceIndex(s string) (int, error) { - if s == "" { - return -1, nil - } - return strconv.Atoi(s) -} - -func ParseResourcePath(s string) []string { - if s == "" { - return nil - } - parts := strings.Split(s, ".") - path := make([]string, 0, len(parts)) - for _, s := range parts { - // Due to the limitations of the regexp match below, the path match has - // some noise in it we have to filter out :| - if s == "" || s == "module" { - continue - } - path = append(path, s) - } - return path -} - -func ParseInstanceType(s string) (InstanceType, error) { - switch s { - case "", "primary": - return TypePrimary, nil - case "deposed": - return TypeDeposed, nil - case "tainted": - return TypeTainted, nil - default: - return TypeInvalid, fmt.Errorf("Unexpected value for InstanceType field: %q", s) - } -} - -func tokenizeResourceAddress(s string) (map[string]string, error) { - // Example of portions of the regexp below using the - // string "aws_instance.web.tainted[1]" - re := regexp.MustCompile(`\A` + - // "module.foo.module.bar" (optional) - `(?P(?:module\.(?P[^.]+)\.?)*)` + - // possibly "data.", if targeting is a data resource - `(?P(?:data\.)?)` + - // "aws_instance.web" (optional when module path specified) - `(?:(?P[^.]+)\.(?P[^.[]+))?` + - // "tainted" (optional, omission implies: "primary") - `(?:\.(?P\w+))?` + - // "1" (optional, omission implies: "0") - `(?:\[(?P\d+)\])?` + - `\z`) - - groupNames := re.SubexpNames() - rawMatches := re.FindAllStringSubmatch(s, -1) - if len(rawMatches) != 1 { - return nil, fmt.Errorf("invalid resource address %q", s) - } - - matches := make(map[string]string) - for i, m := range rawMatches[0] { - matches[groupNames[i]] = m - } - - return matches, nil -} diff --git a/internal/legacy/terraform/resource_address_test.go b/internal/legacy/terraform/resource_address_test.go deleted file mode 100644 index e0e8ed0061..0000000000 --- a/internal/legacy/terraform/resource_address_test.go +++ /dev/null @@ -1,1329 +0,0 @@ -package terraform - -import ( - "fmt" - "reflect" - "testing" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" -) - -func TestParseResourceAddressInternal(t *testing.T) { - cases := map[string]struct { - Input string - Expected *ResourceAddress - Output string - }{ - "basic resource": { - "aws_instance.foo", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "aws_instance.foo", - }, - - "basic resource with count": { - "aws_instance.foo.1", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 1, - }, - "aws_instance.foo[1]", - }, - - "data resource": { - "data.aws_ami.foo", - &ResourceAddress{ - Mode: DataResourceMode, - Type: "aws_ami", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "data.aws_ami.foo", - }, - - "data resource with count": { - "data.aws_ami.foo.1", - &ResourceAddress{ - Mode: DataResourceMode, - Type: "aws_ami", - Name: "foo", - InstanceType: TypePrimary, - Index: 1, - }, - "data.aws_ami.foo[1]", - }, - - "non-data resource with 4 elements": { - "aws_instance.foo.bar.1", - nil, - "", - }, - } - - for tn, tc := range cases { - t.Run(tc.Input, func(t *testing.T) { - out, err := parseResourceAddressInternal(tc.Input) - if (err != nil) != (tc.Expected == nil) { - t.Fatalf("%s: unexpected err: %#v", tn, err) - } - if err != nil { - return - } - - if !reflect.DeepEqual(out, tc.Expected) { - t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out) - } - - // Compare outputs if those exist - expected := tc.Input - if tc.Output != "" { - expected = tc.Output - } - if out.String() != expected { - t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, expected, out) - } - - // Compare equality because the internal parse is used - // to compare equality to equal inputs. - if !out.Equals(tc.Expected) { - t.Fatalf("expected equality:\n\n%#v\n\n%#v", out, tc.Expected) - } - }) - } -} - -func TestParseResourceAddress(t *testing.T) { - cases := map[string]struct { - Input string - Expected *ResourceAddress - Output string - Err bool - }{ - "implicit primary managed instance, no specific index": { - "aws_instance.foo", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "implicit primary data instance, no specific index": { - "data.aws_instance.foo", - &ResourceAddress{ - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "implicit primary, explicit index": { - "aws_instance.foo[2]", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 2, - }, - "", - false, - }, - "implicit primary, explicit index over ten": { - "aws_instance.foo[12]", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 12, - }, - "", - false, - }, - "explicit primary, explicit index": { - "aws_instance.foo.primary[2]", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - InstanceTypeSet: true, - Index: 2, - }, - "", - false, - }, - "tainted": { - "aws_instance.foo.tainted", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypeTainted, - InstanceTypeSet: true, - Index: -1, - }, - "", - false, - }, - "deposed": { - "aws_instance.foo.deposed", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypeDeposed, - InstanceTypeSet: true, - Index: -1, - }, - "", - false, - }, - "with a hyphen": { - "aws_instance.foo-bar", - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo-bar", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "managed in a module": { - "module.child.aws_instance.foo", - &ResourceAddress{ - Path: []string{"child"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "data in a module": { - "module.child.data.aws_instance.foo", - &ResourceAddress{ - Path: []string{"child"}, - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "nested modules": { - "module.a.module.b.module.forever.aws_instance.foo", - &ResourceAddress{ - Path: []string{"a", "b", "forever"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "just a module": { - "module.a", - &ResourceAddress{ - Path: []string{"a"}, - Type: "", - Name: "", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "just a nested module": { - "module.a.module.b", - &ResourceAddress{ - Path: []string{"a", "b"}, - Type: "", - Name: "", - InstanceType: TypePrimary, - Index: -1, - }, - "", - false, - }, - "module missing resource type": { - "module.name.foo", - nil, - "", - true, - }, - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - out, err := ParseResourceAddress(tc.Input) - if (err != nil) != tc.Err { - t.Fatalf("%s: unexpected err: %#v", tn, err) - } - if tc.Err { - return - } - - if !reflect.DeepEqual(out, tc.Expected) { - t.Fatalf("bad: %q\n\nexpected:\n%#v\n\ngot:\n%#v", tn, tc.Expected, out) - } - - expected := tc.Input - if tc.Output != "" { - expected = tc.Output - } - if out.String() != expected { - t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, expected, out) - } - }) - } -} - -func TestResourceAddressContains(t *testing.T) { - tests := []struct { - Address *ResourceAddress - Other *ResourceAddress - Want bool - }{ - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - true, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: 0, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - true, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - true, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - { - &ResourceAddress{ - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - { - &ResourceAddress{ - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"bar"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"bar"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"bar", "baz"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"bar", "baz"}, - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"bar", "baz", "foo", "pizza"}, - InstanceTypeSet: false, - Index: -1, - }, - true, - }, - - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "bar", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - false, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - &ResourceAddress{ - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - false, - }, - { - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"baz"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - false, - }, - { - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"baz", "bar"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: -1, - }, - false, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: true, - InstanceType: TypePrimary, - Index: 0, - }, - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceTypeSet: false, - Index: 0, - }, - false, - }, - { - &ResourceAddress{ - Path: []string{"bar", "baz"}, - InstanceTypeSet: false, - Index: -1, - }, - &ResourceAddress{ - Path: []string{"bar"}, - InstanceTypeSet: false, - Index: -1, - }, - false, - }, - { - &ResourceAddress{ - Type: "aws_instance", - Name: "foo", - Index: 1, - InstanceType: TypePrimary, - Mode: ManagedResourceMode, - }, - &ResourceAddress{ - Type: "aws_instance", - Name: "foo", - Index: -1, - InstanceType: TypePrimary, - Mode: ManagedResourceMode, - }, - false, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%s contains %s", test.Address, test.Other), func(t *testing.T) { - got := test.Address.Contains(test.Other) - if got != test.Want { - t.Errorf( - "wrong result\nrecv: %s\ngiven: %s\ngot: %#v\nwant: %#v", - test.Address, test.Other, - got, test.Want, - ) - } - }) - } -} - -func TestResourceAddressEquals(t *testing.T) { - cases := map[string]struct { - Address *ResourceAddress - Other interface{} - Expect bool - }{ - "basic match": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: true, - }, - "address does not set index": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 3, - }, - Expect: true, - }, - "other does not set index": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 3, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - Expect: true, - }, - "neither sets index": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - Expect: true, - }, - "index over ten": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 1, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 13, - }, - Expect: false, - }, - "different type": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_vpc", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: false, - }, - "different mode": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Other: &ResourceAddress{ - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: false, - }, - "different name": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "bar", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: false, - }, - "different instance type": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypeTainted, - Index: 0, - }, - Expect: false, - }, - "different index": { - Address: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Other: &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 1, - }, - Expect: false, - }, - "module address matches address of managed resource inside module": { - Address: &ResourceAddress{ - Path: []string{"a", "b"}, - Type: "", - Name: "", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Path: []string{"a", "b"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: true, - }, - "module address matches address of data resource inside module": { - Address: &ResourceAddress{ - Path: []string{"a", "b"}, - Type: "", - Name: "", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Path: []string{"a", "b"}, - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: true, - }, - "module address doesn't match managed resource outside module": { - Address: &ResourceAddress{ - Path: []string{"a", "b"}, - Type: "", - Name: "", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Path: []string{"a"}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: false, - }, - "module address doesn't match data resource outside module": { - Address: &ResourceAddress{ - Path: []string{"a", "b"}, - Type: "", - Name: "", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Path: []string{"a"}, - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: false, - }, - "nil path vs empty path should match": { - Address: &ResourceAddress{ - Path: []string{}, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - Other: &ResourceAddress{ - Path: nil, - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 0, - }, - Expect: true, - }, - } - - for tn, tc := range cases { - actual := tc.Address.Equals(tc.Other) - if actual != tc.Expect { - t.Fatalf("%q: expected equals: %t, got %t for:\n%#v\n%#v", - tn, tc.Expect, actual, tc.Address, tc.Other) - } - } -} - -func TestResourceAddressStateId(t *testing.T) { - cases := map[string]struct { - Input *ResourceAddress - Expected string - }{ - "basic resource": { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "aws_instance.foo", - }, - - "basic resource with index": { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: 2, - }, - "aws_instance.foo.2", - }, - - "data resource": { - &ResourceAddress{ - Mode: DataResourceMode, - Type: "aws_instance", - Name: "foo", - InstanceType: TypePrimary, - Index: -1, - }, - "data.aws_instance.foo", - }, - } - - for tn, tc := range cases { - t.Run(tn, func(t *testing.T) { - actual := tc.Input.stateId() - if actual != tc.Expected { - t.Fatalf("bad: %q\n\nexpected: %s\n\ngot: %s", tn, tc.Expected, actual) - } - }) - } -} - -func TestResourceAddressHasResourceSpec(t *testing.T) { - cases := []struct { - Input string - Want bool - }{ - { - "module.foo", - false, - }, - { - "module.foo.module.bar", - false, - }, - { - "null_resource.baz", - true, - }, - { - "null_resource.baz[0]", - true, - }, - { - "data.null_data_source.baz", - true, - }, - { - "data.null_data_source.baz[0]", - true, - }, - { - "module.foo.null_resource.baz", - true, - }, - { - "module.foo.data.null_data_source.baz", - true, - }, - { - "module.foo.module.bar.null_resource.baz", - true, - }, - } - - for _, test := range cases { - t.Run(test.Input, func(t *testing.T) { - addr, err := ParseResourceAddress(test.Input) - if err != nil { - t.Fatalf("error parsing address: %s", err) - } - got := addr.HasResourceSpec() - if got != test.Want { - t.Fatalf("%q: wrong result %#v; want %#v", test.Input, got, test.Want) - } - }) - } -} - -func TestResourceAddressWholeModuleAddress(t *testing.T) { - cases := []struct { - Input string - Want string - }{ - { - "module.foo", - "module.foo", - }, - { - "module.foo.module.bar", - "module.foo.module.bar", - }, - { - "null_resource.baz", - "", - }, - { - "null_resource.baz[0]", - "", - }, - { - "data.null_data_source.baz", - "", - }, - { - "data.null_data_source.baz[0]", - "", - }, - { - "module.foo.null_resource.baz", - "module.foo", - }, - { - "module.foo.data.null_data_source.baz", - "module.foo", - }, - { - "module.foo.module.bar.null_resource.baz", - "module.foo.module.bar", - }, - } - - for _, test := range cases { - t.Run(test.Input, func(t *testing.T) { - addr, err := ParseResourceAddress(test.Input) - if err != nil { - t.Fatalf("error parsing address: %s", err) - } - gotAddr := addr.WholeModuleAddress() - got := gotAddr.String() - if got != test.Want { - t.Fatalf("%q: wrong result %#v; want %#v", test.Input, got, test.Want) - } - }) - } -} - -func TestResourceAddressMatchesResourceConfig(t *testing.T) { - root := []string(nil) - child := []string{"child"} - grandchild := []string{"child", "grandchild"} - irrelevant := []string{"irrelevant"} - - tests := []struct { - Addr *ResourceAddress - ModulePath []string - Resource *configs.Resource - Want bool - }{ - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - root, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"child"}, - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - child, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"child", "grandchild"}, - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - grandchild, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"child"}, - Index: -1, - }, - child, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - true, - }, - { - &ResourceAddress{ - Path: []string{"child", "grandchild"}, - Index: -1, - }, - grandchild, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - true, - }, - { - &ResourceAddress{ - Mode: DataResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - irrelevant, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - false, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - irrelevant, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "pizza", - }, - false, - }, - { - &ResourceAddress{ - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - irrelevant, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "aws_instance", - Name: "baz", - }, - false, - }, - { - &ResourceAddress{ - Path: []string{"child", "grandchild"}, - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - child, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - false, - }, - { - &ResourceAddress{ - Path: []string{"child"}, - Mode: ManagedResourceMode, - Type: "null_resource", - Name: "baz", - Index: -1, - }, - grandchild, - &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "null_resource", - Name: "baz", - }, - false, - }, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("%02d-%s", i, test.Addr), func(t *testing.T) { - got := test.Addr.MatchesResourceConfig(test.ModulePath, test.Resource) - if got != test.Want { - t.Errorf( - "wrong result\naddr: %s\nmod: %#v\nrsrc: %#v\ngot: %#v\nwant: %#v", - test.Addr, test.ModulePath, test.Resource, got, test.Want, - ) - } - }) - } -} - -func TestResourceAddressLess(t *testing.T) { - tests := []struct { - A string - B string - Want bool - }{ - { - "foo.bar", - "module.baz.foo.bar", - true, - }, - { - "module.baz.foo.bar", - "zzz.bar", // would sort after "module" in lexicographical sort - false, - }, - { - "module.baz.foo.bar", - "module.baz.foo.bar", - false, - }, - { - "module.baz.foo.bar", - "module.boz.foo.bar", - true, - }, - { - "module.boz.foo.bar", - "module.baz.foo.bar", - false, - }, - { - "a.b", - "b.c", - true, - }, - { - "a.b", - "a.c", - true, - }, - { - "c.b", - "b.c", - false, - }, - { - "a.b[9]", - "a.b[10]", - true, - }, - { - "b.b[9]", - "a.b[10]", - false, - }, - { - "a.b", - "a.b.deposed", - true, - }, - { - "a.b.tainted", - "a.b.deposed", - true, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%s < %s", test.A, test.B), func(t *testing.T) { - addrA, err := ParseResourceAddress(test.A) - if err != nil { - t.Fatal(err) - } - addrB, err := ParseResourceAddress(test.B) - if err != nil { - t.Fatal(err) - } - got := addrA.Less(addrB) - invGot := addrB.Less(addrA) - if got != test.Want { - t.Errorf( - "wrong result\ntest: %s < %s\ngot: %#v\nwant: %#v", - test.A, test.B, got, test.Want, - ) - } - if test.A != test.B { // inverse test doesn't apply when equal - if invGot != !test.Want { - t.Errorf( - "wrong inverse result\ntest: %s < %s\ngot: %#v\nwant: %#v", - test.B, test.A, invGot, !test.Want, - ) - } - } else { - if invGot != test.Want { - t.Errorf( - "wrong inverse result\ntest: %s < %s\ngot: %#v\nwant: %#v", - test.B, test.A, invGot, test.Want, - ) - } - } - }) - } -} diff --git a/internal/legacy/terraform/resource_mode.go b/internal/legacy/terraform/resource_mode.go index c83643a65c..8eef737730 100644 --- a/internal/legacy/terraform/resource_mode.go +++ b/internal/legacy/terraform/resource_mode.go @@ -1,6 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform -//go:generate go run golang.org/x/tools/cmd/stringer -type=ResourceMode -output=resource_mode_string.go resource_mode.go +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ResourceMode -output=resource_mode_string.go resource_mode.go // ResourceMode is deprecated, use addrs.ResourceMode instead. // It has been preserved for backwards compatibility. diff --git a/internal/legacy/terraform/resource_provider.go b/internal/legacy/terraform/resource_provider.go index dccfec68b6..273dfd3dcf 100644 --- a/internal/legacy/terraform/resource_provider.go +++ b/internal/legacy/terraform/resource_provider.go @@ -1,5 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" +) + // ResourceProvider is a legacy interface for providers. // // This is retained only for compatibility with legacy code. The current @@ -19,14 +27,6 @@ type ResourceProvider interface { // resource or data source has the SchemaAvailable flag set. GetSchema(*ProviderSchemaRequest) (*ProviderSchema, error) - // Input was used prior to v0.12 to ask the provider to prompt the user - // for input to complete the configuration. - // - // From v0.12 onwards this method is never called because Terraform Core - // is able to handle the necessary input logic itself based on the - // schema returned from GetSchema. - Input(UIInput, *ResourceConfig) (*ResourceConfig, error) - // Validate is called once at the beginning with the raw configuration // (no interpolation done) and can return a list of warnings and/or // errors. @@ -164,6 +164,49 @@ type ResourceProviderCloser interface { Close() error } +// ProviderSchemaRequest is used to describe to a ResourceProvider which +// aspects of schema are required, when calling the GetSchema method. +type ProviderSchemaRequest struct { + ResourceTypes []string + DataSources []string +} + +// ProviderSchema represents the schema for a provider's own configuration +// and the configuration for some or all of its resources and data sources. +// +// The completeness of this structure depends on how it was constructed. +// When constructed for a configuration, it will generally include only +// resource types and data sources used by that configuration. +type ProviderSchema struct { + Provider *configschema.Block + ProviderMeta *configschema.Block + ResourceTypes map[string]*configschema.Block + DataSources map[string]*configschema.Block + + ResourceTypeSchemaVersions map[string]uint64 +} + +// SchemaForResourceType attempts to find a schema for the given mode and type. +// Returns nil if no such schema is available. +func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) { + switch mode { + case addrs.ManagedResourceMode: + return ps.ResourceTypes[typeName], ps.ResourceTypeSchemaVersions[typeName] + case addrs.DataResourceMode: + // Data resources don't have schema versions right now, since state is discarded for each refresh + return ps.DataSources[typeName], 0 + default: + // Shouldn't happen, because the above cases are comprehensive. + return nil, 0 + } +} + +// SchemaForResourceAddr attempts to find a schema for the mode and type from +// the given resource address. Returns nil if no such schema is available. +func (ps *ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) { + return ps.SchemaForResourceType(addr.Mode, addr.Type) +} + // ResourceType is a type of resource that a resource provider can manage. type ResourceType struct { Name string // Name of the resource, example "instance" (no provider prefix) diff --git a/internal/legacy/terraform/resource_provider_mock.go b/internal/legacy/terraform/resource_provider_mock.go deleted file mode 100644 index a167b722ae..0000000000 --- a/internal/legacy/terraform/resource_provider_mock.go +++ /dev/null @@ -1,315 +0,0 @@ -package terraform - -import ( - "sync" -) - -// MockResourceProvider implements ResourceProvider but mocks out all the -// calls for testing purposes. -type MockResourceProvider struct { - sync.Mutex - - // Anything you want, in case you need to store extra data with the mock. - Meta interface{} - - CloseCalled bool - CloseError error - GetSchemaCalled bool - GetSchemaRequest *ProviderSchemaRequest - GetSchemaReturn *ProviderSchema - GetSchemaReturnError error - InputCalled bool - InputInput UIInput - InputConfig *ResourceConfig - InputReturnConfig *ResourceConfig - InputReturnError error - InputFn func(UIInput, *ResourceConfig) (*ResourceConfig, error) - ApplyCalled bool - ApplyInfo *InstanceInfo - ApplyState *InstanceState - ApplyDiff *InstanceDiff - ApplyFn func(*InstanceInfo, *InstanceState, *InstanceDiff) (*InstanceState, error) - ApplyReturn *InstanceState - ApplyReturnError error - ConfigureCalled bool - ConfigureConfig *ResourceConfig - ConfigureProviderFn func(*ResourceConfig) error - ConfigureReturnError error - DiffCalled bool - DiffInfo *InstanceInfo - DiffState *InstanceState - DiffDesired *ResourceConfig - DiffFn func(*InstanceInfo, *InstanceState, *ResourceConfig) (*InstanceDiff, error) - DiffReturn *InstanceDiff - DiffReturnError error - RefreshCalled bool - RefreshInfo *InstanceInfo - RefreshState *InstanceState - RefreshFn func(*InstanceInfo, *InstanceState) (*InstanceState, error) - RefreshReturn *InstanceState - RefreshReturnError error - ResourcesCalled bool - ResourcesReturn []ResourceType - ReadDataApplyCalled bool - ReadDataApplyInfo *InstanceInfo - ReadDataApplyDiff *InstanceDiff - ReadDataApplyFn func(*InstanceInfo, *InstanceDiff) (*InstanceState, error) - ReadDataApplyReturn *InstanceState - ReadDataApplyReturnError error - ReadDataDiffCalled bool - ReadDataDiffInfo *InstanceInfo - ReadDataDiffDesired *ResourceConfig - ReadDataDiffFn func(*InstanceInfo, *ResourceConfig) (*InstanceDiff, error) - ReadDataDiffReturn *InstanceDiff - ReadDataDiffReturnError error - StopCalled bool - StopFn func() error - StopReturnError error - DataSourcesCalled bool - DataSourcesReturn []DataSource - ValidateCalled bool - ValidateConfig *ResourceConfig - ValidateFn func(*ResourceConfig) ([]string, []error) - ValidateReturnWarns []string - ValidateReturnErrors []error - ValidateResourceFn func(string, *ResourceConfig) ([]string, []error) - ValidateResourceCalled bool - ValidateResourceType string - ValidateResourceConfig *ResourceConfig - ValidateResourceReturnWarns []string - ValidateResourceReturnErrors []error - ValidateDataSourceFn func(string, *ResourceConfig) ([]string, []error) - ValidateDataSourceCalled bool - ValidateDataSourceType string - ValidateDataSourceConfig *ResourceConfig - ValidateDataSourceReturnWarns []string - ValidateDataSourceReturnErrors []error - - ImportStateCalled bool - ImportStateInfo *InstanceInfo - ImportStateID string - ImportStateReturn []*InstanceState - ImportStateReturnError error - ImportStateFn func(*InstanceInfo, string) ([]*InstanceState, error) -} - -func (p *MockResourceProvider) Close() error { - p.CloseCalled = true - return p.CloseError -} - -func (p *MockResourceProvider) GetSchema(req *ProviderSchemaRequest) (*ProviderSchema, error) { - p.Lock() - defer p.Unlock() - - p.GetSchemaCalled = true - p.GetSchemaRequest = req - return p.GetSchemaReturn, p.GetSchemaReturnError -} - -func (p *MockResourceProvider) Input( - input UIInput, c *ResourceConfig) (*ResourceConfig, error) { - p.Lock() - defer p.Unlock() - p.InputCalled = true - p.InputInput = input - p.InputConfig = c - if p.InputFn != nil { - return p.InputFn(input, c) - } - return p.InputReturnConfig, p.InputReturnError -} - -func (p *MockResourceProvider) Validate(c *ResourceConfig) ([]string, []error) { - p.Lock() - defer p.Unlock() - - p.ValidateCalled = true - p.ValidateConfig = c - if p.ValidateFn != nil { - return p.ValidateFn(c) - } - return p.ValidateReturnWarns, p.ValidateReturnErrors -} - -func (p *MockResourceProvider) ValidateResource(t string, c *ResourceConfig) ([]string, []error) { - p.Lock() - defer p.Unlock() - - p.ValidateResourceCalled = true - p.ValidateResourceType = t - p.ValidateResourceConfig = c - - if p.ValidateResourceFn != nil { - return p.ValidateResourceFn(t, c) - } - - return p.ValidateResourceReturnWarns, p.ValidateResourceReturnErrors -} - -func (p *MockResourceProvider) Configure(c *ResourceConfig) error { - p.Lock() - defer p.Unlock() - - p.ConfigureCalled = true - p.ConfigureConfig = c - - if p.ConfigureProviderFn != nil { - return p.ConfigureProviderFn(c) - } - - return p.ConfigureReturnError -} - -func (p *MockResourceProvider) Stop() error { - p.Lock() - defer p.Unlock() - - p.StopCalled = true - if p.StopFn != nil { - return p.StopFn() - } - - return p.StopReturnError -} - -func (p *MockResourceProvider) Apply( - info *InstanceInfo, - state *InstanceState, - diff *InstanceDiff) (*InstanceState, error) { - // We only lock while writing data. Reading is fine - p.Lock() - p.ApplyCalled = true - p.ApplyInfo = info - p.ApplyState = state - p.ApplyDiff = diff - p.Unlock() - - if p.ApplyFn != nil { - return p.ApplyFn(info, state, diff) - } - - return p.ApplyReturn.DeepCopy(), p.ApplyReturnError -} - -func (p *MockResourceProvider) Diff( - info *InstanceInfo, - state *InstanceState, - desired *ResourceConfig) (*InstanceDiff, error) { - p.Lock() - defer p.Unlock() - - p.DiffCalled = true - p.DiffInfo = info - p.DiffState = state - p.DiffDesired = desired - - if p.DiffFn != nil { - return p.DiffFn(info, state, desired) - } - - return p.DiffReturn.DeepCopy(), p.DiffReturnError -} - -func (p *MockResourceProvider) Refresh( - info *InstanceInfo, - s *InstanceState) (*InstanceState, error) { - p.Lock() - defer p.Unlock() - - p.RefreshCalled = true - p.RefreshInfo = info - p.RefreshState = s - - if p.RefreshFn != nil { - return p.RefreshFn(info, s) - } - - return p.RefreshReturn.DeepCopy(), p.RefreshReturnError -} - -func (p *MockResourceProvider) Resources() []ResourceType { - p.Lock() - defer p.Unlock() - - p.ResourcesCalled = true - return p.ResourcesReturn -} - -func (p *MockResourceProvider) ImportState(info *InstanceInfo, id string) ([]*InstanceState, error) { - p.Lock() - defer p.Unlock() - - p.ImportStateCalled = true - p.ImportStateInfo = info - p.ImportStateID = id - if p.ImportStateFn != nil { - return p.ImportStateFn(info, id) - } - - var result []*InstanceState - if p.ImportStateReturn != nil { - result = make([]*InstanceState, len(p.ImportStateReturn)) - for i, v := range p.ImportStateReturn { - result[i] = v.DeepCopy() - } - } - - return result, p.ImportStateReturnError -} - -func (p *MockResourceProvider) ValidateDataSource(t string, c *ResourceConfig) ([]string, []error) { - p.Lock() - defer p.Unlock() - - p.ValidateDataSourceCalled = true - p.ValidateDataSourceType = t - p.ValidateDataSourceConfig = c - - if p.ValidateDataSourceFn != nil { - return p.ValidateDataSourceFn(t, c) - } - - return p.ValidateDataSourceReturnWarns, p.ValidateDataSourceReturnErrors -} - -func (p *MockResourceProvider) ReadDataDiff( - info *InstanceInfo, - desired *ResourceConfig) (*InstanceDiff, error) { - p.Lock() - defer p.Unlock() - - p.ReadDataDiffCalled = true - p.ReadDataDiffInfo = info - p.ReadDataDiffDesired = desired - if p.ReadDataDiffFn != nil { - return p.ReadDataDiffFn(info, desired) - } - - return p.ReadDataDiffReturn.DeepCopy(), p.ReadDataDiffReturnError -} - -func (p *MockResourceProvider) ReadDataApply( - info *InstanceInfo, - d *InstanceDiff) (*InstanceState, error) { - p.Lock() - defer p.Unlock() - - p.ReadDataApplyCalled = true - p.ReadDataApplyInfo = info - p.ReadDataApplyDiff = d - - if p.ReadDataApplyFn != nil { - return p.ReadDataApplyFn(info, d) - } - - return p.ReadDataApplyReturn.DeepCopy(), p.ReadDataApplyReturnError -} - -func (p *MockResourceProvider) DataSources() []DataSource { - p.Lock() - defer p.Unlock() - - p.DataSourcesCalled = true - return p.DataSourcesReturn -} diff --git a/internal/legacy/terraform/resource_provisioner.go b/internal/legacy/terraform/resource_provisioner.go deleted file mode 100644 index 647693a9fa..0000000000 --- a/internal/legacy/terraform/resource_provisioner.go +++ /dev/null @@ -1,69 +0,0 @@ -package terraform - -import ( - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/provisioners" -) - -// ResourceProvisioner is an interface that must be implemented by any -// resource provisioner: the thing that initializes resources in -// a Terraform configuration. -type ResourceProvisioner interface { - // GetConfigSchema returns the schema for the provisioner type's main - // configuration block. This is called prior to Validate to enable some - // basic structural validation to be performed automatically and to allow - // the configuration to be properly extracted from potentially-ambiguous - // configuration file formats. - GetConfigSchema() (*configschema.Block, error) - - // Validate is called once at the beginning with the raw - // configuration (no interpolation done) and can return a list of warnings - // and/or errors. - // - // This is called once per resource. - // - // This should not assume any of the values in the resource configuration - // are valid since it is possible they have to be interpolated still. - // The primary use case of this call is to check that the required keys - // are set and that the general structure is correct. - Validate(*ResourceConfig) ([]string, []error) - - // Apply runs the provisioner on a specific resource and returns an error. - // Instead of a diff, the ResourceConfig is provided since provisioners - // only run after a resource has been newly created. - Apply(UIOutput, *InstanceState, *ResourceConfig) error - - // Stop is called when the provisioner should halt any in-flight actions. - // - // This can be used to make a nicer Ctrl-C experience for Terraform. - // Even if this isn't implemented to do anything (just returns nil), - // Terraform will still cleanly stop after the currently executing - // graph node is complete. However, this API can be used to make more - // efficient halts. - // - // Stop doesn't have to and shouldn't block waiting for in-flight actions - // to complete. It should take any action it wants and return immediately - // acknowledging it has received the stop request. Terraform core will - // automatically not make any further API calls to the provider soon - // after Stop is called (technically exactly once the currently executing - // graph nodes are complete). - // - // The error returned, if non-nil, is assumed to mean that signaling the - // stop somehow failed and that the user should expect potentially waiting - // a longer period of time. - Stop() error -} - -// ResourceProvisionerCloser is an interface that provisioners that can close -// connections that aren't needed anymore must implement. -type ResourceProvisionerCloser interface { - Close() error -} - -// ResourceProvisionerFactory is a function type that creates a new instance -// of a resource provisioner. -type ResourceProvisionerFactory func() (ResourceProvisioner, error) - -// ProvisionerFactory is a function type that creates a new instance -// of a provisioners.Interface. -type ProvisionerFactory = provisioners.Factory diff --git a/internal/legacy/terraform/resource_provisioner_mock.go b/internal/legacy/terraform/resource_provisioner_mock.go deleted file mode 100644 index 27c07b7dc6..0000000000 --- a/internal/legacy/terraform/resource_provisioner_mock.go +++ /dev/null @@ -1,87 +0,0 @@ -package terraform - -import ( - "sync" - - "github.com/hashicorp/terraform/internal/configs/configschema" -) - -// MockResourceProvisioner implements ResourceProvisioner but mocks out all the -// calls for testing purposes. -type MockResourceProvisioner struct { - sync.Mutex - // Anything you want, in case you need to store extra data with the mock. - Meta interface{} - - GetConfigSchemaCalled bool - GetConfigSchemaReturnSchema *configschema.Block - GetConfigSchemaReturnError error - - ApplyCalled bool - ApplyOutput UIOutput - ApplyState *InstanceState - ApplyConfig *ResourceConfig - ApplyFn func(*InstanceState, *ResourceConfig) error - ApplyReturnError error - - ValidateCalled bool - ValidateConfig *ResourceConfig - ValidateFn func(c *ResourceConfig) ([]string, []error) - ValidateReturnWarns []string - ValidateReturnErrors []error - - StopCalled bool - StopFn func() error - StopReturnError error -} - -var _ ResourceProvisioner = (*MockResourceProvisioner)(nil) - -func (p *MockResourceProvisioner) GetConfigSchema() (*configschema.Block, error) { - p.GetConfigSchemaCalled = true - return p.GetConfigSchemaReturnSchema, p.GetConfigSchemaReturnError -} - -func (p *MockResourceProvisioner) Validate(c *ResourceConfig) ([]string, []error) { - p.Lock() - defer p.Unlock() - - p.ValidateCalled = true - p.ValidateConfig = c - if p.ValidateFn != nil { - return p.ValidateFn(c) - } - return p.ValidateReturnWarns, p.ValidateReturnErrors -} - -func (p *MockResourceProvisioner) Apply( - output UIOutput, - state *InstanceState, - c *ResourceConfig) error { - p.Lock() - - p.ApplyCalled = true - p.ApplyOutput = output - p.ApplyState = state - p.ApplyConfig = c - if p.ApplyFn != nil { - fn := p.ApplyFn - p.Unlock() - return fn(state, c) - } - - defer p.Unlock() - return p.ApplyReturnError -} - -func (p *MockResourceProvisioner) Stop() error { - p.Lock() - defer p.Unlock() - - p.StopCalled = true - if p.StopFn != nil { - return p.StopFn() - } - - return p.StopReturnError -} diff --git a/internal/legacy/terraform/resource_test.go b/internal/legacy/terraform/resource_test.go index c91c70c1c6..00bb349f6a 100644 --- a/internal/legacy/terraform/resource_test.go +++ b/internal/legacy/terraform/resource_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,11 +8,11 @@ import ( "reflect" "testing" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/mitchellh/reflectwalk" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" - "github.com/mitchellh/reflectwalk" ) func TestResourceConfigGet(t *testing.T) { diff --git a/internal/legacy/terraform/schemas.go b/internal/legacy/terraform/schemas.go deleted file mode 100644 index 20b77ea973..0000000000 --- a/internal/legacy/terraform/schemas.go +++ /dev/null @@ -1,285 +0,0 @@ -package terraform - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// Schemas is a container for various kinds of schema that Terraform needs -// during processing. -type Schemas struct { - Providers map[addrs.Provider]*ProviderSchema - Provisioners map[string]*configschema.Block -} - -// ProviderSchema returns the entire ProviderSchema object that was produced -// by the plugin for the given provider, or nil if no such schema is available. -// -// It's usually better to go use the more precise methods offered by type -// Schemas to handle this detail automatically. -func (ss *Schemas) ProviderSchema(provider addrs.Provider) *ProviderSchema { - if ss.Providers == nil { - return nil - } - return ss.Providers[provider] -} - -// ProviderConfig returns the schema for the provider configuration of the -// given provider type, or nil if no such schema is available. -func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block { - ps := ss.ProviderSchema(provider) - if ps == nil { - return nil - } - return ps.Provider -} - -// ResourceTypeConfig returns the schema for the configuration of a given -// resource type belonging to a given provider type, or nil of no such -// schema is available. -// -// In many cases the provider type is inferrable from the resource type name, -// but this is not always true because users can override the provider for -// a resource using the "provider" meta-argument. Therefore it's important to -// always pass the correct provider name, even though it many cases it feels -// redundant. -func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (block *configschema.Block, schemaVersion uint64) { - ps := ss.ProviderSchema(provider) - if ps == nil || ps.ResourceTypes == nil { - return nil, 0 - } - return ps.SchemaForResourceType(resourceMode, resourceType) -} - -// ProvisionerConfig returns the schema for the configuration of a given -// provisioner, or nil of no such schema is available. -func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block { - return ss.Provisioners[name] -} - -// LoadSchemas searches the given configuration, state and plan (any of which -// may be nil) for constructs that have an associated schema, requests the -// necessary schemas from the given component factory (which must _not_ be nil), -// and returns a single object representing all of the necessary schemas. -// -// If an error is returned, it may be a wrapped tfdiags.Diagnostics describing -// errors across multiple separate objects. Errors here will usually indicate -// either misbehavior on the part of one of the providers or of the provider -// protocol itself. When returned with errors, the returned schemas object is -// still valid but may be incomplete. -func LoadSchemas(config *configs.Config, state *states.State, components contextComponentFactory) (*Schemas, error) { - schemas := &Schemas{ - Providers: map[addrs.Provider]*ProviderSchema{}, - Provisioners: map[string]*configschema.Block{}, - } - var diags tfdiags.Diagnostics - - newDiags := loadProviderSchemas(schemas.Providers, config, state, components) - diags = diags.Append(newDiags) - newDiags = loadProvisionerSchemas(schemas.Provisioners, config, components) - diags = diags.Append(newDiags) - - return schemas, diags.Err() -} - -func loadProviderSchemas(schemas map[addrs.Provider]*ProviderSchema, config *configs.Config, state *states.State, components contextComponentFactory) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - ensure := func(fqn addrs.Provider) { - name := fqn.String() - - if _, exists := schemas[fqn]; exists { - return - } - - log.Printf("[TRACE] LoadSchemas: retrieving schema for provider type %q", name) - provider, err := components.ResourceProvider(fqn) - if err != nil { - // We'll put a stub in the map so we won't re-attempt this on - // future calls. - schemas[fqn] = &ProviderSchema{} - diags = diags.Append( - fmt.Errorf("Failed to instantiate provider %q to obtain schema: %s", name, err), - ) - return - } - defer func() { - provider.Close() - }() - - resp := provider.GetProviderSchema() - if resp.Diagnostics.HasErrors() { - // We'll put a stub in the map so we won't re-attempt this on - // future calls. - schemas[fqn] = &ProviderSchema{} - diags = diags.Append( - fmt.Errorf("Failed to retrieve schema from provider %q: %s", name, resp.Diagnostics.Err()), - ) - return - } - - s := &ProviderSchema{ - Provider: resp.Provider.Block, - ResourceTypes: make(map[string]*configschema.Block), - DataSources: make(map[string]*configschema.Block), - - ResourceTypeSchemaVersions: make(map[string]uint64), - } - - if resp.Provider.Version < 0 { - // We're not using the version numbers here yet, but we'll check - // for validity anyway in case we start using them in future. - diags = diags.Append( - fmt.Errorf("invalid negative schema version provider configuration for provider %q", name), - ) - } - - for t, r := range resp.ResourceTypes { - s.ResourceTypes[t] = r.Block - s.ResourceTypeSchemaVersions[t] = uint64(r.Version) - if r.Version < 0 { - diags = diags.Append( - fmt.Errorf("invalid negative schema version for resource type %s in provider %q", t, name), - ) - } - } - - for t, d := range resp.DataSources { - s.DataSources[t] = d.Block - if d.Version < 0 { - // We're not using the version numbers here yet, but we'll check - // for validity anyway in case we start using them in future. - diags = diags.Append( - fmt.Errorf("invalid negative schema version for data source %s in provider %q", t, name), - ) - } - } - - schemas[fqn] = s - - if resp.ProviderMeta.Block != nil { - s.ProviderMeta = resp.ProviderMeta.Block - } - } - - if config != nil { - for _, fqn := range config.ProviderTypes() { - ensure(fqn) - } - } - - if state != nil { - needed := providers.AddressedTypesAbs(state.ProviderAddrs()) - for _, typeAddr := range needed { - ensure(typeAddr) - } - } - - return diags -} - -func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *configs.Config, components contextComponentFactory) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - ensure := func(name string) { - if _, exists := schemas[name]; exists { - return - } - - log.Printf("[TRACE] LoadSchemas: retrieving schema for provisioner %q", name) - provisioner, err := components.ResourceProvisioner(name) - if err != nil { - // We'll put a stub in the map so we won't re-attempt this on - // future calls. - schemas[name] = &configschema.Block{} - diags = diags.Append( - fmt.Errorf("Failed to instantiate provisioner %q to obtain schema: %s", name, err), - ) - return - } - defer func() { - if closer, ok := provisioner.(ResourceProvisionerCloser); ok { - closer.Close() - } - }() - - resp := provisioner.GetSchema() - if resp.Diagnostics.HasErrors() { - // We'll put a stub in the map so we won't re-attempt this on - // future calls. - schemas[name] = &configschema.Block{} - diags = diags.Append( - fmt.Errorf("Failed to retrieve schema from provisioner %q: %s", name, resp.Diagnostics.Err()), - ) - return - } - - schemas[name] = resp.Provisioner - } - - if config != nil { - for _, rc := range config.Module.ManagedResources { - for _, pc := range rc.Managed.Provisioners { - ensure(pc.Type) - } - } - - // Must also visit our child modules, recursively. - for _, cc := range config.Children { - childDiags := loadProvisionerSchemas(schemas, cc, components) - diags = diags.Append(childDiags) - } - } - - return diags -} - -// ProviderSchema represents the schema for a provider's own configuration -// and the configuration for some or all of its resources and data sources. -// -// The completeness of this structure depends on how it was constructed. -// When constructed for a configuration, it will generally include only -// resource types and data sources used by that configuration. -type ProviderSchema struct { - Provider *configschema.Block - ProviderMeta *configschema.Block - ResourceTypes map[string]*configschema.Block - DataSources map[string]*configschema.Block - - ResourceTypeSchemaVersions map[string]uint64 -} - -// SchemaForResourceType attempts to find a schema for the given mode and type. -// Returns nil if no such schema is available. -func (ps *ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) { - switch mode { - case addrs.ManagedResourceMode: - return ps.ResourceTypes[typeName], ps.ResourceTypeSchemaVersions[typeName] - case addrs.DataResourceMode: - // Data resources don't have schema versions right now, since state is discarded for each refresh - return ps.DataSources[typeName], 0 - default: - // Shouldn't happen, because the above cases are comprehensive. - return nil, 0 - } -} - -// SchemaForResourceAddr attempts to find a schema for the mode and type from -// the given resource address. Returns nil if no such schema is available. -func (ps *ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) { - return ps.SchemaForResourceType(addr.Mode, addr.Type) -} - -// ProviderSchemaRequest is used to describe to a ResourceProvider which -// aspects of schema are required, when calling the GetSchema method. -type ProviderSchemaRequest struct { - ResourceTypes []string - DataSources []string -} diff --git a/internal/legacy/terraform/state.go b/internal/legacy/terraform/state.go index 4d68ac956e..921fede24f 100644 --- a/internal/legacy/terraform/state.go +++ b/internal/legacy/terraform/state.go @@ -1,1615 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "bufio" "bytes" "encoding/json" - "errors" "fmt" - "io" - "io/ioutil" - "log" - "os" - "reflect" "sort" - "strconv" - "strings" "sync" - "github.com/hashicorp/errwrap" - multierror "github.com/hashicorp/go-multierror" - uuid "github.com/hashicorp/go-uuid" - version "github.com/hashicorp/go-version" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/tfdiags" - tfversion "github.com/hashicorp/terraform/version" "github.com/mitchellh/copystructure" "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" ) -const ( - // StateVersion is the current version for our state file - StateVersion = 3 -) - -// rootModulePath is the path of the root module -var rootModulePath = []string{"root"} - -// normalizeModulePath transforms a legacy module path (which may or may not -// have a redundant "root" label at the start of it) into an -// addrs.ModuleInstance representing the same module. -// -// For legacy reasons, different parts of Terraform disagree about whether the -// root module has the path []string{} or []string{"root"}, and so this -// function accepts both and trims off the "root". An implication of this is -// that it's not possible to actually have a module call in the root module -// that is itself named "root", since that would be ambiguous. -// -// normalizeModulePath takes a raw module path and returns a path that -// has the rootModulePath prepended to it. If I could go back in time I -// would've never had a rootModulePath (empty path would be root). We can -// still fix this but thats a big refactor that my branch doesn't make sense -// for. Instead, this function normalizes paths. -func normalizeModulePath(p []string) addrs.ModuleInstance { - // FIXME: Remove this once everyone is using addrs.ModuleInstance. - - if len(p) > 0 && p[0] == "root" { - p = p[1:] - } - - ret := make(addrs.ModuleInstance, len(p)) - for i, name := range p { - // For now we don't actually support modules with multiple instances - // identified by keys, so we just treat every path element as a - // step with no key. - ret[i] = addrs.ModuleInstanceStep{ - Name: name, - } - } - return ret -} - -// State keeps track of a snapshot state-of-the-world that Terraform -// can use to keep track of what real world resources it is actually -// managing. -type State struct { - // Version is the state file protocol version. - Version int `json:"version"` - - // TFVersion is the version of Terraform that wrote this state. - TFVersion string `json:"terraform_version,omitempty"` - - // Serial is incremented on any operation that modifies - // the State file. It is used to detect potentially conflicting - // updates. - Serial int64 `json:"serial"` - - // Lineage is set when a new, blank state is created and then - // never updated. This allows us to determine whether the serials - // of two states can be meaningfully compared. - // Apart from the guarantee that collisions between two lineages - // are very unlikely, this value is opaque and external callers - // should only compare lineage strings byte-for-byte for equality. - Lineage string `json:"lineage"` - - // Remote is used to track the metadata required to - // pull and push state files from a remote storage endpoint. - Remote *RemoteState `json:"remote,omitempty"` - - // Backend tracks the configuration for the backend in use with - // this state. This is used to track any changes in the backend - // configuration. - Backend *BackendState `json:"backend,omitempty"` - - // Modules contains all the modules in a breadth-first order - Modules []*ModuleState `json:"modules"` - - mu sync.Mutex -} - -func (s *State) Lock() { s.mu.Lock() } -func (s *State) Unlock() { s.mu.Unlock() } - -// NewState is used to initialize a blank state -func NewState() *State { - s := &State{} - s.init() - return s -} - -// Children returns the ModuleStates that are direct children of -// the given path. If the path is "root", for example, then children -// returned might be "root.child", but not "root.child.grandchild". -func (s *State) Children(path []string) []*ModuleState { - s.Lock() - defer s.Unlock() - // TODO: test - - return s.children(path) -} - -func (s *State) children(path []string) []*ModuleState { - result := make([]*ModuleState, 0) - for _, m := range s.Modules { - if m == nil { - continue - } - - if len(m.Path) != len(path)+1 { - continue - } - if !reflect.DeepEqual(path, m.Path[:len(path)]) { - continue - } - - result = append(result, m) - } - - return result -} - -// AddModule adds the module with the given path to the state. -// -// This should be the preferred method to add module states since it -// allows us to optimize lookups later as well as control sorting. -func (s *State) AddModule(path addrs.ModuleInstance) *ModuleState { - s.Lock() - defer s.Unlock() - - return s.addModule(path) -} - -func (s *State) addModule(path addrs.ModuleInstance) *ModuleState { - // check if the module exists first - m := s.moduleByPath(path) - if m != nil { - return m - } - - // Lower the new-style address into a legacy-style address. - // This requires that none of the steps have instance keys, which is - // true for all addresses at the time of implementing this because - // "count" and "for_each" are not yet implemented for modules. - // For the purposes of state, the legacy address format also includes - // a redundant extra prefix element "root". It is important to include - // this because the "prune" method will remove any module that has a - // path length less than one, and other parts of the state code will - // trim off the first element indiscriminately. - legacyPath := make([]string, len(path)+1) - legacyPath[0] = "root" - for i, step := range path { - if step.InstanceKey != addrs.NoKey { - // FIXME: Once the rest of Terraform is ready to use count and - // for_each, remove all of this and just write the addrs.ModuleInstance - // value itself into the ModuleState. - panic("state cannot represent modules with count or for_each keys") - } - - legacyPath[i+1] = step.Name - } - - m = &ModuleState{Path: legacyPath} - m.init() - s.Modules = append(s.Modules, m) - s.sort() - return m -} - -// ModuleByPath is used to lookup the module state for the given path. -// This should be the preferred lookup mechanism as it allows for future -// lookup optimizations. -func (s *State) ModuleByPath(path addrs.ModuleInstance) *ModuleState { - if s == nil { - return nil - } - s.Lock() - defer s.Unlock() - - return s.moduleByPath(path) -} - -func (s *State) moduleByPath(path addrs.ModuleInstance) *ModuleState { - for _, mod := range s.Modules { - if mod == nil { - continue - } - if mod.Path == nil { - panic("missing module path") - } - modPath := normalizeModulePath(mod.Path) - if modPath.String() == path.String() { - return mod - } - } - return nil -} - -// Empty returns true if the state is empty. -func (s *State) Empty() bool { - if s == nil { - return true - } - s.Lock() - defer s.Unlock() - - return len(s.Modules) == 0 -} - -// HasResources returns true if the state contains any resources. -// -// This is similar to !s.Empty, but returns true also in the case where the -// state has modules but all of them are devoid of resources. -func (s *State) HasResources() bool { - if s.Empty() { - return false - } - - for _, mod := range s.Modules { - if len(mod.Resources) > 0 { - return true - } - } - - return false -} - -// IsRemote returns true if State represents a state that exists and is -// remote. -func (s *State) IsRemote() bool { - if s == nil { - return false - } - s.Lock() - defer s.Unlock() - - if s.Remote == nil { - return false - } - if s.Remote.Type == "" { - return false - } - - return true -} - -// Validate validates the integrity of this state file. -// -// Certain properties of the statefile are expected by Terraform in order -// to behave properly. The core of Terraform will assume that once it -// receives a State structure that it has been validated. This validation -// check should be called to ensure that. -// -// If this returns an error, then the user should be notified. The error -// response will include detailed information on the nature of the error. -func (s *State) Validate() error { - s.Lock() - defer s.Unlock() - - var result error - - // !!!! FOR DEVELOPERS !!!! - // - // Any errors returned from this Validate function will BLOCK TERRAFORM - // from loading a state file. Therefore, this should only contain checks - // that are only resolvable through manual intervention. - // - // !!!! FOR DEVELOPERS !!!! - - // Make sure there are no duplicate module states. We open a new - // block here so we can use basic variable names and future validations - // can do the same. - { - found := make(map[string]struct{}) - for _, ms := range s.Modules { - if ms == nil { - continue - } - - key := strings.Join(ms.Path, ".") - if _, ok := found[key]; ok { - result = multierror.Append(result, fmt.Errorf( - strings.TrimSpace(stateValidateErrMultiModule), key)) - continue - } - - found[key] = struct{}{} - } - } - - return result -} - -// Remove removes the item in the state at the given address, returning -// any errors that may have occurred. -// -// If the address references a module state or resource, it will delete -// all children as well. To check what will be deleted, use a StateFilter -// first. -func (s *State) Remove(addr ...string) error { - s.Lock() - defer s.Unlock() - - // Filter out what we need to delete - filter := &StateFilter{State: s} - results, err := filter.Filter(addr...) - if err != nil { - return err - } - - // If we have no results, just exit early, we're not going to do anything. - // While what happens below is fairly fast, this is an important early - // exit since the prune below might modify the state more and we don't - // want to modify the state if we don't have to. - if len(results) == 0 { - return nil - } - - // Go through each result and grab what we need - removed := make(map[interface{}]struct{}) - for _, r := range results { - // Convert the path to our own type - path := append([]string{"root"}, r.Path...) - - // If we removed this already, then ignore - if _, ok := removed[r.Value]; ok { - continue - } - - // If we removed the parent already, then ignore - if r.Parent != nil { - if _, ok := removed[r.Parent.Value]; ok { - continue - } - } - - // Add this to the removed list - removed[r.Value] = struct{}{} - - switch v := r.Value.(type) { - case *ModuleState: - s.removeModule(path, v) - case *ResourceState: - s.removeResource(path, v) - case *InstanceState: - s.removeInstance(path, r.Parent.Value.(*ResourceState), v) - default: - return fmt.Errorf("unknown type to delete: %T", r.Value) - } - } - - // Prune since the removal functions often do the bare minimum to - // remove a thing and may leave around dangling empty modules, resources, - // etc. Prune will clean that all up. - s.prune() - - return nil -} - -func (s *State) removeModule(path []string, v *ModuleState) { - for i, m := range s.Modules { - if m == v { - s.Modules, s.Modules[len(s.Modules)-1] = append(s.Modules[:i], s.Modules[i+1:]...), nil - return - } - } -} - -func (s *State) removeResource(path []string, v *ResourceState) { - // Get the module this resource lives in. If it doesn't exist, we're done. - mod := s.moduleByPath(normalizeModulePath(path)) - if mod == nil { - return - } - - // Find this resource. This is a O(N) lookup when if we had the key - // it could be O(1) but even with thousands of resources this shouldn't - // matter right now. We can easily up performance here when the time comes. - for k, r := range mod.Resources { - if r == v { - // Found it - delete(mod.Resources, k) - return - } - } -} - -func (s *State) removeInstance(path []string, r *ResourceState, v *InstanceState) { - // Go through the resource and find the instance that matches this - // (if any) and remove it. - - // Check primary - if r.Primary == v { - r.Primary = nil - return - } - - // Check lists - lists := [][]*InstanceState{r.Deposed} - for _, is := range lists { - for i, instance := range is { - if instance == v { - // Found it, remove it - is, is[len(is)-1] = append(is[:i], is[i+1:]...), nil - - // Done - return - } - } - } -} - -// RootModule returns the ModuleState for the root module -func (s *State) RootModule() *ModuleState { - root := s.ModuleByPath(addrs.RootModuleInstance) - if root == nil { - panic("missing root module") - } - return root -} - -// Equal tests if one state is equal to another. -func (s *State) Equal(other *State) bool { - // If one is nil, we do a direct check - if s == nil || other == nil { - return s == other - } - - s.Lock() - defer s.Unlock() - return s.equal(other) -} - -func (s *State) equal(other *State) bool { - if s == nil || other == nil { - return s == other - } - - // If the versions are different, they're certainly not equal - if s.Version != other.Version { - return false - } - - // If any of the modules are not equal, then this state isn't equal - if len(s.Modules) != len(other.Modules) { - return false - } - for _, m := range s.Modules { - // This isn't very optimal currently but works. - otherM := other.moduleByPath(normalizeModulePath(m.Path)) - if otherM == nil { - return false - } - - // If they're not equal, then we're not equal! - if !m.Equal(otherM) { - return false - } - } - - return true -} - -// MarshalEqual is similar to Equal but provides a stronger definition of -// "equal", where two states are equal if and only if their serialized form -// is byte-for-byte identical. -// -// This is primarily useful for callers that are trying to save snapshots -// of state to persistent storage, allowing them to detect when a new -// snapshot must be taken. -// -// Note that the serial number and lineage are included in the serialized form, -// so it's the caller's responsibility to properly manage these attributes -// so that this method is only called on two states that have the same -// serial and lineage, unless detecting such differences is desired. -func (s *State) MarshalEqual(other *State) bool { - if s == nil && other == nil { - return true - } else if s == nil || other == nil { - return false - } - - recvBuf := &bytes.Buffer{} - otherBuf := &bytes.Buffer{} - - err := WriteState(s, recvBuf) - if err != nil { - // should never happen, since we're writing to a buffer - panic(err) - } - - err = WriteState(other, otherBuf) - if err != nil { - // should never happen, since we're writing to a buffer - panic(err) - } - - return bytes.Equal(recvBuf.Bytes(), otherBuf.Bytes()) -} - -type StateAgeComparison int - -const ( - StateAgeEqual StateAgeComparison = 0 - StateAgeReceiverNewer StateAgeComparison = 1 - StateAgeReceiverOlder StateAgeComparison = -1 -) - -// CompareAges compares one state with another for which is "older". -// -// This is a simple check using the state's serial, and is thus only as -// reliable as the serial itself. In the normal case, only one state -// exists for a given combination of lineage/serial, but Terraform -// does not guarantee this and so the result of this method should be -// used with care. -// -// Returns an integer that is negative if the receiver is older than -// the argument, positive if the converse, and zero if they are equal. -// An error is returned if the two states are not of the same lineage, -// in which case the integer returned has no meaning. -func (s *State) CompareAges(other *State) (StateAgeComparison, error) { - // nil states are "older" than actual states - switch { - case s != nil && other == nil: - return StateAgeReceiverNewer, nil - case s == nil && other != nil: - return StateAgeReceiverOlder, nil - case s == nil && other == nil: - return StateAgeEqual, nil - } - - if !s.SameLineage(other) { - return StateAgeEqual, fmt.Errorf( - "can't compare two states of differing lineage", - ) - } - - s.Lock() - defer s.Unlock() - - switch { - case s.Serial < other.Serial: - return StateAgeReceiverOlder, nil - case s.Serial > other.Serial: - return StateAgeReceiverNewer, nil - default: - return StateAgeEqual, nil - } -} - -// SameLineage returns true only if the state given in argument belongs -// to the same "lineage" of states as the receiver. -func (s *State) SameLineage(other *State) bool { - s.Lock() - defer s.Unlock() - - // If one of the states has no lineage then it is assumed to predate - // this concept, and so we'll accept it as belonging to any lineage - // so that a lineage string can be assigned to newer versions - // without breaking compatibility with older versions. - if s.Lineage == "" || other.Lineage == "" { - return true - } - - return s.Lineage == other.Lineage -} - -// DeepCopy performs a deep copy of the state structure and returns -// a new structure. -func (s *State) DeepCopy() *State { - if s == nil { - return nil - } - - copy, err := copystructure.Config{Lock: true}.Copy(s) - if err != nil { - panic(err) - } - - return copy.(*State) -} - -// FromFutureTerraform checks if this state was written by a Terraform -// version from the future. -func (s *State) FromFutureTerraform() bool { - s.Lock() - defer s.Unlock() - - // No TF version means it is certainly from the past - if s.TFVersion == "" { - return false - } - - v := version.Must(version.NewVersion(s.TFVersion)) - return tfversion.SemVer.LessThan(v) -} - -func (s *State) Init() { - s.Lock() - defer s.Unlock() - s.init() -} - -func (s *State) init() { - if s.Version == 0 { - s.Version = StateVersion - } - - if s.moduleByPath(addrs.RootModuleInstance) == nil { - s.addModule(addrs.RootModuleInstance) - } - s.ensureHasLineage() - - for _, mod := range s.Modules { - if mod != nil { - mod.init() - } - } - - if s.Remote != nil { - s.Remote.init() - } - -} - -func (s *State) EnsureHasLineage() { - s.Lock() - defer s.Unlock() - - s.ensureHasLineage() -} - -func (s *State) ensureHasLineage() { - if s.Lineage == "" { - lineage, err := uuid.GenerateUUID() - if err != nil { - panic(fmt.Errorf("Failed to generate lineage: %v", err)) - } - s.Lineage = lineage - log.Printf("[DEBUG] New state was assigned lineage %q\n", s.Lineage) - } else { - log.Printf("[TRACE] Preserving existing state lineage %q\n", s.Lineage) - } -} - -// AddModuleState insert this module state and override any existing ModuleState -func (s *State) AddModuleState(mod *ModuleState) { - mod.init() - s.Lock() - defer s.Unlock() - - s.addModuleState(mod) -} - -func (s *State) addModuleState(mod *ModuleState) { - for i, m := range s.Modules { - if reflect.DeepEqual(m.Path, mod.Path) { - s.Modules[i] = mod - return - } - } - - s.Modules = append(s.Modules, mod) - s.sort() -} - -// prune is used to remove any resources that are no longer required -func (s *State) prune() { - if s == nil { - return - } - - // Filter out empty modules. - // A module is always assumed to have a path, and it's length isn't always - // bounds checked later on. Modules may be "emptied" during destroy, but we - // never want to store those in the state. - for i := 0; i < len(s.Modules); i++ { - if s.Modules[i] == nil || len(s.Modules[i].Path) == 0 { - s.Modules = append(s.Modules[:i], s.Modules[i+1:]...) - i-- - } - } - - for _, mod := range s.Modules { - mod.prune() - } - if s.Remote != nil && s.Remote.Empty() { - s.Remote = nil - } -} - -// sort sorts the modules -func (s *State) sort() { - sort.Sort(moduleStateSort(s.Modules)) - - // Allow modules to be sorted - for _, m := range s.Modules { - if m != nil { - m.sort() - } - } -} - -func (s *State) String() string { - if s == nil { - return "" - } - s.Lock() - defer s.Unlock() - - var buf bytes.Buffer - for _, m := range s.Modules { - mStr := m.String() - - // If we're the root module, we just write the output directly. - if reflect.DeepEqual(m.Path, rootModulePath) { - buf.WriteString(mStr + "\n") - continue - } - - buf.WriteString(fmt.Sprintf("module.%s:\n", strings.Join(m.Path[1:], "."))) - - s := bufio.NewScanner(strings.NewReader(mStr)) - for s.Scan() { - text := s.Text() - if text != "" { - text = " " + text - } - - buf.WriteString(fmt.Sprintf("%s\n", text)) - } - } - - return strings.TrimSpace(buf.String()) -} - -// BackendState stores the configuration to connect to a remote backend. -type BackendState struct { - Type string `json:"type"` // Backend type - ConfigRaw json.RawMessage `json:"config"` // Backend raw config - Hash uint64 `json:"hash"` // Hash of portion of configuration from config files -} - -// Empty returns true if BackendState has no state. -func (s *BackendState) Empty() bool { - return s == nil || s.Type == "" -} - -// Config decodes the type-specific configuration object using the provided -// schema and returns the result as a cty.Value. -// -// An error is returned if the stored configuration does not conform to the -// given schema. -func (s *BackendState) Config(schema *configschema.Block) (cty.Value, error) { - ty := schema.ImpliedType() - if s == nil { - return cty.NullVal(ty), nil - } - return ctyjson.Unmarshal(s.ConfigRaw, ty) -} - -// SetConfig replaces (in-place) the type-specific configuration object using -// the provided value and associated schema. -// -// An error is returned if the given value does not conform to the implied -// type of the schema. -func (s *BackendState) SetConfig(val cty.Value, schema *configschema.Block) error { - ty := schema.ImpliedType() - buf, err := ctyjson.Marshal(val, ty) - if err != nil { - return err - } - s.ConfigRaw = buf - return nil -} - -// ForPlan produces an alternative representation of the reciever that is -// suitable for storing in a plan. The current workspace must additionally -// be provided, to be stored alongside the backend configuration. -// -// The backend configuration schema is required in order to properly -// encode the backend-specific configuration settings. -func (s *BackendState) ForPlan(schema *configschema.Block, workspaceName string) (*plans.Backend, error) { - if s == nil { - return nil, nil - } - - configVal, err := s.Config(schema) - if err != nil { - return nil, errwrap.Wrapf("failed to decode backend config: {{err}}", err) - } - return plans.NewBackend(s.Type, configVal, schema, workspaceName) -} - -// RemoteState is used to track the information about a remote -// state store that we push/pull state to. -type RemoteState struct { - // Type controls the client we use for the remote state - Type string `json:"type"` - - // Config is used to store arbitrary configuration that - // is type specific - Config map[string]string `json:"config"` - - mu sync.Mutex -} - -func (s *RemoteState) Lock() { s.mu.Lock() } -func (s *RemoteState) Unlock() { s.mu.Unlock() } - -func (r *RemoteState) init() { - r.Lock() - defer r.Unlock() - - if r.Config == nil { - r.Config = make(map[string]string) - } -} - -func (r *RemoteState) deepcopy() *RemoteState { - r.Lock() - defer r.Unlock() - - confCopy := make(map[string]string, len(r.Config)) - for k, v := range r.Config { - confCopy[k] = v - } - return &RemoteState{ - Type: r.Type, - Config: confCopy, - } -} - -func (r *RemoteState) Empty() bool { - if r == nil { - return true - } - r.Lock() - defer r.Unlock() - - return r.Type == "" -} - -func (r *RemoteState) Equals(other *RemoteState) bool { - r.Lock() - defer r.Unlock() - - if r.Type != other.Type { - return false - } - if len(r.Config) != len(other.Config) { - return false - } - for k, v := range r.Config { - if other.Config[k] != v { - return false - } - } - return true -} - -// OutputState is used to track the state relevant to a single output. -type OutputState struct { - // Sensitive describes whether the output is considered sensitive, - // which may lead to masking the value on screen in some cases. - Sensitive bool `json:"sensitive"` - // Type describes the structure of Value. Valid values are "string", - // "map" and "list" - Type string `json:"type"` - // Value contains the value of the output, in the structure described - // by the Type field. - Value interface{} `json:"value"` - - mu sync.Mutex -} - -func (s *OutputState) Lock() { s.mu.Lock() } -func (s *OutputState) Unlock() { s.mu.Unlock() } - -func (s *OutputState) String() string { - return fmt.Sprintf("%#v", s.Value) -} - -// Equal compares two OutputState structures for equality. nil values are -// considered equal. -func (s *OutputState) Equal(other *OutputState) bool { - if s == nil && other == nil { - return true - } - - if s == nil || other == nil { - return false - } - s.Lock() - defer s.Unlock() - - if s.Type != other.Type { - return false - } - - if s.Sensitive != other.Sensitive { - return false - } - - if !reflect.DeepEqual(s.Value, other.Value) { - return false - } - - return true -} - -func (s *OutputState) deepcopy() *OutputState { - if s == nil { - return nil - } - - stateCopy, err := copystructure.Config{Lock: true}.Copy(s) - if err != nil { - panic(fmt.Errorf("Error copying output value: %s", err)) - } - - return stateCopy.(*OutputState) -} - -// ModuleState is used to track all the state relevant to a single -// module. Previous to Terraform 0.3, all state belonged to the "root" -// module. -type ModuleState struct { - // Path is the import path from the root module. Modules imports are - // always disjoint, so the path represents amodule tree - Path []string `json:"path"` - - // Locals are kept only transiently in-memory, because we can always - // re-compute them. - Locals map[string]interface{} `json:"-"` - - // Outputs declared by the module and maintained for each module - // even though only the root module technically needs to be kept. - // This allows operators to inspect values at the boundaries. - Outputs map[string]*OutputState `json:"outputs"` - - // Resources is a mapping of the logically named resource to - // the state of the resource. Each resource may actually have - // N instances underneath, although a user only needs to think - // about the 1:1 case. - Resources map[string]*ResourceState `json:"resources"` - - // Dependencies are a list of things that this module relies on - // existing to remain intact. For example: an module may depend - // on a VPC ID given by an aws_vpc resource. - // - // Terraform uses this information to build valid destruction - // orders and to warn the user if they're destroying a module that - // another resource depends on. - // - // Things can be put into this list that may not be managed by - // Terraform. If Terraform doesn't find a matching ID in the - // overall state, then it assumes it isn't managed and doesn't - // worry about it. - Dependencies []string `json:"depends_on"` - - mu sync.Mutex -} - -func (s *ModuleState) Lock() { s.mu.Lock() } -func (s *ModuleState) Unlock() { s.mu.Unlock() } - -// Equal tests whether one module state is equal to another. -func (m *ModuleState) Equal(other *ModuleState) bool { - m.Lock() - defer m.Unlock() - - // Paths must be equal - if !reflect.DeepEqual(m.Path, other.Path) { - return false - } - - // Outputs must be equal - if len(m.Outputs) != len(other.Outputs) { - return false - } - for k, v := range m.Outputs { - if !other.Outputs[k].Equal(v) { - return false - } - } - - // Dependencies must be equal. This sorts these in place but - // this shouldn't cause any problems. - sort.Strings(m.Dependencies) - sort.Strings(other.Dependencies) - if len(m.Dependencies) != len(other.Dependencies) { - return false - } - for i, d := range m.Dependencies { - if other.Dependencies[i] != d { - return false - } - } - - // Resources must be equal - if len(m.Resources) != len(other.Resources) { - return false - } - for k, r := range m.Resources { - otherR, ok := other.Resources[k] - if !ok { - return false - } - - if !r.Equal(otherR) { - return false - } - } - - return true -} - -// IsRoot says whether or not this module diff is for the root module. -func (m *ModuleState) IsRoot() bool { - m.Lock() - defer m.Unlock() - return reflect.DeepEqual(m.Path, rootModulePath) -} - -// IsDescendent returns true if other is a descendent of this module. -func (m *ModuleState) IsDescendent(other *ModuleState) bool { - m.Lock() - defer m.Unlock() - - i := len(m.Path) - return len(other.Path) > i && reflect.DeepEqual(other.Path[:i], m.Path) -} - -// Orphans returns a list of keys of resources that are in the State -// but aren't present in the configuration itself. Hence, these keys -// represent the state of resources that are orphans. -func (m *ModuleState) Orphans(c *configs.Module) []addrs.ResourceInstance { - m.Lock() - defer m.Unlock() - - inConfig := make(map[string]struct{}) - if c != nil { - for _, r := range c.ManagedResources { - inConfig[r.Addr().String()] = struct{}{} - } - for _, r := range c.DataResources { - inConfig[r.Addr().String()] = struct{}{} - } - } - - var result []addrs.ResourceInstance - for k := range m.Resources { - // Since we've not yet updated state to use our new address format, - // we need to do some shimming here. - legacyAddr, err := parseResourceAddressInternal(k) - if err != nil { - // Suggests that the user tampered with the state, since we always - // generate valid internal addresses. - log.Printf("ModuleState has invalid resource key %q. Ignoring.", k) - continue - } - - addr := legacyAddr.AbsResourceInstanceAddr().Resource - compareKey := addr.Resource.String() // compare by resource address, ignoring instance key - if _, exists := inConfig[compareKey]; !exists { - result = append(result, addr) - } - } - return result -} - -// RemovedOutputs returns a list of outputs that are in the State but aren't -// present in the configuration itself. -func (s *ModuleState) RemovedOutputs(outputs map[string]*configs.Output) []addrs.OutputValue { - if outputs == nil { - // If we got no output map at all then we'll just treat our set of - // configured outputs as empty, since that suggests that they've all - // been removed by removing their containing module. - outputs = make(map[string]*configs.Output) - } - - s.Lock() - defer s.Unlock() - - var ret []addrs.OutputValue - for n := range s.Outputs { - if _, declared := outputs[n]; !declared { - ret = append(ret, addrs.OutputValue{ - Name: n, - }) - } - } - - return ret -} - -// View returns a view with the given resource prefix. -func (m *ModuleState) View(id string) *ModuleState { - if m == nil { - return m - } - - r := m.deepcopy() - for k, _ := range r.Resources { - if id == k || strings.HasPrefix(k, id+".") { - continue - } - - delete(r.Resources, k) - } - - return r -} - -func (m *ModuleState) init() { - m.Lock() - defer m.Unlock() - - if m.Path == nil { - m.Path = []string{} - } - if m.Outputs == nil { - m.Outputs = make(map[string]*OutputState) - } - if m.Resources == nil { - m.Resources = make(map[string]*ResourceState) - } - - if m.Dependencies == nil { - m.Dependencies = make([]string, 0) - } - - for _, rs := range m.Resources { - rs.init() - } -} - -func (m *ModuleState) deepcopy() *ModuleState { - if m == nil { - return nil - } - - stateCopy, err := copystructure.Config{Lock: true}.Copy(m) - if err != nil { - panic(err) - } - - return stateCopy.(*ModuleState) -} - -// prune is used to remove any resources that are no longer required -func (m *ModuleState) prune() { - m.Lock() - defer m.Unlock() - - for k, v := range m.Resources { - if v == nil || (v.Primary == nil || v.Primary.ID == "") && len(v.Deposed) == 0 { - delete(m.Resources, k) - continue - } - - v.prune() - } - - for k, v := range m.Outputs { - if v.Value == hcl2shim.UnknownVariableValue { - delete(m.Outputs, k) - } - } - - m.Dependencies = uniqueStrings(m.Dependencies) -} - -func (m *ModuleState) sort() { - for _, v := range m.Resources { - v.sort() - } -} - -func (m *ModuleState) String() string { - m.Lock() - defer m.Unlock() - - var buf bytes.Buffer - - if len(m.Resources) == 0 { - buf.WriteString("") - } - - names := make([]string, 0, len(m.Resources)) - for name, _ := range m.Resources { - names = append(names, name) - } - - sort.Sort(resourceNameSort(names)) - - for _, k := range names { - rs := m.Resources[k] - var id string - if rs.Primary != nil { - id = rs.Primary.ID - } - if id == "" { - id = "" - } - - taintStr := "" - if rs.Primary.Tainted { - taintStr = " (tainted)" - } - - deposedStr := "" - if len(rs.Deposed) > 0 { - deposedStr = fmt.Sprintf(" (%d deposed)", len(rs.Deposed)) - } - - buf.WriteString(fmt.Sprintf("%s:%s%s\n", k, taintStr, deposedStr)) - buf.WriteString(fmt.Sprintf(" ID = %s\n", id)) - if rs.Provider != "" { - buf.WriteString(fmt.Sprintf(" provider = %s\n", rs.Provider)) - } - - var attributes map[string]string - if rs.Primary != nil { - attributes = rs.Primary.Attributes - } - attrKeys := make([]string, 0, len(attributes)) - for ak, _ := range attributes { - if ak == "id" { - continue - } - - attrKeys = append(attrKeys, ak) - } - - sort.Strings(attrKeys) - - for _, ak := range attrKeys { - av := attributes[ak] - buf.WriteString(fmt.Sprintf(" %s = %s\n", ak, av)) - } - - for idx, t := range rs.Deposed { - taintStr := "" - if t.Tainted { - taintStr = " (tainted)" - } - buf.WriteString(fmt.Sprintf(" Deposed ID %d = %s%s\n", idx+1, t.ID, taintStr)) - } - - if len(rs.Dependencies) > 0 { - buf.WriteString(fmt.Sprintf("\n Dependencies:\n")) - for _, dep := range rs.Dependencies { - buf.WriteString(fmt.Sprintf(" %s\n", dep)) - } - } - } - - if len(m.Outputs) > 0 { - buf.WriteString("\nOutputs:\n\n") - - ks := make([]string, 0, len(m.Outputs)) - for k, _ := range m.Outputs { - ks = append(ks, k) - } - - sort.Strings(ks) - - for _, k := range ks { - v := m.Outputs[k] - switch vTyped := v.Value.(type) { - case string: - buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) - case []interface{}: - buf.WriteString(fmt.Sprintf("%s = %s\n", k, vTyped)) - case map[string]interface{}: - var mapKeys []string - for key, _ := range vTyped { - mapKeys = append(mapKeys, key) - } - sort.Strings(mapKeys) - - var mapBuf bytes.Buffer - mapBuf.WriteString("{") - for _, key := range mapKeys { - mapBuf.WriteString(fmt.Sprintf("%s:%s ", key, vTyped[key])) - } - mapBuf.WriteString("}") - - buf.WriteString(fmt.Sprintf("%s = %s\n", k, mapBuf.String())) - } - } - } - - return buf.String() -} - -func (m *ModuleState) Empty() bool { - return len(m.Locals) == 0 && len(m.Outputs) == 0 && len(m.Resources) == 0 -} - -// ResourceStateKey is a structured representation of the key used for the -// ModuleState.Resources mapping -type ResourceStateKey struct { - Name string - Type string - Mode ResourceMode - Index int -} - -// Equal determines whether two ResourceStateKeys are the same -func (rsk *ResourceStateKey) Equal(other *ResourceStateKey) bool { - if rsk == nil || other == nil { - return false - } - if rsk.Mode != other.Mode { - return false - } - if rsk.Type != other.Type { - return false - } - if rsk.Name != other.Name { - return false - } - if rsk.Index != other.Index { - return false - } - return true -} - -func (rsk *ResourceStateKey) String() string { - if rsk == nil { - return "" - } - var prefix string - switch rsk.Mode { - case ManagedResourceMode: - prefix = "" - case DataResourceMode: - prefix = "data." - default: - panic(fmt.Errorf("unknown resource mode %s", rsk.Mode)) - } - if rsk.Index == -1 { - return fmt.Sprintf("%s%s.%s", prefix, rsk.Type, rsk.Name) - } - return fmt.Sprintf("%s%s.%s.%d", prefix, rsk.Type, rsk.Name, rsk.Index) -} - -// ParseResourceStateKey accepts a key in the format used by -// ModuleState.Resources and returns a resource name and resource index. In the -// state, a resource has the format "type.name.index" or "type.name". In the -// latter case, the index is returned as -1. -func ParseResourceStateKey(k string) (*ResourceStateKey, error) { - parts := strings.Split(k, ".") - mode := ManagedResourceMode - if len(parts) > 0 && parts[0] == "data" { - mode = DataResourceMode - // Don't need the constant "data" prefix for parsing - // now that we've figured out the mode. - parts = parts[1:] - } - if len(parts) < 2 || len(parts) > 3 { - return nil, fmt.Errorf("Malformed resource state key: %s", k) - } - rsk := &ResourceStateKey{ - Mode: mode, - Type: parts[0], - Name: parts[1], - Index: -1, - } - if len(parts) == 3 { - index, err := strconv.Atoi(parts[2]) - if err != nil { - return nil, fmt.Errorf("Malformed resource state key index: %s", k) - } - rsk.Index = index - } - return rsk, nil -} - -// ResourceState holds the state of a resource that is used so that -// a provider can find and manage an existing resource as well as for -// storing attributes that are used to populate variables of child -// resources. -// -// Attributes has attributes about the created resource that are -// queryable in interpolation: "${type.id.attr}" -// -// Extra is just extra data that a provider can return that we store -// for later, but is not exposed in any way to the user. -type ResourceState struct { - // This is filled in and managed by Terraform, and is the resource - // type itself such as "mycloud_instance". If a resource provider sets - // this value, it won't be persisted. - Type string `json:"type"` - - // Dependencies are a list of things that this resource relies on - // existing to remain intact. For example: an AWS instance might - // depend on a subnet (which itself might depend on a VPC, and so - // on). - // - // Terraform uses this information to build valid destruction - // orders and to warn the user if they're destroying a resource that - // another resource depends on. - // - // Things can be put into this list that may not be managed by - // Terraform. If Terraform doesn't find a matching ID in the - // overall state, then it assumes it isn't managed and doesn't - // worry about it. - Dependencies []string `json:"depends_on"` - - // Primary is the current active instance for this resource. - // It can be replaced but only after a successful creation. - // This is the instances on which providers will act. - Primary *InstanceState `json:"primary"` - - // Deposed is used in the mechanics of CreateBeforeDestroy: the existing - // Primary is Deposed to get it out of the way for the replacement Primary to - // be created by Apply. If the replacement Primary creates successfully, the - // Deposed instance is cleaned up. - // - // If there were problems creating the replacement Primary, the Deposed - // instance and the (now tainted) replacement Primary will be swapped so the - // tainted replacement will be cleaned up instead. - // - // An instance will remain in the Deposed list until it is successfully - // destroyed and purged. - Deposed []*InstanceState `json:"deposed"` - - // Provider is used when a resource is connected to a provider with an alias. - // If this string is empty, the resource is connected to the default provider, - // e.g. "aws_instance" goes with the "aws" provider. - // If the resource block contained a "provider" key, that value will be set here. - Provider string `json:"provider"` - - mu sync.Mutex -} - -func (s *ResourceState) Lock() { s.mu.Lock() } -func (s *ResourceState) Unlock() { s.mu.Unlock() } - -// Equal tests whether two ResourceStates are equal. -func (s *ResourceState) Equal(other *ResourceState) bool { - s.Lock() - defer s.Unlock() - - if s.Type != other.Type { - return false - } - - if s.Provider != other.Provider { - return false - } - - // Dependencies must be equal - sort.Strings(s.Dependencies) - sort.Strings(other.Dependencies) - if len(s.Dependencies) != len(other.Dependencies) { - return false - } - for i, d := range s.Dependencies { - if other.Dependencies[i] != d { - return false - } - } - - // States must be equal - if !s.Primary.Equal(other.Primary) { - return false - } - - return true -} - -// Taint marks a resource as tainted. -func (s *ResourceState) Taint() { - s.Lock() - defer s.Unlock() - - if s.Primary != nil { - s.Primary.Tainted = true - } -} - -// Untaint unmarks a resource as tainted. -func (s *ResourceState) Untaint() { - s.Lock() - defer s.Unlock() - - if s.Primary != nil { - s.Primary.Tainted = false - } -} - -// ProviderAddr returns the provider address for the receiver, by parsing the -// string representation saved in state. An error can be returned if the -// value in state is corrupt. -func (s *ResourceState) ProviderAddr() (addrs.AbsProviderConfig, error) { - var diags tfdiags.Diagnostics - - str := s.Provider - traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) - diags = diags.Append(travDiags) - if travDiags.HasErrors() { - return addrs.AbsProviderConfig{}, diags.Err() - } - - addr, addrDiags := addrs.ParseAbsProviderConfig(traversal) - diags = diags.Append(addrDiags) - return addr, diags.Err() -} - -func (s *ResourceState) init() { - s.Lock() - defer s.Unlock() - - if s.Primary == nil { - s.Primary = &InstanceState{} - } - s.Primary.init() - - if s.Dependencies == nil { - s.Dependencies = []string{} - } - - if s.Deposed == nil { - s.Deposed = make([]*InstanceState, 0) - } -} - -func (s *ResourceState) deepcopy() *ResourceState { - copy, err := copystructure.Config{Lock: true}.Copy(s) - if err != nil { - panic(err) - } - - return copy.(*ResourceState) -} - -// prune is used to remove any instances that are no longer required -func (s *ResourceState) prune() { - s.Lock() - defer s.Unlock() - - n := len(s.Deposed) - for i := 0; i < n; i++ { - inst := s.Deposed[i] - if inst == nil || inst.ID == "" { - copy(s.Deposed[i:], s.Deposed[i+1:]) - s.Deposed[n-1] = nil - n-- - i-- - } - } - s.Deposed = s.Deposed[:n] - - s.Dependencies = uniqueStrings(s.Dependencies) -} - -func (s *ResourceState) sort() { - s.Lock() - defer s.Unlock() - - sort.Strings(s.Dependencies) -} - -func (s *ResourceState) String() string { - s.Lock() - defer s.Unlock() - - var buf bytes.Buffer - buf.WriteString(fmt.Sprintf("Type = %s", s.Type)) - return buf.String() -} - // InstanceState is used to track the unique state information belonging // to a given instance. type InstanceState struct { @@ -1904,351 +309,3 @@ func (e *EphemeralState) DeepCopy() *EphemeralState { return copy.(*EphemeralState) } - -type jsonStateVersionIdentifier struct { - Version int `json:"version"` -} - -// Check if this is a V0 format - the magic bytes at the start of the file -// should be "tfstate" if so. We no longer support upgrading this type of -// state but return an error message explaining to a user how they can -// upgrade via the 0.6.x series. -func testForV0State(buf *bufio.Reader) error { - start, err := buf.Peek(len("tfstate")) - if err != nil { - return fmt.Errorf("Failed to check for magic bytes: %v", err) - } - if string(start) == "tfstate" { - return fmt.Errorf("Terraform 0.7 no longer supports upgrading the binary state\n" + - "format which was used prior to Terraform 0.3. Please upgrade\n" + - "this state file using Terraform 0.6.16 prior to using it with\n" + - "Terraform 0.7.") - } - - return nil -} - -// ErrNoState is returned by ReadState when the io.Reader contains no data -var ErrNoState = errors.New("no state") - -// ReadState reads a state structure out of a reader in the format that -// was written by WriteState. -func ReadState(src io.Reader) (*State, error) { - // check for a nil file specifically, since that produces a platform - // specific error if we try to use it in a bufio.Reader. - if f, ok := src.(*os.File); ok && f == nil { - return nil, ErrNoState - } - - buf := bufio.NewReader(src) - - if _, err := buf.Peek(1); err != nil { - if err == io.EOF { - return nil, ErrNoState - } - return nil, err - } - - if err := testForV0State(buf); err != nil { - return nil, err - } - - // If we are JSON we buffer the whole thing in memory so we can read it twice. - // This is suboptimal, but will work for now. - jsonBytes, err := ioutil.ReadAll(buf) - if err != nil { - return nil, fmt.Errorf("Reading state file failed: %v", err) - } - - versionIdentifier := &jsonStateVersionIdentifier{} - if err := json.Unmarshal(jsonBytes, versionIdentifier); err != nil { - return nil, fmt.Errorf("Decoding state file version failed: %v", err) - } - - var result *State - switch versionIdentifier.Version { - case 0: - return nil, fmt.Errorf("State version 0 is not supported as JSON.") - case 1: - v1State, err := ReadStateV1(jsonBytes) - if err != nil { - return nil, err - } - - v2State, err := upgradeStateV1ToV2(v1State) - if err != nil { - return nil, err - } - - v3State, err := upgradeStateV2ToV3(v2State) - if err != nil { - return nil, err - } - - // increment the Serial whenever we upgrade state - v3State.Serial++ - result = v3State - case 2: - v2State, err := ReadStateV2(jsonBytes) - if err != nil { - return nil, err - } - v3State, err := upgradeStateV2ToV3(v2State) - if err != nil { - return nil, err - } - - v3State.Serial++ - result = v3State - case 3: - v3State, err := ReadStateV3(jsonBytes) - if err != nil { - return nil, err - } - - result = v3State - default: - return nil, fmt.Errorf("Terraform %s does not support state version %d, please update.", - tfversion.SemVer.String(), versionIdentifier.Version) - } - - // If we reached this place we must have a result set - if result == nil { - panic("resulting state in load not set, assertion failed") - } - - // Prune the state when read it. Its possible to write unpruned states or - // for a user to make a state unpruned (nil-ing a module state for example). - result.prune() - - // Validate the state file is valid - if err := result.Validate(); err != nil { - return nil, err - } - - return result, nil -} - -func ReadStateV1(jsonBytes []byte) (*stateV1, error) { - v1State := &stateV1{} - if err := json.Unmarshal(jsonBytes, v1State); err != nil { - return nil, fmt.Errorf("Decoding state file failed: %v", err) - } - - if v1State.Version != 1 { - return nil, fmt.Errorf("Decoded state version did not match the decoder selection: "+ - "read %d, expected 1", v1State.Version) - } - - return v1State, nil -} - -func ReadStateV2(jsonBytes []byte) (*State, error) { - state := &State{} - if err := json.Unmarshal(jsonBytes, state); err != nil { - return nil, fmt.Errorf("Decoding state file failed: %v", err) - } - - // Check the version, this to ensure we don't read a future - // version that we don't understand - if state.Version > StateVersion { - return nil, fmt.Errorf("Terraform %s does not support state version %d, please update.", - tfversion.SemVer.String(), state.Version) - } - - // Make sure the version is semantic - if state.TFVersion != "" { - if _, err := version.NewVersion(state.TFVersion); err != nil { - return nil, fmt.Errorf( - "State contains invalid version: %s\n\n"+ - "Terraform validates the version format prior to writing it. This\n"+ - "means that this is invalid of the state becoming corrupted through\n"+ - "some external means. Please manually modify the Terraform version\n"+ - "field to be a proper semantic version.", - state.TFVersion) - } - } - - // catch any unitialized fields in the state - state.init() - - // Sort it - state.sort() - - return state, nil -} - -func ReadStateV3(jsonBytes []byte) (*State, error) { - state := &State{} - if err := json.Unmarshal(jsonBytes, state); err != nil { - return nil, fmt.Errorf("Decoding state file failed: %v", err) - } - - // Check the version, this to ensure we don't read a future - // version that we don't understand - if state.Version > StateVersion { - return nil, fmt.Errorf("Terraform %s does not support state version %d, please update.", - tfversion.SemVer.String(), state.Version) - } - - // Make sure the version is semantic - if state.TFVersion != "" { - if _, err := version.NewVersion(state.TFVersion); err != nil { - return nil, fmt.Errorf( - "State contains invalid version: %s\n\n"+ - "Terraform validates the version format prior to writing it. This\n"+ - "means that this is invalid of the state becoming corrupted through\n"+ - "some external means. Please manually modify the Terraform version\n"+ - "field to be a proper semantic version.", - state.TFVersion) - } - } - - // catch any unitialized fields in the state - state.init() - - // Sort it - state.sort() - - // Now we write the state back out to detect any changes in normaliztion. - // If our state is now written out differently, bump the serial number to - // prevent conflicts. - var buf bytes.Buffer - err := WriteState(state, &buf) - if err != nil { - return nil, err - } - - if !bytes.Equal(jsonBytes, buf.Bytes()) { - log.Println("[INFO] state modified during read or write. incrementing serial number") - state.Serial++ - } - - return state, nil -} - -// WriteState writes a state somewhere in a binary format. -func WriteState(d *State, dst io.Writer) error { - // writing a nil state is a noop. - if d == nil { - return nil - } - - // make sure we have no uninitialized fields - d.init() - - // Make sure it is sorted - d.sort() - - // Ensure the version is set - d.Version = StateVersion - - // If the TFVersion is set, verify it. We used to just set the version - // here, but this isn't safe since it changes the MD5 sum on some remote - // state storage backends such as Atlas. We now leave it be if needed. - if d.TFVersion != "" { - if _, err := version.NewVersion(d.TFVersion); err != nil { - return fmt.Errorf( - "Error writing state, invalid version: %s\n\n"+ - "The Terraform version when writing the state must be a semantic\n"+ - "version.", - d.TFVersion) - } - } - - // Encode the data in a human-friendly way - data, err := json.MarshalIndent(d, "", " ") - if err != nil { - return fmt.Errorf("Failed to encode state: %s", err) - } - - // We append a newline to the data because MarshalIndent doesn't - data = append(data, '\n') - - // Write the data out to the dst - if _, err := io.Copy(dst, bytes.NewReader(data)); err != nil { - return fmt.Errorf("Failed to write state: %v", err) - } - - return nil -} - -// resourceNameSort implements the sort.Interface to sort name parts lexically for -// strings and numerically for integer indexes. -type resourceNameSort []string - -func (r resourceNameSort) Len() int { return len(r) } -func (r resourceNameSort) Swap(i, j int) { r[i], r[j] = r[j], r[i] } - -func (r resourceNameSort) Less(i, j int) bool { - iParts := strings.Split(r[i], ".") - jParts := strings.Split(r[j], ".") - - end := len(iParts) - if len(jParts) < end { - end = len(jParts) - } - - for idx := 0; idx < end; idx++ { - if iParts[idx] == jParts[idx] { - continue - } - - // sort on the first non-matching part - iInt, iIntErr := strconv.Atoi(iParts[idx]) - jInt, jIntErr := strconv.Atoi(jParts[idx]) - - switch { - case iIntErr == nil && jIntErr == nil: - // sort numerically if both parts are integers - return iInt < jInt - case iIntErr == nil: - // numbers sort before strings - return true - case jIntErr == nil: - return false - default: - return iParts[idx] < jParts[idx] - } - } - - return r[i] < r[j] -} - -// moduleStateSort implements sort.Interface to sort module states -type moduleStateSort []*ModuleState - -func (s moduleStateSort) Len() int { - return len(s) -} - -func (s moduleStateSort) Less(i, j int) bool { - a := s[i] - b := s[j] - - // If either is nil, then the nil one is "less" than - if a == nil || b == nil { - return a == nil - } - - // If the lengths are different, then the shorter one always wins - if len(a.Path) != len(b.Path) { - return len(a.Path) < len(b.Path) - } - - // Otherwise, compare lexically - return strings.Join(a.Path, ".") < strings.Join(b.Path, ".") -} - -func (s moduleStateSort) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -const stateValidateErrMultiModule = ` -Multiple modules with the same path: %s - -This means that there are multiple entries in the "modules" field -in your state file that point to the same module. This will cause Terraform -to behave in unexpected and error prone ways and is invalid. Please back up -and modify your state file manually to resolve this. -` diff --git a/internal/legacy/terraform/state_filter.go b/internal/legacy/terraform/state_filter.go deleted file mode 100644 index 2dcb11b76b..0000000000 --- a/internal/legacy/terraform/state_filter.go +++ /dev/null @@ -1,267 +0,0 @@ -package terraform - -import ( - "fmt" - "sort" -) - -// StateFilter is responsible for filtering and searching a state. -// -// This is a separate struct from State rather than a method on State -// because StateFilter might create sidecar data structures to optimize -// filtering on the state. -// -// If you change the State, the filter created is invalid and either -// Reset should be called or a new one should be allocated. StateFilter -// will not watch State for changes and do this for you. If you filter after -// changing the State without calling Reset, the behavior is not defined. -type StateFilter struct { - State *State -} - -// Filter takes the addresses specified by fs and finds all the matches. -// The values of fs are resource addressing syntax that can be parsed by -// ParseResourceAddress. -func (f *StateFilter) Filter(fs ...string) ([]*StateFilterResult, error) { - // Parse all the addresses - as := make([]*ResourceAddress, len(fs)) - for i, v := range fs { - a, err := ParseResourceAddress(v) - if err != nil { - return nil, fmt.Errorf("Error parsing address '%s': %s", v, err) - } - - as[i] = a - } - - // If we weren't given any filters, then we list all - if len(fs) == 0 { - as = append(as, &ResourceAddress{Index: -1}) - } - - // Filter each of the address. We keep track of this in a map to - // strip duplicates. - resultSet := make(map[string]*StateFilterResult) - for _, a := range as { - for _, r := range f.filterSingle(a) { - resultSet[r.String()] = r - } - } - - // Make the result list - results := make([]*StateFilterResult, 0, len(resultSet)) - for _, v := range resultSet { - results = append(results, v) - } - - // Sort them and return - sort.Sort(StateFilterResultSlice(results)) - return results, nil -} - -func (f *StateFilter) filterSingle(a *ResourceAddress) []*StateFilterResult { - // The slice to keep track of results - var results []*StateFilterResult - - // Go through modules first. - modules := make([]*ModuleState, 0, len(f.State.Modules)) - for _, m := range f.State.Modules { - if f.relevant(a, m) { - modules = append(modules, m) - - // Only add the module to the results if we haven't specified a type. - // We also ignore the root module. - if a.Type == "" && len(m.Path) > 1 { - results = append(results, &StateFilterResult{ - Path: m.Path[1:], - Address: (&ResourceAddress{Path: m.Path[1:]}).String(), - Value: m, - }) - } - } - } - - // With the modules set, go through all the resources within - // the modules to find relevant resources. - for _, m := range modules { - for n, r := range m.Resources { - // The name in the state contains valuable information. Parse. - key, err := ParseResourceStateKey(n) - if err != nil { - // If we get an error parsing, then just ignore it - // out of the state. - continue - } - - // Older states and test fixtures often don't contain the - // type directly on the ResourceState. We add this so StateFilter - // is a bit more robust. - if r.Type == "" { - r.Type = key.Type - } - - if f.relevant(a, r) { - if a.Name != "" && a.Name != key.Name { - // Name doesn't match - continue - } - - if a.Index >= 0 && key.Index != a.Index { - // Index doesn't match - continue - } - - if a.Name != "" && a.Name != key.Name { - continue - } - - // Build the address for this resource - addr := &ResourceAddress{ - Path: m.Path[1:], - Name: key.Name, - Type: key.Type, - Index: key.Index, - } - - // Add the resource level result - resourceResult := &StateFilterResult{ - Path: addr.Path, - Address: addr.String(), - Value: r, - } - if !a.InstanceTypeSet { - results = append(results, resourceResult) - } - - // Add the instances - if r.Primary != nil { - addr.InstanceType = TypePrimary - addr.InstanceTypeSet = false - results = append(results, &StateFilterResult{ - Path: addr.Path, - Address: addr.String(), - Parent: resourceResult, - Value: r.Primary, - }) - } - - for _, instance := range r.Deposed { - if f.relevant(a, instance) { - addr.InstanceType = TypeDeposed - addr.InstanceTypeSet = true - results = append(results, &StateFilterResult{ - Path: addr.Path, - Address: addr.String(), - Parent: resourceResult, - Value: instance, - }) - } - } - } - } - } - - return results -} - -// relevant checks for relevance of this address against the given value. -func (f *StateFilter) relevant(addr *ResourceAddress, raw interface{}) bool { - switch v := raw.(type) { - case *ModuleState: - path := v.Path[1:] - - if len(addr.Path) > len(path) { - // Longer path in address means there is no way we match. - return false - } - - // Check for a prefix match - for i, p := range addr.Path { - if path[i] != p { - // Any mismatches don't match. - return false - } - } - - return true - case *ResourceState: - if addr.Type == "" { - // If we have no resource type, then we're interested in all! - return true - } - - // If the type doesn't match we fail immediately - if v.Type != addr.Type { - return false - } - - return true - default: - // If we don't know about it, let's just say no - return false - } -} - -// StateFilterResult is a single result from a filter operation. Filter -// can match multiple things within a state (module, resource, instance, etc.) -// and this unifies that. -type StateFilterResult struct { - // Module path of the result - Path []string - - // Address is the address that can be used to reference this exact result. - Address string - - // Parent, if non-nil, is a parent of this result. For instances, the - // parent would be a resource. For resources, the parent would be - // a module. For modules, this is currently nil. - Parent *StateFilterResult - - // Value is the actual value. This must be type switched on. It can be - // any data structures that `State` can hold: `ModuleState`, - // `ResourceState`, `InstanceState`. - Value interface{} -} - -func (r *StateFilterResult) String() string { - return fmt.Sprintf("%T: %s", r.Value, r.Address) -} - -func (r *StateFilterResult) sortedType() int { - switch r.Value.(type) { - case *ModuleState: - return 0 - case *ResourceState: - return 1 - case *InstanceState: - return 2 - default: - return 50 - } -} - -// StateFilterResultSlice is a slice of results that implements -// sort.Interface. The sorting goal is what is most appealing to -// human output. -type StateFilterResultSlice []*StateFilterResult - -func (s StateFilterResultSlice) Len() int { return len(s) } -func (s StateFilterResultSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s StateFilterResultSlice) Less(i, j int) bool { - a, b := s[i], s[j] - - // if these address contain an index, we want to sort by index rather than name - addrA, errA := ParseResourceAddress(a.Address) - addrB, errB := ParseResourceAddress(b.Address) - if errA == nil && errB == nil && addrA.Name == addrB.Name && addrA.Index != addrB.Index { - return addrA.Index < addrB.Index - } - - // If the addresses are different it is just lexographic sorting - if a.Address != b.Address { - return a.Address < b.Address - } - - // Addresses are the same, which means it matters on the type - return a.sortedType() < b.sortedType() -} diff --git a/internal/legacy/terraform/state_test.go b/internal/legacy/terraform/state_test.go deleted file mode 100644 index 1edbfb6912..0000000000 --- a/internal/legacy/terraform/state_test.go +++ /dev/null @@ -1,1894 +0,0 @@ -package terraform - -import ( - "bytes" - "encoding/json" - "fmt" - "os" - "reflect" - "sort" - "strings" - "testing" - - "github.com/davecgh/go-spew/spew" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/hcl2shim" -) - -func TestStateValidate(t *testing.T) { - cases := map[string]struct { - In *State - Err bool - }{ - "empty state": { - &State{}, - false, - }, - - "multiple modules": { - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root", "foo"}, - }, - &ModuleState{ - Path: []string{"root", "foo"}, - }, - }, - }, - true, - }, - } - - for name, tc := range cases { - // Init the state - tc.In.init() - - err := tc.In.Validate() - if (err != nil) != tc.Err { - t.Fatalf("%s: err: %s", name, err) - } - } -} - -func TestStateAddModule(t *testing.T) { - cases := []struct { - In []addrs.ModuleInstance - Out [][]string - }{ - { - []addrs.ModuleInstance{ - addrs.RootModuleInstance, - addrs.RootModuleInstance.Child("child", addrs.NoKey), - }, - [][]string{ - []string{"root"}, - []string{"root", "child"}, - }, - }, - - { - []addrs.ModuleInstance{ - addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("bar", addrs.NoKey), - addrs.RootModuleInstance.Child("foo", addrs.NoKey), - addrs.RootModuleInstance, - addrs.RootModuleInstance.Child("bar", addrs.NoKey), - }, - [][]string{ - []string{"root"}, - []string{"root", "bar"}, - []string{"root", "foo"}, - []string{"root", "foo", "bar"}, - }, - }, - // Same last element, different middle element - { - []addrs.ModuleInstance{ - addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("bar", addrs.NoKey), // This one should sort after... - addrs.RootModuleInstance.Child("foo", addrs.NoKey), - addrs.RootModuleInstance, - addrs.RootModuleInstance.Child("bar", addrs.NoKey).Child("bar", addrs.NoKey), // ...this one. - addrs.RootModuleInstance.Child("bar", addrs.NoKey), - }, - [][]string{ - []string{"root"}, - []string{"root", "bar"}, - []string{"root", "foo"}, - []string{"root", "bar", "bar"}, - []string{"root", "foo", "bar"}, - }, - }, - } - - for _, tc := range cases { - s := new(State) - for _, p := range tc.In { - s.AddModule(p) - } - - actual := make([][]string, 0, len(tc.In)) - for _, m := range s.Modules { - actual = append(actual, m.Path) - } - - if !reflect.DeepEqual(actual, tc.Out) { - t.Fatalf("wrong result\ninput: %sgot: %#v\nwant: %#v", spew.Sdump(tc.In), actual, tc.Out) - } - } -} - -func TestStateOutputTypeRoundTrip(t *testing.T) { - state := &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - Outputs: map[string]*OutputState{ - "string_output": &OutputState{ - Value: "String Value", - Type: "string", - }, - }, - }, - }, - } - state.init() - - buf := new(bytes.Buffer) - if err := WriteState(state, buf); err != nil { - t.Fatalf("err: %s", err) - } - - roundTripped, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(state, roundTripped) { - t.Logf("expected:\n%#v", state) - t.Fatalf("got:\n%#v", roundTripped) - } -} - -func TestStateDeepCopy(t *testing.T) { - cases := []struct { - State *State - }{ - // Nil - {nil}, - - // Version - { - &State{Version: 5}, - }, - // TFVersion - { - &State{TFVersion: "5"}, - }, - // Modules - { - &State{ - Version: 6, - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{}, - }, - }, - }, - }, - }, - }, - }, - // Deposed - // The nil values shouldn't be there if the State was properly init'ed, - // but the Copy should still work anyway. - { - &State{ - Version: 6, - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{}, - }, - Deposed: []*InstanceState{ - {ID: "test"}, - nil, - }, - }, - }, - }, - }, - }, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("copy-%d", i), func(t *testing.T) { - actual := tc.State.DeepCopy() - expected := tc.State - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("Expected: %#v\nRecevied: %#v\n", expected, actual) - } - }) - } -} - -func TestStateEqual(t *testing.T) { - cases := []struct { - Name string - Result bool - One, Two *State - }{ - // Nils - { - "one nil", - false, - nil, - &State{Version: 2}, - }, - - { - "both nil", - true, - nil, - nil, - }, - - // Different versions - { - "different state versions", - false, - &State{Version: 5}, - &State{Version: 2}, - }, - - // Different modules - { - "different module states", - false, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - }, - }, - }, - &State{}, - }, - - { - "same module states", - true, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: []string{"root"}, - }, - }, - }, - }, - - // Meta differs - { - "differing meta values with primitives", - false, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{ - "schema_version": "1", - }, - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{ - "schema_version": "2", - }, - }, - }, - }, - }, - }, - }, - }, - - // Meta with complex types - { - "same meta with complex types", - true, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{ - "timeouts": map[string]interface{}{ - "create": 42, - "read": "27", - }, - }, - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{ - "timeouts": map[string]interface{}{ - "create": 42, - "read": "27", - }, - }, - }, - }, - }, - }, - }, - }, - }, - - // Meta with complex types that have been altered during serialization - { - "same meta with complex types that have been json-ified", - true, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{ - "timeouts": map[string]interface{}{ - "create": int(42), - "read": "27", - }, - }, - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Primary: &InstanceState{ - Meta: map[string]interface{}{ - "timeouts": map[string]interface{}{ - "create": float64(42), - "read": "27", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - if tc.One.Equal(tc.Two) != tc.Result { - t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) - } - if tc.Two.Equal(tc.One) != tc.Result { - t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) - } - }) - } -} - -func TestStateCompareAges(t *testing.T) { - cases := []struct { - Result StateAgeComparison - Err bool - One, Two *State - }{ - { - StateAgeEqual, false, - &State{ - Lineage: "1", - Serial: 2, - }, - &State{ - Lineage: "1", - Serial: 2, - }, - }, - { - StateAgeReceiverOlder, false, - &State{ - Lineage: "1", - Serial: 2, - }, - &State{ - Lineage: "1", - Serial: 3, - }, - }, - { - StateAgeReceiverNewer, false, - &State{ - Lineage: "1", - Serial: 3, - }, - &State{ - Lineage: "1", - Serial: 2, - }, - }, - { - StateAgeEqual, true, - &State{ - Lineage: "1", - Serial: 2, - }, - &State{ - Lineage: "2", - Serial: 2, - }, - }, - { - StateAgeEqual, true, - &State{ - Lineage: "1", - Serial: 3, - }, - &State{ - Lineage: "2", - Serial: 2, - }, - }, - } - - for i, tc := range cases { - result, err := tc.One.CompareAges(tc.Two) - - if err != nil && !tc.Err { - t.Errorf( - "%d: got error, but want success\n\n%s\n\n%s", - i, tc.One, tc.Two, - ) - continue - } - - if err == nil && tc.Err { - t.Errorf( - "%d: got success, but want error\n\n%s\n\n%s", - i, tc.One, tc.Two, - ) - continue - } - - if result != tc.Result { - t.Errorf( - "%d: got result %d, but want %d\n\n%s\n\n%s", - i, result, tc.Result, tc.One, tc.Two, - ) - continue - } - } -} - -func TestStateSameLineage(t *testing.T) { - cases := []struct { - Result bool - One, Two *State - }{ - { - true, - &State{ - Lineage: "1", - }, - &State{ - Lineage: "1", - }, - }, - { - // Empty lineage is compatible with all - true, - &State{ - Lineage: "", - }, - &State{ - Lineage: "1", - }, - }, - { - // Empty lineage is compatible with all - true, - &State{ - Lineage: "1", - }, - &State{ - Lineage: "", - }, - }, - { - false, - &State{ - Lineage: "1", - }, - &State{ - Lineage: "2", - }, - }, - } - - for i, tc := range cases { - result := tc.One.SameLineage(tc.Two) - - if result != tc.Result { - t.Errorf( - "%d: got %v, but want %v\n\n%s\n\n%s", - i, result, tc.Result, tc.One, tc.Two, - ) - continue - } - } -} - -func TestStateMarshalEqual(t *testing.T) { - tests := map[string]struct { - S1, S2 *State - Want bool - }{ - "both nil": { - nil, - nil, - true, - }, - "first zero, second nil": { - &State{}, - nil, - false, - }, - "first nil, second zero": { - nil, - &State{}, - false, - }, - "both zero": { - // These are not equal because they both implicitly init with - // different lineage. - &State{}, - &State{}, - false, - }, - "both set, same lineage": { - &State{ - Lineage: "abc123", - }, - &State{ - Lineage: "abc123", - }, - true, - }, - "both set, same lineage, different serial": { - &State{ - Lineage: "abc123", - Serial: 1, - }, - &State{ - Lineage: "abc123", - Serial: 2, - }, - false, - }, - "both set, same lineage, same serial, same resources": { - &State{ - Lineage: "abc123", - Serial: 1, - Modules: []*ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "foo_bar.baz": {}, - }, - }, - }, - }, - &State{ - Lineage: "abc123", - Serial: 1, - Modules: []*ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "foo_bar.baz": {}, - }, - }, - }, - }, - true, - }, - "both set, same lineage, same serial, different resources": { - &State{ - Lineage: "abc123", - Serial: 1, - Modules: []*ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "foo_bar.baz": {}, - }, - }, - }, - }, - &State{ - Lineage: "abc123", - Serial: 1, - Modules: []*ModuleState{ - { - Path: []string{"root"}, - Resources: map[string]*ResourceState{ - "pizza_crust.tasty": {}, - }, - }, - }, - }, - false, - }, - } - - for name, test := range tests { - t.Run(name, func(t *testing.T) { - got := test.S1.MarshalEqual(test.S2) - if got != test.Want { - t.Errorf("wrong result %#v; want %#v", got, test.Want) - s1Buf := &bytes.Buffer{} - s2Buf := &bytes.Buffer{} - _ = WriteState(test.S1, s1Buf) - _ = WriteState(test.S2, s2Buf) - t.Logf("\nState 1: %s\nState 2: %s", s1Buf.Bytes(), s2Buf.Bytes()) - } - }) - } -} - -func TestStateRemove(t *testing.T) { - cases := map[string]struct { - Address string - One, Two *State - }{ - "simple resource": { - "test_instance.foo", - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "test_instance.bar": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.bar": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }, - - "single instance": { - "test_instance.foo.primary", - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{}, - }, - }, - }, - }, - - "single instance in multi-count": { - "test_instance.foo[0]", - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo.0": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "test_instance.foo.1": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo.1": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }, - - "single resource, multi-count": { - "test_instance.foo", - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo.0": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "test_instance.foo.1": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{}, - }, - }, - }, - }, - - "full module": { - "module.foo", - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - - &ModuleState{ - Path: []string{"root", "foo"}, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "test_instance.bar": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }, - - "module and children": { - "module.foo", - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - - &ModuleState{ - Path: []string{"root", "foo"}, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "test_instance.bar": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - - &ModuleState{ - Path: []string{"root", "foo", "bar"}, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - - "test_instance.bar": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - &State{ - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Resources: map[string]*ResourceState{ - "test_instance.foo": &ResourceState{ - Type: "test_instance", - Primary: &InstanceState{ - ID: "foo", - }, - }, - }, - }, - }, - }, - }, - } - - for k, tc := range cases { - if err := tc.One.Remove(tc.Address); err != nil { - t.Fatalf("bad: %s\n\n%s", k, err) - } - - if !tc.One.Equal(tc.Two) { - t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String()) - } - } -} - -func TestResourceStateEqual(t *testing.T) { - cases := []struct { - Result bool - One, Two *ResourceState - }{ - // Different types - { - false, - &ResourceState{Type: "foo"}, - &ResourceState{Type: "bar"}, - }, - - // Different dependencies - { - false, - &ResourceState{Dependencies: []string{"foo"}}, - &ResourceState{Dependencies: []string{"bar"}}, - }, - - { - false, - &ResourceState{Dependencies: []string{"foo", "bar"}}, - &ResourceState{Dependencies: []string{"foo"}}, - }, - - { - true, - &ResourceState{Dependencies: []string{"bar", "foo"}}, - &ResourceState{Dependencies: []string{"foo", "bar"}}, - }, - - // Different primaries - { - false, - &ResourceState{Primary: nil}, - &ResourceState{Primary: &InstanceState{ID: "foo"}}, - }, - - { - true, - &ResourceState{Primary: &InstanceState{ID: "foo"}}, - &ResourceState{Primary: &InstanceState{ID: "foo"}}, - }, - - // Different tainted - { - false, - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - }, - }, - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - }, - - { - true, - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - }, - } - - for i, tc := range cases { - if tc.One.Equal(tc.Two) != tc.Result { - t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) - } - if tc.Two.Equal(tc.One) != tc.Result { - t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) - } - } -} - -func TestResourceStateTaint(t *testing.T) { - cases := map[string]struct { - Input *ResourceState - Output *ResourceState - }{ - "no primary": { - &ResourceState{}, - &ResourceState{}, - }, - - "primary, not tainted": { - &ResourceState{ - Primary: &InstanceState{ID: "foo"}, - }, - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - }, - - "primary, tainted": { - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - }, - } - - for k, tc := range cases { - tc.Input.Taint() - if !reflect.DeepEqual(tc.Input, tc.Output) { - t.Fatalf( - "Failure: %s\n\nExpected: %#v\n\nGot: %#v", - k, tc.Output, tc.Input) - } - } -} - -func TestResourceStateUntaint(t *testing.T) { - cases := map[string]struct { - Input *ResourceState - ExpectedOutput *ResourceState - }{ - "no primary, err": { - Input: &ResourceState{}, - ExpectedOutput: &ResourceState{}, - }, - - "primary, not tainted": { - Input: &ResourceState{ - Primary: &InstanceState{ID: "foo"}, - }, - ExpectedOutput: &ResourceState{ - Primary: &InstanceState{ID: "foo"}, - }, - }, - "primary, tainted": { - Input: &ResourceState{ - Primary: &InstanceState{ - ID: "foo", - Tainted: true, - }, - }, - ExpectedOutput: &ResourceState{ - Primary: &InstanceState{ID: "foo"}, - }, - }, - } - - for k, tc := range cases { - tc.Input.Untaint() - if !reflect.DeepEqual(tc.Input, tc.ExpectedOutput) { - t.Fatalf( - "Failure: %s\n\nExpected: %#v\n\nGot: %#v", - k, tc.ExpectedOutput, tc.Input) - } - } -} - -func TestInstanceStateEmpty(t *testing.T) { - cases := map[string]struct { - In *InstanceState - Result bool - }{ - "nil is empty": { - nil, - true, - }, - "non-nil but without ID is empty": { - &InstanceState{}, - true, - }, - "with ID is not empty": { - &InstanceState{ - ID: "i-abc123", - }, - false, - }, - } - - for tn, tc := range cases { - if tc.In.Empty() != tc.Result { - t.Fatalf("%q expected %#v to be empty: %#v", tn, tc.In, tc.Result) - } - } -} - -func TestInstanceStateEqual(t *testing.T) { - cases := []struct { - Result bool - One, Two *InstanceState - }{ - // Nils - { - false, - nil, - &InstanceState{}, - }, - - { - false, - &InstanceState{}, - nil, - }, - - // Different IDs - { - false, - &InstanceState{ID: "foo"}, - &InstanceState{ID: "bar"}, - }, - - // Different Attributes - { - false, - &InstanceState{Attributes: map[string]string{"foo": "bar"}}, - &InstanceState{Attributes: map[string]string{"foo": "baz"}}, - }, - - // Different Attribute keys - { - false, - &InstanceState{Attributes: map[string]string{"foo": "bar"}}, - &InstanceState{Attributes: map[string]string{"bar": "baz"}}, - }, - - { - false, - &InstanceState{Attributes: map[string]string{"bar": "baz"}}, - &InstanceState{Attributes: map[string]string{"foo": "bar"}}, - }, - } - - for i, tc := range cases { - if tc.One.Equal(tc.Two) != tc.Result { - t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String()) - } - } -} - -func TestStateEmpty(t *testing.T) { - cases := []struct { - In *State - Result bool - }{ - { - nil, - true, - }, - { - &State{}, - true, - }, - { - &State{ - Remote: &RemoteState{Type: "foo"}, - }, - true, - }, - { - &State{ - Modules: []*ModuleState{ - &ModuleState{}, - }, - }, - false, - }, - } - - for i, tc := range cases { - if tc.In.Empty() != tc.Result { - t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In) - } - } -} - -func TestStateHasResources(t *testing.T) { - cases := []struct { - In *State - Result bool - }{ - { - nil, - false, - }, - { - &State{}, - false, - }, - { - &State{ - Remote: &RemoteState{Type: "foo"}, - }, - false, - }, - { - &State{ - Modules: []*ModuleState{ - &ModuleState{}, - }, - }, - false, - }, - { - &State{ - Modules: []*ModuleState{ - &ModuleState{}, - &ModuleState{}, - }, - }, - false, - }, - { - &State{ - Modules: []*ModuleState{ - &ModuleState{}, - &ModuleState{ - Resources: map[string]*ResourceState{ - "foo.foo": &ResourceState{}, - }, - }, - }, - }, - true, - }, - } - - for i, tc := range cases { - if tc.In.HasResources() != tc.Result { - t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In) - } - } -} - -func TestStateFromFutureTerraform(t *testing.T) { - cases := []struct { - In string - Result bool - }{ - { - "", - false, - }, - { - "0.1", - false, - }, - { - "999.15.1", - true, - }, - } - - for _, tc := range cases { - state := &State{TFVersion: tc.In} - actual := state.FromFutureTerraform() - if actual != tc.Result { - t.Fatalf("%s: bad: %v", tc.In, actual) - } - } -} - -func TestStateIsRemote(t *testing.T) { - cases := []struct { - In *State - Result bool - }{ - { - nil, - false, - }, - { - &State{}, - false, - }, - { - &State{ - Remote: &RemoteState{Type: "foo"}, - }, - true, - }, - } - - for i, tc := range cases { - if tc.In.IsRemote() != tc.Result { - t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In) - } - } -} - -func TestInstanceState_MergeDiff(t *testing.T) { - is := InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "bar", - "port": "8000", - }, - } - - diff := &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "bar", - New: "baz", - }, - "bar": &ResourceAttrDiff{ - Old: "", - New: "foo", - }, - "baz": &ResourceAttrDiff{ - Old: "", - New: "foo", - NewComputed: true, - }, - "port": &ResourceAttrDiff{ - NewRemoved: true, - }, - }, - } - - is2 := is.MergeDiff(diff) - - expected := map[string]string{ - "foo": "baz", - "bar": "foo", - "baz": hcl2shim.UnknownVariableValue, - } - - if !reflect.DeepEqual(expected, is2.Attributes) { - t.Fatalf("bad: %#v", is2.Attributes) - } -} - -// GH-12183. This tests that a list with a computed set generates the -// right partial state. This never failed but is put here for completion -// of the test case for GH-12183. -func TestInstanceState_MergeDiff_computedSet(t *testing.T) { - is := InstanceState{} - - diff := &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "config.#": &ResourceAttrDiff{ - Old: "0", - New: "1", - RequiresNew: true, - }, - - "config.0.name": &ResourceAttrDiff{ - Old: "", - New: "hello", - }, - - "config.0.rules.#": &ResourceAttrDiff{ - Old: "", - NewComputed: true, - }, - }, - } - - is2 := is.MergeDiff(diff) - - expected := map[string]string{ - "config.#": "1", - "config.0.name": "hello", - "config.0.rules.#": hcl2shim.UnknownVariableValue, - } - - if !reflect.DeepEqual(expected, is2.Attributes) { - t.Fatalf("bad: %#v", is2.Attributes) - } -} - -func TestInstanceState_MergeDiff_nil(t *testing.T) { - var is *InstanceState - - diff := &InstanceDiff{ - Attributes: map[string]*ResourceAttrDiff{ - "foo": &ResourceAttrDiff{ - Old: "", - New: "baz", - }, - }, - } - - is2 := is.MergeDiff(diff) - - expected := map[string]string{ - "foo": "baz", - } - - if !reflect.DeepEqual(expected, is2.Attributes) { - t.Fatalf("bad: %#v", is2.Attributes) - } -} - -func TestInstanceState_MergeDiff_nilDiff(t *testing.T) { - is := InstanceState{ - ID: "foo", - Attributes: map[string]string{ - "foo": "bar", - }, - } - - is2 := is.MergeDiff(nil) - - expected := map[string]string{ - "foo": "bar", - } - - if !reflect.DeepEqual(expected, is2.Attributes) { - t.Fatalf("bad: %#v", is2.Attributes) - } -} - -func TestReadWriteState(t *testing.T) { - state := &State{ - Serial: 9, - Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8", - Remote: &RemoteState{ - Type: "http", - Config: map[string]string{ - "url": "http://my-cool-server.com/", - }, - }, - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Dependencies: []string{ - "aws_instance.bar", - }, - Resources: map[string]*ResourceState{ - "foo": &ResourceState{ - Primary: &InstanceState{ - ID: "bar", - Ephemeral: EphemeralState{ - ConnInfo: map[string]string{ - "type": "ssh", - "user": "root", - "password": "supersecret", - }, - }, - }, - }, - }, - }, - }, - } - state.init() - - buf := new(bytes.Buffer) - if err := WriteState(state, buf); err != nil { - t.Fatalf("err: %s", err) - } - - // Verify that the version and serial are set - if state.Version != StateVersion { - t.Fatalf("bad version number: %d", state.Version) - } - - actual, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - // ReadState should not restore sensitive information! - mod := state.RootModule() - mod.Resources["foo"].Primary.Ephemeral = EphemeralState{} - mod.Resources["foo"].Primary.Ephemeral.init() - - if !reflect.DeepEqual(actual, state) { - t.Logf("expected:\n%#v", state) - t.Fatalf("got:\n%#v", actual) - } -} - -func TestReadStateNewVersion(t *testing.T) { - type out struct { - Version int - } - - buf, err := json.Marshal(&out{StateVersion + 1}) - if err != nil { - t.Fatalf("err: %v", err) - } - - s, err := ReadState(bytes.NewReader(buf)) - if s != nil { - t.Fatalf("unexpected: %#v", s) - } - if !strings.Contains(err.Error(), "does not support state version") { - t.Fatalf("err: %v", err) - } -} - -func TestReadStateEmptyOrNilFile(t *testing.T) { - var emptyState bytes.Buffer - _, err := ReadState(&emptyState) - if err != ErrNoState { - t.Fatal("expected ErrNostate, got", err) - } - - var nilFile *os.File - _, err = ReadState(nilFile) - if err != ErrNoState { - t.Fatal("expected ErrNostate, got", err) - } -} - -func TestReadStateTFVersion(t *testing.T) { - type tfVersion struct { - Version int `json:"version"` - TFVersion string `json:"terraform_version"` - } - - cases := []struct { - Written string - Read string - Err bool - }{ - { - "0.0.0", - "0.0.0", - false, - }, - { - "", - "", - false, - }, - { - "bad", - "", - true, - }, - } - - for _, tc := range cases { - buf, err := json.Marshal(&tfVersion{ - Version: 2, - TFVersion: tc.Written, - }) - if err != nil { - t.Fatalf("err: %v", err) - } - - s, err := ReadState(bytes.NewReader(buf)) - if (err != nil) != tc.Err { - t.Fatalf("%s: err: %s", tc.Written, err) - } - if err != nil { - continue - } - - if s.TFVersion != tc.Read { - t.Fatalf("%s: bad: %s", tc.Written, s.TFVersion) - } - } -} - -func TestWriteStateTFVersion(t *testing.T) { - cases := []struct { - Write string - Read string - Err bool - }{ - { - "0.0.0", - "0.0.0", - false, - }, - { - "", - "", - false, - }, - { - "bad", - "", - true, - }, - } - - for _, tc := range cases { - var buf bytes.Buffer - err := WriteState(&State{TFVersion: tc.Write}, &buf) - if (err != nil) != tc.Err { - t.Fatalf("%s: err: %s", tc.Write, err) - } - if err != nil { - continue - } - - s, err := ReadState(&buf) - if err != nil { - t.Fatalf("%s: err: %s", tc.Write, err) - } - - if s.TFVersion != tc.Read { - t.Fatalf("%s: bad: %s", tc.Write, s.TFVersion) - } - } -} - -func TestParseResourceStateKey(t *testing.T) { - cases := []struct { - Input string - Expected *ResourceStateKey - ExpectedErr bool - }{ - { - Input: "aws_instance.foo.3", - Expected: &ResourceStateKey{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - Index: 3, - }, - }, - { - Input: "aws_instance.foo.0", - Expected: &ResourceStateKey{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - Index: 0, - }, - }, - { - Input: "aws_instance.foo", - Expected: &ResourceStateKey{ - Mode: ManagedResourceMode, - Type: "aws_instance", - Name: "foo", - Index: -1, - }, - }, - { - Input: "data.aws_ami.foo", - Expected: &ResourceStateKey{ - Mode: DataResourceMode, - Type: "aws_ami", - Name: "foo", - Index: -1, - }, - }, - { - Input: "aws_instance.foo.malformed", - ExpectedErr: true, - }, - { - Input: "aws_instance.foo.malformedwithnumber.123", - ExpectedErr: true, - }, - { - Input: "malformed", - ExpectedErr: true, - }, - } - for _, tc := range cases { - rsk, err := ParseResourceStateKey(tc.Input) - if rsk != nil && tc.Expected != nil && !rsk.Equal(tc.Expected) { - t.Fatalf("%s: expected %s, got %s", tc.Input, tc.Expected, rsk) - } - if (err != nil) != tc.ExpectedErr { - t.Fatalf("%s: expected err: %t, got %s", tc.Input, tc.ExpectedErr, err) - } - } -} - -func TestReadState_prune(t *testing.T) { - state := &State{ - Modules: []*ModuleState{ - &ModuleState{Path: rootModulePath}, - nil, - }, - } - state.init() - - buf := new(bytes.Buffer) - if err := WriteState(state, buf); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - expected := &State{ - Version: state.Version, - Lineage: state.Lineage, - } - expected.init() - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("got:\n%#v", actual) - } -} - -func TestReadState_pruneDependencies(t *testing.T) { - state := &State{ - Serial: 9, - Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8", - Remote: &RemoteState{ - Type: "http", - Config: map[string]string{ - "url": "http://my-cool-server.com/", - }, - }, - Modules: []*ModuleState{ - &ModuleState{ - Path: rootModulePath, - Dependencies: []string{ - "aws_instance.bar", - "aws_instance.bar", - }, - Resources: map[string]*ResourceState{ - "foo": &ResourceState{ - Dependencies: []string{ - "aws_instance.baz", - "aws_instance.baz", - }, - Primary: &InstanceState{ - ID: "bar", - }, - }, - }, - }, - }, - } - state.init() - - buf := new(bytes.Buffer) - if err := WriteState(state, buf); err != nil { - t.Fatalf("err: %s", err) - } - - actual, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - // make sure the duplicate Dependencies are filtered - modDeps := actual.Modules[0].Dependencies - resourceDeps := actual.Modules[0].Resources["foo"].Dependencies - - if len(modDeps) > 1 || modDeps[0] != "aws_instance.bar" { - t.Fatalf("expected 1 module depends_on entry, got %q", modDeps) - } - - if len(resourceDeps) > 1 || resourceDeps[0] != "aws_instance.baz" { - t.Fatalf("expected 1 resource depends_on entry, got %q", resourceDeps) - } -} - -func TestReadState_bigHash(t *testing.T) { - expected := uint64(14885267135666261723) - s := strings.NewReader(`{"version": 3, "backend":{"hash":14885267135666261723}}`) - - actual, err := ReadState(s) - if err != nil { - t.Fatal(err) - } - - if actual.Backend.Hash != expected { - t.Fatalf("expected backend hash %d, got %d", expected, actual.Backend.Hash) - } -} - -func TestResourceNameSort(t *testing.T) { - names := []string{ - "a", - "b", - "a.0", - "a.c", - "a.d", - "c", - "a.b.0", - "a.b.1", - "a.b.10", - "a.b.2", - } - - sort.Sort(resourceNameSort(names)) - - expected := []string{ - "a", - "a.0", - "a.b.0", - "a.b.1", - "a.b.2", - "a.b.10", - "a.c", - "a.d", - "b", - "c", - } - - if !reflect.DeepEqual(names, expected) { - t.Fatalf("got: %q\nexpected: %q\n", names, expected) - } -} diff --git a/internal/legacy/terraform/state_upgrade_v1_to_v2.go b/internal/legacy/terraform/state_upgrade_v1_to_v2.go deleted file mode 100644 index aa13cce803..0000000000 --- a/internal/legacy/terraform/state_upgrade_v1_to_v2.go +++ /dev/null @@ -1,189 +0,0 @@ -package terraform - -import ( - "fmt" - - "github.com/mitchellh/copystructure" -) - -// upgradeStateV1ToV2 is used to upgrade a V1 state representation -// into a V2 state representation -func upgradeStateV1ToV2(old *stateV1) (*State, error) { - if old == nil { - return nil, nil - } - - remote, err := old.Remote.upgradeToV2() - if err != nil { - return nil, fmt.Errorf("Error upgrading State V1: %v", err) - } - - modules := make([]*ModuleState, len(old.Modules)) - for i, module := range old.Modules { - upgraded, err := module.upgradeToV2() - if err != nil { - return nil, fmt.Errorf("Error upgrading State V1: %v", err) - } - modules[i] = upgraded - } - if len(modules) == 0 { - modules = nil - } - - newState := &State{ - Version: 2, - Serial: old.Serial, - Remote: remote, - Modules: modules, - } - - newState.sort() - newState.init() - - return newState, nil -} - -func (old *remoteStateV1) upgradeToV2() (*RemoteState, error) { - if old == nil { - return nil, nil - } - - config, err := copystructure.Copy(old.Config) - if err != nil { - return nil, fmt.Errorf("Error upgrading RemoteState V1: %v", err) - } - - return &RemoteState{ - Type: old.Type, - Config: config.(map[string]string), - }, nil -} - -func (old *moduleStateV1) upgradeToV2() (*ModuleState, error) { - if old == nil { - return nil, nil - } - - pathRaw, err := copystructure.Copy(old.Path) - if err != nil { - return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) - } - path, ok := pathRaw.([]string) - if !ok { - return nil, fmt.Errorf("Error upgrading ModuleState V1: path is not a list of strings") - } - if len(path) == 0 { - // We found some V1 states with a nil path. Assume root and catch - // duplicate path errors later (as part of Validate). - path = rootModulePath - } - - // Outputs needs upgrading to use the new structure - outputs := make(map[string]*OutputState) - for key, output := range old.Outputs { - outputs[key] = &OutputState{ - Type: "string", - Value: output, - Sensitive: false, - } - } - - resources := make(map[string]*ResourceState) - for key, oldResource := range old.Resources { - upgraded, err := oldResource.upgradeToV2() - if err != nil { - return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) - } - resources[key] = upgraded - } - - dependencies, err := copystructure.Copy(old.Dependencies) - if err != nil { - return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) - } - - return &ModuleState{ - Path: path, - Outputs: outputs, - Resources: resources, - Dependencies: dependencies.([]string), - }, nil -} - -func (old *resourceStateV1) upgradeToV2() (*ResourceState, error) { - if old == nil { - return nil, nil - } - - dependencies, err := copystructure.Copy(old.Dependencies) - if err != nil { - return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) - } - - primary, err := old.Primary.upgradeToV2() - if err != nil { - return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) - } - - deposed := make([]*InstanceState, len(old.Deposed)) - for i, v := range old.Deposed { - upgraded, err := v.upgradeToV2() - if err != nil { - return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) - } - deposed[i] = upgraded - } - if len(deposed) == 0 { - deposed = nil - } - - return &ResourceState{ - Type: old.Type, - Dependencies: dependencies.([]string), - Primary: primary, - Deposed: deposed, - Provider: old.Provider, - }, nil -} - -func (old *instanceStateV1) upgradeToV2() (*InstanceState, error) { - if old == nil { - return nil, nil - } - - attributes, err := copystructure.Copy(old.Attributes) - if err != nil { - return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) - } - ephemeral, err := old.Ephemeral.upgradeToV2() - if err != nil { - return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) - } - - meta, err := copystructure.Copy(old.Meta) - if err != nil { - return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) - } - - newMeta := make(map[string]interface{}) - for k, v := range meta.(map[string]string) { - newMeta[k] = v - } - - return &InstanceState{ - ID: old.ID, - Attributes: attributes.(map[string]string), - Ephemeral: *ephemeral, - Meta: newMeta, - }, nil -} - -func (old *ephemeralStateV1) upgradeToV2() (*EphemeralState, error) { - connInfo, err := copystructure.Copy(old.ConnInfo) - if err != nil { - return nil, fmt.Errorf("Error upgrading EphemeralState V1: %v", err) - } - return &EphemeralState{ - ConnInfo: connInfo.(map[string]string), - }, nil -} diff --git a/internal/legacy/terraform/state_upgrade_v2_to_v3.go b/internal/legacy/terraform/state_upgrade_v2_to_v3.go deleted file mode 100644 index e52d35fcd1..0000000000 --- a/internal/legacy/terraform/state_upgrade_v2_to_v3.go +++ /dev/null @@ -1,142 +0,0 @@ -package terraform - -import ( - "fmt" - "log" - "regexp" - "sort" - "strconv" - "strings" -) - -// The upgrade process from V2 to V3 state does not affect the structure, -// so we do not need to redeclare all of the structs involved - we just -// take a deep copy of the old structure and assert the version number is -// as we expect. -func upgradeStateV2ToV3(old *State) (*State, error) { - new := old.DeepCopy() - - // Ensure the copied version is v2 before attempting to upgrade - if new.Version != 2 { - return nil, fmt.Errorf("Cannot apply v2->v3 state upgrade to " + - "a state which is not version 2.") - } - - // Set the new version number - new.Version = 3 - - // Change the counts for things which look like maps to use the % - // syntax. Remove counts for empty collections - they will be added - // back in later. - for _, module := range new.Modules { - for _, resource := range module.Resources { - // Upgrade Primary - if resource.Primary != nil { - upgradeAttributesV2ToV3(resource.Primary) - } - - // Upgrade Deposed - if resource.Deposed != nil { - for _, deposed := range resource.Deposed { - upgradeAttributesV2ToV3(deposed) - } - } - } - } - - return new, nil -} - -func upgradeAttributesV2ToV3(instanceState *InstanceState) error { - collectionKeyRegexp := regexp.MustCompile(`^(.*\.)#$`) - collectionSubkeyRegexp := regexp.MustCompile(`^([^\.]+)\..*`) - - // Identify the key prefix of anything which is a collection - var collectionKeyPrefixes []string - for key := range instanceState.Attributes { - if submatches := collectionKeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 { - collectionKeyPrefixes = append(collectionKeyPrefixes, submatches[0][1]) - } - } - sort.Strings(collectionKeyPrefixes) - - log.Printf("[STATE UPGRADE] Detected the following collections in state: %v", collectionKeyPrefixes) - - // This could be rolled into fewer loops, but it is somewhat clearer this way, and will not - // run very often. - for _, prefix := range collectionKeyPrefixes { - // First get the actual keys that belong to this prefix - var potentialKeysMatching []string - for key := range instanceState.Attributes { - if strings.HasPrefix(key, prefix) { - potentialKeysMatching = append(potentialKeysMatching, strings.TrimPrefix(key, prefix)) - } - } - sort.Strings(potentialKeysMatching) - - var actualKeysMatching []string - for _, key := range potentialKeysMatching { - if submatches := collectionSubkeyRegexp.FindAllStringSubmatch(key, -1); len(submatches) > 0 { - actualKeysMatching = append(actualKeysMatching, submatches[0][1]) - } else { - if key != "#" { - actualKeysMatching = append(actualKeysMatching, key) - } - } - } - actualKeysMatching = uniqueSortedStrings(actualKeysMatching) - - // Now inspect the keys in order to determine whether this is most likely to be - // a map, list or set. There is room for error here, so we log in each case. If - // there is no method of telling, we remove the key from the InstanceState in - // order that it will be recreated. Again, this could be rolled into fewer loops - // but we prefer clarity. - - oldCountKey := fmt.Sprintf("%s#", prefix) - - // First, detect "obvious" maps - which have non-numeric keys (mostly). - hasNonNumericKeys := false - for _, key := range actualKeysMatching { - if _, err := strconv.Atoi(key); err != nil { - hasNonNumericKeys = true - } - } - if hasNonNumericKeys { - newCountKey := fmt.Sprintf("%s%%", prefix) - - instanceState.Attributes[newCountKey] = instanceState.Attributes[oldCountKey] - delete(instanceState.Attributes, oldCountKey) - log.Printf("[STATE UPGRADE] Detected %s as a map. Replaced count = %s", - strings.TrimSuffix(prefix, "."), instanceState.Attributes[newCountKey]) - } - - // Now detect empty collections and remove them from state. - if len(actualKeysMatching) == 0 { - delete(instanceState.Attributes, oldCountKey) - log.Printf("[STATE UPGRADE] Detected %s as an empty collection. Removed from state.", - strings.TrimSuffix(prefix, ".")) - } - } - - return nil -} - -// uniqueSortedStrings removes duplicates from a slice of strings and returns -// a sorted slice of the unique strings. -func uniqueSortedStrings(input []string) []string { - uniquemap := make(map[string]struct{}) - for _, str := range input { - uniquemap[str] = struct{}{} - } - - output := make([]string, len(uniquemap)) - - i := 0 - for key := range uniquemap { - output[i] = key - i = i + 1 - } - - sort.Strings(output) - return output -} diff --git a/internal/legacy/terraform/state_v1.go b/internal/legacy/terraform/state_v1.go deleted file mode 100644 index 68cffb41b5..0000000000 --- a/internal/legacy/terraform/state_v1.go +++ /dev/null @@ -1,145 +0,0 @@ -package terraform - -// stateV1 keeps track of a snapshot state-of-the-world that Terraform -// can use to keep track of what real world resources it is actually -// managing. -// -// stateV1 is _only used for the purposes of backwards compatibility -// and is no longer used in Terraform. -// -// For the upgrade process, see state_upgrade_v1_to_v2.go -type stateV1 struct { - // Version is the protocol version. "1" for a StateV1. - Version int `json:"version"` - - // Serial is incremented on any operation that modifies - // the State file. It is used to detect potentially conflicting - // updates. - Serial int64 `json:"serial"` - - // Remote is used to track the metadata required to - // pull and push state files from a remote storage endpoint. - Remote *remoteStateV1 `json:"remote,omitempty"` - - // Modules contains all the modules in a breadth-first order - Modules []*moduleStateV1 `json:"modules"` -} - -type remoteStateV1 struct { - // Type controls the client we use for the remote state - Type string `json:"type"` - - // Config is used to store arbitrary configuration that - // is type specific - Config map[string]string `json:"config"` -} - -type moduleStateV1 struct { - // Path is the import path from the root module. Modules imports are - // always disjoint, so the path represents amodule tree - Path []string `json:"path"` - - // Outputs declared by the module and maintained for each module - // even though only the root module technically needs to be kept. - // This allows operators to inspect values at the boundaries. - Outputs map[string]string `json:"outputs"` - - // Resources is a mapping of the logically named resource to - // the state of the resource. Each resource may actually have - // N instances underneath, although a user only needs to think - // about the 1:1 case. - Resources map[string]*resourceStateV1 `json:"resources"` - - // Dependencies are a list of things that this module relies on - // existing to remain intact. For example: an module may depend - // on a VPC ID given by an aws_vpc resource. - // - // Terraform uses this information to build valid destruction - // orders and to warn the user if they're destroying a module that - // another resource depends on. - // - // Things can be put into this list that may not be managed by - // Terraform. If Terraform doesn't find a matching ID in the - // overall state, then it assumes it isn't managed and doesn't - // worry about it. - Dependencies []string `json:"depends_on,omitempty"` -} - -type resourceStateV1 struct { - // This is filled in and managed by Terraform, and is the resource - // type itself such as "mycloud_instance". If a resource provider sets - // this value, it won't be persisted. - Type string `json:"type"` - - // Dependencies are a list of things that this resource relies on - // existing to remain intact. For example: an AWS instance might - // depend on a subnet (which itself might depend on a VPC, and so - // on). - // - // Terraform uses this information to build valid destruction - // orders and to warn the user if they're destroying a resource that - // another resource depends on. - // - // Things can be put into this list that may not be managed by - // Terraform. If Terraform doesn't find a matching ID in the - // overall state, then it assumes it isn't managed and doesn't - // worry about it. - Dependencies []string `json:"depends_on,omitempty"` - - // Primary is the current active instance for this resource. - // It can be replaced but only after a successful creation. - // This is the instances on which providers will act. - Primary *instanceStateV1 `json:"primary"` - - // Tainted is used to track any underlying instances that - // have been created but are in a bad or unknown state and - // need to be cleaned up subsequently. In the - // standard case, there is only at most a single instance. - // However, in pathological cases, it is possible for the number - // of instances to accumulate. - Tainted []*instanceStateV1 `json:"tainted,omitempty"` - - // Deposed is used in the mechanics of CreateBeforeDestroy: the existing - // Primary is Deposed to get it out of the way for the replacement Primary to - // be created by Apply. If the replacement Primary creates successfully, the - // Deposed instance is cleaned up. If there were problems creating the - // replacement, the instance remains in the Deposed list so it can be - // destroyed in a future run. Functionally, Deposed instances are very - // similar to Tainted instances in that Terraform is only tracking them in - // order to remember to destroy them. - Deposed []*instanceStateV1 `json:"deposed,omitempty"` - - // Provider is used when a resource is connected to a provider with an alias. - // If this string is empty, the resource is connected to the default provider, - // e.g. "aws_instance" goes with the "aws" provider. - // If the resource block contained a "provider" key, that value will be set here. - Provider string `json:"provider,omitempty"` -} - -type instanceStateV1 struct { - // A unique ID for this resource. This is opaque to Terraform - // and is only meant as a lookup mechanism for the providers. - ID string `json:"id"` - - // Attributes are basic information about the resource. Any keys here - // are accessible in variable format within Terraform configurations: - // ${resourcetype.name.attribute}. - Attributes map[string]string `json:"attributes,omitempty"` - - // Ephemeral is used to store any state associated with this instance - // that is necessary for the Terraform run to complete, but is not - // persisted to a state file. - Ephemeral ephemeralStateV1 `json:"-"` - - // Meta is a simple K/V map that is persisted to the State but otherwise - // ignored by Terraform core. It's meant to be used for accounting by - // external client code. - Meta map[string]string `json:"meta,omitempty"` -} - -type ephemeralStateV1 struct { - // ConnInfo is used for the providers to export information which is - // used to connect to the resource for provisioning. For example, - // this could contain SSH or WinRM credentials. - ConnInfo map[string]string `json:"-"` -} diff --git a/internal/legacy/terraform/testing.go b/internal/legacy/terraform/testing.go deleted file mode 100644 index 3f0418d927..0000000000 --- a/internal/legacy/terraform/testing.go +++ /dev/null @@ -1,19 +0,0 @@ -package terraform - -import ( - "os" - "testing" -) - -// TestStateFile writes the given state to the path. -func TestStateFile(t *testing.T, path string, state *State) { - f, err := os.Create(path) - if err != nil { - t.Fatalf("err: %s", err) - } - defer f.Close() - - if err := WriteState(state, f); err != nil { - t.Fatalf("err: %s", err) - } -} diff --git a/internal/legacy/terraform/ui_input.go b/internal/legacy/terraform/ui_input.go deleted file mode 100644 index 688bcf71e4..0000000000 --- a/internal/legacy/terraform/ui_input.go +++ /dev/null @@ -1,32 +0,0 @@ -package terraform - -import "context" - -// UIInput is the interface that must be implemented to ask for input -// from this user. This should forward the request to wherever the user -// inputs things to ask for values. -type UIInput interface { - Input(context.Context, *InputOpts) (string, error) -} - -// InputOpts are options for asking for input. -type InputOpts struct { - // Id is a unique ID for the question being asked that might be - // used for logging or to look up a prior answered question. - Id string - - // Query is a human-friendly question for inputting this value. - Query string - - // Description is a description about what this option is. Be wary - // that this will probably be in a terminal so split lines as you see - // necessary. - Description string - - // Default will be the value returned if no data is entered. - Default string - - // Secret should be true if we are asking for sensitive input. - // If attached to a TTY, Terraform will disable echo. - Secret bool -} diff --git a/internal/legacy/terraform/ui_input_mock.go b/internal/legacy/terraform/ui_input_mock.go deleted file mode 100644 index e2d9c38481..0000000000 --- a/internal/legacy/terraform/ui_input_mock.go +++ /dev/null @@ -1,25 +0,0 @@ -package terraform - -import "context" - -// MockUIInput is an implementation of UIInput that can be used for tests. -type MockUIInput struct { - InputCalled bool - InputOpts *InputOpts - InputReturnMap map[string]string - InputReturnString string - InputReturnError error - InputFn func(*InputOpts) (string, error) -} - -func (i *MockUIInput) Input(ctx context.Context, opts *InputOpts) (string, error) { - i.InputCalled = true - i.InputOpts = opts - if i.InputFn != nil { - return i.InputFn(opts) - } - if i.InputReturnMap != nil { - return i.InputReturnMap[opts.Id], i.InputReturnError - } - return i.InputReturnString, i.InputReturnError -} diff --git a/internal/legacy/terraform/ui_input_prefix.go b/internal/legacy/terraform/ui_input_prefix.go deleted file mode 100644 index b5d32b1e85..0000000000 --- a/internal/legacy/terraform/ui_input_prefix.go +++ /dev/null @@ -1,20 +0,0 @@ -package terraform - -import ( - "context" - "fmt" -) - -// PrefixUIInput is an implementation of UIInput that prefixes the ID -// with a string, allowing queries to be namespaced. -type PrefixUIInput struct { - IdPrefix string - QueryPrefix string - UIInput UIInput -} - -func (i *PrefixUIInput) Input(ctx context.Context, opts *InputOpts) (string, error) { - opts.Id = fmt.Sprintf("%s.%s", i.IdPrefix, opts.Id) - opts.Query = fmt.Sprintf("%s%s", i.QueryPrefix, opts.Query) - return i.UIInput.Input(ctx, opts) -} diff --git a/internal/legacy/terraform/ui_input_prefix_test.go b/internal/legacy/terraform/ui_input_prefix_test.go deleted file mode 100644 index dff42c39c5..0000000000 --- a/internal/legacy/terraform/ui_input_prefix_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package terraform - -import ( - "context" - "testing" -) - -func TestPrefixUIInput_impl(t *testing.T) { - var _ UIInput = new(PrefixUIInput) -} - -func TestPrefixUIInput(t *testing.T) { - input := new(MockUIInput) - prefix := &PrefixUIInput{ - IdPrefix: "foo", - UIInput: input, - } - - _, err := prefix.Input(context.Background(), &InputOpts{Id: "bar"}) - if err != nil { - t.Fatalf("err: %s", err) - } - - if input.InputOpts.Id != "foo.bar" { - t.Fatalf("bad: %#v", input.InputOpts) - } -} diff --git a/internal/legacy/terraform/ui_output.go b/internal/legacy/terraform/ui_output.go deleted file mode 100644 index 84427c63de..0000000000 --- a/internal/legacy/terraform/ui_output.go +++ /dev/null @@ -1,7 +0,0 @@ -package terraform - -// UIOutput is the interface that must be implemented to output -// data to the end user. -type UIOutput interface { - Output(string) -} diff --git a/internal/legacy/terraform/ui_output_callback.go b/internal/legacy/terraform/ui_output_callback.go deleted file mode 100644 index 135a91c5f0..0000000000 --- a/internal/legacy/terraform/ui_output_callback.go +++ /dev/null @@ -1,9 +0,0 @@ -package terraform - -type CallbackUIOutput struct { - OutputFn func(string) -} - -func (o *CallbackUIOutput) Output(v string) { - o.OutputFn(v) -} diff --git a/internal/legacy/terraform/ui_output_callback_test.go b/internal/legacy/terraform/ui_output_callback_test.go deleted file mode 100644 index 1dd5ccddf9..0000000000 --- a/internal/legacy/terraform/ui_output_callback_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package terraform - -import ( - "testing" -) - -func TestCallbackUIOutput_impl(t *testing.T) { - var _ UIOutput = new(CallbackUIOutput) -} diff --git a/internal/legacy/terraform/ui_output_mock.go b/internal/legacy/terraform/ui_output_mock.go deleted file mode 100644 index d828c921ca..0000000000 --- a/internal/legacy/terraform/ui_output_mock.go +++ /dev/null @@ -1,21 +0,0 @@ -package terraform - -import "sync" - -// MockUIOutput is an implementation of UIOutput that can be used for tests. -type MockUIOutput struct { - sync.Mutex - OutputCalled bool - OutputMessage string - OutputFn func(string) -} - -func (o *MockUIOutput) Output(v string) { - o.Lock() - defer o.Unlock() - o.OutputCalled = true - o.OutputMessage = v - if o.OutputFn != nil { - o.OutputFn(v) - } -} diff --git a/internal/legacy/terraform/ui_output_mock_test.go b/internal/legacy/terraform/ui_output_mock_test.go deleted file mode 100644 index 0a23c2e234..0000000000 --- a/internal/legacy/terraform/ui_output_mock_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package terraform - -import ( - "testing" -) - -func TestMockUIOutput(t *testing.T) { - var _ UIOutput = new(MockUIOutput) -} diff --git a/internal/legacy/terraform/upgrade_state_v1_test.go b/internal/legacy/terraform/upgrade_state_v1_test.go deleted file mode 100644 index 93e03accae..0000000000 --- a/internal/legacy/terraform/upgrade_state_v1_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package terraform - -import ( - "bytes" - "reflect" - "strings" - "testing" - - "github.com/davecgh/go-spew/spew" -) - -// TestReadUpgradeStateV1toV3 tests the state upgrade process from the V1 state -// to the current version, and needs editing each time. This means it tests the -// entire pipeline of upgrades (which migrate version to version). -func TestReadUpgradeStateV1toV3(t *testing.T) { - // ReadState should transparently detect the old version but will upgrade - // it on Write. - actual, err := ReadState(strings.NewReader(testV1State)) - if err != nil { - t.Fatalf("err: %s", err) - } - - buf := new(bytes.Buffer) - if err := WriteState(actual, buf); err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Version != 3 { - t.Fatalf("bad: State version not incremented; is %d", actual.Version) - } - - roundTripped, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(actual, roundTripped) { - t.Logf("actual:\n%#v", actual) - t.Fatalf("roundTripped:\n%#v", roundTripped) - } -} - -func TestReadUpgradeStateV1toV3_outputs(t *testing.T) { - // ReadState should transparently detect the old version but will upgrade - // it on Write. - actual, err := ReadState(strings.NewReader(testV1StateWithOutputs)) - if err != nil { - t.Fatalf("err: %s", err) - } - - buf := new(bytes.Buffer) - if err := WriteState(actual, buf); err != nil { - t.Fatalf("err: %s", err) - } - - if actual.Version != 3 { - t.Fatalf("bad: State version not incremented; is %d", actual.Version) - } - - roundTripped, err := ReadState(buf) - if err != nil { - t.Fatalf("err: %s", err) - } - - if !reflect.DeepEqual(actual, roundTripped) { - spew.Config.DisableMethods = true - t.Fatalf("bad:\n%s\n\nround tripped:\n%s\n", spew.Sdump(actual), spew.Sdump(roundTripped)) - spew.Config.DisableMethods = false - } -} - -// Upgrading the state should not lose empty module Outputs and Resources maps -// during upgrade. The init for a new module initializes new maps, so we may not -// be expecting to check for a nil map. -func TestReadUpgradeStateV1toV3_emptyState(t *testing.T) { - // ReadState should transparently detect the old version but will upgrade - // it on Write. - orig, err := ReadStateV1([]byte(testV1EmptyState)) - if err != nil { - t.Fatalf("err: %s", err) - } - - stateV2, err := upgradeStateV1ToV2(orig) - if err != nil { - t.Fatalf("error attempting upgradeStateV1ToV2: %s", err) - } - - for _, m := range stateV2.Modules { - if m.Resources == nil { - t.Fatal("V1 to V2 upgrade lost module.Resources") - } - if m.Outputs == nil { - t.Fatal("V1 to V2 upgrade lost module.Outputs") - } - } - - stateV3, err := upgradeStateV2ToV3(stateV2) - if err != nil { - t.Fatalf("error attempting to upgradeStateV2ToV3: %s", err) - } - for _, m := range stateV3.Modules { - if m.Resources == nil { - t.Fatal("V2 to V3 upgrade lost module.Resources") - } - if m.Outputs == nil { - t.Fatal("V2 to V3 upgrade lost module.Outputs") - } - } - -} - -const testV1EmptyState = `{ - "version": 1, - "serial": 0, - "modules": [ - { - "path": [ - "root" - ], - "outputs": {}, - "resources": {} - } - ] -} -` - -const testV1State = `{ - "version": 1, - "serial": 9, - "remote": { - "type": "http", - "config": { - "url": "http://my-cool-server.com/" - } - }, - "modules": [ - { - "path": [ - "root" - ], - "outputs": null, - "resources": { - "foo": { - "type": "", - "primary": { - "id": "bar" - } - } - }, - "depends_on": [ - "aws_instance.bar" - ] - } - ] -} -` - -const testV1StateWithOutputs = `{ - "version": 1, - "serial": 9, - "remote": { - "type": "http", - "config": { - "url": "http://my-cool-server.com/" - } - }, - "modules": [ - { - "path": [ - "root" - ], - "outputs": { - "foo": "bar", - "baz": "foo" - }, - "resources": { - "foo": { - "type": "", - "primary": { - "id": "bar" - } - } - }, - "depends_on": [ - "aws_instance.bar" - ] - } - ] -} -` diff --git a/internal/legacy/terraform/upgrade_state_v2_test.go b/internal/legacy/terraform/upgrade_state_v2_test.go deleted file mode 100644 index 546d749682..0000000000 --- a/internal/legacy/terraform/upgrade_state_v2_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package terraform - -import ( - "bytes" - "strings" - "testing" -) - -// TestReadUpgradeStateV2toV3 tests the state upgrade process from the V2 state -// to the current version, and needs editing each time. This means it tests the -// entire pipeline of upgrades (which migrate version to version). -func TestReadUpgradeStateV2toV3(t *testing.T) { - // ReadState should transparently detect the old version but will upgrade - // it on Write. - upgraded, err := ReadState(strings.NewReader(testV2State)) - if err != nil { - t.Fatalf("err: %s", err) - } - - buf := new(bytes.Buffer) - if err := WriteState(upgraded, buf); err != nil { - t.Fatalf("err: %s", err) - } - - if upgraded.Version != 3 { - t.Fatalf("bad: State version not incremented; is %d", upgraded.Version) - } - - // For this test we cannot assert that we match the round trip because an - // empty map has been removed from state. Instead we make assertions against - // some of the key fields in the _upgraded_ state. - instanceState, ok := upgraded.RootModule().Resources["test_resource.main"] - if !ok { - t.Fatalf("Instance state for test_resource.main was removed from state during upgrade") - } - - primary := instanceState.Primary - if primary == nil { - t.Fatalf("Primary instance was removed from state for test_resource.main") - } - - // Non-empty computed map is moved from .# to .% - if _, ok := primary.Attributes["computed_map.#"]; ok { - t.Fatalf("Count was not upgraded from .# to .%% for computed_map") - } - if count, ok := primary.Attributes["computed_map.%"]; !ok || count != "1" { - t.Fatalf("Count was not in .%% or was not 2 for computed_map") - } - - // list_of_map top level retains .# - if count, ok := primary.Attributes["list_of_map.#"]; !ok || count != "2" { - t.Fatal("Count for list_of_map was migrated incorrectly") - } - - // list_of_map.0 is moved from .# to .% - if _, ok := primary.Attributes["list_of_map.0.#"]; ok { - t.Fatalf("Count was not upgraded from .# to .%% for list_of_map.0") - } - if count, ok := primary.Attributes["list_of_map.0.%"]; !ok || count != "2" { - t.Fatalf("Count was not in .%% or was not 2 for list_of_map.0") - } - - // list_of_map.1 is moved from .# to .% - if _, ok := primary.Attributes["list_of_map.1.#"]; ok { - t.Fatalf("Count was not upgraded from .# to .%% for list_of_map.1") - } - if count, ok := primary.Attributes["list_of_map.1.%"]; !ok || count != "2" { - t.Fatalf("Count was not in .%% or was not 2 for list_of_map.1") - } - - // map is moved from .# to .% - if _, ok := primary.Attributes["map.#"]; ok { - t.Fatalf("Count was not upgraded from .# to .%% for map") - } - if count, ok := primary.Attributes["map.%"]; !ok || count != "2" { - t.Fatalf("Count was not in .%% or was not 2 for map") - } - - // optional_computed_map should be removed from state - if _, ok := primary.Attributes["optional_computed_map"]; ok { - t.Fatal("optional_computed_map was not removed from state") - } - - // required_map is moved from .# to .% - if _, ok := primary.Attributes["required_map.#"]; ok { - t.Fatalf("Count was not upgraded from .# to .%% for required_map") - } - if count, ok := primary.Attributes["required_map.%"]; !ok || count != "3" { - t.Fatalf("Count was not in .%% or was not 3 for map") - } - - // computed_list keeps .# - if count, ok := primary.Attributes["computed_list.#"]; !ok || count != "2" { - t.Fatal("Count was migrated incorrectly for computed_list") - } - - // computed_set keeps .# - if count, ok := primary.Attributes["computed_set.#"]; !ok || count != "2" { - t.Fatal("Count was migrated incorrectly for computed_set") - } - if val, ok := primary.Attributes["computed_set.2337322984"]; !ok || val != "setval1" { - t.Fatal("Set item for computed_set.2337322984 changed or moved") - } - if val, ok := primary.Attributes["computed_set.307881554"]; !ok || val != "setval2" { - t.Fatal("Set item for computed_set.307881554 changed or moved") - } - - // string properties are unaffected - if val, ok := primary.Attributes["id"]; !ok || val != "testId" { - t.Fatal("id was not set correctly after migration") - } -} - -const testV2State = `{ - "version": 2, - "terraform_version": "0.7.0", - "serial": 2, - "modules": [ - { - "path": [ - "root" - ], - "outputs": { - "computed_map": { - "sensitive": false, - "type": "map", - "value": { - "key1": "value1" - } - }, - "computed_set": { - "sensitive": false, - "type": "list", - "value": [ - "setval1", - "setval2" - ] - }, - "map": { - "sensitive": false, - "type": "map", - "value": { - "key": "test", - "test": "test" - } - }, - "set": { - "sensitive": false, - "type": "list", - "value": [ - "test1", - "test2" - ] - } - }, - "resources": { - "test_resource.main": { - "type": "test_resource", - "primary": { - "id": "testId", - "attributes": { - "computed_list.#": "2", - "computed_list.0": "listval1", - "computed_list.1": "listval2", - "computed_map.#": "1", - "computed_map.key1": "value1", - "computed_read_only": "value_from_api", - "computed_read_only_force_new": "value_from_api", - "computed_set.#": "2", - "computed_set.2337322984": "setval1", - "computed_set.307881554": "setval2", - "id": "testId", - "list_of_map.#": "2", - "list_of_map.0.#": "2", - "list_of_map.0.key1": "value1", - "list_of_map.0.key2": "value2", - "list_of_map.1.#": "2", - "list_of_map.1.key3": "value3", - "list_of_map.1.key4": "value4", - "map.#": "2", - "map.key": "test", - "map.test": "test", - "map_that_look_like_set.#": "2", - "map_that_look_like_set.12352223": "hello", - "map_that_look_like_set.36234341": "world", - "optional_computed_map.#": "0", - "required": "Hello World", - "required_map.#": "3", - "required_map.key1": "value1", - "required_map.key2": "value2", - "required_map.key3": "value3", - "set.#": "2", - "set.2326977762": "test1", - "set.331058520": "test2" - } - } - } - } - } - ] -} -` diff --git a/internal/legacy/terraform/util_test.go b/internal/legacy/terraform/util_test.go deleted file mode 100644 index 8b3907e236..0000000000 --- a/internal/legacy/terraform/util_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package terraform - -import ( - "fmt" - "reflect" - "testing" - "time" -) - -func TestSemaphore(t *testing.T) { - s := NewSemaphore(2) - timer := time.AfterFunc(time.Second, func() { - panic("deadlock") - }) - defer timer.Stop() - - s.Acquire() - if !s.TryAcquire() { - t.Fatalf("should acquire") - } - if s.TryAcquire() { - t.Fatalf("should not acquire") - } - s.Release() - s.Release() - - // This release should panic - defer func() { - r := recover() - if r == nil { - t.Fatalf("should panic") - } - }() - s.Release() -} - -func TestStrSliceContains(t *testing.T) { - if strSliceContains(nil, "foo") { - t.Fatalf("Bad") - } - if strSliceContains([]string{}, "foo") { - t.Fatalf("Bad") - } - if strSliceContains([]string{"bar"}, "foo") { - t.Fatalf("Bad") - } - if !strSliceContains([]string{"bar", "foo"}, "foo") { - t.Fatalf("Bad") - } -} - -func TestUniqueStrings(t *testing.T) { - cases := []struct { - Input []string - Expected []string - }{ - { - []string{}, - []string{}, - }, - { - []string{"x"}, - []string{"x"}, - }, - { - []string{"a", "b", "c"}, - []string{"a", "b", "c"}, - }, - { - []string{"a", "a", "a"}, - []string{"a"}, - }, - { - []string{"a", "b", "a", "b", "a", "a"}, - []string{"a", "b"}, - }, - { - []string{"c", "b", "a", "c", "b"}, - []string{"a", "b", "c"}, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("unique-%d", i), func(t *testing.T) { - actual := uniqueStrings(tc.Input) - if !reflect.DeepEqual(tc.Expected, actual) { - t.Fatalf("Expected: %q\nGot: %q", tc.Expected, actual) - } - }) - } -} diff --git a/internal/legacy/terraform/version.go b/internal/legacy/terraform/version.go deleted file mode 100644 index 0caeca0ad9..0000000000 --- a/internal/legacy/terraform/version.go +++ /dev/null @@ -1,10 +0,0 @@ -package terraform - -import ( - "github.com/hashicorp/terraform/version" -) - -// Deprecated: Providers should use schema.Provider.TerraformVersion instead -func VersionString() string { - return version.String() -} diff --git a/internal/legacy/terraform/version_required.go b/internal/legacy/terraform/version_required.go deleted file mode 100644 index f14d93f681..0000000000 --- a/internal/legacy/terraform/version_required.go +++ /dev/null @@ -1,62 +0,0 @@ -package terraform - -import ( - "fmt" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/tfdiags" - - "github.com/hashicorp/terraform/internal/configs" - - tfversion "github.com/hashicorp/terraform/version" -) - -// CheckCoreVersionRequirements visits each of the modules in the given -// configuration tree and verifies that any given Core version constraints -// match with the version of Terraform Core that is being used. -// -// The returned diagnostics will contain errors if any constraints do not match. -// The returned diagnostics might also return warnings, which should be -// displayed to the user. -func CheckCoreVersionRequirements(config *configs.Config) tfdiags.Diagnostics { - if config == nil { - return nil - } - - var diags tfdiags.Diagnostics - module := config.Module - - for _, constraint := range module.CoreVersionConstraints { - if !constraint.Required.Check(tfversion.SemVer) { - switch { - case len(config.Path) == 0: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported Terraform Core version", - Detail: fmt.Sprintf( - "This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", - tfversion.String(), - ), - Subject: constraint.DeclRange.Ptr(), - }) - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported Terraform Core version", - Detail: fmt.Sprintf( - "Module %s (from %s) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", - config.Path, config.SourceAddr, tfversion.String(), - ), - Subject: constraint.DeclRange.Ptr(), - }) - } - } - } - - for _, c := range config.Children { - childDiags := CheckCoreVersionRequirements(c) - diags = diags.Append(childDiags) - } - - return diags -} diff --git a/internal/logging/indent.go b/internal/logging/indent.go index e0da0d7c73..f0753d73da 100644 --- a/internal/logging/indent.go +++ b/internal/logging/indent.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package logging import ( diff --git a/internal/logging/indent_test.go b/internal/logging/indent_test.go index 46b12a42ca..485d5bc9cd 100644 --- a/internal/logging/indent_test.go +++ b/internal/logging/indent_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package logging import ( diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 972f7cfaea..9a71787ddf 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package logging import ( @@ -8,11 +11,6 @@ import ( "strings" "syscall" - // go.etcd.io/etcd imports capnslog, which calls log.SetOutput in its - // init() function, so importing it here means that our log.SetOutput - // wins. this is fixed in coreos v3.5, which is not released yet. See - // https://github.com/etcd-io/etcd/issues/12498 for more information. - _ "github.com/coreos/pkg/capnslog" "github.com/hashicorp/go-hclog" ) @@ -27,6 +25,7 @@ const ( // to other loggers, like provisioners and remote-state backends. envLogCore = "TF_LOG_CORE" envLogProvider = "TF_LOG_PROVIDER" + envLogCloud = "TF_LOG_CLOUD" ) var ( @@ -131,6 +130,20 @@ func NewProviderLogger(prefix string) hclog.Logger { return l } +// NewCloudLogger returns a logger for the cloud plugin, possibly with a +// different log level from the global logger. +func NewCloudLogger() hclog.Logger { + l := &logPanicWrapper{ + Logger: logger.Named("cloud"), + } + + level := cloudLogLevel() + logger.Debug("created cloud logger", "level", level) + + l.SetLevel(level) + return l +} + // CurrentLogLevel returns the current log level string based the environment vars func CurrentLogLevel() string { ll, _ := globalLogLevel() @@ -146,6 +159,15 @@ func providerLogLevel() hclog.Level { return parseLogLevel(providerEnvLevel) } +func cloudLogLevel() hclog.Level { + providerEnvLevel := strings.ToUpper(os.Getenv(envLogCloud)) + if providerEnvLevel == "" { + providerEnvLevel = strings.ToUpper(os.Getenv(envLog)) + } + + return parseLogLevel(providerEnvLevel) +} + func globalLogLevel() (hclog.Level, bool) { var json bool envLevel := strings.ToUpper(os.Getenv(envLog)) diff --git a/internal/logging/panic.go b/internal/logging/panic.go index 25ecc6d452..bcc92f12ba 100644 --- a/internal/logging/panic.go +++ b/internal/logging/panic.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package logging import ( @@ -6,6 +9,7 @@ import ( "runtime/debug" "strings" "sync" + "syscall" "github.com/hashicorp/go-hclog" ) @@ -47,12 +51,37 @@ func PanicHandler() { return } - fmt.Fprint(os.Stderr, panicOutput) - fmt.Fprint(os.Stderr, recovered, "\n") - - // When called from a deferred function, debug.PrintStack will include the - // full stack from the point of the pending panic. - debug.PrintStack() + // We're aiming to behave as much as possible like the built-in panic + // handler aside from our few intentional exceptions. + // + // One detail is that we want the panic information to definitely go to the + // real stderr even if something else in our process has rudely reassigned + // [`os.Stderr`] to point to something else. Software that's designed to + // monitor the output of Go programs to detect and report panics expects + // the panic message to appear on the real process stderr. + // + // (At the time of writing, the go-plugin Serve function is an example + // of modifying the global os.Stderr, causing it to get routed over a + // plugin-specific stream rather than to the real process stderr. If + // we used os.Stderr here then panics under "terraform rpcapi" would + // end up in the wrong place.) + // + // The following mimics how the standard library (package os) constructs + // os.Stderr in the first place. Technically even this syscall.Stderr + // can be overridden rudely at runtime, but thankfully we've not yet + // encountered anything linked into Terraform that does _that_! + stderr := os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") + if stderr == nil { + // os.NewFile has a few esoteric error cases where it'll return nil, + // in which case we'll just do our best with whatever happens to + // be in os.Stderr right now as a last resort. + stderr = os.Stderr + } + fmt.Fprint(stderr, panicOutput) + fmt.Fprint(stderr, "panic: ", recovered, "\n") + // The following mimics the implementation of debug.PrintStack, but + // without the hard-coded reference to os.Stderr. + stderr.Write(debug.Stack()) // An exit code of 11 keeps us out of the way of the detailed exitcodes // from plan, and also happens to be the same code as SIGSEGV which is diff --git a/internal/logging/panic_test.go b/internal/logging/panic_test.go index e83a0ba5ac..a106985422 100644 --- a/internal/logging/panic_test.go +++ b/internal/logging/panic_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package logging import ( diff --git a/internal/modsdir/doc.go b/internal/modsdir/doc.go index 0d7d664fc1..a19ae9bf52 100644 --- a/internal/modsdir/doc.go +++ b/internal/modsdir/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package modsdir is an internal package containing the model types used to // represent the manifest of modules in a local modules cache directory. package modsdir diff --git a/internal/modsdir/manifest.go b/internal/modsdir/manifest.go index 2821ce8043..a2e5387c6f 100644 --- a/internal/modsdir/manifest.go +++ b/internal/modsdir/manifest.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package modsdir import ( @@ -8,11 +11,13 @@ import ( "log" "os" "path/filepath" + "sort" "strings" version "github.com/hashicorp/go-version" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getmodules/moduleaddrs" ) // Record represents some metadata about an installed module, as part @@ -97,7 +102,7 @@ func ReadManifestSnapshot(r io.Reader) (Manifest, error) { // to normalize them back in on read so that we can just gracefully // upgrade on the next "terraform init". if record.SourceAddr != "" { - if addr, err := addrs.ParseModuleSource(record.SourceAddr); err == nil { + if addr, err := moduleaddrs.ParseModuleSource(record.SourceAddr); err == nil { // This is a best effort sort of thing. If the source // address isn't valid then we'll just leave it as-is // and let another component detect that downstream, @@ -136,7 +141,14 @@ func ReadManifestSnapshotForDir(dir string) (Manifest, error) { func (m Manifest) WriteSnapshot(w io.Writer) error { var write manifestSnapshotFile - for _, record := range m { + var keys []string + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + record := m[k] + // Make sure VersionStr is in sync with Version, since we encourage // callers to manipulate Version and ignore VersionStr. if record.Version != nil { diff --git a/internal/modsdir/paths.go b/internal/modsdir/paths.go index 9ebb52431b..ee45d38f28 100644 --- a/internal/modsdir/paths.go +++ b/internal/modsdir/paths.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package modsdir const ManifestSnapshotFilename = "modules.json" diff --git a/internal/moduledeps/dependencies.go b/internal/moduledeps/dependencies.go index 6de7aff0ba..ff4b0bdd90 100644 --- a/internal/moduledeps/dependencies.go +++ b/internal/moduledeps/dependencies.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package moduledeps import ( diff --git a/internal/moduledeps/doc.go b/internal/moduledeps/doc.go index 7eff083157..cdef11ea94 100644 --- a/internal/moduledeps/doc.go +++ b/internal/moduledeps/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package moduledeps contains types that can be used to describe the // providers required for all of the modules in a module tree. // diff --git a/internal/moduledeps/module.go b/internal/moduledeps/module.go index e428a4ec41..18605dc557 100644 --- a/internal/moduledeps/module.go +++ b/internal/moduledeps/module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package moduledeps import ( @@ -20,7 +23,7 @@ type Module struct { type WalkFunc func(path []string, parent *Module, current *Module) error // WalkTree calls the given callback once for the receiver and then -// once for each descendent, in an order such that parents are called +// once for each descendant, in an order such that parents are called // before their children and siblings are called in the order they // appear in the Children slice. // @@ -39,7 +42,7 @@ type WalkFunc func(path []string, parent *Module, current *Module) error // data structure, so it's the caller's responsibility to arrange for that // should it be needed. // -// It is safe for a callback to modify the descendents of the "current" +// It is safe for a callback to modify the descendants of the "current" // module, including the ordering of the Children slice itself, but the // callback MUST NOT modify the parent module. func (m *Module) WalkTree(cb WalkFunc) error { @@ -71,9 +74,9 @@ func (m *Module) SortChildren() { sort.Sort(sortModules{m.Children}) } -// SortDescendents is a convenience wrapper for calling SortChildren on -// the receiver and all of its descendent modules. -func (m *Module) SortDescendents() { +// SortDescendants is a convenience wrapper for calling SortChildren on +// the receiver and all of its descendant modules. +func (m *Module) SortDescendants() { m.WalkTree(func(path []string, parent *Module, current *Module) error { current.SortChildren() return nil @@ -123,7 +126,7 @@ func (m *Module) ProviderRequirements() discovery.PluginRequirements { } // AllProviderRequirements calls ProviderRequirements for the receiver and all -// of its descendents, and merges the result into a single PluginRequirements +// of its descendants, and merges the result into a single PluginRequirements // structure that would satisfy all of the modules together. // // Requirements returned by this method include only version constraints, @@ -142,7 +145,7 @@ func (m *Module) AllProviderRequirements() discovery.PluginRequirements { // the equality of all downstream modules too. // // The children are considered to be ordered, so callers may wish to use -// SortDescendents first to normalize the order of the slices of child nodes. +// SortDescendants first to normalize the order of the slices of child nodes. // // The implementation of this function is not optimized since it is provided // primarily for use in tests. diff --git a/internal/moduledeps/module_test.go b/internal/moduledeps/module_test.go index dfbc99d219..c1fcb3eb8e 100644 --- a/internal/moduledeps/module_test.go +++ b/internal/moduledeps/module_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package moduledeps import ( diff --git a/internal/moduleref/record.go b/internal/moduleref/record.go new file mode 100644 index 0000000000..cd19ef0d61 --- /dev/null +++ b/internal/moduleref/record.go @@ -0,0 +1,48 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleref + +import ( + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/addrs" +) + +const FormatVersion = "1.0" + +// ModuleRecord is the implementation of a module entry defined in the module +// manifest that is declared by configuration. +type Record struct { + Key string + Source addrs.ModuleSource + Version *version.Version + VersionConstraints version.Constraints + Children Records +} + +// ModuleRecordManifest is the view implementation of module entries declared +// in configuration +type Manifest struct { + FormatVersion string + Records Records +} + +func (m *Manifest) addModuleEntry(entry *Record) { + m.Records = append(m.Records, entry) +} + +func (r *Record) addChild(child *Record) { + r.Children = append(r.Children, child) +} + +type Records []*Record + +func (r Records) Len() int { + return len(r) +} +func (r Records) Less(i, j int) bool { + return r[i].Key < r[j].Key +} +func (r Records) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} diff --git a/internal/moduleref/resolver.go b/internal/moduleref/resolver.go new file mode 100644 index 0000000000..94d80c4023 --- /dev/null +++ b/internal/moduleref/resolver.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleref + +import ( + "maps" + "strings" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/modsdir" +) + +// Resolver is the struct responsible for finding all modules references in +// Terraform configuration for a given internal module manifest. +type Resolver struct { + manifest *Manifest + internalManifest modsdir.Manifest +} + +// NewResolver creates a new Resolver, storing a copy of the internal manifest +// that is passed. +func NewResolver(internalManifest modsdir.Manifest) *Resolver { + // Since maps are pointers, create a copy of the internal manifest to + // prevent introducing side effects to the original + internalManifestCopy := maps.Clone(internalManifest) + + // Remove the root module entry from the internal manifest as it is + // never directly referenced. + delete(internalManifestCopy, "") + + return &Resolver{ + internalManifest: internalManifestCopy, + manifest: &Manifest{ + FormatVersion: FormatVersion, + Records: Records{}, + }, + } +} + +// Resolve will attempt to find all module references for the passed configuration +// and return a new manifest encapsulating this information. +func (r *Resolver) Resolve(cfg *configs.Config) *Manifest { + // First find all the referenced modules. + r.findAndTrimReferencedEntries(cfg, nil, nil) + + return r.manifest +} + +// findAndTrimReferencedEntries will traverse a given Terraform configuration +// and attempt find a caller for every entry in the internal module manifest. +// If an entry is found, it will be removed from the internal manifest and +// appended to the manifest that records this new information in a nested heirarchy. +func (r *Resolver) findAndTrimReferencedEntries(cfg *configs.Config, parentRecord *Record, parentKey *string) { + var name string + var versionConstraints version.Constraints + if parentKey != nil { + for key := range cfg.Parent.Children { + if key == *parentKey { + name = key + if cfg.Parent.Module.ModuleCalls[key] != nil { + versionConstraints = cfg.Parent.Module.ModuleCalls[key].Version.Required + } + break + } + } + } + + childRecord := &Record{ + Key: name, + Source: cfg.SourceAddr, + VersionConstraints: versionConstraints, + } + key := strings.Join(cfg.Path, ".") + + for entryKey, entry := range r.internalManifest { + if entryKey == key { + // Use resolved version from manifest + childRecord.Version = entry.Version + if parentRecord.Source != nil { + parentRecord.addChild(childRecord) + } else { + r.manifest.addModuleEntry(childRecord) + } + // "Trim" the entry from the internal manifest, saving us cycles + // as we descend into the module tree. + delete(r.internalManifest, entryKey) + break + } + } + + // Traverse the child configurations + for childKey, childCfg := range cfg.Children { + r.findAndTrimReferencedEntries(childCfg, childRecord, &childKey) + } +} diff --git a/internal/moduleref/resolver_test.go b/internal/moduleref/resolver_test.go new file mode 100644 index 0000000000..1075252e01 --- /dev/null +++ b/internal/moduleref/resolver_test.go @@ -0,0 +1,197 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduleref + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/modsdir" +) + +func TestResolver_Resolve(t *testing.T) { + cfg := configs.NewEmptyConfig() + cfg.Module = &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{ + "foo": {Name: "foo"}, + }, + } + + cfg.Children = map[string]*configs.Config{ + "foo": &configs.Config{ + Path: addrs.Module{"foo"}, + Parent: cfg, + Children: make(map[string]*configs.Config), + SourceAddr: addrs.ModuleSourceLocal("./foo"), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + }, + } + + manifest := modsdir.Manifest{ + "foo": modsdir.Record{ + Key: "foo", + SourceAddr: "./foo", + }, + "bar": modsdir.Record{ + Key: "bar", + SourceAddr: "./bar", + }, + } + + resolver := NewResolver(manifest) + result := resolver.Resolve(cfg) + + if len(result.Records) != 1 { + t.Fatalf("expected the resolved number of entries to equal 1, got: %d", len(result.Records)) + } + + // For the foo record + if result.Records[0].Key != "foo" { + t.Fatal("expected to find reference for module \"foo\"") + } +} + +func TestResolver_ResolveNestedChildren(t *testing.T) { + cfg := configs.NewEmptyConfig() + cfg.Children = make(map[string]*configs.Config) + cfg.Module = &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{ + "foo": {Name: "foo"}, + "fellowship": {Name: "fellowship"}, + }, + } + + cfg.Children["foo"] = &configs.Config{ + Path: addrs.Module{"foo"}, + Parent: cfg, + SourceAddr: addrs.ModuleSourceLocal("./foo"), + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + } + + childCfgFellowship := &configs.Config{ + Path: addrs.Module{"fellowship"}, + Parent: cfg, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship"), + }, + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{ + "frodo": {Name: "frodo"}, + }, + }, + } + cfg.Children["fellowship"] = childCfgFellowship + + childCfgFellowship.Children["frodo"] = &configs.Config{ + Path: addrs.Module{"fellowship", "frodo"}, + Parent: childCfgFellowship, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship/frodo"), + }, + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + } + + childCfgWeapons := &configs.Config{ + Path: addrs.Module{"fellowship", "weapons"}, + Parent: childCfgFellowship, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship/weapons"), + }, + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{ + "sting": {Name: "sting"}, + }, + }, + } + childCfgFellowship.Children["weapons"] = childCfgWeapons + + childCfgWeapons.Children["sting"] = &configs.Config{ + Path: addrs.Module{"fellowship", "weapons", "sting"}, + Parent: childCfgWeapons, + SourceAddr: addrs.ModuleSourceRemote{ + Package: addrs.ModulePackage("fellowship/weapons/sting"), + }, + Children: make(map[string]*configs.Config), + Module: &configs.Module{ + ModuleCalls: map[string]*configs.ModuleCall{}, + }, + } + + manifest := modsdir.Manifest{ + "foo": modsdir.Record{ + Key: "foo", + SourceAddr: "./foo", + }, + "bar": modsdir.Record{ + Key: "bar", + SourceAddr: "./bar", + }, + "fellowship": modsdir.Record{ + Key: "fellowship", + SourceAddr: "fellowship", + }, + "fellowship.frodo": modsdir.Record{ + Key: "fellowship.frodo", + SourceAddr: "fellowship/frodo", + }, + "fellowship.weapons": modsdir.Record{ + Key: "fellowship.weapons", + SourceAddr: "fellowship/weapons", + }, + "fellowship.weapons.sting": modsdir.Record{ + Key: "fellowship.weapons.sting", + SourceAddr: "fellowship/weapons/sting", + }, + "fellowship.weapons.anduril": modsdir.Record{ + Key: "fellowship.weapons.anduril", + SourceAddr: "fellowship/weapons/anduril", + }, + } + + resolver := NewResolver(manifest) + result := resolver.Resolve(cfg) + recordsCount, sources := countAndListSources(result.Records) + if recordsCount != 5 { + t.Fatalf("expected the resolved number of entries to equal 5, got: %d", recordsCount) + } + + assertions := map[string]bool{ + "./foo": true, + "./bar": false, + "fellowship": true, + "fellowship/frodo": true, + "fellowship/weapons": true, + "fellowship/weapons/sting": true, + "fellowship/weapons/anduril": false, + } + + for _, source := range sources { + referenced, ok := assertions[source] + if !ok || !referenced { + t.Fatalf("expected to find referenced entry with key: %s", source) + } + } +} + +func countAndListSources(records Records) (count int, sources []string) { + for _, record := range records { + sources = append(sources, record.Source.String()) + count++ + childCount, childSources := countAndListSources(record.Children) + count += childCount + sources = append(sources, childSources...) + } + return +} diff --git a/internal/moduletest/assertion.go b/internal/moduletest/assertion.go deleted file mode 100644 index 1bacbfac92..0000000000 --- a/internal/moduletest/assertion.go +++ /dev/null @@ -1,66 +0,0 @@ -package moduletest - -import ( - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// Assertion is the description of a single test assertion, whether -// successful or unsuccessful. -type Assertion struct { - Outcome Status - - // Description is a user-provided, human-readable description of what - // this assertion represents. - Description string - - // Message is typically relevant only for TestFailed or TestError - // assertions, giving a human-readable description of the problem, - // formatted in the way our format package expects to receive paragraphs - // for terminal word wrapping. - Message string - - // Diagnostics includes diagnostics specific to the current test assertion, - // if available. - Diagnostics tfdiags.Diagnostics -} - -// Component represents a component being tested, each of which can have -// several associated test assertions. -type Component struct { - Assertions map[string]*Assertion -} - -// Status is an enumeration of possible outcomes of a test assertion. -type Status rune - -//go:generate go run golang.org/x/tools/cmd/stringer -type=Status assertion.go - -const ( - // Pending indicates that the test was registered (during planning) - // but didn't register an outcome during apply, perhaps due to being - // blocked by some other upstream failure. - Pending Status = '?' - - // Passed indicates that the test condition succeeded. - Passed Status = 'P' - - // Failed indicates that the test condition was valid but did not - // succeed. - Failed Status = 'F' - - // Error indicates that the test condition was invalid or that the - // test report failed in some other way. - Error Status = 'E' -) - -// SuiteCanPass returns true if a suite containing an assertion with this -// status could possibly succeed. The suite as a whole succeeds only if all -// of its assertions have statuses where SuiteCanPass returns true. -func (s Status) SuiteCanPass() bool { - switch s { - case Failed, Error: - return false - default: - return true - } -} diff --git a/internal/moduletest/doc.go b/internal/moduletest/doc.go deleted file mode 100644 index 4b378877aa..0000000000 --- a/internal/moduletest/doc.go +++ /dev/null @@ -1,8 +0,0 @@ -// Package moduletest contains the support code for some experimental features -// we're using to evaluate strategies for having an opinionated approach to -// testing of Terraform modules. -// -// At the moment nothing in this module is considered stable, so any features -// that are usable by end-users ought to emit experiment warnings saying that -// everything is subject to change even in patch releases. -package moduletest diff --git a/internal/moduletest/file.go b/internal/moduletest/file.go new file mode 100644 index 0000000000..761839e7c9 --- /dev/null +++ b/internal/moduletest/file.go @@ -0,0 +1,45 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduletest + +import ( + "sync" + + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type File struct { + Config *configs.TestFile + + Name string + Status Status + + Runs []*Run + + Diagnostics tfdiags.Diagnostics + + sync.Mutex +} + +func NewFile(name string, config *configs.TestFile, runs []*Run) *File { + return &File{ + Name: name, + Config: config, + Runs: runs, + Mutex: sync.Mutex{}, + } +} + +func (f *File) UpdateStatus(status Status) { + f.Lock() + defer f.Unlock() + f.Status = f.Status.Merge(status) +} + +func (f *File) GetStatus() Status { + f.Lock() + defer f.Unlock() + return f.Status +} diff --git a/internal/moduletest/graph/apply.go b/internal/moduletest/graph/apply.go new file mode 100644 index 0000000000..632545bcbf --- /dev/null +++ b/internal/moduletest/graph/apply.go @@ -0,0 +1,196 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + "log" + "path/filepath" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func (n *NodeTestRun) testApply(ctx *EvalContext, variables terraform.InputValues, waiter *operationWaiter) { + file, run := n.File(), n.run + config := run.ModuleConfig + key := n.run.GetStateKey() + + // FilterVariablesToModule only returns warnings, so we don't check the + // returned diags for errors. + setVariables, testOnlyVariables, setVariableDiags := n.FilterVariablesToModule(variables) + run.Diagnostics = run.Diagnostics.Append(setVariableDiags) + + // ignore diags because validate has covered it + tfCtx, _ := terraform.NewContext(n.opts.ContextOpts) + + // execute the terraform plan operation + _, plan, planDiags := n.plan(ctx, tfCtx, setVariables, waiter) + + // Any error during the planning prevents our apply from + // continuing which is an error. + planDiags = run.ExplainExpectedFailures(planDiags) + run.Diagnostics = run.Diagnostics.Append(planDiags) + if planDiags.HasErrors() { + run.Status = moduletest.Error + return + } + + // Since we're carrying on an executing the apply operation as well, we're + // just going to do some post processing of the diagnostics. We remove the + // warnings generated from check blocks, as the apply operation will either + // reproduce them or fix them and we don't want fixed diagnostics to be + // reported and we don't want duplicates either. + var filteredDiags tfdiags.Diagnostics + for _, diag := range run.Diagnostics { + if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok && rule.Container.CheckableKind() == addrs.CheckableCheck { + continue + } + filteredDiags = filteredDiags.Append(diag) + } + run.Diagnostics = filteredDiags + + // execute the apply operation + applyScope, updated, applyDiags := n.apply(tfCtx, plan, moduletest.Running, variables, waiter) + + // Remove expected diagnostics, and add diagnostics in case anything that should have failed didn't. + // We'll also update the run status based on the presence of errors or missing expected failures. + failOrErr := n.checkForMissingExpectedFailures(run, applyDiags) + if failOrErr { + // Even though the apply operation failed, the graph may have done + // partial updates and the returned state should reflect this. + ctx.SetFileState(key, &TestFileState{ + Run: run, + State: updated, + }) + return + } + + n.AddVariablesToConfig(variables) + + if ctx.Verbose() { + schemas, diags := tfCtx.Schemas(config, updated) + + // If we're going to fail to render the plan, let's not fail the overall + // test. It can still have succeeded. So we'll add the diagnostics, but + // still report the test status as a success. + if diags.HasErrors() { + // This is very unlikely. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to print verbose output", + fmt.Sprintf("Terraform failed to print the verbose output for %s, other diagnostics will contain more details as to why.", filepath.Join(file.Name, run.Name)))) + } else { + run.Verbose = &moduletest.Verbose{ + Plan: nil, // We don't have a plan to show in apply mode. + State: updated, + Config: config, + Providers: schemas.Providers, + Provisioners: schemas.Provisioners, + } + } + + run.Diagnostics = run.Diagnostics.Append(diags) + } + + // Evaluate the run block directly in the graph context to validate the assertions + // of the run. We also pass in all the + // previous contexts so this run block can refer to outputs from + // previous run blocks. + newStatus, outputVals, moreDiags := ctx.EvaluateRun(run, applyScope, testOnlyVariables) + run.Status = newStatus + run.Diagnostics = run.Diagnostics.Append(moreDiags) + + // Now we've successfully validated this run block, lets add it into + // our prior run outputs so future run blocks can access it. + ctx.SetOutput(run, outputVals) + + // Only update the most recent run and state if the state was + // actually updated by this change. We want to use the run that + // most recently updated the tracked state as the cleanup + // configuration. + ctx.SetFileState(key, &TestFileState{ + Run: run, + State: updated, + }) +} + +func (n *NodeTestRun) apply(tfCtx *terraform.Context, plan *plans.Plan, progress moduletest.Progress, variables terraform.InputValues, waiter *operationWaiter) (*lang.Scope, *states.State, tfdiags.Diagnostics) { + run := n.run + file := n.File() + log.Printf("[TRACE] TestFileRunner: called apply for %s/%s", file.Name, run.Name) + + var diags tfdiags.Diagnostics + config := run.ModuleConfig + + // If things get cancelled while we are executing the apply operation below + // we want to print out all the objects that we were creating so the user + // can verify we managed to tidy everything up possibly. + // + // Unfortunately, this creates a race condition as the apply operation can + // edit the plan (by removing changes once they are applied) while at the + // same time our cancellation process will try to read the plan. + // + // We take a quick copy of the changes we care about here, which will then + // be used in place of the plan when we print out the objects to be created + // as part of the cancellation process. + var created []*plans.ResourceInstanceChangeSrc + for _, change := range plan.Changes.Resources { + if change.Action != plans.Create { + continue + } + created = append(created, change) + } + + // We only need to pass ephemeral variables to the apply operation, as the + // plan has already been evaluated with the full set of variables. + ephemeralVariables := make(terraform.InputValues) + for k, v := range config.Root.Module.Variables { + if v.EphemeralSet { + if value, ok := variables[k]; ok { + ephemeralVariables[k] = value + } + } + } + + applyOpts := &terraform.ApplyOpts{ + SetVariables: ephemeralVariables, + } + + waiter.update(tfCtx, progress, created) + log.Printf("[DEBUG] TestFileRunner: starting apply for %s/%s", file.Name, run.Name) + updated, newScope, applyDiags := tfCtx.ApplyAndEval(plan, config, applyOpts) + log.Printf("[DEBUG] TestFileRunner: completed apply for %s/%s", file.Name, run.Name) + diags = diags.Append(applyDiags) + + return newScope, updated, diags +} + +// checkForMissingExpectedFailures checks for missing expected failures in the diagnostics. +// It updates the run status based on the presence of errors or missing expected failures. +func (n *NodeTestRun) checkForMissingExpectedFailures(run *moduletest.Run, diags tfdiags.Diagnostics) (failOrErr bool) { + // Retrieve and append diagnostics that are either unrelated to expected failures + // or report missing expected failures. + unexpectedDiags := run.ValidateExpectedFailures(diags) + run.Diagnostics = run.Diagnostics.Append(unexpectedDiags) + for _, diag := range unexpectedDiags { + // // If any diagnostic indicates a missing expected failure, set the run status to fail. + if ok := moduletest.DiagnosticFromMissingExpectedFailure(diag); ok { + run.Status = run.Status.Merge(moduletest.Fail) + continue + } + + // upgrade the run status to error if there still are other errors in the diagnostics + if diag.Severity() == tfdiags.Error { + run.Status = run.Status.Merge(moduletest.Error) + break + } + } + return run.Status > moduletest.Pass +} diff --git a/internal/moduletest/graph/diagnostics.go b/internal/moduletest/graph/diagnostics.go new file mode 100644 index 0000000000..3cf3171411 --- /dev/null +++ b/internal/moduletest/graph/diagnostics.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import "github.com/hashicorp/terraform/internal/tfdiags" + +// DiagnosticCausedByTestFailure implements multiple interfaces that enables it to +// be used in the "Extra" field of a diagnostic. This type should only be used as +// the Extra for diagnostics reporting assertions that fail in a run block during +// `terraform test`. +// +// DiagnosticCausedByTestFailure implements the [DiagnosticExtraCausedByTestFailure] +// interface. This allows downstream logic to identify diagnostics that are specifically +// due to assertion failures. +// +// DiagnosticCausedByTestFailure also implements the [DiagnosticExtraBecauseEphemeral], +// [DiagnosticExtraBecauseSensitive], and [DiagnosticExtraBecauseUnknown] interfaces. +// These interfaces allow the diagnostic renderer to include ephemeral, sensitive or +// unknown data if it's present. This is enabled because if a test fails then the user +// will want to know what values contributed to the failing assertion. +// +// When using this, set the Extra to DiagnosticCausedByTestFailure(true) and also +// populate the EvalContext and Expression fields of the diagnostic. + +type DiagnosticCausedByTestFailure struct { + Verbose bool +} + +var _ tfdiags.DiagnosticExtraCausedByTestFailure = DiagnosticCausedByTestFailure{false} +var _ tfdiags.DiagnosticExtraBecauseEphemeral = DiagnosticCausedByTestFailure{false} +var _ tfdiags.DiagnosticExtraBecauseSensitive = DiagnosticCausedByTestFailure{false} +var _ tfdiags.DiagnosticExtraBecauseUnknown = DiagnosticCausedByTestFailure{false} + +func (e DiagnosticCausedByTestFailure) DiagnosticCausedByTestFailure() bool { + return true +} + +func (e DiagnosticCausedByTestFailure) IsTestVerboseMode() bool { + return e.Verbose +} + +func (e DiagnosticCausedByTestFailure) DiagnosticCausedByEphemeral() bool { + return true +} + +func (e DiagnosticCausedByTestFailure) DiagnosticCausedBySensitive() bool { + return true +} + +func (e DiagnosticCausedByTestFailure) DiagnosticCausedByUnknown() bool { + return true +} diff --git a/internal/moduletest/graph/eval_context.go b/internal/moduletest/graph/eval_context.go new file mode 100644 index 0000000000..f925186a0a --- /dev/null +++ b/internal/moduletest/graph/eval_context.go @@ -0,0 +1,514 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "context" + "fmt" + "log" + "sort" + "sync" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "maps" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/didyoumean" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/moduletest" + hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// EvalContext is a container for context relating to the evaluation of a +// particular .tftest.hcl file. +// This context is used to track the various values that are available to the +// test suite, both from the test suite itself and from the results of the runs +// within the suite. +// The struct provides concurrency-safe access to the various maps it contains. +type EvalContext struct { + VariableCaches *hcltest.VariableCaches + + // runOutputs is a mapping from run addresses to cty object values + // representing the collected output values from the module under test. + // + // This is used to allow run blocks to refer back to the output values of + // previous run blocks. It is passed into the Evaluate functions that + // validate the test assertions, and used when calculating values for + // variables within run blocks. + runOutputs map[addrs.Run]cty.Value + outputsLock sync.Mutex + + // configProviders is a cache of config keys mapped to all the providers + // referenced by the given config. + // + // The config keys are globally unique across an entire test suite, so we + // store this at the suite runner level to get maximum efficiency. + configProviders map[string]map[string]bool + providersLock sync.Mutex + + // FileStates is a mapping of module keys to it's last applied state + // file. + // + // This is used to clean up the infrastructure created during the test after + // the test has finished. + FileStates map[string]*TestFileState + stateLock sync.Mutex + + // cancelContext and stopContext can be used to terminate the evaluation of the + // test suite when a cancellation or stop signal is received. + // cancelFunc and stopFunc are the corresponding functions to call to signal + // the termination. + cancelContext context.Context + cancelFunc context.CancelFunc + stopContext context.Context + stopFunc context.CancelFunc + + renderer views.Test + verbose bool +} + +type EvalContextOpts struct { + Verbose bool + Render views.Test + CancelCtx context.Context + StopCtx context.Context +} + +// NewEvalContext constructs a new graph evaluation context for use in +// evaluating the runs within a test suite. +// The context is initialized with the provided cancel and stop contexts, and +// these contexts can be used from external commands to signal the termination of the test suite. +func NewEvalContext(opts *EvalContextOpts) *EvalContext { + cancelCtx, cancel := context.WithCancel(opts.CancelCtx) + stopCtx, stop := context.WithCancel(opts.StopCtx) + return &EvalContext{ + runOutputs: make(map[addrs.Run]cty.Value), + outputsLock: sync.Mutex{}, + configProviders: make(map[string]map[string]bool), + providersLock: sync.Mutex{}, + FileStates: make(map[string]*TestFileState), + stateLock: sync.Mutex{}, + VariableCaches: hcltest.NewVariableCaches(), + cancelContext: cancelCtx, + cancelFunc: cancel, + stopContext: stopCtx, + stopFunc: stop, + verbose: opts.Verbose, + renderer: opts.Render, + } +} + +// Renderer returns the renderer for the test suite. +func (ec *EvalContext) Renderer() views.Test { + return ec.renderer +} + +// Cancel signals to the runs in the test suite that they should stop evaluating +// the test suite, and return immediately. +func (ec *EvalContext) Cancel() { + ec.cancelFunc() +} + +// Cancelled returns true if the context has been stopped. The default cause +// of the error is context.Canceled. +func (ec *EvalContext) Cancelled() bool { + return ec.cancelContext.Err() != nil +} + +// Stop signals to the runs in the test suite that they should stop evaluating +// the test suite, and just skip. +func (ec *EvalContext) Stop() { + ec.stopFunc() +} + +func (ec *EvalContext) Stopped() bool { + return ec.stopContext.Err() != nil +} + +// Verbose returns true if the context is in verbose mode. +func (ec *EvalContext) Verbose() bool { + return ec.verbose +} + +// EvaluateRun processes the assertions inside the provided configs.TestRun against +// the run results, returning a status, an object value representing the output +// values from the module under test, and diagnostics describing any problems. +// +// extraVariableVals, if provided, overlays the input variables that are +// already available in resultScope in case there are additional input +// variables that were defined only for use in the test suite. Any variable +// not defined in extraVariableVals will be evaluated through resultScope instead. +func (ec *EvalContext) EvaluateRun(run *moduletest.Run, resultScope *lang.Scope, extraVariableVals terraform.InputValues) (moduletest.Status, cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if run.ModuleConfig == nil { + // This should never happen, but if it does, we can't evaluate the run + return moduletest.Error, cty.NilVal, tfdiags.Diagnostics{} + } + + mod := run.ModuleConfig.Module + // We need a derived evaluation scope that also supports referring to + // the prior run output values using the "run.NAME" syntax. + evalData := &evaluationData{ + ctx: ec, + module: mod, + current: resultScope.Data, + extraVars: extraVariableVals, + } + scope := &lang.Scope{ + Data: evalData, + ParseRef: addrs.ParseRefFromTestingScope, + SourceAddr: resultScope.SourceAddr, + BaseDir: resultScope.BaseDir, + PureOnly: resultScope.PureOnly, + PlanTimestamp: resultScope.PlanTimestamp, + ExternalFuncs: resultScope.ExternalFuncs, + } + + log.Printf("[TRACE] EvalContext.Evaluate for %s", run.Addr()) + + // We're going to assume the run has passed, and then if anything fails this + // value will be updated. + status := run.Status.Merge(moduletest.Pass) + + // Now validate all the assertions within this run block. + for i, rule := range run.Config.CheckRules { + var ruleDiags tfdiags.Diagnostics + + refs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.Condition) + ruleDiags = ruleDiags.Append(moreDiags) + moreRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, rule.ErrorMessage) + ruleDiags = ruleDiags.Append(moreDiags) + refs = append(refs, moreRefs...) + + // We want to emit diagnostics if users are using ephemeral resources in their checks + // as they are not supported since they are closed before this is evaluated. + // We do not remove the diagnostic about the ephemeral resource being closed already as it + // might be useful to the user. + ruleDiags = ruleDiags.Append(diagsForEphemeralResources(refs)) + + hclCtx, moreDiags := scope.EvalContext(refs) + ruleDiags = ruleDiags.Append(moreDiags) + if moreDiags.HasErrors() { + // if we can't evaluate the context properly, we can't evaulate the rule + // we add the diagnostics to the main diags and continue to the next rule + log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s is invalid, could not evalaute the context, so cannot evaluate it", i, run.Addr()) + status = status.Merge(moduletest.Error) + diags = diags.Append(ruleDiags) + continue + } + + errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, nil) + ruleDiags = ruleDiags.Append(moreDiags) + + runVal, hclDiags := rule.Condition.Value(hclCtx) + ruleDiags = ruleDiags.Append(hclDiags) + + diags = diags.Append(ruleDiags) + if ruleDiags.HasErrors() { + log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s is invalid, so cannot evaluate it", i, run.Addr()) + status = status.Merge(moduletest.Error) + continue + } + + if runVal.IsNull() { + status = status.Merge(moduletest.Error) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid condition run", + Detail: "Condition expression must return either true or false, not null.", + Subject: rule.Condition.Range().Ptr(), + Expression: rule.Condition, + EvalContext: hclCtx, + }) + log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has null condition result", i, run.Addr()) + continue + } + + if !runVal.IsKnown() { + status = status.Merge(moduletest.Error) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unknown condition value", + Detail: "Condition expression could not be evaluated at this time. This means you have executed a `run` block with `command = plan` and one of the values your condition depended on is not known until after the plan has been applied. Either remove this value from your condition, or execute an `apply` command from this `run` block. Alternatively, if there is an override for this value, you can make it available during the plan phase by setting `override_during = plan` in the `override_` block.", + Subject: rule.Condition.Range().Ptr(), + Expression: rule.Condition, + EvalContext: hclCtx, + }) + log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has unknown condition result", i, run.Addr()) + continue + } + + var err error + if runVal, err = convert.Convert(runVal, cty.Bool); err != nil { + status = status.Merge(moduletest.Error) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid condition run", + Detail: fmt.Sprintf("Invalid condition run value: %s.", tfdiags.FormatError(err)), + Subject: rule.Condition.Range().Ptr(), + Expression: rule.Condition, + EvalContext: hclCtx, + }) + log.Printf("[TRACE] EvalContext.Evaluate: check rule %d for %s has non-boolean condition result", i, run.Addr()) + continue + } + + // If the runVal refers to any sensitive values, then we'll have a + // sensitive mark on the resulting value. + runVal, _ = runVal.Unmark() + + if runVal.False() { + log.Printf("[TRACE] EvalContext.Evaluate: test assertion failed for %s assertion %d", run.Addr(), i) + status = status.Merge(moduletest.Fail) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Test assertion failed", + Detail: errorMessage, + Subject: rule.Condition.Range().Ptr(), + Expression: rule.Condition, + EvalContext: hclCtx, + // Diagnostic can be identified as originating from a failing test assertion. + // Also, values that are ephemeral, sensitive, or unknown are replaced with + // redacted values in renderings of the diagnostic. + Extra: DiagnosticCausedByTestFailure{Verbose: ec.verbose}, + }) + continue + } else { + log.Printf("[TRACE] EvalContext.Evaluate: test assertion succeeded for %s assertion %d", run.Addr(), i) + } + } + + // Our result includes an object representing all of the output values + // from the module we've just tested, which will then be available in + // any subsequent test cases in the same test suite. + outputVals := make(map[string]cty.Value, len(mod.Outputs)) + runRng := tfdiags.SourceRangeFromHCL(run.Config.DeclRange) + for _, oc := range mod.Outputs { + addr := oc.Addr() + v, moreDiags := scope.Data.GetOutput(addr, runRng) + diags = diags.Append(moreDiags) + if v == cty.NilVal { + v = cty.NullVal(cty.DynamicPseudoType) + } + outputVals[addr.Name] = v + } + + return status, cty.ObjectVal(outputVals), diags +} + +func (ec *EvalContext) SetOutput(run *moduletest.Run, output cty.Value) { + ec.outputsLock.Lock() + defer ec.outputsLock.Unlock() + ec.runOutputs[run.Addr()] = output +} + +func (ec *EvalContext) GetOutputs() map[addrs.Run]cty.Value { + ec.outputsLock.Lock() + defer ec.outputsLock.Unlock() + outputCopy := make(map[addrs.Run]cty.Value, len(ec.runOutputs)) + maps.Copy(outputCopy, ec.runOutputs) // don't use clone here, so we can return a non-nil map + return outputCopy +} + +func (ec *EvalContext) GetCache(run *moduletest.Run) *hcltest.VariableCache { + return ec.VariableCaches.GetCache(run.Name, run.ModuleConfig) +} + +// ProviderExists returns true if the provider exists for the run inside the context. +func (ec *EvalContext) ProviderExists(run *moduletest.Run, key string) bool { + ec.providersLock.Lock() + defer ec.providersLock.Unlock() + runProviders, ok := ec.configProviders[run.GetModuleConfigID()] + if !ok { + return false + } + + found, ok := runProviders[key] + return ok && found +} + +func (ec *EvalContext) SetProviders(run *moduletest.Run, providers map[string]bool) { + ec.providersLock.Lock() + defer ec.providersLock.Unlock() + ec.configProviders[run.GetModuleConfigID()] = providers +} + +func diagsForEphemeralResources(refs []*addrs.Reference) (diags tfdiags.Diagnostics) { + for _, ref := range refs { + switch v := ref.Subject.(type) { + case addrs.ResourceInstance: + if v.Resource.Mode == addrs.EphemeralResourceMode { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral resources cannot be asserted", + Detail: "Ephemeral resources are closed when the test is finished, and are not available within the test context for assertions.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + } + } + return diags +} + +func (ec *EvalContext) SetFileState(key string, state *TestFileState) { + ec.stateLock.Lock() + defer ec.stateLock.Unlock() + ec.FileStates[key] = &TestFileState{ + Run: state.Run, + State: state.State, + } +} + +func (ec *EvalContext) GetFileState(key string) *TestFileState { + ec.stateLock.Lock() + defer ec.stateLock.Unlock() + return ec.FileStates[key] +} + +// evaluationData augments an underlying lang.Data -- presumably resulting +// from a terraform.Context.PlanAndEval or terraform.Context.ApplyAndEval call -- +// with results from prior runs that should therefore be available when +// evaluating expressions written inside a "run" block. +type evaluationData struct { + ctx *EvalContext + module *configs.Module + current lang.Data + extraVars terraform.InputValues +} + +var _ lang.Data = (*evaluationData)(nil) + +// GetCheckBlock implements lang.Data. +func (d *evaluationData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetCheckBlock(addr, rng) +} + +// GetCountAttr implements lang.Data. +func (d *evaluationData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetCountAttr(addr, rng) +} + +// GetForEachAttr implements lang.Data. +func (d *evaluationData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetForEachAttr(addr, rng) +} + +// GetInputVariable implements lang.Data. +func (d *evaluationData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + if extra, exists := d.extraVars[addr.Name]; exists { + return extra.Value, nil + } + return d.current.GetInputVariable(addr, rng) +} + +// GetLocalValue implements lang.Data. +func (d *evaluationData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetLocalValue(addr, rng) +} + +// GetModule implements lang.Data. +func (d *evaluationData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetModule(addr, rng) +} + +// GetOutput implements lang.Data. +func (d *evaluationData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetOutput(addr, rng) +} + +// GetPathAttr implements lang.Data. +func (d *evaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetPathAttr(addr, rng) +} + +// GetResource implements lang.Data. +func (d *evaluationData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetResource(addr, rng) +} + +// GetRunBlock implements lang.Data. +func (d *evaluationData) GetRunBlock(addr addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret, exists := d.ctx.GetOutputs()[addr] + if !exists { + ret = cty.DynamicVal + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared run block", + Detail: fmt.Sprintf("There is no run %q declared in this test suite.", addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + } + if ret == cty.NilVal { + // An explicit nil value indicates that the block was declared but + // hasn't yet been visited. + ret = cty.DynamicVal + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unevaluated run block", + Detail: fmt.Sprintf("The run %q block has not yet been evaluated, so its results are not available here.", addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + } + return ret, diags +} + +// GetTerraformAttr implements lang.Data. +func (d *evaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return d.current.GetTerraformAttr(addr, rng) +} + +// StaticValidateReferences implements lang.Data. +func (d *evaluationData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { + // We only handle addrs.Run directly here, with everything else delegated + // to the underlying Data object to deal with. + var diags tfdiags.Diagnostics + for _, ref := range refs { + switch ref.Subject.(type) { + case addrs.Run: + diags = diags.Append(d.staticValidateRunRef(ref)) + default: + diags = diags.Append(d.current.StaticValidateReferences([]*addrs.Reference{ref}, self, source)) + } + } + return diags +} + +func (d *evaluationData) staticValidateRunRef(ref *addrs.Reference) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + addr := ref.Subject.(addrs.Run) + outputs := d.ctx.GetOutputs() + _, exists := outputs[addr] + if !exists { + var suggestions []string + for altAddr := range outputs { + suggestions = append(suggestions, altAddr.Name) + } + sort.Strings(suggestions) + suggestion := didyoumean.NameSuggestion(addr.Name, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + // A totally absent priorVals means that there is no run block with + // the given name at all. If it was declared but hasn't yet been + // evaluated then it would have an entry set to cty.NilVal. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared run block", + Detail: fmt.Sprintf("There is no run %q declared in this test suite.%s", addr.Name, suggestion), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + + return diags +} diff --git a/internal/moduletest/graph/eval_context_test.go b/internal/moduletest/graph/eval_context_test.go new file mode 100644 index 0000000000..e85f1d66cb --- /dev/null +++ b/internal/moduletest/graph/eval_context_test.go @@ -0,0 +1,850 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "context" + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configload" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/initwd" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/registry" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestEvalContext_Evaluate(t *testing.T) { + tests := map[string]struct { + configs map[string]string + state *states.State + plan *plans.Plan + variables terraform.InputValues + testOnlyVars terraform.InputValues + provider *testing_provider.MockProvider + priorOutputs map[string]cty.Value + + expectedDiags []tfdiags.Description + expectedStatus moduletest.Status + expectedOutputs cty.Value + }{ + "basic_passing": { + configs: map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + value = "Hello, world!" + } + `, + "main.tftest.hcl": ` + run "test_case" { + assert { + condition = test_resource.a.value == "Hello, world!" + error_message = "invalid value" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.EmptyObjectVal, + }, + "with_variables": { + configs: map[string]string{ + "main.tf": ` + variable "value" { + type = string + } + + resource "test_resource" "a" { + value = var.value + } + `, + "main.tftest.hcl": ` + variables { + value = "Hello, world!" + } + + run "test_case" { + assert { + condition = test_resource.a.value == var.value + error_message = "invalid value" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + variables: terraform.InputValues{ + "value": { + Value: cty.StringVal("Hello, world!"), + }, + }, + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.EmptyObjectVal, + }, + "basic_failing": { + configs: map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + value = "Hello, world!" + } + `, + "main.tftest.hcl": ` + run "test_case" { + assert { + condition = test_resource.a.value == "incorrect!" + error_message = "invalid value" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Fail, + expectedOutputs: cty.EmptyObjectVal, + expectedDiags: []tfdiags.Description{ + { + Summary: "Test assertion failed", + Detail: "invalid value", + }, + }, + }, + "two_failing_assertions": { + configs: map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + value = "Hello, world!" + } + `, + "main.tftest.hcl": ` + run "test_case" { + assert { + condition = test_resource.a.value == "incorrect!" + error_message = "invalid value" + } + + assert { + condition = test_resource.a.value == "also incorrect!" + error_message = "still invalid" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Fail, + expectedOutputs: cty.EmptyObjectVal, + expectedDiags: []tfdiags.Description{ + { + Summary: "Test assertion failed", + Detail: "invalid value", + }, + { + Summary: "Test assertion failed", + Detail: "still invalid", + }, + }, + }, + "sensitive_variables": { + configs: map[string]string{ + "main.tf": ` + variable "input" { + type = string + sensitive = true + } + `, + "main.tftest.hcl": ` + run "test" { + variables { + input = "Hello, world!" + } + + assert { + condition = var.input == "Hello, world!" + error_message = "bad" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.NewState(), + variables: terraform.InputValues{ + "input": &terraform.InputValue{ + Value: cty.StringVal("Hello, world!"), + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRange{ + Filename: "main.tftest.hcl", + Start: tfdiags.SourcePos{Line: 3, Column: 13, Byte: 12}, + End: tfdiags.SourcePos{Line: 3, Column: 28, Byte: 27}, + }, + }, + }, + provider: &testing_provider.MockProvider{}, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.EmptyObjectVal, + expectedDiags: []tfdiags.Description{}, + }, + "sensitive_variables_fail": { + configs: map[string]string{ + "main.tf": ` + variable "input" { + type = string + sensitive = true + } + `, + "main.tftest.hcl": ` + run "test" { + variables { + input = "Hello, world!" + } + + assert { + condition = var.input == "Hello, universe!" + error_message = "bad ${var.input}" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.NewState(), + variables: terraform.InputValues{ + "input": &terraform.InputValue{ + Value: cty.StringVal("Hello, world!"), + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRange{ + Filename: "main.tftest.hcl", + Start: tfdiags.SourcePos{Line: 3, Column: 13, Byte: 12}, + End: tfdiags.SourcePos{Line: 3, Column: 28, Byte: 27}, + }, + }, + }, + provider: &testing_provider.MockProvider{}, + expectedStatus: moduletest.Fail, + expectedOutputs: cty.EmptyObjectVal, + expectedDiags: []tfdiags.Description{ + { + Summary: "Error message refers to sensitive values", + Detail: "The error expression used to explain this condition refers to sensitive values, so Terraform will not display the resulting message.\n\nYou can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.", + }, + { + Summary: "Test assertion failed", + }, + }, + }, + "basic_passing_with_plan": { + configs: map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + value = "Hello, world!" + } + `, + "main.tftest.hcl": ` + run "test_case" { + command = plan + + assert { + condition = test_resource.a.value == "Hello, world!" + error_message = "invalid value" + } + } + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectPlanned, + AttrsJSON: encodeCtyValue(t, cty.NullVal(cty.Object(map[string]cty.Type{ + "value": cty.String, + }))), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + plan: &plans.Plan{ + Changes: &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: nil, + After: encodeDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + }, + }, + }, + }, + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.EmptyObjectVal, + }, + "basic_failing_with_plan": { + configs: map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + value = "Hello, world!" + } + `, + "main.tftest.hcl": ` + run "test_case" { + command = plan + + assert { + condition = test_resource.a.value == "incorrect!" + error_message = "invalid value" + } + } + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectPlanned, + AttrsJSON: encodeCtyValue(t, cty.NullVal(cty.Object(map[string]cty.Type{ + "value": cty.String, + }))), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + plan: &plans.Plan{ + Changes: &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: nil, + After: encodeDynamicValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + }, + }, + }, + }, + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Fail, + expectedOutputs: cty.EmptyObjectVal, + expectedDiags: []tfdiags.Description{ + { + Summary: "Test assertion failed", + Detail: "invalid value", + }, + }, + }, + "with_prior_state": { + configs: map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + value = "Hello, world!" + } + `, + "main.tftest.hcl": ` + run "setup" {} + + run "test_case" { + assert { + condition = test_resource.a.value == run.setup.value + error_message = "invalid value" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "a", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: encodeCtyValue(t, cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + })), + }, + addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + }) + }), + priorOutputs: map[string]cty.Value{ + "setup": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + }), + }, + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + }, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.EmptyObjectVal, + }, + "output_values": { + configs: map[string]string{ + "main.tf": ` + output "foo" { + value = "foo value" + } + output "bar" { + value = "bar value" + } + `, + "main.tftest.hcl": ` + run "test_case" {} + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.NewState(), + provider: &testing_provider.MockProvider{}, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("foo value"), + "bar": cty.StringVal("bar value"), + }), + }, + "provider_functions": { + configs: map[string]string{ + "main.tf": ` + terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } + } + output "true" { + value = true + } + `, + "main.tftest.hcl": ` + run "test_case" { + assert { + condition = provider::test::true() == output.true + error_message = "invalid value" + } + } + `, + }, + plan: &plans.Plan{ + Changes: plans.NewChangesSrc(), + }, + state: states.NewState(), + provider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionDecl{ + "true": { + ReturnType: cty.Bool, + }, + }, + }, + CallFunctionFn: func(request providers.CallFunctionRequest) providers.CallFunctionResponse { + if request.FunctionName != "true" { + return providers.CallFunctionResponse{ + Err: errors.New("unexpected function call"), + } + } + return providers.CallFunctionResponse{ + Result: cty.True, + } + }, + }, + expectedStatus: moduletest.Pass, + expectedOutputs: cty.ObjectVal(map[string]cty.Value{ + "true": cty.True, + }), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + config := testModuleInline(t, test.configs) + + tfCtx, diags := terraform.NewContext(&terraform.ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): providers.FactoryFixed(test.provider), + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors from NewContext\n%s", diags.Err().Error()) + } + + // We just need a vaguely-realistic scope here, so we'll make + // a plan against the given config and state and use its + // resulting scope. + _, planScope, diags := tfCtx.PlanAndEval(config, test.state, &terraform.PlanOpts{ + Mode: plans.NormalMode, + SetVariables: test.variables, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + file := config.Module.Tests["main.tftest.hcl"] + run := &moduletest.Run{ + Config: file.Runs[len(file.Runs)-1], // We always simulate the last run block. + Name: "test_case", // and it should be named test_case + ModuleConfig: config, + } + + priorOutputs := make(map[addrs.Run]cty.Value, len(test.priorOutputs)) + for name, val := range test.priorOutputs { + priorOutputs[addrs.Run{Name: name}] = val + } + + testCtx := NewEvalContext(&EvalContextOpts{ + CancelCtx: context.Background(), + StopCtx: context.Background(), + }) + testCtx.runOutputs = priorOutputs + gotStatus, gotOutputs, diags := testCtx.EvaluateRun(run, planScope, test.testOnlyVars) + + if got, want := gotStatus, test.expectedStatus; got != want { + t.Errorf("wrong status %q; want %q", got, want) + } + if diff := cmp.Diff(gotOutputs, test.expectedOutputs, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong output values\n%s", diff) + } + + compareDiagnosticsFromTestResult(t, test.expectedDiags, diags) + }) + } +} + +func compareDiagnosticsFromTestResult(t *testing.T, expected []tfdiags.Description, actual tfdiags.Diagnostics) { + if len(expected) != len(actual) { + t.Errorf("found invalid number of diagnostics, expected %d but found %d", len(expected), len(actual)) + } + + length := len(expected) + if len(actual) > length { + length = len(actual) + } + + for ix := 0; ix < length; ix++ { + if ix >= len(expected) { + t.Errorf("found extra diagnostic at %d:\n%v", ix, actual[ix].Description()) + } else if ix >= len(actual) { + t.Errorf("missing diagnostic at %d:\n%v", ix, expected[ix]) + } else { + expected := expected[ix] + actual := actual[ix].Description() + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("found different diagnostics at %d:\nexpected:\n%s\nactual:\n%s\ndiff:%s", ix, expected, actual, diff) + } + } + } +} + +func encodeDynamicValue(t *testing.T, value cty.Value) []byte { + data, err := ctymsgpack.Marshal(value, value.Type()) + if err != nil { + t.Fatalf("failed to marshal JSON: %s", err) + } + return data +} + +func encodeCtyValue(t *testing.T, value cty.Value) []byte { + data, err := ctyjson.Marshal(value, value.Type()) + if err != nil { + t.Fatalf("failed to marshal JSON: %s", err) + } + return data +} + +// testModuleInline takes a map of path -> config strings and yields a config +// structure with those files loaded from disk +func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { + t.Helper() + + cfgPath := t.TempDir() + + for path, configStr := range sources { + dir := filepath.Dir(path) + if dir != "." { + err := os.MkdirAll(filepath.Join(cfgPath, dir), os.FileMode(0777)) + if err != nil { + t.Fatalf("Error creating subdir: %s", err) + } + } + // Write the configuration + cfgF, err := os.Create(filepath.Join(cfgPath, path)) + if err != nil { + t.Fatalf("Error creating temporary file for config: %s", err) + } + + _, err = io.Copy(cfgF, strings.NewReader(configStr)) + cfgF.Close() + if err != nil { + t.Fatalf("Error creating temporary file for config: %s", err) + } + } + + loader, cleanup := configload.NewLoaderForTests(t) + defer cleanup() + + // Test modules usually do not refer to remote sources, and for local + // sources only this ultimately just records all of the module paths + // in a JSON file so that we can load them below. + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{}) + if instDiags.HasErrors() { + t.Fatal(instDiags.Err()) + } + + // Since module installer has modified the module manifest on disk, we need + // to refresh the cache of it in the loader. + if err := loader.RefreshModules(); err != nil { + t.Fatalf("failed to refresh modules after installation: %s", err) + } + + config, diags := loader.LoadConfigWithTests(cfgPath, "tests") + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + + return config +} diff --git a/internal/moduletest/graph/node_state_cleanup.go b/internal/moduletest/graph/node_state_cleanup.go new file mode 100644 index 0000000000..c2c43cb085 --- /dev/null +++ b/internal/moduletest/graph/node_state_cleanup.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ GraphNodeExecutable = (*NodeStateCleanup)(nil) +) + +type NodeStateCleanup struct { + stateKey string + opts *graphOptions +} + +func (n *NodeStateCleanup) Name() string { + return fmt.Sprintf("cleanup.%s", n.stateKey) +} + +// Execute destroys the resources created in the state file. +// This function should never return non-fatal error diagnostics, as that would +// prevent further cleanup from happening. Instead, the diagnostics +// will be rendered directly. +func (n *NodeStateCleanup) Execute(evalCtx *EvalContext) tfdiags.Diagnostics { + file := n.opts.File + state := evalCtx.GetFileState(n.stateKey) + log.Printf("[TRACE] TestStateManager: cleaning up state for %s", file.Name) + + if evalCtx.Cancelled() { + // Don't try and clean anything up if the execution has been cancelled. + log.Printf("[DEBUG] TestStateManager: skipping state cleanup for %s due to cancellation", file.Name) + return nil + } + + empty := true + if !state.State.Empty() { + for _, module := range state.State.Modules { + for _, resource := range module.Resources { + if resource.Addr.Resource.Mode == addrs.ManagedResourceMode { + empty = false + break + } + } + } + } + + if empty { + // The state can be empty for a run block that just executed a plan + // command, or a run block that only read data sources. We'll just + // skip empty run blocks. + return nil + } + + if state.Run == nil { + log.Printf("[ERROR] TestFileRunner: found inconsistent run block and state file in %s for module %s", file.Name, n.stateKey) + + // The state can have a nil run block if it only executed a plan + // command. In which case, we shouldn't have reached here as the + // state should also have been empty and this will have been skipped + // above. If we do reach here, then something has gone badly wrong + // and we can't really recover from it. + + diags := tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Inconsistent state", fmt.Sprintf("Found inconsistent state while cleaning up %s. This is a bug in Terraform - please report it", file.Name))} + file.UpdateStatus(moduletest.Error) + evalCtx.Renderer().DestroySummary(diags, nil, file, state.State) + + // intentionally return nil to allow further cleanup + return nil + } + TransformConfigForRun(evalCtx, state.Run, file) + + runNode := &NodeTestRun{run: state.Run, opts: n.opts} + updated := state.State + startTime := time.Now().UTC() + waiter := NewOperationWaiter(nil, evalCtx, runNode, moduletest.Running, startTime.UnixMilli()) + var destroyDiags tfdiags.Diagnostics + cancelled := waiter.Run(func() { + updated, destroyDiags = n.destroy(evalCtx, runNode, waiter) + }) + if cancelled { + destroyDiags = destroyDiags.Append(tfdiags.Sourceless(tfdiags.Error, "Test interrupted", "The test operation could not be completed due to an interrupt signal. Please read the remaining diagnostics carefully for any sign of failed state cleanup or dangling resources.")) + } + + if !updated.Empty() { + // Then we failed to adequately clean up the state, so mark success + // as false. + file.UpdateStatus(moduletest.Error) + } + evalCtx.Renderer().DestroySummary(destroyDiags, state.Run, file, updated) + return nil +} + +func (n *NodeStateCleanup) destroy(ctx *EvalContext, runNode *NodeTestRun, waiter *operationWaiter) (*states.State, tfdiags.Diagnostics) { + file := n.opts.File + fileState := ctx.GetFileState(n.stateKey) + state := fileState.State + run := runNode.run + log.Printf("[TRACE] TestFileRunner: called destroy for %s/%s", file.Name, run.Name) + + if state.Empty() { + // Nothing to do! + return state, nil + } + + var diags tfdiags.Diagnostics + variables, variableDiags := runNode.GetVariables(ctx, false) + diags = diags.Append(variableDiags) + + if diags.HasErrors() { + return state, diags + } + + // During the destroy operation, we don't add warnings from this operation. + // Anything that would have been reported here was already reported during + // the original plan, and a successful destroy operation is the only thing + // we care about. + setVariables, _, _ := runNode.FilterVariablesToModule(variables) + + planOpts := &terraform.PlanOpts{ + Mode: plans.DestroyMode, + SetVariables: setVariables, + Overrides: mocking.PackageOverrides(run.Config, file.Config, run.ModuleConfig), + } + + tfCtx, ctxDiags := terraform.NewContext(n.opts.ContextOpts) + diags = diags.Append(ctxDiags) + if ctxDiags.HasErrors() { + return state, diags + } + ctx.Renderer().Run(run, file, moduletest.TearDown, 0) + + waiter.update(tfCtx, moduletest.TearDown, nil) + plan, planDiags := tfCtx.Plan(run.ModuleConfig, state, planOpts) + diags = diags.Append(planDiags) + if diags.HasErrors() { + return state, diags + } + + _, updated, applyDiags := runNode.apply(tfCtx, plan, moduletest.TearDown, variables, waiter) + diags = diags.Append(applyDiags) + return updated, diags +} diff --git a/internal/moduletest/graph/node_test_run.go b/internal/moduletest/graph/node_test_run.go new file mode 100644 index 0000000000..124d743bc3 --- /dev/null +++ b/internal/moduletest/graph/node_test_run.go @@ -0,0 +1,155 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ GraphNodeExecutable = (*NodeTestRun)(nil) +) + +type NodeTestRun struct { + run *moduletest.Run + opts *graphOptions +} + +func (n *NodeTestRun) Run() *moduletest.Run { + return n.run +} + +func (n *NodeTestRun) File() *moduletest.File { + return n.opts.File +} + +func (n *NodeTestRun) Name() string { + return fmt.Sprintf("%s.%s", n.opts.File.Name, n.run.Name) +} + +func (n *NodeTestRun) References() []*addrs.Reference { + references, _ := n.run.GetReferences() + return references +} + +// Execute executes the test run block and update the status of the run block +// based on the result of the execution. +func (n *NodeTestRun) Execute(evalCtx *EvalContext) tfdiags.Diagnostics { + log.Printf("[TRACE] TestFileRunner: executing run block %s/%s", n.File().Name, n.run.Name) + startTime := time.Now().UTC() + var diags tfdiags.Diagnostics + file, run := n.File(), n.run + + // At the end of the function, we'll update the status of the file based on + // the status of the run block, and render the run summary. + defer func() { + evalCtx.Renderer().Run(run, file, moduletest.Complete, 0) + file.UpdateStatus(run.Status) + }() + + if file.GetStatus() == moduletest.Error { + // If the overall test file has errored, we don't keep trying to + // execute tests. Instead, we mark all remaining run blocks as + // skipped, print the status, and move on. + run.Status = moduletest.Skip + return diags + } + if evalCtx.Cancelled() { + // A cancellation signal has been received. + // Don't do anything, just give up and return immediately. + // The surrounding functions should stop this even being called, but in + // case of race conditions or something we can still verify this. + return diags + } + + if evalCtx.Stopped() { + // Then the test was requested to be stopped, so we just mark each + // following test as skipped, print the status, and move on. + run.Status = moduletest.Skip + return diags + } + + // Create a waiter which handles waiting for terraform operations to complete. + // While waiting, the wait will also respond to cancellation signals, and + // handle them appropriately. + // The test progress is updated periodically, and the progress status + // depends on the async operation being waited on. + // Before the terraform operation is started, the operation updates the + // waiter with the cleanup context on cancellation, as well as the + // progress status. + waiter := NewOperationWaiter(nil, evalCtx, n, moduletest.Running, startTime.UnixMilli()) + cancelled := waiter.Run(func() { + defer logging.PanicHandler() + n.execute(evalCtx, waiter) + }) + + if cancelled { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Test interrupted", "The test operation could not be completed due to an interrupt signal. Please read the remaining diagnostics carefully for any sign of failed state cleanup or dangling resources.")) + } + + // If we got far enough to actually attempt to execute the run then + // we'll give the view some additional metadata about the execution. + n.run.ExecutionMeta = &moduletest.RunExecutionMeta{ + Start: startTime, + Duration: time.Since(startTime), + } + return diags +} + +func (n *NodeTestRun) execute(ctx *EvalContext, waiter *operationWaiter) { + file, run := n.File(), n.run + ctx.Renderer().Run(run, file, moduletest.Starting, 0) + if run.Config.ConfigUnderTest != nil && run.GetStateKey() == moduletest.MainStateIdentifier { + // This is bad, and should not happen because the state key is derived from the custom module source. + panic(fmt.Sprintf("TestFileRunner: custom module %s has the same key as main state", file.Name)) + } + + n.testValidate(ctx, waiter) + if run.Diagnostics.HasErrors() { + return + } + + variables, variableDiags := n.GetVariables(ctx, true) + run.Diagnostics = run.Diagnostics.Append(variableDiags) + if variableDiags.HasErrors() { + run.Status = moduletest.Error + return + } + + if run.Config.Command == configs.PlanTestCommand { + n.testPlan(ctx, variables, waiter) + } else { + n.testApply(ctx, variables, waiter) + } +} + +// Validating the module config which the run acts on +func (n *NodeTestRun) testValidate(ctx *EvalContext, waiter *operationWaiter) { + run := n.run + file := n.File() + config := run.ModuleConfig + + log.Printf("[TRACE] TestFileRunner: called validate for %s/%s", file.Name, run.Name) + TransformConfigForRun(ctx, run, file) + tfCtx, ctxDiags := terraform.NewContext(n.opts.ContextOpts) + if ctxDiags.HasErrors() { + return + } + waiter.update(tfCtx, moduletest.Running, nil) + validateDiags := tfCtx.Validate(config, nil) + run.Diagnostics = run.Diagnostics.Append(validateDiags) + if validateDiags.HasErrors() { + run.Status = moduletest.Error + return + } +} diff --git a/internal/moduletest/graph/plan.go b/internal/moduletest/graph/plan.go new file mode 100644 index 0000000000..f803d84999 --- /dev/null +++ b/internal/moduletest/graph/plan.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + "log" + "path/filepath" + + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func (n *NodeTestRun) testPlan(ctx *EvalContext, variables terraform.InputValues, waiter *operationWaiter) { + file, run := n.File(), n.run + config := run.ModuleConfig + + // FilterVariablesToModule only returns warnings, so we don't check the + // returned diags for errors. + setVariables, testOnlyVariables, setVariableDiags := n.FilterVariablesToModule(variables) + run.Diagnostics = run.Diagnostics.Append(setVariableDiags) + + // ignore diags because validate has covered it + tfCtx, _ := terraform.NewContext(n.opts.ContextOpts) + + // execute the terraform plan operation + planScope, plan, planDiags := n.plan(ctx, tfCtx, setVariables, waiter) + // We exclude the diagnostics that are expected to fail from the plan + // diagnostics, and if an expected failure is not found, we add a new error diagnostic. + planDiags = run.ValidateExpectedFailures(planDiags) + run.Diagnostics = run.Diagnostics.Append(planDiags) + if planDiags.HasErrors() { + run.Status = moduletest.Error + return + } + + n.AddVariablesToConfig(variables) + + if ctx.Verbose() { + schemas, diags := tfCtx.Schemas(config, plan.PriorState) + + // If we're going to fail to render the plan, let's not fail the overall + // test. It can still have succeeded. So we'll add the diagnostics, but + // still report the test status as a success. + if diags.HasErrors() { + // This is very unlikely. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Failed to print verbose output", + fmt.Sprintf("Terraform failed to print the verbose output for %s, other diagnostics will contain more details as to why.", filepath.Join(file.Name, run.Name)))) + } else { + run.Verbose = &moduletest.Verbose{ + Plan: plan, + State: nil, // We don't have a state to show in plan mode. + Config: config, + Providers: schemas.Providers, + Provisioners: schemas.Provisioners, + } + } + + run.Diagnostics = run.Diagnostics.Append(diags) + } + + // Evaluate the run block directly in the graph context to validate the assertions + // of the run. We also pass in all the + // previous contexts so this run block can refer to outputs from + // previous run blocks. + newStatus, outputVals, moreDiags := ctx.EvaluateRun(run, planScope, testOnlyVariables) + run.Status = newStatus + run.Diagnostics = run.Diagnostics.Append(moreDiags) + + // Now we've successfully validated this run block, lets add it into + // our prior run outputs so future run blocks can access it. + ctx.SetOutput(run, outputVals) +} + +func (n *NodeTestRun) plan(ctx *EvalContext, tfCtx *terraform.Context, variables terraform.InputValues, waiter *operationWaiter) (*lang.Scope, *plans.Plan, tfdiags.Diagnostics) { + file, run := n.File(), n.run + config := run.ModuleConfig + log.Printf("[TRACE] TestFileRunner: called plan for %s/%s", file.Name, run.Name) + + var diags tfdiags.Diagnostics + + targets, targetDiags := run.GetTargets() + diags = diags.Append(targetDiags) + + replaces, replaceDiags := run.GetReplaces() + diags = diags.Append(replaceDiags) + + if diags.HasErrors() { + return nil, nil, diags + } + + planOpts := &terraform.PlanOpts{ + Mode: func() plans.Mode { + switch run.Config.Options.Mode { + case configs.RefreshOnlyTestMode: + return plans.RefreshOnlyMode + default: + return plans.NormalMode + } + }(), + Targets: targets, + ForceReplace: replaces, + SkipRefresh: !run.Config.Options.Refresh, + SetVariables: variables, + ExternalReferences: n.References(), + Overrides: mocking.PackageOverrides(run.Config, file.Config, config), + } + + waiter.update(tfCtx, moduletest.Running, nil) + log.Printf("[DEBUG] TestFileRunner: starting plan for %s/%s", file.Name, run.Name) + state := ctx.GetFileState(run.GetStateKey()).State + plan, planScope, planDiags := tfCtx.PlanAndEval(config, state, planOpts) + log.Printf("[DEBUG] TestFileRunner: completed plan for %s/%s", file.Name, run.Name) + diags = diags.Append(planDiags) + + return planScope, plan, diags +} diff --git a/internal/moduletest/graph/test_graph_builder.go b/internal/moduletest/graph/test_graph_builder.go new file mode 100644 index 0000000000..1ee7a697a1 --- /dev/null +++ b/internal/moduletest/graph/test_graph_builder.go @@ -0,0 +1,81 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestGraphBuilder is a GraphBuilder implementation that builds a graph for +// a terraform test file. The file may contain multiple runs, and each run may have +// dependencies on other runs. +type TestGraphBuilder struct { + File *moduletest.File + GlobalVars map[string]backendrun.UnparsedVariableValue + ContextOpts *terraform.ContextOpts +} + +type graphOptions struct { + File *moduletest.File + GlobalVars map[string]backendrun.UnparsedVariableValue + ContextOpts *terraform.ContextOpts +} + +// See GraphBuilder +func (b *TestGraphBuilder) Build() (*terraform.Graph, tfdiags.Diagnostics) { + log.Printf("[TRACE] building graph for terraform test") + return (&terraform.BasicGraphBuilder{ + Steps: b.Steps(), + Name: "TestGraphBuilder", + }).Build(addrs.RootModuleInstance) +} + +// See GraphBuilder +func (b *TestGraphBuilder) Steps() []terraform.GraphTransformer { + opts := &graphOptions{ + File: b.File, + GlobalVars: b.GlobalVars, + ContextOpts: b.ContextOpts, + } + steps := []terraform.GraphTransformer{ + &TestRunTransformer{opts}, + &TestConfigTransformer{File: b.File}, + &TestStateCleanupTransformer{opts}, + terraform.DynamicTransformer(validateRunConfigs), + &TestProvidersTransformer{}, + &CloseTestGraphTransformer{}, + &terraform.TransitiveReductionTransformer{}, + } + + return steps +} + +func validateRunConfigs(g *terraform.Graph) error { + for _, v := range g.Vertices() { + if node, ok := v.(*NodeTestRun); ok { + diags := node.run.Config.Validate(node.run.ModuleConfig) + node.run.Diagnostics = node.run.Diagnostics.Append(diags) + if diags.HasErrors() { + node.run.Status = moduletest.Error + } + } + } + return nil +} + +// dynamicNode is a helper node which can be added to the graph to execute +// a dynamic function at some desired point in the graph. +type dynamicNode struct { + eval func(*EvalContext) tfdiags.Diagnostics +} + +func (n *dynamicNode) Execute(evalCtx *EvalContext) tfdiags.Diagnostics { + return n.eval(evalCtx) +} diff --git a/internal/moduletest/graph/transform_close_graph.go b/internal/moduletest/graph/transform_close_graph.go new file mode 100644 index 0000000000..c366c9fa33 --- /dev/null +++ b/internal/moduletest/graph/transform_close_graph.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/terraform" +) + +// CloseTestGraphTransformer is a GraphTransformer that adds a closing node to the graph. +type CloseTestGraphTransformer struct{} + +func (t *CloseTestGraphTransformer) Transform(g *terraform.Graph) error { + closeRoot := &nodeCloseTest{} + g.Add(closeRoot) + + for _, v := range g.Vertices() { + if v == closeRoot { + continue + } + + // since this is closing the graph, make it depend on everything in + // the graph that does not have a parent. Such nodes are the real roots + // of the graph, and since they are now siblings of the closing root node, + // they are allowed to run in parallel. + if g.UpEdges(v).Len() == 0 { + g.Connect(dag.BasicEdge(closeRoot, v)) + } + } + + return nil +} + +// This node doesn't do anything, it's just to ensure that we have a single +// root node that depends on everything in the graph. The nodes that it depends +// on are the real roots of the graph. +type nodeCloseTest struct { +} + +func (n *nodeCloseTest) Name() string { + return "testcloser" +} diff --git a/internal/moduletest/graph/transform_config.go b/internal/moduletest/graph/transform_config.go new file mode 100644 index 0000000000..317c670452 --- /dev/null +++ b/internal/moduletest/graph/transform_config.go @@ -0,0 +1,188 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/moduletest" + hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type GraphNodeExecutable interface { + Execute(ctx *EvalContext) tfdiags.Diagnostics +} + +// TestFileState is a helper struct that just maps a run block to the state that +// was produced by the execution of that run block. +type TestFileState struct { + Run *moduletest.Run + State *states.State +} + +// TestConfigTransformer is a GraphTransformer that adds all the test runs, +// and the variables defined in each run block, to the graph. +type TestConfigTransformer struct { + File *moduletest.File +} + +func (t *TestConfigTransformer) Transform(g *terraform.Graph) error { + // This map tracks the state of each run in the file. If multiple runs + // have the same state key, they will share the same state. + statesMap := make(map[string]*TestFileState) + + // a root config node that will add the file states to the context + rootConfigNode := t.addRootConfigNode(g, statesMap) + + for _, v := range g.Vertices() { + node, ok := v.(*NodeTestRun) + if !ok { + continue + } + key := node.run.GetStateKey() + if _, exists := statesMap[key]; !exists { + state := &TestFileState{ + Run: nil, + State: states.NewState(), + } + statesMap[key] = state + } + + // Connect all the test runs to the config node, so that the config node + // is executed before any of the test runs. + g.Connect(dag.BasicEdge(node, rootConfigNode)) + } + + return nil +} + +func (t *TestConfigTransformer) addRootConfigNode(g *terraform.Graph, statesMap map[string]*TestFileState) *dynamicNode { + rootConfigNode := &dynamicNode{ + eval: func(ctx *EvalContext) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + ctx.FileStates = statesMap + return diags + }, + } + g.Add(rootConfigNode) + return rootConfigNode +} + +// TransformConfigForRun transforms the run's module configuration to include +// the providers and variables from its block and the test file. +// +// In practice, this actually just means performing some surgery on the +// available providers. We want to copy the relevant providers from the test +// file into the configuration. We also want to process the providers so they +// use variables from the file instead of variables from within the test file. +func TransformConfigForRun(ctx *EvalContext, run *moduletest.Run, file *moduletest.File) hcl.Diagnostics { + var diags hcl.Diagnostics + + // Currently, we only need to override the provider settings. + // + // We can have a set of providers defined within the config, we can also + // have a set of providers defined within the test file. Then the run can + // also specify a set of overrides that tell Terraform exactly which + // providers from the test file to apply into the config. + // + // The process here is as follows: + // 1. Take all the providers in the original config keyed by name.alias, + // we call this `previous` + // 2. Copy them all into a new map, we call this `next`. + // 3a. If the run has configuration specifying provider overrides, we copy + // only the specified providers from the test file into `next`. While + // doing this we ensure to preserve the name and alias from the + // original config. + // 3b. If the run has no override configuration, we copy all the providers + // from the test file into `next`, overriding all providers with name + // collisions from the original config. + // 4. We then modify the original configuration so that the providers it + // holds are the combination specified by the original config, the test + // file and the run file. + // 5. We then return a function that resets the original config back to + // its original state. This can be called by the surrounding test once + // completed so future run blocks can safely execute. + + // First, initialise `previous` and `next`. `previous` contains a backup of + // the providers from the original config. `next` contains the set of + // providers that will be used by the test. `next` starts with the set of + // providers from the original config. + previous := run.ModuleConfig.Module.ProviderConfigs + next := make(map[string]*configs.Provider) + for key, value := range previous { + next[key] = value + } + + runOutputs := ctx.GetOutputs() + + if len(run.Config.Providers) > 0 { + // Then we'll only copy over and overwrite the specific providers asked + // for by this run block. + for _, ref := range run.Config.Providers { + testProvider, ok := file.Config.Providers[ref.InParent.String()] + if !ok { + // Then this reference was invalid as we didn't have the + // specified provider in the parent. This should have been + // caught earlier in validation anyway so is unlikely to happen. + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: fmt.Sprintf("Missing provider definition for %s", ref.InParent.String()), + Detail: "This provider block references a provider definition that does not exist.", + Subject: ref.InParent.NameRange.Ptr(), + }) + continue + } + + next[ref.InChild.String()] = &configs.Provider{ + Name: ref.InChild.Name, + NameRange: ref.InChild.NameRange, + Alias: ref.InChild.Alias, + AliasRange: ref.InChild.AliasRange, + Config: &hcltest.ProviderConfig{ + Original: testProvider.Config, + VariableCache: ctx.GetCache(run), + AvailableRunOutputs: runOutputs, + }, + Mock: testProvider.Mock, + MockData: testProvider.MockData, + DeclRange: testProvider.DeclRange, + } + } + } else { + // Otherwise, let's copy over and overwrite all providers specified by + // the test file itself. + for key, provider := range file.Config.Providers { + + if !ctx.ProviderExists(run, key) { + // Then we don't actually need this provider for this + // configuration, so skip it. + continue + } + + next[key] = &configs.Provider{ + Name: provider.Name, + NameRange: provider.NameRange, + Alias: provider.Alias, + AliasRange: provider.AliasRange, + Config: &hcltest.ProviderConfig{ + Original: provider.Config, + VariableCache: ctx.GetCache(run), + AvailableRunOutputs: runOutputs, + }, + Mock: provider.Mock, + MockData: provider.MockData, + DeclRange: provider.DeclRange, + } + } + } + + run.ModuleConfig.Module.ProviderConfigs = next + return diags +} diff --git a/internal/moduletest/graph/transform_config_test.go b/internal/moduletest/graph/transform_config_test.go new file mode 100644 index 0000000000..7db4763033 --- /dev/null +++ b/internal/moduletest/graph/transform_config_test.go @@ -0,0 +1,263 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "bytes" + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/moduletest" +) + +func TestTransformForTest(t *testing.T) { + + str := func(providers map[string]string) string { + var buffer bytes.Buffer + for key, config := range providers { + buffer.WriteString(fmt.Sprintf("%s: %s\n", key, config)) + } + return buffer.String() + } + + convertToProviders := func(t *testing.T, contents map[string]string) map[string]*configs.Provider { + t.Helper() + + providers := make(map[string]*configs.Provider) + for key, content := range contents { + parser := hclparse.NewParser() + file, diags := parser.ParseHCL([]byte(content), fmt.Sprintf("%s.hcl", key)) + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + + provider := &configs.Provider{ + Config: file.Body, + } + + parts := strings.Split(key, ".") + provider.Name = parts[0] + if len(parts) > 1 { + provider.Alias = parts[1] + } + + providers[key] = provider + } + return providers + } + + tcs := map[string]struct { + configProviders map[string]string + fileProviders map[string]string + runProviders []configs.PassedProviderConfig + expectedProviders map[string]string + expectedErrors []string + }{ + "empty": { + configProviders: make(map[string]string), + expectedProviders: make(map[string]string), + }, + "only providers in config": { + configProviders: map[string]string{ + "foo": "source = \"config\"", + "bar": "source = \"config\"", + }, + expectedProviders: map[string]string{ + "foo": "source = \"config\"", + "bar": "source = \"config\"", + }, + }, + "only providers in test file": { + configProviders: make(map[string]string), + fileProviders: map[string]string{ + "foo": "source = \"testfile\"", + "bar": "source = \"testfile\"", + }, + expectedProviders: map[string]string{ + "foo": "source = \"testfile\"", + "bar": "source = \"testfile\"", + }, + }, + "only providers in run block": { + configProviders: make(map[string]string), + runProviders: []configs.PassedProviderConfig{ + { + InChild: &configs.ProviderConfigRef{ + Name: "foo", + }, + InParent: &configs.ProviderConfigRef{ + Name: "bar", + }, + }, + }, + expectedProviders: make(map[string]string), + expectedErrors: []string{ + ":0,0-0: Missing provider definition for bar; This provider block references a provider definition that does not exist.", + }, + }, + "subset of providers in test file": { + configProviders: make(map[string]string), + fileProviders: map[string]string{ + "bar": "source = \"testfile\"", + }, + runProviders: []configs.PassedProviderConfig{ + { + InChild: &configs.ProviderConfigRef{ + Name: "foo", + }, + InParent: &configs.ProviderConfigRef{ + Name: "bar", + }, + }, + }, + expectedProviders: map[string]string{ + "foo": "source = \"testfile\"", + }, + }, + "overrides providers in config": { + configProviders: map[string]string{ + "foo": "source = \"config\"", + "bar": "source = \"config\"", + }, + fileProviders: map[string]string{ + "bar": "source = \"testfile\"", + }, + expectedProviders: map[string]string{ + "foo": "source = \"config\"", + "bar": "source = \"testfile\"", + }, + }, + "overrides subset of providers in config": { + configProviders: map[string]string{ + "foo": "source = \"config\"", + "bar": "source = \"config\"", + }, + fileProviders: map[string]string{ + "foo": "source = \"testfile\"", + "bar": "source = \"testfile\"", + }, + runProviders: []configs.PassedProviderConfig{ + { + InChild: &configs.ProviderConfigRef{ + Name: "bar", + }, + InParent: &configs.ProviderConfigRef{ + Name: "bar", + }, + }, + }, + expectedProviders: map[string]string{ + "foo": "source = \"config\"", + "bar": "source = \"testfile\"", + }, + }, + "handles aliases": { + configProviders: map[string]string{ + "foo.primary": "source = \"config\"", + "foo.secondary": "source = \"config\"", + }, + fileProviders: map[string]string{ + "foo": "source = \"testfile\"", + }, + runProviders: []configs.PassedProviderConfig{ + { + InChild: &configs.ProviderConfigRef{ + Name: "foo.secondary", + }, + InParent: &configs.ProviderConfigRef{ + Name: "foo", + }, + }, + }, + expectedProviders: map[string]string{ + "foo.primary": "source = \"config\"", + "foo.secondary": "source = \"testfile\"", + }, + }, + "ignores unexpected providers in test file": { + configProviders: make(map[string]string), + fileProviders: map[string]string{ + "foo": "source = \"testfile\"", + "bar": "source = \"testfile\"", + }, + expectedProviders: map[string]string{ + "foo": "source = \"testfile\"", + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + config := &configs.Config{ + Module: &configs.Module{ + ProviderConfigs: convertToProviders(t, tc.configProviders), + }, + } + + file := &moduletest.File{ + Config: &configs.TestFile{ + Providers: convertToProviders(t, tc.fileProviders), + }, + } + + run := &moduletest.Run{ + Config: &configs.TestRun{ + Providers: tc.runProviders, + }, + ModuleConfig: config, + } + + availableProviders := make(map[string]bool, len(tc.expectedProviders)) + for provider := range tc.expectedProviders { + availableProviders[provider] = true + } + + ctx := NewEvalContext(&EvalContextOpts{ + CancelCtx: context.Background(), + StopCtx: context.Background(), + }) + ctx.configProviders = map[string]map[string]bool{ + run.GetModuleConfigID(): availableProviders, + } + + diags := TransformConfigForRun(ctx, run, file) + + var actualErrs []string + for _, err := range diags.Errs() { + actualErrs = append(actualErrs, err.Error()) + } + if diff := cmp.Diff(actualErrs, tc.expectedErrors, cmpopts.IgnoreUnexported()); len(diff) > 0 { + t.Errorf("unmatched errors\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", strings.Join(tc.expectedErrors, "\n"), strings.Join(actualErrs, "\n"), diff) + } + + converted := make(map[string]string) + for key, provider := range config.Module.ProviderConfigs { + content, err := provider.Config.Content(&hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "source", Required: true}, + }, + }) + if err != nil { + t.Fatal(err) + } + + source, diags := content.Attributes["source"].Expr.Value(nil) + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + converted[key] = fmt.Sprintf("source = %q", source.AsString()) + } + + if diff := cmp.Diff(tc.expectedProviders, converted); len(diff) > 0 { + t.Errorf("%s\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", "after transform mismatch", str(tc.expectedProviders), str(converted), diff) + } + }) + } +} diff --git a/internal/moduletest/graph/transform_providers.go b/internal/moduletest/graph/transform_providers.go new file mode 100644 index 0000000000..793b647f42 --- /dev/null +++ b/internal/moduletest/graph/transform_providers.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestProvidersTransformer is a GraphTransformer that gathers all the providers +// from the module configurations that the test runs depend on and attaches the +// required providers to the test run nodes. +type TestProvidersTransformer struct{} + +func (t *TestProvidersTransformer) Transform(g *terraform.Graph) error { + configsProviderMap := make(map[string]map[string]bool) + runProviderMap := make(map[*NodeTestRun]map[string]bool) + + // a root provider node that will add the providers to the context + rootProviderNode := t.createRootNode(g, runProviderMap) + + for _, v := range g.Vertices() { + node, ok := v.(*NodeTestRun) + if !ok { + continue + } + + // Get the providers that the test run depends on + configKey := node.run.GetModuleConfigID() + if _, ok := configsProviderMap[configKey]; !ok { + providers := t.transformSingleConfig(node.run.ModuleConfig) + configsProviderMap[configKey] = providers + } + runProviderMap[node] = configsProviderMap[configKey] + + // Add an edge from the test run node to the root provider node + g.Connect(dag.BasicEdge(v, rootProviderNode)) + } + + return nil +} + +func (t *TestProvidersTransformer) transformSingleConfig(config *configs.Config) map[string]bool { + providers := make(map[string]bool) + + // First, let's look at the required providers first. + for _, provider := range config.Module.ProviderRequirements.RequiredProviders { + providers[provider.Name] = true + for _, alias := range provider.Aliases { + providers[alias.StringCompact()] = true + } + } + + // Second, we look at the defined provider configs. + for _, provider := range config.Module.ProviderConfigs { + providers[provider.Addr().StringCompact()] = true + } + + // Third, we look at the resources and data sources. + for _, resource := range config.Module.ManagedResources { + if resource.ProviderConfigRef != nil { + providers[resource.ProviderConfigRef.String()] = true + continue + } + providers[resource.Provider.Type] = true + } + for _, datasource := range config.Module.DataResources { + if datasource.ProviderConfigRef != nil { + providers[datasource.ProviderConfigRef.String()] = true + continue + } + providers[datasource.Provider.Type] = true + } + + // Finally, we look at any module calls to see if any providers are used + // in there. + for _, module := range config.Module.ModuleCalls { + for _, provider := range module.Providers { + providers[provider.InParent.String()] = true + } + } + + return providers +} + +func (t *TestProvidersTransformer) createRootNode(g *terraform.Graph, providerMap map[*NodeTestRun]map[string]bool) *dynamicNode { + node := &dynamicNode{ + eval: func(ctx *EvalContext) tfdiags.Diagnostics { + for node, providers := range providerMap { + ctx.SetProviders(node.run, providers) + } + return nil + }, + } + g.Add(node) + return node +} diff --git a/internal/moduletest/graph/transform_state_cleanup.go b/internal/moduletest/graph/transform_state_cleanup.go new file mode 100644 index 0000000000..86ebd22507 --- /dev/null +++ b/internal/moduletest/graph/transform_state_cleanup.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "slices" + + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestStateCleanupTransformer is a GraphTransformer that adds a cleanup node +// for each state that is created by the test runs. +type TestStateCleanupTransformer struct { + opts *graphOptions +} + +func (t *TestStateCleanupTransformer) Transform(g *terraform.Graph) error { + cleanupMap := make(map[string]*NodeStateCleanup) + + for _, v := range g.Vertices() { + node, ok := v.(*NodeTestRun) + if !ok { + continue + } + key := node.run.GetStateKey() + if _, exists := cleanupMap[key]; !exists { + cleanupMap[key] = &NodeStateCleanup{stateKey: key, opts: t.opts} + g.Add(cleanupMap[key]) + } + + // Connect the cleanup node to the test run node. + g.Connect(dag.BasicEdge(cleanupMap[key], node)) + } + + // Add a root cleanup node that runs before cleanup nodes for each state. + // Right now it just simply renders a teardown summary, so as to maintain + // existing CLI output. + rootCleanupNode := t.addRootCleanupNode(g) + + for _, v := range g.Vertices() { + switch node := v.(type) { + case *NodeTestRun: + // All the runs that share the same state, must share the same cleanup node, + // which only executes once after all the dependent runs have completed. + g.Connect(dag.BasicEdge(rootCleanupNode, node)) + case *NodeStateCleanup: + // Connect the cleanup node to the root cleanup node. + g.Connect(dag.BasicEdge(node, rootCleanupNode)) + } + } + + // connect all cleanup nodes in reverse-sequential order of run index to + // preserve existing behavior, starting from the root cleanup node. + // TODO: Parallelize cleanup nodes execution instead of sequential. + added := make(map[string]bool) + var prev dag.Vertex + for _, v := range slices.Backward(t.opts.File.Runs) { + key := v.GetStateKey() + if _, exists := added[key]; !exists { + node := cleanupMap[key] + if prev != nil { + g.Connect(dag.BasicEdge(node, prev)) + } + prev = node + added[key] = true + } + } + + return nil +} + +func (t *TestStateCleanupTransformer) addRootCleanupNode(g *terraform.Graph) *dynamicNode { + rootCleanupNode := &dynamicNode{ + eval: func(ctx *EvalContext) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + ctx.Renderer().File(t.opts.File, moduletest.TearDown) + return diags + }, + } + g.Add(rootCleanupNode) + return rootCleanupNode +} diff --git a/internal/moduletest/graph/transform_test_run.go b/internal/moduletest/graph/transform_test_run.go new file mode 100644 index 0000000000..9e652aacec --- /dev/null +++ b/internal/moduletest/graph/transform_test_run.go @@ -0,0 +1,156 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestRunTransformer is a GraphTransformer that adds all the test runs, +// and the variables defined in each run block, to the graph. +type TestRunTransformer struct { + opts *graphOptions +} + +func (t *TestRunTransformer) Transform(g *terraform.Graph) error { + // Create and add nodes for each run + var nodes []*NodeTestRun + for _, run := range t.opts.File.Runs { + node := &NodeTestRun{run: run, opts: t.opts} + g.Add(node) + nodes = append(nodes, node) + } + + // Connect nodes based on dependencies + if diags := t.connectDependencies(g, nodes); diags.HasErrors() { + return tfdiags.DiagnosticsAsError{Diagnostics: diags} + } + + // Runs with the same state key inherently depend on each other, so we + // connect them sequentially. + t.connectSameStateRuns(g, nodes) + + return nil +} + +func (t *TestRunTransformer) connectDependencies(g *terraform.Graph, nodes []*NodeTestRun) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + nodeMap := make(map[string]*NodeTestRun) + // add all nodes to the map. They are initialized to nil, + // and we will update them as we iterate through the nodes in the next loop. + for _, node := range nodes { + nodeMap[node.run.Name] = nil + } + + for _, node := range nodes { + nodeMap[node.run.Name] = node // node encountered, so update the map + + // check for variable references + varRefs := t.getVariableNames(node.run) + + refs, refDiags := node.run.GetReferences() + if refDiags.HasErrors() { + return diags.Append(refDiags) + } + for _, ref := range refs { + switch subj := ref.Subject.(type) { + case addrs.Run: + dependency, ok := nodeMap[subj.Name] + diagPrefix := "You can only reference run blocks that are in the same test file and will execute before the current run block." + // Then this is a made up run block, and it doesn't exist at all. + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown run block", + Detail: fmt.Sprintf("The run block %q does not exist within this test file. %s", subj.Name, diagPrefix), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue + } + + // This run block exists, but it is after the current run block. + if dependency == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unavailable run block", + Detail: fmt.Sprintf("The run block %q has not executed yet. %s", subj.Name, diagPrefix), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue + } + + g.Connect(dag.BasicEdge(node, dependency)) + case addrs.InputVariable: + if _, ok := varRefs[subj.Name]; !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unavailable variable", + Detail: fmt.Sprintf("The input variable %q is not available to the current run block. You can only reference variables defined at the file or global levels.", subj.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + } + } + } + + // If there is a run that has opted out of parallelism, we will connect it + // sequentially to all previous and subsequent runs. This effectively + // divides the parallelizable runs into separate groups, ensuring that + // non-parallelizable runs are executed in sequence with respect to all + // other runs. + for i, node := range nodes { + if node.run.Config.Parallel { + continue + } + + // Connect to all previous runs + for j := 0; j < i; j++ { + g.Connect(dag.BasicEdge(node, nodes[j])) + } + + // Connect to all subsequent runs + for j := i + 1; j < len(nodes); j++ { + g.Connect(dag.BasicEdge(nodes[j], node)) + } + } + return diags +} + +func (t *TestRunTransformer) connectSameStateRuns(g *terraform.Graph, nodes []*NodeTestRun) { + stateRuns := make(map[string][]*NodeTestRun) + for _, node := range nodes { + key := node.run.GetStateKey() + stateRuns[key] = append(stateRuns[key], node) + } + for _, runs := range stateRuns { + for i := 1; i < len(runs); i++ { + g.Connect(dag.BasicEdge(runs[i], runs[i-1])) + } + } +} + +func (t *TestRunTransformer) getVariableNames(run *moduletest.Run) map[string]struct{} { + set := make(map[string]struct{}) + for name := range t.opts.GlobalVars { + set[name] = struct{}{} + } + for name := range run.Config.Variables { + set[name] = struct{}{} + } + + for name := range t.opts.File.Config.Variables { + set[name] = struct{}{} + } + for name := range run.ModuleConfig.Module.Variables { + set[name] = struct{}{} + } + return set +} diff --git a/internal/moduletest/graph/variables.go b/internal/moduletest/graph/variables.go new file mode 100644 index 0000000000..0c19524683 --- /dev/null +++ b/internal/moduletest/graph/variables.go @@ -0,0 +1,239 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/langrefs" + hcltest "github.com/hashicorp/terraform/internal/moduletest/hcl" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +// GetVariables builds the terraform.InputValues required for the provided run +// block. It pulls the relevant variables (ie. the variables needed for the +// run block) from the total pool of all available variables, and converts them +// into input values. +// +// As a run block can reference variables defined within the file and are not +// actually defined within the configuration, this function actually returns +// more variables than are required by the config. FilterVariablesToConfig +// should be called before trying to use these variables within a Terraform +// plan, apply, or destroy operation. +func (n *NodeTestRun) GetVariables(ctx *EvalContext, includeWarnings bool) (terraform.InputValues, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + run := n.run + // relevantVariables contains the variables that are of interest to this + // run block. This is a combination of the variables declared within the + // configuration for this run block, and the variables referenced by the + // run block assertions. + relevantVariables := make(map[string]bool) + + // First, we'll check to see which variables the run block assertions + // reference. + for _, reference := range n.References() { + if addr, ok := reference.Subject.(addrs.InputVariable); ok { + relevantVariables[addr.Name] = true + } + } + + // And check to see which variables the run block configuration references. + for name := range run.ModuleConfig.Module.Variables { + relevantVariables[name] = true + } + + // We'll put the parsed values into this map. + values := make(terraform.InputValues) + + // First, let's step through the expressions within the run block and work + // them out. + for name, expr := range run.Config.Variables { + requiredValues := make(map[string]cty.Value) + + refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr) + for _, ref := range refs { + if addr, ok := ref.Subject.(addrs.InputVariable); ok { + cache := ctx.GetCache(run) + + value, valueDiags := cache.GetFileVariable(addr.Name) + diags = diags.Append(valueDiags) + if value != nil { + requiredValues[addr.Name] = value.Value + continue + } + + // Otherwise, it might be a global variable. + value, valueDiags = cache.GetGlobalVariable(addr.Name) + diags = diags.Append(valueDiags) + if value != nil { + requiredValues[addr.Name] = value.Value + continue + } + } + } + diags = diags.Append(refDiags) + + ctx, ctxDiags := hcltest.EvalContext(hcltest.TargetRunBlock, map[string]hcl.Expression{name: expr}, requiredValues, ctx.GetOutputs()) + diags = diags.Append(ctxDiags) + + value := cty.DynamicVal + if !ctxDiags.HasErrors() { + var valueDiags hcl.Diagnostics + value, valueDiags = expr.Value(ctx) + diags = diags.Append(valueDiags) + } + + // We do this late on so we still validate whatever it was that the user + // wrote in the variable expression. But, we don't want to actually use + // it if it's not actually relevant. + if _, exists := relevantVariables[name]; !exists { + // Do not display warnings during cleanup phase + if includeWarnings { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Value for undeclared variable", + Detail: fmt.Sprintf("The module under test does not declare a variable named %q, but it is declared in run block %q.", name, run.Name), + Subject: expr.Range().Ptr(), + }) + } + continue // Don't add it to our final set of variables. + } + + values[name] = &terraform.InputValue{ + Value: value, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(expr.Range()), + } + } + + for variable := range relevantVariables { + if _, exists := values[variable]; exists { + // Then we've already got a value for this variable. + continue + } + + // Otherwise, we'll get it from the cache as a file-level or global + // variable. + cache := ctx.GetCache(run) + + value, valueDiags := cache.GetFileVariable(variable) + diags = diags.Append(valueDiags) + if value != nil { + values[variable] = value + continue + } + + value, valueDiags = cache.GetGlobalVariable(variable) + diags = diags.Append(valueDiags) + if value != nil { + values[variable] = value + continue + } + } + + // Finally, we check the configuration again. This is where we'll discover + // if there's any missing variables and fill in any optional variables that + // don't have a value already. + + for name, variable := range run.ModuleConfig.Module.Variables { + if _, exists := values[name]; exists { + // Then we've provided a variable for this. It's all good. + continue + } + + // Otherwise, we're going to give these variables a value. They'll be + // processed by the Terraform graph and provided a default value later + // if they have one. + + if variable.Required() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No value for required variable", + Detail: fmt.Sprintf("The module under test for run block %q has a required variable %q with no set value. Use a -var or -var-file command line argument or add this variable into a \"variables\" block within the test file or run block.", + run.Name, variable.Name), + Subject: variable.DeclRange.Ptr(), + }) + + values[name] = &terraform.InputValue{ + Value: cty.DynamicVal, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange), + } + } else { + values[name] = &terraform.InputValue{ + Value: cty.NilVal, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(variable.DeclRange), + } + } + } + + return values, diags +} + +// FilterVariablesToModule splits the provided values into two disjoint maps: +// moduleVars contains the ones that correspond with declarations in the root +// module of the given configuration, while testOnlyVars contains any others +// that are presumably intended only for use in the test configuration file. +// +// This function is essentially the opposite of AddVariablesToConfig which +// makes the config match the variables rather than the variables match the +// config. +// +// This function can only return warnings, and the callers can rely on this so +// please check the callers of this function if you add any error diagnostics. +func (n *NodeTestRun) FilterVariablesToModule(values terraform.InputValues) (moduleVars, testOnlyVars terraform.InputValues, diags tfdiags.Diagnostics) { + moduleVars = make(terraform.InputValues) + testOnlyVars = make(terraform.InputValues) + for name, value := range values { + _, exists := n.run.ModuleConfig.Module.Variables[name] + if !exists { + // If it's not in the configuration then it's a test-only variable. + testOnlyVars[name] = value + continue + } + + moduleVars[name] = value + } + return moduleVars, testOnlyVars, diags +} + +// AddVariablesToConfig extends the provided config to ensure it has definitions +// for all specified variables. +// +// This function is essentially the opposite of FilterVariablesToConfig which +// makes the variables match the config rather than the config match the +// variables. +func (n *NodeTestRun) AddVariablesToConfig(variables terraform.InputValues) { + run := n.run + // If we have got variable values from the test file we need to make sure + // they have an equivalent entry in the configuration. We're going to do + // that dynamically here. + + // First, take a backup of the existing configuration so we can easily + // restore it later. + currentVars := make(map[string]*configs.Variable) + for name, variable := range run.ModuleConfig.Module.Variables { + currentVars[name] = variable + } + + for name, value := range variables { + if _, exists := run.ModuleConfig.Module.Variables[name]; exists { + continue + } + + run.ModuleConfig.Module.Variables[name] = &configs.Variable{ + Name: name, + Type: value.Value.Type(), + ConstraintType: value.Value.Type(), + DeclRange: value.SourceRange.ToHCL(), + } + } + +} diff --git a/internal/moduletest/graph/wait.go b/internal/moduletest/graph/wait.go new file mode 100644 index 0000000000..fd219de478 --- /dev/null +++ b/internal/moduletest/graph/wait.go @@ -0,0 +1,163 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package graph + +import ( + "context" + "fmt" + "log" + "sync/atomic" + "time" + + "github.com/hashicorp/terraform/internal/command/views" + "github.com/hashicorp/terraform/internal/moduletest" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" +) + +// operationWaiter waits for an operation within +// a test run execution to complete. +type operationWaiter struct { + ctx *terraform.Context + runningCtx context.Context + run *moduletest.Run + file *moduletest.File + created []*plans.ResourceInstanceChangeSrc + progress atomicProgress[moduletest.Progress] + start int64 + identifier string + finished bool + evalCtx *EvalContext + renderer views.Test +} + +type atomicProgress[T moduletest.Progress] struct { + internal atomic.Value +} + +func (a *atomicProgress[T]) Load() T { + return a.internal.Load().(T) +} + +func (a *atomicProgress[T]) Store(progress T) { + a.internal.Store(progress) +} + +// NewOperationWaiter creates a new operation waiter. +func NewOperationWaiter(ctx *terraform.Context, evalCtx *EvalContext, n *NodeTestRun, + progress moduletest.Progress, start int64) *operationWaiter { + identifier := "validate" + if n.File() != nil { + identifier = n.File().Name + if n.run != nil { + identifier = fmt.Sprintf("%s/%s", identifier, n.run.Name) + } + } + + p := atomicProgress[moduletest.Progress]{} + p.Store(progress) + + return &operationWaiter{ + ctx: ctx, + run: n.run, + file: n.File(), + progress: p, + start: start, + identifier: identifier, + evalCtx: evalCtx, + renderer: evalCtx.Renderer(), + } +} + +// Run executes the given function in a goroutine and waits for it to finish. +// If the function finishes, it returns false. If the function is cancelled or +// interrupted, it returns true. +func (w *operationWaiter) Run(fn func()) bool { + runningCtx, doneRunning := context.WithCancel(context.Background()) + w.runningCtx = runningCtx + + go func() { + fn() + doneRunning() + }() + + // either the function finishes or a cancel/stop signal is received + return w.wait() +} + +func (w *operationWaiter) wait() bool { + log.Printf("[TRACE] TestFileRunner: waiting for execution during %s", w.identifier) + + for !w.finished { + select { + case <-time.After(2 * time.Second): + w.updateProgress() + case <-w.evalCtx.stopContext.Done(): + // Soft cancel - wait for completion or hard cancel + for !w.finished { + select { + case <-time.After(2 * time.Second): + w.updateProgress() + case <-w.evalCtx.cancelContext.Done(): + return w.handleCancelled() + case <-w.runningCtx.Done(): + w.finished = true + } + } + case <-w.evalCtx.cancelContext.Done(): + return w.handleCancelled() + case <-w.runningCtx.Done(): + w.finished = true + } + } + + return false +} + +// update refreshes the operationWaiter with the latest terraform context, progress, and any newly created resources. +// This should be called before starting a new Terraform operation. +func (w *operationWaiter) update(ctx *terraform.Context, progress moduletest.Progress, created []*plans.ResourceInstanceChangeSrc) { + w.ctx = ctx + w.progress.Store(progress) + w.created = created +} + +func (w *operationWaiter) updateProgress() { + now := time.Now().UTC().UnixMilli() + progress := w.progress.Load() + w.renderer.Run(w.run, w.file, progress, now-w.start) +} + +// handleCancelled is called when the test execution is hard cancelled. +func (w *operationWaiter) handleCancelled() bool { + log.Printf("[DEBUG] TestFileRunner: test execution cancelled during %s", w.identifier) + states := make(map[*moduletest.Run]*states.State) + mainKey := moduletest.MainStateIdentifier + states[nil] = w.evalCtx.GetFileState(mainKey).State + for key, module := range w.evalCtx.FileStates { + if key == mainKey { + continue + } + states[module.Run] = module.State + } + w.renderer.FatalInterruptSummary(w.run, w.file, states, w.created) + + go func() { + if w.ctx != nil { + w.ctx.Stop() + } + }() + + for !w.finished { + select { + case <-time.After(2 * time.Second): + w.updateProgress() + case <-w.runningCtx.Done(): + w.finished = true + } + } + + return true +} diff --git a/internal/moduletest/hcl/context.go b/internal/moduletest/hcl/context.go new file mode 100644 index 0000000000..24ae4fd3fe --- /dev/null +++ b/internal/moduletest/hcl/context.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hcl + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type EvalContextTarget string + +const ( + TargetRunBlock EvalContextTarget = "run" + TargetProvider EvalContextTarget = "provider" + TargetFileVariable EvalContextTarget = "file" +) + +// EvalContext builds hcl.EvalContext objects for use directly within the +// testing framework. +// +// We support referencing variables from the file variables block, and any +// global variables provided via the CLI / environment variables / .tfvars +// files. These should be provided in the availableVariables argument, already +// parsed and ready for use. +// +// We also support referencing outputs from any previous run blocks. These +// should be provided in the availableRunBlocks argument. As we also perform +// validation (see below) the format of this argument matters. If it is +// completely null, then we do not support the `run` argument at all in this +// context. If a run block is not present at all, then we should return a "run +// block does not exist" error. If the run block is present, but contains a +// nil context, then we should return a "run block has not yet executed" error. +// Finally, if the run block is present and contains a valid value we should +// use that value in the returned HCL contexts. +// +// As referenced above, this function performs pre-validation to make sure the +// expressions to be evaluated will pass evaluation. Anything present in the +// expressions argument will be validated to make sure the only reference the +// availableVariables and availableRunBlocks. +// +// We perform some pre-validation of the expected expressions that this context +// will be used to evaluate. This is just so we can provide some better error +// messages and diagnostics. The expressions argument could be empty without +// affecting the returned context. +func EvalContext(target EvalContextTarget, expressions map[string]hcl.Expression, availableVariables map[string]cty.Value, availableRunOutputs map[addrs.Run]cty.Value) (*hcl.EvalContext, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + runs := make(map[string]cty.Value, len(availableRunOutputs)) + for addr, objVal := range availableRunOutputs { + runs[addr.Name] = objVal + } + + for _, expression := range expressions { + refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expression) + diags = diags.Append(refDiags) + + for _, ref := range refs { + if addr, ok := ref.Subject.(addrs.Run); ok { + if target == TargetFileVariable { + // You can't reference run blocks from within the file + // variables block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: "You can not reference run blocks from within the file variables block.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue + } + + objVal, exists := availableRunOutputs[addr] + + var diagPrefix string + switch target { + case TargetRunBlock: + diagPrefix = "You can only reference run blocks that are in the same test file and will execute before the current run block." + case TargetProvider: + diagPrefix = "You can only reference run blocks that are in the same test file and will execute before the provider is required." + } + + if !exists { + // Then this is a made up run block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown run block", + Detail: fmt.Sprintf("The run block %q does not exist within this test file. %s", addr.Name, diagPrefix), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + + continue + } + + if objVal == cty.NilVal { + // This run block exists, but it is after the current run block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unavailable run block", + Detail: fmt.Sprintf("The run block %q has not executed yet. %s", addr.Name, diagPrefix), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + + continue + } + + value, valueDiags := ref.Remaining.TraverseRel(runs[addr.Name]) + diags = diags.Append(valueDiags) + if valueDiags.HasErrors() { + // This means the reference was invalid somehow, we've + // already added the errors to our diagnostics though so + // we'll just carry on. + continue + } + + if !value.IsWhollyKnown() { + // This is not valid, we cannot allow users to pass unknown + // values into run blocks. There's just going to be + // difficult and confusing errors later if this happens. + // + // When reporting this we assume that it's happened because + // the prior run was a plan-only run and that some of its + // output values were not known. If this arises for a + // run that performed a full apply then this is a bug in + // Terraform's modules runtime, because unknown output + // values should not be possible in that case. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown value", + Detail: fmt.Sprintf("The value for %s is unknown. Run block %q is executing a \"plan\" operation, and the specified output value is only known after apply.", ref.DisplayString(), addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue + } + + continue + } + + if addr, ok := ref.Subject.(addrs.InputVariable); ok { + if _, exists := availableVariables[addr.Name]; !exists { + // This variable reference doesn't exist. + + var detail string + switch target { + case TargetRunBlock: + detail = fmt.Sprintf("The input variable %q is not available to the current run block. You can only reference variables defined at the file or global levels.", addr.Name) + case TargetProvider: + detail = fmt.Sprintf("The input variable %q is not available to the current provider configuration. You can only reference variables defined at the file or global levels.", addr.Name) + case TargetFileVariable: + detail = fmt.Sprintf("The input variable %q is not available to the current context. You can only reference global variables.", addr.Name) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unavailable variable", + Detail: detail, + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + + continue + } + + // Otherwise, we're good. This is an acceptable reference. + continue + } + + var detail string + switch target { + case TargetRunBlock: + detail = "You can only reference earlier run blocks, file level, and global variables while defining variables from inside a run block." + case TargetProvider: + detail = "You can only reference run blocks, file level, and global variables while defining variables from inside provider configurations." + case TargetFileVariable: + detail = "You can only reference global variables within the test file variables block." + } + + // You can only reference run blocks and variables from the run + // block variables. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: detail, + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + } + + return &hcl.EvalContext{ + Variables: func() map[string]cty.Value { + variables := make(map[string]cty.Value) + variables["var"] = cty.ObjectVal(availableVariables) + if availableRunOutputs != nil { + variables["run"] = cty.ObjectVal(runs) + } + return variables + }(), + Functions: lang.TestingFunctions(), + }, diags +} diff --git a/internal/moduletest/hcl/provider.go b/internal/moduletest/hcl/provider.go new file mode 100644 index 0000000000..c5bce33de7 --- /dev/null +++ b/internal/moduletest/hcl/provider.go @@ -0,0 +1,139 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hcl + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/langrefs" +) + +var _ hcl.Body = (*ProviderConfig)(nil) + +// ProviderConfig is an implementation of an hcl.Block that evaluates the +// attributes within the block using the provided config and variables before +// returning them. +// +// This is used by configs.Provider objects that are defined within the test +// framework, so they should only use variables available to the test framework +// but are instead initialised within the Terraform graph so we have to delay +// evaluation of their attributes until the schemas are retrieved. +// +// We don't parse the attributes until they are requested, so we can only use +// unparsed values and hcl.Expressions within the struct itself. +type ProviderConfig struct { + Original hcl.Body + + VariableCache *VariableCache + AvailableRunOutputs map[addrs.Run]cty.Value +} + +func (p *ProviderConfig) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { + content, diags := p.Original.Content(schema) + attrs, attrDiags := p.transformAttributes(content.Attributes) + diags = append(diags, attrDiags...) + + return &hcl.BodyContent{ + Attributes: attrs, + Blocks: p.transformBlocks(content.Blocks), + MissingItemRange: content.MissingItemRange, + }, diags +} + +func (p *ProviderConfig) PartialContent(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Body, hcl.Diagnostics) { + content, rest, diags := p.Original.PartialContent(schema) + attrs, attrDiags := p.transformAttributes(content.Attributes) + diags = append(diags, attrDiags...) + + return &hcl.BodyContent{ + Attributes: attrs, + Blocks: p.transformBlocks(content.Blocks), + MissingItemRange: content.MissingItemRange, + }, &ProviderConfig{rest, p.VariableCache, p.AvailableRunOutputs}, diags +} + +func (p *ProviderConfig) JustAttributes() (hcl.Attributes, hcl.Diagnostics) { + originals, diags := p.Original.JustAttributes() + attrs, moreDiags := p.transformAttributes(originals) + return attrs, append(diags, moreDiags...) +} + +func (p *ProviderConfig) MissingItemRange() hcl.Range { + return p.Original.MissingItemRange() +} + +func (p *ProviderConfig) transformAttributes(originals hcl.Attributes) (hcl.Attributes, hcl.Diagnostics) { + var diags hcl.Diagnostics + + availableVariables := make(map[string]cty.Value) + + exprs := make(map[string]hcl.Expression, len(originals)) + for _, original := range originals { + exprs[original.Name] = original.Expr + + // We also need to parse the variables we're going to use, so we extract + // the references from this expression now and see if they reference any + // input variables. If we find an input variable, we'll copy it into + // our availableVariables local. + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, original.Expr) + for _, ref := range refs { + if addr, ok := ref.Subject.(addrs.InputVariable); ok { + value, valueDiags := p.VariableCache.GetFileVariable(addr.Name) + diags = append(diags, valueDiags.ToHCL()...) + if value != nil { + availableVariables[addr.Name] = value.Value + continue + } + + // If the variable wasn't a file variable, it might be a global. + value, valueDiags = p.VariableCache.GetGlobalVariable(addr.Name) + diags = append(diags, valueDiags.ToHCL()...) + if value != nil { + availableVariables[addr.Name] = value.Value + continue + } + } + } + } + + ctx, ctxDiags := EvalContext(TargetProvider, exprs, availableVariables, p.AvailableRunOutputs) + diags = append(diags, ctxDiags.ToHCL()...) + if ctxDiags.HasErrors() { + return nil, diags + } + + attrs := make(hcl.Attributes, len(originals)) + for name, attr := range originals { + value, valueDiags := attr.Expr.Value(ctx) + diags = append(diags, valueDiags...) + if valueDiags.HasErrors() { + continue + } else { + attrs[name] = &hcl.Attribute{ + Name: name, + Expr: hcl.StaticExpr(value, attr.Expr.Range()), + Range: attr.Range, + NameRange: attr.NameRange, + } + } + } + return attrs, diags +} + +func (p *ProviderConfig) transformBlocks(originals hcl.Blocks) hcl.Blocks { + blocks := make(hcl.Blocks, len(originals)) + for name, block := range originals { + blocks[name] = &hcl.Block{ + Type: block.Type, + Labels: block.Labels, + Body: &ProviderConfig{block.Body, p.VariableCache, p.AvailableRunOutputs}, + DefRange: block.DefRange, + TypeRange: block.TypeRange, + LabelRanges: block.LabelRanges, + } + } + return blocks +} diff --git a/internal/moduletest/hcl/provider_test.go b/internal/moduletest/hcl/provider_test.go new file mode 100644 index 0000000000..9e0f66fb36 --- /dev/null +++ b/internal/moduletest/hcl/provider_test.go @@ -0,0 +1,226 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hcl + +import ( + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" +) + +func TestProviderConfig(t *testing.T) { + + tcs := map[string]struct { + content string + schema *hcl.BodySchema + variables map[string]cty.Value + runBlockOutputs map[string]map[string]cty.Value + validate func(t *testing.T, content *hcl.BodyContent) + expectedErrors []string + }{ + "simple_no_vars": { + content: "attribute = \"string\"", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + equals(t, content, "attribute", cty.StringVal("string")) + }, + }, + "simple_var_ref": { + content: "attribute = var.input", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + variables: map[string]cty.Value{ + "input": cty.StringVal("string"), + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + equals(t, content, "attribute", cty.StringVal("string")) + }, + }, + "missing_var_ref": { + content: "attribute = var.missing", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + variables: map[string]cty.Value{ + "input": cty.StringVal("string"), + }, + expectedErrors: []string{ + "The input variable \"missing\" is not available to the current provider configuration. You can only reference variables defined at the file or global levels.", + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + if len(content.Attributes) > 0 { + t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes)) + } + }, + }, + "simple_run_block": { + content: "attribute = run.setup.value", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + runBlockOutputs: map[string]map[string]cty.Value{ + "setup": { + "value": cty.StringVal("string"), + }, + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + equals(t, content, "attribute", cty.StringVal("string")) + }, + }, + "missing_run_block": { + content: "attribute = run.missing.value", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + runBlockOutputs: map[string]map[string]cty.Value{ + "setup": { + "value": cty.StringVal("string"), + }, + }, + expectedErrors: []string{ + "The run block \"missing\" does not exist within this test file. You can only reference run blocks that are in the same test file and will execute before the provider is required.", + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + if len(content.Attributes) > 0 { + t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes)) + } + }, + }, + "late_run_block": { + content: "attribute = run.setup.value", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + runBlockOutputs: map[string]map[string]cty.Value{ + "setup": nil, + }, + expectedErrors: []string{ + "The run block \"setup\" has not executed yet. You can only reference run blocks that are in the same test file and will execute before the provider is required.", + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + if len(content.Attributes) > 0 { + t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes)) + } + }, + }, + "invalid_ref": { + content: "attribute = data.type.name.value", + schema: &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "attribute", + }, + }, + }, + runBlockOutputs: map[string]map[string]cty.Value{ + "setup": nil, + }, + expectedErrors: []string{ + "You can only reference run blocks, file level, and global variables while defining variables from inside provider configurations.", + }, + validate: func(t *testing.T, content *hcl.BodyContent) { + if len(content.Attributes) > 0 { + t.Errorf("should have excluded the invalid attribute but found %d", len(content.Attributes)) + } + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + file, diags := hclsyntax.ParseConfig([]byte(tc.content), "main.tf", hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + t.Fatalf("failed to parse hcl: %s", diags.Error()) + } + + outputs := make(map[addrs.Run]cty.Value) + for name, values := range tc.runBlockOutputs { + addr := addrs.Run{Name: name} + if values == nil { + outputs[addr] = cty.NilVal + continue + } + + attrs := make(map[string]cty.Value) + for name, value := range values { + attrs[name] = value + } + + outputs[addr] = cty.ObjectVal(attrs) + } + + variableCaches := NewVariableCaches(func(vc *VariableCaches) { + vc.FileVariables = func() map[string]hcl.Expression { + variables := make(map[string]hcl.Expression) + for name, value := range tc.variables { + variables[name] = hcl.StaticExpr(value, hcl.Range{}) + } + return variables + }() + }) + + config := ProviderConfig{ + Original: file.Body, + VariableCache: variableCaches.GetCache("test", nil), + AvailableRunOutputs: outputs, + } + + content, diags := config.Content(tc.schema) + + var actualErrs []string + for _, diag := range diags { + actualErrs = append(actualErrs, diag.Detail) + } + if diff := cmp.Diff(actualErrs, tc.expectedErrors); len(diff) > 0 { + t.Errorf("unmatched errors\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", strings.Join(tc.expectedErrors, "\n"), strings.Join(actualErrs, "\n"), diff) + } + + tc.validate(t, content) + }) + } +} + +func equals(t *testing.T, content *hcl.BodyContent, attribute string, expected cty.Value) { + value, diags := content.Attributes[attribute].Expr.Value(nil) + if diags.HasErrors() { + t.Errorf("failed to get value from attribute %s: %s", attribute, diags.Error()) + } + if !value.RawEquals(expected) { + t.Errorf("expected:\n%s\nbut got:\n%s", expected.GoString(), value.GoString()) + } +} diff --git a/internal/moduletest/hcl/variable_cache.go b/internal/moduletest/hcl/variable_cache.go new file mode 100644 index 0000000000..cc1298b16c --- /dev/null +++ b/internal/moduletest/hcl/variable_cache.go @@ -0,0 +1,199 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hcl + +import ( + "sync" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// VariableCaches contains a mapping between test run blocks and evaluated +// variables. This is used to cache the results of evaluating variables so that +// they are only evaluated once per run. +// +// Each run block has its own configuration and therefore its own set of +// evaluated variables. +type VariableCaches struct { + GlobalVariables map[string]backendrun.UnparsedVariableValue + FileVariables map[string]hcl.Expression + + caches map[string]*VariableCache + cacheLock sync.Mutex +} + +func NewVariableCaches(opts ...func(*VariableCaches)) *VariableCaches { + ret := &VariableCaches{ + GlobalVariables: make(map[string]backendrun.UnparsedVariableValue), + FileVariables: make(map[string]hcl.Expression), + caches: make(map[string]*VariableCache), + cacheLock: sync.Mutex{}, + } + + for _, opt := range opts { + opt(ret) + } + + return ret +} + +// VariableCache contains the cache for a single run block. This cache contains +// the evaluated values for global and file-level variables. +type VariableCache struct { + config *configs.Config + + globals terraform.InputValues + files terraform.InputValues + + values *VariableCaches // back reference so we can access the stored values +} + +// GetCache returns the cache for the named run. If the cache does not exist, it +// is created and returned. +func (caches *VariableCaches) GetCache(name string, config *configs.Config) *VariableCache { + caches.cacheLock.Lock() + defer caches.cacheLock.Unlock() + cache, exists := caches.caches[name] + if !exists { + cache = &VariableCache{ + config: config, + globals: make(terraform.InputValues), + files: make(terraform.InputValues), + values: caches, + } + caches.caches[name] = cache + } + return cache +} + +// GetGlobalVariable returns a value for the named global variable evaluated +// against the current run. +// +// This function caches the result of evaluating the variable so that it is +// only evaluated once per run. +// +// This function will return a valid input value if parsing fails for any reason +// so the caller can continue processing the configuration. The diagnostics +// returned will contain the error message that occurred during parsing and as +// such should be shown to the user. +func (cache *VariableCache) GetGlobalVariable(name string) (*terraform.InputValue, tfdiags.Diagnostics) { + val, exists := cache.globals[name] + if exists { + return val, nil + } + + variable, exists := cache.values.GlobalVariables[name] + if !exists { + return nil, nil + } + + // TODO: We should also introduce a way to specify the mode in the test + // file itself. Suggestion, optional variable blocks. + parsingMode := configs.VariableParseHCL + + if cfg, exists := cache.config.Module.Variables[name]; exists { + parsingMode = cfg.ParsingMode + } + + value, diags := variable.ParseVariableValue(parsingMode) + if diags.HasErrors() { + // In this case, the variable exists but we couldn't parse it. We'll + // return a usable value so that we don't compound errors later by + // claiming a variable doesn't exist when it does. We also return the + // diagnostics explaining the error which will be shown to the user. + value = &terraform.InputValue{ + Value: cty.DynamicVal, + } + } + + cache.globals[name] = value + return value, diags +} + +// GetFileVariable returns a value for the named file-level variable evaluated +// against the current run. +// +// This function caches the result of evaluating the variable so that it is +// only evaluated once per run. +// +// This function will return a valid input value if parsing fails for any reason +// so the caller can continue processing the configuration. The diagnostics +// returned will contain the error message that occurred during parsing and as +// such should be shown to the user. +func (cache *VariableCache) GetFileVariable(name string) (*terraform.InputValue, tfdiags.Diagnostics) { + val, exists := cache.files[name] + if exists { + return val, nil + } + + expr, exists := cache.values.FileVariables[name] + if !exists { + return nil, nil + } + + var diags tfdiags.Diagnostics + + availableVariables := make(map[string]cty.Value) + refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr) + for _, ref := range refs { + if input, ok := ref.Subject.(addrs.InputVariable); ok { + variable, variableDiags := cache.GetGlobalVariable(input.Name) + diags = diags.Append(variableDiags) + if variable != nil { + availableVariables[input.Name] = variable.Value + } + } + } + diags = diags.Append(refDiags) + + if diags.HasErrors() { + // There's no point trying to evaluate the variable as we know it will + // fail. We'll just return a usable value so that we don't compound + // errors later by claiming a variable doesn't exist when it does. We + // also return the diagnostics explaining the error which will be shown + // to the user. + cache.files[name] = &terraform.InputValue{ + Value: cty.DynamicVal, + } + return cache.files[name], diags + } + + ctx, ctxDiags := EvalContext(TargetFileVariable, map[string]hcl.Expression{name: expr}, availableVariables, nil) + diags = diags.Append(ctxDiags) + + if ctxDiags.HasErrors() { + // If we couldn't build the context, we won't actually process these + // variables. Instead, we'll fill them with an empty value but still + // make a note that the user did provide them. + cache.files[name] = &terraform.InputValue{ + Value: cty.DynamicVal, + } + return cache.files[name], diags + } + + value, valueDiags := expr.Value(ctx) + diags = diags.Append(valueDiags) + if diags.HasErrors() { + // In this case, the variable exists but we couldn't parse it. We'll + // return a usable value so that we don't compound errors later by + // claiming a variable doesn't exist when it does. We also return the + // diagnostics explaining the error which will be shown to the user. + value = cty.DynamicVal + } + + cache.files[name] = &terraform.InputValue{ + Value: value, + SourceType: terraform.ValueFromConfig, + SourceRange: tfdiags.SourceRangeFromHCL(expr.Range()), + } + return cache.files[name], diags +} diff --git a/internal/moduletest/hcl/variable_cache_test.go b/internal/moduletest/hcl/variable_cache_test.go new file mode 100644 index 0000000000..d67b97c8de --- /dev/null +++ b/internal/moduletest/hcl/variable_cache_test.go @@ -0,0 +1,234 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hcl + +import ( + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/backend/backendrun" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" + + "testing" +) + +func TestFileVariables(t *testing.T) { + + tcs := map[string]struct { + Values map[string]string + GlobalValues map[string]string + Variables map[string]configs.VariableParsingMode + Want map[string]cty.Value + }{ + "no_variables": { + Want: make(map[string]cty.Value), + }, + "string": { + Values: map[string]string{ + "foo": `"bar"`, + }, + Want: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + }, + "boolean": { + Values: map[string]string{ + "foo": "true", + }, + Want: map[string]cty.Value{ + "foo": cty.BoolVal(true), + }, + }, + "reference": { + Values: map[string]string{ + "foo": "var.bar", + }, + GlobalValues: map[string]string{ + "bar": `"baz"`, + }, + Variables: map[string]configs.VariableParsingMode{ + "foo": configs.VariableParseLiteral, + }, + Want: map[string]cty.Value{ + "foo": cty.StringVal("baz"), + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + caches := NewVariableCaches(func(vc *VariableCaches) { + vc.FileVariables = func() map[string]hcl.Expression { + vars := make(map[string]hcl.Expression) + for name, value := range tc.Values { + expr, diags := hclsyntax.ParseExpression([]byte(value), "test.tf", hcl.Pos{Line: 0, Column: 0, Byte: 0}) + if len(diags) > 0 { + t.Fatalf("unexpected errors: %v", diags) + } + vars[name] = expr + } + return vars + }() + vc.GlobalVariables = func() map[string]backendrun.UnparsedVariableValue { + vars := make(map[string]backendrun.UnparsedVariableValue) + for name, value := range tc.GlobalValues { + vars[name] = &variable{name, value} + } + return vars + }() + }) + config := makeConfigWithVariables(tc.Variables) + + cache := caches.GetCache("test", config) + got := make(map[string]cty.Value) + for name := range tc.Want { + value, diags := cache.GetFileVariable(name) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %v", diags) + } + got[name] = value.Value + } + + if diff := cmp.Diff(tc.Want, got, ctydebug.CmpOptions); len(diff) > 0 { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } +} + +func TestGlobalVariables(t *testing.T) { + + tcs := map[string]struct { + Values map[string]string + Variables map[string]configs.VariableParsingMode + Want map[string]cty.Value + }{ + "no_variables": { + Want: make(map[string]cty.Value), + }, + "string": { + Values: map[string]string{ + "foo": "bar", + }, + Variables: map[string]configs.VariableParsingMode{ + "foo": configs.VariableParseLiteral, + }, + Want: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + }, + "boolean_string": { + Values: map[string]string{ + "foo": "true", + }, + Variables: map[string]configs.VariableParsingMode{ + "foo": configs.VariableParseLiteral, + }, + Want: map[string]cty.Value{ + "foo": cty.StringVal("true"), + }, + }, + "boolean": { + Values: map[string]string{ + "foo": "true", + }, + Variables: map[string]configs.VariableParsingMode{ + "foo": configs.VariableParseHCL, + }, + Want: map[string]cty.Value{ + "foo": cty.BoolVal(true), + }, + }, + "string_hcl": { + Values: map[string]string{ + "foo": `"bar"`, + }, + Variables: map[string]configs.VariableParsingMode{ + "foo": configs.VariableParseHCL, + }, + Want: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + }, + "missing_config": { + Values: map[string]string{ + "foo": `"bar"`, + }, + Want: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + caches := NewVariableCaches(func(vc *VariableCaches) { + vc.GlobalVariables = func() map[string]backendrun.UnparsedVariableValue { + vars := make(map[string]backendrun.UnparsedVariableValue) + for name, value := range tc.Values { + vars[name] = &variable{name, value} + } + return vars + }() + }) + + config := makeConfigWithVariables(tc.Variables) + + cache := caches.GetCache("test", config) + got := make(map[string]cty.Value) + for name := range tc.Want { + value, diags := cache.GetGlobalVariable(name) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %v", diags) + } + got[name] = value.Value + } + + if diff := cmp.Diff(tc.Want, got, ctydebug.CmpOptions); len(diff) > 0 { + t.Fatalf("unexpected result\n%s", diff) + } + }) + } + +} + +func makeConfigWithVariables(modes map[string]configs.VariableParsingMode) *configs.Config { + return &configs.Config{ + Module: &configs.Module{ + Variables: func() map[string]*configs.Variable { + vars := make(map[string]*configs.Variable) + for name, mode := range modes { + vars[name] = &configs.Variable{ + ParsingMode: mode, + } + } + return vars + }(), + }, + } +} + +var _ backendrun.UnparsedVariableValue = (*variable)(nil) + +type variable struct { + name string + value string +} + +func (v *variable) ParseVariableValue(mode configs.VariableParsingMode) (*terraform.InputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + value, valueDiags := mode.Parse(v.name, v.value) + diags = diags.Append(valueDiags) + return &terraform.InputValue{ + Value: value, + SourceType: terraform.ValueFromUnknown, + }, diags +} diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go new file mode 100644 index 0000000000..e7f0549819 --- /dev/null +++ b/internal/moduletest/mocking/fill.go @@ -0,0 +1,272 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "fmt" + "sort" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +// FillAttribute makes the input value match the specified attribute by adding +// attributes and/or performing conversions to make the input value correct. +// +// It is similar to FillType, except it accepts attributes instead of types. +func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value, error) { + return fillAttribute(in, attribute, cty.Path{}) +} + +func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + if attribute.NestedType != nil { + + // Then the in value must be an object. + if !in.Type().IsObjectType() { + return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName()) + } + + switch attribute.NestedType.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + var names []string + for name := range attribute.NestedType.Attributes { + names = append(names, name) + } + if len(names) == 0 { + return cty.EmptyObjectVal, nil + } + + // Make the order we iterate through the attributes deterministic. We + // are generating random strings in here so it's worth making the + // operation repeatable. + sort.Strings(names) + + children := make(map[string]cty.Value) + for _, name := range names { + if in.Type().HasAttribute(name) { + child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) + if err != nil { + return cty.NilVal, err + } + children[name] = child + continue + } + children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name]) + } + return cty.ObjectVal(children), nil + case configschema.NestingSet: + return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil + case configschema.NestingList: + return cty.ListValEmpty(attribute.ImpliedType().ElementType()), nil + case configschema.NestingMap: + return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil + default: + panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting)) + } + } + + return fillType(in, attribute.Type, path) +} + +// FillType makes the input value match the target type by adding attributes +// directly to it or to any nested objects. Essentially, this is a "safe" +// conversion between two objects. +// +// This function can error if one of the embedded types within value doesn't +// match the type expected by target. +// +// If the supplied value isn't an object (or a map that can be treated as an +// object) then a normal conversion is attempted from value into target. +// +// Superfluous attributes within the supplied value (ie. attributes not +// mentioned by the target type) are dropped without error. +func FillType(in cty.Value, target cty.Type) (cty.Value, error) { + return fillType(in, target, cty.Path{}) +} + +func fillType(in cty.Value, target cty.Type, path cty.Path) (cty.Value, error) { + // If we're targeting an object directly, then the in value must be an + // object or a map. We'll check for those two cases specifically. + if target.IsObjectType() { + var attributes []string + for attribute := range target.AttributeTypes() { + attributes = append(attributes, attribute) + } + + // Make the order we iterate through the attributes deterministic. We + // are generating random strings in here so it's worth making the + // operation repeatable. + sort.Strings(attributes) + + if in.Type().IsObjectType() { + if len(attributes) == 0 { + return cty.EmptyObjectVal, nil + } + + children := make(map[string]cty.Value) + for _, attribute := range attributes { + if in.Type().HasAttribute(attribute) { + child, err := fillType(in.GetAttr(attribute), target.AttributeType(attribute), path.IndexString(attribute)) + if err != nil { + return cty.NilVal, err + } + children[attribute] = child + continue + } + children[attribute] = GenerateValueForType(target.AttributeType(attribute)) + } + return cty.ObjectVal(children), nil + } + + if in.Type().IsMapType() { + if len(attributes) == 0 { + return cty.EmptyObjectVal, nil + } + + children := make(map[string]cty.Value) + for _, attribute := range attributes { + attributeType := target.AttributeType(attribute) + index := cty.StringVal(attribute) + if in.HasIndex(index).True() { + child, err := fillType(in.Index(index), attributeType, path.Index(index)) + if err != nil { + return cty.NilVal, err + } + children[attribute] = child + continue + } + + children[attribute] = GenerateValueForType(attributeType) + } + return cty.ObjectVal(children), nil + } + + // If the target is an object type, and the input wasn't an object or + // a map, then we have incompatible types. + return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName()) + } + + // We also do a special check for any types that might contain an object as + // we'll need to recursively call fill over the nested objects. + if target.IsCollectionType() && target.ElementType().IsObjectType() { + switch { + case target.IsListType(): + var values []cty.Value + switch { + case in.Type().IsSetType(), in.Type().IsListType(), in.Type().IsTupleType(): + for iterator := in.ElementIterator(); iterator.Next(); { + index, value := iterator.Element() + child, err := fillType(value, target.ElementType(), path.Index(index)) + if err != nil { + return cty.NilVal, err + } + values = append(values, child) + } + default: + return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName()) + } + if len(values) == 0 { + return cty.ListValEmpty(target.ElementType()), nil + } + return cty.ListVal(values), nil + case target.IsSetType(): + var values []cty.Value + switch { + case in.Type().IsSetType(), in.Type().IsListType(), in.Type().IsTupleType(): + for iterator := in.ElementIterator(); iterator.Next(); { + index, value := iterator.Element() + child, err := fillType(value, target.ElementType(), path.Index(index)) + if err != nil { + return cty.NilVal, err + } + values = append(values, child) + } + default: + return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName()) + } + if len(values) == 0 { + return cty.SetValEmpty(target.ElementType()), nil + } + return cty.SetVal(values), nil + case target.IsMapType(): + values := make(map[string]cty.Value) + switch { + case in.Type().IsMapType(): + var keys []string + for key := range in.AsValueMap() { + keys = append(keys, key) + } + + // Make the order we iterate through the map deterministic. We + // are generating random strings in here so it's worth making + // the operation repeatable. + sort.Strings(keys) + + for _, key := range keys { + child, err := fillType(in.Index(cty.StringVal(key)), target.ElementType(), path.IndexString(key)) + if err != nil { + return cty.NilVal, err + } + values[key] = child + } + case in.Type().IsObjectType(): + var attributes []string + for attribute := range in.Type().AttributeTypes() { + attributes = append(attributes, attribute) + } + + // Make the order we iterate through the map deterministic. We + // are generating random strings in here so it's worth making + // the operation repeatable. + sort.Strings(attributes) + + for _, name := range attributes { + child, err := fillType(in.GetAttr(name), target.ElementType(), path.IndexString(name)) + if err != nil { + return cty.NilVal, err + } + values[name] = child + } + default: + return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName()) + } + if len(values) == 0 { + return cty.MapValEmpty(target.ElementType()), nil + } + return cty.MapVal(values), nil + default: + panic(fmt.Errorf("unrecognized collection type: %s", target.FriendlyName())) + } + } + + if target.IsTupleType() && in.Type().IsTupleType() { + if target.Length() != in.Type().Length() { + return cty.NilVal, path.NewErrorf("incompatible types; expected %s with length %d, found %s with length %d", target.FriendlyName(), target.Length(), in.Type().FriendlyName(), in.Type().Length()) + } + + var values []cty.Value + for ix, value := range in.AsValueSlice() { + child, err := fillType(value, target.TupleElementType(ix), path.IndexInt(ix)) + if err != nil { + return cty.NilVal, err + } + values = append(values, child) + } + if len(values) == 0 { + return cty.EmptyTupleVal, nil + } + return cty.TupleVal(values), nil + } + + // Otherwise, we don't have any nested object types we need to fill and this + // isn't an actual object either. So we can just do a simple conversion into + // the target type. + value, err := convert.Convert(in, target) + if err != nil { + return value, path.NewError(err) + } + return value, nil +} diff --git a/internal/moduletest/mocking/fill_test.go b/internal/moduletest/mocking/fill_test.go new file mode 100644 index 0000000000..9519585808 --- /dev/null +++ b/internal/moduletest/mocking/fill_test.go @@ -0,0 +1,262 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "math/rand" + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestFillType(t *testing.T) { + tcs := map[string]struct { + in cty.Value + out cty.Value + }{ + "object_to_object": { + in: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + }), + out: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + "value": cty.StringVal("ssnk9qhr"), + }), + }, + "map_to_object": { + in: cty.MapVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + }), + out: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + "value": cty.StringVal("ssnk9qhr"), + }), + }, + "list_to_list": { + in: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "tuple_to_list": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "set_to_list": { + in: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("amyllmyg"), + }), + }), + out: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("amyllmyg"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + "list_to_set": { + in: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "tuple_to_set": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "set_to_set": { + in: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("amyllmyg"), + }), + }), + out: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("amyllmyg"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + "tuple_to_tuple": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "map_to_map": { + in: cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{}), + "two": cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "object_to_map": { + in: cty.ObjectVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{}), + "two": cty.ObjectVal(map[string]cty.Value{}), + }), + out: cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + }), + }), + }, + "additional_attributes": { + in: cty.ObjectVal(map[string]cty.Value{ + "one": cty.StringVal("hello"), + "two": cty.StringVal("world"), + }), + out: cty.ObjectVal(map[string]cty.Value{ + "one": cty.StringVal("hello"), + "three": cty.StringVal("ssnk9qhr"), + }), + }, + // This is just a sort of safety check to validate it falls through to + // normal conversions for everything we don't handle. + "normal_conversion": { + in: cty.MapVal(map[string]cty.Value{ + "key_one": cty.StringVal("value_one"), + "key_two": cty.StringVal("value_two"), + }), + out: cty.ObjectVal(map[string]cty.Value{ + "key_one": cty.StringVal("value_one"), + "key_two": cty.StringVal("value_two"), + }), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + // Let's have predictable test outcomes. + testRand = rand.New(rand.NewSource(0)) + defer func() { + testRand = nil + }() + + actual, err := FillType(tc.in, tc.out.Type()) + if err != nil { + t.Fatal(err) + } + + expected := tc.out + if !expected.RawEquals(actual) { + t.Errorf("expected:%s\nactual: %s", expected.GoString(), actual.GoString()) + } + }) + } +} + +func TestFillType_Errors(t *testing.T) { + + tcs := map[string]struct { + in cty.Value + target cty.Type + err string + }{ + "error_diff_tuple_types": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + cty.StringVal("not an object"), + }), + target: cty.List(cty.EmptyObject), + err: "incompatible types; expected object, found string", + }, + "error_diff_object_types": { + in: cty.ObjectVal(map[string]cty.Value{ + "object": cty.ObjectVal(map[string]cty.Value{}), + "string": cty.StringVal("not an object"), + }), + target: cty.Map(cty.EmptyObject), + err: "incompatible types; expected object, found string", + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + actual, err := FillType(tc.in, tc.target) + if err == nil { + t.Fatal("should have errored") + } + + if out := err.Error(); out != tc.err { + t.Errorf("\nexpected: %s\nactual: %s", tc.err, out) + } + + if actual != cty.NilVal { + t.Fatal("should have errored") + } + }) + } + +} diff --git a/internal/moduletest/mocking/generate.go b/internal/moduletest/mocking/generate.go new file mode 100644 index 0000000000..3727508031 --- /dev/null +++ b/internal/moduletest/mocking/generate.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "fmt" + "math/rand" + "sort" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +var ( + // testRand and chars are used to generate random strings for the computed + // values. + // + // If testRand is null, then the global random is used. This allows us to + // seed tests for repeatable results. + testRand *rand.Rand + chars = []rune("abcdefghijklmnopqrstuvwxyz0123456789") +) + +// GenerateValueForAttribute accepts a configschema.Attribute and returns a +// valid value for that attribute. +func GenerateValueForAttribute(attribute *configschema.Attribute) cty.Value { + if attribute.NestedType != nil { + switch attribute.NestedType.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + var names []string + for name := range attribute.NestedType.Attributes { + names = append(names, name) + } + if len(names) == 0 { + return cty.EmptyObjectVal + } + + // Make the order we iterate through the attributes deterministic. We + // are generating random strings in here so it's worth making the + // operation repeatable. + sort.Strings(names) + + children := make(map[string]cty.Value) + for _, name := range names { + children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name]) + } + return cty.ObjectVal(children) + case configschema.NestingSet: + return cty.SetValEmpty(attribute.ImpliedType().ElementType()) + case configschema.NestingList: + return cty.ListValEmpty(attribute.ImpliedType().ElementType()) + case configschema.NestingMap: + return cty.MapValEmpty(attribute.ImpliedType().ElementType()) + default: + panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting)) + } + } + + return GenerateValueForType(attribute.Type) +} + +// GenerateValueForType accepts a cty.Type and returns a valid value for that +// type. +func GenerateValueForType(target cty.Type) cty.Value { + switch { + case target.IsPrimitiveType(): + switch target { + case cty.String: + return cty.StringVal(str(8)) + case cty.Number: + return cty.Zero + case cty.Bool: + return cty.False + default: + panic(fmt.Errorf("unknown primitive type: %s", target.FriendlyName())) + } + case target.IsListType(): + return cty.ListValEmpty(target.ElementType()) + case target.IsSetType(): + return cty.SetValEmpty(target.ElementType()) + case target.IsMapType(): + return cty.MapValEmpty(target.ElementType()) + case target.IsObjectType(): + var attributes []string + for attribute := range target.AttributeTypes() { + attributes = append(attributes, attribute) + } + if len(attributes) == 0 { + return cty.EmptyObjectVal + } + + // Make the order we iterate through the attributes deterministic. We + // are generating random strings in here so it's worth making the + // operation repeatable. + sort.Strings(attributes) + + children := make(map[string]cty.Value) + for _, attribute := range attributes { + children[attribute] = GenerateValueForType(target.AttributeType(attribute)) + } + return cty.ObjectVal(children) + case target == cty.DynamicPseudoType: + // For dynamic types, we cannot generate a value that is guaranteed to + // be valid. Instead, we return a null value. This means users will get + // an error saying that the value is null, but it's better than an error + // saying that the type is wrong which will be confusing. + return cty.NullVal(cty.DynamicPseudoType) + default: + panic(fmt.Errorf("unknown complex type: %s", target.FriendlyName())) + } +} + +func str(n int) string { + b := make([]rune, n) + for i := range b { + if testRand != nil { + b[i] = chars[testRand.Intn(len(chars))] + } else { + b[i] = chars[rand.Intn(len(chars))] + } + } + return string(b) +} diff --git a/internal/moduletest/mocking/overrides.go b/internal/moduletest/mocking/overrides.go new file mode 100644 index 0000000000..a3d4457ff9 --- /dev/null +++ b/internal/moduletest/mocking/overrides.go @@ -0,0 +1,225 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +// Overrides contains a summary of all the overrides that should apply for a +// test run. +// +// This requires us to deduplicate between run blocks and test files, and mock +// providers. +type Overrides struct { + providerOverrides map[string]addrs.Map[addrs.Targetable, *configs.Override] + localOverrides addrs.Map[addrs.Targetable, *configs.Override] +} + +func PackageOverrides(run *configs.TestRun, file *configs.TestFile, config *configs.Config) *Overrides { + overrides := &Overrides{ + providerOverrides: make(map[string]addrs.Map[addrs.Targetable, *configs.Override]), + localOverrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + + // The run block overrides have the highest priority, we always include all + // of them. + for _, elem := range run.Overrides.Elems { + overrides.localOverrides.PutElement(elem) + } + + // The file overrides are second, we include these as long as there isn't + // a direct replacement in the current run block or the run block doesn't + // override an entire module that a file override would be inside. + for _, elem := range file.Overrides.Elems { + target := elem.Key + + if overrides.localOverrides.Has(target) { + // The run block provided a value already. + continue + } + + overrides.localOverrides.PutElement(elem) + } + + // Finally, we want to include the overrides for any mock providers we have. + for key, provider := range config.Module.ProviderConfigs { + if !provider.Mock { + // Only mock providers can supply overrides. + continue + } + + for _, elem := range provider.MockData.Overrides.Elems { + target := elem.Key + + if overrides.localOverrides.Has(target) { + // Then the file or the run block is providing an override with + // higher precedence. + continue + } + + if _, exists := overrides.providerOverrides[key]; !exists { + overrides.providerOverrides[key] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + } + overrides.providerOverrides[key].PutElement(elem) + } + } + + return overrides +} + +// IsOverridden returns true if the module is either overridden directly or +// nested within another module that is already being overridden. +// +// For this function, we know that overrides defined within mock providers +// cannot target modules directly. Therefore, we only need to check the local +// overrides within this function. +func (overrides *Overrides) IsOverridden(module addrs.ModuleInstance) bool { + if module.Equal(addrs.RootModuleInstance) { + // The root module is never overridden, so let's just short circuit + // this. + return false + } + + if overrides.localOverrides.Has(module) { + // Short circuit things, if we have an exact match just return now. + return true + } + + // Otherwise, check for parents. + for _, elem := range overrides.localOverrides.Elems { + if elem.Key.TargetContains(module) { + // Then we have an ancestor of module being overridden instead of + // module being overridden directly. + return true + } + } + + return false +} + +// GetResourceOverride checks the overrides for the given resource instance. +// If the provided address is instanced, then we will check the containing +// resource as well. This is because users can mark a resource instance as +// overridden by overriding the instance directly (eg. resource.foo[0]) or by +// overriding the containing resource (eg. resource.foo). +// +// If the resource is being supplied by a mock provider, then we need to check +// the overrides for that provider as well, as such the provider config is +// required so we know which mock provider to check. +func (overrides *Overrides) GetResourceOverride(inst addrs.AbsResourceInstance, provider addrs.AbsProviderConfig) (*configs.Override, bool) { + if overrides.Empty() { + // Short circuit any lookups if we have no overrides. + return nil, false + } + + // First check this specific resource. + if override, ok := overrides.getResourceOverride(inst, provider); ok { + return override, true + } + + // Otherwise check the containing resource in case the user has set for all + // the instances of a resource to be overridden. + return overrides.getResourceOverride(inst.ContainingResource(), provider) +} + +func (overrides *Overrides) getResourceOverride(target addrs.Targetable, provider addrs.AbsProviderConfig) (*configs.Override, bool) { + // If we have a local override, then apply that first. + if override, ok := overrides.localOverrides.GetOk(target); ok { + return override, true + } + + // Otherwise, check if we have overrides for this provider. + providerOverrides, ok := overrides.ProviderMatch(provider) + if ok { + if override, ok := providerOverrides.GetOk(target); ok { + return override, true + } + } + + // If we have no overrides, that's okay. + return nil, false +} + +// GetModuleOverride checks the overrides for the given module instance. This +// function automatically checks if the containing module has been overridden +// if the instance is instanced. +// +// Users can mark a module instance as overridden by overriding the instance +// directly (eg. module.foo[0]) or by overriding the containing module +// (eg. module.foo). +// +// Modules cannot be overridden by mock providers directly, so we don't need +// to know anything about providers for this function (in contrast to +// GetResourceOverride). +func (overrides *Overrides) GetModuleOverride(inst addrs.ModuleInstance) (*configs.Override, bool) { + if len(inst) == 0 || overrides.Empty() { + // The root module is never overridden, so let's just short circuit + // this. + return nil, false + } + + // Otherwise check if this specific instance has been overridden. + if override, ok := overrides.localOverrides.GetOk(inst); ok { + // It has, so just return that. + return override, true + } + + // If this is an instanced address (eg. module.foo[0]), then we need to + // check if the containing module has been overridden as we let users + // override all instances of a module by overriding the containing module + // (eg. module.foo). + + // Check if the last step is actually instanced, so we don't do extra work + // needlessly. + if inst[len(inst)-1].InstanceKey == addrs.NoKey { + // Then we already checked the instance itself and it wasn't overridden. + return nil, false + } + + return overrides.localOverrides.GetOk(inst.ContainingModule()) +} + +// ProviderMatch returns true if we have overrides for the given provider. +// +// This is so that we can selectively apply overrides to resources that are +// being supplied by a given provider. +func (overrides *Overrides) ProviderMatch(provider addrs.AbsProviderConfig) (addrs.Map[addrs.Targetable, *configs.Override], bool) { + if !provider.Module.IsRoot() { + // We can only set mock providers within the root module. + return addrs.Map[addrs.Targetable, *configs.Override]{}, false + } + + name := provider.Provider.Type + if len(provider.Alias) > 0 { + name = fmt.Sprintf("%s.%s", name, provider.Alias) + } + + data, exists := overrides.providerOverrides[name] + return data, exists +} + +// Empty returns true if we have no actual overrides. +// +// This is just a convenience function to make checking for overrides easier. +func (overrides *Overrides) Empty() bool { + if overrides == nil { + return true + } + + if overrides.localOverrides.Len() > 0 { + return false + } + + for _, value := range overrides.providerOverrides { + if value.Len() > 0 { + return false + } + } + + return true +} diff --git a/internal/moduletest/mocking/overrides_test.go b/internal/moduletest/mocking/overrides_test.go new file mode 100644 index 0000000000..22136be97c --- /dev/null +++ b/internal/moduletest/mocking/overrides_test.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +func TestPackageOverrides(t *testing.T) { + mustResourceInstance := func(s string) addrs.AbsResourceInstance { + addr, diags := addrs.ParseAbsResourceInstanceStr(s) + if len(diags) > 0 { + t.Fatal(diags) + } + return addr + } + + primary := mustResourceInstance("test_instance.primary") + secondary := mustResourceInstance("test_instance.secondary") + tertiary := mustResourceInstance("test_instance.tertiary") + + testrun := mustResourceInstance("test_instance.test_run") + testfile := mustResourceInstance("test_instance.test_file") + provider := mustResourceInstance("test_instance.provider") + + // Add a single override to the test run. + run := &configs.TestRun{ + Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + run.Overrides.Put(primary, &configs.Override{ + Target: &addrs.Target{ + Subject: testrun, + }, + }) + + // Add a unique item to the test file, and duplicate the test run data. + file := &configs.TestFile{ + Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + file.Overrides.Put(primary, &configs.Override{ + Target: &addrs.Target{ + Subject: testfile, + }, + }) + file.Overrides.Put(secondary, &configs.Override{ + Target: &addrs.Target{ + Subject: testfile, + }, + }) + + // Add all data from the file and run block are duplicating here, and then + // a unique one. + config := &configs.Config{ + Module: &configs.Module{ + ProviderConfigs: map[string]*configs.Provider{ + "mock": { + Mock: true, + MockData: &configs.MockData{ + Overrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + }, + }, + "real": {}, + }, + }, + } + config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(primary, &configs.Override{ + Target: &addrs.Target{ + Subject: provider, + }, + }) + config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(secondary, &configs.Override{ + Target: &addrs.Target{ + Subject: provider, + }, + }) + config.Module.ProviderConfigs["mock"].MockData.Overrides.Put(tertiary, &configs.Override{ + Target: &addrs.Target{ + Subject: provider, + }, + }) + + overrides := PackageOverrides(run, file, config) + + // We now expect that the run and file overrides took precedence. + first, pOk := overrides.GetResourceOverride(primary, addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "mock", + }, + }) + second, sOk := overrides.GetResourceOverride(secondary, addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "mock", + }, + }) + third, tOk := overrides.GetResourceOverride(tertiary, addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "mock", + }, + }) + + if !pOk || !sOk || !tOk { + t.Fatalf("expected to find all overrides, but got %t %t %t", pOk, sOk, tOk) + } + + if !first.Target.Subject.(addrs.AbsResourceInstance).Equal(testrun) { + t.Errorf("expected %s but got %s for primary", testrun, first.Target.Subject) + } + + if !second.Target.Subject.(addrs.AbsResourceInstance).Equal(testfile) { + t.Errorf("expected %s but got %s for primary", testfile, second.Target.Subject) + } + + if !third.Target.Subject.(addrs.AbsResourceInstance).Equal(provider) { + t.Errorf("expected %s but got %s for primary", provider, third.Target.Subject) + } + + // Also, final sanity check. + _, ok := overrides.providerOverrides["real"] + if ok { + t.Errorf("shouldn't have stored the real provider but did") + } + +} diff --git a/internal/moduletest/mocking/testing.go b/internal/moduletest/mocking/testing.go new file mode 100644 index 0000000000..6ec498f315 --- /dev/null +++ b/internal/moduletest/mocking/testing.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +type InitProviderOverrides func(map[string]addrs.Map[addrs.Targetable, *configs.Override]) +type InitLocalOverrides func(addrs.Map[addrs.Targetable, *configs.Override]) + +func OverridesForTesting(providers InitProviderOverrides, locals InitLocalOverrides) *Overrides { + overrides := &Overrides{ + providerOverrides: make(map[string]addrs.Map[addrs.Targetable, *configs.Override]), + localOverrides: addrs.MakeMap[addrs.Targetable, *configs.Override](), + } + + if providers != nil { + providers(overrides.providerOverrides) + } + + if locals != nil { + locals(overrides.localOverrides) + } + + return overrides +} diff --git a/internal/moduletest/mocking/values.go b/internal/moduletest/mocking/values.go new file mode 100644 index 0000000000..0537a1b860 --- /dev/null +++ b/internal/moduletest/mocking/values.go @@ -0,0 +1,306 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// PlanComputedValuesForResource accepts a target value, and populates its computed +// values with values from the provider 'with' argument, and if 'with' is not provided, +// it sets the computed values to cty.UnknownVal. +// +// The latter behaviour simulates the behaviour of a plan request in a real +// provider. +func PlanComputedValuesForResource(original cty.Value, with *MockedData, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { + if with == nil { + with = &MockedData{ + Value: cty.NilVal, + ComputedAsUnknown: true, + } + } + return populateComputedValues(original, *with, schema, isNull) +} + +// ApplyComputedValuesForResource accepts a target value, and populates it +// either with values from the provided with argument, or with generated values +// created semi-randomly. This will only target values that are computed and +// unknown. +// +// This method basically simulates the behaviour of an apply request in a real +// provider. +func ApplyComputedValuesForResource(original cty.Value, with *MockedData, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { + if with == nil { + with = &MockedData{ + Value: cty.NilVal, + } + } + return populateComputedValues(original, *with, schema, isUnknown) +} + +// ComputedValuesForDataSource accepts a target value, and populates it either +// with values from the provided with argument, or with generated values created +// semi-randomly. This will only target values that are computed and null. +// +// This function does what PlanComputedValuesForResource and +// ApplyComputedValuesForResource do but in a single step with no intermediary +// unknown stage. +// +// This method basically simulates the behaviour of a get data source request +// in a real provider. +func ComputedValuesForDataSource(original cty.Value, with *MockedData, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { + if with == nil { + with = &MockedData{ + Value: cty.NilVal, + } + } + return populateComputedValues(original, *with, schema, isNull) +} + +type processValue func(value cty.Value) bool + +type generateValue func(attribute *configschema.Attribute, with cty.Value, path cty.Path) (cty.Value, tfdiags.Diagnostics) + +func populateComputedValues(target cty.Value, with MockedData, schema *configschema.Block, processValue processValue) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + var generateValue generateValue + // If the computed attributes should be ignored, then we will generate + // unknown values for them, otherwise we will + // generate their values based on the mocked data. + if with.ComputedAsUnknown { + generateValue = makeUnknown + } else { + generateValue = with.makeKnown + } + + if !with.validate() { + // This is actually a user error, it means the user wrote something like + // `values = "not an object"` when defining the replacement values for + // this in the mock or test file. We should have caught this earlier in + // the validation, but we want this function to be robust and not panic + // so we'll check again just in case. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid replacement value", + Detail: fmt.Sprintf("The requested replacement value must be an object type, but was %s.", with.Value.Type().FriendlyName()), + Subject: with.Range.Ptr(), + }) + + // We still need to produce valid data for this. So, let's pretend we + // had no mocked data. We still return the error diagnostic so whatever + // operation was happening will still fail, but we won't cause any + // panics or anything. + with = MockedData{ + Value: cty.NilVal, + } + } + + // We're going to search for any elements within the target value that meet + // the joint criteria of being computed and whatever processValue is + // checking. + // + // We'll then replace anything that meets the criteria with the output of + // generateValue. + // + // This transform should be robust (in that it should never fail), the + // inner call to generateValue should be robust as well so it should always + // return a valid value for us to use even if the embedded diagnostics + // return errors. + value, err := cty.Transform(target, func(path cty.Path, target cty.Value) (cty.Value, error) { + + // Get the attribute for the current target. + attribute := schema.AttributeByPath(path) + + if attribute == nil { + // Then this is an intermediate path which does not represent an + // attribute, and it cannot be computed. + return target, nil + } + + // Now, we check if we should be replacing this value with something. + if attribute.Computed && processValue(target) { + + // Get the value we should be replacing target with. + data, dataDiags := with.getMockedDataForPath(path) + diags = diags.Append(dataDiags) + + // Upstream code (in node_resource_abstract_instance.go) expects + // us to return a valid object (even if we have errors). That means + // no unknown values, no cty.NilVals, etc. So, we're going to go + // ahead and call generateValue with whatever getMockedDataForPath + // gave us. getMockedDataForPath is robust, so even in an error it + // should have given us something we can use in generateValue. + + // Now get the replacement value. This function should be robust in + // that it may return diagnostics explaining why it couldn't replace + // the value, but it'll still return a value for us to use. + value, valueDiags := generateValue(attribute, data, path) + diags = diags.Append(valueDiags) + + // We always return a valid value, the diags are attached to the + // global diags outside the nested function. + return value, nil + } + + // If we don't need to replace this value, then just return it + // untouched. + return target, nil + }) + if err != nil { + // This shouldn't actually happen - we never return an error from inside + // the transform function. But, just in case: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Detail: "Failed to generate values", + Summary: fmt.Sprintf("Terraform failed to generate computed values for a mocked resource, data source, or module: %s. This is a bug in Terraform - please report it.", err), + Subject: with.Range.Ptr(), + }) + } + + return value, diags +} + +func isNull(target cty.Value) bool { + return target.IsNull() +} + +func isUnknown(target cty.Value) bool { + return !target.IsKnown() +} + +// makeUnknown produces an unknown value for the provided attribute. This is +// basically the output of a plan() call for a computed attribute in a mocked +// resource. +func makeUnknown(target *configschema.Attribute, _ cty.Value, _ cty.Path) (cty.Value, tfdiags.Diagnostics) { + return cty.UnknownVal(target.ImpliedType()), nil +} + +// MockedData wraps the value and the source location of the value into a single +// struct for easy access. +type MockedData struct { + Value cty.Value + Range hcl.Range + ComputedAsUnknown bool // If true, computed values are replaced with unknown, otherwise they are replaced with overridden or generated values. +} + +// NewMockedData creates a new MockedData struct with the given value and range. +func NewMockedData(value cty.Value, computedAsUnknown bool, rng hcl.Range) MockedData { + return MockedData{ + Value: value, + ComputedAsUnknown: computedAsUnknown, + Range: rng, + } +} + +// makeKnown produces a valid value for the given attribute. The input value +// can provide data for this attribute or child attributes if this attribute +// represents an object. The input value is expected to be a representation of +// the schema of this attribute rather than a direct value. +func (data MockedData) makeKnown(attribute *configschema.Attribute, with cty.Value, path cty.Path) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if with != cty.NilVal { + // Then we have a pre-made value to use as the basis for our value. We + // just need to make sure the value is of the right type. + + if value, err := FillAttribute(with, attribute); err != nil { + var relPath cty.Path + if err, ok := err.(cty.PathError); ok { + relPath = err.Path + } + + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Failed to compute attribute", + fmt.Sprintf("Terraform could not compute a value for the target type %s with the mocked data defined at %s with the attribute %q: %s.", attribute.ImpliedType().FriendlyName(), data.Range, tfdiags.FormatCtyPath(append(path, relPath...)), err), + path)) + + // We still want to return a valid value here. If the conversion did + // not work we carry on and just create a value instead. We've made + // a note of the diagnostics tracking why it didn't work so the + // overall operation will still fail, but we won't crash later on + // because of an unknown value or something. + + // Fall through to the GenerateValueForAttribute call below. + } else { + // Successful conversion! We can just return the new value. + return value, diags + } + } + + // Otherwise, we'll have to generate some values. + return GenerateValueForAttribute(attribute), diags +} + +// We can only do replacements if the replacement value is an object type. +func (data MockedData) validate() bool { + return data.Value == cty.NilVal || data.Value.Type().IsObjectType() +} + +// getMockedDataForPath walks the path to find any potential mock data for the +// given path. We have implemented custom logic for walking the path here. +// +// This is to support nested block types. It's complicated to work out how to +// replace computed values within nested types. For example, how would a user +// say they just want to replace values at index 3? Or how would users indicate +// they want to replace anything at all within nested sets. The indices for sets +// will never be the same because the user supplied values will, by design, have +// values for the computed attributes which will be null or unknown within the +// values from Terraform so the paths will never match. +// +// What the above paragraph means is that for nested blocks and attributes, +// users can only specify a single replacement value that will apply to all +// the values within the nested collection. +func (data MockedData) getMockedDataForPath(path cty.Path) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if data.Value == cty.NilVal { + return cty.NilVal, diags + } + + // We want to provide a nice print out of the path in case of an error. + // We'll format it as we go. + var currentPath cty.Path + + // We are copying the implementation within AttributeByPath inside the + // schema for this. We skip over GetIndexSteps as they'll be referring to + // the intermediate nested blocks and attributes that we aren't capturing + // within the user supplied mock values. + current := data.Value + for _, step := range path { + switch step := step.(type) { + case cty.GetAttrStep: + + if !current.Type().IsObjectType() { + // As we're still traversing the path, we expect things to be + // objects at every level. + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Failed to compute attribute", + fmt.Sprintf("Terraform expected an object type for attribute %q defined within the mocked data at %s, but found %s.", tfdiags.FormatCtyPath(currentPath), data.Range, current.Type().FriendlyName()), + currentPath)) + + return cty.NilVal, diags + } + + if !current.Type().HasAttribute(step.Name) { + // Then we have no mocked data for this attribute. + return cty.NilVal, diags + } + + current = current.GetAttr(step.Name) + } + + currentPath = append(currentPath, step) + } + + return current, diags +} diff --git a/internal/moduletest/mocking/values_test.go b/internal/moduletest/mocking/values_test.go new file mode 100644 index 0000000000..62826e0bca --- /dev/null +++ b/internal/moduletest/mocking/values_test.go @@ -0,0 +1,1003 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package mocking + +import ( + "math/rand" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" +) + +var ( + normalAttributes = map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + }, + "value": { + Type: cty.String, + }, + } + + computedAttributes = map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + }, + } + + normalBlock = configschema.Block{ + Attributes: normalAttributes, + } + + computedBlock = configschema.Block{ + Attributes: computedAttributes, + } +) + +func TestComputedValuesForDataSource(t *testing.T) { + tcs := map[string]struct { + target cty.Value + with cty.Value + schema *configschema.Block + expected cty.Value + expectedFailures []string + }{ + "nil_target_no_unknowns": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.NilVal, + schema: &normalBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "empty_target_no_unknowns": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.EmptyObjectVal, + schema: &normalBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "basic_computed_attribute_preset": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.NilVal, + schema: &computedBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "basic_computed_attribute_random": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.NilVal, + schema: &computedBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "basic_computed_attribute_supplied": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + schema: &computedBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "nested_single_block_preset": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("Hello, world!"), + }), + }), + with: cty.NilVal, + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingSingle, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("Hello, world!"), + }), + }), + }, + "nested_single_block_supplied": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("Hello, world!"), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingSingle, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("Hello, world!"), + }), + }), + }, + "nested_list_block_preset": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.NilVal, + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingList, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_list_block_supplied": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingList, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_set_block_preset": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.NilVal, + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingSet, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_set_block_supplied": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingSet, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_map_block_preset": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.NilVal, + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingMap, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("one"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("amyllmyg"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_map_block_supplied": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingMap, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("one"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_single_attribute": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("Hello, world!"), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSingle, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("Hello, world!"), + }), + }), + }, + "nested_single_attribute_generated": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + with: cty.NilVal, + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSingle, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("amyllmyg"), + }), + }), + }, + "nested_single_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSingle, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + "nested_list_attribute": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingList, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_list_attribute_generated": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.NilVal, + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingList, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + }, + "nested_list_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingList, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + }, + "nested_set_attribute": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSet, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_set_attribute_generated": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.NilVal, + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSet, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + }, + "nested_set_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSet, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + }, + "nested_map_attribute": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("two"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingMap, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("one"), + }), + "two": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("two"), + }), + }), + }), + }, + "nested_map_attribute_generated": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.NilVal, + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingMap, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapValEmpty(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + }, + "nested_map_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingMap, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapValEmpty(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })), + }), + }, + "invalid_replacement_path": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.StringVal("Hello, world!"), + schema: &normalBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("kj87eb9"), + "value": cty.StringVal("Hello, world!"), + }), + expectedFailures: []string{ + "The requested replacement value must be an object type, but was string.", + }, + }, + "invalid_replacement_path_nested": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.StringVal("Hello, world!"), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested_object": { + NestedType: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + Nesting: configschema.NestingSet, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + }), + }), + expectedFailures: []string{ + "Terraform expected an object type for attribute \".nested_object[...]\" defined within the mocked data at :0,0-0, but found string.", + }, + }, + "invalid_replacement_path_nested_block": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.StringVal("Hello, world!"), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "nested_object": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + Nesting: configschema.NestingSet, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + }), + }), + expectedFailures: []string{ + "Terraform expected an object type for attribute \".nested_object[...]\" defined within the mocked data at :0,0-0, but found string.", + }, + }, + "invalid_replacement_type": { + target: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("Hello, world!"), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "id": cty.ListValEmpty(cty.String), + }), + schema: &computedBlock, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("Hello, world!"), + }), + expectedFailures: []string{ + "Terraform could not compute a value for the target type string with the mocked data defined at :0,0-0 with the attribute \".id\": string required.", + }, + }, + "invalid_replacement_type_nested": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "id": cty.EmptyObjectVal, + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingMap, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("one"), + }), + }), + }), + expectedFailures: []string{ + `Terraform could not compute a value for the target type string with the mocked data defined at :0,0-0 with the attribute ".nested[\"one\"].id": string required.`, + }, + }, + "invalid_replacement_type_nested_block": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ObjectVal(map[string]cty.Value{ + "id": cty.EmptyObjectVal, + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingList, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("one"), + }), + }), + }), + expectedFailures: []string{ + "Terraform could not compute a value for the target type string with the mocked data defined at :0,0-0 with the attribute \".block[0].id\": string required.", + }, + }, + "dynamic_attribute_unset": { + target: cty.ObjectVal(map[string]cty.Value{ + "dynamic_attribute": cty.NullVal(cty.DynamicPseudoType), + }), + with: cty.EmptyObjectVal, + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "dynamic_attribute": { + Type: cty.DynamicPseudoType, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "dynamic_attribute": cty.NullVal(cty.DynamicPseudoType), + }), + }, + "dynamic_attribute_set": { + target: cty.ObjectVal(map[string]cty.Value{ + "dynamic_attribute": cty.NullVal(cty.DynamicPseudoType), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "dynamic_attribute": cty.StringVal("Hello, world!"), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "dynamic_attribute": { + Type: cty.DynamicPseudoType, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "dynamic_attribute": cty.StringVal("Hello, world!"), + }), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + // We'll just make sure that any random strings are deterministic. + testRand = rand.New(rand.NewSource(0)) + defer func() { + testRand = nil + }() + + actual, diags := ComputedValuesForDataSource(tc.target, &MockedData{ + Value: tc.with, + }, tc.schema) + + var actualFailures []string + for _, diag := range diags { + actualFailures = append(actualFailures, diag.Description().Detail) + } + if diff := cmp.Diff(tc.expectedFailures, actualFailures); len(diff) > 0 { + t.Errorf("unexpected failures\nexpected:\n%s\nactual:\n%s\ndiff:\n%s", tc.expectedFailures, actualFailures, diff) + } + + if actual.Equals(tc.expected).False() { + t.Errorf("\nexpected: (%s)\nactual: (%s)", tc.expected.GoString(), actual.GoString()) + } + }) + } +} diff --git a/internal/moduletest/progress.go b/internal/moduletest/progress.go new file mode 100644 index 0000000000..aa15b7203b --- /dev/null +++ b/internal/moduletest/progress.go @@ -0,0 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduletest + +// Progress represents the status of a test file as it executes. +// +// We will include the progress markers to provide feedback as each test file +// executes. +// +//go:generate go tool golang.org/x/tools/cmd/stringer -type=Progress progress.go +type Progress int + +const ( + Starting Progress = iota + Running + TearDown + Complete +) diff --git a/internal/moduletest/progress_string.go b/internal/moduletest/progress_string.go new file mode 100644 index 0000000000..3fef101f70 --- /dev/null +++ b/internal/moduletest/progress_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=Progress progress.go"; DO NOT EDIT. + +package moduletest + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Starting-0] + _ = x[Running-1] + _ = x[TearDown-2] + _ = x[Complete-3] +} + +const _Progress_name = "StartingRunningTearDownComplete" + +var _Progress_index = [...]uint8{0, 8, 15, 23, 31} + +func (i Progress) String() string { + if i < 0 || i >= Progress(len(_Progress_index)-1) { + return "Progress(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Progress_name[_Progress_index[i]:_Progress_index[i+1]] +} diff --git a/internal/moduletest/provider.go b/internal/moduletest/provider.go deleted file mode 100644 index 52601d9d28..0000000000 --- a/internal/moduletest/provider.go +++ /dev/null @@ -1,575 +0,0 @@ -package moduletest - -import ( - "fmt" - "log" - "sync" - - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/gocty" - ctyjson "github.com/zclconf/go-cty/cty/json" - - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/repl" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// Provider is an implementation of providers.Interface which we're -// using as a likely-only-temporary vehicle for research on an opinionated -// module testing workflow in Terraform. -// -// We expose this to configuration as "terraform.io/builtin/test", but -// any attempt to configure it will emit a warning that it is experimental -// and likely to change or be removed entirely in future Terraform CLI -// releases. -// -// The testing provider exists to gather up test results during a Terraform -// apply operation. Its "test_results" managed resource type doesn't have any -// user-visible effect on its own, but when used in conjunction with the -// "terraform test" experimental command it is the intermediary that holds -// the test results while the test runs, so that the test command can then -// report them. -// -// For correct behavior of the assertion tracking, the "terraform test" -// command must be sure to use the same instance of Provider for both the -// plan and apply steps, so that the assertions that were planned can still -// be tracked during apply. For other commands that don't explicitly support -// test assertions, the provider will still succeed but the assertions data -// may not be complete if the apply step fails. -type Provider struct { - // components tracks all of the "component" names that have been - // used in test assertions resources so far. Each resource must have - // a unique component name. - components map[string]*Component - - // Must lock mutex in order to interact with the components map, because - // test assertions can potentially run concurrently. - mutex sync.RWMutex -} - -var _ providers.Interface = (*Provider)(nil) - -// NewProvider returns a new instance of the test provider. -func NewProvider() *Provider { - return &Provider{ - components: make(map[string]*Component), - } -} - -// TestResults returns the current record of test results tracked inside the -// provider. -// -// The result is a direct reference to the internal state of the provider, -// so the caller mustn't modify it nor store it across calls to provider -// operations. -func (p *Provider) TestResults() map[string]*Component { - return p.components -} - -// Reset returns the recieving provider back to its original state, with no -// recorded test results. -// -// It additionally detaches the instance from any data structure previously -// returned by method TestResults, freeing the caller from the constraints -// in its documentation about mutability and storage. -// -// For convenience in the presumed common case of resetting as part of -// capturing the results for storage, this method also returns the result -// that method TestResults would've returned if called prior to the call -// to Reset. -func (p *Provider) Reset() map[string]*Component { - p.mutex.Lock() - log.Print("[TRACE] moduletest.Provider: Reset") - ret := p.components - p.components = make(map[string]*Component) - p.mutex.Unlock() - return ret -} - -// GetProviderSchema returns the complete schema for the provider. -func (p *Provider) GetProviderSchema() providers.GetProviderSchemaResponse { - return providers.GetProviderSchemaResponse{ - ResourceTypes: map[string]providers.Schema{ - "test_assertions": testAssertionsSchema, - }, - } -} - -// ValidateProviderConfig validates the provider configuration. -func (p *Provider) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { - // This provider has no configurable settings, so nothing to validate. - var res providers.ValidateProviderConfigResponse - return res -} - -// ConfigureProvider configures and initializes the provider. -func (p *Provider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { - // This provider has no configurable settings, but we use the configure - // request as an opportunity to generate a warning about it being - // experimental. - var res providers.ConfigureProviderResponse - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Warning, - "The test provider is experimental", - "The Terraform team is using the test provider (terraform.io/builtin/test) as part of ongoing research about declarative testing of Terraform modules.\n\nThe availability and behavior of this provider is expected to change significantly even in patch releases, so we recommend using this provider only in test configurations and constraining your test configurations to an exact Terraform version.", - nil, - )) - return res -} - -// ValidateResourceConfig is used to validate configuration values for a resource. -func (p *Provider) ValidateResourceConfig(req providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { - log.Print("[TRACE] moduletest.Provider: ValidateResourceConfig") - - var res providers.ValidateResourceConfigResponse - if req.TypeName != "test_assertions" { // we only have one resource type - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName)) - return res - } - - config := req.Config - if !config.GetAttr("component").IsKnown() { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid component expression", - "The component name must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.", - cty.GetAttrPath("component"), - )) - } - if !hclsyntax.ValidIdentifier(config.GetAttr("component").AsString()) { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid component name", - "The component name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.", - cty.GetAttrPath("component"), - )) - } - for it := config.GetAttr("equal").ElementIterator(); it.Next(); { - k, obj := it.Element() - if !hclsyntax.ValidIdentifier(k.AsString()) { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid assertion name", - "An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.", - cty.GetAttrPath("equal").Index(k), - )) - } - if !obj.GetAttr("description").IsKnown() { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid description expression", - "The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.", - cty.GetAttrPath("equal").Index(k).GetAttr("description"), - )) - } - } - for it := config.GetAttr("check").ElementIterator(); it.Next(); { - k, obj := it.Element() - if !hclsyntax.ValidIdentifier(k.AsString()) { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid assertion name", - "An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.", - cty.GetAttrPath("check").Index(k), - )) - } - if !obj.GetAttr("description").IsKnown() { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid description expression", - "The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.", - cty.GetAttrPath("equal").Index(k).GetAttr("description"), - )) - } - } - - return res -} - -// ReadResource refreshes a resource and returns its current state. -func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse { - log.Print("[TRACE] moduletest.Provider: ReadResource") - - var res providers.ReadResourceResponse - if req.TypeName != "test_assertions" { // we only have one resource type - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName)) - return res - } - // Test assertions are not a real remote object, so there isn't actually - // anything to refresh here. - res.NewState = req.PriorState - return res -} - -// UpgradeResourceState is called to allow the provider to adapt the raw value -// stored in the state in case the schema has changed since it was originally -// written. -func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { - log.Print("[TRACE] moduletest.Provider: UpgradeResourceState") - - var res providers.UpgradeResourceStateResponse - if req.TypeName != "test_assertions" { // we only have one resource type - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName)) - return res - } - - // We assume here that there can never be a flatmap version of this - // resource type's data, because this provider was never included in a - // version of Terraform that used flatmap and this provider's schema - // contains attributes that are not flatmap-compatible anyway. - if len(req.RawStateFlatmap) != 0 { - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("can't upgrade a flatmap state for %q", req.TypeName)) - return res - } - if req.Version != 0 { - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("the state for this %s was created by a newer version of the provider", req.TypeName)) - return res - } - - v, err := ctyjson.Unmarshal(req.RawStateJSON, testAssertionsSchema.Block.ImpliedType()) - if err != nil { - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("failed to decode state for %s: %s", req.TypeName, err)) - return res - } - - res.UpgradedState = v - return res -} - -// PlanResourceChange takes the current state and proposed state of a -// resource, and returns the planned final state. -func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { - log.Print("[TRACE] moduletest.Provider: PlanResourceChange") - - // this is a destroy plan, - if req.ProposedNewState.IsNull() { - resp.PlannedState = req.ProposedNewState - resp.PlannedPrivate = req.PriorPrivate - return resp - } - - var res providers.PlanResourceChangeResponse - if req.TypeName != "test_assertions" { // we only have one resource type - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName)) - return res - } - - // During planning, our job is to gather up all of the planned test - // assertions marked as pending, which will then allow us to include - // all of them in test results even if there's a failure during apply - // that prevents the full completion of the graph walk. - // - // In a sense our plan phase is similar to the compile step for a - // test program written in another language. Planning itself can fail, - // which means we won't be able to form a complete test plan at all, - // but if we succeed in planning then subsequent problems can be treated - // as test failures at "runtime", while still keeping a full manifest - // of all of the tests that ought to have run if the apply had run to - // completion. - - proposed := req.ProposedNewState - res.PlannedState = proposed - componentName := proposed.GetAttr("component").AsString() // proven known during validate - p.mutex.Lock() - defer p.mutex.Unlock() - // NOTE: Ideally we'd do something here to verify if two assertions - // resources in the configuration attempt to declare the same component, - // but we can't actually do that because Terraform calls PlanResourceChange - // during both plan and apply, and so the second one would always fail. - // Since this is just providing a temporary pseudo-syntax for writing tests - // anyway, we'll live with this for now and aim to solve it with a future - // iteration of testing that's better integrated into the Terraform - // language. - /* - if _, exists := p.components[componentName]; exists { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Duplicate test component", - fmt.Sprintf("Another test_assertions resource already declared assertions for the component name %q.", componentName), - cty.GetAttrPath("component"), - )) - return res - } - */ - - component := Component{ - Assertions: make(map[string]*Assertion), - } - - for it := proposed.GetAttr("equal").ElementIterator(); it.Next(); { - k, obj := it.Element() - name := k.AsString() - if _, exists := component.Assertions[name]; exists { - // We can't actually get here in practice because so far we've - // only been pulling keys from one map, and so any duplicates - // would've been caught during config decoding, but this is here - // just to make these two blocks symmetrical to avoid mishaps in - // future refactoring/reorganization. - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Duplicate test assertion", - fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name), - cty.GetAttrPath("equal").Index(k), - )) - continue - } - - var desc string - descVal := obj.GetAttr("description") - if descVal.IsNull() { - descVal = cty.StringVal("") - } - err := gocty.FromCtyValue(descVal, &desc) - if err != nil { - // We shouldn't get here because we've already validated everything - // that would make FromCtyValue fail above and during validate. - res.Diagnostics = res.Diagnostics.Append(err) - } - - component.Assertions[name] = &Assertion{ - Outcome: Pending, - Description: desc, - } - } - - for it := proposed.GetAttr("check").ElementIterator(); it.Next(); { - k, obj := it.Element() - name := k.AsString() - if _, exists := component.Assertions[name]; exists { - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Duplicate test assertion", - fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name), - cty.GetAttrPath("check").Index(k), - )) - continue - } - - var desc string - descVal := obj.GetAttr("description") - if descVal.IsNull() { - descVal = cty.StringVal("") - } - err := gocty.FromCtyValue(descVal, &desc) - if err != nil { - // We shouldn't get here because we've already validated everything - // that would make FromCtyValue fail above and during validate. - res.Diagnostics = res.Diagnostics.Append(err) - } - - component.Assertions[name] = &Assertion{ - Outcome: Pending, - Description: desc, - } - } - - p.components[componentName] = &component - return res -} - -// ApplyResourceChange takes the planned state for a resource, which may -// yet contain unknown computed values, and applies the changes returning -// the final state. -func (p *Provider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { - log.Print("[TRACE] moduletest.Provider: ApplyResourceChange") - - var res providers.ApplyResourceChangeResponse - if req.TypeName != "test_assertions" { // we only have one resource type - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName)) - return res - } - - // During apply we actually check the assertions and record the results. - // An assertion failure isn't reflected as an error from the apply call - // because if possible we'd like to continue exercising other objects - // downstream in case that allows us to gather more information to report. - // (If something downstream returns an error then that could prevent us - // from completing other assertions, though.) - - planned := req.PlannedState - res.NewState = planned - if res.NewState.IsNull() { - // If we're destroying then we'll just quickly return success to - // allow the test process to clean up after itself. - return res - } - componentName := planned.GetAttr("component").AsString() // proven known during validate - - p.mutex.Lock() - defer p.mutex.Unlock() - component := p.components[componentName] - if component == nil { - // We might get here when using this provider outside of the - // "terraform test" command, where there won't be any mechanism to - // preserve the test provider instance between the plan and apply - // phases. In that case, we assume that nobody will come looking to - // collect the results anyway, and so we can just silently skip - // checking. - return res - } - - for it := planned.GetAttr("equal").ElementIterator(); it.Next(); { - k, obj := it.Element() - name := k.AsString() - var desc string - if plan, exists := component.Assertions[name]; exists { - desc = plan.Description - } - assert := &Assertion{ - Outcome: Pending, - Description: desc, - } - - gotVal := obj.GetAttr("got") - wantVal := obj.GetAttr("want") - switch { - case wantVal.RawEquals(gotVal): - assert.Outcome = Passed - gotStr := repl.FormatValue(gotVal, 4) - assert.Message = fmt.Sprintf("correct value\n got: %s\n", gotStr) - default: - assert.Outcome = Failed - gotStr := repl.FormatValue(gotVal, 4) - wantStr := repl.FormatValue(wantVal, 4) - assert.Message = fmt.Sprintf("wrong value\n got: %s\n want: %s\n", gotStr, wantStr) - } - - component.Assertions[name] = assert - } - - for it := planned.GetAttr("check").ElementIterator(); it.Next(); { - k, obj := it.Element() - name := k.AsString() - var desc string - if plan, exists := component.Assertions[name]; exists { - desc = plan.Description - } - assert := &Assertion{ - Outcome: Pending, - Description: desc, - } - - condVal := obj.GetAttr("condition") - switch { - case condVal.IsNull(): - res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue( - tfdiags.Error, - "Invalid check condition", - "The condition value must be a boolean expression, not null.", - cty.GetAttrPath("check").Index(k).GetAttr("condition"), - )) - continue - case condVal.True(): - assert.Outcome = Passed - assert.Message = "condition passed" - default: - assert.Outcome = Failed - // For "check" we can't really return a decent error message - // because we've lost all of the context by the time we get here. - // "equal" will be better for most tests for that reason, and also - // this is one reason why in the long run it would be better for - // test assertions to be a first-class language feature rather than - // just a provider-based concept. - assert.Message = "condition failed" - } - - component.Assertions[name] = assert - } - - return res -} - -// ImportResourceState requests that the given resource be imported. -func (p *Provider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { - var res providers.ImportResourceStateResponse - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("%s is not importable", req.TypeName)) - return res -} - -// ValidateDataResourceConfig is used to to validate the resource configuration values. -func (p *Provider) ValidateDataResourceConfig(req providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { - // This provider has no data resouce types at all. - var res providers.ValidateDataResourceConfigResponse - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName)) - return res -} - -// ReadDataSource returns the data source's current state. -func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { - // This provider has no data resouce types at all. - var res providers.ReadDataSourceResponse - res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName)) - return res -} - -// Stop is called when the provider should halt any in-flight actions. -func (p *Provider) Stop() error { - // This provider doesn't do anything that can be cancelled. - return nil -} - -// Close is a noop for this provider, since it's run in-process. -func (p *Provider) Close() error { - return nil -} - -var testAssertionsSchema = providers.Schema{ - Block: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "component": { - Type: cty.String, - Description: "The name of the component being tested. This is just for namespacing assertions in a result report.", - DescriptionKind: configschema.StringPlain, - Required: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "equal": { - Nesting: configschema.NestingMap, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "description": { - Type: cty.String, - Description: "An optional human-readable description of what's being tested by this assertion.", - DescriptionKind: configschema.StringPlain, - Required: true, - }, - "got": { - Type: cty.DynamicPseudoType, - Description: "The actual result value generated by the relevant component.", - DescriptionKind: configschema.StringPlain, - Required: true, - }, - "want": { - Type: cty.DynamicPseudoType, - Description: "The value that the component is expected to have generated.", - DescriptionKind: configschema.StringPlain, - Required: true, - }, - }, - }, - }, - "check": { - Nesting: configschema.NestingMap, - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "description": { - Type: cty.String, - Description: "An optional (but strongly recommended) human-readable description of what's being tested by this assertion.", - DescriptionKind: configschema.StringPlain, - Required: true, - }, - "condition": { - Type: cty.Bool, - Description: "An expression that must be true in order for the test to pass.", - DescriptionKind: configschema.StringPlain, - Required: true, - }, - }, - }, - }, - }, - }, -} diff --git a/internal/moduletest/provider_test.go b/internal/moduletest/provider_test.go deleted file mode 100644 index 30ca0359a6..0000000000 --- a/internal/moduletest/provider_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package moduletest - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform/internal/providers" - "github.com/zclconf/go-cty-debug/ctydebug" - "github.com/zclconf/go-cty/cty" -) - -func TestProvider(t *testing.T) { - - assertionConfig := cty.ObjectVal(map[string]cty.Value{ - "component": cty.StringVal("spline_reticulator"), - "equal": cty.MapVal(map[string]cty.Value{ - "match": cty.ObjectVal(map[string]cty.Value{ - "description": cty.StringVal("this should match"), - "got": cty.StringVal("a"), - "want": cty.StringVal("a"), - }), - "unmatch": cty.ObjectVal(map[string]cty.Value{ - "description": cty.StringVal("this should not match"), - "got": cty.StringVal("a"), - "want": cty.StringVal("b"), - }), - }), - "check": cty.MapVal(map[string]cty.Value{ - "pass": cty.ObjectVal(map[string]cty.Value{ - "description": cty.StringVal("this should pass"), - "condition": cty.True, - }), - "fail": cty.ObjectVal(map[string]cty.Value{ - "description": cty.StringVal("this should fail"), - "condition": cty.False, - }), - }), - }) - - // The provider code expects to receive an object that was decoded from - // HCL using the schema, so to make sure we're testing a more realistic - // situation here we'll require the config to conform to the schema. If - // this fails, it's a bug in the configuration definition above rather - // than in the provider itself. - for _, err := range assertionConfig.Type().TestConformance(testAssertionsSchema.Block.ImpliedType()) { - t.Error(err) - } - - p := NewProvider() - - configureResp := p.ConfigureProvider(providers.ConfigureProviderRequest{ - Config: cty.EmptyObjectVal, - }) - if got, want := len(configureResp.Diagnostics), 1; got != want { - t.Fatalf("got %d Configure diagnostics, but want %d", got, want) - } - if got, want := configureResp.Diagnostics[0].Description().Summary, "The test provider is experimental"; got != want { - t.Fatalf("wrong diagnostic message\ngot: %s\nwant: %s", got, want) - } - - validateResp := p.ValidateResourceConfig(providers.ValidateResourceConfigRequest{ - TypeName: "test_assertions", - Config: assertionConfig, - }) - if got, want := len(validateResp.Diagnostics), 0; got != want { - t.Fatalf("got %d ValidateResourceTypeConfig diagnostics, but want %d", got, want) - } - - planResp := p.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: "test_assertions", - Config: assertionConfig, - PriorState: cty.NullVal(assertionConfig.Type()), - ProposedNewState: assertionConfig, - }) - if got, want := len(planResp.Diagnostics), 0; got != want { - t.Fatalf("got %d PlanResourceChange diagnostics, but want %d", got, want) - } - planned := planResp.PlannedState - if got, want := planned, assertionConfig; !want.RawEquals(got) { - t.Fatalf("wrong planned new value\n%s", ctydebug.DiffValues(want, got)) - } - - gotComponents := p.TestResults() - wantComponents := map[string]*Component{ - "spline_reticulator": { - Assertions: map[string]*Assertion{ - "pass": { - Outcome: Pending, - Description: "this should pass", - }, - "fail": { - Outcome: Pending, - Description: "this should fail", - }, - "match": { - Outcome: Pending, - Description: "this should match", - }, - "unmatch": { - Outcome: Pending, - Description: "this should not match", - }, - }, - }, - } - if diff := cmp.Diff(wantComponents, gotComponents); diff != "" { - t.Fatalf("wrong test results after planning\n%s", diff) - } - - applyResp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{ - TypeName: "test_assertions", - Config: assertionConfig, - PriorState: cty.NullVal(assertionConfig.Type()), - PlannedState: planned, - }) - if got, want := len(applyResp.Diagnostics), 0; got != want { - t.Fatalf("got %d ApplyResourceChange diagnostics, but want %d", got, want) - } - final := applyResp.NewState - if got, want := final, assertionConfig; !want.RawEquals(got) { - t.Fatalf("wrong new value\n%s", ctydebug.DiffValues(want, got)) - } - - gotComponents = p.TestResults() - wantComponents = map[string]*Component{ - "spline_reticulator": { - Assertions: map[string]*Assertion{ - "pass": { - Outcome: Passed, - Description: "this should pass", - Message: "condition passed", - }, - "fail": { - Outcome: Failed, - Description: "this should fail", - Message: "condition failed", - }, - "match": { - Outcome: Passed, - Description: "this should match", - Message: "correct value\n got: \"a\"\n", - }, - "unmatch": { - Outcome: Failed, - Description: "this should not match", - Message: "wrong value\n got: \"a\"\n want: \"b\"\n", - }, - }, - }, - } - if diff := cmp.Diff(wantComponents, gotComponents); diff != "" { - t.Fatalf("wrong test results after applying\n%s", diff) - } - -} diff --git a/internal/moduletest/run.go b/internal/moduletest/run.go new file mode 100644 index 0000000000..8c841ea9f6 --- /dev/null +++ b/internal/moduletest/run.go @@ -0,0 +1,579 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduletest + +import ( + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +const ( + MainStateIdentifier = "" +) + +type Run struct { + Config *configs.TestRun + + // ModuleConfig is a copy of the module configuration that the run is testing. + // The variables and provider configurations are copied so that the run can + // modify them safely without affecting the original configuration. + // However, any other fields in the module configuration are still shared between + // all runs that use the same module configuration. + ModuleConfig *configs.Config + + Verbose *Verbose + + Name string + Index int + Status Status + + Diagnostics tfdiags.Diagnostics + + // ExecutionMeta captures metadata about how the test run was executed. + // + // This field is not always populated. A run that has never been executed + // will definitely have a nil value for this field. A run that was + // executed may or may not populate this field, depending on exactly what + // happened during the run execution. Callers accessing this field MUST + // check for nil and handle that case in some reasonable way. + // + // Executing the same run multiple times may or may not update this field + // on each execution. + ExecutionMeta *RunExecutionMeta +} + +func NewRun(config *configs.TestRun, moduleConfig *configs.Config, index int) *Run { + // Make a copy the module configuration variables and provider configuration maps + // so that the run can modify the map safely. + newModuleConfig := *moduleConfig + if moduleConfig.Module != nil { + newModule := *moduleConfig.Module + newModule.Variables = make(map[string]*configs.Variable, len(moduleConfig.Module.Variables)) + for name, variable := range moduleConfig.Module.Variables { + newModule.Variables[name] = variable + } + newModule.ProviderConfigs = make(map[string]*configs.Provider, len(moduleConfig.Module.ProviderConfigs)) + for name, provider := range moduleConfig.Module.ProviderConfigs { + newModule.ProviderConfigs[name] = provider + } + newModuleConfig.Module = &newModule + } + + return &Run{ + Config: config, + ModuleConfig: &newModuleConfig, + Name: config.Name, + Index: index, + } +} + +type RunExecutionMeta struct { + Start time.Time + Duration time.Duration +} + +// StartTimestamp returns the start time metadata as a timestamp formatted as YYYY-MM-DDTHH:MM:SSZ. +// Times are converted to UTC, if they aren't already. +// If the start time is unset an empty string is returned. +func (m *RunExecutionMeta) StartTimestamp() string { + if m.Start.IsZero() { + return "" + } + return m.Start.UTC().Format(time.RFC3339) +} + +// Verbose is a utility struct that holds all the information required for a run +// to render the results verbosely. +// +// At the moment, this basically means printing out the plan. To do that we need +// all the information within this struct. +type Verbose struct { + Plan *plans.Plan + State *states.State + Config *configs.Config + Providers map[addrs.Provider]providers.ProviderSchema + Provisioners map[string]*configschema.Block +} + +func (run *Run) Addr() addrs.Run { + return addrs.Run{Name: run.Name} +} + +func (run *Run) GetTargets() ([]addrs.Targetable, tfdiags.Diagnostics) { + var diagnostics tfdiags.Diagnostics + var targets []addrs.Targetable + + for _, target := range run.Config.Options.Target { + addr, diags := addrs.ParseTarget(target) + diagnostics = diagnostics.Append(diags) + if addr != nil { + targets = append(targets, addr.Subject) + } + } + + return targets, diagnostics +} + +func (run *Run) GetReplaces() ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) { + var diagnostics tfdiags.Diagnostics + var replaces []addrs.AbsResourceInstance + + for _, replace := range run.Config.Options.Replace { + addr, diags := addrs.ParseAbsResourceInstance(replace) + diagnostics = diagnostics.Append(diags) + if diags.HasErrors() { + continue + } + + if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { + diagnostics = diagnostics.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can only target managed resources for forced replacements.", + Detail: addr.String(), + Subject: replace.SourceRange().Ptr(), + }) + continue + } + + replaces = append(replaces, addr) + } + + return replaces, diagnostics +} + +func (run *Run) GetReferences() ([]*addrs.Reference, tfdiags.Diagnostics) { + var diagnostics tfdiags.Diagnostics + var references []*addrs.Reference + + for _, rule := range run.Config.CheckRules { + for _, variable := range rule.Condition.Variables() { + reference, diags := addrs.ParseRefFromTestingScope(variable) + diagnostics = diagnostics.Append(diags) + if reference != nil { + references = append(references, reference) + } + } + for _, variable := range rule.ErrorMessage.Variables() { + reference, diags := addrs.ParseRefFromTestingScope(variable) + diagnostics = diagnostics.Append(diags) + if reference != nil { + references = append(references, reference) + } + } + } + + for _, expr := range run.Config.Variables { + moreRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRefFromTestingScope, expr) + diagnostics = diagnostics.Append(moreDiags) + references = append(references, moreRefs...) + } + + return references, diagnostics +} + +// GetStateKey returns the run's state key. If an explicit state key is set in +// the run's configuration, that key is returned. Otherwise, if the run is using +// an alternate module under test, the source of that module is returned as the +// state key. If neither of these conditions are met, an empty string is +// returned, and this denotes that the run is using the root module under test. +func (run *Run) GetStateKey() string { + if run.Config.StateKey != "" { + return run.Config.StateKey + } + + // The run has an alternate module under test, so we can use the module's source + if run.Config.ConfigUnderTest != nil { + return run.Config.Module.Source.String() + } + + return MainStateIdentifier +} + +// GetModuleConfigID returns the identifier for the module configuration that +// this run is testing. This is used to uniquely identify the module +// configuration in the test state. +func (run *Run) GetModuleConfigID() string { + return run.ModuleConfig.Module.SourceDir +} + +// ExplainExpectedFailures is similar to ValidateExpectedFailures except it +// looks for any diagnostics produced by custom conditions and are included in +// the expected failures and adds an additional explanation that clarifies the +// expected failures are being ignored this time round. +// +// Generally, this function is used during an `apply` operation to explain that +// an expected failure during the planning stage will still result in the +// overall test failing as the plan failed and we couldn't even execute the +// apply stage. +func (run *Run) ExplainExpectedFailures(originals tfdiags.Diagnostics) tfdiags.Diagnostics { + + // We're going to capture all the checkable objects that are referenced + // from the expected failures. + expectedFailures := addrs.MakeMap[addrs.Referenceable, bool]() + sourceRanges := addrs.MakeMap[addrs.Referenceable, tfdiags.SourceRange]() + + for _, traversal := range run.Config.ExpectFailures { + // Ignore the diagnostics returned from the reference parsing, these + // references will have been checked earlier in the process by the + // validate stage so we don't need to do that again here. + reference, _ := addrs.ParseRefFromTestingScope(traversal) + expectedFailures.Put(reference.Subject, false) + sourceRanges.Put(reference.Subject, reference.SourceRange) + } + + var diags tfdiags.Diagnostics + for _, diag := range originals { + if diag.Severity() == tfdiags.Warning { + // Then it's fine, the test will carry on without us doing anything. + diags = diags.Append(diag) + continue + } + + if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok { + + var rng *hcl.Range + expected := false + switch rule.Container.CheckableKind() { + case addrs.CheckableOutputValue: + addr := rule.Container.(addrs.AbsOutputValue) + if !addr.Module.IsRoot() { + // failures can only be expected against checkable objects + // in the root module. This diagnostic will be added into + // returned set below. + break + } + if expectedFailures.Has(addr.OutputValue) { + expected = true + rng = sourceRanges.Get(addr.OutputValue).ToHCL().Ptr() + } + + case addrs.CheckableInputVariable: + addr := rule.Container.(addrs.AbsInputVariableInstance) + if !addr.Module.IsRoot() { + // failures can only be expected against checkable objects + // in the root module. This diagnostic will be added into + // returned set below. + break + } + if expectedFailures.Has(addr.Variable) { + expected = true + } + + case addrs.CheckableResource: + addr := rule.Container.(addrs.AbsResourceInstance) + if !addr.Module.IsRoot() { + // failures can only be expected against checkable objects + // in the root module. This diagnostic will be added into + // returned set below. + break + } + if expectedFailures.Has(addr.Resource) { + expected = true + } + + if expectedFailures.Has(addr.Resource.Resource) { + expected = true + } + + case addrs.CheckableCheck: + // Check blocks only produce warnings so this branch shouldn't + // ever be triggered anyway. + default: + panic("unrecognized CheckableKind: " + rule.Container.CheckableKind().String()) + } + + if expected { + // Then this diagnostic was produced by a custom condition that + // was expected to fail. But, it happened at the wrong time (eg. + // we're trying to run an apply operation and this condition + // failed during the plan so the overall test operation still + // fails). + // + // We'll add a warning diagnostic explaining why the overall + // test is still failing even though the error was expected, and + // then add the original error into our diagnostics directly + // after. + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Expected failure while planning", + Detail: fmt.Sprintf("A custom condition within %s failed during the planning stage and prevented the requested apply operation. While this was an expected failure, the apply operation could not be executed and so the overall test case will be marked as a failure and the original diagnostic included in the test report.", rule.Container.String()), + Subject: rng, + }) + diags = diags.Append(diag) + continue + } + } + + // Otherwise, there is nothing special about this diagnostic so just + // carry it through. + diags = diags.Append(diag) + } + return diags +} + +// ValidateExpectedFailures steps through the provided diagnostics (which should +// be the result of a plan or an apply operation), and does 3 things: +// 1. Removes diagnostics that match the expected failures from the config. +// 2. Upgrades warnings from check blocks into errors where appropriate so the +// test will fail later. +// 3. Adds diagnostics for any expected failures that were not satisfied. +// +// Point 2 is a bit complicated so worth expanding on. In normal Terraform +// execution, any error that originates within a check block (either from an +// assertion or a scoped data source) is wrapped up as a Warning to be +// identified to the user but not to fail the actual Terraform operation. During +// test execution, we want to upgrade (or rollback) these warnings into errors +// again so the test will fail. We do that as part of this function as we are +// already processing the diagnostics from check blocks in here anyway. +// +// The way the function works out which diagnostics are relevant to expected +// failures is by using the tfdiags Extra functionality to detect which +// diagnostics were generated by custom conditions. Terraform adds the +// addrs.CheckRule that generated each diagnostic to the diagnostic itself so we +// can tell which diagnostics can be expected. +func (run *Run) ValidateExpectedFailures(originals tfdiags.Diagnostics) tfdiags.Diagnostics { + + // We're going to capture all the checkable objects that are referenced + // from the expected failures. + expectedFailures := addrs.MakeMap[addrs.Referenceable, bool]() + sourceRanges := addrs.MakeMap[addrs.Referenceable, tfdiags.SourceRange]() + + for _, traversal := range run.Config.ExpectFailures { + // Ignore the diagnostics returned from the reference parsing, these + // references will have been checked earlier in the process by the + // validate stage so we don't need to do that again here. + reference, _ := addrs.ParseRefFromTestingScope(traversal) + expectedFailures.Put(reference.Subject, false) + sourceRanges.Put(reference.Subject, reference.SourceRange) + } + + var diags tfdiags.Diagnostics + for _, diag := range originals { + + if rule, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok { + switch rule.Container.CheckableKind() { + case addrs.CheckableOutputValue: + addr := rule.Container.(addrs.AbsOutputValue) + if !addr.Module.IsRoot() { + // failures can only be expected against checkable objects + // in the root module. This diagnostic will be added into + // returned set below. + break + } + + if diag.Severity() == tfdiags.Warning { + // Warnings don't count as errors. This diagnostic will be + // added into the returned set below. + break + } + + if expectedFailures.Has(addr.OutputValue) { + // Then this failure is expected! Mark the original map as + // having found a failure and swallow this error by + // continuing and not adding it into the returned set of + // diagnostics. + expectedFailures.Put(addr.OutputValue, true) + continue + } + + // Otherwise, this isn't an expected failure so just fall out + // and add it into the returned set of diagnostics below. + + case addrs.CheckableInputVariable: + addr := rule.Container.(addrs.AbsInputVariableInstance) + if !addr.Module.IsRoot() { + // failures can only be expected against checkable objects + // in the root module. This diagnostic will be added into + // returned set below. + break + } + + if diag.Severity() == tfdiags.Warning { + // Warnings don't count as errors. This diagnostic will be + // added into the returned set below. + break + } + if expectedFailures.Has(addr.Variable) { + // Then this failure is expected! Mark the original map as + // having found a failure and swallow this error by + // continuing and not adding it into the returned set of + // diagnostics. + expectedFailures.Put(addr.Variable, true) + continue + } + + // Otherwise, this isn't an expected failure so just fall out + // and add it into the returned set of diagnostics below. + + case addrs.CheckableResource: + addr := rule.Container.(addrs.AbsResourceInstance) + if !addr.Module.IsRoot() { + // failures can only be expected against checkable objects + // in the root module. This diagnostic will be added into + // returned set below. + break + } + + if diag.Severity() == tfdiags.Warning { + // Warnings don't count as errors. This diagnostic will be + // added into the returned set below. + break + } + + if expectedFailures.Has(addr.Resource) { + // Then this failure is expected! Mark the original map as + // having found a failure and swallow this error by + // continuing and not adding it into the returned set of + // diagnostics. + expectedFailures.Put(addr.Resource, true) + continue + } + + if expectedFailures.Has(addr.Resource.Resource) { + // We can also blanket expect failures in all instances for + // a resource so we check for that here as well. + expectedFailures.Put(addr.Resource.Resource, true) + continue + } + + // Otherwise, this isn't an expected failure so just fall out + // and add it into the returned set of diagnostics below. + + case addrs.CheckableCheck: + addr := rule.Container.(addrs.AbsCheck) + + // Check blocks are a bit more difficult than the others. Check + // block diagnostics could be from a nested data block, or + // from a failed assertion, and have all been marked as just + // warning severity. + // + // For diagnostics from failed assertions, we want to check if + // it was expected and skip it if it was. But if it wasn't + // expected we want to upgrade the diagnostic from a warning + // into an error so the test case will fail overall. + // + // For diagnostics from nested data blocks, we have two + // categories of diagnostics. First, diagnostics that were + // originally errors and we mapped into warnings. Second, + // diagnostics that were originally warnings and stayed that + // way. For the first case, we want to turn these back to errors + // and use them as part of the expected failures functionality. + // The second case should remain as warnings and be ignored by + // the expected failures functionality. + // + // Note, as well that we still want to upgrade failed checks + // from child modules into errors, so in the other branches we + // just do a simple blanket skip off all diagnostics not + // from the root module. We're more selective here, only + // diagnostics from the root module are considered for the + // expect failures functionality but we do also upgrade + // diagnostics from child modules back into errors. + + if rule.Type == addrs.CheckAssertion { + // Then this diagnostic is from a check block assertion, it + // is something we want to treat as an error even though it + // is actually claiming to be a warning. + + if addr.Module.IsRoot() && expectedFailures.Has(addr.Check) { + // Then this failure is expected! Mark the original map as + // having found a failure and continue. + expectedFailures.Put(addr.Check, true) + continue + } + + // Otherwise, let's package this up as an error and move on. + diags = diags.Append(tfdiags.Override(diag, tfdiags.Error, nil)) + continue + } else if rule.Type == addrs.CheckDataResource { + // Then the diagnostic we have was actually overridden so + // let's get back to the original. + original := tfdiags.UndoOverride(diag) + + // This diagnostic originated from a scoped data source. + if addr.Module.IsRoot() && original.Severity() == tfdiags.Error { + // Okay, we have a genuine error from the root module, + // so we can now check if we want to ignore it or not. + if expectedFailures.Has(addr.Check) { + // Then this failure is expected! Mark the original map as + // having found a failure and continue. + expectedFailures.Put(addr.Check, true) + continue + } + } + + // In all other cases, we want to add the original error + // into the set we return to the testing framework and move + // onto the next one. + diags = diags.Append(original) + continue + } else { + panic("invalid CheckType: " + rule.Type.String()) + } + default: + panic("unrecognized CheckableKind: " + rule.Container.CheckableKind().String()) + } + } + + // If we get here, then we're not modifying the original diagnostic at + // all. We just want the testing framework to treat it as normal. + diags = diags.Append(diag) + } + + // Okay, we've checked all our diagnostics to see if any were expected. + // Now, let's make sure that all the checkable objects we expected to fail + // actually did! + + for _, elem := range expectedFailures.Elems { + addr := elem.Key + failed := elem.Value + + if !failed { + // Then we expected a failure, and it did not occur. Add it to the + // diagnostics. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing expected failure", + Detail: fmt.Sprintf("The checkable object, %s, was expected to report an error but did not.", addr.String()), + Subject: sourceRanges.Get(addr).ToHCL().Ptr(), + Extra: missingExpectedFailure(true), + }) + } + } + + return diags +} + +// DiagnosticExtraFromMissingExpectedFailure provides an interface for diagnostic ExtraInfo to +// denote that a diagnostic was generated as a result of a missing expected failure. +type DiagnosticExtraFromMissingExpectedFailure interface { + DiagnosticFromMissingExpectedFailure() bool +} + +// DiagnosticFromMissingExpectedFailure checks if the provided diagnostic +// is a result of a missing expected failure. +func DiagnosticFromMissingExpectedFailure(diag tfdiags.Diagnostic) bool { + maybe := tfdiags.ExtraInfo[DiagnosticExtraFromMissingExpectedFailure](diag) + if maybe == nil { + return false + } + return maybe.DiagnosticFromMissingExpectedFailure() +} + +type missingExpectedFailure bool + +func (missingExpectedFailure) DiagnosticFromMissingExpectedFailure() bool { + return true +} diff --git a/internal/moduletest/run_test.go b/internal/moduletest/run_test.go new file mode 100644 index 0000000000..86b9debb1c --- /dev/null +++ b/internal/moduletest/run_test.go @@ -0,0 +1,801 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduletest + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestRun_ValidateExpectedFailures(t *testing.T) { + + type output struct { + Description tfdiags.Description + Severity tfdiags.Severity + } + + tcs := map[string]struct { + ExpectedFailures []string + Input tfdiags.Diagnostics + Output []output + }{ + "empty": { + ExpectedFailures: nil, + Input: nil, + Output: nil, + }, + "carries through simple diags": { + Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "simple error", + Detail: "want to see this in the returned set", + }) + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "simple warning", + Detail: "want to see this in the returned set", + }) + + return diags + }), + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "simple error", + Detail: "want to see this in the returned set", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "simple warning", + Detail: "want to see this in the returned set", + }, + Severity: tfdiags.Warning, + }, + }, + }, + "expected failures did not fail": { + ExpectedFailures: []string{ + "check.example", + }, + Input: nil, + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "Missing expected failure", + Detail: "The checkable object, check.example, was expected to report an error but did not.", + }, + Severity: tfdiags.Error, + }, + }, + }, + "outputs": { + ExpectedFailures: []string{ + "output.expected_one", + "output.expected_two", + }, + Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + + // First, let's create an output that failed that isn't + // expected. This should be unaffected by our function. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "unexpected failure", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "unexpected"}, + }, addrs.OutputPrecondition, 0), + }, + }) + + // Second, let's create an output that failed but is expected. + // Our function should remove this from the set of diags. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "expected_one"}, + }, addrs.OutputPrecondition, 0), + }, + }) + + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "expected warning", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "expected_one"}, + }, addrs.OutputPrecondition, 0), + }, + }) + + // The error we are adding here is for expected_two but in a + // child module. We expect that this diagnostic shouldn't + // trigger our expected failure, and that an extra diagnostic + // should be created complaining that the output wasn't actually + // triggered. + + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error in child module", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsOutputValue{ + Module: []addrs.ModuleInstanceStep{ + { + Name: "child_module", + }, + }, + OutputValue: addrs.OutputValue{Name: "expected_two"}, + }, addrs.OutputPrecondition, 0), + }, + }) + + return diags + }), + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "unexpected failure", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "expected warning", + Detail: "this should not be removed", + }, + Severity: tfdiags.Warning, + }, + { + Description: tfdiags.Description{ + Summary: "error in child module", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "Missing expected failure", + Detail: "The checkable object, output.expected_two, was expected to report an error but did not.", + }, + Severity: tfdiags.Error, + }, + }, + }, + "variables": { + ExpectedFailures: []string{ + "var.expected_one", + "var.expected_two", + }, + Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + + // First, let's create an input that failed that isn't + // expected. This should be unaffected by our function. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "unexpected failure", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsInputVariableInstance{ + Module: addrs.RootModuleInstance, + Variable: addrs.InputVariable{Name: "unexpected"}, + }, addrs.InputValidation, 0), + }, + }) + + // Second, let's create an input that failed but is expected. + // Our function should remove this from the set of diags. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsInputVariableInstance{ + Module: addrs.RootModuleInstance, + Variable: addrs.InputVariable{Name: "expected_one"}, + }, addrs.InputValidation, 0), + }, + }) + + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "expected warning", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsInputVariableInstance{ + Module: addrs.RootModuleInstance, + Variable: addrs.InputVariable{Name: "expected_one"}, + }, addrs.InputValidation, 0), + }, + }) + + // The error we are adding here is for expected_two but in a + // child module. We expect that this diagnostic shouldn't + // trigger our expected failure, and that an extra diagnostic + // should be created complaining that the output wasn't actually + // triggered. + + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error in child module", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsInputVariableInstance{ + Module: []addrs.ModuleInstanceStep{ + { + Name: "child_module", + }, + }, + Variable: addrs.InputVariable{Name: "expected_two"}, + }, addrs.InputValidation, 0), + }, + }) + + return diags + }), + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "unexpected failure", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "expected warning", + Detail: "this should not be removed", + }, + Severity: tfdiags.Warning, + }, + { + Description: tfdiags.Description{ + Summary: "error in child module", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "Missing expected failure", + Detail: "The checkable object, var.expected_two, was expected to report an error but did not.", + }, + Severity: tfdiags.Error, + }, + }, + }, + "resources": { + ExpectedFailures: []string{ + "test_instance.single", + "test_instance.all_instances", + "test_instance.instance[0]", + "test_instance.instance[2]", + "test_instance.missing", + }, + Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + // First, we'll create an unexpected failure that should be + // carried through untouched. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "unexpected failure", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "unexpected", + }, + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + + // Second, we'll create a failure from our test_instance.single + // resource that should be removed. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.single", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "single", + }, + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + + // Third, we'll create a warning from our test_instance.single + // resource that should be propagated as it is only a warning. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "expected warning in test_instance.single", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "single", + }, + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + + // Fourth, we'll create diagnostics from several instances of + // the test_instance.all_instances which should all be removed. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.all_instances[0]", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "all_instances", + }, + Key: addrs.IntKey(0), + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.all_instances[1]", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "all_instances", + }, + Key: addrs.IntKey(1), + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.all_instances[2]", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "all_instances", + }, + Key: addrs.IntKey(2), + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + + // Fifth, we'll create diagnostics for several instances of + // the test_instance.instance resource, only some of which + // should be removed. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.instance[0]", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }, + Key: addrs.IntKey(0), + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.instance[1]", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }, + Key: addrs.IntKey(1), + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "expected failure in test_instance.instance[2]", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "instance", + }, + Key: addrs.IntKey(2), + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + + // Finally, we'll create an error that originated from + // test_instance.missing but in a child module which shouldn't + // be removed. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failure in child module", + Detail: "this should not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsResourceInstance{ + Module: []addrs.ModuleInstanceStep{ + { + Name: "child_module", + }, + }, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "missing", + }, + }, + }, addrs.ResourcePrecondition, 0), + }, + }) + + return diags + }), + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "unexpected failure", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "expected warning in test_instance.single", + Detail: "this should not be removed", + }, + Severity: tfdiags.Warning, + }, + { + Description: tfdiags.Description{ + Summary: "expected failure in test_instance.instance[1]", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "failure in child module", + Detail: "this should not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "Missing expected failure", + Detail: "The checkable object, test_instance.missing, was expected to report an error but did not.", + }, + Severity: tfdiags.Error, + }, + }, + }, + "check_assertions": { + ExpectedFailures: []string{ + "check.expected", + "check.missing", + }, + Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + // First, we'll add an unexpected warning from a check block + // assertion that should get upgraded to an error. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "unexpected failure", + Detail: "this should upgrade and not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: addrs.RootModuleInstance, + Check: addrs.Check{ + Name: "unexpected", + }, + }, addrs.CheckAssertion, 0), + }, + }) + + // Second, we'll add an unexpected warning from a check block + // in a child module that should get upgrade to error. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "expected failure in child module", + Detail: "this should upgrade and not be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: []addrs.ModuleInstanceStep{ + { + Name: "child_module", + }, + }, + Check: addrs.Check{ + Name: "expected", + }, + }, addrs.CheckAssertion, 0), + }, + }) + + // Third, we'll add an expected warning from a check block + // assertion that should be removed. + diags = diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "expected failure", + Detail: "this should be removed", + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: addrs.RootModuleInstance, + Check: addrs.Check{ + Name: "expected", + }, + }, addrs.CheckAssertion, 0), + }, + }) + + // The second expected failure has no diagnostics, we just want + // to make sure that a new diagnostic is added for this case. + + return diags + }), + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "unexpected failure", + Detail: "this should upgrade and not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "expected failure in child module", + Detail: "this should upgrade and not be removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "Missing expected failure", + Detail: "The checkable object, check.missing, was expected to report an error but did not.", + }, + Severity: tfdiags.Error, + }, + }, + }, + "check_data_sources": { + ExpectedFailures: []string{ + "check.expected", + }, + Input: createDiagnostics(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + // First, we'll add an unexpected warning from a check block + // assertion that should be propagated as an error. + diags = diags.Append( + tfdiags.Override( + tfdiags.Sourceless(tfdiags.Error, "unexpected failure", "this should be an error and not removed"), + tfdiags.Warning, + func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: addrs.RootModuleInstance, + Check: addrs.Check{ + Name: "unexpected", + }, + }, addrs.CheckDataResource, 0), + } + })) + + // Second, we'll add an unexpected warning from a check block + // assertion that should remain as a warning. + diags = diags.Append( + tfdiags.Override( + tfdiags.Sourceless(tfdiags.Warning, "unexpected warning", "this should be a warning and not removed"), + tfdiags.Warning, + func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: addrs.RootModuleInstance, + Check: addrs.Check{ + Name: "unexpected", + }, + }, addrs.CheckDataResource, 0), + } + })) + + // Third, we'll add an unexpected warning from a check block + // in a child module that should be propagated as an error. + diags = diags.Append( + tfdiags.Override( + tfdiags.Sourceless(tfdiags.Error, "expected failure from child module", "this should be an error and not removed"), + tfdiags.Warning, + func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: []addrs.ModuleInstanceStep{ + { + Name: "child_module", + }, + }, + Check: addrs.Check{ + Name: "expected", + }, + }, addrs.CheckDataResource, 0), + } + })) + + // Fourth, we'll add an expected warning that should be removed. + diags = diags.Append( + tfdiags.Override( + tfdiags.Sourceless(tfdiags.Error, "expected failure", "this should be removed"), + tfdiags.Warning, + func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addrs.AbsCheck{ + Module: addrs.RootModuleInstance, + Check: addrs.Check{ + Name: "expected", + }, + }, addrs.CheckDataResource, 0), + } + })) + + return diags + }), + Output: []output{ + { + Description: tfdiags.Description{ + Summary: "unexpected failure", + Detail: "this should be an error and not removed", + }, + Severity: tfdiags.Error, + }, + { + Description: tfdiags.Description{ + Summary: "unexpected warning", + Detail: "this should be a warning and not removed", + }, + Severity: tfdiags.Warning, + }, + { + Description: tfdiags.Description{ + Summary: "expected failure from child module", + Detail: "this should be an error and not removed", + }, + Severity: tfdiags.Error, + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + var traversals []hcl.Traversal + for _, ef := range tc.ExpectedFailures { + traversal, diags := hclsyntax.ParseTraversalAbs([]byte(ef), "foo.tf", hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + t.Errorf("invalid expected failure %s: %v", ef, diags.Error()) + } + traversals = append(traversals, traversal) + } + + if t.Failed() { + return + } + + run := Run{ + Config: &configs.TestRun{ + ExpectFailures: traversals, + }, + } + + out := run.ValidateExpectedFailures(tc.Input) + ix := 0 + for ; ix < len(tc.Output); ix++ { + expected := tc.Output[ix] + + if ix >= len(out) { + t.Errorf("missing diagnostic at %d, expected: [%s] %s, %s", ix, expected.Severity, expected.Description.Summary, expected.Description.Detail) + continue + } + + actual := output{ + Description: out[ix].Description(), + Severity: out[ix].Severity(), + } + + if diff := cmp.Diff(expected, actual); len(diff) > 0 { + t.Errorf("mismatched diagnostic at %d:\n%s", ix, diff) + } + } + + for ; ix < len(out); ix++ { + actual := out[ix] + t.Errorf("additional diagnostic at %d: [%s] %s, %s", ix, actual.Severity(), actual.Description().Summary, actual.Description().Detail) + } + }) + } +} + +func createDiagnostics(populate func(diags tfdiags.Diagnostics) tfdiags.Diagnostics) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = populate(diags) + return diags +} diff --git a/internal/moduletest/status.go b/internal/moduletest/status.go new file mode 100644 index 0000000000..a7f4c1440c --- /dev/null +++ b/internal/moduletest/status.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package moduletest + +// Status represents the status of a test case, and is defined as an iota within +// this file. +// +// The order of the definitions matter as different statuses do naturally take +// precedence over others. A test suite that has a mix of pass and fail statuses +// has failed overall and therefore the fail status is of higher precedence than +// the pass status. +// +// See the Status.Merge function for this requirement being used in action. +// +//go:generate go tool golang.org/x/tools/cmd/stringer -type=Status status.go +type Status int + +const ( + Pending Status = iota + Skip + Pass + Fail + Error +) + +// Merge compares two statuses and returns a status that best represents the two +// together. +// +// This should be used to collate the overall status of a test file or test +// suite from the collection of test runs that have been executed. +// +// Essentially, if a test suite has a bunch of failures and passes the overall +// status would be failure. If a test suite has all passes, then the test suite +// would be pass overall. +// +// The implementation basically always returns the highest of the two, which +// means the order the statuses are defined within the iota matters. +func (status Status) Merge(next Status) Status { + if next > status { + return next + } + return status +} diff --git a/internal/moduletest/status_string.go b/internal/moduletest/status_string.go index f224997913..437ad622c2 100644 --- a/internal/moduletest/status_string.go +++ b/internal/moduletest/status_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=Status assertion.go"; DO NOT EDIT. +// Code generated by "stringer -type=Status status.go"; DO NOT EDIT. package moduletest @@ -8,32 +8,20 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[Pending-63] - _ = x[Passed-80] - _ = x[Failed-70] - _ = x[Error-69] + _ = x[Pending-0] + _ = x[Skip-1] + _ = x[Pass-2] + _ = x[Fail-3] + _ = x[Error-4] } -const ( - _Status_name_0 = "Pending" - _Status_name_1 = "ErrorFailed" - _Status_name_2 = "Passed" -) +const _Status_name = "PendingSkipPassFailError" -var ( - _Status_index_1 = [...]uint8{0, 5, 11} -) +var _Status_index = [...]uint8{0, 7, 11, 15, 19, 24} func (i Status) String() string { - switch { - case i == 63: - return _Status_name_0 - case 69 <= i && i <= 70: - i -= 69 - return _Status_name_1[_Status_index_1[i]:_Status_index_1[i+1]] - case i == 80: - return _Status_name_2 - default: + if i < 0 || i >= Status(len(_Status_index)-1) { return "Status(" + strconv.FormatInt(int64(i), 10) + ")" } + return _Status_name[_Status_index[i]:_Status_index[i+1]] } diff --git a/internal/moduletest/suite.go b/internal/moduletest/suite.go index c548c923e3..b7a165d4a7 100644 --- a/internal/moduletest/suite.go +++ b/internal/moduletest/suite.go @@ -1,7 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package moduletest -// A Suite is a set of tests run together as a single Terraform configuration. +import "github.com/hashicorp/terraform/internal/tfdiags" + type Suite struct { - Name string - Components map[string]*Component + Status Status + + Files map[string]*File +} + +type TestSuiteRunner interface { + Test() (Status, tfdiags.Diagnostics) + Stop() + Cancel() + + // IsStopped allows code outside the moduletest package to confirm the suite was stopped + // when handling a graceful exit scenario + IsStopped() bool } diff --git a/internal/namedvals/doc.go b/internal/namedvals/doc.go new file mode 100644 index 0000000000..4cff920a96 --- /dev/null +++ b/internal/namedvals/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package namedvals has container types to help with modelling the gradual +// evaluation of local values (input variable values, local values, output +// values) during a graph walk. +package namedvals diff --git a/internal/namedvals/state.go b/internal/namedvals/state.go new file mode 100644 index 0000000000..a96f10c6e3 --- /dev/null +++ b/internal/namedvals/state.go @@ -0,0 +1,126 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package namedvals + +import ( + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// State is the main type in this package, representing the current state of +// evaluation that can be mutated as Terraform Core visits different graph +// nodes and then queried to find values that were already resolved earlier +// in the graph walk. +// +// Instances of this type are concurrency-safe so callers do not need to +// implement their own locking when reading and writing from named value +// state. +type State struct { + mu sync.Mutex + + variables inputVariableValues + locals localValues + outputs outputValues +} + +func NewState() *State { + return &State{ + variables: newValues[addrs.InputVariable, addrs.AbsInputVariableInstance](), + locals: newValues[addrs.LocalValue, addrs.AbsLocalValue](), + outputs: newValues[addrs.OutputValue, addrs.AbsOutputValue](), + } +} + +func (s *State) SetInputVariableValue(addr addrs.AbsInputVariableInstance, val cty.Value) { + s.mu.Lock() + s.variables.SetExactResult(addr, val) + s.mu.Unlock() +} + +func (s *State) GetInputVariableValue(addr addrs.AbsInputVariableInstance) cty.Value { + s.mu.Lock() + defer s.mu.Unlock() + return s.variables.GetExactResult(addr) +} + +func (s *State) HasInputVariableValue(addr addrs.AbsInputVariableInstance) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.variables.HasExactResult(addr) +} + +func (s *State) SetInputVariablePlaceholder(addr addrs.InPartialExpandedModule[addrs.InputVariable], val cty.Value) { + s.mu.Lock() + s.variables.SetPlaceholderResult(addr, val) + s.mu.Unlock() +} + +func (s *State) GetInputVariablePlaceholder(addr addrs.InPartialExpandedModule[addrs.InputVariable]) cty.Value { + s.mu.Lock() + defer s.mu.Unlock() + return s.variables.GetPlaceholderResult(addr) +} + +func (s *State) SetLocalValue(addr addrs.AbsLocalValue, val cty.Value) { + s.mu.Lock() + s.locals.SetExactResult(addr, val) + s.mu.Unlock() +} + +func (s *State) GetLocalValue(addr addrs.AbsLocalValue) cty.Value { + s.mu.Lock() + defer s.mu.Unlock() + return s.locals.GetExactResult(addr) +} + +func (s *State) HasLocalValue(addr addrs.AbsLocalValue) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.locals.HasExactResult(addr) +} + +func (s *State) SetLocalValuePlaceholder(addr addrs.InPartialExpandedModule[addrs.LocalValue], val cty.Value) { + s.mu.Lock() + s.locals.SetPlaceholderResult(addr, val) + s.mu.Unlock() +} + +func (s *State) GetLocalValuePlaceholder(addr addrs.InPartialExpandedModule[addrs.LocalValue]) cty.Value { + s.mu.Lock() + defer s.mu.Unlock() + return s.locals.GetPlaceholderResult(addr) +} + +func (s *State) SetOutputValue(addr addrs.AbsOutputValue, val cty.Value) { + s.mu.Lock() + s.outputs.SetExactResult(addr, val) + s.mu.Unlock() +} + +func (s *State) GetOutputValue(addr addrs.AbsOutputValue) cty.Value { + s.mu.Lock() + defer s.mu.Unlock() + return s.outputs.GetExactResult(addr) +} + +func (s *State) HasOutputValue(addr addrs.AbsOutputValue) bool { + s.mu.Lock() + defer s.mu.Unlock() + return s.outputs.HasExactResult(addr) +} + +func (s *State) SetOutputValuePlaceholder(addr addrs.InPartialExpandedModule[addrs.OutputValue], val cty.Value) { + s.mu.Lock() + s.outputs.SetPlaceholderResult(addr, val) + s.mu.Unlock() +} + +func (s *State) GetOutputValuePlaceholder(addr addrs.InPartialExpandedModule[addrs.OutputValue]) cty.Value { + s.mu.Lock() + defer s.mu.Unlock() + return s.outputs.GetPlaceholderResult(addr) +} diff --git a/internal/namedvals/values.go b/internal/namedvals/values.go new file mode 100644 index 0000000000..f24a89c162 --- /dev/null +++ b/internal/namedvals/values.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package namedvals + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// Values is a type we use internally to track values that were already +// resolved. +// +// This container encapsulates the problem of dealing with placeholder values +// in modules whose instance addresses are not yet fully known due to unknown +// values in count or for_each. Callers can register values in module paths +// of different levels of "known-ness" and then when queried the result will +// be the value from the most specific registration seen so far. +// +// Values is not concurrency-safe, because we expect to use it only indirectly +// through the public-facing [State] type and it should be the one to ensure +// concurrency safety. +type values[LocalType namedValueAddr, AbsType namedValueAddr] struct { + // exact are values for objects in fully-qualified module instances. + exact addrs.Map[AbsType, cty.Value] + + // placeholder are placeholder values to use for objects under an + // unexpanded module prefix. These placeholders will contain known + // values only in positions that are guaranteed to have equal values + // across all module instances under a given prefix, thereby allowing + // similar placeholder evaluation for other objects downstream of + // the partially-evaluated ones. + // + // This one is more awkward because there can be conflicting placeholders + // for the same object at different levels of specificity, and so we'll + // need to scan over elements to find ones that match the most. To slightly + // optimize that we have the values bucketed by the static module address + // they belong to; maybe we'll optimize this more later if it seems like + // a bottleneck, but we're assuming a relatively small number of declared + // objects of each type in each module. + placeholder addrs.Map[addrs.Module, addrs.Map[addrs.InPartialExpandedModule[LocalType], cty.Value]] +} + +type inputVariableValues = values[addrs.InputVariable, addrs.AbsInputVariableInstance] +type localValues = values[addrs.LocalValue, addrs.AbsLocalValue] +type outputValues = values[addrs.OutputValue, addrs.AbsOutputValue] + +// namedValueAddr describes the address behaviors we need for address types +// we'll use with type [values] as defined above. +type namedValueAddr interface { + addrs.UniqueKeyer + fmt.Stringer +} + +func newValues[LocalType namedValueAddr, AbsType namedValueAddr]() values[LocalType, AbsType] { + return values[LocalType, AbsType]{ + exact: addrs.MakeMap[AbsType, cty.Value](), + placeholder: addrs.MakeMap[addrs.Module, addrs.Map[addrs.InPartialExpandedModule[LocalType], cty.Value]](), + } +} + +func (v *values[LocalType, AbsType]) SetExactResult(addr AbsType, val cty.Value) { + if v.exact.Has(addr) { + // This is always a bug in the caller, because Terraform Core should + // use its graph to ensure that each value gets set exactly once and + // the values get registered in the correct order. + panic(fmt.Sprintf("value for %s was already set by an earlier caller", addr)) + } + v.exact.Put(addr, val) +} + +func (v *values[LocalType, AbsType]) HasExactResult(addr AbsType) bool { + return v.exact.Has(addr) +} + +func (v *values[LocalType, AbsType]) GetExactResult(addr AbsType) cty.Value { + // TODO: Do we need to handle placeholder results in here too? Seems like + // callers should not end up holding AbsType addresses if they are dealing + // with unexpanded objects because they wouldn't have instance keys to + // fill in to the address, so assuming not for now. + if !v.exact.Has(addr) { + // This is always a bug in the caller, because Terraform Core should + // use its graph to ensure that each value gets set exactly once and + // the values get registered in the correct order. + panic(fmt.Sprintf("value for %s was requested before it was provided", addr)) + } + return v.exact.Get(addr) +} + +func (v *values[LocalType, AbsType]) GetExactResults() addrs.Map[AbsType, cty.Value] { + return v.exact +} + +func (v *values[LocalType, AbsType]) SetPlaceholderResult(addr addrs.InPartialExpandedModule[LocalType], val cty.Value) { + modAddr := addr.Module.Module() + if !v.placeholder.Has(modAddr) { + v.placeholder.Put(modAddr, addrs.MakeMap[addrs.InPartialExpandedModule[LocalType], cty.Value]()) + } + placeholders := v.placeholder.Get(modAddr) + if placeholders.Has(addr) { + // This is always a bug in the caller, because Terraform Core should + // use its graph to ensure that each value gets set exactly once and + // the values get registered in the correct order. + panic(fmt.Sprintf("placeholder value for %s was already set by an earlier caller", addr)) + } + placeholders.Put(addr, val) +} + +func (v *values[LocalType, AbsType]) GetPlaceholderResult(addr addrs.InPartialExpandedModule[LocalType]) cty.Value { + modAddr := addr.Module.Module() + if !v.placeholder.Has(modAddr) { + return cty.DynamicVal + } + placeholders := v.placeholder.Get(modAddr) + + // We'll now search the placeholders for just the ones that match the + // given address, and take the one that has the longest known module prefix. + longestVal := cty.DynamicVal + longestLen := -1 + + for _, elem := range placeholders.Elems { + candidate := elem.Key + lenKnown := candidate.ModuleLevelsKnown() + if lenKnown < longestLen { + continue + } + if !addrs.Equivalent(candidate.Local, addr.Local) { + continue + } + if !candidate.Module.MatchesPartial(addr.Module) { + continue + } + longestVal = elem.Value + longestLen = lenKnown + } + + return longestVal +} diff --git a/internal/namedvals/values_test.go b/internal/namedvals/values_test.go new file mode 100644 index 0000000000..dd29f8993b --- /dev/null +++ b/internal/namedvals/values_test.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package namedvals + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/zclconf/go-cty/cty" +) + +func TestValues(t *testing.T) { + // The behavior of [values] is the same for all named value address types, + // and so we'll just use local values here as a placeholder and assume + // that input variables and output values would also work. + + // The following addresses are taking some liberties with which combinations + // would actually be possible in practice with a real Terraform + // configuration -- unknowns and knowns cannot typically mix at the same + // known-expansion module prefix -- but the abstraction in this package + // doesn't aim to enforce those rules, and so we can expect it to be a + // little more flexible here than it really needs to be, as a way to + // reduce the amount of test setup we need. + childInst0 := addrs.ModuleInstance{{Name: "child", InstanceKey: addrs.IntKey(0)}} + childInst1 := addrs.ModuleInstance{{Name: "child", InstanceKey: addrs.IntKey(1)}} + childInst2 := addrs.ModuleInstance{{Name: "child", InstanceKey: addrs.IntKey(2)}} + childInstUnk := addrs.RootModuleInstance.UnexpandedChild(addrs.ModuleCall{Name: "child"}) + grandchildInst0_0 := childInst0.Child("grandchild", addrs.IntKey(0)) + grandchildInst1_unk := childInst1.UnexpandedChild(addrs.ModuleCall{Name: "grandchild"}) + grandchildInst2_unk := childInst2.UnexpandedChild(addrs.ModuleCall{Name: "grandchild"}) + grandchildInstUnk_unk := childInstUnk.Child(addrs.ModuleCall{Name: "grandchild"}) + + inRoot := addrs.LocalValue{Name: "in_root"}.Absolute(addrs.RootModuleInstance) + inChild0 := addrs.LocalValue{Name: "in_child"}.Absolute(childInst0) + inChildUnk := addrs.ObjectInPartialExpandedModule(childInstUnk, addrs.LocalValue{Name: "in_child"}) + inGrandchild0_0 := addrs.LocalValue{Name: "in_grandchild"}.Absolute(grandchildInst0_0) + inGrandchild1_unk := addrs.ObjectInPartialExpandedModule(grandchildInst1_unk, addrs.LocalValue{Name: "in_grandchild"}) + inGrandchild2_unk := addrs.ObjectInPartialExpandedModule(grandchildInst2_unk, addrs.LocalValue{Name: "in_grandchild"}) + inGrandchildUnk_unk := addrs.ObjectInPartialExpandedModule(grandchildInstUnk_unk, addrs.LocalValue{Name: "in_grandchild"}) + + vals := newValues[addrs.LocalValue, addrs.AbsLocalValue]() + vals.SetExactResult(inRoot, cty.StringVal("in root")) + vals.SetExactResult(inChild0, cty.StringVal("in child 0")) + vals.SetExactResult(inGrandchild0_0, cty.StringVal("in grandchild 0, 0")) + vals.SetPlaceholderResult(inChildUnk, cty.StringVal("placeholder for all unknown instances of child")) + vals.SetPlaceholderResult(inGrandchild1_unk, cty.StringVal("placeholder for all unknown instances of child 1 grandchild")) + vals.SetPlaceholderResult(inGrandchildUnk_unk, cty.StringVal("placeholder for all unknown instances of child unknown grandchild")) + + t.Run("exact values", func(t *testing.T) { + // Exact values require the given address to exactly match something + // that was registered. + + if got, want := vals.GetExactResult(inRoot), cty.StringVal("in root"); !want.RawEquals(got) { + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inRoot, got, want) + } + if got, want := vals.GetExactResult(inChild0), cty.StringVal("in child 0"); !want.RawEquals(got) { + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inChild0, got, want) + } + if got, want := vals.GetExactResult(inGrandchild0_0), cty.StringVal("in grandchild 0, 0"); !want.RawEquals(got) { + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inGrandchild0_0, got, want) + } + }) + t.Run("placeholder values", func(t *testing.T) { + // Placeholder values are selected by longest-prefix pattern matching, + // and so we can ask both for specific prefixes we registered above + // and for more specific prefixes that we didn't register but yet + // still match a less-specific address. + + if got, want := vals.GetPlaceholderResult(inChildUnk), cty.StringVal("placeholder for all unknown instances of child"); !want.RawEquals(got) { + // This one exactly matches one of the address patterns we registered. + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inChildUnk, got, want) + } + if got, want := vals.GetPlaceholderResult(inGrandchild1_unk), cty.StringVal("placeholder for all unknown instances of child 1 grandchild"); !want.RawEquals(got) { + // This one exactly matches one of the address patterns we registered. + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inGrandchild1_unk, got, want) + } + if got, want := vals.GetPlaceholderResult(inGrandchild2_unk), cty.StringVal("placeholder for all unknown instances of child unknown grandchild"); !want.RawEquals(got) { + // This one falls back to the placeholder for when the child + // instance key isn't known, because there isn't a more specific + // placeholder available. + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inGrandchild2_unk, got, want) + } + if got, want := vals.GetPlaceholderResult(inGrandchildUnk_unk), cty.StringVal("placeholder for all unknown instances of child unknown grandchild"); !want.RawEquals(got) { + // This one falls back to the placeholder for when the child + // instance key isn't known, because we can't know which of + // the more specific placeholders to select. + t.Errorf("wrong exact value for %s\ngot: %#v\nwant: %#v", inGrandchildUnk_unk, got, want) + } + }) +} diff --git a/internal/plans/action.go b/internal/plans/action.go index c653b106b3..5105deba64 100644 --- a/internal/plans/action.go +++ b/internal/plans/action.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans type Action rune @@ -10,13 +13,17 @@ const ( DeleteThenCreate Action = '∓' CreateThenDelete Action = '±' Delete Action = '-' + Forget Action = '.' + CreateThenForget Action = '⨥' + Open Action = '⟃' + Renew Action = '⟳' + Close Action = '⫏' ) -//go:generate go run golang.org/x/tools/cmd/stringer -type Action +//go:generate go tool golang.org/x/tools/cmd/stringer -type Action -// IsReplace returns true if the action is one of the two actions that -// represents replacing an existing object with a new object: -// DeleteThenCreate or CreateThenDelete. +// IsReplace returns true if the action is one of the actions that +// represent replacing an existing object with a new object. func (a Action) IsReplace() bool { - return a == DeleteThenCreate || a == CreateThenDelete + return a == DeleteThenCreate || a == CreateThenDelete || a == CreateThenForget } diff --git a/internal/plans/action_string.go b/internal/plans/action_string.go index be43ab1757..35f6c13e69 100644 --- a/internal/plans/action_string.go +++ b/internal/plans/action_string.go @@ -15,35 +15,33 @@ func _() { _ = x[DeleteThenCreate-8723] _ = x[CreateThenDelete-177] _ = x[Delete-45] + _ = x[Forget-46] + _ = x[CreateThenForget-10789] + _ = x[Open-10179] + _ = x[Renew-10227] + _ = x[Close-10959] } -const ( - _Action_name_0 = "NoOp" - _Action_name_1 = "Create" - _Action_name_2 = "Delete" - _Action_name_3 = "Update" - _Action_name_4 = "CreateThenDelete" - _Action_name_5 = "Read" - _Action_name_6 = "DeleteThenCreate" -) +const _Action_name = "NoOpCreateDeleteForgetUpdateCreateThenDeleteReadDeleteThenCreateOpenRenewCreateThenForgetClose" + +var _Action_map = map[Action]string{ + 0: _Action_name[0:4], + 43: _Action_name[4:10], + 45: _Action_name[10:16], + 46: _Action_name[16:22], + 126: _Action_name[22:28], + 177: _Action_name[28:44], + 8592: _Action_name[44:48], + 8723: _Action_name[48:64], + 10179: _Action_name[64:68], + 10227: _Action_name[68:73], + 10789: _Action_name[73:89], + 10959: _Action_name[89:94], +} func (i Action) String() string { - switch { - case i == 0: - return _Action_name_0 - case i == 43: - return _Action_name_1 - case i == 45: - return _Action_name_2 - case i == 126: - return _Action_name_3 - case i == 177: - return _Action_name_4 - case i == 8592: - return _Action_name_5 - case i == 8723: - return _Action_name_6 - default: - return "Action(" + strconv.FormatInt(int64(i), 10) + ")" + if str, ok := _Action_map[i]; ok { + return str } + return "Action(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/internal/plans/changes.go b/internal/plans/changes.go index 1a4331528c..c3a389ac69 100644 --- a/internal/plans/changes.go +++ b/internal/plans/changes.go @@ -1,20 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( + "fmt" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) // Changes describes various actions that Terraform will attempt to take if // the corresponding plan is applied. -// -// A Changes object can be rendered into a visual diff (by the caller, using -// code in another package) for display to the user. type Changes struct { // Resources tracks planned changes to resource instance objects. - Resources []*ResourceInstanceChangeSrc + Resources []*ResourceInstanceChange // Outputs tracks planned changes output values. // @@ -24,7 +30,7 @@ type Changes struct { // externally-visible, while other outputs are implementation details and // can be easily re-calculated during the apply phase. Therefore only root // module outputs will survive a round-trip through a plan file. - Outputs []*OutputChangeSrc + Outputs []*OutputChange } // NewChanges returns a valid Changes object that describes no changes. @@ -32,26 +38,54 @@ func NewChanges() *Changes { return &Changes{} } -func (c *Changes) Empty() bool { - for _, res := range c.Resources { - if res.Action != NoOp || res.Moved() { - return false +// Encode encodes all the stored resource and output changes into a new *ChangeSrc value +func (c *Changes) Encode(schemas *schemarepo.Schemas) (*ChangesSrc, error) { + // a plan is always built even when there are errors, so make sure to return + // a valid changesSrc. + changesSrc := NewChangesSrc() + + for _, rc := range c.Resources { + p, ok := schemas.Providers[rc.ProviderAddr.Provider] + if !ok { + return changesSrc, fmt.Errorf("Changes.Encode: missing provider %s for %s", rc.ProviderAddr, rc.Addr) } + + var schema providers.Schema + switch rc.Addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + schema = p.ResourceTypes[rc.Addr.Resource.Resource.Type] + case addrs.DataResourceMode: + schema = p.DataSources[rc.Addr.Resource.Resource.Type] + default: + panic(fmt.Sprintf("unexpected resource mode %s", rc.Addr.Resource.Resource.Mode)) + } + + if schema.Body == nil { + return changesSrc, fmt.Errorf("Changes.Encode: missing schema for %s", rc.Addr) + } + rcs, err := rc.Encode(schema) + if err != nil { + return changesSrc, fmt.Errorf("Changes.Encode: %w", err) + } + + changesSrc.Resources = append(changesSrc.Resources, rcs) } - for _, out := range c.Outputs { - if out.Addr.Module.IsRoot() && out.Action != NoOp { - return false + for _, ocs := range c.Outputs { + oc, err := ocs.Encode() + if err != nil { + return changesSrc, err } + changesSrc.Outputs = append(changesSrc.Outputs, oc) } - return true + return changesSrc, nil } // ResourceInstance returns the planned change for the current object of the // resource instance of the given address, if any. Returns nil if no change is // planned. -func (c *Changes) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstanceChangeSrc { +func (c *Changes) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstanceChange { for _, rc := range c.Resources { if rc.Addr.Equal(addr) && rc.DeposedKey == states.NotDeposed { return rc @@ -65,8 +99,8 @@ func (c *Changes) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInst // InstancesForAbsResource returns the planned change for the current objects // of the resource instances of the given address, if any. Returns nil if no // changes are planned. -func (c *Changes) InstancesForAbsResource(addr addrs.AbsResource) []*ResourceInstanceChangeSrc { - var changes []*ResourceInstanceChangeSrc +func (c *Changes) InstancesForAbsResource(addr addrs.AbsResource) []*ResourceInstanceChange { + var changes []*ResourceInstanceChange for _, rc := range c.Resources { resAddr := rc.Addr.ContainingResource() if resAddr.Equal(addr) && rc.DeposedKey == states.NotDeposed { @@ -80,8 +114,8 @@ func (c *Changes) InstancesForAbsResource(addr addrs.AbsResource) []*ResourceIns // InstancesForConfigResource returns the planned change for the current objects // of the resource instances of the given address, if any. Returns nil if no // changes are planned. -func (c *Changes) InstancesForConfigResource(addr addrs.ConfigResource) []*ResourceInstanceChangeSrc { - var changes []*ResourceInstanceChangeSrc +func (c *Changes) InstancesForConfigResource(addr addrs.ConfigResource) []*ResourceInstanceChange { + var changes []*ResourceInstanceChange for _, rc := range c.Resources { resAddr := rc.Addr.ContainingResource().Config() if resAddr.Equal(addr) && rc.DeposedKey == states.NotDeposed { @@ -95,7 +129,7 @@ func (c *Changes) InstancesForConfigResource(addr addrs.ConfigResource) []*Resou // ResourceInstanceDeposed returns the plan change of a deposed object of // the resource instance of the given address, if any. Returns nil if no change // is planned. -func (c *Changes) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key states.DeposedKey) *ResourceInstanceChangeSrc { +func (c *Changes) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key states.DeposedKey) *ResourceInstanceChange { for _, rc := range c.Resources { if rc.Addr.Equal(addr) && rc.DeposedKey == key { return rc @@ -108,7 +142,7 @@ func (c *Changes) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key st // OutputValue returns the planned change for the output value with the // // given address, if any. Returns nil if no change is planned. -func (c *Changes) OutputValue(addr addrs.AbsOutputValue) *OutputChangeSrc { +func (c *Changes) OutputValue(addr addrs.AbsOutputValue) *OutputChange { for _, oc := range c.Outputs { if oc.Addr.Equal(addr) { return oc @@ -119,8 +153,8 @@ func (c *Changes) OutputValue(addr addrs.AbsOutputValue) *OutputChangeSrc { } // RootOutputValues returns planned changes for all outputs of the root module. -func (c *Changes) RootOutputValues() []*OutputChangeSrc { - var res []*OutputChangeSrc +func (c *Changes) RootOutputValues() []*OutputChange { + var res []*OutputChange for _, oc := range c.Outputs { // we can't evaluate root module outputs @@ -138,8 +172,8 @@ func (c *Changes) RootOutputValues() []*OutputChangeSrc { // OutputValues returns planned changes for all outputs for all module // instances that reside in the parent path. Returns nil if no changes are // planned. -func (c *Changes) OutputValues(parent addrs.ModuleInstance, module addrs.ModuleCall) []*OutputChangeSrc { - var res []*OutputChangeSrc +func (c *Changes) OutputValues(parent addrs.ModuleInstance, module addrs.ModuleCall) []*OutputChange { + var res []*OutputChange for _, oc := range c.Outputs { // we can't evaluate root module outputs @@ -245,8 +279,8 @@ type ResourceInstanceChange struct { // Encode produces a variant of the reciever that has its change values // serialized so it can be written to a plan file. Pass the implied type of the // corresponding resource type schema for correct operation. -func (rc *ResourceInstanceChange) Encode(ty cty.Type) (*ResourceInstanceChangeSrc, error) { - cs, err := rc.Change.Encode(ty) +func (rc *ResourceInstanceChange) Encode(schema providers.Schema) (*ResourceInstanceChangeSrc, error) { + cs, err := rc.Change.Encode(&schema) if err != nil { return nil, err } @@ -257,6 +291,7 @@ func (rc *ResourceInstanceChange) Encode(ty cty.Type) (*ResourceInstanceChangeSr prevRunAddr = rc.Addr } return &ResourceInstanceChangeSrc{ + Addr: rc.Addr, PrevRunAddr: prevRunAddr, DeposedKey: rc.DeposedKey, @@ -268,6 +303,15 @@ func (rc *ResourceInstanceChange) Encode(ty cty.Type) (*ResourceInstanceChangeSr }, err } +func (rc *ResourceInstanceChange) DeepCopy() *ResourceInstanceChange { + if rc == nil { + return rc + } + + ret := *rc + return &ret +} + func (rc *ResourceInstanceChange) Moved() bool { return !rc.Addr.Equal(rc.PrevRunAddr) } @@ -301,9 +345,13 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha Private: rc.Private, ProviderAddr: rc.ProviderAddr, Change: Change{ - Action: Delete, - Before: rc.Before, - After: cty.NullVal(rc.Before.Type()), + Action: Delete, + Before: rc.Before, + BeforeIdentity: rc.BeforeIdentity, + After: cty.NullVal(rc.Before.Type()), + AfterIdentity: cty.NullVal(rc.BeforeIdentity.Type()), + Importing: rc.Importing, + GeneratedConfig: rc.GeneratedConfig, }, } default: @@ -313,9 +361,13 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha Private: rc.Private, ProviderAddr: rc.ProviderAddr, Change: Change{ - Action: NoOp, - Before: rc.Before, - After: rc.Before, + Action: NoOp, + Before: rc.Before, + BeforeIdentity: rc.BeforeIdentity, + After: rc.Before, + AfterIdentity: rc.BeforeIdentity, + Importing: rc.Importing, + GeneratedConfig: rc.GeneratedConfig, }, } } @@ -328,9 +380,13 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha Private: rc.Private, ProviderAddr: rc.ProviderAddr, Change: Change{ - Action: NoOp, - Before: rc.Before, - After: rc.Before, + Action: NoOp, + Before: rc.Before, + BeforeIdentity: rc.BeforeIdentity, + After: rc.Before, + AfterIdentity: rc.BeforeIdentity, + Importing: rc.Importing, + GeneratedConfig: rc.GeneratedConfig, }, } case CreateThenDelete, DeleteThenCreate: @@ -340,9 +396,13 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha Private: rc.Private, ProviderAddr: rc.ProviderAddr, Change: Change{ - Action: Create, - Before: cty.NullVal(rc.After.Type()), - After: rc.After, + Action: Create, + Before: cty.NullVal(rc.After.Type()), + BeforeIdentity: cty.NullVal(rc.AfterIdentity.Type()), + After: rc.After, + AfterIdentity: rc.AfterIdentity, + Importing: rc.Importing, + GeneratedConfig: rc.GeneratedConfig, }, } } @@ -361,7 +421,7 @@ func (rc *ResourceInstanceChange) Simplify(destroying bool) *ResourceInstanceCha // apply step. type ResourceInstanceChangeActionReason rune -//go:generate go run golang.org/x/tools/cmd/stringer -type=ResourceInstanceChangeActionReason changes.go +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ResourceInstanceChangeActionReason changes.go const ( // In most cases there's no special reason for choosing a particular @@ -427,6 +487,12 @@ const ( // specific reasons for a particular instance to no longer be declared. ResourceInstanceDeleteBecauseNoModule ResourceInstanceChangeActionReason = 'M' + // ResourceInstanceDeleteBecauseNoMoveTarget indicates that the resource + // address appears as the target ("to") in a moved block, but no + // configuration exists for that resource. According to our move rules, + // this combination evaluates to a deletion of the "new" resource. + ResourceInstanceDeleteBecauseNoMoveTarget ResourceInstanceChangeActionReason = 'A' + // ResourceInstanceReadBecauseConfigUnknown indicates that the resource // must be read during apply (rather than during planning) because its // configuration contains unknown values. This reason applies only to @@ -438,6 +504,12 @@ const ( // depends on a managed resource instance which has its own changes // pending. ResourceInstanceReadBecauseDependencyPending ResourceInstanceChangeActionReason = '!' + + // ResourceInstanceReadBecauseCheckNested indicates that the resource must + // be read during apply (as well as during planning) because it is inside + // a check block and when the check assertions execute we want them to use + // the most up-to-date data. + ResourceInstanceReadBecauseCheckNested ResourceInstanceChangeActionReason = '#' ) // OutputChange describes a change to an output value. @@ -462,7 +534,7 @@ type OutputChange struct { // Encode produces a variant of the reciever that has its change values // serialized so it can be written to a plan file. func (oc *OutputChange) Encode() (*OutputChangeSrc, error) { - cs, err := oc.Change.Encode(cty.DynamicPseudoType) + cs, err := oc.Change.Encode(nil) if err != nil { return nil, err } @@ -473,6 +545,41 @@ func (oc *OutputChange) Encode() (*OutputChangeSrc, error) { }, err } +// Importing is the part of a ChangeSrc that describes the embedded import +// action. +// +// The fields in here are subject to change, so downstream consumers should be +// prepared for backwards compatibility in case the contents changes. +type Importing struct { + Target cty.Value +} + +// Encode converts the Importing object into a form suitable for serialization +// to a plan file. +func (i *Importing) Encode(identityTy cty.Type) *ImportingSrc { + if i == nil { + return nil + } + if i.Target.IsWhollyKnown() { + if i.Target.Type().IsObjectType() { + identity, err := NewDynamicValue(i.Target, identityTy) + if err != nil { + return nil + } + return &ImportingSrc{ + Identity: identity, + } + } else { + return &ImportingSrc{ + ID: i.Target.AsString(), + } + } + } + return &ImportingSrc{ + Unknown: true, + } +} + // Change describes a single change with a given action. type Change struct { // Action defines what kind of change is being made. @@ -488,11 +595,28 @@ type Change struct { // value after update. // Replace As with Update. // Delete Before is the value prior to delete, and After is always nil. + // Forget As with Delete. // // Unknown values may appear anywhere within the Before and After values, // either as the values themselves or as nested elements within known // collections/structures. Before, After cty.Value + + // Keeping track of how the identity of the resource has changed. + BeforeIdentity, AfterIdentity cty.Value + + // Importing is present if the resource is being imported as part of this + // change. + // + // Use the simple presence of this field to detect if a ChangeSrc is to be + // imported, the contents of this structure may be modified going forward. + Importing *Importing + + // GeneratedConfig contains any HCL config generated for this resource + // during planning, as a string. If GeneratedConfig is populated, Importing + // should be true. However, not all Importing changes contain generated + // config. + GeneratedConfig string } // Encode produces a variant of the reciever that has its change values @@ -503,34 +627,76 @@ type Change struct { // Where a Change is embedded in some other struct, it's generally better // to call the corresponding Encode method of that struct rather than working // directly with its embedded Change. -func (c *Change) Encode(ty cty.Type) (*ChangeSrc, error) { - // Storing unmarked values so that we can encode unmarked values - // and save the PathValueMarks for re-marking the values later - var beforeVM, afterVM []cty.PathValueMarks - unmarkedBefore := c.Before - unmarkedAfter := c.After - - if c.Before.ContainsMarked() { - unmarkedBefore, beforeVM = c.Before.UnmarkDeepWithPaths() +func (c *Change) Encode(schema *providers.Schema) (*ChangeSrc, error) { + // We can't serialize value marks directly so we'll need to extract the + // sensitive marks and store them in a separate field. + // + // We don't accept any other marks here. The caller should have dealt + // with those somehow and replaced them with unmarked placeholders before + // writing the value into the state. + unmarkedBefore, marksesBefore := c.Before.UnmarkDeepWithPaths() + unmarkedAfter, marksesAfter := c.After.UnmarkDeepWithPaths() + sensitiveAttrsBefore, unsupportedMarksesBefore := marks.PathsWithMark(marksesBefore, marks.Sensitive) + sensitiveAttrsAfter, unsupportedMarksesAfter := marks.PathsWithMark(marksesAfter, marks.Sensitive) + if len(unsupportedMarksesBefore) != 0 { + return nil, fmt.Errorf( + "prior value %s: can't serialize value marked with %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(unsupportedMarksesBefore[0].Path), + unsupportedMarksesBefore[0].Marks, + ) } + if len(unsupportedMarksesAfter) != 0 { + return nil, fmt.Errorf( + "new value %s: can't serialize value marked with %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(unsupportedMarksesAfter[0].Path), + unsupportedMarksesAfter[0].Marks, + ) + } + + ty := cty.DynamicPseudoType + if schema != nil { + ty = schema.Body.ImpliedType() + } + beforeDV, err := NewDynamicValue(unmarkedBefore, ty) if err != nil { return nil, err } - - if c.After.ContainsMarked() { - unmarkedAfter, afterVM = c.After.UnmarkDeepWithPaths() - } afterDV, err := NewDynamicValue(unmarkedAfter, ty) if err != nil { return nil, err } + var beforeIdentityDV DynamicValue + var afterIdentityDV DynamicValue + + identityTy := cty.DynamicPseudoType + if schema != nil { + identityTy = schema.Identity.ImpliedType() + } + + if !c.BeforeIdentity.IsNull() { + beforeIdentityDV, err = NewDynamicValue(c.BeforeIdentity, identityTy) + if err != nil { + return nil, err + } + } + + if !c.AfterIdentity.IsNull() { + afterIdentityDV, err = NewDynamicValue(c.AfterIdentity, identityTy) + if err != nil { + return nil, err + } + } return &ChangeSrc{ - Action: c.Action, - Before: beforeDV, - After: afterDV, - BeforeValMarks: beforeVM, - AfterValMarks: afterVM, + Action: c.Action, + Before: beforeDV, + After: afterDV, + BeforeSensitivePaths: sensitiveAttrsBefore, + AfterSensitivePaths: sensitiveAttrsAfter, + BeforeIdentity: beforeIdentityDV, + AfterIdentity: afterIdentityDV, + Importing: c.Importing.Encode(identityTy), + GeneratedConfig: c.GeneratedConfig, }, nil } diff --git a/internal/plans/changes_src.go b/internal/plans/changes_src.go index 3964939567..9af56cf454 100644 --- a/internal/plans/changes_src.go +++ b/internal/plans/changes_src.go @@ -1,21 +1,182 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( "fmt" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/states" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/schemarepo" + "github.com/hashicorp/terraform/internal/states" ) +// ChangesSrc describes various actions that Terraform will attempt to take if +// the corresponding plan is applied. +// +// A Changes object can be rendered into a visual diff (by the caller, using +// code in another package) for display to the user. +type ChangesSrc struct { + // Resources tracks planned changes to resource instance objects. + Resources []*ResourceInstanceChangeSrc + + // Outputs tracks planned changes output values. + // + // Note that although an in-memory plan contains planned changes for + // outputs throughout the configuration, a plan serialized + // to disk retains only the root outputs because they are + // externally-visible, while other outputs are implementation details and + // can be easily re-calculated during the apply phase. Therefore only root + // module outputs will survive a round-trip through a plan file. + Outputs []*OutputChangeSrc +} + +func NewChangesSrc() *ChangesSrc { + return &ChangesSrc{} +} + +func (c *ChangesSrc) Empty() bool { + for _, res := range c.Resources { + if res.Action != NoOp || res.Moved() { + return false + } + + if res.Importing != nil { + return false + } + } + + for _, out := range c.Outputs { + if out.Addr.Module.IsRoot() && out.Action != NoOp { + return false + } + } + + return true +} + +// ResourceInstance returns the planned change for the current object of the +// resource instance of the given address, if any. Returns nil if no change is +// planned. +func (c *ChangesSrc) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstanceChangeSrc { + for _, rc := range c.Resources { + if rc.Addr.Equal(addr) && rc.DeposedKey == states.NotDeposed { + return rc + } + } + + return nil +} + +// ResourceInstanceDeposed returns the plan change of a deposed object of +// the resource instance of the given address, if any. Returns nil if no change +// is planned. +func (c *ChangesSrc) ResourceInstanceDeposed(addr addrs.AbsResourceInstance, key states.DeposedKey) *ResourceInstanceChangeSrc { + for _, rc := range c.Resources { + if rc.Addr.Equal(addr) && rc.DeposedKey == key { + return rc + } + } + + return nil +} + +// OutputValue returns the planned change for the output value with the +// +// given address, if any. Returns nil if no change is planned. +func (c *ChangesSrc) OutputValue(addr addrs.AbsOutputValue) *OutputChangeSrc { + for _, oc := range c.Outputs { + if oc.Addr.Equal(addr) { + return oc + } + } + + return nil +} + +// Decode decodes all the stored resource and output changes into a new *Changes value. +func (c *ChangesSrc) Decode(schemas *schemarepo.Schemas) (*Changes, error) { + changes := NewChanges() + + for _, rcs := range c.Resources { + p, ok := schemas.Providers[rcs.ProviderAddr.Provider] + if !ok { + return nil, fmt.Errorf("ChangesSrc.Decode: missing provider %s for %s", rcs.ProviderAddr, rcs.Addr) + } + + var schema providers.Schema + switch rcs.Addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + schema = p.ResourceTypes[rcs.Addr.Resource.Resource.Type] + case addrs.DataResourceMode: + schema = p.DataSources[rcs.Addr.Resource.Resource.Type] + default: + panic(fmt.Sprintf("unexpected resource mode %s", rcs.Addr.Resource.Resource.Mode)) + } + + if schema.Body == nil { + return nil, fmt.Errorf("ChangesSrc.Decode: missing schema for %s", rcs.Addr) + } + + rc, err := rcs.Decode(schema) + if err != nil { + return nil, err + } + + rc.Before = marks.MarkPaths(rc.Before, marks.Sensitive, rcs.BeforeSensitivePaths) + rc.After = marks.MarkPaths(rc.After, marks.Sensitive, rcs.AfterSensitivePaths) + + changes.Resources = append(changes.Resources, rc) + } + + for _, ocs := range c.Outputs { + oc, err := ocs.Decode() + if err != nil { + return nil, err + } + changes.Outputs = append(changes.Outputs, oc) + } + return changes, nil +} + +// AppendResourceInstanceChange records the given resource instance change in +// the set of planned resource changes. +func (c *ChangesSrc) AppendResourceInstanceChange(change *ResourceInstanceChangeSrc) { + if c == nil { + panic("AppendResourceInstanceChange on nil ChangesSync") + } + + s := change.DeepCopy() + c.Resources = append(c.Resources, s) +} + // ResourceInstanceChangeSrc is a not-yet-decoded ResourceInstanceChange. // Pass the associated resource type's schema type to method Decode to // obtain a ResourceInstanceChange. type ResourceInstanceChangeSrc struct { // Addr is the absolute address of the resource instance that the change // will apply to. + // + // THIS IS NOT A SUFFICIENT UNIQUE IDENTIFIER! It doesn't consider the + // fact that multiple objects for the same resource instance might be + // present in the same plan; use the ObjectAddr method instead if you + // need a unique address for a particular change. Addr addrs.AbsResourceInstance + // DeposedKey is the identifier for a deposed object associated with the + // given instance, or states.NotDeposed if this change applies to the + // current object. + // + // A Replace change for a resource with create_before_destroy set will + // create a new DeposedKey temporarily during replacement. In that case, + // DeposedKey in the plan is always states.NotDeposed, representing that + // the current object is being replaced with the deposed. + DeposedKey states.DeposedKey + // PrevRunAddr is the absolute address that this resource instance had at // the conclusion of a previous run. // @@ -29,16 +190,6 @@ type ResourceInstanceChangeSrc struct { // aims to detect and react to the movement of instances between addresses. PrevRunAddr addrs.AbsResourceInstance - // DeposedKey is the identifier for a deposed object associated with the - // given instance, or states.NotDeposed if this change applies to the - // current object. - // - // A Replace change for a resource with create_before_destroy set will - // create a new DeposedKey temporarily during replacement. In that case, - // DeposedKey in the plan is always states.NotDeposed, representing that - // the current object is being replaced with the deposed. - DeposedKey states.DeposedKey - // Provider is the address of the provider configuration that was used // to plan this change, and thus the configuration that must also be // used to apply it. @@ -71,11 +222,18 @@ type ResourceInstanceChangeSrc struct { Private []byte } +func (rcs *ResourceInstanceChangeSrc) ObjectAddr() addrs.AbsResourceInstanceObject { + return addrs.AbsResourceInstanceObject{ + ResourceInstance: rcs.Addr, + DeposedKey: rcs.DeposedKey, + } +} + // Decode unmarshals the raw representation of the instance object being // changed. Pass the implied type of the corresponding resource type schema // for correct operation. -func (rcs *ResourceInstanceChangeSrc) Decode(ty cty.Type) (*ResourceInstanceChange, error) { - change, err := rcs.ChangeSrc.Decode(ty) +func (rcs *ResourceInstanceChangeSrc) Decode(schema providers.Schema) (*ResourceInstanceChange, error) { + change, err := rcs.ChangeSrc.Decode(&schema) if err != nil { return nil, err } @@ -151,7 +309,7 @@ type OutputChangeSrc struct { // Decode unmarshals the raw representation of the output value being // changed. func (ocs *OutputChangeSrc) Decode() (*OutputChange, error) { - change, err := ocs.ChangeSrc.Decode(cty.DynamicPseudoType) + change, err := ocs.ChangeSrc.Decode(nil) if err != nil { return nil, err } @@ -182,6 +340,57 @@ func (ocs *OutputChangeSrc) DeepCopy() *OutputChangeSrc { return &ret } +// ImportingSrc is the part of a ChangeSrc that describes the embedded import +// action. +// +// The fields in here are subject to change, so downstream consumers should be +// prepared for backwards compatibility in case the contents changes. +type ImportingSrc struct { + // ID is the original ID of the imported resource. + ID string + + // Identity is the original identity of the imported resource. + Identity DynamicValue + + // Unknown is true if the ID was unknown when we tried to import it. This + // should only be true if the overall change is embedded within a deferred + // action. + Unknown bool +} + +// Decode unmarshals the raw representation of the importing action. +func (is *ImportingSrc) Decode(identityType cty.Type) *Importing { + if is == nil { + return nil + } + if is.Unknown { + if is.Identity == nil { + return &Importing{ + Target: cty.UnknownVal(cty.String), + } + } + + return &Importing{ + Target: cty.UnknownVal(cty.EmptyObject), + } + } + + if is.Identity == nil { + return &Importing{ + Target: cty.StringVal(is.ID), + } + } + + target, err := is.Identity.Decode(identityType) + if err != nil { + return &Importing{ + Target: target, + } + } + + return nil +} + // ChangeSrc is a not-yet-decoded Change. type ChangeSrc struct { // Action defines what kind of change is being made. @@ -192,12 +401,31 @@ type ChangeSrc struct { // storage. Before, After DynamicValue - // BeforeValMarks and AfterValMarks are stored path+mark combinations - // that might be discovered when encoding a change. Marks are removed - // to enable encoding (marked values cannot be marshalled), and so storing - // the path+mark combinations allow us to re-mark the value later - // when, for example, displaying the diff to the UI. - BeforeValMarks, AfterValMarks []cty.PathValueMarks + // BeforeIdentity and AfterIdentity correspond to the fields of the same name in Change, + // but have not yet been decoded from the serialized value used for + // storage. + BeforeIdentity, AfterIdentity DynamicValue + + // BeforeSensitivePaths and AfterSensitivePaths are the paths for any + // values in Before or After (respectively) that are considered to be + // sensitive. The sensitive marks are removed from the in-memory values + // to enable encoding (marked values cannot be marshalled), and so we + // store the sensitive paths to allow re-marking later when we decode + // the serialized change. + BeforeSensitivePaths, AfterSensitivePaths []cty.Path + + // Importing is present if the resource is being imported as part of this + // change. + // + // Use the simple presence of this field to detect if a ChangeSrc is to be + // imported, the contents of this structure may be modified going forward. + Importing *ImportingSrc + + // GeneratedConfig contains any HCL config generated for this resource + // during planning, as a string. If GeneratedConfig is populated, Importing + // should be true. However, not all Importing changes contain generated + // config. + GeneratedConfig string } // Decode unmarshals the raw representations of the before and after values @@ -207,10 +435,20 @@ type ChangeSrc struct { // Where a ChangeSrc is embedded in some other struct, it's generally better // to call the corresponding Decode method of that struct rather than working // directly with its embedded Change. -func (cs *ChangeSrc) Decode(ty cty.Type) (*Change, error) { +func (cs *ChangeSrc) Decode(schema *providers.Schema) (*Change, error) { var err error + + ty := cty.DynamicPseudoType + identityType := cty.DynamicPseudoType + if schema != nil { + ty = schema.Body.ImpliedType() + identityType = schema.Identity.ImpliedType() + } + before := cty.NullVal(ty) + beforeIdentity := cty.NullVal(identityType) after := cty.NullVal(ty) + afterIdentity := cty.NullVal(identityType) if len(cs.Before) > 0 { before, err = cs.Before.Decode(ty) @@ -218,16 +456,32 @@ func (cs *ChangeSrc) Decode(ty cty.Type) (*Change, error) { return nil, fmt.Errorf("error decoding 'before' value: %s", err) } } + if len(cs.BeforeIdentity) > 0 { + beforeIdentity, err = cs.BeforeIdentity.Decode(identityType) + if err != nil { + return nil, fmt.Errorf("error decoding 'beforeIdentity' value: %s", err) + } + } if len(cs.After) > 0 { after, err = cs.After.Decode(ty) if err != nil { return nil, fmt.Errorf("error decoding 'after' value: %s", err) } } + if len(cs.AfterIdentity) > 0 { + afterIdentity, err = cs.AfterIdentity.Decode(identityType) + if err != nil { + return nil, fmt.Errorf("error decoding 'afterIdentity' value: %s", err) + } + } return &Change{ - Action: cs.Action, - Before: before.MarkWithPaths(cs.BeforeValMarks), - After: after.MarkWithPaths(cs.AfterValMarks), + Action: cs.Action, + Before: marks.MarkPaths(before, marks.Sensitive, cs.BeforeSensitivePaths), + BeforeIdentity: beforeIdentity, + After: marks.MarkPaths(after, marks.Sensitive, cs.AfterSensitivePaths), + AfterIdentity: afterIdentity, + Importing: cs.Importing.Decode(identityType), + GeneratedConfig: cs.GeneratedConfig, }, nil } diff --git a/internal/plans/changes_state.go b/internal/plans/changes_state.go index 8446e9be66..2ad6df3e3e 100644 --- a/internal/plans/changes_state.go +++ b/internal/plans/changes_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( diff --git a/internal/plans/changes_sync.go b/internal/plans/changes_sync.go index 95920c1c6b..fe2197224c 100644 --- a/internal/plans/changes_sync.go +++ b/internal/plans/changes_sync.go @@ -1,11 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( - "fmt" "sync" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/states" ) // ChangesSync is a wrapper around a Changes that provides a concurrency-safe @@ -21,63 +22,44 @@ type ChangesSync struct { changes *Changes } -// IsFullDestroy returns true if the set of changes indicates we are doing a -// destroy of all resources. -func (cs *ChangesSync) IsFullDestroy() bool { - if cs == nil { - panic("FullDestroy on nil ChangesSync") - } - cs.lock.Lock() - defer cs.lock.Unlock() - - for _, c := range cs.changes.Resources { - if c.Action != Delete { - return false - } - } - - return true -} - // AppendResourceInstanceChange records the given resource instance change in // the set of planned resource changes. // // The caller must ensure that there are no concurrent writes to the given // change while this method is running, but it is safe to resume mutating // it after this method returns without affecting the saved change. -func (cs *ChangesSync) AppendResourceInstanceChange(changeSrc *ResourceInstanceChangeSrc) { +func (cs *ChangesSync) AppendResourceInstanceChange(change *ResourceInstanceChange) { if cs == nil { panic("AppendResourceInstanceChange on nil ChangesSync") } cs.lock.Lock() defer cs.lock.Unlock() - s := changeSrc.DeepCopy() + s := change.DeepCopy() cs.changes.Resources = append(cs.changes.Resources, s) } // GetResourceInstanceChange searches the set of resource instance changes for -// one matching the given address and generation, returning it if it exists. +// one matching the given address and deposed key, returning it if it exists. +// Use [addrs.NotDeposed] as the deposed key to represent the "current" +// object for the given resource instance. // // If no such change exists, nil is returned. // // The returned object is a deep copy of the change recorded in the plan, so // callers may mutate it although it's generally better (less confusing) to // treat planned changes as immutable after they've been initially constructed. -func (cs *ChangesSync) GetResourceInstanceChange(addr addrs.AbsResourceInstance, gen states.Generation) *ResourceInstanceChangeSrc { +func (cs *ChangesSync) GetResourceInstanceChange(addr addrs.AbsResourceInstance, dk addrs.DeposedKey) *ResourceInstanceChange { if cs == nil { panic("GetResourceInstanceChange on nil ChangesSync") } cs.lock.Lock() defer cs.lock.Unlock() - if gen == states.CurrentGen { + if dk == addrs.NotDeposed { return cs.changes.ResourceInstance(addr).DeepCopy() } - if dk, ok := gen.(states.DeposedKey); ok { - return cs.changes.ResourceInstanceDeposed(addr, dk).DeepCopy() - } - panic(fmt.Sprintf("unsupported generation value %#v", gen)) + return cs.changes.ResourceInstanceDeposed(addr, dk).DeepCopy() } // GetChangesForConfigResource searches the set of resource instance @@ -90,13 +72,13 @@ func (cs *ChangesSync) GetResourceInstanceChange(addr addrs.AbsResourceInstance, // The returned objects are a deep copy of the change recorded in the plan, so // callers may mutate them although it's generally better (less confusing) to // treat planned changes as immutable after they've been initially constructed. -func (cs *ChangesSync) GetChangesForConfigResource(addr addrs.ConfigResource) []*ResourceInstanceChangeSrc { +func (cs *ChangesSync) GetChangesForConfigResource(addr addrs.ConfigResource) []*ResourceInstanceChange { if cs == nil { panic("GetChangesForConfigResource on nil ChangesSync") } cs.lock.Lock() defer cs.lock.Unlock() - var changes []*ResourceInstanceChangeSrc + var changes []*ResourceInstanceChange for _, c := range cs.changes.InstancesForConfigResource(addr) { changes = append(changes, c.DeepCopy()) } @@ -111,13 +93,13 @@ func (cs *ChangesSync) GetChangesForConfigResource(addr addrs.ConfigResource) [] // The returned objects are a deep copy of the change recorded in the plan, so // callers may mutate them although it's generally better (less confusing) to // treat planned changes as immutable after they've been initially constructed. -func (cs *ChangesSync) GetChangesForAbsResource(addr addrs.AbsResource) []*ResourceInstanceChangeSrc { +func (cs *ChangesSync) GetChangesForAbsResource(addr addrs.AbsResource) []*ResourceInstanceChange { if cs == nil { panic("GetChangesForAbsResource on nil ChangesSync") } cs.lock.Lock() defer cs.lock.Unlock() - var changes []*ResourceInstanceChangeSrc + var changes []*ResourceInstanceChange for _, c := range cs.changes.InstancesForAbsResource(addr) { changes = append(changes, c.DeepCopy()) } @@ -125,20 +107,15 @@ func (cs *ChangesSync) GetChangesForAbsResource(addr addrs.AbsResource) []*Resou } // RemoveResourceInstanceChange searches the set of resource instance changes -// for one matching the given address and generation, and removes it from the +// for one matching the given address and deposed key, and removes it from the // set if it exists. -func (cs *ChangesSync) RemoveResourceInstanceChange(addr addrs.AbsResourceInstance, gen states.Generation) { +func (cs *ChangesSync) RemoveResourceInstanceChange(addr addrs.AbsResourceInstance, dk addrs.DeposedKey) { if cs == nil { panic("RemoveResourceInstanceChange on nil ChangesSync") } cs.lock.Lock() defer cs.lock.Unlock() - dk := states.NotDeposed - if realDK, ok := gen.(states.DeposedKey); ok { - dk = realDK - } - addrStr := addr.String() for i, r := range cs.changes.Resources { if r.Addr.String() != addrStr || r.DeposedKey != dk { @@ -156,15 +133,14 @@ func (cs *ChangesSync) RemoveResourceInstanceChange(addr addrs.AbsResourceInstan // The caller must ensure that there are no concurrent writes to the given // change while this method is running, but it is safe to resume mutating // it after this method returns without affecting the saved change. -func (cs *ChangesSync) AppendOutputChange(changeSrc *OutputChangeSrc) { +func (cs *ChangesSync) AppendOutputChange(changeSrc *OutputChange) { if cs == nil { panic("AppendOutputChange on nil ChangesSync") } cs.lock.Lock() defer cs.lock.Unlock() - s := changeSrc.DeepCopy() - cs.changes.Outputs = append(cs.changes.Outputs, s) + cs.changes.Outputs = append(cs.changes.Outputs, changeSrc) } // GetOutputChange searches the set of output value changes for one matching @@ -175,7 +151,7 @@ func (cs *ChangesSync) AppendOutputChange(changeSrc *OutputChangeSrc) { // The returned object is a deep copy of the change recorded in the plan, so // callers may mutate it although it's generally better (less confusing) to // treat planned changes as immutable after they've been initially constructed. -func (cs *ChangesSync) GetOutputChange(addr addrs.AbsOutputValue) *OutputChangeSrc { +func (cs *ChangesSync) GetOutputChange(addr addrs.AbsOutputValue) *OutputChange { if cs == nil { panic("GetOutputChange on nil ChangesSync") } @@ -191,7 +167,7 @@ func (cs *ChangesSync) GetOutputChange(addr addrs.AbsOutputValue) *OutputChangeS // The returned objects are a deep copy of the change recorded in the plan, so // callers may mutate them although it's generally better (less confusing) to // treat planned changes as immutable after they've been initially constructed. -func (cs *ChangesSync) GetRootOutputChanges() []*OutputChangeSrc { +func (cs *ChangesSync) GetRootOutputChanges() []*OutputChange { if cs == nil { panic("GetRootOutputChanges on nil ChangesSync") } @@ -208,7 +184,7 @@ func (cs *ChangesSync) GetRootOutputChanges() []*OutputChangeSrc { // The returned objects are a deep copy of the change recorded in the plan, so // callers may mutate them although it's generally better (less confusing) to // treat planned changes as immutable after they've been initially constructed. -func (cs *ChangesSync) GetOutputChanges(parent addrs.ModuleInstance, module addrs.ModuleCall) []*OutputChangeSrc { +func (cs *ChangesSync) GetOutputChanges(parent addrs.ModuleInstance, module addrs.ModuleCall) []*OutputChange { if cs == nil { panic("GetOutputChange on nil ChangesSync") } diff --git a/internal/plans/changes_test.go b/internal/plans/changes_test.go index 5dbe10f08a..b20fd4f045 100644 --- a/internal/plans/changes_test.go +++ b/internal/plans/changes_test.go @@ -1,159 +1,114 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( "fmt" "testing" - "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" ) -func TestChangesEmpty(t *testing.T) { - testCases := map[string]struct { - changes *Changes - want bool - }{ - "no changes": { - &Changes{}, - true, - }, - "resource change": { - &Changes{ - Resources: []*ResourceInstanceChangeSrc{ - { - Addr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - PrevRunAddr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - ChangeSrc: ChangeSrc{ - Action: Update, - }, - }, - }, - }, - false, - }, - "resource change with no-op action": { - &Changes{ - Resources: []*ResourceInstanceChangeSrc{ - { - Addr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - PrevRunAddr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - ChangeSrc: ChangeSrc{ - Action: NoOp, - }, - }, - }, - }, - true, - }, - "resource moved with no-op change": { - &Changes{ - Resources: []*ResourceInstanceChangeSrc{ - { - Addr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "woot", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - PrevRunAddr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "toot", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - ChangeSrc: ChangeSrc{ - Action: NoOp, - }, - }, - }, - }, - false, - }, - "output change": { - &Changes{ - Outputs: []*OutputChangeSrc{ - { - Addr: addrs.OutputValue{ - Name: "result", - }.Absolute(addrs.RootModuleInstance), - ChangeSrc: ChangeSrc{ - Action: Update, - }, - }, - }, - }, - false, - }, - "output change no-op": { - &Changes{ - Outputs: []*OutputChangeSrc{ - { - Addr: addrs.OutputValue{ - Name: "result", - }.Absolute(addrs.RootModuleInstance), - ChangeSrc: ChangeSrc{ - Action: NoOp, - }, - }, - }, - }, - true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - if got, want := tc.changes.Empty(), tc.want; got != want { - t.Fatalf("unexpected result: got %v, want %v", got, want) - } - }) - } -} - func TestChangeEncodeSensitive(t *testing.T) { - testVals := []cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "ding": cty.StringVal("dong").Mark(marks.Sensitive), - }), - cty.StringVal("bleep").Mark("bloop"), - cty.ListVal([]cty.Value{cty.UnknownVal(cty.String).Mark("sup?")}), + testVals := []struct { + val cty.Value + schema providers.Schema + }{ + { + val: cty.ObjectVal(map[string]cty.Value{ + "ding": cty.StringVal("dong").Mark(marks.Sensitive), + }), + schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ding": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + { + val: cty.ObjectVal(map[string]cty.Value{ + "ding": cty.StringVal("bleep").Mark(marks.Sensitive), + }), + schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ding": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + { + val: cty.ObjectVal(map[string]cty.Value{ + "ding": cty.ListVal([]cty.Value{cty.UnknownVal(cty.String).Mark(marks.Sensitive)}), + }), + schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ding": { + Type: cty.List(cty.String), + Required: false, + }, + }, + }, + }, + }, } for _, v := range testVals { t.Run(fmt.Sprintf("%#v", v), func(t *testing.T) { change := Change{ - Before: cty.NullVal(v.Type()), - After: v, + Before: cty.NullVal(v.val.Type()), + After: v.val, } - encoded, err := change.Encode(v.Type()) + encoded, err := change.Encode(&v.schema) if err != nil { t.Fatal(err) } - decoded, err := encoded.Decode(v.Type()) + decoded, err := encoded.Decode(&v.schema) if err != nil { t.Fatal(err) } - if !v.RawEquals(decoded.After) { + if !v.val.RawEquals(decoded.After) { t.Fatalf("%#v != %#v\n", decoded.After, v) } }) } } + +// make sure we get a valid value back even when faced with an error +func TestChangeEncodeError(t *testing.T) { + changes := &Changes{ + Outputs: []*OutputChange{ + { + // Missing Addr + Change: Change{ + Before: cty.NullVal(cty.DynamicPseudoType), + // can't encode a marked value + After: cty.StringVal("test").Mark("shoult not be here"), + }, + }, + }, + } + // no resources so we can get by with no schemas + changesSrc, err := changes.Encode(nil) + if err == nil { + t.Fatal("expected error") + } + if changesSrc == nil { + t.Fatal("changesSrc should not be nil") + } +} diff --git a/internal/plans/deferring.go b/internal/plans/deferring.go new file mode 100644 index 0000000000..826a4ff362 --- /dev/null +++ b/internal/plans/deferring.go @@ -0,0 +1,54 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package plans + +import ( + "github.com/hashicorp/terraform/internal/providers" +) + +// DeferredResourceInstanceChangeSrc tracks information about a resource that +// has been deferred for some reason. +type DeferredResourceInstanceChangeSrc struct { + // DeferredReason is the reason why this resource instance was deferred. + DeferredReason providers.DeferredReason + + // ChangeSrc contains any information we have about the deferred change. + // This could be incomplete so must be parsed with care. + ChangeSrc *ResourceInstanceChangeSrc +} + +func (rcs *DeferredResourceInstanceChangeSrc) Decode(schema providers.Schema) (*DeferredResourceInstanceChange, error) { + change, err := rcs.ChangeSrc.Decode(schema) + if err != nil { + return nil, err + } + + return &DeferredResourceInstanceChange{ + DeferredReason: rcs.DeferredReason, + Change: change, + }, nil +} + +// DeferredResourceInstanceChange tracks information about a resource that +// has been deferred for some reason. +type DeferredResourceInstanceChange struct { + // DeferredReason is the reason why this resource instance was deferred. + DeferredReason providers.DeferredReason + + // Change contains any information we have about the deferred change. This + // could be incomplete so must be parsed with care. + Change *ResourceInstanceChange +} + +func (rcs *DeferredResourceInstanceChange) Encode(schema providers.Schema) (*DeferredResourceInstanceChangeSrc, error) { + change, err := rcs.Change.Encode(schema) + if err != nil { + return nil, err + } + + return &DeferredResourceInstanceChangeSrc{ + DeferredReason: rcs.DeferredReason, + ChangeSrc: change, + }, nil +} diff --git a/internal/plans/deferring/deferred.go b/internal/plans/deferring/deferred.go new file mode 100644 index 0000000000..d6f9e82eb2 --- /dev/null +++ b/internal/plans/deferring/deferred.go @@ -0,0 +1,633 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package deferring + +import ( + "fmt" + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Deferred keeps track of deferrals that have already happened, to help +// guide decisions about whether downstream operations might also need to be +// deferred. +type Deferred struct { + + // deferralAllowed marks whether deferred actions are supported by the + // current runtime. At time of writing, the modules runtime does not support + // deferral, but the stacks runtime does. + deferralAllowed bool + + // externalDependencyDeferred marks the special situation where the + // subsystem that's calling the modules runtime knows that some external + // dependency of the configuration has deferred changes itself, and thus + // all planned actions in this configuration must be deferred even if + // the modules runtime can't find its own reason to do that. + // + // This is used by the stacks runtime when component B depends on + // component A and component A's plan had deferred changes, so therefore + // everything that component B might plan must also be deferred even + // though the planning process for B cannot see into the plan for A. + externalDependencyDeferred bool + + // Must hold this lock when accessing all fields after this one. + mu sync.Mutex + + // dataSourceInstancesDeferred tracks the data source instances that have + // been deferred despite their full addresses being known. This can happen + // either because an upstream change was already deferred, or because + // during planning the owning provider indicated that it doesn't yet have + // enough information to produce a plan. + // + // These are grouped by the static resource configuration address because + // there can potentially be various different deferrals for the same + // configuration block at different amounts of instance expansion under + // different prefixes, and so some queries require us to search across + // all of those options to decide if each instance is relevant. + dataSourceInstancesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]] + + // resourceInstancesDeferred tracks the resource instances that have + // been deferred despite their full addresses being known. This can happen + // either because an upstream change was already deferred, or because + // during planning the owning provider indicated that it doesn't yet have + // enough information to produce a plan. + // + // These are grouped by the static resource configuration address because + // there can potentially be various different deferrals for the same + // configuration block at different amounts of instance expansion under + // different prefixes, and so some queries require us to search across + // all of those options to decide if each instance is relevant. + resourceInstancesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]] + + // ephemeralResourceInstancesDeferred tracks the ephemeral resource instances + // that have been deferred despite their full addresses being known. This can happen + // either because an upstream change was already deferred, or because + // during planning the owning provider indicated that it doesn't yet have + // enough information to produce a plan. + // + // These are grouped by the static resource configuration address because + // there can potentially be various different deferrals for the same + // configuration block at different amounts of instance expansion under + // different prefixes, and so some queries require us to search across + // all of those options to decide if each instance is relevant. + ephemeralResourceInstancesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]] + + // partialExpandedResourcesDeferred tracks placeholders that cover an + // unbounded set of potential resource instances in situations where we + // don't yet even have enough information to predict which instances of + // a resource will exist. + // + // These are grouped by the static resource configuration address because + // there can potentially be various different deferrals for the same + // configuration block at different amounts of instance expansion under + // different prefixes, and so some queries require us to search across + // all of those options to find the one that matches most closely. + partialExpandedResourcesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]] + + // partialExpandedDataSourcesDeferred tracks placeholders that cover an + // unbounded set of potential data sources in situations where we don't yet + // even have enough information to predict which instances of a data source + // will exist. + // + // Data sources are never written into the plan, even when deferred, so we + // are tracking these for purely internal reasons. If a resource depends on + // a deferred data source, then that resource should be deferred as well. + partialExpandedDataSourcesDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]] + + // partialExpandedEphemeralResourceDeferred tracks placeholders that cover an + // unbounded set of potential data sources in situations where we don't yet + // even have enough information to predict which instances of a data source + // will exist. + // + // Data sources are never written into the plan, even when deferred, so we + // are tracking these for purely internal reasons. If a resource depends on + // a deferred data source, then that resource should be deferred as well. + partialExpandedEphemeralResourceDeferred addrs.Map[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]] + + // partialExpandedModulesDeferred tracks all of the partial-expanded module + // prefixes we were notified about. + // + // We don't need to track anything for these other than that we saw them + // reported, because the relevant data is tracked in [instances.Expander] + // and [namedvals.State], but we do need to remember the addresses just + // so that we can inform the caller that there was something deferred + // even if there weren't any resources beneath the partial-expanded prefix. + // + // (If we didn't catch that then we'd mislead the caller into thinking + // we fully-evaluated everything, which would be incorrect if any of the + // root module output values are derived from the results of the + // partial-expanded calls.) + partialExpandedModulesDeferred addrs.Set[addrs.PartialExpandedModule] +} + +// NewDeferred constructs a new empty [Deferred] object. The enabled argument +// controls whether the receiver will actually track any deferrals. If false, +// all methods will return false and no deferrals will be recorded. +func NewDeferred(enabled bool) *Deferred { + return &Deferred{ + deferralAllowed: enabled, + resourceInstancesDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]](), + ephemeralResourceInstancesDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]](), + dataSourceInstancesDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]](), + partialExpandedResourcesDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](), + partialExpandedDataSourcesDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](), + partialExpandedEphemeralResourceDeferred: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]](), + partialExpandedModulesDeferred: addrs.MakeSet[addrs.PartialExpandedModule](), + } +} + +// GetDeferredChanges returns a slice of all the deferred changes that have +// been reported to the receiver. +func (d *Deferred) GetDeferredChanges() []*plans.DeferredResourceInstanceChange { + var changes []*plans.DeferredResourceInstanceChange + + if !d.deferralAllowed { + return changes + } + + for _, configMapElem := range d.resourceInstancesDeferred.Elems { + for _, changeElem := range configMapElem.Value.Elems { + changes = append(changes, changeElem.Value) + } + } + for _, configMapElem := range d.dataSourceInstancesDeferred.Elems { + for _, changeElem := range configMapElem.Value.Elems { + changes = append(changes, changeElem.Value) + } + } + for _, configMapElem := range d.partialExpandedResourcesDeferred.Elems { + for _, changeElem := range configMapElem.Value.Elems { + changes = append(changes, changeElem.Value) + } + } + for _, configMapElem := range d.partialExpandedDataSourcesDeferred.Elems { + for _, changeElem := range configMapElem.Value.Elems { + changes = append(changes, changeElem.Value) + } + } + return changes +} + +// SetExternalDependencyDeferred modifies a freshly-constructed [Deferred] +// so that it will consider all resource instances as needing their actions +// deferred, even if there's no other reason to do that. +// +// This must be called zero or one times before any other use of the receiver. +// Changing this setting after a [Deferred] has already been used, or +// concurrently with any other method call, will cause inconsistent and +// undefined behavior. +func (d *Deferred) SetExternalDependencyDeferred() { + d.externalDependencyDeferred = true +} + +// DeferralAllowed checks whether deferred actions are supported by the current +// runtime. +func (d *Deferred) DeferralAllowed() bool { + // Gracefully recover from being called on nil, for tests that use + // MockEvalContext without a real Deferred pointer set up. + if d == nil { + return false + } + return d.deferralAllowed +} + +// HaveAnyDeferrals returns true if at least one deferral has been registered +// with the receiver. +// +// This method is intended as a summary result to propagate to the modules +// runtime caller so it can know if it should treat any downstream objects +// as having their own changes deferred without having to duplicate the +// modules runtime's rules for what counts as a deferral. +func (d *Deferred) HaveAnyDeferrals() bool { + return d.deferralAllowed && + (d.externalDependencyDeferred || + d.resourceInstancesDeferred.Len() != 0 || + d.dataSourceInstancesDeferred.Len() != 0 || + d.ephemeralResourceInstancesDeferred.Len() != 0 || + d.partialExpandedResourcesDeferred.Len() != 0 || + d.partialExpandedDataSourcesDeferred.Len() != 0 || + d.partialExpandedEphemeralResourceDeferred.Len() != 0 || + len(d.partialExpandedModulesDeferred) != 0) +} + +// GetDeferredResourceInstanceValue returns the deferred value for the given +// resource instance, if any. +func (d *Deferred) GetDeferredResourceInstanceValue(addr addrs.AbsResourceInstance) (cty.Value, bool) { + if !d.deferralAllowed { + return cty.NilVal, false + } + + d.mu.Lock() + defer d.mu.Unlock() + + configAddr := addr.ConfigResource() + var instancesMap addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]] + + switch addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + instancesMap = d.resourceInstancesDeferred + case addrs.DataResourceMode: + instancesMap = d.dataSourceInstancesDeferred + case addrs.EphemeralResourceMode: + instancesMap = d.ephemeralResourceInstancesDeferred + default: + panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource.Resource.Mode, addr)) + } + + change, ok := instancesMap.Get(configAddr).GetOk(addr) + if !ok { + return cty.NilVal, false + } + + return change.Change.After, true +} + +// GetDeferredResourceInstances returns a map of all the deferred instances of +// the given resource. +func (d *Deferred) GetDeferredResourceInstances(addr addrs.AbsResource) map[addrs.InstanceKey]cty.Value { + if !d.deferralAllowed { + return nil + } + + d.mu.Lock() + defer d.mu.Unlock() + + configAddr := addr.Config() + var instancesMap addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]] + + switch addr.Resource.Mode { + case addrs.ManagedResourceMode: + instancesMap = d.resourceInstancesDeferred + case addrs.DataResourceMode: + instancesMap = d.dataSourceInstancesDeferred + case addrs.EphemeralResourceMode: + instancesMap = d.ephemeralResourceInstancesDeferred + + default: + panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource.Mode, addr)) + } + + instances, ok := instancesMap.GetOk(configAddr) + if !ok { + return nil + } + + result := make(map[addrs.InstanceKey]cty.Value) + for _, elem := range instances.Elems { + instanceAddr := elem.Key + change := elem.Value + + if addr.Resource.Mode == addrs.EphemeralResourceMode { + // Deferred ephemeral resources always have an unknown value. + result[instanceAddr.Resource.Key] = cty.UnknownVal(cty.DynamicPseudoType).Mark(marks.Ephemeral) + continue + } + // instances contains all the resources identified by the config address + // regardless of the instances of the module they might be in. We need + // to filter out the instances that are not part of the module we are + // interested in. + if addr.Equal(instanceAddr.ContainingResource()) { + result[instanceAddr.Resource.Key] = change.Change.After + } + } + return result +} + +// ShouldDeferResourceInstanceChanges returns true if the receiver knows some +// reason why the resource instance with the given address should have its +// planned action deferred for a future plan/apply round. +// +// This method is specifically for resource instances whose full address is +// known and thus it would be possible in principle to plan changes, but we +// still need to respect dependency ordering and so any planned changes must +// be deferred if any upstream planned action was already deferred for +// some reason. +// +// Callers who get the answer true should announce an approximation of the +// action they would have planned to [Deferred.ReportResourceInstanceDeferred], +// but should skip writing that change into the live plan so that downstream +// evaluation will be based on the prior state (similar to in a refresh-only +// plan) rather than the result of the deferred action. +// +// It's invalid to call this method for an address that was already reported +// as deferred using [Deferred.ReportResourceInstanceDeferred], and so this +// method will panic in that case. +func (d *Deferred) ShouldDeferResourceInstanceChanges(addr addrs.AbsResourceInstance, deps []addrs.ConfigResource) bool { + if !d.deferralAllowed { + return false + } + configAddr := addr.ConfigResource() + + // Since d.DependenciesDeferred will also acquire the lock we don't use + // the normal defer d.mu.Unlock() but handle it manually. + d.mu.Lock() + if d.resourceInstancesDeferred.Get(configAddr).Has(addr) || d.dataSourceInstancesDeferred.Get(configAddr).Has(addr) || d.ephemeralResourceInstancesDeferred.Get(configAddr).Has(addr) { + d.mu.Unlock() + // Asking for whether a resource instance should be deferred when + // it was already reported as deferred suggests a programming error + // in the caller, because the test for whether a change should be + // deferred should always come before reporting that it has been. + panic(fmt.Sprintf("checking whether %s should be deferred when it was already deferred", addr)) + } + d.mu.Unlock() + + return d.DependenciesDeferred(deps) +} + +// DependenciesDeferred returns true if any of the given configuration +// resources have had their planned actions deferred, either because they +// themselves were deferred or because they depend on something that was +// deferred. +// +// As +func (d *Deferred) DependenciesDeferred(deps []addrs.ConfigResource) bool { + if !d.deferralAllowed { + return false + } + + d.mu.Lock() + defer d.mu.Unlock() + + if d.externalDependencyDeferred { + return true + } + + // If neither of our resource-deferral-tracking collections have anything + // in them then we definitely don't need to defer. This special case is + // here primarily to minimize the amount of code from here that will run + // when the deferred-actions-related experiments are inactive, so we can + // minimize the risk of impacting non-participants. + // (Maybe we'll remove this check once this stuff is non-experimental.) + if d.resourceInstancesDeferred.Len() == 0 && + d.dataSourceInstancesDeferred.Len() == 0 && + d.ephemeralResourceInstancesDeferred.Len() == 0 && + d.partialExpandedResourcesDeferred.Len() == 0 && + d.partialExpandedDataSourcesDeferred.Len() == 0 && + d.partialExpandedEphemeralResourceDeferred.Len() == 0 { + return false + } + + // For this initial implementation we're taking the shortcut of assuming + // that all of the configDeps are required. It would be better to do a + // more precise analysis that takes into account how data could possibly + // flow between instances of resources across different module paths, + // but that may have some subtlety due to dynamic data flow, so we'll + // need to do some more theory work to figure out what kind of analysis + // we'd need to do to get this to be more precise. + // + // This conservative approach is a starting point so we can focus on + // developing the workflow around deferred changes before making its + // analyses more precise. This will defer more changes than strictly + // necessary, but that's better than not deferring changes that should + // have been deferred. + // + // (FWIW, it does seem like we _should_ be able to eliminate some + // dynamic instances from consideration by relying on constraints such as + // how a multi-instance module call can't have an object in one instance + // depending on an object for another instance, but we'll need to make sure + // any additional logic here is well-reasoned to avoid violating dependency + // invariants.) + for _, configDep := range deps { + if d.resourceInstancesDeferred.Has(configDep) || d.dataSourceInstancesDeferred.Has(configDep) || d.ephemeralResourceInstancesDeferred.Has(configDep) { + // For now we don't consider exactly which instances of that + // configuration block were deferred; there being at least + // one is enough. + return true + } + if d.partialExpandedResourcesDeferred.Has(configDep) { + return true + } + if d.partialExpandedDataSourcesDeferred.Has(configDep) { + return true + } + if d.partialExpandedEphemeralResourceDeferred.Has(configDep) { + return true + } + + // We don't check d.partialExpandedModulesDeferred here because + // we expect that the graph nodes representing any resource under + // a partial-expanded module prefix to call + // d.ReportResourceExpansionDeferred once they find out that they + // are under a partial-expanded prefix, and so + // partialExpandedModulesDeferred is effectively just a less-detailed + // summary of the information in partialExpandedResourcesDeferred. + // (instances.Expander is the one responsible for letting the resource + // node discover that it needs to do that; package deferred does + // not participate directly in that concern.) + } + return false +} + +// ReportResourceExpansionDeferred reports that we cannot even predict which +// instances of a resource will be declared and thus we must defer all planning +// for that resource. +func (d *Deferred) ReportResourceExpansionDeferred(addr addrs.PartialExpandedResource, change *plans.ResourceInstanceChange) { + if change == nil { + // This indicates a bug in Terraform, we shouldn't ever be setting a + // null change. Note, if we don't make this check here, then we'll + // just crash later anyway. This way the stack trace points to the + // source of the problem. + panic("change must not be nil") + } + + d.mu.Lock() + defer d.mu.Unlock() + + if addr.Resource().Mode != addrs.ManagedResourceMode { + // Use ReportDataSourceExpansionDeferred for data sources and ReportEphemeralResourceExpansionDeferred for ephemeral resources. + panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource().Mode, addr)) + } + + configAddr := addr.ConfigResource() + if !d.partialExpandedResourcesDeferred.Has(configAddr) { + d.partialExpandedResourcesDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]()) + } + + configMap := d.partialExpandedResourcesDeferred.Get(configAddr) + if configMap.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each distinct partial-expanded + // prefix only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + configMap.Put(addr, &plans.DeferredResourceInstanceChange{ + DeferredReason: providers.DeferredReasonInstanceCountUnknown, + Change: change, + }) +} + +// ReportDataSourceExpansionDeferred reports that we cannot even predict which +// instances of a data source will be declared and thus we must defer all +// planning for that data source. +func (d *Deferred) ReportDataSourceExpansionDeferred(addr addrs.PartialExpandedResource, change *plans.ResourceInstanceChange) { + if change == nil { + // This indicates a bug in Terraform, we shouldn't ever be setting a + // null change. Note, if we don't make this check here, then we'll + // just crash later anyway. This way the stack trace points to the + // source of the problem. + panic("change must not be nil") + } + + d.mu.Lock() + defer d.mu.Unlock() + + if addr.Resource().Mode != addrs.DataResourceMode { + // Use ReportResourceExpansionDeferred for resources and ReportEphemeralResourceExpansionDeferred for ephemeral resources. + panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource().Mode, addr)) + } + + configAddr := addr.ConfigResource() + if !d.partialExpandedDataSourcesDeferred.Has(configAddr) { + d.partialExpandedDataSourcesDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]()) + } + + configMap := d.partialExpandedDataSourcesDeferred.Get(configAddr) + if configMap.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each distinct partial-expanded + // prefix only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + configMap.Put(addr, &plans.DeferredResourceInstanceChange{ + DeferredReason: providers.DeferredReasonInstanceCountUnknown, + Change: change, + }) +} + +func (d *Deferred) ReportEphemeralResourceExpansionDeferred(addr addrs.PartialExpandedResource) { + d.mu.Lock() + defer d.mu.Unlock() + + if addr.Resource().Mode != addrs.EphemeralResourceMode { + // Use ReportResourceExpansionDeferred for resources and ReportDataSourceExpansionDeferred for data sources. + panic(fmt.Sprintf("unexpected resource mode %q for %s", addr.Resource().Mode, addr)) + } + + configAddr := addr.ConfigResource() + if !d.partialExpandedEphemeralResourceDeferred.Has(configAddr) { + d.partialExpandedEphemeralResourceDeferred.Put(configAddr, addrs.MakeMap[addrs.PartialExpandedResource, *plans.DeferredResourceInstanceChange]()) + } + + configMap := d.partialExpandedEphemeralResourceDeferred.Get(configAddr) + if configMap.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each distinct partial-expanded + // prefix only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + configMap.Put(addr, &plans.DeferredResourceInstanceChange{ + DeferredReason: providers.DeferredReasonInstanceCountUnknown, + Change: nil, // since we don't serialize this we can get away with no change, we store the addr, that should be enough + }) +} + +// ReportResourceInstanceDeferred records that a fully-expanded resource +// instance has had its planned action deferred to a future round for a reason +// other than its address being only partially-decided. +func (d *Deferred) ReportResourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason, change *plans.ResourceInstanceChange) { + if change == nil { + // This indicates a bug in Terraform, we shouldn't ever be setting a + // null change. Note, if we don't make this check here, then we'll + // just crash later anyway. This way the stack trace points to the + // source of the problem. + panic("change must not be nil") + } + + d.mu.Lock() + defer d.mu.Unlock() + + configAddr := addr.ConfigResource() + if !d.resourceInstancesDeferred.Has(configAddr) { + d.resourceInstancesDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]()) + } + + configMap := d.resourceInstancesDeferred.Get(configAddr) + if configMap.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each resource instance only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + configMap.Put(addr, &plans.DeferredResourceInstanceChange{ + DeferredReason: reason, + Change: change, + }) +} + +func (d *Deferred) ReportDataSourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason, change *plans.ResourceInstanceChange) { + if change == nil { + // This indicates a bug in Terraform, we shouldn't ever be setting a + // null change. Note, if we don't make this check here, then we'll + // just crash later anyway. This way the stack trace points to the + // source of the problem. + panic("change must not be nil") + } + + d.mu.Lock() + defer d.mu.Unlock() + + configAddr := addr.ConfigResource() + if !d.dataSourceInstancesDeferred.Has(configAddr) { + d.dataSourceInstancesDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]()) + } + + configMap := d.dataSourceInstancesDeferred.Get(configAddr) + if configMap.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each resource instance only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + configMap.Put(addr, &plans.DeferredResourceInstanceChange{ + DeferredReason: reason, + Change: change, + }) +} + +func (d *Deferred) ReportEphemeralResourceInstanceDeferred(addr addrs.AbsResourceInstance, reason providers.DeferredReason) { + d.mu.Lock() + defer d.mu.Unlock() + + configAddr := addr.ConfigResource() + if !d.ephemeralResourceInstancesDeferred.Has(configAddr) { + d.ephemeralResourceInstancesDeferred.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *plans.DeferredResourceInstanceChange]()) + } + + configMap := d.ephemeralResourceInstancesDeferred.Get(configAddr) + if configMap.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each resource instance only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + configMap.Put(addr, &plans.DeferredResourceInstanceChange{ + DeferredReason: reason, + Change: nil, // Since we don't serialize this we can get away with not storing a change + }) +} + +// ReportModuleExpansionDeferred reports that we cannot even predict which +// instances of a module call will be declared and thus we must defer all +// planning for everything inside that module. +// +// Use the most precise partial-expanded module address possible. +func (d *Deferred) ReportModuleExpansionDeferred(addr addrs.PartialExpandedModule) { + if d.partialExpandedModulesDeferred.Has(addr) { + // This indicates a bug in the caller, since our graph walk should + // ensure that we visit and evaluate each distinct partial-expanded + // prefix only once. + panic(fmt.Sprintf("duplicate deferral report for %s", addr)) + } + d.partialExpandedModulesDeferred.Add(addr) +} + +// UnexpectedProviderDeferralDiagnostic is a diagnostic that indicates that a +// provider was deferred although deferrals were not allowed. +func UnexpectedProviderDeferralDiagnostic(addrs addrs.AbsResourceInstance) tfdiags.Diagnostic { + return tfdiags.Sourceless(tfdiags.Error, "Provider deferred changes when Terraform did not allow deferrals", fmt.Sprintf("The provider signaled a deferred action for %q, but in this context deferrals are disabled. This is a bug in the provider, please file an issue with the provider developers.", addrs.String())) +} diff --git a/internal/plans/deferring/deferred_test.go b/internal/plans/deferring/deferred_test.go new file mode 100644 index 0000000000..ca87960fe0 --- /dev/null +++ b/internal/plans/deferring/deferred_test.go @@ -0,0 +1,384 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package deferring + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" +) + +func TestDeferred_externalDependency(t *testing.T) { + deferred := NewDeferred(true) + + // This reports that something outside of the modules runtime knows that + // everything in this configuration depends on some elsewhere-action + // that has been deferred, and so the modules runtime must respect that + // even though it doesn't know the details of why it is so. + deferred.SetExternalDependencyDeferred() + + // With the above flag set, now ShouldDeferResourceInstanceChanges should + // return true regardless of any other information. + got := deferred.ShouldDeferResourceInstanceChanges(addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "anything", + Name: "really-anything", + }, + }, + }, nil) + if !got { + t.Errorf("did not report that the instance should have its changes deferred; should have") + } +} + +func TestDeferred_absResourceInstanceDeferred(t *testing.T) { + instAAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instBAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instCAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "c", + }, + }, + } + + dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource]( + addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{ + Key: instCAddr.ConfigResource(), + Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()}, + }) + + deferred := NewDeferred(true) + + // Before we report anything, all three addresses should indicate that + // they don't need to have their actions deferred. + t.Run("without any deferrals yet", func(t *testing.T) { + for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} { + if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be, yet", instAddr) + } + } + }) + + // Instance A has its Create action deferred for some reason. + deferred.ReportResourceInstanceDeferred(instAAddr, providers.DeferredReasonResourceConfigUnknown, &plans.ResourceInstanceChange{ + Addr: instAAddr, + Change: plans.Change{ + Action: plans.Create, + After: cty.DynamicVal, + }, + }) + + t.Run("with one resource instance deferred", func(t *testing.T) { + if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) { + t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr) + } + if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be", instCAddr) + } + }) +} + +func TestDeferred_absDataSourceInstanceDeferred(t *testing.T) { + instAAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instBAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test", + Name: "b", + }, + }, + } + instCAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "c", + }, + }, + } + + dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource]( + addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{ + Key: instCAddr.ConfigResource(), + Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()}, + }) + deferred := NewDeferred(true) + + // Before we report anything, all three addresses should indicate that + // they don't need to have their actions deferred. + t.Run("without any deferrals yet", func(t *testing.T) { + for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} { + if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be, yet", instAddr) + } + } + }) + + // Instance A has its Read action deferred for some reason. + deferred.ReportDataSourceInstanceDeferred(instAAddr, providers.DeferredReasonProviderConfigUnknown, &plans.ResourceInstanceChange{ + Addr: instAAddr, + PrevRunAddr: instAAddr, + Change: plans.Change{ + Action: plans.Read, + After: cty.DynamicVal, + }, + ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, + }) + + t.Run("with one resource instance deferred", func(t *testing.T) { + if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) { + t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr) + } + if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be", instCAddr) + } + }) +} + +func TestDeferred_absEphemeralResourceInstanceDeferred(t *testing.T) { + instAAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instBAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test", + Name: "b", + }, + }, + } + instCAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "c", + }, + }, + } + + dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource]( + addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{ + Key: instCAddr.ConfigResource(), + Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()}, + }) + deferred := NewDeferred(true) + + // Before we report anything, all three addresses should indicate that + // they don't need to have their actions deferred. + t.Run("without any deferrals yet", func(t *testing.T) { + for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} { + if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be, yet", instAddr) + } + } + }) + + // Instance A has e.g. the open action deferred + deferred.ReportEphemeralResourceInstanceDeferred(instAAddr, providers.DeferredReasonProviderConfigUnknown) + + t.Run("with one resource instance deferred", func(t *testing.T) { + if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) { + t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr) + } + if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be", instCAddr) + } + }) +} + +func TestDeferred_partialExpandedDatasource(t *testing.T) { + instAAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instBAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instCAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test", + Name: "c", + }, + }, + } + instAPartial := addrs.RootModuleInstance. + UnexpandedChild(addrs.ModuleCall{Name: "foo"}). + Resource(instAAddr.Resource.Resource) + + dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource]( + addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{ + Key: instCAddr.ConfigResource(), + Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()}, + }) + deferred := NewDeferred(true) + + // Before we report anything, all three addresses should indicate that + // they don't need to have their actions deferred. + t.Run("without any deferrals yet", func(t *testing.T) { + for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} { + if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be, yet", instAddr) + } + } + }) + + // Resource A hasn't been expanded fully, so is deferred. + deferred.ReportDataSourceExpansionDeferred(instAPartial, &plans.ResourceInstanceChange{ + Addr: instAAddr, + Change: plans.Change{ + Action: plans.Read, + After: cty.DynamicVal, + }, + }) + + t.Run("with one resource instance deferred", func(t *testing.T) { + if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) { + t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr) + } + if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be", instCAddr) + } + }) + +} + +func TestDeferred_partialExpandedResource(t *testing.T) { + instAAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("foo", addrs.NoKey), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instBAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "a", + }, + }, + } + instCAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "c", + }, + }, + } + instAPartial := addrs.RootModuleInstance. + UnexpandedChild(addrs.ModuleCall{Name: "foo"}). + Resource(instAAddr.Resource.Resource) + + dependencies := addrs.MakeMap[addrs.ConfigResource, []addrs.ConfigResource]( + addrs.MapElem[addrs.ConfigResource, []addrs.ConfigResource]{ + Key: instCAddr.ConfigResource(), + Value: []addrs.ConfigResource{instBAddr.ConfigResource(), instAAddr.ConfigResource()}, + }) + deferred := NewDeferred(true) + + // Before we report anything, all three addresses should indicate that + // they don't need to have their actions deferred. + t.Run("without any deferrals yet", func(t *testing.T) { + for _, instAddr := range []addrs.AbsResourceInstance{instAAddr, instBAddr, instCAddr} { + if deferred.ShouldDeferResourceInstanceChanges(instAddr, dependencies.Get(instAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be, yet", instAddr) + } + } + }) + + // Resource A hasn't been expanded fully, so is deferred. + deferred.ReportResourceExpansionDeferred(instAPartial, &plans.ResourceInstanceChange{ + Addr: instAAddr, + Change: plans.Change{ + Action: plans.Create, + After: cty.DynamicVal, + }, + }) + + t.Run("with one resource instance deferred", func(t *testing.T) { + if !deferred.ShouldDeferResourceInstanceChanges(instCAddr, dependencies.Get(instCAddr.ConfigResource())) { + t.Errorf("%s was not reported as needing deferred; should be deferred", instCAddr) + } + if deferred.ShouldDeferResourceInstanceChanges(instBAddr, dependencies.Get(instBAddr.ConfigResource())) { + t.Errorf("%s reported as needing deferred; should not be", instCAddr) + } + }) +} diff --git a/internal/plans/deferring/doc.go b/internal/plans/deferring/doc.go new file mode 100644 index 0000000000..3505225f68 --- /dev/null +++ b/internal/plans/deferring/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package deferring deals with the problem of keeping track of +// "deferred actions", which means the situation where the planning of some +// objects is currently impossible due to incomplete information and so +// Terraform explicitly defers dealing with them until the next plan/apply +// round, while still allowing the operator to apply the partial plan so +// that there will be more information available next time. +package deferring diff --git a/internal/plans/doc.go b/internal/plans/doc.go index 01ca389238..404c23f052 100644 --- a/internal/plans/doc.go +++ b/internal/plans/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package plans contains the types that are used to represent Terraform plans. // // A plan describes a set of changes that Terraform will make to update remote diff --git a/internal/plans/dynamic_value.go b/internal/plans/dynamic_value.go index 51fbb24cfb..647f0b5f29 100644 --- a/internal/plans/dynamic_value.go +++ b/internal/plans/dynamic_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( @@ -21,6 +24,12 @@ import ( // The zero value of DynamicValue is nil, and represents the absense of a // value within the Go type system. This is distinct from a cty.NullVal // result, which represents the absense of a value within the cty type system. +// +// The current format for DynamicValue is cty's MessagePack encoding of the +// value. External callers are not allowed to depend on that, but note that +// our internal stackplan package -- its value serialization code in +// particular -- _does_ rely on that, and so will need to be updated if we +// switch to a different serialization in future. type DynamicValue []byte // NewDynamicValue creates a DynamicValue by serializing the given value diff --git a/internal/plans/internal/planproto/planfile.pb.go b/internal/plans/internal/planproto/planfile.pb.go deleted file mode 100644 index 8e09fee33c..0000000000 --- a/internal/plans/internal/planproto/planfile.pb.go +++ /dev/null @@ -1,1654 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.27.1 -// protoc v3.15.6 -// source: planfile.proto - -package planproto - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// Mode describes the planning mode that created the plan. -type Mode int32 - -const ( - Mode_NORMAL Mode = 0 - Mode_DESTROY Mode = 1 - Mode_REFRESH_ONLY Mode = 2 -) - -// Enum value maps for Mode. -var ( - Mode_name = map[int32]string{ - 0: "NORMAL", - 1: "DESTROY", - 2: "REFRESH_ONLY", - } - Mode_value = map[string]int32{ - "NORMAL": 0, - "DESTROY": 1, - "REFRESH_ONLY": 2, - } -) - -func (x Mode) Enum() *Mode { - p := new(Mode) - *p = x - return p -} - -func (x Mode) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Mode) Descriptor() protoreflect.EnumDescriptor { - return file_planfile_proto_enumTypes[0].Descriptor() -} - -func (Mode) Type() protoreflect.EnumType { - return &file_planfile_proto_enumTypes[0] -} - -func (x Mode) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Mode.Descriptor instead. -func (Mode) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{0} -} - -// Action describes the type of action planned for an object. -// Not all action values are valid for all object types. -type Action int32 - -const ( - Action_NOOP Action = 0 - Action_CREATE Action = 1 - Action_READ Action = 2 - Action_UPDATE Action = 3 - Action_DELETE Action = 5 - Action_DELETE_THEN_CREATE Action = 6 - Action_CREATE_THEN_DELETE Action = 7 -) - -// Enum value maps for Action. -var ( - Action_name = map[int32]string{ - 0: "NOOP", - 1: "CREATE", - 2: "READ", - 3: "UPDATE", - 5: "DELETE", - 6: "DELETE_THEN_CREATE", - 7: "CREATE_THEN_DELETE", - } - Action_value = map[string]int32{ - "NOOP": 0, - "CREATE": 1, - "READ": 2, - "UPDATE": 3, - "DELETE": 5, - "DELETE_THEN_CREATE": 6, - "CREATE_THEN_DELETE": 7, - } -) - -func (x Action) Enum() *Action { - p := new(Action) - *p = x - return p -} - -func (x Action) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (Action) Descriptor() protoreflect.EnumDescriptor { - return file_planfile_proto_enumTypes[1].Descriptor() -} - -func (Action) Type() protoreflect.EnumType { - return &file_planfile_proto_enumTypes[1] -} - -func (x Action) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use Action.Descriptor instead. -func (Action) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{1} -} - -// ResourceInstanceActionReason sometimes provides some additional user-facing -// context for why a particular action was chosen for a resource instance. -// This is for user feedback only and never used to drive behavior during the -// subsequent apply step. -type ResourceInstanceActionReason int32 - -const ( - ResourceInstanceActionReason_NONE ResourceInstanceActionReason = 0 - ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED ResourceInstanceActionReason = 1 - ResourceInstanceActionReason_REPLACE_BY_REQUEST ResourceInstanceActionReason = 2 - ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE ResourceInstanceActionReason = 3 - ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG ResourceInstanceActionReason = 4 - ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION ResourceInstanceActionReason = 5 - ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX ResourceInstanceActionReason = 6 - ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY ResourceInstanceActionReason = 7 - ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE ResourceInstanceActionReason = 8 - ResourceInstanceActionReason_REPLACE_BY_TRIGGERS ResourceInstanceActionReason = 9 - ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN ResourceInstanceActionReason = 10 - ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING ResourceInstanceActionReason = 11 -) - -// Enum value maps for ResourceInstanceActionReason. -var ( - ResourceInstanceActionReason_name = map[int32]string{ - 0: "NONE", - 1: "REPLACE_BECAUSE_TAINTED", - 2: "REPLACE_BY_REQUEST", - 3: "REPLACE_BECAUSE_CANNOT_UPDATE", - 4: "DELETE_BECAUSE_NO_RESOURCE_CONFIG", - 5: "DELETE_BECAUSE_WRONG_REPETITION", - 6: "DELETE_BECAUSE_COUNT_INDEX", - 7: "DELETE_BECAUSE_EACH_KEY", - 8: "DELETE_BECAUSE_NO_MODULE", - 9: "REPLACE_BY_TRIGGERS", - 10: "READ_BECAUSE_CONFIG_UNKNOWN", - 11: "READ_BECAUSE_DEPENDENCY_PENDING", - } - ResourceInstanceActionReason_value = map[string]int32{ - "NONE": 0, - "REPLACE_BECAUSE_TAINTED": 1, - "REPLACE_BY_REQUEST": 2, - "REPLACE_BECAUSE_CANNOT_UPDATE": 3, - "DELETE_BECAUSE_NO_RESOURCE_CONFIG": 4, - "DELETE_BECAUSE_WRONG_REPETITION": 5, - "DELETE_BECAUSE_COUNT_INDEX": 6, - "DELETE_BECAUSE_EACH_KEY": 7, - "DELETE_BECAUSE_NO_MODULE": 8, - "REPLACE_BY_TRIGGERS": 9, - "READ_BECAUSE_CONFIG_UNKNOWN": 10, - "READ_BECAUSE_DEPENDENCY_PENDING": 11, - } -) - -func (x ResourceInstanceActionReason) Enum() *ResourceInstanceActionReason { - p := new(ResourceInstanceActionReason) - *p = x - return p -} - -func (x ResourceInstanceActionReason) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (ResourceInstanceActionReason) Descriptor() protoreflect.EnumDescriptor { - return file_planfile_proto_enumTypes[2].Descriptor() -} - -func (ResourceInstanceActionReason) Type() protoreflect.EnumType { - return &file_planfile_proto_enumTypes[2] -} - -func (x ResourceInstanceActionReason) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use ResourceInstanceActionReason.Descriptor instead. -func (ResourceInstanceActionReason) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{2} -} - -// Status describes the status of a particular checkable object at the -// completion of the plan. -type CheckResults_Status int32 - -const ( - CheckResults_UNKNOWN CheckResults_Status = 0 - CheckResults_PASS CheckResults_Status = 1 - CheckResults_FAIL CheckResults_Status = 2 - CheckResults_ERROR CheckResults_Status = 3 -) - -// Enum value maps for CheckResults_Status. -var ( - CheckResults_Status_name = map[int32]string{ - 0: "UNKNOWN", - 1: "PASS", - 2: "FAIL", - 3: "ERROR", - } - CheckResults_Status_value = map[string]int32{ - "UNKNOWN": 0, - "PASS": 1, - "FAIL": 2, - "ERROR": 3, - } -) - -func (x CheckResults_Status) Enum() *CheckResults_Status { - p := new(CheckResults_Status) - *p = x - return p -} - -func (x CheckResults_Status) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (CheckResults_Status) Descriptor() protoreflect.EnumDescriptor { - return file_planfile_proto_enumTypes[3].Descriptor() -} - -func (CheckResults_Status) Type() protoreflect.EnumType { - return &file_planfile_proto_enumTypes[3] -} - -func (x CheckResults_Status) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use CheckResults_Status.Descriptor instead. -func (CheckResults_Status) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{5, 0} -} - -type CheckResults_ObjectKind int32 - -const ( - CheckResults_UNSPECIFIED CheckResults_ObjectKind = 0 - CheckResults_RESOURCE CheckResults_ObjectKind = 1 - CheckResults_OUTPUT_VALUE CheckResults_ObjectKind = 2 -) - -// Enum value maps for CheckResults_ObjectKind. -var ( - CheckResults_ObjectKind_name = map[int32]string{ - 0: "UNSPECIFIED", - 1: "RESOURCE", - 2: "OUTPUT_VALUE", - } - CheckResults_ObjectKind_value = map[string]int32{ - "UNSPECIFIED": 0, - "RESOURCE": 1, - "OUTPUT_VALUE": 2, - } -) - -func (x CheckResults_ObjectKind) Enum() *CheckResults_ObjectKind { - p := new(CheckResults_ObjectKind) - *p = x - return p -} - -func (x CheckResults_ObjectKind) String() string { - return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) -} - -func (CheckResults_ObjectKind) Descriptor() protoreflect.EnumDescriptor { - return file_planfile_proto_enumTypes[4].Descriptor() -} - -func (CheckResults_ObjectKind) Type() protoreflect.EnumType { - return &file_planfile_proto_enumTypes[4] -} - -func (x CheckResults_ObjectKind) Number() protoreflect.EnumNumber { - return protoreflect.EnumNumber(x) -} - -// Deprecated: Use CheckResults_ObjectKind.Descriptor instead. -func (CheckResults_ObjectKind) EnumDescriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{5, 1} -} - -// Plan is the root message type for the tfplan file -type Plan struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Version is incremented whenever there is a breaking change to - // the serialization format. Programs reading serialized plans should - // verify that version is set to the expected value and abort processing - // if not. A breaking change is any change that may cause an older - // consumer to interpret the structure incorrectly. This number will - // not be incremented if an existing consumer can either safely ignore - // changes to the format or if an existing consumer would fail to process - // the file for another message- or field-specific reason. - Version uint64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - // The mode that was active when this plan was created. - // - // This is saved only for UI purposes, so that Terraform can tailor its - // rendering of the plan depending on the mode. This must never be used to - // make decisions in Terraform Core during the applying of a plan. - UiMode Mode `protobuf:"varint,17,opt,name=ui_mode,json=uiMode,proto3,enum=tfplan.Mode" json:"ui_mode,omitempty"` - // The variables that were set when creating the plan. Each value is - // a msgpack serialization of an HCL value. - Variables map[string]*DynamicValue `protobuf:"bytes,2,rep,name=variables,proto3" json:"variables,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - // An unordered set of proposed changes to resources throughout the - // configuration, including any nested modules. Use the address of - // each resource to determine which module it belongs to. - ResourceChanges []*ResourceInstanceChange `protobuf:"bytes,3,rep,name=resource_changes,json=resourceChanges,proto3" json:"resource_changes,omitempty"` - // An unordered set of detected drift: changes made to resources outside of - // Terraform, computed by comparing the previous run's state to the state - // after refresh. - ResourceDrift []*ResourceInstanceChange `protobuf:"bytes,18,rep,name=resource_drift,json=resourceDrift,proto3" json:"resource_drift,omitempty"` - // An unordered set of proposed changes to outputs in the root module - // of the configuration. This set also includes "no action" changes for - // outputs that are not changing, as context for detecting inconsistencies - // at apply time. - OutputChanges []*OutputChange `protobuf:"bytes,4,rep,name=output_changes,json=outputChanges,proto3" json:"output_changes,omitempty"` - // An unordered set of check results for the entire configuration. - // - // Each element represents a single static configuration object that has - // checks, and each of those may have zero or more dynamic objects that - // the checks were applied to nested within. - CheckResults []*CheckResults `protobuf:"bytes,19,rep,name=check_results,json=checkResults,proto3" json:"check_results,omitempty"` - // An unordered set of target addresses to include when applying. If no - // target addresses are present, the plan applies to the whole - // configuration. - TargetAddrs []string `protobuf:"bytes,5,rep,name=target_addrs,json=targetAddrs,proto3" json:"target_addrs,omitempty"` - // An unordered set of force-replace addresses to include when applying. - // This must match the set of addresses that was used when creating the - // plan, or else applying the plan will fail when it reaches a different - // conclusion about what action a particular resource instance needs. - ForceReplaceAddrs []string `protobuf:"bytes,16,rep,name=force_replace_addrs,json=forceReplaceAddrs,proto3" json:"force_replace_addrs,omitempty"` - // The version string for the Terraform binary that created this plan. - TerraformVersion string `protobuf:"bytes,14,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` - // Backend is a description of the backend configuration and other related - // settings at the time the plan was created. - Backend *Backend `protobuf:"bytes,13,opt,name=backend,proto3" json:"backend,omitempty"` - // RelevantAttributes lists individual resource attributes from - // ResourceDrift which may have contributed to the plan changes. - RelevantAttributes []*PlanResourceAttr `protobuf:"bytes,15,rep,name=relevant_attributes,json=relevantAttributes,proto3" json:"relevant_attributes,omitempty"` -} - -func (x *Plan) Reset() { - *x = Plan{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Plan) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Plan) ProtoMessage() {} - -func (x *Plan) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Plan.ProtoReflect.Descriptor instead. -func (*Plan) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{0} -} - -func (x *Plan) GetVersion() uint64 { - if x != nil { - return x.Version - } - return 0 -} - -func (x *Plan) GetUiMode() Mode { - if x != nil { - return x.UiMode - } - return Mode_NORMAL -} - -func (x *Plan) GetVariables() map[string]*DynamicValue { - if x != nil { - return x.Variables - } - return nil -} - -func (x *Plan) GetResourceChanges() []*ResourceInstanceChange { - if x != nil { - return x.ResourceChanges - } - return nil -} - -func (x *Plan) GetResourceDrift() []*ResourceInstanceChange { - if x != nil { - return x.ResourceDrift - } - return nil -} - -func (x *Plan) GetOutputChanges() []*OutputChange { - if x != nil { - return x.OutputChanges - } - return nil -} - -func (x *Plan) GetCheckResults() []*CheckResults { - if x != nil { - return x.CheckResults - } - return nil -} - -func (x *Plan) GetTargetAddrs() []string { - if x != nil { - return x.TargetAddrs - } - return nil -} - -func (x *Plan) GetForceReplaceAddrs() []string { - if x != nil { - return x.ForceReplaceAddrs - } - return nil -} - -func (x *Plan) GetTerraformVersion() string { - if x != nil { - return x.TerraformVersion - } - return "" -} - -func (x *Plan) GetBackend() *Backend { - if x != nil { - return x.Backend - } - return nil -} - -func (x *Plan) GetRelevantAttributes() []*PlanResourceAttr { - if x != nil { - return x.RelevantAttributes - } - return nil -} - -// Backend is a description of backend configuration and other related settings. -type Backend struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` - Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` -} - -func (x *Backend) Reset() { - *x = Backend{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Backend) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Backend) ProtoMessage() {} - -func (x *Backend) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Backend.ProtoReflect.Descriptor instead. -func (*Backend) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{1} -} - -func (x *Backend) GetType() string { - if x != nil { - return x.Type - } - return "" -} - -func (x *Backend) GetConfig() *DynamicValue { - if x != nil { - return x.Config - } - return nil -} - -func (x *Backend) GetWorkspace() string { - if x != nil { - return x.Workspace - } - return "" -} - -// Change represents a change made to some object, transforming it from an old -// state to a new state. -type Change struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Not all action values are valid for all object types. Consult - // the documentation for any message that embeds Change. - Action Action `protobuf:"varint,1,opt,name=action,proto3,enum=tfplan.Action" json:"action,omitempty"` - // msgpack-encoded HCL values involved in the change. - // - For update and replace, two values are provided that give the old and new values, - // respectively. - // - For create, one value is provided that gives the new value to be created - // - For delete, one value is provided that describes the value being deleted - // - For read, two values are provided that give the prior value for this object - // (or null, if no prior value exists) and the value that was or will be read, - // respectively. - // - For no-op, one value is provided that is left unmodified by this non-change. - Values []*DynamicValue `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` - // An unordered set of paths into the old value which are marked as - // sensitive. Values at these paths should be obscured in human-readable - // output. This set is always empty for create. - BeforeSensitivePaths []*Path `protobuf:"bytes,3,rep,name=before_sensitive_paths,json=beforeSensitivePaths,proto3" json:"before_sensitive_paths,omitempty"` - // An unordered set of paths into the new value which are marked as - // sensitive. Values at these paths should be obscured in human-readable - // output. This set is always empty for delete. - AfterSensitivePaths []*Path `protobuf:"bytes,4,rep,name=after_sensitive_paths,json=afterSensitivePaths,proto3" json:"after_sensitive_paths,omitempty"` -} - -func (x *Change) Reset() { - *x = Change{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Change) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Change) ProtoMessage() {} - -func (x *Change) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Change.ProtoReflect.Descriptor instead. -func (*Change) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{2} -} - -func (x *Change) GetAction() Action { - if x != nil { - return x.Action - } - return Action_NOOP -} - -func (x *Change) GetValues() []*DynamicValue { - if x != nil { - return x.Values - } - return nil -} - -func (x *Change) GetBeforeSensitivePaths() []*Path { - if x != nil { - return x.BeforeSensitivePaths - } - return nil -} - -func (x *Change) GetAfterSensitivePaths() []*Path { - if x != nil { - return x.AfterSensitivePaths - } - return nil -} - -type ResourceInstanceChange struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // addr is a string representation of the resource instance address that - // this change will apply to. - Addr string `protobuf:"bytes,13,opt,name=addr,proto3" json:"addr,omitempty"` - // prev_run_addr is a string representation of the address at which - // this resource instance was tracked during the previous apply operation. - // - // This is populated only if it would be different from addr due to - // Terraform having reacted to refactoring annotations in the configuration. - // If empty, the previous run address is the same as the current address. - PrevRunAddr string `protobuf:"bytes,14,opt,name=prev_run_addr,json=prevRunAddr,proto3" json:"prev_run_addr,omitempty"` - // deposed_key, if set, indicates that this change applies to a deposed - // object for the indicated instance with the given deposed key. If not - // set, the change applies to the instance's current object. - DeposedKey string `protobuf:"bytes,7,opt,name=deposed_key,json=deposedKey,proto3" json:"deposed_key,omitempty"` - // provider is the address of the provider configuration that this change - // was planned with, and thus the configuration that must be used to - // apply it. - Provider string `protobuf:"bytes,8,opt,name=provider,proto3" json:"provider,omitempty"` - // Description of the proposed change. May use "create", "read", "update", - // "replace", "delete" and "no-op" actions. - Change *Change `protobuf:"bytes,9,opt,name=change,proto3" json:"change,omitempty"` - // raw blob value provided by the provider as additional context for the - // change. Must be considered an opaque value for any consumer other than - // the provider that generated it, and will be returned verbatim to the - // provider during the subsequent apply operation. - Private []byte `protobuf:"bytes,10,opt,name=private,proto3" json:"private,omitempty"` - // An unordered set of paths that prompted the change action to be - // "replace" rather than "update". Empty for any action other than - // "replace". - RequiredReplace []*Path `protobuf:"bytes,11,rep,name=required_replace,json=requiredReplace,proto3" json:"required_replace,omitempty"` - // Optional extra user-oriented context for why change.Action was chosen. - // This is for user feedback only and never used to drive behavior during - // apply. - ActionReason ResourceInstanceActionReason `protobuf:"varint,12,opt,name=action_reason,json=actionReason,proto3,enum=tfplan.ResourceInstanceActionReason" json:"action_reason,omitempty"` -} - -func (x *ResourceInstanceChange) Reset() { - *x = ResourceInstanceChange{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *ResourceInstanceChange) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*ResourceInstanceChange) ProtoMessage() {} - -func (x *ResourceInstanceChange) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use ResourceInstanceChange.ProtoReflect.Descriptor instead. -func (*ResourceInstanceChange) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{3} -} - -func (x *ResourceInstanceChange) GetAddr() string { - if x != nil { - return x.Addr - } - return "" -} - -func (x *ResourceInstanceChange) GetPrevRunAddr() string { - if x != nil { - return x.PrevRunAddr - } - return "" -} - -func (x *ResourceInstanceChange) GetDeposedKey() string { - if x != nil { - return x.DeposedKey - } - return "" -} - -func (x *ResourceInstanceChange) GetProvider() string { - if x != nil { - return x.Provider - } - return "" -} - -func (x *ResourceInstanceChange) GetChange() *Change { - if x != nil { - return x.Change - } - return nil -} - -func (x *ResourceInstanceChange) GetPrivate() []byte { - if x != nil { - return x.Private - } - return nil -} - -func (x *ResourceInstanceChange) GetRequiredReplace() []*Path { - if x != nil { - return x.RequiredReplace - } - return nil -} - -func (x *ResourceInstanceChange) GetActionReason() ResourceInstanceActionReason { - if x != nil { - return x.ActionReason - } - return ResourceInstanceActionReason_NONE -} - -type OutputChange struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Name of the output as defined in the root module. - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - // Description of the proposed change. May use "no-op", "create", - // "update" and "delete" actions. - Change *Change `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` - // Sensitive, if true, indicates that one or more of the values given - // in "change" is sensitive and should not be shown directly in any - // rendered plan. - Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"` -} - -func (x *OutputChange) Reset() { - *x = OutputChange{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *OutputChange) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*OutputChange) ProtoMessage() {} - -func (x *OutputChange) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use OutputChange.ProtoReflect.Descriptor instead. -func (*OutputChange) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{4} -} - -func (x *OutputChange) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *OutputChange) GetChange() *Change { - if x != nil { - return x.Change - } - return nil -} - -func (x *OutputChange) GetSensitive() bool { - if x != nil { - return x.Sensitive - } - return false -} - -type CheckResults struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Kind CheckResults_ObjectKind `protobuf:"varint,1,opt,name=kind,proto3,enum=tfplan.CheckResults_ObjectKind" json:"kind,omitempty"` - // Address of the configuration object that declared the checks. - ConfigAddr string `protobuf:"bytes,2,opt,name=config_addr,json=configAddr,proto3" json:"config_addr,omitempty"` - // The aggregate status of the entire configuration object, based on - // the statuses of its zero or more checkable objects. - Status CheckResults_Status `protobuf:"varint,3,opt,name=status,proto3,enum=tfplan.CheckResults_Status" json:"status,omitempty"` - // The results for individual objects that were declared by the - // configuration object named in config_addr. - Objects []*CheckResults_ObjectResult `protobuf:"bytes,4,rep,name=objects,proto3" json:"objects,omitempty"` -} - -func (x *CheckResults) Reset() { - *x = CheckResults{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CheckResults) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CheckResults) ProtoMessage() {} - -func (x *CheckResults) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CheckResults.ProtoReflect.Descriptor instead. -func (*CheckResults) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{5} -} - -func (x *CheckResults) GetKind() CheckResults_ObjectKind { - if x != nil { - return x.Kind - } - return CheckResults_UNSPECIFIED -} - -func (x *CheckResults) GetConfigAddr() string { - if x != nil { - return x.ConfigAddr - } - return "" -} - -func (x *CheckResults) GetStatus() CheckResults_Status { - if x != nil { - return x.Status - } - return CheckResults_UNKNOWN -} - -func (x *CheckResults) GetObjects() []*CheckResults_ObjectResult { - if x != nil { - return x.Objects - } - return nil -} - -// DynamicValue represents a value whose type is not decided until runtime, -// often based on schema information obtained from a plugin. -// -// At present dynamic values are always encoded as msgpack, with extension -// id 0 used to represent the special "unknown" value indicating results -// that won't be known until after apply. -// -// In future other serialization formats may be used, possibly with a -// transitional period of including both as separate attributes of this type. -// Consumers must ignore attributes they don't support and fail if no supported -// attribute is present. The top-level format version will not be incremented -// for changes to the set of dynamic serialization formats. -type DynamicValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` -} - -func (x *DynamicValue) Reset() { - *x = DynamicValue{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *DynamicValue) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*DynamicValue) ProtoMessage() {} - -func (x *DynamicValue) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use DynamicValue.ProtoReflect.Descriptor instead. -func (*DynamicValue) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{6} -} - -func (x *DynamicValue) GetMsgpack() []byte { - if x != nil { - return x.Msgpack - } - return nil -} - -// Path represents a set of steps to traverse into a data structure. It is -// used to refer to a sub-structure within a dynamic data structure presented -// separately. -type Path struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Steps []*Path_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` -} - -func (x *Path) Reset() { - *x = Path{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Path) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Path) ProtoMessage() {} - -func (x *Path) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Path.ProtoReflect.Descriptor instead. -func (*Path) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{7} -} - -func (x *Path) GetSteps() []*Path_Step { - if x != nil { - return x.Steps - } - return nil -} - -type PlanResourceAttr struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Resource string `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` - Attr *Path `protobuf:"bytes,2,opt,name=attr,proto3" json:"attr,omitempty"` -} - -func (x *PlanResourceAttr) Reset() { - *x = PlanResourceAttr{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *PlanResourceAttr) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*PlanResourceAttr) ProtoMessage() {} - -func (x *PlanResourceAttr) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use PlanResourceAttr.ProtoReflect.Descriptor instead. -func (*PlanResourceAttr) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{0, 1} -} - -func (x *PlanResourceAttr) GetResource() string { - if x != nil { - return x.Resource - } - return "" -} - -func (x *PlanResourceAttr) GetAttr() *Path { - if x != nil { - return x.Attr - } - return nil -} - -type CheckResults_ObjectResult struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - ObjectAddr string `protobuf:"bytes,1,opt,name=object_addr,json=objectAddr,proto3" json:"object_addr,omitempty"` - Status CheckResults_Status `protobuf:"varint,2,opt,name=status,proto3,enum=tfplan.CheckResults_Status" json:"status,omitempty"` - FailureMessages []string `protobuf:"bytes,3,rep,name=failure_messages,json=failureMessages,proto3" json:"failure_messages,omitempty"` -} - -func (x *CheckResults_ObjectResult) Reset() { - *x = CheckResults_ObjectResult{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *CheckResults_ObjectResult) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*CheckResults_ObjectResult) ProtoMessage() {} - -func (x *CheckResults_ObjectResult) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use CheckResults_ObjectResult.ProtoReflect.Descriptor instead. -func (*CheckResults_ObjectResult) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{5, 0} -} - -func (x *CheckResults_ObjectResult) GetObjectAddr() string { - if x != nil { - return x.ObjectAddr - } - return "" -} - -func (x *CheckResults_ObjectResult) GetStatus() CheckResults_Status { - if x != nil { - return x.Status - } - return CheckResults_UNKNOWN -} - -func (x *CheckResults_ObjectResult) GetFailureMessages() []string { - if x != nil { - return x.FailureMessages - } - return nil -} - -type Path_Step struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Selector: - // - // *Path_Step_AttributeName - // *Path_Step_ElementKey - Selector isPath_Step_Selector `protobuf_oneof:"selector"` -} - -func (x *Path_Step) Reset() { - *x = Path_Step{} - if protoimpl.UnsafeEnabled { - mi := &file_planfile_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Path_Step) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Path_Step) ProtoMessage() {} - -func (x *Path_Step) ProtoReflect() protoreflect.Message { - mi := &file_planfile_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Path_Step.ProtoReflect.Descriptor instead. -func (*Path_Step) Descriptor() ([]byte, []int) { - return file_planfile_proto_rawDescGZIP(), []int{7, 0} -} - -func (m *Path_Step) GetSelector() isPath_Step_Selector { - if m != nil { - return m.Selector - } - return nil -} - -func (x *Path_Step) GetAttributeName() string { - if x, ok := x.GetSelector().(*Path_Step_AttributeName); ok { - return x.AttributeName - } - return "" -} - -func (x *Path_Step) GetElementKey() *DynamicValue { - if x, ok := x.GetSelector().(*Path_Step_ElementKey); ok { - return x.ElementKey - } - return nil -} - -type isPath_Step_Selector interface { - isPath_Step_Selector() -} - -type Path_Step_AttributeName struct { - // Set "attribute_name" to represent looking up an attribute - // in the current object value. - AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3,oneof"` -} - -type Path_Step_ElementKey struct { - // Set "element_key" to represent looking up an element in - // an indexable collection type. - ElementKey *DynamicValue `protobuf:"bytes,2,opt,name=element_key,json=elementKey,proto3,oneof"` -} - -func (*Path_Step_AttributeName) isPath_Step_Selector() {} - -func (*Path_Step_ElementKey) isPath_Step_Selector() {} - -var File_planfile_proto protoreflect.FileDescriptor - -var file_planfile_proto_rawDesc = []byte{ - 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0xa7, 0x06, 0x0a, 0x04, 0x50, 0x6c, 0x61, - 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x07, 0x75, - 0x69, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x75, 0x69, 0x4d, 0x6f, - 0x64, 0x65, 0x12, 0x39, 0x0a, 0x09, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, - 0x6c, 0x61, 0x6e, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x09, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x49, 0x0a, - 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, - 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x5f, 0x64, 0x72, 0x69, 0x66, 0x74, 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x44, 0x72, 0x69, 0x66, 0x74, 0x12, - 0x3b, 0x0a, 0x0e, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, - 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x6f, - 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0d, - 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x13, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x0c, 0x63, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, - 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x74, - 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x6f, - 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, - 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x11, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x70, 0x6c, 0x61, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, - 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, - 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, - 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, - 0x6e, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x12, 0x4b, 0x0a, 0x13, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x5f, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x52, 0x12, 0x72, 0x65, 0x6c, - 0x65, 0x76, 0x61, 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x1a, - 0x52, 0x0a, 0x0e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, - 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x1a, 0x4d, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x61, 0x74, 0x74, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x12, 0x20, 0x0a, 0x04, 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x61, 0x74, - 0x74, 0x72, 0x22, 0x69, 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x2c, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x1c, 0x0a, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xe4, 0x01, - 0x0a, 0x06, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, - 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x2c, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, - 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x42, - 0x0a, 0x16, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x14, 0x62, 0x65, - 0x66, 0x6f, 0x72, 0x65, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, - 0x68, 0x73, 0x12, 0x40, 0x0a, 0x15, 0x61, 0x66, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, - 0x13, 0x61, 0x66, 0x74, 0x65, 0x72, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, - 0x61, 0x74, 0x68, 0x73, 0x22, 0xd3, 0x02, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, - 0x64, 0x64, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x5f, 0x72, 0x75, 0x6e, 0x5f, - 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x72, 0x65, 0x76, - 0x52, 0x75, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, - 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, - 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, - 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x0c, 0x61, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x68, 0x0a, 0x0c, 0x4f, 0x75, - 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, - 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, - 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, - 0x69, 0x76, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x76, 0x65, 0x22, 0xdd, 0x03, 0x0a, 0x0c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x41, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x06, 0x73, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, - 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x3b, 0x0a, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x1a, 0x8f, 0x01, - 0x0a, 0x0c, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, - 0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, - 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, - 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, - 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, - 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, - 0x34, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x41, 0x53, 0x53, 0x10, 0x01, - 0x12, 0x08, 0x0a, 0x04, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x03, 0x22, 0x3d, 0x0a, 0x0a, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, - 0x69, 0x6e, 0x64, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, - 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x4f, 0x55, 0x54, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x4c, - 0x55, 0x45, 0x10, 0x02, 0x22, 0x28, 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0xa5, - 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, 0x68, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, - 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, - 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, - 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, - 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2a, 0x31, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a, - 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, - 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45, - 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x70, 0x0a, 0x06, 0x41, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a, - 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, - 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, - 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44, - 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, - 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, - 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, 0x2a, 0x86, 0x03, 0x0a, 0x1c, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, - 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, - 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, 0x49, 0x4e, 0x54, 0x45, - 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, - 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x52, - 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, - 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x25, - 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, - 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, - 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, - 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, 0x47, 0x5f, 0x52, 0x45, - 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, 0x0a, 0x1a, 0x44, 0x45, - 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x55, - 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x45, - 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x45, 0x41, 0x43, - 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, 0x45, 0x4c, 0x45, 0x54, - 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x44, - 0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, - 0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, 0x10, 0x09, 0x12, 0x1f, - 0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, - 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x0a, 0x12, - 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, - 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, - 0x4e, 0x47, 0x10, 0x0b, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, - 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, - 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x70, - 0x6c, 0x61, 0x6e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_planfile_proto_rawDescOnce sync.Once - file_planfile_proto_rawDescData = file_planfile_proto_rawDesc -) - -func file_planfile_proto_rawDescGZIP() []byte { - file_planfile_proto_rawDescOnce.Do(func() { - file_planfile_proto_rawDescData = protoimpl.X.CompressGZIP(file_planfile_proto_rawDescData) - }) - return file_planfile_proto_rawDescData -} - -var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 5) -var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 12) -var file_planfile_proto_goTypes = []interface{}{ - (Mode)(0), // 0: tfplan.Mode - (Action)(0), // 1: tfplan.Action - (ResourceInstanceActionReason)(0), // 2: tfplan.ResourceInstanceActionReason - (CheckResults_Status)(0), // 3: tfplan.CheckResults.Status - (CheckResults_ObjectKind)(0), // 4: tfplan.CheckResults.ObjectKind - (*Plan)(nil), // 5: tfplan.Plan - (*Backend)(nil), // 6: tfplan.Backend - (*Change)(nil), // 7: tfplan.Change - (*ResourceInstanceChange)(nil), // 8: tfplan.ResourceInstanceChange - (*OutputChange)(nil), // 9: tfplan.OutputChange - (*CheckResults)(nil), // 10: tfplan.CheckResults - (*DynamicValue)(nil), // 11: tfplan.DynamicValue - (*Path)(nil), // 12: tfplan.Path - nil, // 13: tfplan.Plan.VariablesEntry - (*PlanResourceAttr)(nil), // 14: tfplan.Plan.resource_attr - (*CheckResults_ObjectResult)(nil), // 15: tfplan.CheckResults.ObjectResult - (*Path_Step)(nil), // 16: tfplan.Path.Step -} -var file_planfile_proto_depIdxs = []int32{ - 0, // 0: tfplan.Plan.ui_mode:type_name -> tfplan.Mode - 13, // 1: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry - 8, // 2: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange - 8, // 3: tfplan.Plan.resource_drift:type_name -> tfplan.ResourceInstanceChange - 9, // 4: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange - 10, // 5: tfplan.Plan.check_results:type_name -> tfplan.CheckResults - 6, // 6: tfplan.Plan.backend:type_name -> tfplan.Backend - 14, // 7: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr - 11, // 8: tfplan.Backend.config:type_name -> tfplan.DynamicValue - 1, // 9: tfplan.Change.action:type_name -> tfplan.Action - 11, // 10: tfplan.Change.values:type_name -> tfplan.DynamicValue - 12, // 11: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path - 12, // 12: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path - 7, // 13: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change - 12, // 14: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path - 2, // 15: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason - 7, // 16: tfplan.OutputChange.change:type_name -> tfplan.Change - 4, // 17: tfplan.CheckResults.kind:type_name -> tfplan.CheckResults.ObjectKind - 3, // 18: tfplan.CheckResults.status:type_name -> tfplan.CheckResults.Status - 15, // 19: tfplan.CheckResults.objects:type_name -> tfplan.CheckResults.ObjectResult - 16, // 20: tfplan.Path.steps:type_name -> tfplan.Path.Step - 11, // 21: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue - 12, // 22: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path - 3, // 23: tfplan.CheckResults.ObjectResult.status:type_name -> tfplan.CheckResults.Status - 11, // 24: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue - 25, // [25:25] is the sub-list for method output_type - 25, // [25:25] is the sub-list for method input_type - 25, // [25:25] is the sub-list for extension type_name - 25, // [25:25] is the sub-list for extension extendee - 0, // [0:25] is the sub-list for field type_name -} - -func init() { file_planfile_proto_init() } -func file_planfile_proto_init() { - if File_planfile_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_planfile_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Plan); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Backend); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Change); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ResourceInstanceChange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*OutputChange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CheckResults); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DynamicValue); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Path); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceAttr); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CheckResults_ObjectResult); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_planfile_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Path_Step); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_planfile_proto_msgTypes[11].OneofWrappers = []interface{}{ - (*Path_Step_AttributeName)(nil), - (*Path_Step_ElementKey)(nil), - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_planfile_proto_rawDesc, - NumEnums: 5, - NumMessages: 12, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_planfile_proto_goTypes, - DependencyIndexes: file_planfile_proto_depIdxs, - EnumInfos: file_planfile_proto_enumTypes, - MessageInfos: file_planfile_proto_msgTypes, - }.Build() - File_planfile_proto = out.File - file_planfile_proto_rawDesc = nil - file_planfile_proto_goTypes = nil - file_planfile_proto_depIdxs = nil -} diff --git a/internal/plans/mode.go b/internal/plans/mode.go index 7e78ea859c..ed89aad53c 100644 --- a/internal/plans/mode.go +++ b/internal/plans/mode.go @@ -1,9 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans // Mode represents the various mutually-exclusive modes for creating a plan. type Mode rune -//go:generate go run golang.org/x/tools/cmd/stringer -type Mode +//go:generate go tool golang.org/x/tools/cmd/stringer -type Mode const ( // NormalMode is the default planning mode, which aims to synchronize the diff --git a/internal/plans/objchange/action.go b/internal/plans/objchange/action.go deleted file mode 100644 index 56418aaee8..0000000000 --- a/internal/plans/objchange/action.go +++ /dev/null @@ -1,40 +0,0 @@ -package objchange - -import ( - "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/internal/plans" -) - -// ActionForChange determines which plans.Action value best describes a -// change from the value given in before to the value given in after. -// -// Because it has no context aside from the values, it can only return the -// basic actions NoOp, Create, Update, and Delete. Other codepaths with -// additional information might make this decision differently, such as by -// using the Replace action instead of the Update action where that makes -// sense. -// -// If the after value is unknown then the action can't be properly decided, and -// so ActionForChange will conservatively return either Create or Update -// depending on whether the before value is null. The before value must always -// be fully known; ActionForChange will panic if it contains any unknown values. -func ActionForChange(before, after cty.Value) plans.Action { - switch { - case !after.IsKnown(): - if before.IsNull() { - return plans.Create - } - return plans.Update - case after.IsNull() && before.IsNull(): - return plans.NoOp - case after.IsNull() && !before.IsNull(): - return plans.Delete - case before.IsNull() && !after.IsNull(): - return plans.Create - case after.RawEquals(before): - return plans.NoOp - default: - return plans.Update - } -} diff --git a/internal/plans/objchange/compatible.go b/internal/plans/objchange/compatible.go index ca50263801..18970ec2b1 100644 --- a/internal/plans/objchange/compatible.go +++ b/internal/plans/objchange/compatible.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( @@ -31,15 +34,15 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu var errs []error var atRoot string if len(path) == 0 { - atRoot = "Root resource " + atRoot = "Root object " } if planned.IsNull() && !actual.IsNull() { - errs = append(errs, path.NewErrorf(fmt.Sprintf("%swas absent, but now present", atRoot))) + errs = append(errs, path.NewErrorf("%swas absent, but now present", atRoot)) return errs } if actual.IsNull() && !planned.IsNull() { - errs = append(errs, path.NewErrorf(fmt.Sprintf("%swas present, but now absent", atRoot))) + errs = append(errs, path.NewErrorf("%swas present, but now absent", atRoot)) return errs } if planned.IsNull() { @@ -193,6 +196,18 @@ func assertObjectCompatible(schema *configschema.Block, planned, actual cty.Valu return errs } +// AssertValueCompatible matches the behavior of AssertObjectCompatible but +// for a single value rather than a whole object. This is used by the stacks +// package to compare before and after values of inputs. +// +// This function strips marks from its inputs, as they are not considered +// relevant by the call site. +func AssertValueCompatible(planned, actual cty.Value) []error { + planned, _ = planned.UnmarkDeep() + actual, _ = actual.UnmarkDeep() + return assertValueCompatible(planned, actual, nil) +} + func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error { // NOTE: We don't normally use the GoString rendering of cty.Value in // user-facing error messages as a rule, but we make an exception @@ -213,7 +228,12 @@ func assertValueCompatible(planned, actual cty.Value, path cty.Path) []error { if !planned.IsKnown() { // We didn't know what were going to end up with during plan, so - // anything goes during apply. + // the final value needs only to match the type and refinements of + // the unknown value placeholder. + plannedRng := planned.Range() + if ok := plannedRng.Includes(actual); ok.IsKnown() && ok.False() { + errs = append(errs, path.NewErrorf("final value %#v does not conform to planning placeholder %#v", actual, planned)) + } return errs } diff --git a/internal/plans/objchange/compatible_test.go b/internal/plans/objchange/compatible_test.go index 213d3e103f..10c08fc00d 100644 --- a/internal/plans/objchange/compatible_test.go +++ b/internal/plans/objchange/compatible_test.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( "fmt" "testing" - "github.com/apparentlymart/go-dump/dump" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -115,6 +118,65 @@ func TestAssertObjectCompatible(t *testing.T) { `.name: was cty.StringVal("wotsit"), but now cty.StringVal("thingy")`, }, }, + { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.Zero, + }), + []string{ + `.name: wrong final value type: string required`, + }, + }, + { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).RefineNotNull(), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + }), + []string{ + `.name: final value cty.NullVal(cty.String) does not conform to planning placeholder cty.UnknownVal(cty.String).RefineNotNull()`, + }, + }, + { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefix("boop:"). + NewValue(), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("thingy"), + }), + []string{ + `.name: final value cty.StringVal("thingy") does not conform to planning placeholder cty.UnknownVal(cty.String).Refine().StringPrefixFull("boop:").NewValue()`, + }, + }, { &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -1360,7 +1422,7 @@ func TestAssertObjectCompatible(t *testing.T) { wantErrs[msg] = struct{}{} } - t.Logf("\nplanned: %sactual: %s", dump.Value(test.Planned), dump.Value(test.Actual)) + t.Logf("\nplanned: %sactual: %s", ctydebug.ValueString(test.Planned), ctydebug.ValueString(test.Actual)) for msg := range wantErrs { if _, ok := gotErrs[msg]; !ok { t.Errorf("missing expected error: %s", msg) diff --git a/internal/plans/objchange/doc.go b/internal/plans/objchange/doc.go index 2c18a0108f..c329c53269 100644 --- a/internal/plans/objchange/doc.go +++ b/internal/plans/objchange/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package objchange deals with the business logic of taking a prior state // value and a config value and producing a proposed new merged value, along // with other related rules in this domain. diff --git a/internal/plans/objchange/lcs.go b/internal/plans/objchange/lcs.go index 0ab06f5bf8..fd06aa33b0 100644 --- a/internal/plans/objchange/lcs.go +++ b/internal/plans/objchange/lcs.go @@ -1,9 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( "github.com/zclconf/go-cty/cty" ) +// ValueEqual provides an implementation of the equals function that can be +// passed into LongestCommonSubsequence when comparing cty.Value types. +func ValueEqual(x, y cty.Value) bool { + unmarkedX, xMarks := x.UnmarkDeep() + unmarkedY, yMarks := y.UnmarkDeep() + eqV := unmarkedX.Equals(unmarkedY) + if len(xMarks) != len(yMarks) { + eqV = cty.False + } + if eqV.IsKnown() && eqV.True() { + return true + } + return false +} + // LongestCommonSubsequence finds a sequence of values that are common to both // x and y, with the same relative ordering as in both collections. This result // is useful as a first step towards computing a diff showing added/removed @@ -15,9 +33,9 @@ import ( // // A pair of lists may have multiple longest common subsequences. In that // case, the one selected by this function is undefined. -func LongestCommonSubsequence(xs, ys []cty.Value) []cty.Value { +func LongestCommonSubsequence[V any](xs, ys []V, equals func(x, y V) bool) []V { if len(xs) == 0 || len(ys) == 0 { - return make([]cty.Value, 0) + return make([]V, 0) } c := make([]int, len(xs)*len(ys)) @@ -26,14 +44,8 @@ func LongestCommonSubsequence(xs, ys []cty.Value) []cty.Value { for y := 0; y < len(ys); y++ { for x := 0; x < len(xs); x++ { - unmarkedX, xMarks := xs[x].UnmarkDeep() - unmarkedY, yMarks := ys[y].UnmarkDeep() - eqV := unmarkedX.Equals(unmarkedY) - if len(xMarks) != len(yMarks) { - eqV = cty.False - } eq := false - if eqV.IsKnown() && eqV.True() { + if equals(xs[x], ys[y]) { eq = true eqs[(w*y)+x] = true // equality tests can be expensive, so cache it } @@ -66,7 +78,7 @@ func LongestCommonSubsequence(xs, ys []cty.Value) []cty.Value { } // The bottom right cell tells us how long our longest sequence will be - seq := make([]cty.Value, c[len(c)-1]) + seq := make([]V, c[len(c)-1]) // Now we will walk back from the bottom right cell, finding again all // of the equal pairs to construct our sequence. diff --git a/internal/plans/objchange/lcs_test.go b/internal/plans/objchange/lcs_test.go index 519ebda91c..0627e79f90 100644 --- a/internal/plans/objchange/lcs_test.go +++ b/internal/plans/objchange/lcs_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( @@ -107,7 +110,7 @@ func TestLongestCommonSubsequence(t *testing.T) { for _, test := range tests { t.Run(fmt.Sprintf("%#v,%#v", test.xs, test.ys), func(t *testing.T) { - got := LongestCommonSubsequence(test.xs, test.ys) + got := LongestCommonSubsequence(test.xs, test.ys, ValueEqual) wrong := func() { t.Fatalf( diff --git a/internal/plans/objchange/normalize_obj.go b/internal/plans/objchange/normalize_obj.go index 3db3f66f58..f469b93f72 100644 --- a/internal/plans/objchange/normalize_obj.go +++ b/internal/plans/objchange/normalize_obj.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( diff --git a/internal/plans/objchange/normalize_obj_test.go b/internal/plans/objchange/normalize_obj_test.go index e350e181c4..0502766fa0 100644 --- a/internal/plans/objchange/normalize_obj_test.go +++ b/internal/plans/objchange/normalize_obj_test.go @@ -1,11 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( "testing" - "github.com/apparentlymart/go-dump/dump" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" ) func TestNormalizeObjectFromLegacySDK(t *testing.T) { @@ -297,11 +302,8 @@ func TestNormalizeObjectFromLegacySDK(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { got := NormalizeObjectFromLegacySDK(test.Input, test.Schema) - if !got.RawEquals(test.Want) { - t.Errorf( - "wrong result\ngot: %s\nwant: %s", - dump.Value(got), dump.Value(test.Want), - ) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) } }) } diff --git a/internal/plans/objchange/objchange.go b/internal/plans/objchange/objchange.go index 3164099918..e2030862d2 100644 --- a/internal/plans/objchange/objchange.go +++ b/internal/plans/objchange/objchange.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( + "errors" "fmt" "github.com/zclconf/go-cty/cty" @@ -69,10 +73,15 @@ func PlannedDataResourceObject(schema *configschema.Block, config cty.Value) cty func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value { if config.IsNull() || !config.IsKnown() { - // This is a weird situation, but we'll allow it anyway to free - // callers from needing to specifically check for these cases. + // A block config should never be null at this point. The only nullable + // block type is NestingSingle, which will return early before coming + // back here. We'll allow the null here anyway to free callers from + // needing to specifically check for these cases, and any mismatch will + // be caught in validation, so just take the prior value rather than + // the invalid null. return prior } + if (!prior.Type().IsObjectType()) || (!config.Type().IsObjectType()) { panic("ProposedNew only supports object-typed values") } @@ -95,6 +104,19 @@ func proposedNew(schema *configschema.Block, prior, config cty.Value) cty.Value return cty.ObjectVal(newAttrs) } +// proposedNewBlockOrObject dispatched the schema to either ProposedNew or +// proposedNewObjectAttributes depending on the given type. +func proposedNewBlockOrObject(schema nestedSchema, prior, config cty.Value) cty.Value { + switch schema := schema.(type) { + case *configschema.Block: + return ProposedNew(schema, prior, config) + case *configschema.Object: + return proposedNewObjectAttributes(schema, prior, config) + default: + panic(fmt.Sprintf("unexpected schema type %T", schema)) + } +} + func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty.Value) cty.Value { // The only time we should encounter an entirely unknown block is from the // use of dynamic with an unknown for_each expression. @@ -102,532 +124,375 @@ func proposedNewNestedBlock(schema *configschema.NestedBlock, prior, config cty. return config } - var newV cty.Value + newV := config switch schema.Nesting { + case configschema.NestingSingle: + // A NestingSingle configuration block value can be null, and since it + // cannot be computed we can always take the configuration value. + if config.IsNull() { + break + } - case configschema.NestingSingle, configschema.NestingGroup: + // Otherwise use the same assignment rules as NestingGroup + fallthrough + case configschema.NestingGroup: newV = ProposedNew(&schema.Block, prior, config) case configschema.NestingList: - // Nested blocks are correlated by index. - configVLen := 0 - if !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - newVals := make([]cty.Value, 0, configVLen) - for it := config.ElementIterator(); it.Next(); { - idx, configEV := it.Element() - if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals = append(newVals, configEV) - continue - } - priorEV := prior.Index(idx) - - newEV := ProposedNew(&schema.Block, priorEV, configEV) - newVals = append(newVals, newEV) - } - // Despite the name, a NestingList might also be a tuple, if - // its nested schema contains dynamically-typed attributes. - if config.Type().IsTupleType() { - newV = cty.TupleVal(newVals) - } else { - newV = cty.ListVal(newVals) - } - } else { - // Despite the name, a NestingList might also be a tuple, if - // its nested schema contains dynamically-typed attributes. - if config.Type().IsTupleType() { - newV = cty.EmptyTupleVal - } else { - newV = cty.ListValEmpty(schema.ImpliedType()) - } - } + newV = proposedNewNestingList(&schema.Block, prior, config) case configschema.NestingMap: - // Despite the name, a NestingMap may produce either a map or - // object value, depending on whether the nested schema contains - // dynamically-typed attributes. - if config.Type().IsObjectType() { - // Nested blocks are correlated by key. - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - newVals := make(map[string]cty.Value, configVLen) - atys := config.Type().AttributeTypes() - for name := range atys { - configEV := config.GetAttr(name) - if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals[name] = configEV - continue - } - priorEV := prior.GetAttr(name) - - newEV := ProposedNew(&schema.Block, priorEV, configEV) - newVals[name] = newEV - } - // Although we call the nesting mode "map", we actually use - // object values so that elements might have different types - // in case of dynamically-typed attributes. - newV = cty.ObjectVal(newVals) - } else { - newV = cty.EmptyObjectVal - } - } else { - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - newVals := make(map[string]cty.Value, configVLen) - for it := config.ElementIterator(); it.Next(); { - idx, configEV := it.Element() - k := idx.AsString() - if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals[k] = configEV - continue - } - priorEV := prior.Index(idx) - - newEV := ProposedNew(&schema.Block, priorEV, configEV) - newVals[k] = newEV - } - newV = cty.MapVal(newVals) - } else { - newV = cty.MapValEmpty(schema.ImpliedType()) - } - } + newV = proposedNewNestingMap(&schema.Block, prior, config) case configschema.NestingSet: - if !config.Type().IsSetType() { - panic("configschema.NestingSet value is not a set as expected") - } - - // Nested blocks are correlated by comparing the element values - // after eliminating all of the computed attributes. In practice, - // this means that any config change produces an entirely new - // nested object, and we only propagate prior computed values - // if the non-computed attribute values are identical. - var cmpVals [][2]cty.Value - if prior.IsKnown() && !prior.IsNull() { - cmpVals = setElementCompareValues(&schema.Block, prior, false) - } - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value - newVals := make([]cty.Value, 0, configVLen) - for it := config.ElementIterator(); it.Next(); { - _, configEV := it.Element() - var priorEV cty.Value - for i, cmp := range cmpVals { - if used[i] { - continue - } - if cmp[1].RawEquals(configEV) { - priorEV = cmp[0] - used[i] = true // we can't use this value on a future iteration - break - } - } - if priorEV == cty.NilVal { - priorEV = cty.NullVal(schema.ImpliedType()) - } - - newEV := ProposedNew(&schema.Block, priorEV, configEV) - newVals = append(newVals, newEV) - } - newV = cty.SetVal(newVals) - } else { - newV = cty.SetValEmpty(schema.Block.ImpliedType()) - } + newV = proposedNewNestingSet(&schema.Block, prior, config) default: // Should never happen, since the above cases are comprehensive. panic(fmt.Sprintf("unsupported block nesting mode %s", schema.Nesting)) } + return newV } +func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { + // if the config isn't known at all, then we must use that value + if !config.IsKnown() { + return config + } + + // Even if the config is null or empty, we will be using this default value. + newV := config + + switch schema.Nesting { + case configschema.NestingSingle: + // If the config is null, we already have our value. If the attribute + // is optional+computed, we won't reach this branch with a null value + // since the computed case would have been taken. + if config.IsNull() { + break + } + + newV = proposedNewObjectAttributes(schema, prior, config) + + case configschema.NestingList: + newV = proposedNewNestingList(schema, prior, config) + + case configschema.NestingMap: + newV = proposedNewNestingMap(schema, prior, config) + + case configschema.NestingSet: + newV = proposedNewNestingSet(schema, prior, config) + + default: + // Should never happen, since the above cases are comprehensive. + panic(fmt.Sprintf("unsupported attribute nesting mode %s", schema.Nesting)) + } + + return newV +} + +func proposedNewNestingList(schema nestedSchema, prior, config cty.Value) cty.Value { + newV := config + + // Nested blocks are correlated by index. + configVLen := 0 + if !config.IsNull() { + configVLen = config.LengthInt() + } + if configVLen > 0 { + newVals := make([]cty.Value, 0, configVLen) + for it := config.ElementIterator(); it.Next(); { + idx, configEV := it.Element() + if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { + // If there is no corresponding prior element then + // we just take the config value as-is. + newVals = append(newVals, configEV) + continue + } + priorEV := prior.Index(idx) + + newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV)) + } + // Despite the name, a NestingList might also be a tuple, if + // its nested schema contains dynamically-typed attributes. + if config.Type().IsTupleType() { + newV = cty.TupleVal(newVals) + } else { + newV = cty.ListVal(newVals) + } + } + + return newV +} + +func proposedNewNestingMap(schema nestedSchema, prior, config cty.Value) cty.Value { + newV := config + newVals := map[string]cty.Value{} + + if config.IsNull() || !config.IsKnown() || config.LengthInt() == 0 { + // We already assigned newVal and there's nothing to compare in + // config. + return newV + } + cfgMap := config.AsValueMap() + + // prior may be null, empty or unknown + priorMap := map[string]cty.Value{} + if !prior.IsNull() && prior.IsKnown() && prior.LengthInt() > 0 { + priorMap = prior.AsValueMap() + } + + for name, configEV := range cfgMap { + priorEV, inPrior := priorMap[name] + if !inPrior { + // if the prior cty.Map value was unknown the map won't have any + // keys, so generate an unknown value. + if !prior.IsKnown() { + priorEV = cty.UnknownVal(configEV.Type()) + } else { + priorEV = cty.NullVal(configEV.Type()) + } + } + + newVals[name] = proposedNewBlockOrObject(schema, priorEV, configEV) + } + + // The value must leave as the same type it came in as + switch { + case config.Type().IsObjectType(): + // Although we call the nesting mode "map", we actually use + // object values so that elements might have different types + // in case of dynamically-typed attributes. + newV = cty.ObjectVal(newVals) + default: + newV = cty.MapVal(newVals) + } + + return newV +} + +func proposedNewNestingSet(schema nestedSchema, prior, config cty.Value) cty.Value { + if !config.Type().IsSetType() { + panic("configschema.NestingSet value is not a set as expected") + } + + newV := config + if !config.IsKnown() || config.IsNull() || config.LengthInt() == 0 { + return newV + } + + var priorVals []cty.Value + if prior.IsKnown() && !prior.IsNull() { + priorVals = prior.AsValueSlice() + } + + var newVals []cty.Value + // track which prior elements have been used + used := make([]bool, len(priorVals)) + + for _, configEV := range config.AsValueSlice() { + var priorEV cty.Value + for i, priorCmp := range priorVals { + if used[i] { + continue + } + + // It is possible that multiple prior elements could be valid + // matches for a configuration value, in which case we will end up + // picking the first match encountered (but it will always be + // consistent due to cty's iteration order). Because configured set + // elements must also be entirely unique in order to be included in + // the set, these matches either will not matter because they only + // differ by computed values, or could not have come from a valid + // config with all unique set elements. + if validPriorFromConfig(schema, priorCmp, configEV) { + priorEV = priorCmp + used[i] = true + break + } + } + + if priorEV == cty.NilVal { + priorEV = cty.NullVal(config.Type().ElementType()) + } + + newVals = append(newVals, proposedNewBlockOrObject(schema, priorEV, configEV)) + } + + return cty.SetVal(newVals) +} + +func proposedNewObjectAttributes(schema *configschema.Object, prior, config cty.Value) cty.Value { + if config.IsNull() { + return config + } + + return cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) +} + func proposedNewAttributes(attrs map[string]*configschema.Attribute, prior, config cty.Value) map[string]cty.Value { newAttrs := make(map[string]cty.Value, len(attrs)) for name, attr := range attrs { var priorV cty.Value - if prior.IsNull() { + + switch { + case prior.IsNull(): priorV = cty.NullVal(prior.Type().AttributeType(name)) - } else { + case !prior.IsKnown(): + priorV = cty.UnknownVal(prior.Type().AttributeType(name)) + default: priorV = prior.GetAttr(name) } - configV := config.GetAttr(name) var newV cty.Value switch { - case attr.Computed && attr.Optional: - // This is the trickiest scenario: we want to keep the prior value - // if the config isn't overriding it. Note that due to some - // ambiguity here, setting an optional+computed attribute from - // config and then later switching the config to null in a - // subsequent change causes the initial config value to be "sticky" - // unless the provider specifically overrides it during its own - // plan customization step. - if configV.IsNull() { - newV = priorV - } else { - newV = configV - } - case attr.Computed: + // required isn't considered when constructing the plan, so attributes + // are essentially either computed or not computed. In the case of + // optional+computed, they are only computed when there is no + // configuration. + case attr.Computed && configV.IsNull(): // configV will always be null in this case, by definition. // priorV may also be null, but that's okay. newV = priorV - default: - if attr.NestedType != nil { - // For non-computed NestedType attributes, we need to descend - // into the individual nested attributes to build the final - // value, unless the entire nested attribute is unknown. - if !configV.IsKnown() { - newV = configV - } else { - newV = proposedNewNestedType(attr.NestedType, priorV, configV) - } - } else { - // For non-computed attributes, we always take the config value, - // even if it is null. If it's _required_ then null values - // should've been caught during an earlier validation step, and - // so we don't really care about that here. + + // the exception to the above is that if the config is optional and + // the _prior_ value contains non-computed values, we can infer + // that the config must have been non-null previously. + if optionalValueNotComputable(attr, priorV) { newV = configV } + + case attr.NestedType != nil: + // For non-computed NestedType attributes, we need to descend + // into the individual nested attributes to build the final + // value, unless the entire nested attribute is unknown. + newV = proposedNewNestedType(attr.NestedType, priorV, configV) + default: + // For non-computed attributes, we always take the config value, + // even if it is null. If it's _required_ then null values + // should've been caught during an earlier validation step, and + // so we don't really care about that here. + newV = configV } newAttrs[name] = newV } return newAttrs } -func proposedNewNestedType(schema *configschema.Object, prior, config cty.Value) cty.Value { - // If the config is null or empty, we will be using this default value. - newV := config - - switch schema.Nesting { - case configschema.NestingSingle: - if !config.IsNull() { - newV = cty.ObjectVal(proposedNewAttributes(schema.Attributes, prior, config)) - } else { - newV = cty.NullVal(config.Type()) - } - - case configschema.NestingList: - // Nested blocks are correlated by index. - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - - if configVLen > 0 { - newVals := make([]cty.Value, 0, configVLen) - for it := config.ElementIterator(); it.Next(); { - idx, configEV := it.Element() - if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals = append(newVals, configEV) - continue - } - priorEV := prior.Index(idx) - - newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) - newVals = append(newVals, cty.ObjectVal(newEV)) - } - // Despite the name, a NestingList might also be a tuple, if - // its nested schema contains dynamically-typed attributes. - if config.Type().IsTupleType() { - newV = cty.TupleVal(newVals) - } else { - newV = cty.ListVal(newVals) - } - } - - case configschema.NestingMap: - // Despite the name, a NestingMap may produce either a map or - // object value, depending on whether the nested schema contains - // dynamically-typed attributes. - if config.Type().IsObjectType() { - // Nested blocks are correlated by key. - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - newVals := make(map[string]cty.Value, configVLen) - atys := config.Type().AttributeTypes() - for name := range atys { - configEV := config.GetAttr(name) - if !prior.IsKnown() || prior.IsNull() || !prior.Type().HasAttribute(name) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals[name] = configEV - continue - } - priorEV := prior.GetAttr(name) - newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) - newVals[name] = cty.ObjectVal(newEV) - } - // Although we call the nesting mode "map", we actually use - // object values so that elements might have different types - // in case of dynamically-typed attributes. - newV = cty.ObjectVal(newVals) - } - } else { - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - newVals := make(map[string]cty.Value, configVLen) - for it := config.ElementIterator(); it.Next(); { - idx, configEV := it.Element() - k := idx.AsString() - if prior.IsKnown() && (prior.IsNull() || !prior.HasIndex(idx).True()) { - // If there is no corresponding prior element then - // we just take the config value as-is. - newVals[k] = configEV - continue - } - priorEV := prior.Index(idx) - - newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) - newVals[k] = cty.ObjectVal(newEV) - } - newV = cty.MapVal(newVals) - } - } - - case configschema.NestingSet: - // Nested blocks are correlated by comparing the element values - // after eliminating all of the computed attributes. In practice, - // this means that any config change produces an entirely new - // nested object, and we only propagate prior computed values - // if the non-computed attribute values are identical. - var cmpVals [][2]cty.Value - if prior.IsKnown() && !prior.IsNull() { - cmpVals = setElementCompareValuesFromObject(schema, prior) - } - configVLen := 0 - if config.IsKnown() && !config.IsNull() { - configVLen = config.LengthInt() - } - if configVLen > 0 { - used := make([]bool, len(cmpVals)) // track used elements in case multiple have the same compare value - newVals := make([]cty.Value, 0, configVLen) - for it := config.ElementIterator(); it.Next(); { - _, configEV := it.Element() - var priorEV cty.Value - for i, cmp := range cmpVals { - if used[i] { - continue - } - if cmp[1].RawEquals(configEV) { - priorEV = cmp[0] - used[i] = true // we can't use this value on a future iteration - break - } - } - if priorEV == cty.NilVal { - newVals = append(newVals, configEV) - } else { - newEV := proposedNewAttributes(schema.Attributes, priorEV, configEV) - newVals = append(newVals, cty.ObjectVal(newEV)) - } - } - newV = cty.SetVal(newVals) - } - } - - return newV +// nestedSchema is used as a generic container for either a +// *configschema.Object, or *configschema.Block. +type nestedSchema interface { + AttributeByPath(cty.Path) *configschema.Attribute } -// setElementCompareValues takes a known, non-null value of a cty.Set type and -// returns a table -- constructed of two-element arrays -- that maps original -// set element values to corresponding values that have all of the computed -// values removed, making them suitable for comparison with values obtained -// from configuration. The element type of the set must conform to the implied -// type of the given schema, or this function will panic. +// optionalValueNotComputable is used to check if an object in state must +// have at least partially come from configuration. If the prior value has any +// non-null attributes which are not computed in the schema, then we know there +// was previously a configuration value which set those. // -// In the resulting slice, the zeroth element of each array is the original -// value and the one-indexed element is the corresponding "compare value". -// -// This is intended to help correlate prior elements with configured elements -// in proposedNewBlock. The result is a heuristic rather than an exact science, -// since e.g. two separate elements may reduce to the same value through this -// process. The caller must therefore be ready to deal with duplicates. -func setElementCompareValues(schema *configschema.Block, set cty.Value, isConfig bool) [][2]cty.Value { - ret := make([][2]cty.Value, 0, set.LengthInt()) - for it := set.ElementIterator(); it.Next(); { - _, ev := it.Element() - ret = append(ret, [2]cty.Value{ev, setElementCompareValue(schema, ev, isConfig)}) - } - return ret -} - -// setElementCompareValue creates a new value that has all of the same -// non-computed attribute values as the one given but has all computed -// attribute values forced to null. -// -// If isConfig is true then non-null Optional+Computed attribute values will -// be preserved. Otherwise, they will also be set to null. -// -// The input value must conform to the schema's implied type, and the return -// value is guaranteed to conform to it. -func setElementCompareValue(schema *configschema.Block, v cty.Value, isConfig bool) cty.Value { - if v.IsNull() || !v.IsKnown() { - return v +// This is used when the configuration contains a null optional+computed value, +// and we want to know if we should plan to send the null value or the prior +// state. +func optionalValueNotComputable(schema *configschema.Attribute, val cty.Value) bool { + if !schema.Optional { + return false } - attrs := map[string]cty.Value{} - for name, attr := range schema.Attributes { - switch { - case attr.Computed && attr.Optional: - if isConfig { - attrs[name] = v.GetAttr(name) - } else { - attrs[name] = cty.NullVal(attr.ImpliedType()) - } - case attr.Computed: - attrs[name] = cty.NullVal(attr.ImpliedType()) - default: - attrs[name] = v.GetAttr(name) + // We must have a NestedType for complex nested attributes in order + // to find nested computed values in the first place. + if schema.NestedType == nil { + return false + } + + foundNonComputedAttr := false + cty.Walk(val, func(path cty.Path, v cty.Value) (bool, error) { + if v.IsNull() { + return true, nil } - } - for name, blockType := range schema.BlockTypes { - elementType := blockType.Block.ImpliedType() - - switch blockType.Nesting { - case configschema.NestingSingle, configschema.NestingGroup: - attrs[name] = setElementCompareValue(&blockType.Block, v.GetAttr(name), isConfig) - - case configschema.NestingList, configschema.NestingSet: - cv := v.GetAttr(name) - if cv.IsNull() || !cv.IsKnown() { - attrs[name] = cv - continue - } - - if l := cv.LengthInt(); l > 0 { - elems := make([]cty.Value, 0, l) - for it := cv.ElementIterator(); it.Next(); { - _, ev := it.Element() - elems = append(elems, setElementCompareValue(&blockType.Block, ev, isConfig)) - } - - switch { - case blockType.Nesting == configschema.NestingSet: - // SetValEmpty would panic if given elements that are not - // all of the same type, but that's guaranteed not to - // happen here because our input value was _already_ a - // set and we've not changed the types of any elements here. - attrs[name] = cty.SetVal(elems) - - // NestingList cases - case elementType.HasDynamicTypes(): - attrs[name] = cty.TupleVal(elems) - default: - attrs[name] = cty.ListVal(elems) - } - } else { - switch { - case blockType.Nesting == configschema.NestingSet: - attrs[name] = cty.SetValEmpty(elementType) - - // NestingList cases - case elementType.HasDynamicTypes(): - attrs[name] = cty.EmptyTupleVal - default: - attrs[name] = cty.ListValEmpty(elementType) - } - } - - case configschema.NestingMap: - cv := v.GetAttr(name) - if cv.IsNull() || !cv.IsKnown() || cv.LengthInt() == 0 { - attrs[name] = cv - continue - } - elems := make(map[string]cty.Value) - for it := cv.ElementIterator(); it.Next(); { - kv, ev := it.Element() - elems[kv.AsString()] = setElementCompareValue(&blockType.Block, ev, isConfig) - } - - switch { - case elementType.HasDynamicTypes(): - attrs[name] = cty.ObjectVal(elems) - default: - attrs[name] = cty.MapVal(elems) - } - - default: - // Should never happen, since the above cases are comprehensive. - panic(fmt.Sprintf("unsupported block nesting mode %s", blockType.Nesting)) + attr := schema.NestedType.AttributeByPath(path) + if attr == nil { + return true, nil } - } - return cty.ObjectVal(attrs) -} - -// setElementCompareValues takes a known, non-null value of a cty.Set type and -// returns a table -- constructed of two-element arrays -- that maps original -// set element values to corresponding values that have all of the computed -// values removed, making them suitable for comparison with values obtained -// from configuration. The element type of the set must conform to the implied -// type of the given schema, or this function will panic. -// -// In the resulting slice, the zeroth element of each array is the original -// value and the one-indexed element is the corresponding "compare value". -// -// This is intended to help correlate prior elements with configured elements -// in proposedNewBlock. The result is a heuristic rather than an exact science, -// since e.g. two separate elements may reduce to the same value through this -// process. The caller must therefore be ready to deal with duplicates. -func setElementCompareValuesFromObject(schema *configschema.Object, set cty.Value) [][2]cty.Value { - ret := make([][2]cty.Value, 0, set.LengthInt()) - for it := set.ElementIterator(); it.Next(); { - _, ev := it.Element() - ret = append(ret, [2]cty.Value{ev, setElementCompareValueFromObject(schema, ev)}) - } - return ret -} - -// setElementCompareValue creates a new value that has all of the same -// non-computed attribute values as the one given but has all computed -// attribute values forced to null. -// -// The input value must conform to the schema's implied type, and the return -// value is guaranteed to conform to it. -func setElementCompareValueFromObject(schema *configschema.Object, v cty.Value) cty.Value { - if v.IsNull() || !v.IsKnown() { - return v - } - attrs := map[string]cty.Value{} - - for name, attr := range schema.Attributes { - attrV := v.GetAttr(name) - switch { - case attr.Computed: - attrs[name] = cty.NullVal(attr.Type) - default: - attrs[name] = attrV + if !attr.Computed { + foundNonComputedAttr = true + return false, nil } + return true, nil + }) + + return foundNonComputedAttr +} + +// validPriorFromConfig returns true if the prior object could have been +// derived from the configuration. We do this by walking the prior value to +// determine if it is a valid superset of the config, and only computable +// values have been added. This function is only used to correlated +// configuration with possible valid prior values within sets. +func validPriorFromConfig(schema nestedSchema, prior, config cty.Value) bool { + if unrefinedValue(config).RawEquals(unrefinedValue(prior)) { + return true } - return cty.ObjectVal(attrs) + // error value to halt the walk + stop := errors.New("stop") + + valid := true + cty.Walk(prior, func(path cty.Path, priorV cty.Value) (bool, error) { + configV, err := path.Apply(config) + if err != nil { + // most likely dynamic objects with different types + valid = false + return false, stop + } + + // we don't need to know the schema if both are equal + if unrefinedValue(configV).RawEquals(unrefinedValue(priorV)) { + // we know they are equal, so no need to descend further + return false, nil + } + + // We can't descend into nested sets to correlate configuration, so the + // overall values must be equal. + if configV.Type().IsSetType() { + valid = false + return false, stop + } + + attr := schema.AttributeByPath(path) + if attr == nil { + // Not at a schema attribute, so we can continue until we find leaf + // attributes. + return true, nil + } + + // If we have nested object attributes we'll be descending into those + // to compare the individual values and determine why this level is not + // equal + if attr.NestedType != nil { + return true, nil + } + + // This is a leaf attribute, so it must be computed in order to differ + // from config. + if !attr.Computed { + valid = false + return false, stop + } + + // And if it is computed, the config must be null to allow a change. + if !configV.IsNull() { + valid = false + return false, stop + } + + // We sill stop here. The cty value could be far larger, but this was + // the last level of prescribed schema. + return false, nil + }) + + return valid } diff --git a/internal/plans/objchange/objchange_test.go b/internal/plans/objchange/objchange_test.go index 4c434d3e42..827f8bea55 100644 --- a/internal/plans/objchange/objchange_test.go +++ b/internal/plans/objchange/objchange_test.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( "testing" - "github.com/apparentlymart/go-dump/dump" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -353,6 +357,120 @@ func TestProposedNew(t *testing.T) { }), }), }, + "prior nested single to null": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "foo": { + Nesting: configschema.NestingSingle, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "baz": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + "bleep": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("beep"), + "baz": cty.StringVal("boop"), + }), + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.Object(map[string]cty.Type{ + "bar": cty.String, + "baz": cty.String, + })), + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.Object(map[string]cty.Type{ + "bar": cty.String, + "baz": cty.String, + })), + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + }), + }, + + "prior optional computed nested single to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Required: true, + }, + "bleep": { + Type: cty.String, + Optional: true, + }, + }, + }, + Optional: true, + Computed: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + }), + }, + "prior nested list": { &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ @@ -638,6 +756,120 @@ func TestProposedNew(t *testing.T) { }), }), }, + + "prior optional computed nested map elem to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Optional: true, + }, + "bleep": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Optional: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.StringVal("computed"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + "bleep": cty.StringVal("computed"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + "c": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + "bleep": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + })), + "c": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + "bleep": cty.NullVal(cty.String), + }), + }), + }), + }, + + "prior optional computed nested map to null": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "bloop": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "blop": { + Type: cty.String, + Optional: true, + }, + "bleep": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + Optional: true, + Computed: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("glub"), + "bleep": cty.StringVal("computed"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "blop": cty.StringVal("blub"), + "bleep": cty.StringVal("computed"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Map( + cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + }), + )), + }), + cty.ObjectVal(map[string]cty.Value{ + "bloop": cty.NullVal(cty.Map( + cty.Object(map[string]cty.Type{ + "blop": cty.String, + "bleep": cty.String, + }), + )), + }), + }, + "prior nested map with dynamic": { &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ @@ -722,7 +954,7 @@ func TestProposedNew(t *testing.T) { }), "c": cty.ObjectVal(map[string]cty.Value{ "bar": cty.StringVal("bosh"), - "baz": cty.NullVal(cty.List(cty.String)), + "baz": cty.NullVal(cty.DynamicPseudoType), }), }), "bloop": cty.ObjectVal(map[string]cty.Value{ @@ -844,6 +1076,128 @@ func TestProposedNew(t *testing.T) { }), }), }, + + "set with partial optional computed change": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "multi": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + }, + "cmp": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "cmp": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "cmp": cty.StringVal("OK"), + }), + }), + }), + + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "cmp": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("replaced"), + "cmp": cty.NullVal(cty.String), + }), + }), + }), + // "one" can be correlated because it is a non-computed value in + // the configuration. + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "cmp": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("replaced"), + "cmp": cty.NullVal(cty.String), + }), + }), + }), + }, + + "set without partial optional computed change": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "multi": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "req": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "req": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "req": cty.StringVal("two"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.NullVal(cty.String), + "req": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.NullVal(cty.String), + "req": cty.StringVal("two"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "req": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "req": cty.StringVal("two"), + }), + }), + }), + }, + "sets differing only by unknown": { &configschema.Block{ BlockTypes: map[string]*configschema.NestedBlock{ @@ -1307,12 +1661,14 @@ func TestProposedNew(t *testing.T) { }), }), cty.ObjectVal(map[string]cty.Value{ - "bar": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ - "optional": cty.StringVal("other_prior"), - "computed": cty.StringVal("other_prior"), - "optional_computed": cty.StringVal("other_prior"), - "required": cty.StringVal("other_prior"), - })}), + "bar": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("other_prior"), + "computed": cty.StringVal("other_prior"), + "optional_computed": cty.StringVal("other_prior"), + "required": cty.StringVal("other_prior"), + }), + }), }), }), }), @@ -1564,7 +1920,7 @@ func TestProposedNew(t *testing.T) { Attributes: map[string]*configschema.Attribute{ "map": { NestedType: &configschema.Object{ - Nesting: configschema.NestingList, + Nesting: configschema.NestingMap, Attributes: map[string]*configschema.Attribute{ "foo": { Type: cty.String, @@ -1586,7 +1942,7 @@ func TestProposedNew(t *testing.T) { })), }), "map": cty.Map(cty.Object(map[string]cty.Type{ - "list": cty.List(cty.Object(map[string]cty.Type{ + "map": cty.List(cty.Object(map[string]cty.Type{ "foo": cty.String, })), })), @@ -1604,11 +1960,11 @@ func TestProposedNew(t *testing.T) { }), "map": cty.MapVal(map[string]cty.Value{ "one": cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("a"), }), - cty.ObjectVal(map[string]cty.Value{ + "two": cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("b"), }), }), @@ -1628,11 +1984,11 @@ func TestProposedNew(t *testing.T) { }), "map": cty.MapVal(map[string]cty.Value{ "one": cty.ObjectVal(map[string]cty.Value{ - "list": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("a"), }), - cty.ObjectVal(map[string]cty.Value{ + "two": cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("b"), }), }), @@ -1641,7 +1997,9 @@ func TestProposedNew(t *testing.T) { }), }, - // data sources are planned with an unknown value + // Data sources are planned with an unknown value. + // Note that this plan fails AssertPlanValid, because for managed + // resources an instance would never be completely unknown. "unknown prior nested objects": { &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -1667,34 +2025,850 @@ func TestProposedNew(t *testing.T) { }, }, cty.UnknownVal(cty.Object(map[string]cty.Type{ - "List": cty.List(cty.Object(map[string]cty.Type{ + "list": cty.List(cty.Object(map[string]cty.Type{ "list": cty.List(cty.Object(map[string]cty.Type{ "foo": cty.String, })), })), })), cty.NullVal(cty.Object(map[string]cty.Type{ - "List": cty.List(cty.Object(map[string]cty.Type{ + "list": cty.List(cty.Object(map[string]cty.Type{ "list": cty.List(cty.Object(map[string]cty.Type{ "foo": cty.String, })), })), })), cty.UnknownVal(cty.Object(map[string]cty.Type{ - "List": cty.List(cty.Object(map[string]cty.Type{ + "list": cty.List(cty.Object(map[string]cty.Type{ "list": cty.List(cty.Object(map[string]cty.Type{ "foo": cty.String, })), })), })), }, + + // A nested object with computed attributes, which is contained in an + // optional+computed container. The nested computed values should be + // represented in the proposed new object. + "config within optional+computed": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "list_obj": { + Optional: true, + Computed: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "obj": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "list_obj": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("prior"), + "computed": cty.StringVal("prior computed"), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "list_obj": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("prior"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "list_obj": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("prior"), + "computed": cty.StringVal("prior computed"), + }), + }), + }), + }), + }, + + // A nested object with computed attributes, which is contained in an + // optional+computed container. The prior nested object contains values + // which could not be computed, therefor the proposed new value must be + // the null value from the configuration. + "computed within optional+computed": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "list_obj": { + Optional: true, + Computed: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "obj": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "list_obj": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("prior"), + "computed": cty.StringVal("prior computed"), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "list_obj": cty.NullVal(cty.List( + cty.Object(map[string]cty.Type{ + "obj": cty.Object(map[string]cty.Type{ + "optional": cty.String, + "computed": cty.String, + }), + }), + )), + }), + cty.ObjectVal(map[string]cty.Value{ + "list_obj": cty.NullVal(cty.List( + cty.Object(map[string]cty.Type{ + "obj": cty.Object(map[string]cty.Type{ + "optional": cty.String, + "computed": cty.String, + }), + }), + )), + }), + }, + + // A nested object with computed attributes, which is contained in an + // optional+computed set. The nested computed values should be + // represented in the proposed new object, and correlated with state + // via the non-computed attributes. + "config add within optional+computed set": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "set_obj": { + Optional: true, + Computed: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "obj": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.StringVal("first computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.StringVal("second computed"), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("third"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.StringVal("first computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.StringVal("second computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("third"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + }, + + // A nested object with computed attributes, which is contained in a + // set. The nested computed values should be represented in the + // proposed new object, and correlated with state via the non-computed + // attributes. + "config add within set block": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "set_obj": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "obj": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Optional: true, Computed: true}, + }, + }, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.StringVal("first computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.StringVal("second from config"), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.StringVal("second from config"), + }), + }), + // new "third" value added + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("third"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.StringVal("first computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.StringVal("second from config"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("third"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + }, + + // A nested object with computed attributes, which is contained in a + // set. The nested computed values should be represented in the + // proposed new object, and correlated with state via the non-computed + // attributes. + "config change within set block": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "set_obj": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "obj": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Optional: true, Computed: true}, + }, + }, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.StringVal("first computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("second"), + "computed": cty.StringVal("second computed"), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("changed"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set_obj": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("first"), + "computed": cty.StringVal("first computed"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "obj": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("changed"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + }), + }, + + "set attr with partial optional computed change": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "multi": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + }, + "oc": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("replaced"), + "oc": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("replaced"), + "oc": cty.NullVal(cty.String), + }), + }), + }), + }, + + "set attr without optional computed change": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "multi": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + }, + "oc": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + }), + }), + }), + }, + + "set attr with all optional computed": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "multi": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "oc": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + }), + }), + }), + // Each of these values can be correlated by the existence of the + // optional config attribute. Because "one" and "two" are set in + // the config, they must exist in the state regardless of + // optional&computed. + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + }), + }), + }), + }, + + "set block with all optional computed and nested object types": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "multi": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "oc": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "attr": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "opt": { + Type: cty.String, + Optional: true, + Computed: true, + }, + "oc": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + "attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + })}), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + "attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + })}), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.NullVal(cty.String), + "attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + })}), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + "attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.NullVal(cty.String), + })}), + }), + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("three"), + "oc": cty.NullVal(cty.String), + "attr": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "opt": cty.String, + "oc": cty.String, + }))), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "multi": cty.SetVal([]cty.Value{ + // We can correlate this with prior from the outer object + // attributes, and the equal nested set. + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + "attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("one"), + "oc": cty.StringVal("OK"), + })}), + }), + // This value is overridden by config, because we can't + // correlate optional+computed config values within nested + // sets. + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.StringVal("OK"), + "attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("two"), + "oc": cty.NullVal(cty.String), + })}), + }), + // This value was taken only from config + cty.ObjectVal(map[string]cty.Value{ + "opt": cty.StringVal("three"), + "oc": cty.NullVal(cty.String), + "attr": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "opt": cty.String, + "oc": cty.String, + }))), + }), + }), + }), + }, + "planned data source": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "single_computed_obj": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "computed": {Type: cty.String, Computed: true}, + }, + }, + Computed: true, + }, + "single": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + Optional: true, + }, + "map": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + Optional: true, + }, + "list": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + Optional: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "list_block": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "optional": {Type: cty.String, Optional: true}, + "computed": {Type: cty.String, Computed: true}, + }, + }, + }, + }, + }, + // planning a data resousrce starts with an uknown prior to fill in + // all posible computed attributes + cty.UnknownVal(cty.Object(map[string]cty.Type{ + "single_computed_obj": cty.Object(map[string]cty.Type{ + "computed": cty.String, + }), + "single": cty.Object(map[string]cty.Type{ + "optional": cty.String, + "computed": cty.String, + }), + "map": cty.Map(cty.Object(map[string]cty.Type{ + "optional": cty.String, + "computed": cty.String, + })), + "list": cty.List(cty.Object(map[string]cty.Type{ + "optional": cty.String, + "computed": cty.String, + })), + "list_block": cty.List(cty.Object(map[string]cty.Type{ + "optional": cty.String, + "computed": cty.String, + })), + })), + cty.ObjectVal(map[string]cty.Value{ + "single_computed_obj": cty.NullVal(cty.Object(map[string]cty.Type{ + "computed": cty.String, + })), + "single": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.NullVal(cty.String), + }), + "map": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.NullVal(cty.String), + }), + }), + "list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.NullVal(cty.String), + }), + }), + "list_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "single_computed_obj": cty.UnknownVal(cty.Object(map[string]cty.Type{ + "computed": cty.String, + })), + "single": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.UnknownVal(cty.String), + }), + "map": cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.UnknownVal(cty.String), + }), + }), + "list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.UnknownVal(cty.String), + }), + }), + "list_block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "optional": cty.StringVal("config"), + "computed": cty.UnknownVal(cty.String), + }), + }), + }), + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { got := ProposedNew(test.Schema, test.Prior, test.Config) - if !got.RawEquals(test.Want) { - t.Errorf("wrong result\ngot: %swant: %s", dump.Value(got), dump.Value(test.Want)) + if diff := cmp.Diff(test.Want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) } }) } diff --git a/internal/plans/objchange/plan_valid.go b/internal/plans/objchange/plan_valid.go index 1977ea7dd9..721fca7700 100644 --- a/internal/plans/objchange/plan_valid.go +++ b/internal/plans/objchange/plan_valid.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( @@ -101,6 +104,14 @@ func assertPlanValid(schema *configschema.Block, priorState, config, plannedStat continue } + if configV.IsNull() { + // Configuration cannot decode a block into a null value, but + // we could be dealing with a null returned by a legacy + // provider and inserted via ignore_changes. Fix the value in + // place so the length can still be compared. + configV = cty.ListValEmpty(configV.Type().ElementType()) + } + plannedL := plannedV.LengthInt() configL := configV.LengthInt() if plannedL != configL { @@ -245,12 +256,31 @@ func assertPlannedAttrsValid(schema map[string]*configschema.Attribute, priorSta } func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorState, config, plannedState cty.Value, path cty.Path) []error { - plannedV := plannedState.GetAttr(name) - configV := config.GetAttr(name) - priorV := cty.NullVal(attrS.Type) + // any of the config, prior or planned values may be null at this point if + // we are in nested structural attributes. + var plannedV, configV, priorV cty.Value + if attrS.NestedType != nil { + configV = cty.NullVal(attrS.NestedType.ImpliedType()) + priorV = cty.NullVal(attrS.NestedType.ImpliedType()) + plannedV = cty.NullVal(attrS.NestedType.ImpliedType()) + } else { + configV = cty.NullVal(attrS.Type) + priorV = cty.NullVal(attrS.Type) + plannedV = cty.NullVal(attrS.Type) + } + + if !config.IsNull() { + configV = config.GetAttr(name) + } + if !priorState.IsNull() { priorV = priorState.GetAttr(name) } + + if !plannedState.IsNull() { + plannedV = plannedState.GetAttr(name) + } + path = append(path, cty.GetAttrStep{Name: name}) return assertPlannedValueValid(attrS, priorV, configV, plannedV, path) @@ -259,11 +289,11 @@ func assertPlannedAttrValid(name string, attrS *configschema.Attribute, priorSta func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, plannedV cty.Value, path cty.Path) []error { var errs []error - if plannedV.RawEquals(configV) { + if unrefinedValue(plannedV).RawEquals(unrefinedValue(configV)) { // This is the easy path: provider didn't change anything at all. return errs } - if plannedV.RawEquals(priorV) && !priorV.IsNull() && !configV.IsNull() { + if unrefinedValue(plannedV).RawEquals(unrefinedValue(priorV)) && !priorV.IsNull() && !configV.IsNull() { // Also pretty easy: there is a prior value and the provider has // returned it unchanged. This indicates that configV and plannedV // are functionally equivalent and so the provider wishes to disregard @@ -271,6 +301,16 @@ func assertPlannedValueValid(attrS *configschema.Attribute, priorV, configV, pla return errs } + if attrS.WriteOnly { + // The provider is not allowed to return non-null values for write-only attributes + if !plannedV.IsNull() { + errs = append(errs, path.NewErrorf("planned value for write-only attribute is not null")) + } + + // We don't want to evaluate further if the attribute is write-only and null + return errs + } + switch { // The provider can plan any value for a computed-only attribute. There may // be a config value here in the case where a user used `ignore_changes` on @@ -331,6 +371,11 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne errs = append(errs, path.NewErrorf("planned for existence but config wants absence")) return errs } + if !config.IsNull() && !planned.IsKnown() { + errs = append(errs, path.NewErrorf("planned unknown for configured value")) + return errs + } + if planned.IsNull() { // No further checks possible if the planned value is null return errs @@ -347,10 +392,17 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne // both support a similar-enough API that we can treat them the // same for our purposes here. - plannedL := planned.LengthInt() - configL := config.LengthInt() - if plannedL != configL { - errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) + plannedL := planned.Length() + configL := config.Length() + + // config wasn't known, then planned should be unknown too + if !plannedL.IsKnown() && !configL.IsKnown() { + return errs + } + + lenEqual := plannedL.Equals(configL) + if !lenEqual.IsKnown() || lenEqual.False() { + errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL)) return errs } for it := planned.ElementIterator(); it.Next(); { @@ -377,6 +429,20 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne configVals := map[string]cty.Value{} priorVals := map[string]cty.Value{} + plannedL := planned.Length() + configL := config.Length() + + // config wasn't known, then planned should be unknown too + if !plannedL.IsKnown() && !configL.IsKnown() { + return errs + } + + lenEqual := plannedL.Equals(configL) + if !lenEqual.IsKnown() || lenEqual.False() { + errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL)) + return errs + } + if !planned.IsNull() { plannedVals = planned.AsValueMap() } @@ -410,10 +476,11 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne } case configschema.NestingSet: - plannedL := planned.LengthInt() - configL := config.LengthInt() - if plannedL != configL { - errs = append(errs, path.NewErrorf("count in plan (%d) disagrees with count in config (%d)", plannedL, configL)) + plannedL := planned.Length() + configL := config.Length() + + if ok := plannedL.Range().Includes(configL); ok.IsKnown() && ok.False() { + errs = append(errs, path.NewErrorf("count in plan (%#v) disagrees with count in config (%#v)", plannedL, configL)) return errs } // Because set elements have no identifier with which to correlate @@ -424,3 +491,22 @@ func assertPlannedObjectValid(schema *configschema.Object, prior, config, planne return errs } + +// unrefinedValue returns the given value with any unknown value refinements +// stripped away, making it a basic unknown value with only a type constraint. +// +// This function also considers unknown values nested inside a known container +// such as a collection, which unfortunately makes it relatively expensive +// for large data structures. Over time we should transition away from using +// this trick and prefer to use cty's Equals and value range APIs instead of +// of using Value.RawEquals, which is primarily intended for unit test code +// rather than real application use. +func unrefinedValue(v cty.Value) cty.Value { + ret, _ := cty.Transform(v, func(p cty.Path, v cty.Value) (cty.Value, error) { + if !v.IsKnown() { + return cty.UnknownVal(v.Type()), nil + } + return v, nil + }) + return ret +} diff --git a/internal/plans/objchange/plan_valid_test.go b/internal/plans/objchange/plan_valid_test.go index 8ba1927eec..6144608694 100644 --- a/internal/plans/objchange/plan_valid_test.go +++ b/internal/plans/objchange/plan_valid_test.go @@ -1,12 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package objchange import ( "testing" - "github.com/apparentlymart/go-dump/dump" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -389,6 +393,41 @@ func TestAssertPlanValid(t *testing.T) { }, }, + // but don't panic on a null list just in case + "nested list, null in config": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "b": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "c": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "b": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "c": cty.String, + })), + }), + cty.ObjectVal(map[string]cty.Value{ + "b": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "c": cty.String, + }))), + }), + cty.ObjectVal(map[string]cty.Value{ + "b": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "c": cty.String, + })), + }), + nil, + }, + // blocks can be unknown when using dynamic "nested list, unknown nested dynamic": { &configschema.Block{ @@ -1472,6 +1511,7 @@ func TestAssertPlanValid(t *testing.T) { // When an object has dynamic attrs, the map may be // handled as an object. "map_as_obj": { + Optional: true, NestedType: &configschema.Object{ Nesting: configschema.NestingMap, Attributes: map[string]*configschema.Attribute{ @@ -1484,6 +1524,7 @@ func TestAssertPlanValid(t *testing.T) { }, }, "list": { + Optional: true, NestedType: &configschema.Object{ Nesting: configschema.NestingList, Attributes: map[string]*configschema.Attribute{ @@ -1548,11 +1589,23 @@ func TestAssertPlanValid(t *testing.T) { "one": cty.ObjectVal(map[string]cty.Value{ "name": cty.NullVal(cty.DynamicPseudoType), }), + "two": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.DynamicPseudoType, + })), + "three": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.DynamicPseudoType, + })), }), "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "name": cty.NullVal(cty.String), }), + cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), }), "set": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -1573,11 +1626,26 @@ func TestAssertPlanValid(t *testing.T) { "one": cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("computed"), }), + // The config was null, but some providers may return a + // non-null object here, so we need to accept this for + // compatibility. + "two": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + }), + "three": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.DynamicPseudoType, + })), }), "list": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "name": cty.StringVal("computed"), }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + }), + cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), }), "set": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ @@ -1654,6 +1722,311 @@ func TestAssertPlanValid(t *testing.T) { }), nil, }, + + // When validating collections we start by comparing length, which + // requires guarding for any unknown values incorrectly returned by the + // provider. + "nested collection attrs planned unknown": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "set": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Computed: true, + Optional: true, + }, + }, + }, + }, + "list": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Computed: true, + Optional: true, + }, + }, + }, + }, + "map": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "set": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("from_config"), + }), + }), + "list": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("from_config"), + }), + }), + "map": cty.MapVal(map[string]cty.Value{ + "key": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("from_config"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("from_config"), + }), + }), + "list": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("from_config"), + }), + }), + "map": cty.MapVal(map[string]cty.Value{ + "key": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("from_config"), + }), + }), + }), + // provider cannot override the config + cty.ObjectVal(map[string]cty.Value{ + "set": cty.UnknownVal(cty.Set( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + )), + "list": cty.UnknownVal(cty.Set( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + )), + "map": cty.UnknownVal(cty.Map( + cty.Object(map[string]cty.Type{ + "name": cty.String, + }), + )), + }), + []string{ + `.set: planned unknown for configured value`, + `.list: planned unknown for configured value`, + `.map: planned unknown for configured value`, + }, + }, + + "refined unknown values can become less refined": { + // Providers often can't preserve refinements through the provider + // wire protocol: although we do have a defined serialization for + // it, most providers were written before there was any such + // thing as refinements, and in future there might be new + // refinements that even refinement-aware providers don't know + // how to preserve, so we allow them to get dropped here as + // a concession to backward-compatibility. + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": { + Type: cty.String, + Required: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("old"), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.UnknownVal(cty.String).RefineNotNull(), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.UnknownVal(cty.String), + }), + nil, + }, + + "refined unknown values in collection elements can become less refined": { + // Providers often can't preserve refinements through the provider + // wire protocol: although we do have a defined serialization for + // it, most providers were written before there was any such + // thing as refinements, and in future there might be new + // refinements that even refinement-aware providers don't know + // how to preserve, so we allow them to get dropped here as + // a concession to backward-compatibility. + // + // This is intending to approximate something like this: + // + // resource "null_resource" "hello" { + // triggers = { + // key = uuid() + // } + // } + // + // ...under the assumption that the null_resource implementation + // cannot preserve the not-null refinement that the uuid function + // generates. + // + // https://github.com/hashicorp/terraform/issues/33385 + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "m": { + Type: cty.Map(cty.String), + }, + }, + }, + cty.NullVal(cty.Object(map[string]cty.Type{ + "m": cty.Map(cty.String), + })), + cty.ObjectVal(map[string]cty.Value{ + "m": cty.MapVal(map[string]cty.Value{ + "key": cty.UnknownVal(cty.String).RefineNotNull(), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "m": cty.MapVal(map[string]cty.Value{ + "key": cty.UnknownVal(cty.String), + }), + }), + nil, + }, + + "nested set values can contain computed unknown": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "set": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "input": { + Type: cty.String, + Optional: true, + }, + "computed": { + Type: cty.String, + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "set": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("a"), + "computed": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("b"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "set": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("a"), + "computed": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("b"), + "computed": cty.NullVal(cty.String), + }), + }), + }), + // Plan can mark the null computed values as unknown + cty.ObjectVal(map[string]cty.Value{ + "set": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("a"), + "computed": cty.UnknownVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("b"), + "computed": cty.UnknownVal(cty.String), + }), + }), + }), + []string{}, + }, + + "write-only attributes": { + &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + WriteOnly: true, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.String), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("write-only").Mark(marks.Ephemeral), + }), + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.String), + }), + []string{}, + }, + + "nested write-only attributes": { + &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "nested": { + Nesting: configschema.NestingList, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + WriteOnly: true, + }, + }, + }, + }, + }, + }, + cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("write-only").Mark(marks.Ephemeral), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "foo": cty.NullVal(cty.String), + }), + }), + }), + []string{}, + }, } for name, test := range tests { @@ -1671,9 +2044,9 @@ func TestAssertPlanValid(t *testing.T) { t.Logf( "\nprior: %sconfig: %splanned: %s", - dump.Value(test.Planned), - dump.Value(test.Config), - dump.Value(test.Planned), + ctydebug.ValueString(test.Prior), + ctydebug.ValueString(test.Config), + ctydebug.ValueString(test.Planned), ) for msg := range wantErrs { if _, ok := gotErrs[msg]; !ok { diff --git a/internal/plans/plan.go b/internal/plans/plan.go index 98f462390c..c6b03bd356 100644 --- a/internal/plans/plan.go +++ b/internal/plans/plan.go @@ -1,13 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( "sort" + "time" + + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/globalref" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) // Plan is the top-level type representing a planned set of changes. @@ -28,15 +36,71 @@ type Plan struct { // to the end-user, and so it must not be used to influence apply-time // behavior. The actions during apply must be described entirely by // the Changes field, regardless of how the plan was created. + // + // FIXME: destroy operations still rely on DestroyMode being set, because + // there is no other source of this information in the plan. New behavior + // should not be added based on this flag, and changing the flag should be + // checked carefully against existing destroy behaviors. UIMode Mode - VariableValues map[string]DynamicValue - Changes *Changes + // VariableValues, VariableMarks, and ApplyTimeVariables together describe + // how Terraform should decide the input variable values for the apply + // phase if this plan is to be applied. + // + // VariableValues and VariableMarks describe persisted (non-ephemeral) + // values that were set as part of the planning options and are to be + // re-used during the apply phase. VariableValues can potentially contain + // unknown values for a speculative plan, but the variable values must + // all be known for a plan that will subsequently be applied. + // + // ApplyTimeVariables retains the names of any ephemeral variables that were + // set (non-null) during the planning phase and must therefore be + // re-supplied by the caller (potentially with different values) during + // the apply phase. Ephemeral input variables are intended for populating + // arguments for other ephemeral objects in the configuration, such as + // provider configurations. Although the values for these variables can + // change between plan and apply, their "nullness" may not. + VariableValues map[string]DynamicValue + VariableMarks map[string][]cty.PathValueMarks + ApplyTimeVariables collections.Set[string] + + Changes *ChangesSrc DriftedResources []*ResourceInstanceChangeSrc + DeferredResources []*DeferredResourceInstanceChangeSrc TargetAddrs []addrs.Targetable ForceReplaceAddrs []addrs.AbsResourceInstance Backend Backend + // Complete is true if Terraform considers this to be a "complete" plan, + // which is to say that it includes a planned action (even if no-op) + // for every resource instance object that was mentioned across both + // the desired state and prior state. + // + // If Complete is false then the plan might still be applyable (check + // [Plan.Applyable]) but after applying it the operator should be reminded + // to plan and apply again to hopefully make more progress towards + // convergence. + // + // For an incomplete plan, other fields of this type may give more context + // about why the plan is incomplete, which a UI layer could present to + // the user as part of a warning that the plan is incomplete. + Complete bool + + // Applyable is true if both Terraform was able to create a plan + // successfully and if the plan calls for making some sort of meaningful + // change. + // + // If [Plan.Errored] is also set then that means the plan is non-applyable + // due to an error. If not then the plan was created successfully but found + // no material differences between desired and prior state, and so + // applying this plan would achieve nothing. + Applyable bool + + // Errored is true if the Changes information is incomplete because + // the planning operation failed. An errored plan cannot be applied, + // but can be cautiously inspected for debugging purposes. + Errored bool + // Checks captures a snapshot of the (probably-incomplete) check results // at the end of the planning process. // @@ -73,46 +137,32 @@ type Plan struct { // order to report to the user any out-of-band changes we've detected. PrevRunState *states.State PriorState *states.State -} -// CanApply returns true if and only if the recieving plan includes content -// that would make sense to apply. If it returns false, the plan operation -// should indicate that there's nothing to do and Terraform should exit -// without prompting the user to confirm the changes. -// -// This function represents our main business logic for making the decision -// about whether a given plan represents meaningful "changes", and so its -// exact definition may change over time; the intent is just to centralize the -// rules for that rather than duplicating different versions of it at various -// locations in the UI code. -func (p *Plan) CanApply() bool { - switch { - case !p.Changes.Empty(): - // "Empty" means that everything in the changes is a "NoOp", so if - // not empty then there's at least one non-NoOp change. - return true + // ExternalReferences are references that are being made to resources within + // the plan from external sources. + // + // This is never recorded outside of Terraform. It is not written into the + // binary plan file, and it is not written into the JSON structured outputs. + // The testing framework never writes the plans out but holds everything in + // memory as it executes, so there is no need to add any kind of + // serialization for this field. This does mean that you shouldn't rely on + // this field existing unless you have just generated the plan. + ExternalReferences []*addrs.Reference - case !p.PriorState.ManagedResourcesEqual(p.PrevRunState): - // If there are no changes planned but we detected some - // outside-Terraform changes while refreshing then we consider - // that applyable in isolation only if this was a refresh-only - // plan where we expect updating the state to include these - // changes was the intended goal. - // - // (We don't treat a "refresh only" plan as applyable in normal - // planning mode because historically the refresh result wasn't - // considered part of a plan at all, and so it would be - // a disruptive breaking change if refreshing alone suddenly - // became applyable in the normal case and an existing configuration - // was relying on ignore_changes in order to be convergent in spite - // of intentional out-of-band operations.) - return p.UIMode == RefreshOnlyMode + // Overrides contains the set of overrides that were applied while making + // this plan. We need to provide the same set of overrides when applying + // the plan so we preserve them here. As with ExternalReferences, this is + // only used by the testing framework and so isn't written into any external + // representation of the plan. + Overrides *mocking.Overrides - default: - // Otherwise, there are either no changes to apply or they are changes - // our cases above don't consider as worthy of applying in isolation. - return false - } + // Timestamp is the record of truth for when the plan happened. + Timestamp time.Time + + // FunctionResults stores hashed results from all providers function calls + // and builtin calls which may access external state so that calls during + // apply can be checked for consistency. + FunctionResults []lang.FunctionResultHash } // ProviderAddrs returns a list of all of the provider configuration addresses diff --git a/internal/plans/plan_test.go b/internal/plans/plan_test.go index 34f9361394..2897afbf62 100644 --- a/internal/plans/plan_test.go +++ b/internal/plans/plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plans import ( @@ -12,7 +15,7 @@ func TestProviderAddrs(t *testing.T) { plan := &Plan{ VariableValues: map[string]DynamicValue{}, - Changes: &Changes{ + Changes: &ChangesSrc{ Resources: []*ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ @@ -71,7 +74,7 @@ func TestProviderAddrs(t *testing.T) { // Module outputs should not effect the result of Empty func TestModuleOutputChangesEmpty(t *testing.T) { - changes := &Changes{ + changes := &ChangesSrc{ Outputs: []*OutputChangeSrc{ { Addr: addrs.AbsOutputValue{ diff --git a/internal/plans/planfile/config_snapshot.go b/internal/plans/planfile/config_snapshot.go index 163366d9bc..5f4256a627 100644 --- a/internal/plans/planfile/config_snapshot.go +++ b/internal/plans/planfile/config_snapshot.go @@ -1,12 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( "archive/zip" "encoding/json" "fmt" - "io/ioutil" + "io" + "maps" "path" - "sort" + "slices" "strings" "time" @@ -52,7 +56,7 @@ func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) { if err != nil { return nil, fmt.Errorf("failed to open module manifest: %s", r) } - manifestSrc, err = ioutil.ReadAll(r) + manifestSrc, err = io.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read module manifest: %s", r) } @@ -74,7 +78,7 @@ func readConfigSnapshot(z *zip.Reader) (*configload.Snapshot, error) { if err != nil { return nil, fmt.Errorf("failed to open snapshot of %s from module %q: %s", fileName, moduleKey, err) } - fileSrc, err := ioutil.ReadAll(r) + fileSrc, err := io.ReadAll(r) if err != nil { return nil, fmt.Errorf("failed to read snapshot of %s from module %q: %s", fileName, moduleKey, err) } @@ -155,15 +159,10 @@ func writeConfigSnapshot(snap *configload.Snapshot, z *zip.Writer) error { // need to be user-actionable. var manifest configSnapshotModuleManifest - keys := make([]string, 0, len(snap.Modules)) - for k := range snap.Modules { - keys = append(keys, k) - } - sort.Strings(keys) // We'll re-use this fileheader for each Create we do below. - for _, k := range keys { + for _, k := range slices.Sorted(maps.Keys(snap.Modules)) { snapMod := snap.Modules[k] record := configSnapshotModuleRecord{ Dir: snapMod.Dir, diff --git a/internal/plans/planfile/config_snapshot_test.go b/internal/plans/planfile/config_snapshot_test.go index 91069ea38c..3d42838362 100644 --- a/internal/plans/planfile/config_snapshot_test.go +++ b/internal/plans/planfile/config_snapshot_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( diff --git a/internal/plans/planfile/doc.go b/internal/plans/planfile/doc.go index edd16af2b8..50bd74d6a4 100644 --- a/internal/plans/planfile/doc.go +++ b/internal/plans/planfile/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package planfile deals with the file format used to serialize plans to disk // and then deserialize them back into memory later. // diff --git a/internal/plans/planfile/planfile_test.go b/internal/plans/planfile/planfile_test.go index af3615cfcf..b2819be8d8 100644 --- a/internal/plans/planfile/planfile_test.go +++ b/internal/plans/planfile/planfile_test.go @@ -1,15 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( "path/filepath" + "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/depsfile" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" @@ -49,11 +54,12 @@ func TestRoundtrip(t *testing.T) { // Minimal plan too, since the serialization of the tfplan portion of the // file is tested more fully in tfplan_test.go . planIn := &plans.Plan{ - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{}, Outputs: []*plans.OutputChangeSrc{}, }, - DriftedResources: []*plans.ResourceInstanceChangeSrc{}, + DriftedResources: []*plans.ResourceInstanceChangeSrc{}, + DeferredResources: []*plans.DeferredResourceInstanceChangeSrc{}, VariableValues: map[string]plans.DynamicValue{ "foo": plans.DynamicValue([]byte("foo placeholder")), }, @@ -77,10 +83,10 @@ func TestRoundtrip(t *testing.T) { locksIn := depsfile.NewLocks() locksIn.SetProvider( addrs.NewDefaultProvider("boop"), - getproviders.MustParseVersion("1.0.0"), - getproviders.MustParseVersionConstraints(">= 1.0.0"), - []getproviders.Hash{ - getproviders.MustParseHash("fake:hello"), + providerreqs.MustParseVersion("1.0.0"), + providerreqs.MustParseVersionConstraints(">= 1.0.0"), + []providerreqs.Hash{ + providerreqs.MustParseHash("fake:hello"), }, ) @@ -97,17 +103,24 @@ func TestRoundtrip(t *testing.T) { t.Fatalf("failed to create plan file: %s", err) } - pr, err := Open(planFn) + wpf, err := OpenWrapped(planFn) if err != nil { t.Fatalf("failed to open plan file for reading: %s", err) } + pr, ok := wpf.Local() + if !ok { + t.Fatalf("failed to open plan file as a local plan file") + } + if wpf.IsCloud() { + t.Fatalf("wrapped plan claims to be both kinds of plan at once") + } t.Run("ReadPlan", func(t *testing.T) { planOut, err := pr.ReadPlan() if err != nil { t.Fatalf("failed to read plan: %s", err) } - if diff := cmp.Diff(planIn, planOut); diff != "" { + if diff := cmp.Diff(planIn, planOut, collections.CmpOptions); diff != "" { t.Errorf("plan did not survive round-trip\n%s", diff) } }) @@ -164,3 +177,33 @@ func TestRoundtrip(t *testing.T) { } }) } + +func TestWrappedError(t *testing.T) { + // Open something that isn't a cloud or local planfile: should error + wrongFile := "not a valid zip file" + _, err := OpenWrapped(filepath.Join("testdata", "test-config", "root.tf")) + if !strings.Contains(err.Error(), wrongFile) { + t.Fatalf("expected %q, got %q", wrongFile, err) + } + + // Open something that doesn't exist: should error + missingFile := "no such file or directory" + _, err = OpenWrapped(filepath.Join("testdata", "absent.tfplan")) + if !strings.Contains(err.Error(), missingFile) { + t.Fatalf("expected %q, got %q", missingFile, err) + } +} + +func TestWrappedCloud(t *testing.T) { + // Loading valid cloud plan results in a wrapped cloud plan + wpf, err := OpenWrapped(filepath.Join("testdata", "cloudplan.json")) + if err != nil { + t.Fatalf("failed to open valid cloud plan: %s", err) + } + if !wpf.IsCloud() { + t.Fatalf("failed to open cloud file as a cloud plan") + } + if wpf.IsLocal() { + t.Fatalf("wrapped plan claims to be both kinds of plan at once") + } +} diff --git a/internal/plans/planfile/reader.go b/internal/plans/planfile/reader.go index ff6e129e0b..f5aad59d11 100644 --- a/internal/plans/planfile/reader.go +++ b/internal/plans/planfile/reader.go @@ -1,10 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( "archive/zip" "bytes" "fmt" - "io/ioutil" + "io" + "os" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" @@ -18,6 +22,25 @@ const tfstateFilename = "tfstate" const tfstatePreviousFilename = "tfstate-prev" const dependencyLocksFilename = ".terraform.lock.hcl" // matches the conventional name in an input configuration +// ErrUnusableLocalPlan is an error wrapper to indicate that we *think* the +// input represents plan file data, but can't use it for some reason (as +// explained in the error text). Callers can check against this type with +// errors.As() if they need to distinguish between corrupt plan files and more +// fundamental problems like an empty file. +type ErrUnusableLocalPlan struct { + inner error +} + +func errUnusable(err error) *ErrUnusableLocalPlan { + return &ErrUnusableLocalPlan{inner: err} +} +func (e *ErrUnusableLocalPlan) Error() string { + return e.inner.Error() +} +func (e *ErrUnusableLocalPlan) Unwrap() error { + return e.inner +} + // Reader is the main type used to read plan files. Create a Reader by calling // Open. // @@ -28,16 +51,18 @@ type Reader struct { zip *zip.ReadCloser } -// Open creates a Reader for the file at the given filename, or returns an -// error if the file doesn't seem to be a planfile. +// Open creates a Reader for the file at the given filename, or returns an error +// if the file doesn't seem to be a planfile. NOTE: Most commands that accept a +// plan file should use OpenWrapped instead, so they can support both local and +// cloud plan files. func Open(filename string) (*Reader, error) { r, err := zip.OpenReader(filename) if err != nil { // To give a better error message, we'll sniff to see if this looks // like our old plan format from versions prior to 0.12. - if b, sErr := ioutil.ReadFile(filename); sErr == nil { + if b, sErr := os.ReadFile(filename); sErr == nil { if bytes.HasPrefix(b, []byte("tfplan")) { - return nil, fmt.Errorf("the given plan file was created by an earlier version of Terraform; plan files cannot be shared between different Terraform versions") + return nil, errUnusable(fmt.Errorf("the given plan file was created by an earlier version of Terraform; plan files cannot be shared between different Terraform versions")) } } return nil, err @@ -81,12 +106,12 @@ func (r *Reader) ReadPlan() (*plans.Plan, error) { if planFile == nil { // This should never happen because we checked for this file during // Open, but we'll check anyway to be safe. - return nil, fmt.Errorf("the plan file is invalid") + return nil, errUnusable(fmt.Errorf("the plan file is invalid")) } pr, err := planFile.Open() if err != nil { - return nil, fmt.Errorf("failed to retrieve plan from plan file: %s", err) + return nil, errUnusable(fmt.Errorf("failed to retrieve plan from plan file: %s", err)) } defer pr.Close() @@ -103,16 +128,16 @@ func (r *Reader) ReadPlan() (*plans.Plan, error) { // access the prior state (this and the ReadStateFile method). ret, err := readTfplan(pr) if err != nil { - return nil, err + return nil, errUnusable(err) } prevRunStateFile, err := r.ReadPrevStateFile() if err != nil { - return nil, fmt.Errorf("failed to read previous run state from plan file: %s", err) + return nil, errUnusable(fmt.Errorf("failed to read previous run state from plan file: %s", err)) } priorStateFile, err := r.ReadStateFile() if err != nil { - return nil, fmt.Errorf("failed to read prior state from plan file: %s", err) + return nil, errUnusable(fmt.Errorf("failed to read prior state from plan file: %s", err)) } ret.PrevRunState = prevRunStateFile.State @@ -131,12 +156,12 @@ func (r *Reader) ReadStateFile() (*statefile.File, error) { if file.Name == tfstateFilename { r, err := file.Open() if err != nil { - return nil, fmt.Errorf("failed to extract state from plan file: %s", err) + return nil, errUnusable(fmt.Errorf("failed to extract state from plan file: %s", err)) } return statefile.Read(r) } } - return nil, statefile.ErrNoState + return nil, errUnusable(statefile.ErrNoState) } // ReadPrevStateFile reads the previous state file embedded in the plan file, which @@ -149,12 +174,12 @@ func (r *Reader) ReadPrevStateFile() (*statefile.File, error) { if file.Name == tfstatePreviousFilename { r, err := file.Open() if err != nil { - return nil, fmt.Errorf("failed to extract previous state from plan file: %s", err) + return nil, errUnusable(fmt.Errorf("failed to extract previous state from plan file: %s", err)) } return statefile.Read(r) } } - return nil, statefile.ErrNoState + return nil, errUnusable(statefile.ErrNoState) } // ReadConfigSnapshot reads the configuration snapshot embedded in the plan @@ -212,7 +237,7 @@ func (r *Reader) ReadDependencyLocks() (*depsfile.Locks, tfdiags.Diagnostics) { )) return nil, diags } - src, err := ioutil.ReadAll(r) + src, err := io.ReadAll(r) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/plans/planfile/testdata/cloudplan.json b/internal/plans/planfile/testdata/cloudplan.json new file mode 100644 index 0000000000..0a1c73302a --- /dev/null +++ b/internal/plans/planfile/testdata/cloudplan.json @@ -0,0 +1,5 @@ +{ + "remote_plan_format": 1, + "run_id": "run-GXfuHMkbyHccAGUg", + "hostname": "app.terraform.io" +} diff --git a/internal/plans/planfile/tfplan.go b/internal/plans/planfile/tfplan.go index a840c378ef..5b30d65317 100644 --- a/internal/plans/planfile/tfplan.go +++ b/internal/plans/planfile/tfplan.go @@ -1,21 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( "fmt" "io" - "io/ioutil" + "slices" + "time" + "github.com/zclconf/go-cty/cty" "google.golang.org/protobuf/proto" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/globalref" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/plans/internal/planproto" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/version" - "github.com/zclconf/go-cty/cty" ) const tfplanFormatVersion = 3 @@ -33,7 +40,7 @@ const tfplanFilename = "tfplan" // a plan file, which is stored in a special file in the archive called // "tfplan". func readTfplan(r io.Reader) (*plans.Plan, error) { - src, err := ioutil.ReadAll(r) + src, err := io.ReadAll(r) if err != nil { return nil, err } @@ -54,23 +61,22 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { plan := &plans.Plan{ VariableValues: map[string]plans.DynamicValue{}, - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Outputs: []*plans.OutputChangeSrc{}, Resources: []*plans.ResourceInstanceChangeSrc{}, }, - DriftedResources: []*plans.ResourceInstanceChangeSrc{}, - Checks: &states.CheckResults{}, + DriftedResources: []*plans.ResourceInstanceChangeSrc{}, + DeferredResources: []*plans.DeferredResourceInstanceChangeSrc{}, + Checks: &states.CheckResults{}, } - switch rawPlan.UiMode { - case planproto.Mode_NORMAL: - plan.UIMode = plans.NormalMode - case planproto.Mode_DESTROY: - plan.UIMode = plans.DestroyMode - case planproto.Mode_REFRESH_ONLY: - plan.UIMode = plans.RefreshOnlyMode - default: - return nil, fmt.Errorf("plan has invalid mode %s", rawPlan.UiMode) + plan.Applyable = rawPlan.Applyable + plan.Complete = rawPlan.Complete + plan.Errored = rawPlan.Errored + + plan.UIMode, err = planproto.FromMode(rawPlan.UiMode) + if err != nil { + return nil, err } for _, rawOC := range rawPlan.OutputChanges { @@ -90,94 +96,14 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { }) } - plan.Checks.ConfigResults = addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]() - for _, rawCRs := range rawPlan.CheckResults { - aggr := &states.CheckResultAggregate{} - switch rawCRs.Status { - case planproto.CheckResults_UNKNOWN: - aggr.Status = checks.StatusUnknown - case planproto.CheckResults_PASS: - aggr.Status = checks.StatusPass - case planproto.CheckResults_FAIL: - aggr.Status = checks.StatusFail - case planproto.CheckResults_ERROR: - aggr.Status = checks.StatusError - default: - return nil, fmt.Errorf("aggregate check results for %s have unsupported status %#v", rawCRs.ConfigAddr, rawCRs.Status) - } - - var objKind addrs.CheckableKind - switch rawCRs.Kind { - case planproto.CheckResults_RESOURCE: - objKind = addrs.CheckableResource - case planproto.CheckResults_OUTPUT_VALUE: - objKind = addrs.CheckableOutputValue - default: - return nil, fmt.Errorf("aggregate check results for %s have unsupported object kind %s", rawCRs.ConfigAddr, objKind) - } - - // Some trickiness here: we only have an address parser for - // addrs.Checkable and not for addrs.ConfigCheckable, but that's okay - // because once we have an addrs.Checkable we can always derive an - // addrs.ConfigCheckable from it, and a ConfigCheckable should always - // be the same syntax as a Checkable with no index information and - // thus we can reuse the same parser for both here. - configAddrProxy, diags := addrs.ParseCheckableStr(objKind, rawCRs.ConfigAddr) - if diags.HasErrors() { - return nil, diags.Err() - } - configAddr := configAddrProxy.ConfigCheckable() - if configAddr.String() != configAddrProxy.String() { - // This is how we catch if the config address included index - // information that would be allowed in a Checkable but not - // in a ConfigCheckable. - return nil, fmt.Errorf("invalid checkable config address %s", rawCRs.ConfigAddr) - } - - aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]() - for _, rawCR := range rawCRs.Objects { - objectAddr, diags := addrs.ParseCheckableStr(objKind, rawCR.ObjectAddr) - if diags.HasErrors() { - return nil, diags.Err() - } - if !addrs.Equivalent(objectAddr.ConfigCheckable(), configAddr) { - return nil, fmt.Errorf("checkable object %s should not be grouped under %s", objectAddr, configAddr) - } - - obj := &states.CheckResultObject{ - FailureMessages: rawCR.FailureMessages, - } - switch rawCR.Status { - case planproto.CheckResults_UNKNOWN: - obj.Status = checks.StatusUnknown - case planproto.CheckResults_PASS: - obj.Status = checks.StatusPass - case planproto.CheckResults_FAIL: - obj.Status = checks.StatusFail - case planproto.CheckResults_ERROR: - obj.Status = checks.StatusError - default: - return nil, fmt.Errorf("object check results for %s has unsupported status %#v", rawCR.ObjectAddr, rawCR.Status) - } - - aggr.ObjectResults.Put(objectAddr, obj) - } - // If we ended up with no elements in the map then we'll just nil it, - // primarily just to make life easier for our round-trip tests. - if aggr.ObjectResults.Len() == 0 { - aggr.ObjectResults.Elems = nil - } - - plan.Checks.ConfigResults.Put(configAddr, aggr) - } - // If we ended up with no elements in the map then we'll just nil it, - // primarily just to make life easier for our round-trip tests. - if plan.Checks.ConfigResults.Len() == 0 { - plan.Checks.ConfigResults.Elems = nil + checkResults, err := CheckResultsFromPlanProto(rawPlan.CheckResults) + if err != nil { + return nil, fmt.Errorf("failed to decode check results: %s", err) } + plan.Checks = checkResults for _, rawRC := range rawPlan.ResourceChanges { - change, err := resourceChangeFromTfplan(rawRC) + change, err := resourceChangeFromTfplan(rawRC, addrs.ParseAbsResourceInstanceStr) if err != nil { // errors from resourceChangeFromTfplan already include context return nil, err @@ -187,7 +113,7 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { } for _, rawRC := range rawPlan.ResourceDrift { - change, err := resourceChangeFromTfplan(rawRC) + change, err := resourceChangeFromTfplan(rawRC, addrs.ParseAbsResourceInstanceStr) if err != nil { // errors from resourceChangeFromTfplan already include context return nil, err @@ -196,6 +122,15 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { plan.DriftedResources = append(plan.DriftedResources, change) } + for _, rawDC := range rawPlan.DeferredChanges { + change, err := deferredChangeFromTfplan(rawDC) + if err != nil { + return nil, err + } + + plan.DeferredResources = append(plan.DeferredResources, change) + } + for _, rawRA := range rawPlan.RelevantAttributes { ra, err := resourceAttrFromTfplan(rawRA) if err != nil { @@ -228,6 +163,22 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { plan.VariableValues[name] = val } + if len(rawPlan.ApplyTimeVariables) != 0 { + plan.ApplyTimeVariables = collections.NewSetCmp[string]() + for _, name := range rawPlan.ApplyTimeVariables { + plan.ApplyTimeVariables.Add(name) + } + } + + for _, hash := range rawPlan.FunctionResults { + plan.FunctionResults = append(plan.FunctionResults, + lang.FunctionResultHash{ + Key: hash.Key, + Result: hash.Result, + }, + ) + } + if rawBackend := rawPlan.Backend; rawBackend == nil { return nil, fmt.Errorf("plan file has no backend settings; backend settings are required") } else { @@ -242,10 +193,32 @@ func readTfplan(r io.Reader) (*plans.Plan, error) { } } + if plan.Timestamp, err = time.Parse(time.RFC3339, rawPlan.Timestamp); err != nil { + return nil, fmt.Errorf("invalid value for timestamp %s: %s", rawPlan.Timestamp, err) + } + return plan, nil } -func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { +// ResourceChangeFromProto decodes an isolated resource instance change from +// its representation as a protocol buffers message. +// +// This is used by the stackplan package, which includes planproto messages +// in its own wire format while using a different overall container. +func ResourceChangeFromProto(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { + return resourceChangeFromTfplan(rawChange, addrs.ParseAbsResourceInstanceStr) +} + +// DeferredResourceChangeFromProto decodes an isolated deferred resource +// instance change from its representation as a protocol buffers message. +// +// This the same as ResourceChangeFromProto but internally allows for splat +// addresses, which are not allowed outside deferred changes. +func DeferredResourceChangeFromProto(rawChange *planproto.ResourceInstanceChange) (*plans.ResourceInstanceChangeSrc, error) { + return resourceChangeFromTfplan(rawChange, addrs.ParsePartialResourceInstanceStr) +} + +func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange, parseAddr func(str string) (addrs.AbsResourceInstance, tfdiags.Diagnostics)) (*plans.ResourceInstanceChangeSrc, error) { if rawChange == nil { // Should never happen in practice, since protobuf can't represent // a nil value in a list. @@ -262,13 +235,13 @@ func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*pla return nil, fmt.Errorf("no instance address for resource instance change; perhaps this plan was created by a different version of Terraform?") } - instAddr, diags := addrs.ParseAbsResourceInstanceStr(rawChange.Addr) + instAddr, diags := parseAddr(rawChange.Addr) if diags.HasErrors() { return nil, fmt.Errorf("invalid resource instance address %q: %w", rawChange.Addr, diags.Err()) } prevRunAddr := instAddr if rawChange.PrevRunAddr != "" { - prevRunAddr, diags = addrs.ParseAbsResourceInstanceStr(rawChange.PrevRunAddr) + prevRunAddr, diags = parseAddr(rawChange.PrevRunAddr) if diags.HasErrors() { return nil, fmt.Errorf("invalid resource instance previous run address %q: %w", rawChange.PrevRunAddr, diags.Err()) } @@ -331,6 +304,10 @@ func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*pla ret.ActionReason = plans.ResourceInstanceReadBecauseConfigUnknown case planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING: ret.ActionReason = plans.ResourceInstanceReadBecauseDependencyPending + case planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED: + ret.ActionReason = plans.ResourceInstanceReadBecauseCheckNested + case planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET: + ret.ActionReason = plans.ResourceInstanceDeleteBecauseNoMoveTarget default: return nil, fmt.Errorf("resource has invalid action reason %s", rawChange.ActionReason) } @@ -342,6 +319,35 @@ func resourceChangeFromTfplan(rawChange *planproto.ResourceInstanceChange) (*pla return ret, nil } +// ActionFromProto translates from the protobuf representation of change actions +// into the "plans" package's representation, or returns an error if the +// given action is unrecognized. +func ActionFromProto(rawAction planproto.Action) (plans.Action, error) { + switch rawAction { + case planproto.Action_NOOP: + return plans.NoOp, nil + case planproto.Action_CREATE: + return plans.Create, nil + case planproto.Action_READ: + return plans.Read, nil + case planproto.Action_UPDATE: + return plans.Update, nil + case planproto.Action_DELETE: + return plans.Delete, nil + case planproto.Action_CREATE_THEN_DELETE: + return plans.CreateThenDelete, nil + case planproto.Action_DELETE_THEN_CREATE: + return plans.DeleteThenCreate, nil + case planproto.Action_FORGET: + return plans.Forget, nil + case planproto.Action_CREATE_THEN_FORGET: + return plans.CreateThenForget, nil + default: + return plans.NoOp, fmt.Errorf("invalid change action %s", rawAction) + } + +} + func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { if rawChange == nil { return nil, fmt.Errorf("change object is absent") @@ -353,31 +359,35 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { // depending on the change action, and then decode. beforeIdx, afterIdx := -1, -1 - switch rawChange.Action { - case planproto.Action_NOOP: - ret.Action = plans.NoOp + var err error + ret.Action, err = ActionFromProto(rawChange.Action) + if err != nil { + return nil, err + } + + switch ret.Action { + case plans.NoOp: beforeIdx = 0 afterIdx = 0 - case planproto.Action_CREATE: - ret.Action = plans.Create + case plans.Create: afterIdx = 0 - case planproto.Action_READ: - ret.Action = plans.Read + case plans.Read: beforeIdx = 0 afterIdx = 1 - case planproto.Action_UPDATE: - ret.Action = plans.Update + case plans.Update: beforeIdx = 0 afterIdx = 1 - case planproto.Action_DELETE: - ret.Action = plans.Delete + case plans.Delete: beforeIdx = 0 - case planproto.Action_CREATE_THEN_DELETE: - ret.Action = plans.CreateThenDelete + case plans.CreateThenDelete: beforeIdx = 0 afterIdx = 1 - case planproto.Action_DELETE_THEN_CREATE: - ret.Action = plans.DeleteThenCreate + case plans.DeleteThenCreate: + beforeIdx = 0 + afterIdx = 1 + case plans.Forget: + beforeIdx = 0 + case plans.CreateThenForget: beforeIdx = 0 afterIdx = 1 default: @@ -411,20 +421,51 @@ func changeFromTfplan(rawChange *planproto.Change) (*plans.ChangeSrc, error) { } } - sensitive := cty.NewValueMarks(marks.Sensitive) - beforeValMarks, err := pathValueMarksFromTfplan(rawChange.BeforeSensitivePaths, sensitive) + if rawChange.Importing != nil { + var identity plans.DynamicValue + if rawChange.Importing.Identity != nil { + var err error + identity, err = valueFromTfplan(rawChange.Importing.Identity) + if err != nil { + return nil, fmt.Errorf("invalid \"identity\" value: %s", err) + } + } + ret.Importing = &plans.ImportingSrc{ + ID: rawChange.Importing.Id, + Unknown: rawChange.Importing.Unknown, + Identity: identity, + } + } + ret.GeneratedConfig = rawChange.GeneratedConfig + + beforeValSensitiveAttrs, err := pathsFromTfplan(rawChange.BeforeSensitivePaths) if err != nil { return nil, fmt.Errorf("failed to decode before sensitive paths: %s", err) } - afterValMarks, err := pathValueMarksFromTfplan(rawChange.AfterSensitivePaths, sensitive) + afterValSensitiveAttrs, err := pathsFromTfplan(rawChange.AfterSensitivePaths) if err != nil { return nil, fmt.Errorf("failed to decode after sensitive paths: %s", err) } - if len(beforeValMarks) > 0 { - ret.BeforeValMarks = beforeValMarks + if len(beforeValSensitiveAttrs) > 0 { + ret.BeforeSensitivePaths = beforeValSensitiveAttrs } - if len(afterValMarks) > 0 { - ret.AfterValMarks = afterValMarks + if len(afterValSensitiveAttrs) > 0 { + ret.AfterSensitivePaths = afterValSensitiveAttrs + } + + if rawChange.BeforeIdentity != nil { + beforeIdentity, err := valueFromTfplan(rawChange.BeforeIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decode before identity: %s", err) + } + ret.BeforeIdentity = beforeIdentity + } + if rawChange.AfterIdentity != nil { + afterIdentity, err := valueFromTfplan(rawChange.AfterIdentity) + if err != nil { + return nil, fmt.Errorf("failed to decode after identity: %s", err) + } + ret.AfterIdentity = afterIdentity } return ret, nil @@ -438,6 +479,44 @@ func valueFromTfplan(rawV *planproto.DynamicValue) (plans.DynamicValue, error) { return plans.DynamicValue(rawV.Msgpack), nil } +func deferredChangeFromTfplan(dc *planproto.DeferredResourceInstanceChange) (*plans.DeferredResourceInstanceChangeSrc, error) { + if dc == nil { + return nil, fmt.Errorf("deferred change object is absent") + } + + change, err := resourceChangeFromTfplan(dc.Change, addrs.ParsePartialResourceInstanceStr) + if err != nil { + return nil, err + } + + reason, err := DeferredReasonFromProto(dc.Deferred.Reason) + if err != nil { + return nil, err + } + + return &plans.DeferredResourceInstanceChangeSrc{ + DeferredReason: reason, + ChangeSrc: change, + }, nil +} + +func DeferredReasonFromProto(reason planproto.DeferredReason) (providers.DeferredReason, error) { + switch reason { + case planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN: + return providers.DeferredReasonInstanceCountUnknown, nil + case planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN: + return providers.DeferredReasonResourceConfigUnknown, nil + case planproto.DeferredReason_PROVIDER_CONFIG_UNKNOWN: + return providers.DeferredReasonProviderConfigUnknown, nil + case planproto.DeferredReason_ABSENT_PREREQ: + return providers.DeferredReasonAbsentPrereq, nil + case planproto.DeferredReason_DEFERRED_PREREQ: + return providers.DeferredReasonDeferredPrereq, nil + default: + return providers.DeferredReasonInvalid, fmt.Errorf("invalid deferred reason %s", reason) + } +} + // writeTfplan serializes the given plan into the protobuf-based format used // for the "tfplan" portion of a plan file. func writeTfplan(plan *plans.Plan, w io.Writer) error { @@ -457,17 +536,17 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { CheckResults: []*planproto.CheckResults{}, ResourceChanges: []*planproto.ResourceInstanceChange{}, ResourceDrift: []*planproto.ResourceInstanceChange{}, + DeferredChanges: []*planproto.DeferredResourceInstanceChange{}, } - switch plan.UIMode { - case plans.NormalMode: - rawPlan.UiMode = planproto.Mode_NORMAL - case plans.DestroyMode: - rawPlan.UiMode = planproto.Mode_DESTROY - case plans.RefreshOnlyMode: - rawPlan.UiMode = planproto.Mode_REFRESH_ONLY - default: - return fmt.Errorf("plan has unsupported mode %s", plan.UIMode) + rawPlan.Applyable = plan.Applyable + rawPlan.Complete = plan.Complete + rawPlan.Errored = plan.Errored + + var err error + rawPlan.UiMode, err = planproto.NewMode(plan.UIMode) + if err != nil { + return err } for _, oc := range plan.Changes.Outputs { @@ -495,57 +574,11 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { }) } - if plan.Checks != nil { - for _, configElem := range plan.Checks.ConfigResults.Elems { - crs := configElem.Value - pcrs := &planproto.CheckResults{ - ConfigAddr: configElem.Key.String(), - } - switch crs.Status { - case checks.StatusUnknown: - pcrs.Status = planproto.CheckResults_UNKNOWN - case checks.StatusPass: - pcrs.Status = planproto.CheckResults_PASS - case checks.StatusFail: - pcrs.Status = planproto.CheckResults_FAIL - case checks.StatusError: - pcrs.Status = planproto.CheckResults_ERROR - default: - return fmt.Errorf("checkable configuration %s has unsupported aggregate status %s", configElem.Key, crs.Status) - } - switch kind := configElem.Key.CheckableKind(); kind { - case addrs.CheckableResource: - pcrs.Kind = planproto.CheckResults_RESOURCE - case addrs.CheckableOutputValue: - pcrs.Kind = planproto.CheckResults_OUTPUT_VALUE - default: - return fmt.Errorf("checkable configuration %s has unsupported object type kind %s", configElem.Key, kind) - } - - for _, objectElem := range configElem.Value.ObjectResults.Elems { - cr := objectElem.Value - pcr := &planproto.CheckResults_ObjectResult{ - ObjectAddr: objectElem.Key.String(), - FailureMessages: objectElem.Value.FailureMessages, - } - switch cr.Status { - case checks.StatusUnknown: - pcr.Status = planproto.CheckResults_UNKNOWN - case checks.StatusPass: - pcr.Status = planproto.CheckResults_PASS - case checks.StatusFail: - pcr.Status = planproto.CheckResults_FAIL - case checks.StatusError: - pcr.Status = planproto.CheckResults_ERROR - default: - return fmt.Errorf("checkable object %s has unsupported status %s", objectElem.Key, crs.Status) - } - pcrs.Objects = append(pcrs.Objects, pcr) - } - - rawPlan.CheckResults = append(rawPlan.CheckResults, pcrs) - } + checkResults, err := CheckResultsToPlanProto(plan.Checks) + if err != nil { + return fmt.Errorf("failed to encode check results: %s", err) } + rawPlan.CheckResults = checkResults for _, rc := range plan.Changes.Resources { rawRC, err := resourceChangeToTfplan(rc) @@ -563,6 +596,14 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { rawPlan.ResourceDrift = append(rawPlan.ResourceDrift, rawRC) } + for _, dc := range plan.DeferredResources { + rawDC, err := deferredChangeToTfplan(dc) + if err != nil { + return err + } + rawPlan.DeferredChanges = append(rawPlan.DeferredChanges, rawDC) + } + for _, ra := range plan.RelevantAttributes { rawRA, err := resourceAttrToTfplan(ra) if err != nil { @@ -582,6 +623,18 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { for name, val := range plan.VariableValues { rawPlan.Variables[name] = valueToTfplan(val) } + if plan.ApplyTimeVariables.Len() != 0 { + rawPlan.ApplyTimeVariables = slices.Collect(plan.ApplyTimeVariables.All()) + } + + for _, hash := range plan.FunctionResults { + rawPlan.FunctionResults = append(rawPlan.FunctionResults, + &planproto.FunctionCallHash{ + Key: hash.Key, + Result: hash.Result, + }, + ) + } if plan.Backend.Type == "" || plan.Backend.Config == nil { // This suggests a bug in the code that created the plan, since it @@ -596,6 +649,8 @@ func writeTfplan(plan *plans.Plan, w io.Writer) error { Workspace: plan.Backend.Workspace, } + rawPlan.Timestamp = plan.Timestamp.Format(time.RFC3339) + src, err := proto.Marshal(rawPlan) if err != nil { return fmt.Errorf("serialization error: %s", err) @@ -642,6 +697,19 @@ func resourceAttrFromTfplan(ra *planproto.PlanResourceAttr) (globalref.ResourceA return res, nil } +// ResourceChangeToProto encodes an isolated resource instance change into +// its representation as a protocol buffers message. +// +// This is used by the stackplan package, which includes planproto messages +// in its own wire format while using a different overall container. +func ResourceChangeToProto(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { + if change == nil { + // We assume this represents the absense of a change, then. + return nil, nil + } + return resourceChangeToTfplan(change) +} + func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto.ResourceInstanceChange, error) { ret := &planproto.ResourceInstanceChange{} @@ -705,6 +773,10 @@ func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN case plans.ResourceInstanceReadBecauseDependencyPending: ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING + case plans.ResourceInstanceReadBecauseCheckNested: + ret.ActionReason = planproto.ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED + case plans.ResourceInstanceDeleteBecauseNoMoveTarget: + ret.ActionReason = planproto.ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET default: return nil, fmt.Errorf("resource %s has unsupported action reason %s", change.Addr, change.ActionReason) } @@ -716,44 +788,95 @@ func resourceChangeToTfplan(change *plans.ResourceInstanceChangeSrc) (*planproto return ret, nil } +// ActionToProto translates from the "plans" package's representation of change +// actions into the protobuf representation, or returns an error if the +// given action is unrecognized. +func ActionToProto(action plans.Action) (planproto.Action, error) { + switch action { + case plans.NoOp: + return planproto.Action_NOOP, nil + case plans.Create: + return planproto.Action_CREATE, nil + case plans.Read: + return planproto.Action_READ, nil + case plans.Update: + return planproto.Action_UPDATE, nil + case plans.Delete: + return planproto.Action_DELETE, nil + case plans.DeleteThenCreate: + return planproto.Action_DELETE_THEN_CREATE, nil + case plans.CreateThenDelete: + return planproto.Action_CREATE_THEN_DELETE, nil + case plans.Forget: + return planproto.Action_FORGET, nil + case plans.CreateThenForget: + return planproto.Action_CREATE_THEN_FORGET, nil + default: + return planproto.Action_NOOP, fmt.Errorf("invalid change action %s", action) + } +} + func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) { ret := &planproto.Change{} before := valueToTfplan(change.Before) after := valueToTfplan(change.After) - beforeSensitivePaths, err := pathValueMarksToTfplan(change.BeforeValMarks) + beforeSensitivePaths, err := pathsToTfplan(change.BeforeSensitivePaths) if err != nil { return nil, err } - afterSensitivePaths, err := pathValueMarksToTfplan(change.AfterValMarks) + afterSensitivePaths, err := pathsToTfplan(change.AfterSensitivePaths) if err != nil { return nil, err } ret.BeforeSensitivePaths = beforeSensitivePaths ret.AfterSensitivePaths = afterSensitivePaths - switch change.Action { - case plans.NoOp: - ret.Action = planproto.Action_NOOP + if change.Importing != nil { + var identity *planproto.DynamicValue + if change.Importing.Identity != nil { + identity = planproto.NewPlanDynamicValue(change.Importing.Identity) + } + ret.Importing = &planproto.Importing{ + Id: change.Importing.ID, + Unknown: change.Importing.Unknown, + Identity: identity, + } + + } + ret.GeneratedConfig = change.GeneratedConfig + + if change.BeforeIdentity != nil { + ret.BeforeIdentity = planproto.NewPlanDynamicValue(change.BeforeIdentity) + } + if change.AfterIdentity != nil { + ret.AfterIdentity = planproto.NewPlanDynamicValue(change.AfterIdentity) + } + + ret.Action, err = ActionToProto(change.Action) + if err != nil { + return nil, err + } + + switch ret.Action { + case planproto.Action_NOOP: ret.Values = []*planproto.DynamicValue{before} // before and after should be identical - case plans.Create: - ret.Action = planproto.Action_CREATE + case planproto.Action_CREATE: ret.Values = []*planproto.DynamicValue{after} - case plans.Read: - ret.Action = planproto.Action_READ + case planproto.Action_READ: ret.Values = []*planproto.DynamicValue{before, after} - case plans.Update: - ret.Action = planproto.Action_UPDATE + case planproto.Action_UPDATE: ret.Values = []*planproto.DynamicValue{before, after} - case plans.Delete: - ret.Action = planproto.Action_DELETE + case planproto.Action_DELETE: ret.Values = []*planproto.DynamicValue{before} - case plans.DeleteThenCreate: - ret.Action = planproto.Action_DELETE_THEN_CREATE + case planproto.Action_DELETE_THEN_CREATE: ret.Values = []*planproto.DynamicValue{before, after} - case plans.CreateThenDelete: - ret.Action = planproto.Action_CREATE_THEN_DELETE + case planproto.Action_CREATE_THEN_DELETE: + ret.Values = []*planproto.DynamicValue{before, after} + case planproto.Action_FORGET: + ret.Values = []*planproto.DynamicValue{before} + case planproto.Action_CREATE_THEN_FORGET: ret.Values = []*planproto.DynamicValue{before, after} default: return nil, fmt.Errorf("invalid change action %s", change.Action) @@ -763,43 +886,51 @@ func changeToTfplan(change *plans.ChangeSrc) (*planproto.Change, error) { } func valueToTfplan(val plans.DynamicValue) *planproto.DynamicValue { - if val == nil { - // protobuf can't represent nil, so we'll represent it as a - // DynamicValue that has no serializations at all. - return &planproto.DynamicValue{} - } - return &planproto.DynamicValue{ - Msgpack: []byte(val), - } + return planproto.NewPlanDynamicValue(val) } -func pathValueMarksFromTfplan(paths []*planproto.Path, marks cty.ValueMarks) ([]cty.PathValueMarks, error) { - ret := make([]cty.PathValueMarks, 0, len(paths)) +func pathsFromTfplan(paths []*planproto.Path) ([]cty.Path, error) { + if len(paths) == 0 { + return nil, nil + } + ret := make([]cty.Path, 0, len(paths)) for _, p := range paths { path, err := pathFromTfplan(p) if err != nil { return nil, err } - ret = append(ret, cty.PathValueMarks{ - Path: path, - Marks: marks, - }) - } - return ret, nil -} - -func pathValueMarksToTfplan(pvm []cty.PathValueMarks) ([]*planproto.Path, error) { - ret := make([]*planproto.Path, 0, len(pvm)) - for _, p := range pvm { - path, err := pathToTfplan(p.Path) - if err != nil { - return nil, err - } ret = append(ret, path) } return ret, nil } +func pathsToTfplan(paths []cty.Path) ([]*planproto.Path, error) { + if len(paths) == 0 { + return nil, nil + } + ret := make([]*planproto.Path, 0, len(paths)) + for _, p := range paths { + path, err := pathToTfplan(p) + if err != nil { + return nil, err + } + ret = append(ret, path) + } + return ret, nil +} + +// PathFromProto decodes a path to a nested attribute into a cty.Path for +// use in tracking marked values. +// +// This is used by the stackstate package, which uses planproto.Path messages +// while using a different overall container. +func PathFromProto(path *planproto.Path) (cty.Path, error) { + if path == nil { + return nil, nil + } + return pathFromTfplan(path) +} + func pathFromTfplan(path *planproto.Path) (cty.Path, error) { ret := make([]cty.PathStep, 0, len(path.Steps)) for _, step := range path.Steps { @@ -828,30 +959,220 @@ func pathFromTfplan(path *planproto.Path) (cty.Path, error) { } func pathToTfplan(path cty.Path) (*planproto.Path, error) { - steps := make([]*planproto.Path_Step, 0, len(path)) - for _, step := range path { - switch s := step.(type) { - case cty.IndexStep: - value, err := plans.NewDynamicValue(s.Key, s.Key.Type()) - if err != nil { - return nil, fmt.Errorf("Error encoding path step: %s", err) - } - steps = append(steps, &planproto.Path_Step{ - Selector: &planproto.Path_Step_ElementKey{ - ElementKey: valueToTfplan(value), - }, - }) - case cty.GetAttrStep: - steps = append(steps, &planproto.Path_Step{ - Selector: &planproto.Path_Step_AttributeName{ - AttributeName: s.Name, - }, - }) - default: - return nil, fmt.Errorf("Unsupported path step %#v (%t)", step, step) - } + return planproto.NewPath(path) +} + +func deferredChangeToTfplan(dc *plans.DeferredResourceInstanceChangeSrc) (*planproto.DeferredResourceInstanceChange, error) { + change, err := resourceChangeToTfplan(dc.ChangeSrc) + if err != nil { + return nil, err } - return &planproto.Path{ - Steps: steps, + + reason, err := DeferredReasonToProto(dc.DeferredReason) + if err != nil { + return nil, err + } + + return &planproto.DeferredResourceInstanceChange{ + Change: change, + Deferred: &planproto.Deferred{ + Reason: reason, + }, }, nil } + +func DeferredReasonToProto(reason providers.DeferredReason) (planproto.DeferredReason, error) { + switch reason { + case providers.DeferredReasonInstanceCountUnknown: + return planproto.DeferredReason_INSTANCE_COUNT_UNKNOWN, nil + case providers.DeferredReasonResourceConfigUnknown: + return planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN, nil + case providers.DeferredReasonProviderConfigUnknown: + return planproto.DeferredReason_PROVIDER_CONFIG_UNKNOWN, nil + case providers.DeferredReasonAbsentPrereq: + return planproto.DeferredReason_ABSENT_PREREQ, nil + case providers.DeferredReasonDeferredPrereq: + return planproto.DeferredReason_DEFERRED_PREREQ, nil + default: + return planproto.DeferredReason_INVALID, fmt.Errorf("invalid deferred reason %s", reason) + } +} + +// CheckResultsFromPlanProto decodes a slice of check results from their protobuf +// representation into the "states" package's representation. +// +// It's used by the stackplan package, which includes an identical representation +// of check results within a different overall container. +func CheckResultsFromPlanProto(proto []*planproto.CheckResults) (*states.CheckResults, error) { + configResults := addrs.MakeMap[addrs.ConfigCheckable, *states.CheckResultAggregate]() + + for _, rawCheckResults := range proto { + aggr := &states.CheckResultAggregate{} + switch rawCheckResults.Status { + case planproto.CheckResults_UNKNOWN: + aggr.Status = checks.StatusUnknown + case planproto.CheckResults_PASS: + aggr.Status = checks.StatusPass + case planproto.CheckResults_FAIL: + aggr.Status = checks.StatusFail + case planproto.CheckResults_ERROR: + aggr.Status = checks.StatusError + default: + return nil, + fmt.Errorf("aggregate check results for %s have unsupported status %#v", + rawCheckResults.ConfigAddr, rawCheckResults.Status) + } + + var objKind addrs.CheckableKind + switch rawCheckResults.Kind { + case planproto.CheckResults_RESOURCE: + objKind = addrs.CheckableResource + case planproto.CheckResults_OUTPUT_VALUE: + objKind = addrs.CheckableOutputValue + case planproto.CheckResults_CHECK: + objKind = addrs.CheckableCheck + case planproto.CheckResults_INPUT_VARIABLE: + objKind = addrs.CheckableInputVariable + default: + return nil, fmt.Errorf("aggregate check results for %s have unsupported object kind %s", + rawCheckResults.ConfigAddr, objKind) + } + + // Some trickiness here: we only have an address parser for + // addrs.Checkable and not for addrs.ConfigCheckable, but that's okay + // because once we have an addrs.Checkable we can always derive an + // addrs.ConfigCheckable from it, and a ConfigCheckable should always + // be the same syntax as a Checkable with no index information and + // thus we can reuse the same parser for both here. + configAddrProxy, diags := addrs.ParseCheckableStr(objKind, rawCheckResults.ConfigAddr) + if diags.HasErrors() { + return nil, diags.Err() + } + configAddr := configAddrProxy.ConfigCheckable() + if configAddr.String() != configAddrProxy.String() { + // This is how we catch if the config address included index + // information that would be allowed in a Checkable but not + // in a ConfigCheckable. + return nil, fmt.Errorf("invalid checkable config address %s", rawCheckResults.ConfigAddr) + } + + aggr.ObjectResults = addrs.MakeMap[addrs.Checkable, *states.CheckResultObject]() + for _, rawCheckResult := range rawCheckResults.Objects { + objectAddr, diags := addrs.ParseCheckableStr(objKind, rawCheckResult.ObjectAddr) + if diags.HasErrors() { + return nil, diags.Err() + } + if !addrs.Equivalent(objectAddr.ConfigCheckable(), configAddr) { + return nil, fmt.Errorf("checkable object %s should not be grouped under %s", objectAddr, configAddr) + } + + obj := &states.CheckResultObject{ + FailureMessages: rawCheckResult.FailureMessages, + } + switch rawCheckResult.Status { + case planproto.CheckResults_UNKNOWN: + obj.Status = checks.StatusUnknown + case planproto.CheckResults_PASS: + obj.Status = checks.StatusPass + case planproto.CheckResults_FAIL: + obj.Status = checks.StatusFail + case planproto.CheckResults_ERROR: + obj.Status = checks.StatusError + default: + return nil, fmt.Errorf("object check results for %s has unsupported status %#v", + rawCheckResult.ObjectAddr, rawCheckResult.Status) + } + + aggr.ObjectResults.Put(objectAddr, obj) + } + + // If we ended up with no elements in the map then we'll just nil it, + // primarily just to make life easier for our round-trip tests. + if aggr.ObjectResults.Len() == 0 { + aggr.ObjectResults.Elems = nil + } + + configResults.Put(configAddr, aggr) + } + + // If we ended up with no elements in the map then we'll just nil it, + // primarily just to make life easier for our round-trip tests. + if configResults.Len() == 0 { + configResults.Elems = nil + } + + return &states.CheckResults{ + ConfigResults: configResults, + }, nil +} + +// CheckResultsToPlanProto encodes a slice of check results from the "states" +// package's representation into their protobuf representation. +// +// It's used by the stackplan package, which includes identical representation +// of check results within a different overall container. +func CheckResultsToPlanProto(checkResults *states.CheckResults) ([]*planproto.CheckResults, error) { + if checkResults != nil { + protoResults := make([]*planproto.CheckResults, 0) + for _, configElem := range checkResults.ConfigResults.Elems { + crs := configElem.Value + pcrs := &planproto.CheckResults{ + ConfigAddr: configElem.Key.String(), + } + switch crs.Status { + case checks.StatusUnknown: + pcrs.Status = planproto.CheckResults_UNKNOWN + case checks.StatusPass: + pcrs.Status = planproto.CheckResults_PASS + case checks.StatusFail: + pcrs.Status = planproto.CheckResults_FAIL + case checks.StatusError: + pcrs.Status = planproto.CheckResults_ERROR + default: + return nil, + fmt.Errorf("checkable configuration %s has unsupported aggregate status %s", configElem.Key, crs.Status) + } + switch kind := configElem.Key.CheckableKind(); kind { + case addrs.CheckableResource: + pcrs.Kind = planproto.CheckResults_RESOURCE + case addrs.CheckableOutputValue: + pcrs.Kind = planproto.CheckResults_OUTPUT_VALUE + case addrs.CheckableCheck: + pcrs.Kind = planproto.CheckResults_CHECK + case addrs.CheckableInputVariable: + pcrs.Kind = planproto.CheckResults_INPUT_VARIABLE + default: + return nil, + fmt.Errorf("checkable configuration %s has unsupported object type kind %s", configElem.Key, kind) + } + + for _, objectElem := range configElem.Value.ObjectResults.Elems { + cr := objectElem.Value + pcr := &planproto.CheckResults_ObjectResult{ + ObjectAddr: objectElem.Key.String(), + FailureMessages: objectElem.Value.FailureMessages, + } + switch cr.Status { + case checks.StatusUnknown: + pcr.Status = planproto.CheckResults_UNKNOWN + case checks.StatusPass: + pcr.Status = planproto.CheckResults_PASS + case checks.StatusFail: + pcr.Status = planproto.CheckResults_FAIL + case checks.StatusError: + pcr.Status = planproto.CheckResults_ERROR + default: + return nil, + fmt.Errorf("checkable object %s has unsupported status %s", objectElem.Key, crs.Status) + } + pcrs.Objects = append(pcrs.Objects, pcr) + } + + protoResults = append(protoResults, pcrs) + } + + return protoResults, nil + } else { + return nil, nil + } +} diff --git a/internal/plans/planfile/tfplan_test.go b/internal/plans/planfile/tfplan_test.go index 6984ceafdc..a596864c7d 100644 --- a/internal/plans/planfile/tfplan_test.go +++ b/internal/plans/planfile/tfplan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( @@ -9,9 +12,11 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/globalref" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" ) @@ -19,12 +24,17 @@ func TestTFPlanRoundTrip(t *testing.T) { objTy := cty.Object(map[string]cty.Type{ "id": cty.String, }) + applyTimeVariables := collections.NewSetCmp[string]() + applyTimeVariables.Add("bar") plan := &plans.Plan{ + Applyable: true, + Complete: true, VariableValues: map[string]plans.DynamicValue{ "foo": mustNewDynamicValueStr("foo value"), }, - Changes: &plans.Changes{ + ApplyTimeVariables: applyTimeVariables, + Changes: &plans.ChangesSrc{ Outputs: []*plans.OutputChangeSrc{ { Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), @@ -84,11 +94,8 @@ func TestTFPlanRoundTrip(t *testing.T) { cty.StringVal("honk"), }), }), objTy), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("boop").IndexInt(1), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("boop").IndexInt(1), }, }, RequiredReplace: cty.NewPathSet( @@ -119,6 +126,33 @@ func TestTFPlanRoundTrip(t *testing.T) { }), objTy), }, }, + { + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "importing", + }.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "importing", + }.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("testing"), + }), objTy), + After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("testing"), + }), objTy), + Importing: &plans.ImportingSrc{ID: "testing"}, + GeneratedConfig: "resource \\\"test_thing\\\" \\\"importing\\\" {}", + }, + }, }, }, DriftedResources: []*plans.ResourceInstanceChangeSrc{ @@ -152,11 +186,63 @@ func TestTFPlanRoundTrip(t *testing.T) { cty.StringVal("bonk"), }), }), objTy), - AfterValMarks: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("boop").IndexInt(1), - Marks: cty.NewValueMarks(marks.Sensitive), + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("boop").IndexInt(1), + }, + }, + }, + }, + DeferredResources: []*plans.DeferredResourceInstanceChangeSrc{ + { + DeferredReason: providers.DeferredReasonInstanceCountUnknown, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.WildcardKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "boop": cty.ListVal([]cty.Value{ + cty.StringVal("beep"), + cty.StringVal("bonk"), + }), + }), objTy), + }, + }, + }, + { + DeferredReason: providers.DeferredReasonInstanceCountUnknown, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "woot", + }.Instance(addrs.WildcardKey).Absolute(addrs.ModuleInstance{ + addrs.ModuleInstanceStep{ + Name: "mod", + InstanceKey: addrs.WildcardKey, }, + }), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + After: mustNewDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "boop": cty.ListVal([]cty.Value{ + cty.StringVal("beep"), + cty.StringVal("bonk"), + }), + }), objTy), }, }, }, @@ -196,6 +282,25 @@ func TestTFPlanRoundTrip(t *testing.T) { ), }, ), + addrs.MakeMapElem[addrs.ConfigCheckable]( + addrs.Check{ + Name: "check", + }.InModule(addrs.RootModule), + &states.CheckResultAggregate{ + Status: checks.StatusFail, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.Checkable]( + addrs.Check{ + Name: "check", + }.Absolute(addrs.RootModuleInstance), + &states.CheckResultObject{ + Status: checks.StatusFail, + FailureMessages: []string{"check failed"}, + }, + ), + ), + }, + ), ), }, TargetAddrs: []addrs.Targetable{ @@ -277,8 +382,19 @@ func TestTFPlanRoundTripDestroy(t *testing.T) { "id": cty.String, }) + objSchema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + }, + } + plan := &plans.Plan{ - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Outputs: []*plans.OutputChangeSrc{ { Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), @@ -349,7 +465,7 @@ func TestTFPlanRoundTripDestroy(t *testing.T) { } for _, rics := range newPlan.Changes.Resources { - ric, err := rics.Decode(objTy) + ric, err := rics.Decode(objSchema) if err != nil { t.Fatal(err) } diff --git a/internal/plans/planfile/wrapped.go b/internal/plans/planfile/wrapped.go new file mode 100644 index 0000000000..9d16516a4c --- /dev/null +++ b/internal/plans/planfile/wrapped.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package planfile + +import ( + "errors" + "fmt" + + "github.com/hashicorp/terraform/internal/cloud/cloudplan" +) + +// WrappedPlanFile is a sum type that represents a saved plan, loaded from a +// file path passed on the command line. If the specified file was a thick local +// plan file, the Local field will be populated; if it was a bookmark for a +// remote cloud plan, the Cloud field will be populated. In both cases, the +// other field is expected to be nil. Finally, the outer struct is also expected +// to be used as a pointer, so that a nil value can represent the absence of any +// plan file. +type WrappedPlanFile struct { + local *Reader + cloud *cloudplan.SavedPlanBookmark +} + +func (w *WrappedPlanFile) IsLocal() bool { + return w != nil && w.local != nil +} + +func (w *WrappedPlanFile) IsCloud() bool { + return w != nil && w.cloud != nil +} + +// Local checks whether the wrapped value is a local plan file, and returns it if available. +func (w *WrappedPlanFile) Local() (*Reader, bool) { + if w != nil && w.local != nil { + return w.local, true + } else { + return nil, false + } +} + +// Cloud checks whether the wrapped value is a cloud plan file, and returns it if available. +func (w *WrappedPlanFile) Cloud() (*cloudplan.SavedPlanBookmark, bool) { + if w != nil && w.cloud != nil { + return w.cloud, true + } else { + return nil, false + } +} + +// NewWrappedLocal constructs a WrappedPlanFile from an already loaded local +// plan file reader. Most cases should use OpenWrapped to load from disk +// instead. If the provided reader is nil, the returned pointer is nil. +func NewWrappedLocal(l *Reader) *WrappedPlanFile { + if l != nil { + return &WrappedPlanFile{local: l} + } else { + return nil + } +} + +// NewWrappedCloud constructs a WrappedPlanFile from an already loaded cloud +// plan file. Most cases should use OpenWrapped to load from disk +// instead. If the provided plan file is nil, the returned pointer is nil. +func NewWrappedCloud(c *cloudplan.SavedPlanBookmark) *WrappedPlanFile { + if c != nil { + return &WrappedPlanFile{cloud: c} + } else { + return nil + } +} + +// OpenWrapped loads a local or cloud plan file from a specified file path, or +// returns an error if the file doesn't seem to be a plan file of either kind. +// Most consumers should use this and switch behaviors based on the kind of plan +// they expected, rather than directly using Open. +func OpenWrapped(filename string) (*WrappedPlanFile, error) { + // First, try to load it as a local planfile. + local, localErr := Open(filename) + if localErr == nil { + return &WrappedPlanFile{local: local}, nil + } + // Then, try to load it as a cloud plan. + cloud, cloudErr := cloudplan.LoadSavedPlanBookmark(filename) + if cloudErr == nil { + return &WrappedPlanFile{cloud: &cloud}, nil + } + // If neither worked, prioritize definitive "confirmed the format but can't + // use it" errors, then fall back to dumping everything we know. + var ulp *ErrUnusableLocalPlan + if errors.As(localErr, &ulp) { + return nil, ulp + } + + combinedErr := fmt.Errorf("couldn't load the provided path as either a local plan file (%s) or a saved cloud plan (%s)", localErr, cloudErr) + return nil, combinedErr +} diff --git a/internal/plans/planfile/writer.go b/internal/plans/planfile/writer.go index bdf84c86db..6fa05b5f37 100644 --- a/internal/plans/planfile/writer.go +++ b/internal/plans/planfile/writer.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package planfile import ( diff --git a/internal/plans/planproto/convert.go b/internal/plans/planproto/convert.go new file mode 100644 index 0000000000..4845105abe --- /dev/null +++ b/internal/plans/planproto/convert.go @@ -0,0 +1,133 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package planproto + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/plans" +) + +func NewPath(src cty.Path) (*Path, error) { + ret := &Path{ + Steps: make([]*Path_Step, len(src)), + } + for i, srcStep := range src { + step, err := NewPathStep(srcStep) + if err != nil { + return nil, fmt.Errorf("step %d: %w", i, err) + } + ret.Steps[i] = step + } + return ret, nil +} + +func NewPathStep(step cty.PathStep) (*Path_Step, error) { + switch s := step.(type) { + case cty.IndexStep: + value, err := plans.NewDynamicValue(s.Key, s.Key.Type()) + if err != nil { + return nil, err + } + return &Path_Step{ + Selector: &Path_Step_ElementKey{ + ElementKey: NewPlanDynamicValue(value), + }, + }, nil + case cty.GetAttrStep: + return &Path_Step{ + Selector: &Path_Step_AttributeName{ + AttributeName: s.Name, + }, + }, nil + default: + return nil, fmt.Errorf("unsupported step type %t", step) + } +} + +func NewPlanDynamicValue(dv plans.DynamicValue) *DynamicValue { + if dv == nil { + // protobuf can't represent nil, so we'll represent it as a + // DynamicValue that has no serializations at all. + return &DynamicValue{} + } + return &DynamicValue{ + Msgpack: []byte(dv), + } +} + +func NewAction(action plans.Action) Action { + switch action { + case plans.NoOp: + return Action_NOOP + case plans.Create: + return Action_CREATE + case plans.Read: + return Action_READ + case plans.Update: + return Action_UPDATE + case plans.Delete: + return Action_DELETE + case plans.DeleteThenCreate: + return Action_DELETE_THEN_CREATE + case plans.CreateThenDelete: + return Action_CREATE_THEN_DELETE + case plans.Forget: + return Action_FORGET + default: + // The above should be exhaustive for all possible actions + panic(fmt.Sprintf("unsupported change action %s", action)) + } +} + +func FromAction(protoAction Action) (plans.Action, error) { + switch protoAction { + case Action_NOOP: + return plans.NoOp, nil + case Action_CREATE: + return plans.Create, nil + case Action_READ: + return plans.Read, nil + case Action_UPDATE: + return plans.Update, nil + case Action_DELETE: + return plans.Delete, nil + case Action_DELETE_THEN_CREATE: + return plans.DeleteThenCreate, nil + case Action_CREATE_THEN_DELETE: + return plans.CreateThenDelete, nil + case Action_FORGET: + return plans.Forget, nil + default: + return plans.NoOp, fmt.Errorf("unsupported action %s", protoAction) + } +} + +func NewMode(mode plans.Mode) (Mode, error) { + switch mode { + case plans.NormalMode: + return Mode_NORMAL, nil + case plans.RefreshOnlyMode: + return Mode_REFRESH_ONLY, nil + case plans.DestroyMode: + return Mode_DESTROY, nil + default: + return Mode_NORMAL, fmt.Errorf("unsupported mode %s", mode) + } +} + +func FromMode(protoMode Mode) (plans.Mode, error) { + switch protoMode { + case Mode_NORMAL: + return plans.NormalMode, nil + case Mode_REFRESH_ONLY: + return plans.RefreshOnlyMode, nil + case Mode_DESTROY: + return plans.DestroyMode, nil + default: + return plans.NormalMode, fmt.Errorf("unsupported mode %s", protoMode) + } +} diff --git a/internal/plans/internal/planproto/doc.go b/internal/plans/planproto/doc.go similarity index 79% rename from internal/plans/internal/planproto/doc.go rename to internal/plans/planproto/doc.go index d6ea0f7814..445207c840 100644 --- a/internal/plans/internal/planproto/doc.go +++ b/internal/plans/planproto/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package planproto is home to the Go stubs generated from the tfplan protobuf // schema. // diff --git a/internal/plans/planproto/planfile.pb.go b/internal/plans/planproto/planfile.pb.go new file mode 100644 index 0000000000..da02e50f36 --- /dev/null +++ b/internal/plans/planproto/planfile.pb.go @@ -0,0 +1,2009 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: planfile.proto + +package planproto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Mode describes the planning mode that created the plan. +type Mode int32 + +const ( + Mode_NORMAL Mode = 0 + Mode_DESTROY Mode = 1 + Mode_REFRESH_ONLY Mode = 2 +) + +// Enum value maps for Mode. +var ( + Mode_name = map[int32]string{ + 0: "NORMAL", + 1: "DESTROY", + 2: "REFRESH_ONLY", + } + Mode_value = map[string]int32{ + "NORMAL": 0, + "DESTROY": 1, + "REFRESH_ONLY": 2, + } +) + +func (x Mode) Enum() *Mode { + p := new(Mode) + *p = x + return p +} + +func (x Mode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Mode) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[0].Descriptor() +} + +func (Mode) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[0] +} + +func (x Mode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Mode.Descriptor instead. +func (Mode) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{0} +} + +// Action describes the type of action planned for an object. +// Not all action values are valid for all object types. +type Action int32 + +const ( + Action_NOOP Action = 0 + Action_CREATE Action = 1 + Action_READ Action = 2 + Action_UPDATE Action = 3 + Action_DELETE Action = 5 + Action_DELETE_THEN_CREATE Action = 6 + Action_CREATE_THEN_DELETE Action = 7 + Action_FORGET Action = 8 + Action_CREATE_THEN_FORGET Action = 9 +) + +// Enum value maps for Action. +var ( + Action_name = map[int32]string{ + 0: "NOOP", + 1: "CREATE", + 2: "READ", + 3: "UPDATE", + 5: "DELETE", + 6: "DELETE_THEN_CREATE", + 7: "CREATE_THEN_DELETE", + 8: "FORGET", + 9: "CREATE_THEN_FORGET", + } + Action_value = map[string]int32{ + "NOOP": 0, + "CREATE": 1, + "READ": 2, + "UPDATE": 3, + "DELETE": 5, + "DELETE_THEN_CREATE": 6, + "CREATE_THEN_DELETE": 7, + "FORGET": 8, + "CREATE_THEN_FORGET": 9, + } +) + +func (x Action) Enum() *Action { + p := new(Action) + *p = x + return p +} + +func (x Action) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Action) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[1].Descriptor() +} + +func (Action) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[1] +} + +func (x Action) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Action.Descriptor instead. +func (Action) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{1} +} + +// ResourceInstanceActionReason sometimes provides some additional user-facing +// context for why a particular action was chosen for a resource instance. +// This is for user feedback only and never used to drive behavior during the +// subsequent apply step. +type ResourceInstanceActionReason int32 + +const ( + ResourceInstanceActionReason_NONE ResourceInstanceActionReason = 0 + ResourceInstanceActionReason_REPLACE_BECAUSE_TAINTED ResourceInstanceActionReason = 1 + ResourceInstanceActionReason_REPLACE_BY_REQUEST ResourceInstanceActionReason = 2 + ResourceInstanceActionReason_REPLACE_BECAUSE_CANNOT_UPDATE ResourceInstanceActionReason = 3 + ResourceInstanceActionReason_DELETE_BECAUSE_NO_RESOURCE_CONFIG ResourceInstanceActionReason = 4 + ResourceInstanceActionReason_DELETE_BECAUSE_WRONG_REPETITION ResourceInstanceActionReason = 5 + ResourceInstanceActionReason_DELETE_BECAUSE_COUNT_INDEX ResourceInstanceActionReason = 6 + ResourceInstanceActionReason_DELETE_BECAUSE_EACH_KEY ResourceInstanceActionReason = 7 + ResourceInstanceActionReason_DELETE_BECAUSE_NO_MODULE ResourceInstanceActionReason = 8 + ResourceInstanceActionReason_REPLACE_BY_TRIGGERS ResourceInstanceActionReason = 9 + ResourceInstanceActionReason_READ_BECAUSE_CONFIG_UNKNOWN ResourceInstanceActionReason = 10 + ResourceInstanceActionReason_READ_BECAUSE_DEPENDENCY_PENDING ResourceInstanceActionReason = 11 + ResourceInstanceActionReason_READ_BECAUSE_CHECK_NESTED ResourceInstanceActionReason = 13 + ResourceInstanceActionReason_DELETE_BECAUSE_NO_MOVE_TARGET ResourceInstanceActionReason = 12 +) + +// Enum value maps for ResourceInstanceActionReason. +var ( + ResourceInstanceActionReason_name = map[int32]string{ + 0: "NONE", + 1: "REPLACE_BECAUSE_TAINTED", + 2: "REPLACE_BY_REQUEST", + 3: "REPLACE_BECAUSE_CANNOT_UPDATE", + 4: "DELETE_BECAUSE_NO_RESOURCE_CONFIG", + 5: "DELETE_BECAUSE_WRONG_REPETITION", + 6: "DELETE_BECAUSE_COUNT_INDEX", + 7: "DELETE_BECAUSE_EACH_KEY", + 8: "DELETE_BECAUSE_NO_MODULE", + 9: "REPLACE_BY_TRIGGERS", + 10: "READ_BECAUSE_CONFIG_UNKNOWN", + 11: "READ_BECAUSE_DEPENDENCY_PENDING", + 13: "READ_BECAUSE_CHECK_NESTED", + 12: "DELETE_BECAUSE_NO_MOVE_TARGET", + } + ResourceInstanceActionReason_value = map[string]int32{ + "NONE": 0, + "REPLACE_BECAUSE_TAINTED": 1, + "REPLACE_BY_REQUEST": 2, + "REPLACE_BECAUSE_CANNOT_UPDATE": 3, + "DELETE_BECAUSE_NO_RESOURCE_CONFIG": 4, + "DELETE_BECAUSE_WRONG_REPETITION": 5, + "DELETE_BECAUSE_COUNT_INDEX": 6, + "DELETE_BECAUSE_EACH_KEY": 7, + "DELETE_BECAUSE_NO_MODULE": 8, + "REPLACE_BY_TRIGGERS": 9, + "READ_BECAUSE_CONFIG_UNKNOWN": 10, + "READ_BECAUSE_DEPENDENCY_PENDING": 11, + "READ_BECAUSE_CHECK_NESTED": 13, + "DELETE_BECAUSE_NO_MOVE_TARGET": 12, + } +) + +func (x ResourceInstanceActionReason) Enum() *ResourceInstanceActionReason { + p := new(ResourceInstanceActionReason) + *p = x + return p +} + +func (x ResourceInstanceActionReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResourceInstanceActionReason) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[2].Descriptor() +} + +func (ResourceInstanceActionReason) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[2] +} + +func (x ResourceInstanceActionReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResourceInstanceActionReason.Descriptor instead. +func (ResourceInstanceActionReason) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{2} +} + +// DeferredReason describes the reason why a resource instance change was +// deferred. +type DeferredReason int32 + +const ( + DeferredReason_INVALID DeferredReason = 0 + DeferredReason_INSTANCE_COUNT_UNKNOWN DeferredReason = 1 + DeferredReason_RESOURCE_CONFIG_UNKNOWN DeferredReason = 2 + DeferredReason_PROVIDER_CONFIG_UNKNOWN DeferredReason = 3 + DeferredReason_ABSENT_PREREQ DeferredReason = 4 + DeferredReason_DEFERRED_PREREQ DeferredReason = 5 +) + +// Enum value maps for DeferredReason. +var ( + DeferredReason_name = map[int32]string{ + 0: "INVALID", + 1: "INSTANCE_COUNT_UNKNOWN", + 2: "RESOURCE_CONFIG_UNKNOWN", + 3: "PROVIDER_CONFIG_UNKNOWN", + 4: "ABSENT_PREREQ", + 5: "DEFERRED_PREREQ", + } + DeferredReason_value = map[string]int32{ + "INVALID": 0, + "INSTANCE_COUNT_UNKNOWN": 1, + "RESOURCE_CONFIG_UNKNOWN": 2, + "PROVIDER_CONFIG_UNKNOWN": 3, + "ABSENT_PREREQ": 4, + "DEFERRED_PREREQ": 5, + } +) + +func (x DeferredReason) Enum() *DeferredReason { + p := new(DeferredReason) + *p = x + return p +} + +func (x DeferredReason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (DeferredReason) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[3].Descriptor() +} + +func (DeferredReason) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[3] +} + +func (x DeferredReason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use DeferredReason.Descriptor instead. +func (DeferredReason) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{3} +} + +// Status describes the status of a particular checkable object at the +// completion of the plan. +type CheckResults_Status int32 + +const ( + CheckResults_UNKNOWN CheckResults_Status = 0 + CheckResults_PASS CheckResults_Status = 1 + CheckResults_FAIL CheckResults_Status = 2 + CheckResults_ERROR CheckResults_Status = 3 +) + +// Enum value maps for CheckResults_Status. +var ( + CheckResults_Status_name = map[int32]string{ + 0: "UNKNOWN", + 1: "PASS", + 2: "FAIL", + 3: "ERROR", + } + CheckResults_Status_value = map[string]int32{ + "UNKNOWN": 0, + "PASS": 1, + "FAIL": 2, + "ERROR": 3, + } +) + +func (x CheckResults_Status) Enum() *CheckResults_Status { + p := new(CheckResults_Status) + *p = x + return p +} + +func (x CheckResults_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CheckResults_Status) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[4].Descriptor() +} + +func (CheckResults_Status) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[4] +} + +func (x CheckResults_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CheckResults_Status.Descriptor instead. +func (CheckResults_Status) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{6, 0} +} + +type CheckResults_ObjectKind int32 + +const ( + CheckResults_UNSPECIFIED CheckResults_ObjectKind = 0 + CheckResults_RESOURCE CheckResults_ObjectKind = 1 + CheckResults_OUTPUT_VALUE CheckResults_ObjectKind = 2 + CheckResults_CHECK CheckResults_ObjectKind = 3 + CheckResults_INPUT_VARIABLE CheckResults_ObjectKind = 4 +) + +// Enum value maps for CheckResults_ObjectKind. +var ( + CheckResults_ObjectKind_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "RESOURCE", + 2: "OUTPUT_VALUE", + 3: "CHECK", + 4: "INPUT_VARIABLE", + } + CheckResults_ObjectKind_value = map[string]int32{ + "UNSPECIFIED": 0, + "RESOURCE": 1, + "OUTPUT_VALUE": 2, + "CHECK": 3, + "INPUT_VARIABLE": 4, + } +) + +func (x CheckResults_ObjectKind) Enum() *CheckResults_ObjectKind { + p := new(CheckResults_ObjectKind) + *p = x + return p +} + +func (x CheckResults_ObjectKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (CheckResults_ObjectKind) Descriptor() protoreflect.EnumDescriptor { + return file_planfile_proto_enumTypes[5].Descriptor() +} + +func (CheckResults_ObjectKind) Type() protoreflect.EnumType { + return &file_planfile_proto_enumTypes[5] +} + +func (x CheckResults_ObjectKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use CheckResults_ObjectKind.Descriptor instead. +func (CheckResults_ObjectKind) EnumDescriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{6, 1} +} + +// Plan is the root message type for the tfplan file +type Plan struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Version is incremented whenever there is a breaking change to + // the serialization format. Programs reading serialized plans should + // verify that version is set to the expected value and abort processing + // if not. A breaking change is any change that may cause an older + // consumer to interpret the structure incorrectly. This number will + // not be incremented if an existing consumer can either safely ignore + // changes to the format or if an existing consumer would fail to process + // the file for another message- or field-specific reason. + Version uint64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // The mode that was active when this plan was created. + // + // This is saved only for UI purposes, so that Terraform can tailor its + // rendering of the plan depending on the mode. This must never be used to + // make decisions in Terraform Core during the applying of a plan. + UiMode Mode `protobuf:"varint,17,opt,name=ui_mode,json=uiMode,proto3,enum=tfplan.Mode" json:"ui_mode,omitempty"` + // Applyable is true for any plan where it makes sense to ask an operator + // to approve it and then ask Terraform to apply it. + // + // The other fields provide more context about why a non-applyable plan + // is not applyable, but this field is here so that if new situations + // arise in future then old callers can at least still make the right + // decision about whether to show the approval prompt, even if they don't + // know yet why a particular plan isn't applyable and have to just use + // a generic error message. + Applyable bool `protobuf:"varint,25,opt,name=applyable,proto3" json:"applyable,omitempty"` + // Complete is true for a plan that includes a planned action for every + // resource input object that was present across the desired state and + // prior state, even if the planned action is "no-op". + // + // Conversely, if this field is false then the plan deals with only a + // subset of those objects. The reason for that should always be + // determinable based on other fields in this message, but this flag + // is here to ensure that if new situations arise in future then old + // callers can at least still show a generic message about the plan + // being incomplete, even if they don't know yet how to explain the + // reason to the operator. + Complete bool `protobuf:"varint,26,opt,name=complete,proto3" json:"complete,omitempty"` + // Errored is true for any plan whose creation was interrupted by an + // error. A plan with this flag set cannot be applied + // (i.e. applyable = false), and the changes it proposes are likely to be + // incomplete. + Errored bool `protobuf:"varint,20,opt,name=errored,proto3" json:"errored,omitempty"` + // The variables that were set when creating the plan. Each value is + // a msgpack serialization of an HCL value. + Variables map[string]*DynamicValue `protobuf:"bytes,2,rep,name=variables,proto3" json:"variables,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Variables whose values must be provided during the apply phase. + ApplyTimeVariables []string `protobuf:"bytes,28,rep,name=apply_time_variables,json=applyTimeVariables,proto3" json:"apply_time_variables,omitempty"` + // An unordered set of proposed changes to resources throughout the + // configuration, including any nested modules. Use the address of + // each resource to determine which module it belongs to. + ResourceChanges []*ResourceInstanceChange `protobuf:"bytes,3,rep,name=resource_changes,json=resourceChanges,proto3" json:"resource_changes,omitempty"` + // An unordered set of detected drift: changes made to resources outside of + // Terraform, computed by comparing the previous run's state to the state + // after refresh. + ResourceDrift []*ResourceInstanceChange `protobuf:"bytes,18,rep,name=resource_drift,json=resourceDrift,proto3" json:"resource_drift,omitempty"` + // An unordered set of deferred changes. These are changes that will be + // applied in a subsequent plan, but were deferred in this plan for some + // reason. Generally, if complete is set to false there should be entries + // in this list. + DeferredChanges []*DeferredResourceInstanceChange `protobuf:"bytes,27,rep,name=deferred_changes,json=deferredChanges,proto3" json:"deferred_changes,omitempty"` + // An unordered set of proposed changes to outputs in the root module + // of the configuration. This set also includes "no action" changes for + // outputs that are not changing, as context for detecting inconsistencies + // at apply time. + OutputChanges []*OutputChange `protobuf:"bytes,4,rep,name=output_changes,json=outputChanges,proto3" json:"output_changes,omitempty"` + // An unordered set of check results for the entire configuration. + // + // Each element represents a single static configuration object that has + // checks, and each of those may have zero or more dynamic objects that + // the checks were applied to nested within. + CheckResults []*CheckResults `protobuf:"bytes,19,rep,name=check_results,json=checkResults,proto3" json:"check_results,omitempty"` + // An unordered set of target addresses to include when applying. If no + // target addresses are present, the plan applies to the whole + // configuration. + TargetAddrs []string `protobuf:"bytes,5,rep,name=target_addrs,json=targetAddrs,proto3" json:"target_addrs,omitempty"` + // An unordered set of force-replace addresses to include when applying. + // This must match the set of addresses that was used when creating the + // plan, or else applying the plan will fail when it reaches a different + // conclusion about what action a particular resource instance needs. + ForceReplaceAddrs []string `protobuf:"bytes,16,rep,name=force_replace_addrs,json=forceReplaceAddrs,proto3" json:"force_replace_addrs,omitempty"` + // The version string for the Terraform binary that created this plan. + TerraformVersion string `protobuf:"bytes,14,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` + // Backend is a description of the backend configuration and other related + // settings at the time the plan was created. + Backend *Backend `protobuf:"bytes,13,opt,name=backend,proto3" json:"backend,omitempty"` + // RelevantAttributes lists individual resource attributes from + // ResourceDrift which may have contributed to the plan changes. + RelevantAttributes []*PlanResourceAttr `protobuf:"bytes,15,rep,name=relevant_attributes,json=relevantAttributes,proto3" json:"relevant_attributes,omitempty"` + // timestamp is the record of truth for when the plan happened. + Timestamp string `protobuf:"bytes,21,opt,name=timestamp,proto3" json:"timestamp,omitempty"` + FunctionResults []*FunctionCallHash `protobuf:"bytes,22,rep,name=function_results,json=functionResults,proto3" json:"function_results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Plan) Reset() { + *x = Plan{} + mi := &file_planfile_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Plan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Plan) ProtoMessage() {} + +func (x *Plan) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Plan.ProtoReflect.Descriptor instead. +func (*Plan) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{0} +} + +func (x *Plan) GetVersion() uint64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Plan) GetUiMode() Mode { + if x != nil { + return x.UiMode + } + return Mode_NORMAL +} + +func (x *Plan) GetApplyable() bool { + if x != nil { + return x.Applyable + } + return false +} + +func (x *Plan) GetComplete() bool { + if x != nil { + return x.Complete + } + return false +} + +func (x *Plan) GetErrored() bool { + if x != nil { + return x.Errored + } + return false +} + +func (x *Plan) GetVariables() map[string]*DynamicValue { + if x != nil { + return x.Variables + } + return nil +} + +func (x *Plan) GetApplyTimeVariables() []string { + if x != nil { + return x.ApplyTimeVariables + } + return nil +} + +func (x *Plan) GetResourceChanges() []*ResourceInstanceChange { + if x != nil { + return x.ResourceChanges + } + return nil +} + +func (x *Plan) GetResourceDrift() []*ResourceInstanceChange { + if x != nil { + return x.ResourceDrift + } + return nil +} + +func (x *Plan) GetDeferredChanges() []*DeferredResourceInstanceChange { + if x != nil { + return x.DeferredChanges + } + return nil +} + +func (x *Plan) GetOutputChanges() []*OutputChange { + if x != nil { + return x.OutputChanges + } + return nil +} + +func (x *Plan) GetCheckResults() []*CheckResults { + if x != nil { + return x.CheckResults + } + return nil +} + +func (x *Plan) GetTargetAddrs() []string { + if x != nil { + return x.TargetAddrs + } + return nil +} + +func (x *Plan) GetForceReplaceAddrs() []string { + if x != nil { + return x.ForceReplaceAddrs + } + return nil +} + +func (x *Plan) GetTerraformVersion() string { + if x != nil { + return x.TerraformVersion + } + return "" +} + +func (x *Plan) GetBackend() *Backend { + if x != nil { + return x.Backend + } + return nil +} + +func (x *Plan) GetRelevantAttributes() []*PlanResourceAttr { + if x != nil { + return x.RelevantAttributes + } + return nil +} + +func (x *Plan) GetTimestamp() string { + if x != nil { + return x.Timestamp + } + return "" +} + +func (x *Plan) GetFunctionResults() []*FunctionCallHash { + if x != nil { + return x.FunctionResults + } + return nil +} + +// Backend is a description of backend configuration and other related settings. +type Backend struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + Workspace string `protobuf:"bytes,3,opt,name=workspace,proto3" json:"workspace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Backend) Reset() { + *x = Backend{} + mi := &file_planfile_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Backend) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Backend) ProtoMessage() {} + +func (x *Backend) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Backend.ProtoReflect.Descriptor instead. +func (*Backend) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{1} +} + +func (x *Backend) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Backend) GetConfig() *DynamicValue { + if x != nil { + return x.Config + } + return nil +} + +func (x *Backend) GetWorkspace() string { + if x != nil { + return x.Workspace + } + return "" +} + +// Change represents a change made to some object, transforming it from an old +// state to a new state. +type Change struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Not all action values are valid for all object types. Consult + // the documentation for any message that embeds Change. + Action Action `protobuf:"varint,1,opt,name=action,proto3,enum=tfplan.Action" json:"action,omitempty"` + // msgpack-encoded HCL values involved in the change. + // - For update and replace, two values are provided that give the old and new values, + // respectively. + // - For create, one value is provided that gives the new value to be created + // - For delete, one value is provided that describes the value being deleted + // - For read, two values are provided that give the prior value for this object + // (or null, if no prior value exists) and the value that was or will be read, + // respectively. + // - For no-op, one value is provided that is left unmodified by this non-change. + Values []*DynamicValue `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` + // An unordered set of paths into the old value which are marked as + // sensitive. Values at these paths should be obscured in human-readable + // output. This set is always empty for create. + BeforeSensitivePaths []*Path `protobuf:"bytes,3,rep,name=before_sensitive_paths,json=beforeSensitivePaths,proto3" json:"before_sensitive_paths,omitempty"` + // An unordered set of paths into the new value which are marked as + // sensitive. Values at these paths should be obscured in human-readable + // output. This set is always empty for delete. + AfterSensitivePaths []*Path `protobuf:"bytes,4,rep,name=after_sensitive_paths,json=afterSensitivePaths,proto3" json:"after_sensitive_paths,omitempty"` + // Importing, if true, specifies that the resource is being imported as part + // of the change. + Importing *Importing `protobuf:"bytes,5,opt,name=importing,proto3" json:"importing,omitempty"` + // GeneratedConfig contains any configuration that was generated as part of + // the change, as an HCL string. + GeneratedConfig string `protobuf:"bytes,6,opt,name=generated_config,json=generatedConfig,proto3" json:"generated_config,omitempty"` + // The resource identity before the plan operation + BeforeIdentity *DynamicValue `protobuf:"bytes,7,opt,name=before_identity,json=beforeIdentity,proto3" json:"before_identity,omitempty"` + // The resource identity after the plan operation + AfterIdentity *DynamicValue `protobuf:"bytes,8,opt,name=after_identity,json=afterIdentity,proto3" json:"after_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Change) Reset() { + *x = Change{} + mi := &file_planfile_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Change) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Change) ProtoMessage() {} + +func (x *Change) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Change.ProtoReflect.Descriptor instead. +func (*Change) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{2} +} + +func (x *Change) GetAction() Action { + if x != nil { + return x.Action + } + return Action_NOOP +} + +func (x *Change) GetValues() []*DynamicValue { + if x != nil { + return x.Values + } + return nil +} + +func (x *Change) GetBeforeSensitivePaths() []*Path { + if x != nil { + return x.BeforeSensitivePaths + } + return nil +} + +func (x *Change) GetAfterSensitivePaths() []*Path { + if x != nil { + return x.AfterSensitivePaths + } + return nil +} + +func (x *Change) GetImporting() *Importing { + if x != nil { + return x.Importing + } + return nil +} + +func (x *Change) GetGeneratedConfig() string { + if x != nil { + return x.GeneratedConfig + } + return "" +} + +func (x *Change) GetBeforeIdentity() *DynamicValue { + if x != nil { + return x.BeforeIdentity + } + return nil +} + +func (x *Change) GetAfterIdentity() *DynamicValue { + if x != nil { + return x.AfterIdentity + } + return nil +} + +type ResourceInstanceChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // addr is a string representation of the resource instance address that + // this change will apply to. + Addr string `protobuf:"bytes,13,opt,name=addr,proto3" json:"addr,omitempty"` + // prev_run_addr is a string representation of the address at which + // this resource instance was tracked during the previous apply operation. + // + // This is populated only if it would be different from addr due to + // Terraform having reacted to refactoring annotations in the configuration. + // If empty, the previous run address is the same as the current address. + PrevRunAddr string `protobuf:"bytes,14,opt,name=prev_run_addr,json=prevRunAddr,proto3" json:"prev_run_addr,omitempty"` + // deposed_key, if set, indicates that this change applies to a deposed + // object for the indicated instance with the given deposed key. If not + // set, the change applies to the instance's current object. + DeposedKey string `protobuf:"bytes,7,opt,name=deposed_key,json=deposedKey,proto3" json:"deposed_key,omitempty"` + // provider is the address of the provider configuration that this change + // was planned with, and thus the configuration that must be used to + // apply it. + Provider string `protobuf:"bytes,8,opt,name=provider,proto3" json:"provider,omitempty"` + // Description of the proposed change. May use "create", "read", "update", + // "replace", "delete" and "no-op" actions. + Change *Change `protobuf:"bytes,9,opt,name=change,proto3" json:"change,omitempty"` + // raw blob value provided by the provider as additional context for the + // change. Must be considered an opaque value for any consumer other than + // the provider that generated it, and will be returned verbatim to the + // provider during the subsequent apply operation. + Private []byte `protobuf:"bytes,10,opt,name=private,proto3" json:"private,omitempty"` + // An unordered set of paths that prompted the change action to be + // "replace" rather than "update". Empty for any action other than + // "replace". + RequiredReplace []*Path `protobuf:"bytes,11,rep,name=required_replace,json=requiredReplace,proto3" json:"required_replace,omitempty"` + // Optional extra user-oriented context for why change.Action was chosen. + // This is for user feedback only and never used to drive behavior during + // apply. + ActionReason ResourceInstanceActionReason `protobuf:"varint,12,opt,name=action_reason,json=actionReason,proto3,enum=tfplan.ResourceInstanceActionReason" json:"action_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceInstanceChange) Reset() { + *x = ResourceInstanceChange{} + mi := &file_planfile_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceInstanceChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceInstanceChange) ProtoMessage() {} + +func (x *ResourceInstanceChange) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceInstanceChange.ProtoReflect.Descriptor instead. +func (*ResourceInstanceChange) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{3} +} + +func (x *ResourceInstanceChange) GetAddr() string { + if x != nil { + return x.Addr + } + return "" +} + +func (x *ResourceInstanceChange) GetPrevRunAddr() string { + if x != nil { + return x.PrevRunAddr + } + return "" +} + +func (x *ResourceInstanceChange) GetDeposedKey() string { + if x != nil { + return x.DeposedKey + } + return "" +} + +func (x *ResourceInstanceChange) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *ResourceInstanceChange) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +func (x *ResourceInstanceChange) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +func (x *ResourceInstanceChange) GetRequiredReplace() []*Path { + if x != nil { + return x.RequiredReplace + } + return nil +} + +func (x *ResourceInstanceChange) GetActionReason() ResourceInstanceActionReason { + if x != nil { + return x.ActionReason + } + return ResourceInstanceActionReason_NONE +} + +// DeferredResourceInstanceChange represents a resource instance change that +// was deferred for some reason. +// +// It contains the original change that was deferred, along with the reason +// why it was deferred. +type DeferredResourceInstanceChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The reason why the change was deferred. + Deferred *Deferred `protobuf:"bytes,1,opt,name=deferred,proto3" json:"deferred,omitempty"` + // The original change that was deferred. + Change *ResourceInstanceChange `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeferredResourceInstanceChange) Reset() { + *x = DeferredResourceInstanceChange{} + mi := &file_planfile_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeferredResourceInstanceChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeferredResourceInstanceChange) ProtoMessage() {} + +func (x *DeferredResourceInstanceChange) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeferredResourceInstanceChange.ProtoReflect.Descriptor instead. +func (*DeferredResourceInstanceChange) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{4} +} + +func (x *DeferredResourceInstanceChange) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +func (x *DeferredResourceInstanceChange) GetChange() *ResourceInstanceChange { + if x != nil { + return x.Change + } + return nil +} + +type OutputChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Name of the output as defined in the root module. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Description of the proposed change. May use "no-op", "create", + // "update" and "delete" actions. + Change *Change `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` + // Sensitive, if true, indicates that one or more of the values given + // in "change" is sensitive and should not be shown directly in any + // rendered plan. + Sensitive bool `protobuf:"varint,3,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OutputChange) Reset() { + *x = OutputChange{} + mi := &file_planfile_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OutputChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OutputChange) ProtoMessage() {} + +func (x *OutputChange) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OutputChange.ProtoReflect.Descriptor instead. +func (*OutputChange) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{5} +} + +func (x *OutputChange) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OutputChange) GetChange() *Change { + if x != nil { + return x.Change + } + return nil +} + +func (x *OutputChange) GetSensitive() bool { + if x != nil { + return x.Sensitive + } + return false +} + +type CheckResults struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind CheckResults_ObjectKind `protobuf:"varint,1,opt,name=kind,proto3,enum=tfplan.CheckResults_ObjectKind" json:"kind,omitempty"` + // Address of the configuration object that declared the checks. + ConfigAddr string `protobuf:"bytes,2,opt,name=config_addr,json=configAddr,proto3" json:"config_addr,omitempty"` + // The aggregate status of the entire configuration object, based on + // the statuses of its zero or more checkable objects. + Status CheckResults_Status `protobuf:"varint,3,opt,name=status,proto3,enum=tfplan.CheckResults_Status" json:"status,omitempty"` + // The results for individual objects that were declared by the + // configuration object named in config_addr. + Objects []*CheckResults_ObjectResult `protobuf:"bytes,4,rep,name=objects,proto3" json:"objects,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResults) Reset() { + *x = CheckResults{} + mi := &file_planfile_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResults) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResults) ProtoMessage() {} + +func (x *CheckResults) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResults.ProtoReflect.Descriptor instead. +func (*CheckResults) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{6} +} + +func (x *CheckResults) GetKind() CheckResults_ObjectKind { + if x != nil { + return x.Kind + } + return CheckResults_UNSPECIFIED +} + +func (x *CheckResults) GetConfigAddr() string { + if x != nil { + return x.ConfigAddr + } + return "" +} + +func (x *CheckResults) GetStatus() CheckResults_Status { + if x != nil { + return x.Status + } + return CheckResults_UNKNOWN +} + +func (x *CheckResults) GetObjects() []*CheckResults_ObjectResult { + if x != nil { + return x.Objects + } + return nil +} + +// FunctionCallHash stores a record of a hashed function call and +// result. This is used internally to ensure that providers return consistent +// values between plan and apply given the same arguments. +type FunctionCallHash struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key []byte `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Result []byte `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FunctionCallHash) Reset() { + *x = FunctionCallHash{} + mi := &file_planfile_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FunctionCallHash) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionCallHash) ProtoMessage() {} + +func (x *FunctionCallHash) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FunctionCallHash.ProtoReflect.Descriptor instead. +func (*FunctionCallHash) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{7} +} + +func (x *FunctionCallHash) GetKey() []byte { + if x != nil { + return x.Key + } + return nil +} + +func (x *FunctionCallHash) GetResult() []byte { + if x != nil { + return x.Result + } + return nil +} + +// DynamicValue represents a value whose type is not decided until runtime, +// often based on schema information obtained from a plugin. +// +// At present dynamic values are always encoded as msgpack, with extension +// id 0 used to represent the special "unknown" value indicating results +// that won't be known until after apply. +// +// In future other serialization formats may be used, possibly with a +// transitional period of including both as separate attributes of this type. +// Consumers must ignore attributes they don't support and fail if no supported +// attribute is present. The top-level format version will not be incremented +// for changes to the set of dynamic serialization formats. +type DynamicValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DynamicValue) Reset() { + *x = DynamicValue{} + mi := &file_planfile_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DynamicValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicValue) ProtoMessage() {} + +func (x *DynamicValue) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicValue.ProtoReflect.Descriptor instead. +func (*DynamicValue) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{8} +} + +func (x *DynamicValue) GetMsgpack() []byte { + if x != nil { + return x.Msgpack + } + return nil +} + +// Path represents a set of steps to traverse into a data structure. It is +// used to refer to a sub-structure within a dynamic data structure presented +// separately. +type Path struct { + state protoimpl.MessageState `protogen:"open.v1"` + Steps []*Path_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Path) Reset() { + *x = Path{} + mi := &file_planfile_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Path) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Path) ProtoMessage() {} + +func (x *Path) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Path.ProtoReflect.Descriptor instead. +func (*Path) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{9} +} + +func (x *Path) GetSteps() []*Path_Step { + if x != nil { + return x.Steps + } + return nil +} + +// Importing contains the embedded metadata about the import operation if this +// change describes it. +type Importing struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The original ID of the resource. + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // unknown is true if the original ID of the resource is unknown. + Unknown bool `protobuf:"varint,2,opt,name=unknown,proto3" json:"unknown,omitempty"` + // Identity can be used to import instead of id + Identity *DynamicValue `protobuf:"bytes,3,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Importing) Reset() { + *x = Importing{} + mi := &file_planfile_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Importing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Importing) ProtoMessage() {} + +func (x *Importing) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Importing.ProtoReflect.Descriptor instead. +func (*Importing) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{10} +} + +func (x *Importing) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Importing) GetUnknown() bool { + if x != nil { + return x.Unknown + } + return false +} + +func (x *Importing) GetIdentity() *DynamicValue { + if x != nil { + return x.Identity + } + return nil +} + +// Deferred contains all the metadata about a the deferral of a resource +// instance change. +type Deferred struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reason DeferredReason `protobuf:"varint,1,opt,name=reason,proto3,enum=tfplan.DeferredReason" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Deferred) Reset() { + *x = Deferred{} + mi := &file_planfile_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Deferred) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Deferred) ProtoMessage() {} + +func (x *Deferred) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Deferred.ProtoReflect.Descriptor instead. +func (*Deferred) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{11} +} + +func (x *Deferred) GetReason() DeferredReason { + if x != nil { + return x.Reason + } + return DeferredReason_INVALID +} + +type PlanResourceAttr struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource string `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Attr *Path `protobuf:"bytes,2,opt,name=attr,proto3" json:"attr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanResourceAttr) Reset() { + *x = PlanResourceAttr{} + mi := &file_planfile_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanResourceAttr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanResourceAttr) ProtoMessage() {} + +func (x *PlanResourceAttr) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanResourceAttr.ProtoReflect.Descriptor instead. +func (*PlanResourceAttr) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *PlanResourceAttr) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *PlanResourceAttr) GetAttr() *Path { + if x != nil { + return x.Attr + } + return nil +} + +type CheckResults_ObjectResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + ObjectAddr string `protobuf:"bytes,1,opt,name=object_addr,json=objectAddr,proto3" json:"object_addr,omitempty"` + Status CheckResults_Status `protobuf:"varint,2,opt,name=status,proto3,enum=tfplan.CheckResults_Status" json:"status,omitempty"` + FailureMessages []string `protobuf:"bytes,3,rep,name=failure_messages,json=failureMessages,proto3" json:"failure_messages,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CheckResults_ObjectResult) Reset() { + *x = CheckResults_ObjectResult{} + mi := &file_planfile_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CheckResults_ObjectResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CheckResults_ObjectResult) ProtoMessage() {} + +func (x *CheckResults_ObjectResult) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CheckResults_ObjectResult.ProtoReflect.Descriptor instead. +func (*CheckResults_ObjectResult) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{6, 0} +} + +func (x *CheckResults_ObjectResult) GetObjectAddr() string { + if x != nil { + return x.ObjectAddr + } + return "" +} + +func (x *CheckResults_ObjectResult) GetStatus() CheckResults_Status { + if x != nil { + return x.Status + } + return CheckResults_UNKNOWN +} + +func (x *CheckResults_ObjectResult) GetFailureMessages() []string { + if x != nil { + return x.FailureMessages + } + return nil +} + +type Path_Step struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selector: + // + // *Path_Step_AttributeName + // *Path_Step_ElementKey + Selector isPath_Step_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Path_Step) Reset() { + *x = Path_Step{} + mi := &file_planfile_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Path_Step) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Path_Step) ProtoMessage() {} + +func (x *Path_Step) ProtoReflect() protoreflect.Message { + mi := &file_planfile_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Path_Step.ProtoReflect.Descriptor instead. +func (*Path_Step) Descriptor() ([]byte, []int) { + return file_planfile_proto_rawDescGZIP(), []int{9, 0} +} + +func (x *Path_Step) GetSelector() isPath_Step_Selector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *Path_Step) GetAttributeName() string { + if x != nil { + if x, ok := x.Selector.(*Path_Step_AttributeName); ok { + return x.AttributeName + } + } + return "" +} + +func (x *Path_Step) GetElementKey() *DynamicValue { + if x != nil { + if x, ok := x.Selector.(*Path_Step_ElementKey); ok { + return x.ElementKey + } + } + return nil +} + +type isPath_Step_Selector interface { + isPath_Step_Selector() +} + +type Path_Step_AttributeName struct { + // Set "attribute_name" to represent looking up an attribute + // in the current object value. + AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3,oneof"` +} + +type Path_Step_ElementKey struct { + // Set "element_key" to represent looking up an element in + // an indexable collection type. + ElementKey *DynamicValue `protobuf:"bytes,2,opt,name=element_key,json=elementKey,proto3,oneof"` +} + +func (*Path_Step_AttributeName) isPath_Step_Selector() {} + +func (*Path_Step_ElementKey) isPath_Step_Selector() {} + +var File_planfile_proto protoreflect.FileDescriptor + +var file_planfile_proto_rawDesc = string([]byte{ + 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x06, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x22, 0xe3, 0x08, 0x0a, 0x04, 0x50, 0x6c, 0x61, + 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x07, 0x75, + 0x69, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x06, 0x75, 0x69, 0x4d, 0x6f, + 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x18, + 0x19, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, + 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x1a, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, + 0x72, 0x72, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x39, 0x0a, 0x09, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x61, 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x73, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, + 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x1c, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x12, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x73, 0x12, 0x49, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0f, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x45, + 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x64, 0x72, 0x69, 0x66, 0x74, + 0x18, 0x12, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x44, 0x72, 0x69, 0x66, 0x74, 0x12, 0x51, 0x0a, 0x10, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x1b, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x0e, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x39, 0x0a, 0x0d, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x13, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x52, 0x0c, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, + 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x41, 0x64, + 0x64, 0x72, 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x70, + 0x6c, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x11, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x41, 0x64, + 0x64, 0x72, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x29, 0x0a, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x52, 0x07, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x4b, 0x0a, 0x13, 0x72, + 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, + 0x6e, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x61, 0x74, 0x74, 0x72, 0x52, 0x12, 0x72, 0x65, 0x6c, 0x65, 0x76, 0x61, 0x6e, 0x74, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x43, 0x0a, 0x10, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x16, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x52, 0x0f, 0x66, 0x75, 0x6e, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x1a, 0x52, 0x0a, 0x0e, 0x56, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x4d, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x74, 0x74, 0x72, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x04, + 0x61, 0x74, 0x74, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x04, 0x61, 0x74, 0x74, 0x72, 0x22, 0x69, + 0x0a, 0x07, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2c, 0x0a, + 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0xbc, 0x03, 0x0a, 0x06, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x06, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x16, 0x62, 0x65, + 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, + 0x61, 0x74, 0x68, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x14, 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, + 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x40, + 0x0a, 0x15, 0x61, 0x66, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, + 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x13, 0x61, 0x66, 0x74, + 0x65, 0x72, 0x53, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, + 0x12, 0x2f, 0x0a, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x49, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x52, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, + 0x67, 0x12, 0x29, 0x0a, 0x10, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x67, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3d, 0x0a, 0x0f, + 0x62, 0x65, 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, + 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x62, 0x65, 0x66, + 0x6f, 0x72, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x3b, 0x0a, 0x0e, 0x61, + 0x66, 0x74, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x61, 0x66, 0x74, 0x65, 0x72, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xd3, 0x02, 0x0a, 0x16, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x22, 0x0a, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x5f, + 0x72, 0x75, 0x6e, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x70, 0x72, 0x65, 0x76, 0x52, 0x75, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x64, + 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, + 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x10, 0x72, 0x65, + 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x0b, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, + 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x52, 0x65, 0x70, 0x6c, + 0x61, 0x63, 0x65, 0x12, 0x49, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x86, + 0x01, 0x0a, 0x1e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x12, 0x2c, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, + 0x36, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, + 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x68, 0x0a, 0x0c, 0x4f, 0x75, 0x74, 0x70, 0x75, + 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x06, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, + 0x65, 0x22, 0xfc, 0x03, 0x0a, 0x0c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x12, 0x33, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, + 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x41, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, + 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, + 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x73, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x52, 0x07, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x1a, 0x8f, 0x01, 0x0a, 0x0c, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x33, 0x0a, 0x06, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x12, 0x29, 0x0a, 0x10, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x66, 0x61, 0x69, + 0x6c, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x22, 0x34, 0x0a, 0x06, + 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x41, 0x53, 0x53, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x46, 0x41, 0x49, 0x4c, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, + 0x10, 0x03, 0x22, 0x5c, 0x0a, 0x0a, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x4b, 0x69, 0x6e, 0x64, + 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x10, 0x01, 0x12, + 0x10, 0x0a, 0x0c, 0x4f, 0x55, 0x54, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x4c, 0x55, 0x45, 0x10, + 0x02, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, + 0x49, 0x4e, 0x50, 0x55, 0x54, 0x5f, 0x56, 0x41, 0x52, 0x49, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x04, + 0x22, 0x3c, 0x0a, 0x10, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, + 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x28, + 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, + 0x0a, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x22, 0xa5, 0x01, 0x0a, 0x04, 0x50, 0x61, 0x74, + 0x68, 0x12, 0x27, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, + 0x74, 0x65, 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x74, 0x0a, 0x04, 0x53, 0x74, + 0x65, 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x65, + 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0a, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, + 0x74, 0x4b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x22, 0x67, 0x0a, 0x09, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, + 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, + 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x12, 0x30, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x3a, 0x0a, 0x08, 0x44, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x2e, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x31, 0x0a, 0x04, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, + 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, + 0x54, 0x52, 0x4f, 0x59, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, + 0x48, 0x5f, 0x4f, 0x4e, 0x4c, 0x59, 0x10, 0x02, 0x2a, 0x94, 0x01, 0x0a, 0x06, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x0a, 0x0a, + 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, + 0x44, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, + 0x0a, 0x0a, 0x06, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x05, 0x12, 0x16, 0x0a, 0x12, 0x44, + 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x43, 0x52, 0x45, 0x41, 0x54, + 0x45, 0x10, 0x06, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x5f, 0x54, 0x48, + 0x45, 0x4e, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x07, 0x12, 0x0a, 0x0a, 0x06, 0x46, + 0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x08, 0x12, 0x16, 0x0a, 0x12, 0x43, 0x52, 0x45, 0x41, 0x54, + 0x45, 0x5f, 0x54, 0x48, 0x45, 0x4e, 0x5f, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, 0x10, 0x09, 0x2a, + 0xc8, 0x03, 0x0a, 0x1c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, + 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, + 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x54, 0x41, + 0x49, 0x4e, 0x54, 0x45, 0x44, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x52, 0x45, 0x50, 0x4c, 0x41, + 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x45, 0x53, 0x54, 0x10, 0x02, 0x12, + 0x21, 0x0a, 0x1d, 0x52, 0x45, 0x50, 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, + 0x53, 0x45, 0x5f, 0x43, 0x41, 0x4e, 0x4e, 0x4f, 0x54, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, + 0x10, 0x03, 0x12, 0x25, 0x0a, 0x21, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, + 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, + 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x4c, + 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x57, 0x52, 0x4f, 0x4e, + 0x47, 0x5f, 0x52, 0x45, 0x50, 0x45, 0x54, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x05, 0x12, 0x1e, + 0x0a, 0x1a, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, + 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x49, 0x4e, 0x44, 0x45, 0x58, 0x10, 0x06, 0x12, 0x1b, + 0x0a, 0x17, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, + 0x5f, 0x45, 0x41, 0x43, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x07, 0x12, 0x1c, 0x0a, 0x18, 0x44, + 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, + 0x5f, 0x4d, 0x4f, 0x44, 0x55, 0x4c, 0x45, 0x10, 0x08, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x50, + 0x4c, 0x41, 0x43, 0x45, 0x5f, 0x42, 0x59, 0x5f, 0x54, 0x52, 0x49, 0x47, 0x47, 0x45, 0x52, 0x53, + 0x10, 0x09, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, + 0x53, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x0a, 0x12, 0x23, 0x0a, 0x1f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x42, 0x45, 0x43, 0x41, + 0x55, 0x53, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x45, 0x4e, 0x44, 0x45, 0x4e, 0x43, 0x59, 0x5f, 0x50, + 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x0b, 0x12, 0x1d, 0x0a, 0x19, 0x52, 0x45, 0x41, 0x44, + 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x5f, 0x4e, + 0x45, 0x53, 0x54, 0x45, 0x44, 0x10, 0x0d, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x4c, 0x45, 0x54, + 0x45, 0x5f, 0x42, 0x45, 0x43, 0x41, 0x55, 0x53, 0x45, 0x5f, 0x4e, 0x4f, 0x5f, 0x4d, 0x4f, 0x56, + 0x45, 0x5f, 0x54, 0x41, 0x52, 0x47, 0x45, 0x54, 0x10, 0x0c, 0x2a, 0x9b, 0x01, 0x0a, 0x0e, 0x44, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, + 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x49, 0x4e, + 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, + 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, + 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x03, + 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x42, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, 0x45, 0x52, 0x45, + 0x51, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x45, 0x46, 0x45, 0x52, 0x52, 0x45, 0x44, 0x5f, + 0x50, 0x52, 0x45, 0x52, 0x45, 0x51, 0x10, 0x05, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x73, 0x2f, 0x70, 0x6c, 0x61, 0x6e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_planfile_proto_rawDescOnce sync.Once + file_planfile_proto_rawDescData []byte +) + +func file_planfile_proto_rawDescGZIP() []byte { + file_planfile_proto_rawDescOnce.Do(func() { + file_planfile_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_planfile_proto_rawDesc), len(file_planfile_proto_rawDesc))) + }) + return file_planfile_proto_rawDescData +} + +var file_planfile_proto_enumTypes = make([]protoimpl.EnumInfo, 6) +var file_planfile_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_planfile_proto_goTypes = []any{ + (Mode)(0), // 0: tfplan.Mode + (Action)(0), // 1: tfplan.Action + (ResourceInstanceActionReason)(0), // 2: tfplan.ResourceInstanceActionReason + (DeferredReason)(0), // 3: tfplan.DeferredReason + (CheckResults_Status)(0), // 4: tfplan.CheckResults.Status + (CheckResults_ObjectKind)(0), // 5: tfplan.CheckResults.ObjectKind + (*Plan)(nil), // 6: tfplan.Plan + (*Backend)(nil), // 7: tfplan.Backend + (*Change)(nil), // 8: tfplan.Change + (*ResourceInstanceChange)(nil), // 9: tfplan.ResourceInstanceChange + (*DeferredResourceInstanceChange)(nil), // 10: tfplan.DeferredResourceInstanceChange + (*OutputChange)(nil), // 11: tfplan.OutputChange + (*CheckResults)(nil), // 12: tfplan.CheckResults + (*FunctionCallHash)(nil), // 13: tfplan.FunctionCallHash + (*DynamicValue)(nil), // 14: tfplan.DynamicValue + (*Path)(nil), // 15: tfplan.Path + (*Importing)(nil), // 16: tfplan.Importing + (*Deferred)(nil), // 17: tfplan.Deferred + nil, // 18: tfplan.Plan.VariablesEntry + (*PlanResourceAttr)(nil), // 19: tfplan.Plan.resource_attr + (*CheckResults_ObjectResult)(nil), // 20: tfplan.CheckResults.ObjectResult + (*Path_Step)(nil), // 21: tfplan.Path.Step +} +var file_planfile_proto_depIdxs = []int32{ + 0, // 0: tfplan.Plan.ui_mode:type_name -> tfplan.Mode + 18, // 1: tfplan.Plan.variables:type_name -> tfplan.Plan.VariablesEntry + 9, // 2: tfplan.Plan.resource_changes:type_name -> tfplan.ResourceInstanceChange + 9, // 3: tfplan.Plan.resource_drift:type_name -> tfplan.ResourceInstanceChange + 10, // 4: tfplan.Plan.deferred_changes:type_name -> tfplan.DeferredResourceInstanceChange + 11, // 5: tfplan.Plan.output_changes:type_name -> tfplan.OutputChange + 12, // 6: tfplan.Plan.check_results:type_name -> tfplan.CheckResults + 7, // 7: tfplan.Plan.backend:type_name -> tfplan.Backend + 19, // 8: tfplan.Plan.relevant_attributes:type_name -> tfplan.Plan.resource_attr + 13, // 9: tfplan.Plan.function_results:type_name -> tfplan.FunctionCallHash + 14, // 10: tfplan.Backend.config:type_name -> tfplan.DynamicValue + 1, // 11: tfplan.Change.action:type_name -> tfplan.Action + 14, // 12: tfplan.Change.values:type_name -> tfplan.DynamicValue + 15, // 13: tfplan.Change.before_sensitive_paths:type_name -> tfplan.Path + 15, // 14: tfplan.Change.after_sensitive_paths:type_name -> tfplan.Path + 16, // 15: tfplan.Change.importing:type_name -> tfplan.Importing + 14, // 16: tfplan.Change.before_identity:type_name -> tfplan.DynamicValue + 14, // 17: tfplan.Change.after_identity:type_name -> tfplan.DynamicValue + 8, // 18: tfplan.ResourceInstanceChange.change:type_name -> tfplan.Change + 15, // 19: tfplan.ResourceInstanceChange.required_replace:type_name -> tfplan.Path + 2, // 20: tfplan.ResourceInstanceChange.action_reason:type_name -> tfplan.ResourceInstanceActionReason + 17, // 21: tfplan.DeferredResourceInstanceChange.deferred:type_name -> tfplan.Deferred + 9, // 22: tfplan.DeferredResourceInstanceChange.change:type_name -> tfplan.ResourceInstanceChange + 8, // 23: tfplan.OutputChange.change:type_name -> tfplan.Change + 5, // 24: tfplan.CheckResults.kind:type_name -> tfplan.CheckResults.ObjectKind + 4, // 25: tfplan.CheckResults.status:type_name -> tfplan.CheckResults.Status + 20, // 26: tfplan.CheckResults.objects:type_name -> tfplan.CheckResults.ObjectResult + 21, // 27: tfplan.Path.steps:type_name -> tfplan.Path.Step + 14, // 28: tfplan.Importing.identity:type_name -> tfplan.DynamicValue + 3, // 29: tfplan.Deferred.reason:type_name -> tfplan.DeferredReason + 14, // 30: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue + 15, // 31: tfplan.Plan.resource_attr.attr:type_name -> tfplan.Path + 4, // 32: tfplan.CheckResults.ObjectResult.status:type_name -> tfplan.CheckResults.Status + 14, // 33: tfplan.Path.Step.element_key:type_name -> tfplan.DynamicValue + 34, // [34:34] is the sub-list for method output_type + 34, // [34:34] is the sub-list for method input_type + 34, // [34:34] is the sub-list for extension type_name + 34, // [34:34] is the sub-list for extension extendee + 0, // [0:34] is the sub-list for field type_name +} + +func init() { file_planfile_proto_init() } +func file_planfile_proto_init() { + if File_planfile_proto != nil { + return + } + file_planfile_proto_msgTypes[15].OneofWrappers = []any{ + (*Path_Step_AttributeName)(nil), + (*Path_Step_ElementKey)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_planfile_proto_rawDesc), len(file_planfile_proto_rawDesc)), + NumEnums: 6, + NumMessages: 16, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_planfile_proto_goTypes, + DependencyIndexes: file_planfile_proto_depIdxs, + EnumInfos: file_planfile_proto_enumTypes, + MessageInfos: file_planfile_proto_msgTypes, + }.Build() + File_planfile_proto = out.File + file_planfile_proto_goTypes = nil + file_planfile_proto_depIdxs = nil +} diff --git a/internal/plans/internal/planproto/planfile.proto b/internal/plans/planproto/planfile.proto similarity index 72% rename from internal/plans/internal/planproto/planfile.proto rename to internal/plans/planproto/planfile.proto index 2941b1dc34..fc2c216e39 100644 --- a/internal/plans/internal/planproto/planfile.proto +++ b/internal/plans/planproto/planfile.proto @@ -1,9 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + syntax = "proto3"; package tfplan; // For Terraform's own parsing, the proto stub types go into an internal Go // package. The public API is in github.com/hashicorp/terraform/plans/planfile . -option go_package = "github.com/hashicorp/terraform/internal/plans/internal/planproto"; +option go_package = "github.com/hashicorp/terraform/internal/plans/planproto"; // Plan is the root message type for the tfplan file message Plan { @@ -24,10 +27,43 @@ message Plan { // make decisions in Terraform Core during the applying of a plan. Mode ui_mode = 17; + // Applyable is true for any plan where it makes sense to ask an operator + // to approve it and then ask Terraform to apply it. + // + // The other fields provide more context about why a non-applyable plan + // is not applyable, but this field is here so that if new situations + // arise in future then old callers can at least still make the right + // decision about whether to show the approval prompt, even if they don't + // know yet why a particular plan isn't applyable and have to just use + // a generic error message. + bool applyable = 25; + + // Complete is true for a plan that includes a planned action for every + // resource input object that was present across the desired state and + // prior state, even if the planned action is "no-op". + // + // Conversely, if this field is false then the plan deals with only a + // subset of those objects. The reason for that should always be + // determinable based on other fields in this message, but this flag + // is here to ensure that if new situations arise in future then old + // callers can at least still show a generic message about the plan + // being incomplete, even if they don't know yet how to explain the + // reason to the operator. + bool complete = 26; + + // Errored is true for any plan whose creation was interrupted by an + // error. A plan with this flag set cannot be applied + // (i.e. applyable = false), and the changes it proposes are likely to be + // incomplete. + bool errored = 20; + // The variables that were set when creating the plan. Each value is // a msgpack serialization of an HCL value. map variables = 2; + // Variables whose values must be provided during the apply phase. + repeated string apply_time_variables = 28; + // An unordered set of proposed changes to resources throughout the // configuration, including any nested modules. Use the address of // each resource to determine which module it belongs to. @@ -38,6 +74,12 @@ message Plan { // after refresh. repeated ResourceInstanceChange resource_drift = 18; + // An unordered set of deferred changes. These are changes that will be + // applied in a subsequent plan, but were deferred in this plan for some + // reason. Generally, if complete is set to false there should be entries + // in this list. + repeated DeferredResourceInstanceChange deferred_changes = 27; + // An unordered set of proposed changes to outputs in the root module // of the configuration. This set also includes "no action" changes for // outputs that are not changing, as context for detecting inconsistencies @@ -77,6 +119,11 @@ message Plan { // RelevantAttributes lists individual resource attributes from // ResourceDrift which may have contributed to the plan changes. repeated resource_attr relevant_attributes = 15; + + // timestamp is the record of truth for when the plan happened. + string timestamp = 21; + + repeated FunctionCallHash function_results = 22; } // Mode describes the planning mode that created the plan. @@ -103,6 +150,8 @@ enum Action { DELETE = 5; DELETE_THEN_CREATE = 6; CREATE_THEN_DELETE = 7; + FORGET = 8; + CREATE_THEN_FORGET = 9; } // Change represents a change made to some object, transforming it from an old @@ -132,6 +181,20 @@ message Change { // sensitive. Values at these paths should be obscured in human-readable // output. This set is always empty for delete. repeated Path after_sensitive_paths = 4; + + // Importing, if true, specifies that the resource is being imported as part + // of the change. + Importing importing = 5; + + // GeneratedConfig contains any configuration that was generated as part of + // the change, as an HCL string. + string generated_config = 6; + + // The resource identity before the plan operation + DynamicValue before_identity = 7; + + // The resource identity after the plan operation + DynamicValue after_identity = 8; } // ResourceInstanceActionReason sometimes provides some additional user-facing @@ -151,6 +214,8 @@ enum ResourceInstanceActionReason { REPLACE_BY_TRIGGERS = 9; READ_BECAUSE_CONFIG_UNKNOWN = 10; READ_BECAUSE_DEPENDENCY_PENDING = 11; + READ_BECAUSE_CHECK_NESTED = 13; + DELETE_BECAUSE_NO_MOVE_TARGET = 12; } message ResourceInstanceChange { @@ -203,6 +268,19 @@ message ResourceInstanceChange { ResourceInstanceActionReason action_reason = 12; } +// DeferredResourceInstanceChange represents a resource instance change that +// was deferred for some reason. +// +// It contains the original change that was deferred, along with the reason +// why it was deferred. +message DeferredResourceInstanceChange { + // The reason why the change was deferred. + Deferred deferred = 1; + + // The original change that was deferred. + ResourceInstanceChange change = 2; +} + message OutputChange { // Name of the output as defined in the root module. string name = 1; @@ -231,6 +309,8 @@ message CheckResults { UNSPECIFIED = 0; RESOURCE = 1; OUTPUT_VALUE = 2; + CHECK = 3; + INPUT_VARIABLE = 4; } message ObjectResult { @@ -253,6 +333,14 @@ message CheckResults { repeated ObjectResult objects = 4; } +// FunctionCallHash stores a record of a hashed function call and +// result. This is used internally to ensure that providers return consistent +// values between plan and apply given the same arguments. +message FunctionCallHash { + bytes key = 1; + bytes result = 2; +} + // DynamicValue represents a value whose type is not decided until runtime, // often based on schema information obtained from a plugin. // @@ -286,3 +374,33 @@ message Path { } repeated Step steps = 1; } + +// Importing contains the embedded metadata about the import operation if this +// change describes it. +message Importing { + // The original ID of the resource. + string id = 1; + + // unknown is true if the original ID of the resource is unknown. + bool unknown = 2; + + // Identity can be used to import instead of id + DynamicValue identity = 3; +} + +// DeferredReason describes the reason why a resource instance change was +// deferred. +enum DeferredReason { + INVALID = 0; + INSTANCE_COUNT_UNKNOWN = 1; + RESOURCE_CONFIG_UNKNOWN = 2; + PROVIDER_CONFIG_UNKNOWN = 3; + ABSENT_PREREQ = 4; + DEFERRED_PREREQ = 5; +} + +// Deferred contains all the metadata about a the deferral of a resource +// instance change. +message Deferred { + DeferredReason reason = 1; +} diff --git a/internal/plans/quality.go b/internal/plans/quality.go new file mode 100644 index 0000000000..d21122c093 --- /dev/null +++ b/internal/plans/quality.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package plans + +// Quality represents facts about the nature of a plan that might be relevant +// when rendering it, like whether it errored or contains no changes. A plan can +// have multiple qualities. +type Quality int + +//go:generate go tool golang.org/x/tools/cmd/stringer -type Quality + +const ( + // Errored plans did not successfully complete, and cannot be applied. + Errored Quality = iota + // NoChanges plans won't result in any actions on infrastructure, or any + // semantically meaningful updates to state. They can sometimes still affect + // the format of state if applied. + NoChanges +) diff --git a/internal/plans/quality_string.go b/internal/plans/quality_string.go new file mode 100644 index 0000000000..61a399a1e8 --- /dev/null +++ b/internal/plans/quality_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type Quality"; DO NOT EDIT. + +package plans + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Errored-0] + _ = x[NoChanges-1] +} + +const _Quality_name = "ErroredNoChanges" + +var _Quality_index = [...]uint8{0, 7, 16} + +func (i Quality) String() string { + if i < 0 || i >= Quality(len(_Quality_index)-1) { + return "Quality(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _Quality_name[_Quality_index[i]:_Quality_index[i+1]] +} diff --git a/internal/plans/resourceinstancechangeactionreason_string.go b/internal/plans/resourceinstancechangeactionreason_string.go index fbc2abe0c9..9915ec85bc 100644 --- a/internal/plans/resourceinstancechangeactionreason_string.go +++ b/internal/plans/resourceinstancechangeactionreason_string.go @@ -18,24 +18,28 @@ func _() { _ = x[ResourceInstanceDeleteBecauseCountIndex-67] _ = x[ResourceInstanceDeleteBecauseEachKey-69] _ = x[ResourceInstanceDeleteBecauseNoModule-77] + _ = x[ResourceInstanceDeleteBecauseNoMoveTarget-65] _ = x[ResourceInstanceReadBecauseConfigUnknown-63] _ = x[ResourceInstanceReadBecauseDependencyPending-33] + _ = x[ResourceInstanceReadBecauseCheckNested-35] } const ( _ResourceInstanceChangeActionReason_name_0 = "ResourceInstanceChangeNoReason" _ResourceInstanceChangeActionReason_name_1 = "ResourceInstanceReadBecauseDependencyPending" - _ResourceInstanceChangeActionReason_name_2 = "ResourceInstanceReadBecauseConfigUnknown" - _ResourceInstanceChangeActionReason_name_3 = "ResourceInstanceDeleteBecauseCountIndexResourceInstanceReplaceByTriggersResourceInstanceDeleteBecauseEachKeyResourceInstanceReplaceBecauseCannotUpdate" - _ResourceInstanceChangeActionReason_name_4 = "ResourceInstanceDeleteBecauseNoModuleResourceInstanceDeleteBecauseNoResourceConfig" - _ResourceInstanceChangeActionReason_name_5 = "ResourceInstanceReplaceByRequest" - _ResourceInstanceChangeActionReason_name_6 = "ResourceInstanceReplaceBecauseTainted" - _ResourceInstanceChangeActionReason_name_7 = "ResourceInstanceDeleteBecauseWrongRepetition" + _ResourceInstanceChangeActionReason_name_2 = "ResourceInstanceReadBecauseCheckNested" + _ResourceInstanceChangeActionReason_name_3 = "ResourceInstanceReadBecauseConfigUnknown" + _ResourceInstanceChangeActionReason_name_4 = "ResourceInstanceDeleteBecauseNoMoveTarget" + _ResourceInstanceChangeActionReason_name_5 = "ResourceInstanceDeleteBecauseCountIndexResourceInstanceReplaceByTriggersResourceInstanceDeleteBecauseEachKeyResourceInstanceReplaceBecauseCannotUpdate" + _ResourceInstanceChangeActionReason_name_6 = "ResourceInstanceDeleteBecauseNoModuleResourceInstanceDeleteBecauseNoResourceConfig" + _ResourceInstanceChangeActionReason_name_7 = "ResourceInstanceReplaceByRequest" + _ResourceInstanceChangeActionReason_name_8 = "ResourceInstanceReplaceBecauseTainted" + _ResourceInstanceChangeActionReason_name_9 = "ResourceInstanceDeleteBecauseWrongRepetition" ) var ( - _ResourceInstanceChangeActionReason_index_3 = [...]uint8{0, 39, 72, 108, 150} - _ResourceInstanceChangeActionReason_index_4 = [...]uint8{0, 37, 82} + _ResourceInstanceChangeActionReason_index_5 = [...]uint8{0, 39, 72, 108, 150} + _ResourceInstanceChangeActionReason_index_6 = [...]uint8{0, 37, 82} ) func (i ResourceInstanceChangeActionReason) String() string { @@ -44,20 +48,24 @@ func (i ResourceInstanceChangeActionReason) String() string { return _ResourceInstanceChangeActionReason_name_0 case i == 33: return _ResourceInstanceChangeActionReason_name_1 - case i == 63: + case i == 35: return _ResourceInstanceChangeActionReason_name_2 + case i == 63: + return _ResourceInstanceChangeActionReason_name_3 + case i == 65: + return _ResourceInstanceChangeActionReason_name_4 case 67 <= i && i <= 70: i -= 67 - return _ResourceInstanceChangeActionReason_name_3[_ResourceInstanceChangeActionReason_index_3[i]:_ResourceInstanceChangeActionReason_index_3[i+1]] + return _ResourceInstanceChangeActionReason_name_5[_ResourceInstanceChangeActionReason_index_5[i]:_ResourceInstanceChangeActionReason_index_5[i+1]] case 77 <= i && i <= 78: i -= 77 - return _ResourceInstanceChangeActionReason_name_4[_ResourceInstanceChangeActionReason_index_4[i]:_ResourceInstanceChangeActionReason_index_4[i+1]] + return _ResourceInstanceChangeActionReason_name_6[_ResourceInstanceChangeActionReason_index_6[i]:_ResourceInstanceChangeActionReason_index_6[i+1]] case i == 82: - return _ResourceInstanceChangeActionReason_name_5 - case i == 84: - return _ResourceInstanceChangeActionReason_name_6 - case i == 87: return _ResourceInstanceChangeActionReason_name_7 + case i == 84: + return _ResourceInstanceChangeActionReason_name_8 + case i == 87: + return _ResourceInstanceChangeActionReason_name_9 default: return "ResourceInstanceChangeActionReason(" + strconv.FormatInt(int64(i), 10) + ")" } diff --git a/internal/plugin/convert/deferred.go b/internal/plugin/convert/deferred.go new file mode 100644 index 0000000000..a1e45820a4 --- /dev/null +++ b/internal/plugin/convert/deferred.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "github.com/hashicorp/terraform/internal/providers" + proto "github.com/hashicorp/terraform/internal/tfplugin5" +) + +// ProtoToDeferred translates a proto.Deferred to a providers.Deferred. +func ProtoToDeferred(d *proto.Deferred) *providers.Deferred { + if d == nil { + return nil + } + + var reason providers.DeferredReason + switch d.Reason { + case proto.Deferred_UNKNOWN: + reason = providers.DeferredReasonInvalid + case proto.Deferred_RESOURCE_CONFIG_UNKNOWN: + reason = providers.DeferredReasonResourceConfigUnknown + case proto.Deferred_PROVIDER_CONFIG_UNKNOWN: + reason = providers.DeferredReasonProviderConfigUnknown + case proto.Deferred_ABSENT_PREREQ: + reason = providers.DeferredReasonAbsentPrereq + default: + reason = providers.DeferredReasonInvalid + } + + return &providers.Deferred{ + Reason: reason, + } +} diff --git a/internal/plugin/convert/deferred_test.go b/internal/plugin/convert/deferred_test.go new file mode 100644 index 0000000000..11aace7afe --- /dev/null +++ b/internal/plugin/convert/deferred_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/internal/providers" + proto "github.com/hashicorp/terraform/internal/tfplugin5" +) + +func TestProtoDeferred(t *testing.T) { + testCases := []struct { + reason proto.Deferred_Reason + expected providers.DeferredReason + }{ + { + reason: proto.Deferred_UNKNOWN, + expected: providers.DeferredReasonInvalid, + }, + { + reason: proto.Deferred_RESOURCE_CONFIG_UNKNOWN, + expected: providers.DeferredReasonResourceConfigUnknown, + }, + { + reason: proto.Deferred_PROVIDER_CONFIG_UNKNOWN, + expected: providers.DeferredReasonProviderConfigUnknown, + }, + { + reason: proto.Deferred_ABSENT_PREREQ, + expected: providers.DeferredReasonAbsentPrereq, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("deferred reason %q", tc.reason.String()), func(t *testing.T) { + d := &proto.Deferred{ + Reason: tc.reason, + } + + deferred := ProtoToDeferred(d) + if deferred.Reason != tc.expected { + t.Fatalf("expected %q, got %q", tc.expected, deferred.Reason) + } + }) + } +} + +func TestProtoDeferred_Nil(t *testing.T) { + deferred := ProtoToDeferred(nil) + if deferred != nil { + t.Fatalf("expected nil, got %v", deferred) + } +} diff --git a/internal/plugin/convert/diagnostics.go b/internal/plugin/convert/diagnostics.go index 43824e1b79..7002addd8f 100644 --- a/internal/plugin/convert/diagnostics.go +++ b/internal/plugin/convert/diagnostics.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( diff --git a/internal/plugin/convert/diagnostics_test.go b/internal/plugin/convert/diagnostics_test.go index 3f54985dd5..2c4701c587 100644 --- a/internal/plugin/convert/diagnostics_test.go +++ b/internal/plugin/convert/diagnostics_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -18,6 +21,8 @@ var ignoreUnexported = cmpopts.IgnoreUnexported( proto.Schema_Block{}, proto.Schema_NestedBlock{}, proto.Schema_Attribute{}, + proto.ResourceIdentitySchema{}, + proto.ResourceIdentitySchema_IdentityAttribute{}, ) func TestProtoDiagnostics(t *testing.T) { diff --git a/internal/plugin/convert/functions.go b/internal/plugin/convert/functions.go new file mode 100644 index 0000000000..439068696c --- /dev/null +++ b/internal/plugin/convert/functions.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfplugin5" +) + +func FunctionDeclsFromProto(protoFuncs map[string]*tfplugin5.Function) (map[string]providers.FunctionDecl, error) { + if len(protoFuncs) == 0 { + return nil, nil + } + + ret := make(map[string]providers.FunctionDecl, len(protoFuncs)) + for name, protoFunc := range protoFuncs { + decl, err := FunctionDeclFromProto(protoFunc) + if err != nil { + return nil, fmt.Errorf("invalid declaration for function %q: %s", name, err) + } + ret[name] = decl + } + return ret, nil +} + +func FunctionDeclFromProto(protoFunc *tfplugin5.Function) (providers.FunctionDecl, error) { + var ret providers.FunctionDecl + + ret.Description = protoFunc.Description + ret.DescriptionKind = schemaStringKind(protoFunc.DescriptionKind) + ret.Summary = protoFunc.Summary + ret.DeprecationMessage = protoFunc.DeprecationMessage + + if err := json.Unmarshal(protoFunc.Return.Type, &ret.ReturnType); err != nil { + return ret, fmt.Errorf("invalid return type constraint: %s", err) + } + + if len(protoFunc.Parameters) != 0 { + ret.Parameters = make([]providers.FunctionParam, len(protoFunc.Parameters)) + for i, protoParam := range protoFunc.Parameters { + param, err := functionParamFromProto(protoParam) + if err != nil { + return ret, fmt.Errorf("invalid parameter %d (%q): %s", i, protoParam.Name, err) + } + ret.Parameters[i] = param + } + } + if protoFunc.VariadicParameter != nil { + param, err := functionParamFromProto(protoFunc.VariadicParameter) + if err != nil { + return ret, fmt.Errorf("invalid variadic parameter (%q): %s", protoFunc.VariadicParameter.Name, err) + } + ret.VariadicParameter = ¶m + } + + return ret, nil +} + +func functionParamFromProto(protoParam *tfplugin5.Function_Parameter) (providers.FunctionParam, error) { + var ret providers.FunctionParam + ret.Name = protoParam.Name + ret.Description = protoParam.Description + ret.DescriptionKind = schemaStringKind(protoParam.DescriptionKind) + ret.AllowNullValue = protoParam.AllowNullValue + ret.AllowUnknownValues = protoParam.AllowUnknownValues + if err := json.Unmarshal(protoParam.Type, &ret.Type); err != nil { + return ret, fmt.Errorf("invalid type constraint: %s", err) + } + return ret, nil +} + +func FunctionDeclsToProto(fns map[string]providers.FunctionDecl) (map[string]*tfplugin5.Function, error) { + if len(fns) == 0 { + return nil, nil + } + + ret := make(map[string]*tfplugin5.Function, len(fns)) + for name, fn := range fns { + decl, err := FunctionDeclToProto(fn) + if err != nil { + return nil, fmt.Errorf("invalid declaration for function %q: %s", name, err) + } + ret[name] = decl + } + return ret, nil +} + +func FunctionDeclToProto(fn providers.FunctionDecl) (*tfplugin5.Function, error) { + ret := &tfplugin5.Function{ + Return: &tfplugin5.Function_Return{}, + } + + ret.Description = fn.Description + ret.DescriptionKind = protoStringKind(fn.DescriptionKind) + + retTy, err := json.Marshal(fn.ReturnType) + if err != nil { + return ret, fmt.Errorf("invalid return type constraint: %s", err) + } + ret.Return.Type = retTy + + if len(fn.Parameters) != 0 { + ret.Parameters = make([]*tfplugin5.Function_Parameter, len(fn.Parameters)) + for i, fnParam := range fn.Parameters { + protoParam, err := functionParamToProto(fnParam) + if err != nil { + return ret, fmt.Errorf("invalid parameter %d (%q): %s", i, fnParam.Name, err) + } + ret.Parameters[i] = protoParam + } + } + if fn.VariadicParameter != nil { + param, err := functionParamToProto(*fn.VariadicParameter) + if err != nil { + return ret, fmt.Errorf("invalid variadic parameter (%q): %s", fn.VariadicParameter.Name, err) + } + ret.VariadicParameter = param + } + + return ret, nil +} + +func functionParamToProto(param providers.FunctionParam) (*tfplugin5.Function_Parameter, error) { + ret := &tfplugin5.Function_Parameter{} + ret.Name = param.Name + ret.Description = param.Description + ret.DescriptionKind = protoStringKind(param.DescriptionKind) + ret.AllowNullValue = param.AllowNullValue + ret.AllowUnknownValues = param.AllowUnknownValues + ty, err := json.Marshal(param.Type) + if err != nil { + return ret, fmt.Errorf("invalid type constraint: %s", err) + } + ret.Type = ty + return ret, nil +} diff --git a/internal/plugin/convert/functions_test.go b/internal/plugin/convert/functions_test.go new file mode 100644 index 0000000000..0a9a9547ca --- /dev/null +++ b/internal/plugin/convert/functions_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/zclconf/go-cty/cty" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" +) + +func TestFunctionDeclsToFromProto(t *testing.T) { + fns := map[string]providers.FunctionDecl{ + "basic": providers.FunctionDecl{ + Parameters: []providers.FunctionParam{ + providers.FunctionParam{ + Name: "string", + Type: cty.String, + AllowNullValue: true, + AllowUnknownValues: true, + Description: "must be a string", + DescriptionKind: configschema.StringPlain, + }, + }, + ReturnType: cty.String, + Description: "returns a string", + DescriptionKind: configschema.StringPlain, + }, + "variadic": providers.FunctionDecl{ + VariadicParameter: &providers.FunctionParam{ + Name: "string", + Type: cty.String, + Description: "must be a string", + DescriptionKind: configschema.StringMarkdown, + }, + ReturnType: cty.String, + Description: "returns a string", + DescriptionKind: configschema.StringMarkdown, + }, + } + + protoFns, err := FunctionDeclsToProto(fns) + if err != nil { + t.Fatal(err) + } + + gotFns, err := FunctionDeclsFromProto(protoFns) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(fns, gotFns, ctydebug.CmpOptions); diff != "" { + t.Fatal(diff) + } +} diff --git a/internal/plugin/convert/schema.go b/internal/plugin/convert/schema.go index 4a3f909935..ddd4d69e2c 100644 --- a/internal/plugin/convert/schema.go +++ b/internal/plugin/convert/schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -31,6 +34,7 @@ func ConfigSchemaToProto(b *configschema.Block) *proto.Schema_Block { Required: a.Required, Sensitive: a.Sensitive, Deprecated: a.Deprecated, + WriteOnly: a.WriteOnly, } ty, err := json.Marshal(a.Type) @@ -86,11 +90,19 @@ func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Sch } // ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema. -func ProtoToProviderSchema(s *proto.Schema) providers.Schema { - return providers.Schema{ +// It takes an optional resource identity schema for resources that support identity. +func ProtoToProviderSchema(s *proto.Schema, id *proto.ResourceIdentitySchema) providers.Schema { + schema := providers.Schema{ Version: s.Version, - Block: ProtoToConfigSchema(s.Block), + Body: ProtoToConfigSchema(s.Block), } + + if id != nil { + schema.IdentityVersion = id.Version + schema.Identity = ProtoToIdentitySchema(id.IdentityAttributes) + } + + return schema } // ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it @@ -114,6 +126,7 @@ func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block { Computed: a.Computed, Sensitive: a.Sensitive, Deprecated: a.Deprecated, + WriteOnly: a.WriteOnly, } if err := json.Unmarshal(a.Type, &attr.Type); err != nil { @@ -183,3 +196,54 @@ func sortedKeys(m interface{}) []string { sort.Strings(keys) return keys } + +func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object { + obj := &configschema.Object{ + Attributes: make(map[string]*configschema.Attribute), + Nesting: configschema.NestingSingle, + } + + for _, a := range attributes { + attr := &configschema.Attribute{ + Description: a.Description, + Required: a.RequiredForImport, + Optional: a.OptionalForImport, + } + + if err := json.Unmarshal(a.Type, &attr.Type); err != nil { + panic(err) + } + + obj.Attributes[a.Name] = attr + } + + return obj +} + +func ResourceIdentitySchemaToProto(b providers.IdentitySchema) *proto.ResourceIdentitySchema { + attrs := []*proto.ResourceIdentitySchema_IdentityAttribute{} + for _, name := range sortedKeys(b.Body.Attributes) { + a := b.Body.Attributes[name] + + attr := &proto.ResourceIdentitySchema_IdentityAttribute{ + Name: name, + Description: a.Description, + RequiredForImport: a.Required, + OptionalForImport: a.Optional, + } + + ty, err := json.Marshal(a.Type) + if err != nil { + panic(err) + } + + attr.Type = ty + + attrs = append(attrs, attr) + } + + return &proto.ResourceIdentitySchema{ + Version: b.Version, + IdentityAttributes: attrs, + } +} diff --git a/internal/plugin/convert/schema_test.go b/internal/plugin/convert/schema_test.go index 4df254fb76..5c66e077e1 100644 --- a/internal/plugin/convert/schema_test.go +++ b/internal/plugin/convert/schema_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -6,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" proto "github.com/hashicorp/terraform/internal/tfplugin5" "github.com/zclconf/go-cty/cty" ) @@ -359,3 +363,90 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { }) } } + +func TestProtoToResourceIdentitySchema(t *testing.T) { + tests := map[string]struct { + Attributes []*proto.ResourceIdentitySchema_IdentityAttribute + Want *configschema.Object + }{ + "simple": { + []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "id", + Type: []byte(`"string"`), + RequiredForImport: true, + OptionalForImport: false, + Description: "Something", + }, + }, + &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "Something", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ProtoToIdentitySchema(tc.Attributes) + if !cmp.Equal(converted, tc.Want, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, valueComparer, equateEmpty)) + } + }) + } +} + +func TestResourceIdentitySchemaToProto(t *testing.T) { + tests := map[string]struct { + Want *proto.ResourceIdentitySchema + Schema providers.IdentitySchema + }{ + "attributes": { + &proto.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "optional", + Type: []byte(`"string"`), + OptionalForImport: true, + }, + { + Name: "required", + Type: []byte(`"number"`), + RequiredForImport: true, + }, + }, + }, + providers.IdentitySchema{ + Version: 1, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "optional": { + Type: cty.String, + Optional: true, + }, + "required": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ResourceIdentitySchemaToProto(tc.Schema) + if !cmp.Equal(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported)) + } + }) + } +} diff --git a/internal/plugin/discovery/find.go b/internal/plugin/discovery/find.go index 027a887ebf..cee17c6b65 100644 --- a/internal/plugin/discovery/find.go +++ b/internal/plugin/discovery/find.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/find_test.go b/internal/plugin/discovery/find_test.go index 8bad7061be..d15ab13f38 100644 --- a/internal/plugin/discovery/find_test.go +++ b/internal/plugin/discovery/find_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/get_cache.go b/internal/plugin/discovery/get_cache.go index 1a10042648..97b2b6edbb 100644 --- a/internal/plugin/discovery/get_cache.go +++ b/internal/plugin/discovery/get_cache.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery // PluginCache is an interface implemented by objects that are able to maintain diff --git a/internal/plugin/discovery/get_cache_test.go b/internal/plugin/discovery/get_cache_test.go index a7e3eeab46..f5a7b7a6d9 100644 --- a/internal/plugin/discovery/get_cache_test.go +++ b/internal/plugin/discovery/get_cache_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/meta.go b/internal/plugin/discovery/meta.go index bdcebcb9dc..5ac966fae2 100644 --- a/internal/plugin/discovery/meta.go +++ b/internal/plugin/discovery/meta.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/meta_set.go b/internal/plugin/discovery/meta_set.go index 72e4ce20e7..a08489d108 100644 --- a/internal/plugin/discovery/meta_set.go +++ b/internal/plugin/discovery/meta_set.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery // A PluginMetaSet is a set of PluginMeta objects meeting a certain criteria. diff --git a/internal/plugin/discovery/meta_set_test.go b/internal/plugin/discovery/meta_set_test.go index f2d8ec5cec..3193385819 100644 --- a/internal/plugin/discovery/meta_set_test.go +++ b/internal/plugin/discovery/meta_set_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/meta_test.go b/internal/plugin/discovery/meta_test.go index 5cbd6721ab..8cfc7a0564 100644 --- a/internal/plugin/discovery/meta_test.go +++ b/internal/plugin/discovery/meta_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/requirements.go b/internal/plugin/discovery/requirements.go index 0466ab25ae..88537f0b34 100644 --- a/internal/plugin/discovery/requirements.go +++ b/internal/plugin/discovery/requirements.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/requirements_test.go b/internal/plugin/discovery/requirements_test.go index b3300fab4b..e83d5e1941 100644 --- a/internal/plugin/discovery/requirements_test.go +++ b/internal/plugin/discovery/requirements_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/version.go b/internal/plugin/discovery/version.go index 4311d51076..8f586919fa 100644 --- a/internal/plugin/discovery/version.go +++ b/internal/plugin/discovery/version.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/version_set.go b/internal/plugin/discovery/version_set.go index de02f5ec5b..6cd4d1162b 100644 --- a/internal/plugin/discovery/version_set.go +++ b/internal/plugin/discovery/version_set.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/version_set_test.go b/internal/plugin/discovery/version_set_test.go index 1ad04fd0bd..30beadb972 100644 --- a/internal/plugin/discovery/version_set_test.go +++ b/internal/plugin/discovery/version_set_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/discovery/version_test.go b/internal/plugin/discovery/version_test.go index e34647b803..82e467a5b9 100644 --- a/internal/plugin/discovery/version_test.go +++ b/internal/plugin/discovery/version_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package discovery import ( diff --git a/internal/plugin/grpc_error.go b/internal/plugin/grpc_error.go index 0f638b7fa4..146a8f40e4 100644 --- a/internal/plugin/grpc_error.go +++ b/internal/plugin/grpc_error.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/grpc_provider.go b/internal/plugin/grpc_provider.go index 2c4f2c0360..950928acd6 100644 --- a/internal/plugin/grpc_provider.go +++ b/internal/plugin/grpc_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( @@ -9,13 +12,18 @@ import ( "github.com/zclconf/go-cty/cty" plugin "github.com/hashicorp/go-plugin" + "github.com/zclconf/go-cty/cty/function" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plugin/convert" "github.com/hashicorp/terraform/internal/providers" proto "github.com/hashicorp/terraform/internal/tfplugin5" - ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/zclconf/go-cty/cty/msgpack" - "google.golang.org/grpc" ) var logger = logging.HCLogger() @@ -51,6 +59,11 @@ type GRPCProvider struct { // used in an end to end test of a provider. TestServer *grpc.Server + // Addr uniquely identifies the type of provider. + // Normally executed providers will have this set during initialization, + // but it may not always be available for alternative execute modes. + Addr addrs.Provider + // Proto client use to make the grpc service calls. client proto.ProviderClient @@ -59,35 +72,35 @@ type GRPCProvider struct { ctx context.Context // schema stores the schema for this provider. This is used to properly - // serialize the state for requests. - mu sync.Mutex - schemas providers.GetProviderSchemaResponse + // serialize the requests for schemas. + mu sync.Mutex + schema providers.GetProviderSchemaResponse } -// getSchema is used internally to get the cached provider schema -func (p *GRPCProvider) getSchema() providers.GetProviderSchemaResponse { - p.mu.Lock() - // unlock inline in case GetSchema needs to be called - if p.schemas.Provider.Block != nil { - p.mu.Unlock() - return p.schemas - } - p.mu.Unlock() - - return p.GetProviderSchema() -} - -func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResponse) { +func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { logger.Trace("GRPCProvider: GetProviderSchema") p.mu.Lock() defer p.mu.Unlock() - if p.schemas.Provider.Block != nil { - return p.schemas + // check the global cache if we can + if !p.Addr.IsZero() { + if resp, ok := providers.SchemaCache.Get(p.Addr); ok && resp.ServerCapabilities.GetProviderSchemaOptional { + logger.Trace("GRPCProvider: returning cached schema", p.Addr.String()) + return resp + } } + // If the local cache is non-zero, we know this instance has called + // GetProviderSchema at least once and we can return early. + if p.schema.Provider.Body != nil { + return p.schema + } + + var resp providers.GetProviderSchemaResponse + resp.ResourceTypes = make(map[string]providers.Schema) resp.DataSources = make(map[string]providers.Schema) + resp.EphemeralResourceTypes = make(map[string]providers.Schema) // Some providers may generate quite large schemas, and the internal default // grpc response size limit is 4MB. 64MB should cover most any use case, and @@ -115,26 +128,95 @@ func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResp return resp } - resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider) + identResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We don't treat this as an error if older providers don't implement this method, + // so we create an empty map for identity schemas + identResp = &proto.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto.ResourceIdentitySchema{}, + } + } else { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + } + + resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider, nil) if protoResp.ProviderMeta == nil { logger.Debug("No provider meta schema returned") } else { - resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta) + resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta, nil) } for name, res := range protoResp.ResourceSchemas { - resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res) + id := identResp.IdentitySchemas[name] // We're fine if the id is not found + resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res, id) } for name, data := range protoResp.DataSourceSchemas { - resp.DataSources[name] = convert.ProtoToProviderSchema(data) + resp.DataSources[name] = convert.ProtoToProviderSchema(data, nil) + } + + for name, ephem := range protoResp.EphemeralResourceSchemas { + resp.EphemeralResourceTypes[name] = convert.ProtoToProviderSchema(ephem, nil) + } + + if decls, err := convert.FunctionDeclsFromProto(protoResp.Functions); err == nil { + resp.Functions = decls + } else { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp } if protoResp.ServerCapabilities != nil { resp.ServerCapabilities.PlanDestroy = protoResp.ServerCapabilities.PlanDestroy + resp.ServerCapabilities.GetProviderSchemaOptional = protoResp.ServerCapabilities.GetProviderSchemaOptional + resp.ServerCapabilities.MoveResourceState = protoResp.ServerCapabilities.MoveResourceState } - p.schemas = resp + // set the global cache if we can + if !p.Addr.IsZero() { + providers.SchemaCache.Set(p.Addr, resp) + } + + // always store this here in the client for providers that are not able to + // use GetProviderSchemaOptional + p.schema = resp + + return resp +} + +func (p *GRPCProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + logger.Trace("GRPCProvider: GetResourceIdentitySchemas") + + var resp providers.GetResourceIdentitySchemasResponse + + resp.IdentityTypes = make(map[string]providers.IdentitySchema) + + protoResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We expect no error here if older providers don't implement this method + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + if resp.Diagnostics.HasErrors() { + return resp + } + + for name, res := range protoResp.IdentitySchemas { + resp.IdentityTypes[name] = providers.IdentitySchema{ + Version: res.Version, + Body: convert.ProtoToIdentitySchema(res.IdentityAttributes), + } + } return resp } @@ -142,13 +224,13 @@ func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResp func (p *GRPCProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { logger.Trace("GRPCProvider: ValidateProviderConfig") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp } - ty := schema.Provider.Block.ImpliedType() + ty := schema.Provider.Body.ImpliedType() mp, err := msgpack.Marshal(r.Config, ty) if err != nil { @@ -180,7 +262,7 @@ func (p *GRPCProvider) ValidateProviderConfig(r providers.ValidateProviderConfig func (p *GRPCProvider) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) { logger.Trace("GRPCProvider: ValidateResourceConfig") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -192,15 +274,16 @@ func (p *GRPCProvider) ValidateResourceConfig(r providers.ValidateResourceConfig return resp } - mp, err := msgpack.Marshal(r.Config, resourceSchema.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.Config, resourceSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } protoReq := &proto.ValidateResourceTypeConfig_Request{ - TypeName: r.TypeName, - Config: &proto.DynamicValue{Msgpack: mp}, + TypeName: r.TypeName, + Config: &proto.DynamicValue{Msgpack: mp}, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } protoResp, err := p.client.ValidateResourceTypeConfig(p.ctx, protoReq) @@ -216,7 +299,7 @@ func (p *GRPCProvider) ValidateResourceConfig(r providers.ValidateResourceConfig func (p *GRPCProvider) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) (resp providers.ValidateDataResourceConfigResponse) { logger.Trace("GRPCProvider: ValidateDataResourceConfig") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -228,7 +311,7 @@ func (p *GRPCProvider) ValidateDataResourceConfig(r providers.ValidateDataResour return resp } - mp, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.Config, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -251,7 +334,7 @@ func (p *GRPCProvider) ValidateDataResourceConfig(r providers.ValidateDataResour func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { logger.Trace("GRPCProvider: UpgradeResourceState") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -279,7 +362,7 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - ty := resSchema.Block.ImpliedType() + ty := resSchema.Body.ImpliedType() resp.UpgradedState = cty.NullVal(ty) if protoResp.UpgradedState == nil { return resp @@ -295,10 +378,56 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } +func (p *GRPCProvider) UpgradeResourceIdentity(r providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + logger.Trace("GRPCProvider: UpgradeResourceIdentity") + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + resSchema, ok := schema.ResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource identity type %q", r.TypeName)) + return resp + } + + protoReq := &proto.UpgradeResourceIdentity_Request{ + TypeName: r.TypeName, + Version: int64(r.Version), + RawIdentity: &proto.RawState{ + Json: r.RawIdentityJSON, + }, + } + + protoResp, err := p.client.UpgradeResourceIdentity(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + ty := resSchema.Identity.ImpliedType() + resp.UpgradedIdentity = cty.NullVal(ty) + if protoResp.UpgradedIdentity == nil { + return resp + } + + identity, err := decodeDynamicValue(protoResp.UpgradedIdentity.IdentityData, ty) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.UpgradedIdentity = identity + + return resp +} + func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { logger.Trace("GRPCProvider: ConfigureProvider") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -307,7 +436,7 @@ func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) ( var mp []byte // we don't have anything to marshal if there's no config - mp, err := msgpack.Marshal(r.Config, schema.Provider.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.Config, schema.Provider.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -318,6 +447,7 @@ func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) ( Config: &proto.DynamicValue{ Msgpack: mp, }, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } protoResp, err := p.client.Configure(p.ctx, protoReq) @@ -346,7 +476,7 @@ func (p *GRPCProvider) Stop() error { func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { logger.Trace("GRPCProvider: ReadResource") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -354,26 +484,27 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi resSchema, ok := schema.ResourceTypes[r.TypeName] if !ok { - resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type " + r.TypeName)) + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %s", r.TypeName)) return resp } metaSchema := schema.ProviderMeta - mp, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.PriorState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } protoReq := &proto.ReadResource_Request{ - TypeName: r.TypeName, - CurrentState: &proto.DynamicValue{Msgpack: mp}, - Private: r.Private, + TypeName: r.TypeName, + CurrentState: &proto.DynamicValue{Msgpack: mp}, + Private: r.Private, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -381,14 +512,30 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi protoReq.ProviderMeta = &proto.DynamicValue{Msgpack: metaMP} } + if !r.CurrentIdentity.IsNull() { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %s", r.TypeName)) + return resp + } + currentIdentityMP, err := msgpack.Marshal(r.CurrentIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + protoReq.CurrentIdentity = &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{Msgpack: currentIdentityMP}, + } + } + protoResp, err := p.client.ReadResource(p.ctx, protoReq) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) return resp } + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.NewState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -396,13 +543,25 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi resp.NewState = state resp.Private = protoResp.Private + if protoResp.NewIdentity != nil && protoResp.NewIdentity.IdentityData != nil { + + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %q", r.TypeName)) + } + + resp.Identity, err = decodeDynamicValue(protoResp.NewIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + } + } + return resp } func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { logger.Trace("GRPCProvider: PlanResourceChange") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -425,34 +584,40 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) return resp } - priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType()) + priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType()) + configMP, err := msgpack.Marshal(r.Config, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Block.ImpliedType()) + propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } protoReq := &proto.PlanResourceChange_Request{ - TypeName: r.TypeName, - PriorState: &proto.DynamicValue{Msgpack: priorMP}, - Config: &proto.DynamicValue{Msgpack: configMP}, - ProposedNewState: &proto.DynamicValue{Msgpack: propMP}, - PriorPrivate: r.PriorPrivate, + TypeName: r.TypeName, + PriorState: &proto.DynamicValue{Msgpack: priorMP}, + Config: &proto.DynamicValue{Msgpack: configMP}, + ProposedNewState: &proto.DynamicValue{Msgpack: propMP}, + PriorPrivate: r.PriorPrivate, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaTy := metaSchema.Body.ImpliedType() + metaVal := r.ProviderMeta + if metaVal == cty.NilVal { + metaVal = cty.NullVal(metaTy) + } + metaMP, err := msgpack.Marshal(metaVal, metaTy) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -460,6 +625,21 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) protoReq.ProviderMeta = &proto.DynamicValue{Msgpack: metaMP} } + if !r.PriorIdentity.IsNull() { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %q", r.TypeName)) + return resp + } + priorIdentityMP, err := msgpack.Marshal(r.PriorIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + protoReq.PriorIdentity = &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{Msgpack: priorIdentityMP}, + } + } + protoResp, err := p.client.PlanResourceChange(p.ctx, protoReq) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) @@ -467,7 +647,7 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - state, err := decodeDynamicValue(protoResp.PlannedState, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.PlannedState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -482,13 +662,28 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) resp.LegacyTypeSystem = protoResp.LegacyTypeSystem + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) + + if protoResp.PlannedIdentity != nil && protoResp.PlannedIdentity.IdentityData != nil { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %s", r.TypeName)) + return resp + } + + resp.PlannedIdentity, err = decodeDynamicValue(protoResp.PlannedIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + } + return resp } func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { logger.Trace("GRPCProvider: ApplyResourceChange") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -502,17 +697,17 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques metaSchema := schema.ProviderMeta - priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType()) + priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - plannedMP, err := msgpack.Marshal(r.PlannedState, resSchema.Block.ImpliedType()) + plannedMP, err := msgpack.Marshal(r.PlannedState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType()) + configMP, err := msgpack.Marshal(r.Config, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -526,8 +721,13 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques PlannedPrivate: r.PlannedPrivate, } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaTy := metaSchema.Body.ImpliedType() + metaVal := r.ProviderMeta + if metaVal == cty.NilVal { + metaVal = cty.NullVal(metaTy) + } + metaMP, err := msgpack.Marshal(metaVal, metaTy) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -535,6 +735,21 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques protoReq.ProviderMeta = &proto.DynamicValue{Msgpack: metaMP} } + if !r.PlannedIdentity.IsNull() { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %s", r.TypeName)) + return resp + } + identityMP, err := msgpack.Marshal(r.PlannedIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + protoReq.PlannedIdentity = &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{Msgpack: identityMP}, + } + } + protoResp, err := p.client.ApplyResourceChange(p.ctx, protoReq) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) @@ -544,7 +759,7 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques resp.Private = protoResp.Private - state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.NewState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -553,21 +768,55 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques resp.LegacyTypeSystem = protoResp.LegacyTypeSystem + if protoResp.NewIdentity != nil && protoResp.NewIdentity.IdentityData != nil { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %s", r.TypeName)) + return resp + } + newIdentity, err := decodeDynamicValue(protoResp.NewIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.NewIdentity = newIdentity + } + return resp } func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) { logger.Trace("GRPCProvider: ImportResourceState") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp } protoReq := &proto.ImportResourceState_Request{ - TypeName: r.TypeName, - Id: r.ID, + TypeName: r.TypeName, + Id: r.ID, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), + } + + if !r.Identity.IsNull() { + resSchema := schema.ResourceTypes[r.TypeName] + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %q", r.TypeName)) + return resp + } + + mp, err := msgpack.Marshal(r.Identity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + protoReq.Identity = &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Msgpack: mp, + }, + } } protoResp, err := p.client.ImportResourceState(p.ctx, protoReq) @@ -576,6 +825,7 @@ func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateReques return resp } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) for _, imported := range protoResp.ImportedResources { resource := providers.ImportedResource{ @@ -589,22 +839,106 @@ func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateReques continue } - state, err := decodeDynamicValue(imported.State, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(imported.State, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } resource.State = state + + if imported.Identity != nil && imported.Identity.IdentityData != nil { + importedIdentitySchema, ok := schema.ResourceTypes[imported.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %q", imported.TypeName)) + continue + } + importedIdentity, err := decodeDynamicValue(imported.Identity.IdentityData, importedIdentitySchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resource.Identity = importedIdentity + } + resp.ImportedResources = append(resp.ImportedResources, resource) } return resp } +func (p *GRPCProvider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { + logger.Trace("GRPCProvider: MoveResourceState") + + protoReq := &proto.MoveResourceState_Request{ + SourceProviderAddress: r.SourceProviderAddress, + SourceTypeName: r.SourceTypeName, + SourceSchemaVersion: r.SourceSchemaVersion, + SourceState: &proto.RawState{ + Json: r.SourceStateJSON, + }, + SourcePrivate: r.SourcePrivate, + TargetTypeName: r.TargetTypeName, + } + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + if len(r.SourceIdentity) > 0 { + protoReq.SourceIdentity = &proto.RawState{ + Json: r.SourceIdentity, + } + } + + protoResp, err := p.client.MoveResourceState(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + if resp.Diagnostics.HasErrors() { + return resp + } + + targetType, ok := schema.ResourceTypes[r.TargetTypeName] + if !ok { + // We should have validated this earlier in the process, but we'll + // still return an error instead of crashing in case something went + // wrong. + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %q; this is a bug in Terraform - please report it", r.TargetTypeName)) + return resp + } + resp.TargetState, err = decodeDynamicValue(protoResp.TargetState, targetType.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + resp.TargetPrivate = protoResp.TargetPrivate + + if protoResp.TargetIdentity != nil && protoResp.TargetIdentity.IdentityData != nil { + targetResSchema := schema.ResourceTypes[r.TargetTypeName] + + if targetResSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %s", r.TargetTypeName)) + return resp + } + resp.TargetIdentity, err = decodeDynamicValue(protoResp.TargetIdentity.IdentityData, targetResSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + } + + return resp +} + func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { logger.Trace("GRPCProvider: ReadDataSource") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -617,7 +951,7 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p metaSchema := schema.ProviderMeta - config, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType()) + config, err := msgpack.Marshal(r.Config, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -628,10 +962,11 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p Config: &proto.DynamicValue{ Msgpack: config, }, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -646,16 +981,230 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - state, err := decodeDynamicValue(protoResp.State, dataSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.State, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } resp.State = state + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) return resp } +func (p *GRPCProvider) ValidateEphemeralResourceConfig(r providers.ValidateEphemeralResourceConfigRequest) (resp providers.ValidateEphemeralResourceConfigResponse) { + logger.Trace("GRPCProvider: ValidateEphemeralResourceConfig") + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + ephemSchema, ok := schema.EphemeralResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown ephemeral resource %q", r.TypeName)) + return resp + } + + mp, err := msgpack.Marshal(r.Config, ephemSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + protoReq := &proto.ValidateEphemeralResourceConfig_Request{ + TypeName: r.TypeName, + Config: &proto.DynamicValue{Msgpack: mp}, + } + + protoResp, err := p.client.ValidateEphemeralResourceConfig(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + return resp +} + +func (p *GRPCProvider) OpenEphemeralResource(r providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + logger.Trace("GRPCProvider: OpenEphemeralResource") + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + ephemSchema, ok := schema.EphemeralResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown ephemeral resource %q", r.TypeName)) + return resp + } + + config, err := msgpack.Marshal(r.Config, ephemSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + protoReq := &proto.OpenEphemeralResource_Request{ + TypeName: r.TypeName, + Config: &proto.DynamicValue{ + Msgpack: config, + }, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), + } + + protoResp, err := p.client.OpenEphemeralResource(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + state, err := decodeDynamicValue(protoResp.Result, ephemSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + if protoResp.RenewAt != nil { + resp.RenewAt = protoResp.RenewAt.AsTime() + } + + resp.Result = state + resp.Private = protoResp.Private + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) + + return resp +} + +func (p *GRPCProvider) RenewEphemeralResource(r providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) { + logger.Trace("GRPCProvider: RenewEphemeralResource") + + protoReq := &proto.RenewEphemeralResource_Request{ + TypeName: r.TypeName, + Private: r.Private, + } + + protoResp, err := p.client.RenewEphemeralResource(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + if protoResp.RenewAt != nil { + resp.RenewAt = protoResp.RenewAt.AsTime() + } + + resp.Private = protoResp.Private + + return resp +} + +func (p *GRPCProvider) CloseEphemeralResource(r providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { + logger.Trace("GRPCProvider: CloseEphemeralResource") + + protoReq := &proto.CloseEphemeralResource_Request{ + TypeName: r.TypeName, + Private: r.Private, + } + + protoResp, err := p.client.CloseEphemeralResource(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + return resp +} + +func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + logger.Trace("GRPCProvider", "CallFunction", r.FunctionName) + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Err = schema.Diagnostics.Err() + return resp + } + + funcDecl, ok := schema.Functions[r.FunctionName] + // We check for various problems with the request below in the interests + // of robustness, just to avoid crashing while trying to encode/decode, but + // if we reach any of these errors then that suggests a bug in the caller, + // because we should catch function calls that don't match the schema at an + // earlier point than this. + if !ok { + // Should only get here if the caller has a bug, because we should + // have detected earlier any attempt to call a function that the + // provider didn't declare. + resp.Err = fmt.Errorf("provider has no function named %q", r.FunctionName) + return resp + } + if len(r.Arguments) < len(funcDecl.Parameters) { + resp.Err = fmt.Errorf("not enough arguments for function %q", r.FunctionName) + return resp + } + if funcDecl.VariadicParameter == nil && len(r.Arguments) > len(funcDecl.Parameters) { + resp.Err = fmt.Errorf("too many arguments for function %q", r.FunctionName) + return resp + } + args := make([]*proto.DynamicValue, len(r.Arguments)) + for i, argVal := range r.Arguments { + var paramDecl providers.FunctionParam + if i < len(funcDecl.Parameters) { + paramDecl = funcDecl.Parameters[i] + } else { + paramDecl = *funcDecl.VariadicParameter + } + + argValRaw, err := msgpack.Marshal(argVal, paramDecl.Type) + if err != nil { + resp.Err = err + return resp + } + args[i] = &proto.DynamicValue{ + Msgpack: argValRaw, + } + } + + protoResp, err := p.client.CallFunction(p.ctx, &proto.CallFunction_Request{ + Name: r.FunctionName, + Arguments: args, + }) + if err != nil { + // functions can only support simple errors, but use our grpcError + // diagnostic function to format common problems is a more + // user-friendly manner. + resp.Err = grpcErr(err).Err() + return resp + } + + if protoResp.Error != nil { + resp.Err = errors.New(protoResp.Error.Text) + + // If this is a problem with a specific argument, we can wrap the error + // in a function.ArgError + if protoResp.Error.FunctionArgument != nil { + resp.Err = function.NewArgError(int(*protoResp.Error.FunctionArgument), resp.Err) + } + + return resp + } + + resultVal, err := decodeDynamicValue(protoResp.Result, funcDecl.ReturnType) + if err != nil { + resp.Err = err + return resp + } + + resp.Result = resultVal + return resp +} + // closing the grpc connection is final, and terraform will call it at the end of every phase. func (p *GRPCProvider) Close() error { logger.Trace("GRPCProvider: Close") @@ -695,3 +1244,10 @@ func decodeDynamicValue(v *proto.DynamicValue, ty cty.Type) (cty.Value, error) { } return res, err } + +func clientCapabilitiesToProto(c providers.ClientCapabilities) *proto.ClientCapabilities { + return &proto.ClientCapabilities{ + DeferralAllowed: c.DeferralAllowed, + WriteOnlyAttributesAllowed: c.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/plugin/grpc_provider_test.go b/internal/plugin/grpc_provider_test.go index 4e8d2f6c92..c34052ec7e 100644 --- a/internal/plugin/grpc_provider_test.go +++ b/internal/plugin/grpc_provider_test.go @@ -1,16 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( "bytes" "fmt" "testing" + "time" - "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" mockproto "github.com/hashicorp/terraform/internal/plugin/mock_proto" proto "github.com/hashicorp/terraform/internal/tfplugin5" @@ -29,6 +37,13 @@ func mockProviderClient(t *testing.T) *mockproto.MockProviderClient { gomock.Any(), ).Return(providerProtoSchema(), nil) + // GetResourceIdentitySchemas is called as part of GetSchema + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + return client } @@ -89,6 +104,39 @@ func providerProtoSchema() *proto.GetProviderSchema_Response { }, }, }, + EphemeralResourceSchemas: map[string]*proto.Schema{ + "ephemeral": &proto.Schema{ + Block: &proto.Schema_Block{ + Attributes: []*proto.Schema_Attribute{ + { + Name: "attr", + Type: []byte(`"string"`), + Computed: true, + }, + }, + }, + }, + }, + ServerCapabilities: &proto.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + } +} + +func providerResourceIdentitySchemas() *proto.GetResourceIdentitySchemas_Response { + return &proto.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto.ResourceIdentitySchema{ + "resource": { + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "id_attr", + Type: []byte(`"string"`), + RequiredForImport: true, + }, + }, + }, + }, } } @@ -101,6 +149,27 @@ func TestGRPCProvider_GetSchema(t *testing.T) { checkDiags(t, resp.Diagnostics) } +// ensure that the global schema cache is used when the provider supports +// GetProviderSchemaOptional +func TestGRPCProvider_GetSchema_globalCache(t *testing.T) { + p := &GRPCProvider{ + Addr: addrs.ImpliedProviderForUnqualifiedType("test"), + client: mockProviderClient(t), + } + + // first call primes the cache + resp := p.GetProviderSchema() + + // create a new provider instance which does not expect a GetProviderSchemaCall + p = &GRPCProvider{ + Addr: addrs.ImpliedProviderForUnqualifiedType("test"), + client: mockproto.NewMockProviderClient(gomock.NewController(t)), + } + + resp = p.GetProviderSchema() + checkDiags(t, resp.Diagnostics) +} + // Ensure that gRPC errors are returned early. // Reference: https://github.com/hashicorp/terraform/issues/31047 func TestGRPCProvider_GetSchema_GRPCError(t *testing.T) { @@ -153,6 +222,94 @@ func TestGRPCProvider_GetSchema_ResponseErrorDiagnostic(t *testing.T) { checkDiagsHasError(t, resp.Diagnostics) } +func TestGRPCProvider_GetSchema_IdentityError(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, fmt.Errorf("test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiagsHasError(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetSchema_IdentityUnimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas_Unimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -269,6 +426,84 @@ func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) { } } +func TestGRPCProvider_UpgradeResourceIdentity(t *testing.T) { + testCases := []struct { + desc string + response *proto.UpgradeResourceIdentity_Response + expectError bool + expectedValue cty.Value + }{ + { + "successful upgrade", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"id_attr":"bar"}`), + }, + }, + }, + false, + cty.ObjectVal(map[string]cty.Value{"id_attr": cty.StringVal("bar")}), + }, + { + "response with error diagnostic", + &proto.UpgradeResourceIdentity_Response{ + Diagnostics: []*proto.Diagnostic{ + { + Severity: proto.Diagnostic_ERROR, + Summary: "test error", + Detail: "test error detail", + }, + }, + }, + true, + cty.NilVal, + }, + { + "schema mismatch", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"attr_new":"bar"}`), + }, + }, + }, + true, + cty.NilVal, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().UpgradeResourceIdentity( + gomock.Any(), + gomock.Any(), + ).Return(tc.response, nil) + + resp := p.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: "resource", + Version: 0, + RawIdentityJSON: []byte(`{"old_attr":"bar"}`), + }) + + if tc.expectError { + checkDiagsHasError(t, resp.Diagnostics) + } else { + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty)) + } + } + }) + } +} + func TestGRPCProvider_Configure(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -339,6 +574,41 @@ func TestGRPCProvider_ReadResource(t *testing.T) { } } +func TestGRPCProvider_ReadResource_deferred(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadResource_Response{ + NewState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + Deferred: &proto.Deferred{ + Reason: proto.Deferred_ABSENT_PREREQ, + }, + }, nil) + + resp := p.ReadResource(providers.ReadResourceRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expectedDeferred := &providers.Deferred{ + Reason: providers.DeferredReasonAbsentPrereq, + } + if !cmp.Equal(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty)) + } +} + func TestGRPCProvider_ReadResourceJSON(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -666,6 +936,7 @@ func TestGRPCProvider_ImportResourceState(t *testing.T) { t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty)) } } + func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -710,6 +981,132 @@ func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) { } } +func TestGRPCProvider_ImportResourceState_Identity(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ImportResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ImportResourceState_Response{ + ImportedResources: []*proto.ImportResourceState_ImportedResource{ + { + TypeName: "resource", + State: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + Identity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa7id_attr\xa3foo"), + }, + }, + }, + }, + }, nil) + + resp := p.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: "resource", + Identity: cty.ObjectVal(map[string]cty.Value{ + "id_attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expectedResource := providers.ImportedResource{ + TypeName: "resource", + State: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Identity: cty.ObjectVal(map[string]cty.Value{ + "id_attr": cty.StringVal("foo"), + }), + } + + imported := resp.ImportedResources[0] + if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_MoveResourceState(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedTargetPrivate := []byte(`{"target": "private"}`) + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + client.EXPECT().MoveResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.MoveResourceState_Response{ + TargetState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + TargetPrivate: expectedTargetPrivate, + }, nil) + + resp := p.MoveResourceState(providers.MoveResourceStateRequest{ + SourcePrivate: []byte(`{"source": "private"}`), + SourceStateJSON: []byte(`{"source_attr":"bar"}`), + TargetTypeName: "resource", + }) + + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty)) + } + + if !cmp.Equal(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_MoveResourceStateJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedTargetPrivate := []byte(`{"target": "private"}`) + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + client.EXPECT().MoveResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.MoveResourceState_Response{ + TargetState: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + TargetPrivate: expectedTargetPrivate, + }, nil) + + resp := p.MoveResourceState(providers.MoveResourceStateRequest{ + SourcePrivate: []byte(`{"source": "private"}`), + SourceStateJSON: []byte(`{"source_attr":"bar"}`), + TargetTypeName: "resource", + }) + + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty)) + } + + if !cmp.Equal(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty)) + } +} + func TestGRPCProvider_ReadDataSource(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -775,3 +1172,95 @@ func TestGRPCProvider_ReadDataSourceJSON(t *testing.T) { t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty)) } } + +func TestGRPCProvider_openEphemeralResource(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().OpenEphemeralResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.OpenEphemeralResource_Response{ + Result: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + RenewAt: timestamppb.New(time.Now().Add(time.Second)), + Private: []byte("private data"), + }, nil) + + resp := p.OpenEphemeralResource(providers.OpenEphemeralResourceRequest{ + TypeName: "ephemeral", + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.NullVal(cty.String), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.Result, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.Result, typeComparer, valueComparer, equateEmpty)) + } + + if !resp.RenewAt.After(time.Now()) { + t.Fatal("invalid RenewAt:", resp.RenewAt) + } + + if !bytes.Equal(resp.Private, []byte("private data")) { + t.Fatalf("invalid private data: %q", resp.Private) + } +} + +func TestGRPCProvider_renewEphemeralResource(t *testing.T) { + client := mockproto.NewMockProviderClient(gomock.NewController(t)) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().RenewEphemeralResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.RenewEphemeralResource_Response{ + RenewAt: timestamppb.New(time.Now().Add(time.Second)), + Private: []byte("private data"), + }, nil) + + resp := p.RenewEphemeralResource(providers.RenewEphemeralResourceRequest{ + TypeName: "ephemeral", + Private: []byte("private data"), + }) + + checkDiags(t, resp.Diagnostics) + + if !resp.RenewAt.After(time.Now()) { + t.Fatal("invalid RenewAt:", resp.RenewAt) + } + + if !bytes.Equal(resp.Private, []byte("private data")) { + t.Fatalf("invalid private data: %q", resp.Private) + } +} + +func TestGRPCProvider_closeEphemeralResource(t *testing.T) { + client := mockproto.NewMockProviderClient(gomock.NewController(t)) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().CloseEphemeralResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.CloseEphemeralResource_Response{}, nil) + + resp := p.CloseEphemeralResource(providers.CloseEphemeralResourceRequest{ + TypeName: "ephemeral", + Private: []byte("private data"), + }) + + checkDiags(t, resp.Diagnostics) +} diff --git a/internal/plugin/grpc_provisioner.go b/internal/plugin/grpc_provisioner.go index 0a6ad8e632..27959c70b8 100644 --- a/internal/plugin/grpc_provisioner.go +++ b/internal/plugin/grpc_provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/grpc_provisioner_test.go b/internal/plugin/grpc_provisioner_test.go index 848c9460fa..450f43d455 100644 --- a/internal/plugin/grpc_provisioner_test.go +++ b/internal/plugin/grpc_provisioner_test.go @@ -1,16 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( "io" "testing" - "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/provisioners" proto "github.com/hashicorp/terraform/internal/tfplugin5" "github.com/zclconf/go-cty/cty" + "go.uber.org/mock/gomock" mockproto "github.com/hashicorp/terraform/internal/plugin/mock_proto" ) diff --git a/internal/plugin/mock_proto/generate.go b/internal/plugin/mock_proto/generate.go index 6f004ffd36..fdfc1696bd 100644 --- a/internal/plugin/mock_proto/generate.go +++ b/internal/plugin/mock_proto/generate.go @@ -1,3 +1,6 @@ -//go:generate go run github.com/golang/mock/mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin5 ProviderClient,ProvisionerClient,Provisioner_ProvisionResourceClient,Provisioner_ProvisionResourceServer +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:generate go tool go.uber.org/mock/mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin5 ProviderClient,ProvisionerClient,Provisioner_ProvisionResourceClient,Provisioner_ProvisionResourceServer package mock_tfplugin5 diff --git a/internal/plugin/mock_proto/mock.go b/internal/plugin/mock_proto/mock.go index 054fe1cd82..3e4ce1d823 100644 --- a/internal/plugin/mock_proto/mock.go +++ b/internal/plugin/mock_proto/mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/hashicorp/terraform/internal/tfplugin5 (interfaces: ProviderClient,ProvisionerClient,Provisioner_ProvisionResourceClient,Provisioner_ProvisionResourceServer) +// +// Generated by this command: +// +// mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin5 ProviderClient,ProvisionerClient,Provisioner_ProvisionResourceClient,Provisioner_ProvisionResourceServer +// // Package mock_tfplugin5 is a generated GoMock package. package mock_tfplugin5 @@ -8,8 +13,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" tfplugin5 "github.com/hashicorp/terraform/internal/tfplugin5" + gomock "go.uber.org/mock/gomock" grpc "google.golang.org/grpc" metadata "google.golang.org/grpc/metadata" ) @@ -40,7 +45,7 @@ func (m *MockProviderClient) EXPECT() *MockProviderClientMockRecorder { // ApplyResourceChange mocks base method. func (m *MockProviderClient) ApplyResourceChange(arg0 context.Context, arg1 *tfplugin5.ApplyResourceChange_Request, arg2 ...grpc.CallOption) (*tfplugin5.ApplyResourceChange_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -51,16 +56,56 @@ func (m *MockProviderClient) ApplyResourceChange(arg0 context.Context, arg1 *tfp } // ApplyResourceChange indicates an expected call of ApplyResourceChange. -func (mr *MockProviderClientMockRecorder) ApplyResourceChange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ApplyResourceChange(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyResourceChange", reflect.TypeOf((*MockProviderClient)(nil).ApplyResourceChange), varargs...) } +// CallFunction mocks base method. +func (m *MockProviderClient) CallFunction(arg0 context.Context, arg1 *tfplugin5.CallFunction_Request, arg2 ...grpc.CallOption) (*tfplugin5.CallFunction_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CallFunction", varargs...) + ret0, _ := ret[0].(*tfplugin5.CallFunction_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallFunction indicates an expected call of CallFunction. +func (mr *MockProviderClientMockRecorder) CallFunction(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallFunction", reflect.TypeOf((*MockProviderClient)(nil).CallFunction), varargs...) +} + +// CloseEphemeralResource mocks base method. +func (m *MockProviderClient) CloseEphemeralResource(arg0 context.Context, arg1 *tfplugin5.CloseEphemeralResource_Request, arg2 ...grpc.CallOption) (*tfplugin5.CloseEphemeralResource_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CloseEphemeralResource", varargs...) + ret0, _ := ret[0].(*tfplugin5.CloseEphemeralResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloseEphemeralResource indicates an expected call of CloseEphemeralResource. +func (mr *MockProviderClientMockRecorder) CloseEphemeralResource(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseEphemeralResource", reflect.TypeOf((*MockProviderClient)(nil).CloseEphemeralResource), varargs...) +} + // Configure mocks base method. func (m *MockProviderClient) Configure(arg0 context.Context, arg1 *tfplugin5.Configure_Request, arg2 ...grpc.CallOption) (*tfplugin5.Configure_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -71,16 +116,76 @@ func (m *MockProviderClient) Configure(arg0 context.Context, arg1 *tfplugin5.Con } // Configure indicates an expected call of Configure. -func (mr *MockProviderClientMockRecorder) Configure(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) Configure(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configure", reflect.TypeOf((*MockProviderClient)(nil).Configure), varargs...) } +// GetFunctions mocks base method. +func (m *MockProviderClient) GetFunctions(arg0 context.Context, arg1 *tfplugin5.GetFunctions_Request, arg2 ...grpc.CallOption) (*tfplugin5.GetFunctions_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetFunctions", varargs...) + ret0, _ := ret[0].(*tfplugin5.GetFunctions_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFunctions indicates an expected call of GetFunctions. +func (mr *MockProviderClientMockRecorder) GetFunctions(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFunctions", reflect.TypeOf((*MockProviderClient)(nil).GetFunctions), varargs...) +} + +// GetMetadata mocks base method. +func (m *MockProviderClient) GetMetadata(arg0 context.Context, arg1 *tfplugin5.GetMetadata_Request, arg2 ...grpc.CallOption) (*tfplugin5.GetMetadata_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetMetadata", varargs...) + ret0, _ := ret[0].(*tfplugin5.GetMetadata_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMetadata indicates an expected call of GetMetadata. +func (mr *MockProviderClientMockRecorder) GetMetadata(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*MockProviderClient)(nil).GetMetadata), varargs...) +} + +// GetResourceIdentitySchemas mocks base method. +func (m *MockProviderClient) GetResourceIdentitySchemas(arg0 context.Context, arg1 *tfplugin5.GetResourceIdentitySchemas_Request, arg2 ...grpc.CallOption) (*tfplugin5.GetResourceIdentitySchemas_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetResourceIdentitySchemas", varargs...) + ret0, _ := ret[0].(*tfplugin5.GetResourceIdentitySchemas_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResourceIdentitySchemas indicates an expected call of GetResourceIdentitySchemas. +func (mr *MockProviderClientMockRecorder) GetResourceIdentitySchemas(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceIdentitySchemas", reflect.TypeOf((*MockProviderClient)(nil).GetResourceIdentitySchemas), varargs...) +} + // GetSchema mocks base method. func (m *MockProviderClient) GetSchema(arg0 context.Context, arg1 *tfplugin5.GetProviderSchema_Request, arg2 ...grpc.CallOption) (*tfplugin5.GetProviderSchema_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -91,16 +196,16 @@ func (m *MockProviderClient) GetSchema(arg0 context.Context, arg1 *tfplugin5.Get } // GetSchema indicates an expected call of GetSchema. -func (mr *MockProviderClientMockRecorder) GetSchema(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) GetSchema(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*MockProviderClient)(nil).GetSchema), varargs...) } // ImportResourceState mocks base method. func (m *MockProviderClient) ImportResourceState(arg0 context.Context, arg1 *tfplugin5.ImportResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin5.ImportResourceState_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -111,16 +216,56 @@ func (m *MockProviderClient) ImportResourceState(arg0 context.Context, arg1 *tfp } // ImportResourceState indicates an expected call of ImportResourceState. -func (mr *MockProviderClientMockRecorder) ImportResourceState(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ImportResourceState(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportResourceState", reflect.TypeOf((*MockProviderClient)(nil).ImportResourceState), varargs...) } +// MoveResourceState mocks base method. +func (m *MockProviderClient) MoveResourceState(arg0 context.Context, arg1 *tfplugin5.MoveResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin5.MoveResourceState_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "MoveResourceState", varargs...) + ret0, _ := ret[0].(*tfplugin5.MoveResourceState_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MoveResourceState indicates an expected call of MoveResourceState. +func (mr *MockProviderClientMockRecorder) MoveResourceState(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveResourceState", reflect.TypeOf((*MockProviderClient)(nil).MoveResourceState), varargs...) +} + +// OpenEphemeralResource mocks base method. +func (m *MockProviderClient) OpenEphemeralResource(arg0 context.Context, arg1 *tfplugin5.OpenEphemeralResource_Request, arg2 ...grpc.CallOption) (*tfplugin5.OpenEphemeralResource_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "OpenEphemeralResource", varargs...) + ret0, _ := ret[0].(*tfplugin5.OpenEphemeralResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// OpenEphemeralResource indicates an expected call of OpenEphemeralResource. +func (mr *MockProviderClientMockRecorder) OpenEphemeralResource(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenEphemeralResource", reflect.TypeOf((*MockProviderClient)(nil).OpenEphemeralResource), varargs...) +} + // PlanResourceChange mocks base method. func (m *MockProviderClient) PlanResourceChange(arg0 context.Context, arg1 *tfplugin5.PlanResourceChange_Request, arg2 ...grpc.CallOption) (*tfplugin5.PlanResourceChange_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -131,16 +276,16 @@ func (m *MockProviderClient) PlanResourceChange(arg0 context.Context, arg1 *tfpl } // PlanResourceChange indicates an expected call of PlanResourceChange. -func (mr *MockProviderClientMockRecorder) PlanResourceChange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) PlanResourceChange(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PlanResourceChange", reflect.TypeOf((*MockProviderClient)(nil).PlanResourceChange), varargs...) } // PrepareProviderConfig mocks base method. func (m *MockProviderClient) PrepareProviderConfig(arg0 context.Context, arg1 *tfplugin5.PrepareProviderConfig_Request, arg2 ...grpc.CallOption) (*tfplugin5.PrepareProviderConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -151,16 +296,16 @@ func (m *MockProviderClient) PrepareProviderConfig(arg0 context.Context, arg1 *t } // PrepareProviderConfig indicates an expected call of PrepareProviderConfig. -func (mr *MockProviderClientMockRecorder) PrepareProviderConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) PrepareProviderConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PrepareProviderConfig", reflect.TypeOf((*MockProviderClient)(nil).PrepareProviderConfig), varargs...) } // ReadDataSource mocks base method. func (m *MockProviderClient) ReadDataSource(arg0 context.Context, arg1 *tfplugin5.ReadDataSource_Request, arg2 ...grpc.CallOption) (*tfplugin5.ReadDataSource_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -171,16 +316,16 @@ func (m *MockProviderClient) ReadDataSource(arg0 context.Context, arg1 *tfplugin } // ReadDataSource indicates an expected call of ReadDataSource. -func (mr *MockProviderClientMockRecorder) ReadDataSource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ReadDataSource(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataSource", reflect.TypeOf((*MockProviderClient)(nil).ReadDataSource), varargs...) } // ReadResource mocks base method. func (m *MockProviderClient) ReadResource(arg0 context.Context, arg1 *tfplugin5.ReadResource_Request, arg2 ...grpc.CallOption) (*tfplugin5.ReadResource_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -191,16 +336,36 @@ func (m *MockProviderClient) ReadResource(arg0 context.Context, arg1 *tfplugin5. } // ReadResource indicates an expected call of ReadResource. -func (mr *MockProviderClientMockRecorder) ReadResource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ReadResource(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadResource", reflect.TypeOf((*MockProviderClient)(nil).ReadResource), varargs...) } +// RenewEphemeralResource mocks base method. +func (m *MockProviderClient) RenewEphemeralResource(arg0 context.Context, arg1 *tfplugin5.RenewEphemeralResource_Request, arg2 ...grpc.CallOption) (*tfplugin5.RenewEphemeralResource_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RenewEphemeralResource", varargs...) + ret0, _ := ret[0].(*tfplugin5.RenewEphemeralResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RenewEphemeralResource indicates an expected call of RenewEphemeralResource. +func (mr *MockProviderClientMockRecorder) RenewEphemeralResource(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewEphemeralResource", reflect.TypeOf((*MockProviderClient)(nil).RenewEphemeralResource), varargs...) +} + // Stop mocks base method. func (m *MockProviderClient) Stop(arg0 context.Context, arg1 *tfplugin5.Stop_Request, arg2 ...grpc.CallOption) (*tfplugin5.Stop_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -211,16 +376,36 @@ func (m *MockProviderClient) Stop(arg0 context.Context, arg1 *tfplugin5.Stop_Req } // Stop indicates an expected call of Stop. -func (mr *MockProviderClientMockRecorder) Stop(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) Stop(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockProviderClient)(nil).Stop), varargs...) } +// UpgradeResourceIdentity mocks base method. +func (m *MockProviderClient) UpgradeResourceIdentity(arg0 context.Context, arg1 *tfplugin5.UpgradeResourceIdentity_Request, arg2 ...grpc.CallOption) (*tfplugin5.UpgradeResourceIdentity_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpgradeResourceIdentity", varargs...) + ret0, _ := ret[0].(*tfplugin5.UpgradeResourceIdentity_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpgradeResourceIdentity indicates an expected call of UpgradeResourceIdentity. +func (mr *MockProviderClientMockRecorder) UpgradeResourceIdentity(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeResourceIdentity", reflect.TypeOf((*MockProviderClient)(nil).UpgradeResourceIdentity), varargs...) +} + // UpgradeResourceState mocks base method. func (m *MockProviderClient) UpgradeResourceState(arg0 context.Context, arg1 *tfplugin5.UpgradeResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin5.UpgradeResourceState_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -231,16 +416,16 @@ func (m *MockProviderClient) UpgradeResourceState(arg0 context.Context, arg1 *tf } // UpgradeResourceState indicates an expected call of UpgradeResourceState. -func (mr *MockProviderClientMockRecorder) UpgradeResourceState(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) UpgradeResourceState(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeResourceState", reflect.TypeOf((*MockProviderClient)(nil).UpgradeResourceState), varargs...) } // ValidateDataSourceConfig mocks base method. func (m *MockProviderClient) ValidateDataSourceConfig(arg0 context.Context, arg1 *tfplugin5.ValidateDataSourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin5.ValidateDataSourceConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -251,16 +436,36 @@ func (m *MockProviderClient) ValidateDataSourceConfig(arg0 context.Context, arg1 } // ValidateDataSourceConfig indicates an expected call of ValidateDataSourceConfig. -func (mr *MockProviderClientMockRecorder) ValidateDataSourceConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ValidateDataSourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateDataSourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateDataSourceConfig), varargs...) } +// ValidateEphemeralResourceConfig mocks base method. +func (m *MockProviderClient) ValidateEphemeralResourceConfig(arg0 context.Context, arg1 *tfplugin5.ValidateEphemeralResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin5.ValidateEphemeralResourceConfig_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ValidateEphemeralResourceConfig", varargs...) + ret0, _ := ret[0].(*tfplugin5.ValidateEphemeralResourceConfig_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateEphemeralResourceConfig indicates an expected call of ValidateEphemeralResourceConfig. +func (mr *MockProviderClientMockRecorder) ValidateEphemeralResourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateEphemeralResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateEphemeralResourceConfig), varargs...) +} + // ValidateResourceTypeConfig mocks base method. func (m *MockProviderClient) ValidateResourceTypeConfig(arg0 context.Context, arg1 *tfplugin5.ValidateResourceTypeConfig_Request, arg2 ...grpc.CallOption) (*tfplugin5.ValidateResourceTypeConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -271,9 +476,9 @@ func (m *MockProviderClient) ValidateResourceTypeConfig(arg0 context.Context, ar } // ValidateResourceTypeConfig indicates an expected call of ValidateResourceTypeConfig. -func (mr *MockProviderClientMockRecorder) ValidateResourceTypeConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ValidateResourceTypeConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateResourceTypeConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateResourceTypeConfig), varargs...) } @@ -303,7 +508,7 @@ func (m *MockProvisionerClient) EXPECT() *MockProvisionerClientMockRecorder { // GetSchema mocks base method. func (m *MockProvisionerClient) GetSchema(arg0 context.Context, arg1 *tfplugin5.GetProvisionerSchema_Request, arg2 ...grpc.CallOption) (*tfplugin5.GetProvisionerSchema_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -314,16 +519,16 @@ func (m *MockProvisionerClient) GetSchema(arg0 context.Context, arg1 *tfplugin5. } // GetSchema indicates an expected call of GetSchema. -func (mr *MockProvisionerClientMockRecorder) GetSchema(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProvisionerClientMockRecorder) GetSchema(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSchema", reflect.TypeOf((*MockProvisionerClient)(nil).GetSchema), varargs...) } // ProvisionResource mocks base method. func (m *MockProvisionerClient) ProvisionResource(arg0 context.Context, arg1 *tfplugin5.ProvisionResource_Request, arg2 ...grpc.CallOption) (tfplugin5.Provisioner_ProvisionResourceClient, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -334,16 +539,16 @@ func (m *MockProvisionerClient) ProvisionResource(arg0 context.Context, arg1 *tf } // ProvisionResource indicates an expected call of ProvisionResource. -func (mr *MockProvisionerClientMockRecorder) ProvisionResource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProvisionerClientMockRecorder) ProvisionResource(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProvisionResource", reflect.TypeOf((*MockProvisionerClient)(nil).ProvisionResource), varargs...) } // Stop mocks base method. func (m *MockProvisionerClient) Stop(arg0 context.Context, arg1 *tfplugin5.Stop_Request, arg2 ...grpc.CallOption) (*tfplugin5.Stop_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -354,16 +559,16 @@ func (m *MockProvisionerClient) Stop(arg0 context.Context, arg1 *tfplugin5.Stop_ } // Stop indicates an expected call of Stop. -func (mr *MockProvisionerClientMockRecorder) Stop(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProvisionerClientMockRecorder) Stop(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockProvisionerClient)(nil).Stop), varargs...) } // ValidateProvisionerConfig mocks base method. func (m *MockProvisionerClient) ValidateProvisionerConfig(arg0 context.Context, arg1 *tfplugin5.ValidateProvisionerConfig_Request, arg2 ...grpc.CallOption) (*tfplugin5.ValidateProvisionerConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -374,9 +579,9 @@ func (m *MockProvisionerClient) ValidateProvisionerConfig(arg0 context.Context, } // ValidateProvisionerConfig indicates an expected call of ValidateProvisionerConfig. -func (mr *MockProvisionerClientMockRecorder) ValidateProvisionerConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProvisionerClientMockRecorder) ValidateProvisionerConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateProvisionerConfig", reflect.TypeOf((*MockProvisionerClient)(nil).ValidateProvisionerConfig), varargs...) } @@ -462,7 +667,7 @@ func (mr *MockProvisioner_ProvisionResourceClientMockRecorder) Recv() *gomock.Ca } // RecvMsg mocks base method. -func (m *MockProvisioner_ProvisionResourceClient) RecvMsg(arg0 interface{}) error { +func (m *MockProvisioner_ProvisionResourceClient) RecvMsg(arg0 any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RecvMsg", arg0) ret0, _ := ret[0].(error) @@ -470,13 +675,13 @@ func (m *MockProvisioner_ProvisionResourceClient) RecvMsg(arg0 interface{}) erro } // RecvMsg indicates an expected call of RecvMsg. -func (mr *MockProvisioner_ProvisionResourceClientMockRecorder) RecvMsg(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceClientMockRecorder) RecvMsg(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecvMsg", reflect.TypeOf((*MockProvisioner_ProvisionResourceClient)(nil).RecvMsg), arg0) } // SendMsg mocks base method. -func (m *MockProvisioner_ProvisionResourceClient) SendMsg(arg0 interface{}) error { +func (m *MockProvisioner_ProvisionResourceClient) SendMsg(arg0 any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendMsg", arg0) ret0, _ := ret[0].(error) @@ -484,7 +689,7 @@ func (m *MockProvisioner_ProvisionResourceClient) SendMsg(arg0 interface{}) erro } // SendMsg indicates an expected call of SendMsg. -func (mr *MockProvisioner_ProvisionResourceClientMockRecorder) SendMsg(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceClientMockRecorder) SendMsg(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMsg", reflect.TypeOf((*MockProvisioner_ProvisionResourceClient)(nil).SendMsg), arg0) } @@ -541,7 +746,7 @@ func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) Context() *gomock } // RecvMsg mocks base method. -func (m *MockProvisioner_ProvisionResourceServer) RecvMsg(arg0 interface{}) error { +func (m *MockProvisioner_ProvisionResourceServer) RecvMsg(arg0 any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RecvMsg", arg0) ret0, _ := ret[0].(error) @@ -549,7 +754,7 @@ func (m *MockProvisioner_ProvisionResourceServer) RecvMsg(arg0 interface{}) erro } // RecvMsg indicates an expected call of RecvMsg. -func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) RecvMsg(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) RecvMsg(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecvMsg", reflect.TypeOf((*MockProvisioner_ProvisionResourceServer)(nil).RecvMsg), arg0) } @@ -563,7 +768,7 @@ func (m *MockProvisioner_ProvisionResourceServer) Send(arg0 *tfplugin5.Provision } // Send indicates an expected call of Send. -func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) Send(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) Send(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Send", reflect.TypeOf((*MockProvisioner_ProvisionResourceServer)(nil).Send), arg0) } @@ -577,13 +782,13 @@ func (m *MockProvisioner_ProvisionResourceServer) SendHeader(arg0 metadata.MD) e } // SendHeader indicates an expected call of SendHeader. -func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SendHeader(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SendHeader(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendHeader", reflect.TypeOf((*MockProvisioner_ProvisionResourceServer)(nil).SendHeader), arg0) } // SendMsg mocks base method. -func (m *MockProvisioner_ProvisionResourceServer) SendMsg(arg0 interface{}) error { +func (m *MockProvisioner_ProvisionResourceServer) SendMsg(arg0 any) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SendMsg", arg0) ret0, _ := ret[0].(error) @@ -591,7 +796,7 @@ func (m *MockProvisioner_ProvisionResourceServer) SendMsg(arg0 interface{}) erro } // SendMsg indicates an expected call of SendMsg. -func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SendMsg(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SendMsg(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMsg", reflect.TypeOf((*MockProvisioner_ProvisionResourceServer)(nil).SendMsg), arg0) } @@ -605,7 +810,7 @@ func (m *MockProvisioner_ProvisionResourceServer) SetHeader(arg0 metadata.MD) er } // SetHeader indicates an expected call of SetHeader. -func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SetHeader(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SetHeader(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetHeader", reflect.TypeOf((*MockProvisioner_ProvisionResourceServer)(nil).SetHeader), arg0) } @@ -617,7 +822,7 @@ func (m *MockProvisioner_ProvisionResourceServer) SetTrailer(arg0 metadata.MD) { } // SetTrailer indicates an expected call of SetTrailer. -func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SetTrailer(arg0 interface{}) *gomock.Call { +func (mr *MockProvisioner_ProvisionResourceServerMockRecorder) SetTrailer(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTrailer", reflect.TypeOf((*MockProvisioner_ProvisionResourceServer)(nil).SetTrailer), arg0) } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 27df5ba0c4..4001a60d43 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/serve.go b/internal/plugin/serve.go index 27d3c9e6d4..4b4c813ec9 100644 --- a/internal/plugin/serve.go +++ b/internal/plugin/serve.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/ui_input.go b/internal/plugin/ui_input.go index 9a6f00a8c2..8f92aa3860 100644 --- a/internal/plugin/ui_input.go +++ b/internal/plugin/ui_input.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/ui_input_test.go b/internal/plugin/ui_input_test.go index 59cb0629a6..9f0a72848e 100644 --- a/internal/plugin/ui_input_test.go +++ b/internal/plugin/ui_input_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/ui_output.go b/internal/plugin/ui_output.go index 130bbe30e2..f31b4f8277 100644 --- a/internal/plugin/ui_output.go +++ b/internal/plugin/ui_output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin/ui_output_test.go b/internal/plugin/ui_output_test.go index 5d9b8910d5..3239bc7a00 100644 --- a/internal/plugin/ui_output_test.go +++ b/internal/plugin/ui_output_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin import ( diff --git a/internal/plugin6/convert/deferred.go b/internal/plugin6/convert/deferred.go new file mode 100644 index 0000000000..1998d4ea93 --- /dev/null +++ b/internal/plugin6/convert/deferred.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "github.com/hashicorp/terraform/internal/providers" + proto "github.com/hashicorp/terraform/internal/tfplugin6" +) + +// ProtoToDeferred translates a proto.Deferred to a providers.Deferred. +func ProtoToDeferred(d *proto.Deferred) *providers.Deferred { + if d == nil { + return nil + } + + var reason providers.DeferredReason + switch d.Reason { + case proto.Deferred_UNKNOWN: + reason = providers.DeferredReasonInvalid + case proto.Deferred_RESOURCE_CONFIG_UNKNOWN: + reason = providers.DeferredReasonResourceConfigUnknown + case proto.Deferred_PROVIDER_CONFIG_UNKNOWN: + reason = providers.DeferredReasonProviderConfigUnknown + case proto.Deferred_ABSENT_PREREQ: + reason = providers.DeferredReasonAbsentPrereq + default: + reason = providers.DeferredReasonInvalid + } + + return &providers.Deferred{ + Reason: reason, + } +} diff --git a/internal/plugin6/convert/deferred_test.go b/internal/plugin6/convert/deferred_test.go new file mode 100644 index 0000000000..907a8f7df6 --- /dev/null +++ b/internal/plugin6/convert/deferred_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/internal/providers" + proto "github.com/hashicorp/terraform/internal/tfplugin6" +) + +func TestProtoDeferred(t *testing.T) { + testCases := []struct { + reason proto.Deferred_Reason + expected providers.DeferredReason + }{ + { + reason: proto.Deferred_UNKNOWN, + expected: providers.DeferredReasonInvalid, + }, + { + reason: proto.Deferred_RESOURCE_CONFIG_UNKNOWN, + expected: providers.DeferredReasonResourceConfigUnknown, + }, + { + reason: proto.Deferred_PROVIDER_CONFIG_UNKNOWN, + expected: providers.DeferredReasonProviderConfigUnknown, + }, + { + reason: proto.Deferred_ABSENT_PREREQ, + expected: providers.DeferredReasonAbsentPrereq, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("deferred reason %q", tc.reason.String()), func(t *testing.T) { + d := &proto.Deferred{ + Reason: tc.reason, + } + + deferred := ProtoToDeferred(d) + if deferred.Reason != providers.DeferredReason(tc.expected) { + t.Fatalf("expected %q, got %q", tc.expected, deferred.Reason) + } + }) + } +} + +func TestProtoDeferred_Nil(t *testing.T) { + deferred := ProtoToDeferred(nil) + if deferred != nil { + t.Fatalf("expected nil, got %v", deferred) + } +} diff --git a/internal/plugin6/convert/diagnostics.go b/internal/plugin6/convert/diagnostics.go index 54058533e7..a1cd57a234 100644 --- a/internal/plugin6/convert/diagnostics.go +++ b/internal/plugin6/convert/diagnostics.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -41,6 +44,14 @@ func AppendProtoDiag(diags []*proto.Diagnostic, d interface{}) []*proto.Diagnost Severity: proto.Diagnostic_WARNING, Summary: d, }) + case tfdiags.Diagnostic: + diags = append(diags, DiagnosticToProto(d)) + + case tfdiags.Diagnostics: + for _, diag := range d { + diags = append(diags, DiagnosticToProto(diag)) + } + case *proto.Diagnostic: diags = append(diags, d) case []*proto.Diagnostic: @@ -49,6 +60,21 @@ func AppendProtoDiag(diags []*proto.Diagnostic, d interface{}) []*proto.Diagnost return diags } +func DiagnosticToProto(diag tfdiags.Diagnostic) *proto.Diagnostic { + ret := &proto.Diagnostic{} + switch diag.Severity() { + case tfdiags.Error: + ret.Severity = proto.Diagnostic_ERROR + case tfdiags.Warning: + ret.Severity = proto.Diagnostic_WARNING + } + + desc := diag.Description() + ret.Summary = desc.Summary + ret.Detail = desc.Detail + return ret +} + // ProtoToDiagnostics converts a list of proto.Diagnostics to a tf.Diagnostics. func ProtoToDiagnostics(ds []*proto.Diagnostic) tfdiags.Diagnostics { var diags tfdiags.Diagnostics diff --git a/internal/plugin6/convert/diagnostics_test.go b/internal/plugin6/convert/diagnostics_test.go index 10088a05f5..82cc97d4e8 100644 --- a/internal/plugin6/convert/diagnostics_test.go +++ b/internal/plugin6/convert/diagnostics_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -16,6 +19,9 @@ var ignoreUnexported = cmpopts.IgnoreUnexported( proto.Schema_Block{}, proto.Schema_NestedBlock{}, proto.Schema_Attribute{}, + proto.Schema_Object{}, + proto.ResourceIdentitySchema{}, + proto.ResourceIdentitySchema_IdentityAttribute{}, ) func TestProtoDiagnostics(t *testing.T) { diff --git a/internal/plugin6/convert/functions.go b/internal/plugin6/convert/functions.go new file mode 100644 index 0000000000..957ed1d719 --- /dev/null +++ b/internal/plugin6/convert/functions.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "encoding/json" + "fmt" + + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfplugin6" +) + +func FunctionDeclsFromProto(protoFuncs map[string]*tfplugin6.Function) (map[string]providers.FunctionDecl, error) { + if len(protoFuncs) == 0 { + return nil, nil + } + + ret := make(map[string]providers.FunctionDecl, len(protoFuncs)) + for name, protoFunc := range protoFuncs { + decl, err := FunctionDeclFromProto(protoFunc) + if err != nil { + return nil, fmt.Errorf("invalid declaration for function %q: %s", name, err) + } + ret[name] = decl + } + return ret, nil +} + +func FunctionDeclFromProto(protoFunc *tfplugin6.Function) (providers.FunctionDecl, error) { + var ret providers.FunctionDecl + + ret.Description = protoFunc.Description + ret.DescriptionKind = schemaStringKind(protoFunc.DescriptionKind) + ret.Summary = protoFunc.Summary + ret.DeprecationMessage = protoFunc.DeprecationMessage + + if err := json.Unmarshal(protoFunc.Return.Type, &ret.ReturnType); err != nil { + return ret, fmt.Errorf("invalid return type constraint: %s", err) + } + + if len(protoFunc.Parameters) != 0 { + ret.Parameters = make([]providers.FunctionParam, len(protoFunc.Parameters)) + for i, protoParam := range protoFunc.Parameters { + param, err := functionParamFromProto(protoParam) + if err != nil { + return ret, fmt.Errorf("invalid parameter %d (%q): %s", i, protoParam.Name, err) + } + ret.Parameters[i] = param + } + } + if protoFunc.VariadicParameter != nil { + param, err := functionParamFromProto(protoFunc.VariadicParameter) + if err != nil { + return ret, fmt.Errorf("invalid variadic parameter (%q): %s", protoFunc.VariadicParameter.Name, err) + } + ret.VariadicParameter = ¶m + } + + return ret, nil +} + +func functionParamFromProto(protoParam *tfplugin6.Function_Parameter) (providers.FunctionParam, error) { + var ret providers.FunctionParam + ret.Name = protoParam.Name + ret.Description = protoParam.Description + ret.DescriptionKind = schemaStringKind(protoParam.DescriptionKind) + ret.AllowNullValue = protoParam.AllowNullValue + ret.AllowUnknownValues = protoParam.AllowUnknownValues + if err := json.Unmarshal(protoParam.Type, &ret.Type); err != nil { + return ret, fmt.Errorf("invalid type constraint: %s", err) + } + return ret, nil +} + +func FunctionDeclsToProto(fns map[string]providers.FunctionDecl) (map[string]*tfplugin6.Function, error) { + if len(fns) == 0 { + return nil, nil + } + + ret := make(map[string]*tfplugin6.Function, len(fns)) + for name, fn := range fns { + decl, err := FunctionDeclToProto(fn) + if err != nil { + return nil, fmt.Errorf("invalid declaration for function %q: %s", name, err) + } + ret[name] = decl + } + return ret, nil +} + +func FunctionDeclToProto(fn providers.FunctionDecl) (*tfplugin6.Function, error) { + ret := &tfplugin6.Function{ + Return: &tfplugin6.Function_Return{}, + } + + ret.Description = fn.Description + ret.DescriptionKind = protoStringKind(fn.DescriptionKind) + + retTy, err := json.Marshal(fn.ReturnType) + if err != nil { + return ret, fmt.Errorf("invalid return type constraint: %s", err) + } + ret.Return.Type = retTy + + if len(fn.Parameters) != 0 { + ret.Parameters = make([]*tfplugin6.Function_Parameter, len(fn.Parameters)) + for i, fnParam := range fn.Parameters { + protoParam, err := functionParamToProto(fnParam) + if err != nil { + return ret, fmt.Errorf("invalid parameter %d (%q): %s", i, fnParam.Name, err) + } + ret.Parameters[i] = protoParam + } + } + if fn.VariadicParameter != nil { + param, err := functionParamToProto(*fn.VariadicParameter) + if err != nil { + return ret, fmt.Errorf("invalid variadic parameter (%q): %s", fn.VariadicParameter.Name, err) + } + ret.VariadicParameter = param + } + + return ret, nil +} + +func functionParamToProto(param providers.FunctionParam) (*tfplugin6.Function_Parameter, error) { + ret := &tfplugin6.Function_Parameter{} + ret.Name = param.Name + ret.Description = param.Description + ret.DescriptionKind = protoStringKind(param.DescriptionKind) + ret.AllowNullValue = param.AllowNullValue + ret.AllowUnknownValues = param.AllowUnknownValues + ty, err := json.Marshal(param.Type) + if err != nil { + return ret, fmt.Errorf("invalid type constraint: %s", err) + } + ret.Type = ty + return ret, nil +} diff --git a/internal/plugin6/convert/functions_test.go b/internal/plugin6/convert/functions_test.go new file mode 100644 index 0000000000..0a9a9547ca --- /dev/null +++ b/internal/plugin6/convert/functions_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package convert + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/zclconf/go-cty/cty" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" +) + +func TestFunctionDeclsToFromProto(t *testing.T) { + fns := map[string]providers.FunctionDecl{ + "basic": providers.FunctionDecl{ + Parameters: []providers.FunctionParam{ + providers.FunctionParam{ + Name: "string", + Type: cty.String, + AllowNullValue: true, + AllowUnknownValues: true, + Description: "must be a string", + DescriptionKind: configschema.StringPlain, + }, + }, + ReturnType: cty.String, + Description: "returns a string", + DescriptionKind: configschema.StringPlain, + }, + "variadic": providers.FunctionDecl{ + VariadicParameter: &providers.FunctionParam{ + Name: "string", + Type: cty.String, + Description: "must be a string", + DescriptionKind: configschema.StringMarkdown, + }, + ReturnType: cty.String, + Description: "returns a string", + DescriptionKind: configschema.StringMarkdown, + }, + } + + protoFns, err := FunctionDeclsToProto(fns) + if err != nil { + t.Fatal(err) + } + + gotFns, err := FunctionDeclsFromProto(protoFns) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(fns, gotFns, ctydebug.CmpOptions); diff != "" { + t.Fatal(diff) + } +} diff --git a/internal/plugin6/convert/schema.go b/internal/plugin6/convert/schema.go index 0bdf4e2840..f1f47461d5 100644 --- a/internal/plugin6/convert/schema.go +++ b/internal/plugin6/convert/schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -32,6 +35,7 @@ func ConfigSchemaToProto(b *configschema.Block) *proto.Schema_Block { Required: a.Required, Sensitive: a.Sensitive, Deprecated: a.Deprecated, + WriteOnly: a.WriteOnly, } if a.Type != cty.NilType { @@ -92,11 +96,44 @@ func protoSchemaNestedBlock(name string, b *configschema.NestedBlock) *proto.Sch } // ProtoToProviderSchema takes a proto.Schema and converts it to a providers.Schema. -func ProtoToProviderSchema(s *proto.Schema) providers.Schema { - return providers.Schema{ +// It takes an optional resource identity schema for resources that support identity. +func ProtoToProviderSchema(s *proto.Schema, id *proto.ResourceIdentitySchema) providers.Schema { + schema := providers.Schema{ Version: s.Version, - Block: ProtoToConfigSchema(s.Block), + Body: ProtoToConfigSchema(s.Block), } + + if id != nil { + schema.IdentityVersion = id.Version + schema.Identity = ProtoToIdentitySchema(id.IdentityAttributes) + } + + return schema +} + +func ProtoToIdentitySchema(attributes []*proto.ResourceIdentitySchema_IdentityAttribute) *configschema.Object { + obj := &configschema.Object{ + Attributes: make(map[string]*configschema.Attribute), + Nesting: configschema.NestingSingle, + } + + for _, a := range attributes { + attr := &configschema.Attribute{ + Description: a.Description, + Required: a.RequiredForImport, + Optional: a.OptionalForImport, + } + + if a.Type != nil { + if err := json.Unmarshal(a.Type, &attr.Type); err != nil { + panic(err) + } + } + + obj.Attributes[a.Name] = attr + } + + return obj } // ProtoToConfigSchema takes the GetSchcema_Block from a grpc response and converts it @@ -120,6 +157,7 @@ func ProtoToConfigSchema(b *proto.Schema_Block) *configschema.Block { Computed: a.Computed, Sensitive: a.Sensitive, Deprecated: a.Deprecated, + WriteOnly: a.WriteOnly, } if a.Type != nil { @@ -210,6 +248,7 @@ func protoObjectToConfigSchema(b *proto.Schema_Object) *configschema.Object { Computed: a.Computed, Sensitive: a.Sensitive, Deprecated: a.Deprecated, + WriteOnly: a.WriteOnly, } if a.Type != nil { @@ -259,11 +298,10 @@ func configschemaObjectToProto(b *configschema.Object) *proto.Schema_Object { nesting = proto.Schema_Object_INVALID } - attributes := make([]*proto.Schema_Attribute, len(b.Attributes)) + attributes := make([]*proto.Schema_Attribute, 0, len(b.Attributes)) for _, name := range sortedKeys(b.Attributes) { a := b.Attributes[name] - attr := &proto.Schema_Attribute{ Name: name, Description: a.Description, @@ -273,6 +311,7 @@ func configschemaObjectToProto(b *configschema.Object) *proto.Schema_Object { Required: a.Required, Sensitive: a.Sensitive, Deprecated: a.Deprecated, + WriteOnly: a.WriteOnly, } if a.Type != cty.NilType { @@ -295,3 +334,32 @@ func configschemaObjectToProto(b *configschema.Object) *proto.Schema_Object { Nesting: nesting, } } + +func ResourceIdentitySchemaToProto(schema providers.IdentitySchema) *proto.ResourceIdentitySchema { + identityAttributes := []*proto.ResourceIdentitySchema_IdentityAttribute{} + + for _, name := range sortedKeys(schema.Body.Attributes) { + a := schema.Body.Attributes[name] + attr := &proto.ResourceIdentitySchema_IdentityAttribute{ + Name: name, + Description: a.Description, + RequiredForImport: a.Required, + OptionalForImport: a.Optional, + } + + if a.Type != cty.NilType { + ty, err := json.Marshal(a.Type) + if err != nil { + panic(err) + } + attr.Type = ty + } + + identityAttributes = append(identityAttributes, attr) + } + + return &proto.ResourceIdentitySchema{ + Version: schema.Version, + IdentityAttributes: identityAttributes, + } +} diff --git a/internal/plugin6/convert/schema_test.go b/internal/plugin6/convert/schema_test.go index 9befe4c5af..d7b8caba93 100644 --- a/internal/plugin6/convert/schema_test.go +++ b/internal/plugin6/convert/schema_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package convert import ( @@ -6,6 +9,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" proto "github.com/hashicorp/terraform/internal/tfplugin6" "github.com/zclconf/go-cty/cty" ) @@ -107,6 +111,12 @@ func TestConvertSchemaBlocks(t *testing.T) { Type: []byte(`"number"`), Required: true, }, + { + Name: "write_only", + Type: []byte(`"string"`), + Optional: true, + WriteOnly: true, + }, }, }, Computed: true, @@ -227,6 +237,11 @@ func TestConvertSchemaBlocks(t *testing.T) { Type: cty.Number, Required: true, }, + "write_only": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, }, }, Computed: true, @@ -407,6 +422,25 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { Type: []byte(`["list","bool"]`), Computed: true, }, + { + Name: "object", + NestedType: &proto.Schema_Object{ + Nesting: proto.Schema_Object_SINGLE, + Attributes: []*proto.Schema_Attribute{ + { + Name: "optional", + Type: []byte(`"string"`), + Optional: true, + }, + { + Name: "write_only", + Type: []byte(`"string"`), + Optional: true, + WriteOnly: true, + }, + }, + }, + }, { Name: "optional", Type: []byte(`"string"`), @@ -423,6 +457,12 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { Type: []byte(`"number"`), Required: true, }, + { + Name: "write_only", + Type: []byte(`"string"`), + Optional: true, + WriteOnly: true, + }, }, }, &configschema.Block{ @@ -431,6 +471,22 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { Type: cty.List(cty.Bool), Computed: true, }, + "object": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "optional": { + Type: cty.String, + Optional: true, + }, + "write_only": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, "optional": { Type: cty.String, Optional: true, @@ -444,6 +500,11 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { Type: cty.Number, Required: true, }, + "write_only": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, }, }, }, @@ -564,3 +625,90 @@ func TestConvertProtoSchemaBlocks(t *testing.T) { }) } } + +func TestProtoToResourceIdentitySchema(t *testing.T) { + tests := map[string]struct { + Attributes []*proto.ResourceIdentitySchema_IdentityAttribute + Want *configschema.Object + }{ + "simple": { + []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "id", + Type: []byte(`"string"`), + RequiredForImport: true, + OptionalForImport: false, + Description: "Something", + }, + }, + &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "Something", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ProtoToIdentitySchema(tc.Attributes) + if !cmp.Equal(converted, tc.Want, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, valueComparer, equateEmpty)) + } + }) + } +} + +func TestResourceIdentitySchemaToProto(t *testing.T) { + tests := map[string]struct { + Want *proto.ResourceIdentitySchema + Schema providers.IdentitySchema + }{ + "attributes": { + &proto.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "optional", + Type: []byte(`"string"`), + OptionalForImport: true, + }, + { + Name: "required", + Type: []byte(`"number"`), + RequiredForImport: true, + }, + }, + }, + providers.IdentitySchema{ + Version: 1, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "optional": { + Type: cty.String, + Optional: true, + }, + "required": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + converted := ResourceIdentitySchemaToProto(tc.Schema) + if !cmp.Equal(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported) { + t.Fatal(cmp.Diff(converted, tc.Want, typeComparer, equateEmpty, ignoreUnexported)) + } + }) + } +} diff --git a/internal/plugin6/doc.go b/internal/plugin6/doc.go index 5671893cc1..92d08c5044 100644 --- a/internal/plugin6/doc.go +++ b/internal/plugin6/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin6 // plugin6 builds on types in package plugin to include support for plugin diff --git a/internal/plugin6/grpc_error.go b/internal/plugin6/grpc_error.go index 717c1642bb..0c8dabcc86 100644 --- a/internal/plugin6/grpc_error.go +++ b/internal/plugin6/grpc_error.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin6 import ( diff --git a/internal/plugin6/grpc_provider.go b/internal/plugin6/grpc_provider.go index c6530e0754..5b7113f613 100644 --- a/internal/plugin6/grpc_provider.go +++ b/internal/plugin6/grpc_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin6 import ( @@ -9,13 +12,18 @@ import ( "github.com/zclconf/go-cty/cty" plugin "github.com/hashicorp/go-plugin" + "github.com/zclconf/go-cty/cty/function" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/logging" "github.com/hashicorp/terraform/internal/plugin6/convert" "github.com/hashicorp/terraform/internal/providers" proto6 "github.com/hashicorp/terraform/internal/tfplugin6" - ctyjson "github.com/zclconf/go-cty/cty/json" - "github.com/zclconf/go-cty/cty/msgpack" - "google.golang.org/grpc" ) var logger = logging.HCLogger() @@ -51,6 +59,11 @@ type GRPCProvider struct { // used in an end to end test of a provider. TestServer *grpc.Server + // Addr uniquely identifies the type of provider. + // Normally executed providers will have this set during initialization, + // but it may not always be available for alternative execute modes. + Addr addrs.Provider + // Proto client use to make the grpc service calls. client proto6.ProviderClient @@ -59,42 +72,35 @@ type GRPCProvider struct { ctx context.Context // schema stores the schema for this provider. This is used to properly - // serialize the state for requests. - mu sync.Mutex - schemas providers.GetProviderSchemaResponse + // serialize the requests for schemas. + mu sync.Mutex + schema providers.GetProviderSchemaResponse } -func New(client proto6.ProviderClient, ctx context.Context) GRPCProvider { - return GRPCProvider{ - client: client, - ctx: ctx, - } -} - -// getSchema is used internally to get the cached provider schema. -func (p *GRPCProvider) getSchema() providers.GetProviderSchemaResponse { - p.mu.Lock() - // unlock inline in case GetProviderSchema needs to be called - if p.schemas.Provider.Block != nil { - p.mu.Unlock() - return p.schemas - } - p.mu.Unlock() - - return p.GetProviderSchema() -} - -func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResponse) { - logger.Trace("GRPCProvider.v6: GetProviderSchema") +func (p *GRPCProvider) GetProviderSchema() providers.GetProviderSchemaResponse { p.mu.Lock() defer p.mu.Unlock() - if p.schemas.Provider.Block != nil { - return p.schemas + // check the global cache if we can + if !p.Addr.IsZero() { + if resp, ok := providers.SchemaCache.Get(p.Addr); ok && resp.ServerCapabilities.GetProviderSchemaOptional { + logger.Trace("GRPCProvider.v6: returning cached schema", p.Addr.String()) + return resp + } } + logger.Trace("GRPCProvider.v6: GetProviderSchema") + + // If the local cache is non-zero, we know this instance has called + // GetProviderSchema at least once and we can return early. + if p.schema.Provider.Body != nil { + return p.schema + } + + var resp providers.GetProviderSchemaResponse resp.ResourceTypes = make(map[string]providers.Schema) resp.DataSources = make(map[string]providers.Schema) + resp.EphemeralResourceTypes = make(map[string]providers.Schema) // Some providers may generate quite large schemas, and the internal default // grpc response size limit is 4MB. 64MB should cover most any use case, and @@ -122,26 +128,95 @@ func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResp return resp } - resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider) + identResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto6.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We don't treat this as an error if older providers don't implement this method, + // so we create an empty map for identity schemas + identResp = &proto6.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto6.ResourceIdentitySchema{}, + } + } else { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + } + + resp.Provider = convert.ProtoToProviderSchema(protoResp.Provider, nil) if protoResp.ProviderMeta == nil { logger.Debug("No provider meta schema returned") } else { - resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta) + resp.ProviderMeta = convert.ProtoToProviderSchema(protoResp.ProviderMeta, nil) } for name, res := range protoResp.ResourceSchemas { - resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res) + id := identResp.IdentitySchemas[name] // We're fine if the id is not found + resp.ResourceTypes[name] = convert.ProtoToProviderSchema(res, id) } for name, data := range protoResp.DataSourceSchemas { - resp.DataSources[name] = convert.ProtoToProviderSchema(data) + resp.DataSources[name] = convert.ProtoToProviderSchema(data, nil) + } + + for name, ephem := range protoResp.EphemeralResourceSchemas { + resp.EphemeralResourceTypes[name] = convert.ProtoToProviderSchema(ephem, nil) + } + + if decls, err := convert.FunctionDeclsFromProto(protoResp.Functions); err == nil { + resp.Functions = decls + } else { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp } if protoResp.ServerCapabilities != nil { resp.ServerCapabilities.PlanDestroy = protoResp.ServerCapabilities.PlanDestroy + resp.ServerCapabilities.GetProviderSchemaOptional = protoResp.ServerCapabilities.GetProviderSchemaOptional + resp.ServerCapabilities.MoveResourceState = protoResp.ServerCapabilities.MoveResourceState } - p.schemas = resp + // set the global cache if we can + if !p.Addr.IsZero() { + providers.SchemaCache.Set(p.Addr, resp) + } + + // always store this here in the client for providers that are not able to + // use GetProviderSchemaOptional + p.schema = resp + + return resp +} + +func (p *GRPCProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + logger.Trace("GRPCProvider.v6: GetResourceIdentitySchemas") + + var resp providers.GetResourceIdentitySchemasResponse + + resp.IdentityTypes = make(map[string]providers.IdentitySchema) + + protoResp, err := p.client.GetResourceIdentitySchemas(p.ctx, new(proto6.GetResourceIdentitySchemas_Request)) + if err != nil { + if status.Code(err) == codes.Unimplemented { + // We expect no error here if older providers don't implement this method + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + if resp.Diagnostics.HasErrors() { + return resp + } + + for name, res := range protoResp.IdentitySchemas { + resp.IdentityTypes[name] = providers.IdentitySchema{ + Version: res.Version, + Body: convert.ProtoToIdentitySchema(res.IdentityAttributes), + } + } return resp } @@ -149,13 +224,13 @@ func (p *GRPCProvider) GetProviderSchema() (resp providers.GetProviderSchemaResp func (p *GRPCProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { logger.Trace("GRPCProvider.v6: ValidateProviderConfig") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp } - ty := schema.Provider.Block.ImpliedType() + ty := schema.Provider.Body.ImpliedType() mp, err := msgpack.Marshal(r.Config, ty) if err != nil { @@ -180,7 +255,7 @@ func (p *GRPCProvider) ValidateProviderConfig(r providers.ValidateProviderConfig func (p *GRPCProvider) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) (resp providers.ValidateResourceConfigResponse) { logger.Trace("GRPCProvider.v6: ValidateResourceConfig") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -192,15 +267,16 @@ func (p *GRPCProvider) ValidateResourceConfig(r providers.ValidateResourceConfig return resp } - mp, err := msgpack.Marshal(r.Config, resourceSchema.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.Config, resourceSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } protoReq := &proto6.ValidateResourceConfig_Request{ - TypeName: r.TypeName, - Config: &proto6.DynamicValue{Msgpack: mp}, + TypeName: r.TypeName, + Config: &proto6.DynamicValue{Msgpack: mp}, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } protoResp, err := p.client.ValidateResourceConfig(p.ctx, protoReq) @@ -216,7 +292,7 @@ func (p *GRPCProvider) ValidateResourceConfig(r providers.ValidateResourceConfig func (p *GRPCProvider) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) (resp providers.ValidateDataResourceConfigResponse) { logger.Trace("GRPCProvider.v6: ValidateDataResourceConfig") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -228,7 +304,7 @@ func (p *GRPCProvider) ValidateDataResourceConfig(r providers.ValidateDataResour return resp } - mp, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.Config, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -251,7 +327,7 @@ func (p *GRPCProvider) ValidateDataResourceConfig(r providers.ValidateDataResour func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { logger.Trace("GRPCProvider.v6: UpgradeResourceState") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -279,7 +355,7 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - ty := resSchema.Block.ImpliedType() + ty := resSchema.Body.ImpliedType() resp.UpgradedState = cty.NullVal(ty) if protoResp.UpgradedState == nil { return resp @@ -295,15 +371,61 @@ func (p *GRPCProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } +func (p *GRPCProvider) UpgradeResourceIdentity(r providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + logger.Trace("GRPCProvider.v6: UpgradeResourceIdentity") + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + resSchema, ok := schema.ResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource identity type %q", r.TypeName)) + return resp + } + + protoReq := &proto6.UpgradeResourceIdentity_Request{ + TypeName: r.TypeName, + Version: int64(r.Version), + RawIdentity: &proto6.RawState{ + Json: r.RawIdentityJSON, + }, + } + + protoResp, err := p.client.UpgradeResourceIdentity(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + ty := resSchema.Identity.ImpliedType() + resp.UpgradedIdentity = cty.NullVal(ty) + if protoResp.UpgradedIdentity == nil { + return resp + } + + identity, err := decodeDynamicValue(protoResp.UpgradedIdentity.IdentityData, ty) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.UpgradedIdentity = identity + + return resp +} + func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { logger.Trace("GRPCProvider.v6: ConfigureProvider") - schema := p.getSchema() + schema := p.GetProviderSchema() var mp []byte // we don't have anything to marshal if there's no config - mp, err := msgpack.Marshal(r.Config, schema.Provider.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.Config, schema.Provider.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -314,6 +436,7 @@ func (p *GRPCProvider) ConfigureProvider(r providers.ConfigureProviderRequest) ( Config: &proto6.DynamicValue{ Msgpack: mp, }, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } protoResp, err := p.client.ConfigureProvider(p.ctx, protoReq) @@ -342,7 +465,7 @@ func (p *GRPCProvider) Stop() error { func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { logger.Trace("GRPCProvider.v6: ReadResource") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -350,26 +473,27 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi resSchema, ok := schema.ResourceTypes[r.TypeName] if !ok { - resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type " + r.TypeName)) + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %s", r.TypeName)) return resp } metaSchema := schema.ProviderMeta - mp, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType()) + mp, err := msgpack.Marshal(r.PriorState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } protoReq := &proto6.ReadResource_Request{ - TypeName: r.TypeName, - CurrentState: &proto6.DynamicValue{Msgpack: mp}, - Private: r.Private, + TypeName: r.TypeName, + CurrentState: &proto6.DynamicValue{Msgpack: mp}, + Private: r.Private, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -377,6 +501,21 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP} } + if !r.CurrentIdentity.IsNull() { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %s", r.TypeName)) + return resp + } + currentIdentityMP, err := msgpack.Marshal(r.CurrentIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + protoReq.CurrentIdentity = &proto6.ResourceIdentityData{ + IdentityData: &proto6.DynamicValue{Msgpack: currentIdentityMP}, + } + } + protoResp, err := p.client.ReadResource(p.ctx, protoReq) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) @@ -384,13 +523,26 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.NewState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } resp.NewState = state resp.Private = protoResp.Private + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) + + if protoResp.NewIdentity != nil && protoResp.NewIdentity.IdentityData != nil { + + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %q", r.TypeName)) + } + + resp.Identity, err = decodeDynamicValue(protoResp.NewIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + } + } return resp } @@ -398,7 +550,7 @@ func (p *GRPCProvider) ReadResource(r providers.ReadResourceRequest) (resp provi func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { logger.Trace("GRPCProvider.v6: PlanResourceChange") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -421,34 +573,40 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) return resp } - priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType()) + priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType()) + configMP, err := msgpack.Marshal(r.Config, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Block.ImpliedType()) + propMP, err := msgpack.Marshal(r.ProposedNewState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } protoReq := &proto6.PlanResourceChange_Request{ - TypeName: r.TypeName, - PriorState: &proto6.DynamicValue{Msgpack: priorMP}, - Config: &proto6.DynamicValue{Msgpack: configMP}, - ProposedNewState: &proto6.DynamicValue{Msgpack: propMP}, - PriorPrivate: r.PriorPrivate, + TypeName: r.TypeName, + PriorState: &proto6.DynamicValue{Msgpack: priorMP}, + Config: &proto6.DynamicValue{Msgpack: configMP}, + ProposedNewState: &proto6.DynamicValue{Msgpack: propMP}, + PriorPrivate: r.PriorPrivate, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaTy := metaSchema.Body.ImpliedType() + metaVal := r.ProviderMeta + if metaVal == cty.NilVal { + metaVal = cty.NullVal(metaTy) + } + metaMP, err := msgpack.Marshal(metaVal, metaTy) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -456,6 +614,21 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP} } + if !r.PriorIdentity.IsNull() { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %q", r.TypeName)) + return resp + } + priorIdentityMP, err := msgpack.Marshal(r.PriorIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + protoReq.PriorIdentity = &proto6.ResourceIdentityData{ + IdentityData: &proto6.DynamicValue{Msgpack: priorIdentityMP}, + } + } + protoResp, err := p.client.PlanResourceChange(p.ctx, protoReq) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) @@ -463,7 +636,7 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - state, err := decodeDynamicValue(protoResp.PlannedState, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.PlannedState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -478,13 +651,28 @@ func (p *GRPCProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) resp.LegacyTypeSystem = protoResp.LegacyTypeSystem + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) + + if protoResp.PlannedIdentity != nil && protoResp.PlannedIdentity.IdentityData != nil { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %s", r.TypeName)) + return resp + } + + resp.PlannedIdentity, err = decodeDynamicValue(protoResp.PlannedIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + } + return resp } func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { logger.Trace("GRPCProvider.v6: ApplyResourceChange") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -498,17 +686,17 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques metaSchema := schema.ProviderMeta - priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Block.ImpliedType()) + priorMP, err := msgpack.Marshal(r.PriorState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - plannedMP, err := msgpack.Marshal(r.PlannedState, resSchema.Block.ImpliedType()) + plannedMP, err := msgpack.Marshal(r.PlannedState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - configMP, err := msgpack.Marshal(r.Config, resSchema.Block.ImpliedType()) + configMP, err := msgpack.Marshal(r.Config, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -522,8 +710,13 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques PlannedPrivate: r.PlannedPrivate, } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaTy := metaSchema.Body.ImpliedType() + metaVal := r.ProviderMeta + if metaVal == cty.NilVal { + metaVal = cty.NullVal(metaTy) + } + metaMP, err := msgpack.Marshal(metaVal, metaTy) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -531,6 +724,21 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques protoReq.ProviderMeta = &proto6.DynamicValue{Msgpack: metaMP} } + if !r.PlannedIdentity.IsNull() { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %s", r.TypeName)) + return resp + } + identityMP, err := msgpack.Marshal(r.PlannedIdentity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + protoReq.PlannedIdentity = &proto6.ResourceIdentityData{ + IdentityData: &proto6.DynamicValue{Msgpack: identityMP}, + } + } + protoResp, err := p.client.ApplyResourceChange(p.ctx, protoReq) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) @@ -540,7 +748,7 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques resp.Private = protoResp.Private - state, err := decodeDynamicValue(protoResp.NewState, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.NewState, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -549,21 +757,55 @@ func (p *GRPCProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques resp.LegacyTypeSystem = protoResp.LegacyTypeSystem + if protoResp.NewIdentity != nil && protoResp.NewIdentity.IdentityData != nil { + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("identity type not found for resource type %s", r.TypeName)) + return resp + } + newIdentity, err := decodeDynamicValue(protoResp.NewIdentity.IdentityData, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.NewIdentity = newIdentity + } + return resp } func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateRequest) (resp providers.ImportResourceStateResponse) { logger.Trace("GRPCProvider.v6: ImportResourceState") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp } protoReq := &proto6.ImportResourceState_Request{ - TypeName: r.TypeName, - Id: r.ID, + TypeName: r.TypeName, + Id: r.ID, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), + } + + if !r.Identity.IsNull() { + resSchema := schema.ResourceTypes[r.TypeName] + if resSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %q", r.TypeName)) + return resp + } + + mp, err := msgpack.Marshal(r.Identity, resSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + protoReq.Identity = &proto6.ResourceIdentityData{ + IdentityData: &proto6.DynamicValue{ + Msgpack: mp, + }, + } } protoResp, err := p.client.ImportResourceState(p.ctx, protoReq) @@ -572,6 +814,7 @@ func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateReques return resp } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) for _, imported := range protoResp.ImportedResources { resource := providers.ImportedResource{ @@ -585,22 +828,108 @@ func (p *GRPCProvider) ImportResourceState(r providers.ImportResourceStateReques continue } - state, err := decodeDynamicValue(imported.State, resSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(imported.State, resSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } resource.State = state + + if imported.Identity != nil && imported.Identity.IdentityData != nil { + importedIdentitySchema, ok := schema.ResourceTypes[imported.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %q", imported.TypeName)) + continue + } + importedIdentity, err := decodeDynamicValue(imported.Identity.IdentityData, importedIdentitySchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resource.Identity = importedIdentity + } + resp.ImportedResources = append(resp.ImportedResources, resource) } return resp } +func (p *GRPCProvider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { + logger.Trace("GRPCProvider: MoveResourceState") + + var sourceIdentity *proto6.RawState + if len(r.SourceIdentity) > 0 { + sourceIdentity = &proto6.RawState{ + Json: r.SourceIdentity, + } + } + + protoReq := &proto6.MoveResourceState_Request{ + SourceProviderAddress: r.SourceProviderAddress, + SourceTypeName: r.SourceTypeName, + SourceSchemaVersion: r.SourceSchemaVersion, + SourceState: &proto6.RawState{ + Json: r.SourceStateJSON, + }, + SourcePrivate: r.SourcePrivate, + SourceIdentity: sourceIdentity, + TargetTypeName: r.TargetTypeName, + } + + protoResp, err := p.client.MoveResourceState(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + if resp.Diagnostics.HasErrors() { + return resp + } + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + targetType, ok := schema.ResourceTypes[r.TargetTypeName] + if !ok { + // We should have validated this earlier in the process, but we'll + // still return an error instead of crashing in case something went + // wrong. + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown resource type %q; this is a bug in Terraform - please report it", r.TargetTypeName)) + return resp + } + resp.TargetState, err = decodeDynamicValue(protoResp.TargetState, targetType.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + resp.TargetPrivate = protoResp.TargetPrivate + + if protoResp.TargetIdentity != nil && protoResp.TargetIdentity.IdentityData != nil { + targetResSchema := schema.ResourceTypes[r.TargetTypeName] + + if targetResSchema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown identity type %s", r.TargetTypeName)) + return resp + } + resp.TargetIdentity, err = decodeDynamicValue(protoResp.TargetIdentity.IdentityData, targetResSchema.Identity.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + } + + return resp +} + func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { logger.Trace("GRPCProvider.v6: ReadDataSource") - schema := p.getSchema() + schema := p.GetProviderSchema() if schema.Diagnostics.HasErrors() { resp.Diagnostics = schema.Diagnostics return resp @@ -613,7 +942,7 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p metaSchema := schema.ProviderMeta - config, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType()) + config, err := msgpack.Marshal(r.Config, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -624,10 +953,11 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p Config: &proto6.DynamicValue{ Msgpack: config, }, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), } - if metaSchema.Block != nil { - metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Block.ImpliedType()) + if metaSchema.Body != nil { + metaMP, err := msgpack.Marshal(r.ProviderMeta, metaSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -642,16 +972,230 @@ func (p *GRPCProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p } resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) - state, err := decodeDynamicValue(protoResp.State, dataSchema.Block.ImpliedType()) + state, err := decodeDynamicValue(protoResp.State, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } resp.State = state + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) return resp } +func (p *GRPCProvider) ValidateEphemeralResourceConfig(r providers.ValidateEphemeralResourceConfigRequest) (resp providers.ValidateEphemeralResourceConfigResponse) { + logger.Trace("GRPCProvider.v6: ValidateEphemeralResourceConfig") + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + ephemSchema, ok := schema.EphemeralResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown ephemeral resource %q", r.TypeName)) + return resp + } + + mp, err := msgpack.Marshal(r.Config, ephemSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + protoReq := &proto6.ValidateEphemeralResourceConfig_Request{ + TypeName: r.TypeName, + Config: &proto6.DynamicValue{Msgpack: mp}, + } + + protoResp, err := p.client.ValidateEphemeralResourceConfig(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + return resp +} + +func (p *GRPCProvider) OpenEphemeralResource(r providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + logger.Trace("GRPCProvide.v6: OpenEphemeralResource") + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Diagnostics = schema.Diagnostics + return resp + } + + ephemSchema, ok := schema.EphemeralResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("unknown ephemeral resource %q", r.TypeName)) + return resp + } + + config, err := msgpack.Marshal(r.Config, ephemSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + protoReq := &proto6.OpenEphemeralResource_Request{ + TypeName: r.TypeName, + Config: &proto6.DynamicValue{ + Msgpack: config, + }, + ClientCapabilities: clientCapabilitiesToProto(r.ClientCapabilities), + } + + protoResp, err := p.client.OpenEphemeralResource(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + state, err := decodeDynamicValue(protoResp.Result, ephemSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + if protoResp.RenewAt != nil { + resp.RenewAt = protoResp.RenewAt.AsTime() + } + + resp.Result = state + resp.Private = protoResp.Private + resp.Deferred = convert.ProtoToDeferred(protoResp.Deferred) + + return resp +} + +func (p *GRPCProvider) RenewEphemeralResource(r providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) { + logger.Trace("GRPCProvider.v6: RenewEphemeralResource") + + protoReq := &proto6.RenewEphemeralResource_Request{ + TypeName: r.TypeName, + Private: r.Private, + } + + protoResp, err := p.client.RenewEphemeralResource(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + if protoResp.RenewAt != nil { + resp.RenewAt = protoResp.RenewAt.AsTime() + } + + resp.Private = protoResp.Private + + return resp +} + +func (p *GRPCProvider) CloseEphemeralResource(r providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { + logger.Trace("GRPCProvider.v6: CloseEphemeralResource") + + protoReq := &proto6.CloseEphemeralResource_Request{ + TypeName: r.TypeName, + Private: r.Private, + } + + protoResp, err := p.client.CloseEphemeralResource(p.ctx, protoReq) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(grpcErr(err)) + return resp + } + resp.Diagnostics = resp.Diagnostics.Append(convert.ProtoToDiagnostics(protoResp.Diagnostics)) + + return resp +} + +func (p *GRPCProvider) CallFunction(r providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + logger.Trace("GRPCProvider.v6", "CallFunction", r.FunctionName) + + schema := p.GetProviderSchema() + if schema.Diagnostics.HasErrors() { + resp.Err = schema.Diagnostics.Err() + return resp + } + + funcDecl, ok := schema.Functions[r.FunctionName] + // We check for various problems with the request below in the interests + // of robustness, just to avoid crashing while trying to encode/decode, but + // if we reach any of these errors then that suggests a bug in the caller, + // because we should catch function calls that don't match the schema at an + // earlier point than this. + if !ok { + // Should only get here if the caller has a bug, because we should + // have detected earlier any attempt to call a function that the + // provider didn't declare. + resp.Err = fmt.Errorf("provider has no function named %q", r.FunctionName) + return resp + } + if len(r.Arguments) < len(funcDecl.Parameters) { + resp.Err = fmt.Errorf("not enough arguments for function %q", r.FunctionName) + return resp + } + if funcDecl.VariadicParameter == nil && len(r.Arguments) > len(funcDecl.Parameters) { + resp.Err = fmt.Errorf("too many arguments for function %q", r.FunctionName) + return resp + } + args := make([]*proto6.DynamicValue, len(r.Arguments)) + for i, argVal := range r.Arguments { + var paramDecl providers.FunctionParam + if i < len(funcDecl.Parameters) { + paramDecl = funcDecl.Parameters[i] + } else { + paramDecl = *funcDecl.VariadicParameter + } + + argValRaw, err := msgpack.Marshal(argVal, paramDecl.Type) + if err != nil { + resp.Err = err + return resp + } + args[i] = &proto6.DynamicValue{ + Msgpack: argValRaw, + } + } + + protoResp, err := p.client.CallFunction(p.ctx, &proto6.CallFunction_Request{ + Name: r.FunctionName, + Arguments: args, + }) + if err != nil { + // functions can only support simple errors, but use our grpcError + // diagnostic function to format common problems is a more + // user-friendly manner. + resp.Err = grpcErr(err).Err() + return resp + } + + if protoResp.Error != nil { + resp.Err = errors.New(protoResp.Error.Text) + + // If this is a problem with a specific argument, we can wrap the error + // in a function.ArgError + if protoResp.Error.FunctionArgument != nil { + resp.Err = function.NewArgError(int(*protoResp.Error.FunctionArgument), resp.Err) + } + + return resp + } + + resultVal, err := decodeDynamicValue(protoResp.Result, funcDecl.ReturnType) + if err != nil { + resp.Err = err + return resp + } + + resp.Result = resultVal + return resp +} + // closing the grpc connection is final, and terraform will call it at the end of every phase. func (p *GRPCProvider) Close() error { logger.Trace("GRPCProvider.v6: Close") @@ -691,3 +1235,10 @@ func decodeDynamicValue(v *proto6.DynamicValue, ty cty.Type) (cty.Value, error) } return res, err } + +func clientCapabilitiesToProto(c providers.ClientCapabilities) *proto6.ClientCapabilities { + return &proto6.ClientCapabilities{ + DeferralAllowed: c.DeferralAllowed, + WriteOnlyAttributesAllowed: c.WriteOnlyAttributesAllowed, + } +} diff --git a/internal/plugin6/grpc_provider_test.go b/internal/plugin6/grpc_provider_test.go index 300a09b4ad..0c86d7956d 100644 --- a/internal/plugin6/grpc_provider_test.go +++ b/internal/plugin6/grpc_provider_test.go @@ -1,17 +1,25 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin6 import ( "bytes" "fmt" "testing" + "time" - "github.com/golang/mock/gomock" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + "go.uber.org/mock/gomock" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" mockproto "github.com/hashicorp/terraform/internal/plugin6/mock_proto" proto "github.com/hashicorp/terraform/internal/tfplugin6" @@ -36,6 +44,13 @@ func mockProviderClient(t *testing.T) *mockproto.MockProviderClient { gomock.Any(), ).Return(providerProtoSchema(), nil) + // GetResourceIdentitySchemas is called as part of GetSchema + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + return client } @@ -96,6 +111,39 @@ func providerProtoSchema() *proto.GetProviderSchema_Response { }, }, }, + EphemeralResourceSchemas: map[string]*proto.Schema{ + "ephemeral": &proto.Schema{ + Block: &proto.Schema_Block{ + Attributes: []*proto.Schema_Attribute{ + { + Name: "attr", + Type: []byte(`"string"`), + Computed: true, + }, + }, + }, + }, + }, + ServerCapabilities: &proto.ServerCapabilities{ + GetProviderSchemaOptional: true, + }, + } +} + +func providerResourceIdentitySchemas() *proto.GetResourceIdentitySchemas_Response { + return &proto.GetResourceIdentitySchemas_Response{ + IdentitySchemas: map[string]*proto.ResourceIdentitySchema{ + "resource": { + Version: 1, + IdentityAttributes: []*proto.ResourceIdentitySchema_IdentityAttribute{ + { + Name: "id_attr", + Type: []byte(`"string"`), + RequiredForImport: true, + }, + }, + }, + }, } } @@ -108,6 +156,27 @@ func TestGRPCProvider_GetSchema(t *testing.T) { checkDiags(t, resp.Diagnostics) } +// ensure that the global schema cache is used when the provider supports +// GetProviderSchemaOptional +func TestGRPCProvider_GetSchema_globalCache(t *testing.T) { + p := &GRPCProvider{ + Addr: addrs.ImpliedProviderForUnqualifiedType("test"), + client: mockProviderClient(t), + } + + // first call primes the cache + resp := p.GetProviderSchema() + + // create a new provider instance which does not expect a GetProviderSchemaCall + p = &GRPCProvider{ + Addr: addrs.ImpliedProviderForUnqualifiedType("test"), + client: mockproto.NewMockProviderClient(gomock.NewController(t)), + } + + resp = p.GetProviderSchema() + checkDiags(t, resp.Diagnostics) +} + // Ensure that gRPC errors are returned early. // Reference: https://github.com/hashicorp/terraform/issues/31047 func TestGRPCProvider_GetSchema_GRPCError(t *testing.T) { @@ -160,6 +229,94 @@ func TestGRPCProvider_GetSchema_ResponseErrorDiagnostic(t *testing.T) { checkDiagsHasError(t, resp.Diagnostics) } +func TestGRPCProvider_GetSchema_IdentityError(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetProviderSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, fmt.Errorf("test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiagsHasError(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetSchema_IdentityUnimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetProviderSchema( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerProtoSchema(), nil) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetProviderSchema() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(providerResourceIdentitySchemas(), nil) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + +func TestGRPCProvider_GetResourceIdentitySchemas_Unimplemented(t *testing.T) { + ctrl := gomock.NewController(t) + client := mockproto.NewMockProviderClient(ctrl) + + client.EXPECT().GetResourceIdentitySchemas( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(&proto.GetResourceIdentitySchemas_Response{}, status.Error(codes.Unimplemented, "test error")) + + p := &GRPCProvider{ + client: client, + } + + resp := p.GetResourceIdentitySchemas() + + checkDiags(t, resp.Diagnostics) +} + func TestGRPCProvider_PrepareProviderConfig(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -276,6 +433,84 @@ func TestGRPCProvider_UpgradeResourceStateJSON(t *testing.T) { } } +func TestGRPCProvider_UpgradeResourceIdentity(t *testing.T) { + testCases := []struct { + desc string + response *proto.UpgradeResourceIdentity_Response + expectError bool + expectedValue cty.Value + }{ + { + "successful upgrade", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"id_attr":"bar"}`), + }, + }, + }, + false, + cty.ObjectVal(map[string]cty.Value{"id_attr": cty.StringVal("bar")}), + }, + { + "response with error diagnostic", + &proto.UpgradeResourceIdentity_Response{ + Diagnostics: []*proto.Diagnostic{ + { + Severity: proto.Diagnostic_ERROR, + Summary: "test error", + Detail: "test error detail", + }, + }, + }, + true, + cty.NilVal, + }, + { + "schema mismatch", + &proto.UpgradeResourceIdentity_Response{ + UpgradedIdentity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Json: []byte(`{"attr_new":"bar"}`), + }, + }, + }, + true, + cty.NilVal, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().UpgradeResourceIdentity( + gomock.Any(), + gomock.Any(), + ).Return(tc.response, nil) + + resp := p.UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest{ + TypeName: "resource", + Version: 0, + RawIdentityJSON: []byte(`{"old_attr":"bar"}`), + }) + + if tc.expectError { + checkDiagsHasError(t, resp.Diagnostics) + } else { + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(tc.expectedValue, resp.UpgradedIdentity, typeComparer, valueComparer, equateEmpty)) + } + } + }) + } +} + func TestGRPCProvider_Configure(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -346,6 +581,41 @@ func TestGRPCProvider_ReadResource(t *testing.T) { } } +func TestGRPCProvider_ReadResource_deferred(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ReadResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ReadResource_Response{ + NewState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + Deferred: &proto.Deferred{ + Reason: proto.Deferred_ABSENT_PREREQ, + }, + }, nil) + + resp := p.ReadResource(providers.ReadResourceRequest{ + TypeName: "resource", + PriorState: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expectedDeferred := &providers.Deferred{ + Reason: providers.DeferredReasonAbsentPrereq, + } + if !cmp.Equal(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedDeferred, resp.Deferred, typeComparer, valueComparer, equateEmpty)) + } +} + func TestGRPCProvider_ReadResourceJSON(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -673,6 +943,7 @@ func TestGRPCProvider_ImportResourceState(t *testing.T) { t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty)) } } + func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -717,6 +988,132 @@ func TestGRPCProvider_ImportResourceStateJSON(t *testing.T) { } } +func TestGRPCProvider_ImportResourceState_Identity(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().ImportResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.ImportResourceState_Response{ + ImportedResources: []*proto.ImportResourceState_ImportedResource{ + { + TypeName: "resource", + State: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + Identity: &proto.ResourceIdentityData{ + IdentityData: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa7id_attr\xa3foo"), + }, + }, + }, + }, + }, nil) + + resp := p.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: "resource", + Identity: cty.ObjectVal(map[string]cty.Value{ + "id_attr": cty.StringVal("foo"), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expectedResource := providers.ImportedResource{ + TypeName: "resource", + State: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }), + Identity: cty.ObjectVal(map[string]cty.Value{ + "id_attr": cty.StringVal("foo"), + }), + } + + imported := resp.ImportedResources[0] + if !cmp.Equal(expectedResource, imported, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedResource, imported, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_MoveResourceState(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedTargetPrivate := []byte(`{"target": "private"}`) + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + client.EXPECT().MoveResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.MoveResourceState_Response{ + TargetState: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + TargetPrivate: expectedTargetPrivate, + }, nil) + + resp := p.MoveResourceState(providers.MoveResourceStateRequest{ + SourcePrivate: []byte(`{"source": "private"}`), + SourceStateJSON: []byte(`{"source_attr":"bar"}`), + TargetTypeName: "resource", + }) + + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty)) + } + + if !cmp.Equal(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty)) + } +} + +func TestGRPCProvider_MoveResourceStateJSON(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + expectedTargetPrivate := []byte(`{"target": "private"}`) + expectedTargetState := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + client.EXPECT().MoveResourceState( + gomock.Any(), + gomock.Any(), + ).Return(&proto.MoveResourceState_Response{ + TargetState: &proto.DynamicValue{ + Json: []byte(`{"attr":"bar"}`), + }, + TargetPrivate: expectedTargetPrivate, + }, nil) + + resp := p.MoveResourceState(providers.MoveResourceStateRequest{ + SourcePrivate: []byte(`{"source": "private"}`), + SourceStateJSON: []byte(`{"source_attr":"bar"}`), + TargetTypeName: "resource", + }) + + checkDiags(t, resp.Diagnostics) + + if !cmp.Equal(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetPrivate, resp.TargetPrivate, typeComparer, valueComparer, equateEmpty)) + } + + if !cmp.Equal(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expectedTargetState, resp.TargetState, typeComparer, valueComparer, equateEmpty)) + } +} + func TestGRPCProvider_ReadDataSource(t *testing.T) { client := mockProviderClient(t) p := &GRPCProvider{ @@ -782,3 +1179,95 @@ func TestGRPCProvider_ReadDataSourceJSON(t *testing.T) { t.Fatal(cmp.Diff(expected, resp.State, typeComparer, valueComparer, equateEmpty)) } } + +func TestGRPCProvider_openEphemeralResource(t *testing.T) { + client := mockProviderClient(t) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().OpenEphemeralResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.OpenEphemeralResource_Response{ + Result: &proto.DynamicValue{ + Msgpack: []byte("\x81\xa4attr\xa3bar"), + }, + RenewAt: timestamppb.New(time.Now().Add(time.Second)), + Private: []byte("private data"), + }, nil) + + resp := p.OpenEphemeralResource(providers.OpenEphemeralResourceRequest{ + TypeName: "ephemeral", + Config: cty.ObjectVal(map[string]cty.Value{ + "attr": cty.NullVal(cty.String), + }), + }) + + checkDiags(t, resp.Diagnostics) + + expected := cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar"), + }) + + if !cmp.Equal(expected, resp.Result, typeComparer, valueComparer, equateEmpty) { + t.Fatal(cmp.Diff(expected, resp.Result, typeComparer, valueComparer, equateEmpty)) + } + + if !resp.RenewAt.After(time.Now()) { + t.Fatal("invalid RenewAt:", resp.RenewAt) + } + + if !bytes.Equal(resp.Private, []byte("private data")) { + t.Fatalf("invalid private data: %q", resp.Private) + } +} + +func TestGRPCProvider_renewEphemeralResource(t *testing.T) { + client := mockproto.NewMockProviderClient(gomock.NewController(t)) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().RenewEphemeralResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.RenewEphemeralResource_Response{ + RenewAt: timestamppb.New(time.Now().Add(time.Second)), + Private: []byte("private data"), + }, nil) + + resp := p.RenewEphemeralResource(providers.RenewEphemeralResourceRequest{ + TypeName: "ephemeral", + Private: []byte("private data"), + }) + + checkDiags(t, resp.Diagnostics) + + if !resp.RenewAt.After(time.Now()) { + t.Fatal("invalid RenewAt:", resp.RenewAt) + } + + if !bytes.Equal(resp.Private, []byte("private data")) { + t.Fatalf("invalid private data: %q", resp.Private) + } +} + +func TestGRPCProvider_closeEphemeralResource(t *testing.T) { + client := mockproto.NewMockProviderClient(gomock.NewController(t)) + p := &GRPCProvider{ + client: client, + } + + client.EXPECT().CloseEphemeralResource( + gomock.Any(), + gomock.Any(), + ).Return(&proto.CloseEphemeralResource_Response{}, nil) + + resp := p.CloseEphemeralResource(providers.CloseEphemeralResourceRequest{ + TypeName: "ephemeral", + Private: []byte("private data"), + }) + + checkDiags(t, resp.Diagnostics) +} diff --git a/internal/plugin6/mock_proto/generate.go b/internal/plugin6/mock_proto/generate.go index cde637e4b2..2693a8448c 100644 --- a/internal/plugin6/mock_proto/generate.go +++ b/internal/plugin6/mock_proto/generate.go @@ -1,3 +1,6 @@ -//go:generate go run github.com/golang/mock/mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin6 ProviderClient +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +//go:generate go tool go.uber.org/mock/mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin6 ProviderClient package mock_tfplugin6 diff --git a/internal/plugin6/mock_proto/mock.go b/internal/plugin6/mock_proto/mock.go index 448008ef75..50d3e558a9 100644 --- a/internal/plugin6/mock_proto/mock.go +++ b/internal/plugin6/mock_proto/mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/hashicorp/terraform/internal/tfplugin6 (interfaces: ProviderClient) +// +// Generated by this command: +// +// mockgen -destination mock.go github.com/hashicorp/terraform/internal/tfplugin6 ProviderClient +// // Package mock_tfplugin6 is a generated GoMock package. package mock_tfplugin6 @@ -8,8 +13,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" tfplugin6 "github.com/hashicorp/terraform/internal/tfplugin6" + gomock "go.uber.org/mock/gomock" grpc "google.golang.org/grpc" ) @@ -39,7 +44,7 @@ func (m *MockProviderClient) EXPECT() *MockProviderClientMockRecorder { // ApplyResourceChange mocks base method. func (m *MockProviderClient) ApplyResourceChange(arg0 context.Context, arg1 *tfplugin6.ApplyResourceChange_Request, arg2 ...grpc.CallOption) (*tfplugin6.ApplyResourceChange_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -50,16 +55,56 @@ func (m *MockProviderClient) ApplyResourceChange(arg0 context.Context, arg1 *tfp } // ApplyResourceChange indicates an expected call of ApplyResourceChange. -func (mr *MockProviderClientMockRecorder) ApplyResourceChange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ApplyResourceChange(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyResourceChange", reflect.TypeOf((*MockProviderClient)(nil).ApplyResourceChange), varargs...) } +// CallFunction mocks base method. +func (m *MockProviderClient) CallFunction(arg0 context.Context, arg1 *tfplugin6.CallFunction_Request, arg2 ...grpc.CallOption) (*tfplugin6.CallFunction_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CallFunction", varargs...) + ret0, _ := ret[0].(*tfplugin6.CallFunction_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallFunction indicates an expected call of CallFunction. +func (mr *MockProviderClientMockRecorder) CallFunction(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallFunction", reflect.TypeOf((*MockProviderClient)(nil).CallFunction), varargs...) +} + +// CloseEphemeralResource mocks base method. +func (m *MockProviderClient) CloseEphemeralResource(arg0 context.Context, arg1 *tfplugin6.CloseEphemeralResource_Request, arg2 ...grpc.CallOption) (*tfplugin6.CloseEphemeralResource_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CloseEphemeralResource", varargs...) + ret0, _ := ret[0].(*tfplugin6.CloseEphemeralResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloseEphemeralResource indicates an expected call of CloseEphemeralResource. +func (mr *MockProviderClientMockRecorder) CloseEphemeralResource(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloseEphemeralResource", reflect.TypeOf((*MockProviderClient)(nil).CloseEphemeralResource), varargs...) +} + // ConfigureProvider mocks base method. func (m *MockProviderClient) ConfigureProvider(arg0 context.Context, arg1 *tfplugin6.ConfigureProvider_Request, arg2 ...grpc.CallOption) (*tfplugin6.ConfigureProvider_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -70,16 +115,56 @@ func (m *MockProviderClient) ConfigureProvider(arg0 context.Context, arg1 *tfplu } // ConfigureProvider indicates an expected call of ConfigureProvider. -func (mr *MockProviderClientMockRecorder) ConfigureProvider(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ConfigureProvider(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigureProvider", reflect.TypeOf((*MockProviderClient)(nil).ConfigureProvider), varargs...) } +// GetFunctions mocks base method. +func (m *MockProviderClient) GetFunctions(arg0 context.Context, arg1 *tfplugin6.GetFunctions_Request, arg2 ...grpc.CallOption) (*tfplugin6.GetFunctions_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetFunctions", varargs...) + ret0, _ := ret[0].(*tfplugin6.GetFunctions_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFunctions indicates an expected call of GetFunctions. +func (mr *MockProviderClientMockRecorder) GetFunctions(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFunctions", reflect.TypeOf((*MockProviderClient)(nil).GetFunctions), varargs...) +} + +// GetMetadata mocks base method. +func (m *MockProviderClient) GetMetadata(arg0 context.Context, arg1 *tfplugin6.GetMetadata_Request, arg2 ...grpc.CallOption) (*tfplugin6.GetMetadata_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetMetadata", varargs...) + ret0, _ := ret[0].(*tfplugin6.GetMetadata_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMetadata indicates an expected call of GetMetadata. +func (mr *MockProviderClientMockRecorder) GetMetadata(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetadata", reflect.TypeOf((*MockProviderClient)(nil).GetMetadata), varargs...) +} + // GetProviderSchema mocks base method. func (m *MockProviderClient) GetProviderSchema(arg0 context.Context, arg1 *tfplugin6.GetProviderSchema_Request, arg2 ...grpc.CallOption) (*tfplugin6.GetProviderSchema_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -90,16 +175,36 @@ func (m *MockProviderClient) GetProviderSchema(arg0 context.Context, arg1 *tfplu } // GetProviderSchema indicates an expected call of GetProviderSchema. -func (mr *MockProviderClientMockRecorder) GetProviderSchema(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) GetProviderSchema(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProviderSchema", reflect.TypeOf((*MockProviderClient)(nil).GetProviderSchema), varargs...) } +// GetResourceIdentitySchemas mocks base method. +func (m *MockProviderClient) GetResourceIdentitySchemas(arg0 context.Context, arg1 *tfplugin6.GetResourceIdentitySchemas_Request, arg2 ...grpc.CallOption) (*tfplugin6.GetResourceIdentitySchemas_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetResourceIdentitySchemas", varargs...) + ret0, _ := ret[0].(*tfplugin6.GetResourceIdentitySchemas_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetResourceIdentitySchemas indicates an expected call of GetResourceIdentitySchemas. +func (mr *MockProviderClientMockRecorder) GetResourceIdentitySchemas(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResourceIdentitySchemas", reflect.TypeOf((*MockProviderClient)(nil).GetResourceIdentitySchemas), varargs...) +} + // ImportResourceState mocks base method. func (m *MockProviderClient) ImportResourceState(arg0 context.Context, arg1 *tfplugin6.ImportResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin6.ImportResourceState_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -110,16 +215,56 @@ func (m *MockProviderClient) ImportResourceState(arg0 context.Context, arg1 *tfp } // ImportResourceState indicates an expected call of ImportResourceState. -func (mr *MockProviderClientMockRecorder) ImportResourceState(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ImportResourceState(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportResourceState", reflect.TypeOf((*MockProviderClient)(nil).ImportResourceState), varargs...) } +// MoveResourceState mocks base method. +func (m *MockProviderClient) MoveResourceState(arg0 context.Context, arg1 *tfplugin6.MoveResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin6.MoveResourceState_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "MoveResourceState", varargs...) + ret0, _ := ret[0].(*tfplugin6.MoveResourceState_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MoveResourceState indicates an expected call of MoveResourceState. +func (mr *MockProviderClientMockRecorder) MoveResourceState(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveResourceState", reflect.TypeOf((*MockProviderClient)(nil).MoveResourceState), varargs...) +} + +// OpenEphemeralResource mocks base method. +func (m *MockProviderClient) OpenEphemeralResource(arg0 context.Context, arg1 *tfplugin6.OpenEphemeralResource_Request, arg2 ...grpc.CallOption) (*tfplugin6.OpenEphemeralResource_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "OpenEphemeralResource", varargs...) + ret0, _ := ret[0].(*tfplugin6.OpenEphemeralResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// OpenEphemeralResource indicates an expected call of OpenEphemeralResource. +func (mr *MockProviderClientMockRecorder) OpenEphemeralResource(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenEphemeralResource", reflect.TypeOf((*MockProviderClient)(nil).OpenEphemeralResource), varargs...) +} + // PlanResourceChange mocks base method. func (m *MockProviderClient) PlanResourceChange(arg0 context.Context, arg1 *tfplugin6.PlanResourceChange_Request, arg2 ...grpc.CallOption) (*tfplugin6.PlanResourceChange_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -130,16 +275,16 @@ func (m *MockProviderClient) PlanResourceChange(arg0 context.Context, arg1 *tfpl } // PlanResourceChange indicates an expected call of PlanResourceChange. -func (mr *MockProviderClientMockRecorder) PlanResourceChange(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) PlanResourceChange(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PlanResourceChange", reflect.TypeOf((*MockProviderClient)(nil).PlanResourceChange), varargs...) } // ReadDataSource mocks base method. func (m *MockProviderClient) ReadDataSource(arg0 context.Context, arg1 *tfplugin6.ReadDataSource_Request, arg2 ...grpc.CallOption) (*tfplugin6.ReadDataSource_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -150,16 +295,16 @@ func (m *MockProviderClient) ReadDataSource(arg0 context.Context, arg1 *tfplugin } // ReadDataSource indicates an expected call of ReadDataSource. -func (mr *MockProviderClientMockRecorder) ReadDataSource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ReadDataSource(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDataSource", reflect.TypeOf((*MockProviderClient)(nil).ReadDataSource), varargs...) } // ReadResource mocks base method. func (m *MockProviderClient) ReadResource(arg0 context.Context, arg1 *tfplugin6.ReadResource_Request, arg2 ...grpc.CallOption) (*tfplugin6.ReadResource_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -170,16 +315,36 @@ func (m *MockProviderClient) ReadResource(arg0 context.Context, arg1 *tfplugin6. } // ReadResource indicates an expected call of ReadResource. -func (mr *MockProviderClientMockRecorder) ReadResource(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ReadResource(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadResource", reflect.TypeOf((*MockProviderClient)(nil).ReadResource), varargs...) } +// RenewEphemeralResource mocks base method. +func (m *MockProviderClient) RenewEphemeralResource(arg0 context.Context, arg1 *tfplugin6.RenewEphemeralResource_Request, arg2 ...grpc.CallOption) (*tfplugin6.RenewEphemeralResource_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "RenewEphemeralResource", varargs...) + ret0, _ := ret[0].(*tfplugin6.RenewEphemeralResource_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RenewEphemeralResource indicates an expected call of RenewEphemeralResource. +func (mr *MockProviderClientMockRecorder) RenewEphemeralResource(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenewEphemeralResource", reflect.TypeOf((*MockProviderClient)(nil).RenewEphemeralResource), varargs...) +} + // StopProvider mocks base method. func (m *MockProviderClient) StopProvider(arg0 context.Context, arg1 *tfplugin6.StopProvider_Request, arg2 ...grpc.CallOption) (*tfplugin6.StopProvider_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -190,16 +355,36 @@ func (m *MockProviderClient) StopProvider(arg0 context.Context, arg1 *tfplugin6. } // StopProvider indicates an expected call of StopProvider. -func (mr *MockProviderClientMockRecorder) StopProvider(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) StopProvider(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StopProvider", reflect.TypeOf((*MockProviderClient)(nil).StopProvider), varargs...) } +// UpgradeResourceIdentity mocks base method. +func (m *MockProviderClient) UpgradeResourceIdentity(arg0 context.Context, arg1 *tfplugin6.UpgradeResourceIdentity_Request, arg2 ...grpc.CallOption) (*tfplugin6.UpgradeResourceIdentity_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "UpgradeResourceIdentity", varargs...) + ret0, _ := ret[0].(*tfplugin6.UpgradeResourceIdentity_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpgradeResourceIdentity indicates an expected call of UpgradeResourceIdentity. +func (mr *MockProviderClientMockRecorder) UpgradeResourceIdentity(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeResourceIdentity", reflect.TypeOf((*MockProviderClient)(nil).UpgradeResourceIdentity), varargs...) +} + // UpgradeResourceState mocks base method. func (m *MockProviderClient) UpgradeResourceState(arg0 context.Context, arg1 *tfplugin6.UpgradeResourceState_Request, arg2 ...grpc.CallOption) (*tfplugin6.UpgradeResourceState_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -210,16 +395,16 @@ func (m *MockProviderClient) UpgradeResourceState(arg0 context.Context, arg1 *tf } // UpgradeResourceState indicates an expected call of UpgradeResourceState. -func (mr *MockProviderClientMockRecorder) UpgradeResourceState(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) UpgradeResourceState(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeResourceState", reflect.TypeOf((*MockProviderClient)(nil).UpgradeResourceState), varargs...) } // ValidateDataResourceConfig mocks base method. func (m *MockProviderClient) ValidateDataResourceConfig(arg0 context.Context, arg1 *tfplugin6.ValidateDataResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateDataResourceConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -230,16 +415,36 @@ func (m *MockProviderClient) ValidateDataResourceConfig(arg0 context.Context, ar } // ValidateDataResourceConfig indicates an expected call of ValidateDataResourceConfig. -func (mr *MockProviderClientMockRecorder) ValidateDataResourceConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ValidateDataResourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateDataResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateDataResourceConfig), varargs...) } +// ValidateEphemeralResourceConfig mocks base method. +func (m *MockProviderClient) ValidateEphemeralResourceConfig(arg0 context.Context, arg1 *tfplugin6.ValidateEphemeralResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateEphemeralResourceConfig_Response, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ValidateEphemeralResourceConfig", varargs...) + ret0, _ := ret[0].(*tfplugin6.ValidateEphemeralResourceConfig_Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ValidateEphemeralResourceConfig indicates an expected call of ValidateEphemeralResourceConfig. +func (mr *MockProviderClientMockRecorder) ValidateEphemeralResourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateEphemeralResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateEphemeralResourceConfig), varargs...) +} + // ValidateProviderConfig mocks base method. func (m *MockProviderClient) ValidateProviderConfig(arg0 context.Context, arg1 *tfplugin6.ValidateProviderConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateProviderConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -250,16 +455,16 @@ func (m *MockProviderClient) ValidateProviderConfig(arg0 context.Context, arg1 * } // ValidateProviderConfig indicates an expected call of ValidateProviderConfig. -func (mr *MockProviderClientMockRecorder) ValidateProviderConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ValidateProviderConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateProviderConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateProviderConfig), varargs...) } // ValidateResourceConfig mocks base method. func (m *MockProviderClient) ValidateResourceConfig(arg0 context.Context, arg1 *tfplugin6.ValidateResourceConfig_Request, arg2 ...grpc.CallOption) (*tfplugin6.ValidateResourceConfig_Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{arg0, arg1} + varargs := []any{arg0, arg1} for _, a := range arg2 { varargs = append(varargs, a) } @@ -270,8 +475,8 @@ func (m *MockProviderClient) ValidateResourceConfig(arg0 context.Context, arg1 * } // ValidateResourceConfig indicates an expected call of ValidateResourceConfig. -func (mr *MockProviderClientMockRecorder) ValidateResourceConfig(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { +func (mr *MockProviderClientMockRecorder) ValidateResourceConfig(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0, arg1}, arg2...) + varargs := append([]any{arg0, arg1}, arg2...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateResourceConfig", reflect.TypeOf((*MockProviderClient)(nil).ValidateResourceConfig), varargs...) } diff --git a/internal/plugin6/serve.go b/internal/plugin6/serve.go index 8c5203fcd5..2bf442b8b4 100644 --- a/internal/plugin6/serve.go +++ b/internal/plugin6/serve.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package plugin6 import ( diff --git a/internal/promising/README.md b/internal/promising/README.md new file mode 100644 index 0000000000..1095ed34a6 --- /dev/null +++ b/internal/promising/README.md @@ -0,0 +1,168 @@ +# Deadlock-free Promises + +This directory contains the Go package `promising`, which is an implementation +of promises that guarantees that it cannot deadlock due to self-dependencies +or a failure to resolve. + +This was created to support the internal evaluation of the Terraform Stacks +runtime, although there's nothing specific to Stacks in here and so it may +attract other callers over time. + +## Overview + +The functionality in this package is built around two key concepts: + +- Promises: a placeholder for a result that might not yet have been produced, + and which can then be asynchronously resolved with a specific result at a + later time. +- Tasks: the codepaths that can produce and resolve promises and can wait for + other promises to be resolved. + +These two concepts together allow this package to provide the additional +guarantee, compared to most typical promise implementations, that it will +never deadlock due to a task trying to await a promise it's responsible +for resolving or failing to resolve a promise it's responsible for resolving. + +![visual representation of the bipartite graph, visualizing the text that follows](deadlock-free-promises.png) + +Each promise has exactly one outgoing edge pointing to a single task, +representing that the promise must be resolved by that task. Each task has +zero or one outgoing edges pointing to a single promise, representing that +the task will remain blocked until the promise is resolved. + +Any task can create a new promise. When the promise is first created, its +responsible task is the task that created it. A task may also create new +asynchronous tasks, and when doing so may pass responsiblility for zero or +more of the promises it is responsible for to the new task. + +Any task can await the result of a promise. The task is then blocked until +the promise is resolved. A blocked task makes no progress. + +The tasks, the promises, and the two kinds of edges between them form a +[bipartite graph](https://en.wikipedia.org/wiki/Bipartite_graph) with one part +tasks and the other part promises. + +There are two situations in which the system will return errors to avoid +what would otherwise cause a deadlock: + +1. If the function representing a task returns before resolving all of the + promises that task is responsible for, all of those promises immediately + resolve with an error, thereby unblocking the awaiting tasks. +2. If a task beginning to await a promise would cause a cycle in the graph + leading back to that same task, walking backwards through the promises' + responsiblility edges and the tasks' awaiting edges, all of the promises + in the chain immediately resolve with an error, thereby unblocking all + of the awaiting tasks. + +In the latter case where a self-reference has been detected, the returned error +includes a set of unique identifiers for all of the affected promises, which +the caller can then use, in conjunction with its own mapping table describing +what each promise id represents, to describe the problem to an end-user. + +## Detecting Self-references + +Preventing the deadlock caused by a chain of tasks depending on each other's +promises requires detecting a cycle in the bipartite graph. + +In Terraform's existing `dag` package, responsible for the more general graph +used by the Terraform Core modules runtime, cycle detection works by calculating +the graph's strongly connected sets using +[Tarjan's algorithm](https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm), +which is linear over the number of edges and nodes but still more complex than +we'd prefer for a result that must be recalculated each time a task either +awaits or resolves a promise. + +Instead, package `promising` exploits the fact that each promise has exactly +one responsible task and each task has zero or one awaited promises, walking +backwards alternately through those single edges until it either runs out of +nodes to visit, or reaches the same promise that the task was attempting to +await. + +Nodes and edges that are not in the chain need not be visited at all, meaning +that these graph journeys tend to be short and relatively fast. + +## Representation of Tasks + +Go does not assign any program-visible identity to its closest approximation +of "task", the goroutine. That's an intentional and reasonable design decision +in Go, but it does mean that goroutines alone are not sufficient to represent +tasks at runtime. + +Instead, package `promising` diverges slightly from Go idiom by using the +value-bundle associated with a `context.Context` to propagate task identity +through the call stack, thereby allowing callers to be written as typical +Go code as long as they always propagate the context between requests. + +There are two ways to create a task, both of which use a function (typically a +closure) as the task's implementation: + +* `promising.MainTask` is a blocking, synchronous entry point that wraps the + main entry point into the subsystem that will make use of tasks and promises. + + The call to `MainTask` does not return until the task implementation + function returns. + +* `promising.AsyncTask` begins an asynchronous concurrent task from within + another task. The call may optionally delegate responsibility for a set + of promises by passing something that implements `PromiseContainer`. + + Internally, `AsyncTask` creates a new goroutine to host the asynchronous + task, and returns as soon as that goroutine has been started. The calling + task may then await the promises it delegated or any other promises as + long as it doesn't create any self-dependencies. + +Creating a new promise, awaiting an existing promise, or creating a new async +task all require passing in a `context.Context` value, and will panic if that +context does not carry a task identity. `MainTask` also requires a context, +but that context need not contain an identity. + +The function that acts as the implementation of a task takes a context +as its first argument. That context carries the identity of the task and so +may be propagated to other functions in the usual way. + +A task _may_ create goroutines that are not considered to have a separate +task identity, and may perform blocking operations that are not mediated by +promises, but the no-deadlock guarantee can apply only to relationships between +tasks and promises and so if a caller uses other synchronization primitives +it's the caller's responsibility to ensure that they cannot interfere with +promise resolution in a way that would cause a deadlock. + +## The `Once` utility + +Alongside the fundamental promise and task concepts, package `promising` +also includes a higher-level utility called `Once`, which is a promise-based +asynchronous alternative to the Go standard library `sync.Once`. + +Specifically, it mediates multiple calls to retrieve the same result and +coalesces them into a single asynchronous task resolving a promise. All of +the callers block on resolution of that promise. + +This high-level abstraction is convenient for the common case of multiple +callers all awaiting the completion of the same work. Because it creates +a promise and an asynchronous task as part of its work, it must be passed +a `context.Context` carrying a task identity, or it will panic. + +## Promise Identifiers + +To help with returning user-friendly error messages in response to +dynamically-detected self-references, each promise has associated with it +a comparable unique identifier, of type `PromiseID`. + +Callers which need this facility should arrange to keep track of a user-oriented +meaning of each promise in a table maintained as part of its own state. + +If a promise result getter returns `promising.ErrSelfDependent` as its `error` +result, the underlying type is a slice of `PromiseID` representing a set of +ids of the promises that were involved in the self-dependency chain. The +caller can then look up those IDs in its table to find the user-friendly +description of each promise and list them all in its error message. + +For the Terraform Stacks runtime in particular, the runtime takes a slightly +different approach as a performance tradeoff: instead of explicitly maintaining +a table of promise IDs, it instead simply remembers the promises themselves at +various positions in the runtime state, and then +_only if a promise returns the self-dependency error_ it will perform a tree +walk over the entire runtime data structure to build the table of promise IDs +and user-friendly names just in time to produce an error message. This then +avoids any need to track promise purposes in the happy path where there are +no self-dependencies. diff --git a/internal/promising/deadlock-free-promises.png b/internal/promising/deadlock-free-promises.png new file mode 100644 index 0000000000..eda49ee5f9 Binary files /dev/null and b/internal/promising/deadlock-free-promises.png differ diff --git a/internal/promising/doc.go b/internal/promising/doc.go new file mode 100644 index 0000000000..87379afb5e --- /dev/null +++ b/internal/promising/doc.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package promising is a utility package providing a model for concurrent +// data fetching and preparation which can detect and report deadlocks +// and failure to resolve. +// +// This is based on the structure and algorithms introduced by Caleb Voss and +// Vivek Sarkar of Georgia Institute of Technology in arXiv:2101.01312v1 +// "An Ownership Policy and Deadlock Detector for Promises". +// +// The model includes both promises and tasks, where tasks can wait for and +// resolve promises and each promise has a single task that is responsbile +// for resolving it. Only explicit tasks can interact with promises, and the +// system uses that rule to detect incorrect situations such as: +// +// - Mutual dependency, where one task blocks on a promise owned by another +// and vice-versa. +// - Failure to resolve, where the task responsible for resolving a promise +// completes before it does so. +// +// Mutual dependency is assumed to be the result of invalid user input where +// two objects rely on each others results, and so that situation is reported +// in a way that can allow describing the problem to an end-user. +// +// Failure to resolve is always an implementation error: a task should always +// either resolve all promises it owns or pass ownership to some other task +// before it completes. +// +// This system cannot detect situations not directly related to promise and task +// relationships. For example, if a particular task blocks forever for a +// non-promise-related reason then that can still cause an effective deadlock +// of the overall system. Callers should design their usage of tasks carefully +// so that e.g. tasks also respond to context cancellation/deadlines. +// +// Package promising uses [context.Context] values to represent dynamic task +// scope, so callers must take care to use the contexts provided to task +// functions by this package (or children of those contexts) when performing +// any task-related or promise-related actions. This implicit behavior is not +// ideal but is a pragmatic tradeoff to help keep task identity aligned with +// other cross-cutting concerns that can travel in contexts, such as loggers +// and distributed tracing clients. +// +// Internally the task-related and promise-related operations implicitly +// construct a directed bipartite graph. Between tasks and promises the +// edges represent "awaiting", and between promises and tasks the edges +// represent which task is currently responsible for resolving each promise. +// Self-dependency is therefore detected by noticing when a call to a +// [PromiseGet] would form a cycle in the graph, and immediately returning an +// error in that case to avoid deadlocking the system. +package promising + +import ( + _ "context" +) diff --git a/internal/promising/errors.go b/internal/promising/errors.go new file mode 100644 index 0000000000..adb3c857ab --- /dev/null +++ b/internal/promising/errors.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +// ErrUnresolved is the error type returned by a promise getter or a main +// task execution if a task fails to resolve all of the promises it is +// responsible for before it returns. +type ErrUnresolved []PromiseID + +func (err ErrUnresolved) Error() string { + return "promise unresolved" +} + +// ErrSelfDependent is the error type returned by a promise getter if the +// requesting task is depending on itself for its own progress, by trying +// to read a promise that it is either directly or indirectly responsible +// for resolving. +// +// The built-in error message is generic but callers can type-assert to +// this type to obtain the chain of promises that lead from the task +// to itself, possibly via other tasks that are themselves awaiting the +// caller to resolve a different promise. +type ErrSelfDependent []PromiseID + +func (err ErrSelfDependent) Error() string { + return "task is self-dependent" +} diff --git a/internal/promising/once.go b/internal/promising/once.go new file mode 100644 index 0000000000..3d02c0eb42 --- /dev/null +++ b/internal/promising/once.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +import ( + "context" + "sync" +) + +// Once is a higher-level wrapper around promises that is similar to +// the Go standard library's sync.Once but executes the one-time function +// in an asynchronous task and makes all subsequent calls block until the +// asynchronous task has completed, after which all calls will return the +// result of the initial call. +type Once[T any] struct { + get PromiseGet[T] + promiseID PromiseID + mu sync.Mutex +} + +// Do makes an asynchronous call to function f only on the first call to +// this method. +// +// The first and all subsequent calls then block on the completion of that +// asynchronous call and all return its single result. +// +// The context used for the call must belong to an active task. The context +// passed to f will belong to a separate asynchronous task and so f can +// create its own promises and create further asynchronous tasks to deal +// with them, as normal. +// +// The typical way to use Do is to have only a single callsite where f is +// set to a function literal whose behavior and results would be equivalent +// regardless of which instance of its closure happens to be the chosen on +// to actually run. All subsequent calls will ignore f entirely, so it is +// incorrect (and useless) to try to vary the effect of f between calls. +// +// This function will return an error of type [ErrSelfDependent] if two +// different Once instances attempt to mutually depend on one another for +// completion. This means that, unlike standard library sync.Once, +// self-dependence cannot cause a deadlock. (Other non-promise-related +// synchronization between calls can still potentially deadlock, though.) +// +// If f panics then that prevents the internal promise from being resolved, +// and so all calls to Do will return [ErrUnresolved]. However, there is +// no built-in facility to catch and recover from such panics since they occur +// in a separate goroutine from all of the waiters. +func (o *Once[T]) Do(ctx context.Context, name string, f func(ctx context.Context) (T, error)) (T, error) { + AssertContextInTask(ctx) + o.mu.Lock() + if o.get == nil { + // We seem to be the first call, so we'll get the asynchronous task + // running and then block on its result. + resolver, get := NewPromise[T](ctx, name) + o.get = get + o.promiseID = resolver.PromiseID() + o.mu.Unlock() + + // The responsibility for resolving the promise transfers to the + // asynchronous task, which makes it valid for this main task to + // await it without a self-dependency error. + AsyncTask( + ctx, resolver, + func(ctx context.Context, resolver PromiseResolver[T]) { + v, err := f(ctx) + resolver.Resolve(ctx, v, err) + }, + ) + } else { + o.mu.Unlock() + } + + // Regardless of whether we launched the async task or not, we'll + // wait for it to resolve the promise before we return. + return o.get(ctx) +} + +// PromiseID returns the unique identifier for the backing promise of the +// receiver, or [NoPromise] if the once hasn't been started yet. +// +// If PromiseID returns [NoPromise] then that result might be immediately +// invalidated by a concurrent or subsequent call to [Once.Do]. However, +// if PromiseID returns a nonzero promise ID then it's guaranteed to remain +// consistent for the remaining lifetime of the object. +func (o *Once[T]) PromiseID() PromiseID { + o.mu.Lock() + ret := o.promiseID + o.mu.Unlock() + return ret +} diff --git a/internal/promising/once_test.go b/internal/promising/once_test.go new file mode 100644 index 0000000000..77974ecf6d --- /dev/null +++ b/internal/promising/once_test.go @@ -0,0 +1,60 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising_test + +import ( + "context" + "sync/atomic" + "testing" + + "github.com/hashicorp/terraform/internal/promising" +) + +func TestOnce(t *testing.T) { + type FakeResult struct { + msg string + } + + var o promising.Once[*FakeResult] + + ctx := context.Background() + results := make([]*FakeResult, 5) + var callCount atomic.Int64 + for i := range results { + // The "Once" mechanism expects to be run inside a task so that + // it can create promises and detect self-dependency problems. + result, err := promising.MainTask(ctx, func(ctx context.Context) (*FakeResult, error) { + return o.Do(ctx, "test", func(ctx context.Context) (*FakeResult, error) { + callCount.Add(1) + return &FakeResult{ + msg: "hello", + }, nil + }) + }) + if err != nil { + t.Fatal(err) + } + results[i] = result + } + + if got, want := callCount.Load(), int64(1); got != want { + t.Errorf("incorrect call count %d; want %d", got, want) + } + + gotPtr := results[0] + if gotPtr == nil { + t.Fatal("first result is nil; want non-nil pointer") + } + if got, want := gotPtr.msg, "hello"; got != want { + t.Fatalf("wrong message %q; want %q", got, want) + } + + // Because of the coalescing effect of Once, all of the results should + // point to the same FakeResult object. + for i, result := range results { + if result != gotPtr { + t.Errorf("result %d does not match result 0; all results should be identical", i) + } + } +} diff --git a/internal/promising/promise.go b/internal/promising/promise.go new file mode 100644 index 0000000000..6c8bfc2e3b --- /dev/null +++ b/internal/promising/promise.go @@ -0,0 +1,326 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// promise represents a result that will become available at some point +// in the future, delivered by an asynchronous [Task]. +type promise struct { + name string + + responsible atomic.Pointer[task] + result atomic.Pointer[promiseResult] + traceSpan trace.Span + + waiting []chan<- struct{} + waitingMu sync.Mutex +} + +func (p *promise) promiseID() PromiseID { + return PromiseID{p} +} + +type promiseResult struct { + val any + err error + + // forced is set when this result was generated by the promise machinery + // itself, as opposed to from calling tasks. We use this to behave more + // gracefully when the responsible task resolution races with the internal + // error, so that we can treat that differently to when the responsible + // task itself tries to resolve a promise multiple times. + forced bool +} + +func getResolvedPromiseResult[T any](result *promiseResult) (T, error) { + // v might fail this type assertion if it's been set to nil + // due to its responsible task exiting without resolving it, + // in which case we'll just return the zero value of T along + // with the error. + v, _ := result.val.(T) + err := result.err + return v, err +} + +// PromiseID is an opaque, comparable unique identifier for a promise, which +// can therefore be used by callers to produce a lookup table of metadata for +// each active promise they are interested in. +// +// The identifier for a promise follows it as the responsibility to resolve it +// transfers beween tasks. +// +// For example, this can be useful for retaining contextual information that +// can help explain which work was implicated in a dependency cycle between +// tasks. +type PromiseID struct { + promise *promise +} + +func (id PromiseID) FriendlyName() string { + return id.promise.name +} + +// NoPromise is the zero value of [PromiseID] and used to represent the absense +// of a promise. +var NoPromise PromiseID + +// NewPromise creates a new promise that the calling task is initially +// responsible for and returns both its resolver and its getter. +// +// The given context must be a task context or this function will panic. +// +// The caller should retain the resolver for its own use and pass the getter +// to any other tasks that will consume the result of the promise. +func NewPromise[T any](ctx context.Context, name string) (PromiseResolver[T], PromiseGet[T]) { + callerSpan := trace.SpanFromContext(ctx) + initialResponsible := mustTaskFromContext(ctx) + p := &promise{name: name} + p.responsible.Store(initialResponsible) + initialResponsible.responsible[p] = struct{}{} + + ctx, span := tracer.Start( + ctx, fmt.Sprintf("promise(%s)", name), + trace.WithNewRoot(), + trace.WithLinks(trace.Link{ + SpanContext: trace.SpanContextFromContext(ctx), + }), + ) + _ = ctx // prevent staticcheck from complaining until we have something actually using this + p.traceSpan = span + promiseSpanContext := span.SpanContext() + + callerSpan.AddEvent("new promise", trace.WithAttributes( + attribute.String("promising.responsible_for", promiseSpanContext.SpanID().String()), + )) + + resolver := PromiseResolver[T]{p} + getter := PromiseGet[T](func(ctx context.Context) (T, error) { + reqT := mustTaskFromContext(ctx) + + waiterSpan := trace.SpanFromContext(ctx) + + ok := reqT.awaiting.CompareAndSwap(nil, p) + if !ok { + // If we get here then the task seems to have forked into two + // goroutines that are trying to await promises concurrently, + // which is illegal per the contract for tasks. + panic("racing promise get") + } + defer func() { + ok := reqT.awaiting.CompareAndSwap(p, nil) + if !ok { + panic("racing promise get") + } + }() + + // We'll first test whether waiting for this promise is possible + // without creating a deadlock, by following the awaiting->responsible + // chain. + checkP := p + checkT := p.responsible.Load() + steps := 1 + for checkT != reqT { + steps++ + if checkT == nil { + break + } + nextCheckP := checkT.awaiting.Load() + if nextCheckP == nil { + break + } + if checkP.responsible.Load() != checkT { + break + } + checkP = nextCheckP + checkT = checkP.responsible.Load() + } + if checkT == reqT { + // We've found a self-dependency, but to report it in a useful + // way we need to collect up all of the promises, so we'll + // repeat the above and collect up all of the promises we find + // along the way this time, instead of just counting them. + err := make(ErrSelfDependent, 0, steps) + var affectedPromises []*promise + checkP := p + checkT := p.responsible.Load() + err = append(err, checkP.promiseID()) + affectedPromises = append(affectedPromises, checkP) + for checkT != reqT { + if checkT == nil { + break + } + nextCheckP := checkT.awaiting.Load() + if nextCheckP == nil { + break + } + if checkP.responsible.Load() != checkT { + break + } + checkP = nextCheckP + checkT = checkP.responsible.Load() + err = append(err, checkP.promiseID()) + affectedPromises = append(affectedPromises, checkP) + } + waiterSpan.AddEvent( + "task is self-dependent", + trace.WithAttributes( + attribute.String("promise.waiting_for_id", promiseSpanContext.SpanID().String()), + ), + ) + + // All waiters for this promise need to see this error, because + // otherwise the other waiters might stall forever waiting for + // a result that will never come. + for _, affected := range affectedPromises { + resolvePromiseInternalFailure(affected, err) + } + // The current promise is one of the "affected promises" that + // were resolved above, so we can now fall through to the check + // below for whether the promise is already resolved and have + // it return the error. + } + + // If we get here then it's safe to actually await. + p.waitingMu.Lock() + if result := p.result.Load(); result != nil { + // No need to wait because the result is already available. + p.waitingMu.Unlock() + waiterSpan.AddEvent( + "promise is already resolved", + trace.WithAttributes( + attribute.String("promise.waiting_for_id", promiseSpanContext.SpanID().String()), + ), + ) + return getResolvedPromiseResult[T](result) + } + + ch := make(chan struct{}) + p.waiting = append(p.waiting, ch) + waiterCount := len(p.waiting) + p.waitingMu.Unlock() + + waiterSpan.AddEvent( + "waiting for promise result", + trace.WithAttributes( + attribute.String("promise.waiting_for_id", promiseSpanContext.SpanID().String()), + attribute.Int("promise.waiter_count", waiterCount), + ), + ) + p.traceSpan.AddEvent( + "new task waiting", + trace.WithAttributes( + attribute.String("promise.waiter_id", waiterSpan.SpanContext().SpanID().String()), + attribute.Int("promise.waiter_count", waiterCount), + ), + ) + <-ch // channel will be closed once promise is resolved + waiterSpan.AddEvent( + "promise resolved", + trace.WithAttributes( + attribute.String("promise.waiting_for_id", promiseSpanContext.SpanID().String()), + ), + ) + if result := p.result.Load(); result != nil { + return getResolvedPromiseResult[T](result) + } else { + // If we get here then there's a bug in resolvePromise below + panic("promise signaled resolved but has no result") + } + }) + + return resolver, getter +} + +func resolvePromise(p *promise, v any, err error) { + p.waitingMu.Lock() + defer p.waitingMu.Unlock() + + respT := p.responsible.Load() + p.responsible.Store(nil) + respT.responsible.Remove(p) + + ok := p.result.CompareAndSwap(nil, &promiseResult{ + val: v, + err: err, + }) + if !ok { + // The result that's now present might be a "forced error" generated + // through promiseInternalFailure, in which case we just quietly + // ignore the attempt to actually resolve it since all of the + // waiters will already have received the error. + r := p.result.Load() + if r != nil && r.forced { + return + } + // Any other conflict indicates a bug in the calling task. + panic("promise resolved more than once") + } + + for _, waitingCh := range p.waiting { + close(waitingCh) + } + p.waiting = nil +} + +// resolvePromiseInternalFailure is a variant of resolvePromise that we use for +// internal errors that aren't produced by the task responsible for the +// promise, such as when tasks become self-dependent and so we need to +// immediately fail all of the promises in the chain to prevent any of +// the waiters from potentially stuck forever waiting for completion that +// might never come, or might see an incorrect result while the failures +// propagate through a different return path. +func resolvePromiseInternalFailure(p *promise, err error) { + p.waitingMu.Lock() + defer p.waitingMu.Unlock() + + p.traceSpan.AddEvent("internal promise failure", trace.WithAttributes( + attribute.String("error", err.Error()), + )) + + // For internal failures we leave the responsibility data in place so + // that the responsible task can still try to resolve the promise and + // have it be a no-op, since the task that's responsible for resolving + // will not typically also call the promise getter, and so it won't + // know about the failure. + + ok := p.result.CompareAndSwap(nil, &promiseResult{ + err: err, + forced: true, + }) + if !ok { + // This suggests either that the responsible task beat us to the punch + // and resolved first, or that this promise was involved in two + // different self-dependence situations simultaneously and a different + // one got recorded already. + // + // Both situations are no big deal -- the promise got resolved one + // way or another -- but we'll record a tracing event for it just + // in case it's helpful while debugging something. + p.traceSpan.AddEvent("internal promise failure conflict") + } + + for _, waitingCh := range p.waiting { + close(waitingCh) + } + p.waiting = nil +} + +// PromiseGet is the signature of a promise "getter" function, which blocks +// until a promise is resolved and then returns its result values. +// +// A PromiseGet function may be called only within a task, using a context +// value that descends from that task's context. +// +// If the given context is cancelled or reaches its deadline then the function +// will return the relevant context-related error to describe that situation. +type PromiseGet[T any] func(ctx context.Context) (T, error) diff --git a/internal/promising/promise_container.go b/internal/promising/promise_container.go new file mode 100644 index 0000000000..0a0891d854 --- /dev/null +++ b/internal/promising/promise_container.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +// PromiseContainer is an interface implemented by types whose behavior +// is implemented in terms of at least one promise, which therefore allows +// the responsibility for resolving those promises to be moved from +// one task to another. +// +// All promises in a single container must have the same task responsible +// for resolving them. +type PromiseContainer interface { + // AnnounceContainedPromises calls the given callback exactly once for + // each promise that the receiver is implemented in terms of. + AnnounceContainedPromises(func(AnyPromiseResolver)) +} + +var NoPromises PromiseContainer + +type noPromises struct{} + +func (noPromises) AnnounceContainedPromises(func(AnyPromiseResolver)) { + // Nothing to announce. +} + +func init() { + NoPromises = noPromises{} +} + +// PromiseResolverPair is a convenience [PromiseContainer] for passing a +// pair of promise resolvers of different result types to a child task without +// having to create a custom struct type to do it. +// +// This is a shortcut for simpler cases. If you need something more elaborate +// than this, write your own implementation of [PromiseContainer]. +type PromiseResolverPair[AType, BType any] struct { + A PromiseResolver[AType] + B PromiseResolver[BType] +} + +func (pair PromiseResolverPair[AType, BType]) AnnounceContainedPromises(cb func(AnyPromiseResolver)) { + cb(pair.A) + cb(pair.B) +} + +// PromiseResolverList is a convenience [PromiseContainer] for passing an +// arbitrary number of promise resolvers of the same result type to a child +// task without having to create a custom struct type to do it. +// +// Go's type system does not support variadic generics so we cannot provide +// a single type that collects an arbitrary number of resolvers with different +// result types. If you need that then you must write your own struct type +// which implements [PromiseContainer], or alternatively use +// [PromiseResolverPair] if you happen to have exactly two resolvers to pass. +type PromiseResolverList[T any] []PromiseResolver[T] + +func (l PromiseResolverList[T]) AnnounceContainedPromises(cb func(AnyPromiseResolver)) { + for _, r := range l { + cb(r) + } +} diff --git a/internal/promising/promise_resolver.go b/internal/promising/promise_resolver.go new file mode 100644 index 0000000000..a83b34dcb7 --- /dev/null +++ b/internal/promising/promise_resolver.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// PromiseResolver is an object representing responsibility for a promise, which +// can be passed between tasks to delegate responsibility and then eventually +// be used to provide the promise's final results. +type PromiseResolver[T any] struct { + p *promise +} + +// Resolve provides the final results for a promise. +// +// This may be called only from the task that is currently responsible for +// the promise +func (pr PromiseResolver[T]) Resolve(ctx context.Context, v T, err error) { + callerT := mustTaskFromContext(ctx) + if pr.p.responsible.Load() != callerT { + panic("promise resolved by incorrect task") + } + resolvePromise(pr.p, v, err) + + resolvingTaskSpan := trace.SpanFromContext(ctx) + resolvingTaskSpanContext := resolvingTaskSpan.SpanContext() + promiseSpanContext := pr.p.traceSpan.SpanContext() + pr.p.traceSpan.AddEvent( + "resolved", + trace.WithAttributes( + attribute.String("promising.resolved_by", resolvingTaskSpanContext.SpanID().String()), + ), + ) + resolvingTaskSpan.AddEvent( + "resolved a promise", + trace.WithAttributes( + attribute.String("promising.resolved_id", promiseSpanContext.SpanID().String()), + ), + ) + pr.p.traceSpan.End() +} + +func (pr PromiseResolver[T]) PromiseID() PromiseID { + return PromiseID{pr.p} +} + +// promise implements AnyPromiseResolver. +func (pr PromiseResolver[T]) promise() *promise { + return pr.p +} + +// AnnounceContainedPromises implements PromiseContainer for a single naked +// promise resolver. +func (pr PromiseResolver[T]) AnnounceContainedPromises(cb func(AnyPromiseResolver)) { + cb(pr) +} + +// AnyPromiseResolver is an interface implemented by all [PromiseResolver] +// instantiations, regardless of result type. +// +// Callers should typically not type-assert an AnyPromiseResolver into an +// instance of [PromiseResolver] unless the caller is the task that is +// currently responsible for resolving the promise. +type AnyPromiseResolver interface { + promise() *promise +} diff --git a/internal/promising/ptr_set.go b/internal/promising/ptr_set.go new file mode 100644 index 0000000000..1d84b3ffc7 --- /dev/null +++ b/internal/promising/ptr_set.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +type ptrSet[T any] map[*T]struct{} + +func (s ptrSet[T]) Add(p *T) { + s[p] = struct{}{} +} + +func (s ptrSet[T]) Remove(p *T) { + delete(s, p) +} + +func (s ptrSet[T]) Has(p *T) bool { + _, ret := s[p] + return ret +} + +type promiseSet = ptrSet[promise] diff --git a/internal/promising/public_test.go b/internal/promising/public_test.go new file mode 100644 index 0000000000..17caee8dc9 --- /dev/null +++ b/internal/promising/public_test.go @@ -0,0 +1,254 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising_test + +import ( + "context" + "errors" + "testing" + + "github.com/hashicorp/terraform/internal/promising" +) + +func TestMainTaskNoOp(t *testing.T) { + wantVal := "hello" + wantErr := errors.New("hello") + + ctx := context.Background() + gotVal, gotErr := promising.MainTask(ctx, func(ctx context.Context) (string, error) { + return wantVal, wantErr + }) + + if gotVal != wantVal { + t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal) + } + if gotErr != wantErr { + t.Errorf("wrong error\ngot: %q\nwant: %q", gotErr, wantErr) + } +} + +func TestPromiseResolveSimple(t *testing.T) { + wantVal := "hello" + + ctx := context.Background() + gotVal, err := promising.MainTask(ctx, func(ctx context.Context) (string, error) { + resolver, get := promising.NewPromise[string](ctx, "test") + + promising.AsyncTask( + ctx, resolver, + func(ctx context.Context, resolver promising.PromiseResolver[string]) { + resolver.Resolve(ctx, wantVal, nil) + }, + ) + return get(ctx) + }) + + if gotVal != wantVal { + t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal) + } + if err != nil { + t.Errorf("unexpected error: %s", err) + } +} + +func TestPromiseUnresolvedMainWithoutGet(t *testing.T) { + ctx := context.Background() + var promiseID promising.PromiseID + gotVal, err := promising.MainTask(ctx, func(ctx context.Context) (string, error) { + resolver, _ := promising.NewPromise[string](ctx, "test") + promiseID = resolver.PromiseID() + // Call to PromiseResolver.Resolve intentionally omitted to cause error + // Also not calling the getter to prevent this from being classified as + // a self-dependency. + return "", nil + }) + + if wantVal := ""; gotVal != wantVal { + // When unresolved the return value should be the zero value of the type. + t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal) + } + if promiseIDs, ok := err.(promising.ErrUnresolved); !ok { + t.Errorf("wrong error\ngot: %s\nwant: an ErrUnresolved value", err) + } else if got, want := len(promiseIDs), 1; got != want { + t.Errorf("wrong number of unresolved promises %d; want %d", got, want) + } else if promiseIDs[0] != promiseID { + t.Error("errored promise ID does not match the one returned during the task") + } +} + +func TestPromiseUnresolvedMainWithGet(t *testing.T) { + ctx := context.Background() + var promiseID promising.PromiseID + gotVal, gotErr := promising.MainTask(ctx, func(ctx context.Context) (string, error) { + resolver, get := promising.NewPromise[string](ctx, "test") + promiseID = resolver.PromiseID() + // Call to PromiseResolver.Resolve intentionally omitted to cause error + return get(ctx) + }) + + if wantVal := ""; gotVal != wantVal { + // When unresolved the return value should be the zero value of the type. + t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal) + } + + // Since the main task was both the one responsible for the promise and + // the one trying to read it, this is classified as a self-dependency + // rather than an "unresolved". + if err, ok := gotErr.(promising.ErrSelfDependent); ok { + if got, want := len(err), 1; got != want { + t.Fatalf("wrong number of promise IDs in error %d; want %d", got, want) + } + if got, want := err[0], promiseID; got != want { + t.Errorf("wrong promise ID in error\ngot: %#v\nwant: %#v", got, want) + } + } else { + t.Errorf("wrong error\ngot: %s\nwant: an ErrSelfDependent value", gotErr) + } +} + +func TestPromiseUnresolvedAsync(t *testing.T) { + ctx := context.Background() + var promiseID promising.PromiseID + gotVal, err := promising.MainTask(ctx, func(ctx context.Context) (string, error) { + resolver, get := promising.NewPromise[string](ctx, "test") + promiseID = resolver.PromiseID() + + promising.AsyncTask( + ctx, resolver, + func(ctx context.Context, resolver promising.PromiseResolver[string]) { + // Call to resolver.Resolve intentionally omitted to cause error + }, + ) + return get(ctx) + }) + + if wantVal := ""; gotVal != wantVal { + // When unresolved the return value should be the zero value of the type. + t.Errorf("wrong result value\ngot: %q\nwant: %q", gotVal, wantVal) + } + if promiseIDs, ok := err.(promising.ErrUnresolved); !ok { + t.Errorf("wrong error\ngot: %s\nwant: an ErrUnresolved value", err) + } else if got, want := len(promiseIDs), 1; got != want { + t.Errorf("wrong number of unresolved promises %d; want %d", got, want) + } else if promiseIDs[0] != promiseID { + t.Error("errored promise ID does not match the one returned during the task") + } +} + +func TestPromiseSelfDependentSibling(t *testing.T) { + ctx := context.Background() + var err1, err2 error + promising.MainTask(ctx, func(ctx context.Context) (string, error) { + resolver1, get1 := promising.NewPromise[string](ctx, "test") + resolver2, get2 := promising.NewPromise[string](ctx, "test") + + // The following is an intentional self-dependency, though its + // unpredictable which of the two tasks will actually detect the error, + // since it'll be whichever one reaches its getter second. + promising.AsyncTask( + ctx, resolver1, + func(ctx context.Context, resolver1 promising.PromiseResolver[string]) { + v, err := get2(ctx) + resolver1.Resolve(ctx, v, err) + }, + ) + promising.AsyncTask( + ctx, resolver2, + func(ctx context.Context, resolver1 promising.PromiseResolver[string]) { + v, err := get1(ctx) + resolver2.Resolve(ctx, v, err) + }, + ) + + _, err1 = get1(ctx) + _, err2 = get2(ctx) + return "", nil + }) + + switch { + case err1 == nil && err2 == nil: + t.Fatalf("both promises succeeded; expected both to fail") + case err1 == nil: + t.Fatalf("first promise succeeded; expected both to fail") + case err2 == nil: + t.Fatalf("second promise succeeded; expected both to fail") + } + + if err, ok := err1.(promising.ErrSelfDependent); ok { + if got, want := len(err), 2; got != want { + t.Fatalf("wrong number of promise IDs in err1 %d; want %d", got, want) + } + } else { + t.Errorf("wrong err1\ngot: %s\nwant: an ErrSelfDependent value", err1) + } + if err, ok := err2.(promising.ErrSelfDependent); ok { + if got, want := len(err), 2; got != want { + t.Fatalf("wrong number of promise IDs in err2 %d; want %d", got, want) + } + } else { + t.Errorf("wrong err2\ngot: %s\nwant: an ErrSelfDependent value", err2) + } + +} + +func TestPromiseSelfDependentNested(t *testing.T) { + ctx := context.Background() + var err1, err2 error + promising.MainTask(ctx, func(ctx context.Context) (string, error) { + resolver1, get1 := promising.NewPromise[string](ctx, "test") + resolver2, get2 := promising.NewPromise[string](ctx, "test") + pair := promising.PromiseResolverPair[string, string]{A: resolver1, B: resolver2} + + // The following is an intentional self-dependency. Both calls should + // fail here, since a self-dependency problem causes all affected + // promises to immediately emit an error. + promising.AsyncTask( + ctx, pair, + func(ctx context.Context, pair promising.PromiseResolverPair[string, string]) { + resolver1 := pair.A + resolver2 := pair.B + + promising.AsyncTask( + ctx, resolver2, + func(ctx context.Context, resolver1 promising.PromiseResolver[string]) { + v, err := get1(ctx) + resolver2.Resolve(ctx, v, err) + }, + ) + + v, err := get2(ctx) + resolver1.Resolve(ctx, v, err) + }, + ) + + _, err1 = get1(ctx) + _, err2 = get2(ctx) + return "", nil + }) + + switch { + case err1 == nil && err2 == nil: + t.Fatalf("both promises succeeded; expected both to fail") + case err1 == nil: + t.Fatalf("first promise succeeded; expected both to fail") + case err2 == nil: + t.Fatalf("second promise succeeded; expected both to fail") + } + + if err, ok := err1.(promising.ErrSelfDependent); ok { + if got, want := len(err), 2; got != want { + t.Fatalf("wrong number of promise IDs in err1 %d; want %d", got, want) + } + } else { + t.Errorf("wrong err1\ngot: %s\nwant: an ErrSelfDependent value", err1) + } + if err, ok := err2.(promising.ErrSelfDependent); ok { + if got, want := len(err), 2; got != want { + t.Fatalf("wrong number of promise IDs in err2 %d; want %d", got, want) + } + } else { + t.Errorf("wrong err2\ngot: %s\nwant: an ErrSelfDependent value", err2) + } + +} diff --git a/internal/promising/task.go b/internal/promising/task.go new file mode 100644 index 0000000000..a77596313e --- /dev/null +++ b/internal/promising/task.go @@ -0,0 +1,151 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +import ( + "context" + "sync/atomic" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// task represents one of a set of collaborating tasks that are communicating +// in terms of promises. +type task struct { + awaiting atomic.Pointer[promise] + responsible promiseSet +} + +// MainTask runs the given function as a "main task", which is a task +// that blocks execution of the caller until it is complete and can create +// the promises and other async tasks required to produce its result. +func MainTask[T any](ctx context.Context, impl func(ctx context.Context) (T, error)) (T, error) { + mainT := &task{ + responsible: make(promiseSet), + } + ctx = contextWithTask(ctx, mainT) + v, err := impl(ctx) + + // The implementation function must have either resolved all of its + // promises or transferred responsibility for them to another task + // before it returns. + var unresolvedErr ErrUnresolved + for unresolved := range mainT.responsible { + oneErr := ErrUnresolved{unresolved.promiseID()} + resolvePromise(unresolved, nil, oneErr) + unresolvedErr = append(unresolvedErr, unresolved.promiseID()) + } + if err == nil && len(unresolvedErr) != 0 { + // If the task wasn't already returning its own error then we'll + // make it return our ErrUnresolved so the caller will know that the + // task behaved incorrectly. + err = unresolvedErr + } + return v, err +} + +// AsyncTask runs the given function as a new task, passing responsibility +// for the promises in the given [PromiseContainer] to the new task. +// +// The new task runs concurrently with the caller as a new goroutine. It must +// either resolve all of the given promises or delegate responsibilty for +// them to another task before returning. +// +// The context passed to the implementation function carries the identity of +// the new task, and so the task must use that context for any calls to +// [PromiseGet] functions and for resolving any promises. +// +// A task should typically be a single thread of execution and not spawn +// any new goroutines unless doing so indirectly through another call to +// [AsyncTask]. If a particular task _does_ spawn additional goroutines then +// it's the task implementer's responsibility to prevent concurrent calls to +// any promise getters or resolvers from multiple goroutines. In particular, +// each task is allowed to await only one promise at a time and violating +// this invariant will cause undefined behavior. +func AsyncTask[P PromiseContainer](ctx context.Context, promises P, impl func(ctx context.Context, promises P)) { + callerT := mustTaskFromContext(ctx) + + newT := &task{ + responsible: make(promiseSet), + } + + // We treat async tasks as disconnected from their caller when tracing, + // because each task has an independent lifetime, but we do still track + // the causal relationship between the two using span links. + callerSpanContext := trace.SpanFromContext(ctx).SpanContext() + + childCtx, childSpan := tracer.Start( + ctx, "async task", + trace.WithNewRoot(), + trace.WithLinks( + trace.Link{ + SpanContext: callerSpanContext, + }, + ), + ) + + promises.AnnounceContainedPromises(func(apr AnyPromiseResolver) { + p := apr.promise() + if p.responsible.Load() != callerT { + // TODO: a better error message that gives some information + // about what mismatched? + panic("promise responsibility mismatch") + } + newT.responsible.Add(p) + callerT.responsible.Remove(p) + p.responsible.Store(newT) + p.traceSpan.AddEvent("delegated to new task", trace.WithAttributes( + attribute.String("promising.delegated_from", callerSpanContext.SpanID().String()), + attribute.String("promising.delegated_to", childSpan.SpanContext().SpanID().String()), + )) + childSpan.AddEvent("inherited promise responsibility", trace.WithAttributes( + attribute.String("promising.responsible_for", p.traceSpan.SpanContext().SpanID().String()), + )) + }) + + go func() { + ctx := childCtx + defer childSpan.End() + ctx = contextWithTask(ctx, newT) + impl(ctx, promises) + + // The implementation function must have either resolved all of its + // promises or transferred responsibility for them to another task + // before it returns. + for unresolved := range newT.responsible { + err := ErrUnresolved{unresolved.promiseID()} + resolvePromise(unresolved, nil, err) + } + }() +} + +// AssertContextInTask panics if the given context does not belong to a +// promising task, or does nothing at all if it does. +// +// This is here just as a helper for clearly marking functions that only +// make sense to call when in a task context, typically because they are +// going to rely on promises. +func AssertContextInTask(ctx context.Context) { + _, ok := ctx.Value(taskContextKey).(*task) + if !ok { + panic("function requires an active task, but the given context does not belong to one") + } +} + +func mustTaskFromContext(ctx context.Context) *task { + ret, ok := ctx.Value(taskContextKey).(*task) + if !ok { + panic("cannot interact with promises or tasks from non-task context") + } + return ret +} + +func contextWithTask(ctx context.Context, t *task) context.Context { + return context.WithValue(ctx, taskContextKey, t) +} + +type taskContextKeyType int + +const taskContextKey taskContextKeyType = 0 diff --git a/internal/promising/telemetry.go b/internal/promising/telemetry.go new file mode 100644 index 0000000000..7c3888f363 --- /dev/null +++ b/internal/promising/telemetry.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package promising + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform/internal/promising") +} diff --git a/internal/provider-simple-v6/main/main.go b/internal/provider-simple-v6/main/main.go index cc2bbc3c3f..4581a490fd 100644 --- a/internal/provider-simple-v6/main/main.go +++ b/internal/provider-simple-v6/main/main.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( diff --git a/internal/provider-simple-v6/provider.go b/internal/provider-simple-v6/provider.go index 1fb2ce127b..065121bbbc 100644 --- a/internal/provider-simple-v6/provider.go +++ b/internal/provider-simple-v6/provider.go @@ -1,15 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // simple provider a minimal provider implementation for testing package simple import ( "errors" "fmt" + "log" "time" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" - "github.com/zclconf/go-cty/cty" - ctyjson "github.com/zclconf/go-cty/cty/json" ) type simple struct { @@ -18,7 +23,7 @@ type simple struct { func Provider() providers.Interface { simpleResource := providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Computed: true, @@ -35,7 +40,7 @@ func Provider() providers.Interface { return simple{ schema: providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: nil, + Body: nil, }, ResourceTypes: map[string]providers.Schema{ "simple_resource": simpleResource, @@ -43,8 +48,29 @@ func Provider() providers.Interface { DataSources: map[string]providers.Schema{ "simple_resource": simpleResource, }, + EphemeralResourceTypes: map[string]providers.Schema{ + "simple_resource": simpleResource, + }, ServerCapabilities: providers.ServerCapabilities{ - PlanDestroy: true, + PlanDestroy: true, + GetProviderSchemaOptional: true, + }, + Functions: map[string]providers.FunctionDecl{ + "noop": { + Parameters: []providers.FunctionParam{ + { + Name: "noop", + Type: cty.DynamicPseudoType, + AllowNullValue: true, + AllowUnknownValues: true, + Description: "any value", + DescriptionKind: configschema.StringPlain, + }, + }, + ReturnType: cty.DynamicPseudoType, + Description: "noop takes any single argument and returns the same value", + DescriptionKind: configschema.StringPlain, + }, }, }, } @@ -54,6 +80,25 @@ func (s simple) GetProviderSchema() providers.GetProviderSchemaResponse { return s.schema } +func (s simple) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{ + "simple_resource": { + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } +} + func (s simple) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { return resp } @@ -67,13 +112,22 @@ func (s simple) ValidateDataResourceConfig(req providers.ValidateDataResourceCon } func (p simple) UpgradeResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + ty := p.schema.ResourceTypes[req.TypeName].Body.ImpliedType() val, err := ctyjson.Unmarshal(req.RawStateJSON, ty) resp.Diagnostics = resp.Diagnostics.Append(err) resp.UpgradedState = val return resp } +func (p simple) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + schema := p.GetResourceIdentitySchemas().IdentityTypes[req.TypeName].Body + ty := schema.ImpliedType() + val, err := ctyjson.Unmarshal(req.RawIdentityJSON, ty) + resp.Diagnostics = resp.Diagnostics.Append(err) + resp.UpgradedIdentity = val + return resp +} + func (s simple) ConfigureProvider(providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { return resp } @@ -85,6 +139,7 @@ func (s simple) Stop() error { func (s simple) ReadResource(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { // just return the same state we received resp.NewState = req.PriorState + resp.Identity = req.CurrentIdentity return resp } @@ -117,6 +172,7 @@ func (s simple) ApplyResourceChange(req providers.ApplyResourceChangeRequest) (r } resp.NewState = req.PlannedState + resp.NewIdentity = req.PlannedIdentity return resp } @@ -135,6 +191,13 @@ func (s simple) ImportResourceState(providers.ImportResourceStateRequest) (resp return resp } +func (s simple) MoveResourceState(providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { + // We don't expose the move_resource_state capability, so this should never + // be called. + resp.Diagnostics = resp.Diagnostics.Append(errors.New("unsupported")) + return resp +} + func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { m := req.Config.AsValueMap() m["id"] = cty.StringVal("static_id") @@ -142,6 +205,48 @@ func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp provid return resp } +func (p simple) ValidateEphemeralResourceConfig(req providers.ValidateEphemeralResourceConfigRequest) (resp providers.ValidateEphemeralResourceConfigResponse) { + return resp +} + +func (s simple) OpenEphemeralResource(req providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + // we only have one type, so no need to check + m := req.Config.AsValueMap() + m["id"] = cty.StringVal("ephemeral secret") + resp.Result = cty.ObjectVal(m) + resp.Private = []byte("private data") + resp.RenewAt = time.Now().Add(time.Second) + return resp +} + +func (s simple) RenewEphemeralResource(req providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) { + log.Printf("[DEBUG] renewing ephemeral resource") + if string(req.Private) != "private data" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid private data %q, cannot renew ephemeral resource", req.Private)) + } + resp.Private = req.Private + resp.RenewAt = time.Now().Add(time.Second) + return resp +} + +func (s simple) CloseEphemeralResource(req providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { + log.Printf("[DEBUG] closing ephemeral resource") + if string(req.Private) != "private data" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid private data %q, cannot close ephemeral resource", req.Private)) + } + return resp +} + +func (s simple) CallFunction(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + if req.FunctionName != "noop" { + resp.Err = fmt.Errorf("CallFunction for undefined function %q", req.FunctionName) + return resp + } + + resp.Result = req.Arguments[0] + return resp +} + func (s simple) Close() error { return nil } diff --git a/internal/provider-simple/main/main.go b/internal/provider-simple/main/main.go index 8e8ceadff9..a63da0a2b2 100644 --- a/internal/provider-simple/main/main.go +++ b/internal/provider-simple/main/main.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( diff --git a/internal/provider-simple/provider.go b/internal/provider-simple/provider.go index 8e32dcc10e..7ac1cf0c9d 100644 --- a/internal/provider-simple/provider.go +++ b/internal/provider-simple/provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // simple provider a minimal provider implementation for testing package simple @@ -5,10 +8,11 @@ import ( "errors" "time" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" ) type simple struct { @@ -17,7 +21,7 @@ type simple struct { func Provider() providers.Interface { simpleResource := providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Computed: true, @@ -34,7 +38,7 @@ func Provider() providers.Interface { return simple{ schema: providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: nil, + Body: nil, }, ResourceTypes: map[string]providers.Schema{ "simple_resource": simpleResource, @@ -42,6 +46,9 @@ func Provider() providers.Interface { DataSources: map[string]providers.Schema{ "simple_resource": simpleResource, }, + EphemeralResourceTypes: map[string]providers.Schema{ + "simple_resource": simpleResource, + }, ServerCapabilities: providers.ServerCapabilities{ PlanDestroy: true, }, @@ -53,6 +60,25 @@ func (s simple) GetProviderSchema() providers.GetProviderSchemaResponse { return s.schema } +func (s simple) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{ + "simple_resource": { + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } +} + func (s simple) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { return resp } @@ -66,13 +92,22 @@ func (s simple) ValidateDataResourceConfig(req providers.ValidateDataResourceCon } func (p simple) UpgradeResourceState(req providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { - ty := p.schema.ResourceTypes[req.TypeName].Block.ImpliedType() + ty := p.schema.ResourceTypes[req.TypeName].Body.ImpliedType() val, err := ctyjson.Unmarshal(req.RawStateJSON, ty) resp.Diagnostics = resp.Diagnostics.Append(err) resp.UpgradedState = val return resp } +func (p simple) UpgradeResourceIdentity(req providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + schema := p.GetResourceIdentitySchemas().IdentityTypes[req.TypeName].Body + ty := schema.ImpliedType() + val, err := ctyjson.Unmarshal(req.RawIdentityJSON, ty) + resp.Diagnostics = resp.Diagnostics.Append(err) + resp.UpgradedIdentity = val + return resp +} + func (s simple) ConfigureProvider(providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { return resp } @@ -84,6 +119,7 @@ func (s simple) Stop() error { func (s simple) ReadResource(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { // just return the same state we received resp.NewState = req.PriorState + resp.Identity = req.CurrentIdentity return resp } @@ -117,6 +153,7 @@ func (s simple) ApplyResourceChange(req providers.ApplyResourceChangeRequest) (r m["id"] = cty.StringVal(time.Now().String()) } resp.NewState = cty.ObjectVal(m) + resp.NewIdentity = req.PlannedIdentity return resp } @@ -126,6 +163,13 @@ func (s simple) ImportResourceState(providers.ImportResourceStateRequest) (resp return resp } +func (s simple) MoveResourceState(providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { + // We don't expose the move_resource_state capability, so this should never + // be called. + resp.Diagnostics = resp.Diagnostics.Append(errors.New("unsupported")) + return resp +} + func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { m := req.Config.AsValueMap() m["id"] = cty.StringVal("static_id") @@ -133,6 +177,36 @@ func (s simple) ReadDataSource(req providers.ReadDataSourceRequest) (resp provid return resp } +func (p simple) ValidateEphemeralResourceConfig(req providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse { + // Our schema doesn't include any ephemeral resource types, so it should be + // impossible to get in here. + panic("ValidateEphemeralResourceConfig on provider that didn't declare any ephemeral resource types") +} + +func (s simple) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + // Our schema doesn't include any ephemeral resource types, so it should be + // impossible to get in here. + panic("OpenEphemeralResource on provider that didn't declare any ephemeral resource types") +} + +func (s simple) RenewEphemeralResource(providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse { + // Our schema doesn't include any ephemeral resource types, so it should be + // impossible to get in here. + panic("RenewEphemeralResource on provider that didn't declare any ephemeral resource types") +} + +func (s simple) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse { + // Our schema doesn't include any ephemeral resource types, so it should be + // impossible to get in here. + panic("CloseEphemeralResource on provider that didn't declare any ephemeral resource types") +} + +func (s simple) CallFunction(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + // Our schema doesn't include any functions, so it should be impossible + // to get in here. + panic("CallFunction on provider that didn't declare any functions") +} + func (s simple) Close() error { return nil } diff --git a/internal/provider-terraform/main/main.go b/internal/provider-terraform/main/main.go index a50fef2d9b..bc27af9174 100644 --- a/internal/provider-terraform/main/main.go +++ b/internal/provider-terraform/main/main.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( diff --git a/internal/providercache/cached_provider.go b/internal/providercache/cached_provider.go index 0adbef21ba..284780214c 100644 --- a/internal/providercache/cached_provider.go +++ b/internal/providercache/cached_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/cached_provider_test.go b/internal/providercache/cached_provider_test.go index 5e6f29fb70..4e1441abe6 100644 --- a/internal/providercache/cached_provider_test.go +++ b/internal/providercache/cached_provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/dir.go b/internal/providercache/dir.go index f58184aa21..eab987e453 100644 --- a/internal/providercache/dir.go +++ b/internal/providercache/dir.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( @@ -72,6 +75,12 @@ func (d *Dir) BasePath() string { return filepath.Clean(d.baseDir) } +// WithPlatform creates a new dir with the provided platform based +// on this dir +func (d *Dir) WithPlatform(platform getproviders.Platform) *Dir { + return NewDirWithPlatform(d.baseDir, platform) +} + // AllAvailablePackages returns a description of all of the packages already // present in the directory. The cache entries are grouped by the provider // they relate to and then sorted by version precedence, with highest diff --git a/internal/providercache/dir_modify.go b/internal/providercache/dir_modify.go index 5ac79ba4f7..bcf307b61c 100644 --- a/internal/providercache/dir_modify.go +++ b/internal/providercache/dir_modify.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/dir_modify_test.go b/internal/providercache/dir_modify_test.go index 6e7821b575..c952635434 100644 --- a/internal/providercache/dir_modify_test.go +++ b/internal/providercache/dir_modify_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/dir_test.go b/internal/providercache/dir_test.go index 799a149e32..dedd14fa3c 100644 --- a/internal/providercache/dir_test.go +++ b/internal/providercache/dir_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/doc.go b/internal/providercache/doc.go index 7528cae6a7..2f27b0f1cc 100644 --- a/internal/providercache/doc.go +++ b/internal/providercache/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package providercache contains the logic for auto-installing providers from // packages obtained elsewhere, and for managing the local directories that // serve as global or single-configuration caches of those auto-installed diff --git a/internal/providercache/installer.go b/internal/providercache/installer.go index 0302c77b23..00907548c4 100644 --- a/internal/providercache/installer.go +++ b/internal/providercache/installer.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( "context" "fmt" + "log" "sort" "strings" @@ -36,6 +40,12 @@ type Installer struct { // version between different configurations on the same system. globalCacheDir *Dir + // globalCacheDirMayBreakDependencyLockFile allows a temporary exception to + // the rule that an entry in globalCacheDir can normally only be used if + // its validity is already confirmed by an entry in the dependency lock + // file. + globalCacheDirMayBreakDependencyLockFile bool + // builtInProviderTypes is an optional set of types that should be // considered valid to appear in the special terraform.io/builtin/... // namespace, which we use for providers that are built in to Terraform @@ -103,6 +113,19 @@ func (i *Installer) SetGlobalCacheDir(cacheDir *Dir) { i.globalCacheDir = cacheDir } +// SetGlobalCacheDirMayBreakDependencyLockFile activates or deactivates our +// temporary exception to the rule that the global cache directory can be used +// only when entries are confirmed by existing entries in the dependency lock +// file. +// +// If this is set then if we install a provider for the first time from the +// cache then the dependency lock file will include only the checksum from +// the package in the global cache, which means the lock file won't be portable +// to Terraform running on another operating system or CPU architecture. +func (i *Installer) SetGlobalCacheDirMayBreakDependencyLockFile(mayBreak bool) { + i.globalCacheDirMayBreakDependencyLockFile = mayBreak +} + // HasGlobalCacheDir returns true if someone has previously called // SetGlobalCacheDir to configure a global cache directory for this installer. func (i *Installer) HasGlobalCacheDir() bool { @@ -347,98 +370,169 @@ NeedProvider: // Step 3a: If our global cache already has this version available then // we'll just link it in. if cached := i.globalCacheDir.ProviderVersion(provider, version); cached != nil { - if cb := evts.LinkFromCacheBegin; cb != nil { - cb(provider, version, i.globalCacheDir.baseDir) - } - if _, err := cached.ExecutableFile(); err != nil { - err := fmt.Errorf("provider binary not found: %s", err) - errs[provider] = err - if cb := evts.LinkFromCacheFailure; cb != nil { - cb(provider, version, err) + // An existing cache entry is only an acceptable choice + // if there is already a lock file entry for this provider + // and the cache entry matches its checksums. + // + // If there was no lock file entry at all then we need to + // install the package for real so that we can lock as complete + // as possible a set of checksums for all of this provider's + // packages. + // + // If there was a lock file entry but the cache doesn't match + // it then we assume that the lock file checksums were only + // partially populated (e.g. from a local mirror where we can + // only see one package to checksum it) and so we'll fetch + // from upstream to see if the origin can give us a package + // that _does_ match. This might still not work out, but if + // it does then it allows us to avoid returning a checksum + // mismatch error. + acceptablePackage := false + if len(preferredHashes) != 0 { + var err error + acceptablePackage, err = cached.MatchesAnyHash(preferredHashes) + if err != nil { + // If we can't calculate the checksum for the cached + // package then we'll just treat it as a checksum failure. + acceptablePackage = false } - continue } - err := i.targetDir.LinkFromOtherCache(cached, preferredHashes) - if err != nil { - errs[provider] = err - if cb := evts.LinkFromCacheFailure; cb != nil { - cb(provider, version, err) - } - continue - } - // We'll fetch what we just linked to make sure it actually - // did show up there. - new := i.targetDir.ProviderVersion(provider, version) - if new == nil { - err := fmt.Errorf("after linking %s from provider cache at %s it is still not detected in the target directory; this is a bug in Terraform", provider, i.globalCacheDir.baseDir) - errs[provider] = err - if cb := evts.LinkFromCacheFailure; cb != nil { - cb(provider, version, err) - } - continue - } - - // The LinkFromOtherCache call above should've verified that - // the package matches one of the hashes previously recorded, - // if any. We'll now augment those hashes with one freshly - // calculated from the package we just linked, which allows - // the lock file to gradually transition to recording newer hash - // schemes when they become available. - var priorHashes []getproviders.Hash - if lock != nil && lock.Version() == version { - // If the version we're installing is identical to the - // one we previously locked then we'll keep all of the - // hashes we saved previously and add to it. Otherwise - // we'll be starting fresh, because each version has its - // own set of packages and thus its own hashes. - priorHashes = append(priorHashes, preferredHashes...) - - // NOTE: The behavior here is unfortunate when a particular - // provider version was already cached on the first time - // the current configuration requested it, because that - // means we don't currently get the opportunity to fetch - // and verify the checksums for the new package from - // upstream. That's currently unavoidable because upstream - // checksums are in the "ziphash" format and so we can't - // verify them against our cache directory's unpacked - // packages: we'd need to go fetch the package from the - // origin and compare against it, which would defeat the - // purpose of the global cache. + if !acceptablePackage && i.globalCacheDirMayBreakDependencyLockFile { + // The "may break dependency lock file" setting effectively + // means that we'll accept any matching package that's + // already in the cache, regardless of whether it matches + // what's in the dependency lock file. // - // If we fetch from upstream on the first encounter with - // a particular provider then we'll end up in the other - // codepath below where we're able to also include the - // checksums from the origin registry. - } - newHash, err := cached.Hash() - if err != nil { - err := fmt.Errorf("after linking %s from provider cache at %s, failed to compute a checksum for it: %s", provider, i.globalCacheDir.baseDir, err) - errs[provider] = err - if cb := evts.LinkFromCacheFailure; cb != nil { - cb(provider, version, err) - } - continue - } - // The hashes slice gets deduplicated in the lock file - // implementation, so we don't worry about potentially - // creating a duplicate here. - var newHashes []getproviders.Hash - newHashes = append(newHashes, priorHashes...) - newHashes = append(newHashes, newHash) - locks.SetProvider(provider, version, reqs[provider], newHashes) - if cb := evts.ProvidersLockUpdated; cb != nil { - // We want to ensure that newHash and priorHashes are - // sorted. newHash is a single value, so it's definitely - // sorted. priorHashes are pulled from the lock file, so - // are also already sorted. - cb(provider, version, []getproviders.Hash{newHash}, nil, priorHashes) + // That means two less-ideal situations might occur: + // - If this provider is not currently tracked in the lock + // file at all then after installation the lock file will + // only accept the package that was already present in + // the cache as a valid checksum. That means the generated + // lock file won't be portable to other operating systems + // or CPU architectures. + // - If the provider _is_ currently tracked in the lock file + // but the checksums there don't match what was in the + // cache then the LinkFromOtherCache call below will + // fail with a checksum error, and the user will need to + // either manually remove the entry from the lock file + // or remove the mismatching item from the cache, + // depending on which of these they prefer to use as the + // source of truth for the expected contents of the + // package. + // + // If the lock file already includes this provider and the + // cache entry matches one of the locked checksums then + // there's no problem, but in that case we wouldn't enter + // this branch because acceptablePackage would already be + // true from the check above. + log.Printf( + "[WARN] plugin_cache_may_break_dependency_lock_file: Using global cache dir package for %s v%s even though it doesn't match this configuration's dependency lock file", + provider.String(), version.String(), + ) + acceptablePackage = true } - if cb := evts.LinkFromCacheSuccess; cb != nil { - cb(provider, version, new.PackageDir) + // TODO: Should we emit an event through the events object + // for "there was an entry in the cache but we ignored it + // because the checksum didn't match"? We can't use + // LinkFromCacheFailure in that case because this isn't a + // failure. For now we'll just be quiet about it. + + if acceptablePackage { + if cb := evts.LinkFromCacheBegin; cb != nil { + cb(provider, version, i.globalCacheDir.baseDir) + } + if _, err := cached.ExecutableFile(); err != nil { + err := fmt.Errorf("provider binary not found: %s", err) + errs[provider] = err + if cb := evts.LinkFromCacheFailure; cb != nil { + cb(provider, version, err) + } + continue + } + + err := i.targetDir.LinkFromOtherCache(cached, preferredHashes) + if err != nil { + errs[provider] = err + if cb := evts.LinkFromCacheFailure; cb != nil { + cb(provider, version, err) + } + continue + } + // We'll fetch what we just linked to make sure it actually + // did show up there. + new := i.targetDir.ProviderVersion(provider, version) + if new == nil { + err := fmt.Errorf("after linking %s from provider cache at %s it is still not detected in the target directory; this is a bug in Terraform", provider, i.globalCacheDir.baseDir) + errs[provider] = err + if cb := evts.LinkFromCacheFailure; cb != nil { + cb(provider, version, err) + } + continue + } + + // The LinkFromOtherCache call above should've verified that + // the package matches one of the hashes previously recorded, + // if any. We'll now augment those hashes with one freshly + // calculated from the package we just linked, which allows + // the lock file to gradually transition to recording newer hash + // schemes when they become available. + var priorHashes []getproviders.Hash + if lock != nil && lock.Version() == version { + // If the version we're installing is identical to the + // one we previously locked then we'll keep all of the + // hashes we saved previously and add to it. Otherwise + // we'll be starting fresh, because each version has its + // own set of packages and thus its own hashes. + priorHashes = append(priorHashes, preferredHashes...) + + // NOTE: The behavior here is unfortunate when a particular + // provider version was already cached on the first time + // the current configuration requested it, because that + // means we don't currently get the opportunity to fetch + // and verify the checksums for the new package from + // upstream. That's currently unavoidable because upstream + // checksums are in the "ziphash" format and so we can't + // verify them against our cache directory's unpacked + // packages: we'd need to go fetch the package from the + // origin and compare against it, which would defeat the + // purpose of the global cache. + // + // If we fetch from upstream on the first encounter with + // a particular provider then we'll end up in the other + // codepath below where we're able to also include the + // checksums from the origin registry. + } + newHash, err := cached.Hash() + if err != nil { + err := fmt.Errorf("after linking %s from provider cache at %s, failed to compute a checksum for it: %s", provider, i.globalCacheDir.baseDir, err) + errs[provider] = err + if cb := evts.LinkFromCacheFailure; cb != nil { + cb(provider, version, err) + } + continue + } + // The hashes slice gets deduplicated in the lock file + // implementation, so we don't worry about potentially + // creating a duplicate here. + var newHashes []getproviders.Hash + newHashes = append(newHashes, priorHashes...) + newHashes = append(newHashes, newHash) + locks.SetProvider(provider, version, reqs[provider], newHashes) + if cb := evts.ProvidersLockUpdated; cb != nil { + // We want to ensure that newHash and priorHashes are + // sorted. newHash is a single value, so it's definitely + // sorted. priorHashes are pulled from the lock file, so + // are also already sorted. + cb(provider, version, []getproviders.Hash{newHash}, nil, priorHashes) + } + + if cb := evts.LinkFromCacheSuccess; cb != nil { + cb(provider, version, new.PackageDir) + } + continue // Don't need to do full install, then. } - continue // Don't need to do full install, then. } } @@ -491,7 +585,7 @@ NeedProvider: } new := installTo.ProviderVersion(provider, version) if new == nil { - err := fmt.Errorf("after installing %s it is still not detected in the target directory; this is a bug in Terraform", provider) + err := fmt.Errorf("after installing %s it is still not detected in %s; this is a bug in Terraform", provider, installTo.BasePath()) errs[provider] = err if cb := evts.FetchPackageFailure; cb != nil { cb(provider, version, err) @@ -521,6 +615,28 @@ NeedProvider: } continue } + + // We should now also find the package in the linkTo dir, which + // gives us the final value of "new" where the path points in to + // the true target directory, rather than possibly the global + // cache directory. + new = linkTo.ProviderVersion(provider, version) + if new == nil { + err := fmt.Errorf("after installing %s it is still not detected in %s; this is a bug in Terraform", provider, linkTo.BasePath()) + errs[provider] = err + if cb := evts.FetchPackageFailure; cb != nil { + cb(provider, version, err) + } + continue + } + if _, err := new.ExecutableFile(); err != nil { + err := fmt.Errorf("provider binary not found: %s", err) + errs[provider] = err + if cb := evts.FetchPackageFailure; cb != nil { + cb(provider, version, err) + } + continue + } } authResults[provider] = authResult diff --git a/internal/providercache/installer_events.go b/internal/providercache/installer_events.go index 8fc579af26..8b632a77ea 100644 --- a/internal/providercache/installer_events.go +++ b/internal/providercache/installer_events.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/installer_events_test.go b/internal/providercache/installer_events_test.go index cde5b7f0ab..224d70876e 100644 --- a/internal/providercache/installer_events_test.go +++ b/internal/providercache/installer_events_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( diff --git a/internal/providercache/installer_test.go b/internal/providercache/installer_test.go index bb71f1db24..0463a6b7c8 100644 --- a/internal/providercache/installer_test.go +++ b/internal/providercache/installer_test.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( "context" "encoding/json" + "fmt" "log" "net/http" "net/http/httptest" @@ -327,10 +331,7 @@ func TestEnsureProviderVersions(t *testing.T) { AuthResult string }{ "2.1.0", - // NOTE: With global cache enabled, the fetch - // goes into the global cache dir and - // we then to it from the local cache dir. - filepath.Join(inst.globalCacheDir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"), + filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"), "unauthenticated", }, }, @@ -338,7 +339,7 @@ func TestEnsureProviderVersions(t *testing.T) { } }, }, - "successful initial install of one provider through a warm global cache": { + "successful initial install of one provider through a warm global cache but without a lock file entry": { Source: getproviders.NewMockSource( []getproviders.PackageMeta{ { @@ -407,6 +408,484 @@ func TestEnsureProviderVersions(t *testing.T) { t.Errorf("wrong cache entry\n%s", diff) } }, + WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem { + return map[addrs.Provider][]*testInstallerEventLogItem{ + noProvider: { + { + Event: "PendingProviders", + Args: map[addrs.Provider]getproviders.VersionConstraints{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + beepProvider: { + { + Event: "QueryPackagesBegin", + Provider: beepProvider, + Args: struct { + Constraints string + Locked bool + }{">= 2.0.0", false}, + }, + { + Event: "QueryPackagesSuccess", + Provider: beepProvider, + Args: "2.1.0", + }, + // Existing cache entry is ineligible for linking because + // we have no lock file checksums to compare it to. + // Instead, we install from upstream and lock with + // whatever checksums we learn in that process. + { + Event: "FetchPackageMeta", + Provider: beepProvider, + Args: "2.1.0", + }, + { + Event: "FetchPackageBegin", + Provider: beepProvider, + Args: struct { + Version string + Location getproviders.PackageLocation + }{ + "2.1.0", + beepProviderDir, + }, + }, + { + Event: "ProvidersLockUpdated", + Provider: beepProvider, + Args: struct { + Version string + Local []getproviders.Hash + Signed []getproviders.Hash + Prior []getproviders.Hash + }{ + "2.1.0", + []getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="}, + nil, + nil, + }, + }, + { + Event: "FetchPackageSuccess", + Provider: beepProvider, + Args: struct { + Version string + LocalDir string + AuthResult string + }{ + "2.1.0", + filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"), + "unauthenticated", + }, + }, + }, + } + }, + }, + "successful initial install of one provider through a warm global cache and correct locked checksum": { + Source: getproviders.NewMockSource( + []getproviders.PackageMeta{ + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.0.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + }, + nil, + ), + LockFile: ` + # The existing cache entry is valid only if it matches a + # checksum already recorded in the lock file. + provider "example.com/foo/beep" { + version = "2.1.0" + constraints = ">= 1.0.0" + hashes = [ + "h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=", + ] + } + `, + Prepare: func(t *testing.T, inst *Installer, dir *Dir) { + globalCacheDirPath := tmpDir(t) + globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform) + _, err := globalCacheDir.InstallPackage( + context.Background(), + getproviders.PackageMeta{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + nil, + ) + if err != nil { + t.Fatalf("failed to populate global cache: %s", err) + } + inst.SetGlobalCacheDir(globalCacheDir) + }, + Mode: InstallNewProvidersOnly, + Reqs: getproviders.Requirements{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) { + if allCached := dir.AllAvailablePackages(); len(allCached) != 1 { + t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached)) + } + if allLocked := locks.AllProviders(); len(allLocked) != 1 { + t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked)) + } + + gotLock := locks.Provider(beepProvider) + wantLock := depsfile.NewProviderLock( + beepProvider, + getproviders.MustParseVersion("2.1.0"), + getproviders.MustParseVersionConstraints(">= 2.0.0"), + []getproviders.Hash{beepProviderHash}, + ) + if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" { + t.Errorf("wrong lock entry\n%s", diff) + } + + gotEntry := dir.ProviderLatestVersion(beepProvider) + wantEntry := &CachedProvider{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"), + } + if diff := cmp.Diff(wantEntry, gotEntry); diff != "" { + t.Errorf("wrong cache entry\n%s", diff) + } + }, + WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem { + return map[addrs.Provider][]*testInstallerEventLogItem{ + noProvider: { + { + Event: "PendingProviders", + Args: map[addrs.Provider]getproviders.VersionConstraints{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + }, + }, + beepProvider: { + { + Event: "QueryPackagesBegin", + Provider: beepProvider, + Args: struct { + Constraints string + Locked bool + }{">= 2.0.0", true}, + }, + { + Event: "QueryPackagesSuccess", + Provider: beepProvider, + Args: "2.1.0", + }, + { + Event: "LinkFromCacheBegin", + Provider: beepProvider, + Args: struct { + Version string + CacheRoot string + }{ + "2.1.0", + inst.globalCacheDir.BasePath(), + }, + }, + { + Event: "ProvidersLockUpdated", + Provider: beepProvider, + Args: struct { + Version string + Local []getproviders.Hash + Signed []getproviders.Hash + Prior []getproviders.Hash + }{ + "2.1.0", + []getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="}, + nil, + []getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="}, + }, + }, + { + Event: "LinkFromCacheSuccess", + Provider: beepProvider, + Args: struct { + Version string + LocalDir string + }{ + "2.1.0", + filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"), + }, + }, + }, + } + }, + }, + "successful initial install of one provider through a warm global cache with an incompatible checksum": { + Source: getproviders.NewMockSource( + []getproviders.PackageMeta{ + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.0.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + }, + nil, + ), + LockFile: ` + # This is approximating the awkward situation where the lock + # file was populated by someone who installed from a location + # other than the origin registry annd so the set of checksums + # is incomplete. In this case we can't prove that our cache + # entry is valid and so we silently ignore the cache entry + # and try to install from upstream anyway, in the hope that + # this will give us an opportunity to access the origin + # registry and get a checksum that works for the current + # platform. + provider "example.com/foo/beep" { + version = "2.1.0" + constraints = ">= 1.0.0" + hashes = [ + # NOTE: This is the correct checksum for the + # beepProviderDir package, but we're going to + # intentionally install from a different directory + # below so that the entry in the cache will not + # match this checksum. + "h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84=", + ] + } + `, + Prepare: func(t *testing.T, inst *Installer, dir *Dir) { + // This is another "beep provider" package directory that + // has a different checksum than the one in beepProviderDir. + // We're mimicking the situation where the lock file was + // originally built from beepProviderDir but the local system + // is running on a different platform and so its existing + // cache entry doesn't match the checksum. + beepProviderOtherPlatformDir := getproviders.PackageLocalDir("testdata/beep-provider-other-platform") + + globalCacheDirPath := tmpDir(t) + globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform) + _, err := globalCacheDir.InstallPackage( + context.Background(), + getproviders.PackageMeta{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderOtherPlatformDir, + }, + nil, + ) + if err != nil { + t.Fatalf("failed to populate global cache: %s", err) + } + inst.SetGlobalCacheDir(globalCacheDir) + }, + Mode: InstallNewProvidersOnly, + Reqs: getproviders.Requirements{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) { + if allCached := dir.AllAvailablePackages(); len(allCached) != 1 { + t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached)) + } + if allLocked := locks.AllProviders(); len(allLocked) != 1 { + t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked)) + } + + gotLock := locks.Provider(beepProvider) + wantLock := depsfile.NewProviderLock( + beepProvider, + getproviders.MustParseVersion("2.1.0"), + getproviders.MustParseVersionConstraints(">= 2.0.0"), + []getproviders.Hash{beepProviderHash}, + ) + if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" { + t.Errorf("wrong lock entry\n%s", diff) + } + + gotEntry := dir.ProviderLatestVersion(beepProvider) + wantEntry := &CachedProvider{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"), + } + if diff := cmp.Diff(wantEntry, gotEntry); diff != "" { + t.Errorf("wrong cache entry\n%s", diff) + } + }, + WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem { + return map[addrs.Provider][]*testInstallerEventLogItem{ + noProvider: { + { + Event: "PendingProviders", + Args: map[addrs.Provider]getproviders.VersionConstraints{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + }, + { + Event: "ProvidersFetched", + Args: map[addrs.Provider]*getproviders.PackageAuthenticationResult{ + beepProvider: nil, + }, + }, + }, + beepProvider: { + { + Event: "QueryPackagesBegin", + Provider: beepProvider, + Args: struct { + Constraints string + Locked bool + }{">= 2.0.0", true}, + }, + { + Event: "QueryPackagesSuccess", + Provider: beepProvider, + Args: "2.1.0", + }, + { + Event: "FetchPackageMeta", + Provider: beepProvider, + Args: "2.1.0", + }, + { + Event: "FetchPackageBegin", + Provider: beepProvider, + Args: struct { + Version string + Location getproviders.PackageLocation + }{ + "2.1.0", + beepProviderDir, + }, + }, + { + Event: "ProvidersLockUpdated", + Provider: beepProvider, + Args: struct { + Version string + Local []getproviders.Hash + Signed []getproviders.Hash + Prior []getproviders.Hash + }{ + "2.1.0", + []getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="}, + nil, + []getproviders.Hash{"h1:2y06Ykj0FRneZfGCTxI9wRTori8iB7ZL5kQ6YyEnh84="}, + }, + }, + { + Event: "FetchPackageSuccess", + Provider: beepProvider, + Args: struct { + Version string + LocalDir string + AuthResult string + }{ + "2.1.0", + filepath.Join(dir.BasePath(), "/example.com/foo/beep/2.1.0/bleep_bloop"), + "unauthenticated", + }, + }, + }, + } + }, + }, + "successful initial install of one provider through a warm global cache without a lock file entry but allowing the cache to break the lock file": { + Source: getproviders.NewMockSource( + []getproviders.PackageMeta{ + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.0.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + }, + nil, + ), + LockFile: ` + # (intentionally empty) + `, + Prepare: func(t *testing.T, inst *Installer, dir *Dir) { + globalCacheDirPath := tmpDir(t) + globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform) + _, err := globalCacheDir.InstallPackage( + context.Background(), + getproviders.PackageMeta{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + nil, + ) + if err != nil { + t.Fatalf("failed to populate global cache: %s", err) + } + inst.SetGlobalCacheDir(globalCacheDir) + inst.SetGlobalCacheDirMayBreakDependencyLockFile(true) + }, + Mode: InstallNewProvidersOnly, + Reqs: getproviders.Requirements{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) { + if allCached := dir.AllAvailablePackages(); len(allCached) != 1 { + t.Errorf("wrong number of cache directory entries; want only one\n%s", spew.Sdump(allCached)) + } + if allLocked := locks.AllProviders(); len(allLocked) != 1 { + t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked)) + } + + gotLock := locks.Provider(beepProvider) + wantLock := depsfile.NewProviderLock( + beepProvider, + getproviders.MustParseVersion("2.1.0"), + getproviders.MustParseVersionConstraints(">= 2.0.0"), + []getproviders.Hash{beepProviderHash}, + ) + if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" { + t.Errorf("wrong lock entry\n%s", diff) + } + + gotEntry := dir.ProviderLatestVersion(beepProvider) + wantEntry := &CachedProvider{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + PackageDir: filepath.Join(dir.BasePath(), "example.com/foo/beep/2.1.0/bleep_bloop"), + } + if diff := cmp.Diff(wantEntry, gotEntry); diff != "" { + t.Errorf("wrong cache entry\n%s", diff) + } + }, WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem { return map[addrs.Provider][]*testInstallerEventLogItem{ noProvider: { @@ -472,6 +951,143 @@ func TestEnsureProviderVersions(t *testing.T) { } }, }, + "failing install of one provider through a warm global cache with an incorrect locked checksum while allowing the cache to break the lock file": { + Source: getproviders.NewMockSource( + []getproviders.PackageMeta{ + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.0.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + { + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + }, + nil, + ), + LockFile: ` + # The existing cache entry is valid only if it matches a + # checksum already recorded in the lock file, but this + # test is overriding that rule using a special setting. + provider "example.com/foo/beep" { + version = "2.1.0" + constraints = ">= 1.0.0" + hashes = [ + "h1:wrong-not-matchy", + ] + } + `, + Prepare: func(t *testing.T, inst *Installer, dir *Dir) { + globalCacheDirPath := tmpDir(t) + globalCacheDir := NewDirWithPlatform(globalCacheDirPath, fakePlatform) + _, err := globalCacheDir.InstallPackage( + context.Background(), + getproviders.PackageMeta{ + Provider: beepProvider, + Version: getproviders.MustParseVersion("2.1.0"), + TargetPlatform: fakePlatform, + Location: beepProviderDir, + }, + nil, + ) + if err != nil { + t.Fatalf("failed to populate global cache: %s", err) + } + inst.SetGlobalCacheDir(globalCacheDir) + inst.SetGlobalCacheDirMayBreakDependencyLockFile(true) + }, + Mode: InstallNewProvidersOnly, + Reqs: getproviders.Requirements{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + Check: func(t *testing.T, dir *Dir, locks *depsfile.Locks) { + if allCached := dir.AllAvailablePackages(); len(allCached) != 0 { + t.Errorf("wrong number of cache directory entries; want none\n%s", spew.Sdump(allCached)) + } + if allLocked := locks.AllProviders(); len(allLocked) != 1 { + t.Errorf("wrong number of provider lock entries; want only one\n%s", spew.Sdump(allLocked)) + } + + gotLock := locks.Provider(beepProvider) + wantLock := depsfile.NewProviderLock( + // The lock file entry hasn't changed because the cache + // entry didn't match the existing lock file entry. + beepProvider, + getproviders.MustParseVersion("2.1.0"), + getproviders.MustParseVersionConstraints(">= 1.0.0"), + []getproviders.Hash{"h1:wrong-not-matchy"}, + ) + if diff := cmp.Diff(wantLock, gotLock, depsfile.ProviderLockComparer); diff != "" { + t.Errorf("wrong lock entry\n%s", diff) + } + + // The provider wasn't installed into the local cache directory + // because that would make the local cache mismatch the + // lock file. + gotEntry := dir.ProviderLatestVersion(beepProvider) + wantEntry := (*CachedProvider)(nil) + if diff := cmp.Diff(wantEntry, gotEntry); diff != "" { + t.Errorf("wrong cache entry\n%s", diff) + } + }, + WantErr: `doesn't match any of the checksums`, + WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem { + return map[addrs.Provider][]*testInstallerEventLogItem{ + noProvider: { + { + Event: "PendingProviders", + Args: map[addrs.Provider]getproviders.VersionConstraints{ + beepProvider: getproviders.MustParseVersionConstraints(">= 2.0.0"), + }, + }, + }, + beepProvider: { + { + Event: "QueryPackagesBegin", + Provider: beepProvider, + Args: struct { + Constraints string + Locked bool + }{">= 2.0.0", true}, + }, + { + Event: "QueryPackagesSuccess", + Provider: beepProvider, + Args: "2.1.0", + }, + { + Event: "LinkFromCacheBegin", + Provider: beepProvider, + Args: struct { + Version string + CacheRoot string + }{ + "2.1.0", + inst.globalCacheDir.BasePath(), + }, + }, + { + Event: "LinkFromCacheFailure", + Provider: beepProvider, + Args: struct { + Version string + Error string + }{ + "2.1.0", + fmt.Sprintf( + "the provider cache at %s has a copy of example.com/foo/beep 2.1.0 that doesn't match any of the checksums recorded in the dependency lock file", + dir.BasePath(), + ), + }, + }, + }, + } + }, + }, "successful reinstall of one previously-locked provider": { Source: getproviders.NewMockSource( []getproviders.PackageMeta{ @@ -1402,7 +2018,7 @@ func TestEnsureProviderVersions(t *testing.T) { beepProvider: getproviders.MustParseVersionConstraints(">= 1.0.0"), }, WantErr: `some providers could not be installed: -- example.com/foo/beep: the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://www.terraform.io/language/provider-checksum-verification`, +- example.com/foo/beep: the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://developer.hashicorp.com/terraform/language/files/dependency-lock#checksum-verification`, WantEvents: func(inst *Installer, dir *Dir) map[addrs.Provider][]*testInstallerEventLogItem { return map[addrs.Provider][]*testInstallerEventLogItem{ noProvider: { @@ -1448,7 +2064,7 @@ func TestEnsureProviderVersions(t *testing.T) { Error string }{ "1.0.0", - `the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://www.terraform.io/language/provider-checksum-verification`, + `the local package for example.com/foo/beep 1.0.0 doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://developer.hashicorp.com/terraform/language/files/dependency-lock#checksum-verification`, }, }, }, @@ -1602,7 +2218,7 @@ func TestEnsureProviderVersions(t *testing.T) { inst := NewInstaller(outputDir, source) if test.Prepare != nil { test.Prepare(t, inst, outputDir) - } + } /* boop */ locks, lockDiags := depsfile.LoadLocksFromBytes([]byte(test.LockFile), "test.lock.hcl") if lockDiags.HasErrors() { @@ -1635,8 +2251,8 @@ func TestEnsureProviderVersions(t *testing.T) { if test.WantErr != "" { if instErr == nil { t.Errorf("succeeded; want error\nwant: %s", test.WantErr) - } else if got, want := instErr.Error(), test.WantErr; got != want { - t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } else if got, want := instErr.Error(), test.WantErr; !strings.Contains(got, want) { + t.Errorf("wrong error\ngot: %s\nwant substring: %s", got, want) } } else if instErr != nil { t.Errorf("unexpected error\ngot: %s", instErr.Error()) diff --git a/internal/providercache/package_install.go b/internal/providercache/package_install.go index 655a441d81..78de3ad064 100644 --- a/internal/providercache/package_install.go +++ b/internal/providercache/package_install.go @@ -1,10 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providercache import ( "context" "fmt" - "io/ioutil" - "net/http" + "net/url" "os" "path/filepath" @@ -23,54 +25,41 @@ import ( var unzip = getter.ZipDecompressor{} func installFromHTTPURL(ctx context.Context, meta getproviders.PackageMeta, targetDir string, allowedHashes []getproviders.Hash) (*getproviders.PackageAuthenticationResult, error) { - url := meta.Location.String() + urlStr := meta.Location.String() // When we're installing from an HTTP URL we expect the URL to refer to // a zip file. We'll fetch that into a temporary file here and then // delegate to installFromLocalArchive below to actually extract it. - // (We're not using go-getter here because its HTTP getter has a bunch - // of extraneous functionality we don't need or want, like indirection - // through X-Terraform-Get header, attempting partial fetches for - // files that already exist, etc.) + httpGetter := getter.HttpGetter{ + Client: httpclient.New(), + Netrc: true, + XTerraformGetDisabled: true, + } - httpClient := httpclient.New() - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + urlObj, err := url.Parse(urlStr) if err != nil { + // We don't expect to get non-HTTP locations here because we're + // using the registry source, so this seems like a bug in the + // registry source. return nil, fmt.Errorf("invalid provider download request: %s", err) } - resp, err := httpClient.Do(req) + f, err := os.CreateTemp("", "terraform-provider") + if err != nil { + return nil, fmt.Errorf("failed to open temporary file to download from %s: %w", urlStr, err) + } + defer f.Close() + defer os.Remove(f.Name()) + + archiveFilename := f.Name() + err = httpGetter.GetFile(archiveFilename, urlObj) if err != nil { if ctx.Err() == context.Canceled { // "context canceled" is not a user-friendly error message, // so we'll return a more appropriate one here. return nil, fmt.Errorf("provider download was interrupted") } - return nil, fmt.Errorf("%s: %w", getproviders.HostFromRequest(req), err) + return nil, fmt.Errorf("%s: %w", urlObj.Host, err) } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unsuccessful request to %s: %s", url, resp.Status) - } - - f, err := ioutil.TempFile("", "terraform-provider") - if err != nil { - return nil, fmt.Errorf("failed to open temporary file to download from %s", url) - } - defer f.Close() - defer os.Remove(f.Name()) - - // We'll borrow go-getter's "cancelable copy" implementation here so that - // the download can potentially be interrupted partway through. - n, err := getter.Copy(ctx, f, resp.Body) - if err == nil && n < resp.ContentLength { - err = fmt.Errorf("incorrect response size: expected %d bytes, but got %d bytes", resp.ContentLength, n) - } - if err != nil { - return nil, err - } - - archiveFilename := f.Name() localLocation := getproviders.PackageLocalArchive(archiveFilename) var authResult *getproviders.PackageAuthenticationResult @@ -117,7 +106,7 @@ func installFromLocalArchive(ctx context.Context, meta getproviders.PackageMeta, ) } else if !matches { return authResult, fmt.Errorf( - "the current package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file; for more information: https://www.terraform.io/language/provider-checksum-verification", + "the current package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file; for more information: https://developer.hashicorp.com/terraform/language/files/dependency-lock#checksum-verification", meta.Provider, meta.Version, ) } @@ -125,6 +114,14 @@ func installFromLocalArchive(ctx context.Context, meta getproviders.PackageMeta, filename := meta.Location.String() + // NOTE: We're not checking whether there's already a directory at + // targetDir with some files in it. Packages are supposed to be immutable + // and therefore we'll just be overwriting all of the existing files with + // their same contents unless something unusual is happening. If something + // unusual _is_ happening then this will produce something that doesn't + // match the allowed hashes and so our caller should catch that after + // we return if so. + err := unzip.Decompress(targetDir, filename, true, 0000) if err != nil { return authResult, err @@ -199,7 +196,7 @@ func installFromLocalDir(ctx context.Context, meta getproviders.PackageMeta, tar ) } else if !matches { return authResult, fmt.Errorf( - "the local package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://www.terraform.io/language/provider-checksum-verification", + "the local package for %s %s doesn't match any of the checksums previously recorded in the dependency lock file (this might be because the available checksums are for packages targeting different platforms); for more information: https://developer.hashicorp.com/terraform/language/files/dependency-lock#checksum-verification", meta.Provider, meta.Version, ) } diff --git a/internal/providercache/testdata/beep-provider-other-platform/terraform-provider-beep b/internal/providercache/testdata/beep-provider-other-platform/terraform-provider-beep new file mode 100644 index 0000000000..18929cd34b --- /dev/null +++ b/internal/providercache/testdata/beep-provider-other-platform/terraform-provider-beep @@ -0,0 +1,7 @@ +This is not a real provider executable. It's just here to give the installer +something to copy in some of our installer test cases. + +This must be different than the file of the same name in the sibling directory +"beep-provider", because we're using this to stand in for a valid package +that was built for a different platform than the one whose checksum is recorded +in the lock file. diff --git a/internal/providers/addressed_types.go b/internal/providers/addressed_types.go index 8efa82ca3e..900cfdb5d9 100644 --- a/internal/providers/addressed_types.go +++ b/internal/providers/addressed_types.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providers import ( diff --git a/internal/providers/addressed_types_test.go b/internal/providers/addressed_types_test.go index 3bb4766705..51bfd7352e 100644 --- a/internal/providers/addressed_types_test.go +++ b/internal/providers/addressed_types_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providers import ( diff --git a/internal/providers/doc.go b/internal/providers/doc.go index 39aa1de60f..d9f472dd40 100644 --- a/internal/providers/doc.go +++ b/internal/providers/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package providers contains the interface and primary types required to // implement a Terraform resource provider. package providers diff --git a/internal/providers/ephemeral.go b/internal/providers/ephemeral.go new file mode 100644 index 0000000000..0a4df9f31f --- /dev/null +++ b/internal/providers/ephemeral.go @@ -0,0 +1,195 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providers + +import ( + "time" + + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +// OpenEphemeralResourceRequest represents the arguments for the OpenEphemeralResource +// operation on a provider. +type OpenEphemeralResourceRequest struct { + // TypeName is the type of ephemeral resource to open. This should + // only be one of the type names previously reported in the provider's + // schema. + TypeName string + + // Config is an object-typed value representing the configuration for + // the ephemeral resource instance that the caller is trying to open. + // + // The object type of this value always conforms to the resource type + // schema's implied type, and uses null values to represent attributes + // that were not explicitly assigned in the configuration block. + // Computed-only attributes are always null in the configuration, because + // they can be set only in the response. + Config cty.Value + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities +} + +// OpenEphemeralResourceRequest represents the response from an OpenEphemeralResource +// operation on a provider. +type OpenEphemeralResourceResponse struct { + // Deferred, if present, signals that the provider doesn't have enough + // information to open this ephemeral resource instance. + // + // This implies that any other side-effect-performing object must have its + // planning deferred if its planning operation indirectly depends on this + // ephemeral resource result. For example, if a provider configuration + // refers to an ephemeral resource whose opening is deferred then the + // affected provider configuration must not be instantiated and any resource + // instances that belong to it must have their planning immediately + // deferred. + Deferred *Deferred + + // Result is an object-typed value representing the newly-opened session + // with the opened ephemeral object. + // + // The object type of this value always conforms to the resource type + // schema's implied type. Unknown values are forbidden unless the Deferred + // field is set, in which case the Result represents the provider's best + // approximation of the final object using unknown values in any location + // where a final value cannot be predicted. + Result cty.Value + + // Private is any internal data needed by the provider to perform a + // subsequent [Interface.CloseEphemeralResource] request for the same object. The + // provider may choose any encoding format to represent the needed data, + // because Terraform Core treats this field as opaque. + // + // Providers should aim to keep this data relatively compact to minimize + // overhead. Although Terraform Core does not enforce a specific limit just + // for this field, it would be very unusual for the internal context to be + // more than 256 bytes in size, and in most cases it should be on the order + // of only tens of bytes. For example, a lease ID for the remote system is a + // reasonable thing to encode here. + // + // Because ephemeral resource instances never outlive a single Terraform + // Core phase, it's guaranteed that a CloseEphemeralResource request will be + // received by exactly the same plugin instance that returned this value, + // and so it's valid for this to refer to in-memory state belonging to the + // provider instance. + Private []byte + + // RenewAt, if non-zero, signals that the opened object has an inherent + // expiration time and so must be "renewed" if Terraform needs to use it + // beyond that expiration time. + // + // If a provider sets this field then it may receive a subsequent + // Interface.RenewEphemeralResource call, if Terraform expects to need the + // object beyond the expiration time. + RenewAt time.Time + + // Diagnostics describes any problems encountered while opening the + // ephemeral resource. If this contains errors then the other response + // fields must be assumed invalid. + Diagnostics tfdiags.Diagnostics +} + +// EphemeralRenew describes when and how Terraform Core must request renewal +// of an ephemeral resource instance in order to continue using it. +type EphemeralRenew struct { + // RenewAt is the deadline before which Terraform must renew the + // ephemeral resource instance. + RenewAt time.Time + + // Private is any internal data needed by the provider to + // perform a subsequent [Interface.RenewEphemeralResource] request. The provider + // may choose any encoding format to represent the needed data, because + // Terraform Core treats this field as opaque. + // + // Providers should aim to keep this data relatively compact to minimize + // overhead. Although Terraform Core does not enforce a specific limit + // just for this field, it would be very unusual for the internal context + // to be more than 256 bytes in size, and in most cases it should be + // on the order of only tens of bytes. For example, a lease ID for the + // remote system is a reasonable thing to encode here. + // + // Because ephemeral resource instances never outlive a single Terraform + // Core phase, it's guaranteed that a RenewEphemeralResource request will be + // received by exactly the same plugin instance that previously handled + // the OpenEphemeralResource or RenewEphemeralResource request that produced this internal + // context, and so it's valid for this to refer to in-memory state in the + // provider object. + Private []byte +} + +// RenewEphemeralResourceRequest represents the arguments for the RenewEphemeralResource +// operation on a provider. +type RenewEphemeralResourceRequest struct { + // TypeName is the type of ephemeral resource being renewed. This should + // only be one of the type names previously sent in a successful + // [OpenEphemeralResourceRequest]. + TypeName string + + // Private echoes verbatim the value from the field of the same + // name from the most recent [EphemeralRenew] object, received from either + // an [OpenEphemeralResourceResponse] or a [RenewEphemeralResourceResponse] object. + Private []byte +} + +// RenewEphemeralResourceRequest represents the response from a RenewEphemeralResource +// operation on a provider. +type RenewEphemeralResourceResponse struct { + // RenewAt, if non-zero, describes a new expiration deadline for the + // object, possibly causing a further call to [Interface.RenewEphemeralResource] + // if Terraform needs to exceed the updated deadline. + // + // If this is not set then Terraform Core will not make any further + // renewal requests for the remaining life of the object. + RenewAt time.Time + + // Private is any internal data needed by the provider to + // perform a subsequent [Interface.RenewEphemeralResource] request. The provider + // may choose any encoding format to represent the needed data, because + // Terraform Core treats this field as opaque. + Private []byte + + // Diagnostics describes any problems encountered while renewing the + // ephemeral resource instance. If this contains errors then the other + // response fields must be assumed invalid. + // + // Because renewals happen asynchronously from other uses of the + // ephemeral object, it's unspecified whether a renewal error will block + // any specific usage of the object. For example, a request using the + // object might already be in progress when a renewal error occurs, + // in which case that other request might also fail trying to use a + // now-invalid object, or it might by chance succeed in completing its + // operation before the ephemeral object truly expires. + Diagnostics tfdiags.Diagnostics +} + +// CloseEphemeralResourceRequest represents the arguments for the CloseEphemeralResource +// operation on a provider. +type CloseEphemeralResourceRequest struct { + // TypeName is the type of ephemeral resource being closed. This should + // only be one of the type names previously sent in a successful + // [OpenEphemeralResourceRequest]. + TypeName string + + // Private echoes verbatim the value from the field of the same + // name from the corresponding [OpenEphemeralResourceResponse] object. + Private []byte +} + +// CloseEphemeralResourceRequest represents the response from a CloseEphemeralResource +// operation on a provider. +type CloseEphemeralResourceResponse struct { + // Diagnostics describes any problems encountered while closing the + // ephemeral resource instance. If this contains errors then the other + // response fields must be assumed invalid. + // + // If closing an ephemeral resource instance fails then it's unspecified + // whether a corresponding remote object remains valid or not. + // + // Providers should make a best effort to treat the closure of an + // already-expired ephemeral object as a success in order to exhibit + // idemponent behavior for closing, but some remote systems do not allow + // distinguishing that case from other error conditions. + Diagnostics tfdiags.Diagnostics +} diff --git a/internal/providers/factory.go b/internal/providers/factory.go index 52700633ba..d0478c4950 100644 --- a/internal/providers/factory.go +++ b/internal/providers/factory.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providers // Factory is a function type that creates a new instance of a resource @@ -17,47 +20,3 @@ func FactoryFixed(p Interface) Factory { return p, nil } } - -// ProviderHasResource is a helper that requests schema from the given provider -// and checks if it has a resource type of the given name. -// -// This function is more expensive than it may first appear since it must -// retrieve the entire schema from the underlying provider, and so it should -// be used sparingly and especially not in tight loops. -// -// Since retrieving the provider may fail (e.g. if the provider is accessed -// over an RPC channel that has operational problems), this function will -// return false if the schema cannot be retrieved, under the assumption that -// a subsequent call to do anything with the resource type would fail -// anyway. -func ProviderHasResource(provider Interface, typeName string) bool { - resp := provider.GetProviderSchema() - if resp.Diagnostics.HasErrors() { - return false - } - - _, exists := resp.ResourceTypes[typeName] - return exists -} - -// ProviderHasDataSource is a helper that requests schema from the given -// provider and checks if it has a data source of the given name. -// -// This function is more expensive than it may first appear since it must -// retrieve the entire schema from the underlying provider, and so it should -// be used sparingly and especially not in tight loops. -// -// Since retrieving the provider may fail (e.g. if the provider is accessed -// over an RPC channel that has operational problems), this function will -// return false if the schema cannot be retrieved, under the assumption that -// a subsequent call to do anything with the data source would fail -// anyway. -func ProviderHasDataSource(provider Interface, dataSourceName string) bool { - resp := provider.GetProviderSchema() - if resp.Diagnostics.HasErrors() { - return false - } - - _, exists := resp.DataSources[dataSourceName] - return exists -} diff --git a/internal/providers/functions.go b/internal/providers/functions.go new file mode 100644 index 0000000000..33fe846ec1 --- /dev/null +++ b/internal/providers/functions.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providers + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang" +) + +type FunctionDecl struct { + Parameters []FunctionParam + VariadicParameter *FunctionParam + ReturnType cty.Type + + Description string + DescriptionKind configschema.StringKind + Summary string + DeprecationMessage string +} + +type FunctionParam struct { + Name string // Only for documentation and UI, because arguments are positional + Type cty.Type + + AllowNullValue bool + AllowUnknownValues bool + + Description string + DescriptionKind configschema.StringKind +} + +// BuildFunction takes a factory function which will return an unconfigured +// instance of the provider this declaration belongs to and returns a +// cty function that is ready to be called against that provider. +// +// The given name must be the name under which the provider originally +// registered this declaration, or the returned function will try to use an +// invalid name, leading to errors or undefined behavior. +// +// If the given factory returns an instance of any provider other than the +// one the declaration belongs to, or returns a _configured_ instance of +// the provider rather than an unconfigured one, the behavior of the returned +// function is undefined. +// +// Although not functionally required, callers should ideally pass a factory +// function that either retrieves already-running plugins or memoizes the +// plugins it returns so that many calls to functions in the same provider +// will not incur a repeated startup cost. +// +// The resTable argument is a shared instance of *FunctionResults, used to +// check the result values from each function call. +func (d FunctionDecl) BuildFunction(providerAddr addrs.Provider, name string, resTable *lang.FunctionResults, factory func() (Interface, error)) function.Function { + + var params []function.Parameter + var varParam *function.Parameter + if len(d.Parameters) > 0 { + params = make([]function.Parameter, len(d.Parameters)) + for i, paramDecl := range d.Parameters { + params[i] = paramDecl.ctyParameter() + } + } + if d.VariadicParameter != nil { + cp := d.VariadicParameter.ctyParameter() + varParam = &cp + } + + return function.New(&function.Spec{ + Type: function.StaticReturnType(d.ReturnType), + Params: params, + VarParam: varParam, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + for i, arg := range args { + var param function.Parameter + if i < len(params) { + param = params[i] + } else { + param = *varParam + } + + // We promise provider developers that we won't pass them even + // _nested_ unknown values unless they opt in to dealing with + // them. + if !param.AllowUnknown { + if !arg.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + } + + // We also ensure that null values are never passed where they + // are not expected. + if !param.AllowNull { + if arg.IsNull() { + return cty.UnknownVal(retType), fmt.Errorf("argument %q cannot be null", param.Name) + } + } + } + + provider, err := factory() + if err != nil { + return cty.UnknownVal(retType), fmt.Errorf("failed to launch provider plugin: %s", err) + } + + resp := provider.CallFunction(CallFunctionRequest{ + FunctionName: name, + Arguments: args, + }) + if resp.Err != nil { + return cty.UnknownVal(retType), resp.Err + } + + if resp.Result == cty.NilVal { + return cty.UnknownVal(retType), fmt.Errorf("provider returned no result and no errors") + } + + if resTable != nil { + err = resTable.CheckPriorProvider(providerAddr, name, args, resp.Result) + if err != nil { + return cty.UnknownVal(retType), err + } + } + + return resp.Result, nil + }, + }) +} + +func (p *FunctionParam) ctyParameter() function.Parameter { + return function.Parameter{ + Name: p.Name, + Type: p.Type, + AllowNull: p.AllowNullValue, + + // While the function may not allow DynamicVal, a `null` literal is + // also dynamically typed. If the parameter is dynamically typed, then + // we must allow this for `null` to pass through. + AllowDynamicType: p.Type == cty.DynamicPseudoType, + + // NOTE: Setting this is not a sufficient implementation of + // FunctionParam.AllowUnknownValues, because cty's function + // system only blocks passing in a top-level unknown, but + // our provider-contributed functions API promises to only + // pass wholly-known values unless AllowUnknownValues is true. + // The function implementation itself must also check this. + AllowUnknown: p.AllowUnknownValues, + } +} diff --git a/internal/providers/mock.go b/internal/providers/mock.go new file mode 100644 index 0000000000..f15e4e1ca4 --- /dev/null +++ b/internal/providers/mock.go @@ -0,0 +1,411 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providers + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/lang/ephemeral" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var _ Interface = (*Mock)(nil) + +// Mock is a mock provider that can be used by Terraform authors during test +// executions. +// +// The mock provider wraps an instance of an actual provider so it can return +// the correct schema and validate the configuration accurately. But, it +// intercepts calls to create resources or read data sources and instead reads +// and write the data to/from the state directly instead of needing to +// communicate with actual cloud providers. +// +// Callers can also specify the configs.MockData field to provide some preset +// data to return for any computed fields within the provider schema. The +// provider will make up random / junk data for any computed fields for which +// preset data is not available. +// +// This is distinct from the testing.MockProvider, which is a mock provider +// that is used by the Terraform core itself to test it's own behavior. +type Mock struct { + Provider Interface + Data *configs.MockData + + schema *GetProviderSchemaResponse + identitySchema *GetResourceIdentitySchemasResponse +} + +func (m *Mock) GetProviderSchema() GetProviderSchemaResponse { + if m.schema == nil { + // Cache the schema, it's not changing. + schema := m.Provider.GetProviderSchema() + + // Override the provider schema with the constant mock provider schema. + // This is empty at the moment, check configs/mock_provider.go for the + // actual schema. + // + // The GetProviderSchemaResponse is returned by value, so it should be + // safe for us to modify directly, without affecting any shared state + // that could be in use elsewhere. + schema.Provider = Schema{ + Version: schema.Provider.Version, + Body: nil, // Empty - we support no blocks or attributes in mock provider configurations. + } + + // Note, we leave the resource and data source schemas as they are since + // we want to be able to validate those configurations against the real + // provider schemas. + + m.schema = &schema + } + return *m.schema +} + +func (m *Mock) GetResourceIdentitySchemas() GetResourceIdentitySchemasResponse { + if m.identitySchema == nil { + // Cache the schema, it's not changing. + schema := m.Provider.GetResourceIdentitySchemas() + + m.identitySchema = &schema + } + return *m.identitySchema +} + +func (m *Mock) ValidateProviderConfig(request ValidateProviderConfigRequest) (response ValidateProviderConfigResponse) { + // The config for the mocked providers is consistent, and validated when we + // parse the HCL directly. So we'll just make no change here. + return ValidateProviderConfigResponse{ + PreparedConfig: request.Config, + } +} + +func (m *Mock) ValidateResourceConfig(request ValidateResourceConfigRequest) ValidateResourceConfigResponse { + // We'll just pass this through to the underlying provider. The mock should + // support the same resource syntax as the original provider and we can call + // validate without needing to configure the provider first. + return m.Provider.ValidateResourceConfig(request) +} + +func (m *Mock) ValidateDataResourceConfig(request ValidateDataResourceConfigRequest) ValidateDataResourceConfigResponse { + // We'll just pass this through to the underlying provider. The mock should + // support the same data source syntax as the original provider and we can + // call validate without needing to configure the provider first. + return m.Provider.ValidateDataResourceConfig(request) +} + +func (m *Mock) UpgradeResourceState(request UpgradeResourceStateRequest) (response UpgradeResourceStateResponse) { + // We can't do this from a mocked provider, so we just return whatever state + // is in the request back unchanged. + + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + resource, exists := schema.ResourceTypes[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a resource type doesn't exist. + panic(fmt.Errorf("failed to retrieve schema for resource %s", request.TypeName)) + } + + schemaType := resource.Body.ImpliedType() + + var value cty.Value + var err error + + switch { + case request.RawStateFlatmap != nil: + value, err = hcl2shim.HCL2ValueFromFlatmap(request.RawStateFlatmap, schemaType) + case len(request.RawStateJSON) > 0: + value, err = ctyjson.Unmarshal(request.RawStateJSON, schemaType) + } + + if err != nil { + // Generally, we shouldn't get an error here. The mocked providers are + // only used in tests, and we can't use different versions of providers + // within/between tests so the types should always match up. As such, + // we're not gonna return a super detailed error here. + response.Diagnostics = response.Diagnostics.Append(err) + return response + } + response.UpgradedState = ephemeral.StripWriteOnlyAttributes(value, resource.Body) + return response +} + +func (m *Mock) UpgradeResourceIdentity(request UpgradeResourceIdentityRequest) (response UpgradeResourceIdentityResponse) { + // We can't do this from a mocked provider, so we just return whatever identity + // is in the request back unchanged. + + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + resource, exists := schema.ResourceTypes[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a resource type doesn't exist. + panic(fmt.Errorf("failed to retrieve identity schema for resource %s", request.TypeName)) + } + + schemaType := resource.Identity.ImpliedType() + value, err := ctyjson.Unmarshal(request.RawIdentityJSON, schemaType) + + if err != nil { + // Generally, we shouldn't get an error here. The mocked providers are + // only used in tests, and we can't use different versions of providers + // within/between tests so the types should always match up. As such, + // we're not gonna return a super detailed error here. + response.Diagnostics = response.Diagnostics.Append(err) + return response + } + response.UpgradedIdentity = value + return response +} + +func (m *Mock) ConfigureProvider(request ConfigureProviderRequest) (response ConfigureProviderResponse) { + // Do nothing here, we don't have anything to configure within the mocked + // providers. We don't want to call the original providers from here as + // they may try to talk to their underlying cloud providers and we + // definitely don't have the right configuration or credentials for this. + return response +} + +func (m *Mock) Stop() error { + // Just stop the original resource. + return m.Provider.Stop() +} + +func (m *Mock) ReadResource(request ReadResourceRequest) ReadResourceResponse { + // For a mocked provider, reading a resource is just reading it from the + // state. So we'll return what we have. + return ReadResourceResponse{ + NewState: request.PriorState, + Identity: request.CurrentIdentity, + } +} + +func (m *Mock) PlanResourceChange(request PlanResourceChangeRequest) PlanResourceChangeResponse { + if request.ProposedNewState.IsNull() { + // Then we are deleting this resource - we don't need to do anything. + return PlanResourceChangeResponse{ + PlannedState: request.ProposedNewState, + PlannedPrivate: []byte("destroy"), + } + } + + var response PlanResourceChangeResponse + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + resource, exists := schema.ResourceTypes[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a resource type doesn't exist. + panic(fmt.Errorf("failed to retrieve schema for resource %s", request.TypeName)) + } + + if request.PriorState.IsNull() { + // Then we are creating this resource - we need to populate the computed + // null fields with unknowns so Terraform will render them properly. + + replacement := &mocking.MockedData{ + Value: cty.NilVal, // If we have no data then we use cty.NilVal. + ComputedAsUnknown: true, + } + // if we are allowed to use the mock defaults for plan, we can populate the computed fields with the mock defaults. + if mockedResource, exists := m.Data.MockResources[request.TypeName]; exists && mockedResource.UseForPlan { + replacement.Value = mockedResource.Defaults + replacement.Range = mockedResource.DefaultsRange + replacement.ComputedAsUnknown = false + } + + value, diags := mocking.PlanComputedValuesForResource(request.ProposedNewState, replacement, resource.Body) + response.Diagnostics = response.Diagnostics.Append(diags) + response.PlannedState = ephemeral.StripWriteOnlyAttributes(value, resource.Body) + response.PlannedPrivate = []byte("create") + return response + } + + // Otherwise, we're just doing a simple update and we don't need to populate + // any values for that. + response.PlannedState = ephemeral.StripWriteOnlyAttributes(request.ProposedNewState, resource.Body) + response.PlannedPrivate = []byte("update") + return response +} + +func (m *Mock) ApplyResourceChange(request ApplyResourceChangeRequest) ApplyResourceChangeResponse { + switch string(request.PlannedPrivate) { + case "create": + // A new resource that we've created might have computed fields we need + // to populate. + + var response ApplyResourceChangeResponse + + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + resource, exists := schema.ResourceTypes[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a resource type doesn't exist. + panic(fmt.Errorf("failed to retrieve schema for resource %s", request.TypeName)) + } + + replacement := &mocking.MockedData{ + Value: cty.NilVal, // If we have no data then we use cty.NilVal. + } + if mockedResource, exists := m.Data.MockResources[request.TypeName]; exists { + replacement.Value = mockedResource.Defaults + replacement.Range = mockedResource.DefaultsRange + } + + value, diags := mocking.ApplyComputedValuesForResource(request.PlannedState, replacement, resource.Body) + response.Diagnostics = response.Diagnostics.Append(diags) + response.NewState = value + response.NewIdentity = request.PlannedIdentity + return response + + default: + // For update or destroy operations, we don't have to create any values + // so we'll just return the planned state directly. + return ApplyResourceChangeResponse{ + NewState: request.PlannedState, + NewIdentity: request.PlannedIdentity, + } + } +} + +func (m *Mock) ImportResourceState(request ImportResourceStateRequest) (response ImportResourceStateResponse) { + // You can't import via mock providers. The users should write specific + // `override_resource` blocks for any resources they want to import, so we + // just make them think about it rather than performing a blanket import + // of all resources that are backed by mock providers. + response.Diagnostics = response.Diagnostics.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid import request", "Cannot import resources from mock providers. Use an `override_resource` block to targeting the specific resource being imported instead.")) + return response +} + +func (m *Mock) MoveResourceState(request MoveResourceStateRequest) MoveResourceStateResponse { + // The MoveResourceState operation happens offline, so we can just hand this + // off to the underlying provider. + return m.Provider.MoveResourceState(request) +} + +func (m *Mock) ReadDataSource(request ReadDataSourceRequest) ReadDataSourceResponse { + var response ReadDataSourceResponse + + schema := m.GetProviderSchema() + response.Diagnostics = response.Diagnostics.Append(schema.Diagnostics) + if schema.Diagnostics.HasErrors() { + // We couldn't retrieve the schema for some reason, so the mock + // provider can't really function. + return response + } + + datasource, exists := schema.DataSources[request.TypeName] + if !exists { + // This means something has gone wrong much earlier, we should have + // failed a validation somewhere if a data source type doesn't exist. + panic(fmt.Errorf("failed to retrieve schema for data source %s", request.TypeName)) + } + + mockedData := &mocking.MockedData{ + Value: cty.NilVal, // If we have no mocked data we use cty.NilVal. + } + if mockedDataSource, exists := m.Data.MockDataSources[request.TypeName]; exists { + mockedData.Value = mockedDataSource.Defaults + mockedData.Range = mockedDataSource.DefaultsRange + } + + value, diags := mocking.ComputedValuesForDataSource(request.Config, mockedData, datasource.Body) + response.Diagnostics = response.Diagnostics.Append(diags) + response.State = ephemeral.StripWriteOnlyAttributes(value, datasource.Body) + return response +} + +func (m *Mock) ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfigRequest) ValidateEphemeralResourceConfigResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "No ephemeral resource types in mock providers", + "The provider mocking mechanism does not yet support ephemeral resource types.", + nil, // the topmost configuration object + )) + return ValidateEphemeralResourceConfigResponse{ + Diagnostics: diags, + } +} + +func (m *Mock) OpenEphemeralResource(OpenEphemeralResourceRequest) OpenEphemeralResourceResponse { + // FIXME: Design some means to mock an ephemeral resource type. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "No ephemeral resource types in mock providers", + "The provider mocking mechanism does not yet support ephemeral resource types.", + nil, // the topmost configuration object + )) + return OpenEphemeralResourceResponse{ + Diagnostics: diags, + } +} + +func (m *Mock) RenewEphemeralResource(RenewEphemeralResourceRequest) RenewEphemeralResourceResponse { + // FIXME: Design some means to mock an ephemeral resource type. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "No ephemeral resource types in mock providers", + "The provider mocking mechanism does not yet support ephemeral resource types.", + nil, // the topmost configuration object + )) + return RenewEphemeralResourceResponse{ + Diagnostics: diags, + } +} + +func (m *Mock) CloseEphemeralResource(CloseEphemeralResourceRequest) CloseEphemeralResourceResponse { + // FIXME: Design some means to mock an ephemeral resource type. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "No ephemeral resource types in mock providers", + "The provider mocking mechanism does not yet support ephemeral resource types.", + nil, // the topmost configuration object + )) + return CloseEphemeralResourceResponse{ + Diagnostics: diags, + } +} + +func (m *Mock) CallFunction(request CallFunctionRequest) CallFunctionResponse { + return m.Provider.CallFunction(request) +} + +func (m *Mock) Close() error { + return m.Provider.Close() +} diff --git a/internal/providers/provider.go b/internal/providers/provider.go index 5d98d9bf39..6fa57a511a 100644 --- a/internal/providers/provider.go +++ b/internal/providers/provider.go @@ -1,9 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providers import ( "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -13,6 +16,11 @@ type Interface interface { // GetSchema returns the complete schema for the provider. GetProviderSchema() GetProviderSchemaResponse + // GetResourceIdentitySchemas returns the identity schemas for all managed resources + // for the provider. Usually you don't need to call this method directly as GetProviderSchema + // will merge the identity schemas into the provider schema. + GetResourceIdentitySchemas() GetResourceIdentitySchemasResponse + // ValidateProviderConfig allows the provider to validate the configuration. // The ValidateProviderConfigResponse.PreparedConfig field is unused. The // final configuration is not stored in the state, and any modifications @@ -27,12 +35,22 @@ type Interface interface { // configuration values. ValidateDataResourceConfig(ValidateDataResourceConfigRequest) ValidateDataResourceConfigResponse + // ValidateEphemeralResourceConfig allows the provider to validate the + // ephemeral resource configuration values. + ValidateEphemeralResourceConfig(ValidateEphemeralResourceConfigRequest) ValidateEphemeralResourceConfigResponse + // UpgradeResourceState is called when the state loader encounters an // instance state whose schema version is less than the one reported by the // currently-used version of the corresponding provider, and the upgraded // result is used for any further processing. UpgradeResourceState(UpgradeResourceStateRequest) UpgradeResourceStateResponse + // UpgradeResourceIdentity is called when the state loader encounters an + // instance identity whose schema version is less than the one reported by + // the currently-used version of the corresponding provider, and the upgraded + // result is used for any further processing. + UpgradeResourceIdentity(UpgradeResourceIdentityRequest) UpgradeResourceIdentityResponse + // Configure configures and initialized the provider. ConfigureProvider(ConfigureProviderRequest) ConfigureProviderResponse @@ -63,13 +81,34 @@ type Interface interface { // ImportResourceState requests that the given resource be imported. ImportResourceState(ImportResourceStateRequest) ImportResourceStateResponse + // MoveResourceState retrieves the updated value for a resource after it + // has moved resource types. + MoveResourceState(MoveResourceStateRequest) MoveResourceStateResponse + // ReadDataSource returns the data source's current state. ReadDataSource(ReadDataSourceRequest) ReadDataSourceResponse + // OpenEphemeralResource opens an ephemeral resource instance. + OpenEphemeralResource(OpenEphemeralResourceRequest) OpenEphemeralResourceResponse + // RenewEphemeralResource extends the validity of a previously-opened ephemeral + // resource instance. + RenewEphemeralResource(RenewEphemeralResourceRequest) RenewEphemeralResourceResponse + // CloseEphemeralResource closes an ephemeral resource instance, with the intent + // of rendering it invalid as soon as possible. + CloseEphemeralResource(CloseEphemeralResourceRequest) CloseEphemeralResourceResponse + + // CallFunction calls a provider-contributed function. + CallFunction(CallFunctionRequest) CallFunctionResponse + // Close shuts down the plugin process if applicable. Close() error } +// GetProviderSchemaResponse is the return type for GetProviderSchema, and +// should only be used when handling a value for that method. The handling of +// of schemas in any other context should always use ProviderSchema, so that +// the in-memory representation can be more easily changed separately from the +// RPC protocol. type GetProviderSchemaResponse struct { // Provider is the schema for the provider itself. Provider Schema @@ -83,6 +122,14 @@ type GetProviderSchemaResponse struct { // DataSources maps the data source name to that data source's schema. DataSources map[string]Schema + // EphemeralResourceTypes maps the name of an ephemeral resource type + // to its schema. + EphemeralResourceTypes map[string]Schema + + // Functions maps from local function name (not including an namespace + // prefix) to the declaration of a function. + Functions map[string]FunctionDecl + // Diagnostics contains any warnings or errors from the method call. Diagnostics tfdiags.Diagnostics @@ -90,6 +137,39 @@ type GetProviderSchemaResponse struct { ServerCapabilities ServerCapabilities } +// GetResourceIdentitySchemasResponse is the return type for GetResourceIdentitySchemas, +// and should only be used when handling a value for that method. The handling of +// of schemas in any other context should always use ResourceIdentitySchemas, so that +// the in-memory representation can be more easily changed separately from the +// RPC protocol. +type GetResourceIdentitySchemasResponse struct { + // IdentityTypes map the resource type name to that type's identity schema. + IdentityTypes map[string]IdentitySchema + + // Diagnostics contains any warnings or errors from the method call. + Diagnostics tfdiags.Diagnostics +} + +type IdentitySchema struct { + Version int64 + + Body *configschema.Object +} + +// Schema pairs a provider or resource schema with that schema's version. +// This is used to be able to upgrade the schema in UpgradeResourceState. +// +// This describes the schema for a single object within a provider. Type +// "Schemas" (plural) instead represents the overall collection of schemas +// for everything within a particular provider. +type Schema struct { + Version int64 + Body *configschema.Block + + IdentityVersion int64 + Identity *configschema.Object +} + // ServerCapabilities allows providers to communicate extra information // regarding supported protocol features. This is used to indicate availability // of certain forward-compatible changes which may be optional in a major @@ -98,6 +178,30 @@ type ServerCapabilities struct { // PlanDestroy signals that this provider expects to receive a // PlanResourceChange call for resources that are to be destroyed. PlanDestroy bool + + // The GetProviderSchemaOptional capability indicates that this + // provider does not require calling GetProviderSchema to operate + // normally, and the caller can used a cached copy of the provider's + // schema. + GetProviderSchemaOptional bool + + // The MoveResourceState capability indicates that this provider supports + // the MoveResourceState RPC. + MoveResourceState bool +} + +// ClientCapabilities allows Terraform to publish information regarding +// supported protocol features. This is used to indicate availability of +// certain forward-compatible changes which may be optional in a major +// protocol version, but cannot be tested for directly. +type ClientCapabilities struct { + // The deferral_allowed capability signals that the client is able to + // handle deferred responses from the provider. + DeferralAllowed bool + + // The write_only_attributes_allowed capability signals that the client + // is able to handle write_only attributes for managed resources. + WriteOnlyAttributesAllowed bool } type ValidateProviderConfigRequest struct { @@ -119,6 +223,9 @@ type ValidateResourceConfigRequest struct { // Config is the configuration value to validate, which may contain unknown // values. Config cty.Value + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities } type ValidateResourceConfigResponse struct { @@ -140,6 +247,20 @@ type ValidateDataResourceConfigResponse struct { Diagnostics tfdiags.Diagnostics } +type ValidateEphemeralResourceConfigRequest struct { + // TypeName is the name of the data source type to validate. + TypeName string + + // Config is the configuration value to validate, which may contain unknown + // values. + Config cty.Value +} + +type ValidateEphemeralResourceConfigResponse struct { + // Diagnostics contains any warnings or errors from the method call. + Diagnostics tfdiags.Diagnostics +} + type UpgradeResourceStateRequest struct { // TypeName is the name of the resource type being upgraded TypeName string @@ -147,12 +268,12 @@ type UpgradeResourceStateRequest struct { // Version is version of the schema that created the current state. Version int64 - // RawStateJSON and RawStateFlatmap contiain the state that needs to be + // RawStateJSON and RawStateFlatmap contain the state that needs to be // upgraded to match the current schema version. Because the schema is // unknown, this contains only the raw data as stored in the state. // RawStateJSON is the current json state encoding. // RawStateFlatmap is the legacy flatmap encoding. - // Only on of these fields may be set for the upgrade request. + // Only one of these fields may be set for the upgrade request. RawStateJSON []byte RawStateFlatmap map[string]string } @@ -165,6 +286,26 @@ type UpgradeResourceStateResponse struct { Diagnostics tfdiags.Diagnostics } +type UpgradeResourceIdentityRequest struct { + // TypeName is the name of the resource type being upgraded + TypeName string + + // Version is version of the schema that created the current identity. + Version int64 + + // RawIdentityJSON contains the identity that needs to be + // upgraded to match the current schema version. + RawIdentityJSON []byte +} + +type UpgradeResourceIdentityResponse struct { + // UpgradedState is the newly upgraded resource identity. + UpgradedIdentity cty.Value + + // Diagnostics contains any warnings or errors from the method call. + Diagnostics tfdiags.Diagnostics +} + type ConfigureProviderRequest struct { // Terraform version is the version string from the running instance of // terraform. Providers can use TerraformVersion to verify compatibility, @@ -173,6 +314,9 @@ type ConfigureProviderRequest struct { // Config is the complete configuration value for the provider. Config cty.Value + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities } type ConfigureProviderResponse struct { @@ -196,6 +340,49 @@ type ReadResourceRequest struct { // each provider, and it should not be used without coordination with // HashiCorp. It is considered experimental and subject to change. ProviderMeta cty.Value + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities + + // CurrentIdentity is the current identity data of the resource. + CurrentIdentity cty.Value +} + +// DeferredReason is a string that describes why a resource was deferred. +// It differs from the protobuf enum in that it adds more cases +// since it's more widely used to represent the reason for deferral. +// Reasons like instance count unknown and deferred prereq are not +// relevant for providers but can occur in general. +type DeferredReason string + +const ( + // DeferredReasonInvalid is used when the reason for deferring is + // unknown or irrelevant. + DeferredReasonInvalid DeferredReason = "invalid" + + // DeferredReasonInstanceCountUnknown is used when the reason for deferring + // is that the count or for_each meta-attribute was unknown. + DeferredReasonInstanceCountUnknown DeferredReason = "instance_count_unknown" + + // DeferredReasonResourceConfigUnknown is used when the reason for deferring + // is that the resource configuration was unknown. + DeferredReasonResourceConfigUnknown DeferredReason = "resource_config_unknown" + + // DeferredReasonProviderConfigUnknown is used when the reason for deferring + // is that the provider configuration was unknown. + DeferredReasonProviderConfigUnknown DeferredReason = "provider_config_unknown" + + // DeferredReasonAbsentPrereq is used when the reason for deferring is that + // a required prerequisite resource was absent. + DeferredReasonAbsentPrereq DeferredReason = "absent_prereq" + + // DeferredReasonDeferredPrereq is used when the reason for deferring is + // that a required prerequisite resource was itself deferred. + DeferredReasonDeferredPrereq DeferredReason = "deferred_prereq" +) + +type Deferred struct { + Reason DeferredReason } type ReadResourceResponse struct { @@ -208,6 +395,14 @@ type ReadResourceResponse struct { // Private is an opaque blob that will be stored in state along with the // resource. It is intended only for interpretation by the provider itself. Private []byte + + // Deferred if present signals that the provider was not able to fully + // complete this operation and a susequent run is required. + Deferred *Deferred + + // Identity is the object-typed value representing the identity of the remote + // object within Terraform. + Identity cty.Value } type PlanResourceChangeRequest struct { @@ -238,6 +433,12 @@ type PlanResourceChangeRequest struct { // each provider, and it should not be used without coordination with // HashiCorp. It is considered experimental and subject to change. ProviderMeta cty.Value + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities + + // PriorIdentity is the current identity data of the resource. + PriorIdentity cty.Value } type PlanResourceChangeResponse struct { @@ -263,6 +464,13 @@ type PlanResourceChangeResponse struct { // otherwise fail due to this imprecise mapping. No other provider or SDK // implementation is permitted to set this. LegacyTypeSystem bool + + // Deferred if present signals that the provider was not able to fully + // complete this operation and a susequent run is required. + Deferred *Deferred + + // PlannedIdentity is the planned identity data of the resource. + PlannedIdentity cty.Value } type ApplyResourceChangeRequest struct { @@ -290,6 +498,9 @@ type ApplyResourceChangeRequest struct { // each provider, and it should not be used without coordination with // HashiCorp. It is considered experimental and subject to change. ProviderMeta cty.Value + + // PlannedIdentity is the planned identity data of the resource. + PlannedIdentity cty.Value } type ApplyResourceChangeResponse struct { @@ -311,6 +522,9 @@ type ApplyResourceChangeResponse struct { // otherwise fail due to this imprecise mapping. No other provider or SDK // implementation is permitted to set this. LegacyTypeSystem bool + + // NewIdentity is the new identity data of the resource. + NewIdentity cty.Value } type ImportResourceStateRequest struct { @@ -320,6 +534,12 @@ type ImportResourceStateRequest struct { // ID is a string with which the provider can identify the resource to be // imported. ID string + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities + + // Identity is the identity data of the resource. + Identity cty.Value } type ImportResourceStateResponse struct { @@ -331,10 +551,14 @@ type ImportResourceStateResponse struct { // Diagnostics contains any warnings or errors from the method call. Diagnostics tfdiags.Diagnostics + + // Deferred if present signals that the provider was not able to fully + // complete this operation and a susequent run is required. + Deferred *Deferred } // ImportedResource represents an object being imported into Terraform with the -// help of a provider. An ImportedObject is a RemoteObject that has been read +// help of a provider. An ImportedResource is a RemoteObject that has been read // by the provider's import handler but hasn't yet been committed to state. type ImportedResource struct { // TypeName is the name of the resource type associated with the @@ -350,24 +574,55 @@ type ImportedResource struct { // Private is an opaque blob that will be stored in state along with the // resource. It is intended only for interpretation by the provider itself. Private []byte + + // Identity is the identity data of the resource. + Identity cty.Value } -// AsInstanceObject converts the receiving ImportedObject into a -// ResourceInstanceObject that has status ObjectReady. -// -// The returned object does not know its own resource type, so the caller must -// retain the ResourceType value from the source object if this information is -// needed. -// -// The returned object also has no dependency addresses, but the caller may -// freely modify the direct fields of the returned object without affecting -// the receiver. -func (ir ImportedResource) AsInstanceObject() *states.ResourceInstanceObject { - return &states.ResourceInstanceObject{ - Status: states.ObjectReady, - Value: ir.State, - Private: ir.Private, - } +type MoveResourceStateRequest struct { + // SourceProviderAddress is the address of the provider that the resource + // is being moved from. + SourceProviderAddress string + + // SourceTypeName is the name of the resource type that the resource is + // being moved from. + SourceTypeName string + + // SourceSchemaVersion is the schema version of the resource type that the + // resource is being moved from. + SourceSchemaVersion int64 + + // SourceStateJSON contains the state of the resource that is being moved. + // Because the schema is unknown, this contains only the raw data as stored + // in the state. + SourceStateJSON []byte + + // SourcePrivate contains the private state of the resource that is being + // moved. + SourcePrivate []byte + + // TargetTypeName is the name of the resource type that the resource is + // being moved to. + TargetTypeName string + + // SourceIdentity is the identity data of the resource that is being moved. + SourceIdentity []byte +} + +type MoveResourceStateResponse struct { + // TargetState is the state of the resource after it has been moved to the + // new resource type. + TargetState cty.Value + + // TargetPrivate is the private state of the resource after it has been + // moved to the new resource type. + TargetPrivate []byte + + // Diagnostics contains any warnings or errors from the method call. + Diagnostics tfdiags.Diagnostics + + // TargetIdentity is the identity data of the resource that is being moved. + TargetIdentity cty.Value } type ReadDataSourceRequest struct { @@ -382,6 +637,9 @@ type ReadDataSourceRequest struct { // each provider, and it should not be used without coordination with // HashiCorp. It is considered experimental and subject to change. ProviderMeta cty.Value + + // ClientCapabilities contains information about the client's capabilities. + ClientCapabilities ClientCapabilities } type ReadDataSourceResponse struct { @@ -390,4 +648,42 @@ type ReadDataSourceResponse struct { // Diagnostics contains any warnings or errors from the method call. Diagnostics tfdiags.Diagnostics + + // Deferred if present signals that the provider was not able to fully + // complete this operation and a susequent run is required. + Deferred *Deferred +} + +type CallFunctionRequest struct { + // FunctionName is the local name of the function to call, as it was + // declared by the provider in its schema and without any + // externally-imposed namespace prefixes. + FunctionName string + + // Arguments are the positional argument values given at the call site. + // + // Provider functions are required to behave as pure functions, and so + // if all of the argument values are known then two separate calls with the + // same arguments must always return an identical value, without performing + // any externally-visible side-effects. + Arguments []cty.Value +} + +type CallFunctionResponse struct { + // Result is the successful result of the function call. + // + // If all of the arguments in the call were known then the result must + // also be known. If any arguments were unknown then the result may + // optionally be unknown. The type of the returned value must conform + // to the return type constraint for this function as declared in the + // provider schema. + // + // If Diagnostics contains any errors, this field will be ignored and + // so can be left as cty.NilVal to represent the absense of a value. + Result cty.Value + + // Err is the error value from the function call. This may be an instance + // of function.ArgError from the go-cty package to specify a problem with a + // specific argument. + Err error } diff --git a/internal/providers/schema_cache.go b/internal/providers/schema_cache.go new file mode 100644 index 0000000000..2b851335e3 --- /dev/null +++ b/internal/providers/schema_cache.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package providers + +import ( + "sync" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// SchemaCache is a global cache of Schemas. +// This will be accessed by both core and the provider clients to ensure that +// large schemas are stored in a single location. +var SchemaCache = &schemaCache{ + m: make(map[addrs.Provider]ProviderSchema), +} + +// Global cache for provider schemas +// Cache the entire response to ensure we capture any new fields, like +// ServerCapabilities. This also serves to capture errors so that multiple +// concurrent calls resulting in an error can be handled in the same manner. +type schemaCache struct { + mu sync.Mutex + m map[addrs.Provider]ProviderSchema +} + +func (c *schemaCache) Set(p addrs.Provider, s ProviderSchema) { + c.mu.Lock() + defer c.mu.Unlock() + + c.m[p] = s +} + +func (c *schemaCache) Get(p addrs.Provider) (ProviderSchema, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + s, ok := c.m[p] + return s, ok +} diff --git a/internal/providers/schemas.go b/internal/providers/schemas.go index 213ff4f0e5..083819df1e 100644 --- a/internal/providers/schemas.go +++ b/internal/providers/schemas.go @@ -1,62 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package providers import ( "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" ) -// Schemas is an overall container for all of the schemas for all configurable -// objects defined within a particular provider. -// -// The schema for each individual configurable object is represented by nested -// instances of type Schema (singular) within this data structure. -// -// This type used to be known as terraform.ProviderSchema, but moved out here -// as part of our ongoing efforts to shrink down the "terraform" package. -// There's still a type alias at the old name, but we should prefer using -// providers.Schema in new code. However, a consequence of this transitional -// situation is that the "terraform" package still has the responsibility for -// constructing a providers.Schemas object based on responses from the provider -// API; hopefully we'll continue this refactor later so that functions in this -// package totally encapsulate the unmarshalling and include this as part of -// providers.GetProviderSchemaResponse. -type Schemas struct { - Provider *configschema.Block - ProviderMeta *configschema.Block - ResourceTypes map[string]*configschema.Block - DataSources map[string]*configschema.Block - - ResourceTypeSchemaVersions map[string]uint64 -} +// ProviderSchema is an overall container for all of the schemas for all +// configurable objects defined within a particular provider. All storage of +// provider schemas should use this type. +type ProviderSchema = GetProviderSchemaResponse // SchemaForResourceType attempts to find a schema for the given mode and type. -// Returns nil if no such schema is available. -func (ss *Schemas) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema *configschema.Block, version uint64) { +// Returns an empty schema if none is available. +func (ss ProviderSchema) SchemaForResourceType(mode addrs.ResourceMode, typeName string) (schema Schema) { switch mode { case addrs.ManagedResourceMode: - return ss.ResourceTypes[typeName], ss.ResourceTypeSchemaVersions[typeName] + return ss.ResourceTypes[typeName] case addrs.DataResourceMode: - // Data resources don't have schema versions right now, since state is discarded for each refresh - return ss.DataSources[typeName], 0 + return ss.DataSources[typeName] + case addrs.EphemeralResourceMode: + return ss.EphemeralResourceTypes[typeName] default: // Shouldn't happen, because the above cases are comprehensive. - return nil, 0 + return Schema{} } } // SchemaForResourceAddr attempts to find a schema for the mode and type from -// the given resource address. Returns nil if no such schema is available. -func (ss *Schemas) SchemaForResourceAddr(addr addrs.Resource) (schema *configschema.Block, version uint64) { +// the given resource address. Returns an empty schema if none is available. +func (ss ProviderSchema) SchemaForResourceAddr(addr addrs.Resource) (schema Schema) { return ss.SchemaForResourceType(addr.Mode, addr.Type) } -// Schema pairs a provider or resource schema with that schema's version. -// This is used to be able to upgrade the schema in UpgradeResourceState. -// -// This describes the schema for a single object within a provider. Type -// "Schemas" (plural) instead represents the overall collection of schemas -// for everything within a particular provider. -type Schema struct { - Version int64 - Block *configschema.Block -} +type ResourceIdentitySchemas = GetResourceIdentitySchemasResponse diff --git a/internal/terraform/provider_mock.go b/internal/providers/testing/provider_mock.go similarity index 59% rename from internal/terraform/provider_mock.go rename to internal/providers/testing/provider_mock.go index 23ce9be5b5..c3e0863e69 100644 --- a/internal/terraform/provider_mock.go +++ b/internal/providers/testing/provider_mock.go @@ -1,4 +1,7 @@ -package terraform +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package testing import ( "fmt" @@ -8,7 +11,6 @@ import ( ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/zclconf/go-cty/cty/msgpack" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/providers" ) @@ -17,6 +19,10 @@ var _ providers.Interface = (*MockProvider)(nil) // MockProvider implements providers.Interface but mocks out all the // calls for testing purposes. +// +// This is distinct from providers.Mock which is actually available to Terraform +// configuration and test authors. This type is only for use in internal testing +// of Terraform itself. type MockProvider struct { sync.Mutex @@ -26,29 +32,34 @@ type MockProvider struct { GetProviderSchemaCalled bool GetProviderSchemaResponse *providers.GetProviderSchemaResponse + GetResourceIdentitySchemasCalled bool + GetResourceIdentitySchemasResponse *providers.GetResourceIdentitySchemasResponse + ValidateProviderConfigCalled bool ValidateProviderConfigResponse *providers.ValidateProviderConfigResponse ValidateProviderConfigRequest providers.ValidateProviderConfigRequest ValidateProviderConfigFn func(providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse ValidateResourceConfigCalled bool - ValidateResourceConfigTypeName string ValidateResourceConfigResponse *providers.ValidateResourceConfigResponse ValidateResourceConfigRequest providers.ValidateResourceConfigRequest ValidateResourceConfigFn func(providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse ValidateDataResourceConfigCalled bool - ValidateDataResourceConfigTypeName string ValidateDataResourceConfigResponse *providers.ValidateDataResourceConfigResponse ValidateDataResourceConfigRequest providers.ValidateDataResourceConfigRequest ValidateDataResourceConfigFn func(providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse UpgradeResourceStateCalled bool - UpgradeResourceStateTypeName string UpgradeResourceStateResponse *providers.UpgradeResourceStateResponse UpgradeResourceStateRequest providers.UpgradeResourceStateRequest UpgradeResourceStateFn func(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse + UpgradeResourceIdentityCalled bool + UpgradeResourceIdentityResponse *providers.UpgradeResourceIdentityResponse + UpgradeResourceIdentityRequest providers.UpgradeResourceIdentityRequest + UpgradeResourceIdentityFn func(providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse + ConfigureProviderCalled bool ConfigureProviderResponse *providers.ConfigureProviderResponse ConfigureProviderRequest providers.ConfigureProviderRequest @@ -78,11 +89,38 @@ type MockProvider struct { ImportResourceStateRequest providers.ImportResourceStateRequest ImportResourceStateFn func(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse + MoveResourceStateCalled bool + MoveResourceStateResponse *providers.MoveResourceStateResponse + MoveResourceStateRequest providers.MoveResourceStateRequest + MoveResourceStateFn func(providers.MoveResourceStateRequest) providers.MoveResourceStateResponse + ReadDataSourceCalled bool ReadDataSourceResponse *providers.ReadDataSourceResponse ReadDataSourceRequest providers.ReadDataSourceRequest ReadDataSourceFn func(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse + ValidateEphemeralResourceConfigCalled bool + ValidateEphemeralResourceConfigResponse *providers.ValidateEphemeralResourceConfigResponse + ValidateEphemeralResourceConfigRequest providers.ValidateEphemeralResourceConfigRequest + ValidateEphemeralResourceConfigFn func(providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse + OpenEphemeralResourceCalled bool + OpenEphemeralResourceResponse *providers.OpenEphemeralResourceResponse + OpenEphemeralResourceRequest providers.OpenEphemeralResourceRequest + OpenEphemeralResourceFn func(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse + RenewEphemeralResourceCalled bool + RenewEphemeralResourceResponse *providers.RenewEphemeralResourceResponse + RenewEphemeralResourceRequest providers.RenewEphemeralResourceRequest + RenewEphemeralResourceFn func(providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse + CloseEphemeralResourceCalled bool + CloseEphemeralResourceResponse *providers.CloseEphemeralResourceResponse + CloseEphemeralResourceRequest providers.CloseEphemeralResourceRequest + CloseEphemeralResourceFn func(providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse + + CallFunctionCalled bool + CallFunctionResponse providers.CallFunctionResponse + CallFunctionRequest providers.CallFunctionRequest + CallFunctionFn func(providers.CallFunctionRequest) providers.CallFunctionResponse + CloseCalled bool CloseError error } @@ -109,29 +147,22 @@ func (p *MockProvider) getProviderSchema() providers.GetProviderSchemaResponse { } } -// ProviderSchema is a helper to convert from the internal GetProviderSchemaResponse to -// a ProviderSchema. -func (p *MockProvider) ProviderSchema() *ProviderSchema { - resp := p.getProviderSchema() +func (p *MockProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + p.Lock() + defer p.Unlock() + p.GetResourceIdentitySchemasCalled = true - schema := &ProviderSchema{ - Provider: resp.Provider.Block, - ProviderMeta: resp.ProviderMeta.Block, - ResourceTypes: map[string]*configschema.Block{}, - DataSources: map[string]*configschema.Block{}, - ResourceTypeSchemaVersions: map[string]uint64{}, + return p.getResourceIdentitySchemas() +} + +func (p *MockProvider) getResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + if p.GetResourceIdentitySchemasResponse != nil { + return *p.GetResourceIdentitySchemasResponse } - for resType, s := range resp.ResourceTypes { - schema.ResourceTypes[resType] = s.Block - schema.ResourceTypeSchemaVersions[resType] = uint64(s.Version) + return providers.GetResourceIdentitySchemasResponse{ + IdentityTypes: map[string]providers.IdentitySchema{}, } - - for dataSource, s := range resp.DataSources { - schema.DataSources[dataSource] = s.Block - } - - return schema } func (p *MockProvider) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) (resp providers.ValidateProviderConfigResponse) { @@ -167,7 +198,7 @@ func (p *MockProvider) ValidateResourceConfig(r providers.ValidateResourceConfig return resp } - _, err := msgpack.Marshal(r.Config, resourceSchema.Block.ImpliedType()) + _, err := msgpack.Marshal(r.Config, resourceSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -197,7 +228,7 @@ func (p *MockProvider) ValidateDataResourceConfig(r providers.ValidateDataResour resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", r.TypeName)) return resp } - _, err := msgpack.Marshal(r.Config, dataSchema.Block.ImpliedType()) + _, err := msgpack.Marshal(r.Config, dataSchema.Body.ImpliedType()) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp @@ -214,6 +245,41 @@ func (p *MockProvider) ValidateDataResourceConfig(r providers.ValidateDataResour return resp } +func (p *MockProvider) ValidateEphemeralResourceConfig(r providers.ValidateEphemeralResourceConfigRequest) (resp providers.ValidateEphemeralResourceConfigResponse) { + p.Lock() + defer p.Unlock() + + p.ValidateEphemeralResourceConfigCalled = true + p.ValidateEphemeralResourceConfigRequest = r + + // Marshall the value to replicate behavior by the GRPC protocol + dataSchema, ok := p.getProviderSchema().EphemeralResourceTypes[r.TypeName] + if !ok { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", r.TypeName)) + return resp + } + _, err := msgpack.Marshal(r.Config, dataSchema.Body.ImpliedType()) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + + if p.ValidateDataResourceConfigFn != nil { + return p.ValidateEphemeralResourceConfigFn(r) + } + + if p.ValidateDataResourceConfigResponse != nil { + return *p.ValidateEphemeralResourceConfigResponse + } + + return resp +} + +// UpgradeResourceState mocks out the response from the provider during an UpgradeResourceState RPC +// The default logic will return the resource's state unchanged, unless other logic is defined on the mock (e.g. UpgradeResourceStateFn) +// +// When using this mock you may need to provide custom logic if the plugin-framework alters values in state, +// e.g. when handling write-only attributes. func (p *MockProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { p.Lock() defer p.Unlock() @@ -229,7 +295,7 @@ func (p *MockProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } - schemaType := schema.Block.ImpliedType() + schemaType := schema.Body.ImpliedType() p.UpgradeResourceStateCalled = true p.UpgradeResourceStateRequest = r @@ -263,6 +329,45 @@ func (p *MockProvider) UpgradeResourceState(r providers.UpgradeResourceStateRequ return resp } +func (p *MockProvider) UpgradeResourceIdentity(r providers.UpgradeResourceIdentityRequest) (resp providers.UpgradeResourceIdentityResponse) { + p.Lock() + defer p.Unlock() + + if !p.ConfigureProviderCalled { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Configure not called before UpgradeResourceIdentity %q", r.TypeName)) + return resp + } + p.UpgradeResourceIdentityCalled = true + p.UpgradeResourceIdentityRequest = r + + if p.UpgradeResourceIdentityFn != nil { + return p.UpgradeResourceIdentityFn(r) + } + + if p.UpgradeResourceIdentityResponse != nil { + return *p.UpgradeResourceIdentityResponse + } + + schema, ok := p.getProviderSchema().ResourceTypes[r.TypeName] + + if !ok || schema.Identity == nil { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no identity schema found for %q", r.TypeName)) + return resp + } + + identityType := schema.Identity.ImpliedType() + + v, err := ctyjson.Unmarshal(r.RawIdentityJSON, identityType) + + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + return resp + } + resp.UpgradedIdentity = v + + return resp +} + func (p *MockProvider) ConfigureProvider(r providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { p.Lock() defer p.Unlock() @@ -322,16 +427,51 @@ func (p *MockProvider) ReadResource(r providers.ReadResourceRequest) (resp provi return resp } - newState, err := schema.Block.CoerceValue(resp.NewState) + newState, err := schema.Body.CoerceValue(resp.NewState) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) } resp.NewState = newState + if resp.Identity.IsNull() { + resp.Identity = r.CurrentIdentity + } + return resp } - // otherwise just return the same state we received - resp.NewState = r.PriorState + // otherwise just return the same state we received without the write-only attributes + // since there are old tests without a schema we default to the prior state + if schema, ok := p.getProviderSchema().ResourceTypes[r.TypeName]; ok { + + newVal, err := cty.Transform(r.PriorState, func(path cty.Path, v cty.Value) (cty.Value, error) { + // We're only concerned with known null values, which can be computed + // by the provider. + if !v.IsKnown() { + return v, nil + } + + attrSchema := schema.Body.AttributeByPath(path) + if attrSchema == nil { + // this is an intermediate path which does not represent an attribute + return v, nil + } + + // Write-only attributes always return null + if attrSchema.WriteOnly { + return cty.NullVal(v.Type()), nil + } + + return v, nil + }) + if err != nil { + resp.Diagnostics = resp.Diagnostics.Append(err) + } + resp.NewState = newVal + } else { + resp.NewState = r.PriorState + } + + resp.Identity = r.CurrentIdentity resp.Private = r.Private return resp } @@ -378,17 +518,27 @@ func (p *MockProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) return v, nil } - attrSchema := schema.Block.AttributeByPath(path) + attrSchema := schema.Body.AttributeByPath(path) if attrSchema == nil { // this is an intermediate path which does not represent an attribute return v, nil } + // Write-only attributes always return null + if attrSchema.WriteOnly { + return cty.NullVal(v.Type()), nil + } + // get the current configuration value, to detect when a // computed+optional attributes has become unset configVal, err := path.Apply(r.Config) if err != nil { - return v, err + // cty can't currently apply some paths, so don't try to guess + // what's needed here and return the proposed part of the value. + // This is only a helper to create a default plan value, any tests + // relying on specific plan behavior will create their own + // PlanResourceChange responses. + return v, nil } switch { @@ -418,9 +568,10 @@ func (p *MockProvider) PlanResourceChange(r providers.PlanResourceChangeRequest) func (p *MockProvider) ApplyResourceChange(r providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { p.Lock() + defer p.Unlock() + p.ApplyResourceChangeCalled = true p.ApplyResourceChangeRequest = r - p.Unlock() if !p.ConfigureProviderCalled { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Configure not called before ApplyResourceChange %q", r.TypeName)) @@ -435,28 +586,17 @@ func (p *MockProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques return *p.ApplyResourceChangeResponse } - schema, ok := p.getProviderSchema().ResourceTypes[r.TypeName] - if !ok { - resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", r.TypeName)) - return resp - } - // if the value is nil, we return that directly to correspond to a delete if r.PlannedState.IsNull() { - resp.NewState = cty.NullVal(schema.Block.ImpliedType()) - return resp - } - - val, err := schema.Block.CoerceValue(r.PlannedState) - if err != nil { - resp.Diagnostics = resp.Diagnostics.Append(err) + resp.NewState = r.PlannedState + resp.NewIdentity = r.PlannedIdentity return resp } // the default behavior will be to create the minimal valid apply value by // setting unknowns (which correspond to computed attributes) to a zero // value. - val, _ = cty.Transform(val, func(path cty.Path, v cty.Value) (cty.Value, error) { + val, _ := cty.Transform(r.PlannedState, func(path cty.Path, v cty.Value) (cty.Value, error) { if !v.IsKnown() { ty := v.Type() switch { @@ -479,6 +619,7 @@ func (p *MockProvider) ApplyResourceChange(r providers.ApplyResourceChangeReques resp.NewState = val resp.Private = r.PlannedPrivate + resp.NewIdentity = r.PlannedIdentity return resp } @@ -500,8 +641,13 @@ func (p *MockProvider) ImportResourceState(r providers.ImportResourceStateReques if p.ImportResourceStateResponse != nil { resp = *p.ImportResourceStateResponse + + // take a copy of the slice, because it is read by the resource instance + importedResources := make([]providers.ImportedResource, len(resp.ImportedResources)) + copy(importedResources, resp.ImportedResources) + // fixup the cty value to match the schema - for i, res := range resp.ImportedResources { + for i, res := range importedResources { schema, ok := p.getProviderSchema().ResourceTypes[res.TypeName] if !ok { resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("no schema found for %q", res.TypeName)) @@ -509,14 +655,32 @@ func (p *MockProvider) ImportResourceState(r providers.ImportResourceStateReques } var err error - res.State, err = schema.Block.CoerceValue(res.State) + res.State, err = schema.Body.CoerceValue(res.State) if err != nil { resp.Diagnostics = resp.Diagnostics.Append(err) return resp } - resp.ImportedResources[i] = res + importedResources[i] = res } + resp.ImportedResources = importedResources + } + + return resp +} + +func (p *MockProvider) MoveResourceState(r providers.MoveResourceStateRequest) (resp providers.MoveResourceStateResponse) { + p.Lock() + defer p.Unlock() + + p.MoveResourceStateCalled = true + p.MoveResourceStateRequest = r + if p.MoveResourceStateFn != nil { + return p.MoveResourceStateFn(r) + } + + if p.MoveResourceStateResponse != nil { + resp = *p.MoveResourceStateResponse } return resp @@ -545,7 +709,93 @@ func (p *MockProvider) ReadDataSource(r providers.ReadDataSourceRequest) (resp p return resp } +func (p *MockProvider) OpenEphemeralResource(r providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + p.Lock() + defer p.Unlock() + + if !p.ConfigureProviderCalled { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Configure not called before OpenEphemeralResource %q", r.TypeName)) + return resp + } + + p.OpenEphemeralResourceCalled = true + p.OpenEphemeralResourceRequest = r + + if p.OpenEphemeralResourceFn != nil { + return p.OpenEphemeralResourceFn(r) + } + + if p.OpenEphemeralResourceResponse != nil { + resp = *p.OpenEphemeralResourceResponse + } + + return resp +} + +func (p *MockProvider) RenewEphemeralResource(r providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) { + p.Lock() + defer p.Unlock() + + if !p.ConfigureProviderCalled { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Configure not called before RenewEphemeralResource %q", r.TypeName)) + return resp + } + + p.RenewEphemeralResourceCalled = true + p.RenewEphemeralResourceRequest = r + + if p.RenewEphemeralResourceFn != nil { + return p.RenewEphemeralResourceFn(r) + } + + if p.RenewEphemeralResourceResponse != nil { + resp = *p.RenewEphemeralResourceResponse + } + + return resp +} + +func (p *MockProvider) CloseEphemeralResource(r providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { + p.Lock() + defer p.Unlock() + + if !p.ConfigureProviderCalled { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("Configure not called before CloseEphemeralResource %q", r.TypeName)) + return resp + } + + p.CloseEphemeralResourceCalled = true + p.CloseEphemeralResourceRequest = r + + if p.CloseEphemeralResourceFn != nil { + return p.CloseEphemeralResourceFn(r) + } + + if p.CloseEphemeralResourceResponse != nil { + resp = *p.CloseEphemeralResourceResponse + } + + return resp +} + +func (p *MockProvider) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse { + p.Lock() + defer p.Unlock() + + p.CallFunctionCalled = true + p.CallFunctionRequest = r + + if p.CallFunctionFn != nil { + return p.CallFunctionFn(r) + } + + return p.CallFunctionResponse +} + func (p *MockProvider) Close() error { + p.Lock() + defer p.Unlock() + p.CloseCalled = true return p.CloseError } diff --git a/internal/provisioner-local-exec/main/main.go b/internal/provisioner-local-exec/main/main.go index 78f14b37af..2c533bf20a 100644 --- a/internal/provisioner-local-exec/main/main.go +++ b/internal/provisioner-local-exec/main/main.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( diff --git a/internal/provisioners/doc.go b/internal/provisioners/doc.go index b03ba9a1bb..4e4c3f5a3a 100644 --- a/internal/provisioners/doc.go +++ b/internal/provisioners/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package provisioners contains the interface and primary types to implement a // Terraform resource provisioner. package provisioners diff --git a/internal/provisioners/factory.go b/internal/provisioners/factory.go index 590b97a84f..495d14494c 100644 --- a/internal/provisioners/factory.go +++ b/internal/provisioners/factory.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package provisioners // Factory is a function type that creates a new instance of a resource diff --git a/internal/provisioners/provisioner.go b/internal/provisioners/provisioner.go index 190740a7fe..f1cf652bad 100644 --- a/internal/provisioners/provisioner.go +++ b/internal/provisioners/provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package provisioners import ( diff --git a/internal/refactoring/cross_provider_move.go b/internal/refactoring/cross_provider_move.go new file mode 100644 index 0000000000..097cc3832a --- /dev/null +++ b/internal/refactoring/cross_provider_move.go @@ -0,0 +1,245 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package refactoring + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/ephemeral" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// crossTypeMover is a collection of data that is needed to calculate the +// cross-provider move state changes. +type crossTypeMover struct { + State *states.State + ProviderFactories map[addrs.Provider]providers.Factory + ProviderCache map[addrs.Provider]providers.Interface +} + +// close ensures the cached providers are closed. +func (m *crossTypeMover) close() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + for _, provider := range m.ProviderCache { + diags = diags.Append(provider.Close()) + } + return diags +} + +func (m *crossTypeMover) getProvider(providers addrs.Provider) (providers.Interface, error) { + if provider, ok := m.ProviderCache[providers]; ok { + return provider, nil + } + + if factory, ok := m.ProviderFactories[providers]; ok { + provider, err := factory() + if err != nil { + return nil, err + } + + m.ProviderCache[providers] = provider + return provider, nil + } + + // Then we don't have a provider in the cache - this represents a bug in + // Terraform since we should have already loaded all the providers in the + // configuration and the state. + return nil, fmt.Errorf("provider %s implementation not found; this is a bug in Terraform - please report it", providers) +} + +// prepareCrossTypeMove checks if the provided MoveStatement is a cross-type +// move and if so, prepares the data needed to perform the move. +func (m *crossTypeMover) prepareCrossTypeMove(stmt *MoveStatement, source, target addrs.AbsResource) (*crossTypeMove, tfdiags.Diagnostics) { + if stmt.Provider == nil { + // This means the resource was not in the configuration at all, so we + // can't process this. It'll be picked up in the validation errors + // later. + return nil, nil + } + + targetProviderAddr := stmt.Provider + sourceProviderAddr := m.State.Resource(source).ProviderConfig + + if targetProviderAddr.Provider.Equals(sourceProviderAddr.Provider) { + if source.Resource.Type == target.Resource.Type { + // Then this is a move within the same provider and type, so we + // don't need to do anything special. + return nil, nil + } + } + + var diags tfdiags.Diagnostics + var err error + + targetProvider, err := m.getProvider(targetProviderAddr.Provider) + if err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to initialise provider", err.Error())) + return nil, diags + } + + targetSchema := targetProvider.GetProviderSchema() + diags = diags.Append(targetSchema.Diagnostics) + if targetSchema.Diagnostics.HasErrors() { + return nil, diags + } + + if !targetSchema.ServerCapabilities.MoveResourceState { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported `moved` across resource types", + Detail: fmt.Sprintf("The provider %q does not support moved operations across resource types and providers.", targetProviderAddr.Provider), + Subject: stmt.DeclRange.ToHCL().Ptr(), + }) + return nil, diags + } + targetResourceSchema := targetSchema.SchemaForResourceAddr(target.Resource) + return &crossTypeMove{ + targetProvider: targetProvider, + targetProviderAddr: *targetProviderAddr, + targetResourceSchema: targetResourceSchema, + sourceProviderAddr: sourceProviderAddr, + }, diags +} + +type crossTypeMove struct { + targetProvider providers.Interface + targetProviderAddr addrs.AbsProviderConfig + targetResourceSchema providers.Schema + + sourceProviderAddr addrs.AbsProviderConfig +} + +// applyCrossTypeMove will update the provider states.SyncState so that value +// at source is the result of the providers move operation. Note, that this +// doesn't actually move the resource in the state file, it just updates the +// value at source ready to be moved. +func (move *crossTypeMove) applyCrossTypeMove(stmt *MoveStatement, source, target addrs.AbsResourceInstance, state *states.SyncState) tfdiags.Diagnostics { + if move == nil { + // Then we don't need to do any data transformation. + return nil + } + + var diags tfdiags.Diagnostics + + var sourceIdentity []byte + src := state.ResourceInstance(source).Current + if src != nil { + sourceIdentity = src.IdentityJSON + } + + // Build the request. + + request := providers.MoveResourceStateRequest{ + SourceProviderAddress: move.sourceProviderAddr.Provider.String(), + SourceTypeName: source.Resource.Resource.Type, + SourceSchemaVersion: int64(src.SchemaVersion), + SourceStateJSON: src.AttrsJSON, + SourcePrivate: src.Private, + TargetTypeName: target.Resource.Resource.Type, + SourceIdentity: sourceIdentity, + } + + // Ask the provider to transform the value into the type expected by + // the new resource type. + + resp := move.targetProvider.MoveResourceState(request) + diags = diags.Append(resp.Diagnostics) + if resp.Diagnostics.HasErrors() { + return diags + } + + // Providers are supposed to return null values for all write-only attributes + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( + "Provider returned invalid value", + func(path cty.Path) string { + return fmt.Sprintf( + "The provider %q returned a value for the write-only attribute \"%s%s\" during an across type move operation to %s. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + move.targetProviderAddr, target, tfdiags.FormatCtyPath(path), target, + ) + }, + resp.TargetState, + move.targetResourceSchema.Body, + ) + diags = diags.Append(writeOnlyDiags) + + if writeOnlyDiags.HasErrors() { + return diags + } + + if !resp.TargetIdentity.IsNull() { + // Identities can not contain unknown values + if !resp.TargetIdentity.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q planned an identity with unknown values for the move from %s to %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + move.targetProviderAddr, source, target, + ), + )) + } + + // Identities can not contain marks + if _, marks := resp.TargetIdentity.UnmarkDeep(); len(marks) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q planned an identity with marks for the move from %s to %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + move.targetProviderAddr, source, target, + ), + )) + } + } + + if resp.TargetState == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider returned invalid value", + Detail: fmt.Sprintf("The provider returned an invalid value during an across type move operation to %s. This is a bug in the relevant provider; Please report it.", target), + Subject: stmt.DeclRange.ToHCL().Ptr(), + }) + return diags + } + if !resp.TargetState.IsWhollyKnown() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider returned invalid value", + Detail: fmt.Sprintf("The provider %s returned an invalid value during an across type move operation: The returned state contains unknown values. This is a bug in the relevant provider; Please report it.", + move.targetProviderAddr), + Subject: stmt.DeclRange.ToHCL().Ptr(), + }) + } + + // Finally, we can update the source value with the new value. + + newValue := &states.ResourceInstanceObject{ + Value: resp.TargetState, + Private: resp.TargetPrivate, + Status: src.Status, + Dependencies: src.Dependencies, + CreateBeforeDestroy: src.CreateBeforeDestroy, + Identity: resp.TargetIdentity, + } + + data, err := newValue.Encode(move.targetResourceSchema) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to encode source value", + Detail: fmt.Sprintf("Terraform failed to encode the value in state for %s: %v. This is a bug in Terraform; Please report it.", source.String(), err), + Subject: stmt.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + state.SetResourceInstanceCurrent(source, data, move.targetProviderAddr) + return diags +} diff --git a/internal/refactoring/mock_provider.go b/internal/refactoring/mock_provider.go new file mode 100644 index 0000000000..7304c0ba7e --- /dev/null +++ b/internal/refactoring/mock_provider.go @@ -0,0 +1,119 @@ +package refactoring + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var _ providers.Interface = (*mockProvider)(nil) + +// mockProvider provides a mock implementation of providers.Interface that only +// implements the methods that are used by the refactoring package. +type mockProvider struct { + moveResourceState bool + moveResourceError error +} + +func (provider *mockProvider) GetProviderSchema() providers.GetProviderSchemaResponse { + return providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "foo": {Body: &configschema.Block{}}, + "bar": {Body: &configschema.Block{}}, + }, + ServerCapabilities: providers.ServerCapabilities{ + MoveResourceState: provider.moveResourceState, + }, + } +} + +func (provider *mockProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ValidateProviderConfig(providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ValidateResourceConfig(providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ValidateDataResourceConfig(providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) UpgradeResourceState(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) Stop() error { + panic("not implemented in mock") +} + +func (provider *mockProvider) ReadResource(providers.ReadResourceRequest) providers.ReadResourceResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) PlanResourceChange(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ApplyResourceChange(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) MoveResourceState(providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + if provider.moveResourceError != nil { + return providers.MoveResourceStateResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "expected error", provider.moveResourceError.Error()), + }, + } + } + return providers.MoveResourceStateResponse{ + TargetState: cty.EmptyObjectVal, + } +} + +func (provider *mockProvider) ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) ReadDataSource(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) RenewEphemeralResource(providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) CallFunction(providers.CallFunctionRequest) providers.CallFunctionResponse { + panic("not implemented in mock") +} + +func (provider *mockProvider) Close() error { + return nil // do nothing +} diff --git a/internal/refactoring/move_execute.go b/internal/refactoring/move_execute.go index 9a6d577cfb..d79f77ffd4 100644 --- a/internal/refactoring/move_execute.go +++ b/internal/refactoring/move_execute.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package refactoring import ( @@ -7,7 +10,9 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) // ApplyMoves modifies in-place the given state object so that any existing @@ -25,11 +30,11 @@ import ( // // ApplyMoves expects exclusive access to the given state while it's running. // Don't read or write any part of the state structure until ApplyMoves returns. -func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { +func ApplyMoves(stmts []MoveStatement, state *states.State, providerFactory map[addrs.Provider]providers.Factory) (MoveResults, tfdiags.Diagnostics) { ret := makeMoveResults() if len(stmts) == 0 { - return ret + return ret, nil } // The methodology here is to construct a small graph of all of the move @@ -44,7 +49,7 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { // separate validation step should detect this and return an error. if diags := validateMoveStatementGraph(g); diags.HasErrors() { log.Printf("[ERROR] ApplyMoves: %s", diags.ErrWithWarnings()) - return ret + return ret, nil } // The graph must be reduced in order for ReverseDepthFirstWalk to work @@ -62,7 +67,7 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { if startNodes.Len() == 0 { log.Println("[TRACE] refactoring.ApplyMoves: No 'moved' statements to consider in this configuration") - return ret + return ret, nil } log.Printf("[TRACE] refactoring.ApplyMoves: Processing 'moved' statements in the configuration\n%s", logging.Indent(g.String())) @@ -87,6 +92,15 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { }) } + crossTypeMover := &crossTypeMover{ + State: state, + ProviderFactories: providerFactory, + ProviderCache: make(map[addrs.Provider]providers.Interface), + } + + var diags tfdiags.Diagnostics + + syncState := state.SyncWrapper() for _, v := range g.ReverseTopologicalOrder() { stmt := v.(*MoveStatement) @@ -122,7 +136,7 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { } } - state.MoveModuleInstance(modAddr, newAddr) + syncState.MoveModuleInstance(modAddr, newAddr) continue } case addrs.MoveEndpointResource: @@ -149,9 +163,12 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { continue } + crossTypeMove, prepareDiags := crossTypeMover.prepareCrossTypeMove(stmt, rAddr, newAddr) + diags = diags.Append(prepareDiags) for key := range rs.Instances { oldInst := rAddr.Instance(key) newInst := newAddr.Instance(key) + diags = diags.Append(crossTypeMove.applyCrossTypeMove(stmt, oldInst, newInst, syncState)) recordOldAddr(oldInst, newInst) } state.MoveAbsResource(rAddr, newAddr) @@ -171,9 +188,11 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { continue } + crossTypeMove, crossTypeMoveDiags := crossTypeMover.prepareCrossTypeMove(stmt, iAddr.ContainingResource(), newAddr.ContainingResource()) + diags = diags.Append(crossTypeMoveDiags) + diags = diags.Append(crossTypeMove.applyCrossTypeMove(stmt, iAddr, newAddr, syncState)) recordOldAddr(iAddr, newAddr) - - state.MoveAbsResourceInstance(iAddr, newAddr) + syncState.MoveResourceInstance(iAddr, newAddr) continue } } @@ -183,8 +202,10 @@ func ApplyMoves(stmts []MoveStatement, state *states.State) MoveResults { } } } + syncState.Close() - return ret + diags = diags.Append(crossTypeMover.close()) + return ret, diags } // buildMoveStatementGraph constructs a dependency graph of the given move diff --git a/internal/refactoring/move_execute_test.go b/internal/refactoring/move_execute_test.go index edc8afb9fd..cc2047c849 100644 --- a/internal/refactoring/move_execute_test.go +++ b/internal/refactoring/move_execute_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package refactoring import ( @@ -12,14 +15,19 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" ) func TestApplyMoves(t *testing.T) { - providerAddr := addrs.AbsProviderConfig{ + barProviderAddress := addrs.AbsProviderConfig{ Module: addrs.RootModule, Provider: addrs.MustParseProviderSourceString("example.com/foo/bar"), } + fooProviderAddress := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/bar/foo"), + } mustParseInstAddr := func(s string) addrs.AbsResourceInstance { addr, err := addrs.ParseAbsResourceInstanceStr(s) @@ -35,47 +43,53 @@ func TestApplyMoves(t *testing.T) { Stmts []MoveStatement State *states.State + // We only need providers if we are doing a cross-resource type move + // so most of the test cases don't need this. + Providers map[addrs.Provider]providers.Factory + WantResults MoveResults + WantDiags []string WantInstanceAddrs []string }{ "no moves and empty state": { - []MoveStatement{}, - states.NewState(), - emptyResults, - nil, + Stmts: []MoveStatement{}, + State: states.NewState(), + Providers: nil, + WantResults: emptyResults, + WantInstanceAddrs: nil, }, "no moves": { - []MoveStatement{}, - states.BuildState(func(s *states.SyncState) { + Stmts: []MoveStatement{}, + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - emptyResults, - []string{ + WantResults: emptyResults, + WantInstanceAddrs: []string{ `foo.from`, }, }, "single move of whole singleton resource": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "foo.to"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "foo.to", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("foo.to"), MoveSuccess{ From: mustParseInstAddr("foo.from"), @@ -84,25 +98,25 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `foo.to`, }, }, "single move of whole 'count' resource": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "foo.to"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "foo.to", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from[0]"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("foo.to[0]"), MoveSuccess{ From: mustParseInstAddr("foo.from[0]"), @@ -111,26 +125,26 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `foo.to[0]`, }, }, "chained move of whole singleton resource": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "foo.mid"), - testMoveStatement(t, "", "foo.mid", "foo.to"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "foo.mid", &barProviderAddress), + testMoveStatement(t, "", "foo.mid", "foo.to", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("foo.to"), MoveSuccess{ From: mustParseInstAddr("foo.from"), @@ -139,26 +153,26 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `foo.to`, }, }, "move whole resource into module": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "module.boo.foo.to"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "module.boo.foo.to", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from[0]"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.boo.foo.to[0]"), MoveSuccess{ From: mustParseInstAddr("foo.from[0]"), @@ -167,26 +181,26 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.boo.foo.to[0]`, }, }, "move resource instance between modules": { - []MoveStatement{ - testMoveStatement(t, "", "module.boo.foo.from[0]", "module.bar[0].foo.to[0]"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.boo.foo.from[0]", "module.bar[0].foo.to[0]", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.from[0]"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar[0].foo.to[0]"), MoveSuccess{ From: mustParseInstAddr("module.boo.foo.from[0]"), @@ -195,23 +209,23 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].foo.to[0]`, }, }, "module move with child module": { - []MoveStatement{ - testMoveStatement(t, "", "module.boo", "module.bar"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.boo", "module.bar", nil), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.module.hoo.foo.from"), @@ -219,10 +233,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar.foo.from"), MoveSuccess{ From: mustParseInstAddr("module.boo.foo.from"), @@ -235,27 +249,27 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar.foo.from`, `module.bar.module.hoo.foo.from`, }, }, "move whole single module to indexed module": { - []MoveStatement{ - testMoveStatement(t, "", "module.boo", "module.bar[0]"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.boo", "module.bar[0]", nil), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.from[0]"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar[0].foo.from[0]"), MoveSuccess{ From: mustParseInstAddr("module.boo.foo.from[0]"), @@ -264,27 +278,27 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].foo.from[0]`, }, }, "move whole module to indexed module and move instance chained": { - []MoveStatement{ - testMoveStatement(t, "", "module.boo", "module.bar[0]"), - testMoveStatement(t, "bar", "foo.from[0]", "foo.to[0]"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.boo", "module.bar[0]", nil), + testMoveStatement(t, "bar", "foo.from[0]", "foo.to[0]", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.from[0]"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar[0].foo.to[0]"), MoveSuccess{ From: mustParseInstAddr("module.boo.foo.from[0]"), @@ -293,27 +307,27 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].foo.to[0]`, }, }, "move instance to indexed module and instance chained": { - []MoveStatement{ - testMoveStatement(t, "", "module.boo.foo.from[0]", "module.bar[0].foo.from[0]"), - testMoveStatement(t, "bar", "foo.from[0]", "foo.to[0]"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.boo.foo.from[0]", "module.bar[0].foo.from[0]", &barProviderAddress), + testMoveStatement(t, "bar", "foo.from[0]", "foo.to[0]", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.from[0]"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar[0].foo.to[0]"), MoveSuccess{ From: mustParseInstAddr("module.boo.foo.from[0]"), @@ -322,23 +336,23 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].foo.to[0]`, }, }, "move module instance to already-existing module instance": { - []MoveStatement{ - testMoveStatement(t, "", "module.bar[0]", "module.boo"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.bar[0]", "module.boo", nil), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.bar[0].foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.to[0]"), @@ -346,10 +360,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ // Nothing moved, because the module.b address is already // occupied by another module. Changes: emptyResults.Changes, @@ -363,24 +377,24 @@ func TestApplyMoves(t *testing.T) { ), ), }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].foo.from`, `module.boo.foo.to[0]`, }, }, "move resource to already-existing resource": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "foo.to"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "foo.to", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("foo.to"), @@ -388,10 +402,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ // Nothing moved, because the from.to address is already // occupied by another resource. Changes: emptyResults.Changes, @@ -405,24 +419,24 @@ func TestApplyMoves(t *testing.T) { ), ), }, - []string{ + WantInstanceAddrs: []string{ `foo.from`, `foo.to`, }, }, "move resource instance to already-existing resource instance": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "foo.to[0]"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "foo.to[0]", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("foo.to[0]"), @@ -430,10 +444,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ // Nothing moved, because the from.to[0] address is already // occupied by another resource instance. Changes: emptyResults.Changes, @@ -447,27 +461,27 @@ func TestApplyMoves(t *testing.T) { ), ), }, - []string{ + WantInstanceAddrs: []string{ `foo.from`, `foo.to[0]`, }, }, "move resource and containing module": { - []MoveStatement{ - testMoveStatement(t, "", "module.boo", "module.bar[0]"), - testMoveStatement(t, "boo", "foo.from", "foo.to"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.boo", "module.bar[0]", nil), + testMoveStatement(t, "boo", "foo.from", "foo.to", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.boo.foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar[0].foo.to"), MoveSuccess{ From: mustParseInstAddr("module.boo.foo.from"), @@ -476,24 +490,24 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].foo.to`, }, }, "move module and then move resource into it": { - []MoveStatement{ - testMoveStatement(t, "", "module.bar[0]", "module.boo"), - testMoveStatement(t, "", "foo.from", "module.boo.foo.from"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.bar[0]", "module.boo", nil), + testMoveStatement(t, "", "foo.from", "module.boo.foo.from", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.bar[0].foo.to"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), @@ -501,10 +515,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.boo.foo.from"), MoveSuccess{ mustParseInstAddr("foo.from"), @@ -517,26 +531,26 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.boo.foo.from`, `module.boo.foo.to`, }, }, "move resources into module and then move module": { - []MoveStatement{ - testMoveStatement(t, "", "foo.from", "module.boo.foo.to"), - testMoveStatement(t, "", "bar.from", "module.boo.bar.to"), - testMoveStatement(t, "", "module.boo", "module.bar[0]"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "module.boo.foo.to", &barProviderAddress), + testMoveStatement(t, "", "bar.from", "module.boo.bar.to", &barProviderAddress), + testMoveStatement(t, "", "module.boo", "module.bar[0]", nil), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("bar.from"), @@ -544,10 +558,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.bar[0].foo.to"), MoveSuccess{ mustParseInstAddr("foo.from"), @@ -560,25 +574,25 @@ func TestApplyMoves(t *testing.T) { ), Blocked: emptyResults.Blocked, }, - []string{ + WantInstanceAddrs: []string{ `module.bar[0].bar.to`, `module.bar[0].foo.to`, }, }, "module move collides with resource move": { - []MoveStatement{ - testMoveStatement(t, "", "module.bar[0]", "module.boo"), - testMoveStatement(t, "", "foo.from", "module.boo.foo.from"), + Stmts: []MoveStatement{ + testMoveStatement(t, "", "module.bar[0]", "module.boo", nil), + testMoveStatement(t, "", "foo.from", "module.boo.foo.from", &barProviderAddress), }, - states.BuildState(func(s *states.SyncState) { + State: states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent( mustParseInstAddr("module.bar[0].foo.from"), &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) s.SetResourceInstanceCurrent( mustParseInstAddr("foo.from"), @@ -586,10 +600,10 @@ func TestApplyMoves(t *testing.T) { Status: states.ObjectReady, AttrsJSON: []byte(`{}`), }, - providerAddr, + barProviderAddress, ) }), - MoveResults{ + WantResults: MoveResults{ Changes: addrs.MakeMap( addrs.MakeMapElem(mustParseInstAddr("module.boo.foo.from"), MoveSuccess{ mustParseInstAddr("module.bar[0].foo.from"), @@ -606,11 +620,161 @@ func TestApplyMoves(t *testing.T) { ), ), }, - []string{ + WantInstanceAddrs: []string{ `foo.from`, `module.boo.foo.from`, }, }, + + "cross resource type move unsupported": { + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "bar.to", &barProviderAddress), + }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + mustParseInstAddr("foo.from"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{}`), + }, + barProviderAddress, + ) + }), + Providers: map[addrs.Provider]providers.Factory{ + barProviderAddress.Provider: func() (providers.Interface, error) { + return &mockProvider{ + moveResourceState: false, + moveResourceError: nil, + }, nil + }, + }, + WantResults: MoveResults{ + Changes: addrs.MakeMap( + addrs.MakeMapElem(mustParseInstAddr("bar.to"), MoveSuccess{ + From: mustParseInstAddr("foo.from"), + To: mustParseInstAddr("bar.to"), + }), + ), + Blocked: emptyResults.Blocked, + }, + WantDiags: []string{ + "(Error) Unsupported `moved` across resource types:The provider \"example.com/foo/bar\" does not support moved operations across resource types and providers.", + }, + WantInstanceAddrs: []string{ + `bar.to`, + }, + }, + "cross resource type move errors": { + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "bar.to", &barProviderAddress), + }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + mustParseInstAddr("foo.from"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{}`), + }, + barProviderAddress, + ) + }), + Providers: map[addrs.Provider]providers.Factory{ + barProviderAddress.Provider: func() (providers.Interface, error) { + return &mockProvider{ + moveResourceState: true, + moveResourceError: fmt.Errorf("provider can't move between those resource types"), + }, nil + }, + }, + WantResults: MoveResults{ + Changes: addrs.MakeMap( + addrs.MakeMapElem(mustParseInstAddr("bar.to"), MoveSuccess{ + From: mustParseInstAddr("foo.from"), + To: mustParseInstAddr("bar.to"), + }), + ), + Blocked: emptyResults.Blocked, + }, + WantDiags: []string{ + "(Error) expected error:provider can't move between those resource types", + }, + WantInstanceAddrs: []string{ + `bar.to`, + }, + }, + "cross resource type move": { + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "bar.to", &barProviderAddress), + }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + mustParseInstAddr("foo.from"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{}`), + }, + barProviderAddress, + ) + }), + Providers: map[addrs.Provider]providers.Factory{ + barProviderAddress.Provider: func() (providers.Interface, error) { + return &mockProvider{ + moveResourceState: true, + moveResourceError: nil, + }, nil + }, + }, + WantResults: MoveResults{ + Changes: addrs.MakeMap( + addrs.MakeMapElem(mustParseInstAddr("bar.to"), MoveSuccess{ + From: mustParseInstAddr("foo.from"), + To: mustParseInstAddr("bar.to"), + }), + ), + Blocked: emptyResults.Blocked, + }, + WantInstanceAddrs: []string{ + `bar.to`, + }, + }, + "cross provider move": { + Stmts: []MoveStatement{ + testMoveStatement(t, "", "foo.from", "bar.to", &barProviderAddress), + }, + State: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + mustParseInstAddr("foo.from"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{}`), + }, + fooProviderAddress, + ) + }), + Providers: map[addrs.Provider]providers.Factory{ + barProviderAddress.Provider: func() (providers.Interface, error) { + return &mockProvider{ + moveResourceState: true, + moveResourceError: nil, + }, nil + }, + fooProviderAddress.Provider: func() (providers.Interface, error) { + return &mockProvider{}, nil + }, + }, + WantResults: MoveResults{ + Changes: addrs.MakeMap( + addrs.MakeMapElem(mustParseInstAddr("bar.to"), MoveSuccess{ + From: mustParseInstAddr("foo.from"), + To: mustParseInstAddr("bar.to"), + }), + ), + Blocked: emptyResults.Blocked, + }, + WantInstanceAddrs: []string{ + `bar.to`, + }, + }, } for name, test := range tests { @@ -624,7 +788,15 @@ func TestApplyMoves(t *testing.T) { t.Logf("resource instances in prior state:\n%s", spew.Sdump(allResourceInstanceAddrsInState(test.State))) state := test.State.DeepCopy() // don't modify the test case in-place - gotResults := ApplyMoves(test.Stmts, state) + gotResults, diags := ApplyMoves(test.Stmts, state, test.Providers) + + var actualDiags []string + for _, diag := range diags { + actualDiags = append(actualDiags, fmt.Sprintf("(%s) %s:%s", diag.Severity(), diag.Description().Summary, diag.Description().Detail)) + } + if diff := cmp.Diff(test.WantDiags, actualDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } if diff := cmp.Diff(test.WantResults, gotResults); diff != "" { t.Errorf("wrong results\n%s", diff) @@ -638,7 +810,7 @@ func TestApplyMoves(t *testing.T) { } } -func testMoveStatement(t *testing.T, module string, from string, to string) MoveStatement { +func testMoveStatement(t *testing.T, module string, from string, to string, provider *addrs.AbsProviderConfig) MoveStatement { t.Helper() moduleAddr := addrs.RootModule @@ -668,12 +840,19 @@ func testMoveStatement(t *testing.T, module string, from string, to string) Move t.Fatalf("incompatible endpoints") } - return MoveStatement{ + stmt := MoveStatement{ From: fromInModule, To: toInModule, // DeclRange not populated because it's unimportant for our tests } + + if provider != nil { + // Only set the provider for resource type moves. + stmt.Provider = provider + } + + return stmt } func allResourceInstanceAddrsInState(state *states.State) []string { diff --git a/internal/refactoring/move_statement.go b/internal/refactoring/move_statement.go index 08fffeb6f4..46aa5b6991 100644 --- a/internal/refactoring/move_statement.go +++ b/internal/refactoring/move_statement.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package refactoring import ( @@ -13,6 +16,14 @@ type MoveStatement struct { From, To *addrs.MoveEndpointInModule DeclRange tfdiags.SourceRange + // Provider is the provider configuration that applies to the "to" address + // of this move. As in, the provider that will manage the resource after + // it has been moved. + // + // This may be null if the "to" address points to a module instead of a + // resource. + Provider *addrs.AbsProviderConfig + // Implied is true for statements produced by ImpliedMoveStatements, and // false for statements produced by FindMoveStatements. // @@ -44,12 +55,43 @@ func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStateme panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange)) } - into = append(into, MoveStatement{ + stmt := MoveStatement{ From: fromAddr, To: toAddr, DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), Implied: false, - }) + } + + // We have the statement, let's see if we should attach a provider to + // it. + if toResource, ok := mc.To.ConfigMoveable(addrs.RootModule).(addrs.ConfigResource); ok { + // Only attach providers if we are moving resources, and we attach + // the to resource provider from the config. We can retrieve the + // from resource provider from the state later. + modCfg := cfg.Descendant(toResource.Module) + // It's possible that multiple refactorings have left a moved block + // that points to a module which no longer exists. This may also be + // a mistake, but the user will see the unexpected deletion in the + // plan if it is. + if modCfg != nil { + resourceConfig := modCfg.Module.ResourceByAddr(toResource.Resource) + if resourceConfig != nil { + // Check the target resource config actually exists before we + // try and extract the provider from them. + + stmt.Provider = &addrs.AbsProviderConfig{ + Module: modAddr, + Provider: resourceConfig.Provider, + } + + if resourceConfig.ProviderConfigRef != nil { + stmt.Provider.Alias = resourceConfig.ProviderConfigRef.Alias + } + } + } + } + + into = append(into, stmt) } for _, childCfg := range cfg.Children { @@ -132,14 +174,25 @@ func impliedMoveStatements(cfg *configs.Config, prevRunState *states.State, expl } if fromKey != toKey { - // We mustn't generate an impied statement if the user already + // We mustn't generate an implied statement if the user already // wrote an explicit statement referring to this resource, // because they may wish to select an instance key other than // zero as the one to retain. if !haveMoveStatementForResource(rAddr, explicitStmts) { + + resource := cfg.Descendant(addrs.RootModule).Module.ResourceByAddr(rAddr.Resource) + provider := &addrs.AbsProviderConfig{ + Module: rAddr.Module.Module(), + Provider: resource.Provider, + } + if resource.ProviderConfigRef != nil { + provider.Alias = resource.ProviderConfigRef.Alias + } + into = append(into, MoveStatement{ From: addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(fromKey), approxSrcRange), To: addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(toKey), approxSrcRange), + Provider: provider, DeclRange: approxSrcRange, Implied: true, }) diff --git a/internal/refactoring/move_statement_test.go b/internal/refactoring/move_statement_test.go index a6f0f9f6ee..9332d06d97 100644 --- a/internal/refactoring/move_statement_test.go +++ b/internal/refactoring/move_statement_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package refactoring import ( @@ -115,8 +118,11 @@ func TestImpliedMoveStatements(t *testing.T) { got := ImpliedMoveStatements(rootCfg, prevRunState, explicitStmts) want := []MoveStatement{ { - From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), - To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), + From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), + Provider: &addrs.AbsProviderConfig{ + Provider: addrs.NewProvider("registry.terraform.io", "hashicorp", "foo"), + }, Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/move-statement-implied.tf", @@ -127,8 +133,12 @@ func TestImpliedMoveStatements(t *testing.T) { // Found implied moves in a nested module, ignoring the explicit moves { - From: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), - To: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), + From: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "formerly_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + To: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "formerly_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), + Provider: &addrs.AbsProviderConfig{ + Module: addrs.Module{"child"}, + Provider: addrs.NewProvider("registry.terraform.io", "hashicorp", "foo"), + }, Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/child/move-statement-implied.tf", @@ -138,8 +148,11 @@ func TestImpliedMoveStatements(t *testing.T) { }, { - From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("now_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), - To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("now_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("now_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), + To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("now_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + Provider: &addrs.AbsProviderConfig{ + Provider: addrs.NewProvider("registry.terraform.io", "hashicorp", "foo"), + }, Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/move-statement-implied.tf", @@ -150,8 +163,12 @@ func TestImpliedMoveStatements(t *testing.T) { // Found implied moves in a nested module, ignoring the explicit moves { - From: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "now_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), - To: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "now_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + From: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "now_count").Instance(addrs.NoKey), tfdiags.SourceRange{}), + To: addrs.ImpliedMoveStatementEndpoint(nestedResourceAddr("child", "now_count").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + Provider: &addrs.AbsProviderConfig{ + Module: addrs.Module{"child"}, + Provider: addrs.NewProvider("registry.terraform.io", "hashicorp", "foo"), + }, Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/child/move-statement-implied.tf", @@ -166,8 +183,11 @@ func TestImpliedMoveStatements(t *testing.T) { // situation where an object wants to move into an address already // occupied by another object. { - From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("ambiguous").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), - To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("ambiguous").Instance(addrs.NoKey), tfdiags.SourceRange{}), + From: addrs.ImpliedMoveStatementEndpoint(resourceAddr("ambiguous").Instance(addrs.IntKey(0)), tfdiags.SourceRange{}), + To: addrs.ImpliedMoveStatementEndpoint(resourceAddr("ambiguous").Instance(addrs.NoKey), tfdiags.SourceRange{}), + Provider: &addrs.AbsProviderConfig{ + Provider: addrs.NewProvider("registry.terraform.io", "hashicorp", "foo"), + }, Implied: true, DeclRange: tfdiags.SourceRange{ Filename: "testdata/move-statement-implied/move-statement-implied.tf", diff --git a/internal/refactoring/move_validate.go b/internal/refactoring/move_validate.go index 585f623687..6953ef2dfc 100644 --- a/internal/refactoring/move_validate.go +++ b/internal/refactoring/move_validate.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package refactoring import ( @@ -53,7 +56,7 @@ func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts // both stmt.From and stmt.To always belong to the same statement. fromMod, _ := stmt.From.ModuleCallTraversals() - for _, fromModInst := range declaredInsts.InstancesForModule(fromMod) { + for _, fromModInst := range declaredInsts.InstancesForModule(fromMod, false) { absFrom := stmt.From.InModuleInstance(fromModInst) absTo := stmt.To.InModuleInstance(fromModInst) @@ -155,18 +158,6 @@ func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts StmtRange: stmt.DeclRange, }) } - - // Resource types must match. - if resourceTypesDiffer(absFrom, absTo) { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Resource type mismatch", - Detail: fmt.Sprintf( - "This statement declares a move from %s to %s, which is a %s of a different type.", absFrom, absTo, noun, - ), - }) - } - } } @@ -251,19 +242,6 @@ func moveableObjectExists(addr addrs.AbsMoveable, in instances.Set) bool { } } -func resourceTypesDiffer(absFrom, absTo addrs.AbsMoveable) bool { - switch absFrom := absFrom.(type) { - case addrs.AbsMoveableResource: - // addrs.UnifyMoveEndpoints guarantees that both addresses are of the - // same kind, so at this point we can assume that absTo is also an - // addrs.AbsResourceInstance or addrs.AbsResource. - absTo := absTo.(addrs.AbsMoveableResource) - return absFrom.AffectedAbsResource().Resource.Type != absTo.AffectedAbsResource().Resource.Type - default: - return false - } -} - func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiags.SourceRange, bool) { switch addr := addr.(type) { case addrs.ModuleInstance: @@ -272,7 +250,7 @@ func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiag // (NOTE: This assumes "addr" can never be the root module instance, // because the root module is never moveable.) parentAddr, callAddr := addr.Call() - modCfg := cfg.DescendentForInstance(parentAddr) + modCfg := cfg.DescendantForInstance(parentAddr) if modCfg == nil { return tfdiags.SourceRange{}, false } @@ -293,7 +271,7 @@ func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiag return tfdiags.SourceRangeFromHCL(call.DeclRange), true } case addrs.AbsModuleCall: - modCfg := cfg.DescendentForInstance(addr.Module) + modCfg := cfg.DescendantForInstance(addr.Module) if modCfg == nil { return tfdiags.SourceRange{}, false } @@ -303,7 +281,7 @@ func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiag } return tfdiags.SourceRangeFromHCL(call.DeclRange), true case addrs.AbsResourceInstance: - modCfg := cfg.DescendentForInstance(addr.Module) + modCfg := cfg.DescendantForInstance(addr.Module) if modCfg == nil { return tfdiags.SourceRange{}, false } @@ -324,7 +302,7 @@ func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiag return tfdiags.SourceRangeFromHCL(rc.DeclRange), true } case addrs.AbsResource: - modCfg := cfg.DescendentForInstance(addr.Module) + modCfg := cfg.DescendantForInstance(addr.Module) if modCfg == nil { return tfdiags.SourceRange{}, false } diff --git a/internal/refactoring/move_validate_test.go b/internal/refactoring/move_validate_test.go index 56d767af51..0eba52e7cf 100644 --- a/internal/refactoring/move_validate_test.go +++ b/internal/refactoring/move_validate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package refactoring import ( @@ -431,24 +434,6 @@ Each resource can have moved from only one source resource.`, }, WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to }, - "resource type mismatch": { - Statements: []MoveStatement{ - makeTestMoveStmt(t, ``, - `test.nonexist1`, - `other.single`, - ), - }, - WantError: `Resource type mismatch: This statement declares a move from test.nonexist1 to other.single, which is a resource of a different type.`, - }, - "resource instance type mismatch": { - Statements: []MoveStatement{ - makeTestMoveStmt(t, ``, - `test.nonexist1[0]`, - `other.single`, - ), - }, - WantError: `Resource type mismatch: This statement declares a move from test.nonexist1[0] to other.single, which is a resource instance of a different type.`, - }, "crossing nested statements": { // overlapping nested moves will result in a cycle. Statements: []MoveStatement{ @@ -534,8 +519,8 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance loader, cleanup := configload.NewLoaderForTests(t) defer cleanup() - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) - _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } @@ -551,7 +536,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance t.Fatalf("failed to load root module: %s", diags.Error()) } - expander := instances.NewExpander() + expander := instances.NewExpander(nil) staticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander) return rootCfg, expander.AllInstances() } @@ -559,7 +544,7 @@ func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instance func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) { t.Helper() - modCfg := rootCfg.DescendentForInstance(moduleAddr) + modCfg := rootCfg.DescendantForInstance(moduleAddr) if modCfg == nil { t.Fatalf("no configuration for %s", moduleAddr) } @@ -612,7 +597,7 @@ func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleA // We need to recursively analyze the child modules too. calledMod := modCfg.Path.Child(call.Name) - for _, inst := range expander.ExpandModule(calledMod) { + for _, inst := range expander.ExpandModule(calledMod, false) { staticPopulateExpanderModule(t, rootCfg, inst, expander) } } diff --git a/internal/refactoring/remove_statement.go b/internal/refactoring/remove_statement.go new file mode 100644 index 0000000000..94f2e3f392 --- /dev/null +++ b/internal/refactoring/remove_statement.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package refactoring + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// RemoveStatement is the fully-specified form of addrs.Remove +type RemoveStatement struct { + // From is the absolute address of the configuration object being removed. + From addrs.ConfigMoveable + + // Destroy indicates that the resource should be destroyed, not just removed + // from state. + Destroy bool + DeclRange tfdiags.SourceRange +} + +// FindRemoveStatements recurses through the modules of the given configuration +// and returns a set of all "removed" blocks defined within after deduplication +// on the From address. +// +// Error diagnostics are returned if any resource or module targeted by a remove +// block is still defined in configuration. +// +// A "removed" block in a parent module overrides a removed block in a child +// module when both target the same configuration object. +func FindRemoveStatements(rootCfg *configs.Config) (addrs.Map[addrs.ConfigMoveable, RemoveStatement], tfdiags.Diagnostics) { + stmts := findRemoveStatements(rootCfg, addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement]()) + diags := validateRemoveStatements(rootCfg, stmts) + return stmts, diags +} + +func validateRemoveStatements(cfg *configs.Config, stmts addrs.Map[addrs.ConfigMoveable, RemoveStatement]) (diags tfdiags.Diagnostics) { + for _, rst := range stmts.Keys() { + switch rst := rst.(type) { + case addrs.ConfigResource: + m := cfg.Descendant(rst.Module) + if m == nil { + break + } + + if r := m.Module.ResourceByAddr(rst.Resource); r != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Removed resource still exists", + Detail: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst), + Subject: r.DeclRange.Ptr(), + }) + } + case addrs.Module: + if m := cfg.Descendant(rst); m != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Removed module still exists", + Detail: fmt.Sprintf("This statement declares that %s was removed, but it is still declared in configuration.", rst), + Subject: m.CallRange.Ptr(), + }) + } + } + } + return diags +} + +func findRemoveStatements(cfg *configs.Config, into addrs.Map[addrs.ConfigMoveable, RemoveStatement]) addrs.Map[addrs.ConfigMoveable, RemoveStatement] { + for _, mc := range cfg.Module.Removed { + switch mc.From.ObjectKind() { + case addrs.RemoveTargetResource: + // First, stitch together the module path and the RelSubject to form + // the absolute address of the config object being removed. + res := mc.From.RelSubject.(addrs.ConfigResource) + fromAddr := addrs.ConfigResource{ + Module: append(cfg.Path, res.Module...), + Resource: res.Resource, + } + + // If we already have a remove statement for this ConfigResource, it + // must have come from a parent module, because duplicate removed + // blocks in the same module are ignored during parsing. + // The removed block in the parent module overrides the block in the + // child module. + existingStatement, ok := into.GetOk(fromAddr) + if ok { + if existingResource, ok := existingStatement.From.(addrs.ConfigResource); ok && + existingResource.Equal(fromAddr) { + continue + } + } + + into.Put(fromAddr, RemoveStatement{ + From: fromAddr, + Destroy: mc.Destroy, + DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), + }) + case addrs.RemoveTargetModule: + // First, stitch together the module path and the RelSubject to form + // the absolute address of the config object being removed. + mod := mc.From.RelSubject.(addrs.Module) + absMod := append(cfg.Path, mod...) + + // If there is already a statement for this Module, it must + // have come from a parent module, because duplicate removed blocks + // in the same module are ignored during parsing. + // The removed block in the parent module overrides the block in the + // child module. + existingStatement, ok := into.GetOk(mc.From.RelSubject) + if ok { + if existingModule, ok := existingStatement.From.(addrs.Module); ok && + existingModule.Equal(absMod) { + continue + } + } + + into.Put(absMod, RemoveStatement{ + From: absMod, + Destroy: mc.Destroy, + DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange), + }) + default: + panic("Unsupported remove target kind") + } + } + + for _, childCfg := range cfg.Children { + into = findRemoveStatements(childCfg, into) + } + + return into +} diff --git a/internal/refactoring/remove_statement_test.go b/internal/refactoring/remove_statement_test.go new file mode 100644 index 0000000000..29814d5644 --- /dev/null +++ b/internal/refactoring/remove_statement_test.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package refactoring + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestFindRemoveStatements(t *testing.T) { + // LAZY: We don't need the expanded instances from loadRefactoringFixture + // but reuse the helper function anyway. + rootCfg, _ := loadRefactoringFixture(t, "testdata/remove-statements") + + configResourceBasic := addrs.ConfigResource{ + Module: addrs.RootModule, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }, + } + + configResourceWithModule := addrs.ConfigResource{ + Module: addrs.Module{"gone"}, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "bar", + }, + } + + configModuleBasic := addrs.Module{"gone", "gonechild"} + + configResourceOverridden := addrs.ConfigResource{ + Module: addrs.Module{"child"}, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "baz", + }, + } + + configResourceInModule := addrs.ConfigResource{ + Module: addrs.Module{"child"}, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "boo", + }, + } + + configModuleInModule := addrs.Module{"child", "grandchild"} + + want := addrs.MakeMap[addrs.ConfigMoveable, RemoveStatement]( + addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceBasic, RemoveStatement{ + From: configResourceBasic, + Destroy: false, + DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{ + Filename: "testdata/remove-statements/main.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 27}, + End: hcl.Pos{Line: 2, Column: 8, Byte: 34}, + }), + }), + addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceWithModule, RemoveStatement{ + From: configResourceWithModule, + Destroy: false, + DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{ + Filename: "testdata/remove-statements/main.tf", + Start: hcl.Pos{Line: 10, Column: 1, Byte: 138}, + End: hcl.Pos{Line: 10, Column: 8, Byte: 145}, + }), + }), + addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configModuleBasic, RemoveStatement{ + From: configModuleBasic, + Destroy: false, + DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{ + Filename: "testdata/remove-statements/main.tf", + Start: hcl.Pos{Line: 18, Column: 1, Byte: 253}, + End: hcl.Pos{Line: 18, Column: 8, Byte: 260}, + }), + }), + addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceOverridden, RemoveStatement{ + From: configResourceOverridden, + Destroy: true, + DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{ + Filename: "testdata/remove-statements/main.tf", // the statement in the parent module takes precedence + Start: hcl.Pos{Line: 30, Column: 1, Byte: 428}, + End: hcl.Pos{Line: 30, Column: 8, Byte: 435}, + }), + }), + addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configResourceInModule, RemoveStatement{ + From: configResourceInModule, + Destroy: true, + DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{ + Filename: "testdata/remove-statements/child/main.tf", + Start: hcl.Pos{Line: 10, Column: 1, Byte: 141}, + End: hcl.Pos{Line: 10, Column: 8, Byte: 148}, + }), + }), + addrs.MakeMapElem[addrs.ConfigMoveable, RemoveStatement](configModuleInModule, RemoveStatement{ + From: configModuleInModule, + Destroy: false, + DeclRange: tfdiags.SourceRangeFromHCL(hcl.Range{ + Filename: "testdata/remove-statements/child/main.tf", + Start: hcl.Pos{Line: 18, Column: 1, Byte: 247}, + End: hcl.Pos{Line: 18, Column: 8, Byte: 254}, + }), + }), + ) + + got, diags := FindRemoveStatements(rootCfg) + if diags.HasErrors() { + t.Fatal(diags.Err().Error()) + } + + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} diff --git a/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf b/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf index 4ea628ea65..b8ea759a22 100644 --- a/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf +++ b/internal/refactoring/testdata/move-statement-implied/move-statement-implied.tf @@ -52,3 +52,8 @@ resource "foo" "ambiguous" { module "child" { source = "./child" } + +moved { + from = foo.already_moved + to = module.no_longer_exists.foo.already_moved +} diff --git a/internal/refactoring/testdata/remove-statements/child/main.tf b/internal/refactoring/testdata/remove-statements/child/main.tf new file mode 100644 index 0000000000..ab347f97be --- /dev/null +++ b/internal/refactoring/testdata/remove-statements/child/main.tf @@ -0,0 +1,23 @@ +# (overridden in parent module) +removed { + from = test_resource.baz + lifecycle { + destroy = false + } +} + +# removed resource - in module +removed { + from = test_resource.boo + lifecycle { + destroy = true + } +} + +# removed module - in module +removed { + from = module.grandchild + lifecycle { + destroy = false + } +} diff --git a/internal/refactoring/testdata/remove-statements/main.tf b/internal/refactoring/testdata/remove-statements/main.tf new file mode 100644 index 0000000000..6db007ad07 --- /dev/null +++ b/internal/refactoring/testdata/remove-statements/main.tf @@ -0,0 +1,35 @@ +# removed resource - basic +removed { + from = test_resource.foo + lifecycle { + destroy = false + } +} + +# removed resource - with module +removed { + from = module.gone.test_resource.bar + lifecycle { + destroy = false + } +} + +# removed module - basic +removed { + from = module.gone.module.gonechild + lifecycle { + destroy = false + } +} + +module "child" { + source = "./child" +} + +# removed resource - overridden from module +removed { + from = module.child.test_resource.baz + lifecycle { + destroy = true + } +} diff --git a/internal/registry/client.go b/internal/registry/client.go index 0204674bbf..7ade442c1a 100644 --- a/internal/registry/client.go +++ b/internal/registry/client.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package registry import ( diff --git a/internal/registry/client_test.go b/internal/registry/client_test.go index da3055110a..8588ae3840 100644 --- a/internal/registry/client_test.go +++ b/internal/registry/client_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package registry import ( diff --git a/internal/registry/errors.go b/internal/registry/errors.go index a35eb717ed..b176c9c134 100644 --- a/internal/registry/errors.go +++ b/internal/registry/errors.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package registry import ( diff --git a/internal/registry/regsrc/friendly_host.go b/internal/registry/regsrc/friendly_host.go index c9bc40bee8..47cd2f9ddf 100644 --- a/internal/registry/regsrc/friendly_host.go +++ b/internal/registry/regsrc/friendly_host.go @@ -1,10 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package regsrc import ( "regexp" "strings" - "github.com/hashicorp/terraform-svchost" + svchost "github.com/hashicorp/terraform-svchost" ) var ( diff --git a/internal/registry/regsrc/friendly_host_test.go b/internal/registry/regsrc/friendly_host_test.go index 37589685da..ad422636a3 100644 --- a/internal/registry/regsrc/friendly_host_test.go +++ b/internal/registry/regsrc/friendly_host_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package regsrc import ( diff --git a/internal/registry/regsrc/module.go b/internal/registry/regsrc/module.go index 3ffa002bba..a0f891f1b8 100644 --- a/internal/registry/regsrc/module.go +++ b/internal/registry/regsrc/module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package regsrc import ( diff --git a/internal/registry/regsrc/module_test.go b/internal/registry/regsrc/module_test.go index bae502b0d4..05fb1c9c09 100644 --- a/internal/registry/regsrc/module_test.go +++ b/internal/registry/regsrc/module_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package regsrc import ( diff --git a/internal/registry/regsrc/regsrc.go b/internal/registry/regsrc/regsrc.go index c430bf1413..aa66dbfc68 100644 --- a/internal/registry/regsrc/regsrc.go +++ b/internal/registry/regsrc/regsrc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package regsrc provides helpers for working with source strings that identify // resources within a Terraform registry. package regsrc diff --git a/internal/registry/response/module.go b/internal/registry/response/module.go index 3bd2b3df21..c353e13029 100644 --- a/internal/registry/response/module.go +++ b/internal/registry/response/module.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response import ( diff --git a/internal/registry/response/module_list.go b/internal/registry/response/module_list.go index 978374822f..bab6bdb495 100644 --- a/internal/registry/response/module_list.go +++ b/internal/registry/response/module_list.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response // ModuleList is the response structure for a pageable list of modules. diff --git a/internal/registry/response/module_provider.go b/internal/registry/response/module_provider.go index e48499dcee..afe0666c1a 100644 --- a/internal/registry/response/module_provider.go +++ b/internal/registry/response/module_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response // ModuleProvider represents a single provider for modules. diff --git a/internal/registry/response/module_versions.go b/internal/registry/response/module_versions.go index f69e9750c2..a2dafa0238 100644 --- a/internal/registry/response/module_versions.go +++ b/internal/registry/response/module_versions.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response // ModuleVersions is the response format that contains all metadata about module diff --git a/internal/registry/response/pagination.go b/internal/registry/response/pagination.go index 75a925490a..c2b567a5f5 100644 --- a/internal/registry/response/pagination.go +++ b/internal/registry/response/pagination.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response import ( diff --git a/internal/registry/response/pagination_test.go b/internal/registry/response/pagination_test.go index 09c78e6c2b..466ef644eb 100644 --- a/internal/registry/response/pagination_test.go +++ b/internal/registry/response/pagination_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response import ( diff --git a/internal/registry/response/redirect.go b/internal/registry/response/redirect.go index d5eb49ba66..65e365ccc0 100644 --- a/internal/registry/response/redirect.go +++ b/internal/registry/response/redirect.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package response // Redirect causes the frontend to perform a window redirect. diff --git a/internal/registry/test/mock_registry.go b/internal/registry/test/mock_registry.go index 079df1bfea..8f71125521 100644 --- a/internal/registry/test/mock_registry.go +++ b/internal/registry/test/mock_registry.go @@ -64,7 +64,7 @@ var ( ) // All the locationes from the mockRegistry start with a file:// scheme. If -// the the location string here doesn't have a scheme, the mockRegistry will +// the location string here doesn't have a scheme, the mockRegistry will // find the absolute path and return a complete URL. var testMods = map[string][]testMod{ "registry/foo/bar": {{ diff --git a/internal/releaseauth/all.go b/internal/releaseauth/all.go new file mode 100644 index 0000000000..95c027d138 --- /dev/null +++ b/internal/releaseauth/all.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +// Authenticator is a generic interface for interacting with types that authenticate +// an archive. +type Authenticator interface { + Authenticate() error +} + +// All is a meta Authenticator that wraps other Authenticators and ensures they all +// return without failure. +type All struct { + Authenticator + authenticators []Authenticator +} + +var _ Authenticator = All{} + +// AllAuthenticators creates a meta Authenticator that ensures all the +// given Authenticators return without failure. +func AllAuthenticators(authenticators ...Authenticator) All { + return All{ + authenticators: authenticators, + } +} + +// Authenticate returns the first archive authentication failure from +// the list of Authenticators given. +func (a All) Authenticate() error { + for _, auth := range a.authenticators { + if err := auth.Authenticate(); err != nil { + return err + } + } + return nil +} diff --git a/internal/releaseauth/all_test.go b/internal/releaseauth/all_test.go new file mode 100644 index 0000000000..59baa94c64 --- /dev/null +++ b/internal/releaseauth/all_test.go @@ -0,0 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +import ( + "os" + "testing" +) + +func TestAll(t *testing.T) { + // `sha256sum testdata/sample_release/sample_0.1.0_darwin_amd64.zip | cut -d' ' -f1` + actualChecksum, err := SHA256FromHex("22db2f0c70b50cff42afd4878fea9f6848a63f1b6532bd8b64b899f574acb35d") + if err != nil { + t.Fatal(err) + } + sums, err := os.ReadFile("testdata/sample_release/sample_0.1.0_SHA256SUMS") + if err != nil { + t.Fatal(err) + } + signature, err := os.ReadFile("testdata/sample_release/sample_0.1.0_SHA256SUMS.sig") + if err != nil { + t.Fatal(err) + } + publicKey, err := os.ReadFile("testdata/sample.public.key") + if err != nil { + t.Fatal(err) + } + + sigAuth := NewSignatureAuthentication(signature, sums) + sigAuth.PublicKey = string(publicKey) + + all := AllAuthenticators( + NewChecksumAuthentication(actualChecksum, "testdata/sample_release/sample_0.1.0_darwin_amd64.zip"), + sigAuth, + ) + + if err := all.Authenticate(); err != nil { + t.Fatal(err) + } +} diff --git a/internal/releaseauth/checksum.go b/internal/releaseauth/checksum.go new file mode 100644 index 0000000000..d46addd5f7 --- /dev/null +++ b/internal/releaseauth/checksum.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "io" + "log" + "os" +) + +// ChecksumAuthentication is an archive Authenticator that ensures a given file +// matches a SHA-256 checksum. It is important to verify the authenticity of the +// given checksum prior to using this Authenticator. +type ChecksumAuthentication struct { + Authenticator + + expected SHA256Hash + archiveLocation string +} + +// ErrChecksumDoesNotMatch is the error returned when the archive checksum does +// not match the given checksum. +var ErrChecksumDoesNotMatch = errors.New("downloaded archive does not match the release checksum") + +// NewChecksumAuthentication creates an instance of ChecksumAuthentication with the given +// checksum and file location. +func NewChecksumAuthentication(expected SHA256Hash, archiveLocation string) *ChecksumAuthentication { + return &ChecksumAuthentication{ + expected: expected, + archiveLocation: archiveLocation, + } +} + +func (a ChecksumAuthentication) Authenticate() error { + f, err := os.Open(a.archiveLocation) + if err != nil { + return fmt.Errorf("failed to open downloaded archive: %w", err) + } + defer f.Close() + + h := sha256.New() + _, err = io.Copy(h, f) + if err != nil { + return fmt.Errorf("failed to hash downloaded archive: %w", err) + } + + gotHash := h.Sum(nil) + log.Printf("[TRACE] checksummed %q; got hash %x, expected %x", f.Name(), gotHash, a.expected) + if !bytes.Equal(gotHash, a.expected[:]) { + return ErrChecksumDoesNotMatch + } + + return nil +} diff --git a/internal/releaseauth/doc.go b/internal/releaseauth/doc.go new file mode 100644 index 0000000000..23d8c36bf9 --- /dev/null +++ b/internal/releaseauth/doc.go @@ -0,0 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package releaseauth helps authenticates archives downloaded from a service +// like releases.hashicorp.com by providing some simple authentication tools: +// +// 1. Matching reported SHA-256 hash against a standard SHA256SUMS file. +// 2. Calculates the SHA-256 checksum of an archive and compares it against a reported hash. +// 3. Ensures the checksums were signed by HashiCorp. +package releaseauth diff --git a/internal/releaseauth/hash.go b/internal/releaseauth/hash.go new file mode 100644 index 0000000000..1a8105d610 --- /dev/null +++ b/internal/releaseauth/hash.go @@ -0,0 +1,76 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "log" +) + +// SHA256Hash represents a 256-bit SHA hash +type SHA256Hash [sha256.Size]byte + +// ErrInvalidSHA256Hash is returned when the hash is invalid +var ErrInvalidSHA256Hash = errors.New("the value was not a valid SHA-256 hash") + +// SHA256FromHex decodes a SHA256Hash from a hex string dump +func SHA256FromHex(hashHex string) (SHA256Hash, error) { + var result [sha256.Size]byte + hash, err := hex.DecodeString(hashHex) + if err != nil || len(hash) != sha256.Size { + return result, ErrInvalidSHA256Hash + } + + if copy(result[:], hash) != sha256.Size { + panic("could not copy hash value") + } + + return result, nil +} + +// SHA256Checksums decodes a file generated by the sha256sum program +type SHA256Checksums map[string]SHA256Hash + +func ParseChecksums(data []byte) (SHA256Checksums, error) { + items := bytes.Split(data, []byte("\n")) + result := make(map[string]SHA256Hash, len(items)) + + for _, line := range items { + parts := bytes.SplitN(line, []byte(" "), 2) + + if len(parts) != 2 { + break + } + + log.Printf("[TRACE] parsing SHA256SUMS %q = %q", parts[0], parts[1]) + hash, err := SHA256FromHex(string(parts[0])) + if err != nil { + return result, fmt.Errorf("failed to parse checksums: %w", err) + } + + result[string(parts[1])] = hash + } + + return result, nil +} + +// Validate retrieves a SHA256Hash for the a filename and compares it +// to the specified hash. Validate returns an error if the hash is not found +// or if it does not match. +func (c SHA256Checksums) Validate(filename string, hash SHA256Hash) error { + sum, ok := c[filename] + if !ok { + return fmt.Errorf("no checksum found for filename %q", filename) + } + + if sum != hash { + return fmt.Errorf("checksums do not match") + } + + return nil +} diff --git a/internal/releaseauth/hash_test.go b/internal/releaseauth/hash_test.go new file mode 100644 index 0000000000..fe53baf418 --- /dev/null +++ b/internal/releaseauth/hash_test.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +import ( + "strings" + "testing" +) + +func Test_ParseChecksums(t *testing.T) { + sample := `bb611bb4c082fec9943d3c315fcd7cacd7dabce43fbf79b8e6b451bb4e54096d terraform-cloudplugin_0.1.0-prototype_darwin_amd64.zip +295bf15c2af01d18ce7832d6d357667119e4b14eb8fd2454d506b23ed7825652 terraform-cloudplugin_0.1.0-prototype_darwin_arm64.zip +b0744b9c8c0eb7ea61824728c302d0fd4fda4bb841fb6b3e701ef9eb10adbc39 terraform-cloudplugin_0.1.0-prototype_freebsd_386.zip +8fc967d1402c5106fb0ca1b084b7edd2b11fd8d7c2225f5cd05584a56e0b2a16 terraform-cloudplugin_0.1.0-prototype_freebsd_amd64.zip +2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d terraform-cloudplugin_0.1.0-prototype_linux_386.zip +c877c8cebf76209c2c7d427d31e328212cd4716fdd8b6677939fd2a01e06a2d0 terraform-cloudplugin_0.1.0-prototype_linux_amd64.zip +97ff8fe4e2e853c9ea54605305732e5b16437045230a2df21f410e36edcfe7bd terraform-cloudplugin_0.1.0-prototype_linux_arm.zip +d415a1c39b9ec79bd00efe72d0bf14e557833b6c1ce9898f223a7dd22abd0241 terraform-cloudplugin_0.1.0-prototype_linux_arm64.zip +0f33a13eca612d1b3cda959d655a1535d69bcc1195dee37407c667c12c4900b5 terraform-cloudplugin_0.1.0-prototype_solaris_amd64.zip +a6d572e5064e1b1cf8b0b4e64bc058dc630313c95e975b44e0540f231655d31c terraform-cloudplugin_0.1.0-prototype_windows_386.zip +2aaceed12ebdf25d21f9953a09c328bd8892f5a5bd5382bd502f054478f56998 terraform-cloudplugin_0.1.0-prototype_windows_amd64.zip +` + + sums, err := ParseChecksums([]byte(sample)) + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + expectedSum, err := SHA256FromHex("2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d") + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + if found := sums["terraform-cloudplugin_0.1.0-prototype_linux_386.zip"]; found != expectedSum { + t.Errorf("Expected %q, got %q", expectedSum, found) + } +} + +func Test_ParseChecksums_Empty(t *testing.T) { + sample := ` +` + + sums, err := ParseChecksums([]byte(sample)) + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + expectedSum, err := SHA256FromHex("2f35b2fc748b6f279b067a4eefd65264f811a2ae86a969461851dae546aa402d") + if err != nil { + t.Fatalf("Expected no error, got: %s", err) + } + + err = sums.Validate("terraform-cloudplugin_0.1.0-prototype_linux_arm.zip", expectedSum) + if err == nil || !strings.Contains(err.Error(), "no checksum found for filename") { + t.Errorf("Expected error %q, got nil", "no checksum found for filename") + } +} + +func Test_ParseChecksums_BadFormat(t *testing.T) { + sample := `xxxxxxxxxxxxxxxxxxxxxx terraform-cloudplugin_0.1.0-prototype_darwin_amd64.zip + zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz terraform-cloudplugin_0.1.0-prototype_darwin_arm64.zip +` + + _, err := ParseChecksums([]byte(sample)) + if err == nil || !strings.Contains(err.Error(), "failed to parse checksums") { + t.Fatalf("Expected error %q, got: %s", "failed to parse checksums", err) + } +} diff --git a/internal/releaseauth/signature.go b/internal/releaseauth/signature.go new file mode 100644 index 0000000000..97ffc16d54 --- /dev/null +++ b/internal/releaseauth/signature.go @@ -0,0 +1,184 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package releaseauth + +import ( + "bytes" + "errors" + "fmt" + "log" + "strings" + + "github.com/ProtonMail/go-crypto/openpgp" +) + +// SignatureAuthentication is an archive Authenticator that validates that SHA256SUMS data +// was signed by the given signing key. +type SignatureAuthentication struct { + Authenticator + + // This can be overridden by tests to check arbitrary keys, rather than the HashiCorp public key + PublicKey string + signature []byte + signed []byte +} + +var _ Authenticator = SignatureAuthentication{} + +// ErrNotSignedByHashiCorp is the error returned when there is a mismatch between the SHA256SUMS +// signature data and the data itself. +var ErrNotSignedByHashiCorp = errors.New("failed to authenticate that the archive was signed by HashiCorp") + +// NewSignatureAuthentication creates a new Authenticator given some signature data +// (the SHA256SUMS.sig file), the signed data (the SHA256SUMS file), and a public key +func NewSignatureAuthentication(signature []byte, signed []byte) *SignatureAuthentication { + return &SignatureAuthentication{ + signature: signature, + signed: signed, + PublicKey: HashiCorpPublicKey, + } +} + +func (a SignatureAuthentication) Authenticate() error { + // Verify the signature using the HashiCorp public key. If this succeeds, + // this is an official provider. + hashicorpKeyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(a.PublicKey)) + if err != nil { + return fmt.Errorf("error creating HashiCorp keyring: %s", err) + } + + _, err = openpgp.CheckDetachedSignature(hashicorpKeyring, bytes.NewReader(a.signed), bytes.NewReader(a.signature), nil) + if err != nil { + log.Printf("[DEBUG] GPG reported an error while verifying detached signature: %s", err) + return ErrNotSignedByHashiCorp + } + + return nil +} + +// HashicorpPublicKey is the HashiCorp public key, also available at +// https://www.hashicorp.com/security +const HashiCorpPublicKeyID = "72D7468F" +const HashiCorpPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGB9+xkBEACabYZOWKmgZsHTdRDiyPJxhbuUiKX65GUWkyRMJKi/1dviVxOX +PG6hBPtF48IFnVgxKpIb7G6NjBousAV+CuLlv5yqFKpOZEGC6sBV+Gx8Vu1CICpl +Zm+HpQPcIzwBpN+Ar4l/exCG/f/MZq/oxGgH+TyRF3XcYDjG8dbJCpHO5nQ5Cy9h +QIp3/Bh09kET6lk+4QlofNgHKVT2epV8iK1cXlbQe2tZtfCUtxk+pxvU0UHXp+AB +0xc3/gIhjZp/dePmCOyQyGPJbp5bpO4UeAJ6frqhexmNlaw9Z897ltZmRLGq1p4a +RnWL8FPkBz9SCSKXS8uNyV5oMNVn4G1obCkc106iWuKBTibffYQzq5TG8FYVJKrh +RwWB6piacEB8hl20IIWSxIM3J9tT7CPSnk5RYYCTRHgA5OOrqZhC7JefudrP8n+M +pxkDgNORDu7GCfAuisrf7dXYjLsxG4tu22DBJJC0c/IpRpXDnOuJN1Q5e/3VUKKW +mypNumuQpP5lc1ZFG64TRzb1HR6oIdHfbrVQfdiQXpvdcFx+Fl57WuUraXRV6qfb +4ZmKHX1JEwM/7tu21QE4F1dz0jroLSricZxfaCTHHWNfvGJoZ30/MZUrpSC0IfB3 +iQutxbZrwIlTBt+fGLtm3vDtwMFNWM+Rb1lrOxEQd2eijdxhvBOHtlIcswARAQAB +tERIYXNoaUNvcnAgU2VjdXJpdHkgKGhhc2hpY29ycC5jb20vc2VjdXJpdHkpIDxz +ZWN1cml0eUBoYXNoaWNvcnAuY29tPokCVAQTAQoAPhYhBMh0AR8KtAURDQIQVTQ2 +XZRy10aPBQJgffsZAhsDBQkJZgGABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJ +EDQ2XZRy10aPtpcP/0PhJKiHtC1zREpRTrjGizoyk4Sl2SXpBZYhkdrG++abo6zs +buaAG7kgWWChVXBo5E20L7dbstFK7OjVs7vAg/OLgO9dPD8n2M19rpqSbbvKYWvp +0NSgvFTT7lbyDhtPj0/bzpkZEhmvQaDWGBsbDdb2dBHGitCXhGMpdP0BuuPWEix+ +QnUMaPwU51q9GM2guL45Tgks9EKNnpDR6ZdCeWcqo1IDmklloidxT8aKL21UOb8t +cD+Bg8iPaAr73bW7Jh8TdcV6s6DBFub+xPJEB/0bVPmq3ZHs5B4NItroZ3r+h3ke +VDoSOSIZLl6JtVooOJ2la9ZuMqxchO3mrXLlXxVCo6cGcSuOmOdQSz4OhQE5zBxx +LuzA5ASIjASSeNZaRnffLIHmht17BPslgNPtm6ufyOk02P5XXwa69UCjA3RYrA2P +QNNC+OWZ8qQLnzGldqE4MnRNAxRxV6cFNzv14ooKf7+k686LdZrP/3fQu2p3k5rY +0xQUXKh1uwMUMtGR867ZBYaxYvwqDrg9XB7xi3N6aNyNQ+r7zI2lt65lzwG1v9hg +FG2AHrDlBkQi/t3wiTS3JOo/GCT8BjN0nJh0lGaRFtQv2cXOQGVRW8+V/9IpqEJ1 +qQreftdBFWxvH7VJq2mSOXUJyRsoUrjkUuIivaA9Ocdipk2CkP8bpuGz7ZF4uQIN +BGB9+xkBEACoklYsfvWRCjOwS8TOKBTfl8myuP9V9uBNbyHufzNETbhYeT33Cj0M +GCNd9GdoaknzBQLbQVSQogA+spqVvQPz1MND18GIdtmr0BXENiZE7SRvu76jNqLp +KxYALoK2Pc3yK0JGD30HcIIgx+lOofrVPA2dfVPTj1wXvm0rbSGA4Wd4Ng3d2AoR +G/wZDAQ7sdZi1A9hhfugTFZwfqR3XAYCk+PUeoFrkJ0O7wngaon+6x2GJVedVPOs +2x/XOR4l9ytFP3o+5ILhVnsK+ESVD9AQz2fhDEU6RhvzaqtHe+sQccR3oVLoGcat +ma5rbfzH0Fhj0JtkbP7WreQf9udYgXxVJKXLQFQgel34egEGG+NlbGSPG+qHOZtY +4uWdlDSvmo+1P95P4VG/EBteqyBbDDGDGiMs6lAMg2cULrwOsbxWjsWka8y2IN3z +1stlIJFvW2kggU+bKnQ+sNQnclq3wzCJjeDBfucR3a5WRojDtGoJP6Fc3luUtS7V +5TAdOx4dhaMFU9+01OoH8ZdTRiHZ1K7RFeAIslSyd4iA/xkhOhHq89F4ECQf3Bt4 +ZhGsXDTaA/VgHmf3AULbrC94O7HNqOvTWzwGiWHLfcxXQsr+ijIEQvh6rHKmJK8R +9NMHqc3L18eMO6bqrzEHW0Xoiu9W8Yj+WuB3IKdhclT3w0pO4Pj8gQARAQABiQI8 +BBgBCgAmFiEEyHQBHwq0BRENAhBVNDZdlHLXRo8FAmB9+xkCGwwFCQlmAYAACgkQ +NDZdlHLXRo9ZnA/7BmdpQLeTjEiXEJyW46efxlV1f6THn9U50GWcE9tebxCXgmQf +u+Uju4hreltx6GDi/zbVVV3HCa0yaJ4JVvA4LBULJVe3ym6tXXSYaOfMdkiK6P1v +JgfpBQ/b/mWB0yuWTUtWx18BQQwlNEQWcGe8n1lBbYsH9g7QkacRNb8tKUrUbWlQ +QsU8wuFgly22m+Va1nO2N5C/eE/ZEHyN15jEQ+QwgQgPrK2wThcOMyNMQX/VNEr1 +Y3bI2wHfZFjotmek3d7ZfP2VjyDudnmCPQ5xjezWpKbN1kvjO3as2yhcVKfnvQI5 +P5Frj19NgMIGAp7X6pF5Csr4FX/Vw316+AFJd9Ibhfud79HAylvFydpcYbvZpScl +7zgtgaXMCVtthe3GsG4gO7IdxxEBZ/Fm4NLnmbzCIWOsPMx/FxH06a539xFq/1E2 +1nYFjiKg8a5JFmYU/4mV9MQs4bP/3ip9byi10V+fEIfp5cEEmfNeVeW5E7J8PqG9 +t4rLJ8FR4yJgQUa2gs2SNYsjWQuwS/MJvAv4fDKlkQjQmYRAOp1SszAnyaplvri4 +ncmfDsf0r65/sd6S40g5lHH8LIbGxcOIN6kwthSTPWX89r42CbY8GzjTkaeejNKx +v1aCrO58wAtursO1DiXCvBY7+NdafMRnoHwBk50iPqrVkNA8fv+auRyB2/G5Ag0E +YH3+JQEQALivllTjMolxUW2OxrXb+a2Pt6vjCBsiJzrUj0Pa63U+lT9jldbCCfgP +wDpcDuO1O05Q8k1MoYZ6HddjWnqKG7S3eqkV5c3ct3amAXp513QDKZUfIDylOmhU +qvxjEgvGjdRjz6kECFGYr6Vnj/p6AwWv4/FBRFlrq7cnQgPynbIH4hrWvewp3Tqw +GVgqm5RRofuAugi8iZQVlAiQZJo88yaztAQ/7VsXBiHTn61ugQ8bKdAsr8w/ZZU5 +HScHLqRolcYg0cKN91c0EbJq9k1LUC//CakPB9mhi5+aUVUGusIM8ECShUEgSTCi +KQiJUPZ2CFbbPE9L5o9xoPCxjXoX+r7L/WyoCPTeoS3YRUMEnWKvc42Yxz3meRb+ +BmaqgbheNmzOah5nMwPupJYmHrjWPkX7oyyHxLSFw4dtoP2j6Z7GdRXKa2dUYdk2 +x3JYKocrDoPHh3Q0TAZujtpdjFi1BS8pbxYFb3hHmGSdvz7T7KcqP7ChC7k2RAKO +GiG7QQe4NX3sSMgweYpl4OwvQOn73t5CVWYp/gIBNZGsU3Pto8g27vHeWyH9mKr4 +cSepDhw+/X8FGRNdxNfpLKm7Vc0Sm9Sof8TRFrBTqX+vIQupYHRi5QQCuYaV6OVr +ITeegNK3So4m39d6ajCR9QxRbmjnx9UcnSYYDmIB6fpBuwT0ogNtABEBAAGJBHIE +GAEKACYCGwIWIQTIdAEfCrQFEQ0CEFU0Nl2UctdGjwUCYH4bgAUJAeFQ2wJAwXQg +BBkBCgAdFiEEs2y6kaLAcwxDX8KAsLRBCXaFtnYFAmB9/iUACgkQsLRBCXaFtnYX +BhAAlxejyFXoQwyGo9U+2g9N6LUb/tNtH29RHYxy4A3/ZUY7d/FMkArmh4+dfjf0 +p9MJz98Zkps20kaYP+2YzYmaizO6OA6RIddcEXQDRCPHmLts3097mJ/skx9qLAf6 +rh9J7jWeSqWO6VW6Mlx8j9m7sm3Ae1OsjOx/m7lGZOhY4UYfY627+Jf7WQ5103Qs +lgQ09es/vhTCx0g34SYEmMW15Tc3eCjQ21b1MeJD/V26npeakV8iCZ1kHZHawPq/ +aCCuYEcCeQOOteTWvl7HXaHMhHIx7jjOd8XX9V+UxsGz2WCIxX/j7EEEc7CAxwAN +nWp9jXeLfxYfjrUB7XQZsGCd4EHHzUyCf7iRJL7OJ3tz5Z+rOlNjSgci+ycHEccL +YeFAEV+Fz+sj7q4cFAferkr7imY1XEI0Ji5P8p/uRYw/n8uUf7LrLw5TzHmZsTSC +UaiL4llRzkDC6cVhYfqQWUXDd/r385OkE4oalNNE+n+txNRx92rpvXWZ5qFYfv7E +95fltvpXc0iOugPMzyof3lwo3Xi4WZKc1CC/jEviKTQhfn3WZukuF5lbz3V1PQfI +xFsYe9WYQmp25XGgezjXzp89C/OIcYsVB1KJAKihgbYdHyUN4fRCmOszmOUwEAKR +3k5j4X8V5bk08sA69NVXPn2ofxyk3YYOMYWW8ouObnXoS8QJEDQ2XZRy10aPMpsQ +AIbwX21erVqUDMPn1uONP6o4NBEq4MwG7d+fT85rc1U0RfeKBwjucAE/iStZDQoM +ZKWvGhFR+uoyg1LrXNKuSPB82unh2bpvj4zEnJsJadiwtShTKDsikhrfFEK3aCK8 +Zuhpiu3jxMFDhpFzlxsSwaCcGJqcdwGhWUx0ZAVD2X71UCFoOXPjF9fNnpy80YNp +flPjj2RnOZbJyBIM0sWIVMd8F44qkTASf8K5Qb47WFN5tSpePq7OCm7s8u+lYZGK +wR18K7VliundR+5a8XAOyUXOL5UsDaQCK4Lj4lRaeFXunXl3DJ4E+7BKzZhReJL6 +EugV5eaGonA52TWtFdB8p+79wPUeI3KcdPmQ9Ll5Zi/jBemY4bzasmgKzNeMtwWP +fk6WgrvBwptqohw71HDymGxFUnUP7XYYjic2sVKhv9AevMGycVgwWBiWroDCQ9Ja +btKfxHhI2p+g+rcywmBobWJbZsujTNjhtme+kNn1mhJsD3bKPjKQfAxaTskBLb0V +wgV21891TS1Dq9kdPLwoS4XNpYg2LLB4p9hmeG3fu9+OmqwY5oKXsHiWc43dei9Y +yxZ1AAUOIaIdPkq+YG/PhlGE4YcQZ4RPpltAr0HfGgZhmXWigbGS+66pUj+Ojysc +j0K5tCVxVu0fhhFpOlHv0LWaxCbnkgkQH9jfMEJkAWMOuQINBGCAXCYBEADW6RNr +ZVGNXvHVBqSiOWaxl1XOiEoiHPt50Aijt25yXbG+0kHIFSoR+1g6Lh20JTCChgfQ +kGGjzQvEuG1HTw07YhsvLc0pkjNMfu6gJqFox/ogc53mz69OxXauzUQ/TZ27GDVp +UBu+EhDKt1s3OtA6Bjz/csop/Um7gT0+ivHyvJ/jGdnPEZv8tNuSE/Uo+hn/Q9hg +8SbveZzo3C+U4KcabCESEFl8Gq6aRi9vAfa65oxD5jKaIz7cy+pwb0lizqlW7H9t +Qlr3dBfdIcdzgR55hTFC5/XrcwJ6/nHVH/xGskEasnfCQX8RYKMuy0UADJy72TkZ +bYaCx+XXIcVB8GTOmJVoAhrTSSVLAZspfCnjwnSxisDn3ZzsYrq3cV6sU8b+QlIX +7VAjurE+5cZiVlaxgCjyhKqlGgmonnReWOBacCgL/UvuwMmMp5TTLmiLXLT7uxeG +ojEyoCk4sMrqrU1jevHyGlDJH9Taux15GILDwnYFfAvPF9WCid4UZ4Ouwjcaxfys +3LxNiZIlUsXNKwS3mhiMRL4TRsbs4k4QE+LIMOsauIvcvm8/frydvQ/kUwIhVTH8 +0XGOH909bYtJvY3fudK7ShIwm7ZFTduBJUG473E/Fn3VkhTmBX6+PjOC50HR/Hyb +waRCzfDruMe3TAcE/tSP5CUOb9C7+P+hPzQcDwARAQABiQRyBBgBCgAmFiEEyHQB +Hwq0BRENAhBVNDZdlHLXRo8FAmCAXCYCGwIFCQlmAYACQAkQNDZdlHLXRo/BdCAE +GQEKAB0WIQQ3TsdbSFkTYEqDHMfIIMbVzSerhwUCYIBcJgAKCRDIIMbVzSerh0Xw +D/9ghnUsoNCu1OulcoJdHboMazJvDt/znttdQSnULBVElgM5zk0Uyv87zFBzuCyQ +JWL3bWesQ2uFx5fRWEPDEfWVdDrjpQGb1OCCQyz1QlNPV/1M1/xhKGS9EeXrL8Dw +F6KTGkRwn1yXiP4BGgfeFIQHmJcKXEZ9HkrpNb8mcexkROv4aIPAwn+IaE+NHVtt +IBnufMXLyfpkWJQtJa9elh9PMLlHHnuvnYLvuAoOkhuvs7fXDMpfFZ01C+QSv1dz +Hm52GSStERQzZ51w4c0rYDneYDniC/sQT1x3dP5Xf6wzO+EhRMabkvoTbMqPsTEP +xyWr2pNtTBYp7pfQjsHxhJpQF0xjGN9C39z7f3gJG8IJhnPeulUqEZjhRFyVZQ6/ +siUeq7vu4+dM/JQL+i7KKe7Lp9UMrG6NLMH+ltaoD3+lVm8fdTUxS5MNPoA/I8cK +1OWTJHkrp7V/XaY7mUtvQn5V1yET5b4bogz4nME6WLiFMd+7x73gB+YJ6MGYNuO8 +e/NFK67MfHbk1/AiPTAJ6s5uHRQIkZcBPG7y5PpfcHpIlwPYCDGYlTajZXblyKrw +BttVnYKvKsnlysv11glSg0DphGxQJbXzWpvBNyhMNH5dffcfvd3eXJAxnD81GD2z +ZAriMJ4Av2TfeqQ2nxd2ddn0jX4WVHtAvLXfCgLM2Gveho4jD/9sZ6PZz/rEeTvt +h88t50qPcBa4bb25X0B5FO3TeK2LL3VKLuEp5lgdcHVonrcdqZFobN1CgGJua8TW +SprIkh+8ATZ/FXQTi01NzLhHXT1IQzSpFaZw0gb2f5ruXwvTPpfXzQrs2omY+7s7 +fkCwGPesvpSXPKn9v8uhUwD7NGW/Dm+jUM+QtC/FqzX7+/Q+OuEPjClUh1cqopCZ +EvAI3HjnavGrYuU6DgQdjyGT/UDbuwbCXqHxHojVVkISGzCTGpmBcQYQqhcFRedJ +yJlu6PSXlA7+8Ajh52oiMJ3ez4xSssFgUQAyOB16432tm4erpGmCyakkoRmMUn3p +wx+QIppxRlsHznhcCQKR3tcblUqH3vq5i4/ZAihusMCa0YrShtxfdSb13oKX+pFr +aZXvxyZlCa5qoQQBV1sowmPL1N2j3dR9TVpdTyCFQSv4KeiExmowtLIjeCppRBEK +eeYHJnlfkyKXPhxTVVO6H+dU4nVu0ASQZ07KiQjbI+zTpPKFLPp3/0sPRJM57r1+ +aTS71iR7nZNZ1f8LZV2OvGE6fJVtgJ1J4Nu02K54uuIhU3tg1+7Xt+IqwRc9rbVr +pHH/hFCYBPW2D2dxB+k2pQlg5NI+TpsXj5Zun8kRw5RtVb+dLuiH/xmxArIee8Jq +ZF5q4h4I33PSGDdSvGXn9UMY5Isjpg== +=7pIB +-----END PGP PUBLIC KEY BLOCK-----` diff --git a/internal/releaseauth/testdata/sample.md b/internal/releaseauth/testdata/sample.md new file mode 100644 index 0000000000..5d90462ed4 --- /dev/null +++ b/internal/releaseauth/testdata/sample.md @@ -0,0 +1,11 @@ +# Package releaseauth Test Data Signing Key + +This directory contains a private key that is only used for signing the test data, along with the public key that the package uses to verfify the signing. Here are the steps to reproduce the test data, which would be necessary if the archive and checksum changes. + +1. Import the secret key + +`gpg --import sample.private.key` + +2. Sign the sample_release SHA256SUMS file using the sample key: + +`gpg -u 200BDA882C95B80A --output sample_release/sample_0.1.0_SHA256SUMS.sig --detach-sig sample_release/sample_0.1.0_SHA256SUMS` diff --git a/internal/releaseauth/testdata/sample.private.key b/internal/releaseauth/testdata/sample.private.key new file mode 100644 index 0000000000..b83c2b0295 --- /dev/null +++ b/internal/releaseauth/testdata/sample.private.key @@ -0,0 +1,106 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQcYBGTD2nkBEACzN+0KhgkfyObviYGvtWWCQfznX440nIiu0uag1lRGh6MeupOw +cPFMtSSoltSYTTkS3J6UBvTOQbPq/IAX0H3WBnUWFJpA9h87fHbpxquEeYeUQ65r +J/IRLsPFnzOl43CCSnkDuppJqYSPJc1GJYtb4O8Tq7+Af7rdZFWoPW1bo7NIriUm +MM9y3udb8Catvz5L7aYevQ001x9jP1SzpkMVWY4/TxTrE7Xjxh7Ke7MlJmIeFWHl +ja/mjukcZZAD/KwHU+mid+MZhx1CmiN36CuYBcLpP2eJTrrhYOps8kX5z1sKsIaY +4KF1yVkLGQxGS8y2M0kZXjYtNPxY1DJGveWIRzcx+7KWkavkVbhvmrMRWLkKv+55 +BV6lsLrOYFrCTRxolNOx5ZNcUbaIucqGYe8RzQz/WlPPA3TbMnOQ7OSTIqhqMzzd +s7l7S4N7UHP+ePvMy1Uzajfsb80XI9zYRrRMshHjU6huYITwIF5wWzfRVegSfYw+ +bcxr5XWLYmJ5aMA8JNOhdjpszpbZcCZPYPV6Ya6jsXoO4QxD+ivGNBbzEuB+DolW +/6GXqfuJARPx1vPIAWX2j6uVhJb5ygBahd9aPjaURwuM9SZnCTCoc0x7171R6t+g +AIs/wB9D/iHbLEMUgH5KlW+9VtJLw2afiVAeSd38axvn/ELURvpMoJ1s5wARAQAB +AA/8CZ2IXLut6qQl68J2EEjMXkLkauoqtYeTzRlooUbOimLa7W+iM+G4uIkGzfrF +rjmFGeN3U0bU7z9W2VaHGCqp+F0r/q1yoeXymAed/lNClDQhrOKSuDy86o9r69hA +9kyvwSB2F1f1e4VE+kR+G8jbjhLmkNNyo1YhtEtz+GJPUeQvCXO6b4RH5Qhbjl23 +SDQjXKyD8KXrpaLzE/6WW0siJ0JA9QRCVuMdq6T6CvARv6kBs6PUYZ4KoB/ubC/G +HHNRLCrO4ADMUOwkYD89LuuTHIVr8dq31zCVAI7pakNMs3yctwPwkhbfOB6/kG5d +b7oVDM4kml/+dR92IzKYqJ37SLpZKED8CfY5T9cOVE83TNTFPIbWsxUKtwp6hryK +I8SPPctQTcBFp0lxzUdpFwzYWz5Ss3vx2QPtK0kBmGkruL5Hkq44ji2BBl0Yd8Lp +mUkoJOIHuD6ut5fdz0HEKhSa40/4+VjzD3aEZMeZ0zhgaZnWU0N4IVXFmgGhFJ3k +h/Z8cCq8aiEIcVh2rWKaNiYZKOYAvEQh6X2fFve+Ix2qZQhn0A5KFAwe1mLD0fHR +OQw3fzk8ydg2f6XZcchMpLi+gYoLlXNagoWIJQLbtW/tVr+TwM+RNMw2bjqVLyCx +VxnE+R8RY+nahbAHUKzGKSy/tUuUonsrn9vbG/pLtSRjThkIANQWgakgA112wCH5 +kNPAYxYxpF5+dq+7NiJ6qQkA/k64K1XopDmPKh+Ix7EmopnCJGRb/+i6HaiTvk/m +01/OJmlufJRI1snKrxA74vM6Nhh8bgX5bpqqEDp1Q+74QU5pOD1RAS2jgI7DpefP +8+9B7wCjP3m/KdZrR9r/ZQ/SpCqXQ6rrvcRBP7pp5y90Uu6MtpKhSiJNOvfWovlB +x3jagSyCvW10iBGJtUZxZ5M+IQvniPK/3NDQ7whl9sEpiAYv30qP/bZ4TIl4RjhT +b62P/657moSkFi6ugA2Go2mqruJGYQRIsj2P5L1BT7LLx4YFWKgGMuiBTi/oxcME +9NTVW3kIANhTNYYieyichaa0+nPU0H+C+Ta+iWjWdHrzsgj7v/yq9Qq7ptClIXSb +tkqcy0NSjL7D0fpCJVP5O5i4cUqIYqbhWYwgsGc0vPUqxjFF2qtPtyU5oHN05Ein +8I3kfd5Q1m9egrRmMVqa+/RK9XXHmuRELH83SHYDr3kPfJaNaqcbE+MnZhJBOh41 +hjMV5RPlX6CUZgfsh9Z67yxx2am2lnabdXYOEs7FRc+U5yAQzHpKO7o6jy6yRvAF +HK5brjmazo3h6eV7j+wfhXJL83/ML7I0qvyc//Ezdze90Oua7IbZGDKsg4oK9Or8 +91mnaBqGoL8tS0DmgvmOCmJxFQy6k18IANDlKadc5IabgmRo2beAAhuGvst3I5Cq +L1GmInDxZCUYTTX0eDpIZQz02TBmHiqLJMoUPtgYKdULUyZ8y9XM+sc2U8oxZIwe +aeXX6ZwoW6/lZIfM1Pri/fXRL3H+mc3IJW9cDLmQP6qF7BDxZiIBADfqCwlUSDGf +jG8sMTjaIfXx1rrFf+4gJTdBRfd4KEthbK+T2htXtx4Rce2ju/5FNeMnfTgoGVmM +cblrynCwIqz6tScnq8pm9cvK17ZTzp7tokkDF3ljEbw+kRY+OzbFokHXDzpUlIfR +8x1gzVKFRuk+IwWInr0Y/3fkXSbxicf7N2ZD2HKyYwBUWIrH/Bh8lk+ABbQvSGFz +aGlDb3JwIFRlcnJhZm9ybSBUZXN0IDxiY3JvZnRAaGFzaGljb3JwLmNvbT6JAk4E +EwEIADgWIQQ3TTI1RMz0lxj4EdcgC9qILJW4CgUCZMPaeQIbAwULCQgHAgYVCgkI +CwIEFgIDAQIeAQIXgAAKCRAgC9qILJW4Ch6vD/9P2NavPK8lkdrdEfEL3hS4jsIa +aCwdbY7ilH2EYADtl3Kd8X0JIlZJ6HgAHKe90Va3dJJcno4W32s04/p6tsb1LW9j +mevq1hrVU3dqZn+EBmNySP5QGHAjhgJTOoHW7CZqME/l9P04NTMXWAdWENcCD0+l +hQTftr4sEHn1v9DCVdtXF4WcQvdmsoXSstdf3wiQ2a3QkWk4a46HPSKkRyz8TerX +XEy79fPRYb/675QqQPQ6FfM+Vx/CTsfEpuJGnbtpp11HfXPZsk0TSGJAcNQXMv7V +JovGfc+amqVYqQiicT8nHNUSShJrxA1TUUF6Ige+2ewDeDBhNk5EEAAEH0aZzwci +qlGX4rbZOJjkgMtbTK65agqF67H3WDXGsOhmD+l2GjwuPz0AKxOhvdxkaBz7q08t +tjCOWXdZogfcgeLGZLRHvmAkLJTHc2b40Dhe++ryEhmHp4lXNAwhJVqEFrmW1agb +P5+qS19VjbLv8s9ynxgVXr7J7wi8hAx9t1QRe9yOif2Rmo532bU4sxD/bcGwMoO5 +xsCZJh9AmV/dJGOPfPnPuF5OPncOLPeiP0xiDsBJAeQYDClG1pBwxTCmUpFb3jT9 +N/1qBjykFVX7B+cSYb06w+MHo8bfCQrvS4ifAXDOhlvpYuVpQfouES9VtD5U13np +qoaVTAj4JbhRvVbzOJ0HGARkw9p5ARAAxY+61t9N21O0mtUDcz7hVYklLce8XQye +ucC8T8Vp68vuGIfJNsELaHo6XMJ8iK42t5fb9hk8ZX2Z8A+1ZbXEor+wcGHbrpYH +5TmFNNFdbUYka9eHfu4jZajKHmb8GkBCS7gj1wfjFj0ss3zCUmI+aD/Tjy8zumbb +0ECFfKFRaNEm3qEcGAK0Suh6t+VqpCzxI+5L5w2H/mvWRqJjULz2/IkEqhSVsJDq +mUJBPpelQGzbEyynvNbRpPsvZk9nFX2SfcKS9hqwXyUVniAh2XAnx7NScDnkCArO +GSi3b3nA4Sj7124YO+5CgmAS7FNcPb69uCoYaYgtBzuzDCIcg2OJQ8gpsYzD0h2z +HKY0PAV+k0AqzfpNIFvv/pm8DoiCkeMLtpJLE9Olq9hP45SuOMc8AHrN8QVGlKhX +aNnvct40R3XlGltrVlcFFR4yyedpQWlgvybYPcK/6F0orbAX4MIqinyraUT3sUT/ +UOJjYAu+qlA7xGyI7+MDguWttkAtt3bQQ5ML7Jem8FVe1T2Ex1muOIAa7DrrrnyX +30zCNrGEEKOOfjvxVbJRpPh641LfILrl4z9STyV3GEzUYW7k+13Xu4RNFn921INg +qSv/6oaRFtethymj3/x3+nh9XsuF1cBVw/wqRcgIzx9mK4ZfVP4cgeZoHHFtxW8/ +eFJzs6qE8E0AEQEAAQAP+QFOwPSpqnVTuBdn7l5z8obLlYXcWFUbyjUSjr53mzsT +nHssEcJub3oRvq1A6M9dfGCjk5DfcchAKKDLn0bQwXpOQyeH/9xTL+kdkX54LXjC +OdgcAp4qIkYpv4hO/3t3VQn5AaChxTHIfvr/fGh6/Yy9mI/UIDDle+xUDIP05kSk +wZKlAsm01haGYseOBWacC3Od/+40X5wdaXlAh/uOpnQCMtwH2WEoKOHXhR0EyZbW +atNe8eDyE+8cEOf0feCFLn2zQVKJbrOWhGD0N4H85K7gORNZ+vvyP8GgZ6GCU91e +gdsyR1TM0xwRusThWxEh1HyQp6w136WjBqZeheKvcZ47XpyVpDpB0dzPG9joocy9 +d69lwXJpLEzoY5EV8nTn5YU7SizIItAMKOvUolHJtaAiInNkPdqMeFOfd0wIzWKF +FWa/HrpP6UQBzndKs4shNJSn14c3MeDwa5Cdbw7Gii8Ww6rtiU4ctuqjUXvsXl6P +/5rW9WPqAM/pcSFc17N8hbNWrGRprdyqpOupyFuBQsetun4k+uKVDlMfN2IdjYm7 +jiMAcoUYoWxcvpq4Dzg/mlLr8yGjoZyDqdCuzxdyPXw/BFJYnBWa4zSNLCZ8a+1b +4VKGZLLsLKzUNoJ8HvItsoxy48aiBPTnMzoEBh2yzvR76B4663pGOB4VVaxzi59J +CADYbKEp2nHcbZhOHUo+3r8g482qMp2ygPX6w0wswUiUCWENnb9vcUoEPwk6FWVN +jKU0QEd2xArNfC9K8kj/K7gDyqCnS0Tofc/yrieLWRN0fO5Z8zLq3SKMVvBHUM4Q +6zT471FUlaLMNHlk9uyTFNmK7Ti9LL3T0k1ngPGT60uWXFC8qvALbeRtjKAKjf7s +VeGlDDXbiB2m67MU9hKjyeIMDBciT3IbYTm5Z+FrEoZxWRYEOIQek4Nmp7/waOE3 +Xlk3UmED9BwwXbRliiNzfijwRPb9QZ3FKSEZM8HXK1A0OmtxV6vg7dnVYsDuaRxb +ljl/dQDW7/mpRtugH7sHB+E5CADpsBSq6i/bMU3Q3lY6JVs5FBFZyRYT2K/zC109 +lSfj5fP8TWUyiP9mSaFW6MkDoecGod1HZyA579I16JVulYDnD1A7creYncoMMofN +UJNO8tJRJchywr+829deP+IYGQuRw9lQsk+p/uTszpexMxz5ltr4dBihCv88ZpXO +kBoB+Qkwu+IIiQz3ScnpT7fvhsGZI9hqs2bg7nuvwyoGMnW7S7CZ++YIBhpEvZ3c +jQKQde+2558ldxkyXGiKuHzZ4OzLe+jjqCYiD0hfe7BEI8jXjOn97JFT2c2Ts44t +VNvyrbe80D68id5/bmYPTn5M7qYtG3iO5p9olX8onqnVr0u1B/9dZWud4PNpkUkA +z0FE6HpwsrWYFravMiSDbkTYzpl/AXu8canKguJ7lNzl575BOaA9Fsfgswr5KgVA +urXp/wpZNaX9C/VTtkuqR8t7y3z+tkiXTGA+JT5Lp5Q3QxneqLRp6XZMckfyeKcL +SELaYCseFLaap+uAkV7/sxJynD9CL5h63lIwt/GZjKi5sbRsGL/2LxKwxjNaiQW8 +akxCkw0eupbJt2JAuQWRiJ6Yl4lMAyQxzQM/7a8Jadcy+Tl//6k4aaxR+s1RPbTA +cpiCO8tHM5/4WMMSeA36UX1PlEKtxifaqbDYS5sMeSUAAW1nfYOiNu5gg+WQ8SuN +1jZV+sVOjSCJAjYEGAEIACAWIQQ3TTI1RMz0lxj4EdcgC9qILJW4CgUCZMPaeQIb +DAAKCRAgC9qILJW4CoL9D/9lSJLa941JhieE3nyhhDcG9+Y4iB8WAgRdyfG0nihW +oT2N2PcyYdStUPdRTEQavCZ4DZdH9aRSgwnL8LsVIrQDy5Hhv93a65gUY1+ADlqs +f2ojW6ssZktO5CTfsm5KLHxKv1tF1Ju50cPtJNgU/8Nzxfi7hHDTJEkSUKzwIifK +hmeS4ESXMDo2UxiFFcbxibhLoggcuksu7bwFxQZN+C0rckqBUjipKleAQZE02W1A +o7w+evb/PHomMMVSpTR3STRmK/SVXmj4Fq+t3njy4pDzOUXOC0WKrNvad6tOikHV +wS/MrHlqZhjwULGQAjrzuf1zyzioiYkKhLFaVAAk8jBivJZqYEbTbmCTZfiDK2Jz +FpDsUsc2uNHUIBPdI7rmuBjspxhp1f6eoP04vIh02hLulMg8QA+7IwavSQjcXt2d +ju+MQJeBkEXYwLsVMlpyQ0wEH3Cnj3Wwk9vEvoBRxL/rwzhcRgT6nuVRMybLHDjv +dQKTwIwulrtYGCLcjfQR4EYTUu756BgcuhQfEytd948m3sBsst+m2YZWUT6yfKDg +p95ekAjVN0zcTGbvKB4bnKKLLF85q3ir+9uyMNet2Oi/f+u6cFZbyhLT6DsfVqGp +UKSuKABCqlgH77ztGAhaKv8JDgfHszghd8KvrES7cJmV1HFE/xCEtTBx20Ll6H8f +fA== +=ILcU +-----END PGP PRIVATE KEY BLOCK----- diff --git a/internal/releaseauth/testdata/sample.public.key b/internal/releaseauth/testdata/sample.public.key new file mode 100644 index 0000000000..d5ae2f3bc0 --- /dev/null +++ b/internal/releaseauth/testdata/sample.public.key @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBGTD2nkBEACzN+0KhgkfyObviYGvtWWCQfznX440nIiu0uag1lRGh6MeupOw +cPFMtSSoltSYTTkS3J6UBvTOQbPq/IAX0H3WBnUWFJpA9h87fHbpxquEeYeUQ65r +J/IRLsPFnzOl43CCSnkDuppJqYSPJc1GJYtb4O8Tq7+Af7rdZFWoPW1bo7NIriUm +MM9y3udb8Catvz5L7aYevQ001x9jP1SzpkMVWY4/TxTrE7Xjxh7Ke7MlJmIeFWHl +ja/mjukcZZAD/KwHU+mid+MZhx1CmiN36CuYBcLpP2eJTrrhYOps8kX5z1sKsIaY +4KF1yVkLGQxGS8y2M0kZXjYtNPxY1DJGveWIRzcx+7KWkavkVbhvmrMRWLkKv+55 +BV6lsLrOYFrCTRxolNOx5ZNcUbaIucqGYe8RzQz/WlPPA3TbMnOQ7OSTIqhqMzzd +s7l7S4N7UHP+ePvMy1Uzajfsb80XI9zYRrRMshHjU6huYITwIF5wWzfRVegSfYw+ +bcxr5XWLYmJ5aMA8JNOhdjpszpbZcCZPYPV6Ya6jsXoO4QxD+ivGNBbzEuB+DolW +/6GXqfuJARPx1vPIAWX2j6uVhJb5ygBahd9aPjaURwuM9SZnCTCoc0x7171R6t+g +AIs/wB9D/iHbLEMUgH5KlW+9VtJLw2afiVAeSd38axvn/ELURvpMoJ1s5wARAQAB +tC9IYXNoaUNvcnAgVGVycmFmb3JtIFRlc3QgPGJjcm9mdEBoYXNoaWNvcnAuY29t +PokCTgQTAQgAOBYhBDdNMjVEzPSXGPgR1yAL2ogslbgKBQJkw9p5AhsDBQsJCAcC +BhUKCQgLAgQWAgMBAh4BAheAAAoJECAL2ogslbgKHq8P/0/Y1q88ryWR2t0R8Qve +FLiOwhpoLB1tjuKUfYRgAO2Xcp3xfQkiVknoeAAcp73RVrd0klyejhbfazTj+nq2 +xvUtb2OZ6+rWGtVTd2pmf4QGY3JI/lAYcCOGAlM6gdbsJmowT+X0/Tg1MxdYB1YQ +1wIPT6WFBN+2viwQefW/0MJV21cXhZxC92ayhdKy11/fCJDZrdCRaThrjoc9IqRH +LPxN6tdcTLv189Fhv/rvlCpA9DoV8z5XH8JOx8Sm4kadu2mnXUd9c9myTRNIYkBw +1Bcy/tUmi8Z9z5qapVipCKJxPycc1RJKEmvEDVNRQXoiB77Z7AN4MGE2TkQQAAQf +RpnPByKqUZfittk4mOSAy1tMrrlqCoXrsfdYNcaw6GYP6XYaPC4/PQArE6G93GRo +HPurTy22MI5Zd1miB9yB4sZktEe+YCQslMdzZvjQOF776vISGYeniVc0DCElWoQW +uZbVqBs/n6pLX1WNsu/yz3KfGBVevsnvCLyEDH23VBF73I6J/ZGajnfZtTizEP9t +wbAyg7nGwJkmH0CZX90kY498+c+4Xk4+dw4s96I/TGIOwEkB5BgMKUbWkHDFMKZS +kVveNP03/WoGPKQVVfsH5xJhvTrD4wejxt8JCu9LiJ8BcM6GW+li5WlB+i4RL1W0 +PlTXeemqhpVMCPgluFG9VvM4uQINBGTD2nkBEADFj7rW303bU7Sa1QNzPuFViSUt +x7xdDJ65wLxPxWnry+4Yh8k2wQtoejpcwnyIrja3l9v2GTxlfZnwD7VltcSiv7Bw +YduulgflOYU00V1tRiRr14d+7iNlqMoeZvwaQEJLuCPXB+MWPSyzfMJSYj5oP9OP +LzO6ZtvQQIV8oVFo0SbeoRwYArRK6Hq35WqkLPEj7kvnDYf+a9ZGomNQvPb8iQSq +FJWwkOqZQkE+l6VAbNsTLKe81tGk+y9mT2cVfZJ9wpL2GrBfJRWeICHZcCfHs1Jw +OeQICs4ZKLdvecDhKPvXbhg77kKCYBLsU1w9vr24KhhpiC0HO7MMIhyDY4lDyCmx +jMPSHbMcpjQ8BX6TQCrN+k0gW+/+mbwOiIKR4wu2kksT06Wr2E/jlK44xzwAes3x +BUaUqFdo2e9y3jRHdeUaW2tWVwUVHjLJ52lBaWC/Jtg9wr/oXSitsBfgwiqKfKtp +RPexRP9Q4mNgC76qUDvEbIjv4wOC5a22QC23dtBDkwvsl6bwVV7VPYTHWa44gBrs +OuuufJffTMI2sYQQo45+O/FVslGk+HrjUt8guuXjP1JPJXcYTNRhbuT7Xde7hE0W +f3bUg2CpK//qhpEW162HKaPf/Hf6eH1ey4XVwFXD/CpFyAjPH2Yrhl9U/hyB5mgc +cW3Fbz94UnOzqoTwTQARAQABiQI2BBgBCAAgFiEEN00yNUTM9JcY+BHXIAvaiCyV +uAoFAmTD2nkCGwwACgkQIAvaiCyVuAqC/Q//ZUiS2veNSYYnhN58oYQ3BvfmOIgf +FgIEXcnxtJ4oVqE9jdj3MmHUrVD3UUxEGrwmeA2XR/WkUoMJy/C7FSK0A8uR4b/d +2uuYFGNfgA5arH9qI1urLGZLTuQk37JuSix8Sr9bRdSbudHD7STYFP/Dc8X4u4Rw +0yRJElCs8CInyoZnkuBElzA6NlMYhRXG8Ym4S6IIHLpLLu28BcUGTfgtK3JKgVI4 +qSpXgEGRNNltQKO8Pnr2/zx6JjDFUqU0d0k0Ziv0lV5o+Bavrd548uKQ8zlFzgtF +iqzb2nerTopB1cEvzKx5amYY8FCxkAI687n9c8s4qImJCoSxWlQAJPIwYryWamBG +025gk2X4gyticxaQ7FLHNrjR1CAT3SO65rgY7KcYadX+nqD9OLyIdNoS7pTIPEAP +uyMGr0kI3F7dnY7vjECXgZBF2MC7FTJackNMBB9wp491sJPbxL6AUcS/68M4XEYE ++p7lUTMmyxw473UCk8CMLpa7WBgi3I30EeBGE1Lu+egYHLoUHxMrXfePJt7AbLLf +ptmGVlE+snyg4KfeXpAI1TdM3Exm7ygeG5yiiyxfOat4q/vbsjDXrdjov3/runBW +W8oS0+g7H1ahqVCkrigAQqpYB++87RgIWir/CQ4Hx7M4IXfCr6xEu3CZldRxRP8Q +hLUwcdtC5eh/H3w= +=FKJH +-----END PGP PUBLIC KEY BLOCK----- diff --git a/internal/releaseauth/testdata/sample_release/sample_0.1.0_SHA256SUMS b/internal/releaseauth/testdata/sample_release/sample_0.1.0_SHA256SUMS new file mode 100644 index 0000000000..08cb8872c8 --- /dev/null +++ b/internal/releaseauth/testdata/sample_release/sample_0.1.0_SHA256SUMS @@ -0,0 +1 @@ +22db2f0c70b50cff42afd4878fea9f6848a63f1b6532bd8b64b899f574acb35d sample_0.1.0_darwin_amd64.zip diff --git a/internal/releaseauth/testdata/sample_release/sample_0.1.0_SHA256SUMS.sig b/internal/releaseauth/testdata/sample_release/sample_0.1.0_SHA256SUMS.sig new file mode 100644 index 0000000000..ddc4e1738d Binary files /dev/null and b/internal/releaseauth/testdata/sample_release/sample_0.1.0_SHA256SUMS.sig differ diff --git a/internal/releaseauth/testdata/sample_release/sample_0.1.0_darwin_amd64.zip b/internal/releaseauth/testdata/sample_release/sample_0.1.0_darwin_amd64.zip new file mode 100644 index 0000000000..58dfbabb21 Binary files /dev/null and b/internal/releaseauth/testdata/sample_release/sample_0.1.0_darwin_amd64.zip differ diff --git a/internal/repl/continuation.go b/internal/repl/continuation.go new file mode 100644 index 0000000000..4daee9fba8 --- /dev/null +++ b/internal/repl/continuation.go @@ -0,0 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package repl + +import ( + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" +) + +// ExpressionEntryCouldContinue is a helper for terraform console's interactive +// mode which serves as a heuristic for whether it seems like the author might +// be trying to split an expression over multiple lines of input. +// +// The current heuristic is whether there's at least one bracketing delimiter +// that isn't closed, but only if any closing brackets already present are +// properly balanced. +// +// This function also always returns false if the last line entered is empty, +// because that seems likely to represent a user trying to force Terraform to +// accept something that didn't pass the heuristic for some reason, at which +// point Terraform can try to evaluate the expression and return an error if +// it's invalid syntax. +func ExpressionEntryCouldContinue(linesSoFar []string) bool { + if len(linesSoFar) == 0 || strings.TrimSpace(linesSoFar[len(linesSoFar)-1]) == "" { + // If there's no input at all or if the last line is empty other than + // spaces, we assume the user is trying to force Terraform to evaluate + // what they entered so far without any further continuation. + return false + } + + // We use capacity 8 here as a compromise assuming that most reasonable + // input entered at the console prompt will not use more than eight + // levels of nesting, but even if it does then we'll just reallocate the + // slice and so it's not a big deal. + delimStack := make([]hclsyntax.TokenType, 0, 8) + push := func(typ hclsyntax.TokenType) { + delimStack = append(delimStack, typ) + } + pop := func() hclsyntax.TokenType { + if len(delimStack) == 0 { + return hclsyntax.TokenInvalid + } + ret := delimStack[len(delimStack)-1] + delimStack = delimStack[:len(delimStack)-1] + return ret + } + // We need to scan this all as one string because the HCL lexer has a few + // special cases where it tracks open/close state itself, such as in heredocs. + all := strings.Join(linesSoFar, "\n") + "\n" + toks, diags := hclsyntax.LexExpression([]byte(all), "", hcl.InitialPos) + if diags.HasErrors() { + return false // bail early if the input is already invalid + } + for _, tok := range toks { + switch tok.Type { + case hclsyntax.TokenOBrace, hclsyntax.TokenOBrack, hclsyntax.TokenOParen, hclsyntax.TokenOHeredoc, hclsyntax.TokenTemplateInterp, hclsyntax.TokenTemplateControl: + // Opening delimiters go on our stack so that we can hopefully + // match them with closing delimiters later. + push(tok.Type) + case hclsyntax.TokenCBrace: + open := pop() + if open != hclsyntax.TokenOBrace { + return false + } + case hclsyntax.TokenCBrack: + open := pop() + if open != hclsyntax.TokenOBrack { + return false + } + case hclsyntax.TokenCParen: + open := pop() + if open != hclsyntax.TokenOParen { + return false + } + case hclsyntax.TokenCHeredoc: + open := pop() + if open != hclsyntax.TokenOHeredoc { + return false + } + case hclsyntax.TokenTemplateSeqEnd: + open := pop() + if open != hclsyntax.TokenTemplateInterp && open != hclsyntax.TokenTemplateControl { + return false + } + } + } + + // If we get here without returning early then all of the closing delimeters + // were matched by opening delimiters. If our stack still contains at least + // one opening bracket then it seems like the user is intending to type + // more. + return len(delimStack) != 0 +} diff --git a/internal/repl/continuation_test.go b/internal/repl/continuation_test.go new file mode 100644 index 0000000000..0a1087960a --- /dev/null +++ b/internal/repl/continuation_test.go @@ -0,0 +1,307 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package repl + +import ( + "strings" + "testing" +) + +func TestExpressionEntryCouldContinue(t *testing.T) { + tests := []struct { + Input []string + Want bool + }{ + { + nil, + false, + }, + { + []string{ + "", + }, + false, + }, + { + []string{ + "foo(", + "", // trailing newline forces termination + }, + false, + }, + + // parens + { + []string{ + "foo()", + }, + false, + }, + { + []string{ + "foo(", + }, + true, + }, + { + []string{ + "foo(", + " bar,", + ")", + }, + false, + }, + + // brackets + { + []string{ + "[]", + }, + false, + }, + { + []string{ + "[", + }, + true, + }, + { + []string{ + "[", + "]", + }, + false, + }, + + // braces + { + []string{ + "{}", + }, + false, + }, + { + []string{ + "{", + }, + true, + }, + { + []string{ + "{", + "}", + }, + false, + }, + + // quotes + // HCL doesn't allow splitting quoted strings over multiple lines, so + // these never cause continuation. (Use heredocs instead for that) + { + []string{ + `""`, + }, + false, + }, + { + []string{ + `"`, + }, + false, + }, + { + []string{ + `"`, + `"`, + }, + false, + }, + + // heredoc templates + { + []string{ + `< 0 { + t.Fatalf("%s should not be renewed", ephemB) + } + testB.Unlock() + + // close all instances, which should indicate the values are no longer "live" + diags := resources.CloseInstances(ctx, ephemA0.ConfigResource()) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + diags = resources.CloseInstances(ctx, ephemB.ConfigResource()) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + if !testA0.closed { + t.Fatalf("%s not closed", ephemA0) + } + if !testA1.closed { + t.Fatalf("%s not closed", ephemA1) + } + if !testB.closed { + t.Fatalf("%s not closed", ephemB) + } + + for _, addr := range []addrs.AbsResourceInstance{ephemA0, ephemA1, ephemB} { + val, live := resources.InstanceValue(addr) + if live { + t.Fatalf("%s should not be live", addr) + } + if !val.RawEquals(cty.DynamicVal) { + t.Fatalf("unexpected value %#v\n", val) + } + } +} + +func TestResourcesCancellation(t *testing.T) { + resources := NewResources() + + ephemA0 := addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test", + Name: "a", + }, + Key: addrs.IntKey(0), + }.Absolute(addrs.RootModuleInstance) + ephemA1 := addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test", + Name: "a", + }, + Key: addrs.IntKey(1), + }.Absolute(addrs.RootModuleInstance) + + ephemB := addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.EphemeralResourceMode, + Type: "test", + Name: "b", + }, + Key: addrs.NoKey, + }.Absolute(addrs.RootModuleInstance) + + ctx, cancel := context.WithCancel(context.Background()) + // cancelling now should cause the first renew op to report the cancellation + cancel() + + testA0 := &testResourceInstance{ + renewInterval: time.Second, + } + testA1 := &testResourceInstance{ + renewInterval: time.Second, + } + testB := &testResourceInstance{} + + resources.RegisterInstance(ctx, ephemA0, ResourceInstanceRegistration{ + Value: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("ephemeral.test.a[0]"), + }), + Impl: testA0, + RenewAt: time.Now().Add(10 * time.Millisecond), + }) + + resources.RegisterInstance(ctx, ephemA1, ResourceInstanceRegistration{ + Value: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("ephemeral.test.a[1]"), + }), + Impl: testA1, + RenewAt: time.Now().Add(10 * time.Millisecond), + }) + + resources.RegisterInstance(ctx, ephemB, ResourceInstanceRegistration{ + Value: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("ephemeral.test.b"), + }), + Impl: testB, + }) + + // Use the internal WaitGroup to catch when the renew goroutines have exited from the cancellation. + cancelled := make(chan int) + go func() { + resources.wg.Wait() + close(cancelled) + }() + select { + case <-time.After(time.Second): + t.Fatal("timeout waiting for cancellation") + case <-cancelled: + // this should be almost immediate, but we'll allow a second just in + // case of crazy slow integration test hosts + } + + // ephemB has no call for renew, so shouldn't know about the cancel yet + _, live := resources.InstanceValue(ephemB) + if !live { + t.Fatalf("%s should still be live", ephemB) + } + + for _, addr := range []addrs.AbsResourceInstance{ephemA0, ephemA1} { + _, live := resources.InstanceValue(addr) + if live { + t.Fatalf("%s was canceled, should not be live", addr) + } + } + + testB.Lock() + if testB.renewed > 0 { + t.Fatalf("%s should not be renewed", ephemB) + } + testB.Unlock() + + // close all instances, which should indicate the values are no longer "live" + diags := resources.CloseInstances(ctx, ephemA0.ConfigResource()) + if len(diags) != 2 { + t.Fatalf("expected 2 error diagnostics, got:\n%s", diags.ErrWithWarnings()) + } + diagStr := diags.Err().Error() + if strings.Count(diagStr, "context canceled") != 2 { + t.Fatal("expected 2 context canceled errors, got:\n", diagStr) + } + + diags = resources.CloseInstances(ctx, ephemB.ConfigResource()) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + if !testA0.closed { + t.Fatalf("%s not closed", ephemA0) + } + if !testA1.closed { + t.Fatalf("%s not closed", ephemA1) + } + if !testB.closed { + t.Fatalf("%s not closed", ephemB) + } +} + +type testResourceInstance struct { + sync.Mutex + name string + renewInterval time.Duration + renewed int + notifyRenew chan string + closed bool +} + +func (r *testResourceInstance) Renew(ctx context.Context, req providers.EphemeralRenew) (*providers.EphemeralRenew, tfdiags.Diagnostics) { + nextRenew := &providers.EphemeralRenew{ + RenewAt: time.Now().Add(r.renewInterval), + } + r.Lock() + defer r.Unlock() + r.renewed++ + select { + case r.notifyRenew <- r.name: + case <-time.After(time.Second): + // stop renewing if no-one is listening + return nil, nil + } + return nextRenew, nil +} + +func (r *testResourceInstance) Close(ctx context.Context) tfdiags.Diagnostics { + r.Lock() + defer r.Unlock() + r.closed = true + return nil +} diff --git a/internal/resources/ephemeral/ephemeral_resources.go b/internal/resources/ephemeral/ephemeral_resources.go new file mode 100644 index 0000000000..186bbd6c84 --- /dev/null +++ b/internal/resources/ephemeral/ephemeral_resources.go @@ -0,0 +1,277 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package ephemeral + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" + + "github.com/zclconf/go-cty/cty" +) + +// Resources is a tracking structure for active instances of ephemeral +// resources. +// +// The lifecycle of an ephemeral resource instance is quite different than +// other resource modes because it's live for at most the duration of a single +// graph walk, and because it might need periodic "renewing" in order to +// remain live for the necessary duration. +type Resources struct { + active addrs.Map[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *resourceInstanceInternal]] + mu sync.RWMutex + + // WaitGroup to track renew goroutines + wg sync.WaitGroup +} + +func NewResources() *Resources { + return &Resources{ + active: addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *resourceInstanceInternal]](), + } +} + +type ResourceInstanceRegistration struct { + Value cty.Value + ConfigBody hcl.Body + Impl ResourceInstance + RenewAt time.Time + Private []byte +} + +func (r *Resources) RegisterInstance(ctx context.Context, addr addrs.AbsResourceInstance, reg ResourceInstanceRegistration) { + if addr.Resource.Resource.Mode != addrs.EphemeralResourceMode { + panic(fmt.Sprintf("can't register %s as an ephemeral resource instance", addr)) + } + + r.mu.Lock() + defer r.mu.Unlock() + + configAddr := addr.ConfigResource() + if !r.active.Has(configAddr) { + r.active.Put(configAddr, addrs.MakeMap[addrs.AbsResourceInstance, *resourceInstanceInternal]()) + } + ri := &resourceInstanceInternal{ + value: reg.Value, + configBody: reg.ConfigBody, + impl: reg.Impl, + renewCancel: noopCancel, + } + if !reg.RenewAt.IsZero() { + ctx, cancel := context.WithCancel(ctx) + ri.renewCancel = cancel + + renewal := &providers.EphemeralRenew{ + RenewAt: reg.RenewAt, + Private: reg.Private, + } + + r.wg.Add(1) + go ri.handleRenewal(ctx, &r.wg, renewal) + } + r.active.Get(configAddr).Put(addr, ri) +} + +func (r *Resources) InstanceValue(addr addrs.AbsResourceInstance) (val cty.Value, live bool) { + r.mu.Lock() + defer r.mu.Unlock() + + configAddr := addr.ConfigResource() + insts, ok := r.active.GetOk(configAddr) + if !ok { + return cty.DynamicVal, false + } + inst, ok := insts.GetOk(addr) + if !ok { + // Here we can assume that if the entire resource exists, the instance + // is valid because Close removes resources as a whole. Individual + // instances may not actually be present when checks are evaluated, + // because they are evaluated from instance nodes that are using "self". + // The way an instance gets "self" is to call GetResource which needs to + // compile all instances into a suitable value, so we may be missing + // instances which have not yet been opened. + return cty.DynamicVal, true + } + // If renewal has failed then we can't assume that the object is still + // live, but we can still return the original value regardless. + return inst.value, !inst.renewDiags.HasErrors() +} + +// CloseInstances shuts down any live ephemeral resource instances that are +// associated with the given resource address. +// +// This is the "happy path" way to shut down ephemeral resource instances, +// intended to be called during the visit to a graph node that depends on +// all other nodes that might make use of the instances of this ephemeral +// resource. +// +// The runtime should also eventually call [Resources.Close] once the graph +// walk is complete, to catch any stragglers that we didn't reach for +// piecemeal shutdown, e.g. due to errors during the graph walk. +func (r *Resources) CloseInstances(ctx context.Context, configAddr addrs.ConfigResource) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // Use a read-lock here so we can run multiple close calls concurrently for + // different resources. This needs to call CloseEphemeralResource which is sent to + // the provider and can take an unknown amount of time. + r.mu.RLock() + for _, elem := range r.active.Get(configAddr).Elems { + moreDiags := elem.Value.close(ctx) + diags = diags.Append(moreDiags.InConfigBody(elem.Value.configBody, elem.Key.String())) + } + r.mu.RUnlock() + + // Stop tracking the objects we've just closed, so that we know we don't + // still need to close them. + r.mu.Lock() + defer r.mu.Unlock() + r.active.Remove(configAddr) + + return diags +} + +// Close shuts down any ephemeral resource instances that are still running +// at the time of the call. +// +// This is intended to catch any "stragglers" that we weren't able to clean +// up during the graph walk, such as if an error prevents us from reaching +// the cleanup node. +func (r *Resources) Close(ctx context.Context) tfdiags.Diagnostics { + // TODO: Investigate making sure individual close calls are always called + // even after runtime errors. If individual resources should always be + // closed before we get here, then we may not need this at all. If we only + // get here during exceptional circumstances, then we're probably exiting + // anyway so there's no cleanup needed. + r.mu.Lock() + defer r.mu.Unlock() + + // We might be closing due to a context cancellation, but we still need to + // be able to make non-canceled Close requests. + // + // TODO: if we're going to ignore the cancellation to ensure that Close is + // always called, should we add some sort of timeout? + ctx = context.WithoutCancel(ctx) + + var diags tfdiags.Diagnostics + for _, elem := range r.active.Elems { + for _, elem := range elem.Value.Elems { + moreDiags := elem.Value.close(ctx) + diags = diags.Append(moreDiags.InConfigBody(elem.Value.configBody, elem.Key.String())) + } + } + r.active = addrs.MakeMap[addrs.ConfigResource, addrs.Map[addrs.AbsResourceInstance, *resourceInstanceInternal]]() + + // All renew loops should have returned, or else we're going to leak + // resources which could be continually renewing, or even interfering with + // the same resources during the next operation. + // + // Use an asynchronous check so we can timeout and report the problem. + done := make(chan int) + go func() { + r.wg.Wait() + close(done) + }() + select { + case <-done: + // OK! + case <-time.After(10 * time.Second): + // This is probably harmless a lot of time, but is also indicative of an + // ephemeral resource which would be misbehaving. The message isn't + // very helpful with no context, so we'll have to rely on correlating + // the problem via other log messages. + diags = diags.Append(errors.New("Ephemeral resources failed to Close during renew operations")) + } + + return diags +} + +type resourceInstanceInternal struct { + value cty.Value + configBody hcl.Body + impl ResourceInstance + + renewCancel func() + renewDiags tfdiags.Diagnostics + renewMu sync.Mutex // hold when accessing renewCancel/renewDiags, and while actually renewing +} + +// close halts this instance's asynchronous renewal loop, if any, and then +// calls Close on the resource instance's implementation object. +// +// The returned diagnostics are contextual diagnostics that should have +// [tfdiags.Diagnostics.WithConfigBody] called on them before returning to +// a context-unaware caller. +func (r *resourceInstanceInternal) close(ctx context.Context) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // if the resource could not be opened, there will not be anything to close either + if r.impl == nil { + return diags + } + + // Stop renewing, if indeed we are. If we previously saw any errors during + // renewing then they finally get returned here, to be reported along with + // any errors during close. + r.renewMu.Lock() + r.renewCancel() + diags = diags.Append(r.renewDiags) + r.renewDiags = nil // just to avoid any risk of double-reporting + r.renewMu.Unlock() + + // FIXME: If renewal failed earlier then it's pretty likely that closing + // would fail too. For now this is assuming that it's the provider's + // own responsibility to remember that it previously failed a renewal + // and to avoid returning redundant errors from close, but perhaps we'll + // revisit that in later work. + diags = diags.Append(r.impl.Close(ctx)) + + return diags +} + +func (r *resourceInstanceInternal) handleRenewal(ctx context.Context, wg *sync.WaitGroup, firstRenewal *providers.EphemeralRenew) { + defer wg.Done() + t := time.NewTimer(time.Until(firstRenewal.RenewAt)) + nextRenew := firstRenewal + for { + select { + case <-t.C: + // It's time to renew + r.renewMu.Lock() + anotherRenew, diags := r.impl.Renew(ctx, *nextRenew) + r.renewDiags.Append(diags) + if diags.HasErrors() { + // If renewal fails then we'll stop trying to renew. + r.renewCancel = noopCancel + r.renewMu.Unlock() + return + } + if anotherRenew == nil { + // If we don't have another round of renew to do then we'll stop. + r.renewCancel = noopCancel + r.renewMu.Unlock() + return + } + nextRenew = anotherRenew + t.Reset(time.Until(anotherRenew.RenewAt)) + r.renewMu.Unlock() + case <-ctx.Done(): + // If we're cancelled then we'll halt renewing immediately. + r.renewMu.Lock() + t.Stop() + r.renewCancel = noopCancel + r.renewDiags = r.renewDiags.Append(ctx.Err()) + r.renewMu.Unlock() + return // we don't need to run this loop anymore + } + } +} + +func noopCancel() {} diff --git a/internal/rpcapi/README.md b/internal/rpcapi/README.md new file mode 100644 index 0000000000..b33a8d2682 --- /dev/null +++ b/internal/rpcapi/README.md @@ -0,0 +1,160 @@ +# Terraform Core RPC API + +This directory contains package `rpcapi`, which is the main implementation code +for the Terraform Core RPC API. + +What follows here is documentation aimed at those who are maintaining or +otherwise contributing to this code. +**This is not end-user-oriented documentation**; information on how to _use_ +the RPC API as an external caller belongs elsewhere. + +**NOTE WELL!** The RPC API is currently experimental, existing primarily as +a vehicle for the Terraform Stacks private preview. It is subject to arbitrary +breaking changes -- even in patch releases -- until the Terraform Stacks +features are considered stable. + +## What is the RPC API? + +The RPC API is an integration point for making use of some Terraform Core +functionality within external software. + +It's primarily aimed at entirely separate programs using its gRPC server +configured using HashiCorp's `go-plugin` library, although it does also have an +internal access point for use by the parts of this codebase that are +architecturally part of Terraform CLI rather than Terraform Core, to help +reinforce that architectural boundary despite them both currently living in the +same codebase. + +The relationship between this package's implementation of the RPC API and +external callers of that API is mechanically similar to the relationship +between the Terraform Plugin Framework/SDK and Terraform Core: it's a +client/server protocol using gRPC as the transport, over a local socket. + +The protocol buffers definition for the API lives in +[`terraform1.proto`](./terraform1/terraform1.proto), which when included in +a tagged commit of this repository acts as the source of truth for a particular +version of the API. + +## RPC API services + +The RPC API exposes a few different services that each wrap different parts +of Terraform Core's functionality. These are broad thematic groupings, but +they are all part of the same API and in particular values returned by one +service are often accepted as input to another. + +- `Setup`: This is a special service that's used only to prepare the other + services for use by performing a negotiation handshake. + + Clients must always make exactly one call to `Setup.Handshake` before + interacting with any other part of this API. That call acts as a capability + negotiation which might therefore influence the behavior of other subequent + calls as a measure of forward and backward compatibility. + +- `Dependencies`: Deals with some cross-cutting concerns related to dependencies + such as remote source packages (e.g. external modules) and providers. + +- `Stacks`: Provides external access to the Terraform Stacks runtime, including + planning and applying changes to the infrastructure described by a stack + configuration. + +## API Object Handles + +To allow passing live objects between different services and different +functions within the same service, the RPC API uses _handles_, which are +`int64` values that each uniquely identify a live object of a particular +type. + +Handles are typically (but not always) created by RPC functions whose names +start with the prefix `Open`, and are later closed by functions whose names +start with `Close`. + +Handles persist between calls to the same RPC API process, but are automatically +discarded when that process shuts down. Depending on the handle type, this +automatic discarding may or may not be equivalent to explicitly closing the +handle, and so callers should typically explicitly close handles for objects +they no longer intend to use. + +Objects represented by handles can sometimes depend on other objects. In that +case, it might be necessary to close one handle before closing another. + +Internally, handles are represented as values of the `handle` generic type, +which is parameterized by the type of the underlying object the handle is +representing. This therefore allows a measure of type safety to help avoid +mistakes like using the wrong kind of handle when calling a function. + +In the wire protocol the handle type information is erased, and so when +accepting handles from a client the service implementation must check that +the given handle is of the expected type. + +Currently handles are unique across objects of all types, but that's an +implementation detail that clients are not allowed to rely on. If designing +a service which can accept handles of multiple different types, always design +it to accept each handle type as a separate request field, and never rely on +the system's internal state about what type each handle has, so that we can +give the best possible feedback to clients when they have their own bugs that +cause mixups between different handle types. + +## Handshake Dynamic Initialization + +In order to allow clients to dynamically negotiate capabilities at runtime, +the server implementation of this API uses an extra indirection over the +real service implementations that's implemented in the subdirectory +`dynrpcserver`. + +The service implementations registered with the gRPC server are actually +instances of the wrapper stubs in that package. Initially those stubs are +all wrapping nothing at all, and so all calls to the service functions will +return errors. + +During a `Setup.Handshake` call, the system finally instantiates the real +service implementations that are implemented within this package directory. +The exact details of what types are instantiated and how they are populated +can vary based on the negotiated capabilities, allowing some flexibility in +how we will handle requests based on those capabilities. + +The `dynrpcserver` stubs are automatically generated by a `go:generate` +directive in that package based on the protocol buffers definitions. Therefore +each time we change the set of service functions or the request and response +types for those functions we must first run `make protobuf` to regenerate the +protocol buffers stubs, and then +`go generate ./internal/rpcapi/...` to update the `dynrpcserver` stubs to +match. + +## API Entry Points + +The main entry point is `rpcapi.CLICommandFactory`, which returns a factory +function intended for use with the `github.com/mitchellh/cli` module that +Terraform CLI uses to route execution into its various subcommands. + +Terraform CLI's `package main` binds the subcommand `rpcapi` directly to the +factory returned by that function, thereby providing the smallest possible +amount of Terraform CLI execution before reaching the RPC API. This is +intentional to help reinforce that `rpcapi` is _an alternative to_ using +Terraform CLI, rather than part of Terraform CLI itself, despite the +unavoidable use of some of its early entry-point code to get up and running. + +When Terraform CLI itself needs to access Terraform Core functionality that's +exposed by the RPC API, an alternative entry point is +`rpcapi.NewInternalClient`. This function returns an object which provides +access to gRPC clients just as would be used by an external caller accessing +the API when using `go-plugin`, but arranges for its requests to be routed +via local buffers in-process rather than using a socket. + +The intent of this "internal client" is to reinforce the architectural boundary +between Terraform CLI and Terraform Core despite them living in the same +codebase. Commands that interact with the internal client could potentially +be factored out into separate codebases in future with only minimal +modification to use `go-plugin` to arrange access instead of using the +internal client. + +At the time of writing this documentation there is plenty of surface area in +Terraform CLI that predates the RPC API which accesses Terraform Core +functionality which itself predates the RPC API, and therefore those calls +are made directly via normal function calls. It's fine to continue maintaining +those callers and callees until there's a strong reason to update them, but +most new functionality should be mediated through the RPC API. + +In particular, the RPC API is the only public interface to the Terraform Stacks +runtime, and so any Terraform CLI code which is orchestrating the Stacks +runtime _must_ access it through the RPC API internal client, and must not +directly import anything under `./internal/stacks`. diff --git a/internal/rpcapi/cli.go b/internal/rpcapi/cli.go new file mode 100644 index 0000000000..691f3cfa1c --- /dev/null +++ b/internal/rpcapi/cli.go @@ -0,0 +1,122 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/hashicorp/cli" +) + +// CLICommand is a command initialization callback for use with +// github.com/hashicorp/cli, allowing Terraform's "package main" to +// jump straight into the RPC plugin server without any interference +// from the usual Terraform CLI machinery in package "command", which +// is irrelevant here because this RPC API exists to bypass the +// Terraform CLI layer as much as possible. +func CLICommandFactory(opts CommandFactoryOpts) func() (cli.Command, error) { + return func() (cli.Command, error) { + return cliCommand{opts}, nil + } +} + +type CommandFactoryOpts struct { + ExperimentsAllowed bool + ShutdownCh <-chan struct{} +} + +type cliCommand struct { + opts CommandFactoryOpts +} + +// Help implements cli.Command. +func (c cliCommand) Help() string { + helpText := ` +Usage: terraform [global options] rpcapi + + Starts a gRPC server for programmatic access to Terraform Core from + wrapping automation. + + This interface is currently intended only for HCP Terraform and is + subject to breaking changes even in patch releases. Do not use this. +` + return strings.TrimSpace(helpText) +} + +// Run implements cli.Command. +func (c cliCommand) Run(args []string) int { + if len(args) != 0 { + fmt.Fprintf(os.Stderr, "This command does not accept any arguments.\n") + return 1 + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + // We'll adapt the caller's "shutdown channel" into a context + // cancellation. + for { + select { + case <-c.opts.ShutdownCh: + cancel() + case <-ctx.Done(): + return + } + } + }() + + err := ServePlugin(ctx, ServerOpts{ + ExperimentsAllowed: c.opts.ExperimentsAllowed, + }) + if err != nil { + if err == ErrNotPluginClient { + // TODO: + // + // The following message says that this interface is for HCP + // Terraform only because we're using HCP Terraform's integration + // with it to try to prove out the API/protocol design. By focusing + // only on HCP Terraform as a client first, we can accommodate + // any necessary breaking changes by ensuring that HCP Terraform's + // client is updated before releasing an updated RPC API server + // implementation. + // + // However, in the long run this should ideally become a documented + // public interface with compatibility guarantees, at which point + // we should change this error message only to express that this + // is a machine-oriented integration API rather than something for + // end-users to use directly. For example, the RPC server is likely + // to make a better integration point for tools like the + // Terraform Language Server in future too, assuming it grows to + // include language analysis features. + fmt.Fprintf( + os.Stderr, + ` +This subcommand is for use by HCP Terraform and is not intended for direct use. +Its behavior is not subject to Terraform compatibility promises. To interact +with Terraform using the CLI workflow, refer to the main set of subcommands by +running the following command: + terraform help + +`) + } else { + fmt.Fprintf(os.Stderr, "Failed to start RPC server: %s.\n", err) + } + return 1 + } + + // NOTE: In practice it's impossible to get here, because if ServePlugin + // doesn't error then it blocks forever and then eventually terminates + // the process itself without returning. + + return 0 +} + +// Synopsis implements cli.Command. +func (c cliCommand) Synopsis() string { + return "An RPC server used for integration with wrapping automation" +} diff --git a/internal/rpcapi/convert.go b/internal/rpcapi/convert.go new file mode 100644 index 0000000000..9b9866c76c --- /dev/null +++ b/internal/rpcapi/convert.go @@ -0,0 +1,140 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "errors" + "fmt" + + "github.com/zclconf/go-cty/cty" + msgpack "github.com/zclconf/go-cty/cty/msgpack" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func diagnosticsToProto(diags tfdiags.Diagnostics) []*terraform1.Diagnostic { + if len(diags) == 0 { + return nil + } + + ret := make([]*terraform1.Diagnostic, len(diags)) + for i, diag := range diags { + ret[i] = diagnosticToProto(diag) + } + return ret +} + +func diagnosticToProto(diag tfdiags.Diagnostic) *terraform1.Diagnostic { + protoDiag := &terraform1.Diagnostic{} + + switch diag.Severity() { + case tfdiags.Error: + protoDiag.Severity = terraform1.Diagnostic_ERROR + case tfdiags.Warning: + protoDiag.Severity = terraform1.Diagnostic_WARNING + default: + protoDiag.Severity = terraform1.Diagnostic_INVALID + } + + desc := diag.Description() + protoDiag.Summary = desc.Summary + protoDiag.Detail = desc.Detail + + srcRngs := diag.Source() + if srcRngs.Subject != nil { + protoDiag.Subject = sourceRangeToProto(*srcRngs.Subject) + } + if srcRngs.Context != nil { + protoDiag.Context = sourceRangeToProto(*srcRngs.Context) + } + + return protoDiag +} + +func sourceRangeToProto(rng tfdiags.SourceRange) *terraform1.SourceRange { + return &terraform1.SourceRange{ + // RPC API operations use source address syntax for "filename" by + // convention, because the physical filesystem layout is an + // implementation detail. + SourceAddr: rng.Filename, + + Start: sourcePosToProto(rng.Start), + End: sourcePosToProto(rng.End), + } +} + +func sourceRangeFromProto(protoRng *terraform1.SourceRange) tfdiags.SourceRange { + return tfdiags.SourceRange{ + Filename: protoRng.SourceAddr, + Start: sourcePosFromProto(protoRng.Start), + End: sourcePosFromProto(protoRng.End), + } +} + +func sourcePosToProto(pos tfdiags.SourcePos) *terraform1.SourcePos { + return &terraform1.SourcePos{ + Byte: int64(pos.Byte), + Line: int64(pos.Line), + Column: int64(pos.Column), + } +} + +func sourcePosFromProto(protoPos *terraform1.SourcePos) tfdiags.SourcePos { + return tfdiags.SourcePos{ + Byte: int(protoPos.Byte), + Line: int(protoPos.Line), + Column: int(protoPos.Column), + } +} + +func dynamicTypedValueFromProto(protoVal *stacks.DynamicValue) (cty.Value, error) { + if len(protoVal.Msgpack) == 0 { + return cty.DynamicVal, fmt.Errorf("uses unsupported serialization format (only MessagePack is supported)") + } + v, err := msgpack.Unmarshal(protoVal.Msgpack, cty.DynamicPseudoType) + if err != nil { + return cty.DynamicVal, fmt.Errorf("invalid serialization: %w", err) + } + // FIXME: Incredibly imprecise handling of sensitive values. We should + // actually decode the attribute paths and mark individual leaf attributes + // that are sensitive, but for now we'll just mark the whole thing as + // sensitive if any part of it is sensitive. + if len(protoVal.Sensitive) != 0 { + v = v.Mark(marks.Sensitive) + } + return v, nil +} + +func externalInputValuesFromProto(protoVals map[string]*stacks.DynamicValueWithSource) (map[stackaddrs.InputVariable]stackruntime.ExternalInputValue, error) { + if len(protoVals) == 0 { + return nil, nil + } + var err error + ret := make(map[stackaddrs.InputVariable]stackruntime.ExternalInputValue, len(protoVals)) + for name, protoVal := range protoVals { + v, moreErr := externalInputValueFromProto(protoVal) + if moreErr != nil { + err = errors.Join(err, fmt.Errorf("%s: %w", name, moreErr)) + } + ret[stackaddrs.InputVariable{Name: name}] = v + } + return ret, err +} + +func externalInputValueFromProto(protoVal *stacks.DynamicValueWithSource) (stackruntime.ExternalInputValue, error) { + v, err := dynamicTypedValueFromProto(protoVal.Value) + if err != nil { + return stackruntime.ExternalInputValue{}, nil + } + rng := sourceRangeFromProto(protoVal.SourceRange) + return stackruntime.ExternalInputValue{ + Value: v, + DefRange: rng, + }, nil +} diff --git a/internal/rpcapi/convert_test.go b/internal/rpcapi/convert_test.go new file mode 100644 index 0000000000..00c5790bf2 --- /dev/null +++ b/internal/rpcapi/convert_test.go @@ -0,0 +1,187 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "fmt" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/tfdiags" + + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestDiagnosticsToProto(t *testing.T) { + tests := map[string]struct { + Input tfdiags.Diagnostics + Want []*terraform1.Diagnostic + }{ + "nil": { + Input: nil, + Want: nil, + }, + "empty": { + Input: make(tfdiags.Diagnostics, 0, 5), + Want: nil, + }, + "sourceless": { + Input: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Something annoying", + "But I'll get over it.", + ), + }, + Want: []*terraform1.Diagnostic{ + { + Severity: terraform1.Diagnostic_ERROR, + Summary: "Something annoying", + Detail: "But I'll get over it.", + }, + }, + }, + "warning": { + Input: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "I have a very bad feeling about this", + "That's no moon; it's a space station.", + ), + }, + Want: []*terraform1.Diagnostic{ + { + Severity: terraform1.Diagnostic_WARNING, + Summary: "I have a very bad feeling about this", + Detail: "That's no moon; it's a space station.", + }, + }, + }, + "with subject": { + Input: tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something annoying", + Detail: "But I'll get over it.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/foo.git", + Start: hcl.InitialPos, + End: hcl.Pos{ + Byte: 2, + Line: 3, + Column: 4, + }, + }, + }, + ), + Want: []*terraform1.Diagnostic{ + { + Severity: terraform1.Diagnostic_ERROR, + Summary: "Something annoying", + Detail: "But I'll get over it.", + Subject: &terraform1.SourceRange{ + SourceAddr: "git::https://example.com/foo.git", + Start: &terraform1.SourcePos{ + Byte: 0, + Line: 1, + Column: 1, + }, + End: &terraform1.SourcePos{ + Byte: 2, + Line: 3, + Column: 4, + }, + }, + }, + }, + }, + "with subject and context": { + Input: tfdiags.Diagnostics{}.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something annoying", + Detail: "But I'll get over it.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/foo.git", + Start: hcl.InitialPos, + End: hcl.Pos{ + Byte: 2, + Line: 3, + Column: 4, + }, + }, + Context: &hcl.Range{ + Filename: "git::https://example.com/foo.git", + Start: hcl.InitialPos, + End: hcl.Pos{ + Byte: 5, + Line: 6, + Column: 7, + }, + }, + }, + ), + Want: []*terraform1.Diagnostic{ + { + Severity: terraform1.Diagnostic_ERROR, + Summary: "Something annoying", + Detail: "But I'll get over it.", + Subject: &terraform1.SourceRange{ + SourceAddr: "git::https://example.com/foo.git", + Start: &terraform1.SourcePos{ + Byte: 0, + Line: 1, + Column: 1, + }, + End: &terraform1.SourcePos{ + Byte: 2, + Line: 3, + Column: 4, + }, + }, + Context: &terraform1.SourceRange{ + SourceAddr: "git::https://example.com/foo.git", + Start: &terraform1.SourcePos{ + Byte: 0, + Line: 1, + Column: 1, + }, + End: &terraform1.SourcePos{ + Byte: 5, + Line: 6, + Column: 7, + }, + }, + }, + }, + }, + "with only severity and summary": { + // This is the kind of degenerate diagnostic we produce when + // we're just naively wrapping a Go error, as tends to arise + // in providers that are just passing through their SDK errors. + Input: tfdiags.Diagnostics{}.Append( + fmt.Errorf("oh no bad"), + ), + Want: []*terraform1.Diagnostic{ + { + Severity: terraform1.Diagnostic_ERROR, + Summary: "oh no bad", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got := diagnosticsToProto(test.Input) + want := test.Want + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} diff --git a/internal/rpcapi/dependencies.go b/internal/rpcapi/dependencies.go new file mode 100644 index 0000000000..e5ea7fd491 --- /dev/null +++ b/internal/rpcapi/dependencies.go @@ -0,0 +1,732 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "errors" + "fmt" + "net/url" + "os/exec" + "path/filepath" + "sort" + + "github.com/apparentlymart/go-versions/versions" + plugin "github.com/hashicorp/go-plugin" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/terraform-svchost/disco" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform/internal/addrs" + terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/logging" + tfplugin "github.com/hashicorp/terraform/internal/plugin" + tfplugin6 "github.com/hashicorp/terraform/internal/plugin6" + "github.com/hashicorp/terraform/internal/providercache" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +type dependenciesServer struct { + dependencies.UnimplementedDependenciesServer + + handles *handleTable + services *disco.Disco +} + +func newDependenciesServer(handles *handleTable, services *disco.Disco) *dependenciesServer { + return &dependenciesServer{ + handles: handles, + services: services, + } +} + +func (s *dependenciesServer) OpenSourceBundle(ctx context.Context, req *dependencies.OpenSourceBundle_Request) (*dependencies.OpenSourceBundle_Response, error) { + localDir := filepath.Clean(req.LocalPath) + sources, err := sourcebundle.OpenDir(localDir) + if err != nil { + return nil, status.Error(codes.Unknown, err.Error()) + } + hnd := s.handles.NewSourceBundle(sources) + return &dependencies.OpenSourceBundle_Response{ + SourceBundleHandle: hnd.ForProtobuf(), + }, err +} + +func (s *dependenciesServer) CloseSourceBundle(ctx context.Context, req *dependencies.CloseSourceBundle_Request) (*dependencies.CloseSourceBundle_Response, error) { + hnd := handle[*sourcebundle.Bundle](req.SourceBundleHandle) + err := s.handles.CloseSourceBundle(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &dependencies.CloseSourceBundle_Response{}, nil +} + +func (s *dependenciesServer) OpenDependencyLockFile(ctx context.Context, req *dependencies.OpenDependencyLockFile_Request) (*dependencies.OpenDependencyLockFile_Response, error) { + sourcesHnd := handle[*sourcebundle.Bundle](req.SourceBundleHandle) + sources := s.handles.SourceBundle(sourcesHnd) + if sources == nil { + return nil, status.Error(codes.InvalidArgument, "invalid source bundle handle") + } + + lockFileSource, err := resolveFinalSourceAddr(req.SourceAddress, sources) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid source address: %s", err) + } + + lockFilePath, err := sources.LocalPathForSource(lockFileSource) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "specified lock file is not available: %s", err) + } + + locks, diags := depsfile.LoadLocksFromFile(lockFilePath) + if diags.HasErrors() { + return &dependencies.OpenDependencyLockFile_Response{ + Diagnostics: diagnosticsToProto(diags), + }, nil + } + + locksHnd := s.handles.NewDependencyLocks(locks) + return &dependencies.OpenDependencyLockFile_Response{ + DependencyLocksHandle: locksHnd.ForProtobuf(), + Diagnostics: diagnosticsToProto(diags), + }, nil +} + +func (s *dependenciesServer) CreateDependencyLocks(ctx context.Context, req *dependencies.CreateDependencyLocks_Request) (*dependencies.CreateDependencyLocks_Response, error) { + locks := depsfile.NewLocks() + for _, provider := range req.ProviderSelections { + addr, diags := addrs.ParseProviderSourceString(provider.SourceAddr) + if diags.HasErrors() { + return nil, status.Errorf(codes.InvalidArgument, "invalid provider source string %q", provider.SourceAddr) + } + version, err := getproviders.ParseVersion(provider.Version) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid version %q for %q: %s", provider.Version, addr.ForDisplay(), err) + } + hashes := make([]getproviders.Hash, len(provider.Hashes)) + for i, hashStr := range provider.Hashes { + hash, err := getproviders.ParseHash(hashStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid hash %q for %q: %s", hashStr, addr.ForDisplay(), err) + } + hashes[i] = hash + } + + if existing := locks.Provider(addr); existing != nil { + return nil, status.Errorf(codes.InvalidArgument, "duplicate entry for provider %q", addr.ForDisplay()) + } + + if !depsfile.ProviderIsLockable(addr) { + if addr.IsBuiltIn() { + status.Errorf(codes.InvalidArgument, "cannot lock builtin provider %q", addr.ForDisplay()) + } + return nil, status.Errorf(codes.InvalidArgument, "provider %q does not support dependency locking", addr.ForDisplay()) + } + + locks.SetProvider( + addr, version, + nil, hashes, + ) + } + + locksHnd := s.handles.NewDependencyLocks(locks) + return &dependencies.CreateDependencyLocks_Response{ + DependencyLocksHandle: locksHnd.ForProtobuf(), + }, nil +} + +func (s *dependenciesServer) CloseDependencyLocks(ctx context.Context, req *dependencies.CloseDependencyLocks_Request) (*dependencies.CloseDependencyLocks_Response, error) { + hnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + err := s.handles.CloseDependencyLocks(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid dependency locks handle") + } + return &dependencies.CloseDependencyLocks_Response{}, nil +} + +func (s *dependenciesServer) GetLockedProviderDependencies(ctx context.Context, req *dependencies.GetLockedProviderDependencies_Request) (*dependencies.GetLockedProviderDependencies_Response, error) { + hnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + locks := s.handles.DependencyLocks(hnd) + if locks == nil { + return nil, status.Error(codes.InvalidArgument, "invalid dependency locks handle") + } + + providers := locks.AllProviders() + protoProviders := make([]*terraform1.ProviderPackage, 0, len(providers)) + for _, lock := range providers { + hashes := lock.PreferredHashes() + var hashStrs []string + if len(hashes) != 0 { + hashStrs = make([]string, len(hashes)) + } + for i, hash := range hashes { + hashStrs[i] = hash.String() + } + protoProviders = append(protoProviders, &terraform1.ProviderPackage{ + SourceAddr: lock.Provider().String(), + Version: lock.Version().String(), + Hashes: hashStrs, + }) + } + + // This is just to make the result be consistent between requests. This + // _particular_ ordering is not guaranteed to callers. + sort.Slice(protoProviders, func(i, j int) bool { + return protoProviders[i].SourceAddr < protoProviders[j].SourceAddr + }) + + return &dependencies.GetLockedProviderDependencies_Response{ + SelectedProviders: protoProviders, + }, nil +} + +func (s *dependenciesServer) BuildProviderPluginCache(req *dependencies.BuildProviderPluginCache_Request, evts dependencies.Dependencies_BuildProviderPluginCacheServer) error { + ctx := evts.Context() + + hnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + locks := s.handles.DependencyLocks(hnd) + if locks == nil { + return status.Error(codes.InvalidArgument, "invalid dependency locks handle") + } + + selectors := make([]getproviders.MultiSourceSelector, 0, len(req.InstallationMethods)) + for _, protoMethod := range req.InstallationMethods { + var source getproviders.Source + switch arg := protoMethod.Source.(type) { + case *dependencies.BuildProviderPluginCache_Request_InstallMethod_Direct: + source = getproviders.NewRegistrySource(s.services) + case *dependencies.BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir: + source = getproviders.NewFilesystemMirrorSource(arg.LocalMirrorDir) + case *dependencies.BuildProviderPluginCache_Request_InstallMethod_NetworkMirrorUrl: + u, err := url.Parse(arg.NetworkMirrorUrl) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid network mirror URL %q", arg.NetworkMirrorUrl) + } + source = getproviders.NewHTTPMirrorSource(u, s.services.CredentialsSource()) + default: + // The above should be exhaustive for all variants defined in + // the protocol buffers schema. + return status.Errorf(codes.Internal, "unsupported installation method source type %T", arg) + } + + if len(protoMethod.Include) != 0 || len(protoMethod.Exclude) != 0 { + return status.Error(codes.InvalidArgument, "include/exclude for installation methods is not yet implemented") + } + + selectors = append(selectors, getproviders.MultiSourceSelector{ + Source: source, + // TODO: Deal with the include/exclude options + }) + } + instSrc := getproviders.MultiSource(selectors) + + var cacheDir *providercache.Dir + if req.OverridePlatform == "" { + cacheDir = providercache.NewDir(req.CacheDir) + } else { + platform, err := getproviders.ParsePlatform(req.OverridePlatform) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid overridden platform name %q: %s", req.OverridePlatform, err) + } + cacheDir = providercache.NewDirWithPlatform(req.CacheDir, platform) + } + inst := providercache.NewInstaller(cacheDir, instSrc) + + // The provider installer was originally built to install providers needed + // by a configuration/state with reference to a dependency locks object, + // but the model here is different: we are aiming to install exactly the + // providers selected in the locks. To get there with the installer as + // currently designed, we'll build some synthetic provider requirements + // that call for any version of each of the locked providers, and then + // the lock file will dictate which version we select. + wantProviders := locks.AllProviders() + reqd := make(getproviders.Requirements, len(wantProviders)) + for addr := range wantProviders { + reqd[addr] = nil + } + + // We'll translate most events from the provider installer directly into + // RPC-shaped events, so that the caller can use these to drive + // progress-reporting UI if needed. + sentErrorDiags := false + instEvts := providercache.InstallerEvents{ + PendingProviders: func(reqs map[addrs.Provider]getproviders.VersionConstraints) { + // This one announces which providers we are expecting to install, + // which could potentially help drive a percentage-based progress + // bar or similar in the UI by correlating with the "FetchSuccess" + // events. + protoConstraints := make([]*dependencies.BuildProviderPluginCache_Event_ProviderConstraints, 0, len(reqs)) + for addr, constraints := range reqs { + protoConstraints = append(protoConstraints, &dependencies.BuildProviderPluginCache_Event_ProviderConstraints{ + SourceAddr: addr.ForDisplay(), + Versions: getproviders.VersionConstraintsString(constraints), + }) + } + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_Pending_{ + Pending: &dependencies.BuildProviderPluginCache_Event_Pending{ + Expected: protoConstraints, + }, + }, + }) + }, + ProviderAlreadyInstalled: func(provider addrs.Provider, selectedVersion getproviders.Version) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_AlreadyInstalled{ + AlreadyInstalled: &dependencies.BuildProviderPluginCache_Event_ProviderVersion{ + SourceAddr: provider.ForDisplay(), + Version: selectedVersion.String(), + }, + }, + }) + }, + BuiltInProviderAvailable: func(provider addrs.Provider) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_BuiltIn{ + BuiltIn: &dependencies.BuildProviderPluginCache_Event_ProviderVersion{ + SourceAddr: provider.ForDisplay(), + }, + }, + }) + }, + BuiltInProviderFailure: func(provider addrs.Provider, err error) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_Diagnostic{ + Diagnostic: diagnosticToProto(tfdiags.Sourceless( + tfdiags.Error, + "Built-in provider unavailable", + fmt.Sprintf( + "Terraform v%s does not support the provider %q.", + version.SemVer.String(), provider.ForDisplay(), + ), + )), + }, + }) + sentErrorDiags = true + }, + QueryPackagesBegin: func(provider addrs.Provider, versionConstraints getproviders.VersionConstraints, locked bool) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_QueryBegin{ + QueryBegin: &dependencies.BuildProviderPluginCache_Event_ProviderConstraints{ + SourceAddr: provider.ForDisplay(), + Versions: getproviders.VersionConstraintsString(versionConstraints), + }, + }, + }) + }, + QueryPackagesSuccess: func(provider addrs.Provider, selectedVersion getproviders.Version) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_QuerySuccess{ + QuerySuccess: &dependencies.BuildProviderPluginCache_Event_ProviderVersion{ + SourceAddr: provider.ForDisplay(), + Version: selectedVersion.String(), + }, + }, + }) + }, + QueryPackagesWarning: func(provider addrs.Provider, warn []string) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_QueryWarnings{ + QueryWarnings: &dependencies.BuildProviderPluginCache_Event_ProviderWarnings{ + SourceAddr: provider.ForDisplay(), + Warnings: warn, + }, + }, + }) + }, + QueryPackagesFailure: func(provider addrs.Provider, err error) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_Diagnostic{ + Diagnostic: diagnosticToProto(tfdiags.Sourceless( + tfdiags.Error, + "Provider is unavailable", + fmt.Sprintf( + "Failed to query for provider %s: %s.", + provider.ForDisplay(), + tfdiags.FormatError(err), + ), + )), + }, + }) + sentErrorDiags = true + }, + FetchPackageBegin: func(provider addrs.Provider, version getproviders.Version, location getproviders.PackageLocation) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_FetchBegin_{ + FetchBegin: &dependencies.BuildProviderPluginCache_Event_FetchBegin{ + ProviderVersion: &dependencies.BuildProviderPluginCache_Event_ProviderVersion{ + SourceAddr: provider.ForDisplay(), + Version: version.String(), + }, + Location: location.String(), + }, + }, + }) + }, + FetchPackageSuccess: func(provider addrs.Provider, version getproviders.Version, localDir string, authResult *getproviders.PackageAuthenticationResult) { + var protoAuthResult dependencies.BuildProviderPluginCache_Event_FetchComplete_AuthResult + var keyID string + if authResult != nil { + keyID = authResult.KeyID + switch { + case authResult.SignedByHashiCorp(): + protoAuthResult = dependencies.BuildProviderPluginCache_Event_FetchComplete_OFFICIAL_SIGNED + default: + // TODO: The getproviders.PackageAuthenticationResult type + // only exposes the full detail of the signing outcome as + // a string intended for direct display in the UI, which + // means we can't populate this in full detail. For now + // we'll treat anything signed by a non-HashiCorp key as + // "unknown" and then rationalize this later. + protoAuthResult = dependencies.BuildProviderPluginCache_Event_FetchComplete_UNKNOWN + } + } + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_FetchComplete_{ + FetchComplete: &dependencies.BuildProviderPluginCache_Event_FetchComplete{ + ProviderVersion: &dependencies.BuildProviderPluginCache_Event_ProviderVersion{ + SourceAddr: provider.ForDisplay(), + Version: version.String(), + }, + KeyIdForDisplay: keyID, + AuthResult: protoAuthResult, + }, + }, + }) + }, + FetchPackageFailure: func(provider addrs.Provider, version getproviders.Version, err error) { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_Diagnostic{ + Diagnostic: diagnosticToProto(tfdiags.Sourceless( + tfdiags.Error, + "Failed to fetch provider package", + fmt.Sprintf( + "Failed to fetch provider %s v%s: %s.", + provider.ForDisplay(), version.String(), + tfdiags.FormatError(err), + ), + )), + }, + }) + sentErrorDiags = true + }, + } + ctx = instEvts.OnContext(ctx) + + _, err := inst.EnsureProviderVersions(ctx, locks, reqd, providercache.InstallNewProvidersOnly) + if err != nil { + // If we already emitted errors in the form of diagnostics then + // err will typically just duplicate them, so we'll skip emitting + // another diagnostic in that case. + if !sentErrorDiags { + evts.Send(&dependencies.BuildProviderPluginCache_Event{ + Event: &dependencies.BuildProviderPluginCache_Event_Diagnostic{ + Diagnostic: diagnosticToProto(tfdiags.Sourceless( + tfdiags.Error, + "Failed to install providers", + fmt.Sprintf( + "Cannot install the selected provider plugins: %s.", + tfdiags.FormatError(err), + ), + )), + }, + }) + sentErrorDiags = true + } + } + + // "Success" for this RPC just means that the call was valid and we ran + // to completion. We only return an error for situations that appear to be + // bugs in the calling program, rather than problems with the installation + // process. + return nil +} + +func (s *dependenciesServer) OpenProviderPluginCache(ctx context.Context, req *dependencies.OpenProviderPluginCache_Request) (*dependencies.OpenProviderPluginCache_Response, error) { + var cacheDir *providercache.Dir + if req.OverridePlatform == "" { + cacheDir = providercache.NewDir(req.CacheDir) + } else { + platform, err := getproviders.ParsePlatform(req.OverridePlatform) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid overridden platform name %q: %s", req.OverridePlatform, err) + } + cacheDir = providercache.NewDirWithPlatform(req.CacheDir, platform) + } + + hnd := s.handles.NewProviderPluginCache(cacheDir) + return &dependencies.OpenProviderPluginCache_Response{ + ProviderCacheHandle: hnd.ForProtobuf(), + }, nil +} + +func (s *dependenciesServer) CloseProviderPluginCache(ctx context.Context, req *dependencies.CloseProviderPluginCache_Request) (*dependencies.CloseProviderPluginCache_Response, error) { + hnd := handle[*providercache.Dir](req.ProviderCacheHandle) + err := s.handles.CloseProviderPluginCache(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid provider plugin cache handle") + } + return &dependencies.CloseProviderPluginCache_Response{}, nil +} + +func (s *dependenciesServer) GetCachedProviders(ctx context.Context, req *dependencies.GetCachedProviders_Request) (*dependencies.GetCachedProviders_Response, error) { + hnd := handle[*providercache.Dir](req.ProviderCacheHandle) + cacheDir := s.handles.ProviderPluginCache(hnd) + if cacheDir == nil { + return nil, status.Error(codes.InvalidArgument, "invalid provider plugin cache handle") + } + + avail := cacheDir.AllAvailablePackages() + ret := make([]*terraform1.ProviderPackage, 0, len(avail)) + for addr, pkgs := range avail { + for _, pkg := range pkgs { + hash, err := pkg.Hash() + var protoHashes []string + // We silently invalid hashes here so we can make a best + // effort to return as much information as possible, rather + // than failing if the cache is partially inaccessible. + // Callers can detect this situation by the hash sequence being + // empty. + if err == nil { + protoHashes = append(protoHashes, hash.String()) + } + + ret = append(ret, &terraform1.ProviderPackage{ + SourceAddr: addr.String(), + Version: pkg.Version.String(), + Hashes: protoHashes, + }) + } + } + + return &dependencies.GetCachedProviders_Response{ + AvailableProviders: ret, + }, nil +} + +func (s *dependenciesServer) GetBuiltInProviders(ctx context.Context, req *dependencies.GetBuiltInProviders_Request) (*dependencies.GetBuiltInProviders_Response, error) { + ret := make([]*terraform1.ProviderPackage, 0, len(builtinProviders)) + for typeName := range builtinProviders { + ret = append(ret, &terraform1.ProviderPackage{ + SourceAddr: addrs.NewBuiltInProvider(typeName).ForDisplay(), + }) + } + sort.Slice(ret, func(i, j int) bool { + return ret[i].SourceAddr < ret[j].SourceAddr + }) + return &dependencies.GetBuiltInProviders_Response{ + AvailableProviders: ret, + }, nil +} + +func (s *dependenciesServer) GetProviderSchema(ctx context.Context, req *dependencies.GetProviderSchema_Request) (*dependencies.GetProviderSchema_Response, error) { + var cacheHnd handle[*providercache.Dir] + var cacheDir *providercache.Dir + if req.GetProviderCacheHandle() != 0 { + cacheHnd = handle[*providercache.Dir](req.ProviderCacheHandle) + cacheDir = s.handles.ProviderPluginCache(cacheHnd) + if cacheDir == nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid provider cache handle") + } + } + // NOTE: cacheDir will be nil if the cache handle was absent. We'll + // check that below once we know if the requested provider is a built-in. + + var err error + + providerAddr, diags := addrs.ParseProviderSourceString(req.ProviderAddr) + if diags.HasErrors() { + return nil, status.Error(codes.InvalidArgument, "invalid provider source address syntax") + } + var providerVersion getproviders.Version + if req.ProviderVersion != "" { + if providerAddr.IsBuiltIn() { + return nil, status.Errorf(codes.InvalidArgument, "can't specify version for built-in provider") + } + providerVersion, err = getproviders.ParseVersion(req.ProviderVersion) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid provider version string: %s", err) + } + } + + // For non-builtin providers the caller MUST provide a provider cache + // handle. For built-in providers it's optional. + if cacheHnd.IsNil() && !providerAddr.IsBuiltIn() { + return nil, status.Errorf(codes.InvalidArgument, "provider cache handle is required for non-builtin provider") + } + + schemaResp, err := loadProviderSchema(providerAddr, providerVersion, cacheDir) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &dependencies.GetProviderSchema_Response{ + Schema: providerSchemaToProto(schemaResp), + }, nil +} + +func resolveFinalSourceAddr(protoSourceAddr *terraform1.SourceAddress, sources *sourcebundle.Bundle) (sourceaddrs.FinalSource, error) { + sourceAddr, err := sourceaddrs.ParseSource(protoSourceAddr.Source) + if err != nil { + return nil, fmt.Errorf("invalid location: %w", err) + } + var allowedVersions versions.Set + if sourceAddr.SupportsVersionConstraints() { + allowedVersions, err = versions.MeetingConstraintsStringRuby(protoSourceAddr.Versions) + if err != nil { + return nil, fmt.Errorf("invalid version constraints: %w", err) + } + } else { + if protoSourceAddr.Versions != "" { + return nil, fmt.Errorf("can't use version constraints with this source type") + } + } + + switch sourceAddr := sourceAddr.(type) { + case sourceaddrs.FinalSource: + // Easy case: it's already a final source so we can just return it. + return sourceAddr, nil + case sourceaddrs.RegistrySource: + // Turning a RegistrySource into a final source means we need to + // figure out which exact version the source address is selecting. + availableVersions := sources.RegistryPackageVersions(sourceAddr.Package()) + selectedVersion := availableVersions.NewestInSet(allowedVersions) + return sourceAddr.Versioned(selectedVersion), nil + default: + // Should not get here; if sourceaddrs gets any new non-final source + // types in future then we ought to add a cases for them above at the + // same time as upgrading the go-slug dependency. + return nil, fmt.Errorf("unsupported source address type %T (this is a bug in Terraform)", sourceAddr) + } +} + +// builtinProviders provides the instantiation functions for each of the +// built-in providers that are available when using Terraform Core through +// its RPC API. +// +// TODO: Prior to the RPC API the built-in providers were architecturally +// the responsibility of Terraform CLI, which is a bit strange and means +// we can't readily share this definition with the CLI-driven usage patterns. +// In future it would be nice to factor out the table of built-in providers +// into a common location that both can share, or ideally change Terraform CLI +// to consume this RPC API through an internal API bridge so that the +// architectural divide between CLI and Core is more explicit. +var builtinProviders map[string]func() providers.Interface + +func init() { + builtinProviders = map[string]func() providers.Interface{ + "terraform": func() providers.Interface { + return terraformProvider.NewProvider() + }, + } +} + +// providerFactoriesForLocks builds a map of factory functions for all of the +// providers selected by the given locks and also all of the built-in providers. +// +// Non-builtin providers are assumed to be plugins available in the given +// plugin cache directory. pluginsDir can be nil if and only if the given +// locks is empty of provider selections, in which case the result contains +// only the built-in providers. +// +// If any of the selected providers are not available as plugins in the cache +// directory, returns an error describing a problem with at least one of +// of them. +func providerFactoriesForLocks(locks *depsfile.Locks, pluginsDir *providercache.Dir) (map[addrs.Provider]providers.Factory, error) { + var err error + ret := make(map[addrs.Provider]providers.Factory) + for name, infallibleFactory := range builtinProviders { + infallibleFactory := infallibleFactory // each iteration must have its own symbol + ret[addrs.NewBuiltInProvider(name)] = func() (providers.Interface, error) { + return infallibleFactory(), nil + } + } + selectedProviders := locks.AllProviders() + if pluginsDir == nil { + if len(selectedProviders) != 0 { + return nil, fmt.Errorf("only built-in providers are available without a plugin cache directory") + } + return ret, nil // just the built-in providers then + } + + for addr, lock := range selectedProviders { + addr := addr + lock := lock + + selectedVersion := lock.Version() + cached := pluginsDir.ProviderVersion(addr, selectedVersion) + if cached == nil { + err = errors.Join(err, fmt.Errorf("plugin cache directory does not contain %s v%s", addr, selectedVersion)) + continue + } + + // The cached package must match at least one of the locked + // package checksums. + matchesChecksums, checksumErr := cached.MatchesAnyHash(lock.PreferredHashes()) + if checksumErr != nil { + err = errors.Join(err, fmt.Errorf("failed to calculate checksum for cached %s v%s: %w", addr, selectedVersion, checksumErr)) + continue + } + if !matchesChecksums { + err = errors.Join(err, fmt.Errorf("cached package for %s v%s does not match any of the locked checksums", addr, selectedVersion)) + continue + } + + exeFilename, exeErr := cached.ExecutableFile() + if exeErr != nil { + err = errors.Join(err, fmt.Errorf("unusuable cached package for %s v%s: %w", addr, selectedVersion, exeErr)) + continue + } + + ret[addr] = func() (providers.Interface, error) { + config := &plugin.ClientConfig{ + HandshakeConfig: tfplugin.Handshake, + Logger: logging.NewProviderLogger(""), + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + Managed: true, + Cmd: exec.Command(exeFilename), + AutoMTLS: true, + VersionedPlugins: tfplugin.VersionedPlugins, + SyncStdout: logging.PluginOutputMonitor(fmt.Sprintf("%s:stdout", addr)), + SyncStderr: logging.PluginOutputMonitor(fmt.Sprintf("%s:stderr", addr)), + } + + client := plugin.NewClient(config) + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName) + if err != nil { + return nil, err + } + + protoVer := client.NegotiatedVersion() + switch protoVer { + case 5: + p := raw.(*tfplugin.GRPCProvider) + p.PluginClient = client + p.Addr = addr + return p, nil + case 6: + p := raw.(*tfplugin6.GRPCProvider) + p.PluginClient = client + p.Addr = addr + return p, nil + default: + panic("unsupported protocol version") + } + } + } + return ret, err +} diff --git a/internal/rpcapi/dependencies_provider_schema.go b/internal/rpcapi/dependencies_provider_schema.go new file mode 100644 index 0000000000..a18054c63f --- /dev/null +++ b/internal/rpcapi/dependencies_provider_schema.go @@ -0,0 +1,294 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "fmt" + "os/exec" + "sort" + + "github.com/apparentlymart/go-versions/versions" + "github.com/hashicorp/go-plugin" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/logging" + tfplugin "github.com/hashicorp/terraform/internal/plugin" + tfplugin6 "github.com/hashicorp/terraform/internal/plugin6" + "github.com/hashicorp/terraform/internal/providercache" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" +) + +// This file contains helper functions and supporting logic for +// Dependencies.GetProviderSchema. The function entry point is in +// dependencies.go with all of the other Dependencies functions. + +// loadProviderSchema attempts to load the schema for a given provider. +// +// If the providerAddr is for a built-in provider then version must be +// [versions.Unspecified] and cacheDir may be nil, although that's not +// required. +// +// If providerAddr is for a non-builtin provider then both version and +// cacheDir are required. +func loadProviderSchema(providerAddr addrs.Provider, version getproviders.Version, cacheDir *providercache.Dir) (providers.GetProviderSchemaResponse, error) { + var provider providers.Interface + switch { + case providerAddr.IsBuiltIn(): + if version != versions.Unspecified { + return providers.GetProviderSchemaResponse{}, fmt.Errorf("built-in providers are unversioned") + } + + var err error + provider, err = unconfiguredBuiltinProviderInstance(providerAddr) + if err != nil { + return providers.GetProviderSchemaResponse{}, err + } + + default: + cached := cacheDir.ProviderVersion(providerAddr, version) + if cached == nil { + return providers.GetProviderSchemaResponse{}, fmt.Errorf("provider cache does not include %s v%s", providerAddr, version) + } + + var err error + provider, err = unconfiguredProviderPluginInstance(cached) + if err != nil { + return providers.GetProviderSchemaResponse{}, fmt.Errorf("failed to launch provider plugin: %w", err) + } + } + + resp := provider.GetProviderSchema() + return resp, nil +} + +func unconfiguredProviderPluginInstance(cached *providercache.CachedProvider) (providers.Interface, error) { + execFile, err := cached.ExecutableFile() + if err != nil { + return nil, err + } + + config := &plugin.ClientConfig{ + HandshakeConfig: tfplugin.Handshake, + Logger: logging.NewProviderLogger(""), + AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC}, + Managed: true, + Cmd: exec.Command(execFile), + AutoMTLS: true, + VersionedPlugins: tfplugin.VersionedPlugins, + } + + client := plugin.NewClient(config) + rpcClient, err := client.Client() + if err != nil { + return nil, err + } + + raw, err := rpcClient.Dispense(tfplugin.ProviderPluginName) + if err != nil { + return nil, err + } + + // store the client so that the plugin can kill the child process + protoVer := client.NegotiatedVersion() + switch protoVer { + case 5: + p := raw.(*tfplugin.GRPCProvider) + p.PluginClient = client + p.Addr = cached.Provider + return p, nil + case 6: + p := raw.(*tfplugin6.GRPCProvider) + p.PluginClient = client + p.Addr = cached.Provider + return p, nil + default: + panic("unsupported protocol version") + } +} + +func unconfiguredBuiltinProviderInstance(addr addrs.Provider) (providers.Interface, error) { + if !addr.IsBuiltIn() { + panic("unconfiguredBuiltinProviderInstance for non-builtin provider") + } + factory, ok := builtinProviders[addr.Type] + if !ok { + return nil, fmt.Errorf("this version of Terraform does not support provider %s", addr) + } + return factory(), nil +} + +func providerSchemaToProto(schemaResp providers.GetProviderSchemaResponse) *dependencies.ProviderSchema { + // Due to some historical poor design planning, the provider protocol uses + // different terminology than the user-facing terminology for Terraform + // Core and the Terraform language, and so part of our job here is to + // map between the two so that rpcapi uses Terraform Core's words + // rather than the provider protocol's words. + // + // This result currently includes only the subset of the schema information + // that would be needed to successfully interpret DynamicValue messages + // returned from other rpcapi operations. Exporting the full provider + // protocol schema model here would tightly couple the rpcapi to the + // provider protocol, forcing them to always change together, which is + // undesirable since each one has a different target audience and therefore + // will probably follow different evolutionary paths. For example, Terraform + // can support multiple provider protocol versions concurrently but will + // probably not want to make a new rpcapi protocol major version each time + // a new provider protocol version is added or removed. + + mrtSchemas := make(map[string]*dependencies.Schema, len(schemaResp.ResourceTypes)) + drtSchemas := make(map[string]*dependencies.Schema, len(schemaResp.DataSources)) + + for name, elem := range schemaResp.ResourceTypes { + mrtSchemas[name] = schemaElementToProto(elem) + } + for name, elem := range schemaResp.DataSources { + drtSchemas[name] = schemaElementToProto(elem) + } + + return &dependencies.ProviderSchema{ + ProviderConfig: schemaElementToProto(schemaResp.Provider), + ManagedResourceTypes: mrtSchemas, + DataResourceTypes: drtSchemas, + } +} + +func schemaElementToProto(elem providers.Schema) *dependencies.Schema { + return &dependencies.Schema{ + Block: schemaBlockToProto(elem.Body), + } +} + +func schemaBlockToProto(block *configschema.Block) *dependencies.Schema_Block { + if block == nil { + return &dependencies.Schema_Block{} + } + attributes := make([]*dependencies.Schema_Attribute, 0, len(block.Attributes)) + for name, attr := range block.Attributes { + attributes = append(attributes, schemaAttributeToProto(name, attr)) + } + sort.Slice(attributes, func(i, j int) bool { + return attributes[i].Name < attributes[j].Name + }) + blockTypes := make([]*dependencies.Schema_NestedBlock, 0, len(block.BlockTypes)) + for typeName, blockType := range block.BlockTypes { + blockTypes = append(blockTypes, schemaNestedBlockToProto(typeName, blockType)) + } + sort.Slice(blockTypes, func(i, j int) bool { + return blockTypes[i].TypeName < blockTypes[j].TypeName + }) + return &dependencies.Schema_Block{ + Deprecated: block.Deprecated, + Description: schemaDocstringToProto(block.Description, block.DescriptionKind), + Attributes: attributes, + BlockTypes: blockTypes, + } +} + +func schemaAttributeToProto(name string, attr *configschema.Attribute) *dependencies.Schema_Attribute { + var err error + var typeBytes []byte + var objectType *dependencies.Schema_Object + if attr.NestedType != nil { + objectType = schemaNestedObjectTypeToProto(attr.NestedType) + } else { + typeBytes, err = attr.Type.MarshalJSON() + if err != nil { + // Should never happen because types we get here are either from + // inside this program (for built-in providers) or already transited + // through the plugin protocol's equivalent of this serialization. + panic(fmt.Sprintf("can't encode %#v as JSON: %s", attr.Type, err)) + } + } + + return &dependencies.Schema_Attribute{ + Name: name, + Type: typeBytes, + NestedType: objectType, + Description: schemaDocstringToProto(attr.Description, attr.DescriptionKind), + Required: attr.Required, + Optional: attr.Optional, + Computed: attr.Computed, + Sensitive: attr.Sensitive, + Deprecated: attr.Deprecated, + } +} + +func schemaNestedBlockToProto(typeName string, blockType *configschema.NestedBlock) *dependencies.Schema_NestedBlock { + var protoNesting dependencies.Schema_NestedBlock_NestingMode + switch blockType.Nesting { + case configschema.NestingSingle: + protoNesting = dependencies.Schema_NestedBlock_SINGLE + case configschema.NestingGroup: + protoNesting = dependencies.Schema_NestedBlock_GROUP + case configschema.NestingList: + protoNesting = dependencies.Schema_NestedBlock_LIST + case configschema.NestingSet: + protoNesting = dependencies.Schema_NestedBlock_SET + case configschema.NestingMap: + protoNesting = dependencies.Schema_NestedBlock_MAP + default: + // The above should be exhaustive for all configschema.NestingMode variants + panic(fmt.Sprintf("invalid structural attribute nesting mode %s", blockType.Nesting)) + } + + return &dependencies.Schema_NestedBlock{ + TypeName: typeName, + Block: schemaBlockToProto(&blockType.Block), + Nesting: protoNesting, + } +} + +func schemaNestedObjectTypeToProto(objType *configschema.Object) *dependencies.Schema_Object { + var protoNesting dependencies.Schema_Object_NestingMode + switch objType.Nesting { + case configschema.NestingSingle: + protoNesting = dependencies.Schema_Object_SINGLE + case configschema.NestingList: + protoNesting = dependencies.Schema_Object_LIST + case configschema.NestingSet: + protoNesting = dependencies.Schema_Object_SET + case configschema.NestingMap: + protoNesting = dependencies.Schema_Object_MAP + default: + // The above should be exhaustive for all configschema.NestingMode variants + panic(fmt.Sprintf("invalid structural attribute nesting mode %s", objType.Nesting)) + } + + attributes := make([]*dependencies.Schema_Attribute, 0, len(objType.Attributes)) + for name, attr := range objType.Attributes { + attributes = append(attributes, schemaAttributeToProto(name, attr)) + } + sort.Slice(attributes, func(i, j int) bool { + return attributes[i].Name < attributes[j].Name + }) + + return &dependencies.Schema_Object{ + Nesting: protoNesting, + Attributes: attributes, + } +} + +func schemaDocstringToProto(doc string, format configschema.StringKind) *dependencies.Schema_DocString { + if doc == "" { + return nil + } + var protoFormat dependencies.Schema_DocString_Format + switch format { + case configschema.StringPlain: + protoFormat = dependencies.Schema_DocString_PLAIN + case configschema.StringMarkdown: + protoFormat = dependencies.Schema_DocString_MARKDOWN + default: + // We'll ignore strings in unsupported formats, although we should + // try to keep the above exhaustive if we add new formats in future. + return nil + } + return &dependencies.Schema_DocString{ + Description: doc, + Format: protoFormat, + } +} diff --git a/internal/rpcapi/dependencies_test.go b/internal/rpcapi/dependencies_test.go new file mode 100644 index 0000000000..5e08fd3697 --- /dev/null +++ b/internal/rpcapi/dependencies_test.go @@ -0,0 +1,453 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "io" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/terraform-svchost/disco" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" + + "google.golang.org/grpc" + "google.golang.org/protobuf/testing/protocmp" + + _ "github.com/hashicorp/terraform/internal/logging" +) + +func TestDependenciesOpenCloseSourceBundle(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + depsServer := newDependenciesServer(handles, disco.New()) + + openResp, err := depsServer.OpenSourceBundle(ctx, &dependencies.OpenSourceBundle_Request{ + LocalPath: "testdata/sourcebundle", + }) + if err != nil { + t.Fatal(err) + } + + // A client wouldn't normally be able to interact directly with the + // source bundle, but we're doing that here to simulate what would + // happen in another service that takes source bundle handles as input. + // (This nested scope encapsulates some internal stuff that a normal client + // would not have access to.) + { + hnd := handle[*sourcebundle.Bundle](openResp.SourceBundleHandle) + sources := handles.SourceBundle(hnd) + if sources == nil { + t.Fatal("returned source bundle handle is invalid") + } + + _, err = sources.LocalPathForSource( + // The following is one of the source addresses known to the + // source bundle we requested above. + sourceaddrs.MustParseSource("git::https://example.com/foo.git").(sourceaddrs.FinalSource), + ) + if err != nil { + t.Fatalf("source bundle doesn't have the package we were expecting: %s", err) + } + } + + _, err = depsServer.CloseSourceBundle(ctx, &dependencies.CloseSourceBundle_Request{ + SourceBundleHandle: openResp.SourceBundleHandle, + }) + if err != nil { + t.Fatal(err) + } +} + +func TestDependencyLocks(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + depsServer := newDependenciesServer(handles, disco.New()) + + openSourcesResp, err := depsServer.OpenSourceBundle(ctx, &dependencies.OpenSourceBundle_Request{ + LocalPath: "testdata/sourcebundle", + }) + if err != nil { + t.Fatal(err) + } + defer func() { + depsServer.CloseSourceBundle(ctx, &dependencies.CloseSourceBundle_Request{ + SourceBundleHandle: openSourcesResp.SourceBundleHandle, + }) + }() + + openLocksResp, err := depsServer.OpenDependencyLockFile(ctx, &dependencies.OpenDependencyLockFile_Request{ + SourceBundleHandle: openSourcesResp.SourceBundleHandle, + SourceAddress: &terraform1.SourceAddress{ + Source: "git::https://example.com/foo.git//.terraform.lock.hcl", + }, + }) + if err != nil { + t.Fatal(err) + } + if len(openLocksResp.Diagnostics) != 0 { + t.Error("OpenDependencyLockFile returned unexpected diagnostics") + } + + // A client wouldn't normally be able to interact directly with the + // locks object, but we're doing that here to simulate what would + // happen in another service that takes dependency lock handles as input. + // (This nested scope encapsulates some internal stuff that a normal client + // would not have access to.) + { + hnd := handle[*depsfile.Locks](openLocksResp.DependencyLocksHandle) + locks := handles.DependencyLocks(hnd) + if locks == nil { + t.Fatal("returned dependency locks handle is invalid") + } + + wantProvider := addrs.MustParseProviderSourceString("example.com/foo/bar") + got := locks.AllProviders() + want := map[addrs.Provider]*depsfile.ProviderLock{ + wantProvider: depsfile.NewProviderLock( + wantProvider, getproviders.MustParseVersion("1.2.3"), + nil, + []getproviders.Hash{ + "zh:abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + }, + ), + } + if diff := cmp.Diff(want, got, cmp.AllowUnexported(depsfile.ProviderLock{})); diff != "" { + t.Errorf("wrong locked providers\n%s", diff) + } + } + + getProvidersResp, err := depsServer.GetLockedProviderDependencies(ctx, &dependencies.GetLockedProviderDependencies_Request{ + DependencyLocksHandle: openLocksResp.DependencyLocksHandle, + }) + if err != nil { + t.Fatal(err) + } + wantProviderLocks := []*terraform1.ProviderPackage{ + { + SourceAddr: "example.com/foo/bar", + Version: "1.2.3", + Hashes: []string{ + "zh:abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", + }, + }, + } + if diff := cmp.Diff(wantProviderLocks, getProvidersResp.SelectedProviders, protocmp.Transform()); diff != "" { + t.Errorf("wrong GetLockedProviderDependencies result\n%s", diff) + } + + _, err = depsServer.CloseDependencyLocks(ctx, &dependencies.CloseDependencyLocks_Request{ + DependencyLocksHandle: openLocksResp.DependencyLocksHandle, + }) + if err != nil { + t.Fatal(err) + } + + // We should now be able to create a new locks handle referring to the + // same providers as the one we just closed. This simulates a caller + // propagating its provider locks between separate instances of rpcapi. + newLocksResp, err := depsServer.CreateDependencyLocks(ctx, &dependencies.CreateDependencyLocks_Request{ + ProviderSelections: getProvidersResp.SelectedProviders, + }) + if err != nil { + t.Fatal(err) + } + + getProvidersResp, err = depsServer.GetLockedProviderDependencies(ctx, &dependencies.GetLockedProviderDependencies_Request{ + DependencyLocksHandle: newLocksResp.DependencyLocksHandle, + }) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(wantProviderLocks, getProvidersResp.SelectedProviders, protocmp.Transform()); diff != "" { + t.Errorf("wrong GetLockedProviderDependencies result\n%s", diff) + } +} + +func TestDependenciesProviderCache(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + depsServer := newDependenciesServer(handles, disco.New()) + + // This test involves a streaming RPC operation, so we'll need help from + // a real in-memory gRPC connection to exercise it concisely so that + // we can work with the client API rather than the server API. + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + dependencies.RegisterDependenciesServer(srv, depsServer) + }) + defer close() + depsClient := dependencies.NewDependenciesClient(grpcClient) + + openSourcesResp, err := depsClient.OpenSourceBundle(ctx, &dependencies.OpenSourceBundle_Request{ + LocalPath: "testdata/sourcebundle", + }) + if err != nil { + t.Fatal(err) + } + defer func() { + _, err := depsClient.CloseSourceBundle(ctx, &dependencies.CloseSourceBundle_Request{ + SourceBundleHandle: openSourcesResp.SourceBundleHandle, + }) + if err != nil { + t.Error(err) + } + }() + + openLocksResp, err := depsClient.OpenDependencyLockFile(ctx, &dependencies.OpenDependencyLockFile_Request{ + SourceBundleHandle: openSourcesResp.SourceBundleHandle, + SourceAddress: &terraform1.SourceAddress{ + Source: "git::https://example.com/foo.git//.terraform.lock.hcl", + }, + }) + if err != nil { + t.Fatal(err) + } + if len(openLocksResp.Diagnostics) != 0 { + t.Error("OpenDependencyLockFile returned unexpected diagnostics") + } + + tmpDir := t.TempDir() + cacheDir := filepath.Join(tmpDir, "pc") + + evts, err := depsClient.BuildProviderPluginCache(ctx, &dependencies.BuildProviderPluginCache_Request{ + DependencyLocksHandle: openLocksResp.DependencyLocksHandle, + CacheDir: cacheDir, + + // We force a local provider mirror and fake platform here just to keep + // this test self-contained. This wraps the provider installer which + // already has its own tests for the different installation methods, + // so we don't need to be exhaustive about them all here. + // (A real client of this API would typically just specify the "direct" + // installation method, which retrieves packages from their origin + // registries.) + InstallationMethods: []*dependencies.BuildProviderPluginCache_Request_InstallMethod{ + { + Source: &dependencies.BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir{ + LocalMirrorDir: "testdata/provider-fs-mirror", + }, + }, + }, + OverridePlatform: "os_arch", + }) + if err != nil { + t.Fatal(err) + } + + seenFakeProvider := false + for { + msg, err := evts.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Fatal(err) // not expecting any errors + } + + // TODO: We're not comprehensively testing all of the events right now + // since we're primarily interested in whether the provider gets + // installed at all, but once clients start depending on the events + // for UI purposes we ought to add more coverage here for the other + // event types. + switch evt := msg.Event.(type) { + case *dependencies.BuildProviderPluginCache_Event_Diagnostic: + t.Errorf("unexpected diagnostic:\n\n%s\n\n%s", evt.Diagnostic.Summary, evt.Diagnostic.Detail) + case *dependencies.BuildProviderPluginCache_Event_FetchComplete_: + if evt.FetchComplete.ProviderVersion.SourceAddr == "example.com/foo/bar" { + seenFakeProvider = true + if got, want := evt.FetchComplete.ProviderVersion.Version, "1.2.3"; got != want { + t.Errorf("wrong provider version\ngot: %s\nwant: %s", got, want) + } + } + } + t.Logf("installation event: %s", msg.String()) + } + + if !seenFakeProvider { + t.Error("no 'fetch complete' event for example.com/foo/bar") + } + + openCacheResp, err := depsClient.OpenProviderPluginCache(ctx, &dependencies.OpenProviderPluginCache_Request{ + CacheDir: cacheDir, + OverridePlatform: "os_arch", + }) + if err != nil { + t.Fatal(err) + } + defer func() { + _, err := depsClient.CloseProviderPluginCache(ctx, &dependencies.CloseProviderPluginCache_Request{ + ProviderCacheHandle: openCacheResp.ProviderCacheHandle, + }) + if err != nil { + t.Error(err) + } + }() + pkgsResp, err := depsClient.GetCachedProviders(ctx, &dependencies.GetCachedProviders_Request{ + ProviderCacheHandle: openCacheResp.ProviderCacheHandle, + }) + if err != nil { + t.Fatal(err) + } + + got := pkgsResp.AvailableProviders + want := []*terraform1.ProviderPackage{ + { + SourceAddr: "example.com/foo/bar", + Version: "1.2.3", + Hashes: []string{ + // This hash is of the fake package directory we installed + // from, under testdata/provider-fs-mirror . + "h1:cAp58lPuOAaPN9ZDdFHx9FxVK2NU0UeObQs2/zld9Lc=", + }, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong providers in cache reported after building\n%s", diff) + } +} + +func TestDependenciesProviderSchema(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + depsServer := newDependenciesServer(handles, disco.New()) + + providersResp, err := depsServer.GetBuiltInProviders(ctx, &dependencies.GetBuiltInProviders_Request{}) + if err != nil { + t.Fatal(err) + } + { + got := providersResp.AvailableProviders + want := []*terraform1.ProviderPackage{ + { + SourceAddr: "terraform.io/builtin/terraform", + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong built-in providers\n%s", diff) + } + } + + schemaResp, err := depsServer.GetProviderSchema(ctx, &dependencies.GetProviderSchema_Request{ + ProviderAddr: "terraform.io/builtin/terraform", + }) + if err != nil { + t.Fatal(err) + } + { + got := schemaResp.Schema + want := &dependencies.ProviderSchema{ + ProviderConfig: &dependencies.Schema{ + Block: &dependencies.Schema_Block{ + // This provider has no configuration arguments + }, + }, + DataResourceTypes: map[string]*dependencies.Schema{ + "terraform_remote_state": &dependencies.Schema{ + Block: &dependencies.Schema_Block{ + Attributes: []*dependencies.Schema_Attribute{ + { + Name: "backend", + Type: []byte(`"string"`), + Required: true, + Description: &dependencies.Schema_DocString{ + Description: "The remote backend to use, e.g. `remote` or `http`.", + Format: dependencies.Schema_DocString_MARKDOWN, + }, + }, + { + Name: "config", + Type: []byte(`"dynamic"`), + Optional: true, + Description: &dependencies.Schema_DocString{ + Description: "The configuration of the remote backend. Although this is optional, most backends require some configuration.\n\nThe object can use any arguments that would be valid in the equivalent `terraform { backend \"\" { ... } }` block.", + Format: dependencies.Schema_DocString_MARKDOWN, + }, + }, + { + Name: "defaults", + Type: []byte(`"dynamic"`), + Optional: true, + Description: &dependencies.Schema_DocString{ + Description: "Default values for outputs, in case the state file is empty or lacks a required output.", + Format: dependencies.Schema_DocString_MARKDOWN, + }, + }, + { + Name: "outputs", + Type: []byte(`"dynamic"`), + Computed: true, + Description: &dependencies.Schema_DocString{ + Description: "An object containing every root-level output in the remote state.", + Format: dependencies.Schema_DocString_MARKDOWN, + }, + }, + { + Name: "workspace", + Type: []byte(`"string"`), + Optional: true, + Description: &dependencies.Schema_DocString{ + Description: "The Terraform workspace to use, if the backend supports workspaces.", + Format: dependencies.Schema_DocString_MARKDOWN, + }, + }, + }, + }, + }, + }, + ManagedResourceTypes: map[string]*dependencies.Schema{ + "terraform_data": &dependencies.Schema{ + Block: &dependencies.Schema_Block{ + Attributes: []*dependencies.Schema_Attribute{ + { + Name: "id", + Type: []byte(`"string"`), + Computed: true, + }, + { + Name: "input", + Type: []byte(`"dynamic"`), + Optional: true, + }, + { + Name: "output", + Type: []byte(`"dynamic"`), + Computed: true, + }, + { + Name: "triggers_replace", + Type: []byte(`"dynamic"`), + Optional: true, + }, + }, + }, + }, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + // NOTE: This is testing against the schema of a real provider + // that can evolve independently of rpcapi. If that provider's + // schema changes in future then it's expected that this test + // will fail, and it's okay to change "want" to match as long as + // it's a correct description of that provider's updated schema. + // + // If this turns out to be a big maintenence burden then we could + // consider some way to include a mock provider, but that would + // add another possible kind of provider into the mix and we'd + // rather avoid that complexity if possible. + t.Errorf("unexpected schema for the built-in 'terraform' provider\n%s", diff) + } + } + +} diff --git a/internal/rpcapi/dynrpcserver/common.go b/internal/rpcapi/dynrpcserver/common.go new file mode 100644 index 0000000000..b46c43e760 --- /dev/null +++ b/internal/rpcapi/dynrpcserver/common.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package dynrpcserver + +import ( + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +const unavailableMsg = "must call Setup.Handshake first" + +var unavailableErr error = status.Error(codes.Unavailable, unavailableMsg) diff --git a/internal/rpcapi/dynrpcserver/dependencies.go b/internal/rpcapi/dynrpcserver/dependencies.go new file mode 100644 index 0000000000..0ccdb5490a --- /dev/null +++ b/internal/rpcapi/dynrpcserver/dependencies.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by ./generator. DO NOT EDIT. +package dynrpcserver + +import ( + "context" + "sync" + + dependencies "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" +) + +type Dependencies struct { + impl dependencies.DependenciesServer + mu sync.RWMutex +} + +var _ dependencies.DependenciesServer = (*Dependencies)(nil) + +func NewDependenciesStub() *Dependencies { + return &Dependencies{} +} + +func (s *Dependencies) BuildProviderPluginCache(a0 *dependencies.BuildProviderPluginCache_Request, a1 dependencies.Dependencies_BuildProviderPluginCacheServer) error { + impl, err := s.realRPCServer() + if err != nil { + return err + } + return impl.BuildProviderPluginCache(a0, a1) +} + +func (s *Dependencies) CloseDependencyLocks(a0 context.Context, a1 *dependencies.CloseDependencyLocks_Request) (*dependencies.CloseDependencyLocks_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CloseDependencyLocks(a0, a1) +} + +func (s *Dependencies) CloseProviderPluginCache(a0 context.Context, a1 *dependencies.CloseProviderPluginCache_Request) (*dependencies.CloseProviderPluginCache_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CloseProviderPluginCache(a0, a1) +} + +func (s *Dependencies) CloseSourceBundle(a0 context.Context, a1 *dependencies.CloseSourceBundle_Request) (*dependencies.CloseSourceBundle_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CloseSourceBundle(a0, a1) +} + +func (s *Dependencies) CreateDependencyLocks(a0 context.Context, a1 *dependencies.CreateDependencyLocks_Request) (*dependencies.CreateDependencyLocks_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CreateDependencyLocks(a0, a1) +} + +func (s *Dependencies) GetBuiltInProviders(a0 context.Context, a1 *dependencies.GetBuiltInProviders_Request) (*dependencies.GetBuiltInProviders_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.GetBuiltInProviders(a0, a1) +} + +func (s *Dependencies) GetCachedProviders(a0 context.Context, a1 *dependencies.GetCachedProviders_Request) (*dependencies.GetCachedProviders_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.GetCachedProviders(a0, a1) +} + +func (s *Dependencies) GetLockedProviderDependencies(a0 context.Context, a1 *dependencies.GetLockedProviderDependencies_Request) (*dependencies.GetLockedProviderDependencies_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.GetLockedProviderDependencies(a0, a1) +} + +func (s *Dependencies) GetProviderSchema(a0 context.Context, a1 *dependencies.GetProviderSchema_Request) (*dependencies.GetProviderSchema_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.GetProviderSchema(a0, a1) +} + +func (s *Dependencies) OpenDependencyLockFile(a0 context.Context, a1 *dependencies.OpenDependencyLockFile_Request) (*dependencies.OpenDependencyLockFile_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.OpenDependencyLockFile(a0, a1) +} + +func (s *Dependencies) OpenProviderPluginCache(a0 context.Context, a1 *dependencies.OpenProviderPluginCache_Request) (*dependencies.OpenProviderPluginCache_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.OpenProviderPluginCache(a0, a1) +} + +func (s *Dependencies) OpenSourceBundle(a0 context.Context, a1 *dependencies.OpenSourceBundle_Request) (*dependencies.OpenSourceBundle_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.OpenSourceBundle(a0, a1) +} + +func (s *Dependencies) ActivateRPCServer(impl dependencies.DependenciesServer) { + s.mu.Lock() + s.impl = impl + s.mu.Unlock() +} + +func (s *Dependencies) realRPCServer() (dependencies.DependenciesServer, error) { + s.mu.RLock() + impl := s.impl + s.mu.RUnlock() + if impl == nil { + return nil, unavailableErr + } + return impl, nil +} diff --git a/internal/rpcapi/dynrpcserver/doc.go b/internal/rpcapi/dynrpcserver/doc.go new file mode 100644 index 0000000000..6aba6d0c82 --- /dev/null +++ b/internal/rpcapi/dynrpcserver/doc.go @@ -0,0 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package dynrpcserver deals with an annoying detail of the rpcapi +// implementation: we need to complete the Setup.Handshake call before we can +// instantiate the remaining services (since their behavior might vary +// depending on negotiated capabilities) but the Go gRPC implementation doesn't +// allow registration of a new service after the gRPC server is already running. +// +// To deal with that we generate forwarding wrappers that initially just +// return errors and then, once a real implementation is provided, just forward +// all requests to the real service. +package dynrpcserver diff --git a/internal/rpcapi/dynrpcserver/generate.go b/internal/rpcapi/dynrpcserver/generate.go new file mode 100644 index 0000000000..3ff8e35bb3 --- /dev/null +++ b/internal/rpcapi/dynrpcserver/generate.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package dynrpcserver + +//go:generate go run github.com/hashicorp/terraform/internal/rpcapi/dynrpcserver/generator diff --git a/internal/rpcapi/dynrpcserver/generator/main.go b/internal/rpcapi/dynrpcserver/generator/main.go new file mode 100644 index 0000000000..9f1631a52e --- /dev/null +++ b/internal/rpcapi/dynrpcserver/generator/main.go @@ -0,0 +1,252 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// This program is the generator for the gRPC service wrapper types in the +// parent directory. It's not suitable for any other use. +// +// This makes various assumptions about how the protobuf compiler and +// gRPC stub generators produce code. If those significantly change in future +// then this will probably break. +package main + +import ( + "bytes" + "fmt" + "go/format" + "go/types" + "log" + "os" + "path/filepath" + "regexp" + "strings" + + "golang.org/x/tools/go/packages" +) + +var protobufPkgs = map[string]string{ + "dependencies": "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies", + "stacks": "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks", + "packages": "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages", +} + +func main() { + for shortName, pkgName := range protobufPkgs { + cfg := &packages.Config{ + Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles, + } + pkgs, err := packages.Load(cfg, pkgName) + if err != nil { + log.Fatalf("can't load the protobuf/gRPC proxy package: %s", err) + } + if len(pkgs) != 1 { + log.Fatalf("wrong number of packages found") + } + pkg := pkgs[0] + if pkg.TypesInfo == nil { + log.Fatalf("types info not available") + } + if len(pkg.GoFiles) < 1 { + log.Fatalf("no files included in package") + } + + // We assume that our output directory is sibling to the directory + // containing the protobuf specification. + outDir := filepath.Join(filepath.Dir(pkg.GoFiles[0]), "../../dynrpcserver") + + Types: + for _, obj := range pkg.TypesInfo.Defs { + typ, ok := obj.(*types.TypeName) + if !ok { + continue + } + underTyp := typ.Type().Underlying() + iface, ok := underTyp.(*types.Interface) + if !ok { + continue + } + if !strings.HasSuffix(typ.Name(), "Server") || typ.Name() == "SetupServer" { + // Doesn't look like a generated gRPC server interface + continue + } + + // The interfaces used for streaming requests/responses unfortunately + // also have a "Server" suffix in the generated Go code, and so + // we need to detect those more surgically by noticing that they + // have grpc.ServerStream embedded inside. + for i := 0; i < iface.NumEmbeddeds(); i++ { + emb, ok := iface.EmbeddedType(i).(*types.Named) + if !ok { + continue + } + pkg := emb.Obj().Pkg().Path() + name := emb.Obj().Name() + if pkg == "google.golang.org/grpc" && name == "ServerStream" { + continue Types + } + } + + // If we get here then what we're holding _seems_ to be a gRPC + // server interface, and so we'll generate a dynamic initialization + // wrapper for it. + + ifaceName := typ.Name() + baseName := strings.TrimSuffix(ifaceName, "Server") + filename := toFilenameCase(baseName) + ".go" + absFilename := filepath.Join(outDir, filename) + + var buf bytes.Buffer + + fmt.Fprintf(&buf, `// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by ./generator. DO NOT EDIT. +`) + + fmt.Fprintf(&buf, `package dynrpcserver + + import ( + "context" + "sync" + + %s %q + ) + + `, shortName, pkg) + fmt.Fprintf(&buf, "type %s struct {\n", baseName) + fmt.Fprintf(&buf, "impl %s.%s\n", shortName, ifaceName) + fmt.Fprintln(&buf, "mu sync.RWMutex") + buf.WriteString("}\n\n") + + fmt.Fprintf(&buf, "var _ %s.%s = (*%s)(nil)\n\n", shortName, ifaceName, baseName) + + fmt.Fprintf(&buf, "func New%sStub() *%s {\n", baseName, baseName) + fmt.Fprintf(&buf, "return &%s{}\n", baseName) + fmt.Fprintf(&buf, "}\n\n") + + for i := 0; i < iface.NumMethods(); i++ { + method := iface.Method(i) + sig := method.Type().(*types.Signature) + + fmt.Fprintf(&buf, "func (s *%s) %s(", baseName, method.Name()) + for i := 0; i < sig.Params().Len(); i++ { + param := sig.Params().At(i) + + // The generated interface types don't include parameter names + // and so we just use synthetic parameter names here. + name := fmt.Sprintf("a%d", i) + genType := typeRef(param.Type().String(), shortName, pkgName) + + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(name) + buf.WriteString(" ") + buf.WriteString(genType) + } + fmt.Fprintf(&buf, ")") + if sig.Results().Len() > 1 { + buf.WriteString("(") + } + for i := 0; i < sig.Results().Len(); i++ { + result := sig.Results().At(i) + genType := typeRef(result.Type().String(), shortName, pkgName) + if i > 0 { + buf.WriteString(", ") + } + buf.WriteString(genType) + } + if sig.Results().Len() > 1 { + buf.WriteString(")") + } + switch n := sig.Results().Len(); n { + case 1: + fmt.Fprintf(&buf, ` { + impl, err := s.realRPCServer() + if err != nil { + return err + } + `) + case 2: + fmt.Fprintf(&buf, ` { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + `) + default: + log.Fatalf("don't know how to make a stub for method with %d results", n) + } + fmt.Fprintf(&buf, "return impl.%s(", method.Name()) + for i := 0; i < sig.Params().Len(); i++ { + if i > 0 { + buf.WriteString(", ") + } + fmt.Fprintf(&buf, "a%d", i) + } + fmt.Fprintf(&buf, ")\n}\n\n") + } + + fmt.Fprintf(&buf, ` + func (s *%s) ActivateRPCServer(impl %s.%s) { + s.mu.Lock() + s.impl = impl + s.mu.Unlock() + } + + func (s *%s) realRPCServer() (%s.%s, error) { + s.mu.RLock() + impl := s.impl + s.mu.RUnlock() + if impl == nil { + return nil, unavailableErr + } + return impl, nil + } + `, baseName, shortName, ifaceName, baseName, shortName, ifaceName) + + src, err := format.Source(buf.Bytes()) + if err != nil { + //log.Fatalf("formatting %s: %s", filename, err) + src = buf.Bytes() + } + f, err := os.Create(absFilename) + if err != nil { + log.Fatal(err) + } + _, err = f.Write(src) + if err != nil { + log.Fatalf("writing %s: %s", filename, err) + } + + } + } +} + +func typeRef(fullType, name, pkg string) string { + // The following is specialized to only the parameter types + // we typically expect to see in a server interface. This + // might need extra rules if we step outside the design idiom + // we've used for these services so far. + switch { + case fullType == "context.Context" || fullType == "error": + return fullType + case fullType == "interface{}" || fullType == "any": + return "any" + case strings.HasPrefix(fullType, "*"+pkg+"."): + return "*" + name + "." + fullType[len(pkg)+2:] + case strings.HasPrefix(fullType, pkg+"."): + return name + "." + fullType[len(pkg)+1:] + default: + log.Fatalf("don't know what to do with parameter type %s", fullType) + return "" + } +} + +var firstCapPattern = regexp.MustCompile("(.)([A-Z][a-z]+)") +var otherCapPattern = regexp.MustCompile("([a-z0-9])([A-Z])") + +func toFilenameCase(typeName string) string { + ret := firstCapPattern.ReplaceAllString(typeName, "${1}_${2}") + ret = otherCapPattern.ReplaceAllString(ret, "${1}_${2}") + return strings.ToLower(ret) +} diff --git a/internal/rpcapi/dynrpcserver/packages.go b/internal/rpcapi/dynrpcserver/packages.go new file mode 100644 index 0000000000..da3e72fc17 --- /dev/null +++ b/internal/rpcapi/dynrpcserver/packages.go @@ -0,0 +1,79 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by ./generator. DO NOT EDIT. +package dynrpcserver + +import ( + "context" + "sync" + + packages "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages" +) + +type Packages struct { + impl packages.PackagesServer + mu sync.RWMutex +} + +var _ packages.PackagesServer = (*Packages)(nil) + +func NewPackagesStub() *Packages { + return &Packages{} +} + +func (s *Packages) FetchModulePackage(a0 context.Context, a1 *packages.FetchModulePackage_Request) (*packages.FetchModulePackage_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.FetchModulePackage(a0, a1) +} + +func (s *Packages) FetchProviderPackage(a0 context.Context, a1 *packages.FetchProviderPackage_Request) (*packages.FetchProviderPackage_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.FetchProviderPackage(a0, a1) +} + +func (s *Packages) ModulePackageSourceAddr(a0 context.Context, a1 *packages.ModulePackageSourceAddr_Request) (*packages.ModulePackageSourceAddr_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.ModulePackageSourceAddr(a0, a1) +} + +func (s *Packages) ModulePackageVersions(a0 context.Context, a1 *packages.ModulePackageVersions_Request) (*packages.ModulePackageVersions_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.ModulePackageVersions(a0, a1) +} + +func (s *Packages) ProviderPackageVersions(a0 context.Context, a1 *packages.ProviderPackageVersions_Request) (*packages.ProviderPackageVersions_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.ProviderPackageVersions(a0, a1) +} + +func (s *Packages) ActivateRPCServer(impl packages.PackagesServer) { + s.mu.Lock() + s.impl = impl + s.mu.Unlock() +} + +func (s *Packages) realRPCServer() (packages.PackagesServer, error) { + s.mu.RLock() + impl := s.impl + s.mu.RUnlock() + if impl == nil { + return nil, unavailableErr + } + return impl, nil +} diff --git a/internal/rpcapi/dynrpcserver/stacks.go b/internal/rpcapi/dynrpcserver/stacks.go new file mode 100644 index 0000000000..08cca1fd3c --- /dev/null +++ b/internal/rpcapi/dynrpcserver/stacks.go @@ -0,0 +1,167 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by ./generator. DO NOT EDIT. +package dynrpcserver + +import ( + "context" + "sync" + + stacks "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" +) + +type Stacks struct { + impl stacks.StacksServer + mu sync.RWMutex +} + +var _ stacks.StacksServer = (*Stacks)(nil) + +func NewStacksStub() *Stacks { + return &Stacks{} +} + +func (s *Stacks) ApplyStackChanges(a0 *stacks.ApplyStackChanges_Request, a1 stacks.Stacks_ApplyStackChangesServer) error { + impl, err := s.realRPCServer() + if err != nil { + return err + } + return impl.ApplyStackChanges(a0, a1) +} + +func (s *Stacks) ClosePlan(a0 context.Context, a1 *stacks.CloseStackPlan_Request) (*stacks.CloseStackPlan_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.ClosePlan(a0, a1) +} + +func (s *Stacks) CloseStackConfiguration(a0 context.Context, a1 *stacks.CloseStackConfiguration_Request) (*stacks.CloseStackConfiguration_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CloseStackConfiguration(a0, a1) +} + +func (s *Stacks) CloseState(a0 context.Context, a1 *stacks.CloseStackState_Request) (*stacks.CloseStackState_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CloseState(a0, a1) +} + +func (s *Stacks) CloseTerraformState(a0 context.Context, a1 *stacks.CloseTerraformState_Request) (*stacks.CloseTerraformState_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.CloseTerraformState(a0, a1) +} + +func (s *Stacks) FindStackConfigurationComponents(a0 context.Context, a1 *stacks.FindStackConfigurationComponents_Request) (*stacks.FindStackConfigurationComponents_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.FindStackConfigurationComponents(a0, a1) +} + +func (s *Stacks) InspectExpressionResult(a0 context.Context, a1 *stacks.InspectExpressionResult_Request) (*stacks.InspectExpressionResult_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.InspectExpressionResult(a0, a1) +} + +func (s *Stacks) ListResourceIdentities(a0 context.Context, a1 *stacks.ListResourceIdentities_Request) (*stacks.ListResourceIdentities_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.ListResourceIdentities(a0, a1) +} + +func (s *Stacks) MigrateTerraformState(a0 *stacks.MigrateTerraformState_Request, a1 stacks.Stacks_MigrateTerraformStateServer) error { + impl, err := s.realRPCServer() + if err != nil { + return err + } + return impl.MigrateTerraformState(a0, a1) +} + +func (s *Stacks) OpenPlan(a0 stacks.Stacks_OpenPlanServer) error { + impl, err := s.realRPCServer() + if err != nil { + return err + } + return impl.OpenPlan(a0) +} + +func (s *Stacks) OpenStackConfiguration(a0 context.Context, a1 *stacks.OpenStackConfiguration_Request) (*stacks.OpenStackConfiguration_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.OpenStackConfiguration(a0, a1) +} + +func (s *Stacks) OpenStackInspector(a0 context.Context, a1 *stacks.OpenStackInspector_Request) (*stacks.OpenStackInspector_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.OpenStackInspector(a0, a1) +} + +func (s *Stacks) OpenState(a0 stacks.Stacks_OpenStateServer) error { + impl, err := s.realRPCServer() + if err != nil { + return err + } + return impl.OpenState(a0) +} + +func (s *Stacks) OpenTerraformState(a0 context.Context, a1 *stacks.OpenTerraformState_Request) (*stacks.OpenTerraformState_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.OpenTerraformState(a0, a1) +} + +func (s *Stacks) PlanStackChanges(a0 *stacks.PlanStackChanges_Request, a1 stacks.Stacks_PlanStackChangesServer) error { + impl, err := s.realRPCServer() + if err != nil { + return err + } + return impl.PlanStackChanges(a0, a1) +} + +func (s *Stacks) ValidateStackConfiguration(a0 context.Context, a1 *stacks.ValidateStackConfiguration_Request) (*stacks.ValidateStackConfiguration_Response, error) { + impl, err := s.realRPCServer() + if err != nil { + return nil, err + } + return impl.ValidateStackConfiguration(a0, a1) +} + +func (s *Stacks) ActivateRPCServer(impl stacks.StacksServer) { + s.mu.Lock() + s.impl = impl + s.mu.Unlock() +} + +func (s *Stacks) realRPCServer() (stacks.StacksServer, error) { + s.mu.RLock() + impl := s.impl + s.mu.RUnlock() + if impl == nil { + return nil, unavailableErr + } + return impl, nil +} diff --git a/internal/rpcapi/grpc_helpers.go b/internal/rpcapi/grpc_helpers.go new file mode 100644 index 0000000000..64b6a8a1f6 --- /dev/null +++ b/internal/rpcapi/grpc_helpers.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "sync" + + "google.golang.org/protobuf/proto" +) + +// This interface should match the interfaces that grpc-gen-go tends to +// generate for a the server side of RPC function which produces streaming +// results with a particular message type. +type grpcServerStreamingSender[Message proto.Message] interface { + Send(Message) error +} + +// syncStreamingRPCSender is a wrapper around a generated gprc.ServerStream +// wrapper that makes Send calls concurrency-safe by holding a mutex throughout +// each call to the underlying Send. +// +// Instantiate this using [newSyncStreamingRPCSender] so you can rely on +// type inference to avoid writing out the type parameters explicitly. +// Consider declaring a type alias with specific Server and Message types if +// you need to name an instantiation of this generic type, so you'll only have +// to write the long-winded instantiation expression once and can use a more +// intuitive name elsewhere. +type syncStreamingRPCSender[ + Server grpcServerStreamingSender[Message], + Message proto.Message, +] struct { + wrapped Server + mu sync.Mutex +} + +// newSyncStreamingRPCSender wraps an interface value implementing an interface +// generated for the server side of a streaming RPC response and makes its +// Send method concurrency-safe, by holding a mutex throughout the call to +// the underlying Send. +func newSyncStreamingRPCSender[ + Server grpcServerStreamingSender[Message], + Message proto.Message, +](wrapped Server) *syncStreamingRPCSender[Server, Message] { + return &syncStreamingRPCSender[Server, Message]{ + wrapped: wrapped, + } +} + +// Send holds a mutex while calling Send on the wrapped server, and then +// returns its error value. +func (s *syncStreamingRPCSender[Server, Message]) Send(msg Message) error { + s.mu.Lock() + err := s.wrapped.Send(msg) + s.mu.Unlock() + return err +} diff --git a/internal/rpcapi/grpc_testing.go b/internal/rpcapi/grpc_testing.go new file mode 100644 index 0000000000..4bd8fba05a --- /dev/null +++ b/internal/rpcapi/grpc_testing.go @@ -0,0 +1,137 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "encoding/json" + "net" + "testing" + + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate" +) + +// grpcClientForTesting creates an in-memory-only gRPC server, offers the +// caller a chance to register services with it, and then returns a +// client connected to that fake server, with which the caller can construct +// service-specific client objects. +// +// When finished with the returned client, call the close callback given as +// the second return value or else you will leak some goroutines handling the +// server end of this fake connection. +func grpcClientForTesting(ctx context.Context, t *testing.T, registerServices func(srv *grpc.Server)) (conn grpc.ClientConnInterface, close func()) { + fakeListener := bufconn.Listen(1024 /* buffer size */) + srv := grpc.NewServer( + grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()), + grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()), + ) + + // Caller gets an opportunity to register specific services before + // we actually start "serving". + registerServices(srv) + + go func() { + if err := srv.Serve(fakeListener); err != nil { + // We can't actually return an error here, but this should + // not arise with our fake listener anyway so we'll just panic. + panic(err) + } + }() + + fakeDialer := func(ctx context.Context, fakeAddr string) (net.Conn, error) { + return fakeListener.DialContext(ctx) + } + realConn, err := grpc.DialContext( + ctx, "testfake", + grpc.WithContextDialer(fakeDialer), + grpc.WithInsecure(), + grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()), + grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()), + ) + if err != nil { + t.Fatalf("failed to connect to the fake server: %s", err) + } + + return realConn, func() { + realConn.Close() + srv.Stop() + fakeListener.Close() + } +} + +func appliedChangeToRawState(t *testing.T, changes []stackstate.AppliedChange) map[string]*anypb.Any { + ret := make(map[string]*anypb.Any) + for _, change := range changes { + raw, err := change.AppliedChangeProto() + if err != nil { + t.Fatalf("failed to marshal change to proto: %s", err) + } + for _, raw := range raw.Raw { + ret[raw.Key] = raw.Value + } + } + return ret +} + +func mustDefaultRootProvider(provider string) addrs.AbsProviderConfig { + return addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider(provider), + } +} + +func mustAbsComponentInstance(t *testing.T, addr string) stackaddrs.AbsComponentInstance { + ret, diags := stackaddrs.ParseAbsComponentInstanceStr(addr) + if len(diags) > 0 { + t.Fatalf("failed to parse component instance address %q: %s", addr, diags) + } + return ret +} + +func mustAbsComponent(t *testing.T, addr string) stackaddrs.AbsComponent { + ret, diags := stackaddrs.ParseAbsComponentInstanceStr(addr) + if len(diags) > 0 { + t.Fatalf("failed to parse component instance address %q: %s", addr, diags) + } + if ret.Item.Key != addrs.NoKey { + t.Fatalf("expected component address %q to have no key, but got %q", addr, ret.Item.Key) + } + return stackaddrs.AbsComponent{ + Stack: ret.Stack, + Item: ret.Item.Component, + } +} + +func mustAbsResourceInstanceObject(t *testing.T, addr string) stackaddrs.AbsResourceInstanceObject { + ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr) + if len(diags) > 0 { + t.Fatalf("failed to parse resource instance object address %q: %s", addr, diags) + } + return ret +} + +func mustMarshalAnyPb(msg proto.Message) *anypb.Any { + var ret anypb.Any + err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) + if err != nil { + panic(err) + } + return &ret +} + +func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte { + jsonAttrs, err := json.Marshal(attrs) + if err != nil { + panic(err) + } + return jsonAttrs +} diff --git a/internal/rpcapi/handles.go b/internal/rpcapi/handles.go new file mode 100644 index 0000000000..d901d83bd2 --- /dev/null +++ b/internal/rpcapi/handles.go @@ -0,0 +1,307 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "fmt" + "sync" + + "github.com/hashicorp/go-slug/sourcebundle" + + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/providercache" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" +) + +// handle represents an identifier shared between client and server to identify +// a particular object. +// +// From the server's perspective, these are always issued by and tracked within +// a [handleTable]. The conventional variable name for a handle in this codebase +// is "hnd", or a name with "Hnd" as a suffix such as "planHnd". +type handle[T any] int64 + +// ForProtobuf returns the handle as a naked int64 for use in a protocol buffers +// message. This erases the information about what type of object this handle +// was for, so should be used only when preparing a protobuf response and not +// for anything internal to this package. +func (hnd handle[T]) ForProtobuf() int64 { + return int64(hnd) +} + +// IsNil returns true if the reciever is the "nil handle", which is also the +// zero value of any handle type and represents the absense of a handle. +func (hnd handle[T]) IsNil() bool { + return int64(hnd) == 0 +} + +// handleTable is our shared table of "handles", which are really just integers +// that clients can use to refer to an object they've previously opened. +// +// In our public API contract each different object has a separate numberspace +// of handles, but as an implementation detail we share a single numberspace +// for everything just because that means that if a client gets their handles +// mixed up then it'll fail with an error rather than potentially doing +// something unexpected to an unrelated object, since these all appear as the +// same type in the protobuf-generated API. +// +// The handleTable API intentionally requires callers to be explicit about +// what kind of handle they are intending to work with. There is no function +// to query which kind of object is associated with a handle, because if +// a particular operation can accept handles of multiple types it should +// separate those into separate request fields and decide how to interpret +// the handles based on which fields are populated by the caller. +type handleTable struct { + handleObjs map[int64]any + nextHandle int64 + + // handleDeps tracks dependencies between handles to disallow closing + // a handle that has another open handle depending on it. For example, + // each stack configuration handle depends on a source bundle handle + // because closing the source bundle and deleting its underlying directory + // would cause unwanted misbehavior for future operations against that + // stack configuration. + // + // The first level of map is the object being depended on, and the second + // level are the objects doing the depending. + handleDeps map[int64]map[int64]struct{} + + // TODO: Consider also tracking when a particular handle is being actively + // used by a running RPC operation, so that we can return an error if + // a caller tries to close a handle concurrently with an active operation. + // That would be a weird thing to do though and always a bug in the caller, + // so for now we're just letting it cause unspecified behavior for + // simplicity's sake. + + mu sync.Mutex +} + +func newHandleTable() *handleTable { + return &handleTable{ + handleObjs: make(map[int64]any), + handleDeps: make(map[int64]map[int64]struct{}), + nextHandle: 1, + } +} + +func (t *handleTable) NewSourceBundle(sources *sourcebundle.Bundle) handle[*sourcebundle.Bundle] { + return newHandle(t, sources) +} + +func (t *handleTable) SourceBundle(hnd handle[*sourcebundle.Bundle]) *sourcebundle.Bundle { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseSourceBundle(hnd handle[*sourcebundle.Bundle]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewStackConfig(cfg *stackconfig.Config, owningSourceBundle handle[*sourcebundle.Bundle]) (handle[*stackconfig.Config], error) { + return newHandleWithDependency(t, cfg, owningSourceBundle) +} + +func (t *handleTable) StackConfig(hnd handle[*stackconfig.Config]) *stackconfig.Config { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseStackConfig(hnd handle[*stackconfig.Config]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewStackState(state *stackstate.State) handle[*stackstate.State] { + return newHandle(t, state) +} + +func (t *handleTable) StackState(hnd handle[*stackstate.State]) *stackstate.State { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseStackState(hnd handle[*stackstate.State]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewStackPlan(state *stackplan.Plan) handle[*stackplan.Plan] { + return newHandle(t, state) +} + +func (t *handleTable) StackPlan(hnd handle[*stackplan.Plan]) *stackplan.Plan { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseStackPlan(hnd handle[*stackplan.Plan]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewTerraformState(state *states.State) handle[*states.State] { + return newHandle(t, state) +} + +func (t *handleTable) TerraformState(hnd handle[*states.State]) *states.State { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseTerraformState(hnd handle[*states.State]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewDependencyLocks(locks *depsfile.Locks) handle[*depsfile.Locks] { + // NOTE: We intentionally don't track a dependency on a source bundle + // here for two reasons: + // - Not all lock objects necessarily original from lock files. For example, + // we could be creating a new empty locks that will be mutated and then + // saved to disk for the first time afterwards. + // - The locks object in memory is not dependent on the lock file it was + // loaded from once the load is complete. Closing the source bundle and + // deleting its directory would not affect the validity of the locks + // object. + return newHandle(t, locks) +} + +func (t *handleTable) DependencyLocks(hnd handle[*depsfile.Locks]) *depsfile.Locks { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseDependencyLocks(hnd handle[*depsfile.Locks]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewProviderPluginCache(dir *providercache.Dir) handle[*providercache.Dir] { + return newHandle(t, dir) +} + +func (t *handleTable) ProviderPluginCache(hnd handle[*providercache.Dir]) *providercache.Dir { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseProviderPluginCache(hnd handle[*providercache.Dir]) error { + return closeHandle(t, hnd) +} + +func (t *handleTable) NewStackInspector(dir *stacksInspector) handle[*stacksInspector] { + return newHandle(t, dir) +} + +func (t *handleTable) StackInspector(hnd handle[*stacksInspector]) *stacksInspector { + ret, _ := readHandle(t, hnd) // non-existent or invalid returns nil + return ret +} + +func (t *handleTable) CloseStackInspector(hnd handle[*stacksInspector]) error { + return closeHandle(t, hnd) +} + +func newHandle[ObjT any](t *handleTable, obj ObjT) handle[ObjT] { + t.mu.Lock() + hnd := t.nextHandle + t.nextHandle++ // NOTE: We're assuming int64 is big enough for overflow to be impractical + t.handleObjs[hnd] = obj + t.mu.Unlock() + return handle[ObjT](hnd) +} + +// newHandleWithDependency is a variant of newHandle which also records a +// dependency on some other handle. +// +// Unlike newHandle, this is fallible because creating the new handle might +// race with closing the dependency handle, causing that handle to no longer +// be available by the time this function is running. In that case, the +// returned error will be [newHandleErrorNoParent]. +func newHandleWithDependency[ObjT, DepT any](t *handleTable, obj ObjT, dep handle[DepT]) (handle[ObjT], error) { + t.mu.Lock() + if depObjectI, exists := t.handleObjs[int64(dep)]; !exists { + return handle[ObjT](0), newHandleErrorNoParent + } else if depObject, ok := depObjectI.(DepT); !ok { + // It's caller's responsibility to ensure that it's passing in valid handles. + // (This will typically be ensured by our type-safe wrapper methods) + panic(fmt.Sprintf("dependency handle %d is %T, not %T", int64(dep), depObjectI, depObject)) + } + hnd := t.nextHandle + t.nextHandle++ // NOTE: We're assuming int64 is big enough for overflow to be impractical + t.handleObjs[hnd] = obj + if _, exists := t.handleDeps[int64(dep)]; !exists { + t.handleDeps[int64(dep)] = make(map[int64]struct{}) + } + t.handleDeps[int64(dep)][hnd] = struct{}{} + t.mu.Unlock() + return handle[ObjT](hnd), nil +} + +func readHandle[ObjT any](t *handleTable, hnd handle[ObjT]) (ObjT, bool) { + t.mu.Lock() + defer t.mu.Unlock() + var zero ObjT + if existing, exists := t.handleObjs[int64(hnd)]; !exists { + return zero, false + } else if existing, ok := existing.(ObjT); !ok { + return zero, false + } else { + return existing, true + } +} + +func closeHandle[ObjT any](t *handleTable, hnd handle[ObjT]) error { + t.mu.Lock() + defer t.mu.Unlock() + if existing, exists := t.handleObjs[int64(hnd)]; !exists { + return closeHandleErrorUnknown + } else if _, ok := existing.(ObjT); !ok { + return closeHandleErrorUnknown + } + if len(t.handleDeps[int64(hnd)]) > 0 { + return closeHandleErrorBlocked + } + delete(t.handleObjs, int64(hnd)) + // We'll also revoke this object's dependencies so that they + // can potentially be closed after we return. Our dependency-tracking + // data structure is not optimized for deleting because that's rare + // in comparison to adding and checking, but we expect the handle + // table to typically be small enough for this full scan not to hurt. + for _, m := range t.handleDeps { + delete(m, int64(hnd)) // no-op if not present + } + return nil +} + +type newHandleError rune + +const ( + newHandleErrorNoParent newHandleError = '^' +) + +func (err newHandleError) Error() string { + switch err { + case newHandleErrorNoParent: + return "parent handle does not exist" + default: + return "unknown error creating handle" + } +} + +type closeHandleError rune + +const ( + closeHandleErrorUnknown closeHandleError = '?' + closeHandleErrorBlocked closeHandleError = 'B' +) + +func (err closeHandleError) Error() string { + switch err { + case closeHandleErrorUnknown: + return "unknown handle" + case closeHandleErrorBlocked: + return "handle is in use by another open handle" + default: + return "unknown error closing handle" + } +} diff --git a/internal/rpcapi/internal_client.go b/internal/rpcapi/internal_client.go new file mode 100644 index 0000000000..66e8978e07 --- /dev/null +++ b/internal/rpcapi/internal_client.go @@ -0,0 +1,132 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "fmt" + "net" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" +) + +// Client is a client for the RPC API. +// +// This just wraps a raw gRPC client connection and provides a more convenient +// API to access its services. +type Client struct { + // conn should be a connection to a server that has already completed + // the Setup.Handshake call. + conn *grpc.ClientConn + // serverCaps should be from the result of the Setup.Handshake call + // previously made to the server that conn is connected to. + serverCaps *setup.ServerCapabilities + + close func(context.Context) error +} + +// NewInternalClient returns a client for the RPC API that uses in-memory +// buffers to allow callers within the same Terraform CLI process to access +// the RPC API without any sockets or child processes. +// +// This is intended for exposing Terraform Core functionality through Terraform +// CLI, to establish an explicit interface between those two sides without +// the overhead of forking a child process containing exactly the same code. +// +// Callers should call the Close method of the returned client once they are +// done using it, or else they will leak goroutines. +func NewInternalClient(ctx context.Context, clientCaps *setup.ClientCapabilities) (*Client, error) { + fakeListener := bufconn.Listen(4 * 1024 * 1024 /* buffer size */) + srv := grpc.NewServer() + registerGRPCServices(srv, &serviceOpts{}) + + go func() { + if err := srv.Serve(fakeListener); err != nil { + // We can't actually return an error here, but this should + // not arise with our fake listener anyway so we'll just panic. + panic(err) + } + }() + + fakeDialer := func(ctx context.Context, fakeAddr string) (net.Conn, error) { + return fakeListener.DialContext(ctx) + } + clientConn, err := grpc.DialContext( + ctx, "testfake", + grpc.WithContextDialer(fakeDialer), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + return nil, fmt.Errorf("failed to connect to RPC API: %w", err) + } + + // We perform the setup step on the caller's behalf, so that they can + // immediately use the main services. (The caller would otherwise need + // to do this immediately on return anyway, or the result would be + // useless.) + setupClient := setup.NewSetupClient(clientConn) + setupResp, err := setupClient.Handshake(ctx, &setup.Handshake_Request{ + Capabilities: clientCaps, + }) + if err != nil { + return nil, fmt.Errorf("setup failed: %w", err) + } + + var client *Client + client = &Client{ + conn: clientConn, + serverCaps: setupResp.Capabilities, + close: func(ctx context.Context) error { + clientConn.Close() + srv.Stop() + fakeListener.Close() + client.conn = nil + client.serverCaps = nil + client.close = func(context.Context) error { + return nil + } + return nil + }, + } + + return client, nil +} + +// Close frees the internal buffers and terminates the goroutines that handle +// the internal RPC API connection. +// +// Any service clients previously returned by other methods become invalid +// as soon as this method is called, and must not be used any further. +func (c *Client) Close(ctx context.Context) error { + return c.close(ctx) +} + +// ServerCapabilities returns the server's response to capability negotiation. +// +// Callers must not modify anything reachable through the returned pointer. +func (c *Client) ServerCapabilities() *setup.ServerCapabilities { + return c.serverCaps +} + +// Dependencies returns a client for the Dependencies service of the RPC API. +func (c *Client) Dependencies() dependencies.DependenciesClient { + return dependencies.NewDependenciesClient(c.conn) +} + +// Packages returns a client for the Packages service of the RPC API. +func (c *Client) Packages() packages.PackagesClient { + return packages.NewPackagesClient(c.conn) +} + +// Stacks returns a client for the Stacks service of the RPC API. +func (c *Client) Stacks() stacks.StacksClient { + return stacks.NewStacksClient(c.conn) +} diff --git a/internal/rpcapi/internal_client_test.go b/internal/rpcapi/internal_client_test.go new file mode 100644 index 0000000000..00b12f64da --- /dev/null +++ b/internal/rpcapi/internal_client_test.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi_test + +import ( + "context" + "testing" + + "github.com/davecgh/go-spew/spew" + + "github.com/hashicorp/terraform/internal/rpcapi" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup" +) + +func TestInternalClientOpenClose(t *testing.T) { + ctx := context.Background() + client, err := rpcapi.NewInternalClient(ctx, &setup.ClientCapabilities{}) + if err != nil { + t.Error(err) + } + + t.Logf("server capabilities: %s", spew.Sdump(client.ServerCapabilities())) + + err = client.Close(ctx) + if err != nil { + t.Error(err) + } +} diff --git a/internal/rpcapi/packages.go b/internal/rpcapi/packages.go new file mode 100644 index 0000000000..dac2d2bcc3 --- /dev/null +++ b/internal/rpcapi/packages.go @@ -0,0 +1,257 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "fmt" + "strings" + + "github.com/apparentlymart/go-versions/versions" + "github.com/hashicorp/terraform-svchost/disco" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getmodules" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/providercache" + "github.com/hashicorp/terraform/internal/registry" + "github.com/hashicorp/terraform/internal/registry/regsrc" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages" +) + +var _ packages.PackagesServer = (*packagesServer)(nil) + +func newPackagesServer(services *disco.Disco) *packagesServer { + return &packagesServer{ + services: services, + + // This function lets us control the provider source during tests. + providerSourceFn: func(services *disco.Disco) getproviders.Source { + // TODO: Implement loading from alternate sources like network or filesystem + // mirrors. + return getproviders.NewRegistrySource(services) + }, + } +} + +type providerSourceFn func(services *disco.Disco) getproviders.Source + +type packagesServer struct { + packages.UnimplementedPackagesServer + + services *disco.Disco + providerSourceFn providerSourceFn +} + +func (p *packagesServer) ProviderPackageVersions(ctx context.Context, request *packages.ProviderPackageVersions_Request) (*packages.ProviderPackageVersions_Response, error) { + response := new(packages.ProviderPackageVersions_Response) + + source := p.providerSourceFn(p.services) + provider, diags := addrs.ParseProviderSourceString(request.SourceAddr) + response.Diagnostics = append(response.Diagnostics, diagnosticsToProto(diags)...) + if diags.HasErrors() { + return response, nil + } + + versions, warnings, err := source.AvailableVersions(ctx, provider) + + displayWarnings := make([]string, len(warnings)) + for ix, warning := range warnings { + displayWarnings[ix] = fmt.Sprintf("- %s", warning) + } + if len(displayWarnings) > 0 { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_WARNING, + Summary: "Additional provider information from registry", + Detail: fmt.Sprintf("The remote registry returned warnings for %s:\n%s", provider.ForDisplay(), strings.Join(displayWarnings, "\n")), + }) + } + + if err != nil { + // TODO: Parse the different error types so we can provide specific + // error diagnostics, see commands/init.go:621. + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to query available provider packages", + Detail: fmt.Sprintf("Could not retrieve the list of available versions for provider %s: %s.", provider.ForDisplay(), err), + }) + return response, nil + } + + for _, version := range versions { + response.Versions = append(response.Versions, version.String()) + } + return response, nil +} + +func (p *packagesServer) FetchProviderPackage(ctx context.Context, request *packages.FetchProviderPackage_Request) (*packages.FetchProviderPackage_Response, error) { + + response := new(packages.FetchProviderPackage_Response) + + version, err := versions.ParseVersion(request.Version) + if err != nil { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Invalid platform", + Detail: fmt.Sprintf("The requested version %s is invalid: %s.", request.Version, err), + }) + return response, nil + } + + source := p.providerSourceFn(p.services) + provider, diags := addrs.ParseProviderSourceString(request.SourceAddr) + response.Diagnostics = append(response.Diagnostics, diagnosticsToProto(diags)...) + if diags.HasErrors() { + return response, nil + } + + var allowedHashes []getproviders.Hash + for _, hash := range request.Hashes { + allowedHashes = append(allowedHashes, getproviders.Hash(hash)) + } + + for _, requestPlatform := range request.Platforms { + result := new(packages.FetchProviderPackage_PlatformResult) + response.Results = append(response.Results, result) + + platform, err := getproviders.ParsePlatform(requestPlatform) + if err != nil { + result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Invalid platform", + Detail: fmt.Sprintf("The requested platform %s is invalid: %s.", requestPlatform, err), + }) + continue + } + + meta, err := source.PackageMeta(ctx, provider, version, platform) + if err != nil { + // TODO: Parse the different error types so we can provide specific + // error diagnostics, see commands/init.go:731. + result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to query provider package metadata", + Detail: fmt.Sprintf("Could not retrieve package metadata for provider %s@%s for %s: %s.", provider.ForDisplay(), version.String(), platform.String(), err), + }) + continue + } + + into := providercache.NewDirWithPlatform(request.CacheDir, platform) + authResult, err := into.InstallPackage(ctx, meta, allowedHashes) + if err != nil { + // TODO: Parse the different error types so we can provide specific + // error diagnostics, see commands/init.go:731. + result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to download provider package", + Detail: fmt.Sprintf("Could not download provider %s@%s for %s: %s.", provider.ForDisplay(), version.String(), platform.String(), err), + }) + continue + } + + var hashes []string + if authResult.SignedByAnyParty() { + for _, hash := range meta.AcceptableHashes() { + hashes = append(hashes, string(hash)) + } + } + + providerPackage := into.ProviderVersion(provider, version) + hash, err := providerPackage.Hash() + if err != nil { + result.Diagnostics = append(result.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to hash provider package", + Detail: fmt.Sprintf("Could not hash provider %s@%s for %s: %s.", provider.ForDisplay(), version.String(), platform.String(), err), + }) + continue + } + hashes = append(hashes, string(hash)) + result.Provider = &terraform1.ProviderPackage{ + SourceAddr: request.SourceAddr, + Version: request.Version, + Hashes: hashes, + } + } + + return response, nil +} + +func (p *packagesServer) ModulePackageVersions(ctx context.Context, request *packages.ModulePackageVersions_Request) (*packages.ModulePackageVersions_Response, error) { + response := new(packages.ModulePackageVersions_Response) + + module, err := regsrc.ParseModuleSource(request.SourceAddr) + if err != nil { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Invalid module source", + Detail: fmt.Sprintf("Module source %s is invalid: %s.", request.SourceAddr, err), + }) + return response, nil + } + + client := registry.NewClient(p.services, nil) + versions, err := client.ModuleVersions(ctx, module) + if err != nil { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to query available module packages", + Detail: fmt.Sprintf("Could not retrieve the list of available modules for module %s: %s.", module.Display(), err), + }) + return response, nil + } + + for _, module := range versions.Modules { + for _, version := range module.Versions { + response.Versions = append(response.Versions, version.Version) + } + } + + return response, nil +} + +func (p *packagesServer) ModulePackageSourceAddr(ctx context.Context, request *packages.ModulePackageSourceAddr_Request) (*packages.ModulePackageSourceAddr_Response, error) { + response := new(packages.ModulePackageSourceAddr_Response) + + module, err := regsrc.ParseModuleSource(request.SourceAddr) + if err != nil { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Invalid module source", + Detail: fmt.Sprintf("Module source %s is invalid: %s.", request.SourceAddr, err), + }) + return response, nil + } + + client := registry.NewClient(p.services, nil) + location, err := client.ModuleLocation(ctx, module, request.Version) + if err != nil { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to query module package metadata", + Detail: fmt.Sprintf("Could not retrieve package metadata for provider %s at %s: %s.", module.Display(), request.Version, err), + }) + return response, nil + } + response.Url = location + + return response, nil +} + +func (p *packagesServer) FetchModulePackage(ctx context.Context, request *packages.FetchModulePackage_Request) (*packages.FetchModulePackage_Response, error) { + response := new(packages.FetchModulePackage_Response) + + fetcher := getmodules.NewPackageFetcher() + if err := fetcher.FetchPackage(ctx, request.CacheDir, request.Url); err != nil { + response.Diagnostics = append(response.Diagnostics, &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to download module package", + Detail: fmt.Sprintf("Could not download provider from %s: %s.", request.Url, err), + }) + return response, nil + } + + return response, nil +} diff --git a/internal/rpcapi/packages_test.go b/internal/rpcapi/packages_test.go new file mode 100644 index 0000000000..b9f7954e39 --- /dev/null +++ b/internal/rpcapi/packages_test.go @@ -0,0 +1,308 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path" + "strings" + "testing" + + "github.com/apparentlymart/go-versions/versions" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-svchost/disco" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages" +) + +func TestPackagesServer_ProviderPackageVersions(t *testing.T) { + + tcs := map[string]struct { + source string + expectedVersions []string + expectedWarnings []string + sourceFn providerSourceFn + }{ + "single_version": { + source: "hashicorp/foo", + expectedVersions: []string{"0.1.0"}, + sourceFn: func(_ *disco.Disco) getproviders.Source { + packages := []getproviders.PackageMeta{ + { + Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), + Version: versions.MustParseVersion("0.1.0"), + }, + } + return getproviders.NewMockSource(packages, nil) + }, + }, + "multiple_versions": { + source: "hashicorp/foo", + expectedVersions: []string{"0.1.0", "0.2.0"}, + sourceFn: func(_ *disco.Disco) getproviders.Source { + packages := []getproviders.PackageMeta{ + { + Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), + Version: versions.MustParseVersion("0.1.0"), + }, + { + Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), + Version: versions.MustParseVersion("0.2.0"), + }, + } + return getproviders.NewMockSource(packages, nil) + }, + }, + "with_warnings": { + source: "hashicorp/foo", + expectedVersions: []string{"0.1.0"}, + expectedWarnings: []string{"- warning one", "- warning two"}, + sourceFn: func(_ *disco.Disco) getproviders.Source { + packages := []getproviders.PackageMeta{ + { + Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), + Version: versions.MustParseVersion("0.1.0"), + }, + } + warnings := map[addrs.Provider]getproviders.Warnings{ + addrs.MustParseProviderSourceString("hashicorp/foo"): { + "warning one", + "warning two", + }, + } + return getproviders.NewMockSource(packages, warnings) + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + service := &packagesServer{ + providerSourceFn: tc.sourceFn, + } + + response, err := service.ProviderPackageVersions(context.Background(), &packages.ProviderPackageVersions_Request{ + SourceAddr: tc.source, + }) + if err != nil { + t.Fatal(err) + } + + if len(tc.expectedWarnings) > 0 { + for _, diag := range response.Diagnostics { + if diag.Severity == terraform1.Diagnostic_WARNING && diag.Summary == "Additional provider information from registry" { + expected := fmt.Sprintf("The remote registry returned warnings for %s:\n%s", tc.source, strings.Join(tc.expectedWarnings, "\n")) + if diff := cmp.Diff(expected, diag.Detail); len(diff) > 0 { + t.Error(diff) + } + } + } + + // We're expecting only one diagnostic with the warnings. + if len(response.Diagnostics) > 1 { + for _, diag := range response.Diagnostics { + t.Errorf("unexpected diagnostics: %s", diag.Detail) + } + return + } + } else { + // Otherwise we're expecting no diagnostics. + if len(response.Diagnostics) > 0 { + for _, diag := range response.Diagnostics { + t.Errorf("unexpected diagnostics: %s", diag.Detail) + } + return + } + } + + if diff := cmp.Diff(tc.expectedVersions, response.Versions); len(diff) > 0 { + t.Error(diff) + } + }) + } + +} + +func TestPackagesServer_FetchProviderPackage(t *testing.T) { + providerHashes := providerHashes(t) + + tcs := map[string]struct { + // source, version, platforms, and hashes are what we're going to pass + // in as the request. + source string + version string + platforms []string + hashes []string + + // platformLocations, and platformHashes are what we're going to use to + // create our virtual provider metadata. + platformLocations map[string]string + platformHashes map[string][]string + + // diagnostics are the expected diagnostics for each platform. + diagnostics map[string][]string + }{ + "single_version_and_platform": { + source: "hashicorp/foo", + version: "0.1.0", + platforms: []string{"linux_amd64"}, + platformLocations: map[string]string{ + "linux_amd64": "terraform_provider_foo", + }, + }, + "single_version_multiple_platforms": { + source: "hashicorp/foo", + version: "0.1.0", + platforms: []string{"linux_amd64", "darwin_arm64"}, + platformLocations: map[string]string{ + "linux_amd64": "terraform_provider_foo", + "darwin_arm64": "terraform_provider_bar", + }, + }, + "single_version_and_platform_with_hashes": { + source: "hashicorp/foo", + version: "0.1.0", + platforms: []string{"linux_amd64"}, + platformLocations: map[string]string{ + "linux_amd64": "terraform_provider_foo", + }, + platformHashes: map[string][]string{ + "linux_amd64": { + "h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=", + }, + }, + }, + "single_version_and_platform_with_hashes_clash": { + source: "hashicorp/foo", + version: "0.1.0", + hashes: []string{"h1:Hod4iOH+qbXMtH4orEmCem6F3T+YRPhDSNlXmOIRNuY="}, + platforms: []string{"linux_amd64"}, + platformLocations: map[string]string{ + "linux_amd64": "terraform_provider_foo", + }, + platformHashes: map[string][]string{ + "linux_amd64": { + "h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0=", + }, + }, + diagnostics: map[string][]string{ + "linux_amd64": { + "the local package for registry.terraform.io/hashicorp/foo 0.1.0 doesn't match any of the checksums previously recorded in the dependency lock file", + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + service := &packagesServer{ + providerSourceFn: func(_ *disco.Disco) getproviders.Source { + var providers []getproviders.PackageMeta + for _, p := range tc.platforms { + platform := parsePlatform(t, p) + + var authentication getproviders.PackageAuthentication + if len(tc.platformHashes) > 0 { + authentication = getproviders.NewPackageHashAuthentication(platform, func() []getproviders.Hash { + var hashes []getproviders.Hash + for _, hash := range tc.platformHashes[p] { + hashes = append(hashes, getproviders.Hash(hash)) + } + return hashes + }()) + } + + providers = append(providers, getproviders.PackageMeta{ + Provider: addrs.MustParseProviderSourceString(tc.source), + Version: versions.MustParseVersion(tc.version), + TargetPlatform: platform, + Location: getproviders.PackageLocalDir(path.Join("testdata", "providers", tc.platformLocations[p])), + Authentication: authentication, + }) + } + + return getproviders.NewMockSource(providers, nil) + }, + } + + cacheDir := t.TempDir() + response, err := service.FetchProviderPackage(context.Background(), &packages.FetchProviderPackage_Request{ + CacheDir: cacheDir, + SourceAddr: tc.source, + Version: tc.version, + Platforms: tc.platforms, + Hashes: tc.hashes, + }) + if err != nil { + t.Fatal(err) + } + + if len(response.Diagnostics) > 0 { + for _, diag := range response.Diagnostics { + t.Errorf("unexpected diagnostics: %s", diag.Detail) + } + return + } + + if len(response.Results) != len(tc.platforms) { + t.Fatalf("wrong number of results") + } + + for ix, platform := range tc.platforms { + result := response.Results[ix] + + if tc.diagnostics != nil && len(tc.diagnostics[platform]) > 0 { + if len(result.Diagnostics) != len(tc.diagnostics[platform]) { + t.Fatalf("expected %d diagnostics for %s but found %d", len(tc.diagnostics[platform]), platform, len(result.Diagnostics)) + } + for ix, expected := range tc.diagnostics[platform] { + if !strings.Contains(result.Diagnostics[ix].Detail, expected) { + t.Errorf("expected: %s\nactual: %s", expected, result.Diagnostics[ix]) + } + } + + return + } else { + if len(result.Diagnostics) > 0 { + for _, diag := range result.Diagnostics { + t.Errorf("unexpected diagnostics for %s: %s", platform, diag.Detail) + } + return + } + } + + if diff := cmp.Diff(providerHashes[tc.platformLocations[platform]], result.Provider.Hashes); len(diff) > 0 { + t.Error(diff) + } + } + }) + } +} + +func providerHashes(t *testing.T) map[string][]string { + var hashes map[string][]string + + data, err := os.ReadFile("testdata/providers/hashes.json") + if err != nil { + t.Fatal(err) + } + + if err := json.Unmarshal(data, &hashes); err != nil { + t.Fatal(err) + } + + return hashes +} + +func parsePlatform(t *testing.T, raw string) getproviders.Platform { + platform, err := getproviders.ParsePlatform(raw) + if err != nil { + t.Fatal(err) + } + return platform +} diff --git a/internal/rpcapi/plugin.go b/internal/rpcapi/plugin.go new file mode 100644 index 0000000000..36509e7254 --- /dev/null +++ b/internal/rpcapi/plugin.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "fmt" + + "github.com/hashicorp/go-plugin" + svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/auth" + "github.com/hashicorp/terraform-svchost/disco" + "google.golang.org/grpc" + + "github.com/hashicorp/terraform/internal/command/cliconfig" + pluginDiscovery "github.com/hashicorp/terraform/internal/plugin/discovery" + "github.com/hashicorp/terraform/internal/rpcapi/dynrpcserver" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" +) + +type corePlugin struct { + plugin.Plugin + + experimentsAllowed bool +} + +func (p *corePlugin) GRPCClient(ctx context.Context, broker *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + // This codebase only provides a server implementation of this plugin. + // Clients must live elsewhere. + return nil, fmt.Errorf("there is no client implementation in this codebase") +} + +func (p *corePlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error { + generalOpts := &serviceOpts{ + experimentsAllowed: p.experimentsAllowed, + } + registerGRPCServices(s, generalOpts) + return nil +} + +func registerGRPCServices(s *grpc.Server, opts *serviceOpts) { + // We initially only register the setup server, because the registration + // of other services can vary depending on the capabilities negotiated + // during handshake. + server := newSetupServer(serverHandshake(s, opts)) + setup.RegisterSetupServer(s, server) +} + +func serverHandshake(s *grpc.Server, opts *serviceOpts) func(context.Context, *setup.Handshake_Request, *stopper) (*setup.ServerCapabilities, error) { + dependenciesStub := dynrpcserver.NewDependenciesStub() + dependencies.RegisterDependenciesServer(s, dependenciesStub) + stacksStub := dynrpcserver.NewStacksStub() + stacks.RegisterStacksServer(s, stacksStub) + packagesStub := dynrpcserver.NewPackagesStub() + packages.RegisterPackagesServer(s, packagesStub) + + return func(ctx context.Context, request *setup.Handshake_Request, stopper *stopper) (*setup.ServerCapabilities, error) { + // All of our servers will share a common handles table so that objects + // can be passed from one service to another. + handles := newHandleTable() + + // NOTE: This is intentionally not the same disco that "package main" + // instantiates for Terraform CLI, because the RPC API is + // architecturally independent from CLI despite being launched through + // it, and so it is not subject to any ambient CLI configuration files + // that might be in scope. If we later discover requirements for + // callers to customize the service discovery settings, consider + // adding new fields to terraform1.ClientCapabilities (even though + // this isn't strictly a "capability") so that the RPC caller has + // full control without needing to also tinker with the current user's + // CLI configuration. + services, err := newServiceDisco(request.GetConfig()) + if err != nil { + return &setup.ServerCapabilities{}, err + } + + // If handshaking is successful (which it currently always is, because + // we don't have any special capabilities to negotiate yet) then we + // will initialize all of the other services so the client can begin + // doing real work. In future the details of what we register here + // might vary based on the negotiated capabilities. + dependenciesStub.ActivateRPCServer(newDependenciesServer(handles, services)) + stacksStub.ActivateRPCServer(newStacksServer(stopper, handles, services, opts)) + packagesStub.ActivateRPCServer(newPackagesServer(services)) + + // If the client requested any extra capabililties that we're going + // to honor then we should announce them in this result. + return &setup.ServerCapabilities{}, nil + } +} + +// serviceOpts are options that could potentially apply to all of our +// individual RPC services. +// +// This could potentially be embedded inside a service-specific options +// structure, if needed. +type serviceOpts struct { + experimentsAllowed bool +} + +func newServiceDisco(config *setup.Config) (*disco.Disco, error) { + // First, we'll try and load any credentials that might have been available + // to the UI. It's perfectly fine if there are none so any errors we find + // are from malformed credentials rather than missing ones. + + file, diags := cliconfig.LoadConfig() + if diags.HasErrors() { + return nil, fmt.Errorf("problem loading CLI configuration: %w", diags.ErrWithWarnings()) + } + + helperPlugins := pluginDiscovery.FindPlugins("credentials", cliconfig.GlobalPluginDirs()) + src, err := file.CredentialsSource(helperPlugins) + if err != nil { + return nil, fmt.Errorf("problem creating credentials source: %w", err) + } + services := disco.NewWithCredentialsSource(src) + + // Second, we'll side-load any credentials that might have been passed in. + + credSrc := services.CredentialsSource() + if config != nil { + for host, cred := range config.GetCredentials() { + if err := credSrc.StoreForHost(svchost.Hostname(host), auth.HostCredentialsToken(cred.Token)); err != nil { + return nil, fmt.Errorf("problem storing credential for host %s with: %w", host, err) + } + } + services.SetCredentialsSource(credSrc) + } + + return services, nil +} diff --git a/internal/rpcapi/resource_identity.go b/internal/rpcapi/resource_identity.go new file mode 100644 index 0000000000..a0c5d85ba1 --- /dev/null +++ b/internal/rpcapi/resource_identity.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackstate" +) + +func listResourceIdentities(stackState *stackstate.State, identitySchemas map[addrs.Provider]map[string]providers.IdentitySchema) ([]*stacks.ListResourceIdentities_Resource, error) { + resourceIdentities := make([]*stacks.ListResourceIdentities_Resource, 0) + + // A non-existent stack state has no resource identities + if stackState == nil { + return resourceIdentities, nil + } + + for ci := range stackState.AllComponentInstances() { + componentIdentities := stackState.IdentitiesForComponent(ci) + for ri, src := range componentIdentities { + // We skip resources without identity JSON + if len(src.IdentityJSON) == 0 { + continue + } + + providerAddrs := addrs.ImpliedProviderForUnqualifiedType(ri.ResourceInstance.Resource.Resource.ImpliedProvider()) + + identitySchema, ok := identitySchemas[providerAddrs] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "provider %s could not be found in the identity schema", providerAddrs) + } + + resourceType := ri.ResourceInstance.Resource.Resource.Type + schema, ok := identitySchema[resourceType] + if !ok { + return nil, status.Errorf(codes.InvalidArgument, "resource %s could not be found in the identity schema", ri) + } + if src.IdentitySchemaVersion != uint64(schema.Version) { + return nil, status.Errorf(codes.InvalidArgument, "resource %s has an invalid identity schema version, please update the provider or refresh the state", ri) + } + ty := schema.Body.ImpliedType() + + identity, err := ctyjson.Unmarshal(src.IdentityJSON, ty) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to unmarshal identity JSON for resource %s: %s", ri, err) + } + + identityRaw, err := plans.NewDynamicValue(identity, ty) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to create dynamic value for identity for resource %s: %s", ri, err) + } + stacksIdentityRaw := stacks.NewDynamicValue(identityRaw, []cty.Path{}) + + resourceIdentities = append(resourceIdentities, &stacks.ListResourceIdentities_Resource{ + ComponentAddr: ci.Item.Component.String(), + ComponentInstanceAddr: ci.Item.String(), + ResourceInstanceAddr: ri.String(), + ResourceIdentity: stacksIdentityRaw, + }) + } + } + + return resourceIdentities, nil +} diff --git a/internal/rpcapi/resource_identity_test.go b/internal/rpcapi/resource_identity_test.go new file mode 100644 index 0000000000..dbaf455eca --- /dev/null +++ b/internal/rpcapi/resource_identity_test.go @@ -0,0 +1,157 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/zclconf/go-cty/cty" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestResourceIdentity(t *testing.T) { + for name, tc := range map[string]struct { + state *stackstate.State + + expected []*stacks.ListResourceIdentities_Resource + expectedErr error + }{ + "nil state": { + state: nil, + expected: []*stacks.ListResourceIdentities_Resource{}, + }, + "empty state": { + state: stackstate.NewStateBuilder().Build(), + expected: []*stacks.ListResourceIdentities_Resource{}, + }, + "resource with no identity": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject(t, "component.self.testing_resource.hello")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + })). + Build(), + expected: []*stacks.ListResourceIdentities_Resource{}, + }, + + "resource with identity": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject(t, "component.self.testing_resource.hello")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + IdentityJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "hello", + }), + })). + Build(), + expected: []*stacks.ListResourceIdentities_Resource{ + { + ComponentAddr: "component.self", + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.hello", + ResourceIdentity: &stacks.DynamicValue{ + Msgpack: []byte("\x81\xa2id\xa5hello"), + }, + }, + }, + }, + + "resource with identity and newer identity version": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject(t, "component.self.testing_resource.hello")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + IdentityJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "hello", + }), + IdentitySchemaVersion: 2, + })). + Build(), + expectedErr: status.Errorf(codes.InvalidArgument, "resource testing_resource.hello has an invalid identity schema version, please update the provider or refresh the state"), + }, + } { + t.Run(name, func(t *testing.T) { + + identitySchemas := map[addrs.Provider]map[string]providers.IdentitySchema{ + addrs.NewDefaultProvider("testing"): { + "testing_resource": { + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } + + actual, err := listResourceIdentities(tc.state, identitySchemas) + + if tc.expectedErr != nil { + if err == nil { + t.Errorf("expected error %v, got nil", tc.expectedErr) + } else if err.Error() != tc.expectedErr.Error() { + t.Errorf("expected error %v, got %v", tc.expectedErr, err) + } + } else { + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(actual) != len(tc.expected) { + t.Errorf("expected %d resources, got %d", len(tc.expected), len(actual)) + } + + for index, expected := range tc.expected { + actual := actual[index] + + if actual.ComponentAddr != expected.ComponentAddr { + t.Errorf("expected component address %s, got %s", expected.ComponentAddr, actual.ComponentAddr) + } + + if actual.ComponentInstanceAddr != expected.ComponentInstanceAddr { + t.Errorf("expected component instance address %s, got %s", expected.ComponentInstanceAddr, actual.ComponentInstanceAddr) + } + + if actual.ResourceInstanceAddr != expected.ResourceInstanceAddr { + t.Errorf("expected resource instance address %s, got %s", expected.ResourceInstanceAddr, actual.ResourceInstanceAddr) + } + + if string(actual.ResourceIdentity.Msgpack) != string(expected.ResourceIdentity.Msgpack) { + t.Errorf("expected resource identity %v, got %v", expected.ResourceIdentity, actual.ResourceIdentity) + } + } + } + }) + } +} diff --git a/internal/rpcapi/server.go b/internal/rpcapi/server.go new file mode 100644 index 0000000000..630e36daa0 --- /dev/null +++ b/internal/rpcapi/server.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "errors" + "os" + + "github.com/hashicorp/go-plugin" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "google.golang.org/grpc" +) + +// ServePlugin attempts to complete the go-plugin protocol handshake, and then +// if successful starts the plugin server and blocks until the given context +// is cancelled. +// +// Returns [ErrNotPluginClient] if this program doesn't seem to be running as +// the child of a plugin client, which is detected based on a magic environment +// variable that the client ought to have set. +func ServePlugin(ctx context.Context, opts ServerOpts) error { + // go-plugin has its own check for the environment variable magic cookie + // but it returns a generic error message. We'll pre-check it out here + // instead so we can return a more specific error message. + if os.Getenv(handshake.MagicCookieKey) != handshake.MagicCookieValue { + return ErrNotPluginClient + } + + plugin.Serve(&plugin.ServeConfig{ + HandshakeConfig: handshake, + VersionedPlugins: map[int]plugin.PluginSet{ + 1: { + "tfcore": &corePlugin{ + experimentsAllowed: opts.ExperimentsAllowed, + }, + }, + }, + GRPCServer: func(opts []grpc.ServerOption) *grpc.Server { + fullOpts := []grpc.ServerOption{ + grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()), + grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()), + } + fullOpts = append(fullOpts, opts...) + server := grpc.NewServer(fullOpts...) + // We'll also monitor the given context for cancellation + // and terminate the server gracefully if we get cancelled. + go func() { + <-ctx.Done() + server.GracefulStop() + // The above will block until all of the pending RPCs have + // finished. + os.Exit(0) + }() + return server + }, + }) + return nil +} + +var ErrNotPluginClient = errors.New("caller is not a plugin client") + +type ServerOpts struct { + ExperimentsAllowed bool +} + +// handshake is the HandshakeConfig used to begin negotiation between client +// and server. +var handshake = plugin.HandshakeConfig{ + // The ProtocolVersion is the version that must match between TF core + // and TF plugins. + ProtocolVersion: 1, + + // The magic cookie values should NEVER be changed. + MagicCookieKey: "TERRAFORM_RPCAPI_COOKIE", + MagicCookieValue: "fba0991c9bcd453982f0d88e2da95940", +} diff --git a/internal/rpcapi/setup.go b/internal/rpcapi/setup.go new file mode 100644 index 0000000000..7336f9af23 --- /dev/null +++ b/internal/rpcapi/setup.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "sync" + + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// setupServer is an implementation of the "Setup" service defined in our +// terraform1 package. +// +// This service is here mainly to offer the "Handshake" function, which clients +// must call to negotiate access to any other services. This is really just +// an adapter around a handshake function implemented on [corePlugin]. +type setupServer struct { + setup.UnimplementedSetupServer + + // initOthers is the callback used to perform the capability negotiation + // step and initialize all of the other API services based on what was + // negotiated. + initOthers func(context.Context, *setup.Handshake_Request, *stopper) (*setup.ServerCapabilities, error) + + // stopper is used to track and stop long-running operations when the Stop + // RPC is called. + stopper *stopper + + mu sync.Mutex +} + +func newSetupServer(initOthers func(context.Context, *setup.Handshake_Request, *stopper) (*setup.ServerCapabilities, error)) setup.SetupServer { + return &setupServer{ + initOthers: initOthers, + stopper: newStopper(), + } +} + +func (s *setupServer) Handshake(ctx context.Context, req *setup.Handshake_Request) (*setup.Handshake_Response, error) { + s.mu.Lock() + defer s.mu.Unlock() + + if s.initOthers == nil { + return nil, status.Error(codes.FailedPrecondition, "handshake already completed") + } + + var serverCaps *setup.ServerCapabilities + var err error + { + ctx, span := tracer.Start(ctx, "initialize RPC services") + serverCaps, err = s.initOthers(ctx, req, s.stopper) + span.End() + } + s.initOthers = nil // cannot handshake again + if err != nil { + return nil, err + } + return &setup.Handshake_Response{ + Capabilities: serverCaps, + }, nil +} + +func (s *setupServer) Stop(ctx context.Context, req *setup.Stop_Request) (*setup.Stop_Response, error) { + s.mu.Lock() + defer s.mu.Unlock() + + s.stopper.stop() + + return &setup.Stop_Response{}, nil +} diff --git a/internal/rpcapi/setup_test.go b/internal/rpcapi/setup_test.go new file mode 100644 index 0000000000..a9ee807955 --- /dev/null +++ b/internal/rpcapi/setup_test.go @@ -0,0 +1,85 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "strings" + "sync" + "testing" + + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup" +) + +func TestSetupServer_Handshake(t *testing.T) { + called := 0 + server := newSetupServer(func(ctx context.Context, req *setup.Handshake_Request, stopper *stopper) (*setup.ServerCapabilities, error) { + called++ + if got, want := req.Config.Credentials["localterraform.com"].Token, "boop"; got != want { + t.Fatalf("incorrect token. got %q, want %q", got, want) + } + return &setup.ServerCapabilities{}, nil + }) + + req := &setup.Handshake_Request{ + Capabilities: &setup.ClientCapabilities{}, + Config: &setup.Config{ + Credentials: map[string]*setup.HostCredential{ + "localterraform.com": { + Token: "boop", + }, + }, + }, + } + _, err := server.Handshake(context.Background(), req) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if called != 1 { + t.Errorf("unexpected initOthers call count %d, want 1", called) + } + + _, err = server.Handshake(context.Background(), req) + if err == nil || !strings.Contains(err.Error(), "handshake already completed") { + t.Fatalf("unexpected error: %s", err) + } + if called != 1 { + t.Errorf("unexpected initOthers call count %d, want 1", called) + } +} + +func TestSetupServer_Stop(t *testing.T) { + var s *stopper + server := newSetupServer(func(ctx context.Context, req *setup.Handshake_Request, stopper *stopper) (*setup.ServerCapabilities, error) { + s = stopper + return &setup.ServerCapabilities{}, nil + }) + _, err := server.Handshake(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + if s == nil { + t.Fatal("stopper not passed to initOthers") + } + + var wg sync.WaitGroup + + var stops []stopChan + for range 2 { + stops = append(stops, s.add()) + wg.Add(1) + } + + for _, stop := range stops { + stop := stop + go func() { + <-stop + wg.Done() + }() + } + + server.Stop(context.Background(), nil) + + wg.Wait() +} diff --git a/internal/rpcapi/stacks.go b/internal/rpcapi/stacks.go new file mode 100644 index 0000000000..3afec74580 --- /dev/null +++ b/internal/rpcapi/stacks.go @@ -0,0 +1,1333 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "bytes" + "context" + "fmt" + "io" + "time" + + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/terraform-svchost/disco" + "go.opentelemetry.io/otel/attribute" + otelCodes "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providercache" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackmigrate" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type stacksServer struct { + stacks.UnimplementedStacksServer + + stopper *stopper + services *disco.Disco + handles *handleTable + experimentsAllowed bool + + // providerCacheOverride is a map of provider names to provider factories + // that should be used instead of the default provider cache. This is used + // within tests to side load providers without needing a real provider + // cache. + providerCacheOverride map[addrs.Provider]providers.Factory + // providerDependencyLockOverride is an in-memory override of the provider + // lockfile used for testing when the real provider is side-loaded. + providerDependencyLockOverride *depsfile.Locks + // planTimestampOverride is an in-memory override of the plan timestamp used + // for testing. This just ensures our tests aren't flaky as we can use a + // constant timestamp for the plan. + planTimestampOverride *time.Time +} + +var ( + _ stacks.StacksServer = (*stacksServer)(nil) + + WorkspaceNameEnvVar = "TF_WORKSPACE" +) + +func newStacksServer(stopper *stopper, handles *handleTable, services *disco.Disco, opts *serviceOpts) *stacksServer { + return &stacksServer{ + stopper: stopper, + services: services, + handles: handles, + experimentsAllowed: opts.experimentsAllowed, + } +} + +func (s *stacksServer) OpenStackConfiguration(ctx context.Context, req *stacks.OpenStackConfiguration_Request) (*stacks.OpenStackConfiguration_Response, error) { + sourcesHnd := handle[*sourcebundle.Bundle](req.SourceBundleHandle) + sources := s.handles.SourceBundle(sourcesHnd) + if sources == nil { + return nil, status.Error(codes.InvalidArgument, "the given source bundle handle is invalid") + } + + sourceAddr, err := resolveFinalSourceAddr(req.SourceAddress, sources) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid source address: %s", err) + } + + config, diags := stackconfig.LoadConfigDir(sourceAddr, sources) + if diags.HasErrors() { + // For errors in the configuration itself we treat that as a successful + // result from OpenStackConfiguration but with diagnostics in the + // response and no source handle. + return &stacks.OpenStackConfiguration_Response{ + Diagnostics: diagnosticsToProto(diags), + }, nil + } + + configHnd, err := s.handles.NewStackConfig(config, sourcesHnd) + if err != nil { + // The only reasonable way we can fail here is if the caller made + // a concurrent call to Dependencies.CloseSourceBundle after we + // checked the handle validity above. That'd be a very strange thing + // to do, but in the event it happens we'll just discard the config + // we loaded (since its source files on disk might be gone imminently) + // and return an error. + return nil, status.Errorf(codes.Unknown, "allocating config handle: %s", err) + } + + // If we get here then we're guaranteed that the source bundle handle + // cannot be closed until the config handle is closed -- enforced by + // [handleTable]'s dependency tracking -- and so we can return the config + // handle. (The caller is required to ensure that the source bundle files + // on disk are not modified for as long as the source bundle handle remains + // open, and its lifetime will necessarily exceed the config handle.) + return &stacks.OpenStackConfiguration_Response{ + StackConfigHandle: configHnd.ForProtobuf(), + Diagnostics: diagnosticsToProto(diags), + }, nil +} + +func (s *stacksServer) CloseStackConfiguration(ctx context.Context, req *stacks.CloseStackConfiguration_Request) (*stacks.CloseStackConfiguration_Response, error) { + hnd := handle[*stackconfig.Config](req.StackConfigHandle) + err := s.handles.CloseStackConfig(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &stacks.CloseStackConfiguration_Response{}, nil +} + +func (s *stacksServer) ValidateStackConfiguration(ctx context.Context, req *stacks.ValidateStackConfiguration_Request) (*stacks.ValidateStackConfiguration_Response, error) { + cfgHnd := handle[*stackconfig.Config](req.StackConfigHandle) + cfg := s.handles.StackConfig(cfgHnd) + if cfg == nil { + return nil, status.Error(codes.InvalidArgument, "the given stack configuration handle is invalid") + } + depsHnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + var deps *depsfile.Locks + if !depsHnd.IsNil() { + deps = s.handles.DependencyLocks(depsHnd) + if deps == nil { + return nil, status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid") + } + } else { + deps = depsfile.NewLocks() + } + providerCacheHnd := handle[*providercache.Dir](req.ProviderCacheHandle) + var providerCache *providercache.Dir + if !providerCacheHnd.IsNil() { + providerCache = s.handles.ProviderPluginCache(providerCacheHnd) + if providerCache == nil { + return nil, status.Error(codes.InvalidArgument, "the given provider cache handle is invalid") + } + } + + // (providerFactoriesForLocks explicitly supports a nil providerCache) + providerFactories, err := providerFactoriesForLocks(deps, providerCache) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err) + } + + diags := stackruntime.Validate(ctx, &stackruntime.ValidateRequest{ + Config: cfg, + ExperimentsAllowed: s.experimentsAllowed, + ProviderFactories: providerFactories, + DependencyLocks: *deps, + }) + return &stacks.ValidateStackConfiguration_Response{ + Diagnostics: diagnosticsToProto(diags), + }, nil +} + +func (s *stacksServer) FindStackConfigurationComponents(ctx context.Context, req *stacks.FindStackConfigurationComponents_Request) (*stacks.FindStackConfigurationComponents_Response, error) { + cfgHnd := handle[*stackconfig.Config](req.StackConfigHandle) + cfg := s.handles.StackConfig(cfgHnd) + if cfg == nil { + return nil, status.Error(codes.InvalidArgument, "the given stack configuration handle is invalid") + } + + return &stacks.FindStackConfigurationComponents_Response{ + Config: stackConfigMetaforProto(cfg.Root, stackaddrs.RootStack), + }, nil +} + +func stackConfigMetaforProto(cfgNode *stackconfig.ConfigNode, stackAddr stackaddrs.Stack) *stacks.FindStackConfigurationComponents_StackConfig { + ret := &stacks.FindStackConfigurationComponents_StackConfig{ + Components: make(map[string]*stacks.FindStackConfigurationComponents_Component), + EmbeddedStacks: make(map[string]*stacks.FindStackConfigurationComponents_EmbeddedStack), + InputVariables: make(map[string]*stacks.FindStackConfigurationComponents_InputVariable), + OutputValues: make(map[string]*stacks.FindStackConfigurationComponents_OutputValue), + Removed: make(map[string]*stacks.FindStackConfigurationComponents_Removed), + } + + for name, cc := range cfgNode.Stack.Components { + cProto := &stacks.FindStackConfigurationComponents_Component{ + SourceAddr: cc.FinalSourceAddr.String(), + ComponentAddr: stackaddrs.Config(stackAddr, stackaddrs.Component{Name: cc.Name}).String(), + } + switch { + case cc.ForEach != nil: + cProto.Instances = stacks.FindStackConfigurationComponents_FOR_EACH + default: + cProto.Instances = stacks.FindStackConfigurationComponents_SINGLE + } + ret.Components[name] = cProto + } + + for name, sn := range cfgNode.Children { + sc := cfgNode.Stack.EmbeddedStacks[name] + sProto := &stacks.FindStackConfigurationComponents_EmbeddedStack{ + SourceAddr: sn.Stack.SourceAddr.String(), + Config: stackConfigMetaforProto(sn, stackAddr.Child(name)), + } + switch { + case sc.ForEach != nil: + sProto.Instances = stacks.FindStackConfigurationComponents_FOR_EACH + default: + sProto.Instances = stacks.FindStackConfigurationComponents_SINGLE + } + ret.EmbeddedStacks[name] = sProto + } + + for name, vc := range cfgNode.Stack.InputVariables { + vProto := &stacks.FindStackConfigurationComponents_InputVariable{ + Optional: !vc.DefaultValue.IsNull(), + Sensitive: vc.Sensitive, + Ephemeral: vc.Ephemeral, + } + ret.InputVariables[name] = vProto + } + + for name, oc := range cfgNode.Stack.OutputValues { + oProto := &stacks.FindStackConfigurationComponents_OutputValue{ + Sensitive: oc.Sensitive, + Ephemeral: oc.Ephemeral, + } + ret.OutputValues[name] = oProto + } + + // Currently Components are the only thing that can be removed + for name, rc := range cfgNode.Stack.RemovedComponents.All() { + var blocks []*stacks.FindStackConfigurationComponents_Removed_Block + for _, rc := range rc { + relativeAddress := rc.From.TargetConfigComponent() + cProto := &stacks.FindStackConfigurationComponents_Removed_Block{ + SourceAddr: rc.FinalSourceAddr.String(), + ComponentAddr: stackaddrs.Config(append(stackAddr, relativeAddress.Stack...), relativeAddress.Item).String(), + Destroy: rc.Destroy, + } + switch { + case rc.ForEach != nil: + cProto.Instances = stacks.FindStackConfigurationComponents_FOR_EACH + default: + cProto.Instances = stacks.FindStackConfigurationComponents_SINGLE + } + blocks = append(blocks, cProto) + } + relativeAddress := rc[0].From.TargetConfigComponent() + ret.Removed[name.String()] = &stacks.FindStackConfigurationComponents_Removed{ + // in order to ensure as much backwards and forwards compatibility + // as possible, we're going to set the deprecated single fields + // with the first run block + + SourceAddr: rc[0].FinalSourceAddr.String(), + Instances: func() stacks.FindStackConfigurationComponents_Instances { + switch { + case rc[0].ForEach != nil: + return stacks.FindStackConfigurationComponents_FOR_EACH + default: + return stacks.FindStackConfigurationComponents_SINGLE + } + }(), + ComponentAddr: stackaddrs.Config(append(stackAddr, relativeAddress.Stack...), relativeAddress.Item).String(), + Destroy: rc[0].Destroy, + + // We return all the values here: + + Blocks: blocks, + } + } + + return ret +} + +func (s *stacksServer) OpenState(stream stacks.Stacks_OpenStateServer) error { + loader := stackstate.NewLoader() + for { + item, err := stream.Recv() + if err == io.EOF { + break // All done! + } else if err != nil { + return err + } + err = loader.AddRaw(item.Raw.Key, item.Raw.Value) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid raw state element: %s", err) + } + } + + hnd := s.handles.NewStackState(loader.State()) + return stream.SendAndClose(&stacks.OpenStackState_Response{ + StateHandle: hnd.ForProtobuf(), + }) +} + +func (s *stacksServer) CloseState(ctx context.Context, req *stacks.CloseStackState_Request) (*stacks.CloseStackState_Response, error) { + hnd := handle[*stackstate.State](req.StateHandle) + err := s.handles.CloseStackState(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &stacks.CloseStackState_Response{}, nil +} + +func (s *stacksServer) PlanStackChanges(req *stacks.PlanStackChanges_Request, evts stacks.Stacks_PlanStackChangesServer) error { + ctx := evts.Context() + syncEvts := newSyncStreamingRPCSender(evts) + evts = nil // Prevent accidental unsynchronized usage of this server + + cfgHnd := handle[*stackconfig.Config](req.StackConfigHandle) + cfg := s.handles.StackConfig(cfgHnd) + if cfg == nil { + return status.Error(codes.InvalidArgument, "the given stack configuration handle is invalid") + } + depsHnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + var deps *depsfile.Locks + if s.providerDependencyLockOverride != nil { + deps = s.providerDependencyLockOverride + } else if !depsHnd.IsNil() { + deps = s.handles.DependencyLocks(depsHnd) + if deps == nil { + return status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid") + } + } else { + deps = depsfile.NewLocks() + } + providerCacheHnd := handle[*providercache.Dir](req.ProviderCacheHandle) + var providerCache *providercache.Dir + if !providerCacheHnd.IsNil() { + providerCache = s.handles.ProviderPluginCache(providerCacheHnd) + if providerCache == nil { + return status.Error(codes.InvalidArgument, "the given provider cache handle is invalid") + } + } + // NOTE: providerCache can be nil if no handle was provided, in which + // case the call can only use built-in providers. All code below + // must avoid panicking when providerCache is nil, but is allowed to + // return an InvalidArgument error in that case. + + if req.PreviousStateHandle != 0 && len(req.PreviousState) != 0 { + return status.Error(codes.InvalidArgument, "must not set both previous_state_handle and previous_state") + } + + inputValues, err := externalInputValuesFromProto(req.InputValues) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid input values: %s", err) + } + + var providerFactories map[addrs.Provider]providers.Factory + if s.providerCacheOverride != nil { + // This is only used in tests to side load providers without needing a + // real provider cache. + providerFactories = s.providerCacheOverride + } else { + // (providerFactoriesForLocks explicitly supports a nil providerCache) + providerFactories, err = providerFactoriesForLocks(deps, providerCache) + if err != nil { + return status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err) + } + } + + // We'll hook some internal events in the planning process both to generate + // tracing information if we're in an OpenTelemetry-aware context and + // to propagate a subset of the events to our client. + hooks := stackPlanHooks(syncEvts, cfg.Root.Stack.SourceAddr) + ctx = stackruntime.ContextWithHooks(ctx, hooks) + + var planMode plans.Mode + switch req.PlanMode { + case stacks.PlanMode_NORMAL: + planMode = plans.NormalMode + case stacks.PlanMode_REFRESH_ONLY: + planMode = plans.RefreshOnlyMode + case stacks.PlanMode_DESTROY: + planMode = plans.DestroyMode + default: + return status.Errorf(codes.InvalidArgument, "unsupported planning mode %d", req.PlanMode) + } + + var prevState *stackstate.State + if req.PreviousStateHandle != 0 { + stateHnd := handle[*stackstate.State](req.PreviousStateHandle) + prevState = s.handles.StackState(stateHnd) + if prevState == nil { + return status.Error(codes.InvalidArgument, "the given previous state handle is invalid") + } + } else { + // Deprecated: The previous state is provided inline as a map. + // FIXME: Remove this old field once our existing clients are updated. + prevState, err = stackstate.LoadFromProto(req.PreviousState) + if err != nil { + return status.Errorf(codes.InvalidArgument, "can't load previous state: %s", err) + } + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + rtReq := stackruntime.PlanRequest{ + PlanMode: planMode, + Config: cfg, + PrevState: prevState, + ProviderFactories: providerFactories, + InputValues: inputValues, + ExperimentsAllowed: s.experimentsAllowed, + DependencyLocks: *deps, + + // planTimestampOverride will be null if not set, so it's fine for + // us to just set this all the time. In practice, this will only have + // a value in tests. + ForcePlanTimestamp: s.planTimestampOverride, + } + rtResp := stackruntime.PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + // As a long-running operation, the plan RPC must be able to be stopped. We + // do this by requesting a stop channel from the stopper, and using it to + // cancel the planning process. + stopCh := s.stopper.add() + defer s.stopper.remove(stopCh) + + // We create a new cancellable context for the stack plan operation to + // allow us to respond to stop requests. + planCtx, cancelPlan := context.WithCancel(ctx) + defer cancelPlan() + + // The actual plan operation runs in the background, and emits events + // to us via the channels in rtResp before finally closing changesCh + // to signal that the process is complete. + go stackruntime.Plan(planCtx, &rtReq, &rtResp) + + emitDiag := func(diag tfdiags.Diagnostic) { + diags := tfdiags.Diagnostics{diag} + protoDiags := diagnosticsToProto(diags) + for _, protoDiag := range protoDiags { + syncEvts.Send(&stacks.PlanStackChanges_Event{ + Event: &stacks.PlanStackChanges_Event_Diagnostic{ + Diagnostic: protoDiag, + }, + }) + } + } + + // There is no strong ordering between the planned changes and the + // diagnostics, so we need to be prepared for them to arrive in any + // order. However, stackruntime.Plan does guarantee that it will + // close changesCh only after it's finished writing to and closing + // everything else, and so we can assume that once changesCh is + // closed we only need to worry about whatever's left in the + // diagsCh buffer. +Events: + for { + select { + + case change, ok := <-changesCh: + if !ok { + if diagsCh != nil { + // Async work is done! We do still need to consume the rest + // of diagsCh before we stop, though, because there might + // be some extras in the channel's buffer that we didn't + // get to yet. + for diag := range diagsCh { + emitDiag(diag) + } + } + break Events + } + + protoChange, err := change.PlannedChangeProto() + if err != nil { + // Should not get here: it always indicates a bug in + // PlannedChangeProto or in the code which constructed + // the change over in package stackeval. + emitDiag(tfdiags.Sourceless( + tfdiags.Error, + "Incorrectly-constructed change", + fmt.Sprintf( + "Failed to serialize a %T value for recording in the saved plan: %s.\n\nThis is a bug in Terraform; please report it!", + protoChange, err, + ), + )) + continue + } + + syncEvts.Send(&stacks.PlanStackChanges_Event{ + Event: &stacks.PlanStackChanges_Event_PlannedChange{ + PlannedChange: protoChange, + }, + }) + + case diag, ok := <-diagsCh: + if !ok { + // The diagnostics channel has closed, so we'll just stop + // trying to read from it and wait for changesCh to close, + // which will be our final signal that everything is done. + diagsCh = nil + continue + } + emitDiag(diag) + + case <-stopCh: + // If our stop channel is signalled, we need to cancel the plan. + // This may result in remaining changes or diagnostics being + // emitted, so we continue to monitor those channels if they're + // still active. + cancelPlan() + } + } + + return nil +} + +func (s *stacksServer) OpenPlan(stream stacks.Stacks_OpenPlanServer) error { + loader := stackplan.NewLoader() + for { + item, err := stream.Recv() + if err == io.EOF { + break // All done! + } else if err != nil { + return err + } + err = loader.AddRaw(item.Raw) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid raw plan element: %s", err) + } + } + + plan, err := loader.Plan() + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid raw plan: %s", err) + } + hnd := s.handles.NewStackPlan(plan) + return stream.SendAndClose(&stacks.OpenStackPlan_Response{ + PlanHandle: hnd.ForProtobuf(), + }) +} + +func (s *stacksServer) ClosePlan(ctx context.Context, req *stacks.CloseStackPlan_Request) (*stacks.CloseStackPlan_Response, error) { + hnd := handle[*stackplan.Plan](req.PlanHandle) + err := s.handles.CloseStackPlan(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return &stacks.CloseStackPlan_Response{}, nil +} + +func (s *stacksServer) ApplyStackChanges(req *stacks.ApplyStackChanges_Request, evts stacks.Stacks_ApplyStackChangesServer) error { + ctx := evts.Context() + syncEvts := newSyncStreamingRPCSender(evts) + evts = nil // Prevent accidental unsynchronized usage of this server + + if req.PlanHandle != 0 && len(req.PlannedChanges) != 0 { + return status.Error(codes.InvalidArgument, "must not set both plan_handle and planned_changes") + } + var plan *stackplan.Plan + if req.PlanHandle != 0 { + planHnd := handle[*stackplan.Plan](req.PlanHandle) + plan = s.handles.StackPlan(planHnd) + if plan == nil { + return status.Error(codes.InvalidArgument, "the given plan handle is invalid") + } + // The plan handle is immediately invalidated by trying to apply it; + // plans are not reusable because they are valid only against the + // exact prior state they were generated for. + if err := s.handles.CloseStackPlan(planHnd); err != nil { + // It would be very strange to get here! + return status.Error(codes.Internal, "failed to close the plan handle") + } + } else { + // Deprecated: whole plan specified inline + // FIXME: Remove this old field once our existing clients are updated. + var err error + plan, err = stackplan.LoadFromProto(req.PlannedChanges) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid planned_changes: %s", err) + } + } + + cfgHnd := handle[*stackconfig.Config](req.StackConfigHandle) + cfg := s.handles.StackConfig(cfgHnd) + if cfg == nil { + return status.Error(codes.InvalidArgument, "the given stack configuration handle is invalid") + } + depsHnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + var deps *depsfile.Locks + if !depsHnd.IsNil() { + deps = s.handles.DependencyLocks(depsHnd) + if deps == nil { + return status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid") + } + } else { + deps = depsfile.NewLocks() + } + providerCacheHnd := handle[*providercache.Dir](req.ProviderCacheHandle) + var providerCache *providercache.Dir + if !providerCacheHnd.IsNil() { + providerCache = s.handles.ProviderPluginCache(providerCacheHnd) + if providerCache == nil { + return status.Error(codes.InvalidArgument, "the given provider cache handle is invalid") + } + } + // NOTE: providerCache can be nil if no handle was provided, in which + // case the call can only use built-in providers. All code below + // must avoid panicking when providerCache is nil, but is allowed to + // return an InvalidArgument error in that case. + // (providerFactoriesForLocks explicitly supports a nil providerCache) + providerFactories, err := providerFactoriesForLocks(deps, providerCache) + if err != nil { + return status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err) + } + + inputValues, err := externalInputValuesFromProto(req.InputValues) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid input values: %s", err) + } + + // We'll hook some internal events in the planning process both to generate + // tracing information if we're in an OpenTelemetry-aware context and + // to propagate a subset of the events to our client. + hooks := stackApplyHooks(syncEvts, cfg.Root.Stack.SourceAddr) + ctx = stackruntime.ContextWithHooks(ctx, hooks) + + changesCh := make(chan stackstate.AppliedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + rtReq := stackruntime.ApplyRequest{ + Config: cfg, + InputValues: inputValues, + ProviderFactories: providerFactories, + Plan: plan, + ExperimentsAllowed: s.experimentsAllowed, + DependencyLocks: *deps, + } + rtResp := stackruntime.ApplyResponse{ + AppliedChanges: changesCh, + Diagnostics: diagsCh, + } + + // As a long-running operation, the apply RPC must be able to be stopped. + // We do this by requesting a stop channel from the stopper, and using it + // to cancel the planning process. + stopCh := s.stopper.add() + defer s.stopper.remove(stopCh) + + // We create a new cancellable context for the stack plan operation to + // allow us to respond to stop requests. + applyCtx, cancelApply := context.WithCancel(ctx) + defer cancelApply() + + // The actual apply operation runs in the background, and emits events + // to us via the channels in rtResp before finally closing changesCh + // to signal that the process is complete. + go stackruntime.Apply(applyCtx, &rtReq, &rtResp) + + emitDiag := func(diag tfdiags.Diagnostic) { + diags := tfdiags.Diagnostics{diag} + protoDiags := diagnosticsToProto(diags) + for _, protoDiag := range protoDiags { + syncEvts.Send(&stacks.ApplyStackChanges_Event{ + Event: &stacks.ApplyStackChanges_Event_Diagnostic{ + Diagnostic: protoDiag, + }, + }) + } + } + + // There is no strong ordering between the planned changes and the + // diagnostics, so we need to be prepared for them to arrive in any + // order. However, stackruntime.Apply does guarantee that it will + // close changesCh only after it's finished writing to and closing + // everything else, and so we can assume that once changesCh is + // closed we only need to worry about whatever's left in the + // diagsCh buffer. +Events: + for { + select { + + case change, ok := <-changesCh: + if !ok { + if diagsCh != nil { + // Async work is done! We do still need to consume the rest + // of diagsCh before we stop, though, because there might + // be some extras in the channel's buffer that we didn't + // get to yet. + for diag := range diagsCh { + emitDiag(diag) + } + } + break Events + } + + protoChange, err := change.AppliedChangeProto() + if err != nil { + // Should not get here: it always indicates a bug in + // AppliedChangeProto or in the code which constructed + // the change over in package stackeval. + // If we get here then it's likely that something will be + // left stale in the final stack state, so we should really + // avoid ever getting here. + emitDiag(tfdiags.Sourceless( + tfdiags.Error, + "Incorrectly-constructed apply result", + fmt.Sprintf( + "Failed to serialize a %T value for recording in the updated state: %s.\n\nThis is a bug in Terraform; please report it!", + protoChange, err, + ), + )) + continue + } + + syncEvts.Send(&stacks.ApplyStackChanges_Event{ + Event: &stacks.ApplyStackChanges_Event_AppliedChange{ + AppliedChange: protoChange, + }, + }) + + case diag, ok := <-diagsCh: + if !ok { + // The diagnostics channel has closed, so we'll just stop + // trying to read from it and wait for changesCh to close, + // which will be our final signal that everything is done. + diagsCh = nil + continue + } + emitDiag(diag) + + case <-stopCh: + // If our stop channel is signalled, we need to cancel the apply. + // This may result in remaining changes or diagnostics being + // emitted, so we continue to monitor those channels if they're + // still active. + cancelApply() + + } + } + + return nil +} + +func (s *stacksServer) OpenStackInspector(ctx context.Context, req *stacks.OpenStackInspector_Request) (*stacks.OpenStackInspector_Response, error) { + cfgHnd := handle[*stackconfig.Config](req.StackConfigHandle) + cfg := s.handles.StackConfig(cfgHnd) + if cfg == nil { + return nil, status.Error(codes.InvalidArgument, "the given stack configuration handle is invalid") + } + depsHnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + var deps *depsfile.Locks + if !depsHnd.IsNil() { + deps = s.handles.DependencyLocks(depsHnd) + if deps == nil { + return nil, status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid") + } + } else { + deps = depsfile.NewLocks() + } + providerCacheHnd := handle[*providercache.Dir](req.ProviderCacheHandle) + var providerCache *providercache.Dir + if !providerCacheHnd.IsNil() { + providerCache = s.handles.ProviderPluginCache(providerCacheHnd) + if providerCache == nil { + return nil, status.Error(codes.InvalidArgument, "the given provider cache handle is invalid") + } + } + // NOTE: providerCache can be nil if no handle was provided, in which + // case the call can only use built-in providers. All code below + // must avoid panicking when providerCache is nil, but is allowed to + // return an InvalidArgument error in that case. + // (providerFactoriesForLocks explicitly supports a nil providerCache) + providerFactories, err := providerFactoriesForLocks(deps, providerCache) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err) + } + inputValues, err := externalInputValuesFromProto(req.InputValues) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid input values: %s", err) + } + state, err := stackstate.LoadFromProto(req.State) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "can't load state snapshot: %s", err) + } + + hnd := s.handles.NewStackInspector(&stacksInspector{ + Config: cfg, + State: state, + ProviderFactories: providerFactories, + InputValues: inputValues, + ExperimentsAllowed: s.experimentsAllowed, + }) + + return &stacks.OpenStackInspector_Response{ + StackInspectorHandle: hnd.ForProtobuf(), + // There are currently no situations that return diagnostics, but + // we reserve the right to add some later. + }, nil +} + +func (s *stacksServer) ListResourceIdentities(ctx context.Context, req *stacks.ListResourceIdentities_Request) (*stacks.ListResourceIdentities_Response, error) { + hnd := handle[*stackstate.State](req.StateHandle) + stackState := s.handles.StackState(hnd) + if stackState == nil { + return nil, status.Error(codes.InvalidArgument, "the given stack state handle is invalid") + } + + depsHnd := handle[*depsfile.Locks](req.DependencyLocksHandle) + var deps *depsfile.Locks + if !depsHnd.IsNil() { + deps = s.handles.DependencyLocks(depsHnd) + if deps == nil { + return nil, status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid") + } + } else { + deps = depsfile.NewLocks() + } + providerCacheHnd := handle[*providercache.Dir](req.ProviderCacheHandle) + var providerCache *providercache.Dir + if !providerCacheHnd.IsNil() { + providerCache = s.handles.ProviderPluginCache(providerCacheHnd) + if providerCache == nil { + return nil, status.Error(codes.InvalidArgument, "the given provider cache handle is invalid") + } + } + // NOTE: providerCache can be nil if no handle was provided, in which + // case the call can only use built-in providers. All code below + // must avoid panicking when providerCache is nil, but is allowed to + // return an InvalidArgument error in that case. + // (providerFactoriesForLocks explicitly supports a nil providerCache) + providerFactories, err := providerFactoriesForLocks(deps, providerCache) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err) + } + + identitySchemas := make(map[addrs.Provider]map[string]providers.IdentitySchema) + for name, factory := range providerFactories { + provider, err := factory() + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "provider %s failed to initialize: %s", name, err) + } + + schema := provider.GetResourceIdentitySchemas() + if len(schema.Diagnostics) > 0 { + return nil, status.Errorf(codes.InvalidArgument, "provider %s failed to retrieve schema: %s", name, schema.Diagnostics.Err()) + } else { + identitySchemas[name] = schema.IdentityTypes + } + } + + resourceIdentities, err := listResourceIdentities(stackState, identitySchemas) + if err != nil { + return nil, err + } + + return &stacks.ListResourceIdentities_Response{ + Resource: resourceIdentities, + }, nil +} + +func (s *stacksServer) InspectExpressionResult(ctx context.Context, req *stacks.InspectExpressionResult_Request) (*stacks.InspectExpressionResult_Response, error) { + hnd := handle[*stacksInspector](req.StackInspectorHandle) + insp := s.handles.StackInspector(hnd) + if insp == nil { + return nil, status.Error(codes.InvalidArgument, "the given stack inspector handle is invalid") + } + return insp.InspectExpressionResult(ctx, req) +} + +func (s *stacksServer) OpenTerraformState(ctx context.Context, request *stacks.OpenTerraformState_Request) (*stacks.OpenTerraformState_Response, error) { + switch data := request.State.(type) { + case *stacks.OpenTerraformState_Request_ConfigPath: + // Load the state from the backend. + // This function should return an empty state even if the diags + // has errors. This makes it easier for the caller, as they should + // close the state handle regardless of the diags. + loader := stackmigrate.Loader{Discovery: s.services} + state, diags := loader.LoadState(data.ConfigPath) + + hnd := s.handles.NewTerraformState(state) + return &stacks.OpenTerraformState_Response{ + StateHandle: hnd.ForProtobuf(), + Diagnostics: diagnosticsToProto(diags), + }, nil + + case *stacks.OpenTerraformState_Request_Raw: + // load the state from the raw data + file, err := statefile.Read(bytes.NewReader(data.Raw)) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid raw state data: %s", err) + } + + hnd := s.handles.NewTerraformState(file.State) + return &stacks.OpenTerraformState_Response{ + StateHandle: hnd.ForProtobuf(), + }, nil + + default: + return nil, status.Error(codes.InvalidArgument, "invalid state source") + } +} + +func (s *stacksServer) CloseTerraformState(ctx context.Context, request *stacks.CloseTerraformState_Request) (*stacks.CloseTerraformState_Response, error) { + hnd := handle[*states.State](request.StateHandle) + err := s.handles.CloseTerraformState(hnd) + if err != nil { + return nil, status.Error(codes.InvalidArgument, err.Error()) + } + return new(stacks.CloseTerraformState_Response), nil +} + +func (s *stacksServer) MigrateTerraformState(request *stacks.MigrateTerraformState_Request, server stacks.Stacks_MigrateTerraformStateServer) error { + + previousStateHandle := handle[*states.State](request.StateHandle) + previousState := s.handles.TerraformState(previousStateHandle) + if previousState == nil { + return status.Error(codes.InvalidArgument, "the given state handle is invalid") + } + + configHandle := handle[*stackconfig.Config](request.ConfigHandle) + config := s.handles.StackConfig(configHandle) + if config == nil { + return status.Error(codes.InvalidArgument, "the given config handle is invalid") + } + + dependencyLocksHandle := handle[*depsfile.Locks](request.DependencyLocksHandle) + dependencyLocks := s.handles.DependencyLocks(dependencyLocksHandle) + if dependencyLocks == nil { + return status.Error(codes.InvalidArgument, "the given dependency locks handle is invalid") + } + + var providerFactories map[addrs.Provider]providers.Factory + if s.providerCacheOverride != nil { + // This is only used in tests to side load providers without needing a + // real provider cache. + providerFactories = s.providerCacheOverride + } else { + providerCacheHandle := handle[*providercache.Dir](request.ProviderCacheHandle) + providerCache := s.handles.ProviderPluginCache(providerCacheHandle) + if providerCache == nil { + return status.Error(codes.InvalidArgument, "the given provider cache handle is invalid") + } + + var err error + providerFactories, err = providerFactoriesForLocks(dependencyLocks, providerCache) + if err != nil { + return status.Errorf(codes.InvalidArgument, "provider dependencies are inconsistent: %s", err) + } + } + + migrate := &stackmigrate.Migration{ + Providers: providerFactories, + PreviousState: previousState, + Config: config, + } + + emit := func(change stackstate.AppliedChange) { + proto, err := change.AppliedChangeProto() + if err != nil { + server.Send(&stacks.MigrateTerraformState_Event{ + Result: &stacks.MigrateTerraformState_Event_Diagnostic{ + Diagnostic: &terraform1.Diagnostic{ + Severity: terraform1.Diagnostic_ERROR, + Summary: "Failed to serialize change", + Detail: fmt.Sprintf("Failed to serialize state change for recording in the migration plan: %s", err), + }, + }, + }) + return + } + + server.Send(&stacks.MigrateTerraformState_Event{ + Result: &stacks.MigrateTerraformState_Event_AppliedChange{ + AppliedChange: proto, + }, + }) + } + + emitDiag := func(diagnostic tfdiags.Diagnostic) { + server.Send(&stacks.MigrateTerraformState_Event{ + Result: &stacks.MigrateTerraformState_Event_Diagnostic{ + Diagnostic: diagnosticToProto(diagnostic), + }, + }) + } + + mapping := request.GetMapping() + if mapping == nil { + return status.Error(codes.InvalidArgument, "missing migration mapping") + } + switch mapping := mapping.(type) { + case *stacks.MigrateTerraformState_Request_Simple: + migrate.Migrate( + mapping.Simple.ResourceAddressMap, + mapping.Simple.ModuleAddressMap, + emit, emitDiag) + default: + return status.Error(codes.InvalidArgument, "unsupported migration mapping") + } + + return nil +} + +func stackPlanHooks(evts *syncPlanStackChangesServer, mainStackSource sourceaddrs.FinalSource) *stackruntime.Hooks { + return stackChangeHooks( + func(scp *stacks.StackChangeProgress) error { + return evts.Send(&stacks.PlanStackChanges_Event{ + Event: &stacks.PlanStackChanges_Event_Progress{ + Progress: scp, + }, + }) + }, + mainStackSource, + ) +} + +func stackApplyHooks(evts *syncApplyStackChangesServer, mainStackSource sourceaddrs.FinalSource) *stackruntime.Hooks { + return stackChangeHooks( + func(scp *stacks.StackChangeProgress) error { + return evts.Send(&stacks.ApplyStackChanges_Event{ + Event: &stacks.ApplyStackChanges_Event_Progress{ + Progress: scp, + }, + }) + }, + mainStackSource, + ) +} + +// stackChangeHooks is the shared hook-handling logic for both [stackPlanHooks] +// and [stackApplyHooks]. Each phase emits a different subset of the events +// handled here. +func stackChangeHooks(send func(*stacks.StackChangeProgress) error, mainStackSource sourceaddrs.FinalSource) *stackruntime.Hooks { + return &stackruntime.Hooks{ + // For any BeginFunc-shaped hook that returns an OpenTelemetry tracing + // span, we'll wrap it in a context so that the runtime's downstream + // operations will appear as children of it. + ContextAttach: func(parent context.Context, tracking any) context.Context { + span, ok := tracking.(trace.Span) + if !ok { + return parent + } + return trace.ContextWithSpan(parent, span) + }, + + // For the overall plan operation we don't emit any events to the client, + // since it already knows it has asked us to plan, but we do establish + // a root tracing span for all of the downstream planning operations to + // attach themselves to. + BeginPlan: func(ctx context.Context, s struct{}) any { + _, span := tracer.Start(ctx, "planning", trace.WithAttributes( + attribute.String("main_stack_source", mainStackSource.String()), + )) + return span + }, + EndPlan: func(ctx context.Context, span any, s struct{}) any { + span.(trace.Span).End() + return nil + }, + + // For the overall apply operation we don't emit any events to the client, + // since it already knows it has asked us to apply, but we do establish + // a root tracing span for all of the downstream planning operations to + // attach themselves to. + BeginApply: func(ctx context.Context, s struct{}) any { + _, span := tracer.Start(ctx, "applying", trace.WithAttributes( + attribute.String("main_stack_source", mainStackSource.String()), + )) + return span + }, + EndApply: func(ctx context.Context, span any, s struct{}) any { + span.(trace.Span).End() + return nil + }, + + // After expanding a component, we emit an event to the client to + // list all of the resulting instances. In the common case of an + // unexpanded component, this will be a single address. + ComponentExpanded: func(ctx context.Context, ce *hooks.ComponentInstances) { + ias := make([]string, 0, len(ce.InstanceAddrs)) + for _, ia := range ce.InstanceAddrs { + ias = append(ias, ia.String()) + } + send(&stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_ComponentInstances_{ + ComponentInstances: &stacks.StackChangeProgress_ComponentInstances{ + ComponentAddr: ce.ComponentAddr.String(), + InstanceAddrs: ias, + }, + }, + }) + }, + + // For each component instance, we emit a series of events to the + // client, reporting the status of the plan operation. We also create a + // nested tracing span for the component instance. + PendingComponentInstancePlan: func(ctx context.Context, ci stackaddrs.AbsComponentInstance) { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePending)) + }, + BeginComponentInstancePlan: func(ctx context.Context, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePlanning)) + _, span := tracer.Start(ctx, "planning", trace.WithAttributes( + attribute.String("component_instance", ci.String()), + )) + return span + }, + EndComponentInstancePlan: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePlanned)) + span.(trace.Span).SetStatus(otelCodes.Ok, "planning succeeded") + span.(trace.Span).End() + return nil + }, + ErrorComponentInstancePlan: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstanceErrored)) + span.(trace.Span).SetStatus(otelCodes.Error, "planning failed") + span.(trace.Span).End() + return nil + }, + DeferComponentInstancePlan: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstanceDeferred)) + span.(trace.Span).SetStatus(otelCodes.Error, "planning succeeded, but deferred") + span.(trace.Span).End() + return nil + }, + PendingComponentInstanceApply: func(ctx context.Context, ci stackaddrs.AbsComponentInstance) { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstancePending)) + }, + BeginComponentInstanceApply: func(ctx context.Context, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstanceApplying)) + _, span := tracer.Start(ctx, "applying", trace.WithAttributes( + attribute.String("component_instance", ci.String()), + )) + return span + }, + EndComponentInstanceApply: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstanceApplied)) + span.(trace.Span).SetStatus(otelCodes.Ok, "applying succeeded") + span.(trace.Span).End() + return nil + }, + ErrorComponentInstanceApply: func(ctx context.Context, span any, ci stackaddrs.AbsComponentInstance) any { + send(evtComponentInstanceStatus(ci, hooks.ComponentInstanceErrored)) + span.(trace.Span).SetStatus(otelCodes.Error, "applying failed") + span.(trace.Span).End() + return nil + }, + + // When Terraform core reports a resource instance plan status, we + // forward it to the events client. + ReportResourceInstanceStatus: func(ctx context.Context, span any, rihd *hooks.ResourceInstanceStatusHookData) any { + // addrs.Provider.String() will panic on the zero value. In this + // case, holding a zero provider would mean a bug in our event + // logging code rather than in core logic, so avoid exploding, but + // send a blank string to expose the error later. + providerAddr := "" + if !rihd.ProviderAddr.IsZero() { + providerAddr = rihd.ProviderAddr.String() + } + send(&stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: stacks.NewResourceInstanceObjectInStackAddr(rihd.Addr), + Status: rihd.Status.ForProtobuf(), + ProviderAddr: providerAddr, + }, + }, + }) + return span + }, + + // Upon completion of a component instance plan, we emit a planned + // change sumary event to the client for each resource instance. + ReportResourceInstancePlanned: func(ctx context.Context, span any, ric *hooks.ResourceInstanceChange) any { + span.(trace.Span).AddEvent("planned resource instance", trace.WithAttributes( + attribute.String("component_instance", ric.Addr.Component.String()), + attribute.String("resource_instance", ric.Addr.Item.String()), + )) + + ripc, err := resourceInstancePlanned(ric) + if err != nil { + return span + } + + send(&stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_ResourceInstancePlannedChange_{ + ResourceInstancePlannedChange: ripc, + }, + }) + return span + }, + + ReportResourceInstanceDeferred: func(ctx context.Context, span any, change *hooks.DeferredResourceInstanceChange) any { + span.(trace.Span).AddEvent("deferred resource instance", trace.WithAttributes( + attribute.String("component_instance", change.Change.Addr.Component.String()), + attribute.String("resource_instance", change.Change.Addr.Item.String()), + )) + + ripc, err := resourceInstancePlanned(change.Change) + if err != nil { + return span + } + + deferred := stackplan.EncodeDeferred(change.Reason) + + send(&stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_DeferredResourceInstancePlannedChange_{ + DeferredResourceInstancePlannedChange: &stacks.StackChangeProgress_DeferredResourceInstancePlannedChange{ + Change: ripc, + Deferred: deferred, + }, + }, + }) + return span + }, + + // We also report a roll-up of planned resource action counts after each + // component instance plan or apply completes. + ReportComponentInstancePlanned: func(ctx context.Context, span any, cic *hooks.ComponentInstanceChange) any { + send(&stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(cic.Addr).String(), + ComponentInstanceAddr: cic.Addr.String(), + }, + Total: int32(cic.Total()), + Add: int32(cic.Add), + Change: int32(cic.Change), + Import: int32(cic.Import), + Remove: int32(cic.Remove), + Defer: int32(cic.Defer), + Move: int32(cic.Move), + Forget: int32(cic.Forget), + }, + }, + }) + return span + }, + // The apply rollup should typically report the same information as + // the plan one did earlier, but could vary in some situations if + // e.g. a planned update turned out to be a no-op once some unknown + // values were known, or if the apply phase is handled by a different + // version of the agent than the plan phase which has support for + // a different set of possible change types. + ReportComponentInstanceApplied: func(ctx context.Context, span any, cic *hooks.ComponentInstanceChange) any { + send(&stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(cic.Addr).String(), + ComponentInstanceAddr: cic.Addr.String(), + }, + Total: int32(cic.Total()), + Add: int32(cic.Add), + Change: int32(cic.Change), + Import: int32(cic.Import), + Remove: int32(cic.Remove), + Defer: int32(cic.Defer), + Move: int32(cic.Move), + Forget: int32(cic.Forget), + }, + }, + }) + return span + }, + } +} + +func resourceInstancePlanned(ric *hooks.ResourceInstanceChange) (*stacks.StackChangeProgress_ResourceInstancePlannedChange, error) { + actions, err := stacks.ChangeTypesForPlanAction(ric.Change.Action) + if err != nil { + return nil, err + } + + var moved *stacks.StackChangeProgress_ResourceInstancePlannedChange_Moved + if !ric.Change.PrevRunAddr.Equal(ric.Change.Addr) { + moved = &stacks.StackChangeProgress_ResourceInstancePlannedChange_Moved{ + PrevAddr: &stacks.ResourceInstanceInStackAddr{ + ComponentInstanceAddr: ric.Addr.Component.String(), + ResourceInstanceAddr: ric.Change.PrevRunAddr.String(), + }, + } + } + + var imported *stacks.StackChangeProgress_ResourceInstancePlannedChange_Imported + if ric.Change.Importing != nil { + imported = &stacks.StackChangeProgress_ResourceInstancePlannedChange_Imported{ + ImportId: ric.Change.Importing.ID, + Unknown: ric.Change.Importing.Unknown, + } + } + + return &stacks.StackChangeProgress_ResourceInstancePlannedChange{ + Addr: stacks.NewResourceInstanceObjectInStackAddr(ric.Addr), + Actions: actions, + Moved: moved, + Imported: imported, + ProviderAddr: ric.Change.ProviderAddr.Provider.String(), + }, nil +} + +func evtComponentInstanceStatus(ci stackaddrs.AbsComponentInstance, status hooks.ComponentInstanceStatus) *stacks.StackChangeProgress { + return &stacks.StackChangeProgress{ + Event: &stacks.StackChangeProgress_ComponentInstanceStatus_{ + ComponentInstanceStatus: &stacks.StackChangeProgress_ComponentInstanceStatus{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(ci).String(), + ComponentInstanceAddr: ci.String(), + }, + Status: status.ForProtobuf(), + }, + }, + } +} + +// syncPlanStackChangesServer is a wrapper around a +// stacks.Stacks_PlanStackChangesServer implementation that makes the +// Send method concurrency-safe by holding a mutex throughout the underlying +// call. +type syncPlanStackChangesServer = syncStreamingRPCSender[stacks.Stacks_PlanStackChangesServer, *stacks.PlanStackChanges_Event] + +// syncApplyStackChangesServer is a wrapper around a +// stacks.Stacks_ApplyStackChangesServer implementation that makes the +// Send method concurrency-safe by holding a mutex throughout the underlying +// call. +type syncApplyStackChangesServer = syncStreamingRPCSender[stacks.Stacks_ApplyStackChangesServer, *stacks.ApplyStackChanges_Event] diff --git a/internal/rpcapi/stacks_inspector.go b/internal/rpcapi/stacks_inspector.go new file mode 100644 index 0000000000..8f9d0b1c93 --- /dev/null +++ b/internal/rpcapi/stacks_inspector.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackruntime" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// stacksInspector is the backing representation of a "stack inspector handle" +// as exposed in the stacks part of the RPC API, which allows a caller to +// provide what they want to inspect just once and then perform any number +// of subsequent inspection actions against it. +type stacksInspector struct { + Config *stackconfig.Config + State *stackstate.State + ProviderFactories map[addrs.Provider]providers.Factory + InputValues map[stackaddrs.InputVariable]stackruntime.ExternalInputValue + ExperimentsAllowed bool +} + +// InspectExpressionResult evaluates a given expression string in the +// inspection environment represented by the receiver. +func (i *stacksInspector) InspectExpressionResult(ctx context.Context, req *stacks.InspectExpressionResult_Request) (*stacks.InspectExpressionResult_Response, error) { + var diags tfdiags.Diagnostics + + expr, hclDiags := hclsyntax.ParseExpression(req.ExpressionSrc, "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return &stacks.InspectExpressionResult_Response{ + Diagnostics: diagnosticsToProto(diags), + }, nil + } + + stackAddr := stackaddrs.RootStackInstance + if req.StackAddr != "" { + // FIXME: Support this later. We don't currently have a stack instance + // address parser to parse this input with, but we could build one + // in future. + return nil, status.Error(codes.InvalidArgument, "the InspectExpressionResult operation currently only supports evaluating in the topmost stack") + } + + val, moreDiags := stackruntime.EvalExpr(ctx, expr, &stackruntime.EvalExprRequest{ + Config: i.Config, + State: i.State, + EvalStackInstance: stackAddr, + InputValues: i.InputValues, + ProviderFactories: i.ProviderFactories, + ExperimentsAllowed: i.ExperimentsAllowed, + }) + diags = diags.Append(moreDiags) + if val == cty.NilVal { + // Too invalid to return any value at all, then. + return &stacks.InspectExpressionResult_Response{ + Diagnostics: diagnosticsToProto(diags), + }, nil + } + + result, err := stacks.ToDynamicValue(val, cty.DynamicPseudoType) + if err != nil { + // We might get here if the result was of a type we cannot send + // over the wire, such as a reference to a provider configuration. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Result is not serializable", + fmt.Sprintf("Cannot return the result of the given expression: %s.", err), + )) + } + + return &stacks.InspectExpressionResult_Response{ + Result: result, + Diagnostics: diagnosticsToProto(diags), + }, nil +} diff --git a/internal/rpcapi/stacks_test.go b/internal/rpcapi/stacks_test.go new file mode 100644 index 0000000000..e9091fc75f --- /dev/null +++ b/internal/rpcapi/stacks_test.go @@ -0,0 +1,1299 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "context" + "io" + "maps" + "slices" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/terraform-svchost/disco" + "github.com/zclconf/go-cty/cty" + ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackmigrate" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/version" +) + +func TestStacksOpenCloseStackConfiguration(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + // In normal use a client would have previously opened a source bundle + // using Dependencies.OpenSourceBundle, so we'll simulate the effect + // of that here. + var sourcesHnd handle[*sourcebundle.Bundle] + { + sources, err := sourcebundle.OpenDir("testdata/sourcebundle") + if err != nil { + t.Fatal(err) + } + sourcesHnd = handles.NewSourceBundle(sources) + } + + openResp, err := stacksServer.OpenStackConfiguration(ctx, &stacks.OpenStackConfiguration_Request{ + SourceBundleHandle: sourcesHnd.ForProtobuf(), + SourceAddress: &terraform1.SourceAddress{ + Source: "git::https://example.com/foo.git", + }, + }) + if err != nil { + t.Fatal(err) + } + + // A client wouldn't normally be able to interact directly with the + // stack configuration, but we're doing that here to simulate what would + // happen in another service that takes configuration handles as input. + { + hnd := handle[*stackconfig.Config](openResp.StackConfigHandle) + cfg := handles.StackConfig(hnd) + if cfg == nil { + t.Fatal("returned stack config handle is invalid") + } + } + + // A hypothetical attempt to close the underlying source bundle while + // the stack configuration is active should fail. + { + depsServer := newDependenciesServer(handles, disco.New()) + + _, err := depsServer.CloseSourceBundle(ctx, &dependencies.CloseSourceBundle_Request{ + SourceBundleHandle: sourcesHnd.ForProtobuf(), + }) + if err == nil { + t.Fatal("successfully closed source bundle while stack config was using it; should have failed to close") + } + protoStatus, ok := status.FromError(err) + if !ok { + t.Fatal("error is not a protobuf status code") + } + if got, want := protoStatus.Code(), codes.InvalidArgument; got != want { + t.Errorf("wrong error status\ngot: %s\nwant: %s", got, want) + } + if got, want := protoStatus.Message(), "handle is in use by another open handle"; got != want { + t.Errorf("wrong error message\ngot: %s\nwant: %s", got, want) + } + } + + _, err = stacksServer.CloseStackConfiguration(ctx, &stacks.CloseStackConfiguration_Request{ + StackConfigHandle: openResp.StackConfigHandle, + }) + if err != nil { + t.Fatal(err) + } + + // Should be able to close the source bundle now too. + { + depsServer := newDependenciesServer(handles, disco.New()) + + _, err := depsServer.CloseSourceBundle(ctx, &dependencies.CloseSourceBundle_Request{ + SourceBundleHandle: sourcesHnd.ForProtobuf(), + }) + if err != nil { + t.Fatalf("failed to close the source bundle: %s", err.Error()) + } + } +} + +func TestStacksFindStackConfigurationComponents(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + // In normal use a client would have previously opened a source bundle + // using Dependencies.OpenSourceBundle, so we'll simulate the effect + // of that here. + var sourcesHnd handle[*sourcebundle.Bundle] + { + sources, err := sourcebundle.OpenDir("testdata/sourcebundle") + if err != nil { + t.Fatal(err) + } + sourcesHnd = handles.NewSourceBundle(sources) + } + + t.Run("empty config", func(t *testing.T) { + openResp, err := stacksServer.OpenStackConfiguration(ctx, &stacks.OpenStackConfiguration_Request{ + SourceBundleHandle: sourcesHnd.ForProtobuf(), + SourceAddress: &terraform1.SourceAddress{ + Source: "git::https://example.com/foo.git", + }, + }) + if err != nil { + t.Fatal(err) + } + if len(openResp.Diagnostics) != 0 { + t.Error("empty configuration generated diagnostics; expected none") + if openResp.StackConfigHandle == 0 { + return // Our later operations will fail if given the nil handle + } + } + + cmpntResp, err := stacksServer.FindStackConfigurationComponents(ctx, &stacks.FindStackConfigurationComponents_Request{ + StackConfigHandle: openResp.StackConfigHandle, + }) + if err != nil { + t.Fatal(err) + } + + got := cmpntResp.Config + want := &stacks.FindStackConfigurationComponents_StackConfig{ + // Intentionally empty, because the configuration we've loaded + // is itself empty. + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + t.Run("non-empty config", func(t *testing.T) { + openResp, err := stacksServer.OpenStackConfiguration(ctx, &stacks.OpenStackConfiguration_Request{ + SourceBundleHandle: sourcesHnd.ForProtobuf(), + SourceAddress: &terraform1.SourceAddress{ + Source: "git::https://example.com/foo.git//non-empty-stack", + }, + }) + if err != nil { + t.Fatal(err) + } + if len(openResp.Diagnostics) != 0 { + t.Error("empty configuration generated diagnostics; expected none") + if openResp.StackConfigHandle == 0 { + return // Our later operations will fail if given the nil handle + } + } + + cmpntResp, err := stacksServer.FindStackConfigurationComponents(ctx, &stacks.FindStackConfigurationComponents_Request{ + StackConfigHandle: openResp.StackConfigHandle, + }) + if err != nil { + t.Fatal(err) + } + + got := cmpntResp.Config + want := &stacks.FindStackConfigurationComponents_StackConfig{ + Components: map[string]*stacks.FindStackConfigurationComponents_Component{ + "single": { + SourceAddr: "git::https://example.com/foo.git//non-empty-stack/empty-module", + ComponentAddr: "component.single", + }, + "for_each": { + SourceAddr: "git::https://example.com/foo.git//non-empty-stack/empty-module", + Instances: stacks.FindStackConfigurationComponents_FOR_EACH, + ComponentAddr: "component.for_each", + }, + }, + EmbeddedStacks: map[string]*stacks.FindStackConfigurationComponents_EmbeddedStack{ + "single": { + SourceAddr: "git::https://example.com/foo.git//non-empty-stack/child", + Config: &stacks.FindStackConfigurationComponents_StackConfig{ + Components: map[string]*stacks.FindStackConfigurationComponents_Component{ + "foo": { + SourceAddr: "git::https://example.com/foo.git//non-empty-stack/empty-module", + ComponentAddr: "stack.single.component.foo", + }, + }, + }, + }, + "for_each": { + SourceAddr: "git::https://example.com/foo.git//non-empty-stack/child", + Instances: stacks.FindStackConfigurationComponents_FOR_EACH, + Config: &stacks.FindStackConfigurationComponents_StackConfig{ + Components: map[string]*stacks.FindStackConfigurationComponents_Component{ + "foo": { + SourceAddr: "git::https://example.com/foo.git//non-empty-stack/empty-module", + ComponentAddr: "stack.for_each.component.foo", + }, + }, + }, + }, + }, + InputVariables: map[string]*stacks.FindStackConfigurationComponents_InputVariable{ + "unused": {Optional: false}, + "unused_with_default": {Optional: true}, + "sensitive": {Sensitive: true}, + "ephemeral": {Ephemeral: true}, + }, + OutputValues: map[string]*stacks.FindStackConfigurationComponents_OutputValue{ + "normal": {}, + "sensitive": {Sensitive: true}, + "ephemeral": {Ephemeral: true}, + }, + } + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) +} + +func TestStacksOpenState(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + stacksClient := stacks.NewStacksClient(grpcClient) + stream, err := stacksClient.OpenState(ctx) + if err != nil { + t.Fatal(err) + } + + send := func(t *testing.T, key string, msg proto.Message) { + rawMsg, err := anypb.New(msg) + if err != nil { + t.Fatalf("failed to encode %T message %q: %s", msg, key, err) + } + err = stream.Send(&stacks.OpenStackState_RequestItem{ + Raw: &stacks.AppliedChange_RawChange{ + Key: key, + Value: rawMsg, + }, + }) + if err != nil { + t.Fatalf("failed to send %T message %q: %s", msg, key, err) + } + } + send(t, "CMPTcomponent.foo", &tfstackdata1.StateComponentInstanceV1{}) + + resp, err := stream.CloseAndRecv() + if err != nil { + t.Fatal(err) + } + hnd := handle[*stackstate.State](resp.StateHandle) + state := handles.StackState(hnd) + if state == nil { + t.Fatalf("returned handle %d does not refer to a stack prior state", resp.StateHandle) + } + + // The state should know about component.foo from the message we sent above. + wantComponentInstAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + } + if !state.HasComponentInstance(wantComponentInstAddr) { + t.Errorf("state does not track %s", wantComponentInstAddr) + } + + _, err = stacksClient.CloseState(ctx, &stacks.CloseStackState_Request{ + StateHandle: resp.StateHandle, + }) + if err != nil { + t.Errorf("failed to close the prior state handle: %s", err) + } +} + +func TestStacksOpenPlan(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + stacksClient := stacks.NewStacksClient(grpcClient) + stream, err := stacksClient.OpenPlan(ctx) + if err != nil { + t.Fatal(err) + } + + send := func(t *testing.T, msg proto.Message) { + rawMsg, err := anypb.New(msg) + if err != nil { + t.Fatalf("failed to encode %T message: %s", msg, err) + } + err = stream.Send(&stacks.OpenStackPlan_RequestItem{ + Raw: rawMsg, + }) + if err != nil { + t.Fatalf("failed to send %T message: %s", msg, err) + } + } + send(t, &tfstackdata1.PlanHeader{ + TerraformVersion: version.SemVer.String(), + }) + send(t, &tfstackdata1.PlanPriorStateElem{ + // We don't actually analyze or validate these items while + // just loading a plan, so we can safely just put simple + // garbage in here for testing. + Key: "test-foo", + }) + send(t, &tfstackdata1.PlanApplyable{ + Applyable: true, + }) + resp, err := stream.CloseAndRecv() + if err != nil { + t.Fatal(err) + } + + hnd := handle[*stackplan.Plan](resp.PlanHandle) + plan := handles.StackPlan(hnd) + if plan == nil { + t.Fatalf("returned handle %d does not refer to a stack plan", resp.PlanHandle) + } + if !plan.Applyable { + t.Error("plan is not applyable; should've been") + } + if _, exists := plan.PrevRunStateRaw["test-foo"]; !exists { + t.Error("plan is missing the raw state entry for 'test-foo'") + } + + _, err = stacksClient.ClosePlan(ctx, &stacks.CloseStackPlan_Request{ + PlanHandle: resp.PlanHandle, + }) + if err != nil { + t.Errorf("failed to close the plan handle: %s", err) + } +} + +func TestStacksPlanStackChanges(t *testing.T) { + ctx := context.Background() + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + stacksServer.planTimestampOverride = &fakePlanTimestamp + + fakeSourceBundle := &sourcebundle.Bundle{} + bundleHnd := handles.NewSourceBundle(fakeSourceBundle) + emptyConfig := &stackconfig.Config{ + Root: &stackconfig.ConfigNode{ + Stack: &stackconfig.Stack{ + SourceAddr: sourceaddrs.MustParseSource("git::https://example.com/foo.git").(sourceaddrs.RemoteSource), + }, + }, + } + configHnd, err := handles.NewStackConfig(emptyConfig, bundleHnd) + if err != nil { + t.Fatal(err) + } + + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + stacksClient := stacks.NewStacksClient(grpcClient) + events, err := stacksClient.PlanStackChanges(ctx, &stacks.PlanStackChanges_Request{ + PlanMode: stacks.PlanMode_NORMAL, + StackConfigHandle: configHnd.ForProtobuf(), + }) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + wantEvents := splitStackOperationEvents([]*stacks.PlanStackChanges_Event{ + { + Event: &stacks.PlanStackChanges_Event_PlannedChange{ + PlannedChange: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanHeader{ + TerraformVersion: version.SemVer.String(), + }), + }, + }, + }, + }, + { + Event: &stacks.PlanStackChanges_Event_PlannedChange{ + PlannedChange: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanTimestamp{ + PlanTimestamp: fakePlanTimestamp.Format(time.RFC3339), + }), + }, + }, + }, + }, + { + Event: &stacks.PlanStackChanges_Event_PlannedChange{ + PlannedChange: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanApplyable{ + Applyable: true, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{ + PlanApplyable: true, + }, + }, + }, + }, + }, + }, + }) + var gotEventsAll []*stacks.PlanStackChanges_Event + for { + event, err := events.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + gotEventsAll = append(gotEventsAll, event) + } + gotEvents := splitStackOperationEvents(gotEventsAll) + + if diff := cmp.Diff(wantEvents, gotEvents, protocmp.Transform()); diff != "" { + t.Errorf("wrong events\n%s", diff) + } +} + +func TestStackChangeProgress(t *testing.T) { + tcs := map[string]struct { + source string + store *stacks_testing_provider.ResourceStore + state []stackstate.AppliedChange + inputs map[string]cty.Value + want []*stacks.StackChangeProgress + diagnostics []*terraform1.Diagnostic + }{ + "deferred_changes": { + source: "git::https://example.com/bar.git", + want: []*stacks.StackChangeProgress{ + { + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.deferred", + ComponentInstanceAddr: "component.deferred", + }, + Total: 1, + Defer: 1, + }, + }, + }, + { + Event: &stacks.StackChangeProgress_DeferredResourceInstancePlannedChange_{ + DeferredResourceInstancePlannedChange: &stacks.StackChangeProgress_DeferredResourceInstancePlannedChange{ + Deferred: &stacks.Deferred{ + Reason: stacks.Deferred_RESOURCE_CONFIG_UNKNOWN, + }, + Change: &stacks.StackChangeProgress_ResourceInstancePlannedChange{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.deferred", + ResourceInstanceAddr: "testing_deferred_resource.resource", + }, + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.deferred", + ResourceInstanceAddr: "testing_deferred_resource.resource", + }, + Status: stacks.StackChangeProgress_ResourceInstanceStatus_PLANNING, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.deferred", + ResourceInstanceAddr: "testing_deferred_resource.resource", + }, + Status: stacks.StackChangeProgress_ResourceInstanceStatus_PLANNED, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ComponentInstanceStatus_{ + ComponentInstanceStatus: &stacks.StackChangeProgress_ComponentInstanceStatus{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.deferred", + ComponentInstanceAddr: "component.deferred", + }, + Status: stacks.StackChangeProgress_ComponentInstanceStatus_DEFERRED, + }, + }, + }, + }, + }, + "moved": { + source: "git::https://example.com/moved.git", + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("before", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("before"), + "value": cty.NullVal(cty.String), + })). + Build(), + state: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent(t, "component.self"), + ComponentInstanceAddr: mustAbsComponentInstance(t, "component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject(t, "component.self.testing_resource.before"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "before", + "value": nil, + }), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + want: []*stacks.StackChangeProgress{ + { + Event: &stacks.StackChangeProgress_ResourceInstancePlannedChange_{ + ResourceInstancePlannedChange: &stacks.StackChangeProgress_ResourceInstancePlannedChange{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.after", + }, + Actions: []stacks.ChangeType{ + stacks.ChangeType_NOOP, + }, + Moved: &stacks.StackChangeProgress_ResourceInstancePlannedChange_Moved{ + PrevAddr: &stacks.ResourceInstanceInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.before", + }, + }, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.self", + ComponentInstanceAddr: "component.self", + }, + Total: 1, + Move: 1, + }, + }, + }, + }, + }, + "import": { + source: "git::https://example.com/import.git", + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("self", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("self"), + "value": cty.NullVal(cty.String), + })). + Build(), + inputs: map[string]cty.Value{ + "unknown": cty.UnknownVal(cty.String), + }, + want: []*stacks.StackChangeProgress{ + { + + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.unknown", + ComponentInstanceAddr: "component.unknown", + }, + Total: 1, + Defer: 1, + }, + }, + }, + { + Event: &stacks.StackChangeProgress_DeferredResourceInstancePlannedChange_{ + DeferredResourceInstancePlannedChange: &stacks.StackChangeProgress_DeferredResourceInstancePlannedChange{ + Deferred: &stacks.Deferred{ + Reason: stacks.Deferred_RESOURCE_CONFIG_UNKNOWN, + }, + Change: &stacks.StackChangeProgress_ResourceInstancePlannedChange{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.unknown", + ResourceInstanceAddr: "testing_resource.resource", + }, + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + Imported: &stacks.StackChangeProgress_ResourceInstancePlannedChange_Imported{ + Unknown: true, + }, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.unknown", + ResourceInstanceAddr: "testing_resource.resource", + }, + Status: stacks.StackChangeProgress_ResourceInstanceStatus_PLANNING, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.unknown", + ResourceInstanceAddr: "testing_resource.resource", + }, + Status: stacks.StackChangeProgress_ResourceInstanceStatus_PLANNED, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, { + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.self", + ComponentInstanceAddr: "component.self", + }, + Total: 1, + Import: 1, + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstancePlannedChange_{ + ResourceInstancePlannedChange: &stacks.StackChangeProgress_ResourceInstancePlannedChange{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.resource", + }, + Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, + Imported: &stacks.StackChangeProgress_ResourceInstancePlannedChange_Imported{ + ImportId: "self", + }, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.resource", + }, + Status: stacks.StackChangeProgress_ResourceInstanceStatus_PLANNING, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ResourceInstanceStatus_{ + ResourceInstanceStatus: &stacks.StackChangeProgress_ResourceInstanceStatus{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.resource", + }, + Status: stacks.StackChangeProgress_ResourceInstanceStatus_PLANNED, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + }, + }, + "removed": { + source: "git::https://example.com/removed.git", + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("resource", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("resource"), + "value": cty.NullVal(cty.String), + })). + Build(), + state: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent(t, "component.self"), + ComponentInstanceAddr: mustAbsComponentInstance(t, "component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject(t, "component.self.testing_resource.resource"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "resource", + "value": nil, + }), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + want: []*stacks.StackChangeProgress{ + { + Event: &stacks.StackChangeProgress_ResourceInstancePlannedChange_{ + ResourceInstancePlannedChange: &stacks.StackChangeProgress_ResourceInstancePlannedChange{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_resource.resource", + }, + Actions: []stacks.ChangeType{ + stacks.ChangeType_FORGET, + }, + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Event: &stacks.StackChangeProgress_ComponentInstanceChanges_{ + ComponentInstanceChanges: &stacks.StackChangeProgress_ComponentInstanceChanges{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.self", + ComponentInstanceAddr: "component.self", + }, + Total: 1, + Forget: 1, + }, + }, + }, + }, + diagnostics: []*terraform1.Diagnostic{ + { + Severity: terraform1.Diagnostic_WARNING, + Summary: "Some objects will no longer be managed by Terraform", + Detail: "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.", + }, + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + // For this test, we do actually want to use a "real" provider. We'll + // use the providerCacheOverride to side-load the testing provider. + stacksServer.providerCacheOverride = make(map[addrs.Provider]providers.Factory) + stacksServer.providerCacheOverride[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, tc.store), nil + } + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + stacksServer.providerDependencyLockOverride = lock + + sb, err := sourcebundle.OpenDir("testdata/sourcebundle") + if err != nil { + t.Fatal(err) + } + hnd := handles.NewSourceBundle(sb) + + client, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + stacksClient := stacks.NewStacksClient(client) + + open, err := stacksClient.OpenStackConfiguration(ctx, &stacks.OpenStackConfiguration_Request{ + SourceBundleHandle: hnd.ForProtobuf(), + SourceAddress: &terraform1.SourceAddress{ + Source: tc.source, + }, + }) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + defer stacksClient.CloseStackConfiguration(ctx, &stacks.CloseStackConfiguration_Request{ + StackConfigHandle: open.StackConfigHandle, + }) + + resp, err := stacksClient.PlanStackChanges(ctx, &stacks.PlanStackChanges_Request{ + PlanMode: stacks.PlanMode_NORMAL, + StackConfigHandle: open.StackConfigHandle, + PreviousState: appliedChangeToRawState(t, tc.state), + InputValues: func() map[string]*stacks.DynamicValueWithSource { + values := make(map[string]*stacks.DynamicValueWithSource) + for name, value := range tc.inputs { + values[name] = &stacks.DynamicValueWithSource{ + Value: &stacks.DynamicValue{ + Msgpack: mustMsgpack(t, value, value.Type()), + }, + SourceRange: &terraform1.SourceRange{ + Start: &terraform1.SourcePos{}, + End: &terraform1.SourcePos{}, + }, + } + } + return values + }(), + }) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + wantEvents := splitStackOperationEvents(func() []*stacks.PlanStackChanges_Event { + events := make([]*stacks.PlanStackChanges_Event, 0, len(tc.want)) + for _, want := range tc.want { + events = append(events, &stacks.PlanStackChanges_Event{ + Event: &stacks.PlanStackChanges_Event_Progress{ + Progress: want, + }, + }) + } + return events + }()) + + gotEvents := splitStackOperationEvents(func() []*stacks.PlanStackChanges_Event { + var events []*stacks.PlanStackChanges_Event + for { + event, err := resp.Recv() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + events = append(events, event) + } + return events + }()) + + // First, validate the diagnostics. Most of the tests are either + // expecting a specific single diagnostic so we do actually check + // everything. + + diagIx := 0 + for ; diagIx < len(tc.diagnostics); diagIx++ { + if diagIx >= len(gotEvents.Diagnostics) { + // Then we have more expected diagnostics than we got. + t.Errorf("missing expected diagnostic: %v", tc.diagnostics[diagIx]) + continue + } + diag := gotEvents.Diagnostics[diagIx].Event.(*stacks.PlanStackChanges_Event_Diagnostic).Diagnostic + if diff := cmp.Diff(tc.diagnostics[diagIx], diag, protocmp.Transform()); diff != "" { + // Then we have a diagnostic that doesn't match what we + // expected. + t.Errorf("wrong diagnostic\n%s", diff) + } + } + for ; diagIx < len(gotEvents.Diagnostics); diagIx++ { + // Then we have more diagnostics than we expected. + t.Errorf("unexpected diagnostic: %v", gotEvents.Diagnostics[diagIx]) + } + + // Now we're going to manually verify the existence of some key events. + // We're not looking for every event because (a) the exact ordering of + // events is not guaranteed and (b) we don't want to start failing every + // time a new event is added. + + WantPlannedChange: + for _, want := range wantEvents.PlannedChanges { + for _, got := range gotEvents.PlannedChanges { + if len(cmp.Diff(want, got, protocmp.Transform())) == 0 { + continue WantPlannedChange + } + } + t.Errorf("missing expected planned change: %v", want) + } + + WantMiscHook: + for _, want := range wantEvents.MiscHooks { + for _, got := range gotEvents.MiscHooks { + if len(cmp.Diff(want, got, protocmp.Transform())) == 0 { + continue WantMiscHook + } + } + t.Errorf("missing expected event: %v", want) + } + + if t.Failed() { + // if the test failed, let's print out all the events we got to help + // with debugging. + for _, evt := range gotEvents.MiscHooks { + t.Logf(" returned event: %s", evt.String()) + } + + for _, evt := range gotEvents.PlannedChanges { + t.Logf(" returned event: %s", evt.String()) + } + } + }) + } +} + +func TestStacksOpenTerraformState_ConfigPath(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + s := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + statePath := stackmigrate.TestStateFile(t, s) + + stacksClient := stacks.NewStacksClient(grpcClient) + resp, err := stacksClient.OpenTerraformState(ctx, &stacks.OpenTerraformState_Request{ + State: &stacks.OpenTerraformState_Request_ConfigPath{ + ConfigPath: strings.TrimSuffix(statePath, "/terraform.tfstate"), + }, + }) + if err != nil { + t.Fatal(err) + } + + hnd := handle[*states.State](resp.StateHandle) + state := handles.TerraformState(hnd) + if state == nil { + t.Fatalf("returned handle %d does not refer to a Terraform state", resp.StateHandle) + } + + if !statefile.StatesMarshalEqual(s, state) { + t.Fatalf("loaded state does not match original state") + } +} + +func TestStacksOpenTerraformState_Raw(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + s := []byte(`{ + "version": 4, + "terraform_version": "1.12.0", + "serial": 0, + "lineage": "fake-for-testing", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "test_instance", + "name": "foo", + "provider": "provider[\"registry.terraform.io/hashicorp/test\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "bar", + "foo": "value", + "bar": "value" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} +`) + + stacksClient := stacks.NewStacksClient(grpcClient) + resp, err := stacksClient.OpenTerraformState(ctx, &stacks.OpenTerraformState_Request{ + State: &stacks.OpenTerraformState_Request_Raw{ + Raw: s, + }, + }) + if err != nil { + t.Fatal(err) + } + + hnd := handle[*states.State](resp.StateHandle) + state := handles.TerraformState(hnd) + if state == nil { + t.Fatalf("returned handle %d does not refer to a Terraform state", resp.StateHandle) + } + + if !slices.Contains(slices.Collect(maps.Keys(state.Modules[""].Resources)), "test_instance.foo") { + t.Fatalf("loaded state does not contain expected resource") + } +} + +func TestStacksMigrateTerraformState(t *testing.T) { + ctx := context.Background() + + handles := newHandleTable() + stacksServer := newStacksServer(newStopper(), handles, disco.New(), &serviceOpts{}) + + grpcClient, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + stacks.RegisterStacksServer(srv, stacksServer) + }) + defer close() + + s := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_deferred_resource", + Name: "resource", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"hello","value":"world","deferred":false}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("testing"), + Module: addrs.RootModule, + }, + ) + }) + + statePath := stackmigrate.TestStateFile(t, s) + stacksClient := stacks.NewStacksClient(grpcClient) + resp, err := stacksClient.OpenTerraformState(ctx, &stacks.OpenTerraformState_Request{ + State: &stacks.OpenTerraformState_Request_ConfigPath{ + ConfigPath: strings.TrimSuffix(statePath, "/terraform.tfstate"), + }, + }) + if err != nil { + t.Fatal(err) + } + + hnd := handle[*states.State](resp.StateHandle) + state := handles.TerraformState(hnd) + if state == nil { + t.Fatalf("returned handle %d does not refer to a Terraform state", resp.StateHandle) + } + + if !statefile.StatesMarshalEqual(s, state) { + t.Fatalf("loaded state does not match original state") + } + + // up until now is basically what we did in TestStacksOpenTerraformState_ConfigPath + // now we're going to migrate the state and check that the migration worked + + // In normal use a client would have previously opened a source bundle + // using Dependencies.OpenSourceBundle, so we'll simulate the effect + // of that here. + + sources, err := sourcebundle.OpenDir("testdata/sourcebundle") + if err != nil { + t.Fatal(err) + } + sourcesHnd := handles.NewSourceBundle(sources) + + openResp, err := stacksServer.OpenStackConfiguration(ctx, &stacks.OpenStackConfiguration_Request{ + SourceBundleHandle: sourcesHnd.ForProtobuf(), + SourceAddress: &terraform1.SourceAddress{ + Source: "git::https://example.com/baz.git", + }, + }) + if err != nil { + t.Fatalf("unable to open stack configuration: %s", err) + } + + // For this test, we do actually want to use a "real" provider. We'll + // use the providerCacheOverride to side-load the testing provider. + stacksServer.providerCacheOverride = make(map[addrs.Provider]providers.Factory) + stacksServer.providerCacheOverride[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + lockHandle := handles.NewDependencyLocks(lock) + + stream, err := stacksClient.MigrateTerraformState(ctx, &stacks.MigrateTerraformState_Request{ + StateHandle: resp.StateHandle, + ConfigHandle: openResp.StackConfigHandle, + DependencyLocksHandle: lockHandle.ForProtobuf(), + Mapping: &stacks.MigrateTerraformState_Request_Simple{ + Simple: &stacks.MigrateTerraformState_Request_Mapping{ + ResourceAddressMap: map[string]string{ + "testing_deferred_resource.resource": "self", + }, + }, + }, + }) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + gotEvents := []*stacks.MigrateTerraformState_Event{} + for { + event, err := stream.Recv() + if err == io.EOF { + break + } + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + gotEvents = append(gotEvents, event) + } + + wantChanges := []*stacks.AppliedChange_ChangeDescription{ + { + Key: "RSRCcomponent.self,testing_deferred_resource.resource,cur", + Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ + ResourceInstance: &stacks.AppliedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: "component.self", + ResourceInstanceAddr: "testing_deferred_resource.resource", + }, + NewValue: &stacks.DynamicValue{ + Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("hello"), + "value": cty.StringVal("world"), + "deferred": cty.False, + }), cty.Object(map[string]cty.Type{"id": cty.String, "value": cty.String, "deferred": cty.Bool})), + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "testing_deferred_resource", + ProviderAddr: "registry.terraform.io/hashicorp/testing", + }, + }, + }, + { + Key: "CMPTcomponent.self", + Description: &stacks.AppliedChange_ChangeDescription_ComponentInstance{ + ComponentInstance: &stacks.AppliedChange_ComponentInstance{ + ComponentAddr: "component.self", + ComponentInstanceAddr: "component.self", + }, + }, + }, + } + + if len(gotEvents) != len(wantChanges) { + t.Fatalf("expected %d events, got %d", len(wantChanges), len(gotEvents)) + } + + gotChanges := make([]*stacks.AppliedChange_ChangeDescription, len(gotEvents)) + for i, evt := range gotEvents { + gotChanges[i] = evt.GetAppliedChange().Descriptions[0] + } + if diff := cmp.Diff(wantChanges, gotChanges, protocmp.Transform()); diff != "" { + t.Fatalf("wrong changes\n%s", diff) + } +} + +// stackOperationEventStreams represents the three different kinds of events +// whose emission is independent from one another and so the relative ordering +// between them is not guaranteed between runs. For easier comparison in +// tests, use splitStackOperationEvents to obtain a value of this type. +// +// Note that even after splitting the streams will not be directly comparable +// for most non-trivial operations, because a typical configuration only +// forces a partial order of operations. Except in carefully-crafted tests +// that are explicitly testing an explicit ordering, it may be better to +// just scan the entire event stream and cherry-pick particular events of +// interest, which will also avoid the need to update every test whenever we +// add something entirely new to the even stream. +type stackOperationEventStreams struct { + PlannedChanges []*stacks.PlanStackChanges_Event + Diagnostics []*stacks.PlanStackChanges_Event + + // MiscHooks is the "everything else" category where the detailed begin/end + // events for individual Terraform Core operations appear. + MiscHooks []*stacks.PlanStackChanges_Event +} + +func splitStackOperationEvents(all []*stacks.PlanStackChanges_Event) stackOperationEventStreams { + ret := stackOperationEventStreams{} + for _, evt := range all { + switch evt.Event.(type) { + case *stacks.PlanStackChanges_Event_PlannedChange: + ret.PlannedChanges = append(ret.PlannedChanges, evt) + case *stacks.PlanStackChanges_Event_Diagnostic: + ret.Diagnostics = append(ret.Diagnostics, evt) + default: + ret.MiscHooks = append(ret.MiscHooks, evt) + } + } + return ret +} + +func mustMsgpack(t *testing.T, v cty.Value, ty cty.Type) []byte { + t.Helper() + + ret, err := ctymsgpack.Marshal(v, ty) + if err != nil { + t.Fatalf("error marshalling %#v: %s", v, err) + } + + return ret +} diff --git a/internal/rpcapi/stopper.go b/internal/rpcapi/stopper.go new file mode 100644 index 0000000000..41b0f7973b --- /dev/null +++ b/internal/rpcapi/stopper.go @@ -0,0 +1,57 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "sync" +) + +type stopChan chan struct{} + +// stopper allows the RPC API to stop in-progress long-running operations. Each +// operation must add a new stop to the stopper, and remove it if the operation +// completes successfully. If a Stop RPC is received while the operation is +// running, the stops will all be processed, signalling to each operation that +// it should abort. +// +// Each stop is represented by a channel, which is closed to indicate that the +// operation should stop. +type stopper struct { + stops map[stopChan]struct{} + + mu sync.Mutex +} + +func newStopper() *stopper { + return &stopper{ + stops: make(map[stopChan]struct{}), + } +} + +func (s *stopper) add() stopChan { + s.mu.Lock() + defer s.mu.Unlock() + + stop := make(chan struct{}) + s.stops[stop] = struct{}{} + + return stop +} + +func (s *stopper) remove(stop stopChan) { + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.stops, stop) +} + +func (s *stopper) stop() { + s.mu.Lock() + defer s.mu.Unlock() + + for stop := range s.stops { + close(stop) + delete(s.stops, stop) + } +} diff --git a/internal/rpcapi/telemetry.go b/internal/rpcapi/telemetry.go new file mode 100644 index 0000000000..02605b6259 --- /dev/null +++ b/internal/rpcapi/telemetry.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +// tracer is the OpenTelemetry tracer to use for tracing for code in this +// package. +// +// When creating tracing spans in gRPC service functions, always use the +// a [context.Context] descended from the one passed in to the service +// function so that the spans can attach to the automatically-generated +// server request span and, if the client is also using OpenTelemetry, +// to the client's request span. +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform/internal/rpcapi") +} diff --git a/internal/rpcapi/telemetry_test.go b/internal/rpcapi/telemetry_test.go new file mode 100644 index 0000000000..122bb87fe3 --- /dev/null +++ b/internal/rpcapi/telemetry_test.go @@ -0,0 +1,310 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package rpcapi + +//lint:file-ignore U1000 Some utilities in here are intentionally unused in VCS but are for temporary use while debugging a test. + +import ( + "context" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/instrumentation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.opentelemetry.io/otel/trace" + "google.golang.org/grpc" +) + +// initTelemetryForTest configures OpenTelemetry to collect spans into a +// local in-memory buffer and returns an object that provides access to that +// buffer. +// +// The OpenTelemetry tracer provider is a global cross-cutting concern shared +// throughout the program, so it isn't valid to use this function in any test +// that calls t.Parallel, or in subtests of a parent test that has already +// used this function. +func initTelemetryForTest(t *testing.T, providerOptions ...sdktrace.TracerProviderOption) *tracetest.InMemoryExporter { + t.Helper() + + exp := tracetest.NewInMemoryExporter() + sp := sdktrace.NewSimpleSpanProcessor(exp) + providerOptions = append( + []sdktrace.TracerProviderOption{ + sdktrace.WithSpanProcessor(sp), + }, + providerOptions..., + ) + provider := sdktrace.NewTracerProvider(providerOptions...) + otel.SetTracerProvider(provider) + + pgtr := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) + otel.SetTextMapPropagator(pgtr) + + // We'll automatically shut down the provider at the end of the test run, + // because otherwise a subsequent test which runs something that generates + // telemetry _without_ calling initTelemetryForTest (which is optional) + // could end up appending irrelevant spans to an earlier test's exporter. + t.Cleanup(func() { + provider.Shutdown(context.Background()) + otel.SetTracerProvider(nil) + otel.SetTextMapPropagator(nil) + }) + + t.Log("OpenTelemetry initialized") + return exp +} + +// findTestTelemetrySpan tests each of the spans that have been reported to the +// given [tracetest.InMemoryExporter] with the given predicate function and +// returns the first one for which the predicate matches. +// +// If the predicate returns false for all spans then this function will fail +// the test using the given [testing.T]. +func findTestTelemetrySpan(t *testing.T, exp *tracetest.InMemoryExporter, predicate func(tracetest.SpanStub) bool) tracetest.SpanStub { + for _, span := range exp.GetSpans() { + if predicate(span) { + return span + } + } + t.Fatal("no spans matched the predicate") + return tracetest.SpanStub{} +} + +// findTestTelemetrySpans tests each of the spans that have been reported to the +// given [tracetest.InMemoryExporter] with the given predicate function and +// returns only those for which the predicate matches. +// +// If no spans match at all then the result is a zero-length slice. If you are +// expecting to find exactly one matching span then [findTestTelemetrySpan] +// (singular) might be more convenient. +func findTestTelemetrySpans(t *testing.T, exp *tracetest.InMemoryExporter, predicate func(tracetest.SpanStub) bool) tracetest.SpanStubs { + var ret tracetest.SpanStubs + for _, span := range exp.GetSpans() { + if predicate(span) { + ret = append(ret, span) + } + } + return ret +} + +// overwriteTestSpanTimestamps overwrites the timestamps in all of the given +// spans to be exactly the given fakeTime, as a way to avoid considering exact +// timestamps when comparing actual spans with desired spans. +// +// This function overwrites both the start and end times of the spans themselves +// and also the timestamps of any events associated with the spans. +func overwriteTestSpanTimestamps(spans tracetest.SpanStubs, fakeTime time.Time) { + for i := range spans { + spans[i].StartTime = fakeTime + spans[i].EndTime = fakeTime + for j := range spans[i].Events { + spans[i].Events[j].Time = fakeTime + } + } +} + +func fixedTraceID(n uint32) trace.TraceID { + return trace.TraceID{ + 0xfe, 0xed, 0xfa, 0xce, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + uint8(n >> 24), uint8(n >> 16), uint8(n >> 8), uint8(n >> 0), + } +} + +func fixedSpanID(n uint32) trace.SpanID { + return trace.SpanID{ + 0xfa, 0xce, 0xfe, 0xed, + uint8(n >> 24), uint8(n >> 16), uint8(n >> 8), uint8(n >> 0), + } +} + +func TestTelemetryInTests(t *testing.T) { + ctx := context.Background() + + testResource := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("telemetry test"), + semconv.ServiceVersionKey.String("1.2.3"), + ) + + telemetry := initTelemetryForTest(t, + sdktrace.WithResource(testResource), + ) + + var parentSpanContext, childSpanContext trace.SpanContext + + tracer := otel.Tracer("test thingy") + { + ctx, parentSpan := tracer.Start(ctx, "parent span") + parentSpanContext = parentSpan.SpanContext() + { + _, childSpan := tracer.Start(ctx, "child span") + childSpanContext = childSpan.SpanContext() + childSpan.AddEvent("did something totally hilarious") + childSpan.SetStatus(codes.Error, "it went wrong") + childSpan.End() + } + parentSpan.End() + } + + gotSpans := telemetry.GetSpans() + + // The spans contain real timestamps that make them annoying to compare, + // so we'll just replace those with fixed timestamps so we can easily + // compare everything else. + fakeTime := time.Now() + overwriteTestSpanTimestamps(gotSpans, fakeTime) + + wantSpans := tracetest.SpanStubs{ + // These are ordered by the calls to Span.End above, so child should + // always appear first. (That's a detail of this in-memory-only + // exporter, not a general guarantee about OpenTracing.) + { + Name: "child span", + SpanContext: childSpanContext, + Parent: parentSpanContext, + SpanKind: trace.SpanKindInternal, + StartTime: fakeTime, + EndTime: fakeTime, + Events: []sdktrace.Event{ + { + Name: "did something totally hilarious", + Time: fakeTime, + }, + }, + Status: sdktrace.Status{ + Code: codes.Error, + Description: "it went wrong", + }, + Resource: testResource, + InstrumentationLibrary: instrumentation.Scope{ + Name: "test thingy", + }, + InstrumentationScope: instrumentation.Scope{ + Name: "test thingy", + }, + }, + { + Name: "parent span", + SpanContext: parentSpanContext, + SpanKind: trace.SpanKindInternal, + StartTime: fakeTime, + EndTime: fakeTime, + ChildSpanCount: 1, + Resource: testResource, + InstrumentationLibrary: instrumentation.Scope{ + Name: "test thingy", + }, + InstrumentationScope: instrumentation.Scope{ + Name: "test thingy", + }, + }, + } + + if diff := cmp.Diff(wantSpans, gotSpans); diff != "" { + t.Errorf("wrong spans\n%s", diff) + } +} + +func TestTelemetryInTestsGRPC(t *testing.T) { + ctx := context.Background() + + testResource := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("TestTelemetryInTestsGRPC"), + ) + telemetry := initTelemetryForTest(t, + sdktrace.WithResource(testResource), + ) + + client, close := grpcClientForTesting(ctx, t, func(srv *grpc.Server) { + server := &setupServer{ + initOthers: func(ctx context.Context, cc *setup.Handshake_Request, stopper *stopper) (*setup.ServerCapabilities, error) { + return &setup.ServerCapabilities{}, nil + }, + } + setup.RegisterSetupServer(srv, server) + }) + defer close() + setupClient := setup.NewSetupClient(client) + + { + ctx, span := otel.Tracer("TestTelemetryInTestsGRPC").Start(ctx, "root") + _, err := setupClient.Handshake(ctx, &setup.Handshake_Request{ + Capabilities: &setup.ClientCapabilities{}, + }) + if err != nil { + t.Fatal(err) + } + span.End() + } + + clientSpan := findTestTelemetrySpan(t, telemetry, func(ss tracetest.SpanStub) bool { + return ss.SpanKind == trace.SpanKindClient + }) + serverSpan := findTestTelemetrySpan(t, telemetry, func(ss tracetest.SpanStub) bool { + return ss.SpanKind == trace.SpanKindServer + }) + t.Run("client span", func(t *testing.T) { + span := clientSpan + t.Logf("client span: %s", spew.Sdump(span)) + if got, want := span.Name, "terraform1.setup.Setup/Handshake"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + attrs := otelAttributesMap(span.Attributes) + if got, want := attrs["rpc.system"], "grpc"; got != want { + t.Errorf("wrong rpc.system\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.service"], "terraform1.setup.Setup"; got != want { + t.Errorf("wrong rpc.service\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.method"], "Handshake"; got != want { + t.Errorf("wrong rpc.method\ngot: %s\nwant: %s", got, want) + } + }) + t.Run("server span", func(t *testing.T) { + span := serverSpan + t.Logf("server span: %s", spew.Sdump(span)) + if got, want := span.Name, "terraform1.setup.Setup/Handshake"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := span.Parent.SpanID(), clientSpan.SpanContext.SpanID(); got != want { + t.Errorf("server span is not a child of the client span\nclient span ID: %s\nserver span parent ID: %s", want, got) + } + if got, want := serverSpan.SpanContext.TraceID(), clientSpan.SpanContext.TraceID(); got != want { + t.Errorf("server span belongs to different trace than client span\nclient trace ID: %s\nserver trace ID: %s", want, got) + } + attrs := otelAttributesMap(span.Attributes) + if got, want := attrs["rpc.system"], "grpc"; got != want { + t.Errorf("wrong rpc.system\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.service"], "terraform1.setup.Setup"; got != want { + t.Errorf("wrong rpc.service\ngot: %s\nwant: %s", got, want) + } + if got, want := attrs["rpc.method"], "Handshake"; got != want { + t.Errorf("wrong rpc.method\ngot: %s\nwant: %s", got, want) + } + }) +} + +func otelAttributesMap(kvs []attribute.KeyValue) map[string]any { + ret := make(map[string]any, len(kvs)) + for _, kv := range kvs { + ret[string(kv.Key)] = kv.Value.AsInterface() + } + return ret +} diff --git a/internal/rpcapi/terraform1/dependencies/dependencies.pb.go b/internal/rpcapi/terraform1/dependencies/dependencies.pb.go new file mode 100644 index 0000000000..afb23c12cf --- /dev/null +++ b/internal/rpcapi/terraform1/dependencies/dependencies.pb.go @@ -0,0 +1,4121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: dependencies.proto + +package dependencies + +import ( + context "context" + terraform1 "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type BuildProviderPluginCache_Event_FetchComplete_AuthResult int32 + +const ( + BuildProviderPluginCache_Event_FetchComplete_UNKNOWN BuildProviderPluginCache_Event_FetchComplete_AuthResult = 0 + BuildProviderPluginCache_Event_FetchComplete_VERIFIED_CHECKSUM BuildProviderPluginCache_Event_FetchComplete_AuthResult = 1 + BuildProviderPluginCache_Event_FetchComplete_OFFICIAL_SIGNED BuildProviderPluginCache_Event_FetchComplete_AuthResult = 2 + BuildProviderPluginCache_Event_FetchComplete_PARTNER_SIGNED BuildProviderPluginCache_Event_FetchComplete_AuthResult = 3 + BuildProviderPluginCache_Event_FetchComplete_SELF_SIGNED BuildProviderPluginCache_Event_FetchComplete_AuthResult = 4 +) + +// Enum value maps for BuildProviderPluginCache_Event_FetchComplete_AuthResult. +var ( + BuildProviderPluginCache_Event_FetchComplete_AuthResult_name = map[int32]string{ + 0: "UNKNOWN", + 1: "VERIFIED_CHECKSUM", + 2: "OFFICIAL_SIGNED", + 3: "PARTNER_SIGNED", + 4: "SELF_SIGNED", + } + BuildProviderPluginCache_Event_FetchComplete_AuthResult_value = map[string]int32{ + "UNKNOWN": 0, + "VERIFIED_CHECKSUM": 1, + "OFFICIAL_SIGNED": 2, + "PARTNER_SIGNED": 3, + "SELF_SIGNED": 4, + } +) + +func (x BuildProviderPluginCache_Event_FetchComplete_AuthResult) Enum() *BuildProviderPluginCache_Event_FetchComplete_AuthResult { + p := new(BuildProviderPluginCache_Event_FetchComplete_AuthResult) + *p = x + return p +} + +func (x BuildProviderPluginCache_Event_FetchComplete_AuthResult) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (BuildProviderPluginCache_Event_FetchComplete_AuthResult) Descriptor() protoreflect.EnumDescriptor { + return file_dependencies_proto_enumTypes[0].Descriptor() +} + +func (BuildProviderPluginCache_Event_FetchComplete_AuthResult) Type() protoreflect.EnumType { + return &file_dependencies_proto_enumTypes[0] +} + +func (x BuildProviderPluginCache_Event_FetchComplete_AuthResult) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_FetchComplete_AuthResult.Descriptor instead. +func (BuildProviderPluginCache_Event_FetchComplete_AuthResult) EnumDescriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 5, 0} +} + +type Schema_NestedBlock_NestingMode int32 + +const ( + Schema_NestedBlock_INVALID Schema_NestedBlock_NestingMode = 0 + Schema_NestedBlock_SINGLE Schema_NestedBlock_NestingMode = 1 + Schema_NestedBlock_LIST Schema_NestedBlock_NestingMode = 2 + Schema_NestedBlock_SET Schema_NestedBlock_NestingMode = 3 + Schema_NestedBlock_MAP Schema_NestedBlock_NestingMode = 4 + Schema_NestedBlock_GROUP Schema_NestedBlock_NestingMode = 5 +) + +// Enum value maps for Schema_NestedBlock_NestingMode. +var ( + Schema_NestedBlock_NestingMode_name = map[int32]string{ + 0: "INVALID", + 1: "SINGLE", + 2: "LIST", + 3: "SET", + 4: "MAP", + 5: "GROUP", + } + Schema_NestedBlock_NestingMode_value = map[string]int32{ + "INVALID": 0, + "SINGLE": 1, + "LIST": 2, + "SET": 3, + "MAP": 4, + "GROUP": 5, + } +) + +func (x Schema_NestedBlock_NestingMode) Enum() *Schema_NestedBlock_NestingMode { + p := new(Schema_NestedBlock_NestingMode) + *p = x + return p +} + +func (x Schema_NestedBlock_NestingMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Schema_NestedBlock_NestingMode) Descriptor() protoreflect.EnumDescriptor { + return file_dependencies_proto_enumTypes[1].Descriptor() +} + +func (Schema_NestedBlock_NestingMode) Type() protoreflect.EnumType { + return &file_dependencies_proto_enumTypes[1] +} + +func (x Schema_NestedBlock_NestingMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Schema_NestedBlock_NestingMode.Descriptor instead. +func (Schema_NestedBlock_NestingMode) EnumDescriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 2, 0} +} + +type Schema_Object_NestingMode int32 + +const ( + Schema_Object_INVALID Schema_Object_NestingMode = 0 + Schema_Object_SINGLE Schema_Object_NestingMode = 1 + Schema_Object_LIST Schema_Object_NestingMode = 2 + Schema_Object_SET Schema_Object_NestingMode = 3 + Schema_Object_MAP Schema_Object_NestingMode = 4 +) + +// Enum value maps for Schema_Object_NestingMode. +var ( + Schema_Object_NestingMode_name = map[int32]string{ + 0: "INVALID", + 1: "SINGLE", + 2: "LIST", + 3: "SET", + 4: "MAP", + } + Schema_Object_NestingMode_value = map[string]int32{ + "INVALID": 0, + "SINGLE": 1, + "LIST": 2, + "SET": 3, + "MAP": 4, + } +) + +func (x Schema_Object_NestingMode) Enum() *Schema_Object_NestingMode { + p := new(Schema_Object_NestingMode) + *p = x + return p +} + +func (x Schema_Object_NestingMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Schema_Object_NestingMode) Descriptor() protoreflect.EnumDescriptor { + return file_dependencies_proto_enumTypes[2].Descriptor() +} + +func (Schema_Object_NestingMode) Type() protoreflect.EnumType { + return &file_dependencies_proto_enumTypes[2] +} + +func (x Schema_Object_NestingMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Schema_Object_NestingMode.Descriptor instead. +func (Schema_Object_NestingMode) EnumDescriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 3, 0} +} + +type Schema_DocString_Format int32 + +const ( + Schema_DocString_PLAIN Schema_DocString_Format = 0 + Schema_DocString_MARKDOWN Schema_DocString_Format = 1 +) + +// Enum value maps for Schema_DocString_Format. +var ( + Schema_DocString_Format_name = map[int32]string{ + 0: "PLAIN", + 1: "MARKDOWN", + } + Schema_DocString_Format_value = map[string]int32{ + "PLAIN": 0, + "MARKDOWN": 1, + } +) + +func (x Schema_DocString_Format) Enum() *Schema_DocString_Format { + p := new(Schema_DocString_Format) + *p = x + return p +} + +func (x Schema_DocString_Format) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Schema_DocString_Format) Descriptor() protoreflect.EnumDescriptor { + return file_dependencies_proto_enumTypes[3].Descriptor() +} + +func (Schema_DocString_Format) Type() protoreflect.EnumType { + return &file_dependencies_proto_enumTypes[3] +} + +func (x Schema_DocString_Format) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Schema_DocString_Format.Descriptor instead. +func (Schema_DocString_Format) EnumDescriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 4, 0} +} + +type OpenSourceBundle struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenSourceBundle) Reset() { + *x = OpenSourceBundle{} + mi := &file_dependencies_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenSourceBundle) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenSourceBundle) ProtoMessage() {} + +func (x *OpenSourceBundle) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenSourceBundle.ProtoReflect.Descriptor instead. +func (*OpenSourceBundle) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{0} +} + +type CloseSourceBundle struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseSourceBundle) Reset() { + *x = CloseSourceBundle{} + mi := &file_dependencies_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseSourceBundle) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseSourceBundle) ProtoMessage() {} + +func (x *CloseSourceBundle) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseSourceBundle.ProtoReflect.Descriptor instead. +func (*CloseSourceBundle) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{1} +} + +type OpenDependencyLockFile struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenDependencyLockFile) Reset() { + *x = OpenDependencyLockFile{} + mi := &file_dependencies_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenDependencyLockFile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenDependencyLockFile) ProtoMessage() {} + +func (x *OpenDependencyLockFile) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenDependencyLockFile.ProtoReflect.Descriptor instead. +func (*OpenDependencyLockFile) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{2} +} + +type CreateDependencyLocks struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateDependencyLocks) Reset() { + *x = CreateDependencyLocks{} + mi := &file_dependencies_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateDependencyLocks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateDependencyLocks) ProtoMessage() {} + +func (x *CreateDependencyLocks) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateDependencyLocks.ProtoReflect.Descriptor instead. +func (*CreateDependencyLocks) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{3} +} + +type CloseDependencyLocks struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseDependencyLocks) Reset() { + *x = CloseDependencyLocks{} + mi := &file_dependencies_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseDependencyLocks) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseDependencyLocks) ProtoMessage() {} + +func (x *CloseDependencyLocks) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseDependencyLocks.ProtoReflect.Descriptor instead. +func (*CloseDependencyLocks) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{4} +} + +type GetLockedProviderDependencies struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLockedProviderDependencies) Reset() { + *x = GetLockedProviderDependencies{} + mi := &file_dependencies_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLockedProviderDependencies) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLockedProviderDependencies) ProtoMessage() {} + +func (x *GetLockedProviderDependencies) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLockedProviderDependencies.ProtoReflect.Descriptor instead. +func (*GetLockedProviderDependencies) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{5} +} + +type BuildProviderPluginCache struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache) Reset() { + *x = BuildProviderPluginCache{} + mi := &file_dependencies_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache) ProtoMessage() {} + +func (x *BuildProviderPluginCache) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6} +} + +type OpenProviderPluginCache struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenProviderPluginCache) Reset() { + *x = OpenProviderPluginCache{} + mi := &file_dependencies_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenProviderPluginCache) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenProviderPluginCache) ProtoMessage() {} + +func (x *OpenProviderPluginCache) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenProviderPluginCache.ProtoReflect.Descriptor instead. +func (*OpenProviderPluginCache) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{7} +} + +type CloseProviderPluginCache struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseProviderPluginCache) Reset() { + *x = CloseProviderPluginCache{} + mi := &file_dependencies_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseProviderPluginCache) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseProviderPluginCache) ProtoMessage() {} + +func (x *CloseProviderPluginCache) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseProviderPluginCache.ProtoReflect.Descriptor instead. +func (*CloseProviderPluginCache) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{8} +} + +type GetCachedProviders struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCachedProviders) Reset() { + *x = GetCachedProviders{} + mi := &file_dependencies_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCachedProviders) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCachedProviders) ProtoMessage() {} + +func (x *GetCachedProviders) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCachedProviders.ProtoReflect.Descriptor instead. +func (*GetCachedProviders) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{9} +} + +type GetBuiltInProviders struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBuiltInProviders) Reset() { + *x = GetBuiltInProviders{} + mi := &file_dependencies_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBuiltInProviders) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBuiltInProviders) ProtoMessage() {} + +func (x *GetBuiltInProviders) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBuiltInProviders.ProtoReflect.Descriptor instead. +func (*GetBuiltInProviders) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{10} +} + +type GetProviderSchema struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderSchema) Reset() { + *x = GetProviderSchema{} + mi := &file_dependencies_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderSchema) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderSchema) ProtoMessage() {} + +func (x *GetProviderSchema) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProviderSchema.ProtoReflect.Descriptor instead. +func (*GetProviderSchema) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{11} +} + +// ProviderSchema describes the full schema for a particular provider. +type ProviderSchema struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderConfig *Schema `protobuf:"bytes,1,opt,name=provider_config,json=providerConfig,proto3" json:"provider_config,omitempty"` + ManagedResourceTypes map[string]*Schema `protobuf:"bytes,2,rep,name=managed_resource_types,json=managedResourceTypes,proto3" json:"managed_resource_types,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + DataResourceTypes map[string]*Schema `protobuf:"bytes,3,rep,name=data_resource_types,json=dataResourceTypes,proto3" json:"data_resource_types,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderSchema) Reset() { + *x = ProviderSchema{} + mi := &file_dependencies_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderSchema) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderSchema) ProtoMessage() {} + +func (x *ProviderSchema) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProviderSchema.ProtoReflect.Descriptor instead. +func (*ProviderSchema) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{12} +} + +func (x *ProviderSchema) GetProviderConfig() *Schema { + if x != nil { + return x.ProviderConfig + } + return nil +} + +func (x *ProviderSchema) GetManagedResourceTypes() map[string]*Schema { + if x != nil { + return x.ManagedResourceTypes + } + return nil +} + +func (x *ProviderSchema) GetDataResourceTypes() map[string]*Schema { + if x != nil { + return x.DataResourceTypes + } + return nil +} + +// Schema describes a schema for an instance of a particular object, such as +// a resource type or a provider's overall configuration. +type Schema struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Block is the top level configuration block for this schema. + Block *Schema_Block `protobuf:"bytes,1,opt,name=block,proto3" json:"block,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Schema) Reset() { + *x = Schema{} + mi := &file_dependencies_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schema) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schema) ProtoMessage() {} + +func (x *Schema) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schema.ProtoReflect.Descriptor instead. +func (*Schema) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13} +} + +func (x *Schema) GetBlock() *Schema_Block { + if x != nil { + return x.Block + } + return nil +} + +type OpenSourceBundle_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + LocalPath string `protobuf:"bytes,1,opt,name=local_path,json=localPath,proto3" json:"local_path,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenSourceBundle_Request) Reset() { + *x = OpenSourceBundle_Request{} + mi := &file_dependencies_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenSourceBundle_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenSourceBundle_Request) ProtoMessage() {} + +func (x *OpenSourceBundle_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenSourceBundle_Request.ProtoReflect.Descriptor instead. +func (*OpenSourceBundle_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *OpenSourceBundle_Request) GetLocalPath() string { + if x != nil { + return x.LocalPath + } + return "" +} + +type OpenSourceBundle_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceBundleHandle int64 `protobuf:"varint,1,opt,name=source_bundle_handle,json=sourceBundleHandle,proto3" json:"source_bundle_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenSourceBundle_Response) Reset() { + *x = OpenSourceBundle_Response{} + mi := &file_dependencies_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenSourceBundle_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenSourceBundle_Response) ProtoMessage() {} + +func (x *OpenSourceBundle_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenSourceBundle_Response.ProtoReflect.Descriptor instead. +func (*OpenSourceBundle_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *OpenSourceBundle_Response) GetSourceBundleHandle() int64 { + if x != nil { + return x.SourceBundleHandle + } + return 0 +} + +type CloseSourceBundle_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceBundleHandle int64 `protobuf:"varint,1,opt,name=source_bundle_handle,json=sourceBundleHandle,proto3" json:"source_bundle_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseSourceBundle_Request) Reset() { + *x = CloseSourceBundle_Request{} + mi := &file_dependencies_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseSourceBundle_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseSourceBundle_Request) ProtoMessage() {} + +func (x *CloseSourceBundle_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseSourceBundle_Request.ProtoReflect.Descriptor instead. +func (*CloseSourceBundle_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *CloseSourceBundle_Request) GetSourceBundleHandle() int64 { + if x != nil { + return x.SourceBundleHandle + } + return 0 +} + +type CloseSourceBundle_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseSourceBundle_Response) Reset() { + *x = CloseSourceBundle_Response{} + mi := &file_dependencies_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseSourceBundle_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseSourceBundle_Response) ProtoMessage() {} + +func (x *CloseSourceBundle_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseSourceBundle_Response.ProtoReflect.Descriptor instead. +func (*CloseSourceBundle_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{1, 1} +} + +type OpenDependencyLockFile_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceBundleHandle int64 `protobuf:"varint,1,opt,name=source_bundle_handle,json=sourceBundleHandle,proto3" json:"source_bundle_handle,omitempty"` + SourceAddress *terraform1.SourceAddress `protobuf:"bytes,2,opt,name=source_address,json=sourceAddress,proto3" json:"source_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenDependencyLockFile_Request) Reset() { + *x = OpenDependencyLockFile_Request{} + mi := &file_dependencies_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenDependencyLockFile_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenDependencyLockFile_Request) ProtoMessage() {} + +func (x *OpenDependencyLockFile_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenDependencyLockFile_Request.ProtoReflect.Descriptor instead. +func (*OpenDependencyLockFile_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *OpenDependencyLockFile_Request) GetSourceBundleHandle() int64 { + if x != nil { + return x.SourceBundleHandle + } + return 0 +} + +func (x *OpenDependencyLockFile_Request) GetSourceAddress() *terraform1.SourceAddress { + if x != nil { + return x.SourceAddress + } + return nil +} + +type OpenDependencyLockFile_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + DependencyLocksHandle int64 `protobuf:"varint,1,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenDependencyLockFile_Response) Reset() { + *x = OpenDependencyLockFile_Response{} + mi := &file_dependencies_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenDependencyLockFile_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenDependencyLockFile_Response) ProtoMessage() {} + +func (x *OpenDependencyLockFile_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenDependencyLockFile_Response.ProtoReflect.Descriptor instead. +func (*OpenDependencyLockFile_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *OpenDependencyLockFile_Response) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *OpenDependencyLockFile_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type CreateDependencyLocks_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The provider selections to include in the locks object. + // + // A typical value would be the result of an earlier call to + // GetLockedProviderDependencies on some other locks object, + // e.g. if a caller needs to propagate a set of locks from one + // Terraform Core RPC server to another. + ProviderSelections []*terraform1.ProviderPackage `protobuf:"bytes,1,rep,name=provider_selections,json=providerSelections,proto3" json:"provider_selections,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateDependencyLocks_Request) Reset() { + *x = CreateDependencyLocks_Request{} + mi := &file_dependencies_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateDependencyLocks_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateDependencyLocks_Request) ProtoMessage() {} + +func (x *CreateDependencyLocks_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateDependencyLocks_Request.ProtoReflect.Descriptor instead. +func (*CreateDependencyLocks_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *CreateDependencyLocks_Request) GetProviderSelections() []*terraform1.ProviderPackage { + if x != nil { + return x.ProviderSelections + } + return nil +} + +type CreateDependencyLocks_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + DependencyLocksHandle int64 `protobuf:"varint,1,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateDependencyLocks_Response) Reset() { + *x = CreateDependencyLocks_Response{} + mi := &file_dependencies_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateDependencyLocks_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateDependencyLocks_Response) ProtoMessage() {} + +func (x *CreateDependencyLocks_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateDependencyLocks_Response.ProtoReflect.Descriptor instead. +func (*CreateDependencyLocks_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{3, 1} +} + +func (x *CreateDependencyLocks_Response) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +type CloseDependencyLocks_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + DependencyLocksHandle int64 `protobuf:"varint,1,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseDependencyLocks_Request) Reset() { + *x = CloseDependencyLocks_Request{} + mi := &file_dependencies_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseDependencyLocks_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseDependencyLocks_Request) ProtoMessage() {} + +func (x *CloseDependencyLocks_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseDependencyLocks_Request.ProtoReflect.Descriptor instead. +func (*CloseDependencyLocks_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *CloseDependencyLocks_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +type CloseDependencyLocks_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseDependencyLocks_Response) Reset() { + *x = CloseDependencyLocks_Response{} + mi := &file_dependencies_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseDependencyLocks_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseDependencyLocks_Response) ProtoMessage() {} + +func (x *CloseDependencyLocks_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseDependencyLocks_Response.ProtoReflect.Descriptor instead. +func (*CloseDependencyLocks_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{4, 1} +} + +type GetLockedProviderDependencies_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + DependencyLocksHandle int64 `protobuf:"varint,1,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLockedProviderDependencies_Request) Reset() { + *x = GetLockedProviderDependencies_Request{} + mi := &file_dependencies_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLockedProviderDependencies_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLockedProviderDependencies_Request) ProtoMessage() {} + +func (x *GetLockedProviderDependencies_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLockedProviderDependencies_Request.ProtoReflect.Descriptor instead. +func (*GetLockedProviderDependencies_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *GetLockedProviderDependencies_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +type GetLockedProviderDependencies_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + SelectedProviders []*terraform1.ProviderPackage `protobuf:"bytes,1,rep,name=selected_providers,json=selectedProviders,proto3" json:"selected_providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLockedProviderDependencies_Response) Reset() { + *x = GetLockedProviderDependencies_Response{} + mi := &file_dependencies_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLockedProviderDependencies_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLockedProviderDependencies_Response) ProtoMessage() {} + +func (x *GetLockedProviderDependencies_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLockedProviderDependencies_Response.ProtoReflect.Descriptor instead. +func (*GetLockedProviderDependencies_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{5, 1} +} + +func (x *GetLockedProviderDependencies_Response) GetSelectedProviders() []*terraform1.ProviderPackage { + if x != nil { + return x.SelectedProviders + } + return nil +} + +type BuildProviderPluginCache_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + CacheDir string `protobuf:"bytes,1,opt,name=cache_dir,json=cacheDir,proto3" json:"cache_dir,omitempty"` + DependencyLocksHandle int64 `protobuf:"varint,2,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + InstallationMethods []*BuildProviderPluginCache_Request_InstallMethod `protobuf:"bytes,3,rep,name=installation_methods,json=installationMethods,proto3" json:"installation_methods,omitempty"` + // If set, this populates the cache with plugins for a different + // platform than the one the Terraform Core RPC server is running on. + // If unset (empty) then the cache will be populated with packages + // for the same platform as Terraform Core was built for, if available. + // + // If this is set to a different platform than the Terraform Core RPC + // server's then the generated cache directory will appear empty to + // other operations on this server. + OverridePlatform string `protobuf:"bytes,4,opt,name=override_platform,json=overridePlatform,proto3" json:"override_platform,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Request) Reset() { + *x = BuildProviderPluginCache_Request{} + mi := &file_dependencies_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Request) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Request.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 0} +} + +func (x *BuildProviderPluginCache_Request) GetCacheDir() string { + if x != nil { + return x.CacheDir + } + return "" +} + +func (x *BuildProviderPluginCache_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *BuildProviderPluginCache_Request) GetInstallationMethods() []*BuildProviderPluginCache_Request_InstallMethod { + if x != nil { + return x.InstallationMethods + } + return nil +} + +func (x *BuildProviderPluginCache_Request) GetOverridePlatform() string { + if x != nil { + return x.OverridePlatform + } + return "" +} + +type BuildProviderPluginCache_Event struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *BuildProviderPluginCache_Event_Pending_ + // *BuildProviderPluginCache_Event_AlreadyInstalled + // *BuildProviderPluginCache_Event_BuiltIn + // *BuildProviderPluginCache_Event_QueryBegin + // *BuildProviderPluginCache_Event_QuerySuccess + // *BuildProviderPluginCache_Event_QueryWarnings + // *BuildProviderPluginCache_Event_FetchBegin_ + // *BuildProviderPluginCache_Event_FetchComplete_ + // *BuildProviderPluginCache_Event_Diagnostic + Event isBuildProviderPluginCache_Event_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event) Reset() { + *x = BuildProviderPluginCache_Event{} + mi := &file_dependencies_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1} +} + +func (x *BuildProviderPluginCache_Event) GetEvent() isBuildProviderPluginCache_Event_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetPending() *BuildProviderPluginCache_Event_Pending { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_Pending_); ok { + return x.Pending + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetAlreadyInstalled() *BuildProviderPluginCache_Event_ProviderVersion { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_AlreadyInstalled); ok { + return x.AlreadyInstalled + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetBuiltIn() *BuildProviderPluginCache_Event_ProviderVersion { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_BuiltIn); ok { + return x.BuiltIn + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetQueryBegin() *BuildProviderPluginCache_Event_ProviderConstraints { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_QueryBegin); ok { + return x.QueryBegin + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetQuerySuccess() *BuildProviderPluginCache_Event_ProviderVersion { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_QuerySuccess); ok { + return x.QuerySuccess + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetQueryWarnings() *BuildProviderPluginCache_Event_ProviderWarnings { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_QueryWarnings); ok { + return x.QueryWarnings + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetFetchBegin() *BuildProviderPluginCache_Event_FetchBegin { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_FetchBegin_); ok { + return x.FetchBegin + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetFetchComplete() *BuildProviderPluginCache_Event_FetchComplete { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_FetchComplete_); ok { + return x.FetchComplete + } + } + return nil +} + +func (x *BuildProviderPluginCache_Event) GetDiagnostic() *terraform1.Diagnostic { + if x != nil { + if x, ok := x.Event.(*BuildProviderPluginCache_Event_Diagnostic); ok { + return x.Diagnostic + } + } + return nil +} + +type isBuildProviderPluginCache_Event_Event interface { + isBuildProviderPluginCache_Event_Event() +} + +type BuildProviderPluginCache_Event_Pending_ struct { + Pending *BuildProviderPluginCache_Event_Pending `protobuf:"bytes,1,opt,name=pending,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_AlreadyInstalled struct { + AlreadyInstalled *BuildProviderPluginCache_Event_ProviderVersion `protobuf:"bytes,2,opt,name=already_installed,json=alreadyInstalled,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_BuiltIn struct { + BuiltIn *BuildProviderPluginCache_Event_ProviderVersion `protobuf:"bytes,3,opt,name=built_in,json=builtIn,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_QueryBegin struct { + QueryBegin *BuildProviderPluginCache_Event_ProviderConstraints `protobuf:"bytes,4,opt,name=query_begin,json=queryBegin,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_QuerySuccess struct { + QuerySuccess *BuildProviderPluginCache_Event_ProviderVersion `protobuf:"bytes,5,opt,name=query_success,json=querySuccess,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_QueryWarnings struct { + QueryWarnings *BuildProviderPluginCache_Event_ProviderWarnings `protobuf:"bytes,6,opt,name=query_warnings,json=queryWarnings,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_FetchBegin_ struct { + FetchBegin *BuildProviderPluginCache_Event_FetchBegin `protobuf:"bytes,7,opt,name=fetch_begin,json=fetchBegin,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_FetchComplete_ struct { + FetchComplete *BuildProviderPluginCache_Event_FetchComplete `protobuf:"bytes,8,opt,name=fetch_complete,json=fetchComplete,proto3,oneof"` +} + +type BuildProviderPluginCache_Event_Diagnostic struct { + Diagnostic *terraform1.Diagnostic `protobuf:"bytes,9,opt,name=diagnostic,proto3,oneof"` +} + +func (*BuildProviderPluginCache_Event_Pending_) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_AlreadyInstalled) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_BuiltIn) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_QueryBegin) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_QuerySuccess) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_QueryWarnings) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_FetchBegin_) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_FetchComplete_) isBuildProviderPluginCache_Event_Event() {} + +func (*BuildProviderPluginCache_Event_Diagnostic) isBuildProviderPluginCache_Event_Event() {} + +type BuildProviderPluginCache_Request_InstallMethod struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Source: + // + // *BuildProviderPluginCache_Request_InstallMethod_Direct + // *BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir + // *BuildProviderPluginCache_Request_InstallMethod_NetworkMirrorUrl + Source isBuildProviderPluginCache_Request_InstallMethod_Source `protobuf_oneof:"source"` + Include []string `protobuf:"bytes,4,rep,name=include,proto3" json:"include,omitempty"` + Exclude []string `protobuf:"bytes,5,rep,name=exclude,proto3" json:"exclude,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) Reset() { + *x = BuildProviderPluginCache_Request_InstallMethod{} + mi := &file_dependencies_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Request_InstallMethod) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Request_InstallMethod) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Request_InstallMethod.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Request_InstallMethod) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 0, 0} +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) GetSource() isBuildProviderPluginCache_Request_InstallMethod_Source { + if x != nil { + return x.Source + } + return nil +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) GetDirect() bool { + if x != nil { + if x, ok := x.Source.(*BuildProviderPluginCache_Request_InstallMethod_Direct); ok { + return x.Direct + } + } + return false +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) GetLocalMirrorDir() string { + if x != nil { + if x, ok := x.Source.(*BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir); ok { + return x.LocalMirrorDir + } + } + return "" +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) GetNetworkMirrorUrl() string { + if x != nil { + if x, ok := x.Source.(*BuildProviderPluginCache_Request_InstallMethod_NetworkMirrorUrl); ok { + return x.NetworkMirrorUrl + } + } + return "" +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) GetInclude() []string { + if x != nil { + return x.Include + } + return nil +} + +func (x *BuildProviderPluginCache_Request_InstallMethod) GetExclude() []string { + if x != nil { + return x.Exclude + } + return nil +} + +type isBuildProviderPluginCache_Request_InstallMethod_Source interface { + isBuildProviderPluginCache_Request_InstallMethod_Source() +} + +type BuildProviderPluginCache_Request_InstallMethod_Direct struct { + Direct bool `protobuf:"varint,1,opt,name=direct,proto3,oneof"` +} + +type BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir struct { + LocalMirrorDir string `protobuf:"bytes,2,opt,name=local_mirror_dir,json=localMirrorDir,proto3,oneof"` +} + +type BuildProviderPluginCache_Request_InstallMethod_NetworkMirrorUrl struct { + NetworkMirrorUrl string `protobuf:"bytes,3,opt,name=network_mirror_url,json=networkMirrorUrl,proto3,oneof"` +} + +func (*BuildProviderPluginCache_Request_InstallMethod_Direct) isBuildProviderPluginCache_Request_InstallMethod_Source() { +} + +func (*BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir) isBuildProviderPluginCache_Request_InstallMethod_Source() { +} + +func (*BuildProviderPluginCache_Request_InstallMethod_NetworkMirrorUrl) isBuildProviderPluginCache_Request_InstallMethod_Source() { +} + +type BuildProviderPluginCache_Event_Pending struct { + state protoimpl.MessageState `protogen:"open.v1"` + Expected []*BuildProviderPluginCache_Event_ProviderConstraints `protobuf:"bytes,1,rep,name=expected,proto3" json:"expected,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event_Pending) Reset() { + *x = BuildProviderPluginCache_Event_Pending{} + mi := &file_dependencies_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event_Pending) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event_Pending) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event_Pending) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_Pending.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event_Pending) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 0} +} + +func (x *BuildProviderPluginCache_Event_Pending) GetExpected() []*BuildProviderPluginCache_Event_ProviderConstraints { + if x != nil { + return x.Expected + } + return nil +} + +type BuildProviderPluginCache_Event_ProviderConstraints struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Versions string `protobuf:"bytes,2,opt,name=versions,proto3" json:"versions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event_ProviderConstraints) Reset() { + *x = BuildProviderPluginCache_Event_ProviderConstraints{} + mi := &file_dependencies_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event_ProviderConstraints) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event_ProviderConstraints) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event_ProviderConstraints) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_ProviderConstraints.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event_ProviderConstraints) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 1} +} + +func (x *BuildProviderPluginCache_Event_ProviderConstraints) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *BuildProviderPluginCache_Event_ProviderConstraints) GetVersions() string { + if x != nil { + return x.Versions + } + return "" +} + +type BuildProviderPluginCache_Event_ProviderVersion struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event_ProviderVersion) Reset() { + *x = BuildProviderPluginCache_Event_ProviderVersion{} + mi := &file_dependencies_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event_ProviderVersion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event_ProviderVersion) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event_ProviderVersion) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_ProviderVersion.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event_ProviderVersion) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 2} +} + +func (x *BuildProviderPluginCache_Event_ProviderVersion) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *BuildProviderPluginCache_Event_ProviderVersion) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +type BuildProviderPluginCache_Event_ProviderWarnings struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Warnings []string `protobuf:"bytes,2,rep,name=warnings,proto3" json:"warnings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event_ProviderWarnings) Reset() { + *x = BuildProviderPluginCache_Event_ProviderWarnings{} + mi := &file_dependencies_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event_ProviderWarnings) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event_ProviderWarnings) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event_ProviderWarnings) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_ProviderWarnings.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event_ProviderWarnings) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 3} +} + +func (x *BuildProviderPluginCache_Event_ProviderWarnings) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *BuildProviderPluginCache_Event_ProviderWarnings) GetWarnings() []string { + if x != nil { + return x.Warnings + } + return nil +} + +type BuildProviderPluginCache_Event_FetchBegin struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderVersion *BuildProviderPluginCache_Event_ProviderVersion `protobuf:"bytes,1,opt,name=provider_version,json=providerVersion,proto3" json:"provider_version,omitempty"` + Location string `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event_FetchBegin) Reset() { + *x = BuildProviderPluginCache_Event_FetchBegin{} + mi := &file_dependencies_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event_FetchBegin) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event_FetchBegin) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event_FetchBegin) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_FetchBegin.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event_FetchBegin) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 4} +} + +func (x *BuildProviderPluginCache_Event_FetchBegin) GetProviderVersion() *BuildProviderPluginCache_Event_ProviderVersion { + if x != nil { + return x.ProviderVersion + } + return nil +} + +func (x *BuildProviderPluginCache_Event_FetchBegin) GetLocation() string { + if x != nil { + return x.Location + } + return "" +} + +type BuildProviderPluginCache_Event_FetchComplete struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderVersion *BuildProviderPluginCache_Event_ProviderVersion `protobuf:"bytes,1,opt,name=provider_version,json=providerVersion,proto3" json:"provider_version,omitempty"` + AuthResult BuildProviderPluginCache_Event_FetchComplete_AuthResult `protobuf:"varint,2,opt,name=auth_result,json=authResult,proto3,enum=terraform1.dependencies.BuildProviderPluginCache_Event_FetchComplete_AuthResult" json:"auth_result,omitempty"` + // If auth_result is one of the "_SIGNED" variants then this + // might contain a UI-oriented identifier for the key that + // signed the package. The exact format of this string is not + // guaranteed; do not attempt to parse it or make automated + // decisions based on it. + KeyIdForDisplay string `protobuf:"bytes,3,opt,name=key_id_for_display,json=keyIdForDisplay,proto3" json:"key_id_for_display,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuildProviderPluginCache_Event_FetchComplete) Reset() { + *x = BuildProviderPluginCache_Event_FetchComplete{} + mi := &file_dependencies_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuildProviderPluginCache_Event_FetchComplete) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildProviderPluginCache_Event_FetchComplete) ProtoMessage() {} + +func (x *BuildProviderPluginCache_Event_FetchComplete) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildProviderPluginCache_Event_FetchComplete.ProtoReflect.Descriptor instead. +func (*BuildProviderPluginCache_Event_FetchComplete) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{6, 1, 5} +} + +func (x *BuildProviderPluginCache_Event_FetchComplete) GetProviderVersion() *BuildProviderPluginCache_Event_ProviderVersion { + if x != nil { + return x.ProviderVersion + } + return nil +} + +func (x *BuildProviderPluginCache_Event_FetchComplete) GetAuthResult() BuildProviderPluginCache_Event_FetchComplete_AuthResult { + if x != nil { + return x.AuthResult + } + return BuildProviderPluginCache_Event_FetchComplete_UNKNOWN +} + +func (x *BuildProviderPluginCache_Event_FetchComplete) GetKeyIdForDisplay() string { + if x != nil { + return x.KeyIdForDisplay + } + return "" +} + +type OpenProviderPluginCache_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + CacheDir string `protobuf:"bytes,1,opt,name=cache_dir,json=cacheDir,proto3" json:"cache_dir,omitempty"` + // As with the field of the same name in BuildProviderPluginCache.Request. + // + // If this is set to anything other than this RPC server's native + // platform then any operations that require executing the provider + // plugin are likely to fail due to executable format errors or + // similar. However, it's valid to use the returned handle with + // GetCachedProviders, since it only analyzes the cache metadata + // and doesn't actually run the plugins inside. + OverridePlatform string `protobuf:"bytes,2,opt,name=override_platform,json=overridePlatform,proto3" json:"override_platform,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenProviderPluginCache_Request) Reset() { + *x = OpenProviderPluginCache_Request{} + mi := &file_dependencies_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenProviderPluginCache_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenProviderPluginCache_Request) ProtoMessage() {} + +func (x *OpenProviderPluginCache_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenProviderPluginCache_Request.ProtoReflect.Descriptor instead. +func (*OpenProviderPluginCache_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{7, 0} +} + +func (x *OpenProviderPluginCache_Request) GetCacheDir() string { + if x != nil { + return x.CacheDir + } + return "" +} + +func (x *OpenProviderPluginCache_Request) GetOverridePlatform() string { + if x != nil { + return x.OverridePlatform + } + return "" +} + +type OpenProviderPluginCache_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderCacheHandle int64 `protobuf:"varint,1,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenProviderPluginCache_Response) Reset() { + *x = OpenProviderPluginCache_Response{} + mi := &file_dependencies_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenProviderPluginCache_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenProviderPluginCache_Response) ProtoMessage() {} + +func (x *OpenProviderPluginCache_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenProviderPluginCache_Response.ProtoReflect.Descriptor instead. +func (*OpenProviderPluginCache_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{7, 1} +} + +func (x *OpenProviderPluginCache_Response) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +type CloseProviderPluginCache_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderCacheHandle int64 `protobuf:"varint,1,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseProviderPluginCache_Request) Reset() { + *x = CloseProviderPluginCache_Request{} + mi := &file_dependencies_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseProviderPluginCache_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseProviderPluginCache_Request) ProtoMessage() {} + +func (x *CloseProviderPluginCache_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseProviderPluginCache_Request.ProtoReflect.Descriptor instead. +func (*CloseProviderPluginCache_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{8, 0} +} + +func (x *CloseProviderPluginCache_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +type CloseProviderPluginCache_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseProviderPluginCache_Response) Reset() { + *x = CloseProviderPluginCache_Response{} + mi := &file_dependencies_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseProviderPluginCache_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseProviderPluginCache_Response) ProtoMessage() {} + +func (x *CloseProviderPluginCache_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseProviderPluginCache_Response.ProtoReflect.Descriptor instead. +func (*CloseProviderPluginCache_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{8, 1} +} + +type GetCachedProviders_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + ProviderCacheHandle int64 `protobuf:"varint,1,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCachedProviders_Request) Reset() { + *x = GetCachedProviders_Request{} + mi := &file_dependencies_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCachedProviders_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCachedProviders_Request) ProtoMessage() {} + +func (x *GetCachedProviders_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCachedProviders_Request.ProtoReflect.Descriptor instead. +func (*GetCachedProviders_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{9, 0} +} + +func (x *GetCachedProviders_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +type GetCachedProviders_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + AvailableProviders []*terraform1.ProviderPackage `protobuf:"bytes,1,rep,name=available_providers,json=availableProviders,proto3" json:"available_providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCachedProviders_Response) Reset() { + *x = GetCachedProviders_Response{} + mi := &file_dependencies_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCachedProviders_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCachedProviders_Response) ProtoMessage() {} + +func (x *GetCachedProviders_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCachedProviders_Response.ProtoReflect.Descriptor instead. +func (*GetCachedProviders_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{9, 1} +} + +func (x *GetCachedProviders_Response) GetAvailableProviders() []*terraform1.ProviderPackage { + if x != nil { + return x.AvailableProviders + } + return nil +} + +type GetBuiltInProviders_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBuiltInProviders_Request) Reset() { + *x = GetBuiltInProviders_Request{} + mi := &file_dependencies_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBuiltInProviders_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBuiltInProviders_Request) ProtoMessage() {} + +func (x *GetBuiltInProviders_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBuiltInProviders_Request.ProtoReflect.Descriptor instead. +func (*GetBuiltInProviders_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{10, 0} +} + +type GetBuiltInProviders_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The built-in providers that are compiled in to this Terraform Core + // server. + // + // This uses terraform1.ProviderPackage messages for consistency with the other + // operations which list providers, but built-in providers do not + // have version numbers nor hashes so those fields will always be + // unset in the result. + AvailableProviders []*terraform1.ProviderPackage `protobuf:"bytes,1,rep,name=available_providers,json=availableProviders,proto3" json:"available_providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetBuiltInProviders_Response) Reset() { + *x = GetBuiltInProviders_Response{} + mi := &file_dependencies_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetBuiltInProviders_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetBuiltInProviders_Response) ProtoMessage() {} + +func (x *GetBuiltInProviders_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetBuiltInProviders_Response.ProtoReflect.Descriptor instead. +func (*GetBuiltInProviders_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{10, 1} +} + +func (x *GetBuiltInProviders_Response) GetAvailableProviders() []*terraform1.ProviderPackage { + if x != nil { + return x.AvailableProviders + } + return nil +} + +type GetProviderSchema_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The address of the provider to retrieve schema for, using the + // typical provider source address syntax. + // + // When requesting schema based on a terraform1.ProviderPackage message, populate + // this with its "source_addr" field. + ProviderAddr string `protobuf:"bytes,1,opt,name=provider_addr,json=providerAddr,proto3" json:"provider_addr,omitempty"` + // The version number of the given provider to retrieve the schema + // of, which must have already been populated into the cache directory. + // + // Not supported for built-in providers because we can only access the + // single "version" of the provider that's compiled into this Terraform + // Core server, and so must be left unset or empty for those. + // + // When requesting schema based on a terraform1.ProviderPackage message, populate + // this with its "version" field. + ProviderVersion string `protobuf:"bytes,2,opt,name=provider_version,json=providerVersion,proto3" json:"provider_version,omitempty"` + // The handle for the previously-opened provider plugin cache to + // load the provider plugin from. + // + // Optional for built-in providers, but can still be specified in that + // case if desired so that callers can safely just send the handle they + // have in all cases and be naive about which providers are and are + // not built in. + ProviderCacheHandle int64 `protobuf:"varint,3,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderSchema_Request) Reset() { + *x = GetProviderSchema_Request{} + mi := &file_dependencies_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderSchema_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderSchema_Request) ProtoMessage() {} + +func (x *GetProviderSchema_Request) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProviderSchema_Request.ProtoReflect.Descriptor instead. +func (*GetProviderSchema_Request) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{11, 0} +} + +func (x *GetProviderSchema_Request) GetProviderAddr() string { + if x != nil { + return x.ProviderAddr + } + return "" +} + +func (x *GetProviderSchema_Request) GetProviderVersion() string { + if x != nil { + return x.ProviderVersion + } + return "" +} + +func (x *GetProviderSchema_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +type GetProviderSchema_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Schema *ProviderSchema `protobuf:"bytes,1,opt,name=schema,proto3" json:"schema,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetProviderSchema_Response) Reset() { + *x = GetProviderSchema_Response{} + mi := &file_dependencies_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetProviderSchema_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetProviderSchema_Response) ProtoMessage() {} + +func (x *GetProviderSchema_Response) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetProviderSchema_Response.ProtoReflect.Descriptor instead. +func (*GetProviderSchema_Response) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{11, 1} +} + +func (x *GetProviderSchema_Response) GetSchema() *ProviderSchema { + if x != nil { + return x.Schema + } + return nil +} + +type Schema_Block struct { + state protoimpl.MessageState `protogen:"open.v1"` + Attributes []*Schema_Attribute `protobuf:"bytes,1,rep,name=attributes,proto3" json:"attributes,omitempty"` + BlockTypes []*Schema_NestedBlock `protobuf:"bytes,2,rep,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"` + Description *Schema_DocString `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Deprecated bool `protobuf:"varint,4,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Schema_Block) Reset() { + *x = Schema_Block{} + mi := &file_dependencies_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schema_Block) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schema_Block) ProtoMessage() {} + +func (x *Schema_Block) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schema_Block.ProtoReflect.Descriptor instead. +func (*Schema_Block) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 0} +} + +func (x *Schema_Block) GetAttributes() []*Schema_Attribute { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *Schema_Block) GetBlockTypes() []*Schema_NestedBlock { + if x != nil { + return x.BlockTypes + } + return nil +} + +func (x *Schema_Block) GetDescription() *Schema_DocString { + if x != nil { + return x.Description + } + return nil +} + +func (x *Schema_Block) GetDeprecated() bool { + if x != nil { + return x.Deprecated + } + return false +} + +type Schema_Attribute struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + NestedType *Schema_Object `protobuf:"bytes,10,opt,name=nested_type,json=nestedType,proto3" json:"nested_type,omitempty"` + Description *Schema_DocString `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + Optional bool `protobuf:"varint,5,opt,name=optional,proto3" json:"optional,omitempty"` + Computed bool `protobuf:"varint,6,opt,name=computed,proto3" json:"computed,omitempty"` + Sensitive bool `protobuf:"varint,7,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + Deprecated bool `protobuf:"varint,8,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Schema_Attribute) Reset() { + *x = Schema_Attribute{} + mi := &file_dependencies_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schema_Attribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schema_Attribute) ProtoMessage() {} + +func (x *Schema_Attribute) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schema_Attribute.ProtoReflect.Descriptor instead. +func (*Schema_Attribute) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 1} +} + +func (x *Schema_Attribute) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Schema_Attribute) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +func (x *Schema_Attribute) GetNestedType() *Schema_Object { + if x != nil { + return x.NestedType + } + return nil +} + +func (x *Schema_Attribute) GetDescription() *Schema_DocString { + if x != nil { + return x.Description + } + return nil +} + +func (x *Schema_Attribute) GetRequired() bool { + if x != nil { + return x.Required + } + return false +} + +func (x *Schema_Attribute) GetOptional() bool { + if x != nil { + return x.Optional + } + return false +} + +func (x *Schema_Attribute) GetComputed() bool { + if x != nil { + return x.Computed + } + return false +} + +func (x *Schema_Attribute) GetSensitive() bool { + if x != nil { + return x.Sensitive + } + return false +} + +func (x *Schema_Attribute) GetDeprecated() bool { + if x != nil { + return x.Deprecated + } + return false +} + +type Schema_NestedBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + Nesting Schema_NestedBlock_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=terraform1.dependencies.Schema_NestedBlock_NestingMode" json:"nesting,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Schema_NestedBlock) Reset() { + *x = Schema_NestedBlock{} + mi := &file_dependencies_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schema_NestedBlock) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schema_NestedBlock) ProtoMessage() {} + +func (x *Schema_NestedBlock) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schema_NestedBlock.ProtoReflect.Descriptor instead. +func (*Schema_NestedBlock) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 2} +} + +func (x *Schema_NestedBlock) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *Schema_NestedBlock) GetBlock() *Schema_Block { + if x != nil { + return x.Block + } + return nil +} + +func (x *Schema_NestedBlock) GetNesting() Schema_NestedBlock_NestingMode { + if x != nil { + return x.Nesting + } + return Schema_NestedBlock_INVALID +} + +type Schema_Object struct { + state protoimpl.MessageState `protogen:"open.v1"` + Attributes []*Schema_Attribute `protobuf:"bytes,1,rep,name=attributes,proto3" json:"attributes,omitempty"` + Nesting Schema_Object_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=terraform1.dependencies.Schema_Object_NestingMode" json:"nesting,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Schema_Object) Reset() { + *x = Schema_Object{} + mi := &file_dependencies_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schema_Object) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schema_Object) ProtoMessage() {} + +func (x *Schema_Object) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schema_Object.ProtoReflect.Descriptor instead. +func (*Schema_Object) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 3} +} + +func (x *Schema_Object) GetAttributes() []*Schema_Attribute { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *Schema_Object) GetNesting() Schema_Object_NestingMode { + if x != nil { + return x.Nesting + } + return Schema_Object_INVALID +} + +type Schema_DocString struct { + state protoimpl.MessageState `protogen:"open.v1"` + Description string `protobuf:"bytes,1,opt,name=description,proto3" json:"description,omitempty"` + Format Schema_DocString_Format `protobuf:"varint,2,opt,name=format,proto3,enum=terraform1.dependencies.Schema_DocString_Format" json:"format,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Schema_DocString) Reset() { + *x = Schema_DocString{} + mi := &file_dependencies_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Schema_DocString) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Schema_DocString) ProtoMessage() {} + +func (x *Schema_DocString) ProtoReflect() protoreflect.Message { + mi := &file_dependencies_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Schema_DocString.ProtoReflect.Descriptor instead. +func (*Schema_DocString) Descriptor() ([]byte, []int) { + return file_dependencies_proto_rawDescGZIP(), []int{13, 4} +} + +func (x *Schema_DocString) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Schema_DocString) GetFormat() Schema_DocString_Format { + if x != nil { + return x.Format + } + return Schema_DocString_PLAIN +} + +var File_dependencies_proto protoreflect.FileDescriptor + +var file_dependencies_proto_rawDesc = string([]byte{ + 0x0a, 0x12, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x17, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x1a, 0x10, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, + 0x7a, 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, + 0x64, 0x6c, 0x65, 0x1a, 0x28, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x61, 0x74, 0x68, 0x1a, 0x3c, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0x5c, 0x0a, 0x11, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x1a, 0x3b, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, 0x14, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x5f, 0x68, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x0a, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x95, 0x02, 0x0a, 0x16, 0x4f, 0x70, + 0x65, 0x6e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, + 0x46, 0x69, 0x6c, 0x65, 0x1a, 0x7d, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x30, 0x0a, 0x14, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, + 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x12, 0x40, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x52, 0x0d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x1a, 0x7c, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, + 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, + 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x70, 0x65, + 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x1a, 0x57, 0x0a, 0x07, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4c, 0x0a, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x5f, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x52, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x42, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, + 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, + 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x22, 0x65, 0x0a, 0x14, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, + 0x1a, 0x41, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x64, + 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, + 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, + 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x1a, 0x0a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0xba, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, + 0x73, 0x1a, 0x41, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36, 0x0a, 0x17, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, + 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, + 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x56, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4a, 0x0a, 0x12, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x11, 0x73, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0xb4, 0x12, 0x0a, + 0x18, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x1a, 0xcd, 0x03, 0x0a, 0x07, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x64, + 0x69, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x63, 0x68, 0x65, 0x44, + 0x69, 0x72, 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, + 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, + 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x7a, 0x0a, 0x14, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, + 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x52, 0x13, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, + 0x64, 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x10, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x50, 0x6c, 0x61, 0x74, 0x66, + 0x6f, 0x72, 0x6d, 0x1a, 0xc3, 0x01, 0x0a, 0x0d, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x18, 0x0a, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, + 0x2a, 0x0a, 0x10, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x6d, 0x69, 0x72, 0x72, 0x6f, 0x72, 0x5f, + 0x64, 0x69, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0e, 0x6c, 0x6f, 0x63, + 0x61, 0x6c, 0x4d, 0x69, 0x72, 0x72, 0x6f, 0x72, 0x44, 0x69, 0x72, 0x12, 0x2e, 0x0a, 0x12, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x6d, 0x69, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x4d, 0x69, 0x72, 0x72, 0x6f, 0x72, 0x55, 0x72, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x69, + 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6e, + 0x63, 0x6c, 0x75, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x42, + 0x08, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0xc7, 0x0e, 0x0a, 0x05, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x12, 0x5b, 0x0a, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x65, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x07, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x12, 0x76, 0x0a, 0x11, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x5f, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, + 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x10, 0x61, 0x6c, 0x72, 0x65, 0x61, 0x64, 0x79, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x12, 0x64, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, + 0x74, 0x5f, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, + 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x07, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x49, 0x6e, 0x12, 0x6e, + 0x0a, 0x0b, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x62, 0x65, 0x67, 0x69, 0x6e, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, + 0x48, 0x00, 0x52, 0x0a, 0x71, 0x75, 0x65, 0x72, 0x79, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x6e, + 0x0a, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, + 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x00, + 0x52, 0x0c, 0x71, 0x75, 0x65, 0x72, 0x79, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x71, + 0x0a, 0x0e, 0x71, 0x75, 0x65, 0x72, 0x79, 0x5f, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x48, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, + 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, + 0x48, 0x00, 0x52, 0x0d, 0x71, 0x75, 0x65, 0x72, 0x79, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, + 0x73, 0x12, 0x65, 0x0a, 0x0b, 0x66, 0x65, 0x74, 0x63, 0x68, 0x5f, 0x62, 0x65, 0x67, 0x69, 0x6e, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, + 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, + 0x46, 0x65, 0x74, 0x63, 0x68, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x48, 0x00, 0x52, 0x0a, 0x66, 0x65, + 0x74, 0x63, 0x68, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x6e, 0x0a, 0x0e, 0x66, 0x65, 0x74, 0x63, + 0x68, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x45, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, + 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, + 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x66, 0x65, 0x74, 0x63, 0x68, + 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x38, 0x0a, 0x0a, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x1a, 0x72, 0x0a, 0x07, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x67, 0x0a, + 0x08, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x4b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, + 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, + 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x52, 0x08, 0x65, 0x78, + 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x1a, 0x52, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x12, 0x1f, 0x0a, + 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1a, + 0x0a, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x4c, 0x0a, 0x0f, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, + 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x4f, 0x0a, 0x10, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x57, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1a, 0x0a, + 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x1a, 0x9c, 0x01, 0x0a, 0x0a, 0x46, 0x65, + 0x74, 0x63, 0x68, 0x42, 0x65, 0x67, 0x69, 0x6e, 0x12, 0x72, 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, + 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, + 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x8f, 0x03, 0x0a, 0x0d, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x72, 0x0a, 0x10, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x47, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x0f, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x71, + 0x0a, 0x0b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x50, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x0a, 0x61, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x2b, 0x0a, 0x12, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x5f, + 0x64, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6b, + 0x65, 0x79, 0x49, 0x64, 0x46, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x70, 0x6c, 0x61, 0x79, 0x22, 0x6a, + 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x0b, 0x0a, 0x07, + 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11, 0x56, 0x45, 0x52, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x5f, 0x43, 0x48, 0x45, 0x43, 0x4b, 0x53, 0x55, 0x4d, 0x10, 0x01, + 0x12, 0x13, 0x0a, 0x0f, 0x4f, 0x46, 0x46, 0x49, 0x43, 0x49, 0x41, 0x4c, 0x5f, 0x53, 0x49, 0x47, + 0x4e, 0x45, 0x44, 0x10, 0x02, 0x12, 0x12, 0x0a, 0x0e, 0x50, 0x41, 0x52, 0x54, 0x4e, 0x45, 0x52, + 0x5f, 0x53, 0x49, 0x47, 0x4e, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x53, 0x45, 0x4c, + 0x46, 0x5f, 0x53, 0x49, 0x47, 0x4e, 0x45, 0x44, 0x10, 0x04, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0xae, 0x01, 0x0a, 0x17, 0x4f, 0x70, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x1a, + 0x53, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x61, + 0x63, 0x68, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, + 0x61, 0x63, 0x68, 0x65, 0x44, 0x69, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x6f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x10, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x50, 0x6c, 0x61, 0x74, + 0x66, 0x6f, 0x72, 0x6d, 0x1a, 0x3e, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, + 0x68, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x22, 0x65, 0x0a, 0x18, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, + 0x1a, 0x3d, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x68, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, + 0x0a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xad, 0x01, 0x0a, 0x12, + 0x47, 0x65, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x1a, 0x3d, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x32, 0x0a, + 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, + 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x1a, 0x58, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, + 0x13, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, + 0x6c, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0x7a, 0x0a, 0x13, 0x47, + 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x49, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x73, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x58, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x13, 0x61, 0x76, 0x61, + 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x52, 0x12, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x22, 0xf0, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x8d, 0x01, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x12, 0x29, + 0x0a, 0x10, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x4b, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3f, 0x0a, 0x06, 0x73, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x69, 0x65, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x52, 0x06, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x22, 0x94, 0x04, 0x0a, 0x0e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x48, 0x0a, + 0x0f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, + 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x77, 0x0a, 0x16, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, + 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x2e, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x6d, 0x61, 0x6e, 0x61, + 0x67, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, + 0x12, 0x6e, 0x0a, 0x13, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, + 0x1a, 0x68, 0x0a, 0x19, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, + 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x65, 0x0a, 0x16, 0x44, 0x61, + 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0xc4, 0x0a, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x3b, 0x0a, 0x05, + 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x1a, 0x8d, 0x02, 0x0a, 0x05, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x12, 0x49, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, + 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x4c, + 0x0a, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x4b, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, + 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x2e, 0x44, 0x6f, 0x63, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, + 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, + 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xdb, 0x02, 0x0a, 0x09, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, + 0x47, 0x0a, 0x0b, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0a, 0x6e, 0x65, + 0x73, 0x74, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, + 0x6f, 0x63, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x1a, 0x0a, + 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, + 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, + 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, + 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0x89, 0x02, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3b, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x12, 0x51, 0x0a, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x37, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, + 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6e, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x22, 0x4d, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, + 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, + 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, 0x03, 0x12, + 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x52, 0x4f, 0x55, + 0x50, 0x10, 0x05, 0x1a, 0xe5, 0x01, 0x0a, 0x06, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x49, + 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x4c, 0x0a, 0x07, 0x6e, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, + 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x22, 0x42, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, + 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, + 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, + 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x10, 0x04, 0x1a, 0x9a, 0x01, 0x0a, 0x09, + 0x44, 0x6f, 0x63, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x48, 0x0a, 0x06, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x30, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x44, 0x6f, 0x63, + 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x2e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x06, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x21, 0x0a, 0x06, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, + 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x41, + 0x52, 0x4b, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x32, 0x87, 0x0d, 0x0a, 0x0c, 0x44, 0x65, 0x70, + 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x12, 0x79, 0x0a, 0x10, 0x4f, 0x70, 0x65, + 0x6e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x31, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, + 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x11, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x42, + 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x16, 0x4f, 0x70, 0x65, 0x6e, 0x44, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x37, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x44, 0x65, 0x70, 0x65, + 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, + 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, + 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x88, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x36, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, + 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x14, + 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, + 0x6f, 0x63, 0x6b, 0x73, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, + 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x44, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0xa0, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65, + 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x69, 0x65, 0x73, 0x12, 0x3e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, + 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, + 0x47, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x44, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x90, 0x01, 0x0a, 0x18, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, + 0x63, 0x68, 0x65, 0x12, 0x39, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, + 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x8e, 0x01, 0x0a, 0x17, 0x4f, 0x70, + 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x38, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, + 0x4f, 0x70, 0x65, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x39, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, + 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x91, 0x01, 0x0a, 0x18, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x39, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, + 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x6f, + 0x73, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x43, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, + 0x0a, 0x12, 0x47, 0x65, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x73, 0x12, 0x33, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x47, + 0x65, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x69, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x82, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x49, 0x6e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, + 0x73, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x74, 0x49, 0x6e, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x75, 0x69, 0x6c, 0x74, + 0x49, 0x6e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x69, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_dependencies_proto_rawDescOnce sync.Once + file_dependencies_proto_rawDescData []byte +) + +func file_dependencies_proto_rawDescGZIP() []byte { + file_dependencies_proto_rawDescOnce.Do(func() { + file_dependencies_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_dependencies_proto_rawDesc), len(file_dependencies_proto_rawDesc))) + }) + return file_dependencies_proto_rawDescData +} + +var file_dependencies_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_dependencies_proto_msgTypes = make([]protoimpl.MessageInfo, 52) +var file_dependencies_proto_goTypes = []any{ + (BuildProviderPluginCache_Event_FetchComplete_AuthResult)(0), // 0: terraform1.dependencies.BuildProviderPluginCache.Event.FetchComplete.AuthResult + (Schema_NestedBlock_NestingMode)(0), // 1: terraform1.dependencies.Schema.NestedBlock.NestingMode + (Schema_Object_NestingMode)(0), // 2: terraform1.dependencies.Schema.Object.NestingMode + (Schema_DocString_Format)(0), // 3: terraform1.dependencies.Schema.DocString.Format + (*OpenSourceBundle)(nil), // 4: terraform1.dependencies.OpenSourceBundle + (*CloseSourceBundle)(nil), // 5: terraform1.dependencies.CloseSourceBundle + (*OpenDependencyLockFile)(nil), // 6: terraform1.dependencies.OpenDependencyLockFile + (*CreateDependencyLocks)(nil), // 7: terraform1.dependencies.CreateDependencyLocks + (*CloseDependencyLocks)(nil), // 8: terraform1.dependencies.CloseDependencyLocks + (*GetLockedProviderDependencies)(nil), // 9: terraform1.dependencies.GetLockedProviderDependencies + (*BuildProviderPluginCache)(nil), // 10: terraform1.dependencies.BuildProviderPluginCache + (*OpenProviderPluginCache)(nil), // 11: terraform1.dependencies.OpenProviderPluginCache + (*CloseProviderPluginCache)(nil), // 12: terraform1.dependencies.CloseProviderPluginCache + (*GetCachedProviders)(nil), // 13: terraform1.dependencies.GetCachedProviders + (*GetBuiltInProviders)(nil), // 14: terraform1.dependencies.GetBuiltInProviders + (*GetProviderSchema)(nil), // 15: terraform1.dependencies.GetProviderSchema + (*ProviderSchema)(nil), // 16: terraform1.dependencies.ProviderSchema + (*Schema)(nil), // 17: terraform1.dependencies.Schema + (*OpenSourceBundle_Request)(nil), // 18: terraform1.dependencies.OpenSourceBundle.Request + (*OpenSourceBundle_Response)(nil), // 19: terraform1.dependencies.OpenSourceBundle.Response + (*CloseSourceBundle_Request)(nil), // 20: terraform1.dependencies.CloseSourceBundle.Request + (*CloseSourceBundle_Response)(nil), // 21: terraform1.dependencies.CloseSourceBundle.Response + (*OpenDependencyLockFile_Request)(nil), // 22: terraform1.dependencies.OpenDependencyLockFile.Request + (*OpenDependencyLockFile_Response)(nil), // 23: terraform1.dependencies.OpenDependencyLockFile.Response + (*CreateDependencyLocks_Request)(nil), // 24: terraform1.dependencies.CreateDependencyLocks.Request + (*CreateDependencyLocks_Response)(nil), // 25: terraform1.dependencies.CreateDependencyLocks.Response + (*CloseDependencyLocks_Request)(nil), // 26: terraform1.dependencies.CloseDependencyLocks.Request + (*CloseDependencyLocks_Response)(nil), // 27: terraform1.dependencies.CloseDependencyLocks.Response + (*GetLockedProviderDependencies_Request)(nil), // 28: terraform1.dependencies.GetLockedProviderDependencies.Request + (*GetLockedProviderDependencies_Response)(nil), // 29: terraform1.dependencies.GetLockedProviderDependencies.Response + (*BuildProviderPluginCache_Request)(nil), // 30: terraform1.dependencies.BuildProviderPluginCache.Request + (*BuildProviderPluginCache_Event)(nil), // 31: terraform1.dependencies.BuildProviderPluginCache.Event + (*BuildProviderPluginCache_Request_InstallMethod)(nil), // 32: terraform1.dependencies.BuildProviderPluginCache.Request.InstallMethod + (*BuildProviderPluginCache_Event_Pending)(nil), // 33: terraform1.dependencies.BuildProviderPluginCache.Event.Pending + (*BuildProviderPluginCache_Event_ProviderConstraints)(nil), // 34: terraform1.dependencies.BuildProviderPluginCache.Event.ProviderConstraints + (*BuildProviderPluginCache_Event_ProviderVersion)(nil), // 35: terraform1.dependencies.BuildProviderPluginCache.Event.ProviderVersion + (*BuildProviderPluginCache_Event_ProviderWarnings)(nil), // 36: terraform1.dependencies.BuildProviderPluginCache.Event.ProviderWarnings + (*BuildProviderPluginCache_Event_FetchBegin)(nil), // 37: terraform1.dependencies.BuildProviderPluginCache.Event.FetchBegin + (*BuildProviderPluginCache_Event_FetchComplete)(nil), // 38: terraform1.dependencies.BuildProviderPluginCache.Event.FetchComplete + (*OpenProviderPluginCache_Request)(nil), // 39: terraform1.dependencies.OpenProviderPluginCache.Request + (*OpenProviderPluginCache_Response)(nil), // 40: terraform1.dependencies.OpenProviderPluginCache.Response + (*CloseProviderPluginCache_Request)(nil), // 41: terraform1.dependencies.CloseProviderPluginCache.Request + (*CloseProviderPluginCache_Response)(nil), // 42: terraform1.dependencies.CloseProviderPluginCache.Response + (*GetCachedProviders_Request)(nil), // 43: terraform1.dependencies.GetCachedProviders.Request + (*GetCachedProviders_Response)(nil), // 44: terraform1.dependencies.GetCachedProviders.Response + (*GetBuiltInProviders_Request)(nil), // 45: terraform1.dependencies.GetBuiltInProviders.Request + (*GetBuiltInProviders_Response)(nil), // 46: terraform1.dependencies.GetBuiltInProviders.Response + (*GetProviderSchema_Request)(nil), // 47: terraform1.dependencies.GetProviderSchema.Request + (*GetProviderSchema_Response)(nil), // 48: terraform1.dependencies.GetProviderSchema.Response + nil, // 49: terraform1.dependencies.ProviderSchema.ManagedResourceTypesEntry + nil, // 50: terraform1.dependencies.ProviderSchema.DataResourceTypesEntry + (*Schema_Block)(nil), // 51: terraform1.dependencies.Schema.Block + (*Schema_Attribute)(nil), // 52: terraform1.dependencies.Schema.Attribute + (*Schema_NestedBlock)(nil), // 53: terraform1.dependencies.Schema.NestedBlock + (*Schema_Object)(nil), // 54: terraform1.dependencies.Schema.Object + (*Schema_DocString)(nil), // 55: terraform1.dependencies.Schema.DocString + (*terraform1.SourceAddress)(nil), // 56: terraform1.SourceAddress + (*terraform1.Diagnostic)(nil), // 57: terraform1.Diagnostic + (*terraform1.ProviderPackage)(nil), // 58: terraform1.ProviderPackage +} +var file_dependencies_proto_depIdxs = []int32{ + 17, // 0: terraform1.dependencies.ProviderSchema.provider_config:type_name -> terraform1.dependencies.Schema + 49, // 1: terraform1.dependencies.ProviderSchema.managed_resource_types:type_name -> terraform1.dependencies.ProviderSchema.ManagedResourceTypesEntry + 50, // 2: terraform1.dependencies.ProviderSchema.data_resource_types:type_name -> terraform1.dependencies.ProviderSchema.DataResourceTypesEntry + 51, // 3: terraform1.dependencies.Schema.block:type_name -> terraform1.dependencies.Schema.Block + 56, // 4: terraform1.dependencies.OpenDependencyLockFile.Request.source_address:type_name -> terraform1.SourceAddress + 57, // 5: terraform1.dependencies.OpenDependencyLockFile.Response.diagnostics:type_name -> terraform1.Diagnostic + 58, // 6: terraform1.dependencies.CreateDependencyLocks.Request.provider_selections:type_name -> terraform1.ProviderPackage + 58, // 7: terraform1.dependencies.GetLockedProviderDependencies.Response.selected_providers:type_name -> terraform1.ProviderPackage + 32, // 8: terraform1.dependencies.BuildProviderPluginCache.Request.installation_methods:type_name -> terraform1.dependencies.BuildProviderPluginCache.Request.InstallMethod + 33, // 9: terraform1.dependencies.BuildProviderPluginCache.Event.pending:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.Pending + 35, // 10: terraform1.dependencies.BuildProviderPluginCache.Event.already_installed:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderVersion + 35, // 11: terraform1.dependencies.BuildProviderPluginCache.Event.built_in:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderVersion + 34, // 12: terraform1.dependencies.BuildProviderPluginCache.Event.query_begin:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderConstraints + 35, // 13: terraform1.dependencies.BuildProviderPluginCache.Event.query_success:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderVersion + 36, // 14: terraform1.dependencies.BuildProviderPluginCache.Event.query_warnings:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderWarnings + 37, // 15: terraform1.dependencies.BuildProviderPluginCache.Event.fetch_begin:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.FetchBegin + 38, // 16: terraform1.dependencies.BuildProviderPluginCache.Event.fetch_complete:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.FetchComplete + 57, // 17: terraform1.dependencies.BuildProviderPluginCache.Event.diagnostic:type_name -> terraform1.Diagnostic + 34, // 18: terraform1.dependencies.BuildProviderPluginCache.Event.Pending.expected:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderConstraints + 35, // 19: terraform1.dependencies.BuildProviderPluginCache.Event.FetchBegin.provider_version:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderVersion + 35, // 20: terraform1.dependencies.BuildProviderPluginCache.Event.FetchComplete.provider_version:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.ProviderVersion + 0, // 21: terraform1.dependencies.BuildProviderPluginCache.Event.FetchComplete.auth_result:type_name -> terraform1.dependencies.BuildProviderPluginCache.Event.FetchComplete.AuthResult + 58, // 22: terraform1.dependencies.GetCachedProviders.Response.available_providers:type_name -> terraform1.ProviderPackage + 58, // 23: terraform1.dependencies.GetBuiltInProviders.Response.available_providers:type_name -> terraform1.ProviderPackage + 16, // 24: terraform1.dependencies.GetProviderSchema.Response.schema:type_name -> terraform1.dependencies.ProviderSchema + 17, // 25: terraform1.dependencies.ProviderSchema.ManagedResourceTypesEntry.value:type_name -> terraform1.dependencies.Schema + 17, // 26: terraform1.dependencies.ProviderSchema.DataResourceTypesEntry.value:type_name -> terraform1.dependencies.Schema + 52, // 27: terraform1.dependencies.Schema.Block.attributes:type_name -> terraform1.dependencies.Schema.Attribute + 53, // 28: terraform1.dependencies.Schema.Block.block_types:type_name -> terraform1.dependencies.Schema.NestedBlock + 55, // 29: terraform1.dependencies.Schema.Block.description:type_name -> terraform1.dependencies.Schema.DocString + 54, // 30: terraform1.dependencies.Schema.Attribute.nested_type:type_name -> terraform1.dependencies.Schema.Object + 55, // 31: terraform1.dependencies.Schema.Attribute.description:type_name -> terraform1.dependencies.Schema.DocString + 51, // 32: terraform1.dependencies.Schema.NestedBlock.block:type_name -> terraform1.dependencies.Schema.Block + 1, // 33: terraform1.dependencies.Schema.NestedBlock.nesting:type_name -> terraform1.dependencies.Schema.NestedBlock.NestingMode + 52, // 34: terraform1.dependencies.Schema.Object.attributes:type_name -> terraform1.dependencies.Schema.Attribute + 2, // 35: terraform1.dependencies.Schema.Object.nesting:type_name -> terraform1.dependencies.Schema.Object.NestingMode + 3, // 36: terraform1.dependencies.Schema.DocString.format:type_name -> terraform1.dependencies.Schema.DocString.Format + 18, // 37: terraform1.dependencies.Dependencies.OpenSourceBundle:input_type -> terraform1.dependencies.OpenSourceBundle.Request + 20, // 38: terraform1.dependencies.Dependencies.CloseSourceBundle:input_type -> terraform1.dependencies.CloseSourceBundle.Request + 22, // 39: terraform1.dependencies.Dependencies.OpenDependencyLockFile:input_type -> terraform1.dependencies.OpenDependencyLockFile.Request + 24, // 40: terraform1.dependencies.Dependencies.CreateDependencyLocks:input_type -> terraform1.dependencies.CreateDependencyLocks.Request + 26, // 41: terraform1.dependencies.Dependencies.CloseDependencyLocks:input_type -> terraform1.dependencies.CloseDependencyLocks.Request + 28, // 42: terraform1.dependencies.Dependencies.GetLockedProviderDependencies:input_type -> terraform1.dependencies.GetLockedProviderDependencies.Request + 30, // 43: terraform1.dependencies.Dependencies.BuildProviderPluginCache:input_type -> terraform1.dependencies.BuildProviderPluginCache.Request + 39, // 44: terraform1.dependencies.Dependencies.OpenProviderPluginCache:input_type -> terraform1.dependencies.OpenProviderPluginCache.Request + 41, // 45: terraform1.dependencies.Dependencies.CloseProviderPluginCache:input_type -> terraform1.dependencies.CloseProviderPluginCache.Request + 43, // 46: terraform1.dependencies.Dependencies.GetCachedProviders:input_type -> terraform1.dependencies.GetCachedProviders.Request + 45, // 47: terraform1.dependencies.Dependencies.GetBuiltInProviders:input_type -> terraform1.dependencies.GetBuiltInProviders.Request + 47, // 48: terraform1.dependencies.Dependencies.GetProviderSchema:input_type -> terraform1.dependencies.GetProviderSchema.Request + 19, // 49: terraform1.dependencies.Dependencies.OpenSourceBundle:output_type -> terraform1.dependencies.OpenSourceBundle.Response + 21, // 50: terraform1.dependencies.Dependencies.CloseSourceBundle:output_type -> terraform1.dependencies.CloseSourceBundle.Response + 23, // 51: terraform1.dependencies.Dependencies.OpenDependencyLockFile:output_type -> terraform1.dependencies.OpenDependencyLockFile.Response + 25, // 52: terraform1.dependencies.Dependencies.CreateDependencyLocks:output_type -> terraform1.dependencies.CreateDependencyLocks.Response + 27, // 53: terraform1.dependencies.Dependencies.CloseDependencyLocks:output_type -> terraform1.dependencies.CloseDependencyLocks.Response + 29, // 54: terraform1.dependencies.Dependencies.GetLockedProviderDependencies:output_type -> terraform1.dependencies.GetLockedProviderDependencies.Response + 31, // 55: terraform1.dependencies.Dependencies.BuildProviderPluginCache:output_type -> terraform1.dependencies.BuildProviderPluginCache.Event + 40, // 56: terraform1.dependencies.Dependencies.OpenProviderPluginCache:output_type -> terraform1.dependencies.OpenProviderPluginCache.Response + 42, // 57: terraform1.dependencies.Dependencies.CloseProviderPluginCache:output_type -> terraform1.dependencies.CloseProviderPluginCache.Response + 44, // 58: terraform1.dependencies.Dependencies.GetCachedProviders:output_type -> terraform1.dependencies.GetCachedProviders.Response + 46, // 59: terraform1.dependencies.Dependencies.GetBuiltInProviders:output_type -> terraform1.dependencies.GetBuiltInProviders.Response + 48, // 60: terraform1.dependencies.Dependencies.GetProviderSchema:output_type -> terraform1.dependencies.GetProviderSchema.Response + 49, // [49:61] is the sub-list for method output_type + 37, // [37:49] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name +} + +func init() { file_dependencies_proto_init() } +func file_dependencies_proto_init() { + if File_dependencies_proto != nil { + return + } + file_dependencies_proto_msgTypes[27].OneofWrappers = []any{ + (*BuildProviderPluginCache_Event_Pending_)(nil), + (*BuildProviderPluginCache_Event_AlreadyInstalled)(nil), + (*BuildProviderPluginCache_Event_BuiltIn)(nil), + (*BuildProviderPluginCache_Event_QueryBegin)(nil), + (*BuildProviderPluginCache_Event_QuerySuccess)(nil), + (*BuildProviderPluginCache_Event_QueryWarnings)(nil), + (*BuildProviderPluginCache_Event_FetchBegin_)(nil), + (*BuildProviderPluginCache_Event_FetchComplete_)(nil), + (*BuildProviderPluginCache_Event_Diagnostic)(nil), + } + file_dependencies_proto_msgTypes[28].OneofWrappers = []any{ + (*BuildProviderPluginCache_Request_InstallMethod_Direct)(nil), + (*BuildProviderPluginCache_Request_InstallMethod_LocalMirrorDir)(nil), + (*BuildProviderPluginCache_Request_InstallMethod_NetworkMirrorUrl)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_dependencies_proto_rawDesc), len(file_dependencies_proto_rawDesc)), + NumEnums: 4, + NumMessages: 52, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_dependencies_proto_goTypes, + DependencyIndexes: file_dependencies_proto_depIdxs, + EnumInfos: file_dependencies_proto_enumTypes, + MessageInfos: file_dependencies_proto_msgTypes, + }.Build() + File_dependencies_proto = out.File + file_dependencies_proto_goTypes = nil + file_dependencies_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// DependenciesClient is the client API for Dependencies service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type DependenciesClient interface { + // Opens a source bundle that was already extracted into the filesystem + // somewhere, returning an opaque source bundle handle that can be used for + // subsequent operations. + OpenSourceBundle(ctx context.Context, in *OpenSourceBundle_Request, opts ...grpc.CallOption) (*OpenSourceBundle_Response, error) + // Closes a previously-opened source bundle, invalidating the given handle + // and therefore making it safe to delete or modify the bundle directory + // on disk. + CloseSourceBundle(ctx context.Context, in *CloseSourceBundle_Request, opts ...grpc.CallOption) (*CloseSourceBundle_Response, error) + // Reads and parses an existing dependency lock file from the filesystem, + // returning a dependency locks handle. + // + // This function parses a user-provided source file, and so invalid content + // in that file is treated as diagnostics in a successful response rather + // than as an RPC error. Callers must check whether the dependency locks + // handle in the response is set (non-zero) before using it, and treat + // an unset handle as indicating a user error which is described in the + // accompanying diagnostics. Diagnostics can also be returned along with + // a valid handle, e.g. if there are non-blocking warning diagnostics. + OpenDependencyLockFile(ctx context.Context, in *OpenDependencyLockFile_Request, opts ...grpc.CallOption) (*OpenDependencyLockFile_Response, error) + // Creates an in-memory-only dependency locks handle with a fixed set of + // dependency selections provided as arguments. + CreateDependencyLocks(ctx context.Context, in *CreateDependencyLocks_Request, opts ...grpc.CallOption) (*CreateDependencyLocks_Response, error) + CloseDependencyLocks(ctx context.Context, in *CloseDependencyLocks_Request, opts ...grpc.CallOption) (*CloseDependencyLocks_Response, error) + // Returns information about the provider version selections in a + // dependency locks object. + GetLockedProviderDependencies(ctx context.Context, in *GetLockedProviderDependencies_Request, opts ...grpc.CallOption) (*GetLockedProviderDependencies_Response, error) + // Populates a new provider plugin cache directory in the local filesystem + // based on the provider version selections in a given dependency locks + // object. + // + // This particular RPC can only install already-selected provider packages + // recorded in a dependency locks object; it does not support "upgrading" + // provider selections to newer versions as a CLI user would do with + // "terraform init -upgrade", because there would be no way to then + // commit the updated locks to disk as a lock file. + BuildProviderPluginCache(ctx context.Context, in *BuildProviderPluginCache_Request, opts ...grpc.CallOption) (Dependencies_BuildProviderPluginCacheClient, error) + // Opens an existing local filesystem directory as a provider plugin cache + // directory, returning a plugin cache handle that can be used with other + // RPC operations. + OpenProviderPluginCache(ctx context.Context, in *OpenProviderPluginCache_Request, opts ...grpc.CallOption) (*OpenProviderPluginCache_Response, error) + CloseProviderPluginCache(ctx context.Context, in *CloseProviderPluginCache_Request, opts ...grpc.CallOption) (*CloseProviderPluginCache_Response, error) + // Returns information about the specific provider packages that are + // available in the given provider plugin cache. + GetCachedProviders(ctx context.Context, in *GetCachedProviders_Request, opts ...grpc.CallOption) (*GetCachedProviders_Response, error) + // Returns information about the built-in providers that are compiled in + // to this Terraform Core server. + GetBuiltInProviders(ctx context.Context, in *GetBuiltInProviders_Request, opts ...grpc.CallOption) (*GetBuiltInProviders_Response, error) + // Returns a description of the schema for a particular provider in a + // given provider plugin cache, or of a particular built-in provider + // known to this version of Terraform Core. + // + // WARNING: This operation requires executing the selected provider plugin, + // which therefore allows it to run arbitrary code as a child process of + // this Terraform Core server, with access to all of the same resources. + // This should typically be used only with providers explicitly selected + // in a dependency lock file, so users can control what external code + // has the potential to run in a context that probably has access to + // private source code and other sensitive information. + GetProviderSchema(ctx context.Context, in *GetProviderSchema_Request, opts ...grpc.CallOption) (*GetProviderSchema_Response, error) +} + +type dependenciesClient struct { + cc grpc.ClientConnInterface +} + +func NewDependenciesClient(cc grpc.ClientConnInterface) DependenciesClient { + return &dependenciesClient{cc} +} + +func (c *dependenciesClient) OpenSourceBundle(ctx context.Context, in *OpenSourceBundle_Request, opts ...grpc.CallOption) (*OpenSourceBundle_Response, error) { + out := new(OpenSourceBundle_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/OpenSourceBundle", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) CloseSourceBundle(ctx context.Context, in *CloseSourceBundle_Request, opts ...grpc.CallOption) (*CloseSourceBundle_Response, error) { + out := new(CloseSourceBundle_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/CloseSourceBundle", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) OpenDependencyLockFile(ctx context.Context, in *OpenDependencyLockFile_Request, opts ...grpc.CallOption) (*OpenDependencyLockFile_Response, error) { + out := new(OpenDependencyLockFile_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/OpenDependencyLockFile", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) CreateDependencyLocks(ctx context.Context, in *CreateDependencyLocks_Request, opts ...grpc.CallOption) (*CreateDependencyLocks_Response, error) { + out := new(CreateDependencyLocks_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/CreateDependencyLocks", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) CloseDependencyLocks(ctx context.Context, in *CloseDependencyLocks_Request, opts ...grpc.CallOption) (*CloseDependencyLocks_Response, error) { + out := new(CloseDependencyLocks_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/CloseDependencyLocks", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) GetLockedProviderDependencies(ctx context.Context, in *GetLockedProviderDependencies_Request, opts ...grpc.CallOption) (*GetLockedProviderDependencies_Response, error) { + out := new(GetLockedProviderDependencies_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/GetLockedProviderDependencies", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) BuildProviderPluginCache(ctx context.Context, in *BuildProviderPluginCache_Request, opts ...grpc.CallOption) (Dependencies_BuildProviderPluginCacheClient, error) { + stream, err := c.cc.NewStream(ctx, &_Dependencies_serviceDesc.Streams[0], "/terraform1.dependencies.Dependencies/BuildProviderPluginCache", opts...) + if err != nil { + return nil, err + } + x := &dependenciesBuildProviderPluginCacheClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Dependencies_BuildProviderPluginCacheClient interface { + Recv() (*BuildProviderPluginCache_Event, error) + grpc.ClientStream +} + +type dependenciesBuildProviderPluginCacheClient struct { + grpc.ClientStream +} + +func (x *dependenciesBuildProviderPluginCacheClient) Recv() (*BuildProviderPluginCache_Event, error) { + m := new(BuildProviderPluginCache_Event) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *dependenciesClient) OpenProviderPluginCache(ctx context.Context, in *OpenProviderPluginCache_Request, opts ...grpc.CallOption) (*OpenProviderPluginCache_Response, error) { + out := new(OpenProviderPluginCache_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/OpenProviderPluginCache", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) CloseProviderPluginCache(ctx context.Context, in *CloseProviderPluginCache_Request, opts ...grpc.CallOption) (*CloseProviderPluginCache_Response, error) { + out := new(CloseProviderPluginCache_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/CloseProviderPluginCache", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) GetCachedProviders(ctx context.Context, in *GetCachedProviders_Request, opts ...grpc.CallOption) (*GetCachedProviders_Response, error) { + out := new(GetCachedProviders_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/GetCachedProviders", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) GetBuiltInProviders(ctx context.Context, in *GetBuiltInProviders_Request, opts ...grpc.CallOption) (*GetBuiltInProviders_Response, error) { + out := new(GetBuiltInProviders_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/GetBuiltInProviders", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *dependenciesClient) GetProviderSchema(ctx context.Context, in *GetProviderSchema_Request, opts ...grpc.CallOption) (*GetProviderSchema_Response, error) { + out := new(GetProviderSchema_Response) + err := c.cc.Invoke(ctx, "/terraform1.dependencies.Dependencies/GetProviderSchema", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DependenciesServer is the server API for Dependencies service. +type DependenciesServer interface { + // Opens a source bundle that was already extracted into the filesystem + // somewhere, returning an opaque source bundle handle that can be used for + // subsequent operations. + OpenSourceBundle(context.Context, *OpenSourceBundle_Request) (*OpenSourceBundle_Response, error) + // Closes a previously-opened source bundle, invalidating the given handle + // and therefore making it safe to delete or modify the bundle directory + // on disk. + CloseSourceBundle(context.Context, *CloseSourceBundle_Request) (*CloseSourceBundle_Response, error) + // Reads and parses an existing dependency lock file from the filesystem, + // returning a dependency locks handle. + // + // This function parses a user-provided source file, and so invalid content + // in that file is treated as diagnostics in a successful response rather + // than as an RPC error. Callers must check whether the dependency locks + // handle in the response is set (non-zero) before using it, and treat + // an unset handle as indicating a user error which is described in the + // accompanying diagnostics. Diagnostics can also be returned along with + // a valid handle, e.g. if there are non-blocking warning diagnostics. + OpenDependencyLockFile(context.Context, *OpenDependencyLockFile_Request) (*OpenDependencyLockFile_Response, error) + // Creates an in-memory-only dependency locks handle with a fixed set of + // dependency selections provided as arguments. + CreateDependencyLocks(context.Context, *CreateDependencyLocks_Request) (*CreateDependencyLocks_Response, error) + CloseDependencyLocks(context.Context, *CloseDependencyLocks_Request) (*CloseDependencyLocks_Response, error) + // Returns information about the provider version selections in a + // dependency locks object. + GetLockedProviderDependencies(context.Context, *GetLockedProviderDependencies_Request) (*GetLockedProviderDependencies_Response, error) + // Populates a new provider plugin cache directory in the local filesystem + // based on the provider version selections in a given dependency locks + // object. + // + // This particular RPC can only install already-selected provider packages + // recorded in a dependency locks object; it does not support "upgrading" + // provider selections to newer versions as a CLI user would do with + // "terraform init -upgrade", because there would be no way to then + // commit the updated locks to disk as a lock file. + BuildProviderPluginCache(*BuildProviderPluginCache_Request, Dependencies_BuildProviderPluginCacheServer) error + // Opens an existing local filesystem directory as a provider plugin cache + // directory, returning a plugin cache handle that can be used with other + // RPC operations. + OpenProviderPluginCache(context.Context, *OpenProviderPluginCache_Request) (*OpenProviderPluginCache_Response, error) + CloseProviderPluginCache(context.Context, *CloseProviderPluginCache_Request) (*CloseProviderPluginCache_Response, error) + // Returns information about the specific provider packages that are + // available in the given provider plugin cache. + GetCachedProviders(context.Context, *GetCachedProviders_Request) (*GetCachedProviders_Response, error) + // Returns information about the built-in providers that are compiled in + // to this Terraform Core server. + GetBuiltInProviders(context.Context, *GetBuiltInProviders_Request) (*GetBuiltInProviders_Response, error) + // Returns a description of the schema for a particular provider in a + // given provider plugin cache, or of a particular built-in provider + // known to this version of Terraform Core. + // + // WARNING: This operation requires executing the selected provider plugin, + // which therefore allows it to run arbitrary code as a child process of + // this Terraform Core server, with access to all of the same resources. + // This should typically be used only with providers explicitly selected + // in a dependency lock file, so users can control what external code + // has the potential to run in a context that probably has access to + // private source code and other sensitive information. + GetProviderSchema(context.Context, *GetProviderSchema_Request) (*GetProviderSchema_Response, error) +} + +// UnimplementedDependenciesServer can be embedded to have forward compatible implementations. +type UnimplementedDependenciesServer struct { +} + +func (*UnimplementedDependenciesServer) OpenSourceBundle(context.Context, *OpenSourceBundle_Request) (*OpenSourceBundle_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenSourceBundle not implemented") +} +func (*UnimplementedDependenciesServer) CloseSourceBundle(context.Context, *CloseSourceBundle_Request) (*CloseSourceBundle_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseSourceBundle not implemented") +} +func (*UnimplementedDependenciesServer) OpenDependencyLockFile(context.Context, *OpenDependencyLockFile_Request) (*OpenDependencyLockFile_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenDependencyLockFile not implemented") +} +func (*UnimplementedDependenciesServer) CreateDependencyLocks(context.Context, *CreateDependencyLocks_Request) (*CreateDependencyLocks_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateDependencyLocks not implemented") +} +func (*UnimplementedDependenciesServer) CloseDependencyLocks(context.Context, *CloseDependencyLocks_Request) (*CloseDependencyLocks_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseDependencyLocks not implemented") +} +func (*UnimplementedDependenciesServer) GetLockedProviderDependencies(context.Context, *GetLockedProviderDependencies_Request) (*GetLockedProviderDependencies_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetLockedProviderDependencies not implemented") +} +func (*UnimplementedDependenciesServer) BuildProviderPluginCache(*BuildProviderPluginCache_Request, Dependencies_BuildProviderPluginCacheServer) error { + return status.Errorf(codes.Unimplemented, "method BuildProviderPluginCache not implemented") +} +func (*UnimplementedDependenciesServer) OpenProviderPluginCache(context.Context, *OpenProviderPluginCache_Request) (*OpenProviderPluginCache_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenProviderPluginCache not implemented") +} +func (*UnimplementedDependenciesServer) CloseProviderPluginCache(context.Context, *CloseProviderPluginCache_Request) (*CloseProviderPluginCache_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseProviderPluginCache not implemented") +} +func (*UnimplementedDependenciesServer) GetCachedProviders(context.Context, *GetCachedProviders_Request) (*GetCachedProviders_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCachedProviders not implemented") +} +func (*UnimplementedDependenciesServer) GetBuiltInProviders(context.Context, *GetBuiltInProviders_Request) (*GetBuiltInProviders_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetBuiltInProviders not implemented") +} +func (*UnimplementedDependenciesServer) GetProviderSchema(context.Context, *GetProviderSchema_Request) (*GetProviderSchema_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetProviderSchema not implemented") +} + +func RegisterDependenciesServer(s *grpc.Server, srv DependenciesServer) { + s.RegisterService(&_Dependencies_serviceDesc, srv) +} + +func _Dependencies_OpenSourceBundle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenSourceBundle_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).OpenSourceBundle(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/OpenSourceBundle", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).OpenSourceBundle(ctx, req.(*OpenSourceBundle_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_CloseSourceBundle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseSourceBundle_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).CloseSourceBundle(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/CloseSourceBundle", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).CloseSourceBundle(ctx, req.(*CloseSourceBundle_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_OpenDependencyLockFile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenDependencyLockFile_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).OpenDependencyLockFile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/OpenDependencyLockFile", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).OpenDependencyLockFile(ctx, req.(*OpenDependencyLockFile_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_CreateDependencyLocks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateDependencyLocks_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).CreateDependencyLocks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/CreateDependencyLocks", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).CreateDependencyLocks(ctx, req.(*CreateDependencyLocks_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_CloseDependencyLocks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseDependencyLocks_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).CloseDependencyLocks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/CloseDependencyLocks", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).CloseDependencyLocks(ctx, req.(*CloseDependencyLocks_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_GetLockedProviderDependencies_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetLockedProviderDependencies_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).GetLockedProviderDependencies(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/GetLockedProviderDependencies", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).GetLockedProviderDependencies(ctx, req.(*GetLockedProviderDependencies_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_BuildProviderPluginCache_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(BuildProviderPluginCache_Request) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(DependenciesServer).BuildProviderPluginCache(m, &dependenciesBuildProviderPluginCacheServer{stream}) +} + +type Dependencies_BuildProviderPluginCacheServer interface { + Send(*BuildProviderPluginCache_Event) error + grpc.ServerStream +} + +type dependenciesBuildProviderPluginCacheServer struct { + grpc.ServerStream +} + +func (x *dependenciesBuildProviderPluginCacheServer) Send(m *BuildProviderPluginCache_Event) error { + return x.ServerStream.SendMsg(m) +} + +func _Dependencies_OpenProviderPluginCache_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenProviderPluginCache_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).OpenProviderPluginCache(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/OpenProviderPluginCache", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).OpenProviderPluginCache(ctx, req.(*OpenProviderPluginCache_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_CloseProviderPluginCache_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseProviderPluginCache_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).CloseProviderPluginCache(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/CloseProviderPluginCache", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).CloseProviderPluginCache(ctx, req.(*CloseProviderPluginCache_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_GetCachedProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCachedProviders_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).GetCachedProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/GetCachedProviders", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).GetCachedProviders(ctx, req.(*GetCachedProviders_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_GetBuiltInProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetBuiltInProviders_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).GetBuiltInProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/GetBuiltInProviders", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).GetBuiltInProviders(ctx, req.(*GetBuiltInProviders_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Dependencies_GetProviderSchema_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetProviderSchema_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DependenciesServer).GetProviderSchema(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.dependencies.Dependencies/GetProviderSchema", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DependenciesServer).GetProviderSchema(ctx, req.(*GetProviderSchema_Request)) + } + return interceptor(ctx, in, info, handler) +} + +var _Dependencies_serviceDesc = grpc.ServiceDesc{ + ServiceName: "terraform1.dependencies.Dependencies", + HandlerType: (*DependenciesServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "OpenSourceBundle", + Handler: _Dependencies_OpenSourceBundle_Handler, + }, + { + MethodName: "CloseSourceBundle", + Handler: _Dependencies_CloseSourceBundle_Handler, + }, + { + MethodName: "OpenDependencyLockFile", + Handler: _Dependencies_OpenDependencyLockFile_Handler, + }, + { + MethodName: "CreateDependencyLocks", + Handler: _Dependencies_CreateDependencyLocks_Handler, + }, + { + MethodName: "CloseDependencyLocks", + Handler: _Dependencies_CloseDependencyLocks_Handler, + }, + { + MethodName: "GetLockedProviderDependencies", + Handler: _Dependencies_GetLockedProviderDependencies_Handler, + }, + { + MethodName: "OpenProviderPluginCache", + Handler: _Dependencies_OpenProviderPluginCache_Handler, + }, + { + MethodName: "CloseProviderPluginCache", + Handler: _Dependencies_CloseProviderPluginCache_Handler, + }, + { + MethodName: "GetCachedProviders", + Handler: _Dependencies_GetCachedProviders_Handler, + }, + { + MethodName: "GetBuiltInProviders", + Handler: _Dependencies_GetBuiltInProviders_Handler, + }, + { + MethodName: "GetProviderSchema", + Handler: _Dependencies_GetProviderSchema_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "BuildProviderPluginCache", + Handler: _Dependencies_BuildProviderPluginCache_Handler, + ServerStreams: true, + }, + }, + Metadata: "dependencies.proto", +} diff --git a/internal/rpcapi/terraform1/dependencies/dependencies.proto b/internal/rpcapi/terraform1/dependencies/dependencies.proto new file mode 100644 index 0000000000..40d142d64f --- /dev/null +++ b/internal/rpcapi/terraform1/dependencies/dependencies.proto @@ -0,0 +1,379 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package terraform1.dependencies; + +import "terraform1.proto"; + +service Dependencies { + // Opens a source bundle that was already extracted into the filesystem + // somewhere, returning an opaque source bundle handle that can be used for + // subsequent operations. + rpc OpenSourceBundle(OpenSourceBundle.Request) returns (OpenSourceBundle.Response); + + // Closes a previously-opened source bundle, invalidating the given handle + // and therefore making it safe to delete or modify the bundle directory + // on disk. + rpc CloseSourceBundle(CloseSourceBundle.Request) returns (CloseSourceBundle.Response); + + // Reads and parses an existing dependency lock file from the filesystem, + // returning a dependency locks handle. + // + // This function parses a user-provided source file, and so invalid content + // in that file is treated as diagnostics in a successful response rather + // than as an RPC error. Callers must check whether the dependency locks + // handle in the response is set (non-zero) before using it, and treat + // an unset handle as indicating a user error which is described in the + // accompanying diagnostics. Diagnostics can also be returned along with + // a valid handle, e.g. if there are non-blocking warning diagnostics. + rpc OpenDependencyLockFile(OpenDependencyLockFile.Request) returns (OpenDependencyLockFile.Response); + + // Creates an in-memory-only dependency locks handle with a fixed set of + // dependency selections provided as arguments. + rpc CreateDependencyLocks(CreateDependencyLocks.Request) returns (CreateDependencyLocks.Response); + + rpc CloseDependencyLocks(CloseDependencyLocks.Request) returns (CloseDependencyLocks.Response); + + // Returns information about the provider version selections in a + // dependency locks object. + rpc GetLockedProviderDependencies(GetLockedProviderDependencies.Request) returns (GetLockedProviderDependencies.Response); + + // Populates a new provider plugin cache directory in the local filesystem + // based on the provider version selections in a given dependency locks + // object. + // + // This particular RPC can only install already-selected provider packages + // recorded in a dependency locks object; it does not support "upgrading" + // provider selections to newer versions as a CLI user would do with + // "terraform init -upgrade", because there would be no way to then + // commit the updated locks to disk as a lock file. + rpc BuildProviderPluginCache(BuildProviderPluginCache.Request) returns (stream BuildProviderPluginCache.Event); + + // Opens an existing local filesystem directory as a provider plugin cache + // directory, returning a plugin cache handle that can be used with other + // RPC operations. + rpc OpenProviderPluginCache(OpenProviderPluginCache.Request) returns (OpenProviderPluginCache.Response); + + rpc CloseProviderPluginCache(CloseProviderPluginCache.Request) returns (CloseProviderPluginCache.Response); + + // Returns information about the specific provider packages that are + // available in the given provider plugin cache. + rpc GetCachedProviders(GetCachedProviders.Request) returns (GetCachedProviders.Response); + + // Returns information about the built-in providers that are compiled in + // to this Terraform Core server. + rpc GetBuiltInProviders(GetBuiltInProviders.Request) returns (GetBuiltInProviders.Response); + + // Returns a description of the schema for a particular provider in a + // given provider plugin cache, or of a particular built-in provider + // known to this version of Terraform Core. + // + // WARNING: This operation requires executing the selected provider plugin, + // which therefore allows it to run arbitrary code as a child process of + // this Terraform Core server, with access to all of the same resources. + // This should typically be used only with providers explicitly selected + // in a dependency lock file, so users can control what external code + // has the potential to run in a context that probably has access to + // private source code and other sensitive information. + rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response); +} + + +message OpenSourceBundle { + message Request { + string local_path = 1; + } + message Response { + int64 source_bundle_handle = 1; + } +} + +message CloseSourceBundle { + message Request { + int64 source_bundle_handle = 1; + } + message Response { + } +} + +message OpenDependencyLockFile { + message Request { + int64 source_bundle_handle = 1; + terraform1.SourceAddress source_address = 2; + } + message Response { + int64 dependency_locks_handle = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message CreateDependencyLocks { + message Request { + // The provider selections to include in the locks object. + // + // A typical value would be the result of an earlier call to + // GetLockedProviderDependencies on some other locks object, + // e.g. if a caller needs to propagate a set of locks from one + // Terraform Core RPC server to another. + repeated terraform1.ProviderPackage provider_selections = 1; + } + message Response { + int64 dependency_locks_handle = 1; + } +} + +message CloseDependencyLocks { + message Request { + int64 dependency_locks_handle = 1; + } + message Response { + } +} + +message GetLockedProviderDependencies { + message Request { + int64 dependency_locks_handle = 1; + } + message Response { + repeated terraform1.ProviderPackage selected_providers = 1; + } +} + +message BuildProviderPluginCache { + message Request { + string cache_dir = 1; + int64 dependency_locks_handle = 2; + repeated InstallMethod installation_methods = 3; + + // If set, this populates the cache with plugins for a different + // platform than the one the Terraform Core RPC server is running on. + // If unset (empty) then the cache will be populated with packages + // for the same platform as Terraform Core was built for, if available. + // + // If this is set to a different platform than the Terraform Core RPC + // server's then the generated cache directory will appear empty to + // other operations on this server. + string override_platform = 4; + + message InstallMethod { + oneof source { + bool direct = 1; + string local_mirror_dir = 2; + string network_mirror_url = 3; + } + repeated string include = 4; + repeated string exclude = 5; + } + } + message Event { + oneof event { + Pending pending = 1; + ProviderVersion already_installed = 2; + + ProviderVersion built_in = 3; + ProviderConstraints query_begin = 4; + ProviderVersion query_success = 5; + ProviderWarnings query_warnings = 6; + + FetchBegin fetch_begin = 7; + FetchComplete fetch_complete = 8; + + terraform1.Diagnostic diagnostic = 9; + } + + message Pending { + repeated ProviderConstraints expected = 1; + } + message ProviderConstraints { + string source_addr = 1; + string versions = 2; + } + message ProviderVersion { + string source_addr = 1; + string version = 2; + } + message ProviderWarnings { + string source_addr = 1; + repeated string warnings = 2; + } + message FetchBegin { + ProviderVersion provider_version = 1; + string location = 2; + } + message FetchComplete { + ProviderVersion provider_version = 1; + AuthResult auth_result = 2; + + // If auth_result is one of the "_SIGNED" variants then this + // might contain a UI-oriented identifier for the key that + // signed the package. The exact format of this string is not + // guaranteed; do not attempt to parse it or make automated + // decisions based on it. + string key_id_for_display = 3; + + enum AuthResult { + UNKNOWN = 0; + VERIFIED_CHECKSUM = 1; + OFFICIAL_SIGNED = 2; + PARTNER_SIGNED = 3; + SELF_SIGNED = 4; + } + } + } +} + +message OpenProviderPluginCache { + message Request { + string cache_dir = 1; + + // As with the field of the same name in BuildProviderPluginCache.Request. + // + // If this is set to anything other than this RPC server's native + // platform then any operations that require executing the provider + // plugin are likely to fail due to executable format errors or + // similar. However, it's valid to use the returned handle with + // GetCachedProviders, since it only analyzes the cache metadata + // and doesn't actually run the plugins inside. + string override_platform = 2; + } + message Response { + int64 provider_cache_handle = 1; + } +} + +message CloseProviderPluginCache { + message Request { + int64 provider_cache_handle = 1; + } + message Response { + } +} + +message GetCachedProviders { + message Request { + int64 provider_cache_handle = 1; + } + message Response { + repeated terraform1.ProviderPackage available_providers = 1; + } +} + +message GetBuiltInProviders { + message Request { + } + message Response { + // The built-in providers that are compiled in to this Terraform Core + // server. + // + // This uses terraform1.ProviderPackage messages for consistency with the other + // operations which list providers, but built-in providers do not + // have version numbers nor hashes so those fields will always be + // unset in the result. + repeated terraform1.ProviderPackage available_providers = 1; + } +} + +message GetProviderSchema { + message Request { + // The address of the provider to retrieve schema for, using the + // typical provider source address syntax. + // + // When requesting schema based on a terraform1.ProviderPackage message, populate + // this with its "source_addr" field. + string provider_addr = 1; + // The version number of the given provider to retrieve the schema + // of, which must have already been populated into the cache directory. + // + // Not supported for built-in providers because we can only access the + // single "version" of the provider that's compiled into this Terraform + // Core server, and so must be left unset or empty for those. + // + // When requesting schema based on a terraform1.ProviderPackage message, populate + // this with its "version" field. + string provider_version = 2; + + // The handle for the previously-opened provider plugin cache to + // load the provider plugin from. + // + // Optional for built-in providers, but can still be specified in that + // case if desired so that callers can safely just send the handle they + // have in all cases and be naive about which providers are and are + // not built in. + int64 provider_cache_handle = 3; + } + message Response { + ProviderSchema schema = 1; + } +} + +// ProviderSchema describes the full schema for a particular provider. +message ProviderSchema { + Schema provider_config = 1; + map managed_resource_types = 2; + map data_resource_types = 3; +} + +// Schema describes a schema for an instance of a particular object, such as +// a resource type or a provider's overall configuration. +message Schema { + // Block is the top level configuration block for this schema. + Block block = 1; + + message Block { + repeated Attribute attributes = 1; + repeated NestedBlock block_types = 2; + DocString description = 3; + bool deprecated = 4; + } + + message Attribute { + string name = 1; + bytes type = 2; + Object nested_type = 10; + DocString description = 3; + bool required = 4; + bool optional = 5; + bool computed = 6; + bool sensitive = 7; + bool deprecated = 8; + } + + message NestedBlock { + enum NestingMode { + INVALID = 0; + SINGLE = 1; + LIST = 2; + SET = 3; + MAP = 4; + GROUP = 5; + } + + string type_name = 1; + Block block = 2; + NestingMode nesting = 3; + } + + message Object { + enum NestingMode { + INVALID = 0; + SINGLE = 1; + LIST = 2; + SET = 3; + MAP = 4; + } + + repeated Attribute attributes = 1; + NestingMode nesting = 3; + } + + message DocString { + string description = 1; + Format format = 2; + + enum Format { + PLAIN = 0; + MARKDOWN = 1; + } + } +} diff --git a/internal/rpcapi/terraform1/packages/packages.pb.go b/internal/rpcapi/terraform1/packages/packages.pb.go new file mode 100644 index 0000000000..2f43d69634 --- /dev/null +++ b/internal/rpcapi/terraform1/packages/packages.pb.go @@ -0,0 +1,1216 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: packages.proto + +package packages + +import ( + context "context" + terraform1 "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ProviderPackageVersions struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderPackageVersions) Reset() { + *x = ProviderPackageVersions{} + mi := &file_packages_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderPackageVersions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderPackageVersions) ProtoMessage() {} + +func (x *ProviderPackageVersions) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProviderPackageVersions.ProtoReflect.Descriptor instead. +func (*ProviderPackageVersions) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{0} +} + +type FetchProviderPackage struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchProviderPackage) Reset() { + *x = FetchProviderPackage{} + mi := &file_packages_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchProviderPackage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchProviderPackage) ProtoMessage() {} + +func (x *FetchProviderPackage) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchProviderPackage.ProtoReflect.Descriptor instead. +func (*FetchProviderPackage) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{1} +} + +type ModulePackageVersions struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModulePackageVersions) Reset() { + *x = ModulePackageVersions{} + mi := &file_packages_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModulePackageVersions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModulePackageVersions) ProtoMessage() {} + +func (x *ModulePackageVersions) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModulePackageVersions.ProtoReflect.Descriptor instead. +func (*ModulePackageVersions) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{2} +} + +type ModulePackageSourceAddr struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModulePackageSourceAddr) Reset() { + *x = ModulePackageSourceAddr{} + mi := &file_packages_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModulePackageSourceAddr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModulePackageSourceAddr) ProtoMessage() {} + +func (x *ModulePackageSourceAddr) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModulePackageSourceAddr.ProtoReflect.Descriptor instead. +func (*ModulePackageSourceAddr) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{3} +} + +type FetchModulePackage struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchModulePackage) Reset() { + *x = FetchModulePackage{} + mi := &file_packages_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchModulePackage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchModulePackage) ProtoMessage() {} + +func (x *FetchModulePackage) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchModulePackage.ProtoReflect.Descriptor instead. +func (*FetchModulePackage) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{4} +} + +type ProviderPackageVersions_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderPackageVersions_Request) Reset() { + *x = ProviderPackageVersions_Request{} + mi := &file_packages_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderPackageVersions_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderPackageVersions_Request) ProtoMessage() {} + +func (x *ProviderPackageVersions_Request) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProviderPackageVersions_Request.ProtoReflect.Descriptor instead. +func (*ProviderPackageVersions_Request) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *ProviderPackageVersions_Request) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +type ProviderPackageVersions_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Versions []string `protobuf:"bytes,1,rep,name=versions,proto3" json:"versions,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderPackageVersions_Response) Reset() { + *x = ProviderPackageVersions_Response{} + mi := &file_packages_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderPackageVersions_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderPackageVersions_Response) ProtoMessage() {} + +func (x *ProviderPackageVersions_Response) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProviderPackageVersions_Response.ProtoReflect.Descriptor instead. +func (*ProviderPackageVersions_Response) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *ProviderPackageVersions_Response) GetVersions() []string { + if x != nil { + return x.Versions + } + return nil +} + +func (x *ProviderPackageVersions_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type FetchProviderPackage_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + CacheDir string `protobuf:"bytes,1,opt,name=cache_dir,json=cacheDir,proto3" json:"cache_dir,omitempty"` + SourceAddr string `protobuf:"bytes,2,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Platforms []string `protobuf:"bytes,4,rep,name=platforms,proto3" json:"platforms,omitempty"` + Hashes []string `protobuf:"bytes,5,rep,name=hashes,proto3" json:"hashes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchProviderPackage_Request) Reset() { + *x = FetchProviderPackage_Request{} + mi := &file_packages_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchProviderPackage_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchProviderPackage_Request) ProtoMessage() {} + +func (x *FetchProviderPackage_Request) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchProviderPackage_Request.ProtoReflect.Descriptor instead. +func (*FetchProviderPackage_Request) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *FetchProviderPackage_Request) GetCacheDir() string { + if x != nil { + return x.CacheDir + } + return "" +} + +func (x *FetchProviderPackage_Request) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *FetchProviderPackage_Request) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *FetchProviderPackage_Request) GetPlatforms() []string { + if x != nil { + return x.Platforms + } + return nil +} + +func (x *FetchProviderPackage_Request) GetHashes() []string { + if x != nil { + return x.Hashes + } + return nil +} + +type FetchProviderPackage_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Each requested platform will return a result in this list. The order + // of the returned results will match the order of the requested + // platforms. If the binary for a given platform could not be downloaded + // there will still be an entry in the results with diagnostics + // explaining why. + Results []*FetchProviderPackage_PlatformResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchProviderPackage_Response) Reset() { + *x = FetchProviderPackage_Response{} + mi := &file_packages_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchProviderPackage_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchProviderPackage_Response) ProtoMessage() {} + +func (x *FetchProviderPackage_Response) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchProviderPackage_Response.ProtoReflect.Descriptor instead. +func (*FetchProviderPackage_Response) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{1, 1} +} + +func (x *FetchProviderPackage_Response) GetResults() []*FetchProviderPackage_PlatformResult { + if x != nil { + return x.Results + } + return nil +} + +func (x *FetchProviderPackage_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type FetchProviderPackage_PlatformResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + Provider *terraform1.ProviderPackage `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchProviderPackage_PlatformResult) Reset() { + *x = FetchProviderPackage_PlatformResult{} + mi := &file_packages_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchProviderPackage_PlatformResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchProviderPackage_PlatformResult) ProtoMessage() {} + +func (x *FetchProviderPackage_PlatformResult) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchProviderPackage_PlatformResult.ProtoReflect.Descriptor instead. +func (*FetchProviderPackage_PlatformResult) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{1, 2} +} + +func (x *FetchProviderPackage_PlatformResult) GetProvider() *terraform1.ProviderPackage { + if x != nil { + return x.Provider + } + return nil +} + +func (x *FetchProviderPackage_PlatformResult) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type ModulePackageVersions_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,2,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModulePackageVersions_Request) Reset() { + *x = ModulePackageVersions_Request{} + mi := &file_packages_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModulePackageVersions_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModulePackageVersions_Request) ProtoMessage() {} + +func (x *ModulePackageVersions_Request) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModulePackageVersions_Request.ProtoReflect.Descriptor instead. +func (*ModulePackageVersions_Request) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *ModulePackageVersions_Request) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +type ModulePackageVersions_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Versions []string `protobuf:"bytes,1,rep,name=versions,proto3" json:"versions,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModulePackageVersions_Response) Reset() { + *x = ModulePackageVersions_Response{} + mi := &file_packages_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModulePackageVersions_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModulePackageVersions_Response) ProtoMessage() {} + +func (x *ModulePackageVersions_Response) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModulePackageVersions_Response.ProtoReflect.Descriptor instead. +func (*ModulePackageVersions_Response) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *ModulePackageVersions_Response) GetVersions() []string { + if x != nil { + return x.Versions + } + return nil +} + +func (x *ModulePackageVersions_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type ModulePackageSourceAddr_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModulePackageSourceAddr_Request) Reset() { + *x = ModulePackageSourceAddr_Request{} + mi := &file_packages_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModulePackageSourceAddr_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModulePackageSourceAddr_Request) ProtoMessage() {} + +func (x *ModulePackageSourceAddr_Request) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModulePackageSourceAddr_Request.ProtoReflect.Descriptor instead. +func (*ModulePackageSourceAddr_Request) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *ModulePackageSourceAddr_Request) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *ModulePackageSourceAddr_Request) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +type ModulePackageSourceAddr_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ModulePackageSourceAddr_Response) Reset() { + *x = ModulePackageSourceAddr_Response{} + mi := &file_packages_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ModulePackageSourceAddr_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ModulePackageSourceAddr_Response) ProtoMessage() {} + +func (x *ModulePackageSourceAddr_Response) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ModulePackageSourceAddr_Response.ProtoReflect.Descriptor instead. +func (*ModulePackageSourceAddr_Response) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{3, 1} +} + +func (x *ModulePackageSourceAddr_Response) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *ModulePackageSourceAddr_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type FetchModulePackage_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + CacheDir string `protobuf:"bytes,1,opt,name=cache_dir,json=cacheDir,proto3" json:"cache_dir,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchModulePackage_Request) Reset() { + *x = FetchModulePackage_Request{} + mi := &file_packages_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchModulePackage_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchModulePackage_Request) ProtoMessage() {} + +func (x *FetchModulePackage_Request) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchModulePackage_Request.ProtoReflect.Descriptor instead. +func (*FetchModulePackage_Request) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *FetchModulePackage_Request) GetCacheDir() string { + if x != nil { + return x.CacheDir + } + return "" +} + +func (x *FetchModulePackage_Request) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type FetchModulePackage_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FetchModulePackage_Response) Reset() { + *x = FetchModulePackage_Response{} + mi := &file_packages_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FetchModulePackage_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FetchModulePackage_Response) ProtoMessage() {} + +func (x *FetchModulePackage_Response) ProtoReflect() protoreflect.Message { + mi := &file_packages_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FetchModulePackage_Response.ProtoReflect.Descriptor instead. +func (*FetchModulePackage_Response) Descriptor() ([]byte, []int) { + return file_packages_proto_rawDescGZIP(), []int{4, 1} +} + +func (x *FetchModulePackage_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +var File_packages_proto protoreflect.FileDescriptor + +var file_packages_proto_rawDesc = string([]byte{ + 0x0a, 0x0e, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x12, 0x13, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa7, 0x01, 0x0a, 0x17, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x1a, 0x2a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x1a, + 0x60, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x22, 0xd1, 0x03, 0x0a, 0x14, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x1a, 0x97, 0x01, 0x0a, 0x07, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, + 0x64, 0x69, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x63, 0x68, 0x65, + 0x44, 0x69, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x41, 0x64, 0x64, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1c, + 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x09, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x73, 0x12, 0x16, 0x0a, 0x06, + 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x68, 0x61, + 0x73, 0x68, 0x65, 0x73, 0x1a, 0x98, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x52, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x50, 0x6c, + 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x07, 0x72, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x1a, + 0x83, 0x01, 0x0a, 0x0e, 0x50, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x12, 0x37, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xa5, 0x01, 0x0a, 0x15, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x1a, + 0x2a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x1a, 0x60, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xb7, 0x01, + 0x0a, 0x17, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x1a, 0x44, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, + 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, + 0x56, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, + 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x38, 0x0a, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x12, 0x46, 0x65, 0x74, 0x63, + 0x68, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x1a, 0x38, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x61, 0x63, + 0x68, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, + 0x63, 0x68, 0x65, 0x44, 0x69, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x1a, 0x44, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x32, 0x97, + 0x05, 0x0a, 0x08, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x86, 0x01, 0x0a, 0x17, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7d, 0x0a, 0x14, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x31, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x80, 0x01, 0x0a, 0x15, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x32, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x86, 0x01, 0x0a, 0x17, 0x4d, 0x6f, 0x64, 0x75, 0x6c, + 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, + 0x64, 0x72, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, + 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4d, + 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x77, 0x0a, 0x12, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, + 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x2f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, + 0x68, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_packages_proto_rawDescOnce sync.Once + file_packages_proto_rawDescData []byte +) + +func file_packages_proto_rawDescGZIP() []byte { + file_packages_proto_rawDescOnce.Do(func() { + file_packages_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_packages_proto_rawDesc), len(file_packages_proto_rawDesc))) + }) + return file_packages_proto_rawDescData +} + +var file_packages_proto_msgTypes = make([]protoimpl.MessageInfo, 16) +var file_packages_proto_goTypes = []any{ + (*ProviderPackageVersions)(nil), // 0: terraform1.packages.ProviderPackageVersions + (*FetchProviderPackage)(nil), // 1: terraform1.packages.FetchProviderPackage + (*ModulePackageVersions)(nil), // 2: terraform1.packages.ModulePackageVersions + (*ModulePackageSourceAddr)(nil), // 3: terraform1.packages.ModulePackageSourceAddr + (*FetchModulePackage)(nil), // 4: terraform1.packages.FetchModulePackage + (*ProviderPackageVersions_Request)(nil), // 5: terraform1.packages.ProviderPackageVersions.Request + (*ProviderPackageVersions_Response)(nil), // 6: terraform1.packages.ProviderPackageVersions.Response + (*FetchProviderPackage_Request)(nil), // 7: terraform1.packages.FetchProviderPackage.Request + (*FetchProviderPackage_Response)(nil), // 8: terraform1.packages.FetchProviderPackage.Response + (*FetchProviderPackage_PlatformResult)(nil), // 9: terraform1.packages.FetchProviderPackage.PlatformResult + (*ModulePackageVersions_Request)(nil), // 10: terraform1.packages.ModulePackageVersions.Request + (*ModulePackageVersions_Response)(nil), // 11: terraform1.packages.ModulePackageVersions.Response + (*ModulePackageSourceAddr_Request)(nil), // 12: terraform1.packages.ModulePackageSourceAddr.Request + (*ModulePackageSourceAddr_Response)(nil), // 13: terraform1.packages.ModulePackageSourceAddr.Response + (*FetchModulePackage_Request)(nil), // 14: terraform1.packages.FetchModulePackage.Request + (*FetchModulePackage_Response)(nil), // 15: terraform1.packages.FetchModulePackage.Response + (*terraform1.Diagnostic)(nil), // 16: terraform1.Diagnostic + (*terraform1.ProviderPackage)(nil), // 17: terraform1.ProviderPackage +} +var file_packages_proto_depIdxs = []int32{ + 16, // 0: terraform1.packages.ProviderPackageVersions.Response.diagnostics:type_name -> terraform1.Diagnostic + 9, // 1: terraform1.packages.FetchProviderPackage.Response.results:type_name -> terraform1.packages.FetchProviderPackage.PlatformResult + 16, // 2: terraform1.packages.FetchProviderPackage.Response.diagnostics:type_name -> terraform1.Diagnostic + 17, // 3: terraform1.packages.FetchProviderPackage.PlatformResult.provider:type_name -> terraform1.ProviderPackage + 16, // 4: terraform1.packages.FetchProviderPackage.PlatformResult.diagnostics:type_name -> terraform1.Diagnostic + 16, // 5: terraform1.packages.ModulePackageVersions.Response.diagnostics:type_name -> terraform1.Diagnostic + 16, // 6: terraform1.packages.ModulePackageSourceAddr.Response.diagnostics:type_name -> terraform1.Diagnostic + 16, // 7: terraform1.packages.FetchModulePackage.Response.diagnostics:type_name -> terraform1.Diagnostic + 5, // 8: terraform1.packages.Packages.ProviderPackageVersions:input_type -> terraform1.packages.ProviderPackageVersions.Request + 7, // 9: terraform1.packages.Packages.FetchProviderPackage:input_type -> terraform1.packages.FetchProviderPackage.Request + 10, // 10: terraform1.packages.Packages.ModulePackageVersions:input_type -> terraform1.packages.ModulePackageVersions.Request + 12, // 11: terraform1.packages.Packages.ModulePackageSourceAddr:input_type -> terraform1.packages.ModulePackageSourceAddr.Request + 14, // 12: terraform1.packages.Packages.FetchModulePackage:input_type -> terraform1.packages.FetchModulePackage.Request + 6, // 13: terraform1.packages.Packages.ProviderPackageVersions:output_type -> terraform1.packages.ProviderPackageVersions.Response + 8, // 14: terraform1.packages.Packages.FetchProviderPackage:output_type -> terraform1.packages.FetchProviderPackage.Response + 11, // 15: terraform1.packages.Packages.ModulePackageVersions:output_type -> terraform1.packages.ModulePackageVersions.Response + 13, // 16: terraform1.packages.Packages.ModulePackageSourceAddr:output_type -> terraform1.packages.ModulePackageSourceAddr.Response + 15, // 17: terraform1.packages.Packages.FetchModulePackage:output_type -> terraform1.packages.FetchModulePackage.Response + 13, // [13:18] is the sub-list for method output_type + 8, // [8:13] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_packages_proto_init() } +func file_packages_proto_init() { + if File_packages_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_packages_proto_rawDesc), len(file_packages_proto_rawDesc)), + NumEnums: 0, + NumMessages: 16, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_packages_proto_goTypes, + DependencyIndexes: file_packages_proto_depIdxs, + MessageInfos: file_packages_proto_msgTypes, + }.Build() + File_packages_proto = out.File + file_packages_proto_goTypes = nil + file_packages_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// PackagesClient is the client API for Packages service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type PackagesClient interface { + ProviderPackageVersions(ctx context.Context, in *ProviderPackageVersions_Request, opts ...grpc.CallOption) (*ProviderPackageVersions_Response, error) + FetchProviderPackage(ctx context.Context, in *FetchProviderPackage_Request, opts ...grpc.CallOption) (*FetchProviderPackage_Response, error) + ModulePackageVersions(ctx context.Context, in *ModulePackageVersions_Request, opts ...grpc.CallOption) (*ModulePackageVersions_Response, error) + ModulePackageSourceAddr(ctx context.Context, in *ModulePackageSourceAddr_Request, opts ...grpc.CallOption) (*ModulePackageSourceAddr_Response, error) + FetchModulePackage(ctx context.Context, in *FetchModulePackage_Request, opts ...grpc.CallOption) (*FetchModulePackage_Response, error) +} + +type packagesClient struct { + cc grpc.ClientConnInterface +} + +func NewPackagesClient(cc grpc.ClientConnInterface) PackagesClient { + return &packagesClient{cc} +} + +func (c *packagesClient) ProviderPackageVersions(ctx context.Context, in *ProviderPackageVersions_Request, opts ...grpc.CallOption) (*ProviderPackageVersions_Response, error) { + out := new(ProviderPackageVersions_Response) + err := c.cc.Invoke(ctx, "/terraform1.packages.Packages/ProviderPackageVersions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *packagesClient) FetchProviderPackage(ctx context.Context, in *FetchProviderPackage_Request, opts ...grpc.CallOption) (*FetchProviderPackage_Response, error) { + out := new(FetchProviderPackage_Response) + err := c.cc.Invoke(ctx, "/terraform1.packages.Packages/FetchProviderPackage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *packagesClient) ModulePackageVersions(ctx context.Context, in *ModulePackageVersions_Request, opts ...grpc.CallOption) (*ModulePackageVersions_Response, error) { + out := new(ModulePackageVersions_Response) + err := c.cc.Invoke(ctx, "/terraform1.packages.Packages/ModulePackageVersions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *packagesClient) ModulePackageSourceAddr(ctx context.Context, in *ModulePackageSourceAddr_Request, opts ...grpc.CallOption) (*ModulePackageSourceAddr_Response, error) { + out := new(ModulePackageSourceAddr_Response) + err := c.cc.Invoke(ctx, "/terraform1.packages.Packages/ModulePackageSourceAddr", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *packagesClient) FetchModulePackage(ctx context.Context, in *FetchModulePackage_Request, opts ...grpc.CallOption) (*FetchModulePackage_Response, error) { + out := new(FetchModulePackage_Response) + err := c.cc.Invoke(ctx, "/terraform1.packages.Packages/FetchModulePackage", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// PackagesServer is the server API for Packages service. +type PackagesServer interface { + ProviderPackageVersions(context.Context, *ProviderPackageVersions_Request) (*ProviderPackageVersions_Response, error) + FetchProviderPackage(context.Context, *FetchProviderPackage_Request) (*FetchProviderPackage_Response, error) + ModulePackageVersions(context.Context, *ModulePackageVersions_Request) (*ModulePackageVersions_Response, error) + ModulePackageSourceAddr(context.Context, *ModulePackageSourceAddr_Request) (*ModulePackageSourceAddr_Response, error) + FetchModulePackage(context.Context, *FetchModulePackage_Request) (*FetchModulePackage_Response, error) +} + +// UnimplementedPackagesServer can be embedded to have forward compatible implementations. +type UnimplementedPackagesServer struct { +} + +func (*UnimplementedPackagesServer) ProviderPackageVersions(context.Context, *ProviderPackageVersions_Request) (*ProviderPackageVersions_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ProviderPackageVersions not implemented") +} +func (*UnimplementedPackagesServer) FetchProviderPackage(context.Context, *FetchProviderPackage_Request) (*FetchProviderPackage_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchProviderPackage not implemented") +} +func (*UnimplementedPackagesServer) ModulePackageVersions(context.Context, *ModulePackageVersions_Request) (*ModulePackageVersions_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ModulePackageVersions not implemented") +} +func (*UnimplementedPackagesServer) ModulePackageSourceAddr(context.Context, *ModulePackageSourceAddr_Request) (*ModulePackageSourceAddr_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ModulePackageSourceAddr not implemented") +} +func (*UnimplementedPackagesServer) FetchModulePackage(context.Context, *FetchModulePackage_Request) (*FetchModulePackage_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method FetchModulePackage not implemented") +} + +func RegisterPackagesServer(s *grpc.Server, srv PackagesServer) { + s.RegisterService(&_Packages_serviceDesc, srv) +} + +func _Packages_ProviderPackageVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ProviderPackageVersions_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PackagesServer).ProviderPackageVersions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.packages.Packages/ProviderPackageVersions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PackagesServer).ProviderPackageVersions(ctx, req.(*ProviderPackageVersions_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Packages_FetchProviderPackage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchProviderPackage_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PackagesServer).FetchProviderPackage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.packages.Packages/FetchProviderPackage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PackagesServer).FetchProviderPackage(ctx, req.(*FetchProviderPackage_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Packages_ModulePackageVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ModulePackageVersions_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PackagesServer).ModulePackageVersions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.packages.Packages/ModulePackageVersions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PackagesServer).ModulePackageVersions(ctx, req.(*ModulePackageVersions_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Packages_ModulePackageSourceAddr_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ModulePackageSourceAddr_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PackagesServer).ModulePackageSourceAddr(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.packages.Packages/ModulePackageSourceAddr", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PackagesServer).ModulePackageSourceAddr(ctx, req.(*ModulePackageSourceAddr_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Packages_FetchModulePackage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FetchModulePackage_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PackagesServer).FetchModulePackage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.packages.Packages/FetchModulePackage", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PackagesServer).FetchModulePackage(ctx, req.(*FetchModulePackage_Request)) + } + return interceptor(ctx, in, info, handler) +} + +var _Packages_serviceDesc = grpc.ServiceDesc{ + ServiceName: "terraform1.packages.Packages", + HandlerType: (*PackagesServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ProviderPackageVersions", + Handler: _Packages_ProviderPackageVersions_Handler, + }, + { + MethodName: "FetchProviderPackage", + Handler: _Packages_FetchProviderPackage_Handler, + }, + { + MethodName: "ModulePackageVersions", + Handler: _Packages_ModulePackageVersions_Handler, + }, + { + MethodName: "ModulePackageSourceAddr", + Handler: _Packages_ModulePackageSourceAddr_Handler, + }, + { + MethodName: "FetchModulePackage", + Handler: _Packages_FetchModulePackage_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "packages.proto", +} diff --git a/internal/rpcapi/terraform1/packages/packages.proto b/internal/rpcapi/terraform1/packages/packages.proto new file mode 100644 index 0000000000..d90d3dc033 --- /dev/null +++ b/internal/rpcapi/terraform1/packages/packages.proto @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package terraform1.packages; + +import "terraform1.proto"; + +// The Packages service provides helper functions for retrieving Terraform +// modules and providers. +// +// Unlike the Dependencies service, the Packages service does not require any +// existing configuration or sourcebundle to function. +// +// This service is designed for use with a specific command-line tool, and is +// currently experimental. It can be changed and removed without warning, even +// in patch releases. +service Packages { + rpc ProviderPackageVersions(ProviderPackageVersions.Request) returns (ProviderPackageVersions.Response); + rpc FetchProviderPackage(FetchProviderPackage.Request) returns (FetchProviderPackage.Response); + + rpc ModulePackageVersions(ModulePackageVersions.Request) returns (ModulePackageVersions.Response); + rpc ModulePackageSourceAddr(ModulePackageSourceAddr.Request) returns (ModulePackageSourceAddr.Response); + rpc FetchModulePackage(FetchModulePackage.Request) returns (FetchModulePackage.Response); +} + +message ProviderPackageVersions { + message Request { + string source_addr = 1; + } + message Response { + repeated string versions = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message FetchProviderPackage { + message Request { + string cache_dir = 1; + + string source_addr = 2; + string version = 3; + repeated string platforms = 4; + repeated string hashes = 5; + } + message Response { + // Each requested platform will return a result in this list. The order + // of the returned results will match the order of the requested + // platforms. If the binary for a given platform could not be downloaded + // there will still be an entry in the results with diagnostics + // explaining why. + repeated FetchProviderPackage.PlatformResult results = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } + message PlatformResult { + terraform1.ProviderPackage provider = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message ModulePackageVersions { + message Request { + string source_addr = 2; + } + message Response { + repeated string versions = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message ModulePackageSourceAddr { + message Request { + string source_addr = 1; + string version = 2; + } + message Response { + string url = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message FetchModulePackage { + message Request { + string cache_dir = 1; + + string url = 2; + } + message Response { + repeated terraform1.Diagnostic diagnostics = 1; + } +} diff --git a/internal/rpcapi/terraform1/setup/setup.pb.go b/internal/rpcapi/terraform1/setup/setup.pb.go new file mode 100644 index 0000000000..33f2f0e466 --- /dev/null +++ b/internal/rpcapi/terraform1/setup/setup.pb.go @@ -0,0 +1,685 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: setup.proto + +package setup + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Handshake struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Handshake) Reset() { + *x = Handshake{} + mi := &file_setup_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Handshake) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Handshake) ProtoMessage() {} + +func (x *Handshake) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Handshake.ProtoReflect.Descriptor instead. +func (*Handshake) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{0} +} + +type Stop struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Stop) Reset() { + *x = Stop{} + mi := &file_setup_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Stop) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stop) ProtoMessage() {} + +func (x *Stop) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stop.ProtoReflect.Descriptor instead. +func (*Stop) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{1} +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Credentials map[string]*HostCredential `protobuf:"bytes,1,rep,name=credentials,proto3" json:"credentials,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_setup_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{2} +} + +func (x *Config) GetCredentials() map[string]*HostCredential { + if x != nil { + return x.Credentials + } + return nil +} + +type HostCredential struct { + state protoimpl.MessageState `protogen:"open.v1"` + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HostCredential) Reset() { + *x = HostCredential{} + mi := &file_setup_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HostCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HostCredential) ProtoMessage() {} + +func (x *HostCredential) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HostCredential.ProtoReflect.Descriptor instead. +func (*HostCredential) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{3} +} + +func (x *HostCredential) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +// The capabilities that the client wishes to advertise to the server during +// handshake. +type ClientCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientCapabilities) Reset() { + *x = ClientCapabilities{} + mi := &file_setup_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientCapabilities) ProtoMessage() {} + +func (x *ClientCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientCapabilities.ProtoReflect.Descriptor instead. +func (*ClientCapabilities) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{4} +} + +// The capabilities that the server wishes to advertise to the client during +// handshake. Fields in this message can also be used to acknowledge and +// confirm support for client capabilities advertised in ClientCapabilities, +// in situations where the client must vary its behavior based on the server's +// level of support. +type ServerCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerCapabilities) Reset() { + *x = ServerCapabilities{} + mi := &file_setup_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerCapabilities) ProtoMessage() {} + +func (x *ServerCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerCapabilities.ProtoReflect.Descriptor instead. +func (*ServerCapabilities) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{5} +} + +type Handshake_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + Capabilities *ClientCapabilities `protobuf:"bytes,1,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + Config *Config `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Handshake_Request) Reset() { + *x = Handshake_Request{} + mi := &file_setup_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Handshake_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Handshake_Request) ProtoMessage() {} + +func (x *Handshake_Request) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Handshake_Request.ProtoReflect.Descriptor instead. +func (*Handshake_Request) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *Handshake_Request) GetCapabilities() *ClientCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +func (x *Handshake_Request) GetConfig() *Config { + if x != nil { + return x.Config + } + return nil +} + +type Handshake_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Capabilities *ServerCapabilities `protobuf:"bytes,2,opt,name=capabilities,proto3" json:"capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Handshake_Response) Reset() { + *x = Handshake_Response{} + mi := &file_setup_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Handshake_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Handshake_Response) ProtoMessage() {} + +func (x *Handshake_Response) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Handshake_Response.ProtoReflect.Descriptor instead. +func (*Handshake_Response) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *Handshake_Response) GetCapabilities() *ServerCapabilities { + if x != nil { + return x.Capabilities + } + return nil +} + +type Stop_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Stop_Request) Reset() { + *x = Stop_Request{} + mi := &file_setup_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Stop_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stop_Request) ProtoMessage() {} + +func (x *Stop_Request) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stop_Request.ProtoReflect.Descriptor instead. +func (*Stop_Request) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{1, 0} +} + +type Stop_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Stop_Response) Reset() { + *x = Stop_Response{} + mi := &file_setup_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Stop_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stop_Response) ProtoMessage() {} + +func (x *Stop_Response) ProtoReflect() protoreflect.Message { + mi := &file_setup_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stop_Response.ProtoReflect.Descriptor instead. +func (*Stop_Response) Descriptor() ([]byte, []int) { + return file_setup_proto_rawDescGZIP(), []int{1, 1} +} + +var File_setup_proto protoreflect.FileDescriptor + +var file_setup_proto_rawDesc = string([]byte{ + 0x0a, 0x0b, 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x22, + 0xe9, 0x01, 0x0a, 0x09, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x1a, 0x85, 0x01, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x48, 0x0a, 0x0c, 0x63, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x24, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x65, 0x74, + 0x75, 0x70, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x69, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x54, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x48, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x0c, 0x63, + 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x22, 0x1d, 0x0a, 0x04, 0x53, + 0x74, 0x6f, 0x70, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0a, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xb7, 0x01, 0x0a, 0x06, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 0x73, 0x1a, 0x60, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x36, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x43, + 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x26, 0x0a, 0x0e, 0x48, 0x6f, 0x73, 0x74, 0x43, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x14, 0x0a, 0x12, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x22, 0x14, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x32, 0xa8, 0x01, 0x0a, 0x05, 0x53, 0x65, 0x74, + 0x75, 0x70, 0x12, 0x56, 0x0a, 0x09, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x12, + 0x23, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x65, 0x74, + 0x75, 0x70, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, 0x65, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x48, 0x61, 0x6e, 0x64, 0x73, 0x68, 0x61, 0x6b, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, 0x04, 0x53, 0x74, + 0x6f, 0x70, 0x12, 0x1e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x65, 0x74, 0x75, 0x70, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_setup_proto_rawDescOnce sync.Once + file_setup_proto_rawDescData []byte +) + +func file_setup_proto_rawDescGZIP() []byte { + file_setup_proto_rawDescOnce.Do(func() { + file_setup_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_setup_proto_rawDesc), len(file_setup_proto_rawDesc))) + }) + return file_setup_proto_rawDescData +} + +var file_setup_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_setup_proto_goTypes = []any{ + (*Handshake)(nil), // 0: terraform1.setup.Handshake + (*Stop)(nil), // 1: terraform1.setup.Stop + (*Config)(nil), // 2: terraform1.setup.Config + (*HostCredential)(nil), // 3: terraform1.setup.HostCredential + (*ClientCapabilities)(nil), // 4: terraform1.setup.ClientCapabilities + (*ServerCapabilities)(nil), // 5: terraform1.setup.ServerCapabilities + (*Handshake_Request)(nil), // 6: terraform1.setup.Handshake.Request + (*Handshake_Response)(nil), // 7: terraform1.setup.Handshake.Response + (*Stop_Request)(nil), // 8: terraform1.setup.Stop.Request + (*Stop_Response)(nil), // 9: terraform1.setup.Stop.Response + nil, // 10: terraform1.setup.Config.CredentialsEntry +} +var file_setup_proto_depIdxs = []int32{ + 10, // 0: terraform1.setup.Config.credentials:type_name -> terraform1.setup.Config.CredentialsEntry + 4, // 1: terraform1.setup.Handshake.Request.capabilities:type_name -> terraform1.setup.ClientCapabilities + 2, // 2: terraform1.setup.Handshake.Request.config:type_name -> terraform1.setup.Config + 5, // 3: terraform1.setup.Handshake.Response.capabilities:type_name -> terraform1.setup.ServerCapabilities + 3, // 4: terraform1.setup.Config.CredentialsEntry.value:type_name -> terraform1.setup.HostCredential + 6, // 5: terraform1.setup.Setup.Handshake:input_type -> terraform1.setup.Handshake.Request + 8, // 6: terraform1.setup.Setup.Stop:input_type -> terraform1.setup.Stop.Request + 7, // 7: terraform1.setup.Setup.Handshake:output_type -> terraform1.setup.Handshake.Response + 9, // 8: terraform1.setup.Setup.Stop:output_type -> terraform1.setup.Stop.Response + 7, // [7:9] is the sub-list for method output_type + 5, // [5:7] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_setup_proto_init() } +func file_setup_proto_init() { + if File_setup_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_setup_proto_rawDesc), len(file_setup_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_setup_proto_goTypes, + DependencyIndexes: file_setup_proto_depIdxs, + MessageInfos: file_setup_proto_msgTypes, + }.Build() + File_setup_proto = out.File + file_setup_proto_goTypes = nil + file_setup_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// SetupClient is the client API for Setup service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type SetupClient interface { + // Clients must call Handshake before any other function of any other + // service, to complete the capability negotiation step that may + // then affect the behaviors of subsequent operations. + // + // This function can be called only once per RPC server. + Handshake(ctx context.Context, in *Handshake_Request, opts ...grpc.CallOption) (*Handshake_Response, error) + // At any time after handshaking, clients may call Stop to initiate a + // graceful shutdown of the server. + Stop(ctx context.Context, in *Stop_Request, opts ...grpc.CallOption) (*Stop_Response, error) +} + +type setupClient struct { + cc grpc.ClientConnInterface +} + +func NewSetupClient(cc grpc.ClientConnInterface) SetupClient { + return &setupClient{cc} +} + +func (c *setupClient) Handshake(ctx context.Context, in *Handshake_Request, opts ...grpc.CallOption) (*Handshake_Response, error) { + out := new(Handshake_Response) + err := c.cc.Invoke(ctx, "/terraform1.setup.Setup/Handshake", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *setupClient) Stop(ctx context.Context, in *Stop_Request, opts ...grpc.CallOption) (*Stop_Response, error) { + out := new(Stop_Response) + err := c.cc.Invoke(ctx, "/terraform1.setup.Setup/Stop", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// SetupServer is the server API for Setup service. +type SetupServer interface { + // Clients must call Handshake before any other function of any other + // service, to complete the capability negotiation step that may + // then affect the behaviors of subsequent operations. + // + // This function can be called only once per RPC server. + Handshake(context.Context, *Handshake_Request) (*Handshake_Response, error) + // At any time after handshaking, clients may call Stop to initiate a + // graceful shutdown of the server. + Stop(context.Context, *Stop_Request) (*Stop_Response, error) +} + +// UnimplementedSetupServer can be embedded to have forward compatible implementations. +type UnimplementedSetupServer struct { +} + +func (*UnimplementedSetupServer) Handshake(context.Context, *Handshake_Request) (*Handshake_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Handshake not implemented") +} +func (*UnimplementedSetupServer) Stop(context.Context, *Stop_Request) (*Stop_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") +} + +func RegisterSetupServer(s *grpc.Server, srv SetupServer) { + s.RegisterService(&_Setup_serviceDesc, srv) +} + +func _Setup_Handshake_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Handshake_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SetupServer).Handshake(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.setup.Setup/Handshake", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SetupServer).Handshake(ctx, req.(*Handshake_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Setup_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Stop_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SetupServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.setup.Setup/Stop", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SetupServer).Stop(ctx, req.(*Stop_Request)) + } + return interceptor(ctx, in, info, handler) +} + +var _Setup_serviceDesc = grpc.ServiceDesc{ + ServiceName: "terraform1.setup.Setup", + HandlerType: (*SetupServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Handshake", + Handler: _Setup_Handshake_Handler, + }, + { + MethodName: "Stop", + Handler: _Setup_Stop_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "setup.proto", +} diff --git a/internal/rpcapi/terraform1/setup/setup.proto b/internal/rpcapi/terraform1/setup/setup.proto new file mode 100644 index 0000000000..863f638991 --- /dev/null +++ b/internal/rpcapi/terraform1/setup/setup.proto @@ -0,0 +1,58 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package terraform1.setup; + +service Setup { + // Clients must call Handshake before any other function of any other + // service, to complete the capability negotiation step that may + // then affect the behaviors of subsequent operations. + // + // This function can be called only once per RPC server. + rpc Handshake(Handshake.Request) returns (Handshake.Response); + + // At any time after handshaking, clients may call Stop to initiate a + // graceful shutdown of the server. + rpc Stop(Stop.Request) returns (Stop.Response); +} + +message Handshake { + message Request { + ClientCapabilities capabilities = 1; + Config config = 2; + } + message Response { + ServerCapabilities capabilities = 2; + } +} + +message Stop { + message Request { + } + message Response { + } +} + +message Config { + map credentials = 1; +} + +message HostCredential { + string token = 1; +} + +// The capabilities that the client wishes to advertise to the server during +// handshake. +message ClientCapabilities { + // There are not yet any negotiatable capabilities. +} + +// The capabilities that the server wishes to advertise to the client during +// handshake. Fields in this message can also be used to acknowledge and +// confirm support for client capabilities advertised in ClientCapabilities, +// in situations where the client must vary its behavior based on the server's +// level of support. +message ServerCapabilities { + // There are not yet any negotiatable capabilities. +} diff --git a/internal/rpcapi/terraform1/stacks/conversion.go b/internal/rpcapi/terraform1/stacks/conversion.go new file mode 100644 index 0000000000..e2cb6dcc48 --- /dev/null +++ b/internal/rpcapi/terraform1/stacks/conversion.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stacks + +import ( + "fmt" + "math/big" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// This file contains some hand-written type conversion helpers to complement +// the generated stubs. + +// ChangeTypesForPlanAction returns the [ChangeType] sequence that corresponds +// to the given plan action, or an error if there is no known equivalent. +func ChangeTypesForPlanAction(action plans.Action) ([]ChangeType, error) { + switch action { + case plans.NoOp: + return []ChangeType{ChangeType_NOOP}, nil + case plans.Create: + return []ChangeType{ChangeType_CREATE}, nil + case plans.Read: + return []ChangeType{ChangeType_READ}, nil + case plans.Update: + return []ChangeType{ChangeType_UPDATE}, nil + case plans.Delete: + return []ChangeType{ChangeType_DELETE}, nil + case plans.DeleteThenCreate: + return []ChangeType{ChangeType_DELETE, ChangeType_CREATE}, nil + case plans.CreateThenDelete: + return []ChangeType{ChangeType_CREATE, ChangeType_DELETE}, nil + case plans.Forget: + return []ChangeType{ChangeType_FORGET}, nil + case plans.CreateThenForget: + return []ChangeType{ChangeType_CREATE, ChangeType_FORGET}, nil + default: + return nil, fmt.Errorf("unsupported action %s", action) + } +} + +// ToDynamicValue uses NewDynamicValue to construct a DynamicValue from the +// provider cty.Value. This function will strip out the sensitive paths and +// include them in the returned dynamic value. If from contains marks other +// than sensitive then this function will return an error. +func ToDynamicValue(from cty.Value, ty cty.Type) (*DynamicValue, error) { + // Separate out sensitive marks from the decoded value so we can re-serialize it + // with MessagePack. Sensitive paths get encoded separately in the final message. + unmarkedValue, markses := from.UnmarkDeepWithPaths() + sensitivePaths, otherMarkses := marks.PathsWithMark(markses, marks.Sensitive) + if len(otherMarkses) != 0 { + // Any other marks should've been dealt with by our caller before + // getting here, since we only know how to preserve the sensitive + // marking. + return nil, fmt.Errorf( + "%s: unhandled value marks %#v (this is a bug in Terraform)", + tfdiags.FormatCtyPath(otherMarkses[0].Path), otherMarkses[0].Marks, + ) + } + encValue, err := plans.NewDynamicValue(unmarkedValue, ty) + if err != nil { + return nil, err + } + return NewDynamicValue(encValue, sensitivePaths), nil +} + +// NewDynamicValue constructs a [DynamicValue] message object from a +// [plans.DynamicValue], which is Terraform Core's typical in-memory +// representation of an already-serialized dynamic value. +// +// The plans package represents the sensitive value mark as a separate field +// in [plans.ChangeSrc] rather than as part of the value itself, so callers must +// also provide a separate set of paths that are marked as sensitive. +func NewDynamicValue(from plans.DynamicValue, sensitivePaths []cty.Path) *DynamicValue { + // plans.DynamicValue is always MessagePack-serialized today, so we'll + // just write its bytes into the field for msgpack serialization + // unconditionally. If plans.DynamicValue grows to support different + // serialization formats in future we will need some additional logic here. + ret := &DynamicValue{ + Msgpack: []byte(from), + } + + if len(sensitivePaths) != 0 { + ret.Sensitive = make([]*AttributePath, 0, len(sensitivePaths)) + for _, path := range sensitivePaths { + ret.Sensitive = append(ret.Sensitive, NewAttributePath(path)) + } + } + + return ret +} + +// NewAttributePath constructs an [AttributePath] message object from +// a [cty.Path] value. +func NewAttributePath(from cty.Path) *AttributePath { + ret := &AttributePath{} + if len(from) == 0 { + return ret + } + ret.Steps = make([]*AttributePath_Step, len(from)) + for i, step := range from { + switch step := step.(type) { + case cty.GetAttrStep: + ret.Steps[i] = &AttributePath_Step{ + Selector: &AttributePath_Step_AttributeName{ + AttributeName: step.Name, + }, + } + case cty.IndexStep: + k := step.Key + // Although the key is cty.Value, in practice it should typically + // be constrained only to known and non-null strings and numbers. + // If we encounter anything else then we'll just abort and return + // a truncated path, since the only way other values should be + // able to appear is if we're traversing through a set, and we + // typically avoid doing that in callers by truncating the path + // at the same point anyway. (Note that marked values -- one of + // our main uses for AttributePath -- cannot exist inside + // sets anyway, so that case can't arise there.) + if k.IsNull() || !k.IsKnown() { + k = cty.DynamicVal // to force falling into the default case for the switch below + } + + switch k.Type() { + case cty.String: + ret.Steps[i] = &AttributePath_Step{ + Selector: &AttributePath_Step_ElementKeyString{ + ElementKeyString: k.AsString(), + }, + } + case cty.Number: + // We require an integer in int64 range. We might not get that + // in the unlikely event that this is a traversal through a + // cty.Set(cty.Number), since any number would be valid in + // principle for that case. + bf := k.AsBigFloat() + idx, acc := bf.Int64() + if acc != big.Exact { + ret.Steps = ret.Steps[:i] + return ret + } + ret.Steps[i] = &AttributePath_Step{ + Selector: &AttributePath_Step_ElementKeyInt{ + ElementKeyInt: idx, + }, + } + default: + ret.Steps = ret.Steps[:i] + return ret + } + default: + // Should not get here because the above should be exhaustive for + // all cty.PathStep implementations. + panic(fmt.Sprintf("path has unsupported step type %T", step)) + } + } + return ret +} + +func NewResourceInstanceInStackAddr(addr stackaddrs.AbsResourceInstance) *ResourceInstanceInStackAddr { + return &ResourceInstanceInStackAddr{ + ComponentInstanceAddr: addr.Component.String(), + ResourceInstanceAddr: addr.Item.String(), + } +} + +func NewResourceInstanceObjectInStackAddr(addr stackaddrs.AbsResourceInstanceObject) *ResourceInstanceObjectInStackAddr { + return &ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: addr.Component.String(), + ResourceInstanceAddr: addr.Item.ResourceInstance.String(), + DeposedKey: addr.Item.DeposedKey.String(), + } +} diff --git a/internal/rpcapi/terraform1/stacks/stacks.pb.go b/internal/rpcapi/terraform1/stacks/stacks.pb.go new file mode 100644 index 0000000000..36244e767b --- /dev/null +++ b/internal/rpcapi/terraform1/stacks/stacks.pb.go @@ -0,0 +1,8781 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: stacks.proto + +package stacks + +import ( + context "context" + terraform1 "github.com/hashicorp/terraform/internal/rpcapi/terraform1" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ResourceMode int32 + +const ( + ResourceMode_UNKNOWN ResourceMode = 0 + ResourceMode_MANAGED ResourceMode = 1 + ResourceMode_DATA ResourceMode = 2 +) + +// Enum value maps for ResourceMode. +var ( + ResourceMode_name = map[int32]string{ + 0: "UNKNOWN", + 1: "MANAGED", + 2: "DATA", + } + ResourceMode_value = map[string]int32{ + "UNKNOWN": 0, + "MANAGED": 1, + "DATA": 2, + } +) + +func (x ResourceMode) Enum() *ResourceMode { + p := new(ResourceMode) + *p = x + return p +} + +func (x ResourceMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ResourceMode) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[0].Descriptor() +} + +func (ResourceMode) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[0] +} + +func (x ResourceMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ResourceMode.Descriptor instead. +func (ResourceMode) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{0} +} + +type PlanMode int32 + +const ( + PlanMode_NORMAL PlanMode = 0 + PlanMode_REFRESH_ONLY PlanMode = 1 + PlanMode_DESTROY PlanMode = 2 +) + +// Enum value maps for PlanMode. +var ( + PlanMode_name = map[int32]string{ + 0: "NORMAL", + 1: "REFRESH_ONLY", + 2: "DESTROY", + } + PlanMode_value = map[string]int32{ + "NORMAL": 0, + "REFRESH_ONLY": 1, + "DESTROY": 2, + } +) + +func (x PlanMode) Enum() *PlanMode { + p := new(PlanMode) + *p = x + return p +} + +func (x PlanMode) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (PlanMode) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[1].Descriptor() +} + +func (PlanMode) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[1] +} + +func (x PlanMode) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use PlanMode.Descriptor instead. +func (PlanMode) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{1} +} + +type ChangeType int32 + +const ( + ChangeType_NOOP ChangeType = 0 + ChangeType_READ ChangeType = 1 + ChangeType_CREATE ChangeType = 2 + ChangeType_UPDATE ChangeType = 3 + ChangeType_DELETE ChangeType = 4 + ChangeType_FORGET ChangeType = 5 +) + +// Enum value maps for ChangeType. +var ( + ChangeType_name = map[int32]string{ + 0: "NOOP", + 1: "READ", + 2: "CREATE", + 3: "UPDATE", + 4: "DELETE", + 5: "FORGET", + } + ChangeType_value = map[string]int32{ + "NOOP": 0, + "READ": 1, + "CREATE": 2, + "UPDATE": 3, + "DELETE": 4, + "FORGET": 5, + } +) + +func (x ChangeType) Enum() *ChangeType { + p := new(ChangeType) + *p = x + return p +} + +func (x ChangeType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ChangeType) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[2].Descriptor() +} + +func (ChangeType) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[2] +} + +func (x ChangeType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ChangeType.Descriptor instead. +func (ChangeType) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{2} +} + +type FindStackConfigurationComponents_Instances int32 + +const ( + FindStackConfigurationComponents_SINGLE FindStackConfigurationComponents_Instances = 0 + FindStackConfigurationComponents_COUNT FindStackConfigurationComponents_Instances = 1 + FindStackConfigurationComponents_FOR_EACH FindStackConfigurationComponents_Instances = 2 +) + +// Enum value maps for FindStackConfigurationComponents_Instances. +var ( + FindStackConfigurationComponents_Instances_name = map[int32]string{ + 0: "SINGLE", + 1: "COUNT", + 2: "FOR_EACH", + } + FindStackConfigurationComponents_Instances_value = map[string]int32{ + "SINGLE": 0, + "COUNT": 1, + "FOR_EACH": 2, + } +) + +func (x FindStackConfigurationComponents_Instances) Enum() *FindStackConfigurationComponents_Instances { + p := new(FindStackConfigurationComponents_Instances) + *p = x + return p +} + +func (x FindStackConfigurationComponents_Instances) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (FindStackConfigurationComponents_Instances) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[3].Descriptor() +} + +func (FindStackConfigurationComponents_Instances) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[3] +} + +func (x FindStackConfigurationComponents_Instances) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use FindStackConfigurationComponents_Instances.Descriptor instead. +func (FindStackConfigurationComponents_Instances) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 0} +} + +// Reason describes the reason why a resource instance change was +// deferred. +type Deferred_Reason int32 + +const ( + Deferred_INVALID Deferred_Reason = 0 + Deferred_INSTANCE_COUNT_UNKNOWN Deferred_Reason = 1 + Deferred_RESOURCE_CONFIG_UNKNOWN Deferred_Reason = 2 + Deferred_PROVIDER_CONFIG_UNKNOWN Deferred_Reason = 3 + Deferred_ABSENT_PREREQ Deferred_Reason = 4 + Deferred_DEFERRED_PREREQ Deferred_Reason = 5 +) + +// Enum value maps for Deferred_Reason. +var ( + Deferred_Reason_name = map[int32]string{ + 0: "INVALID", + 1: "INSTANCE_COUNT_UNKNOWN", + 2: "RESOURCE_CONFIG_UNKNOWN", + 3: "PROVIDER_CONFIG_UNKNOWN", + 4: "ABSENT_PREREQ", + 5: "DEFERRED_PREREQ", + } + Deferred_Reason_value = map[string]int32{ + "INVALID": 0, + "INSTANCE_COUNT_UNKNOWN": 1, + "RESOURCE_CONFIG_UNKNOWN": 2, + "PROVIDER_CONFIG_UNKNOWN": 3, + "ABSENT_PREREQ": 4, + "DEFERRED_PREREQ": 5, + } +) + +func (x Deferred_Reason) Enum() *Deferred_Reason { + p := new(Deferred_Reason) + *p = x + return p +} + +func (x Deferred_Reason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Deferred_Reason) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[4].Descriptor() +} + +func (Deferred_Reason) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[4] +} + +func (x Deferred_Reason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Deferred_Reason.Descriptor instead. +func (Deferred_Reason) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{23, 0} +} + +type StackChangeProgress_ComponentInstanceStatus_Status int32 + +const ( + StackChangeProgress_ComponentInstanceStatus_INVALID StackChangeProgress_ComponentInstanceStatus_Status = 0 + StackChangeProgress_ComponentInstanceStatus_PENDING StackChangeProgress_ComponentInstanceStatus_Status = 1 + StackChangeProgress_ComponentInstanceStatus_PLANNING StackChangeProgress_ComponentInstanceStatus_Status = 2 + StackChangeProgress_ComponentInstanceStatus_PLANNED StackChangeProgress_ComponentInstanceStatus_Status = 3 + StackChangeProgress_ComponentInstanceStatus_APPLYING StackChangeProgress_ComponentInstanceStatus_Status = 4 + StackChangeProgress_ComponentInstanceStatus_APPLIED StackChangeProgress_ComponentInstanceStatus_Status = 5 + StackChangeProgress_ComponentInstanceStatus_ERRORED StackChangeProgress_ComponentInstanceStatus_Status = 6 + StackChangeProgress_ComponentInstanceStatus_DEFERRED StackChangeProgress_ComponentInstanceStatus_Status = 7 +) + +// Enum value maps for StackChangeProgress_ComponentInstanceStatus_Status. +var ( + StackChangeProgress_ComponentInstanceStatus_Status_name = map[int32]string{ + 0: "INVALID", + 1: "PENDING", + 2: "PLANNING", + 3: "PLANNED", + 4: "APPLYING", + 5: "APPLIED", + 6: "ERRORED", + 7: "DEFERRED", + } + StackChangeProgress_ComponentInstanceStatus_Status_value = map[string]int32{ + "INVALID": 0, + "PENDING": 1, + "PLANNING": 2, + "PLANNED": 3, + "APPLYING": 4, + "APPLIED": 5, + "ERRORED": 6, + "DEFERRED": 7, + } +) + +func (x StackChangeProgress_ComponentInstanceStatus_Status) Enum() *StackChangeProgress_ComponentInstanceStatus_Status { + p := new(StackChangeProgress_ComponentInstanceStatus_Status) + *p = x + return p +} + +func (x StackChangeProgress_ComponentInstanceStatus_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StackChangeProgress_ComponentInstanceStatus_Status) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[5].Descriptor() +} + +func (StackChangeProgress_ComponentInstanceStatus_Status) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[5] +} + +func (x StackChangeProgress_ComponentInstanceStatus_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StackChangeProgress_ComponentInstanceStatus_Status.Descriptor instead. +func (StackChangeProgress_ComponentInstanceStatus_Status) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 0, 0} +} + +type StackChangeProgress_ResourceInstanceStatus_Status int32 + +const ( + StackChangeProgress_ResourceInstanceStatus_INVALID StackChangeProgress_ResourceInstanceStatus_Status = 0 + StackChangeProgress_ResourceInstanceStatus_PENDING StackChangeProgress_ResourceInstanceStatus_Status = 1 + StackChangeProgress_ResourceInstanceStatus_REFRESHING StackChangeProgress_ResourceInstanceStatus_Status = 2 + StackChangeProgress_ResourceInstanceStatus_REFRESHED StackChangeProgress_ResourceInstanceStatus_Status = 3 + StackChangeProgress_ResourceInstanceStatus_PLANNING StackChangeProgress_ResourceInstanceStatus_Status = 4 + StackChangeProgress_ResourceInstanceStatus_PLANNED StackChangeProgress_ResourceInstanceStatus_Status = 5 + StackChangeProgress_ResourceInstanceStatus_APPLYING StackChangeProgress_ResourceInstanceStatus_Status = 6 + StackChangeProgress_ResourceInstanceStatus_APPLIED StackChangeProgress_ResourceInstanceStatus_Status = 7 + StackChangeProgress_ResourceInstanceStatus_ERRORED StackChangeProgress_ResourceInstanceStatus_Status = 8 +) + +// Enum value maps for StackChangeProgress_ResourceInstanceStatus_Status. +var ( + StackChangeProgress_ResourceInstanceStatus_Status_name = map[int32]string{ + 0: "INVALID", + 1: "PENDING", + 2: "REFRESHING", + 3: "REFRESHED", + 4: "PLANNING", + 5: "PLANNED", + 6: "APPLYING", + 7: "APPLIED", + 8: "ERRORED", + } + StackChangeProgress_ResourceInstanceStatus_Status_value = map[string]int32{ + "INVALID": 0, + "PENDING": 1, + "REFRESHING": 2, + "REFRESHED": 3, + "PLANNING": 4, + "PLANNED": 5, + "APPLYING": 6, + "APPLIED": 7, + "ERRORED": 8, + } +) + +func (x StackChangeProgress_ResourceInstanceStatus_Status) Enum() *StackChangeProgress_ResourceInstanceStatus_Status { + p := new(StackChangeProgress_ResourceInstanceStatus_Status) + *p = x + return p +} + +func (x StackChangeProgress_ResourceInstanceStatus_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StackChangeProgress_ResourceInstanceStatus_Status) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[6].Descriptor() +} + +func (StackChangeProgress_ResourceInstanceStatus_Status) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[6] +} + +func (x StackChangeProgress_ResourceInstanceStatus_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StackChangeProgress_ResourceInstanceStatus_Status.Descriptor instead. +func (StackChangeProgress_ResourceInstanceStatus_Status) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 1, 0} +} + +type StackChangeProgress_ProvisionerStatus_Status int32 + +const ( + StackChangeProgress_ProvisionerStatus_INVALID StackChangeProgress_ProvisionerStatus_Status = 0 + StackChangeProgress_ProvisionerStatus_PROVISIONING StackChangeProgress_ProvisionerStatus_Status = 1 + StackChangeProgress_ProvisionerStatus_PROVISIONED StackChangeProgress_ProvisionerStatus_Status = 2 + StackChangeProgress_ProvisionerStatus_ERRORED StackChangeProgress_ProvisionerStatus_Status = 3 +) + +// Enum value maps for StackChangeProgress_ProvisionerStatus_Status. +var ( + StackChangeProgress_ProvisionerStatus_Status_name = map[int32]string{ + 0: "INVALID", + 1: "PROVISIONING", + 2: "PROVISIONED", + 3: "ERRORED", + } + StackChangeProgress_ProvisionerStatus_Status_value = map[string]int32{ + "INVALID": 0, + "PROVISIONING": 1, + "PROVISIONED": 2, + "ERRORED": 3, + } +) + +func (x StackChangeProgress_ProvisionerStatus_Status) Enum() *StackChangeProgress_ProvisionerStatus_Status { + p := new(StackChangeProgress_ProvisionerStatus_Status) + *p = x + return p +} + +func (x StackChangeProgress_ProvisionerStatus_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StackChangeProgress_ProvisionerStatus_Status) Descriptor() protoreflect.EnumDescriptor { + return file_stacks_proto_enumTypes[7].Descriptor() +} + +func (StackChangeProgress_ProvisionerStatus_Status) Type() protoreflect.EnumType { + return &file_stacks_proto_enumTypes[7] +} + +func (x StackChangeProgress_ProvisionerStatus_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StackChangeProgress_ProvisionerStatus_Status.Descriptor instead. +func (StackChangeProgress_ProvisionerStatus_Status) EnumDescriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 4, 0} +} + +// OpenTerraformState opens a previously-saved Terraform state, returning a +// handle that can be used with other operations. This is distinct from +// OpenState because it means core state rather than stack state. +type OpenTerraformState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenTerraformState) Reset() { + *x = OpenTerraformState{} + mi := &file_stacks_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenTerraformState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenTerraformState) ProtoMessage() {} + +func (x *OpenTerraformState) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenTerraformState.ProtoReflect.Descriptor instead. +func (*OpenTerraformState) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{0} +} + +// CloseTerraformState closes a previously-opened Terraform state using its +// handle. +type CloseTerraformState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseTerraformState) Reset() { + *x = CloseTerraformState{} + mi := &file_stacks_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseTerraformState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseTerraformState) ProtoMessage() {} + +func (x *CloseTerraformState) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseTerraformState.ProtoReflect.Descriptor instead. +func (*CloseTerraformState) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{1} +} + +// MigrateTerraformState migrates a Terraform state into Stacks state using +// a mapping of addresses. +// +// Only resources and modules from the root module should be specified. All +// resources in nested modules maintain their nested structure within the new +// components the base modules were moved into. +type MigrateTerraformState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MigrateTerraformState) Reset() { + *x = MigrateTerraformState{} + mi := &file_stacks_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MigrateTerraformState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateTerraformState) ProtoMessage() {} + +func (x *MigrateTerraformState) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateTerraformState.ProtoReflect.Descriptor instead. +func (*MigrateTerraformState) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{2} +} + +type OpenStackConfiguration struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackConfiguration) Reset() { + *x = OpenStackConfiguration{} + mi := &file_stacks_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackConfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackConfiguration) ProtoMessage() {} + +func (x *OpenStackConfiguration) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackConfiguration.ProtoReflect.Descriptor instead. +func (*OpenStackConfiguration) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{3} +} + +type CloseStackConfiguration struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackConfiguration) Reset() { + *x = CloseStackConfiguration{} + mi := &file_stacks_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackConfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackConfiguration) ProtoMessage() {} + +func (x *CloseStackConfiguration) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackConfiguration.ProtoReflect.Descriptor instead. +func (*CloseStackConfiguration) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{4} +} + +type ValidateStackConfiguration struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateStackConfiguration) Reset() { + *x = ValidateStackConfiguration{} + mi := &file_stacks_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateStackConfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateStackConfiguration) ProtoMessage() {} + +func (x *ValidateStackConfiguration) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateStackConfiguration.ProtoReflect.Descriptor instead. +func (*ValidateStackConfiguration) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{5} +} + +type FindStackConfigurationComponents struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents) Reset() { + *x = FindStackConfigurationComponents{} + mi := &file_stacks_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents) ProtoMessage() {} + +func (x *FindStackConfigurationComponents) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6} +} + +type OpenStackState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackState) Reset() { + *x = OpenStackState{} + mi := &file_stacks_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackState) ProtoMessage() {} + +func (x *OpenStackState) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackState.ProtoReflect.Descriptor instead. +func (*OpenStackState) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{7} +} + +type CloseStackState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackState) Reset() { + *x = CloseStackState{} + mi := &file_stacks_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackState) ProtoMessage() {} + +func (x *CloseStackState) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackState.ProtoReflect.Descriptor instead. +func (*CloseStackState) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{8} +} + +type PlanStackChanges struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanStackChanges) Reset() { + *x = PlanStackChanges{} + mi := &file_stacks_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanStackChanges) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanStackChanges) ProtoMessage() {} + +func (x *PlanStackChanges) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanStackChanges.ProtoReflect.Descriptor instead. +func (*PlanStackChanges) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{9} +} + +type OpenStackPlan struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackPlan) Reset() { + *x = OpenStackPlan{} + mi := &file_stacks_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackPlan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackPlan) ProtoMessage() {} + +func (x *OpenStackPlan) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackPlan.ProtoReflect.Descriptor instead. +func (*OpenStackPlan) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{10} +} + +type CloseStackPlan struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackPlan) Reset() { + *x = CloseStackPlan{} + mi := &file_stacks_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackPlan) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackPlan) ProtoMessage() {} + +func (x *CloseStackPlan) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackPlan.ProtoReflect.Descriptor instead. +func (*CloseStackPlan) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{11} +} + +type ApplyStackChanges struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyStackChanges) Reset() { + *x = ApplyStackChanges{} + mi := &file_stacks_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyStackChanges) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyStackChanges) ProtoMessage() {} + +func (x *ApplyStackChanges) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyStackChanges.ProtoReflect.Descriptor instead. +func (*ApplyStackChanges) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{12} +} + +type OpenStackInspector struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackInspector) Reset() { + *x = OpenStackInspector{} + mi := &file_stacks_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackInspector) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackInspector) ProtoMessage() {} + +func (x *OpenStackInspector) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackInspector.ProtoReflect.Descriptor instead. +func (*OpenStackInspector) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{13} +} + +type InspectExpressionResult struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InspectExpressionResult) Reset() { + *x = InspectExpressionResult{} + mi := &file_stacks_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InspectExpressionResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InspectExpressionResult) ProtoMessage() {} + +func (x *InspectExpressionResult) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InspectExpressionResult.ProtoReflect.Descriptor instead. +func (*InspectExpressionResult) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{14} +} + +// Represents dynamically-typed data from within the Terraform language. +// Typically only one of the available serialization formats will be populated, +// depending on what serializations are appropriate for a particular context +// and what capabilities the client and the server negotiated during Handshake. +type DynamicValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` // The default serialization format + Sensitive []*AttributePath `protobuf:"bytes,2,rep,name=sensitive,proto3" json:"sensitive,omitempty"` // Paths to any sensitive-marked values. + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DynamicValue) Reset() { + *x = DynamicValue{} + mi := &file_stacks_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DynamicValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicValue) ProtoMessage() {} + +func (x *DynamicValue) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicValue.ProtoReflect.Descriptor instead. +func (*DynamicValue) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{15} +} + +func (x *DynamicValue) GetMsgpack() []byte { + if x != nil { + return x.Msgpack + } + return nil +} + +func (x *DynamicValue) GetSensitive() []*AttributePath { + if x != nil { + return x.Sensitive + } + return nil +} + +// Represents a change of some object from one dynamic value to another. +type DynamicValueChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + Old *DynamicValue `protobuf:"bytes,1,opt,name=old,proto3" json:"old,omitempty"` + New *DynamicValue `protobuf:"bytes,2,opt,name=new,proto3" json:"new,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DynamicValueChange) Reset() { + *x = DynamicValueChange{} + mi := &file_stacks_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DynamicValueChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicValueChange) ProtoMessage() {} + +func (x *DynamicValueChange) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicValueChange.ProtoReflect.Descriptor instead. +func (*DynamicValueChange) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{16} +} + +func (x *DynamicValueChange) GetOld() *DynamicValue { + if x != nil { + return x.Old + } + return nil +} + +func (x *DynamicValueChange) GetNew() *DynamicValue { + if x != nil { + return x.New + } + return nil +} + +// Represents a DynamicValue accompanied by a source location where it was +// presumably defined, for values that originated in configuration files for +// situations such as returning error messages. +type DynamicValueWithSource struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value *DynamicValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + SourceRange *terraform1.SourceRange `protobuf:"bytes,2,opt,name=source_range,json=sourceRange,proto3" json:"source_range,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DynamicValueWithSource) Reset() { + *x = DynamicValueWithSource{} + mi := &file_stacks_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DynamicValueWithSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicValueWithSource) ProtoMessage() {} + +func (x *DynamicValueWithSource) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicValueWithSource.ProtoReflect.Descriptor instead. +func (*DynamicValueWithSource) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{17} +} + +func (x *DynamicValueWithSource) GetValue() *DynamicValue { + if x != nil { + return x.Value + } + return nil +} + +func (x *DynamicValueWithSource) GetSourceRange() *terraform1.SourceRange { + if x != nil { + return x.SourceRange + } + return nil +} + +type AttributePath struct { + state protoimpl.MessageState `protogen:"open.v1"` + Steps []*AttributePath_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttributePath) Reset() { + *x = AttributePath{} + mi := &file_stacks_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttributePath) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributePath) ProtoMessage() {} + +func (x *AttributePath) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributePath.ProtoReflect.Descriptor instead. +func (*AttributePath) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{18} +} + +func (x *AttributePath) GetSteps() []*AttributePath_Step { + if x != nil { + return x.Steps + } + return nil +} + +// Represents the address of a specific component instance within a stack. +type ComponentInstanceInStackAddr struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The address of the static component that this is an instance of. + ComponentAddr string `protobuf:"bytes,1,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + // The address of the instance that's being announced. For + // multi-instance components this could have any combination of + // instance keys on the component itself or instance keys on any + // of the containing embedded stacks. + ComponentInstanceAddr string `protobuf:"bytes,2,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ComponentInstanceInStackAddr) Reset() { + *x = ComponentInstanceInStackAddr{} + mi := &file_stacks_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ComponentInstanceInStackAddr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ComponentInstanceInStackAddr) ProtoMessage() {} + +func (x *ComponentInstanceInStackAddr) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ComponentInstanceInStackAddr.ProtoReflect.Descriptor instead. +func (*ComponentInstanceInStackAddr) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{19} +} + +func (x *ComponentInstanceInStackAddr) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +func (x *ComponentInstanceInStackAddr) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +// Represents the address of a specific resource instance inside a specific +// component instance within the containing stack. +type ResourceInstanceInStackAddr struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique address of the component instance that this resource instance + // belongs to. This is comparable with + ComponentInstanceAddr string `protobuf:"bytes,1,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + // Unique address of the resource instance within the given component + // instance. Each component instance has a separate namespace of + // resource instance addresses, so callers must take both fields together + // to produce a key that's unique throughout the entire plan. + ResourceInstanceAddr string `protobuf:"bytes,2,opt,name=resource_instance_addr,json=resourceInstanceAddr,proto3" json:"resource_instance_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceInstanceInStackAddr) Reset() { + *x = ResourceInstanceInStackAddr{} + mi := &file_stacks_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceInstanceInStackAddr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceInstanceInStackAddr) ProtoMessage() {} + +func (x *ResourceInstanceInStackAddr) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceInstanceInStackAddr.ProtoReflect.Descriptor instead. +func (*ResourceInstanceInStackAddr) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{20} +} + +func (x *ResourceInstanceInStackAddr) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +func (x *ResourceInstanceInStackAddr) GetResourceInstanceAddr() string { + if x != nil { + return x.ResourceInstanceAddr + } + return "" +} + +// Represents the address of a specific resource instance object inside a +// specific component instance within the containing stack. +type ResourceInstanceObjectInStackAddr struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique address of the component instance that this resource instance + // belongs to. This is comparable with + ComponentInstanceAddr string `protobuf:"bytes,1,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + // Unique address of the resource instance within the given component + // instance. Each component instance has a separate namespace of + // resource instance addresses, so callers must take both fields together + // to produce a key that's unique throughout the entire plan. + ResourceInstanceAddr string `protobuf:"bytes,2,opt,name=resource_instance_addr,json=resourceInstanceAddr,proto3" json:"resource_instance_addr,omitempty"` + // Optional "deposed key" populated only for non-current (deposed) objects, + // which can appear for "create before destroy" replacements where the + // create succeeds but then the destroy fails, leaving us with two different + // objects to track for the same resource instance. + DeposedKey string `protobuf:"bytes,3,opt,name=deposed_key,json=deposedKey,proto3" json:"deposed_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceInstanceObjectInStackAddr) Reset() { + *x = ResourceInstanceObjectInStackAddr{} + mi := &file_stacks_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceInstanceObjectInStackAddr) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceInstanceObjectInStackAddr) ProtoMessage() {} + +func (x *ResourceInstanceObjectInStackAddr) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceInstanceObjectInStackAddr.ProtoReflect.Descriptor instead. +func (*ResourceInstanceObjectInStackAddr) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{21} +} + +func (x *ResourceInstanceObjectInStackAddr) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +func (x *ResourceInstanceObjectInStackAddr) GetResourceInstanceAddr() string { + if x != nil { + return x.ResourceInstanceAddr + } + return "" +} + +func (x *ResourceInstanceObjectInStackAddr) GetDeposedKey() string { + if x != nil { + return x.DeposedKey + } + return "" +} + +// Describes one item in a stack plan. The overall plan is the concatentation +// of all messages of this type emitted as events during the plan; splitting +// this information over multiple messages just allows the individual events +// to double as progress notifications for an interactive UI. +type PlannedChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Terraform Core's internal representation(s) of this change. Callers + // must provide the messages in this field, if any, verbatim to the + // ApplyStackChanges RPC in order to apply this change, and must not + // attempt to decode or analyze the contents because they are subject + // to change in future versions of Terraform Core. + // + // This might be unpopulated if this message represents only information + // for the caller and Terraform Core doesn't actually need to recall this + // information during the apply step. Callers must append each raw item + // to the raw plan in the order specified, and provide them all together + // in the same order to ApplyStackChanges. + Raw []*anypb.Any `protobuf:"bytes,1,rep,name=raw,proto3" json:"raw,omitempty"` + // Caller-facing descriptions of this change, to use for presenting + // information to end-users in the UI and for other subsystems such as + // imposing policy rules on the resulting plan. + // + // There can be zero or more description objects associated with each + // change. More than one is not common, but should be supported by clients + // by treating them the same way as if each description had arrived in + // a separate PlannedChange message. Clients should not treat the grouping + // or not-grouping of change description objects as meaningful information, + // since it's subject to change in future Terraform Core versions. + // + // DO NOT attempt to use this to surgically filter particular changes + // from a larger plan. Although external descriptions often match with + // the raw representations in field "raw", that is not guaranteed and + // Terraform Core assumes that it will always be provided with the full + // set of raw messages -- in the same order they were emitted -- during + // the apply step. For example, some raw messages might omit information + // that is implied by earlier raw messages and would therefore be + // incomplete if isolated. + Descriptions []*PlannedChange_ChangeDescription `protobuf:"bytes,2,rep,name=descriptions,proto3" json:"descriptions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange) Reset() { + *x = PlannedChange{} + mi := &file_stacks_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange) ProtoMessage() {} + +func (x *PlannedChange) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange.ProtoReflect.Descriptor instead. +func (*PlannedChange) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22} +} + +func (x *PlannedChange) GetRaw() []*anypb.Any { + if x != nil { + return x.Raw + } + return nil +} + +func (x *PlannedChange) GetDescriptions() []*PlannedChange_ChangeDescription { + if x != nil { + return x.Descriptions + } + return nil +} + +// Deferred contains all the metadata about a the deferral of a resource +// instance change. +type Deferred struct { + state protoimpl.MessageState `protogen:"open.v1"` + Reason Deferred_Reason `protobuf:"varint,1,opt,name=reason,proto3,enum=terraform1.stacks.Deferred_Reason" json:"reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Deferred) Reset() { + *x = Deferred{} + mi := &file_stacks_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Deferred) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Deferred) ProtoMessage() {} + +func (x *Deferred) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Deferred.ProtoReflect.Descriptor instead. +func (*Deferred) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{23} +} + +func (x *Deferred) GetReason() Deferred_Reason { + if x != nil { + return x.Reason + } + return Deferred_INVALID +} + +// Describes a change made during a Stacks.ApplyStackChanges call. +// +// All of the events of this type taken together represent a sort of "patch" +// modifying the two data structures that the caller must maintain: the +// raw state map, and the description map. Callers must apply these changes +// in the order of the emission of the messages and then retain the entirety +// of both data structures to populate fields in the next PlanStackChanges call. +type AppliedChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Terraform Core's internal representation of the change, presented as + // a sequence of modifications to the raw state data structure. + // + // For each element, in order: + // - If both key and value are set and the key matches an element + // already in the raw state map, the new value replaces the existing one. + // - If both key and value are set but the key does not match an + // element in the raw state map, this represents inserting a new element + // into the map. + // - If key is set and value is not, this represents removing any existing + // element from the raw state map which has the given key, or a no-op + // if no such element exists. + // - No other situation is legal. + // + // This sequence can potentially be zero-length if a particular event only + // has a external-facing "description" component and no raw equivalent. In + // that case the raw state map is unmodified. + Raw []*AppliedChange_RawChange `protobuf:"bytes,1,rep,name=raw,proto3" json:"raw,omitempty"` + // Caller-facing description of this change, to use for presenting + // information to end-users in the UI and for other subsystems such as + // billing. + // + // Callers are expected to maintain a map of description objects that + // gets updated piecemeal by messages in this field. Callers must treat + // the keys as entirely opaque and thus treat the resulting data structure + // as if it were an unsorted set of ChangeDescription objects; the keys + // exist only to allow patching the data structure over time. + // + // For each element, in order: + // - If both key and description are set and the key matches an element + // from the previous apply's description map, the new value replaces + // the existing one. + // - If both key and value are set but the key does not match an + // element in the previous apply's description map, this represents + // inserting a new element into the map. + // - If key is set and description is "deleted", this represents removing + // any existing element from the previous apply's description map which + // has the given key, or a no-op if no such element exists. + // - If a description field is set that the caller doesn't understand, + // the caller should still write it to the updated description map + // but ignore it in further processing. + // - No other situation is legal. + // + // Callers MUST preserve the verbatim description message in the + // description map, even if it contains fields that are not present in + // the caller's current protobuf stubs. In other words, callers must use + // a protocol buffers implementation that is able to preserve unknown + // fields and store them so that future versions of the caller might + // use an updated set of stubs to interact with the previously-stored + // description. + // + // DO NOT attempt to use this to surgically filter particular raw state + // updates from a larger plan. Although external descriptions often match + // with the raw representations in field "raw", that is not guaranteed and + // Terraform Core assumes that it will always be provided with the full + // raw state map during the next plan step. + Descriptions []*AppliedChange_ChangeDescription `protobuf:"bytes,2,rep,name=descriptions,proto3" json:"descriptions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange) Reset() { + *x = AppliedChange{} + mi := &file_stacks_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange) ProtoMessage() {} + +func (x *AppliedChange) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange.ProtoReflect.Descriptor instead. +func (*AppliedChange) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24} +} + +func (x *AppliedChange) GetRaw() []*AppliedChange_RawChange { + if x != nil { + return x.Raw + } + return nil +} + +func (x *AppliedChange) GetDescriptions() []*AppliedChange_ChangeDescription { + if x != nil { + return x.Descriptions + } + return nil +} + +// A container for "progress report" events in both Stacks.PlanStackChanges +// and Stacks.ApplyStackChanges, which share this message type to allow +// clients to share event-handling code between the two phases. +type StackChangeProgress struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Some event types are relevant only to one of the two operations, while + // others are common across both but will include different status codes, + // etc in different phases. + // + // Types that are valid to be assigned to Event: + // + // *StackChangeProgress_ComponentInstanceStatus_ + // *StackChangeProgress_ResourceInstanceStatus_ + // *StackChangeProgress_ResourceInstancePlannedChange_ + // *StackChangeProgress_ProvisionerStatus_ + // *StackChangeProgress_ProvisionerOutput_ + // *StackChangeProgress_ComponentInstanceChanges_ + // *StackChangeProgress_ComponentInstances_ + // *StackChangeProgress_DeferredResourceInstancePlannedChange_ + Event isStackChangeProgress_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress) Reset() { + *x = StackChangeProgress{} + mi := &file_stacks_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress) ProtoMessage() {} + +func (x *StackChangeProgress) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress.ProtoReflect.Descriptor instead. +func (*StackChangeProgress) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25} +} + +func (x *StackChangeProgress) GetEvent() isStackChangeProgress_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *StackChangeProgress) GetComponentInstanceStatus() *StackChangeProgress_ComponentInstanceStatus { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ComponentInstanceStatus_); ok { + return x.ComponentInstanceStatus + } + } + return nil +} + +func (x *StackChangeProgress) GetResourceInstanceStatus() *StackChangeProgress_ResourceInstanceStatus { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ResourceInstanceStatus_); ok { + return x.ResourceInstanceStatus + } + } + return nil +} + +func (x *StackChangeProgress) GetResourceInstancePlannedChange() *StackChangeProgress_ResourceInstancePlannedChange { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ResourceInstancePlannedChange_); ok { + return x.ResourceInstancePlannedChange + } + } + return nil +} + +func (x *StackChangeProgress) GetProvisionerStatus() *StackChangeProgress_ProvisionerStatus { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ProvisionerStatus_); ok { + return x.ProvisionerStatus + } + } + return nil +} + +func (x *StackChangeProgress) GetProvisionerOutput() *StackChangeProgress_ProvisionerOutput { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ProvisionerOutput_); ok { + return x.ProvisionerOutput + } + } + return nil +} + +func (x *StackChangeProgress) GetComponentInstanceChanges() *StackChangeProgress_ComponentInstanceChanges { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ComponentInstanceChanges_); ok { + return x.ComponentInstanceChanges + } + } + return nil +} + +func (x *StackChangeProgress) GetComponentInstances() *StackChangeProgress_ComponentInstances { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_ComponentInstances_); ok { + return x.ComponentInstances + } + } + return nil +} + +func (x *StackChangeProgress) GetDeferredResourceInstancePlannedChange() *StackChangeProgress_DeferredResourceInstancePlannedChange { + if x != nil { + if x, ok := x.Event.(*StackChangeProgress_DeferredResourceInstancePlannedChange_); ok { + return x.DeferredResourceInstancePlannedChange + } + } + return nil +} + +type isStackChangeProgress_Event interface { + isStackChangeProgress_Event() +} + +type StackChangeProgress_ComponentInstanceStatus_ struct { + ComponentInstanceStatus *StackChangeProgress_ComponentInstanceStatus `protobuf:"bytes,1,opt,name=component_instance_status,json=componentInstanceStatus,proto3,oneof"` +} + +type StackChangeProgress_ResourceInstanceStatus_ struct { + ResourceInstanceStatus *StackChangeProgress_ResourceInstanceStatus `protobuf:"bytes,2,opt,name=resource_instance_status,json=resourceInstanceStatus,proto3,oneof"` +} + +type StackChangeProgress_ResourceInstancePlannedChange_ struct { + ResourceInstancePlannedChange *StackChangeProgress_ResourceInstancePlannedChange `protobuf:"bytes,3,opt,name=resource_instance_planned_change,json=resourceInstancePlannedChange,proto3,oneof"` +} + +type StackChangeProgress_ProvisionerStatus_ struct { + ProvisionerStatus *StackChangeProgress_ProvisionerStatus `protobuf:"bytes,4,opt,name=provisioner_status,json=provisionerStatus,proto3,oneof"` +} + +type StackChangeProgress_ProvisionerOutput_ struct { + ProvisionerOutput *StackChangeProgress_ProvisionerOutput `protobuf:"bytes,5,opt,name=provisioner_output,json=provisionerOutput,proto3,oneof"` +} + +type StackChangeProgress_ComponentInstanceChanges_ struct { + ComponentInstanceChanges *StackChangeProgress_ComponentInstanceChanges `protobuf:"bytes,6,opt,name=component_instance_changes,json=componentInstanceChanges,proto3,oneof"` +} + +type StackChangeProgress_ComponentInstances_ struct { + ComponentInstances *StackChangeProgress_ComponentInstances `protobuf:"bytes,7,opt,name=component_instances,json=componentInstances,proto3,oneof"` +} + +type StackChangeProgress_DeferredResourceInstancePlannedChange_ struct { + DeferredResourceInstancePlannedChange *StackChangeProgress_DeferredResourceInstancePlannedChange `protobuf:"bytes,8,opt,name=deferred_resource_instance_planned_change,json=deferredResourceInstancePlannedChange,proto3,oneof"` +} + +func (*StackChangeProgress_ComponentInstanceStatus_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_ResourceInstanceStatus_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_ResourceInstancePlannedChange_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_ProvisionerStatus_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_ProvisionerOutput_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_ComponentInstanceChanges_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_ComponentInstances_) isStackChangeProgress_Event() {} + +func (*StackChangeProgress_DeferredResourceInstancePlannedChange_) isStackChangeProgress_Event() {} + +type ListResourceIdentities struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListResourceIdentities) Reset() { + *x = ListResourceIdentities{} + mi := &file_stacks_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListResourceIdentities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListResourceIdentities) ProtoMessage() {} + +func (x *ListResourceIdentities) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListResourceIdentities.ProtoReflect.Descriptor instead. +func (*ListResourceIdentities) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{26} +} + +type OpenTerraformState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to State: + // + // *OpenTerraformState_Request_ConfigPath + // *OpenTerraformState_Request_Raw + State isOpenTerraformState_Request_State `protobuf_oneof:"state"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenTerraformState_Request) Reset() { + *x = OpenTerraformState_Request{} + mi := &file_stacks_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenTerraformState_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenTerraformState_Request) ProtoMessage() {} + +func (x *OpenTerraformState_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenTerraformState_Request.ProtoReflect.Descriptor instead. +func (*OpenTerraformState_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *OpenTerraformState_Request) GetState() isOpenTerraformState_Request_State { + if x != nil { + return x.State + } + return nil +} + +func (x *OpenTerraformState_Request) GetConfigPath() string { + if x != nil { + if x, ok := x.State.(*OpenTerraformState_Request_ConfigPath); ok { + return x.ConfigPath + } + } + return "" +} + +func (x *OpenTerraformState_Request) GetRaw() []byte { + if x != nil { + if x, ok := x.State.(*OpenTerraformState_Request_Raw); ok { + return x.Raw + } + } + return nil +} + +type isOpenTerraformState_Request_State interface { + isOpenTerraformState_Request_State() +} + +type OpenTerraformState_Request_ConfigPath struct { + // We can open a state based on configuration that has been initialized. + ConfigPath string `protobuf:"bytes,1,opt,name=config_path,json=configPath,proto3,oneof"` +} + +type OpenTerraformState_Request_Raw struct { + // Or a state file based on raw bytes. + Raw []byte `protobuf:"bytes,2,opt,name=raw,proto3,oneof"` +} + +func (*OpenTerraformState_Request_ConfigPath) isOpenTerraformState_Request_State() {} + +func (*OpenTerraformState_Request_Raw) isOpenTerraformState_Request_State() {} + +type OpenTerraformState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + StateHandle int64 `protobuf:"varint,1,opt,name=state_handle,json=stateHandle,proto3" json:"state_handle,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenTerraformState_Response) Reset() { + *x = OpenTerraformState_Response{} + mi := &file_stacks_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenTerraformState_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenTerraformState_Response) ProtoMessage() {} + +func (x *OpenTerraformState_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenTerraformState_Response.ProtoReflect.Descriptor instead. +func (*OpenTerraformState_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *OpenTerraformState_Response) GetStateHandle() int64 { + if x != nil { + return x.StateHandle + } + return 0 +} + +func (x *OpenTerraformState_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type CloseTerraformState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StateHandle int64 `protobuf:"varint,1,opt,name=state_handle,json=stateHandle,proto3" json:"state_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseTerraformState_Request) Reset() { + *x = CloseTerraformState_Request{} + mi := &file_stacks_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseTerraformState_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseTerraformState_Request) ProtoMessage() {} + +func (x *CloseTerraformState_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseTerraformState_Request.ProtoReflect.Descriptor instead. +func (*CloseTerraformState_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *CloseTerraformState_Request) GetStateHandle() int64 { + if x != nil { + return x.StateHandle + } + return 0 +} + +type CloseTerraformState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseTerraformState_Response) Reset() { + *x = CloseTerraformState_Response{} + mi := &file_stacks_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseTerraformState_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseTerraformState_Response) ProtoMessage() {} + +func (x *CloseTerraformState_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseTerraformState_Response.ProtoReflect.Descriptor instead. +func (*CloseTerraformState_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{1, 1} +} + +type MigrateTerraformState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StateHandle int64 `protobuf:"varint,1,opt,name=state_handle,json=stateHandle,proto3" json:"state_handle,omitempty"` // previously opened Terraform state + ConfigHandle int64 `protobuf:"varint,2,opt,name=config_handle,json=configHandle,proto3" json:"config_handle,omitempty"` // new stacks configuration + DependencyLocksHandle int64 `protobuf:"varint,3,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + ProviderCacheHandle int64 `protobuf:"varint,4,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + // Types that are valid to be assigned to Mapping: + // + // *MigrateTerraformState_Request_Simple + Mapping isMigrateTerraformState_Request_Mapping `protobuf_oneof:"mapping"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MigrateTerraformState_Request) Reset() { + *x = MigrateTerraformState_Request{} + mi := &file_stacks_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MigrateTerraformState_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateTerraformState_Request) ProtoMessage() {} + +func (x *MigrateTerraformState_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateTerraformState_Request.ProtoReflect.Descriptor instead. +func (*MigrateTerraformState_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *MigrateTerraformState_Request) GetStateHandle() int64 { + if x != nil { + return x.StateHandle + } + return 0 +} + +func (x *MigrateTerraformState_Request) GetConfigHandle() int64 { + if x != nil { + return x.ConfigHandle + } + return 0 +} + +func (x *MigrateTerraformState_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *MigrateTerraformState_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +func (x *MigrateTerraformState_Request) GetMapping() isMigrateTerraformState_Request_Mapping { + if x != nil { + return x.Mapping + } + return nil +} + +func (x *MigrateTerraformState_Request) GetSimple() *MigrateTerraformState_Request_Mapping { + if x != nil { + if x, ok := x.Mapping.(*MigrateTerraformState_Request_Simple); ok { + return x.Simple + } + } + return nil +} + +type isMigrateTerraformState_Request_Mapping interface { + isMigrateTerraformState_Request_Mapping() +} + +type MigrateTerraformState_Request_Simple struct { + // simple is a simple mapping of Terraform addresses to stack components + Simple *MigrateTerraformState_Request_Mapping `protobuf:"bytes,5,opt,name=simple,proto3,oneof"` +} + +func (*MigrateTerraformState_Request_Simple) isMigrateTerraformState_Request_Mapping() {} + +type MigrateTerraformState_Event struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Result: + // + // *MigrateTerraformState_Event_Diagnostic + // *MigrateTerraformState_Event_AppliedChange + Result isMigrateTerraformState_Event_Result `protobuf_oneof:"result"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MigrateTerraformState_Event) Reset() { + *x = MigrateTerraformState_Event{} + mi := &file_stacks_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MigrateTerraformState_Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateTerraformState_Event) ProtoMessage() {} + +func (x *MigrateTerraformState_Event) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateTerraformState_Event.ProtoReflect.Descriptor instead. +func (*MigrateTerraformState_Event) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *MigrateTerraformState_Event) GetResult() isMigrateTerraformState_Event_Result { + if x != nil { + return x.Result + } + return nil +} + +func (x *MigrateTerraformState_Event) GetDiagnostic() *terraform1.Diagnostic { + if x != nil { + if x, ok := x.Result.(*MigrateTerraformState_Event_Diagnostic); ok { + return x.Diagnostic + } + } + return nil +} + +func (x *MigrateTerraformState_Event) GetAppliedChange() *AppliedChange { + if x != nil { + if x, ok := x.Result.(*MigrateTerraformState_Event_AppliedChange); ok { + return x.AppliedChange + } + } + return nil +} + +type isMigrateTerraformState_Event_Result interface { + isMigrateTerraformState_Event_Result() +} + +type MigrateTerraformState_Event_Diagnostic struct { + Diagnostic *terraform1.Diagnostic `protobuf:"bytes,1,opt,name=diagnostic,proto3,oneof"` +} + +type MigrateTerraformState_Event_AppliedChange struct { + AppliedChange *AppliedChange `protobuf:"bytes,2,opt,name=applied_change,json=appliedChange,proto3,oneof"` +} + +func (*MigrateTerraformState_Event_Diagnostic) isMigrateTerraformState_Event_Result() {} + +func (*MigrateTerraformState_Event_AppliedChange) isMigrateTerraformState_Event_Result() {} + +// Mapping of terraform constructs to stack components. +type MigrateTerraformState_Request_Mapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + // resource_address_map maps resources in the root module to their new + // components. The keys are the addresses of the resources in the Terraform + // state, and the values are the names of the new components. + // + // eg. resource_type.resource_name -> component_name + ResourceAddressMap map[string]string `protobuf:"bytes,1,rep,name=resource_address_map,json=resourceAddressMap,proto3" json:"resource_address_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // module_address_map maps modules in the root module to their new + // components. The keys are the module names in the Terraform state, and + // the values are the names of the new components. + // + // eg. module_name -> component_name + ModuleAddressMap map[string]string `protobuf:"bytes,2,rep,name=module_address_map,json=moduleAddressMap,proto3" json:"module_address_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MigrateTerraformState_Request_Mapping) Reset() { + *x = MigrateTerraformState_Request_Mapping{} + mi := &file_stacks_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MigrateTerraformState_Request_Mapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MigrateTerraformState_Request_Mapping) ProtoMessage() {} + +func (x *MigrateTerraformState_Request_Mapping) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MigrateTerraformState_Request_Mapping.ProtoReflect.Descriptor instead. +func (*MigrateTerraformState_Request_Mapping) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{2, 0, 0} +} + +func (x *MigrateTerraformState_Request_Mapping) GetResourceAddressMap() map[string]string { + if x != nil { + return x.ResourceAddressMap + } + return nil +} + +func (x *MigrateTerraformState_Request_Mapping) GetModuleAddressMap() map[string]string { + if x != nil { + return x.ModuleAddressMap + } + return nil +} + +type OpenStackConfiguration_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceBundleHandle int64 `protobuf:"varint,1,opt,name=source_bundle_handle,json=sourceBundleHandle,proto3" json:"source_bundle_handle,omitempty"` + SourceAddress *terraform1.SourceAddress `protobuf:"bytes,2,opt,name=source_address,json=sourceAddress,proto3" json:"source_address,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackConfiguration_Request) Reset() { + *x = OpenStackConfiguration_Request{} + mi := &file_stacks_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackConfiguration_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackConfiguration_Request) ProtoMessage() {} + +func (x *OpenStackConfiguration_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackConfiguration_Request.ProtoReflect.Descriptor instead. +func (*OpenStackConfiguration_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *OpenStackConfiguration_Request) GetSourceBundleHandle() int64 { + if x != nil { + return x.SourceBundleHandle + } + return 0 +} + +func (x *OpenStackConfiguration_Request) GetSourceAddress() *terraform1.SourceAddress { + if x != nil { + return x.SourceAddress + } + return nil +} + +type OpenStackConfiguration_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackConfigHandle int64 `protobuf:"varint,1,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackConfiguration_Response) Reset() { + *x = OpenStackConfiguration_Response{} + mi := &file_stacks_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackConfiguration_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackConfiguration_Response) ProtoMessage() {} + +func (x *OpenStackConfiguration_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackConfiguration_Response.ProtoReflect.Descriptor instead. +func (*OpenStackConfiguration_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{3, 1} +} + +func (x *OpenStackConfiguration_Response) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +func (x *OpenStackConfiguration_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type CloseStackConfiguration_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackConfigHandle int64 `protobuf:"varint,1,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackConfiguration_Request) Reset() { + *x = CloseStackConfiguration_Request{} + mi := &file_stacks_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackConfiguration_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackConfiguration_Request) ProtoMessage() {} + +func (x *CloseStackConfiguration_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackConfiguration_Request.ProtoReflect.Descriptor instead. +func (*CloseStackConfiguration_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *CloseStackConfiguration_Request) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +type CloseStackConfiguration_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackConfiguration_Response) Reset() { + *x = CloseStackConfiguration_Response{} + mi := &file_stacks_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackConfiguration_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackConfiguration_Response) ProtoMessage() {} + +func (x *CloseStackConfiguration_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackConfiguration_Response.ProtoReflect.Descriptor instead. +func (*CloseStackConfiguration_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{4, 1} +} + +type ValidateStackConfiguration_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackConfigHandle int64 `protobuf:"varint,1,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + DependencyLocksHandle int64 `protobuf:"varint,2,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + ProviderCacheHandle int64 `protobuf:"varint,3,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateStackConfiguration_Request) Reset() { + *x = ValidateStackConfiguration_Request{} + mi := &file_stacks_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateStackConfiguration_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateStackConfiguration_Request) ProtoMessage() {} + +func (x *ValidateStackConfiguration_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateStackConfiguration_Request.ProtoReflect.Descriptor instead. +func (*ValidateStackConfiguration_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *ValidateStackConfiguration_Request) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +func (x *ValidateStackConfiguration_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *ValidateStackConfiguration_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +type ValidateStackConfiguration_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateStackConfiguration_Response) Reset() { + *x = ValidateStackConfiguration_Response{} + mi := &file_stacks_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateStackConfiguration_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateStackConfiguration_Response) ProtoMessage() {} + +func (x *ValidateStackConfiguration_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateStackConfiguration_Response.ProtoReflect.Descriptor instead. +func (*ValidateStackConfiguration_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{5, 1} +} + +func (x *ValidateStackConfiguration_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type FindStackConfigurationComponents_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackConfigHandle int64 `protobuf:"varint,1,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_Request) Reset() { + *x = FindStackConfigurationComponents_Request{} + mi := &file_stacks_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_Request) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[42] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_Request.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 0} +} + +func (x *FindStackConfigurationComponents_Request) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +type FindStackConfigurationComponents_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *FindStackConfigurationComponents_StackConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_Response) Reset() { + *x = FindStackConfigurationComponents_Response{} + mi := &file_stacks_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_Response) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_Response.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 1} +} + +func (x *FindStackConfigurationComponents_Response) GetConfig() *FindStackConfigurationComponents_StackConfig { + if x != nil { + return x.Config + } + return nil +} + +type FindStackConfigurationComponents_StackConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Components map[string]*FindStackConfigurationComponents_Component `protobuf:"bytes,1,rep,name=components,proto3" json:"components,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + EmbeddedStacks map[string]*FindStackConfigurationComponents_EmbeddedStack `protobuf:"bytes,2,rep,name=embedded_stacks,json=embeddedStacks,proto3" json:"embedded_stacks,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + InputVariables map[string]*FindStackConfigurationComponents_InputVariable `protobuf:"bytes,3,rep,name=input_variables,json=inputVariables,proto3" json:"input_variables,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + OutputValues map[string]*FindStackConfigurationComponents_OutputValue `protobuf:"bytes,4,rep,name=output_values,json=outputValues,proto3" json:"output_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Removed map[string]*FindStackConfigurationComponents_Removed `protobuf:"bytes,5,rep,name=removed,proto3" json:"removed,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_StackConfig) Reset() { + *x = FindStackConfigurationComponents_StackConfig{} + mi := &file_stacks_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_StackConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_StackConfig) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_StackConfig) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_StackConfig.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_StackConfig) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 2} +} + +func (x *FindStackConfigurationComponents_StackConfig) GetComponents() map[string]*FindStackConfigurationComponents_Component { + if x != nil { + return x.Components + } + return nil +} + +func (x *FindStackConfigurationComponents_StackConfig) GetEmbeddedStacks() map[string]*FindStackConfigurationComponents_EmbeddedStack { + if x != nil { + return x.EmbeddedStacks + } + return nil +} + +func (x *FindStackConfigurationComponents_StackConfig) GetInputVariables() map[string]*FindStackConfigurationComponents_InputVariable { + if x != nil { + return x.InputVariables + } + return nil +} + +func (x *FindStackConfigurationComponents_StackConfig) GetOutputValues() map[string]*FindStackConfigurationComponents_OutputValue { + if x != nil { + return x.OutputValues + } + return nil +} + +func (x *FindStackConfigurationComponents_StackConfig) GetRemoved() map[string]*FindStackConfigurationComponents_Removed { + if x != nil { + return x.Removed + } + return nil +} + +type FindStackConfigurationComponents_EmbeddedStack struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Instances FindStackConfigurationComponents_Instances `protobuf:"varint,2,opt,name=instances,proto3,enum=terraform1.stacks.FindStackConfigurationComponents_Instances" json:"instances,omitempty"` + Config *FindStackConfigurationComponents_StackConfig `protobuf:"bytes,3,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_EmbeddedStack) Reset() { + *x = FindStackConfigurationComponents_EmbeddedStack{} + mi := &file_stacks_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_EmbeddedStack) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_EmbeddedStack) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_EmbeddedStack) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_EmbeddedStack.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_EmbeddedStack) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 3} +} + +func (x *FindStackConfigurationComponents_EmbeddedStack) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *FindStackConfigurationComponents_EmbeddedStack) GetInstances() FindStackConfigurationComponents_Instances { + if x != nil { + return x.Instances + } + return FindStackConfigurationComponents_SINGLE +} + +func (x *FindStackConfigurationComponents_EmbeddedStack) GetConfig() *FindStackConfigurationComponents_StackConfig { + if x != nil { + return x.Config + } + return nil +} + +type FindStackConfigurationComponents_Component struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Instances FindStackConfigurationComponents_Instances `protobuf:"varint,2,opt,name=instances,proto3,enum=terraform1.stacks.FindStackConfigurationComponents_Instances" json:"instances,omitempty"` + ComponentAddr string `protobuf:"bytes,3,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_Component) Reset() { + *x = FindStackConfigurationComponents_Component{} + mi := &file_stacks_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_Component) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_Component) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_Component) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_Component.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_Component) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 4} +} + +func (x *FindStackConfigurationComponents_Component) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *FindStackConfigurationComponents_Component) GetInstances() FindStackConfigurationComponents_Instances { + if x != nil { + return x.Instances + } + return FindStackConfigurationComponents_SINGLE +} + +func (x *FindStackConfigurationComponents_Component) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +type FindStackConfigurationComponents_Removed struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Deprecated: Marked as deprecated in stacks.proto. + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + // Deprecated: Marked as deprecated in stacks.proto. + Instances FindStackConfigurationComponents_Instances `protobuf:"varint,2,opt,name=instances,proto3,enum=terraform1.stacks.FindStackConfigurationComponents_Instances" json:"instances,omitempty"` + // Deprecated: Marked as deprecated in stacks.proto. + ComponentAddr string `protobuf:"bytes,3,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + // Deprecated: Marked as deprecated in stacks.proto. + Destroy bool `protobuf:"varint,4,opt,name=destroy,proto3" json:"destroy,omitempty"` + Blocks []*FindStackConfigurationComponents_Removed_Block `protobuf:"bytes,5,rep,name=blocks,proto3" json:"blocks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_Removed) Reset() { + *x = FindStackConfigurationComponents_Removed{} + mi := &file_stacks_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_Removed) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_Removed) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_Removed) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_Removed.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_Removed) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 5} +} + +// Deprecated: Marked as deprecated in stacks.proto. +func (x *FindStackConfigurationComponents_Removed) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +// Deprecated: Marked as deprecated in stacks.proto. +func (x *FindStackConfigurationComponents_Removed) GetInstances() FindStackConfigurationComponents_Instances { + if x != nil { + return x.Instances + } + return FindStackConfigurationComponents_SINGLE +} + +// Deprecated: Marked as deprecated in stacks.proto. +func (x *FindStackConfigurationComponents_Removed) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +// Deprecated: Marked as deprecated in stacks.proto. +func (x *FindStackConfigurationComponents_Removed) GetDestroy() bool { + if x != nil { + return x.Destroy + } + return false +} + +func (x *FindStackConfigurationComponents_Removed) GetBlocks() []*FindStackConfigurationComponents_Removed_Block { + if x != nil { + return x.Blocks + } + return nil +} + +type FindStackConfigurationComponents_InputVariable struct { + state protoimpl.MessageState `protogen:"open.v1"` + Optional bool `protobuf:"varint,1,opt,name=optional,proto3" json:"optional,omitempty"` + Sensitive bool `protobuf:"varint,2,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + Ephemeral bool `protobuf:"varint,3,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_InputVariable) Reset() { + *x = FindStackConfigurationComponents_InputVariable{} + mi := &file_stacks_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_InputVariable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_InputVariable) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_InputVariable) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_InputVariable.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_InputVariable) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 6} +} + +func (x *FindStackConfigurationComponents_InputVariable) GetOptional() bool { + if x != nil { + return x.Optional + } + return false +} + +func (x *FindStackConfigurationComponents_InputVariable) GetSensitive() bool { + if x != nil { + return x.Sensitive + } + return false +} + +func (x *FindStackConfigurationComponents_InputVariable) GetEphemeral() bool { + if x != nil { + return x.Ephemeral + } + return false +} + +type FindStackConfigurationComponents_OutputValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sensitive bool `protobuf:"varint,1,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + Ephemeral bool `protobuf:"varint,2,opt,name=ephemeral,proto3" json:"ephemeral,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_OutputValue) Reset() { + *x = FindStackConfigurationComponents_OutputValue{} + mi := &file_stacks_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_OutputValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_OutputValue) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_OutputValue) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_OutputValue.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_OutputValue) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 7} +} + +func (x *FindStackConfigurationComponents_OutputValue) GetSensitive() bool { + if x != nil { + return x.Sensitive + } + return false +} + +func (x *FindStackConfigurationComponents_OutputValue) GetEphemeral() bool { + if x != nil { + return x.Ephemeral + } + return false +} + +type FindStackConfigurationComponents_Removed_Block struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Instances FindStackConfigurationComponents_Instances `protobuf:"varint,2,opt,name=instances,proto3,enum=terraform1.stacks.FindStackConfigurationComponents_Instances" json:"instances,omitempty"` + ComponentAddr string `protobuf:"bytes,3,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + Destroy bool `protobuf:"varint,4,opt,name=destroy,proto3" json:"destroy,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FindStackConfigurationComponents_Removed_Block) Reset() { + *x = FindStackConfigurationComponents_Removed_Block{} + mi := &file_stacks_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FindStackConfigurationComponents_Removed_Block) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FindStackConfigurationComponents_Removed_Block) ProtoMessage() {} + +func (x *FindStackConfigurationComponents_Removed_Block) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FindStackConfigurationComponents_Removed_Block.ProtoReflect.Descriptor instead. +func (*FindStackConfigurationComponents_Removed_Block) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{6, 5, 0} +} + +func (x *FindStackConfigurationComponents_Removed_Block) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *FindStackConfigurationComponents_Removed_Block) GetInstances() FindStackConfigurationComponents_Instances { + if x != nil { + return x.Instances + } + return FindStackConfigurationComponents_SINGLE +} + +func (x *FindStackConfigurationComponents_Removed_Block) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +func (x *FindStackConfigurationComponents_Removed_Block) GetDestroy() bool { + if x != nil { + return x.Destroy + } + return false +} + +type OpenStackState_RequestItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Raw *AppliedChange_RawChange `protobuf:"bytes,1,opt,name=raw,proto3" json:"raw,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackState_RequestItem) Reset() { + *x = OpenStackState_RequestItem{} + mi := &file_stacks_proto_msgTypes[56] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackState_RequestItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackState_RequestItem) ProtoMessage() {} + +func (x *OpenStackState_RequestItem) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[56] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackState_RequestItem.ProtoReflect.Descriptor instead. +func (*OpenStackState_RequestItem) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{7, 0} +} + +func (x *OpenStackState_RequestItem) GetRaw() *AppliedChange_RawChange { + if x != nil { + return x.Raw + } + return nil +} + +type OpenStackState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + StateHandle int64 `protobuf:"varint,1,opt,name=state_handle,json=stateHandle,proto3" json:"state_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackState_Response) Reset() { + *x = OpenStackState_Response{} + mi := &file_stacks_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackState_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackState_Response) ProtoMessage() {} + +func (x *OpenStackState_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[57] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackState_Response.ProtoReflect.Descriptor instead. +func (*OpenStackState_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{7, 1} +} + +func (x *OpenStackState_Response) GetStateHandle() int64 { + if x != nil { + return x.StateHandle + } + return 0 +} + +type CloseStackState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StateHandle int64 `protobuf:"varint,1,opt,name=state_handle,json=stateHandle,proto3" json:"state_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackState_Request) Reset() { + *x = CloseStackState_Request{} + mi := &file_stacks_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackState_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackState_Request) ProtoMessage() {} + +func (x *CloseStackState_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[58] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackState_Request.ProtoReflect.Descriptor instead. +func (*CloseStackState_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{8, 0} +} + +func (x *CloseStackState_Request) GetStateHandle() int64 { + if x != nil { + return x.StateHandle + } + return 0 +} + +type CloseStackState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackState_Response) Reset() { + *x = CloseStackState_Response{} + mi := &file_stacks_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackState_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackState_Response) ProtoMessage() {} + +func (x *CloseStackState_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[59] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackState_Response.ProtoReflect.Descriptor instead. +func (*CloseStackState_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{8, 1} +} + +type PlanStackChanges_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + PlanMode PlanMode `protobuf:"varint,1,opt,name=plan_mode,json=planMode,proto3,enum=terraform1.stacks.PlanMode" json:"plan_mode,omitempty"` + StackConfigHandle int64 `protobuf:"varint,2,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + PreviousStateHandle int64 `protobuf:"varint,7,opt,name=previous_state_handle,json=previousStateHandle,proto3" json:"previous_state_handle,omitempty"` + // Deprecated: Marked as deprecated in stacks.proto. + PreviousState map[string]*anypb.Any `protobuf:"bytes,3,rep,name=previous_state,json=previousState,proto3" json:"previous_state,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + DependencyLocksHandle int64 `protobuf:"varint,4,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + ProviderCacheHandle int64 `protobuf:"varint,5,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + InputValues map[string]*DynamicValueWithSource `protobuf:"bytes,6,rep,name=input_values,json=inputValues,proto3" json:"input_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // TODO: Various other planning options + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanStackChanges_Request) Reset() { + *x = PlanStackChanges_Request{} + mi := &file_stacks_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanStackChanges_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanStackChanges_Request) ProtoMessage() {} + +func (x *PlanStackChanges_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[60] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanStackChanges_Request.ProtoReflect.Descriptor instead. +func (*PlanStackChanges_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{9, 0} +} + +func (x *PlanStackChanges_Request) GetPlanMode() PlanMode { + if x != nil { + return x.PlanMode + } + return PlanMode_NORMAL +} + +func (x *PlanStackChanges_Request) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +func (x *PlanStackChanges_Request) GetPreviousStateHandle() int64 { + if x != nil { + return x.PreviousStateHandle + } + return 0 +} + +// Deprecated: Marked as deprecated in stacks.proto. +func (x *PlanStackChanges_Request) GetPreviousState() map[string]*anypb.Any { + if x != nil { + return x.PreviousState + } + return nil +} + +func (x *PlanStackChanges_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *PlanStackChanges_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +func (x *PlanStackChanges_Request) GetInputValues() map[string]*DynamicValueWithSource { + if x != nil { + return x.InputValues + } + return nil +} + +type PlanStackChanges_Event struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *PlanStackChanges_Event_PlannedChange + // *PlanStackChanges_Event_Diagnostic + // *PlanStackChanges_Event_Progress + Event isPlanStackChanges_Event_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanStackChanges_Event) Reset() { + *x = PlanStackChanges_Event{} + mi := &file_stacks_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanStackChanges_Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanStackChanges_Event) ProtoMessage() {} + +func (x *PlanStackChanges_Event) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanStackChanges_Event.ProtoReflect.Descriptor instead. +func (*PlanStackChanges_Event) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{9, 1} +} + +func (x *PlanStackChanges_Event) GetEvent() isPlanStackChanges_Event_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *PlanStackChanges_Event) GetPlannedChange() *PlannedChange { + if x != nil { + if x, ok := x.Event.(*PlanStackChanges_Event_PlannedChange); ok { + return x.PlannedChange + } + } + return nil +} + +func (x *PlanStackChanges_Event) GetDiagnostic() *terraform1.Diagnostic { + if x != nil { + if x, ok := x.Event.(*PlanStackChanges_Event_Diagnostic); ok { + return x.Diagnostic + } + } + return nil +} + +func (x *PlanStackChanges_Event) GetProgress() *StackChangeProgress { + if x != nil { + if x, ok := x.Event.(*PlanStackChanges_Event_Progress); ok { + return x.Progress + } + } + return nil +} + +type isPlanStackChanges_Event_Event interface { + isPlanStackChanges_Event_Event() +} + +type PlanStackChanges_Event_PlannedChange struct { + PlannedChange *PlannedChange `protobuf:"bytes,1,opt,name=planned_change,json=plannedChange,proto3,oneof"` +} + +type PlanStackChanges_Event_Diagnostic struct { + Diagnostic *terraform1.Diagnostic `protobuf:"bytes,2,opt,name=diagnostic,proto3,oneof"` +} + +type PlanStackChanges_Event_Progress struct { + Progress *StackChangeProgress `protobuf:"bytes,10,opt,name=progress,proto3,oneof"` +} + +func (*PlanStackChanges_Event_PlannedChange) isPlanStackChanges_Event_Event() {} + +func (*PlanStackChanges_Event_Diagnostic) isPlanStackChanges_Event_Event() {} + +func (*PlanStackChanges_Event_Progress) isPlanStackChanges_Event_Event() {} + +type OpenStackPlan_RequestItem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Raw *anypb.Any `protobuf:"bytes,1,opt,name=raw,proto3" json:"raw,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackPlan_RequestItem) Reset() { + *x = OpenStackPlan_RequestItem{} + mi := &file_stacks_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackPlan_RequestItem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackPlan_RequestItem) ProtoMessage() {} + +func (x *OpenStackPlan_RequestItem) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackPlan_RequestItem.ProtoReflect.Descriptor instead. +func (*OpenStackPlan_RequestItem) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{10, 0} +} + +func (x *OpenStackPlan_RequestItem) GetRaw() *anypb.Any { + if x != nil { + return x.Raw + } + return nil +} + +type OpenStackPlan_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + PlanHandle int64 `protobuf:"varint,1,opt,name=plan_handle,json=planHandle,proto3" json:"plan_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackPlan_Response) Reset() { + *x = OpenStackPlan_Response{} + mi := &file_stacks_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackPlan_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackPlan_Response) ProtoMessage() {} + +func (x *OpenStackPlan_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackPlan_Response.ProtoReflect.Descriptor instead. +func (*OpenStackPlan_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{10, 1} +} + +func (x *OpenStackPlan_Response) GetPlanHandle() int64 { + if x != nil { + return x.PlanHandle + } + return 0 +} + +type CloseStackPlan_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + PlanHandle int64 `protobuf:"varint,1,opt,name=plan_handle,json=planHandle,proto3" json:"plan_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackPlan_Request) Reset() { + *x = CloseStackPlan_Request{} + mi := &file_stacks_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackPlan_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackPlan_Request) ProtoMessage() {} + +func (x *CloseStackPlan_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackPlan_Request.ProtoReflect.Descriptor instead. +func (*CloseStackPlan_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{11, 0} +} + +func (x *CloseStackPlan_Request) GetPlanHandle() int64 { + if x != nil { + return x.PlanHandle + } + return 0 +} + +type CloseStackPlan_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseStackPlan_Response) Reset() { + *x = CloseStackPlan_Response{} + mi := &file_stacks_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseStackPlan_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseStackPlan_Response) ProtoMessage() {} + +func (x *CloseStackPlan_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseStackPlan_Response.ProtoReflect.Descriptor instead. +func (*CloseStackPlan_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{11, 1} +} + +type ApplyStackChanges_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // This must refer to exactly the same configuration that was + // passed to PlanStackChanges when creating this plan, or the + // results will be unpredictable. + StackConfigHandle int64 `protobuf:"varint,1,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + // The caller should send all of the keys present in the previous + // apply's description map. Terraform Core will use this for + // situations such as updating existing descriptions to newer + // formats even if no change is being made to the corresponding + // real objects. + KnownDescriptionKeys []string `protobuf:"bytes,3,rep,name=known_description_keys,json=knownDescriptionKeys,proto3" json:"known_description_keys,omitempty"` + // The handle for a saved plan previously loaded using the + // Stacks.OpenPlan function. + // Applying a plan immediately invalidates it, so the handle will + // be automatically closed. + PlanHandle int64 `protobuf:"varint,8,opt,name=plan_handle,json=planHandle,proto3" json:"plan_handle,omitempty"` + // This must include all of the "raw" values emitted through + // PlannedChange events during the PlanStackChanges operation + // that created this plan, concatenated together in the same + // order they were written to the PlanStackChanges event stream. + // + // Use plan_handle instead. This will be removed in future. + // + // Deprecated: Marked as deprecated in stacks.proto. + PlannedChanges []*anypb.Any `protobuf:"bytes,4,rep,name=planned_changes,json=plannedChanges,proto3" json:"planned_changes,omitempty"` + // This must be equivalent to the argument of the same name + // passed to PlanStackChanges when creating this plan. + DependencyLocksHandle int64 `protobuf:"varint,5,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + // This must be equivalent to the argument of the same name + // passed to PlanStackChanges when creating this plan. + ProviderCacheHandle int64 `protobuf:"varint,6,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + // Any input variables identified as an "apply-time input variable" + // in the plan must have values provided here. + // + // Callers may also optionally include values for other declared input + // variables, but if so their values must exactly match those used when + // creating the plan. + InputValues map[string]*DynamicValueWithSource `protobuf:"bytes,7,rep,name=input_values,json=inputValues,proto3" json:"input_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyStackChanges_Request) Reset() { + *x = ApplyStackChanges_Request{} + mi := &file_stacks_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyStackChanges_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyStackChanges_Request) ProtoMessage() {} + +func (x *ApplyStackChanges_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyStackChanges_Request.ProtoReflect.Descriptor instead. +func (*ApplyStackChanges_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{12, 0} +} + +func (x *ApplyStackChanges_Request) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +func (x *ApplyStackChanges_Request) GetKnownDescriptionKeys() []string { + if x != nil { + return x.KnownDescriptionKeys + } + return nil +} + +func (x *ApplyStackChanges_Request) GetPlanHandle() int64 { + if x != nil { + return x.PlanHandle + } + return 0 +} + +// Deprecated: Marked as deprecated in stacks.proto. +func (x *ApplyStackChanges_Request) GetPlannedChanges() []*anypb.Any { + if x != nil { + return x.PlannedChanges + } + return nil +} + +func (x *ApplyStackChanges_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *ApplyStackChanges_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +func (x *ApplyStackChanges_Request) GetInputValues() map[string]*DynamicValueWithSource { + if x != nil { + return x.InputValues + } + return nil +} + +type ApplyStackChanges_Event struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Event: + // + // *ApplyStackChanges_Event_AppliedChange + // *ApplyStackChanges_Event_Diagnostic + // *ApplyStackChanges_Event_Progress + Event isApplyStackChanges_Event_Event `protobuf_oneof:"event"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyStackChanges_Event) Reset() { + *x = ApplyStackChanges_Event{} + mi := &file_stacks_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyStackChanges_Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyStackChanges_Event) ProtoMessage() {} + +func (x *ApplyStackChanges_Event) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyStackChanges_Event.ProtoReflect.Descriptor instead. +func (*ApplyStackChanges_Event) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{12, 1} +} + +func (x *ApplyStackChanges_Event) GetEvent() isApplyStackChanges_Event_Event { + if x != nil { + return x.Event + } + return nil +} + +func (x *ApplyStackChanges_Event) GetAppliedChange() *AppliedChange { + if x != nil { + if x, ok := x.Event.(*ApplyStackChanges_Event_AppliedChange); ok { + return x.AppliedChange + } + } + return nil +} + +func (x *ApplyStackChanges_Event) GetDiagnostic() *terraform1.Diagnostic { + if x != nil { + if x, ok := x.Event.(*ApplyStackChanges_Event_Diagnostic); ok { + return x.Diagnostic + } + } + return nil +} + +func (x *ApplyStackChanges_Event) GetProgress() *StackChangeProgress { + if x != nil { + if x, ok := x.Event.(*ApplyStackChanges_Event_Progress); ok { + return x.Progress + } + } + return nil +} + +type isApplyStackChanges_Event_Event interface { + isApplyStackChanges_Event_Event() +} + +type ApplyStackChanges_Event_AppliedChange struct { + AppliedChange *AppliedChange `protobuf:"bytes,1,opt,name=applied_change,json=appliedChange,proto3,oneof"` +} + +type ApplyStackChanges_Event_Diagnostic struct { + Diagnostic *terraform1.Diagnostic `protobuf:"bytes,2,opt,name=diagnostic,proto3,oneof"` +} + +type ApplyStackChanges_Event_Progress struct { + Progress *StackChangeProgress `protobuf:"bytes,3,opt,name=progress,proto3,oneof"` +} + +func (*ApplyStackChanges_Event_AppliedChange) isApplyStackChanges_Event_Event() {} + +func (*ApplyStackChanges_Event_Diagnostic) isApplyStackChanges_Event_Event() {} + +func (*ApplyStackChanges_Event_Progress) isApplyStackChanges_Event_Event() {} + +type OpenStackInspector_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackConfigHandle int64 `protobuf:"varint,1,opt,name=stack_config_handle,json=stackConfigHandle,proto3" json:"stack_config_handle,omitempty"` + State map[string]*anypb.Any `protobuf:"bytes,2,rep,name=state,proto3" json:"state,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + DependencyLocksHandle int64 `protobuf:"varint,3,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + ProviderCacheHandle int64 `protobuf:"varint,4,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + InputValues map[string]*DynamicValueWithSource `protobuf:"bytes,5,rep,name=input_values,json=inputValues,proto3" json:"input_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackInspector_Request) Reset() { + *x = OpenStackInspector_Request{} + mi := &file_stacks_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackInspector_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackInspector_Request) ProtoMessage() {} + +func (x *OpenStackInspector_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackInspector_Request.ProtoReflect.Descriptor instead. +func (*OpenStackInspector_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{13, 0} +} + +func (x *OpenStackInspector_Request) GetStackConfigHandle() int64 { + if x != nil { + return x.StackConfigHandle + } + return 0 +} + +func (x *OpenStackInspector_Request) GetState() map[string]*anypb.Any { + if x != nil { + return x.State + } + return nil +} + +func (x *OpenStackInspector_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *OpenStackInspector_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +func (x *OpenStackInspector_Request) GetInputValues() map[string]*DynamicValueWithSource { + if x != nil { + return x.InputValues + } + return nil +} + +type OpenStackInspector_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackInspectorHandle int64 `protobuf:"varint,1,opt,name=stack_inspector_handle,json=stackInspectorHandle,proto3" json:"stack_inspector_handle,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenStackInspector_Response) Reset() { + *x = OpenStackInspector_Response{} + mi := &file_stacks_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenStackInspector_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenStackInspector_Response) ProtoMessage() {} + +func (x *OpenStackInspector_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[72] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenStackInspector_Response.ProtoReflect.Descriptor instead. +func (*OpenStackInspector_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{13, 1} +} + +func (x *OpenStackInspector_Response) GetStackInspectorHandle() int64 { + if x != nil { + return x.StackInspectorHandle + } + return 0 +} + +func (x *OpenStackInspector_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type InspectExpressionResult_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StackInspectorHandle int64 `protobuf:"varint,1,opt,name=stack_inspector_handle,json=stackInspectorHandle,proto3" json:"stack_inspector_handle,omitempty"` + ExpressionSrc []byte `protobuf:"bytes,2,opt,name=expression_src,json=expressionSrc,proto3" json:"expression_src,omitempty"` + StackAddr string `protobuf:"bytes,3,opt,name=stack_addr,json=stackAddr,proto3" json:"stack_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InspectExpressionResult_Request) Reset() { + *x = InspectExpressionResult_Request{} + mi := &file_stacks_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InspectExpressionResult_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InspectExpressionResult_Request) ProtoMessage() {} + +func (x *InspectExpressionResult_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[75] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InspectExpressionResult_Request.ProtoReflect.Descriptor instead. +func (*InspectExpressionResult_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{14, 0} +} + +func (x *InspectExpressionResult_Request) GetStackInspectorHandle() int64 { + if x != nil { + return x.StackInspectorHandle + } + return 0 +} + +func (x *InspectExpressionResult_Request) GetExpressionSrc() []byte { + if x != nil { + return x.ExpressionSrc + } + return nil +} + +func (x *InspectExpressionResult_Request) GetStackAddr() string { + if x != nil { + return x.StackAddr + } + return "" +} + +type InspectExpressionResult_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The result of evaluating the expression, if successful enough to + // produce a result. Unpopulated if the expression was too invalid + // to produce a result, with the problem then described in the + // associated diagnostics. + // + // Uses a MessagePack encoding with in-band type information. + Result *DynamicValue `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Diagnostics []*terraform1.Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InspectExpressionResult_Response) Reset() { + *x = InspectExpressionResult_Response{} + mi := &file_stacks_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InspectExpressionResult_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InspectExpressionResult_Response) ProtoMessage() {} + +func (x *InspectExpressionResult_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[76] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InspectExpressionResult_Response.ProtoReflect.Descriptor instead. +func (*InspectExpressionResult_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{14, 1} +} + +func (x *InspectExpressionResult_Response) GetResult() *DynamicValue { + if x != nil { + return x.Result + } + return nil +} + +func (x *InspectExpressionResult_Response) GetDiagnostics() []*terraform1.Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type AttributePath_Step struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selector: + // + // *AttributePath_Step_AttributeName + // *AttributePath_Step_ElementKeyString + // *AttributePath_Step_ElementKeyInt + Selector isAttributePath_Step_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttributePath_Step) Reset() { + *x = AttributePath_Step{} + mi := &file_stacks_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttributePath_Step) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttributePath_Step) ProtoMessage() {} + +func (x *AttributePath_Step) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[77] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttributePath_Step.ProtoReflect.Descriptor instead. +func (*AttributePath_Step) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{18, 0} +} + +func (x *AttributePath_Step) GetSelector() isAttributePath_Step_Selector { + if x != nil { + return x.Selector + } + return nil +} + +func (x *AttributePath_Step) GetAttributeName() string { + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_AttributeName); ok { + return x.AttributeName + } + } + return "" +} + +func (x *AttributePath_Step) GetElementKeyString() string { + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_ElementKeyString); ok { + return x.ElementKeyString + } + } + return "" +} + +func (x *AttributePath_Step) GetElementKeyInt() int64 { + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_ElementKeyInt); ok { + return x.ElementKeyInt + } + } + return 0 +} + +type isAttributePath_Step_Selector interface { + isAttributePath_Step_Selector() +} + +type AttributePath_Step_AttributeName struct { + // Set "attribute_name" to represent looking up an attribute + // in the current object value. + AttributeName string `protobuf:"bytes,1,opt,name=attribute_name,json=attributeName,proto3,oneof"` +} + +type AttributePath_Step_ElementKeyString struct { + // Set "element_key_*" to represent looking up an element in + // an indexable collection type. + ElementKeyString string `protobuf:"bytes,2,opt,name=element_key_string,json=elementKeyString,proto3,oneof"` +} + +type AttributePath_Step_ElementKeyInt struct { + ElementKeyInt int64 `protobuf:"varint,3,opt,name=element_key_int,json=elementKeyInt,proto3,oneof"` +} + +func (*AttributePath_Step_AttributeName) isAttributePath_Step_Selector() {} + +func (*AttributePath_Step_ElementKeyString) isAttributePath_Step_Selector() {} + +func (*AttributePath_Step_ElementKeyInt) isAttributePath_Step_Selector() {} + +// Represents a single caller-facing description of a change, to use for +// presenting information to end users in the UI and for other subsystems +// such as imposing policy rules on the resulting plan. +// +// New description types might be added in future versions of Terraform +// Core, and so clients should tolerate description messages that appear +// to have none of the oneof fields set, and should just ignore those +// messages entirely. +type PlannedChange_ChangeDescription struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Description: + // + // *PlannedChange_ChangeDescription_ComponentInstancePlanned + // *PlannedChange_ChangeDescription_ResourceInstancePlanned + // *PlannedChange_ChangeDescription_OutputValuePlanned + // *PlannedChange_ChangeDescription_PlanApplyable + // *PlannedChange_ChangeDescription_ResourceInstanceDeferred + // *PlannedChange_ChangeDescription_InputVariablePlanned + Description isPlannedChange_ChangeDescription_Description `protobuf_oneof:"description"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ChangeDescription) Reset() { + *x = PlannedChange_ChangeDescription{} + mi := &file_stacks_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ChangeDescription) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ChangeDescription) ProtoMessage() {} + +func (x *PlannedChange_ChangeDescription) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[78] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ChangeDescription.ProtoReflect.Descriptor instead. +func (*PlannedChange_ChangeDescription) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 0} +} + +func (x *PlannedChange_ChangeDescription) GetDescription() isPlannedChange_ChangeDescription_Description { + if x != nil { + return x.Description + } + return nil +} + +func (x *PlannedChange_ChangeDescription) GetComponentInstancePlanned() *PlannedChange_ComponentInstance { + if x != nil { + if x, ok := x.Description.(*PlannedChange_ChangeDescription_ComponentInstancePlanned); ok { + return x.ComponentInstancePlanned + } + } + return nil +} + +func (x *PlannedChange_ChangeDescription) GetResourceInstancePlanned() *PlannedChange_ResourceInstance { + if x != nil { + if x, ok := x.Description.(*PlannedChange_ChangeDescription_ResourceInstancePlanned); ok { + return x.ResourceInstancePlanned + } + } + return nil +} + +func (x *PlannedChange_ChangeDescription) GetOutputValuePlanned() *PlannedChange_OutputValue { + if x != nil { + if x, ok := x.Description.(*PlannedChange_ChangeDescription_OutputValuePlanned); ok { + return x.OutputValuePlanned + } + } + return nil +} + +func (x *PlannedChange_ChangeDescription) GetPlanApplyable() bool { + if x != nil { + if x, ok := x.Description.(*PlannedChange_ChangeDescription_PlanApplyable); ok { + return x.PlanApplyable + } + } + return false +} + +func (x *PlannedChange_ChangeDescription) GetResourceInstanceDeferred() *PlannedChange_ResourceInstanceDeferred { + if x != nil { + if x, ok := x.Description.(*PlannedChange_ChangeDescription_ResourceInstanceDeferred); ok { + return x.ResourceInstanceDeferred + } + } + return nil +} + +func (x *PlannedChange_ChangeDescription) GetInputVariablePlanned() *PlannedChange_InputVariable { + if x != nil { + if x, ok := x.Description.(*PlannedChange_ChangeDescription_InputVariablePlanned); ok { + return x.InputVariablePlanned + } + } + return nil +} + +type isPlannedChange_ChangeDescription_Description interface { + isPlannedChange_ChangeDescription_Description() +} + +type PlannedChange_ChangeDescription_ComponentInstancePlanned struct { + ComponentInstancePlanned *PlannedChange_ComponentInstance `protobuf:"bytes,1,opt,name=component_instance_planned,json=componentInstancePlanned,proto3,oneof"` +} + +type PlannedChange_ChangeDescription_ResourceInstancePlanned struct { + ResourceInstancePlanned *PlannedChange_ResourceInstance `protobuf:"bytes,2,opt,name=resource_instance_planned,json=resourceInstancePlanned,proto3,oneof"` +} + +type PlannedChange_ChangeDescription_OutputValuePlanned struct { + OutputValuePlanned *PlannedChange_OutputValue `protobuf:"bytes,3,opt,name=output_value_planned,json=outputValuePlanned,proto3,oneof"` +} + +type PlannedChange_ChangeDescription_PlanApplyable struct { + PlanApplyable bool `protobuf:"varint,4,opt,name=plan_applyable,json=planApplyable,proto3,oneof"` +} + +type PlannedChange_ChangeDescription_ResourceInstanceDeferred struct { + ResourceInstanceDeferred *PlannedChange_ResourceInstanceDeferred `protobuf:"bytes,5,opt,name=resource_instance_deferred,json=resourceInstanceDeferred,proto3,oneof"` +} + +type PlannedChange_ChangeDescription_InputVariablePlanned struct { + InputVariablePlanned *PlannedChange_InputVariable `protobuf:"bytes,6,opt,name=input_variable_planned,json=inputVariablePlanned,proto3,oneof"` +} + +func (*PlannedChange_ChangeDescription_ComponentInstancePlanned) isPlannedChange_ChangeDescription_Description() { +} + +func (*PlannedChange_ChangeDescription_ResourceInstancePlanned) isPlannedChange_ChangeDescription_Description() { +} + +func (*PlannedChange_ChangeDescription_OutputValuePlanned) isPlannedChange_ChangeDescription_Description() { +} + +func (*PlannedChange_ChangeDescription_PlanApplyable) isPlannedChange_ChangeDescription_Description() { +} + +func (*PlannedChange_ChangeDescription_ResourceInstanceDeferred) isPlannedChange_ChangeDescription_Description() { +} + +func (*PlannedChange_ChangeDescription_InputVariablePlanned) isPlannedChange_ChangeDescription_Description() { +} + +// Reports the existence of a particular instance of a component, +// once Terraform has resolved arguments such as "for_each" that +// might make the set of instances dynamic. +type PlannedChange_ComponentInstance struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ComponentInstanceInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + // The changes to the existence of this instance relative to the + // prior state. This only considers the component instance directly, + // and doesn't take into account what actions are planned for any + // resource instances inside. + Actions []ChangeType `protobuf:"varint,2,rep,packed,name=actions,proto3,enum=terraform1.stacks.ChangeType" json:"actions,omitempty"` + // A flag for whether applying this plan is expected to cause the + // desired state and actual state to become converged. + // + // If this field is false, that means Terraform expects that at least + // one more plan/apply round will be needed to reach convergence. + // + // If this field is true then Terraform hopes to be able to converge + // after this plan is applied, but callers should ideally still check + // anyway by running one more plan to confirm that there aren't any + // unexpected differences caused by such situations as contradictory + // configuration or provider bugs. + PlanComplete bool `protobuf:"varint,3,opt,name=plan_complete,json=planComplete,proto3" json:"plan_complete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ComponentInstance) Reset() { + *x = PlannedChange_ComponentInstance{} + mi := &file_stacks_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ComponentInstance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ComponentInstance) ProtoMessage() {} + +func (x *PlannedChange_ComponentInstance) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[79] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ComponentInstance.ProtoReflect.Descriptor instead. +func (*PlannedChange_ComponentInstance) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 1} +} + +func (x *PlannedChange_ComponentInstance) GetAddr() *ComponentInstanceInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *PlannedChange_ComponentInstance) GetActions() []ChangeType { + if x != nil { + return x.Actions + } + return nil +} + +func (x *PlannedChange_ComponentInstance) GetPlanComplete() bool { + if x != nil { + return x.PlanComplete + } + return false +} + +type PlannedChange_ResourceInstance struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ResourceInstanceObjectInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + Actions []ChangeType `protobuf:"varint,2,rep,packed,name=actions,proto3,enum=terraform1.stacks.ChangeType" json:"actions,omitempty"` + Values *DynamicValueChange `protobuf:"bytes,3,opt,name=values,proto3" json:"values,omitempty"` + Moved *PlannedChange_ResourceInstance_Moved `protobuf:"bytes,4,opt,name=moved,proto3" json:"moved,omitempty"` + Imported *PlannedChange_ResourceInstance_Imported `protobuf:"bytes,5,opt,name=imported,proto3" json:"imported,omitempty"` + ResourceMode ResourceMode `protobuf:"varint,6,opt,name=resource_mode,json=resourceMode,proto3,enum=terraform1.stacks.ResourceMode" json:"resource_mode,omitempty"` + ResourceType string `protobuf:"bytes,7,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + ProviderAddr string `protobuf:"bytes,8,opt,name=provider_addr,json=providerAddr,proto3" json:"provider_addr,omitempty"` + // previous_run_value is included only if it would be + // different from values.old, which typically means that + // Terraform detected some changes made outside of Terraform + // since the previous run. In that case, this field is + // the un-refreshed (but still upgraded) value from + // the previous run and values.old is the refreshed version. + // + // If this isn't set then values.old should be used as the + // previous run value, if needed. + PreviousRunValue *DynamicValue `protobuf:"bytes,9,opt,name=previous_run_value,json=previousRunValue,proto3" json:"previous_run_value,omitempty"` + // This flag is set if Terraform Core considers the difference + // between previous_run_value and values.old to be "notable", + // which is a heuristic subject to change over time but is + // broadly intended to mean that it would be worth mentioning + // the difference between the two in the UI as a + // "change outside of Terraform". If this isn't set then the + // difference is probably not worth mentioning to the user + // by default, although it could still be shown behind an + // optional disclosure in UI contexts where such things are possible. + NotableChangeOutside bool `protobuf:"varint,10,opt,name=notable_change_outside,json=notableChangeOutside,proto3" json:"notable_change_outside,omitempty"` + ReplacePaths []*AttributePath `protobuf:"bytes,11,rep,name=replace_paths,json=replacePaths,proto3" json:"replace_paths,omitempty"` + ResourceName string `protobuf:"bytes,12,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"` + Index *PlannedChange_ResourceInstance_Index `protobuf:"bytes,13,opt,name=index,proto3" json:"index,omitempty"` + ModuleAddr string `protobuf:"bytes,14,opt,name=module_addr,json=moduleAddr,proto3" json:"module_addr,omitempty"` + ActionReason string `protobuf:"bytes,15,opt,name=action_reason,json=actionReason,proto3" json:"action_reason,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ResourceInstance) Reset() { + *x = PlannedChange_ResourceInstance{} + mi := &file_stacks_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ResourceInstance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ResourceInstance) ProtoMessage() {} + +func (x *PlannedChange_ResourceInstance) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[80] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ResourceInstance.ProtoReflect.Descriptor instead. +func (*PlannedChange_ResourceInstance) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 2} +} + +func (x *PlannedChange_ResourceInstance) GetAddr() *ResourceInstanceObjectInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetActions() []ChangeType { + if x != nil { + return x.Actions + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetValues() *DynamicValueChange { + if x != nil { + return x.Values + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetMoved() *PlannedChange_ResourceInstance_Moved { + if x != nil { + return x.Moved + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetImported() *PlannedChange_ResourceInstance_Imported { + if x != nil { + return x.Imported + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetResourceMode() ResourceMode { + if x != nil { + return x.ResourceMode + } + return ResourceMode_UNKNOWN +} + +func (x *PlannedChange_ResourceInstance) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *PlannedChange_ResourceInstance) GetProviderAddr() string { + if x != nil { + return x.ProviderAddr + } + return "" +} + +func (x *PlannedChange_ResourceInstance) GetPreviousRunValue() *DynamicValue { + if x != nil { + return x.PreviousRunValue + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetNotableChangeOutside() bool { + if x != nil { + return x.NotableChangeOutside + } + return false +} + +func (x *PlannedChange_ResourceInstance) GetReplacePaths() []*AttributePath { + if x != nil { + return x.ReplacePaths + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetResourceName() string { + if x != nil { + return x.ResourceName + } + return "" +} + +func (x *PlannedChange_ResourceInstance) GetIndex() *PlannedChange_ResourceInstance_Index { + if x != nil { + return x.Index + } + return nil +} + +func (x *PlannedChange_ResourceInstance) GetModuleAddr() string { + if x != nil { + return x.ModuleAddr + } + return "" +} + +func (x *PlannedChange_ResourceInstance) GetActionReason() string { + if x != nil { + return x.ActionReason + } + return "" +} + +// Note: this is only for output values from the topmost +// stack configuration, because all other output values are +// internal to the configuration and not part of its public API. +type PlannedChange_OutputValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Actions []ChangeType `protobuf:"varint,2,rep,packed,name=actions,proto3,enum=terraform1.stacks.ChangeType" json:"actions,omitempty"` + Values *DynamicValueChange `protobuf:"bytes,3,opt,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_OutputValue) Reset() { + *x = PlannedChange_OutputValue{} + mi := &file_stacks_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_OutputValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_OutputValue) ProtoMessage() {} + +func (x *PlannedChange_OutputValue) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[81] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_OutputValue.ProtoReflect.Descriptor instead. +func (*PlannedChange_OutputValue) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 3} +} + +func (x *PlannedChange_OutputValue) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PlannedChange_OutputValue) GetActions() []ChangeType { + if x != nil { + return x.Actions + } + return nil +} + +func (x *PlannedChange_OutputValue) GetValues() *DynamicValueChange { + if x != nil { + return x.Values + } + return nil +} + +type PlannedChange_ResourceInstanceDeferred struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResourceInstance *PlannedChange_ResourceInstance `protobuf:"bytes,1,opt,name=resource_instance,json=resourceInstance,proto3" json:"resource_instance,omitempty"` + Deferred *Deferred `protobuf:"bytes,2,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ResourceInstanceDeferred) Reset() { + *x = PlannedChange_ResourceInstanceDeferred{} + mi := &file_stacks_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ResourceInstanceDeferred) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ResourceInstanceDeferred) ProtoMessage() {} + +func (x *PlannedChange_ResourceInstanceDeferred) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[82] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ResourceInstanceDeferred.ProtoReflect.Descriptor instead. +func (*PlannedChange_ResourceInstanceDeferred) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 4} +} + +func (x *PlannedChange_ResourceInstanceDeferred) GetResourceInstance() *PlannedChange_ResourceInstance { + if x != nil { + return x.ResourceInstance + } + return nil +} + +func (x *PlannedChange_ResourceInstanceDeferred) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +// Note: this is only for input variables from the topmost +// stack configuration, because all other input variables are +// internal to the configuration and not part of its public API. +type PlannedChange_InputVariable struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Actions []ChangeType `protobuf:"varint,2,rep,packed,name=actions,proto3,enum=terraform1.stacks.ChangeType" json:"actions,omitempty"` + Values *DynamicValueChange `protobuf:"bytes,3,opt,name=values,proto3" json:"values,omitempty"` + RequiredDuringApply bool `protobuf:"varint,4,opt,name=required_during_apply,json=requiredDuringApply,proto3" json:"required_during_apply,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_InputVariable) Reset() { + *x = PlannedChange_InputVariable{} + mi := &file_stacks_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_InputVariable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_InputVariable) ProtoMessage() {} + +func (x *PlannedChange_InputVariable) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[83] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_InputVariable.ProtoReflect.Descriptor instead. +func (*PlannedChange_InputVariable) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 5} +} + +func (x *PlannedChange_InputVariable) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PlannedChange_InputVariable) GetActions() []ChangeType { + if x != nil { + return x.Actions + } + return nil +} + +func (x *PlannedChange_InputVariable) GetValues() *DynamicValueChange { + if x != nil { + return x.Values + } + return nil +} + +func (x *PlannedChange_InputVariable) GetRequiredDuringApply() bool { + if x != nil { + return x.RequiredDuringApply + } + return false +} + +type PlannedChange_ResourceInstance_Index struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value *DynamicValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + Unknown bool `protobuf:"varint,2,opt,name=unknown,proto3" json:"unknown,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ResourceInstance_Index) Reset() { + *x = PlannedChange_ResourceInstance_Index{} + mi := &file_stacks_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ResourceInstance_Index) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ResourceInstance_Index) ProtoMessage() {} + +func (x *PlannedChange_ResourceInstance_Index) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[84] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ResourceInstance_Index.ProtoReflect.Descriptor instead. +func (*PlannedChange_ResourceInstance_Index) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 2, 0} +} + +func (x *PlannedChange_ResourceInstance_Index) GetValue() *DynamicValue { + if x != nil { + return x.Value + } + return nil +} + +func (x *PlannedChange_ResourceInstance_Index) GetUnknown() bool { + if x != nil { + return x.Unknown + } + return false +} + +type PlannedChange_ResourceInstance_Moved struct { + state protoimpl.MessageState `protogen:"open.v1"` + PrevAddr *ResourceInstanceInStackAddr `protobuf:"bytes,1,opt,name=prev_addr,json=prevAddr,proto3" json:"prev_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ResourceInstance_Moved) Reset() { + *x = PlannedChange_ResourceInstance_Moved{} + mi := &file_stacks_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ResourceInstance_Moved) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ResourceInstance_Moved) ProtoMessage() {} + +func (x *PlannedChange_ResourceInstance_Moved) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[85] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ResourceInstance_Moved.ProtoReflect.Descriptor instead. +func (*PlannedChange_ResourceInstance_Moved) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 2, 1} +} + +func (x *PlannedChange_ResourceInstance_Moved) GetPrevAddr() *ResourceInstanceInStackAddr { + if x != nil { + return x.PrevAddr + } + return nil +} + +type PlannedChange_ResourceInstance_Imported struct { + state protoimpl.MessageState `protogen:"open.v1"` + ImportId string `protobuf:"bytes,1,opt,name=import_id,json=importId,proto3" json:"import_id,omitempty"` + Unknown bool `protobuf:"varint,2,opt,name=unknown,proto3" json:"unknown,omitempty"` + GeneratedConfig string `protobuf:"bytes,3,opt,name=generated_config,json=generatedConfig,proto3" json:"generated_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlannedChange_ResourceInstance_Imported) Reset() { + *x = PlannedChange_ResourceInstance_Imported{} + mi := &file_stacks_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlannedChange_ResourceInstance_Imported) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlannedChange_ResourceInstance_Imported) ProtoMessage() {} + +func (x *PlannedChange_ResourceInstance_Imported) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[86] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlannedChange_ResourceInstance_Imported.ProtoReflect.Descriptor instead. +func (*PlannedChange_ResourceInstance_Imported) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{22, 2, 2} +} + +func (x *PlannedChange_ResourceInstance_Imported) GetImportId() string { + if x != nil { + return x.ImportId + } + return "" +} + +func (x *PlannedChange_ResourceInstance_Imported) GetUnknown() bool { + if x != nil { + return x.Unknown + } + return false +} + +func (x *PlannedChange_ResourceInstance_Imported) GetGeneratedConfig() string { + if x != nil { + return x.GeneratedConfig + } + return "" +} + +type AppliedChange_RawChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Value *anypb.Any `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_RawChange) Reset() { + *x = AppliedChange_RawChange{} + mi := &file_stacks_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_RawChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_RawChange) ProtoMessage() {} + +func (x *AppliedChange_RawChange) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[87] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_RawChange.ProtoReflect.Descriptor instead. +func (*AppliedChange_RawChange) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 0} +} + +func (x *AppliedChange_RawChange) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AppliedChange_RawChange) GetValue() *anypb.Any { + if x != nil { + return x.Value + } + return nil +} + +type AppliedChange_ChangeDescription struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + // Types that are valid to be assigned to Description: + // + // *AppliedChange_ChangeDescription_Deleted + // *AppliedChange_ChangeDescription_Moved + // *AppliedChange_ChangeDescription_ResourceInstance + // *AppliedChange_ChangeDescription_OutputValue + // *AppliedChange_ChangeDescription_InputVariable + // *AppliedChange_ChangeDescription_ComponentInstance + Description isAppliedChange_ChangeDescription_Description `protobuf_oneof:"description"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_ChangeDescription) Reset() { + *x = AppliedChange_ChangeDescription{} + mi := &file_stacks_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_ChangeDescription) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_ChangeDescription) ProtoMessage() {} + +func (x *AppliedChange_ChangeDescription) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[88] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_ChangeDescription.ProtoReflect.Descriptor instead. +func (*AppliedChange_ChangeDescription) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 1} +} + +func (x *AppliedChange_ChangeDescription) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AppliedChange_ChangeDescription) GetDescription() isAppliedChange_ChangeDescription_Description { + if x != nil { + return x.Description + } + return nil +} + +func (x *AppliedChange_ChangeDescription) GetDeleted() *AppliedChange_Nothing { + if x != nil { + if x, ok := x.Description.(*AppliedChange_ChangeDescription_Deleted); ok { + return x.Deleted + } + } + return nil +} + +func (x *AppliedChange_ChangeDescription) GetMoved() *AppliedChange_Nothing { + if x != nil { + if x, ok := x.Description.(*AppliedChange_ChangeDescription_Moved); ok { + return x.Moved + } + } + return nil +} + +func (x *AppliedChange_ChangeDescription) GetResourceInstance() *AppliedChange_ResourceInstance { + if x != nil { + if x, ok := x.Description.(*AppliedChange_ChangeDescription_ResourceInstance); ok { + return x.ResourceInstance + } + } + return nil +} + +func (x *AppliedChange_ChangeDescription) GetOutputValue() *AppliedChange_OutputValue { + if x != nil { + if x, ok := x.Description.(*AppliedChange_ChangeDescription_OutputValue); ok { + return x.OutputValue + } + } + return nil +} + +func (x *AppliedChange_ChangeDescription) GetInputVariable() *AppliedChange_InputVariable { + if x != nil { + if x, ok := x.Description.(*AppliedChange_ChangeDescription_InputVariable); ok { + return x.InputVariable + } + } + return nil +} + +func (x *AppliedChange_ChangeDescription) GetComponentInstance() *AppliedChange_ComponentInstance { + if x != nil { + if x, ok := x.Description.(*AppliedChange_ChangeDescription_ComponentInstance); ok { + return x.ComponentInstance + } + } + return nil +} + +type isAppliedChange_ChangeDescription_Description interface { + isAppliedChange_ChangeDescription_Description() +} + +type AppliedChange_ChangeDescription_Deleted struct { + Deleted *AppliedChange_Nothing `protobuf:"bytes,4,opt,name=deleted,proto3,oneof"` // explicitly represents the absence of a description +} + +type AppliedChange_ChangeDescription_Moved struct { + Moved *AppliedChange_Nothing `protobuf:"bytes,6,opt,name=moved,proto3,oneof"` // explicitly represents the absence of a description +} + +type AppliedChange_ChangeDescription_ResourceInstance struct { + ResourceInstance *AppliedChange_ResourceInstance `protobuf:"bytes,2,opt,name=resource_instance,json=resourceInstance,proto3,oneof"` +} + +type AppliedChange_ChangeDescription_OutputValue struct { + OutputValue *AppliedChange_OutputValue `protobuf:"bytes,3,opt,name=output_value,json=outputValue,proto3,oneof"` +} + +type AppliedChange_ChangeDescription_InputVariable struct { + InputVariable *AppliedChange_InputVariable `protobuf:"bytes,7,opt,name=input_variable,json=inputVariable,proto3,oneof"` +} + +type AppliedChange_ChangeDescription_ComponentInstance struct { + ComponentInstance *AppliedChange_ComponentInstance `protobuf:"bytes,5,opt,name=component_instance,json=componentInstance,proto3,oneof"` +} + +func (*AppliedChange_ChangeDescription_Deleted) isAppliedChange_ChangeDescription_Description() {} + +func (*AppliedChange_ChangeDescription_Moved) isAppliedChange_ChangeDescription_Description() {} + +func (*AppliedChange_ChangeDescription_ResourceInstance) isAppliedChange_ChangeDescription_Description() { +} + +func (*AppliedChange_ChangeDescription_OutputValue) isAppliedChange_ChangeDescription_Description() {} + +func (*AppliedChange_ChangeDescription_InputVariable) isAppliedChange_ChangeDescription_Description() { +} + +func (*AppliedChange_ChangeDescription_ComponentInstance) isAppliedChange_ChangeDescription_Description() { +} + +type AppliedChange_ResourceInstance struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ResourceInstanceObjectInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + NewValue *DynamicValue `protobuf:"bytes,2,opt,name=new_value,json=newValue,proto3" json:"new_value,omitempty"` + ResourceMode ResourceMode `protobuf:"varint,4,opt,name=resource_mode,json=resourceMode,proto3,enum=terraform1.stacks.ResourceMode" json:"resource_mode,omitempty"` + ResourceType string `protobuf:"bytes,5,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + ProviderAddr string `protobuf:"bytes,6,opt,name=provider_addr,json=providerAddr,proto3" json:"provider_addr,omitempty"` + // Sometimes Terraform needs to make changes to a resource in + // multiple steps during the apply phase, with each step + // changing something about the state. This flag will be set + // for such interim updates, and left unset for whatever + // description Terraform Core considers to be "final", at + // which point the new value should be converged with the + // desired state. + // + // The intended use for this is when presenting updated values + // to users in the UI, where it might be best to ignore or + // present differently interim updates to avoid creating + // confusion by showing the not-yet-converged intermediate + // states. + // + // If Terraform encounters a problem during the apply phase + // and needs to stop partway through then a "final" change + // description might never arrive. In that case, callers + // should save the most recent interim object as the final + // description, since it would represent the most accurate + // description of the state the remote system has been left + // in. + Interim bool `protobuf:"varint,3,opt,name=interim,proto3" json:"interim,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_ResourceInstance) Reset() { + *x = AppliedChange_ResourceInstance{} + mi := &file_stacks_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_ResourceInstance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_ResourceInstance) ProtoMessage() {} + +func (x *AppliedChange_ResourceInstance) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[89] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_ResourceInstance.ProtoReflect.Descriptor instead. +func (*AppliedChange_ResourceInstance) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 2} +} + +func (x *AppliedChange_ResourceInstance) GetAddr() *ResourceInstanceObjectInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *AppliedChange_ResourceInstance) GetNewValue() *DynamicValue { + if x != nil { + return x.NewValue + } + return nil +} + +func (x *AppliedChange_ResourceInstance) GetResourceMode() ResourceMode { + if x != nil { + return x.ResourceMode + } + return ResourceMode_UNKNOWN +} + +func (x *AppliedChange_ResourceInstance) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *AppliedChange_ResourceInstance) GetProviderAddr() string { + if x != nil { + return x.ProviderAddr + } + return "" +} + +func (x *AppliedChange_ResourceInstance) GetInterim() bool { + if x != nil { + return x.Interim + } + return false +} + +type AppliedChange_ComponentInstance struct { + state protoimpl.MessageState `protogen:"open.v1"` + ComponentAddr string `protobuf:"bytes,3,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + ComponentInstanceAddr string `protobuf:"bytes,1,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + OutputValues map[string]*DynamicValue `protobuf:"bytes,2,rep,name=output_values,json=outputValues,proto3" json:"output_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_ComponentInstance) Reset() { + *x = AppliedChange_ComponentInstance{} + mi := &file_stacks_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_ComponentInstance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_ComponentInstance) ProtoMessage() {} + +func (x *AppliedChange_ComponentInstance) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[90] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_ComponentInstance.ProtoReflect.Descriptor instead. +func (*AppliedChange_ComponentInstance) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 3} +} + +func (x *AppliedChange_ComponentInstance) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +func (x *AppliedChange_ComponentInstance) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +func (x *AppliedChange_ComponentInstance) GetOutputValues() map[string]*DynamicValue { + if x != nil { + return x.OutputValues + } + return nil +} + +type AppliedChange_OutputValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + NewValue *DynamicValue `protobuf:"bytes,2,opt,name=new_value,json=newValue,proto3" json:"new_value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_OutputValue) Reset() { + *x = AppliedChange_OutputValue{} + mi := &file_stacks_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_OutputValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_OutputValue) ProtoMessage() {} + +func (x *AppliedChange_OutputValue) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[91] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_OutputValue.ProtoReflect.Descriptor instead. +func (*AppliedChange_OutputValue) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 4} +} + +func (x *AppliedChange_OutputValue) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AppliedChange_OutputValue) GetNewValue() *DynamicValue { + if x != nil { + return x.NewValue + } + return nil +} + +type AppliedChange_InputVariable struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + NewValue *DynamicValue `protobuf:"bytes,2,opt,name=new_value,json=newValue,proto3" json:"new_value,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_InputVariable) Reset() { + *x = AppliedChange_InputVariable{} + mi := &file_stacks_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_InputVariable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_InputVariable) ProtoMessage() {} + +func (x *AppliedChange_InputVariable) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[92] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_InputVariable.ProtoReflect.Descriptor instead. +func (*AppliedChange_InputVariable) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 5} +} + +func (x *AppliedChange_InputVariable) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AppliedChange_InputVariable) GetNewValue() *DynamicValue { + if x != nil { + return x.NewValue + } + return nil +} + +type AppliedChange_Nothing struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AppliedChange_Nothing) Reset() { + *x = AppliedChange_Nothing{} + mi := &file_stacks_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AppliedChange_Nothing) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AppliedChange_Nothing) ProtoMessage() {} + +func (x *AppliedChange_Nothing) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[93] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AppliedChange_Nothing.ProtoReflect.Descriptor instead. +func (*AppliedChange_Nothing) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{24, 6} +} + +// ComponentInstanceStatus describes the current status of a component instance +// undergoing a plan or apply operation. +type StackChangeProgress_ComponentInstanceStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ComponentInstanceInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + Status StackChangeProgress_ComponentInstanceStatus_Status `protobuf:"varint,2,opt,name=status,proto3,enum=terraform1.stacks.StackChangeProgress_ComponentInstanceStatus_Status" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ComponentInstanceStatus) Reset() { + *x = StackChangeProgress_ComponentInstanceStatus{} + mi := &file_stacks_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ComponentInstanceStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ComponentInstanceStatus) ProtoMessage() {} + +func (x *StackChangeProgress_ComponentInstanceStatus) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[95] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ComponentInstanceStatus.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ComponentInstanceStatus) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 0} +} + +func (x *StackChangeProgress_ComponentInstanceStatus) GetAddr() *ComponentInstanceInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *StackChangeProgress_ComponentInstanceStatus) GetStatus() StackChangeProgress_ComponentInstanceStatus_Status { + if x != nil { + return x.Status + } + return StackChangeProgress_ComponentInstanceStatus_INVALID +} + +// ComponentInstanceStatus describes the current status of a resource instance +// undergoing a plan or apply operation. +type StackChangeProgress_ResourceInstanceStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ResourceInstanceObjectInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + Status StackChangeProgress_ResourceInstanceStatus_Status `protobuf:"varint,2,opt,name=status,proto3,enum=terraform1.stacks.StackChangeProgress_ResourceInstanceStatus_Status" json:"status,omitempty"` + ProviderAddr string `protobuf:"bytes,3,opt,name=provider_addr,json=providerAddr,proto3" json:"provider_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ResourceInstanceStatus) Reset() { + *x = StackChangeProgress_ResourceInstanceStatus{} + mi := &file_stacks_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ResourceInstanceStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ResourceInstanceStatus) ProtoMessage() {} + +func (x *StackChangeProgress_ResourceInstanceStatus) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[96] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ResourceInstanceStatus.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ResourceInstanceStatus) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 1} +} + +func (x *StackChangeProgress_ResourceInstanceStatus) GetAddr() *ResourceInstanceObjectInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *StackChangeProgress_ResourceInstanceStatus) GetStatus() StackChangeProgress_ResourceInstanceStatus_Status { + if x != nil { + return x.Status + } + return StackChangeProgress_ResourceInstanceStatus_INVALID +} + +func (x *StackChangeProgress_ResourceInstanceStatus) GetProviderAddr() string { + if x != nil { + return x.ProviderAddr + } + return "" +} + +// ResourceInstancePlannedChange describes summary information about a planned +// change for a resource instance. This does not include the full object change, +// which is described in PlannedChange.ResourceChange. The information in this +// message is intended for the event stream and need not include the instance's +// full object values. +type StackChangeProgress_ResourceInstancePlannedChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ResourceInstanceObjectInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + Actions []ChangeType `protobuf:"varint,2,rep,packed,name=actions,proto3,enum=terraform1.stacks.ChangeType" json:"actions,omitempty"` + Moved *StackChangeProgress_ResourceInstancePlannedChange_Moved `protobuf:"bytes,3,opt,name=moved,proto3" json:"moved,omitempty"` + Imported *StackChangeProgress_ResourceInstancePlannedChange_Imported `protobuf:"bytes,4,opt,name=imported,proto3" json:"imported,omitempty"` + ProviderAddr string `protobuf:"bytes,5,opt,name=provider_addr,json=providerAddr,proto3" json:"provider_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) Reset() { + *x = StackChangeProgress_ResourceInstancePlannedChange{} + mi := &file_stacks_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ResourceInstancePlannedChange) ProtoMessage() {} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[97] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ResourceInstancePlannedChange.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ResourceInstancePlannedChange) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 2} +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) GetAddr() *ResourceInstanceObjectInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) GetActions() []ChangeType { + if x != nil { + return x.Actions + } + return nil +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) GetMoved() *StackChangeProgress_ResourceInstancePlannedChange_Moved { + if x != nil { + return x.Moved + } + return nil +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) GetImported() *StackChangeProgress_ResourceInstancePlannedChange_Imported { + if x != nil { + return x.Imported + } + return nil +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange) GetProviderAddr() string { + if x != nil { + return x.ProviderAddr + } + return "" +} + +// DeferredResourceInstancePlannedChange represents a planned change for a +// resource instance that is deferred due to the reason provided. +type StackChangeProgress_DeferredResourceInstancePlannedChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deferred *Deferred `protobuf:"bytes,1,opt,name=deferred,proto3" json:"deferred,omitempty"` + Change *StackChangeProgress_ResourceInstancePlannedChange `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_DeferredResourceInstancePlannedChange) Reset() { + *x = StackChangeProgress_DeferredResourceInstancePlannedChange{} + mi := &file_stacks_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_DeferredResourceInstancePlannedChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_DeferredResourceInstancePlannedChange) ProtoMessage() {} + +func (x *StackChangeProgress_DeferredResourceInstancePlannedChange) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[98] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_DeferredResourceInstancePlannedChange.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_DeferredResourceInstancePlannedChange) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 3} +} + +func (x *StackChangeProgress_DeferredResourceInstancePlannedChange) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +func (x *StackChangeProgress_DeferredResourceInstancePlannedChange) GetChange() *StackChangeProgress_ResourceInstancePlannedChange { + if x != nil { + return x.Change + } + return nil +} + +// ProvisionerStatus represents the progress of a given provisioner during its +// resource instance's apply operation. +type StackChangeProgress_ProvisionerStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ResourceInstanceObjectInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Status *StackChangeProgress_ProvisionerStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ProvisionerStatus) Reset() { + *x = StackChangeProgress_ProvisionerStatus{} + mi := &file_stacks_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ProvisionerStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ProvisionerStatus) ProtoMessage() {} + +func (x *StackChangeProgress_ProvisionerStatus) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[99] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ProvisionerStatus.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ProvisionerStatus) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 4} +} + +func (x *StackChangeProgress_ProvisionerStatus) GetAddr() *ResourceInstanceObjectInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *StackChangeProgress_ProvisionerStatus) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StackChangeProgress_ProvisionerStatus) GetStatus() *StackChangeProgress_ProvisionerStatus { + if x != nil { + return x.Status + } + return nil +} + +// ProvisionerOutput represents recorded output data emitted by a provisioner +// during a resource instance's apply operation. +type StackChangeProgress_ProvisionerOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ResourceInstanceObjectInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Output string `protobuf:"bytes,3,opt,name=output,proto3" json:"output,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ProvisionerOutput) Reset() { + *x = StackChangeProgress_ProvisionerOutput{} + mi := &file_stacks_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ProvisionerOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ProvisionerOutput) ProtoMessage() {} + +func (x *StackChangeProgress_ProvisionerOutput) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[100] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ProvisionerOutput.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ProvisionerOutput) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 5} +} + +func (x *StackChangeProgress_ProvisionerOutput) GetAddr() *ResourceInstanceObjectInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *StackChangeProgress_ProvisionerOutput) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *StackChangeProgress_ProvisionerOutput) GetOutput() string { + if x != nil { + return x.Output + } + return "" +} + +// ComponentInstanceChanges represents a roll-up of change counts for a +// component instance plan or apply operation. +type StackChangeProgress_ComponentInstanceChanges struct { + state protoimpl.MessageState `protogen:"open.v1"` + Addr *ComponentInstanceInStackAddr `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` + // total is the sum of all of the other count fields. + // + // Clients should sum all of the other count fields they know about + // and compare to total. If the sum is less than total then the + // difference should be treated as an "other change types" category, + // for forward-compatibility when the Terraform Core RPC server is + // using a newer version of this protocol than the client. + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + Add int32 `protobuf:"varint,3,opt,name=add,proto3" json:"add,omitempty"` + Change int32 `protobuf:"varint,4,opt,name=change,proto3" json:"change,omitempty"` + Import int32 `protobuf:"varint,5,opt,name=import,proto3" json:"import,omitempty"` + Remove int32 `protobuf:"varint,6,opt,name=remove,proto3" json:"remove,omitempty"` + Defer int32 `protobuf:"varint,7,opt,name=defer,proto3" json:"defer,omitempty"` + Move int32 `protobuf:"varint,8,opt,name=move,proto3" json:"move,omitempty"` + Forget int32 `protobuf:"varint,9,opt,name=forget,proto3" json:"forget,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ComponentInstanceChanges) Reset() { + *x = StackChangeProgress_ComponentInstanceChanges{} + mi := &file_stacks_proto_msgTypes[101] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ComponentInstanceChanges) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ComponentInstanceChanges) ProtoMessage() {} + +func (x *StackChangeProgress_ComponentInstanceChanges) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[101] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ComponentInstanceChanges.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ComponentInstanceChanges) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 6} +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetAddr() *ComponentInstanceInStackAddr { + if x != nil { + return x.Addr + } + return nil +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetAdd() int32 { + if x != nil { + return x.Add + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetChange() int32 { + if x != nil { + return x.Change + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetImport() int32 { + if x != nil { + return x.Import + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetRemove() int32 { + if x != nil { + return x.Remove + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetDefer() int32 { + if x != nil { + return x.Defer + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetMove() int32 { + if x != nil { + return x.Move + } + return 0 +} + +func (x *StackChangeProgress_ComponentInstanceChanges) GetForget() int32 { + if x != nil { + return x.Forget + } + return 0 +} + +// ComponentInstances represents the result of expanding a component into zero +// or more instances. +type StackChangeProgress_ComponentInstances struct { + state protoimpl.MessageState `protogen:"open.v1"` + ComponentAddr string `protobuf:"bytes,1,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + InstanceAddrs []string `protobuf:"bytes,2,rep,name=instance_addrs,json=instanceAddrs,proto3" json:"instance_addrs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ComponentInstances) Reset() { + *x = StackChangeProgress_ComponentInstances{} + mi := &file_stacks_proto_msgTypes[102] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ComponentInstances) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ComponentInstances) ProtoMessage() {} + +func (x *StackChangeProgress_ComponentInstances) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[102] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ComponentInstances.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ComponentInstances) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 7} +} + +func (x *StackChangeProgress_ComponentInstances) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +func (x *StackChangeProgress_ComponentInstances) GetInstanceAddrs() []string { + if x != nil { + return x.InstanceAddrs + } + return nil +} + +type StackChangeProgress_ResourceInstancePlannedChange_Moved struct { + state protoimpl.MessageState `protogen:"open.v1"` + PrevAddr *ResourceInstanceInStackAddr `protobuf:"bytes,1,opt,name=prev_addr,json=prevAddr,proto3" json:"prev_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Moved) Reset() { + *x = StackChangeProgress_ResourceInstancePlannedChange_Moved{} + mi := &file_stacks_proto_msgTypes[103] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Moved) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ResourceInstancePlannedChange_Moved) ProtoMessage() {} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Moved) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[103] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ResourceInstancePlannedChange_Moved.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ResourceInstancePlannedChange_Moved) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 2, 0} +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Moved) GetPrevAddr() *ResourceInstanceInStackAddr { + if x != nil { + return x.PrevAddr + } + return nil +} + +type StackChangeProgress_ResourceInstancePlannedChange_Imported struct { + state protoimpl.MessageState `protogen:"open.v1"` + ImportId string `protobuf:"bytes,1,opt,name=import_id,json=importId,proto3" json:"import_id,omitempty"` + Unknown bool `protobuf:"varint,2,opt,name=unknown,proto3" json:"unknown,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Imported) Reset() { + *x = StackChangeProgress_ResourceInstancePlannedChange_Imported{} + mi := &file_stacks_proto_msgTypes[104] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Imported) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StackChangeProgress_ResourceInstancePlannedChange_Imported) ProtoMessage() {} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Imported) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[104] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StackChangeProgress_ResourceInstancePlannedChange_Imported.ProtoReflect.Descriptor instead. +func (*StackChangeProgress_ResourceInstancePlannedChange_Imported) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{25, 2, 1} +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Imported) GetImportId() string { + if x != nil { + return x.ImportId + } + return "" +} + +func (x *StackChangeProgress_ResourceInstancePlannedChange_Imported) GetUnknown() bool { + if x != nil { + return x.Unknown + } + return false +} + +type ListResourceIdentities_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + StateHandle int64 `protobuf:"varint,1,opt,name=state_handle,json=stateHandle,proto3" json:"state_handle,omitempty"` + DependencyLocksHandle int64 `protobuf:"varint,2,opt,name=dependency_locks_handle,json=dependencyLocksHandle,proto3" json:"dependency_locks_handle,omitempty"` + ProviderCacheHandle int64 `protobuf:"varint,3,opt,name=provider_cache_handle,json=providerCacheHandle,proto3" json:"provider_cache_handle,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListResourceIdentities_Request) Reset() { + *x = ListResourceIdentities_Request{} + mi := &file_stacks_proto_msgTypes[105] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListResourceIdentities_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListResourceIdentities_Request) ProtoMessage() {} + +func (x *ListResourceIdentities_Request) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[105] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListResourceIdentities_Request.ProtoReflect.Descriptor instead. +func (*ListResourceIdentities_Request) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{26, 0} +} + +func (x *ListResourceIdentities_Request) GetStateHandle() int64 { + if x != nil { + return x.StateHandle + } + return 0 +} + +func (x *ListResourceIdentities_Request) GetDependencyLocksHandle() int64 { + if x != nil { + return x.DependencyLocksHandle + } + return 0 +} + +func (x *ListResourceIdentities_Request) GetProviderCacheHandle() int64 { + if x != nil { + return x.ProviderCacheHandle + } + return 0 +} + +type ListResourceIdentities_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Resource []*ListResourceIdentities_Resource `protobuf:"bytes,1,rep,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListResourceIdentities_Response) Reset() { + *x = ListResourceIdentities_Response{} + mi := &file_stacks_proto_msgTypes[106] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListResourceIdentities_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListResourceIdentities_Response) ProtoMessage() {} + +func (x *ListResourceIdentities_Response) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[106] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListResourceIdentities_Response.ProtoReflect.Descriptor instead. +func (*ListResourceIdentities_Response) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{26, 1} +} + +func (x *ListResourceIdentities_Response) GetResource() []*ListResourceIdentities_Resource { + if x != nil { + return x.Resource + } + return nil +} + +type ListResourceIdentities_Resource struct { + state protoimpl.MessageState `protogen:"open.v1"` + ComponentAddr string `protobuf:"bytes,1,opt,name=component_addr,json=componentAddr,proto3" json:"component_addr,omitempty"` + ComponentInstanceAddr string `protobuf:"bytes,2,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + // Unique address of the resource instance within the given component + // instance. Each component instance has a separate namespace of + // resource instance addresses, so callers must take both fields together + // to produce a key that's unique throughout the entire plan. + ResourceInstanceAddr string `protobuf:"bytes,3,opt,name=resource_instance_addr,json=resourceInstanceAddr,proto3" json:"resource_instance_addr,omitempty"` + ResourceIdentity *DynamicValue `protobuf:"bytes,4,opt,name=resource_identity,json=resourceIdentity,proto3" json:"resource_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListResourceIdentities_Resource) Reset() { + *x = ListResourceIdentities_Resource{} + mi := &file_stacks_proto_msgTypes[107] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListResourceIdentities_Resource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListResourceIdentities_Resource) ProtoMessage() {} + +func (x *ListResourceIdentities_Resource) ProtoReflect() protoreflect.Message { + mi := &file_stacks_proto_msgTypes[107] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListResourceIdentities_Resource.ProtoReflect.Descriptor instead. +func (*ListResourceIdentities_Resource) Descriptor() ([]byte, []int) { + return file_stacks_proto_rawDescGZIP(), []int{26, 2} +} + +func (x *ListResourceIdentities_Resource) GetComponentAddr() string { + if x != nil { + return x.ComponentAddr + } + return "" +} + +func (x *ListResourceIdentities_Resource) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +func (x *ListResourceIdentities_Resource) GetResourceInstanceAddr() string { + if x != nil { + return x.ResourceInstanceAddr + } + return "" +} + +func (x *ListResourceIdentities_Resource) GetResourceIdentity() *DynamicValue { + if x != nil { + return x.ResourceIdentity + } + return nil +} + +var File_stacks_proto protoreflect.FileDescriptor + +var file_stacks_proto_rawDesc = string([]byte{ + 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x10, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc8, + 0x01, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x49, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x21, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x50, + 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x48, 0x00, 0x52, 0x03, 0x72, 0x61, 0x77, 0x42, 0x07, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x1a, 0x67, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, + 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x4f, 0x0a, 0x13, 0x43, 0x6c, 0x6f, + 0x73, 0x65, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x1a, 0x2c, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x0a, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xea, 0x06, 0x0a, 0x15, 0x4d, + 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x1a, 0xb7, 0x05, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, + 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, + 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, + 0x68, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x52, 0x0a, 0x06, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, + 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x48, 0x00, + 0x52, 0x06, 0x73, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x1a, 0x98, 0x03, 0x0a, 0x07, 0x4d, 0x61, 0x70, + 0x70, 0x69, 0x6e, 0x67, 0x12, 0x82, 0x01, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x50, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x54, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4d, 0x61, 0x70, 0x12, 0x7c, 0x0a, 0x12, 0x6d, 0x6f, 0x64, + 0x75, 0x6c, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x6d, 0x61, 0x70, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, + 0x65, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x2e, + 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x10, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x65, 0x73, 0x73, 0x4d, 0x61, 0x70, 0x1a, 0x45, 0x0a, 0x17, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x43, + 0x0a, 0x15, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x4d, + 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x1a, 0x96, + 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x38, 0x0a, 0x0a, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x12, 0x49, 0x0a, 0x0e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x5f, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x0d, + 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x08, 0x0a, + 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0x8d, 0x02, 0x0a, 0x16, 0x4f, 0x70, 0x65, 0x6e, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x7d, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30, 0x0a, + 0x14, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x62, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x5f, 0x68, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x42, 0x75, 0x6e, 0x64, 0x6c, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, + 0x40, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x52, 0x0d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x1a, 0x74, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, + 0x13, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x38, 0x0a, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x60, 0x0a, 0x17, 0x43, 0x6c, 0x6f, 0x73, 0x65, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x39, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, + 0x13, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x0a, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8a, 0x02, 0x0a, 0x1a, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0xa5, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, + 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x32, 0x0a, 0x15, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x68, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, + 0x1a, 0x44, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x0b, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xa8, 0x14, 0x0a, 0x20, 0x46, 0x69, 0x6e, 0x64, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x1a, 0x39, 0x0a, 0x07, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x63, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x57, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0xe2, 0x09, 0x0a, 0x0b, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x6f, 0x0a, 0x0a, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x4f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x0a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x7c, 0x0a, 0x0f, + 0x65, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x53, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, + 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x65, 0x6d, 0x62, 0x65, + 0x64, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x7c, 0x0a, 0x0f, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x53, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, + 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, + 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x56, + 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x12, 0x76, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x51, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x12, 0x66, 0x0a, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x18, 0x05, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x4c, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x1a, 0x7c, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x53, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x84, 0x01, 0x0a, 0x13, 0x45, 0x6d, 0x62, 0x65, 0x64, + 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x57, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x41, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x45, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, + 0x63, 0x6b, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x84, 0x01, + 0x0a, 0x13, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x57, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x49, 0x6e, 0x70, 0x75, + 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x80, 0x01, 0x0a, 0x11, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x55, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x77, 0x0a, 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x76, + 0x65, 0x64, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x51, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, + 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x1a, 0xe6, 0x01, 0x0a, 0x0d, 0x45, 0x6d, 0x62, 0x65, 0x64, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, + 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, + 0x64, 0x64, 0x72, 0x12, 0x5b, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, + 0x12, 0x57, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x3f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, + 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0xb0, 0x01, 0x0a, 0x09, 0x43, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x5b, 0x0a, 0x09, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x3d, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, + 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x1a, 0xfc, 0x03, 0x0a, + 0x07, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x23, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, + 0x01, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x5f, 0x0a, + 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x3d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, + 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x42, + 0x02, 0x18, 0x01, 0x52, 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x29, + 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1c, 0x0a, 0x07, 0x64, 0x65, 0x73, + 0x74, 0x72, 0x6f, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x42, 0x02, 0x18, 0x01, 0x52, 0x07, + 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x12, 0x59, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x41, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x76, 0x65, 0x64, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, + 0x6b, 0x73, 0x1a, 0xc6, 0x01, 0x0a, 0x05, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x5b, 0x0a, + 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x3d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, + 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, + 0x09, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, + 0x72, 0x12, 0x18, 0x0a, 0x07, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x1a, 0x67, 0x0a, 0x0d, 0x49, + 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1a, 0x0a, 0x08, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, + 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, + 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, + 0x72, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, + 0x65, 0x72, 0x61, 0x6c, 0x1a, 0x49, 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, + 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x22, + 0x30, 0x0a, 0x09, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x0a, 0x0a, 0x06, + 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4f, 0x55, 0x4e, + 0x54, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x46, 0x4f, 0x52, 0x5f, 0x45, 0x41, 0x43, 0x48, 0x10, + 0x02, 0x22, 0x8c, 0x01, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x1a, 0x4b, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x2e, 0x52, 0x61, 0x77, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x72, 0x61, + 0x77, 0x1a, 0x2d, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, + 0x22, 0x4b, 0x0a, 0x0f, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x1a, 0x2c, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x1a, 0x0a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9b, 0x07, + 0x0a, 0x10, 0x50, 0x6c, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x73, 0x1a, 0xa2, 0x05, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, + 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x08, + 0x70, 0x6c, 0x61, 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, + 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x69, 0x0a, 0x0e, + 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x53, 0x74, 0x61, + 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0d, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, + 0x75, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, + 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, + 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x12, 0x5f, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x56, 0x0a, 0x12, 0x50, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, + 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x10, + 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x3f, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x57, 0x69, 0x74, 0x68, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0xe1, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, + 0x74, 0x12, 0x49, 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x70, + 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x0a, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x12, 0x44, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, + 0x73, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, + 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x48, 0x00, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x42, 0x07, 0x0a, 0x05, + 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x0a, 0x22, 0x73, 0x0a, 0x0d, 0x4f, + 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x6c, 0x61, 0x6e, 0x1a, 0x35, 0x0a, 0x0b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x26, 0x0a, 0x03, 0x72, + 0x61, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x03, + 0x72, 0x61, 0x77, 0x1a, 0x2b, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x1f, 0x0a, 0x0b, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x6c, 0x61, 0x6e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, + 0x22, 0x48, 0x0a, 0x0e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x6c, + 0x61, 0x6e, 0x1a, 0x2a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, + 0x0b, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0a, 0x70, 0x6c, 0x61, 0x6e, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x0a, + 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x86, 0x06, 0x0a, 0x11, 0x41, + 0x70, 0x70, 0x6c, 0x79, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x1a, 0x92, 0x04, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x13, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x34, 0x0a, 0x16, + 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x14, 0x6b, 0x6e, + 0x6f, 0x77, 0x6e, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x65, + 0x79, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x6c, 0x61, 0x6e, 0x48, 0x61, 0x6e, + 0x64, 0x6c, 0x65, 0x12, 0x41, 0x0a, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, + 0x6e, 0x79, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x32, + 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, + 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x12, 0x60, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, + 0x6c, 0x79, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x69, 0x0a, 0x10, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3f, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x57, 0x69, 0x74, 0x68, 0x53, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, + 0x04, 0x08, 0x02, 0x10, 0x03, 0x1a, 0xdb, 0x01, 0x0a, 0x05, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, + 0x49, 0x0a, 0x0e, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x70, 0x70, + 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x38, 0x0a, 0x0a, 0x64, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x12, 0x44, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x48, 0x00, + 0x52, 0x08, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0xa6, 0x05, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, + 0x6b, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x1a, 0x93, 0x04, 0x0a, 0x07, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x11, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x4e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, + 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, + 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x32, + 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, + 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x12, 0x61, 0x0a, 0x0c, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, 0x70, 0x65, + 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x4e, 0x0a, 0x0a, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x69, 0x0a, 0x10, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x3f, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, + 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x57, 0x69, 0x74, 0x68, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x1a, 0x7a, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x16, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, + 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, + 0x6c, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xa0, 0x02, 0x0a, + 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x1a, 0x85, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x16, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x69, 0x6e, + 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x14, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x73, 0x70, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x65, 0x78, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x72, 0x63, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0d, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x72, + 0x63, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, + 0x1a, 0x7d, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x38, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, + 0x68, 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x3e, 0x0a, 0x09, 0x73, 0x65, 0x6e, + 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x09, + 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x22, 0x7a, 0x0a, 0x12, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, + 0x31, 0x0a, 0x03, 0x6f, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x03, 0x6f, + 0x6c, 0x64, 0x12, 0x31, 0x0a, 0x03, 0x6e, 0x65, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x03, 0x6e, 0x65, 0x77, 0x22, 0x8b, 0x01, 0x0a, 0x16, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x57, 0x69, 0x74, 0x68, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, + 0x6e, 0x67, 0x65, 0x22, 0xe4, 0x01, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, 0x73, 0x74, 0x65, + 0x70, 0x73, 0x1a, 0x95, 0x01, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, 0x0a, 0x0e, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x10, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x53, 0x74, + 0x72, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x48, 0x00, 0x52, + 0x0d, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x49, 0x6e, 0x74, 0x42, 0x0a, + 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x7d, 0x0a, 0x1c, 0x43, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, + 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, + 0x72, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x22, 0x8b, 0x01, 0x0a, 0x1b, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x22, 0xb2, 0x01, 0x0a, 0x21, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, + 0x17, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x1f, 0x0a, 0x0b, 0x64, + 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x22, 0x9c, 0x15, 0x0a, + 0x0d, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, + 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, + 0x79, 0x52, 0x03, 0x72, 0x61, 0x77, 0x12, 0x56, 0x0a, 0x0c, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0c, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0xf5, + 0x04, 0x0a, 0x11, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x72, 0x0a, 0x1a, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, + 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x48, 0x00, 0x52, 0x18, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x6f, 0x0a, 0x19, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x70, 0x6c, + 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x48, 0x00, + 0x52, 0x17, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x60, 0x0a, 0x14, 0x6f, 0x75, 0x74, + 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x12, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x27, 0x0a, 0x0e, 0x70, + 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x79, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x79, 0x0a, 0x1a, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, + 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x65, 0x66, 0x65, 0x72, + 0x72, 0x65, 0x64, 0x48, 0x00, 0x52, 0x18, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, + 0x66, 0x0a, 0x16, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x2e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x48, + 0x00, 0x52, 0x14, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, + 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0xb6, 0x01, 0x0a, 0x11, 0x43, 0x6f, 0x6d, 0x70, 0x6f, + 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x43, 0x0a, 0x04, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x04, 0x61, 0x64, 0x64, + 0x72, 0x12, 0x37, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x6c, + 0x61, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x1a, + 0xaf, 0x09, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x37, + 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, + 0x1d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3d, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, + 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x05, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, + 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x64, 0x52, 0x05, + 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x56, 0x0a, 0x08, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x65, 0x64, 0x52, 0x08, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x44, 0x0a, + 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x12, 0x4d, 0x0a, + 0x12, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x10, 0x70, 0x72, 0x65, 0x76, + 0x69, 0x6f, 0x75, 0x73, 0x52, 0x75, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x34, 0x0a, 0x16, + 0x6e, 0x6f, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x5f, 0x6f, + 0x75, 0x74, 0x73, 0x69, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x6e, 0x6f, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x4f, 0x75, 0x74, 0x73, 0x69, + 0x64, 0x65, 0x12, 0x45, 0x0a, 0x0d, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x5f, 0x70, 0x61, + 0x74, 0x68, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0c, 0x72, 0x65, 0x70, + 0x6c, 0x61, 0x63, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x4d, + 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x37, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1f, 0x0a, + 0x0b, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x23, + 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, + 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x1a, 0x58, 0x0a, 0x05, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x35, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x1a, 0x54, 0x0a, + 0x05, 0x4d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x4b, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x76, 0x5f, 0x61, + 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x08, 0x70, 0x72, 0x65, 0x76, 0x41, + 0x64, 0x64, 0x72, 0x1a, 0x6c, 0x0a, 0x08, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, + 0x1b, 0x0a, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, + 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x75, + 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x12, 0x29, 0x0a, 0x10, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, + 0x74, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0f, 0x67, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x1a, 0x99, 0x01, 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x3d, + 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0xb3, 0x01, + 0x0a, 0x18, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x5e, 0x0a, 0x11, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, + 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x64, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, + 0x72, 0x65, 0x64, 0x1a, 0xcf, 0x01, 0x0a, 0x0d, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, + 0x69, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x61, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x3d, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x12, 0x32, 0x0a, 0x15, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x64, 0x75, + 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x13, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x44, 0x75, 0x72, 0x69, 0x6e, 0x67, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x07, 0x22, 0xdc, 0x01, 0x0a, 0x08, + 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x3a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x2e, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x93, 0x01, 0x0a, 0x06, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, + 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, + 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x5f, 0x55, + 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, 0x4f, + 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x02, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, + 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x41, 0x42, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, 0x45, + 0x52, 0x45, 0x51, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x45, 0x46, 0x45, 0x52, 0x52, 0x45, + 0x44, 0x5f, 0x50, 0x52, 0x45, 0x52, 0x45, 0x51, 0x10, 0x05, 0x22, 0x84, 0x0d, 0x0a, 0x0d, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x3c, 0x0a, 0x03, + 0x72, 0x61, 0x77, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, + 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x61, 0x77, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x03, 0x72, 0x61, 0x77, 0x12, 0x56, 0x0a, 0x0c, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x1a, 0x49, 0x0a, 0x09, 0x52, 0x61, 0x77, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0xb9, 0x04, + 0x0a, 0x11, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x44, 0x0a, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, + 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, + 0x48, 0x00, 0x52, 0x07, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x40, 0x0a, 0x05, 0x6d, + 0x6f, 0x76, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x4e, 0x6f, 0x74, + 0x68, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x05, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x60, 0x0a, + 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, + 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x48, 0x00, 0x52, 0x10, 0x72, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, + 0x51, 0x0a, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, + 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x57, 0x0a, 0x0e, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, + 0x61, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, + 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x70, + 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x69, 0x6e, + 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x48, 0x00, 0x52, 0x11, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x42, 0x0d, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4a, + 0x08, 0x08, 0xa0, 0x9c, 0x01, 0x10, 0xa1, 0x9c, 0x01, 0x1a, 0xc4, 0x02, 0x0a, 0x10, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x48, + 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, + 0x64, 0x72, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x3c, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, + 0x77, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x44, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x0c, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x23, 0x0a, 0x0d, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, + 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x69, + 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x69, 0x6d, + 0x1a, 0xbf, 0x02, 0x0a, 0x11, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, + 0x17, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x69, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x43, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x1a, 0x60, 0x0a, 0x11, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, + 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0b, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x1a, 0x61, 0x0a, 0x0d, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, + 0x77, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x09, 0x0a, 0x07, 0x4e, 0x6f, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x22, 0xbf, 0x19, 0x0a, 0x13, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x7c, 0x0a, 0x19, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3e, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x17, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x79, 0x0a, 0x18, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, + 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, + 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x16, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x8f, 0x01, 0x0a, 0x20, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, + 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x44, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x1d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x12, 0x69, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x38, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x48, 0x00, 0x52, 0x11, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x69, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x5f, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, + 0x65, 0x73, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x48, 0x00, 0x52, 0x11, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, + 0x6f, 0x6e, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x7f, 0x0a, 0x1a, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3f, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, + 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x48, + 0x00, 0x52, 0x18, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x6c, 0x0a, 0x13, 0x63, + 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, + 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x39, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, + 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x73, 0x48, 0x00, 0x52, 0x12, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0xa8, 0x01, 0x0a, 0x29, 0x64, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, + 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4c, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, + 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x48, 0x00, 0x52, 0x25, 0x64, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x1a, 0xb2, 0x02, 0x0a, 0x17, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, + 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x43, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, + 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x5d, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x45, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x43, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x22, 0x73, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, + 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, + 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x4c, 0x41, 0x4e, + 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x4c, 0x41, 0x4e, 0x4e, 0x45, + 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x50, 0x50, 0x4c, 0x59, 0x49, 0x4e, 0x47, 0x10, + 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x41, 0x50, 0x50, 0x4c, 0x49, 0x45, 0x44, 0x10, 0x05, 0x12, 0x0b, + 0x0a, 0x07, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x45, 0x44, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x44, + 0x45, 0x46, 0x45, 0x52, 0x52, 0x45, 0x44, 0x10, 0x07, 0x1a, 0xec, 0x02, 0x0a, 0x16, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x48, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x5c, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x44, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, + 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x23, 0x0a, 0x0d, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x41, 0x64, 0x64, + 0x72, 0x22, 0x84, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, + 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x45, 0x4e, + 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, + 0x48, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, + 0x48, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0c, 0x0a, 0x08, 0x50, 0x4c, 0x41, 0x4e, 0x4e, 0x49, 0x4e, + 0x47, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x4c, 0x41, 0x4e, 0x4e, 0x45, 0x44, 0x10, 0x05, + 0x12, 0x0c, 0x0a, 0x08, 0x41, 0x50, 0x50, 0x4c, 0x59, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x0b, + 0x0a, 0x07, 0x41, 0x50, 0x50, 0x4c, 0x49, 0x45, 0x44, 0x10, 0x07, 0x12, 0x0b, 0x0a, 0x07, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x45, 0x44, 0x10, 0x08, 0x1a, 0xad, 0x04, 0x0a, 0x1d, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x48, 0x0a, 0x04, 0x61, 0x64, + 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x04, + 0x61, 0x64, 0x64, 0x72, 0x12, 0x37, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x60, 0x0a, + 0x05, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x4a, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, + 0x72, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x64, 0x52, 0x05, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, + 0x69, 0x0a, 0x08, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x4d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, + 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, + 0x52, 0x08, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x1a, + 0x54, 0x0a, 0x05, 0x4d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x4b, 0x0a, 0x09, 0x70, 0x72, 0x65, 0x76, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x08, 0x70, 0x72, 0x65, + 0x76, 0x41, 0x64, 0x64, 0x72, 0x1a, 0x41, 0x0a, 0x08, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x49, 0x64, 0x12, 0x18, + 0x0a, 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x07, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x1a, 0xbe, 0x01, 0x0a, 0x25, 0x44, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, + 0x67, 0x65, 0x12, 0x37, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x5c, 0x0a, 0x06, 0x63, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x44, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, 0x67, 0x72, + 0x65, 0x73, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x8a, 0x02, 0x0a, 0x11, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x48, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, + 0x64, 0x64, 0x72, 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x50, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x38, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x72, 0x6f, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, + 0x45, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, + 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, + 0x49, 0x4f, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x50, 0x52, 0x4f, 0x56, + 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x45, 0x44, 0x10, 0x03, 0x1a, 0x89, 0x01, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x48, 0x0a, 0x04, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, + 0x52, 0x04, 0x61, 0x64, 0x64, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, + 0x74, 0x70, 0x75, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x1a, 0x91, 0x02, 0x0a, 0x18, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, + 0x43, 0x0a, 0x04, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x49, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x41, 0x64, 0x64, 0x72, 0x52, 0x04, + 0x61, 0x64, 0x64, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x64, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x61, 0x64, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x16, 0x0a, 0x06, + 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x72, 0x65, + 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x66, 0x65, 0x72, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x05, 0x64, 0x65, 0x66, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x6f, + 0x76, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x6d, 0x6f, 0x76, 0x65, 0x12, 0x16, + 0x0a, 0x06, 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, + 0x66, 0x6f, 0x72, 0x67, 0x65, 0x74, 0x1a, 0x62, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, + 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x25, 0x0a, 0x0e, + 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, + 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x69, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x65, 0x76, + 0x65, 0x6e, 0x74, 0x22, 0xff, 0x03, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x98, + 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0b, 0x73, 0x74, 0x61, 0x74, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x36, 0x0a, + 0x17, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, + 0x73, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x15, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x4c, 0x6f, 0x63, 0x6b, 0x73, 0x48, + 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x5f, 0x68, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x61, + 0x63, 0x68, 0x65, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x1a, 0x5a, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0xed, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, + 0x72, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x4c, 0x0a, 0x11, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2a, 0x32, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, + 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4d, 0x41, 0x4e, 0x41, 0x47, 0x45, 0x44, 0x10, 0x01, 0x12, + 0x08, 0x0a, 0x04, 0x44, 0x41, 0x54, 0x41, 0x10, 0x02, 0x2a, 0x35, 0x0a, 0x08, 0x50, 0x6c, 0x61, + 0x6e, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, 0x52, 0x4d, 0x41, 0x4c, 0x10, + 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x46, 0x52, 0x45, 0x53, 0x48, 0x5f, 0x4f, 0x4e, 0x4c, + 0x59, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, + 0x2a, 0x50, 0x0a, 0x0a, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, + 0x0a, 0x04, 0x4e, 0x4f, 0x4f, 0x50, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x52, 0x45, 0x41, 0x44, + 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x02, 0x12, 0x0a, + 0x0a, 0x06, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x10, 0x04, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x4f, 0x52, 0x47, 0x45, 0x54, + 0x10, 0x05, 0x32, 0x9c, 0x0f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x12, 0x7f, 0x0a, + 0x16, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, + 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, + 0x01, 0x0a, 0x17, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x9d, 0x01, 0x0a, 0x20, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x3b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x3c, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x46, 0x69, 0x6e, 0x64, 0x53, 0x74, 0x61, 0x63, + 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x68, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x1a, 0x2a, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x65, 0x0a, 0x0a, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2a, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x6c, + 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x10, 0x50, 0x6c, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x2b, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, + 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x53, + 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, + 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x53, 0x74, 0x61, 0x63, + 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, + 0x12, 0x65, 0x0a, 0x08, 0x4f, 0x70, 0x65, 0x6e, 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x2c, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x74, 0x65, 0x6d, 0x1a, 0x29, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, + 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x62, 0x0a, 0x09, 0x43, 0x6c, 0x6f, 0x73, 0x65, + 0x50, 0x6c, 0x61, 0x6e, 0x12, 0x29, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, + 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, + 0x61, 0x63, 0x6b, 0x50, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x2a, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x50, 0x6c, + 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x11, 0x41, + 0x70, 0x70, 0x6c, 0x79, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x12, 0x2c, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, + 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x73, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x73, 0x0a, 0x12, + 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x12, 0x2d, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, + 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x45, 0x78, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x32, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, + 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x45, 0x78, 0x70, + 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x73, 0x0a, 0x12, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2d, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, + 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x76, 0x0a, 0x13, 0x43, + 0x6c, 0x6f, 0x73, 0x65, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x54, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, + 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x54, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x15, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x54, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x30, 0x2e, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, + 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x73, 0x2e, 0x4d, 0x69, 0x67, 0x72, 0x61, 0x74, 0x65, 0x54, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, + 0x12, 0x7f, 0x0a, 0x16, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x73, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, + 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x73, 0x74, 0x61, 0x63, 0x6b, + 0x73, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_stacks_proto_rawDescOnce sync.Once + file_stacks_proto_rawDescData []byte +) + +func file_stacks_proto_rawDescGZIP() []byte { + file_stacks_proto_rawDescOnce.Do(func() { + file_stacks_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_stacks_proto_rawDesc), len(file_stacks_proto_rawDesc))) + }) + return file_stacks_proto_rawDescData +} + +var file_stacks_proto_enumTypes = make([]protoimpl.EnumInfo, 8) +var file_stacks_proto_msgTypes = make([]protoimpl.MessageInfo, 108) +var file_stacks_proto_goTypes = []any{ + (ResourceMode)(0), // 0: terraform1.stacks.ResourceMode + (PlanMode)(0), // 1: terraform1.stacks.PlanMode + (ChangeType)(0), // 2: terraform1.stacks.ChangeType + (FindStackConfigurationComponents_Instances)(0), // 3: terraform1.stacks.FindStackConfigurationComponents.Instances + (Deferred_Reason)(0), // 4: terraform1.stacks.Deferred.Reason + (StackChangeProgress_ComponentInstanceStatus_Status)(0), // 5: terraform1.stacks.StackChangeProgress.ComponentInstanceStatus.Status + (StackChangeProgress_ResourceInstanceStatus_Status)(0), // 6: terraform1.stacks.StackChangeProgress.ResourceInstanceStatus.Status + (StackChangeProgress_ProvisionerStatus_Status)(0), // 7: terraform1.stacks.StackChangeProgress.ProvisionerStatus.Status + (*OpenTerraformState)(nil), // 8: terraform1.stacks.OpenTerraformState + (*CloseTerraformState)(nil), // 9: terraform1.stacks.CloseTerraformState + (*MigrateTerraformState)(nil), // 10: terraform1.stacks.MigrateTerraformState + (*OpenStackConfiguration)(nil), // 11: terraform1.stacks.OpenStackConfiguration + (*CloseStackConfiguration)(nil), // 12: terraform1.stacks.CloseStackConfiguration + (*ValidateStackConfiguration)(nil), // 13: terraform1.stacks.ValidateStackConfiguration + (*FindStackConfigurationComponents)(nil), // 14: terraform1.stacks.FindStackConfigurationComponents + (*OpenStackState)(nil), // 15: terraform1.stacks.OpenStackState + (*CloseStackState)(nil), // 16: terraform1.stacks.CloseStackState + (*PlanStackChanges)(nil), // 17: terraform1.stacks.PlanStackChanges + (*OpenStackPlan)(nil), // 18: terraform1.stacks.OpenStackPlan + (*CloseStackPlan)(nil), // 19: terraform1.stacks.CloseStackPlan + (*ApplyStackChanges)(nil), // 20: terraform1.stacks.ApplyStackChanges + (*OpenStackInspector)(nil), // 21: terraform1.stacks.OpenStackInspector + (*InspectExpressionResult)(nil), // 22: terraform1.stacks.InspectExpressionResult + (*DynamicValue)(nil), // 23: terraform1.stacks.DynamicValue + (*DynamicValueChange)(nil), // 24: terraform1.stacks.DynamicValueChange + (*DynamicValueWithSource)(nil), // 25: terraform1.stacks.DynamicValueWithSource + (*AttributePath)(nil), // 26: terraform1.stacks.AttributePath + (*ComponentInstanceInStackAddr)(nil), // 27: terraform1.stacks.ComponentInstanceInStackAddr + (*ResourceInstanceInStackAddr)(nil), // 28: terraform1.stacks.ResourceInstanceInStackAddr + (*ResourceInstanceObjectInStackAddr)(nil), // 29: terraform1.stacks.ResourceInstanceObjectInStackAddr + (*PlannedChange)(nil), // 30: terraform1.stacks.PlannedChange + (*Deferred)(nil), // 31: terraform1.stacks.Deferred + (*AppliedChange)(nil), // 32: terraform1.stacks.AppliedChange + (*StackChangeProgress)(nil), // 33: terraform1.stacks.StackChangeProgress + (*ListResourceIdentities)(nil), // 34: terraform1.stacks.ListResourceIdentities + (*OpenTerraformState_Request)(nil), // 35: terraform1.stacks.OpenTerraformState.Request + (*OpenTerraformState_Response)(nil), // 36: terraform1.stacks.OpenTerraformState.Response + (*CloseTerraformState_Request)(nil), // 37: terraform1.stacks.CloseTerraformState.Request + (*CloseTerraformState_Response)(nil), // 38: terraform1.stacks.CloseTerraformState.Response + (*MigrateTerraformState_Request)(nil), // 39: terraform1.stacks.MigrateTerraformState.Request + (*MigrateTerraformState_Event)(nil), // 40: terraform1.stacks.MigrateTerraformState.Event + (*MigrateTerraformState_Request_Mapping)(nil), // 41: terraform1.stacks.MigrateTerraformState.Request.Mapping + nil, // 42: terraform1.stacks.MigrateTerraformState.Request.Mapping.ResourceAddressMapEntry + nil, // 43: terraform1.stacks.MigrateTerraformState.Request.Mapping.ModuleAddressMapEntry + (*OpenStackConfiguration_Request)(nil), // 44: terraform1.stacks.OpenStackConfiguration.Request + (*OpenStackConfiguration_Response)(nil), // 45: terraform1.stacks.OpenStackConfiguration.Response + (*CloseStackConfiguration_Request)(nil), // 46: terraform1.stacks.CloseStackConfiguration.Request + (*CloseStackConfiguration_Response)(nil), // 47: terraform1.stacks.CloseStackConfiguration.Response + (*ValidateStackConfiguration_Request)(nil), // 48: terraform1.stacks.ValidateStackConfiguration.Request + (*ValidateStackConfiguration_Response)(nil), // 49: terraform1.stacks.ValidateStackConfiguration.Response + (*FindStackConfigurationComponents_Request)(nil), // 50: terraform1.stacks.FindStackConfigurationComponents.Request + (*FindStackConfigurationComponents_Response)(nil), // 51: terraform1.stacks.FindStackConfigurationComponents.Response + (*FindStackConfigurationComponents_StackConfig)(nil), // 52: terraform1.stacks.FindStackConfigurationComponents.StackConfig + (*FindStackConfigurationComponents_EmbeddedStack)(nil), // 53: terraform1.stacks.FindStackConfigurationComponents.EmbeddedStack + (*FindStackConfigurationComponents_Component)(nil), // 54: terraform1.stacks.FindStackConfigurationComponents.Component + (*FindStackConfigurationComponents_Removed)(nil), // 55: terraform1.stacks.FindStackConfigurationComponents.Removed + (*FindStackConfigurationComponents_InputVariable)(nil), // 56: terraform1.stacks.FindStackConfigurationComponents.InputVariable + (*FindStackConfigurationComponents_OutputValue)(nil), // 57: terraform1.stacks.FindStackConfigurationComponents.OutputValue + nil, // 58: terraform1.stacks.FindStackConfigurationComponents.StackConfig.ComponentsEntry + nil, // 59: terraform1.stacks.FindStackConfigurationComponents.StackConfig.EmbeddedStacksEntry + nil, // 60: terraform1.stacks.FindStackConfigurationComponents.StackConfig.InputVariablesEntry + nil, // 61: terraform1.stacks.FindStackConfigurationComponents.StackConfig.OutputValuesEntry + nil, // 62: terraform1.stacks.FindStackConfigurationComponents.StackConfig.RemovedEntry + (*FindStackConfigurationComponents_Removed_Block)(nil), // 63: terraform1.stacks.FindStackConfigurationComponents.Removed.Block + (*OpenStackState_RequestItem)(nil), // 64: terraform1.stacks.OpenStackState.RequestItem + (*OpenStackState_Response)(nil), // 65: terraform1.stacks.OpenStackState.Response + (*CloseStackState_Request)(nil), // 66: terraform1.stacks.CloseStackState.Request + (*CloseStackState_Response)(nil), // 67: terraform1.stacks.CloseStackState.Response + (*PlanStackChanges_Request)(nil), // 68: terraform1.stacks.PlanStackChanges.Request + (*PlanStackChanges_Event)(nil), // 69: terraform1.stacks.PlanStackChanges.Event + nil, // 70: terraform1.stacks.PlanStackChanges.Request.PreviousStateEntry + nil, // 71: terraform1.stacks.PlanStackChanges.Request.InputValuesEntry + (*OpenStackPlan_RequestItem)(nil), // 72: terraform1.stacks.OpenStackPlan.RequestItem + (*OpenStackPlan_Response)(nil), // 73: terraform1.stacks.OpenStackPlan.Response + (*CloseStackPlan_Request)(nil), // 74: terraform1.stacks.CloseStackPlan.Request + (*CloseStackPlan_Response)(nil), // 75: terraform1.stacks.CloseStackPlan.Response + (*ApplyStackChanges_Request)(nil), // 76: terraform1.stacks.ApplyStackChanges.Request + (*ApplyStackChanges_Event)(nil), // 77: terraform1.stacks.ApplyStackChanges.Event + nil, // 78: terraform1.stacks.ApplyStackChanges.Request.InputValuesEntry + (*OpenStackInspector_Request)(nil), // 79: terraform1.stacks.OpenStackInspector.Request + (*OpenStackInspector_Response)(nil), // 80: terraform1.stacks.OpenStackInspector.Response + nil, // 81: terraform1.stacks.OpenStackInspector.Request.StateEntry + nil, // 82: terraform1.stacks.OpenStackInspector.Request.InputValuesEntry + (*InspectExpressionResult_Request)(nil), // 83: terraform1.stacks.InspectExpressionResult.Request + (*InspectExpressionResult_Response)(nil), // 84: terraform1.stacks.InspectExpressionResult.Response + (*AttributePath_Step)(nil), // 85: terraform1.stacks.AttributePath.Step + (*PlannedChange_ChangeDescription)(nil), // 86: terraform1.stacks.PlannedChange.ChangeDescription + (*PlannedChange_ComponentInstance)(nil), // 87: terraform1.stacks.PlannedChange.ComponentInstance + (*PlannedChange_ResourceInstance)(nil), // 88: terraform1.stacks.PlannedChange.ResourceInstance + (*PlannedChange_OutputValue)(nil), // 89: terraform1.stacks.PlannedChange.OutputValue + (*PlannedChange_ResourceInstanceDeferred)(nil), // 90: terraform1.stacks.PlannedChange.ResourceInstanceDeferred + (*PlannedChange_InputVariable)(nil), // 91: terraform1.stacks.PlannedChange.InputVariable + (*PlannedChange_ResourceInstance_Index)(nil), // 92: terraform1.stacks.PlannedChange.ResourceInstance.Index + (*PlannedChange_ResourceInstance_Moved)(nil), // 93: terraform1.stacks.PlannedChange.ResourceInstance.Moved + (*PlannedChange_ResourceInstance_Imported)(nil), // 94: terraform1.stacks.PlannedChange.ResourceInstance.Imported + (*AppliedChange_RawChange)(nil), // 95: terraform1.stacks.AppliedChange.RawChange + (*AppliedChange_ChangeDescription)(nil), // 96: terraform1.stacks.AppliedChange.ChangeDescription + (*AppliedChange_ResourceInstance)(nil), // 97: terraform1.stacks.AppliedChange.ResourceInstance + (*AppliedChange_ComponentInstance)(nil), // 98: terraform1.stacks.AppliedChange.ComponentInstance + (*AppliedChange_OutputValue)(nil), // 99: terraform1.stacks.AppliedChange.OutputValue + (*AppliedChange_InputVariable)(nil), // 100: terraform1.stacks.AppliedChange.InputVariable + (*AppliedChange_Nothing)(nil), // 101: terraform1.stacks.AppliedChange.Nothing + nil, // 102: terraform1.stacks.AppliedChange.ComponentInstance.OutputValuesEntry + (*StackChangeProgress_ComponentInstanceStatus)(nil), // 103: terraform1.stacks.StackChangeProgress.ComponentInstanceStatus + (*StackChangeProgress_ResourceInstanceStatus)(nil), // 104: terraform1.stacks.StackChangeProgress.ResourceInstanceStatus + (*StackChangeProgress_ResourceInstancePlannedChange)(nil), // 105: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange + (*StackChangeProgress_DeferredResourceInstancePlannedChange)(nil), // 106: terraform1.stacks.StackChangeProgress.DeferredResourceInstancePlannedChange + (*StackChangeProgress_ProvisionerStatus)(nil), // 107: terraform1.stacks.StackChangeProgress.ProvisionerStatus + (*StackChangeProgress_ProvisionerOutput)(nil), // 108: terraform1.stacks.StackChangeProgress.ProvisionerOutput + (*StackChangeProgress_ComponentInstanceChanges)(nil), // 109: terraform1.stacks.StackChangeProgress.ComponentInstanceChanges + (*StackChangeProgress_ComponentInstances)(nil), // 110: terraform1.stacks.StackChangeProgress.ComponentInstances + (*StackChangeProgress_ResourceInstancePlannedChange_Moved)(nil), // 111: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.Moved + (*StackChangeProgress_ResourceInstancePlannedChange_Imported)(nil), // 112: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.Imported + (*ListResourceIdentities_Request)(nil), // 113: terraform1.stacks.ListResourceIdentities.Request + (*ListResourceIdentities_Response)(nil), // 114: terraform1.stacks.ListResourceIdentities.Response + (*ListResourceIdentities_Resource)(nil), // 115: terraform1.stacks.ListResourceIdentities.Resource + (*terraform1.SourceRange)(nil), // 116: terraform1.SourceRange + (*anypb.Any)(nil), // 117: google.protobuf.Any + (*terraform1.Diagnostic)(nil), // 118: terraform1.Diagnostic + (*terraform1.SourceAddress)(nil), // 119: terraform1.SourceAddress +} +var file_stacks_proto_depIdxs = []int32{ + 26, // 0: terraform1.stacks.DynamicValue.sensitive:type_name -> terraform1.stacks.AttributePath + 23, // 1: terraform1.stacks.DynamicValueChange.old:type_name -> terraform1.stacks.DynamicValue + 23, // 2: terraform1.stacks.DynamicValueChange.new:type_name -> terraform1.stacks.DynamicValue + 23, // 3: terraform1.stacks.DynamicValueWithSource.value:type_name -> terraform1.stacks.DynamicValue + 116, // 4: terraform1.stacks.DynamicValueWithSource.source_range:type_name -> terraform1.SourceRange + 85, // 5: terraform1.stacks.AttributePath.steps:type_name -> terraform1.stacks.AttributePath.Step + 117, // 6: terraform1.stacks.PlannedChange.raw:type_name -> google.protobuf.Any + 86, // 7: terraform1.stacks.PlannedChange.descriptions:type_name -> terraform1.stacks.PlannedChange.ChangeDescription + 4, // 8: terraform1.stacks.Deferred.reason:type_name -> terraform1.stacks.Deferred.Reason + 95, // 9: terraform1.stacks.AppliedChange.raw:type_name -> terraform1.stacks.AppliedChange.RawChange + 96, // 10: terraform1.stacks.AppliedChange.descriptions:type_name -> terraform1.stacks.AppliedChange.ChangeDescription + 103, // 11: terraform1.stacks.StackChangeProgress.component_instance_status:type_name -> terraform1.stacks.StackChangeProgress.ComponentInstanceStatus + 104, // 12: terraform1.stacks.StackChangeProgress.resource_instance_status:type_name -> terraform1.stacks.StackChangeProgress.ResourceInstanceStatus + 105, // 13: terraform1.stacks.StackChangeProgress.resource_instance_planned_change:type_name -> terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange + 107, // 14: terraform1.stacks.StackChangeProgress.provisioner_status:type_name -> terraform1.stacks.StackChangeProgress.ProvisionerStatus + 108, // 15: terraform1.stacks.StackChangeProgress.provisioner_output:type_name -> terraform1.stacks.StackChangeProgress.ProvisionerOutput + 109, // 16: terraform1.stacks.StackChangeProgress.component_instance_changes:type_name -> terraform1.stacks.StackChangeProgress.ComponentInstanceChanges + 110, // 17: terraform1.stacks.StackChangeProgress.component_instances:type_name -> terraform1.stacks.StackChangeProgress.ComponentInstances + 106, // 18: terraform1.stacks.StackChangeProgress.deferred_resource_instance_planned_change:type_name -> terraform1.stacks.StackChangeProgress.DeferredResourceInstancePlannedChange + 118, // 19: terraform1.stacks.OpenTerraformState.Response.diagnostics:type_name -> terraform1.Diagnostic + 41, // 20: terraform1.stacks.MigrateTerraformState.Request.simple:type_name -> terraform1.stacks.MigrateTerraformState.Request.Mapping + 118, // 21: terraform1.stacks.MigrateTerraformState.Event.diagnostic:type_name -> terraform1.Diagnostic + 32, // 22: terraform1.stacks.MigrateTerraformState.Event.applied_change:type_name -> terraform1.stacks.AppliedChange + 42, // 23: terraform1.stacks.MigrateTerraformState.Request.Mapping.resource_address_map:type_name -> terraform1.stacks.MigrateTerraformState.Request.Mapping.ResourceAddressMapEntry + 43, // 24: terraform1.stacks.MigrateTerraformState.Request.Mapping.module_address_map:type_name -> terraform1.stacks.MigrateTerraformState.Request.Mapping.ModuleAddressMapEntry + 119, // 25: terraform1.stacks.OpenStackConfiguration.Request.source_address:type_name -> terraform1.SourceAddress + 118, // 26: terraform1.stacks.OpenStackConfiguration.Response.diagnostics:type_name -> terraform1.Diagnostic + 118, // 27: terraform1.stacks.ValidateStackConfiguration.Response.diagnostics:type_name -> terraform1.Diagnostic + 52, // 28: terraform1.stacks.FindStackConfigurationComponents.Response.config:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig + 58, // 29: terraform1.stacks.FindStackConfigurationComponents.StackConfig.components:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig.ComponentsEntry + 59, // 30: terraform1.stacks.FindStackConfigurationComponents.StackConfig.embedded_stacks:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig.EmbeddedStacksEntry + 60, // 31: terraform1.stacks.FindStackConfigurationComponents.StackConfig.input_variables:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig.InputVariablesEntry + 61, // 32: terraform1.stacks.FindStackConfigurationComponents.StackConfig.output_values:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig.OutputValuesEntry + 62, // 33: terraform1.stacks.FindStackConfigurationComponents.StackConfig.removed:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig.RemovedEntry + 3, // 34: terraform1.stacks.FindStackConfigurationComponents.EmbeddedStack.instances:type_name -> terraform1.stacks.FindStackConfigurationComponents.Instances + 52, // 35: terraform1.stacks.FindStackConfigurationComponents.EmbeddedStack.config:type_name -> terraform1.stacks.FindStackConfigurationComponents.StackConfig + 3, // 36: terraform1.stacks.FindStackConfigurationComponents.Component.instances:type_name -> terraform1.stacks.FindStackConfigurationComponents.Instances + 3, // 37: terraform1.stacks.FindStackConfigurationComponents.Removed.instances:type_name -> terraform1.stacks.FindStackConfigurationComponents.Instances + 63, // 38: terraform1.stacks.FindStackConfigurationComponents.Removed.blocks:type_name -> terraform1.stacks.FindStackConfigurationComponents.Removed.Block + 54, // 39: terraform1.stacks.FindStackConfigurationComponents.StackConfig.ComponentsEntry.value:type_name -> terraform1.stacks.FindStackConfigurationComponents.Component + 53, // 40: terraform1.stacks.FindStackConfigurationComponents.StackConfig.EmbeddedStacksEntry.value:type_name -> terraform1.stacks.FindStackConfigurationComponents.EmbeddedStack + 56, // 41: terraform1.stacks.FindStackConfigurationComponents.StackConfig.InputVariablesEntry.value:type_name -> terraform1.stacks.FindStackConfigurationComponents.InputVariable + 57, // 42: terraform1.stacks.FindStackConfigurationComponents.StackConfig.OutputValuesEntry.value:type_name -> terraform1.stacks.FindStackConfigurationComponents.OutputValue + 55, // 43: terraform1.stacks.FindStackConfigurationComponents.StackConfig.RemovedEntry.value:type_name -> terraform1.stacks.FindStackConfigurationComponents.Removed + 3, // 44: terraform1.stacks.FindStackConfigurationComponents.Removed.Block.instances:type_name -> terraform1.stacks.FindStackConfigurationComponents.Instances + 95, // 45: terraform1.stacks.OpenStackState.RequestItem.raw:type_name -> terraform1.stacks.AppliedChange.RawChange + 1, // 46: terraform1.stacks.PlanStackChanges.Request.plan_mode:type_name -> terraform1.stacks.PlanMode + 70, // 47: terraform1.stacks.PlanStackChanges.Request.previous_state:type_name -> terraform1.stacks.PlanStackChanges.Request.PreviousStateEntry + 71, // 48: terraform1.stacks.PlanStackChanges.Request.input_values:type_name -> terraform1.stacks.PlanStackChanges.Request.InputValuesEntry + 30, // 49: terraform1.stacks.PlanStackChanges.Event.planned_change:type_name -> terraform1.stacks.PlannedChange + 118, // 50: terraform1.stacks.PlanStackChanges.Event.diagnostic:type_name -> terraform1.Diagnostic + 33, // 51: terraform1.stacks.PlanStackChanges.Event.progress:type_name -> terraform1.stacks.StackChangeProgress + 117, // 52: terraform1.stacks.PlanStackChanges.Request.PreviousStateEntry.value:type_name -> google.protobuf.Any + 25, // 53: terraform1.stacks.PlanStackChanges.Request.InputValuesEntry.value:type_name -> terraform1.stacks.DynamicValueWithSource + 117, // 54: terraform1.stacks.OpenStackPlan.RequestItem.raw:type_name -> google.protobuf.Any + 117, // 55: terraform1.stacks.ApplyStackChanges.Request.planned_changes:type_name -> google.protobuf.Any + 78, // 56: terraform1.stacks.ApplyStackChanges.Request.input_values:type_name -> terraform1.stacks.ApplyStackChanges.Request.InputValuesEntry + 32, // 57: terraform1.stacks.ApplyStackChanges.Event.applied_change:type_name -> terraform1.stacks.AppliedChange + 118, // 58: terraform1.stacks.ApplyStackChanges.Event.diagnostic:type_name -> terraform1.Diagnostic + 33, // 59: terraform1.stacks.ApplyStackChanges.Event.progress:type_name -> terraform1.stacks.StackChangeProgress + 25, // 60: terraform1.stacks.ApplyStackChanges.Request.InputValuesEntry.value:type_name -> terraform1.stacks.DynamicValueWithSource + 81, // 61: terraform1.stacks.OpenStackInspector.Request.state:type_name -> terraform1.stacks.OpenStackInspector.Request.StateEntry + 82, // 62: terraform1.stacks.OpenStackInspector.Request.input_values:type_name -> terraform1.stacks.OpenStackInspector.Request.InputValuesEntry + 118, // 63: terraform1.stacks.OpenStackInspector.Response.diagnostics:type_name -> terraform1.Diagnostic + 117, // 64: terraform1.stacks.OpenStackInspector.Request.StateEntry.value:type_name -> google.protobuf.Any + 25, // 65: terraform1.stacks.OpenStackInspector.Request.InputValuesEntry.value:type_name -> terraform1.stacks.DynamicValueWithSource + 23, // 66: terraform1.stacks.InspectExpressionResult.Response.result:type_name -> terraform1.stacks.DynamicValue + 118, // 67: terraform1.stacks.InspectExpressionResult.Response.diagnostics:type_name -> terraform1.Diagnostic + 87, // 68: terraform1.stacks.PlannedChange.ChangeDescription.component_instance_planned:type_name -> terraform1.stacks.PlannedChange.ComponentInstance + 88, // 69: terraform1.stacks.PlannedChange.ChangeDescription.resource_instance_planned:type_name -> terraform1.stacks.PlannedChange.ResourceInstance + 89, // 70: terraform1.stacks.PlannedChange.ChangeDescription.output_value_planned:type_name -> terraform1.stacks.PlannedChange.OutputValue + 90, // 71: terraform1.stacks.PlannedChange.ChangeDescription.resource_instance_deferred:type_name -> terraform1.stacks.PlannedChange.ResourceInstanceDeferred + 91, // 72: terraform1.stacks.PlannedChange.ChangeDescription.input_variable_planned:type_name -> terraform1.stacks.PlannedChange.InputVariable + 27, // 73: terraform1.stacks.PlannedChange.ComponentInstance.addr:type_name -> terraform1.stacks.ComponentInstanceInStackAddr + 2, // 74: terraform1.stacks.PlannedChange.ComponentInstance.actions:type_name -> terraform1.stacks.ChangeType + 29, // 75: terraform1.stacks.PlannedChange.ResourceInstance.addr:type_name -> terraform1.stacks.ResourceInstanceObjectInStackAddr + 2, // 76: terraform1.stacks.PlannedChange.ResourceInstance.actions:type_name -> terraform1.stacks.ChangeType + 24, // 77: terraform1.stacks.PlannedChange.ResourceInstance.values:type_name -> terraform1.stacks.DynamicValueChange + 93, // 78: terraform1.stacks.PlannedChange.ResourceInstance.moved:type_name -> terraform1.stacks.PlannedChange.ResourceInstance.Moved + 94, // 79: terraform1.stacks.PlannedChange.ResourceInstance.imported:type_name -> terraform1.stacks.PlannedChange.ResourceInstance.Imported + 0, // 80: terraform1.stacks.PlannedChange.ResourceInstance.resource_mode:type_name -> terraform1.stacks.ResourceMode + 23, // 81: terraform1.stacks.PlannedChange.ResourceInstance.previous_run_value:type_name -> terraform1.stacks.DynamicValue + 26, // 82: terraform1.stacks.PlannedChange.ResourceInstance.replace_paths:type_name -> terraform1.stacks.AttributePath + 92, // 83: terraform1.stacks.PlannedChange.ResourceInstance.index:type_name -> terraform1.stacks.PlannedChange.ResourceInstance.Index + 2, // 84: terraform1.stacks.PlannedChange.OutputValue.actions:type_name -> terraform1.stacks.ChangeType + 24, // 85: terraform1.stacks.PlannedChange.OutputValue.values:type_name -> terraform1.stacks.DynamicValueChange + 88, // 86: terraform1.stacks.PlannedChange.ResourceInstanceDeferred.resource_instance:type_name -> terraform1.stacks.PlannedChange.ResourceInstance + 31, // 87: terraform1.stacks.PlannedChange.ResourceInstanceDeferred.deferred:type_name -> terraform1.stacks.Deferred + 2, // 88: terraform1.stacks.PlannedChange.InputVariable.actions:type_name -> terraform1.stacks.ChangeType + 24, // 89: terraform1.stacks.PlannedChange.InputVariable.values:type_name -> terraform1.stacks.DynamicValueChange + 23, // 90: terraform1.stacks.PlannedChange.ResourceInstance.Index.value:type_name -> terraform1.stacks.DynamicValue + 28, // 91: terraform1.stacks.PlannedChange.ResourceInstance.Moved.prev_addr:type_name -> terraform1.stacks.ResourceInstanceInStackAddr + 117, // 92: terraform1.stacks.AppliedChange.RawChange.value:type_name -> google.protobuf.Any + 101, // 93: terraform1.stacks.AppliedChange.ChangeDescription.deleted:type_name -> terraform1.stacks.AppliedChange.Nothing + 101, // 94: terraform1.stacks.AppliedChange.ChangeDescription.moved:type_name -> terraform1.stacks.AppliedChange.Nothing + 97, // 95: terraform1.stacks.AppliedChange.ChangeDescription.resource_instance:type_name -> terraform1.stacks.AppliedChange.ResourceInstance + 99, // 96: terraform1.stacks.AppliedChange.ChangeDescription.output_value:type_name -> terraform1.stacks.AppliedChange.OutputValue + 100, // 97: terraform1.stacks.AppliedChange.ChangeDescription.input_variable:type_name -> terraform1.stacks.AppliedChange.InputVariable + 98, // 98: terraform1.stacks.AppliedChange.ChangeDescription.component_instance:type_name -> terraform1.stacks.AppliedChange.ComponentInstance + 29, // 99: terraform1.stacks.AppliedChange.ResourceInstance.addr:type_name -> terraform1.stacks.ResourceInstanceObjectInStackAddr + 23, // 100: terraform1.stacks.AppliedChange.ResourceInstance.new_value:type_name -> terraform1.stacks.DynamicValue + 0, // 101: terraform1.stacks.AppliedChange.ResourceInstance.resource_mode:type_name -> terraform1.stacks.ResourceMode + 102, // 102: terraform1.stacks.AppliedChange.ComponentInstance.output_values:type_name -> terraform1.stacks.AppliedChange.ComponentInstance.OutputValuesEntry + 23, // 103: terraform1.stacks.AppliedChange.OutputValue.new_value:type_name -> terraform1.stacks.DynamicValue + 23, // 104: terraform1.stacks.AppliedChange.InputVariable.new_value:type_name -> terraform1.stacks.DynamicValue + 23, // 105: terraform1.stacks.AppliedChange.ComponentInstance.OutputValuesEntry.value:type_name -> terraform1.stacks.DynamicValue + 27, // 106: terraform1.stacks.StackChangeProgress.ComponentInstanceStatus.addr:type_name -> terraform1.stacks.ComponentInstanceInStackAddr + 5, // 107: terraform1.stacks.StackChangeProgress.ComponentInstanceStatus.status:type_name -> terraform1.stacks.StackChangeProgress.ComponentInstanceStatus.Status + 29, // 108: terraform1.stacks.StackChangeProgress.ResourceInstanceStatus.addr:type_name -> terraform1.stacks.ResourceInstanceObjectInStackAddr + 6, // 109: terraform1.stacks.StackChangeProgress.ResourceInstanceStatus.status:type_name -> terraform1.stacks.StackChangeProgress.ResourceInstanceStatus.Status + 29, // 110: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.addr:type_name -> terraform1.stacks.ResourceInstanceObjectInStackAddr + 2, // 111: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.actions:type_name -> terraform1.stacks.ChangeType + 111, // 112: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.moved:type_name -> terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.Moved + 112, // 113: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.imported:type_name -> terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.Imported + 31, // 114: terraform1.stacks.StackChangeProgress.DeferredResourceInstancePlannedChange.deferred:type_name -> terraform1.stacks.Deferred + 105, // 115: terraform1.stacks.StackChangeProgress.DeferredResourceInstancePlannedChange.change:type_name -> terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange + 29, // 116: terraform1.stacks.StackChangeProgress.ProvisionerStatus.addr:type_name -> terraform1.stacks.ResourceInstanceObjectInStackAddr + 107, // 117: terraform1.stacks.StackChangeProgress.ProvisionerStatus.status:type_name -> terraform1.stacks.StackChangeProgress.ProvisionerStatus + 29, // 118: terraform1.stacks.StackChangeProgress.ProvisionerOutput.addr:type_name -> terraform1.stacks.ResourceInstanceObjectInStackAddr + 27, // 119: terraform1.stacks.StackChangeProgress.ComponentInstanceChanges.addr:type_name -> terraform1.stacks.ComponentInstanceInStackAddr + 28, // 120: terraform1.stacks.StackChangeProgress.ResourceInstancePlannedChange.Moved.prev_addr:type_name -> terraform1.stacks.ResourceInstanceInStackAddr + 115, // 121: terraform1.stacks.ListResourceIdentities.Response.resource:type_name -> terraform1.stacks.ListResourceIdentities.Resource + 23, // 122: terraform1.stacks.ListResourceIdentities.Resource.resource_identity:type_name -> terraform1.stacks.DynamicValue + 44, // 123: terraform1.stacks.Stacks.OpenStackConfiguration:input_type -> terraform1.stacks.OpenStackConfiguration.Request + 46, // 124: terraform1.stacks.Stacks.CloseStackConfiguration:input_type -> terraform1.stacks.CloseStackConfiguration.Request + 48, // 125: terraform1.stacks.Stacks.ValidateStackConfiguration:input_type -> terraform1.stacks.ValidateStackConfiguration.Request + 50, // 126: terraform1.stacks.Stacks.FindStackConfigurationComponents:input_type -> terraform1.stacks.FindStackConfigurationComponents.Request + 64, // 127: terraform1.stacks.Stacks.OpenState:input_type -> terraform1.stacks.OpenStackState.RequestItem + 66, // 128: terraform1.stacks.Stacks.CloseState:input_type -> terraform1.stacks.CloseStackState.Request + 68, // 129: terraform1.stacks.Stacks.PlanStackChanges:input_type -> terraform1.stacks.PlanStackChanges.Request + 72, // 130: terraform1.stacks.Stacks.OpenPlan:input_type -> terraform1.stacks.OpenStackPlan.RequestItem + 74, // 131: terraform1.stacks.Stacks.ClosePlan:input_type -> terraform1.stacks.CloseStackPlan.Request + 76, // 132: terraform1.stacks.Stacks.ApplyStackChanges:input_type -> terraform1.stacks.ApplyStackChanges.Request + 79, // 133: terraform1.stacks.Stacks.OpenStackInspector:input_type -> terraform1.stacks.OpenStackInspector.Request + 83, // 134: terraform1.stacks.Stacks.InspectExpressionResult:input_type -> terraform1.stacks.InspectExpressionResult.Request + 35, // 135: terraform1.stacks.Stacks.OpenTerraformState:input_type -> terraform1.stacks.OpenTerraformState.Request + 37, // 136: terraform1.stacks.Stacks.CloseTerraformState:input_type -> terraform1.stacks.CloseTerraformState.Request + 39, // 137: terraform1.stacks.Stacks.MigrateTerraformState:input_type -> terraform1.stacks.MigrateTerraformState.Request + 113, // 138: terraform1.stacks.Stacks.ListResourceIdentities:input_type -> terraform1.stacks.ListResourceIdentities.Request + 45, // 139: terraform1.stacks.Stacks.OpenStackConfiguration:output_type -> terraform1.stacks.OpenStackConfiguration.Response + 47, // 140: terraform1.stacks.Stacks.CloseStackConfiguration:output_type -> terraform1.stacks.CloseStackConfiguration.Response + 49, // 141: terraform1.stacks.Stacks.ValidateStackConfiguration:output_type -> terraform1.stacks.ValidateStackConfiguration.Response + 51, // 142: terraform1.stacks.Stacks.FindStackConfigurationComponents:output_type -> terraform1.stacks.FindStackConfigurationComponents.Response + 65, // 143: terraform1.stacks.Stacks.OpenState:output_type -> terraform1.stacks.OpenStackState.Response + 67, // 144: terraform1.stacks.Stacks.CloseState:output_type -> terraform1.stacks.CloseStackState.Response + 69, // 145: terraform1.stacks.Stacks.PlanStackChanges:output_type -> terraform1.stacks.PlanStackChanges.Event + 73, // 146: terraform1.stacks.Stacks.OpenPlan:output_type -> terraform1.stacks.OpenStackPlan.Response + 75, // 147: terraform1.stacks.Stacks.ClosePlan:output_type -> terraform1.stacks.CloseStackPlan.Response + 77, // 148: terraform1.stacks.Stacks.ApplyStackChanges:output_type -> terraform1.stacks.ApplyStackChanges.Event + 80, // 149: terraform1.stacks.Stacks.OpenStackInspector:output_type -> terraform1.stacks.OpenStackInspector.Response + 84, // 150: terraform1.stacks.Stacks.InspectExpressionResult:output_type -> terraform1.stacks.InspectExpressionResult.Response + 36, // 151: terraform1.stacks.Stacks.OpenTerraformState:output_type -> terraform1.stacks.OpenTerraformState.Response + 38, // 152: terraform1.stacks.Stacks.CloseTerraformState:output_type -> terraform1.stacks.CloseTerraformState.Response + 40, // 153: terraform1.stacks.Stacks.MigrateTerraformState:output_type -> terraform1.stacks.MigrateTerraformState.Event + 114, // 154: terraform1.stacks.Stacks.ListResourceIdentities:output_type -> terraform1.stacks.ListResourceIdentities.Response + 139, // [139:155] is the sub-list for method output_type + 123, // [123:139] is the sub-list for method input_type + 123, // [123:123] is the sub-list for extension type_name + 123, // [123:123] is the sub-list for extension extendee + 0, // [0:123] is the sub-list for field type_name +} + +func init() { file_stacks_proto_init() } +func file_stacks_proto_init() { + if File_stacks_proto != nil { + return + } + file_stacks_proto_msgTypes[25].OneofWrappers = []any{ + (*StackChangeProgress_ComponentInstanceStatus_)(nil), + (*StackChangeProgress_ResourceInstanceStatus_)(nil), + (*StackChangeProgress_ResourceInstancePlannedChange_)(nil), + (*StackChangeProgress_ProvisionerStatus_)(nil), + (*StackChangeProgress_ProvisionerOutput_)(nil), + (*StackChangeProgress_ComponentInstanceChanges_)(nil), + (*StackChangeProgress_ComponentInstances_)(nil), + (*StackChangeProgress_DeferredResourceInstancePlannedChange_)(nil), + } + file_stacks_proto_msgTypes[27].OneofWrappers = []any{ + (*OpenTerraformState_Request_ConfigPath)(nil), + (*OpenTerraformState_Request_Raw)(nil), + } + file_stacks_proto_msgTypes[31].OneofWrappers = []any{ + (*MigrateTerraformState_Request_Simple)(nil), + } + file_stacks_proto_msgTypes[32].OneofWrappers = []any{ + (*MigrateTerraformState_Event_Diagnostic)(nil), + (*MigrateTerraformState_Event_AppliedChange)(nil), + } + file_stacks_proto_msgTypes[61].OneofWrappers = []any{ + (*PlanStackChanges_Event_PlannedChange)(nil), + (*PlanStackChanges_Event_Diagnostic)(nil), + (*PlanStackChanges_Event_Progress)(nil), + } + file_stacks_proto_msgTypes[69].OneofWrappers = []any{ + (*ApplyStackChanges_Event_AppliedChange)(nil), + (*ApplyStackChanges_Event_Diagnostic)(nil), + (*ApplyStackChanges_Event_Progress)(nil), + } + file_stacks_proto_msgTypes[77].OneofWrappers = []any{ + (*AttributePath_Step_AttributeName)(nil), + (*AttributePath_Step_ElementKeyString)(nil), + (*AttributePath_Step_ElementKeyInt)(nil), + } + file_stacks_proto_msgTypes[78].OneofWrappers = []any{ + (*PlannedChange_ChangeDescription_ComponentInstancePlanned)(nil), + (*PlannedChange_ChangeDescription_ResourceInstancePlanned)(nil), + (*PlannedChange_ChangeDescription_OutputValuePlanned)(nil), + (*PlannedChange_ChangeDescription_PlanApplyable)(nil), + (*PlannedChange_ChangeDescription_ResourceInstanceDeferred)(nil), + (*PlannedChange_ChangeDescription_InputVariablePlanned)(nil), + } + file_stacks_proto_msgTypes[88].OneofWrappers = []any{ + (*AppliedChange_ChangeDescription_Deleted)(nil), + (*AppliedChange_ChangeDescription_Moved)(nil), + (*AppliedChange_ChangeDescription_ResourceInstance)(nil), + (*AppliedChange_ChangeDescription_OutputValue)(nil), + (*AppliedChange_ChangeDescription_InputVariable)(nil), + (*AppliedChange_ChangeDescription_ComponentInstance)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_stacks_proto_rawDesc), len(file_stacks_proto_rawDesc)), + NumEnums: 8, + NumMessages: 108, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_stacks_proto_goTypes, + DependencyIndexes: file_stacks_proto_depIdxs, + EnumInfos: file_stacks_proto_enumTypes, + MessageInfos: file_stacks_proto_msgTypes, + }.Build() + File_stacks_proto = out.File + file_stacks_proto_goTypes = nil + file_stacks_proto_depIdxs = nil +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConnInterface + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion6 + +// StacksClient is the client API for Stacks service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type StacksClient interface { + // Load and perform initial static validation of a stack configuration + // in a previously-opened source bundle. If successful, returns a + // stack configuration handle that can be used with other operations. + OpenStackConfiguration(ctx context.Context, in *OpenStackConfiguration_Request, opts ...grpc.CallOption) (*OpenStackConfiguration_Response, error) + // Close a previously-opened stack configuration using its handle. + CloseStackConfiguration(ctx context.Context, in *CloseStackConfiguration_Request, opts ...grpc.CallOption) (*CloseStackConfiguration_Response, error) + // Validate an open stack configuration. + ValidateStackConfiguration(ctx context.Context, in *ValidateStackConfiguration_Request, opts ...grpc.CallOption) (*ValidateStackConfiguration_Response, error) + // Analyze a stack configuration to find all of the components it declares. + // This is static analysis only, so it cannot produce dynamic information + // such as the number of instances of each component. + FindStackConfigurationComponents(ctx context.Context, in *FindStackConfigurationComponents_Request, opts ...grpc.CallOption) (*FindStackConfigurationComponents_Response, error) + // Load a stack state by sending a stream of raw state objects that were + // streamed from a previous ApplyStackChanges response. + OpenState(ctx context.Context, opts ...grpc.CallOption) (Stacks_OpenStateClient, error) + // Close a stack state handle, discarding the associated state. + CloseState(ctx context.Context, in *CloseStackState_Request, opts ...grpc.CallOption) (*CloseStackState_Response, error) + // Calculate a desired state from the given configuration and compare it + // with the current state to propose a set of changes to converge the + // current state with the desired state, at least in part. + PlanStackChanges(ctx context.Context, in *PlanStackChanges_Request, opts ...grpc.CallOption) (Stacks_PlanStackChangesClient, error) + // Load a previously-created plan by sending a stream of raw change objects + // that were streamed from a previous PlanStackChanges response. + OpenPlan(ctx context.Context, opts ...grpc.CallOption) (Stacks_OpenPlanClient, error) + // Close a saved plan handle, discarding the associated saved plan. + ClosePlan(ctx context.Context, in *CloseStackPlan_Request, opts ...grpc.CallOption) (*CloseStackPlan_Response, error) + // Execute the changes proposed by an earlier call to PlanStackChanges. + ApplyStackChanges(ctx context.Context, in *ApplyStackChanges_Request, opts ...grpc.CallOption) (Stacks_ApplyStackChangesClient, error) + // OpenStackInspector creates a stack inspector handle that can be used + // with subsequent calls to the "Inspect"-prefixed functions. + OpenStackInspector(ctx context.Context, in *OpenStackInspector_Request, opts ...grpc.CallOption) (*OpenStackInspector_Response, error) + // InspectExpressionResult evaluates an arbitrary expression in the context + // of a stack inspector handle. + InspectExpressionResult(ctx context.Context, in *InspectExpressionResult_Request, opts ...grpc.CallOption) (*InspectExpressionResult_Response, error) + // Open a previously-saved Terraform state, returning a handle that can be + // used with other operations. This is distinct from OpenState because it + // means core state rather than stack state. + OpenTerraformState(ctx context.Context, in *OpenTerraformState_Request, opts ...grpc.CallOption) (*OpenTerraformState_Response, error) + // Close a previously-opened Terraform state using its handle. + CloseTerraformState(ctx context.Context, in *CloseTerraformState_Request, opts ...grpc.CallOption) (*CloseTerraformState_Response, error) + // MigrateTerraformState migrates a Terraform state into Stacks state using + // a mapping of addresses. + MigrateTerraformState(ctx context.Context, in *MigrateTerraformState_Request, opts ...grpc.CallOption) (Stacks_MigrateTerraformStateClient, error) + // ListResourceIdentities lists the identities of all resources in a stack. + ListResourceIdentities(ctx context.Context, in *ListResourceIdentities_Request, opts ...grpc.CallOption) (*ListResourceIdentities_Response, error) +} + +type stacksClient struct { + cc grpc.ClientConnInterface +} + +func NewStacksClient(cc grpc.ClientConnInterface) StacksClient { + return &stacksClient{cc} +} + +func (c *stacksClient) OpenStackConfiguration(ctx context.Context, in *OpenStackConfiguration_Request, opts ...grpc.CallOption) (*OpenStackConfiguration_Response, error) { + out := new(OpenStackConfiguration_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/OpenStackConfiguration", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) CloseStackConfiguration(ctx context.Context, in *CloseStackConfiguration_Request, opts ...grpc.CallOption) (*CloseStackConfiguration_Response, error) { + out := new(CloseStackConfiguration_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/CloseStackConfiguration", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) ValidateStackConfiguration(ctx context.Context, in *ValidateStackConfiguration_Request, opts ...grpc.CallOption) (*ValidateStackConfiguration_Response, error) { + out := new(ValidateStackConfiguration_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/ValidateStackConfiguration", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) FindStackConfigurationComponents(ctx context.Context, in *FindStackConfigurationComponents_Request, opts ...grpc.CallOption) (*FindStackConfigurationComponents_Response, error) { + out := new(FindStackConfigurationComponents_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/FindStackConfigurationComponents", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) OpenState(ctx context.Context, opts ...grpc.CallOption) (Stacks_OpenStateClient, error) { + stream, err := c.cc.NewStream(ctx, &_Stacks_serviceDesc.Streams[0], "/terraform1.stacks.Stacks/OpenState", opts...) + if err != nil { + return nil, err + } + x := &stacksOpenStateClient{stream} + return x, nil +} + +type Stacks_OpenStateClient interface { + Send(*OpenStackState_RequestItem) error + CloseAndRecv() (*OpenStackState_Response, error) + grpc.ClientStream +} + +type stacksOpenStateClient struct { + grpc.ClientStream +} + +func (x *stacksOpenStateClient) Send(m *OpenStackState_RequestItem) error { + return x.ClientStream.SendMsg(m) +} + +func (x *stacksOpenStateClient) CloseAndRecv() (*OpenStackState_Response, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(OpenStackState_Response) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *stacksClient) CloseState(ctx context.Context, in *CloseStackState_Request, opts ...grpc.CallOption) (*CloseStackState_Response, error) { + out := new(CloseStackState_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/CloseState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) PlanStackChanges(ctx context.Context, in *PlanStackChanges_Request, opts ...grpc.CallOption) (Stacks_PlanStackChangesClient, error) { + stream, err := c.cc.NewStream(ctx, &_Stacks_serviceDesc.Streams[1], "/terraform1.stacks.Stacks/PlanStackChanges", opts...) + if err != nil { + return nil, err + } + x := &stacksPlanStackChangesClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Stacks_PlanStackChangesClient interface { + Recv() (*PlanStackChanges_Event, error) + grpc.ClientStream +} + +type stacksPlanStackChangesClient struct { + grpc.ClientStream +} + +func (x *stacksPlanStackChangesClient) Recv() (*PlanStackChanges_Event, error) { + m := new(PlanStackChanges_Event) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *stacksClient) OpenPlan(ctx context.Context, opts ...grpc.CallOption) (Stacks_OpenPlanClient, error) { + stream, err := c.cc.NewStream(ctx, &_Stacks_serviceDesc.Streams[2], "/terraform1.stacks.Stacks/OpenPlan", opts...) + if err != nil { + return nil, err + } + x := &stacksOpenPlanClient{stream} + return x, nil +} + +type Stacks_OpenPlanClient interface { + Send(*OpenStackPlan_RequestItem) error + CloseAndRecv() (*OpenStackPlan_Response, error) + grpc.ClientStream +} + +type stacksOpenPlanClient struct { + grpc.ClientStream +} + +func (x *stacksOpenPlanClient) Send(m *OpenStackPlan_RequestItem) error { + return x.ClientStream.SendMsg(m) +} + +func (x *stacksOpenPlanClient) CloseAndRecv() (*OpenStackPlan_Response, error) { + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + m := new(OpenStackPlan_Response) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *stacksClient) ClosePlan(ctx context.Context, in *CloseStackPlan_Request, opts ...grpc.CallOption) (*CloseStackPlan_Response, error) { + out := new(CloseStackPlan_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/ClosePlan", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) ApplyStackChanges(ctx context.Context, in *ApplyStackChanges_Request, opts ...grpc.CallOption) (Stacks_ApplyStackChangesClient, error) { + stream, err := c.cc.NewStream(ctx, &_Stacks_serviceDesc.Streams[3], "/terraform1.stacks.Stacks/ApplyStackChanges", opts...) + if err != nil { + return nil, err + } + x := &stacksApplyStackChangesClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Stacks_ApplyStackChangesClient interface { + Recv() (*ApplyStackChanges_Event, error) + grpc.ClientStream +} + +type stacksApplyStackChangesClient struct { + grpc.ClientStream +} + +func (x *stacksApplyStackChangesClient) Recv() (*ApplyStackChanges_Event, error) { + m := new(ApplyStackChanges_Event) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *stacksClient) OpenStackInspector(ctx context.Context, in *OpenStackInspector_Request, opts ...grpc.CallOption) (*OpenStackInspector_Response, error) { + out := new(OpenStackInspector_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/OpenStackInspector", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) InspectExpressionResult(ctx context.Context, in *InspectExpressionResult_Request, opts ...grpc.CallOption) (*InspectExpressionResult_Response, error) { + out := new(InspectExpressionResult_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/InspectExpressionResult", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) OpenTerraformState(ctx context.Context, in *OpenTerraformState_Request, opts ...grpc.CallOption) (*OpenTerraformState_Response, error) { + out := new(OpenTerraformState_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/OpenTerraformState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) CloseTerraformState(ctx context.Context, in *CloseTerraformState_Request, opts ...grpc.CallOption) (*CloseTerraformState_Response, error) { + out := new(CloseTerraformState_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/CloseTerraformState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *stacksClient) MigrateTerraformState(ctx context.Context, in *MigrateTerraformState_Request, opts ...grpc.CallOption) (Stacks_MigrateTerraformStateClient, error) { + stream, err := c.cc.NewStream(ctx, &_Stacks_serviceDesc.Streams[4], "/terraform1.stacks.Stacks/MigrateTerraformState", opts...) + if err != nil { + return nil, err + } + x := &stacksMigrateTerraformStateClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type Stacks_MigrateTerraformStateClient interface { + Recv() (*MigrateTerraformState_Event, error) + grpc.ClientStream +} + +type stacksMigrateTerraformStateClient struct { + grpc.ClientStream +} + +func (x *stacksMigrateTerraformStateClient) Recv() (*MigrateTerraformState_Event, error) { + m := new(MigrateTerraformState_Event) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func (c *stacksClient) ListResourceIdentities(ctx context.Context, in *ListResourceIdentities_Request, opts ...grpc.CallOption) (*ListResourceIdentities_Response, error) { + out := new(ListResourceIdentities_Response) + err := c.cc.Invoke(ctx, "/terraform1.stacks.Stacks/ListResourceIdentities", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// StacksServer is the server API for Stacks service. +type StacksServer interface { + // Load and perform initial static validation of a stack configuration + // in a previously-opened source bundle. If successful, returns a + // stack configuration handle that can be used with other operations. + OpenStackConfiguration(context.Context, *OpenStackConfiguration_Request) (*OpenStackConfiguration_Response, error) + // Close a previously-opened stack configuration using its handle. + CloseStackConfiguration(context.Context, *CloseStackConfiguration_Request) (*CloseStackConfiguration_Response, error) + // Validate an open stack configuration. + ValidateStackConfiguration(context.Context, *ValidateStackConfiguration_Request) (*ValidateStackConfiguration_Response, error) + // Analyze a stack configuration to find all of the components it declares. + // This is static analysis only, so it cannot produce dynamic information + // such as the number of instances of each component. + FindStackConfigurationComponents(context.Context, *FindStackConfigurationComponents_Request) (*FindStackConfigurationComponents_Response, error) + // Load a stack state by sending a stream of raw state objects that were + // streamed from a previous ApplyStackChanges response. + OpenState(Stacks_OpenStateServer) error + // Close a stack state handle, discarding the associated state. + CloseState(context.Context, *CloseStackState_Request) (*CloseStackState_Response, error) + // Calculate a desired state from the given configuration and compare it + // with the current state to propose a set of changes to converge the + // current state with the desired state, at least in part. + PlanStackChanges(*PlanStackChanges_Request, Stacks_PlanStackChangesServer) error + // Load a previously-created plan by sending a stream of raw change objects + // that were streamed from a previous PlanStackChanges response. + OpenPlan(Stacks_OpenPlanServer) error + // Close a saved plan handle, discarding the associated saved plan. + ClosePlan(context.Context, *CloseStackPlan_Request) (*CloseStackPlan_Response, error) + // Execute the changes proposed by an earlier call to PlanStackChanges. + ApplyStackChanges(*ApplyStackChanges_Request, Stacks_ApplyStackChangesServer) error + // OpenStackInspector creates a stack inspector handle that can be used + // with subsequent calls to the "Inspect"-prefixed functions. + OpenStackInspector(context.Context, *OpenStackInspector_Request) (*OpenStackInspector_Response, error) + // InspectExpressionResult evaluates an arbitrary expression in the context + // of a stack inspector handle. + InspectExpressionResult(context.Context, *InspectExpressionResult_Request) (*InspectExpressionResult_Response, error) + // Open a previously-saved Terraform state, returning a handle that can be + // used with other operations. This is distinct from OpenState because it + // means core state rather than stack state. + OpenTerraformState(context.Context, *OpenTerraformState_Request) (*OpenTerraformState_Response, error) + // Close a previously-opened Terraform state using its handle. + CloseTerraformState(context.Context, *CloseTerraformState_Request) (*CloseTerraformState_Response, error) + // MigrateTerraformState migrates a Terraform state into Stacks state using + // a mapping of addresses. + MigrateTerraformState(*MigrateTerraformState_Request, Stacks_MigrateTerraformStateServer) error + // ListResourceIdentities lists the identities of all resources in a stack. + ListResourceIdentities(context.Context, *ListResourceIdentities_Request) (*ListResourceIdentities_Response, error) +} + +// UnimplementedStacksServer can be embedded to have forward compatible implementations. +type UnimplementedStacksServer struct { +} + +func (*UnimplementedStacksServer) OpenStackConfiguration(context.Context, *OpenStackConfiguration_Request) (*OpenStackConfiguration_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenStackConfiguration not implemented") +} +func (*UnimplementedStacksServer) CloseStackConfiguration(context.Context, *CloseStackConfiguration_Request) (*CloseStackConfiguration_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseStackConfiguration not implemented") +} +func (*UnimplementedStacksServer) ValidateStackConfiguration(context.Context, *ValidateStackConfiguration_Request) (*ValidateStackConfiguration_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ValidateStackConfiguration not implemented") +} +func (*UnimplementedStacksServer) FindStackConfigurationComponents(context.Context, *FindStackConfigurationComponents_Request) (*FindStackConfigurationComponents_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method FindStackConfigurationComponents not implemented") +} +func (*UnimplementedStacksServer) OpenState(Stacks_OpenStateServer) error { + return status.Errorf(codes.Unimplemented, "method OpenState not implemented") +} +func (*UnimplementedStacksServer) CloseState(context.Context, *CloseStackState_Request) (*CloseStackState_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseState not implemented") +} +func (*UnimplementedStacksServer) PlanStackChanges(*PlanStackChanges_Request, Stacks_PlanStackChangesServer) error { + return status.Errorf(codes.Unimplemented, "method PlanStackChanges not implemented") +} +func (*UnimplementedStacksServer) OpenPlan(Stacks_OpenPlanServer) error { + return status.Errorf(codes.Unimplemented, "method OpenPlan not implemented") +} +func (*UnimplementedStacksServer) ClosePlan(context.Context, *CloseStackPlan_Request) (*CloseStackPlan_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ClosePlan not implemented") +} +func (*UnimplementedStacksServer) ApplyStackChanges(*ApplyStackChanges_Request, Stacks_ApplyStackChangesServer) error { + return status.Errorf(codes.Unimplemented, "method ApplyStackChanges not implemented") +} +func (*UnimplementedStacksServer) OpenStackInspector(context.Context, *OpenStackInspector_Request) (*OpenStackInspector_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenStackInspector not implemented") +} +func (*UnimplementedStacksServer) InspectExpressionResult(context.Context, *InspectExpressionResult_Request) (*InspectExpressionResult_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method InspectExpressionResult not implemented") +} +func (*UnimplementedStacksServer) OpenTerraformState(context.Context, *OpenTerraformState_Request) (*OpenTerraformState_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenTerraformState not implemented") +} +func (*UnimplementedStacksServer) CloseTerraformState(context.Context, *CloseTerraformState_Request) (*CloseTerraformState_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseTerraformState not implemented") +} +func (*UnimplementedStacksServer) MigrateTerraformState(*MigrateTerraformState_Request, Stacks_MigrateTerraformStateServer) error { + return status.Errorf(codes.Unimplemented, "method MigrateTerraformState not implemented") +} +func (*UnimplementedStacksServer) ListResourceIdentities(context.Context, *ListResourceIdentities_Request) (*ListResourceIdentities_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListResourceIdentities not implemented") +} + +func RegisterStacksServer(s *grpc.Server, srv StacksServer) { + s.RegisterService(&_Stacks_serviceDesc, srv) +} + +func _Stacks_OpenStackConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenStackConfiguration_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).OpenStackConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/OpenStackConfiguration", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).OpenStackConfiguration(ctx, req.(*OpenStackConfiguration_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_CloseStackConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseStackConfiguration_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).CloseStackConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/CloseStackConfiguration", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).CloseStackConfiguration(ctx, req.(*CloseStackConfiguration_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_ValidateStackConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateStackConfiguration_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).ValidateStackConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/ValidateStackConfiguration", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).ValidateStackConfiguration(ctx, req.(*ValidateStackConfiguration_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_FindStackConfigurationComponents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(FindStackConfigurationComponents_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).FindStackConfigurationComponents(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/FindStackConfigurationComponents", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).FindStackConfigurationComponents(ctx, req.(*FindStackConfigurationComponents_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_OpenState_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StacksServer).OpenState(&stacksOpenStateServer{stream}) +} + +type Stacks_OpenStateServer interface { + SendAndClose(*OpenStackState_Response) error + Recv() (*OpenStackState_RequestItem, error) + grpc.ServerStream +} + +type stacksOpenStateServer struct { + grpc.ServerStream +} + +func (x *stacksOpenStateServer) SendAndClose(m *OpenStackState_Response) error { + return x.ServerStream.SendMsg(m) +} + +func (x *stacksOpenStateServer) Recv() (*OpenStackState_RequestItem, error) { + m := new(OpenStackState_RequestItem) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _Stacks_CloseState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseStackState_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).CloseState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/CloseState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).CloseState(ctx, req.(*CloseStackState_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_PlanStackChanges_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(PlanStackChanges_Request) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StacksServer).PlanStackChanges(m, &stacksPlanStackChangesServer{stream}) +} + +type Stacks_PlanStackChangesServer interface { + Send(*PlanStackChanges_Event) error + grpc.ServerStream +} + +type stacksPlanStackChangesServer struct { + grpc.ServerStream +} + +func (x *stacksPlanStackChangesServer) Send(m *PlanStackChanges_Event) error { + return x.ServerStream.SendMsg(m) +} + +func _Stacks_OpenPlan_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StacksServer).OpenPlan(&stacksOpenPlanServer{stream}) +} + +type Stacks_OpenPlanServer interface { + SendAndClose(*OpenStackPlan_Response) error + Recv() (*OpenStackPlan_RequestItem, error) + grpc.ServerStream +} + +type stacksOpenPlanServer struct { + grpc.ServerStream +} + +func (x *stacksOpenPlanServer) SendAndClose(m *OpenStackPlan_Response) error { + return x.ServerStream.SendMsg(m) +} + +func (x *stacksOpenPlanServer) Recv() (*OpenStackPlan_RequestItem, error) { + m := new(OpenStackPlan_RequestItem) + if err := x.ServerStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + +func _Stacks_ClosePlan_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseStackPlan_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).ClosePlan(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/ClosePlan", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).ClosePlan(ctx, req.(*CloseStackPlan_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_ApplyStackChanges_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(ApplyStackChanges_Request) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StacksServer).ApplyStackChanges(m, &stacksApplyStackChangesServer{stream}) +} + +type Stacks_ApplyStackChangesServer interface { + Send(*ApplyStackChanges_Event) error + grpc.ServerStream +} + +type stacksApplyStackChangesServer struct { + grpc.ServerStream +} + +func (x *stacksApplyStackChangesServer) Send(m *ApplyStackChanges_Event) error { + return x.ServerStream.SendMsg(m) +} + +func _Stacks_OpenStackInspector_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenStackInspector_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).OpenStackInspector(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/OpenStackInspector", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).OpenStackInspector(ctx, req.(*OpenStackInspector_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_InspectExpressionResult_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(InspectExpressionResult_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).InspectExpressionResult(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/InspectExpressionResult", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).InspectExpressionResult(ctx, req.(*InspectExpressionResult_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_OpenTerraformState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenTerraformState_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).OpenTerraformState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/OpenTerraformState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).OpenTerraformState(ctx, req.(*OpenTerraformState_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_CloseTerraformState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseTerraformState_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).CloseTerraformState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/CloseTerraformState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).CloseTerraformState(ctx, req.(*CloseTerraformState_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Stacks_MigrateTerraformState_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(MigrateTerraformState_Request) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StacksServer).MigrateTerraformState(m, &stacksMigrateTerraformStateServer{stream}) +} + +type Stacks_MigrateTerraformStateServer interface { + Send(*MigrateTerraformState_Event) error + grpc.ServerStream +} + +type stacksMigrateTerraformStateServer struct { + grpc.ServerStream +} + +func (x *stacksMigrateTerraformStateServer) Send(m *MigrateTerraformState_Event) error { + return x.ServerStream.SendMsg(m) +} + +func _Stacks_ListResourceIdentities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListResourceIdentities_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StacksServer).ListResourceIdentities(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/terraform1.stacks.Stacks/ListResourceIdentities", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StacksServer).ListResourceIdentities(ctx, req.(*ListResourceIdentities_Request)) + } + return interceptor(ctx, in, info, handler) +} + +var _Stacks_serviceDesc = grpc.ServiceDesc{ + ServiceName: "terraform1.stacks.Stacks", + HandlerType: (*StacksServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "OpenStackConfiguration", + Handler: _Stacks_OpenStackConfiguration_Handler, + }, + { + MethodName: "CloseStackConfiguration", + Handler: _Stacks_CloseStackConfiguration_Handler, + }, + { + MethodName: "ValidateStackConfiguration", + Handler: _Stacks_ValidateStackConfiguration_Handler, + }, + { + MethodName: "FindStackConfigurationComponents", + Handler: _Stacks_FindStackConfigurationComponents_Handler, + }, + { + MethodName: "CloseState", + Handler: _Stacks_CloseState_Handler, + }, + { + MethodName: "ClosePlan", + Handler: _Stacks_ClosePlan_Handler, + }, + { + MethodName: "OpenStackInspector", + Handler: _Stacks_OpenStackInspector_Handler, + }, + { + MethodName: "InspectExpressionResult", + Handler: _Stacks_InspectExpressionResult_Handler, + }, + { + MethodName: "OpenTerraformState", + Handler: _Stacks_OpenTerraformState_Handler, + }, + { + MethodName: "CloseTerraformState", + Handler: _Stacks_CloseTerraformState_Handler, + }, + { + MethodName: "ListResourceIdentities", + Handler: _Stacks_ListResourceIdentities_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "OpenState", + Handler: _Stacks_OpenState_Handler, + ClientStreams: true, + }, + { + StreamName: "PlanStackChanges", + Handler: _Stacks_PlanStackChanges_Handler, + ServerStreams: true, + }, + { + StreamName: "OpenPlan", + Handler: _Stacks_OpenPlan_Handler, + ClientStreams: true, + }, + { + StreamName: "ApplyStackChanges", + Handler: _Stacks_ApplyStackChanges_Handler, + ServerStreams: true, + }, + { + StreamName: "MigrateTerraformState", + Handler: _Stacks_MigrateTerraformState_Handler, + ServerStreams: true, + }, + }, + Metadata: "stacks.proto", +} diff --git a/internal/rpcapi/terraform1/stacks/stacks.proto b/internal/rpcapi/terraform1/stacks/stacks.proto new file mode 100644 index 0000000000..8a34f3e733 --- /dev/null +++ b/internal/rpcapi/terraform1/stacks/stacks.proto @@ -0,0 +1,927 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package terraform1.stacks; + +import "google/protobuf/any.proto"; +import "terraform1.proto"; + + +service Stacks { + // Load and perform initial static validation of a stack configuration + // in a previously-opened source bundle. If successful, returns a + // stack configuration handle that can be used with other operations. + rpc OpenStackConfiguration(OpenStackConfiguration.Request) + returns (OpenStackConfiguration.Response); + // Close a previously-opened stack configuration using its handle. + rpc CloseStackConfiguration(CloseStackConfiguration.Request) + returns (CloseStackConfiguration.Response); + // Validate an open stack configuration. + rpc ValidateStackConfiguration(ValidateStackConfiguration.Request) + returns (ValidateStackConfiguration.Response); + // Analyze a stack configuration to find all of the components it declares. + // This is static analysis only, so it cannot produce dynamic information + // such as the number of instances of each component. + rpc FindStackConfigurationComponents(FindStackConfigurationComponents.Request) + returns (FindStackConfigurationComponents.Response); + // Load a stack state by sending a stream of raw state objects that were + // streamed from a previous ApplyStackChanges response. + rpc OpenState(stream OpenStackState.RequestItem) returns (OpenStackState.Response); + // Close a stack state handle, discarding the associated state. + rpc CloseState(CloseStackState.Request) returns (CloseStackState.Response); + // Calculate a desired state from the given configuration and compare it + // with the current state to propose a set of changes to converge the + // current state with the desired state, at least in part. + rpc PlanStackChanges(PlanStackChanges.Request) + returns (stream PlanStackChanges.Event); + // Load a previously-created plan by sending a stream of raw change objects + // that were streamed from a previous PlanStackChanges response. + rpc OpenPlan(stream OpenStackPlan.RequestItem) returns (OpenStackPlan.Response); + // Close a saved plan handle, discarding the associated saved plan. + rpc ClosePlan(CloseStackPlan.Request) returns (CloseStackPlan.Response); + // Execute the changes proposed by an earlier call to PlanStackChanges. + rpc ApplyStackChanges(ApplyStackChanges.Request) + returns (stream ApplyStackChanges.Event); + // OpenStackInspector creates a stack inspector handle that can be used + // with subsequent calls to the "Inspect"-prefixed functions. + rpc OpenStackInspector(OpenStackInspector.Request) + returns (OpenStackInspector.Response); + // InspectExpressionResult evaluates an arbitrary expression in the context + // of a stack inspector handle. + rpc InspectExpressionResult(InspectExpressionResult.Request) + returns (InspectExpressionResult.Response); + // Open a previously-saved Terraform state, returning a handle that can be + // used with other operations. This is distinct from OpenState because it + // means core state rather than stack state. + rpc OpenTerraformState(OpenTerraformState.Request) returns (OpenTerraformState.Response); + // Close a previously-opened Terraform state using its handle. + rpc CloseTerraformState(CloseTerraformState.Request) returns (CloseTerraformState.Response); + // MigrateTerraformState migrates a Terraform state into Stacks state using + // a mapping of addresses. + rpc MigrateTerraformState(MigrateTerraformState.Request) returns (stream MigrateTerraformState.Event); + // ListResourceIdentities lists the identities of all resources in a stack. + rpc ListResourceIdentities(ListResourceIdentities.Request) returns (ListResourceIdentities.Response); +} + +// OpenTerraformState opens a previously-saved Terraform state, returning a +// handle that can be used with other operations. This is distinct from +// OpenState because it means core state rather than stack state. +message OpenTerraformState { + message Request { + oneof state { + // We can open a state based on configuration that has been initialized. + string config_path = 1; + + // Or a state file based on raw bytes. + bytes raw = 2; + } + } + message Response { + int64 state_handle = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +// CloseTerraformState closes a previously-opened Terraform state using its +// handle. +message CloseTerraformState { + message Request { + int64 state_handle = 1; + } + message Response { + } +} + +// MigrateTerraformState migrates a Terraform state into Stacks state using +// a mapping of addresses. +// +// Only resources and modules from the root module should be specified. All +// resources in nested modules maintain their nested structure within the new +// components the base modules were moved into. +message MigrateTerraformState { + message Request { + int64 state_handle = 1; // previously opened Terraform state + int64 config_handle = 2; // new stacks configuration + int64 dependency_locks_handle = 3; + int64 provider_cache_handle = 4; + + // Mapping of terraform constructs to stack components. + message Mapping { + // resource_address_map maps resources in the root module to their new + // components. The keys are the addresses of the resources in the Terraform + // state, and the values are the names of the new components. + // + // eg. resource_type.resource_name -> component_name + map resource_address_map = 1; + // module_address_map maps modules in the root module to their new + // components. The keys are the module names in the Terraform state, and + // the values are the names of the new components. + // + // eg. module_name -> component_name + map module_address_map = 2; + } + + oneof mapping { + // simple is a simple mapping of Terraform addresses to stack components + Mapping simple = 5; + } + + } + message Event { + oneof result { + terraform1.Diagnostic diagnostic = 1; + AppliedChange applied_change = 2; + } + } +} + +message OpenStackConfiguration { + message Request { + int64 source_bundle_handle = 1; + terraform1.SourceAddress source_address = 2; + } + message Response { + int64 stack_config_handle = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message CloseStackConfiguration { + message Request { + int64 stack_config_handle = 1; + } + message Response { + } +} + +message ValidateStackConfiguration { + message Request { + int64 stack_config_handle = 1; + int64 dependency_locks_handle = 2; + int64 provider_cache_handle = 3; + } + message Response { + repeated terraform1.Diagnostic diagnostics = 1; + } +} + +message FindStackConfigurationComponents { + message Request { + int64 stack_config_handle = 1; + } + message Response { + StackConfig config = 1; + } + + enum Instances { + SINGLE = 0; + COUNT = 1; + FOR_EACH = 2; + } + message StackConfig { + map components = 1; + map embedded_stacks = 2; + map input_variables = 3; + map output_values = 4; + map removed = 5; + } + message EmbeddedStack { + string source_addr = 1; + Instances instances = 2; + StackConfig config = 3; + } + message Component { + string source_addr = 1; + Instances instances = 2; + string component_addr = 3; + } + message Removed { + string source_addr = 1 [deprecated = true]; + Instances instances = 2 [deprecated = true]; + string component_addr = 3 [deprecated = true]; + bool destroy = 4 [deprecated = true]; + + message Block { + string source_addr = 1; + Instances instances = 2; + string component_addr = 3; + bool destroy = 4; + } + repeated Block blocks = 5; + } + message InputVariable { + bool optional = 1; + bool sensitive = 2; + bool ephemeral = 3; + } + message OutputValue { + bool sensitive = 1; + bool ephemeral = 2; + } +} + +message OpenStackState { + message RequestItem { + AppliedChange.RawChange raw = 1; + } + message Response { + int64 state_handle = 1; + } +} + +message CloseStackState { + message Request { + int64 state_handle = 1; + } + message Response { + } +} + +message PlanStackChanges { + message Request { + PlanMode plan_mode = 1; + int64 stack_config_handle = 2; + int64 previous_state_handle = 7; + map previous_state = 3 [deprecated = true]; + int64 dependency_locks_handle = 4; + int64 provider_cache_handle = 5; + map input_values = 6; + // TODO: Various other planning options + } + message Event { + oneof event { + PlannedChange planned_change = 1; + terraform1.Diagnostic diagnostic = 2; + StackChangeProgress progress = 10; + } + reserved 3 to 9; // formerly used for individual progress events + } +} + +message OpenStackPlan { + message RequestItem { + google.protobuf.Any raw = 1; + } + message Response { + int64 plan_handle = 1; + } +} + +message CloseStackPlan { + message Request { + int64 plan_handle = 1; + } + message Response { + } +} + +message ApplyStackChanges { + message Request { + // This must refer to exactly the same configuration that was + // passed to PlanStackChanges when creating this plan, or the + // results will be unpredictable. + int64 stack_config_handle = 1; + // The caller should send all of the keys present in the previous + // apply's description map. Terraform Core will use this for + // situations such as updating existing descriptions to newer + // formats even if no change is being made to the corresponding + // real objects. + repeated string known_description_keys = 3; + // The handle for a saved plan previously loaded using the + // Stacks.OpenPlan function. + // Applying a plan immediately invalidates it, so the handle will + // be automatically closed. + int64 plan_handle = 8; + // This must include all of the "raw" values emitted through + // PlannedChange events during the PlanStackChanges operation + // that created this plan, concatenated together in the same + // order they were written to the PlanStackChanges event stream. + // + // Use plan_handle instead. This will be removed in future. + repeated google.protobuf.Any planned_changes = 4 [deprecated = true]; + // This must be equivalent to the argument of the same name + // passed to PlanStackChanges when creating this plan. + int64 dependency_locks_handle = 5; + // This must be equivalent to the argument of the same name + // passed to PlanStackChanges when creating this plan. + int64 provider_cache_handle = 6; + + // Any input variables identified as an "apply-time input variable" + // in the plan must have values provided here. + // + // Callers may also optionally include values for other declared input + // variables, but if so their values must exactly match those used when + // creating the plan. + map input_values = 7; + + reserved 2; // (formerly the previous state, but we now propagate that as part of planned_changes as an implementation detail) + } + message Event { + oneof event { + AppliedChange applied_change = 1; + terraform1.Diagnostic diagnostic = 2; + StackChangeProgress progress = 3; + } + } +} + +message OpenStackInspector { + message Request { + int64 stack_config_handle = 1; + map state = 2; + int64 dependency_locks_handle = 3; + int64 provider_cache_handle = 4; + map input_values = 5; + } + message Response { + int64 stack_inspector_handle = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +message InspectExpressionResult { + message Request { + int64 stack_inspector_handle = 1; + bytes expression_src = 2; + string stack_addr = 3; + } + message Response { + // The result of evaluating the expression, if successful enough to + // produce a result. Unpopulated if the expression was too invalid + // to produce a result, with the problem then described in the + // associated diagnostics. + // + // Uses a MessagePack encoding with in-band type information. + DynamicValue result = 1; + repeated terraform1.Diagnostic diagnostics = 2; + } +} + +// Represents dynamically-typed data from within the Terraform language. +// Typically only one of the available serialization formats will be populated, +// depending on what serializations are appropriate for a particular context +// and what capabilities the client and the server negotiated during Handshake. +message DynamicValue { + bytes msgpack = 1; // The default serialization format + repeated AttributePath sensitive = 2; // Paths to any sensitive-marked values. +} + +// Represents a change of some object from one dynamic value to another. +message DynamicValueChange { + DynamicValue old = 1; + DynamicValue new = 2; +} + +// Represents a DynamicValue accompanied by a source location where it was +// presumably defined, for values that originated in configuration files for +// situations such as returning error messages. +message DynamicValueWithSource { + DynamicValue value = 1; + terraform1.SourceRange source_range = 2; +} + +message AttributePath { + message Step { + oneof selector { + // Set "attribute_name" to represent looking up an attribute + // in the current object value. + string attribute_name = 1; + // Set "element_key_*" to represent looking up an element in + // an indexable collection type. + string element_key_string = 2; + int64 element_key_int = 3; + } + } + repeated Step steps = 1; +} + +// Represents the address of a specific component instance within a stack. +message ComponentInstanceInStackAddr { + // The address of the static component that this is an instance of. + string component_addr = 1; + // The address of the instance that's being announced. For + // multi-instance components this could have any combination of + // instance keys on the component itself or instance keys on any + // of the containing embedded stacks. + string component_instance_addr = 2; +} + +// Represents the address of a specific resource instance inside a specific +// component instance within the containing stack. +message ResourceInstanceInStackAddr { + // Unique address of the component instance that this resource instance + // belongs to. This is comparable with + string component_instance_addr = 1; + // Unique address of the resource instance within the given component + // instance. Each component instance has a separate namespace of + // resource instance addresses, so callers must take both fields together + // to produce a key that's unique throughout the entire plan. + string resource_instance_addr = 2; +} + +// Represents the address of a specific resource instance object inside a +// specific component instance within the containing stack. +message ResourceInstanceObjectInStackAddr { + // Unique address of the component instance that this resource instance + // belongs to. This is comparable with + string component_instance_addr = 1; + // Unique address of the resource instance within the given component + // instance. Each component instance has a separate namespace of + // resource instance addresses, so callers must take both fields together + // to produce a key that's unique throughout the entire plan. + string resource_instance_addr = 2; + // Optional "deposed key" populated only for non-current (deposed) objects, + // which can appear for "create before destroy" replacements where the + // create succeeds but then the destroy fails, leaving us with two different + // objects to track for the same resource instance. + string deposed_key = 3; +} + +enum ResourceMode { + UNKNOWN = 0; + MANAGED = 1; + DATA = 2; +} + +enum PlanMode { + NORMAL = 0; + REFRESH_ONLY = 1; + DESTROY = 2; +} + +enum ChangeType { + NOOP = 0; + READ = 1; + CREATE = 2; + UPDATE = 3; + DELETE = 4; + FORGET = 5; +} + +// Describes one item in a stack plan. The overall plan is the concatentation +// of all messages of this type emitted as events during the plan; splitting +// this information over multiple messages just allows the individual events +// to double as progress notifications for an interactive UI. +message PlannedChange { + // Terraform Core's internal representation(s) of this change. Callers + // must provide the messages in this field, if any, verbatim to the + // ApplyStackChanges RPC in order to apply this change, and must not + // attempt to decode or analyze the contents because they are subject + // to change in future versions of Terraform Core. + // + // This might be unpopulated if this message represents only information + // for the caller and Terraform Core doesn't actually need to recall this + // information during the apply step. Callers must append each raw item + // to the raw plan in the order specified, and provide them all together + // in the same order to ApplyStackChanges. + repeated google.protobuf.Any raw = 1; + + // Caller-facing descriptions of this change, to use for presenting + // information to end-users in the UI and for other subsystems such as + // imposing policy rules on the resulting plan. + // + // There can be zero or more description objects associated with each + // change. More than one is not common, but should be supported by clients + // by treating them the same way as if each description had arrived in + // a separate PlannedChange message. Clients should not treat the grouping + // or not-grouping of change description objects as meaningful information, + // since it's subject to change in future Terraform Core versions. + // + // DO NOT attempt to use this to surgically filter particular changes + // from a larger plan. Although external descriptions often match with + // the raw representations in field "raw", that is not guaranteed and + // Terraform Core assumes that it will always be provided with the full + // set of raw messages -- in the same order they were emitted -- during + // the apply step. For example, some raw messages might omit information + // that is implied by earlier raw messages and would therefore be + // incomplete if isolated. + repeated ChangeDescription descriptions = 2; + reserved 3 to 6; // formerly used for an inline "oneof description", now factored out into a separate message type + + // Represents a single caller-facing description of a change, to use for + // presenting information to end users in the UI and for other subsystems + // such as imposing policy rules on the resulting plan. + // + // New description types might be added in future versions of Terraform + // Core, and so clients should tolerate description messages that appear + // to have none of the oneof fields set, and should just ignore those + // messages entirely. + message ChangeDescription { + oneof description { + ComponentInstance component_instance_planned = 1; + ResourceInstance resource_instance_planned = 2; + OutputValue output_value_planned = 3; + bool plan_applyable = 4; + ResourceInstanceDeferred resource_instance_deferred = 5; + InputVariable input_variable_planned = 6; + } + } + + // Reports the existence of a particular instance of a component, + // once Terraform has resolved arguments such as "for_each" that + // might make the set of instances dynamic. + message ComponentInstance { + ComponentInstanceInStackAddr addr = 1; + // The changes to the existence of this instance relative to the + // prior state. This only considers the component instance directly, + // and doesn't take into account what actions are planned for any + // resource instances inside. + repeated ChangeType actions = 2; + // A flag for whether applying this plan is expected to cause the + // desired state and actual state to become converged. + // + // If this field is false, that means Terraform expects that at least + // one more plan/apply round will be needed to reach convergence. + // + // If this field is true then Terraform hopes to be able to converge + // after this plan is applied, but callers should ideally still check + // anyway by running one more plan to confirm that there aren't any + // unexpected differences caused by such situations as contradictory + // configuration or provider bugs. + bool plan_complete = 3; + } + message ResourceInstance { + ResourceInstanceObjectInStackAddr addr = 1; + repeated ChangeType actions = 2; + DynamicValueChange values = 3; + Moved moved = 4; + Imported imported = 5; + ResourceMode resource_mode = 6; + string resource_type = 7; + string provider_addr = 8; + + // previous_run_value is included only if it would be + // different from values.old, which typically means that + // Terraform detected some changes made outside of Terraform + // since the previous run. In that case, this field is + // the un-refreshed (but still upgraded) value from + // the previous run and values.old is the refreshed version. + // + // If this isn't set then values.old should be used as the + // previous run value, if needed. + DynamicValue previous_run_value = 9; + + // This flag is set if Terraform Core considers the difference + // between previous_run_value and values.old to be "notable", + // which is a heuristic subject to change over time but is + // broadly intended to mean that it would be worth mentioning + // the difference between the two in the UI as a + // "change outside of Terraform". If this isn't set then the + // difference is probably not worth mentioning to the user + // by default, although it could still be shown behind an + // optional disclosure in UI contexts where such things are possible. + bool notable_change_outside = 10; + + repeated AttributePath replace_paths = 11; + + string resource_name = 12; + Index index = 13; + string module_addr = 14; + string action_reason = 15; + + message Index { + DynamicValue value = 1; + bool unknown = 2; + } + + message Moved { + ResourceInstanceInStackAddr prev_addr = 1; + } + message Imported { + string import_id = 1; + bool unknown = 2; + string generated_config = 3; + } + } + // Note: this is only for output values from the topmost + // stack configuration, because all other output values are + // internal to the configuration and not part of its public API. + message OutputValue { + string name = 1; + repeated ChangeType actions = 2; + DynamicValueChange values = 3; + } + + message ResourceInstanceDeferred { + ResourceInstance resource_instance = 1; + Deferred deferred = 2; + } + + // Note: this is only for input variables from the topmost + // stack configuration, because all other input variables are + // internal to the configuration and not part of its public API. + message InputVariable { + string name = 1; + repeated ChangeType actions = 2; + DynamicValueChange values = 3; + bool required_during_apply = 4; + } +} + +// Deferred contains all the metadata about a the deferral of a resource +// instance change. +message Deferred { + // Reason describes the reason why a resource instance change was + // deferred. + enum Reason { + INVALID = 0; + INSTANCE_COUNT_UNKNOWN = 1; + RESOURCE_CONFIG_UNKNOWN = 2; + PROVIDER_CONFIG_UNKNOWN = 3; + ABSENT_PREREQ = 4; + DEFERRED_PREREQ = 5; + } + Reason reason = 1; +} + +// Describes a change made during a Stacks.ApplyStackChanges call. +// +// All of the events of this type taken together represent a sort of "patch" +// modifying the two data structures that the caller must maintain: the +// raw state map, and the description map. Callers must apply these changes +// in the order of the emission of the messages and then retain the entirety +// of both data structures to populate fields in the next PlanStackChanges call. +message AppliedChange { + // Terraform Core's internal representation of the change, presented as + // a sequence of modifications to the raw state data structure. + // + // For each element, in order: + // - If both key and value are set and the key matches an element + // already in the raw state map, the new value replaces the existing one. + // - If both key and value are set but the key does not match an + // element in the raw state map, this represents inserting a new element + // into the map. + // - If key is set and value is not, this represents removing any existing + // element from the raw state map which has the given key, or a no-op + // if no such element exists. + // - No other situation is legal. + // + // This sequence can potentially be zero-length if a particular event only + // has a external-facing "description" component and no raw equivalent. In + // that case the raw state map is unmodified. + repeated RawChange raw = 1; + + // Caller-facing description of this change, to use for presenting + // information to end-users in the UI and for other subsystems such as + // billing. + // + // Callers are expected to maintain a map of description objects that + // gets updated piecemeal by messages in this field. Callers must treat + // the keys as entirely opaque and thus treat the resulting data structure + // as if it were an unsorted set of ChangeDescription objects; the keys + // exist only to allow patching the data structure over time. + // + // For each element, in order: + // - If both key and description are set and the key matches an element + // from the previous apply's description map, the new value replaces + // the existing one. + // - If both key and value are set but the key does not match an + // element in the previous apply's description map, this represents + // inserting a new element into the map. + // - If key is set and description is "deleted", this represents removing + // any existing element from the previous apply's description map which + // has the given key, or a no-op if no such element exists. + // - If a description field is set that the caller doesn't understand, + // the caller should still write it to the updated description map + // but ignore it in further processing. + // - No other situation is legal. + // + // Callers MUST preserve the verbatim description message in the + // description map, even if it contains fields that are not present in + // the caller's current protobuf stubs. In other words, callers must use + // a protocol buffers implementation that is able to preserve unknown + // fields and store them so that future versions of the caller might + // use an updated set of stubs to interact with the previously-stored + // description. + // + // DO NOT attempt to use this to surgically filter particular raw state + // updates from a larger plan. Although external descriptions often match + // with the raw representations in field "raw", that is not guaranteed and + // Terraform Core assumes that it will always be provided with the full + // raw state map during the next plan step. + repeated ChangeDescription descriptions = 2; + + message RawChange { + string key = 1; + google.protobuf.Any value = 2; + } + message ChangeDescription { + string key = 1; + oneof description { + Nothing deleted = 4; // explicitly represents the absence of a description + Nothing moved = 6; // explicitly represents the absence of a description + ResourceInstance resource_instance = 2; + OutputValue output_value = 3; + InputVariable input_variable = 7; + ComponentInstance component_instance = 5; + } + // Field number 20000 is reserved as a field number that will + // always be unknown to any client, to allow clients to test + // whether they correctly preserve unexpected fields. + reserved 20000; + } + message ResourceInstance { + ResourceInstanceObjectInStackAddr addr = 1; + DynamicValue new_value = 2; + ResourceMode resource_mode = 4; + string resource_type = 5; + string provider_addr = 6; + + // Sometimes Terraform needs to make changes to a resource in + // multiple steps during the apply phase, with each step + // changing something about the state. This flag will be set + // for such interim updates, and left unset for whatever + // description Terraform Core considers to be "final", at + // which point the new value should be converged with the + // desired state. + // + // The intended use for this is when presenting updated values + // to users in the UI, where it might be best to ignore or + // present differently interim updates to avoid creating + // confusion by showing the not-yet-converged intermediate + // states. + // + // If Terraform encounters a problem during the apply phase + // and needs to stop partway through then a "final" change + // description might never arrive. In that case, callers + // should save the most recent interim object as the final + // description, since it would represent the most accurate + // description of the state the remote system has been left + // in. + bool interim = 3; + } + message ComponentInstance { + string component_addr = 3; + string component_instance_addr = 1; + map output_values = 2; + } + message OutputValue { + string name = 1; + DynamicValue new_value = 2; + } + message InputVariable { + string name = 1; + DynamicValue new_value = 2; + } + message Nothing {} +} + +// A container for "progress report" events in both Stacks.PlanStackChanges +// and Stacks.ApplyStackChanges, which share this message type to allow +// clients to share event-handling code between the two phases. +message StackChangeProgress { + // Some event types are relevant only to one of the two operations, while + // others are common across both but will include different status codes, + // etc in different phases. + oneof event { + ComponentInstanceStatus component_instance_status = 1; + ResourceInstanceStatus resource_instance_status = 2; + ResourceInstancePlannedChange resource_instance_planned_change = 3; + ProvisionerStatus provisioner_status = 4; + ProvisionerOutput provisioner_output = 5; + ComponentInstanceChanges component_instance_changes = 6; + ComponentInstances component_instances = 7; + DeferredResourceInstancePlannedChange deferred_resource_instance_planned_change = 8; + } + + // ComponentInstanceStatus describes the current status of a component instance + // undergoing a plan or apply operation. + message ComponentInstanceStatus { + ComponentInstanceInStackAddr addr = 1; + Status status = 2; + + enum Status { + INVALID = 0; + PENDING = 1; + PLANNING = 2; + PLANNED = 3; + APPLYING = 4; + APPLIED = 5; + ERRORED = 6; + DEFERRED = 7; + } + } + + // ComponentInstanceStatus describes the current status of a resource instance + // undergoing a plan or apply operation. + message ResourceInstanceStatus { + ResourceInstanceObjectInStackAddr addr = 1; + Status status = 2; + string provider_addr = 3; + + enum Status { + INVALID = 0; + PENDING = 1; + REFRESHING = 2; + REFRESHED = 3; + PLANNING = 4; + PLANNED = 5; + APPLYING = 6; + APPLIED = 7; + ERRORED = 8; + } + } + + // ResourceInstancePlannedChange describes summary information about a planned + // change for a resource instance. This does not include the full object change, + // which is described in PlannedChange.ResourceChange. The information in this + // message is intended for the event stream and need not include the instance's + // full object values. + message ResourceInstancePlannedChange { + ResourceInstanceObjectInStackAddr addr = 1; + repeated ChangeType actions = 2; + Moved moved = 3; + Imported imported = 4; + string provider_addr = 5; + + message Moved { + ResourceInstanceInStackAddr prev_addr = 1; + } + message Imported { + string import_id = 1; + bool unknown = 2; + } + } + + // DeferredResourceInstancePlannedChange represents a planned change for a + // resource instance that is deferred due to the reason provided. + message DeferredResourceInstancePlannedChange { + Deferred deferred = 1; + ResourceInstancePlannedChange change = 2; + } + + // ProvisionerStatus represents the progress of a given provisioner during its + // resource instance's apply operation. + message ProvisionerStatus { + ResourceInstanceObjectInStackAddr addr = 1; + string name = 2; + ProvisionerStatus status = 3; + + enum Status { + INVALID = 0; + PROVISIONING = 1; + PROVISIONED = 2; + ERRORED = 3; + } + } + + // ProvisionerOutput represents recorded output data emitted by a provisioner + // during a resource instance's apply operation. + message ProvisionerOutput { + ResourceInstanceObjectInStackAddr addr = 1; + string name = 2; + string output = 3; + } + + // ComponentInstanceChanges represents a roll-up of change counts for a + // component instance plan or apply operation. + message ComponentInstanceChanges { + ComponentInstanceInStackAddr addr = 1; + + // total is the sum of all of the other count fields. + // + // Clients should sum all of the other count fields they know about + // and compare to total. If the sum is less than total then the + // difference should be treated as an "other change types" category, + // for forward-compatibility when the Terraform Core RPC server is + // using a newer version of this protocol than the client. + int32 total = 2; + int32 add = 3; + int32 change = 4; + int32 import = 5; + int32 remove = 6; + int32 defer = 7; + int32 move = 8; + int32 forget = 9; + } + + // ComponentInstances represents the result of expanding a component into zero + // or more instances. + message ComponentInstances { + string component_addr = 1; + repeated string instance_addrs = 2; + } +} + +message ListResourceIdentities { + message Request { + int64 state_handle = 1; + int64 dependency_locks_handle = 2; + int64 provider_cache_handle = 3; + } + message Response { + repeated Resource resource = 1; + } + + message Resource { + string component_addr = 1; + string component_instance_addr = 2; + // Unique address of the resource instance within the given component + // instance. Each component instance has a separate namespace of + // resource instance addresses, so callers must take both fields together + // to produce a key that's unique throughout the entire plan. + string resource_instance_addr = 3; + DynamicValue resource_identity = 4; + } +} diff --git a/internal/rpcapi/terraform1/terraform1.pb.go b/internal/rpcapi/terraform1/terraform1.pb.go new file mode 100644 index 0000000000..a027ce0412 --- /dev/null +++ b/internal/rpcapi/terraform1/terraform1.pb.go @@ -0,0 +1,525 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: terraform1.proto + +package terraform1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Diagnostic_Severity int32 + +const ( + Diagnostic_INVALID Diagnostic_Severity = 0 + Diagnostic_ERROR Diagnostic_Severity = 1 + Diagnostic_WARNING Diagnostic_Severity = 2 +) + +// Enum value maps for Diagnostic_Severity. +var ( + Diagnostic_Severity_name = map[int32]string{ + 0: "INVALID", + 1: "ERROR", + 2: "WARNING", + } + Diagnostic_Severity_value = map[string]int32{ + "INVALID": 0, + "ERROR": 1, + "WARNING": 2, + } +) + +func (x Diagnostic_Severity) Enum() *Diagnostic_Severity { + p := new(Diagnostic_Severity) + *p = x + return p +} + +func (x Diagnostic_Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Diagnostic_Severity) Descriptor() protoreflect.EnumDescriptor { + return file_terraform1_proto_enumTypes[0].Descriptor() +} + +func (Diagnostic_Severity) Type() protoreflect.EnumType { + return &file_terraform1_proto_enumTypes[0] +} + +func (x Diagnostic_Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Diagnostic_Severity.Descriptor instead. +func (Diagnostic_Severity) EnumDescriptor() ([]byte, []int) { + return file_terraform1_proto_rawDescGZIP(), []int{2, 0} +} + +// Represents a selected or available version of a provider, from either a +// dependency lock object (selected) or a provider cache object (available). +// +// This message type corresponds in meaning with a single "provider" block in a +// dependency lock file, but not all messages of this type directly represent +// such a physical block. +type ProviderPackage struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The address of the provider using the canonical form of the provider + // source address syntax. + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + // The version number of this provider package. Unset for (and only for) + // built-in providers; callers may use the set-ness of this field to + // distinguish installable vs. built-in providers without having to + // parse the source address syntax. + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // The hash strings that Terraform knows about for this provider package, + // using the same "scheme:hash" syntax used in Terraform's dependency + // lock file format. + // + // For a message representing a "selected" provider package this enumerates + // all of the checksums that were previously loaded from a dependency + // lock file or otherwise inserted into a dependency locks object, which + // are usually (but not necessarily) originally obtained from the + // provider's origin registry and then cached in the lock file. + // + // For a message representing an "available" provider package this + // describes only the actual package on disk, and so will typically + // include only the subset of the checksums from the corresponding + // "selected" package that are relevant to the current platform where + // Terraform Core is running. + Hashes []string `protobuf:"bytes,3,rep,name=hashes,proto3" json:"hashes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ProviderPackage) Reset() { + *x = ProviderPackage{} + mi := &file_terraform1_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ProviderPackage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ProviderPackage) ProtoMessage() {} + +func (x *ProviderPackage) ProtoReflect() protoreflect.Message { + mi := &file_terraform1_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ProviderPackage.ProtoReflect.Descriptor instead. +func (*ProviderPackage) Descriptor() ([]byte, []int) { + return file_terraform1_proto_rawDescGZIP(), []int{0} +} + +func (x *ProviderPackage) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *ProviderPackage) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *ProviderPackage) GetHashes() []string { + if x != nil { + return x.Hashes + } + return nil +} + +// A source address in the same form as it would appear in a Terraform +// configuration: a source string combined with an optional version constraint +// string, where the latter is valid only for registry module addresses. +// +// This is not used for "final" source addresses that have already been reduced +// to an exact version selection. For those we just directly encode the string +// representation of the final address, including a version number if necessary. +type SourceAddress struct { + state protoimpl.MessageState `protogen:"open.v1"` + Source string `protobuf:"bytes,1,opt,name=source,proto3" json:"source,omitempty"` + Versions string `protobuf:"bytes,2,opt,name=versions,proto3" json:"versions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceAddress) Reset() { + *x = SourceAddress{} + mi := &file_terraform1_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceAddress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceAddress) ProtoMessage() {} + +func (x *SourceAddress) ProtoReflect() protoreflect.Message { + mi := &file_terraform1_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourceAddress.ProtoReflect.Descriptor instead. +func (*SourceAddress) Descriptor() ([]byte, []int) { + return file_terraform1_proto_rawDescGZIP(), []int{1} +} + +func (x *SourceAddress) GetSource() string { + if x != nil { + return x.Source + } + return "" +} + +func (x *SourceAddress) GetVersions() string { + if x != nil { + return x.Versions + } + return "" +} + +type Diagnostic struct { + state protoimpl.MessageState `protogen:"open.v1"` + Severity Diagnostic_Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=terraform1.Diagnostic_Severity" json:"severity,omitempty"` + Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` + Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` + Subject *SourceRange `protobuf:"bytes,4,opt,name=subject,proto3" json:"subject,omitempty"` + Context *SourceRange `protobuf:"bytes,5,opt,name=context,proto3" json:"context,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Diagnostic) Reset() { + *x = Diagnostic{} + mi := &file_terraform1_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Diagnostic) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Diagnostic) ProtoMessage() {} + +func (x *Diagnostic) ProtoReflect() protoreflect.Message { + mi := &file_terraform1_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Diagnostic.ProtoReflect.Descriptor instead. +func (*Diagnostic) Descriptor() ([]byte, []int) { + return file_terraform1_proto_rawDescGZIP(), []int{2} +} + +func (x *Diagnostic) GetSeverity() Diagnostic_Severity { + if x != nil { + return x.Severity + } + return Diagnostic_INVALID +} + +func (x *Diagnostic) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *Diagnostic) GetDetail() string { + if x != nil { + return x.Detail + } + return "" +} + +func (x *Diagnostic) GetSubject() *SourceRange { + if x != nil { + return x.Subject + } + return nil +} + +func (x *Diagnostic) GetContext() *SourceRange { + if x != nil { + return x.Context + } + return nil +} + +type SourceRange struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceAddr string `protobuf:"bytes,1,opt,name=source_addr,json=sourceAddr,proto3" json:"source_addr,omitempty"` + Start *SourcePos `protobuf:"bytes,2,opt,name=start,proto3" json:"start,omitempty"` + End *SourcePos `protobuf:"bytes,3,opt,name=end,proto3" json:"end,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourceRange) Reset() { + *x = SourceRange{} + mi := &file_terraform1_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourceRange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourceRange) ProtoMessage() {} + +func (x *SourceRange) ProtoReflect() protoreflect.Message { + mi := &file_terraform1_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourceRange.ProtoReflect.Descriptor instead. +func (*SourceRange) Descriptor() ([]byte, []int) { + return file_terraform1_proto_rawDescGZIP(), []int{3} +} + +func (x *SourceRange) GetSourceAddr() string { + if x != nil { + return x.SourceAddr + } + return "" +} + +func (x *SourceRange) GetStart() *SourcePos { + if x != nil { + return x.Start + } + return nil +} + +func (x *SourceRange) GetEnd() *SourcePos { + if x != nil { + return x.End + } + return nil +} + +type SourcePos struct { + state protoimpl.MessageState `protogen:"open.v1"` + Byte int64 `protobuf:"varint,1,opt,name=byte,proto3" json:"byte,omitempty"` + Line int64 `protobuf:"varint,2,opt,name=line,proto3" json:"line,omitempty"` + Column int64 `protobuf:"varint,3,opt,name=column,proto3" json:"column,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SourcePos) Reset() { + *x = SourcePos{} + mi := &file_terraform1_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SourcePos) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SourcePos) ProtoMessage() {} + +func (x *SourcePos) ProtoReflect() protoreflect.Message { + mi := &file_terraform1_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SourcePos.ProtoReflect.Descriptor instead. +func (*SourcePos) Descriptor() ([]byte, []int) { + return file_terraform1_proto_rawDescGZIP(), []int{4} +} + +func (x *SourcePos) GetByte() int64 { + if x != nil { + return x.Byte + } + return 0 +} + +func (x *SourcePos) GetLine() int64 { + if x != nil { + return x.Line + } + return 0 +} + +func (x *SourcePos) GetColumn() int64 { + if x != nil { + return x.Column + } + return 0 +} + +var File_terraform1_proto protoreflect.FileDescriptor + +var file_terraform1_proto_rawDesc = string([]byte{ + 0x0a, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0a, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x22, 0x64, + 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, + 0x64, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, + 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x68, 0x61, + 0x73, 0x68, 0x65, 0x73, 0x22, 0x43, 0x0a, 0x0d, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, + 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x0a, + 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x92, 0x02, 0x0a, 0x0a, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x12, 0x3b, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, + 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, + 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, + 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x31, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, + 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, + 0x65, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x31, 0x0a, 0x07, 0x63, 0x6f, + 0x6e, 0x74, 0x65, 0x78, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x65, + 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x61, 0x6e, 0x67, 0x65, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x22, 0x2f, 0x0a, + 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, + 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, + 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x22, 0x84, + 0x01, 0x0a, 0x0b, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x1f, + 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, + 0x2b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x50, 0x6f, 0x73, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x27, 0x0a, 0x03, + 0x65, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x65, 0x72, 0x72, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x31, 0x2e, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x6f, 0x73, + 0x52, 0x03, 0x65, 0x6e, 0x64, 0x22, 0x4b, 0x0a, 0x09, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, + 0x6f, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x79, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x62, 0x79, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, + 0x6c, 0x75, 0x6d, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x63, 0x6f, 0x6c, 0x75, + 0x6d, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_terraform1_proto_rawDescOnce sync.Once + file_terraform1_proto_rawDescData []byte +) + +func file_terraform1_proto_rawDescGZIP() []byte { + file_terraform1_proto_rawDescOnce.Do(func() { + file_terraform1_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_terraform1_proto_rawDesc), len(file_terraform1_proto_rawDesc))) + }) + return file_terraform1_proto_rawDescData +} + +var file_terraform1_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_terraform1_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_terraform1_proto_goTypes = []any{ + (Diagnostic_Severity)(0), // 0: terraform1.Diagnostic.Severity + (*ProviderPackage)(nil), // 1: terraform1.ProviderPackage + (*SourceAddress)(nil), // 2: terraform1.SourceAddress + (*Diagnostic)(nil), // 3: terraform1.Diagnostic + (*SourceRange)(nil), // 4: terraform1.SourceRange + (*SourcePos)(nil), // 5: terraform1.SourcePos +} +var file_terraform1_proto_depIdxs = []int32{ + 0, // 0: terraform1.Diagnostic.severity:type_name -> terraform1.Diagnostic.Severity + 4, // 1: terraform1.Diagnostic.subject:type_name -> terraform1.SourceRange + 4, // 2: terraform1.Diagnostic.context:type_name -> terraform1.SourceRange + 5, // 3: terraform1.SourceRange.start:type_name -> terraform1.SourcePos + 5, // 4: terraform1.SourceRange.end:type_name -> terraform1.SourcePos + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_terraform1_proto_init() } +func file_terraform1_proto_init() { + if File_terraform1_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_terraform1_proto_rawDesc), len(file_terraform1_proto_rawDesc)), + NumEnums: 1, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_terraform1_proto_goTypes, + DependencyIndexes: file_terraform1_proto_depIdxs, + EnumInfos: file_terraform1_proto_enumTypes, + MessageInfos: file_terraform1_proto_msgTypes, + }.Build() + File_terraform1_proto = out.File + file_terraform1_proto_goTypes = nil + file_terraform1_proto_depIdxs = nil +} diff --git a/internal/rpcapi/terraform1/terraform1.proto b/internal/rpcapi/terraform1/terraform1.proto new file mode 100644 index 0000000000..988d9b93d1 --- /dev/null +++ b/internal/rpcapi/terraform1/terraform1.proto @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package terraform1; + + +// Represents a selected or available version of a provider, from either a +// dependency lock object (selected) or a provider cache object (available). +// +// This message type corresponds in meaning with a single "provider" block in a +// dependency lock file, but not all messages of this type directly represent +// such a physical block. +message ProviderPackage { + // The address of the provider using the canonical form of the provider + // source address syntax. + string source_addr = 1; + + // The version number of this provider package. Unset for (and only for) + // built-in providers; callers may use the set-ness of this field to + // distinguish installable vs. built-in providers without having to + // parse the source address syntax. + string version = 2; + + // The hash strings that Terraform knows about for this provider package, + // using the same "scheme:hash" syntax used in Terraform's dependency + // lock file format. + // + // For a message representing a "selected" provider package this enumerates + // all of the checksums that were previously loaded from a dependency + // lock file or otherwise inserted into a dependency locks object, which + // are usually (but not necessarily) originally obtained from the + // provider's origin registry and then cached in the lock file. + // + // For a message representing an "available" provider package this + // describes only the actual package on disk, and so will typically + // include only the subset of the checksums from the corresponding + // "selected" package that are relevant to the current platform where + // Terraform Core is running. + repeated string hashes = 3; +} + +// A source address in the same form as it would appear in a Terraform +// configuration: a source string combined with an optional version constraint +// string, where the latter is valid only for registry module addresses. +// +// This is not used for "final" source addresses that have already been reduced +// to an exact version selection. For those we just directly encode the string +// representation of the final address, including a version number if necessary. +message SourceAddress { + string source = 1; + string versions = 2; +} + +message Diagnostic { + enum Severity { + INVALID = 0; + ERROR = 1; + WARNING = 2; + } + Severity severity = 1; + string summary = 2; + string detail = 3; + SourceRange subject = 4; + SourceRange context = 5; +} + +message SourceRange { + string source_addr = 1; + SourcePos start = 2; + SourcePos end = 3; +} + +message SourcePos { + int64 byte = 1; + int64 line = 2; + int64 column = 3; +} diff --git a/internal/rpcapi/testdata/provider-fs-mirror/example.com/foo/bar/1.2.3/os_arch/terraform-provider-bar b/internal/rpcapi/testdata/provider-fs-mirror/example.com/foo/bar/1.2.3/os_arch/terraform-provider-bar new file mode 100644 index 0000000000..8e8c5f0eb8 --- /dev/null +++ b/internal/rpcapi/testdata/provider-fs-mirror/example.com/foo/bar/1.2.3/os_arch/terraform-provider-bar @@ -0,0 +1,2 @@ +This is not a real Terraform provider plugin, just a placeholder for us to +try to install in some tests. diff --git a/internal/rpcapi/testdata/providers/hashes.json b/internal/rpcapi/testdata/providers/hashes.json new file mode 100644 index 0000000000..7086b2e2b1 --- /dev/null +++ b/internal/rpcapi/testdata/providers/hashes.json @@ -0,0 +1,4 @@ +{ + "terraform_provider_foo": ["h1:dJTExJ11p+lRE8FAm4HWzTw+uMEyfE6AXXxiOgl/nB0="], + "terraform_provider_bar": ["h1:Hod4iOH+qbXMtH4orEmCem6F3T+YRPhDSNlXmOIRNuY="] +} diff --git a/internal/rpcapi/testdata/providers/terraform_provider_bar/terraform-provider-bar b/internal/rpcapi/testdata/providers/terraform_provider_bar/terraform-provider-bar new file mode 100644 index 0000000000..7215f7db42 --- /dev/null +++ b/internal/rpcapi/testdata/providers/terraform_provider_bar/terraform-provider-bar @@ -0,0 +1,2 @@ +This is not a real provider executable. It's just here to give the packages +service something to install. diff --git a/internal/rpcapi/testdata/providers/terraform_provider_foo/terraform-provider-foo b/internal/rpcapi/testdata/providers/terraform_provider_foo/terraform-provider-foo new file mode 100644 index 0000000000..7215f7db42 --- /dev/null +++ b/internal/rpcapi/testdata/providers/terraform_provider_foo/terraform-provider-foo @@ -0,0 +1,2 @@ +This is not a real provider executable. It's just here to give the packages +service something to install. diff --git a/internal/rpcapi/testdata/sourcebundle/bar/bar.tf b/internal/rpcapi/testdata/sourcebundle/bar/bar.tf new file mode 100644 index 0000000000..077a632855 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/bar/bar.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "deferred" { + type = bool + default = false +} + +resource "testing_deferred_resource" "resource" { + id = "hello" + value = "world" + deferred = var.deferred +} diff --git a/internal/rpcapi/testdata/sourcebundle/bar/bar.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/bar/bar.tfstack.hcl new file mode 100644 index 0000000000..0cff3ac628 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/bar/bar.tfstack.hcl @@ -0,0 +1,26 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } +} + +component "deferred" { + source = "./" + providers = { + testing = provider.testing.default + } + + inputs = { + deferred = true + } +} diff --git a/internal/rpcapi/testdata/sourcebundle/baz/main.tf b/internal/rpcapi/testdata/sourcebundle/baz/main.tf new file mode 100644 index 0000000000..077a632855 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/baz/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "deferred" { + type = bool + default = false +} + +resource "testing_deferred_resource" "resource" { + id = "hello" + value = "world" + deferred = var.deferred +} diff --git a/internal/rpcapi/testdata/sourcebundle/baz/main.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/baz/main.tfstack.hcl new file mode 100644 index 0000000000..0cff3ac628 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/baz/main.tfstack.hcl @@ -0,0 +1,26 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } +} + +component "deferred" { + source = "./" + providers = { + testing = provider.testing.default + } + + inputs = { + deferred = true + } +} diff --git a/internal/rpcapi/testdata/sourcebundle/foo/.terraform.lock.hcl b/internal/rpcapi/testdata/sourcebundle/foo/.terraform.lock.hcl new file mode 100644 index 0000000000..929e393b85 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/foo/.terraform.lock.hcl @@ -0,0 +1,7 @@ + +provider "example.com/foo/bar" { + version = "1.2.3" + hashes = [ + "zh:abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd" + ] +} diff --git a/internal/rpcapi/testdata/sourcebundle/foo/foo.tf b/internal/rpcapi/testdata/sourcebundle/foo/foo.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/rpcapi/testdata/sourcebundle/foo/foo.tfdeploy.hcl b/internal/rpcapi/testdata/sourcebundle/foo/foo.tfdeploy.hcl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/rpcapi/testdata/sourcebundle/foo/foo.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/foo/foo.tfstack.hcl new file mode 100644 index 0000000000..4be2ddcd2b --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/foo/foo.tfstack.hcl @@ -0,0 +1,3 @@ +# This is intentionally empty. Some of our tests use this to straightfowardly +# test the empty configuration case, so adding declarations here will break +# those tests. diff --git a/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/child/non-empty-stack-child.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/child/non-empty-stack-child.tfstack.hcl new file mode 100644 index 0000000000..43a242b42f --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/child/non-empty-stack-child.tfstack.hcl @@ -0,0 +1,3 @@ +component "foo" { + source = "../empty-module" +} diff --git a/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/empty-module/nothing.tf b/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/empty-module/nothing.tf new file mode 100644 index 0000000000..7e92eb7021 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/empty-module/nothing.tf @@ -0,0 +1,3 @@ +# This module is intentionally empty, since it's called from stack +# configurations where we're only testing the stack configuration stuff and +# not the main Terraform language stuff. diff --git a/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/non-empty-stack.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/non-empty-stack.tfstack.hcl new file mode 100644 index 0000000000..7e5c1df1e0 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/foo/non-empty-stack/non-empty-stack.tfstack.hcl @@ -0,0 +1,56 @@ + +component "single" { + source = "./empty-module" +} + +component "for_each" { + source = "./empty-module" + for_each = {} +} + +stack "single" { + source = "./child" +} + +stack "for_each" { + source = "./child" + for_each = {} +} + +variable "unused" { + type = string +} + +variable "unused_with_default" { + type = string + default = "default" +} + +variable "ephemeral" { + type = string + default = null + ephemeral = true +} + +variable "sensitive" { + type = string + default = null + sensitive = true +} + +output "normal" { + type = string + value = var.optional +} + +output "ephemeral" { + type = string + value = var.ephemeral + ephemeral = true +} + +output "sensitive" { + type = string + value = var.sensitive + sensitive = true +} diff --git a/internal/rpcapi/testdata/sourcebundle/import/import.tf b/internal/rpcapi/testdata/sourcebundle/import/import.tf new file mode 100644 index 0000000000..6e50687862 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/import/import.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string +} + +import { + id = var.id + to = testing_resource.resource +} + +resource "testing_resource" "resource" {} diff --git a/internal/rpcapi/testdata/sourcebundle/import/import.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/import/import.tfstack.hcl new file mode 100644 index 0000000000..1e2fcea6f4 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/import/import.tfstack.hcl @@ -0,0 +1,33 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "unknown" { + type = string +} + +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } + inputs = { + id = "self" + } +} + +component "unknown" { + source = "./" + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.unknown + } +} diff --git a/internal/rpcapi/testdata/sourcebundle/moved/moved.tf b/internal/rpcapi/testdata/sourcebundle/moved/moved.tf new file mode 100644 index 0000000000..22965b3926 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/moved/moved.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +moved { + from = testing_resource.before + to = testing_resource.after +} + +resource "testing_resource" "after" { + id = "before" + value = null +} diff --git a/internal/rpcapi/testdata/sourcebundle/moved/moved.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/moved/moved.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/moved/moved.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/rpcapi/testdata/sourcebundle/removed/removed.tf b/internal/rpcapi/testdata/sourcebundle/removed/removed.tf new file mode 100644 index 0000000000..e62ae9a67e --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/removed/removed.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +removed { + from = testing_resource.resource + + lifecycle { + destroy = false + } +} diff --git a/internal/rpcapi/testdata/sourcebundle/removed/removed.tfstack.hcl b/internal/rpcapi/testdata/sourcebundle/removed/removed.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/removed/removed.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/rpcapi/testdata/sourcebundle/terraform-sources.json b/internal/rpcapi/testdata/sourcebundle/terraform-sources.json new file mode 100644 index 0000000000..aba8a33459 --- /dev/null +++ b/internal/rpcapi/testdata/sourcebundle/terraform-sources.json @@ -0,0 +1,35 @@ +{ + "terraform_source_bundle": 1, + "packages": [ + { + "source": "git::https://example.com/foo.git", + "local": "foo", + "meta": {} + }, + { + "source": "git::https://example.com/bar.git", + "local": "bar", + "meta": {} + }, + { + "source": "git::https://example.com/baz.git", + "local": "baz", + "meta": {} + }, + { + "source": "git::https://example.com/import.git", + "local": "import", + "meta": {} + }, + { + "source": "git::https://example.com/moved.git", + "local": "moved", + "meta": {} + }, + { + "source": "git::https://example.com/removed.git", + "local": "removed", + "meta": {} + } + ] +} diff --git a/internal/schemarepo/doc.go b/internal/schemarepo/doc.go new file mode 100644 index 0000000000..4d66764f5e --- /dev/null +++ b/internal/schemarepo/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package schemarepo deals with the cross-cutting concern of gathering together +// all of the schemas needed for manipulating data produced by the various +// kinds of plugins. +package schemarepo diff --git a/internal/schemarepo/loadschemas/doc.go b/internal/schemarepo/loadschemas/doc.go new file mode 100644 index 0000000000..5c7b25c2ef --- /dev/null +++ b/internal/schemarepo/loadschemas/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package loadschemas knows how to load schemas from plugins to produce a +// schema repository. +package loadschemas diff --git a/internal/schemarepo/loadschemas/load.go b/internal/schemarepo/loadschemas/load.go new file mode 100644 index 0000000000..509c142a12 --- /dev/null +++ b/internal/schemarepo/loadschemas/load.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package loadschemas + +import ( + "fmt" + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/schemarepo" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// LoadSchemas loads all of the schemas that might be needed to work with the +// given configuration and state, using the given plugins. +func LoadSchemas(config *configs.Config, state *states.State, plugins *Plugins) (*schemarepo.Schemas, error) { + schemas := &schemarepo.Schemas{ + Providers: map[addrs.Provider]providers.ProviderSchema{}, + Provisioners: map[string]*configschema.Block{}, + } + var diags tfdiags.Diagnostics + + newDiags := loadProviderSchemas(schemas.Providers, config, state, plugins) + diags = diags.Append(newDiags) + newDiags = loadProvisionerSchemas(schemas.Provisioners, config, plugins) + diags = diags.Append(newDiags) + + return schemas, diags.Err() +} + +func loadProviderSchemas(schemas map[addrs.Provider]providers.ProviderSchema, config *configs.Config, state *states.State, plugins *Plugins) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + ensure := func(fqn addrs.Provider) { + name := fqn.String() + + if _, exists := schemas[fqn]; exists { + return + } + + log.Printf("[TRACE] LoadSchemas: retrieving schema for provider type %q", name) + schema, err := plugins.ProviderSchema(fqn) + if err != nil { + // We'll put a stub in the map so we won't re-attempt this on + // future calls, which would then repeat the same error message + // multiple times. + schemas[fqn] = providers.ProviderSchema{} + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Failed to obtain provider schema", + fmt.Sprintf("Could not load the schema for provider %s: %s.", fqn, err), + ), + ) + return + } + + schemas[fqn] = schema + } + + if config != nil { + for _, fqn := range config.ProviderTypes() { + ensure(fqn) + } + } + + if state != nil { + needed := providers.AddressedTypesAbs(state.ProviderAddrs()) + for _, typeAddr := range needed { + ensure(typeAddr) + } + } + + return diags +} + +func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *configs.Config, plugins *Plugins) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + ensure := func(name string) { + if _, exists := schemas[name]; exists { + return + } + + log.Printf("[TRACE] LoadSchemas: retrieving schema for provisioner %q", name) + schema, err := plugins.ProvisionerSchema(name) + if err != nil { + // We'll put a stub in the map so we won't re-attempt this on + // future calls, which would then repeat the same error message + // multiple times. + schemas[name] = &configschema.Block{} + diags = diags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Failed to obtain provisioner schema", + fmt.Sprintf("Could not load the schema for provisioner %q: %s.", name, err), + ), + ) + return + } + + schemas[name] = schema + } + + if config != nil { + for _, rc := range config.Module.ManagedResources { + for _, pc := range rc.Managed.Provisioners { + ensure(pc.Type) + } + } + + // Must also visit our child modules, recursively. + for _, cc := range config.Children { + childDiags := loadProvisionerSchemas(schemas, cc, plugins) + diags = diags.Append(childDiags) + } + } + + return diags +} diff --git a/internal/schemarepo/loadschemas/plugins.go b/internal/schemarepo/loadschemas/plugins.go new file mode 100644 index 0000000000..5408adece0 --- /dev/null +++ b/internal/schemarepo/loadschemas/plugins.go @@ -0,0 +1,279 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package loadschemas + +import ( + "fmt" + "log" + + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/provisioners" +) + +// Plugins represents a library of available plugins for which it's safe +// to cache certain information for performance reasons. +type Plugins struct { + providerFactories map[addrs.Provider]providers.Factory + provisionerFactories map[string]provisioners.Factory + + preloadedProviderSchemas map[addrs.Provider]providers.ProviderSchema +} + +func NewPlugins( + providerFactories map[addrs.Provider]providers.Factory, + provisionerFactories map[string]provisioners.Factory, + preloadedProviderSchemas map[addrs.Provider]providers.ProviderSchema, +) *Plugins { + ret := &Plugins{ + providerFactories: providerFactories, + provisionerFactories: provisionerFactories, + preloadedProviderSchemas: preloadedProviderSchemas, + } + return ret +} + +// ProviderFactories returns a map of all of the registered provider factories. +// +// Callers must not modify the returned map and must not access it concurrently +// with any other method of this type. +func (cp *Plugins) ProviderFactories() map[addrs.Provider]providers.Factory { + return cp.providerFactories +} + +func (cp *Plugins) HasProvider(addr addrs.Provider) bool { + _, ok := cp.providerFactories[addr] + return ok +} + +func (cp *Plugins) HasPreloadedSchemaForProvider(addr addrs.Provider) bool { + _, ok := cp.preloadedProviderSchemas[addr] + return ok +} + +func (cp *Plugins) NewProviderInstance(addr addrs.Provider) (providers.Interface, error) { + f, ok := cp.providerFactories[addr] + if !ok { + return nil, fmt.Errorf("unavailable provider %q", addr.String()) + } + + return f() +} + +// ProvisionerFactories returns a map of all of the registered provisioner +// factories. +// +// Callers must not modify the returned map and must not access it concurrently +// with any other method of this type. +func (cp *Plugins) ProvisionerFactories() map[string]provisioners.Factory { + return cp.provisionerFactories +} + +func (cp *Plugins) HasProvisioner(typ string) bool { + _, ok := cp.provisionerFactories[typ] + return ok +} + +func (cp *Plugins) NewProvisionerInstance(typ string) (provisioners.Interface, error) { + f, ok := cp.provisionerFactories[typ] + if !ok { + return nil, fmt.Errorf("unavailable provisioner %q", typ) + } + + return f() +} + +// ProviderSchema uses a temporary instance of the provider with the given +// address to obtain the full schema for all aspects of that provider. +// +// ProviderSchema memoizes results by unique provider address, so it's fine +// to repeatedly call this method with the same address if various different +// parts of Terraform all need the same schema information. +func (cp *Plugins) ProviderSchema(addr addrs.Provider) (providers.ProviderSchema, error) { + // Check the global schema cache first. + // This cache is only written by the provider client, and transparently + // used by GetProviderSchema, but we check it here because at this point we + // may be able to avoid spinning up the provider instance at all. + // We skip this if we have preloaded schemas because that suggests that + // our caller is not Terraform CLI and therefore it's probably inappropriate + // to assume that provider schemas are unique process-wide. + schemas, ok := providers.SchemaCache.Get(addr) + if ok { + log.Printf("[TRACE] terraform.contextPlugins: Schema for provider %q is in the global cache", addr) + return schemas, nil + } + + // We might have a non-global preloaded copy of this provider's schema. + if schema, ok := cp.preloadedProviderSchemas[addr]; ok { + log.Printf("[TRACE] terraform.contextPlugins: Provider %q has a preloaded schema", addr) + return schema, nil + } + + log.Printf("[TRACE] terraform.contextPlugins: Initializing provider %q to read its schema", addr) + provider, err := cp.NewProviderInstance(addr) + if err != nil { + return schemas, fmt.Errorf("failed to instantiate provider %q to obtain schema: %s", addr, err) + } + defer provider.Close() + + resp := provider.GetProviderSchema() + if resp.Diagnostics.HasErrors() { + return resp, fmt.Errorf("failed to retrieve schema from provider %q: %s", addr, resp.Diagnostics.Err()) + } + + if resp.Provider.Version < 0 { + // We're not using the version numbers here yet, but we'll check + // for validity anyway in case we start using them in future. + return resp, fmt.Errorf("provider %s has invalid negative schema version for its configuration blocks,which is a bug in the provider ", addr) + } + + for t, r := range resp.ResourceTypes { + if err := r.Body.InternalValidate(); err != nil { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err) + } + if r.Version < 0 { + return resp, fmt.Errorf("provider %s has invalid negative schema version for managed resource type %q, which is a bug in the provider", addr, t) + } + + // Validate resource identity schema if the resource has one + if r.Identity != nil { + if err := r.Identity.InternalValidate(); err != nil { + return resp, fmt.Errorf("provider %s has invalid identity schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err) + } + if r.IdentityVersion < 0 { + return resp, fmt.Errorf("provider %s has invalid negative identity schema version for managed resource type %q, which is a bug in the provider", addr, t) + } + + for attrName, attrTy := range r.Identity.ImpliedType().AttributeTypes() { + if attrTy.MapElementType() != nil { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is a map, which is not allowed in identity schemas", addr, t, attrName) + } + + if attrTy.SetElementType() != nil { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is a set, which is not allowed in identity schemas", addr, t, attrName) + } + + if attrTy.IsObjectType() { + return resp, fmt.Errorf("provider %s has invalid schema for managed resource type %q, attribute %q is an object, which is not allowed in identity schemas", addr, t, attrName) + } + } + } + } + + for t, d := range resp.DataSources { + if err := d.Body.InternalValidate(); err != nil { + return resp, fmt.Errorf("provider %s has invalid schema for data resource type %q, which is a bug in the provider: %q", addr, t, err) + } + if d.Version < 0 { + // We're not using the version numbers here yet, but we'll check + // for validity anyway in case we start using them in future. + return resp, fmt.Errorf("provider %s has invalid negative schema version for data resource type %q, which is a bug in the provider", addr, t) + } + } + + for t, r := range resp.EphemeralResourceTypes { + if err := r.Body.InternalValidate(); err != nil { + return resp, fmt.Errorf("provider %s has invalid schema for ephemeral resource type %q, which is a bug in the provider: %q", addr, t, err) + } + } + + for n, f := range resp.Functions { + if !hclsyntax.ValidIdentifier(n) { + return resp, fmt.Errorf("provider %s declares function with invalid name %q", addr, n) + } + // We'll also do some enforcement of parameter names, even though they + // are only for docs/UI for now, to leave room for us to potentially + // use them for other purposes later. + seenParams := make(map[string]int, len(f.Parameters)) + for i, p := range f.Parameters { + if !hclsyntax.ValidIdentifier(p.Name) { + return resp, fmt.Errorf("provider %s function %q declares invalid name %q for parameter %d", addr, n, p.Name, i) + } + if prevIdx, exists := seenParams[p.Name]; exists { + return resp, fmt.Errorf("provider %s function %q reuses name %q for both parameters %d and %d", addr, n, p.Name, prevIdx, i) + } + seenParams[p.Name] = i + } + if p := f.VariadicParameter; p != nil { + if !hclsyntax.ValidIdentifier(p.Name) { + return resp, fmt.Errorf("provider %s function %q declares invalid name %q for its variadic parameter", addr, n, p.Name) + } + if prevIdx, exists := seenParams[p.Name]; exists { + return resp, fmt.Errorf("provider %s function %q reuses name %q for both parameter %d and its variadic parameter", addr, n, p.Name, prevIdx) + } + } + } + + return resp, nil +} + +// ProviderConfigSchema is a helper wrapper around ProviderSchema which first +// reads the full schema of the given provider and then extracts just the +// provider's configuration schema, which defines what's expected in a +// "provider" block in the configuration when configuring this provider. +func (cp *Plugins) ProviderConfigSchema(providerAddr addrs.Provider) (*configschema.Block, error) { + providerSchema, err := cp.ProviderSchema(providerAddr) + if err != nil { + return nil, err + } + + return providerSchema.Provider.Body, nil +} + +// ResourceTypeSchema is a helper wrapper around ProviderSchema which first +// reads the schema of the given provider and then tries to find the schema +// for the resource type of the given resource mode in that provider. +// +// ResourceTypeSchema will return an error if the provider schema lookup +// fails, but will return an empty schema if the provider schema lookup +// succeeds but then the provider doesn't have a resource of the requested type. +func (cp *Plugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (providers.Schema, error) { + providerSchema, err := cp.ProviderSchema(providerAddr) + if err != nil { + return providers.Schema{}, err + } + + return providerSchema.SchemaForResourceType(resourceMode, resourceType), nil +} + +// ProvisionerSchema uses a temporary instance of the provisioner with the +// given type name to obtain the schema for that provisioner's configuration. +// +// ProvisionerSchema memoizes results by provisioner type name, so it's fine +// to repeatedly call this method with the same name if various different +// parts of Terraform all need the same schema information. +func (cp *Plugins) ProvisionerSchema(typ string) (*configschema.Block, error) { + log.Printf("[TRACE] terraform.contextPlugins: Initializing provisioner %q to read its schema", typ) + provisioner, err := cp.NewProvisionerInstance(typ) + if err != nil { + return nil, fmt.Errorf("failed to instantiate provisioner %q to obtain schema: %s", typ, err) + } + defer provisioner.Close() + + resp := provisioner.GetSchema() + if resp.Diagnostics.HasErrors() { + return nil, fmt.Errorf("failed to retrieve schema from provisioner %q: %s", typ, resp.Diagnostics.Err()) + } + + return resp.Provisioner, nil +} + +// ProviderFunctionDecls is a helper wrapper around ProviderSchema which first +// reads the schema of the given provider and then returns all of the +// functions it declares, if any. +// +// ProviderFunctionDecl will return an error if the provider schema lookup +// fails, but will return an empty set of functions if a successful response +// returns no functions, or if the provider is using an older protocol version +// which has no support for provider-contributed functions. +func (cp *Plugins) ProviderFunctionDecls(providerAddr addrs.Provider) (map[string]providers.FunctionDecl, error) { + providerSchema, err := cp.ProviderSchema(providerAddr) + if err != nil { + return nil, err + } + + return providerSchema.Functions, nil +} diff --git a/internal/schemarepo/schemas.go b/internal/schemarepo/schemas.go new file mode 100644 index 0000000000..b076d80c02 --- /dev/null +++ b/internal/schemarepo/schemas.go @@ -0,0 +1,55 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package schemarepo + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" +) + +// Schemas is a container for various kinds of schema that Terraform needs +// during processing. +type Schemas struct { + Providers map[addrs.Provider]providers.ProviderSchema + Provisioners map[string]*configschema.Block +} + +// ProviderSchema returns the entire ProviderSchema object that was produced +// by the plugin for the given provider, or nil if no such schema is available. +// +// It's usually better to go use the more precise methods offered by type +// Schemas to handle this detail automatically. +func (ss *Schemas) ProviderSchema(provider addrs.Provider) providers.ProviderSchema { + return ss.Providers[provider] +} + +// ProviderConfig returns the schema for the provider configuration of the +// given provider type, or nil if no such schema is available. +func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block { + return ss.ProviderSchema(provider).Provider.Body +} + +// ResourceTypeConfig returns the schema for the configuration of a given +// resource type belonging to a given provider type, or an empty schema +// if no such schema is available. +// +// In many cases the provider type is inferrable from the resource type name, +// but this is not always true because users can override the provider for +// a resource using the "provider" meta-argument. Therefore it's important to +// always pass the correct provider name, even though it many cases it feels +// redundant. +func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) providers.Schema { + ps := ss.ProviderSchema(provider) + if ps.ResourceTypes == nil { + return providers.Schema{} + } + return ps.SchemaForResourceType(resourceMode, resourceType) +} + +// ProvisionerConfig returns the schema for the configuration of a given +// provisioner, or nil of no such schema is available. +func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block { + return ss.Provisioners[name] +} diff --git a/internal/stacks/README.md b/internal/stacks/README.md new file mode 100644 index 0000000000..feed5ca64f --- /dev/null +++ b/internal/stacks/README.md @@ -0,0 +1,48 @@ +# Terraform Stacks functionality + +The Go packages under this directory together implement the Terraform Stacks +features. + +Terraform Stacks is an orchestration layer on top of zero or more trees of +Terraform modules, and so much of what you'll find here is analogous to +a top-level package that serves a similar purpose for individual Terraform +modules or trees of modules. + +The main components here are: + +- `stackaddrs`: A stacks-specific analog to the top-level package `addrs`, + containing types we use to refer to objects within the stacks language and + runtime, and some logic for navigating between different types of addresses. + + This package builds on package `addrs`, since the stacks runtime wraps + the modules runtime. Therefore some of the stack-specific address types + incorporate more general address types from the other package. + +- `stackconfig`: Implements the loading, parsing, and static decoding for + the stacks language, analogous to the top-level package `configs` that + does similarly for Terraform's module language. + +- `stackplan` and `stackstate` together provide the models and + marshalling/unmarshalling logic for the Stacks variants of Terraform's + "plan" and "state" concepts. + +- `stackruntime` deals with the runtime behavior of stacks, including + the creation of plans based on a comparison between desired and actual state, + and then applying those plans. + + All of the dynamic behavior of the stacks language lives here. + +- `tfstackdata1` is a Go representation of an internal protocol buffers schema + used for preserving plan and state data between runs. These formats are + implementation details that external callers are not permitted to rely on. + + (The public interface is via the Terraform Core RPC API, which is + implemented in the sibling directory `rpcapi`.) + +## More Documentation + +The following are some more specific and therefore more detailed documents +about some particular parts of the implementation of the Terraform Stacks +features: + +* [Stacks Runtime internal architecture](./stackruntime/internal/stackeval/README.md) diff --git a/internal/stacks/stackaddrs/component.go b/internal/stacks/stackaddrs/component.go new file mode 100644 index 0000000000..d605c6d7f6 --- /dev/null +++ b/internal/stacks/stackaddrs/component.go @@ -0,0 +1,229 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Component is the address of a "component" block within a stack config. +type Component struct { + Name string +} + +func (Component) referenceableSigil() {} +func (Component) inStackConfigSigil() {} +func (Component) inStackInstanceSigil() {} + +func (c Component) String() string { + return "component." + c.Name +} + +func (c Component) UniqueKey() collections.UniqueKey[Component] { + return c +} + +// A Component is its own [collections.UniqueKey]. +func (Component) IsUniqueKey(Component) {} + +// ConfigComponent places a [Component] in the context of a particular [Stack]. +type ConfigComponent = InStackConfig[Component] + +// AbsComponent places a [Component] in the context of a particular [StackInstance]. +type AbsComponent = InStackInstance[Component] + +func AbsComponentToInstance(ist AbsComponent, ik addrs.InstanceKey) AbsComponentInstance { + return AbsComponentInstance{ + Stack: ist.Stack, + Item: ComponentInstance{ + Component: ist.Item, + Key: ik, + }, + } +} + +// ComponentInstance is the address of a dynamic instance of a component. +type ComponentInstance struct { + Component Component + Key addrs.InstanceKey +} + +func (ComponentInstance) inStackConfigSigil() {} +func (ComponentInstance) inStackInstanceSigil() {} + +func (c ComponentInstance) String() string { + if c.Key == nil { + return c.Component.String() + } + return c.Component.String() + c.Key.String() +} + +func (c ComponentInstance) UniqueKey() collections.UniqueKey[ComponentInstance] { + return c +} + +// A ComponentInstance is its own [collections.UniqueKey]. +func (ComponentInstance) IsUniqueKey(ComponentInstance) {} + +// ConfigComponentInstance places a [ComponentInstance] in the context of a +// particular [Stack]. +type ConfigComponentInstance = InStackConfig[ComponentInstance] + +// AbsComponentInstance places a [ComponentInstance] in the context of a +// particular [StackInstance]. +type AbsComponentInstance = InStackInstance[ComponentInstance] + +func ConfigComponentForAbsInstance(instAddr AbsComponentInstance) ConfigComponent { + configInst := ConfigForAbs(instAddr) // a ConfigComponentInstance + return ConfigComponent{ + Stack: configInst.Stack, + Item: Component{ + Name: configInst.Item.Component.Name, + }, + } +} + +func ParseAbsComponentInstance(traversal hcl.Traversal) (AbsComponentInstance, tfdiags.Diagnostics) { + inst, remain, diags := ParseAbsComponentInstanceOnly(traversal) + if diags.HasErrors() { + return AbsComponentInstance{}, diags + } + + if len(remain) > 0 { + // Then we have some remaining traversal steps that weren't consumed + // by the component instance address itself, which is an error when the + // caller is using this function. + rng := remain.SourceRange() + // if "remain" is empty then the source range would be zero length, + // and so we'll use the original traversal instead. + if len(remain) == 0 { + rng = traversal.SourceRange() + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid component instance address", + Detail: "The component instance address must include the keyword \"component\" followed by a component name.", + Subject: &rng, + }) + return AbsComponentInstance{}, diags + } + + return inst, diags +} + +func ParseAbsComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return AbsComponentInstance{}, diags + } + + ret, moreDiags := ParseAbsComponentInstance(traversal) + diags = diags.Append(moreDiags) + return ret, diags +} + +func ParsePartialComponentInstanceStr(s string) (AbsComponentInstance, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(s), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return AbsComponentInstance{}, diags + } + + ret, moreDiags := ParseAbsComponentInstance(traversal) + diags = diags.Append(moreDiags) + return ret, diags +} + +func ParseAbsComponentInstanceStrOnly(s string) (AbsComponentInstance, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(s), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return AbsComponentInstance{}, traversal, diags + } + + ret, rest, moreDiags := ParseAbsComponentInstanceOnly(traversal) + diags = diags.Append(moreDiags) + return ret, rest, diags +} + +func ParseAbsComponentInstanceOnly(traversal hcl.Traversal) (AbsComponentInstance, hcl.Traversal, tfdiags.Diagnostics) { + if traversal.IsRelative() { + // This is always a caller bug: caller must only pass absolute + // traversals in here. + panic("ParseAbsComponentInstanceOnly with relative traversal") + } + + stackInst, remain, diags := parseInStackInstancePrefix(traversal) + if diags.HasErrors() { + return AbsComponentInstance{}, remain, diags + } + + // "remain" should now be the keyword "component" followed by a valid + // component name, optionally followed by an instance key. + const diagSummary = "Invalid component instance address" + + if kwStep, ok := remain[0].(hcl.TraverseAttr); !ok || kwStep.Name != "component" { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: diagSummary, + Detail: "The component instance address must include the keyword \"component\" followed by a component name.", + Subject: remain[0].SourceRange().Ptr(), + }) + return AbsComponentInstance{}, remain, diags + } + remain = remain[1:] + + nameStep, ok := remain[0].(hcl.TraverseAttr) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: diagSummary, + Detail: "The component instance address must include the keyword \"component\" followed by a component name.", + Subject: remain[1].SourceRange().Ptr(), + }) + return AbsComponentInstance{}, remain, diags + } + remain = remain[1:] + componentAddr := ComponentInstance{ + Component: Component{Name: nameStep.Name}, + } + + if len(remain) > 0 { + switch instStep := remain[0].(type) { + case hcl.TraverseIndex: + var err error + componentAddr.Key, err = addrs.ParseInstanceKey(instStep.Key) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: diagSummary, + Detail: fmt.Sprintf("Invalid instance key: %s.", err), + Subject: instStep.SourceRange().Ptr(), + }) + return AbsComponentInstance{}, remain, diags + } + + remain = remain[1:] + case hcl.TraverseSplat: + componentAddr.Key = addrs.WildcardKey + remain = remain[1:] + } + } + + return AbsComponentInstance{ + Stack: stackInst, + Item: componentAddr, + }, remain, diags +} diff --git a/internal/stacks/stackaddrs/doc.go b/internal/stacks/stackaddrs/doc.go new file mode 100644 index 0000000000..d8bfb87814 --- /dev/null +++ b/internal/stacks/stackaddrs/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package stackaddrs builds on the top-level "addrs" package to provide the +// addresses for the extra layer of concepts that the stacks configuration +// language and its runtime are concerned about. +package stackaddrs diff --git a/internal/stacks/stackaddrs/in_component.go b/internal/stacks/stackaddrs/in_component.go new file mode 100644 index 0000000000..ed26d0cdfc --- /dev/null +++ b/internal/stacks/stackaddrs/in_component.go @@ -0,0 +1,160 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// InConfigComponent represents addresses of objects that belong to the modules +// associated with a particular component. +// +// Although the type parameter is rather unconstrained, it doesn't make sense to +// use this for types other than those from package addrs that represent +// configuration constructs, like [addrs.ConfigResource], etc. +type InConfigComponent[T InComponentable] struct { + Component ConfigComponent + Item T +} + +// ConfigResource represents a resource configuration from inside a +// particular component. +type ConfigResource = InConfigComponent[addrs.ConfigResource] + +// ConfigModule represents a module from inside a particular component. +// +// Note that the string representation of the address of the root module of +// a component is identical to the string representation of the component +// address alone. +type ConfigModule = InConfigComponent[addrs.Module] + +func (c InConfigComponent[T]) String() string { + itemStr := c.Item.String() + componentStr := c.Component.String() + if itemStr == "" { + return componentStr + } + return componentStr + "." + itemStr +} + +// UniqueKey implements collections.UniqueKeyer. +func (c InConfigComponent[T]) UniqueKey() collections.UniqueKey[InConfigComponent[T]] { + return inConfigComponentKey[T]{ + componentKey: c.Component.UniqueKey(), + itemKey: c.Item.UniqueKey(), + } +} + +type inConfigComponentKey[T InComponentable] struct { + componentKey collections.UniqueKey[ConfigComponent] + itemKey addrs.UniqueKey +} + +// IsUniqueKey implements collections.UniqueKey. +func (inConfigComponentKey[T]) IsUniqueKey(InConfigComponent[T]) {} + +// InAbsComponentInstance represents addresses of objects that belong to the module +// instances associated with a particular component instance. +// +// Although the type parameter is rather unconstrained, it doesn't make sense to +// use this for types other than those from package addrs that represent +// objects that can belong to Terraform modules, like +// [addrs.AbsResourceInstance], etc. +type InAbsComponentInstance[T InComponentable] struct { + Component AbsComponentInstance + Item T +} + +// AbsResource represents a not-yet-expanded resource from inside a particular +// component instance. +type AbsResource = InAbsComponentInstance[addrs.AbsResource] + +var _ collections.UniqueKeyer[AbsResource] = AbsResource{} + +// AbsResourceInstance represents an instance of a resource from inside a +// particular component instance. +type AbsResourceInstance = InAbsComponentInstance[addrs.AbsResourceInstance] + +// AbsResourceInstanceObject represents an object associated with an instance +// of a resource from inside a particular component instance. +type AbsResourceInstanceObject = InAbsComponentInstance[addrs.AbsResourceInstanceObject] + +// AbsModuleInstance represents an instance of a module from inside a +// particular component instance. +// +// Note that the string representation of the address of the root module of +// a component instance is identical to the string representation of the +// component instance address alone. +type AbsModuleInstance = InAbsComponentInstance[addrs.ModuleInstance] + +func (c InAbsComponentInstance[T]) String() string { + itemStr := c.Item.String() + componentStr := c.Component.String() + if itemStr == "" { + return componentStr + } + return componentStr + "." + itemStr +} + +// UniqueKey implements collections.UniqueKeyer. +func (c InAbsComponentInstance[T]) UniqueKey() collections.UniqueKey[InAbsComponentInstance[T]] { + return inAbsComponentInstanceKey[T]{ + componentKey: c.Component.UniqueKey(), + itemKey: c.Item.UniqueKey(), + } +} + +type inAbsComponentInstanceKey[T InComponentable] struct { + componentKey collections.UniqueKey[AbsComponentInstance] + itemKey addrs.UniqueKey +} + +// IsUniqueKey implements collections.UniqueKey. +func (inAbsComponentInstanceKey[T]) IsUniqueKey(InAbsComponentInstance[T]) {} + +// InComponentable just embeds the interfaces that we require for the type +// parameters of both the [InConfigComponent] and [InAbsComponent] types. +type InComponentable interface { + addrs.UniqueKeyer + fmt.Stringer +} + +func ParseAbsResourceInstanceObject(traversal hcl.Traversal) (AbsResourceInstanceObject, tfdiags.Diagnostics) { + stack, remain, diags := ParseAbsComponentInstanceOnly(traversal) + if diags.HasErrors() { + return AbsResourceInstanceObject{}, diags + } + + resource, diags := addrs.ParseAbsResourceInstance(remain) + if diags.HasErrors() { + return AbsResourceInstanceObject{}, diags + } + + return AbsResourceInstanceObject{ + Component: stack, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: resource, + }, + }, diags +} + +func ParseAbsResourceInstanceObjectStr(s string) (AbsResourceInstanceObject, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return AbsResourceInstanceObject{}, diags + } + + ret, moreDiags := ParseAbsResourceInstanceObject(traversal) + diags = diags.Append(moreDiags) + return ret, diags +} diff --git a/internal/stacks/stackaddrs/in_stack.go b/internal/stacks/stackaddrs/in_stack.go new file mode 100644 index 0000000000..0ac90f00d2 --- /dev/null +++ b/internal/stacks/stackaddrs/in_stack.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StackItemConfig is a type set containing all of the address types that make +// sense to consider as belonging statically to a [Stack]. +type StackItemConfig[T any] interface { + inStackConfigSigil() + String() string + collections.UniqueKeyer[T] +} + +// StackItemDynamic is a type set containing all of the address types that make +// sense to consider as belonging dynamically to a [StackInstance]. +type StackItemDynamic[T any] interface { + inStackInstanceSigil() + String() string + collections.UniqueKeyer[T] +} + +// InStackConfig is the generic form of addresses representing configuration +// objects belonging to particular nodes in the static tree of stack +// configurations. +type InStackConfig[T StackItemConfig[T]] struct { + Stack Stack + Item T +} + +func Config[T StackItemConfig[T]](stackAddr Stack, relAddr T) InStackConfig[T] { + return InStackConfig[T]{ + Stack: stackAddr, + Item: relAddr, + } +} + +func (ist InStackConfig[T]) String() string { + if ist.Stack.IsRoot() { + return ist.Item.String() + } + return ist.Stack.String() + "." + ist.Item.String() +} + +func (ist InStackConfig[T]) UniqueKey() collections.UniqueKey[InStackConfig[T]] { + return inStackConfigKey[T]{ + stackKey: ist.Stack.UniqueKey(), + itemKey: ist.Item.UniqueKey(), + } +} + +type inStackConfigKey[T StackItemConfig[T]] struct { + stackKey collections.UniqueKey[Stack] + itemKey collections.UniqueKey[T] +} + +// IsUniqueKey implements collections.UniqueKey. +func (inStackConfigKey[T]) IsUniqueKey(InStackConfig[T]) {} + +// InStackInstance is the generic form of addresses representing dynamic +// instances of objects that exist within an instance of a stack. +type InStackInstance[T StackItemDynamic[T]] struct { + Stack StackInstance + Item T +} + +func Absolute[T StackItemDynamic[T]](stackAddr StackInstance, relAddr T) InStackInstance[T] { + return InStackInstance[T]{ + Stack: stackAddr, + Item: relAddr, + } +} + +func (ist InStackInstance[T]) String() string { + if ist.Stack.IsRoot() { + return ist.Item.String() + } + return ist.Stack.String() + "." + ist.Item.String() +} + +func (ist InStackInstance[T]) UniqueKey() collections.UniqueKey[InStackInstance[T]] { + return inStackInstanceKey[T]{ + stackKey: ist.Stack.UniqueKey(), + itemKey: ist.Item.UniqueKey(), + } +} + +type inStackInstanceKey[T StackItemDynamic[T]] struct { + stackKey collections.UniqueKey[StackInstance] + itemKey collections.UniqueKey[T] +} + +// IsUniqueKey implements collections.UniqueKey. +func (inStackInstanceKey[T]) IsUniqueKey(InStackInstance[T]) {} + +// ConfigForAbs returns the "in stack config" equivalent of the given +// "in stack instance" (absolute) address by just discarding any +// instance keys from the stack instance steps. +func ConfigForAbs[T interface { + StackItemDynamic[T] + StackItemConfig[T] +}](absAddr InStackInstance[T]) InStackConfig[T] { + return Config(absAddr.Stack.ConfigAddr(), absAddr.Item) +} + +// parseInStackInstancePrefix parses as many nested stack traversal steps +// as possible from the start of the given traversal, and then returns +// the resulting StackInstance address along with a relative traversal +// covering all of the remaining traversal steps, if any. +func parseInStackInstancePrefix(traversal hcl.Traversal) (StackInstance, hcl.Traversal, tfdiags.Diagnostics) { + if len(traversal) == 0 { + return RootStackInstance, nil, nil + } + + const errSummary = "Invalid stack instance address" + var diags tfdiags.Diagnostics + var stackInst StackInstance +Steps: + for len(traversal) > 0 { + switch step := traversal[0].(type) { + case hcl.TraverseRoot: + if step.Name != "stack" { + break Steps + } + case hcl.TraverseAttr: + if step.Name != "stack" { + break Steps + } + default: + break Steps + } + + // If we get here then we know that we're expecting a valid + // stack instance step prefix, which always consists of the + // literal step "stack" (which we found above) followed + // by an embedded stack name. That might then be followed + // by one optional index step for a multi-instance embedded stack. + if len(traversal) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: "The \"stack\" keyword must be followed by an attribute specifying the name of the embedded stack.", + Subject: traversal.SourceRange().Ptr(), + }) + return nil, nil, diags + } + nameStep, ok := traversal[1].(hcl.TraverseAttr) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: "The \"stack\" keyword must be followed by an attribute specifying the name of the embedded stack.", + Subject: traversal[1].SourceRange().Ptr(), + }) + return nil, nil, diags + } + if !hclsyntax.ValidIdentifier(nameStep.Name) { + // This check is redundant since the HCL parser should've caught + // an invalid identifier while parsing this traversal, but this + // is here for robustness in case we obtained this traversal + // value in an unusual way. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: "A stack name must be a valid identifier.", + Subject: nameStep.SourceRange().Ptr(), + }) + return nil, nil, diags + } + addrStep := StackInstanceStep{ + Name: nameStep.Name, + Key: addrs.NoKey, + } + traversal = traversal[2:] // consume the first two steps that we already dealt with + if len(traversal) > 0 { + switch idxStep := traversal[0].(type) { + case hcl.TraverseIndex: + var err error + addrStep.Key, err = addrs.ParseInstanceKey(idxStep.Key) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: fmt.Sprintf("Invalid instance key: %s.", err), + Subject: idxStep.SourceRange().Ptr(), + }) + return nil, nil, diags + } + traversal = traversal[1:] // consume the step we just dealt with + case hcl.TraverseSplat: + addrStep.Key = addrs.WildcardKey + traversal = traversal[1:] + } + } + stackInst = append(stackInst, addrStep) + } + return stackInst, forceTraversalRelative(traversal), diags +} + +// forceTraversalRelative takes any traversal and if it's absolute transforms +// it into a relative one by changing the first step from a TraverseRoot +// to an equivalent TraverseAttr. +func forceTraversalRelative(given hcl.Traversal) hcl.Traversal { + if len(given) == 0 { + return nil + } + firstStep, ok := given[0].(hcl.TraverseRoot) + if !ok { + return given + } + + // If we get here then we have an absolute traversal. We shouldn't + // mutate the backing array of the traversal because others might + // still be using it, so we'll allocate a new traversal and copy + // the steps into it. + ret := make(hcl.Traversal, len(given)) + ret[0] = hcl.TraverseAttr{ + Name: firstStep.Name, + SrcRange: firstStep.SrcRange, + } + copy(ret[1:], given[1:]) + return ret +} diff --git a/internal/stacks/stackaddrs/input_variable.go b/internal/stacks/stackaddrs/input_variable.go new file mode 100644 index 0000000000..30e3d33ebf --- /dev/null +++ b/internal/stacks/stackaddrs/input_variable.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type InputVariable struct { + Name string +} + +func (InputVariable) referenceableSigil() {} +func (InputVariable) inStackConfigSigil() {} +func (InputVariable) inStackInstanceSigil() {} + +func (v InputVariable) String() string { + return "var." + v.Name +} + +func (v InputVariable) UniqueKey() collections.UniqueKey[InputVariable] { + return v +} + +// An InputVariable is its own [collections.UniqueKey]. +func (InputVariable) IsUniqueKey(InputVariable) {} + +// ConfigInputVariable places an [InputVariable] in the context of a particular [Stack]. +type ConfigInputVariable = InStackConfig[InputVariable] + +// AbsInputVariable places an [InputVariable] in the context of a particular [StackInstance]. +type AbsInputVariable = InStackInstance[InputVariable] + +func ParseAbsInputVariableStr(s string) (AbsInputVariable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return AbsInputVariable{}, diags + } + + ret, moreDiags := ParseAbsInputVariable(traversal) + return ret, diags.Append(moreDiags) +} + +func ParseAbsInputVariable(traversal hcl.Traversal) (AbsInputVariable, tfdiags.Diagnostics) { + if traversal.IsRelative() { + // This is always a caller bug: caller must only pass absolute + // traversals in here. + panic("ParseAbsInputVariable with relative traversal") + } + + stackInst, remain, diags := parseInStackInstancePrefix(traversal) + if diags.HasErrors() { + return AbsInputVariable{}, diags + } + + if len(remain) != 2 { + // it must be output.name, no more and no less. + return AbsInputVariable{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid input variable address", + Detail: "The input variable address must be the keyword \"var\" followed by a variable name.", + Subject: traversal.SourceRange().Ptr(), + }) + } + + if kwStep, ok := remain[0].(hcl.TraverseAttr); !ok || kwStep.Name != "var" { + return AbsInputVariable{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid input variable address", + Detail: "The input variable address must be the keyword \"var\" followed by a variable name.", + Subject: remain[0].SourceRange().Ptr(), + }) + } + + nameStep, ok := remain[1].(hcl.TraverseAttr) + if !ok { + return AbsInputVariable{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid input variable address", + Detail: "The input variable address must be the keyword \"var\" followed by a variable name.", + Subject: remain[1].SourceRange().Ptr(), + }) + } + + return AbsInputVariable{ + Stack: stackInst, + Item: InputVariable{ + Name: nameStep.Name, + }, + }, diags +} diff --git a/internal/stacks/stackaddrs/local_value.go b/internal/stacks/stackaddrs/local_value.go new file mode 100644 index 0000000000..9801256162 --- /dev/null +++ b/internal/stacks/stackaddrs/local_value.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import "github.com/hashicorp/terraform/internal/collections" + +type LocalValue struct { + Name string +} + +func (LocalValue) referenceableSigil() {} +func (LocalValue) inStackConfigSigil() {} +func (LocalValue) inStackInstanceSigil() {} + +func (v LocalValue) String() string { + return "local." + v.Name +} + +func (v LocalValue) UniqueKey() collections.UniqueKey[LocalValue] { + return v +} + +// A LocalValue is its own [collections.UniqueKey]. +func (LocalValue) IsUniqueKey(LocalValue) {} + +// ConfigLocalValue places a [LocalValue] in the context of a particular [Stack]. +type ConfigLocalValue = InStackConfig[LocalValue] + +// AbsLocalValue places a [LocalValue] in the context of a particular [StackInstance]. +type AbsLocalValue = InStackInstance[LocalValue] diff --git a/internal/stacks/stackaddrs/output_value.go b/internal/stacks/stackaddrs/output_value.go new file mode 100644 index 0000000000..0c955d95e2 --- /dev/null +++ b/internal/stacks/stackaddrs/output_value.go @@ -0,0 +1,97 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type OutputValue struct { + Name string +} + +func (OutputValue) inStackConfigSigil() {} +func (OutputValue) inStackInstanceSigil() {} + +func (v OutputValue) String() string { + return "output." + v.Name +} + +func (v OutputValue) UniqueKey() collections.UniqueKey[OutputValue] { + return v +} + +// An OutputValue is its own [collections.UniqueKey]. +func (OutputValue) IsUniqueKey(OutputValue) {} + +// ConfigOutputValue places an [OutputValue] in the context of a particular [Stack]. +type ConfigOutputValue = InStackConfig[OutputValue] + +// AbsOutputValue places an [OutputValue] in the context of a particular [StackInstance]. +type AbsOutputValue = InStackInstance[OutputValue] + +func ParseAbsOutputValueStr(s string) (AbsOutputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(s), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return AbsOutputValue{}, diags + } + + ret, moreDiags := ParseAbsOutputValue(traversal) + return ret, diags.Append(moreDiags) +} + +func ParseAbsOutputValue(traversal hcl.Traversal) (AbsOutputValue, tfdiags.Diagnostics) { + if traversal.IsRelative() { + // This is always a caller bug: caller must only pass absolute + // traversals in here. + panic("ParseAbsOutputValue with relative traversal") + } + + stackInst, remain, diags := parseInStackInstancePrefix(traversal) + if diags.HasErrors() { + return AbsOutputValue{}, diags + } + + if len(remain) != 2 { + // it must be output.name, no more and no less. + return AbsOutputValue{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output address", + Detail: "The output address must be the keyword \"output\" followed by an output name.", + Subject: traversal.SourceRange().Ptr(), + }) + } + + if kwStep, ok := remain[0].(hcl.TraverseAttr); !ok || kwStep.Name != "output" { + return AbsOutputValue{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output address", + Detail: "The output address must be the keyword \"output\" followed by an output name.", + Subject: remain[0].SourceRange().Ptr(), + }) + } + + nameStep, ok := remain[1].(hcl.TraverseAttr) + if !ok { + return AbsOutputValue{}, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output address", + Detail: "The output address must be the keyword \"output\" followed by an output name.", + Subject: remain[1].SourceRange().Ptr(), + }) + } + + return AbsOutputValue{ + Stack: stackInst, + Item: OutputValue{ + Name: nameStep.Name, + }, + }, diags +} diff --git a/internal/stacks/stackaddrs/provider_config.go b/internal/stacks/stackaddrs/provider_config.go new file mode 100644 index 0000000000..b73f1e054d --- /dev/null +++ b/internal/stacks/stackaddrs/provider_config.go @@ -0,0 +1,96 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" +) + +// ProviderConfigRef is a reference-only address type representing a reference +// to a particular provider configuration using its local name, since local +// name is how we refer to providers when they appear in expressions. +// +// The referent of a ProviderConfigRef is a [ProviderConfig], so resolving +// the reference will always require a lookup table from local name to +// fully-qualified provider address. +type ProviderConfigRef struct { + ProviderLocalName string + Name string +} + +func (ProviderConfigRef) referenceableSigil() {} + +func (r ProviderConfigRef) String() string { + return "provider." + r.ProviderLocalName + "." + r.Name +} + +// ProviderConfig is the address of a "provider" block in a stack configuration. +type ProviderConfig struct { + Provider addrs.Provider + Name string +} + +func (ProviderConfig) inStackConfigSigil() {} +func (ProviderConfig) inStackInstanceSigil() {} + +func (c ProviderConfig) String() string { + return fmt.Sprintf("provider[%q].%s", c.Provider, c.Name) +} + +func (v ProviderConfig) UniqueKey() collections.UniqueKey[ProviderConfig] { + return v +} + +// A ProviderConfig is its own [collections.UniqueKey]. +func (ProviderConfig) IsUniqueKey(ProviderConfig) {} + +// ConfigProviderConfig places a [ProviderConfig] in the context of a particular [Stack]. +type ConfigProviderConfig = InStackConfig[ProviderConfig] + +// AbsProviderConfig places a [ProviderConfig] in the context of a particular [StackInstance]. +type AbsProviderConfig = InStackInstance[ProviderConfig] + +// ProviderConfigInstance is the address of a specific provider configuration, +// of which there might potentially be many associated with a given +// [ProviderConfig] if that block uses the "for_each" argument. +type ProviderConfigInstance struct { + ProviderConfig ProviderConfig + Key addrs.InstanceKey +} + +func (ProviderConfigInstance) inStackConfigSigil() {} +func (ProviderConfigInstance) inStackInstanceSigil() {} + +func (c ProviderConfigInstance) String() string { + if c.Key == nil { + return c.ProviderConfig.String() + } + return c.ProviderConfig.String() + c.Key.String() +} + +func (v ProviderConfigInstance) UniqueKey() collections.UniqueKey[ProviderConfigInstance] { + return v +} + +// A ProviderConfigInstance is its own [collections.UniqueKey]. +func (ProviderConfigInstance) IsUniqueKey(ProviderConfigInstance) {} + +// ConfigProviderConfigInstance places a [ProviderConfigInstance] in the context of a particular [Stack]. +type ConfigProviderConfigInstance = InStackConfig[ProviderConfigInstance] + +// AbsProviderConfigInstance places a [ProviderConfigInstance] in the context of a particular [StackInstance]. +type AbsProviderConfigInstance = InStackInstance[ProviderConfigInstance] + +func AbsProviderToInstance(addr AbsProviderConfig, ik addrs.InstanceKey) AbsProviderConfigInstance { + return AbsProviderConfigInstance{ + Stack: addr.Stack, + Item: ProviderConfigInstance{ + ProviderConfig: addr.Item, + Key: ik, + }, + } +} diff --git a/internal/stacks/stackaddrs/reference.go b/internal/stacks/stackaddrs/reference.go new file mode 100644 index 0000000000..82763271af --- /dev/null +++ b/internal/stacks/stackaddrs/reference.go @@ -0,0 +1,231 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Reference describes a reference expression found in the configuration, +// capturing what it referred to and where it was found in source code. +type Reference struct { + Target Referenceable + SourceRange tfdiags.SourceRange +} + +// ParseReference raises a raw absolute traversal into a higher-level reference, +// or returns error diagnostics explaining why it cannot. +// +// The returned traversal is a relative traversal covering the remainder of +// the given traversal after the part captured into the returned reference, +// in case the caller wants to do further validation or analysis of the +// subsequent steps. +func ParseReference(traversal hcl.Traversal) (Reference, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var ret Reference + switch rootName := traversal.RootName(); rootName { + + case "var": + name, rng, remain, diags := parseSingleAttrRef(traversal) + ret.Target = InputVariable{Name: name} + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + return ret, remain, diags + + case "local": + name, rng, remain, diags := parseSingleAttrRef(traversal) + ret.Target = LocalValue{Name: name} + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + return ret, remain, diags + + case "component": + name, rng, remain, diags := parseSingleAttrRef(traversal) + ret.Target = Component{Name: name} + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + return ret, remain, diags + + case "stack": + name, rng, remain, diags := parseSingleAttrRef(traversal) + ret.Target = StackCall{Name: name} + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + return ret, remain, diags + + case "provider": + target, rng, remain, diags := parseProviderRef(traversal) + ret.Target = target + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + return ret, remain, diags + + case "each", "count": + attrName, rng, remain, diags := parseSingleAttrRef(traversal) + if diags.HasErrors() { + return ret, nil, diags + } + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + + switch rootName { + case "each": + switch attrName { + case "key": + ret.Target = EachKey + return ret, remain, diags + case "value": + ret.Target = EachValue + return ret, remain, diags + } + case "count": + switch attrName { + case "index": + ret.Target = CountIndex + return ret, remain, diags + } + } + // If we get here then rootName and attrName are not a valid combination. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown symbol", + Detail: fmt.Sprintf("The object %q has no attribute named %q.", rootName, attrName), + Subject: traversal[1].SourceRange().Ptr(), + }) + return ret, nil, diags + + case "self": + ret.Target = Self + ret.SourceRange = tfdiags.SourceRangeFromHCL(traversal[0].SourceRange()) + return ret, traversal[1:], diags + + case "terraform": + attrName, rng, remain, diags := parseSingleAttrRef(traversal) + if diags.HasErrors() { + return ret, nil, diags + } + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + + switch attrName { + case "applying": + ret.Target = TerraformApplying + return ret, remain, diags + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown symbol", + Detail: fmt.Sprintf("The object %q has no attribute named %q.", rootName, attrName), + Subject: traversal[1].SourceRange().Ptr(), + }) + return ret, remain, diags + } + + case "_test_only_global": + name, rng, remain, diags := parseSingleAttrRef(traversal) + ret.Target = TestOnlyGlobal{Name: name} + ret.SourceRange = tfdiags.SourceRangeFromHCL(rng) + return ret, remain, diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown symbol", + Detail: fmt.Sprintf("There is no symbol %q defined in the current scope.", rootName), + Subject: traversal[0].SourceRange().Ptr(), + }) + return ret, nil, diags + } +} + +func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + root := traversal.RootName() + rootRange := traversal[0].SourceRange() + + if len(traversal) < 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root), + Subject: &rootRange, + }) + return "", hcl.Range{}, nil, diags + } + if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok { + return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The %q object does not support this operation.", root), + Subject: traversal[1].SourceRange().Ptr(), + }) + return "", hcl.Range{}, nil, diags +} + +func parseProviderRef(traversal hcl.Traversal) (ProviderConfigRef, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if len(traversal) < 3 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: "The \"provider\" symbol must be followed by two attribute access operations, selecting a provider type and a provider configuration name.", + Subject: traversal.SourceRange().Ptr(), + }) + return ProviderConfigRef{}, hcl.Range{}, nil, diags + } + if typeTrav, ok := traversal[1].(hcl.TraverseAttr); ok { + if nameTrav, ok := traversal[2].(hcl.TraverseAttr); ok { + ret := ProviderConfigRef{ + ProviderLocalName: typeTrav.Name, + Name: nameTrav.Name, + } + return ret, traversal.SourceRange(), traversal[3:], diags + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: "The \"provider\" object's attributes do not support this operation.", + Subject: traversal[1].SourceRange().Ptr(), + }) + } + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: "The \"provider\" object does not support this operation.", + Subject: traversal[1].SourceRange().Ptr(), + }) + } + return ProviderConfigRef{}, hcl.Range{}, nil, diags +} + +func (r Reference) Absolute(stack StackInstance) AbsReference { + return AbsReference{ + Stack: stack, + Ref: r, + } +} + +// AbsReference is an absolute form of [Reference] that is to be resolved +// in the global scope of a particular stack. +// +// It's not meaningful to use this type for references to objects that exist +// only in a more specific scope, such as each.key, each.value, etc, because +// those would require additional information about exactly which object +// they are being resolved in terms of. +type AbsReference struct { + Stack StackInstance + Ref Reference +} + +func (r AbsReference) Target() AbsReferenceable { + return AbsReferenceable{ + Stack: r.Stack, + Item: r.Ref.Target, + } +} + +func (r AbsReference) SourceRange() tfdiags.SourceRange { + return r.Ref.SourceRange +} diff --git a/internal/stacks/stackaddrs/referenceable.go b/internal/stacks/stackaddrs/referenceable.go new file mode 100644 index 0000000000..383e5729cb --- /dev/null +++ b/internal/stacks/stackaddrs/referenceable.go @@ -0,0 +1,107 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "reflect" + + "github.com/hashicorp/terraform/internal/collections" +) + +// Referenceable is a type set containing all address types that can be +// the target of an expression-based reference within a particular stack. +type Referenceable interface { + referenceableSigil() + String() string +} + +var _ Referenceable = Component{} +var _ Referenceable = StackCall{} +var _ Referenceable = InputVariable{} +var _ Referenceable = LocalValue{} +var _ Referenceable = ProviderConfigRef{} +var _ Referenceable = TestOnlyGlobal{} +var _ Referenceable = ContextualRef(0) + +// ReferenceableUniqueKey returns a unique key for a dynamically-typed +// referenceable address. +// +// Since the type of the target isn't statically known, the resulting unique +// key is not comparable with unique keys of the specific static types +// that implement [Referenceable]. +func ReferenceableUniqueKey(addr Referenceable) collections.UniqueKey[Referenceable] { + // NOTE: This assumes that all distinct referenceable addresses have + // distinct and unique string representations. + return referenceableUniqueKey{ + ty: reflect.TypeOf(addr), + str: addr.String(), + } +} + +type referenceableUniqueKey struct { + ty reflect.Type + str string +} + +// IsUniqueKey implements collections.UniqueKey. +func (referenceableUniqueKey) IsUniqueKey(Referenceable) {} + +type ContextualRef rune + +const EachValue = ContextualRef('v') +const EachKey = ContextualRef('k') +const CountIndex = ContextualRef('i') +const Self = ContextualRef('s') +const TerraformApplying = ContextualRef('a') + +// String implements Referenceable. +func (e ContextualRef) String() string { + switch e { + case EachKey: + return "each.key" + case EachValue: + return "each.value" + case CountIndex: + return "count.index" + case Self: + return "self" + case TerraformApplying: + return "terraform.applying" + default: + // The four constants in this package are the only valid values of this type + panic("invalid ContextualRef instance") + } +} + +// referenceableSigil implements Referenceable. +func (e ContextualRef) referenceableSigil() {} + +// AbsReferenceable is a [Referenceable] combined with the stack it would +// be resolved in. +// +// This type can be used only for [Referenceable] types that have stack-wide +// scope. It's not appropriate for referenceable objects with more specific +// scope, such as [ContextualRef], since describing those would require +// information about which specific block they are to be resolved within. +type AbsReferenceable struct { + Stack StackInstance + Item Referenceable +} + +func (r AbsReferenceable) UniqueKey() collections.UniqueKey[AbsReferenceable] { + return absReferenceableKey{ + stackKey: r.Stack.UniqueKey(), + itemKey: ReferenceableUniqueKey(r.Item), + } +} + +type absReferenceableKey struct { + stackKey collections.UniqueKey[StackInstance] + itemKey collections.UniqueKey[Referenceable] +} + +// IsUniqueKey implements collections.UniqueKey. +func (absReferenceableKey) IsUniqueKey(AbsReferenceable) { + panic("unimplemented") +} diff --git a/internal/stacks/stackaddrs/removed.go b/internal/stacks/stackaddrs/removed.go new file mode 100644 index 0000000000..3d6697d596 --- /dev/null +++ b/internal/stacks/stackaddrs/removed.go @@ -0,0 +1,374 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type RemovedFrom struct { + Stack []StackRemovedFrom + + // Component to be removed. Optional, if not set then the whole stack + // should be removed. + Component *ComponentRemovedFrom +} + +func (rf RemovedFrom) TargetStack() Stack { + stack := make(Stack, 0, len(rf.Stack)) + for _, step := range rf.Stack { + stack = append(stack, StackStep{Name: step.Name}) + } + return stack +} + +func (rf RemovedFrom) TargetConfigComponent() ConfigComponent { + if rf.Component == nil { + panic("should call TargetStack() when no component was specified") + } + return ConfigComponent{ + Stack: rf.TargetStack(), + Item: Component{ + rf.Component.Name, + }, + } +} + +func (rf RemovedFrom) Variables() []hcl.Traversal { + var traversals []hcl.Traversal + for _, step := range rf.Stack { + if step.Index != nil { + traversals = append(traversals, step.Index.Variables()...) + } + } + if rf.Component != nil && rf.Component.Index != nil { + traversals = append(traversals, rf.Component.Index.Variables()...) + } + return traversals +} + +func (rf RemovedFrom) TargetStackInstance(ctx *hcl.EvalContext, parent StackInstance) (StackInstance, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var stackInstance StackInstance + for _, stack := range rf.Stack { + step, moreDiags := stack.StackInstanceStep(ctx) + diags = diags.Append(moreDiags) + + stackInstance = append(stackInstance, step) + } + return append(parent, stackInstance...), diags +} + +func (rf RemovedFrom) TargetAbsComponentInstance(ctx *hcl.EvalContext, parent StackInstance) (AbsComponentInstance, tfdiags.Diagnostics) { + if rf.Component == nil { + panic("should call TargetStackInstance() when no component was specified") + } + var diags tfdiags.Diagnostics + stackInstance, moreDiags := rf.TargetStackInstance(ctx, parent) + diags = diags.Append(moreDiags) + componentInstance, moreDiags := rf.Component.ComponentInstance(ctx) + diags = diags.Append(moreDiags) + + return AbsComponentInstance{Stack: stackInstance, Item: componentInstance}, diags +} + +type StackRemovedFrom struct { + Name string + Index hcl.Expression +} + +func (rf StackRemovedFrom) StackStep() StackStep { + return StackStep{Name: rf.Name} +} + +func (rf StackRemovedFrom) StackInstanceStep(ctx *hcl.EvalContext) (StackInstanceStep, tfdiags.Diagnostics) { + key, diags := exprAsKey(rf.Index, ctx) + return StackInstanceStep{ + Name: rf.Name, + Key: key, + }, diags +} + +type ComponentRemovedFrom struct { + Name string + Index hcl.Expression +} + +func (rf ComponentRemovedFrom) Component() Component { + return Component{ + Name: rf.Name, + } +} + +func (rf ComponentRemovedFrom) ComponentInstance(ctx *hcl.EvalContext) (ComponentInstance, tfdiags.Diagnostics) { + key, diags := exprAsKey(rf.Index, ctx) + return ComponentInstance{ + Component: Component{ + Name: rf.Name, + }, + Key: key, + }, diags +} + +// ParseRemovedFrom parses the "from" attribute of a "removed" block in a +// configuration and returns the address of the configuration object being +// removed. +// +// In addition to the address, this function also returns a traversal that +// represents the unparsed index within the from expression. Users can +// optionally specify a specific index of a component to target. +func ParseRemovedFrom(expr hcl.Expression) (RemovedFrom, tfdiags.Diagnostics) { + // we always return the same diagnostic from this function when we + // error, so we'll encapsulate it here. + diag := &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: expr.Range().Ptr(), + } + + var diags tfdiags.Diagnostics + + removedFrom := RemovedFrom{} + + current, moreDiags := exprToComponentTraversal(expr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return RemovedFrom{}, diags + } + + for current != nil { + + // we're going to parse the traversal in sets of 2-3 depending on + // the indices, so we'll check that now. + nextTraversal := current.Current + + for len(nextTraversal) > 0 { + var currentTraversal hcl.Traversal + var indexExpr hcl.Expression + + switch { + case len(nextTraversal) < 2: + // this is simply an error, we always need at least 2 values + // for either stack.name or component.name. + return RemovedFrom{}, diags.Append(diag) + case len(nextTraversal) == 2: + indexExpr = current.Index + currentTraversal = nextTraversal + nextTraversal = nil + case len(nextTraversal) == 3: + if current.Index != nil { + // this is an error, the last traversal should be taking + // its index from the outer value if it exists, and to be + // exactly three means something is invalid somewhere. + return RemovedFrom{}, diags.Append(diag) + } + + index, ok := nextTraversal[2].(hcl.TraverseIndex) + if !ok { + // This is an error, with exactly 3 we don't have another + // traversal to go to after this so the last entry must + // be the index. + return RemovedFrom{}, diags.Append(diag) + } + + currentTraversal = nextTraversal + nextTraversal = nil + indexExpr = hcl.StaticExpr(index.Key, index.SrcRange) + + default: // len(nextTraversal) > 3 + if index, ok := nextTraversal[2].(hcl.TraverseIndex); ok { + currentTraversal = nextTraversal[:3] + nextTraversal = nextTraversal[3:] + indexExpr = hcl.StaticExpr(index.Key, index.SrcRange) + break + } + currentTraversal = nextTraversal[:2] + nextTraversal = nextTraversal[2:] + } + + var name string + + switch root := currentTraversal[0].(type) { + case hcl.TraverseRoot: + name = root.Name + case hcl.TraverseAttr: + name = root.Name + default: + return RemovedFrom{}, diags.Append(diag) + } + + switch name { + case "component": + name, ok := currentTraversal[1].(hcl.TraverseAttr) + if !ok { + return RemovedFrom{}, diags.Append(diag) + } + + if len(nextTraversal) > 0 || current.Rest != nil { + return RemovedFrom{}, diags.Append(diag) + } + + removedFrom.Component = &ComponentRemovedFrom{ + Name: name.Name, + Index: indexExpr, + } + return removedFrom, diags + case "stack": + name, ok := currentTraversal[1].(hcl.TraverseAttr) + if !ok { + return RemovedFrom{}, diags.Append(diag) + } + + removedFrom.Stack = append(removedFrom.Stack, StackRemovedFrom{ + Name: name.Name, + Index: indexExpr, + }) + + default: + return RemovedFrom{}, diags.Append(diag) + } + } + + current = current.Rest + } + + // if we fall out, then we're just targeting a stack directly instead of a + // component in a stack + return removedFrom, diags +} + +type parsedFromExpr struct { + Current hcl.Traversal + Index hcl.Expression + Rest *parsedFromExpr +} + +// exprToComponentTraversal converts an HCL expression into a traversal that +// represents the component being targeted. We have to handle parsing this +// ourselves because removed block from arguments can contain index expressions +// which are not supported by hcl.AbsTraversalForExpr. +// +// The return values are (1) the part of the expression that can be converted +// into a traversal, (2) the index at the end of the traversal if it is an +// expression, (3) the remainder of the expression that needs to be parsed +// after (1) has been, and (4) the diagnostics. +func exprToComponentTraversal(expr hcl.Expression) (*parsedFromExpr, hcl.Diagnostics) { + switch e := expr.(type) { + case *hclsyntax.IndexExpr: + + current, diags := exprToComponentTraversal(e.Collection) + if diags.HasErrors() { + return nil, diags + } + + for next := current; next != nil; next = next.Rest { + if next.Rest == nil { + next.Index = e.Key + } + } + + return current, diags + + case *hclsyntax.RelativeTraversalExpr: + + current, diags := exprToComponentTraversal(e.Source) + if diags.HasErrors() { + return nil, diags + } + + for next := current; next != nil; next = next.Rest { + if next.Rest == nil { + next.Rest = &parsedFromExpr{ + Current: e.Traversal, + } + break + } + } + + return current, diags + + default: + + // For anything else, just rely on the default traversal logic. + + t, diags := hcl.AbsTraversalForExpr(expr) + if diags.HasErrors() { + return nil, diags + } + return &parsedFromExpr{ + Current: t, + Index: nil, + Rest: nil, + }, diags + + } +} + +func exprAsKey(expr hcl.Expression, ctx *hcl.EvalContext) (addrs.InstanceKey, tfdiags.Diagnostics) { + if expr == nil { + return addrs.NoKey, nil + } + var diags tfdiags.Diagnostics + + value, moreDiags := expr.Value(ctx) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return addrs.WildcardKey, diags + } + + if value.IsNull() { + return addrs.WildcardKey, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid `from` attribute", + Detail: "The `from` attribute has an invalid index: cannot be null.", + Subject: expr.Range().Ptr(), + Expression: expr, + EvalContext: ctx, + }) + } + + if !value.IsKnown() { + switch value.Type() { + case cty.String, cty.Number: + // this is potentially the right type, so we'll allow this + return addrs.WildcardKey, diags + case cty.DynamicPseudoType: + // not ideal, but we can't confirm this for sure so we'll allow it + return addrs.WildcardKey, diags + default: + // bad, this isn't the right type even if we don't know what the + // value actually will be in the end + return addrs.WildcardKey, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid `from` attribute", + Detail: "The `from` attribute has an invalid index: either a string or integer is required.", + Subject: expr.Range().Ptr(), + Expression: expr, + EvalContext: ctx, + }) + } + } + + key, err := addrs.ParseInstanceKey(value) + if err != nil { + return addrs.WildcardKey, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid `from` attribute", + Detail: fmt.Sprintf("The `from` attribute has an invalid index: %s.", err), + Subject: expr.Range().Ptr(), + Expression: expr, + EvalContext: ctx, + }) + } + + return key, diags +} diff --git a/internal/stacks/stackaddrs/removed_test.go b/internal/stacks/stackaddrs/removed_test.go new file mode 100644 index 0000000000..b2b0b85274 --- /dev/null +++ b/internal/stacks/stackaddrs/removed_test.go @@ -0,0 +1,459 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestParseRemovedFrom_Stacks(t *testing.T) { + tcs := []struct { + from string + want StackInstance + vars map[string]cty.Value + parseDiags func() tfdiags.Diagnostics + addrDiags func() tfdiags.Diagnostics + }{ + { + from: "stack.stack_name", + want: mustStackInstance(t, "stack.stack_name"), + }, + { + from: "stack.parent.stack.child", + want: mustStackInstance(t, "stack.parent.stack.child"), + }, + { + from: "stack.parent[each.key].stack.child", + want: mustStackInstance(t, "stack.parent[\"parent\"].stack.child"), + vars: map[string]cty.Value{ + "each": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("parent"), + }), + }, + }, + { + from: "stack.parent.stack.child[each.key]", + want: mustStackInstance(t, "stack.parent.stack.child[\"child\"]"), + vars: map[string]cty.Value{ + "each": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("child"), + }), + }, + }, + } + for _, tc := range tcs { + t.Run(tc.from, func(t *testing.T) { + expr := mustExpr(t, tc.from) + from, parseDiags := ParseRemovedFrom(expr) + + var wantParseDiags tfdiags.Diagnostics + if tc.parseDiags != nil { + wantParseDiags = tc.parseDiags() + } + tfdiags.AssertDiagnosticsMatch(t, parseDiags, wantParseDiags) + + if from.Component != nil { + t.Fatal("from.Component should be empty") + } + + configAddress := from.TargetStack() + instanceAddress, addrDiags := from.TargetStackInstance(&hcl.EvalContext{ + Variables: tc.vars, + }, RootStackInstance) + var wantAddrDiags tfdiags.Diagnostics + if tc.addrDiags != nil { + wantAddrDiags = tc.addrDiags() + } + tfdiags.AssertDiagnosticsMatch(t, addrDiags, wantAddrDiags) + + wantConfigAddress := tc.want.ConfigAddr() + if diff := cmp.Diff(configAddress.String(), wantConfigAddress.String()); len(diff) > 0 { + t.Errorf("wrong config address; %s", diff) + } + if diff := cmp.Diff(instanceAddress.String(), tc.want.String()); len(diff) > 0 { + t.Errorf("wrong instance address: %s", diff) + } + }) + } +} + +func TestParseRemovedFrom_Components(t *testing.T) { + tcs := []struct { + from string + want AbsComponentInstance + vars map[string]cty.Value + parseDiags func() tfdiags.Diagnostics + addrDiags func() tfdiags.Diagnostics + }{ + { + from: "component.component_name", + want: mustAbsComponentInstance(t, "component.component_name"), + }, + { + from: "component.component_name[0]", + want: mustAbsComponentInstance(t, "component.component_name[0]"), + }, + { + from: "component.component_name[\"key\"]", + want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), + }, + { + from: "component.component_name[each.key]", + want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("key"), + }), + }, + }, + { + from: "component.component_name[each.value.attribute]", + want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "attribute": cty.StringVal("attribute"), + }), + }), + }, + }, + { + from: "component.component_name[each.value[\"key\"]]", + want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("key"), + }), + }), + }, + }, + { + from: "component.component_name[each.value[\"key\"].attribute]", + want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.MapVal(map[string]cty.Value{ + "key": cty.ObjectVal(map[string]cty.Value{ + "attribute": cty.StringVal("attribute"), + }), + }), + }), + }, + }, + { + from: "component.component_name[each.value[local.key]]", + want: mustAbsComponentInstance(t, "component.component_name[\"key\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("key"), + }), + }), + "local": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("key"), + }), + }, + }, + { + from: "component.component_name[each.value[local.key].attribute]", + want: mustAbsComponentInstance(t, "component.component_name[\"attribute\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.MapVal(map[string]cty.Value{ + "key": cty.ObjectVal(map[string]cty.Value{ + "attribute": cty.StringVal("attribute"), + }), + }), + }), + "local": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("key"), + }), + }, + }, + { + from: "stack.stack_name.component.component_name", + want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name"), + }, + { + from: "stack.parent.stack.child.component.component_name", + want: mustAbsComponentInstance(t, "stack.parent.stack.child.component.component_name"), + }, + { + from: "stack.stack_name[\"stack\"].component.component_name", + want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name"), + }, + { + from: "stack.stack_name.component.component_name[\"component\"]", + want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name[\"component\"]"), + }, + { + from: "stack.stack_name[\"stack\"].component.component_name[\"component\"]", + want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), + }, + { + from: "stack.stack_name.component.component_name[each.value]", + want: mustAbsComponentInstance(t, "stack.stack_name.component.component_name[\"component\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("component"), + }), + }, + }, + { + from: "stack.stack_name[\"stack\"].component.component_name[each.value]", + want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("component"), + }), + }, + }, + { + from: "stack.stack_name[each.value].component.component_name", + want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("stack"), + }), + }, + }, + { + from: "stack.stack_name[each.value].component.component_name[\"component\"]", + want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("stack"), + }), + }, + }, + { + from: "stack.stack_name[each.value[\"stack\"]].component.component_name[each.value[\"component\"]]", + want: mustAbsComponentInstance(t, "stack.stack_name[\"stack\"].component.component_name[\"component\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "stack": cty.StringVal("stack"), + "component": cty.StringVal("component"), + }), + }), + }, + }, + { + from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name", + want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "parent": cty.StringVal("parent"), + "child": cty.StringVal("child"), + "component": cty.StringVal("component"), + }), + }), + }, + }, + { + from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name[\"component\"]", + want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name[\"component\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "parent": cty.StringVal("parent"), + "child": cty.StringVal("child"), + }), + }), + }, + }, + { + from: "stack.parent[each.value[\"parent\"]].stack.child[each.value[\"child\"]].component.component_name[each.value[\"component\"]]", + want: mustAbsComponentInstance(t, "stack.parent[\"parent\"].stack.child[\"child\"].component.component_name[\"component\"]"), + vars: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "value": cty.ObjectVal(map[string]cty.Value{ + "parent": cty.StringVal("parent"), + "child": cty.StringVal("child"), + "component": cty.StringVal("component"), + }), + }), + }, + }, + { + from: "component.component_name.attribute_key", + parseDiags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 39, Byte: 38}, + }, + }) + return diags + }, + }, + { + from: "component.component_name[0].attribute_key", + parseDiags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 42, Byte: 41}, + }, + }) + return diags + }, + }, + { + from: "component.component_name[\"key\"].attribute_key", + parseDiags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 46, Byte: 45}, + }, + }) + return diags + }, + }, + { + from: "component.component_name[each.key].attribute_key", + parseDiags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 49, Byte: 48}, + }, + }) + return diags + }, + }, + { + from: "component.component_name.attribute_key[0]", + parseDiags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 42, Byte: 41}, + }, + }) + return diags + }, + }, + { + from: "component[0].component_name", + parseDiags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'from' attribute", + Detail: "The 'from' attribute must designate a component or stack that has been removed, in the form of an address such as `component.component_name` or `stack.stack_name`.", + Subject: &hcl.Range{ + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 28, Byte: 27}, + }, + }) + return diags + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.from, func(t *testing.T) { + expr := mustExpr(t, tc.from) + from, parseDiags := ParseRemovedFrom(expr) + + var wantParseDiags tfdiags.Diagnostics + if tc.parseDiags != nil { + wantParseDiags = tc.parseDiags() + } + tfdiags.AssertDiagnosticsMatch(t, parseDiags, wantParseDiags) + if len(wantParseDiags) > 0 { + return // don't do the rest of the test if we expected to fail here + } + + if from.Component == nil { + t.Fatal("from.Component should not be empty") + } + + configAddress := from.TargetConfigComponent() + instanceAddress, addrDiags := from.TargetAbsComponentInstance(&hcl.EvalContext{ + Variables: tc.vars, + }, RootStackInstance) + var wantAddrDiags tfdiags.Diagnostics + if tc.addrDiags != nil { + wantAddrDiags = tc.addrDiags() + } + tfdiags.AssertDiagnosticsMatch(t, addrDiags, wantAddrDiags) + + wantConfigAddress := ConfigComponent{ + Stack: tc.want.Stack.ConfigAddr(), + Item: tc.want.Item.Component, + } + if diff := cmp.Diff(configAddress.String(), wantConfigAddress.String()); len(diff) > 0 { + t.Errorf("wrong config address; %s", diff) + } + if diff := cmp.Diff(instanceAddress.String(), tc.want.String()); len(diff) > 0 { + t.Errorf("wrong instance address: %s", diff) + } + }) + } + +} + +func mustStackInstance(t *testing.T, str string) StackInstance { + traversal, hclDiags := hclsyntax.ParseTraversalPartial([]byte(str), "", hcl.InitialPos) + if len(hclDiags) > 0 { + t.Fatal(hclDiags.Error()) + } + inst, rest, diags := parseInStackInstancePrefix(traversal) + if len(diags) > 0 { + t.Fatal(diags.Err()) + } + + if len(rest) > 0 { + t.Fatal("invalid stack instance, has extra steps") + } + return inst +} + +func mustAbsComponentInstance(t *testing.T, str string) AbsComponentInstance { + inst, diags := ParseAbsComponentInstanceStr(str) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + return inst +} + +func mustExpr(t *testing.T, expr string) hcl.Expression { + ret, diags := hclsyntax.ParseExpression([]byte(expr), "", hcl.InitialPos) + if diags.HasErrors() { + t.Fatal(diags.Error()) + } + return ret +} diff --git a/internal/stacks/stackaddrs/stack.go b/internal/stacks/stackaddrs/stack.go new file mode 100644 index 0000000000..b4b45182b7 --- /dev/null +++ b/internal/stacks/stackaddrs/stack.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "strings" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" +) + +// Stack represents the address of a stack within the tree of stacks. +// +// The root stack [RootStack] represents the top-level stack and then any +// other value of this type represents an embedded stack descending from it. +type Stack []StackStep + +type StackStep struct { + Name string +} + +var RootStack Stack + +// IsRoot returns true if this object represents the root stack, or false +// otherwise. +func (s Stack) IsRoot() bool { + return len(s) == 0 +} + +// Parent returns the parent of the reciever, or panics if the receiver is +// representing the root stack. +func (s Stack) Parent() Stack { + newLen := len(s) - 1 + if newLen < 0 { + panic("root stack has no parent") + } + return s[:newLen:newLen] +} + +// Child constructs the address of an embedded stack that's a child of the +// receiver. +func (s Stack) Child(name string) Stack { + ret := make([]StackStep, len(s), len(s)+1) + copy(ret, s) + return append(ret, StackStep{name}) +} + +func (s Stack) String() string { + if s.IsRoot() { + // Callers should typically not ask for the string representation of + // the main root stack, but we'll return a reasonable placeholder + // for situations like e.g. internal logs where we just fmt %s in an + // arbitrary stack address that is sometimes the main stack. + return "
" + } + var buf strings.Builder + for i, step := range s { + if i != 0 { + buf.WriteByte('.') + } + buf.WriteString("stack.") + buf.WriteString(step.Name) + } + return buf.String() +} + +func (s Stack) UniqueKey() collections.UniqueKey[Stack] { + return stackUniqueKey(s.String()) +} + +// ToStackCall converts the stack address into the absolute address of the stack +// call that would create this stack. +func (s Stack) ToStackCall() ConfigStackCall { + return ConfigStackCall{ + Stack: s.Parent(), + Item: StackCall{ + Name: s[len(s)-1].Name, + }, + } +} + +type stackUniqueKey string + +// IsUniqueKey implements collections.UniqueKey. +func (stackUniqueKey) IsUniqueKey(Stack) {} + +// StackInstance represents the address of an instance of a stack within +// the tree of stacks. +// +// [RootStackInstance] represents the singleton instance of the top-level stack +// and then any other value of this type represents an instance of an embedded +// stack descending from it. +type StackInstance []StackInstanceStep + +type StackInstanceStep struct { + Name string + Key addrs.InstanceKey +} + +var RootStackInstance StackInstance + +// IsRoot returns true if this object represents the singleton instance of the +// root stack, or false otherwise. +func (s StackInstance) IsRoot() bool { + return len(s) == 0 +} + +// Parent returns the parent of the reciever, or panics if the receiver is +// representing the root stack. +func (s StackInstance) Parent() StackInstance { + newLen := len(s) - 1 + if newLen < 0 { + panic("root stack has no parent") + } + return s[:newLen:newLen] +} + +// Child constructs the address of an embedded stack that's a child of the +// receiver. +func (s StackInstance) Child(name string, key addrs.InstanceKey) StackInstance { + ret := make([]StackInstanceStep, len(s), len(s)+1) + copy(ret, s) + return append(ret, StackInstanceStep{ + Name: name, + Key: key, + }) +} + +// Call returns the address of the embedded stack call that the receiever +// belongs to, or panics if the receiver is the root module since the root +// module is called only implicitly. +func (s StackInstance) Call() AbsStackCall { + last := s[len(s)-1] + si := s[: len(s)-1 : len(s)-1] + return AbsStackCall{ + Stack: si, + Item: StackCall{ + Name: last.Name, + }, + } +} + +// ConfigAddr returns the [Stack] corresponding to the receiving [StackInstance]. +func (s StackInstance) ConfigAddr() Stack { + if s.IsRoot() { + return RootStack + } + ret := make(Stack, len(s)) + for i, step := range s { + ret[i] = StackStep{Name: step.Name} + } + return ret +} + +func (s StackInstance) String() string { + if s.IsRoot() { + // Callers should typically not ask for the string representation of + // the main root stack, but we'll return a reasonable placeholder + // for situations like e.g. internal logs where we just fmt %s in an + // arbitrary stack address that is sometimes the main stack. + return "
" + } + var buf strings.Builder + for i, step := range s { + if i != 0 { + buf.WriteByte('.') + } + buf.WriteString("stack.") + buf.WriteString(step.Name) + if step.Key != nil { + buf.WriteString(step.Key.String()) + } + } + return buf.String() +} + +func (s StackInstance) UniqueKey() collections.UniqueKey[StackInstance] { + return stackInstanceUniqueKey(s.String()) +} + +// Contains returns true if the receiver contains the given stack, or false +// otherwise. Contains is true if stack is a child stack of the receiver. If +// stack is the same as the receiver, Contains returns true. +func (s StackInstance) Contains(stack StackInstance) bool { + if len(s) > len(stack) { + return false + } + + for ix, step := range s { + if stack[ix].Name != step.Name { + return false + } + if stack[ix].Key != step.Key { + return false + } + } + return true +} + +type stackInstanceUniqueKey string + +// IsUniqueKey implements collections.UniqueKey. +func (stackInstanceUniqueKey) IsUniqueKey(StackInstance) {} diff --git a/internal/stacks/stackaddrs/stack_call.go b/internal/stacks/stackaddrs/stack_call.go new file mode 100644 index 0000000000..d77aab60b3 --- /dev/null +++ b/internal/stacks/stackaddrs/stack_call.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" +) + +// StackCall represents a call to an embedded stack. This is essentially the +// address of a "stack" block in the configuration, before it's been fully +// expanded into zero or more instances. +type StackCall struct { + Name string +} + +func (StackCall) referenceableSigil() {} +func (StackCall) inStackConfigSigil() {} +func (StackCall) inStackInstanceSigil() {} + +func (c StackCall) String() string { + return "stack." + c.Name +} + +func (c StackCall) UniqueKey() collections.UniqueKey[StackCall] { + return stackCallUniqueKey(c.String()) +} + +type stackCallUniqueKey string + +// IsUniqueKey implements collections.UniqueKey. +func (stackCallUniqueKey) IsUniqueKey(StackCall) {} + +// ConfigStackCall represents a static stack call inside a particular [Stack]. +type ConfigStackCall = InStackConfig[StackCall] + +// AbsStackCall represents an instance of a stack call inside a particular +// [StackInstance[. +type AbsStackCall = InStackInstance[StackCall] + +func AbsStackCallInstance(call AbsStackCall, key addrs.InstanceKey) StackInstance { + ret := make(StackInstance, len(call.Stack), len(call.Stack)+1) + copy(ret, call.Stack) + return append(ret, StackInstanceStep{ + Name: call.Item.Name, + Key: key, + }) +} diff --git a/internal/stacks/stackaddrs/stack_test.go b/internal/stacks/stackaddrs/stack_test.go new file mode 100644 index 0000000000..d5caa8b06f --- /dev/null +++ b/internal/stacks/stackaddrs/stack_test.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" +) + +func TestStackInstance_Contains(t *testing.T) { + tests := []struct { + name string + parent StackInstance + arg StackInstance + want bool + }{ + { + name: "root contains root", + parent: RootStackInstance, + arg: RootStackInstance, + want: true, + }, + { + name: "root contains child", + parent: RootStackInstance, + arg: RootStackInstance.Child("child", addrs.NoKey), + want: true, + }, + { + name: "child does not contain root", + parent: RootStackInstance.Child("child", addrs.NoKey), + arg: RootStackInstance, + want: false, + }, + { + name: "child contains itself", + parent: RootStackInstance.Child("child", addrs.NoKey), + arg: RootStackInstance.Child("child", addrs.NoKey), + want: true, + }, + { + name: "child contains grandchild", + parent: RootStackInstance.Child("child", addrs.NoKey), + arg: RootStackInstance.Child("child", addrs.NoKey).Child("grandchild", addrs.NoKey), + want: true, + }, + { + name: "grandchild does not contain child", + parent: RootStackInstance.Child("child", addrs.NoKey).Child("grandchild", addrs.NoKey), + arg: RootStackInstance.Child("child", addrs.NoKey), + want: false, + }, + { + name: "different keys are not contained", + parent: RootStackInstance.Child("child", addrs.NoKey), + arg: RootStackInstance.Child("child", addrs.IntKey(1)), + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.parent.Contains(tt.arg); got != tt.want { + t.Errorf("StackInstance.Contains() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/stacks/stackaddrs/targetable.go b/internal/stacks/stackaddrs/targetable.go new file mode 100644 index 0000000000..70d3a8c66d --- /dev/null +++ b/internal/stacks/stackaddrs/targetable.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +import ( + "github.com/hashicorp/terraform/internal/addrs" +) + +// Targetable is the stacks analog to [addrs.Targetable], representing something +// that can be "targeted" inside a stack configuration. +type Targetable interface { + targetableSigil() +} + +// ComponentTargetable is an adapter type that makes everything that's +// targetable in the main Terraform language also targetable through a +// component instance when in a stack configuration. +// +// To represent targeting an entire component, place [addrs.RootModuleInstance] +// in field Item to describe targeting the component's root module. +type ComponentTargetable[T addrs.Targetable] struct { + Component AbsComponentInstance + Item T +} + +func (ComponentTargetable[T]) targetableSigil() {} diff --git a/internal/stacks/stackaddrs/test_only_global.go b/internal/stacks/stackaddrs/test_only_global.go new file mode 100644 index 0000000000..364aa7d0f5 --- /dev/null +++ b/internal/stacks/stackaddrs/test_only_global.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackaddrs + +// TestOnlyGlobal is a special referenceable address type used only in +// stackruntime and stackeval package unit tests, as a way to introduce +// arbitrary test data into scope with minimal ceremony and thus in a way +// that's less likely to be regressed by changes to real language features. +// +// Addresses of this type behave as if they are completely unrecognized +// addresses when used in a non-test context. +type TestOnlyGlobal struct { + Name string +} + +// String implements Referenceable. +func (g TestOnlyGlobal) String() string { + return "_test_only_global." + g.Name +} + +// referenceableSigil implements Referenceable. +func (g TestOnlyGlobal) referenceableSigil() {} diff --git a/internal/stacks/stackconfig/component.go b/internal/stacks/stackconfig/component.go new file mode 100644 index 0000000000..46960e5590 --- /dev/null +++ b/internal/stacks/stackconfig/component.go @@ -0,0 +1,335 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + stackparser "github.com/hashicorp/terraform/internal/stacks/stackconfig/parser" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Component represents the declaration of a single component within a +// particular [Stack]. +// +// Components are the most important object in a stack configuration, just as +// resources are the most important object in a Terraform module: each one +// refers to a Terraform module that describes the infrastructure that the +// component is "made of". +type Component struct { + Name string + + SourceAddr sourceaddrs.Source + VersionConstraints constraints.IntersectionSpec + SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange + + // FinalSourceAddr is populated only when a configuration is loaded + // through [LoadConfigDir], and in that case contains the finalized + // address produced by resolving the SourceAddr field relative to + // the address of the file where the component was declared. This + // is the address to use if you intend to load the component's + // root module from a source bundle. + // + // If this Component was created through one of the narrower configuration + // loading functions, such as [LoadSingleStackConfig] or [ParseFileSource], + // then this field will be nil and it won't be possible to determine the + // finalized source location for the root module. + FinalSourceAddr sourceaddrs.FinalSource + + ForEach hcl.Expression + + // Inputs is an expression that should produce a value that can convert + // to an object type derived from the component's input variable + // declarations, and whose attribute values will then be used to populate + // those input variables. + Inputs hcl.Expression + + // ProviderConfigs describes the mapping between the static provider + // configuration slots declared in the component's root module and the + // dynamic provider configuration objects in scope in the calling + // stack configuration. + // + // This map deals with the slight schism between the stacks language's + // treatment of provider configurations as regular values of a special + // data type vs. the main Terraform language's treatment of provider + // configurations as something special passed out of band from the + // input variables. The overall structure and the map keys are fixed + // statically during decoding, but the final provider configuration objects + // are determined only at runtime by normal expression evaluation. + // + // The keys of this map refer to provider configuration slots inside + // the module being called, but use the local names defined in the + // calling stack configuration. The stacks language runtime will + // translate the caller's local names into the callee's declared provider + // configurations by using the stack configuration's table of local + // provider names. + ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression + + // DependsOn forces a dependency between this resource and the list + // resources, allowing users to specify ordering of components without + // direct references. + DependsOn []hcl.Traversal + + DeclRange tfdiags.SourceRange +} + +// ModuleConfig returns the module configuration for the given address within +// the provided source bundle. +func (c *Component) ModuleConfig(bundle *sourcebundle.Bundle) (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + parser := configs.NewSourceBundleParser(bundle) + if !parser.IsConfigDir(c.FinalSourceAddr) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Component configuration not found", + Detail: fmt.Sprintf("No module configuration found for component %q at %s.", c.Name, c.FinalSourceAddr), + Subject: c.SourceAddrRange.ToHCL().Ptr(), + }) + return nil, diags + } + + module, moreDiags := parser.LoadConfigDir(c.FinalSourceAddr) + diags = diags.Append(moreDiags) + + if module != nil { + walker := stackparser.NewSourceBundleModuleWalker(c.FinalSourceAddr, bundle, parser) + config, moreDiags := configs.BuildConfig(module, walker, nil) + diags = diags.Append(moreDiags) + return config, diags + } + + return nil, diags +} + +func decodeComponentBlock(block *hcl.Block) (*Component, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &Component{ + Name: block.Labels[0], + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + if !hclsyntax.ValidIdentifier(ret.Name) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid component name", + block.LabelRanges[0], + )) + return nil, diags + } + + content, hclDiags := block.Body.Content(componentBlockSchema) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments( + content.Attributes["source"], + content.Attributes["version"], + ) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + ret.SourceAddr = sourceAddr + ret.VersionConstraints = versionConstraints + ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range) + if content.Attributes["version"] != nil { + ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range) + } + // Now that we've populated the mandatory source location fields we can + // safely return a partial ret if we encounter any further errors, as + // long as we leave the other fields either unset or in some other + // reasonable state for careful partial analysis. + + if attr, ok := content.Attributes["for_each"]; ok { + ret.ForEach = attr.Expr + } + if attr, ok := content.Attributes["inputs"]; ok { + ret.Inputs = attr.Expr + } + if attr, ok := content.Attributes["providers"]; ok { + var providerDiags tfdiags.Diagnostics + ret.ProviderConfigs, providerDiags = decodeProvidersAttribute(attr) + diags = diags.Append(providerDiags) + } + if attr, exists := content.Attributes["depends_on"]; exists { + ret.DependsOn, hclDiags = configs.DecodeDependsOn(attr) + diags = diags.Append(hclDiags) + } + + return ret, diags +} + +func decodeSourceAddrArguments(sourceAttr, versionAttr *hcl.Attribute) (sourceaddrs.Source, constraints.IntersectionSpec, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + var sourceStr string + hclDiags := gohcl.DecodeExpression(sourceAttr.Expr, nil, &sourceStr) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, nil, diags + } + + sourceAddr, err := sourceaddrs.ParseSource(sourceStr) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf( + "Cannot parse %q as a source address: %s.", + sourceStr, err, + ), + Subject: sourceAttr.Expr.Range().Ptr(), + }) + return nil, nil, diags + } + + var versionConstraints constraints.IntersectionSpec + if sourceAddr.SupportsVersionConstraints() { + if versionAttr == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required version constraints", + Detail: "The specified source address requires version constraints specified in a separate \"version\" argument.", + Subject: sourceAttr.Expr.Range().Ptr(), + }) + return nil, nil, diags + } + var versionStr string + hclDiags := gohcl.DecodeExpression(versionAttr.Expr, nil, &versionStr) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, nil, diags + } + versionConstraints, err = constraints.ParseRubyStyleMulti(versionStr) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraints", + Detail: fmt.Sprintf( + "Cannot parse %q as source package version constraints: %s.", + versionStr, err, + ), + Subject: versionAttr.Expr.Range().Ptr(), + }) + return nil, nil, diags + } + } else { + if versionAttr != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported version constraints", + Detail: "The specified source address does not support version constraints.", + Subject: versionAttr.Range.Ptr(), + }) + return nil, nil, diags + } + } + + return sourceAddr, versionConstraints, diags +} + +func decodeProvidersAttribute(attr *hcl.Attribute) (map[addrs.LocalProviderConfig]hcl.Expression, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // This particular argument has some enforced static structure because + // it's populating an inflexible part of Terraform Core's input. + // This argument, if present, must always be an object constructor + // whose attributes are Terraform Core-style provider configuration + // addresses, but whose values are just arbitrary expressions for now + // and will be resolved into specific provider configuration addresses + // dynamically at runtime. + pairs, hclDiags := hcl.ExprMap(attr.Expr) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + ret := map[addrs.LocalProviderConfig]hcl.Expression{} + for _, pair := range pairs { + insideAddrExpr := pair.Key + outsideAddrExpr := pair.Value + + traversal, hclDiags := hcl.AbsTraversalForExpr(insideAddrExpr) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + continue + } + + if len(traversal) < 1 || len(traversal) > 2 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration reference", + Detail: "Each item in the providers argument requires a provider local name, optionally followed by a period and then a configuration alias, matching one of the provider configuration import slots declared by the component's root module.", + Subject: insideAddrExpr.Range().Ptr(), + }) + continue + } + + localName := traversal.RootName() + if !hclsyntax.ValidIdentifier(localName) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid provider local name", + traversal[0].SourceRange(), + )) + continue + } + + var alias string + if len(traversal) > 1 { + aliasStep, ok := traversal[1].(hcl.TraverseAttr) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration reference", + Detail: "Provider local name must either stand alone or be followed by a period and then a configuration alias.", + Subject: traversal[1].SourceRange().Ptr(), + }) + continue + } + alias = aliasStep.Name + } + + addr := addrs.LocalProviderConfig{ + LocalName: localName, + Alias: alias, + } + if existing, exists := ret[addr]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider configuration assignment", + Detail: fmt.Sprintf( + "A provider configuration for %s was already assigned at %s.", + addr.StringCompact(), existing.Range().Ptr(), + ), + Subject: outsideAddrExpr.Range().Ptr(), + }) + continue + } else { + ret[addr] = outsideAddrExpr + } + } + + return ret, diags +} + +var componentBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "source", Required: true}, + {Name: "version", Required: false}, + {Name: "for_each", Required: false}, + {Name: "inputs", Required: false}, + {Name: "providers", Required: false}, + {Name: "depends_on", Required: false}, + }, +} diff --git a/internal/stacks/stackconfig/config.go b/internal/stacks/stackconfig/config.go new file mode 100644 index 0000000000..883b52f40e --- /dev/null +++ b/internal/stacks/stackconfig/config.go @@ -0,0 +1,518 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + "sort" + "strings" + + "github.com/apparentlymart/go-versions/versions" + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// maxEmbeddedStackNesting is an arbitrary, hopefully-reasonable limit on +// how much embedded stack nesting is allowed in a stack configuration. +// +// This is here to avoid unbounded resource usage for configurations with +// mistakes such as self-referencing source addresses or call cycles. +const maxEmbeddedStackNesting = 20 + +// Config represents an overall stack configuration tree, consisting of a +// root stack that might optionally have embedded stacks inside it, and +// so on for arbitrary levels of nesting. +type Config struct { + Root *ConfigNode + + // Sources is the source bundle that the configuration was loaded from. + // + // This is also the source bundle that any Terraform modules used by + // components should be loaded from. + Sources *sourcebundle.Bundle + + // ProviderRefTypes tracks the cty capsule type that represents a + // reference for each provider type mentioned in the configuration. + ProviderRefTypes map[addrs.Provider]cty.Type +} + +func (config *Config) Stack(stack stackaddrs.Stack) *Stack { + current := config.Root + for _, part := range stack { + var ok bool + current, ok = current.Children[part.Name] + if !ok { + return nil + } + } + return current.Stack +} + +func (config *Config) Component(component stackaddrs.ConfigComponent) *Component { + stack := config.Stack(component.Stack) + if stack == nil || stack.Components == nil { + return nil + } + return stack.Components[component.Item.Name] +} + +// ConfigNode represents a node in a tree of stacks that are to be planned and +// applied together. +// +// A fully-resolved stack configuration has a root node of this type, which +// can have zero or more child nodes that are also of this type, and so on +// to arbitrary levels of nesting. +type ConfigNode struct { + // Stack is the definition of this node in the stack tree. + Stack *Stack + + // Source is the source address of this stack. This is mainly used to + // ensure consistency in places where a stack might be initialised in + // multiple places (like in different source blocks). + Source sourceaddrs.FinalSource + + // Children describes all of the embedded stacks nested directly beneath + // this node in the stack tree. The keys match the labels on the "stack" + // blocks in the configuration that [Config.Stack] was built from, and + // so also match the keys in the EmbeddedStacks field of that Stack. + Children map[string]*ConfigNode +} + +// LoadConfigDir loads, parses, decodes, and partially-validates the +// stack configuration rooted at the given source address. +// +// If the given source address is a [sourceaddrs.LocalSource] then it is +// interpreted relative to the current process working directory. If it's +// a remote our registry source address then LoadConfigDir will attempt +// to read it from the provided source bundle. +// +// LoadConfigDir follows calls to embedded stacks and recursively loads +// those too, using the same source bundle for any non-local sources. +func LoadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle) (*Config, tfdiags.Diagnostics) { + rootNode, diags := loadConfigDir(sourceAddr, sources, make([]sourceaddrs.FinalSource, 0, 3)) + if rootNode == nil { + if !diags.HasErrors() { + panic("LoadConfigDir returned no root node and no errors") + } + return nil, diags + } + + ret := &Config{ + Root: rootNode, + Sources: sources, + } + + // Before we return we need to walk the tree and find all of the mentions + // of provider types and make sure we have a singleton cty.Type for each + // one representing a reference to a configuration of each type. + providerRefTypes, moreDiags := collectProviderRefCapsuleTypes(ret) + ret.ProviderRefTypes = providerRefTypes + diags = diags.Append(moreDiags) + + return ret, diags +} + +// NewEmptyConfig returns a representation of an empty configuration that's +// primarily intended for unit testing situations that don't actually depend +// on any configuration objects being present. +// +// The result has non-nil pointers to some items that callers would reasonably +// expect should always be present, but in particular doesn't include any +// actual declarations and so closely resembles what would happen if +// parsing a totally-empty configuration. +func NewEmptyConfig(fakeSourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle) *Config { + return &Config{ + Root: &ConfigNode{ + Stack: &Stack{ + SourceAddr: fakeSourceAddr, + Declarations: Declarations{ + RequiredProviders: &ProviderRequirements{}, + }, + }, + }, + Sources: sources, + } +} + +func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle, callers []sourceaddrs.FinalSource) (*ConfigNode, tfdiags.Diagnostics) { + stack, diags := LoadSingleStackConfig(sourceAddr, sources) + if stack == nil { + if !diags.HasErrors() { + panic("LoadSingleStackConfig returned no root node and no errors") + } + return nil, diags + } + + ret := &ConfigNode{ + Stack: stack, + Source: sourceAddr, + Children: make(map[string]*ConfigNode), + } + for _, call := range stack.EmbeddedStacks { + effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, call.SourceAddr, call.VersionConstraints, sources) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf( + "Cannot use %q as a source address here: %s.", + call.SourceAddr, err, + ), + Subject: call.SourceAddrRange.ToHCL().Ptr(), + }) + continue + } + call.FinalSourceAddr = effectiveSourceAddr + + if len(callers) == maxEmbeddedStackNesting { + var callersBuf strings.Builder + for i, addr := range callers { + fmt.Fprintf(&callersBuf, "\n %2d: %s", i+1, addr) + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Too much embedded stack nesting", + Detail: fmt.Sprintf( + "This embedded stack call is nested %d levels deep, which is greater than Terraform's nesting safety limit.\n\nWe recommend keeping stack configuration trees relatively flat, ideally using composition of a flat set of nested calls at the root.\n\nEmbedded stacks leading to this point:%s", + len(callers), callersBuf.String(), + ), + Subject: call.DeclRange.ToHCL().Ptr(), + }) + continue + } + + childNode, moreDiags := loadConfigDir(effectiveSourceAddr, sources, append(callers, sourceAddr)) + diags = diags.Append(moreDiags) + if childNode != nil { + ret.Children[call.Name] = childNode + } + } + + var removedTargets []stackaddrs.ConfigStackCall + for target := range stack.RemovedEmbeddedStacks.All() { + // removed embedded stacks can point to deeply embedded stacks, + // which we actually want to load into the embedded stacks if they + // naturally exist. But, the parents of those deeply embedded stacks + // will only exist if we have already added their parents to the + // tree of objects. So, we're going to store all our removed blocks + // in a flattened list and sort them so we add children before + // grandchildren and onwards and properly build the list to place + // everything in the correct place. + removedTargets = append(removedTargets, target) + } + + sort.Slice(removedTargets, func(i, j int) bool { + return len(removedTargets[i].Stack) < len(removedTargets[j].Stack) + }) + + for _, target := range removedTargets { + for _, block := range stack.RemovedEmbeddedStacks.Get(target) { + effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, block.SourceAddr, block.VersionConstraints, sources) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf( + "Cannot use %q as a source address here: %s.", + block.SourceAddr, err, + ), + Subject: block.SourceAddrRange.ToHCL().Ptr(), + }) + continue + } + block.FinalSourceAddr = effectiveSourceAddr + + current := ret + for _, step := range target.Stack { + current = current.Children[step.Name] + if current == nil { + // this is invalid, we can't have orphaned removed blocks + // so we'll just return an error and skip this block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid removed block", + Detail: "The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", + Subject: block.DeclRange.ToHCL().Ptr(), + }) + break + } + } + + if current != nil { + next := target.Item.Name + if childNode, ok := current.Children[next]; ok { + // Then we've already loaded the configuration for this + // stack in the direct stack call or in another removed + // block. + + if childNode.Source != block.FinalSourceAddr { + // but apparently the blocks don't agree on what the + // source should be here, so that is an error + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf("Cannot use %q as a source address here: the target stack is already initialised with another source %q.", block.FinalSourceAddr, childNode.Source), + Subject: block.SourceAddrRange.ToHCL().Ptr(), + }) + } + continue + } + + childNode, moreDiags := loadConfigDir(effectiveSourceAddr, sources, append(callers, sourceAddr)) + diags = diags.Append(moreDiags) + if childNode != nil { + current.Children[next] = childNode + } + } + } + } + + // We'll also populate the FinalSourceAddr field on each component, + // so that callers can know the final absolute address of this + // component's root module without having to retrace through our + // recursive process here. + for _, cmpn := range stack.Components { + effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, cmpn.SourceAddr, cmpn.VersionConstraints, sources) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf( + "Cannot use %q as a source address here: %s.", + cmpn.SourceAddr, err, + ), + Subject: cmpn.SourceAddrRange.ToHCL().Ptr(), + }) + continue + } + + cmpn.FinalSourceAddr = effectiveSourceAddr + } + + for addr, blocks := range stack.RemovedComponents.All() { + + var source sourceaddrs.FinalSource + if len(addr.Stack) == 0 { + if cmpn, ok := stack.Components[addr.Item.Name]; ok { + source = cmpn.FinalSourceAddr + } + } + + for _, rmvd := range blocks { + effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, rmvd.SourceAddr, rmvd.VersionConstraints, sources) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf( + "Cannot use %q as a source address here: %s.", + rmvd.SourceAddr, err, + ), + Subject: rmvd.SourceAddrRange.ToHCL().Ptr(), + }) + continue + } + + if source == nil { + source = effectiveSourceAddr + } else if source != effectiveSourceAddr { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid source address", + Detail: fmt.Sprintf("Cannot use %q as a source address here: the target stack is already initialised with another source %q.", effectiveSourceAddr, source), + Subject: rmvd.SourceAddrRange.ToHCL().Ptr(), + }) + } + + rmvd.FinalSourceAddr = effectiveSourceAddr + } + } + + return ret, diags +} + +func resolveFinalSourceAddr(base sourceaddrs.FinalSource, rel sourceaddrs.Source, versionConstraints constraints.IntersectionSpec, sources *sourcebundle.Bundle) (sourceaddrs.FinalSource, error) { + switch rel := rel.(type) { + case sourceaddrs.FinalSource: + switch base := base.(type) { + case sourceaddrs.RegistrySourceFinal: + // This case is awkward because we'd ideally like to return + // another registry source address in the same registry package + // as base, but that might not actually be possible if "rel" + // is a local source that traverses up out of the scope of + // the registry package and into other parts of the real + // underlying package. Therefore we'll first try the ideal + // case but then do some more complex finagling if it fails. + ret, err := sourceaddrs.ResolveRelativeFinalSource(base, rel) + if err == nil { + return ret, nil + } + + // If we can't resolve relative to the registry source then + // we need to resolve relative to its underlying remote source + // instead. + underlyingSource, ok := sources.RegistryPackageSourceAddr(base.Package(), base.SelectedVersion()) + if !ok { + // If we also can't find the underlying source for some reason + // then we're stuck. + return nil, fmt.Errorf("can't find underlying source address for %s", base.Package()) + } + underlyingSource = base.FinalSourceAddr(underlyingSource) + return sourceaddrs.ResolveRelativeFinalSource(underlyingSource, rel) + + default: + // Easy case: this source type is already a final type + return sourceaddrs.ResolveRelativeFinalSource(base, rel) + } + case sourceaddrs.RegistrySource: + // Registry sources are more annoying because we need to figure out + // exactly which version the given version constraints select, which + // we infer by what's available in the source bundle on the assumption + // that the source bundler also selected the latest available version + // that meets the given constraints. + allowedVersions := versions.MeetingConstraints(versionConstraints) + availableVersions := sources.RegistryPackageVersions(rel.Package()) + selectedVersion := availableVersions.NewestInSet(allowedVersions) + if selectedVersion == versions.Unspecified { + // We should get here only if the source bundle was built + // incorrectly. A valid source bundle should always contain + // at least one entry that matches each version constraint. + return nil, fmt.Errorf("no cached versions of %s match the given version constraints", rel.Package()) + } + finalRel := rel.Versioned(selectedVersion) + return sourceaddrs.ResolveRelativeFinalSource(base, finalRel) + default: + // Should not get here because the above cases should be exhaustive + // for all implementations of sourceaddrs.Source. + return nil, fmt.Errorf("cannot resolve final source address for %T (this is a bug in Terraform)", rel) + } +} + +// collectProviderRefCapsuleTypes searches the entire configuration tree for +// any mentions of provider types and instantiates the singleton cty capsule +// type representing configurations for each one, returning a mapping from +// provider source address to type. +// +// This operation involves some further analysis of some configuration elements +// which can potentially produce additional diagnostics. +func collectProviderRefCapsuleTypes(config *Config) (map[addrs.Provider]cty.Type, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := make(map[addrs.Provider]cty.Type) + + // Our main source for provider references is required_providers blocks + // in each of the individual stack configurations. This should be + // exhaustive for a valid configuration because we require that all + // provider requirements be declared before use elsewhere. + collectProviderRefCapsuleTypesSingle(config.Root, ret) + + // Type constraints in input variables and output values can include + // provider reference types. This makes sure we'll have capsule types + // for each one and also, as a side-effect, updates the input variable + // and output value objects to refer to those type constraints for + // use in later evaluation. (In practice this should not discover any + // new provider types in a valid configuration, but populating extra + // fields on the InputVariable and OutputValue objects is an important + // side-effect.) + diags = diags.Append( + decodeTypeConstraints(config, ret), + ) + + return ret, diags +} + +func collectProviderRefCapsuleTypesSingle(node *ConfigNode, types map[addrs.Provider]cty.Type) { + reqs := node.Stack.RequiredProviders + if reqs == nil { + return + } + for _, req := range reqs.Requirements { + pTy := req.Provider + if _, ok := types[pTy]; ok { + continue + } + types[pTy] = stackconfigtypes.ProviderConfigType(pTy) + } + + for _, child := range node.Children { + collectProviderRefCapsuleTypesSingle(child, types) + } +} + +// decodeTypeConstraints handles the just-in-time postprocessing we do before +// returning from [LoadConfigDir], making sure that the type constraints +// on input variables and output values throughout the configuration are +// valid and consistent. +func decodeTypeConstraints(config *Config, types map[addrs.Provider]cty.Type) tfdiags.Diagnostics { + return decodeTypeConstraintsSingle(config.Root, types) +} + +func decodeTypeConstraintsSingle(node *ConfigNode, types map[addrs.Provider]cty.Type) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + typeInfo := &decodeTypeConstraintsTypeInfo{ + types: types, + reqs: node.Stack.RequiredProviders, + } + for _, c := range node.Stack.InputVariables { + diags = diags.Append( + decodeTypeConstraint(&c.Type, typeInfo), + ) + } + for _, c := range node.Stack.OutputValues { + diags = diags.Append( + decodeTypeConstraint(&c.Type, typeInfo), + ) + } + + for _, child := range node.Children { + diags = diags.Append( + decodeTypeConstraintsSingle(child, types), + ) + } + + return diags +} + +func decodeTypeConstraint(c *TypeConstraint, typeInfo *decodeTypeConstraintsTypeInfo) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + ty, defaults, hclDiags := typeexpr.TypeConstraint(c.Expression, typeInfo) + c.Constraint = ty + c.Defaults = defaults + diags = diags.Append(hclDiags) + return diags +} + +type decodeTypeConstraintsTypeInfo struct { + types map[addrs.Provider]cty.Type + reqs *ProviderRequirements +} + +var _ typeexpr.TypeInformation = (*decodeTypeConstraintsTypeInfo)(nil) + +// ProviderConfigType implements typeexpr.TypeInformation +func (ti *decodeTypeConstraintsTypeInfo) ProviderConfigType(providerAddr addrs.Provider) cty.Type { + return ti.types[providerAddr] +} + +// ProviderForLocalName implements typeexpr.TypeInformation +func (ti *decodeTypeConstraintsTypeInfo) ProviderForLocalName(localName string) (addrs.Provider, bool) { + if ti.reqs == nil { + return addrs.Provider{}, false + } + return ti.reqs.ProviderForLocalName(localName) +} + +// SetProviderConfigType implements typeexpr.TypeInformation +func (ti *decodeTypeConstraintsTypeInfo) SetProviderConfigType(providerAddr addrs.Provider, ty cty.Type) { + ti.types[providerAddr] = ty +} diff --git a/internal/stacks/stackconfig/config_test.go b/internal/stacks/stackconfig/config_test.go new file mode 100644 index 0000000000..c088b5ee5c --- /dev/null +++ b/internal/stacks/stackconfig/config_test.go @@ -0,0 +1,248 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "sort" + "testing" + + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestLoadConfigDirErrors(t *testing.T) { + bundle, err := sourcebundle.OpenDir("testdata/basics-bundle") + if err != nil { + t.Fatal(err) + } + + rootAddr := sourceaddrs.MustParseSource("git::https://example.com/errored.git").(sourceaddrs.RemoteSource) + _, gotDiags := LoadConfigDir(rootAddr, bundle) + + sort.SliceStable(gotDiags, func(i, j int) bool { + if gotDiags[i].Severity() != gotDiags[j].Severity() { + return gotDiags[i].Severity() < gotDiags[j].Severity() + } + + if gotDiags[i].Description().Summary != gotDiags[j].Description().Summary { + return gotDiags[i].Description().Summary < gotDiags[j].Description().Summary + } + + return gotDiags[i].Description().Detail < gotDiags[j].Description().Detail + }) + + wantDiags := tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "Component exists for removed block", "A removed block for component \"a\" was declared without an index, but a component block with the same name was declared at git::https://example.com/errored.git//main.tfstack.hcl:10,1-14.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case."), + tfdiags.Sourceless(tfdiags.Error, "Invalid for_each expression", "A removed block with a for_each expression must reference that expression within the `from` attribute."), + tfdiags.Sourceless(tfdiags.Error, "Invalid for_each expression", "A removed block with a for_each expression must reference that expression within the `from` attribute."), + } + + count := len(wantDiags) + if len(gotDiags) > count { + count = len(gotDiags) + } + + for i := 0; i < count; i++ { + if i >= len(wantDiags) { + t.Errorf("unexpected diagnostic:\n%s", gotDiags[i]) + continue + } + + if i >= len(gotDiags) { + t.Errorf("missing diagnostic:\n%s", wantDiags[i]) + continue + } + + got, want := gotDiags[i], wantDiags[i] + + if got, want := got.Severity(), want.Severity(); got != want { + t.Errorf("diagnostics[%d] severity\ngot: %s\nwant: %s", i, got, want) + } + + if got, want := got.Description().Summary, want.Description().Summary; got != want { + t.Errorf("diagnostics[%d] summary\ngot: %s\nwant: %s", i, got, want) + } + + if got, want := got.Description().Detail, want.Description().Detail; got != want { + t.Errorf("diagnostics[%d] detail\ngot: %s\nwant: %s", i, got, want) + } + } +} + +func TestLoadConfigDirSourceErrors(t *testing.T) { + bundle, err := sourcebundle.OpenDir("testdata/basics-bundle") + if err != nil { + t.Fatal(err) + } + + rootAddr := sourceaddrs.MustParseSource("git::https://example.com/errored-sources.git").(sourceaddrs.RemoteSource) + _, gotDiags := LoadConfigDir(rootAddr, bundle) + + sort.SliceStable(gotDiags, func(i, j int) bool { + if gotDiags[i].Severity() != gotDiags[j].Severity() { + return gotDiags[i].Severity() < gotDiags[j].Severity() + } + + if gotDiags[i].Description().Summary != gotDiags[j].Description().Summary { + return gotDiags[i].Description().Summary < gotDiags[j].Description().Summary + } + + return gotDiags[i].Description().Detail < gotDiags[j].Description().Detail + }) + + wantDiags := tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "Invalid removed block", "The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it."), + tfdiags.Sourceless(tfdiags.Error, "Invalid source address", "Cannot use \"git::https://example.com/errored-sources.git\" as a source address here: the target stack is already initialised with another source \"git::https://example.com/errored-sources.git//subdir\"."), + tfdiags.Sourceless(tfdiags.Error, "Invalid source address", "Cannot use \"git::https://example.com/errored-sources.git//subdir\" as a source address here: the target stack is already initialised with another source \"git::https://example.com/errored-sources.git\"."), + } + + count := len(wantDiags) + if len(gotDiags) > count { + count = len(gotDiags) + } + + for i := 0; i < count; i++ { + if i >= len(wantDiags) { + t.Errorf("unexpected diagnostic:\n%s", gotDiags[i]) + continue + } + + if i >= len(gotDiags) { + t.Errorf("missing diagnostic:\n%s", wantDiags[i]) + continue + } + + got, want := gotDiags[i], wantDiags[i] + + if got, want := got.Severity(), want.Severity(); got != want { + t.Errorf("diagnostics[%d] severity\ngot: %s\nwant: %s", i, got, want) + } + + if got, want := got.Description().Summary, want.Description().Summary; got != want { + t.Errorf("diagnostics[%d] summary\ngot: %s\nwant: %s", i, got, want) + } + + if got, want := got.Description().Detail, want.Description().Detail; got != want { + t.Errorf("diagnostics[%d] detail\ngot: %s\nwant: %s", i, got, want) + } + } +} + +func TestLoadConfigDirBasics(t *testing.T) { + bundle, err := sourcebundle.OpenDir("testdata/basics-bundle") + if err != nil { + t.Fatal(err) + } + + rootAddr := sourceaddrs.MustParseSource("git::https://example.com/root.git").(sourceaddrs.RemoteSource) + config, diags := LoadConfigDir(rootAddr, bundle) + if len(diags) != 0 { + t.Fatalf("unexpected diagnostics:\n%s", diags.NonFatalErr().Error()) + } + + t.Run("root input variables", func(t *testing.T) { + if got, want := len(config.Root.Stack.InputVariables), 2; got != want { + t.Errorf("wrong number of input variables %d; want %d", got, want) + } + t.Run("name", func(t *testing.T) { + cfg, ok := config.Root.Stack.InputVariables["name"] + if !ok { + t.Fatal("Root stack config has no variable named \"name\".") + } + if got, want := cfg.Name, "name"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := cfg.Type.Constraint, cty.String; got != want { + t.Errorf("wrong name\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Sensitive, false; got != want { + t.Errorf("wrong sensitive\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Ephemeral, false; got != want { + t.Errorf("wrong ephemeral\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("auth_jwt", func(t *testing.T) { + cfg, ok := config.Root.Stack.InputVariables["auth_jwt"] + if !ok { + t.Fatal("Root stack config has no variable named \"auth_jwt\".") + } + if got, want := cfg.Name, "auth_jwt"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := cfg.Type.Constraint, cty.String; got != want { + t.Errorf("wrong name\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Sensitive, true; got != want { + t.Errorf("wrong sensitive\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Ephemeral, true; got != want { + t.Errorf("wrong ephemeral\ngot: %#v\nwant: %#v", got, want) + } + }) + }) + t.Run("root output values", func(t *testing.T) { + if got, want := len(config.Root.Stack.OutputValues), 3; got != want { + t.Errorf("wrong number of output values %d; want %d", got, want) + } + t.Run("greeting", func(t *testing.T) { + cfg, ok := config.Root.Stack.OutputValues["greeting"] + if !ok { + t.Fatal("Root stack config has no output value named \"greeting\".") + } + if got, want := cfg.Name, "greeting"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := cfg.Type.Constraint, cty.String; got != want { + t.Errorf("wrong name\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Sensitive, false; got != want { + t.Errorf("wrong sensitive\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Ephemeral, false; got != want { + t.Errorf("wrong ephemeral\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("sound", func(t *testing.T) { + cfg, ok := config.Root.Stack.OutputValues["sound"] + if !ok { + t.Fatal("Root stack config has no output value named \"sound\".") + } + if got, want := cfg.Name, "sound"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := cfg.Type.Constraint, cty.String; got != want { + t.Errorf("wrong name\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Sensitive, false; got != want { + t.Errorf("wrong sensitive\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Ephemeral, false; got != want { + t.Errorf("wrong ephemeral\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("password", func(t *testing.T) { + cfg, ok := config.Root.Stack.OutputValues["password"] + if !ok { + t.Fatal("Root stack config has no output value named \"password\".") + } + if got, want := cfg.Name, "password"; got != want { + t.Errorf("wrong name\ngot: %s\nwant: %s", got, want) + } + if got, want := cfg.Type.Constraint, cty.String; got != want { + t.Errorf("wrong name\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Sensitive, true; got != want { + t.Errorf("wrong sensitive\ngot: %#v\nwant: %#v", got, want) + } + if got, want := cfg.Ephemeral, true; got != want { + t.Errorf("wrong ephemeral\ngot: %#v\nwant: %#v", got, want) + } + }) + }) + // TODO: More thorough testing! +} diff --git a/internal/stacks/stackconfig/declarations.go b/internal/stacks/stackconfig/declarations.go new file mode 100644 index 0000000000..bcd6b42b19 --- /dev/null +++ b/internal/stacks/stackconfig/declarations.go @@ -0,0 +1,400 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Declarations represent the various items that can be declared in a stack +// configuration. +// +// This just represents the fields that both [Stack] and [File] have in common, +// so we can share some code between the two. +type Declarations struct { + // EmbeddedStacks are calls to other stack configurations that should + // be treated as a part of the overall desired state produced from this + // stack. These are declared with "stack" blocks in the stack language. + EmbeddedStacks map[string]*EmbeddedStack + + // Components are calls to trees of Terraform modules that represent the + // real infrastructure described by a stack. + Components map[string]*Component + + // InputVariables, LocalValues, and OutputValues together represent all + // of the "named values" in the stack configuration, which are just glue + // to pass values between scopes or to factor out common expressions for + // reuse in multiple locations. + InputVariables map[string]*InputVariable + LocalValues map[string]*LocalValue + OutputValues map[string]*OutputValue + + // RequiredProviders represents the single required_providers block + // that's allowed in any stack, declaring which providers this stack + // depends on and which versions of those providers it is compatible with. + RequiredProviders *ProviderRequirements + + // ProviderConfigs are the provider configurations declared in this + // particular stack configuration. Other stack configurations in the + // overall tree might have their own provider configurations. + ProviderConfigs map[addrs.LocalProviderConfig]*ProviderConfig + + // RemovedComponents is the list of components that have been removed from + // the configuration. + RemovedComponents collections.Map[stackaddrs.ConfigComponent, []*Removed] + + // RemovedEmbeddedStacks is the list of embedded stacks that have been removed + // from the configuration. + RemovedEmbeddedStacks collections.Map[stackaddrs.ConfigStackCall, []*Removed] +} + +func makeDeclarations() Declarations { + return Declarations{ + EmbeddedStacks: make(map[string]*EmbeddedStack), + Components: make(map[string]*Component), + InputVariables: make(map[string]*InputVariable), + LocalValues: make(map[string]*LocalValue), + OutputValues: make(map[string]*OutputValue), + ProviderConfigs: make(map[addrs.LocalProviderConfig]*ProviderConfig), + RemovedComponents: collections.NewMap[stackaddrs.ConfigComponent, []*Removed](), + RemovedEmbeddedStacks: collections.NewMap[stackaddrs.ConfigStackCall, []*Removed](), + } +} + +func (d *Declarations) addComponent(decl *Component) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + + name := decl.Name + if existing, exists := d.Components[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate component declaration", + Detail: fmt.Sprintf( + "An component named %q was already declared at %s.", + name, existing.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + if blocks, exists := d.RemovedComponents.GetOk(stackaddrs.ConfigComponent{ + Stack: nil, + Item: stackaddrs.Component{ + Name: name, + }, + }); exists { + for _, removed := range blocks { + if removed.From.Component.Index == nil { + // If a component has been removed, we should not also find it + // in the configuration. + // + // If the removed block has an index, then it's possible that + // only a specific instance was removed and not the whole thing. + // This is okay at this point, and will be validated more later. + // See the addRemoved method for more information. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Component exists for removed block", + Detail: fmt.Sprintf( + "A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.", + name, decl.DeclRange.ToHCL(), + ), + Subject: removed.DeclRange.ToHCL().Ptr(), + }) + return diags + } + } + } + + d.Components[name] = decl + return diags +} + +func (d *Declarations) addEmbeddedStack(decl *EmbeddedStack) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + + name := decl.Name + if existing, exists := d.EmbeddedStacks[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate embedded stack call", + Detail: fmt.Sprintf( + "An embedded stack call named %q was already declared at %s.", + name, existing.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + if blocks, exists := d.RemovedEmbeddedStacks.GetOk(stackaddrs.ConfigStackCall{ + Stack: nil, + Item: stackaddrs.StackCall{ + Name: name, + }, + }); exists { + for _, removed := range blocks { + if removed.From.Stack[0].Index == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Stack exists for removed block", + Detail: fmt.Sprintf( + "A removed block for stack %q was declared without an index, but a stack block with the same name was declared at %s.\n\nA removed block without an index indicates that the stack and all instances were removed from the configuration, and this is not the case.", + name, decl.DeclRange.ToHCL(), + ), + Subject: removed.DeclRange.ToHCL().Ptr(), + }) + return diags + } + } + } + + d.EmbeddedStacks[name] = decl + return diags +} + +func (d *Declarations) addInputVariable(decl *InputVariable) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + + name := decl.Name + if existing, exists := d.InputVariables[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate input variable declaration", + Detail: fmt.Sprintf( + "An input variable named %q was already declared at %s.", + name, existing.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + d.InputVariables[name] = decl + return diags +} + +func (d *Declarations) addLocalValue(decl *LocalValue) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + + name := decl.Name + if existing, exists := d.LocalValues[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate local value declaration", + Detail: fmt.Sprintf( + "A local value named %q was already declared at %s.", + name, existing.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + d.LocalValues[name] = decl + return diags +} + +func (d *Declarations) addOutputValue(decl *OutputValue) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + + name := decl.Name + if existing, exists := d.OutputValues[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate output value declaration", + Detail: fmt.Sprintf( + "An output value named %q was already declared at %s.", + name, existing.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + d.OutputValues[name] = decl + return diags +} + +func (d *Declarations) addRequiredProviders(decl *ProviderRequirements) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + if d.RequiredProviders != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider requirements", + Detail: fmt.Sprintf( + "This stack's provider requirements were already declared at %s.", + d.RequiredProviders.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + d.RequiredProviders = decl + return diags +} + +func (d *Declarations) addProviderConfig(decl *ProviderConfig) tfdiags.Diagnostics { + if decl == nil { + return nil + } + var diags tfdiags.Diagnostics + + addr := decl.LocalAddr + if existing, exists := d.ProviderConfigs[addr]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider configuration", + Detail: fmt.Sprintf( + "An configuration named %q for provider %q was already declared at %s.", + addr.LocalName, addr.Alias, existing.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + + d.ProviderConfigs[addr] = decl + return diags +} + +func (d *Declarations) addRemoved(decl *Removed) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if decl == nil { + return diags + } + + if decl.From.Component != nil { + addr := decl.From.TargetConfigComponent() + + if decl.From.Component.Index == nil && len(decl.From.Stack) == 0 { + // If the removed block does not have an index, then we shouldn't also + // have a component block with the same name. A removed block without + // an index indicates that the component and all instances were removed + // from the configuration. + // + // Note that a removed block with an index is allowed to coexist with a + // component block with the same name, because it indicates that only + // a specific instance was removed and not the whole thing. During the + // validate and planning stages we will validate that the clashing + // component and removed blocks are not both pointing to the same index. + if component, exists := d.Components[decl.From.Component.Name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Component exists for removed block", + Detail: fmt.Sprintf( + "A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.", + decl.From.Component.Name, component.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + } + + d.RemovedComponents.Put(addr, append(d.RemovedComponents.Get(addr), decl)) + } else { + addr := decl.From.TargetStack().ToStackCall() + + if len(decl.From.Stack) == 1 && decl.From.Stack[0].Index == nil { + // Same logic as for components, we can just error a bit earlier + // here if the user is targeting a stack that definitely exists + // in the configuration. + if stack, exists := d.EmbeddedStacks[decl.From.Stack[0].Name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Stack exists for removed block", + Detail: fmt.Sprintf( + "A removed block for stack %q was declared without an index, but a stack block with the same name was declared at %s.\n\nA removed block without an index indicates that the stack and all instances were removed from the configuration, and this is not the case.", + decl.From.Component.Name, stack.DeclRange.ToHCL(), + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return diags + } + } + + d.RemovedEmbeddedStacks.Put(addr, append(d.RemovedEmbeddedStacks.Get(addr), decl)) + } + + return diags +} + +func (d *Declarations) merge(other *Declarations) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + for _, decl := range other.EmbeddedStacks { + diags = diags.Append( + d.addEmbeddedStack(decl), + ) + } + for _, blocks := range other.RemovedEmbeddedStacks.All() { + for _, decl := range blocks { + diags = diags.Append(d.addRemoved(decl)) + } + } + for _, decl := range other.Components { + diags = diags.Append( + d.addComponent(decl), + ) + } + for _, decl := range other.InputVariables { + diags = diags.Append( + d.addInputVariable(decl), + ) + } + for _, decl := range other.LocalValues { + diags = diags.Append( + d.addLocalValue(decl), + ) + } + for _, decl := range other.OutputValues { + diags = diags.Append( + d.addOutputValue(decl), + ) + } + if other.RequiredProviders != nil { + d.addRequiredProviders(other.RequiredProviders) + } + for _, decl := range other.ProviderConfigs { + diags = diags.Append( + d.addProviderConfig(decl), + ) + } + for _, blocks := range other.RemovedComponents.All() { + for _, decl := range blocks { + diags = diags.Append( + d.addRemoved(decl), + ) + } + } + + return diags +} diff --git a/internal/stacks/stackconfig/diagnostics.go b/internal/stacks/stackconfig/diagnostics.go new file mode 100644 index 0000000000..a2dca8bcf5 --- /dev/null +++ b/internal/stacks/stackconfig/diagnostics.go @@ -0,0 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "github.com/hashicorp/hcl/v2" +) + +func invalidNameDiagnostic(summary string, rng hcl.Range) *hcl.Diagnostic { + return &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: summary, + Detail: "Names must be valid identifiers: beginning with a letter or underscore, followed by zero or more letters, digits, or underscores.", + Subject: &rng, + } +} diff --git a/internal/stacks/stackconfig/doc.go b/internal/stacks/stackconfig/doc.go new file mode 100644 index 0000000000..6f47b37b93 --- /dev/null +++ b/internal/stacks/stackconfig/doc.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package stackconfig deals with decoding and some static validation of the +// Terraform Stack language, which uses files with the suffixes .tfstack.hcl +// and .tfstack.json to describe a set of components to be planned and applied +// together. +// +// The Stack language has some elements that are intentionally similar to the +// main Terraform language (used to describe individual modules), but is +// currently implemented separately so they can evolve independently while +// the stacks language is still relatively new. Over time it might make sense +// to refactor so that there's only one implementation of each of the common +// elements, but we'll wait to see how similar things are once this language +// has been in real use for some time. +package stackconfig diff --git a/internal/stacks/stackconfig/embedded_stack.go b/internal/stacks/stackconfig/embedded_stack.go new file mode 100644 index 0000000000..f937951292 --- /dev/null +++ b/internal/stacks/stackconfig/embedded_stack.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// EmbeddedStack describes a call to another stack configuration whose +// declarations should be included as part of the overall stack configuration +// tree. +// +// An embedded stack exists only as a child of another stack and doesn't have +// its own independent identity outside of that calling stack. +// +// HCP Terraform offers a related concept of "linked stacks" where the +// deployment configuration for one stack can refer to the outputs of another, +// while the other stack retains its own independent identity and lifecycle, +// but that concept only makes sense in an environment like HCP Terraform +// where the stack outputs can be published for external consumption. +type EmbeddedStack struct { + Name string + + SourceAddr sourceaddrs.Source + VersionConstraints constraints.IntersectionSpec + SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange + + // FinalSourceAddr is populated only when a configuration is loaded + // through [LoadConfigDir], and in that case contains the finalized + // address produced by resolving the SourceAddr field relative to + // the address of the file where the component was declared. This + // is the address to use if you intend to load the component's + // root module from a source bundle. + FinalSourceAddr sourceaddrs.FinalSource + + ForEach hcl.Expression + + // Inputs is an expression that should produce a value that can convert + // to an object type derived from the child stack's input variable + // declarations, and whose attribute values will then be used to populate + // those input variables. + Inputs hcl.Expression + + DependsOn []hcl.Traversal + + DeclRange tfdiags.SourceRange +} + +func decodeEmbeddedStackBlock(block *hcl.Block) (*EmbeddedStack, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &EmbeddedStack{ + Name: block.Labels[0], + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + if !hclsyntax.ValidIdentifier(ret.Name) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid name for call to embedded stack", + block.LabelRanges[0], + )) + return nil, diags + } + + content, hclDiags := block.Body.Content(embeddedStackBlockSchema) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments( + content.Attributes["source"], + content.Attributes["version"], + ) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + ret.SourceAddr = sourceAddr + ret.VersionConstraints = versionConstraints + ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range) + if content.Attributes["version"] != nil { + ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range) + } + // Now that we've populated the mandatory source location fields we can + // safely return a partial ret if we encounter any further errors, as + // long as we leave the other fields either unset or in some other + // reasonable state for careful partial analysis. + + if attr, ok := content.Attributes["for_each"]; ok { + ret.ForEach = attr.Expr + } + if attr, ok := content.Attributes["inputs"]; ok { + ret.Inputs = attr.Expr + } + if attr, ok := content.Attributes["depends_on"]; ok { + ret.DependsOn, hclDiags = configs.DecodeDependsOn(attr) + diags = diags.Append(hclDiags) + } + + return ret, diags +} + +var embeddedStackBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "source", Required: true}, + {Name: "version", Required: false}, + {Name: "for_each", Required: false}, + {Name: "inputs", Required: false}, + {Name: "depends_on", Required: false}, + }, +} diff --git a/internal/stacks/stackconfig/file.go b/internal/stacks/stackconfig/file.go new file mode 100644 index 0000000000..260d9323e4 --- /dev/null +++ b/internal/stacks/stackconfig/file.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + hcljson "github.com/hashicorp/hcl/v2/json" + + "github.com/hashicorp/terraform/internal/tfdiags" +) + +const initialLanguageEdition = "TFStack2023" + +// File represents the content of a single .tfstack.hcl or .tfstack.json file +// before it's been merged with its siblings in the same directory to produce +// the overall [Stack] object. +type File struct { + // SourceAddr is the source location for this particular file, meaning + // that the "sub-path" portion of the address should always be populated + // and refer to a particular file rather than to a directory. + SourceAddr sourceaddrs.FinalSource + + Declarations +} + +// DecodeFileBody takes a body that is assumed to represent the root of a +// .tfstack.hcl or .tfstack.json file and decodes the declarations inside. +// +// If you have a []byte containing source code then consider using [ParseFile] +// instead, which parses the source code and then delegates to this function. +// +// This is exported for unusual situations where it's useful to analyze just +// a single file in isolation, without considering its context in a +// configuration tree. Some fields of the objects representing declarations in +// the configuration will be unpopulated when loading through this entry point. +// Prefer [LoadConfigDir] in most cases. +func DecodeFileBody(body hcl.Body, fileAddr sourceaddrs.FinalSource) (*File, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &File{ + SourceAddr: fileAddr, + Declarations: makeDeclarations(), + } + + content, hclDiags := body.Content(rootConfigSchema) + diags = diags.Append(hclDiags) + if content == nil { + return ret, diags + } + // Even if there are some errors we'll still try to analyze a partial + // result, in case it allows us to give the user more context to work + // with when resolving the errors detected so far. + + if langAttr, ok := content.Attributes["language"]; ok { + // For now there is only one edition of the language and so we'll just + // reject anything other than the current version. If we add other + // editions later then we'll probably need to move the check for this + // up into LoadSingleStackConfig so we can make sure that all of the + // files in a directory agree on a language edition to use. + editionKW := hcl.ExprAsKeyword(langAttr.Expr) + if editionKW != initialLanguageEdition { + var extra string + if strings.HasPrefix(editionKW, "TFStack") { + extra = "\n\nThis stack configuration might be intended for a newer version of Terraform." + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid language edition", + Detail: fmt.Sprintf( + "If you declare an explicit language edition then it must currently be the keyword %s, because no other editions are supported.%s", + initialLanguageEdition, extra, + ), + }) + // We'll halt processing here if it's not for our current edition, + // because we'll probably encounter language features from whatever + // later language edition this config was written for. + return ret, diags + } + } + + for _, block := range content.Blocks { + switch block.Type { + + case "component": + decl, moreDiags := decodeComponentBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addComponent(decl), + ) + + case "stack": + decl, moreDiags := decodeEmbeddedStackBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addEmbeddedStack(decl), + ) + + case "variable": + decl, moreDiags := decodeInputVariableBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addInputVariable(decl), + ) + + case "locals": + decls, moreDiags := decodeLocalValuesBlock(block) + diags = diags.Append(moreDiags) + for _, decl := range decls { + diags = diags.Append( + ret.Declarations.addLocalValue(decl), + ) + } + + case "output": + decl, moreDiags := decodeOutputValueBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addOutputValue(decl), + ) + + case "provider": + decl, moreDiags := decodeProviderConfigBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addProviderConfig(decl), + ) + + case "required_providers": + decl, moreDiags := decodeProviderRequirementsBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addRequiredProviders(decl), + ) + + case "removed": + decl, moreDiags := decodeRemovedBlock(block) + diags = diags.Append(moreDiags) + diags = diags.Append( + ret.Declarations.addRemoved(decl), + ) + + default: + // Should not get here because the cases above should be exhaustive + // for everything declared in rootConfigSchema. + panic(fmt.Sprintf("unhandled block type %q", block.Type)) + } + } + + return ret, diags +} + +// ParseFileSource parses the given source code as the content of either a +// .tfstack.hcl or .tfstack.json file, and then delegates the result to +// [DecodeFileBody] for analysis, returning that final result. +// +// ParseFileSource chooses between native vs. JSON syntax based on the suffix +// of the filename in the given source address, which must be either +// ".tfstack.hcl" or ".tfstack.json". +func ParseFileSource(src []byte, fileAddr sourceaddrs.FinalSource) (*File, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + filename := sourceaddrs.FinalSourceFilename(fileAddr) + + var body hcl.Body + switch validFilenameSuffix(filename) { + case ".tfstack.hcl": + hclFile, hclDiags := hclsyntax.ParseConfig(src, fileAddr.String(), hcl.InitialPos) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return nil, diags + } + body = hclFile.Body + case ".tfstack.json": + hclFile, hclDiags := hcljson.Parse(src, fileAddr.String()) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + return nil, diags + } + body = hclFile.Body + default: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unsupported file type", + fmt.Sprintf( + "Cannot load %s as a stack configuration file: filename must have either a .tfstack.hcl or .tfstack.json suffix.", + fileAddr, + ), + )) + return nil, diags + } + + ret, moreDiags := DecodeFileBody(body, fileAddr) + diags = diags.Append(moreDiags) + return ret, diags +} + +// validFilenameSuffix returns ".tfstack.hcl" or ".tfstack.json" if the +// given filename ends with that suffix, and otherwise returns an empty +// string to indicate that the suffix was invalid. +func validFilenameSuffix(filename string) string { + const nativeSuffix = ".tfstack.hcl" + const jsonSuffix = ".tfstack.json" + + switch { + case strings.HasSuffix(filename, nativeSuffix): + return nativeSuffix + case strings.HasSuffix(filename, jsonSuffix): + return jsonSuffix + default: + return "" + } +} + +var rootConfigSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "language"}, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "stack", LabelNames: []string{"name"}}, + {Type: "component", LabelNames: []string{"name"}}, + {Type: "variable", LabelNames: []string{"name"}}, + {Type: "locals"}, + {Type: "output", LabelNames: []string{"name"}}, + {Type: "provider", LabelNames: []string{"type", "name"}}, + {Type: "required_providers"}, + {Type: "removed"}, + }, +} diff --git a/internal/stacks/stackconfig/input_variable.go b/internal/stacks/stackconfig/input_variable.go new file mode 100644 index 0000000000..134b09797c --- /dev/null +++ b/internal/stacks/stackconfig/input_variable.go @@ -0,0 +1,115 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +// InputVariable is a declaration of an input variable within a stack +// configuration. Callers must provide the values for these variables. +type InputVariable struct { + Name string + + Type TypeConstraint + DefaultValue cty.Value + Description string + + Sensitive bool + Ephemeral bool + + DeclRange tfdiags.SourceRange +} + +// TypeConstraint represents all of the type constraint information for either +// an input variable or an output value. +// +// After initial decoding only Expression is populated, and it has not yet been +// analyzed at all so is not even guaranteed to be a valid type constraint +// expression. +// +// For configurations loaded through the main entry point [LoadConfigDir], +// Constraint is populated with the result of decoding Expression as a type +// constraint only if the expression is a valid type constraint expression. +// When loading through shallower entry points such as [DecodeFileBody], +// Constraint is not populated. +// +// Defaults is populated only if Constraint is, and if not nil represents any +// default values from the type constraint expression. +type TypeConstraint struct { + Expression hcl.Expression + Constraint cty.Type + Defaults *typeexpr.Defaults +} + +func decodeInputVariableBlock(block *hcl.Block) (*InputVariable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &InputVariable{ + Name: block.Labels[0], + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + if !hclsyntax.ValidIdentifier(ret.Name) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid name for input variable", + block.LabelRanges[0], + )) + return nil, diags + } + + content, hclDiags := block.Body.Content(inputVariableBlockSchema) + diags = diags.Append(hclDiags) + + if attr, ok := content.Attributes["type"]; ok { + ret.Type.Expression = attr.Expr + } + if attr, ok := content.Attributes["default"]; ok { + val, hclDiags := attr.Expr.Value(nil) + diags = diags.Append(hclDiags) + if val == cty.NilVal { + val = cty.DynamicVal + } + ret.DefaultValue = val + } + if attr, ok := content.Attributes["description"]; ok { + hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Description) + diags = diags.Append(hclDiags) + } + if attr, ok := content.Attributes["sensitive"]; ok { + hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Sensitive) + diags = diags.Append(hclDiags) + } + if attr, ok := content.Attributes["ephemeral"]; ok { + hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Ephemeral) + diags = diags.Append(hclDiags) + } + + for _, block := range content.Blocks { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Custom variable validation not yet supported", + Detail: "Input variables for a stack configuration do not yet support custom variable validation.", + Subject: block.DefRange.Ptr(), + }) + } + + return ret, diags +} + +var inputVariableBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "type", Required: true}, + {Name: "default", Required: false}, + {Name: "description", Required: false}, + {Name: "sensitive", Required: false}, + {Name: "ephemeral", Required: false}, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "validation"}, + }, +} diff --git a/internal/stacks/stackconfig/local_value.go b/internal/stacks/stackconfig/local_value.go new file mode 100644 index 0000000000..4d17bb9987 --- /dev/null +++ b/internal/stacks/stackconfig/local_value.go @@ -0,0 +1,47 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// LocalValue is a declaration of a private local value within a particular +// stack configuration. These are visible only within the scope of a particular +// [Stack]. +type LocalValue struct { + Name string + Value hcl.Expression + + DeclRange tfdiags.SourceRange +} + +func decodeLocalValuesBlock(block *hcl.Block) ([]*LocalValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + attrs, hclDiags := block.Body.JustAttributes() + diags = diags.Append(hclDiags) + if len(attrs) == 0 { + return nil, diags + } + + ret := make([]*LocalValue, 0, len(attrs)) + for name, attr := range attrs { + v := &LocalValue{ + Name: name, + Value: attr.Expr, + DeclRange: tfdiags.SourceRangeFromHCL(attr.NameRange), + } + if !hclsyntax.ValidIdentifier(v.Name) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid name for local value", + attr.NameRange, + )) + continue + } + ret = append(ret, v) + } + return ret, diags +} diff --git a/internal/stacks/stackconfig/output_value.go b/internal/stacks/stackconfig/output_value.go new file mode 100644 index 0000000000..313ad0228a --- /dev/null +++ b/internal/stacks/stackconfig/output_value.go @@ -0,0 +1,87 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// OutputValue is a declaration of a result from a stack configuration, which +// can be read by the stack's caller. +type OutputValue struct { + Name string + + Type TypeConstraint + + Value hcl.Expression + Description string + Sensitive bool + Ephemeral bool + + DeclRange tfdiags.SourceRange +} + +func decodeOutputValueBlock(block *hcl.Block) (*OutputValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &OutputValue{ + Name: block.Labels[0], + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + if !hclsyntax.ValidIdentifier(ret.Name) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid name for output value", + block.LabelRanges[0], + )) + return nil, diags + } + + content, hclDiags := block.Body.Content(outputValueBlockSchema) + diags = diags.Append(hclDiags) + + if attr, ok := content.Attributes["type"]; ok { + ret.Type.Expression = attr.Expr + } + if attr, ok := content.Attributes["value"]; ok { + ret.Value = attr.Expr + } + if attr, ok := content.Attributes["description"]; ok { + hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Description) + diags = diags.Append(hclDiags) + } + if attr, ok := content.Attributes["sensitive"]; ok { + hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Sensitive) + diags = diags.Append(hclDiags) + } + if attr, ok := content.Attributes["ephemeral"]; ok { + hclDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Ephemeral) + diags = diags.Append(hclDiags) + } + + for _, block := range content.Blocks { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Preconditions not yet supported", + Detail: "Output values for a stack configuration do not yet support preconditions.", + Subject: block.DefRange.Ptr(), + }) + } + + return ret, diags +} + +var outputValueBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "type", Required: true}, + {Name: "value", Required: false}, + {Name: "description", Required: false}, + {Name: "sensitive", Required: false}, + {Name: "ephemeral", Required: false}, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "precondition"}, + }, +} diff --git a/internal/stacks/stackconfig/parser/walker.go b/internal/stacks/stackconfig/parser/walker.go new file mode 100644 index 0000000000..ab781413da --- /dev/null +++ b/internal/stacks/stackconfig/parser/walker.go @@ -0,0 +1,198 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package parser + +import ( + "fmt" + + "github.com/apparentlymart/go-versions/versions" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/go-version" + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// SourceBundleModuleWalker is an implementation of [configs.ModuleWalker] +// that loads all modules from a single source bundle. +type SourceBundleModuleWalker struct { + absoluteSourceAddrs map[string]sourceaddrs.FinalSource + sources *sourcebundle.Bundle + parser *configs.SourceBundleParser +} + +func NewSourceBundleModuleWalker(rootModuleSource sourceaddrs.FinalSource, sources *sourcebundle.Bundle, parser *configs.SourceBundleParser) *SourceBundleModuleWalker { + absoluteSourceAddrs := make(map[string]sourceaddrs.FinalSource, 1) + absoluteSourceAddrs[addrs.RootModule.String()] = rootModuleSource + return &SourceBundleModuleWalker{ + absoluteSourceAddrs: absoluteSourceAddrs, + sources: sources, + parser: parser, + } +} + +// LoadModule implements configs.ModuleWalker. +func (w *SourceBundleModuleWalker) LoadModule(req *configs.ModuleRequest) (*configs.Module, *version.Version, hcl.Diagnostics) { + var diags hcl.Diagnostics + + // First we need to assemble the "final source address" for the module + // by asking the source bundle to match the given source address and + // version against what's in the bundle manifest. This should cause + // us to make the same decision that the source bundler made about + // which real package to use. + finalSourceAddr, err := w.finalSourceForModule(req.SourceAddr, &req.VersionConstraint.Required) + if err != nil { + // We should not typically get here because we're translating + // Terraform's own source address representations to the same + // representations the source bundle builder would've used, but + // we'll be robust about it nonetheless. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't load module for component", + Detail: fmt.Sprintf("Invalid source address: %s.", err), + Subject: req.SourceAddrRange.Ptr(), + }) + return nil, nil, diags + } + + absoluteSourceAddr, err := w.absoluteSourceAddr(finalSourceAddr, req.Parent) + if err != nil { + // Again, this should not happen, but let's ensure we can debug if it + // does. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't load module for component", + Detail: fmt.Sprintf("Unable to determine absolute source address: %s.", err), + Subject: req.SourceAddrRange.Ptr(), + }) + return nil, nil, diags + } + + // We store the absolute source address for this module so that any in-repo + // child modules can use it to construct their absolute source addresses + // too. + w.absoluteSourceAddrs[req.Path.String()] = absoluteSourceAddr + + _, err = w.sources.LocalPathForSource(absoluteSourceAddr) + if err != nil { + // We should not get here if the source bundle was constructed + // correctly. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't load module for component", + Detail: fmt.Sprintf("Failed to load this component's module %s: %s.", req.Path.String(), tfdiags.FormatError(err)), + Subject: req.SourceAddrRange.Ptr(), + }) + return nil, nil, diags + } + + mod, moreDiags := w.parser.LoadConfigDir(absoluteSourceAddr) + diags = append(diags, moreDiags...) + + // Annoyingly we now need to translate our version selection back into + // the legacy type again, so we can return it through the ModuleWalker API. + var legacyV *version.Version + if modSrc, ok := finalSourceAddr.(sourceaddrs.RegistrySourceFinal); ok { + legacyV, err = w.legacyVersionForVersion(modSrc.SelectedVersion()) + if err != nil { + // It would be very strange to get in here because by now we've + // already round-tripped between the legacy and modern version + // constraint representations once, so we should have a version + // number that's compatible with both. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't load module for component", + Detail: fmt.Sprintf("Invalid version string %q: %s.", modSrc.SelectedVersion(), err), + Subject: req.SourceAddrRange.Ptr(), + }) + } + } + return mod, legacyV, diags +} + +func (w *SourceBundleModuleWalker) finalSourceForModule(tfSourceAddr addrs.ModuleSource, versionConstraints *version.Constraints) (sourceaddrs.FinalSource, error) { + // Unfortunately the configs package still uses our old model of version + // constraints and Terraform's own form of source addresses, so we need + // to adapt to what the sourcebundle API is expecting. + sourceAddr, err := w.bundleSourceAddrForTerraformSourceAddr(tfSourceAddr) + if err != nil { + return nil, err + } + var allowedVersions versions.Set + if versionConstraints != nil { + allowedVersions, err = w.versionSetForLegacyVersionConstraints(versionConstraints) + if err != nil { + return nil, fmt.Errorf("invalid version constraints: %w", err) + } + } else { + allowedVersions = versions.Released + } + + switch sourceAddr := sourceAddr.(type) { + case sourceaddrs.FinalSource: + // Most source address types are already final source addresses. + return sourceAddr, nil + case sourceaddrs.RegistrySource: + // Registry sources are trickier because we need to figure out which + // exact version we're using. + vs := w.sources.RegistryPackageVersions(sourceAddr.Package()) + v := vs.NewestInSet(allowedVersions) + return sourceAddr.Versioned(v), nil + default: + // Should not get here because the above should be exhaustive for all + // possible address types. + return nil, fmt.Errorf("unsupported source address type %T", tfSourceAddr) + } +} + +func (w *SourceBundleModuleWalker) bundleSourceAddrForTerraformSourceAddr(tfSourceAddr addrs.ModuleSource) (sourceaddrs.Source, error) { + // In practice this should always succeed because the source bundle builder + // would've parsed the same source addresses using these same parsers + // and so source bundle building would've failed if the given address were + // outside the subset supported for source bundles. + switch tfSourceAddr := tfSourceAddr.(type) { + case addrs.ModuleSourceLocal: + return sourceaddrs.ParseLocalSource(tfSourceAddr.String()) + case addrs.ModuleSourceRemote: + return sourceaddrs.ParseRemoteSource(tfSourceAddr.String()) + case addrs.ModuleSourceRegistry: + return sourceaddrs.ParseRegistrySource(tfSourceAddr.String()) + default: + // Should not get here because the above should be exhaustive for all + // possible address types. + return nil, fmt.Errorf("unsupported source address type %T", tfSourceAddr) + } +} + +func (w *SourceBundleModuleWalker) absoluteSourceAddr(sourceAddr sourceaddrs.FinalSource, parent *configs.Config) (sourceaddrs.FinalSource, error) { + switch source := sourceAddr.(type) { + case sourceaddrs.LocalSource: + parentPath := addrs.RootModule + if parent != nil { + parentPath = parent.Path + } + absoluteParentSourceAddr, ok := w.absoluteSourceAddrs[parentPath.String()] + if !ok { + return nil, fmt.Errorf("unexpected missing source address for module parent %q", parentPath) + } + return sourceaddrs.ResolveRelativeFinalSource(absoluteParentSourceAddr, source) + default: + return sourceAddr, nil + } +} + +func (w *SourceBundleModuleWalker) versionSetForLegacyVersionConstraints(versionConstraints *version.Constraints) (versions.Set, error) { + // In practice this should always succeed because the source bundle builder + // would've parsed the same version constraints using this same parser + // and so source bundle building would've failed if the given address were + // outside the subset supported for source bundles. + return versions.MeetingConstraintsStringRuby(versionConstraints.String()) +} + +func (w *SourceBundleModuleWalker) legacyVersionForVersion(v versions.Version) (*version.Version, error) { + return version.NewVersion(v.String()) +} diff --git a/internal/stacks/stackconfig/provider_config.go b/internal/stacks/stackconfig/provider_config.go new file mode 100644 index 0000000000..d35d23bde0 --- /dev/null +++ b/internal/stacks/stackconfig/provider_config.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ProviderConfig is a provider configuration declared within a [Stack]. +type ProviderConfig struct { + LocalAddr addrs.LocalProviderConfig + + // ProviderAddr is populated only when loaded through either + // [LoadSingleStackConfig] or [LoadConfigDir], and contains the + // fully-qualified provider address corresponding to the local name + // given in field LocalAddr. + ProviderAddr addrs.Provider + + // TODO: Figure out how we're going to retain the relevant subset of + // a provider configuration in the state so that we still have what + // we need to destroy any associated objects when a provider is removed + // from the configuration. + ForEach hcl.Expression + + // Config is the body of the nested block containing the provider-specific + // configuration arguments, if specified. Some providers do not require + // explicit arguments and so the nested block is optional; this field + // will be nil if no block was included. + Config hcl.Body + + ProviderNameRange tfdiags.SourceRange + DeclRange tfdiags.SourceRange +} + +func decodeProviderConfigBlock(block *hcl.Block) (*ProviderConfig, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &ProviderConfig{ + LocalAddr: addrs.LocalProviderConfig{ + LocalName: block.Labels[0], + + // we call this "name" in the stacks configuration language, + // but it's "Alias" here because we're reusing an address type + // made for the Terraform module language. + Alias: block.Labels[1], + }, + ProviderNameRange: tfdiags.SourceRangeFromHCL(block.LabelRanges[0]), + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + if !hclsyntax.ValidIdentifier(ret.LocalAddr.LocalName) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid provider local name", + block.LabelRanges[0], + )) + return nil, diags + } + if !hclsyntax.ValidIdentifier(ret.LocalAddr.Alias) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid provider configuration name", + block.LabelRanges[0], + )) + return nil, diags + } + + content, hclDiags := block.Body.Content(providerConfigBlockSchema) + diags = diags.Append(hclDiags) + + if attr, ok := content.Attributes["for_each"]; ok { + ret.ForEach = attr.Expr + } + + for _, block := range content.Blocks { + switch block.Type { + case "config": + if ret.Config != nil { + if !hclsyntax.ValidIdentifier(ret.LocalAddr.LocalName) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate config block", + Detail: "A provider configuration block must contain only one nested \"config\" block.", + Subject: block.DefRange.Ptr(), + }) + return nil, diags + } + continue + } + ret.Config = block.Body + default: + // Should not get here because the above should cover all + // block types declared in the schema. + panic(fmt.Sprintf("unhandled block type %q", block.Type)) + } + } + + return ret, diags +} + +var providerConfigBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "for_each", Required: false}, + }, + Blocks: []hcl.BlockHeaderSchema{ + {Type: "config"}, + }, +} diff --git a/internal/stacks/stackconfig/provider_requirements.go b/internal/stacks/stackconfig/provider_requirements.go new file mode 100644 index 0000000000..a96af22505 --- /dev/null +++ b/internal/stacks/stackconfig/provider_requirements.go @@ -0,0 +1,223 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ProviderRequirements struct { + Requirements map[string]ProviderRequirement + + DeclRange tfdiags.SourceRange +} + +type ProviderRequirement struct { + LocalName string + + Provider addrs.Provider + VersionConstraints constraints.IntersectionSpec + + DeclRange tfdiags.SourceRange +} + +func decodeProviderRequirementsBlock(block *hcl.Block) (*ProviderRequirements, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + attrs, hclDiags := block.Body.JustAttributes() + diags = diags.Append(hclDiags) + if len(attrs) == 0 { + return nil, diags + } + + reverseMap := make(map[addrs.Provider]string) + + ret := &ProviderRequirements{ + Requirements: make(map[string]ProviderRequirement, len(attrs)), + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + for name, attr := range attrs { + if !hclsyntax.ValidIdentifier(name) { + diags = diags.Append(invalidNameDiagnostic( + "Invalid local name for provider", + attr.NameRange, + )) + continue + } + if existing, exists := ret.Requirements[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider local name", + Detail: fmt.Sprintf("A provider requirement with local name %q was already declared at %s.", name, existing.DeclRange.StartString()), + Subject: attr.NameRange.Ptr(), + }) + continue + } + declPairs, hclDiags := hcl.ExprMap(attr.Expr) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + continue + } + declAttrs := make(map[string]*hcl.KeyValuePair, len(declPairs)) + for i := range declPairs { + pair := &declPairs[i] + name := hcl.ExprAsKeyword(pair.Key) + if name == "" { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider requirement attribute", + Detail: "All of the attributes of a required_providers entry must be simple keywords.", + Subject: pair.Key.Range().Ptr(), + }) + continue + } + if existing, exists := declAttrs[name]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate attribute", + Detail: fmt.Sprintf("The attribute %q was already defined at %s.", name, existing.Key.Range()), + Subject: pair.Key.Range().Ptr(), + }) + continue + } + declAttrs[name] = pair + } + + var sourceAddrStr, versionConstraintsStr string + sourceAddrPair := declAttrs["source"] + versionConstraintsPair := declAttrs["version"] + delete(declAttrs, "source") + delete(declAttrs, "version") + + if sourceAddrPair == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required attribute", + Detail: "All required_providers entries must include the attribute \"source\", giving the qualified provider source address to use.", + Subject: attr.Expr.StartRange().Ptr(), + }) + continue + } + hclDiags = gohcl.DecodeExpression(sourceAddrPair.Value, nil, &sourceAddrStr) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + continue + } + providerAddr, moreDiags := addrs.ParseProviderSourceString(sourceAddrStr) + // Ugh: ParseProviderSourceString returns sourceless diagnostics, + // so we need to postprocess the diagnostics to add source locations + // to them. + for _, diag := range moreDiags { + diags = diags.Append(&hcl.Diagnostic{ + Severity: diag.Severity().ToHCL(), + Summary: diag.Description().Summary, + Detail: diag.Description().Detail, + Subject: sourceAddrPair.Value.Range().Ptr(), + }) + } + if moreDiags.HasErrors() { + continue + } + + var versionConstraints constraints.IntersectionSpec + if !providerAddr.IsBuiltIn() { + if versionConstraintsPair == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required attribute", + Detail: "Each required_providers entry for an installable provider must include the attribute \"version\", specifying the provider versions that this stack is compatible with.", + Subject: attr.Expr.StartRange().Ptr(), + }) + continue + } + for name, pair := range declAttrs { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider requirement attribute", + Detail: fmt.Sprintf("An attribute named %q is not expected here.", name), + Subject: pair.Key.Range().Ptr(), + }) + continue + } + hclDiags = gohcl.DecodeExpression(versionConstraintsPair.Value, nil, &versionConstraintsStr) + diags = diags.Append(hclDiags) + if diags.HasErrors() { + continue + } + var err error + versionConstraints, err = constraints.ParseRubyStyleMulti(versionConstraintsStr) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid version constraint", + Detail: fmt.Sprintf("Cannot use %q as a version constraint: %s.", versionConstraintsStr, err), + Subject: sourceAddrPair.Value.Range().Ptr(), + }) + continue + } + } else { + if versionConstraintsPair != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported attribute", + Detail: fmt.Sprintf("The provider %q is built in to Terraform, so does not support version constraints.", providerAddr.ForDisplay()), + Subject: attr.Expr.StartRange().Ptr(), + }) + continue + } + } + + if existingName, exists := reverseMap[providerAddr]; exists { + existing := ret.Requirements[existingName] + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider local name", + Detail: fmt.Sprintf( + "A requirement for provider %s was already declared with local name %q at %s.", + providerAddr, existingName, existing.DeclRange.StartString(), + ), + Subject: attr.NameRange.Ptr(), + }) + continue + } + + ret.Requirements[name] = ProviderRequirement{ + LocalName: name, + Provider: providerAddr, + VersionConstraints: versionConstraints, + DeclRange: tfdiags.SourceRangeFromHCL(attr.NameRange), + } + reverseMap[providerAddr] = name + } + return ret, diags +} + +func (pr *ProviderRequirements) ProviderForLocalName(localName string) (addrs.Provider, bool) { + if pr == nil { + return addrs.Provider{}, false + } + obj, ok := pr.Requirements[localName] + if !ok { + return addrs.Provider{}, false + } + return obj.Provider, true +} + +func (pr *ProviderRequirements) LocalNameForProvider(providerAddr addrs.Provider) (string, bool) { + if pr == nil { + return "", false + } + for localName, obj := range pr.Requirements { + if obj.Provider == providerAddr { + return localName, true + } + } + return "", false +} diff --git a/internal/stacks/stackconfig/removed.go b/internal/stacks/stackconfig/removed.go new file mode 100644 index 0000000000..a73bf11fa1 --- /dev/null +++ b/internal/stacks/stackconfig/removed.go @@ -0,0 +1,221 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "github.com/apparentlymart/go-versions/versions/constraints" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/gohcl" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Removed represents a component that was removed from the configuration. +// +// Removed blocks don't have labels associated with them, instead they have +// a "from" attribute that points directly to the old component that was +// removed. Removed blocks can also point to component instances specifically, +// using an index expression. The "for_each" attribute also means that the +// "from" attribute can't always be evaluated statically. +// +// Removed blocks are, therefore, represented by the FromComponent and FromIndex +// fields, which together represent the address of the removed component. The +// FromComponent field is the address of the component itself, and the FromIndex +// field is the index expression that will be evaluated to determine the +// specific instance of the component that was removed. +// +// FromIndex can be null if either the removed block is pointing to a component +// that was not instanced, or is pointing to all the instances of a removed +// component. +// +// For this reason, multiple Removed blocks can be associated with the same +// FromComponent, but with different FromIndex values. When the FromIndex values +// are evaluated, during the planning stage, we will validate that the FromIndex +// values are unique. +type Removed struct { + From stackaddrs.RemovedFrom + + SourceAddr sourceaddrs.Source + VersionConstraints constraints.IntersectionSpec + SourceAddrRange, VersionConstraintsRange tfdiags.SourceRange + + // FinalSourceAddr is populated only when a configuration is loaded + // through [LoadConfigDir], and in that case contains the finalized + // address produced by resolving the SourceAddr field relative to + // the address of the file where the component was declared. This + // is the address to use if you intend to load the component's + // root module from a source bundle. + FinalSourceAddr sourceaddrs.FinalSource + + ForEach hcl.Expression + + // ProviderConfigs describes the mapping between the static provider + // configuration slots declared in the component's root module and the + // dynamic provider configuration objects in scope in the calling + // stack configuration. + // + // This map deals with the slight schism between the stacks language's + // treatment of provider configurations as regular values of a special + // data type vs. the main Terraform language's treatment of provider + // configurations as something special passed out of band from the + // input variables. The overall structure and the map keys are fixed + // statically during decoding, but the final provider configuration objects + // are determined only at runtime by normal expression evaluation. + // + // The keys of this map refer to provider configuration slots inside + // the module being called, but use the local names defined in the + // calling stack configuration. The stacks language runtime will + // translate the caller's local names into the callee's declared provider + // configurations by using the stack configuration's table of local + // provider names. + // + // This will only be populated if From points to a component. + ProviderConfigs map[addrs.LocalProviderConfig]hcl.Expression + + // Inputs describes the inputs that will be used to destroy all components + // within the target stack. + // + // This will only be populated if From points to a stack. + Inputs hcl.Expression + + // Destroy controls whether this removed block will actually destroy all + // instances of resources within this component, or just removed them from + // the state. Defaults to true. + Destroy bool + + DeclRange tfdiags.SourceRange +} + +func decodeRemovedBlock(block *hcl.Block) (*Removed, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &Removed{ + DeclRange: tfdiags.SourceRangeFromHCL(block.DefRange), + } + + content, hclDiags := block.Body.Content(removedBlockSchema) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + // We're splitting out the component and the index now, as we can decode and + // analyse the component now. The index might be referencing the for_each + // variable, which we can't decode yet. + from, moreDiags := stackaddrs.ParseRemovedFrom(content.Attributes["from"].Expr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + ret.From = from + + sourceAddr, versionConstraints, moreDiags := decodeSourceAddrArguments( + content.Attributes["source"], + content.Attributes["version"], + ) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + ret.SourceAddr = sourceAddr + ret.VersionConstraints = versionConstraints + ret.SourceAddrRange = tfdiags.SourceRangeFromHCL(content.Attributes["source"].Range) + if content.Attributes["version"] != nil { + ret.VersionConstraintsRange = tfdiags.SourceRangeFromHCL(content.Attributes["version"].Range) + } + // Now that we've populated the mandatory source location fields we can + // safely return a partial ret if we encounter any further errors, as + // long as we leave the other fields either unset or in some other + // reasonable state for careful partial analysis. + + if attr, ok := content.Attributes["for_each"]; ok { + matches := false + for _, variable := range ret.From.Variables() { + if root, ok := variable[0].(hcl.TraverseRoot); ok { + if root.Name == "each" { + matches = true + break + } + } + } + if !matches { + // You have to refer to the for_each attribute somewhere in the + // from attribute. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each expression", + Detail: "A removed block with a for_each expression must reference that expression within the `from` attribute.", + Subject: attr.NameRange.Ptr(), + }) + } + + ret.ForEach = attr.Expr + } + if attr, ok := content.Attributes["providers"]; ok { + if ret.From.Component == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid providers attribute", + Detail: "A removed block that does not target a component should not specify any providers.", + Subject: attr.NameRange.Ptr(), + }) + } + + var providerDiags tfdiags.Diagnostics + ret.ProviderConfigs, providerDiags = decodeProvidersAttribute(attr) + diags = diags.Append(providerDiags) + } + + if attr, ok := content.Attributes["inputs"]; ok { + if ret.From.Component != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs attribute", + Detail: "A removed block that does not target an embedded stack should not specify any inputs.", + Subject: attr.NameRange.Ptr(), + }) + } + + ret.Inputs = attr.Expr + } + + ret.Destroy = true // default to true + for _, block := range content.Blocks { + switch block.Type { + case "lifecycle": + lcContent, lcDiags := block.Body.Content(removedLifecycleBlockSchema) + diags = diags.Append(lcDiags) + + if attr, ok := lcContent.Attributes["destroy"]; ok { + valDiags := gohcl.DecodeExpression(attr.Expr, nil, &ret.Destroy) + diags = diags.Append(valDiags) + } + } + } + + return ret, diags +} + +var removedBlockSchema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + {Type: "lifecycle"}, + }, + Attributes: []hcl.AttributeSchema{ + {Name: "from", Required: true}, + {Name: "source", Required: true}, + {Name: "version", Required: false}, + {Name: "for_each", Required: false}, + {Name: "providers", Required: false}, + {Name: "inputs", Required: false}, + }, +} + +var removedLifecycleBlockSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + {Name: "destroy"}, + }, +} diff --git a/internal/stacks/stackconfig/stack.go b/internal/stacks/stackconfig/stack.go new file mode 100644 index 0000000000..d0a5065733 --- /dev/null +++ b/internal/stacks/stackconfig/stack.go @@ -0,0 +1,166 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfig + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Stack represents a single stack, which can potentially call other +// "embedded stacks" in a similar manner to how Terraform modules can call +// other modules. +type Stack struct { + SourceAddr sourceaddrs.FinalSource + + // ConfigFiles describes the individual .tfstack.hcl or .tfstack.json + // files that this stack configuration object was built from. Most callers + // should ignore the detail of which file each declaration originated + // in, but we retain this in case it's useful for generating better error + // messages, etc. + // + // The keys of this map are the string representations of each file's + // source address, which also matches how we populate the "Filename" + // field of source ranges referring to the files and so callers can + // attempt to look up files by the diagnostic range filename, but must + // be resilient to cases where nothing matches because not all diagnostics + // will refer to stack configuration files. + ConfigFiles map[string]*File + + Declarations +} + +// LoadSingleStackConfig loads the configuration for only a single stack from +// the given source address. +// +// If the given address is a local source then it's interpreted relative to +// the process's current working directory. Otherwise it will be loaded from +// the provided source bundle. +// +// This is exported for unusual situations where it's useful to analyze just +// a single stack configuration directory in isolation, without considering +// its context in a configuration tree. Some fields of the objects representing +// declarations in the configuration will be unpopulated when loading through +// this entry point. Prefer [LoadConfigDir] in most cases. +func LoadSingleStackConfig(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle) (*Stack, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + localDir, err := sources.LocalPathForSource(sourceAddr) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot find configuration source code", + fmt.Sprintf("Failed to load %s from the pre-installed source packages: %s.", sourceAddr, err), + )) + return nil, diags + } + + allEntries, err := os.ReadDir(localDir) + if err != nil { + if os.IsNotExist(err) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Missing stack configuration", + fmt.Sprintf("There is no stack configuration directory at %s.", sourceAddr), + )) + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot read stack configuration", + // In this case the error message from the Go standard library + // is likely to disclose the real local directory name + // from the source bundle, but that's okay because it may + // sometimes help with debugging. + fmt.Sprintf("Error while reading the cached snapshot of %s: %s.", sourceAddr, err), + )) + } + return nil, diags + } + + ret := &Stack{ + SourceAddr: sourceAddr, + ConfigFiles: make(map[string]*File), + Declarations: makeDeclarations(), + } + + for _, entry := range allEntries { + if suffix := validFilenameSuffix(entry.Name()); suffix == "" { + // not a file we're interested in, then + continue + } + + asLocalSourcePath := "./" + filepath.Base(entry.Name()) + relSource, err := sourceaddrs.ParseLocalSource(asLocalSourcePath) + if err != nil { + // If we get here then it's a bug in how we constructed the + // path above, not invalid user input. + panic(fmt.Sprintf("constructed invalid relative source path: %s", err)) + } + fileSourceAddr, err := sourceaddrs.ResolveRelativeFinalSource(sourceAddr, relSource) + if err != nil { + // If we get here then it's a bug in how we constructed the + // path above, not invalid user input. + panic(fmt.Sprintf("constructed invalid relative source path: %s", err)) + } + if entry.IsDir() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid stack configuration directory", + fmt.Sprintf("The entry %s is a directory. All entries with the stack configuration name suffixes must be files.", fileSourceAddr), + )) + } + + src, err := os.ReadFile(filepath.Join(localDir, entry.Name())) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot read stack configuration", + // In this case the error message from the Go standard library + // is likely to disclose the real local directory name + // from the source bundle, but that's okay because it may + // sometimes help with debugging. + fmt.Sprintf("Error while reading the cached snapshot of %s: %s.", fileSourceAddr, err), + )) + } + + file, moreDiags := ParseFileSource(src, fileSourceAddr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + // We'll still try to analyze other files, so we can gather up + // as many diagnostics as possible to return all together in + // case there's some pattern between them that the user can + // fix systematically across all instances. + continue + } + + // Incorporate this file's declarations into the overall stack + // configuration. + diags = diags.Append(ret.Declarations.merge(&file.Declarations)) + ret.ConfigFiles[file.SourceAddr.String()] = file + } + + for _, pc := range ret.ProviderConfigs { + localName := pc.LocalAddr.LocalName + providerAddr, ok := ret.RequiredProviders.ProviderForLocalName(localName) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Undeclared provider local name", + Detail: fmt.Sprintf( + "This configuration's required_providers block does not include a definition for the local name %q.", + localName, + ), + }) + continue + } + pc.ProviderAddr = providerAddr + } + + return ret, diags +} diff --git a/internal/stacks/stackconfig/stackconfigtypes/provider_config.go b/internal/stacks/stackconfig/stackconfigtypes/provider_config.go new file mode 100644 index 0000000000..7a75f16122 --- /dev/null +++ b/internal/stacks/stackconfig/stackconfigtypes/provider_config.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackconfigtypes + +import ( + "fmt" + "reflect" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/zclconf/go-cty/cty" +) + +// ProviderConfigType constructs a new cty capsule type for representing an +// active provider configuration. +// +// Each call to this function will produce a distinct type, even if the +// given provider address matches a previous call. Callers should retain their +// own data structure of previously-constructed to ensure that they will use +// only a single type per distinct provider address. +func ProviderConfigType(providerAddr addrs.Provider) cty.Type { + return cty.CapsuleWithOps( + fmt.Sprintf("configuration for %s provider", providerAddr.ForDisplay()), + reflect.TypeOf(stackaddrs.AbsProviderConfigInstance{}), + &cty.CapsuleOps{ + TypeGoString: func(goTy reflect.Type) string { + return fmt.Sprintf( + "stackconfigtypes.ProviderConfigType(addrs.MustParseProviderSourceString(%q))", + providerAddr.String(), + ) + }, + RawEquals: func(a, b interface{}) bool { + return a.(*stackaddrs.AbsProviderConfigInstance).UniqueKey() == b.(*stackaddrs.AbsProviderConfigInstance).UniqueKey() + }, + ExtensionData: func(key interface{}) interface{} { + switch key { + case providerConfigExtDataKey: + return providerAddr + default: + return nil + } + }, + }, + ) +} + +// IsProviderConfigType returns true if the given type is one that was +// previously constructed with [ProviderConfigType], or false otherwise. +// +// If and only if this function returns true, callers can use +// [ProviderForProviderConfigType] to learn which specific provider the +// type is representing configurations for. +func IsProviderConfigType(ty cty.Type) bool { + if !ty.IsCapsuleType() { + return false + } + ops := ty.CapsuleOps() + if ops == nil { + return false + } + providerAddrI := ops.ExtensionData(providerConfigExtDataKey) + return providerAddrI != nil +} + +// ProviderForProviderConfigType returns the address of the provider that +// the given type can store instances of, or panics if the given type is +// not one produced by an earlier call to [ProviderConfigType]. +// +// Use [IsProviderConfigType] before calling to confirm whether an unknown +// type is safe to pass to this function. +func ProviderForProviderConfigType(ty cty.Type) addrs.Provider { + if !ty.IsCapsuleType() { + panic("not a provider config type") + } + ops := ty.CapsuleOps() + if ops == nil { + panic("not a provider config type") + } + providerAddrI := ops.ExtensionData(providerConfigExtDataKey) + return providerAddrI.(addrs.Provider) +} + +// ProviderInstanceValue encapsulates a provider config instance address in +// a cty.Value of the given provider config type, or panics if the type and +// address are inconsistent with one another. +func ProviderInstanceValue(ty cty.Type, addr stackaddrs.AbsProviderConfigInstance) cty.Value { + wantProvider := ProviderForProviderConfigType(ty) + if addr.Item.ProviderConfig.Provider != wantProvider { + panic(fmt.Sprintf("can't use %s instance for %s reference", addr.Item.ProviderConfig.Provider, wantProvider)) + } + return cty.CapsuleVal(ty, &addr) +} + +// ProviderInstanceForValue returns the provider configuration instance +// address encapsulated inside the given value, or panics if the value is +// not of a provider configuration reference type. +// +// Use [IsProviderConfigType] with the value's type to check first if a +// given value is suitable to pass to this function. +func ProviderInstanceForValue(v cty.Value) stackaddrs.AbsProviderConfigInstance { + if !IsProviderConfigType(v.Type()) { + panic("not a provider config value") + } + addrP := v.EncapsulatedValue().(*stackaddrs.AbsProviderConfigInstance) + return *addrP +} + +// ProviderInstancePathsInValue searches the leaves of the given value, +// which can be of any type, and returns all of the paths that lead to +// provider configuration references in no particular order. +// +// This is primarily intended for returning errors when values are traversing +// out of the stacks runtime into other subsystems, since provider configuration +// references are a stacks-language-specific concept. +func ProviderInstancePathsInValue(v cty.Value) []cty.Path { + var ret []cty.Path + cty.Transform(v, func(p cty.Path, v cty.Value) (cty.Value, error) { + if IsProviderConfigType(v.Type()) { + ret = append(ret, p) + } + return cty.NilVal, nil + }) + return ret +} + +// ProviderConfigPathsInType searches the leaves of the given type and returns +// all of the paths that lead to provider configuration references in no +// particular order. +// +// This is a type-oriented version of [ProviderInstancePathsInValue], for +// situations in the language where an author describes a specific type +// constraint that must not include provider configuration reference types +// regardless of final value. +// +// Because this function deals in types rather than values, the returned +// paths will include unknown value placeholders for any index operations +// traversing through collections. +func ProviderConfigPathsInType(ty cty.Type) []cty.Path { + return providerConfigPathsInType(ty, make(cty.Path, 0, 2)) +} + +func providerConfigPathsInType(ty cty.Type, prefix cty.Path) []cty.Path { + var ret []cty.Path + switch { + case IsProviderConfigType(ty): + // The rest of our traversal is constantly modifying the + // backing array of the prefix slice, so we must make + // a snapshot copy of it here to return. + result := make(cty.Path, len(prefix)) + copy(result, prefix) + ret = append(ret, result) + case ty.IsListType(): + ret = providerConfigPathsInType(ty.ElementType(), prefix.Index(cty.UnknownVal(cty.Number))) + case ty.IsMapType(): + ret = providerConfigPathsInType(ty.ElementType(), prefix.Index(cty.UnknownVal(cty.String))) + case ty.IsSetType(): + ret = providerConfigPathsInType(ty.ElementType(), prefix.Index(cty.DynamicVal)) + case ty.IsTupleType(): + etys := ty.TupleElementTypes() + ret = make([]cty.Path, 0, len(etys)) + for i, ety := range etys { + ret = append(ret, providerConfigPathsInType(ety, prefix.IndexInt(i))...) + } + case ty.IsObjectType(): + atys := ty.AttributeTypes() + ret = make([]cty.Path, 0, len(atys)) + for n, aty := range atys { + ret = append(ret, providerConfigPathsInType(aty, prefix.GetAttr(n))...) + } + default: + // No other types can potentially have nested provider configurations. + } + return ret +} + +type providerConfigExtDataKeyType int + +const providerConfigExtDataKey = providerConfigExtDataKeyType(0) diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/README b/internal/stacks/stackconfig/testdata/basics-bundle/README new file mode 100644 index 0000000000..13f57f39cd --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/README @@ -0,0 +1,28 @@ +This directory is a source bundle set up to behave as though it contains +the following remote source packages: + +- git::https://example.com/root.git +- git::https://example.com/nested.git + +The bundle also recognizes the following module registry address mapping: + +- example.com/awesomecorp/nested/happycloud@1.0.0 -> git::https://example.com/nested.git//subdir +- example.com/awesomecorp/nested/happycloud@2.0.0 -> git::https://example.com/nested.git//intentionally-missing-v2 + +The following source addresses contain valid stack configurations when +interpreted trough this bundle: + +- git::https://example.com/root.git +- git::https://example.com/nested.git//subdir +- example.com/awesomecorp/nested/happycloud if resolved with a version constraint that includes v1.0.0 and excludes v2.0.0 + +Requesting example.com/awesomecorp/nested/happycloud without a version +constraint that excludes v2.0.0 will select +git::https://example.com/nested.git//intentionally-missing-v2, which +as the name suggests is intentionally missing and so will cause configuration +loading to fail. + +There's also a regular Terraform module at this address, usable as a component +implementation: + +- git::https://example.com/nested.git diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/main.tf b/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/main.tf new file mode 100644 index 0000000000..5ab385503e --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/main.tf @@ -0,0 +1,17 @@ +variable "name" { + type = string +} + +resource "null_resource" "example" { + triggers = { + name = var.name + } +} + +output "greeting" { + value = "Hello, ${var.name}!" +} + +output "resource_id" { + value = null_resource.example.id +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/main.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/main.tfstack.hcl new file mode 100644 index 0000000000..960c36e29f --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/main.tfstack.hcl @@ -0,0 +1,53 @@ +required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } +} + +provider "null" "a" {} + +removed { + from = stack.a.component.a // bad, stack.a is undefined so this is orphaned + + source = "./" + + providers = { + null = provider.null.a + } +} + +removed { + from = stack.a.stack.b // bad, stack.a is undefined so this is orphaned + source = "./subdir" +} + +removed { + from = stack.b["a"] + source = "./subdir" +} + +removed { + from = stack.b["b"] + source = "./" // bad, the sources should be the same for stack.b +} + +removed { + from = stack.a.component.b["a"] + + source = "./" + + providers = { + null = provider.null.a + } +} + +removed { + from = stack.a.component.b["b"] // bad, the sources should be the same for component.b + + source = "./subdir" + + providers = { + null = provider.null.a + } +} \ No newline at end of file diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/subdir/main.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/subdir/main.tfstack.hcl new file mode 100644 index 0000000000..2ea566a8c4 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/errored-sources/subdir/main.tfstack.hcl @@ -0,0 +1,20 @@ +required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } +} + +provider "null" "a" {} + +component "a" { + source = "../" + + inputs = { + name = var.name + } + + providers = { + null = provider.null.a + } +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/errored/main.tf b/internal/stacks/stackconfig/testdata/basics-bundle/errored/main.tf new file mode 100644 index 0000000000..5ab385503e --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/errored/main.tf @@ -0,0 +1,17 @@ +variable "name" { + type = string +} + +resource "null_resource" "example" { + triggers = { + name = var.name + } +} + +output "greeting" { + value = "Hello, ${var.name}!" +} + +output "resource_id" { + value = null_resource.example.id +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/errored/main.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/errored/main.tfstack.hcl new file mode 100644 index 0000000000..d753473335 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/errored/main.tfstack.hcl @@ -0,0 +1,61 @@ +required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } +} + +provider "null" "a" {} + +component "a" { + source = "./" + + inputs = { + name = var.name + } + + providers = { + null = provider.null.a + } +} + +removed { + // This is invalid, you can't reference the whole component like this if + // the target component is still in the config. + from = component.a + + source = "./" + + providers = { + null = provider.null.a + } +} + + +removed { + // This is invalid, you must reference the for_each somewhere in the + // from attribute if both are present. + from = component.b["something"] + + for_each = ["a", "b"] + + source = "./" + + providers = { + null = provider.null.a + } +} + +removed { + // This is invalid, you must reference the for_each somewhere in the + // from attribute if both are present. + from = component.c + + for_each = ["a", "b"] + + source = "./" + + providers = { + null = provider.null.a + } +} \ No newline at end of file diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/nested/main.tf b/internal/stacks/stackconfig/testdata/basics-bundle/nested/main.tf new file mode 100644 index 0000000000..5ab385503e --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/nested/main.tf @@ -0,0 +1,17 @@ +variable "name" { + type = string +} + +resource "null_resource" "example" { + triggers = { + name = var.name + } +} + +output "greeting" { + value = "Hello, ${var.name}!" +} + +output "resource_id" { + value = null_resource.example.id +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/nested/subdir/main.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/nested/subdir/main.tfstack.hcl new file mode 100644 index 0000000000..a4efeb44e2 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/nested/subdir/main.tfstack.hcl @@ -0,0 +1,43 @@ +required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } +} + +variable "name" { + type = string +} + +variable "provider" { + type = providerconfig(null) +} + +component "a" { + source = "../" + + inputs = { + name = var.name + } + providers = { + null = var.provider + } +} + +removed { + from = component.b + + source = "../" + providers = { + null = var.provider + } + + lifecycle { + destroy = true + } +} + +output "greeting" { + type = string + value = component.a.greeting +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/root/main.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/root/main.tfstack.hcl new file mode 100644 index 0000000000..900029e8be --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/root/main.tfstack.hcl @@ -0,0 +1,16 @@ +stack "nested" { + source = "example.com/awesomecorp/nested/happycloud" + version = "< 2.0.0" + + inputs = { + name = var.name + provider = provider.null.a + } +} + +provider "null" "a" { +} + +locals { + sound = "bleep bloop" +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/root/outputs.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/root/outputs.tfstack.hcl new file mode 100644 index 0000000000..258d8dc75b --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/root/outputs.tfstack.hcl @@ -0,0 +1,16 @@ +output "greeting" { + type = string + value = stack.nested.greeting +} + +output "sound" { + type = string + value = local.sound +} + +output "password" { + type = string + value = "not really" + sensitive = true + ephemeral = true +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/root/variables.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/root/variables.tfstack.hcl new file mode 100644 index 0000000000..6f2d1eac07 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/root/variables.tfstack.hcl @@ -0,0 +1,9 @@ +variable "name" { + type = string +} + +variable "auth_jwt" { + type = string + ephemeral = true + sensitive = true +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/root/versions.tfstack.hcl b/internal/stacks/stackconfig/testdata/basics-bundle/root/versions.tfstack.hcl new file mode 100644 index 0000000000..85673c4882 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/root/versions.tfstack.hcl @@ -0,0 +1,8 @@ +language = TFStack2023 + +required_providers { + null = { + source = "hashicorp/null" + version = "3.2.1" + } +} diff --git a/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json b/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json new file mode 100644 index 0000000000..16e1f0a660 --- /dev/null +++ b/internal/stacks/stackconfig/testdata/basics-bundle/terraform-sources.json @@ -0,0 +1,38 @@ +{ + "terraform_source_bundle": 1, + "packages": [ + { + "source": "git::https://example.com/root.git", + "local": "root", + "meta": {} + }, + { + "source": "git::https://example.com/nested.git", + "local": "nested", + "meta": {} + }, + { + "source": "git::https://example.com/errored.git", + "local": "errored", + "meta": {} + }, + { + "source": "git::https://example.com/errored-sources.git", + "local": "errored-sources", + "meta": {} + } + ], + "registry": [ + { + "source": "example.com/awesomecorp/nested/happycloud", + "versions": { + "1.0.0": { + "source": "git::https://example.com/nested.git//subdir" + }, + "2.0.0": { + "source": "git::https://example.com/nested.git//intentionally-missing-v2" + } + } + } + ] +} \ No newline at end of file diff --git a/internal/stacks/stackconfig/typeexpr/type_info.go b/internal/stacks/stackconfig/typeexpr/type_info.go new file mode 100644 index 0000000000..0942801694 --- /dev/null +++ b/internal/stacks/stackconfig/typeexpr/type_info.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package typeexpr + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/zclconf/go-cty/cty" +) + +// TypeInformation is an interface used to give [TypeConstraint] information +// about its surrounding environment to use during type expression decoding. +// +// [TypeInformation]'s API is not concurrency-safe, so the same object should +// not be passed to multiple concurrent calls of [TypeConstraint]. +type TypeInformation interface { + // SetProviderConfigType stores a capsule type allocated to represent + // provider configurations for the given provider. The same type + // will then be returned from subsequent calls to ProviderConfigType + // using the same provider address. + SetProviderConfigType(providerAddr addrs.Provider, ty cty.Type) + + // ProviderConfigType retrieves a provider configurationc capsule type + // previously stored by SetProviderConfigType, or [cty.NilType] if + // there was no previous call for the given provider address. + ProviderConfigType(providerAddr addrs.Provider) cty.Type + + // ProviderForLocalName translates a provider local name into its + // corresponding fully-qualified provider address, or sets its second + // return value to false if there is no such local name defined. + ProviderForLocalName(localName string) (addrs.Provider, bool) +} diff --git a/internal/typeexpr/get_type.go b/internal/stacks/stackconfig/typeexpr/typeexpr.go similarity index 76% rename from internal/typeexpr/get_type.go rename to internal/stacks/stackconfig/typeexpr/typeexpr.go index 10ed611cb2..d78edbb016 100644 --- a/internal/typeexpr/get_type.go +++ b/internal/stacks/stackconfig/typeexpr/typeexpr.go @@ -1,21 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package typeexpr import ( "fmt" "github.com/hashicorp/hcl/v2" + hcltypeexpr "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" ) const invalidTypeSummary = "Invalid type specification" -// getType is the internal implementation of Type, TypeConstraint, and -// TypeConstraintWithDefaults, using the passed flags to distinguish. When -// `constraint` is true, the "any" keyword can be used in place of a concrete -// type. When `withDefaults` is true, the "optional" call expression supports -// an additional argument describing a default value. -func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) { +type Defaults = hcltypeexpr.Defaults + +// TypeConstraint attempts to parse the given expression as a type constraint +// and, if successful, returns the resulting type. If unsuccessful, error +// diagnostics are returned. +// +// A type constraint has the same structure as a type, but it additionally +// allows the keyword "any" to represent cty.DynamicPseudoType, which is often +// used as a wildcard in type checking and type conversion operations. +func TypeConstraint(expr hcl.Expression, typeInfo TypeInformation) (cty.Type, *Defaults, hcl.Diagnostics) { + return getType(expr, typeInfo, true, true) +} + +func getType(expr hcl.Expression, typeInfo TypeInformation, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) { // First we'll try for one of our keywords kw := hcl.ExprAsKeyword(expr) switch kw { @@ -26,13 +39,10 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def case "number": return cty.Number, nil, nil case "any": - if constraint { - return cty.DynamicPseudoType, nil, nil - } return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ Severity: hcl.DiagError, Summary: invalidTypeSummary, - Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw), + Detail: "There is no automatic type inference placeholder \"any\". Write an explicit type constraint instead.", Subject: expr.Range().Ptr(), }} case "list", "map", "set": @@ -56,6 +66,13 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def Detail: "The tuple type constructor requires one argument specifying the element types as a list.", Subject: expr.Range().Ptr(), }} + case "providerconfig": + return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: invalidTypeSummary, + Detail: "The providerconfig type constructor requires one argument specifying the local name of the provider this configuration is for.", + Subject: expr.Range().Ptr(), + }} case "": // okay! we'll fall through and try processing as a call, then. default: @@ -131,21 +148,29 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def Subject: &subjectRange, Context: &contextRange, }} + case "providerconfig": + return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: invalidTypeSummary, + Detail: "The providerconfig type constructor requires one argument specifying the local name of the provider this configuration is for.", + Subject: &subjectRange, + Context: &contextRange, + }} } } switch call.Name { case "list": - ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) + ety, defaults, diags := getType(call.Arguments[0], typeInfo, constraint, withDefaults) ty := cty.List(ety) return ty, collectionDefaults(ty, defaults), diags case "set": - ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) + ety, defaults, diags := getType(call.Arguments[0], typeInfo, constraint, withDefaults) ty := cty.Set(ety) return ty, collectionDefaults(ty, defaults), diags case "map": - ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) + ety, defaults, diags := getType(call.Arguments[0], typeInfo, constraint, withDefaults) ty := cty.Map(ety) return ty, collectionDefaults(ty, defaults), diags case "object": @@ -243,13 +268,13 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def } } - aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults) + aty, aDefaults, attrDiags := getType(atyExpr, typeInfo, constraint, withDefaults) diags = append(diags, attrDiags...) // If a default is set for an optional attribute, verify that it is // convertible to the attribute type. if defaultVal, ok := defaultValues[attrName]; ok { - _, err := convert.Convert(defaultVal, aty) + convertedDefaultVal, err := convert.Convert(defaultVal, aty) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, @@ -258,6 +283,8 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def Subject: defaultExpr.Range().Ptr(), }) delete(defaultValues, attrName) + } else { + defaultValues[attrName] = convertedDefaultVal } } @@ -266,10 +293,6 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def children[attrName] = aDefaults } } - // NOTE: ObjectWithOptionalAttrs is experimental in cty at the - // time of writing, so this interface might change even in future - // minor versions of cty. We're accepting that because Terraform - // itself is considering optional attributes as experimental right now. ty := cty.ObjectWithOptionalAttrs(atys, optAttrs) return ty, structuredDefaults(ty, defaultValues, children), diags case "tuple": @@ -286,7 +309,7 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def etys := make([]cty.Type, len(elemDefs)) children := make(map[string]*Defaults, len(elemDefs)) for i, defExpr := range elemDefs { - ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults) + ety, elemDefaults, elemDiags := getType(defExpr, typeInfo, constraint, withDefaults) diags = append(diags, elemDiags...) etys[i] = ety if elemDefaults != nil { @@ -295,6 +318,31 @@ func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Def } ty := cty.Tuple(etys) return ty, structuredDefaults(ty, nil, children), diags + case "providerconfig": + localName := hcl.ExprAsKeyword(call.Arguments[0]) + if localName == "" { + return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: invalidTypeSummary, + Detail: "The argument to providerconfig must be just the local name of the provider this configuration is for, given as an identifier.", + Subject: call.Arguments[0].Range().Ptr(), + }} + } + providerAddr, ok := typeInfo.ProviderForLocalName(localName) + if !ok { + return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ + Severity: hcl.DiagError, + Summary: invalidTypeSummary, + Detail: fmt.Sprintf("The name %q does not match any provider local name defined in this configuration's required_providers block.", localName), + Subject: call.Arguments[0].Range().Ptr(), + }} + } + ty := typeInfo.ProviderConfigType(providerAddr) + if ty == cty.NilType { + ty = stackconfigtypes.ProviderConfigType(providerAddr) + typeInfo.SetProviderConfigType(providerAddr, ty) + } + return ty, nil, diags case "optional": return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ Severity: hcl.DiagError, diff --git a/internal/stacks/stackmigrate/components.go b/internal/stacks/stackmigrate/components.go new file mode 100644 index 0000000000..b892cbf305 --- /dev/null +++ b/internal/stacks/stackmigrate/components.go @@ -0,0 +1,265 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func (m *migration) migrateComponents(components collections.Map[Instance, collections.Set[*stackResource]]) { + // We need to calculate the dependencies between components, so we can + // populate the dependencies and dependents fields in the component instances. + dependencies, dependents := m.calculateDependencies(components) + + for instance := range components.All() { + // We need to see the inputs and outputs from the component, so we can + // create the component instance with the correct values. + // ignore the diag because we already found this when loading the config. + config, _ := m.moduleConfig(m.Config.Component(stackaddrs.ConfigComponentForAbsInstance(instance))) + + // We can put unknown values into the state for now, as Stacks should + // perform a refresh before actually using any of these anyway. + inputs := make(map[addrs.InputVariable]cty.Value, len(config.Module.Variables)) + for name := range config.Module.Variables { + inputs[addrs.InputVariable{Name: name}] = cty.DynamicVal + } + outputs := make(map[addrs.OutputValue]cty.Value, len(config.Module.Outputs)) + for name := range config.Module.Outputs { + outputs[addrs.OutputValue{Name: name}] = cty.DynamicVal + } + + // We need this address to be able to look up dependencies and + // dependents later. + addr := AbsComponent{ + Stack: instance.Stack, + Item: instance.Item.Component, + } + + // We emit a change a change for each component instance + m.emit(&stackstate.AppliedChangeComponentInstance{ + ComponentAddr: AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: instance.Item.Component, + }, + ComponentInstanceAddr: instance, + + OutputValues: outputs, + InputVariables: inputs, + + // If a destroy plan, or a removed block, is executed before the + // next plan is applied, the component will break without this + // metadata. + Dependencies: dependencies.Get(addr), + Dependents: dependents.Get(addr), + }) + } +} + +func (m *migration) calculateDependencies(components collections.Map[Instance, collections.Set[*stackResource]]) (collections.Map[AbsComponent, collections.Set[AbsComponent]], collections.Map[AbsComponent, collections.Set[AbsComponent]]) { + // The dependency map cares only about config components rather than instances, + // so we need to convert the map to use the config component address. + cfgComponents := collections.NewMap[AbsComponent, collections.Set[*stackResource]]() + for in, cmpnts := range components.All() { + cfgComponents.Put(AbsComponent{ + Stack: in.Stack, + Item: in.Item.Component, + }, cmpnts) + } + + dependencies := collections.NewMap[AbsComponent, collections.Set[AbsComponent]]() + dependents := collections.NewMap[AbsComponent, collections.Set[AbsComponent]]() + + // First, we're going to work out the dependencies between components. + for addr, cmpnts := range cfgComponents.All() { + for resource := range cmpnts.All() { + instance := resource.AbsResource.Component + + compDepSet := collections.NewSet[AbsComponent]() + + // We collect the component's dependencies, and also + // add the component to the dependent set of its dependencies. + addDependencies := func(dss collections.Set[AbsComponent]) { + compDepSet.AddAll(dss) + for cmpt := range dss.All() { + if !dependents.HasKey(cmpt) { + dependents.Put(cmpt, collections.NewSet[AbsComponent]()) + } + dependents.Get(cmpt).Add(addr) + } + } + + component := resource.ComponentConfig + stack := resource.StackConfig + // First, check the inputs. + inputDependencies, inputDiags := m.componentDependenciesFromExpression(component.Inputs, instance.Stack, cfgComponents) + m.emitDiags(inputDiags) + addDependencies(inputDependencies) + + // Then, check the depends_on directly. + for _, traversal := range component.DependsOn { + dependsOnDependencies, dependsOnDiags := m.componentDependenciesFromTraversal(traversal, instance.Stack, cfgComponents) + m.emitDiags(dependsOnDiags) + addDependencies(dependsOnDependencies) + } + + // Then, check the foreach. + forEachDependencies, forEachDiags := m.componentDependenciesFromExpression(component.ForEach, instance.Stack, cfgComponents) + m.emitDiags(forEachDiags) + addDependencies(forEachDependencies) + + // Finally, we're going to look at the providers, and see if they + // depend on any other components. + for _, expr := range component.ProviderConfigs { + pds, diags := m.providerDependencies(expr, instance.Stack, stack, cfgComponents) + m.emitDiags(diags) + addDependencies(pds) + } + + // We're happy we got all the dependencies for this component, so we + // can store them now. + dependencies.Put(addr, compDepSet) + } + } + return dependencies, dependents +} + +// componentDependenciesFromExpression returns a set of components that are +// referenced in the given expression. +func (m *migration) componentDependenciesFromExpression(expr hcl.Expression, current stackaddrs.StackInstance, components collections.Map[AbsComponent, collections.Set[*stackResource]]) (ds collections.Set[AbsComponent], diags tfdiags.Diagnostics) { + ds = collections.NewSet[AbsComponent]() + if expr == nil { + return ds, diags + } + + for _, v := range expr.Variables() { + dss, moreDiags := m.componentDependenciesFromTraversal(v, current, components) + ds.AddAll(dss) + diags = diags.Append(moreDiags) + } + return ds, diags +} + +// componentDependenciesFromTraversal returns the component that is referenced +// in the given traversal, if it is a component reference. +func (m *migration) componentDependenciesFromTraversal(traversal hcl.Traversal, current stackaddrs.StackInstance, components collections.Map[AbsComponent, collections.Set[*stackResource]]) (deps collections.Set[AbsComponent], diags tfdiags.Diagnostics) { + deps = collections.NewSet[AbsComponent]() + + parsed, _, moreDiags := stackaddrs.ParseReference(traversal) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + // Then the configuration is invalid, so we'll skip this variable. + // The user should have ran a separate validation step before + // performing the migration to catch this. + return deps, diags + } + + switch ref := parsed.Target.(type) { + case stackaddrs.Component: + // We have a reference to a component in the current stack. + deps.Add(AbsComponent{ + Stack: current, + Item: ref, + }) + return deps, diags + case stackaddrs.StackCall: + targetStackAddress := append(current.ConfigAddr(), stackaddrs.StackStep(ref)) + stack := m.Config.Stack(targetStackAddress) + + if stack == nil { + // reference to a stack that does not exist in the configuration. + diags = diags.Append(hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Stack not found", + Detail: fmt.Sprintf("Stack %q not found in configuration.", targetStackAddress), + Subject: parsed.SourceRange.ToHCL().Ptr(), + }) + return deps, diags + } + + for name := range stack.Components { + // If the component in the stack call is part of the mapping, then it will + // be present in the map, and we will add it to the dependencies. + // Otherwise, we will ignore it. + componentAddr := AbsComponent{ + Stack: current.Child(ref.Name, addrs.NoKey), + Item: stackaddrs.Component{Name: name}, + } + + if _, ok := components.GetOk(componentAddr); ok { + deps.Add(componentAddr) + } + } + return deps, diags + default: + // This is not a component reference, and we only care about + // component dependencies. + return deps, diags + } +} + +func (m *migration) providerDependencies(expr hcl.Expression, current stackaddrs.StackInstance, stack *stackconfig.Stack, components collections.Map[AbsComponent, collections.Set[*stackResource]]) (ds collections.Set[AbsComponent], diags tfdiags.Diagnostics) { + ds = collections.NewSet[AbsComponent]() + for _, v := range expr.Variables() { + ref, _, moreDiags := stackaddrs.ParseReference(v) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + // Invalid configuration, so skip it. + continue + } + + switch ref := ref.Target.(type) { + case stackaddrs.ProviderConfigRef: + config := stack.ProviderConfigs[addrs.LocalProviderConfig{ + LocalName: ref.ProviderLocalName, + Alias: ref.Name, + }] + + dss, moreDiags := m.componentDependenciesFromExpression(config.ForEach, current, components) + diags = diags.Append(moreDiags) + ds.AddAll(dss) + + if config.Config == nil { + // if there is no configuration, then there won't be any + // dependencies. + break + } + + addr, ok := stack.RequiredProviders.ProviderForLocalName(ref.ProviderLocalName) + if !ok { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Provider %s not found in required_providers.", ref.ProviderLocalName))) + continue + } + + provider, pDiags := m.provider(addr) + if pDiags.HasErrors() { + diags = diags.Append(pDiags) + continue // skip this provider if we can't get the schema + } + + spec := provider.GetProviderSchema().Provider.Body.DecoderSpec() + traversals := hcldec.Variables(config.Config, spec) + for _, traversal := range traversals { + dss, moreDiags := m.componentDependenciesFromTraversal(traversal, current, components) + diags = diags.Append(moreDiags) + ds.AddAll(dss) + } + + default: + // This is not a provider reference, and we only care about + // provider dependencies. + continue + } + } + return ds, diags +} diff --git a/internal/stacks/stackmigrate/load.go b/internal/stacks/stackmigrate/load.go new file mode 100644 index 0000000000..1bdbe0eb3c --- /dev/null +++ b/internal/stacks/stackmigrate/load.go @@ -0,0 +1,165 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform-svchost/disco" + "github.com/hashicorp/terraform/internal/backend" + backendInit "github.com/hashicorp/terraform/internal/backend/init" + "github.com/hashicorp/terraform/internal/backend/local" + "github.com/hashicorp/terraform/internal/backend/remote" + "github.com/hashicorp/terraform/internal/command/clistate" + "github.com/hashicorp/terraform/internal/command/workdir" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statemgr" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type Loader struct { + Discovery *disco.Disco +} + +var ( + WorkspaceNameEnvVar = "TF_WORKSPACE" +) + +// LoadState loads a state from the given configPath. The configuration at configPath +// must have been initialized via `terraform init` before calling this function. +// The function returns an empty state even if there are errors. +func (l *Loader) LoadState(configPath string) (*states.State, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + state := states.NewState() + workingDirectory, workspace, err := l.loadWorkingDir(configPath) + if err != nil { + return state, diags.Append(fmt.Errorf("error loading working directory: %s", err)) + } + + backendInit.Init(l.Discovery) + + // First, we'll load the "backend state". This should have been initialised + // by the `terraform init` command, and contains the configuration for the + // backend that we're using. + var backendState *workdir.BackendStateFile + backendStatePath := filepath.Join(workingDirectory.DataDir(), ".terraform.tfstate") + st := &clistate.LocalState{Path: backendStatePath} + // If the backend state file is not provided, RefreshState will + // return nil error and State will be empty. + // In this case, we assume that we're using a local backend. + if err := st.RefreshState(); err != nil { + diags = diags.Append(fmt.Errorf("error loading backend state: %s", err)) + return state, diags + } + backendState = st.State() + + // Now that we have the backend state, we can initialise the backend itself + // based on what we had from the `terraform init` command. + var backend backend.Backend + var backendConfig cty.Value + + // the absence of backend state file indicates a local backend + if backendState == nil { + backend = local.New() + backendConfig = cty.ObjectVal(map[string]cty.Value{ + "path": cty.StringVal(fmt.Sprintf("%s/%s", configPath, "terraform.tfstate")), + "workspace_dir": cty.StringVal(configPath), + }) + } else { + initFn := backendInit.Backend(backendState.Backend.Type) + if initFn == nil { + diags = diags.Append(fmt.Errorf("unknown backend type %q", backendState.Backend.Type)) + return state, diags + } + + backend = initFn() + schema := backend.ConfigSchema() + config, err := backendState.Backend.Config(schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to decode current backend config", + fmt.Sprintf("The backend configuration created by the most recent run of \"terraform init\" could not be decoded: %s. The configuration may have been initialized by an earlier version that used an incompatible configuration structure. Run \"terraform init -reconfigure\" to force re-initialization of the backend.", err), + )) + return state, diags + } + + var moreDiags tfdiags.Diagnostics + backendConfig, moreDiags = backend.PrepareConfig(config) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return state, diags + } + + // it's safe to ignore terraform version conflict between the local and remote environments, + // as we are only reading the state + if backendR, ok := backend.(*remote.Remote); ok { + backendR.IgnoreVersionConflict() + } + } + + // Now that we have the backend and its configuration, we can configure it. + moreDiags := backend.Configure(backendConfig) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return state, diags + } + + // The backend is initialised and configured, so now we can load the state + // from the backend. + stateManager, err := backend.StateMgr(workspace) + if err != nil { + diags = diags.Append(fmt.Errorf("error loading state: %s", err)) + return state, diags + } + + // We'll lock the backend here to ensure that we don't have any concurrent + // operations on the state. If this fails, we'll return an error and the + // user should retry the migration later when nothing is currently updating + // the state. + id, err := stateManager.Lock(statemgr.NewLockInfo()) + if err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to lock state", fmt.Sprintf("The state is currently locked by another operation: %s. Please retry the migration later.", err))) + return state, diags + } + // Remember to unlock the state when we're done. + defer func() { + // Remember to unlock the state when we're done. + if err := stateManager.Unlock(id); err != nil { + // If we couldn't unlock the state, we'll warn about that but the + // migration can actually continue. + diags = diags.Append(tfdiags.Sourceless(tfdiags.Warning, "Failed to unlock state", fmt.Sprintf("The state was successfully loaded but could not be unlocked: %s. The migration can continue but the state many need to be unlocked manually.", err))) + } + }() + + if err := stateManager.RefreshState(); err != nil { + diags = diags.Append(fmt.Errorf("error loading state: %s", err)) + return state, diags + } + state = stateManager.State() + + return state, diags +} + +func (l *Loader) loadWorkingDir(configPath string) (*workdir.Dir, string, error) { + // load the state specified by this configuration + workingDirectory := workdir.NewDir(configPath) + if data := os.Getenv("TF_DATA_DIR"); len(data) > 0 { + workingDirectory.OverrideDataDir(data) + } + meta := Meta{WorkingDir: workingDirectory} + + // Load the currently active workspace from the environment, defaulting + // to the default workspace if not set. + workspace, err := meta.Workspace() + if err != nil { + return nil, "", fmt.Errorf("failed to load workspace: %s", err) + } + + return workingDirectory, workspace, nil +} diff --git a/internal/stacks/stackmigrate/load_test.go b/internal/stacks/stackmigrate/load_test.go new file mode 100644 index 0000000000..ad3b1766ce --- /dev/null +++ b/internal/stacks/stackmigrate/load_test.go @@ -0,0 +1,376 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "strings" + "testing" + + svchost "github.com/hashicorp/terraform-svchost" + "github.com/hashicorp/terraform-svchost/auth" + "github.com/hashicorp/terraform-svchost/disco" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/httpclient" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" + "github.com/hashicorp/terraform/version" + "github.com/zclconf/go-cty/cty" +) + +func TestLoad_Local(t *testing.T) { + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + statePath := TestStateFile(t, state) + loader := &Loader{} + loadedState, diags := loader.LoadState(strings.TrimSuffix(statePath, "/terraform.tfstate")) + if diags.HasErrors() { + t.Fatalf("failed to load state: %s", diags.Err()) + } + + if !statefile.StatesMarshalEqual(state, loadedState) { + t.Fatalf("loaded state does not match original state") + } +} + +func TestLoad(t *testing.T) { + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_instance", + Name: "baz", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"foo","foo":"value","bar":"value"}`), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")}, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + statePath := TestStateFile(t, state) + + s := testServer(t, statePath) + backendStatePath := testBackendStateFile(t, cty.ObjectVal(map[string]cty.Value{ + "organization": cty.StringVal("hashicorp"), + "hostname": cty.StringVal("localhost"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.NullVal(cty.String), + "prefix": cty.StringVal("my-app-"), + }), + })) + dir := strings.TrimSuffix(backendStatePath, ".terraform/.terraform.tfstate") + defer s.Close() + loader := Loader{Discovery: testDisco(s)} + t.Setenv(WorkspaceNameEnvVar, "test") + loadedState, diags := loader.LoadState(dir) + if diags.HasErrors() { + t.Fatalf("failed to load state: %s", diags.Err()) + } + + if !statefile.StatesMarshalEqual(state, loadedState) { + t.Fatalf("loaded state does not match original state") + } +} + +func mustResourceAddr(s string) addrs.ConfigResource { + addr, diags := addrs.ParseAbsResourceStr(s) + if diags.HasErrors() { + panic(diags.Err()) + } + return addr.Config() +} + +func testBackendStateFile(t *testing.T, value cty.Value) string { + t.Helper() + + path := filepath.Join(t.TempDir(), ".terraform", ".terraform.tfstate") + + err := os.MkdirAll(filepath.Dir(path), 0755) + if err != nil { + t.Fatalf("failed to create directories for temporary state file %s: %s", path, err) + } + + f, err := os.Create(path) + if err != nil { + t.Fatalf("failed to create temporary state file %s: %s", path, err) + } + + fmt.Fprintf(f, `{ + "version": 3, + "terraform_version": "1.9.4", + "backend": { + "type": "remote", + "config": { + "hostname": %q, + "organization": %q, + "token": "foo", + "workspaces": { + "name": null, + "prefix": %q + } + }, + "hash": 2143736989 + } + }`, value.GetAttr("hostname").AsString(), + value.GetAttr("organization").AsString(), + value.GetAttr("workspaces").GetAttr("prefix").AsString()) + + f.Close() + return path +} + +func createTempFile(t *testing.T, dir, filename, content string) string { + t.Helper() + filePath := filepath.Join(dir, filename) + err := os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + return filePath +} + +// testDisco returns a *disco.Disco mapping app.terraform.io and +// localhost to a local test server. +func testDisco(s *httptest.Server) *disco.Disco { + services := map[string]interface{}{ + "state.v2": fmt.Sprintf("%s/api/v2/", s.URL), + "tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL), + "versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL), + } + d := disco.NewWithCredentialsSource(auth.NoCredentials) + d.SetUserAgent(httpclient.TerraformUserAgent(version.String())) + + d.ForceHostServices(svchost.Hostname("localhost"), services) + d.ForceHostServices(svchost.Hostname("app.terraform.io"), services) + return d +} + +// testServer returns a *httptest.Server used for local testing. +// This server simulates the APIs needed to load a remote state. +func testServer(t *testing.T, statePath string) *httptest.Server { + mux := http.NewServeMux() + + f, err := os.Open(statePath) + if err != nil { + t.Fatalf("failed to open state file: %s", err) + } + + // Respond to service discovery calls. + mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, `{ + "state.v2": "/api/v2/", + "tfe.v2.1": "/api/v2/", + "versions.v1": "/v1/versions/" + }`) + }) + + // Respond to service version constraints calls. + mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + io.WriteString(w, fmt.Sprintf(`{ + "service": "%s", + "product": "terraform", + "minimum": "0.1.0", + "maximum": "10.0.0" +}`, path.Base(r.URL.Path))) + }) + + // Respond to pings to get the API version header. + mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("TFP-API-Version", "2.4") + }) + + // Respond to the initial query to read the hashicorp org entitlements. + mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.api+json") + io.WriteString(w, `{ + "data": { + "id": "org-GExadygjSbKP8hsY", + "type": "entitlement-sets", + "attributes": { + "operations": true, + "private-module-registry": true, + "sentinel": true, + "state-storage": true, + "teams": true, + "vcs-integrations": true + } + } +}`) + }) + + // Respond to the initial query to read the no-operations org entitlements. + mux.HandleFunc("/api/v2/organizations/no-operations/entitlement-set", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.api+json") + io.WriteString(w, `{ + "data": { + "id": "org-ufxa3y8jSbKP8hsT", + "type": "entitlement-sets", + "attributes": { + "operations": false, + "private-module-registry": true, + "sentinel": true, + "state-storage": true, + "teams": true, + "vcs-integrations": true + } + } +}`) + }) + + mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/my-app-test", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + io.WriteString(w, `{ + "data": { + "id": "ws-EUht4zmoJaZTZMv8", + "type": "workspaces", + "attributes": { + "locked": false, + "name": "my-app-test", + "queue-all-runs": false, + "speculative-enabled": true, + "structured-run-output-enabled": true, + "terraform-version": "1.9.4", + "operations": true, + "execution-mode": "remote", + "file-triggers-enabled": true, + "locked-reason": "", + "source": "terraform" + } + } +}`) + }) + + mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/actions/lock", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + io.WriteString(w, `{ + "data": { + "id": "ws-EUht4zmoJaZTZMv8", + "type": "workspaces", + "attributes": { + "locked": true, + "name": "my-app-test", + "queue-all-runs": false, + "speculative-enabled": true, + "structured-run-output-enabled": true, + "terraform-version": "1.9.4", + "source": "terraform", + "source-name": null, + "source-url": null, + "tag-names": [] + } + } +}`) + }) + + mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/current-state-version", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + io.WriteString(w, ` + { + "data": { + "id": "sv-XJmHFY12zJFmwkWN", + "type": "state-versions", + "attributes": { + "created-at": "2025-02-12T14:16:43.541Z", + "size": 878, + "hosted-state-download-url": "/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_state", + "hosted-json-state-download-url": "/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_json_state", + "serial": 1, + "state-version": 4, + "status": "finalized", + "terraform-version": "1.9.4" + } + } + } + `) + }) + + mux.HandleFunc("/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_state", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + io.Copy(w, f) + }) + + mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/actions/unlock", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(200) + io.WriteString(w, `{ + "data": { + "id": "ws-EUht4zmoJaZTZMv8", + "type": "workspaces", + "attributes": { + "locked": false, + "name": "my-app-test", + "queue-all-runs": false, + "speculative-enabled": true, + "structured-run-output-enabled": true, + "terraform-version": "1.9.4", + "source": "terraform", + "source-name": null, + "source-url": null, + "tag-names": [] + } + } +}`) + }) + + return httptest.NewServer(mux) +} diff --git a/internal/stacks/stackmigrate/meta.go b/internal/stacks/stackmigrate/meta.go new file mode 100644 index 0000000000..f171b57142 --- /dev/null +++ b/internal/stacks/stackmigrate/meta.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "net/url" + "os" + "path/filepath" + + "github.com/hashicorp/terraform/internal/backend" + "github.com/hashicorp/terraform/internal/backend/local" + "github.com/hashicorp/terraform/internal/command/workdir" +) + +type Meta struct { + // WorkingDir is an object representing the "working directory" where we're + // running commands. In the normal case this literally refers to the + // working directory of the Terraform process, though this can take on + // a more symbolic meaning when the user has overridden default behavior + // to specify a different working directory or to override the special + // data directory where we'll persist settings that must survive between + // consecutive commands. + WorkingDir *workdir.Dir +} + +var errInvalidWorkspaceNameEnvVar = fmt.Errorf("Invalid workspace name set using %s", WorkspaceNameEnvVar) + +// Workspace returns the name of the currently configured workspace, corresponding +// to the desired named state. +func (m *Meta) Workspace() (string, error) { + current, overridden := m.WorkspaceOverridden() + if overridden && !validWorkspaceName(current) { + return "", errInvalidWorkspaceNameEnvVar + } + return current, nil +} + +// WorkspaceOverridden returns the name of the currently configured workspace, +// corresponding to the desired named state, as well as a bool saying whether +// this was set via the TF_WORKSPACE environment variable. +func (m *Meta) WorkspaceOverridden() (string, bool) { + if envVar := os.Getenv(WorkspaceNameEnvVar); envVar != "" { + return envVar, true + } + + envData, err := ioutil.ReadFile(filepath.Join(m.DataDir(), local.DefaultWorkspaceFile)) + current := string(bytes.TrimSpace(envData)) + if current == "" { + current = backend.DefaultStateName + } + + if err != nil && !os.IsNotExist(err) { + // always return the default if we can't get a workspace name + log.Printf("[ERROR] failed to read current workspace: %s", err) + } + + return current, false +} + +// fixupMissingWorkingDir is a compensation for various existing tests which +// directly construct incomplete "Meta" objects. Specifically, it deals with +// a test that omits a WorkingDir value by constructing one just-in-time. +// +// We shouldn't ever rely on this in any real codepath, because it doesn't +// take into account the various ways users can override our default +// directory selection behaviors. +func (m *Meta) fixupMissingWorkingDir() { + if m.WorkingDir == nil { + log.Printf("[WARN] This 'Meta' object is missing its WorkingDir, so we're creating a default one suitable only for tests") + m.WorkingDir = workdir.NewDir(".") + } +} + +// DataDir returns the directory where local data will be stored. +// Defaults to DefaultDataDir in the current working directory. +func (m *Meta) DataDir() string { + m.fixupMissingWorkingDir() + return m.WorkingDir.DataDir() +} + +// validWorkspaceName returns true is this name is valid to use as a workspace name. +// Since most named states are accessed via a filesystem path or URL, check if +// escaping the name would be required. +func validWorkspaceName(name string) bool { + return name == url.PathEscape(name) +} diff --git a/internal/stacks/stackmigrate/meta_test.go b/internal/stacks/stackmigrate/meta_test.go new file mode 100644 index 0000000000..08d8121ade --- /dev/null +++ b/internal/stacks/stackmigrate/meta_test.go @@ -0,0 +1,108 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/internal/backend/local" +) + +// DefaultDataDir is the default directory for storing local data. +const DefaultDataDir = ".terraform" + +func TestMeta_Workspace_override(t *testing.T) { + defer func(value string) { + os.Setenv(WorkspaceNameEnvVar, value) + }(os.Getenv(WorkspaceNameEnvVar)) + + m := new(Meta) + + testCases := map[string]struct { + workspace string + err error + }{ + "": { + "default", + nil, + }, + "development": { + "development", + nil, + }, + "invalid name": { + "", + errInvalidWorkspaceNameEnvVar, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + os.Setenv(WorkspaceNameEnvVar, name) + workspace, err := m.Workspace() + if workspace != tc.workspace { + t.Errorf("Unexpected workspace\n got: %s\nwant: %s\n", workspace, tc.workspace) + } + if err != tc.err { + t.Errorf("Unexpected error\n got: %s\nwant: %s\n", err, tc.err) + } + }) + } +} + +// If somehow an invalid workspace has been selected, the Meta.Workspace +// method should not return an error, to ensure that we don't break any +// existing workflows with invalid workspace names. +func TestMeta_Workspace_invalidSelected(t *testing.T) { + td := t.TempDir() + defer testChdir(t, td)() + + // this is an invalid workspace name + workspace := "test workspace" + + // create the workspace directories + if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, workspace), 0755); err != nil { + t.Fatal(err) + } + + // create the workspace file to select it + if err := os.MkdirAll(DefaultDataDir, 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte(workspace), 0644); err != nil { + t.Fatal(err) + } + + m := new(Meta) + + ws, err := m.Workspace() + if ws != workspace { + t.Errorf("Unexpected workspace\n got: %s\nwant: %s\n", ws, workspace) + } + if err != nil { + t.Errorf("Unexpected error: %s", err) + } +} + +// testChdir changes the directory and returns a function to defer to +// revert the old cwd. +func testChdir(t *testing.T, new string) func() { + t.Helper() + + old, err := os.Getwd() + if err != nil { + t.Fatalf("err: %s", err) + } + + if err := os.Chdir(new); err != nil { + t.Fatalf("err: %v", err) + } + + return func() { + // Re-run the function ignoring the defer result + testChdir(t, old) + } +} diff --git a/internal/stacks/stackmigrate/migrate.go b/internal/stacks/stackmigrate/migrate.go new file mode 100644 index 0000000000..8218f4e011 --- /dev/null +++ b/internal/stacks/stackmigrate/migrate.go @@ -0,0 +1,139 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "fmt" + "iter" + + "github.com/hashicorp/go-slug/sourceaddrs" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Migration is a struct that aids in migrating a terraform state to a stack configuration. +type Migration struct { + // Providers is a map of provider addresses available to the stack. + Providers map[addrs.Provider]providers.Factory + + // PreviousState is the terraform core state that we are migrating from. + PreviousState *states.State + Config *stackconfig.Config +} + +// Alias common types to make the code more readable. +type ( + // ConfigComponent is the definition of a component in a stack configuration, + // and therefore is unique for all instances of a component in a stack. + Config = stackaddrs.ConfigComponent + + // Every instance of a component in a stack instance has a unique address. + Instance = stackaddrs.AbsComponentInstance + + // Every instance of a component in a stack has the same AbsComponent address. + AbsComponent = stackaddrs.AbsComponent +) + +func (m *Migration) Migrate(resources map[string]string, modules map[string]string, emit func(change stackstate.AppliedChange), emitDiag func(diagnostic tfdiags.Diagnostic)) { + + migration := &migration{ + Migration: m, + emit: emit, + emitDiag: emitDiag, + providers: make(map[addrs.Provider]providers.Interface), + parser: configs.NewSourceBundleParser(m.Config.Sources), + configs: make(map[sourceaddrs.FinalSource]*configs.Config), + } + + defer migration.close() // cleanup any opened providers. + + components := migration.migrateResources(resources, modules) + migration.migrateComponents(components) + + // Everything is migrated! +} + +type migration struct { + *Migration + + emit func(change stackstate.AppliedChange) + emitDiag func(diagnostic tfdiags.Diagnostic) + + providers map[addrs.Provider]providers.Interface + parser *configs.SourceBundleParser + configs map[sourceaddrs.FinalSource]*configs.Config +} + +func (m *migration) stateResources() iter.Seq2[addrs.AbsResource, *states.Resource] { + return func(yield func(addrs.AbsResource, *states.Resource) bool) { + for _, module := range m.PreviousState.Modules { + for _, resource := range module.Resources { + if !yield(resource.Addr, resource) { + return + } + } + } + } +} + +// moduleConfig returns the module configuration for the component. If the configuration +// has already been loaded, it will be returned from the cache. +func (m *migration) moduleConfig(component *stackconfig.Component) (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if component.FinalSourceAddr == nil { + // if there is no final source address, then the configuration was likely + // loaded via a shallow load, but we need the full configuration. + panic("component has no final source address") + } + if cfg, ok := m.configs[component.FinalSourceAddr]; ok { + return cfg, diags + } + moduleConfig, diags := component.ModuleConfig(m.parser.Bundle()) + if diags.HasErrors() { + return nil, diags + } + m.configs[component.FinalSourceAddr] = moduleConfig + return moduleConfig, diags +} + +func (m *migration) emitDiags(diags tfdiags.Diagnostics) { + for _, diag := range diags { + m.emitDiag(diag) + } +} + +func (m *migration) provider(provider addrs.Provider) (providers.Interface, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if p, ok := m.providers[provider]; ok { + return p, diags + } + + factory, ok := m.Migration.Providers[provider] + if !ok { + return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Provider %s not found in required_providers.", provider.ForDisplay()))} + } + + p, err := factory() + if err != nil { + return nil, tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "Provider initialization failed", fmt.Sprintf("Failed to initialize provider %s: %s", provider.ForDisplay(), err.Error()))} + } + + m.providers[provider] = p + return p, diags +} + +func (m *migration) close() { + for addr, provider := range m.providers { + if err := provider.Close(); err != nil { + m.emitDiag(tfdiags.Sourceless(tfdiags.Error, "Provider cleanup failed", fmt.Sprintf("Failed to close provider %s: %s", addr.ForDisplay(), err.Error()))) + } + } +} diff --git a/internal/stacks/stackmigrate/migrate_test.go b/internal/stacks/stackmigrate/migrate_test.go new file mode 100644 index 0000000000..88a968835b --- /dev/null +++ b/internal/stacks/stackmigrate/migrate_test.go @@ -0,0 +1,1911 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + stdcmp "cmp" + "encoding/json" + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestMigrate_Module(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceDeposed( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + states.NewDeposedKey(), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + resources := map[string]string{ + "testing_resource.data": "self", + } + modules := map[string]string{} + + applied := []stackstate.AppliedChange{} + expected := []stackstate.AppliedChange{ + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: mustAbsResourceInstanceObject("component.self.testing_resource.data").Component, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: mustAbsResourceInstanceObject("component.self.testing_resource.data").Item.ResourceInstance, + DeposedKey: states.NewDeposedKey(), + }, + }, + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: map[addrs.OutputValue]cty.Value{}, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + applied = append(applied, change) + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := cmp.Diff(expected, applied, changesCmpOpts, cmpopts.IgnoreFields( + addrs.AbsResourceInstanceObject{}, "DeposedKey", + )); diff != "" { + t.Fatalf("unexpected applied changes:\n%s", diff) + } + + if diff := cmp.Diff(expDiags, gotDiags); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } +} + +func TestMigrate_RootResources(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceDeposed( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + states.NewDeposedKey(), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + resources := map[string]string{ + "testing_resource.data": "self", + } + modules := map[string]string{} + + applied := []stackstate.AppliedChange{} + expected := []stackstate.AppliedChange{ + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: mustAbsResourceInstanceObject("component.self.testing_resource.data").Component, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: mustAbsResourceInstanceObject("component.self.testing_resource.data").Item.ResourceInstance, + DeposedKey: states.NewDeposedKey(), + }, + }, + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: map[addrs.OutputValue]cty.Value{}, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + applied = append(applied, change) + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := cmp.Diff(expected, applied, changesCmpOpts, cmpopts.IgnoreFields( + addrs.AbsResourceInstanceObject{}, "DeposedKey", + )); diff != "" { + t.Fatalf("unexpected applied changes:\n%s", diff) + } + + if diff := cmp.Diff(expDiags, gotDiags); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } +} + +func TestMigrate_ComponentDependency(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-dependency", "input-dependency")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(0)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(1)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + resources := map[string]string{ + "testing_resource.data": "parent", + "testing_resource.another": "child", + } + modules := map[string]string{} + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another[0]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another[1]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + { + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependents: collections.NewSet(mustAbsComponent("component.child")), + }, + { + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependencies: collections.NewSet(mustAbsComponent("component.parent")), + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Fatalf("unexpected applied resource changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Fatalf("unexpected applied component changes:\n%s", diff) + } + + if diff := cmp.Diff(expDiags, gotDiags); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } +} + +func TestMigrateConfig_NestedModuleResources(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-nested-module")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(0)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(1)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + children := []*states.Module{ + state.EnsureModule(addrs.RootModuleInstance.Child("child_mod", addrs.NoKey)), + state.EnsureModule(addrs.RootModuleInstance.Child("child_mod2", addrs.NoKey)), + } + for _, childModule := range children { + childModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "child_data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + childModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another_child_data", + }.Instance(addrs.IntKey(0)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + childModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another_child_data", + }.Instance(addrs.IntKey(1)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + } + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + resources := map[string]string{ + "testing_resource.data": "parent", + "testing_resource.another": "parent", + } + modules := map[string]string{ + "child_mod": "child", + "child_mod2": "child2", + } + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.child_data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another_child_data[0]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another_child_data[1]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child2.testing_resource.child_data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child2.testing_resource.another_child_data[0]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child2.testing_resource.another_child_data[1]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + { + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependents: collections.NewSet(mustAbsComponent("component.child"), mustAbsComponent("component.child2")), + }, + { + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependencies: collections.NewSet(mustAbsComponent("component.parent")), + }, + { + ComponentAddr: mustAbsComponent("component.child2"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child2"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependencies: collections.NewSet(mustAbsComponent("component.parent")), + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := cmp.Diff(expDiags, gotDiags); diff != "" { + t.Errorf("unexpected diagnostics:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Errorf("unexpected applied resource changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Errorf("unexpected applied component changes:\n%s", diff) + } +} + +func TestMigrateConfig_MissingConfigResource(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-nested-module")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(0)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(1)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "for_child", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + resources := map[string]string{ + "testing_resource.data": "parent", + "testing_resource.another": "parent", + "testing_resource.for_child": "child", + } + modules := map[string]string{} + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + { + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependents: collections.NewSet(mustAbsComponent("component.child")), + }, + { + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + Dependencies: collections.NewSet(mustAbsComponent("component.parent")), + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + // all components and resources should be migrated except for the missing "testing_resource.for_child" + expDiags = expDiags.Append(hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Provider not found", + Detail: "Resource \"testing_resource.for_child\" not found in root module.", + }, + }) + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := cmp.Diff(expDiags.ForRPC(), gotDiags.ForRPC(), tfdiags.DiagnosticComparer); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Errorf("unexpected applied resource changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Errorf("unexpected applied component changes:\n%s", diff) + } +} + +func TestMigrateConfig_MissingMappingForStateResource(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-nested-module")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(0)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "another", + }.Instance(addrs.IntKey(1)), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "for_child", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "hello" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + resources := map[string]string{ + "testing_resource.data": "parent", + "testing_resource.another": "parent", + } + modules := map[string]string{} + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + // this component has a dependent "child", but that other component + // is not present in the modules mapping, so it is not included here + { + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "id"}: cty.DynamicVal, + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "id"}: cty.DynamicVal, + {Name: "input"}: cty.DynamicVal, + }, + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + expDiags = expDiags.Append(hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Resource not found", + Detail: "Resource \"testing_resource.for_child\" not found in mapping.", + }, + }) + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := cmp.Diff(expDiags.ForRPC(), gotDiags.ForRPC(), tfdiags.DiagnosticComparer); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Errorf("unexpected applied resource changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Errorf("unexpected applied component changes:\n%s", diff) + } +} + +func TestMigrateConfigDependsOn(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-depends-on")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "depends_test" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "second", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "depends_test" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "third", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "depends_test" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + + resources := map[string]string{ + "testing_resource.data": "component.first", + "testing_resource.second": "component.second", + "testing_resource.third": "component.second", + } + modules := map[string]string{} + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + // component.first depends on component.second + { + ComponentAddr: mustAbsComponent("component.first"), + ComponentInstanceAddr: mustAbsComponentInstance("component.first"), + + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "input"}: cty.DynamicVal, + {Name: "id"}: cty.DynamicVal, + }, + Dependents: collections.NewSet(mustAbsComponent("component.second")), + }, + { + ComponentAddr: mustAbsComponent("component.second"), + ComponentInstanceAddr: mustAbsComponentInstance("component.second"), + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "input"}: cty.DynamicVal, + {Name: "id"}: cty.DynamicVal, + }, + Dependencies: collections.NewSet(mustAbsComponent("component.first")), + }, + } + + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "depends_test", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.second"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "depends_test", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.third"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "depends_test", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Fatalf("unexpected applied component changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Fatalf("unexpected applied resource changes:\n%s", diff) + } + + if diff := cmp.Diff(expDiags, gotDiags); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } +} + +func TestMigrate_UnsupportedComponentRef(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "with-depends-on")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + state := states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{Name: "output"}, + }, cty.StringVal("before"), false) + }) + + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "depends_test" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "second", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "depends_test" + }`), + }, + mustDefaultRootProvider("testing"), + ) + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "third", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "foo", + "value": "depends_test" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + + resources := map[string]string{ + "testing_resource.data": "component.first", + "testing_resource.second": "component.second", + "testing_resource.third": "stack.embedded.component.self", + } + modules := map[string]string{} + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + { + ComponentAddr: mustAbsComponent("component.first"), + ComponentInstanceAddr: mustAbsComponentInstance("component.first"), + + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "input"}: cty.DynamicVal, + {Name: "id"}: cty.DynamicVal, + }, + Dependents: collections.NewSet(mustAbsComponent("component.second")), + }, + { + ComponentAddr: mustAbsComponent("component.second"), + ComponentInstanceAddr: mustAbsComponentInstance("component.second"), + InputVariables: map[addrs.InputVariable]cty.Value{ + {Name: "input"}: cty.DynamicVal, + {Name: "id"}: cty.DynamicVal, + }, + Dependencies: collections.NewSet(mustAbsComponent("component.first")), + }, + } + + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "depends_test", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.second"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "depends_test", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + + var gotDiags tfdiags.Diagnostics + expDiags := tfdiags.Diagnostics{}.Append(hcl.Diagnostics{ + { + Severity: hcl.DiagError, + Summary: "Invalid component instance", + Detail: "Only root component instances are allowed, got \"stack.embedded.component.self\"", + }, + }) + + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Fatalf("unexpected applied component changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Fatalf("unexpected applied resource changes:\n%s", diff) + } + + if diff := cmp.Diff(expDiags.ForRPC(), gotDiags.ForRPC(), tfdiags.DiagnosticComparer); diff != "" { + t.Fatalf("unexpected diagnostics:\n%s", diff) + } +} + +func compareAppliedChanges[A stackstate.AppliedChange, U stdcmp.Ordered](t *testing.T, expected, actual []A, cb func(A) U) string { + t.Helper() + + if len(expected) != len(actual) { + t.Fatalf("expected %d changes, got %d", len(expected), len(actual)) + } + + _exp := make([]U, len(expected)) + _act := make([]U, len(actual)) + mp_exp := make(map[U]A) + mp_act := make(map[U]A) + + for i, exp := range expected { + _exp[i] = cb(exp) + _act[i] = cb(actual[i]) + mp_exp[_exp[i]] = exp + mp_act[_act[i]] = actual[i] + } + + sorter := cmpopts.SortMaps(func(a, b U) bool { + return a < b + }) + + return cmp.Diff(mp_exp, mp_act, sorter, changesCmpOpts, cmpopts.EquateEmpty()) +} + +func cmpJSONMap() cmp.Option { + return cmp.FilterValues(func(x, y interface{}) bool { + _, okX := x.([]uint8) + _, okY := y.([]uint8) + return okX && okY + }, cmp.Comparer(func(x, y interface{}) bool { + var xJSON, yJSON map[string]interface{} + err := json.Unmarshal(x.([]uint8), &xJSON) + if err != nil { + return false + } + err = json.Unmarshal(y.([]uint8), &yJSON) + if err != nil { + return false + } + + return cmp.Equal(xJSON, yJSON) + })) +} + +var changesCmpOpts = cmp.Options{ + ctydebug.CmpOptions, + cmpCollectionsSet, + cmpopts.IgnoreUnexported(addrs.InputVariable{}), + cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{}), + cmpJSONMap(), + cmpopts.IgnoreFields(states.ResourceInstanceObjectSrc{}, "Private"), + cmpopts.IgnoreFields(states.ResourceInstanceObjectSrc{}, "IdentityJSON"), +} + +var cmpCollectionsSet = cmp.Comparer(func(x, y collections.Set[stackaddrs.AbsComponent]) bool { + if x.Len() != y.Len() { + return false + } + + for v := range x.All() { + if !y.Has(v) { + return false + } + } + + return true +}) + +func TestMigrateConfigWithNoChanges(t *testing.T) { + +} + +// collectPlanOutput consumes the two output channels emitting results from +// a call to [Plan], and collects all of the data written to them before +// returning once changesCh has been closed by the sender to indicate that +// the planning process is complete. +func collectPlanOutput(changesCh <-chan stackplan.PlannedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var changes []stackplan.PlannedChange + var diags tfdiags.Diagnostics + + for { + select { + case change, ok := <-changesCh: + if !ok { + // The plan operation is complete but we might still have + // some buffered diagnostics to consume. + if diagsCh != nil { + for diag := range diagsCh { + diags = append(diags, diag) + } + } + return changes, diags + } + changes = append(changes, change) + case diag, ok := <-diagsCh: + if !ok { + // no more diagnostics to read + diagsCh = nil + continue + } + diags = append(diags, diag) + } + } +} + +func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte { + jsonAttrs, err := json.Marshal(attrs) + if err != nil { + panic(err) + } + return jsonAttrs +} + +func mustDefaultRootProvider(provider string) addrs.AbsProviderConfig { + return addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider(provider), + } +} + +func mustAbsResourceInstance(addr string) addrs.AbsResourceInstance { + ret, diags := addrs.ParseAbsResourceInstanceStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse resource instance address %q: %s", addr, diags)) + } + return ret +} + +func mustAbsResourceInstanceObject(addr string) stackaddrs.AbsResourceInstanceObject { + ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse resource instance object address %q: %s", addr, diags)) + } + return ret +} + +func mustAbsResourceInstanceObjectPtr(addr string) *stackaddrs.AbsResourceInstanceObject { + ret := mustAbsResourceInstanceObject(addr) + return &ret +} + +func mustAbsComponentInstance(addr string) stackaddrs.AbsComponentInstance { + ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) + } + return ret +} + +func mustAbsComponent(addr string) stackaddrs.AbsComponent { + ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) + } + return stackaddrs.AbsComponent{ + Stack: ret.Stack, + Item: ret.Item.Component, + } +} + +// TODO: Perhaps export this from helper_test instead +func loadMainBundleConfigForTest(t *testing.T, dirName string) *stackconfig.Config { + t.Helper() + fullSourceAddr := mainBundleSourceAddrStr(dirName) + return loadConfigForTest(t, "../stackruntime/testdata/mainbundle", fullSourceAddr) +} + +func mainBundleSourceAddrStr(dirName string) string { + return "git::https://example.com/test.git//" + dirName +} + +// loadConfigForTest is a test helper that tries to open bundleRoot as a +// source bundle, and then if successful tries to load the given source address +// from it as a stack configuration. If any part of the operation fails then +// it halts execution of the test and doesn't return. +func loadConfigForTest(t *testing.T, bundleRoot string, configSourceAddr string) *stackconfig.Config { + t.Helper() + sources, err := sourcebundle.OpenDir(bundleRoot) + if err != nil { + t.Fatalf("cannot load source bundle: %s", err) + } + + // We force using remote source addresses here because that avoids + // us having to deal with the extra version constraints argument + // that registry sources require. Exactly what source address type + // we use isn't relevant for tests in this package, since it's + // the sourcebundle package's responsibility to make sure its + // abstraction works for all of the source types. + sourceAddr, err := sourceaddrs.ParseRemoteSource(configSourceAddr) + if err != nil { + t.Fatalf("invalid config source address: %s", err) + } + + cfg, diags := stackconfig.LoadConfigDir(sourceAddr, sources) + reportDiagnosticsForTest(t, diags) + return cfg +} + +// reportDiagnosticsForTest creates a test log entry for every diagnostic in +// the given diags, and halts the test if any of them are error diagnostics. +func reportDiagnosticsForTest(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + for _, diag := range diags { + var b strings.Builder + desc := diag.Description() + locs := diag.Source() + + switch sev := diag.Severity(); sev { + case tfdiags.Error: + b.WriteString("Error: ") + case tfdiags.Warning: + b.WriteString("Warning: ") + default: + t.Errorf("unsupported diagnostic type %s", sev) + } + b.WriteString(desc.Summary) + if desc.Address != "" { + b.WriteString("\nwith ") + b.WriteString(desc.Summary) + } + if locs.Subject != nil { + b.WriteString("\nat ") + b.WriteString(locs.Subject.StartString()) + } + if desc.Detail != "" { + b.WriteString("\n\n") + b.WriteString(desc.Detail) + } + t.Log(b.String()) + } + if diags.HasErrors() { + t.FailNow() + } +} + +func TestMigrateConfig_ChildModuleAsComponentSource(t *testing.T) { + cfg := loadMainBundleConfigForTest(t, filepath.Join("for-stacks-migrate", "child-module-as-component-source")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + state := states.BuildState(func(ss *states.SyncState) {}) + rootModule := state.RootModule() + rootModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "root_id", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "root_id", + "value": "root_output" + }`), + }, + mustDefaultRootProvider("testing"), + ) + + childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child_module", addrs.NoKey)) + childProv := mustDefaultRootProvider("testing") + childProv.Module = childModule.Addr.Module() + childModule.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "child_data", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{ + "id": "child_data", + "value": "child_output" + }`), + }, + childProv, + ) + + mig := Migration{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + PreviousState: state, + Config: cfg, + } + + resources := map[string]string{ + "testing_resource.root_id": "self", + "testing_resource.child_data": "self", + } + modules := map[string]string{ + "child_module": "triage", + } + + appliedResources := []*stackstate.AppliedChangeResourceInstanceObject{} + appliedComponents := []*stackstate.AppliedChangeComponentInstance{} + expectedResources := []*stackstate.AppliedChangeResourceInstanceObject{ + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.root_id"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "root_id", + "value": "root_output", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + { + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.triage.testing_resource.child_data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "child_data", + "value": "child_output", + }), + Status: states.ObjectReady, + Private: nil, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + expectedComponents := []*stackstate.AppliedChangeComponentInstance{ + { + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: map[addrs.OutputValue]cty.Value{}, + InputVariables: map[addrs.InputVariable]cty.Value{}, + }, + { + ComponentAddr: mustAbsComponent("component.triage"), + ComponentInstanceAddr: mustAbsComponentInstance("component.triage"), + OutputValues: map[addrs.OutputValue]cty.Value{}, + InputVariables: map[addrs.InputVariable]cty.Value{ + addrs.InputVariable{Name: "input"}: cty.DynamicVal, + }, + }, + } + + var expDiags, gotDiags tfdiags.Diagnostics + mig.Migrate(resources, modules, func(change stackstate.AppliedChange) { + switch c := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + appliedResources = append(appliedResources, c) + case *stackstate.AppliedChangeComponentInstance: + appliedComponents = append(appliedComponents, c) + } + }, func(diagnostic tfdiags.Diagnostic) { + gotDiags = append(gotDiags, diagnostic) + }) + + if diff := compareAppliedChanges(t, expectedResources, appliedResources, func(c *stackstate.AppliedChangeResourceInstanceObject) string { + return c.ResourceInstanceObjectAddr.String() + }); diff != "" { + t.Errorf("unexpected applied resource changes:\n%s", diff) + } + + if diff := compareAppliedChanges(t, expectedComponents, appliedComponents, func(c *stackstate.AppliedChangeComponentInstance) string { + return c.ComponentAddr.String() + }); diff != "" { + t.Errorf("unexpected applied component changes:\n%s", diff) + } + + if diff := cmp.Diff(expDiags, gotDiags); diff != "" { + t.Errorf("unexpected diagnostics:\n%s", diff) + } +} diff --git a/internal/stacks/stackmigrate/resources.go b/internal/stacks/stackmigrate/resources.go new file mode 100644 index 0000000000..18a8e01712 --- /dev/null +++ b/internal/stacks/stackmigrate/resources.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// stackResource represents a resource that was found in the terraform state. +// It contains the stack and component configuration for the resource. +type stackResource struct { + // The unexpanded resource address + AbsResource stackaddrs.AbsResource + + // The stack and component configuration for the resource. + StackConfig *stackconfig.Stack + ComponentConfig *stackconfig.Component + + // The source module configuration for the stack component. + StackModuleConfig *configs.Config +} + +// implement the UniqueKeyer interface for stackResource +// The key of a stackResource pointer is simply itself. +func (r *stackResource) UniqueKey() collections.UniqueKey[*stackResource] { + return r +} + +// implement the UniqueKey interface for stackResource +func (r *stackResource) IsUniqueKey(*stackResource) {} + +func (m *migration) migrateResources(resources map[string]string, modules map[string]string) collections.Map[Instance, collections.Set[*stackResource]] { + components := collections.NewMap[Instance, collections.Set[*stackResource]]() + + // for each resource in the config, we track the instances that belong to the + // same component. + trackComponent := func(resource *stackResource) { + instance := resource.AbsResource.Component + if !components.HasKey(instance) { + components.Put(instance, collections.NewSet[*stackResource]()) + } + components.Get(instance).Add(resource) + } + + for _, resource := range m.stateResources() { + // check if the state resource has been requested for migration, + // either by being in the resources map, or its module being in the modules map. + // The returned target builds a new address for the resource within the + // stack component where it will be migrated to. + target, diags := m.search(resource.Addr, resources, modules) + if diags.HasErrors() { + // if there are errors, we can't migrate this resource. + m.emitDiags(diags) + continue + } + + // We have the component address, now load the stack and component configuration + // for the resource. + // If this is successful, we can now start adding source information + // to diagnostics. + diags = m.loadConfig(target) + if diags.HasErrors() { + m.emitDiags(diags) + continue + } + component := target.AbsResource.Component + componentAddr := target.AbsResource.Item + + trackComponent(target) + + // retrieve the provider that was uses to create the resource instance. + providerAddr, provider, diags := m.getOwningProvider(target) + if diags.HasErrors() { + m.emitDiags(diags) + continue + } + + schema := provider.GetProviderSchema().SchemaForResourceType(resource.Addr.Resource.Mode, resource.Addr.Resource.Type) + if schema.Body == nil { + m.emitDiags(diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Resource type not found", + Detail: fmt.Sprintf("Resource type %s not found in provider schema.", resource.Addr.Resource.Type), + Subject: target.StackModuleConfig.SourceAddrRange.Ptr(), + })) + continue + } + + for instanceKey, instance := range resource.Instances { + instanceAddr := stackaddrs.AbsResourceInstance{ + Component: component, + Item: componentAddr.Instance(instanceKey), + } + + m.emit(&stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: instanceAddr.Component, + Item: instanceAddr.Item.DeposedObject(addrs.NotDeposed), + }, + NewStateSrc: instance.Current, + ProviderConfigAddr: providerAddr, + Schema: schema, + }) + + for deposedKey, deposed := range instance.Deposed { + m.emit(&stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: instanceAddr.Component, + Item: instanceAddr.Item.DeposedObject(deposedKey), + }, + NewStateSrc: deposed, + ProviderConfigAddr: providerAddr, + Schema: schema, + }) + } + } + } + return components +} + +// search searches for the state resource in the resource mappings and when found, converts and returns the relevant +// stackResource. +// +// If the resource or module is nested within the root module, they will be migrated to the component with the address structure retained. +// For example, a resource with the address module.my_module.module.child.aws_instance.foo will be migrated to +// component.my_component.module.child.aws_instance.foo if the corresponding map key is found. +// E.g module.child.aws_instance.foo will be replaced with component.child.aws_instance.foo +func (m *migration) search(resource addrs.AbsResource, resources map[string]string, modules map[string]string) (*stackResource, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret := &stackResource{} + + parseComponentInstance := func(target string) (Instance, tfdiags.Diagnostics) { + fullTarget := "component." + strings.TrimPrefix(target, "component.") + if len(strings.Split(fullTarget, ".")) > 2 { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid component instance", fmt.Sprintf("Only root component instances are allowed, got %q", target))) + return Instance{}, diags + } + inst, _, diags := stackaddrs.ParseAbsComponentInstanceStrOnly(fullTarget) + return inst, diags + } + + if resource.Module.IsRoot() { + target, ok := resources[resource.Resource.String()] + if !ok { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Resource not found", fmt.Sprintf("Resource %q not found in mapping.", resource.Resource.String()))) + return ret, diags + } + + inst, diags := parseComponentInstance(target) + if diags.HasErrors() { + return ret, diags + } + ret.AbsResource = stackaddrs.AbsResource{ + Component: inst, + Item: resource, + } + return ret, diags + } + + // The resource is in a child module, so we need to find the component. + // When found, we replace the module with the component instance, i.e + // a resource of module.child.aws_instance.foo will be replaced with + // component.child.aws_instance.foo + if targetComponent, ok := modules[resource.Module[0].Name]; ok { + inst, diags := parseComponentInstance(targetComponent) + if diags.HasErrors() { + return ret, diags + } + // retain the instance key + inst.Item.Key = resource.Module[0].InstanceKey + ret.AbsResource = stackaddrs.AbsResource{ + Component: inst, + Item: addrs.AbsResource{ + Module: resource.Module[1:], // the first module instance is replaced by the component instance + Resource: resource.Resource, + }, + } + return ret, diags + } else { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Module not found", fmt.Sprintf("Module %q not found in mapping.", resource.Module[0].Name))) + return ret, diags + } +} + +// getOwningProvider returns the address of the provider configuration, +// as well as the provider instance, that was used to create the given resource instance. +func (m *migration) getOwningProvider(resource *stackResource) (addrs.AbsProviderConfig, providers.Interface, tfdiags.Diagnostics) { + var ret addrs.AbsProviderConfig + // At this point, we already worked out the stack component where we are migrating + // the resource to. Now we need to look into the module configuration of the stack component, + // and ensure that it has a provider configuration that matches the one used to create + // the resource instance. + + moduleAddr := resource.AbsResource.Item.Module.Module() // the module address within the stack component's module configuration + providerConfig, diags := m.findProviderConfig(moduleAddr, resource.AbsResource.Item.Resource, resource.StackModuleConfig) + if diags.HasErrors() { + return ret, nil, diags + } + component := resource.ComponentConfig + stackCfg := resource.StackConfig + + // we found the provider configuration within the module configuration, + // now look it up in the stack configuration. + expr, ok := component.ProviderConfigs[providerConfig] + if !ok { + // Then the module uses a provider not referenced in the component. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider not found for component", + Detail: fmt.Sprintf("Provider %q not found in component %q.", providerConfig.LocalName, resource.AbsResource.Component.Item.Component.Name), + Subject: component.SourceAddrRange.ToHCL().Ptr(), + }) + return ret, nil, diags + } + + vars := expr.Variables() + if len(vars) != 1 { + // This should be an exact reference to a single provider, if it's not + // we can't really do anything. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider reference", + Detail: "Provider references should be a simple reference to a single provider.", + Subject: expr.Range().Ptr(), + }) + return ret, nil, diags + } + + ref, _, moreDiags := stackaddrs.ParseReference(vars[0]) + diags = diags.Append(moreDiags) + + switch ref := ref.Target.(type) { + case stackaddrs.ProviderConfigRef: + providerAddr, ok := stackCfg.RequiredProviders.ProviderForLocalName(ref.ProviderLocalName) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider not found for component", + Detail: fmt.Sprintf("Provider %s was needed by the resource %s but was not found in the stack configuration.", ref.ProviderLocalName, resource.AbsResource.Item.Resource.String()), + Subject: component.SourceAddrRange.ToHCL().Ptr(), + }) + return ret, nil, diags + } + + addr := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: providerAddr, + Alias: providerConfig.Alias, // we still use the alias from the module provider as this is referenced as if from within the module. + } + + provider, pDiags := m.provider(providerAddr) + // pull in source information for diagnostics if available. + for _, diag := range pDiags { + if diag.Source().Subject == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: diag.Severity().ToHCL(), + Summary: diag.Description().Summary, + Detail: diag.Description().Detail, + Subject: resource.ComponentConfig.SourceAddrRange.ToHCL().Ptr(), + }) + } + } + + return addr, provider, diags + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: "Non-provider reference found in provider configuration.", + Subject: expr.Range().Ptr(), + }) + return ret, nil, diags + } +} + +// findProviderConfig recursively searches through the stack module configuration to find the provider +// that was used to create the resource instance. +func (m *migration) findProviderConfig(module addrs.Module, resource addrs.Resource, config *configs.Config) (addrs.LocalProviderConfig, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if module.IsRoot() { + r := config.Module.ResourceByAddr(resource) + if r == nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Resource %q not found in root module.", resource.String()))) + return addrs.LocalProviderConfig{}, diags + } + + return r.ProviderConfigAddr(), diags + } + + next, ok := config.Children[module[0]] + if !ok { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Module %q not found in root module children.", module[0]))) + return addrs.LocalProviderConfig{}, diags + } + + // the address points to a nested module, so we continue the search + // within the next module's configuration. + provider, moreDiags := m.findProviderConfig(module[1:], resource, next) + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return addrs.LocalProviderConfig{}, diags + } + + call, ok := config.Module.ModuleCalls[module[0]] + if !ok { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Module call %q not found in configuration.", module[0]))) + return addrs.LocalProviderConfig{}, diags + } + + for _, p := range call.Providers { + if p.InChild.Name == provider.LocalName && p.InChild.Alias == provider.Alias { + return p.InParent.Addr(), diags + } + } + + // if we reach here, then the provider was not passed to the module call. + // Let's check the provider within the child module configuration. + r := next.Module.ResourceByAddr(resource) + if r == nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Provider not found", fmt.Sprintf("Resource %q not found in containing module.", resource.String()))) + return addrs.LocalProviderConfig{}, diags + } + return r.ProviderConfigAddr(), diags +} + +// loadConfig loads the module and component configuration from the stack directory. +func (m *migration) loadConfig(resource *stackResource) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + instance := resource.AbsResource.Component + stack := m.Config.Stack(instance.Stack.ConfigAddr()) + if stack == nil { + return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Stack not found", fmt.Sprintf("Stack %q not found in configuration.", instance.Stack.ConfigAddr()))) + } + resource.StackConfig = stack + + component := m.Config.Component(stackaddrs.ConfigComponentForAbsInstance(instance)) + if component == nil { + return diags.Append(tfdiags.Sourceless(tfdiags.Error, "Component not found", fmt.Sprintf("Component %q not found in stack %q.", instance.Item.Component.Name, instance.Stack.ConfigAddr()))) + } + + resource.ComponentConfig = component + + moduleConfig, diags := m.moduleConfig(component) + if diags.HasErrors() { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Module configuration not found", + Detail: fmt.Sprintf("Module configuration for component %q not found", instance.Item.Component.Name), + Subject: component.SourceAddrRange.ToHCL().Ptr(), + }) + } + resource.StackModuleConfig = moduleConfig + return diags +} diff --git a/internal/stacks/stackmigrate/testing.go b/internal/stacks/stackmigrate/testing.go new file mode 100644 index 0000000000..5ea9ad764b --- /dev/null +++ b/internal/stacks/stackmigrate/testing.go @@ -0,0 +1,37 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackmigrate + +import ( + "os" + "path/filepath" + "testing" + + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/states/statefile" +) + +func TestStateFile(t *testing.T, s *states.State) string { + t.Helper() + + path := filepath.Join(t.TempDir(), "terraform.tfstate") + + f, err := os.Create(path) + if err != nil { + t.Fatalf("failed to create temporary state file %s: %s", path, err) + } + defer f.Close() + + sf := &statefile.File{ + Serial: 0, + Lineage: "fake-for-testing", + State: s, + } + statefile.Write(sf, f) + if err != nil { + t.Fatalf("failed to write state to temporary file %s: %s", path, err) + } + + return path +} diff --git a/internal/stacks/stackplan/component.go b/internal/stacks/stackplan/component.go new file mode 100644 index 0000000000..2eed20a60c --- /dev/null +++ b/internal/stacks/stackplan/component.go @@ -0,0 +1,167 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "fmt" + "time" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +// Component is a container for a set of changes that all belong to the same +// component instance as declared in a stack configuration. +// +// Each instance of component essentially maps to one call into the main +// Terraform language runtime to apply all of the described changes together as +// a single operation. +type Component struct { + PlannedAction plans.Action + Mode plans.Mode + + // These fields echo the [plans.Plan.Applyable] and [plans.Plan.Complete] + // field respectively. See the docs for those fields for more information. + PlanApplyable, PlanComplete bool + + // ResourceInstancePlanned describes the changes that Terraform is proposing + // to make to try to converge the real system state with the desired state + // as described by the configuration. + ResourceInstancePlanned addrs.Map[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc] + + // ResourceInstancePriorState describes the state as it was when making + // the proposals described in [Component.ResourceInstancePlanned]. + // + // Elements of this map have nil values if the planned action is "create", + // since in that case there is no prior object. + ResourceInstancePriorState addrs.Map[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc] + + // ResourceInstanceProviderConfig is a lookup table from resource instance + // object address to the address of the provider configuration that + // will handle any apply-time actions for that object. + ResourceInstanceProviderConfig addrs.Map[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig] + + // DeferredResourceInstanceChanges is a set of resource instance objects + // that have changes that are deferred to a later plan and apply cycle. + DeferredResourceInstanceChanges addrs.Map[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc] + + // PlanTimestamp is the time Terraform Core recorded as the single "plan + // timestamp", which is used only for the result of the "plantimestamp" + // function during apply and must not be used for any other purpose. + PlanTimestamp time.Time + + // Dependencies is a set of addresses of other components that this one + // expects to exist for as long as this one exists. + Dependencies collections.Set[stackaddrs.AbsComponent] + + // Dependents is the reverse of [Component.Dependencies], describing + // the other components that must be destroyed before this one could + // be destroyed. + Dependents collections.Set[stackaddrs.AbsComponent] + + // PlannedFunctionResults is a shared table of results from calling + // provider functions. This is stored and loaded from during the planning + // stage to use during apply operations. + PlannedFunctionResults []lang.FunctionResultHash + + // PlannedInputValues and PlannedInputValueMarks are the values that + // Terraform has planned to use for input variables in this component. + PlannedInputValues map[addrs.InputVariable]plans.DynamicValue + PlannedInputValueMarks map[addrs.InputVariable][]cty.PathValueMarks + + PlannedOutputValues map[addrs.OutputValue]cty.Value + + PlannedChecks *states.CheckResults +} + +// ForModulesRuntime translates the component instance plan into the form +// expected by the modules runtime, which is what would ultimately be used +// to apply the plan. +// +// The stack component planning model preserves only the most crucial details +// of a component plan produced by the modules runtime, and so the result +// will not exactly match the [plans.Plan] that the component plan was produced +// from, but should be complete enough to successfully apply the plan. +// +// Conversion with this method should always succeed if the given previous +// run state is truly the one that the plan was created from. If this method +// returns an error then that suggests that the recieving plan is inconsistent +// with the given previous run state, which should not happen if the caller +// is using Terraform Core correctly. +func (c *Component) ForModulesRuntime() (*plans.Plan, error) { + changes := &plans.ChangesSrc{} + plan := &plans.Plan{ + UIMode: c.Mode, + Changes: changes, + Timestamp: c.PlanTimestamp, + Applyable: c.PlanApplyable, + Complete: c.PlanComplete, + Checks: c.PlannedChecks, + FunctionResults: c.PlannedFunctionResults, + } + + for _, elem := range c.ResourceInstancePlanned.Elems { + changeSrc := elem.Value + if changeSrc != nil { + changes.Resources = append(changes.Resources, changeSrc) + } + } + + priorState := states.NewState() + ss := priorState.SyncWrapper() + for _, elem := range c.ResourceInstancePriorState.Elems { + addr := elem.Key + providerConfigAddr, ok := c.ResourceInstanceProviderConfig.GetOk(addr) + if !ok { + return nil, fmt.Errorf("no provider config address for %s", addr) + } + stateSrc := elem.Value + if addr.IsCurrent() { + ss.SetResourceInstanceCurrent(addr.ResourceInstance, stateSrc, providerConfigAddr) + } else { + ss.SetResourceInstanceDeposed(addr.ResourceInstance, addr.DeposedKey, stateSrc, providerConfigAddr) + } + } + + variableValues := make(map[string]plans.DynamicValue, len(c.PlannedInputValues)) + variableMarks := make(map[string][]cty.PathValueMarks, len(c.PlannedInputValueMarks)) + for k, v := range c.PlannedInputValues { + variableValues[k.Name] = v + } + plan.VariableValues = variableValues + for k, v := range c.PlannedInputValueMarks { + variableMarks[k.Name] = v + } + plan.VariableMarks = variableMarks + + plan.PriorState = priorState + plan.PrevRunState = priorState.DeepCopy() // This is just here to complete the data structure; we don't really do anything with it + + return plan, nil +} + +// RequiredProviderInstances returns a description of all the provider instance +// slots that are required to satisfy the resource instances planned for this +// component. +// +// See also stackstate.State.RequiredProviderInstances and +// stackeval.ComponentConfig.RequiredProviderInstances for similar functions +// that retrieve the provider instances for a components in the config and in +// the state. +func (c *Component) RequiredProviderInstances() addrs.Set[addrs.RootProviderConfig] { + providerInstances := addrs.MakeSet[addrs.RootProviderConfig]() + for _, elem := range c.ResourceInstanceProviderConfig.Elems { + providerInstances.Add(addrs.RootProviderConfig{ + Provider: elem.Value.Provider, + Alias: elem.Value.Alias, + }) + } + return providerInstances +} diff --git a/internal/stacks/stackplan/doc.go b/internal/stacks/stackplan/doc.go new file mode 100644 index 0000000000..6cc9509e99 --- /dev/null +++ b/internal/stacks/stackplan/doc.go @@ -0,0 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package stackplan contains the models and some business logic for stack-wide +// "meta-plans", which in practice are equivalent to multiple of what we +// traditionally think of as a "plan" in the non-stacks Terraform workflow, +// typically represented as a [plans.Plan] object. +// +// The stack plan model is intentionally slightly different from the original +// plan model because in the stack runtime we need to be able to split a +// traditional plan into smaller parts that we stream out to the caller as +// events, but the model here should be isomorphic so that we can translate +// to and from the models expected by the main Terraform language runtime. +package stackplan diff --git a/internal/stacks/stackplan/from_plan.go b/internal/stacks/stackplan/from_plan.go new file mode 100644 index 0000000000..7b276d64c7 --- /dev/null +++ b/internal/stacks/stackplan/from_plan.go @@ -0,0 +1,375 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "context" + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// PlanProducer is an interface of an object that can produce a plan and +// require it to be converted into PlannedChange objects. +type PlanProducer interface { + Addr() stackaddrs.AbsComponentInstance + + // RequiredComponents returns the static set of components that this + // component depends on. Static in this context means based on the + // configuration, so this result shouldn't change based on the type of + // plan. + // + // Normal and destroy plans should return the same set of components, + // with dependents and dependencies computed from this set during the + // apply phase. + RequiredComponents(ctx context.Context) collections.Set[stackaddrs.AbsComponent] + + // ResourceSchema returns the schema for a resource type from a provider. + ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, resourceType string) (providers.Schema, error) +} + +func FromPlan(ctx context.Context, config *configs.Config, plan *plans.Plan, refreshPlan *plans.Plan, action plans.Action, producer PlanProducer) ([]PlannedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var changes []PlannedChange + + var outputs map[string]cty.Value + if refreshPlan != nil { + // we're going to be a little cheeky and publish the outputs as being + // the results from the refresh part of the plan. This will then be + // consumed by the apply part of the plan to ensure that the outputs + // are correctly updated. The refresh plan should only be present if the + // main plan was a destroy plan in which case the outputs that the + // apply needs do actually come from the refresh. + outputs = OutputsFromPlan(config, refreshPlan) + } else { + outputs = OutputsFromPlan(config, plan) + } + + // We must always at least announce that the component instance exists, + // and that must come before any resource instance changes referring to it. + changes = append(changes, &PlannedChangeComponentInstance{ + Addr: producer.Addr(), + + Action: action, + Mode: plan.UIMode, + PlanApplyable: plan.Applyable, + PlanComplete: plan.Complete, + RequiredComponents: producer.RequiredComponents(ctx), + PlannedInputValues: plan.VariableValues, + PlannedInputValueMarks: plan.VariableMarks, + PlannedOutputValues: outputs, + PlannedCheckResults: plan.Checks, + PlannedProviderFunctionResults: plan.FunctionResults, + + // We must remember the plan timestamp so that the plantimestamp + // function can return a consistent result during a later apply phase. + PlanTimestamp: plan.Timestamp, + }) + + seenObjects := addrs.MakeSet[addrs.AbsResourceInstanceObject]() + for _, rsrcChange := range plan.Changes.Resources { + schema, err := producer.ResourceSchema( + ctx, + rsrcChange.ProviderAddr.Provider, + rsrcChange.Addr.Resource.Resource.Mode, + rsrcChange.Addr.Resource.Resource.Type, + ) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Can't fetch provider schema to save plan", + fmt.Sprintf( + "Failed to retrieve the schema for %s from provider %s: %s. This is a bug in Terraform.", + rsrcChange.Addr, rsrcChange.ProviderAddr.Provider, err, + ), + )) + continue + } + + objAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: rsrcChange.Addr, + DeposedKey: rsrcChange.DeposedKey, + } + var priorStateSrc *states.ResourceInstanceObjectSrc + if plan.PriorState != nil { + priorStateSrc = plan.PriorState.ResourceInstanceObjectSrc(objAddr) + } + + changes = append(changes, &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: producer.Addr(), + Item: objAddr, + }, + ChangeSrc: rsrcChange, + Schema: schema, + PriorStateSrc: priorStateSrc, + ProviderConfigAddr: rsrcChange.ProviderAddr, + + // TODO: Also provide the previous run state, if it's + // different from the prior state, and signal whether the + // difference from previous run seems "notable" per + // Terraform Core's heuristics. Only the external plan + // description needs that info, to populate the + // "changes outside of Terraform" part of the plan UI; + // the raw plan only needs the prior state. + }) + seenObjects.Add(objAddr) + } + + // We need to keep track of the deferred changes as well + for _, dr := range plan.DeferredResources { + rsrcChange := dr.ChangeSrc + objAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: rsrcChange.Addr, + DeposedKey: rsrcChange.DeposedKey, + } + var priorStateSrc *states.ResourceInstanceObjectSrc + if plan.PriorState != nil { + priorStateSrc = plan.PriorState.ResourceInstanceObjectSrc(objAddr) + } + + schema, err := producer.ResourceSchema( + ctx, + rsrcChange.ProviderAddr.Provider, + rsrcChange.Addr.Resource.Resource.Mode, + rsrcChange.Addr.Resource.Resource.Type, + ) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Can't fetch provider schema to save plan", + fmt.Sprintf( + "Failed to retrieve the schema for %s from provider %s: %s. This is a bug in Terraform.", + rsrcChange.Addr, rsrcChange.ProviderAddr.Provider, err, + ), + )) + continue + } + + plannedChangeResourceInstance := PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: producer.Addr(), + Item: objAddr, + }, + ChangeSrc: rsrcChange, + Schema: schema, + PriorStateSrc: priorStateSrc, + ProviderConfigAddr: rsrcChange.ProviderAddr, + } + changes = append(changes, &PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: dr.DeferredReason, + ResourceInstancePlanned: plannedChangeResourceInstance, + }) + seenObjects.Add(objAddr) + } + + // We also need to catch any objects that exist in the "prior state" + // but don't have any actions planned, since we still need to capture + // the prior state part in case it was updated by refreshing during + // the plan walk. + if priorState := plan.PriorState; priorState != nil { + for _, addr := range priorState.AllResourceInstanceObjectAddrs() { + if seenObjects.Has(addr) { + // We're only interested in objects that didn't appear + // in the plan, such as data resources whose read has + // completed during the plan phase. + continue + } + + rs := priorState.Resource(addr.ResourceInstance.ContainingResource()) + os := priorState.ResourceInstanceObjectSrc(addr) + schema, err := producer.ResourceSchema( + ctx, + rs.ProviderConfig.Provider, + addr.ResourceInstance.Resource.Resource.Mode, + addr.ResourceInstance.Resource.Resource.Type, + ) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Can't fetch provider schema to save plan", + fmt.Sprintf( + "Failed to retrieve the schema for %s from provider %s: %s. This is a bug in Terraform.", + addr, rs.ProviderConfig.Provider, err, + ), + )) + continue + } + + changes = append(changes, &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: producer.Addr(), + Item: addr, + }, + Schema: schema, + PriorStateSrc: os, + ProviderConfigAddr: rs.ProviderConfig, + // We intentionally omit ChangeSrc, because we're not actually + // planning to change this object during the apply phase, only + // to update its state data. + }) + seenObjects.Add(addr) + } + } + + prevRunState := plan.PrevRunState + if refreshPlan != nil { + // If we executed a refresh plan as part of this, then the true + // previous run state is the one from the refresh plan, because + // the later plan used the output of the refresh plan as the + // previous state. + prevRunState = refreshPlan.PrevRunState + } + + // We also have one more unusual case to deal with: if an object + // existed at the end of the previous run but was found to have + // been deleted when we refreshed during planning then it will + // not be present in either the prior state _or_ the plan, but + // we still need to include a stubby object for it in the plan + // so we can remember to discard it from the state during the + // apply phase. + if prevRunState != nil { + for _, addr := range prevRunState.AllResourceInstanceObjectAddrs() { + if seenObjects.Has(addr) { + // We're only interested in objects that didn't appear + // in the plan, such as data resources whose read has + // completed during the plan phase. + continue + } + + rs := prevRunState.Resource(addr.ResourceInstance.ContainingResource()) + + changes = append(changes, &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: producer.Addr(), + Item: addr, + }, + ProviderConfigAddr: rs.ProviderConfig, + // Everything except the addresses are omitted in this case, + // which represents that we should just delete the object + // from the state when applied, and not take any other + // action. + }) + seenObjects.Add(addr) + } + } + + return changes, diags +} + +func OutputsFromPlan(config *configs.Config, plan *plans.Plan) map[string]cty.Value { + if plan == nil { + return nil + } + + // We need to vary our behavior here slightly depending on what action + // we're planning to take with this overall component: normally we want + // to use the "planned new state"'s output values, but if we're actually + // planning to destroy all of the infrastructure managed by this + // component then the planned new state has no output values at all, + // so we'll use the prior state's output values instead just in case + // we also need to plan destroying another component instance + // downstream of this one which will make use of this instance's + // output values _before_ we destroy it. + // + // FIXME: We're using UIMode for this decision, despite its doc comment + // saying we shouldn't, because this behavior is an offshoot of the + // already-documented annoying exception to that rule where various + // parts of Terraform use UIMode == DestroyMode in particular to deal + // with necessary variations during a "full destroy". Hopefully we'll + // eventually find a more satisfying solution for that, in which case + // we should update the following to use that solution too. + attrs := make(map[string]cty.Value) + switch plan.UIMode { + case plans.DestroyMode: + // The "prior state" of the plan includes any new information we + // learned by "refreshing" before we planned to destroy anything, + // and so should be as close as possible to the current + // (pre-destroy) state of whatever infrastructure this component + // instance is managing. + for _, os := range plan.PriorState.RootOutputValues { + v := os.Value + if os.Sensitive { + // For our purposes here, a static sensitive flag on the + // output value is indistinguishable from the value having + // been dynamically marked as sensitive. + v = v.Mark(marks.Sensitive) + } + attrs[os.Addr.OutputValue.Name] = v + } + default: + for _, changeSrc := range plan.Changes.Outputs { + if len(changeSrc.Addr.Module) > 0 { + // Only include output values of the root module as part + // of the component. + continue + } + + name := changeSrc.Addr.OutputValue.Name + change, err := changeSrc.Decode() + if err != nil { + attrs[name] = cty.DynamicVal + continue + } + + if changeSrc.Sensitive { + // For our purposes here, a static sensitive flag on the + // output value is indistinguishable from the value having + // been dynamically marked as sensitive. + attrs[name] = change.After.Mark(marks.Sensitive) + continue + } + + // Otherwise, just use the value as-is. + attrs[name] = change.After + } + } + + if config != nil { + // If the plan only ran partially then we might be missing + // some planned changes for output values, which could + // cause "attrs" to have an incomplete set of attributes. + // To avoid confusing downstream errors we'll insert unknown + // values for any declared output values that don't yet + // have a final value. + for name := range config.Module.Outputs { + if _, ok := attrs[name]; !ok { + // We can't do any better than DynamicVal because + // output values in the modules language don't + // have static type constraints. + attrs[name] = cty.DynamicVal + } + } + // In the DestroyMode case above we might also find ourselves + // with some remnant additional output values that have since + // been removed from the configuration, but yet remain in the + // state. Destroying with a different configuration than was + // most recently applied is not guaranteed to work, but we + // can make it more likely to work by dropping anything that + // isn't currently declared, since referring directly to these + // would be a static validation error anyway, and including + // them might cause aggregate operations like keys(component.foo) + // to produce broken results. + for name := range attrs { + _, declared := config.Module.Outputs[name] + if !declared { + // (deleting map elements during iteration is valid in Go, + // unlike some other languages.) + delete(attrs, name) + } + } + } + + return attrs +} diff --git a/internal/stacks/stackplan/from_proto.go b/internal/stacks/stackplan/from_proto.go new file mode 100644 index 0000000000..0024261b80 --- /dev/null +++ b/internal/stacks/stackplan/from_proto.go @@ -0,0 +1,471 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "fmt" + "sync" + + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/planfile" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/version" +) + +// A helper for loading saved plans in a streaming manner. +type Loader struct { + ret *Plan + foundHeader bool + + mu sync.Mutex +} + +// Constructs a new [Loader], with an initial empty plan. +func NewLoader() *Loader { + ret := &Plan{ + Root: newStackInstance(stackaddrs.RootStackInstance), + RootInputValues: make(map[stackaddrs.InputVariable]cty.Value), + ApplyTimeInputVariables: collections.NewSetCmp[stackaddrs.InputVariable](), + DeletedInputVariables: collections.NewSet[stackaddrs.InputVariable](), + DeletedOutputValues: collections.NewSet[stackaddrs.OutputValue](), + DeletedComponents: collections.NewSet[stackaddrs.AbsComponentInstance](), + PrevRunStateRaw: make(map[string]*anypb.Any), + } + return &Loader{ + ret: ret, + } +} + +// AddRaw adds a single raw change object to the plan being loaded. +func (l *Loader) AddRaw(rawMsg *anypb.Any) error { + l.mu.Lock() + defer l.mu.Unlock() + if l.ret == nil { + return fmt.Errorf("loader has been consumed") + } + + msg, err := anypb.UnmarshalNew(rawMsg, proto.UnmarshalOptions{ + // Just the default unmarshalling options + }) + if err != nil { + return fmt.Errorf("invalid raw message: %w", err) + } + + // The references to specific message types below ensure that + // the protobuf descriptors for these types are included in the + // compiled program, and thus available in the global protobuf + // registry that anypb.UnmarshalNew relies on above. + switch msg := msg.(type) { + + case *tfstackdata1.PlanHeader: + wantVersion := version.SemVer.String() + gotVersion := msg.TerraformVersion + if gotVersion != wantVersion { + return fmt.Errorf("plan was created by Terraform %s, but this is Terraform %s", gotVersion, wantVersion) + } + l.foundHeader = true + + case *tfstackdata1.PlanPriorStateElem: + if _, exists := l.ret.PrevRunStateRaw[msg.Key]; exists { + // Suggests a bug in the caller, because a valid prior state + // can only have one object associated with each key. + return fmt.Errorf("duplicate prior state key %q", msg.Key) + } + // NOTE: We intentionally don't actually decode and validate the + // state elements here; we'll deal with that piecemeal as we make + // further use of this data structure elsewhere. This avoids spending + // time on decoding here if a caller is loading the plan only to + // extract some metadata from it, and doesn't care about the prior + // state. + l.ret.PrevRunStateRaw[msg.Key] = msg.Raw + + case *tfstackdata1.PlanApplyable: + l.ret.Applyable = msg.Applyable + + case *tfstackdata1.PlanTimestamp: + err = l.ret.PlanTimestamp.UnmarshalText([]byte(msg.PlanTimestamp)) + if err != nil { + return fmt.Errorf("invalid plan timestamp %q", msg.PlanTimestamp) + } + + case *tfstackdata1.DeletedRootOutputValue: + l.ret.DeletedOutputValues.Add(stackaddrs.OutputValue{Name: msg.Name}) + + case *tfstackdata1.DeletedRootInputVariable: + l.ret.DeletedInputVariables.Add(stackaddrs.InputVariable{Name: msg.Name}) + + case *tfstackdata1.DeletedComponent: + addr, diags := stackaddrs.ParseAbsComponentInstanceStr(msg.ComponentInstanceAddr) + if diags.HasErrors() { + // Should not get here because the address we're parsing + // should've been produced by this same version of Terraform. + return fmt.Errorf("invalid component instance address syntax in %q", msg.ComponentInstanceAddr) + } + l.ret.DeletedComponents.Add(addr) + + case *tfstackdata1.PlanRootInputValue: + addr := stackaddrs.InputVariable{ + Name: msg.Name, + } + + val, err := tfstackdata1.DynamicValueFromTFStackData1(msg.Value, cty.DynamicPseudoType) + if err != nil { + return fmt.Errorf("invalid stored value for %s: %w", addr, err) + } + l.ret.RootInputValues[addr] = val + if msg.RequiredOnApply { + if !val.IsNull() { + // A variable can't be both persisted _and_ required on apply. + return fmt.Errorf("plan has value for required-on-apply input variable %s", addr) + } + l.ret.ApplyTimeInputVariables.Add(addr) + } + + case *tfstackdata1.FunctionResults: + for _, hash := range msg.FunctionResults { + l.ret.FunctionResults = append(l.ret.FunctionResults, lang.FunctionResultHash{ + Key: hash.Key, + Result: hash.Result, + }) + } + + case *tfstackdata1.PlanComponentInstance: + addr, diags := stackaddrs.ParsePartialComponentInstanceStr(msg.ComponentInstanceAddr) + if diags.HasErrors() { + // Should not get here because the address we're parsing + // should've been produced by this same version of Terraform. + return fmt.Errorf("invalid component instance address syntax in %q", msg.ComponentInstanceAddr) + } + + dependencies := collections.NewSet[stackaddrs.AbsComponent]() + for _, rawAddr := range msg.DependsOnComponentAddrs { + // NOTE: We're using the component _instance_ address parser + // here, but we really want just components, so we'll need to + // check afterwards to make sure we don't have an instance key. + addr, diags := stackaddrs.ParseAbsComponentInstanceStr(rawAddr) + if diags.HasErrors() { + return fmt.Errorf("invalid component address syntax in %q", rawAddr) + } + if addr.Item.Key != addrs.NoKey { + return fmt.Errorf("invalid component address syntax in %q: is actually a component instance address", rawAddr) + } + realAddr := stackaddrs.AbsComponent{ + Stack: addr.Stack, + Item: addr.Item.Component, + } + dependencies.Add(realAddr) + } + + plannedAction, err := planproto.FromAction(msg.PlannedAction) + if err != nil { + return fmt.Errorf("decoding plan for %s: %w", addr, err) + } + + mode, err := planproto.FromMode(msg.Mode) + if err != nil { + return fmt.Errorf("decoding mode for %s: %w", addr, err) + } + + inputVals := make(map[addrs.InputVariable]plans.DynamicValue) + inputValMarks := make(map[addrs.InputVariable][]cty.PathValueMarks) + for name, rawVal := range msg.PlannedInputValues { + val := addrs.InputVariable{ + Name: name, + } + inputVals[val] = rawVal.Value.Msgpack + inputValMarks[val] = make([]cty.PathValueMarks, len(rawVal.SensitivePaths)) + for _, path := range rawVal.SensitivePaths { + path, err := planfile.PathFromProto(path) + if err != nil { + return fmt.Errorf("decoding sensitive path %q for %s: %w", val, addr, err) + } + inputValMarks[val] = append(inputValMarks[val], cty.PathValueMarks{ + Path: path, + Marks: cty.NewValueMarks(marks.Sensitive), + }) + } + } + + outputVals := make(map[addrs.OutputValue]cty.Value) + for name, rawVal := range msg.PlannedOutputValues { + v, err := tfstackdata1.DynamicValueFromTFStackData1(rawVal, cty.DynamicPseudoType) + if err != nil { + return fmt.Errorf("decoding output value %q for %s: %w", name, addr, err) + } + outputVals[addrs.OutputValue{Name: name}] = v + } + + checkResults, err := planfile.CheckResultsFromPlanProto(msg.PlannedCheckResults) + if err != nil { + return fmt.Errorf("decoding check results: %w", err) + } + + var functionResults []lang.FunctionResultHash + for _, hash := range msg.FunctionResults { + functionResults = append(functionResults, lang.FunctionResultHash{ + Key: hash.Key, + Result: hash.Result, + }) + } + + c := l.ret.GetOrCreate(addr, &Component{ + PlannedAction: plannedAction, + Mode: mode, + PlanApplyable: msg.PlanApplyable, + PlanComplete: msg.PlanComplete, + Dependencies: dependencies, + Dependents: collections.NewSet[stackaddrs.AbsComponent](), + PlannedInputValues: inputVals, + PlannedInputValueMarks: inputValMarks, + PlannedOutputValues: outputVals, + PlannedChecks: checkResults, + PlannedFunctionResults: functionResults, + + ResourceInstancePlanned: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.ResourceInstanceChangeSrc](), + ResourceInstancePriorState: addrs.MakeMap[addrs.AbsResourceInstanceObject, *states.ResourceInstanceObjectSrc](), + ResourceInstanceProviderConfig: addrs.MakeMap[addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig](), + DeferredResourceInstanceChanges: addrs.MakeMap[addrs.AbsResourceInstanceObject, *plans.DeferredResourceInstanceChangeSrc](), + }) + err = c.PlanTimestamp.UnmarshalText([]byte(msg.PlanTimestamp)) + if err != nil { + return fmt.Errorf("invalid plan timestamp %q for %s", msg.PlanTimestamp, addr) + } + + case *tfstackdata1.PlanResourceInstanceChangePlanned: + c, fullAddr, providerConfigAddr, err := LoadComponentForResourceInstance(l.ret, msg) + if err != nil { + return err + } + c.ResourceInstanceProviderConfig.Put(fullAddr, providerConfigAddr) + + // Not all "planned changes" for resource instances are actually + // changes in the plans.Change sense, confusingly: sometimes the + // "change" we're recording is just to overwrite the state entry + // with a refreshed copy, in which case riPlan is nil and + // msg.PriorState is the main content of this change, handled below. + if msg.Change != nil { + riPlan, err := ValidateResourceInstanceChange(msg, fullAddr, providerConfigAddr) + if err != nil { + return err + } + c.ResourceInstancePlanned.Put(fullAddr, riPlan) + } + + if msg.PriorState != nil { + stateSrc, err := tfstackdata1.DecodeProtoResourceInstanceObject(msg.PriorState) + if err != nil { + return fmt.Errorf("invalid prior state for %s: %w", fullAddr, err) + } + c.ResourceInstancePriorState.Put(fullAddr, stateSrc) + } else { + // We'll record an explicit nil just to affirm that there's + // intentionally no prior state for this resource instance + // object. + c.ResourceInstancePriorState.Put(fullAddr, nil) + } + + case *tfstackdata1.PlanDeferredResourceInstanceChange: + if msg.Deferred == nil { + return fmt.Errorf("missing deferred from PlanDeferredResourceInstanceChange") + } + + c, fullAddr, providerConfigAddr, err := LoadComponentForPartialResourceInstance(l.ret, msg.Change) + if err != nil { + return err + } + + riPlan, err := ValidatePartialResourceInstanceChange(msg.Change, fullAddr, providerConfigAddr) + if err != nil { + return err + } + + // We'll just swallow the error here. A missing deferred reason + // could be the only cause and we want to be forward and backward + // compatible. This will just render as INVALID, which is fine. + deferredReason, _ := planfile.DeferredReasonFromProto(msg.Deferred.Reason) + + c.DeferredResourceInstanceChanges.Put(fullAddr, &plans.DeferredResourceInstanceChangeSrc{ + ChangeSrc: riPlan, + DeferredReason: deferredReason, + }) + + default: + // Should not get here, because a stack plan can only be loaded by + // the same version of Terraform that created it, and the above + // should cover everything this version of Terraform can possibly + // emit during PlanStackChanges. + return fmt.Errorf("unsupported raw message type %T", msg) + } + return nil +} + +// Plan consumes the loaded plan, making the associated loader closed to +// further additions. +func (l *Loader) Plan() (*Plan, error) { + l.mu.Lock() + defer l.mu.Unlock() + + // If we got through all of the messages without encountering at least + // one *PlanHeader then we'll abort because we may have lost part of the + // plan sequence somehow. + if !l.foundHeader { + return nil, fmt.Errorf("missing PlanHeader") + } + + // Before we return we'll calculate the reverse dependency information + // based on the forward dependency information we loaded above. + for dependentInstAddr, dependencyInst := range l.ret.AllComponents() { + dependentAddr := stackaddrs.AbsComponent{ + Stack: dependentInstAddr.Stack, + Item: dependentInstAddr.Item.Component, + } + + for dependencyAddr := range dependencyInst.Dependencies.All() { + for _, dependencyInst := range l.ret.ComponentInstances(dependencyAddr) { + dependencyInst.Dependents.Add(dependentAddr) + } + } + } + + ret := l.ret + l.ret = nil + + return ret, nil +} + +func LoadFromProto(msgs []*anypb.Any) (*Plan, error) { + loader := NewLoader() + for i, rawMsg := range msgs { + err := loader.AddRaw(rawMsg) + if err != nil { + return nil, fmt.Errorf("raw item %d: %w", i, err) + } + } + return loader.Plan() +} + +func ValidateResourceInstanceChange(change *tfstackdata1.PlanResourceInstanceChangePlanned, fullAddr addrs.AbsResourceInstanceObject, providerConfigAddr addrs.AbsProviderConfig) (*plans.ResourceInstanceChangeSrc, error) { + riPlan, err := planfile.ResourceChangeFromProto(change.Change) + if err != nil { + return nil, fmt.Errorf("invalid resource instance change: %w", err) + } + // We currently have some redundant information in the nested + // "change" object due to having reused some protobuf message + // types from the traditional Terraform CLI planproto format. + // We'll make sure the redundant information is consistent + // here because otherwise they're likely to cause + // difficult-to-debug problems downstream. + if !riPlan.Addr.Equal(fullAddr.ResourceInstance) && riPlan.DeposedKey == fullAddr.DeposedKey { + return nil, fmt.Errorf("planned change has inconsistent address to its containing object") + } + if !riPlan.ProviderAddr.Equal(providerConfigAddr) { + return nil, fmt.Errorf("planned change has inconsistent provider configuration address to its containing object") + } + return riPlan, nil +} + +func ValidatePartialResourceInstanceChange(change *tfstackdata1.PlanResourceInstanceChangePlanned, fullAddr addrs.AbsResourceInstanceObject, providerConfigAddr addrs.AbsProviderConfig) (*plans.ResourceInstanceChangeSrc, error) { + riPlan, err := planfile.DeferredResourceChangeFromProto(change.Change) + if err != nil { + return nil, fmt.Errorf("invalid resource instance change: %w", err) + } + // We currently have some redundant information in the nested + // "change" object due to having reused some protobuf message + // types from the traditional Terraform CLI planproto format. + // We'll make sure the redundant information is consistent + // here because otherwise they're likely to cause + // difficult-to-debug problems downstream. + if !riPlan.Addr.Equal(fullAddr.ResourceInstance) && riPlan.DeposedKey == fullAddr.DeposedKey { + return nil, fmt.Errorf("planned change has inconsistent address to its containing object") + } + if !riPlan.ProviderAddr.Equal(providerConfigAddr) { + return nil, fmt.Errorf("planned change has inconsistent provider configuration address to its containing object") + } + return riPlan, nil +} + +func LoadComponentForResourceInstance(plan *Plan, change *tfstackdata1.PlanResourceInstanceChangePlanned) (*Component, addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig, error) { + cAddr, diags := stackaddrs.ParseAbsComponentInstanceStr(change.ComponentInstanceAddr) + if diags.HasErrors() { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid component instance address syntax in %q", change.ComponentInstanceAddr) + } + + providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(change.ProviderConfigAddr) + if diags.HasErrors() { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid provider configuration address syntax in %q", change.ProviderConfigAddr) + } + + riAddr, diags := addrs.ParseAbsResourceInstanceStr(change.ResourceInstanceAddr) + if diags.HasErrors() { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid resource instance address syntax in %q", change.ResourceInstanceAddr) + } + + var deposedKey addrs.DeposedKey + if change.DeposedKey != "" { + var err error + deposedKey, err = addrs.ParseDeposedKey(change.DeposedKey) + if err != nil { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid deposed key syntax in %q", change.DeposedKey) + } + } + fullAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: riAddr, + DeposedKey: deposedKey, + } + + c, ok := plan.Root.GetOk(cAddr) + if !ok { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("resource instance change for unannounced component instance %s", cAddr) + } + + return c, fullAddr, providerConfigAddr, nil +} + +func LoadComponentForPartialResourceInstance(plan *Plan, change *tfstackdata1.PlanResourceInstanceChangePlanned) (*Component, addrs.AbsResourceInstanceObject, addrs.AbsProviderConfig, error) { + cAddr, diags := stackaddrs.ParsePartialComponentInstanceStr(change.ComponentInstanceAddr) + if diags.HasErrors() { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid component instance address syntax in %q", change.ComponentInstanceAddr) + } + + providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(change.ProviderConfigAddr) + if diags.HasErrors() { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid provider configuration address syntax in %q", change.ProviderConfigAddr) + } + + riAddr, diags := addrs.ParsePartialResourceInstanceStr(change.ResourceInstanceAddr) + if diags.HasErrors() { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid resource instance address syntax in %q", change.ResourceInstanceAddr) + } + + var deposedKey addrs.DeposedKey + if change.DeposedKey != "" { + var err error + deposedKey, err = addrs.ParseDeposedKey(change.DeposedKey) + if err != nil { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("invalid deposed key syntax in %q", change.DeposedKey) + } + } + fullAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: riAddr, + DeposedKey: deposedKey, + } + + c, ok := plan.Root.GetOk(cAddr) + if !ok { + return nil, addrs.AbsResourceInstanceObject{}, addrs.AbsProviderConfig{}, fmt.Errorf("resource instance change for unannounced component instance %s", cAddr) + } + + return c, fullAddr, providerConfigAddr, nil +} diff --git a/internal/stacks/stackplan/from_proto_test.go b/internal/stacks/stackplan/from_proto_test.go new file mode 100644 index 0000000000..c5ff04e314 --- /dev/null +++ b/internal/stacks/stackplan/from_proto_test.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" +) + +func TestAddRaw(t *testing.T) { + tests := map[string]struct { + Raw []*anypb.Any + Want *Plan + }{ + "empty": { + Raw: nil, + Want: &Plan{ + Root: newStackInstance(stackaddrs.RootStackInstance), + PrevRunStateRaw: make(map[string]*anypb.Any), + RootInputValues: make(map[stackaddrs.InputVariable]cty.Value), + }, + }, + "sensitive input value": { + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ + Name: "foo", + Value: &tfstackdata1.DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"), + }, + SensitivePaths: []*planproto.Path{ + { + Steps: make([]*planproto.Path_Step, 0), // no steps as it is the root value + }, + }, + }, + RequiredOnApply: false, + }), + }, + Want: &Plan{ + Root: newStackInstance(stackaddrs.RootStackInstance), + PrevRunStateRaw: make(map[string]*anypb.Any), + RootInputValues: map[stackaddrs.InputVariable]cty.Value{ + stackaddrs.InputVariable{Name: "foo"}: cty.StringVal("boop").Mark(marks.Sensitive), + }, + }, + }, + "input value": { + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ + Name: "foo", + Value: &tfstackdata1.DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"), + }, + }, + RequiredOnApply: false, + }), + }, + Want: &Plan{ + Root: newStackInstance(stackaddrs.RootStackInstance), + PrevRunStateRaw: make(map[string]*anypb.Any), + RootInputValues: map[stackaddrs.InputVariable]cty.Value{ + stackaddrs.InputVariable{Name: "foo"}: cty.StringVal("boop"), + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + loader := NewLoader() + for _, raw := range test.Raw { + if err := loader.AddRaw(raw); err != nil { + t.Errorf("AddRaw() error = %v", err) + } + } + + if t.Failed() { + return + } + + opts := cmp.Options{ + ctydebug.CmpOptions, + cmpCollectionsSet[stackaddrs.InputVariable](), + cmpCollectionsSet[stackaddrs.OutputValue](), + cmpCollectionsSet[stackaddrs.AbsComponentInstance](), + cmpCollectionsMap[stackaddrs.AbsComponentInstance, *Component](), + } + if diff := cmp.Diff(test.Want, loader.ret, opts...); diff != "" { + t.Errorf("AddRaw() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +func cmpCollectionsSet[V any]() cmp.Option { + return cmp.Comparer(func(x, y collections.Set[V]) bool { + if x.Len() != y.Len() { + return false + } + + for v := range x.All() { + if !y.Has(v) { + return false + } + } + + return true + }) +} + +func cmpCollectionsMap[K, V any]() cmp.Option { + return cmp.Comparer(func(x, y collections.Map[K, V]) bool { + if x.Len() != y.Len() { + return false + } + + for key, entry := range x.All() { + if !y.HasKey(key) { + return false + } + + if !cmp.Equal(entry, y.Get(key)) { + return false + } + } + + return true + }) +} diff --git a/internal/stacks/stackplan/plan.go b/internal/stacks/stackplan/plan.go new file mode 100644 index 0000000000..7f1e9bf1fa --- /dev/null +++ b/internal/stacks/stackplan/plan.go @@ -0,0 +1,330 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "iter" + "time" + + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +// Plan is the main type in this package, representing an entire stack plan, +// or at least the subset of the information that Terraform needs to reliably +// apply the plan and detect any inconsistencies during the apply process. +// +// However, the process of _creating_ a plan doesn't actually produce a single +// object of this type, and instead produces fragments of it gradually as the +// planning process proceeds. The caller of the stack runtime must retain +// all of the raw parts in the order they were emitted and provide them back +// during the apply phase, and then we will finally construct a single instance +// of Plan covering the entire set of changes before we begin applying it. +type Plan struct { + // Applyable is true for a plan that was successfully created in full and + // is sufficient to be applied, or false if the plan is incomplete for + // some reason, such as if an error occurred during planning and so + // the planning process did not entirely run. + Applyable bool + + // Complete is true for a plan that shouldn't need any follow-up plans to + // converge. + Complete bool + + // Mode is the original mode of the plan. + Mode plans.Mode + + // Root is the root StackInstance for the configuration being planned. + // The StackInstance object wraps the specific components for each stack + // instance. + Root *StackInstance + + // The raw representation of the raw state that was provided in the request + // to create the plan. We use this primarily to perform mundane state + // data structure maintenence operations, such as discarding keys that + // are no longer needed or replacing data in old formats with the + // equivalent new representations. + PrevRunStateRaw map[string]*anypb.Any + + // RootInputValues are the input variable values provided to calculate + // the plan. We must use the same values during the apply step to + // sure that the actions taken can be consistent with what was planned. + RootInputValues map[stackaddrs.InputVariable]cty.Value + + // ApplyTimeInputVariables are the names of the root input variable + // values whose values must be re-supplied during the apply phase, + // instead of being persisted in [Plan.RootInputValues]. + ApplyTimeInputVariables collections.Set[stackaddrs.InputVariable] + + // DeletedInputVariables tracks the set of input variables that are being + // deleted by this plan. The apply operation will miss any values + // that are not defined in the configuration, but should still emit + // deletion events to remove them from the state. + DeletedInputVariables collections.Set[stackaddrs.InputVariable] + + // DeletedOutputValues tracks the set of output values that are being + // deleted by this plan. The apply operation will miss any output values + // that are not defined in the configuration, but should still emit + // deletion events to remove them from the state. Output values not being + // deleted will be recomputed during the apply so are not needed. + DeletedOutputValues collections.Set[stackaddrs.OutputValue] + + // DeletedComponents are a set of components that are in the state that + // should just be removed without any apply operation. This is typically + // because they are not referenced in the configuration and have no + // associated resources. + DeletedComponents collections.Set[stackaddrs.AbsComponentInstance] + + // FunctionResults is a shared table of results from calling + // provider functions. This is stored and loaded from during the planning + // stage to use during apply operations. + FunctionResults []lang.FunctionResultHash + + // PlanTimestamp is the time at which the plan was created. + PlanTimestamp time.Time +} + +func (p *Plan) AllComponents() iter.Seq2[stackaddrs.AbsComponentInstance, *Component] { + return func(yield func(stackaddrs.AbsComponentInstance, *Component) bool) { + p.Root.iterate(yield) + } +} + +func (p *Plan) ComponentInstanceAddresses(addr stackaddrs.AbsComponent) iter.Seq[stackaddrs.ComponentInstance] { + return func(yield func(stackaddrs.ComponentInstance) bool) { + stack := p.Root.GetDescendentStack(addr.Stack) + if stack != nil { + components := stack.Components[addr.Item] + for key := range components { + proceed := yield(stackaddrs.ComponentInstance{ + Component: addr.Item, + Key: key, + }) + if !proceed { + return + } + } + } + } +} + +// ComponentInstances returns a set of the component instances that belong to +// the given component. +func (p *Plan) ComponentInstances(addr stackaddrs.AbsComponent) iter.Seq2[stackaddrs.ComponentInstance, *Component] { + return func(yield func(stackaddrs.ComponentInstance, *Component) bool) { + stack := p.Root.GetDescendentStack(addr.Stack) + if stack != nil { + components := stack.Components[addr.Item] + for key, component := range components { + proceed := yield(stackaddrs.ComponentInstance{ + Component: addr.Item, + Key: key, + }, component) + if !proceed { + return + } + } + } + } +} + +func (p *Plan) StackInstances(addr stackaddrs.AbsStackCall) iter.Seq[stackaddrs.StackInstance] { + return func(yield func(stackaddrs.StackInstance) bool) { + stack := p.Root.GetDescendentStack(addr.Stack) + if stack != nil { + stacks := stack.Children[addr.Item.Name] + for key := range stacks { + proceed := yield(append(addr.Stack, stackaddrs.StackInstanceStep{ + Name: addr.Item.Name, + Key: key, + })) + if !proceed { + return + } + } + } + } +} + +func (p *Plan) GetOrCreate(addr stackaddrs.AbsComponentInstance, component *Component) *Component { + targetStackInstance := p.Root.GetOrCreateDescendentStack(addr.Stack) + return targetStackInstance.GetOrCreateComponent(addr.Item, component) +} + +func (p *Plan) GetComponent(addr stackaddrs.AbsComponentInstance) *Component { + targetStackInstance := p.Root.GetDescendentStack(addr.Stack) + return targetStackInstance.GetComponent(addr.Item) +} + +func (p *Plan) GetStack(addr stackaddrs.StackInstance) *StackInstance { + return p.Root.GetDescendentStack(addr) +} + +// RequiredProviderInstances returns a description of all of the provider +// instance slots that are required to satisfy the resource instances +// belonging to the given component instance. +// +// See also stackeval.ComponentConfig.RequiredProviderInstances for a similar +// function that operates on the configuration of a component instance rather +// than the plan of one. +func (p *Plan) RequiredProviderInstances(addr stackaddrs.AbsComponentInstance) addrs.Set[addrs.RootProviderConfig] { + stack := p.Root.GetDescendentStack(addr.Stack) + if stack == nil { + return addrs.MakeSet[addrs.RootProviderConfig]() + } + + components, ok := stack.Components[addr.Item.Component] + if !ok { + return addrs.MakeSet[addrs.RootProviderConfig]() + } + + component, ok := components[addr.Item.Key] + if !ok { + return addrs.MakeSet[addrs.RootProviderConfig]() + } + return component.RequiredProviderInstances() +} + +// StackInstance stores the components and embedded stacks for a single stack +// instance. +type StackInstance struct { + Address stackaddrs.StackInstance + Children map[string]map[addrs.InstanceKey]*StackInstance + Components map[stackaddrs.Component]map[addrs.InstanceKey]*Component +} + +func newStackInstance(address stackaddrs.StackInstance) *StackInstance { + return &StackInstance{ + Address: address, + Components: make(map[stackaddrs.Component]map[addrs.InstanceKey]*Component), + Children: make(map[string]map[addrs.InstanceKey]*StackInstance), + } +} + +func (stack *StackInstance) GetComponent(addr stackaddrs.ComponentInstance) *Component { + components, ok := stack.Components[addr.Component] + if !ok { + return nil + } + return components[addr.Key] +} + +func (stack *StackInstance) GetOrCreateComponent(addr stackaddrs.ComponentInstance, component *Component) *Component { + components, ok := stack.Components[addr.Component] + if !ok { + components = make(map[addrs.InstanceKey]*Component) + } + existing, ok := components[addr.Key] + if ok { + return existing + } + components[addr.Key] = component + stack.Components[addr.Component] = components + return component +} + +func (stack *StackInstance) GetOrCreateDescendentStack(addr stackaddrs.StackInstance) *StackInstance { + if len(addr) == 0 { + return stack + } + next := stack.GetOrCreateChildStack(addr[0]) + return next.GetOrCreateDescendentStack(addr[1:]) +} + +func (stack *StackInstance) GetOrCreateChildStack(step stackaddrs.StackInstanceStep) *StackInstance { + child := stack.GetChildStack(step) + if child == nil { + child = stack.CreateChildStack(step) + } + return child +} + +func (stack *StackInstance) GetDescendentStack(addr stackaddrs.StackInstance) *StackInstance { + if len(addr) == 0 { + return stack + } + + next := stack.GetChildStack(addr[0]) + if next == nil { + return nil + } + return next.GetDescendentStack(addr[1:]) +} + +func (stack *StackInstance) GetChildStack(step stackaddrs.StackInstanceStep) *StackInstance { + insts, ok := stack.Children[step.Name] + if !ok { + return nil + } + return insts[step.Key] +} + +func (stack *StackInstance) CreateChildStack(step stackaddrs.StackInstanceStep) *StackInstance { + stacks, ok := stack.Children[step.Name] + if !ok { + stacks = make(map[addrs.InstanceKey]*StackInstance) + } + stacks[step.Key] = newStackInstance(append(stack.Address, step)) + stack.Children[step.Name] = stacks + return stacks[step.Key] +} + +func (stack *StackInstance) GetOk(addr stackaddrs.AbsComponentInstance) (*Component, bool) { + if len(addr.Stack) == 0 { + component, ok := stack.Components[addr.Item.Component] + if !ok { + return nil, false + } + + instance, ok := component[addr.Item.Key] + return instance, ok + } + + stacks, ok := stack.Children[addr.Stack[0].Name] + if !ok { + return nil, false + } + next, ok := stacks[addr.Stack[0].Key] + if !ok { + return nil, false + } + return next.GetOk(stackaddrs.AbsComponentInstance{ + Stack: addr.Stack[1:], + Item: addr.Item, + }) +} + +func (stack *StackInstance) iterate(yield func(stackaddrs.AbsComponentInstance, *Component) bool) bool { + for name, components := range stack.Components { + for key, component := range components { + proceed := yield(stackaddrs.AbsComponentInstance{ + Stack: stack.Address, + Item: stackaddrs.ComponentInstance{ + Component: name, + Key: key, + }, + }, component) + if !proceed { + return false + } + } + } + + for _, stacks := range stack.Children { + for _, inst := range stacks { + proceed := inst.iterate(yield) + if !proceed { + return false + } + } + } + + return true +} diff --git a/internal/stacks/stackplan/planned_change.go b/internal/stacks/stackplan/planned_change.go new file mode 100644 index 0000000000..7cb8888ca9 --- /dev/null +++ b/internal/stacks/stackplan/planned_change.go @@ -0,0 +1,855 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "fmt" + "time" + + "github.com/hashicorp/go-version" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/planfile" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackutils" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" +) + +// PlannedChange represents a single isolated planned changed, emitted as +// part of a stream of planned changes during the PlanStackChanges RPC API +// operation. +// +// Each PlannedChange becomes a single event in the RPC API, which itself +// has zero or more opaque raw plan messages that the caller must collect and +// provide verbatim during planning and zero or one "description" messages +// that are to give the caller realtime updates about the planning process. +// +// The aggregated sequence of "raw" messages can be provided later to +// [LoadFromProto] to obtain a [Plan] object containing the information +// Terraform Core would need to apply the plan. +type PlannedChange interface { + // PlannedChangeProto returns the protocol buffers representation of + // the change, ready to be sent verbatim to an RPC API client. + PlannedChangeProto() (*stacks.PlannedChange, error) +} + +// PlannedChangeRootInputValue announces the existence of a root stack input +// variable and captures its plan-time value so we can make sure to use +// the same value during the apply phase. +type PlannedChangeRootInputValue struct { + Addr stackaddrs.InputVariable + + // Action is the change being applied to this input variable. + Action plans.Action + + // Before and After provide the values for before and after this plan. + // Both could be cty.NilValue if the before or after was ephemeral at the + // time it was set. Before will be cty.NullVal if Action is plans.Create. + Before cty.Value + After cty.Value + + // RequiredOnApply is true if a non-null value for this variable + // must be supplied during the apply phase. + // + // If this field is false then the variable must either be left unset + // or must be set to the same value during the apply phase, both of + // which are equivalent. + // + // This is set for an input variable that was declared as ephemeral + // and was set to a non-null value during the planning phase. The + // "null-ness" of an ephemeral value is not allowed to change between + // plan and apply, but a value set during planning can have a different + // value during apply. + RequiredOnApply bool + + // DeleteOnApply is true if this variable should be removed from the state + // on apply even if it was not actively removed from the configuration in + // a delete action. This is typically the case during a destroy only plan + // in which we want to update the state to remove everything. + DeleteOnApply bool +} + +var _ PlannedChange = (*PlannedChangeRootInputValue)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangeRootInputValue) PlannedChangeProto() (*stacks.PlannedChange, error) { + protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.Action) + if err != nil { + return nil, err + } + + var raws []*anypb.Any + if pc.Action == plans.Delete || pc.DeleteOnApply { + var raw anypb.Any + if err := anypb.MarshalFrom(&raw, &tfstackdata1.DeletedRootInputVariable{ + Name: pc.Addr.Name, + }, proto.MarshalOptions{}); err != nil { + return nil, fmt.Errorf("failed to encode raw state for %s: %w", pc.Addr, err) + } + raws = append(raws, &raw) + } + + before, err := stacks.ToDynamicValue(pc.Before, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("failed to encode before planned input variable %s: %w", pc.Addr, err) + } + after, err := stacks.ToDynamicValue(pc.After, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("failed to encode after planned input variable %s: %w", pc.Addr, err) + } + + if pc.Action != plans.Delete { + var raw anypb.Any + if err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanRootInputValue{ + Name: pc.Addr.Name, + Value: tfstackdata1.Terraform1ToStackDataDynamicValue(after), + RequiredOnApply: pc.RequiredOnApply, + }, proto.MarshalOptions{}); err != nil { + return nil, err + } + raws = append(raws, &raw) + } + + return &stacks.PlannedChange{ + Raw: raws, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ + InputVariablePlanned: &stacks.PlannedChange_InputVariable{ + Name: pc.Addr.Name, + Actions: protoChangeTypes, + Values: &stacks.DynamicValueChange{ + Old: before, + New: after, + }, + RequiredDuringApply: pc.RequiredOnApply, + }, + }, + }, + }, + }, nil +} + +// PlannedChangeComponentInstanceRemoved is just a reminder for the apply +// operation to delete this component from the state because it's not in +// the configuration and is empty. +type PlannedChangeComponentInstanceRemoved struct { + Addr stackaddrs.AbsComponentInstance +} + +var _ PlannedChange = (*PlannedChangeComponentInstanceRemoved)(nil) + +func (pc *PlannedChangeComponentInstanceRemoved) PlannedChangeProto() (*stacks.PlannedChange, error) { + var raw anypb.Any + if err := anypb.MarshalFrom(&raw, &tfstackdata1.DeletedComponent{ + ComponentInstanceAddr: pc.Addr.String(), + }, proto.MarshalOptions{}); err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + }, nil +} + +// PlannedChangeComponentInstance announces the existence of a component +// instance and describes (using a plan action) whether it is being added +// or removed. +type PlannedChangeComponentInstance struct { + Addr stackaddrs.AbsComponentInstance + + // PlanApplyable is true if the modules runtime ruled that this particular + // component's plan is applyable. + // + // See the documentation for [plans.Plan.Applyable] for details on what + // exactly this represents. + PlanApplyable bool + + // PlanApplyable is true if the modules runtime ruled that this particular + // component's plan is complete. + // + // See the documentation for [plans.Plan.Complete] for details on what + // exactly this represents. + PlanComplete bool + + // Action describes any difference in the existence of this component + // instance compared to the prior state. + // + // Currently it can only be "Create", "Delete", or "NoOp". This action + // relates to the existence of the component instance itself and does + // not consider the resource instances inside, whose change actions + // are tracked in their own [PlannedChange] objects. + Action plans.Action + + // Mode describes the mode that the component instance is being planned + // in. + Mode plans.Mode + + // RequiredComponents is a set of the addresses of all of the components + // that provide infrastructure that this one's infrastructure will + // depend on. Any component named here must exist for the entire lifespan + // of this component instance. + RequiredComponents collections.Set[stackaddrs.AbsComponent] + + // PlannedInputValues records our best approximation of the component's + // topmost input values during the planning phase. This could contain + // unknown values if one component is configured from results of another. + // This therefore won't be used directly as the input values during apply, + // but the final set of input values during apply should be consistent + // with what's captured here. + PlannedInputValues map[string]plans.DynamicValue + + PlannedInputValueMarks map[string][]cty.PathValueMarks + + PlannedOutputValues map[string]cty.Value + + PlannedCheckResults *states.CheckResults + + PlannedProviderFunctionResults []lang.FunctionResultHash + + // PlanTimestamp is the timestamp that would be returned from the + // "plantimestamp" function in modules inside this component. We + // must preserve this in the raw plan data to ensure that we can + // return the same timestamp again during the apply phase. + PlanTimestamp time.Time +} + +var _ PlannedChange = (*PlannedChangeComponentInstance)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangeComponentInstance) PlannedChangeProto() (*stacks.PlannedChange, error) { + var plannedInputValues map[string]*tfstackdata1.DynamicValue + if n := len(pc.PlannedInputValues); n != 0 { + plannedInputValues = make(map[string]*tfstackdata1.DynamicValue, n) + for k, v := range pc.PlannedInputValues { + var sensitivePaths []*planproto.Path + if pvm, ok := pc.PlannedInputValueMarks[k]; ok { + for _, p := range pvm { + path, err := planproto.NewPath(p.Path) + if err != nil { + return nil, err + } + sensitivePaths = append(sensitivePaths, path) + } + } + plannedInputValues[k] = &tfstackdata1.DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: v, + }, + SensitivePaths: sensitivePaths, + } + } + } + + var planTimestampStr string + var zeroTime time.Time + if pc.PlanTimestamp != zeroTime { + planTimestampStr = pc.PlanTimestamp.Format(time.RFC3339) + } + + componentAddrsRaw := make([]string, 0, pc.RequiredComponents.Len()) + for componentAddr := range pc.RequiredComponents.All() { + componentAddrsRaw = append(componentAddrsRaw, componentAddr.String()) + } + + plannedOutputValues := make(map[string]*tfstackdata1.DynamicValue) + for k, v := range pc.PlannedOutputValues { + dv, err := stacks.ToDynamicValue(v, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("encoding output value %q: %w", k, err) + } + plannedOutputValues[k] = tfstackdata1.Terraform1ToStackDataDynamicValue(dv) + } + + plannedCheckResults, err := planfile.CheckResultsToPlanProto(pc.PlannedCheckResults) + if err != nil { + return nil, fmt.Errorf("failed to encode check results: %s", err) + } + + var plannedFunctionResults []*planproto.FunctionCallHash + for _, result := range pc.PlannedProviderFunctionResults { + plannedFunctionResults = append(plannedFunctionResults, &planproto.FunctionCallHash{ + Key: result.Key, + Result: result.Result, + }) + } + + mode, err := planproto.NewMode(pc.Mode) + if err != nil { + return nil, fmt.Errorf("failed to encode mode: %s", err) + } + + var raw anypb.Any + err = anypb.MarshalFrom(&raw, &tfstackdata1.PlanComponentInstance{ + ComponentInstanceAddr: pc.Addr.String(), + PlanTimestamp: planTimestampStr, + PlannedInputValues: plannedInputValues, + PlannedAction: planproto.NewAction(pc.Action), + Mode: mode, + PlanApplyable: pc.PlanApplyable, + PlanComplete: pc.PlanComplete, + DependsOnComponentAddrs: componentAddrsRaw, + PlannedOutputValues: plannedOutputValues, + PlannedCheckResults: plannedCheckResults, + FunctionResults: plannedFunctionResults, + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.Action) + if err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ + ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: stackaddrs.ConfigComponentForAbsInstance(pc.Addr).String(), + ComponentInstanceAddr: pc.Addr.String(), + }, + Actions: protoChangeTypes, + PlanComplete: pc.PlanComplete, + // We don't include "applyable" in here since for a + // stack operation it's the overall stack plan applyable + // flag that matters, and the per-component flags + // are just an implementation detail. + }, + }, + }, + }, + }, nil +} + +// PlannedChangeResourceInstancePlanned announces an action that Terraform +// is proposing to take if this plan is applied. +type PlannedChangeResourceInstancePlanned struct { + ResourceInstanceObjectAddr stackaddrs.AbsResourceInstanceObject + + // ChangeSrc describes the planned change, if any. This can be nil if + // we're only intending to update the state to match PriorStateSrc. + ChangeSrc *plans.ResourceInstanceChangeSrc + + // PriorStateSrc describes the "prior state" that the planned change, if + // any, was generated against. + // + // This can be nil if the object didn't previously exist. If both + // PriorStateSrc and ChangeSrc are nil then that suggests that the + // object existed in the previous run's state but was found to no + // longer exist while refreshing during plan. + PriorStateSrc *states.ResourceInstanceObjectSrc + + // ProviderConfigAddr is the address of the provider configuration + // that planned this change, resolved in terms of the configuration for + // the component this resource instance object belongs to. + ProviderConfigAddr addrs.AbsProviderConfig + + // Schema MUST be the same schema that was used to encode the dynamic + // values inside ChangeSrc and PriorStateSrc. + // + // Can be empty if and only if ChangeSrc and PriorStateSrc are both nil + // themselves. + Schema providers.Schema +} + +var _ PlannedChange = (*PlannedChangeResourceInstancePlanned)(nil) + +func (pc *PlannedChangeResourceInstancePlanned) PlanResourceInstanceChangePlannedProto() (*tfstackdata1.PlanResourceInstanceChangePlanned, error) { + rioAddr := pc.ResourceInstanceObjectAddr + + if pc.ChangeSrc == nil && pc.PriorStateSrc == nil { + // This is just a stubby placeholder to remind us to drop the + // apparently-deleted-outside-of-Terraform object from the state + // if this plan later gets applied. + + return &tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: rioAddr.Component.String(), + ResourceInstanceAddr: rioAddr.Item.ResourceInstance.String(), + DeposedKey: rioAddr.Item.DeposedKey.String(), + ProviderConfigAddr: pc.ProviderConfigAddr.String(), + }, nil + } + + // We include the prior state as part of the raw plan because that + // contains the result of upgrading the state to the provider's latest + // schema version and incorporating any changes detected in the refresh + // step, which we'll rely on during the apply step to make sure that + // the final plan is consistent, etc. + priorStateProto := tfstackdata1.ResourceInstanceObjectStateToTFStackData1(pc.PriorStateSrc, pc.ProviderConfigAddr) + + changeProto, err := planfile.ResourceChangeToProto(pc.ChangeSrc) + if err != nil { + return nil, fmt.Errorf("converting resource instance change to proto: %w", err) + } + + return &tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: rioAddr.Component.String(), + ResourceInstanceAddr: rioAddr.Item.ResourceInstance.String(), + DeposedKey: rioAddr.Item.DeposedKey.String(), + ProviderConfigAddr: pc.ProviderConfigAddr.String(), + Change: changeProto, + PriorState: priorStateProto, + }, nil +} + +func (pc *PlannedChangeResourceInstancePlanned) ChangeDescription() (*stacks.PlannedChange_ChangeDescription, error) { + rioAddr := pc.ResourceInstanceObjectAddr + // We only emit an external description if there's a change to describe. + // Otherwise, we just emit a raw to remind us to update the state for + // this object during the apply step, to match the prior state. + if pc.ChangeSrc == nil { + return nil, nil + } + + protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.ChangeSrc.Action) + if err != nil { + return nil, err + } + replacePaths, err := encodePathSet(pc.ChangeSrc.RequiredReplace) + if err != nil { + return nil, err + } + + var moved *stacks.PlannedChange_ResourceInstance_Moved + var imported *stacks.PlannedChange_ResourceInstance_Imported + + if pc.ChangeSrc.Moved() { + moved = &stacks.PlannedChange_ResourceInstance_Moved{ + PrevAddr: stacks.NewResourceInstanceInStackAddr(stackaddrs.AbsResourceInstance{ + Component: rioAddr.Component, + Item: pc.ChangeSrc.PrevRunAddr, + }), + } + } + + if pc.ChangeSrc.Importing != nil { + imported = &stacks.PlannedChange_ResourceInstance_Imported{ + ImportId: pc.ChangeSrc.Importing.ID, + Unknown: pc.ChangeSrc.Importing.Unknown, + } + } + + var index *stacks.PlannedChange_ResourceInstance_Index + if pc.ChangeSrc.Addr.Resource.Key != nil { + key := pc.ChangeSrc.Addr.Resource.Key + if key == addrs.WildcardKey { + index = &stacks.PlannedChange_ResourceInstance_Index{ + Unknown: true, + } + } else { + value, err := DynamicValueToTerraform1(key.Value(), cty.DynamicPseudoType) + if err != nil { + return nil, err + } + index = &stacks.PlannedChange_ResourceInstance_Index{ + Value: value, + } + } + } + + return &stacks.PlannedChange_ChangeDescription{ + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ + ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ + Addr: stacks.NewResourceInstanceObjectInStackAddr(rioAddr), + ResourceName: pc.ChangeSrc.Addr.Resource.Resource.Name, + Index: index, + ModuleAddr: pc.ChangeSrc.Addr.Module.String(), + ResourceMode: stackutils.ResourceModeForProto(pc.ChangeSrc.Addr.Resource.Resource.Mode), + ResourceType: pc.ChangeSrc.Addr.Resource.Resource.Type, + ProviderAddr: pc.ChangeSrc.ProviderAddr.Provider.String(), + ActionReason: pc.ChangeSrc.ActionReason.String(), + + Actions: protoChangeTypes, + Values: &stacks.DynamicValueChange{ + Old: stacks.NewDynamicValue( + pc.ChangeSrc.Before, + pc.ChangeSrc.BeforeSensitivePaths, + ), + New: stacks.NewDynamicValue( + pc.ChangeSrc.After, + pc.ChangeSrc.AfterSensitivePaths, + ), + }, + ReplacePaths: replacePaths, + Moved: moved, + Imported: imported, + }, + }, + }, nil + +} + +func DynamicValueToTerraform1(val cty.Value, ty cty.Type) (*stacks.DynamicValue, error) { + unmarkedVal, markPaths := val.UnmarkDeepWithPaths() + sensitivePaths, withOtherMarks := marks.PathsWithMark(markPaths, marks.Sensitive) + if len(withOtherMarks) != 0 { + return nil, withOtherMarks[0].Path.NewErrorf( + "can't serialize value marked with %#v (this is a bug in Terraform)", + withOtherMarks[0].Marks, + ) + } + + rawVal, err := msgpack.Marshal(unmarkedVal, ty) + if err != nil { + return nil, err + } + ret := &stacks.DynamicValue{ + Msgpack: rawVal, + } + + if len(markPaths) == 0 { + return ret, nil + } + + ret.Sensitive = make([]*stacks.AttributePath, 0, len(markPaths)) + for _, path := range sensitivePaths { + ret.Sensitive = append(ret.Sensitive, stacks.NewAttributePath(path)) + } + return ret, nil +} + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangeResourceInstancePlanned) PlannedChangeProto() (*stacks.PlannedChange, error) { + pric, err := pc.PlanResourceInstanceChangePlannedProto() + if err != nil { + return nil, err + } + var raw anypb.Any + err = anypb.MarshalFrom(&raw, pric, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + if pc.ChangeSrc == nil && pc.PriorStateSrc == nil { + // We only emit a "raw" in this case, because this is a relatively + // uninteresting edge-case. The PlanResourceInstanceChangePlannedProto + // function should have returned a placeholder value for this use case. + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + }, nil + } + + var descs []*stacks.PlannedChange_ChangeDescription + desc, err := pc.ChangeDescription() + if err != nil { + return nil, err + } + if desc != nil { + descs = append(descs, desc) + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + Descriptions: descs, + }, nil +} + +// PlannedChangeDeferredResourceInstancePlanned announces that an action that Terraform +// is proposing to take if this plan is applied is being deferred. +type PlannedChangeDeferredResourceInstancePlanned struct { + // ResourceInstancePlanned is the planned change that is being deferred. + ResourceInstancePlanned PlannedChangeResourceInstancePlanned + + // DeferredReason is the reason why the change is being deferred. + DeferredReason providers.DeferredReason +} + +var _ PlannedChange = (*PlannedChangeDeferredResourceInstancePlanned)(nil) + +// PlannedChangeProto implements PlannedChange. +func (dpc *PlannedChangeDeferredResourceInstancePlanned) PlannedChangeProto() (*stacks.PlannedChange, error) { + change, err := dpc.ResourceInstancePlanned.PlanResourceInstanceChangePlannedProto() + if err != nil { + return nil, err + } + + // We'll ignore the error here. We certainly should not have got this far + // if we have a deferred reason that the Terraform Core runtime doesn't + // recognise. There will be diagnostics elsewhere to reflect this, as we + // can just use INVALID to capture this. This also makes us forwards and + // backwards compatible, as we'll return INVALID for any new deferred + // reasons that are added in the future without erroring. + deferredReason, _ := planfile.DeferredReasonToProto(dpc.DeferredReason) + + var raw anypb.Any + err = anypb.MarshalFrom(&raw, &tfstackdata1.PlanDeferredResourceInstanceChange{ + Change: change, + Deferred: &planproto.Deferred{ + Reason: deferredReason, + }, + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + ricd, err := dpc.ResourceInstancePlanned.ChangeDescription() + if err != nil { + return nil, err + } + + var descs []*stacks.PlannedChange_ChangeDescription + descs = append(descs, &stacks.PlannedChange_ChangeDescription{ + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstanceDeferred{ + ResourceInstanceDeferred: &stacks.PlannedChange_ResourceInstanceDeferred{ + ResourceInstance: ricd.GetResourceInstancePlanned(), + Deferred: EncodeDeferred(dpc.DeferredReason), + }, + }, + }) + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + Descriptions: descs, + }, nil +} + +func EncodeDeferred(reason providers.DeferredReason) *stacks.Deferred { + deferred := new(stacks.Deferred) + switch reason { + case providers.DeferredReasonInstanceCountUnknown: + deferred.Reason = stacks.Deferred_INSTANCE_COUNT_UNKNOWN + case providers.DeferredReasonResourceConfigUnknown: + deferred.Reason = stacks.Deferred_RESOURCE_CONFIG_UNKNOWN + case providers.DeferredReasonProviderConfigUnknown: + deferred.Reason = stacks.Deferred_PROVIDER_CONFIG_UNKNOWN + case providers.DeferredReasonAbsentPrereq: + deferred.Reason = stacks.Deferred_ABSENT_PREREQ + case providers.DeferredReasonDeferredPrereq: + deferred.Reason = stacks.Deferred_DEFERRED_PREREQ + default: + deferred.Reason = stacks.Deferred_INVALID + } + return deferred +} + +func encodePathSet(pathSet cty.PathSet) ([]*stacks.AttributePath, error) { + if pathSet.Empty() { + return nil, nil + } + + pathList := pathSet.List() + paths := make([]*stacks.AttributePath, 0, len(pathList)) + + for _, path := range pathList { + paths = append(paths, stacks.NewAttributePath(path)) + } + return paths, nil +} + +// PlannedChangeOutputValue announces the change action for one output value +// declared in the top-level stack configuration. +// +// This change type only includes an external description, and does not +// contribute anything to the raw plan sequence. +type PlannedChangeOutputValue struct { + Addr stackaddrs.OutputValue // Covers only root stack output values + Action plans.Action + Before, After cty.Value +} + +var _ PlannedChange = (*PlannedChangeOutputValue)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangeOutputValue) PlannedChangeProto() (*stacks.PlannedChange, error) { + protoChangeTypes, err := stacks.ChangeTypesForPlanAction(pc.Action) + if err != nil { + return nil, err + } + + before, err := stacks.ToDynamicValue(pc.Before, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("failed to encode planned output value %s: %w", pc.Addr, err) + } + + after, err := stacks.ToDynamicValue(pc.After, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("failed to encode planned output value %s: %w", pc.Addr, err) + } + + var raw []*anypb.Any + if pc.Action == plans.Delete { + var r anypb.Any + if err := anypb.MarshalFrom(&r, &tfstackdata1.DeletedRootOutputValue{ + Name: pc.Addr.Name, + }, proto.MarshalOptions{}); err != nil { + return nil, fmt.Errorf("failed to encode raw state for %s: %w", pc.Addr, err) + } + + raw = []*anypb.Any{&r} + } + + return &stacks.PlannedChange{ + Raw: raw, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_OutputValuePlanned{ + OutputValuePlanned: &stacks.PlannedChange_OutputValue{ + Name: pc.Addr.Name, + Actions: protoChangeTypes, + Values: &stacks.DynamicValueChange{ + Old: before, + New: after, + }, + }, + }, + }, + }, + }, nil +} + +// PlannedChangeHeader is a special change type we typically emit before any +// others to capture overall metadata about a plan. [LoadFromProto] fails if +// asked to decode a plan sequence that doesn't include at least one raw +// message generated from this change type. +// +// PlannedChangeHeader has only a raw message and does not contribute to +// the external-facing plan description. +type PlannedChangeHeader struct { + TerraformVersion *version.Version +} + +var _ PlannedChange = (*PlannedChangeHeader)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangeHeader) PlannedChangeProto() (*stacks.PlannedChange, error) { + var raw anypb.Any + err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanHeader{ + TerraformVersion: pc.TerraformVersion.String(), + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + }, nil +} + +// PlannedChangePriorStateElement is a special change type we emit to capture +// each element of the prior state. +// +// PlannedChangePriorStateElement has only a raw message and does not +// contribute to the external-facing plan description, since it's really just +// an implementation detail that allows us to deal with various state cleanup +// concerns during the apply phase; this isn't really a "planned change" in +// the typical sense. +type PlannedChangePriorStateElement struct { + Key string + Raw *anypb.Any +} + +var _ PlannedChange = (*PlannedChangePriorStateElement)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangePriorStateElement) PlannedChangeProto() (*stacks.PlannedChange, error) { + var raw anypb.Any + err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanPriorStateElem{ + Key: pc.Key, + Raw: pc.Raw, + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + }, nil +} + +// PlannedChangePlannedTimestamp is a special change type we emit to record the timestamp +// of when the plan was generated. This is being used in the plantimestamp function. +type PlannedChangePlannedTimestamp struct { + PlannedTimestamp time.Time +} + +var _ PlannedChange = (*PlannedChangePlannedTimestamp)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangePlannedTimestamp) PlannedChangeProto() (*stacks.PlannedChange, error) { + var raw anypb.Any + err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanTimestamp{ + PlanTimestamp: pc.PlannedTimestamp.Format(time.RFC3339), + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + }, nil +} + +// PlannedChangeApplyable is a special change type we typically append at the +// end of the raw plan stream to represent that the planning process ran to +// completion without encountering any errors, and therefore the plan could +// potentially be applied. +type PlannedChangeApplyable struct { + Applyable bool +} + +var _ PlannedChange = (*PlannedChangeApplyable)(nil) + +// PlannedChangeProto implements PlannedChange. +func (pc *PlannedChangeApplyable) PlannedChangeProto() (*stacks.PlannedChange, error) { + var raw anypb.Any + err := anypb.MarshalFrom(&raw, &tfstackdata1.PlanApplyable{ + Applyable: pc.Applyable, + }, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{ + PlanApplyable: pc.Applyable, + }, + }, + }, + }, nil +} + +type PlannedChangeProviderFunctionResults struct { + Results []lang.FunctionResultHash +} + +var _ PlannedChange = (*PlannedChangeProviderFunctionResults)(nil) + +func (pc *PlannedChangeProviderFunctionResults) PlannedChangeProto() (*stacks.PlannedChange, error) { + var results tfstackdata1.FunctionResults + for _, result := range pc.Results { + results.FunctionResults = append(results.FunctionResults, &planproto.FunctionCallHash{ + Key: result.Key, + Result: result.Result, + }) + } + + var raw anypb.Any + err := anypb.MarshalFrom(&raw, &results, proto.MarshalOptions{}) + if err != nil { + return nil, err + } + + return &stacks.PlannedChange{ + Raw: []*anypb.Any{&raw}, + }, nil +} diff --git a/internal/stacks/stackplan/planned_change_test.go b/internal/stacks/stackplan/planned_change_test.go new file mode 100644 index 0000000000..72f719b5ac --- /dev/null +++ b/internal/stacks/stackplan/planned_change_test.go @@ -0,0 +1,973 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackplan + +import ( + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-version" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" +) + +func TestPlannedChangeAsProto(t *testing.T) { + emptyObjectForPlan, err := plans.NewDynamicValue(cty.EmptyObjectVal, cty.EmptyObject) + if err != nil { + t.Fatal(err) + } + nonEmptyType := cty.Map(cty.String) + beforeObjectForPlan, err := plans.NewDynamicValue(cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), nonEmptyType) + if err != nil { + t.Fatal(err) + } + afterObjectForPlan, err := plans.NewDynamicValue(cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("baz"), + }), nonEmptyType) + if err != nil { + t.Fatal(err) + } + nullObjectForPlan, err := plans.NewDynamicValue(cty.NullVal(cty.EmptyObject), cty.EmptyObject) + if err != nil { + t.Fatal(err) + } + fakePlanTimestamp, err := time.Parse(time.RFC3339, "2017-03-27T10:00:00-08:00") + if err != nil { + t.Fatal(err) + } + + tests := map[string]struct { + Receiver PlannedChange + Want *stacks.PlannedChange + WantErr string + }{ + "header": { + Receiver: &PlannedChangeHeader{ + TerraformVersion: version.Must(version.NewSemver("1.2.3-beta4")), + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanHeader{ + TerraformVersion: "1.2.3-beta4", + }), + }, + }, + }, + "applyable true": { + Receiver: &PlannedChangeApplyable{ + Applyable: true, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanApplyable{ + Applyable: true, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{ + PlanApplyable: true, + }, + }, + }, + }, + }, + "applyable false": { + Receiver: &PlannedChangeApplyable{ + Applyable: false, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanApplyable{ + // false is the default + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_PlanApplyable{ + PlanApplyable: false, + }, + }, + }, + }, + }, + "component instance create": { + Receiver: &PlannedChangeComponentInstance{ + Addr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + }, + }, + Action: plans.Create, + PlanTimestamp: fakePlanTimestamp, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ + ComponentInstanceAddr: "component.foo", + PlanTimestamp: "2017-03-27T10:00:00-08:00", + PlannedAction: planproto.Action_CREATE, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ + ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.foo", + ComponentInstanceAddr: "component.foo", + }, + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + }, + }, + }, + }, + }, + }, + "component instance noop": { + Receiver: &PlannedChangeComponentInstance{ + Addr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("bar"), + }, + }, + Action: plans.NoOp, + PlanTimestamp: fakePlanTimestamp, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ + ComponentInstanceAddr: `component.foo["bar"]`, + PlanTimestamp: "2017-03-27T10:00:00-08:00", + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ + ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ + Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "component.foo", + ComponentInstanceAddr: `component.foo["bar"]`, + }, + }, + }, + }, + }, + }, + }, + "component instance delete": { + Receiver: &PlannedChangeComponentInstance{ + Addr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + }, + }, + Action: plans.Delete, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanComponentInstance{ + ComponentInstanceAddr: `stack.a["boop"].component.foo`, + PlannedAction: planproto.Action_DELETE, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ComponentInstancePlanned{ + ComponentInstancePlanned: &stacks.PlannedChange_ComponentInstance{ + Addr: &stacks.ComponentInstanceInStackAddr{ + ComponentAddr: "stack.a.component.foo", + ComponentInstanceAddr: `stack.a["boop"].component.foo`, + }, + Actions: []stacks.ChangeType{stacks.ChangeType_DELETE}, + }, + }, + }, + }, + }, + }, + "resource instance deferred": { + Receiver: &PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + DeposedKey: addrs.DeposedKey("aaaaaaaa"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + DeposedKey: addrs.DeposedKey("aaaaaaaa"), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: nullObjectForPlan, + After: emptyObjectForPlan, + }, + }, + }, + DeferredReason: providers.DeferredReasonResourceConfigUnknown, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanDeferredResourceInstanceChange{ + Change: &tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Change: &planproto.ResourceInstanceChange{ + Addr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + Change: &planproto.Change{ + Action: planproto.Action_CREATE, + Values: []*planproto.DynamicValue{ + {Msgpack: []byte{'\x80'}}, // zero-length mapping + }, + }, + Provider: `provider["example.com/thingers/thingy"]`, + }, + }, + Deferred: &planproto.Deferred{ + Reason: planproto.DeferredReason_RESOURCE_CONFIG_UNKNOWN, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstanceDeferred{ + ResourceInstanceDeferred: &stacks.PlannedChange_ResourceInstanceDeferred{ + ResourceInstance: &stacks.PlannedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + ActionReason: "ResourceInstanceChangeNoReason", + Index: &stacks.PlannedChange_ResourceInstance_Index{ + Value: &stacks.DynamicValue{ + Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 + }, + }, + ModuleAddr: `module.pizza["chicken"]`, + ResourceName: "wotsit", + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: []byte{'\xc0'}, // null + }, + New: &stacks.DynamicValue{ + Msgpack: []byte{'\x80'}, // zero-length mapping + }, + }, + }, + Deferred: &stacks.Deferred{ + Reason: stacks.Deferred_RESOURCE_CONFIG_UNKNOWN, + }, + }, + }, + }, + }, + }, + }, + "resource instance planned create": { + Receiver: &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + DeposedKey: addrs.DeposedKey("aaaaaaaa"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + DeposedKey: addrs.DeposedKey("aaaaaaaa"), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: nullObjectForPlan, + After: emptyObjectForPlan, + }, + }, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Change: &planproto.ResourceInstanceChange{ + Addr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + Change: &planproto.Change{ + Action: planproto.Action_CREATE, + Values: []*planproto.DynamicValue{ + {Msgpack: []byte{'\x80'}}, // zero-length mapping + }, + }, + Provider: `provider["example.com/thingers/thingy"]`, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ + ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + ActionReason: "ResourceInstanceChangeNoReason", + Index: &stacks.PlannedChange_ResourceInstance_Index{ + Value: &stacks.DynamicValue{ + Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 + }, + }, + ModuleAddr: `module.pizza["chicken"]`, + ResourceName: "wotsit", + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: []byte{'\xc0'}, // null + }, + New: &stacks.DynamicValue{ + Msgpack: []byte{'\x80'}, // zero-length mapping + }, + }, + }, + }, + }, + }, + }, + }, + "resource instance planned replace": { + Receiver: &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + DeposedKey: addrs.DeposedKey("aaaaaaaa"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + DeposedKey: addrs.DeposedKey("aaaaaaaa"), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.DeleteThenCreate, + Before: beforeObjectForPlan, + After: afterObjectForPlan, + }, + RequiredReplace: cty.NewPathSet(cty.GetAttrPath("foo")), + }, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Change: &planproto.ResourceInstanceChange{ + Addr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + Change: &planproto.Change{ + Action: planproto.Action_DELETE_THEN_CREATE, + Values: []*planproto.DynamicValue{ + {Msgpack: []byte("\x81\xa3foo\xa3bar")}, + {Msgpack: []byte("\x81\xa3foo\xa3baz")}, + }, + }, + Provider: `provider["example.com/thingers/thingy"]`, + RequiredReplace: []*planproto.Path{ + { + Steps: []*planproto.Path_Step{ + { + Selector: &planproto.Path_Step_AttributeName{AttributeName: "foo"}, + }, + }, + }, + }, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ + ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + DeposedKey: "aaaaaaaa", + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + Actions: []stacks.ChangeType{stacks.ChangeType_DELETE, stacks.ChangeType_CREATE}, + ActionReason: "ResourceInstanceChangeNoReason", + Index: &stacks.PlannedChange_ResourceInstance_Index{ + Value: &stacks.DynamicValue{ + Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 + }, + }, + ModuleAddr: `module.pizza["chicken"]`, + ResourceName: "wotsit", + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: []byte("\x81\xa3foo\xa3bar"), + }, + New: &stacks.DynamicValue{ + Msgpack: []byte("\x81\xa3foo\xa3baz"), + }, + }, + ReplacePaths: []*stacks.AttributePath{ + { + Steps: []*stacks.AttributePath_Step{ + { + Selector: &stacks.AttributePath_Step_AttributeName{ + AttributeName: "foo", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "resource instance planned import": { + Receiver: &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: emptyObjectForPlan, + After: emptyObjectForPlan, + Importing: &plans.ImportingSrc{ + ID: "bbbbbbb", + }, + }, + }, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Change: &planproto.ResourceInstanceChange{ + Addr: `module.pizza["chicken"].thingy.wotsit[1]`, + Change: &planproto.Change{ + Action: planproto.Action_NOOP, + Values: []*planproto.DynamicValue{ + {Msgpack: []byte{'\x80'}}, // zero-length mapping + }, + Importing: &planproto.Importing{ + Id: "bbbbbbb", + }, + }, + Provider: `provider["example.com/thingers/thingy"]`, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ + ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, + ActionReason: "ResourceInstanceChangeNoReason", + Index: &stacks.PlannedChange_ResourceInstance_Index{ + Value: &stacks.DynamicValue{ + Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 + }, + }, + ModuleAddr: `module.pizza["chicken"]`, + ResourceName: "wotsit", + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: []byte{'\x80'}, // zero-length mapping + }, + New: &stacks.DynamicValue{ + Msgpack: []byte{'\x80'}, // zero-length mapping + }, + }, + Imported: &stacks.PlannedChange_ResourceInstance_Imported{ + ImportId: "bbbbbbb", + }, + }, + }, + }, + }, + }, + }, + "resource instance planned moved": { + Receiver: &PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + PrevRunAddr: addrs.AbsResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "wotsit", + }.Instance(addrs.NoKey), + Module: addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: emptyObjectForPlan, + After: emptyObjectForPlan, + }, + }, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanResourceInstanceChangePlanned{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Change: &planproto.ResourceInstanceChange{ + Addr: `module.pizza["chicken"].thingy.wotsit[1]`, + PrevRunAddr: `module.pizza["chicken"].thingy.wotsit`, + Change: &planproto.Change{ + Action: planproto.Action_NOOP, + Values: []*planproto.DynamicValue{ + {Msgpack: []byte{'\x80'}}, // zero-length mapping + }, + }, + Provider: `provider["example.com/thingers/thingy"]`, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_ResourceInstancePlanned{ + ResourceInstancePlanned: &stacks.PlannedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit[1]`, + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + Actions: []stacks.ChangeType{stacks.ChangeType_NOOP}, + ActionReason: "ResourceInstanceChangeNoReason", + Index: &stacks.PlannedChange_ResourceInstance_Index{ + Value: &stacks.DynamicValue{ + Msgpack: []byte{0x92, 0xc4, 0x08, 0x22, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x22, 0x01}, // 1 + }, + }, + ModuleAddr: `module.pizza["chicken"]`, + ResourceName: "wotsit", + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: []byte{'\x80'}, // zero-length mapping + }, + New: &stacks.DynamicValue{ + Msgpack: []byte{'\x80'}, // zero-length mapping + }, + }, + Moved: &stacks.PlannedChange_ResourceInstance_Moved{ + PrevAddr: &stacks.ResourceInstanceInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.wotsit`, + }, + }, + }, + }, + }, + }, + }, + }, + "output value updated": { + Receiver: &PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "thingy_id"}, + Action: plans.Update, + + // NOTE: This is a bit unrealistic since we're reporting an + // update but there's no difference between these two values. + // In a real planned change this situation would be a "no-op". + Before: cty.EmptyObjectVal, + After: cty.EmptyObjectVal, + }, + Want: &stacks.PlannedChange{ + // Output value changes don't generate any raw representation; + // the diff is only for the benefit of the operator and + // other subsystems operating on their behalf. + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_OutputValuePlanned{ + OutputValuePlanned: &stacks.PlannedChange_OutputValue{ + Name: "thingy_id", + Actions: []stacks.ChangeType{stacks.ChangeType_UPDATE}, + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.EmptyObjectVal), + }, + New: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.EmptyObjectVal), + }, + }, + }, + }, + }, + }, + }, + }, + "create sensitive root input variable": { + Receiver: &PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "thingy_id"}, + Action: plans.Create, + Before: cty.NullVal(cty.String), + After: cty.StringVal("boop").Mark(marks.Sensitive), + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ + Name: "thingy_id", + Value: &tfstackdata1.DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"), + }, + SensitivePaths: []*planproto.Path{ + { + Steps: make([]*planproto.Path_Step, 0), // no steps as it is the root value + }, + }, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ + InputVariablePlanned: &stacks.PlannedChange_InputVariable{ + Name: "thingy_id", + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), + }, + New: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.StringVal("boop")), + Sensitive: []*stacks.AttributePath{ + { + Steps: make([]*stacks.AttributePath_Step, 0), // no steps as it is the root value + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + "ephemeral root input variable": { + Receiver: &PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "thingy_id"}, + Action: plans.Create, + Before: cty.NullVal(cty.String), + After: cty.StringVal("boop").Mark(marks.Ephemeral), + }, + WantErr: "failed to encode after planned input variable var.thingy_id: : unhandled value marks cty.NewValueMarks(marks.Ephemeral) (this is a bug in Terraform)", // Ephemeral values should never make it this far. + }, + "update root input variable": { + Receiver: &PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "thingy_id"}, + Action: plans.Update, + Before: cty.StringVal("beep"), + After: cty.StringVal("boop"), + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ + Name: "thingy_id", + Value: &tfstackdata1.DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: []byte("\x92\xc4\b\"string\"\xa4boop"), + }, + }, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ + InputVariablePlanned: &stacks.PlannedChange_InputVariable{ + Name: "thingy_id", + Actions: []stacks.ChangeType{stacks.ChangeType_UPDATE}, + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.StringVal("beep")), + }, + New: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.StringVal("boop")), + }, + }, + }, + }, + }, + }, + }, + }, + "root input variable that must be re-supplied during apply": { + Receiver: &PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "thingy_id"}, + Action: plans.Create, + Before: cty.NullVal(cty.String), + After: cty.NullVal(cty.String), + RequiredOnApply: true, + }, + Want: &stacks.PlannedChange{ + Raw: []*anypb.Any{ + mustMarshalAnyPb(&tfstackdata1.PlanRootInputValue{ + Name: "thingy_id", + Value: &tfstackdata1.DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), + }, + }, + RequiredOnApply: true, + }), + }, + Descriptions: []*stacks.PlannedChange_ChangeDescription{ + { + Description: &stacks.PlannedChange_ChangeDescription_InputVariablePlanned{ + InputVariablePlanned: &stacks.PlannedChange_InputVariable{ + Name: "thingy_id", + Actions: []stacks.ChangeType{stacks.ChangeType_CREATE}, + Values: &stacks.DynamicValueChange{ + Old: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), + }, + New: &stacks.DynamicValue{ + Msgpack: mustMsgPack(t, cty.NullVal(cty.String)), + }, + }, + RequiredDuringApply: true, + }, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := test.Receiver.PlannedChangeProto() + if len(test.WantErr) > 0 { + if diff := cmp.Diff(test.WantErr, err.Error()); diff != "" { + t.Errorf("wrong error\n%s", diff) + } + if got != nil { + t.Errorf("unexpected result: %v", got) + } + return + } + + if err != nil { + // All errors this can generate are caused by bugs in Terraform + // because we're serializing content that we created, and so + // there are no _expected_ error cases. + t.Fatal(err) + } + if diff := cmp.Diff(test.Want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + +func mustMarshalAnyPb(msg proto.Message) *anypb.Any { + var ret anypb.Any + err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) + if err != nil { + panic(err) + } + return &ret +} + +func mustMsgPack(t *testing.T, value cty.Value) []byte { + data, err := msgpack.Marshal(value, cty.DynamicPseudoType) + if err != nil { + t.Fatal(err) + } + return data +} diff --git a/internal/stacks/stackruntime/apply.go b/internal/stacks/stackruntime/apply.go new file mode 100644 index 0000000000..a6a53d0768 --- /dev/null +++ b/internal/stacks/stackruntime/apply.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "fmt" + "sync/atomic" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Apply performs the changes described in a previously-generated plan, +// aiming to make the real system converge with the desired state and +// then emit a series of patches that the caller must make to the +// current state to represent what has changed. +// +// Apply does not return a result directly because it emits results in a +// streaming fashion using channels provided in the given [ApplyResponse]. +// +// Callers must not modify any values reachable directly or indirectly +// through resp after passing it to this function, aside from the implicit +// modifications to the internal state of channels caused by reading them. +func Apply(ctx context.Context, req *ApplyRequest, resp *ApplyResponse) { + resp.Complete = false // We'll reset this to true only if we actually succeed + + var seenAnyErrors atomic.Bool + outp := stackeval.ApplyOutput{ + AnnounceAppliedChange: func(ctx context.Context, change stackstate.AppliedChange) { + resp.AppliedChanges <- change + }, + AnnounceDiagnostics: func(ctx context.Context, diags tfdiags.Diagnostics) { + for _, diag := range diags { + if diag.Severity() == tfdiags.Error { + seenAnyErrors.Store(true) // never becomes false again + } + resp.Diagnostics <- diag + } + }, + } + + // Whatever return path we take, we must close our channels to allow + // a caller to see that the operation is complete. + defer func() { + close(resp.Diagnostics) + close(resp.AppliedChanges) // MUST be the last channel to close + }() + + main, err := stackeval.ApplyPlan( + ctx, + req.Config, + req.Plan, + stackeval.ApplyOpts{ + InputVariableValues: req.InputValues, + ProviderFactories: req.ProviderFactories, + ExperimentsAllowed: req.ExperimentsAllowed, + DependencyLocks: req.DependencyLocks, + }, + outp, + ) + if err != nil { + // An error here means that the apply wasn't even able to _start_, + // typically because the request itself was invalid. We'll announce + // that as a diagnostic and then halt, though if we get here then + // it's most likely a bug in the caller rather than end-user error. + resp.Diagnostics <- tfdiags.Sourceless( + tfdiags.Error, + "Invalid apply request", + fmt.Sprintf("Cannot begin the apply phase: %s.", err), + ) + return + } + + if !seenAnyErrors.Load() { + resp.Complete = true + } + + cleanupDiags := main.DoCleanup(ctx) + for _, diag := range cleanupDiags { + // cleanup diagnostics don't stop the apply from being "complete", + // since this should include only transient operational errors such + // as failing to terminate a provider plugin. + resp.Diagnostics <- diag + } + +} + +// ApplyRequest represents the inputs to an [Apply] call. +type ApplyRequest struct { + Config *stackconfig.Config + Plan *stackplan.Plan + + InputValues map[stackaddrs.InputVariable]ExternalInputValue + ProviderFactories map[addrs.Provider]providers.Factory + + ExperimentsAllowed bool + DependencyLocks depsfile.Locks +} + +// ApplyResponse is used by [Apply] to describe the results of applying. +// +// [Apply] produces streaming results throughout its execution, and so it +// communicates with the caller by writing to provided channels during its work +// and then modifying other fields in this structure before returning. Callers +// MUST NOT access any non-channel fields of ApplyResponse until the +// AppliedChanges channel has been closed to signal the completion of the +// apply process. +type ApplyResponse struct { + // [Apply] will set this field to true if the apply ran to completion + // without encountering any errors, or set this to false if not. + // + // A caller might react to Complete: true by creating one follow-up plan + // just to confirm that everything has converged and then, if so, consider + // all of the configuration versions that contributed to this plan to now + // be converged. If unsuccessful, none of the contributing configurations + // are known to be converged and the operator will need to decide whether + // to immediately try creating a new plan (if they think the error was + // transient) or push a new configuration update to correct the problem. + // + // If this field is false after applying is complete then it's likely that + // at least some of the planned side-effects already occurred, and so + // it's important to still handle anything that was written to the + // AppliedChanges channel to partially update the state with the subset + // of changes that were completed. + // + // The initial value of this field is ignored; there's no reason to set + // it to anything other than the zero value. + Complete bool + + // AppliedChanges is the channel that will be sent each individual + // applied change, in no predictable order, during the apply + // operation. + // + // Callers MUST provide a non-nil channel and read from it from + // another Goroutine throughout the apply operation, or apply + // progress will be blocked. Callers that read slowly should provide + // a buffered channel to reduce the backpressure they exert on the + // apply process. + // + // The apply operation will close this channel before it returns. + // AppliedChanges is guaranteed to be the last channel to close + // (i.e. after Diagnostics is closed) so callers can use the close + // signal of this channel alone to mark that the apply process is + // over, but if Diagnostics is a buffered channel they must take + // care to deplete its buffer afterwards to avoid losing diagnostics + // delivered near the end of the apply process. + AppliedChanges chan<- stackstate.AppliedChange + + // Diagnostics is the channel that will be sent any diagnostics + // that arise during the apply process, in no particular order. + // + // In particular note that there's no guarantee that the diagnostics + // for applying changes to a particular object will be emitted in close + // proximity to an AppliedChanges write for that same object. Diagnostics + // and applied changes are totally decoupled, since diagnostics might be + // collected up and emitted later as a large batch if the runtime + // needs to perform aggregate operations such as deduplication on + // the diagnostics before exposing them. + // + // Callers MUST provide a non-nil channel and read from it from + // another Goroutine throughout the plan operation, or apply + // progress will be blocked. Callers that read slowly should provide + // a buffered channel to reduce the backpressure they exert on the + // apply process. + // + // The apply operation will close this channel before it returns, but + // callers should use the close event of AppliedChanges as the definitive + // signal that planning is complete. + Diagnostics chan<- tfdiags.Diagnostic +} diff --git a/internal/stacks/stackruntime/apply_destroy_test.go b/internal/stacks/stackruntime/apply_destroy_test.go new file mode 100644 index 0000000000..0632bd1ebb --- /dev/null +++ b/internal/stacks/stackruntime/apply_destroy_test.go @@ -0,0 +1,1647 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "path" + "strconv" + "testing" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +func TestApplyDestroy(t *testing.T) { + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") + if err != nil { + t.Fatal(err) + } + + tcs := map[string]struct { + path string + description string + state *stackstate.State + store *stacks_testing_provider.ResourceStore + mutators []func(*stacks_testing_provider.ResourceStore, TestContext) TestContext + cycles []TestCycle + }{ + "inputs-and-outputs": { + path: "component-input-output", + state: stackstate.NewStateBuilder(). + AddInput("value", cty.StringVal("foo")). + AddOutput("value", cty.StringVal("foo")). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.Delete, + Before: cty.StringVal("foo"), + After: cty.NullVal(cty.String), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + DeleteOnApply: true, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Value: cty.NilVal, // destroyed + }, + }, + }, + }, + }, + "missing-resource": { + path: path.Join("with-single-input", "valid"), + description: "tests what happens when a resource is in state but not in the provider", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). + AddInputVariable("id", cty.StringVal("e84b59f2")). + AddInputVariable("value", cty.StringVal("hello"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "e84b59f2", + "value": "hello", + }), + Status: states.ObjectReady, + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "input": cty.StringVal("hello"), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + // The resource that was in state but not in the data store should still + // be included to be destroyed. + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, // We should be removing this from the state file. + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.NilVal, // destroyed + }, + }, + }, + }, + }, + "datasource-in-state": { + path: "with-data-source", + description: "tests that we emit removal notices for data sources", + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("foo", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "value": cty.StringVal("hello"), + })).Build(), + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.data.testing_data_source.missing")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "e84b59f2", + "value": "hello", + }), + Status: states.ObjectReady, + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("foo"), + "resource": cty.StringVal("bar"), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + // This is a bit of a quirk of the system, this wasn't in the state + // file before so we don't need to emit this. But since Terraform + // pushes data sources into the refresh state, it's very difficult to + // tell the difference between this kind of change that doesn't need to + // be emitted, and the next change that does need to be emitted. It's + // better to emit both than to miss one, and emitting this doesn't + // actually harm anything. + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: providers.Schema{}, + NewStateSrc: nil, // deleted + }, + + // This was in the state file, so we're emitting the destroy notice. + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.missing"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: providers.Schema{}, + NewStateSrc: nil, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("resource"), + Value: cty.NilVal, // destroyed + }, + }, + }, + }, + }, + "orphaned-data-sources-removed": { + path: "with-data-source", + description: "tests that we emit removal notices for data sources that are no longer in the configuration", + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("foo", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "value": cty.StringVal("hello"), + })).Build(), + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.data.testing_data_source.missing")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "e84b59f2", + "value": "hello", + }), + Status: states.ObjectReady, + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("foo"), + "resource": cty.StringVal("bar"), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("foo"), + mustInputVariable("resource"): cty.StringVal("bar"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + AttrSensitivePaths: make([]cty.Path, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingDataSourceSchema, + }, + // This data source should be removed from the state file as it is no + // longer in the configuration. + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.missing"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: providers.Schema{}, + NewStateSrc: nil, // deleted + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "bar", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{ + mustAbsResourceInstance("data.testing_data_source.data").ConfigResource(), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("resource"), + Value: cty.StringVal("bar"), + }, + }, + }, + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("foo"), + "resource": cty.StringVal("bar"), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.data.testing_data_source.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: providers.Schema{}, + NewStateSrc: nil, // deleted + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: providers.Schema{}, + NewStateSrc: nil, // deleted + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("resource"), + Value: cty.NilVal, // destroyed + }, + }, + }, + }, + }, + "dependent-resources": { + path: "dependent-component", + description: "test the order of operations during create and destroy", + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + Dependencies: collections.NewSet(mustAbsComponent("component.valid")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("dependent"), + mustInputVariable("requirements"): cty.SetVal([]cty.Value{ + cty.StringVal("valid"), + }), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_blocked_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "dependent", + "value": nil, + "required_resources": []interface{}{"valid"}, + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.BlockedResourceSchema, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.valid"), + ComponentInstanceAddr: mustAbsComponentInstance("component.valid"), + Dependents: collections.NewSet(mustAbsComponent("component.self")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("valid"), + mustInputVariable("input"): cty.StringVal("resource"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.valid.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "valid", + "value": "resource", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_blocked_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.valid"), + ComponentInstanceAddr: mustAbsComponentInstance("component.valid"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.valid.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "failed-destroy": { + path: "failed-component", + description: "tests what happens if a component fails to destroy", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_failed_resource.data")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "failed", + "value": "resource", + "fail_plan": false, + "fail_apply": true, + }), + Status: states.ObjectReady, + }). + SetProviderAddr(mustDefaultRootProvider("testing"))). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("failed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("failed"), + "value": cty.StringVal("resource"), + "fail_plan": cty.False, + "fail_apply": cty.True, + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("failed"), + mustInputVariable("input"): cty.StringVal("resource"), + mustInputVariable("fail_plan"): cty.False, + mustInputVariable("fail_apply"): cty.False, + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "failed", + "value": "resource", + "fail_plan": false, + "fail_apply": true, + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.FailedResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("fail_apply"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("fail_plan"), + Value: cty.NilVal, // destroyed + }, + }, + wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failedResource error", + Detail: "failed during apply", + }) + }), + }, + }, + }, + "destroy-after-failed-apply": { + path: path.Join("with-single-input", "failed-child"), + description: "tests destroying when state is only partially applied", + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + Dependencies: collections.NewSet(mustAbsComponent("component.self")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.NullVal(cty.String), + mustInputVariable("input"): cty.StringVal("child"), + mustInputVariable("fail_plan"): cty.NullVal(cty.Bool), + mustInputVariable("fail_apply"): cty.True, + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_failed_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + Dependents: collections.NewSet(mustAbsComponent("component.child")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("self"), + mustInputVariable("input"): cty.StringVal("value"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "self", + "value": "value", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "failedResource error", + Detail: "failed during apply", + }) + }), + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "destroy-after-deferred-apply": { + path: "deferred-dependent", + description: "tests what happens when a destroy plan is applied after components have been deferred", + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.deferred"), + ComponentInstanceAddr: mustAbsComponentInstance("component.deferred"), + Dependencies: collections.NewSet(mustAbsComponent("component.valid")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("deferred"), + mustInputVariable("defer"): cty.True, + }, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.valid"), + ComponentInstanceAddr: mustAbsComponentInstance("component.valid"), + Dependents: collections.NewSet(mustAbsComponent("component.deferred")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("valid"), + mustInputVariable("input"): cty.StringVal("valid"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.valid.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "valid", + "value": "valid", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.deferred"), + ComponentInstanceAddr: mustAbsComponentInstance("component.deferred"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.valid"), + ComponentInstanceAddr: mustAbsComponentInstance("component.valid"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.valid.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "deferred-destroy": { + path: "deferred-dependent", + description: "tests what happens when a destroy operation is deferred", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.valid")). + AddDependent(mustAbsComponent("component.deferred"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.valid.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "valid", + "value": "valid", + }), + Status: states.ObjectReady, + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.deferred")). + AddDependency(mustAbsComponent("component.valid"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.deferred.testing_deferred_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "deferred", + "value": nil, + "deferred": true, + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("valid", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("valid"), + "value": cty.StringVal("valid"), + })). + AddResource("deferred", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("deferred"), + "value": cty.NullVal(cty.String), + "deferred": cty.True, + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.deferred"), + Action: plans.Delete, + Mode: plans.DestroyMode, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](mustAbsComponent("component.valid")), + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("deferred")), + "defer": mustPlanDynamicValueDynamicType(cty.True), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "defer": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.deferred.testing_deferred_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_deferred_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_deferred_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("deferred"), + "value": cty.NullVal(cty.String), + "deferred": cty.True, + })), + After: mustPlanDynamicValue(cty.NullVal(cty.String)), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "deferred", + "value": nil, + "deferred": true, + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.DeferredResourceSchema, + }, + DeferredReason: "resource_config_unknown", + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.valid"), + PlanApplyable: false, + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("valid")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("valid")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.valid.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("valid"), + "value": cty.StringVal("valid"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.String)), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "valid", + "value": "valid", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: "deferred_prereq", + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.deferred"), + ComponentInstanceAddr: mustAbsComponentInstance("component.deferred"), + Dependencies: collections.NewSet(mustAbsComponent("component.valid")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("deferred"), + mustInputVariable("defer"): cty.True, + }, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.valid"), + ComponentInstanceAddr: mustAbsComponentInstance("component.valid"), + Dependents: collections.NewSet(mustAbsComponent("component.deferred")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("valid"), + mustInputVariable("input"): cty.StringVal("valid"), + }, + }, + }, + }, + }, + }, + "destroy-with-input-dependency": { + path: path.Join("with-single-input-and-output", "input-dependency"), + description: "tests destroy operations with input dependencies", + cycles: []TestCycle{ + { + // Just create everything normally, and don't validate it. + planMode: plans.NormalMode, + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "destroy-with-provider-dependency": { + path: path.Join("with-single-input-and-output", "provider-dependency"), + description: "tests destroy operations with provider dependencies", + cycles: []TestCycle{ + { + // Just create everything normally, and don't validate it. + planMode: plans.NormalMode, + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "destroy-with-for-each-dependency": { + path: path.Join("with-single-input-and-output", "for-each-dependency"), + description: "tests destroy operations with for-each dependencies", + cycles: []TestCycle{ + { + // Just create everything normally, and don't validate it. + planMode: plans.NormalMode, + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child[\"a\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child[\"a\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent[\"a\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent[\"a\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "destroy-with-provider-req": { + path: "auth-provider-w-data", + mutators: []func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext{ + func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext { + store.Set("credentials", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("credentials"), + "value": cty.StringVal("zero"), + })) + testContext.providers[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) { + provider := stacks_testing_provider.NewProviderWithData(t, store) + provider.Authentication = "zero" + return provider, nil + } + return testContext + }, + func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext { + store.Set("credentials", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("credentials"), + "value": cty.StringVal("one"), + })) + testContext.providers[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) { + provider := stacks_testing_provider.NewProviderWithData(t, store) + provider.Authentication = "one" // So we must reload the data source in order to authenticate. + return provider, nil + } + return testContext + }, + }, + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + }, + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.create"), + ComponentInstanceAddr: mustAbsComponentInstance("component.create"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.create.testing_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.load"), + ComponentInstanceAddr: mustAbsComponentInstance("component.load"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.load.data.testing_data_source.credentials"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "destroy-with-provider-req-and-removed": { + path: path.Join("auth-provider-w-data", "removed"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.load")). + AddDependent(mustAbsComponent("component.create")). + AddOutputValue("credentials", cty.StringVal("wrong"))). // must reload the credentials + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.create")). + AddDependency(mustAbsComponent("component.load"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.create.testing_resource.resource")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "resource", + "value": nil, + }), + Status: states.ObjectReady, + }). + SetProviderAddr(mustDefaultRootProvider("testing"))). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder().AddResource("credentials", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("credentials"), + // we have the wrong value in state, so this correct value must + // be loaded for this test to work. + "value": cty.StringVal("authn"), + })).Build(), + mutators: []func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext{ + func(store *stacks_testing_provider.ResourceStore, testContext TestContext) TestContext { + testContext.providers[addrs.NewDefaultProvider("testing")] = func() (providers.Interface, error) { + provider := stacks_testing_provider.NewProviderWithData(t, store) + provider.Authentication = "authn" // So we must reload the data source in order to authenticate. + return provider, nil + } + return testContext + }, + }, + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.create"), + ComponentInstanceAddr: mustAbsComponentInstance("component.create"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.create.testing_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.load"), + ComponentInstanceAddr: mustAbsComponentInstance("component.load"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.load.data.testing_data_source.credentials"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "empty-destroy-with-data-source": { + path: path.Join("with-data-source", "dependent"), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("foo"), + }, + // deliberately empty, as we expect no changes from an + // empty state. + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.data"), + ComponentInstanceAddr: mustAbsComponentInstance("component.data"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + }, + }, + }, + }, + }, + "destroy after manual removal": { + path: "removed-offline", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.parent")). + AddDependent(mustAbsComponent("component.child")). + AddOutputValue("value", cty.StringVal("hello"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.parent.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "parent", + "value": "hello", + }), + Status: states.ObjectReady, + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.child")). + AddDependency(mustAbsComponent("component.parent")). + AddInputVariable("value", cty.StringVal("hello"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.child.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "child", + "value": "hello", + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("child", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("child"), + "value": cty.StringVal("hello"), + })).Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.child"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanComplete: true, + PlanApplyable: true, + RequiredComponents: collections.NewSet(mustAbsComponent("component.parent")), + PlannedInputValues: map[string]plans.DynamicValue{ + "value": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "value": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.resource"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.resource"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.resource"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("child"), + "value": cty.StringVal("hello"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "child", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.parent"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanComplete: true, + PlanApplyable: false, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "value": cty.UnknownVal(cty.String), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "partial destroy recovery": { + path: "component-chain", + description: "this test simulates a partial destroy recovery", + state: stackstate.NewStateBuilder(). + // we only have data for the first component, indicating that + // the second and third components were destroyed but not the + // first one for some reason + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")). + AddDependent(mustAbsComponent("component.two")). + AddInputVariable("id", cty.StringVal("one")). + AddInputVariable("value", cty.StringVal("foo")). + AddOutputValue("value", cty.StringVal("foo"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "one", + "value": "foo", + }), + Status: states.ObjectReady, + })). + AddInput("value", cty.StringVal("foo")). + AddOutput("value", cty.StringVal("foo")). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("one", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("one"), + "value": cty.StringVal("foo"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.one"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanComplete: true, + PlanApplyable: true, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("one")), + "value": mustPlanDynamicValueDynamicType(cty.StringVal("foo")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "value": nil, + }, + PlannedOutputValues: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("one"), + "value": cty.StringVal("foo"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "one", + "value": "foo", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.three"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanComplete: true, + PlanApplyable: true, + RequiredComponents: collections.NewSet(mustAbsComponent("component.two")), + PlannedOutputValues: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.two"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanComplete: true, + PlanApplyable: true, + RequiredComponents: collections.NewSet(mustAbsComponent("component.one")), + PlannedOutputValues: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.Delete, + Before: cty.StringVal("foo"), + After: cty.NullVal(cty.String), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + DeleteOnApply: true, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.one"), + ComponentInstanceAddr: mustAbsComponentInstance("component.one"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.three"), + ComponentInstanceAddr: mustAbsComponentInstance("component.three"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.two"), + ComponentInstanceAddr: mustAbsComponentInstance("component.two"), + }, + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + }, + }, + }, + }, + }, + "destroy-partial-state-with-module": { + path: "with-module", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). + AddInputVariable("id", cty.StringVal("self")). + AddInputVariable("input", cty.StringVal("self"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.outside")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "self", + "value": "self", + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("self", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("self"), + "value": cty.StringVal("self"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("self"), + "input": cty.StringVal("self"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanApplyable: true, + PlanComplete: true, + PlannedInputValues: map[string]plans.DynamicValue{ + "create": mustPlanDynamicValueDynamicType(cty.True), + "id": mustPlanDynamicValueDynamicType(cty.StringVal("self")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("self")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "create": nil, + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: new(states.CheckResults), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.outside"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.outside"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.outside"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("self"), + "value": cty.StringVal("self"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "self", + "value": "self", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("id"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("self"), + DeleteOnApply: true, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("input"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("self"), + DeleteOnApply: true, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.outside"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + }, + }, + }, + }, + }, + "destroy-partial-state": { + path: "destroy-partial-state", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.parent")). + AddDependent(mustAbsComponent("component.child"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.parent.testing_resource.primary")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "primary", + }), + Status: states.ObjectReady, + })). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.parent.testing_resource.secondary")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "secondary", + "value": "primary", + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("primary", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("primary"), + "value": cty.NullVal(cty.String), + })). + AddResource("secondary", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("secondary"), + "value": cty.StringVal("primary"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.DestroyMode, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.child"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanApplyable: true, + PlanComplete: true, + RequiredComponents: collections.NewSet(mustAbsComponent("component.parent")), + PlannedOutputValues: make(map[string]cty.Value), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.parent"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlanApplyable: true, + PlanComplete: true, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "deleted_id": cty.UnknownVal(cty.String), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.primary"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.primary"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.primary"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("primary"), + "value": cty.NullVal(cty.String), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "primary", + "value": nil, + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.secondary"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.secondary"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.secondary"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("secondary"), + "value": cty.StringVal("primary"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "secondary", + "value": "primary", + }), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{ + mustAbsResourceInstance("testing_resource.primary").ConfigResource(), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.child"), + ComponentInstanceAddr: mustAbsComponentInstance("component.child"), + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.primary"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.secondary"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + store := tc.store + if store == nil { + store = stacks_testing_provider.NewResourceStore() + } + + testContext := TestContext{ + timestamp: &fakePlanTimestamp, + config: loadMainBundleConfigForTest(t, tc.path), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, store), nil + }, + }, + dependencyLocks: *lock, + } + + state := tc.state + for ix, cycle := range tc.cycles { + + if tc.mutators != nil { + testContext = tc.mutators[ix](store, testContext) + } + + t.Run(strconv.FormatInt(int64(ix), 10), func(t *testing.T) { + var plan *stackplan.Plan + t.Run("plan", func(t *testing.T) { + plan = testContext.Plan(t, ctx, state, cycle) + }) + t.Run("apply", func(t *testing.T) { + state = testContext.Apply(t, ctx, plan, cycle) + }) + }) + } + + }) + } +} diff --git a/internal/stacks/stackruntime/apply_test.go b/internal/stacks/stackruntime/apply_test.go new file mode 100644 index 0000000000..ec2f600e69 --- /dev/null +++ b/internal/stacks/stackruntime/apply_test.go @@ -0,0 +1,4743 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "fmt" + "path" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +var changesCmpOpts = cmp.Options{ + ctydebug.CmpOptions, + cmpCollectionsSet, + cmpopts.IgnoreUnexported(addrs.InputVariable{}), + cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{}), +} + +// TestApply uses a generic framework for running apply integration tests +// against Stacks. Generally, new tests should be added into this function +// rather than copying the large amount of duplicate code from the other +// tests in this file. +// +// If you are editing other tests in this file, please consider moving them +// into this test function so they can reuse the shared setup and boilerplate +// code managing the boring parts of the test. +func TestApply(t *testing.T) { + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") + if err != nil { + t.Fatal(err) + } + + tcs := map[string]struct { + path string + state *stackstate.State + store *stacks_testing_provider.ResourceStore + cycles []TestCycle + }{ + "creating inputs and outputs": { + path: "component-input-output", + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Value: cty.StringVal("foo"), + }, + }, + }, + }, + }, + "updating inputs and outputs": { + path: "component-input-output", + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + }, + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("bar"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.Update, + Before: cty.StringVal("foo"), + After: cty.StringVal("bar"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.Update, + Before: cty.StringVal("foo"), + After: cty.StringVal("bar"), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.StringVal("bar"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Value: cty.StringVal("bar"), + }, + }, + }, + }, + }, + "updating inputs and outputs (noop)": { + path: "component-input-output", + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + }, + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.NoOp, + Before: cty.StringVal("foo"), + After: cty.StringVal("foo"), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Value: cty.StringVal("foo"), + }, + }, + }, + }, + }, + "deleting inputs and outputs": { + path: "component-input-output", + state: stackstate.NewStateBuilder(). + AddInput("removed", cty.StringVal("bar")). + AddOutput("removed", cty.StringVal("bar")). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "value": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("removed"), + Action: plans.Delete, + Before: cty.StringVal("bar"), + After: cty.NullVal(cty.DynamicPseudoType), + }, + &stackplan.PlannedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("removed"), + Action: plans.Delete, + Before: cty.StringVal("bar"), + After: cty.NullVal(cty.DynamicPseudoType), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("value"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("removed"), + }, + &stackstate.AppliedChangeOutputValue{ + Addr: mustStackOutputValue("value"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed"), + Value: cty.NilVal, // destroyed + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("value"), + Value: cty.StringVal("foo"), + }, + }, + }, + }, + }, + "checkable objects": { + path: "checkable-objects", + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Check block assertion failed", + Detail: `value must be 'baz'`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"), + Start: hcl.Pos{Line: 41, Column: 21, Byte: 716}, + End: hcl.Pos{Line: 41, Column: 57, Byte: 752}, + }, + }) + }), + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.single"), + ComponentInstanceAddr: mustAbsComponentInstance("component.single"), + OutputValues: map[addrs.OutputValue]cty.Value{ + addrs.OutputValue{Name: "foo"}: cty.StringVal("bar"), + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("foo"): cty.StringVal("bar"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "test", + "value": "bar", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Check block assertion failed", + Detail: `value must be 'baz'`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"), + Start: hcl.Pos{Line: 41, Column: 21, Byte: 716}, + End: hcl.Pos{Line: 41, Column: 57, Byte: 752}, + }, + }) + }), + }, + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.single"), + ComponentInstanceAddr: mustAbsComponentInstance("component.single"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.main"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + }, + }, + }, + "removed component": { + path: filepath.Join("with-single-input", "removed-component"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{}, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + }, + }, + }, + }, + "removed component instance": { + path: filepath.Join("with-single-input", "removed-component-instance"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + "removed": cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + // we're expecting the new component to be created + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"added\"]"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("added")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("added")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("added"), + "value": cty.StringVal("added"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"removed\"]"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"added\"]"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("added"), + mustInputVariable("input"): cty.StringVal("added"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "added", + "value": "added", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"removed\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed"), + Value: cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + }, + }, + }, + }, + "duplicate removed blocks": { + path: path.Join("with-single-input", "removed-component-duplicate"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"one\"]")). + AddInputVariable("id", cty.StringVal("one")). + AddInputVariable("input", cty.StringVal("one"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "one", + "value": "one", + }), + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"two\"]")). + AddInputVariable("id", cty.StringVal("two")). + AddInputVariable("input", cty.StringVal("two"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "two", + "value": "two", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("one", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("one"), + "value": cty.StringVal("one"), + })). + AddResource("two", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("two"), + "value": cty.StringVal("two"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "input": cty.SetValEmpty(cty.String), + "removed_one": cty.SetVal([]cty.Value{ + cty.StringVal("one"), + }), + "removed_two": cty.SetVal([]cty.Value{ + cty.StringVal("two"), + }), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"one\"]"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("one")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("one")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("one"), + "value": cty.StringVal("one"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "one", + "value": "one", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"two\"]"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("two")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("two")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("two"), + "value": cty.StringVal("two"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "two", + "value": "two", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed_one"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("one"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed_two"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("two"), + }), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"one\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"one\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"two\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"two\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.SetValEmpty(cty.String), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed_one"), + Value: cty.SetVal([]cty.Value{ + cty.StringVal("one"), + }), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed_two"), + Value: cty.SetVal([]cty.Value{ + cty.StringVal("two"), + }), + }, + }, + }, + }, + }, + "removed component instance direct": { + path: filepath.Join("with-single-input", "removed-component-instance-direct"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + // we're expecting the new component to be created + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"added\"]"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("added")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("added")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("added"), + "value": cty.StringVal("added"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"removed\"]"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"added\"]"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("added"), + mustInputVariable("input"): cty.StringVal("added"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "added", + "value": "added", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"removed\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + }, + }, + }, + }, + }, + "removed stack instance": { + path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"removed\"].component.self")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.simple[\"removed\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "input": cty.MapVal(map[string]cty.Value{ + "added": cty.StringVal("added"), + }), + "removed": cty.MapVal(map[string]cty.Value{ + "removed": cty.StringVal("removed"), + }), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("stack.simple[\"added\"].component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("stack.simple[\"added\"].component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("added"), + mustInputVariable("input"): cty.StringVal("added"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"added\"].component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "added", + "value": "added", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("stack.simple[\"removed\"].component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("stack.simple[\"removed\"].component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"removed\"].component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.MapVal(map[string]cty.Value{ + "added": cty.StringVal("added"), + }), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed"), + Value: cty.MapVal(map[string]cty.Value{ + "removed": cty.StringVal("removed"), + }), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed-direct"), + Value: cty.SetValEmpty(cty.String), + }, + }, + }, + }, + }, + "removed embedded dynamic component from stack": { + path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.for_each.component.self[\"removed\"]")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.for_each.component.self[\"removed\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "for_each_input": cty.MapVal(map[string]cty.Value{ + "added": cty.StringVal("added"), + }), + "for_each_removed": cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("stack.for_each.component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("stack.for_each.component.self[\"added\"]"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("added"), + mustInputVariable("input"): cty.StringVal("added"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.for_each.component.self[\"added\"].testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "added", + "value": "added", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("stack.for_each.component.self[\"removed\"]"), + ComponentInstanceAddr: mustAbsComponentInstance("stack.for_each.component.self[\"removed\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.for_each.component.self[\"removed\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("for_each_input"), + Value: cty.MapVal(map[string]cty.Value{ + "added": cty.StringVal("added"), + }), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("for_each_removed"), + Value: cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("simple_input"), + Value: cty.MapValEmpty(cty.String), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("simple_removed"), + Value: cty.SetValEmpty(cty.String), + }, + }, + }, + }, + }, + "removed embedded component relative": { + path: filepath.Join("with-single-input", "removed-component-from-stack"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.nested.component.self[\"foo\"]")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.nested.component.self[\"foo\"]"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("stack.nested.component.self[\"foo\"]"), + ComponentInstanceAddr: mustAbsComponentInstance("stack.nested.component.self[\"foo\"]"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.nested.component.self[\"foo\"].testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + }, + }, + }, + }, + "removed embedded component local": { + path: filepath.Join("with-single-input", "removed-embedded-component"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a.component.self")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.a.component.self"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("stack.a.component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("stack.a.component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.a.component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + }, + }, + }, + }, + "forgotten component": { + path: filepath.Join("with-single-input", "forgotten-component"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "destroy": cty.BoolVal(false), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Forget, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Forget, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Some objects will no longer be managed by Terraform", + `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: + - testing_resource.data + +After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`, + )) + }), + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + Schema: providers.Schema{}, + }, + }, + }, + }, + }, + "orphaned component": { + path: filepath.Join("with-single-input", "valid"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.orphan"))). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "id": cty.StringVal("foo"), + "input": cty.StringVal("bar"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstanceRemoved{ + // The orphaned component is just silently being removed. + Addr: mustAbsComponentInstance("component.orphan"), + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("foo")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("bar")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "value": cty.StringVal("bar"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "id", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "input", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("bar"), + }, + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + // The orphaned component is just silently being removed. + ComponentAddr: mustAbsComponent("component.orphan"), + ComponentInstanceAddr: mustAbsComponentInstance("component.orphan"), + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("foo"), + mustInputVariable("input"): cty.StringVal("bar"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "bar", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.StringVal("bar"), + }, + }, + }, + }, + }, + "forget with dependency": { + path: "forget_with_dependency", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")). + AddDependent(mustAbsComponent("component.two")). + AddInputVariable("value", cty.StringVal("bar")). + AddOutputValue("id", cty.StringVal("foo"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "bar", + }), + Status: states.ObjectReady, + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.two")). + AddDependency(mustAbsComponent("component.one")). + AddInputVariable("value", cty.StringVal("foo")). + AddOutputValue("id", cty.StringVal("baz"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.two.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "baz", + "value": "foo", + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("foo", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "value": cty.StringVal("bar"), + })). + AddResource("baz", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("baz"), + "value": cty.StringVal("foo"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + wantPlannedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: + - testing_resource.resource + +After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.one"), + ComponentInstanceAddr: mustAbsComponentInstance("component.one"), + OutputValues: map[addrs.OutputValue]cty.Value{ + addrs.OutputValue{Name: "id"}: cty.StringVal("foo"), + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + addrs.InputVariable{Name: "value"}: cty.StringVal("bar"), + }, + Dependents: collections.NewSet(mustAbsComponent("component.two")), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "bar", + }), + Status: states.ObjectReady, + AttrSensitivePaths: make([]cty.Path, 0), + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.two"), + ComponentInstanceAddr: mustAbsComponentInstance("component.two"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"), + NewStateSrc: nil, // Resource is forgotten + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + }, + }, + }, + }, + "forget with dependency on component to forget": { + path: "forget_with_dependency_to_forget", + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.one")). + AddDependent(mustAbsComponent("component.two")). + AddInputVariable("value", cty.StringVal("bar")). + AddOutputValue("id", cty.StringVal("foo"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.one.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "bar", + }), + Status: states.ObjectReady, + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.two")). + AddDependency(mustAbsComponent("component.one")). + AddInputVariable("value", cty.StringVal("foo")). + AddOutputValue("id", cty.StringVal("baz"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.two.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "baz", + "value": "foo", + }), + Status: states.ObjectReady, + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("foo", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + "value": cty.StringVal("bar"), + })). + AddResource("baz", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("baz"), + "value": cty.StringVal("foo"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + wantPlannedDiags: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: + - testing_resource.resource + +After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`), + tfdiags.Sourceless(tfdiags.Warning, "Some objects will no longer be managed by Terraform", `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: + - testing_resource.resource + +After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.one"), + ComponentInstanceAddr: mustAbsComponentInstance("component.one"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"), + NewStateSrc: nil, // Resource is forgotten + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.two"), + ComponentInstanceAddr: mustAbsComponentInstance("component.two"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"), + NewStateSrc: nil, // Resource is forgotten + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + }, + }, + }, + }, + "removed block with provider-to-component dep": { + path: path.Join("auth-provider-w-data", "removed"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.load")). + AddDependent(mustAbsComponent("component.create")). + AddOutputValue("credentials", cty.StringVal("wrong"))). // must reload the credentials + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.create")). + AddDependency(mustAbsComponent("component.load"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.create.testing_resource.resource")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "resource", + "value": nil, + }), + Status: states.ObjectReady, + }). + SetProviderAddr(mustDefaultRootProvider("testing"))). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder().AddResource("credentials", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("credentials"), + // we have the wrong value in state, so this correct value must + // be loaded for this test to work. + "value": cty.StringVal("authn"), + })).Build(), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.create"), + ComponentInstanceAddr: mustAbsComponentInstance("component.create"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.create.testing_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, // deleted + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.load"), + ComponentInstanceAddr: mustAbsComponentInstance("component.load"), + OutputValues: map[addrs.OutputValue]cty.Value{ + addrs.OutputValue{Name: "credentials"}: cty.StringVal("authn").Mark(marks.Sensitive), + }, + InputVariables: make(map[addrs.InputVariable]cty.Value), + Dependents: collections.NewSet(mustAbsComponent("component.create")), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.load.data.testing_data_source.credentials"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "credentials", + "value": "authn", + }), + AttrSensitivePaths: make([]cty.Path, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingDataSourceSchema, + }, + }, + }, + }, + }, + "ephemeral": { + path: path.Join("with-single-input", "ephemeral"), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "input": cty.StringVal("hello"), + "ephemeral": cty.StringVal("planning"), + }, + applyInputs: map[string]cty.Value{ + "ephemeral": cty.StringVal("applying"), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("2f9f3b84"), + mustInputVariable("input"): cty.StringVal("hello"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "2f9f3b84", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("ephemeral"), + Value: cty.NullVal(cty.String), // ephemeral + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.StringVal("hello"), + }, + }, + }, + }, + }, + "missing-ephemeral": { + path: path.Join("with-single-input", "ephemeral"), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "input": cty.StringVal("hello"), + "ephemeral": cty.StringVal("planning"), + }, + applyInputs: make(map[string]cty.Value), // deliberately omitting ephemeral + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("2f9f3b84"), + mustInputVariable("input"): cty.StringVal("hello"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "2f9f3b84", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.StringVal("hello"), + }, + }, + wantAppliedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No value for required variable", + Detail: "The root input variable \"var.ephemeral\" is not set, and has no default value.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/ephemeral/ephemeral.tfstack.hcl", + Start: hcl.Pos{ + Line: 14, + Column: 1, + Byte: 175, + }, + End: hcl.Pos{ + Line: 14, + Column: 21, + Byte: 195, + }, + }, + }) + }), + }, + }, + }, + "ephemeral-default": { + path: path.Join("with-single-input", "ephemeral-default"), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "input": cty.StringVal("hello"), + // deliberately omitting ephemeral + }, + applyInputs: make(map[string]cty.Value), // deliberately omitting ephemeral + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("2f9f3b84"), + mustInputVariable("input"): cty.StringVal("hello"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "2f9f3b84", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("ephemeral"), + Value: cty.NullVal(cty.String), // ephemeral + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.StringVal("hello"), + }, + }, + }, + }, + }, + "deferred-components": { + path: path.Join("with-data-source", "deferred-provider-for-each"), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("data_known", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("data_known"), + "value": cty.StringVal("known"), + })). + Build(), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "providers": cty.UnknownVal(cty.Set(cty.String)), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.const"), + ComponentInstanceAddr: mustAbsComponentInstance("component.const"), + Dependencies: collections.NewSet[stackaddrs.AbsComponent](), + Dependents: collections.NewSet[stackaddrs.AbsComponent](), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("data_known"), + mustInputVariable("resource"): cty.StringVal("resource_known"), + }, + OutputValues: make(map[addrs.OutputValue]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "data_known", + "value": "known", + }), + AttrSensitivePaths: make([]cty.Path, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingDataSourceSchema, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "resource_known", + "value": "known", + }), + Dependencies: []addrs.ConfigResource{ + mustAbsResourceInstance("data.testing_data_source.data").ConfigResource(), + }, + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("providers"), + Value: cty.UnknownVal(cty.Set(cty.String)), + }, + }, + }, + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "providers": cty.UnknownVal(cty.Set(cty.String)), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.const"), + ComponentInstanceAddr: mustAbsComponentInstance("component.const"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("providers"), + Value: cty.NilVal, // destroyed + }, + }, + }, + }, + }, + "unknown-component-input": { + path: path.Join("map-object-input", "for-each-input"), + cycles: []TestCycle{ + { + planMode: plans.NormalMode, + planInputs: map[string]cty.Value{ + "inputs": cty.UnknownVal(cty.Map(cty.String)), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.main"), + ComponentInstanceAddr: mustAbsComponentInstance("component.main"), + Dependencies: collections.NewSet(mustAbsComponent("component.self")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("input"): cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "output": cty.String, + }))), + }, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("inputs"), + Value: cty.UnknownVal(cty.Map(cty.String)), + }, + }, + }, + { + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "inputs": cty.MapValEmpty(cty.String), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: mustAbsComponent("component.main"), + ComponentInstanceAddr: mustAbsComponentInstance("component.main"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("inputs"), + Value: cty.NilVal, // destroyed + }, + }, + }, + }, + }, + "unknown-component": { + path: path.Join("with-single-input", "removed-component-instance"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"main\"]")). + AddInputVariable("id", cty.StringVal("main")). + AddInputVariable("input", cty.StringVal("main"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"main\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "main", + "value": "main", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("main", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("main"), + "value": cty.StringVal("main"), + })). + Build(), + cycles: []TestCycle{ + { + planInputs: map[string]cty.Value{ + "input": cty.UnknownVal(cty.Set(cty.String)), + "removed": cty.SetValEmpty(cty.String), + }, + wantAppliedChanges: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"main\"]"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("main"), + mustInputVariable("input"): cty.StringVal("main"), + }, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.UnknownVal(cty.Set(cty.String)), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("removed"), + Value: cty.SetValEmpty(cty.String), + }, + }, + }, + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + store := tc.store + if store == nil { + store = stacks_testing_provider.NewResourceStore() + } + + testContext := TestContext{ + timestamp: &fakePlanTimestamp, + config: loadMainBundleConfigForTest(t, tc.path), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + provider := stacks_testing_provider.NewProviderWithData(t, store) + provider.Authentication = "authn" + return provider, nil + }, + }, + dependencyLocks: *lock, + } + + state := tc.state + for ix, cycle := range tc.cycles { + t.Run(strconv.FormatInt(int64(ix), 10), func(t *testing.T) { + var plan *stackplan.Plan + t.Run("plan", func(t *testing.T) { + plan = testContext.Plan(t, ctx, state, cycle) + }) + t.Run("apply", func(t *testing.T) { + state = testContext.Apply(t, ctx, plan, cycle) + }) + }) + } + }) + } +} + +func TestApplyWithRemovedResource(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", "valid-providers")) + lock := depsfile.NewLocks() + planReq := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + // PrevState specifies a state with a resource that is not present in + // the current configuration. This is a common situation when a resource + // is removed from the configuration but still exists in the state. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + Key: addrs.NoKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }, + Key: addrs.NoKey, + }, + }, + DeposedKey: addrs.NotDeposed, + }, + }). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "FE1D5830765C", + "input": map[string]interface{}{ + "value": "hello", + "type": "string", + }, + "output": map[string]interface{}{ + "value": nil, + "type": "string", + }, + "triggers_replace": nil, + }), + Status: states.ObjectReady, + }). + SetProviderAddr(addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), + })). + Build(), + } + + planChangesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + planResp := PlanResponse{ + PlannedChanges: planChangesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &planReq, &planResp) + planChanges, diags := collectPlanOutput(planChangesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.terraform_data.main"), + NewStateSrc: nil, // Deleted, so is nil. + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "terraform", + Namespace: "builtin", + Hostname: "terraform.io", + }, + }, + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApplyWithMovedResource(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", "moved")) + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + planReq := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("moved", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })). + Build()), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + // PrevState specifies a state with a resource that is not present in + // the current configuration. This is a common situation when a resource + // is removed from the configuration but still exists in the state. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + Key: addrs.NoKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "before", + }, + Key: addrs.NoKey, + }, + }, + DeposedKey: addrs.NotDeposed, + }, + }). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "moved", + "value": "moved", + }), + Status: states.ObjectReady, + }). + SetProviderAddr(addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + })). + Build(), + } + + planChangesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + planResp := PlanResponse{ + PlannedChanges: planChangesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &planReq, &planResp) + planChanges, diags := collectPlanOutput(planChangesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, go %s", diags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("moved", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })). + Build()), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + expectedPreviousAddr := mustAbsResourceInstanceObject("component.self.testing_resource.before") + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), + PreviousResourceInstanceObjectAddr: &expectedPreviousAddr, + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "moved", + "value": "moved", + }), + Status: states.ObjectReady, + AttrSensitivePaths: make([]cty.Path, 0), + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApplyWithSensitivePropagation(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "id"}: { + Value: cty.StringVal("bb5cf32312ec"), + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + planChanges, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + Dependencies: collections.NewSet(mustAbsComponent("component.sensitive")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("bb5cf32312ec"), + mustInputVariable("input"): cty.StringVal("secret").Mark(marks.Sensitive), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "bb5cf32312ec", + "value": "secret", + }), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + }, + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.sensitive"), + ComponentInstanceAddr: mustAbsComponentInstance("component.sensitive"), + Dependents: collections.NewSet(mustAbsComponent("component.self")), + OutputValues: map[addrs.OutputValue]cty.Value{ + addrs.OutputValue{Name: "out"}: cty.StringVal("secret").Mark(marks.Sensitive), + }, + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.StringVal("bb5cf32312ec"), + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApplyWithForcePlanTimestamp(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "with-plantimestamp") + + forcedPlanTimestamp := "1991-08-25T20:57:08Z" + fakePlanTimestamp, err := time.Parse(time.RFC3339, forcedPlanTimestamp) + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + planChanges, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) + } + // Sanity check that the plan timestamp was set correctly + output := expectOutput(t, "plantimestamp", planChanges) + plantimestampValue := output.After + + if plantimestampValue.AsString() != forcedPlanTimestamp { + t.Errorf("expected plantimestamp to be %q, got %q", forcedPlanTimestamp, plantimestampValue.AsString()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.second-self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.second-self"), + OutputValues: map[addrs.OutputValue]cty.Value{ + // We want to make sure the plantimestamp is set correctly + {Name: "input"}: cty.StringVal(forcedPlanTimestamp), + // plantimestamp should also be set for the module runtime used in the components + {Name: "out"}: cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("value"): cty.StringVal(forcedPlanTimestamp), + }, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: map[addrs.OutputValue]cty.Value{ + // We want to make sure the plantimestamp is set correctly + {Name: "input"}: cty.StringVal(forcedPlanTimestamp), + // plantimestamp should also be set for the module runtime used in the components + {Name: "out"}: cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("value"): cty.StringVal(forcedPlanTimestamp), + }, + }, + &stackstate.AppliedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "plantimestamp"}, + Value: cty.StringVal(forcedPlanTimestamp), + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApplyWithDefaultPlanTimestamp(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "with-plantimestamp") + + dayOfWritingThisTest := "2024-06-21T06:37:08Z" + dayOfWritingThisTestTime, err := time.Parse(time.RFC3339, dayOfWritingThisTest) + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + planChanges, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) + } + // Sanity check that the plan timestamp was set correctly + output := expectOutput(t, "plantimestamp", planChanges) + plantimestampValue := output.After + + plantimestamp, err := time.Parse(time.RFC3339, plantimestampValue.AsString()) + if err != nil { + t.Fatal(err) + } + + if plantimestamp.Before(dayOfWritingThisTestTime) { + t.Errorf("expected plantimestamp to be later than %q, got %q", dayOfWritingThisTest, plantimestampValue.AsString()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + for _, x := range applyChanges { + if v, ok := x.(*stackstate.AppliedChangeComponentInstance); ok { + if actualTimestampValue, ok := v.OutputValues[addrs.OutputValue{ + Name: "input", + }]; ok { + actualTimestamp, err := time.Parse(time.RFC3339, actualTimestampValue.AsString()) + if err != nil { + t.Fatalf("Could not parse component output value: %q", err) + } + if actualTimestamp.Before(dayOfWritingThisTestTime) { + t.Error("Timestamp is before day of writing this test, that should be incorrect.") + } + } + + if actualTimestampValue, ok := v.OutputValues[addrs.OutputValue{ + Name: "out", + }]; ok { + actualTimestamp, err := time.Parse(time.RFC3339, strings.ReplaceAll(actualTimestampValue.AsString(), "module-output-", "")) + if err != nil { + t.Fatalf("Could not parse component output value: %q", err) + } + if actualTimestamp.Before(dayOfWritingThisTestTime) { + t.Error("Timestamp is before day of writing this test, that should be incorrect.") + } + } + } + } +} + +func TestApplyWithFailedComponent(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "failed-parent")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + planChanges, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + + expectDiagnosticsForTest(t, applyDiags, + // This is the expected failure, from our testing_failed_resource. + expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply")) + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + Dependents: collections.NewSet(mustAbsComponent("component.self")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("input"): cty.StringVal("Hello, world!"), + mustInputVariable("id"): cty.NullVal(cty.String), + mustInputVariable("fail_plan"): cty.NullVal(cty.Bool), + mustInputVariable("fail_apply"): cty.BoolVal(true), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + Dependencies: collections.NewSet(mustAbsComponent("component.parent")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.NullVal(cty.String), + mustInputVariable("input"): cty.UnknownVal(cty.String), + }, + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + +} + +func TestApplyWithFailedProviderLinkedComponent(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "failed-component-to-provider")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + planChanges, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + + expectDiagnosticsForTest(t, applyDiags, + // This is the expected failure, from our testing_failed_resource. + expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply")) + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.parent"), + ComponentInstanceAddr: mustAbsComponentInstance("component.parent"), + Dependents: collections.NewSet(mustAbsComponent("component.self")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("input"): cty.NullVal(cty.String), + mustInputVariable("id"): cty.NullVal(cty.String), + mustInputVariable("fail_plan"): cty.NullVal(cty.Bool), + mustInputVariable("fail_apply"): cty.BoolVal(true), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_failed_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + Dependencies: collections.NewSet(mustAbsComponent("component.parent")), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.NullVal(cty.String), + mustInputVariable("input"): cty.StringVal("Hello, world!"), + }, + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + +} + +func TestApplyWithStateManipulation(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + tcs := map[string]struct { + state *stackstate.State + store *stacks_testing_provider.ResourceStore + inputs map[string]cty.Value + changes []stackstate.AppliedChange + counts collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange] + planDiags []expectedDiagnostic + applyDiags []expectedDiagnostic + }{ + "moved": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("moved", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })). + Build(), + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "moved", + "value": "moved", + }), + Status: states.ObjectReady, + AttrSensitivePaths: make([]cty.Path, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + PreviousResourceInstanceObjectAddr: mustAbsResourceInstanceObjectPtr("component.self.testing_resource.before"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Move: 1, + }, + }), + }, + "moved-failed-dep": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("moved", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })). + Build(), + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "moved", + "value": "moved", + }), + Status: states.ObjectReady, + AttrSensitivePaths: make([]cty.Path, 0), + Dependencies: []addrs.ConfigResource{ + { + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_failed_resource", + Name: "resource", + }, + }, + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + PreviousResourceInstanceObjectAddr: mustAbsResourceInstanceObjectPtr("component.self.testing_resource.before"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Move: 1, + }, + }), + applyDiags: []expectedDiagnostic{ + // This error comes from the testing_failed_resource + expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"), + }, + }, + "import": { + state: stackstate.NewStateBuilder().Build(), // We start with an empty state for this. + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("imported", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported"), + "value": cty.StringVal("imported"), + })). + Build(), + inputs: map[string]cty.Value{ + "id": cty.StringVal("imported"), + }, + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("imported"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "imported", + "value": "imported", + }), + Status: states.ObjectReady, + AttrSensitivePaths: make([]cty.Path, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.StringVal("imported"), + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Import: 1, + }, + }), + }, + "import-failed-dep": { + state: stackstate.NewStateBuilder().Build(), // We start with an empty state for this. + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("imported", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported"), + "value": cty.StringVal("imported"), + })). + Build(), + inputs: map[string]cty.Value{ + "id": cty.StringVal("imported"), + }, + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("imported"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "imported", + "value": "imported", + }), + Status: states.ObjectReady, + AttrSensitivePaths: make([]cty.Path, 0), + Dependencies: []addrs.ConfigResource{ + { + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_failed_resource", + Name: "resource", + }, + }, + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.StringVal("imported"), + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Import: 1, + }, + }), + applyDiags: []expectedDiagnostic{ + // This error comes from the testing_failed_resource + expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"), + }, + }, + "removed": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"), + NewStateSrc: nil, // Deleted, so is nil. + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Forget: 1, + }, + }), + planDiags: []expectedDiagnostic{ + expectDiagnostic(tfdiags.Warning, "Some objects will no longer be managed by Terraform", "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again."), + }, + }, + "removed-failed-dep": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.resource"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"), + NewStateSrc: nil, // Deleted, so is nil. + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Forget: 1, + }, + }), + planDiags: []expectedDiagnostic{ + expectDiagnostic(tfdiags.Warning, "Some objects will no longer be managed by Terraform", "If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n - testing_resource.resource\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again."), + }, + applyDiags: []expectedDiagnostic{ + // This error comes from the testing_failed_resource + expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply"), + }, + }, + "deferred": { + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("self", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("deferred"), + "value": cty.UnknownVal(cty.String), + })). + Build(), + changes: []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.deferred"), + ComponentInstanceAddr: mustAbsComponentInstance("component.deferred"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.ok"), + ComponentInstanceAddr: mustAbsComponentInstance("component.ok"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.ok.testing_resource.self"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "ok", + "value": "ok", + }), + Status: states.ObjectReady, + AttrSensitivePaths: nil, + Dependencies: []addrs.ConfigResource{}, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + PreviousResourceInstanceObjectAddr: nil, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.ok"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.ok"), + Add: 1, + Defer: 0, + }, + }, + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.deferred"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.deferred"), + Defer: 1, + }, + }, + ), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name)) + + inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs)) + for name, input := range tc.inputs { + inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{ + Value: input, + } + } + + providers := map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, tc.store), nil + }, + } + + planChangeCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + planReq := PlanRequest{ + Config: cfg, + ProviderFactories: providers, + InputValues: inputs, + ForcePlanTimestamp: &fakePlanTimestamp, + PrevState: tc.state, + DependencyLocks: *lock, + } + planResp := PlanResponse{ + PlannedChanges: planChangeCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &planReq, &planResp) + planChanges, diags := collectPlanOutput(planChangeCh, diagsCh) + + sort.SliceStable(diags, diagnosticSortFunc(diags)) + expectDiagnosticsForTest(t, diags, tc.planDiags...) + + // Check the counts during the apply for this test. + gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]() + ctx = ContextWithHooks(ctx, &stackeval.Hooks{ + ReportComponentInstanceApplied: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any { + gotCounts.Put(change.Addr, change) + return span + }, + }) + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: providers, + DependencyLocks: *lock, + } + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, diags := collectApplyOutput(applyChangesCh, diagsCh) + + sort.SliceStable(diags, diagnosticSortFunc(diags)) + expectDiagnosticsForTest(t, diags, tc.applyDiags...) + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(tc.changes, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + + wantCounts := tc.counts + for key, elem := range wantCounts.All() { + // First, make sure everything we wanted is present. + if !gotCounts.HasKey(key) { + t.Errorf("wrong counts: wanted %s but didn't get it", key) + } + + // And that the values actually match. + got, want := gotCounts.Get(key), elem + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong counts for %s: %s", want.Addr, diff) + } + + } + + for key := range gotCounts.All() { + // Then, make sure we didn't get anything we didn't want. + if !wantCounts.HasKey(key) { + t.Errorf("wrong counts: got %s but didn't want it", key) + } + } + }) + } +} + +func TestApplyWithChangedInputValues(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "input"}: { + Value: cty.StringVal("hello"), + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + planChanges, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) > 0 { + t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + // This time we're deliberately changing the values we're giving + // to the apply operation. We expect this to fail earlier than + // the previous test. + stackaddrs.InputVariable{Name: "input"}: { + Value: cty.StringVal("world"), + }, + }, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + + sort.SliceStable(applyDiags, diagnosticSortFunc(applyDiags)) + expectDiagnosticsForTest(t, applyDiags, + expectDiagnostic( + tfdiags.Error, + "Inconsistent value for input variable during apply", + "The value for non-ephemeral input variable \"input\" was set to a different value during apply than was set during plan. Only ephemeral input variables can change between the plan and apply phases."), + expectDiagnostic(tfdiags.Error, "Invalid inputs for component", "Invalid input variable definition object: attribute \"input\": string required."), + ) + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: make(map[addrs.InputVariable]cty.Value), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.NullVal(cty.String), + }, + // no resources should have been created because the input variable was + // invalid. + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApplyAutomaticInputConversion(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "for-each-component")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "input"}: { + // The stack expects a map of strings, but we're giving it + // an object. Terraform should automatically convert this to + // the expected type. + Value: cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("hello"), + "world": cty.StringVal("world"), + }), + }, + }, + } + + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + planChanges, planDiags := collectPlanOutput(changesCh, diagsCh) + if len(planDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", planDiags.ErrWithWarnings()) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "input"}: { + // The stack expects a map of strings, but we're giving it + // an object. Terraform should automatically convert this to + // the expected type. + Value: cty.ObjectVal(map[string]cty.Value{ + "hello": cty.StringVal("hello"), + "world": cty.StringVal("world"), + }), + }, + }, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + if len(applyDiags) > 0 { + t.Fatalf("expected no diagnostics, got %s", applyDiags.ErrWithWarnings()) + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"hello\"]"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("hello"), + mustInputVariable("input"): cty.StringVal("hello"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"hello\"].testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "hello", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self[\"world\"]"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("world"), + mustInputVariable("input"): cty.StringVal("world"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"world\"].testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "world", + "value": "world", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.MapVal(map[string]cty.Value{ + "hello": cty.StringVal("hello"), + "world": cty.StringVal("world"), + }), + }, + } + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApply_DependsOnComponentWithNoInstances(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "depends-on")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + planRequest := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "input"}: { + Value: cty.StringVal("hello, world!"), + }, + }, + } + + planResponse := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &planRequest, &planResponse) + planChanges, planDiags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, planDiags) + if len(planDiags) != 0 { + t.FailNow() + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + _, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + reportDiagnosticsForTest(t, applyDiags) + if len(applyDiags) != 0 { + t.FailNow() + } + + // don't care about the changes - just want to make sure that depends_on + // reference to a component with zero instances doesn't break anything +} + +func TestApply_WithProviderFunctions(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-provider-functions")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + + planRequest := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "input"}: { + Value: cty.StringVal("hello, world!"), + }, + }, + } + + planResponse := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &planRequest, &planResponse) + planChanges, planDiags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, planDiags) + if len(planDiags) != 0 { + t.FailNow() + } + + sort.SliceStable(planChanges, func(i, j int) bool { + return plannedChangeSortKey(planChanges[i]) < plannedChangeSortKey(planChanges[j]) + }) + wantPlanChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("2f9f3b84")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: map[string]cty.Value{ + "value": cty.StringVal("hello, world!"), + }, + PlannedCheckResults: &states.CheckResults{}, + PlannedProviderFunctionResults: []lang.FunctionResultHash{ + { + Key: providerFunctionHashArgs(mustDefaultRootProvider("testing").Provider, "echo", cty.StringVal("hello, world!")), + Result: providerFunctionHashResult(cty.StringVal("hello, world!")), + }, + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("2f9f3b84"), + "value": cty.StringVal("hello, world!"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeProviderFunctionResults{ + Results: []lang.FunctionResultHash{ + { + Key: providerFunctionHashArgs(mustDefaultRootProvider("testing").Provider, "echo", cty.StringVal("hello, world!")), + Result: providerFunctionHashResult(cty.StringVal("hello, world!")), + }, + }, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "value"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("hello, world!"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("hello, world!"), + }, + } + if diff := cmp.Diff(wantPlanChanges, planChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + // just verify the plan is correctly loading the provider function results + // as well + if len(plan.FunctionResults) == 0 { + t.Errorf("expected provider function results, got none") + + if len(plan.GetComponent(mustAbsComponentInstance("component.self")).PlannedFunctionResults) == 0 { + t.Errorf("expected component function results, got none") + } + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + diagsCh = make(chan tfdiags.Diagnostic) + + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh) + reportDiagnosticsForTest(t, applyDiags) + if len(applyDiags) != 0 { + t.FailNow() + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + wantApplyChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: map[addrs.OutputValue]cty.Value{ + {Name: "value"}: cty.StringVal("hello, world!"), + }, + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("2f9f3b84"), + mustInputVariable("input"): cty.StringVal("hello, world!"), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "2f9f3b84", + "value": "hello, world!", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "value"}, + Value: cty.StringVal("hello, world!"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.StringVal("hello, world!"), + }, + } + + if diff := cmp.Diff(wantApplyChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestApplyFailedDependencyWithResourceInState(t *testing.T) { + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "failed-dependency") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + store := stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("resource", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("resource"), + "value": cty.NullVal(cty.String), + })). + Build() + + planReq := PlanRequest{ + PlanMode: plans.NormalMode, + + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, store), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "fail_apply"}: { + Value: cty.True, + }, + }, + + // We have a resource in the state from a previous run. We shouldn't + // emit any state changes to this resource as a result of the dependency + // failing. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "resource", + "value": nil, + }), + Status: states.ObjectReady, + })). + Build(), + } + + planChangesCh := make(chan stackplan.PlannedChange) + planDiagsCh := make(chan tfdiags.Diagnostic) + planResp := PlanResponse{ + PlannedChanges: planChangesCh, + Diagnostics: planDiagsCh, + } + + go Plan(ctx, &planReq, &planResp) + planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh) + if len(planDiags) > 0 { + t.Fatalf("unexpected diagnostics during planning: %s", planDiags) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, store), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + applyDiagsCh := make(chan tfdiags.Diagnostic) + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: applyDiagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh) + + expectDiagnosticsForTest(t, applyDiags, expectDiagnostic(tfdiags.Error, "failedResource error", "failed during apply")) + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("resource_id"): cty.StringVal("resource"), + mustInputVariable("failed_id"): cty.StringVal("failed"), + mustInputVariable("fail_apply"): cty.True, + mustInputVariable("fail_plan"): cty.False, + mustInputVariable("input"): cty.NullVal(cty.String), + }, + }, + &stackstate.AppliedChangeResourceInstanceObject{ + // This has no state as the apply operation failed and it wasn't + // in the state before. + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_failed_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + }, + &stackstate.AppliedChangeResourceInstanceObject{ + // This emits the state from the previous run, as it was not + // changed during this run. + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "resource", + "value": nil, + }), + AttrSensitivePaths: make([]cty.Path, 0), + Status: states.ObjectReady, + Dependencies: []addrs.ConfigResource{mustAbsResourceInstance("testing_failed_resource.data").ConfigResource()}, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("fail_apply"), + Value: cty.True, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("fail_plan"), + Value: cty.False, + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + +} + +func TestApplyManuallyRemovedResource(t *testing.T) { + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "2021-01-01T00:00:00Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + planReq := PlanRequest{ + PlanMode: plans.NormalMode, + + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + stackaddrs.InputVariable{Name: "id"}: { + Value: cty.StringVal("foo"), + }, + stackaddrs.InputVariable{Name: "input"}: { + Value: cty.StringVal("hello"), + }, + }, + + // We have in the previous state a resource that is not in our + // underlying data store. This simulates the case where someone went + // in and manually deleted a resource that Terraform is managing. + // + // Some providers will return an error in this case, but some will + // not. We need to ensure that we handle the second case gracefully. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.missing")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "e84b59f2", + "value": "hello", + }), + Status: states.ObjectReady, + })). + Build(), + } + + planChangesCh := make(chan stackplan.PlannedChange) + planDiagsCh := make(chan tfdiags.Diagnostic) + planResp := PlanResponse{ + PlannedChanges: planChangesCh, + Diagnostics: planDiagsCh, + } + + go Plan(ctx, &planReq, &planResp) + planChanges, planDiags := collectPlanOutput(planChangesCh, planDiagsCh) + if len(planDiags) > 0 { + t.Fatalf("unexpected diagnostics during planning: %s", planDiags) + } + + planLoader := stackplan.NewLoader() + for _, change := range planChanges { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + applyReq := ApplyRequest{ + Config: cfg, + Plan: plan, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + + applyChangesCh := make(chan stackstate.AppliedChange) + applyDiagsCh := make(chan tfdiags.Diagnostic) + applyResp := ApplyResponse{ + AppliedChanges: applyChangesCh, + Diagnostics: applyDiagsCh, + } + + go Apply(ctx, &applyReq, &applyResp) + applyChanges, applyDiags := collectApplyOutput(applyChangesCh, applyDiagsCh) + if len(applyDiags) > 0 { + t.Fatalf("unexpected diagnostics during apply: %s", applyDiags) + } + + wantChanges := []stackstate.AppliedChange{ + &stackstate.AppliedChangeComponentInstance{ + ComponentAddr: mustAbsComponent("component.self"), + ComponentInstanceAddr: mustAbsComponentInstance("component.self"), + OutputValues: make(map[addrs.OutputValue]cty.Value), + InputVariables: map[addrs.InputVariable]cty.Value{ + mustInputVariable("id"): cty.StringVal("foo"), + mustInputVariable("input"): cty.StringVal("hello"), + }, + }, + // The resource in our configuration has been updated, so that is + // present as normal. + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "foo", + "value": "hello", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + // The resource that was in state but not in the configuration should + // be removed from state. + &stackstate.AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.missing"), + ProviderConfigAddr: mustDefaultRootProvider("testing"), + NewStateSrc: nil, // We should be removing this from the state file. + Schema: providers.Schema{}, + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("id"), + Value: cty.StringVal("foo"), + }, + &stackstate.AppliedChangeInputVariable{ + Addr: mustStackInputVariable("input"), + Value: cty.StringVal("hello"), + }, + } + + sort.SliceStable(applyChanges, func(i, j int) bool { + return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, applyChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func collectApplyOutput(changesCh <-chan stackstate.AppliedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + var changes []stackstate.AppliedChange + var diags tfdiags.Diagnostics + for { + select { + case change, ok := <-changesCh: + if !ok { + // The plan operation is complete but we might still have + // some buffered diagnostics to consume. + if diagsCh != nil { + for diag := range diagsCh { + diags = append(diags, diag) + } + } + return changes, diags + } + changes = append(changes, change) + case diag, ok := <-diagsCh: + if !ok { + // no more diagnostics to read + diagsCh = nil + continue + } + diags = append(diags, diag) + } + } +} diff --git a/internal/stacks/stackruntime/doc.go b/internal/stacks/stackruntime/doc.go new file mode 100644 index 0000000000..d8995d2aeb --- /dev/null +++ b/internal/stacks/stackruntime/doc.go @@ -0,0 +1,7 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package stackruntime contains the runtime implementation of the Stacks +// language, allowing the various operations over a stack configuration and +// its associated state and plans. +package stackruntime diff --git a/internal/stacks/stackruntime/eval_expr.go b/internal/stacks/stackruntime/eval_expr.go new file mode 100644 index 0000000000..84925d1834 --- /dev/null +++ b/internal/stacks/stackruntime/eval_expr.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +// EvalExpr evaluates the given expression in a specified evaluation +// environment and scope. +// +// This is intended for situations like the "terraform console" command which +// need to evaluate arbitrary expressions against a configuration and +// previously-established state snapshot. +func EvalExpr(ctx context.Context, expr hcl.Expression, req *EvalExprRequest) (cty.Value, tfdiags.Diagnostics) { + main := stackeval.NewForInspecting(req.Config, req.State, stackeval.InspectOpts{ + InputVariableValues: req.InputValues, + ProviderFactories: req.ProviderFactories, + }) + main.AllowLanguageExperiments(req.ExperimentsAllowed) + return main.EvalExpr(ctx, expr, req.EvalStackInstance, stackeval.InspectPhase) +} + +// EvalExprRequest represents the inputs to an [EvalExpr] call. +type EvalExprRequest struct { + // Config and State together provide the global environment in which + // the expression will be evaluated. + Config *stackconfig.Config + State *stackstate.State + + // EvalStackInstance is the address of the stack instance where the + // expression is to be evaluated. If unspecified, the default is + // to evaluate in the root stack instance. + EvalStackInstance stackaddrs.StackInstance + + // InputValues and ProviderFactories are both optional extras to + // provide a more complete evaluation environment, although neither + // needs to be provided if the expression to be evaluated doesn't + // (directly or indirectly) make use of input variables or provider + // configurations corresponding to these. + InputValues map[stackaddrs.InputVariable]ExternalInputValue + ProviderFactories map[addrs.Provider]providers.Factory + + ExperimentsAllowed bool +} diff --git a/internal/stacks/stackruntime/helper_test.go b/internal/stacks/stackruntime/helper_test.go new file mode 100644 index 0000000000..0aa148737e --- /dev/null +++ b/internal/stacks/stackruntime/helper_test.go @@ -0,0 +1,598 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "crypto/sha256" + "encoding/json" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// This file has helper functions used by other tests. It doesn't contain any +// test cases of its own. + +// TestContext contains all the information shared across multiple operations +// in a single test. +type TestContext struct { + // timestamp is the timestamp that should be applied for this test. + timestamp *time.Time + + // config is the config to use for this test. + config *stackconfig.Config + + // providers are the providers that should be available within this test. + providers map[addrs.Provider]providers.Factory + + // dependencyLocks is the locks file that should be used for this test. + dependencyLocks depsfile.Locks +} + +// TestCycle defines a single plan / apply cycle that should be performed within +// a test. +type TestCycle struct { + + // Validate options + + wantValidateDiags tfdiags.Diagnostics + + // Plan options + + planMode plans.Mode + planInputs map[string]cty.Value + wantPlannedChanges []stackplan.PlannedChange + wantPlannedDiags tfdiags.Diagnostics + + // Apply options + + applyInputs map[string]cty.Value + wantAppliedChanges []stackstate.AppliedChange + wantAppliedDiags tfdiags.Diagnostics +} + +func (tc TestContext) Validate(t *testing.T, ctx context.Context, cycle TestCycle) { + t.Helper() + + gotDiags := Validate(ctx, &ValidateRequest{ + Config: tc.config, + ProviderFactories: tc.providers, + DependencyLocks: tc.dependencyLocks, + ExperimentsAllowed: true, + }) + validateDiags(t, cycle.wantValidateDiags, gotDiags) +} + +func (tc TestContext) Plan(t *testing.T, ctx context.Context, state *stackstate.State, cycle TestCycle) *stackplan.Plan { + request := PlanRequest{ + PlanMode: cycle.planMode, + Config: tc.config, + PrevState: state, + InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { + inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.planInputs)) + for k, v := range cycle.planInputs { + inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v} + } + return inputs + }(), + ProviderFactories: tc.providers, + DependencyLocks: tc.dependencyLocks, + ForcePlanTimestamp: tc.timestamp, + ExperimentsAllowed: true, + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + response := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &request, &response) + changes, diags := collectPlanOutput(changesCh, diagsCh) + validateDiags(t, cycle.wantPlannedDiags, diags) + + if cycle.wantPlannedChanges != nil { + + var filteredChanges []stackplan.PlannedChange + for _, change := range changes { + if _, ok := change.(*stackplan.PlannedChangePriorStateElement); ok { + // Remove the prior state elements from the analysis for tests + // using this framework. They're really difficult to properly + // compare as they use the raw state and raw key, and they're + // actually ignored by most of the stacks runtime and so aren't + // useful to be included in these kind of tests. + continue + } + filteredChanges = append(filteredChanges, change) + } + + // if this is nil (as opposed to empty) then we don't validate the + // returned changes. + + sort.SliceStable(filteredChanges, func(i, j int) bool { + return plannedChangeSortKey(filteredChanges[i]) < plannedChangeSortKey(filteredChanges[j]) + }) + if diff := cmp.Diff(cycle.wantPlannedChanges, filteredChanges, changesCmpOpts); len(diff) > 0 { + t.Errorf("wrong planned changes\n%s", diff) + } + } + + planLoader := stackplan.NewLoader() + for _, change := range changes { + proto, err := change.PlannedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + err = planLoader.AddRaw(rawMsg) + if err != nil { + t.Fatal(err) + } + } + } + plan, err := planLoader.Plan() + if err != nil { + t.Fatal(err) + } + + return plan +} + +func (tc TestContext) Apply(t *testing.T, ctx context.Context, plan *stackplan.Plan, cycle TestCycle) *stackstate.State { + request := ApplyRequest{ + Config: tc.config, + Plan: plan, + InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { + inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(cycle.applyInputs)) + for k, v := range cycle.applyInputs { + inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v} + } + return inputs + }(), + ProviderFactories: tc.providers, + ExperimentsAllowed: true, + DependencyLocks: tc.dependencyLocks, + } + + changesCh := make(chan stackstate.AppliedChange) + diagsCh := make(chan tfdiags.Diagnostic) + response := ApplyResponse{ + AppliedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Apply(ctx, &request, &response) + changes, diags := collectApplyOutput(changesCh, diagsCh) + validateDiags(t, cycle.wantAppliedDiags, diags) + + if cycle.wantAppliedChanges != nil { + // nil indicates skip this check, empty slice indicates no changes expected. + sort.SliceStable(changes, func(i, j int) bool { + return appliedChangeSortKey(changes[i]) < appliedChangeSortKey(changes[j]) + }) + if diff := cmp.Diff(cycle.wantAppliedChanges, changes, changesCmpOpts); diff != "" { + t.Errorf("wrong applied changes\n%s", diff) + } + } + + stateLoader := stackstate.NewLoader() + for _, change := range changes { + proto, err := change.AppliedChangeProto() + if err != nil { + t.Fatal(err) + } + + for _, rawMsg := range proto.Raw { + if rawMsg.Value == nil { + // This is a removal notice, so we don't need to add it to the + // state. + continue + } + err = stateLoader.AddRaw(rawMsg.Key, rawMsg.Value) + if err != nil { + t.Fatal(err) + } + } + } + return stateLoader.State() +} + +func initDiags(cb func(diags tfdiags.Diagnostics) tfdiags.Diagnostics) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + return cb(diags) +} + +func validateDiags(t *testing.T, wantDiags, gotDiags tfdiags.Diagnostics) { + t.Helper() + + sort.SliceStable(gotDiags, diagnosticSortFunc(gotDiags)) + sort.SliceStable(wantDiags, diagnosticSortFunc(wantDiags)) + + gotDiags = gotDiags.ForRPC() + wantDiags = wantDiags.ForRPC() + if diff := cmp.Diff(wantDiags, gotDiags); len(diff) > 0 { + t.Errorf("wrong diagnostics\n%s", diff) + } +} + +// loadConfigForTest is a test helper that tries to open bundleRoot as a +// source bundle, and then if successful tries to load the given source address +// from it as a stack configuration. If any part of the operation fails then +// it halts execution of the test and doesn't return. +func loadConfigForTest(t *testing.T, bundleRoot string, configSourceAddr string) *stackconfig.Config { + t.Helper() + sources, err := sourcebundle.OpenDir(bundleRoot) + if err != nil { + t.Fatalf("cannot load source bundle: %s", err) + } + + // We force using remote source addresses here because that avoids + // us having to deal with the extra version constraints argument + // that registry sources require. Exactly what source address type + // we use isn't relevant for tests in this package, since it's + // the sourcebundle package's responsibility to make sure its + // abstraction works for all of the source types. + sourceAddr, err := sourceaddrs.ParseRemoteSource(configSourceAddr) + if err != nil { + t.Fatalf("invalid config source address: %s", err) + } + + cfg, diags := stackconfig.LoadConfigDir(sourceAddr, sources) + reportDiagnosticsForTest(t, diags) + return cfg +} + +func mainBundleSourceAddrStr(dirName string) string { + return "git::https://example.com/test.git//" + dirName +} + +// loadMainBundleConfigForTest is a convenience wrapper around +// loadConfigForTest that knows the location and package address of our +// "main" source bundle, in ./testdata/mainbundle, so that we can use that +// conveniently without duplicating its location and synthetic package address +// in every single test function. +// +// dirName should begin with the name of a subdirectory that's present in +// ./testdata/mainbundle/test . It can optionally refer to subdirectories +// thereof, using forward slashes as the path separator just as we'd do +// in the subdirectory portion of a remote source address (which is exactly +// what we're using this as.) +func loadMainBundleConfigForTest(t *testing.T, dirName string) *stackconfig.Config { + t.Helper() + fullSourceAddr := mainBundleSourceAddrStr(dirName) + return loadConfigForTest(t, "./testdata/mainbundle", fullSourceAddr) +} + +type expectedDiagnostic struct { + severity tfdiags.Severity + summary string + detail string +} + +func expectDiagnostic(severity tfdiags.Severity, summary, detail string) expectedDiagnostic { + return expectedDiagnostic{ + severity: severity, + summary: summary, + detail: detail, + } +} + +func expectDiagnosticsForTest(t *testing.T, actual tfdiags.Diagnostics, expected ...expectedDiagnostic) { + t.Helper() + + max := len(expected) + if len(actual) > max { + max = len(actual) + } + + for ix := 0; ix < max; ix++ { + if ix >= len(expected) { + t.Errorf("unexpected diagnostic [%d]: %s - %s", ix, actual[ix].Description().Summary, actual[ix].Description().Detail) + continue + } + + if ix >= len(actual) { + t.Errorf("missing diagnostic [%d]: %s - %s", ix, expected[ix].summary, expected[ix].detail) + continue + } + + if actual[ix].Severity() != expected[ix].severity { + t.Errorf("diagnostic [%d] has wrong severity: %s (expected %s)", ix, actual[ix].Severity(), expected[ix].severity) + } + + if diff := cmp.Diff(actual[ix].Description().Summary, expected[ix].summary); len(diff) > 0 { + t.Errorf("diagnostic [%d] has wrong summary: %s", ix, diff) + } + + if diff := cmp.Diff(actual[ix].Description().Detail, expected[ix].detail); len(diff) > 0 { + t.Errorf("diagnostic [%d] has wrong detail: %s", ix, diff) + } + } +} + +// reportDiagnosticsForTest creates a test log entry for every diagnostic in +// the given diags, and halts the test if any of them are error diagnostics. +func reportDiagnosticsForTest(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + for _, diag := range diags { + var b strings.Builder + desc := diag.Description() + locs := diag.Source() + + switch sev := diag.Severity(); sev { + case tfdiags.Error: + b.WriteString("Error: ") + case tfdiags.Warning: + b.WriteString("Warning: ") + default: + t.Errorf("unsupported diagnostic type %s", sev) + } + b.WriteString(desc.Summary) + if desc.Address != "" { + b.WriteString("\nwith ") + b.WriteString(desc.Summary) + } + if locs.Subject != nil { + b.WriteString("\nat ") + b.WriteString(locs.Subject.StartString()) + } + if desc.Detail != "" { + b.WriteString("\n\n") + b.WriteString(desc.Detail) + } + t.Log(b.String()) + } + if diags.HasErrors() { + t.FailNow() + } +} + +// appliedChangeSortKey returns a string that can be used to sort applied +// changes in a predictable order for testing purposes. This is used to +// ensure that we can compare applied changes in a consistent way across +// different test runs. +func appliedChangeSortKey(change stackstate.AppliedChange) string { + switch change := change.(type) { + case *stackstate.AppliedChangeResourceInstanceObject: + return change.ResourceInstanceObjectAddr.String() + case *stackstate.AppliedChangeComponentInstance: + return change.ComponentInstanceAddr.String() + case *stackstate.AppliedChangeComponentInstanceRemoved: + return change.ComponentInstanceAddr.String() + case *stackstate.AppliedChangeOutputValue: + return change.Addr.String() + case *stackstate.AppliedChangeInputVariable: + return change.Addr.String() + case *stackstate.AppliedChangeDiscardKeys: + // There should only be a single discard keys in a plan, so we can just + // return a static string here. + return "discard" + default: + // This is only going to happen during tests, so we can panic here. + panic(fmt.Errorf("unrecognized applied change type: %T", change)) + } +} + +// plannedChangeSortKey returns a string that can be used to sort planned +// changes in a predictable order for testing purposes. This is used to +// ensure that we can compare planned changes in a consistent way across +// different test runs. +func plannedChangeSortKey(change stackplan.PlannedChange) string { + switch change := change.(type) { + case *stackplan.PlannedChangeRootInputValue: + return change.Addr.String() + case *stackplan.PlannedChangeComponentInstance: + return change.Addr.String() + case *stackplan.PlannedChangeComponentInstanceRemoved: + return change.Addr.String() + case *stackplan.PlannedChangeResourceInstancePlanned: + return change.ResourceInstanceObjectAddr.String() + case *stackplan.PlannedChangeDeferredResourceInstancePlanned: + return change.ResourceInstancePlanned.ResourceInstanceObjectAddr.String() + case *stackplan.PlannedChangeOutputValue: + return change.Addr.String() + case *stackplan.PlannedChangeHeader: + // There should only be a single header in a plan, so we can just return + // a static string here. + return "header" + case *stackplan.PlannedChangeApplyable: + // There should only be a single applyable marker in a plan, so we can + // just return a static string here. + return "applyable" + case *stackplan.PlannedChangePlannedTimestamp: + // There should only be a single timestamp in a plan, so we can + // just return a static string here. + return "planned-timestamp" + case *stackplan.PlannedChangeProviderFunctionResults: + // There should only be a single timestamp in a plan, so we can just + // return a simple string. + return "function-results" + default: + // This is only going to happen during tests, so we can panic here. + panic(fmt.Errorf("unrecognized planned change type: %T", change)) + } +} + +func diagnosticSortFunc(diags tfdiags.Diagnostics) func(i, j int) bool { + sortDescription := func(i, j tfdiags.Description) bool { + if i.Summary != j.Summary { + return i.Summary < j.Summary + } + return i.Detail < j.Detail + } + + sortPos := func(i, j tfdiags.SourcePos) bool { + if i.Line != j.Line { + return i.Line < j.Line + } + return i.Column < j.Column + } + + sortRange := func(i, j *tfdiags.SourceRange) bool { + if i.Filename != j.Filename { + return i.Filename < j.Filename + } + if !cmp.Equal(i.Start, j.Start) { + return sortPos(i.Start, j.Start) + } + return sortPos(i.End, j.End) + } + + return func(i, j int) bool { + id, jd := diags[i], diags[j] + if id.Severity() != jd.Severity() { + return id.Severity() == tfdiags.Error + } + if !cmp.Equal(id.Description(), jd.Description()) { + return sortDescription(id.Description(), jd.Description()) + } + if id.Source().Subject != nil && jd.Source().Subject != nil { + + return sortRange(id.Source().Subject, jd.Source().Subject) + } + + return false + } +} + +func mustDefaultRootProvider(provider string) addrs.AbsProviderConfig { + return addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider(provider), + } +} + +func mustAbsResourceInstance(addr string) addrs.AbsResourceInstance { + ret, diags := addrs.ParseAbsResourceInstanceStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse resource instance address %q: %s", addr, diags)) + } + return ret +} + +func mustAbsResourceInstanceObject(addr string) stackaddrs.AbsResourceInstanceObject { + ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse resource instance object address %q: %s", addr, diags)) + } + return ret +} + +func mustAbsResourceInstanceObjectPtr(addr string) *stackaddrs.AbsResourceInstanceObject { + ret := mustAbsResourceInstanceObject(addr) + return &ret +} + +func mustAbsComponentInstance(addr string) stackaddrs.AbsComponentInstance { + ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) + } + return ret +} + +func mustAbsComponent(addr string) stackaddrs.AbsComponent { + ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr) + if len(diags) > 0 { + panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags)) + } + return stackaddrs.AbsComponent{ + Stack: ret.Stack, + Item: ret.Item.Component, + } +} + +// mustPlanDynamicValue is a helper function that constructs a +// plans.DynamicValue from the given cty.Value, panicking if the construction +// fails. +func mustPlanDynamicValue(v cty.Value) plans.DynamicValue { + ret, err := plans.NewDynamicValue(v, v.Type()) + if err != nil { + panic(err) + } + return ret +} + +// mustPlanDynamicValueDynamicType is a helper function that constructs a +// plans.DynamicValue from the given cty.Value, using cty.DynamicPseudoType as +// the type, and panicking if the construction fails. +func mustPlanDynamicValueDynamicType(v cty.Value) plans.DynamicValue { + ret, err := plans.NewDynamicValue(v, cty.DynamicPseudoType) + if err != nil { + panic(err) + } + return ret +} + +// mustPlanDynamicValueSchema is a helper function that constructs a +// plans.DynamicValue from the given cty.Value and configschema.Block, panicking +// if the construction fails. +func mustPlanDynamicValueSchema(v cty.Value, block *configschema.Block) plans.DynamicValue { + ty := block.ImpliedType() + ret, err := plans.NewDynamicValue(v, ty) + if err != nil { + panic(err) + } + return ret +} + +func mustInputVariable(name string) addrs.InputVariable { + return addrs.InputVariable{Name: name} +} + +func mustStackInputVariable(name string) stackaddrs.InputVariable { + return stackaddrs.InputVariable{Name: name} +} + +func mustStackOutputValue(name string) stackaddrs.OutputValue { + return stackaddrs.OutputValue{Name: name} +} + +func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte { + jsonAttrs, err := json.Marshal(attrs) + if err != nil { + panic(err) + } + return jsonAttrs +} + +func providerFunctionHashArgs(provider addrs.Provider, name string, args ...cty.Value) []byte { + sum := sha256.New() + + sum.Write([]byte(provider.String())) + sum.Write([]byte("|")) + sum.Write([]byte(name)) + for _, arg := range args { + sum.Write([]byte("|")) + sum.Write([]byte(arg.GoString())) + } + + return sum.Sum(nil) +} + +func providerFunctionHashResult(value cty.Value) []byte { + bytes := sha256.Sum256([]byte(value.GoString())) + return bytes[:] +} diff --git a/internal/stacks/stackruntime/hooks.go b/internal/stacks/stackruntime/hooks.go new file mode 100644 index 0000000000..a80a08f0f9 --- /dev/null +++ b/internal/stacks/stackruntime/hooks.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" +) + +// This file exposes a small part of the API surface of "stackeval" to external +// callers. We need to orient it this way because package stackeval cannot +// itself depend on stackruntime. + +// Hooks is an optional mechanism for callers to get streaming notifications +// of various kinds of events that can occur during plan and apply operations. +// +// To use this, construct a Hooks object and then wrap it in a [context.Context] +// using the [ContextWithHooks] function, and then use that context (or another +// context derived from it that inherits the values) when calling into +// [Plan] or [Apply]. +// +// All of the callback fields in Hooks are optional and may be left as nil +// if the caller has no interest in a particular event. +// +// The events exposed by Hooks are intended for ancillary use-cases like +// realtime UI updates, and so a caller that is only concerned with the primary +// results of an operation can safely ignore this and just consume the direct +// results from the [Plan] and [Apply] functions as described in their +// own documentation. +// +// Hook functions should typically run to completion quickly to avoid noticable +// delays to the progress of the operation being monitored. In particular, +// if a hook implementation is sending data to a network service then the +// actual transmission of the events should be decoupled from the notifications, +// such as by using a buffered channel as a FIFO queue and ideally transmitting +// the events in batches where possible. +type Hooks = stackeval.Hooks + +// ContextWithHooks returns a context that carries the given [Hooks] as +// one of its values. +// +// Pass the resulting context -- or a descendant that preserves the values -- +// to [Plan] or [Apply] to be notified when the different hookable events +// occur during that plan or apply process. +func ContextWithHooks(parent context.Context, hooks *Hooks) context.Context { + return stackeval.ContextWithHooks(parent, hooks) +} diff --git a/internal/stacks/stackruntime/hooks/callbacks.go b/internal/stacks/stackruntime/hooks/callbacks.go new file mode 100644 index 0000000000..7e4ea10067 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/callbacks.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hooks + +import ( + "context" +) + +// BeginFunc is the signature of a callback for a hook which begins a +// series of related events. +// +// The given context is guaranteed to preserve the values from whichever +// context was passed to the top-level [stackruntime.Plan] or +// [stackruntime.Apply] call. +// +// The hook callback may return any value, and that value will be passed +// verbatim to the corresponding [MoreFunc]. A typical use for that +// extra arbitrary value would be to begin a tracing span in "Begin" and then +// either adding events to or ending that span in "More". +// +// If a particular "begin" hook isn't implemented but one of its "more" hooks +// is implemented then the extra tracking value will always be nil when +// the first "more" hook runs. +type BeginFunc[Msg any] func(context.Context, Msg) any + +// MoreFunc is the signature of a callback for a hook which reports +// ongoing process or completion of a multi-step process previously reported +// using a [HookFuncBegin] callback. +// +// The given context is guaranteed to preserve the values from whichever +// context was passed to the top-level [stackruntime.Plan] or +// [stackruntime.Apply] call. +// +// The hook callback recieves an additional argument which is guaranteed to be +// the same value returned from the corresponding [BeginFunc]. See +// [BeginFunc]'s documentation for more information. +// +// If the overall hooks also defines a [ContextAttachFunc] then a context +// descended from its result will be passed into the [MoreFunc] for any events +// related to the operation previously signalled by [BeginFunc]. +// +// The hook callback may optionally return a new arbitrary tracking value. If +// the return value is non-nil then it replaces the original value for future +// hooks belonging to the same context. If it's nil then the previous value +// is retained. +// +// MoreFunc is also sometimes used in isolation for one-shot events, +// in which case the extra value will always be nil unless stated otherwise +// in a particular hook's documentation. +type MoreFunc[Msg any] func(context.Context, any, Msg) any + +// ContextAttachFunc is the signature of an optional callback that knows +// how to bind an arbitrary tracking value previously returned by a [BeginFunc] +// to the values of a [context.Context] so that the tracking value can be +// made available to downstream operations outside the direct scope of the +// stack runtime, such as external HTTP requests. +// +// Use this if your [BeginFunc]s return something that should be visible to +// all context-aware operations within the scope of the operation that was +// begun. +// +// If you use this then your related [MoreFunc] callbacks for the same event +// should always return nil, because there is no way to mutate the context +// with a new tracking value after the fact. +type ContextAttachFunc func(parent context.Context, tracking any) context.Context + +// SingleFunc is the signature of a callback for a hook which operates in +// isolation, and has no related or enclosed events. +// +// The given context is guaranteed to preserve the values from whichever +// context was passed to the top-level [stackruntime.Plan] or +// [stackruntime.Apply] call. +type SingleFunc[Msg any] func(context.Context, Msg) diff --git a/internal/stacks/stackruntime/hooks/component.go b/internal/stacks/stackruntime/hooks/component.go new file mode 100644 index 0000000000..aaf024dfb9 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/component.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hooks + +import "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + +// ComponentInstances is the argument type for the ComponentExpanded hook +// callback, which signals the result of expanding a component into zero or +// more instances. +type ComponentInstances struct { + ComponentAddr stackaddrs.AbsComponent + InstanceAddrs []stackaddrs.AbsComponentInstance +} + +// RemovedComponentInstances is the argument type for the RemovedComponentExpanded hook callback. +type RemovedComponentInstances struct { + Source stackaddrs.StackInstance + InstanceAddrs []stackaddrs.AbsComponentInstance +} diff --git a/internal/stacks/stackruntime/hooks/component_instance.go b/internal/stacks/stackruntime/hooks/component_instance.go new file mode 100644 index 0000000000..721cf976a2 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/component_instance.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hooks + +import ( + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +// ComponentInstanceStatus is a UI-focused description of the overall status +// for a given component instance undergoing a Terraform plan or apply +// operation. The "pending" and "errored" status are used for both operation +// types, and the others will be used only for one of plan or apply. +type ComponentInstanceStatus rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ComponentInstanceStatus component_instance.go + +const ( + ComponentInstanceStatusInvalid ComponentInstanceStatus = 0 + ComponentInstancePending ComponentInstanceStatus = '.' + ComponentInstancePlanning ComponentInstanceStatus = 'p' + ComponentInstancePlanned ComponentInstanceStatus = 'P' + ComponentInstanceApplying ComponentInstanceStatus = 'a' + ComponentInstanceApplied ComponentInstanceStatus = 'A' + ComponentInstanceErrored ComponentInstanceStatus = 'E' + ComponentInstanceDeferred ComponentInstanceStatus = 'D' +) + +// TODO: move this into the rpcapi package somewhere +func (s ComponentInstanceStatus) ForProtobuf() stacks.StackChangeProgress_ComponentInstanceStatus_Status { + switch s { + case ComponentInstancePending: + return stacks.StackChangeProgress_ComponentInstanceStatus_PENDING + case ComponentInstancePlanning: + return stacks.StackChangeProgress_ComponentInstanceStatus_PLANNING + case ComponentInstancePlanned: + return stacks.StackChangeProgress_ComponentInstanceStatus_PLANNED + case ComponentInstanceApplying: + return stacks.StackChangeProgress_ComponentInstanceStatus_APPLYING + case ComponentInstanceApplied: + return stacks.StackChangeProgress_ComponentInstanceStatus_APPLIED + case ComponentInstanceErrored: + return stacks.StackChangeProgress_ComponentInstanceStatus_ERRORED + case ComponentInstanceDeferred: + return stacks.StackChangeProgress_ComponentInstanceStatus_DEFERRED + default: + return stacks.StackChangeProgress_ComponentInstanceStatus_INVALID + } +} + +// ComponentInstanceChange is the argument type for hook callbacks which +// signal a set of planned or applied changes for a component instance. +type ComponentInstanceChange struct { + Addr stackaddrs.AbsComponentInstance + Add int + Change int + Import int + Remove int + Defer int + Move int + Forget int +} + +// Total sums all of the change counts as a forwards-compatibility measure. If +// we later add a new change type, older clients will still be able to detect +// that the component instance has some unknown changes, rather than falsely +// stating that there are no changes at all. +func (cic ComponentInstanceChange) Total() int { + return cic.Add + cic.Change + cic.Import + cic.Remove + cic.Defer + cic.Move + cic.Forget +} + +// CountNewAction increments zero or more of the count fields based on the +// given action. +func (cic *ComponentInstanceChange) CountNewAction(action plans.Action) { + switch action { + case plans.Create: + cic.Add++ + case plans.Delete: + cic.Remove++ + case plans.Update: + cic.Change++ + case plans.CreateThenDelete, plans.DeleteThenCreate: + cic.Add++ + cic.Remove++ + case plans.Forget: + cic.Forget++ + case plans.CreateThenForget: + cic.Add++ + cic.Forget++ + } +} diff --git a/internal/stacks/stackruntime/hooks/componentinstancestatus_string.go b/internal/stacks/stackruntime/hooks/componentinstancestatus_string.go new file mode 100644 index 0000000000..8abf8df383 --- /dev/null +++ b/internal/stacks/stackruntime/hooks/componentinstancestatus_string.go @@ -0,0 +1,55 @@ +// Code generated by "stringer -type=ComponentInstanceStatus component_instance.go"; DO NOT EDIT. + +package hooks + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ComponentInstanceStatusInvalid-0] + _ = x[ComponentInstancePending-46] + _ = x[ComponentInstancePlanning-112] + _ = x[ComponentInstancePlanned-80] + _ = x[ComponentInstanceApplying-97] + _ = x[ComponentInstanceApplied-65] + _ = x[ComponentInstanceErrored-69] + _ = x[ComponentInstanceDeferred-68] +} + +const ( + _ComponentInstanceStatus_name_0 = "ComponentInstanceStatusInvalid" + _ComponentInstanceStatus_name_1 = "ComponentInstancePending" + _ComponentInstanceStatus_name_2 = "ComponentInstanceApplied" + _ComponentInstanceStatus_name_3 = "ComponentInstanceDeferredComponentInstanceErrored" + _ComponentInstanceStatus_name_4 = "ComponentInstancePlanned" + _ComponentInstanceStatus_name_5 = "ComponentInstanceApplying" + _ComponentInstanceStatus_name_6 = "ComponentInstancePlanning" +) + +var ( + _ComponentInstanceStatus_index_3 = [...]uint8{0, 25, 49} +) + +func (i ComponentInstanceStatus) String() string { + switch { + case i == 0: + return _ComponentInstanceStatus_name_0 + case i == 46: + return _ComponentInstanceStatus_name_1 + case i == 65: + return _ComponentInstanceStatus_name_2 + case 68 <= i && i <= 69: + i -= 68 + return _ComponentInstanceStatus_name_3[_ComponentInstanceStatus_index_3[i]:_ComponentInstanceStatus_index_3[i+1]] + case i == 80: + return _ComponentInstanceStatus_name_4 + case i == 97: + return _ComponentInstanceStatus_name_5 + case i == 112: + return _ComponentInstanceStatus_name_6 + default: + return "ComponentInstanceStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/hooks/doc.go b/internal/stacks/stackruntime/hooks/doc.go new file mode 100644 index 0000000000..a7f951019a --- /dev/null +++ b/internal/stacks/stackruntime/hooks/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package hooks is part of an optional API for callers to get realtime +// notifications of various events during the stack runtime's plan and apply +// processes. +// +// [stackruntime.Hooks] is the main entry-point into this API. This package +// contains supporting types and functions that hook implementers will typically +// need. +package hooks diff --git a/internal/stacks/stackruntime/hooks/provisionerstatus_string.go b/internal/stacks/stackruntime/hooks/provisionerstatus_string.go new file mode 100644 index 0000000000..f2fe900c8f --- /dev/null +++ b/internal/stacks/stackruntime/hooks/provisionerstatus_string.go @@ -0,0 +1,37 @@ +// Code generated by "stringer -type=ProvisionerStatus resource_instance.go"; DO NOT EDIT. + +package hooks + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ProvisionerStatusInvalid-0] + _ = x[ProvisionerProvisioning-112] + _ = x[ProvisionerProvisioned-80] + _ = x[ProvisionerErrored-69] +} + +const ( + _ProvisionerStatus_name_0 = "ProvisionerStatusInvalid" + _ProvisionerStatus_name_1 = "ProvisionerErrored" + _ProvisionerStatus_name_2 = "ProvisionerProvisioned" + _ProvisionerStatus_name_3 = "ProvisionerProvisioning" +) + +func (i ProvisionerStatus) String() string { + switch { + case i == 0: + return _ProvisionerStatus_name_0 + case i == 69: + return _ProvisionerStatus_name_1 + case i == 80: + return _ProvisionerStatus_name_2 + case i == 112: + return _ProvisionerStatus_name_3 + default: + return "ProvisionerStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/hooks/resource_instance.go b/internal/stacks/stackruntime/hooks/resource_instance.go new file mode 100644 index 0000000000..b4c82acf2d --- /dev/null +++ b/internal/stacks/stackruntime/hooks/resource_instance.go @@ -0,0 +1,119 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package hooks + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +// ResourceInstanceStatus is a UI-focused description of the overall status +// for a given resource instance undergoing a Terraform plan or apply +// operation. The "pending" and "errored" status are used for both operation +// types, and the others will be used only for one of plan or apply. +type ResourceInstanceStatus rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ResourceInstanceStatus resource_instance.go + +const ( + ResourceInstanceStatusInvalid ResourceInstanceStatus = 0 + ResourceInstancePending ResourceInstanceStatus = '.' + ResourceInstanceRefreshing ResourceInstanceStatus = 'r' + ResourceInstanceRefreshed ResourceInstanceStatus = 'R' + ResourceInstancePlanning ResourceInstanceStatus = 'p' + ResourceInstancePlanned ResourceInstanceStatus = 'P' + ResourceInstanceApplying ResourceInstanceStatus = 'a' + ResourceInstanceApplied ResourceInstanceStatus = 'A' + ResourceInstanceErrored ResourceInstanceStatus = 'E' +) + +// TODO: move this into the rpcapi package somewhere +func (s ResourceInstanceStatus) ForProtobuf() stacks.StackChangeProgress_ResourceInstanceStatus_Status { + switch s { + case ResourceInstancePending: + return stacks.StackChangeProgress_ResourceInstanceStatus_PENDING + case ResourceInstanceRefreshing: + return stacks.StackChangeProgress_ResourceInstanceStatus_REFRESHING + case ResourceInstanceRefreshed: + return stacks.StackChangeProgress_ResourceInstanceStatus_REFRESHED + case ResourceInstancePlanning: + return stacks.StackChangeProgress_ResourceInstanceStatus_PLANNING + case ResourceInstancePlanned: + return stacks.StackChangeProgress_ResourceInstanceStatus_PLANNED + case ResourceInstanceApplying: + return stacks.StackChangeProgress_ResourceInstanceStatus_APPLYING + case ResourceInstanceApplied: + return stacks.StackChangeProgress_ResourceInstanceStatus_APPLIED + case ResourceInstanceErrored: + return stacks.StackChangeProgress_ResourceInstanceStatus_ERRORED + default: + return stacks.StackChangeProgress_ResourceInstanceStatus_INVALID + } +} + +// ProvisionerStatus is a UI-focused description of the progress of a given +// resource instance's provisioner during a Terraform apply operation. Each +// specified provisioner will start in "provisioning" state, and progress to +// either "provisioned" or "errored". +type ProvisionerStatus rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type=ProvisionerStatus resource_instance.go + +const ( + ProvisionerStatusInvalid ProvisionerStatus = 0 + ProvisionerProvisioning ProvisionerStatus = 'p' + ProvisionerProvisioned ProvisionerStatus = 'P' + ProvisionerErrored ProvisionerStatus = 'E' +) + +// TODO: move this into the rpcapi package somewhere +func (s ProvisionerStatus) ForProtobuf() stacks.StackChangeProgress_ProvisionerStatus_Status { + switch s { + case ProvisionerProvisioning: + return stacks.StackChangeProgress_ProvisionerStatus_PROVISIONING + case ProvisionerProvisioned: + return stacks.StackChangeProgress_ProvisionerStatus_PROVISIONING + case ProvisionerErrored: + return stacks.StackChangeProgress_ProvisionerStatus_ERRORED + default: + return stacks.StackChangeProgress_ProvisionerStatus_INVALID + } +} + +// ResourceInstanceStatusHookData is the argument type for hook callbacks which +// signal a resource instance's status updates. +type ResourceInstanceStatusHookData struct { + Addr stackaddrs.AbsResourceInstanceObject + ProviderAddr addrs.Provider + Status ResourceInstanceStatus +} + +// ResourceInstanceProvisionerHookData is the argument type for hook callbacks +// which signal a resource instance's provisioner progress, including both +// status updates and optional provisioner output data. +type ResourceInstanceProvisionerHookData struct { + Addr stackaddrs.AbsResourceInstanceObject + Name string + Status ProvisionerStatus + Output *string +} + +// ResourceInstanceChange is the argument type for hook callbacks which signal +// a detected or planned change for a resource instance resulting from a plan +// operation. +type ResourceInstanceChange struct { + Addr stackaddrs.AbsResourceInstanceObject + Change *plans.ResourceInstanceChangeSrc +} + +// DeferredResourceInstanceChange is the argument type for hook callbacks which +// signal a deferred change for a resource instance resulting from a plan +// operation. +type DeferredResourceInstanceChange struct { + Reason providers.DeferredReason + Change *ResourceInstanceChange +} diff --git a/internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go b/internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go new file mode 100644 index 0000000000..217e48460e --- /dev/null +++ b/internal/stacks/stackruntime/hooks/resourceinstancestatus_string.go @@ -0,0 +1,57 @@ +// Code generated by "stringer -type=ResourceInstanceStatus resource_instance.go"; DO NOT EDIT. + +package hooks + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ResourceInstanceStatusInvalid-0] + _ = x[ResourceInstancePending-46] + _ = x[ResourceInstanceRefreshing-114] + _ = x[ResourceInstanceRefreshed-82] + _ = x[ResourceInstancePlanning-112] + _ = x[ResourceInstancePlanned-80] + _ = x[ResourceInstanceApplying-97] + _ = x[ResourceInstanceApplied-65] + _ = x[ResourceInstanceErrored-69] +} + +const ( + _ResourceInstanceStatus_name_0 = "ResourceInstanceStatusInvalid" + _ResourceInstanceStatus_name_1 = "ResourceInstancePending" + _ResourceInstanceStatus_name_2 = "ResourceInstanceApplied" + _ResourceInstanceStatus_name_3 = "ResourceInstanceErrored" + _ResourceInstanceStatus_name_4 = "ResourceInstancePlanned" + _ResourceInstanceStatus_name_5 = "ResourceInstanceRefreshed" + _ResourceInstanceStatus_name_6 = "ResourceInstanceApplying" + _ResourceInstanceStatus_name_7 = "ResourceInstancePlanning" + _ResourceInstanceStatus_name_8 = "ResourceInstanceRefreshing" +) + +func (i ResourceInstanceStatus) String() string { + switch { + case i == 0: + return _ResourceInstanceStatus_name_0 + case i == 46: + return _ResourceInstanceStatus_name_1 + case i == 65: + return _ResourceInstanceStatus_name_2 + case i == 69: + return _ResourceInstanceStatus_name_3 + case i == 80: + return _ResourceInstanceStatus_name_4 + case i == 82: + return _ResourceInstanceStatus_name_5 + case i == 97: + return _ResourceInstanceStatus_name_6 + case i == 112: + return _ResourceInstanceStatus_name_7 + case i == 114: + return _ResourceInstanceStatus_name_8 + default: + return "ResourceInstanceStatus(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/README.md b/internal/stacks/stackruntime/internal/stackeval/README.md new file mode 100644 index 0000000000..60412bf39d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/README.md @@ -0,0 +1,423 @@ +# Terraform Stacks Runtime Internal Architecture + +This directory contains the guts of the Terraform Stacks language runtime. +The public API to this is in the package two levels above this one, +called `stackruntime`. + +The following documentation is aimed at future maintainers of the code in +this package. There is no end-user documentation here. + +## Overview + +If you're arriving here familiar with the runtime of the traditional Terraform +language used for modules -- which we'll call the "modules runtime" in the +remainder of this document -- you will find that things work quite differently +in here. + +The modules runtime works by first explicitly building a dependency graph and +then performing a concurrent walk of that graph, visiting each node and asking +it to "evaluate" itself. "Evaluate" could mean something as simple as just +tweaking some in-memory data, or it could involve a time-consuming call to +a provider plugin. The nodes all collaborate via a shared mutable data structure +called `EvalContext`, which nodes use both to read from and modify the state, +plan, and other relavant metadata during evaluation. + +The stacks runtime is solving broadly the same problem -- scheduling the +execution of various calculations and side-effects into an appropriate order -- +but does so in a different way that relies on an _implicit_ data flow graph +constructed dynamically during evaluation. + +The evaluator does still have a sort of global "god object" that everything +belongs to, which is an instance of type `Main`. However, in this runtime +that object is the entry point to a tree of other objects that each encapsulate +the data only for a particular concept within the language, with data flowing +between them using method calls and return values. + +## Config objects vs. Dynamic Objects + +There are various pairs of types in this package that represent a static object +in the configuration and dynamic instances of that object respectively. + +For example, `InputVariableConfig` directly represents a `variable` block +from a `.tfstack.hcl` file, while `InputVariable` represents the possibly-many +dynamic instances of that object that can be caused by being within a stack +that was called using `for_each`. + +In general, the static types are responsible for "static validation"-type tasks, +such as checking whether expressions refer to instances of other configuration +objects where the configuration object itself doesn't even exist, let alone +any instances of it. The goal is to perform as many checks as possible as +static checks, because that allows us to give feedback about detected problems +as early as possible (during the validation phase), and also avoids redundantly +reporting errors for these problems multiple times when there are multiple +instances of the same problematic object. + +Dynamic types are therefore responsible for everything that needs to respond +to dynamic expression evaluation, and anything which involves interacting with +external systems. For example, creating a plan for a component must be dynamic +because it involves asking providers to perform planning operations that might +contact external servers over the network, and then anything which makes use +of the results from planning is itself a dynamic operation, transitively. + +## Calls vs. Instances + +A subset of the object types in this package have an additional distinction +aside from Config vs. Dynamic. + +`StackCall`, `Component`, and `Provider` all represent dynamic instances +of objects in the configuration that can themselves produce dynamic child +objects. `StackCallInstance`, `ComponentInstance`, and `ProviderInstance` +represent those specific instances. + +What all of these types have in common is that the configuration constructs +they represent each support a `for_each` argument for dynamically declaring +zero or more instances of the object. + +The breakdown of responsibilities for this process has three parts. We'll +use components for the sake of example here, but the same breakdown applies +to stack calls and provider configurations too: + +* `ComponentConfig` represents the actual `component` block in the configuration, + and is responsible for verifying that the component declaration is even valid + regardless of any dynamic information. +* `Component` represents a dynamic instance of one of those `component` blocks, + in the context of a particular stack. This deals with the situation where + a component is itself inside a child stack that was called using a `stack` + block which had `for_each` set, and therefore there are multiple instances + of this component block even before we deal with the component block's _own_ + `for_each` argument. + + The `Component` type is responsible for evaluating the `for_each` expression. + + The `Component` type is also responsible for producing the value that + would be placed in scope to handle a reference like `component.foo`, + which it does by collecting up the results from each instance implied + by the `for_each` expression and returning them as a mapping. +* `ComponentInstance` represents just one of the instances produced by the + `component` block's own `for_each` expression. + + This type is therefore responsible for evaluating any of the arguments + that are permitted to refer to `each.key` or `each.value` and could + therefore vary between instances. It's also responsible for the main + dynamic behavior of components, which is creating plans, applying them, + and reporting their results. + +## Object Singletons + +Almost everything reachable from a `Main` object must be treated as a singleton, +because these objects contain the tracking information for asynchronous +work in progress and the results of previously-completed asynchronous work. + +The guarantee of ensuring that each object is indeed treated as a singleton +is the responsiblity of some other object which we consider the child to be +contained within. + +For example, the `Main` object itself is responsible for instantiating the +`Stack` object representing the main stack (aka the "root" stack) and then +remembering it so it can return the same object on future requests. However, +any child stacks are tracked inside the state of the root stack object, +and so the root stack is responsible for ensuring the uniqueness of those +across multiple calls. This continues down the tree, with every object +except the `Main` object being the responsibility of exactly one managing +parent. + +Failing to preserve this guarantee would cause duplicate work and potentially +inconsistent results, assuming that the work in question does not behave as a +pure function. To help future maintainers preserve the guarantee, there is +a convention that new instances of all of the model types in this package +are produced using an unexported function, such as `newStack`, and that +each of those functions must be called only from one other place within +the managing parent of each type. + +(Stacks themselves are a slight exception to this rule because the managing +parent of the main stack is `Main` while the managing parent of all other +stacks is the parent `Stack`. There must therefore be two callsites for +`newStack`, but they are written in such a way as to avoid trampling on each +other's responsibilities.) + +The actual singleton objects are retained in an unexported map inside the +managing parent. They are typically created only on first request from +some other caller, via a method of the managing parent. The resulting new object +is then saved in the map to be returned on future calls. + +Instances of the `...Config` types should typically be singleton per +`Main` object, because they are static by definition. + +Instances of dynamic types are actually only singleton per _evaluation phase_, +since e.g. the behavior of a `ComponentInstance` is different when we're trying +to create a plan than when we are trying to apply a plan previously created. +More on that in the next section. + +## Evaluation Phases + +Each `Main` object is typically instantiated for only one evaluation phase, +which from the external caller's perspective is controlled by which of the +factory functions they call. + +Internally we track evaluation phases as instances of `EvalPhase`, which +is a comparable type that we use internally to differentiate between the +singletons created for one phase and the singletons created for another. + +Since currently each `Main` has only one evaluation phase, this is actually +technically redundant: a `Main` instantiated for planning would produce +only objects for the `PlanPhase` phase. + +However, the implementation nonetheless tracks a separate pool of singletons +per phase and requires any operation that performs expression evaluation to +explicitly say which evaluation phase it's for, as some insurance both against +bugs that might otherwise be quite hard to track down and against possible +future needs that might call for us needing to blend work for multiple phases +into the same `Main` object for some reason. + +* `NewForValidating` returns a `Main` for `ValidatePhase`, which is capable + only of static validation and will fail any dynamic evaluation work. +* `NewForPlanning` returns a `Main` for `PlanPhase`, bound to a particular + prior state and planning options. +* `NewForApplying` returns a `Main` for `ApplyPhase`, bound to a particular + stack plan, which itself includes the usual stuff like the prior state, + the planned changes, input variable values that were specified during + planning, etc. +* `NewForInspecting` returns a `Main` for `InspectPhase`, which is a special + phase that is intended for implementing less-commonly-used utilities such + as something equivalent to `terraform console` but for Stacks. In this + case, the evaluator is bound only to a prior state, and just returns values + directly from that state without trying to plan or apply any changes. + + This phase is also handy for unit testing of parts of the runtime that + don't rely on external side-effects; many of the unit tests in this + package do their work in `InspectPhase`, particularly if testing an + object whose direct behavior does not vary based on the evaluation + phase. It's still important to test in other phases for operations whose + behavior varies by phase, of course! + +## Expression Evaluation + +The most important cross-cutting behavior in the language runtime is the +evaluation of user-provided expressions. The main function for that is +`EvalExpr`, but there's also `EvalBody` for evaluating all of the expressions +in a dynamic body at once, and extensions such as `EvalExprAndEvalContext` +which also returns some of the information that was used during evaluation +so that callers can produce more helpful diagnostic messages. + +The actual evaluation process involves two important concepts: + +- `EvaluationScope` is an interface implemented by objects that can have + expressions evaluated inside them. Each `Stack` is effectively a + "global scope", and then some child objects like `Component`, `StackCall`, + and `Provider` act as _child_ scopes which extend the global scope with + local context like `each.key`, `each.value`, and `self`. + + An evaluation scope's responsibility is to translate a `stackaddrs.Reference` + (a representation of an already-decoded reference expression) into an object + that implements `Referenceable`. + +- `Referenceable` is an interface implemented by objects that can be referred + to in expressions. For example, a reference expression like `var.foo` + should refer to an `InputVariable` object, and so `InputVariable` implements + `Referenceable` to decide the actual value to use for that reference. + + The responsibility of an implementation of this interface is simply to + return a `cty.Value` to insert into the expression scope for a particular + `EvalPhase`. For example, a `Component` object implements this interface + by returning an object containing all of the output values from the + component's plan when asked for `PlanPhase`, but returns the output values + from the final state instead when asked for `ApplyPhase`. + +Overall then, the expression evaluation process has the following main steps: + +1. Analyze the expression or collection of expressions to find all of the + HCL symbol references (`hcl.Traversal` values). +2. Use `stackaddrs.ParseReference` to try to raise the reference into one of + the higher-level address types, wrapped in a `stackaddrs.Reference`. + + We fail at this step for syntactically-invalid references, but this step + has no access to the dynamic symbol table so it cannot catch references to + objects that don't exist. +3. Pass the `stackaddrs.Reference` value to the caller's selected + `EvaluationScope` implementation, which checks whether the address refers + to an object that's actually declared, and if so returns that object. + This uses `EvaluationScope.ResolveExpressionReference`. + + This step fails if the reference is syntactically valid but refers to + something that isn't actually declared. + + Objects that expressions can refer to must implement `Referenceable`. +4. Call `ExprReferenceValue` on each of the collected `Referenceable` objects, + passing the caller's `EvalPhase`. + + That method must then return a `cty.Value`. If something has gone wrong + upstream that prevents returning a concrete value, the method should return + some kind of unknown value -- ideally with a type constraint, but as + `cty.DynamicVal` as a last resort -- so that evaluation can continue + downstream just enough to let the call stacks all unwind and collect + all the error diagnostics up at the top. +5. Assemble all of the collected values into a suitably-shaped `hcl.EvalContext`, + attach the usual repertiore of available functions, and finally ask the + original expression to evaluate itself in that evaluation context. + + Failures can occur here if the expression itself is invalid in some way, + such as trying to add together values that cannot convert to number, or + other similar kinds of type/value expectation mismatch. + +## Checked vs. Unchecked Results + +Data flow between objects in a particular evaluator happens mostly on request. + +For example, if a `component` block contains a reference to `var.foo` then +as part of evaluating that expression the `Component` or `ComponentInstance` +object will (indirectly, through the expression evaluator) ask the +`InputVariable` object for `variable "foo"` to produce its value, and only +at that point will the `InputVariable` object begin the work of evaluating +that value, which could involve evaluating yet another expression, and so on. + +Because the flow of requests between objects is dynamic, and because many +different requesters can potentially ask for the same result via different +call paths, if an error or warning diagnostic is returned we need to make sure +_that_ propagates by only one return path to avoid returning the same +diagnostic message multiple times. + +To deal with that problem, operations that can return diagnostics are typically +split into two methods. One of them has a `Check` prefix, indicating that +it is responsible for propagating any diagnostics, and the other lacks the +prefix. + +For example, `InputVariable` has both `Value` and `CheckValue`. The latter +returns `(cty.Value, tfdiags.Diagnostics)`, while the former just wraps the +latter and discards the diagnostics completely. + +This strategy assumes two important invariants: +- Every fallible operation can produce some kind of inert placeholder result + when it fails, which we can use to unwind everything else that's depending + on the result without producing any new errors. (or, in some cases, producing + a minimal amount of additional errors that each add more information than + the original one did, as a last resort when the ideal isn't possible). +- Only one codepath is responsible for calling the `Check...` variant of the + function, and everything else will use the unprefixed version and just + deal with getting a placeholder result sometimes. + +This is quite different than how we've dealt with diagnostics in other parts +of Terraform, and does unfortunately require some additional care under future +maintenence to preserve those invariants, but following the naming convention +across all of the object types will hopefully make these special rules easier +to learn and then maintain under future changes. + +In practice, the one codepath that calls the `Check...` variants is the +"walk" codepath, which is discussed in the next section. + +## Static and Dynamic "Walks" + +As discussed in the previous section, most results in the stacks runtime +are produced only when requested. That means that if no other object in +the configuration were to include an expression referring to `var.foo`, +it might never get any opportunity to evaluate itself and raise any errors +in its declaration or definition. + +To make sure that every relevant object gets visited at least once, each of +the main evaluation phases (not `InspectPhase`) has at least one "walk" +associated with it, which navigates the entire tree of relevant objects +accessible from the `Main` object and calls a phase-specific method on +each one. + +There are two "walk drivers" that arrange for traversing different subsets +of the objects: +- The "static" walk is used for both `ValidatePhase` and `PlanPhase`, and + visits only the objects of `Config`-suffixed types, representing static + configuration objects. +- The "dynamic" walk is used for both `PlanPhase` and `ApplyPhase`, and + visits both the main dynamic objects (the ones of types with no special + suffix) and the objects of `Instance`-suffixed types that represent + dynamic instances of each configuration object. + +The "walk driver" decides which objects need to be visited, calling a callback +function for each object. Each phase calls a different method of each visited +object in its callback: +- `ValidatePhase` calls the `Validate` method of interface `Validatable`, + which is only allowed to return diagnostics and should not have any + externally-visible side-effects. +- `PlanPhase` calls the `PlanChanges` method of interface `Plannable`, + which can return an arbitrary number of "planned change" objects that + should be returned to the caller to contribute to the plan, and an arbitrary + number of diagnostics. +- `ApplyPhase` calls the `CheckApply` method of interface `Applyable`, + which is responsible for collecting the results of apply actions that are + actually scheduled elsewhere, since the runtime wants a little more control + over the execution of the side-effect heavy apply actions. This returns an + arbitrary number of "applied change" objects that each represents a + mutation of the state, and an arbitrary number of diagnostics. + +Those who are familiar with Terraform's modules runtime might find this +"walk" idea roughly analogous to the process of building a graph and then +walking it concurrently while preserving dependencies. The stack runtime +walks are different in that they are instead walking the _tree_ of objects +accessible from `Main`, and they don't need to be concerned about ordering +because the dynamic data flow between the different objects -- where a method +of one object can block on the completion of a method of another -- causes a +suitable evaluation order automatically. + +The scheduling here is dynamic and emerges automatically from the control +flow. The runtime achieves this by having any operation that depends on +expensive or side-effect-ish work from another object pass the data using +the promises and tasks model implemented by +[package `promising`](../../../../promising/README.md). + +## Apply-phase Scheduling + +During the validation and planning operations the order of work is driven +entirely by the dynamically-constructed data flow graph that gets assembled +automatically based on control flow between the different functions in this +package. That works under the assumption that those phases should not be +modifying anything outside of Terraform itself and so our only concern is +ensuring that data is available at the appropriate time for other functions +that will make use of it. + +However, the apply phase deals with externally-visible side-effects whose +relative ordering is very important. For example, in some remote APIs an +attempt to destroy one object before destroying another object that depends +on it will either fail with an error or hang until a timeout is reached, and +so it's crucially important that Terraform directly consider the sequence +of operations to make sure that situation cannot possibly arise, even if +the relationship is not implied naturally by data flow. + +We deal with those additional requirements with both an additional scheduling +primitive -- function `ChangeExec` -- and with some explicit dependency data +gathered during the planning phase. + +In practice, it's only _components_ that represent operations with explicit +ordering constraints, because nothing else in the stacks runtime directly +interacts with Terraform's resource instance change lifecycle. Therefore +we can achieve a correct result with only a graph of dependencies between +components, without considering any other objects. Interface `Applyable` +includes the method `RequiredComponents`, which must return a set of all +of the components that a particular applyable object depends on. + +In practice, most of our implementations of `Applyable.RequiredComponents` +wrap a single implementation that works in terms of interface `Referrer`, which +works at a lower level of abstraction that deals only in HCL-level expression +references, regardless of what object types they refer to. The shared +implementation then raises the graph of references into a graph of components +by essentially removing the non-component nodes while preserving the +edges between them. + +Once the plan phase has derived the relationships between components, it +includes that information as part of the plan, so that it's immediately ready +to use in the apply phase without any further graph construction. + +The apply phase then uses the `ChangeExec` function to actually schedule the +changes. That function's own documentation contains more documentation about +its usage, but at a high level it wraps the concepts from +[package `promising`](../../../../promising/README.md) in such a way that +it can oversee the execution of each of the individual component instance apply +phases, and capture the results in a central place for downstream work to +refer to. Each component instance is represented by a single task which blocks +on the completion of the promise of each component it depends on, thus explicitly +ensuring that the component instance changes get applied in the correct +order relative to one another. + +Since the `ChangeExec` usage is concerned only with component instances, the +apply phase still performs a concurrent dynamic walk as described in the +previous section to ensure that all other objects in the configuration will be +visited and have a chance to announce any problems they detect. The significant +difference for the apply phase is that anything which refers to a component +instance will block until the `ChangeExec`-managed apply phase for that +component instance has completed. Otherwise, the usual data-flow-driven +scheduling decides on the evaluation order for all other object types. diff --git a/internal/stacks/stackruntime/internal/stackeval/applying.go b/internal/stacks/stackruntime/internal/stackeval/applying.go new file mode 100644 index 0000000000..8b8ffd7337 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/applying.go @@ -0,0 +1,334 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval/stubs" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ApplyOpts struct { + ProviderFactories ProviderFactories + DependencyLocks depsfile.Locks + + // PrevStateDescKeys is a set of all of the state description keys currently + // known by the caller. + // + // The apply phase uses this to perform any broad "description maintenence" + // that might need to happen to contend with changes to the state + // description representation over time. For example, if any of the given + // keys are unrecognized and classifed as needing to be discarded when + // unrecognized then the apply phase will use this to emit the necessary + // "discard" events to keep the state consistent. + PrevStateDescKeys collections.Set[statekeys.Key] + + // InputVariableValues are variable values to use during the apply phase. + // + // This should typically include values for only variables that were + // marked as being "required on apply" in the plan, but for ease of use + // it's also valid to set other input variables here as long as the + // given value is exactly equal to what was used during planning. + InputVariableValues map[stackaddrs.InputVariable]ExternalInputValue + + ExperimentsAllowed bool +} + +// Applyable is an interface implemented by types which represent objects +// that can potentially produce diagnostics and object change reports during +// the apply phase. +// +// Unlike [Plannable], Applyable implementations do not actually apply +// changes themselves. Instead, the real changes get driven separately using +// the [ChangeExec] function (see [ApplyPlan]) and then we collect up any +// reports to send to the caller separately using this interface. +type Applyable interface { + // CheckApply checks the receiver's apply-time result and returns zero + // or more applied change descriptions and zero or more diagnostics + // describing any problems that occured for this specific object during + // the apply phase. + // + // CheckApply must not report any diagnostics raised indirectly by + // evaluating other objects. Those will be collected separately by calling + // this same method on those other objects. + CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) + + // Our general async planning helper relies on this to name its + // tracing span. + tracingNamer +} + +// ApplyableComponentInstance is an interface that represents a single instance +// of a component that can be applied. This is going to be a ComponentInstance +// or a RemovedComponentInstance. +type ApplyableComponentInstance interface { + ConfigComponentExpressionScope[stackaddrs.AbsComponentInstance] + + // ApplyModuleTreePlan applies the given plan to the module tree of this + // component instance, returning the result of the apply operation and + // any diagnostics that were generated. + ApplyModuleTreePlan(ctx context.Context, plan *plans.Plan) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) + + // PlaceholderApplyResultForSkippedApply returns a placeholder apply result + // for the case where the apply operation was skipped. This is used to + // ensure that the apply operation always returns a result, even if it + // didn't actually do anything. + PlaceholderApplyResultForSkippedApply(plan *plans.Plan) *ComponentInstanceApplyResult +} + +func ApplyComponentPlan(ctx context.Context, main *Main, plan *plans.Plan, requiredProviders map[addrs.LocalProviderConfig]hcl.Expression, inst ApplyableComponentInstance) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // NOTE WELL: This function MUST either successfully apply the component + // instance's plan or return at least one error diagnostic explaining why + // it cannot. + // + // All return paths must include a non-nil ComponentInstanceApplyResult. + // If an error occurs before we even begin applying the plan then the + // result should report that the changes are incomplete and that the + // new state is exactly the previous run state. + // + // If the underlying modules runtime raises errors when asked to apply the + // plan, then this function should pass all of those errors through to its + // own diagnostics while still returning the presumably-partially-updated + // result state. + + // This is the result to return along with any errors that prevent us from + // even starting the modules runtime apply phase. It reports that nothing + // changed at all. + noOpResult := inst.PlaceholderApplyResultForSkippedApply(plan) + + stackPlan := main.PlanBeingApplied().GetComponent(inst.Addr()) + + // We'll gather up our set of potentially-affected objects before we do + // anything else, because the modules runtime tends to mutate the objects + // accessible through the given plan pointer while it does its work and + // so we're likely to get a different/incomplete answer if we ask after + // work has already been done. + affectedResourceInstanceObjects := resourceInstanceObjectsAffectedByStackPlan(stackPlan) + + h := hooksFromContext(ctx) + hookSingle(ctx, hooksFromContext(ctx).PendingComponentInstanceApply, inst.Addr()) + seq, ctx := hookBegin(ctx, h.BeginComponentInstanceApply, h.ContextAttach, inst.Addr()) + + moduleTree := inst.ModuleTree(ctx) + if moduleTree == nil { + // We should not get here because if the configuration was statically + // invalid then we should've detected that during the plan phase. + // We'll emit a diagnostic about it just to make sure we're explicit + // that the plan didn't get applied, but if anyone sees this error + // it suggests a bug in whatever calling system sent us the plan + // and configuration -- it's sent us the wrong configuration, perhaps -- + // and so we cannot know exactly what to blame with only the information + // we have here. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Component configuration is invalid during apply", + fmt.Sprintf( + "Despite apparently successfully creating a plan earlier, %s seems to have an invalid configuration during the apply phase. This should not be possible, and suggests a bug in whatever subsystem is managing the plan and apply workflow.", + inst.Addr(), + ), + )) + return noOpResult, diags + } + + providerSchemas, moreDiags, _ := neededProviderSchemas(ctx, main, ApplyPhase, inst) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return noOpResult, diags + } + + providerFactories := make(map[addrs.Provider]providers.Factory, len(providerSchemas)) + for addr := range providerSchemas { + providerFactories[addr] = func() (providers.Interface, error) { + // Lazily fetch the unconfigured client for the provider + // as and when we need it. + provider, err := main.ProviderType(addr).UnconfiguredClient() + if err != nil { + return nil, err + } + // this provider should only be used for selected operations + return stubs.OfflineProvider(provider), nil + } + } + + tfHook := &componentInstanceTerraformHook{ + ctx: ctx, + seq: seq, + hooks: hooksFromContext(ctx), + addr: inst.Addr(), + } + tfCtx, err := terraform.NewContext(&terraform.ContextOpts{ + Hooks: []terraform.Hook{ + tfHook, + }, + Providers: providerFactories, + PreloadedProviderSchemas: providerSchemas, + Provisioners: main.availableProvisioners(), + }) + if err != nil { + // Should not get here because we should always pass a valid + // ContextOpts above. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to instantiate Terraform modules runtime", + fmt.Sprintf("Could not load the main Terraform language runtime: %s.\n\nThis is a bug in Terraform; please report it!", err), + )) + return noOpResult, diags + } + + known, unknown, moreDiags := EvalProviderValues(ctx, main, requiredProviders, ApplyPhase, inst) + if moreDiags.HasErrors() { + // We won't actually add the diagnostics here, they should be + // exposed via a different return path. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot apply component plan", + Detail: fmt.Sprintf("Cannot apply the plan for %s because the configured provider configuration assignments are invalid.", inst.Addr()), + Subject: inst.DeclRange(), + }) + return nil, diags + } + + providerClients := configuredProviderClients(ctx, main, known, unknown, ApplyPhase) + + var newState *states.State + if plan.Applyable { + // When our given context is cancelled, we want to instruct the + // modules runtime to stop the running operation. We use this + // nested context to ensure that we don't leak a goroutine when the + // parent context isn't cancelled. + operationCtx, operationCancel := context.WithCancel(ctx) + defer operationCancel() + go func() { + <-operationCtx.Done() + if ctx.Err() == context.Canceled { + tfCtx.Stop() + } + }() + + // NOTE: tfCtx.Apply tends to make changes to the given plan while it + // works, and so code after this point should not make any further use + // of either "modifiedPlan" or "plan" (since they share lots of the same + // pointers to mutable objects and so both can get modified together.) + newState, moreDiags = tfCtx.Apply(plan, moduleTree, &terraform.ApplyOpts{ + ExternalProviders: providerClients, + }) + diags = diags.Append(moreDiags) + } else { + // For a non-applyable plan, we just skip trying to apply it altogether + // and just propagate the prior state (including any refreshing we + // did during the plan phase) forward. + newState = plan.PriorState + } + + if newState != nil { + cic := &hooks.ComponentInstanceChange{ + Addr: inst.Addr(), + + // We'll increment these gradually as we visit each change below. + Add: 0, + Change: 0, + Import: 0, + Remove: 0, + Move: 0, + Forget: 0, + + // The defer changes amount is a bit funny - we just copy over the + // count of deferred changes from the plan, but we're not actually + // making changes for this so the "true" count is zero. + Defer: stackPlan.DeferredResourceInstanceChanges.Len(), + } + + // We need to report what changes were applied, which is mostly just + // re-announcing what was planned but we'll check to see if our + // terraform.Hook implementation saw a "successfully applied" event + // for each resource instance object before counting it. + applied := tfHook.ResourceInstanceObjectsSuccessfullyApplied() + for _, rioAddr := range applied { + action := tfHook.ResourceInstanceObjectAppliedAction(rioAddr) + cic.CountNewAction(action) + } + + // The state management actions (move, import, forget) don't emit + // actions during an apply so they're not being counted by looking + // at the ResourceInstanceObjectAppliedAction above. + // + // Instead, we'll recheck the planned actions here to count them. + for _, rioAddr := range affectedResourceInstanceObjects { + if applied.Has(rioAddr) { + // Then we processed this above. + continue + } + + change, exists := stackPlan.ResourceInstancePlanned.GetOk(rioAddr) + if !exists { + // This is a bit weird, but not something we should prevent + // the apply from continuing for. We'll just ignore it and + // assume that the plan was incomplete in some way. + continue + } + + // Otherwise, we have a change that wasn't successfully applied + // for some reason. If the change was a no-op and a move or import + // then it was still successful so we'll count it as such. Also, + // forget actions don't count as applied changes but still happened + // so we'll count them here. + + switch change.Action { + case plans.NoOp: + if change.Importing != nil { + cic.Import++ + } + if change.Moved() { + cic.Move++ + } + case plans.Forget: + cic.Forget++ + } + } + + hookMore(ctx, seq, h.ReportComponentInstanceApplied, cic) + } + + if diags.HasErrors() { + hookMore(ctx, seq, h.ErrorComponentInstanceApply, inst.Addr()) + } else { + hookMore(ctx, seq, h.EndComponentInstanceApply, inst.Addr()) + } + + if newState == nil { + // The modules runtime returns a nil state only if an error occurs + // so early that it couldn't take any actions at all, and so we + // must assume that the state is totally unchanged in that case. + newState = plan.PrevRunState + affectedResourceInstanceObjects = nil + } + + return &ComponentInstanceApplyResult{ + FinalState: newState, + AffectedResourceInstanceObjects: affectedResourceInstanceObjects, + + // Currently our definition of "complete" is that the apply phase + // didn't return any errors, since we expect the modules runtime + // to either perform all of the actions that were planned or + // return errors explaining why it cannot. + Complete: !diags.HasErrors(), + }, diags +} diff --git a/internal/stacks/stackruntime/internal/stackeval/applying_test.go b/internal/stacks/stackruntime/internal/stackeval/applying_test.go new file mode 100644 index 0000000000..82f6074191 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/applying_test.go @@ -0,0 +1,417 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "log" + "slices" + "strings" + "sync" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/encoding/prototext" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate" +) + +func TestApply_componentOrdering(t *testing.T) { + // This verifies that component instances have their plans applied in a + // suitable order during the apply phase, both for normal plans and for + // destroy plans. + // + // This test also creates a plan using the normal planning logic, so + // it partially acts as an integration test for planning and applying + // with component inter-dependencies (since the plan phase is the one + // responsible for actually calculating the dependencies.) + // + // Since this is testing some concurrent code, the test might produce + // false-positives if things just happen to occur in the right order + // despite the sequencing code being incorrect. Consider running this + // test under the Go data race detector to find memory-safety-related + // problems, but also keep in mind that not all sequencing problems are + // caused by data races. + // + // If this test seems to be flaking and the race detector doesn't dig up + // any clues, you might consider the following: + // - Is the code in function ApplyPlan waiting for all of the prerequisites + // captured in the plan? Is it honoring the reversed order expected + // for destroy plans? + // - Is the ChangeExec function, and its subsequent execution, correctly + // scheduling all of the apply tasks that were registered? + // + // If other tests in this package (or that call into this package) are + // also consistently failing, it'd likely be more productive to debug and + // fix those first, which might then give a clue as to what's making this + // test misbehave. + + cfg := testStackConfig(t, "applying", "component_dependencies") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + testProviderAddr := addrs.NewBuiltInProvider("test") + testProviderSchema := providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_report": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "marker": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + } + + cmpAAddr := stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{ + Name: "a", + }, + } + cmpBAddr := stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{ + Name: "b", + }, + } + cmpBInst1Addr := stackaddrs.AbsComponentInstance{ + Stack: cmpBAddr.Stack, + Item: stackaddrs.ComponentInstance{ + Component: cmpBAddr.Item, + Key: addrs.StringKey("i"), + }, + } + cmpCAddr := stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{ + Name: "c", + }, + } + cmpCInstAddr := stackaddrs.AbsComponentInstance{ + Stack: cmpCAddr.Stack, + Item: stackaddrs.ComponentInstance{ + Component: cmpCAddr.Item, + Key: addrs.NoKey, + }, + } + + // First we need to create a plan for this configuration, which will + // include the calculated component dependencies. + planOutput, err := promising.MainTask(ctx, func(ctx context.Context) (*planOutputTester, error) { + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + ProviderFactories: ProviderFactories{ + testProviderAddr: func() (providers.Interface, error) { + return &testing_provider.MockProvider{ + GetProviderSchemaResponse: &testProviderSchema, + PlanResourceChangeFn: func(prcr providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: prcr.ProposedNewState, + } + }, + }, nil + }, + }, + PlanTimestamp: time.Now().UTC(), + }) + + outp, outpTester := testPlanOutput(t) + main.PlanAll(ctx, outp) + + return outpTester, nil + }) + if err != nil { + t.Fatalf("planning failed: %s", err) + } + + rawPlan := planOutput.RawChanges(t) + plan, diags := planOutput.Close(t) + assertNoDiagnostics(t, diags) + + // Before we proceed further we'll check that the plan contains the + // expected dependency relationships, because missing dependency edges + // will make the following tests invalid, and testing this is not + // subject to concurrency-related false-positives. + // + // This is not comprehensive, because the dependency calculation logic + // should already be tested more completely elsewhere. If this part fails + // then hopefully at least one of the planning-specific tests is also + // failing, and will give some more clues as to what's gone wrong here. + if !plan.Applyable { + m := prototext.MarshalOptions{ + Multiline: true, + Indent: " ", + } + for _, raw := range rawPlan { + t.Log(m.Format(raw)) + } + t.Fatalf("plan is not applyable") + } + { + cmpPlan := plan.GetComponent(cmpCInstAddr) + gotDeps := cmpPlan.Dependencies + wantDeps := collections.NewSet[stackaddrs.AbsComponent]() + wantDeps.Add(cmpBAddr) + if diff := cmp.Diff(wantDeps, gotDeps, collections.CmpOptions); diff != "" { + t.Fatalf("wrong dependencies for component.c\n%s", diff) + } + } + { + cmpPlan := plan.GetComponent(cmpBInst1Addr) + gotDeps := cmpPlan.Dependencies + wantDeps := collections.NewSet[stackaddrs.AbsComponent]() + wantDeps.Add(cmpAAddr) + if diff := cmp.Diff(wantDeps, gotDeps, collections.CmpOptions); diff != "" { + t.Fatalf("wrong dependencies for component.b[\"i\"]\n%s", diff) + } + } + + type applyResultData struct { + NewRawState map[string]*anypb.Any + NewState *stackstate.State + VisitedMarkers []string + } + + // Now we're finally ready for the first apply, during which we expect + // the component ordering decided during the plan phase to be respected. + applyResult, err := promising.MainTask(ctx, func(ctx context.Context) (applyResultData, error) { + var visitedMarkers []string + var visitedMarkersMu sync.Mutex + + outp, outpTester := testApplyOutput(t, nil) + + main, err := ApplyPlan(ctx, cfg, plan, ApplyOpts{ + ProviderFactories: ProviderFactories{ + testProviderAddr: func() (providers.Interface, error) { + return &testing_provider.MockProvider{ + GetProviderSchemaResponse: &testProviderSchema, + ApplyResourceChangeFn: func(arcr providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + markerStr := arcr.PlannedState.GetAttr("marker").AsString() + log.Printf("[TRACE] TestApply_componentOrdering: visiting %q", markerStr) + visitedMarkersMu.Lock() + visitedMarkers = append(visitedMarkers, markerStr) + visitedMarkersMu.Unlock() + + return providers.ApplyResourceChangeResponse{ + NewState: arcr.PlannedState, + } + }, + }, nil + }, + }, + }, outp) + if main != nil { + defer main.DoCleanup(ctx) + } + if err != nil { + t.Fatal(err) + } + + assertNoDiagnostics(t, outpTester.Diags()) + + rawState := outpTester.RawUpdatedState(t) + state, diags := outpTester.Close(t) + assertNoDiagnostics(t, diags) + + return applyResultData{ + NewRawState: rawState, + NewState: state, + VisitedMarkers: visitedMarkers, + }, nil + }) + if err != nil { + t.Fatal(err) + } + + { + if len(applyResult.VisitedMarkers) != 5 { + t.Fatalf("apply didn't visit all of the resources\n%s", spew.Sdump(applyResult.VisitedMarkers)) + } + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "a", "b.i", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "a", "b.ii", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "a", "b.iii", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "b.i", "c", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "b.ii", "c", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "b.iii", "c", + ) + } + + // If the initial plan and apply was successful and made its changes in + // the correct order, then we'll also test creating and applying a + // destroy-mode plan. + t.Log("destroy plan") + planOutput, err = promising.MainTask(ctx, func(ctx context.Context) (*planOutputTester, error) { + main := NewForPlanning(cfg, applyResult.NewState, PlanOpts{ + PlanningMode: plans.DestroyMode, + ProviderFactories: ProviderFactories{ + testProviderAddr: func() (providers.Interface, error) { + return &testing_provider.MockProvider{ + GetProviderSchemaResponse: &testProviderSchema, + PlanResourceChangeFn: func(prcr providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: prcr.ProposedNewState, + } + }, + }, nil + }, + }, + PlanTimestamp: time.Now().UTC(), + }) + + outp, outpTester := testPlanOutput(t) + main.PlanAll(ctx, outp) + + return outpTester, nil + }) + if err != nil { + t.Fatalf("planning failed: %s", err) + } + + rawPlan = planOutput.RawChanges(t) + plan, diags = planOutput.Close(t) + assertNoDiagnostics(t, diags) + if !plan.Applyable { + m := prototext.MarshalOptions{ + Multiline: true, + Indent: " ", + } + for _, raw := range rawPlan { + t.Log(m.Format(raw)) + } + t.Fatalf("plan is not applyable") + } + + // When we apply the destroy plan, the components should be visited in + // reverse dependency order to ensure that dependencies outlive their + // dependents. + t.Log("destroy apply") + applyResult, err = promising.MainTask(ctx, func(ctx context.Context) (applyResultData, error) { + var visitedMarkers []string + var visitedMarkersMu sync.Mutex + + outp, outpTester := testApplyOutput(t, nil) + + main, err := ApplyPlan(ctx, cfg, plan, ApplyOpts{ + ProviderFactories: ProviderFactories{ + testProviderAddr: func() (providers.Interface, error) { + return &testing_provider.MockProvider{ + GetProviderSchemaResponse: &testProviderSchema, + ApplyResourceChangeFn: func(arcr providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + markerStr := arcr.PriorState.GetAttr("marker").AsString() + log.Printf("[TRACE] TestApply_componentOrdering: visiting %q", markerStr) + visitedMarkersMu.Lock() + visitedMarkers = append(visitedMarkers, markerStr) + visitedMarkersMu.Unlock() + + return providers.ApplyResourceChangeResponse{ + NewState: arcr.PlannedState, + } + }, + }, nil + }, + }, + }, outp) + if main != nil { + defer main.DoCleanup(ctx) + } + if err != nil { + t.Fatal(err) + } + + assertNoDiagnostics(t, outpTester.Diags()) + + rawState := outpTester.RawUpdatedState(t) + state, diags := outpTester.Close(t) + assertNoDiagnostics(t, diags) + + return applyResultData{ + NewRawState: rawState, + NewState: state, + VisitedMarkers: visitedMarkers, + }, nil + }) + if err != nil { + t.Fatal(err) + } + { + if len(applyResult.VisitedMarkers) != 5 { + t.Fatalf("apply didn't visit all of the resources\n%s", spew.Sdump(applyResult.VisitedMarkers)) + } + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "b.i", "a", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "b.ii", "a", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "b.iii", "a", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "c", "b.i", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "c", "b.ii", + ) + assertSliceElementsInRelativeOrder( + t, applyResult.VisitedMarkers, + "c", "b.iii", + ) + } +} + +func sliceElementsInRelativeOrder[S ~[]E, E comparable](s S, v1, v2 E) bool { + idx1 := slices.Index(s, v1) + idx2 := slices.Index(s, v2) + if idx1 < 0 || idx2 < 0 { + // both values must actually be present for this test to be meaningful + return false + } + return idx1 < idx2 +} + +func assertSliceElementsInRelativeOrder[S ~[]E, E comparable](t *testing.T, s S, v1, v2 E) { + t.Helper() + + if !sliceElementsInRelativeOrder(s, v1, v2) { + t.Fatalf("incorrect element order\ngot: %s\nwant: %#v before %#v", strings.TrimSpace(spew.Sdump(s)), v1, v2) + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/change_exec.go b/internal/stacks/stackruntime/internal/stackeval/change_exec.go new file mode 100644 index 0000000000..fe9368160d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/change_exec.go @@ -0,0 +1,190 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ChangeExec is a helper for making concurrent changes to a set of objects +// (known ahead of time, statically) during the apply phase. +// +// Most of the work stackeval does is assumed to be just harmless reads that +// can be scheduled at any time as long as implied data dependencies are +// preserved, but during the apply phase we want tighter supervision of the +// changes that actually affect external systems so we can ensure they will +// all run to completion (whether succeeding or failing) and provide the +// data needed for other evaluation work. +// +// The goal of ChangeExec is to encapsulate the promise-based machinery of +// scheduling actions during the apply phase, providing both automatic +// scheduling of the change code and an API for other parts of the system +// to consume the results from the changes, while also helping to deal with +// some slightly-awkward juggling we need to do to make sure that the change +// tasks can interact (in both directions) with the rest of the stackeval +// functionality. +// +// The expected usage pattern is: +// - Call this function with a setup function that synchronously registers +// execution tasks for any changes that appear in a plan that was +// passed into this package's "apply" entry-point. +// - Instantiate a [Main] object that represents the context for the apply +// phase, including inside it the [ChangeExecResults] object returned +// by this function. +// - Call the function returned as this function's second return value with +// the [Main] object just constructed, which will then allow all of the +// registered tasks to begin execution using that object. This MUST be +// done before the completion of whatever task called [ChangeExec]. +// - Evaluate all other relevant objects to collect up any errors they might +// return. This process will typically cause indirect calls to the +// methods of the [ChangeExecResults] object, which will therefore wait +// until the action has completed and obtain its associated result. +func ChangeExec[Main any]( + ctx context.Context, + setup func(ctx context.Context, reg *ChangeExecRegistry[Main])) (*ChangeExecResults, func(context.Context, Main), +) { + // Internally here we're orchestrating a two-phase process: the "setup" + // phase must synchronously register all of the change tasks that need + // to be performed, and then the caller gets an opportunity to store the + // now-frozen results object inside a Main object before starting the + // "execution" phase, where the registered tasks will all become runnable + // simultaneously. + + setupComplete, waitSetupComplete := promising.NewPromise[struct{}](ctx, "setupComplete") + beginExec, waitBeginExec := promising.NewPromise[Main](ctx, "beginExec") + + reg := &ChangeExecRegistry[Main]{ + waitBeginExec: waitBeginExec, + + results: ChangeExecResults{ + componentInstances: collections.NewMap[ + stackaddrs.AbsComponentInstance, + func(ctx context.Context) (withDiagnostics[*ComponentInstanceApplyResult], error), + ](), + }, + } + + // The asynchronous setup task is responsible for resolving setupComplete. + promising.AsyncTask(ctx, setupComplete, func(ctx context.Context, setupComplete promising.PromiseResolver[struct{}]) { + setup(ctx, reg) + setupComplete.Resolve(ctx, struct{}{}, nil) + }) + + // We'll wait until the async setup callback has completed before we return, + // so we can assume that all tasks are registered once we pass this point. + waitSetupComplete(ctx) + + return ®.results, func(ctx context.Context, m Main) { + beginExec.Resolve(ctx, m, nil) + } +} + +// ChangeExecRegistry is used by [ChangeExec] setup functions to register +// change tasks that should run once the caller is ready to begin execution. +type ChangeExecRegistry[Main any] struct { + waitBeginExec func(ctx context.Context) (Main, error) + + // Hold mu for changes to "results" during setup. After setup is + // complete results becomes read-only and so it's no longer + // necessary to hold mu when reading from it. + mu sync.Mutex + results ChangeExecResults +} + +// RegisterComponentInstanceChange registers a change task for a particular +// component instance, which will presumably apply any planned changes for +// that component instance and then return an object representing its +// finalized output values. +func (r *ChangeExecRegistry[Main]) RegisterComponentInstanceChange( + ctx context.Context, + addr stackaddrs.AbsComponentInstance, + run func(ctx context.Context, main Main) (*ComponentInstanceApplyResult, tfdiags.Diagnostics), +) { + resultProvider, waitResult := promising.NewPromise[withDiagnostics[*ComponentInstanceApplyResult]](ctx, "resultProvider") + r.mu.Lock() + if r.results.componentInstances.HasKey(addr) { + // This is always a bug in the caller. + panic(fmt.Sprintf("duplicate change task registration for %s", addr)) + } + r.results.componentInstances.Put(addr, waitResult) + r.mu.Unlock() + + // The asynchronous execution task is responsible for resolving waitResult + // through resultProvider. + promising.AsyncTask(ctx, resultProvider, func(ctx context.Context, resultProvider promising.PromiseResolver[withDiagnostics[*ComponentInstanceApplyResult]]) { + // We'll hold here until the ChangeExec caller signals that it's + // time to begin, by providing a Main object to the begin-execution + // callback that ChangeExec returned. + main, err := r.waitBeginExec(ctx) + if err != nil { + // If we get here then that suggests that there was a self-reference + // error or other promise-related inconsistency, so we'll just + // bail out with a placeholder value and the error. + resultProvider.Resolve(ctx, withDiagnostics[*ComponentInstanceApplyResult]{}, err) + return + } + + // Now the registered task can begin running, with access to the Main + // object that is presumably by now configured to retrieve apply-phase + // results from our corresponding [ChangeExecResults] object. + applyResult, diags := run(ctx, main) + resultProvider.Resolve(ctx, withDiagnostics[*ComponentInstanceApplyResult]{ + Result: applyResult, + Diagnostics: diags, + }, nil) + }) +} + +// ChangeExecResults is the API for callers of [ChangeExec] to access the +// results of any change tasks that were registered by the setup callback. +// +// The accessor methods of this type will block until the associated change +// action has completed, and so callers should first allow the ChangeExec +// tasks to begin executing by calling the activation function that was +// returned from [ChangeExec] alongside the [ChangeExecResults] object. +type ChangeExecResults struct { + componentInstances collections.Map[ + stackaddrs.AbsComponentInstance, + func(context.Context) (withDiagnostics[*ComponentInstanceApplyResult], error), + ] +} + +func (r *ChangeExecResults) ComponentInstanceResult(ctx context.Context, addr stackaddrs.AbsComponentInstance) (*ComponentInstanceApplyResult, tfdiags.Diagnostics, error) { + if r == nil { + panic("no results for nil ChangeExecResults") + } + getter, ok := r.componentInstances.GetOk(addr) + if !ok { + return nil, nil, ErrChangeExecUnregistered{addr} + } + // This call will block until the corresponding execution function has + // completed and resolved this promise. + valWithDiags, err := getter(ctx) + return valWithDiags.Result, valWithDiags.Diagnostics, err +} + +// AwaitCompletion blocks until all of the scheduled changes have completed. +func (r *ChangeExecResults) AwaitCompletion(ctx context.Context) { + // We don't have any single signal that everything is complete here, + // but it's sufficient for us to just visit each of our saved promise + // getters in turn and read from them. + for _, elem := range r.componentInstances.All() { + elem(ctx) // intentionally discards result; we only care that it's complete + } +} + +type ErrChangeExecUnregistered struct { + Addr fmt.Stringer +} + +func (err ErrChangeExecUnregistered) Error() string { + return fmt.Sprintf("no result for unscheduled change to %s", err.Addr.String()) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go b/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go new file mode 100644 index 0000000000..3b2cde274b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/change_exec_test.go @@ -0,0 +1,172 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" +) + +func TestChangeExec(t *testing.T) { + ctx := context.Background() + + type FakeMain struct { + results *ChangeExecResults + } + instAAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "A", + }, + }, + } + instBAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "B", + }, + }, + } + // We don't actually register a task for instCAddr; this one's here + // to test how we handle requesting the result from an unregistered task. + instCAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "C", + }, + }, + } + valueAddr := addrs.OutputValue{Name: "v"}.Absolute(addrs.RootModuleInstance) + + _, err := promising.MainTask(ctx, func(ctx context.Context) (FakeMain, error) { + changeResults, begin := ChangeExec(ctx, func(ctx context.Context, reg *ChangeExecRegistry[FakeMain]) { + t.Logf("begin setup phase") + reg.RegisterComponentInstanceChange(ctx, instAAddr, func(ctx context.Context, main FakeMain) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + t.Logf("producing result for A") + return &ComponentInstanceApplyResult{ + FinalState: states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue(valueAddr, cty.StringVal("a"), false) + }), + }, nil + }) + reg.RegisterComponentInstanceChange(ctx, instBAddr, func(ctx context.Context, main FakeMain) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + t.Logf("B is waiting for A") + aState, _, err := main.results.ComponentInstanceResult(ctx, instAAddr) + if err != nil { + return nil, nil + } + t.Logf("producing result for B") + aOutputVal := aState.FinalState.OutputValue(valueAddr) + if aOutputVal == nil { + return nil, nil + } + return &ComponentInstanceApplyResult{ + FinalState: states.BuildState(func(ss *states.SyncState) { + ss.SetOutputValue( + valueAddr, cty.TupleVal([]cty.Value{aOutputVal.Value, cty.StringVal("b")}), false, + ) + }), + }, nil + }) + t.Logf("end setup phase") + }) + + main := FakeMain{ + results: changeResults, + } + + // We must call "begin" before this task returns, since internally + // there's now a promise that our task is responsible for resolving. + t.Logf("about to start execution phase") + begin(ctx, main) + + // Now we'll pretend that we're doing normal stackeval stuff that + // involves some interdependencies between the results. Specifically, + // the "B" task depends on the result from the "A" task. + var wg sync.WaitGroup + wg.Add(3) + var gotAResult, gotBResult, gotCResult *ComponentInstanceApplyResult + var errA, errB, errC error + promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, _ promising.PromiseContainer) { + t.Logf("requesting result C") + gotCResult, _, errC = main.results.ComponentInstanceResult(ctx, instCAddr) + t.Logf("got result C") + wg.Done() + }) + promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, _ promising.PromiseContainer) { + t.Logf("requesting result B") + gotBResult, _, errB = main.results.ComponentInstanceResult(ctx, instBAddr) + t.Logf("got result B") + wg.Done() + }) + promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, _ promising.PromiseContainer) { + t.Logf("requesting result A") + gotAResult, _, errA = main.results.ComponentInstanceResult(ctx, instAAddr) + t.Logf("got result A") + wg.Done() + }) + wg.Wait() + + if errA != nil { + t.Errorf("A failed: %s", errA) + } + if errB != nil { + t.Errorf("B failed: %s", errB) + } + if diff := cmp.Diff(ErrChangeExecUnregistered{instCAddr}, errC); diff != "" { + t.Errorf("wrong error for C\n%s", diff) + } + if errA != nil || errB != nil { + t.FailNow() + } + if gotAResult == nil { + t.Fatal("A state is nil") + } + if gotBResult == nil { + t.Fatal("B state is nil") + } + if gotCResult != nil { + t.Fatal("C state isn't nil, but should have been") + } + + gotAOutputVal := gotAResult.FinalState.OutputValue(valueAddr) + if gotAOutputVal == nil { + t.Fatal("A state has no value") + } + gotBOutputVal := gotBResult.FinalState.OutputValue(valueAddr) + if gotBOutputVal == nil { + t.Fatal("B state has no value") + } + + gotAVal := gotAOutputVal.Value + wantAVal := cty.StringVal("a") + gotBVal := gotBOutputVal.Value + wantBVal := cty.TupleVal([]cty.Value{wantAVal, cty.StringVal("b")}) + if diff := cmp.Diff(wantAVal, gotAVal, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result for A\n%s", diff) + } + if diff := cmp.Diff(wantBVal, gotBVal, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result for B\n%s", diff) + } + + return main, nil + }) + if err != nil { + t.Fatal(err) + } + +} diff --git a/internal/stacks/stackruntime/internal/stackeval/client_capabilities.go b/internal/stacks/stackruntime/internal/stackeval/client_capabilities.go new file mode 100644 index 0000000000..15202fbb6a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/client_capabilities.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import "github.com/hashicorp/terraform/internal/providers" + +// ClientCapabilities returns the client capabilities sent to the providers +// for each request. They define what this terraform instance is capable of. +func ClientCapabilities() providers.ClientCapabilities { + return providers.ClientCapabilities{ + DeferralAllowed: true, + WriteOnlyAttributesAllowed: true, + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component.go b/internal/stacks/stackruntime/internal/stackeval/component.go new file mode 100644 index 0000000000..fff1123105 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/component.go @@ -0,0 +1,362 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type Component struct { + addr stackaddrs.AbsComponent + + main *Main + stack *Stack + config *ComponentConfig + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*ComponentInstance]]]] + + unknownInstancesMutex sync.Mutex + unknownInstances map[addrs.InstanceKey]*ComponentInstance +} + +var _ Plannable = (*Component)(nil) +var _ Applyable = (*Component)(nil) +var _ Referenceable = (*Component)(nil) + +func newComponent(main *Main, addr stackaddrs.AbsComponent, stack *Stack, config *ComponentConfig) *Component { + return &Component{ + addr: addr, + main: main, + stack: stack, + config: config, + unknownInstances: make(map[addrs.InstanceKey]*ComponentInstance), + } +} + +// ForEachValue returns the result of evaluating the "for_each" expression +// for this stack call, with the following exceptions: +// - If the stack call doesn't use "for_each" at all, returns [cty.NilVal]. +// - If the for_each expression is present but too invalid to evaluate, +// returns [cty.DynamicVal] to represent that the for_each value cannot +// be determined. +// +// A present and valid "for_each" expression produces a result that's +// guaranteed to be: +// - Either a set of strings, a map of any element type, or an object type +// - Known and not null (only the top-level value) +// - Not sensitive (only the top-level value) +func (c *Component) ForEachValue(ctx context.Context, phase EvalPhase) cty.Value { + ret, _ := c.CheckForEachValue(ctx, phase) + return ret +} + +// CheckForEachValue evaluates the "for_each" expression if present, validates +// that its value is valid, and then returns that value. +// +// If this call does not use "for_each" then this immediately returns cty.NilVal +// representing the absense of the value. +// +// If the diagnostics does not include errors and the result is not cty.NilVal +// then callers can assume that the result value will be: +// - Either a set of strings, a map of any element type, or an object type +// - Known and not null (except for nested map/object element values) +// - Not sensitive (only the top-level value) +// +// If the diagnostics _does_ include errors then the result might be +// [cty.DynamicVal], which represents that the for_each expression was so invalid +// that we cannot know the for_each value. +func (c *Component) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + val, diags := doOnceWithDiags( + ctx, c.tracingName()+" for_each", c.forEachValue.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + cfg := c.config.config + + switch { + + case cfg.ForEach != nil: + result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, c.stack, "component") + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + + return result.Value, diags + + default: + // This stack config doesn't use for_each at all + return cty.NilVal, diags + } + }, + ) + if val == cty.NilVal && diags.HasErrors() { + // We use cty.DynamicVal as the placeholder for an invalid for_each, + // to represent "unknown for_each value" as distinct from "no for_each + // expression at all". + val = cty.DynamicVal + } + return val, diags +} + +// Instances returns all of the instances of the call known to be declared +// by the configuration. +// +// Calcluating this involves evaluating the call's for_each expression if any, +// and so this call may block on evaluation of other objects in the +// configuration. +// +// If the configuration has an invalid definition of the instances then the +// result will be nil. Callers that need to distinguish between invalid +// definitions and valid definitions of zero instances can rely on the +// result being a non-nil zero-length map in the latter case. +// +// This function doesn't return any diagnostics describing ways in which the +// for_each expression is invalid because we assume that the main plan walk +// will visit the stack call directly and ask it to check itself, and that +// call will be the one responsible for returning any diagnostics. +func (c *Component) Instances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ComponentInstance, bool) { + ret, unknown, _ := c.CheckInstances(ctx, phase) + return ret, unknown +} + +func (c *Component) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ComponentInstance, bool, tfdiags.Diagnostics) { + result, diags := doOnceWithDiags( + ctx, c.tracingName()+" instances", c.instances.For(phase), + func(ctx context.Context) (instancesResult[*ComponentInstance], tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + forEachVal, forEachValueDiags := c.CheckForEachValue(ctx, phase) + + diags = diags.Append(forEachValueDiags) + if diags.HasErrors() { + return instancesResult[*ComponentInstance]{}, diags + } + + result := instancesMap(forEachVal, func(ik addrs.InstanceKey, rd instances.RepetitionData) *ComponentInstance { + return newComponentInstance(c, stackaddrs.AbsComponentToInstance(c.addr, ik), rd, c.stack.mode, c.stack.deferred) + }) + + addrs := make([]stackaddrs.AbsComponentInstance, 0, len(result.insts)) + for _, ci := range result.insts { + addrs = append(addrs, ci.Addr()) + } + + h := hooksFromContext(ctx) + hookSingle(ctx, h.ComponentExpanded, &hooks.ComponentInstances{ + ComponentAddr: c.addr, + InstanceAddrs: addrs, + }) + + return result, diags + }, + ) + return result.insts, result.unknown, diags +} + +func (c *Component) UnknownInstance(ctx context.Context, key addrs.InstanceKey, phase EvalPhase) *ComponentInstance { + c.unknownInstancesMutex.Lock() + defer c.unknownInstancesMutex.Unlock() + + if inst, ok := c.unknownInstances[key]; ok { + return inst + } + + forEachType := c.ForEachValue(ctx, phase).Type() + repetitionData := instances.UnknownForEachRepetitionData(forEachType) + if key != addrs.WildcardKey { + repetitionData.EachKey = key.Value() + } + + inst := newComponentInstance(c, stackaddrs.AbsComponentToInstance(c.addr, key), repetitionData, c.stack.mode, true) + c.unknownInstances[key] = inst + return inst +} + +func (c *Component) ResultValue(ctx context.Context, phase EvalPhase) cty.Value { + decl := c.config.config + insts, unknown := c.Instances(ctx, phase) + + switch { + case decl.ForEach != nil: + // NOTE: Unlike with StackCall, we must return object types rather than + // map types here since the main Terraform language does not require + // exact type constraints for its output values and so each instance of + // a component can potentially produce a different object type. + + if unknown { + // We can't predict the result if we don't know what the instances + // are, so we'll return dynamic. + return cty.DynamicVal + } + + if insts == nil { + // Then we errored during instance calculation, this should have + // been caught before we got here. + return cty.NilVal + } + + // We expect that the instances all have string keys, which will + // become the keys of a map that we're returning. + elems := make(map[string]cty.Value, len(insts)) + for instKey, inst := range insts { + k, ok := instKey.(addrs.StringKey) + if !ok { + panic(fmt.Sprintf("stack call with for_each has invalid instance key of type %T", instKey)) + } + elems[string(k)] = inst.ResultValue(ctx, phase) + } + if len(elems) == 0 { + return cty.EmptyObjectVal + } + return cty.ObjectVal(elems) + + default: + if insts == nil { + // If we don't even know what instances we have then we can't + // predict anything about our result. + return cty.DynamicVal + } + if len(insts) != 1 { + // Should not happen: we should have exactly one instance with addrs.NoKey + panic("single-instance stack call does not have exactly one instance") + } + inst, ok := insts[addrs.NoKey] + if !ok { + panic("single-instance stack call does not have an addrs.NoKey instance") + } + return inst.ResultValue(ctx, phase) + } +} + +// PlanIsComplete can be called only during the planning phase, and returns +// true only if all instances of this component have "complete" plans. +// +// A component instance plan is "incomplete" if it was either created with +// resource targets set in its planning options or if the modules runtime +// decided it needed to defer at least one action for a future round. +func (c *Component) PlanIsComplete(ctx context.Context) bool { + if !c.main.Planning() { + panic("PlanIsComplete used when not in the planning phase") + } + insts, unknown := c.Instances(ctx, PlanPhase) + if insts == nil { + // Suggests that the configuration was not even valid enough to + // decide what the instances are, so we'll return false to be + // conservative and let the error be returned by a different path. + return false + } + + if unknown { + // If the wildcard key is used the instance originates from an unknown + // for_each value, which means the result is unknown. + return false + } + + for _, inst := range insts { + plan := inst.ModuleTreePlan(ctx) + if plan == nil { + // Seems that we weren't even able to create a plan for this + // one, so we'll just assume it was incomplete to be conservative, + // and assume that whatever errors caused this nil result will + // get returned by a different return path. + return false + } + + if !plan.Complete { + return false + } + } + // If we get here without returning false then we can say that + // all of the instance plans are complete. + return true +} + +// ExprReferenceValue implements Referenceable. +func (c *Component) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + return c.ResultValue(ctx, phase) +} + +func (c *Component) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, _, moreDiags := c.CheckInstances(ctx, phase) + diags = diags.Append(moreDiags) + + return diags +} + +// PlanChanges implements Plannable by performing plan-time validation of +// the component call itself. +// +// The plan walk driver must call [Component.Instances] and also call +// PlanChanges for each instance separately in order to produce a complete +// plan. +func (c *Component) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, c.checkValid(ctx, PlanPhase) +} + +// References implements Referrer +func (c *Component) References(context.Context) []stackaddrs.AbsReference { + cfg := c.config.config + var ret []stackaddrs.Reference + ret = append(ret, ReferencesInExpr(cfg.ForEach)...) + ret = append(ret, ReferencesInExpr(cfg.Inputs)...) + for _, expr := range cfg.ProviderConfigs { + ret = append(ret, ReferencesInExpr(expr)...) + } + ret = append(ret, referencesInTraversals(cfg.DependsOn)...) + return makeReferencesAbsolute(ret, c.addr.Stack) +} + +// RequiredComponents returns the set of required components for this component. +func (c *Component) RequiredComponents(ctx context.Context) collections.Set[stackaddrs.AbsComponent] { + return c.main.requiredComponentsForReferrer(ctx, c, PlanPhase) +} + +// CheckApply implements Applyable. +func (c *Component) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + return nil, c.checkValid(ctx, ApplyPhase) +} + +// ApplySuccessful blocks until all instances of this component have +// completed their apply step and returns whether the apply was successful, +// or panics if called not during the apply phase. +func (c *Component) ApplySuccessful(ctx context.Context) bool { + if !c.main.Applying() { + panic("ApplySuccessful when not applying") + } + + // Apply is successful if all of our instances fully completed their + // apply phases. + insts, _ := c.Instances(ctx, ApplyPhase) + + for _, inst := range insts { + result := inst.ApplyResult(ctx) + if result == nil || !result.Complete { + return false + } + } + + // If we get here then either we had no instances at all or they all + // applied completely, and so our aggregate result is success. + return true +} + +func (c *Component) tracingName() string { + return c.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_config.go b/internal/stacks/stackruntime/internal/stackeval/component_config.go new file mode 100644 index 0000000000..330b52244e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/component_config.go @@ -0,0 +1,416 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + stackparser "github.com/hashicorp/terraform/internal/stacks/stackconfig/parser" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval/stubs" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ Validatable = (*ComponentConfig)(nil) + _ Plannable = (*ComponentConfig)(nil) + _ ExpressionScope = (*ComponentConfig)(nil) + _ ConfigComponentExpressionScope[stackaddrs.ConfigComponent] = (*ComponentConfig)(nil) +) + +type ComponentConfig struct { + addr stackaddrs.ConfigComponent + stack *StackConfig + config *stackconfig.Component + + main *Main + + validate perEvalPhase[promising.Once[tfdiags.Diagnostics]] + moduleTree promising.Once[withDiagnostics[*configs.Config]] // moduleTree is constant across all phases +} + +func newComponentConfig(main *Main, addr stackaddrs.ConfigComponent, stack *StackConfig, config *stackconfig.Component) *ComponentConfig { + return &ComponentConfig{ + addr: addr, + stack: stack, + config: config, + main: main, + } +} + +// Addr implements ConfigComponentExpressionScope +func (c *ComponentConfig) Addr() stackaddrs.ConfigComponent { + return c.addr +} + +// DeclRange implements ConfigComponentExpressionScope +func (c *ComponentConfig) DeclRange() *hcl.Range { + return c.config.DeclRange.ToHCL().Ptr() +} + +// StackConfig implements ConfigComponentExpressionScope +func (c *ComponentConfig) StackConfig() *StackConfig { + return c.stack +} + +// ModuleTree returns the static representation of the tree of modules starting +// at the component's configured source address, or nil if any of the +// modules have errors that prevent even static decoding. +func (c *ComponentConfig) ModuleTree(ctx context.Context) *configs.Config { + ret, _ := c.CheckModuleTree(ctx) + return ret +} + +// CheckModuleTree loads the tree of Terraform modules starting at the +// component block's configured source address, returning the resulting +// configuration object if successful. +// +// If the module has any problems that prevent even static decoding then +// this instead returns diagnostics and a nil configuration object. +func (c *ComponentConfig) CheckModuleTree(ctx context.Context) (*configs.Config, tfdiags.Diagnostics) { + return doOnceWithDiags( + ctx, c.tracingName()+" modules", &c.moduleTree, + func(ctx context.Context) (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + sources := c.main.SourceBundle() + + rootModuleSource := c.config.FinalSourceAddr + if rootModuleSource == nil { + // If we get here then the configuration was loaded incorrectly, + // either by the stackconfig package or by the caller of the + // stackconfig package using the wrong loading function. + panic("component configuration lacks final source address") + } + + parser := configs.NewSourceBundleParser(sources) + parser.AllowLanguageExperiments(c.main.LanguageExperimentsAllowed()) + + if !parser.IsConfigDir(rootModuleSource) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't load module for component", + Detail: fmt.Sprintf("The source location %s does not contain a Terraform module.", rootModuleSource), + Subject: c.config.SourceAddrRange.ToHCL().Ptr(), + }) + return nil, diags + } + + rootMod, hclDiags := parser.LoadConfigDir(rootModuleSource) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + walker := stackparser.NewSourceBundleModuleWalker(rootModuleSource, sources, parser) + configRoot, hclDiags := configs.BuildConfig(rootMod, walker, nil) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + // We also have a small selection of additional static validation + // rules that apply only to modules used within stack components. + diags = diags.Append(validateModuleTreeForStacks(configRoot)) + + return configRoot, diags + }, + ) +} + +// validateModuleTreeForStacks imposes some additional validation constraints +// on a module tree after it's been loaded by the main configuration packages. +// +// These rules deal with a small number of exceptions where the modules language +// as used by stacks is a subset of the modules language from traditional +// Terraform. Not all such exceptions are handled in this way because +// some of them cannot be handled statically, but this is a reasonable place +// to handle the simpler concerns and allows us to return error messages that +// talk specifically about stacks, which would be harder to achieve if these +// exceptions were made at a different layer. +func validateModuleTreeForStacks(startNode *configs.Config) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(validateModuleForStacks(startNode.Path, startNode.Module)) + for _, childNode := range startNode.Children { + diags = diags.Append(validateModuleTreeForStacks(childNode)) + } + return diags +} + +func validateModuleForStacks(moduleAddr addrs.Module, module *configs.Module) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // Inline provider configurations are not allowed when running under stacks, + // because provider configurations live in the stack configuration and + // then get passed in to the modules as special arguments. + for _, pc := range module.ProviderConfigs { + // We use some slightly different language for the topmost module + // that's being directly called from the stack configuration, because + // we can give some direct advice for how to correct the problem there, + // whereas for a nested module we assume that it's a third-party module + // written for much older versions of Terraform before we deprecated + // inline provider configurations and thus the solution is most likely + // to be selecting a different module that is Stacks-compatible, because + // removing a legacy inline provider configuration from a shared module + // would be a breaking change to that module. + if moduleAddr.IsRoot() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Inline provider configuration not allowed", + Detail: "A module used as a stack component must have all of its provider configurations passed from the stack configuration, using the \"providers\" argument within the component configuration block.", + Subject: pc.DeclRange.Ptr(), + }) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Inline provider configuration not allowed", + Detail: "This module is not compatible with Terraform Stacks, because it declares an inline provider configuration.\n\nTo be used with stacks, this module must instead accept provider configurations from its caller.", + Subject: pc.DeclRange.Ptr(), + }) + } + } + + return diags +} + +func (c *ComponentConfig) RootModuleVariableDecls(ctx context.Context) map[string]*configs.Variable { + moduleTree := c.ModuleTree(ctx) + if moduleTree == nil { + // If the module tree is invalid then we'll just assume there aren't + // any variables declared. + return nil + } + return moduleTree.Module.Variables +} + +// InputsType returns an object type that the object representing the caller's +// values for this component's input variables must conform to. +func (c *ComponentConfig) InputsType(ctx context.Context) (cty.Type, *typeexpr.Defaults) { + moduleTree := c.ModuleTree(ctx) + if moduleTree == nil { + // If the module tree is invalid itself then we can't determine which + // input variables are declared. + return cty.NilType, nil + } + + vars := moduleTree.Module.Variables + atys := make(map[string]cty.Type, len(vars)) + defs := &typeexpr.Defaults{ + DefaultValues: make(map[string]cty.Value), + Children: map[string]*typeexpr.Defaults{}, + } + var opts []string + for name, v := range vars { + atys[name] = v.ConstraintType + if def := v.Default; def != cty.NilVal { + defs.DefaultValues[name] = def + opts = append(opts, name) + } + if childDefs := v.TypeDefaults; childDefs != nil { + defs.Children[name] = childDefs + } + } + retTy := cty.ObjectWithOptionalAttrs(atys, opts) + defs.Type = retTy + return retTy, defs +} + +func (c *ComponentConfig) CheckInputVariableValues(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + wantTy, defs := c.InputsType(ctx) + if wantTy == cty.NilType { + // Suggests that the module tree is invalid. We validate the full module + // tree elsewhere, which will hopefully detect the problems here. + return nil + } + + varDecls := c.RootModuleVariableDecls(ctx) + + // We don't care about the returned value, only that it has no errors. + _, diags := EvalComponentInputVariables(ctx, varDecls, wantTy, defs, c.config, phase, c) + return diags +} + +// ExprReferenceValue implements Referenceable. +func (c *ComponentConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + // Currently we don't say anything at all about component results during + // validation, since the main Terraform language's validate call doesn't + // return any information about hypothetical root module output values. + // We don't expose ComponentConfig in any scope outside of the validation + // phase, so this is sufficient for all phases. (See [Component] for how + // component results get calculated during the plan and apply phases.) + + // By calling `checkValid` on ourself here, we will cause a cycle error to be exposed if we ended + // up within this function while executing c.checkValid initially. This just makes sure that there + // are no cycles between components. + c.checkValid(ctx, phase) + return cty.DynamicVal +} + +// ResolveExpressionReference implements ExpressionScope. +func (c *ComponentConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + repetition := instances.RepetitionData{} + if c.config.ForEach != nil { + // For validation, we'll return unknown for the instance data. + repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() + repetition.EachValue = cty.DynamicVal + } + return c.stack.resolveExpressionReference(ctx, ref, nil, repetition) +} + +// ExternalFunctions implements ExpressionScope. +func (c *ComponentConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return c.main.ProviderFunctions(ctx, c.stack) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (c *ComponentConfig) PlanTimestamp() time.Time { + return c.main.PlanTimestamp() +} + +func (c *ComponentConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + diags, err := c.validate.For(phase).Do(ctx, c.tracingName(), func(ctx context.Context) (tfdiags.Diagnostics, error) { + var diags tfdiags.Diagnostics + + moduleTree, moreDiags := c.CheckModuleTree(ctx) + diags = diags.Append(moreDiags) + if moduleTree == nil { + return diags, nil + } + + variableDiags := c.CheckInputVariableValues(ctx, phase) + diags = diags.Append(variableDiags) + + dependsOnDiags := ValidateDependsOn(c.stack, c.config.DependsOn) + diags = diags.Append(dependsOnDiags) + + // We don't actually exit if we found errors with the input variables + // or depends_on attribute, we can still validate the actual module tree + // without them. + + providerTypes, providerDiags := EvalProviderTypes(ctx, c.stack, c.config.ProviderConfigs, phase, c) + diags = diags.Append(providerDiags) + if providerDiags.HasErrors() { + // If there's invalid provider configuration, we can't actually go + // on and validate the module tree. We need the providers and if + // they're invalid we'll just get crazy and confusing errors + // later if we try and carry on. + return diags, nil + } + + providerSchemas, moreDiags, skipFurtherValidation := neededProviderSchemas(ctx, c.main, phase, c) + if skipFurtherValidation { + return diags.Append(moreDiags), nil + } + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return diags, nil + } + + providerFactories := make(map[addrs.Provider]providers.Factory, len(providerSchemas)) + for addr := range providerSchemas { + providerFactories[addr] = func() (providers.Interface, error) { + // Lazily fetch the unconfigured client for the provider + // as and when we need it. + provider, err := c.main.ProviderType(addr).UnconfiguredClient() + if err != nil { + return nil, err + } + // this provider should only be used for selected operations + return stubs.OfflineProvider(provider), nil + } + } + + tfCtx, err := terraform.NewContext(&terraform.ContextOpts{ + Providers: providerFactories, + PreloadedProviderSchemas: providerSchemas, + Provisioners: c.main.availableProvisioners(), + }) + if err != nil { + // Should not get here because we should always pass a valid + // ContextOpts above. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to instantiate Terraform modules runtime", + fmt.Sprintf("Could not load the main Terraform language runtime: %s.\n\nThis is a bug in Terraform; please report it!", err), + )) + return diags, nil + } + + providerClients, valid := unconfiguredProviderClients(c.main, providerTypes) + if !valid { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot validate component", + Detail: fmt.Sprintf("Cannot validate %s because its provider configuration assignments are invalid.", c.addr), + Subject: c.config.DeclRange.ToHCL().Ptr(), + }) + return diags, nil + } + + // When our given context is cancelled, we want to instruct the + // modules runtime to stop the running operation. We use this + // nested context to ensure that we don't leak a goroutine when the + // parent context isn't cancelled. + operationCtx, operationCancel := context.WithCancel(ctx) + defer operationCancel() + go func() { + <-operationCtx.Done() + if ctx.Err() == context.Canceled { + tfCtx.Stop() + } + }() + + diags = diags.Append(tfCtx.Validate(moduleTree, &terraform.ValidateOpts{ + ExternalProviders: providerClients, + })) + return diags, nil + }) + switch err := err.(type) { + case promising.ErrSelfDependent: + // This is a case where the component is self-dependent, which is + // a cycle that we can't resolve. We'll report this as a diagnostic + // and then continue on to report any other diagnostics that we found. + // The promise reporter is main, so that we can get the names of all promises + // involved in the cycle. + diags = diags.Append(diagnosticsForPromisingTaskError(err)) + default: + if err != nil { + // this is crazy, we never return an error from the inner function so + // this really shouldn't happen. + panic(fmt.Sprintf("unexpected error from validate.Do: %s", err)) + } + } + + return diags +} + +// Validate implements Validatable. +func (c *ComponentConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + return c.checkValid(ctx, ValidatePhase) +} + +// PlanChanges implements Plannable. +func (c *ComponentConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, c.checkValid(ctx, PlanPhase) +} + +func (c *ComponentConfig) tracingName() string { + return c.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance.go b/internal/stacks/stackruntime/internal/stackeval/component_instance.go new file mode 100644 index 0000000000..9556f6e436 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance.go @@ -0,0 +1,762 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ComponentInstance struct { + call *Component + addr stackaddrs.AbsComponentInstance + mode plans.Mode + deferred bool + + main *Main + refresh *RefreshInstance + + repetition instances.RepetitionData + + moduleTreePlan promising.Once[withDiagnostics[*plans.Plan]] // moduleTreePlan is only called during the plan phase + inputVariableValues perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ Applyable = (*ComponentInstance)(nil) +var _ Plannable = (*ComponentInstance)(nil) +var _ ExpressionScope = (*ComponentInstance)(nil) +var _ ConfigComponentExpressionScope[stackaddrs.AbsComponentInstance] = (*ComponentInstance)(nil) + +func newComponentInstance(call *Component, addr stackaddrs.AbsComponentInstance, repetition instances.RepetitionData, mode plans.Mode, deferred bool) *ComponentInstance { + component := &ComponentInstance{ + call: call, + addr: addr, + mode: mode, + deferred: deferred, + main: call.main, + repetition: repetition, + } + component.refresh = newRefreshInstance(component) + return component +} + +func (c *ComponentInstance) RepetitionData() instances.RepetitionData { + return c.repetition +} + +func (c *ComponentInstance) InputVariableValues(ctx context.Context, phase EvalPhase) cty.Value { + ret, _ := c.CheckInputVariableValues(ctx, phase) + return ret +} + +func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, c.tracingName()+" inputs", c.inputVariableValues.For(phase), func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + config := c.call.config + wantTy, defs := config.InputsType(ctx) + varDecls := config.RootModuleVariableDecls(ctx) + decl := c.call.config.config + + if wantTy == cty.NilType { + // Suggests that the target module is invalid in some way, so we'll + // just report that we don't know the input variable values and trust + // that the module's problems will be reported by some other return + // path. + return cty.DynamicVal, nil + } + + // We actually checked the errors statically already, so we only care about + // the value here. + val, diags := EvalComponentInputVariables(ctx, varDecls, wantTy, defs, decl, phase, c) + if diags.HasErrors() { + return cty.NilVal, diags + } + return val, diags + }) +} + +// inputValuesForModulesRuntime adapts the result of +// [ComponentInstance.InputVariableValues] to the representation that the +// main Terraform modules runtime expects. +// +// The second argument (expectedValues) is the value that the apply operation +// expects to see for the input variables, which is typically the input +// values from the plan. +// +// During the planning phase, the expectedValues should be nil, as they will +// only be checked during the apply phase. +func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, phase EvalPhase) terraform.InputValues { + valsObj := c.InputVariableValues(ctx, phase) + if valsObj == cty.NilVal { + return nil + } + + // valsObj might be an unknown value during the planning phase, in which + // case we'll return an InputValues with all of the expected variables + // defined as unknown values of their expected type constraints. To + // achieve that, we'll do our work with the configuration's object type + // constraint instead of with the value we've been given directly. + wantTy, _ := c.call.config.InputsType(ctx) + if wantTy == cty.NilType { + // The configuration is too invalid for us to know what type we're + // expecting, so we'll just bail. + return nil + } + wantAttrs := wantTy.AttributeTypes() + ret := make(terraform.InputValues, len(wantAttrs)) + for name, aty := range wantAttrs { + v := valsObj.GetAttr(name) + if !v.IsKnown() { + // We'll ensure that it has the expected type even if + // InputVariableValues didn't know what types to use. + v = cty.UnknownVal(aty) + } + ret[name] = &terraform.InputValue{ + Value: v, + SourceType: terraform.ValueFromCaller, + } + } + return ret + +} + +func (c *ComponentInstance) PlanOpts(ctx context.Context, mode plans.Mode, skipRefresh bool) (*terraform.PlanOpts, tfdiags.Diagnostics) { + decl := c.call.config.config + + inputValues := c.inputValuesForModulesRuntime(ctx, PlanPhase) + if inputValues == nil { + return nil, nil + } + + known, unknown, moreDiags := EvalProviderValues(ctx, c.main, decl.ProviderConfigs, PlanPhase, c) + if moreDiags.HasErrors() { + // We won't actually add the diagnostics here, they should be + // exposed via a different return path. + var diags tfdiags.Diagnostics + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot plan component", + Detail: fmt.Sprintf("Cannot generate a plan for %s because its provider configuration assignments are invalid.", c.Addr()), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + } + + providerClients := configuredProviderClients(ctx, c.main, known, unknown, PlanPhase) + + plantimestamp := c.main.PlanTimestamp() + return &terraform.PlanOpts{ + Mode: mode, + SkipRefresh: skipRefresh, + SetVariables: inputValues, + ExternalProviders: providerClients, + ExternalDependencyDeferred: c.deferred, + DeferralAllowed: true, + // We want the same plantimestamp between all components and the stacks language + ForcePlanTimestamp: &plantimestamp, + }, nil +} + +func (c *ComponentInstance) ModuleTreePlan(ctx context.Context) *plans.Plan { + ret, _ := c.CheckModuleTreePlan(ctx) + return ret +} + +func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { + if !c.main.Planning() { + panic("called CheckModuleTreePlan with an evaluator not instantiated for planning") + } + + return doOnceWithDiags( + ctx, c.tracingName()+" modules", &c.moduleTreePlan, + func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if c.mode == plans.DestroyMode { + + if !c.main.PlanPrevState().HasComponentInstance(c.Addr()) { + // If the component instance doesn't exist in the previous + // state at all, then we don't need to do anything. + // + // This means the component instance was added to the config + // and never applied, or that it was previously destroyed + // via an earlier destroy operation. + // + // Return a dummy plan: + return &plans.Plan{ + UIMode: plans.DestroyMode, + Complete: true, + Applyable: true, + Errored: false, + Timestamp: c.main.PlanTimestamp(), + Changes: plans.NewChangesSrc(), // no changes + }, nil + } + + // If we are destroying, then we are going to do the refresh + // and destroy plan in two separate stages. This helps resolves + // cycles within the dependency graph, as anything requiring + // outputs from this component can read from the refresh result + // without causing a cycle. + + refresh, moreDiags := c.refresh.Plan(ctx) + var filteredDiags tfdiags.Diagnostics + for _, diag := range moreDiags { + if _, ok := addrs.DiagnosticOriginatesFromCheckRule(diag); ok && diag.Severity() == tfdiags.Warning { + // We'll discard diagnostics from check rules here, + // we're about to delete everything so anything not + // valid will go away anyway. + continue + } + filteredDiags = filteredDiags.Append(diag) + } + diags = diags.Append(filteredDiags) + if refresh == nil { + return nil, diags + } + + // For the actual destroy plan, we'll skip the refresh and + // simply use the refreshed state from the refresh plan. + opts, moreDiags := c.PlanOpts(ctx, c.mode, true) + diags = diags.Append(moreDiags) + if opts == nil { + return nil, diags + } + + // If we're destroying this instance, then the dependencies + // should be reversed. Unfortunately, we can't compute that + // easily so instead we'll use the dependents computed at the + // last apply operation. + Dependents: + for depAddr := range c.PlanPrevDependents().All() { + depStack := c.main.Stack(ctx, depAddr.Stack, PlanPhase) + if depStack == nil { + // something weird has happened, but this means that + // whatever thing we're depending on being deleted first + // doesn't exist so it's fine. + continue + } + depComponent, depRemoveds := depStack.ApplyableComponents(depAddr.Item) + if depComponent != nil && !depComponent.PlanIsComplete(ctx) { + opts.ExternalDependencyDeferred = true + break + } + for _, depRemoved := range depRemoveds { + if !depRemoved.PlanIsComplete(ctx, depStack.addr) { + opts.ExternalDependencyDeferred = true + break Dependents + } + } + } + + plan, moreDiags := PlanComponentInstance(ctx, c.main, refresh.PriorState, opts, c) + return plan, diags.Append(moreDiags) + } + + opts, moreDiags := c.PlanOpts(ctx, c.mode, false) + diags = diags.Append(moreDiags) + if opts == nil { + return nil, diags + } + + // If any of our upstream components have incomplete plans then + // we need to force treating everything in this component as + // deferred so we can preserve the correct dependency ordering. + for depAddr := range c.call.RequiredComponents(ctx).All() { + depStack := c.main.Stack(ctx, depAddr.Stack, PlanPhase) + if depStack == nil { + opts.ExternalDependencyDeferred = true // to be conservative + break + } + depComponent := depStack.Component(depAddr.Item) + if depComponent == nil { + opts.ExternalDependencyDeferred = true // to be conservative + break + } + if !depComponent.PlanIsComplete(ctx) { + opts.ExternalDependencyDeferred = true + break + } + } + + // The instance is also upstream deferred if the for_each value for + // this instance or any parent stacks is unknown. + if c.addr.Item.Key == addrs.WildcardKey { + opts.ExternalDependencyDeferred = true + } else { + for _, step := range c.call.addr.Stack { + if step.Key == addrs.WildcardKey { + opts.ExternalDependencyDeferred = true + break + } + } + } + + plan, moreDiags := PlanComponentInstance(ctx, c.main, c.PlanPrevState(), opts, c) + return plan, diags.Append(moreDiags) + }, + ) +} + +// ApplyModuleTreePlan applies a plan returned by a previous call to +// [ComponentInstance.CheckModuleTreePlan]. +// +// Applying a plan often has significant externally-visible side-effects, and +// so this method should be called only once for a given plan. In practice +// we currently ensure that is true by calling it only from the package-level +// [ApplyPlan] function, which arranges for this function to be called +// concurrently with the same method on other component instances and with +// a whole-tree walk to gather up results and diagnostics. +func (c *ComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans.Plan) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + if !c.main.Applying() { + panic("called ApplyModuleTreePlan with an evaluator not instantiated for applying") + } + + if plan.UIMode == plans.DestroyMode && plan.Changes.Empty() { + stackPlan := c.main.PlanBeingApplied().GetComponent(c.Addr()) + + // If we're destroying and there's nothing to destroy, then we can + // consider this a no-op. + return &ComponentInstanceApplyResult{ + FinalState: plan.PriorState, // after refresh + AffectedResourceInstanceObjects: resourceInstanceObjectsAffectedByStackPlan(stackPlan), + Complete: true, + }, diags + } + + // This is the result to return along with any errors that prevent us from + // even starting the modules runtime apply phase. It reports that nothing + // changed at all. + noOpResult := c.PlaceholderApplyResultForSkippedApply(plan) + + // We'll need to make some light modifications to the plan to include + // information we've learned in other parts of the apply walk that + // should've filled in some unknown value placeholders. It would be rude + // to modify the plan that our caller is holding though, so we'll + // shallow-copy it. This is NOT a deep copy, so don't modify anything + // that's reachable through any pointers without copying those first too. + modifiedPlan := *plan + inputValues := c.inputValuesForModulesRuntime(ctx, ApplyPhase) + if inputValues == nil { + // inputValuesForModulesRuntime uses nil (as opposed to a + // non-nil zerolen map) to represent that the definition of + // the input variables was so invalid that we cannot do + // anything with it, in which case we'll just return early + // and assume the plan walk driver will find the diagnostics + // via another return path. + return noOpResult, diags + } + // UGH: the "modules runtime"'s model of planning was designed around + // the goal of producing a traditional Terraform CLI-style saved plan + // file and so it has the input variable values already encoded as + // plans.DynamicValue opaque byte arrays, and so we need to convert + // our resolved input values into that format. It would be better + // if plans.Plan used the typical in-memory format for input values + // and let the plan file serializer worry about encoding, but we'll + // defer that API change for now to avoid disrupting other codepaths. + modifiedPlan.VariableValues = make(map[string]plans.DynamicValue, len(inputValues)) + modifiedPlan.VariableMarks = make(map[string][]cty.PathValueMarks, len(inputValues)) + for name, iv := range inputValues { + val, pvm := iv.Value.UnmarkDeepWithPaths() + dv, err := plans.NewDynamicValue(val, cty.DynamicPseudoType) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to encode input variable value", + fmt.Sprintf( + "Could not encode the value of input variable %q of %s: %s.\n\nThis is a bug in Terraform; please report it!", + name, c.Addr(), err, + ), + )) + continue + } + modifiedPlan.VariableValues[name] = dv + modifiedPlan.VariableMarks[name] = pvm + } + if diags.HasErrors() { + return noOpResult, diags + } + + result, moreDiags := ApplyComponentPlan(ctx, c.main, &modifiedPlan, c.call.config.config.ProviderConfigs, c) + return result, diags.Append(moreDiags) +} + +// PlanPrevState returns the previous state for this component instance during +// the planning phase, or panics if called in any other phase. +func (c *ComponentInstance) PlanPrevState() *states.State { + // The following call will panic if we aren't in the plan phase. + stackState := c.main.PlanPrevState() + ret := stackState.ComponentInstanceStateForModulesRuntime(c.Addr()) + if ret == nil { + ret = states.NewState() // so caller doesn't need to worry about nil + } + return ret +} + +// PlanPrevDependents returns the set of dependents based on the state. +func (c *ComponentInstance) PlanPrevDependents() collections.Set[stackaddrs.AbsComponent] { + return c.main.PlanPrevState().DependentsForComponent(c.Addr()) +} + +func (c *ComponentInstance) PlanPrevResult() map[addrs.OutputValue]cty.Value { + return c.main.PlanPrevState().ResultsForComponent(c.Addr()) +} + +// ApplyResult returns the result from applying a plan for this object using +// [ApplyModuleTreePlan]. +// +// Use the Complete field of the returned object to determine whether the +// apply ran to completion successfully enough for dependent work to proceed. +// If Complete is false then dependent work should not start, and instead +// dependents should unwind their stacks in a way that describes a no-op result. +func (c *ComponentInstance) ApplyResult(ctx context.Context) *ComponentInstanceApplyResult { + ret, _ := c.CheckApplyResult(ctx) + return ret +} + +// CheckApplyResult returns the results from applying a plan for this object +// using [ApplyModuleTreePlan], and diagnostics describing any problems +// encountered when applying it. +func (c *ComponentInstance) CheckApplyResult(ctx context.Context) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + changes := c.main.ApplyChangeResults() + applyResult, moreDiags, err := changes.ComponentInstanceResult(ctx, c.Addr()) + diags = diags.Append(moreDiags) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Component instance apply not scheduled", + fmt.Sprintf("Terraform needs the result from applying changes to %s, but that apply was apparently not scheduled to run: %s. This is a bug in Terraform.", c.Addr(), err), + )) + } + return applyResult, diags +} + +// PlaceholderApplyResultForSkippedApply returns a [ComponentInstanceApplyResult] +// which describes the hypothetical result of skipping the apply phase for +// this component instance altogether. +// +// It doesn't have any logic to check whether the apply _was_ actually skipped; +// the caller that's orchestrating the changes during the apply phase must +// decided that for itself and then choose between either calling +// [ComponentInstance.ApplyModuleTreePlan] to apply as normal, or returning +// the result of this function instead to explain that the apply was skipped. +func (c *ComponentInstance) PlaceholderApplyResultForSkippedApply(plan *plans.Plan) *ComponentInstanceApplyResult { + // (We have this in here as a method just because it helps keep all of + // the logic for constructing [ComponentInstanceApplyResult] objects + // together in the same file, rather than having the caller synthesize + // a result itself only in this one special situation.) + return &ComponentInstanceApplyResult{ + FinalState: plan.PrevRunState, + Complete: false, + } +} + +// ApplyResultState returns the new state resulting from applying a plan for +// this object using [ApplyModuleTreePlan], or nil if the apply failed and +// so there is no new state to return. +func (c *ComponentInstance) ApplyResultState(ctx context.Context) *states.State { + ret, _ := c.CheckApplyResultState(ctx) + return ret +} + +// CheckApplyResultState returns the new state resulting from applying a plan for +// this object using [ApplyModuleTreePlan] and diagnostics describing any +// problems encountered when applying it. +func (c *ComponentInstance) CheckApplyResultState(ctx context.Context) (*states.State, tfdiags.Diagnostics) { + result, diags := c.CheckApplyResult(ctx) + var newState *states.State + if result != nil { + newState = result.FinalState + } + return newState, diags +} + +// InspectingState returns the state as captured in the snapshot provided when +// instantiating [Main] for [InspectPhase] evaluation. +func (c *ComponentInstance) InspectingState() *states.State { + wholeState := c.main.InspectingState() + return wholeState.ComponentInstanceStateForModulesRuntime(c.Addr()) +} + +func (c *ComponentInstance) ResultValue(ctx context.Context, phase EvalPhase) cty.Value { + switch phase { + case PlanPhase: + + if c.mode == plans.DestroyMode { + // If we are running a destroy plan, then we'll return the result + // of our refresh operation. + return cty.ObjectVal(c.refresh.Result(ctx)) + } + + plan := c.ModuleTreePlan(ctx) + if plan == nil { + // Planning seems to have failed so we cannot decide a result value yet. + // We can't do any better than DynamicVal here because in the + // modules language output values don't have statically-declared + // result types. + return cty.DynamicVal + } + return cty.ObjectVal(stackplan.OutputsFromPlan(c.ModuleTree(ctx), plan)) + + case ApplyPhase, InspectPhase: + // As a special case, if we're applying and the planned action is + // to destroy then we'll just return the planned output values + // verbatim without waiting for anything, so that downstreams can + // begin their own destroy phases before we start ours. + if phase == ApplyPhase { + fullPlan := c.main.PlanBeingApplied() + ourPlan := fullPlan.GetComponent(c.Addr()) + if ourPlan == nil { + // Weird, but we'll tolerate it. + return cty.DynamicVal + } + + if ourPlan.PlannedAction == plans.Delete || ourPlan.PlannedAction == plans.Forget { + // In this case our result was already decided during the + // planning phase, because we can't block on anything else + // here to make sure we don't create a self-dependency + // while our downstreams are trying to destroy themselves. + attrs := make(map[string]cty.Value, len(ourPlan.PlannedOutputValues)) + for addr, val := range ourPlan.PlannedOutputValues { + attrs[addr.Name] = val + } + return cty.ObjectVal(attrs) + } + } + + var state *states.State + switch phase { + case ApplyPhase: + state = c.ApplyResultState(ctx) + case InspectPhase: + state = c.InspectingState() + default: + panic(fmt.Sprintf("unsupported evaluation phase %s", state)) // should not get here + } + if state == nil { + // Applying seems to have failed so we cannot provide a result + // value, and so we'll return a placeholder to help our caller + // unwind gracefully with its own placeholder result. + // We can't do any better than DynamicVal here because in the + // modules language output values don't have statically-declared + // result types. + // (This should not typically happen in InspectPhase if the caller + // provided a valid state snapshot, but we'll still tolerate it in + // that case because InspectPhase is sometimes used in our unit + // tests which might provide contrived input if testing component + // instances is not their primary focus.) + return cty.DynamicVal + } + + // For apply and inspect phases we use the root module output values + // from the state to construct our value. + outputVals := state.RootOutputValues + attrs := make(map[string]cty.Value, len(outputVals)) + for _, ov := range outputVals { + name := ov.Addr.OutputValue.Name + + if ov.Sensitive { + // For our purposes here, a static sensitive flag on the + // output value is indistinguishable from the value having + // been dynamically marked as sensitive. + attrs[name] = ov.Value.Mark(marks.Sensitive) + continue + } + + // Otherwise, just set the value as is. + attrs[name] = ov.Value + } + + // If the apply operation was unsuccessful for any reason then we + // might have some output values that are missing from the state, + // because the state is only updated with the results of successful + // operations. To avoid downstream errors we'll insert unknown values + // for any declared output values that don't yet have a final value. + // + // The status of the apply operation will have been recorded elsewhere + // so we don't need to worry about that here. This also ensures that + // nothing will actually attempt to apply the unknown values here. + config := c.call.config.ModuleTree(ctx) + for _, output := range config.Module.Outputs { + if _, ok := attrs[output.Name]; !ok { + attrs[output.Name] = cty.DynamicVal + } + } + + return cty.ObjectVal(attrs) + + default: + // We can't produce a concrete value for any other phase. + return cty.DynamicVal + } +} + +// ResolveExpressionReference implements ExpressionScope. +func (c *ComponentInstance) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + stack := c.call.stack + return stack.resolveExpressionReference(ctx, ref, nil, c.repetition) +} + +// ExternalFunctions implements ExpressionScope. +func (c *ComponentInstance) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return c.main.ProviderFunctions(ctx, c.call.config.stack) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (c *ComponentInstance) PlanTimestamp() time.Time { + return c.main.PlanTimestamp() +} + +// Addr implements ConfigComponentExpressionScope +func (c *ComponentInstance) Addr() stackaddrs.AbsComponentInstance { + return c.addr +} + +// StackConfig implements ConfigComponentExpressionScope +func (c *ComponentInstance) StackConfig() *StackConfig { + return c.call.stack.config +} + +// ModuleTree implements ConfigComponentExpressionScope. +func (c *ComponentInstance) ModuleTree(ctx context.Context) *configs.Config { + return c.call.config.ModuleTree(ctx) +} + +// DeclRange implements ConfigComponentExpressionScope. +func (c *ComponentInstance) DeclRange() *hcl.Range { + return c.call.config.config.DeclRange.ToHCL().Ptr() +} + +// PlanChanges implements Plannable by validating that all of the per-instance +// arguments are suitable, and then asking the main Terraform language runtime +// to produce a plan in terms of the component's selected module. +func (c *ComponentInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var changes []stackplan.PlannedChange + var diags tfdiags.Diagnostics + + _, moreDiags := c.CheckInputVariableValues(ctx, PlanPhase) + diags = diags.Append(moreDiags) + + _, _, moreDiags = EvalProviderValues(ctx, c.main, c.call.config.config.ProviderConfigs, PlanPhase, c) + diags = diags.Append(moreDiags) + + corePlan, moreDiags := c.CheckModuleTreePlan(ctx) + diags = diags.Append(moreDiags) + if corePlan != nil { + existedBefore := false + if prevState := c.main.PlanPrevState(); prevState != nil { + existedBefore = prevState.HasComponentInstance(c.Addr()) + } + destroying := corePlan.UIMode == plans.DestroyMode + refreshOnly := corePlan.UIMode == plans.RefreshOnlyMode + + var action plans.Action + switch { + case destroying: + action = plans.Delete + case refreshOnly: + action = plans.Read + case existedBefore: + action = plans.Update + default: + action = plans.Create + } + + var refreshPlan *plans.Plan + if c.mode == plans.DestroyMode { + // if we're in destroy mode, then we did a separate refresh plan + // so we'll make sure to pass that in as extra information the + // FromPlan function can use. + refreshPlan, _ = c.refresh.Plan(ctx) + } + + changes, moreDiags = stackplan.FromPlan(ctx, c.ModuleTree(ctx), corePlan, refreshPlan, action, c) + diags = diags.Append(moreDiags) + } + + return changes, diags +} + +// CheckApply implements Applyable. +func (c *ComponentInstance) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // FIXME: We need to report an AppliedChange object for the component + // instance itself, and we need to emit "interim" objects representing + // the "prior state" (refreshed) in each resource instance change in + // the plan, so that the effect of refreshing will still get committed + // to the state even if other downstream changes don't succeed. + + inputs, moreDiags := c.CheckInputVariableValues(ctx, ApplyPhase) + diags = diags.Append(moreDiags) + + if inputs == cty.NilVal { + // there was some error retrieving the input values, this should have + // raised a diagnostic elsewhere, so we'll just use an empty object to + // avoid panicking later. + inputs = cty.EmptyObjectVal + } + + _, _, moreDiags = EvalProviderValues(ctx, c.main, c.call.config.config.ProviderConfigs, ApplyPhase, c) + diags = diags.Append(moreDiags) + + applyResult, moreDiags := c.CheckApplyResult(ctx) + diags = diags.Append(moreDiags) + + var changes []stackstate.AppliedChange + if applyResult != nil { + changes, moreDiags = stackstate.FromState(ctx, applyResult.FinalState, c.main.PlanBeingApplied().GetComponent(c.Addr()), inputs, applyResult.AffectedResourceInstanceObjects, c) + diags = diags.Append(moreDiags) + } + return changes, diags +} + +// ResourceSchema implements stackplan.PlanProducer. +func (c *ComponentInstance) ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, typ string) (providers.Schema, error) { + // This should not be able to fail with an error because we should + // be retrieving the same schema that was already used to encode + // the object we're working with. The error handling here is for + // robustness but any error here suggests a bug in Terraform. + + providerType := c.main.ProviderType(providerTypeAddr) + providerSchema, err := providerType.Schema(ctx) + if err != nil { + return providers.Schema{}, err + } + ret := providerSchema.SchemaForResourceType(mode, typ) + if ret.Body == nil { + return providers.Schema{}, fmt.Errorf("schema does not include %v %q", mode, typ) + } + return ret, nil +} + +// RequiredComponents implements stackplan.PlanProducer. +func (c *ComponentInstance) RequiredComponents(ctx context.Context) collections.Set[stackaddrs.AbsComponent] { + return c.call.RequiredComponents(ctx) +} + +func (c *ComponentInstance) tracingName() string { + return c.Addr().String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_instance_results.go b/internal/stacks/stackruntime/internal/stackeval/component_instance_results.go new file mode 100644 index 0000000000..be391a8f3c --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/component_instance_results.go @@ -0,0 +1,64 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/states" +) + +type ComponentInstanceApplyResult struct { + // FinalState is the final state snapshot returned by the modules runtime + // after the apply phase completed. + FinalState *states.State + + // AffectedResourceInstanceObjects is a set of the addresses of all + // resource instance objects that might've been affected by any part + // of this apply process. + // + // This includes both objects that had real planned changes and also + // objects that might have had their state updated by the refresh actions + // during plan, even though no external actions were taken during the + // apply phase. + AffectedResourceInstanceObjects addrs.Set[addrs.AbsResourceInstanceObject] + + // Complete is set to true if the apply ran to completion successfully + // such that it should be safe to try to apply other component instances + // that are awaiting the completion of this one. + // + // If set to false, some changes may have been made but there may be + // some changes still pending and so other waiting component instances + // should not try to apply themselves at all. + Complete bool +} + +// resourceInstanceObjectsAffectedByStackPlan finds an exhaustive set of +// addresses for all resource instance objects that could potentially have their +// state changed while applying the given plan. +// +// Along with the objects targeted by explicit planned changes, this also +// includes objects whose state might just get updated to capture changes +// made outside of Terraform that were detected during the planning phase. +func resourceInstanceObjectsAffectedByStackPlan(plan *stackplan.Component) addrs.Set[addrs.AbsResourceInstanceObject] { + // For now we conservatively just enumerate everything that exists + // either before or after the change. This is technically more than + // we strictly need to return -- it will include objects that have + // no planned change and whose refresh step changed nothing -- but + // it's better to over-report than to under-report because under-reporting + // will cause stale objects to get left in the state. + + ret := addrs.MakeSet[addrs.AbsResourceInstanceObject]() + if plan.ResourceInstancePlanned.Len() > 0 { + for _, ch := range plan.ResourceInstancePlanned.Elems { + ret.Add(ch.Key) + } + } + if plan.ResourceInstancePriorState.Len() > 0 { + for _, addr := range plan.ResourceInstancePriorState.Elems { + ret.Add(addr.Key) + } + } + return ret +} diff --git a/internal/stacks/stackruntime/internal/stackeval/component_test.go b/internal/stacks/stackruntime/internal/stackeval/component_test.go new file mode 100644 index 0000000000..0c0491197a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/component_test.go @@ -0,0 +1,397 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// TestComponentInstances is a test of the [Component.CheckInstances] function. +// +// In particular, note that it's _not_ a test of the [ComponentInstance] type +// as a whole, although [Component.CheckInstances] does return a collection of +// those so there is _some_ coverage of that in here. +func TestComponentCheckInstances(t *testing.T) { + getComponent := func(ctx context.Context, main *Main) *Component { + mainStack := main.MainStack() + component := mainStack.Component(stackaddrs.Component{Name: "foo"}) + if component == nil { + t.Fatal("component.foo does not exist, but it should exist") + } + return component + } + + subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) { + cfg := testStackConfig(t, "component", "single_instance") + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_inputs": cty.EmptyObjectVal, + }, + }) + + component := getComponent(ctx, main) + forEachVal, diags := component.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if forEachVal != cty.NilVal { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: cty.NilVal", forEachVal) + } + + insts, unknown, diags := component.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 1; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + inst, ok := insts[addrs.NoKey] + if !ok { + t.Fatalf("missing expected addrs.NoKey instance\n%s", spew.Sdump(insts)) + } + if diff := cmp.Diff(instances.RepetitionData{}, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + t.Run("for_each", func(t *testing.T) { + cfg := testStackConfig(t, "component", "for_each") + + subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.MapValEmpty(cty.EmptyObject), + }, + }) + + component := getComponent(ctx, main) + forEachVal, diags := component.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if got, want := forEachVal, cty.MapValEmpty(cty.EmptyObject); !want.RawEquals(got) { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", got, want) + } + insts, unknown, diags := component.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 0; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + + // For this particular function we take the unusual approach of + // distinguishing between a nil map and a non-nil empty map so + // we can distinguish between "definitely no instances" (this case) + // and "we don't know how many instances there are" (tested in other + // subtests of this test, below.) + if insts == nil { + t.Error("CheckInstances result is nil; should be non-nil empty map") + } + }) + subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) { + wantForEachVal := cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in b"), + }), + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": wantForEachVal, + }, + }) + + component := getComponent(ctx, main) + gotForEachVal, diags := component.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if !wantForEachVal.RawEquals(gotForEachVal) { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", gotForEachVal, wantForEachVal) + } + insts, unknown, diags := component.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 2; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + t.Run("instance a", func(t *testing.T) { + inst, ok := insts[addrs.StringKey("a")] + if !ok { + t.Fatalf("missing expected addrs.StringKey(\"a\") instance\n%s", spew.Sdump(insts)) + } + wantRepData := instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in a"), + }), + } + if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + t.Run("instance b", func(t *testing.T) { + inst, ok := insts[addrs.StringKey("b")] + if !ok { + t.Fatalf("missing expected addrs.StringKey(\"b\") instance\n%s", spew.Sdump(insts)) + } + wantRepData := instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in b"), + }), + } + if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + }) + subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.NullVal(cty.Map(cty.EmptyObject)), + }, + }) + + component := getComponent(ctx, main) + gotVal, diags := component.CheckForEachValue(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return diag.Severity() == tfdiags.Error && strings.Contains(diag.Description().Detail, "The for_each expression produced a null value") + }) + wantVal := cty.DynamicVal // placeholder for invalid result + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + }) + subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.StringVal("nope"), + }, + }) + + component := getComponent(ctx, main) + gotVal, diags := component.CheckForEachValue(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return (diag.Severity() == tfdiags.Error && + diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component.") + }) + wantVal := cty.DynamicVal // placeholder for invalid result + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + + // When the for_each expression is invalid, CheckInstances should + // return nil and diagnostics. + gotInsts, unknown, diags := component.CheckInstances(ctx, InspectPhase) + assertFalse(t, unknown) + if gotInsts != nil { + t.Fatalf("unexpected instances\ngot: %#v\nwant: nil", gotInsts) + } + + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return (diag.Severity() == tfdiags.Error && + diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component.") + }) + }) + subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.UnknownVal(cty.Map(cty.EmptyObject)), + }, + }) + + component := getComponent(ctx, main) + gotVal, diags := component.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + + wantVal := cty.UnknownVal(cty.Map(cty.EmptyObject)) + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + + // When the for_each expression is unknown, CheckInstances should + // return a single instance with dynamic values in the repetition data. + gotInsts, unknown, diags := component.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertTrue(t, unknown) + if got, want := len(gotInsts), 0; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, gotInsts) + } + }) + }) + +} + +func TestComponentResultValue(t *testing.T) { + getComponent := func(ctx context.Context, t *testing.T, main *Main) *Component { + mainStack := main.MainStack() + component := mainStack.Component(stackaddrs.Component{Name: "foo"}) + if component == nil { + t.Fatal("component.foo does not exist, but it should exist") + } + return component + } + + subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) { + cfg := testStackConfig(t, "component", "single_instance") + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_inputs": cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("hello"), + }), + }, + }) + + component := getComponent(ctx, t, main) + got := component.ResultValue(ctx, InspectPhase) + want := cty.ObjectVal(map[string]cty.Value{ + // FIXME: This currently returns an unknown value because we + // aren't tracking component output values in prior state. + // Once we fix that, we should see an output value called "test" + // here. + "test": cty.DynamicVal, + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + t.Run("for_each", func(t *testing.T) { + cfg := testStackConfig(t, "component", "for_each") + + subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.MapValEmpty(cty.EmptyObject), + }, + }) + + component := getComponent(ctx, t, main) + got := component.ResultValue(ctx, InspectPhase) + want := cty.EmptyObjectVal + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) { + forEachVal := cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("in a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("in b"), + }), + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": forEachVal, + }, + }) + + component := getComponent(ctx, t, main) + got := component.ResultValue(ctx, InspectPhase) + want := cty.ObjectVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + // FIXME: This currently returns an unknown value because we + // aren't tracking component output values in prior state. + // Once we fix that, we should see an output value called "test" + // here. + "test": cty.DynamicVal, + }), + "b": cty.ObjectVal(map[string]cty.Value{ + // FIXME: This currently returns an unknown value because we + // aren't tracking component output values in prior state. + // Once we fix that, we should see an output value called "test" + // here. + "test": cty.DynamicVal, + }), + }) + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.NullVal(cty.Map(cty.EmptyObject)), + }, + }) + + component := getComponent(ctx, t, main) + got := component.ResultValue(ctx, InspectPhase) + // When the for_each expression is null, the result value should + // be a cty.NilVal. + want := cty.NilVal + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.StringVal("nope"), + }, + }) + + component := getComponent(ctx, t, main) + got := component.ResultValue(ctx, InspectPhase) + // When the for_each expression is null, the result value should + // be a cty.NilVal. + want := cty.NilVal + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "component_instances": cty.UnknownVal(cty.Map(cty.EmptyObject)), + }, + }) + + component := getComponent(ctx, t, main) + got := component.ResultValue(ctx, InspectPhase) + // When the for_each expression is unknown, the result value + // is a dynamic instance. + want := cty.DynamicVal + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/diagnostics.go b/internal/stacks/stackruntime/internal/stackeval/diagnostics.go new file mode 100644 index 0000000000..ff7a2d7235 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/diagnostics.go @@ -0,0 +1,458 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type withDiagnostics[T any] struct { + Result T + Diagnostics tfdiags.Diagnostics +} + +// doOnceWithDiags is a helper for the common pattern of evaluating a +// promising.Once that returns both a result and some diagnostics. +// +// It encapsulates all of the visual noise of sneaking the (result, diags) +// tuple through a struct type to compensate for the fact that Go generics +// cannot make a function generic over the number of results it returns. +// +// It also transforms any promise/task-related errors into user-oriented +// diagnostics, for which it needs to be provided a namedPromiseReporter "root" +// that covers the whole scope of possible promises that could be involved +// in the valuation. Typically root should be set to the relevant [Main] +// object to cover all of the promises across the whole evaluation. +func doOnceWithDiags[T any]( + ctx context.Context, + name string, + once *promising.Once[withDiagnostics[T]], + f func(ctx context.Context) (T, tfdiags.Diagnostics), +) (T, tfdiags.Diagnostics) { + if once == nil { + panic("doOnceWithDiags with nil Once") + } + ret, err := once.Do(ctx, name, func(ctx context.Context) (withDiagnostics[T], error) { + ret, diags := f(ctx) + return withDiagnostics[T]{ + Result: ret, + Diagnostics: diags, + }, nil + }) + if err != nil { + ret.Diagnostics = ret.Diagnostics.Append(diagnosticsForPromisingTaskError(err)) + } + return ret.Result, ret.Diagnostics +} + +// withCtyDynamicValPlaceholder is a workaround for an annoying wrinkle with +// [doOnceWithDiags] where a wrapped function can't return its own placeholder +// value if the call fails due to a promise-related error like a +// self-dependency. +// +// In that case the result is generated by the promises system rather than +// by the function being called and so it ends up returning [cty.NilVal], the +// zero value of [cty.Value]. This function intercepts that result and +// replaces the zero value with [cty.DynamicVal], which is typically the more +// reasonable placeholder since it allows dependent expressions to resolve +// without any knock-on errors. +// +// To use this, pass the result of [doOnceWithDiags] directly into it: +// +// return withCtyDynamicValPlaceholder(doOnceWithDiags(/* ... */)) +func withCtyDynamicValPlaceholder(result cty.Value, diags tfdiags.Diagnostics) (cty.Value, tfdiags.Diagnostics) { + if result == cty.NilVal { + result = cty.DynamicVal + } + return result, diags +} + +// syncDiagnostics is a synchronization helper for functions that run two or +// more asynchronous tasks that can potentially generate diagnostics. +// +// It allows concurrent tasks to all safely append new diagnostics into a +// mutable container without data races. +type syncDiagnostics struct { + diags tfdiags.Diagnostics + mu sync.Mutex +} + +// Append converts all of the given arguments to zero or more diagnostics +// and appends them to the internal diagnostics list, modifying this object +// in-place. +func (sd *syncDiagnostics) Append(new ...any) { + sd.mu.Lock() + sd.diags = sd.diags.Append(new...) + sd.mu.Unlock() +} + +// Take retrieves all of the diagnostics accumulated so far and resets +// the internal list to empty so that future calls can append more without +// any confusion about which diagnostics were already taken. +func (sd *syncDiagnostics) Take() tfdiags.Diagnostics { + sd.mu.Lock() + ret := sd.diags + sd.diags = nil + sd.mu.Unlock() + return ret +} + +// finalDiagnosticsFromEval prepares a set of diagnostics generated by some +// calls to evaluation functions to be returned to a caller outside of this +// package. This should typically be used as a final step in functions that +// act as entry points into this package from callers in package stackruntime. +// +// Currently the only special work this does is removing any duplicate +// diagnostics relating to self-dependency problems. These tend to appear +// multiple times since all of the promises in the chain all fail at the +// same time and thus effectively the same diagnostic gets appended multiple +// times by different paths. Only the first such diagnostic will be preserved +// by this function. +func finalDiagnosticsFromEval(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + if len(diags) == 0 { + return diags // handle the happy path as quickly as possible + } + if !diags.HasErrors() { + return diags // also a relatively happy path: just warnings + } + + // If we have at least two errors then we could potentially have a + // duplicate self-dependency error. Self-dependency errors should be + // relatively rare so we'll first count how many we have and only + // go to the trouble of shuffling the diagnostics once we've proven + // we really need to. + foundSelfDepErrs := 0 + for _, diag := range diags { + if diagIsPromiseSelfReference(diag) { + foundSelfDepErrs++ + } + } + if foundSelfDepErrs <= 1 { + return diags // no massaging needed + } + + // If we get here then we _do_ have at least two self-dependency errors, + // and so we'll perform a more expensive scan-and-shift process over the + // diagnostics, skipping over all but the first of these errors. + fixedSelfDepErrors := 0 + for i := 0; i < len(diags); i++ { + diag := diags[i] + if !diagIsPromiseSelfReference(diag) { + continue + } + fixedSelfDepErrors++ + if fixedSelfDepErrors == 1 { + continue // don't actually need to "fix" the first one + } + // If we get here then we have found a duplicate error, and so we'll + // shift all of the subsequent errors to earlier indices in the slice. + copy(diags[i:], diags[i+1:]) + diags = diags[:len(diags)-1] + i-- // must still visit the next item that we've moved to an earlier index + } + return diags +} + +func diagIsPromiseSelfReference(diag tfdiags.Diagnostic) bool { + // This intentionally diverges from our usual convention of + // using interface types for "extra info" matching because this + // is a very specialized case confined only to this package, and + // so we can be sure that nothing else will need to generate + // differently-typed variants of this information. + // (Refer to type taskSelfDependencyDiagnostic below for more on this.) + ptr := tfdiags.ExtraInfo[*promising.ErrSelfDependent](diag) + return ptr != nil +} + +// diagnosticsForPromisingTaskError takes an error returned by +// promising.MainTask or promising.Once.Do, if any, and transforms it into one +// or more diagnostics describing the problem in a manner suitable for +// presentation directly to end-users. +// +// If the given error is nil then this always returns an empty diagnostics. +// +// This is intended only for tasks where the error result is exclusively +// used for promise- and task-related errors, with other errors already being +// presented as diagnostics. The result of this function will be relatively +// unhelpful for other errors and so better to handle those some other way. +func diagnosticsForPromisingTaskError(err error) tfdiags.Diagnostics { + if err == nil { + return nil + } + + var diags tfdiags.Diagnostics + switch err := err.(type) { + case promising.ErrSelfDependent: + diags = diags.Append(taskSelfDependencyDiagnostics(err)) + case promising.ErrUnresolved: + diags = diags.Append(taskPromisesUnresolvedDiagnostics(err)) + default: + // For all other errors we'll just let tfdiags.Diagnostics do its + // usual best effort to coerse into diagnostics. + diags = diags.Append(err) + } + return diags +} + +// taskSelfDependencyDiagnostics transforms a [promising.ErrSelfDependent] +// error into one or more error diagnostics suitable for returning to an +// end user, after first trying to discover user-friendly names for each +// of the promises involved using the given namedPromiseReporter. +func taskSelfDependencyDiagnostics(err promising.ErrSelfDependent) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + // For now we just save the context about the problem, and then we'll + // generate the human-readable description on demand once someone asks + // for the diagnostic description. + diags = diags.Append(taskSelfDependencyDiagnostic{ + err: err, + }) + return diags +} + +// taskPromisesUnresolvedDiagnostics transforms a [promising.ErrUnresolved] +// error into one or more error diagnostics suitable for returning to an +// end user, after first trying to discover user-friendly names for each +// of the promises involved using the given namedPromiseReporter. +func taskPromisesUnresolvedDiagnostics(err promising.ErrUnresolved) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + // For now we just save the context about the problem, and then we'll + // generate the human-readable description on demand once someone asks + // for the diagnostic description. + diags = diags.Append(taskPromisesUnresolvedDiagnostic{ + err: err, + }) + return diags +} + +// taskSelfDependencyDiagnostic is an implementation of tfdiags.Diagnostic +// which represents self-dependency errors in a user-oriented way. +// +// This is a special diagnostic type because self-dependency errors tend to +// emerge via multiple return paths (since they blow up all of the promises +// in the cycle all at once) and so our main entry points rely on the +// behaviors of this special type to dedupe the diagnostics before returning. +type taskSelfDependencyDiagnostic struct { + err promising.ErrSelfDependent +} + +var _ tfdiags.Diagnostic = taskSelfDependencyDiagnostic{} + +// Description implements tfdiags.Diagnostic. +func (diag taskSelfDependencyDiagnostic) Description() tfdiags.Description { + // We build the user-oriented error message on demand, since it + // requires collecting some supporting information from the + // evaluation root so we know what result each of the promises was + // actually representing. + + err := diag.err + distinctPromises := make(map[promising.PromiseID]struct{}) + for _, id := range err { + distinctPromises[id] = struct{}{} + } + + switch len(distinctPromises) { + case 0: + // Should not get here; there can't be a promise cycle without any + // promises involved in it. + panic("promising.ErrSelfDependent without any promises") + case 1: + const diagSummary = "Self-dependent item in configuration" + var promiseID promising.PromiseID + for id := range distinctPromises { + promiseID = id + } + return tfdiags.Description{ + Summary: diagSummary, + Detail: fmt.Sprintf("The item %q depends on its own results, so there is no correct order of operations.", promiseID.FriendlyName()), + } + default: + // If we have more than one promise involved then it's non-deterministic + // which one we'll detect, since it depends on how the tasks get + // scheduled by the Go runtime. To return a deterministic-ish result + // anyway we'll arbitrarily descide to report whichever promise has + // the lexically-least name as defined by Go's own less than operator + // when applied to strings. + selectedIdx := 0 + selectedName := err[0].FriendlyName() + for i, id := range err { + candidateName := id.FriendlyName() + if candidateName < selectedName { + selectedIdx = i + selectedName = candidateName + } + } + // Now we'll rotate the list of promise IDs so that the one we selected + // appears first. + ids := make([]promising.PromiseID, 0, len(err)) + ids = append(ids, err[selectedIdx:]...) + ids = append(ids, err[:selectedIdx]...) + var nameList strings.Builder + for _, id := range ids { + name := id.FriendlyName() + if name == "" { + // We should minimize the number of unnamed promises so that + // we can typically say at least something useful about what + // objects are involved. + name = "(...)" + } + fmt.Fprintf(&nameList, "\n - %s", name) + } + return tfdiags.Description{ + Summary: "Self-dependent items in configuration", + Detail: fmt.Sprintf( + "The following items in your configuration form a circular dependency chain through their references:%s\n\nTerraform uses references to decide a suitable order for performing operations, so configuration items may not refer to their own results either directly or indirectly.", + nameList.String(), + ), + } + } +} + +// ExtraInfo implements tfdiags.Diagnostic. +func (diag taskSelfDependencyDiagnostic) ExtraInfo() interface{} { + // The "extra info" for a self-dependency error is the error itself, + // so callers can access the original promise IDs if they want to for + // some reason. + return &diag.err +} + +// FromExpr implements tfdiags.Diagnostic. +func (diag taskSelfDependencyDiagnostic) FromExpr() *tfdiags.FromExpr { + return nil +} + +// Severity implements tfdiags.Diagnostic. +func (diag taskSelfDependencyDiagnostic) Severity() tfdiags.Severity { + return tfdiags.Error +} + +// Source implements tfdiags.Diagnostic. +func (diag taskSelfDependencyDiagnostic) Source() tfdiags.Source { + // Self-dependency errors tend to involve multiple configuration locations + // all at once, and so we describe the affected objects in the detail + // text rather than as a single source location. + return tfdiags.Source{} +} + +// taskPromisesUnresolvedDiagnostic is an implementation of tfdiags.Diagnostic +// which represents a task's failure to resolve promises in a user-oriented way. +// +// This is a special dependency type just because that way we can defer +// formatting the description as long as possible, once our namedPromiseReporter +// has accumulated name information for as many promises as possible. +type taskPromisesUnresolvedDiagnostic struct { + err promising.ErrUnresolved +} + +var _ tfdiags.Diagnostic = taskPromisesUnresolvedDiagnostic{} + +// Description implements tfdiags.Diagnostic. +func (diag taskPromisesUnresolvedDiagnostic) Description() tfdiags.Description { + // We build the user-oriented error message on demand, since it + // requires collecting some supporting information from the + // evaluation root so we know what result each of the promises was + // actually representing. + + err := diag.err + distinctPromises := make(map[promising.PromiseID]struct{}) + for _, id := range err { + distinctPromises[id] = struct{}{} + } + + // If we have more than one promise involved then it's non-deterministic + // which one we'll detect, since it depends on how the tasks get + // scheduled by the Go runtime. To return a deterministic-ish result + // anyway we'll arbitrarily decide to report whichever promise has + // the lexically-least name as defined by Go's own less than operator + // when applied to strings. + selectedIdx := 0 + selectedName := err[0].FriendlyName() + for i, id := range err { + candidateName := id.FriendlyName() + if candidateName < selectedName { + selectedIdx = i + selectedName = candidateName + } + } + // Now we'll rotate the list of promise IDs so that the one we selected + // appears first. + ids := make([]promising.PromiseID, 0, len(err)) + ids = append(ids, err[selectedIdx:]...) + ids = append(ids, err[:selectedIdx]...) + var nameList strings.Builder + for _, id := range ids { + name := id.FriendlyName() + if name == "" { + // We should minimize the number of unnamed promises so that + // we can typically say at least something useful about what + // objects are involved. + name = "(unnamed promise)" + } + fmt.Fprintf(&nameList, "\n - %s", name) + } + return tfdiags.Description{ + Summary: "Stack language evaluation error", + Detail: fmt.Sprintf( + "While evaluating the stack configuration, the following items were left unresolved:%s\n\nOther errors returned along with this one may provide more details. This is a bug in Teraform; please report it!", + nameList.String(), + ), + } +} + +// ExtraInfo implements tfdiags.Diagnostic. +func (diag taskPromisesUnresolvedDiagnostic) ExtraInfo() interface{} { + // The "extra info" for a resolution error is the error itself, + // so callers can access the original promise IDs if they want to for + // some reason. + return &diag.err +} + +// FromExpr implements tfdiags.Diagnostic. +func (diag taskPromisesUnresolvedDiagnostic) FromExpr() *tfdiags.FromExpr { + return nil +} + +// Severity implements tfdiags.Diagnostic. +func (diag taskPromisesUnresolvedDiagnostic) Severity() tfdiags.Severity { + return tfdiags.Error +} + +// Source implements tfdiags.Diagnostic. +func (diag taskPromisesUnresolvedDiagnostic) Source() tfdiags.Source { + // A failure to resolve promises is a bug in the stacks runtime rather + // than a problem with the provided configuration, so there's no + // particularly-relevant source location to report. + return tfdiags.Source{} +} + +// diagnosticCausedBySensitive can be assigned to the "Extra" field of a +// diagnostic to hint to the UI layer that the sensitivity of values in scope +// is relevant to the diagnostic message. +type diagnosticCausedBySensitive bool + +var _ tfdiags.DiagnosticExtraBecauseSensitive = diagnosticCausedBySensitive(false) + +// DiagnosticCausedBySensitive implements tfdiags.DiagnosticExtraBecauseSensitive. +func (d diagnosticCausedBySensitive) DiagnosticCausedBySensitive() bool { + return bool(d) +} + +// diagnosticCausedByEphemeral can be assigned to the "Extra" field of a +// diagnostic to hint to the UI layer that the ephemerality of values in scope +// is relevant to the diagnostic message. +type diagnosticCausedByEphemeral bool + +var _ tfdiags.DiagnosticExtraBecauseEphemeral = diagnosticCausedByEphemeral(false) + +// DiagnosticCausedByEphemeral implements tfdiags.DiagnosticExtraBecauseEphemeral. +func (d diagnosticCausedByEphemeral) DiagnosticCausedByEphemeral() bool { + return bool(d) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go b/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go new file mode 100644 index 0000000000..a366948ef3 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/diagnostics_test.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "testing" + "time" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + providertest "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate" +) + +func TestNamedPromisesPlan(t *testing.T) { + // The goal of this test is to make sure we retain namedPromiseReporter + // coverage over various important object types, so that we don't + // accidentally regress the quality of self-reference ("dependency cycle") + // errors under future maintenence. + // + // It isn't totally comprehensive over all implementations of + // namedPromiseReporter, but we do aim to cover the main cases that a + // typical stack configuration might hit. + // + // This is intentionally a test of the namedPromiseReporter implementations + // directly, rather than of the dependency-message-building logic built + // in terms of it, because the goal is for namedPromiseReporter to return + // everything and then the diagnostic reporter to cherry-pick only the + // subset of names it needs, and because this way we can get more test + // coverage without needing fixtures for every possible combination of + // self-references. + + cfg := testStackConfig(t, "planning", "named_promises") + + providerAddrs := addrs.MustParseProviderSourceString("example.com/test/happycloud") + lock := depsfile.NewLocks() + lock.SetProvider( + providerAddrs, + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + InputVariableValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "in"}: ExternalInputValue{ + Value: cty.StringVal("hello"), + }, + }, + ProviderFactories: ProviderFactories{ + providerAddrs: providers.FactoryFixed( + &providertest.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{}, + }, + ResourceTypes: map[string]providers.Schema{ + "happycloud_thingy": providers.Schema{ + Body: &configschema.Block{}, + }, + }, + }, + }, + ), + }, + DependencyLocks: *lock, + PlanTimestamp: time.Now().UTC(), + }) + + // We don't actually really care about the plan here. We just want the + // side-effect of getting a bunch of promises created inside "main", which + // we'll then ask about below. + _, diags := testPlan(t, main) + assertNoDiagnostics(t, diags) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/doc.go b/internal/stacks/stackruntime/internal/stackeval/doc.go new file mode 100644 index 0000000000..c49c05de5e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/doc.go @@ -0,0 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package stackeval contains all of the internal logic of the stacks language +// runtime. +// +// This package may be imported only by the "stackruntime" package. It's a +// separate package only so we can draw a distinction between symbols that +// are exported to stackruntime vs. symbols that are private to this package, +// since there are lots of symbols in here. +// +// All functions in this package which take a [context.Context] value require +// that context to represent a task started by the package "promising", and +// may use the given task context to create promises and then wait for and/or +// resolve them. Calling with a non-task context will typically panic. +package stackeval diff --git a/internal/stacks/stackruntime/internal/stackeval/evalphase_string.go b/internal/stacks/stackruntime/internal/stackeval/evalphase_string.go new file mode 100644 index 0000000000..52d0861a6a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/evalphase_string.go @@ -0,0 +1,41 @@ +// Code generated by "stringer -type EvalPhase"; DO NOT EDIT. + +package stackeval + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[NoPhase-0] + _ = x[ValidatePhase-86] + _ = x[PlanPhase-80] + _ = x[ApplyPhase-65] + _ = x[InspectPhase-73] +} + +const ( + _EvalPhase_name_0 = "NoPhase" + _EvalPhase_name_1 = "ApplyPhase" + _EvalPhase_name_2 = "InspectPhase" + _EvalPhase_name_3 = "PlanPhase" + _EvalPhase_name_4 = "ValidatePhase" +) + +func (i EvalPhase) String() string { + switch { + case i == 0: + return _EvalPhase_name_0 + case i == 65: + return _EvalPhase_name_1 + case i == 73: + return _EvalPhase_name_2 + case i == 80: + return _EvalPhase_name_3 + case i == 86: + return _EvalPhase_name_4 + default: + return "EvalPhase(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/expression_refs.go b/internal/stacks/stackruntime/internal/stackeval/expression_refs.go new file mode 100644 index 0000000000..46f97b0834 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/expression_refs.go @@ -0,0 +1,297 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Referrer is implemented by types that have expressions that can refer to +// [Referenceable] objects. +type Referrer interface { + // References returns descriptions of all of the expression references + // made from the configuration of the receiver. + References(ctx context.Context) []stackaddrs.AbsReference +} + +// ReferencesInExpr returns all of the valid references contained in the given +// HCL expression. +// +// It ignores any invalid references, on the assumption that the expression +// will eventually be evaluated and then those invalid references would be +// reported as errors at that point. +func ReferencesInExpr(expr hcl.Expression) []stackaddrs.Reference { + if expr == nil { + return nil + } + return referencesInTraversals(expr.Variables()) +} + +// ReferencesInBody returns all of the valid references contained in the given +// HCL body. +// +// It ignores any invalid references, on the assumption that the body +// will eventually be evaluated and then those invalid references would be +// reported as errors at that point. +func ReferencesInBody(body hcl.Body, spec hcldec.Spec) []stackaddrs.Reference { + if body == nil { + return nil + } + return referencesInTraversals(hcldec.Variables(body, spec)) +} + +func referencesInTraversals(traversals []hcl.Traversal) []stackaddrs.Reference { + if len(traversals) == 0 { + return nil + } + ret := make([]stackaddrs.Reference, 0, len(traversals)) + for _, traversal := range traversals { + ref, _, moreDiags := stackaddrs.ParseReference(traversal) + if moreDiags.HasErrors() { + // We'll ignore any traversals that are not valid references, + // on the assumption that we'd catch them during a subsequent + // evaluation of the same expression/body/etc. + continue + } + ret = append(ret, ref) + } + return ret +} + +func makeReferencesAbsolute(localRefs []stackaddrs.Reference, stackAddr stackaddrs.StackInstance) []stackaddrs.AbsReference { + if len(localRefs) == 0 { + return nil + } + ret := make([]stackaddrs.AbsReference, 0, len(localRefs)) + for _, localRef := range localRefs { + // contextual refs require a more specific scope than an entire + // stack, so they can't be represented as [AbsReference]. + if _, isContextual := localRef.Target.(stackaddrs.ContextualRef); isContextual { + continue + } + ret = append(ret, localRef.Absolute(stackAddr)) + } + return ret +} + +// requiredComponentsForReferrer is the main underlying implementation +// of Applyable.RequiredComponents, allowing the types which directly implement +// that interface to worry only about their own unique way of gathering up +// the relevant references from their configuration, since the work of +// peeling away references until we've found all of the components is the +// same regardless of where the references came from. +// +// This is a best-effort which will produce a complete result only if the +// configuration is completely valid. If not, the result is likely to be +// incomplete, which we accept on the assumption that the invalidity would +// also make the resulting plan non-applyable and thus it doesn't actually +// matter what the required components are. +func (m *Main) requiredComponentsForReferrer(ctx context.Context, obj Referrer, phase EvalPhase) collections.Set[stackaddrs.AbsComponent] { + ret := collections.NewSet[stackaddrs.AbsComponent]() + initialRefs := obj.References(ctx) + + // queued tracks objects we've previously queued -- which may or may not + // still be in the queue -- so that we can avoid re-visiting the same + // object multiple times and thus ensure the following loop will definitely + // eventually terminate, even in the presence of reference cycles, because + // the number of unique reference addresses in the configuration is + // finite. + queued := collections.NewSet[stackaddrs.AbsReferenceable]() + queue := make([]stackaddrs.AbsReferenceable, len(initialRefs)) + for i, ref := range initialRefs { + queue[i] = ref.Target() + queued.Add(queue[i]) + } + + for len(queue) != 0 { + targetAddr, remain := queue[0], queue[1:] + queue = remain + + // If this is a direct reference to a component then we can just + // add it and continue. + if componentAddr, ok := targetAddr.Item.(stackaddrs.Component); ok { + ret.Add(stackaddrs.AbsComponent{ + Stack: targetAddr.Stack, + Item: componentAddr, + }) + continue + } + + // A stack call reference is also special, as we now want all the + // components of this stack call to be added to the queue as well. + // This doesn't happen automatically with the references as stack calls + // do not have a direct reference to their internal components (it + // actually goes the other way). + if stackCallAddr, ok := targetAddr.Item.(stackaddrs.StackCall); ok { + // We're just adding all the components within the stack to the + // queue. We could be a bit clever if, for example, the reference + // is to an output of the stack call. We could only add the + // components needed by that output. This is an okay compromise for + // now, in which the apply will wait for the whole stack to finish + // before moving on. + currentStack := m.Stack(ctx, targetAddr.Stack, phase) + if currentStack != nil { + next := currentStack.EmbeddedStackCall(stackCallAddr) + instances, _ := next.Instances(ctx, phase) + for _, instance := range instances { + nextStack := instance.Stack(ctx, phase) + for _, component := range nextStack.Components() { + ref := stackaddrs.AbsReferenceable{ + Stack: component.addr.Stack, + Item: stackaddrs.Component{ + Name: component.addr.Item.Name, + }, + } + if !queued.Has(ref) { + queue = append(queue, ref) + queued.Add(ref) + } + } + + // We'll also include any other stack calls within the embedded + // stack. + for _, call := range nextStack.EmbeddedStackCalls() { + ref := stackaddrs.AbsReferenceable{ + Stack: call.addr.Stack, + Item: call.addr.Item, + } + if !queued.Has(ref) { + queue = append(queue, ref) + queued.Add(ref) + } + } + } + } + + // We don't continue here, as we still want to add anything that + // the stack call references below. + } + + // For all other address types, we need to find the corresponding + // object and, if it's also Applyable, ask it for its references. + // + // For all of the fallible situations below, we'll just skip over + // this item on failure, because it's not this function's responsibility + // to report problems with the configuration. + // + // Since we're going to ignore all errors anyway, we can safely use + // a reference with no source location information. + ref := stackaddrs.AbsReference{ + Stack: targetAddr.Stack, + Ref: stackaddrs.Reference{ + Target: targetAddr.Item, + }, + } + target, _ := m.ResolveAbsExpressionReference(ctx, ref, phase) + if target == nil { + continue + } + targetReferrer, ok := target.(Referrer) + if !ok { + // Anything that isn't a referer cannot possibly indirectly + // refer to a component. + continue + } + for _, newRef := range targetReferrer.References(ctx) { + newTargetAddr := newRef.Target() + if !queued.Has(newTargetAddr) { + queue = append(queue, newTargetAddr) + queued.Add(newTargetAddr) + } + } + } + + return ret +} + +// ValidateDependsOn is a helper function that can be used to validate the +// DependsOn field of a component or an embedded stack. It returns diagnostics +// for any invalid references. +// +// The StackConfig argument should be the stack that the component or embedded +// stack is a part of. It is used to validate any references actually exist. +func ValidateDependsOn(source *StackConfig, traversals []hcl.Traversal) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + for _, traversal := range traversals { + // We don't actually care about the result here, only that it has no + // errors. + ref, rest, moreDiags := stackaddrs.ParseReference(traversal) + if moreDiags.HasErrors() { + diags = diags.Append(moreDiags) + continue + } + + switch addr := ref.Target.(type) { + case stackaddrs.StackCall: + // Make sure this stack call exists. + if source.StackCall(addr) == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: fmt.Sprintf("The depends_on reference %q does not exist.", addr), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + case stackaddrs.Component: + // Make sure this component exists. + if source.Component(addr) == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: fmt.Sprintf("The depends_on reference %q does not exist.", addr), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: fmt.Sprintf("The depends_on argument must refer to an embedded stack or component, but this reference refers to %q.", addr), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + continue // don't do the rest of the checks + } + + if len(rest) > 0 { + // for now, we can only reference components and stacks in + // configuration, and not instances of them or outputs from them. + // eg. component.self is valid, but component.self[0] is not. + // + // we'll add a warning, as we don't want users thinking the + // dependency is more precise than it is. But, we'll allow the + // reference as we can still use it just by ignoring the rest. + // + // FIXME: Allowing more fine grained references requires updating + // the requiredComponentsForReferrer function (above) to support + // AbsComponentInstance instead of AbsComponent. This is a + // potentially large refactor, and so only worth it for good + // reason and this isn't really that. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Non-valid depends_on target", + Detail: fmt.Sprintf(DependsOnDeepReferenceDetail, ref.Target), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + } + + return diags +} + +var ( + DependsOnDeepReferenceDetail = strings.TrimSpace(` +The depends_on argument should refer directly to an embedded stack or component in configuration, but this reference is too deep. + +Terraform Stacks has simplified the reference to the nearest valid target, %q. To remove this warning, update the configuration to the same target. +`) +) diff --git a/internal/stacks/stackruntime/internal/stackeval/expressions.go b/internal/stacks/stackruntime/internal/stackeval/expressions.go new file mode 100644 index 0000000000..db5df9dd28 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/expressions.go @@ -0,0 +1,508 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type EvalPhase rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type EvalPhase + +const ( + NoPhase EvalPhase = 0 + ValidatePhase EvalPhase = 'V' + PlanPhase EvalPhase = 'P' + ApplyPhase EvalPhase = 'A' + + // InspectPhase is a special phase that is used only to inspect the + // current dynamic situation, without any intention of changing it. + // This mode allows evaluation against some existing state (possibly + // empty) but cannot plan to make changes nor apply previously-created + // plans. + InspectPhase EvalPhase = 'I' +) + +// Referenceable is implemented by types that are identified by the +// implementations of [stackaddrs.Referenceable], returning the value that +// should be used to resolve a reference to that object in an expression +// elsewhere in the configuration. +type Referenceable interface { + // ExprReferenceValue returns the value that a reference to this object + // should resolve to during expression evaluation. + // + // This method cannot fail, because it's not the expression evaluator's + // responsibility to report errors or warnings that might arise while + // processing the target object. Instead, this method will respond to + // internal problems by returning a suitable placeholder value, and + // assume that diagnostics will be returned by another concurrent + // call path. + ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value +} + +// ExpressionScope is implemented by types that can have expressions evaluated +// within them, providing the rules for mapping between references in +// expressions to the underlying objects that will provide their values. +type ExpressionScope interface { + // ResolveExpressionReference decides what a particular expression reference + // means in the receiver's evaluation scope and returns the concrete object + // that the address is referring to. + ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) + + // PlanTimestamp returns the timestamp that should be used as part of the + // plantimestamp function in expressions. + PlanTimestamp() time.Time + + // ExternalFunctions should return the set of external functions that are + // available to the current scope. The returned function should be called + // when the returned functions are no longer needed. + ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) +} + +// EvalContextForExpr produces an HCL expression evaluation context for the +// given expression in the given evaluation phase within the given expression +// scope. +// +// [EvalExprAndEvalContext] is a convenient wrapper around this which also does +// the final step of evaluating the expression, returning both the value +// and the evaluation context that was used to build it. +func EvalContextForExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) { + return evalContextForTraversals(ctx, expr.Variables(), phase, scope) +} + +// EvalContextForBody produces an HCL expression context for decoding the +// given [hcl.Body] into a value using the given [hcldec.Spec]. +func EvalContextForBody(ctx context.Context, body hcl.Body, spec hcldec.Spec, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) { + if body == nil { + panic("EvalContextForBody with nil body") + } + if spec == nil { + panic("EvalContextForBody with nil spec") + } + return evalContextForTraversals(ctx, hcldec.Variables(body, spec), phase, scope) +} + +func evalContextForTraversals(ctx context.Context, traversals []hcl.Traversal, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) { + functions, diags := scope.ExternalFunctions(ctx) + + refs := make(map[stackaddrs.Referenceable]Referenceable) + for _, traversal := range traversals { + ref, _, moreDiags := stackaddrs.ParseReference(traversal) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + continue + } + obj, moreDiags := scope.ResolveExpressionReference(ctx, ref) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + continue + } + refs[ref.Target] = obj + } + if diags.HasErrors() { + return nil, diags + } + + varVals := make(map[string]cty.Value) + localVals := make(map[string]cty.Value) + componentVals := make(map[string]cty.Value) + stackVals := make(map[string]cty.Value) + providerVals := make(map[string]map[string]cty.Value) + eachVals := make(map[string]cty.Value) + countVals := make(map[string]cty.Value) + terraformVals := make(map[string]cty.Value) + var selfVal cty.Value + var testOnlyGlobals map[string]cty.Value // allocated only when needed (see below) + + for addr, obj := range refs { + val := obj.ExprReferenceValue(ctx, phase) + switch addr := addr.(type) { + case stackaddrs.InputVariable: + varVals[addr.Name] = val + case stackaddrs.LocalValue: + localVals[addr.Name] = val + case stackaddrs.Component: + componentVals[addr.Name] = val + case stackaddrs.StackCall: + stackVals[addr.Name] = val + case stackaddrs.ProviderConfigRef: + if _, exists := providerVals[addr.ProviderLocalName]; !exists { + providerVals[addr.ProviderLocalName] = make(map[string]cty.Value) + } + providerVals[addr.ProviderLocalName][addr.Name] = val + case stackaddrs.ContextualRef: + switch addr { + case stackaddrs.EachKey: + eachVals["key"] = val + case stackaddrs.EachValue: + eachVals["value"] = val + case stackaddrs.CountIndex: + countVals["index"] = val + case stackaddrs.Self: + selfVal = val + case stackaddrs.TerraformApplying: + terraformVals["applying"] = val + default: + // The above should be exhaustive for all values of this enumeration + panic(fmt.Sprintf("unsupported ContextualRef %#v", addr)) + } + case stackaddrs.TestOnlyGlobal: + // These are available only to some select unit tests in this + // package, and are not exposed as a real language feature to + // end-users. + if testOnlyGlobals == nil { + testOnlyGlobals = make(map[string]cty.Value) + } + testOnlyGlobals[addr.Name] = val + default: + // The above should cover all possible referenceable address types. + panic(fmt.Sprintf("don't know how to place %T in expression scope", addr)) + } + } + + providerValVals := make(map[string]cty.Value, len(providerVals)) + for k, v := range providerVals { + providerValVals[k] = cty.ObjectVal(v) + } + + // HACK: The top-level lang package bundles together the problem + // of resolving variables with the generation of the functions table. + // We only need the functions table here, so we're going to make a + // pseudo-scope just to load the functions from. + // FIXME: Separate these concerns better so that both languages can + // use the same functions but have entirely separate implementations + // of what data is in scope. + fakeScope := &lang.Scope{ + Data: nil, // not a real scope; can't actually make an evalcontext + BaseDir: ".", + PureOnly: phase != ApplyPhase, + ConsoleMode: false, + PlanTimestamp: scope.PlanTimestamp(), + ExternalFuncs: functions, + } + hclCtx := &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "var": cty.ObjectVal(varVals), + "local": cty.ObjectVal(localVals), + "component": cty.ObjectVal(componentVals), + "stack": cty.ObjectVal(stackVals), + "provider": cty.ObjectVal(providerValVals), + }, + Functions: fakeScope.Functions(), + } + if len(eachVals) != 0 { + hclCtx.Variables["each"] = cty.ObjectVal(eachVals) + } + if len(countVals) != 0 { + hclCtx.Variables["count"] = cty.ObjectVal(countVals) + } + if len(terraformVals) != 0 { + hclCtx.Variables["terraform"] = cty.ObjectVal(terraformVals) + } + if selfVal != cty.NilVal { + hclCtx.Variables["self"] = selfVal + } + if testOnlyGlobals != nil { + hclCtx.Variables["_test_only_global"] = cty.ObjectVal(testOnlyGlobals) + } + + return hclCtx, diags +} + +func EvalComponentInputVariables(ctx context.Context, decls map[string]*configs.Variable, wantTy cty.Type, defs *typeexpr.Defaults, decl *stackconfig.Component, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + v := cty.EmptyObjectVal + expr := decl.Inputs + rng := decl.DeclRange + var hclCtx *hcl.EvalContext + if expr != nil { + result, moreDiags := EvalExprAndEvalContext(ctx, expr, phase, scope) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.DynamicVal, diags + } + expr = result.Expression + hclCtx = result.EvalContext + v = result.Value + rng = tfdiags.SourceRangeFromHCL(result.Expression.Range()) + } + + if defs != nil { + v = defs.Apply(v) + } + v, err := convert.Convert(v, wantTy) + if err != nil { + // A conversion failure here could either be caused by an author-provided + // expression that's invalid or by the author omitting the argument + // altogether when there's at least one required attribute, so we'll + // return slightly different messages in each case. + if expr != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: fmt.Sprintf("Invalid input variable definition object: %s.", tfdiags.FormatError(err)), + Subject: rng.ToHCL().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required inputs for component", + Detail: fmt.Sprintf("Must provide \"inputs\" argument to define the component's input variables: %s.", tfdiags.FormatError(err)), + Subject: rng.ToHCL().Ptr(), + }) + } + return cty.DynamicVal, diags + } + + for _, path := range stackconfigtypes.ProviderInstancePathsInValue(v) { + err := path.NewErrorf("cannot send provider configuration reference to Terraform module input variable") + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: fmt.Sprintf( + "Invalid input variable definition object: %s.\n\nUse the separate \"providers\" argument to specify the provider configurations to use for this component's root module.", + tfdiags.FormatError(err), + ), + Subject: rng.ToHCL().Ptr(), + Expression: expr, + EvalContext: hclCtx, + }) + } + + if v.IsKnown() && !v.IsNull() { + var markDiags tfdiags.Diagnostics + for varName, varDecl := range decls { + varVal := v.GetAttr(varName) + + if !varDecl.Ephemeral { + // If the variable isn't declared as being ephemeral then we + // cannot allow ephemeral values to be assigned to it. + _, markses := varVal.UnmarkDeepWithPaths() + ephemeralPaths, _ := marks.PathsWithMark(markses, marks.Ephemeral) + for _, path := range ephemeralPaths { + if len(path) == 0 { + // The entire value is ephemeral, then. + markDiags = markDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: fmt.Sprintf("The input variable %q does not accept ephemeral values.", varName), + Subject: rng.ToHCL().Ptr(), + Expression: expr, + EvalContext: hclCtx, + Extra: diagnosticCausedByEphemeral(true), + }) + } else { + // Something nested inside is ephemeral, so we'll be + // more specific. + markDiags = markDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: fmt.Sprintf( + "The input variable %q does not accept ephemeral values, so the value for %s is not compatible.", + varName, tfdiags.FormatCtyPath(path), + ), + Subject: rng.ToHCL().Ptr(), + Expression: expr, + EvalContext: hclCtx, + Extra: diagnosticCausedByEphemeral(true), + }) + } + } + } + } + diags = diags.Append(markDiags) + if markDiags.HasErrors() { + // If we have an ephemeral value in a place where there shouldn't + // be one then we'll return an entirely-unknown value to make sure + // that downstreams that aren't checking the errors can't leak the + // value into somewhere it ought not to be. We'll still preserve + // the type constraint so that we can do type checking downstream. + return cty.UnknownVal(v.Type()), diags + } + } + + return v, diags +} + +// EvalExprAndEvalContext evaluates the given HCL expression in the given +// expression scope and returns the resulting value, along with the HCL +// evaluation context that was used to produce it. +// +// This compact helper function is intended for the relatively-common case +// where a caller needs to perform some additional validation on the result +// of the expression which might generate additional diagnostics, and so +// the caller will need the HCL evaluation context in order to construct +// a fully-annotated diagnostic object. +func EvalExprAndEvalContext(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (ExprResultValue, tfdiags.Diagnostics) { + hclCtx, diags := EvalContextForExpr(ctx, expr, phase, scope) + if hclCtx == nil { + return ExprResultValue{ + Value: cty.NilVal, + Expression: expr, + EvalContext: hclCtx, + }, diags + } + val, hclDiags := expr.Value(hclCtx) + diags = diags.Append(hclDiags) + if val == cty.NilVal { + val = cty.DynamicVal // just so the caller can assume the result is always a value + } + return ExprResultValue{ + Value: val, + Expression: expr, + EvalContext: hclCtx, + }, diags +} + +// EvalExpr evaluates the given HCL expression in the given expression scope +// and returns the resulting value. +// +// Sometimes callers also need the [hcl.EvalContext] that the expression was +// evaluated with in order to annotate later diagnostics. In that case, +// use [EvalExprAndEvalContext] instead to obtain both the resulting value +// and the evaluation context that was used to produce it. +func EvalExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) { + result, diags := EvalExprAndEvalContext(ctx, expr, phase, scope) + return result.Value, diags +} + +// EvalBody evaluates the expressions in the given body using hcldec with +// the given schema, returning the resulting value. +func EvalBody(ctx context.Context, body hcl.Body, spec hcldec.Spec, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) { + hclCtx, diags := EvalContextForBody(ctx, body, spec, phase, scope) + if hclCtx == nil { + return cty.NilVal, diags + } + val, hclDiags := hcldec.Decode(body, spec, hclCtx) + diags = diags.Append(hclDiags) + if val == cty.NilVal { + val = cty.DynamicVal // just so the caller can assume the result is always a value + } + return val, diags +} + +// ExprResult bundles an arbitrary result value with the expression and +// evaluation context it was derived from, allowing the recipient to +// potentially emit additional diagnostics if the result is problematic. +// +// (HCL diagnostics related to expressions should typically carry both +// the expression and evaluation context so that we can describe the +// values that were in scope as part of our user-facing diagnostic messages.) +type ExprResult[T any] struct { + Value T + + Expression hcl.Expression + EvalContext *hcl.EvalContext +} + +// ExprResultValue is an alias for the common case of an expression result +// being a [cty.Value]. +type ExprResultValue = ExprResult[cty.Value] + +// DerivedExprResult propagates the expression evaluation context through to +// a new result that was presumably derived from the original result but +// still, from a user perspective, associated with the original expression. +func DerivedExprResult[From, To any](from ExprResult[From], newResult To) ExprResult[To] { + return ExprResult[To]{ + Value: newResult, + Expression: from.Expression, + EvalContext: from.EvalContext, + } +} + +func (r ExprResult[T]) Diagnostic(severity tfdiags.Severity, summary string, detail string) *hcl.Diagnostic { + return &hcl.Diagnostic{ + Severity: severity.ToHCL(), + Summary: summary, + Detail: detail, + Subject: r.Expression.Range().Ptr(), + Expression: r.Expression, + EvalContext: r.EvalContext, + } +} + +// perEvalPhase is a helper for segregating multiple results for the same +// conceptual operation into a separate result per evaluation phase. +// This is typically needed for any result that's derived from expression +// evaluation, since the values produced for references are constructed +// differently depending on the phase. +// +// This utility works best for types that have a ready-to-use zero value. +type perEvalPhase[T any] struct { + mu sync.Mutex + vals map[EvalPhase]*T +} + +// For returns a pointer to the value belonging to the given evaluation phase, +// automatically allocating a new zero-value T if this is the first call for +// the given phase. +// +// This method is itself safe to call concurrently, but it does not constrain +// access to the returned value, and so interaction with that object may +// require additional care depending on the definition of T. +func (pep *perEvalPhase[T]) For(phase EvalPhase) *T { + if phase == NoPhase { + // Asking for the value for no phase at all is a nonsense. + panic("perEvalPhase.For(NoPhase)") + } + pep.mu.Lock() + if pep.vals == nil { + pep.vals = make(map[EvalPhase]*T) + } + if _, exists := pep.vals[phase]; !exists { + pep.vals[phase] = new(T) + } + ret := pep.vals[phase] + pep.mu.Unlock() + return ret +} + +// Each calls the given reporting callback for all of the values the +// receiver is currently tracking. +// +// Each blocks calls to the For method throughout its execution, so callback +// functions must not interact with the receiver to avoid a deadlock. +func (pep *perEvalPhase[T]) Each(report func(EvalPhase, *T)) { + pep.mu.Lock() + for phase, val := range pep.vals { + report(phase, val) + } + pep.mu.Unlock() +} + +// JustValue is a special implementation of [Referenceable] used in special +// situations where an [ExpressionScope] needs to just return a specific +// value directly, rather athn indirect through some other referencable object +// for dynamic value resolution. +type JustValue struct { + v cty.Value +} + +var _ Referenceable = JustValue{} + +// ExprReferenceValue implements Referenceable. +func (jv JustValue) ExprReferenceValue(context.Context, EvalPhase) cty.Value { + return jv.v +} diff --git a/internal/stacks/stackruntime/internal/stackeval/expressions_test.go b/internal/stacks/stackruntime/internal/stackeval/expressions_test.go new file mode 100644 index 0000000000..ee168f74f2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/expressions_test.go @@ -0,0 +1,378 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestEvalExpr(t *testing.T) { + t.Run("literal", func(t *testing.T) { + ctx := context.Background() + + v := cty.StringVal("hello") + expr := hcltest.MockExprLiteral(v) + scope := newStaticExpressionScope() + got, diags := EvalExpr(ctx, expr, PlanPhase, scope) + if diags.HasErrors() { + t.Errorf("unexpected diagnostics\n%s", diags.Err().Error()) + } + if got, want := got, v; !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("valid reference", func(t *testing.T) { + ctx := context.Background() + + v := cty.StringVal("indirect hello") + expr := hcltest.MockExprTraversalSrc("local.example") + scope := newStaticExpressionScope() + scope.AddVal(stackaddrs.LocalValue{Name: "example"}, v) + got, diags := EvalExpr(ctx, expr, PlanPhase, scope) + if diags.HasErrors() { + t.Errorf("unexpected diagnostics\n%s", diags.Err().Error()) + } + if got, want := got, v; !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("invalid reference", func(t *testing.T) { + ctx := context.Background() + + expr := hcltest.MockExprTraversalSrc("local.nonexist") + scope := newStaticExpressionScope() + _, diags := EvalExpr(ctx, expr, PlanPhase, scope) + if !diags.HasErrors() { + t.Errorf("unexpected success; want an error about local.nonexist not being defined") + } + }) + t.Run("multiple valid references", func(t *testing.T) { + ctx := context.Background() + + // The following is aiming for coverage of all of the valid + // stackaddrs.Referenceable implementations, since there's some + // address-type-specific logic in EvalExpr. This also includes + // some examples with extra traversal steps after the main address, + // which tests that we can handle references where only a prefix + // of the traversal is a referenceable object. + expr := hcltest.MockExprList([]hcl.Expression{ + hcltest.MockExprTraversalSrc("local.example"), + hcltest.MockExprTraversalSrc("var.example"), + hcltest.MockExprTraversalSrc("component.example"), + hcltest.MockExprTraversalSrc(`component.multi["foo"]`), + hcltest.MockExprTraversalSrc("stack.example"), + hcltest.MockExprTraversalSrc(`stack.multi["bar"]`), + hcltest.MockExprTraversalSrc("provider.beep.boop"), + hcltest.MockExprTraversalSrc(`provider.beep.boops["baz"]`), + hcltest.MockExprTraversalSrc(`terraform.applying`), + }) + + scope := newStaticExpressionScope() + scope.AddVal(stackaddrs.LocalValue{Name: "example"}, cty.StringVal("local value")) + scope.AddVal(stackaddrs.InputVariable{Name: "example"}, cty.StringVal("input variable")) + scope.AddVal(stackaddrs.Component{Name: "example"}, cty.StringVal("component singleton")) + scope.AddVal(stackaddrs.Component{Name: "multi"}, cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("component from for_each"), + })) + scope.AddVal(stackaddrs.StackCall{Name: "example"}, cty.StringVal("stack call singleton")) + scope.AddVal(stackaddrs.StackCall{Name: "multi"}, cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("stack call from for_each"), + })) + scope.AddVal(stackaddrs.ProviderConfigRef{ProviderLocalName: "beep", Name: "boop"}, cty.StringVal("provider config singleton")) + scope.AddVal(stackaddrs.ProviderConfigRef{ProviderLocalName: "beep", Name: "boops"}, cty.ObjectVal(map[string]cty.Value{ + "baz": cty.StringVal("provider config from for_each"), + })) + scope.AddVal(stackaddrs.TerraformApplying, cty.StringVal("terraform.applying value")) // NOTE: Not a realistic terraform.applying value; just a placeholder to help exercise EvalExpr + + got, diags := EvalExpr(ctx, expr, PlanPhase, scope) + if diags.HasErrors() { + t.Errorf("unexpected diagnostics\n%s", diags.Err().Error()) + } + want := cty.ListVal([]cty.Value{ + cty.StringVal("local value"), + cty.StringVal("input variable"), + cty.StringVal("component singleton"), + cty.StringVal("component from for_each"), + cty.StringVal("stack call singleton"), + cty.StringVal("stack call from for_each"), + cty.StringVal("provider config singleton"), + cty.StringVal("provider config from for_each"), + cty.StringVal("terraform.applying value"), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) +} + +func TestReferencesInExpr(t *testing.T) { + tests := []struct { + exprSrc string + wantTargets []stackaddrs.Referenceable + }{ + { + `"hello"`, + []stackaddrs.Referenceable{}, + }, + { + `var.foo`, + []stackaddrs.Referenceable{ + stackaddrs.InputVariable{ + Name: "foo", + }, + }, + }, + { + `var.foo + var.foo`, + []stackaddrs.Referenceable{ + stackaddrs.InputVariable{ + Name: "foo", + }, + stackaddrs.InputVariable{ + Name: "foo", + }, + }, + }, + { + `local.bar`, + []stackaddrs.Referenceable{ + stackaddrs.LocalValue{ + Name: "bar", + }, + }, + }, + { + `component.foo["bar"]`, + []stackaddrs.Referenceable{ + stackaddrs.Component{ + Name: "foo", + }, + }, + }, + { + `stack.foo["bar"]`, + []stackaddrs.Referenceable{ + stackaddrs.StackCall{ + Name: "foo", + }, + }, + }, + { + `provider.foo.bar["baz"]`, + []stackaddrs.Referenceable{ + stackaddrs.ProviderConfigRef{ + ProviderLocalName: "foo", + Name: "bar", + }, + }, + }, + { + `terraform.applying`, + []stackaddrs.Referenceable{ + stackaddrs.TerraformApplying, + }, + }, + } + + for _, test := range tests { + t.Run(test.exprSrc, func(t *testing.T) { + var diags tfdiags.Diagnostics + expr, hclDiags := hclsyntax.ParseExpression([]byte(test.exprSrc), "", hcl.InitialPos) + diags = diags.Append(hclDiags) + assertNoDiagnostics(t, diags) + + gotRefs := ReferencesInExpr(expr) + gotTargets := make([]stackaddrs.Referenceable, len(gotRefs)) + for i, ref := range gotRefs { + gotTargets[i] = ref.Target + } + + if diff := cmp.Diff(test.wantTargets, gotTargets); diff != "" { + t.Errorf("wrong reference targets\n%s", diff) + } + }) + } +} + +func TestEvalBody(t *testing.T) { + t.Run("success", func(t *testing.T) { + ctx := context.Background() + + body := hcltest.MockBody(&hcl.BodyContent{ + Attributes: hcl.Attributes{ + "literal": { + Name: "literal", + Expr: hcltest.MockExprLiteral(cty.StringVal("literal value")), + }, + "reference": { + Name: "reference", + Expr: hcltest.MockExprTraversalSrc("local.example"), + }, + }, + }) + + scope := newStaticExpressionScope() + scope.AddVal(stackaddrs.LocalValue{Name: "example"}, cty.StringVal("reference value")) + + spec := hcldec.ObjectSpec{ + "lit": &hcldec.AttrSpec{ + Name: "literal", + Type: cty.String, + }, + "ref": &hcldec.AttrSpec{ + Name: "reference", + Type: cty.String, + }, + } + + got, diags := EvalBody(ctx, body, spec, PlanPhase, scope) + if diags.HasErrors() { + t.Errorf("unexpected diagnostics\n%s", diags.Err().Error()) + } + want := cty.ObjectVal(map[string]cty.Value{ + "lit": cty.StringVal("literal value"), + "ref": cty.StringVal("reference value"), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) +} + +func TestPerEvalPhase(t *testing.T) { + pep := perEvalPhase[string]{} + + forPlan := pep.For(PlanPhase) + if forPlan == nil || *forPlan != "" { + t.Error("value should initially be the zero value of T") + } + forApply := pep.For(ApplyPhase) + if forApply == nil || *forApply != "" { + t.Error("value should initially be the zero value of T") + } + + *forPlan = "plan phase" + *forApply = "apply phase" + + forPlan = pep.For(PlanPhase) + if forPlan == nil || *forPlan != "plan phase" { + t.Error("didn't remember the value for the plan phase") + } + + forApply = pep.For(ApplyPhase) + if forApply == nil || *forApply != "apply phase" { + t.Error("didn't remember the value for the apply phase") + } + + *(pep.For(ValidatePhase)) = "validate phase" + + gotVals := map[EvalPhase]string{} + pep.Each(func(ep EvalPhase, t *string) { + gotVals[ep] = *t + }) + wantVals := map[EvalPhase]string{ + ValidatePhase: "validate phase", + PlanPhase: "plan phase", + ApplyPhase: "apply phase", + } + if diff := cmp.Diff(wantVals, gotVals); diff != "" { + t.Errorf("wrong values\n%s", diff) + } +} + +// staticReferenceable is an implementation of [Referenceable] that just +// returns a statically-provided value, as an aid to unit testing. +type staticReferenceable struct { + v cty.Value +} + +var _ Referenceable = staticReferenceable{} + +// ExprReferenceValue implements Referenceable. +func (r staticReferenceable) ExprReferenceValue(context.Context, EvalPhase) cty.Value { + return r.v +} + +// staticExpressionScope is an implementation of [ExpressionScope] that +// has a static table of referenceable objects that it returns on request, +// as an aid to unit testing. +type staticExpressionScope struct { + vs collections.Map[stackaddrs.Referenceable, Referenceable] +} + +var _ ExpressionScope = staticExpressionScope{} + +func newStaticExpressionScope() staticExpressionScope { + return staticExpressionScope{ + vs: collections.NewMapFunc[stackaddrs.Referenceable, Referenceable]( + func(r stackaddrs.Referenceable) collections.UniqueKey[stackaddrs.Referenceable] { + // Since this is just for testing purposes we'll use just + // string comparison for our key lookups. This should be fine + // as long as we continue to preserve the property that there + // is no overlap between string representations of different + // refereceable types, which is true at the time of writing + // this function. + return staticExpressionScopeKey(r.String()) + }, + ), + } +} + +// ResolveExpressionReference implements ExpressionScope. +func (s staticExpressionScope) ResolveExpressionReference(_ context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ret, ok := s.vs.GetOk(ref.Target) + if !ok { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The address %s does not match anything known to this test-focused static expression scope.", ref.Target.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags +} + +// ExternalFunctions implements ExpressionScope +func (s staticExpressionScope) ExternalFunctions(_ context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return lang.ExternalFuncs{}, nil +} + +// PlanTimestamp implements ExpressionScope +func (s staticExpressionScope) PlanTimestamp() time.Time { + return time.Now().UTC() +} + +// Add makes the given object available in the scope at the given address. +func (s staticExpressionScope) Add(addr stackaddrs.Referenceable, obj Referenceable) { + s.vs.Put(addr, obj) +} + +// AddVal is an convenience wrapper for Add which wraps the given value in a +// [staticReferenceable] before adding it. +func (s staticExpressionScope) AddVal(addr stackaddrs.Referenceable, val cty.Value) { + s.Add(addr, staticReferenceable{val}) +} + +type staticExpressionScopeKey string + +// IsUniqueKey implements collections.UniqueKey. +func (staticExpressionScopeKey) IsUniqueKey(stackaddrs.Referenceable) {} diff --git a/internal/stacks/stackruntime/internal/stackeval/for_each.go b/internal/stacks/stackruntime/internal/stackeval/for_each.go new file mode 100644 index 0000000000..bc1cab1277 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/for_each.go @@ -0,0 +1,251 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type instancesResult[T any] struct { + insts map[addrs.InstanceKey]T + unknown bool +} + +// evaluateForEachExpr deals with all of the for_each evaluation concerns +// that are common across all uses of for_each in all evaluation phases. +// +// The caller might still need to do some further validation or post-processing +// of the result for concerns that are specific to a particular phase or +// evaluation context. +func evaluateForEachExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope, callerDiagName string) (ExprResultValue, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + result, moreDiags := EvalExprAndEvalContext( + ctx, expr, phase, scope, + ) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return ExprResultValue{ + Value: cty.DynamicVal, + Expression: expr, + EvalContext: nil, + }, diags + } + ty := result.Value.Type() + + const invalidForEachSummary = "Invalid for_each value" + invalidForEachDetail := fmt.Sprintf("The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this %s.", callerDiagName) + const sensitiveForEachDetail = "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key." + switch { + case result.Value.HasMark(marks.Sensitive): + // Sensitive values are not allowed as for_each arguments because + // they could be exposed as resource instance keys. + // TODO: This should have Extra: tdiagnosticCausedBySensitive(true), + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: invalidForEachSummary, + Detail: sensitiveForEachDetail, + Subject: result.Expression.Range().Ptr(), + Expression: result.Expression, + EvalContext: result.EvalContext, + }) + return result, diags + + case result.Value.IsNull(): + // we don't alllow null values + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: invalidForEachSummary, + Detail: fmt.Sprintf("%s The for_each expression produced a null value.", invalidForEachDetail), + Subject: result.Expression.Range().Ptr(), + Expression: result.Expression, + EvalContext: result.EvalContext, + }) + return DerivedExprResult(result, cty.DynamicVal), diags + + case ty.IsObjectType() || ty.IsMapType(): + // okay + + case ty.IsSetType(): + // since we can't use a set values that are unknown, we treat the + // entire set as unknown + if !result.Value.IsWhollyKnown() { + return result, diags + } + + if markSafeLengthInt(result.Value) == 0 { + // we are okay with an empty set + return result, diags + } + + if !ty.ElementType().Equals(cty.String) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: invalidForEachSummary, + Detail: fmt.Sprintf(`%s "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, invalidForEachDetail, ty.ElementType().FriendlyName()), + Subject: result.Expression.Range().Ptr(), + Expression: result.Expression, + EvalContext: result.EvalContext, + }) + return DerivedExprResult(result, cty.DynamicVal), diags + } + + // Check if one of the values in the set is null + for k, v := range result.Value.AsValueSet().Values() { + if v.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: invalidForEachSummary, + Detail: fmt.Sprintf("%s The for_each value must not contain null elements, but the element at index %d was null.", invalidForEachDetail, k), + Subject: result.Expression.Range().Ptr(), + Expression: result.Expression, + EvalContext: result.EvalContext, + }) + } + } + + case !result.Value.IsWhollyKnown() && ty.HasDynamicTypes(): + // If the value is unknown and has dynamic types, we can't + // determine if it's a valid for_each value, so we'll just + // return the unknown value. + return result, diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: invalidForEachSummary, + Detail: invalidForEachDetail, + Subject: result.Expression.Range().Ptr(), + Expression: result.Expression, + EvalContext: result.EvalContext, + }) + return DerivedExprResult(result, cty.DynamicVal), diags + } + + // Sensitive values are also typically disallowed, but sensitivity gets + // decided dynamically based on data flow and so we'll treat those as + // plan-time errors, to be handled by the caller. + + return result, diags +} + +// instancesMap constructs a map of instances of some expandable object, +// based on its for_each value or on the absence of such a value. +// +// If maybeForEachVal is [cty.NilVal] then the result is always a +// single-element map with an `addrs.NoKey` instance. +// +// If maybeForEachVal is non-nil then it must be a non-error result from +// an earlier call to [evaluateForEachExpr] which analyzed the given for_each +// expression. If the value is unknown then the result will be nil. Otherwise, +// the result is guaranteed to be a non-nil map with the same number of elements +// as the given for_each collection/structure. +// +// If maybeForEach value is non-nil but not a valid value produced by +// [evaluateForEachExpr] then the behavior is unpredictable, including the +// possibility of a panic. +func instancesMap[T any](maybeForEachVal cty.Value, makeInst func(addrs.InstanceKey, instances.RepetitionData) T) instancesResult[T] { + switch { + case maybeForEachVal == cty.NilVal: + // No for_each expression at all, then. We have exactly one instance + // without an instance key and with no repetition data. + return instancesResult[T]{noForEachInstancesMap(makeInst), false} + + case !maybeForEachVal.IsKnown(): + // This is temporary to gradually rollout support for unknown for_each values + return instancesResult[T]{nil, true} + + default: + // Otherwise we should be able to assume the value is valid per the + // definition of [evaluateForEachExpr]. The following will panic if + // that other function doesn't satisfy its documented contract; + // if that happens, prefer to correct the either that function or + // its caller rather than adding further complexity here. + + // NOTE: We MUST return a non-nil map from every return path under + // this case, even if there are zero elements in it, because a nil map + // represents an _invalid_ for_each expression (handled above). + // forEachInstancesMap guarantees to never return a nil map. + return instancesResult[T]{forEachInstancesMap(maybeForEachVal, makeInst), false} + + } +} + +// forEachInstanceKeys takes a value previously returned by +// [evaluateForEachExpr] and produces a map where each element maps from an +// instance key to a corresponding object decided by the givenc callback +// function. +// +// The result is guaranteed to be a non-nil map, even if the given value +// produces zero instances, because some callers use a nil map to represent +// the situation where the for_each value is too invalid to construct any +// map at all. +// +// This function is only designed to deal with valid (non-error) results from +// [evaluateForEachExpr] and so might panic if given other values. +func forEachInstancesMap[T any](forEachVal cty.Value, makeInst func(addrs.InstanceKey, instances.RepetitionData) T) map[addrs.InstanceKey]T { + ty := forEachVal.Type() + switch { + case ty.IsObjectType() || ty.IsMapType(): + elems := forEachVal.AsValueMap() + ret := make(map[addrs.InstanceKey]T, len(elems)) + for k, v := range elems { + ik := addrs.StringKey(k) + ret[ik] = makeInst(ik, instances.RepetitionData{ + EachKey: cty.StringVal(k), + EachValue: v, + }) + } + return ret + + case ty.IsSetType(): + if markSafeLengthInt(forEachVal) == 0 { + // Zero-length for_each, so we have no instances. + return make(map[addrs.InstanceKey]T) + } + + // evaluateForEachExpr should have already guaranteed us a set of + // strings, but we'll check again here just so we can panic more + // intellgibly if that function is buggy. + if ty.ElementType() != cty.String { + panic(fmt.Sprintf("invalid forEachVal %#v", forEachVal)) + } + + elems := forEachVal.AsValueSlice() + ret := make(map[addrs.InstanceKey]T, len(elems)) + for _, sv := range elems { + k := addrs.StringKey(sv.AsString()) + ret[k] = makeInst(k, instances.RepetitionData{ + EachKey: sv, + EachValue: sv, + }) + } + return ret + + default: + panic(fmt.Sprintf("invalid forEachVal %#v", forEachVal)) + } +} + +func noForEachInstancesMap[T any](makeInst func(addrs.InstanceKey, instances.RepetitionData) T) map[addrs.InstanceKey]T { + return map[addrs.InstanceKey]T{ + addrs.NoKey: makeInst(addrs.NoKey, instances.RepetitionData{ + // no repetition symbols available in this case + }), + } +} + +// markSafeLengthInt allows calling LengthInt on marked values safely +func markSafeLengthInt(val cty.Value) int { + v, _ := val.UnmarkDeep() + return v.LengthInt() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/for_each_test.go b/internal/stacks/stackruntime/internal/stackeval/for_each_test.go new file mode 100644 index 0000000000..3804703d91 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/for_each_test.go @@ -0,0 +1,463 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestEvaluateForEachExpr(t *testing.T) { + tests := map[string]struct { + Expr hcl.Expression + Want cty.Value + WantErr string + }{ + // Objects + "empty object": { + Expr: hcltest.MockExprLiteral(cty.EmptyObjectVal), + Want: cty.EmptyObjectVal, + }, + "non-empty object": { + Expr: hcltest.MockExprLiteral(cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + "b": cty.StringVal("beep"), + })), + Want: cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + "b": cty.StringVal("beep"), + }), + }, + + // Maps + "map of string": { + Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + "b": cty.StringVal("boop"), + })), + Want: cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + "b": cty.StringVal("boop"), + }), + }, + "empty map of string": { + Expr: hcltest.MockExprLiteral(cty.MapValEmpty(cty.String)), + Want: cty.MapValEmpty(cty.String), + }, + "unknown map of string": { + Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), + Want: cty.UnknownVal(cty.Map(cty.String)), + }, + "sensitive map of string": { + Expr: hcltest.MockExprLiteral(cty.MapValEmpty(cty.String).Mark(marks.Sensitive)), + WantErr: `Invalid for_each value`, + }, + "map of object": { + Expr: hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ + "a": cty.EmptyObjectVal, + "b": cty.EmptyObjectVal, + })), + Want: cty.MapVal(map[string]cty.Value{ + "a": cty.EmptyObjectVal, + "b": cty.EmptyObjectVal, + }), + }, + "empty map of object": { + Expr: hcltest.MockExprLiteral(cty.MapValEmpty(cty.EmptyObject)), + Want: cty.MapValEmpty(cty.EmptyObject), + }, + + // Sets + "set of string": { + Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{ + cty.StringVal("beep"), + cty.StringVal("boop"), + })), + Want: cty.SetVal([]cty.Value{ + cty.StringVal("beep"), + cty.StringVal("boop"), + }), + }, + "empty set of string": { + Expr: hcltest.MockExprLiteral(cty.SetValEmpty(cty.String)), + Want: cty.SetValEmpty(cty.String), + }, + "unknown set of string": { + Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), + Want: cty.UnknownVal(cty.Set(cty.String)), + }, + "empty set": { + Expr: hcltest.MockExprLiteral(cty.SetValEmpty(cty.EmptyTuple)), + Want: cty.SetValEmpty(cty.EmptyTuple), + }, + "sensitive set of string": { + Expr: hcltest.MockExprLiteral(cty.SetValEmpty(cty.String).Mark(marks.Sensitive)), + WantErr: `Invalid for_each value`, + }, + "empty set of object": { + Expr: hcltest.MockExprLiteral(cty.SetValEmpty(cty.EmptyObject)), + Want: cty.SetValEmpty(cty.EmptyObject), + }, + "set with null": { + Expr: hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("valid"), cty.NullVal(cty.String)})), + WantErr: `Invalid for_each value`, + }, + + // Nulls of any type are not allowed + "null object": { + Expr: hcltest.MockExprLiteral(cty.NullVal(cty.EmptyObject)), + WantErr: `Invalid for_each value`, + }, + "null map": { + Expr: hcltest.MockExprLiteral(cty.NullVal(cty.Map(cty.String))), + WantErr: `Invalid for_each value`, + }, + "null set": { + Expr: hcltest.MockExprLiteral(cty.NullVal(cty.Set(cty.String))), + WantErr: `Invalid for_each value`, + }, + "null string": { + Expr: hcltest.MockExprLiteral(cty.NullVal(cty.String)), + WantErr: `Invalid for_each value`, + }, + + // Unknown sets, maps, objects, and dynamic types are allowed + "unknown set": { + Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), + Want: cty.UnknownVal(cty.Set(cty.String)), + }, + "unknown map": { + Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.String))), + Want: cty.UnknownVal(cty.Map(cty.String)), + }, + "unknown object": { + Expr: hcltest.MockExprLiteral(cty.UnknownVal(cty.EmptyObject)), + Want: cty.UnknownVal(cty.EmptyObject), + }, + "unknown dynamic type": { + Expr: hcltest.MockExprLiteral(cty.DynamicVal), + Want: cty.DynamicVal, + }, + } + + ctx := context.Background() + scope := newStaticExpressionScope() + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + gotResult, diags := evaluateForEachExpr(ctx, test.Expr, PlanPhase, scope, "test") + got := gotResult.Value + + if test.WantErr != "" { + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error\ngot: %#v", got) + } + foundErr := false + for _, diag := range diags { + if diag.Severity() != tfdiags.Error { + continue + } + if diag.Description().Summary == test.WantErr { + foundErr = true + break + } + } + if !foundErr { + t.Errorf("missing expected error\nwant summary: %s\ngot: %s", test.WantErr, spew.Sdump(diags.ForRPC())) + } + return + } + + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", spew.Sdump(diags.ForRPC())) + } + if !test.Want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestInstancesMap(t *testing.T) { + + type InstanceObj struct { + Key addrs.InstanceKey + Rep instances.RepetitionData + } + // This is a temporary nusiance while we gradually rollout support for + // unknown for_each values. + type Expectation struct { + UnknownValue bool + UnknownForEachSupported map[addrs.InstanceKey]InstanceObj + UnknownForEachUnsupported map[addrs.InstanceKey]InstanceObj + } + makeObj := func(k addrs.InstanceKey, r instances.RepetitionData) InstanceObj { + return InstanceObj{ + Key: k, + Rep: r, + } + } + + tests := []struct { + Name string + Input cty.Value + Want Expectation + + // This function always either succeeds or panics, because it + // expects to be given already-validated input from another function. + // We're only testing the success cases here. + }{ + // No for_each at all + { + "nil", + cty.NilVal, + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + addrs.NoKey: { + Key: addrs.NoKey, + Rep: instances.RepetitionData{ + // No data available for the non-repeating case + }, + }, + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + addrs.NoKey: { + Key: addrs.NoKey, + Rep: instances.RepetitionData{ + // No data available for the non-repeating case + }, + }, + }, + }, + }, + + // Unknowns + { + "unknown empty object", + cty.UnknownVal(cty.EmptyObject), + Expectation{ + UnknownValue: true, + UnknownForEachSupported: nil, + UnknownForEachUnsupported: nil, + }, + }, + { + "unknown bool map", + cty.UnknownVal(cty.Map(cty.Bool)), + Expectation{ + UnknownValue: true, + UnknownForEachSupported: nil, + UnknownForEachUnsupported: nil, + }, + }, + { + "unknown set of strings", + cty.UnknownVal(cty.Set(cty.String)), + Expectation{ + UnknownValue: true, + UnknownForEachSupported: nil, + UnknownForEachUnsupported: nil, + }, + }, + + // Empties + { + "empty object", + cty.EmptyObjectVal, + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + // intentionally a non-nil empty map to assert that we know + // that there are zero instances, rather than that we don't + // know how many there are. + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + // intentionally a non-nil empty map to assert that we know + // that there are zero instances, rather than that we don't + // know how many there are. + }, + }, + }, + { + "empty string map", + cty.MapValEmpty(cty.String), + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + // intentionally a non-nil empty map to assert that we know + // that there are zero instances, rather than that we don't + // know how many there are. + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + // intentionally a non-nil empty map to assert that we know + // that there are zero instances, rather than that we don't + // know how many there are. + }, + }, + }, + { + "empty string set", + cty.SetValEmpty(cty.String), + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + // intentionally a non-nil empty map to assert that we know + // that there are zero instances, rather than that we don't + // know how many there are. + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + // intentionally a non-nil empty map to assert that we know + // that there are zero instances, rather than that we don't + // know how many there are. + }, + }, + }, + + // Known and not empty + { + "object", + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + "b": cty.StringVal("boop"), + }), + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + addrs.StringKey("a"): { + Key: addrs.StringKey("a"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.StringVal("beep"), + }, + }, + addrs.StringKey("b"): { + Key: addrs.StringKey("b"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.StringVal("boop"), + }, + }, + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + addrs.StringKey("a"): { + Key: addrs.StringKey("a"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.StringVal("beep"), + }, + }, + addrs.StringKey("b"): { + Key: addrs.StringKey("b"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.StringVal("boop"), + }, + }, + }, + }, + }, + { + "map", + cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + "b": cty.StringVal("boop"), + }), + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + addrs.StringKey("a"): { + Key: addrs.StringKey("a"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.StringVal("beep"), + }, + }, + addrs.StringKey("b"): { + Key: addrs.StringKey("b"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.StringVal("boop"), + }, + }, + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + addrs.StringKey("a"): { + Key: addrs.StringKey("a"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.StringVal("beep"), + }, + }, + addrs.StringKey("b"): { + Key: addrs.StringKey("b"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.StringVal("boop"), + }, + }, + }, + }, + }, + { + "set", + cty.SetVal([]cty.Value{ + cty.StringVal("beep"), + cty.StringVal("boop"), + }), + Expectation{ + UnknownForEachSupported: map[addrs.InstanceKey]InstanceObj{ + addrs.StringKey("beep"): { + Key: addrs.StringKey("beep"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("beep"), + EachValue: cty.StringVal("beep"), + }, + }, + addrs.StringKey("boop"): { + Key: addrs.StringKey("boop"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("boop"), + EachValue: cty.StringVal("boop"), + }, + }, + }, + UnknownForEachUnsupported: map[addrs.InstanceKey]InstanceObj{ + addrs.StringKey("beep"): { + Key: addrs.StringKey("beep"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("beep"), + EachValue: cty.StringVal("beep"), + }, + }, + addrs.StringKey("boop"): { + Key: addrs.StringKey("boop"), + Rep: instances.RepetitionData{ + EachKey: cty.StringVal("boop"), + EachValue: cty.StringVal("boop"), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + got := instancesMap(test.Input, makeObj) + if got.unknown != test.Want.UnknownValue { + t.Errorf("wrong unknown value\ngot: %#v\nwant: %#v", got.unknown, test.Want.UnknownValue) + } + if diff := cmp.Diff(test.Want.UnknownForEachSupported, got.insts, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\ninput: %#v\n%s", test.Input, diff) + } + }) + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/hooks.go b/internal/stacks/stackruntime/internal/stackeval/hooks.go new file mode 100644 index 0000000000..e6879dace2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/hooks.go @@ -0,0 +1,260 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync" + + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" +) + +// Hooks is an optional API for external callers to be notified about various +// progress events during plan and apply operations. +// +// This type is exposed to external callers through a type alias in package +// stackruntime, and so it is part of the public API of that package despite +// being defined in here. +type Hooks struct { + // BeginPlan is called at the very start of a stack plan operation, + // encompassing that entire operation to allow establishing a top-level + // tracing context for that operation. + // + // BeginPlan does not provide any additional data, because no work has + // happened yet. + BeginPlan hooks.BeginFunc[struct{}] + + // EndPlan marks the end of the overall planning process started at + // [Hooks.BeginPlan]. If [Hooks.BeginPlan] opened a tracing span then + // this EndPlan should end it. + // + // EndPlan does not provide any additional data, because all relevant + // information is provided by other means. + EndPlan hooks.MoreFunc[struct{}] + + // BeginApply is called at the very start of a stack apply operation, + // encompassing that entire operation to allow establishing a top-level + // tracing context for that operation. + // + // BeginApply does not provide any additional data, because no work has + // happened yet. + BeginApply hooks.BeginFunc[struct{}] + + // EndApply marks the end of the overall apply process started at + // [Hooks.BeginApply]. If [Hooks.BeginApply] opened a tracing span then + // this EndApply should end it. + // + // EndApply does not provide any additional data, because all relevant + // information is provided by other means. + EndApply hooks.MoreFunc[struct{}] + + // ComponentExpanded is called when a plan operation evaluates the + // expansion argument for a component, resulting in zero or more instances. + ComponentExpanded hooks.SingleFunc[*hooks.ComponentInstances] + + // RemovedComponentExpanded is called when a plan operation evaluates the + // expansion argument for a removed block. + RemovedComponentExpanded hooks.SingleFunc[*hooks.RemovedComponentInstances] + + // PendingComponentInstancePlan is called at the start of the plan + // operation, before evaluating the component instance's inputs and + // providers. + PendingComponentInstancePlan hooks.SingleFunc[stackaddrs.AbsComponentInstance] + + // BeginComponentInstancePlan is called when the component instance's + // inputs and providers are ready and planning begins, and can be used to + // establish a nested tracing context wrapping the plan operation. + BeginComponentInstancePlan hooks.BeginFunc[stackaddrs.AbsComponentInstance] + + // EndComponentInstancePlan is called when the component instance plan + // started at [Hooks.BeginComponentInstancePlan] completes successfully. If + // a context is established by [Hooks.BeginComponentInstancePlan] then this + // hook should end it. + EndComponentInstancePlan hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // ErrorComponentInstancePlan is similar to [Hooks.EndComponentInstancePlan], but + // is called when the plan operation failed. + ErrorComponentInstancePlan hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // DeferComponentInstancePlan is similar to [Hooks.EndComponentInstancePlan], but + // is called when the plan operation succeeded but signaled a deferral. + DeferComponentInstancePlan hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // PendingComponentInstanceApply is called at the start of the apply + // operation. + PendingComponentInstanceApply hooks.SingleFunc[stackaddrs.AbsComponentInstance] + + // BeginComponentInstanceApply is called when the component instance starts + // applying the plan, and can be used to establish a nested tracing context + // wrapping the apply operation. + BeginComponentInstanceApply hooks.BeginFunc[stackaddrs.AbsComponentInstance] + + // EndComponentInstanceApply is called when the component instance plan + // started at [Hooks.BeginComponentInstanceApply] completes successfully. If + // a context is established by [Hooks.BeginComponentInstanceApply] then + // this hook should end it. + EndComponentInstanceApply hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // ErrorComponentInstanceApply is similar to [Hooks.EndComponentInstanceApply], but + // is called when the apply operation failed. + ErrorComponentInstanceApply hooks.MoreFunc[stackaddrs.AbsComponentInstance] + + // ReportResourceInstanceStatus is called when a resource instance's status + // changes during a plan or apply operation. It should be called inside a + // tracing context established by [Hooks.BeginComponentInstancePlan] or + // [Hooks.BeginComponentInstanceApply]. + ReportResourceInstanceStatus hooks.MoreFunc[*hooks.ResourceInstanceStatusHookData] + + // ReportResourceInstanceProvisionerStatus is called when a provisioner for + // a resource instance begins or ends. It should be called inside a tracing + // context established by [Hooks.BeginComponentInstanceApply]. + ReportResourceInstanceProvisionerStatus hooks.MoreFunc[*hooks.ResourceInstanceProvisionerHookData] + + // ReportResourceInstanceDrift is called after a component instance's plan + // determines that a resource instance has experienced changes outside of + // Terraform. It should be called inside a tracing context established by + // [Hooks.BeginComponentInstancePlan]. + ReportResourceInstanceDrift hooks.MoreFunc[*hooks.ResourceInstanceChange] + + // ReportResourceInstancePlanned is called after a component instance's + // plan results in proposed changes for a resource instance. It should be + // called inside a tracing context established by + // [Hooks.BeginComponentInstancePlan]. + ReportResourceInstancePlanned hooks.MoreFunc[*hooks.ResourceInstanceChange] + + // ReportResourceInstanceDeferred is called after a component instance's + // plan results in a resource instance being deferred. It should be called + // inside a tracing context established by + // [Hooks.BeginComponentInstancePlan]. + ReportResourceInstanceDeferred hooks.MoreFunc[*hooks.DeferredResourceInstanceChange] + + // ReportComponentInstancePlanned is called after a component instance + // is planned. It should be called inside a tracing context established by + // [Hooks.BeginComponentInstancePlan]. + ReportComponentInstancePlanned hooks.MoreFunc[*hooks.ComponentInstanceChange] + + // ReportComponentInstanceApplied is called after a component instance + // plan is applied. It should be called inside a tracing context + // established by [Hooks.BeginComponentInstanceApply]. + ReportComponentInstanceApplied hooks.MoreFunc[*hooks.ComponentInstanceChange] + + // ContextAttach is an optional callback for wrapping a non-nil value + // returned by a [hooks.BeginFunc] into a [context.Context] to be passed + // to other context-aware operations that descend from the operation that + // was begun. + // + // See the docs for [hooks.ContextAttachFunc] for more information. + ContextAttach hooks.ContextAttachFunc +} + +// A do-nothing default Hooks that we use when the caller doesn't provide one. +var noHooks = &Hooks{} + +// ContextWithHooks returns a context that carries the given [Hooks] as +// one of its values. +func ContextWithHooks(parent context.Context, hooks *Hooks) context.Context { + return context.WithValue(parent, hooksContextKey{}, hooks) +} + +func hooksFromContext(ctx context.Context) *Hooks { + hooks, ok := ctx.Value(hooksContextKey{}).(*Hooks) + if !ok { + return noHooks + } + return hooks +} + +type hooksContextKey struct{} + +// hookSeq is a small helper for keeping track of a sequence of hooks related +// to the same multi-step action. +// +// It retains the hook implementer's arbitrary tracking values between calls +// so as to reduce the visual noise and complexity of our main evaluation code. +// Once a hook sequence has begun using a "begin" callback, it's safe to run +// subsequent hooks concurrently from multiple goroutines, although from +// the caller's perspective that will make the propagation of changes to their +// tracking values appear unpredictable. +type hookSeq struct { + tracking any + mu sync.Mutex +} + +// hookBegin begins a hook sequence by calling a [hooks.BeginFunc] callback. +// +// The result can be used with [hookMore] to report ongoing progress or +// completion of whatever multi-step process has begun. +// +// This function also deals with the optional [hook.ContextAttachFunc] that +// hook implementers may provide. If it's non-nil then the returned context +// is the result of that function. Otherwise it is the same context provided +// by the caller. + +// Callers should use the returned context for all subsequent context-aware +// calls that are related to whatever multi-step operation this hook sequence +// represents, so that the hook subscriber can use this mechanism to propagate +// distributed tracing spans to downstream operations. Callers MUST also use +// descendants of the resulting context for any subsequent calls to +// [runHookBegin] using the returned [hookSeq]. +func hookBegin[Msg any](ctx context.Context, cb hooks.BeginFunc[Msg], ctxCb hooks.ContextAttachFunc, msg Msg) (*hookSeq, context.Context) { + tracking := runHookBegin(ctx, cb, msg) + if ctxCb != nil { + ctx = ctxCb(ctx, tracking) + } + return &hookSeq{ + tracking: tracking, + }, ctx +} + +// hookMore continues a hook sequence by calling a [hooks.MoreFunc] callback +// using the tracking state retained by the given [hookSeq]. +// +// It's safe to use [hookMore] with the same [hookSeq] from multiple goroutines +// concurrently, and it's guaranteed that no two hooks will run concurrently +// within the same sequence, but it'll be unpredictable from the caller's +// standpoint which order the hooks will occur. +func hookMore[Msg any](ctx context.Context, seq *hookSeq, cb hooks.MoreFunc[Msg], msg Msg) { + // We hold the lock throughout the hook call so that callers don't need + // to worry about concurrent calls to their hooks and so that the + // propagation of the arbitrary "tracking" values from one hook to the + // next will always exact follow the sequence of the calls. + seq.mu.Lock() + seq.tracking = runHookMore(ctx, cb, seq.tracking, msg) + seq.mu.Unlock() +} + +// hookSingle calls an isolated [hooks.SingleFunc] callback, if it is non-nil. +func hookSingle[Msg any](ctx context.Context, cb hooks.SingleFunc[Msg], msg Msg) { + if cb != nil { + cb(ctx, msg) + } +} + +// runHookBegin is a lower-level helper that just directly runs a given +// callback if it isn't nil and returns its result. If the given callback is +// nil then runHookBegin immediately returns nil. +func runHookBegin[Msg any](ctx context.Context, cb hooks.BeginFunc[Msg], msg Msg) any { + if cb == nil { + return nil + } + return cb(ctx, msg) +} + +// runHookMore is a lower-level helper that just directly runs a given +// callback if it isn't nil and returns the effective new tracking value, +// which may or may not be the same value passed as "tracking". +// If the given callback is nil then runHookMore immediately returns the given +// tracking value. +func runHookMore[Msg any](ctx context.Context, cb hooks.MoreFunc[Msg], tracking any, msg Msg) any { + if cb == nil { + // We'll retain any existing tracking value, then. + return tracking + } + newTracking := cb(ctx, tracking, msg) + if newTracking != nil { + return newTracking + } + return tracking +} diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go new file mode 100644 index 0000000000..2dbab6d8c1 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -0,0 +1,372 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/objchange" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// InputVariable represents an input variable belonging to a [Stack]. +type InputVariable struct { + addr stackaddrs.AbsInputVariable + stack *Stack + config *InputVariableConfig + + main *Main + + value perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ Plannable = (*InputVariable)(nil) +var _ Referenceable = (*InputVariable)(nil) + +func newInputVariable(main *Main, addr stackaddrs.AbsInputVariable, stack *Stack, config *InputVariableConfig) *InputVariable { + return &InputVariable{ + addr: addr, + stack: stack, + config: config, + main: main, + } +} + +// DefinedByStackCallInstance returns the stack call which ought to provide +// the definition (i.e. the final value) of this input variable. The source +// of the stack could either be a regular stack call instance or a removed +// stack call instance. One of the two will be returned. They are mutually +// exclusive as it is an error for two blocks to create the same stack instance. +// +// Returns nil if this input variable belongs to the main stack, because +// the main stack's input variables come from the planning options instead. +// +// Also returns nil if the receiver belongs to a stack config instance +// that isn't actually declared in the configuration, which typically suggests +// that we don't yet know the number of instances of one of the stack calls +// along the chain. +func (v *InputVariable) DefinedByStackCallInstance(ctx context.Context, phase EvalPhase) (*StackCallInstance, *RemovedStackCallInstance) { + declarerAddr := v.addr.Stack + if declarerAddr.IsRoot() { + return nil, nil + } + + callAddr := declarerAddr.Call() + + if call := v.stack.parent.EmbeddedStackCall(callAddr.Item); call != nil { + lastStep := declarerAddr[len(declarerAddr)-1] + instKey := lastStep.Key + + callInsts, unknown := call.Instances(ctx, phase) + if unknown { + // Return our static unknown instance for this variable. + return call.UnknownInstance(ctx, instKey, phase), nil + } + if inst, ok := callInsts[instKey]; ok { + return inst, nil + } + + // otherwise, let's check if we have any removed calls that match the + // target instance + } + + if calls := v.stack.parent.RemovedEmbeddedStackCall(callAddr.Item); calls != nil { + for _, call := range calls { + callInsts, unknown := call.InstancesFor(ctx, v.stack.addr, phase) + if unknown { + return nil, call.UnknownInstance(ctx, v.stack.addr, phase) + } + for _, inst := range callInsts { + // because we used the exact v.stack.addr in InstancesFor above + // then we should have at most one entry here if there were any + // matches. + return nil, inst + } + } + } + + return nil, nil +} + +func (v *InputVariable) Value(ctx context.Context, phase EvalPhase) cty.Value { + val, _ := v.CheckValue(ctx, phase) + return val +} + +func (v *InputVariable) CheckValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags( + ctx, v.tracingName(), v.value.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + cfg := v.config + decl := cfg.config + + switch { + case v.addr.Stack.IsRoot(): + var err error + + wantTy := decl.Type.Constraint + extVal := v.main.RootVariableValue(v.addr.Item, phase) + + val := extVal.Value + if val.IsNull() { + // A null value is equivalent to an unspecified value, so + // we'll replace it with the variable's default value. + val = cfg.DefaultValue() + if val == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No value for required variable", + Detail: fmt.Sprintf("The root input variable %q is not set, and has no default value.", v.addr), + Subject: cfg.config.DeclRange.ToHCL().Ptr(), + }) + return cty.UnknownVal(wantTy), diags + } + } else { + // The DefaultValue function already validated the default + // value, and applied the defaults, so we only apply the + // defaults to a user supplied value. + if defaults := decl.Type.Defaults; defaults != nil { + val = defaults.Apply(val) + } + } + + // First, apply any defaults that are declared in the + // configuration. + + // Next, convert the value to the expected type. + val, err = convert.Convert(val, wantTy) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid value for root input variable", + Detail: fmt.Sprintf( + "Cannot use the given value for input variable %q: %s.", + v.addr.Item.Name, err, + ), + }) + val = cfg.markValue(cty.UnknownVal(wantTy)) + return val, diags + } + + if phase == ApplyPhase && !cfg.config.Ephemeral { + // Now, we're just going to check the apply time value + // against the plan time value. It is expected that + // ephemeral variables will have different values between + // plan and apply time, so these are not checked here. + plan := v.main.PlanBeingApplied() + planValue := plan.RootInputValues[v.addr.Item] + if errs := objchange.AssertValueCompatible(planValue, val); errs != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Inconsistent value for input variable during apply", + Detail: fmt.Sprintf("The value for non-ephemeral input variable %q was set to a different value during apply than was set during plan. Only ephemeral input variables can change between the plan and apply phases.", v.addr.Item.Name), + Subject: cfg.config.DeclRange.ToHCL().Ptr(), + }) + // Return a solidly invalid value to prevent further + // processing of this variable. This is a rare case and + // a bug in Terraform so it's okay that might cause + // additional errors to be raised later. We just want + // to make sure we don't continue when something has + // gone wrong elsewhere. + return cty.NilVal, diags + } + } + + // TODO: check the value against any custom validation rules + // declared in the configuration. + return cfg.markValue(val), diags + + default: + definedByCallInst, definedByRemovedCallInst := v.DefinedByStackCallInstance(ctx, phase) + switch { + case definedByCallInst != nil: + allVals := definedByCallInst.InputVariableValues(ctx, phase) + val := allVals.GetAttr(v.addr.Item.Name) + + // TODO: check the value against any custom validation rules + // declared in the configuration. + + return cfg.markValue(val), diags + case definedByRemovedCallInst != nil: + allVals, _ := definedByRemovedCallInst.InputVariableValues(ctx, phase) + val := allVals.GetAttr(v.addr.Item.Name) + + // TODO: check the value against any custom validation rules + // declared in the configuration. + + return cfg.markValue(val), diags + default: + // We seem to belong to a call instance that doesn't actually + // exist in the configuration. That either means that + // something's gone wrong or we are descended from a stack + // call whose instances aren't known yet; we'll assume + // the latter and return a placeholder. + return cfg.markValue(cty.UnknownVal(v.config.config.Type.Constraint)), diags + } + } + }, + ) +} + +// ExprReferenceValue implements Referenceable. +func (v *InputVariable) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + return v.Value(ctx, phase) +} + +func (v *InputVariable) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, moreDiags := v.CheckValue(ctx, phase) + diags = diags.Append(moreDiags) + + return diags +} + +// PlanChanges implements Plannable as a plan-time validation of the variable's +// declaration and of the caller's definition of the variable. +func (v *InputVariable) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + diags := v.checkValid(ctx, PlanPhase) + if diags.HasErrors() { + return nil, diags + } + + // Only the root stack's input values can contribute directly to the plan. + // Embedded stack inputs will be recalculated during the apply phase + // because the values might be derived from component outputs that aren't + // known yet during planning. + if !v.addr.Stack.IsRoot() { + return nil, diags + } + + destroy := v.main.PlanningOpts().PlanningMode == plans.DestroyMode + + before := v.main.PlanPrevState().RootInputVariable(v.addr.Item) + + decl := v.config.config + after := v.Value(ctx, PlanPhase) + requiredOnApply := false + if decl.Ephemeral { + // we don't persist the value for an ephemeral variable, but we + // do need to remember whether it was set. + requiredOnApply = !after.IsNull() + + // we'll set the after value to null now that we've captured the + // requiredOnApply flag. + after = cty.NullVal(after.Type()) + } + + var action plans.Action + if before != cty.NilVal { + if decl.Ephemeral { + // if the value is ephemeral, we always mark is as an update + action = plans.Update + } else { + unmarkedBefore, beforePaths := before.UnmarkDeepWithPaths() + unmarkedAfter, afterPaths := after.UnmarkDeepWithPaths() + result := unmarkedBefore.Equals(unmarkedAfter) + if result.IsKnown() && result.True() && marks.MarksEqual(beforePaths, afterPaths) { + action = plans.NoOp + } else { + // If we don't know for sure that the values are equal, then we'll + // call this an update. + action = plans.Update + } + } + } else { + action = plans.Create + before = cty.NullVal(cty.DynamicPseudoType) + } + + return []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: v.addr.Item, + Action: action, + Before: before, + After: after, + RequiredOnApply: requiredOnApply, + DeleteOnApply: destroy, + }, + }, diags +} + +// References implements Referrer +func (v *InputVariable) References(ctx context.Context) []stackaddrs.AbsReference { + // The references for an input variable actually come from the + // call that defines it, in the parent stack. + if v.addr.Stack.IsRoot() { + // Variables declared in the root module can't refer to anything, + // because they are defined outside of the stack configuration by + // our caller. + return nil + } + if v.stack.parent == nil { + // Weird, but we'll tolerate it for robustness. + return nil + } + callAddr := v.addr.Stack.Call() + call := v.stack.parent.EmbeddedStackCall(callAddr.Item) + if call == nil { + // Weird, but we'll tolerate it for robustness. + return nil + } + return call.References(ctx) +} + +// CheckApply implements Applyable. +func (v *InputVariable) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + if !v.addr.Stack.IsRoot() { + return nil, v.checkValid(ctx, ApplyPhase) + } + + diags := v.checkValid(ctx, ApplyPhase) + if diags.HasErrors() { + return nil, diags + } + + if v.main.PlanBeingApplied().DeletedInputVariables.Has(v.addr.Item) { + // If the plan being applied has this variable as being deleted, then + // we won't handle it here. This is usually the case during a destroy + // only plan in which we wanted to both capture the value for an input + // as we still need it, while also noting that everything is being + // destroyed. + return nil, diags + } + + decl := v.config.config + value := v.Value(ctx, ApplyPhase) + if decl.Ephemeral { + value = cty.NullVal(value.Type()) + } + + return []stackstate.AppliedChange{ + &stackstate.AppliedChangeInputVariable{ + Addr: v.addr.Item, + Value: value, + }, + }, diags +} + +func (v *InputVariable) tracingName() string { + return v.addr.String() +} + +// ExternalInputValue represents the value of an input variable provided +// from outside the stack configuration. +type ExternalInputValue struct { + Value cty.Value + DefRange tfdiags.SourceRange +} diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable_config.go b/internal/stacks/stackruntime/internal/stackeval/input_variable_config.go new file mode 100644 index 0000000000..abd823e607 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable_config.go @@ -0,0 +1,163 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// InputVariableConfig represents a "variable" block in a stack configuration. +type InputVariableConfig struct { + addr stackaddrs.ConfigInputVariable + stack *StackConfig + config *stackconfig.InputVariable + + main *Main +} + +var _ Validatable = (*InputVariableConfig)(nil) +var _ Referenceable = (*InputVariableConfig)(nil) + +func newInputVariableConfig(main *Main, addr stackaddrs.ConfigInputVariable, stack *StackConfig, config *stackconfig.InputVariable) *InputVariableConfig { + if config == nil { + panic("newInputVariableConfig with nil configuration") + } + return &InputVariableConfig{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +func (v *InputVariableConfig) tracingName() string { + return v.addr.String() +} + +func (v *InputVariableConfig) TypeConstraint() cty.Type { + return v.config.Type.Constraint +} + +func (v *InputVariableConfig) NestedDefaults() *typeexpr.Defaults { + return v.config.Type.Defaults +} + +// DefaultValue returns the effective default value for this input variable, +// or cty.NilVal if this variable is required. +// +// If the configured default value is invalid, this returns a placeholder +// unknown value of the correct type. Use +// [InputVariableConfig.ValidateDefaultValue] instead if you are intending +// to report configuration diagnostics to the user. +func (v *InputVariableConfig) DefaultValue() cty.Value { + ret, _ := v.ValidateDefaultValue() + return ret +} + +// ValidateDefaultValue verifies that the specified default value is valid +// and then returns the validated value. If the result is cty.NilVal then +// this input variable is required and so has no default value. +// +// If the returned diagnostics has errors then the returned value is a +// placeholder unknown value of the correct type. +func (v *InputVariableConfig) ValidateDefaultValue() (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + val := v.config.DefaultValue + if val == cty.NilVal { + return cty.NilVal, diags + } + want := v.TypeConstraint() + if defs := v.NestedDefaults(); defs != nil { + val = defs.Apply(val) + } + val, err := convert.Convert(val, want) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid default value for input variable", + Detail: fmt.Sprintf("The default value does not conform to the variable's type constraint: %s.", err), + // TODO: Better to indicate the default value itself, but + // stackconfig.InputVariable doesn't currently retain that. + Subject: v.config.DeclRange.ToHCL().Ptr(), + }) + return cty.UnknownVal(want), diags + } + return val, diags +} + +// StackCallConfig returns the stack call that would be providing the value +// for this input variable, or nil if this input variable belongs to the +// main (root) stack and therefore its value would come from outside of +// the configuration. +func (v *InputVariableConfig) StackCallConfig() *StackCallConfig { + if v.stack.parent == nil { + return nil + } + targetStack := v.addr.Stack + parentStack := v.stack.parent + return parentStack.StackCall(stackaddrs.StackCall{Name: targetStack[len(targetStack)-1].Name}) +} + +// ExprReferenceValue implements Referenceable +func (v *InputVariableConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + if v.addr.Stack.IsRoot() { + // During validation the root input variable values are always unknown, + // because validation tests whether the configuration is valid for + // _any_ inputs, rather than for _specific_ inputs. + return v.markValue(cty.UnknownVal(v.TypeConstraint())) + } else { + // Our apparent value is the value assigned in the definition object + // in the parent call. + call := v.StackCallConfig() + val := call.InputVariableValues(ctx, phase)[v.addr.Item] + if val == cty.NilVal { + val = cty.UnknownVal(v.TypeConstraint()) + } + return v.markValue(val) + } +} + +// markValue returns the given value with any additional cty marks that +// ought to be applied to the value of the variable based on its configuration. +func (v *InputVariableConfig) markValue(val cty.Value) cty.Value { + if val == cty.NilVal { + return val + } + if v.config.Sensitive { + val = val.Mark(marks.Sensitive) + } + if v.config.Ephemeral { + val = val.Mark(marks.Ephemeral) + } + return val +} + +func (v *InputVariableConfig) checkValid() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + _, moreDiags := v.ValidateDefaultValue() + diags = diags.Append(moreDiags) + return diags +} + +// Validate implements Validatable +func (v *InputVariableConfig) Validate(context.Context) tfdiags.Diagnostics { + return v.checkValid() +} + +// PlanChanges implements Plannable. +func (v *InputVariableConfig) PlanChanges(context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, v.checkValid() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go b/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go new file mode 100644 index 0000000000..8d604bffbc --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable_test.go @@ -0,0 +1,489 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/encoding/prototext" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" +) + +func TestInputVariableValue(t *testing.T) { + ctx := context.Background() + cfg := testStackConfig(t, "input_variable", "basics") + + // NOTE: This also indirectly tests the propagation of input values + // from a parent stack into one of its children, even though that's + // technically the responsibility of [StackCall] rather than [InputVariable], + // because propagating downward into child stacks is a major purpose + // of input variables that must keep working. + childStackAddr := stackaddrs.RootStackInstance.Child("child", addrs.NoKey) + + tests := map[string]struct { + NameVal cty.Value + WantRootVal cty.Value + WantChildVal cty.Value + + WantRootErr bool + }{ + "known string": { + NameVal: cty.StringVal("jackson"), + WantRootVal: cty.StringVal("jackson"), + WantChildVal: cty.StringVal("child of jackson"), + }, + "unknown string": { + NameVal: cty.UnknownVal(cty.String), + WantRootVal: cty.UnknownVal(cty.String), + WantChildVal: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefix("child of "). + NewValue(), + }, + "unknown of unknown type": { + NameVal: cty.DynamicVal, + WantRootVal: cty.UnknownVal(cty.String), + WantChildVal: cty.UnknownVal(cty.String).Refine(). + NotNull(). + StringPrefix("child of "). + NewValue(), + }, + "bool": { + // This one is testing that the given value gets converted to + // the declared type constraint, which is string in this case. + NameVal: cty.True, + WantRootVal: cty.StringVal("true"), + WantChildVal: cty.StringVal("child of true"), + }, + "object": { + // This one is testing that the given value gets converted to + // the declared type constraint, which is string in this case. + NameVal: cty.EmptyObjectVal, + WantRootErr: true, // Type mismatch error + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + InputVariableValues: map[string]cty.Value{ + "name": test.NameVal, + }, + }) + + t.Run("root", func(t *testing.T) { + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + mainStack := main.MainStack() + rootVar := mainStack.InputVariable(stackaddrs.InputVariable{Name: "name"}) + got, diags := rootVar.CheckValue(ctx, InspectPhase) + + if test.WantRootErr { + if !diags.HasErrors() { + t.Errorf("succeeded; want error\ngot: %#v", got) + } + return struct{}{}, nil + } + + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + want := test.WantRootVal + if !want.RawEquals(got) { + t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want) + } + return struct{}{}, nil + }) + }) + if !test.WantRootErr { + t.Run("child", func(t *testing.T) { + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + childStack := main.Stack(ctx, childStackAddr, InspectPhase) + rootVar := childStack.InputVariable(stackaddrs.InputVariable{Name: "name"}) + got, diags := rootVar.CheckValue(ctx, InspectPhase) + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + want := test.WantChildVal + if !want.RawEquals(got) { + t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want) + } + return struct{}{}, nil + }) + }) + } + }) + } +} + +func TestInputVariableEphemeral(t *testing.T) { + ctx := context.Background() + + tests := map[string]struct { + fixtureName string + givenVal cty.Value + allowed bool + wantInputs cty.Value + wantVal cty.Value + }{ + "ephemeral and allowed": { + fixtureName: "ephemeral_yes", + givenVal: cty.StringVal("beep").Mark(marks.Ephemeral), + allowed: true, + wantInputs: cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("beep").Mark(marks.Ephemeral), + }), + wantVal: cty.StringVal("beep").Mark(marks.Ephemeral), + }, + "ephemeral and not allowed": { + fixtureName: "ephemeral_no", + givenVal: cty.StringVal("beep").Mark(marks.Ephemeral), + allowed: false, + wantInputs: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "a": cty.String, + })), + wantVal: cty.UnknownVal(cty.String), + }, + "non-ephemeral and allowed": { + fixtureName: "ephemeral_yes", + givenVal: cty.StringVal("beep"), + allowed: true, + wantInputs: cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), // not marked on the input side... + }), + wantVal: cty.StringVal("beep").Mark(marks.Ephemeral), // ...but marked on the result side + }, + "non-ephemeral and not allowed": { + fixtureName: "ephemeral_no", + givenVal: cty.StringVal("beep"), + allowed: true, + wantInputs: cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("beep"), + }), + wantVal: cty.StringVal("beep"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + cfg := testStackConfig(t, "input_variable", test.fixtureName) + childStackAddr := stackaddrs.RootStackInstance.Child("child", addrs.NoKey) + childStackCallAddr := stackaddrs.StackCall{Name: "child"} + aVarAddr := stackaddrs.InputVariable{Name: "a"} + + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "var_val": test.givenVal, + }, + }) + + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + childStack := main.Stack(ctx, childStackAddr, InspectPhase) + if childStack == nil { + t.Fatalf("missing %s", childStackAddr) + } + childStackCall := main.MainStack().EmbeddedStackCall(childStackCallAddr) + if childStackCall == nil { + t.Fatalf("missing %s", childStackCallAddr) + } + insts, unknown := childStackCall.Instances(ctx, InspectPhase) + if unknown { + t.Fatalf("stack call instances are unknown") + } + childStackCallInst := insts[addrs.NoKey] + if childStackCallInst == nil { + t.Fatalf("missing %s instance", childStackCallAddr) + } + + // The responsibility for handling ephemeral input variables + // is split between the stack call which decides whether an + // ephemeral value is acceptable, and the variable declaration + // itself which ensures that variables declared as ephemeral + // always appear as ephemeral inside even if the given value + // wasn't. + + wantInputs := test.wantInputs + gotInputs, diags := childStackCallInst.CheckInputVariableValues(ctx, InspectPhase) + if diff := cmp.Diff(wantInputs, gotInputs, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong inputs for %s\n%s", childStackCallAddr, diff) + } + + aVar := childStack.InputVariable(aVarAddr) + if aVar == nil { + t.Fatalf("missing %s", stackaddrs.Absolute(childStackAddr, aVarAddr)) + } + want := test.wantVal + got, moreDiags := aVar.CheckValue(ctx, InspectPhase) + diags = diags.Append(moreDiags) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong value for %s\n%s", aVarAddr, diff) + } + + if test.allowed { + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + } else { + if !diags.HasErrors() { + t.Fatalf("no errors; should have failed") + } + found := 0 + for _, diag := range diags { + summary := diag.Description().Summary + if summary == "Ephemeral value not allowed" { + found++ + } + } + if found == 0 { + t.Errorf("no diagnostics about disallowed ephemeral values\n%s", diags.Err().Error()) + } else if found > 1 { + t.Errorf("found %d errors about disallowed ephemeral values, but wanted only one\n%s", found, diags.Err().Error()) + } + } + return struct{}{}, nil + }) + }) + } +} + +func TestInputVariablePlanApply(t *testing.T) { + ctx := context.Background() + cfg := testStackConfig(t, "input_variable", "basics") + + tests := map[string]struct { + PlanVal cty.Value + ApplyVal cty.Value + WantErr bool + }{ + "unmarked": { + PlanVal: cty.StringVal("alisdair"), + ApplyVal: cty.StringVal("alisdair"), + }, + "sensitive": { + PlanVal: cty.StringVal("alisdair").Mark(marks.Sensitive), + ApplyVal: cty.StringVal("alisdair").Mark(marks.Sensitive), + }, + "changed": { + PlanVal: cty.StringVal("alice"), + ApplyVal: cty.StringVal("bob"), + WantErr: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + planOutput, err := promising.MainTask(ctx, func(ctx context.Context) (*planOutputTester, error) { + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + PlanTimestamp: time.Now().UTC(), + InputVariableValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "name"}: { + Value: test.PlanVal, + }, + }, + }) + + outp, outpTester := testPlanOutput(t) + main.PlanAll(ctx, outp) + + return outpTester, nil + }) + if err != nil { + t.Fatalf("planning failed: %s", err) + } + + rawPlan := planOutput.RawChanges(t) + plan, diags := planOutput.Close(t) + assertNoDiagnostics(t, diags) + + if !plan.Applyable { + m := prototext.MarshalOptions{ + Multiline: true, + Indent: " ", + } + for _, raw := range rawPlan { + t.Log(m.Format(raw)) + } + t.Fatalf("plan is not applyable") + } + + _, err = promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + main := NewForApplying(cfg, plan, nil, ApplyOpts{ + InputVariableValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "name"}: { + Value: test.ApplyVal, + }, + }, + }) + mainStack := main.MainStack() + rootVar := mainStack.InputVariable(stackaddrs.InputVariable{Name: "name"}) + got, diags := rootVar.CheckValue(ctx, ApplyPhase) + + if test.WantErr { + if !diags.HasErrors() { + t.Errorf("succeeded; want error\ngot: %#v", got) + } + return struct{}{}, nil + } + + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + want := test.ApplyVal + if !want.RawEquals(got) { + t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want) + } + + return struct{}{}, nil + }) + if err != nil { + t.Fatal(err) + } + }) + } +} + +func TestInputVariablePlanChanges(t *testing.T) { + ctx := context.Background() + cfg := testStackConfig(t, "input_variable", "basics") + + tests := map[string]struct { + PlanVal cty.Value + PreviousPlanVal cty.Value + WantPlannedChanges []stackplan.PlannedChange + }{ + "unmarked": { + PlanVal: cty.StringVal("value_1"), + PreviousPlanVal: cty.NullVal(cty.String), + WantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "name"}, + Action: plans.Update, + Before: cty.NullVal(cty.String), + After: cty.StringVal("value_1"), + RequiredOnApply: false, + DeleteOnApply: false, + }, + }, + }, + "sensitive": { + PlanVal: cty.StringVal("value_2").Mark(marks.Sensitive), + PreviousPlanVal: cty.NullVal(cty.String), + WantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "name"}, + Action: plans.Update, + Before: cty.NullVal(cty.String), + After: cty.StringVal("value_2").Mark(marks.Sensitive), + RequiredOnApply: false, + DeleteOnApply: false, + }, + }, + }, + "ephemeral": { + PlanVal: cty.StringVal("value_3").Mark(marks.Ephemeral), + PreviousPlanVal: cty.NullVal(cty.String), + WantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "name"}, + Action: plans.Update, + Before: cty.NullVal(cty.String), + After: cty.StringVal("value_3").Mark(marks.Ephemeral), + RequiredOnApply: false, + DeleteOnApply: false, + }, + }, + }, + "sensitive_and_ephemeral": { + PlanVal: cty.StringVal("value_4").Mark(marks.Ephemeral).Mark(marks.Sensitive), + PreviousPlanVal: cty.NullVal(cty.String), + WantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "name"}, + Action: plans.Update, + Before: cty.NullVal(cty.String), + After: cty.StringVal("value_4").Mark(marks.Ephemeral).Mark(marks.Sensitive), + RequiredOnApply: false, + DeleteOnApply: false, + }, + }, + }, + "from_non_null_to_sensitive": { + PlanVal: cty.StringVal("value_2").Mark(marks.Sensitive), + PreviousPlanVal: cty.StringVal("value_1"), + WantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "name"}, + Action: plans.Update, + Before: cty.StringVal("value_1"), + After: cty.StringVal("value_2").Mark(marks.Sensitive), + RequiredOnApply: false, + DeleteOnApply: false, + }, + }, + }, + "from_ephemeral_to_unmark": { + PlanVal: cty.StringVal("value_2"), + PreviousPlanVal: cty.StringVal("value_1").Mark(marks.Ephemeral), + WantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "name"}, + Action: plans.Update, + Before: cty.StringVal("value_1").Mark(marks.Ephemeral), + After: cty.StringVal("value_2"), + RequiredOnApply: false, + DeleteOnApply: false, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + _, err := promising.MainTask(ctx, func(ctx context.Context) (*planOutputTester, error) { + previousState := stackstate.NewStateBuilder().AddInput("name", test.PreviousPlanVal).Build() + + main := NewForPlanning(cfg, previousState, PlanOpts{ + PlanningMode: plans.NormalMode, + PlanTimestamp: time.Now().UTC(), + InputVariableValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "name"}: { + Value: test.PlanVal, + }, + }, + }) + + mainStack := main.MainStack() + rootVar := mainStack.InputVariable(stackaddrs.InputVariable{Name: "name"}) + got, diags := rootVar.PlanChanges(ctx) + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + + opts := cmp.Options{ctydebug.CmpOptions} + if diff := cmp.Diff(test.WantPlannedChanges, got, opts); len(diff) > 0 { + t.Errorf("wrong planned changes\n%s", diff) + } + + return nil, nil + }) + if err != nil { + t.Fatalf("planning failed: %s", err) + } + }) + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/local_value.go b/internal/stacks/stackruntime/internal/stackeval/local_value.go new file mode 100644 index 0000000000..5c175be7c1 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/local_value.go @@ -0,0 +1,98 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// LocalValue represents a local value defined within a [Stack]. +type LocalValue struct { + addr stackaddrs.AbsLocalValue + config *LocalValueConfig + stack *Stack + + main *Main + + value perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ Referenceable = (*LocalValue)(nil) +var _ Plannable = (*LocalValue)(nil) + +func newLocalValue(main *Main, addr stackaddrs.AbsLocalValue, stack *Stack, config *LocalValueConfig) *LocalValue { + return &LocalValue{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +func (v *LocalValue) Value(ctx context.Context, phase EvalPhase) cty.Value { + val, _ := v.CheckValue(ctx, phase) + return val +} + +// ExprReferenceValue implements Referenceable. +func (v *LocalValue) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + return v.Value(ctx, phase) +} + +func (v *LocalValue) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, moreDiags := v.CheckValue(ctx, phase) + diags = diags.Append(moreDiags) + + return diags +} + +func (v *LocalValue) CheckValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return withCtyDynamicValPlaceholder(doOnceWithDiags( + ctx, v.tracingName(), v.value.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + decl := v.config.config + result, moreDiags := EvalExprAndEvalContext(ctx, decl.Value, phase, v.stack) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.DynamicVal, diags + } + + return result.Value, diags + }, + )) +} + +// PlanChanges implements Plannable as a plan-time validation of the local value +func (v *LocalValue) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, v.checkValid(ctx, PlanPhase) +} + +// References implements Referrer +func (v *LocalValue) References(context.Context) []stackaddrs.AbsReference { + cfg := v.config.config + var ret []stackaddrs.Reference + ret = append(ret, ReferencesInExpr(cfg.Value)...) + return makeReferencesAbsolute(ret, v.addr.Stack) +} + +// CheckApply implements Applyable. +func (v *LocalValue) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + return nil, v.checkValid(ctx, ApplyPhase) +} + +func (v *LocalValue) tracingName() string { + return v.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/local_value_config.go b/internal/stacks/stackruntime/internal/stackeval/local_value_config.go new file mode 100644 index 0000000000..7aa73803b8 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/local_value_config.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// LocalValueConfig represents a "locals" block in a stack configuration. +type LocalValueConfig struct { + addr stackaddrs.ConfigLocalValue + config *stackconfig.LocalValue + stack *StackConfig + + main *Main + + validatedValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var ( + _ Validatable = (*LocalValueConfig)(nil) + _ Referenceable = (*LocalValueConfig)(nil) +) + +func newLocalValueConfig(main *Main, addr stackaddrs.ConfigLocalValue, stack *StackConfig, config *stackconfig.LocalValue) *LocalValueConfig { + if config == nil { + panic("newLocalValueConfig with nil configuration") + } + return &LocalValueConfig{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +func (v *LocalValueConfig) tracingName() string { + return v.addr.String() +} + +// ExprReferenceValue implements Referenceable +func (v *LocalValueConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + out, _ := v.ValidateValue(ctx, phase) + return out +} + +// ValidateValue validates that the value expression is evaluatable and that +// its result can convert to the declared type constraint, returning the +// resulting value. +// +// If the returned diagnostics has errors then the returned value might be +// just an approximation of the result, such as an unknown value with the +// declared type constraint. +func (v *LocalValueConfig) ValidateValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return withCtyDynamicValPlaceholder(doOnceWithDiags( + ctx, v.tracingName(), v.validatedValue.For(phase), + v.validateValueInner(phase), + )) +} + +// validateValueInner is the real implementation of ValidateValue, which runs +// in the background only once per instance of [OutputValueConfig] and then +// provides the result for all ValidateValue callers simultaneously. +func (v *LocalValueConfig) validateValueInner(phase EvalPhase) func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + return func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + result, moreDiags := EvalExprAndEvalContext(ctx, v.config.Value, phase, v.stack) + value := result.Value + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + value = cty.UnknownVal(cty.DynamicPseudoType) + } + + return value, diags + } +} + +func (v *LocalValueConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + _, moreDiags := v.ValidateValue(ctx, phase) + diags = diags.Append(moreDiags) + return diags +} + +// Validate implements Validatable +func (v *LocalValueConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + return v.checkValid(ctx, ValidatePhase) +} + +// PlanChanges implements Plannable. +func (v *LocalValueConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, v.checkValid(ctx, PlanPhase) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/local_value_test.go b/internal/stacks/stackruntime/internal/stackeval/local_value_test.go new file mode 100644 index 0000000000..8ab15bbfe6 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/local_value_test.go @@ -0,0 +1,82 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/zclconf/go-cty/cty" +) + +func TestLocalValueValue(t *testing.T) { + ctx := context.Background() + cfg := testStackConfig(t, "local_value", "basics") + + tests := map[string]struct { + LocalName string + WantVal cty.Value + }{ + "name": { + LocalName: "name", + WantVal: cty.StringVal("jackson"), + }, + "childName": { + LocalName: "childName", + WantVal: cty.StringVal("outputted-child of jackson"), + }, + "functional": { + LocalName: "functional", + WantVal: cty.StringVal("Hello, Ander!"), + }, + "mappy": { + LocalName: "mappy", + WantVal: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("jackson"), + "age": cty.NumberIntVal(30), + }), + }, + "listy": { + LocalName: "listy", + WantVal: cty.TupleVal([]cty.Value{ + cty.StringVal("jackson"), + cty.NumberIntVal(30), + }), + }, + "booleany": { + LocalName: "booleany", + WantVal: cty.BoolVal(true), + }, + "conditiony": { + LocalName: "conditiony", + WantVal: cty.StringVal("true"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + }) + + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + mainStack := main.MainStack() + rootVal := mainStack.LocalValue(stackaddrs.LocalValue{Name: test.LocalName}) + got, diags := rootVal.CheckValue(ctx, InspectPhase) + + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + + if got.Equals(test.WantVal).False() { + t.Errorf("got %s, want %s", got, test.WantVal) + } + + return struct{}{}, nil + }) + }) + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main.go b/internal/stacks/stackruntime/internal/stackeval/main.go new file mode 100644 index 0000000000..017403a94c --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/main.go @@ -0,0 +1,641 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "log" + "sync" + "time" + + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + + "github.com/hashicorp/terraform/internal/addrs" + fileProvisioner "github.com/hashicorp/terraform/internal/builtin/provisioners/file" + remoteExecProvisioner "github.com/hashicorp/terraform/internal/builtin/provisioners/remote-exec" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/provisioners" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval/stubs" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Main is the central node of all data required for performing the major +// actions against a stack: validation, planning, and applying. +// +// This type delegates to various other types in this package to implement +// the real logic, with Main focused on enabling the collaboration between +// objects of those other types. +type Main struct { + config *stackconfig.Config + + // validating captures the data needed when validating configuration, + // in which case we consider _only_ the configuration and don't take + // into account any existing state or specific input variable values. + validating *mainValidating + + // planning captures the data needed when creating or applying a plan, + // but which need not be populated when only using the validation-related + // functionality of this package. + planning *mainPlanning + + // applying captures the data needed when applying a plan. This can + // only be populated if "planning" is also populated, because the process + // of applying includes re-evaluation of plans based on new information + // gathered during the apply process. + applying *mainApplying + + // inspecting captures the data needed when operating in the special + // "inspect" mode, which doesn't plan or apply but can still evaluate + // expressions and inspect the current configuration/state. + inspecting *mainInspecting + + // providerFactories is a set of callback functions through which the + // runtime can obtain new instances of each of the available providers. + providerFactories ProviderFactories + + // testOnlyGlobals is a unit testing affordance: if non-nil, expressions + // shaped like _test_only_global.name (with the leading underscore) + // become valid throughout the stack configuration and evaluate to the + // correspondingly-named values here. + // + // This must never be used outside of test code in this package. + testOnlyGlobals map[string]cty.Value + + // languageExperimentsAllowed gets set if our caller enables the use + // of language experiments by calling [Main.AllowLanguageExperiments] + // shortly after creating this object. + languageExperimentsAllowed bool + + // The remaining fields memoize other objects we might create in response + // to method calls. Must lock "mu" before interacting with them. + mu sync.Mutex + mainStackConfig *StackConfig + mainStack *Stack + providerTypes map[addrs.Provider]*ProviderType + providerFunctionResults *lang.FunctionResults + cleanupFuncs []func(context.Context) tfdiags.Diagnostics +} + +type mainValidating struct { + opts ValidateOpts +} + +type mainPlanning struct { + opts PlanOpts + prevState *stackstate.State +} + +type mainApplying struct { + opts ApplyOpts + plan *stackplan.Plan + results *ChangeExecResults +} + +type mainInspecting struct { + opts InspectOpts + state *stackstate.State +} + +func NewForValidating(config *stackconfig.Config, opts ValidateOpts) *Main { + return &Main{ + config: config, + validating: &mainValidating{ + opts: opts, + }, + providerFactories: opts.ProviderFactories, + providerTypes: make(map[addrs.Provider]*ProviderType), + providerFunctionResults: lang.NewFunctionResultsTable(nil), + } +} + +func NewForPlanning(config *stackconfig.Config, prevState *stackstate.State, opts PlanOpts) *Main { + if prevState == nil { + // We'll make an empty state just to avoid other code needing to deal + // with this possibly being nil. + prevState = stackstate.NewState() + } + return &Main{ + config: config, + planning: &mainPlanning{ + opts: opts, + prevState: prevState, + }, + providerFactories: opts.ProviderFactories, + providerTypes: make(map[addrs.Provider]*ProviderType), + providerFunctionResults: lang.NewFunctionResultsTable(nil), + } +} + +func NewForApplying(config *stackconfig.Config, plan *stackplan.Plan, execResults *ChangeExecResults, opts ApplyOpts) *Main { + return &Main{ + config: config, + applying: &mainApplying{ + opts: opts, + plan: plan, + results: execResults, + }, + providerFactories: opts.ProviderFactories, + providerTypes: make(map[addrs.Provider]*ProviderType), + providerFunctionResults: lang.NewFunctionResultsTable(plan.FunctionResults), + } +} + +func NewForInspecting(config *stackconfig.Config, state *stackstate.State, opts InspectOpts) *Main { + return &Main{ + config: config, + inspecting: &mainInspecting{ + state: state, + opts: opts, + }, + providerFactories: opts.ProviderFactories, + providerTypes: make(map[addrs.Provider]*ProviderType), + providerFunctionResults: lang.NewFunctionResultsTable(nil), + testOnlyGlobals: opts.TestOnlyGlobals, + } +} + +// AllowLanguageExperiments changes the flag for whether language experiments +// are allowed during evaluation. +// +// Call this very shortly after creating a [Main], before performing any other +// actions on it. Changing this setting after other methods have been called +// will produce unpredictable results. +func (m *Main) AllowLanguageExperiments(allow bool) { + m.languageExperimentsAllowed = allow +} + +// LanguageExperimentsAllowed returns true if language experiments are allowed +// to be used during evaluation. +func (m *Main) LanguageExperimentsAllowed() bool { + return m.languageExperimentsAllowed +} + +// Validating returns true if the receiving [Main] is configured for validating. +// +// If this returns false then validation methods may panic or return strange +// results. +func (m *Main) Validating() bool { + return m.validating != nil +} + +// Planning returns true if the receiving [Main] is configured for planning. +// +// If this returns false then planning methods may panic or return strange +// results. +func (m *Main) Planning() bool { + return m.planning != nil +} + +// Applying returns true if the receiving [Main] is configured for applying. +// +// If this returns false then applying methods may panic or return strange +// results. +func (m *Main) Applying() bool { + return m.applying != nil +} + +// Inspecting returns true if the receiving [Main] is configured for inspecting. +// +// If this returns false then expression evaluation in [InspectPhase] is +// likely to panic or return strange results. +func (m *Main) Inspecting() bool { + return m.inspecting != nil +} + +// ValidatingOpts returns the validation options to use during the validate phase, +// or panics if this [Main] was not instantiated for validation. +// +// Do not modify anything reachable through the returned pointer. +func (m *Main) ValidatingOpts() *ValidateOpts { + if !m.Validating() { + panic("stacks language runtime is not instantiated for validating") + } + return &m.validating.opts +} + +// PlanningOpts returns the planning options to use during the planning phase, +// or panics if this [Main] was not instantiated for planning. +// +// Do not modify anything reachable through the returned pointer. +func (m *Main) PlanningOpts() *PlanOpts { + if !m.Planning() { + panic("stacks language runtime is not instantiated for planning") + } + return &m.planning.opts +} + +// PlanPrevState returns the "previous state" object to use as the basis for +// planning, or panics if this [Main] is not instantiated for planning. +func (m *Main) PlanPrevState() *stackstate.State { + if !m.Planning() { + panic("previous state is only available in the planning phase") + } + return m.planning.prevState +} + +// ApplyChangeResults returns the object that tracks the results of the actual +// changes being made during the apply phase, or panics if this [Main] is not +// instantiated for applying. +func (m *Main) ApplyChangeResults() *ChangeExecResults { + if !m.Applying() { + panic("stacks language runtime is not instantiated for applying") + } + if m.applying.results == nil { + panic("stacks language runtime is instantiated for applying but somehow has no change results") + } + return m.applying.results +} + +// PlanBeingApplied returns the plan that's currently being applied, or panics +// if called not during an apply phase. +func (m *Main) PlanBeingApplied() *stackplan.Plan { + if !m.Applying() { + panic("stacks language runtime is not instantiated for applying") + } + return m.applying.plan +} + +// InspectingState returns the state snapshot that was provided when +// instantiating [Main] "for inspecting", or panics if this object was not +// instantiated in that mode. +func (m *Main) InspectingState() *stackstate.State { + if !m.Inspecting() { + panic("stacks language runtime is not instantiated for inspecting") + } + return m.inspecting.state +} + +// SourceBundle returns the source code bundle that the stack configuration +// was originally loaded from and that should also contain the source code +// for any modules that "component" blocks refer to. +func (m *Main) SourceBundle() *sourcebundle.Bundle { + return m.config.Sources +} + +// MainStackConfig returns the [StackConfig] object representing the main +// stack configuration, which is at the root of the configuration tree. +// +// This represents the static configuration. The main stack configuration +// always has exactly one "dynamic" instance, which you can access by +// calling [Main.MainStack] instead. The static configuration object is used +// for validation, but plan and apply both use the stack instance. +func (m *Main) MainStackConfig() *StackConfig { + m.mu.Lock() + defer m.mu.Unlock() + + if m.mainStackConfig == nil { + m.mainStackConfig = newStackConfig(m, stackaddrs.RootStack, nil, m.config.Root) + } + return m.mainStackConfig +} + +// MainStack returns the [Stack] object representing the main stack, which +// is the root of the configuration tree. +func (m *Main) MainStack() *Stack { + config := m.MainStackConfig() // fetch the main stack config + + m.mu.Lock() + defer m.mu.Unlock() + + if m.mainStack == nil { + m.mainStack = newStack(m, stackaddrs.RootStackInstance, nil, config, newRemoved(), m.PlanningMode(), false) + } + return m.mainStack +} + +// Stack returns the [Stack] object representing the stack instance with the +// given address, or nil if we know for certain that such a stack instance +// is not declared in the configuration. +// +// This is like [Main.StackUnchecked] but additionally checks whether all of +// the instance keys in the stack instance path correspond to instances declared +// by for_each arguments (or lack thereof) in the configuration. This involves +// evaluating all of the "for_each" expressions, and so will block on whatever +// those expressions depend on. +// +// If any of the stack calls along the path have an as-yet-unknown set of +// instances, this function will optimistically return a non-nil stack but +// further operations with that stack are likely to return unknown values +// themselves. +// +// If you know you are holding a [stackaddrs.StackInstance] that was from +// a valid [Stack] previously returned (directly or indirectly) then you can +// avoid the additional overhead by using [Main.StackUnchecked] instead. +func (m *Main) Stack(ctx context.Context, addr stackaddrs.StackInstance, phase EvalPhase) *Stack { + ret := m.MainStack() + for _, step := range addr { + ret = ret.ChildStack(ctx, step, phase) + if ret == nil { + return nil + } + } + return ret +} + +// ProviderFactories returns the collection of factory functions for providers +// that are available to this instance of the evaluation runtime. +func (m *Main) ProviderFactories() ProviderFactories { + return m.providerFactories +} + +// ProviderFunctions returns the collection of externally defined provider +// functions available to the current stack. +func (m *Main) ProviderFunctions(ctx context.Context, config *StackConfig) (lang.ExternalFuncs, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + fns := make(map[string]map[string]function.Function, len(m.providerFactories)) + + for addr := range m.providerFactories { + provider := m.ProviderType(addr) + + local, ok := config.ProviderLocalName(addr) + if !ok { + log.Printf("[ERROR] Provider %s is not in the required providers block", addr) + // This also shouldn't happen, as every provider should be + // in the required providers block and that should have been + // validated - but we can recover from this by just using the + // default local name. + local = addr.Type + } + + schema, err := provider.Schema(ctx) + if err != nil { + // We should have started these providers before we got here, so + // this error shouldn't ever occur. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to retrieve provider schema", + Detail: fmt.Sprintf("Failed to retrieve schema for provider %s while gathering provider functions: %s. This is a bug in Terraform, please report it!", addr, err), + }) + continue // just skip this provider and keep going + } + + // Now we can build the functions for this provider. + fns[local] = make(map[string]function.Function, len(schema.Functions)) + for name, fn := range schema.Functions { + fns[local][name] = fn.BuildFunction(addr, name, m.providerFunctionResults, func() (providers.Interface, error) { + client, err := provider.UnconfiguredClient() + if err != nil { + return nil, err + } + return stubs.OfflineProvider(client), nil + }) + } + } + + return lang.ExternalFuncs{Provider: fns}, diags + +} + +// ProviderType returns the [ProviderType] object representing the given +// provider source address. +// +// This does not check whether the given provider type is available in the +// current evaluation context, but attempting to create a client for a +// provider that isn't available will return an error at startup time. +func (m *Main) ProviderType(addr addrs.Provider) *ProviderType { + m.mu.Lock() + defer m.mu.Unlock() + + if m.providerTypes[addr] == nil { + m.providerTypes[addr] = newProviderType(m, addr) + } + return m.providerTypes[addr] +} + +func (m *Main) ProviderRefTypes() map[addrs.Provider]cty.Type { + return m.config.ProviderRefTypes +} + +// PreviousProviderInstances fetches the set of providers that are required +// based on the current plan or state file. They are previous in the sense that +// they're not based on the current config. So if a provider has been removed +// from the config, this function will still find it. +func (m *Main) PreviousProviderInstances(addr stackaddrs.AbsComponentInstance, phase EvalPhase) addrs.Set[addrs.RootProviderConfig] { + switch phase { + case ApplyPhase: + return m.PlanBeingApplied().RequiredProviderInstances(addr) + case PlanPhase: + return m.PlanPrevState().RequiredProviderInstances(addr) + case InspectPhase: + return m.InspectingState().RequiredProviderInstances(addr) + default: + // We don't have the required information (like a plan or a state file) + // in the other phases so we can't do anything even if we wanted to. + // In general, for the other phases we're not doing anything with the + // previous provider instances anyway, so we don't need them. + return addrs.MakeSet[addrs.RootProviderConfig]() + } +} + +// RootVariableValue returns the original root variable value specified by the +// caller, if any. The caller of this function is responsible for replacing +// missing values with defaults, and performing type conversion and and +// validation. +func (m *Main) RootVariableValue(addr stackaddrs.InputVariable, phase EvalPhase) ExternalInputValue { + switch phase { + case PlanPhase: + if !m.Planning() { + panic("using PlanPhase input variable values when not configured for planning") + } + ret, ok := m.planning.opts.InputVariableValues[addr] + if !ok { + // If no value is specified for the given input variable, we return + // a null value. Callers should treat a null value as equivalent to + // an unspecified one, applying default (if present) or raising an + // error (if not). + return ExternalInputValue{ + Value: cty.NullVal(cty.DynamicPseudoType), + } + } + return ret + + case ApplyPhase: + if !m.Applying() { + panic("using ApplyPhase input variable values when not configured for applying") + } + + // First, check the values given to use directly by the caller. + if ret, ok := m.applying.opts.InputVariableValues[addr]; ok { + return ret + } + + // If the caller didn't provide a value, we need to look up the value + // that was used during planning. + + if ret, ok := m.applying.plan.RootInputValues[addr]; ok { + return ExternalInputValue{ + Value: ret, + } + } + + // If we had nothing set, we'll return a null value. This means the + // default value will be applied, if any, or an error will be raised + // if no default is available. This should only be possible for an + // ephemeral value in which the caller didn't provide a value during + // the apply operation. + return ExternalInputValue{ + Value: cty.NullVal(cty.DynamicPseudoType), + } + + case InspectPhase: + if !m.Inspecting() { + panic("using InspectPhase input variable values when not configured for inspecting") + } + ret, ok := m.inspecting.opts.InputVariableValues[addr] + if !ok { + return ExternalInputValue{ + // We use the generic "unknown value of unknown type" + // placeholder here because this method provides the external + // view of the input variables, and so we expect internal + // access points like methods of [InputVariable] to convert + // this result into the appropriate type constraint themselves. + Value: cty.DynamicVal, + } + } + return ret + + default: + // Root input variable values are not available in any other phase. + + return ExternalInputValue{ + Value: cty.DynamicVal, // placeholder value + } + } +} + +// ResolveAbsExpressionReference tries to resolve the given absolute +// expression reference within this evaluation context. +func (m *Main) ResolveAbsExpressionReference(ctx context.Context, ref stackaddrs.AbsReference, phase EvalPhase) (Referenceable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + stack := m.Stack(ctx, ref.Stack, phase) + if stack == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared stack", + Detail: fmt.Sprintf("Cannot resolve reference to object in undeclared stack %s.", ref.Stack), + Subject: ref.SourceRange().ToHCL().Ptr(), + }) + return nil, diags + } + return stack.ResolveExpressionReference(ctx, ref.Ref) +} + +// RegisterCleanup registers an arbitrary callback function to run when a +// walk driver eventually calls [Main.RunCleanup] on the same receiver. +// +// This is intended for cleaning up any resources that would not naturally +// be cleaned up as a result of garbage-collecting the [Main] object and its +// many descendants. +// +// The context passed to a callback function may be already cancelled by the +// time the callback is running, if the cleanup is running in response to +// cancellation. +func (m *Main) RegisterCleanup(cb func(ctx context.Context) tfdiags.Diagnostics) { + m.mu.Lock() + m.cleanupFuncs = append(m.cleanupFuncs, cb) + m.mu.Unlock() +} + +// DoCleanup executes any cleanup functions previously registered using +// [Main.RegisterCleanup], returning any collected diagnostics. +// +// Call this only once evaluation has completed and there aren't any requests +// outstanding that might be using resources that this will free. After calling +// this, the [Main] and all other objects created through it become invalid +// and must not be used anymore. +func (m *Main) DoCleanup(ctx context.Context) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + m.mu.Lock() + funcs := m.cleanupFuncs + m.cleanupFuncs = nil + m.mu.Unlock() + for _, cb := range funcs { + diags = diags.Append( + cb(ctx), + ) + } + return diags +} + +// availableProvisioners returns the table of provisioner factories that should +// be made available to modules in this component. +func (m *Main) availableProvisioners() map[string]provisioners.Factory { + return map[string]provisioners.Factory{ + "remote-exec": func() (provisioners.Interface, error) { + return remoteExecProvisioner.New(), nil + }, + "file": func() (provisioners.Interface, error) { + return fileProvisioner.New(), nil + }, + "local-exec": func() (provisioners.Interface, error) { + // We don't yet have any way to ensure a consistent execution + // environment for local-exec, which means that use of this + // provisioner is very likely to hurt portability between + // local and remote usage of stacks. Existing use of local-exec + // also tends to assume a writable module directory, whereas + // stack components execute from a read-only directory. + // + // Therefore we'll leave this unavailable for now with an explicit + // error message, although we might revisit this later if there's + // a strong reason to allow it and if we can find a suitable + // way to avoid the portability pitfalls that might inhibit + // moving execution of a stack from one execution environment to + // another. + return nil, fmt.Errorf("local-exec provisioners are not supported in stack components; use provider functionality or remote provisioners instead") + }, + } +} + +// PlanTimestamp provides the timestamp at which the plan +// associated with this operation is being executed. +// If we are planning we either take the forced timestamp or the saved current time +// If we are applying we take the timestamp time from the plan +func (m *Main) PlanTimestamp() time.Time { + if m.applying != nil { + return m.applying.plan.PlanTimestamp + } + if m.planning != nil { + return m.planning.opts.PlanTimestamp + } + + // This is the default case, we are not planning / applying + return time.Now().UTC() +} + +func (m *Main) PlanningMode() plans.Mode { + if m.applying != nil { + return m.applying.plan.Mode + } + if m.planning != nil { + return m.planning.opts.PlanningMode + } + return plans.NormalMode +} + +// DependencyLocks returns the dependency locks for the given phase. +func (m *Main) DependencyLocks(phase EvalPhase) *depsfile.Locks { + switch phase { + case ValidatePhase: + return &m.ValidatingOpts().DependencyLocks + case PlanPhase: + return &m.PlanningOpts().DependencyLocks + case ApplyPhase: + return &m.applying.opts.DependencyLocks + default: + return nil + + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main_apply.go b/internal/stacks/stackruntime/internal/stackeval/main_apply.go new file mode 100644 index 0000000000..b3e9019068 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/main_apply.go @@ -0,0 +1,457 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "log" + "sync/atomic" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ApplyPlan internally instantiates a [Main] configured to apply the given +// raw plan, and then visits all of the relevant objects to collect up any +// diagnostics they emit while evaluating in terms of the change results. +// +// If the error result is non-nil then that means the apply process didn't +// even begin, because the given arguments were invalid. If the arguments +// are valid enough to start the apply process then the error will always +// be nil and any problems along the way will be reported as diagnostics +// through the [ApplyOutput] object. +// +// Returns the [Main] object that was used to track state during the process. +// Callers must call [Main.DoCleanup] on that object once they've finished +// with it to avoid leaking non-memory resources such as goroutines and +// provider plugin processes. +func ApplyPlan(ctx context.Context, config *stackconfig.Config, plan *stackplan.Plan, opts ApplyOpts, outp ApplyOutput) (*Main, error) { + if !plan.Applyable { + // We should not get here because a caller should not ask us to try + // to apply a plan that wasn't marked as applyable, but we'll check + // it anyway just to be robust in case there's a bug further up + // the call stack. + return nil, fmt.Errorf("plan is not applyable") + } + + // We might need to discard some of the keys from the previous run state -- + // either in the raw state or in the state description -- if they are + // unrecognized keys classified as needing to be discarded when unrecognized. + discardRawKeys, discardDescKeys, err := stateKeysToDiscard(plan.PrevRunStateRaw, opts.PrevStateDescKeys) + if err != nil { + return nil, fmt.Errorf("invalid previous run state: %w", err) + } + + // -------------------------------------------------------------------- + // NO ERROR RETURNS AFTER THIS POINT! + // From here on we're actually executing the operation, so any problems + // must be reported as diagnostics through outp. + // -------------------------------------------------------------------- + + hooks := hooksFromContext(ctx) + hs, ctx := hookBegin(ctx, hooks.BeginApply, hooks.ContextAttach, struct{}{}) + defer hookMore(ctx, hs, hooks.EndApply, struct{}{}) + + // Before doing anything else we'll emit zero or more events to deal + // with discarding the previous run state data that's no longer needed. + emitStateKeyDiscardEvents(ctx, discardRawKeys, discardDescKeys, outp) + + log.Printf("[TRACE] stackeval.ApplyPlan starting") + withDiags, err := promising.MainTask(ctx, func(ctx context.Context) (withDiagnostics[*Main], error) { + // We'll register all of the changes we intend to make up front, so we + // can error rather than deadlock if something goes wrong and causes + // us to try to depend on a result that isn't coming. + results, begin := ChangeExec(ctx, func(ctx context.Context, reg *ChangeExecRegistry[*Main]) { + for key, elem := range plan.AllComponents() { + addr := key + componentInstPlan := elem + action := componentInstPlan.PlannedAction + dependencyAddrs := componentInstPlan.Dependencies + dependentAddrs := componentInstPlan.Dependents + + reg.RegisterComponentInstanceChange( + ctx, addr, + func(ctx context.Context, main *Main) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + ctx, span := tracer.Start(ctx, addr.String()+" apply") + defer span.End() + log.Printf("[TRACE] stackeval: %s preparing to apply", addr) + + stack := main.Stack(ctx, addr.Stack, ApplyPhase) + component, removed := stack.ApplyableComponents(addr.Item.Component) + + // A component change can be sourced from a removed + // block or a component block. We'll try to find the + // instance that we need to use to apply these changes. + + var inst ApplyableComponentInstance + + matchedUnknownBlock := false + + Blocks: + for _, block := range removed { + if insts, unknown := block.InstancesFor(ctx, stack.addr, ApplyPhase); unknown { + matchedUnknownBlock = true + } else { + for _, i := range insts { + if i.from.Item.Key == addr.Item.Key { + inst = i + break Blocks + } + } + } + } + + if component != nil { + if insts, unknown := component.Instances(ctx, ApplyPhase); unknown { + matchedUnknownBlock = true + } else { + if i, ok := insts[addr.Item.Key]; ok { + if inst != nil { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid component instance", + fmt.Sprintf("There was a plan for %s, which matched both removed and component blocks.", addr), + )) + log.Printf("[ERROR] stackeval: %s has both a component and a removed block that point to the same address", addr) + span.SetStatus(codes.Error, "both component and removed block present") + return nil, diags + } + inst = i + } + } + } + + if inst == nil && !matchedUnknownBlock { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid component instance", + fmt.Sprintf("There was a plan for %s, which matched no known component instances.", addr), + )) + log.Printf("[ERROR] stackeval: %s has planned changes, but no instance to apply them to", addr) + span.SetStatus(codes.Error, "no instance to apply changes to") + return nil, diags + } + + modulesRuntimePlan, err := componentInstPlan.ForModulesRuntime() + if err != nil { + // Suggests that the state is inconsistent with the + // plan, which is a bug in whatever provided us with + // those two artifacts, but we don't know who that + // caller is (it probably came from a client of the + // Core RPC API) so we don't include our typical + // "This is a bug in Terraform" language here. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Inconsistent component instance plan", + fmt.Sprintf("The plan for %s is inconsistent with its prior state: %s.", addr, err), + )) + log.Printf("[ERROR] stackeval: %s has a plan inconsistent with its prior state: %s", addr, err) + span.SetStatus(codes.Error, "plan is inconsistent with prior state") + return nil, diags + } + + if matchedUnknownBlock && inst == nil { + log.Printf("[TRACE] stackeval: %s matched an unknown block so returning prior state unchanged.", addr) + span.SetStatus(codes.Ok, "apply unknown") + + for _, step := range addr.Stack { + if step.Key == addrs.WildcardKey { + // for instances that were unknown we don't + // emit any updates for them. + return nil, nil + } + } + if addr.Item.Key == addrs.WildcardKey { + // for instances that were unknown we don't + // emit any updates for them. + return nil, nil + } + + // if we get here, then this was a concrete instance + // that was in state before the original plan, we + // do want to emit a "nothing changed" update for + // this instance. + + return &ComponentInstanceApplyResult{ + FinalState: modulesRuntimePlan.PrevRunState, + Complete: false, + }, nil + } + + var waitForComponents collections.Set[stackaddrs.AbsComponent] + var waitForRemoveds collections.Set[stackaddrs.AbsComponent] + if action == plans.Delete || action == plans.Forget { + // If the effect of this apply will be to destroy this + // component instance then we need to wait for all + // of our dependents to be destroyed first, because + // we're required to outlive them. + // + // (We can assume that all of the dependents are + // also performing destroy plans, because we'd have + // rejected the configuration as invalid if a + // downstream component were referring to a + // component that's been removed from the config.) + waitForComponents = dependentAddrs + + // If we're being destroyed, then we're waiting for + // everything that depended on us anyway. + waitForRemoveds = dependencyAddrs + } else { + // For all other actions, we must wait for our + // dependencies to finish applying their changes. + waitForComponents = dependencyAddrs + } + if depCount := waitForComponents.Len(); depCount != 0 { + log.Printf("[TRACE] stackeval: %s waiting for its predecessors (%d) to complete", addr, depCount) + } + for waitComponentAddr := range waitForComponents.All() { + if stack := main.Stack(ctx, waitComponentAddr.Stack, ApplyPhase); stack != nil { + if component := stack.Component(waitComponentAddr.Item); component != nil { + span.AddEvent("awaiting predecessor", trace.WithAttributes( + attribute.String("component_addr", waitComponentAddr.String()), + )) + success := component.ApplySuccessful(ctx) + if !success { + // If anything we're waiting on does not succeed then we can't proceed without + // violating the dependency invariants. + log.Printf("[TRACE] stackeval: %s cannot start because %s changes did not apply completely", addr, waitComponentAddr) + span.AddEvent("predecessor is incomplete", trace.WithAttributes( + attribute.String("component_addr", waitComponentAddr.String()), + )) + span.SetStatus(codes.Error, "predecessors did not completely apply") + + // We'll return a stub result that reports that nothing was changed, since + // we're not going to run our apply phase at all. + return inst.PlaceholderApplyResultForSkippedApply(modulesRuntimePlan), nil + // Since we're not calling inst.ApplyModuleTreePlan at all in this + // codepath, the stacks runtime will not emit any progress events for + // this component instance or any of the objects inside it. + } + } + } + } + for waitComponentAddr := range waitForRemoveds.All() { + if stack := main.Stack(ctx, waitComponentAddr.Stack, ApplyPhase); stack != nil { + if removed := stack.RemovedComponent(waitComponentAddr.Item); removed != nil { + span.AddEvent("awaiting predecessor", trace.WithAttributes( + attribute.String("component_addr", waitComponentAddr.String()), + )) + success := true + for _, block := range removed { + if !block.ApplySuccessful(ctx, stack.addr) { + success = false + } + } + if !success { + // If anything we're waiting on does not succeed then we can't proceed without + // violating the dependency invariants. + log.Printf("[TRACE] stackeval: %s cannot start because %s changes did not apply completely", addr, waitComponentAddr) + span.AddEvent("predecessor is incomplete", trace.WithAttributes( + attribute.String("component_addr", waitComponentAddr.String()), + )) + span.SetStatus(codes.Error, "predecessors did not completely apply") + + // We'll return a stub result that reports that nothing was changed, since + // we're not going to run our apply phase at all. + return inst.PlaceholderApplyResultForSkippedApply(modulesRuntimePlan), nil + // Since we're not calling inst.ApplyModuleTreePlan at all in this + // codepath, the stacks runtime will not emit any progress events for + // this component instance or any of the objects inside it. + } + } + } + } + log.Printf("[TRACE] stackeval: %s now applying", addr) + + ret, diags := inst.ApplyModuleTreePlan(ctx, modulesRuntimePlan) + if !ret.Complete { + span.SetStatus(codes.Error, "apply did not complete successfully") + } else { + span.SetStatus(codes.Ok, "apply complete") + } + return ret, diags + }, + ) + } + }) + + main := NewForApplying(config, plan, results, opts) + main.AllowLanguageExperiments(opts.ExperimentsAllowed) + begin(ctx, main) // the change tasks registered above become runnable + + // With the planned changes now in progress, we'll visit everything and + // each object to check itself (producing diagnostics) and announce any + // changes that were applied to it. + ctx, span := tracer.Start(ctx, "apply-time checks") + defer span.End() + + var seenSelfDepDiag atomic.Bool + ws, complete := newWalkStateCustomDiags( + func(diags tfdiags.Diagnostics) { + for _, diag := range diags { + if diagIsPromiseSelfReference(diag) { + // We'll discard all but the first promise-self-reference + // diagnostic we see; these tend to get duplicated + // because they emerge from all codepaths participating + // in the self-reference at once. + if !seenSelfDepDiag.CompareAndSwap(false, true) { + continue + } + } + outp.AnnounceDiagnostics(ctx, tfdiags.Diagnostics{diag}) + } + }, + func() tfdiags.Diagnostics { + // We emit all diagnostics immediately as they arrive, so + // we never have any accumulated diagnostics to emit at the end. + return nil + }, + ) + walk := &applyWalk{ + state: ws, + out: &outp, + } + + walkDynamicObjects( + ctx, walk, main, + ApplyPhase, + func(ctx context.Context, walk *walkWithOutput[*ApplyOutput], obj DynamicEvaler) { + main.walkApplyCheckObjectChanges(ctx, walk, obj) + }, + ) + + // Note: in practice this "complete" cannot actually return any + // diagnostics because our custom walkstate hooks above just announce + // the diagnostics immediately. But "complete" still serves the purpose + // of blocking until all of the async jobs are complete. + diags := complete() + + // By the time we get here all of the scheduled changes should be + // complete already anyway, since we should have visited them all + // in walkCheckAppliedChanges, but just to make sure we don't leave + // anything hanging in the background if walkCheckAppliedChanges is + // buggy we'll also pause here until the ChangeExec scheduler thinks + // everything it's supervising is complete. + results.AwaitCompletion(ctx) + + return withDiagnostics[*Main]{ + Result: main, + Diagnostics: diags, + }, nil + }) + diags := withDiags.Diagnostics + main := withDiags.Result + diags = diags.Append(diagnosticsForPromisingTaskError(err)) + if len(diags) > 0 { + outp.AnnounceDiagnostics(ctx, diags) + } + log.Printf("[TRACE] stackeval.ApplyPlan complete") + + return main, nil +} + +type ApplyOutput struct { + // Called each time we confirm that a planned change has now been applied. + // + // Each announced change can have a raw element, an external-facing + // element, or both. The raw element is opaque to anything outside of + // Terraform Core, while the external-facing element is never consumed + // by Terraform Core and is instead for other uses such as presenting + // changes in the UI. + // + // The callback should return relatively quickly to minimize the + // backpressure applied to the planning process. + AnnounceAppliedChange func(context.Context, stackstate.AppliedChange) + + // Called each time we encounter some diagnostics. These are asynchronous + // from planned changes because the evaluator will sometimes need to + // aggregate together some diagnostics and post-process the set before + // announcing them. Callers should not try to correlate diagnostics + // with planned changes by announcement-time-proximity. + // + // The callback should return relatively quickly to minimize the + // backpressure applied to the planning process. + AnnounceDiagnostics func(context.Context, tfdiags.Diagnostics) +} + +// applyWalk just bundles a [walkState] and an [ApplyOutput] together so we can +// concisely pass them both as a single argument between the all the apply walk +// driver functions below. +type applyWalk = walkWithOutput[*ApplyOutput] + +// walkApplyCheckObjectChanges deals with the leaf objects that can directly +// contribute changes and/or diagnostics to the apply result, which should each +// implement [ApplyChecker]. +// +// This function is not responsible for actually making the changes; they must +// be scheduled separately or this function will either block forever or +// return strange errors. (See [ApplyPlan] for more about how the apply phase +// deals with changes.) +func (m *Main) walkApplyCheckObjectChanges(ctx context.Context, walk *applyWalk, obj Applyable) { + walk.AsyncTask(ctx, func(ctx context.Context) { + ctx, span := tracer.Start(ctx, obj.tracingName()+" apply-time checks") + defer span.End() + + changes, diags := obj.CheckApply(ctx) + for _, change := range changes { + walk.out.AnnounceAppliedChange(ctx, change) + } + if len(diags) != 0 { + walk.out.AnnounceDiagnostics(ctx, diags) + } + }) +} + +func stateKeysToDiscard(prevRunState map[string]*anypb.Any, prevDescKeys collections.Set[statekeys.Key]) (discardRaws, discardDescs collections.Set[statekeys.Key], err error) { + discardRaws = statekeys.NewKeySet() + discardDescs = statekeys.NewKeySet() + + for rawKey := range prevRunState { + key, err := statekeys.Parse(rawKey) + if err != nil { + // We should not typically get here because if there was an invalid + // key then we should've caught it during planning. + return discardRaws, discardDescs, fmt.Errorf("invalid tracking key %q in previous run state: %w", rawKey, err) + } + if statekeys.RecognizedType(key) { + // Nothing to do for a key of a recognized type. + continue + } + if key.KeyType().UnrecognizedKeyHandling() == statekeys.DiscardIfUnrecognized { + discardRaws.Add(key) + } + } + + return discardDescs, discardDescs, nil +} + +func emitStateKeyDiscardEvents(ctx context.Context, discardRaws, discardDescs collections.Set[statekeys.Key], outp ApplyOutput) { + if discardRaws.Len() == 0 && discardDescs.Len() == 0 { + // Nothing to do, then! + return + } + // If we have at least one key in either set then we can deal with all + // of them at once in a single "applied change". + outp.AnnounceAppliedChange(ctx, &stackstate.AppliedChangeDiscardKeys{ + DiscardRawKeys: discardRaws, + DiscardDescKeys: discardDescs, + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main_inspect.go b/internal/stacks/stackruntime/internal/stackeval/main_inspect.go new file mode 100644 index 0000000000..d2109be9cc --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/main_inspect.go @@ -0,0 +1,85 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type InspectOpts struct { + // Optional values to use when asked for the values of input variables. + // + // Any that are not specified will appear in expressions as an unknown + // value using the declared type constraint, thereby acting as + // placeholders for whatever real values might be defined as planning + // options. + InputVariableValues map[stackaddrs.InputVariable]ExternalInputValue + + // Provider factories to use for operations that involve provider clients. + // + // Populating this is optional but if not populated then operations which + // expect to call into providers will return errors. + ProviderFactories ProviderFactories + + // TestOnlyGlobals is optional and if set makes it possible to use + // references like _test_only_global.name to refer to values from this + // map from anywhere in the entire stack configuration. + // + // This is intended as a kind of "test double" so that we can write more + // minimal unit tests that can avoid relying on too many language features + // all at once, so that hopefully future maintenance will not require + // making broad changes across many different tests at once, which would + // then risk inadvertently treating a regression as expected behavior. + // + // Configurations that refer to test-only globals are not valid for use + // outside of the test suite of this package. + TestOnlyGlobals map[string]cty.Value +} + +// EvalExpr evaluates an arbitrary expression in the main scope of the +// specified stack instance using the approach that's appropriate for the +// specified evaluation phase. +// +// Typical use of this method would be with a Main configured for "inspecting", +// using [InspectPhase] as the phase. This method can be used for any phase +// that supports dynamic expression evaluation in principle, but in that case +// evaluation might cause relatively-expensive effects such as creating +// plans for components. +func (m *Main) EvalExpr(ctx context.Context, expr hcl.Expression, scopeStackInst stackaddrs.StackInstance, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + ret, err := promising.MainTask(ctx, func(ctx context.Context) (withDiagnostics[cty.Value], error) { + var diags tfdiags.Diagnostics + + scope := m.Stack(ctx, scopeStackInst, phase) + if scope == nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Evaluating expression in undeclared stack", + fmt.Sprintf("Cannot evaluate an expression in %s, because it's not declared by the current configuration.", scopeStackInst), + )) + return withDiagnostics[cty.Value]{ + Result: cty.DynamicVal, + Diagnostics: diags, + }, nil + } + + val, moreDiags := EvalExpr(ctx, expr, phase, scope) + diags = diags.Append(moreDiags) + return withDiagnostics[cty.Value]{ + Result: val, + Diagnostics: diags, + }, nil + }) + if err != nil { + ret.Diagnostics = ret.Diagnostics.Append(diagnosticsForPromisingTaskError(err)) + } + return ret.Result, ret.Diagnostics +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main_plan.go b/internal/stacks/stackruntime/internal/stackeval/main_plan.go new file mode 100644 index 0000000000..fb183a540c --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/main_plan.go @@ -0,0 +1,210 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync/atomic" + + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +// PlanAll visits all of the objects in the configuration and the prior state, +// performs all of the necessary internal preparation work, and emits a +// series of planned changes and diagnostics through the callbacks in the +// given [PlanOutput] value. +// +// Planning is a streaming operation and so this function does not directly +// return a value. Instead, callers must consume the data gradually passed into +// the provided callbacks and, if necessary, construct their own overall +// data structure by aggregating the results. +func (m *Main) PlanAll(ctx context.Context, outp PlanOutput) { + hooks := hooksFromContext(ctx) + hs, ctx := hookBegin(ctx, hooks.BeginPlan, hooks.ContextAttach, struct{}{}) + defer hookMore(ctx, hs, hooks.EndPlan, struct{}{}) + + // An important design goal here is that only our main walk code in this + // file interacts directly with the async PlanOutput API, with it calling + // into "normal-shaped" functions elsewhere that just run to completion + // and provide their results as return values. + // + // The purpose of the logic in this file is to provide that abstraction to + // the rest of the code so that the async streaming behavior does not + // dominate the overall design of package stackeval. + + var prevRunStateRaw map[string]*anypb.Any + if prevRunState := m.PlanPrevState(); prevRunState != nil { + prevRunStateRaw = prevRunState.InputRaw() + } + outp.AnnouncePlannedChange(ctx, &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }) + for k, raw := range prevRunStateRaw { + outp.AnnouncePlannedChange(ctx, &stackplan.PlannedChangePriorStateElement{ + Key: k, + Raw: raw, + }) + } + + outp.AnnouncePlannedChange(ctx, &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: m.PlanTimestamp(), + }) + + // TODO: Announce an extra planned change here if we have any unrecognized + // raw state or state description keys that we'll need to delete during the + // apply phase. + + diags, err := promising.MainTask(ctx, func(ctx context.Context) (tfdiags.Diagnostics, error) { + // The idea here is just to iterate over everything in the configuration, + // find its corresponding evaluation object, and then ask it to validate + // itself. We make all of these calls asynchronously so that everything + // can get started and then downstream calls will block on promise + // resolution to achieve the correct evaluation order. + + var seenSelfDepDiag atomic.Bool + var seenAnyErrors atomic.Bool + reportDiags := func(diags tfdiags.Diagnostics) { + for _, diag := range diags { + if diag.Severity() == tfdiags.Error { + seenAnyErrors.Store(true) + } + if diagIsPromiseSelfReference(diag) { + // We'll discard all but the first promise-self-reference + // diagnostic we see; these tend to get duplicated + // because they emerge from all codepaths participating + // in the self-reference at once. + if !seenSelfDepDiag.CompareAndSwap(false, true) { + continue + } + } + outp.AnnounceDiagnostics(ctx, tfdiags.Diagnostics{diag}) + } + } + noopComplete := func() tfdiags.Diagnostics { + // We emit all diagnostics immediately as they arrive, so + // we never have any accumulated diagnostics to emit at the end. + return nil + } + + // First we walk the static objects to give them a chance to check + // whether they are configured appropriately for planning. This + // allows us to report static problems only once for an entire + // configuration object, rather than redundantly reporting for every + // instance of the object. + ws, complete := newWalkStateCustomDiags(reportDiags, noopComplete) + walk := &planWalk{ + state: ws, + out: &outp, + } + walkStaticObjects( + ctx, walk, m, + func(ctx context.Context, walk *walkWithOutput[*PlanOutput], obj StaticEvaler) { + m.walkPlanObjectChanges(ctx, walk, obj) + }, + ) + // Note: in practice this "complete" cannot actually return any + // diagnostics because our custom walkstate hooks above just announce + // the diagnostics immediately. But "complete" still serves the purpose + // of blocking until all of the async jobs are complete. + diags := complete() + if seenAnyErrors.Load() { + // If we already found static errors then we'll halt here to have + // the user correct those first. + return diags, nil + } + + // If the static walk completed then we'll now perform a dynamic walk + // which is where we'll actually produce the plan and where we'll + // learn about any dynamic errors which affect only specific instances + // of objects. + // We'll use a fresh walkState here because we already completed + // the previous one after the static walk. + ws, complete = newWalkStateCustomDiags(reportDiags, noopComplete) + walk.state = ws + walkDynamicObjects( + ctx, walk, m, + PlanPhase, + func(ctx context.Context, walk *planWalk, obj DynamicEvaler) { + m.walkPlanObjectChanges(ctx, walk, obj) + }, + ) + + // Note: in practice this "complete" cannot actually return any + // diagnostics because our custom walkstate hooks above just announce + // the diagnostics immediately. But "complete" still serves the purpose + // of blocking until all of the async jobs are complete. + return complete(), nil + }) + diags = diags.Append(diagnosticsForPromisingTaskError(err)) + if len(diags) > 0 { + outp.AnnounceDiagnostics(ctx, diags) + } + + // Now, that we've finished walking the graph. We'll announce the + // provider function results so that they can be used during the apply + // phase. + hashes := m.providerFunctionResults.GetHashes() + if len(hashes) > 0 { + // Only add this planned change if we actually have any results. + outp.AnnouncePlannedChange(ctx, &stackplan.PlannedChangeProviderFunctionResults{ + Results: m.providerFunctionResults.GetHashes(), + }) + } + + // The caller (in stackruntime) is responsible for generating the final + // stackplan.PlannedChangeApplyable message, just in case it detects + // problems of its own before finally returning. +} + +type PlanOutput struct { + // Called each time we find a new change to announce as part of the + // overall plan. + // + // Each announced change can have a raw element, an external-facing + // element, or both. The raw element is opaque to anything outside of + // Terraform Core, while the external-facing element is never consumed + // by Terraform Core and is instead for other uses such as presenting + // changes in the UI. + // + // The callback should return relatively quickly to minimize the + // backpressure applied to the planning process. + AnnouncePlannedChange func(context.Context, stackplan.PlannedChange) + + // Called each time we encounter some diagnostics. These are asynchronous + // from planned changes because the evaluator will sometimes need to + // aggregate together some diagnostics and post-process the set before + // announcing them. Callers should not try to correlate diagnostics + // with planned changes by announcement-time-proximity. + // + // The callback should return relatively quickly to minimize the + // backpressure applied to the planning process. + AnnounceDiagnostics func(context.Context, tfdiags.Diagnostics) +} + +// planWalk just bundles a [walkState] and a [PlanOutput] together so we can +// concisely pass them both as a single argument between the all the plan walk +// driver functions below. +type planWalk = walkWithOutput[*PlanOutput] + +// walkPlanObjectChanges deals with the leaf objects that can directly +// contribute changes to the plan, which should each implement [Plannable]. +func (m *Main) walkPlanObjectChanges(ctx context.Context, walk *planWalk, obj Plannable) { + walk.AsyncTask(ctx, func(ctx context.Context) { + ctx, span := tracer.Start(ctx, obj.tracingName()+" planning") + defer span.End() + + changes, diags := obj.PlanChanges(ctx) + for _, change := range changes { + walk.out.AnnouncePlannedChange(ctx, change) + } + if len(diags) != 0 { + walk.state.AddDiags(diags) + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main_validate.go b/internal/stacks/stackruntime/internal/stackeval/main_validate.go new file mode 100644 index 0000000000..9387b722f0 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/main_validate.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + + "go.opentelemetry.io/otel/codes" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ValidateAll checks the validation rules for declared in the configuration +// and returns any diagnostics returned by any of those checks. +// +// This function starts its own [promising.MainTask] and so is a good entry +// point for external callers that don't deal with promises directly themselves, +// encapsulating all of the promise-related implementation details. +// +// This function must be called with a context that belongs to a task started +// from the "promising" package, or else it will immediately panic. +func (m *Main) ValidateAll(ctx context.Context) tfdiags.Diagnostics { + diags, err := promising.MainTask(ctx, func(ctx context.Context) (tfdiags.Diagnostics, error) { + // The idea here is just to iterate over everything in the configuration, + // find its corresponding evaluation object, and then ask it to validate + // itself. We make all of these calls asynchronously so that everything + // can get started and then downstream calls will block on promise + // resolution to achieve the correct evaluation order. + ws, complete := newWalkState() + + // Our generic static walker is built to support the more advanced + // needs of the plan walk which produces streaming results through + // an "output" object. We don't need that here so we'll just stub + // it out as a zero-length type. + walk := &walkWithOutput[struct{}]{ + out: struct{}{}, + state: ws, + } + + walkStaticObjects( + ctx, walk, m, + func(ctx context.Context, walk *walkWithOutput[struct{}], obj StaticEvaler) { + m.walkValidateObject(ctx, walk.state, obj) + }, + ) + + return complete(), nil + }) + diags = diags.Append(diagnosticsForPromisingTaskError(err)) + return finalDiagnosticsFromEval(diags) +} + +// walkValidateObject arranges for any given [Validatable] object to be +// asynchronously validated, reporting any of its diagnostics to the +// [walkState]. +// +// Just like the [Validatable] interface itself, this performs only shallow +// validation of the direct content of the given object. For object types +// that have child objects the caller must also discover each of those and +// arrange for them to be validated by a separate call to this method. +func (m *Main) walkValidateObject(ctx context.Context, ws *walkState, obj Validatable) { + ws.AsyncTask(ctx, func(ctx context.Context) { + ctx, span := tracer.Start(ctx, obj.tracingName()+" validation") + defer span.End() + diags := obj.Validate(ctx) + ws.AddDiags(diags) + if diags.HasErrors() { + span.SetStatus(codes.Error, "validation returned errors") + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/output_value.go b/internal/stacks/stackruntime/internal/stackeval/output_value.go new file mode 100644 index 0000000000..209e74675d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/output_value.go @@ -0,0 +1,303 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// OutputValue represents an input variable belonging to a [Stack]. +type OutputValue struct { + addr stackaddrs.AbsOutputValue + stack *Stack + config *OutputValueConfig + + main *Main + + resultValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ Plannable = (*OutputValue)(nil) + +func newOutputValue(main *Main, addr stackaddrs.AbsOutputValue, stack *Stack, config *OutputValueConfig) *OutputValue { + return &OutputValue{ + addr: addr, + stack: stack, + config: config, + main: main, + } +} + +func (v *OutputValue) ResultType() (cty.Type, *typeexpr.Defaults) { + decl := v.config.config + if decl == nil { + // If we get here then something odd must be going on, but + // we don't have enough context to guess why so we'll just + // return, in effect, "I don't know". + return cty.DynamicPseudoType, &typeexpr.Defaults{ + Type: cty.DynamicPseudoType, + } + } + return decl.Type.Constraint, decl.Type.Defaults +} + +func (v *OutputValue) CheckResultType() (cty.Type, *typeexpr.Defaults, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + ty, defs := v.ResultType() + decl := v.config.config + if v.addr.Stack.IsRoot() { + // A root output value cannot return provider configuration references, + // because root outputs outlive the operation that generated them but + // provider instances are live only during a single evaluation. + for _, path := range stackconfigtypes.ProviderConfigPathsInType(ty) { + // We'll construct a synthetic error so that we can conveniently + // use tfdiags.FormatError to help construct a more specific error + // message. + err := path.NewErrorf("cannot return provider configuration reference from the root stack") + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid output value type", + Detail: fmt.Sprintf( + "Unsupported output value type: %s.", + tfdiags.FormatError(err), + ), + Subject: decl.Type.Expression.Range().Ptr(), + }) + } + } + return ty, defs, diags +} + +func (v *OutputValue) ResultValue(ctx context.Context, phase EvalPhase) cty.Value { + val, _ := v.CheckResultValue(ctx, phase) + return val +} + +func (v *OutputValue) CheckResultValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return withCtyDynamicValPlaceholder(doOnceWithDiags( + ctx, v.tracingName(), v.resultValue.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + cfg := v.config + ty, defs := v.ResultType() + + result, moreDiags := EvalExprAndEvalContext(ctx, v.config.config.Value, phase, v.stack) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cfg.markResultValue(cty.UnknownVal(ty)), diags + } + + var err error + if defs != nil { + result.Value = defs.Apply(result.Value) + } + result.Value, err = convert.Convert(result.Value, ty) + if err != nil { + diags = diags.Append(result.Diagnostic( + tfdiags.Error, + "Invalid output value result", + fmt.Sprintf("Unsuitable value for output %q: %s.", v.addr.Item.Name, tfdiags.FormatError(err)), + )) + return cfg.markResultValue(cty.UnknownVal(ty)), diags + } + + if cfg.config.Ephemeral { + // Verify that ephemeral outputs are not declared on the root stack. + if v.addr.Stack.IsRoot() { + diags = diags.Append(result.Diagnostic( + tfdiags.Error, + "Ephemeral output value not allowed on root stack", + fmt.Sprintf("Output value %q is marked as ephemeral, this is only allowed in embedded stacks.", v.addr.Item.Name), + )) + } + + // Verify that the value is ephemeral. + if !marks.Contains(result.Value, marks.Ephemeral) { + diags = diags.Append(result.Diagnostic( + tfdiags.Error, + "Expected ephemeral value", + fmt.Sprintf("The output value %q is marked as ephemeral, but the value is not ephemeral.", v.addr.Item.Name), + )) + } + + } else { + _, markses := result.Value.UnmarkDeepWithPaths() + problemPaths, _ := marks.PathsWithMark(markses, marks.Ephemeral) + var moreDiags tfdiags.Diagnostics + for _, path := range problemPaths { + if len(path) == 0 { + moreDiags = moreDiags.Append(result.Diagnostic( + tfdiags.Error, + "Ephemeral value not allowed", + fmt.Sprintf("The output value %q does not accept ephemeral values.", v.addr.Item.Name), + )) + } else { + moreDiags = moreDiags.Append(result.Diagnostic( + tfdiags.Error, + "Ephemeral value not allowed", + fmt.Sprintf( + "The output value %q does not accept ephemeral values, so the value of %s is not compatible.", + v.addr.Item.Name, + tfdiags.FormatCtyPath(path), + ), + )) + } + } + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + // We return an unknown value placeholder here to avoid + // the risk of a recipient of this value using it in a + // way that would be inappropriate for an ephemeral value. + result.Value = cty.UnknownVal(ty) + } + } + + return cfg.markResultValue(result.Value), diags + }, + )) +} + +func (v *OutputValue) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // FIXME: We should really check the type during the validation phase + // in OutputValueConfig, rather than the planning phase in OutputValue. + _, _, moreDiags := v.CheckResultType() + diags = diags.Append(moreDiags) + _, moreDiags = v.CheckResultValue(ctx, phase) + diags = diags.Append(moreDiags) + + return diags +} + +// PlanChanges implements Plannable as a plan-time validation of the variable's +// declaration and of the caller's definition of the variable. +func (v *OutputValue) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + diags := v.checkValid(ctx, PlanPhase) + if diags.HasErrors() { + return nil, diags + } + + // Only the root stack's outputs are exposed externally. + if !v.addr.Stack.IsRoot() { + return nil, diags + } + + before := v.main.PlanPrevState().RootOutputValue(v.addr.Item) + if v.main.PlanningOpts().PlanningMode == plans.DestroyMode { + if before == cty.NilVal { + // If the value didn't exist before and we're in destroy mode, + // then we'll just ignore this value. + return nil, diags + } + + // Otherwise, return a planned change deleting the value. + ty, _ := v.ResultType() + return []stackplan.PlannedChange{ + &stackplan.PlannedChangeOutputValue{ + Addr: v.addr.Item, + Action: plans.Delete, + Before: before, + After: cty.NullVal(ty), + }, + }, diags + } + + decl := v.config.config + after := v.ResultValue(ctx, PlanPhase) + if decl.Ephemeral { + after = cty.NullVal(after.Type()) + } + + var action plans.Action + if before != cty.NilVal { + if decl.Ephemeral { + // if the value is ephemeral, we always consider it to be updated + action = plans.Update + } else { + unmarkedBefore, beforePaths := before.UnmarkDeepWithPaths() + unmarkedAfter, afterPaths := after.UnmarkDeepWithPaths() + result := unmarkedBefore.Equals(unmarkedAfter) + if result.IsKnown() && result.True() && marks.MarksEqual(beforePaths, afterPaths) { + action = plans.NoOp + } else { + // If we don't know for sure that the values are equal, then we'll + // call this an update. + action = plans.Update + } + } + } else { + action = plans.Create + before = cty.NullVal(cty.DynamicPseudoType) + } + + return []stackplan.PlannedChange{ + &stackplan.PlannedChangeOutputValue{ + Addr: v.addr.Item, + Action: action, + Before: before, + After: after, + }, + }, diags +} + +// References implements Referrer +func (v *OutputValue) References(context.Context) []stackaddrs.AbsReference { + cfg := v.config.config + var ret []stackaddrs.Reference + ret = append(ret, ReferencesInExpr(cfg.Value)...) + return makeReferencesAbsolute(ret, v.addr.Stack) +} + +// CheckApply implements Applyable. +func (v *OutputValue) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + if !v.addr.Stack.IsRoot() { + return nil, v.checkValid(ctx, ApplyPhase) + } + + diags := v.checkValid(ctx, ApplyPhase) + if diags.HasErrors() { + return nil, diags + } + + if v.main.PlanBeingApplied().DeletedOutputValues.Has(v.addr.Item) { + // If the plan being applied has marked this output value for deletion, + // we won't handle it here. The stack will take care of removing + // everything related to this output value. + return nil, diags + } + + decl := v.config.config + value := v.ResultValue(ctx, ApplyPhase) + if decl.Ephemeral { + value = cty.NullVal(value.Type()) + } + + return []stackstate.AppliedChange{ + &stackstate.AppliedChangeOutputValue{ + Addr: v.addr.Item, + Value: value, + }, + }, diags +} + +func (v *OutputValue) tracingName() string { + return v.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/output_value_config.go b/internal/stacks/stackruntime/internal/stackeval/output_value_config.go new file mode 100644 index 0000000000..06c4fa1645 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/output_value_config.go @@ -0,0 +1,143 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// OutputValueConfig represents an "output" block in a stack configuration. +type OutputValueConfig struct { + addr stackaddrs.ConfigOutputValue + config *stackconfig.OutputValue + stack *StackConfig + + main *Main + + validatedValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ Validatable = (*OutputValueConfig)(nil) + +func newOutputValueConfig(main *Main, addr stackaddrs.ConfigOutputValue, stack *StackConfig, config *stackconfig.OutputValue) *OutputValueConfig { + if config == nil { + panic("newOutputValueConfig with nil configuration") + } + return &OutputValueConfig{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +func (ov *OutputValueConfig) tracingName() string { + return ov.addr.String() +} + +// Value returns the result value for this output value that should be used +// for validating other objects that refer to this output value. +// +// If this output value is itself invalid then the result may be a +// compatibly-typed unknown placeholder value that's suitable for partial +// downstream validation. +func (ov *OutputValueConfig) Value(ctx context.Context, phase EvalPhase) cty.Value { + v, _ := ov.ValidateValue(ctx, phase) + return v +} + +// ValueTypeConstraint returns the type that the final result of this output +// value is guaranteed to have. +func (ov *OutputValueConfig) ValueTypeConstraint() cty.Type { + return ov.config.Type.Constraint +} + +// ValidateValue validates that the value expression is evaluatable and that +// its result can convert to the declared type constraint, returning the +// resulting value. +// +// If the returned diagnostics has errors then the returned value might be +// just an approximation of the result, such as an unknown value with the +// declared type constraint. +func (ov *OutputValueConfig) ValidateValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return withCtyDynamicValPlaceholder(doOnceWithDiags( + ctx, ov.tracingName(), ov.validatedValue.For(phase), + ov.validateValueInner(phase), + )) +} + +// validateValueInner is the real implementation of ValidateValue, which runs +// in the background only once per instance of [OutputValueConfig] and then +// provides the result for all ValidateValue callers simultaneously. +func (ov *OutputValueConfig) validateValueInner(phase EvalPhase) func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + return func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + result, moreDiags := EvalExprAndEvalContext(ctx, ov.config.Value, phase, ov.stack) + v := result.Value + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + v = ov.markResultValue(cty.UnknownVal(ov.ValueTypeConstraint())) + } + + var err error + v, err = convert.Convert(v, ov.config.Type.Constraint) + if err != nil { + v = cty.UnknownVal(ov.ValueTypeConstraint()) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid result for output value", + Detail: fmt.Sprintf( + "The result value does not match the declared type constraint: %s.", + tfdiags.FormatError(err), + ), + Subject: ov.config.Value.Range().Ptr(), + Expression: result.Expression, + EvalContext: result.EvalContext, + }) + } + + return ov.markResultValue(v), diags + } +} + +func (ov *OutputValueConfig) markResultValue(v cty.Value) cty.Value { + decl := ov.config + if decl.Sensitive { + v = v.Mark(marks.Sensitive) + } + if decl.Ephemeral { + v = v.Mark(marks.Ephemeral) + } + return v +} + +func (ov *OutputValueConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + _, moreDiags := ov.ValidateValue(ctx, phase) + diags = diags.Append(moreDiags) + return diags +} + +// Validate implements Validatable. +func (ov *OutputValueConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + return ov.checkValid(ctx, ValidatePhase) +} + +// PlanChanges implements Plannable. +func (ov *OutputValueConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, ov.checkValid(ctx, PlanPhase) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/output_value_test.go b/internal/stacks/stackruntime/internal/stackeval/output_value_test.go new file mode 100644 index 0000000000..7b63de0835 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/output_value_test.go @@ -0,0 +1,373 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +func TestOutputValueResultValue(t *testing.T) { + ctx := context.Background() + cfg := testStackConfig(t, "output_value", "basics") + + // NOTE: This also indirectly tests the propagation of output values + // from a child stack into its parent, even though that's technically + // the responsibility of [StackCall] rather than [OutputValue], + // because propagating upward from child stacks is a major purpose + // of output values that must keep working. + childStackAddr := stackaddrs.RootStackInstance.Child("child", addrs.NoKey) + + tests := map[string]struct { + RootVal cty.Value + ChildVal cty.Value + WantRootVal cty.Value + WantChildVal cty.Value + WantRootErr string + WantChildErr string + }{ + "valid with no type conversions": { + RootVal: cty.StringVal("root value"), + ChildVal: cty.StringVal("child value"), + + WantRootVal: cty.StringVal("root value"), + WantChildVal: cty.StringVal("child value"), + }, + "valid after type conversions": { + RootVal: cty.True, + ChildVal: cty.NumberIntVal(4), + + WantRootVal: cty.StringVal("true"), + WantChildVal: cty.StringVal("4"), + }, + "type mismatch root": { + RootVal: cty.EmptyObjectVal, + ChildVal: cty.StringVal("irrelevant"), + + WantRootVal: cty.UnknownVal(cty.String), + WantChildVal: cty.StringVal("irrelevant"), + + WantRootErr: `Unsuitable value for output "root": string required.`, + }, + "type mismatch child": { + RootVal: cty.StringVal("irrelevant"), + ChildVal: cty.EmptyTupleVal, + + WantRootVal: cty.StringVal("irrelevant"), + WantChildVal: cty.UnknownVal(cty.String), + + WantChildErr: `Unsuitable value for output "foo": string required.`, + }, + "dynamic value placeholders": { + RootVal: cty.DynamicVal, + ChildVal: cty.DynamicVal, + + WantRootVal: cty.UnknownVal(cty.String), + WantChildVal: cty.UnknownVal(cty.String), + }, + "ephemeral value when not allowed": { + RootVal: cty.StringVal("root value").Mark(marks.Ephemeral), + ChildVal: cty.StringVal("child value").Mark(marks.Ephemeral), + + WantRootVal: cty.UnknownVal(cty.String), + WantChildVal: cty.UnknownVal(cty.String), + + WantRootErr: `The output value "root" does not accept ephemeral values.`, + WantChildErr: `The output value "foo" does not accept ephemeral values.`, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "root_output": test.RootVal, + "child_output": test.ChildVal, + }, + }) + + t.Run("root", func(t *testing.T) { + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + mainStack := main.MainStack() + rootOutput := mainStack.OutputValues()[stackaddrs.OutputValue{Name: "root"}] + if rootOutput == nil { + t.Fatal("root output value doesn't exist at all") + } + got, diags := rootOutput.CheckResultValue(ctx, InspectPhase) + + if wantErr := test.WantRootErr; wantErr != "" { + if !diags.HasErrors() { + t.Errorf("unexpected success\ngot: %#v\nwant error: %s", got, wantErr) + } + if len(diags) != 1 { + t.Fatalf("extraneous diagnostics\n%s", diags.Err()) + } + if gotErr := diags[0].Description().Detail; gotErr != wantErr { + t.Errorf("wrong error message detail\ngot: %s\nwant: %s", gotErr, wantErr) + } + return struct{}{}, nil + } + + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err()) + } + want := test.WantRootVal + if !want.RawEquals(got) { + t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want) + } + return struct{}{}, nil + }) + }) + t.Run("child", func(t *testing.T) { + t.Run("from the child perspective", func(t *testing.T) { + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + childStack := main.Stack(ctx, childStackAddr, InspectPhase) + if childStack == nil { + t.Fatal("child stack doesn't exist at all") + } + childOutput := childStack.OutputValues()[stackaddrs.OutputValue{Name: "foo"}] + if childOutput == nil { + t.Fatal("child output value doesn't exist at all") + } + got, diags := childOutput.CheckResultValue(ctx, InspectPhase) + + if wantErr := test.WantChildErr; wantErr != "" { + if !diags.HasErrors() { + t.Errorf("unexpected success\ngot: %#v\nwant error: %s", got, wantErr) + } + if len(diags) != 1 { + t.Fatalf("extraneous diagnostics\n%s", diags.Err()) + } + if gotErr := diags[0].Description().Detail; gotErr != wantErr { + t.Errorf("wrong error message detail\ngot: %s\nwant: %s", gotErr, wantErr) + } + return struct{}{}, nil + } + + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err()) + } + want := test.WantChildVal + if !want.RawEquals(got) { + t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want) + } + return struct{}{}, nil + }) + }) + t.Run("from the root perspective", func(t *testing.T) { + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + mainStack := main.MainStack() + childOutput := mainStack.OutputValues()[stackaddrs.OutputValue{Name: "child"}] + if childOutput == nil { + t.Fatal("child output value doesn't exist at all") + } + got, diags := childOutput.CheckResultValue(ctx, InspectPhase) + + // We should never see any errors when viewed from the + // root perspective, because the root output value + // only reports its _own_ errors, not the indirect + // errors caused by things it refers to. + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err()) + } + want := test.WantChildVal + if !want.RawEquals(got) { + t.Errorf("wrong value\ngot: %#v\nwant: %#v", got, want) + } + return struct{}{}, nil + }) + }) + }) + }) + } +} + +func TestOutputValueEphemeral(t *testing.T) { + ctx := context.Background() + + tests := map[string]struct { + fixtureName string + givenVal cty.Value + allowed bool + expectedDiagnosticSummaries []string + wantVal cty.Value + }{ + "ephemeral and declared as ephemeral": { + fixtureName: "ephemeral_yes", + givenVal: cty.StringVal("beep").Mark(marks.Ephemeral), + allowed: false, + expectedDiagnosticSummaries: []string{"Ephemeral output value not allowed on root stack"}, + wantVal: cty.StringVal("beep").Mark(marks.Ephemeral), + }, + "ephemeral and not declared as ephemeral": { + fixtureName: "ephemeral_no", + givenVal: cty.StringVal("beep").Mark(marks.Ephemeral), + allowed: false, + expectedDiagnosticSummaries: []string{"Ephemeral value not allowed"}, + wantVal: cty.UnknownVal(cty.String), + }, + "non-ephemeral and declared as ephemeral": { + fixtureName: "ephemeral_yes", + givenVal: cty.StringVal("beep"), + allowed: false, + expectedDiagnosticSummaries: []string{"Ephemeral output value not allowed on root stack", "Expected ephemeral value"}, + wantVal: cty.StringVal("beep").Mark(marks.Ephemeral), + }, + "non-ephemeral and not declared as ephemeral": { + fixtureName: "ephemeral_no", + givenVal: cty.StringVal("beep"), + allowed: true, + wantVal: cty.StringVal("beep"), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + cfg := testStackConfig(t, "output_value", test.fixtureName) + outputAddr := stackaddrs.OutputValue{Name: "result"} + + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "result": test.givenVal, + }, + }) + + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + stack := main.MainStack() + output := stack.OutputValues()[outputAddr] + if output == nil { + t.Fatalf("missing %s", outputAddr) + } + want := test.wantVal + got, diags := output.CheckResultValue(ctx, InspectPhase) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong value for %s\n%s", outputAddr, diff) + } + + if test.allowed { + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + } else { + if !diags.HasErrors() { + t.Fatalf("no errors; should have failed") + } + + foundDiagSummaries := make(map[string]bool) + for _, diag := range diags { + summary := diag.Description().Summary + foundDiagSummaries[summary] = true + } + + if len(foundDiagSummaries) != len(test.expectedDiagnosticSummaries) { + t.Fatalf("wrong number of diagnostics, expected %v, got \n%s", test.expectedDiagnosticSummaries, diags.Err().Error()) + } + + for _, expectedSummary := range test.expectedDiagnosticSummaries { + if !foundDiagSummaries[expectedSummary] { + t.Fatalf("missing diagnostic with summary %s", expectedSummary) + } + } + } + return struct{}{}, nil + }) + }) + } +} + +func TestOutputValueEphemeralInChildStack(t *testing.T) { + ctx := context.Background() + + tests := map[string]struct { + fixtureName string + givenVal cty.Value + allowed bool + expectedDiagnosticSummaries []string + wantVal cty.Value + }{ + "ephemeral and declared as ephemeral": { + fixtureName: "ephemeral_child", + givenVal: cty.StringVal("beep").Mark(marks.Ephemeral), + allowed: true, + wantVal: cty.StringVal("beep").Mark(marks.Ephemeral), + }, + "non-ephemeral and declared as ephemeral": { + fixtureName: "ephemeral_child", + givenVal: cty.StringVal("beep"), + allowed: false, + expectedDiagnosticSummaries: []string{"Expected ephemeral value"}, + wantVal: cty.StringVal("beep").Mark(marks.Ephemeral), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + cfg := testStackConfig(t, "output_value", test.fixtureName) + outputAddr := stackaddrs.OutputValue{Name: "result"} + + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "result": test.givenVal, + }, + }) + + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + rootStack := main.MainStack() + childStackStep := stackaddrs.StackInstanceStep{ + Name: "child", + Key: addrs.NoKey, + } + stack := rootStack.ChildStack(ctx, childStackStep, ValidatePhase) + output := stack.OutputValues()[outputAddr] + if output == nil { + t.Fatalf("missing %s", outputAddr) + } + want := test.wantVal + got, diags := output.CheckResultValue(ctx, InspectPhase) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong value for %s\n%s", outputAddr, diff) + } + + if test.allowed { + if diags.HasErrors() { + t.Errorf("unexpected errors\n%s", diags.Err().Error()) + } + } else { + if !diags.HasErrors() { + t.Fatalf("no errors; should have failed") + } + + foundDiagSummaries := make(map[string]bool) + for _, diag := range diags { + summary := diag.Description().Summary + foundDiagSummaries[summary] = true + } + + if len(foundDiagSummaries) != len(test.expectedDiagnosticSummaries) { + t.Fatalf("wrong number of diagnostics, expected %v, got \n%s", test.expectedDiagnosticSummaries, diags.Err().Error()) + } + + for _, expectedSummary := range test.expectedDiagnosticSummaries { + if !foundDiagSummaries[expectedSummary] { + t.Fatalf("missing diagnostic with summary %s", expectedSummary) + } + } + } + return struct{}{}, nil + }) + }) + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/planning.go b/internal/stacks/stackruntime/internal/stackeval/planning.go new file mode 100644 index 0000000000..1d082008ae --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/planning.go @@ -0,0 +1,220 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval/stubs" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type PlanOpts struct { + PlanningMode plans.Mode + + InputVariableValues map[stackaddrs.InputVariable]ExternalInputValue + + ProviderFactories ProviderFactories + + PlanTimestamp time.Time + + DependencyLocks depsfile.Locks +} + +// Plannable is implemented by objects that can participate in planning. +type Plannable interface { + // PlanChanges produces zero or more [stackplan.PlannedChange] objects + // representing changes needed to converge the current and desired states + // for the reciever, and zero or more diagnostics that represent any + // problems encountered while calcuating the changes. + // + // The diagnostics returned by PlanChanges must be shallow, which is to + // say that in particular they _must not_ call the PlanChanges methods + // of other objects that implement Plannable, and should also think + // very hard about calling any planning-related methods of other objects, + // to avoid generating duplicate diagnostics via two different return + // paths. + // + // In general, assume that _all_ objects that implement Plannable will + // have their Validate methods called at some point during planning, and + // so it's unnecessary and harmful to for one object to try to handle + // planning (or plan-time validation) on behalf of some other object. + PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) + + // Our general async planning helper relies on this to name its + // tracing span. + tracingNamer +} + +func PlanComponentInstance(ctx context.Context, main *Main, state *states.State, opts *terraform.PlanOpts, scope ConfigComponentExpressionScope[stackaddrs.AbsComponentInstance]) (*plans.Plan, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + addr := scope.Addr() + + h := hooksFromContext(ctx) + hookSingle(ctx, hooksFromContext(ctx).PendingComponentInstancePlan, addr) + seq, ctx := hookBegin(ctx, h.BeginComponentInstancePlan, h.ContextAttach, addr) + + // This is our main bridge from the stacks language into the main Terraform + // module language during the planning phase. We need to ask the main + // language runtime to plan the module tree associated with this + // component and return the result. + + moduleTree := scope.ModuleTree(ctx) + if moduleTree == nil { + // Presumably the configuration is invalid in some way, so + // we can't create a plan and the relevant diagnostics will + // get reported when the plan driver visits the ComponentConfig + // object. + return nil, diags + } + + providerSchemas, moreDiags, _ := neededProviderSchemas(ctx, main, PlanPhase, scope) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, diags + } + + // We're actually going to provide two sets of providers to Core + // for Stacks operations. + // + // First, we provide the basic set of factories here. These are used + // by Terraform Core to handle operations that require an + // unconfigured provider, such as cross-provider move operations and + // provider functions. The provider factories return the shared + // unconfigured client that stacks holds for the same reasons. The + // factories will lazily request the unconfigured clients here as + // they are requested by Terraform. + // + // Second, we provide provider clients that are already configured + // for any operations that require configured clients. This is + // because we want to provide the clients built using the provider + // configurations from the stack that exist outside of Terraform's + // concerns. There are provided directly in the PlanOpts argument. + + providerFactories := make(map[addrs.Provider]providers.Factory, len(providerSchemas)) + for addr := range providerSchemas { + providerFactories[addr] = func() (providers.Interface, error) { + // Lazily fetch the unconfigured client for the provider + // as and when we need it. + provider, err := main.ProviderType(addr).UnconfiguredClient() + if err != nil { + return nil, err + } + // this provider should only be used for selected operations + return stubs.OfflineProvider(provider), nil + } + } + + tfCtx, err := terraform.NewContext(&terraform.ContextOpts{ + Hooks: []terraform.Hook{ + &componentInstanceTerraformHook{ + ctx: ctx, + seq: seq, + hooks: hooksFromContext(ctx), + addr: addr, + }, + }, + Providers: providerFactories, + PreloadedProviderSchemas: providerSchemas, + Provisioners: main.availableProvisioners(), + }) + if err != nil { + // Should not get here because we should always pass a valid + // ContextOpts above. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to instantiate Terraform modules runtime", + fmt.Sprintf("Could not load the main Terraform language runtime: %s.\n\nThis is a bug in Terraform; please report it!", err), + )) + return nil, diags + } + + // When our given context is cancelled, we want to instruct the + // modules runtime to stop the running operation. We use this + // nested context to ensure that we don't leak a goroutine when the + // parent context isn't cancelled. + operationCtx, operationCancel := context.WithCancel(ctx) + defer operationCancel() + go func() { + <-operationCtx.Done() + if ctx.Err() == context.Canceled { + tfCtx.Stop() + } + }() + + plan, moreDiags := tfCtx.Plan(moduleTree, state, opts) + diags = diags.Append(moreDiags) + + if plan != nil { + cic := &hooks.ComponentInstanceChange{ + Addr: addr, + } + + for _, rsrcChange := range plan.DriftedResources { + hookMore(ctx, seq, h.ReportResourceInstanceDrift, &hooks.ResourceInstanceChange{ + Addr: stackaddrs.AbsResourceInstanceObject{ + Component: addr, + Item: rsrcChange.ObjectAddr(), + }, + Change: rsrcChange, + }) + } + for _, rsrcChange := range plan.Changes.Resources { + if rsrcChange.Importing != nil { + cic.Import++ + } + if rsrcChange.Moved() { + cic.Move++ + } + cic.CountNewAction(rsrcChange.Action) + + hookMore(ctx, seq, h.ReportResourceInstancePlanned, &hooks.ResourceInstanceChange{ + Addr: stackaddrs.AbsResourceInstanceObject{ + Component: addr, + Item: rsrcChange.ObjectAddr(), + }, + Change: rsrcChange, + }) + } + for _, rsrcChange := range plan.DeferredResources { + cic.Defer++ + hookMore(ctx, seq, h.ReportResourceInstanceDeferred, &hooks.DeferredResourceInstanceChange{ + Reason: rsrcChange.DeferredReason, + Change: &hooks.ResourceInstanceChange{ + Addr: stackaddrs.AbsResourceInstanceObject{ + Component: addr, + Item: rsrcChange.ChangeSrc.ObjectAddr(), + }, + Change: rsrcChange.ChangeSrc, + }, + }) + } + hookMore(ctx, seq, h.ReportComponentInstancePlanned, cic) + } + + if diags.HasErrors() { + hookMore(ctx, seq, h.ErrorComponentInstancePlan, addr) + } else { + if plan.Complete { + hookMore(ctx, seq, h.EndComponentInstancePlan, addr) + + } else { + hookMore(ctx, seq, h.DeferComponentInstancePlan, addr) + } + } + + return plan, diags +} diff --git a/internal/stacks/stackruntime/internal/stackeval/planning_test.go b/internal/stacks/stackruntime/internal/stackeval/planning_test.go new file mode 100644 index 0000000000..e032d6f0fd --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/planning_test.go @@ -0,0 +1,1064 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + providerTesting "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestPlanning_DestroyMode(t *testing.T) { + // This integration test aims to verify the overall problem of planning in + // destroy mode, which has some special exceptions to deal with the fact + // that downstream components need to plan against the _current_ outputs of + // other component instances they depend on, rather than the _planned_ + // outputs which would necessarily be missing in a full-destroy situation. + // + // This behavior differs from other planning modes because when applying + // destroys we work in reverse dependency order, destroying the dependent + // before we destroy the dependency, and therefore the downstream destroy + // action happens _before_ the upstream has been destroyed, when its prior + // state outputs are still available.) + + cfg := testStackConfig(t, "planning", "plan_destroy") + aComponentInstAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "a", + }, + }, + } + aResourceInstAddr := stackaddrs.AbsResourceInstance{ + Component: aComponentInstAddr, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }, + }, + }, + } + bComponentInstAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "b", + }, + }, + } + bResourceInstAddr := stackaddrs.AbsResourceInstance{ + Component: bComponentInstAddr, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }, + }, + }, + } + providerAddr := addrs.NewBuiltInProvider("test") + providerInstAddr := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: providerAddr, + } + priorState := testPriorState(t, map[string]protoreflect.ProtoMessage{ + statekeys.String(statekeys.ComponentInstance{ + ComponentInstanceAddr: aComponentInstAddr, + }): &tfstackdata1.StateComponentInstanceV1{ + DependentAddrs: []string{"component.b"}, + OutputValues: map[string]*tfstackdata1.DynamicValue{ + "result": mustPlanDynamicValue(t, cty.StringVal(`result for "a" from prior state`)), + }, + }, + + statekeys.String(statekeys.ComponentInstance{ + ComponentInstanceAddr: bComponentInstAddr, + }): &tfstackdata1.StateComponentInstanceV1{ + DependencyAddrs: []string{"component.a"}, + OutputValues: map[string]*tfstackdata1.DynamicValue{ + "result": mustPlanDynamicValue(t, cty.StringVal(`result for "b" from prior state`)), + }, + }, + + statekeys.String(statekeys.ResourceInstanceObject{ + ResourceInstance: aResourceInstAddr, + }): &tfstackdata1.StateResourceInstanceObjectV1{ + Status: tfstackdata1.StateResourceInstanceObjectV1_READY, + ProviderConfigAddr: providerInstAddr.String(), + ValueJson: []byte(` + { + "for_module": "a", + "arg": null, + "result": "result for \"a\" from prior state" + } + `), + }, + + statekeys.String(statekeys.ResourceInstanceObject{ + ResourceInstance: bResourceInstAddr, + }): &tfstackdata1.StateResourceInstanceObjectV1{ + Status: tfstackdata1.StateResourceInstanceObjectV1_READY, + ProviderConfigAddr: providerInstAddr.String(), + ValueJson: []byte(` + { + "for_module": "b", + "arg": "result for \"a\" from prior state", + "result": "result for \"b\" from prior state" + } + `), + }, + }) + + resourceTypeSchema := &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "for_module": { + Type: cty.String, + Required: true, + }, + "arg": { + Type: cty.String, + Optional: true, + }, + "result": { + Type: cty.String, + Computed: true, + }, + }, + } + main := NewForPlanning(cfg, priorState, PlanOpts{ + PlanningMode: plans.DestroyMode, + PlanTimestamp: time.Now().UTC(), + ProviderFactories: ProviderFactories{ + addrs.NewBuiltInProvider("test"): func() (providers.Interface, error) { + return &providerTesting.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{}, + }, + ResourceTypes: map[string]providers.Schema{ + "test": { + Body: resourceTypeSchema, + }, + }, + ServerCapabilities: providers.ServerCapabilities{ + PlanDestroy: true, + }, + }, + ConfigureProviderFn: func(cpr providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + t.Logf("configuring the provider: %#v", cpr.Config) + return providers.ConfigureProviderResponse{} + }, + ReadResourceFn: func(rrr providers.ReadResourceRequest) providers.ReadResourceResponse { + forModule := rrr.PriorState.GetAttr("for_module").AsString() + t.Logf("refreshing for_module = %q", forModule) + arg := rrr.PriorState.GetAttr("arg") + var result string + if !arg.IsNull() { + argStr := arg.AsString() + result = fmt.Sprintf("result for %q refreshed with %q", forModule, argStr) + } else { + result = fmt.Sprintf("result for %q refreshed without arg", forModule) + } + + return providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "for_module": cty.StringVal(forModule), + "arg": arg, + "result": cty.StringVal(result), + }), + } + }, + PlanResourceChangeFn: func(prcr providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + if prcr.ProposedNewState.IsNull() { + // We're destroying then, which is what we expect. + forModule := prcr.PriorState.GetAttr("for_module").AsString() + t.Logf("planning destroy for_module = %q", forModule) + return providers.PlanResourceChangeResponse{ + PlannedState: prcr.ProposedNewState, + } + } + + // Although we're planning for destroy, as an + // implementation detail the modules runtime uses a + // normal planning pass to get the prior state updated + // before doing the real destroy plan, and then + // discards the result. + + forModule := prcr.ProposedNewState.GetAttr("for_module").AsString() + t.Logf("planning non-destroy for_module = %q (should be ignored by the modules runtime)", forModule) + + arg := prcr.ProposedNewState.GetAttr("arg") + var result string + if !arg.IsNull() { + argStr := arg.AsString() + result = fmt.Sprintf("result for %q planned with %q", forModule, argStr) + } else { + result = fmt.Sprintf("result for %q planned without arg", forModule) + } + + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "for_module": cty.StringVal(forModule), + "arg": arg, + "result": cty.StringVal(result), + }), + } + }, + }, nil + }, + }, + }) + + plan, diags := testPlan(t, main) + assertNoDiagnostics(t, diags) + + aCmpPlan := plan.GetComponent(aComponentInstAddr) + bCmpPlan := plan.GetComponent(bComponentInstAddr) + if aCmpPlan == nil || bCmpPlan == nil { + t.Fatalf( + "incomplete plan\n%s: %#v\n%s: %#v", + aComponentInstAddr, aCmpPlan, + bComponentInstAddr, bCmpPlan, + ) + } + + aPlan, err := aCmpPlan.ForModulesRuntime() + if err != nil { + t.Fatalf("inconsistent plan for %s: %s", aComponentInstAddr, err) + } + bPlan, err := bCmpPlan.ForModulesRuntime() + if err != nil { + t.Fatalf("inconsistent plan for %s: %s", bComponentInstAddr, err) + } + + if planSrc := aPlan.Changes.ResourceInstance(aResourceInstAddr.Item); planSrc != nil { + rAddr := aResourceInstAddr + plan, err := planSrc.Decode(providers.Schema{ + Body: resourceTypeSchema, + }) + if err != nil { + t.Fatalf("can't decode change for %s: %s", rAddr, err) + } + if got, want := plan.Action, plans.Delete; got != want { + t.Errorf("wrong action for %s\ngot: %s\nwant: %s", rAddr, got, want) + } + if !plan.After.IsNull() { + t.Errorf("unexpected non-nil 'after' value for %s: %#v", rAddr, plan.After) + } + wantBefore := cty.ObjectVal(map[string]cty.Value{ + "arg": cty.NullVal(cty.String), + "for_module": cty.StringVal("a"), + "result": cty.StringVal(`result for "a" refreshed without arg`), + }) + if !wantBefore.RawEquals(plan.Before) { + t.Errorf("wrong before value for %s\ngot: %#v\nwant: %#v", rAddr, plan.Before, wantBefore) + } + } else { + t.Errorf("no plan for %s", aResourceInstAddr) + } + + if planSrc := bPlan.Changes.ResourceInstance(bResourceInstAddr.Item); planSrc != nil { + rAddr := bResourceInstAddr + plan, err := planSrc.Decode(providers.Schema{ + Body: resourceTypeSchema, + }) + if err != nil { + t.Fatalf("can't decode change for %s: %s", rAddr, err) + } + if got, want := plan.Action, plans.Delete; got != want { + t.Errorf("wrong action for %s\ngot: %s\nwant: %s", rAddr, got, want) + } + if !plan.After.IsNull() { + t.Errorf("unexpected non-nil 'after' value for %s: %#v", rAddr, plan.After) + } + wantBefore := cty.ObjectVal(map[string]cty.Value{ + // FIXME: The modules runtime has a long-standing historical quirk + // that it not-so-secretly does a full normal plan before it runs + // the destroy plan, as its way to get the prior state updated. + // + // Unfortunately, that means that the output values in the prior + // state end up not reflecting the refreshed state properly, + // and that's why the below says that "a" came from the prior state. + // This quirk only really matters if there has been a significant + // change in the remote system that needs to be considered for + // destroy to be successful, which thankfully isn't true _often_ + // but does happen somtimes, and so we should find a way to fix + // the modules runtime to produce its output values based on the + // refreshed state instead of the prior state. Perhaps using + // a refresh-only plan instead of a normal plan would do it? + // + // Once the quirk in the modules runtime is fixed, "arg" below + // (and the copy of it embedded in "result") should become: + // `result for "a" refreshed without arg` + "arg": cty.StringVal(`result for "a" from prior state`), + "for_module": cty.StringVal("b"), + + // If this appears as `result for "b" refreshed without arg` after + // future maintenence, then that suggests that the special case + // for destroy mode in ComponentInstance.ResultValue is no longer + // working correctly. Propagating the new desired state instead + // of the prior state will cause the "a" value to be null, and + // therefore "arg" on this resource instance would also be null. + "result": cty.StringVal(`result for "b" refreshed with "result for \"a\" from prior state"`), + }) + if !wantBefore.RawEquals(plan.Before) { + t.Errorf("wrong before value for %s\ngot: %#v\nwant: %#v", rAddr, plan.Before, wantBefore) + } + } else { + t.Errorf("no plan for %s", bResourceInstAddr) + } +} + +func TestPlanning_RequiredComponents(t *testing.T) { + // This test acts both as some unit tests for the component requirement + // analysis of various different object types and as an integration test + // for the overall component dependency analysis during the plan phase, + // ensuring that the dependency graph is reflected correctly in the + // resulting plan. + + cfg := testStackConfig(t, "planning", "required_components") + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + ProviderFactories: ProviderFactories{ + addrs.NewBuiltInProvider("foo"): func() (providers.Interface, error) { + return &providerTesting.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "in": { + Type: cty.Map(cty.String), + Optional: true, + }, + }, + }, + }, + }, + ConfigureProviderFn: func(cpr providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + t.Logf("configuring the provider: %#v", cpr.Config) + return providers.ConfigureProviderResponse{} + }, + }, nil + }, + }, + PlanTimestamp: time.Now().UTC(), + }) + + cmpA := stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{Name: "a"}, + } + cmpB := stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{Name: "b"}, + } + cmpC := stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{Name: "c"}, + } + + cmpOpts := collections.CmpOptions + + t.Run("integrated", func(t *testing.T) { + // This integration tests runs a full plan of the test configuration + // and checks that the resulting plan contains the expected component + // dependency information, without concern for exactly how that + // information got populated. + // + // The other subtests below check that the individual objects + // participating in this plan are reporting their own component + // dependencies correctly, and so if this integrated test fails + // then the simultaneous failure of one of those other tests might be + // a good clue as to what's broken. + + plan, diags := testPlan(t, main) + assertNoDiagnostics(t, diags) + + tests := []struct { + component stackaddrs.AbsComponent + wantDependencies []stackaddrs.AbsComponent + wantDependents []stackaddrs.AbsComponent + }{ + { + component: cmpA, + wantDependencies: []stackaddrs.AbsComponent{}, + wantDependents: []stackaddrs.AbsComponent{ + cmpB, + cmpC, + }, + }, + { + component: cmpB, + wantDependencies: []stackaddrs.AbsComponent{ + cmpA, + }, + wantDependents: []stackaddrs.AbsComponent{ + cmpC, + }, + }, + { + component: cmpC, + wantDependencies: []stackaddrs.AbsComponent{ + cmpA, + cmpB, + }, + wantDependents: []stackaddrs.AbsComponent{}, + }, + } + for _, test := range tests { + t.Run(test.component.String(), func(t *testing.T) { + instAddr := stackaddrs.AbsComponentInstance{ + Stack: test.component.Stack, + Item: stackaddrs.ComponentInstance{ + Component: test.component.Item, + }, + } + cp := plan.GetComponent(instAddr) + { + got := cp.Dependencies + want := collections.NewSet[stackaddrs.AbsComponent]() + want.Add(test.wantDependencies...) + if diff := cmp.Diff(want, got, cmpOpts); diff != "" { + t.Errorf("wrong dependencies\n%s", diff) + } + } + { + got := cp.Dependents + want := collections.NewSet[stackaddrs.AbsComponent]() + want.Add(test.wantDependents...) + if diff := cmp.Diff(want, got, cmpOpts); diff != "" { + t.Errorf("wrong dependents\n%s", diff) + } + } + }) + } + }) + + t.Run("component dependents", func(t *testing.T) { + ctx := context.Background() + promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + tests := []struct { + componentAddr stackaddrs.AbsComponent + wantDependencies []stackaddrs.AbsComponent + }{ + { + cmpA, + []stackaddrs.AbsComponent{}, + }, + { + cmpB, + []stackaddrs.AbsComponent{ + cmpA, + }, + }, + { + cmpC, + []stackaddrs.AbsComponent{ + cmpA, + cmpB, + }, + }, + } + + for _, test := range tests { + t.Run(test.componentAddr.String(), func(t *testing.T) { + stack := main.Stack(ctx, test.componentAddr.Stack, PlanPhase) + if stack == nil { + t.Fatalf("no declaration for %s", test.componentAddr.Stack) + } + component := stack.Component(test.componentAddr.Item) + if component == nil { + t.Fatalf("no declaration for %s", test.componentAddr) + } + + got := component.RequiredComponents(ctx) + want := collections.NewSet[stackaddrs.AbsComponent]() + want.Add(test.wantDependencies...) + + if diff := cmp.Diff(want, got, cmpOpts); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } + + return struct{}{}, nil + }) + }) +} + +func TestPlanning_DeferredChangesPropagation(t *testing.T) { + // This test arranges for one component's plan to signal deferred changes, + // and checks that a downstream component's plan also has everything + // deferred even though it could potentially have been plannable in + // isolation, since we need to respect the dependency ordering between + // components. + + cfg := testStackConfig(t, "planning", "deferred_changes_propagation") + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + PlanTimestamp: time.Now().UTC(), + InputVariableValues: map[stackaddrs.InputVariable]ExternalInputValue{ + // This causes the first component to have a module whose + // instance count isn't known yet. + {Name: "first_count"}: { + Value: cty.UnknownVal(cty.Number), + }, + }, + ProviderFactories: ProviderFactories{ + addrs.NewBuiltInProvider("test"): func() (providers.Interface, error) { + return &providerTesting.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{}, + }, + ResourceTypes: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{}, + }, + }, + }, + }, nil + }, + }, + }) + + componentFirstInstAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "first", + }, + }, + } + componentSecondInstAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "second", + }, + }, + } + + componentPlanResourceActions := func(plan *stackplan.Component) map[string]plans.Action { + ret := make(map[string]plans.Action) + for _, elem := range plan.ResourceInstancePlanned.Elems { + ret[elem.Key.String()] = elem.Value.Action + } + return ret + } + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + plan, diags := testPlan(t, main) + assertNoErrors(t, diags) + + firstPlan := plan.GetComponent(componentFirstInstAddr) + if firstPlan.PlanComplete { + t.Error("first component has a complete plan; should be incomplete because it has deferred actions") + } + secondPlan := plan.GetComponent(componentSecondInstAddr) + if secondPlan.PlanComplete { + t.Error("second component has a complete plan; should be incomplete because everything in it should've been deferred") + } + + gotFirstActions := componentPlanResourceActions(firstPlan) + wantFirstActions := map[string]plans.Action{ + // Only test.a is planned, because test.b has unknown count + // and must therefore be deferred. + "test.a": plans.Create, + } + gotSecondActions := componentPlanResourceActions(secondPlan) + wantSecondActions := map[string]plans.Action{ + // Nothing at all expected for the second, because all of its + // planned actions should've been deferred to respect the + // dependency on the first component. + } + + if diff := cmp.Diff(wantFirstActions, gotFirstActions); diff != "" { + t.Errorf("wrong actions for first component\n%s", diff) + } + if diff := cmp.Diff(wantSecondActions, gotSecondActions); diff != "" { + t.Errorf("wrong actions for second component\n%s", diff) + } + }) +} + +func TestPlanning_RemoveDataResource(t *testing.T) { + // This test is here because there was a historical bug where we'd generate + // an invalid plan (unparsable) whenever the plan included deletion of + // a previously-declared data resource, where the provider configuration + // address would not be populated correctly. + // + // Therefore this test is narrowly focused on that specific situation. + // Anything else it's exercising as a side-effect is not crucial for + // this test in particular, although of course unrelated regressions might + // still be important in some other way beyond this test's scope. + + providerFactories := map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("test"): func() (providers.Interface, error) { + return &providerTesting.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{}, + }, + }, + }, + ReadDataSourceFn: func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.EmptyObjectVal, + } + }, + }, nil + }, + } + objAddr := stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "main"}, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "test", + Name: "test", + }, + }, + }, + DeposedKey: addrs.NotDeposed, + }, + } + + var state *stackstate.State + + // Round 1: data.test.test is present inside component.main + { + ctx := context.Background() + cfg := testStackConfig(t, "planning", "remove_data_resource/step1") + + // Plan + rawPlan, err := promising.MainTask(ctx, func(ctx context.Context) ([]*anypb.Any, error) { + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + ProviderFactories: providerFactories, + PlanTimestamp: time.Now().UTC(), + }) + outp, outpTest := testPlanOutput(t) + main.PlanAll(ctx, outp) + rawPlan := outpTest.RawChanges(t) + _, diags := outpTest.Close(t) + assertNoDiagnostics(t, diags) + return rawPlan, nil + }) + if err != nil { + t.Fatal(err) + } + plan, err := stackplan.LoadFromProto(rawPlan) + if err != nil { + t.Fatal(err) + } + + // Apply + newState, err := promising.MainTask(ctx, func(ctx context.Context) (*stackstate.State, error) { + outp, outpTest := testApplyOutput(t, nil) + _, err := ApplyPlan(ctx, cfg, plan, ApplyOpts{ + ProviderFactories: providerFactories, + }, outp) + if err != nil { + t.Fatal(err) + } + state, diags := outpTest.Close(t) + assertNoDiagnostics(t, diags) + + // This test is only valid if the data resource instance is actually + // tracked in the state. + obj := state.ResourceInstanceObjectSrc(objAddr) + if obj == nil { + t.Fatalf("data.test.test is not in the final state for round 1") + } + + return state, nil + }) + if err != nil { + t.Fatal(err) + } + + // We'll use the new state as the input for the next round. + state = newState + } + + // Round 2: data.test.test has its remnant left in the prior state, but + // it's no longer present in the configuration. + { + ctx := context.Background() + cfg := testStackConfig(t, "planning", "remove_data_resource/step2") + + // Plan + type Plans struct { + Nice *stackplan.Plan + Raw []*anypb.Any + } + plan, err := promising.MainTask(ctx, func(ctx context.Context) (*stackplan.Plan, error) { + main := NewForPlanning(cfg, state, PlanOpts{ + PlanningMode: plans.NormalMode, + ProviderFactories: providerFactories, + PlanTimestamp: time.Now().UTC(), + }) + outp, outpTest := testPlanOutput(t) + main.PlanAll(ctx, outp) + // The original bug would occur at this point, because + // outpTest.Close attempts to parse the raw plan, which fails if + // any part of that structure is not syntactically valid. + plan, diags := outpTest.Close(t) + assertNoDiagnostics(t, diags) + return plan, nil + }) + if err != nil { + t.Fatal(err) + } + + // We'll check whether the data resource even appears in the plan, + // because if not then this test is no longer testing what it thinks + // it's testing and should probably be revised. + // + // (That doesn't necessarily mean that any new behavior is wrong: if + // plan at all anymore then we can update this test to agree with that.) + // + // Specifically we expect to have a prior state and a provider config + // address for this data resource, but no planned action because + // dropping a data resource from the state is not an "action" in the + // usual sense (it doesn't cause any calls to the provider). + mainPlan := plan.GetComponent(stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "main"}, + }, + }) + if mainPlan == nil { + t.Fatalf("main component not appear in the plan at all") + } + riAddr := objAddr.Item + _, ok := mainPlan.ResourceInstancePriorState.GetOk(riAddr) + if !ok { + t.Fatalf("data resource instance does not appear in the prior state at all") + } + providerConfig, ok := mainPlan.ResourceInstanceProviderConfig.GetOk(riAddr) + if !ok { + t.Fatalf("data resource instance does not have a provider config in the plan") + } + if got, want := providerConfig.Provider, addrs.NewBuiltInProvider("test"); got != want { + t.Errorf("wrong provider configuration address\ngot: %s\nwant: %s", got, want) + } + + // For good measure we'll also apply this new plan, to make sure that + // we're left with no remnant of the data resource in the updated state. + newState, err := promising.MainTask(ctx, func(ctx context.Context) (*stackstate.State, error) { + outp, outpTest := testApplyOutput(t, nil) + _, err := ApplyPlan(ctx, cfg, plan, ApplyOpts{ + ProviderFactories: providerFactories, + }, outp) + if err != nil { + t.Fatal(err) + } + state, diags := outpTest.Close(t) + assertNoDiagnostics(t, diags) + + return state, nil + }) + if err != nil { + t.Fatal(err) + } + + state = newState + } + + // Our final state should not include the data resource at all. + objState := state.ResourceInstanceObjectSrc(objAddr) + if objState != nil { + t.Errorf("%s is still in the state after it should've been dropped", objAddr) + } +} + +func TestPlanning_PathValues(t *testing.T) { + cfg := testStackConfig(t, "planning", "path_values") + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + PlanTimestamp: time.Now().UTC(), + }) + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + plan, diags := testPlan(t, main) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + component := plan.GetComponent(stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "path_values", + }, + Key: addrs.NoKey, + }, + }) + if component == nil { + t.Fatalf("component not found in plan") + } + + cwd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get current working directory: %s", err) + } + + normalizePath := func(path string) string { + rel, err := filepath.Rel(cwd, path) + if err != nil { + t.Errorf("rel(%s,%s): %s", cwd, path, err) + return path + } + return rel + } + + expected := map[string]string{ + "cwd": ".", + "root": "testdata/sourcebundle/planning/path_values/module", // this is the root module of the component + "module": "testdata/sourcebundle/planning/path_values/module", // this is the root module + "child_root": "testdata/sourcebundle/planning/path_values/module", // should be the same for all modules + "child_module": "testdata/sourcebundle/planning/path_values/module/child", // this is the child module + } + + actual := map[string]string{ + "cwd": normalizePath(component.PlannedOutputValues[addrs.OutputValue{Name: "cwd"}].AsString()), + "root": normalizePath(component.PlannedOutputValues[addrs.OutputValue{Name: "root"}].AsString()), + "module": normalizePath(component.PlannedOutputValues[addrs.OutputValue{Name: "module"}].AsString()), + "child_root": normalizePath(component.PlannedOutputValues[addrs.OutputValue{Name: "child_root"}].AsString()), + "child_module": normalizePath(component.PlannedOutputValues[addrs.OutputValue{Name: "child_module"}].AsString()), + } + + if cmp.Diff(expected, actual) != "" { + t.Fatalf("unexpected path values\n%s", cmp.Diff(expected, actual)) + } + }) +} + +func TestPlanning_NoWorkspaceNameRef(t *testing.T) { + // This test verifies that a reference to terraform.workspace is treated + // as invalid for modules used in a stacks context, because there's + // no comparable single string to use in stacks context and we expect + // modules used in stack components to vary declarations based only + // on their input variables. + // + // (If something needs to vary between stack deployments then that's + // a good candidate for an input variable on the root stack configuration, + // set differently for each deployment, and then passed in to the + // components that need it.) + + cfg := testStackConfig(t, "planning", "no_workspace_name_ref") + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + }) + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + _, diags := testPlan(t, main) + if !diags.HasErrors() { + t.Fatal("success; want error about invalid terraform.workspace reference") + } + + // At least one of the diagnostics must mention the terraform.workspace + // attribute in its detail. + seenRelevantDiag := false + for _, diag := range diags { + if diag.Severity() != tfdiags.Error { + continue + } + if strings.Contains(diag.Description().Detail, "terraform.workspace") { + seenRelevantDiag = true + break + } + } + if !seenRelevantDiag { + t.Fatalf("none of the error diagnostics mentions terraform.workspace\n%s", spew.Sdump(diags.ForRPC())) + } + }) +} + +func TestPlanning_Locals(t *testing.T) { + cfg := testStackConfig(t, "local_value", "basics") + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + }) + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + _, diags := testPlan(t, main) + if diags.HasErrors() { + t.Fatalf("errors encountered\n%s", spew.Sdump(diags.ForRPC())) + } + }) +} + +func TestPlanning_LocalsDataSource(t *testing.T) { + ctx := context.Background() + cfg := testStackConfig(t, "local_value", "custom_provider") + providerFactories := map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + provider := stacks_testing_provider.NewProvider(t) + return provider, nil + }, + } + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + comp2Addr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "child2"}, + }, + } + + rawPlan, err := promising.MainTask(ctx, func(ctx context.Context) ([]*anypb.Any, error) { + outp, outpTest := testPlanOutput(t) + main := NewForPlanning(cfg, stackstate.NewState(), PlanOpts{ + PlanningMode: plans.NormalMode, + ProviderFactories: providerFactories, + DependencyLocks: *lock, + PlanTimestamp: time.Now().UTC(), + }) + main.PlanAll(ctx, outp) + defer main.DoCleanup(ctx) + rawPlan := outpTest.RawChanges(t) + _, diags := outpTest.Close(t) + assertNoDiagnostics(t, diags) + return rawPlan, nil + }) + + if err != nil { + t.Fatal(err) + } + + plan, err := stackplan.LoadFromProto(rawPlan) + if err != nil { + t.Fatal(err) + } + + _, err = promising.MainTask(ctx, func(ctx context.Context) (*stackstate.State, error) { + outp, outpTest := testApplyOutput(t, nil) + main, err := ApplyPlan(ctx, cfg, plan, ApplyOpts{ + ProviderFactories: providerFactories, + DependencyLocks: *lock, + }, outp) + if main != nil { + defer main.DoCleanup(ctx) + } + if err != nil { + t.Fatal(err) + } + state, diags := outpTest.Close(t) + applies := outpTest.AppliedChanges() + for _, apply := range applies { + switch v := apply.(type) { + case *stackstate.AppliedChangeComponentInstance: + if v.ComponentAddr.Item.Name == comp2Addr.Item.Component.Name { + stringKey := addrs.OutputValue{ + Name: "bar", + } + listKey := addrs.OutputValue{ + Name: "list", + } + mapKey := addrs.OutputValue{ + Name: "map", + } + + stringOutput := v.OutputValues[stringKey] + listOutput := v.OutputValues[listKey].AsValueSlice() + mapOutput := v.OutputValues[mapKey].AsValueMap() + + expectedString := cty.StringVal("through-local-aloha-foo-foo") + expectedList := []cty.Value{ + cty.StringVal("through-local-aloha-foo"), + cty.StringVal("foo")} + + expectedMap := map[string]cty.Value{ + "key": cty.StringVal("through-local-aloha-foo"), + "value": cty.StringVal("foo"), + } + + if cmp.Diff(stringOutput, expectedString, ctydebug.CmpOptions) != "" { + t.Fatalf("string output is wrong, expected %q", expectedString.AsString()) + } + + if cmp.Diff(listOutput, expectedList, ctydebug.CmpOptions) != "" { + t.Fatalf("list output is wrong, expected \n%+v,\ngot\n%+v", expectedList, listOutput) + } + + if cmp.Diff(mapOutput, expectedMap, ctydebug.CmpOptions) != "" { + t.Fatalf("map output is wrong, expected \n%+v,\ngot\n%+v", expectedMap, mapOutput) + } + } + default: + break + } + } + assertNoDiagnostics(t, diags) + + return state, nil + }) + + if err != nil { + t.Fatal(err) + } +} + +func mustPlanDynamicValue(t *testing.T, v cty.Value) *tfstackdata1.DynamicValue { + ret, err := stacks.ToDynamicValue(v, cty.DynamicPseudoType) + if err != nil { + t.Fatal(err) + } + return tfstackdata1.Terraform1ToStackDataDynamicValue(ret) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider.go b/internal/stacks/stackruntime/internal/stackeval/provider.go new file mode 100644 index 0000000000..d62085c54d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider.go @@ -0,0 +1,244 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Provider represents a provider configuration in a particular stack config. +type Provider struct { + addr stackaddrs.AbsProviderConfig + config *ProviderConfig + stack *Stack + + main *Main + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*ProviderInstance]]]] +} + +func newProvider(main *Main, addr stackaddrs.AbsProviderConfig, stack *Stack, config *ProviderConfig) *Provider { + return &Provider{ + addr: addr, + stack: stack, + config: config, + main: main, + } +} + +func (p *Provider) ProviderType() *ProviderType { + return p.main.ProviderType(p.addr.Item.Provider) +} + +// InstRefValueType returns the type of any values that represent references to +// instances of this provider configuration. +// +// All configurations for the same provider share the same type. +func (p *Provider) InstRefValueType() cty.Type { + decl := p.config.config + return providerInstanceRefType(decl.ProviderAddr) +} + +// ForEachValue returns the result of evaluating the "for_each" expression +// for this provider configuration, with the following exceptions: +// - If the provider config doesn't use "for_each" at all, returns [cty.NilVal]. +// - If the for_each expression is present but too invalid to evaluate, +// returns [cty.DynamicVal] to represent that the for_each value cannot +// be determined. +// +// A present and valid "for_each" expression produces a result that's +// guaranteed to be: +// - Either a set of strings, a map of any element type, or an object type +// - Known and not null (only the top-level value) +// - Not sensitive (only the top-level value) +func (p *Provider) ForEachValue(ctx context.Context, phase EvalPhase) cty.Value { + ret, _ := p.CheckForEachValue(ctx, phase) + return ret +} + +// CheckForEachValue evaluates the "for_each" expression if present, validates +// that its value is valid, and then returns that value. +// +// If this call does not use "for_each" then this immediately returns cty.NilVal +// representing the absense of the value. +// +// If the diagnostics does not include errors and the result is not cty.NilVal +// then callers can assume that the result value will be: +// - Either a set of strings, a map of any element type, or an object type +// - Known and not null (except for nested map/object element values) +// - Not sensitive (only the top-level value) +// +// If the diagnostics _does_ include errors then the result might be +// [cty.DynamicVal], which represents that the for_each expression was so invalid +// that we cannot know the for_each value. +func (p *Provider) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + val, diags := doOnceWithDiags( + ctx, p.tracingName()+" for_each", p.forEachValue.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + cfg := p.config.config + + switch { + + case cfg.ForEach != nil: + result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, p.stack, "provider") + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + return result.Value, diags + + default: + // This stack config doesn't use for_each at all + return cty.NilVal, diags + } + }, + ) + if val == cty.NilVal && diags.HasErrors() { + // We use cty.DynamicVal as the placeholder for an invalid for_each, + // to represent "unknown for_each value" as distinct from "no for_each + // expression at all". + val = cty.DynamicVal + } + return val, diags +} + +// Instances returns all of the instances of the provider config known to be +// declared by the configuration. +// +// Calcluating this involves evaluating the call's for_each expression if any, +// and so this call may block on evaluation of other objects in the +// configuration. +// +// If the configuration has an invalid definition of the instances then the +// result will be nil. Callers that need to distinguish between invalid +// definitions and valid definitions of zero instances can rely on the +// result being a non-nil zero-length map in the latter case. +// +// This function doesn't return any diagnostics describing ways in which the +// for_each expression is invalid because we assume that the main plan walk +// will visit the stack call directly and ask it to check itself, and that +// call will be the one responsible for returning any diagnostics. +func (p *Provider) Instances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ProviderInstance, bool) { + ret, unknown, _ := p.CheckInstances(ctx, phase) + return ret, unknown +} + +func (p *Provider) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ProviderInstance, bool, tfdiags.Diagnostics) { + result, diags := doOnceWithDiags( + ctx, p.tracingName()+" instances", p.instances.For(phase), + func(ctx context.Context) (instancesResult[*ProviderInstance], tfdiags.Diagnostics) { + forEachVal, diags := p.CheckForEachValue(ctx, phase) + if diags.HasErrors() { + return instancesResult[*ProviderInstance]{}, diags + } + + return instancesMap(forEachVal, func(ik addrs.InstanceKey, rd instances.RepetitionData) *ProviderInstance { + return newProviderInstance(p, stackaddrs.AbsProviderToInstance(p.addr, ik), rd) + }), diags + }, + ) + return result.insts, result.unknown, diags +} + +// ExprReferenceValue implements Referenceable, returning a value containing +// one or more values that act as references to instances of the provider. +func (p *Provider) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + decl := p.config.config + insts, unknown := p.Instances(ctx, phase) + refType := p.InstRefValueType() + + switch { + case decl.ForEach != nil: + if unknown { + return cty.UnknownVal(cty.Map(refType)) + } + + if insts == nil { + // Then we errored during instance calculation, this should have + // been caught before we got here. + return cty.NilVal + } + elems := make(map[string]cty.Value, len(insts)) + for instKey := range insts { + k, ok := instKey.(addrs.StringKey) + if !ok { + panic(fmt.Sprintf("provider config with for_each has invalid instance key of type %T", instKey)) + } + elems[string(k)] = cty.CapsuleVal(refType, &stackaddrs.AbsProviderConfigInstance{ + Stack: p.addr.Stack, + Item: stackaddrs.ProviderConfigInstance{ + ProviderConfig: p.addr.Item, + Key: instKey, + }, + }) + } + if len(elems) == 0 { + return cty.MapValEmpty(refType) + } + return cty.MapVal(elems) + default: + if insts == nil { + return cty.UnknownVal(refType) + } + return cty.CapsuleVal(refType, &stackaddrs.AbsProviderConfigInstance{ + Stack: p.addr.Stack, + Item: stackaddrs.ProviderConfigInstance{ + ProviderConfig: p.addr.Item, + Key: addrs.NoKey, + }, + }) + } +} + +func (p *Provider) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, moreDiags := p.CheckForEachValue(ctx, phase) + diags = diags.Append(moreDiags) + _, _, moreDiags = p.CheckInstances(ctx, phase) + diags = diags.Append(moreDiags) + // Everything else is instance-specific and so the plan walk driver must + // call p.Instances and ask each instance to plan itself. + + return diags +} + +// PlanChanges implements Plannable. +func (p *Provider) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, p.checkValid(ctx, PlanPhase) +} + +// References implements Referrer +func (p *Provider) References(ctx context.Context) []stackaddrs.AbsReference { + cfg := p.config.config + var ret []stackaddrs.Reference + ret = append(ret, ReferencesInExpr(cfg.ForEach)...) + if schema, err := p.ProviderType().Schema(ctx); err == nil { + ret = append(ret, ReferencesInBody(cfg.Config, schema.Provider.Body.DecoderSpec())...) + } + return makeReferencesAbsolute(ret, p.addr.Stack) +} + +// CheckApply implements ApplyChecker. +func (p *Provider) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + return nil, p.checkValid(ctx, ApplyPhase) +} + +// tracingName implements Plannable. +func (p *Provider) tracingName() string { + return p.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config.go b/internal/stacks/stackruntime/internal/stackeval/provider_config.go new file mode 100644 index 0000000000..ff8a7915ce --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config.go @@ -0,0 +1,262 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ProviderConfig represents a single "provider" block in a stack configuration. +type ProviderConfig struct { + addr stackaddrs.ConfigProviderConfig + config *stackconfig.ProviderConfig + stack *StackConfig + + main *Main + + providerArgs perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +func newProviderConfig(main *Main, addr stackaddrs.ConfigProviderConfig, stack *StackConfig, config *stackconfig.ProviderConfig) *ProviderConfig { + return &ProviderConfig{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +func (p *ProviderConfig) ProviderType() *ProviderType { + return p.main.ProviderType(p.addr.Item.Provider) +} + +func (p *ProviderConfig) InstRefValueType() cty.Type { + decl := p.config + return providerInstanceRefType(decl.ProviderAddr) +} + +func (p *ProviderConfig) ProviderArgsDecoderSpec(ctx context.Context) (hcldec.Spec, error) { + providerType := p.ProviderType() + schema, err := providerType.Schema(ctx) + if err != nil { + return nil, err + } + if schema.Provider.Body == nil { + return hcldec.ObjectSpec{}, nil + } + return schema.Provider.Body.DecoderSpec(), nil +} + +// ProviderArgs returns an object value representing an approximation of all +// provider instances declared by this provider configuration, or +// an unknown value (possibly [cty.DynamicVal]) if the configuration is too +// invalid to produce any answer at all. +func (p *ProviderConfig) ProviderArgs(ctx context.Context, phase EvalPhase) cty.Value { + v, _ := p.CheckProviderArgs(ctx, phase) + return v +} + +func CheckProviderInLockfile(locks depsfile.Locks, providerType *ProviderType, declRange *hcl.Range) (diags tfdiags.Diagnostics) { + if !depsfile.ProviderIsLockable(providerType.Addr()) { + return diags + } + + if p := locks.Provider(providerType.Addr()); p == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider missing from lockfile", + Detail: fmt.Sprintf( + "Provider %q is not in the lockfile. This provider must be in the lockfile to be used in the configuration. Please run `tfstacks providers lock` to update the lockfile and run this operation again with an updated configuration.", + providerType.Addr(), + ), + Subject: declRange, + }) + } + return diags +} + +func (p *ProviderConfig) CheckProviderArgs(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags( + ctx, p.tracingName(), p.providerArgs.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + providerType := p.ProviderType() + decl := p.config + + depLocks := p.main.DependencyLocks(phase) + if depLocks != nil { + // Check if the provider is in the lockfile, + // if it is not we can not read the provider schema + lockfileDiags := CheckProviderInLockfile(*depLocks, providerType, decl.DeclRange.ToHCL().Ptr()) + if lockfileDiags.HasErrors() { + return cty.DynamicVal, lockfileDiags + } + diags = diags.Append(lockfileDiags) + } + + spec, err := p.ProviderArgsDecoderSpec(ctx) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to read provider schema", + Detail: fmt.Sprintf( + "Error while reading the schema for %q: %s.", + providerType.Addr(), err, + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + client, err := providerType.UnconfiguredClient() + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to initialize provider", + Detail: fmt.Sprintf( + "Error initializing %q to validate %s: %s.", + providerType.Addr(), p.addr, err, + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return cty.UnknownVal(hcldec.ImpliedType(spec)), diags + } + + body := decl.Config + if body == nil { + // A provider with no configuration is valid (just means no + // attributes or blocks), but we need to pass an empty body to + // the evaluator to avoid a panic. + body = hcl.EmptyBody() + } + + configVal, moreDiags := EvalBody(ctx, body, spec, phase, p) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.UnknownVal(hcldec.ImpliedType(spec)), diags + } + // We unmark the config before making the RPC call, but will still + // return the original possibly-marked config if successful. + unmarkedConfigVal, _ := configVal.UnmarkDeep() + validateResp := client.ValidateProviderConfig(providers.ValidateProviderConfigRequest{ + Config: unmarkedConfigVal, + }) + diags = diags.Append(validateResp.Diagnostics) + if validateResp.Diagnostics.HasErrors() { + return cty.UnknownVal(hcldec.ImpliedType(spec)), diags + } + + return configVal, diags + }, + ) +} + +// ResolveExpressionReference implements ExpressionScope for the purposes +// of validating the static provider configuration before it has been expanded +// into multiple instances. +func (p *ProviderConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + repetition := instances.RepetitionData{} + if p.config.ForEach != nil { + // We're producing an approximation across all eventual instances + // of this call, so we'll set each.key and each.value to unknown + // values. + repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() + repetition.EachValue = cty.DynamicVal + } + ret, diags := p.stack.resolveExpressionReference(ctx, ref, nil, repetition) + + if _, ok := ret.(*ProviderConfig); ok { + // We can't reference other providers from anywhere inside a provider + // configuration block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", ref.Target.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + + return ret, diags +} + +// ExternalFunctions implements ExpressionScope. +func (p *ProviderConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return p.main.ProviderFunctions(ctx, p.stack) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (p *ProviderConfig) PlanTimestamp() time.Time { + return p.main.PlanTimestamp() +} + +// ExprReferenceValue implements Referenceable. +func (p *ProviderConfig) ExprReferenceValue(context.Context, EvalPhase) cty.Value { + // We don't say anything about the contents of a provider during the + // static evaluation phase. We still return the type of the provider so + // we can use it to verify type constraints, but we don't return any + // actual values. + if p.config.ForEach != nil { + return cty.UnknownVal(cty.Map(p.InstRefValueType())) + } + return cty.UnknownVal(p.InstRefValueType()) +} + +var providerInstanceRefTypes = map[addrs.Provider]cty.Type{} +var providerInstanceRefTypesMu sync.Mutex + +// providerInstanceRefType returns the singleton cty capsule type for a given +// provider source address, creating a new type if a particular source address +// was not requested before. +func providerInstanceRefType(sourceAddr addrs.Provider) cty.Type { + providerInstanceRefTypesMu.Lock() + defer providerInstanceRefTypesMu.Unlock() + + ret, ok := providerInstanceRefTypes[sourceAddr] + if ok { + return ret + } + providerInstanceRefTypes[sourceAddr] = stackconfigtypes.ProviderConfigType(sourceAddr) + return providerInstanceRefTypes[sourceAddr] +} + +func (p *ProviderConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + _, diags := p.CheckProviderArgs(ctx, phase) + return diags +} + +// Validate implements Validatable. +func (p *ProviderConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + return p.checkValid(ctx, ValidatePhase) +} + +// PlanChanges implements Plannable. +func (p *ProviderConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, p.checkValid(ctx, PlanPhase) +} + +// tracingName implements Validatable. +func (p *ProviderConfig) tracingName() string { + return p.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go b/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go new file mode 100644 index 0000000000..549fe62940 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_config_test.go @@ -0,0 +1,220 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestProviderConfig_CheckProviderArgs_EmptyConfig(t *testing.T) { + cfg := testStackConfig(t, "provider", "single_instance") + providerTypeAddr := addrs.NewBuiltInProvider("foo") + newMockProvider := func(t *testing.T) (*testing_provider.MockProvider, providers.Factory) { + t.Helper() + mockProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{}, + }, + ValidateProviderConfigFn: func(vpcr providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + if vpcr.Config.ContainsMarked() { + panic("config has marks") + } + var diags tfdiags.Diagnostics + if vpcr.Config.Type().HasAttribute("test") { + if vpcr.Config.GetAttr("test").RawEquals(cty.StringVal("invalid")) { + diags = diags.Append(fmt.Errorf("invalid value checked by provider itself")) + } + } + return providers.ValidateProviderConfigResponse{ + PreparedConfig: vpcr.Config, + Diagnostics: diags, + } + }, + } + providerFactory := providers.FactoryFixed(mockProvider) + return mockProvider, providerFactory + } + getProviderConfig := func(ctx context.Context, t *testing.T, main *Main) *ProviderConfig { + t.Helper() + mainStack := main.MainStack() + provider := mainStack.Provider(stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }) + if provider == nil { + t.Fatal("no provider.foo.bar is available") + } + return provider.config + } + + subtestInPromisingTask(t, "valid", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + config := getProviderConfig(ctx, t, main) + + want := cty.EmptyObjectVal + got, diags := config.CheckProviderArgs(ctx, InspectPhase) + assertNoDiags(t, diags) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if !mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was not called; should've been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.EmptyObjectVal, + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) +} + +func TestProviderConfig_CheckProviderArgs(t *testing.T) { + cfg := testStackConfig(t, "provider", "single_instance_configured") + providerTypeAddr := addrs.NewBuiltInProvider("foo") + newMockProvider := func(t *testing.T) (*testing_provider.MockProvider, providers.Factory) { + t.Helper() + mockProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "test": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + ValidateProviderConfigFn: func(vpcr providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + if vpcr.Config.ContainsMarked() { + panic("config has marks") + } + var diags tfdiags.Diagnostics + if vpcr.Config.Type().HasAttribute("test") { + if vpcr.Config.GetAttr("test").RawEquals(cty.StringVal("invalid")) { + diags = diags.Append(fmt.Errorf("invalid value checked by provider itself")) + } + } + return providers.ValidateProviderConfigResponse{ + PreparedConfig: vpcr.Config, + Diagnostics: diags, + } + }, + } + providerFactory := providers.FactoryFixed(mockProvider) + return mockProvider, providerFactory + } + getProviderConfig := func(ctx context.Context, t *testing.T, main *Main) *ProviderConfig { + t.Helper() + mainStack := main.MainStack() + provider := mainStack.Provider(stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }) + if provider == nil { + t.Fatal("no provider.foo.bar is available") + } + return provider.config + } + + subtestInPromisingTask(t, "valid", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("yep"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + config := getProviderConfig(ctx, t, main) + + want := cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }) + got, diags := config.CheckProviderArgs(ctx, InspectPhase) + assertNoDiags(t, diags) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if !mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was not called; should've been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) + subtestInPromisingTask(t, "valid with marks", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("yep").Mark("nope"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + config := getProviderConfig(ctx, t, main) + + want := cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep").Mark("nope"), + }) + got, diags := config.CheckProviderArgs(ctx, InspectPhase) + assertNoDiags(t, diags) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if !mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was not called; should've been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go b/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go new file mode 100644 index 0000000000..bedad562c1 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_expressions.go @@ -0,0 +1,517 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval/stubs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ConfigComponentExpressionScope is an extension to ExpressionScope that +// also provides access to the underlying configuration module that is being +// evaluated by this scope. +// +// This is typically used to share code between removed and component blocks +// which both load and execute Terraform configurations. +type ConfigComponentExpressionScope[Addr any] interface { + ExpressionScope + + Addr() Addr + StackConfig() *StackConfig + ModuleTree(ctx context.Context) *configs.Config + DeclRange() *hcl.Range +} + +// EvalProviderTypes evaluates the provider configurations for a component, +// ensuring that all required providers are present and have the correct type. +// +// This function should be called during static evaluations of components and +// removed blocks. +func EvalProviderTypes(ctx context.Context, stack *StackConfig, providers map[addrs.LocalProviderConfig]hcl.Expression, phase EvalPhase, scope ConfigComponentExpressionScope[stackaddrs.ConfigComponent]) (addrs.Set[addrs.RootProviderConfig], tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + neededProviders := requiredProviderInstances(ctx, scope) + + ret := addrs.MakeSet[addrs.RootProviderConfig]() + for _, elem := range neededProviders.Elems { + + // sourceAddr is the addrs.RootProviderConfig that should be used to + // set this provider in the component later. + sourceAddr := elem.Key + + // componentAddr is the addrs.LocalProviderConfig that specifies the + // local name and (optional) alias of the provider in the component. + componentAddr := elem.Value.Local + + // typeAddr is the absolute address of the provider type itself. + typeAddr := sourceAddr.Provider + + expr, exists := providers[componentAddr] + if !exists { + // Then this provider isn't listed in the `providers` block of this + // component. Which is bad! + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: fmt.Sprintf( + "The root module for %s requires a provider configuration named %q for provider %q, which is not assigned in the block's \"providers\" argument.", + scope.Addr(), componentAddr.StringCompact(), typeAddr.ForDisplay(), + ), + Subject: scope.DeclRange(), + }) + continue + } + + // This means we now have an expression that should be providing the + // configuration for this required provider. We'll evaluate it now. + + result, hclDiags := EvalExprAndEvalContext(ctx, expr, phase, scope) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + continue + } + + // Now, we received something from the expression. We need to make sure + // it's a valid provider configuration and it's the right type of + // provider. + + const errSummary = "Invalid provider configuration" + if actualTy := result.Value.Type(); stackconfigtypes.IsProviderConfigType(actualTy) { + // Then we at least got a provider reference of some kind. + actualTypeAddr := stackconfigtypes.ProviderForProviderConfigType(actualTy) + if actualTypeAddr != typeAddr { + var errorDetail string + + stackName, matchingTypeExists := stack.ProviderLocalName(typeAddr) + _, matchingNameExists := stack.ProviderForLocalName(componentAddr.LocalName) + moduleProviderTypeExplicit := elem.Value.Explicit + if !matchingTypeExists && !matchingNameExists { + // Then the user just hasn't declared the target provider + // type or name at all. We'll return a generic error message + // asking the user to update the required_providers list. + errorDetail = "\n\nDeclare the required provider in the stack's required_providers block, and then assign a configuration for that provider in this block's \"providers\" argument." + } else if !matchingNameExists { + // Then we have a type that matches, but the name doesn't. + errorDetail = fmt.Sprintf("\n\nThis stack has a configured provider of the correct type under the name %q. Update this block's \"providers\" argument to reference this provider.", stackName) + } else if !matchingTypeExists { + // Then we have a name that matches, but the type doesn't. + + // If the types don't match and the names do, then maybe + // the user hasn't properly filled in the required types + // within the module. + if !moduleProviderTypeExplicit { + // Yes! The provider type within the module has been + // implied by Terraform and not explicitly set within + // the required_providers block. We'll suggest the user + // to update the required_providers block of the module. + errorDetail = fmt.Sprintf("\n\nThe module does not declare a source address for %q in its required_providers block, so Terraform assumed %q for backward-compatibility with older versions of Terraform", componentAddr.LocalName, elem.Key.Provider.ForDisplay()) + } + + // Otherwise the user has explicitly set the provider type + // within the module, but it doesn't match the provider type + // within the stack configuration. The generic error message + // should be sufficient. + } + + // But, unfortunately, the underlying types of the providers + // do not match up. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: fmt.Sprintf( + "The provider configuration slot %q requires a configuration for provider %q, not for provider %q.%s", + componentAddr.StringCompact(), typeAddr, actualTypeAddr, errorDetail, + ), + Subject: result.Expression.Range().Ptr(), + }) + continue + } + } else if result.Value == cty.DynamicVal { + // Then we don't know the concrete type of this reference at this + // time, so we'll just have to accept it. This is somewhat expected + // during the validation phase, and even during the planning phase + // if we have deferred attributes. We'll get an error later (ie. + // during the plan phase) if the type doesn't match up then. + } else { + // We got something that isn't a provider reference at all. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: fmt.Sprintf( + "The provider configuration slot %s requires a configuration for provider %q.", + componentAddr.StringCompact(), typeAddr, + ), + Subject: result.Expression.Range().Ptr(), + }) + continue + } + + // If we made it here, the types all matched up so we've done everything + // we can. component_instance.go will do additional checks to make sure + // the result is known and not null when it comes time to actually + // check the plan. + + ret.Add(sourceAddr) + } + + return ret, diags +} + +// EvalProviderValues evaluates the provider configuration for a component, and +// returns the provider configuration instances that should be used in the +// component. +// +// This function should be called during dynamic evaluations of components and +// removed blocks. +func EvalProviderValues(ctx context.Context, main *Main, providers map[addrs.LocalProviderConfig]hcl.Expression, phase EvalPhase, scope ConfigComponentExpressionScope[stackaddrs.AbsComponentInstance]) (map[addrs.RootProviderConfig]stackaddrs.AbsProviderConfigInstance, map[addrs.RootProviderConfig]addrs.Provider, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + knownProviders := make(map[addrs.RootProviderConfig]stackaddrs.AbsProviderConfigInstance) + unknownProviders := make(map[addrs.RootProviderConfig]addrs.Provider) + + neededProviders := requiredProviderInstances(ctx, scope) + + for _, elem := range neededProviders.Elems { + // sourceAddr is the addrs.RootProviderConfig that should be used to + // set this provider in the component later. + sourceAddr := elem.Key + + // componentAddr is the addrs.LocalProviderConfig that specifies the + // local name and (optional) alias of the provider in the component. + componentAddr := elem.Value.Local + + // We validated the config providers during the static analysis, so we + // know this expression exists and resolves to the correct type. + expr := providers[componentAddr] + + inst, unknown, instDiags := evalProviderValue(ctx, sourceAddr, componentAddr, expr, phase, scope) + diags = diags.Append(instDiags) + if instDiags.HasErrors() { + continue + } + + if unknown { + unknownProviders[sourceAddr] = sourceAddr.Provider + continue + } + + knownProviders[sourceAddr] = inst + } + + // Second, we want to iterate through the providers that are required by + // the state and not required by the configuration. Unfortunately, we don't + // currently store enough information to be able to retrieve the original + // provider directly from the state. We only store the provider type and + // alias of the original provider. Stacks can have multiple instances of the + // same provider type, local name, and alias. This means we need the user to + // still provide an entry for this provider in the declConfigs. + // TODO: There's another TODO in the state package that suggests we should + // store the additional information we need. Once this is fixed we can + // come and tidy this up as well. + + moduleTree := scope.ModuleTree(ctx) + + // We'll search through the declConfigs to find any keys that match the + // type and alias of a any provider needed by the state. This is backwards + // when compared to how we resolved the configProviders. But we don't have + // the information we need to do it the other way around. + + previousProviders := main.PreviousProviderInstances(scope.Addr(), phase) + for localProviderAddr, expr := range providers { + provider := moduleTree.ProviderForConfigAddr(localProviderAddr) + + sourceAddr := addrs.RootProviderConfig{ + Provider: provider, + Alias: localProviderAddr.Alias, + } + + if _, exists := knownProviders[sourceAddr]; exists || !previousProviders.Has(sourceAddr) { + // Then this declConfig either matches a configProvider and we've + // already processed it, or it matches a provider that isn't + // required by the config or the state. In the first case, this is + // fine we have matched the right provider already. In the second + // case, we could raise a warning or something but it's not a big + // deal so we can ignore it. + continue + } + + // Otherwise, this is a declConfig for a provider that is not in the + // configProviders and is in the previousProviders. So, we should + // process it. + + inst, unknown, instDiags := evalProviderValue(ctx, sourceAddr, localProviderAddr, expr, phase, scope) + diags = diags.Append(instDiags) + if instDiags.HasErrors() { + continue + } + + if unknown { + unknownProviders[sourceAddr] = provider + } else { + knownProviders[sourceAddr] = inst + } + + if _, ok := scope.StackConfig().ProviderLocalName(provider); !ok { + // Even though we have an entry for this provider in the declConfigs + // doesn't mean we have an entry for this in our required providers. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Block requires undeclared provider", + Detail: fmt.Sprintf( + "The root module for %s has resources in state that require a configuration for provider %q, which isn't declared as a dependency of this stack configuration.\n\nDeclare this provider in the stack's required_providers block, and then assign a configuration for that provider in this block's \"providers\" argument.", + scope.Addr(), provider.ForDisplay(), + ), + Subject: scope.DeclRange(), + }) + } + } + + // Finally, let's check that we have a provider configuration for every + // provider needed by the state. + + for _, previousProvider := range previousProviders { + if _, ok := knownProviders[previousProvider]; ok { + // Then we have a provider for this, so great! + continue + } + + // If we get here, then we didn't find an entry for this provider in + // the declConfigs. This is an error because we need to have an entry + // for every provider that we have in the state. + + // localAddr helps with the error message. + localAddr := addrs.LocalProviderConfig{ + LocalName: moduleTree.Module.LocalNameForProvider(previousProvider.Provider), + Alias: previousProvider.Alias, + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: fmt.Sprintf( + "The root module for %s has resources in state that require a provider configuration named %q for provider %q, which is not assigned in the block's \"providers\" argument.", + scope.Addr(), localAddr.StringCompact(), previousProvider.Provider.ForDisplay(), + ), + Subject: scope.DeclRange(), + }) + } + + return knownProviders, unknownProviders, diags +} + +func evalProviderValue(ctx context.Context, sourceAddr addrs.RootProviderConfig, componentAddr addrs.LocalProviderConfig, expr hcl.Expression, phase EvalPhase, scope ConfigComponentExpressionScope[stackaddrs.AbsComponentInstance]) (stackaddrs.AbsProviderConfigInstance, bool, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var ret stackaddrs.AbsProviderConfigInstance + + result, hclDiags := EvalExprAndEvalContext(ctx, expr, phase, scope) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return ret, false, diags + } + v := result.Value + + // The first set of checks can perform a redundant check in some cases. For + // providers required by the configuration the type validation should have + // been performed by the static analysis. However, we'll repeat the checks + // here to also catch the case where providers are required by the existing + // state but are not defined in the configuration. This isn't checked by + // the static analysis. + const errSummary = "Invalid provider configuration" + if actualTy := result.Value.Type(); stackconfigtypes.IsProviderConfigType(actualTy) { + // Then we at least got a provider reference of some kind. + actualTypeAddr := stackconfigtypes.ProviderForProviderConfigType(actualTy) + if actualTypeAddr != sourceAddr.Provider { + // But, unfortunately, the underlying types of the providers + // do not match up. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: fmt.Sprintf( + "The provider configuration slot %s requires a configuration for provider %q, not for provider %q.", + componentAddr.StringCompact(), sourceAddr.Provider, actualTypeAddr, + ), + Subject: result.Expression.Range().Ptr(), + }) + return ret, false, diags + } + } else { + // We got something that isn't a provider reference at all. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: fmt.Sprintf( + "The provider configuration slot %s requires a configuration for provider %q.", + componentAddr.StringCompact(), sourceAddr.Provider, + ), + Subject: result.Expression.Range().Ptr(), + }) + return ret, false, diags + } + + // Now, we differ from the static analysis in that we should have + // returned a concrete value while we may have got unknown during the + // static analysis. + if v.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errSummary, + Detail: fmt.Sprintf( + "The provider configuration slot %s is required, but this definition returned null.", + componentAddr.StringCompact(), + ), + Subject: result.Expression.Range().Ptr(), + }) + return ret, false, diags + } + if !v.IsKnown() { + return ret, true, diags + } + + // If it's of the correct type, known, and not null then we should + // be able to retrieve a specific provider instance address that + // this value refers to. + return stackconfigtypes.ProviderInstanceForValue(v), false, diags +} + +// requiredProviderInstances returns a description of all of the provider +// instance slots ("provider configurations" in main Terraform language +// terminology) that are either explicitly declared or implied by the +// root module of the scope's module tree. +// +// In the returned map the keys describe provider configurations from +// the perspective of an object inside the root module, and so the LocalName +// field values are an implementation detail that must not be exposed into +// the calling stack and are included here only so that we can potentially +// return error messages referring to declarations inside the module. +// +// If any modules in the component's root module tree are invalid then this +// result could under-promise or over-promise depending on the kind of +// invalidity. +func requiredProviderInstances[Addr any](ctx context.Context, scope ConfigComponentExpressionScope[Addr]) addrs.Map[addrs.RootProviderConfig, configs.RequiredProviderConfig] { + moduleTree := scope.ModuleTree(ctx) + if moduleTree == nil || moduleTree.Root == nil { + return addrs.MakeMap[addrs.RootProviderConfig, configs.RequiredProviderConfig]() + } + return moduleTree.Root.EffectiveRequiredProviderConfigs() +} + +// neededProviderSchemas returns the provider schemas for all of the providers +// required by the configuration of the given component, along with any +// diagnostics that were encountered while fetching those schemas. +func neededProviderSchemas[Addr any](ctx context.Context, main *Main, phase EvalPhase, scope ConfigComponentExpressionScope[Addr]) (map[addrs.Provider]providers.ProviderSchema, tfdiags.Diagnostics, bool) { + var diags tfdiags.Diagnostics + skipFutherValidation := false + + config := scope.ModuleTree(ctx) + + providerSchemas := make(map[addrs.Provider]providers.ProviderSchema) + for _, sourceAddr := range config.ProviderTypes() { + pTy := main.ProviderType(sourceAddr) + if pTy == nil { + continue // not our job to report a missing provider + } + + // If this phase has a dependency lockfile, check if the provider is in it. + depLocks := main.DependencyLocks(phase) + if depLocks != nil { + // Check if the provider is in the lockfile, + // if it is not we can not read the provider schema + providerLockfileDiags := CheckProviderInLockfile(*depLocks, pTy, scope.DeclRange()) + + // We report these diagnostics in a different place + if providerLockfileDiags.HasErrors() { + skipFutherValidation = true + continue + } + } + + schema, err := pTy.Schema(ctx) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider initialization error", + Detail: fmt.Sprintf("Failed to fetch the provider schema for %s: %s.", sourceAddr, err), + Subject: scope.DeclRange(), + }) + continue + } + providerSchemas[sourceAddr] = schema + } + return providerSchemas, diags, skipFutherValidation +} + +// unconfiguredProviderClients returns the provider clients for the providers +// required by the configuration of the given component, along with any +// diagnostics that were encountered while fetching those clients. +func unconfiguredProviderClients(main *Main, ps addrs.Set[addrs.RootProviderConfig]) (map[addrs.RootProviderConfig]providers.Interface, bool) { + insts := make(map[addrs.RootProviderConfig]providers.Interface) + valid := true + + for _, provider := range ps { + pTy := main.ProviderType(provider.Provider) + if pTy == nil { + valid = false + continue // not our job to report a missing provider + } + + // We don't need to configure the client for validate functionality. + inst, err := pTy.UnconfiguredClient() + if err != nil { + valid = false + continue + } + insts[provider] = inst + } + + return insts, valid +} + +// configuredProviderClients return s +func configuredProviderClients(ctx context.Context, main *Main, known map[addrs.RootProviderConfig]stackaddrs.AbsProviderConfigInstance, unknown map[addrs.RootProviderConfig]addrs.Provider, phase EvalPhase) map[addrs.RootProviderConfig]providers.Interface { + providerInsts := make(map[addrs.RootProviderConfig]providers.Interface) + for calleeAddr, callerAddr := range known { + providerInstStack := main.Stack(ctx, callerAddr.Stack, phase) + if providerInstStack == nil { + continue + } + provider := providerInstStack.Provider(callerAddr.Item.ProviderConfig) + if provider == nil { + continue + } + insts, unknown := provider.Instances(ctx, phase) + if unknown { + // an unknown provider should have been added to the unknown + // providers and not the known providers, so this is a bug if we get + // here. + panic(fmt.Errorf("provider %s returned unknown instances", callerAddr)) + } + if insts == nil { + continue + } + inst, exists := insts[callerAddr.Item.Key] + if !exists { + continue + } + providerInsts[calleeAddr] = inst.Client(ctx, phase) + } + for calleeAddr, provider := range unknown { + pTy := main.ProviderType(provider) + client, err := pTy.UnconfiguredClient() + if err != nil { + continue + } + providerInsts[calleeAddr] = stubs.UnknownProvider(client) + } + return providerInsts +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_factory.go b/internal/stacks/stackruntime/internal/stackeval/provider_factory.go new file mode 100644 index 0000000000..e3e9e9e85e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_factory.go @@ -0,0 +1,71 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// ProviderFactories is a collection of factory functions for starting new +// instances of various providers. +type ProviderFactories map[addrs.Provider]providers.Factory + +func (pf ProviderFactories) ProviderAvailable(providerAddr addrs.Provider) bool { + _, available := pf[providerAddr] + return available +} + +// NewUnconfiguredClient launches a new instance of the requested provider, +// if available, and returns it in an unconfigured state. +// +// Callers that need a _configured_ provider can then call +// [providers.Interface.Configure] on the result to configure it, making it +// ready for the majority of operations that require a configured provider. +func (pf ProviderFactories) NewUnconfiguredClient(providerAddr addrs.Provider) (providers.Interface, error) { + f, ok := pf[providerAddr] + if !ok { + return nil, fmt.Errorf("provider is not available in this execution context") + } + return f() +} + +// unconfigurableProvider is a wrapper around a provider.Interface that +// prevents it from being configured. This is because the underlying interface +// should already have been configured by the time we get here, or should never +// be configured. +// +// In addition, unconfigurableProviders are not closeable, because they should +// be closed by the external thing that configured them when they are done +// with them. +type unconfigurableProvider struct { + providers.Interface +} + +var _ providers.Interface = unconfigurableProvider{} + +func (p unconfigurableProvider) Close() error { + // whatever created the underlying provider should be responsible for + // closing it, so we'll do nothing here. + return nil +} + +func (p unconfigurableProvider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + // the real provider should either already have been configured by the time + // we get here or should never get configured, so we should never see this + // method called. + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.AttributeValue( + tfdiags.Error, + "Called ConfigureProvider on an unconfigurable provider", + "This provider should have already been configured, or should never be configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + ), + }, + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_instance.go b/internal/stacks/stackruntime/internal/stackeval/provider_instance.go new file mode 100644 index 0000000000..e521bc4aec --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_instance.go @@ -0,0 +1,314 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval/stubs" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +// ProviderInstance represents one instance of a provider. +// +// A provider configuration block with the for_each argument appears as a +// single [ProviderConfig], then one [Provider] for each stack config instance +// the provider belongs to, and then one [ProviderInstance] for each +// element of for_each for each [Provider]. +type ProviderInstance struct { + provider *Provider + addr stackaddrs.AbsProviderConfigInstance + repetition instances.RepetitionData + + main *Main + + providerArgs perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + client perEvalPhase[promising.Once[withDiagnostics[providers.Interface]]] +} + +var _ ExpressionScope = (*ProviderInstance)(nil) + +func newProviderInstance(provider *Provider, addr stackaddrs.AbsProviderConfigInstance, repetition instances.RepetitionData) *ProviderInstance { + return &ProviderInstance{ + provider: provider, + addr: addr, + main: provider.main, + repetition: repetition, + } +} + +func (p *ProviderInstance) RepetitionData() instances.RepetitionData { + return p.repetition +} + +func (p *ProviderInstance) ProviderType() *ProviderType { + return p.main.ProviderType(p.addr.Item.ProviderConfig.Provider) +} + +func (p *ProviderInstance) ProviderArgsDecoderSpec(ctx context.Context) (hcldec.Spec, error) { + return p.provider.config.ProviderArgsDecoderSpec(ctx) +} + +// ProviderArgs returns an object value representing the provider configuration +// for this instance, or an unknown value of the correct type if the +// configuration is invalid. If a provider error occurs, it returns +// [cty.DynamicVal]. +func (p *ProviderInstance) ProviderArgs(ctx context.Context, phase EvalPhase) cty.Value { + v, _ := p.CheckProviderArgs(ctx, phase) + return v +} + +func (p *ProviderInstance) CheckProviderArgs(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags( + ctx, p.tracingName(), p.providerArgs.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + providerType := p.ProviderType() + decl := p.provider.config.config + spec, err := p.ProviderArgsDecoderSpec(ctx) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to read provider schema", + Detail: fmt.Sprintf( + "Error while reading the schema for %q: %s.", + providerType.Addr(), err, + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + var configVal cty.Value + var moreDiags tfdiags.Diagnostics + configBody := decl.Config + if configBody == nil { + configBody = hcl.EmptyBody() + } + configVal, moreDiags = EvalBody(ctx, configBody, spec, phase, p) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.UnknownVal(hcldec.ImpliedType(spec)), diags + } + + unconfClient, err := providerType.UnconfiguredClient() + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to start provider plugin", + Detail: fmt.Sprintf( + "Error while instantiating %q: %s.", + providerType.Addr(), err, + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + // We unmark the config before making the RPC call, but will still + // return the original possibly-marked config if successful. + unmarkedConfigVal, _ := configVal.UnmarkDeep() + validateResp := unconfClient.ValidateProviderConfig(providers.ValidateProviderConfigRequest{ + Config: unmarkedConfigVal, + }) + diags = diags.Append(validateResp.Diagnostics) + if validateResp.Diagnostics.HasErrors() { + return cty.DynamicVal, diags + } + + return configVal, diags + }, + ) +} + +// Client returns a client object for the provider instance, already configured +// per the provider configuration arguments and ready to use. +// +// If the configured arguments are invalid then this might return a stub +// provider client that implements all methods either as silent no-ops or as +// returning error diagnostics, so callers can just treat the returned client +// as always valid. +// +// Callers must call Close on the returned client once they have finished using +// the client. +func (p *ProviderInstance) Client(ctx context.Context, phase EvalPhase) providers.Interface { + ret, _ := p.CheckClient(ctx, phase) + return ret +} + +func (p *ProviderInstance) CheckClient(ctx context.Context, phase EvalPhase) (providers.Interface, tfdiags.Diagnostics) { + return doOnceWithDiags( + ctx, p.tracingName()+" plugin client", p.client.For(phase), + func(ctx context.Context) (providers.Interface, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if p.repetition.EachKey != cty.NilVal && !p.repetition.EachKey.IsKnown() { + // We should have triggered and returned a stub.UnknownProvider + // in this case, so there's a bug somewhere in Terraform if + // this happens. + panic("provider instance with unknown for_each key") + } + if p.repetition.CountIndex != cty.NilVal && !p.repetition.CountIndex.IsKnown() { + // Providers don't even support the count index argument, so + // something crazy is happening if we get here. + panic("provider instance with unknown count index") + } + + providerType := p.ProviderType() + decl := p.provider.config.config + + client, err := p.main.ProviderFactories().NewUnconfiguredClient(providerType.Addr()) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to start provider plugin", + Detail: fmt.Sprintf( + "Could not create an instance of %s for %s: %s.", + providerType.Addr(), p.addr, err, + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + return stubs.ErroredProvider(), diags + } + + // If the context we recieved gets cancelled then we want providers + // to try to cancel any operations they have in progress, so we'll + // watch for that in a separate goroutine. This extra context + // is here just so we can avoid leaking this goroutine if the + // parent doesn't get cancelled. + providerCtx, localCancel := context.WithCancel(ctx) + go func() { + <-providerCtx.Done() + if ctx.Err() == context.Canceled { + // Not all providers respond to this, but some will quickly + // abort operations currently in progress and return a + // cancellation error, thus allowing us to halt more quickly + // when interrupted. + client.Stop() + } + }() + + // If this provider is implemented as a separate plugin then we + // must terminate its child process once evaluation is complete. + p.main.RegisterCleanup(func(ctx context.Context) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + localCancel() // make sure our cancel-monitoring goroutine terminates + err := client.Close() + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Failed to terminate provider plugin", + Detail: fmt.Sprintf( + "Error closing the instance of %s for %s: %s.", + providerType.Addr(), p.addr, err, + ), + Subject: decl.DeclRange.ToHCL().Ptr(), + }) + } + return diags + }) + + // TODO: Some providers will malfunction if the caller doesn't + // fetch their schema at least once before use. That's not something + // the provider protocol promises but it's an implementation + // detail that a certain generation of providers relied on + // nonetheless. We'll probably need to check whether the provider + // supports the "I don't need you to fetch my schema" capability + // and, if not, do a redundant re-fetch of the schema in here + // somewhere. Refer to the corresponding behavior in the + // "terraform" package for non-Stacks usage and try to mimick + // what it does in as lightweight a way as possible. + + // We unmark the config before making the RPC call, as marks cannot + // be serialized. + unmarkedArgs, _ := p.ProviderArgs(ctx, phase).UnmarkDeep() + if unmarkedArgs == cty.NilVal { + // Then we had an error previously, so we'll rely on that error + // being exposed elsewhere. + return stubs.ErroredProvider(), diags + } + + resp := client.ConfigureProvider(providers.ConfigureProviderRequest{ + TerraformVersion: version.SemVer.String(), + Config: unmarkedArgs, + ClientCapabilities: ClientCapabilities(), + }) + diags = diags.Append(resp.Diagnostics) + if resp.Diagnostics.HasErrors() { + // If the provider didn't configure successfully then it won't + // meet the expectations of our callers and so we'll return a + // stub instead. (The real provider stays running until it + // gets cleaned up by the cleanup function above, despite being + // inaccessible to the caller.) + return stubs.ErroredProvider(), diags + } + + return unconfigurableProvider{ + Interface: client, + }, diags + }, + ) +} + +// ResolveExpressionReference implements ExpressionScope for expressions other +// than the for_each argument inside a provider block, which get evaluated +// once per provider instance. +func (p *ProviderInstance) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return p.provider.stack.resolveExpressionReference(ctx, ref, nil, p.repetition) +} + +// ExternalFunctions implements ExpressionScope. +func (p *ProviderInstance) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return p.main.ProviderFunctions(ctx, p.provider.config.stack) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (p *ProviderInstance) PlanTimestamp() time.Time { + return p.main.PlanTimestamp() +} + +func (p *ProviderInstance) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, moreDiags := p.CheckProviderArgs(ctx, phase) + diags = diags.Append(moreDiags) + + // NOTE: CheckClient starts and configures the provider as a side-effect. + // If this is a plugin-based provider then the plugin process will stay + // running for the remainder of the specified evaluation phase. + _, moreDiags = p.CheckClient(ctx, phase) + diags = diags.Append(moreDiags) + + return diags +} + +// PlanChanges implements Plannable. +func (p *ProviderInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, p.checkValid(ctx, PlanPhase) +} + +// CheckApply implements Applyable. +func (p *ProviderInstance) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + return nil, p.checkValid(ctx, ApplyPhase) +} + +// tracingName implements Plannable. +func (p *ProviderInstance) tracingName() string { + return p.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_instance_test.go b/internal/stacks/stackruntime/internal/stackeval/provider_instance_test.go new file mode 100644 index 0000000000..43f526dcfc --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_instance_test.go @@ -0,0 +1,433 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +func TestProviderInstanceCheckProviderArgs(t *testing.T) { + cfg := testStackConfig(t, "provider", "single_instance_configured") + providerTypeAddr := addrs.NewBuiltInProvider("foo") + newMockProvider := func(t *testing.T) (*testing_provider.MockProvider, providers.Factory) { + t.Helper() + mockProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "test": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + ValidateProviderConfigFn: func(vpcr providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + if vpcr.Config.ContainsMarked() { + panic("config has marks") + } + var diags tfdiags.Diagnostics + if vpcr.Config.Type().HasAttribute("test") { + if vpcr.Config.GetAttr("test").RawEquals(cty.StringVal("invalid")) { + diags = diags.Append(fmt.Errorf("invalid value checked by provider itself")) + } + } + return providers.ValidateProviderConfigResponse{ + PreparedConfig: vpcr.Config, + Diagnostics: diags, + } + }, + } + providerFactory := providers.FactoryFixed(mockProvider) + return mockProvider, providerFactory + } + getProviderInstance := func(ctx context.Context, t *testing.T, main *Main) *ProviderInstance { + t.Helper() + mainStack := main.MainStack() + provider := mainStack.Provider(stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }) + if provider == nil { + t.Fatal("no provider.foo.bar is available") + } + insts, unknown := provider.Instances(ctx, InspectPhase) + assertFalse(t, unknown) + inst, ok := insts[addrs.NoKey] + if !ok { + t.Fatal("missing NoKey instance of provider.foo.bar") + } + return inst + } + + subtestInPromisingTask(t, "valid", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("yep"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + want := cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }) + got, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertNoDiags(t, diags) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if !mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was not called; should've been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) + subtestInPromisingTask(t, "valid with marks", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("yep").Mark("nope"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + want := cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep").Mark("nope"), + }) + got, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertNoDiags(t, diags) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if !mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was not called; should've been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) + subtestInPromisingTask(t, "valid with no config block at all", func(ctx context.Context, t *testing.T) { + // For this one we'll use a different configuration fixture that + // doesn't include a "config" block at all. + cfg := testStackConfig(t, "provider", "single_instance") + + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + // We'll make sure the configuration really does omit the config + // block, in case someone modifies the fixture in future without + // realizing we're relying on that invariant here. + decl := inst.provider.config.config + if decl.Config != nil { + t.Fatal("test fixture has a config block for the provider; should omit it") + } + + want := cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }) + got, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertNoDiags(t, diags) + + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + + if !mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was not called; should've been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.NullVal(cty.String), + }), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) + subtestInPromisingTask(t, "invalid per schema", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.EmptyObjectVal, + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + _, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + // The "test" argument expects a string, but we assigned an object + return diag.Severity() == tfdiags.Error && diag.Description().Summary == `Incorrect attribute value type` + }) + if mockProvider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig was called, but should not have been because the config didn't conform to the schema") + } + }) + subtestInPromisingTask(t, "invalid per provider logic", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("invalid"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + _, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return diag.Severity() == tfdiags.Error && diag.Description().Summary == `invalid value checked by provider itself` + }) + if !mockProvider.ValidateProviderConfigCalled { + // It would be strange to get here because that would suggest + // that we got the diagnostic from the provider without asking + // the provider for it. Is terraform.MockProvider broken? + t.Error("ValidateProviderConfig was not called, but should have been") + } else { + got := mockProvider.ValidateProviderConfigRequest + want := providers.ValidateProviderConfigRequest{ + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("invalid"), + }), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) + subtestInPromisingTask(t, "can't fetch schema at all", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + mockProvider.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Diagnostics: tfdiags.Diagnostics(nil).Append(fmt.Errorf("nope")), + } + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.EmptyObjectVal, + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + _, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return diag.Severity() == tfdiags.Error && diag.Description().Summary == `Failed to read provider schema` + }) + }) + subtestInPromisingTask(t, "provider doesn't even start up", func(ctx context.Context, t *testing.T) { + providerFactory := providers.Factory(func() (providers.Interface, error) { + return nil, fmt.Errorf("uh-oh") + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.EmptyObjectVal, + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + _, diags := inst.CheckProviderArgs(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return diag.Severity() == tfdiags.Error && diag.Description().Summary == `Failed to read provider schema` + }) + }) +} + +func TestProviderInstanceCheckClient(t *testing.T) { + cfg := testStackConfig(t, "provider", "single_instance_configured") + providerTypeAddr := addrs.NewBuiltInProvider("foo") + newMockProvider := func(t *testing.T) (*testing_provider.MockProvider, providers.Factory) { + t.Helper() + mockProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "test": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + ConfigureProviderFn: func(vpcr providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + if vpcr.Config.ContainsMarked() { + panic("config has marks") + } + var diags tfdiags.Diagnostics + if vpcr.Config.Type().HasAttribute("test") { + if vpcr.Config.GetAttr("test").RawEquals(cty.StringVal("invalid")) { + diags = diags.Append(fmt.Errorf("invalid value checked by provider itself")) + } + } + return providers.ConfigureProviderResponse{ + Diagnostics: diags, + } + }, + } + providerFactory := providers.FactoryFixed(mockProvider) + return mockProvider, providerFactory + } + getProviderInstance := func(ctx context.Context, t *testing.T, main *Main) *ProviderInstance { + t.Helper() + mainStack := main.MainStack() + provider := mainStack.Provider(stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }) + if provider == nil { + t.Fatal("no provider.foo.bar is available") + } + insts, unknown := provider.Instances(ctx, InspectPhase) + assertFalse(t, unknown) + inst, ok := insts[addrs.NoKey] + if !ok { + t.Fatal("missing NoKey instance of provider.foo.bar") + } + return inst + } + + subtestInPromisingTask(t, "valid", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("yep"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + client, diags := inst.CheckClient(ctx, InspectPhase) + assertNoDiags(t, diags) + + switch c := client.(type) { + case unconfigurableProvider: + break + default: + t.Errorf("unexpected client type %#T", c) + } + + if !mockProvider.ConfigureProviderCalled { + t.Error("ConfigureProvider was not called; should've been") + } else { + got := mockProvider.ConfigureProviderRequest + want := providers.ConfigureProviderRequest{ + TerraformVersion: version.SemVer.String(), + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }), + ClientCapabilities: ClientCapabilities(), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) + + subtestInPromisingTask(t, "valid with marks", func(ctx context.Context, t *testing.T) { + mockProvider, providerFactory := newMockProvider(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_configuration": cty.StringVal("yep").Mark("nope"), + }, + ProviderFactories: ProviderFactories{ + providerTypeAddr: providerFactory, + }, + }) + inst := getProviderInstance(ctx, t, main) + + client, diags := inst.CheckClient(ctx, InspectPhase) + assertNoDiags(t, diags) + + switch c := client.(type) { + case unconfigurableProvider: + break + default: + t.Errorf("unexpected client type %#T", c) + } + + if !mockProvider.ConfigureProviderCalled { + t.Error("ConfigureProvider was not called; should've been") + } else { + got := mockProvider.ConfigureProviderRequest + want := providers.ConfigureProviderRequest{ + TerraformVersion: version.SemVer.String(), + Config: cty.ObjectVal(map[string]cty.Value{ + "test": cty.StringVal("yep"), + }), + ClientCapabilities: ClientCapabilities(), + } + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong request\n%s", diff) + } + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_test.go b/internal/stacks/stackruntime/internal/stackeval/provider_test.go new file mode 100644 index 0000000000..aa40f9bf62 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_test.go @@ -0,0 +1,426 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestProviderCheckInstances(t *testing.T) { + getProvider := func(ctx context.Context, t *testing.T, main *Main) *Provider { + t.Helper() + mainStack := main.MainStack() + provider := mainStack.Provider(stackaddrs.ProviderConfig{ + Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/foo"), + Name: "bar", + }) + if provider == nil { + t.Fatal("provider.foo.bar does not exist, but it should exist") + } + return provider + } + + subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) { + cfg := testStackConfig(t, "provider", "single_instance") + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + }) + + provider := getProvider(ctx, t, main) + forEachVal, diags := provider.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if forEachVal != cty.NilVal { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: cty.NilVal", forEachVal) + } + + insts, unknown, diags := provider.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 1; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + inst, ok := insts[addrs.NoKey] + if !ok { + t.Fatalf("missing expected addrs.NoKey instance\n%s", spew.Sdump(insts)) + } + if diff := cmp.Diff(instances.RepetitionData{}, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + t.Run("for_each", func(t *testing.T) { + cfg := testStackConfig(t, "provider", "for_each") + + subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.MapValEmpty(cty.EmptyObject), + }, + }) + + provider := getProvider(ctx, t, main) + forEachVal, diags := provider.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if got, want := forEachVal, cty.MapValEmpty(cty.EmptyObject); !want.RawEquals(got) { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", got, want) + } + insts, unknown, diags := provider.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 0; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + + // For this particular function we take the unusual approach of + // distinguishing between a nil map and a non-nil empty map so + // we can distinguish between "definitely no instances" (this case) + // and "we don't know how many instances there are" (tested in other + // subtests of this test, below.) + if insts == nil { + t.Error("CheckInstances result is nil; should be non-nil empty map") + } + }) + subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) { + wantForEachVal := cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("in a"), + "b": cty.StringVal("in b"), + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": wantForEachVal, + }, + }) + + provider := getProvider(ctx, t, main) + gotForEachVal, diags := provider.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if !wantForEachVal.RawEquals(gotForEachVal) { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", gotForEachVal, wantForEachVal) + } + insts, unknown, diags := provider.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 2; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + t.Run("instance a", func(t *testing.T) { + inst, ok := insts[addrs.StringKey("a")] + if !ok { + t.Fatalf("missing expected addrs.StringKey(\"a\") instance\n%s", spew.Sdump(insts)) + } + wantRepData := instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.StringVal("in a"), + } + if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + t.Run("instance b", func(t *testing.T) { + inst, ok := insts[addrs.StringKey("b")] + if !ok { + t.Fatalf("missing expected addrs.StringKey(\"b\") instance\n%s", spew.Sdump(insts)) + } + wantRepData := instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.StringVal("in b"), + } + if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + }) + subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.NullVal(cty.Map(cty.EmptyObject)), + }, + }) + + provider := getProvider(ctx, t, main) + gotVal, diags := provider.CheckForEachValue(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return diag.Severity() == tfdiags.Error && strings.Contains(diag.Description().Detail, "The for_each expression produced a null value") + }) + wantVal := cty.DynamicVal // placeholder for invalid result + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + }) + subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.StringVal("nope"), + }, + }) + + provider := getProvider(ctx, t, main) + gotVal, diags := provider.CheckForEachValue(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return (diag.Severity() == tfdiags.Error && + diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this provider.") + }) + wantVal := cty.DynamicVal // placeholder for invalid result + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + + // When the for_each expression is invalid, CheckInstances should + // return nil to represent that we don't know enough to predict + // how many instances there are. This is a different result than + // when we know there are zero instances, which would be a non-nil + // empty map. + gotInsts, unknown, diags := provider.CheckInstances(ctx, InspectPhase) + assertFalse(t, unknown) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return (diag.Severity() == tfdiags.Error && + diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this provider.") + }) + if gotInsts != nil { + t.Errorf("wrong instances; want nil\n%#v", gotInsts) + } + }) + subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.UnknownVal(cty.Map(cty.EmptyObject)), + }, + }) + + // For now it's invalid to use an unknown value in for_each. + // Later we're expecting to make this succeed but announce that + // planning everything beneath this provider must be deferred to a + // future plan after everything else has been applied first. + provider := getProvider(ctx, t, main) + gotVal, diags := provider.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + wantVal := cty.UnknownVal(cty.Map(cty.EmptyObject)) + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + + insts, unknown, diags := provider.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertTrue(t, unknown) + if got, want := len(insts), 0; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + }) + }) + +} + +func TestProviderExprReferenceValue(t *testing.T) { + providerTypeAddr := addrs.MustParseProviderSourceString("terraform.io/builtin/foo") + providerRefType := providerInstanceRefType(providerTypeAddr) + getProvider := func(ctx context.Context, t *testing.T, main *Main) *Provider { + t.Helper() + mainStack := main.MainStack() + provider := mainStack.Provider(stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }) + if provider == nil { + t.Fatal("provider.foo.bar does not exist, but it should exist") + } + return provider + } + getRefFromVal := func(t *testing.T, v cty.Value) stackaddrs.AbsProviderConfigInstance { + t.Helper() + if !stackconfigtypes.IsProviderConfigType(v.Type()) { + t.Fatalf("result is not of a provider configuration reference type\ngot type: %#v", v.Type()) + } + return stackconfigtypes.ProviderInstanceForValue(v) + } + + subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) { + cfg := testStackConfig(t, "provider", "single_instance") + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{}, + }) + + provider := getProvider(ctx, t, main) + got := getRefFromVal(t, provider.ExprReferenceValue(ctx, InspectPhase)) + want := stackaddrs.AbsProviderConfigInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ProviderConfigInstance{ + ProviderConfig: stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }, + Key: addrs.NoKey, + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + t.Run("for_each", func(t *testing.T) { + cfg := testStackConfig(t, "provider", "for_each") + + subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.MapValEmpty(cty.EmptyObject), + }, + }) + + provider := getProvider(ctx, t, main) + got := provider.ExprReferenceValue(ctx, InspectPhase) + want := cty.MapValEmpty(providerRefType) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) { + forEachVal := cty.MapVal(map[string]cty.Value{ + "a": cty.StringVal("in a"), + "b": cty.StringVal("in b"), + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": forEachVal, + }, + }) + + provider := getProvider(ctx, t, main) + gotVal := provider.ExprReferenceValue(ctx, InspectPhase) + if !gotVal.Type().IsMapType() { + t.Fatalf("wrong result type\ngot type: %#v\nwant: map of provider references", gotVal.Type()) + } + if gotVal.IsNull() || !gotVal.IsKnown() { + t.Fatalf("wrong result\ngot: %#v\nwant: a known, non-null map of provider references", gotVal) + } + gotValMap := gotVal.AsValueMap() + if got, want := len(gotValMap), 2; got != want { + t.Errorf("wrong number of instances %d; want %d\n", got, want) + } + if gotVal := gotValMap["a"]; gotVal != cty.NilVal { + got := getRefFromVal(t, gotVal) + want := stackaddrs.AbsProviderConfigInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ProviderConfigInstance{ + ProviderConfig: stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }, + Key: addrs.StringKey("a"), + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("wrong result for instance 'a'\n%s", diff) + } + } else { + t.Errorf("no element for instance 'a'") + } + if gotVal := gotValMap["b"]; gotVal != cty.NilVal { + got := getRefFromVal(t, gotVal) + want := stackaddrs.AbsProviderConfigInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ProviderConfigInstance{ + ProviderConfig: stackaddrs.ProviderConfig{ + Provider: providerTypeAddr, + Name: "bar", + }, + Key: addrs.StringKey("b"), + }, + } + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("wrong result for instance 'b'\n%s", diff) + } + } else { + t.Errorf("no element for instance 'b'") + } + }) + subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.NullVal(cty.Map(cty.EmptyObject)), + }, + }) + + provider := getProvider(ctx, t, main) + got := provider.ExprReferenceValue(ctx, InspectPhase) + // When the for_each expression is invalid, the result value + // is unknown so we can use it as a placeholder for partial + // downstream checking. + want := cty.NilVal + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.StringVal("nope"), + }, + }) + + provider := getProvider(ctx, t, main) + got := provider.ExprReferenceValue(ctx, InspectPhase) + // When the for_each expression is invalid, the result value + // is unknown so we can use it as a placeholder for partial + // downstream checking. + want := cty.NilVal + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "provider_instances": cty.UnknownVal(cty.Map(cty.EmptyObject)), + }, + }) + + provider := getProvider(ctx, t, main) + got := provider.ExprReferenceValue(ctx, InspectPhase) + // When the for_each expression is unknown, the result value + // is unknown too so we can use it as a placeholder for partial + // downstream checking. + want := cty.UnknownVal(cty.Map(providerRefType)) + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/provider_type.go b/internal/stacks/stackruntime/internal/stackeval/provider_type.go new file mode 100644 index 0000000000..369b9e59c8 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/provider_type.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ProviderType struct { + mu sync.Mutex + + addr addrs.Provider + + main *Main + + schema promising.Once[providers.GetProviderSchemaResponse] + unconfiguredClient providers.Interface +} + +func newProviderType(main *Main, addr addrs.Provider) *ProviderType { + return &ProviderType{ + addr: addr, + main: main, + } +} + +func (pt *ProviderType) Addr() addrs.Provider { + return pt.addr +} + +// ProviderRefType returns the cty capsule type that represents references to +// providers of this type when passed through expressions. +func (pt *ProviderType) ProviderRefType() cty.Type { + allTypes := pt.main.ProviderRefTypes() + return allTypes[pt.Addr()] +} + +// UnconfiguredClient returns the client for the singleton unconfigured +// provider of this type, initializing the provider first if necessary. +// +// Callers must call Close on the returned client once they are finished +// with it, which will internally decrement a reference count so that +// the shared provider can be eventually closed once no longer needed. +func (pt *ProviderType) UnconfiguredClient() (providers.Interface, error) { + pt.mu.Lock() + defer pt.mu.Unlock() + + if pt.unconfiguredClient == nil { + client, err := pt.main.ProviderFactories().NewUnconfiguredClient(pt.Addr()) + if err != nil { + return nil, err + } + pt.unconfiguredClient = client + + pt.main.RegisterCleanup(func(_ context.Context) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + if err := pt.unconfiguredClient.Close(); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to terminate provider plugin", + fmt.Sprintf( + "Error closing the unconfigured instance of %s: %s.", + pt.Addr(), err, + ), + )) + } + return diags + }) + } + + return unconfigurableProvider{ + Interface: pt.unconfiguredClient, + }, nil +} + +func (pt *ProviderType) Schema(ctx context.Context) (providers.GetProviderSchemaResponse, error) { + return pt.schema.Do(ctx, pt.Addr().String()+" schema", func(ctx context.Context) (providers.GetProviderSchemaResponse, error) { + client, err := pt.UnconfiguredClient() + if err != nil { + return providers.GetProviderSchemaResponse{}, fmt.Errorf("provider startup failed: %w", err) + } + + ret := client.GetProviderSchema() + if ret.Diagnostics.HasErrors() { + return providers.GetProviderSchemaResponse{}, fmt.Errorf("provider failed to return its schema") + } + return ret, nil + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go b/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go new file mode 100644 index 0000000000..fe58dba89b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/refresh_instance.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// RefreshInstance is different kind of node in the graph. Rather than being +// instantiated by the configuration, it is loaded dynamically by a relevant +// component or removed block. It represents the refresh action of a given +// instance within state. +// +// This is only ever called during a destroy operation, and is used to refresh +// the state of the component before it is destroyed. If this changes, then +// the PreDestroyRefresh option should be removed from the plan options. +type RefreshInstance struct { + component *ComponentInstance + + result promising.Once[map[string]cty.Value] + moduleTreePlan promising.Once[withDiagnostics[*plans.Plan]] +} + +func newRefreshInstance(component *ComponentInstance) *RefreshInstance { + return &RefreshInstance{ + component: component, + } +} + +// Result returns the outputs of the refresh action for this instance. +func (r *RefreshInstance) Result(ctx context.Context) map[string]cty.Value { + result, err := r.result.Do(ctx, r.component.Addr().String()+" result", func(ctx context.Context) (map[string]cty.Value, error) { + config := r.component.ModuleTree(ctx) + + plan, _ := r.Plan(ctx) + if plan == nil { + // Then we'll return dynamic values for all outputs, and the error + // from the plan will be raised elsewhere. + outputs := make(map[string]cty.Value, len(config.Module.Outputs)) + for output := range config.Module.Outputs { + outputs[output] = cty.DynamicVal + } + return outputs, nil + } + return stackplan.OutputsFromPlan(config, plan), nil + }) + if err != nil { + // This should never happen as we do not return an error from within + // the function literal passed to Do. But, if somehow we do this, then + // it means we will skip the refresh for this component. + return nil + } + return result +} + +func (r *RefreshInstance) Plan(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.component.Addr().String()+" plan", &r.moduleTreePlan, func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { + opts, diags := r.component.PlanOpts(ctx, plans.NormalMode, false) + if opts == nil { + return nil, diags + } + + // For now, the refresh option is only used to separate the refresh + // from the apply during a destroy operation. So, we want to use that + // option here to ensure that the refresh is done in a way that is + // compatible with the destroy operation. + opts.PreDestroyRefresh = true + + plan, moreDiags := PlanComponentInstance(ctx, r.component.main, r.component.PlanPrevState(), opts, r.component) + return plan, diags.Append(moreDiags) + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed.go b/internal/stacks/stackruntime/internal/stackeval/removed.go new file mode 100644 index 0000000000..4fc72ca748 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed.go @@ -0,0 +1,154 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync" + + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" +) + +// Removed encapsulates the somewhat complicated logic for tracking and +// managing the removed block instances in a given stack. +// +// The Removed block does actually capture the entire tree of removed blocks +// in a single instance via the children field. Each Stack has a reference to +// its Removed instance, from which it can access all of its children. +type Removed struct { + sync.Mutex + + components map[stackaddrs.Component][]*RemovedComponent + stackCalls map[stackaddrs.StackCall][]*RemovedStackCall + + children map[string]*Removed +} + +func newRemoved() *Removed { + return &Removed{ + components: make(map[stackaddrs.Component][]*RemovedComponent), + stackCalls: make(map[stackaddrs.StackCall][]*RemovedStackCall), + children: make(map[string]*Removed), + } +} + +func (removed *Removed) Get(addr stackaddrs.ConfigStackCall) *Removed { + if len(addr.Stack) == 0 { + return removed.Next(addr.Item.Name) + } + return removed.Next(addr.Stack[0].Name).Get(stackaddrs.ConfigStackCall{ + Stack: addr.Stack[1:], + Item: addr.Item, + }) +} + +func (removed *Removed) Next(step string) *Removed { + removed.Lock() + defer removed.Unlock() + + next := removed.children[step] + if next == nil { + next = newRemoved() + removed.children[step] = next + } + return next +} + +func (removed *Removed) AddComponent(addr stackaddrs.ConfigComponent, components []*RemovedComponent) { + if len(addr.Stack) == 0 { + removed.components[addr.Item] = append(removed.components[addr.Item], components...) + return + } + removed.Next(addr.Stack[0].Name).AddComponent(stackaddrs.ConfigComponent{ + Stack: addr.Stack[1:], + Item: addr.Item, + }, components) +} + +func (removed *Removed) AddStackCall(addr stackaddrs.ConfigStackCall, stackCalls []*RemovedStackCall) { + if len(addr.Stack) == 0 { + removed.stackCalls[addr.Item] = append(removed.stackCalls[addr.Item], stackCalls...) + return + } + removed.Next(addr.Stack[0].Name).AddStackCall(stackaddrs.ConfigStackCall{ + Stack: addr.Stack[1:], + Item: addr.Item, + }, stackCalls) +} + +// validateMissingInstanceAgainstRemovedBlocks matches the function of the same +// name defined on Stack. +// +// This function should only ever be called from inside that function and it +// performs the same purpose except it exclusively looks for orphaned blocks +// with the children. +// +// This function assumes all the checks made in the equivalent function in Stack +// have been completed, so again (!!!) it should only be called from within +// the other function. +func (removed *Removed) validateMissingInstanceAgainstRemovedBlocks(ctx context.Context, addr stackaddrs.StackInstance, target stackaddrs.AbsComponentInstance, phase EvalPhase) (*stackconfig.Removed, *stackconfig.Component) { + + // we're just jumping directly into checking the children, the removed + // stack calls should have already been checked by the function on + // Stack. + + if len(target.Stack) == 0 { + + if components, ok := removed.components[target.Item.Component]; ok { + for _, component := range components { + insts, _ := component.InstancesFor(ctx, addr, phase) + for _, inst := range insts { + if inst.from.Item.Key == target.Item.Key { + // then we have actually found it! this is a removed + // block that targets the target address, but isn't + // in any stacks. + return inst.call.config.config, nil + } + } + } + } + + return nil, nil // we found no potential blocks + } + + // otherwise, we'll keep looking! + + // first, we'll check to see if we have a removed block targeting + // the entire stack. + + next := target.Stack[0] + rest := stackaddrs.AbsComponentInstance{ + Stack: target.Stack[1:], + Item: target.Item, + } + + if calls, ok := removed.stackCalls[stackaddrs.StackCall{Name: next.Name}]; ok { + for _, call := range calls { + insts, _ := call.InstancesFor(ctx, append(addr, next), phase) + for _, inst := range insts { + stack := inst.Stack(ctx, phase) + + // now, hand the search back over to the stack to check if + // the target instance is actually claimed by this removed + // stack. + removed, component := stack.validateMissingInstanceAgainstRemovedBlocks(ctx, rest, phase) + if removed != nil || component != nil { + // if we found any match, then return this removed block + // as the original source + return call.config.config, nil + } + } + } + + } + + // finally, we'll keep going through the children of the next one. + + if child, ok := removed.children[next.Name]; ok { + return child.validateMissingInstanceAgainstRemovedBlocks(ctx, append(addr, next), rest, phase) + } + + return nil, nil +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_component.go b/internal/stacks/stackruntime/internal/stackeval/removed_component.go new file mode 100644 index 0000000000..51e22379b2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed_component.go @@ -0,0 +1,304 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ Plannable = (*RemovedComponent)(nil) + _ Applyable = (*RemovedComponent)(nil) +) + +type RemovedComponent struct { + target stackaddrs.ConfigComponent // relative to stack.addr + + config *RemovedComponentConfig + stack *Stack + main *Main + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*RemovedComponentInstance]]]] + + unknownInstancesMutex sync.Mutex + unknownInstances collections.Map[stackaddrs.AbsComponentInstance, *RemovedComponentInstance] +} + +func newRemovedComponent(main *Main, target stackaddrs.ConfigComponent, stack *Stack, config *RemovedComponentConfig) *RemovedComponent { + return &RemovedComponent{ + target: target, + main: main, + config: config, + stack: stack, + unknownInstances: collections.NewMap[stackaddrs.AbsComponentInstance, *RemovedComponentInstance](), + } +} + +func (r *RemovedComponent) ForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.tracingName()+" for_each", r.forEachValue.For(phase), func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + config := r.config.config + + switch { + case config.ForEach != nil: + result, diags := evaluateForEachExpr(ctx, config.ForEach, phase, r.stack, "removed") + if diags.HasErrors() { + return cty.DynamicVal, diags + } + + return result.Value, diags + + default: + return cty.NilVal, nil + } + }) +} + +// InstancesFor is a wrapper around Instances, but it returns only the instances +// that target components within the target stack instance. +// +// Essentially, a removed block can target components across multiple stack +// instances, and this function allows callers to only get the relevant +// instances. +func (r *RemovedComponent) InstancesFor(ctx context.Context, target stackaddrs.StackInstance, phase EvalPhase) (map[addrs.InstanceKey]*RemovedComponentInstance, bool) { + results, unknown, _ := r.Instances(ctx, phase) + + insts := make(map[addrs.InstanceKey]*RemovedComponentInstance) + for key, inst := range results { + if inst.Addr().Stack.String() != target.String() { + continue + } + insts[key] = inst + } + + return insts, unknown +} + +func (r *RemovedComponent) Instances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*RemovedComponentInstance, bool, tfdiags.Diagnostics) { + result, diags := doOnceWithDiags(ctx, r.tracingName()+" instances", r.instances.For(phase), func(ctx context.Context) (instancesResult[*RemovedComponentInstance], tfdiags.Diagnostics) { + forEachValue, diags := r.ForEachValue(ctx, phase) + if diags.HasErrors() { + return instancesResult[*RemovedComponentInstance]{}, diags + } + + // First, evaluate the for_each value to get the set of instances the + // user has asked to be removed. + result := instancesMap(forEachValue, func(ik addrs.InstanceKey, rd instances.RepetitionData) *RemovedComponentInstance { + from := r.config.config.From + + evalContext, moreDiags := evalContextForTraversals(ctx, from.Variables(), phase, &removedInstanceExpressionScope{r, rd}) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil + } + + addr, moreDiags := from.TargetAbsComponentInstance(evalContext, r.stack.addr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil + } + + return newRemovedComponentInstance(r, addr, rd, r.stack.deferred) + }) + + // Now, filter out any instances that are not known to the previous + // state. This means the user has targeted a component that (a) never + // existed or (b) was removed in a previous operation. + // + // This stops us emitting planned and applied changes for instances that + // do not exist. + knownAddrs := make([]stackaddrs.AbsComponentInstance, 0, len(result.insts)) + knownInstances := make(map[addrs.InstanceKey]*RemovedComponentInstance, len(result.insts)) + for key, ci := range result.insts { + if ci == nil { + // if ci is nil, then it means we couldn't process the address + // for this instance above + continue + } + + // Now we know the concrete instances for this removed block, + // we're going to verify that there are no component instances in + // the configuration that also claim this instance. + addr := ci.Addr() + if stack := r.main.Stack(ctx, addr.Stack, phase); stack != nil { + if component := stack.Component(addr.Item.Component); component != nil { + components, _ := component.Instances(ctx, phase) + if _, ok := components[addr.Item.Key]; ok { + // Then this removed instance is targeting an instance + // that is also claimed by a component block. We have to make + // this check at this stage, because it is only now we now + // the actual instances targeted by this removed block. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove component instance", + Detail: fmt.Sprintf("The component instance %s is targeted by a component block and cannot be removed. The relevant component is defined at %s.", addr, component.config.config.DeclRange.ToHCL()), + Subject: ci.DeclRange(), + }) + + // don't add this to the known instances, so only the + // component block will return values for this instance. + continue + } + } + } + + switch phase { + case PlanPhase: + if r.main.PlanPrevState().HasComponentInstance(ci.Addr()) { + knownInstances[key] = ci + knownAddrs = append(knownAddrs, ci.Addr()) + continue + } + case ApplyPhase: + if component := r.main.PlanBeingApplied().GetComponent(ci.Addr()); component != nil { + knownInstances[key] = ci + knownAddrs = append(knownAddrs, ci.Addr()) + continue + } + default: + // Otherwise, we're running in a stage that doesn't evaluate + // a state or the plan so we'll just include everything. + knownInstances[key] = ci + knownAddrs = append(knownAddrs, ci.Addr()) + + } + } + result.insts = knownInstances + + h := hooksFromContext(ctx) + hookSingle(ctx, h.RemovedComponentExpanded, &hooks.RemovedComponentInstances{ + Source: r.stack.addr, + InstanceAddrs: knownAddrs, + }) + + return result, diags + }) + return result.insts, result.unknown, diags +} + +func (r *RemovedComponent) UnknownInstance(ctx context.Context, from stackaddrs.AbsComponentInstance, phase EvalPhase) *RemovedComponentInstance { + r.unknownInstancesMutex.Lock() + defer r.unknownInstancesMutex.Unlock() + + if inst, ok := r.unknownInstances.GetOk(from); ok { + return inst + } + + forEachType, _ := r.ForEachValue(ctx, phase) + repetitionData := instances.UnknownForEachRepetitionData(forEachType.Type()) + + inst := newRemovedComponentInstance(r, from, repetitionData, true) + r.unknownInstances.Put(from, inst) + return inst +} + +func (r *RemovedComponent) PlanIsComplete(ctx context.Context, stack stackaddrs.StackInstance) bool { + if !r.main.Planning() { + panic("PlanIsComplete used when not in the planning phase") + } + insts, unknown := r.InstancesFor(ctx, stack, PlanPhase) + if insts == nil { + // Suggests that the configuration was not even valid enough to + // decide what the instances are, so we'll return false to be + // conservative and let the error be returned by a different path. + return false + } + + if unknown { + // If the wildcard key is used the instance originates from an unknown + // for_each value, which means the result is unknown. + return false + } + + for _, inst := range insts { + plan, _ := inst.ModuleTreePlan(ctx) + if plan == nil { + // Seems that we weren't even able to create a plan for this + // one, so we'll just assume it was incomplete to be conservative, + // and assume that whatever errors caused this nil result will + // get returned by a different return path. + return false + } + + if !plan.Complete { + return false + } + } + // If we get here without returning false then we can say that + // all of the instance plans are complete. + return true +} + +// PlanChanges implements Plannable. +func (r *RemovedComponent) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + _, _, diags := r.Instances(ctx, PlanPhase) + return nil, diags +} + +// tracingName implements Plannable. +func (r *RemovedComponent) tracingName() string { + return fmt.Sprintf("%s -> %s (removed)", r.stack.addr, r.target) +} + +func (r *RemovedComponent) ApplySuccessful(ctx context.Context, addr stackaddrs.StackInstance) bool { + if !r.main.Applying() { + panic("ApplySuccessful when not applying") + } + + // Apply is successful if all of our instances fully completed their + // apply phases. + insts, _ := r.InstancesFor(ctx, addr, ApplyPhase) + for _, inst := range insts { + result, _ := inst.ApplyResult(ctx) + if result == nil || !result.Complete { + return false + } + } + return true +} + +// CheckApply implements Applyable. +func (r *RemovedComponent) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + _, _, diags := r.Instances(ctx, ApplyPhase) + return nil, diags +} + +var _ ExpressionScope = (*removedInstanceExpressionScope)(nil) + +// removedInstanceExpressionScope is wrapper around the RemovedComponent expression +// scope that also includes repetition data for a specific instance of this +// removed block. +type removedInstanceExpressionScope struct { + call *RemovedComponent + rd instances.RepetitionData +} + +func (r *removedInstanceExpressionScope) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return r.call.stack.resolveExpressionReference(ctx, ref, nil, r.rd) +} + +func (r *removedInstanceExpressionScope) PlanTimestamp() time.Time { + return r.call.main.PlanTimestamp() +} + +func (r *removedInstanceExpressionScope) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return r.call.main.ProviderFunctions(ctx, r.call.config.stack) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_component_config.go b/internal/stacks/stackruntime/internal/stackeval/removed_component_config.go new file mode 100644 index 0000000000..93bee6d26c --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed_component_config.go @@ -0,0 +1,245 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + stackparser "github.com/hashicorp/terraform/internal/stacks/stackconfig/parser" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ Validatable = (*RemovedComponentConfig)(nil) + _ Plannable = (*RemovedComponentConfig)(nil) + _ ExpressionScope = (*RemovedComponentConfig)(nil) + _ ConfigComponentExpressionScope[stackaddrs.ConfigComponent] = (*RemovedComponentConfig)(nil) +) + +type RemovedComponentConfig struct { + addr stackaddrs.ConfigComponent + config *stackconfig.Removed + stack *StackConfig + + main *Main + + validate perEvalPhase[promising.Once[tfdiags.Diagnostics]] + moduleTree promising.Once[withDiagnostics[*configs.Config]] // moduleTree is constant for every phase +} + +func newRemovedComponentConfig(main *Main, addr stackaddrs.ConfigComponent, stack *StackConfig, config *stackconfig.Removed) *RemovedComponentConfig { + return &RemovedComponentConfig{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +// Addr implements ConfigComponentExpressionScope. +func (r *RemovedComponentConfig) Addr() stackaddrs.ConfigComponent { + return r.addr +} + +// DeclRange implements ConfigComponentExpressionScope. +func (r *RemovedComponentConfig) DeclRange() *hcl.Range { + return r.config.DeclRange.ToHCL().Ptr() +} + +// StackConfig implements ConfigComponentExpressionScope +func (r *RemovedComponentConfig) StackConfig() *StackConfig { + return r.stack +} + +// ModuleTree implements ConfigComponentExpressionScope +func (r *RemovedComponentConfig) ModuleTree(ctx context.Context) *configs.Config { + cfg, _ := r.CheckModuleTree(ctx) + return cfg +} + +// CheckModuleTree loads and validates the module tree for the component that +// is being removed. +func (r *RemovedComponentConfig) CheckModuleTree(ctx context.Context) (*configs.Config, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.tracingName()+" modules", &r.moduleTree, func(ctx context.Context) (*configs.Config, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + decl := r.config + sources := r.main.SourceBundle() + + rootModuleSource := decl.FinalSourceAddr + if rootModuleSource == nil { + // If we get here then the configuration was loaded incorrectly, + // either by the stackconfig package or by the caller of the + // stackconfig package using the wrong loading function. + panic("component configuration lacks final source address") + } + + parser := configs.NewSourceBundleParser(sources) + parser.AllowLanguageExperiments(r.main.LanguageExperimentsAllowed()) + + if !parser.IsConfigDir(rootModuleSource) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Can't load module for removed component", + Detail: fmt.Sprintf("The source location %s does not contain a Terraform module.", rootModuleSource), + Subject: decl.SourceAddrRange.ToHCL().Ptr(), + }) + return nil, diags + } + + rootMod, hclDiags := parser.LoadConfigDir(rootModuleSource) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + walker := stackparser.NewSourceBundleModuleWalker(rootModuleSource, sources, parser) + configRoot, hclDiags := configs.BuildConfig(rootMod, walker, nil) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return nil, diags + } + + // We also have a small selection of additional static validation + // rules that apply only to modules used within stack components. + diags = diags.Append(validateModuleTreeForStacks(configRoot)) + + return configRoot, diags + }) +} + +// CheckValid validates the module tree and provider configurations for the +// component being removed. +func (r *RemovedComponentConfig) CheckValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + diags, err := r.validate.For(phase).Do(ctx, r.tracingName(), func(ctx context.Context) (tfdiags.Diagnostics, error) { + var diags tfdiags.Diagnostics + + moduleTree, moreDiags := r.CheckModuleTree(ctx) + diags = diags.Append(moreDiags) + if moduleTree == nil { + return diags, nil + } + + providers, moreDiags := EvalProviderTypes(ctx, r.stack, r.config.ProviderConfigs, phase, r) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return diags, nil + } + + providerSchemas, moreDiags, skipFurtherValidation := neededProviderSchemas(ctx, r.main, phase, r) + if skipFurtherValidation { + return diags.Append(moreDiags), nil + } + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return diags, nil + } + + tfCtx, err := terraform.NewContext(&terraform.ContextOpts{ + PreloadedProviderSchemas: providerSchemas, + Provisioners: r.main.availableProvisioners(), + }) + if err != nil { + // Should not get here because we should always pass a valid + // ContextOpts above. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to instantiate Terraform modules runtime", + fmt.Sprintf("Could not load the main Terraform language runtime: %s.\n\nThis is a bug in Terraform; please report it!", err), + )) + return diags, nil + } + + providerClients, valid := unconfiguredProviderClients(r.main, providers) + if !valid { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot validate component", + Detail: fmt.Sprintf("Cannot validate %s because its provider configuration assignments are invalid.", r.Addr()), + Subject: r.DeclRange(), + }) + return diags, nil + } + defer func() { + // Close the unconfigured provider clients that we opened in + // unconfiguredProviderClients. + for _, client := range providerClients { + client.Close() + } + }() + + // When our given context is cancelled, we want to instruct the + // modules runtime to stop the running operation. We use this + // nested context to ensure that we don't leak a goroutine when the + // parent context isn't cancelled. + operationCtx, operationCancel := context.WithCancel(ctx) + defer operationCancel() + go func() { + <-operationCtx.Done() + if ctx.Err() == context.Canceled { + tfCtx.Stop() + } + }() + + diags = diags.Append(tfCtx.Validate(moduleTree, &terraform.ValidateOpts{ + ExternalProviders: providerClients, + })) + return diags, nil + }) + if err != nil { + // this is crazy, we never return an error from the inner function so + // this really shouldn't happen. + panic(fmt.Sprintf("unexpected error from validate.Do: %s", err)) + } + return diags +} + +// PlanChanges implements Plannable. +func (r *RemovedComponentConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, r.CheckValid(ctx, PlanPhase) +} + +// Validate implements Validatable. +func (r *RemovedComponentConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + return r.CheckValid(ctx, ValidatePhase) +} + +// tracingName implements tracingNamer. +func (r *RemovedComponentConfig) tracingName() string { + return fmt.Sprintf("%s (removed)", r.Addr()) +} + +// ResolveExpressionReference implements ExpressionScope. +func (r *RemovedComponentConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + repetition := instances.RepetitionData{} + if r.config.ForEach != nil { + // For validation, we'll return unknown for the instance data. + repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() + repetition.EachValue = cty.DynamicVal + } + return r.stack.resolveExpressionReference(ctx, ref, nil, repetition) +} + +// PlanTimestamp implements ExpressionScope. +func (r *RemovedComponentConfig) PlanTimestamp() time.Time { + return r.main.PlanTimestamp() +} + +// ExternalFunctions implements ExpressionScope. +func (r *RemovedComponentConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return r.main.ProviderFunctions(ctx, r.stack) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_component_instance.go b/internal/stacks/stackruntime/internal/stackeval/removed_component_instance.go new file mode 100644 index 0000000000..3a182d7c6e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed_component_instance.go @@ -0,0 +1,347 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ Plannable = (*RemovedComponentInstance)(nil) + _ Applyable = (*RemovedComponentInstance)(nil) + _ ExpressionScope = (*RemovedComponentInstance)(nil) + _ ConfigComponentExpressionScope[stackaddrs.AbsComponentInstance] = (*RemovedComponentInstance)(nil) + _ ApplyableComponentInstance = (*RemovedComponentInstance)(nil) +) + +type RemovedComponentInstance struct { + call *RemovedComponent + from stackaddrs.AbsComponentInstance + deferred bool + + main *Main + + repetition instances.RepetitionData + + moduleTreePlan promising.Once[withDiagnostics[*plans.Plan]] +} + +func newRemovedComponentInstance(call *RemovedComponent, from stackaddrs.AbsComponentInstance, repetition instances.RepetitionData, deferred bool) *RemovedComponentInstance { + return &RemovedComponentInstance{ + call: call, + from: from, + deferred: deferred, + main: call.main, + repetition: repetition, + } +} + +func (r *RemovedComponentInstance) Addr() stackaddrs.AbsComponentInstance { + return r.from +} + +func (r *RemovedComponentInstance) ModuleTreePlan(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.tracingName()+" plan", &r.moduleTreePlan, func(ctx context.Context) (*plans.Plan, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + component := r.call.stack.Component(r.Addr().Item.Component) + if component != nil { + insts, unknown := component.Instances(ctx, PlanPhase) + if !unknown { + if _, exists := insts[r.Addr().Item.Key]; exists { + // The instance we're planning to remove is also targeted + // by a component block. We won't remove it, and we'll + // report a diagnostic to that effect. + return nil, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove component instance", + Detail: fmt.Sprintf("The component instance %s is targeted by a component block and cannot be removed. The relevant component is defined at %s.", r.Addr(), component.config.config.DeclRange.ToHCL()), + Subject: r.DeclRange(), + }) + } + } + } + + known, unknown, moreDiags := EvalProviderValues(ctx, r.main, r.call.config.config.ProviderConfigs, PlanPhase, r) + if moreDiags.HasErrors() { + // We won't actually add the diagnostics here, they should be + // exposed via a different return path. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot plan component", + Detail: fmt.Sprintf("Cannot generate a plan for %s because its provider configuration assignments are invalid.", r.Addr()), + Subject: r.DeclRange(), + }) + return nil, diags + } + + providerClients := configuredProviderClients(ctx, r.main, known, unknown, PlanPhase) + + deferred := r.deferred + Dependents: + for depAddr := range r.PlanPrevDependents().All() { + depStack := r.main.Stack(ctx, depAddr.Stack, PlanPhase) + if depStack == nil { + // something weird has happened, but this means that + // whatever thing we're depending on being deleted first + // doesn't exist so it's fine. + break + } + depComponent, depRemoveds := depStack.ApplyableComponents(depAddr.Item) + if depComponent != nil && !depComponent.PlanIsComplete(ctx) { + deferred = true + break + } + for _, depRemoved := range depRemoveds { + if !depRemoved.PlanIsComplete(ctx, depStack.addr) { + deferred = true + break Dependents + } + } + } + + mode := plans.DestroyMode + if r.main.PlanningOpts().PlanningMode == plans.RefreshOnlyMode { + mode = plans.RefreshOnlyMode + } + + plantimestamp := r.main.PlanTimestamp() + forget := !r.call.config.config.Destroy + opts := &terraform.PlanOpts{ + Mode: mode, + SetVariables: r.PlanPrevInputs(), + ExternalProviders: providerClients, + DeferralAllowed: true, + ExternalDependencyDeferred: deferred, + Forget: forget, + + // We want the same plantimestamp between all components and the stacks language + ForcePlanTimestamp: &plantimestamp, + } + + plan, moreDiags := PlanComponentInstance(ctx, r.main, r.PlanPrevState(), opts, r) + return plan, diags.Append(moreDiags) + }) +} + +// PlanPrevState returns the previous state for this component instance during +// the planning phase, or panics if called in any other phase. +func (r *RemovedComponentInstance) PlanPrevState() *states.State { + // The following call will panic if we aren't in the plan phase. + stackState := r.main.PlanPrevState() + ret := stackState.ComponentInstanceStateForModulesRuntime(r.Addr()) + if ret == nil { + ret = states.NewState() // so caller doesn't need to worry about nil + } + return ret +} + +// PlanPrevDependents returns the set of dependents based on the state. +func (r *RemovedComponentInstance) PlanPrevDependents() collections.Set[stackaddrs.AbsComponent] { + return r.main.PlanPrevState().DependentsForComponent(r.Addr()) +} + +func (r *RemovedComponentInstance) PlanPrevInputs() terraform.InputValues { + variables := r.main.PlanPrevState().InputsForComponent(r.Addr()) + + inputs := make(terraform.InputValues, len(variables)) + for k, v := range variables { + inputs[k.Name] = &terraform.InputValue{ + Value: v, + SourceType: terraform.ValueFromPlan, + } + } + return inputs +} + +func (r *RemovedComponentInstance) PlanCurrentInputs() (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + plan := r.main.PlanBeingApplied().GetComponent(r.Addr()) + inputs := make(map[string]cty.Value, len(plan.PlannedInputValues)) + for name, input := range plan.PlannedInputValues { + value, err := input.Decode(cty.DynamicPseudoType) + if err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Invalid input variable", fmt.Sprintf("Failed to decode the input value for %s in removed block for %s: %s", name, r.Addr(), err))) + continue + } + + if paths, ok := plan.PlannedInputValueMarks[name]; ok { + inputs[name.Name] = value.MarkWithPaths(paths) + } else { + inputs[name.Name] = value + } + } + return cty.ObjectVal(inputs), diags +} + +// ApplyModuleTreePlan implements ApplyableComponentInstance. +// +// See the equivalent function within ComponentInstance for more details. +func (r *RemovedComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans.Plan) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + if !r.main.Applying() { + panic("called ApplyModuleTreePlan with an evaluator not instantiated for applying") + } + + // Unlike a regular component, the removed block should have had any + // unknown variables. With that in mind, we can just the plan directly + // onto the shared function with no modifications. + + return ApplyComponentPlan(ctx, r.main, plan, r.call.config.config.ProviderConfigs, r) +} + +func (r *RemovedComponentInstance) ApplyResult(ctx context.Context) (*ComponentInstanceApplyResult, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + changes := r.main.ApplyChangeResults() + applyResult, moreDiags, err := changes.ComponentInstanceResult(ctx, r.Addr()) + diags = diags.Append(moreDiags) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Component instance apply not scheduled", + fmt.Sprintf("Terraform needs the result from applying changes to %s, but that apply was apparently not scheduled to run: %s. This is a bug in Terraform.", r.Addr(), err), + )) + } + return applyResult, diags +} + +func (r *RemovedComponentInstance) PlaceholderApplyResultForSkippedApply(plan *plans.Plan) *ComponentInstanceApplyResult { + // (We have this in here as a method just because it helps keep all of + // the logic for constructing [ComponentInstanceApplyResult] objects + // together in the same file, rather than having the caller synthesize + // a result itself only in this one special situation.) + return &ComponentInstanceApplyResult{ + FinalState: plan.PrevRunState, + Complete: false, + } +} + +// PlanChanges implements Plannable. +func (r *RemovedComponentInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + _, _, moreDiags := EvalProviderValues(ctx, r.main, r.call.config.config.ProviderConfigs, PlanPhase, r) + diags = diags.Append(moreDiags) + + plan, moreDiags := r.ModuleTreePlan(ctx) + diags = diags.Append(moreDiags) + + var changes []stackplan.PlannedChange + if plan != nil { + action := plans.Delete + switch { + case r.main.PlanningOpts().PlanningMode == plans.RefreshOnlyMode: + action = plans.Read + case !r.call.config.config.Destroy: + action = plans.Forget + } + changes, moreDiags = stackplan.FromPlan(ctx, r.ModuleTree(ctx), plan, nil, action, r) + diags = diags.Append(moreDiags) + } + return changes, diags +} + +// CheckApply implements Applyable. +func (r *RemovedComponentInstance) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + _, _, moreDiags := EvalProviderValues(ctx, r.main, r.call.config.config.ProviderConfigs, ApplyPhase, r) + diags = diags.Append(moreDiags) + + inputs, moreDiags := r.PlanCurrentInputs() + diags = diags.Append(moreDiags) + + result, moreDiags := r.ApplyResult(ctx) + diags = diags.Append(moreDiags) + + var changes []stackstate.AppliedChange + if result != nil { + changes, moreDiags = stackstate.FromState(ctx, result.FinalState, r.main.PlanBeingApplied().GetComponent(r.Addr()), inputs, result.AffectedResourceInstanceObjects, r) + diags = diags.Append(moreDiags) + } + return changes, diags +} + +// ResolveExpressionReference implements ExpressionScope. +func (r *RemovedComponentInstance) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return r.call.stack.resolveExpressionReference(ctx, ref, nil, r.repetition) +} + +// PlanTimestamp implements ExpressionScope. +func (r *RemovedComponentInstance) PlanTimestamp() time.Time { + return r.main.PlanTimestamp() +} + +// ExternalFunctions implements ExpressionScope. +func (r *RemovedComponentInstance) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return r.main.ProviderFunctions(ctx, r.call.config.stack) +} + +// ModuleTree implements ConfigComponentExpressionScope. +func (r *RemovedComponentInstance) ModuleTree(ctx context.Context) *configs.Config { + return r.call.config.ModuleTree(ctx) +} + +// DeclRange implements ConfigComponentExpressionScope. +func (r *RemovedComponentInstance) DeclRange() *hcl.Range { + return r.call.config.config.DeclRange.ToHCL().Ptr() +} + +// StackConfig implements ConfigComponentExpressionScope +func (r *RemovedComponentInstance) StackConfig() *StackConfig { + return r.call.stack.config +} + +// RequiredComponents implements stackplan.PlanProducer. +func (r *RemovedComponentInstance) RequiredComponents(_ context.Context) collections.Set[stackaddrs.AbsComponent] { + // We return the dependencies from the state, based on the required + // components when this component was last applied. In reality, destroy + // operations require "dependents" to have been executed first but + // we compute that in the plan phase based on the dependencies + return r.main.PlanPrevState().DependenciesForComponent(r.Addr()) +} + +// ResourceSchema implements stackplan.PlanProducer. +func (r *RemovedComponentInstance) ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, typ string) (providers.Schema, error) { + // This should not be able to fail with an error because we should + // be retrieving the same schema that was already used to encode + // the object we're working with. The error handling here is for + // robustness but any error here suggests a bug in Terraform. + + providerType := r.main.ProviderType(providerTypeAddr) + providerSchema, err := providerType.Schema(ctx) + if err != nil { + return providers.Schema{}, err + } + ret := providerSchema.SchemaForResourceType(mode, typ) + if ret.Body == nil { + return providers.Schema{}, fmt.Errorf("schema does not include %v %q", mode, typ) + } + return ret, nil +} + +// tracingName implements Plannable. +func (r *RemovedComponentInstance) tracingName() string { + return r.Addr().String() + " (removed)" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_stack_call.go b/internal/stacks/stackruntime/internal/stackeval/removed_stack_call.go new file mode 100644 index 0000000000..68fea75671 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed_stack_call.go @@ -0,0 +1,221 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var _ Plannable = (*RemovedStackCall)(nil) +var _ Applyable = (*RemovedStackCall)(nil) + +type RemovedStackCall struct { + stack *Stack + target stackaddrs.ConfigStackCall // relative to stack + + config *RemovedStackCallConfig + + main *Main + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*RemovedStackCallInstance]]]] + + unknownInstancesMutex sync.Mutex + unknownInstances collections.Map[stackaddrs.StackInstance, *RemovedStackCallInstance] +} + +func newRemovedStackCall(main *Main, target stackaddrs.ConfigStackCall, stack *Stack, config *RemovedStackCallConfig) *RemovedStackCall { + return &RemovedStackCall{ + stack: stack, + target: target, + config: config, + main: main, + unknownInstances: collections.NewMap[stackaddrs.StackInstance, *RemovedStackCallInstance](), + } +} + +// GetExternalRemovedBlocks fetches the removed blocks that target the stack +// instances being created by this stack call. +func (r *RemovedStackCall) GetExternalRemovedBlocks() *Removed { + return r.stack.Removed().Get(r.target) +} + +func (r *RemovedStackCall) ForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.tracingName()+" for_each", r.forEachValue.For(phase), func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + config := r.config.config + + switch { + case config.ForEach != nil: + result, diags := evaluateForEachExpr(ctx, config.ForEach, phase, r.stack, "removed") + if diags.HasErrors() { + return cty.DynamicVal, diags + } + + return result.Value, diags + + default: + return cty.NilVal, nil + } + }) +} + +func (r *RemovedStackCall) InstancesFor(ctx context.Context, stack stackaddrs.StackInstance, phase EvalPhase) (map[addrs.InstanceKey]*RemovedStackCallInstance, bool) { + results, unknown, _ := r.Instances(ctx, phase) + + insts := make(map[addrs.InstanceKey]*RemovedStackCallInstance) + for key, inst := range results { + if stack.Contains(inst.from) { + insts[key] = inst + } + } + + return insts, unknown +} + +func (r *RemovedStackCall) Instances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*RemovedStackCallInstance, bool, tfdiags.Diagnostics) { + result, diags := doOnceWithDiags(ctx, r.tracingName()+" instances", r.instances.For(phase), func(ctx context.Context) (instancesResult[*RemovedStackCallInstance], tfdiags.Diagnostics) { + forEachValue, diags := r.ForEachValue(ctx, phase) + if diags.HasErrors() { + return instancesResult[*RemovedStackCallInstance]{}, diags + } + + // First, evaluate the for_each value to get the set of instances the + // user has asked to be removed. + result := instancesMap(forEachValue, func(ik addrs.InstanceKey, rd instances.RepetitionData) *RemovedStackCallInstance { + from := r.config.config.From + + evalContext, moreDiags := evalContextForTraversals(ctx, from.Variables(), phase, &removedStackCallInstanceExpressionScope{r, rd}) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil + } + + addr, moreDiags := from.TargetStackInstance(evalContext, r.stack.addr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil + } + + return newRemovedStackCallInstance(r, addr, rd, r.stack.deferred) + }) + + knownInstances := make(map[addrs.InstanceKey]*RemovedStackCallInstance) + for key, rsc := range result.insts { + if rsc == nil { + // if rsc is nil, it means it was invalid above and we should + // have attached diags explaining this. + continue + } + + if stack := r.main.Stack(ctx, rsc.from.Parent(), phase); stack != nil { + embeddedCall := stack.EmbeddedStackCall(stackaddrs.StackCall{ + Name: rsc.from[len(rsc.from)-1].Name, + }) + + if embeddedCall != nil { + insts, _ := embeddedCall.Instances(ctx, phase) + if _, exists := insts[key]; exists { + // error, we have an embedded stack call and a removed block + // pointing at the same instance + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove stack instance", + Detail: fmt.Sprintf("The stack instance %s is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at %s.", rsc.from, embeddedCall.config.config.DeclRange.ToHCL()), + Subject: rsc.call.config.config.DeclRange.ToHCL().Ptr(), + }) + + continue // don't add this to the known instances + } + } + } + + switch phase { + case PlanPhase: + if r.main.PlanPrevState().HasStackInstance(rsc.from) { + knownInstances[key] = rsc + continue + } + case ApplyPhase: + if stack := r.main.PlanBeingApplied().GetStack(rsc.from); stack != nil { + knownInstances[key] = rsc + continue + } + default: + // Otherwise, we're running in a stage that doesn't evaluate + // a state or the plan so we'll just include everything. + knownInstances[key] = rsc + } + } + + result.insts = knownInstances + return result, diags + }) + return result.insts, result.unknown, diags +} + +func (r *RemovedStackCall) UnknownInstance(ctx context.Context, from stackaddrs.StackInstance, phase EvalPhase) *RemovedStackCallInstance { + r.unknownInstancesMutex.Lock() + defer r.unknownInstancesMutex.Unlock() + + if inst, ok := r.unknownInstances.GetOk(from); ok { + return inst + } + + forEachType, _ := r.ForEachValue(ctx, phase) + repetitionData := instances.UnknownForEachRepetitionData(forEachType.Type()) + + inst := newRemovedStackCallInstance(r, from, repetitionData, true) + r.unknownInstances.Put(from, inst) + return inst +} + +func (r *RemovedStackCall) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + _, _, diags := r.Instances(ctx, PlanPhase) + return nil, diags +} + +func (r *RemovedStackCall) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + _, _, diags := r.Instances(ctx, ApplyPhase) + return nil, diags +} + +func (r *RemovedStackCall) tracingName() string { + return fmt.Sprintf("%s -> %s (removed)", r.stack.addr, r.target) +} + +// removedStackCallInstanceExpressionScope is wrapper around the +// RemovedStackCall expression scope that also includes repetition data for a +// specific instance of this removed block. +type removedStackCallInstanceExpressionScope struct { + call *RemovedStackCall + rd instances.RepetitionData +} + +func (r *removedStackCallInstanceExpressionScope) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return r.call.stack.resolveExpressionReference(ctx, ref, nil, r.rd) +} + +func (r *removedStackCallInstanceExpressionScope) PlanTimestamp() time.Time { + return r.call.main.PlanTimestamp() +} + +func (r *removedStackCallInstanceExpressionScope) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return r.call.main.ProviderFunctions(ctx, r.call.stack.config) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_stack_call_config.go b/internal/stacks/stackruntime/internal/stackeval/removed_stack_call_config.go new file mode 100644 index 0000000000..44e9465a4e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed_stack_call_config.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ Validatable = (*RemovedStackCallConfig)(nil) + _ Plannable = (*RemovedStackCallConfig)(nil) + _ ExpressionScope = (*RemovedStackCallConfig)(nil) +) + +type RemovedStackCallConfig struct { + target stackaddrs.ConfigStackCall // relative to stack + config *stackconfig.Removed + stack *StackConfig + + main *Main + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + inputVariableValues perEvalPhase[promising.Once[withDiagnostics[map[stackaddrs.InputVariable]cty.Value]]] +} + +func newRemovedStackCallConfig(main *Main, target stackaddrs.ConfigStackCall, stack *StackConfig, config *stackconfig.Removed) *RemovedStackCallConfig { + return &RemovedStackCallConfig{ + target: target, + config: config, + stack: stack, + main: main, + } +} + +func (r *RemovedStackCallConfig) TargetConfig() *StackConfig { + current := r.stack + for _, step := range r.target.Stack { + current = current.ChildConfig(step) + } + return current.ChildConfig(stackaddrs.StackStep{Name: r.target.Item.Name}) +} + +func (r *RemovedStackCallConfig) InputVariableValues(ctx context.Context, phase EvalPhase) (map[stackaddrs.InputVariable]cty.Value, tfdiags.Diagnostics) { + + return doOnceWithDiags(ctx, r.tracingName()+" inputs", r.inputVariableValues.For(phase), validateStackCallInputsFn(r.config.Inputs, r.config.DeclRange.ToHCL(), r.TargetConfig(), r, phase)) +} + +func (r *RemovedStackCallConfig) ForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.tracingName()+" for_each", r.forEachValue.For(phase), func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + if r.config.ForEach == nil { + // This stack config isn't even using for_each. + return cty.NilVal, nil + } + + var diags tfdiags.Diagnostics + result, moreDiags := evaluateForEachExpr(ctx, r.config.ForEach, ValidatePhase, r.stack, "stack") + diags = diags.Append(moreDiags) + return result.Value, diags + }) +} + +func (r *RemovedStackCallConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + _, moreDiags := r.ForEachValue(ctx, ValidatePhase) + diags = diags.Append(moreDiags) + _, moreDiags = r.InputVariableValues(ctx, ValidatePhase) + diags = diags.Append(moreDiags) + return diags +} + +func (r *RemovedStackCallConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + _, moreDiags := r.ForEachValue(ctx, PlanPhase) + diags = diags.Append(moreDiags) + _, moreDiags = r.InputVariableValues(ctx, PlanPhase) + diags = diags.Append(moreDiags) + return nil, diags +} + +func (r *RemovedStackCallConfig) tracingName() string { + return fmt.Sprintf("%s -> %s (removed)", r.stack.addr, r.target) +} + +func (r *RemovedStackCallConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + repetition := instances.RepetitionData{} + if r.config.ForEach != nil { + // We're producing an approximation across all eventual instances + // of this call, so we'll set each.key and each.value to unknown + // values. + repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() + repetition.EachValue = cty.DynamicVal + } + ret, diags := r.stack.resolveExpressionReference(ctx, ref, nil, repetition) + + if _, ok := ret.(*ProviderConfig); ok { + // We can't reference other providers from anywhere inside an embedded + // stack call - they should define their own providers. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", ref.Target.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + + return ret, diags +} + +func (r *RemovedStackCallConfig) PlanTimestamp() time.Time { + return r.main.PlanTimestamp() +} + +func (r *RemovedStackCallConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return r.main.ProviderFunctions(ctx, r.stack) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/removed_stack_call_instance.go b/internal/stacks/stackruntime/internal/stackeval/removed_stack_call_instance.go new file mode 100644 index 0000000000..1c386fc389 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/removed_stack_call_instance.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "time" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var _ ExpressionScope = (*RemovedStackCallInstance)(nil) +var _ Plannable = (*RemovedStackCallInstance)(nil) +var _ Applyable = (*RemovedStackCallInstance)(nil) + +type RemovedStackCallInstance struct { + call *RemovedStackCall + from stackaddrs.StackInstance + deferred bool + + main *Main + + repetition instances.RepetitionData + + stack perEvalPhase[promising.Once[*Stack]] + inputVariableValues perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +func newRemovedStackCallInstance(call *RemovedStackCall, from stackaddrs.StackInstance, repetition instances.RepetitionData, deferred bool) *RemovedStackCallInstance { + return &RemovedStackCallInstance{ + call: call, + from: from, + repetition: repetition, + deferred: deferred, + main: call.main, + } +} + +func (r *RemovedStackCallInstance) Stack(ctx context.Context, phase EvalPhase) *Stack { + stack, err := r.stack.For(phase).Do(ctx, r.from.String()+" create", func(ctx context.Context) (*Stack, error) { + + mode := plans.DestroyMode + if r.main.PlanningMode() == plans.RefreshOnlyMode { + mode = plans.RefreshOnlyMode + } + + return newStack(r.main, r.from, r.call.stack, r.call.config.TargetConfig(), r.call.GetExternalRemovedBlocks(), mode, r.deferred), nil + }) + if err != nil { + // we never return an error from within the once call, so this shouldn't + // happen + return nil + } + return stack +} + +func (r *RemovedStackCallInstance) InputVariableValues(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, r.tracingName()+" inputs", r.inputVariableValues.For(phase), + validateStackCallInstanceInputsFn(r.Stack(ctx, phase), r.call.config.config.Inputs, r.call.config.config.DeclRange.ToHCL().Ptr(), r, phase)) +} + +func (r *RemovedStackCallInstance) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + _, moreDiags := r.InputVariableValues(ctx, ApplyPhase) + diags = diags.Append(moreDiags) + + return nil, diags +} + +func (r *RemovedStackCallInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + _, moreDiags := r.InputVariableValues(ctx, PlanPhase) + diags = diags.Append(moreDiags) + + return nil, diags +} + +func (r *RemovedStackCallInstance) tracingName() string { + return r.from.String() + " (removed)" +} + +func (r *RemovedStackCallInstance) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return r.call.stack.resolveExpressionReference(ctx, ref, nil, r.repetition) +} + +func (r *RemovedStackCallInstance) PlanTimestamp() time.Time { + return r.call.stack.main.PlanTimestamp() +} + +func (r *RemovedStackCallInstance) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return r.call.stack.main.ProviderFunctions(ctx, r.call.stack.config) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go new file mode 100644 index 0000000000..186c8f0470 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -0,0 +1,973 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "iter" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Stack represents an instance of a [StackConfig] after it's had its +// repetition arguments (if any) evaluated to determine how many instances +// it has. +type Stack struct { + addr stackaddrs.StackInstance + parent *Stack + config *StackConfig + main *Main + deferred bool + mode plans.Mode + + removed *Removed // contains removed logic + + // The remaining fields memoize other objects we might create in response + // to method calls. Must lock "mu" before interacting with them. + mu sync.Mutex + inputVariables map[stackaddrs.InputVariable]*InputVariable + localValues map[stackaddrs.LocalValue]*LocalValue + stackCalls map[stackaddrs.StackCall]*StackCall + outputValues map[stackaddrs.OutputValue]*OutputValue + components map[stackaddrs.Component]*Component + providers map[stackaddrs.ProviderConfigRef]*Provider + removedInitialised bool +} + +var ( + _ ExpressionScope = (*Stack)(nil) + _ Plannable = (*Stack)(nil) + _ Applyable = (*Stack)(nil) +) + +func newStack( + main *Main, + addr stackaddrs.StackInstance, + parent *Stack, + config *StackConfig, + removed *Removed, + mode plans.Mode, + deferred bool) *Stack { + return &Stack{ + parent: parent, + config: config, + addr: addr, + deferred: deferred, + mode: mode, + main: main, + removed: removed, + } +} + +// ChildStack returns the child stack at the given address. +func (s *Stack) ChildStack(ctx context.Context, addr stackaddrs.StackInstanceStep, phase EvalPhase) *Stack { + callAddr := stackaddrs.StackCall{Name: addr.Name} + + if call := s.EmbeddedStackCalls()[callAddr]; call != nil { + instances, unknown := call.Instances(ctx, phase) + if unknown { + return call.UnknownInstance(ctx, addr.Key, phase).Stack(ctx, phase) + } + + if instance, exists := instances[addr.Key]; exists { + return instance.Stack(ctx, phase) + } + } + + calls := s.Removed().stackCalls[callAddr] + for _, call := range calls { + absolute := append(s.addr, addr) + + instances, unknown := call.InstancesFor(ctx, absolute, phase) + if unknown { + return call.UnknownInstance(ctx, absolute, phase).Stack(ctx, phase) + } + for _, instance := range instances { + return instance.Stack(ctx, phase) + } + } + + return nil +} + +// InputVariables returns a map of all of the input variables declared within +// this stack's configuration. +func (s *Stack) InputVariables() map[stackaddrs.InputVariable]*InputVariable { + s.mu.Lock() + defer s.mu.Unlock() + + // We intentionally save a non-nil map below even if it's empty so that + // we can unambiguously recognize whether we've loaded this or not. + if s.inputVariables != nil { + return s.inputVariables + } + + decls := s.config.config.Stack + ret := make(map[stackaddrs.InputVariable]*InputVariable, len(decls.InputVariables)) + for _, c := range decls.InputVariables { + absAddr := stackaddrs.AbsInputVariable{ + Stack: s.addr, + Item: stackaddrs.InputVariable{Name: c.Name}, + } + ret[absAddr.Item] = newInputVariable(s.main, absAddr, s, s.config.InputVariable(absAddr.Item)) + } + s.inputVariables = ret + return ret +} + +func (s *Stack) InputVariable(addr stackaddrs.InputVariable) *InputVariable { + return s.InputVariables()[addr] +} + +// LocalValues returns a map of all of the input variables declared within +// this stack's configuration. +func (s *Stack) LocalValues() map[stackaddrs.LocalValue]*LocalValue { + s.mu.Lock() + defer s.mu.Unlock() + + // We intentionally save a non-nil map below even if it's empty so that + // we can unambiguously recognize whether we've loaded this or not. + if s.localValues != nil { + return s.localValues + } + + decls := s.config.config.Stack + ret := make(map[stackaddrs.LocalValue]*LocalValue, len(decls.LocalValues)) + for _, c := range decls.LocalValues { + absAddr := stackaddrs.AbsLocalValue{ + Stack: s.addr, + Item: stackaddrs.LocalValue{Name: c.Name}, + } + ret[absAddr.Item] = newLocalValue(s.main, absAddr, s, s.config.LocalValue(absAddr.Item)) + } + s.localValues = ret + return ret +} + +// LocalValue returns the [LocalValue] specified by address +func (s *Stack) LocalValue(addr stackaddrs.LocalValue) *LocalValue { + return s.LocalValues()[addr] +} + +// InputsType returns an object type that the object representing the caller's +// values for this stack's input variables must conform to. +func (s *Stack) InputsType() (cty.Type, *typeexpr.Defaults) { + vars := s.InputVariables() + atys := make(map[string]cty.Type, len(vars)) + defs := &typeexpr.Defaults{ + DefaultValues: make(map[string]cty.Value), + Children: map[string]*typeexpr.Defaults{}, + } + var opts []string + for vAddr, v := range vars { + cfg := v.config + atys[vAddr.Name] = cfg.TypeConstraint() + if def := cfg.DefaultValue(); def != cty.NilVal { + defs.DefaultValues[vAddr.Name] = def + opts = append(opts, vAddr.Name) + } + if childDefs := cfg.NestedDefaults(); childDefs != nil { + defs.Children[vAddr.Name] = childDefs + } + } + retTy := cty.ObjectWithOptionalAttrs(atys, opts) + defs.Type = retTy + return retTy, defs +} + +// EmbeddedStackCalls returns a map of all of the embedded stack calls declared +// within this stack's configuration. +func (s *Stack) EmbeddedStackCalls() map[stackaddrs.StackCall]*StackCall { + s.mu.Lock() + defer s.mu.Unlock() + + // We intentionally save a non-nil map below even if it's empty so that + // we can unambiguously recognize whether we've loaded this or not. + if s.stackCalls != nil { + return s.stackCalls + } + + decls := s.config.config.Stack + ret := make(map[stackaddrs.StackCall]*StackCall, len(decls.EmbeddedStacks)) + for _, c := range decls.EmbeddedStacks { + absAddr := stackaddrs.AbsStackCall{ + Stack: s.addr, + Item: stackaddrs.StackCall{Name: c.Name}, + } + ret[absAddr.Item] = newStackCall(s.main, absAddr, s, s.config.StackCall(absAddr.Item)) + } + s.stackCalls = ret + return ret +} + +func (s *Stack) EmbeddedStackCall(addr stackaddrs.StackCall) *StackCall { + return s.EmbeddedStackCalls()[addr] +} + +func (s *Stack) RemovedEmbeddedStackCall(addr stackaddrs.StackCall) []*RemovedStackCall { + return s.Removed().stackCalls[addr] +} + +func (s *Stack) KnownEmbeddedStacks(addr stackaddrs.StackCall, phase EvalPhase) iter.Seq[stackaddrs.StackInstance] { + switch phase { + case PlanPhase: + return s.main.PlanPrevState().StackInstances(stackaddrs.AbsStackCall{ + Stack: s.addr, + Item: addr, + }) + case ApplyPhase: + return s.main.PlanBeingApplied().StackInstances(stackaddrs.AbsStackCall{ + Stack: s.addr, + Item: addr, + }) + default: + // We're not executing with an existing state in the other phases, so + // we have no known instances. + return func(yield func(stackaddrs.StackInstance) bool) {} + } +} + +func (s *Stack) Components() map[stackaddrs.Component]*Component { + s.mu.Lock() + defer s.mu.Unlock() + + // We intentionally save a non-nil map below even if it's empty so that + // we can unambiguously recognize whether we've loaded this or not. + if s.components != nil { + return s.components + } + + decls := s.config.config.Stack + ret := make(map[stackaddrs.Component]*Component, len(decls.Components)) + for _, c := range decls.Components { + absAddr := stackaddrs.AbsComponent{ + Stack: s.addr, + Item: stackaddrs.Component{Name: c.Name}, + } + ret[absAddr.Item] = newComponent(s.main, absAddr, s, s.config.Component(absAddr.Item)) + } + s.components = ret + return ret +} + +func (s *Stack) Component(addr stackaddrs.Component) *Component { + return s.Components()[addr] +} + +func (s *Stack) Removed() *Removed { + s.mu.Lock() + defer s.mu.Unlock() + + if s.removedInitialised { + return s.removed + } + + // otherwise we're going to initialise removed. + + for addr, configs := range s.config.RemovedComponents().All() { + blocks := make([]*RemovedComponent, 0, len(configs)) + for _, config := range configs { + blocks = append(blocks, newRemovedComponent(s.main, addr, s, config)) + } + + s.removed.AddComponent(addr, blocks) + } + + for addr, configs := range s.config.RemovedStackCalls().All() { + blocks := make([]*RemovedStackCall, 0, len(configs)) + for _, config := range configs { + blocks = append(blocks, newRemovedStackCall(s.main, addr, s, config)) + } + + s.removed.AddStackCall(addr, blocks) + } + + s.removedInitialised = true + return s.removed +} + +func (s *Stack) RemovedComponent(addr stackaddrs.Component) []*RemovedComponent { + return s.Removed().components[addr] +} + +// ApplyableComponents returns the combination of removed blocks and declared +// components for a given component address. +func (s *Stack) ApplyableComponents(addr stackaddrs.Component) (*Component, []*RemovedComponent) { + return s.Component(addr), s.RemovedComponent(addr) +} + +// KnownComponentInstances returns a set of the component instances that belong +// to the given component from the current state or plan. +func (s *Stack) KnownComponentInstances(component stackaddrs.Component, phase EvalPhase) iter.Seq[stackaddrs.ComponentInstance] { + switch phase { + case PlanPhase: + return s.main.PlanPrevState().ComponentInstances(stackaddrs.AbsComponent{ + Stack: s.addr, + Item: component, + }) + case ApplyPhase: + return s.main.PlanBeingApplied().ComponentInstanceAddresses(stackaddrs.AbsComponent{ + Stack: s.addr, + Item: component, + }) + default: + // We're not executing with an existing state in the other phases, so + // we have no known instances. + return func(yield func(stackaddrs.ComponentInstance) bool) {} + } +} + +func (s *Stack) ProviderByLocalAddr(localAddr stackaddrs.ProviderConfigRef) *Provider { + s.mu.Lock() + defer s.mu.Unlock() + + if existing, ok := s.providers[localAddr]; ok { + return existing + } + if s.providers == nil { + s.providers = make(map[stackaddrs.ProviderConfigRef]*Provider) + } + + decls := s.config.config.Stack + + sourceAddr, ok := decls.RequiredProviders.ProviderForLocalName(localAddr.ProviderLocalName) + if !ok { + return nil + } + configAddr := stackaddrs.AbsProviderConfig{ + Stack: s.addr, + Item: stackaddrs.ProviderConfig{ + Provider: sourceAddr, + Name: localAddr.Name, + }, + } + + provider := newProvider(s.main, configAddr, s, s.config.ProviderByLocalAddr(localAddr)) + s.providers[localAddr] = provider + return provider +} + +func (s *Stack) Provider(addr stackaddrs.ProviderConfig) *Provider { + decls := s.config.config.Stack + + localName, ok := decls.RequiredProviders.LocalNameForProvider(addr.Provider) + if !ok { + return nil + } + return s.ProviderByLocalAddr(stackaddrs.ProviderConfigRef{ + ProviderLocalName: localName, + Name: addr.Name, + }) +} + +func (s *Stack) Providers() map[stackaddrs.ProviderConfigRef]*Provider { + decls := s.config.config.Stack + if len(decls.ProviderConfigs) == 0 { + return nil + } + ret := make(map[stackaddrs.ProviderConfigRef]*Provider, len(decls.ProviderConfigs)) + // package stackconfig is using the addrs package for provider configuration + // addresses instead of stackaddrs, because it was written before we had + // stackaddrs, so we need to do some address adaptation for now. + // FIXME: Rationalize this so that stackconfig uses the stackaddrs types. + for weirdAddr := range decls.ProviderConfigs { + addr := stackaddrs.ProviderConfigRef{ + ProviderLocalName: weirdAddr.LocalName, + Name: weirdAddr.Alias, + } + ret[addr] = s.ProviderByLocalAddr(addr) + // FIXME: The above doesn't deal with the case where the provider + // block refers to an undeclared provider local name. What should + // we do in that case? Maybe it doesn't matter if package stackconfig + // validates that during configuration loading anyway. + } + return ret +} + +// OutputValues returns a map of all of the output values declared within +// this stack's configuration. +func (s *Stack) OutputValues() map[stackaddrs.OutputValue]*OutputValue { + s.mu.Lock() + defer s.mu.Unlock() + + // We intentionally save a non-nil map below even if it's empty so that + // we can unambiguously recognize whether we've loaded this or not. + if s.outputValues != nil { + return s.outputValues + } + + decls := s.config.config.Stack + ret := make(map[stackaddrs.OutputValue]*OutputValue, len(decls.OutputValues)) + for _, c := range decls.OutputValues { + absAddr := stackaddrs.AbsOutputValue{ + Stack: s.addr, + Item: stackaddrs.OutputValue{Name: c.Name}, + } + ret[absAddr.Item] = newOutputValue(s.main, absAddr, s, s.config.OutputValue(absAddr.Item)) + } + s.outputValues = ret + return ret +} + +func (s *Stack) OutputValue(addr stackaddrs.OutputValue) *OutputValue { + return s.OutputValues()[addr] +} + +func (s *Stack) ResultValue(ctx context.Context, phase EvalPhase) cty.Value { + ovs := s.OutputValues() + elems := make(map[string]cty.Value, len(ovs)) + for addr, ov := range ovs { + elems[addr.Name] = ov.ResultValue(ctx, phase) + } + return cty.ObjectVal(elems) +} + +// ResolveExpressionReference implements ExpressionScope, providing the +// global scope for evaluation within an already-instanciated stack during the +// plan and apply phases. +func (s *Stack) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return s.resolveExpressionReference(ctx, ref, nil, instances.RepetitionData{}) +} + +// ExternalFunctions implements ExpressionScope. +func (s *Stack) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return s.main.ProviderFunctions(ctx, s.config) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (s *Stack) PlanTimestamp() time.Time { + return s.main.PlanTimestamp() +} + +// resolveExpressionReference is a shared implementation of [ExpressionScope] +// used for this stack's scope and all of the nested scopes of declarations +// in the same stack, since they tend to differ only in what "self" means +// and what each.key, each.value, or count.index are set to (if anything). +func (s *Stack) resolveExpressionReference( + ctx context.Context, + ref stackaddrs.Reference, + selfAddr stackaddrs.Referenceable, + repetition instances.RepetitionData, +) (Referenceable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // "Test-only globals" is a special affordance we have only when running + // unit tests in this package. The function called in this branch will + // return an error itself if we're not running in a suitable test situation. + if addr, ok := ref.Target.(stackaddrs.TestOnlyGlobal); ok { + return s.main.resolveTestOnlyGlobalReference(addr, ref.SourceRange) + } + + // TODO: Most of the below would benefit from "Did you mean..." suggestions + // when something is missing but there's a similarly-named object nearby. + + // See also a very similar function in stack_config.go. Both are returning + // similar referenceable objects but the context is different. For example, + // in this function we return an instanced Component, while in the other + // function we return a static ComponentConfig. + // + // Some of the returned types are the same across both functions, but most + // are different in terms of static vs dynamic types. + switch addr := ref.Target.(type) { + case stackaddrs.InputVariable: + ret := s.InputVariable(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared input variable", + Detail: fmt.Sprintf("There is no variable %q block declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.LocalValue: + ret := s.LocalValue(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared local value", + Detail: fmt.Sprintf("There is no local %q declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.Component: + ret := s.Component(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared component", + Detail: fmt.Sprintf("There is no component %q block declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.StackCall: + ret := s.EmbeddedStackCall(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared embedded stack", + Detail: fmt.Sprintf("There is no stack %q block declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.ProviderConfigRef: + ret := s.ProviderByLocalAddr(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared provider configuration", + Detail: fmt.Sprintf("There is no provider %q %q block declared in this stack.", addr.ProviderLocalName, addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.ContextualRef: + switch addr { + case stackaddrs.EachKey: + if repetition.EachKey == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'each' reference", + Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.EachKey}, diags + case stackaddrs.EachValue: + if repetition.EachValue == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'each' reference", + Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.EachValue}, diags + case stackaddrs.CountIndex: + if repetition.CountIndex == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'count' reference", + Detail: "The special symbol 'count' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'count' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.CountIndex}, diags + case stackaddrs.Self: + if selfAddr != nil { + // We'll just pretend the reference was to whatever "self" + // is referring to, then. + ref.Target = selfAddr + return s.resolveExpressionReference(ctx, ref, nil, repetition) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'self' reference", + Detail: "The special symbol 'self' is not defined in this location.", + Context: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + case stackaddrs.TerraformApplying: + return JustValue{cty.BoolVal(s.main.Applying()).Mark(marks.Ephemeral)}, diags + default: + // The above should be exhaustive for all defined values of this type. + panic(fmt.Sprintf("unsupported ContextualRef %#v", addr)) + } + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", addr.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } +} + +// PlanChanges implements Plannable for the root stack by emitting a planned +// change for each output value. +// +// It does nothing for a non-root stack, because embedded stacks themselves are +// just inert containers; the plan walk driver must also explore everything +// nested inside the stack and plan those separately. +func (s *Stack) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // We're going to validate that all the removed blocks in this stack resolve + // to unique instance addresses. + for _, blocks := range s.Removed().components { + seen := make(map[addrs.InstanceKey]*RemovedComponentInstance) + for _, block := range blocks { + insts, unknown := block.InstancesFor(ctx, s.addr, PlanPhase) + if unknown { + continue + } + + for _, inst := range insts { + if existing, exists := seen[inst.from.Item.Key]; exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid `from` attribute", + Detail: fmt.Sprintf("The `from` attribute resolved to component instance %s, which is already claimed by another removed block at %s.", inst.from, existing.call.config.config.DeclRange.ToHCL()), + Subject: inst.call.config.config.DeclRange.ToHCL().Ptr(), + }) + continue + } + seen[inst.from.Item.Key] = inst + } + } + } + + for _, blocks := range s.Removed().stackCalls { + seen := collections.NewMap[stackaddrs.StackInstance, *RemovedStackCallInstance]() + for _, block := range blocks { + insts, unknown := block.InstancesFor(ctx, s.addr, PlanPhase) + if unknown { + continue + } + + for _, inst := range insts { + if existing, exists := seen.GetOk(inst.from); exists { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid `from` attribute", + Detail: fmt.Sprintf("The `from` attribute resolved to stack instance %s, which is already claimed by another removed block at %s.", inst.from, existing.call.config.config.DeclRange.ToHCL()), + Subject: inst.call.config.config.DeclRange.ToHCL().Ptr(), + }) + continue + } + seen.Put(inst.from, inst) + } + } + } + + if !s.addr.IsRoot() { + // Nothing more to do for non-root stacks + return nil, diags + } + + // We want to check that all of the components we have in state are + // targeted by something (either a component or a removed block) in + // the configuration. + // + // The root stack analysis is the best place to do this. We must do this + // during the plan (and not during the analysis) because we may have + // for-each attributes that need to be expanded before we can determine + // if a component is targeted. + + var changes []stackplan.PlannedChange +Instance: + for inst := range s.main.PlanPrevState().AllComponentInstances() { + + // We track here whether this component instance has any associated + // resources. If this component is empty, and not referenced in the + // configuration, then we won't return an error. Instead, we'll just + // mark this as to-be deleted. There could have been some error + // marking the state previously, but whatever it is we can just fix + // this so why bother the user with it. + empty := s.main.PlanPrevState().ComponentInstanceResourceInstanceObjects(inst).Len() == 0 + + stack := s.main.Stack(ctx, inst.Stack, PlanPhase) + if stack == nil { + if empty { + changes = append(changes, &stackplan.PlannedChangeComponentInstanceRemoved{ + Addr: inst, + }) + continue + } + + // Normally, this is a simple error. The user has deleted an entire + // stack without adding an equivalent removed block for the stack + // so now the instances in that stack are all unclaimed. + // + // However, the user may have tried to write removed blocks that + // target specific components within a removed stack instead of + // just targeting the entire stack. This is invalid, for one it is + // easier for the user if they could just remove the whole stack, + // and for two it is very difficult for us to reconcile orphaned + // removed components and removed embedded stacks that could be + // floating anywhere in the configuration - instead, we'll just + // not allow this. + // + // In this case, we want to change the error message to be more + // user-friendly than the generic one, so we need to discover if + // this has happened here, and if so, modify the error message. + + removed, _ := s.validateMissingInstanceAgainstRemovedBlocks(ctx, inst, PlanPhase) + if removed != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid removed block", + Detail: fmt.Sprintf("The component instance %s could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", inst.String()), + Subject: removed.DeclRange.ToHCL().Ptr(), + }) + continue + } + + // If we fall out here, then we found no relevant removed blocks + // so we can return the generic error message! + + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unclaimed component instance", + fmt.Sprintf("The component instance %s is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.", inst.String()), + )) + continue + } + + component, removeds := stack.ApplyableComponents(inst.Item.Component) + if component != nil { + insts, unknown := component.Instances(ctx, PlanPhase) + if unknown { + // We can't determine if the component is targeted or not. This + // is okay, as any changes to this component will be deferred + // anyway and a follow up plan will then detect the missing + // component if it exists. + continue + } + + if _, exists := insts[inst.Item.Key]; exists { + // This component is targeted by a component block, so we won't + // add an error. + continue + } + } + + for _, removed := range removeds { + insts, unknown := removed.InstancesFor(ctx, inst.Stack, PlanPhase) + if unknown { + // We can't determine if the component is targeted or not. This + // is okay, as any changes to this component will be deferred + // anyway and a follow up plan will then detect the missing + // component if it exists. + continue Instance + } + + for _, i := range insts { + // the instance key for a removed block doesn't always translate + // directly into the instance key in the address, so we have + // to check for the correct one. + if i.from.Item.Key == inst.Item.Key { + continue Instance + } + } + } + + // Otherwise, we have a component that is not targeted by anything in + // the configuration. + + if empty { + // It's empty, so we can just remove it. + changes = append(changes, &stackplan.PlannedChangeComponentInstanceRemoved{ + Addr: inst, + }) + continue + } + + // Otherwise, it's an error. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unclaimed component instance", + fmt.Sprintf("The component instance %s is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.", inst.String()), + )) + } + + // Finally, we'll look at the input and output values we have in state + // and any that do not appear in the configuration we'll mark as deleted. + + for addr, value := range s.main.PlanPrevState().RootOutputValues() { + if s.OutputValue(addr) != nil { + // Then this output value is in the configuration, and will be + // processed independently. + continue + } + + // Otherwise, it's been removed from the configuration. + changes = append(changes, &stackplan.PlannedChangeOutputValue{ + Addr: addr, + Action: plans.Delete, + Before: value, + After: cty.NullVal(cty.DynamicPseudoType), + }) + } + + // Finally, we'll look at the input variables we have in state and delete + // any that don't appear in the configuration any more. + + for addr, variable := range s.main.PlanPrevState().RootInputVariables() { + if s.InputVariable(addr) != nil { + // Then this input variable is in the configuration, and will + // be processed independently. + continue + } + + // Otherwise, we'll add a delete notification for this root input + // variable. + changes = append(changes, &stackplan.PlannedChangeRootInputValue{ + Addr: addr, + Action: plans.Delete, + Before: variable, + After: cty.NullVal(cty.DynamicPseudoType), + }) + } + + return changes, diags +} + +// CheckApply implements Applyable. +func (s *Stack) CheckApply(_ context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + if !s.addr.IsRoot() { + return nil, nil + } + + var diags tfdiags.Diagnostics + var changes []stackstate.AppliedChange + + // We're also just going to quickly emit any cleanup . These remaining + // values are basically just everything that have been in the configuration + // in the past but is no longer and so needs to be removed from the state. + + for value := range s.main.PlanBeingApplied().DeletedOutputValues.All() { + changes = append(changes, &stackstate.AppliedChangeOutputValue{ + Addr: value, + Value: cty.NilVal, + }) + } + + for value := range s.main.PlanBeingApplied().DeletedInputVariables.All() { + changes = append(changes, &stackstate.AppliedChangeInputVariable{ + Addr: value, + Value: cty.NilVal, + }) + } + + for value := range s.main.PlanBeingApplied().DeletedComponents.All() { + changes = append(changes, &stackstate.AppliedChangeComponentInstanceRemoved{ + ComponentAddr: stackaddrs.AbsComponent{ + Stack: value.Stack, + Item: value.Item.Component, + }, + ComponentInstanceAddr: value, + }) + } + + return changes, diags +} + +func (s *Stack) tracingName() string { + addr := s.addr + if addr.IsRoot() { + return "root stack" + } + return addr.String() +} + +// validateMissingInstanceAgainstRemovedBlocks returns the removed config most +// applicable to the target address if it exists. +// +// We have an edge case where a user has written a removed block that targets +// a stacks or components within stacks that are not defined anywhere in the +// stack (either in a removed blocks or an embedded stack). We consider this to +// be an error - if you remove an entire stack from the configuration then you +// should write a removed block that targets that stack not several removed +// blocks that target things inside the removed block. +// +// The above edge case is exposed when we check that all component instances +// in state are included in the plan. This function is called with the absolute +// address of the problematic component (the target). The error we would +// normally return would say that the component isn't targeted by any component +// or removed blocks. This is misleading for the discussed edge case, as the +// user may have written a removed block that targets the component specifically +// but it is just not getting executed as it is in a stack that is also not +// in the configuration. +// +// The function aims to discover if a removed block does exist that might target +// this component. Note, that since we can have removed blocks that target +// entire stacks we do check both removed blocks and direct components on the +// assumption that a removed stack might expand to include the target component +// and we want to capture that removed stack specifically. +func (s *Stack) validateMissingInstanceAgainstRemovedBlocks(ctx context.Context, target stackaddrs.AbsComponentInstance, phase EvalPhase) (*stackconfig.Removed, *stackconfig.Component) { + if len(target.Stack) == 0 { + + // First, we'll handle the simple case. This means we are actually + // targeting a component that should be in the current stack, so we'll + // just look to see if there is a removed block that targets this + // component directly. + + components, ok := s.Removed().components[target.Item.Component] + if ok { + for _, component := range components { + // we have the component, let's check the + insts, _ := component.InstancesFor(ctx, s.addr, phase) + if inst, ok := insts[target.Item.Key]; ok { + return inst.call.config.config, nil + } + } + } + + if component := s.Component(target.Item.Component); component != nil { + insts, _ := component.Instances(ctx, phase) + if inst, ok := insts[target.Item.Key]; ok { + return nil, inst.call.config.config + } + } + + return nil, nil + } + + // more complicated now, we need to look into a child stack + + next := target.Stack[0] + rest := stackaddrs.AbsComponentInstance{ + Stack: target.Stack[1:], + Item: target.Item, + } + + if child := s.ChildStack(ctx, next, phase); child != nil { + return child.validateMissingInstanceAgainstRemovedBlocks(ctx, rest, phase) + } + + // if we get here, then we had no child stack to check against. But, things + // are not over yet! we also have might have orphaned removed blocks. + // these are tracked in the Removed() struct directly, so we'll also look + // into there. this is the actual troublesome case we're checking for so + // we do expect to actually get here for these checks. + + if child, ok := s.Removed().children[next.Name]; ok { + return child.validateMissingInstanceAgainstRemovedBlocks(ctx, append(s.addr, next), rest, phase) + } + + return nil, nil +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call.go b/internal/stacks/stackruntime/internal/stackeval/stack_call.go new file mode 100644 index 0000000000..eecfcb382b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call.go @@ -0,0 +1,286 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StackCall represents a "stack" block in a stack configuration after +// its containing stacks have been expanded into stack instances. +type StackCall struct { + addr stackaddrs.AbsStackCall + stack *Stack + config *StackCallConfig + + main *Main + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + instances perEvalPhase[promising.Once[withDiagnostics[instancesResult[*StackCallInstance]]]] + + unknownInstancesMutex sync.Mutex + unknownInstances map[addrs.InstanceKey]*StackCallInstance +} + +var _ Plannable = (*StackCall)(nil) +var _ Referenceable = (*StackCall)(nil) + +func newStackCall(main *Main, addr stackaddrs.AbsStackCall, stack *Stack, config *StackCallConfig) *StackCall { + return &StackCall{ + addr: addr, + main: main, + stack: stack, + config: config, + unknownInstances: make(map[addrs.InstanceKey]*StackCallInstance), + } +} + +// GetExternalRemovedBlocks fetches the removed blocks that target the stack +// instances being created by this stack call. +func (c *StackCall) GetExternalRemovedBlocks() *Removed { + return c.stack.Removed().Next(c.addr.Item.Name) +} + +// ForEachValue returns the result of evaluating the "for_each" expression +// for this stack call, with the following exceptions: +// - If the stack call doesn't use "for_each" at all, returns [cty.NilVal]. +// - If the for_each expression is present but too invalid to evaluate, +// returns [cty.DynamicVal] to represent that the for_each value cannot +// be determined. +// +// A present and valid "for_each" expression produces a result that's +// guaranteed to be: +// - Either a set of strings, a map of any element type, or an object type +// - Known and not null (only the top-level value) +// - Not sensitive (only the top-level value) +func (c *StackCall) ForEachValue(ctx context.Context, phase EvalPhase) cty.Value { + ret, _ := c.CheckForEachValue(ctx, phase) + return ret +} + +// CheckForEachValue evaluates the "for_each" expression if present, validates +// that its value is valid, and then returns that value. +// +// If this call does not use "for_each" then this immediately returns cty.NilVal +// representing the absense of the value. +// +// If the diagnostics does not include errors and the result is not cty.NilVal +// then callers can assume that the result value will be: +// - Either a set of strings, a map of any element type, or an object type +// - Known and not null (except for nested map/object element values) +// - Not sensitive (only the top-level value) +// +// If the diagnostics _does_ include errors then the result might be +// [cty.DynamicVal], which represents that the for_each expression was so invalid +// that we cannot know the for_each value. +func (c *StackCall) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + val, diags := doOnceWithDiags( + ctx, c.tracingName()+" for_each", c.forEachValue.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + cfg := c.config.config + + switch { + + case cfg.ForEach != nil: + result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, c.stack, "stack") + diags = diags.Append(moreDiags) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + + return result.Value, diags + + default: + // This stack config doesn't use for_each at all + return cty.NilVal, diags + } + }, + ) + if val == cty.NilVal && diags.HasErrors() { + // We use cty.DynamicVal as the placeholder for an invalid for_each, + // to represent "unknown for_each value" as distinct from "no for_each + // expression at all". + val = cty.DynamicVal + } + return val, diags +} + +// Instances returns all of the instances of the call known to be declared +// by the configuration. +// +// Calcluating this involves evaluating the call's for_each expression if any, +// and so this call may block on evaluation of other objects in the +// configuration. +// +// If the configuration has an invalid definition of the instances then the +// result will be nil. Callers that need to distinguish between invalid +// definitions and valid definitions of zero instances can rely on the +// result being a non-nil zero-length map in the latter case. +// +// This function doesn't return any diagnostics describing ways in which the +// for_each expression is invalid because we assume that the main plan walk +// will visit the stack call directly and ask it to check itself, and that +// call will be the one responsible for returning any diagnostics. +func (c *StackCall) Instances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*StackCallInstance, bool) { + ret, unknown, _ := c.CheckInstances(ctx, phase) + return ret, unknown +} + +func (c *StackCall) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*StackCallInstance, bool, tfdiags.Diagnostics) { + result, diags := doOnceWithDiags( + ctx, c.tracingName()+" instances", c.instances.For(phase), + func(ctx context.Context) (instancesResult[*StackCallInstance], tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + forEachVal, forEachValueDiags := c.CheckForEachValue(ctx, phase) + + diags = diags.Append(forEachValueDiags) + if diags.HasErrors() { + return instancesResult[*StackCallInstance]{}, diags + } + + return instancesMap(forEachVal, func(ik addrs.InstanceKey, rd instances.RepetitionData) *StackCallInstance { + return newStackCallInstance(c, ik, rd, c.stack.mode, c.stack.deferred) + }), diags + }, + ) + return result.insts, result.unknown, diags +} + +func (c *StackCall) UnknownInstance(ctx context.Context, key addrs.InstanceKey, phase EvalPhase) *StackCallInstance { + c.unknownInstancesMutex.Lock() + defer c.unknownInstancesMutex.Unlock() + + if inst, ok := c.unknownInstances[key]; ok { + return inst + } + + forEachType := c.ForEachValue(ctx, phase).Type() + repetitionData := instances.UnknownForEachRepetitionData(forEachType) + if key != addrs.WildcardKey { + repetitionData.EachKey = key.Value() + } + + inst := newStackCallInstance(c, key, repetitionData, c.stack.mode, true) + c.unknownInstances[key] = inst + return inst +} + +func (c *StackCall) ResultValue(ctx context.Context, phase EvalPhase) cty.Value { + decl := c.config.config + insts, unknown := c.Instances(ctx, phase) + childResultType := c.config.TargetConfig().ResultType() + + switch { + case decl.ForEach != nil: + + if unknown { + // We don't know what instances we have, so we can't know what + // the result will be. + return cty.UnknownVal(cty.Map(childResultType)) + } + + if insts == nil { + // Then we errored during instance calculation, this should have + // already been reported. + return cty.NilVal + } + + // We expect that the instances all have string keys, which will + // become the keys of a map that we're returning. + elems := make(map[string]cty.Value, len(insts)) + for instKey, inst := range insts { + k, ok := instKey.(addrs.StringKey) + if !ok { + panic(fmt.Sprintf("stack call with for_each has invalid instance key of type %T", instKey)) + } + elems[string(k)] = inst.Stack(ctx, phase).ResultValue(ctx, phase) + } + if len(elems) == 0 { + return cty.MapValEmpty(childResultType) + } + return cty.MapVal(elems) + + default: + if insts == nil { + // If we don't even know what instances we have then all we can + // say is that our result ought to have an object type + // constructed from the child stack's output values. + return cty.UnknownVal(childResultType) + } + if len(insts) != 1 { + // Should not happen: we should have exactly one instance with addrs.NoKey + panic("single-instance stack call does not have exactly one instance") + } + inst, ok := insts[addrs.NoKey] + if !ok { + panic("single-instance stack call does not have an addrs.NoKey instance") + } + + return inst.Stack(ctx, phase).ResultValue(ctx, phase) + } +} + +// ExprReferenceValue implements Referenceable. +func (c *StackCall) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + return c.ResultValue(ctx, phase) +} + +func (c *StackCall) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, _, moreDiags := c.CheckInstances(ctx, phase) + diags = diags.Append(moreDiags) + + // All of the other arguments in a stack call get evaluated separately + // for each instance of the call, so [StackCallInstance] must deal + // with those. + + return diags +} + +// PlanChanges implements Plannable to perform "plan-time validation" of the +// stack call. +// +// This does not validate the instances of the stack call or the child stack +// instances they imply, so the plan walk driver must also call +// [StackCall.Instances] and explore the child objects directly. +func (c *StackCall) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + // Stack calls never contribute "planned changes" directly, but we + // can potentially generate diagnostics if the call configuration is + // invalid. This is therefore more a "plan-time validation" than actually + // planning. + return nil, c.checkValid(ctx, PlanPhase) +} + +// References implements Referrer +func (c *StackCall) References(context.Context) []stackaddrs.AbsReference { + cfg := c.config.config + var ret []stackaddrs.Reference + ret = append(ret, ReferencesInExpr(cfg.ForEach)...) + ret = append(ret, ReferencesInExpr(cfg.Inputs)...) + ret = append(ret, referencesInTraversals(cfg.DependsOn)...) + return makeReferencesAbsolute(ret, c.addr.Stack) +} + +// CheckApply implements Applyable. +func (c *StackCall) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + return nil, c.checkValid(ctx, ApplyPhase) +} + +func (c *StackCall) tracingName() string { + return c.addr.String() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go new file mode 100644 index 0000000000..166324ad7b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go @@ -0,0 +1,371 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StackCallConfig represents a "stack" block in a stack configuration, +// representing a call to an embedded stack. +type StackCallConfig struct { + addr stackaddrs.ConfigStackCall + config *stackconfig.EmbeddedStack + stack *StackConfig + + main *Main + + forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] + inputVariableValues perEvalPhase[promising.Once[withDiagnostics[map[stackaddrs.InputVariable]cty.Value]]] + resultValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ Validatable = (*StackCallConfig)(nil) +var _ Referenceable = (*StackCallConfig)(nil) +var _ ExpressionScope = (*StackCallConfig)(nil) + +func newStackCallConfig(main *Main, addr stackaddrs.ConfigStackCall, stack *StackConfig, config *stackconfig.EmbeddedStack) *StackCallConfig { + return &StackCallConfig{ + addr: addr, + config: config, + stack: stack, + main: main, + } +} + +func (s *StackCallConfig) tracingName() string { + return s.addr.String() +} + +// TargetConfig returns the object representing the child stack configuration +// that this stack call is referring to. +func (s *StackCallConfig) TargetConfig() *StackConfig { + return s.stack.ChildConfig(stackaddrs.StackStep{Name: s.addr.Item.Name}) +} + +// ResultType returns the type of the overall result value for this call. +// +// If this call uses for_each then the result type is a map of object types. +// If it has no repetition then it's just a naked object type. +func (s *StackCallConfig) ResultType() cty.Type { + // The result type of each of our instances is an object type constructed + // from all of the declared output values in the child stack. + calleeStack := s.TargetConfig() + calleeOutputs := calleeStack.OutputValues() + atys := make(map[string]cty.Type, len(calleeOutputs)) + for addr, ov := range calleeOutputs { + atys[addr.Name] = ov.ValueTypeConstraint() + } + instTy := cty.Object(atys) + + switch { + case s.config.ForEach != nil: + return cty.Map(instTy) + default: + // No repetition + return instTy + } +} + +// ValidateForEachValue validates and returns the value from this stack call's +// for_each argument, or returns [cty.NilVal] if it doesn't use for_each. +// +// If the for_each expression is invalid in some way then the returned +// diagnostics will contain errors and the returned value will be a placeholder +// unknown value. +func (s *StackCallConfig) ValidateForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return withCtyDynamicValPlaceholder(doOnceWithDiags( + ctx, s.tracingName()+" for_each", s.forEachValue.For(phase), + s.validateForEachValueInner(phase), + )) +} + +func (s *StackCallConfig) validateForEachValueInner(phase EvalPhase) func(context.Context) (cty.Value, tfdiags.Diagnostics) { + return func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if s.config.ForEach == nil { + // This stack config isn't even using for_each. + return cty.NilVal, diags + } + + result, moreDiags := evaluateForEachExpr(ctx, s.config.ForEach, phase, s.stack, "stack") + diags = diags.Append(moreDiags) + return result.Value, diags + } +} + +// ValidateInputVariableValues evaluates the "inputs" argument inside the +// configuration block, ensure that it's valid per the expectations of the +// child stack config, and then returns the resulting values. +// +// A [StackCallConfig] represents the not-yet-expanded stack call, so the +// result is an approximation of the input variables for all instances of +// this call. To get the final values for a particular instance, use +// [StackCall.InputVariableValues] instead. +// +// If the returned diagnostics contains errors then the returned values may +// be incomplete, but should at least be of the types specified in the +// variable declarations. +func (s *StackCallConfig) ValidateInputVariableValues(ctx context.Context, phase EvalPhase) (map[stackaddrs.InputVariable]cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags( + ctx, s.tracingName()+" inputs", s.inputVariableValues.For(phase), + validateStackCallInputsFn(s.config.Inputs, s.config.DeclRange.ToHCL(), s.TargetConfig(), s, phase), + ) +} + +// InputVariableValues returns the effective input variable values specified +// in this call, or correctly-typed placeholders if any values are invalid. +// +// This is intended to support downstream evaluation of other objects during +// the validate phase, rather than for direct validation of this object. If you +// are intending to report problems directly to the user, use +// [StackCallConfig.ValidateInputVariableValues] instead. +func (s *StackCallConfig) InputVariableValues(ctx context.Context, phase EvalPhase) map[stackaddrs.InputVariable]cty.Value { + ret, _ := s.ValidateInputVariableValues(ctx, phase) + return ret +} + +// ResultValue returns a suitable placeholder value to use to approximate the +// result of this call during the validation phase, where we typically don't +// yet have access to all necessary information. +// +// If the stack configuration is itself invalid then this will still return +// a suitably-typed unknown value, to permit partial validation downstream. +// +// The result is a good value to use for resolving "stack.foo" references +// in expressions elsewhere while running in validation mode. +func (s *StackCallConfig) ResultValue(ctx context.Context, phase EvalPhase) cty.Value { + v, _ := s.ValidateResultValue(ctx, phase) + return v +} + +// ValidateResultValue returns a validation-time approximation of the overall +// result of the embedded stack call, along with diagnostics describing any +// problems with the stack call itself (NOT with the child stack that was called) +// that we discover in the process of building it. +// +// During validation we don't perform instance expansion of any embedded stacks +// and so the validation-time approximation of a multi-instance embedded stack +// is always an unknown value with a suitable type constraint, allowing +// downstream references to detect type-related errors but not value-related +// errors. +func (s *StackCallConfig) ValidateResultValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return withCtyDynamicValPlaceholder(doOnceWithDiags( + ctx, s.tracingName()+" collected outputs", s.resultValue.For(phase), + func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // Our result is really just all of the output values of all of our + // instances aggregated together into a single data structure, but + // we do need to do this a little differently depending on what + // kind of repetition (if any) this stack call is using. + switch { + case s.config.ForEach != nil: + // The call uses for_each, and so we can't actually build a known + // result just yet because we don't know yet how many instances + // there will be and what their keys will be. We'll just construct + // an unknown value of a suitable type instead. + return cty.UnknownVal(s.ResultType()), diags + default: + // No repetition at all, then. In this case we _can_ attempt to + // construct at least a partial result, because we already know + // there will be exactly one instance and can assume that + // the output value implementation will provide a suitable + // approximation of the final value. + calleeStack := s.TargetConfig() + calleeOutputs := calleeStack.OutputValues() + attrs := make(map[string]cty.Value, len(calleeOutputs)) + for addr, ov := range calleeOutputs { + attrs[addr.Name] = ov.Value(ctx, phase) + } + return cty.ObjectVal(attrs), diags + } + }, + )) +} + +// ResolveExpressionReference implements ExpressionScope for evaluating +// expressions within a "stack" block during the validation phase. +// +// Note that the "stack" block lives in the caller scope rather than the +// callee scope, so this scope is not appropriate for evaluating anything +// inside the child variable declarations: they belong to the callee +// scope. +// +// This scope produces an approximation of expression results that is true +// for all instances of the stack call, not final results for a specific +// instance of a stack call. This is not the right scope to use during the +// plan and apply phases. +func (s *StackCallConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + repetition := instances.RepetitionData{} + if s.config.ForEach != nil { + // We're producing an approximation across all eventual instances + // of this call, so we'll set each.key and each.value to unknown + // values. + repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull() + repetition.EachValue = cty.DynamicVal + } + ret, diags := s.stack.resolveExpressionReference(ctx, ref, nil, repetition) + + if _, ok := ret.(*ProviderConfig); ok { + // We can't reference other providers from anywhere inside an embedded + // stack call - they should define their own providers. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", ref.Target.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + } + + return ret, diags +} + +// ExternalFunctions implements ExpressionScope. +func (s *StackCallConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return s.main.ProviderFunctions(ctx, s.stack) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (s *StackCallConfig) PlanTimestamp() time.Time { + return s.main.PlanTimestamp() +} + +func (s *StackCallConfig) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + _, moreDiags := s.ValidateForEachValue(ctx, phase) + diags = diags.Append(moreDiags) + _, moreDiags = s.ValidateInputVariableValues(ctx, phase) + diags = diags.Append(moreDiags) + _, moreDiags = s.ValidateResultValue(ctx, phase) + diags = diags.Append(moreDiags) + moreDiags = ValidateDependsOn(s.stack, s.config.DependsOn) + diags = diags.Append(moreDiags) + return diags +} + +// Validate implements Validatable +func (s *StackCallConfig) Validate(ctx context.Context) tfdiags.Diagnostics { + return s.checkValid(ctx, ValidatePhase) +} + +// PlanChanges implements Plannable. +func (s *StackCallConfig) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + return nil, s.checkValid(ctx, PlanPhase) +} + +// ExprReferenceValue implements Referenceable. +func (s *StackCallConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value { + return s.ResultValue(ctx, phase) +} + +func validateStackCallInputsFn(inputs hcl.Expression, callRange hcl.Range, config *StackConfig, scope ExpressionScope, phase EvalPhase) func(ctx context.Context) (map[stackaddrs.InputVariable]cty.Value, tfdiags.Diagnostics) { + return func(ctx context.Context) (map[stackaddrs.InputVariable]cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + vars := config.InputVariables() + + atys := make(map[string]cty.Type, len(vars)) + var optional []string + defs := make(map[string]cty.Value, len(vars)) + for addr, v := range vars { + aty := v.TypeConstraint() + + atys[addr.Name] = aty + if def := v.DefaultValue(); def != cty.NilVal { + optional = append(optional, addr.Name) + defs[addr.Name] = def + } + } + + oty := cty.ObjectWithOptionalAttrs(atys, optional) + + var varsObj cty.Value + var hclCtx *hcl.EvalContext // NOTE: remains nil when h.config.Inputs is unset + var inputsRange hcl.Range + if inputs != nil { + result, moreDiags := EvalExprAndEvalContext(ctx, inputs, phase, scope) + v := result.Value + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + v = cty.UnknownVal(oty.WithoutOptionalAttributesDeep()) + } + varsObj = v + hclCtx = result.EvalContext + inputsRange = inputs.Range() + } else { + varsObj = cty.EmptyObjectVal + inputsRange = callRange + } + + // FIXME: TODO: We need to apply the nested optional attribute defaults + // somewhere in here too, but it isn't clear where we should do that since + // we're supposed to do that before type conversion but we don't yet have + // the isolated variable values to apply the defaults to. + + varsObj, err := convert.Convert(varsObj, oty) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid input variable definitions", + Detail: fmt.Sprintf( + "Unsuitable input variable definitions: %s.", + tfdiags.FormatError(err), + ), + Subject: inputsRange.Ptr(), + + // NOTE: The following two will be nil if the author didn't + // actually define the "inputs" argument, but that's okay + // because these fields are both optional anyway. + Expression: inputs, + EvalContext: hclCtx, + }) + varsObj = cty.UnknownVal(oty.WithoutOptionalAttributesDeep()) + } + + ret := make(map[stackaddrs.InputVariable]cty.Value, len(vars)) + + for addr := range vars { + val := varsObj.GetAttr(addr.Name) + if val.IsNull() { + if def, ok := defs[addr.Name]; ok { + ret[addr] = def + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing definition for required input variable", + Detail: fmt.Sprintf("The input variable %q is required, so cannot be omitted.", addr.Name), + Subject: inputs.Range().Ptr(), + + // NOTE: The following two will be nil if the author didn't + // actually define the "inputs" argument, but that's okay + // because these fields are both optional anyway. + Expression: inputs, + EvalContext: hclCtx, + }) + ret[addr] = cty.UnknownVal(atys[addr.Name]) + } + } else { + ret[addr] = val + } + } + + return ret, diags + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call_instance.go b/internal/stacks/stackruntime/internal/stackeval/stack_call_instance.go new file mode 100644 index 0000000000..c69a9613e6 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call_instance.go @@ -0,0 +1,270 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StackCallInstance represents an instance of a [StackCall], acting as +// an expression scope for evaluating the expressions inside the configuration +// block. +// +// This does not represent the child stack object itself, although if you +// are holding a valid [StackCallInstance] then you can call +// [StackCallInstance.CalledStack] to get that stack. +type StackCallInstance struct { + call *StackCall + key addrs.InstanceKey + deferred bool + mode plans.Mode + + main *Main + + repetition instances.RepetitionData + + stack perEvalPhase[promising.Once[*Stack]] + inputVariableValues perEvalPhase[promising.Once[withDiagnostics[cty.Value]]] +} + +var _ ExpressionScope = (*StackCallInstance)(nil) +var _ Plannable = (*StackCallInstance)(nil) + +func newStackCallInstance(call *StackCall, key addrs.InstanceKey, repetition instances.RepetitionData, mode plans.Mode, deferred bool) *StackCallInstance { + return &StackCallInstance{ + call: call, + key: key, + deferred: deferred, + mode: mode, + main: call.main, + repetition: repetition, + } +} + +func (c *StackCallInstance) RepetitionData() instances.RepetitionData { + return c.repetition +} + +func (c *StackCallInstance) CalledStackAddr() stackaddrs.StackInstance { + callAddr := c.call.addr + callerAddr := callAddr.Stack + return callerAddr.Child(callAddr.Item.Name, c.key) + +} + +func (c *StackCallInstance) Stack(ctx context.Context, phase EvalPhase) *Stack { + stack, err := c.stack.For(phase).Do(ctx, c.tracingName(), func(ctx context.Context) (*Stack, error) { + return newStack(c.main, c.CalledStackAddr(), c.call.stack, c.call.config.TargetConfig(), c.call.GetExternalRemovedBlocks(), c.mode, c.deferred), nil + }) + if err != nil { + // we don't have cycles in here, and we don't return an error so this + // should never happen. + panic(err) + } + return stack +} + +// InputVariableValues returns the [cty.Value] representing the input variable +// values to pass to the child stack. +// +// If the definition of the input variable values is invalid then the result +// is [cty.DynamicVal] to represent that the values aren't known. +func (c *StackCallInstance) InputVariableValues(ctx context.Context, phase EvalPhase) cty.Value { + v, _ := c.CheckInputVariableValues(ctx, phase) + return v +} + +// CheckInputVariableValues returns the [cty.Value] representing the input +// variable values to pass to the child stack. +// +// If the configuration is valid then the resulting value is always of an +// object type derived from the child stack's input variable declarations. +// The resulting object type is guaranteed to have an attribute for each of +// the child stack's input variables, whose type conforms to the input +// variable's declared type constraint. +// +// If the configuration is invalid then the returned diagnostics will have +// errors and the result value will be [cty.DynamicVal] representing that +// we don't actually know the input variable values. +// +// CheckInputVariableValues checks whether the given object conforms to +// the input variables' type constraints and inserts default values where +// appropriate, but it doesn't check other details such as whether the +// values pass any author-defined custom validation rules. Those other details +// must be handled by the [InputVariable] objects representing each individual +// child stack input variable declaration, as part of preparing the individual +// attributes of the result for their appearance in downstream expressions. +func (c *StackCallInstance) CheckInputVariableValues(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) { + return doOnceWithDiags(ctx, c.tracingName()+" inputs", c.inputVariableValues.For(phase), + validateStackCallInstanceInputsFn(c.Stack(ctx, phase), c.call.config.config.Inputs, c.call.config.config.DeclRange.ToHCL().Ptr(), c, phase)) +} + +// ResolveExpressionReference implements ExpressionScope for the arguments +// inside an embedded stack call block, evaluated in the context of a +// particular instance of that call. +func (c *StackCallInstance) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return c.call.stack.resolveExpressionReference(ctx, ref, nil, c.repetition) +} + +// ExternalFunctions implements ExpressionScope. +func (c *StackCallInstance) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return c.main.ProviderFunctions(ctx, c.call.stack.config) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (c *StackCallInstance) PlanTimestamp() time.Time { + return c.main.PlanTimestamp() +} + +func (c *StackCallInstance) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + _, moreDiags := c.CheckInputVariableValues(ctx, phase) + diags = diags.Append(moreDiags) + + return diags +} + +// PlanChanges implements Plannable by performing plan-time validation of +// all of the per-instance arguments in the stack call configuration. +// +// This does not check the child stack instance implied by the call, so the +// plan walk driver must call [StackCallInstance.CalledStack] and explore +// it and all of its contents too. +func (c *StackCallInstance) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + // This is really just a "plan-time validation" behavior, since stack + // calls never contribute directly to the planned changes. + return nil, c.checkValid(ctx, PlanPhase) +} + +// CheckApply implements Applyable by confirming that the input variable +// values are still valid after resolving any upstream changes. +func (c *StackCallInstance) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) { + return nil, c.checkValid(ctx, ApplyPhase) +} + +// tracingName implements Plannable. +func (c *StackCallInstance) tracingName() string { + return fmt.Sprintf("%s call", c.CalledStackAddr()) +} + +func validateStackCallInstanceInputsFn(stack *Stack, expr hcl.Expression, rng *hcl.Range, scope ExpressionScope, phase EvalPhase) func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + return func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + wantTy, defs := stack.InputsType() + + v := cty.EmptyObjectVal + var hclCtx *hcl.EvalContext + if expr != nil { + result, moreDiags := EvalExprAndEvalContext(ctx, expr, phase, scope) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return cty.DynamicVal, diags + } + expr = result.Expression + hclCtx = result.EvalContext + v = result.Value + } + + v = defs.Apply(v) + v, err := convert.Convert(v, wantTy) + if err != nil { + // A conversion failure here could either be caused by an author-provided + // expression that's invalid or by the author omitting the argument + // altogether when there's at least one required attribute, so we'll + // return slightly different messages in each case. + if expr != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for embedded stack", + Detail: fmt.Sprintf("Invalid input variable definition object: %s.", tfdiags.FormatError(err)), + Subject: rng, + Expression: expr, + EvalContext: hclCtx, + }) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required inputs for embedded stack", + Detail: fmt.Sprintf("Must provide \"inputs\" argument to define the embedded stack's input variables: %s.", tfdiags.FormatError(err)), + Subject: rng, + }) + } + return cty.DynamicVal, diags + } + + if v.IsKnown() && !v.IsNull() { + var markDiags tfdiags.Diagnostics + for varAddr, variable := range stack.InputVariables() { + varVal := v.GetAttr(varAddr.Name) + varDecl := variable.config.config + + if !varDecl.Ephemeral { + // If the variable isn't declared as being ephemeral then we + // cannot allow ephemeral values to be assigned to it. + _, markses := varVal.UnmarkDeepWithPaths() + ephemeralPaths, _ := marks.PathsWithMark(markses, marks.Ephemeral) + for _, path := range ephemeralPaths { + if len(path) == 0 { + // The entire value is ephemeral, then. + markDiags = markDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: fmt.Sprintf("The input variable %q does not accept ephemeral values.", varAddr.Name), + Subject: rng, + Expression: expr, + EvalContext: hclCtx, + Extra: diagnosticCausedByEphemeral(true), + }) + } else { + // Something nested inside is ephemeral, so we'll be + // more specific. + markDiags = markDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: fmt.Sprintf( + "The input variable %q does not accept ephemeral values, so the value for %s is not compatible.", + varAddr.Name, tfdiags.FormatCtyPath(path), + ), + Subject: rng, + Expression: expr, + EvalContext: hclCtx, + Extra: diagnosticCausedByEphemeral(true), + }) + } + } + } + } + diags = diags.Append(markDiags) + if markDiags.HasErrors() { + // If we have an ephemeral value in a place where there shouldn't + // be one then we'll return an entirely-unknown value to make sure + // that downstreams that aren't checking the errors can't leak the + // value into somewhere it ought not to be. We'll still preserve + // the type constraint so that we can do type checking downstream. + return cty.UnknownVal(v.Type()), diags + } + } + + return v, diags + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call_test.go b/internal/stacks/stackruntime/internal/stackeval/stack_call_test.go new file mode 100644 index 0000000000..3a18a34869 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call_test.go @@ -0,0 +1,396 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "strings" + "testing" + + "github.com/davecgh/go-spew/spew" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestStackCallCheckInstances(t *testing.T) { + getStackCall := func(ctx context.Context, main *Main) *StackCall { + mainStack := main.MainStack() + call := mainStack.EmbeddedStackCall(stackaddrs.StackCall{Name: "child"}) + if call == nil { + t.Fatal("stack.child does not exist, but it should exist") + } + return call + } + + subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) { + cfg := testStackConfig(t, "stack_call", "single_instance") + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_inputs": cty.EmptyObjectVal, + }, + }) + + call := getStackCall(ctx, main) + forEachVal, diags := call.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if forEachVal != cty.NilVal { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: cty.NilVal", forEachVal) + } + + insts, unknown, diags := call.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 1; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + inst, ok := insts[addrs.NoKey] + if !ok { + t.Fatalf("missing expected addrs.NoKey instance\n%s", spew.Sdump(insts)) + } + if diff := cmp.Diff(instances.RepetitionData{}, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + t.Run("for_each", func(t *testing.T) { + cfg := testStackConfig(t, "stack_call", "for_each") + + subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.MapValEmpty(cty.EmptyObject), + }, + }) + + call := getStackCall(ctx, main) + forEachVal, diags := call.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if got, want := forEachVal, cty.MapValEmpty(cty.EmptyObject); !want.RawEquals(got) { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", got, want) + } + insts, unknown, diags := call.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 0; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + + // For this particular function we take the unusual approach of + // distinguishing between a nil map and a non-nil empty map so + // we can distinguish between "definitely no instances" (this case) + // and "we don't know how many instances there are" (tested in other + // subtests of this test, below.) + if insts == nil { + t.Error("CheckInstances result is nil; should be non-nil empty map") + } + }) + subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) { + wantForEachVal := cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in b"), + }), + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": wantForEachVal, + }, + }) + + call := getStackCall(ctx, main) + gotForEachVal, diags := call.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + if !wantForEachVal.RawEquals(gotForEachVal) { + t.Fatalf("unexpected for_each value\ngot: %#v\nwant: %#v", gotForEachVal, wantForEachVal) + } + insts, unknown, diags := call.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertFalse(t, unknown) + if got, want := len(insts), 2; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, insts) + } + t.Run("instance a", func(t *testing.T) { + inst, ok := insts[addrs.StringKey("a")] + if !ok { + t.Fatalf("missing expected addrs.StringKey(\"a\") instance\n%s", spew.Sdump(insts)) + } + wantRepData := instances.RepetitionData{ + EachKey: cty.StringVal("a"), + EachValue: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in a"), + }), + } + if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + t.Run("instance b", func(t *testing.T) { + inst, ok := insts[addrs.StringKey("b")] + if !ok { + t.Fatalf("missing expected addrs.StringKey(\"b\") instance\n%s", spew.Sdump(insts)) + } + wantRepData := instances.RepetitionData{ + EachKey: cty.StringVal("b"), + EachValue: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in b"), + }), + } + if diff := cmp.Diff(wantRepData, inst.RepetitionData(), ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong repetition data\n%s", diff) + } + }) + }) + subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.NullVal(cty.Map(cty.EmptyObject)), + }, + }) + + call := getStackCall(ctx, main) + gotVal, diags := call.CheckForEachValue(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return diag.Severity() == tfdiags.Error && strings.Contains(diag.Description().Detail, "The for_each expression produced a null value") + }) + wantVal := cty.DynamicVal // placeholder for invalid result + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + }) + subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.StringVal("nope"), + }, + }) + + call := getStackCall(ctx, main) + gotVal, diags := call.CheckForEachValue(ctx, InspectPhase) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return (diag.Severity() == tfdiags.Error && + diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this stack.") + }) + wantVal := cty.DynamicVal // placeholder for invalid result + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + + // When the for_each expression is invalid, CheckInstances should + // return nil to represent that we don't know enough to predict + // how many instances there are. This is a different result than + // when we know there are zero instances, which would be a non-nil + // empty map. + gotInsts, unknown, diags := call.CheckInstances(ctx, InspectPhase) + assertFalse(t, unknown) + assertMatchingDiag(t, diags, func(diag tfdiags.Diagnostic) bool { + return (diag.Severity() == tfdiags.Error && + diag.Description().Detail == "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this stack.") + }) + if gotInsts != nil { + t.Errorf("wrong instances; want nil\n%#v", gotInsts) + } + }) + subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.UnknownVal(cty.Map(cty.EmptyObject)), + }, + }) + + call := getStackCall(ctx, main) + gotVal, diags := call.CheckForEachValue(ctx, InspectPhase) + assertNoDiags(t, diags) + wantVal := cty.UnknownVal(cty.Map(cty.EmptyObject)) + if !wantVal.RawEquals(gotVal) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", gotVal, wantVal) + } + + gotInsts, unknown, diags := call.CheckInstances(ctx, InspectPhase) + assertNoDiags(t, diags) + assertTrue(t, unknown) + if got, want := len(gotInsts), 0; got != want { + t.Fatalf("wrong number of instances %d; want %d\n%#v", got, want, gotInsts) + } + }) + }) +} + +func TestStackCallResultValue(t *testing.T) { + getStackCall := func(ctx context.Context, main *Main) *StackCall { + mainStack := main.MainStack() + call := mainStack.EmbeddedStackCall(stackaddrs.StackCall{Name: "child"}) + if call == nil { + t.Fatal("stack.child does not exist, but it should exist") + } + return call + } + + subtestInPromisingTask(t, "single instance", func(ctx context.Context, t *testing.T) { + cfg := testStackConfig(t, "stack_call", "single_instance") + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_inputs": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("hello"), + "test_map": cty.MapValEmpty(cty.String), + }), + }, + }) + + call := getStackCall(ctx, main) + got := call.ResultValue(ctx, InspectPhase) + want := cty.ObjectVal(map[string]cty.Value{ + "test_map": cty.MapValEmpty(cty.String), + "test_string": cty.StringVal("hello"), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + t.Run("for_each", func(t *testing.T) { + cfg := testStackConfig(t, "stack_call", "for_each") + + subtestInPromisingTask(t, "no instances", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.MapValEmpty(cty.EmptyObject), + }, + }) + + call := getStackCall(ctx, main) + got := call.ResultValue(ctx, InspectPhase) + want := cty.MapValEmpty(cty.Map(cty.Object(map[string]cty.Type{ + "test_string": cty.String, + "test_map": cty.Map(cty.String), + }))) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + }) + subtestInPromisingTask(t, "two instances", func(ctx context.Context, t *testing.T) { + forEachVal := cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in b"), + }), + }) + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": forEachVal, + }, + }) + + call := getStackCall(ctx, main) + got := call.ResultValue(ctx, InspectPhase) + want := cty.MapVal(map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in a"), + "test_map": cty.NullVal(cty.Map(cty.String)), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("in b"), + "test_map": cty.NullVal(cty.Map(cty.String)), + }), + }) + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "null", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.NullVal(cty.Map(cty.EmptyObject)), + }, + }) + + call := getStackCall(ctx, main) + got := call.ResultValue(ctx, InspectPhase) + // When the for_each expression is invalid, the result value + // is unknown so we can use it as a placeholder for partial + // downstream checking. + want := cty.NilVal + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "string", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.StringVal("nope"), + }, + }) + + call := getStackCall(ctx, main) + got := call.ResultValue(ctx, InspectPhase) + // When the for_each expression is invalid, the result value + // is unknown so we can use it as a placeholder for partial + // downstream checking. + want := cty.NilVal + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + subtestInPromisingTask(t, "unknown", func(ctx context.Context, t *testing.T) { + main := testEvaluator(t, testEvaluatorOpts{ + Config: cfg, + TestOnlyGlobals: map[string]cty.Value{ + "child_stack_for_each": cty.UnknownVal(cty.Map(cty.EmptyObject)), + }, + }) + + call := getStackCall(ctx, main) + got := call.ResultValue(ctx, InspectPhase) + // When the for_each expression is unknown, the result value + // is a placeholder instance, with a wildcard key and potentially + // unknown attributes. + want := cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "test_map": cty.Map(cty.String), + "test_string": cty.String, + }))) + + // FIXME: the cmp transformer ctydebug.CmpOptions seems to find + // this particular pair of values troubling, causing it to get + // into an infinite recursion. For now we'll just use RawEquals, + // at the expense of a less helpful failure message. This seems + // to be a bug in upstream ctydebug. + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_config.go new file mode 100644 index 0000000000..7d07cdb468 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_config.go @@ -0,0 +1,616 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StackConfig represents a stack as represented in the configuration: either the +// root stack or one of the embedded stacks before it's been expanded into +// individual instances. +// +// After instance expansion we use [StackInstance] to represent each of the +// individual instances. +type StackConfig struct { + addr stackaddrs.Stack + + config *stackconfig.ConfigNode + parent *StackConfig + + main *Main + + // The remaining fields are where we memoize related objects that we've + // constructed and returned. Must lock "mu" before interacting with these. + mu sync.Mutex + children map[stackaddrs.StackStep]*StackConfig + inputVariables map[stackaddrs.InputVariable]*InputVariableConfig + localValues map[stackaddrs.LocalValue]*LocalValueConfig + outputValues map[stackaddrs.OutputValue]*OutputValueConfig + stackCalls map[stackaddrs.StackCall]*StackCallConfig + removedStackCalls collections.Map[stackaddrs.ConfigStackCall, []*RemovedStackCallConfig] + components map[stackaddrs.Component]*ComponentConfig + removedComponents collections.Map[stackaddrs.ConfigComponent, []*RemovedComponentConfig] + providers map[stackaddrs.ProviderConfig]*ProviderConfig +} + +var ( + _ ExpressionScope = (*StackConfig)(nil) +) + +func newStackConfig(main *Main, addr stackaddrs.Stack, parent *StackConfig, config *stackconfig.ConfigNode) *StackConfig { + return &StackConfig{ + addr: addr, + parent: parent, + config: config, + main: main, + + children: make(map[stackaddrs.StackStep]*StackConfig, len(config.Children)), + inputVariables: make(map[stackaddrs.InputVariable]*InputVariableConfig, len(config.Stack.Declarations.InputVariables)), + localValues: make(map[stackaddrs.LocalValue]*LocalValueConfig, len(config.Stack.Declarations.LocalValues)), + outputValues: make(map[stackaddrs.OutputValue]*OutputValueConfig, len(config.Stack.Declarations.OutputValues)), + stackCalls: make(map[stackaddrs.StackCall]*StackCallConfig, len(config.Stack.Declarations.EmbeddedStacks)), + removedStackCalls: collections.NewMap[stackaddrs.ConfigStackCall, []*RemovedStackCallConfig](), + components: make(map[stackaddrs.Component]*ComponentConfig, len(config.Stack.Declarations.Components)), + removedComponents: collections.NewMap[stackaddrs.ConfigComponent, []*RemovedComponentConfig](), + providers: make(map[stackaddrs.ProviderConfig]*ProviderConfig, len(config.Stack.Declarations.ProviderConfigs)), + } +} + +// ChildConfig returns a [StackConfig] representing the embedded stack matching +// the given address step, or nil if there is no such stack. +func (s *StackConfig) ChildConfig(step stackaddrs.StackStep) *StackConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.children[step] + if !ok { + childNode, ok := s.config.Children[step.Name] + if !ok { + return nil + } + childAddr := s.addr.Child(step.Name) + s.children[step] = newStackConfig(s.main, childAddr, s, childNode) + ret = s.children[step] + } + return ret +} + +func (s *StackConfig) ChildConfigs() map[stackaddrs.StackStep]*StackConfig { + if len(s.config.Children) == 0 { + return nil + } + ret := make(map[stackaddrs.StackStep]*StackConfig, len(s.config.Children)) + for n := range s.config.Children { + stepAddr := stackaddrs.StackStep{Name: n} + ret[stepAddr] = s.ChildConfig(stepAddr) + } + return ret +} + +// InputVariables returns a map of the objects representing all of the +// input variables declared inside this stack configuration. +func (s *StackConfig) InputVariables() map[stackaddrs.InputVariable]*InputVariableConfig { + if len(s.config.Stack.InputVariables) == 0 { + return nil + } + ret := make(map[stackaddrs.InputVariable]*InputVariableConfig, len(s.config.Stack.InputVariables)) + for name := range s.config.Stack.InputVariables { + addr := stackaddrs.InputVariable{Name: name} + ret[addr] = s.InputVariable(addr) + } + return ret +} + +// InputVariable returns an [InputVariableConfig] representing the input +// variable declared within this stack config that matches the given +// address, or nil if there is no such declaration. +func (s *StackConfig) InputVariable(addr stackaddrs.InputVariable) *InputVariableConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.inputVariables[addr] + if !ok { + cfg, ok := s.config.Stack.InputVariables[addr.Name] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newInputVariableConfig(s.main, cfgAddr, s, cfg) + s.inputVariables[addr] = ret + } + return ret +} + +// LocalValues returns a map of the objects representing all of the +// local values declared inside this stack configuration. +func (s *StackConfig) LocalValues() map[stackaddrs.LocalValue]*LocalValueConfig { + if len(s.config.Stack.LocalValues) == 0 { + return nil + } + ret := make(map[stackaddrs.LocalValue]*LocalValueConfig, len(s.config.Stack.LocalValues)) + for name := range s.config.Stack.LocalValues { + addr := stackaddrs.LocalValue{Name: name} + ret[addr] = s.LocalValue(addr) + } + return ret +} + +// LocalValue returns an [LocalValueConfig] representing the input +// variable declared within this stack config that matches the given +// address, or nil if there is no such declaration. +func (s *StackConfig) LocalValue(addr stackaddrs.LocalValue) *LocalValueConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.localValues[addr] + if !ok { + cfg, ok := s.config.Stack.LocalValues[addr.Name] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newLocalValueConfig(s.main, cfgAddr, s, cfg) + s.localValues[addr] = ret + } + return ret +} + +// OutputValue returns an [OutputValueConfig] representing the output +// value declared within this stack config that matches the given +// address, or nil if there is no such declaration. +func (s *StackConfig) OutputValue(addr stackaddrs.OutputValue) *OutputValueConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.outputValues[addr] + if !ok { + cfg, ok := s.config.Stack.OutputValues[addr.Name] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newOutputValueConfig(s.main, cfgAddr, s, cfg) + s.outputValues[addr] = ret + } + return ret +} + +// OutputValues returns a map of the objects representing all of the +// output values declared inside this stack configuration. +func (s *StackConfig) OutputValues() map[stackaddrs.OutputValue]*OutputValueConfig { + if len(s.config.Stack.OutputValues) == 0 { + return nil + } + ret := make(map[stackaddrs.OutputValue]*OutputValueConfig, len(s.config.Stack.OutputValues)) + for name := range s.config.Stack.OutputValues { + addr := stackaddrs.OutputValue{Name: name} + ret[addr] = s.OutputValue(addr) + } + return ret +} + +// ResultType returns the type of the result object that will be produced +// by this stack configuration, based on the output values declared within +// it. +func (s *StackConfig) ResultType() cty.Type { + os := s.OutputValues() + atys := make(map[string]cty.Type, len(os)) + for addr, o := range os { + atys[addr.Name] = o.ValueTypeConstraint() + } + return cty.Object(atys) +} + +// Providers returns a map of the objects representing all of the provider +// configurations declared inside this stack configuration. +func (s *StackConfig) Providers() map[stackaddrs.ProviderConfig]*ProviderConfig { + if len(s.config.Stack.ProviderConfigs) == 0 { + return nil + } + ret := make(map[stackaddrs.ProviderConfig]*ProviderConfig, len(s.config.Stack.ProviderConfigs)) + for configAddr := range s.config.Stack.ProviderConfigs { + provider, ok := s.config.Stack.RequiredProviders.ProviderForLocalName(configAddr.LocalName) + if !ok { + // Then we are missing a provider declaration, this will be caught + // elsewhere so we'll just skip it here. + continue + } + + addr := stackaddrs.ProviderConfig{ + Provider: provider, + Name: configAddr.Alias, + } + ret[addr] = s.Provider(addr) + } + return ret +} + +// Provider returns a [ProviderConfig] representing the provider configuration +// block within the stack configuration that matches the given address, +// or nil if there is no such declaration. +func (s *StackConfig) Provider(addr stackaddrs.ProviderConfig) *ProviderConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.providers[addr] + if !ok { + localName, ok := s.config.Stack.RequiredProviders.LocalNameForProvider(addr.Provider) + if !ok { + return nil + } + // FIXME: stackconfig package currently uses addrs.LocalProviderConfig + // instead of stackaddrs.ProviderConfigRef. + configAddr := addrs.LocalProviderConfig{ + LocalName: localName, + Alias: addr.Name, + } + cfg, ok := s.config.Stack.ProviderConfigs[configAddr] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newProviderConfig(s.main, cfgAddr, s, cfg) + s.providers[addr] = ret + } + return ret +} + +// ProviderByLocalAddr returns a [ProviderConfig] representing the provider +// configuration block within the stack configuration that matches the given +// local address, or nil if there is no such declaration. +// +// This is equivalent to calling [Provider] just using a reference address +// instead of a config address. +func (s *StackConfig) ProviderByLocalAddr(localAddr stackaddrs.ProviderConfigRef) *ProviderConfig { + s.mu.Lock() + defer s.mu.Unlock() + + provider, ok := s.config.Stack.RequiredProviders.ProviderForLocalName(localAddr.ProviderLocalName) + if !ok { + return nil + } + + addr := stackaddrs.ProviderConfig{ + Provider: provider, + Name: localAddr.Name, + } + ret, ok := s.providers[addr] + if !ok { + configAddr := addrs.LocalProviderConfig{ + LocalName: localAddr.ProviderLocalName, + Alias: localAddr.Name, + } + cfg, ok := s.config.Stack.ProviderConfigs[configAddr] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newProviderConfig(s.main, cfgAddr, s, cfg) + s.providers[addr] = ret + } + return ret +} + +// ProviderLocalName returns the local name used for the given provider +// in this particular stack configuration, based on the declarations in +// the required_providers configuration block. +// +// If the second return value is false then there is no local name declared +// for the given provider, and so the first return value is invalid. +func (s *StackConfig) ProviderLocalName(addr addrs.Provider) (string, bool) { + return s.config.Stack.RequiredProviders.LocalNameForProvider(addr) +} + +// ProviderForLocalName returns the provider for the given local name in this +// particular stack configuration, based on the declarations in the +// required_providers configuration block. +// +// If the second return value is false then there is no provider declared +// for the given local name, and so the first return value is invalid. +func (s *StackConfig) ProviderForLocalName(localName string) (addrs.Provider, bool) { + return s.config.Stack.RequiredProviders.ProviderForLocalName(localName) +} + +// StackCall returns a [StackCallConfig] representing the "stack" block +// matching the given address declared within this stack config, or nil if +// there is no such declaration. +func (s *StackConfig) StackCall(addr stackaddrs.StackCall) *StackCallConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.stackCalls[addr] + if !ok { + cfg, ok := s.config.Stack.EmbeddedStacks[addr.Name] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newStackCallConfig(s.main, cfgAddr, s, cfg) + s.stackCalls[addr] = ret + } + return ret +} + +// StackCalls returns a map of objects representing all of the embedded stack +// calls inside this stack configuration. +func (s *StackConfig) StackCalls() map[stackaddrs.StackCall]*StackCallConfig { + if len(s.config.Children) == 0 { + return nil + } + ret := make(map[stackaddrs.StackCall]*StackCallConfig, len(s.config.Children)) + for n := range s.config.Stack.EmbeddedStacks { + stepAddr := stackaddrs.StackCall{Name: n} + ret[stepAddr] = s.StackCall(stepAddr) + } + return ret +} + +func (s *StackConfig) RemovedStackCall(addr stackaddrs.ConfigStackCall) []*RemovedStackCallConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.removedStackCalls.GetOk(addr) + if !ok { + for _, cfg := range s.config.Stack.RemovedEmbeddedStacks.Get(addr) { + removed := newRemovedStackCallConfig(s.main, addr, s, cfg) + ret = append(ret, removed) + } + s.removedStackCalls.Put(addr, ret) + } + return ret +} + +func (s *StackConfig) RemovedStackCalls() collections.Map[stackaddrs.ConfigStackCall, []*RemovedStackCallConfig] { + ret := collections.NewMap[stackaddrs.ConfigStackCall, []*RemovedStackCallConfig]() + for addr := range s.config.Stack.RemovedEmbeddedStacks.All() { + ret.Put(addr, s.RemovedStackCall(addr)) + } + return ret +} + +// Component returns a [ComponentConfig] representing the component call +// declared within this stack config that matches the given address, or nil if +// there is no such declaration. +func (s *StackConfig) Component(addr stackaddrs.Component) *ComponentConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.components[addr] + if !ok { + cfg, ok := s.config.Stack.Components[addr.Name] + if !ok { + return nil + } + cfgAddr := stackaddrs.Config(s.addr, addr) + ret = newComponentConfig(s.main, cfgAddr, s, cfg) + s.components[addr] = ret + } + return ret +} + +// Components returns a map of the objects representing all of the +// component calls declared inside this stack configuration. +func (s *StackConfig) Components() map[stackaddrs.Component]*ComponentConfig { + if len(s.config.Stack.Components) == 0 { + return nil + } + ret := make(map[stackaddrs.Component]*ComponentConfig, len(s.config.Stack.Components)) + for name := range s.config.Stack.Components { + addr := stackaddrs.Component{Name: name} + ret[addr] = s.Component(addr) + } + return ret +} + +// RemovedComponent returns a [RemovedComponentConfig] representing the +// component call declared within this stack config that matches the given +// address, or nil if there is no such declaration. +func (s *StackConfig) RemovedComponent(addr stackaddrs.ConfigComponent) []*RemovedComponentConfig { + s.mu.Lock() + defer s.mu.Unlock() + + ret, ok := s.removedComponents.GetOk(addr) + if !ok { + for _, cfg := range s.config.Stack.RemovedComponents.Get(addr) { + cfgAddr := stackaddrs.ConfigComponent{ + Stack: append(s.addr, addr.Stack...), + Item: addr.Item, + } + removed := newRemovedComponentConfig(s.main, cfgAddr, s, cfg) + ret = append(ret, removed) + } + s.removedComponents.Put(addr, ret) + } + return ret +} + +// RemovedComponents returns a map of the objects representing all of the +// removed calls declared inside this stack configuration. +func (s *StackConfig) RemovedComponents() collections.Map[stackaddrs.ConfigComponent, []*RemovedComponentConfig] { + ret := collections.NewMap[stackaddrs.ConfigComponent, []*RemovedComponentConfig]() + for addr := range s.config.Stack.RemovedComponents.All() { + ret.Put(addr, s.RemovedComponent(addr)) + } + return ret +} + +// ResolveExpressionReference implements ExpressionScope, providing the +// global scope for evaluation within an unexpanded stack during the validate +// phase. +func (s *StackConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) { + return s.resolveExpressionReference(ctx, ref, nil, instances.RepetitionData{}) +} + +// resolveExpressionReference is the shared implementation of various +// validation-time ResolveExpressionReference methods, factoring out all +// of the common parts into one place. +func (s *StackConfig) resolveExpressionReference( + ctx context.Context, + ref stackaddrs.Reference, + selfAddr stackaddrs.Referenceable, + repetition instances.RepetitionData, +) (Referenceable, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // "Test-only globals" is a special affordance we have only when running + // unit tests in this package. The function called in this branch will + // return an error itself if we're not running in a suitable test situation. + if addr, ok := ref.Target.(stackaddrs.TestOnlyGlobal); ok { + return s.main.resolveTestOnlyGlobalReference(addr, ref.SourceRange) + } + + // TODO: Most of the below would benefit from "Did you mean..." suggestions + // when something is missing but there's a similarly-named object nearby. + + switch addr := ref.Target.(type) { + case stackaddrs.InputVariable: + ret := s.InputVariable(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared input variable", + Detail: fmt.Sprintf("There is no variable %q block declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.LocalValue: + ret := s.LocalValue(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared local value", + Detail: fmt.Sprintf("There is no local %q declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.Component: + ret := s.Component(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared component", + Detail: fmt.Sprintf("There is no component %q block declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.StackCall: + ret := s.StackCall(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared embedded stack", + Detail: fmt.Sprintf("There is no stack %q block declared in this stack.", addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.ProviderConfigRef: + ret := s.ProviderByLocalAddr(addr) + if ret == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared provider configuration", + Detail: fmt.Sprintf("There is no provider %q %q block declared in this stack.", addr.ProviderLocalName, addr.Name), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return ret, diags + case stackaddrs.ContextualRef: + switch addr { + case stackaddrs.EachKey: + if repetition.EachKey == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'each' reference", + Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.EachKey}, diags + case stackaddrs.EachValue: + if repetition.EachValue == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'each' reference", + Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.EachValue}, diags + case stackaddrs.CountIndex: + if repetition.CountIndex == cty.NilVal { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'count' reference", + Detail: "The special symbol 'count' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'count' argument.", + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + return JustValue{repetition.CountIndex}, diags + case stackaddrs.Self: + if selfAddr != nil { + // We'll just pretend the reference was to whatever "self" + // is referring to, then. + ref.Target = selfAddr + return s.resolveExpressionReference(ctx, ref, nil, repetition) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid 'self' reference", + Detail: "The special symbol 'self' is not defined in this location.", + Context: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } + default: + // The above should be exhaustive for all defined values of this type. + panic(fmt.Sprintf("unsupported ContextualRef %#v", addr)) + } + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid reference", + Detail: fmt.Sprintf("The object %s is not in scope at this location.", addr.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + }) + return nil, diags + } +} + +// ExternalFunctions implements ExpressionScope. +func (s *StackConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { + return s.main.ProviderFunctions(ctx, s) +} + +// PlanTimestamp implements ExpressionScope, providing the timestamp at which +// the current plan is being run. +func (s *StackConfig) PlanTimestamp() time.Time { + return s.main.PlanTimestamp() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go b/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go new file mode 100644 index 0000000000..cd6a66197b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stubs/errored.go @@ -0,0 +1,239 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stubs + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// erroredProvider is a stub provider that is used in place of a provider that +// failed the configuration step. Within the context of Stacks, an errored +// provider would have been configured by Stacks, and therefore should not be +// configured again, or used for any offline functionality. +type erroredProvider struct{} + +var _ providers.Interface = &erroredProvider{} + +func ErroredProvider() providers.Interface { + return &erroredProvider{} +} + +// ApplyResourceChange implements providers.Interface. +func (p *erroredProvider) ApplyResourceChange(providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot apply changes because this resource's associated provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ApplyResourceChangeResponse{ + Diagnostics: diags, + } +} + +func (p *erroredProvider) CallFunction(providers.CallFunctionRequest) providers.CallFunctionResponse { + return providers.CallFunctionResponse{ + Err: fmt.Errorf("CallFunction shouldn't be called on an errored provider; this is a bug in Terraform - please report this error"), + } +} + +// Close implements providers.Interface. +func (p *erroredProvider) Close() error { + return nil +} + +// ConfigureProvider implements providers.Interface. +func (p *erroredProvider) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + // This provider is used only in situations where ConfigureProvider on + // a real provider fails and the recipient was expecting a configured + // provider, so it doesn't make sense to configure it. + panic("can't configure the stub provider") +} + +// GetProviderSchema implements providers.Interface. +func (p *erroredProvider) GetProviderSchema() providers.GetProviderSchemaResponse { + return providers.GetProviderSchemaResponse{} +} + +func (p *erroredProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return providers.GetResourceIdentitySchemasResponse{} +} + +// ImportResourceState implements providers.Interface. +func (p *erroredProvider) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot import an existing object into this resource because its associated provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ImportResourceStateResponse{ + Diagnostics: diags, + } +} + +// MoveResourceState implements providers.Interface. +func (p *erroredProvider) MoveResourceState(providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called MoveResourceState on an errored provider", + "Terraform called MoveResourceState on an errored provider. This is a bug in Terraform - please report this error.", + nil, // nil attribute path means the overall configuration block + )) + return providers.MoveResourceStateResponse{ + Diagnostics: diags, + } +} + +// PlanResourceChange implements providers.Interface. +func (p *erroredProvider) PlanResourceChange(providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot plan changes for this resource because its associated provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.PlanResourceChangeResponse{ + Diagnostics: diags, + } +} + +// ReadDataSource implements providers.Interface. +func (p *erroredProvider) ReadDataSource(providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot read from this data source because its associated provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ReadDataSourceResponse{ + Diagnostics: diags, + } +} + +// ReadResource implements providers.Interface. +func (p *erroredProvider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse { + // For this one we'll just optimistically assume that the remote object + // hasn't changed. In many cases we'll fail calling PlanResourceChange + // right afterwards anyway, and even if not we'll get another opportunity + // to refresh on a future run once the provider configuration is fixed. + return providers.ReadResourceResponse{ + NewState: req.PriorState, + Private: req.Private, + } +} + +// OpenEphemeralResource implements providers.Interface. +func (p *erroredProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot open this ephemeral resource instance because its associated provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.OpenEphemeralResourceResponse{ + Diagnostics: diags, + } +} + +// RenewEphemeralResource implements providers.Interface. +func (p *erroredProvider) RenewEphemeralResource(providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse { + // We don't have anything to do here because OpenEphemeralResource didn't really + // actually "open" anything. + return providers.RenewEphemeralResourceResponse{} +} + +// CloseEphemeralResource implements providers.Interface. +func (p *erroredProvider) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse { + // We don't have anything to do here because OpenEphemeralResource didn't really + // actually "open" anything. + return providers.CloseEphemeralResourceResponse{} +} + +// Stop implements providers.Interface. +func (p *erroredProvider) Stop() error { + // This stub provider never actually does any real work, so there's nothing + // for us to stop. + return nil +} + +// UpgradeResourceState implements providers.Interface. +func (p *erroredProvider) UpgradeResourceState(providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + // Ideally we'd just skip this altogether and echo back what the caller + // provided, but the request is in a different serialization format than + // the response and so only the real provider can deal with this one. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot decode the prior state for this resource instance because its provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.UpgradeResourceStateResponse{ + Diagnostics: diags, + } +} + +func (p *erroredProvider) UpgradeResourceIdentity(providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + // Ideally we'd just skip this altogether and echo back what the caller + // provided, but the request is in a different serialization format than + // the response and so only the real provider can deal with this one. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is invalid", + "Cannot decode the prior state for this resource instance because its provider configuration is invalid.", + nil, // nil attribute path means the overall configuration block + )) + return providers.UpgradeResourceIdentityResponse{ + Diagnostics: diags, + } +} + +// ValidateDataResourceConfig implements providers.Interface. +func (p *erroredProvider) ValidateDataResourceConfig(providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { + // We'll just optimistically assume the configuration is valid, so that + // we can progress to planning and return an error there instead. + return providers.ValidateDataResourceConfigResponse{ + Diagnostics: nil, + } +} + +// ValidateProviderConfig implements providers.Interface. +func (p *erroredProvider) ValidateProviderConfig(req providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + // It doesn't make sense to call this one on stubProvider, because + // we only use stubProvider for situations where ConfigureProvider failed + // on a real provider and we should already have called + // ValidateProviderConfig on that provider by then anyway. + return providers.ValidateProviderConfigResponse{ + PreparedConfig: req.Config, + Diagnostics: nil, + } +} + +// ValidateEphemeralResourceConfig implements providers.Interface. +func (p *erroredProvider) ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse { + + return providers.ValidateEphemeralResourceConfigResponse{ + Diagnostics: nil, + } +} + +// ValidateResourceConfig implements providers.Interface. +func (p *erroredProvider) ValidateResourceConfig(providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { + // We'll just optimistically assume the configuration is valid, so that + // we can progress to reading and return an error there instead. + return providers.ValidateResourceConfigResponse{ + Diagnostics: nil, + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go b/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go new file mode 100644 index 0000000000..a184b6d8d3 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stubs/offline.go @@ -0,0 +1,241 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stubs + +import ( + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// offlineProvider is a stub provider that is used in place of a provider that +// is not configured and should never be configured by the current Terraform +// configuration. +// +// The only functionality that should be called on an offlineProvider are +// provider function calls and move resource state. +// +// For everything else, Stacks should have provided a pre-configured provider +// that should be used instead. +type offlineProvider struct { + unconfiguredClient providers.Interface +} + +func OfflineProvider(unconfiguredClient providers.Interface) providers.Interface { + return &offlineProvider{ + unconfiguredClient: unconfiguredClient, + } +} + +func (o *offlineProvider) GetProviderSchema() providers.GetProviderSchemaResponse { + // We do actually use the schema to work out which functions are available + // and whether cross-resource moves are even supported. + return o.unconfiguredClient.GetProviderSchema() +} + +func (o *offlineProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return o.unconfiguredClient.GetResourceIdentitySchemas() +} + +func (o *offlineProvider) ValidateProviderConfig(_ providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ValidateProviderConfig on an unconfigured provider", + "Cannot validate provider configuration because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ValidateProviderConfigResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) ValidateResourceConfig(_ providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ValidateResourceConfig on an unconfigured provider", + "Cannot validate resource configuration because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ValidateResourceConfigResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) ValidateDataResourceConfig(_ providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ValidateDataResourceConfig on an unconfigured provider", + "Cannot validate data source configuration because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ValidateDataResourceConfigResponse{ + Diagnostics: diags, + } +} + +// ValidateEphemeralResourceConfig implements providers.Interface. +func (p *offlineProvider) ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ValidateEphemeralResourceConfig on an unconfigured provider", + "Cannot validate this resource config because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ValidateEphemeralResourceConfigResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) UpgradeResourceState(_ providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called UpgradeResourceState on an unconfigured provider", + "Cannot upgrade the state of this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.UpgradeResourceStateResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) UpgradeResourceIdentity(_ providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called UpgradeResourceIdentity on an unconfigured provider", + "Cannot upgrade the state of this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.UpgradeResourceIdentityResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) ConfigureProvider(_ providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ConfigureProvider on an unconfigured provider", + "Cannot configure this provider because it is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ConfigureProviderResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) Stop() error { + // pass the stop call to the underlying unconfigured client + return o.unconfiguredClient.Stop() +} + +func (o *offlineProvider) ReadResource(_ providers.ReadResourceRequest) providers.ReadResourceResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ReadResource on an unconfigured provider", + "Cannot read from this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ReadResourceResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) PlanResourceChange(_ providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called PlanResourceChange on an unconfigured provider", + "Cannot plan changes to this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.PlanResourceChangeResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) ApplyResourceChange(_ providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ApplyResourceChange on an unconfigured provider", + "Cannot apply changes to this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ApplyResourceChangeResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) ImportResourceState(_ providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ImportResourceState on an unconfigured provider", + "Cannot import an existing object into this resource because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ImportResourceStateResponse{ + Diagnostics: diags, + } +} + +func (o *offlineProvider) MoveResourceState(request providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + return o.unconfiguredClient.MoveResourceState(request) +} + +func (o *offlineProvider) ReadDataSource(_ providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called ReadDataSource on an unconfigured provider", + "Cannot read from this data source because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.ReadDataSourceResponse{ + Diagnostics: diags, + } +} + +// OpenEphemeralResource implements providers.Interface. +func (u *offlineProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called OpenEphemeralResource on an unconfigured provider", + "Cannot open this resource instance because this provider is not configured. This is a bug in Terraform - please report it.", + nil, // nil attribute path means the overall configuration block + )) + return providers.OpenEphemeralResourceResponse{ + Diagnostics: diags, + } +} + +// RenewEphemeralResource implements providers.Interface. +func (u *offlineProvider) RenewEphemeralResource(providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse { + // We don't have anything to do here because OpenEphemeralResource didn't really + // actually "open" anything. + return providers.RenewEphemeralResourceResponse{} +} + +// CloseEphemeralResource implements providers.Interface. +func (u *offlineProvider) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse { + // We don't have anything to do here because OpenEphemeralResource didn't really + // actually "open" anything. + return providers.CloseEphemeralResourceResponse{} +} + +func (o *offlineProvider) CallFunction(request providers.CallFunctionRequest) providers.CallFunctionResponse { + return o.unconfiguredClient.CallFunction(request) +} + +func (o *offlineProvider) Close() error { + // pass the close call to the underlying unconfigured client + return o.unconfiguredClient.Close() +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go b/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go new file mode 100644 index 0000000000..c604e6c65e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stubs/unknown.go @@ -0,0 +1,297 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stubs + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/lang/ephemeral" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var _ providers.Interface = (*unknownProvider)(nil) + +// unknownProvider is a stub provider that represents a provider that is +// unknown to the current Terraform configuration. This is used when a reference +// to a provider is unknown, or the provider itself has unknown instances. +// +// An unknownProvider is only returned in the context of a provider that should +// have been configured by Stacks. This provider should not be configured again, +// or used for any dedicated offline functionality (such as moving resources and +// provider functions). +type unknownProvider struct { + unconfiguredClient providers.Interface +} + +func UnknownProvider(unconfiguredClient providers.Interface) providers.Interface { + return &unknownProvider{ + unconfiguredClient: unconfiguredClient, + } +} + +func (u *unknownProvider) GetProviderSchema() providers.GetProviderSchemaResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.GetProviderSchema() +} + +func (u *unknownProvider) GetResourceIdentitySchemas() providers.GetResourceIdentitySchemasResponse { + return u.unconfiguredClient.GetResourceIdentitySchemas() +} + +func (u *unknownProvider) ValidateProviderConfig(request providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.ValidateProviderConfig(request) +} + +func (u *unknownProvider) ValidateResourceConfig(request providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.ValidateResourceConfig(request) +} + +func (u *unknownProvider) ValidateDataResourceConfig(request providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.ValidateDataResourceConfig(request) +} + +// ValidateEphemeralResourceConfig implements providers.Interface. +func (p *unknownProvider) ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest) providers.ValidateEphemeralResourceConfigResponse { + return providers.ValidateEphemeralResourceConfigResponse{ + Diagnostics: nil, + } +} + +func (u *unknownProvider) UpgradeResourceState(request providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.UpgradeResourceState(request) +} + +func (u *unknownProvider) UpgradeResourceIdentity(request providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + // This is offline functionality, so we can hand it off to the unconfigured + // client. + return u.unconfiguredClient.UpgradeResourceIdentity(request) +} + +func (u *unknownProvider) ConfigureProvider(_ providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + // This shouldn't be called, we don't configure an unknown provider within + // stacks and Terraform Core shouldn't call this method. + panic("attempted to configure an unknown provider") +} + +func (u *unknownProvider) Stop() error { + // the underlying unconfiguredClient is managed elsewhere. + return nil +} + +func (u *unknownProvider) ReadResource(request providers.ReadResourceRequest) providers.ReadResourceResponse { + if request.ClientCapabilities.DeferralAllowed { + // For ReadResource, we'll just return the existing state and defer + // the operation. + return providers.ReadResourceResponse{ + NewState: request.PriorState, + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + return providers.ReadResourceResponse{ + Diagnostics: []tfdiags.Diagnostic{ + tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is unknown", + "Cannot read from this data source because its associated provider configuration is unknown.", + nil, // nil attribute path means the overall configuration block + ), + }, + } +} + +func (u *unknownProvider) PlanResourceChange(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + if request.ClientCapabilities.DeferralAllowed { + // For PlanResourceChange, we'll kind of abuse the mocking library to + // populate the computed values with unknown values so that future + // operations can still be used. + // + // PlanComputedValuesForResource populates the computed values with + // unknown values. This isn't the original use case for the mocking + // library, but it is doing exactly what we need it to do. + + schema := u.GetProviderSchema().ResourceTypes[request.TypeName] + val, diags := mocking.PlanComputedValuesForResource(request.ProposedNewState, nil, schema.Body) + if diags.HasErrors() { + // All the potential errors we get back from this function are + // related to the user badly defining mocks. We should never hit + // this as we are just using the default behaviour. + panic(diags.Err()) + } + + return providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(val, schema.Body), + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + return providers.PlanResourceChangeResponse{ + Diagnostics: []tfdiags.Diagnostic{ + tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is unknown", + "Cannot plan changes for this resource because its associated provider configuration is unknown.", + nil, // nil attribute path means the overall configuration block + ), + }, + } +} + +func (u *unknownProvider) ApplyResourceChange(_ providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + Diagnostics: []tfdiags.Diagnostic{ + tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is unknown", + "Cannot apply changes for this resource because its associated provider configuration is unknown.", + nil, // nil attribute path means the overall configuration block + ), + }, + } +} + +func (u *unknownProvider) ImportResourceState(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + if request.ClientCapabilities.DeferralAllowed { + // For ImportResourceState, we don't have any config to work with and + // we don't know enough to work out which value the ID corresponds to. + // + // We'll just return an unknown value that corresponds to the correct + // type. Terraform should know how to handle this when it arrives + // alongside the deferred metadata. + + schema := u.GetProviderSchema().ResourceTypes[request.TypeName] + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: request.TypeName, + State: cty.UnknownVal(schema.Body.ImpliedType()), + }, + }, + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + return providers.ImportResourceStateResponse{ + Diagnostics: []tfdiags.Diagnostic{ + tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is unknown", + "Cannot import an existing object into this resource because its associated provider configuration is unknown.", + nil, // nil attribute path means the overall configuration block + ), + }, + } +} + +func (u *unknownProvider) MoveResourceState(_ providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Called MoveResourceState on an unknown provider", + "Terraform called MoveResourceState on an unknown provider. This is a bug in Terraform - please report this error.", + nil, // nil attribute path means the overall configuration block + )) + return providers.MoveResourceStateResponse{ + Diagnostics: diags, + } +} + +func (u *unknownProvider) ReadDataSource(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + if request.ClientCapabilities.DeferralAllowed { + // For ReadDataSource, we'll kind of abuse the mocking library to + // populate the computed values with unknown values so that future + // operations can still be used. + // + // PlanComputedValuesForResource populates the computed values with + // unknown values. This isn't the original use case for the mocking + // library, but it is doing exactly what we need it to do. + + schema := u.GetProviderSchema().DataSources[request.TypeName] + val, diags := mocking.PlanComputedValuesForResource(request.Config, nil, schema.Body) + if diags.HasErrors() { + // All the potential errors we get back from this function are + // related to the user badly defining mocks. We should never hit + // this as we are just using the default behaviour. + panic(diags.Err()) + } + + return providers.ReadDataSourceResponse{ + State: ephemeral.StripWriteOnlyAttributes(val, schema.Body), + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + return providers.ReadDataSourceResponse{ + Diagnostics: []tfdiags.Diagnostic{ + tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is unknown", + "Cannot read from this data source because its associated provider configuration is unknown.", + nil, // nil attribute path means the overall configuration block + ), + }, + } +} + +// OpenEphemeralResource implements providers.Interface. +func (u *unknownProvider) OpenEphemeralResource(providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + // TODO: Once there's a definition for how deferred actions ought to work + // for ephemeral resource instances, make this report that this one needs + // to be deferred if the client announced that it supports deferral. + // + // For now this is just always an error, because ephemeral resources are + // just a prototype being developed concurrently with deferred actions. + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider configuration is unknown", + "Cannot open this resource instance because its associated provider configuration is unknown.", + nil, // nil attribute path means the overall configuration block + )) + return providers.OpenEphemeralResourceResponse{ + Diagnostics: diags, + } +} + +// RenewEphemeralResource implements providers.Interface. +func (u *unknownProvider) RenewEphemeralResource(providers.RenewEphemeralResourceRequest) providers.RenewEphemeralResourceResponse { + // We don't have anything to do here because OpenEphemeralResource didn't really + // actually "open" anything. + return providers.RenewEphemeralResourceResponse{} +} + +// CloseEphemeralResource implements providers.Interface. +func (u *unknownProvider) CloseEphemeralResource(providers.CloseEphemeralResourceRequest) providers.CloseEphemeralResourceResponse { + // We don't have anything to do here because OpenEphemeralResource didn't really + // actually "open" anything. + return providers.CloseEphemeralResourceResponse{} +} + +func (u *unknownProvider) CallFunction(_ providers.CallFunctionRequest) providers.CallFunctionResponse { + return providers.CallFunctionResponse{ + Err: fmt.Errorf("CallFunction shouldn't be called on an unknown provider; this is a bug in Terraform - please report this error"), + } +} + +func (u *unknownProvider) Close() error { + // the underlying unconfiguredClient is managed elsewhere. + return nil +} diff --git a/internal/stacks/stackruntime/internal/stackeval/telemetry.go b/internal/stacks/stackruntime/internal/stackeval/telemetry.go new file mode 100644 index 0000000000..bf057e4d83 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/telemetry.go @@ -0,0 +1,21 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval") +} + +// tracingNamer is implemented by types that can return a suitable name for +// themselves to use in the names or attributes of tracing spans. +type tracingNamer interface { + tracingName() string +} diff --git a/internal/stacks/stackruntime/internal/stackeval/terraform_hook.go b/internal/stacks/stackruntime/internal/stackeval/terraform_hook.go new file mode 100644 index 0000000000..fc219a9faa --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/terraform_hook.go @@ -0,0 +1,205 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/zclconf/go-cty/cty" +) + +// componentInstanceTerraformHook implements terraform.Hook for plan and apply +// operations on a specified component instance. It connects the standard +// terraform.Hook callbacks to the given stackruntime.Hooks callbacks. +// +// We unfortunately must embed a context.Context in this type, as the existing +// Terraform core hook interface does not support threading a context through. +// The lifetime of this hook instance is strictly smaller than its surrounding +// context, but we should migrate away from this for clarity when possible. +type componentInstanceTerraformHook struct { + terraform.NilHook + + ctx context.Context + seq *hookSeq + hooks *Hooks + addr stackaddrs.AbsComponentInstance + + mu sync.Mutex + + // We record the current action for a resource instance during the + // pre-apply hook, so that we can refer to it in the post-apply hook, and + // report on the apply action to our caller. + resourceInstanceObjectApplyAction addrs.Map[addrs.AbsResourceInstanceObject, plans.Action] + + // Only successfully applied resource instances should be included in the + // change counts for the apply operation, so we record whether or not apply + // failed here. + resourceInstanceObjectApplySuccess addrs.Set[addrs.AbsResourceInstanceObject] +} + +var _ terraform.Hook = (*componentInstanceTerraformHook)(nil) + +func (h *componentInstanceTerraformHook) resourceInstanceObjectAddr(riAddr addrs.AbsResourceInstance, dk addrs.DeposedKey) stackaddrs.AbsResourceInstanceObject { + return stackaddrs.AbsResourceInstanceObject{ + Component: h.addr, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: riAddr, + DeposedKey: dk, + }, + } +} + +func (h *componentInstanceTerraformHook) PreDiff(id terraform.HookResourceIdentity, dk addrs.DeposedKey, priorState, proposedNewState cty.Value) (terraform.HookAction, error) { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceObjectAddr(id.Addr, dk), + ProviderAddr: id.ProviderAddr, + Status: hooks.ResourceInstancePlanning, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PostDiff(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceObjectAddr(id.Addr, dk), + ProviderAddr: id.ProviderAddr, + Status: hooks.ResourceInstancePlanned, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PreApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (terraform.HookAction, error) { + if action != plans.NoOp { + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: h.resourceInstanceObjectAddr(id.Addr, dk), + ProviderAddr: id.ProviderAddr, + Status: hooks.ResourceInstanceApplying, + }) + } + + h.mu.Lock() + if h.resourceInstanceObjectApplyAction.Len() == 0 { + h.resourceInstanceObjectApplyAction = addrs.MakeMap[addrs.AbsResourceInstanceObject, plans.Action]() + } + localObjAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: id.Addr, + DeposedKey: dk, + } + + // We may have stored a previous action for this resource instance if it is + // planned as create-then-destroy or destroy-then-create. For those two + // cases we need to synthesize the compound action so that it is reported + // correctly at the end of the apply process. + if prevAction, ok := h.resourceInstanceObjectApplyAction.GetOk(localObjAddr); ok { + if prevAction == plans.Delete && action == plans.Create { + action = plans.DeleteThenCreate + } else if prevAction == plans.Create && action == plans.Delete { + action = plans.CreateThenDelete + } + } + h.resourceInstanceObjectApplyAction.Put(localObjAddr, action) + h.mu.Unlock() + + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PostApply(id terraform.HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (terraform.HookAction, error) { + objAddr := h.resourceInstanceObjectAddr(id.Addr, dk) + localObjAddr := id.Addr.DeposedObject(dk) + + h.mu.Lock() + action, ok := h.resourceInstanceObjectApplyAction.GetOk(localObjAddr) + h.mu.Unlock() + if !ok { + // Weird, but we'll just tolerate it to be robust. + return terraform.HookActionContinue, nil + } + + if action == plans.NoOp { + // We don't emit starting hooks for no-op changes and so we shouldn't + // emit ending hooks for them either. + return terraform.HookActionContinue, nil + } + + status := hooks.ResourceInstanceApplied + if err != nil { + status = hooks.ResourceInstanceErrored + } else { + h.mu.Lock() + if h.resourceInstanceObjectApplySuccess == nil { + h.resourceInstanceObjectApplySuccess = addrs.MakeSet[addrs.AbsResourceInstanceObject]() + } + h.resourceInstanceObjectApplySuccess.Add(localObjAddr) + h.mu.Unlock() + } + + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceStatus, &hooks.ResourceInstanceStatusHookData{ + Addr: objAddr, + ProviderAddr: id.ProviderAddr, + Status: status, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) PreProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string) (terraform.HookAction, error) { + // NOTE: We assume provisioner events are always about the "current" + // object for the given resource instance, because the hook API does + // not include a DeposedKey argument in this case. + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceProvisionerStatus, &hooks.ResourceInstanceProvisionerHookData{ + Addr: h.resourceInstanceObjectAddr(id.Addr, addrs.NotDeposed), + Name: typeName, + Status: hooks.ProvisionerProvisioning, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) ProvisionOutput(id terraform.HookResourceIdentity, typeName string, msg string) { + // TODO: determine whether we should continue line splitting as we do with jsonHook + + // NOTE: We assume provisioner events are always about the "current" + // object for the given resource instance, because the hook API does + // not include a DeposedKey argument in this case. + output := msg + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceProvisionerStatus, &hooks.ResourceInstanceProvisionerHookData{ + Addr: h.resourceInstanceObjectAddr(id.Addr, addrs.NotDeposed), + Name: typeName, + Status: hooks.ProvisionerProvisioning, + Output: &output, + }) +} + +func (h *componentInstanceTerraformHook) PostProvisionInstanceStep(id terraform.HookResourceIdentity, typeName string, err error) (terraform.HookAction, error) { + // NOTE: We assume provisioner events are always about the "current" + // object for the given resource instance, because the hook API does + // not include a DeposedKey argument in this case. + status := hooks.ProvisionerProvisioned + if err != nil { + status = hooks.ProvisionerErrored + } + hookMore(h.ctx, h.seq, h.hooks.ReportResourceInstanceProvisionerStatus, &hooks.ResourceInstanceProvisionerHookData{ + Addr: h.resourceInstanceObjectAddr(id.Addr, addrs.NotDeposed), + Name: typeName, + Status: status, + }) + return terraform.HookActionContinue, nil +} + +func (h *componentInstanceTerraformHook) ResourceInstanceObjectAppliedAction(addr addrs.AbsResourceInstanceObject) plans.Action { + h.mu.Lock() + ret, ok := h.resourceInstanceObjectApplyAction.GetOk(addr) + h.mu.Unlock() + if !ok { + return plans.NoOp + } + return ret +} + +func (h *componentInstanceTerraformHook) ResourceInstanceObjectsSuccessfullyApplied() addrs.Set[addrs.AbsResourceInstanceObject] { + return h.resourceInstanceObjectApplySuccess +} diff --git a/internal/stacks/stackruntime/internal/stackeval/terraform_hook_test.go b/internal/stacks/stackruntime/internal/stackeval/terraform_hook_test.go new file mode 100644 index 0000000000..72fce5c2a0 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/terraform_hook_test.go @@ -0,0 +1,271 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + "github.com/hashicorp/terraform/internal/terraform" + "github.com/zclconf/go-cty/cty" +) + +func TestTerraformHook(t *testing.T) { + var gotRihd *hooks.ResourceInstanceStatusHookData + testHooks := &Hooks{ + ReportResourceInstanceStatus: func(ctx context.Context, span any, rihd *hooks.ResourceInstanceStatusHookData) any { + gotRihd = rihd + return span + }, + } + componentAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + } + + makeHook := func() *componentInstanceTerraformHook { + return &componentInstanceTerraformHook{ + ctx: context.Background(), + seq: &hookSeq{ + tracking: "boop", + }, + hooks: testHooks, + addr: componentAddr, + } + } + + resourceAddr := addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "foo", + Name: "bar", + }, + Key: addrs.NoKey, + }, + } + providerAddr := addrs.Provider{ + Type: "foo", + Namespace: "hashicorp", + Hostname: "example.com", + } + resourceIdentity := terraform.HookResourceIdentity{ + Addr: resourceAddr, + ProviderAddr: providerAddr, + } + stackAddr := stackaddrs.AbsResourceInstanceObject{ + Component: componentAddr, + Item: resourceAddr.CurrentObject(), + } + + t.Run("PreDiff", func(t *testing.T) { + hook := makeHook() + action, err := hook.PreDiff(resourceIdentity, addrs.NotDeposed, cty.NilVal, cty.NilVal) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + if hook.seq.tracking != "boop" { + t.Errorf("wrong tracking value: %#v", hook.seq.tracking) + } + + wantRihd := &hooks.ResourceInstanceStatusHookData{ + Addr: stackAddr, + ProviderAddr: providerAddr, + Status: hooks.ResourceInstancePlanning, + } + if diff := cmp.Diff(gotRihd, wantRihd); diff != "" { + t.Errorf("wrong status hook data:\n%s", diff) + } + }) + + t.Run("PostDiff", func(t *testing.T) { + hook := makeHook() + action, err := hook.PostDiff(resourceIdentity, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + if hook.seq.tracking != "boop" { + t.Errorf("wrong tracking value: %#v", hook.seq.tracking) + } + + wantRihd := &hooks.ResourceInstanceStatusHookData{ + Addr: stackAddr, + ProviderAddr: providerAddr, + Status: hooks.ResourceInstancePlanned, + } + if diff := cmp.Diff(gotRihd, wantRihd); diff != "" { + t.Errorf("wrong status hook data:\n%s", diff) + } + }) + + t.Run("PreApply", func(t *testing.T) { + hook := makeHook() + action, err := hook.PreApply(resourceIdentity, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + if hook.seq.tracking != "boop" { + t.Errorf("wrong tracking value: %#v", hook.seq.tracking) + } + + wantRihd := &hooks.ResourceInstanceStatusHookData{ + Addr: stackAddr, + ProviderAddr: providerAddr, + Status: hooks.ResourceInstanceApplying, + } + if diff := cmp.Diff(gotRihd, wantRihd); diff != "" { + t.Errorf("wrong status hook data:\n%s", diff) + } + }) + + t.Run("PostApply", func(t *testing.T) { + hook := makeHook() + // It is invalid to call PostApply without first calling PreApply + action, err := hook.PreApply(resourceIdentity, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + + action, err = hook.PostApply(resourceIdentity, addrs.NotDeposed, cty.NilVal, nil) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + if hook.seq.tracking != "boop" { + t.Errorf("wrong tracking value: %#v", hook.seq.tracking) + } + + wantRihd := &hooks.ResourceInstanceStatusHookData{ + Addr: stackAddr, + ProviderAddr: providerAddr, + Status: hooks.ResourceInstanceApplied, + } + if diff := cmp.Diff(gotRihd, wantRihd); diff != "" { + t.Errorf("wrong status hook data:\n%s", diff) + } + }) + + t.Run("PostApply errored", func(t *testing.T) { + hook := makeHook() + // It is invalid to call PostApply without first calling PreApply + action, err := hook.PreApply(resourceIdentity, addrs.NotDeposed, plans.Create, cty.NilVal, cty.NilVal) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + + action, err = hook.PostApply(resourceIdentity, addrs.NotDeposed, cty.NilVal, errors.New("splines unreticulatable")) + if err != nil { + t.Errorf("unexpected error: %s", err) + } + if action != terraform.HookActionContinue { + t.Errorf("wrong action: %#v", action) + } + if hook.seq.tracking != "boop" { + t.Errorf("wrong tracking value: %#v", hook.seq.tracking) + } + + wantRihd := &hooks.ResourceInstanceStatusHookData{ + Addr: stackAddr, + ProviderAddr: providerAddr, + Status: hooks.ResourceInstanceErrored, + } + if diff := cmp.Diff(gotRihd, wantRihd); diff != "" { + t.Errorf("wrong status hook data:\n%s", diff) + } + }) + + t.Run("ResourceInstanceObjectAppliedAction", func(t *testing.T) { + testCases := []struct { + actions []plans.Action + want plans.Action + }{ + { + actions: []plans.Action{plans.NoOp}, + want: plans.NoOp, + }, + { + actions: []plans.Action{plans.Create}, + want: plans.Create, + }, + { + actions: []plans.Action{plans.Delete}, + want: plans.Delete, + }, + { + actions: []plans.Action{plans.Update}, + want: plans.Update, + }, + { + // We return a fallback of no-op if the object has no recorded + // applied action. + actions: []plans.Action{}, + want: plans.NoOp, + }, + { + // Create-then-delete plans result in two separate apply + // operations, which we need to recombine into a single one in + // order to correctly count the operations. + actions: []plans.Action{plans.Create, plans.Delete}, + want: plans.CreateThenDelete, + }, + { + // See above: same for delete-then-create. + actions: []plans.Action{plans.Delete, plans.Create}, + want: plans.DeleteThenCreate, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%v", tc.actions), func(t *testing.T) { + hook := makeHook() + + for _, action := range tc.actions { + _, err := hook.PreApply(resourceIdentity, addrs.NotDeposed, action, cty.NilVal, cty.NilVal) + if err != nil { + t.Fatalf("unexpected error in PreApply: %s", err) + } + + _, err = hook.PostApply(resourceIdentity, addrs.NotDeposed, cty.NilVal, nil) + if err != nil { + t.Fatalf("unexpected error in PostApply: %s", err) + } + } + + got := hook.ResourceInstanceObjectAppliedAction(resourceAddr.CurrentObject()) + + if got != tc.want { + t.Errorf("wrong result: got %v, want %v", got, tc.want) + } + }) + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/test_only_global.go b/internal/stacks/stackruntime/internal/stackeval/test_only_global.go new file mode 100644 index 0000000000..cb0db6e1a3 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/test_only_global.go @@ -0,0 +1,59 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func (m *Main) resolveTestOnlyGlobalReference(addr stackaddrs.TestOnlyGlobal, rng tfdiags.SourceRange) (Referenceable, tfdiags.Diagnostics) { + if m.testOnlyGlobals == nil { + var diags tfdiags.Diagnostics + // We don't seem to be running in a testing context, so we'll pretend + // that test-only globals don't exist at all. + // + // This diagnostic is designed to resemble the one that + // stackaddrs.ParseReference would return if given a traversal + // that has no recognizable prefix, since this reference type should + // behave as if it doesn't exist at all when we're not doing internal + // testing. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to unknown symbol", + Detail: "There is no symbol _test_only_global defined in the current scope.", + Subject: rng.ToHCL().Ptr(), + }) + return nil, diags + } + if _, exists := m.testOnlyGlobals[addr.Name]; !exists { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undefined test-only global", + Detail: fmt.Sprintf("Test-only globals are available here, but there's no definition for one named %q.", addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return nil, diags + } + return &testOnlyGlobal{name: addr.Name, main: m}, nil +} + +type testOnlyGlobal struct { + name string + main *Main +} + +// ExprReferenceValue implements Referenceable. +func (g *testOnlyGlobal) ExprReferenceValue(context.Context, EvalPhase) cty.Value { + // By the time we get here we can assume that we represent an + // actually-defined test-only global, because + // Main.resolveTestOnlyGlobalReference checks that. + return g.main.testOnlyGlobals[g.name] +} diff --git a/internal/stacks/stackruntime/internal/stackeval/test_only_global_test.go b/internal/stacks/stackruntime/internal/stackeval/test_only_global_test.go new file mode 100644 index 0000000000..10ed414f28 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/test_only_global_test.go @@ -0,0 +1,128 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/zclconf/go-cty/cty" +) + +// This file contains tests for the "test-only globals" mechanism itself, and +// so if tests from this file fail at the same time as tests in other files +// in this package it's probably productive to address the failures in here +// first, in case they are indirectly causing failures in other files where +// unit tests are written to rely on this mechanism. + +func TestTestOnlyGlobals_parseAndEval(t *testing.T) { + // We'll do our best to try to attract the attention of the hypothetical + // maintainer who is looking at a wall of test failures of many different + // tests that were all depending on test-only globals to function + // correctly. + t.Log(` +-------------------------------------------------------------------------------------------------------------------------- +NOTE: If any part of this test fails, the problem might also be the cause of other test failures elsewhere in this package + (so maybe prioritize fixing this one first!) +-------------------------------------------------------------------------------------------------------------------------- +`) + + // We're evaluating individual expressions in isolation here because + // that allows us to focus as closely as possible on only testing the + // test-only globals mechanism itself, without relying on any other + // language features such as output values. + // + // This does mean that this test might miss some situations that can + // arise only when a test only global is mentioned in a particular + // evaluation context. If that arises later, consider adding another + // test alongside this one which tests _that_ situation as tightly + // as possible too; the goal of the tests in this file is to give a + // clear signal if the test utilities themselves are malfunctioning, + // so that maintainers can minimize time wasted trying to debug another + // test that's relying on this utility. + fooExpr, hclDiags := hclsyntax.ParseExpression([]byte("_test_only_global.foo"), "test", hcl.InitialPos) + if hclDiags.HasErrors() { + t.Fatalf("failed to parse expression: %s", hclDiags.Error()) + } + barAttrExpr, hclDiags := hclsyntax.ParseExpression([]byte("_test_only_global.bar.attr"), "test", hcl.InitialPos) + if hclDiags.HasErrors() { + t.Fatalf("failed to parse expression: %s", hclDiags.Error()) + } + nonExistExpr, hclDiags := hclsyntax.ParseExpression([]byte("_test_only_global.nonexist"), "test", hcl.InitialPos) + if hclDiags.HasErrors() { + t.Fatalf("failed to parse expression: %s", hclDiags.Error()) + } + + fakeConfig := testStackConfigEmpty(t) + main := testEvaluator(t, testEvaluatorOpts{ + Config: fakeConfig, + TestOnlyGlobals: map[string]cty.Value{ + "foo": cty.StringVal("foo value"), + "bar": cty.ObjectVal(map[string]cty.Value{ + "attr": cty.StringVal("bar.attr value"), + }), + }, + }) + + ctx := context.Background() + mainStack := main.MainStack() + + t.Run("foo", func(t *testing.T) { + got, diags := EvalExpr(ctx, fooExpr, InspectPhase, mainStack) + if diags.HasErrors() { + t.Errorf("unexpected errors: %s", diags.Err().Error()) + } + want := cty.StringVal("foo value") + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + + t.Run("without test-only globals enabled", func(t *testing.T) { + noGlobalsMain := NewForInspecting(fakeConfig, stackstate.NewState(), InspectOpts{}) + mainStack := noGlobalsMain.MainStack() + got, diags := EvalExpr(ctx, fooExpr, InspectPhase, mainStack) + if !diags.HasErrors() { + t.Fatalf("unexpected success\ngot: %#v\nwant: an error diagnostic", got) + } + if len(diags) != 1 { + t.Fatalf("unexpected diagnostics: %s", diags.Err().Error()) + } + // Without test-only globals enabled, we try our best to pretend + // that test-only globals don't exist at all, since they are an + // implementation detail as far as end-users are concerned. + gotSummary := diags[0].Description().Summary + wantSummary := `Reference to unknown symbol` + if gotSummary != wantSummary { + t.Errorf("unexpected diagnostic summary\ngot: %s\nwant: %s", gotSummary, wantSummary) + } + }) + }) + t.Run("bar.attr", func(t *testing.T) { + got, diags := EvalExpr(ctx, barAttrExpr, InspectPhase, mainStack) + if diags.HasErrors() { + t.Errorf("unexpected errors: %s", diags.Err().Error()) + } + want := cty.StringVal("bar.attr value") + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("nonexist", func(t *testing.T) { + got, diags := EvalExpr(ctx, nonExistExpr, InspectPhase, mainStack) + if !diags.HasErrors() { + t.Fatalf("unexpected success\ngot: %#v\nwant: an error diagnostic", got) + } + if len(diags) != 1 { + t.Fatalf("unexpected diagnostics: %s", diags.Err().Error()) + } + gotSummary := diags[0].Description().Summary + wantSummary := `Reference to undefined test-only global` + if gotSummary != wantSummary { + t.Errorf("unexpected diagnostic summary\ngot: %s\nwant: %s", gotSummary, wantSummary) + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/applying/component_dependencies/applying_component_dependencies.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/applying/component_dependencies/applying_component_dependencies.tf new file mode 100644 index 0000000000..f1c054b3ed --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/applying/component_dependencies/applying_component_dependencies.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } +} + +variable "marker" { + type = string +} + +variable "deps" { + type = set(string) + default = [] +} + +resource "test_report" "main" { + marker = var.marker +} + +output "marker" { + value = var.marker +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/applying/component_dependencies/applying_component_dependencies.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/applying/component_dependencies/applying_component_dependencies.tfstack.hcl new file mode 100644 index 0000000000..1138ca48a3 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/applying/component_dependencies/applying_component_dependencies.tfstack.hcl @@ -0,0 +1,44 @@ +required_providers { + test = { + source = "terraform.io/builtin/test" + } +} + +provider "test" "main" { +} + +component "a" { + source = "./" + + inputs = { + marker = "a" + } + providers = { + test = provider.test.main + } +} + +component "b" { + source = "./" + for_each = toset(["i", "ii", "iii"]) + + inputs = { + marker = "b.${each.key}" + deps = [component.a.marker] + } + providers = { + test = provider.test.main + } +} + +component "c" { + source = "./" + + inputs = { + marker = "c" + deps = [ for b in component.b : b.marker ] + } + providers = { + test = provider.test.main + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/for_each/component-for-each.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/for_each/component-for-each.tfstack.hcl new file mode 100644 index 0000000000..df334d7007 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/for_each/component-for-each.tfstack.hcl @@ -0,0 +1,14 @@ +# Set the test-only global "component_instances" to a map to use for the +# for_each expression of the test component. + +component "foo" { + source = "../modules/with_variable_and_output" + for_each = _test_only_global.component_instances + + inputs = { + test = { + key = each.key + value = each.value + } + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/modules/with_variable_and_output/with-variable-and-output.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/modules/with_variable_and_output/with-variable-and-output.tf new file mode 100644 index 0000000000..7c3bdd76eb --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/modules/with_variable_and_output/with-variable-and-output.tf @@ -0,0 +1,8 @@ +variable "test" { + type = any + default = null +} + +output "test" { + value = var.test +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/single_instance/component-single-instance.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/single_instance/component-single-instance.tfstack.hcl new file mode 100644 index 0000000000..1e765a6bf2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/component/single_instance/component-single-instance.tfstack.hcl @@ -0,0 +1,11 @@ +# Set the test-only global "component_inputs" to an object. +# +# The child module we're using here expects a single input value of any type +# called "test", and will echo it back verbatim as an output value also called +# "test". + +component "foo" { + source = "../modules/with_variable_and_output" + + inputs = _test_only_global.component_inputs +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/basics/child/input-variable-basics-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/basics/child/input-variable-basics-child.tfstack.hcl new file mode 100644 index 0000000000..3f2eec322d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/basics/child/input-variable-basics-child.tfstack.hcl @@ -0,0 +1,4 @@ + +variable "name" { + type = string +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/basics/input-variable-basics.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/basics/input-variable-basics.tfstack.hcl new file mode 100644 index 0000000000..d0544ee830 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/basics/input-variable-basics.tfstack.hcl @@ -0,0 +1,12 @@ + +variable "name" { + type = string +} + +stack "child" { + source = "./child" + + inputs = { + name = "child of ${var.name}" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_no/child/ephemeral-no-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_no/child/ephemeral-no-child.tfstack.hcl new file mode 100644 index 0000000000..6fcab8f3fd --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_no/child/ephemeral-no-child.tfstack.hcl @@ -0,0 +1,3 @@ +variable "a" { + type = string +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_no/ephemeral-no.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_no/ephemeral-no.tfstack.hcl new file mode 100644 index 0000000000..da473bfa5e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_no/ephemeral-no.tfstack.hcl @@ -0,0 +1,8 @@ + +stack "child" { + source = "./child" + + inputs = { + a = _test_only_global.var_val + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_yes/child/ephemeral-yes-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_yes/child/ephemeral-yes-child.tfstack.hcl new file mode 100644 index 0000000000..e1d2b0e740 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_yes/child/ephemeral-yes-child.tfstack.hcl @@ -0,0 +1,4 @@ +variable "a" { + type = string + ephemeral = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_yes/ephemeral-yes.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_yes/ephemeral-yes.tfstack.hcl new file mode 100644 index 0000000000..da473bfa5e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/input_variable/ephemeral_yes/ephemeral-yes.tfstack.hcl @@ -0,0 +1,8 @@ + +stack "child" { + source = "./child" + + inputs = { + a = _test_only_global.var_val + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid/invalid.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid/invalid.tf new file mode 100644 index 0000000000..dd21f75b3a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid/invalid.tf @@ -0,0 +1,3 @@ +invalid "top_level" "block" { + not_valid = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/.terraform/modules/modules.json b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/.terraform/modules/modules.json new file mode 100644 index 0000000000..89f17c8ae9 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"child","Source":"./child","Dir":"child"}]} \ No newline at end of file diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/child/invalid_child.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/child/invalid_child.tf new file mode 100644 index 0000000000..dd21f75b3a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/child/invalid_child.tf @@ -0,0 +1,3 @@ +invalid "top_level" "block" { + not_valid = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/invalid.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/invalid.tf new file mode 100644 index 0000000000..1f95749fa7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_child/invalid.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/first/child/invalid_child.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/first/child/invalid_child.tf new file mode 100644 index 0000000000..dd21f75b3a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/first/child/invalid_child.tf @@ -0,0 +1,3 @@ +invalid "top_level" "block" { + not_valid = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/first/first.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/first/first.tf new file mode 100644 index 0000000000..1f95749fa7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/first/first.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/root.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/root.tf new file mode 100644 index 0000000000..5c20dce100 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/root.tf @@ -0,0 +1,7 @@ +module "first" { + source = "./first" +} + +module "second" { + source = "./second" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/second/child/invalid_child.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/second/child/invalid_child.tf new file mode 100644 index 0000000000..dd21f75b3a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/second/child/invalid_child.tf @@ -0,0 +1,3 @@ +invalid "top_level" "block" { + not_valid = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/second/second.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/second/second.tf new file mode 100644 index 0000000000..1f95749fa7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/invalid_grandchildren/second/second.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/basics/child/local-value-basics-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/basics/child/local-value-basics-child.tfstack.hcl new file mode 100644 index 0000000000..397d986a3d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/basics/child/local-value-basics-child.tfstack.hcl @@ -0,0 +1,9 @@ + +variable "name" { + type = string +} + +output "outputted_name" { + type = string + value = "outputted-${var.name}" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/basics/local-value-basics.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/basics/local-value-basics.tfstack.hcl new file mode 100644 index 0000000000..6dcff9b065 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/basics/local-value-basics.tfstack.hcl @@ -0,0 +1,22 @@ + +locals { + name = "jackson" + childName = stack.child.outputted_name + functional = format("Hello, %s!", "Ander") + mappy = { + name = "jackson", + age = 30 + } + + listy = ["jackson", 30] + booleany = true + conditiony = local.booleany == true ? "true" : "false" +} + +stack "child" { + source = "./child" + + inputs = { + name = "child of ${local.name}" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/custom_provider/child/child.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/custom_provider/child/child.tf new file mode 100644 index 0000000000..7d76519732 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/custom_provider/child/child.tf @@ -0,0 +1,39 @@ +terraform { +required_providers { + testing = { + source = "hashicorp/testing" + } + } +} + +variable "name" { + type = string +} +variable "list" { + type = list(string) +} +variable "map" { + type = map(string) +} + +resource "testing_resource" "resource" { + id = var.name + value = "foo" +} + +data "testing_data_source" "data_source" { + id = var.name + depends_on = [testing_resource.resource] +} + +output "bar" { + value = "${var.name}-${data.testing_data_source.data_source.value}" +} + +output "list" { + value = concat(var.list, ["${data.testing_data_source.data_source.value}"]) +} + +output "map" { + value = merge(var.map, { "value" = data.testing_data_source.data_source.value }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/custom_provider/custom-provider-local.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/custom_provider/custom-provider-local.tfstack.hcl new file mode 100644 index 0000000000..9e2eb19373 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/local_value/custom_provider/custom-provider-local.tfstack.hcl @@ -0,0 +1,46 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.0.1" + } +} + +locals { + stringName = "through-local-${component.child.bar}" + listName = ["through-local-${component.child.bar}"] + mapName = { + key = "through-local-${component.child.bar}" + } +} + +provider "testing" "this" {} + +component "child" { + source = "./child" + + inputs = { + name = "aloha" + list = ["aloha"] + map = { + key = "aloha" + } + } + + providers = { + testing = provider.testing.this + } +} + +component "child2" { + source = "./child" + + inputs = { + name = local.stringName + list = local.listName + map = local.mapName + } + + providers = { + testing = provider.testing.this + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/basics/child/output-value-basics-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/basics/child/output-value-basics-child.tfstack.hcl new file mode 100644 index 0000000000..c6e4f551a6 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/basics/child/output-value-basics-child.tfstack.hcl @@ -0,0 +1,4 @@ +output "foo" { + type = string + value = _test_only_global.child_output +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/basics/output-value-basics.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/basics/output-value-basics.tfstack.hcl new file mode 100644 index 0000000000..365cc02077 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/basics/output-value-basics.tfstack.hcl @@ -0,0 +1,13 @@ +output "root" { + type = string + value = _test_only_global.root_output +} + +output "child" { + type = string + value = stack.child.foo +} + +stack "child" { + source = "./child" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_child/child/ephemeral-child-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_child/child/ephemeral-child-child.tfstack.hcl new file mode 100644 index 0000000000..2aef272554 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_child/child/ephemeral-child-child.tfstack.hcl @@ -0,0 +1,5 @@ +output "result" { + type = string + value = _test_only_global.result + ephemeral = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_child/ephemeral-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_child/ephemeral-child.tfstack.hcl new file mode 100644 index 0000000000..667059557a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_child/ephemeral-child.tfstack.hcl @@ -0,0 +1,3 @@ +stack "child" { + source = "./child" +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_no/ephemeral-no.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_no/ephemeral-no.tfstack.hcl new file mode 100644 index 0000000000..8c5782bdbd --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_no/ephemeral-no.tfstack.hcl @@ -0,0 +1,4 @@ +output "result" { + type = string + value = _test_only_global.result +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_yes/ephemeral-yes.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_yes/ephemeral-yes.tfstack.hcl new file mode 100644 index 0000000000..2aef272554 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/output_value/ephemeral_yes/ephemeral-yes.tfstack.hcl @@ -0,0 +1,5 @@ +output "result" { + type = string + value = _test_only_global.result + ephemeral = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/README.md b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/README.md new file mode 100644 index 0000000000..dca0e3c88b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/README.md @@ -0,0 +1,7 @@ +This virtual source package contains test fixtures for integration testing +of the overall planning phase. + +The test configurations in here are typically intended only for one or two +specific tests, and we should prefer to create more simple test fixtures rather +than trying to share test fixtures too freely between different tests and thus +make it harder to understand what exactly they are testing. diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/deferred_changes_propagation/deferred-changes-propagation.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/deferred_changes_propagation/deferred-changes-propagation.tf new file mode 100644 index 0000000000..91dc06d332 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/deferred_changes_propagation/deferred-changes-propagation.tf @@ -0,0 +1,26 @@ + +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } +} + +variable "instance_count" { + type = number +} + +resource "test" "a" { + # This one has on intrinsic need to be deferred, but + # should still be deferred when an upstream component + # has a deferral. +} + +resource "test" "b" { + count = var.instance_count +} + +output "constant_one" { + value = 1 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/deferred_changes_propagation/deferred-changes-propagation.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/deferred_changes_propagation/deferred-changes-propagation.tfstack.hcl new file mode 100644 index 0000000000..d429a4d63e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/deferred_changes_propagation/deferred-changes-propagation.tfstack.hcl @@ -0,0 +1,35 @@ + +required_providers { + test = { + source = "terraform.io/builtin/test" + } +} + +provider "test" "main" { +} + +variable "first_count" { + type = number +} + +component "first" { + source = "./" + + inputs = { + instance_count = var.first_count + } + providers = { + test = provider.test.main + } +} + +component "second" { + source = "./" + + inputs = { + instance_count = component.first.constant_one + } + providers = { + test = provider.test.main + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/child/named_promises_child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/child/named_promises_child.tfstack.hcl new file mode 100644 index 0000000000..8da88fe3a2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/child/named_promises_child.tfstack.hcl @@ -0,0 +1,11 @@ +# This is a very minimal stack configuration just to give us something to +# call as a nested stack in the parent stack configuration. + +variable "in" { + type = string +} + +output "out" { + type = string + value = var.in +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/named_promises.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/named_promises.tf new file mode 100644 index 0000000000..d54b6cb567 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/named_promises.tf @@ -0,0 +1,13 @@ +# This is an intentionally-minimal module just to give us something to +# point a component block at. + +terraform { + required_providers { + happycloud = { + source = "example.com/test/happycloud" + } + } +} + +resource "happycloud_thingy" "foo" { +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/named_promises.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/named_promises.tfstack.hcl new file mode 100644 index 0000000000..11cc036709 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/named_promises/named_promises.tfstack.hcl @@ -0,0 +1,35 @@ + +required_providers { + happycloud = { + source = "example.com/test/happycloud" + version = "1.0.0" + } +} + +variable "in" { + type = string +} + +provider "happycloud" "main" { +} + +stack "child" { + source = "./child" + + inputs = { + in = var.in + } +} + +component "foo" { + source = "./" + + providers = { + happycloud = provider.happycloud.main + } +} + +output "out" { + type = string + value = var.in +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf new file mode 100644 index 0000000000..e911ee4453 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tf @@ -0,0 +1,6 @@ +output "invalid" { + # terraform.workspace is not available when this module is used as part + # of a stack component, so this should produce an error during + # planning. + value = terraform.workspace +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl new file mode 100644 index 0000000000..ac4b9e559f --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/no_workspace_name_ref/no-workspace-name-ref.tfstack.hcl @@ -0,0 +1,3 @@ +component "mod" { + source = "./" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/module/child/main.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/module/child/main.tf new file mode 100644 index 0000000000..37dd809389 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/module/child/main.tf @@ -0,0 +1,12 @@ + +output "cwd" { + value = path.cwd +} + +output "root" { + value = path.root +} + +output "module" { + value = path.module +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/module/main.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/module/main.tf new file mode 100644 index 0000000000..58cbed5e97 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/module/main.tf @@ -0,0 +1,25 @@ + + +module "child" { + source = "./child" +} + +output "child_module" { + value = module.child.module +} + +output "child_root" { + value = module.child.root +} + +output "module" { + value = path.module +} + +output "root" { + value = path.root +} + +output "cwd" { + value = path.cwd +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/path_values.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/path_values.tfstack.hcl new file mode 100644 index 0000000000..c3255396eb --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/path_values/path_values.tfstack.hcl @@ -0,0 +1,4 @@ + +component "path_values" { + source = "./module" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/module_a/plan-destroy-a.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/module_a/plan-destroy-a.tf new file mode 100644 index 0000000000..4553e85fc7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/module_a/plan-destroy-a.tf @@ -0,0 +1,18 @@ + +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + + configuration_aliases = [ test ] + } + } +} + +resource "test" "foo" { + for_module = "a" +} + +output "result" { + value = test.foo.result +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/module_b/plan-destroy-b.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/module_b/plan-destroy-b.tf new file mode 100644 index 0000000000..4e306f1956 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/module_b/plan-destroy-b.tf @@ -0,0 +1,24 @@ + +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + + configuration_aliases = [ test ] + } + } +} + +variable "from_a" { + type = string +} + +resource "test" "foo" { + for_module = "b" + + arg = var.from_a +} + +output "result" { + value = test.foo.result +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/plan-destroy.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/plan-destroy.tfstack.hcl new file mode 100644 index 0000000000..3cbd941932 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/plan_destroy/plan-destroy.tfstack.hcl @@ -0,0 +1,39 @@ + +required_providers { + test = { + source = "terraform.io/builtin/test" + } +} + +component "a" { + source = "./module_a" + + providers = { + test = provider.test.main + } +} + +component "b" { + source = "./module_b" + + inputs = { + from_a = component.a.result + } + + providers = { + test = provider.test.main + } +} + +provider "test" "main" { +} + +output "from_a" { + type = string + value = component.a.result +} + +output "from_b" { + type = string + value = component.b.result +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step1/remove-data-resource-step1.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step1/remove-data-resource-step1.tf new file mode 100644 index 0000000000..430b5bf3d6 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step1/remove-data-resource-step1.tf @@ -0,0 +1,11 @@ + +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } +} + +data "test" "test" { +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step1/remove-data-resource-step1.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step1/remove-data-resource-step1.tfstack.hcl new file mode 100644 index 0000000000..a7aca6df80 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step1/remove-data-resource-step1.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + test = { + source = "terraform.io/builtin/test" + } +} + +provider "test" "main" { +} + +component "main" { + source = "./" + + providers = { + test = provider.test.main + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step2/remove-data-resource-step2.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step2/remove-data-resource-step2.tf new file mode 100644 index 0000000000..8e75ad23f6 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step2/remove-data-resource-step2.tf @@ -0,0 +1,10 @@ + +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } +} + +# data.test.test is now gone! diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step2/remove-data-resource-step2.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step2/remove-data-resource-step2.tfstack.hcl new file mode 100644 index 0000000000..a7aca6df80 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/remove_data_resource/step2/remove-data-resource-step2.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + test = { + source = "terraform.io/builtin/test" + } +} + +provider "test" "main" { +} + +component "main" { + source = "./" + + providers = { + test = provider.test.main + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/child/required-components-child.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/child/required-components-child.tfstack.hcl new file mode 100644 index 0000000000..259d917ff0 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/child/required-components-child.tfstack.hcl @@ -0,0 +1,9 @@ +variable "in" { + type = string + default = "" +} + +output "out" { + type = string + value = var.in +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/module/required-components.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/module/required-components.tf new file mode 100644 index 0000000000..806295f926 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/module/required-components.tf @@ -0,0 +1,8 @@ +variable "in" { + type = string + default = "" +} + +output "out" { + value = var.in +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/required-components.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/required-components.tfstack.hcl new file mode 100644 index 0000000000..73ea9b828e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/planning/required_components/required-components.tfstack.hcl @@ -0,0 +1,50 @@ + +required_providers { + foo = { + source = "terraform.io/builtin/foo" + } +} + +component "a" { + source = "./module" +} + +component "b" { + source = "./module" + + inputs = { + in = component.a.out + } +} + +output "out" { + type = string + value = component.a.out +} + +provider "foo" "bar" { + config { + in = { + a = component.a.out + b = component.b.out + } + } +} + +stack "child" { + source = "./child" + + inputs = { + in = component.b.out + } +} + +component "c" { + source = "./module" + + inputs = { + # stack.child.out depends indirectly on component.b, so therefore + # component.c should transitively depend on component.b. + in = "${component.a.out}-${stack.child.out}" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/for_each/provider-for-each.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/for_each/provider-for-each.tfstack.hcl new file mode 100644 index 0000000000..7a13f28ed0 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/for_each/provider-for-each.tfstack.hcl @@ -0,0 +1,12 @@ +# Set the test-only global "provider_instances" to the value that should be +# assigned to the provider blocks' for_each argument. + +required_providers { + foo = { + source = "terraform.io/builtin/foo" + } +} + +provider "foo" "bar" { + for_each = _test_only_global.provider_instances +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/single_instance/provider-single-instance.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/single_instance/provider-single-instance.tfstack.hcl new file mode 100644 index 0000000000..b7f2855efa --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/single_instance/provider-single-instance.tfstack.hcl @@ -0,0 +1,8 @@ +required_providers { + foo = { + source = "terraform.io/builtin/foo" + } +} + +provider "foo" "bar" { +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/single_instance_configured/provider-single-instance-configured.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/single_instance_configured/provider-single-instance-configured.tfstack.hcl new file mode 100644 index 0000000000..76cfbc0fcd --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/provider/single_instance_configured/provider-single-instance-configured.tfstack.hcl @@ -0,0 +1,14 @@ +# Set the test-only global "provider_configuration" to a value that should +# be assigned to the "test" argument in the provider configuration. + +required_providers { + foo = { + source = "terraform.io/builtin/foo" + } +} + +provider "foo" "bar" { + config { + test = _test_only_global.provider_configuration + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/empty/empty.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/empty/empty.tfstack.hcl new file mode 100644 index 0000000000..d2235eb2e5 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/empty/empty.tfstack.hcl @@ -0,0 +1,3 @@ +# This stack configuration is intentionally left empty; we use it as the +# configuration for a child stack in some of our StackCall and StackCallInstance +# tests where the content of the child stack is irrelevant to the test. diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/for_each/stack-call-for-each.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/for_each/stack-call-for-each.tfstack.hcl new file mode 100644 index 0000000000..29044c05f2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/for_each/stack-call-for-each.tfstack.hcl @@ -0,0 +1,14 @@ +# Set the test-only global "child_stack_for_each" to a map conforming +# to the following type constraint: +# +# map(object({ +# test_string = optional(string) +# test_map = optional(map(string)) +# })) + +stack "child" { + source = "../with_variables_and_outputs" + for_each = _test_only_global.child_stack_for_each + + inputs = each.value +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/single_instance/stack-call-single-instance.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/single_instance/stack-call-single-instance.tfstack.hcl new file mode 100644 index 0000000000..7ee7070512 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/single_instance/stack-call-single-instance.tfstack.hcl @@ -0,0 +1,13 @@ +# Set the test-only global "child_stack_inputs" to an object conforming +# to the following type constraint: +# +# object({ +# test_string = optional(string) +# test_map = optional(map(string)) +# }) + +stack "child" { + source = "../with_variables_and_outputs" + + inputs = _test_only_global.child_stack_inputs +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/with_variables_and_outputs/with-variables_and_outputs.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/with_variables_and_outputs/with-variables_and_outputs.tfstack.hcl new file mode 100644 index 0000000000..8750106b23 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/stack_call/with_variables_and_outputs/with-variables_and_outputs.tfstack.hcl @@ -0,0 +1,29 @@ +# This is intended for use as a child stack configuration for situations +# where we're testing the propagation of input variables into the child +# stack. +# +# It contains some input variable declarations that we can assign values to +# for testing purposes. Both are optional to give flexibility for reusing +# this across multiple test cases. If you need something more sophisticated +# for your test, prefer to write a new configuration rather than growing this +# one any further. + +variable "test_string" { + type = string + default = null +} + +variable "test_map" { + type = map(string) + default = null +} + +output "test_string" { + type = string + value = var.test_string +} + +output "test_map" { + type = map(string) + value = var.test_map +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/terraform-sources.json b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/terraform-sources.json new file mode 100644 index 0000000000..1d8b211e49 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/terraform-sources.json @@ -0,0 +1,53 @@ +{ + "terraform_source_bundle": 1, + "packages": [ + { + "source": "https://testing.invalid/input_variable.tar.gz", + "local": "input_variable" + }, + { + "source": "https://testing.invalid/local_value.tar.gz", + "local": "local_value" + }, + { + "source": "https://testing.invalid/output_value.tar.gz", + "local": "output_value" + }, + { + "source": "https://testing.invalid/stack_call.tar.gz", + "local": "stack_call" + }, + { + "source": "https://testing.invalid/component.tar.gz", + "local": "component" + }, + { + "source": "https://testing.invalid/provider.tar.gz", + "local": "provider" + }, + { + "source": "https://testing.invalid/planning.tar.gz", + "local": "planning" + }, + { + "source": "https://testing.invalid/applying.tar.gz", + "local": "applying" + }, + { + "source": "https://testing.invalid/validating.tar.gz", + "local": "validating" + }, + { + "source": "https://testing.invalid/invalid.tar.gz", + "local": "invalid" + }, + { + "source": "https://testing.invalid/invalid_child.tar.gz", + "local": "invalid_child" + }, + { + "source": "https://testing.invalid/invalid_grandchildren.tar.gz", + "local": "invalid_grandchildren" + } + ] +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/module-a/modules-with-provider-configs-a.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/module-a/modules-with-provider-configs-a.tf new file mode 100644 index 0000000000..1fd0f89ffc --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/module-a/modules-with-provider-configs-a.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } +} + +provider "test" { + arg = "foo" +} + +module "b" { + source = "../module-b" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/module-b/modules-with-provider-configs-b.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/module-b/modules-with-provider-configs-b.tf new file mode 100644 index 0000000000..260de54648 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/module-b/modules-with-provider-configs-b.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } +} + +provider "test" { + arg = "foo" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/modules-with-provider-configs.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/modules-with-provider-configs.tfstack.hcl new file mode 100644 index 0000000000..3afa6087e5 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/modules_with_provider_configs/modules-with-provider-configs.tfstack.hcl @@ -0,0 +1,3 @@ +component "a" { + source = "./module-a" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid/invalid.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid/invalid.tf new file mode 100644 index 0000000000..dd21f75b3a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid/invalid.tf @@ -0,0 +1,3 @@ +invalid "top_level" "block" { + not_valid = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/.terraform/modules/modules.json b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/.terraform/modules/modules.json new file mode 100644 index 0000000000..89f17c8ae9 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"child","Source":"./child","Dir":"child"}]} \ No newline at end of file diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/child/invalid_child.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/child/invalid_child.tf new file mode 100644 index 0000000000..dd21f75b3a --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/child/invalid_child.tf @@ -0,0 +1,3 @@ +invalid "top_level" "block" { + not_valid = true +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/invalid.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/invalid.tf new file mode 100644 index 0000000000..1f95749fa7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_child/invalid.tf @@ -0,0 +1,3 @@ +module "child" { + source = "./child" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_nested_remote/invalid_nested_remote.tf b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_nested_remote/invalid_nested_remote.tf new file mode 100644 index 0000000000..d0021562ce --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/invalid_nested_remote/invalid_nested_remote.tf @@ -0,0 +1,3 @@ +module "invalid_grandchild" { + source = "https://testing.invalid/invalid_child.tar.gz" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/nested_module_diagnostics.tfstack.hcl b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/nested_module_diagnostics.tfstack.hcl new file mode 100644 index 0000000000..a950c12048 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testdata/sourcebundle/validating/nested_module_diagnostics/nested_module_diagnostics.tfstack.hcl @@ -0,0 +1,32 @@ +// This in-repo component has an invalid root module +component "in_repo_invalid" { + source = "./invalid" +} + +// This in-repo component has a valid root module and an invalid child in-repo +// module +component "in_repo_invalid_child" { + source = "./invalid_child" +} + +// This in-repo component has a remote source child module with a valid root +// module and an invalid child in-repo module +component "in_repo_invalid_nested_remote" { + source = "./invalid_nested_remote" +} + +// This remote source component has an invalid root module +component "remote_invalid" { + source = "https://testing.invalid/invalid.tar.gz" +} + +// This remote source component has an invalid child in-repo module +component "remote_invalid_child" { + source = "https://testing.invalid/invalid_child.tar.gz" +} + +// This remote source component has two invalid grandchildren which are both +// in-repo modules and share the same relative source +component "remote_invalid_grandchildren" { + source = "https://testing.invalid/invalid_grandchildren.tar.gz" +} diff --git a/internal/stacks/stackruntime/internal/stackeval/testing_test.go b/internal/stacks/stackruntime/internal/stackeval/testing_test.go new file mode 100644 index 0000000000..06ec88b88c --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/testing_test.go @@ -0,0 +1,510 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "fmt" + "strings" + "sync" + "testing" + + "github.com/hashicorp/go-slug/sourceaddrs" + "github.com/hashicorp/go-slug/sourcebundle" + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" + + _ "github.com/hashicorp/terraform/internal/logging" + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// This file contains some general test utilities that many of our other +// _test.go files rely on. It doesn't actually contain any tests itself. + +// testStackConfig loads a stack configuration from the source bundle in this +// package's testdata directory. +// +// "collection" is the name of one of the synthetic source packages that's +// declared in the source bundle, and "subPath" is the path within that +// package. +func testStackConfig(t *testing.T, collection string, subPath string) *stackconfig.Config { + t.Helper() + + // Our collection of test configurations is laid out like a source + // bundle that was installed from some source addresses that don't + // really exist, and so we'll construct a suitable fake source + // address following that scheme. + fakeSrcStr := fmt.Sprintf("https://testing.invalid/%s.tar.gz//%s", collection, subPath) + fakeSrc, err := sourceaddrs.ParseRemoteSource(fakeSrcStr) + if err != nil { + t.Fatalf("artificial source address string %q is invalid: %s", fakeSrcStr, err) + } + + sources := testSourceBundle(t) + ret, diags := stackconfig.LoadConfigDir(fakeSrc, sources) + if diags.HasErrors() { + diags.Sort() + t.Fatalf("configuration is invalid\n%s", testFormatDiagnostics(t, diags)) + } + return ret +} + +func testStackConfigEmpty(t *testing.T) *stackconfig.Config { + t.Helper() + sources := testSourceBundle(t) + fakeAddr := sourceaddrs.MustParseSource("https://testing.invalid/nonexist.tar.gz").(sourceaddrs.RemoteSource) + return stackconfig.NewEmptyConfig(fakeAddr, sources) +} + +func testSourceBundle(t *testing.T) *sourcebundle.Bundle { + t.Helper() + sources, err := sourcebundle.OpenDir("testdata/sourcebundle") + if err != nil { + t.Fatalf("cannot open source bundle: %s", err) + } + return sources +} + +func testPriorState(t *testing.T, msgs map[string]protoreflect.ProtoMessage) *stackstate.State { + t.Helper() + ret, err := stackstate.LoadFromDirectProto(msgs) + if err != nil { + t.Fatal(err) + } + return ret +} + +func testPlan(t *testing.T, main *Main) (*stackplan.Plan, tfdiags.Diagnostics) { + t.Helper() + outp, outpTest := testPlanOutput(t) + main.PlanAll(context.Background(), outp) + return outpTest.Close(t) +} + +func testPlanOutput(t *testing.T) (PlanOutput, *planOutputTester) { + t.Helper() + tester := &planOutputTester{} + outp := PlanOutput{ + AnnouncePlannedChange: func(ctx context.Context, pc stackplan.PlannedChange) { + tester.mu.Lock() + tester.planned = append(tester.planned, pc) + tester.mu.Unlock() + }, + AnnounceDiagnostics: func(ctx context.Context, d tfdiags.Diagnostics) { + tester.mu.Lock() + tester.diags = tester.diags.Append(d) + tester.mu.Unlock() + }, + } + return outp, tester +} + +type planOutputTester struct { + planned []stackplan.PlannedChange + diags tfdiags.Diagnostics + mu sync.Mutex +} + +// PlannedChanges returns the planned changes that have been accumulated in the +// receiver. +// +// It isn't safe to access the returned slice concurrently with a planning +// operation. Use this method only once the plan operation is complete and +// thus the changes are finalized. +func (pot *planOutputTester) PlannedChanges() []stackplan.PlannedChange { + return pot.planned +} + +// RawChanges returns the protobuf representation changes that have been +// accumulated in the receiver. +// +// It isn't safe to call this method concurrently with a planning +// operation. Use this method only once the plan operation is complete and +// thus the raw changes are finalized. +func (pot *planOutputTester) RawChanges(t *testing.T) []*anypb.Any { + t.Helper() + + var msgs []*anypb.Any + for _, change := range pot.planned { + protoChange, err := change.PlannedChangeProto() + if err != nil { + t.Fatalf("failed to encode %T: %s", change, err) + } + msgs = append(msgs, protoChange.Raw...) + } + + // Normally it's the stackeval caller (in stackruntime) that marks a + // plan as "applyable", but since we're calling into the stackeval functions + // directly here we'll need to add that extra item ourselves. + if !pot.diags.HasErrors() { + change := stackplan.PlannedChangeApplyable{ + Applyable: true, + } + protoChange, err := change.PlannedChangeProto() + if err != nil { + t.Fatalf("failed to encode %T: %s", change, err) + } + msgs = append(msgs, protoChange.Raw...) + } + + return msgs +} + +// Diags returns the diagnostics that have been accumulated in the +// receiver. +// +// It isn't safe to access the returned slice concurrently with a planning +// operation. Use this method only once the plan operation is complete and +// thus the diagnostics are finalized. +func (pot *planOutputTester) Diags() tfdiags.Diagnostics { + return pot.diags +} + +func (pot *planOutputTester) Close(t *testing.T) (*stackplan.Plan, tfdiags.Diagnostics) { + t.Helper() + + // Caller shouldn't close concurrently with other work anyway, but we'll + // include this just to help make things behave more consistently even when + // the caller is buggy. + pot.mu.Lock() + defer pot.mu.Unlock() + + // We'll now round-trip all of the planned changes through the serialize + // and deserialize logic to approximate the effect of this plan having been + // saved and then reloaded during a subsequent apply phase, since + // the reloaded plan is a more convenient artifact to inspect in tests. + msgs := pot.RawChanges(t) + plan, err := stackplan.LoadFromProto(msgs) + if err != nil { + t.Fatalf("failed to reload saved plan: %s", err) + } + return plan, pot.diags +} + +func testApplyOutput(t *testing.T, priorStateRaw map[string]*anypb.Any) (ApplyOutput, *applyOutputTester) { + t.Helper() + tester := &applyOutputTester{} + outp := ApplyOutput{ + AnnounceAppliedChange: func(ctx context.Context, ac stackstate.AppliedChange) { + tester.mu.Lock() + tester.applied = append(tester.applied, ac) + tester.mu.Unlock() + }, + AnnounceDiagnostics: func(ctx context.Context, d tfdiags.Diagnostics) { + tester.mu.Lock() + tester.diags = tester.diags.Append(d) + tester.mu.Unlock() + }, + } + return outp, tester +} + +type applyOutputTester struct { + prior map[string]*anypb.Any + applied []stackstate.AppliedChange + diags tfdiags.Diagnostics + mu sync.Mutex +} + +// AppliedChanges returns the applied change objects that have been accumulated +// in the receiver. +// +// It isn't safe to access the returned slice concurrently with an apply +// operation. Use this method only once the apply operation is complete and +// thus the changes are finalized. +func (aot *applyOutputTester) AppliedChanges() []stackstate.AppliedChange { + return aot.applied +} + +// RawUpdatedState returns the protobuf representation of the state with the +// accumulated changes merged into it. +// +// It isn't safe to call this method concurrently with an apply +// operation. Use this method only once the apply operation is complete and +// thus the changes are finalized. +func (aot *applyOutputTester) RawUpdatedState(t *testing.T) map[string]*anypb.Any { + t.Helper() + + msgs := make(map[string]*anypb.Any) + for k, v := range aot.prior { + msgs[k] = v + } + for _, change := range aot.applied { + protoChange, err := change.AppliedChangeProto() + if err != nil { + t.Fatalf("failed to encode %T: %s", change, err) + } + for _, protoRaw := range protoChange.Raw { + if protoRaw.Value != nil { + msgs[protoRaw.Key] = protoRaw.Value + } else { + delete(msgs, protoRaw.Key) + } + } + } + + return msgs +} + +// Diags returns the diagnostics that have been accumulated in the +// receiver. +// +// It isn't safe to access the returned slice concurrently with an apply +// operation. Use this method only once the apply operation is complete and +// thus the diagnostics are finalized. +func (aot *applyOutputTester) Diags() tfdiags.Diagnostics { + return aot.diags +} + +func (aot *applyOutputTester) Close(t *testing.T) (*stackstate.State, tfdiags.Diagnostics) { + t.Helper() + + // Caller shouldn't close concurrently with other work anyway, but we'll + // include this just to help make things behave more consistently even when + // the caller is buggy. + aot.mu.Lock() + defer aot.mu.Unlock() + + // We'll now round-trip all of the applied changes through the serialize + // and deserialize logic to approximate the effect of this having having been + // saved and then reloaded during a subsequent planning phase. + msgs := aot.RawUpdatedState(t) + state, err := stackstate.LoadFromProto(msgs) + if err != nil { + t.Fatalf("failed to reload saved state: %s", err) + } + return state, aot.diags +} + +func testFormatDiagnostics(t *testing.T, diags tfdiags.Diagnostics) string { + t.Helper() + var buf strings.Builder + for _, diag := range diags { + buf.WriteString(testFormatDiagnostic(t, diag)) + buf.WriteByte('\n') + } + return buf.String() +} + +func testFormatDiagnostic(t *testing.T, diag tfdiags.Diagnostic) string { + t.Helper() + + var buf strings.Builder + switch diag.Severity() { + case tfdiags.Error: + buf.WriteString("[ERROR] ") + case tfdiags.Warning: + buf.WriteString("[WARNING] ") + default: + buf.WriteString("[PROBLEM] ") + } + desc := diag.Description() + buf.WriteString(desc.Summary) + buf.WriteByte('\n') + if subj := diag.Source().Subject; subj != nil { + buf.WriteString("at " + subj.StartString() + "\n") + } + if desc.Detail != "" { + buf.WriteByte('\n') + buf.WriteString(desc.Detail) + buf.WriteByte('\n') + } + return buf.String() +} + +func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + if len(diags) != 0 { + diags.Sort() + t.Fatalf("unexpected diagnostics\n\n%s", testFormatDiagnostics(t, diags)) + } +} + +func assertNoErrors(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + if diags.HasErrors() { + diags.Sort() + t.Fatalf("unexpected errors\n\n%s", testFormatDiagnostics(t, diags)) + } +} + +// testEvaluator constructs a [Main] that's configured for [InspectPhase] using +// the given configuration, state, and other options. +// +// This evaluator is suitable for tests that focus only on evaluation logic +// within this package, but will not be suitable for all situations. Some +// tests should instantiate [Main] directly, particularly if they intend to +// exercise phase-specific functionality like planning or applying component +// instances. +func testEvaluator(t *testing.T, opts testEvaluatorOpts) *Main { + t.Helper() + if opts.Config == nil { + t.Fatal("Config field must not be nil") + } + if opts.State == nil { + opts.State = stackstate.NewState() + } + + inputVals := make(map[stackaddrs.InputVariable]ExternalInputValue, len(opts.InputVariableValues)) + for name, val := range opts.InputVariableValues { + inputVals[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{ + Value: val, + DefRange: tfdiags.SourceRange{ + Filename: "", + Start: tfdiags.SourcePos{ + Line: 1, + Column: 1, + Byte: 0, + }, + End: tfdiags.SourcePos{ + Line: 1, + Column: 1, + Byte: 0, + }, + }, + } + } + + main := NewForInspecting(opts.Config, opts.State, InspectOpts{ + InputVariableValues: inputVals, + ProviderFactories: opts.ProviderFactories, + TestOnlyGlobals: opts.TestOnlyGlobals, + }) + t.Cleanup(func() { + main.DoCleanup(context.Background()) + }) + return main +} + +type testEvaluatorOpts struct { + // Config is required. + Config *stackconfig.Config + + // State is optional; testEvaluator will use an empty state if this is nil. + State *stackstate.State + + // InputVariableValues is optional and if set will provide the values + // for the root stack input variables. Any variable not defined here + // will evaluate to an unknown value of the configured type. + InputVariableValues map[string]cty.Value + + // ProviderFactories is optional and if set provides factory functions + // for provider types that the test can use. If not set then any attempt + // to use provider configurations will lead to some sort of error. + ProviderFactories ProviderFactories + + // TestOnlyGlobals is optional and if set makes it possible to use + // references like _test_only_global.name to refer to values from this + // map from anywhere in the entire stack configuration. + // + // This is intended as a kind of "test double" so that we can write more + // minimal unit tests that can avoid relying on too many language features + // all at once, so that hopefully future maintenance will not require + // making broad changes across many different tests at once, which would + // then risk inadvertently treating a regression as expected behavior. + // + // Configurations that refer to test-only globals are not valid for use + // outside of the test suite of this package. + TestOnlyGlobals map[string]cty.Value +} + +// SetTestOnlyGlobals assigns the test-only globals map for the receiving +// main evaluator. +// +// This may be used only from unit tests in this package and must be called +// before performing any other operations against the reciever. It's invalid +// to change the test-only globals after some evaluation has already been +// performed, because the evaluator expects its input to be immutable and +// caches values derived from that input, and there's no mechanism to +// invalidate those caches. +// +// This is intentionally defined in a _test.go file to prevent it from +// being used from non-test code, despite being named as if it's exported. +// It's named as if exported to help differentiate it from unexported +// methods that are intended only as internal API, since it's a public API +// from the perspective of a test caller even though it's not public to +// other callers. +func (m *Main) SetTestOnlyGlobals(t *testing.T, vals map[string]cty.Value) { + m.testOnlyGlobals = vals +} + +func assertFalse(t *testing.T, value bool) { + t.Helper() + if value { + t.Fatalf("expected false but got true") + } +} + +func assertTrue(t *testing.T, value bool) { + t.Helper() + if !value { + t.Fatalf("expected true but got false") + } +} + +func assertNoDiags(t *testing.T, diags tfdiags.Diagnostics) { + t.Helper() + if len(diags) != 0 { + t.Fatalf("unexpected diagnostics\n%s", diags.Err()) + } +} + +func assertMatchingDiag(t *testing.T, diags tfdiags.Diagnostics, check func(diag tfdiags.Diagnostic) bool) { + t.Helper() + for _, diag := range diags { + if check(diag) { + return + } + } + t.Fatalf("none of the diagnostics is the one we are expecting\n%s", diags.Err()) +} + +// inPromisingTask is a helper for conveniently running some code in the context +// of a [promising.MainTask], with automatic promise error checking. This +// makes it valid to call functions that expect to run only as part of a +// promising task, which is true of essentially every method in this package +// that takes a [context.Context] as its first argument. +// +// Specifically, if the function encounters any direct promise-related failures, +// such as failure to resolve a promise before returning, this function will +// halt the test with an error message. +func inPromisingTask(t *testing.T, f func(ctx context.Context, t *testing.T)) { + t.Helper() + + // We'll introduce an extra cancellable context here just to make + // sure everything descending from this task gets terminated promptly + // after the test is complete. + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(func() { + cancel() + }) + + _, err := promising.MainTask(ctx, func(ctx context.Context) (struct{}, error) { + t.Helper() + + f(ctx, t) + return struct{}{}, nil + }) + if err != nil { + // We could get here if the test produces any self-references or + // if it creates any promises that are left unresolved once it exits. + t.Fatalf("promise resolution failure: %s", err) + } +} + +// subtestInPromisingTask compiles [testing.T.Run] with [inPromisingTask] as +// a convenience wrapper for running an entire subtest as a [promising.MainTask]. +func subtestInPromisingTask(t *testing.T, name string, f func(ctx context.Context, t *testing.T)) { + t.Helper() + t.Run(name, func(t *testing.T) { + t.Helper() + inPromisingTask(t, f) + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/validating.go b/internal/stacks/stackruntime/internal/stackeval/validating.go new file mode 100644 index 0000000000..ee14e8f3c2 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/validating.go @@ -0,0 +1,39 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ValidateOpts struct { + ProviderFactories ProviderFactories + DependencyLocks depsfile.Locks +} + +// Validatable is implemented by objects that can participate in validation. +type Validatable interface { + // Validate returns diagnostics for any part of the reciever which + // has an invalid configuration. + // + // Validate implementations should be shallow, which is to say that + // in particular they _must not_ call the Validate method of other + // objects that implement Validatable, and should also think very + // hard about calling any validation-related methods of other objects, + // so as to avoid generating duplicate diagnostics via two different + // return paths. + // + // In general, assume that _all_ objects that implement Validatable will + // have their Validate methods called at some point during validation, and + // so it's unnecessary and harmful to try to handle validation on behalf of + // some other related object. + Validate(ctx context.Context) tfdiags.Diagnostics + + // Our general async validation helper relies on this to name its + // tracing span. + tracingNamer +} diff --git a/internal/stacks/stackruntime/internal/stackeval/validating_test.go b/internal/stacks/stackruntime/internal/stackeval/validating_test.go new file mode 100644 index 0000000000..d2863044b0 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/validating_test.go @@ -0,0 +1,144 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestValidate_modulesWithProviderConfigs(t *testing.T) { + // This test checks that we're correctly prohibiting inline provider + // configurations in Terraform modules used as stack components, which + // is forbidden because the stacks language is responsible for provider + // configurations. + // + // The underlying modules runtime isn't configured with any ability to + // instantiate provider plugins itself, so failing to prohibit this + // at the stacks language layer would just cause a lower-quality and + // more confusing error message to be emited by the modules runtime. + + cfg := testStackConfig(t, "validating", "modules_with_provider_configs") + main := NewForValidating(cfg, ValidateOpts{ + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("test"): func() (providers.Interface, error) { + // The test fails before it has to do any schema validation so + // we can safely return an empty mock provider here. + return &testing_provider.MockProvider{}, nil + }, + }, + }) + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + diags := main.ValidateAll(ctx) + if !diags.HasErrors() { + t.Fatalf("succeeded; want errors") + } + diags.Sort() + + // We'll use the ForRPC method just as a convenient way to discard + // the specific diagnostic object types, so that we can compare + // the objects without worrying about exactly which diagnostic + // implementation each is using. + gotDiags := diags.ForRPC() + + var wantDiags tfdiags.Diagnostics + // Configurations in the root module get a different detail message + // than those in descendant modules, because for descendants we don't + // assume that the author is empowered to make the module + // stacks-compatible, while for the root it's more likely to be + // directly intended for stacks use, at least for now while things are + // relatively early. (We could revisit this tradeoff later.) + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Inline provider configuration not allowed", + Detail: `A module used as a stack component must have all of its provider configurations passed from the stack configuration, using the "providers" argument within the component configuration block.`, + Subject: &hcl.Range{ + Filename: "https://testing.invalid/validating.tar.gz//modules_with_provider_configs/module-a/modules-with-provider-configs-a.tf", + Start: hcl.Pos{Line: 9, Column: 1, Byte: 104}, + End: hcl.Pos{Line: 9, Column: 16, Byte: 119}, + }, + }) + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Inline provider configuration not allowed", + Detail: "This module is not compatible with Terraform Stacks, because it declares an inline provider configuration.\n\nTo be used with stacks, this module must instead accept provider configurations from its caller.", + Subject: &hcl.Range{ + Filename: "https://testing.invalid/validating.tar.gz//modules_with_provider_configs/module-b/modules-with-provider-configs-b.tf", + Start: hcl.Pos{Line: 9, Column: 1, Byte: 104}, + End: hcl.Pos{Line: 9, Column: 16, Byte: 119}, + }, + }) + wantDiags = wantDiags.ForRPC() + + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } + }) +} + +func TestValidate_nestedModuleDiagnostics(t *testing.T) { + // This test verifies that our source bundle aware module loader correctly + // builds diagnostic source addresses for various kinds of nested modules. + // It covers both in-repo components and remote components, both having + // top-level and nested diagnostic errors. + + cfg := testStackConfig(t, "validating", "nested_module_diagnostics") + main := NewForValidating(cfg, ValidateOpts{}) + + inPromisingTask(t, func(ctx context.Context, t *testing.T) { + diags := main.ValidateAll(ctx) + if !diags.HasErrors() { + t.Fatalf("succeeded; want errors") + } + diags.Sort() + + // We'll use the ForRPC method just as a convenient way to discard + // the specific diagnostic object types, so that we can compare + // the objects without worrying about exactly which diagnostic + // implementation each is using. + gotDiags := diags.ForRPC() + + var wantDiags tfdiags.Diagnostics + // This configuration has the same errors repeated multiple times, + // varying only on filename (source address). + filenames := []string{ + "https://testing.invalid/invalid.tar.gz//invalid.tf", + "https://testing.invalid/invalid_child.tar.gz//child/invalid_child.tf", + "https://testing.invalid/invalid_child.tar.gz//child/invalid_child.tf", + "https://testing.invalid/invalid_grandchildren.tar.gz//first/child/invalid_child.tf", + "https://testing.invalid/invalid_grandchildren.tar.gz//second/child/invalid_child.tf", + "https://testing.invalid/validating.tar.gz//nested_module_diagnostics/invalid/invalid.tf", + "https://testing.invalid/validating.tar.gz//nested_module_diagnostics/invalid_child/child/invalid_child.tf", + } + for _, filename := range filenames { + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported block type", + Detail: `Blocks of type "invalid" are not expected here.`, + Subject: &hcl.Range{ + Filename: filename, + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 8, Byte: 7}, + }, + }) + } + wantDiags = wantDiags.ForRPC() + + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + for i, diag := range gotDiags { + t.Logf("diagnostic %d: %s", i, diag) + } + t.Errorf("wrong diagnostics\n%s", diff) + } + }) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/walk.go b/internal/stacks/stackruntime/internal/stackeval/walk.go new file mode 100644 index 0000000000..60d532f8c8 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/walk.go @@ -0,0 +1,142 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync" + + "github.com/hashicorp/terraform/internal/promising" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// walkState is a helper for codepaths that intend to visit various different +// interdependent objects and evaluate them all concurrently, waiting for any +// dependencies to be resolved and accumulating diagnostics along the way. +// +// Unlike in traditional Terraform Core, there isn't any special inversion of +// control technique to sequence the work, and instead callers are expected +// to use normal control flow in conjunction with the "promising" package's +// async tasks and promises to drive the evaluation forward. Therefore this +// only carries some minimal state that all of the asynchronous tasks need +// to share, with everything else dealt with using -- as much as possible -- +// "normal code". +type walkState struct { + wg sync.WaitGroup + + // handleDiags is called for each call to [walkState.AddDiags], with + // a set of diagnostics produced from that call's arguments. + // + // Callback functions can choose between either accumulating diagnostics + // into an overall set and finally returning it from getFinalDiags, or + // immediately dispatching the diagnostics to some other location and + // then returning nothing from the final call to getFinalDiags. + handleDiags func(tfdiags.Diagnostics) + + // getFinalDiags should return any diagnostics that were previously + // passed to handleDiags but not yet sent anywhere other than the + // internal state of a particular walkState object. + // + // For handleDiags implementations that immediately send all diagnostics + // somewhere out-of-hand, this should return nil to avoid those diagnostics + // getting duplicated by being returned through multiple paths. + getFinalDiags func() tfdiags.Diagnostics +} + +// newWalkState creates a new walkState object that's ready to be passed down +// to child functions that will start asynchronous work. +// +// The second return value is a completion function which should be retained +// by the top-level function that is orchestrating the walk and called only +// once all downstream work has had a chance to start, so that it can block +// for all of those tasks to complete. +// +// This default variant of newWalkState maintains an internal set of +// accumulated diagnostics and eventually returns it from the completion +// callback. Callers that need to handle diagnostics differently -- for example, +// by streaming them to callers via an out-of-band mechanism as they arrive -- +// can use newWalkStateCustomDiags to customize the diagnostics handling. +func newWalkState() (ws *walkState, complete func() tfdiags.Diagnostics) { + var diags syncDiagnostics + handleDiags := func(moreDiags tfdiags.Diagnostics) { + diags.Append(moreDiags) + } + getFinalDiags := func() tfdiags.Diagnostics { + return diags.Take() + } + return newWalkStateCustomDiags( + handleDiags, + getFinalDiags, + ) +} + +// newWalkStateCustomDiags is like [newWalkState] except it allows for the +// caller to provide custom callbacks for handling diagnostics. +// +// See the documentation of the fields of the same name in [walkState] +// above for what each of these callbacks represents and how it ought to +// behave. +func newWalkStateCustomDiags( + handleDiags func(tfdiags.Diagnostics), getFinalDiags func() tfdiags.Diagnostics, +) (ws *walkState, complete func() tfdiags.Diagnostics) { + ret := &walkState{ + handleDiags: handleDiags, + getFinalDiags: getFinalDiags, + } + return ret, func() tfdiags.Diagnostics { + ret.wg.Wait() + diags := ret.getFinalDiags() + diags.Sort() + return diags + } +} + +// AsyncTask runs the given callback function as an asynchronous task and +// ensures that a future call to the [walkState]'s completion function will +// block until it has returned. +// +// The given callback runs under a [promising.AsyncTask] call and so +// is allowed to interact with promises, but if it passes responsibility for +// a promise to another async task it must block until that promise has +// been resolved, so that the promise resolution cannot outlive the +// supervising walkState. +// +// It's safe to make nested calls to AsyncTask (inside another AsyncTask) as +// long as the child call returns, scheduling the child task, before the +// calling task completes. This constraint normally holds automatically when +// the child call is directly inside the parent's callback, but will require +// extra care if a task starts goroutines or non-walkState-supervised async +// tasks that might call this function. +func (ws *walkState) AsyncTask(ctx context.Context, impl func(ctx context.Context)) { + ws.wg.Add(1) + promising.AsyncTask(ctx, promising.NoPromises, func(ctx context.Context, none promising.PromiseContainer) { + impl(ctx) + ws.wg.Done() + }) +} + +// AddDiags converts each of the arguments to zero or more diagnostics and +// appends them to the internal log of diagnostics for the walk. +// +// This is safe to call from multiple concurrent tasks. The full set of +// diagnostics will be returned from the [walkState]'s completion function. +func (ws *walkState) AddDiags(new ...any) { + var diags tfdiags.Diagnostics + diags = diags.Append(new...) + ws.handleDiags(diags) +} + +type walkTaskContextKey struct{} + +// walkWithOutput combines a [walkState] with some other object that allows +// emitting output events to a caller, so that walk codepaths can conveniently +// pass these both together as a single argument. +type walkWithOutput[Output any] struct { + state *walkState + out Output +} + +func (w *walkWithOutput[Output]) AsyncTask(ctx context.Context, impl func(ctx context.Context)) { + w.state.AsyncTask(ctx, impl) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/walk_dynamic.go b/internal/stacks/stackruntime/internal/stackeval/walk_dynamic.go new file mode 100644 index 0000000000..b63233eecb --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/walk_dynamic.go @@ -0,0 +1,402 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" + "sync" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +// DynamicEvaler is implemented by types that participate in dynamic +// evaluation phases, which currently includes [PlanPhase] and [ApplyPhase]. +type DynamicEvaler interface { + Plannable + Applyable +} + +// walkDynamicObjects is a generic helper for visiting all of the "dynamic +// objects" in scope for a particular [Main] object. "Dynamic objects" +// essentially means the objects that are involved in the plan and apply +// operations, which includes instances of objects that can expand using +// "count" or "for_each" arguments. +// +// The walk value stays constant throughout the walk, being passed to +// all visited objects. Visits can happen concurrently, so any methods +// offered by Output must be concurrency-safe. +// +// The type parameter Object should be either [Plannable] or [ApplyChecker] +// depending on which walk this call is intending to drive. All dynamic +// objects must implement both of those interfaces, although for many +// object types the logic is equivalent across both. +func walkDynamicObjects[Output any]( + ctx context.Context, + walk *walkWithOutput[Output], + main *Main, + phase EvalPhase, + visit func(ctx context.Context, walk *walkWithOutput[Output], obj DynamicEvaler), +) { + walkDynamicObjectsInStack(ctx, walk, main.MainStack(), phase, visit) +} + +func walkDynamicObjectsInStack[Output any]( + ctx context.Context, + walk *walkWithOutput[Output], + stack *Stack, + phase EvalPhase, + visit func(ctx context.Context, walk *walkWithOutput[Output], obj DynamicEvaler), +) { + // We'll get the expansion of any child stack calls going first, so that + // we can explore downstream stacks concurrently with this one. Each + // stack call can represent zero or more child stacks that we'll analyze + // by recursive calls to this function. + for call := range stack.EmbeddedStackCalls() { + walkEmbeddedStack(ctx, walk, stack, call, phase, visit) + } + for call := range stack.Removed().stackCalls { + if stack.EmbeddedStackCall(call) != nil { + continue + } + walkEmbeddedStack(ctx, walk, stack, call, phase, visit) + } + + for component := range stack.Components() { + walkComponent(ctx, walk, stack, component, phase, visit) + } + for component := range stack.Removed().components { + if stack.Component(component) != nil { + continue // then we processed this as part of the component stage + } + walkComponent(ctx, walk, stack, component, phase, visit) + } + + // Now, we'll do the rest of the declarations in the stack. These are + // straightforward since we don't have to reconcile blocks that overlap. + + for _, provider := range stack.Providers() { + provider := provider // separate symbol per loop iteration + + visit(ctx, walk, provider) + + // We need to perform the instance expansion in an overall async + // task because it involves potentially evaluating a for_each expression, + // and that might depend on data from elsewhere in the same stack. + walk.AsyncTask(ctx, func(ctx context.Context) { + insts, unknown := provider.Instances(ctx, phase) + if unknown { + // We use the unconfigured client for unknown instances of a + // provider so there is nothing for us to do here. + return + } + + for _, inst := range insts { + visit(ctx, walk, inst) + } + }) + } + for _, variable := range stack.InputVariables() { + visit(ctx, walk, variable) + } + // TODO: Local values + for _, output := range stack.OutputValues() { + visit(ctx, walk, output) + } + + // Finally we'll also check the stack itself, to deal with any problems + // with the stack as a whole rather than individual declarations inside. + visit(ctx, walk, stack) +} + +// walkComponent just encapsulates the behaviour for visiting all the +// components in a stack. Components are more complicated than the other +// parts of a stack as they can be claimed by both component and removed blocks +// and both of these can evaluate to unknown. +// +// What we do here is we go through all the blocks within the configuration +// and visit them and mark the known instances as "claimed". We also want to +// find any blocks that evaluate to unknown. This block is then used in the +// final step where we'll search through any instances within the state that +// haven't been claimed and assign them to an unknown block, if one was found. +func walkComponent[Output any]( + ctx context.Context, + walk *walkWithOutput[Output], + stack *Stack, + addr stackaddrs.Component, + phase EvalPhase, + visit func(ctx context.Context, walk *walkWithOutput[Output], obj DynamicEvaler)) { + + var unknownComponentBlock *Component + var unknownRemovedComponentBlock *RemovedComponent + + var wg sync.WaitGroup + var mutex sync.Mutex + + claimedInstances := collections.NewSet[stackaddrs.ComponentInstance]() + + component := stack.Component(addr) + if component != nil { + visit(ctx, walk, component) // first, just visit the component directly + + // then visit the component instances. we must do this in an async task as + // we evaulate the for_each valuate within the instances call. + + wg.Add(1) + walk.AsyncTask(ctx, func(ctx context.Context) { + defer wg.Done() + + insts, unknown := component.Instances(ctx, phase) + if unknown { + unknownComponentBlock = component + return + } + + for key, inst := range insts { + instAddr := stackaddrs.ComponentInstance{ + Component: addr, + Key: key, + } + + mutex.Lock() + if claimedInstances.Has(instAddr) { + // this will be picked up as an error elsewhere, but + // two blocks have claimed this instance so we'll just + // allow whichever got their first to claim it and we'll + // just skip it here. + mutex.Unlock() + continue + } + claimedInstances.Add(instAddr) + mutex.Unlock() + + visit(ctx, walk, inst) + } + }) + } + + for _, block := range stack.Removed().components[addr] { + visit(ctx, walk, block) // first, just visit the removed block directly + + wg.Add(1) + walk.AsyncTask(ctx, func(ctx context.Context) { + defer wg.Done() + + insts, unknown := block.InstancesFor(ctx, stack.addr, phase) + if unknown { + mutex.Lock() + // we might have multiple removed blocks that evaluate to + // unknown. if so, we'll just pick a random one that actually + // gets assigned to handle any unclaimed instances. + unknownRemovedComponentBlock = block + mutex.Unlock() + + return + } + + for _, inst := range insts { + mutex.Lock() + if claimedInstances.Has(inst.from.Item) { + // this will be picked up as an error elsewhere, but + // two blocks have claimed this instance so we'll just + // allow whichever got their first to claim it and we'll + // just skip it here. + mutex.Unlock() + continue + } + claimedInstances.Add(inst.from.Item) + mutex.Unlock() + + visit(ctx, walk, inst) + } + }) + } + + // finally, we're going to look at the instances that are in state and + // hopefully assign any unclaimed ones to an unknown block. + + walk.AsyncTask(ctx, func(ctx context.Context) { + wg.Wait() // wait for all the other tasks to finish + + // if we have an unknown component block we want to make sure the + // output says something about it. This means if we have unclaimed + // instances then the component block will claim those, but if no + // unclaimed instances exist we'll create a partial unknown component + // instance that means the component block will appear in the plan + // somewhere. This starts as true if there is no unknown component block + // to make us not do anything by default for this case. + unknownComponentBlockClaimedSomething := unknownComponentBlock == nil + + knownInstances := stack.KnownComponentInstances(addr, phase) + for inst := range knownInstances { + if claimedInstances.Has(inst) { + // don't need the mutex any more since this will be fully + // initialised when all the wait groups are finished. + continue + } + + // this is unclaimed, so we'll see if + + if unknownComponentBlock != nil { + // then we have a component block to claim it so we make an + // instance dynamically and off we go + + unknownComponentBlockClaimedSomething = true + inst := unknownComponentBlock.UnknownInstance(ctx, inst.Key, phase) + visit(ctx, walk, inst) + continue + } + + if unknownRemovedComponentBlock != nil { + // then we didn't have an unknown component block, but we do + // have an unknown removed component block to claim it + + from := stackaddrs.AbsComponentInstance{ + Stack: stack.addr, + Item: inst, + } + inst := unknownRemovedComponentBlock.UnknownInstance(ctx, from, phase) + visit(ctx, walk, inst) + + continue + } + + // then nothing claimed it - this is an error. We don't actually + // raise this as an error here though (it will be caught elsewhere). + + } + + if !unknownComponentBlockClaimedSomething { + // then we want to include the partial unknown component instance + inst := unknownComponentBlock.UnknownInstance(ctx, addrs.WildcardKey, phase) + visit(ctx, walk, inst) + } + }) +} + +// walkEmbeddedStack follows the pattern of walkComponent but for embedded +// stack calls rather than components. +func walkEmbeddedStack[Output any]( + ctx context.Context, + walk *walkWithOutput[Output], + stack *Stack, + addr stackaddrs.StackCall, + phase EvalPhase, + visit func(ctx context.Context, walk *walkWithOutput[Output], obj DynamicEvaler)) { + + var unknownStackCall *StackCall + var unknownRemovedStackCall *RemovedStackCall + + var wg sync.WaitGroup + var mutex sync.Mutex + + claimedInstances := collections.NewSet[stackaddrs.StackInstance]() + + embeddedStack := stack.EmbeddedStackCall(addr) + if embeddedStack != nil { + visit(ctx, walk, embeddedStack) + + wg.Add(1) + walk.AsyncTask(ctx, func(ctx context.Context) { + defer wg.Done() + + insts, unknown := embeddedStack.Instances(ctx, phase) + if unknown { + unknownStackCall = embeddedStack + return + } + + for _, inst := range insts { + instAddr := inst.CalledStackAddr() + + mutex.Lock() + if claimedInstances.Has(instAddr) { + mutex.Unlock() + continue + } + claimedInstances.Add(instAddr) + mutex.Unlock() + + visit(ctx, walk, inst) + childStack := inst.Stack(ctx, phase) + walkDynamicObjectsInStack(ctx, walk, childStack, phase, visit) + } + }) + } + + for _, block := range stack.Removed().stackCalls[addr] { + visit(ctx, walk, block) + + wg.Add(1) + walk.AsyncTask(ctx, func(ctx context.Context) { + defer wg.Done() + + insts, unknown := block.InstancesFor(ctx, stack.addr, phase) + if unknown { + mutex.Lock() + unknownRemovedStackCall = block + mutex.Unlock() + + return + } + + for _, inst := range insts { + mutex.Lock() + if claimedInstances.Has(inst.from) { + mutex.Unlock() + continue + } + claimedInstances.Add(inst.from) + mutex.Unlock() + + visit(ctx, walk, inst) + childStack := inst.Stack(ctx, phase) + walkDynamicObjectsInStack(ctx, walk, childStack, phase, visit) + } + }) + } + + walk.AsyncTask(ctx, func(ctx context.Context) { + wg.Wait() + + unknownStackCallClaimedSomething := unknownStackCall == nil + + knownStacks := stack.KnownEmbeddedStacks(addr, phase) + for inst := range knownStacks { + if claimedInstances.Has(inst) { + continue + } + + if unknownStackCall != nil { + unknownStackCallClaimedSomething = true + inst := unknownStackCall.UnknownInstance(ctx, inst[len(inst)-1].Key, phase) + visit(ctx, walk, inst) + childStack := inst.Stack(ctx, phase) + walkDynamicObjectsInStack(ctx, walk, childStack, phase, visit) + + continue + } + + if unknownRemovedStackCall != nil { + inst := unknownRemovedStackCall.UnknownInstance(ctx, inst, phase) + visit(ctx, walk, inst) + childStack := inst.Stack(ctx, phase) + walkDynamicObjectsInStack(ctx, walk, childStack, phase, visit) + + continue + } + } + + if !unknownStackCallClaimedSomething { + // then we want to include the partial unknown component instance + inst := unknownStackCall.UnknownInstance(ctx, addrs.WildcardKey, phase) + visit(ctx, walk, inst) + childStack := inst.Stack(ctx, phase) + walkDynamicObjectsInStack(ctx, walk, childStack, phase, visit) + } + + }) + +} diff --git a/internal/stacks/stackruntime/internal/stackeval/walk_static.go b/internal/stacks/stackruntime/internal/stackeval/walk_static.go new file mode 100644 index 0000000000..1ee00f7bf7 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/walk_static.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackeval + +import ( + "context" +) + +// StaticEvaler is implemented by types that participate in static +// evaluation phases, which currently includes [ValidatePhase] and [PlanPhase]. +type StaticEvaler interface { + Validatable + Plannable +} + +// walkDynamicObjects is a generic helper for visiting all of the "static +// objects" in scope for a particular [Main] object. "Static objects" +// essentially means the objects that are involved in the validation +// operation, which typically includes objects representing static +// configuration elements that haven't yet been expanded into their +// dynamic counterparts. +// +// The walk value stays constant throughout the walk, being passed to +// all visited objects. Visits can happen concurrently, so any methods +// offered by Output must be concurrency-safe. +// +// The Object type parameter should either be Validatable or Plannable +// depending on which of the two relevant evaluation phases this function +// is supposed to be driving. +func walkStaticObjects[Output any]( + ctx context.Context, + walk *walkWithOutput[Output], + main *Main, + visit func(ctx context.Context, walk *walkWithOutput[Output], obj StaticEvaler), +) { + walkStaticObjectsInStackConfig(ctx, walk, main.MainStackConfig(), visit) +} + +func walkStaticObjectsInStackConfig[Output any]( + ctx context.Context, + walk *walkWithOutput[Output], + stackConfig *StackConfig, + visit func(ctx context.Context, walk *walkWithOutput[Output], obj StaticEvaler), +) { + for _, obj := range stackConfig.InputVariables() { + visit(ctx, walk, obj) + } + + for _, obj := range stackConfig.OutputValues() { + visit(ctx, walk, obj) + } + + // TODO: All of the other static object types + for _, obj := range stackConfig.LocalValues() { + visit(ctx, walk, obj) + } + + for _, obj := range stackConfig.Providers() { + visit(ctx, walk, obj) + } + + for _, obj := range stackConfig.Components() { + visit(ctx, walk, obj) + } + + for _, objs := range stackConfig.RemovedComponents().All() { + for _, obj := range objs { + visit(ctx, walk, obj) + } + } + + for _, obj := range stackConfig.StackCalls() { + visit(ctx, walk, obj) + } + + for _, objs := range stackConfig.RemovedStackCalls().All() { + for _, obj := range objs { + visit(ctx, walk, obj) + } + } + + for _, childCfg := range stackConfig.ChildConfigs() { + walkStaticObjectsInStackConfig(ctx, walk, childCfg, visit) + } +} diff --git a/internal/stacks/stackruntime/plan.go b/internal/stacks/stackruntime/plan.go new file mode 100644 index 0000000000..2481fbd527 --- /dev/null +++ b/internal/stacks/stackruntime/plan.go @@ -0,0 +1,168 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "time" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Plan evaluates the given configuration to calculate a desired state, +// updates the given prior state to match the current state of real +// infrastructure, and then compares the desired state with the updated prior +// state to produce a proposed set of changes that should reduce the number +// of differences between the two. +// +// Plan does not return a result directly because it emits results in a +// streaming fashion using channels provided in the given [PlanResponse]. +// +// Callers must not modify any values reachable directly or indirectly +// through resp after passing it to this function, aside from the implicit +// modifications to the internal state of channels caused by reading them. +func Plan(ctx context.Context, req *PlanRequest, resp *PlanResponse) { + // Whatever return path we take, we must close our channels to allow + // a caller to see that the operation is complete. + defer func() { + close(resp.Diagnostics) + close(resp.PlannedChanges) // MUST be the last channel to close + }() + + var errored bool + + planTimestamp := time.Now().UTC() + if req.ForcePlanTimestamp != nil { + planTimestamp = *req.ForcePlanTimestamp + } + + main := stackeval.NewForPlanning(req.Config, req.PrevState, stackeval.PlanOpts{ + PlanningMode: req.PlanMode, + InputVariableValues: req.InputValues, + ProviderFactories: req.ProviderFactories, + DependencyLocks: req.DependencyLocks, + + PlanTimestamp: planTimestamp, + }) + main.AllowLanguageExperiments(req.ExperimentsAllowed) + main.PlanAll(ctx, stackeval.PlanOutput{ + AnnouncePlannedChange: func(ctx context.Context, change stackplan.PlannedChange) { + resp.PlannedChanges <- change + }, + AnnounceDiagnostics: func(ctx context.Context, diags tfdiags.Diagnostics) { + for _, diag := range diags { + if diag.Severity() == tfdiags.Error { + errored = true + } + resp.Diagnostics <- diag + } + }, + }) + cleanupDiags := main.DoCleanup(ctx) + for _, diag := range cleanupDiags { + // cleanup diagnostics don't stop a plan from being applyable, because + // the cleanup process should not affect the content of and validity + // of the plan. This should only include transient operational errors + // such as failing to terminate a provider plugin. + resp.Diagnostics <- diag + } + + // An overall stack plan is applyable if it has no error diagnostics. + resp.Applyable = !errored + + // Before we return we'll emit one more special planned change just to + // remember in the raw plan sequence whether we considered this plan to be + // applyable, so we don't need to rely on the caller to remember + // resp.Applyable separately. + resp.PlannedChanges <- &stackplan.PlannedChangeApplyable{ + Applyable: resp.Applyable, + } +} + +// PlanRequest represents the inputs to a [Plan] call. +type PlanRequest struct { + PlanMode plans.Mode + + Config *stackconfig.Config + PrevState *stackstate.State + + InputValues map[stackaddrs.InputVariable]ExternalInputValue + ProviderFactories map[addrs.Provider]providers.Factory + DependencyLocks depsfile.Locks + + // ForcePlanTimestamp, if not nil, will force the plantimestamp function + // to return the given value instead of whatever real time the plan + // operation started. This is for testing purposes only. + ForcePlanTimestamp *time.Time + + ExperimentsAllowed bool +} + +// PlanResponse is used by [Plan] to describe the results of planning. +// +// [Plan] produces streaming results throughout its execution, and so it +// communicates with the caller by writing to provided channels during its work +// and then modifying other fields in this structure before returning. Callers +// MUST NOT access any fields of PlanResponse until the PlannedChanges +// channel has been closed to signal the completion of the planning process. +type PlanResponse struct { + // [Plan] will set this field to true if the plan ran to completion and + // is valid enough to be applied, or set this to false if not. + // + // The initial value of this field is ignored; there's no reason to set + // it to anything other than the zero value. + Applyable bool + + // PlannedChanges is the channel that will be sent each individual + // planned change, in no predictable order, during the planning + // operation. + // + // Callers MUST provide a non-nil channel and read from it from + // another Goroutine throughout the plan operation, or planning + // progress will be blocked. Callers that read slowly should provide + // a buffered channel to reduce the backpressure they exert on the + // planning process. + // + // The plan operation will close this channel before it returns + // PlannedChanges is guaranteed to be the last channel to close + // (i.e. after Diagnostics is closed) so callers can use the close + // signal of this channel alone to mark that the plan process is + // over, but if Diagnostics is a buffered channel they must take + // care to deplete its buffer afterwards to avoid losing diagnostics + // delivered near the end of the planning process. + PlannedChanges chan<- stackplan.PlannedChange + + // Diagnostics is the channel that will be sent any diagnostics + // that arise during the planning process, in no particular order. + // + // In particular note that there's no guarantee that the diagnostics + // for planning a particular object will be emitted in close proximity + // to a PlannedChanges write for that same object. Diagnostics and + // planned changes are totally decoupled, since diagnostics might be + // collected up and emitted later as a large batch if the runtime + // needs to perform aggregate operations such as deduplication on + // the diagnostics before exposing them. + // + // Callers MUST provide a non-nil channel and read from it from + // another Goroutine throughout the plan operation, or planning + // progress will be blocked. Callers that read slowly should provide + // a buffered channel to reduce the backpressure they exert on the + // planning process. + // + // The plan operation will close this channel before it returns, but + // callers should use the close event of PlannedChanges as the definitive + // signal that planning is complete. + Diagnostics chan<- tfdiags.Diagnostic +} + +type ExternalInputValue = stackeval.ExternalInputValue diff --git a/internal/stacks/stackruntime/plan_refresh_test.go b/internal/stacks/stackruntime/plan_refresh_test.go new file mode 100644 index 0000000000..d77185632f --- /dev/null +++ b/internal/stacks/stackruntime/plan_refresh_test.go @@ -0,0 +1,317 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/version" +) + +func TestRefreshPlan(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + tcs := map[string]struct { + path string + state *stackstate.State + store *stacks_testing_provider.ResourceStore + cycle TestCycle + }{ + "simple-valid": { + path: filepath.Join("with-single-input", "valid"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "old", + "value": "old", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("old", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("old"), + "value": cty.StringVal("new"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "id": cty.StringVal("old"), + "input": cty.StringVal("old"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Read, + Mode: plans.RefreshOnlyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("old")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("old")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "old", + "value": "new", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("id"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("old"), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("input"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("old"), + }, + }, + }, + }, + "removed-component": { + path: filepath.Join("with-single-input", "removed-component"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self")). + AddInputVariable("id", cty.StringVal("old")). + AddInputVariable("input", cty.StringVal("old"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "old", + "value": "old", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("old", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("old"), + "value": cty.StringVal("new"), + })). + Build(), + cycle: TestCycle{ + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Read, + Mode: plans.RefreshOnlyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("old")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("old")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "old", + "value": "new", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + }, + }, + "removed-stack": { + path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"old\"].component.self")). + AddInputVariable("id", cty.StringVal("old")). + AddInputVariable("input", cty.StringVal("old"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.simple[\"old\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "old", + "value": "old", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("old", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("old"), + "value": cty.StringVal("new"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "removed": cty.MapVal(map[string]cty.Value{ + "old": cty.StringVal("old"), + }), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.simple[\"old\"].component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Read, + Mode: plans.RefreshOnlyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("old")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("old")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"old\"].component.self.testing_resource.data"), + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "old", + "value": "new", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapVal(map[string]cty.Value{ + "old": cty.StringVal("old"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed-direct"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + cycle := tc.cycle + cycle.planMode = plans.RefreshOnlyMode // set this for all the tests here + + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + store := tc.store + if store == nil { + store = stacks_testing_provider.NewResourceStore() + } + + testContext := TestContext{ + timestamp: &fakePlanTimestamp, + config: loadMainBundleConfigForTest(t, tc.path), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, store), nil + }, + }, + dependencyLocks: *lock, + } + + testContext.Plan(t, ctx, tc.state, cycle) + }) + } +} diff --git a/internal/stacks/stackruntime/plan_test.go b/internal/stacks/stackruntime/plan_test.go new file mode 100644 index 0000000000..b7fa858a5f --- /dev/null +++ b/internal/stacks/stackruntime/plan_test.go @@ -0,0 +1,6334 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "encoding/json" + "fmt" + "path" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks" + + "github.com/hashicorp/terraform/internal/addrs" + terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + default_testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/stacks/stackstate" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/terraform/version" +) + +// TestPlan_valid runs the same set of configurations as TestValidate_valid. +// +// Plan should execute the same set of validations as validate, so we expect +// all of the following to be valid for both plan and validate. +// +// We also want to make sure the static and dynamic evaluations are not +// returning duplicate / conflicting diagnostics. This test will tell us if +// either plan or validate is reporting diagnostics the others are missing. +func TestPlan_valid(t *testing.T) { + for name, tc := range validConfigurations { + t.Run(name, func(t *testing.T) { + if tc.skip { + // We've added this test before the implementation was ready. + t.SkipNow() + } + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + lock.SetProvider( + addrs.NewDefaultProvider("other"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + testContext := TestContext{ + config: loadMainBundleConfigForTest(t, name), + providers: map[addrs.Provider]providers.Factory{ + // We support both hashicorp/testing and + // terraform.io/builtin/testing as providers. This lets us + // test the provider aliasing feature. Both providers + // support the same set of resources and data sources. + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + // We also support an "other" provider out of the box to + // test the provider aliasing feature. + addrs.NewDefaultProvider("other"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + dependencyLocks: *lock, + timestamp: &fakePlanTimestamp, + } + + cycle := TestCycle{ + planInputs: tc.planInputVars, + wantPlannedChanges: nil, // don't care about the planned changes in this test. + wantPlannedDiags: nil, // should return no diagnostics. + } + testContext.Plan(t, ctx, nil, cycle) + }) + } +} + +// TestPlan_invalid runs the same set of configurations as TestValidate_invalid. +// +// Plan should execute the same set of validations as validate, so we expect +// all of the following to be invalid for both plan and validate. +// +// We also want to make sure the static and dynamic evaluations are not +// returning duplicate / conflicting diagnostics. This test will tell us if +// either plan or validate is reporting diagnostics the others are missing. +// +// The dynamic validation that happens during the plan *might* introduce +// additional diagnostics that are not present in the static validation. These +// should be added manually into this function. +func TestPlan_invalid(t *testing.T) { + for name, tc := range invalidConfigurations { + t.Run(name, func(t *testing.T) { + if tc.skip { + // We've added this test before the implementation was ready. + t.SkipNow() + } + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + testContext := TestContext{ + config: loadMainBundleConfigForTest(t, name), + providers: map[addrs.Provider]providers.Factory{ + // We support both hashicorp/testing and + // terraform.io/builtin/testing as providers. This lets us + // test the provider aliasing feature. Both providers + // support the same set of resources and data sources. + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + dependencyLocks: *lock, + timestamp: &fakePlanTimestamp, + } + + cycle := TestCycle{ + planInputs: tc.planInputVars, + wantPlannedChanges: nil, // don't care about the planned changes in this test. + wantPlannedDiags: tc.diags(), + } + testContext.Plan(t, ctx, nil, cycle) + }) + } +} + +// TestPlan uses a generic framework for running plan integration tests +// against Stacks. Generally, new tests should be added into this function +// rather than copying the large amount of duplicate code from the other +// tests in this file. +// +// If you are editing other tests in this file, please consider moving them +// into this test function so they can reuse the shared setup and boilerplate +// code managing the boring parts of the test. +func TestPlan(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + tcs := map[string]struct { + path string + state *stackstate.State + store *stacks_testing_provider.ResourceStore + cycle TestCycle + }{ + "empty-destroy-with-data-source": { + path: path.Join("with-data-source", "dependent"), + cycle: TestCycle{ + planMode: plans.DestroyMode, + planInputs: map[string]cty.Value{ + "id": cty.StringVal("foo"), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.data"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Delete, + Mode: plans.DestroyMode, + RequiredComponents: collections.NewSet(mustAbsComponent("component.self")), + PlannedOutputValues: make(map[string]cty.Value), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedOutputValues: map[string]cty.Value{ + "id": cty.StringVal("foo"), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("id"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + DeleteOnApply: true, + }, + }, + }, + }, + "deferred-provider-with-write-only": { + path: "with-write-only-attribute", + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "providers": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.main"), + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "datasource_id": mustPlanDynamicValueDynamicType(cty.StringVal("datasource")), + "resource_id": mustPlanDynamicValueDynamicType(cty.StringVal("resource")), + "write_only_input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "datasource_id": nil, + "resource_id": nil, + "write_only_input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.data.testing_write_only_data_source.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("data.testing_write_only_data_source.data"), + PrevRunAddr: mustAbsResourceInstance("data.testing_write_only_data_source.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Read, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("datasource"), + "value": cty.UnknownVal(cty.String), + "write_only": cty.NullVal(cty.String), + })), + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("write_only"), + }, + }, + ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.WriteOnlyDataSourceSchema, + }, + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.testing_write_only_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_write_only_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_write_only_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("resource"), + "value": cty.UnknownVal(cty.String), + "write_only": cty.NullVal(cty.String), + })), + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("write_only"), + }, + }, + }, + PriorStateSrc: nil, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.WriteOnlyResourceSchema, + }, + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("providers"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + }, + }, + }, + "deferred-provider-with-data-sources": { + path: path.Join("with-data-source", "deferred-provider-for-each"), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("data_known", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("data_known"), + "value": cty.StringVal("known"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "providers": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.const"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("data_known")), + "resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_known")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "resource": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"), + ChangeSrc: nil, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "data_known", + "value": "known", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingDataSourceSchema, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("resource_known"), + "value": cty.StringVal("known"), + })), + }, + }, + PriorStateSrc: nil, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.main[*]"), + PlanApplyable: false, // only deferred changes + PlanComplete: false, // deferred + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("data_unknown")), + "resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_unknown")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "resource": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "main", + }, + Key: addrs.WildcardKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: mustAbsResourceInstance("data.testing_data_source.data"), + }, + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("data.testing_data_source.data"), + PrevRunAddr: mustAbsResourceInstance("data.testing_data_source.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Read, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("data_unknown"), + "value": cty.UnknownVal(cty.String), + })), + }, + ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, + }, + PriorStateSrc: nil, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingDataSourceSchema, + }, + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "main", + }, + Key: addrs.WildcardKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: mustAbsResourceInstance("testing_resource.data"), + }, + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("resource_unknown"), + "value": cty.UnknownVal(cty.String), + })), + }, + }, + PriorStateSrc: nil, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: mustStackInputVariable("providers"), + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + }, + }, + }, + "removed embedded component duplicate": { + path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "for_each_input": cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + "simple_input": cty.MapVal(map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }), + "for_each_removed": cty.SetVal([]cty.Value{ + cty.StringVal("foo"), + }), + "simple_removed": cty.SetVal([]cty.Value{ + cty.StringVal("foo"), + }), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove component instance", + Detail: "The component instance stack.for_each.component.self[\"foo\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/for-each-component/for-each-component.tfstack.hcl:15,1-17.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfstack.hcl", + Start: hcl.Pos{Line: 38, Column: 1, Byte: 505}, + End: hcl.Pos{Line: 38, Column: 8, Byte: 512}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove component instance", + Detail: "The component instance stack.simple[\"foo\"].component.self is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/valid/valid.tfstack.hcl:19,1-17.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfstack.hcl", + Start: hcl.Pos{Line: 60, Column: 1, Byte: 811}, + End: hcl.Pos{Line: 60, Column: 8, Byte: 818}, + }, + }) + return diags + }), + }, + }, + "deferred-embedded-stack-update": { + path: path.Join("with-single-input", "deferred-embedded-stack-for-each"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a[\"deferred\"].component.self")). + AddInputVariable("id", cty.StringVal("deferred")). + AddInputVariable("input", cty.StringVal("deferred"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.a[\"deferred\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "deferred", + "value": "deferred", + }), + })). + AddInput("stacks", cty.MapVal(map[string]cty.Value{ + "deferred": cty.StringVal("deferred"), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("deferred", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("deferred"), + "value": cty.StringVal("deferred"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "stacks": cty.UnknownVal(cty.Map(cty.String)), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("a", addrs.StringKey("deferred")), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + PlanApplyable: false, // Everything is deferred, so nothing to apply. + PlanComplete: false, + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("deferred")), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedOutputValues: map[string]cty.Value{}, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("a", addrs.StringKey("deferred")), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Update, + Before: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("deferred"), + "value": cty.StringVal("deferred"), + }), stacks_testing_provider.TestingResourceSchema.Body), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("deferred"), + "value": cty.UnknownVal(cty.String), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "deferred", + "value": "deferred", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "stacks"}, + Action: plans.Update, + Before: cty.MapVal(map[string]cty.Value{ + "deferred": cty.StringVal("deferred"), + }), + After: cty.UnknownVal(cty.Map(cty.String)), + }, + }, + }, + }, + "deferred-embedded-stack-create": { + path: path.Join("with-single-input", "deferred-embedded-stack-for-each"), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "stacks": cty.UnknownVal(cty.Map(cty.String)), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + PlanApplyable: false, // Everything is deferred, so nothing to apply. + PlanComplete: false, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedOutputValues: map[string]cty.Value{}, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.UnknownVal(cty.String), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "stacks"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Map(cty.String)), + }, + }, + }, + }, + "deferred-embedded-stack-and-component-for-each": { + path: path.Join("with-single-input", "deferred-embedded-stack-and-component-for-each"), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "stacks": cty.UnknownVal(cty.Map(cty.Set(cty.String))), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + Key: addrs.WildcardKey, + }, + ), + PlanApplyable: false, // Everything is deferred, so nothing to apply. + PlanComplete: false, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedOutputValues: map[string]cty.Value{}, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + Key: addrs.WildcardKey, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.UnknownVal(cty.String), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "stacks"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Map(cty.Set(cty.String))), + }, + }, + }, + }, + "removed block targets stack not in configuration or state": { + path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "input": cty.MapValEmpty(cty.String), + "removed": cty.MapVal(map[string]cty.Value{ + "component": cty.StringVal("component"), + }), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapVal(map[string]cty.Value{ + "component": cty.StringVal("component"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed-direct"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + }, + }, + }, + "embedded stack in state but not in configuration": { + path: filepath.Join("with-single-input", "valid"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.child.component.self"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.child.component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "leftover", + "value": "leftover", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("leftover", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("leftover"), + "value": cty.StringVal("leftover"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "input": cty.StringVal("input"), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unclaimed component instance", + Detail: "The component instance stack.child.component.self is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.", + }) + }), + }, + }, + "removed and stack block target the same stack": { + path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "input": cty.MapVal(map[string]cty.Value{ + "component": cty.StringVal("component"), + }), + "removed": cty.MapVal(map[string]cty.Value{ + "component": cty.StringVal("component"), + }), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove stack instance", + Detail: "The stack instance stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl:25,1-15.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl", + Start: hcl.Pos{Line: 36, Column: 1, Byte: 441}, + End: hcl.Pos{Line: 36, Column: 8, Byte: 448}, + }, + }) + }), + }, + }, + "removed targets stack block in embedded stack that exists": { + path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "input": cty.MapVal(map[string]cty.Value{ + "component": cty.MapVal(map[string]cty.Value{ + "component": cty.StringVal("component"), + }), + }), + "removed": cty.MapVal(map[string]cty.Value{ + "component": cty.MapVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "input": cty.StringVal("component"), + }), + }), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove stack instance", + Detail: "The stack instance stack.embedded[\"component\"].stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl:25,1-15.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfstack.hcl", + Start: hcl.Pos{Line: 28, Column: 1, Byte: 360}, + End: hcl.Pos{Line: 28, Column: 8, Byte: 367}, + }, + }) + }), + }, + }, + "removed block targets component inside removed stack": { + path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")). + AddInputVariable("id", cty.StringVal("component")). + AddInputVariable("input", cty.StringVal("component"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "component", + "value": "component", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("component", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "value": cty.StringVal("component"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "removed": cty.MapVal(map[string]cty.Value{ + "component": cty.StringVal("component"), + }), + "removed-direct": cty.SetVal([]cty.Value{ + cty.StringVal("component"), + }), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Cannot remove component instance", + Detail: "The component instance stack.simple[\"component\"].component.self is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/valid/valid.tfstack.hcl:19,1-17.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl", + Start: hcl.Pos{Line: 51, Column: 1, Byte: 708}, + End: hcl.Pos{Line: 51, Column: 8, Byte: 715}, + }, + }) + }), + }, + }, + "removed block targets orphaned component": { + path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")). + AddInputVariable("id", cty.StringVal("component")). + AddInputVariable("input", cty.StringVal("component"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "component", + "value": "component", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("component", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "value": cty.StringVal("component"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "simple_input": cty.MapValEmpty(cty.String), + "simple_removed": cty.SetVal([]cty.Value{ + cty.StringVal("component"), + }), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid removed block", + Detail: "The component instance stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfstack.hcl", + Start: hcl.Pos{Line: 60, Column: 1, Byte: 811}, + End: hcl.Pos{Line: 60, Column: 8, Byte: 818}, + }, + }) + }), + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: false, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "for_each_input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "for_each_removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "simple_input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "simple_removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("component"), + }), + }, + }, + }, + }, + "removed block targets orphaned stack": { + path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded[\"component\"].stack.simple[\"component\"].component.self")). + AddInputVariable("id", cty.StringVal("component")). + AddInputVariable("input", cty.StringVal("component"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.embedded[\"component\"].stack.simple[\"component\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "component", + "value": "component", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("component", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "value": cty.StringVal("component"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "input": cty.MapValEmpty(cty.Map(cty.String)), + "removed": cty.MapVal(map[string]cty.Value{ + "component": cty.MapVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "input": cty.StringVal("component"), + }), + }), + }, + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid removed block", + Detail: "The component instance stack.embedded[\"component\"].stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfstack.hcl", + Start: hcl.Pos{Line: 28, Column: 1, Byte: 360}, + End: hcl.Pos{Line: 28, Column: 8, Byte: 367}, + }, + }) + }), + }, + }, + "removed block targets orphaned component without config definition": { + path: filepath.Join("with-single-input", "orphaned-component"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded.component.self")). + AddInputVariable("id", cty.StringVal("component")). + AddInputVariable("input", cty.StringVal("component"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.embedded.component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "component", + "value": "component", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("component", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "value": cty.StringVal("component"), + })). + Build(), + cycle: TestCycle{ + wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid removed block", + Detail: "The component instance stack.embedded.component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/orphaned-component/orphaned-component.tfstack.hcl", + Start: hcl.Pos{Line: 10, Column: 1, Byte: 131}, + End: hcl.Pos{Line: 10, Column: 8, Byte: 138}, + }, + }) + }), + }, + }, + "unknown embedded stack with internal component targeted by concrete removed block": { + path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")). + AddInputVariable("id", cty.StringVal("component")). + AddInputVariable("input", cty.StringVal("component"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "component", + "value": "component", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("component", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "value": cty.StringVal("component"), + })). + Build(), + cycle: TestCycle{ + planInputs: map[string]cty.Value{ + "removed": cty.UnknownVal(cty.Map(cty.String)), + }, + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.simple[\"component\"].component.self"), + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("component"), + "value": cty.StringVal("component"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "component", + "value": "component", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonDeferredPrereq, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.MapValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Map(cty.String)), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed-direct"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + }, + }, + }, + "remove partial stack": { + path: filepath.Join("with-single-input", "multiple-components", "removed"), + state: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.multiple.component.one")). + AddInputVariable("id", cty.StringVal("one")). + AddInputVariable("input", cty.StringVal("one"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "one", + "value": "one", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("one", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("one"), + "value": cty.StringVal("one"), + })). + Build(), + cycle: TestCycle{ + wantPlannedChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.multiple.component.one"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("one")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("one")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("one"), + "value": cty.StringVal("one"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "one", + "value": "one", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.multiple.component.two"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedOutputValues: make(map[string]cty.Value), + PlanTimestamp: fakePlanTimestamp, + }, + }, + }, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + store := tc.store + if store == nil { + store = stacks_testing_provider.NewResourceStore() + } + + testContext := TestContext{ + timestamp: &fakePlanTimestamp, + config: loadMainBundleConfigForTest(t, tc.path), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, store), nil + }, + }, + dependencyLocks: *lock, + } + + testContext.Plan(t, ctx, tc.state, tc.cycle) + }) + } +} + +func TestPlanWithMissingInputVariable(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "plan-undeclared-variable-in-component") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + _, gotDiags := collectPlanOutput(changesCh, diagsCh) + + // We'll normalize the diagnostics to be of consistent underlying type + // using ForRPC, so that we can easily diff them; we don't actually care + // about which underlying implementation is in use. + gotDiags = gotDiags.ForRPC() + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared input variable", + Detail: `There is no variable "input" block declared in this stack.`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("plan-undeclared-variable-in-component/undeclared-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 17, Column: 13, Byte: 250}, + End: hcl.Pos{Line: 17, Column: 22, Byte: 259}, + }, + }) + wantDiags = wantDiags.ForRPC() + + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } +} + +func TestPlanWithNoValueForRequiredVariable(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "plan-no-value-for-required-variable") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + _, gotDiags := collectPlanOutput(changesCh, diagsCh) + + // We'll normalize the diagnostics to be of consistent underlying type + // using ForRPC, so that we can easily diff them; we don't actually care + // about which underlying implementation is in use. + gotDiags = gotDiags.ForRPC() + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "No value for required variable", + Detail: `The root input variable "var.beep" is not set, and has no default value.`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("plan-no-value-for-required-variable/unset-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 16, Byte: 15}, + }, + }) + wantDiags = wantDiags.ForRPC() + + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } +} + +func TestPlanWithVariableDefaults(t *testing.T) { + // Test that defaults are applied correctly for both unspecified input + // variables and those with an explicit null value. + testCases := map[string]struct { + inputs map[stackaddrs.InputVariable]ExternalInputValue + }{ + "unspecified": { + inputs: make(map[stackaddrs.InputVariable]ExternalInputValue), + }, + "explicit null": { + inputs: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "beep"}: { + Value: cty.NullVal(cty.DynamicPseudoType), + DefRange: tfdiags.SourceRange{Filename: "fake.tfstack.hcl"}, + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "plan-variable-defaults") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + InputValues: tc.inputs, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "beep"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("BEEP"), + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "defaulted"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("BOOP"), + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "specified"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("BEEP"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "beep", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("BEEP"), + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + }) + } +} + +func TestPlanWithComplexVariableDefaults(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("complex-inputs")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "optional"}: { + Value: cty.EmptyObjectVal, // This should be populated by defaults. + DefRange: tfdiags.SourceRange{}, + }, + }, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + changes, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) != 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + sort.SliceStable(changes, func(i, j int) bool { + return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlannedInputValues: map[string]plans.DynamicValue{ + "input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("cec9bc39"), + "value": cty.StringVal("hello, mercury!"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("78d8b3d7"), + "value": cty.StringVal("hello, venus!"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("hello, earth!"), + }), + })), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[0]"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data[0]"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("cec9bc39"), + "value": cty.StringVal("hello, mercury!"), + })), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[1]"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data[1]"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("78d8b3d7"), + "value": cty.StringVal("hello, venus!"), + })), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[2]"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data[2]"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("hello, earth!"), + })), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.child.component.parent"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlannedInputValues: map[string]plans.DynamicValue{ + "input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("cec9bc39"), + "value": cty.StringVal("hello, mercury!"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("78d8b3d7"), + "value": cty.StringVal("hello, venus!"), + }), + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("hello, earth!"), + }), + })), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[0]"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data[0]"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("cec9bc39"), + "value": cty.StringVal("hello, mercury!"), + })), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[1]"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data[1]"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("78d8b3d7"), + "value": cty.StringVal("hello, venus!"), + })), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[2]"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data[2]"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("hello, earth!"), + })), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "default"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("cec9bc39"), + "value": cty.StringVal("hello, mercury!"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "optional"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("hello, earth!"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "optional_default"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("78d8b3d7"), + "value": cty.StringVal("hello, venus!"), + }), + }, + } + + if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + +} + +func TestPlanWithSingleResource(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "with-single-resource") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + // The order of emission for our planned changes is unspecified since it + // depends on how the various goroutines get scheduled, and so we'll + // arbitrarily sort gotChanges lexically by the name of the change type + // so that we have some dependable order to diff against below. + sort.Slice(gotChanges, func(i, j int) bool { + ic := gotChanges[i] + jc := gotChanges[j] + return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "input": cty.StringVal("hello"), + "output": cty.UnknownVal(cty.String), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "obj"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("hello"), + "output": cty.UnknownVal(cty.String), + }), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewBuiltInProvider("terraform"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: plans.DynamicValue{ + // This is an object conforming to the terraform_data + // resource type's schema. + // + // FIXME: Should write this a different way that is + // scrutable and won't break each time something gets + // added to the terraform_data schema. (We can't use + // mustPlanDynamicValue here because the resource type + // uses DynamicPseudoType attributes, which require + // explicitly-typed encoding.) + 0x84, 0xa2, 0x69, 0x64, 0xc7, 0x03, 0x0c, 0x81, + 0x01, 0xc2, 0xa5, 0x69, 0x6e, 0x70, 0x75, 0x74, + 0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x22, 0xa5, 0x68, 0x65, 0x6c, 0x6c, + 0x6f, 0xa6, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x92, 0xc4, 0x08, 0x22, 0x73, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x22, 0xd4, 0x00, 0x00, 0xb0, 0x74, + 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x73, 0x5f, + 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0xc0, + }, + }, + }, + + // The following is schema for the real terraform_data resource + // type from the real terraform.io/builtin/terraform provider + // maintained elsewhere in this codebase. If that schema changes + // in future then this should change to match it. + Schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.DynamicPseudoType, Optional: true}, + "output": {Type: cty.DynamicPseudoType, Computed: true}, + "triggers_replace": {Type: cty.DynamicPseudoType, Optional: true}, + "id": {Type: cty.String, Computed: true}, + }, + }, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Description: "The unique identifier for the data store.", + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithEphemeralInputVariables(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "variable-ephemeral") + + t.Run("with variables set", func(t *testing.T) { + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + req := PlanRequest{ + Config: cfg, + InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{ + {Name: "eph"}: {Value: cty.StringVal("eph value")}, + {Name: "noneph"}: {Value: cty.StringVal("noneph value")}, + }, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "eph", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.String), // ephemeral + RequiredOnApply: true, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "noneph", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("noneph value"), + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + }) + + t.Run("without variables set", func(t *testing.T) { + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + req := PlanRequest{ + InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{ + // Intentionally not set for this subtest. + }, + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "eph", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.String), // ephemeral + RequiredOnApply: false, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "noneph", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.String), + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + }) +} + +func TestPlanVariableOutputRoundtripNested(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "variable-output-roundtrip-nested") + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "msg"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("default"), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "msg", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("default"), + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanSensitiveOutput(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "sensitive-output") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("secret").Mark(marks.Sensitive), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanSensitiveOutputNested(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "sensitive-output-nested") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("secret").Mark(marks.Sensitive), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("child", addrs.NoKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanSensitiveOutputAsInput(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "sensitive-output-as-input") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + mustAbsComponent("stack.sensitive.component.self"), + ), + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: map[string]plans.DynamicValue{ + "secret": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "secret": { + { + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, + }, + PlannedOutputValues: map[string]cty.Value{ + "result": cty.StringVal("SECRET").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "result"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), // MessagePack nil + After: cty.StringVal("SECRET").Mark(marks.Sensitive), + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithProviderConfig(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "with-provider-config") + providerAddr := addrs.MustParseProviderSourceString("example.com/test/test") + providerSchema := &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + } + inputVarAddr := stackaddrs.InputVariable{Name: "name"} + fakeSrcRng := tfdiags.SourceRange{ + Filename: "fake-source", + } + lock := depsfile.NewLocks() + lock.SetProvider( + providerAddr, + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + t.Run("valid", func(t *testing.T) { + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + + provider := &default_testing_provider.MockProvider{ + GetProviderSchemaResponse: providerSchema, + ValidateProviderConfigResponse: &providers.ValidateProviderConfigResponse{}, + ConfigureProviderResponse: &providers.ConfigureProviderResponse{}, + } + + req := PlanRequest{ + Config: cfg, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + inputVarAddr: { + Value: cty.StringVal("Jackson"), + DefRange: fakeSrcRng, + }, + }, + ProviderFactories: map[addrs.Provider]providers.Factory{ + providerAddr: func() (providers.Interface, error) { + return provider, nil + }, + }, + DependencyLocks: *lock, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + _, diags := collectPlanOutput(changesCh, diagsCh) + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + if !provider.ValidateProviderConfigCalled { + t.Error("ValidateProviderConfig wasn't called") + } else { + req := provider.ValidateProviderConfigRequest + if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) { + t.Errorf("wrong name in ValidateProviderConfig\ngot: %#v\nwant: %#v", got, want) + } + } + if !provider.ConfigureProviderCalled { + t.Error("ConfigureProvider wasn't called") + } else { + req := provider.ConfigureProviderRequest + if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) { + t.Errorf("wrong name in ConfigureProvider\ngot: %#v\nwant: %#v", got, want) + } + } + if !provider.CloseCalled { + t.Error("provider wasn't closed") + } + }) +} + +func TestPlanWithRemovedResource(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + attrs := map[string]interface{}{ + "id": "FE1D5830765C", + "input": map[string]interface{}{ + "value": "hello", + "type": "string", + }, + "output": map[string]interface{}{ + "value": nil, + "type": "string", + }, + "triggers_replace": nil, + } + attrsJSON, err := json.Marshal(attrs) + if err != nil { + t.Fatal(err) + } + + // We want to see that it's adding the extra context for when a provider is + // missing for a resource that's in state and not in config. + expectedDiagnostic := "has resources in state that" + + tcs := make(map[string]*string) + tcs["missing-providers"] = &expectedDiagnostic + tcs["valid-providers"] = nil + + for name, diag := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", name)) + + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) { + return terraformProvider.NewProvider(), nil + }, + }, + + ForcePlanTimestamp: &fakePlanTimestamp, + + // PrevState specifies a state with a resource that is not present in + // the current configuration. This is a common situation when a resource + // is removed from the configuration but still exists in the state. + PrevState: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + Key: addrs.NoKey, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_data", + Name: "main", + }, + Key: addrs.NoKey, + }, + }, + DeposedKey: addrs.NotDeposed, + }, + }). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + SchemaVersion: 0, + AttrsJSON: attrsJSON, + Status: states.ObjectReady, + }). + SetProviderAddr(addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"), + })). + Build(), + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + _, diags := collectPlanOutput(changesCh, diagsCh) + + if diag != nil { + if len(diags) == 0 { + t.Fatalf("expected diagnostics, got none") + } + if !strings.Contains(diags[0].Description().Detail, *diag) { + t.Fatalf("expected diagnostic %q, got %q", *diag, diags[0].Description().Detail) + } + } else if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error()) + } + }) + } +} + +func TestPlanWithSensitivePropagation(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{Name: "sensitive"}, + }, + ), + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": { + { + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("secret"), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + }, + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "sensitive"}, + }, + ), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "id"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.String), + }, + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithSensitivePropagationNested(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input-nested")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + mustAbsComponent("stack.sensitive.component.self"), + ), + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": { + { + Marks: cty.NewValueMarks(marks.Sensitive), + }, + }, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("secret"), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + }, + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey), + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: map[string]cty.Value{ + "out": cty.StringVal("secret").Mark(marks.Sensitive), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "id"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.String), + }, + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithForEach(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "input-from-component-list")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "components"}: { + Value: cty.ListVal([]cty.Value{cty.StringVal("one"), cty.StringVal("two"), cty.StringVal("three")}), + DefRange: tfdiags.SourceRange{}, + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + _, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() // We reported the diags above/ + } +} + +func TestPlanWithCheckableObjects(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "checkable-objects") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "foo"}: { + Value: cty.StringVal("bar"), + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + + Summary: "Check block assertion failed", + Detail: `value must be 'baz'`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"), + Start: hcl.Pos{Line: 41, Column: 21, Byte: 716}, + End: hcl.Pos{Line: 41, Column: 57, Byte: 752}, + }, + }) + + go Plan(ctx, &req, &resp) + gotChanges, gotDiags := collectPlanOutput(changesCh, diagsCh) + + if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } + + // The order of emission for our planned changes is unspecified since it + // depends on how the various goroutines get scheduled, and so we'll + // arbitrarily sort gotChanges lexically by the name of the change type + // so that we have some dependable order to diff against below. + sort.Slice(gotChanges, func(i, j int) bool { + ic := gotChanges[i] + jc := gotChanges[j] + return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "single"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedInputValues: map[string]plans.DynamicValue{ + "foo": mustPlanDynamicValueDynamicType(cty.StringVal("bar")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{"foo": nil}, + PlannedOutputValues: map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + PlannedCheckResults: &states.CheckResults{ + ConfigResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.ConfigCheckable]( + addrs.Check{ + Name: "value_is_baz", + }.InModule(addrs.RootModule), + &states.CheckResultAggregate{ + Status: checks.StatusFail, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.Checkable]( + addrs.Check{ + Name: "value_is_baz", + }.Absolute(addrs.RootModuleInstance), + &states.CheckResultObject{ + Status: checks.StatusFail, + FailureMessages: []string{"value must be 'baz'"}, + }, + ), + ), + }, + ), + addrs.MakeMapElem[addrs.ConfigCheckable]( + addrs.InputVariable{ + Name: "foo", + }.InModule(addrs.RootModule), + &states.CheckResultAggregate{ + Status: checks.StatusPass, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.Checkable]( + addrs.InputVariable{ + Name: "foo", + }.Absolute(addrs.RootModuleInstance), + &states.CheckResultObject{ + Status: checks.StatusPass, + }, + ), + ), + }, + ), + addrs.MakeMapElem[addrs.ConfigCheckable]( + addrs.OutputValue{ + Name: "foo", + }.InModule(addrs.RootModule), + &states.CheckResultAggregate{ + Status: checks.StatusPass, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.Checkable]( + addrs.OutputValue{ + Name: "foo", + }.Absolute(addrs.RootModuleInstance), + &states.CheckResultObject{ + Status: checks.StatusPass, + }, + ), + ), + }, + ), + addrs.MakeMapElem[addrs.ConfigCheckable]( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "main", + }.InModule(addrs.RootModule), + &states.CheckResultAggregate{ + Status: checks.StatusPass, + ObjectResults: addrs.MakeMap( + addrs.MakeMapElem[addrs.Checkable]( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.CheckResultObject{ + Status: checks.StatusPass, + }, + ), + ), + }, + ), + ), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "single"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "main", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + "value": cty.StringVal("bar"), + }), stacks_testing_provider.TestingResourceSchema.Body), + }, + }, + + Schema: stacks_testing_provider.TestingResourceSchema, + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithDeferredResource(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "deferrable-component") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "id"}: { + Value: cty.StringVal("62594ae3"), + }, + {Name: "defer"}: { + Value: cty.BoolVal(true), + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() // We reported the diags above + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + PlanComplete: false, + PlanApplyable: false, // We don't have any resources to apply since they're deferred. + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("62594ae3")), + "defer": mustPlanDynamicValueDynamicType(cty.BoolVal(true)), + }, + PlannedOutputValues: map[string]cty.Value{}, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "defer": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_deferred_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_deferred_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_deferred_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("62594ae3"), + "value": cty.NullVal(cty.String), + "deferred": cty.BoolVal(true), + }), stacks_testing_provider.DeferredResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + Schema: stacks_testing_provider.DeferredResourceSchema, + }, + DeferredReason: providers.DeferredReasonResourceConfigUnknown, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "defer"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.BoolVal(true), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "id"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("62594ae3"), + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithDeferredComponentForEach(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-for-each")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "components"}: { + Value: cty.UnknownVal(cty.Set(cty.String)), + DefRange: tfdiags.SourceRange{}, + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() // We reported the diags above/ + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "child"}, + }, + ), + PlanApplyable: true, + PlanComplete: false, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{ + Name: "self", + }, + }, + ), + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "child", + }, + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }, + Key: addrs.NoKey, + }, + }, + }, + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.UnknownVal(cty.String), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonDeferredPrereq, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + Key: addrs.WildcardKey, + }, + ), + PlanApplyable: true, // TODO: Questionable? We only have outputs. + PlanComplete: false, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedOutputValues: map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + Key: addrs.WildcardKey, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.UnknownVal(cty.String), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "components"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithDeferredComponentReferences(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-references")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "known_components"}: { + Value: cty.ListVal([]cty.Value{cty.StringVal("known")}), + DefRange: tfdiags.SourceRange{}, + }, + {Name: "unknown_components"}: { + Value: cty.UnknownVal(cty.Set(cty.String)), + DefRange: tfdiags.SourceRange{}, + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() // We reported the diags above. + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "children"}, + Key: addrs.WildcardKey, + }, + ), + PlanApplyable: true, // TODO: Questionable? We only have outputs. + PlanComplete: false, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + stackaddrs.AbsComponent{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.Component{ + Name: "self", + }, + }, + ), + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + DeferredReason: providers.DeferredReasonDeferredPrereq, + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "children"}, + Key: addrs.WildcardKey, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.UnknownVal(cty.String), + }), stacks_testing_provider.TestingResourceSchema.Body), + AfterSensitivePaths: nil, + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + Key: addrs.StringKey("known"), + }), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("known")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlannedOutputValues: map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "self", + }, + Key: addrs.StringKey("known"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }, + Key: addrs.NoKey, + }, + }, + }, + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }, + Key: addrs.NoKey, + }, + }, + PrevRunAddr: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }, + Key: addrs.NoKey, + }, + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("known"), + }), stacks_testing_provider.TestingResourceSchema.Body), + }, + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "known_components"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{cty.StringVal("known")}), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "unknown_components"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithDeferredComponentForEachOfInvalidType(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "deferred-component-for-each-from-component-of-invalid-type") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + + ForcePlanTimestamp: &fakePlanTimestamp, + + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "components"}: { + Value: cty.UnknownVal(cty.Set(cty.String)), + DefRange: tfdiags.SourceRange{}, + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + _, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 1 { + t.Fatalf("expected 1 diagnostic, got %d: %s", len(diags), diags) + } + + if diags[0].Severity() != tfdiags.Error { + t.Errorf("expected error diagnostic, got %q", diags[0].Severity()) + } + + expectedSummary := "Invalid for_each value" + if diags[0].Description().Summary != expectedSummary { + t.Errorf("expected diagnostic with summary %q, got %q", expectedSummary, diags[0].Description().Summary) + } + + expectedDetail := "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component." + if diags[0].Description().Detail != expectedDetail { + t.Errorf("expected diagnostic with detail %q, got %q", expectedDetail, diags[0].Description().Detail) + } +} + +func TestPlanWithDeferredProviderForEach(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "deferred-provider-for-each")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "providers"}: { + Value: cty.UnknownVal(cty.Set(cty.String)), + DefRange: tfdiags.SourceRange{}, + }, + }, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() // We reported the diags above + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "known"}, + }), + PlanComplete: false, + PlanApplyable: false, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("primary")), + }, + PlannedOutputValues: map[string]cty.Value{}, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "known"}, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("primary"), + }), stacks_testing_provider.TestingResourceSchema.Body), + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "unknown"}, + Key: addrs.WildcardKey, + }), + PlanComplete: false, + PlanApplyable: false, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("secondary")), + }, + PlannedOutputValues: map[string]cty.Value{}, + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "unknown"}, + Key: addrs.WildcardKey, + }, + ), + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + PrevRunAddr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "testing_resource", + Name: "data", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("hashicorp/testing"), + }, + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("secondary"), + }), stacks_testing_provider.TestingResourceSchema.Body), + }, + }, + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonProviderConfigUnknown, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "providers"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanInvalidProvidersFailGracefully(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("invalid-providers")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + changes, diags := collectPlanOutput(changesCh, diagsCh) + + sort.SliceStable(diags, diagnosticSortFunc(diags)) + expectDiagnosticsForTest(t, diags, + expectDiagnostic(tfdiags.Error, "Provider configuration is invalid", "Cannot plan changes for this resource because its associated provider configuration is invalid."), + expectDiagnostic(tfdiags.Error, "invalid configuration", "configure_error attribute was set")) + + sort.SliceStable(changes, func(i, j int) bool { + return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{}, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanTimestamp: fakePlanTimestamp, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + } + + if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlanWithStateManipulation(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + tcs := map[string]struct { + state *stackstate.State + store *stacks_testing_provider.ResourceStore + inputs map[string]cty.Value + changes []stackplan.PlannedChange + counts collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange] + expectedWarnings []string + }{ + "moved": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("moved", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })). + Build(), + changes: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Update, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.after"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.before"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Move: 1, + }, + }), + }, + "cross-type-moved": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("moved", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + })). + Build(), + changes: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Update, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_deferred_resource.after"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_deferred_resource.after"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.before"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + "deferred": cty.False, + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("moved"), + "value": cty.StringVal("moved"), + "deferred": cty.False, + })), + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "moved", + "value": "moved", + "deferred": false, + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.DeferredResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Move: 1, + }, + }), + }, + "import": { + state: stackstate.NewStateBuilder().Build(), // We start with an empty state for this. + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("imported", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported"), + "value": cty.StringVal("imported"), + })). + Build(), + inputs: map[string]cty.Value{ + "id": cty.StringVal("imported"), + }, + changes: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + // The component is still CREATE even though all the + // instances are NoOps, because the component itself didn't + // exist before even though all the resources might have. + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("imported")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported"), + "value": cty.StringVal("imported"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("imported"), + "value": cty.StringVal("imported"), + })), + Importing: &plans.ImportingSrc{ + ID: "imported", + }, + }, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "imported", + "value": "imported", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "id", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("imported"), + RequiredOnApply: false, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Import: 1, + }, + }), + }, + "removed": { + state: stackstate.NewStateBuilder(). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + Build(), + changes: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Update, + PlannedInputValues: make(map[string]plans.DynamicValue), + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.resource"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.resource"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Forget, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig, + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]( + collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{ + K: mustAbsComponentInstance("component.self"), + V: &hooks.ComponentInstanceChange{ + Addr: mustAbsComponentInstance("component.self"), + Forget: 1, + }, + }), + expectedWarnings: []string{"Some objects will no longer be managed by Terraform"}, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name)) + + gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]() + ctx = ContextWithHooks(ctx, &stackeval.Hooks{ + ReportComponentInstancePlanned: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any { + gotCounts.Put(change.Addr, change) + return span + }, + }) + + inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs)) + for name, input := range tc.inputs { + inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{ + Value: input, + } + } + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, tc.store), nil + }, + }, + DependencyLocks: *lock, + InputValues: inputs, + ForcePlanTimestamp: &fakePlanTimestamp, + PrevState: tc.state, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + go Plan(ctx, &req, &resp) + changes, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) > len(tc.expectedWarnings) { + t.Fatalf("had unexpected warnings") + } + for i, diag := range diags { + if diag.Description().Summary != tc.expectedWarnings[i] { + t.Fatalf("expected diagnostic with summary %q, got %q", tc.expectedWarnings[i], diag.Description().Summary) + } + } + + sort.SliceStable(changes, func(i, j int) bool { + return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j]) + }) + + if diff := cmp.Diff(tc.changes, changes, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + + wantCounts := tc.counts + for key, elem := range wantCounts.All() { + // First, make sure everything we wanted is present. + if !gotCounts.HasKey(key) { + t.Errorf("wrong counts: wanted %s but didn't get it", key) + } + + // And that the values actually match. + got, want := gotCounts.Get(key), elem + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong counts for %s: %s", want.Addr, diff) + } + + } + + for key := range gotCounts.All() { + // Then, make sure we didn't get anything we didn't want. + if !wantCounts.HasKey(key) { + t.Errorf("wrong counts: got %s but didn't want it", key) + } + } + }) + } +} + +func TestPlan_plantimestamp_force_timestamp(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "with-plantimestamp") + + forcedPlanTimestamp := "1991-08-25T20:57:08Z" + fakePlanTimestamp, err := time.Parse(time.RFC3339, forcedPlanTimestamp) + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + // We support both hashicorp/testing and + // terraform.io/builtin/testing as providers. This lets us + // test the provider aliasing feature. Both providers + // support the same set of resources and data sources. + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { + return map[stackaddrs.InputVariable]ExternalInputValue{} + }(), + ForcePlanTimestamp: &fakePlanTimestamp, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + // The following will fail the test if there are any error + // diagnostics. + reportDiagnosticsForTest(t, diags) + + // We also want to fail if there are just warnings, since the + // configurations here are supposed to be totally problem-free. + if len(diags) != 0 { + // reportDiagnosticsForTest already showed the diagnostics in + // the log + t.FailNow() + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "second-self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "value": nil, + }, + PlannedInputValues: map[string]plans.DynamicValue{ + "value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)), + }, + PlannedOutputValues: map[string]cty.Value{ + "input": cty.StringVal(forcedPlanTimestamp), + "out": cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "value": nil, + }, + PlannedInputValues: map[string]plans.DynamicValue{ + "value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)), + }, + PlannedOutputValues: map[string]cty.Value{ + "input": cty.StringVal(forcedPlanTimestamp), + "out": cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)), + }, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangeOutputValue{ + Addr: stackaddrs.OutputValue{Name: "plantimestamp"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal(forcedPlanTimestamp), + }, + &stackplan.PlannedChangePlannedTimestamp{PlannedTimestamp: fakePlanTimestamp}, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlan_plantimestamp_later_than_when_writing_this_test(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "with-plantimestamp") + + dayOfWritingThisTest := "2024-06-21T06:37:08Z" + dayOfWritingThisTestTime, err := time.Parse(time.RFC3339, dayOfWritingThisTest) + if err != nil { + t.Fatal(err) + } + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + // We support both hashicorp/testing and + // terraform.io/builtin/testing as providers. This lets us + // test the provider aliasing feature. Both providers + // support the same set of resources and data sources. + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue { + return map[stackaddrs.InputVariable]ExternalInputValue{} + }(), + ForcePlanTimestamp: nil, // This is what we want to test + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + changes, diags := collectPlanOutput(changesCh, diagsCh) + output := expectOutput(t, "plantimestamp", changes) + + plantimestampValue := output.After + plantimestamp, err := time.Parse(time.RFC3339, plantimestampValue.AsString()) + if err != nil { + t.Fatal(err) + } + + if plantimestamp.Before(dayOfWritingThisTestTime) { + t.Errorf("expected plantimestamp to be later than %q, got %q", dayOfWritingThisTest, plantimestampValue.AsString()) + } + + // The following will fail the test if there are any error + // diagnostics. + reportDiagnosticsForTest(t, diags) + + // We also want to fail if there are just warnings, since the + // configurations here are supposed to be totally problem-free. + if len(diags) != 0 { + // reportDiagnosticsForTest already showed the diagnostics in + // the log + t.FailNow() + } +} + +func TestPlan_DependsOnUpdatesRequirements(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "depends-on")) + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + changesCh := make(chan stackplan.PlannedChange) + diagsCh := make(chan tfdiags.Diagnostic) + req := PlanRequest{ + Config: cfg, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + ForcePlanTimestamp: &fakePlanTimestamp, + InputValues: map[stackaddrs.InputVariable]ExternalInputValue{ + {Name: "input"}: { + Value: cty.StringVal("hello, world!"), + }, + }, + } + + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + reportDiagnosticsForTest(t, diags) + if len(diags) != 0 { + t.FailNow() + } + + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.first"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](), + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlanTimestamp: fakePlanTimestamp, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("hello, world!"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.second"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + mustAbsComponent("component.first"), + mustAbsComponent("stack.second.component.self"), + ), + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlanTimestamp: fakePlanTimestamp, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("hello, world!"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.first.component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + RequiredComponents: collections.NewSet[stackaddrs.AbsComponent]( + mustAbsComponent("component.first"), + mustAbsComponent("component.empty"), + ), + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlanTimestamp: fakePlanTimestamp, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.first.component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("hello, world!"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("stack.second.component.self"), + PlanApplyable: true, + PlanComplete: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "id": nil, + "input": nil, + }, + PlanTimestamp: fakePlanTimestamp, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.second.component.self.testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + "value": cty.StringVal("hello, world!"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "empty", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{ + Name: "input", + }, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("hello, world!"), + }, + } + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +func TestPlan_RemovedBlocks(t *testing.T) { + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + tcs := map[string]struct { + source string + initialState *stackstate.State + store *stacks_testing_provider.ResourceStore + inputs map[string]cty.Value + wantPlanChanges []stackplan.PlannedChange + wantPlanDiags []expectedDiagnostic + }{ + "unknown removed block with nothing to remove": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + // we have a single component instance in state + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "removed": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: true, + PlanApplyable: false, // all changes are no-ops + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "a", + "value": "a", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + }, + }, + "unknown removed block with elements in state": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + // we have a single component instance in state + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.SetValEmpty(cty.String), + "removed": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: false, // has deferred changes + PlanApplyable: false, // only deferred changes + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "a", + "value": "a", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonDeferredPrereq, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetValEmpty(cty.String), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + }, + }, + "unknown component block with element to remove": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"b\"]")). + AddInputVariable("id", cty.StringVal("b")). + AddInputVariable("input", cty.StringVal("b"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "b", + "value": "b", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + AddResource("b", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("b"), + "value": cty.StringVal("b"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.UnknownVal(cty.Set(cty.String)), + "removed": cty.SetVal([]cty.Value{cty.StringVal("b")}), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: false, // has deferred changes + PlanApplyable: false, // only deferred changes + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "a", + "value": "a", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonDeferredPrereq, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"b\"]"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Delete, + Mode: plans.DestroyMode, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("b")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("b")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("b"), + "value": cty.StringVal("b"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "b", + "value": "b", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{cty.StringVal("b")}), + }, + }, + }, + "unknown component and removed block with element in state": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.UnknownVal(cty.Set(cty.String)), + "removed": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: false, // has deferred changes + PlanApplyable: false, // only deferred changes + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeDeferredResourceInstancePlanned{ + ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{ + "id": "a", + "value": "a", + }), + Status: states.ObjectReady, + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + DeferredReason: providers.DeferredReasonDeferredPrereq, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.UnknownVal(cty.Set(cty.String)), + }, + }, + }, + "absent component": { + source: filepath.Join("with-single-input", "removed-component"), + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + }, + }, + "absent component instance": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "removed": cty.SetVal([]cty.Value{ + cty.StringVal("b"), // Doesn't exist! + }), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + // we're expecting the new component to be created + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: true, + PlanApplyable: false, // no changes + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + Dependencies: make([]addrs.ConfigResource, 0), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstanceRemoved{ + Addr: mustAbsComponentInstance("component.self[\"removed\"]"), + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("b"), + }), + }, + }, + }, + "orphaned component": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")). + AddInputVariable("id", cty.StringVal("removed")). + AddInputVariable("input", cty.StringVal("removed"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + })). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"orphaned\"]")). + AddInputVariable("id", cty.StringVal("orphaned")). + AddInputVariable("input", cty.StringVal("orphaned"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"orphaned\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "orphaned", + "value": "orphaned", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("removed", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })). + AddResource("orphaned", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("orphaned"), + "value": cty.StringVal("orphaned"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + "removed": cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: false, // No! We have an unclaimed instance! + }, + // we're expecting the new component to be created + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"added\"]"), + PlanComplete: true, + PlanApplyable: true, + Action: plans.Create, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("added")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("added")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("added"), + "value": cty.StringVal("added"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"removed\"]"), + PlanComplete: true, + PlanApplyable: true, + Mode: plans.DestroyMode, + Action: plans.Delete, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Delete, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("removed"), + "value": cty.StringVal("removed"), + })), + After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "removed", + "value": "removed", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("added"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("removed"), + }), + }, + }, + wantPlanDiags: []expectedDiagnostic{ + { + severity: tfdiags.Error, + summary: "Unclaimed component instance", + detail: "The component instance component.self[\"orphaned\"] is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.", + }, + }, + }, + "duplicate component": { + source: filepath.Join("with-single-input", "removed-component-instance"), + initialState: stackstate.NewStateBuilder(). + AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")). + AddInputVariable("id", cty.StringVal("a")). + AddInputVariable("input", cty.StringVal("a"))). + AddResourceInstance(stackstate.NewResourceInstanceBuilder(). + SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")). + SetProviderAddr(mustDefaultRootProvider("testing")). + SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + })). + Build(), + store: stacks_testing_provider.NewResourceStoreBuilder(). + AddResource("a", cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })). + Build(), + inputs: map[string]cty.Value{ + "input": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "removed": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + wantPlanChanges: []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: false, // No! The removed block is a duplicate of the component! + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: mustAbsComponentInstance("component.self[\"a\"]"), + PlanComplete: true, + PlanApplyable: false, // no changes + Action: plans.Update, + PlannedInputValues: map[string]plans.DynamicValue{ + "id": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + "input": mustPlanDynamicValueDynamicType(cty.StringVal("a")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{ + "input": nil, + "id": nil, + }, + PlannedOutputValues: make(map[string]cty.Value), + PlannedCheckResults: &states.CheckResults{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource.data"), + PrevRunAddr: mustAbsResourceInstance("testing_resource.data"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.NoOp, + Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("a"), + "value": cty.StringVal("a"), + })), + }, + ProviderAddr: mustDefaultRootProvider("testing"), + }, + PriorStateSrc: &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustMarshalJSONAttrs(map[string]any{ + "id": "a", + "value": "a", + }), + Dependencies: make([]addrs.ConfigResource, 0), + Status: states.ObjectReady, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceSchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "input"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + &stackplan.PlannedChangeRootInputValue{ + Addr: stackaddrs.InputVariable{Name: "removed"}, + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + }, + }, + wantPlanDiags: []expectedDiagnostic{ + { + severity: tfdiags.Error, + summary: "Cannot remove component instance", + detail: "The component instance component.self[\"a\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/removed-component-instance/removed-component-instance.tfstack.hcl:18,1-17.", + }, + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, tc.source) + + inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs)) + for name, input := range tc.inputs { + inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{ + Value: input, + } + } + + providers := map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProviderWithData(t, tc.store), nil + }, + } + + planChangesCh := make(chan stackplan.PlannedChange) + planDiagsCh := make(chan tfdiags.Diagnostic) + planReq := PlanRequest{ + Config: cfg, + ProviderFactories: providers, + InputValues: inputs, + ForcePlanTimestamp: &fakePlanTimestamp, + PrevState: tc.initialState, + DependencyLocks: *lock, + } + planResp := PlanResponse{ + PlannedChanges: planChangesCh, + Diagnostics: planDiagsCh, + } + go Plan(ctx, &planReq, &planResp) + gotPlanChanges, gotPlanDiags := collectPlanOutput(planChangesCh, planDiagsCh) + + sort.SliceStable(gotPlanChanges, func(i, j int) bool { + return plannedChangeSortKey(gotPlanChanges[i]) < plannedChangeSortKey(gotPlanChanges[j]) + }) + sort.SliceStable(gotPlanDiags, diagnosticSortFunc(gotPlanDiags)) + + expectDiagnosticsForTest(t, gotPlanDiags, tc.wantPlanDiags...) + if diff := cmp.Diff(tc.wantPlanChanges, gotPlanChanges, ctydebug.CmpOptions, cmpCollectionsSet, cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{})); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } + }) + } +} + +func TestPlanWithResourceIdentities(t *testing.T) { + ctx := context.Background() + cfg := loadMainBundleConfigForTest(t, "resource-identity") + + fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z") + if err != nil { + t.Fatal(err) + } + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + changesCh := make(chan stackplan.PlannedChange, 8) + diagsCh := make(chan tfdiags.Diagnostic, 2) + req := PlanRequest{ + Config: cfg, + ForcePlanTimestamp: &fakePlanTimestamp, + ProviderFactories: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + DependencyLocks: *lock, + } + resp := PlanResponse{ + PlannedChanges: changesCh, + Diagnostics: diagsCh, + } + + go Plan(ctx, &req, &resp) + gotChanges, diags := collectPlanOutput(changesCh, diagsCh) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error()) + } + + wantChanges := []stackplan.PlannedChange{ + &stackplan.PlannedChangeApplyable{ + Applyable: true, + }, + &stackplan.PlannedChangeComponentInstance{ + Addr: stackaddrs.Absolute( + stackaddrs.RootStackInstance, + stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "self"}, + }, + ), + Action: plans.Create, + PlanApplyable: true, + PlanComplete: true, + PlannedCheckResults: &states.CheckResults{}, + PlannedInputValues: map[string]plans.DynamicValue{ + "name": mustPlanDynamicValueDynamicType(cty.StringVal("example")), + }, + PlannedInputValueMarks: map[string][]cty.PathValueMarks{"name": nil}, + PlannedOutputValues: map[string]cty.Value{}, + PlanTimestamp: fakePlanTimestamp, + }, + &stackplan.PlannedChangeResourceInstancePlanned{ + ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource_with_identity.hello"), + ChangeSrc: &plans.ResourceInstanceChangeSrc{ + Addr: mustAbsResourceInstance("testing_resource_with_identity.hello"), + PrevRunAddr: mustAbsResourceInstance("testing_resource_with_identity.hello"), + ProviderAddr: mustDefaultRootProvider("testing"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)), + After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("example"), + "value": cty.NullVal(cty.String), + })), + AfterIdentity: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("id:example"), + })), + }, + }, + ProviderConfigAddr: mustDefaultRootProvider("testing"), + Schema: stacks_testing_provider.TestingResourceWithIdentitySchema, + }, + &stackplan.PlannedChangeHeader{ + TerraformVersion: version.SemVer, + }, + &stackplan.PlannedChangePlannedTimestamp{ + PlannedTimestamp: fakePlanTimestamp, + }, + } + sort.SliceStable(gotChanges, func(i, j int) bool { + return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j]) + }) + + if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +// collectPlanOutput consumes the two output channels emitting results from +// a call to [Plan], and collects all of the data written to them before +// returning once changesCh has been closed by the sender to indicate that +// the planning process is complete. +func collectPlanOutput(changesCh <-chan stackplan.PlannedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackplan.PlannedChange, tfdiags.Diagnostics) { + var changes []stackplan.PlannedChange + var diags tfdiags.Diagnostics + + for { + select { + case change, ok := <-changesCh: + if !ok { + // The plan operation is complete but we might still have + // some buffered diagnostics to consume. + if diagsCh != nil { + for diag := range diagsCh { + diags = append(diags, diag) + } + } + return changes, diags + } + changes = append(changes, change) + case diag, ok := <-diagsCh: + if !ok { + // no more diagnostics to read + diagsCh = nil + continue + } + diags = append(diags, diag) + } + } +} + +func expectOutput(t *testing.T, name string, changes []stackplan.PlannedChange) *stackplan.PlannedChangeOutputValue { + t.Helper() + for _, change := range changes { + if v, ok := change.(*stackplan.PlannedChangeOutputValue); ok && v.Addr.Name == name { + return v + + } + } + + t.Fatalf("expected output value %q", name) + return nil +} + +var cmpCollectionsSet = cmp.Comparer(func(x, y collections.Set[stackaddrs.AbsComponent]) bool { + if x.Len() != y.Len() { + return false + } + + for v := range x.All() { + if !y.Has(v) { + return false + } + } + + return true +}) diff --git a/internal/stacks/stackruntime/telemetry.go b/internal/stacks/stackruntime/telemetry.go new file mode 100644 index 0000000000..1d9f02c827 --- /dev/null +++ b/internal/stacks/stackruntime/telemetry.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/trace" +) + +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform/internal/stacks/stackruntime") +} diff --git a/internal/stacks/stackruntime/telemetry_test.go b/internal/stacks/stackruntime/telemetry_test.go new file mode 100644 index 0000000000..8a5f4c5be2 --- /dev/null +++ b/internal/stacks/stackruntime/telemetry_test.go @@ -0,0 +1,299 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +//lint:file-ignore U1000 Some utilities in here are intentionally unused in VCS but are for temporary use while debugging a test. + +import ( + "context" + "encoding/hex" + "fmt" + "strings" + "sync" + "testing" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/embedded" +) + +// tracesToTestLog arranges for any traces generated by the current test to +// be emitted directly into the test log using the log methods of the given +// [testing.T]. +// +// This works by temporarily reassigning the global tracer provider and so +// is not suitable for parallel tests or subtests of tests that have already +// called this function. +// +// The results of this function are pretty chatty, so we should typically not +// leave this in a test checked in to version control, but it can be helpful to +// add temporarily during test debugging if it's unclear exactly how different +// components are interacting with one another. +func tracesToTestLog(t *testing.T) { + t.Helper() + oldProvider := otel.GetTracerProvider() + if _, ok := oldProvider.(*testLogTracerProvider); ok { + // This suggests that someone's tried to use tracesToTestLog in + // a parallel test or in a subtest of a test that already called it. + t.Fatal("overlapping tracesToTestLog") + } + t.Cleanup(func() { + otel.SetTracerProvider(oldProvider) + }) + + provider := testLogTracerProvider{ + t: t, + spanTracker: &spanTracker{ + names: make(map[trace.SpanID]string), + nextID: 1, + }, + } + otel.SetTracerProvider(provider) +} + +type testLogTracerProvider struct { + t *testing.T + spanTracker *spanTracker + + embedded.TracerProvider +} + +type spanTracker struct { + names map[trace.SpanID]string + nextID int + mu sync.Mutex +} + +func (t *spanTracker) StartNew(name string) trace.SpanID { + t.mu.Lock() + idRaw := t.nextID + t.nextID++ + t.mu.Unlock() + + ret := trace.SpanID{ + 0x00, 0x00, 0x00, 0x00, + byte(idRaw >> 24), + byte(idRaw >> 16), + byte(idRaw >> 8), + byte(idRaw >> 0), + } + t.TrackNew(ret, name) + return ret +} + +func (sn *spanTracker) TrackNew(id trace.SpanID, name string) { + sn.mu.Lock() + sn.names[id] = name + sn.mu.Unlock() +} + +func (sn *spanTracker) Get(id trace.SpanID) string { + sn.mu.Lock() + defer sn.mu.Unlock() + return sn.names[id] +} + +func (sn *spanTracker) SpanDisplay(id trace.SpanID) string { + // we only use the last 32bits of the ids in this fake tracer, because + // the others will always be zero. (see testLogTracer.generateSpanID) + name := sn.Get(id) + idStr := testingSpanIDString(id) + if name == "" { + return idStr + } + return fmt.Sprintf("%s(%q)", idStr, name) +} + +func (sn *spanTracker) SpanAttrDisplay(kv attribute.KeyValue) string { + v := kv.Value.AsInterface() + switch string(kv.Key) { + case "promise.waiting_for_id", "promise.waiter_id", + "promising.resolved_by", "promising.resolved_id", + "promising.delegated_from", "promising.delegated_to", + "promising.responsible_for": + + // These conventionally contain stringified span IDs, which + // are 16 hex digits. + if v, ok := v.(string); ok && len(v) == 16 { + if bytes, err := hex.DecodeString(v); err == nil { + var spanID trace.SpanID + copy(spanID[:], bytes) + return sn.SpanDisplay(spanID) + } + } + } + // If all else fails we'll just GoString it + return fmt.Sprintf("%#v", v) +} + +var _ trace.TracerProvider = (*testLogTracerProvider)(nil) + +// Tracer implements trace.TracerProvider. +func (p testLogTracerProvider) Tracer(name string, options ...trace.TracerOption) trace.Tracer { + p.t.Helper() + return &testLogTracer{ + t: p.t, + nextSpanID: 1, + spanTracker: p.spanTracker, + } +} + +type testLogTracer struct { + t *testing.T + spanTracker *spanTracker + nextSpanID uint32 + mu sync.Mutex + + embedded.Tracer +} + +var _ trace.Tracer = (*testLogTracer)(nil) + +var fakeTraceIDForTesting = trace.TraceID{ + 0xfe, 0xed, 0xfa, 0xce, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +} + +// Start implements trace.Tracer. +func (t *testLogTracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { + t.t.Helper() + + parentSpanCtx := trace.SpanContextFromContext(ctx) + + dispName := spanName + switch dispName { // some shorthands for common names + case "async task": + dispName = "🗘" + case "promise": + dispName = "⋯" + } + if parentName := t.spanTracker.Get(parentSpanCtx.SpanID()); parentName != "" { + dispName = parentName + " ⇨ " + dispName + } + spanID := t.spanTracker.StartNew(dispName) + + spanCtx := trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: fakeTraceIDForTesting, + SpanID: spanID, + }) + span := &testLogTraceSpan{ + name: spanName, + context: &spanCtx, + t: t.t, + spanTracker: t.spanTracker, + } + ctx = trace.ContextWithSpan(ctx, span) + + cfg := trace.NewSpanStartConfig(opts...) + var attrsBuilder strings.Builder + if parentSpanCtx.HasSpanID() && !cfg.NewRoot() { + fmt.Fprintf(&attrsBuilder, "\nPARENT: %s", t.spanTracker.SpanDisplay(parentSpanCtx.SpanID())) + } + for _, link := range cfg.Links() { + fmt.Fprintf(&attrsBuilder, "\nLINK: %s", t.spanTracker.SpanDisplay(link.SpanContext.SpanID())) + } + for _, kv := range cfg.Attributes() { + fmt.Fprintf(&attrsBuilder, "\n%s = %s", kv.Key, t.spanTracker.SpanAttrDisplay(kv)) + } + span.log("START%s", attrsBuilder.String()) + return ctx, span +} + +type testLogTraceSpan struct { + name string + context *trace.SpanContext + t *testing.T + spanTracker *spanTracker + + embedded.Span +} + +var _ trace.Span = (*testLogTraceSpan)(nil) + +func (s testLogTraceSpan) log(f string, args ...any) { + s.t.Helper() + s.t.Logf( + "[trace:%s] %s\n%s", + testingSpanIDString(s.context.SpanID()), + s.spanTracker.Get(s.context.SpanID()), + fmt.Sprintf(f, args...), + ) +} + +func testingSpanIDString(id trace.SpanID) string { + // we only use the last 32bits of the ids in this fake tracer, because + // the others will always be zero. (see testLogTracer.generateSpanID) + return fmt.Sprintf("%x", id[4:]) +} + +// AddEvent implements trace.Span. +func (s testLogTraceSpan) AddEvent(name string, options ...trace.EventOption) { + s.t.Helper() + cfg := trace.NewEventConfig(options...) + var attrsBuilder strings.Builder + for _, kv := range cfg.Attributes() { + fmt.Fprintf(&attrsBuilder, "\n%s = %s", kv.Key, s.spanTracker.SpanAttrDisplay(kv)) + } + s.log("EVENT %s%s", name, attrsBuilder.String()) +} + +// End implements trace.Span. +func (s testLogTraceSpan) End(options ...trace.SpanEndOption) { + s.t.Helper() + s.log("END") +} + +// IsRecording implements trace.Span. +func (s testLogTraceSpan) IsRecording() bool { + s.t.Helper() + return true +} + +// RecordError implements trace.Span. +func (s testLogTraceSpan) RecordError(err error, options ...trace.EventOption) { + s.t.Helper() + s.log("ERROR %s", err) +} + +// SetAttributes implements trace.Span. +func (s testLogTraceSpan) SetAttributes(kv ...attribute.KeyValue) { + s.t.Helper() +} + +// SetName implements trace.Span. +func (s *testLogTraceSpan) SetName(name string) { + s.t.Helper() + s.log("RENAMED to %s", name) + s.name = name +} + +// SetStatus implements trace.Span. +func (s testLogTraceSpan) SetStatus(code codes.Code, description string) { + s.t.Helper() + s.log("STATUS %s: %s", code, description) +} + +// SpanContext implements trace.Span. +func (s testLogTraceSpan) SpanContext() trace.SpanContext { + s.t.Helper() + return *s.context +} + +// TracerProvider implements trace.Span. +func (s testLogTraceSpan) TracerProvider() trace.TracerProvider { + s.t.Helper() + return testLogTracerProvider{ + t: s.t, + spanTracker: s.spanTracker, + } +} + +// AddLink implements trace.Span. +func (s testLogTraceSpan) AddLink(link trace.Link) { + // Noop +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/README.md b/internal/stacks/stackruntime/testdata/mainbundle/README.md new file mode 100644 index 0000000000..a4f459eeb5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/README.md @@ -0,0 +1,16 @@ +# Synthetic source bundle for most tests + +Since the tests in this package are concerned primilary with configuration +evaluation and less concerned about configuration bundling or loading, +most of our tests can just use subdirectories of the only package in this +synthetic source bundle to avoid the inconvenience of maintaining an entire +source bundle for each separate test. + +To use this: +- Make a subdirectory under `test/` with a name that's related to your test + case(s). +- Use the `loadMainBundleConfigForTest` helper, passing the name of your + test directory as the source directory. + + (The helper function will automatically construct the synthetic remote + source address needed to locate that subdirectory within the source bundle.) diff --git a/internal/stacks/stackruntime/testdata/mainbundle/terraform-sources.json b/internal/stacks/stackruntime/testdata/mainbundle/terraform-sources.json new file mode 100644 index 0000000000..10c5fc7725 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/terraform-sources.json @@ -0,0 +1,10 @@ +{ + "terraform_source_bundle": 1, + "packages": [ + { + "source": "git::https://example.com/test.git", + "local": "test", + "meta": {} + } + ] +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/aliased-provider/aliased-provider.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/aliased-provider/aliased-provider.tf new file mode 100644 index 0000000000..d1cdbb9a57 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/aliased-provider/aliased-provider.tf @@ -0,0 +1,4 @@ + +resource "testing_resource" "data" { + provider = other +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/aliased-provider/aliased-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/aliased-provider/aliased-provider.tfstack.hcl new file mode 100644 index 0000000000..c439b87703 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/aliased-provider/aliased-provider.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + other = { + source = "hashicorp/other" + version = "0.1.0" + } +} + +provider "other" "main" {} + +component "self" { + source = "./" + + providers = { + other = provider.other.main + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/auth-provider-w-data.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/auth-provider-w-data.tfstack.hcl new file mode 100644 index 0000000000..860ccfa5a3 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/auth-provider-w-data.tfstack.hcl @@ -0,0 +1,33 @@ + +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "main" {} + +provider "testing" "credentialed" { + config { + require_auth = true + authentication = component.load.credentials + } +} + +component "load" { + source = "./load" + + providers = { + testing = provider.testing.main + } +} + +component "create" { + source = "./create" + + providers = { + testing = provider.testing.credentialed + } +} + diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/create/create.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/create/create.tf new file mode 100644 index 0000000000..b9753bb292 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/create/create.tf @@ -0,0 +1,3 @@ +resource "testing_resource" "resource" { + id = "resource" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/load/load.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/load/load.tf new file mode 100644 index 0000000000..f522c16aaa --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/load/load.tf @@ -0,0 +1,8 @@ +data "testing_data_source" "credentials" { + id = "credentials" +} + +output "credentials" { + value = data.testing_data_source.credentials.value + sensitive = true +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/removed/removed.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/removed/removed.tfstack.hcl new file mode 100644 index 0000000000..4f39e4b42a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/auth-provider-w-data/removed/removed.tfstack.hcl @@ -0,0 +1,32 @@ + +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "main" {} + +provider "testing" "credentialed" { + config { + require_auth = true + authentication = component.load.credentials + } +} + +component "load" { + source = "../load" + + providers = { + testing = provider.testing.main + } +} + +removed { + source = "../create" + from = component.create + providers = { + testing = provider.testing.credentialed + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/checkable-objects/checkable-objects.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/checkable-objects/checkable-objects.tf new file mode 100644 index 0000000000..1564734388 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/checkable-objects/checkable-objects.tf @@ -0,0 +1,44 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "foo" { + type = string + validation { + condition = length(var.foo) > 0 + error_message = "input must not be empty" + } +} + +resource "testing_resource" "main" { + id = "test" + value = var.foo + + lifecycle { + postcondition { + condition = length(self.value) > 0 + error_message = "value must not be empty" + } + } +} + +output "foo" { + value = testing_resource.main.value + + precondition { + condition = length(testing_resource.main.value) > 0 + error_message = "value must not be empty" + } +} + +check "value_is_baz" { + assert { + condition = testing_resource.main.value == "baz" + error_message = "value must be 'baz'" + } +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/checkable-objects/checkable-objects.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/checkable-objects/checkable-objects.tfstack.hcl new file mode 100644 index 0000000000..3bef8ea6b5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/checkable-objects/checkable-objects.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" { +} + +component "single" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + foo = "bar" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl new file mode 100644 index 0000000000..06b3249093 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/child/main.tfstack.hcl @@ -0,0 +1,50 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "default" { + type = object({ + id = string + value = string + }) + default = { + id = "cec9bc39" + value = "hello, mercury!" + } +} + +variable "optional_default" { + type = object({ + id = optional(string) + value = optional(string, "hello, venus!") + }) + default = { + id = "78d8b3d7" + } +} + +variable "optional" { + type = object({ + id = optional(string) + value = optional(string, "hello, earth!") + }) +} + +component "parent" { + source = "../" + providers = { + testing = provider.testing.default + } + inputs = { + input = [ + var.default, + var.optional_default, + var.optional, + ] + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf new file mode 100644 index 0000000000..20aeb58ddf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "input" { + type = list(object({ + id = string + value = string + })) +} + +resource "testing_resource" "data" { + count = length(var.input) + id = var.input[count.index].id + value = var.input[count.index].value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl new file mode 100644 index 0000000000..629cf6170c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/complex-inputs/main.tfstack.hcl @@ -0,0 +1,58 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "default" { + type = object({ + id = string + value = string + }) + default = { + id = "cec9bc39" + value = "hello, mercury!" + } +} + +variable "optional_default" { + type = object({ + id = optional(string) + value = optional(string, "hello, venus!") + }) + default = { + id = "78d8b3d7" + } +} + +variable "optional" { + type = object({ + id = optional(string) + value = optional(string, "hello, earth!") + }) +} + +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } + inputs = { + input = [ + var.default, + var.optional_default, + var.optional, + ] + } +} + +stack "child" { + source = "./child" + + inputs = { + optional = {} + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/component-chain/component-chain.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/component-chain/component-chain.tf new file mode 100644 index 0000000000..294393f1f2 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/component-chain/component-chain.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "value" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.value +} + +output "value" { + value = testing_resource.data.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/component-chain/component-chain.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/component-chain/component-chain.tfstack.hcl new file mode 100644 index 0000000000..799cd57df1 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/component-chain/component-chain.tfstack.hcl @@ -0,0 +1,56 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "value" { + type = string +} + +component "one" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "one" + value = var.value + } +} + +component "two" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "two" + value = component.one.value + } +} + +component "three" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "three" + value = component.two.value + } +} + +output "value" { + value = component.three.value + type = string +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/component-input-output/component-input-output.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/component-input-output/component-input-output.tfstack.hcl new file mode 100644 index 0000000000..1396467816 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/component-input-output/component-input-output.tfstack.hcl @@ -0,0 +1,8 @@ +variable "value" { + type = string +} + +output "value" { + type = string + value = var.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/deferrable-component/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/deferrable-component/main.tf new file mode 100644 index 0000000000..ced7f7834f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/deferrable-component/main.tf @@ -0,0 +1,24 @@ + +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "defer" { + type = bool +} + +resource "testing_deferred_resource" "data" { + id = var.id + deferred = var.defer +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/deferrable-component/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/deferrable-component/main.tfstack.hcl new file mode 100644 index 0000000000..3cd97d237a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/deferrable-component/main.tfstack.hcl @@ -0,0 +1,31 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "id" { + type = string +} + +variable "defer" { + type = bool +} + +component "self" { + source = "./" + + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + defer = var.defer + } + +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/deferred-component-for-each-from-component-of-invalid-type.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/deferred-component-for-each-from-component-of-invalid-type.tfstack.hcl new file mode 100644 index 0000000000..4ace97c011 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/deferred-component-for-each-from-component-of-invalid-type.tfstack.hcl @@ -0,0 +1,37 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + + +provider "testing" "default" {} + + +component "parent" { + source = "./parent" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = "parent" + } +} + + +component "self" { + source = "./self" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = each.value + } + + for_each = component.parent.letters_in_id // This is a list and no set or map +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/parent/parent.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/parent/parent.tf new file mode 100644 index 0000000000..006a3818fe --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/parent/parent.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} + +output "letters_in_id" { + value = split("", testing_resource.data.id) +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/self/self.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/self/self.tf new file mode 100644 index 0000000000..4da49727a5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-component-for-each-from-component-of-invalid-type/self/self.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-dependent/deferred-dependent.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-dependent/deferred-dependent.tfstack.hcl new file mode 100644 index 0000000000..f46173bc7a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/deferred-dependent/deferred-dependent.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "valid" { + source = "../with-single-input" + + providers = { + testing = provider.testing.default + } + inputs = { + id = "valid" + input = "valid" + } +} + +component "deferred" { + source = "../deferrable-component" + providers = { + testing = provider.testing.default + } + inputs = { + id = "deferred" + defer = true + } + depends_on = [ + component.valid + ] +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/dependent-component/dependent-component.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/dependent-component/dependent-component.tf new file mode 100644 index 0000000000..92aee222b3 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/dependent-component/dependent-component.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "requirements" { + type = set(string) +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +resource "testing_blocked_resource" "resource" { + id = var.id + required_resources = var.requirements +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/dependent-component/dependent-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/dependent-component/dependent-component.tfstack.hcl new file mode 100644 index 0000000000..c8f35b4a89 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/dependent-component/dependent-component.tfstack.hcl @@ -0,0 +1,37 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "valid" { + source = "../with-single-input" + providers = { + testing = provider.testing.default + } + inputs = { + id = "valid" + input = "resource" + } +} + +// this component must be created after component.valid +// this component must be destroyed before component.valid +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } + inputs = { + id = "dependent" + requirements = [ + "valid" + ] + } + depends_on = [ + component.valid + ] +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/child/child.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/child/child.tf new file mode 100644 index 0000000000..41dab5ad1d --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/child/child.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "key" { + type = string +} + +resource "testing_resource" "primary" { + for_each = { + (var.key) = "primary" + } + id = each.value +} + + diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/main.tfstack.hcl new file mode 100644 index 0000000000..66416b6c54 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/main.tfstack.hcl @@ -0,0 +1,29 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + + +component "parent" { + source = "./parent" + + providers = { + testing = provider.testing.default + } +} + +component "child" { + source = "./child" + + providers = { + testing = provider.testing.default + } + + inputs = { + key = component.parent.deleted_id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/parent/parent.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/parent/parent.tf new file mode 100644 index 0000000000..529bc7a4d5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/destroy-partial-state/parent/parent.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "deleted" {} + +resource "testing_resource" "primary" {} + +resource "testing_resource" "secondary" { + value = testing_resource.primary.id +} + +resource "testing_resource" "depends_on_deleted" { + for_each = { + (testing_resource.deleted.id) = "tertiary" + } + id = each.value +} + +output "deleted_id" { + value = testing_resource.deleted.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf new file mode 100644 index 0000000000..3b77397fa0 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/empty-component.tf @@ -0,0 +1,7 @@ +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl new file mode 100644 index 0000000000..48e5e4959a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/missing-providers/missing-providers.tfstack.hcl @@ -0,0 +1,13 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" {} + +component "self" { + source = "../" + + providers = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl new file mode 100644 index 0000000000..5355ab8efb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty-component/valid-providers/valid-providers.tfstack.hcl @@ -0,0 +1,15 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" {} + +component "self" { + source = "../" + + providers = { + terraform = provider.terraform.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/empty/empty.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/empty/empty.tfstack.hcl new file mode 100644 index 0000000000..fc4dca0a5a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/empty/empty.tfstack.hcl @@ -0,0 +1 @@ +# This stack configuration is intentionally empty. Don't add anything here. diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/failed-component/failed-component.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-component/failed-component.tf new file mode 100644 index 0000000000..cfc09b78bb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-component/failed-component.tf @@ -0,0 +1,43 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string + default = null + nullable = true +} + +variable "fail_plan" { + type = bool + default = null + nullable = true +} + +variable "fail_apply" { + type = bool + default = null + nullable = true +} + +resource "testing_failed_resource" "data" { + id = var.id + value = var.input + fail_plan = var.fail_plan + fail_apply = var.fail_apply +} + +output "value" { + value = testing_failed_resource.data.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/failed-component/failed-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-component/failed-component.tfstack.hcl new file mode 100644 index 0000000000..6d9cbd6abf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-component/failed-component.tfstack.hcl @@ -0,0 +1,31 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "fail_plan" { + type = bool + default = false +} + +variable "fail_apply" { + type = bool + default = false +} + +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } + inputs = { + id = "failed" + input = "resource" + fail_plan = var.fail_plan + fail_apply = var.fail_apply + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/failed-dependency/failed-dependency.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-dependency/failed-dependency.tf new file mode 100644 index 0000000000..f834d56d9e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-dependency/failed-dependency.tf @@ -0,0 +1,57 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "failed_id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "resource_id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string + default = null + nullable = true +} + +variable "fail_plan" { + type = bool + default = null + nullable = true +} + +variable "fail_apply" { + type = bool + default = null + nullable = true +} + +resource "testing_failed_resource" "data" { + id = var.failed_id + value = var.input + fail_plan = var.fail_plan + fail_apply = var.fail_apply +} + +resource "testing_resource" "data" { + id = var.resource_id + + depends_on = [ + testing_failed_resource.data + ] +} + +output "value" { + value = testing_failed_resource.data.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/failed-dependency/failed-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-dependency/failed-dependency.tfstack.hcl new file mode 100644 index 0000000000..e17be6af7a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/failed-dependency/failed-dependency.tfstack.hcl @@ -0,0 +1,31 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "fail_plan" { + type = bool + default = false +} + +variable "fail_apply" { + type = bool + default = false +} + +component "self" { + source = "./" + providers = { + testing = provider.testing.default + } + inputs = { + failed_id = "failed" + resource_id = "resource" + fail_plan = var.fail_plan + fail_apply = var.fail_apply + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/child/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/child/main.tf new file mode 100644 index 0000000000..699ea441d9 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/child/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "input" { + type = string +} + +resource "testing_resource" "child_data" { + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/main.tf new file mode 100644 index 0000000000..be3544c92f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/main.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "root_id" { + value = "root_value" +} + +module "child_module" { + source = "./child" + input = "child_input" +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/main.tfstack.hcl new file mode 100644 index 0000000000..867ec1b13e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/child-module-as-component-source/main.tfstack.hcl @@ -0,0 +1,32 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + } +} + +component "triage" { + source = "./child" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "triage" + input = "triage_input" + } +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/input-dependency/input-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/input-dependency/input-dependency.tfstack.hcl new file mode 100644 index 0000000000..bb544fac32 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/input-dependency/input-dependency.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "parent" + input = "parent" + } +} + +component "child" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "child" + input = component.parent.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/main.tf new file mode 100644 index 0000000000..442680e979 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/main.tf @@ -0,0 +1,33 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} + +resource "testing_resource" "another" { + count = 2 + id = var.id + value = var.input +} + +output "id" { + value = testing_resource.data.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/provider-dependency/provider-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/provider-dependency/provider-dependency.tfstack.hcl new file mode 100644 index 0000000000..f522b0774f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-dependency/provider-dependency/provider-dependency.tfstack.hcl @@ -0,0 +1,40 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "parent" + input = "parent" + } +} + +provider "testing" "dependent" { + config { + ignored = component.parent.id + } +} + +component "child" { + source = "../" + + providers = { + testing = provider.testing.dependent + } + + inputs = { + id = "child" + input = "child" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/depends-on.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/depends-on.tfstack.hcl new file mode 100644 index 0000000000..97005e6819 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/depends-on.tfstack.hcl @@ -0,0 +1,73 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +variable "empty" { + type = set(string) + default = [] +} + +component "empty" { + source = "./" + + for_each = var.empty + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} + +component "first" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} + +component "second" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } + + depends_on = [component.first, stack.embedded] +} + +stack "embedded" { + source = "./valid" + + inputs = { + input = var.input + } +} + +# stack "second" { +# source = "./valid" + +# inputs = { +# input = var.input +# } +# } diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/main.tf new file mode 100644 index 0000000000..b0e40a9d62 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/main.tf @@ -0,0 +1,33 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} + +resource "testing_resource" "second" { + id = var.id + value = var.input +} + +resource "testing_resource" "third" { + id = var.id + value = var.input +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/valid/valid.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/valid/valid.tfstack.hcl new file mode 100644 index 0000000000..969672f3fb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-depends-on/valid/valid.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +variable "id" { + type = string + default = null +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/child/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/child/main.tf new file mode 100644 index 0000000000..eaa61b17b5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/child/main.tf @@ -0,0 +1,33 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "child_data" { + id = var.id + value = var.input +} + +resource "testing_resource" "another_child_data" { + count = 2 + id = var.id + value = var.input +} + +output "id" { + value = testing_resource.data.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/input-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/input-dependency.tfstack.hcl new file mode 100644 index 0000000000..cbea525b64 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/input-dependency.tfstack.hcl @@ -0,0 +1,47 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "parent" + input = "parent" + } +} + +component "child" { + source = "./child" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "child" + input = component.parent.id + } +} + +component "child2" { + source = "./child" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "child" + input = component.parent.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/main.tf new file mode 100644 index 0000000000..965fadc2e9 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/for-stacks-migrate/with-nested-module/main.tf @@ -0,0 +1,47 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} + +resource "testing_resource" "another" { + count = 2 + id = var.id + value = var.input +} + +module "child_mod" { + source = "./child" + input = var.input + providers = { + testing = testing + } +} + +module "child_mod2" { + source = "./child" + input = var.input + # provider block not passed in here +} + +output "id" { + value = testing_resource.data.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency/forget_with_dependency.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency/forget_with_dependency.tf new file mode 100644 index 0000000000..8e7a20e79c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency/forget_with_dependency.tf @@ -0,0 +1,11 @@ +variable "value" { + type = string +} + +resource "testing_resource" "resource" { + value = var.value +} + +output "id" { + value = testing_resource.resource.id +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency/forget_with_dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency/forget_with_dependency.tfstack.hcl new file mode 100644 index 0000000000..aa90910c02 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency/forget_with_dependency.tfstack.hcl @@ -0,0 +1,33 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "one" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + value = "bar" + } +} + +removed { + source = "./" + from = component.two + + providers = { + testing = provider.testing.default + } + + lifecycle { + destroy = false + } +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency_to_forget/forget_with_dependency_to_forget.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency_to_forget/forget_with_dependency_to_forget.tf new file mode 100644 index 0000000000..8e7a20e79c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency_to_forget/forget_with_dependency_to_forget.tf @@ -0,0 +1,11 @@ +variable "value" { + type = string +} + +resource "testing_resource" "resource" { + value = var.value +} + +output "id" { + value = testing_resource.resource.id +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency_to_forget/forget_with_dependency_to_forget.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency_to_forget/forget_with_dependency_to_forget.tfstack.hcl new file mode 100644 index 0000000000..0cbfe5801f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/forget_with_dependency_to_forget/forget_with_dependency_to_forget.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +removed { + source = "./" + from = component.one + + providers = { + testing = provider.testing.default + } + + lifecycle { + destroy = false + } +} + +removed { + source = "./" + from = component.two + + providers = { + testing = provider.testing.default + } + + lifecycle { + destroy = false + } +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-configuration/invalid-configuration.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-configuration/invalid-configuration.tf new file mode 100644 index 0000000000..71bc6818db --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-configuration/invalid-configuration.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "data" { + invalid = "this should fail validation" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-configuration/invalid-configuration.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-configuration/invalid-configuration.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-configuration/invalid-configuration.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-providers/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-providers/main.tf new file mode 100644 index 0000000000..6bb2a0f28b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-providers/main.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "data" {} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-providers/main.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-providers/main.tfstack.hcl new file mode 100644 index 0000000000..1699a617bf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/invalid-providers/main.tfstack.hcl @@ -0,0 +1,22 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" { + config { + // This provider is going to fail to configure. + configure_error = "invalid configuration" + } +} + + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/README.md b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/README.md new file mode 100644 index 0000000000..fcf72b236f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/README.md @@ -0,0 +1,5 @@ +# legacy-module + +This "legacy module" is so named because it lacks a `required_providers` block +in its configuration. This means that all provider information must be deduced +from the calling stack components. diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/main.tf new file mode 100644 index 0000000000..9dcd74cfbc --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/main.tf @@ -0,0 +1,14 @@ +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/with-hashicorp-provider/with-hashicorp-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/with-hashicorp-provider/with-hashicorp-provider.tfstack.hcl new file mode 100644 index 0000000000..040050309f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/with-hashicorp-provider/with-hashicorp-provider.tfstack.hcl @@ -0,0 +1,24 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/with-non-hashicorp-provider/with-non-hashicorp-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/with-non-hashicorp-provider/with-non-hashicorp-provider.tfstack.hcl new file mode 100644 index 0000000000..0d690fb13e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/legacy-module/with-non-hashicorp-provider/with-non-hashicorp-provider.tfstack.hcl @@ -0,0 +1,27 @@ +required_providers { + testing = { + source = "other/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + # We don't actually specify a provider type for testing in the underlying + # module. Terraform will assume it's a HashiCorp provider, but it's not. + # This should cause an error with a reasonable message. + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/for-each-input/for-each-input.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/for-each-input/for-each-input.tf new file mode 100644 index 0000000000..af99659240 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/for-each-input/for-each-input.tf @@ -0,0 +1,7 @@ +variable "input" { + type = string +} + +output "value" { + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/for-each-input/for-each-input.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/for-each-input/for-each-input.tfstack.hcl new file mode 100644 index 0000000000..044b7e9f77 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/for-each-input/for-each-input.tfstack.hcl @@ -0,0 +1,38 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "inputs" { + type = map(string) +} + +provider "testing" "default" {} + +component "self" { + for_each = var.inputs + + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = each.value + } +} + +component "main" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = component.self + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/map-object-input.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/map-object-input.tf new file mode 100644 index 0000000000..80ec7dc2c6 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/map-object-input/map-object-input.tf @@ -0,0 +1,12 @@ + +variable "input" { + type = map(object({ + output = string + })) +} + +resource "testing_resource" "main" { + for_each = var.input + id = each.key + value = each.value.output +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/plan-no-value-for-required-variable/unset-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-no-value-for-required-variable/unset-variable.tfstack.hcl new file mode 100644 index 0000000000..d02969f012 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-no-value-for-required-variable/unset-variable.tfstack.hcl @@ -0,0 +1,3 @@ +variable "beep" { + type = string +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/plan-undeclared-variable-in-component/undeclared-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-undeclared-variable-in-component/undeclared-variable.tfstack.hcl new file mode 100644 index 0000000000..c039268e82 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-undeclared-variable-in-component/undeclared-variable.tfstack.hcl @@ -0,0 +1,19 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" {} + +component "self" { + source = "./" + + providers = { + terraform = provider.terraform.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/plan-undeclared-variable-in-component/with-single-input.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-undeclared-variable-in-component/with-single-input.tf new file mode 100644 index 0000000000..e3de4867e6 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-undeclared-variable-in-component/with-single-input.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} + +variable "input" { + type = string +} + +resource "terraform_data" "main" { + input = var.input +} + +output "output" { + value = terraform_data.main.output +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/child/child.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/child/child.tfstack.hcl new file mode 100644 index 0000000000..7fa976ffda --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/child/child.tfstack.hcl @@ -0,0 +1,9 @@ +variable "boop" { + type = string + default = "BOOP" +} + +output "result" { + type = string + value = var.boop +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/deployments.tfdeploy.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/deployments.tfdeploy.hcl new file mode 100644 index 0000000000..811bbfdf56 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/deployments.tfdeploy.hcl @@ -0,0 +1,3 @@ +deployment "main" { + inputs = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/plan-variable-default.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/plan-variable-default.tfstack.hcl new file mode 100644 index 0000000000..37d763eeef --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/plan-variable-defaults/plan-variable-default.tfstack.hcl @@ -0,0 +1,31 @@ +variable "beep" { + type = string + default = "BEEP" +} + +output "beep" { + type = string + value = var.beep +} + +stack "specified" { + source = "./child" + inputs = { + boop = var.beep + } +} + +stack "defaulted" { + source = "./child" + inputs = {} +} + +output "specified" { + type = string + value = stack.specified.result +} + +output "defaulted" { + type = string + value = stack.defaulted.result +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/child/child.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/child/child.tf new file mode 100644 index 0000000000..302855cac6 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/child/child.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "value" { + type = string +} + +resource "testing_resource" "resource" { + value = var.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/parent/parent.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/parent/parent.tf new file mode 100644 index 0000000000..30b6659dc5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/parent/parent.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "resource" {} + +output "value" { + value = testing_resource.resource.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/removed-offline.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/removed-offline.tfstack.hcl new file mode 100644 index 0000000000..4e47135151 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/removed-offline/removed-offline.tfstack.hcl @@ -0,0 +1,28 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "./parent" + + providers = { + testing = provider.testing.default + } +} + +component "child" { + source = "./child" + + providers = { + testing = provider.testing.default + } + + inputs = { + value = component.parent.value + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/resource-identity/resource-identity.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/resource-identity/resource-identity.tf new file mode 100644 index 0000000000..3966256bd6 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/resource-identity/resource-identity.tf @@ -0,0 +1,7 @@ +variable "name" { + type = string +} + +resource "testing_resource_with_identity" "hello" { + id = var.name +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/resource-identity/resource-identity.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/resource-identity/resource-identity.tfstack.hcl new file mode 100644 index 0000000000..11a8b41e73 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/resource-identity/resource-identity.tfstack.hcl @@ -0,0 +1,19 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "main" {} + +component "self" { + source = "./" + inputs = { + name = "example" + } + + providers = { + testing = provider.testing.main + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf new file mode 100644 index 0000000000..797d7db5c6 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tf @@ -0,0 +1,8 @@ +variable "secret" { + type = string +} + +output "result" { + value = sensitive(upper(var.secret)) + sensitive = true +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl new file mode 100644 index 0000000000..7d9d932a5b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-as-input/sensitive-output-as-input.tfstack.hcl @@ -0,0 +1,19 @@ +stack "sensitive" { + source = "../sensitive-output" + + inputs = { + } +} + +component "self" { + source = "./" + + inputs = { + secret = stack.sensitive.result + } +} + +output "result" { + type = string + value = component.self.result +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl new file mode 100644 index 0000000000..58aa9630bb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output-nested/sensitive-output-nested.tfstack.hcl @@ -0,0 +1,11 @@ +stack "child" { + source = "../sensitive-output" + + inputs = { + } +} + +output "result" { + type = string + value = stack.child.result +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf new file mode 100644 index 0000000000..1031c01a58 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tf @@ -0,0 +1,4 @@ +output "out" { + value = sensitive("secret") + sensitive = true +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl new file mode 100644 index 0000000000..1df80f4d0c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/sensitive-output/sensitive-output.tfstack.hcl @@ -0,0 +1,10 @@ +component "self" { + source = "./" + inputs = { + } +} + +output "result" { + type = string + value = component.self.out +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/cross-type-moved/cross-type-moved.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/cross-type-moved/cross-type-moved.tf new file mode 100644 index 0000000000..814d7f5682 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/cross-type-moved/cross-type-moved.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +moved { + from = testing_resource.before + to = testing_deferred_resource.after +} + +resource "testing_deferred_resource" "after" { + id = "moved" + value = "moved" + deferred = false +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/cross-type-moved/cross-type-moved.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/cross-type-moved/cross-type-moved.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/cross-type-moved/cross-type-moved.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/deferred.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/deferred.tfstack.hcl new file mode 100644 index 0000000000..4d61b7e473 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/deferred.tfstack.hcl @@ -0,0 +1,24 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "ok" { + source = "./ok" + + providers = { + testing = provider.testing.default + } +} + +component "deferred" { + source = "./deferred" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/deferred/deferred.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/deferred/deferred.tf new file mode 100644 index 0000000000..4a1e84af45 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/deferred/deferred.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_deferred_resource" "resource" { + id = "hello" + value = "world" + deferred = true +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/ok/ok.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/ok/ok.tf new file mode 100644 index 0000000000..4ece07d8e4 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/deferred/ok/ok.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "self" { + id = "ok" + value = "ok" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import-failed-dep/import.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import-failed-dep/import.tf new file mode 100644 index 0000000000..b15624212a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import-failed-dep/import.tf @@ -0,0 +1,28 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string +} + +resource "testing_failed_resource" "resource" { + fail_apply = true +} + +import { + to = testing_resource.data + id = var.id +} + +resource "testing_resource" "data" { + id = var.id + value = "imported" + + depends_on = [testing_failed_resource.resource] +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import-failed-dep/import.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import-failed-dep/import.tfstack.hcl new file mode 100644 index 0000000000..b0cf34642a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import-failed-dep/import.tfstack.hcl @@ -0,0 +1,24 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "id" { + type = string +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import/import.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import/import.tf new file mode 100644 index 0000000000..ee05e4b2ae --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import/import.tf @@ -0,0 +1,22 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string +} + +import { + to = testing_resource.data + id = var.id +} + +resource "testing_resource" "data" { + id = var.id + value = "imported" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import/import.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import/import.tfstack.hcl new file mode 100644 index 0000000000..b0cf34642a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/import/import.tfstack.hcl @@ -0,0 +1,24 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "id" { + type = string +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved-failed-dep/moved.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved-failed-dep/moved.tf new file mode 100644 index 0000000000..e36a651c1c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved-failed-dep/moved.tf @@ -0,0 +1,24 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_failed_resource" "resource" { + fail_apply = true +} + +moved { + from = testing_resource.before + to = testing_resource.after +} + +resource "testing_resource" "after" { + id = "moved" + value = "moved" + + depends_on = [testing_failed_resource.resource] +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved-failed-dep/moved.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved-failed-dep/moved.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved-failed-dep/moved.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved/moved.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved/moved.tf new file mode 100644 index 0000000000..f5ec7e09fc --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved/moved.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +moved { + from = testing_resource.before + to = testing_resource.after +} + +resource "testing_resource" "after" { + id = "moved" + value = "moved" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved/moved.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved/moved.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/moved/moved.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed-failed-dep/removed.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed-failed-dep/removed.tf new file mode 100644 index 0000000000..285352032c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed-failed-dep/removed.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_failed_resource" "resource" { + fail_apply = true +} + +removed { + from = testing_resource.resource + + lifecycle { + destroy = false + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed-failed-dep/removed.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed-failed-dep/removed.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed-failed-dep/removed.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed/removed.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed/removed.tf new file mode 100644 index 0000000000..e62ae9a67e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed/removed.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +removed { + from = testing_resource.resource + + lifecycle { + destroy = false + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed/removed.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed/removed.tfstack.hcl new file mode 100644 index 0000000000..18c62cbcbf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/state-manipulation/removed/removed.tfstack.hcl @@ -0,0 +1,16 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/validate-cyclic-dependency/main.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-cyclic-dependency/main.tf new file mode 100644 index 0000000000..e69de29bb2 diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/validate-cyclic-dependency/validate-cyclic-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-cyclic-dependency/validate-cyclic-dependency.tfstack.hcl new file mode 100644 index 0000000000..e7e747a23e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-cyclic-dependency/validate-cyclic-dependency.tfstack.hcl @@ -0,0 +1,18 @@ +locals { + foo = "bar" +} + +component "vault-config" { + source = "./" + inputs = { + ssh_key_private = component.boundary.ssh_key_private + bar = local.foo + } +} + +component "boundary" { + source = "./" + inputs = { + boundary_vault_token = component.vault-config.boundary_vault_token + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/validate-embedded-stack-selfref/child/validate-embedded-stack-selfref-child.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-embedded-stack-selfref/child/validate-embedded-stack-selfref-child.tfstack.hcl new file mode 100644 index 0000000000..bc65877d5c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-embedded-stack-selfref/child/validate-embedded-stack-selfref-child.tfstack.hcl @@ -0,0 +1,8 @@ +variable "a" { + type = string +} + +output "a" { + type = string + value = var.a +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/validate-embedded-stack-selfref/validate-embedded-stack-selfref.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-embedded-stack-selfref/validate-embedded-stack-selfref.tfstack.hcl new file mode 100644 index 0000000000..115c5d3daf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-embedded-stack-selfref/validate-embedded-stack-selfref.tfstack.hcl @@ -0,0 +1,8 @@ + +stack "a" { + source = "./child" + + inputs = { + a = stack.a.a + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/validate-undeclared-variable/validate-undeclared-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-undeclared-variable/validate-undeclared-variable.tfstack.hcl new file mode 100644 index 0000000000..6bce8e3701 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/validate-undeclared-variable/validate-undeclared-variable.tfstack.hcl @@ -0,0 +1,4 @@ +output "a" { + type = string + value = var.a +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/variable-ephemeral/variable-ephemeral.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/variable-ephemeral/variable-ephemeral.tfstack.hcl new file mode 100644 index 0000000000..1ded833961 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/variable-ephemeral/variable-ephemeral.tfstack.hcl @@ -0,0 +1,10 @@ +variable "eph" { + type = string + default = null + ephemeral = true +} + +variable "noneph" { + type = string + default = null +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/variable-output-roundtrip-nested/variable-output-roundtrip-nested.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/variable-output-roundtrip-nested/variable-output-roundtrip-nested.tfstack.hcl new file mode 100644 index 0000000000..27b55d56ce --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/variable-output-roundtrip-nested/variable-output-roundtrip-nested.tfstack.hcl @@ -0,0 +1,17 @@ +variable "msg" { + type = string + default = "default" +} + +stack "child" { + source = "../variable-output-roundtrip" + + inputs = { + msg = var.msg + } +} + +output "msg" { + type = string + value = stack.child.msg +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/variable-output-roundtrip/variable-output-roundtrip.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/variable-output-roundtrip/variable-output-roundtrip.tfstack.hcl new file mode 100644 index 0000000000..ee04bb4966 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/variable-output-roundtrip/variable-output-roundtrip.tfstack.hcl @@ -0,0 +1,9 @@ +variable "msg" { + type = string + default = "default" +} + +output "msg" { + type = string + value = var.msg +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/deferred-provider-for-each/deferred-provider-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/deferred-provider-for-each/deferred-provider-for-each.tfstack.hcl new file mode 100644 index 0000000000..dbad796aaa --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/deferred-provider-for-each/deferred-provider-for-each.tfstack.hcl @@ -0,0 +1,45 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "providers" { + type = set(string) +} + +provider "testing" "main" { + for_each = var.providers +} + +provider "testing" "const" {} + +component "main" { + source = "../" + + for_each = var.providers + + providers = { + // We're testing an unknown component referencing a known provider here. + testing = provider.testing.main[each.key] + } + + inputs = { + id = "data_unknown" + resource = "resource_unknown" + } +} + +component "const" { + source = "../" + + providers = { + testing = provider.testing.const + } + + inputs = { + id = "data_known" + resource = "resource_known" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/dependent/dependent.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/dependent/dependent.tf new file mode 100644 index 0000000000..df77bf1a74 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/dependent/dependent.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string +} + +resource "testing_resource" "data" { + count = 1 + id = var.id +} + +output "id" { + value = try(testing_resource.data[0].id, null) +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/dependent/dependent.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/dependent/dependent.tfstack.hcl new file mode 100644 index 0000000000..d15a15b3ac --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/dependent/dependent.tfstack.hcl @@ -0,0 +1,37 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "id" { + type = string +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + } +} + +component "data" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = component.self.id + resource = "resource" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/with-data-source.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/with-data-source.tf new file mode 100644 index 0000000000..c5e968fffe --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/with-data-source.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string +} + +variable "resource" { + type = string +} + +data "testing_data_source" "data" { + id = var.id +} + +resource "testing_resource" "data" { + id = var.resource + value = data.testing_data_source.data.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/with-data-source.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/with-data-source.tfstack.hcl new file mode 100644 index 0000000000..4c01f0bdfd --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-data-source/with-data-source.tfstack.hcl @@ -0,0 +1,29 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "id" { + type = string +} + +variable "resource" { + type = string +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + resource = var.resource + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/module/module.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/module/module.tf new file mode 100644 index 0000000000..4da49727a5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/module/module.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tf new file mode 100644 index 0000000000..0bbca09ac7 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tf @@ -0,0 +1,44 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "create" { + type = bool + default = true +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "resource" { + count = var.create ? 1 : 0 +} + + +module "module" { + source = "./module" + + providers = { + testing = testing + } + + id = testing_resource.resource[0].id + input = var.input +} + +resource "testing_resource" "outside" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tfstack.hcl new file mode 100644 index 0000000000..7d1db6d2ab --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-module/with-module.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +variable "id" { + type = string + default = null +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/deployments.tfdeploy.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/deployments.tfdeploy.hcl new file mode 100644 index 0000000000..811bbfdf56 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/deployments.tfdeploy.hcl @@ -0,0 +1,3 @@ +deployment "main" { + inputs = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/with-timestamp.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/with-timestamp.tf new file mode 100644 index 0000000000..6426f2b9e3 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/with-timestamp.tf @@ -0,0 +1,11 @@ +output "out" { + value = "module-output-${plantimestamp()}" +} + +variable "value" { + type = string +} + +output "input" { + value = var.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/with-timestamp.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/with-timestamp.tfstack.hcl new file mode 100644 index 0000000000..dddc9e9870 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-plantimestamp/with-timestamp.tfstack.hcl @@ -0,0 +1,18 @@ +component "self" { + source = "./" + inputs = { + value = plantimestamp() + } +} + +component "second-self" { + source = "./" + inputs = { + value = plantimestamp() + } +} + +output "plantimestamp" { + type = string + value = plantimestamp() +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-config/with-provider-config.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-config/with-provider-config.tfstack.hcl new file mode 100644 index 0000000000..a1858e31d4 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-config/with-provider-config.tfstack.hcl @@ -0,0 +1,17 @@ + +required_providers { + test = { + source = "example.com/test/test" + version = "1.0.0" + } +} + +variable "name" { + type = string +} + +provider "test" "foo" { + config { + name = var.name + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-functions/with-provider-functions.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-functions/with-provider-functions.tf new file mode 100644 index 0000000000..aaae6a94cb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-functions/with-provider-functions.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = provider::testing::echo(var.input) +} + +output "value" { + value = testing_resource.data.value +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-functions/with-provider-functions.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-functions/with-provider-functions.tfstack.hcl new file mode 100644 index 0000000000..c907ca1740 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-provider-functions/with-provider-functions.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "./" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "2f9f3b84" + input = provider::testing::echo(var.input) + } +} + +output "value" { + type = string + value = provider::testing::echo(component.self.value) +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/deferred-component-for-each/deferred-component-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/deferred-component-for-each/deferred-component-for-each.tfstack.hcl new file mode 100644 index 0000000000..acf459e586 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/deferred-component-for-each/deferred-component-for-each.tfstack.hcl @@ -0,0 +1,48 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "components" { + type = set(string) +} + +provider "testing" "default" {} + +component "self" { + // This component validates the behaviour of an unknown for_each value. + + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = each.value + } + + for_each = var.components +} + +component "child" { + // This component validates the behaviour of referencing a partial component + // with a known key. Since we don't know the available keys of the component + // yet, this should use the outputs of the partial instance. + + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + // It's really unlikely that `const` is actually going to exist once + // component.self has known keys, but for now we don't know it doesn't + // exist so we should defer this component and make a reasonable attempt + // at planning something. + input = component.self["const"].id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/deferred-component-references/deferred-component-references.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/deferred-component-references/deferred-component-references.tfstack.hcl new file mode 100644 index 0000000000..4c26bf2eb1 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/deferred-component-references/deferred-component-references.tfstack.hcl @@ -0,0 +1,48 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "known_components" { + type = set(string) +} + +variable "unknown_components" { + type = set(string) +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = each.value + } + + for_each = var.known_components +} + +component "children" { + // This component validates the behaviour of referencing a known component + // with an unknown key. + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + // each.key is unknown, but we should still get a typed reference to an + // output here so we can plan using unknown values. + input = component.self[each.key].id + } + + for_each = var.unknown_components +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/for-each-dependency/for-each-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/for-each-dependency/for-each-dependency.tfstack.hcl new file mode 100644 index 0000000000..39379673d8 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/for-each-dependency/for-each-dependency.tfstack.hcl @@ -0,0 +1,38 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + for_each = toset(["a"]) + + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = each.key + input = "parent" + } +} + +component "child" { + for_each = toset([ for c in component.parent : c.id ]) + + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "child:${each.key}" + input = "child" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/input-dependency/input-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/input-dependency/input-dependency.tfstack.hcl new file mode 100644 index 0000000000..bb544fac32 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/input-dependency/input-dependency.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "parent" + input = "parent" + } +} + +component "child" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "child" + input = component.parent.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/provider-dependency/provider-dependency.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/provider-dependency/provider-dependency.tfstack.hcl new file mode 100644 index 0000000000..f522b0774f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/provider-dependency/provider-dependency.tfstack.hcl @@ -0,0 +1,40 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "parent" + input = "parent" + } +} + +provider "testing" "dependent" { + config { + ignored = component.parent.id + } +} + +component "child" { + source = "../" + + providers = { + testing = provider.testing.dependent + } + + inputs = { + id = "child" + input = "child" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/single-input-and-output.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/single-input-and-output.tf new file mode 100644 index 0000000000..c5ddc84f76 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input-and-output/single-input-and-output.tf @@ -0,0 +1,27 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} + +output "id" { + value = testing_resource.data.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-component-for-each/deferred-component-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-component-for-each/deferred-component-for-each.tfstack.hcl new file mode 100644 index 0000000000..be31797b6f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-component-for-each/deferred-component-for-each.tfstack.hcl @@ -0,0 +1,26 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "components" { + type = set(string) +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = each.value + } + + for_each = var.components +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-embedded-stack-and-component-for-each/deferred-embedded-stack-and-component-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-embedded-stack-and-component-for-each/deferred-embedded-stack-and-component-for-each.tfstack.hcl new file mode 100644 index 0000000000..232d7f1257 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-embedded-stack-and-component-for-each/deferred-embedded-stack-and-component-for-each.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "stacks" { + type = map(set(string)) +} + +provider "testing" "default" {} + +stack "a" { + source = "../deferred-component-for-each" + for_each = var.stacks + + inputs = { + components = each.value + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-embedded-stack-for-each/deferred-embedded-stack-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-embedded-stack-for-each/deferred-embedded-stack-for-each.tfstack.hcl new file mode 100644 index 0000000000..ff68107d0e --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-embedded-stack-for-each/deferred-embedded-stack-for-each.tfstack.hcl @@ -0,0 +1,22 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "stacks" { + type = map(string) +} + +provider "testing" "default" {} + +stack "a" { + source = "../valid" + for_each = var.stacks + + inputs = { + id = each.key + input = each.value + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-provider-for-each/deferred-provider-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-provider-for-each/deferred-provider-for-each.tfstack.hcl new file mode 100644 index 0000000000..73f143816b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/deferred-provider-for-each/deferred-provider-for-each.tfstack.hcl @@ -0,0 +1,46 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "providers" { + type = set(string) +} + +provider "testing" "unknown" { + for_each = var.providers +} + +provider "testing" "known" { + for_each = toset(["primary"]) +} + +component "known" { + source = "../" + + providers = { + // We're testing a known component referencing an unknown provider here. + testing = provider.testing.unknown["primary"] + } + + inputs = { + input = "primary" + } +} + +component "unknown" { + source = "../" + + for_each = var.providers + + providers = { + // We're testing an unknown component referencing a known provider here. + testing = provider.testing.known[each.key] + } + + inputs = { + input = "secondary" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl new file mode 100644 index 0000000000..855f895c0c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl @@ -0,0 +1,53 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +stack "first" { + source = "../valid" + + inputs = { + input = var.input + } + + # nu-uh, this isn't valid. + depends_on = [var.input, component.missing] +} + +component "first" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } + + # nu-uh, this isn't valid. + depends_on = [var.input, stack.missing] +} + +component "second" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } + + # nu-uh, this isn't valid. + depends_on = [component.first[1]] +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/depends-on/depends-on.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/depends-on/depends-on.tfstack.hcl new file mode 100644 index 0000000000..cfc7134fef --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/depends-on/depends-on.tfstack.hcl @@ -0,0 +1,77 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +variable "empty" { + type = set(string) + default = [] +} + +component "empty" { + source = "../" + + // This is a test to validate what happens when depending on a + // component that dynamically has no instances. + for_each = var.empty + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} + +component "first" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} + +component "second" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } + + depends_on = [component.first, stack.second] +} + +stack "first" { + source = "../valid" + + inputs = { + input = var.input + } + + depends_on = [component.first, component.empty] +} + +stack "second" { + source = "../valid" + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/ephemeral-default/ephemeral-default.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/ephemeral-default/ephemeral-default.tfstack.hcl new file mode 100644 index 0000000000..2daabfe19f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/ephemeral-default/ephemeral-default.tfstack.hcl @@ -0,0 +1,35 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" { + config { + ignored = var.ephemeral + } +} + +variable "ephemeral" { + type = string + default = "Hello, world!" + ephemeral = true +} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + id = "2f9f3b84" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/ephemeral/ephemeral.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/ephemeral/ephemeral.tfstack.hcl new file mode 100644 index 0000000000..195bed3fc9 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/ephemeral/ephemeral.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" { + config { + ignored = var.ephemeral + } +} + +variable "ephemeral" { + type = string + ephemeral = true +} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + id = "2f9f3b84" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-child/failed-child.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-child/failed-child.tfstack.hcl new file mode 100644 index 0000000000..5f642c7b8a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-child/failed-child.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + providers = { + testing = provider.testing.default + } + inputs = { + id = "self" + input = "value" + } +} + +component "child" { + source = "../../failed-component" + + providers = { + testing = provider.testing.default + } + inputs = { + input = "child" + fail_apply = true // This will cause the component to fail during apply. + } + depends_on = [ + component.self + ] +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-component-to-provider/failed-component-to-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-component-to-provider/failed-component-to-provider.tfstack.hcl new file mode 100644 index 0000000000..f232b5ffa8 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-component-to-provider/failed-component-to-provider.tfstack.hcl @@ -0,0 +1,35 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "../../failed-component" + + providers = { + testing = provider.testing.default + } + inputs = { + fail_apply = true // This will cause the component to fail during apply. + } +} + +provider "testing" "next" { + config { + configure_error = component.parent.value + } +} + +component "self" { + source = "../" + providers = { + testing = provider.testing.next + } + inputs = { + input = "Hello, world!" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-parent/failed-parent.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-parent/failed-parent.tfstack.hcl new file mode 100644 index 0000000000..dce69f8d15 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/failed-parent/failed-parent.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "parent" { + source = "../../failed-component" + + providers = { + testing = provider.testing.default + } + inputs = { + input = "Hello, world!" + fail_apply = true // This will cause the component to fail during apply. + } +} + +component "self" { + source = "../" + providers = { + testing = provider.testing.default + } + inputs = { + input = component.parent.value + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/for-each-component/for-each-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/for-each-component/for-each-component.tfstack.hcl new file mode 100644 index 0000000000..0a70326e9a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/for-each-component/for-each-component.tfstack.hcl @@ -0,0 +1,28 @@ + +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = map(string) +} + +component "self" { + for_each = var.input + + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = each.key + input = each.value + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/forgotten-component/forgotten-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/forgotten-component/forgotten-component.tfstack.hcl new file mode 100644 index 0000000000..11b3e1e103 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/forgotten-component/forgotten-component.tfstack.hcl @@ -0,0 +1,22 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +removed { + from = component.self + + source = "../" + + lifecycle { + destroy = false + } + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl new file mode 100644 index 0000000000..3996006c2f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component-list/input-from-component-list.tfstack.hcl @@ -0,0 +1,36 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "components" { + type = set(string) +} + +provider "testing" "default" {} + +component "output" { + source = "../../with-single-output" + + providers = { + testing = provider.testing.default + } + + for_each = var.components +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = component.output[each.value].id + } + + for_each = var.components +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl new file mode 100644 index 0000000000..ecb50730df --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-component/input-from-component.tfstack.hcl @@ -0,0 +1,28 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "output" { + source = "../../with-single-output" + + providers = { + testing = provider.testing.default + } +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = component.output.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl new file mode 100644 index 0000000000..750ceb3fe3 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + // This component doesn't exist. We should see an error. + input = component.output.id + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl new file mode 100644 index 0000000000..f8c4990368 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/input-from-provider/input-from-provider.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + # Shouldn't be able to reference providers from here. + input = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider-config/invalid-provider-config.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider-config/invalid-provider-config.tfstack.hcl new file mode 100644 index 0000000000..fbf7e3acde --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider-config/invalid-provider-config.tfstack.hcl @@ -0,0 +1,29 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" { + config { + // The `imaginary` attribute is not valid for the `testing` provider. + imaginary = "imaginary" + } +} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider-type/invalid-provider-type.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider-type/invalid-provider-type.tfstack.hcl new file mode 100644 index 0000000000..17593aabfd --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/invalid-provider-type/invalid-provider-type.tfstack.hcl @@ -0,0 +1,39 @@ +required_providers { + testing = { + source = "terraform.io/builtin/testing" + } + external = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + // Everything looks okay here, but the provider types are actually wrong. + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} + +removed { + from = component.removed + + source = "../" + + providers = { + // Everything looks okay here, but the provider types are actually wrong. + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-provider/missing-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-provider/missing-provider.tfstack.hcl new file mode 100644 index 0000000000..8eadab5021 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-provider/missing-provider.tfstack.hcl @@ -0,0 +1,32 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + # We do actually require a provider here, Validate() should warn us. + providers = {} + + inputs = { + input = var.input + } +} + +removed { + from = component.removed + + source = "../" + + # We do actually require a provider here, Validate() should warn us. + providers = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl new file mode 100644 index 0000000000..84c35a1f83 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/missing-variable/missing-variable.tfstack.hcl @@ -0,0 +1,23 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + # We do have a required variable, so this should complain. + inputs = {} +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/multiple-components/multiple-components.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/multiple-components/multiple-components.tfstack.hcl new file mode 100644 index 0000000000..cf4076afa5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/multiple-components/multiple-components.tfstack.hcl @@ -0,0 +1,34 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "one" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "one" + input = "one" + } +} + +component "two" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = "two" + input = "two" + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/multiple-components/removed/removed.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/multiple-components/removed/removed.tfstack.hcl new file mode 100644 index 0000000000..6823a34f0a --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/multiple-components/removed/removed.tfstack.hcl @@ -0,0 +1,11 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +removed { + from = stack.multiple + source = "../" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/orphaned-component/orphaned-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/orphaned-component/orphaned-component.tfstack.hcl new file mode 100644 index 0000000000..b0b65a73da --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/orphaned-component/orphaned-component.tfstack.hcl @@ -0,0 +1,19 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +removed { + // this is invalid, without a definition of the stack itself we can't remove + // components from it directly, instead we should removed the whole stack + from = stack.embedded.component.self + source = "../" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-for-each/provider-for-each.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-for-each/provider-for-each.tfstack.hcl new file mode 100644 index 0000000000..09b6b482bf --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-for-each/provider-for-each.tfstack.hcl @@ -0,0 +1,32 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "provider_set" { + type = set(string) + default = ["a", "b"] +} + +provider "testing" "configurations" { + for_each = var.provider_set +} + +variable "input" { + type = string +} + +component "self" { + source = "../" + for_each = var.provider_set + + providers = { + testing = provider.testing.configurations[each.value] + } + + inputs = { + input = var.input + } +} \ No newline at end of file diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-name-clash/provider-name-clash.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-name-clash/provider-name-clash.tfstack.hcl new file mode 100644 index 0000000000..42df655435 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/provider-name-clash/provider-name-clash.tfstack.hcl @@ -0,0 +1,26 @@ +required_providers { + other = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "other" "default" {} + +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + // Even though the names are wrong, the underlying types are the same + // so this should be okay. + testing = provider.other.default + } + + inputs = { + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-duplicate/removed-component-duplicate.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-duplicate/removed-component-duplicate.tfstack.hcl new file mode 100644 index 0000000000..026b39c7e7 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-duplicate/removed-component-duplicate.tfstack.hcl @@ -0,0 +1,59 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = set(string) +} + +variable "removed_one" { + type = set(string) +} + +variable "removed_two" { + type = set(string) +} + +component "self" { + source = "../" + + for_each = var.input + + providers = { + testing = provider.testing.default + } + + inputs = { + id = each.key + input = each.key + } +} + +removed { + from = component.self[each.key] + + source = "../" + + for_each = var.removed_one + + providers = { + testing = provider.testing.default + } +} + +removed { + from = component.self[each.key] + + source = "../" + + for_each = var.removed_two + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfstack.hcl new file mode 100644 index 0000000000..b0fdb23d4b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfstack.hcl @@ -0,0 +1,69 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "for_each_input" { + type = map(string) + default = {} +} + +variable "for_each_removed" { + type = set(string) + default = [] +} + +variable "simple_input" { + type = map(string) + default = {} +} + +variable "simple_removed" { + type = set(string) + default = [] +} + +stack "for_each" { + source = "../for-each-component" + + inputs = { + input = var.for_each_input + } +} + +removed { + for_each = var.for_each_removed + + from = stack.for_each.component.self[each.key] + source = "../" + + providers = { + testing = provider.testing.default + } +} + +stack "simple" { + for_each = var.simple_input + + source = "../valid" + + inputs = { + id = each.key + input = each.value + } +} + +removed { + for_each = var.simple_removed + + from = stack.simple[each.key].component.self + source = "../" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-from-stack/removed-component-from-stack.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-from-stack/removed-component-from-stack.tfstack.hcl new file mode 100644 index 0000000000..96a6272d74 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-from-stack/removed-component-from-stack.tfstack.hcl @@ -0,0 +1,25 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +stack "nested" { + source = "../for-each-component" + + inputs = { + input = {} + } +} + +removed { + from = stack.nested.component.self["foo"] + source = "../" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-instance-direct/removed-component-instance-direct.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-instance-direct/removed-component-instance-direct.tfstack.hcl new file mode 100644 index 0000000000..75f5173858 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-instance-direct/removed-component-instance-direct.tfstack.hcl @@ -0,0 +1,37 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = set(string) +} + +component "self" { + source = "../" + + for_each = var.input + + providers = { + testing = provider.testing.default + } + + inputs = { + id = each.key + input = each.key + } +} + +removed { + from = component.self["removed"] + + source = "../" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-instance/removed-component-instance.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-instance/removed-component-instance.tfstack.hcl new file mode 100644 index 0000000000..e8fb3ff8b0 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component-instance/removed-component-instance.tfstack.hcl @@ -0,0 +1,43 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = set(string) +} + +variable "removed" { + type = set(string) +} + +component "self" { + source = "../" + + for_each = var.input + + providers = { + testing = provider.testing.default + } + + inputs = { + id = each.key + input = each.key + } +} + +removed { + from = component.self[each.key] + + source = "../" + + for_each = var.removed + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component/removed-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component/removed-component.tfstack.hcl new file mode 100644 index 0000000000..ef91c478ec --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-component/removed-component.tfstack.hcl @@ -0,0 +1,18 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +removed { + from = component.self + + source = "../" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-embedded-component/removed-embedded-component.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-embedded-component/removed-embedded-component.tfstack.hcl new file mode 100644 index 0000000000..ca8b6cd0ca --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-embedded-component/removed-embedded-component.tfstack.hcl @@ -0,0 +1,11 @@ + +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +stack "a" { + source = "../removed-component" +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfstack.hcl new file mode 100644 index 0000000000..5a805d9d8c --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfstack.hcl @@ -0,0 +1,38 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "input" { + type = map(map(string)) + default = {} +} + +variable "removed" { + type = map(map(string)) + default = {} +} + +stack "embedded" { + source = "../removed-stack-instance-dynamic" + + for_each = var.input + + inputs = { + input = each.value + } +} + +removed { + for_each = var.removed + + from = stack.embedded[each.key].stack.simple[each.value["id"]] + source = "../valid" + + inputs = { + id = each.value["id"] + input = each.value["input"] + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl new file mode 100644 index 0000000000..107b1cbd92 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfstack.hcl @@ -0,0 +1,65 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = map(string) + default = {} +} + +variable "removed" { + type = map(string) + default = {} +} + +variable "removed-direct" { + type = set(string) + default = [] +} + +stack "simple" { + for_each = var.input + + source = "../valid" + + inputs = { + id = each.key + input = each.value + } +} + +removed { + for_each = var.removed + + // This removed block targets the stack directly, and just tells it to + // remove all components in the stack. + + from = stack.simple[each.key] + source = "../valid" + + inputs = { + id = each.key + input = each.value + } +} + +removed { + for_each = var.removed-direct + + // This removed block removes the component in the specified stack directly. + // This is okay as long as only a single component in the stack is being + // removed. If an entire stack is being removed, you should use the other + // approach. + + from = stack.simple[each.key].component.self + source = "../" + + providers = { + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/sensitive-input-nested/sensitive-input-nested.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/sensitive-input-nested/sensitive-input-nested.tfstack.hcl new file mode 100644 index 0000000000..55eecce413 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/sensitive-input-nested/sensitive-input-nested.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "id" { + type = string + default = null +} + +provider "testing" "default" {} + +stack "sensitive" { + source = "../../sensitive-output" +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + input = stack.sensitive.result + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/sensitive-input/sensitive-input.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/sensitive-input/sensitive-input.tfstack.hcl new file mode 100644 index 0000000000..87d3fd8f1f --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/sensitive-input/sensitive-input.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "id" { + type = string + default = null +} + +provider "testing" "default" {} + +component "sensitive" { + source = "../../sensitive-output" +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + input = component.sensitive.out + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/single-input.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/single-input.tf new file mode 100644 index 0000000000..4da49727a5 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/single-input.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "id" { + type = string + default = null + nullable = true # We'll generate an ID if none provided. +} + +variable "input" { + type = string +} + +resource "testing_resource" "data" { + id = var.id + value = var.input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl new file mode 100644 index 0000000000..44503088ad --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl @@ -0,0 +1,27 @@ +variable "input" { + type = string +} + +component "self" { + source = "../" + + providers = { + # We haven't provided a definition for this anywhere. + testing = provider.testing.default + } + + inputs = { + input = var.input + } +} + +removed { + from = component.removed + + source = "../" + + providers = { + # We haven't provided a definition for this anywhere. + testing = provider.testing.default + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl new file mode 100644 index 0000000000..37531f212b --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl @@ -0,0 +1,21 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + # var.input is not defined + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/valid/valid.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/valid/valid.tfstack.hcl new file mode 100644 index 0000000000..969672f3fb --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-input/valid/valid.tfstack.hcl @@ -0,0 +1,30 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +provider "testing" "default" {} + +variable "input" { + type = string +} + +variable "id" { + type = string + default = null +} + +component "self" { + source = "../" + + providers = { + testing = provider.testing.default + } + + inputs = { + id = var.id + input = var.input + } +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf new file mode 100644 index 0000000000..554f80a069 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-output/single-output.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +resource "testing_resource" "data" {} + +output "id" { + value = testing_resource.data.id +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf new file mode 100644 index 0000000000..b89447e101 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} + +resource "terraform_data" "main" { + input = "hello" +} + +output "input" { + value = terraform_data.main.input +} + +output "output" { + value = terraform_data.main.output +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl new file mode 100644 index 0000000000..2d3a30d087 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource-missing-provider/with-single-resource.tfstack.hcl @@ -0,0 +1,22 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" { +} + +component "self" { + source = "./" + + providers = {} +} + +output "obj" { + type = object({ + input = string + output = string + }) + value = component.self +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource/with-single-resource.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource/with-single-resource.tf new file mode 100644 index 0000000000..b89447e101 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource/with-single-resource.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} + +resource "terraform_data" "main" { + input = "hello" +} + +output "input" { + value = terraform_data.main.input +} + +output "output" { + value = terraform_data.main.output +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource/with-single-resource.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource/with-single-resource.tfstack.hcl new file mode 100644 index 0000000000..f50d3973d2 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-single-resource/with-single-resource.tfstack.hcl @@ -0,0 +1,24 @@ +required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } +} + +provider "terraform" "default" { +} + +component "self" { + source = "./" + + providers = { + terraform = provider.terraform.default + } +} + +output "obj" { + type = object({ + input = string + output = string + }) + value = component.self +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-write-only-attribute/with-write-only-attribute.tf b/internal/stacks/stackruntime/testdata/mainbundle/test/with-write-only-attribute/with-write-only-attribute.tf new file mode 100644 index 0000000000..3d05a71916 --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-write-only-attribute/with-write-only-attribute.tf @@ -0,0 +1,32 @@ +terraform { + required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } + } +} + +variable "datasource_id" { + type = string +} + +variable "resource_id" { + type = string +} + +variable "write_only_input" { + type = string + sensitive = true +} + +data "testing_write_only_data_source" "data" { + id = var.datasource_id + write_only = var.write_only_input +} + +resource "testing_write_only_resource" "data" { + id = var.resource_id + value = data.testing_write_only_data_source.data.value + write_only = var.write_only_input +} diff --git a/internal/stacks/stackruntime/testdata/mainbundle/test/with-write-only-attribute/with-write-only-attribute.tfstack.hcl b/internal/stacks/stackruntime/testdata/mainbundle/test/with-write-only-attribute/with-write-only-attribute.tfstack.hcl new file mode 100644 index 0000000000..39c88a5dbe --- /dev/null +++ b/internal/stacks/stackruntime/testdata/mainbundle/test/with-write-only-attribute/with-write-only-attribute.tfstack.hcl @@ -0,0 +1,28 @@ +required_providers { + testing = { + source = "hashicorp/testing" + version = "0.1.0" + } +} + +variable "providers" { + type = set(string) +} + +provider "testing" "main" { + for_each = var.providers +} + +component "main" { + source = "./" + + providers = { + testing = provider.testing.main["single"] + } + + inputs = { + datasource_id = "datasource" + resource_id = "resource" + write_only_input = "secret" + } +} diff --git a/internal/stacks/stackruntime/testing/provider.go b/internal/stacks/stackruntime/testing/provider.go new file mode 100644 index 0000000000..210d50b143 --- /dev/null +++ b/internal/stacks/stackruntime/testing/provider.go @@ -0,0 +1,365 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package testing + +import ( + "fmt" + "runtime/debug" + "testing" + + "github.com/hashicorp/go-uuid" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + TestingResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + } + + DeferredResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "deferred": {Type: cty.Bool, Required: true}, + }, + }, + } + + FailedResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "fail_plan": {Type: cty.Bool, Optional: true, Computed: true}, + "fail_apply": {Type: cty.Bool, Optional: true, Computed: true}, + }, + }, + } + + BlockedResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "required_resources": {Type: cty.Set(cty.String), Optional: true}, + }, + }, + } + + WriteOnlyResourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "write_only": {Type: cty.String, WriteOnly: true, Optional: true}, + }, + }, + } + + TestingDataSourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "value": {Type: cty.String, Computed: true}, + }, + }, + } + + WriteOnlyDataSourceSchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "value": {Type: cty.String, Computed: true}, + "write_only": {Type: cty.String, WriteOnly: true, Optional: true}, + }, + }, + } + + TestingResourceWithIdentitySchema = providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + }, + Nesting: configschema.NestingSingle, + }, + } +) + +// MockProvider wraps the standard MockProvider with a simple in-memory +// data store for resources and data sources. +type MockProvider struct { + *testing_provider.MockProvider + + ResourceStore *ResourceStore + + // If set, authentication means the configuration must provide a value + // that matches the value here otherwise the Configure function will + // fail. + Authentication string +} + +// NewProvider returns a new MockProvider with an empty data store. +func NewProvider(t *testing.T) *MockProvider { + provider := NewProviderWithData(t, NewResourceStore()) + return provider +} + +// NewProviderWithData returns a new MockProvider with the given data store. +func NewProviderWithData(t *testing.T, store *ResourceStore) *MockProvider { + if store == nil { + store = NewResourceStore() + } + + // grab the current stack trace so we know where the provider was created + // in case it isn't being cleaned up properly + currentStackTrace := debug.Stack() + + provider := &MockProvider{ + MockProvider: &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + // if the configuration sets require_auth then it + // must also provide the correct value for + // authentication + "authentication": { + Type: cty.String, + Sensitive: true, + Optional: true, + }, + "require_auth": { + Type: cty.Bool, + Optional: true, + }, + + // If this value is provider, the Configure + // function call will fail and return the value + // here as part of the error. + "configure_error": { + Type: cty.String, + Optional: true, + }, + + // ignored allows the configuration to create + // dependencies from this provider to component + // blocks and inputs without affecting behaviour. + "ignored": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "testing_resource": { + Body: TestingResourceSchema.Body, + }, + "testing_deferred_resource": { + Body: DeferredResourceSchema.Body, + }, + "testing_failed_resource": { + Body: FailedResourceSchema.Body, + }, + "testing_blocked_resource": { + Body: BlockedResourceSchema.Body, + }, + "testing_resource_with_identity": { + Body: TestingResourceSchema.Body, + Identity: TestingResourceWithIdentitySchema.Identity, + }, + "testing_write_only_resource": { + Body: WriteOnlyResourceSchema.Body, + }, + }, + DataSources: map[string]providers.Schema{ + "testing_data_source": { + Body: TestingDataSourceSchema.Body, + }, + "testing_write_only_data_source": { + Body: WriteOnlyDataSourceSchema.Body, + }, + }, + Functions: map[string]providers.FunctionDecl{ + "echo": { + Parameters: []providers.FunctionParam{ + {Name: "value", Type: cty.DynamicPseudoType}, + }, + ReturnType: cty.DynamicPseudoType, + }, + }, + ServerCapabilities: providers.ServerCapabilities{ + MoveResourceState: true, + }, + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return getResource(request.TypeName).Plan(request, store) + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return getResource(request.TypeName).Apply(request, store) + }, + ReadResourceFn: func(request providers.ReadResourceRequest) providers.ReadResourceResponse { + return getResource(request.TypeName).Read(request, store) + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + var diags tfdiags.Diagnostics + + id := request.Config.GetAttr("id") + if id.IsNull() { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "missing id", "id is required")) + return providers.ReadDataSourceResponse{ + Diagnostics: diags, + } + } + + value, exists := store.Get(id.AsString()) + if !exists { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%q not found", id))) + } + return providers.ReadDataSourceResponse{ + State: value, + Diagnostics: diags, + } + }, + ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + id := request.ID + value, exists := store.Get(id) + if !exists { + return providers.ImportResourceStateResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%q not found", id)), + }, + } + } + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: request.TypeName, + State: value, + }, + }, + } + }, + MoveResourceStateFn: func(request providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + if request.SourceTypeName != "testing_resource" && request.TargetTypeName != "testing_deferred_resource" { + return providers.MoveResourceStateResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "unsupported", "unsupported move"), + }, + } + } + // So, we know we're moving from `testing_resource` to + // `testing_deferred_resource`. + + source, err := ctyjson.Unmarshal(request.SourceStateJSON, cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + })) + if err != nil { + return providers.MoveResourceStateResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "invalid source state", err.Error()), + }, + } + } + + target := cty.ObjectVal(map[string]cty.Value{ + "id": source.GetAttr("id"), + "value": source.GetAttr("value"), + "deferred": cty.False, + }) + store.Set(source.GetAttr("id").AsString(), target) + + return providers.MoveResourceStateResponse{ + TargetState: target, + } + }, + CallFunctionFn: func(request providers.CallFunctionRequest) providers.CallFunctionResponse { + // Just echo the first argument back as the result. + return providers.CallFunctionResponse{ + Result: request.Arguments[0], + } + }, + }, + ResourceStore: store, + } + + // We want to use internal fields in this function so we have to set it + // like this. + provider.ConfigureProviderFn = provider.configure + + t.Cleanup(func() { + // Fail the test if this provider is not closed. + if !provider.CloseCalled { + t.Log(string(currentStackTrace)) + t.Fatalf("provider.Close was not called") + } + }) + + return provider +} + +func (provider *MockProvider) configure(request providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + // If configure_error is set, return an error. + err := request.Config.GetAttr("configure_error") + if err.IsKnown() && !err.IsNull() { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.AttributeValue(tfdiags.Error, err.AsString(), "configure_error attribute was set", cty.GetAttrPath("configure_error")), + }, + } + } + + // We deliberately only check the authentication if the configuration + // is providing it. It's entirely up to the config to opt into the + // authentication which would be crazy for a real provider but just + // makes things so much simpler for us in testing world. + requireAuth := request.Config.GetAttr("require_auth") + if requireAuth.True() { + authn := request.Config.GetAttr("authentication") + if authn.IsNull() || !authn.IsKnown() { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field is required", cty.GetAttrPath("authentication")), + }, + } + } + if authn.AsString() != provider.Authentication { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.AttributeValue(tfdiags.Error, "Authentication failed", "authentication field did not match expected", cty.GetAttrPath("authentication")), + }, + } + } + } + + return providers.ConfigureProviderResponse{} +} + +// mustGenerateUUID is a helper to generate a UUID and panic if it fails. +func mustGenerateUUID() string { + val, err := uuid.GenerateUUID() + if err != nil { + panic(err) + } + return val +} diff --git a/internal/stacks/stackruntime/testing/resource.go b/internal/stacks/stackruntime/testing/resource.go new file mode 100644 index 0000000000..1f3aedb0f1 --- /dev/null +++ b/internal/stacks/stackruntime/testing/resource.go @@ -0,0 +1,501 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package testing + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// resource is an interface that represents a resource that can be managed by +// the mock provider defined in this package. +type resource interface { + // Read reads the current state of the resource from the store. + Read(request providers.ReadResourceRequest, store *ResourceStore) providers.ReadResourceResponse + + // Plan plans the resource for creation. + Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) providers.PlanResourceChangeResponse + + // Apply applies the planned changes to the resource. + Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) providers.ApplyResourceChangeResponse +} + +func getResource(name string) resource { + switch name { + case "testing_resource": + return &testingResource{} + case "testing_deferred_resource": + return &deferredResource{} + case "testing_failed_resource": + return &failedResource{} + case "testing_blocked_resource": + return &blockedResource{} + case "testing_write_only_resource": + return &writeOnlyResource{} + case "testing_resource_with_identity": + return &testingResourceWithIdentity{} + default: + panic("unknown resource: " + name) + } +} + +var ( + _ resource = (*testingResource)(nil) + _ resource = (*deferredResource)(nil) + _ resource = (*failedResource)(nil) + _ resource = (*blockedResource)(nil) + _ resource = (*writeOnlyResource)(nil) + _ resource = (*testingResourceWithIdentity)(nil) +) + +// testingResource is a simple resource that can be managed by the mock provider +// defined in this package. +type testingResource struct{} + +func (t *testingResource) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) { + id := request.PriorState.GetAttr("id").AsString() + var exists bool + response.NewState, exists = store.Get(id) + if !exists { + response.NewState = cty.NullVal(TestingResourceSchema.Body.ImpliedType()) + } + return +} + +func (t *testingResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) { + if request.ProposedNewState.IsNull() { + response.PlannedState = request.ProposedNewState + return + } + + response.PlannedState = planEnsureId(request.ProposedNewState) + replace, err := validateId(response.PlannedState, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + if replace { + response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")} + } + return +} + +func (t *testingResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) { + if request.PlannedState.IsNull() { + store.Delete(request.PriorState.GetAttr("id").AsString()) + response.NewState = request.PlannedState + return + } + + value := applyEnsureId(request.PlannedState) + replace, err := validateId(value, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + response.NewState = value + + if replace { + store.Delete(request.PriorState.GetAttr("id").AsString()) + } + store.Set(response.NewState.GetAttr("id").AsString(), response.NewState) + return +} + +// deferredResource is a resource that can defer itself based on the provided +// configuration. +type deferredResource struct{} + +func (d *deferredResource) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) { + id := request.PriorState.GetAttr("id").AsString() + var exists bool + response.NewState, exists = store.Get(id) + if !exists { + response.NewState = cty.NullVal(DeferredResourceSchema.Body.ImpliedType()) + } + return +} + +func (d *deferredResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) { + if request.ProposedNewState.IsNull() { + if deferred := request.PriorState.GetAttr("deferred"); !deferred.IsNull() && deferred.IsKnown() && deferred.True() { + response.Deferred = &providers.Deferred{ + Reason: providers.DeferredReasonResourceConfigUnknown, + } + } + response.PlannedState = request.ProposedNewState + return + } + + response.PlannedState = planEnsureId(request.ProposedNewState) + replace, err := validateId(response.PlannedState, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "deferredResource error", err.Error())) + return + } + if deferred := response.PlannedState.GetAttr("deferred"); !deferred.IsNull() && deferred.IsKnown() && deferred.True() { + response.Deferred = &providers.Deferred{ + Reason: providers.DeferredReasonResourceConfigUnknown, + } + } + if replace { + response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")} + } + return +} + +func (d *deferredResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) { + if request.PlannedState.IsNull() { + store.Delete(request.PriorState.GetAttr("id").AsString()) + response.NewState = request.PlannedState + return + } + + value := applyEnsureId(request.PlannedState) + replace, err := validateId(value, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "deferredResource error", err.Error())) + return + } + response.NewState = value + + if replace { + store.Delete(request.PriorState.GetAttr("id").AsString()) + } + store.Set(response.NewState.GetAttr("id").AsString(), response.NewState) + return +} + +// failedResource is a resource that can be set to fail during Plan or Apply. +type failedResource struct{} + +func (f *failedResource) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) { + id := request.PriorState.GetAttr("id").AsString() + var exists bool + response.NewState, exists = store.Get(id) + if !exists { + response.NewState = cty.NullVal(FailedResourceSchema.Body.ImpliedType()) + } + return +} + +func (f *failedResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) { + if request.ProposedNewState.IsNull() { + response.PlannedState = request.ProposedNewState + if attr := request.PriorState.GetAttr("fail_plan"); !attr.IsNull() && attr.IsKnown() && attr.True() { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during plan")) + return + } + return + } + + response.PlannedState = planEnsureId(request.ProposedNewState) + replace, err := validateId(response.PlannedState, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", err.Error())) + return + } + + setUnknown(response.PlannedState, "fail_apply") + setUnknown(response.PlannedState, "fail_plan") + + if attr := response.PlannedState.GetAttr("fail_plan"); !attr.IsNull() && attr.IsKnown() && attr.True() { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during plan")) + } + if replace { + response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")} + } + + return +} + +func (f *failedResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) { + if request.PlannedState.IsNull() { + if attr := request.PriorState.GetAttr("fail_apply"); !attr.IsNull() && attr.IsKnown() && attr.True() { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during apply")) + return + } + response.NewState = request.PlannedState + store.Delete(request.PriorState.GetAttr("id").AsString()) + return + } + + value := applyEnsureId(request.PlannedState) + replace, err := validateId(value, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + + setKnown(value, "fail_apply", cty.False) + setKnown(value, "fail_plan", cty.False) + + if attr := value.GetAttr("fail_apply"); !attr.IsNull() && attr.IsKnown() && attr.True() { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "failedResource error", "failed during apply")) + return + } + response.NewState = value + + if replace { + store.Delete(request.PriorState.GetAttr("id").AsString()) + } + store.Set(response.NewState.GetAttr("id").AsString(), response.NewState) + return +} + +// blockedResource is a resource that accepts a list of required resource ids +// and will fail to apply if those resources don't exist. They will also fail to +// destroy if the resources do not exist - this ensures they have to be created +// and destroyed in the correct order. +type blockedResource struct{} + +func (b *blockedResource) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) { + id := request.PriorState.GetAttr("id").AsString() + var exists bool + response.NewState, exists = store.Get(id) + if !exists { + response.NewState = cty.NullVal(DeferredResourceSchema.Body.ImpliedType()) + } + return +} + +func (b *blockedResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) { + if request.ProposedNewState.IsNull() { + response.PlannedState = request.ProposedNewState + return + } + + response.PlannedState = planEnsureId(request.ProposedNewState) + replace, err := validateId(response.PlannedState, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + if replace { + response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")} + } + return +} + +func (b *blockedResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) { + if request.PlannedState.IsNull() { + if required := request.PriorState.GetAttr("required_resources"); !required.IsNull() && required.IsKnown() { + for _, id := range required.AsValueSlice() { + if _, exists := store.Get(id.AsString()); !exists { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "blockedResource error", fmt.Sprintf("required resource %q does not exists, so can't destroy self", id.AsString()))) + return + } + } + } + + store.Delete(request.PriorState.GetAttr("id").AsString()) + response.NewState = request.PlannedState + return + } + + value := applyEnsureId(request.PlannedState) + replace, err := validateId(value, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + + if required := value.GetAttr("required_resources"); !required.IsNull() && required.IsKnown() { + for _, id := range required.AsValueSlice() { + if _, exists := store.Get(id.AsString()); !exists { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "blockedResource error", fmt.Sprintf("required resource %q does not exist, so can't apply self", id.AsString()))) + return + } + } + } + response.NewState = value + + if replace { + store.Delete(request.PriorState.GetAttr("id").AsString()) + } + store.Set(response.NewState.GetAttr("id").AsString(), response.NewState) + return +} + +// writeOnlyResource is the same as testingResource but it includes an extra +// write-only attribute. +type writeOnlyResource struct{} + +func (w *writeOnlyResource) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) { + id := request.PriorState.GetAttr("id").AsString() + var exists bool + response.NewState, exists = store.Get(id) + if !exists { + response.NewState = cty.NullVal(WriteOnlyResourceSchema.Body.ImpliedType()) + } + return +} + +func (w *writeOnlyResource) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) { + if request.ProposedNewState.IsNull() { + response.PlannedState = request.ProposedNewState + return + } + + response.PlannedState = setNull(planEnsureId(request.ProposedNewState), "write_only") + replace, err := validateId(response.PlannedState, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + if replace { + response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")} + } + return +} + +func (w *writeOnlyResource) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) { + if request.PlannedState.IsNull() { + store.Delete(request.PriorState.GetAttr("id").AsString()) + response.NewState = request.PlannedState + return + } + + value := applyEnsureId(request.PlannedState) + replace, err := validateId(value, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResource error", err.Error())) + return + } + response.NewState = value + + if replace { + store.Delete(request.PriorState.GetAttr("id").AsString()) + } + store.Set(response.NewState.GetAttr("id").AsString(), response.NewState) + return +} + +// testingResourceWithIdentity is the same as testingResource but it returns an identity. +type testingResourceWithIdentity struct{} + +func (t *testingResourceWithIdentity) Read(request providers.ReadResourceRequest, store *ResourceStore) (response providers.ReadResourceResponse) { + id := request.PriorState.GetAttr("id").AsString() + var exists bool + response.NewState, exists = store.Get(id) + if !exists { + response.NewState = cty.NullVal(TestingResourceSchema.Body.ImpliedType()) + response.Identity = cty.UnknownVal(TestingResourceWithIdentitySchema.Identity.ImpliedType()) + } else { + response.Identity = cty.StringVal("id:" + id) + } + return +} + +func (t *testingResourceWithIdentity) Plan(request providers.PlanResourceChangeRequest, store *ResourceStore) (response providers.PlanResourceChangeResponse) { + if request.ProposedNewState.IsNull() { + response.PlannedState = request.ProposedNewState + return + } + + response.PlannedState = planEnsureId(request.ProposedNewState) + response.PlannedIdentity = cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("id:" + response.PlannedState.GetAttr("id").AsString()), + }) + replace, err := validateId(response.PlannedState, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResourceWithIdentity error", err.Error())) + return + } + if replace { + response.RequiresReplace = []cty.Path{cty.GetAttrPath("id")} + } + return +} + +func (t *testingResourceWithIdentity) Apply(request providers.ApplyResourceChangeRequest, store *ResourceStore) (response providers.ApplyResourceChangeResponse) { + if request.PlannedState.IsNull() { + store.Delete(request.PriorState.GetAttr("id").AsString()) + response.NewState = request.PlannedState + return + } + + value := applyEnsureId(request.PlannedState) + replace, err := validateId(value, request.PriorState, store) + if err != nil { + response.Diagnostics = append(response.Diagnostics, tfdiags.Sourceless(tfdiags.Error, "testingResourceWithIdentity error", err.Error())) + return + } + response.NewState = value + response.NewIdentity = request.PlannedIdentity + + if replace { + store.Delete(request.PriorState.GetAttr("id").AsString()) + } + store.Set(response.NewState.GetAttr("id").AsString(), response.NewState) + return +} + +func validateId(target cty.Value, prior cty.Value, store *ResourceStore) (bool, error) { + if prior.IsNull() { + // Then we're creating a resource, we want to make sure we're not + // creating a resource with an existing ID. + if id := target.GetAttr("id"); id.IsKnown() { + if _, exists := store.Get(id.AsString()); exists { + return false, fmt.Errorf("resource with id %q already exists", id.AsString()) + } + } + + return false, nil + } + + if attr := target.GetAttr("id"); !attr.IsKnown() { + // Then the attribute has been set to unknown, which means we're + // potentially changing the id. + return true, nil + } + + // Now, we know that the ID is known in both the prior and target states. + if result := prior.GetAttr("id").Equals(target.GetAttr("id")); result.False() { + // Then the ID value is changing, so we need to delete the old ID + // and create the new one. + return true, nil + } + + return false, nil +} + +func planEnsureId(value cty.Value) cty.Value { + return setUnknown(value, "id") +} + +func applyEnsureId(value cty.Value) cty.Value { + return setKnown(value, "id", cty.StringVal(mustGenerateUUID())) +} + +func setUnknown(value cty.Value, attr string) cty.Value { + if v := value.GetAttr(attr); v.IsNull() { + vals := value.AsValueMap() + vals[attr] = cty.UnknownVal(cty.String) + return cty.ObjectVal(vals) + } + return value +} + +func setKnown(value cty.Value, attr string, attrValue cty.Value) cty.Value { + if v := value.GetAttr(attr); !v.IsKnown() { + vals := value.AsValueMap() + vals[attr] = attrValue + return cty.ObjectVal(vals) + } + return value +} + +func setNull(value cty.Value, attr string) cty.Value { + if v := value.GetAttr(attr); !v.IsKnown() { + vals := value.AsValueMap() + vals[attr] = cty.NullVal(v.Type()) + return cty.ObjectVal(vals) + } + return value +} diff --git a/internal/stacks/stackruntime/testing/store.go b/internal/stacks/stackruntime/testing/store.go new file mode 100644 index 0000000000..903d030fae --- /dev/null +++ b/internal/stacks/stackruntime/testing/store.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package testing + +import ( + "sync" + + "github.com/zclconf/go-cty/cty" +) + +// ResourceStore is a simple data store, that can let the mock provider defined +// in this package store and return interesting values for resources and data +// sources. +type ResourceStore struct { + mutex sync.RWMutex + + Resources map[string]cty.Value +} + +func NewResourceStore() *ResourceStore { + return &ResourceStore{ + Resources: map[string]cty.Value{}, + } +} + +func (rs *ResourceStore) Get(id string) (cty.Value, bool) { + rs.mutex.RLock() + defer rs.mutex.RUnlock() + + value, exists := rs.Resources[id] + return value, exists +} + +func (rs *ResourceStore) Set(id string, value cty.Value) { + rs.mutex.Lock() + defer rs.mutex.Unlock() + + rs.Resources[id] = value +} + +func (rs *ResourceStore) Delete(id string) { + rs.mutex.Lock() + defer rs.mutex.Unlock() + + delete(rs.Resources, id) +} + +// ResourceStoreBuilder is an implementation of the builder pattern for building +// a ResourceStore with prepopulated values. +type ResourceStoreBuilder struct { + store *ResourceStore +} + +func NewResourceStoreBuilder() *ResourceStoreBuilder { + return &ResourceStoreBuilder{ + store: NewResourceStore(), + } +} + +func (b *ResourceStoreBuilder) AddResource(id string, value cty.Value) *ResourceStoreBuilder { + if b.store == nil { + panic("cannot add resources after calling Build()") + } + + b.store.Set(id, value) + return b +} + +func (b *ResourceStoreBuilder) Build() *ResourceStore { + if b.store == nil { + panic("cannot call Build() more than once") + } + + store := b.store + b.store = nil + return store +} diff --git a/internal/stacks/stackruntime/validate.go b/internal/stacks/stackruntime/validate.go new file mode 100644 index 0000000000..e607235c7d --- /dev/null +++ b/internal/stacks/stackruntime/validate.go @@ -0,0 +1,46 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + + "go.opentelemetry.io/otel/codes" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackconfig" + "github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// Validate performs static validation of a full stack configuration, returning +// diagnostics in case of any detected problems. +func Validate(ctx context.Context, req *ValidateRequest) tfdiags.Diagnostics { + ctx, span := tracer.Start(ctx, "validate stack configuration") + defer span.End() + + main := stackeval.NewForValidating(req.Config, stackeval.ValidateOpts{ + ProviderFactories: req.ProviderFactories, + DependencyLocks: req.DependencyLocks, + }) + main.AllowLanguageExperiments(req.ExperimentsAllowed) + diags := main.ValidateAll(ctx) + diags = diags.Append( + main.DoCleanup(ctx), + ) + if diags.HasErrors() { + span.SetStatus(codes.Error, "validation returned errors") + } + return diags +} + +type ValidateRequest struct { + Config *stackconfig.Config + ProviderFactories map[addrs.Provider]providers.Factory + DependencyLocks depsfile.Locks + + ExperimentsAllowed bool +} diff --git a/internal/stacks/stackruntime/validate_test.go b/internal/stacks/stackruntime/validate_test.go new file mode 100644 index 0000000000..adc7f4a6f9 --- /dev/null +++ b/internal/stacks/stackruntime/validate_test.go @@ -0,0 +1,550 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackruntime + +import ( + "context" + "path/filepath" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/depsfile" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" + "github.com/hashicorp/terraform/internal/providers" + stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type validateTestInput struct { + // skip lets us write tests for behaviour we want to add in the future. Set + // this to true for any tests that are not yet implemented. + skip bool + + // diags is a function that returns the expected diagnostics for the + // test. + diags func() tfdiags.Diagnostics + + // planInputVars is used only in the plan tests to provide a set of input + // variables to use for the plan request. Validate operates statically so + // does not need any input variables. + planInputVars map[string]cty.Value +} + +var ( + // validConfigurations are shared between the validate and plan tests. + validConfigurations = map[string]validateTestInput{ + "empty": {}, + "plan-variable-defaults": {}, + "variable-output-roundtrip": {}, + "variable-output-roundtrip-nested": {}, + "aliased-provider": {}, + filepath.Join("with-single-input", "input-from-component"): {}, + filepath.Join("with-single-input", "input-from-component-list"): { + planInputVars: map[string]cty.Value{ + "components": cty.SetVal([]cty.Value{ + cty.StringVal("one"), + cty.StringVal("two"), + cty.StringVal("three"), + }), + }, + }, + filepath.Join("with-single-input", "provider-name-clash"): { + planInputVars: map[string]cty.Value{ + "input": cty.StringVal("input"), + }, + }, + filepath.Join("with-single-input", "valid"): { + planInputVars: map[string]cty.Value{ + "input": cty.StringVal("input"), + }, + }, + filepath.Join("with-single-input", "provider-for-each"): { + planInputVars: map[string]cty.Value{ + "input": cty.StringVal("input"), + }, + }, + } + + // invalidConfigurations are shared between the validate and plan tests. + invalidConfigurations = map[string]validateTestInput{ + "validate-undeclared-variable": { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared input variable", + Detail: `There is no variable "a" block declared in this stack.`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("validate-undeclared-variable/validate-undeclared-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 3, Column: 11, Byte: 40}, + End: hcl.Pos{Line: 3, Column: 16, Byte: 45}, + }, + }) + return diags + }, + }, + "invalid-configuration": { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported argument", + Detail: "An argument named \"invalid\" is not expected here.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("invalid-configuration/invalid-configuration.tf"), + Start: hcl.Pos{Line: 11, Column: 3, Byte: 163}, + End: hcl.Pos{Line: 11, Column: 10, Byte: 170}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "undeclared-provider"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared provider configuration", + Detail: "There is no provider \"testing\" \"default\" block declared in this stack.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 10, Column: 15, Byte: 163}, + End: hcl.Pos{Line: 10, Column: 39, Byte: 187}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared provider configuration", + Detail: "There is no provider \"testing\" \"default\" block declared in this stack.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/undeclared-provider/undeclared-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 25, Column: 15, Byte: 379}, + End: hcl.Pos{Line: 25, Column: 39, Byte: 403}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "missing-provider"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: "The root module for component.removed requires a provider configuration named \"testing\" for provider \"hashicorp/testing\", which is not assigned in the block's \"providers\" argument.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/missing-provider/missing-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 25, Column: 1, Byte: 337}, + End: hcl.Pos{Line: 25, Column: 8, Byte: 344}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing required provider configuration", + Detail: "The root module for component.self requires a provider configuration named \"testing\" for provider \"hashicorp/testing\", which is not assigned in the block's \"providers\" argument.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/missing-provider/missing-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 14, Column: 1, Byte: 169}, + End: hcl.Pos{Line: 14, Column: 17, Byte: 185}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "invalid-provider-type"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration", + Detail: "The provider configuration slot \"testing\" requires a configuration for provider \"registry.terraform.io/hashicorp/testing\", not for provider \"terraform.io/builtin/testing\".", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/invalid-provider-type/invalid-provider-type.tfstack.hcl"), + Start: hcl.Pos{Line: 22, Column: 15, Byte: 378}, + End: hcl.Pos{Line: 22, Column: 39, Byte: 402}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration", + Detail: "The provider configuration slot \"testing\" requires a configuration for provider \"registry.terraform.io/hashicorp/testing\", not for provider \"terraform.io/builtin/testing\".", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/invalid-provider-type/invalid-provider-type.tfstack.hcl"), + Start: hcl.Pos{Line: 37, Column: 15, Byte: 614}, + End: hcl.Pos{Line: 37, Column: 39, Byte: 638}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "invalid-provider-config"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unsupported argument", + Detail: "An argument named \"imaginary\" is not expected here.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/invalid-provider-config/invalid-provider-config.tfstack.hcl"), + Start: hcl.Pos{Line: 11, Column: 5, Byte: 218}, + End: hcl.Pos{Line: 11, Column: 14, Byte: 227}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "undeclared-variable"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared input variable", + Detail: `There is no variable "input" block declared in this stack.`, + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/undeclared-variable/undeclared-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 19, Column: 13, Byte: 284}, + End: hcl.Pos{Line: 19, Column: 22, Byte: 293}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "missing-variable"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: "Invalid input variable definition object: attribute \"input\" is required.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/missing-variable/missing-variable.tfstack.hcl"), + Start: hcl.Pos{Line: 22, Column: 12, Byte: 338}, + End: hcl.Pos{Line: 22, Column: 14, Byte: 340}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "input-from-missing-component"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to undeclared component", + Detail: "There is no component \"output\" block declared in this stack.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/input-from-missing-component/input-from-missing-component.tfstack.hcl"), + Start: hcl.Pos{Line: 19, Column: 13, Byte: 314}, + End: hcl.Pos{Line: 19, Column: 29, Byte: 330}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "input-from-provider"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid inputs for component", + Detail: "Invalid input variable definition object: attribute \"input\": string required.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/input-from-provider/input-from-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 17, Column: 12, Byte: 239}, + End: hcl.Pos{Line: 20, Column: 4, Byte: 339}, + }, + }) + return diags + }, + }, + filepath.Join("with-single-input", "depends-on-invalid"): { + diags: func() tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: "The depends_on argument must refer to an embedded stack or component, but this reference refers to \"var.input\".", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl"), + Start: hcl.Pos{Line: 22, Column: 17, Byte: 293}, + End: hcl.Pos{Line: 22, Column: 26, Byte: 302}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: "The depends_on argument must refer to an embedded stack or component, but this reference refers to \"var.input\".", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl"), + Start: hcl.Pos{Line: 37, Column: 17, Byte: 509}, + End: hcl.Pos{Line: 37, Column: 26, Byte: 518}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: "The depends_on reference \"component.missing\" does not exist.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl"), + Start: hcl.Pos{Line: 22, Column: 28, Byte: 304}, + End: hcl.Pos{Line: 22, Column: 45, Byte: 321}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid depends_on target", + Detail: "The depends_on reference \"stack.missing\" does not exist.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl"), + Start: hcl.Pos{Line: 37, Column: 28, Byte: 520}, + End: hcl.Pos{Line: 37, Column: 41, Byte: 533}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Non-valid depends_on target", + Detail: "The depends_on argument should refer directly to an embedded stack or component in configuration, but this reference is too deep.\n\n" + + "Terraform Stacks has simplified the reference to the nearest valid target, \"component.first\". To remove this warning, update the configuration to the same target.", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("with-single-input/depends-on-invalid/depends-on-invalid.tfstack.hcl"), + Start: hcl.Pos{Line: 52, Column: 17, Byte: 722}, + End: hcl.Pos{Line: 52, Column: 32, Byte: 737}, + }, + }) + return diags + }, + }, + } +) + +// TestValidate_valid tests that a variety of configurations under the main +// test source bundle each generate no diagnostics at all, as a +// relatively-simple way to detect accidental regressions. +// +// Any stack configuration directory that we expect should be valid can +// potentially be included in here unless it depends on provider plugins +// to complete validation, since this test cannot supply provider plugins. +func TestValidate_valid(t *testing.T) { + for name, tc := range validConfigurations { + t.Run(name, func(t *testing.T) { + if tc.skip { + // We've added this test before the implementation was ready. + t.SkipNow() + } + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + lock.SetProvider( + addrs.NewDefaultProvider("other"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + testContext := TestContext{ + config: loadMainBundleConfigForTest(t, name), + providers: map[addrs.Provider]providers.Factory{ + // We support both hashicorp/testing and + // terraform.io/builtin/testing as providers. This lets us + // test the provider aliasing feature. Both providers + // support the same set of resources and data sources. + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + // We also support an "other" provider out of the box to + // test the provider aliasing feature. + addrs.NewDefaultProvider("other"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + dependencyLocks: *lock, + } + + cycle := TestCycle{} // empty, as we expect no diagnostics + testContext.Validate(t, ctx, cycle) + }) + } +} + +func TestValidate_invalid(t *testing.T) { + for name, tc := range invalidConfigurations { + t.Run(name, func(t *testing.T) { + if tc.skip { + // We've added this test before the implementation was ready. + t.SkipNow() + } + ctx := context.Background() + + lock := depsfile.NewLocks() + lock.SetProvider( + addrs.NewDefaultProvider("testing"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + lock.SetProvider( + addrs.NewDefaultProvider("other"), + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + + testContext := TestContext{ + config: loadMainBundleConfigForTest(t, name), + providers: map[addrs.Provider]providers.Factory{ + // We support both hashicorp/testing and + // terraform.io/builtin/testing as providers. This lets us + // test the provider aliasing feature. Both providers + // support the same set of resources and data sources. + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + // We also support an "other" provider out of the box to + // test the provider aliasing feature. + addrs.NewDefaultProvider("other"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + dependencyLocks: *lock, + } + testContext.Validate(t, ctx, TestCycle{ + wantValidateDiags: tc.diags(), + }) + }) + } +} + +func TestValidate(t *testing.T) { + tcs := map[string]struct { + path string + providers map[addrs.Provider]providers.Factory + locks *depsfile.Locks + wantDiags tfdiags.Diagnostics + }{ + "embedded-stack-selfref": { + path: "validate-embedded-stack-selfref", + wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Self-dependent items in configuration", + `The following items in your configuration form a circular dependency chain through their references: + - stack.a collected outputs + - stack.a.output.a + - stack.a inputs + +Terraform uses references to decide a suitable order for performing operations, so configuration items may not refer to their own results either directly or indirectly.`, + )) + }), + }, + "cyclic-component-dependency": { + path: "validate-cyclic-dependency", + wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Self-dependent items in configuration", + `The following items in your configuration form a circular dependency chain through their references: + - component.boundary + - component.vault-config + +Terraform uses references to decide a suitable order for performing operations, so configuration items may not refer to their own results either directly or indirectly.`, + )) + }), + }, + "missing-provider-from-lockfile": { + path: filepath.Join("with-single-input", "input-from-component"), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + locks: depsfile.NewLocks(), // deliberately empty + wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Provider missing from lockfile", + Detail: "Provider \"registry.terraform.io/hashicorp/testing\" is not in the lockfile. This provider must be in the lockfile to be used in the configuration. Please run `tfstacks providers lock` to update the lockfile and run this operation again with an updated configuration.", + Subject: &hcl.Range{ + Filename: "git::https://example.com/test.git//with-single-input/input-from-component/input-from-component.tfstack.hcl", + Start: hcl.Pos{Line: 8, Column: 1, Byte: 98}, + End: hcl.Pos{Line: 8, Column: 29, Byte: 126}, + }, + }) + }), + }, + "implied-provider-type-with-hashicorp-provider": { + path: filepath.Join("legacy-module", "with-hashicorp-provider"), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + }, + "implied-provider-type-with-non-hashicorp-provider": { + path: filepath.Join("legacy-module", "with-non-hashicorp-provider"), + providers: map[addrs.Provider]providers.Factory{ + addrs.NewProvider(addrs.DefaultProviderRegistryHost, "other", "testing"): func() (providers.Interface, error) { + return stacks_testing_provider.NewProvider(t), nil + }, + }, + wantDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration", + Detail: "The provider configuration slot \"testing\" requires a configuration for provider \"registry.terraform.io/hashicorp/testing\", not for provider \"registry.terraform.io/other/testing\"." + + "\n\nThe module does not declare a source address for \"testing\" in its required_providers block, so Terraform assumed \"hashicorp/testing\" for backward-compatibility with older versions of Terraform", + Subject: &hcl.Range{ + Filename: mainBundleSourceAddrStr("legacy-module/with-non-hashicorp-provider/with-non-hashicorp-provider.tfstack.hcl"), + Start: hcl.Pos{Line: 21, Column: 15, Byte: 447}, + End: hcl.Pos{Line: 21, Column: 39, Byte: 471}, + }, + }) + }), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + ctx, span := tracer.Start(ctx, name) + defer span.End() + + locks := tc.locks + if locks == nil { + locks = depsfile.NewLocks() + for addr := range tc.providers { + locks.SetProvider( + addr, + providerreqs.MustParseVersion("0.0.0"), + providerreqs.MustParseVersionConstraints("=0.0.0"), + providerreqs.PreferredHashes([]providerreqs.Hash{}), + ) + } + } + + testContext := TestContext{ + config: loadMainBundleConfigForTest(t, tc.path), + providers: tc.providers, + dependencyLocks: *locks, + } + testContext.Validate(t, ctx, TestCycle{ + wantValidateDiags: tc.wantDiags, + }) + }) + } +} diff --git a/internal/stacks/stackstate/applied_change.go b/internal/stacks/stackstate/applied_change.go new file mode 100644 index 0000000000..5ada555be1 --- /dev/null +++ b/internal/stacks/stackstate/applied_change.go @@ -0,0 +1,505 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/stacks/stackutils" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" +) + +// AppliedChange represents a single isolated change, emitted as +// part of a stream of applied changes during the ApplyStackChanges RPC API +// operation. +// +// Each AppliedChange becomes a single event in the RPC API, which itself +// has zero or more opaque raw plan messages that the caller must collect and +// provide verbatim during planning and zero or more "description" messages +// that are to give the caller realtime updates about the planning process. +type AppliedChange interface { + // AppliedChangeProto returns the protocol buffers representation of + // the change, ready to be sent verbatim to an RPC API client. + AppliedChangeProto() (*stacks.AppliedChange, error) +} + +// AppliedChangeResourceInstanceObject announces the result of applying changes to +// a particular resource instance object. +type AppliedChangeResourceInstanceObject struct { + // ResourceInstanceObjectAddr is the absolute address of the resource + // instance object within the component instance that declared it. + // + // Typically a stream of applied changes with a resource instance object + // will also include a separate description of the component instance + // that the resource instance belongs to, but that isn't guaranteed in + // cases where problems occur during the apply phase and so consumers + // should tolerate seeing a resource instance for a component instance + // they don't know about yet, and should behave as if that component + // instance had been previously announced. + ResourceInstanceObjectAddr stackaddrs.AbsResourceInstanceObject + NewStateSrc *states.ResourceInstanceObjectSrc + ProviderConfigAddr addrs.AbsProviderConfig + + // PreviousResourceInstanceObjectAddr is the absolute address of the + // resource instance object within the component instance if this object + // was moved from another address. This will be nil if the object was not + // moved. + PreviousResourceInstanceObjectAddr *stackaddrs.AbsResourceInstanceObject + + // Schema MUST be the same schema that was used to encode the dynamic + // values inside NewStateSrc. This can be left as empty if NewStateSrc + // is nil, which represents that the object has been deleted. + Schema providers.Schema +} + +var _ AppliedChange = (*AppliedChangeResourceInstanceObject)(nil) + +// AppliedChangeProto implements AppliedChange. +func (ac *AppliedChangeResourceInstanceObject) AppliedChangeProto() (*stacks.AppliedChange, error) { + descs, raws, err := ac.protosForObject() + if err != nil { + return nil, fmt.Errorf("encoding %s: %w", ac.ResourceInstanceObjectAddr, err) + } + return &stacks.AppliedChange{ + Raw: raws, + Descriptions: descs, + }, nil +} + +func (ac *AppliedChangeResourceInstanceObject) protosForObject() ([]*stacks.AppliedChange_ChangeDescription, []*stacks.AppliedChange_RawChange, error) { + var descs []*stacks.AppliedChange_ChangeDescription + var raws []*stacks.AppliedChange_RawChange + + var addr = ac.ResourceInstanceObjectAddr + var provider = ac.ProviderConfigAddr + var objSrc = ac.NewStateSrc + + // For resource instance objects we use the same key format for both the + // raw and description representations, but callers MUST NOT rely on this. + objKey := statekeys.ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: addr.Component, + Item: addr.Item.ResourceInstance, + }, + DeposedKey: addr.Item.DeposedKey, + } + objKeyRaw := statekeys.String(objKey) + + if objSrc == nil { + // If the new object is nil then we'll emit a "deleted" description + // to ensure that any existing prior state value gets removed. + descs = append(descs, &stacks.AppliedChange_ChangeDescription{ + Key: objKeyRaw, + Description: &stacks.AppliedChange_ChangeDescription_Deleted{ + Deleted: &stacks.AppliedChange_Nothing{}, + }, + }) + raws = append(raws, &stacks.AppliedChange_RawChange{ + Key: objKeyRaw, + Value: nil, // unset Value field represents "delete" for raw changes + }) + return descs, raws, nil + } + + if ac.PreviousResourceInstanceObjectAddr != nil { + // If the object was moved, we need to emit a "deleted" description + // for the old address to ensure that any existing prior state value + // gets removed. + prevKey := statekeys.ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: ac.PreviousResourceInstanceObjectAddr.Component, + Item: ac.PreviousResourceInstanceObjectAddr.Item.ResourceInstance, + }, + DeposedKey: ac.PreviousResourceInstanceObjectAddr.Item.DeposedKey, + } + prevKeyRaw := statekeys.String(prevKey) + + descs = append(descs, &stacks.AppliedChange_ChangeDescription{ + Key: prevKeyRaw, + Description: &stacks.AppliedChange_ChangeDescription_Moved{ + Moved: &stacks.AppliedChange_Nothing{}, + }, + }) + raws = append(raws, &stacks.AppliedChange_RawChange{ + Key: prevKeyRaw, + Value: nil, // unset Value field represents "delete" for raw changes + }) + + // Don't return now - we'll still add the main change below. + } + + // TRICKY: For historical reasons, a states.ResourceInstance + // contains pre-JSON-encoded dynamic data ready to be + // inserted verbatim into Terraform CLI's traditional + // JSON-based state file format. However, our RPC API + // exclusively uses MessagePack encoding for dynamic + // values, and so we will need to use the ac.Schema to + // transcode the data. + obj, err := objSrc.Decode(ac.Schema) + if err != nil { + // It would be _very_ strange to get here because we should just + // be reversing the same encoding operation done earlier to + // produce this object, using exactly the same schema. + return nil, nil, fmt.Errorf("cannot decode new state for %s in preparation for saving it: %w", addr, err) + } + + protoValue, err := stacks.ToDynamicValue(obj.Value, ac.Schema.Body.ImpliedType()) + if err != nil { + return nil, nil, fmt.Errorf("cannot encode new state for %s in preparation for saving it: %w", addr, err) + } + + descs = append(descs, &stacks.AppliedChange_ChangeDescription{ + Key: objKeyRaw, + Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ + ResourceInstance: &stacks.AppliedChange_ResourceInstance{ + Addr: stacks.NewResourceInstanceObjectInStackAddr(addr), + NewValue: protoValue, + ResourceMode: stackutils.ResourceModeForProto(addr.Item.ResourceInstance.Resource.Resource.Mode), + ResourceType: addr.Item.ResourceInstance.Resource.Resource.Type, + ProviderAddr: provider.Provider.String(), + }, + }, + }) + + rawMsg := tfstackdata1.ResourceInstanceObjectStateToTFStackData1(objSrc, ac.ProviderConfigAddr) + var raw anypb.Any + err = anypb.MarshalFrom(&raw, rawMsg, proto.MarshalOptions{}) + if err != nil { + return nil, nil, fmt.Errorf("encoding raw state object: %w", err) + } + raws = append(raws, &stacks.AppliedChange_RawChange{ + Key: objKeyRaw, + Value: &raw, + }) + + return descs, raws, nil +} + +// AppliedChangeComponentInstanceRemoved is the equivalent of +// AppliedChangeComponentInstance but it represents the component instance +// being removed from state instead of created or updated. +type AppliedChangeComponentInstanceRemoved struct { + ComponentAddr stackaddrs.AbsComponent + ComponentInstanceAddr stackaddrs.AbsComponentInstance +} + +var _ AppliedChange = (*AppliedChangeComponentInstanceRemoved)(nil) + +// AppliedChangeProto implements AppliedChange. +func (ac *AppliedChangeComponentInstanceRemoved) AppliedChangeProto() (*stacks.AppliedChange, error) { + stateKey := statekeys.String(statekeys.ComponentInstance{ + ComponentInstanceAddr: ac.ComponentInstanceAddr, + }) + return &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: stateKey, + Value: nil, + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: stateKey, + Description: &stacks.AppliedChange_ChangeDescription_Deleted{ + Deleted: &stacks.AppliedChange_Nothing{}, + }, + }, + }, + }, nil +} + +// AppliedChangeComponentInstance announces the result of applying changes to +// an overall component instance. +// +// This deals with external-facing metadata about component instances, but +// does not directly track any resource instances inside. Those are tracked +// using individual [AppliedChangeResourceInstanceObject] objects for each. +type AppliedChangeComponentInstance struct { + ComponentAddr stackaddrs.AbsComponent + ComponentInstanceAddr stackaddrs.AbsComponentInstance + + // Dependencies "remembers" the set of component instances that were + // required by the most recent apply of this component instance. + // + // This will be used by the stacks runtime to determine the order in + // which components should be destroyed when the original component block + // is no longer available. + Dependencies collections.Set[stackaddrs.AbsComponent] + + // Dependents "remembers" the set of component instances that depended on + // this component instance at the most recent apply of this component + // instance. + // + // This will be used by the stacks runtime to determine the order in + // which components should be destroyed when the original component block + // is no longer available. + Dependents collections.Set[stackaddrs.AbsComponent] + + // OutputValues "remembers" the output values from the most recent + // apply of the component instance. We store this primarily for external + // consumption, since the stacks runtime is able to recalculate the + // output values based on the prior state when needed, but we do have + // the option of using this internally in certain special cases where it + // would be too expensive to recalculate. + // + // If any output values are declared as sensitive then they should be + // marked as such here using the usual cty marking strategy. + OutputValues map[addrs.OutputValue]cty.Value + + // InputVariables "remembers" the input values from the most recent + // apply of the component instance. We store this primarily for usage + // within the removed blocks in which the input values from the last + // applied state are required to destroy the existing resources. + InputVariables map[addrs.InputVariable]cty.Value +} + +var _ AppliedChange = (*AppliedChangeComponentInstance)(nil) + +// AppliedChangeProto implements AppliedChange. +func (ac *AppliedChangeComponentInstance) AppliedChangeProto() (*stacks.AppliedChange, error) { + stateKey := statekeys.String(statekeys.ComponentInstance{ + ComponentInstanceAddr: ac.ComponentInstanceAddr, + }) + + outputDescs := make(map[string]*stacks.DynamicValue, len(ac.OutputValues)) + for addr, val := range ac.OutputValues { + protoValue, err := stacks.ToDynamicValue(val, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("encoding new state for %s in %s in preparation for saving it: %w", addr, ac.ComponentInstanceAddr, err) + } + outputDescs[addr.Name] = protoValue + } + + inputDescs := make(map[string]*stacks.DynamicValue, len(ac.InputVariables)) + for addr, val := range ac.InputVariables { + protoValue, err := stacks.ToDynamicValue(val, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("encoding new state for %s in %s in preparation for saving it: %w", addr, ac.ComponentInstanceAddr, err) + } + inputDescs[addr.Name] = protoValue + } + + var raw anypb.Any + if err := anypb.MarshalFrom(&raw, &tfstackdata1.StateComponentInstanceV1{ + OutputValues: func() map[string]*tfstackdata1.DynamicValue { + outputs := make(map[string]*tfstackdata1.DynamicValue, len(outputDescs)) + for name, value := range outputDescs { + outputs[name] = tfstackdata1.Terraform1ToStackDataDynamicValue(value) + } + return outputs + }(), + InputVariables: func() map[string]*tfstackdata1.DynamicValue { + inputs := make(map[string]*tfstackdata1.DynamicValue, len(inputDescs)) + for name, value := range inputDescs { + inputs[name] = tfstackdata1.Terraform1ToStackDataDynamicValue(value) + } + return inputs + }(), + DependencyAddrs: func() []string { + var dependencies []string + for dependency := range ac.Dependencies.All() { + dependencies = append(dependencies, dependency.String()) + } + return dependencies + }(), + DependentAddrs: func() []string { + var dependents []string + for dependent := range ac.Dependents.All() { + dependents = append(dependents, dependent.String()) + } + return dependents + }(), + }, proto.MarshalOptions{}); err != nil { + return nil, fmt.Errorf("encoding raw state for %s: %w", ac.ComponentInstanceAddr, err) + } + + return &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: stateKey, + Value: &raw, + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: stateKey, + Description: &stacks.AppliedChange_ChangeDescription_ComponentInstance{ + ComponentInstance: &stacks.AppliedChange_ComponentInstance{ + ComponentAddr: ac.ComponentAddr.String(), + ComponentInstanceAddr: ac.ComponentInstanceAddr.String(), + OutputValues: outputDescs, + }, + }, + }, + }, + }, nil +} + +type AppliedChangeInputVariable struct { + Addr stackaddrs.InputVariable + Value cty.Value +} + +var _ AppliedChange = (*AppliedChangeInputVariable)(nil) + +func (ac *AppliedChangeInputVariable) AppliedChangeProto() (*stacks.AppliedChange, error) { + key := statekeys.String(statekeys.Variable{ + VariableAddr: ac.Addr, + }) + + if ac.Value == cty.NilVal { + // Then we're deleting this input variable from the state. + return &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: key, + Value: nil, + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: key, + Description: &stacks.AppliedChange_ChangeDescription_Deleted{ + Deleted: &stacks.AppliedChange_Nothing{}, + }, + }, + }, + }, nil + } + + var raw anypb.Any + description := &stacks.AppliedChange_InputVariable{ + Name: ac.Addr.Name, + } + + value, err := stacks.ToDynamicValue(ac.Value, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("encoding new state for %s in preparation for saving it: %w", ac.Addr, err) + } + description.NewValue = value + if err := anypb.MarshalFrom(&raw, tfstackdata1.Terraform1ToStackDataDynamicValue(value), proto.MarshalOptions{}); err != nil { + return nil, fmt.Errorf("encoding raw state for %s: %w", ac.Addr, err) + } + + return &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: key, + Value: &raw, + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: key, + Description: &stacks.AppliedChange_ChangeDescription_InputVariable{ + InputVariable: description, + }, + }, + }, + }, nil +} + +type AppliedChangeOutputValue struct { + Addr stackaddrs.OutputValue + Value cty.Value +} + +var _ AppliedChange = (*AppliedChangeOutputValue)(nil) + +func (ac *AppliedChangeOutputValue) AppliedChangeProto() (*stacks.AppliedChange, error) { + key := statekeys.String(statekeys.Output{ + OutputAddr: ac.Addr, + }) + + if ac.Value == cty.NilVal { + // Then we're deleting this output value from the state. + return &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: key, + Value: nil, + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: key, + Description: &stacks.AppliedChange_ChangeDescription_Deleted{ + Deleted: &stacks.AppliedChange_Nothing{}, + }, + }, + }, + }, nil + } + + value, err := stacks.ToDynamicValue(ac.Value, cty.DynamicPseudoType) + if err != nil { + return nil, fmt.Errorf("encoding new state for %s in preparation for saving it: %w", ac.Addr, err) + } + + var raw anypb.Any + if err := anypb.MarshalFrom(&raw, tfstackdata1.Terraform1ToStackDataDynamicValue(value), proto.MarshalOptions{}); err != nil { + return nil, fmt.Errorf("encoding raw state for %s: %w", ac.Addr, err) + } + + return &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: key, + Value: &raw, + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: key, + Description: &stacks.AppliedChange_ChangeDescription_OutputValue{ + OutputValue: &stacks.AppliedChange_OutputValue{ + Name: ac.Addr.Name, + NewValue: value, + }, + }, + }, + }, + }, nil +} + +type AppliedChangeDiscardKeys struct { + DiscardRawKeys collections.Set[statekeys.Key] + DiscardDescKeys collections.Set[statekeys.Key] +} + +var _ AppliedChange = (*AppliedChangeDiscardKeys)(nil) + +// AppliedChangeProto implements AppliedChange. +func (ac *AppliedChangeDiscardKeys) AppliedChangeProto() (*stacks.AppliedChange, error) { + ret := &stacks.AppliedChange{ + Raw: make([]*stacks.AppliedChange_RawChange, 0, ac.DiscardRawKeys.Len()), + Descriptions: make([]*stacks.AppliedChange_ChangeDescription, 0, ac.DiscardDescKeys.Len()), + } + for key := range ac.DiscardRawKeys.All() { + ret.Raw = append(ret.Raw, &stacks.AppliedChange_RawChange{ + Key: statekeys.String(key), + Value: nil, // nil represents deletion + }) + } + for key := range ac.DiscardDescKeys.All() { + ret.Descriptions = append(ret.Descriptions, &stacks.AppliedChange_ChangeDescription{ + Key: statekeys.String(key), + Description: &stacks.AppliedChange_ChangeDescription_Deleted{ + // Selection of this empty variant represents deletion + }, + }) + } + return ret, nil +} diff --git a/internal/stacks/stackstate/applied_change_test.go b/internal/stacks/stackstate/applied_change_test.go new file mode 100644 index 0000000000..780599e151 --- /dev/null +++ b/internal/stacks/stackstate/applied_change_test.go @@ -0,0 +1,278 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + ctymsgpack "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" +) + +func TestAppliedChangeAsProto(t *testing.T) { + tests := map[string]struct { + Receiver AppliedChange + Want *stacks.AppliedChange + }{ + "resource instance": { + Receiver: &AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "thingamajig", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + Schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "secret": { + Type: cty.String, + Sensitive: true, + }, + }, + }, + }, + NewStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("secret"), + }, + }, + }, + Want: &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, + Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ + ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), + SensitivePaths: []*planproto.Path{ + { + Steps: []*planproto.Path_Step{{ + Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, + }, + }, + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Status: tfstackdata1.StateResourceInstanceObjectV1_READY, + }), + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, + Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ + ResourceInstance: &stacks.AppliedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, + }, + NewValue: &stacks.DynamicValue{ + Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + "secret": cty.StringVal("top"), + }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), + Sensitive: []*stacks.AttributePath{{ + Steps: []*stacks.AttributePath_Step{{ + Selector: &stacks.AttributePath_Step_AttributeName{AttributeName: "secret"}, + }}}, + }, + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + }, + }, + }, + }, + }, + }, + "moved_resource instance": { + Receiver: &AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "thingamajig", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + }, + }, + PreviousResourceInstanceObjectAddr: &stackaddrs.AbsResourceInstanceObject{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("a", addrs.StringKey("boop")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{Name: "foo"}, + Key: addrs.StringKey("beep"), + }, + }, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "thingy", + Name: "previous_thingamajig", + }.Instance(addrs.IntKey(1)).Absolute( + addrs.RootModuleInstance.Child("pizza", addrs.StringKey("chicken")), + ), + }, + }, + ProviderConfigAddr: addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.MustParseProviderSourceString("example.com/thingers/thingy"), + }, + Schema: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "secret": { + Type: cty.String, + Sensitive: true, + }, + }, + }, + }, + NewStateSrc: &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","secret":"top"}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("secret"), + }, + }, + }, + Want: &stacks.AppliedChange{ + Raw: []*stacks.AppliedChange_RawChange{ + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.previous_thingamajig[1],cur`, + Value: nil, + }, + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, + Value: mustMarshalAnyPb(t, &tfstackdata1.StateResourceInstanceObjectV1{ + ValueJson: json.RawMessage(`{"id":"bar","secret":"top"}`), + SensitivePaths: []*planproto.Path{ + { + Steps: []*planproto.Path_Step{{ + Selector: &planproto.Path_Step_AttributeName{AttributeName: "secret"}}}, + }, + }, + ProviderConfigAddr: `provider["example.com/thingers/thingy"]`, + Status: tfstackdata1.StateResourceInstanceObjectV1_READY, + }), + }, + }, + Descriptions: []*stacks.AppliedChange_ChangeDescription{ + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.previous_thingamajig[1],cur`, + Description: &stacks.AppliedChange_ChangeDescription_Moved{ + Moved: &stacks.AppliedChange_Nothing{}, + }, + }, + { + Key: `RSRCstack.a["boop"].component.foo["beep"],module.pizza["chicken"].thingy.thingamajig[1],cur`, + Description: &stacks.AppliedChange_ChangeDescription_ResourceInstance{ + ResourceInstance: &stacks.AppliedChange_ResourceInstance{ + Addr: &stacks.ResourceInstanceObjectInStackAddr{ + ComponentInstanceAddr: `stack.a["boop"].component.foo["beep"]`, + ResourceInstanceAddr: `module.pizza["chicken"].thingy.thingamajig[1]`, + }, + NewValue: &stacks.DynamicValue{ + Msgpack: mustMsgpack(t, cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + "secret": cty.StringVal("top"), + }), cty.Object(map[string]cty.Type{"id": cty.String, "secret": cty.String})), + Sensitive: []*stacks.AttributePath{{ + Steps: []*stacks.AttributePath_Step{{ + Selector: &stacks.AttributePath_Step_AttributeName{AttributeName: "secret"}, + }}}, + }, + }, + ResourceMode: stacks.ResourceMode_MANAGED, + ResourceType: "thingy", + ProviderAddr: "example.com/thingers/thingy", + }, + }, + }, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + got, err := test.Receiver.AppliedChangeProto() + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(test.Want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } + }) + } +} + +func mustMarshalAnyPb(t *testing.T, msg proto.Message) *anypb.Any { + var ret anypb.Any + err := anypb.MarshalFrom(&ret, msg, proto.MarshalOptions{}) + if err != nil { + t.Fatalf("error marshalling anypb: %q", err) + } + return &ret +} + +func mustMsgpack(t *testing.T, v cty.Value, ty cty.Type) []byte { + t.Helper() + + ret, err := ctymsgpack.Marshal(v, ty) + if err != nil { + t.Fatalf("error marshalling %#v: %s", v, err) + } + + return ret +} diff --git a/internal/stacks/stackstate/from_proto.go b/internal/stacks/stackstate/from_proto.go new file mode 100644 index 0000000000..d0a6495322 --- /dev/null +++ b/internal/stacks/stackstate/from_proto.go @@ -0,0 +1,371 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "fmt" + "log" + "sync" + + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" +) + +// A helper for loading prior state snapshots in a streaming manner. +type Loader struct { + ret *State + mu sync.Mutex +} + +// Constructs a new [Loader], with an initial empty state. +func NewLoader() *Loader { + ret := NewState() + ret.inputRaw = make(map[string]*anypb.Any) + return &Loader{ + ret: ret, + } +} + +// AddRaw adds a single raw state object to the state being loaded. +func (l *Loader) AddRaw(rawKey string, rawMsg *anypb.Any) error { + l.mu.Lock() + defer l.mu.Unlock() + if l.ret == nil { + return fmt.Errorf("loader has been consumed") + } + + if _, exists := l.ret.inputRaw[rawKey]; exists { + // This suggests a client bug because the recipient of state events + // from ApplyStackChanges is supposed to keep only the latest + // object associated with each distinct key. + return fmt.Errorf("duplicate raw state object key %q", rawKey) + } + l.ret.inputRaw[rawKey] = rawMsg + + key, err := statekeys.Parse(rawKey) + if err != nil { + // "invalid" here means that it was either not syntactically + // valid at all or was a recognized type but with the wrong + // syntax for that type. + // An unrecognized key type is NOT invalid; we handle that below. + return fmt.Errorf("invalid tracking key %q in state: %w", rawKey, err) + } + + if !statekeys.RecognizedType(key) { + err = handleUnrecognizedKey(key, l.ret) + if err != nil { + return err + } + return nil + } + + if rawMsg == nil { + // This suggests a state mutation bug where a deleted object was + // written as a map entry without a value, as opposed to deleting + // the value. We tolerate this here just because otherwise it + // would be harder to recover once a state has been mutated + // incorrectly. + log.Panicf("[WARN] stackstate.Loader: key %s has no associated object; ignoring", rawKey) + return nil + } + + msg, err := anypb.UnmarshalNew(rawMsg, proto.UnmarshalOptions{}) + if err != nil { + return fmt.Errorf("invalid raw value for raw state key %q: %w", rawKey, err) + } + + err = handleProtoMsg(key, msg, l.ret) + if err != nil { + return err + } + + return nil +} + +// AddDirectProto is like AddRaw but accepts direct messages of the relevant types +// from the tfstackdata1 package, rather than the [anypb.Raw] representation +// thereof. +// +// This is primarily for internal testing purposes, where it's typically more +// convenient to write out a struct literal for one of the message types +// directly rather than having to first serialize it to [anypb.Any] only for +// it to be unserialized again promptly afterwards. +// +// Unlike [Loader.AddRaw], the object added by this function will not have +// a raw representation recorded in the "raw state" of the final result, +// because this function is bypassing the concept of raw state. [State.InputRaw] +// will therefore return a map where the given key is associated with a nil +// message. +// +// Prefer to use [Loader.AddRaw] when processing user input. This function +// cannot accept [anypb.Any] messages even though the Go compiler can't +// check that at compile time. +func (l *Loader) AddDirectProto(keyStr string, msg protoreflect.ProtoMessage) error { + l.mu.Lock() + defer l.mu.Unlock() + if l.ret == nil { + return fmt.Errorf("loader has been consumed") + } + + if _, exists := l.ret.inputRaw[keyStr]; exists { + // This suggests a client bug because the recipient of state events + // from ApplyStackChanges is supposed to keep only the latest + // object associated with each distinct key. + return fmt.Errorf("duplicate raw state object key %q", keyStr) + } + l.ret.inputRaw[keyStr] = nil // this weird entrypoint does not provide raw state + + // The following should be equivalent to the similar logic in + // [LoadFromProto] except for skipping the parsing/unmarshalling + // steps since msg is already in its in-memory form. + key, err := statekeys.Parse(keyStr) + if err != nil { + return fmt.Errorf("invalid tracking key %q: %w", keyStr, err) + } + if !statekeys.RecognizedType(key) { + err := handleUnrecognizedKey(key, l.ret) + if err != nil { + return err + } + return nil + } + err = handleProtoMsg(key, msg, l.ret) + if err != nil { + return err + } + return nil +} + +// State consumes the loaded state, making the associated loader closed to +// further additions. +func (l *Loader) State() *State { + l.mu.Lock() + defer l.mu.Unlock() + ret := l.ret + l.ret = nil + return ret +} + +// LoadFromProto produces a [State] object by decoding a raw state map. +// +// This is a helper wrapper around [Loader.AddRaw] for when the state was already +// loaded into a single map. +func LoadFromProto(msgs map[string]*anypb.Any) (*State, error) { + loader := NewLoader() + for rawKey, rawMsg := range msgs { + err := loader.AddRaw(rawKey, rawMsg) + if err != nil { + return nil, err + } + } + return loader.State(), nil +} + +// LoadFromDirectProto is a variation of the primary entry-point [LoadFromProto] +// which accepts direct messages of the relevant types from the tfstackdata1 +// package, rather than the [anypb.Raw] representation thereof. +// +// This is a helper wrapper around [Loader.AddDirectProto] for when the state +// was already built into a single map. +func LoadFromDirectProto(msgs map[string]protoreflect.ProtoMessage) (*State, error) { + loader := NewLoader() + for rawKey, rawMsg := range msgs { + err := loader.AddDirectProto(rawKey, rawMsg) + if err != nil { + return nil, err + } + } + return loader.State(), nil +} + +func handleUnrecognizedKey(key statekeys.Key, state *State) error { + // There are three different strategies for dealing with + // unrecognized keys, which we recognize based on naming + // conventions of the key types. + switch handling := key.KeyType().UnrecognizedKeyHandling(); handling { + + case statekeys.FailIfUnrecognized: + // This is for keys whose messages materially change the + // meaning of the state and so cannot be ignored. Keys + // with this treatment are forwards-incompatible (old versions + // of Terraform will fail to load a state containing them) so + // should be added only as a last resort. + return fmt.Errorf("state was created by a newer version of Terraform Core (unrecognized tracking key %q)", statekeys.String(key)) + + case statekeys.PreserveIfUnrecognized: + // This is for keys whose messages can safely be left entirely + // unchanged if applying a plan with a version of Terraform + // that doesn't understand them. Keys in this category should + // typically be standalone and not refer to or depend on any + // other objects in the state, to ensure that removing or + // updating other objects will not cause the preserved message + // to become misleading or invalid. + // We don't need to do anything special with these ones because + // the caller should preserve any object we don't explicitly + // update or delete during the apply phase. + return nil + + case statekeys.DiscardIfUnrecognized: + // This is for keys which can be discarded when planning or + // applying with an older version of Terraform that doesn't + // understand them. This category is for optional ancillary + // information -- not actually required for correct subsequent + // planning -- especially if it could be recomputed again and + // repopulated if later planning and applying with a newer + // version of Terraform Core. + // For these ones we need to remember their keys so that we + // can emit "delete" messages early in the apply phase to + // actually discard them from the caller's records. + state.discardUnsupportedKeys.Add(key) + return nil + + default: + // Should not get here. The above should be exhaustive. + panic(fmt.Sprintf("unsupported UnrecognizedKeyHandling value %s", handling)) + } +} + +func handleProtoMsg(key statekeys.Key, msg protoreflect.ProtoMessage, state *State) error { + switch key := key.(type) { + + case statekeys.ComponentInstance: + return handleComponentInstanceMsg(key, msg, state) + + case statekeys.ResourceInstanceObject: + return handleResourceInstanceObjectMsg(key, msg, state) + + case statekeys.Output: + return handleOutputMsg(key, msg, state) + + case statekeys.Variable: + return handleVariableMsg(key, msg, state) + + default: + // Should not get here: the above should be exhaustive for all + // possible key types. + panic(fmt.Sprintf("unsupported state key type %T", key)) + } +} + +func handleVariableMsg(key statekeys.Variable, msg protoreflect.ProtoMessage, state *State) error { + switch msg := msg.(type) { + case *emptypb.Empty: + // for backwards compatibility reasons, ephemeral values used to be + // stored in state as empty messages. We'll upgrade these to null + // values with ephemeral marks. + state.addInputVariable(key.VariableAddr, cty.NullVal(cty.DynamicPseudoType)) + return nil + case *tfstackdata1.DynamicValue: + value, err := tfstackdata1.DynamicValueFromTFStackData1(msg, cty.DynamicPseudoType) + if err != nil { + return fmt.Errorf("failed to decode %s: %w", key.VariableAddr, err) + } + state.addInputVariable(key.VariableAddr, value) + return nil + default: + return fmt.Errorf("unsupported message type %T for %s state", msg, key.VariableAddr) + } +} + +func handleOutputMsg(key statekeys.Output, msg protoreflect.ProtoMessage, state *State) error { + outputState, ok := msg.(*tfstackdata1.DynamicValue) + if !ok { + return fmt.Errorf("unsupported message type %T for %s state", msg, key.OutputAddr) + } + + value, err := tfstackdata1.DynamicValueFromTFStackData1(outputState, cty.DynamicPseudoType) + if err != nil { + return fmt.Errorf("failed to decode %s: %w", key.OutputAddr, err) + } + + state.addOutputValue(key.OutputAddr, value) + return nil +} + +func handleComponentInstanceMsg(key statekeys.ComponentInstance, msg protoreflect.ProtoMessage, state *State) error { + // For this particular object type all of the information is in the key, + // for now at least. + componentState, ok := msg.(*tfstackdata1.StateComponentInstanceV1) + if !ok { + return fmt.Errorf("unsupported message type %T for %s state", msg, key.ComponentInstanceAddr) + } + + instance := state.ensureComponentInstanceState(key.ComponentInstanceAddr) + + for _, addr := range componentState.DependencyAddrs { + stackaddr, diags := stackaddrs.ParseAbsComponentInstanceStr(addr) + if diags.HasErrors() { + return fmt.Errorf("invalid required component address %q for %s", addr, key.ComponentInstanceAddr) + } + instance.dependencies.Add(stackaddrs.AbsComponent{ + Stack: stackaddr.Stack, + Item: stackaddr.Item.Component, + }) + } + + for _, addr := range componentState.DependentAddrs { + stackaddr, diags := stackaddrs.ParseAbsComponentInstanceStr(addr) + if diags.HasErrors() { + return fmt.Errorf("invalid required component address %q for %s", addr, key.ComponentInstanceAddr) + } + instance.dependents.Add(stackaddrs.AbsComponent{ + Stack: stackaddr.Stack, + Item: stackaddr.Item.Component, + }) + } + + for name, output := range componentState.OutputValues { + value, err := tfstackdata1.DynamicValueFromTFStackData1(output, cty.DynamicPseudoType) + if err != nil { + return fmt.Errorf("decoding output value %q for %s: %w", name, key.ComponentInstanceAddr, err) + } + instance.outputValues[addrs.OutputValue{Name: name}] = value + } + + for name, input := range componentState.InputVariables { + value, err := tfstackdata1.DynamicValueFromTFStackData1(input, cty.DynamicPseudoType) + if err != nil { + return fmt.Errorf("decoding input value %q for %s: %w", name, key.ComponentInstanceAddr, err) + } + instance.inputVariables[addrs.InputVariable{Name: name}] = value + } + + return nil +} + +func handleResourceInstanceObjectMsg(key statekeys.ResourceInstanceObject, msg protoreflect.ProtoMessage, state *State) error { + fullAddr := stackaddrs.AbsResourceInstanceObject{ + Component: key.ResourceInstance.Component, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: key.ResourceInstance.Item, + DeposedKey: key.DeposedKey, + }, + } + + riMsg, ok := msg.(*tfstackdata1.StateResourceInstanceObjectV1) + if !ok { + return fmt.Errorf("unsupported message type %T for state of %s", msg, fullAddr.String()) + } + + objSrc, err := tfstackdata1.DecodeProtoResourceInstanceObject(riMsg) + if err != nil { + return fmt.Errorf("invalid stored state object for %s: %w", fullAddr, err) + } + + providerConfigAddr, diags := addrs.ParseAbsProviderConfigStr(riMsg.ProviderConfigAddr) + if diags.HasErrors() { + return fmt.Errorf("provider configuration reference %q for %s", riMsg.ProviderConfigAddr, fullAddr) + } + + state.addResourceInstanceObject(fullAddr, objSrc, providerConfigAddr) + return nil +} diff --git a/internal/stacks/stackstate/from_proto_test.go b/internal/stacks/stackstate/from_proto_test.go new file mode 100644 index 0000000000..21fe71a4b9 --- /dev/null +++ b/internal/stacks/stackstate/from_proto_test.go @@ -0,0 +1,103 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/stacks/tfstackdata1" + "github.com/hashicorp/terraform/internal/states" + "github.com/zclconf/go-cty/cty" +) + +func TestLoader_basic(t *testing.T) { + aComponentInstAddr := stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "a", + }, + }, + } + aResourceInstAddr := stackaddrs.AbsResourceInstance{ + Component: aComponentInstAddr, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "foo", + }, + }, + }, + } + providerAddr := addrs.NewBuiltInProvider("test") + providerInstAddr := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: providerAddr, + } + + loader := NewLoader() + loader.AddDirectProto( + statekeys.String(statekeys.ComponentInstance{ + ComponentInstanceAddr: aComponentInstAddr, + }), + &tfstackdata1.StateComponentInstanceV1{ + OutputValues: make(map[string]*tfstackdata1.DynamicValue), + }, + ) + attrs := ` + { + "for_module": "a", + "arg": null, + "result": "result for \"a\"" + } +` + loader.AddDirectProto( + statekeys.String(statekeys.ResourceInstanceObject{ + ResourceInstance: aResourceInstAddr, + }), + &tfstackdata1.StateResourceInstanceObjectV1{ + Status: tfstackdata1.StateResourceInstanceObjectV1_READY, + ProviderConfigAddr: providerInstAddr.String(), + ValueJson: []byte(attrs), + }, + ) + state := loader.State() + + if !state.HasComponentInstance(aComponentInstAddr) { + t.Errorf("component instance %s not found in state", aComponentInstAddr) + } + + got := state.ResourceInstanceObjectSrc( + stackaddrs.AbsResourceInstanceObject{ + Component: aComponentInstAddr, + Item: aResourceInstAddr.Item.CurrentObject(), + }, + ) + want := &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(attrs), + AttrSensitivePaths: []cty.Path{}, + Status: states.ObjectReady, + } + + if diff := cmp.Diff(got, want, cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{})); diff != "" { + t.Errorf("unexpected resource instance object\ndiff: %s", diff) + } +} + +func TestLoader_consumed(t *testing.T) { + loader := NewLoader() + loader.State() + err := loader.AddRaw("foo", nil) + if err == nil { + t.Error("expected error on mutating consumed loader") + } +} diff --git a/internal/stacks/stackstate/from_state.go b/internal/stacks/stackstate/from_state.go new file mode 100644 index 0000000000..c8030f1076 --- /dev/null +++ b/internal/stacks/stackstate/from_state.go @@ -0,0 +1,177 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "context" + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackplan" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// StateProducer is an interface of an object that can produce a state file +// and required it to be into AppliedChange objects. +type StateProducer interface { + Addr() stackaddrs.AbsComponentInstance + + // ResourceSchema returns the schema for a resource type from a provider. + ResourceSchema(ctx context.Context, providerTypeAddr addrs.Provider, mode addrs.ResourceMode, resourceType string) (providers.Schema, error) +} + +func FromState(ctx context.Context, state *states.State, plan *stackplan.Component, applyTimeInputs cty.Value, affectedResources addrs.Set[addrs.AbsResourceInstanceObject], producer StateProducer) ([]AppliedChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var changes []AppliedChange + + addr := producer.Addr() + + for _, rioAddr := range affectedResources { + os := state.ResourceInstanceObjectSrc(rioAddr) + var providerConfigAddr addrs.AbsProviderConfig + var schema providers.Schema + if os != nil { + rAddr := rioAddr.ResourceInstance.ContainingResource() + rs := state.Resource(rAddr) + if rs == nil { + // We should not get here: it should be impossible to + // have state for a resource instance object without + // also having state for its containing resource, because + // the object is nested inside the resource state. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Inconsistent updated state for resource", + fmt.Sprintf( + "There is a state for %s specifically, but somehow no state for its containing resource %s. This is a bug in Terraform.", + rioAddr, rAddr, + ), + )) + continue + } + providerConfigAddr = rs.ProviderConfig + + var err error + schema, err = producer.ResourceSchema( + ctx, + rs.ProviderConfig.Provider, + rAddr.Resource.Mode, + rAddr.Resource.Type, + ) + if err != nil { + // It shouldn't be possible to get here because we would've + // used the same schema we were just trying to retrieve + // to encode the dynamic data in this states.State object + // in the first place. If we _do_ get here then we won't + // actually be able to save the updated state, which will + // force the user to manually clean things up. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Can't fetch provider schema to save new state", + fmt.Sprintf( + "Failed to retrieve the schema for %s from provider %s: %s. This is a bug in Terraform.\n\nThe new state for this object cannot be saved. If this object was only just created, you may need to delete it manually in the target system to reconcile with the Terraform state before trying again.", + rAddr, rs.ProviderConfig.Provider, err, + ), + )) + continue + } + } else { + // Our model doesn't have any way to represent the absense + // of a provider configuration, so if we're trying to describe + // just that the object has been deleted then we'll just + // use a synthetic provider config address, this won't get + // used for anything significant anyway. + providerAddr := addrs.ImpliedProviderForUnqualifiedType(rioAddr.ResourceInstance.Resource.Resource.ImpliedProvider()) + providerConfigAddr = addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: providerAddr, + } + } + + var previousAddress *stackaddrs.AbsResourceInstanceObject + if plannedChange := plan.ResourceInstancePlanned.Get(rioAddr); plannedChange != nil && plannedChange.Moved() { + // If we moved the resource instance object, we need to record + // the previous address in the applied change. The planned + // change might be nil if the resource instance object was + // deleted. + previousAddress = &stackaddrs.AbsResourceInstanceObject{ + Component: addr, + Item: addrs.AbsResourceInstanceObject{ + ResourceInstance: plannedChange.PrevRunAddr, + DeposedKey: addrs.NotDeposed, + }, + } + } + + changes = append(changes, &AppliedChangeResourceInstanceObject{ + ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{ + Component: addr, + Item: rioAddr, + }, + PreviousResourceInstanceObjectAddr: previousAddress, + NewStateSrc: os, + ProviderConfigAddr: providerConfigAddr, + Schema: schema, + }) + } + + destroyPlan := plan.PlannedAction == plans.Delete || plan.PlannedAction == plans.Forget + if plan.PlanComplete && destroyPlan && state.Empty() && !diags.HasErrors() { + + // We'll publish a special change type for the case where the + // component instance was deleted and the state is now empty. + // + // We check here that we: + // - were planning to delete the component instance + // - have a complete plan (so no changes were deferred) + // - the state is now empty (so everything was actually deleted) + // - there were no errors in the diagnostics (so we published all changes) + // + // If all of the above are true, we'll happily publish this special + // change type to indicate that the component instance was deleted. + // + // If the above weren't true then we'll publish the normal update + // change type, which will mean this component stays in state for + // now and will be tidied up properly in a follow-up change. + + changes = append(changes, &AppliedChangeComponentInstanceRemoved{ + ComponentAddr: stackaddrs.AbsComponent{ + Stack: addr.Stack, + Item: addr.Item.Component, + }, + ComponentInstanceAddr: addr, + }) + } else { + ourChange := &AppliedChangeComponentInstance{ + ComponentAddr: stackaddrs.AbsComponent{ + Stack: addr.Stack, + Item: addr.Item.Component, + }, + ComponentInstanceAddr: addr, + Dependents: plan.Dependents, + Dependencies: plan.Dependencies, + OutputValues: make(map[addrs.OutputValue]cty.Value, len(state.RootOutputValues)), + InputVariables: make(map[addrs.InputVariable]cty.Value, len(applyTimeInputs.Type().AttributeTypes())), + } + for name, os := range state.RootOutputValues { + val := os.Value + if os.Sensitive { + val = val.Mark(marks.Sensitive) + } + ourChange.OutputValues[addrs.OutputValue{Name: name}] = val + } + for name, value := range applyTimeInputs.AsValueMap() { + ourChange.InputVariables[addrs.InputVariable{Name: name}] = value + } + changes = append(changes, ourChange) + } + + return changes, diags +} diff --git a/internal/stacks/stackstate/state.go b/internal/stacks/stackstate/state.go new file mode 100644 index 0000000000..c98c931642 --- /dev/null +++ b/internal/stacks/stackstate/state.go @@ -0,0 +1,473 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "iter" + + "github.com/zclconf/go-cty/cty" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/stacks/stackstate/statekeys" + "github.com/hashicorp/terraform/internal/states" +) + +// State represents a previous run's state snapshot. +// +// Unlike [states.State] and its associates, State is an immutable data +// structure constructed to represent only the previous run state. It should +// not be modified after it's been constructed; results of planning or applying +// changes are represented in other ways inside the stacks language runtime. +type State struct { + root *stackInstanceState + outputs map[stackaddrs.OutputValue]cty.Value + inputs map[stackaddrs.InputVariable]cty.Value + + // discardUnsupportedKeys is the set of state keys that we encountered + // during decoding which are of types that are not supported by this + // version of Terraform, if and only if they are of a type which is + // specified as being discarded when unrecognized. We should emit + // events during the apply phase to delete the objects associated with + // these keys. + discardUnsupportedKeys collections.Set[statekeys.Key] + + inputRaw map[string]*anypb.Any +} + +// NewState constructs a new, empty state. +func NewState() *State { + return &State{ + root: newStackInstanceState(stackaddrs.RootStackInstance), + outputs: make(map[stackaddrs.OutputValue]cty.Value), + inputs: make(map[stackaddrs.InputVariable]cty.Value), + discardUnsupportedKeys: statekeys.NewKeySet(), + inputRaw: nil, + } +} + +// RootInputVariables returns the values for the input variables currently in +// the state. An address that is in the map and maps to cty.NilVal is an +// ephemeral input, so it was present during the last operation but the value +// in unknown. Compared to an input variable not in the map at all, which +// indicates a new input variable that wasn't in the configuration during the +// last operation. +func (s *State) RootInputVariables() map[stackaddrs.InputVariable]cty.Value { + return s.inputs +} + +// RootInputVariable returns the input variable defined at the given address. +// If the second return value is true, then the value is present but is +// ephemeral and not known. If the first returned value is cty.NilVal and the +// second is false then the value isn't present in the state. +func (s *State) RootInputVariable(addr stackaddrs.InputVariable) cty.Value { + return s.inputs[addr] +} + +func (s *State) RootOutputValues() map[stackaddrs.OutputValue]cty.Value { + return s.outputs +} + +func (s *State) RootOutputValue(addr stackaddrs.OutputValue) cty.Value { + return s.outputs[addr] +} + +func (s *State) HasComponentInstance(addr stackaddrs.AbsComponentInstance) bool { + stack := s.root.getDescendent(addr.Stack) + if stack == nil { + return false + } + return stack.getComponentInstance(addr.Item) != nil +} + +func (s *State) HasStackInstance(addr stackaddrs.StackInstance) bool { + stack := s.root.getDescendent(addr) + return stack != nil +} + +// AllComponentInstances returns a set of addresses for all of the component +// instances that are tracked in the state. +// +// This includes both instances that were explicitly represented in the source +// raw state _and_ any that were missing but implied by a resource instance +// existing inside them. There should typically be an explicit component +// instance record tracked in raw state, but it can potentially be absent in +// exceptional cases such as if Terraform Core crashed partway through the +// previous run. +func (s *State) AllComponentInstances() iter.Seq[stackaddrs.AbsComponentInstance] { + return func(yield func(stackaddrs.AbsComponentInstance) bool) { + s.root.iterate(yield) + } +} + +// ComponentInstances returns the set of component instances that belong to the +// given component, or an empty set if no such component is tracked in the +// state. +// +// This will always be a subset of AllComponentInstances. +func (s *State) ComponentInstances(addr stackaddrs.AbsComponent) iter.Seq[stackaddrs.ComponentInstance] { + return func(yield func(stackaddrs.ComponentInstance) bool) { + target := s.root.getDescendent(addr.Stack) + if target == nil { + return + } + + for key := range target.components[addr.Item] { + yield(stackaddrs.ComponentInstance{ + Component: addr.Item, + Key: key, + }) + } + } +} + +// StackInstances returns the set of known stack instances for the given stack +// call. +func (s *State) StackInstances(call stackaddrs.AbsStackCall) iter.Seq[stackaddrs.StackInstance] { + return func(yield func(stackaddrs.StackInstance) bool) { + target := s.root.getDescendent(call.Stack) + if target == nil { + return + } + + for _, stack := range target.children[call.Item.Name] { + yield(stack.address) + } + } +} + +func (s *State) componentInstanceState(addr stackaddrs.AbsComponentInstance) *componentInstanceState { + target := s.root.getDescendent(addr.Stack) + if target == nil { + return nil + } + return target.getComponentInstance(addr.Item) +} + +// DependenciesForComponent returns the list of components that are required by +// the given component instance, or an empty set if no such component instance +// is tracked in the state. +func (s *State) DependenciesForComponent(addr stackaddrs.AbsComponentInstance) collections.Set[stackaddrs.AbsComponent] { + cs := s.componentInstanceState(addr) + if cs == nil { + return collections.NewSet[stackaddrs.AbsComponent]() + } + return cs.dependencies +} + +// DependentsForComponent returns the list of components that are require the +// given component instance, or an empty set if no such component instance is +// tracked in the state. +func (s *State) DependentsForComponent(addr stackaddrs.AbsComponentInstance) collections.Set[stackaddrs.AbsComponent] { + cs := s.componentInstanceState(addr) + if cs == nil { + return collections.NewSet[stackaddrs.AbsComponent]() + } + return cs.dependents +} + +// ResultsForComponent returns the output values for the given component +// instance, or nil if no such component instance is tracked in the state. +func (s *State) ResultsForComponent(addr stackaddrs.AbsComponentInstance) map[addrs.OutputValue]cty.Value { + cs := s.componentInstanceState(addr) + if cs == nil { + return nil + } + return cs.outputValues +} + +// InputsForComponent returns the input values for the given component +// instance, or nil if no such component instance is tracked in the state. +func (s *State) InputsForComponent(addr stackaddrs.AbsComponentInstance) map[addrs.InputVariable]cty.Value { + cs := s.componentInstanceState(addr) + if cs == nil { + return nil + } + return cs.inputVariables +} + +type IdentitySrc struct { + IdentitySchemaVersion uint64 + IdentityJSON []byte +} + +// IdentitiesForComponent returns the identity values for the given component +// instance, or nil if no such component instance is tracked in the state. +func (s *State) IdentitiesForComponent(addr stackaddrs.AbsComponentInstance) map[*addrs.AbsResourceInstanceObject]IdentitySrc { + cs := s.componentInstanceState(addr) + if cs == nil { + return nil + } + + res := make(map[*addrs.AbsResourceInstanceObject]IdentitySrc) + for _, rio := range cs.resourceInstanceObjects.Elements() { + res[&rio.Key] = IdentitySrc{ + IdentitySchemaVersion: rio.Value.src.IdentitySchemaVersion, + IdentityJSON: rio.Value.src.IdentityJSON, + } + } + + return res +} + +// ComponentInstanceResourceInstanceObjects returns a set of addresses for +// all of the resource instance objects belonging to the component instance +// with the given address. +func (s *State) ComponentInstanceResourceInstanceObjects(addr stackaddrs.AbsComponentInstance) collections.Set[stackaddrs.AbsResourceInstanceObject] { + var ret collections.Set[stackaddrs.AbsResourceInstanceObject] + cs := s.componentInstanceState(addr) + if cs == nil { + return ret + } + ret = collections.NewSet[stackaddrs.AbsResourceInstanceObject]() + for _, elem := range cs.resourceInstanceObjects.Elems { + objKey := stackaddrs.AbsResourceInstanceObject{ + Component: addr, + Item: elem.Key, + } + ret.Add(objKey) + } + return ret +} + +// ResourceInstanceObjectSrc returns the source (i.e. still encoded) version of +// the resource instance object for the given address, or nil if no such +// object is tracked in the state. +func (s *State) ResourceInstanceObjectSrc(addr stackaddrs.AbsResourceInstanceObject) *states.ResourceInstanceObjectSrc { + rios := s.resourceInstanceObjectState(addr) + if rios == nil { + return nil + } + return rios.src +} + +// RequiredProviderInstances returns a description of all of the provider +// instance slots that are required to satisfy the resource instances +// belonging to the given component instance. +// +// See also stackeval.ComponentConfig.RequiredProviderInstances for a similar +// function that operates on the configuration of a component instance rather +// than the state of one. +func (s *State) RequiredProviderInstances(component stackaddrs.AbsComponentInstance) addrs.Set[addrs.RootProviderConfig] { + state := s.componentInstanceState(component) + if state == nil { + // Then we have no state for this component, which is fine. + return addrs.MakeSet[addrs.RootProviderConfig]() + } + + providerInstances := addrs.MakeSet[addrs.RootProviderConfig]() + for _, elem := range state.resourceInstanceObjects.Elems { + providerInstances.Add(addrs.RootProviderConfig{ + Provider: elem.Value.providerConfigAddr.Provider, + Alias: elem.Value.providerConfigAddr.Alias, + }) + } + return providerInstances +} + +func (s *State) resourceInstanceObjectState(addr stackaddrs.AbsResourceInstanceObject) *resourceInstanceObjectState { + cs := s.componentInstanceState(addr.Component) + if cs == nil { + return nil + } + return cs.resourceInstanceObjects.Get(addr.Item) +} + +// ComponentInstanceStateForModulesRuntime returns a [states.State] +// representation of the objects tracked for the given component instance. +// +// This produces only a very bare-bones [states.State] that should be +// sufficient for use as a prior state for the modules runtime's plan function +// to consider, but likely won't be of much other use. +func (s *State) ComponentInstanceStateForModulesRuntime(addr stackaddrs.AbsComponentInstance) *states.State { + return states.BuildState(func(ss *states.SyncState) { + objAddrs := s.ComponentInstanceResourceInstanceObjects(addr) + for objAddr := range objAddrs.All() { + rios := s.resourceInstanceObjectState(objAddr) + + if objAddr.Item.IsCurrent() { + ss.SetResourceInstanceCurrent( + objAddr.Item.ResourceInstance, + rios.src, rios.providerConfigAddr, + ) + } else { + ss.SetResourceInstanceDeposed( + objAddr.Item.ResourceInstance, objAddr.Item.DeposedKey, + rios.src, rios.providerConfigAddr, + ) + } + } + }) +} + +// RawKeysToDiscard returns a set of raw state keys that the apply phase should +// emit "delete" events for to remove objects from the raw state map that +// will no longer be relevant or meaningful after this plan is applied. +// +// Do not modify the returned set. +func (s *State) RawKeysToDiscard() collections.Set[statekeys.Key] { + return s.discardUnsupportedKeys +} + +// InputRaw returns the raw representation of state that this object was built +// from, or nil if this object wasn't constructed by decoding a protocol buffers +// representation. +// +// All callers of this method get the same map, so callers must not modify +// the map or anything reachable through it. +func (s *State) InputRaw() map[string]*anypb.Any { + return s.inputRaw +} + +func (s *State) addOutputValue(addr stackaddrs.OutputValue, value cty.Value) { + s.outputs[addr] = value +} + +func (s *State) addInputVariable(addr stackaddrs.InputVariable, value cty.Value) { + s.inputs[addr] = value +} + +func (s *State) ensureComponentInstanceState(addr stackaddrs.AbsComponentInstance) *componentInstanceState { + current := s.root + for _, step := range addr.Stack { + next := current.getChild(step) + if next == nil { + next = newStackInstanceState(append(current.address, step)) + + children, ok := current.children[step.Name] + if !ok { + children = make(map[addrs.InstanceKey]*stackInstanceState) + } + children[step.Key] = next + current.children[step.Name] = children + } + current = next + } + + component := current.getComponentInstance(addr.Item) + if component == nil { + component = &componentInstanceState{ + dependencies: collections.NewSet[stackaddrs.AbsComponent](), + dependents: collections.NewSet[stackaddrs.AbsComponent](), + outputValues: make(map[addrs.OutputValue]cty.Value), + inputVariables: make(map[addrs.InputVariable]cty.Value), + resourceInstanceObjects: addrs.MakeMap[addrs.AbsResourceInstanceObject, *resourceInstanceObjectState](), + } + + components, ok := current.components[addr.Item.Component] + if !ok { + components = make(map[addrs.InstanceKey]*componentInstanceState) + } + components[addr.Item.Key] = component + current.components[addr.Item.Component] = components + } + return component +} + +func (s *State) addResourceInstanceObject(addr stackaddrs.AbsResourceInstanceObject, src *states.ResourceInstanceObjectSrc, providerConfigAddr addrs.AbsProviderConfig) { + cs := s.ensureComponentInstanceState(addr.Component) + + cs.resourceInstanceObjects.Put(addr.Item, &resourceInstanceObjectState{ + src: src, + providerConfigAddr: providerConfigAddr, + }) +} + +type componentInstanceState struct { + // dependencies is the set of component instances that this component + // depended on the last time it was updated. + dependencies collections.Set[stackaddrs.AbsComponent] + + // dependents is a set of component instances that depended on this + // component the last time it was updated. + dependents collections.Set[stackaddrs.AbsComponent] + + // outputValues is a map from output value addresses to their values at + // completion of the last apply operation. + outputValues map[addrs.OutputValue]cty.Value + + // inputVariables is a map from input variable addresses to their values at + // completion of the last apply operation. + inputVariables map[addrs.InputVariable]cty.Value + + // resourceInstanceObjects is a map from resource instance object addresses + // to their state. + resourceInstanceObjects addrs.Map[addrs.AbsResourceInstanceObject, *resourceInstanceObjectState] +} + +type resourceInstanceObjectState struct { + src *states.ResourceInstanceObjectSrc + providerConfigAddr addrs.AbsProviderConfig +} + +type stackInstanceState struct { + address stackaddrs.StackInstance + components map[stackaddrs.Component]map[addrs.InstanceKey]*componentInstanceState + children map[string]map[addrs.InstanceKey]*stackInstanceState +} + +func newStackInstanceState(address stackaddrs.StackInstance) *stackInstanceState { + return &stackInstanceState{ + address: address, + components: make(map[stackaddrs.Component]map[addrs.InstanceKey]*componentInstanceState), + children: make(map[string]map[addrs.InstanceKey]*stackInstanceState), + } +} + +func (s *stackInstanceState) getDescendent(stack stackaddrs.StackInstance) *stackInstanceState { + if len(stack) == 0 { + return s + } + + next := s.getChild(stack[0]) + if next == nil { + return nil + } + return next.getDescendent(stack[1:]) +} + +func (s *stackInstanceState) getChild(step stackaddrs.StackInstanceStep) *stackInstanceState { + stacks, ok := s.children[step.Name] + if !ok { + return nil + } + return stacks[step.Key] +} + +func (s *stackInstanceState) getComponentInstance(component stackaddrs.ComponentInstance) *componentInstanceState { + components, ok := s.components[component.Component] + if !ok { + return nil + } + return components[component.Key] +} + +func (s *stackInstanceState) iterate(yield func(stackaddrs.AbsComponentInstance) bool) bool { + for component, components := range s.components { + for key := range components { + proceed := yield(stackaddrs.AbsComponentInstance{ + Stack: s.address, + Item: stackaddrs.ComponentInstance{ + Component: component, + Key: key, + }, + }) + if !proceed { + return false + } + } + } + + for _, children := range s.children { + for _, child := range children { + if !child.iterate(yield) { + return false + } + } + } + + return true +} diff --git a/internal/stacks/stackstate/state_builder.go b/internal/stacks/stackstate/state_builder.go new file mode 100644 index 0000000000..fe905c9a7a --- /dev/null +++ b/internal/stacks/stackstate/state_builder.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackstate + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +// StateBuilder wraps State, and provides some write-only methods to update the +// state. +// +// This is generally used to build up a new state from scratch during tests. +type StateBuilder struct { + state *State +} + +func NewStateBuilder() *StateBuilder { + return &StateBuilder{ + state: NewState(), + } +} + +// Build returns the state and invalidates the StateBuilder. +// +// You will get nil pointer exceptions if you attempt to use the builder after +// calling Build. +func (s *StateBuilder) Build() *State { + ret := s.state + s.state = nil + return ret +} + +// AddResourceInstance adds a resource instance to the state. +func (s *StateBuilder) AddResourceInstance(builder *ResourceInstanceBuilder) *StateBuilder { + if builder.addr == nil || builder.src == nil || builder.providerAddr == nil { + panic("ResourceInstanceBuilder is missing required fields") + } + s.state.addResourceInstanceObject(*builder.addr, builder.src, *builder.providerAddr) + return s +} + +// AddComponentInstance adds a component instance to the state. +func (s *StateBuilder) AddComponentInstance(builder *ComponentInstanceBuilder) *StateBuilder { + component := s.state.ensureComponentInstanceState(builder.addr) + component.outputValues = builder.outputValues + component.inputVariables = builder.inputVariables + + for dep := range builder.dependencies.All() { + component.dependencies.Add(dep) + } + for dep := range builder.dependents.All() { + component.dependents.Add(dep) + } + return s +} + +// AddOutput adds an output to the state. +func (s *StateBuilder) AddOutput(name string, value cty.Value) *StateBuilder { + s.state.outputs[stackaddrs.OutputValue{Name: name}] = value + return s +} + +// AddInput adds an input variable to the state. +func (s *StateBuilder) AddInput(name string, value cty.Value) *StateBuilder { + s.state.inputs[stackaddrs.InputVariable{Name: name}] = value + return s +} + +type ResourceInstanceBuilder struct { + addr *stackaddrs.AbsResourceInstanceObject + src *states.ResourceInstanceObjectSrc + providerAddr *addrs.AbsProviderConfig +} + +func NewResourceInstanceBuilder() *ResourceInstanceBuilder { + return &ResourceInstanceBuilder{} +} + +func (b *ResourceInstanceBuilder) SetAddr(addr stackaddrs.AbsResourceInstanceObject) *ResourceInstanceBuilder { + b.addr = &addr + return b +} + +func (b *ResourceInstanceBuilder) SetResourceInstanceObjectSrc(src states.ResourceInstanceObjectSrc) *ResourceInstanceBuilder { + b.src = &src + return b +} + +func (b *ResourceInstanceBuilder) SetProviderAddr(addr addrs.AbsProviderConfig) *ResourceInstanceBuilder { + b.providerAddr = &addr + return b +} + +type ComponentInstanceBuilder struct { + addr stackaddrs.AbsComponentInstance + dependencies collections.Set[stackaddrs.AbsComponent] + dependents collections.Set[stackaddrs.AbsComponent] + outputValues map[addrs.OutputValue]cty.Value + inputVariables map[addrs.InputVariable]cty.Value +} + +func NewComponentInstanceBuilder(instance stackaddrs.AbsComponentInstance) *ComponentInstanceBuilder { + return &ComponentInstanceBuilder{ + addr: instance, + dependencies: collections.NewSet[stackaddrs.AbsComponent](), + dependents: collections.NewSet[stackaddrs.AbsComponent](), + outputValues: make(map[addrs.OutputValue]cty.Value), + inputVariables: make(map[addrs.InputVariable]cty.Value), + } +} + +func (b *ComponentInstanceBuilder) AddDependency(addr stackaddrs.AbsComponent) *ComponentInstanceBuilder { + b.dependencies.Add(addr) + return b +} + +func (b *ComponentInstanceBuilder) AddDependent(addr stackaddrs.AbsComponent) *ComponentInstanceBuilder { + b.dependents.Add(addr) + return b +} + +func (b *ComponentInstanceBuilder) AddOutputValue(name string, value cty.Value) *ComponentInstanceBuilder { + b.outputValues[addrs.OutputValue{Name: name}] = value + return b +} + +func (b *ComponentInstanceBuilder) AddInputVariable(name string, value cty.Value) *ComponentInstanceBuilder { + b.inputVariables[addrs.InputVariable{Name: name}] = value + return b +} diff --git a/internal/stacks/stackstate/statekeys/collections.go b/internal/stacks/stackstate/statekeys/collections.go new file mode 100644 index 0000000000..f5425a938e --- /dev/null +++ b/internal/stacks/stackstate/statekeys/collections.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "github.com/hashicorp/terraform/internal/collections" +) + +type KeySet collections.Set[Key] + +// NewKeySet returns an initialized set of [Key] that's ready to use and +// treats two keys as unique if they have the same string representation. +func NewKeySet() collections.Set[Key] { + return collections.NewSetFunc[Key](stateKeyUniqueKey) +} + +// NewKeyMap returns an initialized map from [Key] to V that's ready to use and +// treats two keys as unique if they have the same string representation. +func NewKeyMap[V any]() collections.Map[Key, V] { + return collections.NewMapFunc[Key, V](stateKeyUniqueKey) +} + +// stateKeyCollectionsKey is an internal adapter so that [statekeys.Key] values +// can be used as [collections.Set] elements and [collections.Map] keys. +type stateKeyCollectionsKey string + +// IsUniqueKey implements collections.UniqueKey. +func (stateKeyCollectionsKey) IsUniqueKey(Key) { +} + +func stateKeyUniqueKey(k Key) collections.UniqueKey[Key] { + return stateKeyCollectionsKey(String(k)) +} diff --git a/internal/stacks/stackstate/statekeys/components.go b/internal/stacks/stackstate/statekeys/components.go new file mode 100644 index 0000000000..6f020f21e2 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/components.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +type ComponentInstance struct { + ComponentInstanceAddr stackaddrs.AbsComponentInstance +} + +func parseComponentInstance(s string) (Key, error) { + addrRaw, ok := finalKeyField(s) + if !ok { + return nil, fmt.Errorf("unsupported extra field in component instance key") + } + addr, diags := stackaddrs.ParseAbsComponentInstanceStr(addrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("component instance key has invalid component instance address %q", addrRaw) + } + return ComponentInstance{ + ComponentInstanceAddr: addr, + }, nil +} + +func (k ComponentInstance) KeyType() KeyType { + return ComponentInstanceType +} + +func (k ComponentInstance) rawSuffix() string { + return k.ComponentInstanceAddr.String() +} diff --git a/internal/stacks/stackstate/statekeys/doc.go b/internal/stacks/stackstate/statekeys/doc.go new file mode 100644 index 0000000000..856a1d2f0d --- /dev/null +++ b/internal/stacks/stackstate/statekeys/doc.go @@ -0,0 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package statekeys contains the definitions for the various different kinds +// of tracking key we use (or have historically used) for objects in a stack +// state. +// +// Stack state is a mutable data structure whose storage strategy is delegated +// to whatever is calling into Terraform Core. To allow Terraform Core to +// emit updates to that data structure piecemeal, rather than having to return +// the whole dataset over and over, we use tracking keys for each +// separately-updatable element of the state that are opaque to the caller but +// meaningful to Terraform Core. +// +// Callers are expected to use simple character-for-character string matching +// to compare these to recognize whether an update is describing an entirely +// new object or a replacement for ane existing object, and so the main +// requirement is that the content of these keys remains consistent across +// Terraform Core releases. However, from Terraform Core's perspective we +// also use these keys to carry some metadata about what is being tracked +// so we can avoid redundantly storing the same information in both the key +// and in the associated stored object. +// +// The keys defined in this package are in principle valid for use both as +// raw state keys and as external description keys, but some of them are used +// only for one or the other since the raw and external description forms +// don't necessarily have the same level of detail. +package statekeys diff --git a/internal/stacks/stackstate/statekeys/key.go b/internal/stacks/stackstate/statekeys/key.go new file mode 100644 index 0000000000..6b647de189 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key.go @@ -0,0 +1,74 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +// Key is implemented by types that can be used as state keys. +type Key interface { + // KeyType returns the [KeyType] used for keys belonging to a particular + // implementation of [Key]. + KeyType() KeyType + + // rawSuffix returns additional characters that should appear after the + // key type portion of the final raw key. + // + // This is unexported both to help prevent accidental misuse (external + // callers MUST use [String] to obtain the correct string representation], + // and to prevent implementations of this interface from other packages. + // This package is the sole authority on state keys. + rawSuffix() string +} + +// String returns the string representation of the given key, ready to be used +// in the RPC API representation of a [stackstate.AppliedChange] object. +func String(k Key) string { + if k == nil { + panic("called statekeys.String with nil Key") + } + return string(k.KeyType()) + k.rawSuffix() +} + +// RecognizedType returns true if the given key has a [KeyType] that's known +// to the current version of this package, or false otherwise. +// +// If RecognizedType returns false, use the key's KeyType method to obtain +// the unrecognized type and then use its UnrecognizedKeyHandling method +// to determine the appropriate handling for the unrecognized key type. +func RecognizedType(k Key) bool { + if k == nil { + panic("called statekeys.RecognizedType with nil Key") + } + _, unrecognized := k.(Unrecognized) + return !unrecognized +} + +// Unrecognized is a fallback [Key] implementation used when a given +// key has an unrecognized type. +// +// Unrecognized keys are round-trippable in that the RawKey method will return +// the same string that was originally parsed. Use +// KeyType.UnrecognizedKeyHandling to determine how Terraform Core should +// respond to the key having an unrecognized type. +type Unrecognized struct { + // ApparentKeyType is a [KeyType] representation of the type portion of the + // unrecognized key. Unlike most other [KeyType] values, this one + // will presumably not match any of the [KeyType] constants defined + // elsewhere in this package. + ApparentKeyType KeyType + + // Remainder is a verbatim copy of whatever appeared after the type + // in the given key string. This is preserved only for round-tripping + // purposes and so should be treated as opaque. + remainder string +} + +// KeyType returns the value from the ApparentKeyType field, which will +// presumably not match any of the [KeyType] constants in this package +// (because otherwise we would've used a different implementation of [Key]). +func (k Unrecognized) KeyType() KeyType { + return k.ApparentKeyType +} + +func (k Unrecognized) rawSuffix() string { + return k.remainder +} diff --git a/internal/stacks/stackstate/statekeys/key_build.go b/internal/stacks/stackstate/statekeys/key_build.go new file mode 100644 index 0000000000..4b6fc0a7bd --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_build.go @@ -0,0 +1,38 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "strings" +) + +// rawKeyBuilder is a helper for building multi-field keys in the format +// that's expected by [cutKeyField]. +// +// The zero value of rawKeyBuilder is ready to use. +type rawKeyBuilder struct { + b strings.Builder + w bool +} + +// AppendField appends the given string to the key-in-progress as an additional +// field. +// +// The given string must not contain any unquoted commas, because comma is the +// field delimiter. If given an invalid field value this function will panic. +func (b *rawKeyBuilder) AppendField(s string) { + if keyDelimiterIdx(s) != -1 { + panic("key field contains the field delimiter") + } + if b.w { + b.b.WriteByte(',') + } + b.w = true + b.b.WriteString(s) +} + +// Raw returns the assembled raw key string. +func (b *rawKeyBuilder) Raw() string { + return b.b.String() +} diff --git a/internal/stacks/stackstate/statekeys/key_parse.go b/internal/stacks/stackstate/statekeys/key_parse.go new file mode 100644 index 0000000000..c73c1cb985 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_parse.go @@ -0,0 +1,104 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" +) + +// Parse attempts to parse the given string as a state key, and returns the +// result if successful. +// +// A returned error means that the given string is syntactically invalid, +// which could mean either that it doesn't meet the basic requirements for +// any state key, or that it has a recognized key type but the remainder is +// not valid for that type. +// +// Parse DOES NOT return an error for a syntactically-valid key of an +// unrecognized type. Instead, it returns an [UnrecognizedKey] value which +// callers can detect using [RecognizedType], which will return false for +// a key of an unrecognized type. +func Parse(raw string) (Key, error) { + if len(raw) < 4 { + // All state keys must have at least four characters, since that's + // how long a key prefix is. + return nil, fmt.Errorf("too short to be a valid state key") + } + keyType := KeyType(raw[:4]) + remain := raw[4:] + parser := keyParsers[keyType] + if parser == nil { + if !isPlausibleRawKeyType(string(keyType)) { + return nil, fmt.Errorf("invalid key type prefix %q", keyType) + } + return Unrecognized{ + ApparentKeyType: keyType, + remainder: remain, + }, nil + } + return parser(remain) +} + +var keyParsers = map[KeyType]func(string) (Key, error){ + ResourceInstanceObjectType: parseResourceInstanceObject, + ComponentInstanceType: parseComponentInstance, + OutputType: parseOutput, + VariableType: parseVariable, +} + +// cutKeyField is a key parsing helper for key types that consist of +// multiple fields concatenated together. +// +// cutKeyField returns the raw string content of the next field, and +// also returns any remaining text after the field delimeter which +// could therefore be used in a subsequent call to cutKeyField. +// +// The field delimiter is a comma, but the parser ignores any comma +// that appears to be inside a pair of double-quote characters (") +// so that it's safe to include an address with a string-based instance key +// (which could potentially contain a literal comma) and get back that same +// address as a single field. +// +// If the given string does not contain any delimiters, the result is the +// same string verbatim and an empty "remain" result. +func cutKeyField(raw string) (field, remain string) { + i := keyDelimiterIdx(raw) + if i == -1 { + return raw, "" + } + return raw[:i], raw[i+1:] +} + +// finalKeyField returns the given string and true if it doesn't contain a key +// field delimiter, or "", false if the string does have a delimiter. +func finalKeyField(raw string) (string, bool) { + i := keyDelimiterIdx(raw) + if i != -1 { + return "", false + } + return raw, true +} + +// keyDelimiterIdx finds the index of the first delimiter in the given +// string, or returns -1 if there is no delimiter in the string. +func keyDelimiterIdx(raw string) int { + inQuotes := false + escape := false + for i, c := range raw { + if c == ',' && !inQuotes { + return i + } + if c == '\\' { + escape = true + continue + } + if c == '"' && !escape { + inQuotes = !inQuotes + } + escape = false + } + // If we fall out here then the entire string seems to be + // a single field, with no delimiters. + return -1 +} diff --git a/internal/stacks/stackstate/statekeys/key_parse_test.go b/internal/stacks/stackstate/statekeys/key_parse_test.go new file mode 100644 index 0000000000..65abf7a951 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_parse_test.go @@ -0,0 +1,358 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +func TestParse(t *testing.T) { + tests := []struct { + Input string + Want Key + WantErr string + + WantUnrecognizedHandling UnrecognizedKeyHandling + }{ + { + Input: "", + WantErr: `too short to be a valid state key`, + }, + { + Input: "a", + WantErr: `too short to be a valid state key`, + }, + { + Input: "aa", + WantErr: `too short to be a valid state key`, + }, + { + Input: "aaa", + WantErr: `too short to be a valid state key`, + }, + { + Input: "aaa!", // this is a suitable length but contains an invalid character + WantErr: `invalid key type prefix "aaa!"`, + }, + { + Input: "aaaa", + Want: Unrecognized{ + ApparentKeyType: KeyType("aaaa"), + remainder: "", + }, + WantUnrecognizedHandling: DiscardIfUnrecognized, + }, + { + Input: "AAAA", + Want: Unrecognized{ + ApparentKeyType: KeyType("AAAA"), + remainder: "", + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + Input: "aaaA", + Want: Unrecognized{ + ApparentKeyType: KeyType("aaaA"), + remainder: "", + }, + WantUnrecognizedHandling: PreserveIfUnrecognized, + }, + + // Resource instance object keys + { + Input: "RSRC", + WantErr: `resource instance object key has invalid component instance address ""`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,cur", + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + // Commas inside quoted instance keys are not treated as + // delimiters. + Input: `RSRCcomponent.foo["a,a"],aws_instance.bar["c,c"],cur`, + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("a,a"), + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + Key: addrs.StringKey("c,c"), + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + // Commas inside quoted instance keys are not treated as + // delimiters even when there's quote-escaping hazards. + Input: `RSRCcomponent.foo["a\",a"],aws_instance.bar["c\",c"],cur`, + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey(`a",a`), + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + Key: addrs.StringKey(`c",c`), + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + Input: `RSRCstack.beep["a"].component.foo["b"],module.boop[1].aws_instance.bar[2],cur`, + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("beep", addrs.StringKey("a")), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("b"), + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance.Child("boop", addrs.IntKey(1)), + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + Key: addrs.IntKey(2), + }, + }, + }, + DeposedKey: states.NotDeposed, + }, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,facecafe", + Want: ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + }, + Item: addrs.AbsResourceInstance{ + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }, + }, + }, + }, + DeposedKey: states.DeposedKey("facecafe"), + }, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,beef", // deposed key is invalid because it's not long enough + WantErr: `resource instance object key has invalid deposed key "beef"`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,tootcafe", // deposed key is invalid because it isn't all hex digits + WantErr: `resource instance object key has invalid deposed key "tootcafe"`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,FACECAFE", // deposed key is invalid because it uses uppercase hex digits + WantErr: `resource instance object key has invalid deposed key "FACECAFE"`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,", // last field must either be "cur" or a deposed key + WantErr: `resource instance object key has invalid deposed key ""`, + }, + { + Input: "RSRCcomponent.foo,aws_instance.bar,cur,", + WantErr: `unsupported extra field in resource instance object key`, + }, + + // Component instance keys + { + Input: "CMPT", + WantErr: `component instance key has invalid component instance address ""`, + }, + { + Input: "CMPTcomponent.foo", + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + }, + }, + }, + WantUnrecognizedHandling: FailIfUnrecognized, + }, + { + Input: `CMPTcomponent.foo["baz"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("baz"), + }, + }, + }, + }, + { + Input: `CMPTstack.boop.component.foo["baz"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance.Child("boop", addrs.NoKey), + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey("baz"), + }, + }, + }, + }, + { + Input: `CMPTcomponent.foo["b,b"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey(`b,b`), + }, + }, + }, + }, + { + Input: `CMPTcomponent.foo["b\",b"]`, + Want: ComponentInstance{ + ComponentInstanceAddr: stackaddrs.AbsComponentInstance{ + Stack: stackaddrs.RootStackInstance, + Item: stackaddrs.ComponentInstance{ + Component: stackaddrs.Component{ + Name: "foo", + }, + Key: addrs.StringKey(`b",b`), + }, + }, + }, + }, + { + Input: "CMPTcomponent.foo,", + WantErr: `unsupported extra field in component instance key`, + }, + } + + cmpOpts := cmp.AllowUnexported(Unrecognized{}) + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + got, err := Parse(test.Input) + + if diff := cmp.Diff(test.Want, got, cmpOpts); diff != "" { + t.Errorf("wrong result for: %s\n%s", test.Input, diff) + } + + if test.WantErr == "" { + if err != nil { + t.Errorf("unexpected error: %s", err) + } + + // Any valid key should round-trip back to what we were given. + if got != nil { + gotAsStr := String(got) + if gotAsStr != test.Input { + t.Errorf("valid key of type %T did not round-trip\ngot: %s\nwant: %s", got, gotAsStr, test.Input) + } + if test.WantUnrecognizedHandling != UnrecognizedKeyHandling(0) { + if got, want := got.KeyType().UnrecognizedKeyHandling(), test.WantUnrecognizedHandling; got != want { + t.Errorf("unexpected UnrecognizedKeyHandling\ngot: %s\nwant: %s", got, want) + } + } + } else if err == nil { + t.Error("Parse returned nil Key and nil error") + } + } else { + if err == nil { + t.Errorf("unexpected success\nwant error: %s", test.WantErr) + } else { + if got, want := err.Error(), test.WantErr; got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } + } + } + }) + } +} diff --git a/internal/stacks/stackstate/statekeys/key_type.go b/internal/stacks/stackstate/statekeys/key_type.go new file mode 100644 index 0000000000..ebe64d926b --- /dev/null +++ b/internal/stacks/stackstate/statekeys/key_type.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" +) + +// A KeyType represents a particular type of state key, which is typically +// associated with a particular kind of object that can be represented in +// stack state. +// +// Each KeyType consists of four ASCII letters which are intended to be +// somewhat mnemonic (at least for the more commonly-appearing ones) +// but are not intended for end-user consumption, because state storage +// keys are to be considered opaque by anything other than Terraform Core. +// +// There are some additional semantics encoded in the case of some of the +// letters, to help keep the encoding relatively compact: +// - If the first letter is uppercase then that means the key type is +// "mandatory", while if it's lowercase then the key type is "ignorable". +// Terraform Core will raise an error during state decoding if it encounters +// a mandatory key type that it isn't familiar with, but it will silently +// allow unrecognized key types that are ignorable. +// - For key types that are ignorable, if the _last_ letter is lowercase +// then the key type is "discarded", while if it's uppercase then the +// key type is "preserved". When Terraform Core encounters an unrecognized +// key type that is both ignorable and "discarded" then it will proactively +// emit an event to delete that unrecognized object from the state. +// If the key type is "preserved" then Terraform Core will just ignore it +// and let the existing object with that key continue to exist in the +// state. +// +// These behaviors are intended as a lightweight way to achieve some +// forward-compatibility by allowing an older version of Terraform Core to, +// when it's safe to do so, silently discard or preserve objects that were +// presumably added by a later version of Terraform. When we add new key types +// in future we should consider which of the three unrecognized key handling +// methods is most appropriate, preferring one of the two "ignorable" modes +// if possible but using a "mandatory" key type if ignoring a particular +// object could cause an older version of Terraform Core to misinterpret +// the overall meaning of the prior state. +type KeyType string + +const ( + ResourceInstanceObjectType KeyType = "RSRC" + ComponentInstanceType KeyType = "CMPT" + OutputType KeyType = "OTPT" + VariableType KeyType = "VRBL" +) + +// UnrecognizedKeyHandling returns an indication of which of the three possible +// actions should be taken if the receiver is an unrecognized key type. +// +// It only really makes sense to use this method for a [KeyType] included in +// an [UnrecognizedKey] value. +func (kt KeyType) UnrecognizedKeyHandling() UnrecognizedKeyHandling { + first := kt[0] + last := kt[3] + switch { + case first >= 'A' && first <= 'Z': + return FailIfUnrecognized + case last >= 'A' && last <= 'Z': + return PreserveIfUnrecognized + default: + return DiscardIfUnrecognized + } +} + +func (kt KeyType) GoString() string { + return fmt.Sprintf("statekeys.KeyType(%q)", kt) +} + +func isPlausibleRawKeyType(s string) bool { + if len(s) != 4 { + return false + } + // All of the characters must be ASCII letters + for _, c := range s { + if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { + return false + } + } + return true +} + +// UnrecognizedKeyHandling models the three different ways an unrecognized +// key type can be handled when decoding prior state. +// +// See the documentation for [KeyType] for more information. +type UnrecognizedKeyHandling rune + +//go:generate go tool golang.org/x/tools/cmd/stringer -type UnrecognizedKeyHandling + +const ( + FailIfUnrecognized UnrecognizedKeyHandling = 'F' + PreserveIfUnrecognized UnrecognizedKeyHandling = 'P' + DiscardIfUnrecognized UnrecognizedKeyHandling = 'D' +) diff --git a/internal/stacks/stackstate/statekeys/outputs.go b/internal/stacks/stackstate/statekeys/outputs.go new file mode 100644 index 0000000000..0328ef6550 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/outputs.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +var ( + _ Key = Output{} +) + +type Output struct { + OutputAddr stackaddrs.OutputValue +} + +func parseOutput(s string) (Key, error) { + addrRaw, ok := finalKeyField(s) + if !ok { + return nil, fmt.Errorf("unsupported extra field in component instance key") + } + addr, diags := stackaddrs.ParseAbsOutputValueStr(addrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("output key has invalid output address %q", addrRaw) + } + if !addr.Stack.IsRoot() { + return nil, fmt.Errorf("output key was for non-root stack %q", addrRaw) + } + + return Output{ + OutputAddr: addr.Item, + }, nil +} + +func (o Output) KeyType() KeyType { + return OutputType +} + +func (o Output) rawSuffix() string { + return o.OutputAddr.String() +} diff --git a/internal/stacks/stackstate/statekeys/resources.go b/internal/stacks/stackstate/statekeys/resources.go new file mode 100644 index 0000000000..66bfbc3132 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/resources.go @@ -0,0 +1,70 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" + "github.com/hashicorp/terraform/internal/states" +) + +// ResourceInstanceObject represents state keys for resource instance objects. +type ResourceInstanceObject struct { + ResourceInstance stackaddrs.AbsResourceInstance + DeposedKey states.DeposedKey +} + +func parseResourceInstanceObject(s string) (Key, error) { + componentInstAddrRaw, s := cutKeyField(s) + resourceInstAddrRaw, s := cutKeyField(s) + deposedRaw, ok := finalKeyField(s) + if !ok { + return nil, fmt.Errorf("unsupported extra field in resource instance object key") + } + componentInstAddr, diags := stackaddrs.ParseAbsComponentInstanceStr(componentInstAddrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("resource instance object key has invalid component instance address %q", componentInstAddrRaw) + } + resourceInstAddr, diags := addrs.ParseAbsResourceInstanceStr(resourceInstAddrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("resource instance object key has invalid resource instance address %q", resourceInstAddrRaw) + } + var deposedKey states.DeposedKey + if deposedRaw != "cur" { + var err error + deposedKey, err = states.ParseDeposedKey(deposedRaw) + if err != nil { + return nil, fmt.Errorf("resource instance object key has invalid deposed key %q", deposedRaw) + } + } else { + deposedKey = states.NotDeposed + } + return ResourceInstanceObject{ + ResourceInstance: stackaddrs.AbsResourceInstance{ + Component: componentInstAddr, + Item: resourceInstAddr, + }, + DeposedKey: deposedKey, + }, nil +} + +func (k ResourceInstanceObject) KeyType() KeyType { + return ResourceInstanceObjectType +} + +func (k ResourceInstanceObject) rawSuffix() string { + var b rawKeyBuilder + b.AppendField(k.ResourceInstance.Component.String()) + b.AppendField(k.ResourceInstance.Item.String()) + if k.DeposedKey != states.NotDeposed { + // A valid deposed key is always eight hex digits, and never + // contains a comma so we can write it unquoted. + b.AppendField(string(k.DeposedKey)) + } else { + b.AppendField("cur") // short for "current" + } + return b.Raw() +} diff --git a/internal/stacks/stackstate/statekeys/unrecognizedkeyhandling_string.go b/internal/stacks/stackstate/statekeys/unrecognizedkeyhandling_string.go new file mode 100644 index 0000000000..59c7ec892b --- /dev/null +++ b/internal/stacks/stackstate/statekeys/unrecognizedkeyhandling_string.go @@ -0,0 +1,33 @@ +// Code generated by "stringer -type UnrecognizedKeyHandling"; DO NOT EDIT. + +package statekeys + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[FailIfUnrecognized-70] + _ = x[PreserveIfUnrecognized-80] + _ = x[DiscardIfUnrecognized-68] +} + +const ( + _UnrecognizedKeyHandling_name_0 = "DiscardIfUnrecognized" + _UnrecognizedKeyHandling_name_1 = "FailIfUnrecognized" + _UnrecognizedKeyHandling_name_2 = "PreserveIfUnrecognized" +) + +func (i UnrecognizedKeyHandling) String() string { + switch { + case i == 68: + return _UnrecognizedKeyHandling_name_0 + case i == 70: + return _UnrecognizedKeyHandling_name_1 + case i == 80: + return _UnrecognizedKeyHandling_name_2 + default: + return "UnrecognizedKeyHandling(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/internal/stacks/stackstate/statekeys/variables.go b/internal/stacks/stackstate/statekeys/variables.go new file mode 100644 index 0000000000..79d3319795 --- /dev/null +++ b/internal/stacks/stackstate/statekeys/variables.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statekeys + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/stacks/stackaddrs" +) + +var ( + _ Key = Variable{} +) + +type Variable struct { + VariableAddr stackaddrs.InputVariable +} + +func parseVariable(s string) (Key, error) { + addrRaw, ok := finalKeyField(s) + if !ok { + return nil, fmt.Errorf("unsupported extra field in component instance key") + } + addr, diags := stackaddrs.ParseAbsInputVariableStr(addrRaw) + if diags.HasErrors() { + return nil, fmt.Errorf("variable key has invalid output address %q", addrRaw) + } + if !addr.Stack.IsRoot() { + return nil, fmt.Errorf("variable key was for non-root stack %q", addrRaw) + } + + return Variable{ + VariableAddr: addr.Item, + }, nil +} + +func (v Variable) KeyType() KeyType { + return VariableType +} + +func (v Variable) rawSuffix() string { + return v.VariableAddr.String() +} diff --git a/internal/stacks/stackutils/proto_util.go b/internal/stacks/stackutils/proto_util.go new file mode 100644 index 0000000000..8683395e6c --- /dev/null +++ b/internal/stacks/stackutils/proto_util.go @@ -0,0 +1,22 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package stackutils + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" +) + +func ResourceModeForProto(mode addrs.ResourceMode) stacks.ResourceMode { + switch mode { + case addrs.ManagedResourceMode: + return stacks.ResourceMode_MANAGED + case addrs.DataResourceMode: + return stacks.ResourceMode_DATA + default: + // Should not get here, because the above should be exhaustive for + // all addrs.ResourceMode variants. + return stacks.ResourceMode_UNKNOWN + } +} diff --git a/internal/stacks/staticcheck.conf b/internal/stacks/staticcheck.conf new file mode 100644 index 0000000000..d56fe005bc --- /dev/null +++ b/internal/stacks/staticcheck.conf @@ -0,0 +1,4 @@ +# This package is currently under active development and contains stubs for +# various things that are not yet all filled in, so we'll permit unused code +# here until the changes have settled. +checks = ["inherit", "-U1000"] diff --git a/internal/stacks/tfstackdata1/convert.go b/internal/stacks/tfstackdata1/convert.go new file mode 100644 index 0000000000..efb9afeced --- /dev/null +++ b/internal/stacks/tfstackdata1/convert.go @@ -0,0 +1,204 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tfstackdata1 + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/msgpack" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/planfile" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" + "github.com/hashicorp/terraform/internal/states" +) + +func ResourceInstanceObjectStateToTFStackData1(objSrc *states.ResourceInstanceObjectSrc, providerConfigAddr addrs.AbsProviderConfig) *StateResourceInstanceObjectV1 { + if objSrc == nil { + // This is presumably representing the absense of any prior state, + // such as when an object is being planned for creation. + return nil + } + + // Hack: we'll borrow NewDynamicValue's treatment of the sensitive + // attribute paths here just so we don't need to reimplement the + // slice-of-paths conversion in yet another place. We don't + // actually do anything with the value part of this. + protoValue := stacks.NewDynamicValue(plans.DynamicValue(nil), objSrc.AttrSensitivePaths) + rawMsg := &StateResourceInstanceObjectV1{ + SchemaVersion: objSrc.SchemaVersion, + ValueJson: objSrc.AttrsJSON, + SensitivePaths: Terraform1ToPlanProtoAttributePaths(protoValue.Sensitive), + CreateBeforeDestroy: objSrc.CreateBeforeDestroy, + ProviderConfigAddr: providerConfigAddr.String(), + ProviderSpecificData: objSrc.Private, + } + switch objSrc.Status { + case states.ObjectReady: + rawMsg.Status = StateResourceInstanceObjectV1_READY + case states.ObjectTainted: + rawMsg.Status = StateResourceInstanceObjectV1_DAMAGED + default: + rawMsg.Status = StateResourceInstanceObjectV1_UNKNOWN + } + + rawMsg.Dependencies = make([]string, len(objSrc.Dependencies)) + for i, addr := range objSrc.Dependencies { + rawMsg.Dependencies[i] = addr.String() + } + + return rawMsg +} + +func Terraform1ToStackDataDynamicValue(value *stacks.DynamicValue) *DynamicValue { + return &DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: value.Msgpack, + }, + SensitivePaths: Terraform1ToPlanProtoAttributePaths(value.Sensitive), + } +} + +func DynamicValueFromTFStackData1(protoVal *DynamicValue, ty cty.Type) (cty.Value, error) { + raw := protoVal.Value.Msgpack + + unmarkedV, err := msgpack.Unmarshal(raw, ty) + if err != nil { + return cty.NilVal, err + } + + var markses []cty.PathValueMarks + if len(protoVal.SensitivePaths) != 0 { + markses = make([]cty.PathValueMarks, 0, len(protoVal.SensitivePaths)) + marks := cty.NewValueMarks(marks.Sensitive) + for _, protoPath := range protoVal.SensitivePaths { + path, err := planfile.PathFromProto(protoPath) + if err != nil { + return cty.NilVal, fmt.Errorf("invalid sensitive value path: %w", err) + } + markses = append(markses, cty.PathValueMarks{ + Path: path, + Marks: marks, + }) + } + } + return unmarkedV.MarkWithPaths(markses), nil +} + +func Terraform1ToPlanProtoAttributePaths(paths []*stacks.AttributePath) []*planproto.Path { + if len(paths) == 0 { + return nil + } + ret := make([]*planproto.Path, len(paths)) + for i, tf1Path := range paths { + ret[i] = Terraform1ToPlanProtoAttributePath(tf1Path) + } + return ret +} + +func Terraform1ToPlanProtoAttributePath(path *stacks.AttributePath) *planproto.Path { + if path == nil { + return nil + } + ret := &planproto.Path{} + if len(path.Steps) == 0 { + return ret + } + ret.Steps = make([]*planproto.Path_Step, len(path.Steps)) + for i, tf1Step := range path.Steps { + ret.Steps[i] = Terraform1ToPlanProtoAttributePathStep(tf1Step) + } + return ret +} + +func Terraform1ToPlanProtoAttributePathStep(step *stacks.AttributePath_Step) *planproto.Path_Step { + if step == nil { + return nil + } + ret := &planproto.Path_Step{} + switch sel := step.Selector.(type) { + case *stacks.AttributePath_Step_AttributeName: + ret.Selector = &planproto.Path_Step_AttributeName{ + AttributeName: sel.AttributeName, + } + case *stacks.AttributePath_Step_ElementKeyInt: + encInt, err := msgpack.Marshal(cty.NumberIntVal(sel.ElementKeyInt), cty.Number) + if err != nil { + // This should not be possible because all integers have a cty msgpack encoding + panic(fmt.Sprintf("unencodable element index: %s", err)) + } + ret.Selector = &planproto.Path_Step_ElementKey{ + ElementKey: &planproto.DynamicValue{ + Msgpack: encInt, + }, + } + case *stacks.AttributePath_Step_ElementKeyString: + encStr, err := msgpack.Marshal(cty.StringVal(sel.ElementKeyString), cty.String) + if err != nil { + // This should not be possible because all strings have a cty msgpack encoding + panic(fmt.Sprintf("unencodable element key: %s", err)) + } + ret.Selector = &planproto.Path_Step_ElementKey{ + ElementKey: &planproto.DynamicValue{ + Msgpack: encStr, + }, + } + default: + // Should not get here, because the above cases should be exhaustive + // for all possible *terraform1.AttributePath_Step selector types. + panic(fmt.Sprintf("unsupported path step selector type %T", sel)) + } + return ret +} + +func DecodeProtoResourceInstanceObject(protoObj *StateResourceInstanceObjectV1) (*states.ResourceInstanceObjectSrc, error) { + objSrc := &states.ResourceInstanceObjectSrc{ + SchemaVersion: protoObj.SchemaVersion, + AttrsJSON: protoObj.ValueJson, + CreateBeforeDestroy: protoObj.CreateBeforeDestroy, + Private: protoObj.ProviderSpecificData, + } + + switch protoObj.Status { + case StateResourceInstanceObjectV1_READY: + objSrc.Status = states.ObjectReady + case StateResourceInstanceObjectV1_DAMAGED: + objSrc.Status = states.ObjectTainted + default: + return nil, fmt.Errorf("unsupported status %s", protoObj.Status.String()) + } + + paths := make([]cty.Path, 0, len(protoObj.SensitivePaths)) + for _, p := range protoObj.SensitivePaths { + path, err := planfile.PathFromProto(p) + if err != nil { + return nil, err + } + paths = append(paths, path) + } + objSrc.AttrSensitivePaths = paths + + if len(protoObj.Dependencies) != 0 { + objSrc.Dependencies = make([]addrs.ConfigResource, len(protoObj.Dependencies)) + for i, raw := range protoObj.Dependencies { + instAddr, diags := addrs.ParseAbsResourceInstanceStr(raw) + if diags.HasErrors() { + return nil, fmt.Errorf("invalid dependency %q", raw) + } + // We used the resource instance address parser here but we + // actually want the "config resource" subset of that syntax only. + configAddr := instAddr.ConfigResource() + if configAddr.String() != instAddr.String() { + return nil, fmt.Errorf("invalid dependency %q", raw) + } + objSrc.Dependencies[i] = configAddr + } + } + + return objSrc, nil +} diff --git a/internal/stacks/tfstackdata1/convert_test.go b/internal/stacks/tfstackdata1/convert_test.go new file mode 100644 index 0000000000..fa036bc1ad --- /dev/null +++ b/internal/stacks/tfstackdata1/convert_test.go @@ -0,0 +1,150 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tfstackdata1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/msgpack" + "google.golang.org/protobuf/testing/protocmp" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans/planproto" + "github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks" +) + +func TestDynamicValueToTFStackData1(t *testing.T) { + startVal := cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a").Mark(marks.Sensitive), + "b": cty.StringVal("b"), + "c": cty.ListVal([]cty.Value{ + cty.StringVal("c[0]"), + cty.StringVal("c[1]").Mark(marks.Sensitive), + }), + }) + ty := startVal.Type() + + partial, err := stacks.ToDynamicValue(startVal, ty) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + got := Terraform1ToStackDataDynamicValue(partial) + want := &DynamicValue{ + Value: &planproto.DynamicValue{ + // The following is cty's canonical MessagePack encoding of + // the unmarked version of startVal: + // - \x83 marks the start of a three-element "fixmap" + // - \xa1 and \xa4 mark a one-element and a four-element fixstr respectively + // - \x92 marks the start of a two-element "fixarray" + // cty/msgpack always orders object attribute names lexically when + // serializing, so we can safely rely on the order of the attrs. + Msgpack: []byte("\x83\xa1a\xa1a\xa1b\xa1b\xa1c\x92\xa4c[0]\xa4c[1]"), + }, + + SensitivePaths: []*planproto.Path{ + { + Steps: []*planproto.Path_Step{ + { + Selector: &planproto.Path_Step_AttributeName{ + AttributeName: "a", + }, + }, + }, + }, + { + Steps: []*planproto.Path_Step{ + { + Selector: &planproto.Path_Step_AttributeName{ + AttributeName: "c", + }, + }, + { + Selector: &planproto.Path_Step_ElementKey{ + ElementKey: &planproto.DynamicValue{ + Msgpack: []byte{0b00000001}, // MessagePack-encoded fixint 1 + }, + }, + }, + }, + }, + }, + } + + // DynamicValueToTFStackData1 doesn't guarantee the order of the + // entries in SensitivePaths, so we'll normalize what we got. + // We distinguish the two expected paths by their number of steps. + if len(got.SensitivePaths) == 2 && len(got.SensitivePaths[0].Steps) == 2 { + got.SensitivePaths[0], got.SensitivePaths[1] = got.SensitivePaths[1], got.SensitivePaths[0] + } + + if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" { + t.Errorf("wrong result\n%s", diff) + } +} + +func TestDynamicValueFromTFStackData1(t *testing.T) { + startVal := cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a").Mark(marks.Sensitive), + "b": cty.StringVal("b"), + "c": cty.ListVal([]cty.Value{ + cty.StringVal("c[0]"), + cty.StringVal("c[1]").Mark(marks.Sensitive), + }), + }) + ty := startVal.Type() + + // We'll use the MessagePack encoder directly to get the raw bytes + // representing the above, just for maintainability's sake since it's + // challenging to read and modify raw MessagePack values. + unmarkedVal, _ := startVal.UnmarkDeep() + raw, err := msgpack.Marshal(unmarkedVal, ty) + if err != nil { + t.Fatal(err) + } + + input := &DynamicValue{ + Value: &planproto.DynamicValue{ + Msgpack: raw, + }, + SensitivePaths: []*planproto.Path{ + { + Steps: []*planproto.Path_Step{ + { + Selector: &planproto.Path_Step_AttributeName{ + AttributeName: "a", + }, + }, + }, + }, + { + Steps: []*planproto.Path_Step{ + { + Selector: &planproto.Path_Step_AttributeName{ + AttributeName: "c", + }, + }, + { + Selector: &planproto.Path_Step_ElementKey{ + ElementKey: &planproto.DynamicValue{ + Msgpack: []byte{0b00000001}, // MessagePack-encoded fixint 1 + }, + }, + }, + }, + }, + }, + } + + got, err := DynamicValueFromTFStackData1(input, ty) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + want := startVal + + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } +} diff --git a/internal/stacks/tfstackdata1/tfstackdata1.pb.go b/internal/stacks/tfstackdata1/tfstackdata1.pb.go new file mode 100644 index 0000000000..53fac0c21a --- /dev/null +++ b/internal/stacks/tfstackdata1/tfstackdata1.pb.go @@ -0,0 +1,1554 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.5 +// protoc v3.15.6 +// source: tfstackdata1.proto + +package tfstackdata1 + +import ( + planproto "github.com/hashicorp/terraform/internal/plans/planproto" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type StateResourceInstanceObjectV1_Status int32 + +const ( + StateResourceInstanceObjectV1_UNKNOWN StateResourceInstanceObjectV1_Status = 0 + StateResourceInstanceObjectV1_READY StateResourceInstanceObjectV1_Status = 1 + StateResourceInstanceObjectV1_DAMAGED StateResourceInstanceObjectV1_Status = 2 // (formerly known as "tainted") +) + +// Enum value maps for StateResourceInstanceObjectV1_Status. +var ( + StateResourceInstanceObjectV1_Status_name = map[int32]string{ + 0: "UNKNOWN", + 1: "READY", + 2: "DAMAGED", + } + StateResourceInstanceObjectV1_Status_value = map[string]int32{ + "UNKNOWN": 0, + "READY": 1, + "DAMAGED": 2, + } +) + +func (x StateResourceInstanceObjectV1_Status) Enum() *StateResourceInstanceObjectV1_Status { + p := new(StateResourceInstanceObjectV1_Status) + *p = x + return p +} + +func (x StateResourceInstanceObjectV1_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (StateResourceInstanceObjectV1_Status) Descriptor() protoreflect.EnumDescriptor { + return file_tfstackdata1_proto_enumTypes[0].Descriptor() +} + +func (StateResourceInstanceObjectV1_Status) Type() protoreflect.EnumType { + return &file_tfstackdata1_proto_enumTypes[0] +} + +func (x StateResourceInstanceObjectV1_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use StateResourceInstanceObjectV1_Status.Descriptor instead. +func (StateResourceInstanceObjectV1_Status) EnumDescriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{14, 0} +} + +// Appears early in a raw plan sequence to capture some metadata that we need +// to process subsequent messages, or to abort if we're being asked to decode +// a plan created by a different version of Terraform. +type PlanHeader struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The canonical version string for the version of Terraform that created + // the plan sequence that this message belongs to. + // + // The raw plan sequence loader will fail if it finds a message of this + // type with a version string that disagrees with the version of Terraform + // decoding the message, because we always expect plans to be applied by + // the same version of Terraform that created them. + TerraformVersion string `protobuf:"bytes,1,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanHeader) Reset() { + *x = PlanHeader{} + mi := &file_tfstackdata1_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanHeader) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanHeader) ProtoMessage() {} + +func (x *PlanHeader) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanHeader.ProtoReflect.Descriptor instead. +func (*PlanHeader) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{0} +} + +func (x *PlanHeader) GetTerraformVersion() string { + if x != nil { + return x.TerraformVersion + } + return "" +} + +// Captures one element from the raw prior state that was provided when +// creating the plan. A valid plan includes a copy of its entire prior state +// represented as zero or more messages of this type, which we then interpret +// as a map from key to raw during load. +type PlanPriorStateElem struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Raw *anypb.Any `protobuf:"bytes,2,opt,name=raw,proto3" json:"raw,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanPriorStateElem) Reset() { + *x = PlanPriorStateElem{} + mi := &file_tfstackdata1_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanPriorStateElem) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanPriorStateElem) ProtoMessage() {} + +func (x *PlanPriorStateElem) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanPriorStateElem.ProtoReflect.Descriptor instead. +func (*PlanPriorStateElem) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{1} +} + +func (x *PlanPriorStateElem) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *PlanPriorStateElem) GetRaw() *anypb.Any { + if x != nil { + return x.Raw + } + return nil +} + +// Confirms whether the overall plan whose raw plan sequence includes this +// message is complete enough and valid enough to be applied. +// +// If a the sequence of raw plan messages includes multiple messages of this +// type then the one with the latest position in the list "wins" during +// decoding of the overall sequence, although in practice there isn't yet +// any clear reason to include more than one instance of this message type in a +// plan. +type PlanApplyable struct { + state protoimpl.MessageState `protogen:"open.v1"` + Applyable bool `protobuf:"varint,1,opt,name=applyable,proto3" json:"applyable,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanApplyable) Reset() { + *x = PlanApplyable{} + mi := &file_tfstackdata1_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanApplyable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanApplyable) ProtoMessage() {} + +func (x *PlanApplyable) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanApplyable.ProtoReflect.Descriptor instead. +func (*PlanApplyable) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{2} +} + +func (x *PlanApplyable) GetApplyable() bool { + if x != nil { + return x.Applyable + } + return false +} + +// Records the plan timestamp to be used for all components and the stacks language. +type PlanTimestamp struct { + state protoimpl.MessageState `protogen:"open.v1"` + PlanTimestamp string `protobuf:"bytes,1,opt,name=plan_timestamp,json=planTimestamp,proto3" json:"plan_timestamp,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanTimestamp) Reset() { + *x = PlanTimestamp{} + mi := &file_tfstackdata1_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanTimestamp) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanTimestamp) ProtoMessage() {} + +func (x *PlanTimestamp) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanTimestamp.ProtoReflect.Descriptor instead. +func (*PlanTimestamp) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{3} +} + +func (x *PlanTimestamp) GetPlanTimestamp() string { + if x != nil { + return x.PlanTimestamp + } + return "" +} + +// Records the value of one of the main stack's input values during planning. +// +// These values get fixed during the plan phase so that we can ensure that we +// use identical values when subsequently applying the plan. +type PlanRootInputValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value *DynamicValue `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + RequiredOnApply bool `protobuf:"varint,3,opt,name=required_on_apply,json=requiredOnApply,proto3" json:"required_on_apply,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanRootInputValue) Reset() { + *x = PlanRootInputValue{} + mi := &file_tfstackdata1_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanRootInputValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanRootInputValue) ProtoMessage() {} + +func (x *PlanRootInputValue) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanRootInputValue.ProtoReflect.Descriptor instead. +func (*PlanRootInputValue) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{4} +} + +func (x *PlanRootInputValue) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *PlanRootInputValue) GetValue() *DynamicValue { + if x != nil { + return x.Value + } + return nil +} + +func (x *PlanRootInputValue) GetRequiredOnApply() bool { + if x != nil { + return x.RequiredOnApply + } + return false +} + +// Records that a root input variable should be deleted by the apply operation. +type DeletedRootInputVariable struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletedRootInputVariable) Reset() { + *x = DeletedRootInputVariable{} + mi := &file_tfstackdata1_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletedRootInputVariable) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletedRootInputVariable) ProtoMessage() {} + +func (x *DeletedRootInputVariable) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletedRootInputVariable.ProtoReflect.Descriptor instead. +func (*DeletedRootInputVariable) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{5} +} + +func (x *DeletedRootInputVariable) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Records that a root output should be deleted by the apply operation. +type DeletedRootOutputValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletedRootOutputValue) Reset() { + *x = DeletedRootOutputValue{} + mi := &file_tfstackdata1_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletedRootOutputValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletedRootOutputValue) ProtoMessage() {} + +func (x *DeletedRootOutputValue) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletedRootOutputValue.ProtoReflect.Descriptor instead. +func (*DeletedRootOutputValue) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{6} +} + +func (x *DeletedRootOutputValue) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Records that a component should just be deleted from the state. +type DeletedComponent struct { + state protoimpl.MessageState `protogen:"open.v1"` + ComponentInstanceAddr string `protobuf:"bytes,1,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeletedComponent) Reset() { + *x = DeletedComponent{} + mi := &file_tfstackdata1_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeletedComponent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeletedComponent) ProtoMessage() {} + +func (x *DeletedComponent) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeletedComponent.ProtoReflect.Descriptor instead. +func (*DeletedComponent) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{7} +} + +func (x *DeletedComponent) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +// FunctionResults stores a record of the results of provider functions +// that were called during the planning phase. This is used to ensure that the +// same results are returned during the apply phase. +type FunctionResults struct { + state protoimpl.MessageState `protogen:"open.v1"` + FunctionResults []*planproto.FunctionCallHash `protobuf:"bytes,1,rep,name=function_results,json=functionResults,proto3" json:"function_results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FunctionResults) Reset() { + *x = FunctionResults{} + mi := &file_tfstackdata1_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FunctionResults) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionResults) ProtoMessage() {} + +func (x *FunctionResults) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FunctionResults.ProtoReflect.Descriptor instead. +func (*FunctionResults) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{8} +} + +func (x *FunctionResults) GetFunctionResults() []*planproto.FunctionCallHash { + if x != nil { + return x.FunctionResults + } + return nil +} + +// Represents the existence of a particular component instance, and so must +// always appear before any messages representing objects that belong to that +// component instance. +// +// This message type exists to avoid the ambiguity between a component instance +// existing with zero resource instances inside vs. a component instance +// not existing at all. +type PlanComponentInstance struct { + state protoimpl.MessageState `protogen:"open.v1"` + ComponentInstanceAddr string `protobuf:"bytes,1,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + // plan_timestamp records the time when the plan for this component + // instance was created, exclusively for making sure that the + // "plantimestamp" function can return the same value during the apply + // phase. It must not be used for any other purpose. + PlanTimestamp string `protobuf:"bytes,2,opt,name=plan_timestamp,json=planTimestamp,proto3" json:"plan_timestamp,omitempty"` + // Captures an approximation of the input values for this component with + // as much detail as we knew during the planning phase. This might + // contain unknown values as placeholders for values that won't be + // determined until the apply phase, so this isn't usable directly as + // the input to subsequently applying the component plan but the final + // input values should be a valid concretization of what's described here. + PlannedInputValues map[string]*DynamicValue `protobuf:"bytes,3,rep,name=planned_input_values,json=plannedInputValues,proto3" json:"planned_input_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // The action planned for the component as a whole. + // + // This does not directly incorporate actions planned for resource + // instances within this component instance, but does capture a sense + // of the overall action being taken for this particular component + // instance. + // + // The currently-possible values are: + // - CREATE and UPDATE both describe applying a "normal" plan, where + // CREATE additionally represents that the component instance + // did not previously exist. + // - READ describes a refresh-only plan. This is currently possible only + // if the overall stack plan is refresh-only. + // - DELETE describes applying a destroy plan, with the intent of + // deleting all remote objects currently bound to resource instances + // in this component instance. + // + // The value recorded here is used to achieve a few variations needed in + // the apply phase. + PlannedAction planproto.Action `protobuf:"varint,4,opt,name=planned_action,json=plannedAction,proto3,enum=tfplan.Action" json:"planned_action,omitempty"` + // The mode that was used to plan this component. + // + // This is used to determine the behavior of the apply phase for this + // component instance. + // + // Ideally, we wouldn't need to include this at all as the plan should + // contain everything we need without a general mode. However, this is + // not currently the case. See context_apply.go:332 for more details. + // TODO: Remove this once walkDestroy has been properly audited. + Mode planproto.Mode `protobuf:"varint,10,opt,name=mode,proto3,enum=tfplan.Mode" json:"mode,omitempty"` + // The appliability flag decided by the modules runtime for this component's + // plan. See the docs for plans.Plan.Applyable for details on what this + // represents. (It's here largely just so that we can repopulate it + // faithfully when we rebuild a plans.Plan object at apply time.) + PlanApplyable bool `protobuf:"varint,7,opt,name=plan_applyable,json=planApplyable,proto3" json:"plan_applyable,omitempty"` + // The completion flag decided by the modules runtime for this component's + // plan. See the docs for plans.Plan.Complete for details on what this + // represents. (It's here largely just so that we can repopulate it + // faithfully when we rebuild a plans.Plan object at apply time.) + PlanComplete bool `protobuf:"varint,8,opt,name=plan_complete,json=planComplete,proto3" json:"plan_complete,omitempty"` + // A list of absolute component addresses that this component + // instance depends on according to the configuration the plan was + // created from. (These are components rather than component instances + // because the stacks language evaluation model uses components as the + // most specific granularity for dependency resolution.) + // + // Applying this component instance's plan must wait until any + // CREATE or UPDATE plans for any of the listed component instances have + // completed successfully. Additionally, if any of the component instances + // listed here have DELETE plans then this component instance must also + // have a DELETE plan and the upstream DELETE must wait until this one + // has completed. + // + // A component instance plan that is not DELETE cannot depend on another + // component instance that is not also DELETE, since that would imply that + // this component instance's configuration refers to a component that isn't + // declared, which should therefore have failed validation. + DependsOnComponentAddrs []string `protobuf:"bytes,5,rep,name=depends_on_component_addrs,json=dependsOnComponentAddrs,proto3" json:"depends_on_component_addrs,omitempty"` + // Captures an approximation of the output values for this component with + // as much detail as we knew during the planning phase. + // + // For any planned action other than DELETE this might contain unknown + // values as placeholders for values that won't be determined until the + // apply phase + // + // For a DELETE plan the values should always be known because they are + // based on the prior state for the component, before it has been destroyed. + // The apply phase should use these values to build the representation of + // the component instance as an expression, because for DELETE any + // dependent objects must also be pending DELETE and their delete must + // happen before this instance is destroyed. + PlannedOutputValues map[string]*DynamicValue `protobuf:"bytes,6,rep,name=planned_output_values,json=plannedOutputValues,proto3" json:"planned_output_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // A list of check results for this component instance, as produced by + // the modules runtime during the planning phase. The apply expects to + // update check results which were unknown during planning to reflect + // the actual results from the apply phase. + PlannedCheckResults []*planproto.CheckResults `protobuf:"bytes,9,rep,name=planned_check_results,json=plannedCheckResults,proto3" json:"planned_check_results,omitempty"` + // The set of provider function results that were produced during the + // planning phase for this component instance. These results are used + // to ensure that the same results are returned during the apply phase. + FunctionResults []*planproto.FunctionCallHash `protobuf:"bytes,11,rep,name=function_results,json=functionResults,proto3" json:"function_results,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanComponentInstance) Reset() { + *x = PlanComponentInstance{} + mi := &file_tfstackdata1_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanComponentInstance) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanComponentInstance) ProtoMessage() {} + +func (x *PlanComponentInstance) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanComponentInstance.ProtoReflect.Descriptor instead. +func (*PlanComponentInstance) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{9} +} + +func (x *PlanComponentInstance) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +func (x *PlanComponentInstance) GetPlanTimestamp() string { + if x != nil { + return x.PlanTimestamp + } + return "" +} + +func (x *PlanComponentInstance) GetPlannedInputValues() map[string]*DynamicValue { + if x != nil { + return x.PlannedInputValues + } + return nil +} + +func (x *PlanComponentInstance) GetPlannedAction() planproto.Action { + if x != nil { + return x.PlannedAction + } + return planproto.Action(0) +} + +func (x *PlanComponentInstance) GetMode() planproto.Mode { + if x != nil { + return x.Mode + } + return planproto.Mode(0) +} + +func (x *PlanComponentInstance) GetPlanApplyable() bool { + if x != nil { + return x.PlanApplyable + } + return false +} + +func (x *PlanComponentInstance) GetPlanComplete() bool { + if x != nil { + return x.PlanComplete + } + return false +} + +func (x *PlanComponentInstance) GetDependsOnComponentAddrs() []string { + if x != nil { + return x.DependsOnComponentAddrs + } + return nil +} + +func (x *PlanComponentInstance) GetPlannedOutputValues() map[string]*DynamicValue { + if x != nil { + return x.PlannedOutputValues + } + return nil +} + +func (x *PlanComponentInstance) GetPlannedCheckResults() []*planproto.CheckResults { + if x != nil { + return x.PlannedCheckResults + } + return nil +} + +func (x *PlanComponentInstance) GetFunctionResults() []*planproto.FunctionCallHash { + if x != nil { + return x.FunctionResults + } + return nil +} + +// Represents a planned change to a particular resource instance within a +// particular component instance. +type PlanResourceInstanceChangePlanned struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The same string must previously have been announced with a + // PlanComponentInstance message, or the overall plan sequence is invalid. + ComponentInstanceAddr string `protobuf:"bytes,1,opt,name=component_instance_addr,json=componentInstanceAddr,proto3" json:"component_instance_addr,omitempty"` + ResourceInstanceAddr string `protobuf:"bytes,4,opt,name=resource_instance_addr,json=resourceInstanceAddr,proto3" json:"resource_instance_addr,omitempty"` + DeposedKey string `protobuf:"bytes,5,opt,name=deposed_key,json=deposedKey,proto3" json:"deposed_key,omitempty"` + // The address of the provider configuration that planned this change, + // or that produced the prior state for messages where "change" is + // unpopulated. This is a module-centric view relative to the root module + // of the component identified in component_instance_addr. + ProviderConfigAddr string `protobuf:"bytes,6,opt,name=provider_config_addr,json=providerConfigAddr,proto3" json:"provider_config_addr,omitempty"` + // Description of the planned change in the standard "tfplan" (planproto) + // format. + Change *planproto.ResourceInstanceChange `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` + // A snapshot of the "prior state", which is the result of upgrading and + // refreshing the previous run's state. + // + // The very first action on applying this plan should be to update the + // raw state for the resource instance to match this value, since + // the main apply phase for each component instance assumes that the + // prior state has already been updated to match the "old" value from + // the "change" message. + PriorState *StateResourceInstanceObjectV1 `protobuf:"bytes,3,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanResourceInstanceChangePlanned) Reset() { + *x = PlanResourceInstanceChangePlanned{} + mi := &file_tfstackdata1_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanResourceInstanceChangePlanned) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanResourceInstanceChangePlanned) ProtoMessage() {} + +func (x *PlanResourceInstanceChangePlanned) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanResourceInstanceChangePlanned.ProtoReflect.Descriptor instead. +func (*PlanResourceInstanceChangePlanned) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{10} +} + +func (x *PlanResourceInstanceChangePlanned) GetComponentInstanceAddr() string { + if x != nil { + return x.ComponentInstanceAddr + } + return "" +} + +func (x *PlanResourceInstanceChangePlanned) GetResourceInstanceAddr() string { + if x != nil { + return x.ResourceInstanceAddr + } + return "" +} + +func (x *PlanResourceInstanceChangePlanned) GetDeposedKey() string { + if x != nil { + return x.DeposedKey + } + return "" +} + +func (x *PlanResourceInstanceChangePlanned) GetProviderConfigAddr() string { + if x != nil { + return x.ProviderConfigAddr + } + return "" +} + +func (x *PlanResourceInstanceChangePlanned) GetChange() *planproto.ResourceInstanceChange { + if x != nil { + return x.Change + } + return nil +} + +func (x *PlanResourceInstanceChangePlanned) GetPriorState() *StateResourceInstanceObjectV1 { + if x != nil { + return x.PriorState + } + return nil +} + +// Represents a deferred change to a particular resource instance within a +// particular component instance. +type PlanDeferredResourceInstanceChange struct { + state protoimpl.MessageState `protogen:"open.v1"` + Deferred *planproto.Deferred `protobuf:"bytes,1,opt,name=deferred,proto3" json:"deferred,omitempty"` + Change *PlanResourceInstanceChangePlanned `protobuf:"bytes,2,opt,name=change,proto3" json:"change,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanDeferredResourceInstanceChange) Reset() { + *x = PlanDeferredResourceInstanceChange{} + mi := &file_tfstackdata1_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanDeferredResourceInstanceChange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanDeferredResourceInstanceChange) ProtoMessage() {} + +func (x *PlanDeferredResourceInstanceChange) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanDeferredResourceInstanceChange.ProtoReflect.Descriptor instead. +func (*PlanDeferredResourceInstanceChange) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{11} +} + +func (x *PlanDeferredResourceInstanceChange) GetDeferred() *planproto.Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +func (x *PlanDeferredResourceInstanceChange) GetChange() *PlanResourceInstanceChangePlanned { + if x != nil { + return x.Change + } + return nil +} + +// Represents that we need to emit "delete" requests for one or more raw +// state and/or state description objects during the apply phase. +// +// This situation arises if the previous state (given as input to the apply +// phase) contains keys that are of a type unrecognized by the current +// version of Terraform and that are marked as "discard if unrecognized", +// suggesting that their content is likely to become somehow invalid if +// other parts of the state were to get updated. +type PlanDiscardStateMapKeys struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A set of keys to delete from the "raw state". + RawStateKeys []string `protobuf:"bytes,1,rep,name=raw_state_keys,json=rawStateKeys,proto3" json:"raw_state_keys,omitempty"` + // A set of keys to delete from the "state description". + DescriptionKeys []string `protobuf:"bytes,2,rep,name=description_keys,json=descriptionKeys,proto3" json:"description_keys,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PlanDiscardStateMapKeys) Reset() { + *x = PlanDiscardStateMapKeys{} + mi := &file_tfstackdata1_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PlanDiscardStateMapKeys) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PlanDiscardStateMapKeys) ProtoMessage() {} + +func (x *PlanDiscardStateMapKeys) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PlanDiscardStateMapKeys.ProtoReflect.Descriptor instead. +func (*PlanDiscardStateMapKeys) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{12} +} + +func (x *PlanDiscardStateMapKeys) GetRawStateKeys() []string { + if x != nil { + return x.RawStateKeys + } + return nil +} + +func (x *PlanDiscardStateMapKeys) GetDescriptionKeys() []string { + if x != nil { + return x.DescriptionKeys + } + return nil +} + +// Represents the existence of a particular component instance. +// +// This is here mainly to remove the ambiguity between a component instance that +// exists but contains no resource instances vs. a component instance that +// doesn't exist at all. +// +// Because the state map is updated on a per-element basis rather than +// atomically, it's possible that the state map might contain resource instances +// which belong to a component instance that is not tracked by a message of +// this type. In that case, the state loader will just assume an implied +// message of this type with a matching component instance address and with +// all other fields unset. +type StateComponentInstanceV1 struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The component instance's output values as reported from the most recent + // apply action. We retain this only so that we have some values to use + // in cases where the values in the configuration are unavailable or + // insufficient, such as when we're making a destroy-mode plan and therefore + // the desired state would be for the component instance to cease existing + // but yet we still need to have _some_ output values to use when planning + // and applying other component instances that refer to this one. + OutputValues map[string]*DynamicValue `protobuf:"bytes,1,rep,name=output_values,json=outputValues,proto3" json:"output_values,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // The input variables for this component instance as reported from the + // most recent apply action. We retain this only for usage within removed + // blocks, where we need to know the input variables to be able to plan + // and apply the destroy action without asking the user to resupply or + // remember them. + InputVariables map[string]*DynamicValue `protobuf:"bytes,2,rep,name=input_variables,json=inputVariables,proto3" json:"input_variables,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // The absolute configuration addresses of components that this component + // instance depended on when it was created. We preserve this information + // to help with plan and apply ordering during destroy plans or for removed + // blocks. + DependencyAddrs []string `protobuf:"bytes,3,rep,name=dependency_addrs,json=dependencyAddrs,proto3" json:"dependency_addrs,omitempty"` + // The absolute configuration addresses of components that depended on this + // component instance when it was created. We preserve this information + // to help with plan and apply ordering during destroy plans or for removed + // blocks. + DependentAddrs []string `protobuf:"bytes,4,rep,name=dependent_addrs,json=dependentAddrs,proto3" json:"dependent_addrs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StateComponentInstanceV1) Reset() { + *x = StateComponentInstanceV1{} + mi := &file_tfstackdata1_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StateComponentInstanceV1) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StateComponentInstanceV1) ProtoMessage() {} + +func (x *StateComponentInstanceV1) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StateComponentInstanceV1.ProtoReflect.Descriptor instead. +func (*StateComponentInstanceV1) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{13} +} + +func (x *StateComponentInstanceV1) GetOutputValues() map[string]*DynamicValue { + if x != nil { + return x.OutputValues + } + return nil +} + +func (x *StateComponentInstanceV1) GetInputVariables() map[string]*DynamicValue { + if x != nil { + return x.InputVariables + } + return nil +} + +func (x *StateComponentInstanceV1) GetDependencyAddrs() []string { + if x != nil { + return x.DependencyAddrs + } + return nil +} + +func (x *StateComponentInstanceV1) GetDependentAddrs() []string { + if x != nil { + return x.DependentAddrs + } + return nil +} + +// Represents the existence of a particular resource instance object in a +// particular component instance. +// +// A resource instance message object should typically be accompanied by a +// StateComponentInstanceV1 (or later version) that represents the existence +// of the component itself, but for robustness we tolerate the absense of +// such a message and just assume that all of its fields (other than the +// component instance address) are unset. +type StateResourceInstanceObjectV1 struct { + state protoimpl.MessageState `protogen:"open.v1"` + // value_json is a JSON representation of the object value representing + // this resource instance object. + // + // This is JSON-serialized rather than MessagePack serialized (as we do + // for everything else in this format and in the RPC API) because + // the provider protocol only supports legacy flatmap and JSON as input + // to the state upgrade process, and we won't be able to transcode from + // MessagePack to JSON once we decode this because we won't know the + // schema that the value was encoded with. + // + // This is a pragmatic exception for this particular quirk of Terraform's + // provider API design. Other parts of this format and associated protocol + // should use tfplan.DynamicValue and MessagePack encoding for consistency. + ValueJson []byte `protobuf:"bytes,1,opt,name=value_json,json=valueJson,proto3" json:"value_json,omitempty"` + SensitivePaths []*planproto.Path `protobuf:"bytes,2,rep,name=sensitive_paths,json=sensitivePaths,proto3" json:"sensitive_paths,omitempty"` + SchemaVersion uint64 `protobuf:"varint,3,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + Status StateResourceInstanceObjectV1_Status `protobuf:"varint,4,opt,name=status,proto3,enum=tfstackdata1.StateResourceInstanceObjectV1_Status" json:"status,omitempty"` + Dependencies []string `protobuf:"bytes,5,rep,name=dependencies,proto3" json:"dependencies,omitempty"` + CreateBeforeDestroy bool `protobuf:"varint,6,opt,name=create_before_destroy,json=createBeforeDestroy,proto3" json:"create_before_destroy,omitempty"` + ProviderConfigAddr string `protobuf:"bytes,7,opt,name=provider_config_addr,json=providerConfigAddr,proto3" json:"provider_config_addr,omitempty"` + // provider_specific_data is arbitrary bytes produced by the provider + // in its apply response which we preserve and pass back to it in any + // subsequent plan operation. + ProviderSpecificData []byte `protobuf:"bytes,8,opt,name=provider_specific_data,json=providerSpecificData,proto3" json:"provider_specific_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StateResourceInstanceObjectV1) Reset() { + *x = StateResourceInstanceObjectV1{} + mi := &file_tfstackdata1_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StateResourceInstanceObjectV1) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StateResourceInstanceObjectV1) ProtoMessage() {} + +func (x *StateResourceInstanceObjectV1) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StateResourceInstanceObjectV1.ProtoReflect.Descriptor instead. +func (*StateResourceInstanceObjectV1) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{14} +} + +func (x *StateResourceInstanceObjectV1) GetValueJson() []byte { + if x != nil { + return x.ValueJson + } + return nil +} + +func (x *StateResourceInstanceObjectV1) GetSensitivePaths() []*planproto.Path { + if x != nil { + return x.SensitivePaths + } + return nil +} + +func (x *StateResourceInstanceObjectV1) GetSchemaVersion() uint64 { + if x != nil { + return x.SchemaVersion + } + return 0 +} + +func (x *StateResourceInstanceObjectV1) GetStatus() StateResourceInstanceObjectV1_Status { + if x != nil { + return x.Status + } + return StateResourceInstanceObjectV1_UNKNOWN +} + +func (x *StateResourceInstanceObjectV1) GetDependencies() []string { + if x != nil { + return x.Dependencies + } + return nil +} + +func (x *StateResourceInstanceObjectV1) GetCreateBeforeDestroy() bool { + if x != nil { + return x.CreateBeforeDestroy + } + return false +} + +func (x *StateResourceInstanceObjectV1) GetProviderConfigAddr() string { + if x != nil { + return x.ProviderConfigAddr + } + return "" +} + +func (x *StateResourceInstanceObjectV1) GetProviderSpecificData() []byte { + if x != nil { + return x.ProviderSpecificData + } + return nil +} + +type DynamicValue struct { + state protoimpl.MessageState `protogen:"open.v1"` + Value *planproto.DynamicValue `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"` + SensitivePaths []*planproto.Path `protobuf:"bytes,2,rep,name=sensitive_paths,json=sensitivePaths,proto3" json:"sensitive_paths,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DynamicValue) Reset() { + *x = DynamicValue{} + mi := &file_tfstackdata1_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DynamicValue) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DynamicValue) ProtoMessage() {} + +func (x *DynamicValue) ProtoReflect() protoreflect.Message { + mi := &file_tfstackdata1_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DynamicValue.ProtoReflect.Descriptor instead. +func (*DynamicValue) Descriptor() ([]byte, []int) { + return file_tfstackdata1_proto_rawDescGZIP(), []int{15} +} + +func (x *DynamicValue) GetValue() *planproto.DynamicValue { + if x != nil { + return x.Value + } + return nil +} + +func (x *DynamicValue) GetSensitivePaths() []*planproto.Path { + if x != nil { + return x.SensitivePaths + } + return nil +} + +var File_tfstackdata1_proto protoreflect.FileDescriptor + +var file_tfstackdata1_proto_rawDesc = string([]byte{ + 0x0a, 0x12, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, + 0x61, 0x31, 0x1a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x66, 0x69, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x39, 0x0a, + 0x0a, 0x50, 0x6c, 0x61, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x4e, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, + 0x50, 0x72, 0x69, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x45, 0x6c, 0x65, 0x6d, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x26, 0x0a, 0x03, 0x72, 0x61, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x41, 0x6e, 0x79, 0x52, 0x03, 0x72, 0x61, 0x77, 0x22, 0x2d, 0x0a, 0x0d, 0x50, 0x6c, 0x61, 0x6e, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x70, 0x70, + 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x61, 0x70, + 0x70, 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x36, 0x0a, 0x0d, 0x50, 0x6c, 0x61, 0x6e, 0x54, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, + 0x86, 0x01, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x6f, 0x6f, 0x74, 0x49, 0x6e, 0x70, 0x75, + 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, + 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x73, 0x74, + 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2a, 0x0a, 0x11, + 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x6f, 0x6e, 0x5f, 0x61, 0x70, 0x70, 0x6c, + 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x4f, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x22, 0x2e, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x52, 0x6f, 0x6f, 0x74, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, + 0x61, 0x62, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x64, 0x52, 0x6f, 0x6f, 0x74, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x4a, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x64, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, + 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, + 0x64, 0x72, 0x22, 0x56, 0x0a, 0x0f, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x43, 0x0a, 0x10, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x43, 0x61, 0x6c, 0x6c, 0x48, 0x61, 0x73, 0x68, 0x52, 0x0f, 0x66, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x22, 0x8f, 0x07, 0x0a, 0x15, 0x50, + 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x25, 0x0a, 0x0e, + 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x12, 0x6d, 0x0a, 0x14, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x69, + 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x3b, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, + 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x6e, + 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, + 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x12, 0x35, 0x0a, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x61, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x70, 0x6c, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x04, 0x6d, 0x6f, 0x64, + 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, + 0x2e, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x6d, 0x6f, 0x64, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x70, + 0x6c, 0x61, 0x6e, 0x5f, 0x61, 0x70, 0x70, 0x6c, 0x79, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x61, 0x62, + 0x6c, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6c, + 0x65, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x43, + 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x64, 0x65, 0x70, 0x65, 0x6e, + 0x64, 0x73, 0x5f, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x17, 0x64, 0x65, 0x70, + 0x65, 0x6e, 0x64, 0x73, 0x4f, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x41, + 0x64, 0x64, 0x72, 0x73, 0x12, 0x70, 0x0a, 0x15, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, + 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, 0x06, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, + 0x61, 0x31, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x43, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x13, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x48, 0x0a, 0x15, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, + 0x64, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, + 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x13, 0x70, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, + 0x12, 0x43, 0x0a, 0x10, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x72, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x61, 0x6e, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x43, 0x61, 0x6c, 0x6c, + 0x48, 0x61, 0x73, 0x68, 0x52, 0x0f, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x73, 0x1a, 0x61, 0x0a, 0x17, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, + 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x62, 0x0a, 0x18, 0x50, 0x6c, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, + 0x61, 0x74, 0x61, 0x31, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xea, 0x02, 0x0a, + 0x21, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, + 0x65, 0x64, 0x12, 0x36, 0x0a, 0x17, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x15, 0x63, 0x6f, 0x6d, 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, + 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x65, 0x64, 0x4b, 0x65, + 0x79, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x41, + 0x64, 0x64, 0x72, 0x12, 0x36, 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x52, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x4c, 0x0a, 0x0b, 0x70, + 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2b, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x56, 0x31, 0x52, 0x0a, 0x70, + 0x72, 0x69, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x9b, 0x01, 0x0a, 0x22, 0x50, 0x6c, + 0x61, 0x6e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x12, 0x2c, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x65, 0x66, 0x65, + 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x47, + 0x0a, 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, + 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x50, 0x6c, + 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x50, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x52, + 0x06, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x22, 0x6a, 0x0a, 0x17, 0x50, 0x6c, 0x61, 0x6e, 0x44, + 0x69, 0x73, 0x63, 0x61, 0x72, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x70, 0x4b, 0x65, + 0x79, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x72, 0x61, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x61, 0x77, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, + 0x65, 0x79, 0x73, 0x22, 0xee, 0x03, 0x0a, 0x18, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x31, + 0x12, 0x5d, 0x0a, 0x0d, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, + 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, 0x70, + 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x31, 0x2e, + 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, + 0x63, 0x0a, 0x0f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, + 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6d, + 0x70, 0x6f, 0x6e, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x56, 0x31, + 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, + 0x62, 0x6c, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, + 0x63, 0x79, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x79, 0x41, 0x64, 0x64, 0x72, 0x73, 0x12, + 0x27, 0x0a, 0x0f, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x64, 0x64, + 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, + 0x65, 0x6e, 0x74, 0x41, 0x64, 0x64, 0x72, 0x73, 0x1a, 0x5b, 0x0a, 0x11, 0x4f, 0x75, 0x74, 0x70, + 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5d, 0x0a, 0x13, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, + 0x72, 0x69, 0x61, 0x62, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, 0x61, 0x31, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd7, 0x03, 0x0a, 0x1d, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x56, 0x31, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, + 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x35, 0x0a, 0x0f, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0c, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0e, 0x73, 0x65, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, 0x73, 0x12, 0x25, 0x0a, 0x0e, + 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x4a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x32, 0x2e, 0x74, 0x66, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x64, 0x61, 0x74, + 0x61, 0x31, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x56, 0x31, + 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x22, 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0c, 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x65, 0x6e, 0x63, + 0x69, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x62, 0x65, + 0x66, 0x6f, 0x72, 0x65, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x65, 0x66, 0x6f, 0x72, 0x65, + 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x41, 0x64, 0x64, 0x72, 0x12, 0x34, 0x0a, 0x16, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x14, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x44, 0x61, 0x74, 0x61, 0x22, + 0x2d, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, + 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, + 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x41, 0x4d, 0x41, 0x47, 0x45, 0x44, 0x10, 0x02, 0x22, 0x71, + 0x0a, 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2a, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x35, 0x0a, 0x0f, 0x73, 0x65, + 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x61, 0x6e, 0x2e, 0x50, 0x61, 0x74, + 0x68, 0x52, 0x0e, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x50, 0x61, 0x74, 0x68, + 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) + +var ( + file_tfstackdata1_proto_rawDescOnce sync.Once + file_tfstackdata1_proto_rawDescData []byte +) + +func file_tfstackdata1_proto_rawDescGZIP() []byte { + file_tfstackdata1_proto_rawDescOnce.Do(func() { + file_tfstackdata1_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tfstackdata1_proto_rawDesc), len(file_tfstackdata1_proto_rawDesc))) + }) + return file_tfstackdata1_proto_rawDescData +} + +var file_tfstackdata1_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_tfstackdata1_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_tfstackdata1_proto_goTypes = []any{ + (StateResourceInstanceObjectV1_Status)(0), // 0: tfstackdata1.StateResourceInstanceObjectV1.Status + (*PlanHeader)(nil), // 1: tfstackdata1.PlanHeader + (*PlanPriorStateElem)(nil), // 2: tfstackdata1.PlanPriorStateElem + (*PlanApplyable)(nil), // 3: tfstackdata1.PlanApplyable + (*PlanTimestamp)(nil), // 4: tfstackdata1.PlanTimestamp + (*PlanRootInputValue)(nil), // 5: tfstackdata1.PlanRootInputValue + (*DeletedRootInputVariable)(nil), // 6: tfstackdata1.DeletedRootInputVariable + (*DeletedRootOutputValue)(nil), // 7: tfstackdata1.DeletedRootOutputValue + (*DeletedComponent)(nil), // 8: tfstackdata1.DeletedComponent + (*FunctionResults)(nil), // 9: tfstackdata1.FunctionResults + (*PlanComponentInstance)(nil), // 10: tfstackdata1.PlanComponentInstance + (*PlanResourceInstanceChangePlanned)(nil), // 11: tfstackdata1.PlanResourceInstanceChangePlanned + (*PlanDeferredResourceInstanceChange)(nil), // 12: tfstackdata1.PlanDeferredResourceInstanceChange + (*PlanDiscardStateMapKeys)(nil), // 13: tfstackdata1.PlanDiscardStateMapKeys + (*StateComponentInstanceV1)(nil), // 14: tfstackdata1.StateComponentInstanceV1 + (*StateResourceInstanceObjectV1)(nil), // 15: tfstackdata1.StateResourceInstanceObjectV1 + (*DynamicValue)(nil), // 16: tfstackdata1.DynamicValue + nil, // 17: tfstackdata1.PlanComponentInstance.PlannedInputValuesEntry + nil, // 18: tfstackdata1.PlanComponentInstance.PlannedOutputValuesEntry + nil, // 19: tfstackdata1.StateComponentInstanceV1.OutputValuesEntry + nil, // 20: tfstackdata1.StateComponentInstanceV1.InputVariablesEntry + (*anypb.Any)(nil), // 21: google.protobuf.Any + (*planproto.FunctionCallHash)(nil), // 22: tfplan.FunctionCallHash + (planproto.Action)(0), // 23: tfplan.Action + (planproto.Mode)(0), // 24: tfplan.Mode + (*planproto.CheckResults)(nil), // 25: tfplan.CheckResults + (*planproto.ResourceInstanceChange)(nil), // 26: tfplan.ResourceInstanceChange + (*planproto.Deferred)(nil), // 27: tfplan.Deferred + (*planproto.Path)(nil), // 28: tfplan.Path + (*planproto.DynamicValue)(nil), // 29: tfplan.DynamicValue +} +var file_tfstackdata1_proto_depIdxs = []int32{ + 21, // 0: tfstackdata1.PlanPriorStateElem.raw:type_name -> google.protobuf.Any + 16, // 1: tfstackdata1.PlanRootInputValue.value:type_name -> tfstackdata1.DynamicValue + 22, // 2: tfstackdata1.FunctionResults.function_results:type_name -> tfplan.FunctionCallHash + 17, // 3: tfstackdata1.PlanComponentInstance.planned_input_values:type_name -> tfstackdata1.PlanComponentInstance.PlannedInputValuesEntry + 23, // 4: tfstackdata1.PlanComponentInstance.planned_action:type_name -> tfplan.Action + 24, // 5: tfstackdata1.PlanComponentInstance.mode:type_name -> tfplan.Mode + 18, // 6: tfstackdata1.PlanComponentInstance.planned_output_values:type_name -> tfstackdata1.PlanComponentInstance.PlannedOutputValuesEntry + 25, // 7: tfstackdata1.PlanComponentInstance.planned_check_results:type_name -> tfplan.CheckResults + 22, // 8: tfstackdata1.PlanComponentInstance.function_results:type_name -> tfplan.FunctionCallHash + 26, // 9: tfstackdata1.PlanResourceInstanceChangePlanned.change:type_name -> tfplan.ResourceInstanceChange + 15, // 10: tfstackdata1.PlanResourceInstanceChangePlanned.prior_state:type_name -> tfstackdata1.StateResourceInstanceObjectV1 + 27, // 11: tfstackdata1.PlanDeferredResourceInstanceChange.deferred:type_name -> tfplan.Deferred + 11, // 12: tfstackdata1.PlanDeferredResourceInstanceChange.change:type_name -> tfstackdata1.PlanResourceInstanceChangePlanned + 19, // 13: tfstackdata1.StateComponentInstanceV1.output_values:type_name -> tfstackdata1.StateComponentInstanceV1.OutputValuesEntry + 20, // 14: tfstackdata1.StateComponentInstanceV1.input_variables:type_name -> tfstackdata1.StateComponentInstanceV1.InputVariablesEntry + 28, // 15: tfstackdata1.StateResourceInstanceObjectV1.sensitive_paths:type_name -> tfplan.Path + 0, // 16: tfstackdata1.StateResourceInstanceObjectV1.status:type_name -> tfstackdata1.StateResourceInstanceObjectV1.Status + 29, // 17: tfstackdata1.DynamicValue.value:type_name -> tfplan.DynamicValue + 28, // 18: tfstackdata1.DynamicValue.sensitive_paths:type_name -> tfplan.Path + 16, // 19: tfstackdata1.PlanComponentInstance.PlannedInputValuesEntry.value:type_name -> tfstackdata1.DynamicValue + 16, // 20: tfstackdata1.PlanComponentInstance.PlannedOutputValuesEntry.value:type_name -> tfstackdata1.DynamicValue + 16, // 21: tfstackdata1.StateComponentInstanceV1.OutputValuesEntry.value:type_name -> tfstackdata1.DynamicValue + 16, // 22: tfstackdata1.StateComponentInstanceV1.InputVariablesEntry.value:type_name -> tfstackdata1.DynamicValue + 23, // [23:23] is the sub-list for method output_type + 23, // [23:23] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name +} + +func init() { file_tfstackdata1_proto_init() } +func file_tfstackdata1_proto_init() { + if File_tfstackdata1_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tfstackdata1_proto_rawDesc), len(file_tfstackdata1_proto_rawDesc)), + NumEnums: 1, + NumMessages: 20, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_tfstackdata1_proto_goTypes, + DependencyIndexes: file_tfstackdata1_proto_depIdxs, + EnumInfos: file_tfstackdata1_proto_enumTypes, + MessageInfos: file_tfstackdata1_proto_msgTypes, + }.Build() + File_tfstackdata1_proto = out.File + file_tfstackdata1_proto_goTypes = nil + file_tfstackdata1_proto_depIdxs = nil +} diff --git a/internal/stacks/tfstackdata1/tfstackdata1.proto b/internal/stacks/tfstackdata1/tfstackdata1.proto new file mode 100644 index 0000000000..91aac6d6da --- /dev/null +++ b/internal/stacks/tfstackdata1/tfstackdata1.proto @@ -0,0 +1,388 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +syntax = "proto3"; +package tfstackdata1; + +import "planfile.proto"; // tfplan, from internal/plans/planproto +import "google/protobuf/any.proto"; + +// These definitions describe the PRIVATE raw format that we use to persist +// stack and plan information for stacks between operations. +// +// Nothing outside of this codebase should attempt to produce or consume +// these formats. They are subject to change at any time. + +///////////// PLAN SEQUENCE MESSAGES +// +// A "stack plan" consists of a sequence of messages emitted gradually from +// the streaming Stacks.PlanStackChanges RPC in the Terraform Core RPC API. +// +// From the perspective of that protocol the objects in the sequence are +// opaque and to be preserved byte-for-byte without any external interpretation, +// in the same order they were emitted from Terraform Core. +// +// Internally, we decode each one based on the type field of google.protobuf.Any, +// treating each one as some kind of mutation of our in-memory plan data +// structure. +// +// These message types only cover the data that Terraform needs to apply the +// plan, and so don't cover any information that Terraform Core might emit +// only for the caller's benefit. +////////////// + +// Appears early in a raw plan sequence to capture some metadata that we need +// to process subsequent messages, or to abort if we're being asked to decode +// a plan created by a different version of Terraform. +message PlanHeader { + // The canonical version string for the version of Terraform that created + // the plan sequence that this message belongs to. + // + // The raw plan sequence loader will fail if it finds a message of this + // type with a version string that disagrees with the version of Terraform + // decoding the message, because we always expect plans to be applied by + // the same version of Terraform that created them. + string terraform_version = 1; +} + +// Captures one element from the raw prior state that was provided when +// creating the plan. A valid plan includes a copy of its entire prior state +// represented as zero or more messages of this type, which we then interpret +// as a map from key to raw during load. +message PlanPriorStateElem { + string key = 1; + google.protobuf.Any raw = 2; +} + +// Confirms whether the overall plan whose raw plan sequence includes this +// message is complete enough and valid enough to be applied. +// +// If a the sequence of raw plan messages includes multiple messages of this +// type then the one with the latest position in the list "wins" during +// decoding of the overall sequence, although in practice there isn't yet +// any clear reason to include more than one instance of this message type in a +// plan. +message PlanApplyable { + bool applyable = 1; +} + +// Records the plan timestamp to be used for all components and the stacks language. +message PlanTimestamp { + string plan_timestamp = 1; +} + +// Records the value of one of the main stack's input values during planning. +// +// These values get fixed during the plan phase so that we can ensure that we +// use identical values when subsequently applying the plan. +message PlanRootInputValue { + string name = 1; + DynamicValue value = 2; + bool required_on_apply = 3; +} + +// Records that a root input variable should be deleted by the apply operation. +message DeletedRootInputVariable { + string name = 1; +} + +// Records that a root output should be deleted by the apply operation. +message DeletedRootOutputValue { + string name = 1; +} + +// Records that a component should just be deleted from the state. +message DeletedComponent { + string component_instance_addr = 1; +} + +// FunctionResults stores a record of the results of provider functions +// that were called during the planning phase. This is used to ensure that the +// same results are returned during the apply phase. +message FunctionResults { + repeated tfplan.FunctionCallHash function_results = 1; +} + +// Represents the existence of a particular component instance, and so must +// always appear before any messages representing objects that belong to that +// component instance. +// +// This message type exists to avoid the ambiguity between a component instance +// existing with zero resource instances inside vs. a component instance +// not existing at all. +message PlanComponentInstance { + string component_instance_addr = 1; + + // plan_timestamp records the time when the plan for this component + // instance was created, exclusively for making sure that the + // "plantimestamp" function can return the same value during the apply + // phase. It must not be used for any other purpose. + string plan_timestamp = 2; + + // Captures an approximation of the input values for this component with + // as much detail as we knew during the planning phase. This might + // contain unknown values as placeholders for values that won't be + // determined until the apply phase, so this isn't usable directly as + // the input to subsequently applying the component plan but the final + // input values should be a valid concretization of what's described here. + map planned_input_values = 3; + + // The action planned for the component as a whole. + // + // This does not directly incorporate actions planned for resource + // instances within this component instance, but does capture a sense + // of the overall action being taken for this particular component + // instance. + // + // The currently-possible values are: + // - CREATE and UPDATE both describe applying a "normal" plan, where + // CREATE additionally represents that the component instance + // did not previously exist. + // - READ describes a refresh-only plan. This is currently possible only + // if the overall stack plan is refresh-only. + // - DELETE describes applying a destroy plan, with the intent of + // deleting all remote objects currently bound to resource instances + // in this component instance. + // + // The value recorded here is used to achieve a few variations needed in + // the apply phase. + tfplan.Action planned_action = 4; + + // The mode that was used to plan this component. + // + // This is used to determine the behavior of the apply phase for this + // component instance. + // + // Ideally, we wouldn't need to include this at all as the plan should + // contain everything we need without a general mode. However, this is + // not currently the case. See context_apply.go:332 for more details. + // TODO: Remove this once walkDestroy has been properly audited. + tfplan.Mode mode = 10; + + // The appliability flag decided by the modules runtime for this component's + // plan. See the docs for plans.Plan.Applyable for details on what this + // represents. (It's here largely just so that we can repopulate it + // faithfully when we rebuild a plans.Plan object at apply time.) + bool plan_applyable = 7; + + // The completion flag decided by the modules runtime for this component's + // plan. See the docs for plans.Plan.Complete for details on what this + // represents. (It's here largely just so that we can repopulate it + // faithfully when we rebuild a plans.Plan object at apply time.) + bool plan_complete = 8; + + // A list of absolute component addresses that this component + // instance depends on according to the configuration the plan was + // created from. (These are components rather than component instances + // because the stacks language evaluation model uses components as the + // most specific granularity for dependency resolution.) + // + // Applying this component instance's plan must wait until any + // CREATE or UPDATE plans for any of the listed component instances have + // completed successfully. Additionally, if any of the component instances + // listed here have DELETE plans then this component instance must also + // have a DELETE plan and the upstream DELETE must wait until this one + // has completed. + // + // A component instance plan that is not DELETE cannot depend on another + // component instance that is not also DELETE, since that would imply that + // this component instance's configuration refers to a component that isn't + // declared, which should therefore have failed validation. + repeated string depends_on_component_addrs = 5; + + // Captures an approximation of the output values for this component with + // as much detail as we knew during the planning phase. + // + // For any planned action other than DELETE this might contain unknown + // values as placeholders for values that won't be determined until the + // apply phase + // + // For a DELETE plan the values should always be known because they are + // based on the prior state for the component, before it has been destroyed. + // The apply phase should use these values to build the representation of + // the component instance as an expression, because for DELETE any + // dependent objects must also be pending DELETE and their delete must + // happen before this instance is destroyed. + map planned_output_values = 6; + + // A list of check results for this component instance, as produced by + // the modules runtime during the planning phase. The apply expects to + // update check results which were unknown during planning to reflect + // the actual results from the apply phase. + repeated tfplan.CheckResults planned_check_results = 9; + + // The set of provider function results that were produced during the + // planning phase for this component instance. These results are used + // to ensure that the same results are returned during the apply phase. + repeated tfplan.FunctionCallHash function_results = 11; +} + +// Represents a planned change to a particular resource instance within a +// particular component instance. +message PlanResourceInstanceChangePlanned { + // The same string must previously have been announced with a + // PlanComponentInstance message, or the overall plan sequence is invalid. + string component_instance_addr = 1; + string resource_instance_addr = 4; + string deposed_key = 5; + + // The address of the provider configuration that planned this change, + // or that produced the prior state for messages where "change" is + // unpopulated. This is a module-centric view relative to the root module + // of the component identified in component_instance_addr. + string provider_config_addr = 6; + + // Description of the planned change in the standard "tfplan" (planproto) + // format. + tfplan.ResourceInstanceChange change = 2; + + // A snapshot of the "prior state", which is the result of upgrading and + // refreshing the previous run's state. + // + // The very first action on applying this plan should be to update the + // raw state for the resource instance to match this value, since + // the main apply phase for each component instance assumes that the + // prior state has already been updated to match the "old" value from + // the "change" message. + StateResourceInstanceObjectV1 prior_state = 3; +} + +// Represents a deferred change to a particular resource instance within a +// particular component instance. +message PlanDeferredResourceInstanceChange { + tfplan.Deferred deferred = 1; + PlanResourceInstanceChangePlanned change = 2; +} + +// Represents that we need to emit "delete" requests for one or more raw +// state and/or state description objects during the apply phase. +// +// This situation arises if the previous state (given as input to the apply +// phase) contains keys that are of a type unrecognized by the current +// version of Terraform and that are marked as "discard if unrecognized", +// suggesting that their content is likely to become somehow invalid if +// other parts of the state were to get updated. +message PlanDiscardStateMapKeys { + // A set of keys to delete from the "raw state". + repeated string raw_state_keys = 1; + + // A set of keys to delete from the "state description". + repeated string description_keys = 2; +} + +///////////// STATE MAP MESSAGES +// +// A "stack state snapshot" is a mapping from arbitrary keys to messages +// emitted gradually from the streaming Stacks.ApplyStackChanges RPC in the +// Terraform Core RPC API. +// +// From the perspective of that protocol the keys and values in the map are +// opaque and to be preserved verbatim without any external interpretation, +// overwriting any previous value that had the same key. +// +// Internally, we decode each one based on the type field of google.protobuf.Any, +// treating each one as some kind of mutation of our in-memory plan data +// structure. +// +// These message types only cover the data that Terraform needs to produce +// a future plan based on this snapshot, and don't cover any information that +// Terraform Core might emit only for the caller's benefit. +// +// Because state messages survive from one run to the next, all top-level +// messages used for state snapshots have a format version suffix that is +// currently always 1. The functions that load a state map into the in-memory +// state structure will fail if any of the messages are of an unknown type, so +// we should increment the format version only as a last resort because this +// will prevent users from downgrading to an earlier version of Terraform once +// they've got at least one state map message that is of a newer version. +////////////// + +// Represents the existence of a particular component instance. +// +// This is here mainly to remove the ambiguity between a component instance that +// exists but contains no resource instances vs. a component instance that +// doesn't exist at all. +// +// Because the state map is updated on a per-element basis rather than +// atomically, it's possible that the state map might contain resource instances +// which belong to a component instance that is not tracked by a message of +// this type. In that case, the state loader will just assume an implied +// message of this type with a matching component instance address and with +// all other fields unset. +message StateComponentInstanceV1 { + // The component instance's output values as reported from the most recent + // apply action. We retain this only so that we have some values to use + // in cases where the values in the configuration are unavailable or + // insufficient, such as when we're making a destroy-mode plan and therefore + // the desired state would be for the component instance to cease existing + // but yet we still need to have _some_ output values to use when planning + // and applying other component instances that refer to this one. + map output_values = 1; + + // The input variables for this component instance as reported from the + // most recent apply action. We retain this only for usage within removed + // blocks, where we need to know the input variables to be able to plan + // and apply the destroy action without asking the user to resupply or + // remember them. + map input_variables = 2; + + // The absolute configuration addresses of components that this component + // instance depended on when it was created. We preserve this information + // to help with plan and apply ordering during destroy plans or for removed + // blocks. + repeated string dependency_addrs = 3; + + // The absolute configuration addresses of components that depended on this + // component instance when it was created. We preserve this information + // to help with plan and apply ordering during destroy plans or for removed + // blocks. + repeated string dependent_addrs = 4; +} + +// Represents the existence of a particular resource instance object in a +// particular component instance. +// +// A resource instance message object should typically be accompanied by a +// StateComponentInstanceV1 (or later version) that represents the existence +// of the component itself, but for robustness we tolerate the absense of +// such a message and just assume that all of its fields (other than the +// component instance address) are unset. +message StateResourceInstanceObjectV1 { + // value_json is a JSON representation of the object value representing + // this resource instance object. + // + // This is JSON-serialized rather than MessagePack serialized (as we do + // for everything else in this format and in the RPC API) because + // the provider protocol only supports legacy flatmap and JSON as input + // to the state upgrade process, and we won't be able to transcode from + // MessagePack to JSON once we decode this because we won't know the + // schema that the value was encoded with. + // + // This is a pragmatic exception for this particular quirk of Terraform's + // provider API design. Other parts of this format and associated protocol + // should use tfplan.DynamicValue and MessagePack encoding for consistency. + bytes value_json = 1; + repeated tfplan.Path sensitive_paths = 2; + uint64 schema_version = 3; + + Status status = 4; + repeated string dependencies = 5; + bool create_before_destroy = 6; + string provider_config_addr = 7; + + // provider_specific_data is arbitrary bytes produced by the provider + // in its apply response which we preserve and pass back to it in any + // subsequent plan operation. + bytes provider_specific_data = 8; + + enum Status { + UNKNOWN = 0; + READY = 1; + DAMAGED = 2; // (formerly known as "tainted") + } +} + +message DynamicValue { + tfplan.DynamicValue value = 1; + repeated tfplan.Path sensitive_paths = 2; +} diff --git a/internal/states/checks.go b/internal/states/checks.go index 181871a766..f5718e49f2 100644 --- a/internal/states/checks.go +++ b/internal/states/checks.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( diff --git a/internal/states/doc.go b/internal/states/doc.go index 7dd74ac785..0c49132618 100644 --- a/internal/states/doc.go +++ b/internal/states/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package states contains the types that are used to represent Terraform // states. package states diff --git a/internal/states/instance_generation.go b/internal/states/instance_generation.go deleted file mode 100644 index 891adc003c..0000000000 --- a/internal/states/instance_generation.go +++ /dev/null @@ -1,20 +0,0 @@ -package states - -// Generation is used to represent multiple objects in a succession of objects -// represented by a single resource instance address. A resource instance can -// have multiple generations over its lifetime due to object replacement -// (when a change can't be applied without destroying and re-creating), and -// multiple generations can exist at the same time when create_before_destroy -// is used. -// -// A Generation value can either be the value of the variable "CurrentGen" or -// a value of type DeposedKey. Generation values can be compared for equality -// using "==" and used as map keys. The zero value of Generation (nil) is not -// a valid generation and must not be used. -type Generation interface { - generation() -} - -// CurrentGen is the Generation representing the currently-active object for -// a resource instance. -var CurrentGen Generation diff --git a/internal/states/instance_object.go b/internal/states/instance_object.go index 0e790bba1a..82a4ab1e25 100644 --- a/internal/states/instance_object.go +++ b/internal/states/instance_object.go @@ -1,12 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( + "fmt" "sort" "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/format" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" ) // ResourceInstanceObject is the local representation of a specific remote @@ -21,6 +28,10 @@ type ResourceInstanceObject struct { // Terraform. Value cty.Value + // Identity is the object-typed value representing the identity of the remote + // object within Terraform. + Identity cty.Value + // Private is an opaque value set by the provider when this object was // last created or updated. Terraform Core does not use this value in // any way and it is not exposed anywhere in the user interface, so @@ -46,10 +57,29 @@ type ResourceInstanceObject struct { CreateBeforeDestroy bool } +// NewResourceInstanceObjectFromIR converts the receiving +// ImportedResource into a ResourceInstanceObject that has status ObjectReady. +// +// The returned object does not know its own resource type, so the caller must +// retain the ResourceType value from the source object if this information is +// needed. +// +// The returned object also has no dependency addresses, but the caller may +// freely modify the direct fields of the returned object without affecting +// the receiver. +func NewResourceInstanceObjectFromIR(ir providers.ImportedResource) *ResourceInstanceObject { + return &ResourceInstanceObject{ + Status: ObjectReady, + Value: ir.State, + Private: ir.Private, + Identity: ir.Identity, + } +} + // ObjectStatus represents the status of a RemoteObject. type ObjectStatus rune -//go:generate go run golang.org/x/tools/cmd/stringer -type ObjectStatus +//go:generate go tool golang.org/x/tools/cmd/stringer -type ObjectStatus const ( // ObjectReady is an object status for an object that is ready to use. @@ -74,25 +104,27 @@ const ( ObjectPlanned ObjectStatus = 'P' ) -// Encode marshals the value within the receiver to produce a +// Encode marshals values within the receiver to produce a // ResourceInstanceObjectSrc ready to be written to a state file. // -// The given type must be the implied type of the resource type schema, and -// the given value must conform to it. It is important to pass the schema -// type and not the object's own type so that dynamically-typed attributes -// will be stored correctly. The caller must also provide the version number -// of the schema that the given type was derived from, which will be recorded -// in the source object so it can be used to detect when schema migration is -// required on read. +// The schema must contain the resource type body, and the given value must +// conform its implied type. The schema must also contain the version number +// of the schema, which will be recorded in the source object so it can be +// used to detect when schema migration is required on read. +// The schema may also contain an resource identity schema and version number, +// which will be used to encode the resource identity. // // The returned object may share internal references with the receiver and // so the caller must not mutate the receiver any further once once this // method is called. -func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) { +func (o *ResourceInstanceObject) Encode(schema providers.Schema) (*ResourceInstanceObjectSrc, error) { // If it contains marks, remove these marks before traversing the // structure with UnknownAsNull, and save the PathValueMarks // so we can save them in state. - val, pvm := o.Value.UnmarkDeepWithPaths() + val, sensitivePaths, err := unmarkValueForStorage(o.Value) + if err != nil { + return nil, err + } // Our state serialization can't represent unknown values, so we convert // them to nulls here. This is lossy, but nobody should be writing unknown @@ -105,11 +137,20 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res // and raise an error about that. val = cty.UnknownAsNull(val) - src, err := ctyjson.Marshal(val, ty) + src, err := ctyjson.Marshal(val, schema.Body.ImpliedType()) if err != nil { return nil, err } + var idJSON []byte + // If the Identity is known and not null we can marshal it. + if !o.Identity.IsNull() && o.Identity.IsWhollyKnown() && schema.Identity != nil { + idJSON, err = ctyjson.Marshal(o.Identity, schema.Identity.ImpliedType()) + if err != nil { + return nil, err + } + } + // Dependencies are collected and merged in an unordered format (using map // keys as a set), then later changed to a slice (in random ordering) to be // stored in state as an array. To avoid pointless thrashing of state in @@ -123,13 +164,18 @@ func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*Res sort.Slice(dependencies, func(i, j int) bool { return dependencies[i].String() < dependencies[j].String() }) return &ResourceInstanceObjectSrc{ - SchemaVersion: schemaVersion, - AttrsJSON: src, - AttrSensitivePaths: pvm, - Private: o.Private, - Status: o.Status, - Dependencies: dependencies, - CreateBeforeDestroy: o.CreateBeforeDestroy, + SchemaVersion: uint64(schema.Version), + AttrsJSON: src, + AttrSensitivePaths: sensitivePaths, + Private: o.Private, + Status: o.Status, + Dependencies: dependencies, + CreateBeforeDestroy: o.CreateBeforeDestroy, + IdentityJSON: idJSON, + IdentitySchemaVersion: uint64(schema.IdentityVersion), + // The cached value must have all its marks since it bypasses decoding. + decodeValueCache: o.Value, + decodeIdentityCache: o.Identity, }, nil } @@ -146,3 +192,31 @@ func (o *ResourceInstanceObject) AsTainted() *ResourceInstanceObject { ret.Status = ObjectTainted return ret } + +// unmarkValueForStorage takes a value that possibly contains marked values +// and returns an equal value without markings along with the separated mark +// metadata that should be stored alongside the value in another field. +// +// This function only accepts the marks that are valid to store, and so will +// return an error if other marks are present. Marks that this package doesn't +// know how to store must be dealt with somehow by a caller -- presumably by +// replacing each marked value with some sort of storage placeholder -- before +// writing a value into the state. +func unmarkValueForStorage(v cty.Value) (unmarkedV cty.Value, sensitivePaths []cty.Path, err error) { + val, pvms := v.UnmarkDeepWithPaths() + sensitivePaths, withOtherMarks := marks.PathsWithMark(pvms, marks.Sensitive) + if len(withOtherMarks) != 0 { + return cty.NilVal, nil, fmt.Errorf( + "%s: cannot serialize value marked as %#v for inclusion in a state snapshot (this is a bug in Terraform)", + format.CtyPath(withOtherMarks[0].Path), withOtherMarks[0].Marks, + ) + } + + // sort the sensitive paths for consistency in comparison and serialization + sort.Slice(sensitivePaths, func(i, j int) bool { + // use our human-readable format of paths for comparison + return format.CtyPath(sensitivePaths[i]) < format.CtyPath(sensitivePaths[j]) + }) + + return val, sensitivePaths, nil +} diff --git a/internal/states/instance_object_src.go b/internal/states/instance_object_src.go index a564e0d907..49600d9552 100644 --- a/internal/states/instance_object_src.go +++ b/internal/states/instance_object_src.go @@ -1,11 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( + "fmt" + "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" ) // ResourceInstanceObjectSrc is a not-fully-decoded version of @@ -36,6 +43,10 @@ type ResourceInstanceObjectSrc struct { // schema version should be recorded in the SchemaVersion field. AttrsJSON []byte + IdentitySchemaVersion uint64 + + IdentityJSON []byte + // AttrsFlat is a legacy form of attributes used in older state file // formats, and in the new state format for objects that haven't yet been // upgraded. This attribute is mutually exclusive with Attrs: for any @@ -51,7 +62,7 @@ type ResourceInstanceObjectSrc struct { // AttrSensitivePaths is an array of paths to mark as sensitive coming out of // state, or to save as sensitive paths when saving state - AttrSensitivePaths []cty.PathValueMarks + AttrSensitivePaths []cty.Path // These fields all correspond to the fields of the same name on // ResourceInstanceObject. @@ -59,40 +70,63 @@ type ResourceInstanceObjectSrc struct { Status ObjectStatus Dependencies []addrs.ConfigResource CreateBeforeDestroy bool + + // decodeValueCache stored the decoded value for repeated decodings. + decodeValueCache cty.Value + // decodeIdentityCache stored the decoded identity for repeated decodings. + decodeIdentityCache cty.Value } // Decode unmarshals the raw representation of the object attributes. Pass the -// implied type of the corresponding resource type schema for correct operation. +// schema of the corresponding resource type for correct operation. // // Before calling Decode, the caller must check that the SchemaVersion field // exactly equals the version number of the schema whose implied type is being // passed, or else the result is undefined. // +// If the object has an identity, the schema must also contain a resource +// identity schema for the identity to be decoded. +// // The returned object may share internal references with the receiver and // so the caller must not mutate the receiver any further once once this // method is called. -func (os *ResourceInstanceObjectSrc) Decode(ty cty.Type) (*ResourceInstanceObject, error) { +func (os *ResourceInstanceObjectSrc) Decode(schema providers.Schema) (*ResourceInstanceObject, error) { var val cty.Value var err error - if os.AttrsFlat != nil { + attrsTy := schema.Body.ImpliedType() + + switch { + case os.decodeValueCache != cty.NilVal: + val = os.decodeValueCache + + case os.AttrsFlat != nil: // Legacy mode. We'll do our best to unpick this from the flatmap. - val, err = hcl2shim.HCL2ValueFromFlatmap(os.AttrsFlat, ty) + val, err = hcl2shim.HCL2ValueFromFlatmap(os.AttrsFlat, attrsTy) if err != nil { return nil, err } - } else { - val, err = ctyjson.Unmarshal(os.AttrsJSON, ty) - // Mark the value with paths if applicable - if os.AttrSensitivePaths != nil { - val = val.MarkWithPaths(os.AttrSensitivePaths) - } + + default: + val, err = ctyjson.Unmarshal(os.AttrsJSON, attrsTy) + val = marks.MarkPaths(val, marks.Sensitive, os.AttrSensitivePaths) if err != nil { return nil, err } } + var identity cty.Value + if os.decodeIdentityCache != cty.NilVal { + identity = os.decodeIdentityCache + } else if os.IdentityJSON != nil { + identity, err = ctyjson.Unmarshal(os.IdentityJSON, schema.Identity.ImpliedType()) + if err != nil { + return nil, fmt.Errorf("failed to decode identity: %s. This is most likely a bug in the Provider, providers must not change the identity schema without updating the identity schema version", err.Error()) + } + } + return &ResourceInstanceObject{ Value: val, + Identity: identity, Status: os.Status, Dependencies: os.Dependencies, Private: os.Private, @@ -121,3 +155,16 @@ func (os *ResourceInstanceObjectSrc) CompleteUpgrade(newAttrs cty.Value, newType new.SchemaVersion = newSchemaVersion return new, nil } + +func (os *ResourceInstanceObjectSrc) CompleteIdentityUpgrade(newAttrs cty.Value, schema providers.Schema) (*ResourceInstanceObjectSrc, error) { + new := os.DeepCopy() + + src, err := ctyjson.Marshal(newAttrs, schema.Identity.ImpliedType()) + if err != nil { + return nil, err + } + + new.IdentityJSON = src + new.IdentitySchemaVersion = uint64(schema.IdentityVersion) + return new, nil +} diff --git a/internal/states/instance_object_test.go b/internal/states/instance_object_test.go index e7f4eca6a0..7b5a1a4bde 100644 --- a/internal/states/instance_object_test.go +++ b/internal/states/instance_object_test.go @@ -1,18 +1,51 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( + "fmt" "sync" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" ) func TestResourceInstanceObject_encode(t *testing.T) { value := cty.ObjectVal(map[string]cty.Value{ "foo": cty.True, + "obj": cty.ObjectVal(map[string]cty.Value{ + "sensitive": cty.StringVal("secret").Mark(marks.Sensitive), + }), + "sensitive_a": cty.StringVal("secret").Mark(marks.Sensitive), + "sensitive_b": cty.StringVal("secret").Mark(marks.Sensitive), }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.Bool, + }, + "obj": { + Type: cty.Object(map[string]cty.Type{ + "sensitive": cty.String, + }), + }, + "sensitive_a": { + Type: cty.String, + }, + "sensitive_b": { + Type: cty.String, + }, + }, + }, + Version: 0, + } // The in-memory order of resource dependencies is random, since they're an // unordered set. depsOne := []addrs.ConfigResource{ @@ -62,7 +95,7 @@ func TestResourceInstanceObject_encode(t *testing.T) { wg.Add(1) go func() { defer wg.Done() - rios, err := obj.Encode(value.Type(), 0) + rios, err := obj.Encode(schema) if err != nil { t.Errorf("unexpected error: %s", err) } @@ -80,4 +113,46 @@ func TestResourceInstanceObject_encode(t *testing.T) { t.Errorf("identical dependencies got encoded in different orders:\n%s", diff) } } + + // sensitive paths must also be consistent got comparison + for i := 0; i < len(encoded)-1; i++ { + a, b := fmt.Sprintf("%#v", encoded[i].AttrSensitivePaths), fmt.Sprintf("%#v", encoded[i+1].AttrSensitivePaths) + if diff := cmp.Diff(a, b); diff != "" { + t.Errorf("sensitive paths got encoded in different orders:\n%s", diff) + } + } +} + +func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) { + value := cty.ObjectVal(map[string]cty.Value{ + // State only supports a subset of marks that we know how to persist + // between plan/apply rounds. All values with other marks must be + // replaced with unmarked placeholders before attempting to store the + // value in the state. + "foo": cty.True.Mark("unsupported"), + }) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.Bool, + }, + }, + }, + Version: 0, + } + + obj := &ResourceInstanceObject{ + Value: value, + Status: ObjectReady, + } + _, err := obj.Encode(schema) + if err == nil { + t.Fatalf("unexpected success; want error") + } + got := err.Error() + want := `.foo: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)` + if got != want { + t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) + } } diff --git a/internal/states/module.go b/internal/states/module.go index 2f6242ace5..0beadd94d4 100644 --- a/internal/states/module.go +++ b/internal/states/module.go @@ -1,8 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( - "github.com/zclconf/go-cty/cty" - "github.com/hashicorp/terraform/internal/addrs" ) @@ -13,23 +14,13 @@ type Module struct { // Resources contains the state for each resource. The keys in this map are // an implementation detail and must not be used by outside callers. Resources map[string]*Resource - - // OutputValues contains the state for each output value. The keys in this - // map are output value names. - OutputValues map[string]*OutputValue - - // LocalValues contains the value for each named output value. The keys - // in this map are local value names. - LocalValues map[string]cty.Value } // NewModule constructs an empty module state for the given module address. func NewModule(addr addrs.ModuleInstance) *Module { return &Module{ - Addr: addr, - Resources: map[string]*Resource{}, - OutputValues: map[string]*OutputValue{}, - LocalValues: map[string]cty.Value{}, + Addr: addr, + Resources: map[string]*Resource{}, } } @@ -193,6 +184,32 @@ func (ms *Module) ForgetResourceInstanceAll(addr addrs.ResourceInstance) { } } +// ForgetResourceInstanceCurrent removes the record of the current object with +// the given address, if present. If not present, this is a no-op. +func (ms *Module) ForgetResourceInstanceCurrent(addr addrs.ResourceInstance) { + rs := ms.Resource(addr.Resource) + if rs == nil { + return + } + is := rs.Instance(addr.Key) + if is == nil { + return + } + + is.Current = nil + + if !is.HasObjects() { + // If we have no objects at all then we'll clean up. + delete(rs.Instances, addr.Key) + } + if len(rs.Instances) == 0 { + // Also clean up if we only expect to have one instance anyway + // and there are none. We leave the resource behind if an each mode + // is active because an empty list or map of instances is a valid state. + delete(ms.Resources, addr.Resource.String()) + } +} + // ForgetResourceInstanceDeposed removes the record of the deposed object with // the given address and key, if present. If not present, this is a no-op. func (ms *Module) ForgetResourceInstanceDeposed(addr addrs.ResourceInstance, key DeposedKey) { @@ -250,43 +267,6 @@ func (ms *Module) maybeRestoreResourceInstanceDeposed(addr addrs.ResourceInstanc return true } -// SetOutputValue writes an output value into the state, overwriting any -// existing value of the same name. -func (ms *Module) SetOutputValue(name string, value cty.Value, sensitive bool) *OutputValue { - os := &OutputValue{ - Addr: addrs.AbsOutputValue{ - Module: ms.Addr, - OutputValue: addrs.OutputValue{ - Name: name, - }, - }, - Value: value, - Sensitive: sensitive, - } - ms.OutputValues[name] = os - return os -} - -// RemoveOutputValue removes the output value of the given name from the state, -// if it exists. This method is a no-op if there is no value of the given -// name. -func (ms *Module) RemoveOutputValue(name string) { - delete(ms.OutputValues, name) -} - -// SetLocalValue writes a local value into the state, overwriting any -// existing value of the same name. -func (ms *Module) SetLocalValue(name string, value cty.Value) { - ms.LocalValues[name] = value -} - -// RemoveLocalValue removes the local value of the given name from the state, -// if it exists. This method is a no-op if there is no value of the given -// name. -func (ms *Module) RemoveLocalValue(name string) { - delete(ms.LocalValues, name) -} - // PruneResourceHusks is a specialized method that will remove any Resource // objects that do not contain any instances, even if they have an EachMode. // @@ -313,9 +293,9 @@ func (ms *Module) empty() bool { return true } - // This must be updated to cover any new collections added to Module - // in future. - return (len(ms.Resources) == 0 && - len(ms.OutputValues) == 0 && - len(ms.LocalValues) == 0) + // Resource instance objects -- each of which must belong to a resource -- + // are the only significant thing we track on a per-module basis. + // (The presence of root module output values also causes a state to + // be "not empty", but the main [State] object tracks those.) + return len(ms.Resources) == 0 } diff --git a/internal/states/output_value.go b/internal/states/output_value.go index 5415951648..98a3606c22 100644 --- a/internal/states/output_value.go +++ b/internal/states/output_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( diff --git a/internal/states/remote/remote.go b/internal/states/remote/remote.go index a87c9145a8..3eb1c573b5 100644 --- a/internal/states/remote/remote.go +++ b/internal/states/remote/remote.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( diff --git a/internal/states/remote/remote_test.go b/internal/states/remote/remote_test.go index 55e23342a3..0741a5b959 100644 --- a/internal/states/remote/remote_test.go +++ b/internal/states/remote/remote_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( diff --git a/internal/states/remote/state.go b/internal/states/remote/state.go index ae123f8e36..34fbbc638c 100644 --- a/internal/states/remote/state.go +++ b/internal/states/remote/state.go @@ -1,12 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( "bytes" + "context" "fmt" + "log" "sync" uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -34,10 +40,17 @@ type State struct { serial, readSerial uint64 state, readState *states.State disableLocks bool + + // If this is set then the state manager will decline to store intermediate + // state snapshots created while a Terraform Core apply operation is in + // progress. Otherwise (by default) it will accept persistent snapshots + // using the default rules defined in the local backend. + DisableIntermediateSnapshots bool } var _ statemgr.Full = (*State)(nil) var _ statemgr.Migrator = (*State)(nil) +var _ statemgr.IntermediateStateConditionalPersister = (*State)(nil) // statemgr.Reader impl. func (s *State) State() *states.State { @@ -47,7 +60,7 @@ func (s *State) State() *states.State { return s.state.DeepCopy() } -func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { +func (s *State) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { if err := s.RefreshState(); err != nil { return nil, fmt.Errorf("Failed to load state: %s", err) } @@ -57,7 +70,7 @@ func (s *State) GetRootOutputValues() (map[string]*states.OutputValue, error) { state = states.NewState() } - return state.RootModule().OutputValues, nil + return state.RootOutputValues, nil } // StateForMigration is part of our implementation of statemgr.Migrator. @@ -153,10 +166,13 @@ func (s *State) refreshState() error { } // statemgr.Persister impl. -func (s *State) PersistState() error { +func (s *State) PersistState(schemas *schemarepo.Schemas) error { s.mu.Lock() defer s.mu.Unlock() + log.Printf("[DEBUG] states/remote: state read serial is: %d; serial is: %d", s.readSerial, s.serial) + log.Printf("[DEBUG] states/remote: state read lineage is: %s; lineage is: %s", s.readLineage, s.lineage) + if s.readState != nil { lineageUnchanged := s.readLineage != "" && s.lineage == s.readLineage serialUnchanged := s.readSerial != 0 && s.serial == s.readSerial @@ -174,13 +190,15 @@ func (s *State) PersistState() error { if err != nil { return fmt.Errorf("failed checking for existing remote state: %s", err) } + log.Printf("[DEBUG] states/remote: after refresh, state read serial is: %d; serial is: %d", s.readSerial, s.serial) + log.Printf("[DEBUG] states/remote: after refresh, state read lineage is: %s; lineage is: %s", s.readLineage, s.lineage) if s.lineage == "" { // indicates that no state snapshot is present yet lineage, err := uuid.GenerateUUID() if err != nil { return fmt.Errorf("failed to generate initial lineage: %v", err) } s.lineage = lineage - s.serial = 0 + s.serial++ } } @@ -209,6 +227,14 @@ func (s *State) PersistState() error { return nil } +// ShouldPersistIntermediateState implements statemgr.IntermediateStateConditionalPersister +func (s *State) ShouldPersistIntermediateState(info *statemgr.IntermediateStatePersistInfo) bool { + if s.DisableIntermediateSnapshots { + return false + } + return statemgr.DefaultIntermediateStatePersistRule(info) +} + // Lock calls the Client's Lock method if it's implemented. func (s *State) Lock(info *statemgr.LockInfo) (string, error) { s.mu.Lock() diff --git a/internal/states/remote/state_test.go b/internal/states/remote/state_test.go index d8f8f5757c..8d207f7a35 100644 --- a/internal/states/remote/state_test.go +++ b/internal/states/remote/state_test.go @@ -1,13 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( + "context" "log" "sync" "testing" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "github.com/zclconf/go-cty/cty" + tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/states/statemgr" @@ -37,7 +44,7 @@ func TestStateRace(t *testing.T) { go func() { defer wg.Done() s.WriteState(current) - s.PersistState() + s.PersistState(nil) s.RefreshState() }() } @@ -49,8 +56,8 @@ type testCase struct { name string // A function to mutate state and return a cleanup function mutationFunc func(*State) (*states.State, func()) - // The expected request to have taken place - expectedRequest mockClientRequest + // The expected requests to have taken place + expectedRequests []mockClientRequest // Mark this case as not having a request noRequest bool } @@ -59,55 +66,147 @@ type testCase struct { // a request doesn't have one by checking if a method exists // on the expectedRequest. func (tc testCase) isRequested(t *testing.T) bool { - hasMethod := tc.expectedRequest.Method != "" - if tc.noRequest && hasMethod { - t.Fatalf("expected no content for %q but got: %v", tc.name, tc.expectedRequest) + for _, expectedMethod := range tc.expectedRequests { + hasMethod := expectedMethod.Method != "" + if tc.noRequest && hasMethod { + t.Fatalf("expected no content for %q but got: %v", tc.name, expectedMethod) + } } return !tc.noRequest } func TestStatePersist(t *testing.T) { testCases := []testCase{ - // Refreshing state before we run the test loop causes a GET { - name: "refresh state", + name: "first state persistence", mutationFunc: func(mgr *State) (*states.State, func()) { - return mgr.State(), func() {} + mgr.state = &states.State{ + Modules: map[string]*states.Module{"": {}}, + } + s := mgr.State() + s.RootModule().SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Name: "myfile", + Type: "local_file", + }.Instance(addrs.NoKey), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "filename": "file.txt", + }, + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: tfaddr.Provider{Namespace: "local"}, + }, + ) + return s, func() {} }, - expectedRequest: mockClientRequest{ - Method: "Get", - Content: map[string]interface{}{ - "version": 4.0, // encoding/json decodes this as float64 by default - "lineage": "mock-lineage", - "serial": 1.0, // encoding/json decodes this as float64 by default - "terraform_version": "0.0.0", - "outputs": map[string]interface{}{}, - "resources": []interface{}{}, + expectedRequests: []mockClientRequest{ + // Expect an initial refresh, which returns nothing since there is no remote state. + { + Method: "Get", + Content: nil, + }, + // Expect a second refresh, since the read state is nil + { + Method: "Get", + Content: nil, + }, + // Expect an initial push with values and a serial of 1 + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "some meaningless value", + "serial": 1.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{}, + "resources": []interface{}{ + map[string]interface{}{ + "instances": []interface{}{ + map[string]interface{}{ + "attributes_flat": map[string]interface{}{ + "filename": "file.txt", + }, + "identity_schema_version": 0.0, + "schema_version": 0.0, + "sensitive_attributes": []interface{}{}, + }, + }, + "mode": "managed", + "name": "myfile", + "provider": `provider["/local/"]`, + "type": "local_file", + }, + }, + "check_results": nil, + }, }, }, }, + // If lineage changes, expect the serial to increment { name: "change lineage", mutationFunc: func(mgr *State) (*states.State, func()) { - originalLineage := mgr.lineage - mgr.lineage = "some-new-lineage" - return mgr.State(), func() { - mgr.lineage = originalLineage - } + mgr.lineage = "mock-lineage" + return mgr.State(), func() {} }, - expectedRequest: mockClientRequest{ - Method: "Put", - Content: map[string]interface{}{ - "version": 4.0, // encoding/json decodes this as float64 by default - "lineage": "some-new-lineage", - "serial": 2.0, // encoding/json decodes this as float64 by default - "terraform_version": version.Version, - "outputs": map[string]interface{}{}, - "resources": []interface{}{}, - "check_results": nil, + expectedRequests: []mockClientRequest{ + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "mock-lineage", + "serial": 2.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{}, + "resources": []interface{}{ + map[string]interface{}{ + "instances": []interface{}{ + map[string]interface{}{ + "attributes_flat": map[string]interface{}{ + "filename": "file.txt", + }, + "identity_schema_version": 0.0, + "schema_version": 0.0, + "sensitive_attributes": []interface{}{}, + }, + }, + "mode": "managed", + "name": "myfile", + "provider": `provider["/local/"]`, + "type": "local_file", + }, + }, + "check_results": nil, + }, }, }, }, + // removing resources should increment the serial + { + name: "remove resources", + mutationFunc: func(mgr *State) (*states.State, func()) { + mgr.state.RootModule().Resources = map[string]*states.Resource{} + return mgr.State(), func() {} + }, + expectedRequests: []mockClientRequest{ + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "mock-lineage", + "serial": 3.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{}, + "resources": []interface{}{}, + "check_results": nil, + }, + }, + }, + }, + // If the remote serial is incremented, then we increment it once more. { name: "change serial", mutationFunc: func(mgr *State) (*states.State, func()) { @@ -117,66 +216,80 @@ func TestStatePersist(t *testing.T) { mgr.serial = originalSerial } }, - expectedRequest: mockClientRequest{ - Method: "Put", - Content: map[string]interface{}{ - "version": 4.0, // encoding/json decodes this as float64 by default - "lineage": "mock-lineage", - "serial": 4.0, // encoding/json decodes this as float64 by default - "terraform_version": version.Version, - "outputs": map[string]interface{}{}, - "resources": []interface{}{}, - "check_results": nil, + expectedRequests: []mockClientRequest{ + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "mock-lineage", + "serial": 5.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{}, + "resources": []interface{}{}, + "check_results": nil, + }, }, }, }, + // Adding an output should cause the serial to increment as well. { name: "add output to state", mutationFunc: func(mgr *State) (*states.State, func()) { s := mgr.State() - s.RootModule().SetOutputValue("foo", cty.StringVal("bar"), false) + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) return s, func() {} }, - expectedRequest: mockClientRequest{ - Method: "Put", - Content: map[string]interface{}{ - "version": 4.0, // encoding/json decodes this as float64 by default - "lineage": "mock-lineage", - "serial": 3.0, // encoding/json decodes this as float64 by default - "terraform_version": version.Version, - "outputs": map[string]interface{}{ - "foo": map[string]interface{}{ - "type": "string", - "value": "bar", + expectedRequests: []mockClientRequest{ + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "mock-lineage", + "serial": 4.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + "value": "bar", + }, }, + "resources": []interface{}{}, + "check_results": nil, }, - "resources": []interface{}{}, - "check_results": nil, }, }, }, + // ...as should changing an output { name: "mutate state bar -> baz", mutationFunc: func(mgr *State) (*states.State, func()) { s := mgr.State() - s.RootModule().SetOutputValue("foo", cty.StringVal("baz"), false) + s.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("baz"), false, + ) return s, func() {} }, - expectedRequest: mockClientRequest{ - Method: "Put", - Content: map[string]interface{}{ - "version": 4.0, // encoding/json decodes this as float64 by default - "lineage": "mock-lineage", - "serial": 4.0, // encoding/json decodes this as float64 by default - "terraform_version": version.Version, - "outputs": map[string]interface{}{ - "foo": map[string]interface{}{ - "type": "string", - "value": "baz", + expectedRequests: []mockClientRequest{ + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "mock-lineage", + "serial": 5.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + "value": "baz", + }, }, + "resources": []interface{}{}, + "check_results": nil, }, - "resources": []interface{}{}, - "check_results": nil, }, }, }, @@ -188,27 +301,31 @@ func TestStatePersist(t *testing.T) { }, noRequest: true, }, + // If the remote state's serial is less (force push), then we + // increment it once from there. { name: "reset serial (force push style)", mutationFunc: func(mgr *State) (*states.State, func()) { mgr.serial = 2 return mgr.State(), func() {} }, - expectedRequest: mockClientRequest{ - Method: "Put", - Content: map[string]interface{}{ - "version": 4.0, // encoding/json decodes this as float64 by default - "lineage": "mock-lineage", - "serial": 3.0, // encoding/json decodes this as float64 by default - "terraform_version": version.Version, - "outputs": map[string]interface{}{ - "foo": map[string]interface{}{ - "type": "string", - "value": "baz", + expectedRequests: []mockClientRequest{ + { + Method: "Put", + Content: map[string]interface{}{ + "version": 4.0, // encoding/json decodes this as float64 by default + "lineage": "mock-lineage", + "serial": 3.0, // encoding/json decodes this as float64 by default + "terraform_version": version.Version, + "outputs": map[string]interface{}{ + "foo": map[string]interface{}{ + "type": "string", + "value": "baz", + }, }, + "resources": []interface{}{}, + "check_results": nil, }, - "resources": []interface{}{}, - "check_results": nil, }, }, }, @@ -218,18 +335,7 @@ func TestStatePersist(t *testing.T) { // test assertions below, or else we'd need to deal with // random lineage. mgr := &State{ - Client: &mockClient{ - current: []byte(` - { - "version": 4, - "lineage": "mock-lineage", - "serial": 1, - "terraform_version":"0.0.0", - "outputs": {}, - "resources": [] - } - `), - }, + Client: &mockClient{}, } // In normal use (during a Terraform operation) we always refresh and read @@ -257,7 +363,7 @@ func TestStatePersist(t *testing.T) { if err := mgr.WriteState(s); err != nil { t.Fatalf("failed to WriteState for %q: %s", tc.name, err) } - if err := mgr.PersistState(); err != nil { + if err := mgr.PersistState(nil); err != nil { t.Fatalf("failed to PersistState for %q: %s", tc.name, err) } @@ -267,10 +373,16 @@ func TestStatePersist(t *testing.T) { if logIdx >= len(mockClient.log) { t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) } - loggedRequest := mockClient.log[logIdx] - logIdx++ - if diff := cmp.Diff(tc.expectedRequest, loggedRequest); len(diff) > 0 { - t.Fatalf("incorrect client requests for %q:\n%s", tc.name, diff) + for expectedRequestIdx := 0; expectedRequestIdx < len(tc.expectedRequests); expectedRequestIdx++ { + loggedRequest := mockClient.log[logIdx] + logIdx++ + if diff := cmp.Diff(tc.expectedRequests[expectedRequestIdx], loggedRequest, cmpopts.IgnoreMapEntries(func(key string, value interface{}) bool { + // This is required since the initial state creation causes the lineage to be a UUID that is not known at test time. + return tc.name == "first state persistence" && key == "lineage" + })); len(diff) > 0 { + t.Logf("incorrect client requests for %q:\n%s", tc.name, diff) + t.Fail() + } } } cleanup() @@ -278,7 +390,7 @@ func TestStatePersist(t *testing.T) { } logCnt := len(mockClient.log) if logIdx != logCnt { - log.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) + t.Fatalf("not all requests were read. Expected logIdx to be %d but got %d", logCnt, logIdx) } } @@ -299,7 +411,7 @@ func TestState_GetRootOutputValues(t *testing.T) { }, } - outputs, err := mgr.GetRootOutputValues() + outputs, err := mgr.GetRootOutputValues(context.Background()) if err != nil { t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err) } @@ -454,7 +566,7 @@ func TestWriteStateForMigration(t *testing.T) { // At this point we should just do a normal write and persist // as would happen from the CLI mgr.WriteState(mgr.State()) - mgr.PersistState() + mgr.PersistState(nil) if logIdx >= len(mockClient.log) { t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) @@ -620,7 +732,7 @@ func TestWriteStateForMigrationWithForcePushClient(t *testing.T) { // At this point we should just do a normal write and persist // as would happen from the CLI mgr.WriteState(mgr.State()) - mgr.PersistState() + mgr.PersistState(nil) if logIdx >= len(mockClient.log) { t.Fatalf("request lock and index are out of sync on %q: idx=%d len=%d", tc.name, logIdx, len(mockClient.log)) diff --git a/internal/states/remote/testing.go b/internal/states/remote/testing.go index 197f87ac8c..2de180f559 100644 --- a/internal/states/remote/testing.go +++ b/internal/states/remote/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package remote import ( diff --git a/internal/states/resource.go b/internal/states/resource.go index 1c1f65bede..25eed13c5e 100644 --- a/internal/states/resource.go +++ b/internal/states/resource.go @@ -1,9 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( "fmt" - "math/rand" - "time" "github.com/hashicorp/terraform/internal/addrs" ) @@ -123,23 +124,14 @@ func (i *ResourceInstance) deposeCurrentObject(forceKey DeposedKey) DeposedKey { return key } -// GetGeneration retrieves the object of the given generation from the -// ResourceInstance, or returns nil if there is no such object. -// -// If the given generation is nil or invalid, this method will panic. -func (i *ResourceInstance) GetGeneration(gen Generation) *ResourceInstanceObjectSrc { - if gen == CurrentGen { +// Object retrieves the object with the given deposed key from the +// ResourceInstance, or returns nil if there is no such object. Use +// [addrs.NotDeposed] to retrieve the "current" object, if any. +func (i *ResourceInstance) Object(dk DeposedKey) *ResourceInstanceObjectSrc { + if dk == addrs.NotDeposed { return i.Current } - if dk, ok := gen.(DeposedKey); ok { - return i.Deposed[dk] - } - if gen == nil { - panic("get with nil Generation") - } - // Should never fall out here, since the above covers all possible - // Generation values. - panic(fmt.Sprintf("get invalid Generation %#v", gen)) + return i.Deposed[dk] } // FindUnusedDeposedKey generates a unique DeposedKey that is guaranteed not to @@ -167,49 +159,21 @@ func (i *ResourceInstance) findUnusedDeposedKey() DeposedKey { } } -// DeposedKey is a 8-character hex string used to uniquely identify deposed -// instance objects in the state. -type DeposedKey string +// DeposedKey is an alias for [addrs.DeposedKey], representing keys assigned +// to deposed resource instance objects. +type DeposedKey = addrs.DeposedKey -// NotDeposed is a special invalid value of DeposedKey that is used to represent -// the absense of a deposed key. It must not be used as an actual deposed key. -const NotDeposed = DeposedKey("") +// NotDeposed is an alias for the zero value of [addrs.DeposedKey], which +// represents the absense of a deposed key, i.e. that the associated object +// is the "current" object for some resource instance. +const NotDeposed = addrs.NotDeposed -var deposedKeyRand = rand.New(rand.NewSource(time.Now().UnixNano())) - -// NewDeposedKey generates a pseudo-random deposed key. Because of the short -// length of these keys, uniqueness is not a natural consequence and so the -// caller should test to see if the generated key is already in use and generate -// another if so, until a unique key is found. +// NewDeposedKey is an alias for [addrs.NewDeposedKey]. func NewDeposedKey() DeposedKey { - v := deposedKeyRand.Uint32() - return DeposedKey(fmt.Sprintf("%08x", v)) + return addrs.NewDeposedKey() } -func (k DeposedKey) String() string { - return string(k) +// ParseDeposedKey is an alias for [addrs.ParseDeposedKey]. +func ParseDeposedKey(raw string) (DeposedKey, error) { + return addrs.ParseDeposedKey(raw) } - -func (k DeposedKey) GoString() string { - ks := string(k) - switch { - case ks == "": - return "states.NotDeposed" - default: - return fmt.Sprintf("states.DeposedKey(%s)", ks) - } -} - -// Generation is a helper method to convert a DeposedKey into a Generation. -// If the reciever is anything other than NotDeposed then the result is -// just the same value as a Generation. If the receiver is NotDeposed then -// the result is CurrentGen. -func (k DeposedKey) Generation() Generation { - if k == NotDeposed { - return CurrentGen - } - return k -} - -// generation is an implementation of Generation. -func (k DeposedKey) generation() {} diff --git a/internal/states/resource_test.go b/internal/states/resource_test.go index ffe81ee7b6..53447e3d40 100644 --- a/internal/states/resource_test.go +++ b/internal/states/resource_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( diff --git a/internal/states/state.go b/internal/states/state.go index 9992f93caf..48a1d12af5 100644 --- a/internal/states/state.go +++ b/internal/states/state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( @@ -7,7 +10,7 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/getproviders" + "github.com/hashicorp/terraform/internal/getproviders/providerreqs" ) // State is the top-level type of a Terraform state. @@ -26,6 +29,14 @@ type State struct { // an implementation detail and must not be used by outside callers. Modules map[string]*Module + // OutputValues contains the state for each output value defined in the + // root module. + // + // Output values in other modules don't persist anywhere between runs, + // so Terraform Core tracks those only internally and does not expose + // them in any artifacts that survive between runs. + RootOutputValues map[string]*OutputValue + // CheckResults contains a snapshot of the statuses of checks at the // end of the most recent update to the state. Callers might compare // checks between runs to see if e.g. a previously-failing check has @@ -45,7 +56,8 @@ func NewState() *State { modules := map[string]*Module{} modules[addrs.RootModuleInstance.String()] = NewModule(addrs.RootModuleInstance) return &State{ - Modules: modules, + Modules: modules, + RootOutputValues: make(map[string]*OutputValue), } } @@ -65,11 +77,11 @@ func (s *State) Empty() bool { if s == nil { return true } + if len(s.RootOutputValues) != 0 { + return false + } for _, ms := range s.Modules { - if len(ms.Resources) != 0 { - return false - } - if len(ms.OutputValues) != 0 { + if !ms.empty() { return false } } @@ -96,35 +108,6 @@ func (s *State) ModuleInstances(addr addrs.Module) []*Module { return ms } -// ModuleOutputs returns all outputs for the given module call under the -// parentAddr instance. -func (s *State) ModuleOutputs(parentAddr addrs.ModuleInstance, module addrs.ModuleCall) []*OutputValue { - var os []*OutputValue - for _, m := range s.Modules { - // can't get outputs from the root module - if m.Addr.IsRoot() { - continue - } - - parent, call := m.Addr.Call() - // make sure this is a descendent in the correct path - if !parentAddr.Equal(parent) { - continue - } - - // and check if this is the correct child - if call.Name != module.Name { - continue - } - - for _, o := range m.OutputValues { - os = append(os, o) - } - } - - return os -} - // RemoveModule removes the module with the given address from the state, // unless it is the root module. The root module cannot be deleted, and so // this method will panic if that is attempted. @@ -190,6 +173,14 @@ func (s *State) HasManagedResourceInstanceObjects() bool { return false } +// HasRootOutputValues returns true if there's at least one root output value in the receiving state. +func (s *State) HasRootOutputValues() bool { + if s == nil { + return false + } + return len(s.RootOutputValues) > 0 +} + // Resource returns the state for the resource with the given address, or nil // if no such resource is tracked in the state. func (s *State) Resource(addr addrs.AbsResource) *Resource { @@ -212,6 +203,18 @@ func (s *State) Resources(addr addrs.ConfigResource) []*Resource { return ret } +// AllResourceInstanceObjectAddrs returns a set of addresses for all of +// the leaf resource instance objects of any mode that are tracked in this +// state. +// +// If you only care about objects belonging to managed resources, use +// [State.AllManagedResourceInstanceObjectAddrs] instead. +func (s *State) AllResourceInstanceObjectAddrs() addrs.Set[addrs.AbsResourceInstanceObject] { + return s.allResourceInstanceObjectAddrs(func(addr addrs.AbsResourceInstanceObject) bool { + return true // we filter nothing + }) +} + // AllManagedResourceInstanceObjectAddrs returns a set of addresses for all of // the leaf resource instance objects associated with managed resources that // are tracked in this state. @@ -221,62 +224,44 @@ func (s *State) Resources(addr addrs.ConfigResource) []*Resource { // by deleting a workspace. This function is intended only for reporting // context in error messages, such as when we reject deleting a "non-empty" // workspace as detected by s.HasManagedResourceInstanceObjects. -// -// The ordering of the result is meaningless but consistent. DeposedKey will -// be NotDeposed (the zero value of DeposedKey) for any "current" objects. -// This method is guaranteed to return at least one item if -// s.HasManagedResourceInstanceObjects returns true for the same state, and -// to return a zero-length slice if it returns false. -func (s *State) AllResourceInstanceObjectAddrs() []struct { - Instance addrs.AbsResourceInstance - DeposedKey DeposedKey -} { +func (s *State) AllManagedResourceInstanceObjectAddrs() addrs.Set[addrs.AbsResourceInstanceObject] { + return s.allResourceInstanceObjectAddrs(func(addr addrs.AbsResourceInstanceObject) bool { + return addr.ResourceInstance.Resource.Resource.Mode == addrs.ManagedResourceMode + }) +} + +func (s *State) allResourceInstanceObjectAddrs(keepAddr func(addr addrs.AbsResourceInstanceObject) bool) addrs.Set[addrs.AbsResourceInstanceObject] { if s == nil { return nil } - // We use an unnamed return type here just because we currently have no - // general need to return pairs of instance address and deposed key aside - // from this method, and this method itself is only of marginal value - // when producing some error messages. - // - // If that need ends up arising more in future then it might make sense to - // name this as addrs.AbsResourceInstanceObject, although that would require - // moving DeposedKey into the addrs package too. - type ResourceInstanceObject = struct { - Instance addrs.AbsResourceInstance - DeposedKey DeposedKey - } - var ret []ResourceInstanceObject - + ret := addrs.MakeSet[addrs.AbsResourceInstanceObject]() for _, ms := range s.Modules { for _, rs := range ms.Resources { - if rs.Addr.Resource.Mode != addrs.ManagedResourceMode { - continue - } - for instKey, is := range rs.Instances { instAddr := rs.Addr.Instance(instKey) if is.Current != nil { - ret = append(ret, ResourceInstanceObject{instAddr, NotDeposed}) + objAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: instAddr, + DeposedKey: addrs.NotDeposed, + } + if keepAddr(objAddr) { + ret.Add(objAddr) + } } for deposedKey := range is.Deposed { - ret = append(ret, ResourceInstanceObject{instAddr, deposedKey}) + objAddr := addrs.AbsResourceInstanceObject{ + ResourceInstance: instAddr, + DeposedKey: deposedKey, + } + if keepAddr(objAddr) { + ret.Add(objAddr) + } } } } } - sort.SliceStable(ret, func(i, j int) bool { - objI, objJ := ret[i], ret[j] - switch { - case !objI.Instance.Equal(objJ.Instance): - return objI.Instance.Less(objJ.Instance) - default: - return objI.DeposedKey < objJ.DeposedKey - } - }) - return ret } @@ -293,24 +278,57 @@ func (s *State) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceInstan return ms.ResourceInstance(addr.Resource) } -// OutputValue returns the state for the output value with the given address, -// or nil if no such output value is tracked in the state. -func (s *State) OutputValue(addr addrs.AbsOutputValue) *OutputValue { - ms := s.Module(addr.Module) - if ms == nil { +// ResourceInstance returns the (encoded) state for the resource instance object +// with the given address, or nil if no such object is tracked in the state. +func (s *State) ResourceInstanceObjectSrc(addr addrs.AbsResourceInstanceObject) *ResourceInstanceObjectSrc { + if s == nil { + panic("State.ResourceInstanceObjectSrc on nil *State") + } + rs := s.ResourceInstance(addr.ResourceInstance) + if rs == nil { return nil } - return ms.OutputValues[addr.OutputValue.Name] + if addr.DeposedKey != addrs.NotDeposed { + return rs.Deposed[addr.DeposedKey] + } + return rs.Current } -// LocalValue returns the value of the named local value with the given address, -// or cty.NilVal if no such value is tracked in the state. -func (s *State) LocalValue(addr addrs.AbsLocalValue) cty.Value { - ms := s.Module(addr.Module) - if ms == nil { - return cty.NilVal +// OutputValue returns the state for the output value with the given address, +// or nil if no such output value is tracked in the state. +// +// Only root module output values are tracked in the state, so this always +// returns nil for output values in any other module. +func (s *State) OutputValue(addr addrs.AbsOutputValue) *OutputValue { + if !addr.Module.IsRoot() { + return nil } - return ms.LocalValues[addr.LocalValue.Name] + return s.RootOutputValues[addr.OutputValue.Name] +} + +// SetOutputValue updates the value stored for the given output value if and +// only if it's a root module output value. +// +// All other output values will just be silently ignored, because we don't +// store those here anymore. (They live in a namedvals.State object hidden +// in the internals of Terraform Core.) +func (s *State) SetOutputValue(addr addrs.AbsOutputValue, value cty.Value, sensitive bool) { + if !addr.Module.IsRoot() { + return + } + s.RootOutputValues[addr.OutputValue.Name] = &OutputValue{ + Addr: addr, + Value: value, + Sensitive: sensitive, + } +} + +// RemoveOutputValue removes the record of a previously-stored output value. +func (s *State) RemoveOutputValue(addr addrs.AbsOutputValue) { + if !addr.Module.IsRoot() { + return + } + delete(s.RootOutputValues, addr.OutputValue.Name) } // ProviderAddrs returns a list of all of the provider configuration addresses @@ -354,9 +372,9 @@ func (s *State) ProviderAddrs() []addrs.AbsProviderConfig { // the requirements returned by this method will always be unconstrained. // The result should usually be merged with a Requirements derived from the // current configuration in order to apply some constraints. -func (s *State) ProviderRequirements() getproviders.Requirements { +func (s *State) ProviderRequirements() providerreqs.Requirements { configAddrs := s.ProviderAddrs() - ret := make(getproviders.Requirements, len(configAddrs)) + ret := make(providerreqs.Requirements, len(configAddrs)) for _, configAddr := range configAddrs { ret[configAddr.Provider] = nil // unconstrained dependency } @@ -387,7 +405,8 @@ func (s *State) PruneResourceHusks() { // SyncWrapper returns a SyncState object wrapping the receiver. func (s *State) SyncWrapper() *SyncState { return &SyncState{ - state: s, + state: s, + writable: true, // initially writable, becoming read-only once closed } } @@ -553,13 +572,6 @@ func (s *State) MoveModuleInstance(src, dst addrs.ModuleInstance) { r.Addr.Module = dst } } - - // Update any OutputValues's addresses. - if srcMod.OutputValues != nil { - for _, ov := range srcMod.OutputValues { - ov.Addr.Module = dst - } - } } // MaybeMoveModuleInstance moves the given src ModuleInstance's current state to diff --git a/internal/states/state_deepcopy.go b/internal/states/state_deepcopy.go index b8498d53ff..1a0c102362 100644 --- a/internal/states/state_deepcopy.go +++ b/internal/states/state_deepcopy.go @@ -1,8 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( + "maps" + "slices" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/zclconf/go-cty/cty" ) // Taking deep copies of states is an important operation because state is @@ -28,9 +33,14 @@ func (s *State) DeepCopy() *State { for k, m := range s.Modules { modules[k] = m.DeepCopy() } + outputValues := make(map[string]*OutputValue, len(s.RootOutputValues)) + for k, v := range s.RootOutputValues { + outputValues[k] = v.DeepCopy() + } return &State{ - Modules: modules, - CheckResults: s.CheckResults.DeepCopy(), + Modules: modules, + RootOutputValues: outputValues, + CheckResults: s.CheckResults.DeepCopy(), } } @@ -51,21 +61,10 @@ func (ms *Module) DeepCopy() *Module { for k, r := range ms.Resources { resources[k] = r.DeepCopy() } - outputValues := make(map[string]*OutputValue, len(ms.OutputValues)) - for k, v := range ms.OutputValues { - outputValues[k] = v.DeepCopy() - } - localValues := make(map[string]cty.Value, len(ms.LocalValues)) - for k, v := range ms.LocalValues { - // cty.Value is immutable, so we don't need to copy these. - localValues[k] = v - } return &Module{ - Addr: ms.Addr, // technically mutable, but immutable by convention - Resources: resources, - OutputValues: outputValues, - LocalValues: localValues, + Addr: ms.Addr, // technically mutable, but immutable by convention + Resources: resources, } } @@ -131,49 +130,29 @@ func (os *ResourceInstanceObjectSrc) DeepCopy() *ResourceInstanceObjectSrc { return nil } - var attrsFlat map[string]string - if os.AttrsFlat != nil { - attrsFlat = make(map[string]string, len(os.AttrsFlat)) - for k, v := range os.AttrsFlat { - attrsFlat[k] = v - } - } - - var attrsJSON []byte - if os.AttrsJSON != nil { - attrsJSON = make([]byte, len(os.AttrsJSON)) - copy(attrsJSON, os.AttrsJSON) - } - - var attrPaths []cty.PathValueMarks - if os.AttrSensitivePaths != nil { - attrPaths = make([]cty.PathValueMarks, len(os.AttrSensitivePaths)) - copy(attrPaths, os.AttrSensitivePaths) - } - - var private []byte - if os.Private != nil { - private = make([]byte, len(os.Private)) - copy(private, os.Private) - } + attrsFlat := maps.Clone(os.AttrsFlat) + attrsJSON := slices.Clone(os.AttrsJSON) + identityJSON := slices.Clone(os.IdentityJSON) + sensitiveAttrPaths := slices.Clone(os.AttrSensitivePaths) + private := slices.Clone(os.Private) // Some addrs.Referencable implementations are technically mutable, but // we treat them as immutable by convention and so we don't deep-copy here. - var dependencies []addrs.ConfigResource - if os.Dependencies != nil { - dependencies = make([]addrs.ConfigResource, len(os.Dependencies)) - copy(dependencies, os.Dependencies) - } + dependencies := slices.Clone(os.Dependencies) return &ResourceInstanceObjectSrc{ - Status: os.Status, - SchemaVersion: os.SchemaVersion, - Private: private, - AttrsFlat: attrsFlat, - AttrsJSON: attrsJSON, - AttrSensitivePaths: attrPaths, - Dependencies: dependencies, - CreateBeforeDestroy: os.CreateBeforeDestroy, + Status: os.Status, + SchemaVersion: os.SchemaVersion, + Private: private, + AttrsFlat: attrsFlat, + AttrsJSON: attrsJSON, + AttrSensitivePaths: sensitiveAttrPaths, + Dependencies: dependencies, + CreateBeforeDestroy: os.CreateBeforeDestroy, + decodeValueCache: os.decodeValueCache, + IdentityJSON: identityJSON, + IdentitySchemaVersion: os.IdentitySchemaVersion, + decodeIdentityCache: os.decodeIdentityCache, } } @@ -190,22 +169,15 @@ func (o *ResourceInstanceObject) DeepCopy() *ResourceInstanceObject { return nil } - var private []byte - if o.Private != nil { - private = make([]byte, len(o.Private)) - copy(private, o.Private) - } + private := slices.Clone(o.Private) // Some addrs.Referenceable implementations are technically mutable, but // we treat them as immutable by convention and so we don't deep-copy here. - var dependencies []addrs.ConfigResource - if o.Dependencies != nil { - dependencies = make([]addrs.ConfigResource, len(o.Dependencies)) - copy(dependencies, o.Dependencies) - } + dependencies := slices.Clone(o.Dependencies) return &ResourceInstanceObject{ Value: o.Value, + Identity: o.Identity, Status: o.Status, Private: private, Dependencies: dependencies, diff --git a/internal/states/state_equal.go b/internal/states/state_equal.go index b37aba0627..f771a714b2 100644 --- a/internal/states/state_equal.go +++ b/internal/states/state_equal.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( @@ -20,7 +23,7 @@ func (s *State) Equal(other *State) bool { } // ManagedResourcesEqual returns true if all of the managed resources tracked -// in the reciever are functionally equivalent to the same tracked in the +// in the receiver are functionally equivalent to the same tracked in the // other given state. // // This is a more constrained version of Equal that disregards other @@ -66,3 +69,36 @@ func sameManagedResources(s1, s2 *State) bool { return true } + +// RootOutputValuesEqual returns true if the root output values tracked in the +// receiver are functionally equivalent to the same tracked in the other given +// state. +func (s *State) RootOutputValuesEqual(s2 *State) bool { + if s == nil && s2 == nil { + return true + } + if s == nil { + return !s2.HasRootOutputValues() + } + if s2 == nil { + return !s.HasRootOutputValues() + } + + return sameRootOutputValues(s, s2) +} + +// sameRootOutputValues returns true if the two states have the same root output values. +func sameRootOutputValues(s1, s2 *State) bool { + if len(s1.RootOutputValues) != len(s2.RootOutputValues) { + return false + } + + for k, v1 := range s1.RootOutputValues { + v2, ok := s2.RootOutputValues[k] + if !ok || !reflect.DeepEqual(v1, v2) { + return false + } + } + + return true +} diff --git a/internal/states/state_string.go b/internal/states/state_string.go index 2e34834a55..6e6796ea2d 100644 --- a/internal/states/state_string.go +++ b/internal/states/state_string.go @@ -38,7 +38,7 @@ func (s *State) String() string { var buf bytes.Buffer for _, name := range modules { m := s.Modules[name] - mStr := m.testString() + mStr := m.testString(s) // If we're the root module, we just write the output directly. if m.Addr.IsRoot() { @@ -76,7 +76,7 @@ func (s *State) String() string { // testString is used to produce part of the output of State.String. It should // never be used directly. -func (ms *Module) testString() string { +func (ms *Module) testString(state *State) string { var buf bytes.Buffer if len(ms.Resources) == 0 { @@ -204,17 +204,25 @@ func (ms *Module) testString() string { } } - if len(ms.OutputValues) > 0 { + // This is a bit weird because we used to store output values for all + // modules in the state, but now we use it only for the root output + // values since they are the only ones that persist between runs. + // + // To keep this long-suffering legacy string representation compatible + // (since so many of our older tests depend on it) we have this structured + // in as close as possible to the same way it was when OutputValues was + // a field of ms, instead of RootOutputValues in State. + if ms.Addr.IsRoot() && len(state.RootOutputValues) != 0 { buf.WriteString("\nOutputs:\n\n") - ks := make([]string, 0, len(ms.OutputValues)) - for k := range ms.OutputValues { + ks := make([]string, 0, len(state.RootOutputValues)) + for k := range state.RootOutputValues { ks = append(ks, k) } sort.Strings(ks) for _, k := range ks { - v := ms.OutputValues[k] + v := state.RootOutputValues[k] lv := hcl2shim.ConfigValueFromHCL2(v.Value) switch vTyped := lv.(type) { case string: diff --git a/internal/states/state_test.go b/internal/states/state_test.go index 768772aebe..5dc36e4b0b 100644 --- a/internal/states/state_test.go +++ b/internal/states/state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( @@ -6,10 +9,10 @@ import ( "testing" "github.com/go-test/deep" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/lang/marks" ) func TestState(t *testing.T) { @@ -24,9 +27,14 @@ func TestState(t *testing.T) { t.Errorf("root module is nil; want valid object") } - rootModule.SetLocalValue("foo", cty.StringVal("foo value")) - rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false) - rootModule.SetOutputValue("secret", cty.StringVal("secret value"), true) + state.SetOutputValue( + addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar value"), false, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "secret"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("secret value"), true, + ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, @@ -44,40 +52,43 @@ func TestState(t *testing.T) { }, ) + // State silently ignores attempts to write to non-root outputs, because + // historically we did track those here but these days we track them in + // namedvals.State instead, and we're being gracious to existing callers + // that might not know yet that they need to treat root module output + // values in a special way. childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) - childModule.SetOutputValue("pizza", cty.StringVal("hawaiian"), false) + state.SetOutputValue(addrs.OutputValue{Name: "pizza"}.Absolute(childModule.Addr), cty.StringVal("hawaiian"), false) multiModA := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a"))) - multiModA.SetOutputValue("pizza", cty.StringVal("cheese"), false) + state.SetOutputValue(addrs.OutputValue{Name: "pizza"}.Absolute(multiModA.Addr), cty.StringVal("cheese"), false) multiModB := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("b"))) - multiModB.SetOutputValue("pizza", cty.StringVal("sausage"), false) + state.SetOutputValue(addrs.OutputValue{Name: "pizza"}.Absolute(multiModB.Addr), cty.StringVal("sausage"), false) want := &State{ + RootOutputValues: map[string]*OutputValue{ + "bar": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "bar", + }, + }, + Value: cty.StringVal("bar value"), + Sensitive: false, + }, + "secret": { + Addr: addrs.AbsOutputValue{ + OutputValue: addrs.OutputValue{ + Name: "secret", + }, + }, + Value: cty.StringVal("secret value"), + Sensitive: true, + }, + }, + Modules: map[string]*Module{ "": { Addr: addrs.RootModuleInstance, - LocalValues: map[string]cty.Value{ - "foo": cty.StringVal("foo value"), - }, - OutputValues: map[string]*OutputValue{ - "bar": { - Addr: addrs.AbsOutputValue{ - OutputValue: addrs.OutputValue{ - Name: "bar", - }, - }, - Value: cty.StringVal("bar value"), - Sensitive: false, - }, - "secret": { - Addr: addrs.AbsOutputValue{ - OutputValue: addrs.OutputValue{ - Name: "secret", - }, - }, - Value: cty.StringVal("secret value"), - Sensitive: true, - }, - }, Resources: map[string]*Resource{ "test_thing.baz": { Addr: addrs.Resource{ @@ -104,54 +115,15 @@ func TestState(t *testing.T) { }, }, "module.child": { - Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey), - LocalValues: map[string]cty.Value{}, - OutputValues: map[string]*OutputValue{ - "pizza": { - Addr: addrs.AbsOutputValue{ - Module: addrs.RootModuleInstance.Child("child", addrs.NoKey), - OutputValue: addrs.OutputValue{ - Name: "pizza", - }, - }, - Value: cty.StringVal("hawaiian"), - Sensitive: false, - }, - }, + Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey), Resources: map[string]*Resource{}, }, `module.multi["a"]`: { - Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")), - LocalValues: map[string]cty.Value{}, - OutputValues: map[string]*OutputValue{ - "pizza": { - Addr: addrs.AbsOutputValue{ - Module: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")), - OutputValue: addrs.OutputValue{ - Name: "pizza", - }, - }, - Value: cty.StringVal("cheese"), - Sensitive: false, - }, - }, + Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")), Resources: map[string]*Resource{}, }, `module.multi["b"]`: { - Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")), - LocalValues: map[string]cty.Value{}, - OutputValues: map[string]*OutputValue{ - "pizza": { - Addr: addrs.AbsOutputValue{ - Module: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")), - OutputValue: addrs.OutputValue{ - Name: "pizza", - }, - }, - Value: cty.StringVal("sausage"), - Sensitive: false, - }, - }, + Addr: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")), Resources: map[string]*Resource{}, }, }, @@ -170,27 +142,8 @@ func TestState(t *testing.T) { }() } - for _, problem := range deep.Equal(state, want) { - t.Error(problem) - } - - expectedOutputs := map[string]string{ - `module.multi["a"].output.pizza`: "cheese", - `module.multi["b"].output.pizza`: "sausage", - } - - for _, o := range state.ModuleOutputs(addrs.RootModuleInstance, addrs.ModuleCall{Name: "multi"}) { - addr := o.Addr.String() - expected := expectedOutputs[addr] - delete(expectedOutputs, addr) - - if expected != o.Value.AsString() { - t.Fatalf("expected %q:%q, got %q", addr, expected, o.Value.AsString()) - } - } - - for addr, o := range expectedOutputs { - t.Fatalf("missing output %q:%q", addr, o) + if diff := cmp.Diff(want.String(), state.String()); diff != "" { + t.Errorf("wrong result\n%s", diff) } } @@ -225,12 +178,17 @@ func TestStateDeepCopy(t *testing.T) { rootModule := state.RootModule() if rootModule == nil { - t.Errorf("root module is nil; want valid object") + t.Fatalf("root module is nil; want valid object") } - rootModule.SetLocalValue("foo", cty.StringVal("foo value")) - rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false) - rootModule.SetOutputValue("secret", cty.StringVal("secret value"), true) + state.SetOutputValue( + addrs.OutputValue{Name: "bar"}.Absolute(rootModule.Addr), + cty.StringVal("bar value"), false, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "secret"}.Absolute(rootModule.Addr), + cty.StringVal("secret value"), true, + ) rootModule.SetResourceInstanceCurrent( addrs.Resource{ Mode: addrs.ManagedResourceMode, @@ -261,11 +219,8 @@ func TestStateDeepCopy(t *testing.T) { SchemaVersion: 1, AttrsJSON: []byte(`{"woozles":"confuzles"}`), // Sensitive path at "woozles" - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.Path{cty.GetAttrStep{Name: "woozles"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("woozles"), }, Private: []byte("private data"), Dependencies: []addrs.ConfigResource{ @@ -285,8 +240,7 @@ func TestStateDeepCopy(t *testing.T) { }, ) - childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) - childModule.SetOutputValue("pizza", cty.StringVal("hawaiian"), false) + state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) stateCopy := state.DeepCopy() if !state.Equal(stateCopy) { @@ -413,6 +367,70 @@ func TestStateHasResourceInstanceObjects(t *testing.T) { } +func TestStateHasRootOutputValues(t *testing.T) { + tests := map[string]struct { + Setup func(ss *SyncState) + Want bool + }{ + "empty": { + func(ss *SyncState) {}, + false, + }, + "one output value": { + func(ss *SyncState) { + ss.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar value"), false, + ) + }, + true, + }, + "one secret output value": { + func(ss *SyncState) { + ss.SetOutputValue( + addrs.OutputValue{Name: "secret"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("secret value"), true, + ) + }, + true, + }, + + // The output value tests below are in other modules and do not persist between runs. + // Terraform Core tracks them internally and does not expose them in any + // artifacts that survive between executions. + "one output value in child module": { + func(ss *SyncState) { + ss.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), + cty.StringVal("bar value"), false, + ) + }, + false, + }, + "one output value in multi module": { + func(ss *SyncState) { + ss.state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a"))) + ss.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a"))), + cty.StringVal("bar"), false, + ) + }, + false, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + state := BuildState(test.Setup) + got := state.HasRootOutputValues() + if got != test.Want { + t.Errorf("wrong result\nstate content: (using legacy state string format; might not be comprehensive)\n%s\n\ngot: %t\nwant: %t", state, got, test.Want) + } + }) + } + +} + func TestState_MoveAbsResource(t *testing.T) { // Set up a starter state for the embedded tests, which should start from a copy of this state. state := NewState() diff --git a/internal/states/statefile/diagnostics.go b/internal/states/statefile/diagnostics.go index b45b05ee0b..7374312f25 100644 --- a/internal/states/statefile/diagnostics.go +++ b/internal/states/statefile/diagnostics.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statefile/doc.go b/internal/states/statefile/doc.go index 625d0cf429..94662c7e2d 100644 --- a/internal/states/statefile/doc.go +++ b/internal/states/statefile/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package statefile deals with the file format used to serialize states for // persistent storage and then deserialize them into memory again later. package statefile diff --git a/internal/states/statefile/file.go b/internal/states/statefile/file.go index 631807b113..c6b52e9555 100644 --- a/internal/states/statefile/file.go +++ b/internal/states/statefile/file.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statefile/marshal_equal.go b/internal/states/statefile/marshal_equal.go index 2b386cbb73..5184df6e35 100644 --- a/internal/states/statefile/marshal_equal.go +++ b/internal/states/statefile/marshal_equal.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( @@ -11,7 +14,7 @@ import ( // // This function compares only the portions of the state that are persisted // in state files, so for example it will not return false if the only -// differences between the two states are local values or descendent module +// differences between the two states are local values or descendant module // outputs. func StatesMarshalEqual(a, b *states.State) bool { var aBuf bytes.Buffer diff --git a/internal/states/statefile/read.go b/internal/states/statefile/read.go index 61f8e87d6c..be1439e60c 100644 --- a/internal/states/statefile/read.go +++ b/internal/states/statefile/read.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( @@ -17,6 +20,25 @@ import ( // ErrNoState is returned by ReadState when the state file is empty. var ErrNoState = errors.New("no state") +// ErrUnusableState is an error wrapper to indicate that we *think* the input +// represents state data, but can't use it for some reason (as explained in the +// error text). Callers can check against this type with errors.As() if they +// need to distinguish between corrupt state and more fundamental problems like +// an empty file. +type ErrUnusableState struct { + inner error +} + +func errUnusable(err error) *ErrUnusableState { + return &ErrUnusableState{inner: err} +} +func (e *ErrUnusableState) Error() string { + return e.inner.Error() +} +func (e *ErrUnusableState) Unwrap() error { + return e.inner +} + // Read reads a state from the given reader. // // Legacy state format versions 1 through 3 are supported, but the result will @@ -52,9 +74,9 @@ func Read(r io.Reader) (*File, error) { return nil, ErrNoState } - state, diags := readState(src) - if diags.HasErrors() { - return nil, diags.Err() + state, err := readState(src) + if err != nil { + return nil, err } if state == nil { @@ -65,7 +87,7 @@ func Read(r io.Reader) (*File, error) { return state, diags.Err() } -func readState(src []byte) (*File, tfdiags.Diagnostics) { +func readState(src []byte) (*File, error) { var diags tfdiags.Diagnostics if looksLikeVersion0(src) { @@ -74,15 +96,20 @@ func readState(src []byte) (*File, tfdiags.Diagnostics) { unsupportedFormat, "The state is stored in a legacy binary format that is not supported since Terraform v0.7. To continue, first upgrade the state using Terraform 0.6.16 or earlier.", )) - return nil, diags + return nil, errUnusable(diags.Err()) } version, versionDiags := sniffJSONStateVersion(src) diags = diags.Append(versionDiags) if versionDiags.HasErrors() { - return nil, diags + // This is the last point where there's a really good chance it's not a + // state file at all. Past here, we'll assume errors mean it's state but + // we can't use it. + return nil, diags.Err() } + var result *File + var err error switch version { case 0: diags = diags.Append(tfdiags.Sourceless( @@ -90,15 +117,14 @@ func readState(src []byte) (*File, tfdiags.Diagnostics) { unsupportedFormat, "The state file uses JSON syntax but has a version number of zero. There was never a JSON-based state format zero, so this state file is invalid and cannot be processed.", )) - return nil, diags case 1: - return readStateV1(src) + result, diags = readStateV1(src) case 2: - return readStateV2(src) + result, diags = readStateV2(src) case 3: - return readStateV3(src) + result, diags = readStateV3(src) case 4: - return readStateV4(src) + result, diags = readStateV4(src) default: thisVersion := tfversion.SemVer.String() creatingVersion := sniffJSONStateTerraformVersion(src) @@ -116,8 +142,13 @@ func readState(src []byte) (*File, tfdiags.Diagnostics) { fmt.Sprintf("The state file uses format version %d, which is not supported by Terraform %s. This state file may have been created by a newer version of Terraform.", version, thisVersion), )) } - return nil, diags } + + if diags.HasErrors() { + err = errUnusable(diags.Err()) + } + + return result, err } func sniffJSONStateVersion(src []byte) (uint64, tfdiags.Diagnostics) { diff --git a/internal/states/statefile/roundtrip_test.go b/internal/states/statefile/roundtrip_test.go index 693a9be968..5464b0bc70 100644 --- a/internal/states/statefile/roundtrip_test.go +++ b/internal/states/statefile/roundtrip_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statefile/upgrade_helper.go b/internal/states/statefile/upgrade_helper.go new file mode 100644 index 0000000000..f1ca839bb0 --- /dev/null +++ b/internal/states/statefile/upgrade_helper.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package statefile + +import ( + "maps" +) + +// shallowCopySlice produces a new slice with the same length as s that +// contains shallow copies of the elements of s. +func shallowCopySlice[S ~[]E, E any](s S) S { + if s == nil { + return nil + } + ret := make(S, len(s)) + copy(ret, s) + return ret +} + +// shallowCopySlice produces a new map with the same keys as m that all +// map to shallow copies of the corresponding elements in m. +func shallowCopyMap[M ~map[K]V, K comparable, V any](m M) M { + if m == nil { + return nil + } + ret := make(M, len(m)) + maps.Copy(ret, m) + return ret +} diff --git a/internal/states/statefile/version0.go b/internal/states/statefile/version0.go index 9b533317bd..697c689045 100644 --- a/internal/states/statefile/version0.go +++ b/internal/states/statefile/version0.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile // looksLikeVersion0 sniffs for the signature indicating a version 0 state diff --git a/internal/states/statefile/version1.go b/internal/states/statefile/version1.go index 0b82a13e22..3347cf9afa 100644 --- a/internal/states/statefile/version1.go +++ b/internal/states/statefile/version1.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statefile/version1_upgrade.go b/internal/states/statefile/version1_upgrade.go index 0b417e1c40..19237dd046 100644 --- a/internal/states/statefile/version1_upgrade.go +++ b/internal/states/statefile/version1_upgrade.go @@ -1,10 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( "fmt" "log" - - "github.com/mitchellh/copystructure" ) // upgradeStateV1ToV2 is used to upgrade a V1 state representation @@ -47,14 +48,9 @@ func (old *remoteStateV1) upgradeToV2() (*remoteStateV2, error) { return nil, nil } - config, err := copystructure.Copy(old.Config) - if err != nil { - return nil, fmt.Errorf("Error upgrading RemoteState V1: %v", err) - } - return &remoteStateV2{ Type: old.Type, - Config: config.(map[string]string), + Config: shallowCopyMap(old.Config), }, nil } @@ -63,14 +59,7 @@ func (old *moduleStateV1) upgradeToV2() (*moduleStateV2, error) { return nil, nil } - pathRaw, err := copystructure.Copy(old.Path) - if err != nil { - return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) - } - path, ok := pathRaw.([]string) - if !ok { - return nil, fmt.Errorf("Error upgrading ModuleState V1: path is not a list of strings") - } + path := shallowCopySlice(old.Path) if len(path) == 0 { // We found some V1 states with a nil path. Assume root. path = []string{"root"} @@ -95,16 +84,11 @@ func (old *moduleStateV1) upgradeToV2() (*moduleStateV2, error) { resources[key] = upgraded } - dependencies, err := copystructure.Copy(old.Dependencies) - if err != nil { - return nil, fmt.Errorf("Error upgrading ModuleState V1: %v", err) - } - return &moduleStateV2{ Path: path, Outputs: outputs, Resources: resources, - Dependencies: dependencies.([]string), + Dependencies: shallowCopySlice(old.Dependencies), }, nil } @@ -113,11 +97,6 @@ func (old *resourceStateV1) upgradeToV2() (*resourceStateV2, error) { return nil, nil } - dependencies, err := copystructure.Copy(old.Dependencies) - if err != nil { - return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) - } - primary, err := old.Primary.upgradeToV2() if err != nil { return nil, fmt.Errorf("Error upgrading ResourceState V1: %v", err) @@ -137,7 +116,7 @@ func (old *resourceStateV1) upgradeToV2() (*resourceStateV2, error) { return &resourceStateV2{ Type: old.Type, - Dependencies: dependencies.([]string), + Dependencies: shallowCopySlice(old.Dependencies), Primary: primary, Deposed: deposed, Provider: old.Provider, @@ -149,24 +128,19 @@ func (old *instanceStateV1) upgradeToV2() (*instanceStateV2, error) { return nil, nil } - attributes, err := copystructure.Copy(old.Attributes) - if err != nil { - return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) - } - - meta, err := copystructure.Copy(old.Meta) - if err != nil { - return nil, fmt.Errorf("Error upgrading InstanceState V1: %v", err) - } - - newMeta := make(map[string]interface{}) - for k, v := range meta.(map[string]string) { - newMeta[k] = v + // "Meta" changed from map[string]string to map[string]interface{}, + // so we'll need to wrap all of the prior strings as interface values. + var newMeta map[string]interface{} + if old.Meta != nil { + newMeta = make(map[string]interface{}, len(old.Meta)) + for k, v := range old.Meta { + newMeta[k] = v + } } return &instanceStateV2{ ID: old.ID, - Attributes: attributes.(map[string]string), + Attributes: shallowCopyMap(old.Attributes), Meta: newMeta, }, nil } diff --git a/internal/states/statefile/version2.go b/internal/states/statefile/version2.go index 2c5908c37c..38ce72236d 100644 --- a/internal/states/statefile/version2.go +++ b/internal/states/statefile/version2.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statefile/version2_upgrade.go b/internal/states/statefile/version2_upgrade.go index 2d03c07c9d..0677754c47 100644 --- a/internal/states/statefile/version2_upgrade.go +++ b/internal/states/statefile/version2_upgrade.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( @@ -8,7 +11,7 @@ import ( "strconv" "strings" - "github.com/mitchellh/copystructure" + "github.com/hashicorp/terraform/internal/copy" ) func upgradeStateV2ToV3(old *stateV2) (*stateV3, error) { @@ -18,13 +21,17 @@ func upgradeStateV2ToV3(old *stateV2) (*stateV3, error) { var new *stateV3 { - copy, err := copystructure.Config{Lock: true}.Copy(old) - if err != nil { - panic(err) - } - newWrongType := copy.(*stateV2) - newRightType := (stateV3)(*newWrongType) - new = &newRightType + // Trickery: the V2 and V3 types happen to match enough that we can + // let the language do the initial work to convert between them. + newCopy := (stateV3)(*old) + // However, we don't want our subsequent work to modify the caller's + // original object, so we'll deep-copy it. This copying utility is + // only really intended for use in tests, but this old data structure + // is simple enough that it does the job here, saving us from having + // to hand-write a bunch of copy logic for a long-forgotten, obsolete + // state snapshot format that is unlikely to exist in the wild anywhere + // anyway. + new = copy.DeepCopyValue(&newCopy) } // Set the new version number diff --git a/internal/states/statefile/version3.go b/internal/states/statefile/version3.go index 480cae8f4e..3ac8a92ec0 100644 --- a/internal/states/statefile/version3.go +++ b/internal/states/statefile/version3.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statefile/version3_upgrade.go b/internal/states/statefile/version3_upgrade.go index f46430af38..4f36058684 100644 --- a/internal/states/statefile/version3_upgrade.go +++ b/internal/states/statefile/version3_upgrade.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( @@ -10,6 +13,8 @@ import ( "github.com/zclconf/go-cty/cty" ctyjson "github.com/zclconf/go-cty/cty/json" + "maps" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/states" @@ -312,13 +317,7 @@ func upgradeInstanceObjectV3ToV4(rsOld *resourceStateV2, isOld *instanceStateV2, } } - var attributes map[string]string - if isOld.Attributes != nil { - attributes = make(map[string]string, len(isOld.Attributes)) - for k, v := range isOld.Attributes { - attributes[k] = v - } - } + attributes := maps.Clone(isOld.Attributes) if isOld.ID != "" { // As a special case, if we don't already have an "id" attribute and // yet there's a non-empty first-class ID on the old object then we'll diff --git a/internal/states/statefile/version4.go b/internal/states/statefile/version4.go index cb3dd694dd..aa389cbf41 100644 --- a/internal/states/statefile/version4.go +++ b/internal/states/statefile/version4.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( @@ -12,7 +15,6 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -134,8 +136,9 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { instAddr := rAddr.Instance(key) obj := &states.ResourceInstanceObjectSrc{ - SchemaVersion: isV4.SchemaVersion, - CreateBeforeDestroy: isV4.CreateBeforeDestroy, + SchemaVersion: isV4.SchemaVersion, + CreateBeforeDestroy: isV4.CreateBeforeDestroy, + IdentitySchemaVersion: isV4.IdentitySchemaVersion, } { @@ -154,6 +157,10 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { } } + if isV4.IdentityRaw != nil { + obj.IdentityJSON = isV4.IdentityRaw + } + // Sensitive paths if isV4.AttributeSensitivePaths != nil { paths, pathsDiags := unmarshalPaths([]byte(isV4.AttributeSensitivePaths)) @@ -161,15 +168,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { if pathsDiags.HasErrors() { continue } - - var pvm []cty.PathValueMarks - for _, path := range paths { - pvm = append(pvm, cty.PathValueMarks{ - Path: path, - Marks: cty.NewValueMarks(marks.Sensitive), - }) - } - obj.AttrSensitivePaths = pvm + obj.AttrSensitivePaths = paths } { @@ -254,10 +253,9 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { } // The root module is special in that we persist its attributes and thus - // need to reload them now. (For descendent modules we just re-calculate + // need to reload them now. (For descendant modules we just re-calculate // them based on the latest configuration on each run.) { - rootModule := state.RootModule() for name, fos := range sV4.RootOutputs { os := &states.OutputValue{ Addr: addrs.AbsOutputValue{ @@ -289,7 +287,7 @@ func prepareStateV4(sV4 *stateV4) (*File, tfdiags.Diagnostics) { } os.Value = val - rootModule.OutputValues[name] = os + state.RootOutputValues[name] = os } } @@ -335,7 +333,7 @@ func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics { Resources: []resourceStateV4{}, } - for name, os := range file.State.RootModule().OutputValues { + for name, os := range file.State.RootOutputValues { src, err := ctyjson.Marshal(os.Value, os.Value.Type()) if err != nil { diags = diags.Append(tfdiags.Sourceless( @@ -414,9 +412,7 @@ func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics { } } - if file.State.CheckResults != nil { - sV4.CheckResults = encodeCheckResultsV4(file.State.CheckResults) - } + sV4.CheckResults = encodeCheckResultsV4(file.State.CheckResults) sV4.normalize() @@ -488,14 +484,8 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc } } - // Extract paths from path value marks - var paths []cty.Path - for _, vm := range obj.AttrSensitivePaths { - paths = append(paths, vm.Path) - } - // Marshal paths to JSON - attributeSensitivePaths, pathsDiags := marshalPaths(paths) + attributeSensitivePaths, pathsDiags := marshalPaths(obj.AttrSensitivePaths) diags = diags.Append(pathsDiags) return append(isV4s, instanceObjectStateV4{ @@ -509,6 +499,8 @@ func appendInstanceObjectStateV4(rs *states.Resource, is *states.ResourceInstanc PrivateRaw: privateRaw, Dependencies: deps, CreateBeforeDestroy: obj.CreateBeforeDestroy, + IdentitySchemaVersion: obj.IdentitySchemaVersion, + IdentityRaw: obj.IdentityJSON, }), diags } @@ -524,7 +516,11 @@ func decodeCheckResultsV4(in []checkResultsV4) (*states.CheckResults, tfdiags.Di for _, aggrIn := range in { objectKind := decodeCheckableObjectKindV4(aggrIn.ObjectKind) if objectKind == addrs.CheckableKindInvalid { - diags = diags.Append(fmt.Errorf("unsupported checkable object kind %q", aggrIn.ObjectKind)) + // We cannot decode a future unknown check result kind, but + // for forwards compatibility we need not treat this as an + // error. Eliding unknown check results will not result in + // significant data loss and allows us to maintain state file + // interoperability in the 1.x series. continue } @@ -576,6 +572,11 @@ func decodeCheckResultsV4(in []checkResultsV4) (*states.CheckResults, tfdiags.Di } func encodeCheckResultsV4(in *states.CheckResults) []checkResultsV4 { + // normalize empty and nil sets in the serialized state + if in == nil || in.ConfigResults.Len() == 0 { + return nil + } + ret := make([]checkResultsV4, 0, in.ConfigResults.Len()) for _, configElem := range in.ConfigResults.Elems { @@ -635,6 +636,10 @@ func decodeCheckableObjectKindV4(in string) addrs.CheckableKind { return addrs.CheckableResource case "output": return addrs.CheckableOutputValue + case "check": + return addrs.CheckableCheck + case "var": + return addrs.CheckableInputVariable default: // We'll treat anything else as invalid just as a concession to // forward-compatible parsing, in case a later version of Terraform @@ -649,6 +654,10 @@ func encodeCheckableObjectKindV4(in addrs.CheckableKind) string { return "resource" case addrs.CheckableOutputValue: return "output" + case addrs.CheckableCheck: + return "check" + case addrs.CheckableInputVariable: + return "var" default: panic(fmt.Sprintf("unsupported checkable object kind %s", in)) } @@ -700,6 +709,9 @@ type instanceObjectStateV4 struct { AttributesFlat map[string]string `json:"attributes_flat,omitempty"` AttributeSensitivePaths json.RawMessage `json:"sensitive_attributes,omitempty"` + IdentitySchemaVersion uint64 `json:"identity_schema_version"` + IdentityRaw json.RawMessage `json:"identity,omitempty"` + PrivateRaw []byte `json:"private,omitempty"` Dependencies []string `json:"dependencies,omitempty"` @@ -808,6 +820,9 @@ func unmarshalPaths(buf []byte) ([]cty.Path, tfdiags.Diagnostics) { )) } + if len(jsonPaths) == 0 { + return nil, diags + } paths := make([]cty.Path, 0, len(jsonPaths)) unmarshalOuter: diff --git a/internal/states/statefile/version4_test.go b/internal/states/statefile/version4_test.go index d71d33734f..f39f999719 100644 --- a/internal/states/statefile/version4_test.go +++ b/internal/states/statefile/version4_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( @@ -5,8 +8,9 @@ import ( "strings" "testing" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/tfdiags" ) // This test verifies that modules are sorted before resources: diff --git a/internal/states/statefile/write.go b/internal/states/statefile/write.go index fe18fad5da..b9210f6f59 100644 --- a/internal/states/statefile/write.go +++ b/internal/states/statefile/write.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statefile import ( diff --git a/internal/states/statemgr/doc.go b/internal/states/statemgr/doc.go index 058b36d91d..7b8389e3b6 100644 --- a/internal/states/statemgr/doc.go +++ b/internal/states/statemgr/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package statemgr defines the interfaces and some supporting functionality // for "state managers", which are components responsible for writing state // to some persistent storage and then later retrieving it. diff --git a/internal/states/statemgr/filesystem.go b/internal/states/statemgr/filesystem.go index 1406f04656..7a7a63e3ca 100644 --- a/internal/states/statemgr/filesystem.go +++ b/internal/states/statemgr/filesystem.go @@ -1,8 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( "bytes" + "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -12,8 +17,7 @@ import ( "sync" "time" - multierror "github.com/hashicorp/go-multierror" - + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" ) @@ -223,7 +227,7 @@ func (s *Filesystem) writeState(state *states.State, meta *SnapshotMeta) error { // PersistState is an implementation of Persister that does nothing because // this type's Writer implementation does its own persistence. -func (s *Filesystem) PersistState() error { +func (s *Filesystem) PersistState(schemas *schemarepo.Schemas) error { return nil } @@ -233,7 +237,7 @@ func (s *Filesystem) RefreshState() error { return s.refreshState() } -func (s *Filesystem) GetRootOutputValues() (map[string]*states.OutputValue, error) { +func (s *Filesystem) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { err := s.RefreshState() if err != nil { return nil, err @@ -244,7 +248,7 @@ func (s *Filesystem) GetRootOutputValues() (map[string]*states.OutputValue, erro state = states.NewState() } - return state.RootModule().OutputValues, nil + return state.RootOutputValues, nil } func (s *Filesystem) refreshState() error { @@ -323,7 +327,7 @@ func (s *Filesystem) Lock(info *LockInfo) (string, error) { if err := s.lock(); err != nil { info, infoErr := s.lockInfo() if infoErr != nil { - err = multierror.Append(err, infoErr) + err = errors.Join(err, infoErr) } lockErr := &LockError{ @@ -350,7 +354,7 @@ func (s *Filesystem) Unlock(id string) error { idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID) info, err := s.lockInfo() if err != nil { - idErr = multierror.Append(idErr, err) + idErr = errors.Join(idErr, err) } return &LockError{ diff --git a/internal/states/statemgr/filesystem_lock_unix.go b/internal/states/statemgr/filesystem_lock_unix.go index baa991a6d3..5eed34c52e 100644 --- a/internal/states/statemgr/filesystem_lock_unix.go +++ b/internal/states/statemgr/filesystem_lock_unix.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !windows // +build !windows diff --git a/internal/states/statemgr/filesystem_lock_windows.go b/internal/states/statemgr/filesystem_lock_windows.go index e4f78b6772..53fd55d44d 100644 --- a/internal/states/statemgr/filesystem_lock_windows.go +++ b/internal/states/statemgr/filesystem_lock_windows.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build windows // +build windows diff --git a/internal/states/statemgr/filesystem_test.go b/internal/states/statemgr/filesystem_test.go index 2f6285fbd1..ea180275c5 100644 --- a/internal/states/statemgr/filesystem_test.go +++ b/internal/states/statemgr/filesystem_test.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( + "context" "io/ioutil" "os" "os/exec" @@ -414,7 +418,7 @@ func TestFilesystem_refreshWhileLocked(t *testing.T) { func TestFilesystem_GetRootOutputValues(t *testing.T) { fs := testFilesystem(t) - outputs, err := fs.GetRootOutputValues() + outputs, err := fs.GetRootOutputValues(context.Background()) if err != nil { t.Errorf("Expected GetRootOutputValues to not return an error, but it returned %v", err) } diff --git a/internal/states/statemgr/helper.go b/internal/states/statemgr/helper.go index a019b2c431..9da55924a0 100644 --- a/internal/states/statemgr/helper.go +++ b/internal/states/statemgr/helper.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr // The functions in this file are helper wrappers for common sequences of // operations done against full state managers. import ( + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/version" @@ -44,10 +48,10 @@ func RefreshAndRead(mgr Storage) (*states.State, error) { // out quickly with a user-facing error. In situations where more control // is required, call WriteState and PersistState on the state manager directly // and handle their errors. -func WriteAndPersist(mgr Storage, state *states.State) error { +func WriteAndPersist(mgr Storage, state *states.State, schemas *schemarepo.Schemas) error { err := mgr.WriteState(state) if err != nil { return err } - return mgr.PersistState() + return mgr.PersistState(schemas) } diff --git a/internal/states/statemgr/lineage.go b/internal/states/statemgr/lineage.go index b06b12c233..f5854fc65f 100644 --- a/internal/states/statemgr/lineage.go +++ b/internal/states/statemgr/lineage.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/statemgr/lock.go b/internal/states/statemgr/lock.go index 79c149fe73..9d34c20415 100644 --- a/internal/states/statemgr/lock.go +++ b/internal/states/statemgr/lock.go @@ -1,6 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr -import "github.com/hashicorp/terraform/internal/states" +import ( + "context" + + "github.com/hashicorp/terraform/internal/schemarepo" + "github.com/hashicorp/terraform/internal/states" +) // LockDisabled implements State and Locker but disables state locking. // If State doesn't support locking, this is a no-op. This is useful for @@ -15,8 +23,8 @@ func (s *LockDisabled) State() *states.State { return s.Inner.State() } -func (s *LockDisabled) GetRootOutputValues() (map[string]*states.OutputValue, error) { - return s.Inner.GetRootOutputValues() +func (s *LockDisabled) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { + return s.Inner.GetRootOutputValues(ctx) } func (s *LockDisabled) WriteState(v *states.State) error { @@ -27,8 +35,8 @@ func (s *LockDisabled) RefreshState() error { return s.Inner.RefreshState() } -func (s *LockDisabled) PersistState() error { - return s.Inner.PersistState() +func (s *LockDisabled) PersistState(schemas *schemarepo.Schemas) error { + return s.Inner.PersistState(schemas) } func (s *LockDisabled) Lock(info *LockInfo) (string, error) { diff --git a/internal/states/statemgr/lock_test.go b/internal/states/statemgr/lock_test.go index 326252b5c1..38747e2837 100644 --- a/internal/states/statemgr/lock_test.go +++ b/internal/states/statemgr/lock_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/statemgr/locker.go b/internal/states/statemgr/locker.go index 71617111ec..852d4da3d2 100644 --- a/internal/states/statemgr/locker.go +++ b/internal/states/statemgr/locker.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/statemgr/migrate.go b/internal/states/statemgr/migrate.go index 099e26a88e..4e688a8842 100644 --- a/internal/states/statemgr/migrate.go +++ b/internal/states/statemgr/migrate.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( @@ -129,7 +132,7 @@ func Export(mgr Reader) *statefile.File { // is the receiver of that method and the "second" is the given argument. type SnapshotMetaRel rune -//go:generate go run golang.org/x/tools/cmd/stringer -type=SnapshotMetaRel +//go:generate go tool golang.org/x/tools/cmd/stringer -type=SnapshotMetaRel const ( // SnapshotOlder indicates that two snapshots have a common lineage and diff --git a/internal/states/statemgr/migrate_test.go b/internal/states/statemgr/migrate_test.go index e9269a1770..bd6c3ff296 100644 --- a/internal/states/statemgr/migrate_test.go +++ b/internal/states/statemgr/migrate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/statemgr/persistent.go b/internal/states/statemgr/persistent.go index fde4a7f0f8..1d69bb26cf 100644 --- a/internal/states/statemgr/persistent.go +++ b/internal/states/statemgr/persistent.go @@ -1,8 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( + "context" + "time" + version "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" ) @@ -23,11 +30,11 @@ type Persistent interface { // OutputReader is the interface for managers that fetches output values from state // or another source. This is a refinement of fetching the entire state and digging -// the output values from it because enhanced backends can apply special permissions +// the output values from it because operations backends can apply special permissions // to differentiate reading the state and reading the outputs within the state. type OutputReader interface { // GetRootOutputValues fetches the root module output values from state or another source - GetRootOutputValues() (map[string]*states.OutputValue, error) + GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) } // Refresher is the interface for managers that can read snapshots from @@ -72,8 +79,12 @@ type Refresher interface { // is most commonly achieved by making use of atomic write capabilities on // the remote storage backend in conjunction with book-keeping with the // Serial and Lineage fields in the standard state file formats. +// +// Some implementations may optionally utilize config schema to persist +// state. For example, when representing state in an external JSON +// representation. type Persister interface { - PersistState() error + PersistState(*schemarepo.Schemas) error } // PersistentMeta is an optional extension to Persistent that allows inspecting @@ -114,3 +125,68 @@ type SnapshotMeta struct { // the snapshot. TerraformVersion *version.Version } + +// IntermediateStateConditionalPersister is an optional extension of +// [Persister] that allows an implementation to tailor the rules for +// whether to create intermediate state snapshots when Terraform Core emits +// events reporting that the state might have changed. This interface is used +// by the local backend when it's been configured to use another backend for +// state storage. +// +// For state managers that don't implement this interface, the local backend's +// StateHook uses a default set of rules that aim to be a good compromise +// between how long a state change can be active before it gets committed as a +// snapshot vs. how many intermediate snapshots will get created. That +// compromise is subject to change over time, but a state manager can implement +// this interface to exert full control over those rules. +type IntermediateStateConditionalPersister interface { + // ShouldPersistIntermediateState will be called each time Terraform Core + // emits an intermediate state event that is potentially eligible to be + // persisted. + // + // The implemention should return true to signal that the state snapshot + // most recently provided to the object's WriteState should be persisted, + // or false if it should not be persisted. If this function returns true + // then the receiver will see a subsequent call to + // [statemgr.Persister.PersistState] to request persistence. + // + // The implementation must not modify anything reachable through the + // arguments, and must not retain pointers to anything reachable through + // them after the function returns. However, implementers can assume that + // nothing will write to anything reachable through the arguments while + // this function is active. + ShouldPersistIntermediateState(info *IntermediateStatePersistInfo) bool +} + +type IntermediateStatePersistInfo struct { + // RequestedPersistInterval is the persist interval requested by whatever + // instantiated the StateHook. + // + // Implementations of [IntermediateStateConditionalPersister] should ideally + // respect this, but may ignore it if they use something other than the + // passage of time to make their decision. + RequestedPersistInterval time.Duration + + // LastPersist is the time when the last intermediate state snapshot was + // persisted, or the time of the first report for Terraform Core if there + // hasn't yet been a persisted snapshot. + LastPersist time.Time + + // ForcePersist is true when Terraform CLI has receieved an interrupt + // signal and is therefore trying to create snapshots more aggressively + // in anticipation of possibly being terminated ungracefully. + // [IntermediateStateConditionalPersister] implementations should ideally + // persist every snapshot they get when this flag is set, unless they have + // some external information that implies this shouldn't be necessary. + ForcePersist bool +} + +// DefaultIntermediateStatePersistRule is the default implementation of +// [IntermediateStateConditionalPersister.ShouldPersistIntermediateState] used +// when the selected state manager doesn't implement that interface. +// +// Implementers of that interface can optionally wrap a call to this function +// if they want to combine the default behavior with some logic of their own. +func DefaultIntermediateStatePersistRule(info *IntermediateStatePersistInfo) bool { + return info.ForcePersist || time.Since(info.LastPersist) >= info.RequestedPersistInterval +} diff --git a/internal/states/statemgr/plan.go b/internal/states/statemgr/plan.go index fb42df3129..19015c7dbe 100644 --- a/internal/states/statemgr/plan.go +++ b/internal/states/statemgr/plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/statemgr/statemgr.go b/internal/states/statemgr/statemgr.go index 8e9e12c937..9d1984e5c4 100644 --- a/internal/states/statemgr/statemgr.go +++ b/internal/states/statemgr/statemgr.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr // Storage is the union of Transient and Persistent, for state managers that diff --git a/internal/states/statemgr/statemgr_fake.go b/internal/states/statemgr/statemgr_fake.go index 8d88e4d24e..25800fdbbd 100644 --- a/internal/states/statemgr/statemgr_fake.go +++ b/internal/states/statemgr/statemgr_fake.go @@ -1,9 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( + "context" "errors" "sync" + "github.com/hashicorp/terraform/internal/schemarepo" "github.com/hashicorp/terraform/internal/states" ) @@ -61,12 +66,12 @@ func (m *fakeFull) RefreshState() error { return m.t.WriteState(m.fakeP.State()) } -func (m *fakeFull) PersistState() error { +func (m *fakeFull) PersistState(schemas *schemarepo.Schemas) error { return m.fakeP.WriteState(m.t.State()) } -func (m *fakeFull) GetRootOutputValues() (map[string]*states.OutputValue, error) { - return m.State().RootModule().OutputValues, nil +func (m *fakeFull) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { + return m.State().RootOutputValues, nil } func (m *fakeFull) Lock(info *LockInfo) (string, error) { @@ -115,7 +120,7 @@ func (m *fakeErrorFull) State() *states.State { return nil } -func (m *fakeErrorFull) GetRootOutputValues() (map[string]*states.OutputValue, error) { +func (m *fakeErrorFull) GetRootOutputValues(ctx context.Context) (map[string]*states.OutputValue, error) { return nil, errors.New("fake state manager error") } @@ -127,7 +132,7 @@ func (m *fakeErrorFull) RefreshState() error { return errors.New("fake state manager error") } -func (m *fakeErrorFull) PersistState() error { +func (m *fakeErrorFull) PersistState(schemas *schemarepo.Schemas) error { return errors.New("fake state manager error") } diff --git a/internal/states/statemgr/statemgr_test.go b/internal/states/statemgr/statemgr_test.go index e9e8226712..951a26dcad 100644 --- a/internal/states/statemgr/statemgr_test.go +++ b/internal/states/statemgr/statemgr_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/statemgr/testing.go b/internal/states/statemgr/testing.go index 171b21ad2e..1174cb2930 100644 --- a/internal/states/statemgr/testing.go +++ b/internal/states/statemgr/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( @@ -45,7 +48,10 @@ func TestFull(t *testing.T, s Full) { current := s.State() // Write a new state and verify that we have it - current.RootModule().SetOutputValue("bar", cty.StringVal("baz"), false) + current.SetOutputValue( + addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("baz"), false, + ) if err := s.WriteState(current); err != nil { t.Fatalf("err: %s", err) @@ -56,7 +62,7 @@ func TestFull(t *testing.T, s Full) { } // Test persistence - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("err: %s", err) } @@ -81,7 +87,7 @@ func TestFull(t *testing.T, s Full) { if err := s.WriteState(current); err != nil { t.Fatalf("err: %s", err) } - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("err: %s", err) } @@ -98,13 +104,15 @@ func TestFull(t *testing.T, s Full) { // Change the serial current = current.DeepCopy() - current.EnsureModule(addrs.RootModuleInstance).SetOutputValue( - "serialCheck", cty.StringVal("true"), false, + current.SetOutputValue( + addrs.OutputValue{Name: "serialCheck"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("true"), false, ) + if err := s.WriteState(current); err != nil { t.Fatalf("err: %s", err) } - if err := s.PersistState(); err != nil { + if err := s.PersistState(nil); err != nil { t.Fatalf("err: %s", err) } @@ -156,8 +164,14 @@ func TestFullInitialState() *states.State { } childMod.SetResourceProvider(rAddr, providerAddr) - state.RootModule().SetOutputValue("sensitive_output", cty.StringVal("it's a secret"), true) - state.RootModule().SetOutputValue("nonsensitive_output", cty.StringVal("hello, world!"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "sensitive_output"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("it's a secret"), true, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "nonsensitive_output"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("hello, world!"), false, + ) return state } diff --git a/internal/states/statemgr/transient.go b/internal/states/statemgr/transient.go index 0ac9b1deda..8912c6eced 100644 --- a/internal/states/statemgr/transient.go +++ b/internal/states/statemgr/transient.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import "github.com/hashicorp/terraform/internal/states" @@ -57,7 +60,7 @@ type Reader interface { // since the caller may continue to modify the given state object after // WriteState returns. type Writer interface { - // Write state saves a transient snapshot of the given state. + // WriteState saves a transient snapshot of the given state. // // The caller must ensure that the given state object is not concurrently // modified while a WriteState call is in progress. WriteState itself diff --git a/internal/states/statemgr/transient_inmem.go b/internal/states/statemgr/transient_inmem.go index 4692225cb5..5e5ac55965 100644 --- a/internal/states/statemgr/transient_inmem.go +++ b/internal/states/statemgr/transient_inmem.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package statemgr import ( diff --git a/internal/states/sync.go b/internal/states/sync.go index d48fa75518..da423876cb 100644 --- a/internal/states/sync.go +++ b/internal/states/sync.go @@ -1,12 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package states import ( "log" "sync" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" - "github.com/zclconf/go-cty/cty" ) // SyncState is a wrapper around State that provides concurrency-safe access to @@ -30,8 +34,9 @@ import ( // for ensuring correct operation sequencing, such as building and walking // a dependency graph. type SyncState struct { - state *State - lock sync.RWMutex + state *State + writable bool + lock sync.RWMutex } // Module returns a snapshot of the state of the module instance with the given @@ -49,16 +54,18 @@ func (s *SyncState) Module(addr addrs.ModuleInstance) *Module { return ret } -// ModuleOutputs returns the set of OutputValues that matches the given path. -func (s *SyncState) ModuleOutputs(parentAddr addrs.ModuleInstance, module addrs.ModuleCall) []*OutputValue { +// ModuleInstances returns the addresses of the module instances currently +// store within state for the given module. +func (s *SyncState) ModuleInstances(addr addrs.Module) []addrs.ModuleInstance { s.lock.RLock() - defer s.lock.RUnlock() - var os []*OutputValue + ret := s.state.ModuleInstances(addr) + s.lock.RUnlock() - for _, o := range s.state.ModuleOutputs(parentAddr, module) { - os = append(os, o.DeepCopy()) + insts := make([]addrs.ModuleInstance, len(ret)) + for i, inst := range ret { + insts[i] = inst.Addr } - return os + return insts } // RemoveModule removes the entire state for the given module, taking with @@ -66,8 +73,7 @@ func (s *SyncState) ModuleOutputs(parentAddr addrs.ModuleInstance, module addrs. // called only for modules whose resources have all been destroyed, but // that is not enforced by this method. func (s *SyncState) RemoveModule(addr addrs.ModuleInstance) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() s.state.RemoveModule(addr) } @@ -87,71 +93,29 @@ func (s *SyncState) OutputValue(addr addrs.AbsOutputValue) *OutputValue { // SetOutputValue writes a given output value into the state, overwriting // any existing value of the same name. // -// If the module containing the output is not yet tracked in state then it -// be added as a side-effect. +// The state only tracks output values for the root module, so attempts to +// write output values for any other module will be silently ignored. func (s *SyncState) SetOutputValue(addr addrs.AbsOutputValue, value cty.Value, sensitive bool) { - s.lock.Lock() - defer s.lock.Unlock() + if !addr.Module.IsRoot() { + return + } - ms := s.state.EnsureModule(addr.Module) - ms.SetOutputValue(addr.OutputValue.Name, value, sensitive) + defer s.beginWrite()() + s.state.SetOutputValue(addr, value, sensitive) } // RemoveOutputValue removes the stored value for the output value with the // given address. // -// If this results in its containing module being empty, the module will be -// pruned from the state as a side-effect. +// The state only tracks output values for the root module, so attempts to +// remove output values for any other module will be silently ignored. func (s *SyncState) RemoveOutputValue(addr addrs.AbsOutputValue) { - s.lock.Lock() - defer s.lock.Unlock() - - ms := s.state.Module(addr.Module) - if ms == nil { + if !addr.Module.IsRoot() { return } - ms.RemoveOutputValue(addr.OutputValue.Name) - s.maybePruneModule(addr.Module) -} -// LocalValue returns the current value associated with the given local value -// address. -func (s *SyncState) LocalValue(addr addrs.AbsLocalValue) cty.Value { - s.lock.RLock() - // cty.Value is immutable, so we don't need any extra copying here. - ret := s.state.LocalValue(addr) - s.lock.RUnlock() - return ret -} - -// SetLocalValue writes a given output value into the state, overwriting -// any existing value of the same name. -// -// If the module containing the local value is not yet tracked in state then it -// will be added as a side-effect. -func (s *SyncState) SetLocalValue(addr addrs.AbsLocalValue, value cty.Value) { - s.lock.Lock() - defer s.lock.Unlock() - - ms := s.state.EnsureModule(addr.Module) - ms.SetLocalValue(addr.LocalValue.Name, value) -} - -// RemoveLocalValue removes the stored value for the local value with the -// given address. -// -// If this results in its containing module being empty, the module will be -// pruned from the state as a side-effect. -func (s *SyncState) RemoveLocalValue(addr addrs.AbsLocalValue) { - s.lock.Lock() - defer s.lock.Unlock() - - ms := s.state.Module(addr.Module) - if ms == nil { - return - } - ms.RemoveLocalValue(addr.LocalValue.Name) - s.maybePruneModule(addr.Module) + defer s.beginWrite()() + s.state.RemoveOutputValue(addr) } // Resource returns a snapshot of the state of the resource with the given @@ -178,13 +142,14 @@ func (s *SyncState) ResourceInstance(addr addrs.AbsResourceInstance) *ResourceIn return ret } -// ResourceInstanceObject returns a snapshot of the current instance object -// of the given generation belonging to the instance with the given address, -// or nil if no such object is tracked.. +// ResourceInstanceObject returns a snapshot of the resource instance object +// of the given deposed key belonging to the given resource instance. Use +// [addrs.NotDeposed] for the deposed key to retrieve the "current" object, +// if any. Returns nil if there is no such object. // // The return value is a pointer to a copy of the object, which the caller may // then freely access and mutate. -func (s *SyncState) ResourceInstanceObject(addr addrs.AbsResourceInstance, gen Generation) *ResourceInstanceObjectSrc { +func (s *SyncState) ResourceInstanceObject(addr addrs.AbsResourceInstance, dk DeposedKey) *ResourceInstanceObjectSrc { s.lock.RLock() defer s.lock.RUnlock() @@ -192,15 +157,14 @@ func (s *SyncState) ResourceInstanceObject(addr addrs.AbsResourceInstance, gen G if inst == nil { return nil } - return inst.GetGeneration(gen).DeepCopy() + return inst.Object(dk).DeepCopy() } // SetResourceMeta updates the resource-level metadata for the resource at // the given address, creating the containing module state and resource state // as a side-effect if not already present. func (s *SyncState) SetResourceProvider(addr addrs.AbsResource, provider addrs.AbsProviderConfig) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.EnsureModule(addr.Module) ms.SetResourceProvider(addr.Resource, provider) @@ -212,8 +176,7 @@ func (s *SyncState) SetResourceProvider(addr addrs.AbsResource, provider addrs.A // but that is not enforced by this method. (Use RemoveResourceIfEmpty instead // to safely check first.) func (s *SyncState) RemoveResource(addr addrs.AbsResource) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.EnsureModule(addr.Module) ms.RemoveResource(addr.Resource) @@ -227,8 +190,7 @@ func (s *SyncState) RemoveResource(addr addrs.AbsResource) { // objects prevented its removal. Returns true also if the resource was // already absent, and thus no action needed to be taken. func (s *SyncState) RemoveResourceIfEmpty(addr addrs.AbsResource) bool { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.Module(addr.Module) if ms == nil { @@ -269,8 +231,7 @@ func (s *SyncState) RemoveResourceIfEmpty(addr addrs.AbsResource) bool { // If the containing module for this resource or the resource itself are not // already tracked in state then they will be added as a side-effect. func (s *SyncState) SetResourceInstanceCurrent(addr addrs.AbsResourceInstance, obj *ResourceInstanceObjectSrc, provider addrs.AbsProviderConfig) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.EnsureModule(addr.Module) ms.SetResourceInstanceCurrent(addr.Resource, obj.DeepCopy(), provider) @@ -301,8 +262,7 @@ func (s *SyncState) SetResourceInstanceCurrent(addr addrs.AbsResourceInstance, o // If the containing module for this resource or the resource itself are not // already tracked in state then they will be added as a side-effect. func (s *SyncState) SetResourceInstanceDeposed(addr addrs.AbsResourceInstance, key DeposedKey, obj *ResourceInstanceObjectSrc, provider addrs.AbsProviderConfig) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.EnsureModule(addr.Module) ms.SetResourceInstanceDeposed(addr.Resource, key, obj.DeepCopy(), provider) @@ -321,8 +281,7 @@ func (s *SyncState) SetResourceInstanceDeposed(addr addrs.AbsResourceInstance, k // given instance, and so NotDeposed will be returned without modifying the // state at all. func (s *SyncState) DeposeResourceInstanceObject(addr addrs.AbsResourceInstance) DeposedKey { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.Module(addr.Module) if ms == nil { @@ -337,8 +296,7 @@ func (s *SyncState) DeposeResourceInstanceObject(addr addrs.AbsResourceInstance) // that there aren't any races to use a particular key; this method will panic // if the given key is already in use. func (s *SyncState) DeposeResourceInstanceObjectForceKey(addr addrs.AbsResourceInstance, forcedKey DeposedKey) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() if forcedKey == NotDeposed { // Usage error: should use DeposeResourceInstanceObject in this case @@ -356,8 +314,7 @@ func (s *SyncState) DeposeResourceInstanceObjectForceKey(addr addrs.AbsResourceI // ForgetResourceInstanceAll removes the record of all objects associated with // the specified resource instance, if present. If not present, this is a no-op. func (s *SyncState) ForgetResourceInstanceAll(addr addrs.AbsResourceInstance) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.Module(addr.Module) if ms == nil { @@ -367,11 +324,23 @@ func (s *SyncState) ForgetResourceInstanceAll(addr addrs.AbsResourceInstance) { s.maybePruneModule(addr.Module) } +// ForgetResourceInstanceCurrent removes the record of the current object with +// the given address, if present. If not present, this is a no-op. +func (s *SyncState) ForgetResourceInstanceCurrent(addr addrs.AbsResourceInstance) { + defer s.beginWrite()() + + ms := s.state.Module(addr.Module) + if ms == nil { + return + } + ms.ForgetResourceInstanceCurrent(addr.Resource) + s.maybePruneModule(addr.Module) +} + // ForgetResourceInstanceDeposed removes the record of the deposed object with // the given address and key, if present. If not present, this is a no-op. func (s *SyncState) ForgetResourceInstanceDeposed(addr addrs.AbsResourceInstance, key DeposedKey) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() ms := s.state.Module(addr.Module) if ms == nil { @@ -389,8 +358,7 @@ func (s *SyncState) ForgetResourceInstanceDeposed(addr addrs.AbsResourceInstance // Returns true if the object was restored to current, or false if no change // was made at all. func (s *SyncState) MaybeRestoreResourceInstanceDeposed(addr addrs.AbsResourceInstance, key DeposedKey) bool { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() if key == NotDeposed { panic("MaybeRestoreResourceInstanceDeposed called without DeposedKey") @@ -423,8 +391,7 @@ func (s *SyncState) RemovePlannedResourceInstanceObjects() { // so we can remove the need to create this "partial plan" during refresh // that we then need to clean up before proceeding. - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() for _, ms := range s.state.Modules { moduleAddr := ms.Addr @@ -462,18 +429,16 @@ func (s *SyncState) RemovePlannedResourceInstanceObjects() { // the intent of preventing any references to them after they have become // stale due to starting (but possibly not completing) an update. func (s *SyncState) DiscardCheckResults() { - s.lock.Lock() + defer s.beginWrite()() s.state.CheckResults = nil - s.lock.Unlock() } // RecordCheckResults replaces any check results already recorded in the state // with a new set taken from the given check state object. func (s *SyncState) RecordCheckResults(checkState *checks.State) { newResults := NewCheckResults(checkState) - s.lock.Lock() + defer s.beginWrite()() s.state.CheckResults = newResults - s.lock.Unlock() } // Lock acquires an explicit lock on the state, allowing direct read and write @@ -481,6 +446,10 @@ func (s *SyncState) RecordCheckResults(checkState *checks.State) { // access is no longer needed, and then immediately discard the state pointer // pointer. // +// If the [SyncState.Close] method was previously called on this object then +// the caller must treat the result as read-only, since it now has shared +// ownership with whoever called Close. +// // Most callers should not use this. Instead, use the concurrency-safe // accessors and mutators provided directly on SyncState. func (s *SyncState) Lock() *State { @@ -498,12 +467,16 @@ func (s *SyncState) Unlock() { s.lock.Unlock() } -// Close extracts the underlying state from inside this wrapper, making the -// wrapper invalid for any future operations. +// Close extracts the underlying state from inside this wrapper. +// +// After this function returns, the [State] object is shared between the +// caller and this [SyncState] object, but the [SyncState] will only allow +// read access. Callers must avoid accessing the [SyncState] concurrently +// with any subsequent modifications to the state. func (s *SyncState) Close() *State { s.lock.Lock() ret := s.state - s.state = nil // make sure future operations can't still modify it + s.writable = false // only reading is allowed after close s.lock.Unlock() return ret } @@ -531,43 +504,48 @@ func (s *SyncState) maybePruneModule(addr addrs.ModuleInstance) { } func (s *SyncState) MoveAbsResource(src, dst addrs.AbsResource) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() s.state.MoveAbsResource(src, dst) } func (s *SyncState) MaybeMoveAbsResource(src, dst addrs.AbsResource) bool { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() return s.state.MaybeMoveAbsResource(src, dst) } func (s *SyncState) MoveResourceInstance(src, dst addrs.AbsResourceInstance) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() s.state.MoveAbsResourceInstance(src, dst) } func (s *SyncState) MaybeMoveResourceInstance(src, dst addrs.AbsResourceInstance) bool { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() return s.state.MaybeMoveAbsResourceInstance(src, dst) } func (s *SyncState) MoveModuleInstance(src, dst addrs.ModuleInstance) { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() s.state.MoveModuleInstance(src, dst) } func (s *SyncState) MaybeMoveModuleInstance(src, dst addrs.ModuleInstance) bool { - s.lock.Lock() - defer s.lock.Unlock() + defer s.beginWrite()() return s.state.MaybeMoveModuleInstance(src, dst) } + +func (s *SyncState) beginWrite() func() { + s.lock.Lock() + if !s.writable { + s.lock.Unlock() + panic("attempt to write through non-writable SyncState") + } + return func() { + s.lock.Unlock() + } +} diff --git a/internal/terminal/impl_others.go b/internal/terminal/impl_others.go index 9c0abbc66b..de9f7ecaef 100644 --- a/internal/terminal/impl_others.go +++ b/internal/terminal/impl_others.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !windows // +build !windows diff --git a/internal/terminal/impl_windows.go b/internal/terminal/impl_windows.go index 46cc6ee3f5..e4ed31762f 100644 --- a/internal/terminal/impl_windows.go +++ b/internal/terminal/impl_windows.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build windows // +build windows diff --git a/internal/terminal/stream.go b/internal/terminal/stream.go index 6d40e1b184..3d627eaf26 100644 --- a/internal/terminal/stream.go +++ b/internal/terminal/stream.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terminal import ( diff --git a/internal/terminal/streams.go b/internal/terminal/streams.go index 1e1de7d967..4d401ca339 100644 --- a/internal/terminal/streams.go +++ b/internal/terminal/streams.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package terminal encapsulates some platform-specific logic for detecting // if we're running in a terminal and, if so, properly configuring that // terminal to meet the assumptions that the rest of Terraform makes. diff --git a/internal/terminal/streams_test.go b/internal/terminal/streams_test.go index 9826b93418..68bff41836 100644 --- a/internal/terminal/streams_test.go +++ b/internal/terminal/streams_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terminal import ( diff --git a/internal/terminal/testing.go b/internal/terminal/testing.go index 2830b5d0bf..e597dcae29 100644 --- a/internal/terminal/testing.go +++ b/internal/terminal/testing.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terminal import ( diff --git a/internal/terraform/context.go b/internal/terraform/context.go index 115f62276c..425c4faf66 100644 --- a/internal/terraform/context.go +++ b/internal/terraform/context.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -7,6 +10,8 @@ import ( "sort" "sync" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/logging" @@ -14,9 +19,6 @@ import ( "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" - - _ "github.com/hashicorp/terraform/internal/logging" ) // InputMode defines what sort of input will be asked for when Input @@ -41,6 +43,23 @@ type ContextOpts struct { Providers map[addrs.Provider]providers.Factory Provisioners map[string]provisioners.Factory + // PreloadedProviderSchemas is an optional map of provider schemas that + // were already loaded from providers by the caller. This is intended + // to avoid redundant re-fetching of schemas when the caller has already + // loaded them for some other reason. + // + // The preloaded schemas do not need to be exhaustive. Terraform will + // use a preloaded schema if available, or will load a schema directly from + // a provider if no preloaded schema is available. + // + // The caller MUST ensure that the given schemas exactly match those that + // would be returned from a running provider of the given type or else the + // runtime behavior is likely to be erratic. + // + // Callers must not access (read or write) the given map once it has + // been passed to Terraform Core using this field. + PreloadedProviderSchemas map[addrs.Provider]providers.ProviderSchema + UIInput UIInput } @@ -76,9 +95,10 @@ type Context struct { plugins *contextPlugins - hooks []Hook - sh *stopHook - uiInput UIInput + hooks []Hook + sh *stopHook + uiInput UIInput + graphOpts *ContextGraphOpts l sync.Mutex // Lock acquired during any task parallelSem Semaphore @@ -127,14 +147,15 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { par = 10 } - plugins := newContextPlugins(opts.Providers, opts.Provisioners) + plugins := newContextPlugins(opts.Providers, opts.Provisioners, opts.PreloadedProviderSchemas) log.Printf("[TRACE] terraform.NewContext: complete") return &Context{ - hooks: hooks, - meta: opts.Meta, - uiInput: opts.UIInput, + hooks: hooks, + meta: opts.Meta, + uiInput: opts.UIInput, + graphOpts: &ContextGraphOpts{}, plugins: plugins, @@ -144,14 +165,12 @@ func NewContext(opts *ContextOpts) (*Context, tfdiags.Diagnostics) { }, diags } -func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) { - // TODO: This method gets called multiple times on the same context with - // the same inputs by different parts of Terraform that all need the - // schemas, and it's typically quite expensive because it has to spin up - // plugins to gather their schemas, so it'd be good to have some caching - // here to remember plugin schemas we already loaded since the plugin - // selections can't change during the life of a *Context object. +func (c *Context) SetGraphOpts(opts *ContextGraphOpts) tfdiags.Diagnostics { + c.graphOpts = opts + return nil +} +func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics ret, err := loadSchemas(config, state, c.plugins) @@ -167,8 +186,8 @@ func (c *Context) Schemas(config *configs.Config, state *states.State) (*Schemas } type ContextGraphOpts struct { - // If true, validates the graph structure (checks for cycles). - Validate bool + // If false, skip the graph structure validation. + SkipGraphValidation bool // Legacy graphs only: won't prune the graph Verbose bool @@ -195,6 +214,12 @@ func (c *Context) Stop() { c.runContextCancel = nil } + // Notify all of the hooks that we're stopping, in case they want to try + // to flush in-memory state to disk before a subsequent hard kill. + for _, hook := range c.hooks { + hook.Stopping() + } + // Grab the condition var before we exit if cond := c.runCond; cond != nil { log.Printf("[INFO] terraform: waiting for graceful stop to complete") @@ -320,6 +345,51 @@ func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan s return stop, wait } +func (c *Context) checkStateDependencies(state *states.State) tfdiags.Diagnostics { + if state == nil { + // no state is fine, first time execution etc + return nil + } + + var diags tfdiags.Diagnostics + for providerAddr := range state.ProviderRequirements() { + if !c.plugins.HasProvider(providerAddr) { + if c.plugins.HasPreloadedSchemaForProvider(providerAddr) { + // If the caller provided a preloaded schema for this provider + // then we'll take that as a hint that the caller is intending + // to handle some of these pre-validation tasks itself and + // so we'll just optimistically assume that the caller + // has arranged for this to work some other way, or will + // return its own version of this error before calling + // into here if not. + continue + } + if !providerAddr.IsBuiltIn() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Missing required provider", + fmt.Sprintf( + "This state requires provider %s, but that provider isn't available. You may be able to install it automatically by running:\n terraform init", + providerAddr, + ), + )) + } else { + // Built-in providers can never be installed by "terraform init", + // so no point in confusing the user by suggesting that. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Missing required provider", + fmt.Sprintf( + "This state requires built-in provider %s, but that provider isn't available in this Terraform version.", + providerAddr, + ), + )) + } + } + } + return diags +} + // checkConfigDependencies checks whether the recieving context is able to // support the given configuration, returning error diagnostics if not. // @@ -347,13 +417,23 @@ func (c *Context) checkConfigDependencies(config *configs.Config) tfdiags.Diagno // We only check that we have a factory for each required provider, and // assume the caller already assured that any separately-installed // plugins are of a suitable version, match expected checksums, etc. - providerReqs, hclDiags := config.ProviderRequirements() + providerReqs, hclDiags := config.ProviderRequirementsConfigOnly() diags = diags.Append(hclDiags) if hclDiags.HasErrors() { return diags } for providerAddr := range providerReqs { if !c.plugins.HasProvider(providerAddr) { + if c.plugins.HasPreloadedSchemaForProvider(providerAddr) { + // If the caller provided a preloaded schema for this provider + // then we'll take that as a hint that the caller is intending + // to handle some of these pre-validation tasks itself and + // so we'll just optimistically assume that the caller + // has arranged for this to work some other way, or will + // return its own version of this error before calling + // into here if not. + continue + } if !providerAddr.IsBuiltIn() { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/terraform/context_apply.go b/internal/terraform/context_apply.go index 1d66628ad7..0f996321ce 100644 --- a/internal/terraform/context_apply.go +++ b/internal/terraform/context_apply.go @@ -1,17 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) +// ApplyOpts are options that affect the details of how Terraform will apply a +// previously-generated plan. +type ApplyOpts struct { + // ExternalProviders is a set of pre-configured provider instances with + // the same purpose as [PlanOpts.ExternalProviders]. + // + // Callers must pass providers that are configured in a similar way as + // the providers that were passed when creating the plan that's being + // applied, or the results will be erratic. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface + + // SetVariables are the raw values for root module variables as provided + // by the user who is requesting the run, prior to any normalization or + // substitution of defaults. See the documentation for the InputValue + // type for more information on how to correctly populate this. + // + // During the apply phase it's only valid to specify values for input + // values that were declared as ephemeral, because all other input + // values must retain the values that were specified during planning. + SetVariables InputValues +} + +// ApplyOpts creates an [ApplyOpts] with copies of all of the elements that +// are expected to propagate from plan to apply when planning and applying +// in the same process. +// +// In practice planning and applying are often separated into two different +// executions, in which case callers must retain enough information between +// plan and apply to construct an equivalent [ApplyOpts] themselves without +// using this function. This is here mainly for convenient internal use such +// as in test cases. +func (po *PlanOpts) ApplyOpts() *ApplyOpts { + return &ApplyOpts{ + ExternalProviders: po.ExternalProviders, + } +} + // Apply performs the actions described by the given Plan object and returns // the resulting updated state. // @@ -20,26 +64,141 @@ import ( // // Even if the returned diagnostics contains errors, Apply always returns the // resulting state which is likely to have been partially-updated. -func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State, tfdiags.Diagnostics) { - defer c.acquireRun("apply")() +// +// The [opts] argument may be nil to indicate that no special options are +// required. In that case, Apply will use a default set of options. Some +// options in [PlanOpts] when creating a plan must be echoed with equivalent +// settings during apply, so leaving opts as nil might not be valid for +// certain combinations of plan-time options. +func (c *Context) Apply(plan *plans.Plan, config *configs.Config, opts *ApplyOpts) (*states.State, tfdiags.Diagnostics) { + state, _, diags := c.ApplyAndEval(plan, config, opts) + return state, diags +} +// ApplyAndEval is like [Context.Apply] except that it additionally makes a +// best effort to return a [lang.Scope] which can evaluate expressions in the +// root module based on the content of the new state. +// +// The scope will be nil if the apply process doesn't complete successfully +// enough to produce a valid evaluation scope. If the returned state is nil +// then the scope will always be nil, but it's also possible for the scope +// to be nil even when the state isn't, if the apply didn't complete enough for +// the evaluation scope to produce consistent results. +func (c *Context) ApplyAndEval(plan *plans.Plan, config *configs.Config, opts *ApplyOpts) (*states.State, *lang.Scope, tfdiags.Diagnostics) { + defer c.acquireRun("apply")() + var diags tfdiags.Diagnostics + + if plan == nil { + panic("cannot apply nil plan") + } log.Printf("[DEBUG] Building and walking apply graph for %s plan", plan.UIMode) - graph, operation, diags := c.applyGraph(plan, config, true) + if opts == nil { + opts = &ApplyOpts{} + } + + if plan.Errored { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot apply failed plan", + `The given plan is incomplete due to errors during planning, and so it cannot be applied.`, + )) + return nil, nil, diags + } + if !plan.Applyable { + if plan.Changes.Empty() { + // If a plan is not applyable but it didn't have any errors then that + // suggests it was a "no-op" plan, which doesn't really do any + // harm to apply, so we'll just do it but leave ourselves a note + // in the trace log in case it ends up relevant to a bug report. + log.Printf("[TRACE] Applying a no-op plan") + } else { + // This situation isn't something we expect, since our own rules + // for what "applyable" means make this scenario impossible. We'll + // reject it on the assumption that something very strange is + // going on. and so better to halt than do something incorrect. + // This error message is generic and useless because we don't + // expect anyone to ever see it in normal use. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot apply non-applyable plan", + `The given plan is not applyable. If this seems like a bug in Terraform, then please report it!`, + )) + return nil, nil, diags + } + } + + // The caller must provide values for all of the "apply-time variables" + // mentioned in the plan, and for no others because the others come from + // the plan itself. + diags = diags.Append(checkApplyTimeVariables(plan.ApplyTimeVariables, opts.SetVariables, config)) + if diags.HasErrors() { - return nil, diags + // If the apply request is invalid in some way then we'll bail out + // here before we do any real work. + return nil, nil, diags + } + + for _, rc := range plan.Changes.Resources { + // Import is a no-op change during an apply (all the real action happens during the plan) but we'd + // like to show some helpful output that mirrors the way we show other changes. + if rc.Importing != nil { + hookResourceID := HookResourceIdentity{ + Addr: rc.Addr, + ProviderAddr: rc.ProviderAddr.Provider, + } + for _, h := range c.hooks { + // In future, we may need to call PostApplyImport separately elsewhere in the apply + // operation. For now, though, we'll call Pre and Post hooks together. + h.PreApplyImport(hookResourceID, *rc.Importing) + h.PostApplyImport(hookResourceID, *rc.Importing) + } + } + } + + graph, operation, moreDiags := c.applyGraph(plan, config, opts, true) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, nil, diags + } + + moreDiags = checkExternalProviders(config, plan, nil, opts.ExternalProviders) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, nil, diags + } + + schemas, schemaDiags := c.Schemas(config, plan.PriorState) + diags = diags.Append(schemaDiags) + if diags.HasErrors() { + return nil, nil, diags + } + + changes, err := plan.Changes.Decode(schemas) + if err != nil { + diags = diags.Append(err) + return nil, nil, diags } workingState := plan.PriorState.DeepCopy() walker, walkDiags := c.walk(graph, operation, &graphWalkOpts{ - Config: config, - InputState: workingState, - Changes: plan.Changes, + Config: config, + InputState: workingState, + Changes: changes, + Overrides: plan.Overrides, + ExternalProviderConfigs: opts.ExternalProviders, + + DeferralAllowed: true, // We need to propagate the check results from the plan phase, // because that will tell us which checkable objects we're expecting // to see updated results from during the apply step. PlanTimeCheckResults: plan.Checks, + + // We also want to propagate the timestamp from the plan file. + PlanTimeTimestamp: plan.Timestamp, + + FunctionResults: lang.NewFunctionResultsTable(plan.FunctionResults), }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) @@ -64,15 +223,73 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State "Applied changes may be incomplete", `The plan was created with the -target option in effect, so some changes requested in the configuration may have been ignored and the output values may not be fully updated. Run the following command to verify that no other changes are pending: terraform plan - + Note that the -target option is not suitable for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, )) } - return newState, diags + // FIXME: we cannot check for an empty plan for refresh-only, because root + // outputs are always stored as changes. The final condition of the state + // also depends on some cleanup which happens during the apply walk. It + // would probably make more sense if applying a refresh-only plan were + // simply just returning the planned state and checks, but some extra + // cleanup is going to be needed to make the plan state match what apply + // would do. For now we can copy the checks over which were overwritten + // during the apply walk. + // Despite the intent of UIMode, it must still be used for apply-time + // differences in destroy plans too, so we can make use of that here as + // well. + if plan.UIMode == plans.RefreshOnlyMode { + newState.CheckResults = plan.Checks.DeepCopy() + } + + // The caller also gets access to an expression evaluation scope in the + // root module, in case it needs to extract other information using + // expressions, like in "terraform console" or the test harness. + evalScope := evalScopeFromGraphWalk(walker, addrs.RootModuleInstance) + + return newState, evalScope, diags } -func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { +func checkApplyTimeVariables(needed collections.Set[string], gotValues InputValues, config *configs.Config) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + for name := range needed.All() { + if vv, exists := gotValues[name]; !exists || vv.Value == cty.NilVal || vv.Value.IsNull() { + // This error message assumes that the only possible reason for + // an apply-time variable is because the variable is ephemeral, + // which is true at the time of writing. This error message might + // need to be generalized if we introduce other reasons for + // apply-time variables in future. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No value for required variable", + fmt.Sprintf("The ephemeral input variable %q was set during the plan phase, and so must also be set during the apply phase.", name), + )) + } + } + for name := range gotValues { + if !needed.Has(name) { + // We'll treat this a little differently depending on whether + // the variable is declared as ephemeral or not. + if vc, ok := config.Module.Variables[name]; ok && vc.Ephemeral { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "No value for required variable", + fmt.Sprintf("The ephemeral input variable %q was not set during the plan phase, and so must remain unset during the apply phase.", name), + )) + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected new value for variable", + fmt.Sprintf("Input variable %q is non-ephemeral, so its value was fixed during planning and cannot be reset during apply.", name), + )) + } + } + } + return diags +} + +func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, opts *ApplyOpts, validate bool) (*Graph, walkOperation, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics variables := InputValues{} @@ -86,12 +303,24 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate )) continue } + if pvm, ok := plan.VariableMarks[name]; ok { + val = val.MarkWithPaths(pvm) + } variables[name] = &InputValue{ Value: val, SourceType: ValueFromPlan, } } + // Apply-time variables need to be merged in too. + // FIXME: We should check that all of these match declared variables and + // that all of them are declared as ephemeral, because all non-ephemeral + // variables are supposed to come exclusively from plan.VariableValues. + if opts != nil { + for n, vv := range opts.SetVariables { + variables[n] = vv + } + } if diags.HasErrors() { return nil, walkApply, diags } @@ -111,30 +340,43 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate } } + operation := walkApply + if plan.UIMode == plans.DestroyMode { + // FIXME: Due to differences in how objects must be handled in the + // graph and evaluated during a complete destroy, we must continue to + // use plans.DestroyMode to switch on this behavior. If all objects + // which require special destroy handling can be tracked in the plan, + // then this switch will no longer be needed and we can remove the + // walkDestroy operation mode. + // TODO: Audit that and remove walkDestroy as an operation mode. + operation = walkDestroy + } + + var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + if opts != nil { + externalProviderConfigs = opts.ExternalProviders + } + graph, moreDiags := (&ApplyGraphBuilder{ - Config: config, - Changes: plan.Changes, - State: plan.PriorState, - RootVariableValues: variables, - Plugins: c.plugins, - Targets: plan.TargetAddrs, - ForceReplace: plan.ForceReplaceAddrs, + Config: config, + Changes: plan.Changes, + DeferredChanges: plan.DeferredResources, + State: plan.PriorState, + RootVariableValues: variables, + ExternalProviderConfigs: externalProviderConfigs, + Plugins: c.plugins, + Targets: plan.TargetAddrs, + ForceReplace: plan.ForceReplaceAddrs, + Operation: operation, + ExternalReferences: plan.ExternalReferences, + Overrides: plan.Overrides, + SkipGraphValidation: c.graphOpts.SkipGraphValidation, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { return nil, walkApply, diags } - operation := walkApply - if plan.UIMode == plans.DestroyMode { - // NOTE: This is a vestigial violation of the rule that we mustn't - // use plan.UIMode to affect apply-time behavior. It's a design error - // if anything downstream switches behavior when operation is set - // to walkDestroy, but we've not yet fully audited that. - // TODO: Audit that and remove walkDestroy as an operation mode. - operation = walkDestroy - } - return graph, operation, diags } @@ -146,14 +388,14 @@ func (c *Context) applyGraph(plan *plans.Plan, config *configs.Config, validate // The result of this is intended only for rendering ot the user as a dot // graph, and so may change in future in order to make the result more useful // in that context, even if drifts away from the physical graph that Terraform -// Core currently uses as an implementation detail of planning. +// Core currently uses as an implementation detail of applying. func (c *Context) ApplyGraphForUI(plan *plans.Plan, config *configs.Config) (*Graph, tfdiags.Diagnostics) { // For now though, this really is just the internal graph, confusing // implementation details and all. var diags tfdiags.Diagnostics - graph, _, moreDiags := c.applyGraph(plan, config, false) + graph, _, moreDiags := c.applyGraph(plan, config, nil, false) diags = diags.Append(moreDiags) return graph, diags } diff --git a/internal/terraform/context_apply2_test.go b/internal/terraform/context_apply2_test.go index 9637a6951e..4ee21b591b 100644 --- a/internal/terraform/context_apply2_test.go +++ b/internal/terraform/context_apply2_test.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "bytes" "errors" "fmt" + "sort" "strings" "sync" "testing" @@ -11,14 +15,21 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/collections" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) // Test that the PreApply hook is called with the correct deposed key @@ -62,10 +73,10 @@ func TestContext2Apply_createBeforeDestroy_deposedKeyPreApply(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -157,7 +168,7 @@ output "data" { t.Fatal(diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -180,7 +191,7 @@ output "data" { return resp } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -240,9 +251,9 @@ resource "test_instance" "a" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -328,7 +339,7 @@ resource "aws_instance" "bin" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) bar := plan.PriorState.ResourceInstance(barAddr) if len(bar.Current.Dependencies) == 0 || !bar.Current.Dependencies[0].Equal(fooAddr.ContainingResource().Config()) { @@ -350,7 +361,7 @@ resource "aws_instance" "bin" { t.Fatalf("baz should depend on bam after refresh, but got %s", baz.Current.Dependencies) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -386,8 +397,8 @@ resource "test_resource" "b" { `, }) - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -414,11 +425,8 @@ resource "test_resource" "b" { mustResourceInstanceAddr(`test_resource.a`), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"id":"a","sensitive_attr":["secret"]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("sensitive_attr"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("sensitive_attr"), }, Status: states.ObjectReady, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -432,9 +440,9 @@ resource "test_resource" "b" { }) plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -473,9 +481,9 @@ output "out" { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -486,7 +494,7 @@ output "out" { } plan, diags = ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // make sure the same marks are compared in the next plan as well for _, c := range plan.Changes.Resources { @@ -536,20 +544,20 @@ resource "test_object" "y" { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) // FINAL PLAN: plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // make sure the same marks are compared in the next plan as well for _, c := range plan.Changes.Resources { if c.Action != plans.NoOp { - t.Logf("marks before: %#v", c.BeforeValMarks) - t.Logf("marks after: %#v", c.AfterValMarks) + t.Logf("sensitive paths before: %#v", c.BeforeSensitivePaths) + t.Logf("sensitive paths after: %#v", c.AfterSensitivePaths) t.Errorf("Unexpcetd %s change for %s", c.Action, c.Addr) } } @@ -595,7 +603,7 @@ resource "test_object" "x" { t.Fatalf("plan: %s", diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply: %s", diags.Err()) } @@ -610,12 +618,12 @@ func TestContext2Apply_nullableVariables(t *testing.T) { if diags.HasErrors() { t.Fatalf("plan: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply: %s", diags.Err()) } - outputs := state.Module(addrs.RootModuleInstance).OutputValues + outputs := state.RootOutputValues // we check for null outputs be seeing that they don't exists if _, ok := outputs["nullable_null_default"]; ok { t.Error("nullable_null_default: expected no output value") @@ -671,17 +679,17 @@ resource "test_object" "s" { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) // destroy only a single instance not included in the moved statements _, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, Targets: []addrs.Targetable{mustResourceInstanceAddr(`module.modb["a"].test_object.a`)}, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) } func TestContext2Apply_graphError(t *testing.T) { @@ -736,7 +744,7 @@ resource "test_object" "b" { testObjA := plan.PriorState.Modules[""].Resources["test_object.a"].Instances[addrs.NoKey].Current testObjA.Dependencies = append(testObjA.Dependencies, mustResourceInstanceAddr("test_object.b").ContainingResource().Config()) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("expected cycle error from apply") } @@ -770,7 +778,7 @@ resource "test_resource" "c" { }) p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -810,7 +818,7 @@ resource "test_resource" "c" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if len(plan.Changes.Resources) != 3 { t.Fatalf("unexpected plan changes: %#v", plan.Changes) } @@ -822,8 +830,8 @@ resource "test_resource" "c" { resp.NewState = cty.ObjectVal(m) return resp } - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) wantResourceAttrs := map[string]struct{ value, output string }{ "a": {"boop", "new-boop"}, @@ -833,10 +841,20 @@ resource "test_resource" "c" { for name, attrs := range wantResourceAttrs { addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) r := state.ResourceInstance(addr) - rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ - "value": cty.String, - "output": cty.String, - })) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + "output": { + Type: cty.String, + }, + }, + }, + Version: 0, + } + rd, err := r.Current.Decode(schema) if err != nil { t.Fatalf("error decoding test_resource.a: %s", err) } @@ -859,7 +877,7 @@ resource "test_resource" "c" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if len(plan.Changes.Resources) != 3 { t.Fatalf("unexpected plan changes: %#v", plan.Changes) } @@ -878,7 +896,7 @@ resource "test_resource" "c" { resp.NewState = cty.ObjectVal(m) return resp } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -894,10 +912,20 @@ resource "test_resource" "c" { for name, attrs := range wantResourceAttrs { addr := mustResourceInstanceAddr(fmt.Sprintf("test_resource.%s", name)) r := state.ResourceInstance(addr) - rd, err := r.Current.Decode(cty.Object(map[string]cty.Type{ - "value": cty.String, - "output": cty.String, - })) + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + }, + "output": { + Type: cty.String, + }, + }, + }, + Version: 0, + } + rd, err := r.Current.Decode(schema) if err != nil { t.Fatalf("error decoding test_resource.a: %s", err) } @@ -917,6 +945,119 @@ resource "test_resource" "c" { }) } +func TestContext2Apply_outputValuePrecondition(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + variable "input" { + type = string + } + + module "child" { + source = "./child" + + input = var.input + } + + output "result" { + value = module.child.result + + precondition { + condition = var.input != "" + error_message = "Input must not be empty." + } + } + `, + "child/main.tf": ` + variable "input" { + type = string + } + + output "result" { + value = var.input + + precondition { + condition = var.input != "" + error_message = "Input must not be empty." + } + } + `, + }) + + checkableObjects := []addrs.Checkable{ + addrs.OutputValue{Name: "result"}.Absolute(addrs.RootModuleInstance), + addrs.OutputValue{Name: "result"}.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey)), + } + + t.Run("pass", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{}) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "input": &InputValue{ + Value: cty.StringVal("beep"), + SourceType: ValueFromCLIArg, + }, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + for _, addr := range checkableObjects { + result := plan.Checks.GetObjectResult(addr) + if result == nil { + t.Fatalf("no check result for %s in the plan", addr) + } + if got, want := result.Status, checks.StatusPass; got != want { + t.Fatalf("wrong check status for %s during planning\ngot: %s\nwant: %s", addr, got, want) + } + } + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) + for _, addr := range checkableObjects { + result := state.CheckResults.GetObjectResult(addr) + if result == nil { + t.Fatalf("no check result for %s in the final state", addr) + } + if got, want := result.Status, checks.StatusPass; got != want { + t.Errorf("wrong check status for %s after apply\ngot: %s\nwant: %s", addr, got, want) + } + } + }) + + t.Run("fail", func(t *testing.T) { + // NOTE: This test actually catches a failure during planning and so + // cannot proceed to apply, so it's really more of a plan test + // than an apply test but better to keep all of these + // thematically-related test cases together. + ctx := testContext2(t, &ContextOpts{}) + _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "input": &InputValue{ + Value: cty.StringVal(""), + SourceType: ValueFromCLIArg, + }, + }, + }) + if !diags.HasErrors() { + t.Fatalf("succeeded; want error") + } + + const wantSummary = "Module output value precondition failed" + found := false + for _, diag := range diags { + if diag.Severity() == tfdiags.Error && diag.Description().Summary == wantSummary { + found = true + break + } + } + + if !found { + t.Fatalf("missing expected error\nwant summary: %s\ngot: %s", wantSummary, diags.Err().Error()) + } + }) +} + func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { // This tests the less common situation where a condition fails due to // a change in a resource other than the one the condition is attached to, @@ -952,7 +1093,7 @@ func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { }) p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -1013,7 +1154,7 @@ func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) planA := plan.Changes.ResourceInstance(instA) if planA == nil || planA.Action != plans.Create { t.Fatalf("incorrect initial plan for instance A\nwant a 'create' change\ngot: %s", spew.Sdump(planA)) @@ -1023,8 +1164,8 @@ func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { t.Fatalf("incorrect initial plan for instance B\nwant a 'create' change\ngot: %s", spew.Sdump(planB)) } - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) stateA := state.ResourceInstance(instA) if stateA == nil || stateA.Current == nil || !bytes.Contains(stateA.Current.AttrsJSON, []byte(`"beep"`)) { @@ -1050,7 +1191,7 @@ func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) planA := plan.Changes.ResourceInstance(instA) if planA == nil || planA.Action != plans.Update { t.Fatalf("incorrect initial plan for instance A\nwant an 'update' change\ngot: %s", spew.Sdump(planA)) @@ -1060,7 +1201,7 @@ func TestContext2Apply_resourceConditionApplyTimeFail(t *testing.T) { t.Fatalf("incorrect initial plan for instance B\nwant a 'no-op' change\ngot: %s", spew.Sdump(planB)) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("final apply succeeded, but should've failed with a postcondition error") } @@ -1123,15 +1264,15 @@ output "out" { } `}) - testProvider := &MockProvider{ + testProvider := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ - "test_object": providers.Schema{Block: simpleTestSchema()}, + "test_object": providers.Schema{Body: simpleTestSchema()}, }, DataSources: map[string]providers.Schema{ "test_object": providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "test_string": { Type: cty.String, @@ -1161,10 +1302,10 @@ output "out" { return resp } - otherProvider := &MockProvider{ + otherProvider := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "output": { Type: cty.List(cty.String), @@ -1182,7 +1323,7 @@ output "out" { }, }, ResourceTypes: map[string]providers.Schema{ - "other_object": providers.Schema{Block: simpleTestSchema()}, + "other_object": providers.Schema{Body: simpleTestSchema()}, }, }, } @@ -1196,10 +1337,28 @@ output "out" { opts := SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)) plan, diags := ctx.Plan(m, states.NewState(), opts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + // Resource changes which have dependencies across providers which + // themselves depend on resources can result in cycles. + // Because other_object transitively depends on the module resources + // through its provider, we trigger changes on both sides of this boundary + // to ensure we can create a valid plan. + // + // Taint the object to make sure a replacement works in the plan. + otherObjAddr := mustResourceInstanceAddr("other_object.other") + otherObj := state.ResourceInstance(otherObjAddr) + otherObj.Current.Status = states.ObjectTainted + // Force a change which needs to be reverted. + testObjAddr := mustResourceInstanceAddr(`module.mod["a"].test_object.a`) + testObjA := state.ResourceInstance(testObjAddr) + testObjA.Current.AttrsJSON = []byte(`{"test_bool":null,"test_list":null,"test_map":null,"test_number":null,"test_string":"changed"}`) + + _, diags = ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) otherProvider.ConfigureProviderCalled = false otherProvider.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { @@ -1230,7 +1389,7 @@ got: %#v`, // destroy only a single instance not included in the moved statements _, diags = ctx.Plan(m, state, opts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !otherProvider.ConfigureProviderCalled { t.Fatal("failed to configure provider during destroy plan") @@ -1303,8 +1462,2553 @@ resource "test_object" "x" { t.Fatalf("plan: %s", diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply: %s", diags.Err()) } } + +func TestContext2Apply_missingOrphanedResource(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +# changed resource address to create a new object +resource "test_object" "y" { + test_string = "y" +} +`, + }) + + p := simpleMockProvider() + + // report the prior value is missing + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + resp.NewState = cty.NullVal(req.PriorState.Type()) + return resp + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.x").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"x"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + opts := SimplePlanOpts(plans.NormalMode, nil) + plan, diags := ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +// Outputs should not cause evaluation errors during destroy +// Check eval from both root level outputs and module outputs, which are +// handled differently during apply. +func TestContext2Apply_outputsNotToEvaluate(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" + cond = false +} + +output "from_resource" { + value = module.mod.from_resource +} + +output "from_data" { + value = module.mod.from_data +} +`, + + "./mod/main.tf": ` +variable "cond" { + type = bool +} + +module "mod" { + source = "../mod2/" + cond = var.cond +} + +output "from_resource" { + value = module.mod.resource +} + +output "from_data" { + value = module.mod.data +} +`, + + "./mod2/main.tf": ` +variable "cond" { + type = bool +} + +resource "test_object" "x" { + count = var.cond ? 0:1 +} + +data "test_object" "d" { + count = var.cond ? 0:1 +} + +output "resource" { + value = var.cond ? null : test_object.x.*.test_string[0] +} + +output "data" { + value = one(data.test_object.d[*].test_string) +} +`}) + + p := simpleMockProvider() + p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { + resp.State = req.Config + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // apply the state + opts := SimplePlanOpts(plans.NormalMode, nil) + plan, diags := ctx.Plan(m, states.NewState(), opts) + tfdiags.AssertNoErrors(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + // and destroy + opts = SimplePlanOpts(plans.DestroyMode, nil) + plan, diags = ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + state, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + // and destroy again with no state + if !state.Empty() { + t.Fatal("expected empty state, got", state) + } + + opts = SimplePlanOpts(plans.DestroyMode, nil) + plan, diags = ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +// don't evaluate conditions on outputs when destroying +func TestContext2Apply_noOutputChecksOnDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + +output "from_resource" { + value = module.mod.from_resource +} +`, + + "./mod/main.tf": ` +resource "test_object" "x" { + test_string = "wrong val" +} + +output "from_resource" { + value = test_object.x.test_string + precondition { + condition = test_object.x.test_string == "ok" + error_message = "resource error" + } +} +`}) + + p := simpleMockProvider() + + state := states.NewState() + mod := state.EnsureModule(addrs.RootModuleInstance.Child("mod", addrs.NoKey)) + mod.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.x").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"wrong_val"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + opts := SimplePlanOpts(plans.DestroyMode, nil) + plan, diags := ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +// -refresh-only should update checks +func TestContext2Apply_refreshApplyUpdatesChecks(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "x" { + test_string = "ok" + lifecycle { + postcondition { + condition = self.test_string == "ok" + error_message = "wrong val" + } + } +} + +output "from_resource" { + value = test_object.x.test_string + precondition { + condition = test_object.x.test_string == "ok" + error_message = "wrong val" + } +} +`}) + + p := simpleMockProvider() + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("ok"), + }), + } + + state := states.NewState() + mod := state.EnsureModule(addrs.RootModuleInstance) + mod.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.x").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"wrong val"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + state.SetOutputValue( + addrs.OutputValue{Name: "from_resource"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("wrong val"), false, + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + opts := SimplePlanOpts(plans.RefreshOnlyMode, nil) + plan, diags := ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + state, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + resCheck := state.CheckResults.GetObjectResult(mustResourceInstanceAddr("test_object.x")) + if resCheck.Status != checks.StatusPass { + t.Fatalf("unexpected check %s: %s\n", resCheck.Status, resCheck.FailureMessages) + } + + outAddr := addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{ + Name: "from_resource", + }, + } + outCheck := state.CheckResults.GetObjectResult(outAddr) + if outCheck.Status != checks.StatusPass { + t.Fatalf("unexpected check %s: %s\n", outCheck.Status, outCheck.FailureMessages) + } +} + +// NoOp changes may have conditions to evaluate, but should not re-plan and +// apply the entire resource. +func TestContext2Apply_noRePlanNoOp(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "x" { +} + +resource "test_object" "y" { + # test_object.w is being re-created, so this precondition must be evaluated + # during apply, however this resource should otherwise be a NoOp. + lifecycle { + precondition { + condition = test_object.x.test_string == null + error_message = "test_object.x.test_string should be null" + } + } +} +`}) + + p := simpleMockProvider() + // make sure we can compute the attr + testString := p.GetProviderSchemaResponse.ResourceTypes["test_object"].Body.Attributes["test_string"] + testString.Computed = true + testString.Optional = false + + yAddr := mustResourceInstanceAddr("test_object.y") + + state := states.NewState() + mod := state.RootModule() + mod.SetResourceInstanceCurrent( + yAddr.Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"y"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + opts := SimplePlanOpts(plans.NormalMode, nil) + plan, diags := ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + for _, c := range plan.Changes.Resources { + if c.Addr.Equal(yAddr) && c.Action != plans.NoOp { + t.Fatalf("unexpected %s change for test_object.y", c.Action) + } + } + + // test_object.y is a NoOp change from the plan, but is included in the + // graph due to the conditions which must be evaluated. This however should + // not cause the resource to be re-planned. + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + testString := req.ProposedNewState.GetAttr("test_string") + if !testString.IsNull() && testString.AsString() == "y" { + resp.Diagnostics = resp.Diagnostics.Append(errors.New("Unexpected apply-time plan for test_object.y. Original plan was a NoOp")) + } + resp.PlannedState = req.ProposedNewState + return resp + } + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +// ensure all references from preconditions are tracked through plan and apply +func TestContext2Apply_preconditionErrorMessageRef(t *testing.T) { + p := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "nested" { + source = "./mod" +} + +output "nested_a" { + value = module.nested.a +} +`, + + "mod/main.tf": ` +variable "boop" { + default = "boop" +} + +variable "msg" { + default = "Incorrect boop." +} + +output "a" { + value = "x" + + precondition { + condition = var.boop == "boop" + error_message = var.msg + } +} +`, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Apply_destroyNullModuleOutput(t *testing.T) { + p := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "null_module" { + source = "./mod" +} + +locals { + module_output = module.null_module.null_module_test +} + +output "test_root" { + value = module.null_module.test_output +} + +output "root_module" { + value = local.module_output #fails +} +`, + + "mod/main.tf": ` +output "test_output" { + value = "test" +} + +output "null_module_test" { + value = null +} +`, + }) + + // verify plan and apply + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + // now destroy + plan, diags = ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + tfdiags.AssertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Apply_moduleOutputWithSensitiveAttrs(t *testing.T) { + // Ensure that nested sensitive marks are stored when accessing non-root + // module outputs, and that they do not cause the entire output value to + // become sensitive. + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + +resource "test_resource" "b" { + // if the module output were wholly sensitive it would not be valid to use in + // for_each + for_each = module.mod.resources + value = each.value.output +} + +output "root_output" { + // The root output cannot contain any sensitive marks at all. + // Applying nonsensitive would fail here if the nested sensitive mark were + // not maintained through the output. + value = [ for k, v in module.mod.resources : nonsensitive(v.output) ] +} +`, + "./mod/main.tf": ` +resource "test_resource" "a" { + for_each = {"key": "value"} + value = each.key +} + +output "resources" { + value = test_resource.a +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + "output": { + Type: cty.String, + Sensitive: true, + Computed: true, + }, + }, + }, + }, + }) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Apply_timestamps(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "a" { + id = "timestamp" + value = timestamp() +} + +resource "test_resource" "b" { + id = "plantimestamp" + value = plantimestamp() +} +`, + }) + + var plantime time.Time + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + p.PlanResourceChangeFn = func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + values := request.ProposedNewState.AsValueMap() + if id := values["id"]; id.AsString() == "plantimestamp" { + var err error + plantime, err = time.Parse(time.RFC3339, values["value"].AsString()) + if err != nil { + t.Errorf("couldn't parse plan time: %s", err) + } + } + + return providers.PlanResourceChangeResponse{ + PlannedState: request.ProposedNewState, + } + } + p.ApplyResourceChangeFn = func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + values := request.PlannedState.AsValueMap() + if id := values["id"]; id.AsString() == "timestamp" { + applytime, err := time.Parse(time.RFC3339, values["value"].AsString()) + if err != nil { + t.Errorf("couldn't parse apply time: %s", err) + } + + if applytime.Before(plantime) { + t.Errorf("applytime (%s) should be after plantime (%s)", applytime.Format(time.RFC3339), plantime.Format(time.RFC3339)) + } + } else if id.AsString() == "plantimestamp" { + otherplantime, err := time.Parse(time.RFC3339, values["value"].AsString()) + if err != nil { + t.Errorf("couldn't parse plan time: %s", err) + } + + if !plantime.Equal(otherplantime) { + t.Errorf("plantime changed from (%s) to (%s) during apply", plantime.Format(time.RFC3339), otherplantime.Format(time.RFC3339)) + } + } + + return providers.ApplyResourceChangeResponse{ + NewState: request.PlannedState, + } + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Apply_destroyUnusedModuleProvider(t *testing.T) { + // an unsued provider within a module should not be called during destroy + unusedProvider := testProvider("unused") + testProvider := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + addrs.NewDefaultProvider("unused"): testProviderFuncFixed(unusedProvider), + }, + }) + + unusedProvider.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + resp.Diagnostics = resp.Diagnostics.Append(errors.New("configuration failed")) + return resp + } + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + +resource "test_resource" "test" { +} +`, + + "mod/main.tf": ` +provider "unused" { +} + +resource "unused_resource" "test" { +} +`, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.DestroyMode, + }) + tfdiags.AssertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Apply_import_ID(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "a" { + id = "importable" +} + +import { + to = test_resource.a + id = "importable" +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + } + } + p.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_instance", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("importable"), + }), + }, + }, + } + } + hook := new(MockHook) + ctx := testContext2(t, &ContextOpts{ + Hooks: []Hook{hook}, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + if !hook.PreApplyImportCalled { + t.Fatalf("PreApplyImport hook not called") + } + if addr, wantAddr := hook.PreApplyImportAddr, mustResourceInstanceAddr("test_resource.a"); !addr.Equal(wantAddr) { + t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) + } + + if !hook.PostApplyImportCalled { + t.Fatalf("PostApplyImport hook not called") + } + if addr, wantAddr := hook.PostApplyImportAddr, mustResourceInstanceAddr("test_resource.a"); !addr.Equal(wantAddr) { + t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) + } +} + +func TestContext2Apply_import_identity(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "a" { + id = "importable" +} + +import { + to = test_resource.a + identity = { + id = "importable" + } +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }) + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + } + } + p.ImportResourceStateFn = func(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_instance", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("importable"), + }), + Identity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("importable"), + }), + }, + }, + } + } + hook := new(MockHook) + ctx := testContext2(t, &ContextOpts{ + Hooks: []Hook{hook}, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + if !hook.PreApplyImportCalled { + t.Fatalf("PreApplyImport hook not called") + } + if addr, wantAddr := hook.PreApplyImportAddr, mustResourceInstanceAddr("test_resource.a"); !addr.Equal(wantAddr) { + t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) + } + + if !hook.PostApplyImportCalled { + t.Fatalf("PostApplyImport hook not called") + } + if addr, wantAddr := hook.PostApplyImportAddr, mustResourceInstanceAddr("test_resource.a"); !addr.Equal(wantAddr) { + t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) + } +} + +func TestContext2Apply_destroySkipsVariableValidations(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "input" { + type = string + + validation { + condition = var.input == "foo" + error_message = "bad input" + } +} + +# In order for the variable to be validated during destroy, it must be required +# by the destroy plan. This is done by having the test provider require the +# value in order to destroy the test_object instance. +provider "test" { + test_string = var.input +} + +resource "test_object" "a" { + test_string = var.input +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), &PlanOpts{ + Mode: plans.DestroyMode, + SetVariables: InputValues{ + "input": { + Value: cty.StringVal("foo"), + SourceType: ValueFromCLIArg, + SourceRange: tfdiags.SourceRange{}, + }, + }, + }) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } + + planResult := plan.Checks.GetObjectResult(addrs.AbsInputVariableInstance{ + Variable: addrs.InputVariable{ + Name: "input", + }, + Module: addrs.RootModuleInstance, + }) + + if planResult.Status != checks.StatusPass { + // Should have passed during the planning stage indicating that it did + // actually execute. + t.Errorf("expected checks to be pass but was %s", planResult.Status) + } + + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } + + applyResult := state.CheckResults.GetObjectResult(addrs.AbsInputVariableInstance{ + Variable: addrs.InputVariable{ + Name: "input", + }, + Module: addrs.RootModuleInstance, + }) + + if applyResult.Status != checks.StatusUnknown { + // Shouldn't have made any validations here, so result should have + // stayed as unknown. + t.Errorf("expected checks to be unknown but was %s", applyResult.Status) + } +} + +func TestContext2Apply_pruneNoExternalReferences(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +locals { + local_value = test_object.a.test_string +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + addrA := mustResourceInstanceAddr("test_object.a") + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"test_string":"foo"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } + + g, _, diags := ctx.applyGraph(plan, m, &ApplyOpts{}, true) + tfdiags.AssertNoDiagnostics(t, diags) + + // The local value should've been pruned from the graph because nothing + // refers to it and this was a destroy run. + gotGraph := g.String() + wantGraph := `provider["registry.terraform.io/hashicorp/test"] +provider["registry.terraform.io/hashicorp/test"] (close) + test_object.a (destroy) +root + provider["registry.terraform.io/hashicorp/test"] (close) +test_object.a (destroy) + provider["registry.terraform.io/hashicorp/test"] +` + if diff := cmp.Diff(wantGraph, gotGraph); diff != "" { + t.Errorf("wrong apply graph\n%s", diff) + } + + _, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } +} + +func TestContext2Apply_pruneWithExternalReferences(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +locals { + local_value = test_object.a.test_string +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + addrA := mustResourceInstanceAddr("test_object.a") + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"test_string":"foo"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + ExternalReferences: []*addrs.Reference{ + mustReference("local.local_value"), + }, + }) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } + + g, _, diags := ctx.applyGraph(plan, m, &ApplyOpts{}, true) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } + + // The local value should remain in the graph because the external + // reference uses it. + gotGraph := g.String() + wantGraph := `provider["registry.terraform.io/hashicorp/test"] +provider["registry.terraform.io/hashicorp/test"] (close) + test_object.a (destroy) +root + provider["registry.terraform.io/hashicorp/test"] (close) +test_object.a (destroy) + provider["registry.terraform.io/hashicorp/test"] +` + if diff := cmp.Diff(wantGraph, gotGraph); diff != "" { + t.Errorf("wrong graph\n%s", diff) + } + + _, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } +} + +func TestContext2Apply_pruneNonDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +locals { + local_value = test_object.a.test_string +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } + + g, _, diags := ctx.applyGraph(plan, m, &ApplyOpts{}, true) + tfdiags.AssertNoDiagnostics(t, diags) + + // Although nothing refers to the local value, it should remain in the graph + // because this was NOT a destroy run and the prune transform exits early. + gotGraph := g.String() + wantGraph := `local.local_value (expand) + test_object.a +provider["registry.terraform.io/hashicorp/test"] +provider["registry.terraform.io/hashicorp/test"] (close) + test_object.a +root + local.local_value (expand) + provider["registry.terraform.io/hashicorp/test"] (close) +test_object.a + test_object.a (expand) +test_object.a (expand) + provider["registry.terraform.io/hashicorp/test"] +` + if diff := cmp.Diff(wantGraph, gotGraph); diff != "" { + t.Errorf("wrong apply graph\n%s", diff) + } + + _, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Errorf("expected no errors, but got %s", diags) + } +} + +func TestContext2Apply_mockProvider(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +provider "test" {} + +data "test_object" "foo" {} + +resource "test_object" "foo" { + value = data.test_object.foo.output +} +`, + }) + + // Manually mark the provider config as being mocked. + m.Module.ProviderConfigs["test"].Mock = true + m.Module.ProviderConfigs["test"].MockData = &configs.MockData{ + MockDataSources: map[string]*configs.MockResource{ + "test_object": { + Mode: addrs.DataResourceMode, + Type: "test_object", + Defaults: cty.ObjectVal(map[string]cty.Value{ + "output": cty.StringVal("expected data output"), + }), + }, + }, + MockResources: map[string]*configs.MockResource{ + "test_object": { + Mode: addrs.ManagedResourceMode, + Type: "test_object", + Defaults: cty.ObjectVal(map[string]cty.Value{ + "output": cty.StringVal("expected resource output"), + }), + }, + }, + } + + testProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + reachedReadDataSourceFn := false + reachedPlanResourceChangeFn := false + reachedApplyResourceChangeFn := false + testProvider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { + reachedReadDataSourceFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.StringVal("unexpected data output") + resp.State = cty.ObjectVal(cfg) + return resp + } + testProvider.PlanResourceChangeFn = func(request providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + reachedPlanResourceChangeFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.UnknownVal(cty.String) + resp.PlannedState = cty.ObjectVal(cfg) + return resp + } + testProvider.ApplyResourceChangeFn = func(request providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + reachedApplyResourceChangeFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.StringVal("unexpected resource output") + resp.NewState = cty.ObjectVal(cfg) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags) + } + + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags) + } + + // Check we never made it to the actual provider. + if reachedReadDataSourceFn { + t.Errorf("read the data source in the provider when it should have been mocked") + } + if reachedPlanResourceChangeFn { + t.Errorf("planned the resource in the provider when it should have been mocked") + } + if reachedApplyResourceChangeFn { + t.Errorf("applied the resource in the provider when it should have been mocked") + } + + // Check we got the right data back from our mocked provider. + instance := state.ResourceInstance(mustResourceInstanceAddr("test_object.foo")) + expected := "{\"output\":\"expected resource output\",\"value\":\"expected data output\"}" + if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff) + } +} + +func TestContext2Apply_mockProviderRequiredSchema(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +provider "test" {} + +data "test_object" "foo" {} + +resource "test_object" "foo" { + value = data.test_object.foo.output +} +`, + }) + + // Manually mark the provider config as being mocked. + m.Module.ProviderConfigs["test"].Mock = true + m.Module.ProviderConfigs["test"].MockData = &configs.MockData{ + MockDataSources: map[string]*configs.MockResource{ + "test_object": { + Mode: addrs.DataResourceMode, + Type: "test_object", + Defaults: cty.ObjectVal(map[string]cty.Value{ + "output": cty.StringVal("expected data output"), + }), + }, + }, + MockResources: map[string]*configs.MockResource{ + "test_object": { + Mode: addrs.ManagedResourceMode, + Type: "test_object", + Defaults: cty.ObjectVal(map[string]cty.Value{ + "output": cty.StringVal("expected resource output"), + }), + }, + }, + } + + // This time our test provider has a required attribute that we don't + // provide in the configuration. The fact we've marked this provider as a + // mock means the missing required attribute doesn't matter. + + testProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "required": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + reachedReadDataSourceFn := false + reachedPlanResourceChangeFn := false + reachedApplyResourceChangeFn := false + testProvider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) (resp providers.ReadDataSourceResponse) { + reachedReadDataSourceFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.StringVal("unexpected data output") + resp.State = cty.ObjectVal(cfg) + return resp + } + testProvider.PlanResourceChangeFn = func(request providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + reachedPlanResourceChangeFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.UnknownVal(cty.String) + resp.PlannedState = cty.ObjectVal(cfg) + return resp + } + testProvider.ApplyResourceChangeFn = func(request providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + reachedApplyResourceChangeFn = true + cfg := request.Config.AsValueMap() + cfg["output"] = cty.StringVal("unexpected resource output") + resp.NewState = cty.ObjectVal(cfg) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags) + } + + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags) + } + + // Check we never made it to the actual provider. + if reachedReadDataSourceFn { + t.Errorf("read the data source in the provider when it should have been mocked") + } + if reachedPlanResourceChangeFn { + t.Errorf("planned the resource in the provider when it should have been mocked") + } + if reachedApplyResourceChangeFn { + t.Errorf("applied the resource in the provider when it should have been mocked") + } + + // Check we got the right data back from our mocked provider. + instance := state.ResourceInstance(mustResourceInstanceAddr("test_object.foo")) + expected := "{\"output\":\"expected resource output\",\"value\":\"expected data output\"}" + if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff) + } +} + +func TestContext2Apply_forget(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +removed { + from = test_object.a + lifecycle { + destroy = false + } +} +`}) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + // check that the provider was not asked to refresh the resource + if p.ReadResourceCalled { + t.Fatalf("Expected ReadResource not to be called, but it was called") + } + + // check that the provider was not asked to destroy the resource + if p.ApplyResourceChangeCalled { + t.Fatalf("Expected ApplyResourceChange not to be called, but it was called") + } + + checkStateString(t, state, ``) +} + +func TestContext2Apply_forgetDeposed(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + deposedKey := states.DeposedKey("gone") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +removed { + from = test_object.a + lifecycle { + destroy = false + } +} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceDeposed(addrA, deposedKey, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + // check that the provider was not asked to refresh the resource + if p.ReadResourceCalled { + t.Fatalf("Expected ReadResource not to be called, but it was called") + } + + // check that the provider was not asked to destroy the resource + if p.ApplyResourceChangeCalled { + t.Fatalf("Expected ApplyResourceChange not to be called, but it was called") + } + + checkStateString(t, state, ``) +} + +// TestContext2Apply_destroy_and_forget tests that a destroy plan with the forget flag set to true. +// The expectation is that all resources should be forgotten and not destroyed. +func TestContext2Apply_destroy_and_forget(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + addrB := mustResourceInstanceAddr("test_object.b") + addrAFirst := mustResourceInstanceAddr(`test_object.a["first"]`) + addrASecond := mustResourceInstanceAddr(`test_object.a["second"]`) + addrAThird := mustResourceInstanceAddr(`test_object.a["third"]`) + + testCases := []struct { + name string + config string + buildState func(*states.SyncState) + + expectedChangeAddresses []string + }{ + { + name: "standard", + config: ` + resource "test_object" "a" { + test_string = "foo" + } + + resource "test_object" "b" { + test_string = "foo" + } + `, + buildState: func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }, + + expectedChangeAddresses: []string{addrA.String(), addrB.String()}, + }, + { + name: "in state but not in config", + config: ` + resource "test_object" "a" { + test_string = "foo" + } + `, + buildState: func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }, + + expectedChangeAddresses: []string{addrA.String(), addrB.String()}, + }, + { + name: "orphaned expanded resource", + config: ` + locals { + items = toset(["first", "third"]) + } + resource "test_object" "a" { + for_each = local.items + + test_string = each.value + } + `, + buildState: func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrAFirst, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(addrASecond, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(addrAThird, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }, + + expectedChangeAddresses: []string{addrAFirst.String(), addrASecond.String(), addrAThird.String()}, + }, + { + name: "deposed resource", + config: ` + resource "test_object" "a" { + test_string = "foo" + } + `, + buildState: func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceDeposed(addrA, states.DeposedKey("uhoh"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }, + + expectedChangeAddresses: []string{addrA.String(), addrA.String()}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + m := testModuleInline(t, map[string]string{ + "main.tf": testCase.config, + }) + + state := states.BuildState(testCase.buildState) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + Forget: true, + }) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + actualChangeAddresses := make([]string, len(plan.Changes.Resources)) + // We expect a forget action for each resource + for i, change := range plan.Changes.Resources { + actualChangeAddresses[i] = change.Addr.String() + if change.Action != plans.Forget { + t.Fatalf("Expected all actions to be forget, but got %s at plan.Changes.Resources[%d]", change.Action, i) + } + } + + // Sort ahead of comparison to avoid order issues + sort.Strings(actualChangeAddresses) + sort.Strings(testCase.expectedChangeAddresses) + + if diff := cmp.Diff(actualChangeAddresses, testCase.expectedChangeAddresses); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", testCase.expectedChangeAddresses, actualChangeAddresses, diff) + } + + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + // check that the provider was not asked to destroy the resource + if p.ApplyResourceChangeCalled { + t.Fatalf("Expected ApplyResourceChange not to be called, but it was called") + } + + checkStateString(t, state, ``) + }) + } +} + +func TestContext2Apply_destroy_and_forget_single_resource(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + + m := testModuleInline(t, map[string]string{ + "main.tf": ` + removed { + from = test_object.a + + lifecycle { + destroy = false + } + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceDeposed(addrA, states.DeposedKey("uhoh"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + actualChangeAddresses := make([]string, len(plan.Changes.Resources)) + // We expect a forget action for each resource + for i, change := range plan.Changes.Resources { + actualChangeAddresses[i] = change.Addr.String() + if change.Action != plans.Forget { + t.Fatalf("Expected all actions to be forget, but got %s at plan.Changes.Resources[%d]", change.Action, i) + } + } + + // Sort ahead of comparison to avoid order issues + sort.Strings(actualChangeAddresses) + expectedAddresses := []string{addrA.String(), addrA.String()} + + if diff := cmp.Diff(actualChangeAddresses, expectedAddresses); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expectedAddresses, actualChangeAddresses, diff) + } + + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + // check that the provider was not asked to destroy the resource + if p.ApplyResourceChangeCalled { + t.Fatalf("Expected ApplyResourceChange not to be called, but it was called") + } + + checkStateString(t, state, ``) + +} + +func TestContext2Apply_sensitiveInputVariableValue(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "a" { + type = string + # this variable is not marked sensitive +} + +resource "test_resource" "a" { + value = var.a +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Build state with sensitive value in resource object + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"value":"secret"}]}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + }, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + // Create a sensitive-marked value for the input variable. This is not + // possible through the normal CLI path, but is possible when the plan is + // created and modified by the stacks runtime. + secret := cty.StringVal("updated").Mark(marks.Sensitive) + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "a": &InputValue{ + Value: secret, + SourceType: ValueFromUnknown, + }, + }, + }) + tfdiags.AssertNoErrors(t, diags) + + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + // check that the provider was not asked to destroy the resource + if !p.ApplyResourceChangeCalled { + t.Fatalf("Expected ApplyResourceChange to be called, but it was not called") + } + + instance := state.ResourceInstance(mustResourceInstanceAddr("test_resource.a")) + expected := "{\"value\":\"updated\"}" + if diff := cmp.Diff(string(instance.Current.AttrsJSON), expected); len(diff) > 0 { + t.Errorf("expected:\n%s\nactual:\n%s\ndiff:\n%s", expected, string(instance.Current.AttrsJSON), diff) + } + expectedSensitivePaths := []cty.Path{ + cty.GetAttrPath("value"), + } + if diff := cmp.Diff(expectedSensitivePaths, instance.Current.AttrSensitivePaths, ctydebug.CmpOptions); len(diff) > 0 { + t.Errorf("unexpected sensitive paths\ndiff:\n%s", diff) + } +} + +func TestContext2Apply_sensitiveNestedComputedAttributes(t *testing.T) { + // Ensure we're not trying to double-mark values decoded from state + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_object": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "list": { + Computed: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "secret": { + Type: cty.String, + Computed: true, + Sensitive: true, + }, + }, + }, + }, + }, + }, + }, + }) + p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + obj := req.PlannedState.AsValueMap() + obj["list"] = cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "secret": cty.StringVal("secret"), + }), + }) + obj["id"] = cty.StringVal("id") + resp.NewState = cty.ObjectVal(obj) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + if len(state.ResourceInstance(mustResourceInstanceAddr("test_object.a")).Current.AttrSensitivePaths) < 1 { + t.Fatal("no attributes marked as sensitive in state") + } + + plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + if c := plan.Changes.ResourceInstance(mustResourceInstanceAddr("test_object.a")); c.Action != plans.NoOp { + t.Errorf("Unexpected %s change for %s", c.Action, c.Addr) + } +} + +// This test explicitly reproduces the issue described in #34976. +func TestContext2Apply_34976(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "a" { + source = "./mod" + count = 1 +} + +resource "test_object" "obj" { + test_number = length(module.a) +} +`, + "mod/main.tf": ``, // just an empty module + }) + + p := simpleMockProvider() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + // Just don't crash. + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Apply_applyingFlag(t *testing.T) { + // This test is for references to the symbol "terraform.applying", which + // is an ephemeral value that's true during an apply phase but false in + // all other phases. + + m := testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + test = { + source = "terraform.io/builtin/test" + } + } + } + + provider "test" { + applying = terraform.applying + } + + resource "test_thing" "placeholder" { + # This is here just to give Terraform a reason to configure + # the provider. + } + `, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "applying": { + Type: cty.Bool, + Required: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_thing": { + Body: &configschema.Block{}, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + if !p.ConfigureProviderCalled { + t.Fatalf("ConfigureProvider was not called during planning") + } + { + got := p.ConfigureProviderRequest.Config + want := cty.ObjectVal(map[string]cty.Value{ + "applying": cty.False, // false during the planning phase + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong provider configuration during planning\n%s", diff) + } + } + + // reset the mock provider so we can check it again after apply + p.ConfigureProviderCalled = false + p.ConfigureProviderRequest = providers.ConfigureProviderRequest{} + + _, diags = ctx.Apply(plan, m, &ApplyOpts{}) + tfdiags.AssertNoErrors(t, diags) + + if !p.ConfigureProviderCalled { + t.Fatalf("ConfigureProvider was not called while applying") + } + { + got := p.ConfigureProviderRequest.Config + want := cty.ObjectVal(map[string]cty.Value{ + "applying": cty.True, // now true during the apply phase + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong provider configuration while applying\n%s", diff) + } + } +} + +func TestContext2Apply_applyTimeVariables(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + variable "e" { + type = string + default = null + ephemeral = true + } + + variable "p" { + type = string + default = null + } + `, + }) + + t.Run("set during plan", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{}) + plan, diags := ctx.Plan( + m, states.NewState(), + SimplePlanOpts(plans.NormalMode, InputValues{ + "e": {Value: cty.StringVal("e value")}, + "p": {Value: cty.StringVal("p value")}, + }), + ) + tfdiags.AssertNoErrors(t, diags) + + { + got := plan.ApplyTimeVariables + want := collections.NewSetCmp[string]("e") + if diff := cmp.Diff(want, got, collections.CmpOptions); diff != "" { + t.Errorf("wrong apply-time variables\n%s", diff) + } + } + { + got := plan.VariableValues + want := map[string]plans.DynamicValue{ + // The following is a msgpack-encoded representation of + // the type and value of the variable. + "p": plans.DynamicValue("\x92\xc4\x08\x22string\x22\xa7p value"), + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong persisted variables\n%s", diff) + } + } + + _, diags = ctx.Apply(plan, m, &ApplyOpts{ + // Intentionally not setting any variables for this first + // check, which should therefore fail. + }) + if !diags.HasErrors() { + t.Fatal("apply succeeded without value for 'e'; should have failed") + } + + _, diags = ctx.Apply(plan, m, &ApplyOpts{ + SetVariables: InputValues{ + "e": {Value: cty.StringVal("different e value")}, + }, + }) + tfdiags.AssertNoErrors(t, diags) + }) + + t.Run("unset during plan", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{}) + plan, diags := ctx.Plan( + m, states.NewState(), + SimplePlanOpts(plans.NormalMode, InputValues{ + "e": {Value: cty.NilVal}, + "p": {Value: cty.StringVal("p value")}, + }), + ) + tfdiags.AssertNoErrors(t, diags) + + { + got := plan.ApplyTimeVariables + want := collections.NewSetCmp[string]( /* none */ ) + if diff := cmp.Diff(want, got, collections.CmpOptions); diff != "" { + t.Errorf("wrong apply-time variables\n%s", diff) + } + } + { + got := plan.VariableValues + want := map[string]plans.DynamicValue{ + // The following is a msgpack-encoded representation of + // the type and value of the variable. + "p": plans.DynamicValue("\x92\xc4\x08\x22string\x22\xa7p value"), + } + if diff := cmp.Diff(want, got); diff != "" { + t.Errorf("wrong persisted variables\n%s", diff) + } + } + + _, diags = ctx.Apply(plan, m, &ApplyOpts{ + SetVariables: InputValues{ + // 'e' was unset during planning, so this is invalid because + // it must remain unset during apply too. + "e": {Value: cty.StringVal("surprising e value")}, + }, + }) + if !diags.HasErrors() { + t.Fatal("apply succeeded with invalid new value for 'e'; should have failed") + } + + _, diags = ctx.Apply(plan, m, &ApplyOpts{ + // Applying with 'e' still unset should be valid. + }) + tfdiags.AssertNoErrors(t, diags) + }) +} + +func TestContext2Apply_35039(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "obj" { + list = ["a", "b", "c"] +} +`, + }) + + p := testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "output": { + Type: cty.String, + Computed: true, + }, + "list": { + Type: cty.List(cty.String), + Required: true, + Sensitive: true, + }, + }, + }, + }, + }, + } + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "output": cty.UnknownVal(cty.String), + "list": req.ProposedNewState.GetAttr("list"), + }), + } + } + p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + // This is a bug, the provider shouldn't return unknown values from + // ApplyResourceChange. But, Terraform shouldn't crash in response + // to this. It should return a nice error message. + NewState: req.PlannedState, + } + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(&p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + // Just don't crash, should report an error about the provider. + _, diags = ctx.Apply(plan, m, nil) + if len(diags) != 1 { + t.Fatalf("expected exactly one diagnostic, but got %d: %s", len(diags), diags) + } +} + +// Using refresh=false when create_before_destroy disagrees between state and +// config, should still destroy instance. +func TestContext2Apply_35218(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_instance" "obj" { + // was created with create_before_destroy=true + lifecycle { + // create_before_destroy=true + } + value = "replace" +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse.ServerCapabilities.PlanDestroy = true + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + if req.ProposedNewState.IsNull() { + // plan destroy + resp.PlannedState = req.ProposedNewState + return resp + } + + obj := req.ProposedNewState.AsValueMap() + if obj["id"].IsNull() { + obj["id"] = cty.UnknownVal(cty.String) + resp.PlannedState = cty.ObjectVal(obj) + return resp + } + + // plan to replace the configured instance + resp.PlannedState = cty.ObjectVal(obj) + resp.RequiresReplace = []cty.Path{cty.GetAttrPath("value")} + return resp + } + + destroyCalled := false + p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + if req.PlannedState.IsNull() { + destroyCalled = true + resp.NewState = req.PlannedState + return resp + } + + obj := req.PlannedState.AsValueMap() + obj["id"] = cty.StringVal("new_id") + resp.NewState = cty.ObjectVal(obj) + return resp + } + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_instance.obj"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"old_id"}`), + Status: states.ObjectReady, + CreateBeforeDestroy: true, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + SkipRefresh: true, + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + if !destroyCalled { + t.Fatal("old instance not destroyed") + } +} + +func TestContext2Apply_updateForcedCreateBeforeDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { +} + +resource "test_object" "b" { + ref = test_object.a.id + update = "new" +} + +resource "test_object" "c" { + ref = test_object.b.id + lifecycle { + create_before_destroy = true + } +} +`, + }) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "ref": { + Type: cty.String, + Optional: true, + }, + "update": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.a"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"a"}`), + Status: states.ObjectReady, + CreateBeforeDestroy: true, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.b"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"b","ref":"a","update":"old"}`), + Status: states.ObjectReady, + CreateBeforeDestroy: true, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.c"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"c","ref":"b"}`), + Status: states.ObjectReady, + CreateBeforeDestroy: true, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + state, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + for _, res := range state.RootModule().Resources { + if !res.Instances[addrs.NoKey].Current.CreateBeforeDestroy { + t.Errorf("%s should be create_before_destroy", res.Addr) + } + } +} + +func TestContext2Apply_transitiveDestroyOrder(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + replace = "first" +} + +resource "test_object" "b" { + ref = test_object.a.id +} + +resource "test_object" "c" { + replace = test_object.b.ref +} +`}) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "ref": { + Type: cty.String, + Optional: true, + }, + "replace": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + obj := req.ProposedNewState.AsValueMap() + if req.PriorState.IsNull() { + obj["id"] = cty.UnknownVal(cty.String) + } else { + replace := req.PriorState.GetAttr("replace") + if !replace.RawEquals(obj["replace"]) { + resp.RequiresReplace = append(resp.RequiresReplace, cty.GetAttrPath("replace")) + } + } + resp.PlannedState = cty.ObjectVal(obj) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // we're going to plan and apply the config rather than build a test state, + // because because the test also depends on how the dependencies are stored + // during the plan. + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + // update the config to force replacement on a, c, and an update with b + m = testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + replace = "second" +} + +resource "test_object" "b" { + ref = test_object.a.id +} + +resource "test_object" "c" { + replace = test_object.b.ref +} +`}) + + plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + // grab the graph we build during apply to check the actual dependencies, + // rather than the observed order which may not be stable if the + // dependencies are not correct. + g, _, diags := ctx.applyGraph(plan, m, nil, false) + tfdiags.AssertNoErrors(t, diags) + + // the destroy node for "a" must depend on the destroy node for "c" + for _, v := range g.Vertices() { + if dag.VertexName(v) != "test_object.a (destroy)" { + continue + } + + // make sure the "c" destroy node is a dependency + for _, dep := range g.Ancestors(v) { + if dag.VertexName(dep) == "test_object.c (destroy)" { + // OK! + return + } + } + } + t.Fatal("failed to find destroy destroy dependency between test_object.a(destroy) and test_object.c(destroy)") +} + +func TestContext2Apply_writeOnlyDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "x" { + test_string = "ok" + test_wo = "secret" +}`, + }) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + ResourceTypes: map[string]providers.Schema{ + "test_object": providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "test_string": { + Type: cty.String, + Optional: true, + }, + "test_wo": { + Type: cty.Number, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.x").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"ok", "test_wo": null}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + // we don't want to refresh, because that actually runs a normal plan + SkipRefresh: true, + }) + if diags.HasErrors() { + t.Fatalf("plan: %s", diags.Err()) + } + + _, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("apply: %s", diags.Err()) + } +} + +func TestContext2Apply_writeOnlyApplyError(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "x" { + test_string = "ok" + test_wo = "secret" +}`, + }) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + ResourceTypes: map[string]providers.Schema{ + "test_object": providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "test_string": { + Type: cty.String, + Optional: true, + }, + "test_wo": { + Type: cty.Number, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + + p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + resp.Diagnostics = resp.Diagnostics.Append(errors.New("provider oops")) + return resp + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.x").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"ok", "test_wo": null}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + // we don't want to refresh, because that actually runs a normal plan + SkipRefresh: true, + }) + if diags.HasErrors() { + t.Fatalf("plan: %s", diags.Err()) + } + + _, diags = ctx.Apply(plan, m, nil) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + msg := diags.ErrWithWarnings().Error() + if len(diags) != 1 && !strings.Contains(msg, "provider oops") { + t.Fatalf("expected only 'provider oops', but got: %s", msg) + } +} diff --git a/internal/terraform/context_apply_checks_test.go b/internal/terraform/context_apply_checks_test.go new file mode 100644 index 0000000000..30469e7c83 --- /dev/null +++ b/internal/terraform/context_apply_checks_test.go @@ -0,0 +1,872 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// This file contains 'integration' tests for the Terraform check blocks. +// +// These tests could live in context_apply_test or context_apply2_test but given +// the size of those files, it makes sense to keep these check related tests +// grouped together. + +type checksTestingStatus struct { + status checks.Status + messages []string +} + +func TestContextChecks(t *testing.T) { + tests := map[string]struct { + configs map[string]string + plan map[string]checksTestingStatus + planError string + planWarning string + apply map[string]checksTestingStatus + applyError string + applyWarning string + state *states.State + provider *testing_provider.MockProvider + providerHook func(*testing_provider.MockProvider) + }{ + "passing": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "passing" { + data "checks_object" "positive" {} + + assert { + condition = data.checks_object.positive.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + apply: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + }, + }, + "failing": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "failing" { + data "checks_object" "positive" {} + + assert { + condition = data.checks_object.positive.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + planWarning: "Check block assertion failed: negative number", + apply: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + applyWarning: "Check block assertion failed: negative number", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(-1), + }), + } + }, + }, + }, + "mixed": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "failing" { + data "checks_object" "neutral" {} + + assert { + condition = data.checks_object.neutral.number >= 0 + error_message = "negative number" + } + + assert { + condition = data.checks_object.neutral.number < 0 + error_message = "positive number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"positive number"}, + }, + }, + planWarning: "Check block assertion failed: positive number", + apply: map[string]checksTestingStatus{ + "failing": { + status: checks.StatusFail, + messages: []string{"positive number"}, + }, + }, + applyWarning: "Check block assertion failed: positive number", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + }, + }, + "nested data blocks reload during apply": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +data "checks_object" "data_block" {} + +check "data_block" { + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} + +check "nested_data_block" { + data "checks_object" "nested_data_block" {} + + assert { + condition = data.checks_object.nested_data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "nested_data_block": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + "data_block": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + planWarning: "2 warnings:\n\n- Check block assertion failed: negative number\n- Check block assertion failed: negative number", + apply: map[string]checksTestingStatus{ + "nested_data_block": { + status: checks.StatusPass, + }, + "data_block": { + status: checks.StatusFail, + messages: []string{"negative number"}, + }, + }, + applyWarning: "Check block assertion failed: negative number", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(-1), + }), + } + }, + }, + providerHook: func(provider *testing_provider.MockProvider) { + provider.ReadDataSourceFn = func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + // The data returned by the data sources are changing + // between the plan and apply stage. The nested data block + // will update to reflect this while the normal data block + // will not detect the change. + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + } + }, + }, + "returns unknown for unknown config": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +resource "checks_object" "resource_block" {} + +check "resource_block" { + data "checks_object" "data_block" { + id = checks_object.resource_block.id + } + + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "resource_block": { + status: checks.StatusUnknown, + }, + }, + planWarning: "Check block assertion known after apply: The condition could not be evaluated at this time, a result will be known when this plan is applied.", + apply: map[string]checksTestingStatus{ + "resource_block": { + status: checks.StatusPass, + }, + }, + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + } + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("7A9F887D-44C7-4281-80E5-578E41F99DFC"), + }), + } + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + values := request.Config.AsValueMap() + if id, ok := values["id"]; ok { + if id.IsKnown() && id.AsString() == "7A9F887D-44C7-4281-80E5-578E41F99DFC" { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("7A9F887D-44C7-4281-80E5-578E41F99DFC"), + "number": cty.NumberIntVal(0), + }), + } + } + } + + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "shouldn't make it here", "really shouldn't make it here")}, + } + }, + }, + }, + "failing nested data source doesn't block the plan": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +check "error" { + data "checks_object" "data_block" {} + + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + plan: map[string]checksTestingStatus{ + "error": { + status: checks.StatusFail, + messages: []string{ + "data source read failed: something bad happened and the provider couldn't read the data source", + }, + }, + }, + planWarning: "data source read failed: something bad happened and the provider couldn't read the data source", + apply: map[string]checksTestingStatus{ + "error": { + status: checks.StatusFail, + messages: []string{ + "data source read failed: something bad happened and the provider couldn't read the data source", + }, + }, + }, + applyWarning: "data source read failed: something bad happened and the provider couldn't read the data source", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, + } + }, + }, + }, "failing nested data source should prevent checks from executing": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +resource "checks_object" "resource_block" { + number = -1 +} + +check "error" { + data "checks_object" "data_block" {} + + assert { + condition = checks_object.resource_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "checks_object", + Name: "resource_block", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"number": -1}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("checks"), + Module: addrs.RootModule, + }) + }), + plan: map[string]checksTestingStatus{ + "error": { + status: checks.StatusFail, + messages: []string{ + "data source read failed: something bad happened and the provider couldn't read the data source", + }, + }, + }, + planWarning: "data source read failed: something bad happened and the provider couldn't read the data source", + apply: map[string]checksTestingStatus{ + "error": { + status: checks.StatusFail, + messages: []string{ + "data source read failed: something bad happened and the provider couldn't read the data source", + }, + }, + }, + applyWarning: "data source read failed: something bad happened and the provider couldn't read the data source", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(-1), + }), + } + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(-1), + }), + } + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, + } + }, + }, + }, + "check failing in state and passing after plan and apply": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +resource "checks_object" "resource" { + number = 0 +} + +check "passing" { + assert { + condition = checks_object.resource.number >= 0 + error_message = "negative number" + } +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "checks_object", + Name: "resource", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"number": -1}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("checks"), + Module: addrs.RootModule, + }) + }), + plan: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + apply: map[string]checksTestingStatus{ + "passing": { + status: checks.StatusPass, + }, + }, + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + return providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(0), + }), + } + }, + }, + }, + "failing data source does block the plan": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +data "checks_object" "data_block" {} + +check "error" { + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + planError: "data source read failed: something bad happened and the provider couldn't read the data source", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "number": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + Diagnostics: tfdiags.Diagnostics{tfdiags.Sourceless(tfdiags.Error, "data source read failed", "something bad happened and the provider couldn't read the data source")}, + } + }, + }, + }, + "invalid reference into check block": { + configs: map[string]string{ + "main.tf": ` +provider "checks" {} + +data "checks_object" "data_block" { + id = data.checks_object.nested_data_block.id +} + +check "error" { + data "checks_object" "nested_data_block" {} + + assert { + condition = data.checks_object.data_block.number >= 0 + error_message = "negative number" + } +} +`, + }, + planError: "Reference to scoped resource: The referenced data resource \"checks_object\" \"nested_data_block\" is not available from this context.", + provider: &testing_provider.MockProvider{ + Meta: "checks", + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + DataSources: map[string]providers.Schema{ + "checks_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + Optional: true, + }, + }, + }, + }, + }, + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + input := request.Config.AsValueMap() + if _, ok := input["id"]; ok { + return providers.ReadDataSourceResponse{ + State: request.Config, + } + } + + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + } + }, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + configs := testModuleInline(t, test.configs) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider(test.provider.Meta.(string)): testProviderFuncFixed(test.provider), + }, + }) + + initialState := states.NewState() + if test.state != nil { + initialState = test.state + } + + plan, diags := ctx.Plan(configs, initialState, &PlanOpts{ + Mode: plans.NormalMode, + }) + if validateCheckDiagnostics(t, "planning", test.planWarning, test.planError, diags) { + return + } + validateCheckResults(t, "planning", test.plan, plan.Checks) + + if test.providerHook != nil { + // This gives an opportunity to change the behaviour of the + // provider between the plan and apply stages. + test.providerHook(test.provider) + } + + state, diags := ctx.Apply(plan, configs, nil) + if validateCheckDiagnostics(t, "apply", test.applyWarning, test.applyError, diags) { + return + } + validateCheckResults(t, "apply", test.apply, state.CheckResults) + }) + } +} + +func TestContextChecks_DoesNotPanicOnModuleExpansion(t *testing.T) { + // This is a bit of a special test, we're adding it to verify that + // https://github.com/hashicorp/terraform/issues/34062 is fixed. + // + // Essentially we make a check block in a child module that depends on a + // resource that has no changes. We don't care about the actual behaviour + // of the check block. We just don't want the apply operation to crash. + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "panic_at_the_disco" { + source = "./panic" +} +`, + "panic/main.tf": ` +resource "test_object" "object" { + test_string = "Hello, world!" +} + +check "check_should_not_panic" { + assert { + condition = test_object.object.test_string == "Hello, world!" + error_message = "condition violated" + } +} +`, + }) + + p := simpleMockProvider() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("module.panic_at_the_disco.test_object.object"), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"test_string":"Hello, world!"}`), + Status: states.ObjectReady, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) +} + +func validateCheckDiagnostics(t *testing.T, stage string, expectedWarning, expectedError string, actual tfdiags.Diagnostics) bool { + if expectedError != "" { + if !actual.HasErrors() { + t.Errorf("expected %s to error with \"%s\", but no errors were returned", stage, expectedError) + } else if expectedError != actual.Err().Error() { + t.Errorf("expected %s to error with \"%s\" but found \"%s\"", stage, expectedError, actual.Err()) + } + + // If we expected an error then we won't finish the rest of the test. + return true + } + + if expectedWarning != "" { + warnings := actual.ErrWithWarnings() + if actual.ErrWithWarnings() == nil { + t.Errorf("expected %s to warn with \"%s\", but no errors were returned", stage, expectedWarning) + } else if expectedWarning != warnings.Error() { + t.Errorf("expected %s to warn with \"%s\" but found \"%s\"", stage, expectedWarning, warnings) + } + } else { + if actual.ErrWithWarnings() != nil { + t.Errorf("expected %s to produce no diagnostics but found \"%s\"", stage, actual.ErrWithWarnings()) + } + } + + tfdiags.AssertNoErrors(t, actual) + return false +} + +func validateCheckResults(t *testing.T, stage string, expected map[string]checksTestingStatus, actual *states.CheckResults) { + + // Just a quick sanity check that the plan or apply process didn't create + // some non-existent checks. + if len(expected) != len(actual.ConfigResults.Keys()) { + t.Errorf("expected %d check results but found %d after %s", len(expected), len(actual.ConfigResults.Keys()), stage) + } + + // Now, lets make sure the checks all match what we expect. + for check, want := range expected { + results := actual.GetObjectResult(addrs.Check{ + Name: check, + }.Absolute(addrs.RootModuleInstance)) + + if results.Status != want.status { + t.Errorf("%s: wanted %s but got %s after %s", check, want.status, results.Status, stage) + } + + if len(want.messages) != len(results.FailureMessages) { + t.Errorf("%s: expected %d failure messages but had %d after %s", check, len(want.messages), len(results.FailureMessages), stage) + } + + max := len(want.messages) + if len(results.FailureMessages) > max { + max = len(results.FailureMessages) + } + + for ix := 0; ix < max; ix++ { + var expected, actual string + if ix < len(want.messages) { + expected = want.messages[ix] + } + if ix < len(results.FailureMessages) { + actual = results.FailureMessages[ix] + } + + // Order matters! + if actual != expected { + t.Errorf("%s: expected failure message at %d to be \"%s\" but was \"%s\" after %s", check, ix, expected, actual, stage) + } + } + + } +} diff --git a/internal/terraform/context_apply_deferred_test.go b/internal/terraform/context_apply_deferred_test.go new file mode 100644 index 0000000000..ba6ee6c5db --- /dev/null +++ b/internal/terraform/context_apply_deferred_test.go @@ -0,0 +1,4145 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "encoding/json" + "fmt" + "sync" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type deferredActionsTest struct { + // If true, this test will be skipped. + skip bool + + // The configuration to use for this test. The keys are the filenames. + configs map[string]string + + // The starting state for the first stage. This can be nil, and the test + // will create a new empty state if so. + state *states.State + + // This test will execute a plan-apply cycle for every entry in this + // slice. At each stage the plan and apply outputs will be validated + // against the expected values. + stages []deferredActionsTestStage +} + +type deferredActionsTestStage struct { + // The inputs at each plan-apply cycle. + inputs map[string]cty.Value + + // The values we want to be planned within each cycle. + wantPlanned map[string]cty.Value + + // The values we want to be deferred within each cycle. + wantDeferred map[string]ExpectedDeferred + + // The expected actions from the plan step. + wantActions map[string]plans.Action + + // The values we want to be applied during each cycle. If this is + // nil, then the apply step will be skipped. + wantApplied map[string]cty.Value + + // The values we want to be returned by the outputs. If applied is + // nil, then this should also be nil as the apply step will be + // skipped. + wantOutputs map[string]cty.Value + + // Whether the plan should be completed during this stage. + complete bool + + // Some of our tests produce expected warnings, set this to true to allow + // warnings to be present in the returned diagnostics. + allowWarnings bool + + // buildOpts is an optional field, that lets the test specify additional + // options to be used when building the plan. + buildOpts func(opts *PlanOpts) + + // wantDiagnostic is an optional field, that lets the test specify the + // expected diagnostics to be returned by the plan. + wantDiagnostic func(diags tfdiags.Diagnostics) bool +} + +type ExpectedDeferred struct { + Reason providers.DeferredReason + Action plans.Action +} + +var ( + // We build some fairly complex configurations here, so we'll use separate + // variables for each one outside of the test function itself for clarity. + + // dataForEachTest is a test for deferral of data sources due to unknown + // for_each values. Since data sources don't result in planned changes, + // deferral has to be observed indirectly by checking for deferral of + // downstream objects that would otherwise have no reason to be deferred. + dataForEachTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "each" { + type = set(string) +} + +# Partial-expanded and deferred due to unknown for_each +data "test" "a" { + for_each = var.each + + name = "a:${each.key}" +} + +# Instance deferred due to dependency on deferred data source +resource "test" "b" { + name = "b" + upstream_names = [for v in data.test.a : v.name] +} + +# Instance deferred due to dependency on deferred resource +data "test" "c" { + name = test.b.output +} + +output "from_data" { + value = [for v in data.test.a : v.output] +} + +output "from_resource" { + value = test.b.output +} +`, + }, + stages: []deferredActionsTestStage{ + // Stage 0. Unknown for_each in data source. The resource and + // outputs get transitively deferred. + { + inputs: map[string]cty.Value{ + "each": cty.DynamicVal, + }, + wantPlanned: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "output": cty.UnknownVal(cty.String), + "upstream_names": cty.UnknownVal(cty.Set(cty.String)), + }), + }, + wantActions: map[string]plans.Action{}, + wantDeferred: map[string]ExpectedDeferred{ + // Much like a data source with unknown config results in a + // planned Read action to be performed in the apply, a + // deferred data source results in a *deferred* Read action + // to be performed in a future plan/apply round. + "data.test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Read}, + "test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + "data.test.c": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Read}, + }, + wantApplied: map[string]cty.Value{}, + wantOutputs: map[string]cty.Value{ + // To start with: outputs that refer to deferred values are + // null values of some type. + + // The from_data output's value is the result of a [for] + // expression that maps over an object of objects (the value + // of the data.test.a block). Since the for_each keys of the + // whole data source object are unknown (and the keys are an + // inherent part of the object type), we can't say anything + // about the type... and thus, can't say anything about the + // type of the tuple value that the [for] would derive from it. + "from_data": cty.NullVal(cty.DynamicPseudoType), + // The from_resource output's value is just a string + // attribute from a singleton resource instance, but it's + // still null because the resource got deferred. + "from_resource": cty.NullVal(cty.String), + }, + complete: false, + allowWarnings: false, + }, + // Stage 1. Everything's known now, so it converges. + { + inputs: map[string]cty.Value{ + "each": cty.SetVal([]cty.Value{cty.StringVal("hey"), cty.StringVal("ho"), cty.StringVal("let's go")}), + }, + wantPlanned: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "output": cty.UnknownVal(cty.String), + "upstream_names": cty.SetVal([]cty.Value{cty.StringVal("a:hey"), cty.StringVal("a:ho"), cty.StringVal("a:let's go")}), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + // Not deferred anymore, but Read still gets delayed til + // apply due to unknown config. + "data.test.c": plans.Read, + }, + wantDeferred: map[string]ExpectedDeferred{}, + wantApplied: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "output": cty.StringVal("b"), + "upstream_names": cty.SetVal([]cty.Value{cty.StringVal("a:hey"), cty.StringVal("a:ho"), cty.StringVal("a:let's go")}), + }), + }, + wantOutputs: map[string]cty.Value{ + "from_data": cty.TupleVal([]cty.Value{cty.StringVal("a:hey"), cty.StringVal("a:ho"), cty.StringVal("a:let's go")}), + "from_resource": cty.StringVal("b"), + }, + complete: true, + allowWarnings: false, + }, + }, + } + + // dataCountTest is a test for deferral of data sources due to unknown + // count values. Since data sources don't result in planned changes, + // deferral has to be observed indirectly by checking for deferral of + // downstream objects that would otherwise have no reason to be deferred. + dataCountTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "data_count" { + type = number +} + +data "test" "a" { + count = var.data_count + + name = "a:${count.index}" +} + +resource "test" "b" { + name = "b" + upstream_names = [for v in data.test.a : v.name] +} + +output "from_data" { + value = [for v in data.test.a : v.output] +} + +output "from_resource" { + value = test.b.output +} +`, + }, + stages: []deferredActionsTestStage{ + // Stage 0. Unknown count in data source. The resource and + // outputs get transitively deferred. + { + inputs: map[string]cty.Value{ + "data_count": cty.DynamicVal, + }, + wantPlanned: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "output": cty.UnknownVal(cty.String), + "upstream_names": cty.UnknownVal(cty.Set(cty.String)), + }), + }, + wantActions: map[string]plans.Action{}, + wantDeferred: map[string]ExpectedDeferred{ + "data.test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Read}, + "test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{}, + wantOutputs: map[string]cty.Value{ + // Although this will be a TupleVal later, the count of + // items in the tuple is part of the type itself, and that's + // unknown at this point, so, DynamicPseudoType. + "from_data": cty.NullVal(cty.DynamicPseudoType), + "from_resource": cty.NullVal(cty.String), + }, + complete: false, + allowWarnings: false, + }, + // Stage 1. Everything's known now, so it converges. + { + inputs: map[string]cty.Value{ + "data_count": cty.NumberIntVal(3), + }, + wantPlanned: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "output": cty.UnknownVal(cty.String), + "upstream_names": cty.SetVal([]cty.Value{cty.StringVal("a:0"), cty.StringVal("a:1"), cty.StringVal("a:2")}), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{}, + wantApplied: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "output": cty.StringVal("b"), + "upstream_names": cty.SetVal([]cty.Value{cty.StringVal("a:0"), cty.StringVal("a:1"), cty.StringVal("a:2")}), + }), + }, + wantOutputs: map[string]cty.Value{ + "from_data": cty.TupleVal([]cty.Value{cty.StringVal("a:0"), cty.StringVal("a:1"), cty.StringVal("a:2")}), + "from_resource": cty.StringVal("b"), + }, + complete: true, + allowWarnings: false, + }, + }, + } + + // resourceForEachTest is a test that exercises the deferred actions + // mechanism with a configuration that has a resource with an unknown + // for_each attribute. + // + // We execute three plan-apply cycles. The first one with an unknown input + // into the for_each. The second with a known for_each value. The final + // with the same known for_each value to ensure that the plan is empty. + resourceForEachTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "each" { + type = set(string) +} + +resource "test" "a" { + name = "a" +} + +resource "test" "b" { + for_each = var.each + + name = "b:${each.key}" + upstream_names = [test.a.name] +} + +resource "test" "c" { + name = "c" + upstream_names = setunion( + [for v in test.b : v.name], + [test.a.name], + ) +} + +output "a" { + value = test.a +} +output "b" { + value = test.b +} +output "c" { + value = test.c +} + `, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "each": cty.DynamicVal, + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("b:"). + NotNull(). + NewValue(), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.UnknownVal(cty.String), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.UnknownVal(cty.Set(cty.String)).RefineNotNull(), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.a": plans.Create, + // The other resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.b[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + "test.c": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + }, + wantOutputs: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + + // To start with: outputs that refer to deferred values are + // null values of some type. + + // Output b is the value of the test.b resource block. The + // "b" resource has a for_each, so the type of the entire + // resource block will be an object of objects. (for_each + // key => resource instance.) But since the keys of an + // object are an inherent part of the object type, and our + // for_each keys are unknown, the object type is totally + // unknowable. + "b": cty.NullVal(cty.DynamicPseudoType), + + // Output c is the value of the test.c resource block. The + // "c" resource is a singleton instance, so its type is + // wholly known from the schema! But it's still a null + // value, because the resource got transitively deferred. + "c": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "output": cty.String, + "upstream_names": cty.Set(cty.String), + })), + }, + }, + { + inputs: map[string]cty.Value{ + "each": cty.SetVal([]cty.Value{ + cty.StringVal("1"), + cty.StringVal("2"), + }), + }, + wantPlanned: map[string]cty.Value{ + // test.a gets re-planned (to confirm that nothing has changed) + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + // test.b is now planned for real, once for each instance + "b:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.UnknownVal(cty.String), + }), + "b:2": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:2"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.UnknownVal(cty.String), + }), + // test.c gets re-planned, so we can finalize its values + // based on the new results from test.b. + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b:1"), + cty.StringVal("b:2"), + }), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + // Since this plan is "complete", we expect to have a planned + // action for every resource instance, although test.a is + // no-op because nothing has changed for it since last round. + `test.a`: plans.NoOp, + `test.b["1"]`: plans.Create, + `test.b["2"]`: plans.Create, + `test.c`: plans.Create, + }, + wantDeferred: make(map[string]ExpectedDeferred), + wantApplied: map[string]cty.Value{ + // Since test.a is no-op, it isn't visited during apply. The + // other instances should all be applied, though. + "b:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.StringVal("b:1"), + }), + "b:2": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:2"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.StringVal("b:2"), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b:1"), + cty.StringVal("b:2"), + }), + "output": cty.StringVal("c"), + }), + }, + wantOutputs: map[string]cty.Value{ + // Now everything should be fully resolved and known. + // A is fully resolved and known. + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.StringVal("b:1"), + }), + "2": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:2"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.StringVal("b:2"), + }), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b:1"), + cty.StringVal("b:2"), + }), + "output": cty.StringVal("c"), + }), + }, + complete: true, + }, + { + inputs: map[string]cty.Value{ + "each": cty.SetVal([]cty.Value{ + cty.StringVal("1"), + cty.StringVal("2"), + }), + }, + wantPlanned: map[string]cty.Value{ + // Everything gets re-planned to confirm that nothing has changed. + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.StringVal("b:1"), + }), + "b:2": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:2"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.StringVal("b:2"), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b:1"), + cty.StringVal("b:2"), + }), + "output": cty.StringVal("c"), + }), + }, + wantActions: map[string]plans.Action{ + // No changes needed + `test.a`: plans.NoOp, + `test.b["1"]`: plans.NoOp, + `test.b["2"]`: plans.NoOp, + `test.c`: plans.NoOp, + }, + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + // We won't execute an apply step in this stage, because the + // plan should be empty. + }, + }, + } + + resourceCountTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "a" { + name = "a" +} + +resource "test" "b" { + count = var.resource_count + name = "b:${count.index}" + upstream_names = [test.a.name] +} + +resource "test" "c" { + name = "c" + upstream_names = setunion( + [for v in test.b : v.name], + [test.a.name], + ) +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "resource_count": cty.DynamicVal, + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("b:"). + NotNull(). + NewValue(), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.UnknownVal(cty.String), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.UnknownVal(cty.Set(cty.String)).RefineNotNull(), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.a": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.b[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + "test.c": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + }, + wantOutputs: make(map[string]cty.Value), + }, + { + inputs: map[string]cty.Value{ + "resource_count": cty.NumberIntVal(2), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:0"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.UnknownVal(cty.String), + }), + "b:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + }), + "output": cty.UnknownVal(cty.String), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b:0"), + cty.StringVal("b:1"), + }), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + // Since this plan is "complete", we expect to have a planned + // action for every resource instance, although test.a is + // no-op because nothing has changed for it since last round. + `test.a`: plans.NoOp, + `test.b[0]`: plans.Create, + `test.b[1]`: plans.Create, + `test.c`: plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: true, + // Don't run an apply for this cycle. + }, + }, + } + + resourceInModuleForEachTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "each" { + type = set(string) +} + +module "mod" { + source = "./mod" + + each = var.each +} + +resource "test" "a" { + name = "a" + upstream_names = module.mod.names +} +`, + "mod/main.tf": ` +variable "each" { + type = set(string) +} + +resource "test" "names" { + for_each = var.each + name = "b:${each.key}" +} + +output "names" { + value = [for v in test.names : v.name] +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "each": cty.DynamicVal, + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("b:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.UnknownVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{}, + wantDeferred: map[string]ExpectedDeferred{ + "module.mod.test.names[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + "test.a": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + }, + { + inputs: map[string]cty.Value{ + "each": cty.SetVal([]cty.Value{ + cty.StringVal("1"), + cty.StringVal("2"), + }), + }, + wantPlanned: map[string]cty.Value{ + "b:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:1"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b:2": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:2"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.SetVal([]cty.Value{cty.StringVal("b:1"), cty.StringVal("b:2")}), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "module.mod.test.names[\"1\"]": plans.Create, + "module.mod.test.names[\"2\"]": plans.Create, + "test.a": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: true, + }, + }, + } + + createBeforeDestroyLifecycleTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +# This resource should be replaced in the plan, with create before destroy. +resource "test" "a" { + name = "a" + + lifecycle { + create_before_destroy = true + } +} + +# This resource should be replaced in the plan, with destroy before create. +resource "test" "b" { + name = "b" +} + +variable "resource_count" { + type = number +} + +# These resources are "maybe-orphans", we should see a generic plan action for +# these, but nothing in the actual plan. +resource "test" "c" { + count = var.resource_count + name = "c:${count.index}" + + lifecycle { + create_before_destroy = true + } +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, // force a replace in our plan + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.b"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, // force a replace in our plan + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "b", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.c[0]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, // force a replace in our plan + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "c:0", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("c:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.a": plans.CreateThenDelete, + "test.b": plans.DeleteThenCreate, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.c[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + }, + }, + } + + // The next test isn't testing deferred actions specifically. Instead, + // they're just testing the "removed" block works within the alternate + // execution path for deferred actions. + + forgetResourcesTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +# This should work as expected, with the resource being removed from state +# but not destroyed. This should work even with the unknown_instances experiment +# enabled. +removed { + from = test.a + + lifecycle { + destroy = false + } +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[0]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, // force a replace in our plan + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[1]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, // force a replace in our plan + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + wantPlanned: map[string]cty.Value{}, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.Forget, + "test.a[1]": plans.Forget, + }, + wantDeferred: map[string]ExpectedDeferred{}, + allowWarnings: true, + complete: true, + }, + }, + } + + importIntoUnknownInstancesTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "a" { + count = var.resource_count + name = "a" +} + +import { + id = "a" + to = test.a[0] +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + wantPlanned: map[string]cty.Value{ + // This time round, we don't actually perform the import + // because we don't know which instances we're importing. + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + }, + { + inputs: map[string]cty.Value{ + "resource_count": cty.NumberIntVal(1), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + }, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.NoOp, // noop not create because of the import. + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: true, + }, + }, + } + + targetDeferredResourceTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "a" { + count = var.resource_count + name = "a:${count.index}" +} + +resource "test" "b" { + name = "b" +} + +resource "test" "c" { + name = "c" +} +`, + }, + stages: []deferredActionsTestStage{ + // In this stage, we're testing that targeting test.a[0] will still + // prompt the plan to include the deferral of the unknown + // test.a[*] instances. + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustResourceInstanceAddr("test.a[0]"), mustResourceInstanceAddr("test.b")} + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("a:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + allowWarnings: true, + }, + // This stage is the same as above, except we're targeting the + // non-instanced test.a. This should still make the unknown + // test.a[*] instances appear in the plan as deferrals. + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustResourceInstanceAddr("test.a"), mustResourceInstanceAddr("test.b")} + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("a:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + allowWarnings: true, + }, + // Finally, we don't target test.a at all. So we shouldn't see it + // anywhere in planning or deferrals. + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustResourceInstanceAddr("test.b")} + }, + wantPlanned: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{}, + allowWarnings: true, + }, + }, + } + + targetResourceThatDependsOnDeferredResourceTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "c" { + name = "c" +} + +resource "test" "a" { + count = var.resource_count + name = "a:${count.index}" + upstream_names = [test.c.name] +} + +resource "test" "b" { + name = "b" + upstream_names = [for v in test.a : v.name] +} +`, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustResourceInstanceAddr("test.b")} + }, + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("a:"). + NotNull(). + NewValue(), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("c"), + }), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.UnknownVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.c": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + "test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{ + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("c"), + }), + }, + wantOutputs: map[string]cty.Value{}, + allowWarnings: true, + }, + { + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustResourceInstanceAddr("test.b")} + }, + inputs: map[string]cty.Value{ + "resource_count": cty.NumberIntVal(2), + }, + wantPlanned: map[string]cty.Value{ + "a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:0"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("c"), + }), + "output": cty.UnknownVal(cty.String), + }), + "a:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("c"), + }), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a:0"), + cty.StringVal("a:1"), + }), + "output": cty.UnknownVal(cty.String), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("c"), + }), + }, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.Create, + "test.a[1]": plans.Create, + "test.b": plans.Create, + "test.c": plans.NoOp, + }, + wantDeferred: map[string]ExpectedDeferred{}, + wantApplied: map[string]cty.Value{ + "a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:0"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("c"), + }), + "output": cty.StringVal("a:0"), + }), + "a:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:1"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("c"), + }), + "output": cty.StringVal("a:1"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.SetVal([]cty.Value{ + cty.StringVal("a:0"), + cty.StringVal("a:1"), + }), + "output": cty.StringVal("b"), + }), + }, + wantOutputs: map[string]cty.Value{}, + allowWarnings: true, + }, + }, + } + + targetDeferredResourceTriggersDependenciesTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + count = 2 + name = "a:${count.index}" +} + +resource "test" "b" { + for_each = toset([ for v in test.a : v.output ]) + name = "b:${each.value}" +} +`, + }, + stages: []deferredActionsTestStage{ + // The first time round, we target test.b only. Because test.b + // depends on test.a, we should see test.a instances in the plan. + // Then, when we apply the plan test.a should still be applied even + // through test.b was deferred and is technically not in the plan. + { + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustAbsResourceAddr("test.b")} + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("b:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:0"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "a:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:1"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.Create, + "test.a[1]": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.b[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{ + "a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:0"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a:0"), + }), + "a:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:1"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a:1"), + }), + }, + wantOutputs: make(map[string]cty.Value), + allowWarnings: true, + }, + { + buildOpts: func(opts *PlanOpts) { + opts.Targets = []addrs.Targetable{mustAbsResourceAddr("test.b")} + }, + wantPlanned: map[string]cty.Value{ + "a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:0"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a:0"), + }), + "a:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:1"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a:1"), + }), + "b:a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:a:0"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b:a:1": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b:a:1"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.NoOp, + "test.a[1]": plans.NoOp, + "test.b[\"a:0\"]": plans.Create, + "test.b[\"a:1\"]": plans.Create, + }, + wantDeferred: make(map[string]ExpectedDeferred), + allowWarnings: true, + complete: false, // because we still did targeting + }, + }, + } + + replaceDeferredResourceTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "a" { + count = var.resource_count + name = "a:${count.index}" +} + +resource "test" "b" { + name = "b" +} + +resource "test" "c" { + name = "c" +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[0]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a:0", + "output": "a:0", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.b"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "b", + "output": "b", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.c"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "c", + "output": "c", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + buildOpts: func(opts *PlanOpts) { + opts.ForceReplace = []addrs.AbsResourceInstance{mustResourceInstanceAddr("test.a[0]"), mustResourceInstanceAddr("test.b")} + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("a:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "c": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("c"), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.DeleteThenCreate, + "test.c": plans.NoOp, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + }, + }, + } + + customConditionsTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "a" { + count = var.resource_count + name = "a:${count.index}" + + lifecycle { + postcondition { + condition = self.name == "a:${count.index}" + error_message = "self.name is not a:${count.index}" + } + } +} + +resource "test" "b" { + name = "b" + + lifecycle { + postcondition { + condition = self.name == "b" + error_message = "self.name is not b" + } + } +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("a:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantOutputs: make(map[string]cty.Value), + }, + { + inputs: map[string]cty.Value{ + "resource_count": cty.NumberIntVal(1), + }, + wantPlanned: map[string]cty.Value{ + "a:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a:0"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.Create, + "test.b": plans.NoOp, + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: true, + }, + }, + } + + customConditionsWithOrphansTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "resource_count" { + type = number +} + +resource "test" "b" { + name = "b" + + lifecycle { + postcondition { + condition = self.name == "b" + error_message = "self.name is not b" + } + } +} + +# test.c will already be in state, so we can test the actions of orphaned +# resources with custom conditions. +resource "test" "c" { + count = var.resource_count + name = "c:${count.index}" + + lifecycle { + postcondition { + condition = self.name == "c:${count.index}" + error_message = "self.name is not c:${count.index}" + } + } +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.c[0]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "c:0", + "output": "c:0", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.c[1]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "c:1", + "output": "c:1", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "resource_count": cty.UnknownVal(cty.Number), + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String).Refine(). + StringPrefixFull("c:"). + NotNull(). + NewValue(), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.b": plans.Create, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.c[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + wantApplied: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantOutputs: make(map[string]cty.Value), + }, + { + inputs: map[string]cty.Value{ + "resource_count": cty.NumberIntVal(1), + }, + wantPlanned: map[string]cty.Value{ + "c:0": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("c:0"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("c:0"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantActions: map[string]plans.Action{ + "test.c[0]": plans.NoOp, + "test.c[1]": plans.Delete, + "test.b": plans.NoOp, + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: true, + }, + }, + } + + // resourceReadTest is a test that covers the behavior of reading resources + // in a refresh when the refresh is responding with a deferral. + resourceReadTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "a" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_read", // this signals the mock provider to defer the read + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.RefreshOnlyMode + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + // Empty because it's a refresh-only plan in this stage. + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The resource will be deferred, so shouldn't + // have any action at this stage. + }, + // The output refers to a resource that is unready, so in the + // state it becomes a null value of the appropriate type, + // despite the fact that we can predict *some* information (i.e. + // the future `name`) about the eventual value. + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "upstream_names": cty.Set(cty.String), + "output": cty.String, + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Read}, + }, + complete: false, + }, + + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + // The read is deferred but the plan is not so we can still + // plan the resource. + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "upstream_names": cty.Set(cty.String), + "output": cty.String, + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Update}, + }, + complete: false, + }, + }, + } + + resourceReadButForbiddenTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "a" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_read", // this signals the mock provider to defer the read + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.RefreshOnlyMode + opts.DeferralAllowed = false + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{}, + + wantActions: map[string]plans.Action{}, + wantOutputs: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + }), + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: false, + + wantDiagnostic: func(diags tfdiags.Diagnostics) bool { + for _, diag := range diags { + if diag.Description().Summary == "Provider deferred changes when Terraform did not allow deferrals" { + return true + } + } + return false + }, + }, + }, + } + + readDataSourceTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +data "test" "a" { + name = "deferred_read" +} + +resource "test" "b" { + name = data.test.a.name +} + +output "a" { + value = data.test.a +} + +output "b" { + value = test.b +} + `, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + // test.b is deferred but still being planned. It being listed + // here does not mean it's in the plan. + "deferred_read": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_read"), + "output": cty.UnknownVal(cty.String), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + }), + }, + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "output": cty.String, + })), + "b": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "output": cty.String, + "upstream_names": cty.Set(cty.String), + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "data.test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Read}, + "test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + complete: false, + }, + }, + } + + readDataSourceButForbiddenTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +data "test" "a" { + name = "deferred_read" +} + +resource "test" "b" { + name = data.test.a.name +} + +output "a" { + value = data.test.a +} + +output "b" { + value = test.b +} + `, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.DeferralAllowed = false + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{}, + wantActions: map[string]plans.Action{}, + + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + })), + "b": cty.NullVal(cty.DynamicPseudoType), + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: false, + + wantDiagnostic: func(diags tfdiags.Diagnostics) bool { + for _, diag := range diags { + if diag.Description().Summary == "Provider deferred changes when Terraform did not allow deferrals" { + return true + } + } + return false + }, + }, + }, + } + + // planCreateResourceChange is a test that covers the behavior of planning a resource that is being created. + planCreateResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "output": cty.String, + "upstream_names": cty.Set(cty.String), + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Create}, + }, + complete: false, + }, + }, + } + + // planUpdateResourceChange is a test that covers the behavior of planning a resource that is being updated + planUpdateResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "old_value", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "upstream_names": cty.Set(cty.String), + "output": cty.String, + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Update}, + }, + complete: false, + }, + }, + } + + // planNoOpResourceChange is a test that covers the behavior of planning a resource that is the same as the current state. + planNoOpResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_resource_change", + "output": "computed_output", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("computed_output"), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + // This example is strange (possibly unrealistic?) because the + // provider deferred the PlanResourceChange call but responded + // immediately on the ReadResource call; usually you would + // expect to defer both or defer neither. So the output is still + // the current concrete value (not a cty.NullVal), even though + // the resource "is deferred." + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "upstream_names": cty.Set(cty.String), + "output": cty.String, + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.NoOp}, + }, + complete: false, + }, + }, + } + + // planReplaceResourceChange is a test that covers the behavior of planning a resource that the provider + // marks as needing replacement. + planReplaceResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "old_value", + "output": "mark_for_replacement", // tells the mock provider to replace the resource + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "upstream_names": cty.Set(cty.String), + "output": cty.String, + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.DeleteThenCreate}, + }, + complete: false, + }, + }, + } + + // planForceReplaceResourceChange is a test that covers the behavior of planning a resource that is marked for replacement + planForceReplaceResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "old_value", + "output": "computed_output", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.ForceReplace = []addrs.AbsResourceInstance{ + { + Module: addrs.RootModuleInstance, + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test", + Name: "a", + }, + Key: addrs.NoKey, + }, + }, + } + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{ + "a": cty.NullVal(cty.Object(map[string]cty.Type{ + "name": cty.String, + "upstream_names": cty.Set(cty.String), + "output": cty.String, + })), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.DeleteThenCreate}, + }, + complete: false, + }, + }, + } + + // planDeleteResourceChange is a test that covers the behavior of planning a resource that is removed from the config. + planDeleteResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +// Empty config, expect to delete everything + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_resource_change", + "output": "computed_output", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{}, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Delete}, + }, + complete: false, + }, + }, + } + + // planDestroyResourceChange is a test that covers the behavior of planning a resource + planDestroyResourceChange = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_resource_change", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.DestroyMode + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + // This is here because of the additional full plan run if + // the previous state is not empty (and refresh is not skipped). + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + + wantActions: map[string]plans.Action{}, + wantApplied: map[string]cty.Value{ + // The all resources will be deferred, so shouldn't + // have any action at this stage. + }, + wantOutputs: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Delete}, + }, + complete: false, + }, + }, + } + + planDestroyResourceChangeButForbidden = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +output "a" { + value = test.a +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_resource_change", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.DestroyMode + opts.DeferralAllowed = false + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{}, + + wantActions: map[string]plans.Action{}, + + wantOutputs: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{}, + complete: false, + wantDiagnostic: func(diags tfdiags.Diagnostics) bool { + for _, diag := range diags { + if diag.Description().Summary == "Provider deferred changes when Terraform did not allow deferrals" { + return true + } + } + return false + }, + }, + }, + } + + importDeferredTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "import_id" { + type = string +} + +resource "test" "a" { + name = "a" +} + +import { + id = var.import_id + to = test.a +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "import_id": cty.StringVal("deferred"), // Telling the test case to defer the import + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Create}, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + complete: false, + }, + { + inputs: map[string]cty.Value{ + "import_id": cty.StringVal("can_be_imported"), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("can_be_imported"), + }), + }, + wantActions: map[string]plans.Action{ + "test.a": plans.Update, + }, + wantDeferred: map[string]ExpectedDeferred{}, + complete: true, + }, + }, + } + + importDeferredButForbiddenTest = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "import_id" { + type = string +} + +resource "test" "a" { + name = "a" +} + +import { + id = var.import_id + to = test.a +} +`, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + // We want to test if the user gets presented with a diagnostic in case no deferrals are allowed + opts.DeferralAllowed = false + }, + inputs: map[string]cty.Value{ + "import_id": cty.StringVal("deferred"), // Telling the test case to defer the import + }, + wantPlanned: map[string]cty.Value{}, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{}, + wantOutputs: make(map[string]cty.Value), + complete: false, + + wantDiagnostic: func(diags tfdiags.Diagnostics) bool { + for _, diag := range diags { + if diag.Description().Summary == "Provider deferred changes when Terraform did not allow deferrals" { + return true + } + } + return false + }, + }, + }, + } + + moduleDeferredForEachValue = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "input" { + type = set(string) +} + +module "my_module" { + for_each = var.input + source = "../module" + + + name = each.value +} +`, + "../module/main.tf": ` +variable "name" { + type = string +} + +resource "test" "a" { + name = var.name +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "input": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanned: map[string]cty.Value{ + "": cty.ObjectVal(map[string]cty.Value{ + "name": cty.UnknownVal(cty.String), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "module.my_module[*].test.a[*]": {Reason: providers.DeferredReasonInstanceCountUnknown, Action: plans.Create}, + }, + wantOutputs: make(map[string]cty.Value), + complete: false, + }, + }, + } + + moduleInnerResourceInstanceDeferred = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +module "my_module" { + source = "../module" +} +`, + "../module/main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + `module.my_module.test.a`: {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Create}, + }, + wantOutputs: make(map[string]cty.Value), + complete: false, + }, + }, + } + + unknownImportId = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "id" { + type = string +} + +resource "test" "a" { + name = "a" +} + +import { + id = var.id + to = test.a +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonResourceConfigUnknown, Action: plans.Create}, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + }, + }, + } + + unknownImportDefersConfigGeneration = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "id" { + type = string +} + +import { + id = var.id + to = test.a +} +`, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.GenerateConfigPath = "generated.tf" + }, + inputs: map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }, + wantPlanned: make(map[string]cty.Value), + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonResourceConfigUnknown, Action: plans.NoOp}, + }, + }, + }, + } + + unknownImportTo = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "strings" { + type = set(string) +} + +resource "test" "a" { + for_each = toset(["a", "b"]) + name = each.value +} + +import { + for_each = var.strings + id = each.value + to = test.a[each.key] +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "strings": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + // Both should be deferred, as we don't know which one is + // being imported. + "test.a[\"a\"]": {Reason: providers.DeferredReasonResourceConfigUnknown, Action: plans.Create}, + "test.a[\"b\"]": {Reason: providers.DeferredReasonResourceConfigUnknown, Action: plans.Create}, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + }, + { + inputs: map[string]cty.Value{ + "strings": cty.SetVal([]cty.Value{cty.StringVal("a")}), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantDeferred: make(map[string]ExpectedDeferred), + wantActions: map[string]plans.Action{ + "test.a[\"a\"]": plans.NoOp, + "test.a[\"b\"]": plans.Create, + }, + wantApplied: map[string]cty.Value{ + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantOutputs: make(map[string]cty.Value), + complete: true, + }, + }, + } + + unknownImportToExistingState = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "strings" { + type = set(string) +} + +resource "test" "a" { + for_each = toset(["a", "b"]) + name = each.value +} + +import { + for_each = var.strings + id = each.value + to = test.a[each.key] +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent(mustResourceInstanceAddr("test.a[\"a\"]"), &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + "output": "a", + }), + }, addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + state.SetResourceInstanceCurrent(mustResourceInstanceAddr("test.a[\"b\"]"), &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "b", + "output": "b", + }), + }, addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "strings": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantActions: map[string]plans.Action{ + // In this case, both the resources exist in state so + // even though they might be targeted by the unknown import + // it is still safe to apply the changes. + "test.a[\"a\"]": plans.NoOp, + "test.a[\"b\"]": plans.NoOp, + }, + wantDeferred: make(map[string]ExpectedDeferred), + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + complete: true, + }, + { + // The second stage demonstrates the known or unknown status of + // the import block doesn't impact the actual behaviour. + inputs: map[string]cty.Value{ + "strings": cty.SetVal([]cty.Value{cty.StringVal("a")}), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("b"), + }), + }, + wantDeferred: make(map[string]ExpectedDeferred), + wantActions: map[string]plans.Action{ + "test.a[\"a\"]": plans.NoOp, + "test.a[\"b\"]": plans.NoOp, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + complete: true, + }, + }, + } + + unknownImportToPartialExistingState = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "strings" { + type = set(string) +} + +resource "test" "a" { + for_each = toset(["a", "b"]) + name = each.value +} + +import { + for_each = var.strings + id = each.value + to = test.a[each.key] +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent(mustResourceInstanceAddr("test.a[\"a\"]"), &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + "output": "a", + }), + }, addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "strings": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("a"), + }), + "b": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("b"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{ + "test.a[\"a\"]": plans.NoOp, + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[\"b\"]": {Reason: providers.DeferredReasonResourceConfigUnknown, Action: plans.Create}, + }, + wantApplied: make(map[string]cty.Value), + wantOutputs: make(map[string]cty.Value), + }, + }, + } + + unknownImportReportsMissingConfiguration = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "strings" { + type = set(string) +} + +import { + for_each = var.strings + id = each.value + to = test.a[each.key] +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "strings": cty.UnknownVal(cty.Set(cty.String)), + }, + wantPlanned: make(map[string]cty.Value), + wantActions: make(map[string]plans.Action), + wantDeferred: make(map[string]ExpectedDeferred), + wantDiagnostic: func(diags tfdiags.Diagnostics) bool { + for _, diag := range diags { + if diag.Description().Summary == "Resource has no configuration" { + return true + } + } + return false + }, + }, + }, + } + + dataSourceDependsOnDeferredResource = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "deferred_resource_change" +} + +data "test" "b" { + name = "load_me" + depends_on = [test.a] +} +`, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "deferred_resource_change": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_resource_change"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "data.test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Read}, + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Create}, + }, + complete: false, + }, + }, + } + + // This is a super rare edge case here. It's very unlikely that a provider + // or a resource would get deferred during a refresh operation. Since it + // successfully applied whatever is being refreshed previously, it should + // not suddenly need to start deferring things. However, it is totally + // possible for providers to do this if they wanted so we'll add a test + // for it in case. + dataSourceDependsOnDeferredResourceDuringRefresh = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "name" { + type = string +} + +resource "test" "a" { + name = "deferred_resource_change" +} + +data "test" "b" { + name = var.name + depends_on = [test.a] +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent(mustResourceInstanceAddr("test.a"), &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "deferred_read", + "output": "a", + }), + }, addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.RefreshOnlyMode + }, + inputs: map[string]cty.Value{ + "name": cty.UnknownVal(cty.String), + }, + wantPlanned: make(map[string]cty.Value), // No planned changes, as we are only refreshing + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Read}, + }, + complete: false, + }, + }, + } + + // The following tests execute from the perspective of Stacks, where an + // external factor means everything in the plan should be deferred. + + resourceReferencesDeferredDataSource = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` + +variable "create" { + type = bool +} + +data "test" "a" { + count = var.create ? 1 : 0 + name = "foo" +} + +resource "test" "a" { + count = var.create ? 1 : 0 + name = data.test.a[0].output +} +`, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + // mark everything as deferred + opts.ExternalDependencyDeferred = true + }, + inputs: map[string]cty.Value{ + "create": cty.BoolVal(true), + }, + wantPlanned: map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: make(map[string]plans.Action), + wantDeferred: map[string]ExpectedDeferred{ + "data.test.a[0]": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Read}, + "test.a[0]": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + complete: false, + }, + }, + } + + resourceReferencesUnknownAndDeferredDataSource = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "name" { + type = string +} + +data "test" "a" { + name = "deferred_read" +} + +resource "test" "b" { + name = data.test.a.name +} + `, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + // mark everything as deferred + opts.ExternalDependencyDeferred = true + }, + inputs: map[string]cty.Value{ + "name": cty.UnknownVal(cty.String), + }, + wantPlanned: map[string]cty.Value{ + "deferred_read": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("deferred_read"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{}, + wantDeferred: map[string]ExpectedDeferred{ + "data.test.a": {Reason: providers.DeferredReasonProviderConfigUnknown, Action: plans.Read}, + "test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + complete: false, + }, + }, + } + + createAndReferenceResourceInDeferredComponent = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + count = 1 + name = "a" +} + +resource "test" "b" { + name = test.a[0].name +} +`, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.ExternalDependencyDeferred = true + }, + inputs: map[string]cty.Value{}, + wantPlanned: map[string]cty.Value{ + "a": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("a"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantActions: map[string]plans.Action{}, + wantDeferred: map[string]ExpectedDeferred{ + "test.a[0]": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + "test.b": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + complete: false, + }, + }, + } + + planCreateExternalDeferral = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "foo" +} + `, + }, + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + // This means everything should be deferred. + opts.ExternalDependencyDeferred = true + }, + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.UnknownVal(cty.String), + }), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + }, + }, + } + + planUpdateExternalDeferral = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "foo" +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "bar", + "output": "computed_output", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + // This means everything should be deferred. + opts.ExternalDependencyDeferred = true + }, + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("computed_output"), + }), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Update}, + }, + }, + }, + } + + planDeleteExternalDeferral = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +# empty, should delete the resource + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "bar", + "output": "computed_output", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + // This means everything should be deferred. + opts.ExternalDependencyDeferred = true + }, + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Delete}, + }, + }, + }, + } + + // planDeleteExternalDeferral tests to ensure that we still defer to-be + // deleted resources when the plan asks for everything to be deferred. + planDeleteModeExternalDeferral = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +resource "test" "a" { + name = "foo" +} + `, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "foo", + "output": "computed_output", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.DestroyMode + + // This means everything should be deferred. + opts.ExternalDependencyDeferred = true + }, + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{ + "foo": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo"), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal("computed_output"), + }), + }, + wantDeferred: map[string]ExpectedDeferred{ + "test.a": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Delete}, + }, + }, + }, + } + + ephemeralResourceOpenDeferral = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +ephemeral "test" "data" { + name = "deferred_open" +} + `, + }, + stages: []deferredActionsTestStage{ + { + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + // We don't record the ephemeral deferrals + }, + }, + }, + } + + ephemeralResourceOpenDeferralWithDependency = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +ephemeral "test" "data" { + name = "deferred_open" +} + +ephemeral "test" "dep" { + name = ephemeral.test.data.value +} + `, + }, + stages: []deferredActionsTestStage{ + { + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + // We don't record the ephemeral deferrals + }, + }, + }, + } + + ephemeralResourceOpenDeferralExpanded = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` + +variable "each" { + type = set(string) +} + +ephemeral "test" "data" { + for_each = var.each + + name = each.value +} + `, + }, + stages: []deferredActionsTestStage{ + { + inputs: map[string]cty.Value{ + "each": cty.DynamicVal, + }, + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + // We don't record the ephemeral deferrals + }, + }, + }, + } + + ephemeralResourceOpenDeferralProviderUsage = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +ephemeral "test" "data" { + name = "deferred_open" +} + + +provider "other" { + test_string = ephemeral.test.data.value +} + +resource "test_object" "test" { + provider = other +} + `, + }, + stages: []deferredActionsTestStage{ + { + complete: false, + wantActions: map[string]plans.Action{}, + wantPlanned: map[string]cty.Value{}, + wantDeferred: map[string]ExpectedDeferred{ + "test_object.test": {Reason: providers.DeferredReasonDeferredPrereq, Action: plans.Create}, + }, + }, + }, + } + + // refresh and destroy operations are a little different than normal + // operations. As they only execute against known resources in state, if + // a count or foreach attribute is unknown we don't actually have to defer + // the resources for these operations. We do know the resources that are all + // in state, and the operation should ignore the configuration anyway. + // + // the following tests iterate through various scenarios where count and + // foreach might be unknown during a refresh or destroy operation. + + unknownCountDuringRefresh = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "number" { + type = number +} + +resource "test" "a" { + count = var.number + name = "a${count.index}" +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[0]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a0", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.RefreshOnlyMode + }, + inputs: map[string]cty.Value{ + "number": cty.UnknownVal(cty.Number), + }, + wantActions: map[string]plans.Action{}, + wantPlanned: make(map[string]cty.Value), + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + }, + }, + } + unknownCountDuringDestroy = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "number" { + type = number +} + +resource "test" "a" { + count = var.number + name = "a${count.index}" +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[0]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a0", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.DestroyMode + }, + inputs: map[string]cty.Value{ + "number": cty.UnknownVal(cty.Number), + }, + wantActions: map[string]plans.Action{ + "test.a[0]": plans.Delete, + }, + wantPlanned: make(map[string]cty.Value), + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + }, + }, + } + unknownForEachDuringRefresh = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "foreach" { + type = set(string) +} + +resource "test" "a" { + for_each = var.foreach + name = each.value +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[\"a\"]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.RefreshOnlyMode + }, + inputs: map[string]cty.Value{ + "foreach": cty.UnknownVal(cty.Set(cty.String)), + }, + wantActions: map[string]plans.Action{}, + wantPlanned: make(map[string]cty.Value), + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + }, + }, + } + unknownForEachDuringDestroy = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "foreach" { + type = set(string) +} + +resource "test" "a" { + for_each = var.foreach + name = each.value +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[\"a\"]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.DestroyMode + }, + inputs: map[string]cty.Value{ + "foreach": cty.UnknownVal(cty.Set(cty.String)), + }, + wantActions: map[string]plans.Action{ + "test.a[\"a\"]": plans.Delete, + }, + wantPlanned: make(map[string]cty.Value), + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + }, + }, + } + unknownForEachDuringRefreshWithImport = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "foreach" { + type = set(string) +} + +resource "test" "a" { + for_each = var.foreach + name = each.value +} + +import { + for_each = var.foreach + id = each.value + to = test.a[each.key] +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[\"a\"]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.RefreshOnlyMode + }, + inputs: map[string]cty.Value{ + "foreach": cty.UnknownVal(cty.Set(cty.String)), + }, + wantActions: map[string]plans.Action{}, + wantPlanned: make(map[string]cty.Value), + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + }, + }, + } + unknownForEachDuringDestroyWithImport = deferredActionsTest{ + configs: map[string]string{ + "main.tf": ` +variable "foreach" { + type = set(string) +} + +resource "test" "a" { + for_each = var.foreach + name = each.value +} + +import { + for_each = var.foreach + id = each.value + to = test.a[each.key] +} +`, + }, + state: states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test.a[\"a\"]"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "name": "a", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }) + }), + stages: []deferredActionsTestStage{ + { + buildOpts: func(opts *PlanOpts) { + opts.Mode = plans.DestroyMode + }, + inputs: map[string]cty.Value{ + "foreach": cty.UnknownVal(cty.Set(cty.String)), + }, + wantActions: map[string]plans.Action{ + "test.a[\"a\"]": plans.Delete, + }, + wantPlanned: make(map[string]cty.Value), + wantDeferred: make(map[string]ExpectedDeferred), + complete: true, + }, + }, + } +) + +func TestContextApply_deferredActions(t *testing.T) { + tests := map[string]deferredActionsTest{ + "resource_for_each": resourceForEachTest, + "resource_in_module_for_each": resourceInModuleForEachTest, + "resource_count": resourceCountTest, + "create_before_destroy": createBeforeDestroyLifecycleTest, + "forget_resources": forgetResourcesTest, + "import_into_unknown": importIntoUnknownInstancesTest, + "target_deferred_resource": targetDeferredResourceTest, + "target_resource_that_depends_on_deferred_resource": targetResourceThatDependsOnDeferredResourceTest, + "target_deferred_resource_triggers_dependencies": targetDeferredResourceTriggersDependenciesTest, + "replace_deferred_resource": replaceDeferredResourceTest, + "custom_conditions": customConditionsTest, + "custom_conditions_with_orphans": customConditionsWithOrphansTest, + "resource_read": resourceReadTest, + "data_read": readDataSourceTest, + "data_for_each": dataForEachTest, + "data_count": dataCountTest, + "plan_create_resource_change": planCreateResourceChange, + "plan_update_resource_change": planUpdateResourceChange, + "plan_noop_resource_change": planNoOpResourceChange, + "plan_replace_resource_change": planReplaceResourceChange, + "plan_force_replace_resource_change": planForceReplaceResourceChange, + "plan_delete_resource_change": planDeleteResourceChange, + "plan_destroy_resource_change": planDestroyResourceChange, + "import_deferred": importDeferredTest, + "import_deferred_but_forbidden": importDeferredButForbiddenTest, + "resource_read_but_forbidden": resourceReadButForbiddenTest, + "data_read_but_forbidden": readDataSourceButForbiddenTest, + "plan_destroy_resource_change_but_forbidden": planDestroyResourceChangeButForbidden, + "module_deferred_for_each_value": moduleDeferredForEachValue, + "module_inner_resource_instance_deferred": moduleInnerResourceInstanceDeferred, + "unknown_import_id": unknownImportId, + "unknown_import_defers_config_generation": unknownImportDefersConfigGeneration, + "unknown_import_to": unknownImportTo, + "unknown_import_to_existing_state": unknownImportToExistingState, + "unknown_import_to_partial_existing_state": unknownImportToPartialExistingState, + "unknown_import_reports_missing_configuration": unknownImportReportsMissingConfiguration, + "data_source_depends_on_deferred_resource": dataSourceDependsOnDeferredResource, + "data_source_depends_on_deferred_resource_during_refresh": dataSourceDependsOnDeferredResourceDuringRefresh, + "resource_references_deferred_data_source": resourceReferencesDeferredDataSource, + "resource_references_unknown_and_deferred_data_source": resourceReferencesUnknownAndDeferredDataSource, + "create_and_reference_resource_in_deferred_component": createAndReferenceResourceInDeferredComponent, + "plan_create_external_deferral": planCreateExternalDeferral, + "plan_update_external_deferral": planUpdateExternalDeferral, + "plan_delete_external_deferral": planDeleteExternalDeferral, + "plan_delete_mode_external_deferral": planDeleteModeExternalDeferral, + "ephemeral_open_deferral": ephemeralResourceOpenDeferral, + "ephemeral_open_deferral_dependencies": ephemeralResourceOpenDeferralWithDependency, + "ephemeral_open_deferral_expanded": ephemeralResourceOpenDeferralExpanded, + "ephemeral_open_deferral_provider_usage": ephemeralResourceOpenDeferralProviderUsage, + "unknown_count_during_refresh": unknownCountDuringRefresh, + "unknown_count_during_destroy": unknownCountDuringDestroy, + "unknown_foreach_during_refresh": unknownForEachDuringRefresh, + "unknown_foreach_during_destroy": unknownForEachDuringDestroy, + "unknown_foreach_during_refresh_with_import": unknownForEachDuringRefreshWithImport, + "unknown_foreach_during_destroy_with_import": unknownForEachDuringDestroyWithImport, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + if test.skip { + t.SkipNow() + } + + // Initialise the config. + cfg := testModuleInline(t, test.configs) + + // Initialise the state. + state := test.state + if state == nil { + state = states.NewState() + } + + // Run through our cycle of planning and applying changes, checking + // the results at each step. + for ix, stage := range test.stages { + t.Run(fmt.Sprintf("round-%d", ix), func(t *testing.T) { + + provider := &deferredActionsProvider{ + plannedChanges: &deferredActionsChanges{ + changes: make(map[string]cty.Value), + }, + appliedChanges: &deferredActionsChanges{ + changes: make(map[string]cty.Value), + }, + } + other := simpleMockProvider() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(provider.Provider()), + addrs.NewDefaultProvider("other"): testProviderFuncFixed(other), + }, + }) + + opts := &PlanOpts{ + Mode: plans.NormalMode, + DeferralAllowed: true, + SetVariables: func() InputValues { + values := InputValues{} + for name, value := range stage.inputs { + values[name] = &InputValue{ + Value: value, + SourceType: ValueFromCaller, + } + } + return values + }(), + } + + if stage.buildOpts != nil { + stage.buildOpts(opts) + } + + var plan *plans.Plan + t.Run("plan", func(t *testing.T) { + var diags tfdiags.Diagnostics + + // Validate is run by default for any plan from the CLI + diags = diags.Append(ctx.Validate(cfg, &ValidateOpts{})) + // Plan won't proceed if validate failed + if !diags.HasErrors() { + p, pDiags := ctx.Plan(cfg, state, opts) + diags = diags.Append(pDiags) + plan = p + } + + if stage.wantDiagnostic == nil { + // We expect the correct planned changes and no diagnostics. + if stage.allowWarnings { + tfdiags.AssertNoErrors(t, diags) + } else { + tfdiags.AssertNoDiagnostics(t, diags) + } + } else { + if !stage.wantDiagnostic(diags) { + t.Fatalf("missing expected diagnostics: %s", diags.ErrWithWarnings()) + } else { + // We don't want to make any further assertions in this case. + // If diagnostics are expected it's valid that no plan may be returned. + return + } + } + + if plan == nil { + t.Fatalf("plan is nil") + } + + if plan.Complete != stage.complete { + t.Errorf("wrong completion status in plan: got %v, want %v", plan.Complete, stage.complete) + } + + provider.plannedChanges.Test(t, stage.wantPlanned) + + // We expect the correct actions. + gotActions := make(map[string]plans.Action) + for _, cs := range plan.Changes.Resources { + gotActions[cs.Addr.String()] = cs.Action + } + if diff := cmp.Diff(stage.wantActions, gotActions); diff != "" { + t.Errorf("wrong actions in plan\n%s", diff) + } + + gotDeferred := make(map[string]ExpectedDeferred) + for _, dc := range plan.DeferredResources { + gotDeferred[dc.ChangeSrc.Addr.String()] = ExpectedDeferred{Reason: dc.DeferredReason, Action: dc.ChangeSrc.Action} + } + if diff := cmp.Diff(stage.wantDeferred, gotDeferred); diff != "" { + t.Errorf("wrong deferred reasons or actions in plan\n%s", diff) + } + + // all the deferred changes should include a valid + // provider. + for _, change := range plan.DeferredResources { + if diff := cmp.Diff("provider[\"registry.terraform.io/hashicorp/test\"]", change.ChangeSrc.ProviderAddr.String()); diff != "" { + if otherDiff := cmp.Diff("provider[\"registry.terraform.io/hashicorp/other\"]", change.ChangeSrc.ProviderAddr.String()); otherDiff != "" { + t.Errorf("wrong provider address in plan\n, should be hashicorp/test or hashicorp/other %s", diff) + } + } + } + }) + + if stage.wantApplied == nil { + // Don't execute the apply stage if wantApplied is nil. + return + } + + if opts.Mode == plans.RefreshOnlyMode { + // Don't execute the apply stage if mode is refresh-only. + return + } + + t.Run("apply", func(t *testing.T) { + if plan == nil { + // if the previous step failed we won't know because it was another subtest + t.Fatal("cannot apply a nil plan") + } + + updatedState, diags := ctx.Apply(plan, cfg, nil) + + // We expect the correct applied changes and no diagnostics. + if stage.allowWarnings { + tfdiags.AssertNoErrors(t, diags) + } else { + tfdiags.AssertNoDiagnostics(t, diags) + } + provider.appliedChanges.Test(t, stage.wantApplied) + + // We also want the correct output values. + gotOutputs := make(map[string]cty.Value) + for name, output := range updatedState.RootOutputValues { + gotOutputs[name] = output.Value + } + if diff := cmp.Diff(stage.wantOutputs, gotOutputs, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong output values\n%s", diff) + } + + // Update the state for the next stage. + state = updatedState + }) + }) + } + }) + } +} + +// deferredActionsChanges is a concurrent-safe map of changes from a +// deferredActionsProvider. +type deferredActionsChanges struct { + sync.RWMutex + changes map[string]cty.Value +} + +func (d *deferredActionsChanges) Set(key string, value cty.Value) { + d.Lock() + defer d.Unlock() + if d.changes == nil { + d.changes = make(map[string]cty.Value) + } + d.changes[key] = value +} + +func (d *deferredActionsChanges) Get(key string) cty.Value { + d.RLock() + defer d.RUnlock() + return d.changes[key] +} + +func (d *deferredActionsChanges) Test(t *testing.T, expected map[string]cty.Value) { + t.Helper() + d.RLock() + defer d.RUnlock() + if diff := cmp.Diff(expected, d.changes, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong changes\n%s", diff) + } +} + +// deferredActionsProvider is a wrapper around the mock provider that keeps +// track of its own planned changes. +type deferredActionsProvider struct { + plannedChanges *deferredActionsChanges + appliedChanges *deferredActionsChanges +} + +func (provider *deferredActionsProvider) Provider() providers.Interface { + return &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + "upstream_names": { + Type: cty.Set(cty.String), + Optional: true, + }, + "output": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + "output": { + Computed: true, + Type: cty.String, + }, + }, + }, + }, + }, + EphemeralResourceTypes: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadResourceFn: func(req providers.ReadResourceRequest) providers.ReadResourceResponse { + if key := req.PriorState.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_read" { + return providers.ReadResourceResponse{ + NewState: req.PriorState, + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + + return providers.ReadResourceResponse{ + NewState: req.PriorState, + } + }, + ReadDataSourceFn: func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + if key := req.Config.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_read" { + return providers.ReadDataSourceResponse{ + State: req.Config, + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "name": req.Config.GetAttr("name"), + "output": req.Config.GetAttr("name"), + }), + } + }, + PlanResourceChangeFn: func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + var deferred *providers.Deferred + var requiresReplace []cty.Path + if req.ProposedNewState.IsNull() { + // Then we're deleting a concrete instance. + if key := req.PriorState.GetAttr("name"); key.IsKnown() && key.AsString() == "deferred_resource_change" { + deferred = &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + } + } + + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + Deferred: deferred, + } + } + + key := "" + if v := req.Config.GetAttr("name"); v.IsKnown() { + key = v.AsString() + } + + plannedState := req.ProposedNewState + if key == "deferred_resource_change" { + deferred = &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + } + } + + if plannedState.GetAttr("output").IsNull() { + plannedStateValues := req.ProposedNewState.AsValueMap() + plannedStateValues["output"] = cty.UnknownVal(cty.String) + plannedState = cty.ObjectVal(plannedStateValues) + } else if plannedState.GetAttr("output").AsString() == "mark_for_replacement" { + requiresReplace = append(requiresReplace, cty.GetAttrPath("name"), cty.GetAttrPath("output")) + } + + provider.plannedChanges.Set(key, plannedState) + return providers.PlanResourceChangeResponse{ + PlannedState: plannedState, + Deferred: deferred, + RequiresReplace: requiresReplace, + } + }, + ApplyResourceChangeFn: func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + key := req.Config.GetAttr("name").AsString() + newState := req.PlannedState + + if !newState.GetAttr("output").IsKnown() { + newStateValues := req.PlannedState.AsValueMap() + newStateValues["output"] = cty.StringVal(key) + newState = cty.ObjectVal(newStateValues) + } + + provider.appliedChanges.Set(key, newState) + return providers.ApplyResourceChangeResponse{ + NewState: newState, + } + }, + ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + if request.ID == "deferred" { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{}, + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + }, + } + } + + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: request.TypeName, + State: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal(request.ID), + "upstream_names": cty.NullVal(cty.Set(cty.String)), + "output": cty.StringVal(request.ID), + }), + }, + }, + } + }, + OpenEphemeralResourceFn: func(op providers.OpenEphemeralResourceRequest) providers.OpenEphemeralResourceResponse { + name := op.Config.GetAttr("name").AsString() + + res := providers.OpenEphemeralResourceResponse{ + Result: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal(name), + "value": cty.StringVal("ephemeral_value"), + }), + } + + if name == "deferred_open" { + res.Deferred = &providers.Deferred{ + Reason: providers.DeferredReasonProviderConfigUnknown, + } + } + + return res + }, + } +} + +func mustParseJson(values map[string]interface{}) []byte { + data, err := json.Marshal(values) + if err != nil { + panic(err) + } + return data +} diff --git a/internal/terraform/context_apply_ephemeral_test.go b/internal/terraform/context_apply_ephemeral_test.go new file mode 100644 index 0000000000..38179a1a1d --- /dev/null +++ b/internal/terraform/context_apply_ephemeral_test.go @@ -0,0 +1,984 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func TestContext2Apply_ephemeralProviderRef(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" { +} + +provider "test" { + test_string = ephemeral.ephem_resource.data.value +} + +resource "test_object" "test" { +} +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + EphemeralResourceTypes: map[string]providers.Schema{ + "ephem_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + ephem.OpenEphemeralResourceFn = func(providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + resp.Result = cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("test string"), + }) + resp.RenewAt = time.Now().Add(11 * time.Millisecond) + resp.Private = []byte("private data") + return resp + } + + // make sure we can wait for renew to be called + renewed := make(chan bool) + renewDone := sync.OnceFunc(func() { close(renewed) }) + + ephem.RenewEphemeralResourceFn = func(req providers.RenewEphemeralResourceRequest) (resp providers.RenewEphemeralResourceResponse) { + defer renewDone() + if string(req.Private) != "private data" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("invalid private data %q", req.Private)) + return resp + } + + resp.RenewAt = time.Now().Add(10 * time.Millisecond) + resp.Private = req.Private + return resp + } + + p := simpleMockProvider() + p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + // wait here for the ephemeral value to be renewed at least once + <-renewed + if req.Config.GetAttr("test_string").AsString() != "test string" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("received config did not contain \"test string\", got %#v\n", req.Config)) + } + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, nil, DefaultPlanOpts) + tfdiags.AssertNoDiagnostics(t, diags) + + if !ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled not called") + } + if !ephem.RenewEphemeralResourceCalled { + t.Error("RenewEphemeralResourceCalled not called") + } + if !ephem.CloseEphemeralResourceCalled { + t.Error("CloseEphemeralResourceCalled not called") + } + + // reset the ephemeral call flags and the gate + ephem.OpenEphemeralResourceCalled = false + ephem.RenewEphemeralResourceCalled = false + ephem.CloseEphemeralResourceCalled = false + renewed = make(chan bool) + renewDone = sync.OnceFunc(func() { close(renewed) }) + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) + + if !ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled not called") + } + if !ephem.RenewEphemeralResourceCalled { + t.Error("RenewEphemeralResourceCalled not called") + } + if !ephem.CloseEphemeralResourceCalled { + t.Error("CloseEphemeralResourceCalled not called") + } +} + +func TestContext2Apply_ephemeralApplyAndDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "test" { + for_each = toset(["a"]) + source = "./mod" + input = each.value +} + +provider "test" { + test_string = module.test["a"].data +} + +resource "test_object" "test" { +} + +# make sure ephemerals with zero instances are processed correctly too +module "zero" { + count = 0 + source = "./mod" + input = "test" +} +`, + "./mod/main.tf": ` +variable input { +} + +ephemeral "ephem_resource" "data" { +} + +output "data" { + ephemeral = true + value = ephemeral.ephem_resource.data.value +} + +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + EphemeralResourceTypes: map[string]providers.Schema{ + "ephem_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + ephemeralData := struct { + sync.Mutex + data string + }{} + + ephem.OpenEphemeralResourceFn = func(providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + ephemeralData.Lock() + defer ephemeralData.Unlock() + // open sets the data + ephemeralData.data = "test string" + + resp.Result = cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal(ephemeralData.data), + }) + return resp + } + + // closing with invalidate the ephemeral data + ephem.CloseEphemeralResourceFn = func(providers.CloseEphemeralResourceRequest) (resp providers.CloseEphemeralResourceResponse) { + ephemeralData.Lock() + defer ephemeralData.Unlock() + + // close invalidates the data + ephemeralData.data = "" + return resp + } + + p := simpleMockProvider() + p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + // wait here for the ephemeral value to be renewed at least once + if req.Config.GetAttr("test_string").AsString() != "test string" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("received config did not contain \"test string\", got %#v\n", req.Config)) + + // check if the ephemeral data is actually valid, as if we were + // using something like a temporary authentication token which gets + // revoked. + ephemeralData.Lock() + defer ephemeralData.Unlock() + if ephemeralData.data == "" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("ephemeralData from config not valid: %#v", req.Config)) + } + } + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, nil, DefaultPlanOpts) + tfdiags.AssertNoDiagnostics(t, diags) + + if !ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled not called") + } + if !ephem.CloseEphemeralResourceCalled { + t.Error("CloseEphemeralResourceCalled not called") + } + + // reset the ephemeral call flags and data + ephem.OpenEphemeralResourceCalled = false + ephem.CloseEphemeralResourceCalled = false + + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) + + if !ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled not called") + } + if !ephem.CloseEphemeralResourceCalled { + t.Error("CloseEphemeralResourceCalled not called") + } + + // now reverse the process + ephem.OpenEphemeralResourceCalled = false + ephem.CloseEphemeralResourceCalled = false + + plan, diags = ctx.Plan(m, state, &PlanOpts{Mode: plans.DestroyMode}) + tfdiags.AssertNoDiagnostics(t, diags) + + if !ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled not called") + } + if !ephem.CloseEphemeralResourceCalled { + t.Error("CloseEphemeralResourceCalled not called") + } + + ephem.OpenEphemeralResourceCalled = false + ephem.CloseEphemeralResourceCalled = false + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) + + if !ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled not called") + } + if !ephem.CloseEphemeralResourceCalled { + t.Error("CloseEphemeralResourceCalled not called") + } +} + +func TestContext2Apply_ephemeralChecks(t *testing.T) { + // test the full validate-plan-apply lifecycle for ephemeral conditions + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "input" { + type = string +} + +ephemeral "ephem_resource" "data" { + for_each = toset(["a", "b"]) + lifecycle { + precondition { + condition = var.input == "ok" + error_message = "input not ok" + } + postcondition { + condition = self.value != null + error_message = "value is null" + } + } +} + +provider "test" { + test_string = ephemeral.ephem_resource.data["a"].value +} + +resource "test_object" "test" { +} +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + EphemeralResourceTypes: map[string]providers.Schema{ + "ephem_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + ephem.OpenEphemeralResourceFn = func(providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + resp.Result = cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("test string"), + }) + return resp + } + + p := simpleMockProvider() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + tfdiags.AssertNoDiagnostics(t, diags) + + plan, diags := ctx.Plan(m, nil, &PlanOpts{ + SetVariables: InputValues{ + "input": &InputValue{ + Value: cty.StringVal("ok"), + SourceType: ValueFromConfig, + }, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // reset the ephemeral call flags + ephem.ConfigureProviderCalled = false + + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) +} + +func TestContext2Apply_write_only_attribute_not_in_plan_and_state(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true +} + +resource "ephem_write_only" "wo" { + normal = "normal" + write_only = var.ephem +} +`, + }) + + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + Version: 0, + } + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": schema, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + }, + }) + + ephemVar := &InputValue{ + Value: cty.StringVal("ephemeral_value"), + SourceType: ValueFromCLIArg, + } + plan, diags := ctx.Plan(m, nil, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if len(plan.Changes.Resources) != 1 { + t.Fatalf("Expected 1 resource change, got %d", len(plan.Changes.Resources)) + } + + schemas, schemaDiags := ctx.Schemas(m, plan.PriorState) + tfdiags.AssertNoDiagnostics(t, schemaDiags) + planChanges, err := plan.Changes.Decode(schemas) + if err != nil { + t.Fatalf("Failed to decode plan changes: %v.", err) + } + + if !planChanges.Resources[0].After.GetAttr("write_only").IsNull() { + t.Fatalf("Expected write_only to be null, got %v", planChanges.Resources[0].After.GetAttr("write_only")) + } + + state, diags := ctx.Apply(plan, m, &ApplyOpts{ + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + resource := state.Resource(addrs.AbsResource{ + Module: addrs.RootModuleInstance, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "ephem_write_only", + Name: "wo", + }, + }) + + if resource == nil { + t.Fatalf("Resource not found") + } + + resourceInstance := resource.Instances[addrs.NoKey] + if resourceInstance == nil { + t.Fatalf("Resource instance not found") + } + + attrs, err := resourceInstance.Current.Decode(schema) + if err != nil { + t.Fatalf("Failed to decode attributes: %v", err) + } + + if attrs.Value.GetAttr("normal").AsString() != "normal" { + t.Fatalf("normal attribute not as expected") + } + + if !attrs.Value.GetAttr("write_only").IsNull() { + t.Fatalf("write_only attribute should be null") + } +} + +func TestContext2Apply_update_write_only_attribute_not_in_plan_and_state(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true +} + +resource "ephem_write_only" "wo" { + normal = "normal" + write_only = var.ephem +} +`, + }) + + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + } + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": schema, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + }, + }) + + ephemVar := &InputValue{ + Value: cty.StringVal("ephemeral_value"), + SourceType: ValueFromCLIArg, + } + + priorState := states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("ephem_write_only.wo"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "normal": "outdated", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("ephem"), + Module: addrs.RootModule, + }) + }) + + plan, diags := ctx.Plan(m, priorState, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if len(plan.Changes.Resources) != 1 { + t.Fatalf("Expected 1 resource change, got %d", len(plan.Changes.Resources)) + } + + schemas, schemaDiags := ctx.Schemas(m, plan.PriorState) + tfdiags.AssertNoDiagnostics(t, schemaDiags) + planChanges, err := plan.Changes.Decode(schemas) + if err != nil { + t.Fatalf("Failed to decode plan changes: %v.", err) + } + + if !planChanges.Resources[0].After.GetAttr("write_only").IsNull() { + t.Fatalf("Expected write_only to be null, got %v", planChanges.Resources[0].After.GetAttr("write_only")) + } + + state, diags := ctx.Apply(plan, m, &ApplyOpts{ + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + resource := state.Resource(addrs.AbsResource{ + Module: addrs.RootModuleInstance, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "ephem_write_only", + Name: "wo", + }, + }) + + if resource == nil { + t.Fatalf("Resource not found") + } + + resourceInstance := resource.Instances[addrs.NoKey] + if resourceInstance == nil { + t.Fatalf("Resource instance not found") + } + + attrs, err := resourceInstance.Current.Decode(schema) + if err != nil { + t.Fatalf("Failed to decode attributes: %v", err) + } + + if attrs.Value.GetAttr("normal").AsString() != "normal" { + t.Fatalf("normal attribute not as expected") + } + + if !attrs.Value.GetAttr("write_only").IsNull() { + t.Fatalf("write_only attribute should be null") + } +} + +func TestContext2Apply_normal_attributes_becomes_write_only_attribute(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true +} + +resource "ephem_write_only" "wo" { + normal = "normal" + write_only = var.ephem +} +`, + }) + + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + } + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": schema, + }, + }, + } + // Below we force the write_only attribute's returned state to be Null, mimicking what the plugin-framework would + // return during an UpgradeResourceState RPC + ephem.UpgradeResourceStateFn = func(ursr providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse { + return providers.UpgradeResourceStateResponse{ + UpgradedState: cty.ObjectVal(map[string]cty.Value{ + "normal": cty.StringVal("normal"), + "write_only": cty.NullVal(cty.String), + }), + } + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + }, + }) + + ephemVar := &InputValue{ + Value: cty.StringVal("ephemeral_value"), + SourceType: ValueFromCLIArg, + } + + priorState := states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("ephem_write_only.wo"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "normal": "normal", + "write_only": "this was not ephemeral but now is", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("ephem"), + Module: addrs.RootModule, + }) + }) + + plan, diags := ctx.Plan(m, priorState, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if len(plan.Changes.Resources) != 1 { + t.Fatalf("Expected 1 resource change, got %d", len(plan.Changes.Resources)) + } + + schemas, schemaDiags := ctx.Schemas(m, plan.PriorState) + tfdiags.AssertNoDiagnostics(t, schemaDiags) + planChanges, err := plan.Changes.Decode(schemas) + if err != nil { + t.Fatalf("Failed to decode plan changes: %v.", err) + } + + if !planChanges.Resources[0].After.GetAttr("write_only").IsNull() { + t.Fatalf("Expected write_only to be null, got %v", planChanges.Resources[0].After.GetAttr("write_only")) + } + + state, diags := ctx.Apply(plan, m, &ApplyOpts{ + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + resource := state.Resource(addrs.AbsResource{ + Module: addrs.RootModuleInstance, + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "ephem_write_only", + Name: "wo", + }, + }) + + if resource == nil { + t.Fatalf("Resource not found") + } + + resourceInstance := resource.Instances[addrs.NoKey] + if resourceInstance == nil { + t.Fatalf("Resource instance not found") + } + + attrs, err := resourceInstance.Current.Decode(schema) + if err != nil { + t.Fatalf("Failed to decode attributes: %v", err) + } + + if attrs.Value.GetAttr("normal").AsString() != "normal" { + t.Fatalf("normal attribute not as expected") + } + + if !attrs.Value.GetAttr("write_only").IsNull() { + t.Fatalf("write_only attribute should be null") + } +} + +func TestContext2Apply_write_only_attribute_provider_applies_with_non_null_value(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true +} + +resource "ephem_write_only" "wo" { + normal = "normal" + write_only = var.ephem +} +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + }, + ApplyResourceChangeResponse: &providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "normal": cty.StringVal("normal"), + "write_only": cty.StringVal("the provider should have set this to null"), + }), + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + }, + }) + + ephemVar := &InputValue{ + Value: cty.StringVal("ephemeral_value"), + SourceType: ValueFromCLIArg, + } + + plan, planDiags := ctx.Plan(m, nil, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + + tfdiags.AssertNoDiagnostics(t, planDiags) + + _, diags := ctx.Apply(plan, m, &ApplyOpts{ + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + + var expectedDiags tfdiags.Diagnostics + + expectedDiags = append(expectedDiags, tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid object", + `Provider "provider[\"registry.terraform.io/hashicorp/ephem\"]" returned a value for the write-only attribute "ephem_write_only.wo.write_only" after apply. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.`, + )) + + tfdiags.AssertDiagnosticsMatch(t, diags, expectedDiags) +} + +func TestContext2Apply_write_only_attribute_provider_plan_with_non_null_value(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true +} + +resource "ephem_write_only" "wo" { + normal = "normal" + write_only = var.ephem +} +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeResponse: &providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "normal": cty.StringVal("normal"), + "write_only": cty.StringVal("the provider should have set this to null"), + }), + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + }, + }) + + ephemVar := &InputValue{ + Value: cty.StringVal("ephemeral_value"), + SourceType: ValueFromCLIArg, + } + + _, diags := ctx.Plan(m, nil, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "ephem": ephemVar, + }, + }) + + var expectedDiags tfdiags.Diagnostics + + expectedDiags = append(expectedDiags, tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + `Provider "provider[\"registry.terraform.io/hashicorp/ephem\"]" returned a value for the write-only attribute "ephem_write_only.wo.write_only" during planning. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.`, + )) + + tfdiags.AssertDiagnosticsMatch(t, diags, expectedDiags) +} + +func TestContext2Apply_write_only_attribute_provider_read_with_non_null_value(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true +} + +resource "ephem_write_only" "wo" { + normal = "normal" + write_only = var.ephem +} +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "normal": { + Type: cty.String, + Required: true, + }, + "write_only": { + Type: cty.String, + WriteOnly: true, + Required: true, + }, + }, + }, + }, + }, + }, + ReadResourceResponse: &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "normal": cty.StringVal("normal"), + "write_only": cty.StringVal("the provider should have set this to null"), + }), + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + }, + }) + + ephemVar := &InputValue{ + Value: cty.StringVal("ephemeral_value"), + SourceType: ValueFromCLIArg, + } + priorState := states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("ephem_write_only.wo"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: mustParseJson(map[string]interface{}{ + "normal": "outdated", + }), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("ephem"), + Module: addrs.RootModule, + }) + }) + + _, diags := ctx.Plan(m, priorState, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "ephem": ephemVar, + }, + SkipRefresh: false, + }) + + var expectedDiags tfdiags.Diagnostics + + expectedDiags = append(expectedDiags, tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid object", + `Provider "provider[\"registry.terraform.io/hashicorp/ephem\"]" returned a value for the write-only attribute "ephem_write_only.wo.write_only" during refresh. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.`, + )) + + tfdiags.AssertDiagnosticsMatch(t, diags, expectedDiags) +} diff --git a/internal/terraform/context_apply_identity_test.go b/internal/terraform/context_apply_identity_test.go new file mode 100644 index 0000000000..d7d0a9802f --- /dev/null +++ b/internal/terraform/context_apply_identity_test.go @@ -0,0 +1,216 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestContext2Apply_identity(t *testing.T) { + for name, tc := range map[string]struct { + mode plans.Mode + prevRunState *states.State + requiresReplace []cty.Path + plannedIdentity cty.Value + appliedIdentity cty.Value + + expectedIdentity cty.Value + expectDiagnostics tfdiags.Diagnostics + }{ + "create": { + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + "create - invalid applied identity schema": { + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + appliedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.BoolVal(false), + }), + expectDiagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "Provider produced an identity that doesn't match the schema", "Provider \"registry.terraform.io/hashicorp/test\" returned an identity for test_resource.test that doesn't match the identity schema: .id: string required, but received bool. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + }, + + "update": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + + "delete": { + mode: plans.DestroyMode, + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"bar"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.NilVal, + expectedIdentity: cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + }, + "replace": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + requiresReplace: []cty.Path{cty.GetAttrPath("id")}, + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + } { + t.Run(name, func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_resource" "test" { + id = "bar" + } + `, + }) + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "test_resource": &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "test_resource": 0, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + PlannedIdentity: tc.plannedIdentity, + RequiresReplace: tc.requiresReplace, + } + } + + if !tc.appliedIdentity.IsNull() { + p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + resp := providers.ApplyResourceChangeResponse{} + resp.NewState = req.PlannedState + resp.NewIdentity = tc.appliedIdentity + return resp + } + } + + plan, diags := ctx.Plan(m, tc.prevRunState, &PlanOpts{Mode: tc.mode}) + tfdiags.AssertNoDiagnostics(t, diags) + + state, diags := ctx.Apply(plan, m, nil) + if tc.expectDiagnostics.HasErrors() { + tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics) + return + } + tfdiags.AssertNoDiagnostics(t, diags) + + if !tc.expectedIdentity.IsNull() { + schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"] + + resourceInstanceStateSrc := state.Modules[""].Resources["test_resource.test"].Instance(addrs.NoKey).Current + + resourceInstanceState, err := resourceInstanceStateSrc.Decode(schema) + if err != nil { + t.Fatalf("failed to decode resource instance state: %s", err) + } + + if !resourceInstanceState.Identity.RawEquals(tc.expectedIdentity) { + t.Fatalf("unexpected identity: \n expected: %s\n got: %s", tc.expectedIdentity.GoString(), resourceInstanceState.Identity.GoString()) + } + } + }) + } +} diff --git a/internal/terraform/context_apply_overrides_test.go b/internal/terraform/context_apply_overrides_test.go new file mode 100644 index 0000000000..41b07d6cdc --- /dev/null +++ b/internal/terraform/context_apply_overrides_test.go @@ -0,0 +1,818 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" +) + +// This file contains 'integration' tests for the Terraform test overrides +// functionality. +// +// These tests could live in context_apply_test or context_apply2_test but given +// the size of those files, it makes sense to keep these tests grouped together. + +func TestContextOverrides(t *testing.T) { + + // The approach to the testing here, is to create some configuration that + // would panic if executed normally because of the underlying provider. + // + // We then write overrides that make sure the underlying provider is never + // called. + // + // We then run a plan, apply, refresh, destroy sequence that tests all the + // potential function calls to the underlying provider to make sure we + // have covered everything. + // + // Finally, we validate some expected values after the apply stage to make + // sure the overrides are returning the values we want them to. + + tcs := map[string]struct { + configs map[string]string + overrides *mocking.Overrides + outputs cty.Value + expectedErr string + }{ + "resource": { + configs: map[string]string{ + "main.tf": ` +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +}`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustResourceInstanceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "resource_from_provider": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +}`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "selectively_applies_provider": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +provider "test" { + alias = "secondary" +} + +resource "test_instance" "primary" { + value = "primary" +} + +resource "test_instance" "secondary" { + provider = test.secondary + value = "secondary" +} + +output "primary_value" { + value = test_instance.primary.value +} + +output "primary_id" { + value = test_instance.primary.id +} + +output "secondary_value" { + value = test_instance.secondary.value +} + +output "secondary_id" { + value = test_instance.secondary.id +}`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test.secondary"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + // Test should not apply this override, as this provider is + // not being used for this resource. + overrides["test.secondary"].Put(mustResourceInstanceAddr("test_instance.primary"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("primary_id"), + }), + }) + overrides["test.secondary"].Put(mustResourceInstanceAddr("test_instance.secondary"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("secondary_id"), + }), + }) + }, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustResourceInstanceAddr("test_instance.primary"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "primary_id": cty.StringVal("h3ll0"), + "primary_value": cty.StringVal("primary"), + "secondary_id": cty.StringVal("secondary_id"), + "secondary_value": cty.StringVal("secondary"), + }), + }, + "propagates_provider_to_modules_explicit": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + source = "./mod" + + providers = { + test = test + } +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +provider "test" {} + +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} +`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("module.mod.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "propagates_provider_to_modules_implicit": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + source = "./mod" +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("module.mod.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "data_source": { + configs: map[string]string{ + "main.tf": ` +data "test_instance" "instance" { + id = "data-source" +} + +resource "test_instance" "instance" { + value = data.test_instance.instance.value +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +}`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustResourceInstanceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + overrides.Put(mustResourceInstanceAddr("data.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("Hello, world!"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "module": { + configs: map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +resource "test_instance" "instance" { + value = "random" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +check "value" { + assert { + condition = test_instance.instance.value == "definitely wrong" + error_message = "bad value" + } +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.mod"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "provider_type_override": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + source = "./mod" +} + +output "value" { + value = module.mod.value +} + +output "id" { + value = module.mod.id +}`, + "mod/main.tf": ` +terraform { + required_providers { + replaced = { + source = "hashicorp/test" + } + } +} + +resource "test_instance" "instance" { + provider = replaced + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +`, + }, + overrides: mocking.OverridesForTesting(func(overrides map[string]addrs.Map[addrs.Targetable, *configs.Override]) { + overrides["test"] = addrs.MakeMap[addrs.Targetable, *configs.Override]() + overrides["test"].Put(mustResourceInstanceAddr("module.mod.test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }, nil), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + "value": cty.StringVal("Hello, world!"), + }), + }, + "resource_instance_overrides": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +resource "test_instance" "instance" { + count = 3 + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.*.value +} + +output "id" { + value = test_instance.instance.*.id +}`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustAbsResourceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("generic"), + }), + }) + overrides.Put(mustResourceInstanceAddr("test_instance.instance[1]"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("specific"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.TupleVal([]cty.Value{ + cty.StringVal("generic"), + cty.StringVal("specific"), + cty.StringVal("generic"), + }), + "value": cty.TupleVal([]cty.Value{ + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + }), + }), + }, + "imports": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +import { + id = "29C1E645FF91" + to = test_instance.instance +} + +resource "test_instance" "instance" { + value = "Hello, world!" +} + +output "id" { + value = test_instance.instance.id +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustAbsResourceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("29C1E645FF91"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("29C1E645FF91"), + }), + }, + // This test is designed to fail as documentation that we do not support + // config generation during tests. It's actually impossible in normal + // usage to do this since `terraform test` never triggers config + // generation. + "imports_config_gen": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +import { + id = "29C1E645FF91" + to = test_instance.instance +} + +output "id" { + value = test_instance.instance.id +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustAbsResourceAddr("test_instance.instance"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("29C1E645FF91"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("29C1E645FF91"), + }), + expectedErr: "override blocks do not support config generation", + }, + "module_instance_overrides": { + configs: map[string]string{ + "main.tf": ` +provider "test" {} + +module "mod" { + count = 3 + source = "./mod" +} + +output "value" { + value = module.mod.*.value +} + +output "id" { + value = module.mod.*.id +}`, + "mod/main.tf": ` +terraform { + required_providers { + replaced = { + source = "hashicorp/test" + } + } +} + +resource "test_instance" "instance" { + provider = replaced + value = "Hello, world!" +} + +output "value" { + value = test_instance.instance.value +} + +output "id" { + value = test_instance.instance.id +} + +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.mod"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("generic"), + "value": cty.StringVal("Hello, world!"), + }), + }) + overrides.Put(mustModuleInstance("module.mod[1]"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("specific"), + "value": cty.StringVal("Hello, world!"), + }), + }) + }), + outputs: cty.ObjectVal(map[string]cty.Value{ + "id": cty.TupleVal([]cty.Value{ + cty.StringVal("generic"), + cty.StringVal("specific"), + cty.StringVal("generic"), + }), + "value": cty.TupleVal([]cty.Value{ + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + cty.StringVal("Hello, world!"), + }), + }), + }, + "expansion inside overridden module": { + configs: map[string]string{ + "main.tf": ` +module "test" { + source = "./mod" +} +`, + "mod/main.tf": ` +locals { + instances = 2 + value = "Hello, world!" +} + +resource "test_instance" "resource" { + count = local.instances + string = local.value +} + +output "id" { + value = test_instance.resource[0].id +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.test"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.EmptyObjectVal, + }, + "expansion inside deeply nested overridden module": { + configs: map[string]string{ + "main.tf": ` +module "test" { + source = "./child" +} +`, + "child/main.tf": ` +module "grandchild" { + source = "../grandchild" +} + +locals { + instances = 2 + value = "Hello, world!" +} + +resource "test_instance" "resource" { + count = local.instances + string = local.value +} + +output "id" { + value = test_instance.resource[0].id +} +`, + "grandchild/main.tf": ` +locals { + instances = 2 + value = "Hello, world!" +} + +resource "test_instance" "resource" { + count = local.instances + string = local.value +} + +output "id" { + value = test_instance.resource[0].id +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.test"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.EmptyObjectVal, + }, + "legacy provider config inside overridden module": { + configs: map[string]string{ + "main.tf": ` +module "test" { + source = "./child" +} +`, + "child/main.tf": ` +module "grandchild" { + source = "../grandchild" +} +output "id" { + value = "child" +} +`, + "grandchild/main.tf": ` +variable "in" { + default = "test_value" +} + +provider "test" { + value = var.in +} + +resource "test_instance" "resource" { +} +`, + }, + overrides: mocking.OverridesForTesting(nil, func(overrides addrs.Map[addrs.Targetable, *configs.Override]) { + overrides.Put(mustModuleInstance("module.test"), &configs.Override{ + Values: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("h3ll0"), + }), + }) + }), + outputs: cty.EmptyObjectVal, + }, + } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + cfg := testModuleInline(t, tc.configs) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(underlyingOverridesProvider), + }, + }) + + plan, diags := ctx.Plan(cfg, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + Overrides: tc.overrides, + GenerateConfigPath: "out.tf", + }) + if len(tc.expectedErr) > 0 { + if diags.ErrWithWarnings().Error() != tc.expectedErr { + t.Fatal(diags) + } + return // Don't do the rest of the test if we were expecting errors. + } + + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + state, diags := ctx.Apply(plan, cfg, nil) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + outputs := make(map[string]cty.Value, len(cfg.Module.Outputs)) + for _, output := range cfg.Module.Outputs { + outputs[output.Name] = state.OutputValue(output.Addr().Absolute(addrs.RootModuleInstance)).Value + } + actual := cty.ObjectVal(outputs) + + if !actual.RawEquals(tc.outputs) { + t.Fatalf("expected:\n%s\nactual:\n%s", tc.outputs.GoString(), actual.GoString()) + } + + _, diags = ctx.Plan(cfg, state, &PlanOpts{ + Mode: plans.RefreshOnlyMode, + Overrides: tc.overrides, + }) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + destroyPlan, diags := ctx.Plan(cfg, state, &PlanOpts{ + Mode: plans.DestroyMode, + Overrides: tc.overrides, + }) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + _, diags = ctx.Apply(destroyPlan, cfg, nil) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + }) + } + +} + +// underlyingOverridesProvider returns a provider that always panics for +// important calls. This is to validate the behaviour of the overrides +// functionality, in that they should stop the provider from being executed. +var underlyingOverridesProvider = &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + DataSources: map[string]providers.Schema{ + "test_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + ReadResourceFn: func(request providers.ReadResourceRequest) providers.ReadResourceResponse { + panic("ReadResourceFn called, should have been overridden.") + }, + PlanResourceChangeFn: func(request providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + panic("PlanResourceChangeFn called, should have been overridden.") + }, + ApplyResourceChangeFn: func(request providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + panic("ApplyResourceChangeFn called, should have been overridden.") + }, + ReadDataSourceFn: func(request providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + panic("ReadDataSourceFn called, should have been overridden.") + }, + ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + panic("ImportResourceStateFn called, should have been overridden.") + }, +} diff --git a/internal/terraform/context_apply_test.go b/internal/terraform/context_apply_test.go index c3e3beecbc..b1099dfa1b 100644 --- a/internal/terraform/context_apply_test.go +++ b/internal/terraform/context_apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -25,9 +28,9 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/configs/hcl2shim" - "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -45,9 +48,9 @@ func TestContext2Apply_basic(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -64,6 +67,184 @@ func TestContext2Apply_basic(t *testing.T) { } } +func TestContext2Apply_stop(t *testing.T) { + t.Parallel() + + m := testModule(t, "apply-stop") + stopCh := make(chan struct{}) + waitCh := make(chan struct{}) + stoppedCh := make(chan struct{}) + stopCalled := uint32(0) + applyStopped := uint32(0) + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "indefinite": { + Version: 1, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "result": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(prcr providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + log.Printf("[TRACE] TestContext2Apply_stop: no-op PlanResourceChange") + return providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "result": cty.UnknownVal(cty.String), + }), + } + }, + ApplyResourceChangeFn: func(arcr providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse { + // This will unblock the main test code once we reach this + // point, so that it'll then be guaranteed to call Stop + // while we're waiting in here. + close(waitCh) + + log.Printf("[TRACE] TestContext2Apply_stop: ApplyResourceChange waiting for Stop call") + // This will block until StopFn closes this channel below. + <-stopCh + atomic.AddUint32(&applyStopped, 1) + // This unblocks StopFn below, thereby acknowledging the request + // to stop. + close(stoppedCh) + return providers.ApplyResourceChangeResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "result": cty.StringVal("complete"), + }), + } + }, + StopFn: func() error { + // Closing this channel will unblock the channel read in + // ApplyResourceChangeFn above. + log.Printf("[TRACE] TestContext2Apply_stop: Stop called") + atomic.AddUint32(&stopCalled, 1) + close(stopCh) + // This will block until ApplyResourceChange has reacted to + // being stopped. + log.Printf("[TRACE] TestContext2Apply_stop: Waiting for ApplyResourceChange to react to being stopped") + <-stoppedCh + log.Printf("[TRACE] TestContext2Apply_stop: Stop is completing") + return nil + }, + } + + hook := &testHook{} + ctx := testContext2(t, &ContextOpts{ + Hooks: []Hook{hook}, + Providers: map[addrs.Provider]providers.Factory{ + addrs.MustParseProviderSourceString("terraform.io/test/indefinite"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + // We'll reset the hook events before we apply because we only care about + // the apply-time events. + hook.Calls = hook.Calls[:0] + + // We'll apply in the background so that we can call Stop in the foreground. + stateCh := make(chan *states.State) + go func(plan *plans.Plan) { + state, _ := ctx.Apply(plan, m, nil) + stateCh <- state + }(plan) + + // We'll wait until the provider signals that we've reached the + // ApplyResourceChange function, so we can guarantee the expected + // order of operations so our hook events below will always match. + t.Log("waiting for the apply phase to get started") + <-waitCh + + // This will block until the apply operation has unwound, so we should + // be able to observe all of the apply side-effects afterwards. + t.Log("waiting for ctx.Stop to return") + ctx.Stop() + + t.Log("waiting for apply goroutine to return state") + state := <-stateCh + + t.Log("apply is all complete") + if state == nil { + t.Fatalf("final state is nil") + } + + if got, want := atomic.LoadUint32(&stopCalled), uint32(1); got != want { + t.Errorf("provider's Stop method was not called") + } + if got, want := atomic.LoadUint32(&applyStopped), uint32(1); got != want { + // This should not happen if things are working correctly but this is + // to catch weird situations such as if a bug in this test causes us + // to inadvertently stop Terraform before it reaches te apply phase, + // or if the apply operation fails in a way that causes it not to reach + // the ApplyResourceChange function. + t.Errorf("somehow provider's ApplyResourceChange didn't react to being stopped") + } + + // Because we interrupted the apply phase while applying the resource, + // we should have halted immediately after we finished visiting that + // resource. We don't visit indefinite.bar at all. + gotEvents := hook.Calls + wantEvents := []*testHookCall{ + {"PreDiff", "indefinite.foo"}, + {"PostDiff", "indefinite.foo"}, + {"PreApply", "indefinite.foo"}, + {"PostApply", "indefinite.foo"}, + {"PostStateUpdate", ""}, // State gets updated one more time to include the apply result. + } + // The "Stopping" event gets sent to the hook asynchronously from the others + // because it is triggered in the ctx.Stop call above, rather than from + // the goroutine where ctx.Apply was running, and therefore it doesn't + // appear in a guaranteed position in gotEvents. We already checked above + // that the provider's Stop method was called, so we'll just strip that + // event out of our gotEvents. + seenStopped := false + for i, call := range gotEvents { + if call.Action == "Stopping" { + seenStopped = true + // We'll shift up everything else in the slice to create the + // effect of the Stopping event not having been present at all, + // which should therefore make this slice match "wantEvents". + copy(gotEvents[i:], gotEvents[i+1:]) + gotEvents = gotEvents[:len(gotEvents)-1] + break + } + } + if diff := cmp.Diff(wantEvents, gotEvents); diff != "" { + t.Errorf("wrong hook events\n%s", diff) + } + if !seenStopped { + t.Errorf("'Stopping' event did not get sent to the hook") + } + + rov := state.OutputValue(addrs.OutputValue{Name: "result"}.Absolute(addrs.RootModuleInstance)) + if rov != nil && rov.Value != cty.NilVal && !rov.Value.IsNull() { + t.Errorf("'result' output value unexpectedly populated: %#v", rov.Value) + } + + resourceAddr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "indefinite", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + rv := state.ResourceInstance(resourceAddr) + if rv == nil || rv.Current == nil { + t.Fatalf("no state entry for %s", resourceAddr) + } + + resourceAddr.Resource.Resource.Name = "bar" + rv = state.ResourceInstance(resourceAddr) + if rv != nil && rv.Current != nil { + t.Fatalf("unexpected state entry for %s", resourceAddr) + } +} + func TestContext2Apply_unstable(t *testing.T) { // This tests behavior when the configuration contains an unstable value, // such as the result of uuid() or timestamp(), where each call produces @@ -92,9 +273,9 @@ func TestContext2Apply_unstable(t *testing.T) { Type: "test_resource", Name: "foo", }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) - schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"].Block + schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"] rds := plan.Changes.ResourceInstance(addr) - rd, err := rds.Decode(schema.ImpliedType()) + rd, err := rds.Decode(schema) if err != nil { t.Fatal(err) } @@ -102,7 +283,7 @@ func TestContext2Apply_unstable(t *testing.T) { t.Fatalf("Attribute 'random' has known value %#v; should be unknown in plan", rd.After.GetAttr("random")) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("unexpected error during Apply: %s", diags.Err()) } @@ -114,7 +295,7 @@ func TestContext2Apply_unstable(t *testing.T) { t.Fatalf("wrong number of resources %d; want 1", len(mod.Resources)) } - rs, err := rss.Current.Decode(schema.ImpliedType()) + rs, err := rss.Current.Decode(schema) if err != nil { t.Fatalf("decode error: %v", err) } @@ -139,9 +320,9 @@ func TestContext2Apply_escape(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -167,10 +348,10 @@ func TestContext2Apply_resourceCountOneList(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoDiagnostics(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) got := strings.TrimSpace(state.String()) want := strings.TrimSpace(`null_resource.foo.0: @@ -195,9 +376,9 @@ func TestContext2Apply_resourceCountZeroList(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -248,9 +429,9 @@ func TestContext2Apply_resourceDependsOnModule(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -320,10 +501,10 @@ func TestContext2Apply_resourceDependsOnModuleStateOnly(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) if !reflect.DeepEqual(order, []string{"child", "parent"}) { t.Fatal("resources applied out of order") @@ -347,9 +528,9 @@ func TestContext2Apply_resourceDependsOnModuleDestroy(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -390,9 +571,9 @@ func TestContext2Apply_resourceDependsOnModuleDestroy(t *testing.T) { plan, diags := ctx.Plan(m, globalState, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -441,9 +622,9 @@ func TestContext2Apply_resourceDependsOnModuleGrandchild(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -492,9 +673,9 @@ func TestContext2Apply_resourceDependsOnModuleInModule(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -519,9 +700,9 @@ func TestContext2Apply_mapVarBetweenModules(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -536,10 +717,7 @@ module.test: null_resource.noop: ID = foo provider = provider["registry.terraform.io/hashicorp/null"] - - Outputs: - - amis_out = {eu-west-1:ami-789012 eu-west-2:ami-989484 us-west-1:ami-123456 us-west-2:ami-456789 }`) +`) if actual != expected { t.Fatalf("expected: \n%s\n\ngot: \n%s\n", expected, actual) } @@ -557,9 +735,9 @@ func TestContext2Apply_refCount(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -594,9 +772,9 @@ func TestContext2Apply_providerAlias(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -636,7 +814,7 @@ func TestContext2Apply_providerAliasConfigure(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } // Configure to record calls AFTER Plan above @@ -664,7 +842,7 @@ func TestContext2Apply_providerAliasConfigure(t *testing.T) { }, }) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -697,9 +875,9 @@ func TestContext2Apply_providerWarning(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -732,9 +910,9 @@ func TestContext2Apply_emptyModule(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -772,10 +950,10 @@ func TestContext2Apply_createBeforeDestroy(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -852,10 +1030,10 @@ func TestContext2Apply_createBeforeDestroyUpdate(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -909,10 +1087,10 @@ func TestContext2Apply_createBeforeDestroy_dependsNonCBD(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -954,7 +1132,7 @@ func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) { var actual []cty.Value var actualLock sync.Mutex - h.PostApplyFn = func(addr addrs.AbsResourceInstance, gen states.Generation, sv cty.Value, e error) (HookAction, error) { + h.PostApplyFn = func(addr addrs.AbsResourceInstance, dk addrs.DeposedKey, sv cty.Value, e error) (HookAction, error) { actualLock.Lock() defer actualLock.Unlock() @@ -973,10 +1151,10 @@ func TestContext2Apply_createBeforeDestroy_hook(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -1049,10 +1227,10 @@ func TestContext2Apply_createBeforeDestroy_deposedCount(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -1109,10 +1287,10 @@ func TestContext2Apply_createBeforeDestroy_deposedOnly(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -1149,14 +1327,14 @@ func TestContext2Apply_destroyComputed(t *testing.T) { Mode: plans.DestroyMode, }) if diags.HasErrors() { - logDiagnostics(t, diags) + tfdiags.LogDiagnostics(t, diags) t.Fatal("plan failed") } else { t.Logf("plan:\n\n%s", legacyDiffComparisonString(plan.Changes)) } - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { - logDiagnostics(t, diags) + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { + tfdiags.LogDiagnostics(t, diags) t.Fatal("apply failed") } } @@ -1217,9 +1395,9 @@ func testContext2Apply_destroyDependsOn(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -1311,9 +1489,9 @@ func testContext2Apply_destroyDependsOnStateOnly(t *testing.T, state *states.Sta plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -1406,9 +1584,9 @@ func testContext2Apply_destroyDependsOnStateOnlyModule(t *testing.T, state *stat plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -1441,11 +1619,11 @@ func TestContext2Apply_dataBasic(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testTerraformApplyDataBasicStr) @@ -1496,10 +1674,10 @@ func TestContext2Apply_destroyData(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - newState, diags := ctx.Apply(plan, m) + newState, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -1561,10 +1739,10 @@ func TestContext2Apply_destroySkipsCBD(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -1595,9 +1773,9 @@ func TestContext2Apply_destroyModuleVarProviderConfig(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -1609,7 +1787,7 @@ func TestContext2Apply_destroyCrossProviders(t *testing.T) { p_aws := testProvider("aws") p_aws.ApplyResourceChangeFn = testApplyFn p_aws.PlanResourceChangeFn = testDiffFn - p_aws.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p_aws.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1643,10 +1821,10 @@ func TestContext2Apply_destroyCrossProviders(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { - logDiagnostics(t, diags) + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { + tfdiags.LogDiagnostics(t, diags) t.Fatal("apply failed") } } @@ -1691,9 +1869,9 @@ func TestContext2Apply_minimal(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -1733,13 +1911,13 @@ func TestContext2Apply_cancel(t *testing.T) { p.PlanResourceChangeFn = testDiffFn plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics stateCh := make(chan *states.State) go func() { - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) applyDiags = diags stateCh <- state @@ -1795,13 +1973,13 @@ func TestContext2Apply_cancelBlock(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics stateCh := make(chan *states.State) go func() { - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) applyDiags = diags stateCh <- state @@ -1892,13 +2070,13 @@ func TestContext2Apply_cancelProvisioner(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // Start the Apply in a goroutine var applyDiags tfdiags.Diagnostics stateCh := make(chan *states.State) go func() { - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) applyDiags = diags stateCh <- state @@ -1937,7 +2115,7 @@ func TestContext2Apply_compute(t *testing.T) { p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1988,9 +2166,9 @@ func TestContext2Apply_compute(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } @@ -2041,10 +2219,10 @@ func TestContext2Apply_countDecrease(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + s, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(testTerraformApplyCountDecStr) @@ -2091,9 +2269,9 @@ func TestContext2Apply_countDecreaseToOneX(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2153,7 +2331,7 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) { got := strings.TrimSpace(legacyPlanComparisonString(state, plan.Changes)) want := strings.TrimSpace(testTerraformApplyCountDecToOneCorruptedPlanStr) @@ -2186,7 +2364,7 @@ func TestContext2Apply_countDecreaseToOneCorrupted(t *testing.T) { } } - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2220,7 +2398,7 @@ func TestContext2Apply_countTainted(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) { got := strings.TrimSpace(legacyDiffComparisonString(plan.Changes)) want := strings.TrimSpace(` @@ -2238,8 +2416,8 @@ CREATE: aws_instance.foo[1] } } - s, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + s, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) got := strings.TrimSpace(s.String()) want := strings.TrimSpace(` @@ -2271,9 +2449,9 @@ func TestContext2Apply_countVariable(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2297,9 +2475,9 @@ func TestContext2Apply_countVariableRef(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2336,7 +2514,7 @@ func TestContext2Apply_provisionerInterpCount(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // We'll marshal and unmarshal the plan here, to ensure that we have // a clean new context as would be created if we separately ran @@ -2353,7 +2531,7 @@ func TestContext2Apply_provisionerInterpCount(t *testing.T) { } // Applying the plan should now succeed - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply failed unexpectedly: %s", diags.Err()) } @@ -2383,9 +2561,9 @@ func TestContext2Apply_foreachVariable(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2409,9 +2587,9 @@ func TestContext2Apply_moduleBasic(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2448,7 +2626,7 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) { return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2490,9 +2668,9 @@ func TestContext2Apply_moduleDestroyOrder(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2538,9 +2716,9 @@ func TestContext2Apply_moduleInheritAlias(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2565,7 +2743,7 @@ func TestContext2Apply_orphanResource(t *testing.T) { p := testProvider("test") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ @@ -2584,9 +2762,9 @@ func TestContext2Apply_orphanResource(t *testing.T) { }, }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) // At this point both resources should be recorded in the state, along // with the single instance associated with test_thing.one. @@ -2619,7 +2797,7 @@ func TestContext2Apply_orphanResource(t *testing.T) { }, }) plan, diags = ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) { addr := mustResourceInstanceAddr("test_thing.one[0]") change := plan.Changes.ResourceInstance(addr) @@ -2634,8 +2812,8 @@ func TestContext2Apply_orphanResource(t *testing.T) { } } - state, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) // The state should now be _totally_ empty, with just an empty root module // (since that always exists) and no resources at all. @@ -2685,7 +2863,7 @@ func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) { addr := mustResourceInstanceAddr("module.child.aws_instance.bar") change := plan.Changes.ResourceInstance(addr) @@ -2703,7 +2881,7 @@ func TestContext2Apply_moduleOrphanInheritAlias(t *testing.T) { } } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2748,9 +2926,9 @@ func TestContext2Apply_moduleOrphanProvider(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -2788,9 +2966,9 @@ func TestContext2Apply_moduleOrphanGrandchildProvider(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -2822,9 +3000,9 @@ func TestContext2Apply_moduleGrandchildProvider(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -2856,9 +3034,9 @@ func TestContext2Apply_moduleOnlyProvider(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2882,9 +3060,9 @@ func TestContext2Apply_moduleProviderAlias(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2919,9 +3097,9 @@ func TestContext2Apply_moduleProviderAliasTargets(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -2959,9 +3137,9 @@ func TestContext2Apply_moduleProviderCloseNested(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -2994,9 +3172,9 @@ func TestContext2Apply_moduleVarRefExisting(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3027,10 +3205,10 @@ func TestContext2Apply_moduleVarResourceCount(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) ctx = testContext2(t, &ContextOpts{ Providers: map[addrs.Provider]providers.Factory{ @@ -3047,9 +3225,9 @@ func TestContext2Apply_moduleVarResourceCount(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -3067,9 +3245,9 @@ func TestContext2Apply_moduleBool(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3100,9 +3278,9 @@ func TestContext2Apply_moduleTarget(t *testing.T) { addrs.RootModuleInstance.Child("B", addrs.NoKey), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3115,10 +3293,6 @@ module.A: provider = provider["registry.terraform.io/hashicorp/aws"] foo = bar type = aws_instance - - Outputs: - - value = foo module.B: aws_instance.bar: ID = foo @@ -3149,9 +3323,9 @@ func TestContext2Apply_multiProvider(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3172,7 +3346,7 @@ func TestContext2Apply_multiProviderDestroy(t *testing.T) { m := testModule(t, "apply-multi-provider-destroy") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "addr": {Type: cty.String, Optional: true}, @@ -3191,7 +3365,7 @@ func TestContext2Apply_multiProviderDestroy(t *testing.T) { p2 := testProvider("vault") p2.ApplyResourceChangeFn = testApplyFn p2.PlanResourceChangeFn = testDiffFn - p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "vault_instance": { Attributes: map[string]*configschema.Attribute{ @@ -3213,10 +3387,10 @@ func TestContext2Apply_multiProviderDestroy(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + s, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) state = s } @@ -3262,10 +3436,10 @@ func TestContext2Apply_multiProviderDestroy(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + s, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) if !checked { t.Fatal("should be checked") @@ -3284,7 +3458,7 @@ func TestContext2Apply_multiProviderDestroyChild(t *testing.T) { m := testModule(t, "apply-multi-provider-destroy-child") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, @@ -3303,7 +3477,7 @@ func TestContext2Apply_multiProviderDestroyChild(t *testing.T) { p2 := testProvider("vault") p2.ApplyResourceChangeFn = testApplyFn p2.PlanResourceChangeFn = testDiffFn - p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p2.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "vault_instance": { @@ -3326,9 +3500,9 @@ func TestContext2Apply_multiProviderDestroyChild(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3377,9 +3551,9 @@ func TestContext2Apply_multiProviderDestroyChild(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3417,14 +3591,14 @@ func TestContext2Apply_multiVar(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } - actual := state.RootModule().OutputValues["output"] + actual := state.RootOutputValues["output"] expected := cty.StringVal("bar0,bar1,bar2") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) @@ -3449,16 +3623,16 @@ func TestContext2Apply_multiVar(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("End state: %s", state.String()) - actual := state.RootModule().OutputValues["output"] + actual := state.RootOutputValues["output"] if actual == nil { t.Fatal("missing output") } @@ -3514,7 +3688,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) { } } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ @@ -3554,7 +3728,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) checkConfig := func(key string, want cty.Value) { configsLock.Lock() @@ -3568,7 +3742,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) { t.Run("config for "+key, func(t *testing.T) { for _, problem := range deep.Equal(got, want) { - t.Errorf(problem) + t.Error(problem) } }) } @@ -3651,7 +3825,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) { })) t.Run("apply", func(t *testing.T) { - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } @@ -3665,7 +3839,7 @@ func TestContext2Apply_multiVarComprehensive(t *testing.T) { }, } got := map[string]interface{}{} - for k, s := range state.RootModule().OutputValues { + for k, s := range state.RootOutputValues { got[k] = hcl2shim.ConfigValueFromHCL2(s.Value) } if !reflect.DeepEqual(got, want) { @@ -3692,16 +3866,16 @@ func TestContext2Apply_multiVarOrder(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("State: %s", state.String()) - actual := state.RootModule().OutputValues["should-be-11"] + actual := state.RootOutputValues["should-be-11"] expected := cty.StringVal("index-11") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) @@ -3723,16 +3897,16 @@ func TestContext2Apply_multiVarOrderInterp(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } t.Logf("State: %s", state.String()) - actual := state.RootModule().OutputValues["should-be-11"] + actual := state.RootOutputValues["should-be-11"] expected := cty.StringVal("baz-index-11") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) @@ -3766,10 +3940,10 @@ func TestContext2Apply_multiVarCountDec(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) log.Print("\n========\nStep 1 Apply\n========") - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3830,12 +4004,12 @@ func TestContext2Apply_multiVarCountDec(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) t.Logf("Step 2 plan:\n%s", legacyDiffComparisonString(plan.Changes)) log.Print("\n========\nStep 2 Apply\n========") - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -3854,7 +4028,7 @@ func TestContext2Apply_multiVarMissingState(t *testing.T) { m := testModule(t, "apply-multi-var-missing-state") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ @@ -3873,10 +4047,10 @@ func TestContext2Apply_multiVarMissingState(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // Before the relevant bug was fixed, Terraform would panic during apply. - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply failed: %s", diags.Err()) } @@ -3889,9 +4063,14 @@ func TestContext2Apply_outputOrphan(t *testing.T) { p.PlanResourceChangeFn = testDiffFn state := states.NewState() - root := state.EnsureModule(addrs.RootModuleInstance) - root.SetOutputValue("foo", cty.StringVal("bar"), false) - root.SetOutputValue("bar", cty.StringVal("baz"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) + state.SetOutputValue( + addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar-val"), false, + ) ctx := testContext2(t, &ContextOpts{ Providers: map[addrs.Provider]providers.Factory{ @@ -3900,17 +4079,22 @@ func TestContext2Apply_outputOrphan(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyOutputOrphanStr) - if actual != expected { - t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) + expectedState := states.NewState() + expectedState.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) + expectedState.CheckResults = &states.CheckResults{} + + if diff := cmp.Diff(expectedState, state); diff != "" { + t.Fatalf("unexpected state: %s", diff) } } @@ -3928,9 +4112,9 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3958,9 +4142,9 @@ func TestContext2Apply_outputOrphanModule(t *testing.T) { // it to avoid changing the flow of this test in case that's important // for some reason. plan, diags = ctx.Plan(emptyConfig, state.DeepCopy(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, emptyConfig) + state, diags = ctx.Apply(plan, emptyConfig, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -3996,9 +4180,9 @@ func TestContext2Apply_providerComputedVar(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -4024,9 +4208,9 @@ func TestContext2Apply_providerConfigureDisabled(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -4061,9 +4245,9 @@ func TestContext2Apply_provisionerModule(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -4116,9 +4300,9 @@ func TestContext2Apply_Provisioner_compute(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -4166,9 +4350,9 @@ func TestContext2Apply_provisionerCreateFail(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should error") } @@ -4201,9 +4385,9 @@ func TestContext2Apply_provisionerCreateFailNoId(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should error") } @@ -4236,9 +4420,9 @@ func TestContext2Apply_provisionerFail(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should error") } @@ -4282,9 +4466,9 @@ func TestContext2Apply_provisionerFail_createBeforeDestroy(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should error") } @@ -4323,9 +4507,9 @@ func TestContext2Apply_error_createBeforeDestroy(t *testing.T) { p.PlanResourceChangeFn = testDiffFn plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should have error") } @@ -4375,9 +4559,9 @@ func TestContext2Apply_errorDestroy_createBeforeDestroy(t *testing.T) { p.PlanResourceChangeFn = testDiffFn plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should have error") } @@ -4393,7 +4577,7 @@ func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { m := testModule(t, "apply-multi-depose-create-before-destroy") p := testProvider("aws") ps := map[addrs.Provider]providers.Factory{addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p)} - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -4451,11 +4635,11 @@ func TestContext2Apply_multiDepose_createBeforeDestroy(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // Destroy is broken, so even though CBD successfully replaces the instance, // we'll have to save the Deposed instance to destroy later - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should have error") } @@ -4481,11 +4665,11 @@ aws_instance.web: (1 deposed) }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // We're replacing the primary instance once again. Destroy is _still_ // broken, so the Deposed list gets longer - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should have error") } @@ -4546,9 +4730,9 @@ aws_instance.web: (1 deposed) }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) // Expect error because 1/2 of Deposed destroys failed if !diags.HasErrors() { t.Fatal("should have error") @@ -4581,8 +4765,8 @@ aws_instance.web: (1 deposed) }, }, }) - assertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + tfdiags.AssertNoErrors(t, diags) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal("should not have error:", diags.Err()) } @@ -4620,9 +4804,9 @@ func TestContext2Apply_provisionerFailContinue(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -4665,9 +4849,9 @@ func TestContext2Apply_provisionerFailContinueHook(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -4714,9 +4898,60 @@ func TestContext2Apply_provisionerDestroy(t *testing.T) { }) plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.DestroyMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatalf("diags: %s", diags.Err()) + } + + checkStateString(t, state, ``) + + // Verify apply was invoked + if !pr.ProvisionResourceCalled { + t.Fatalf("provisioner not invoked") + } +} + +func TestContext2Apply_provisionerDestroyRemoved(t *testing.T) { + m := testModule(t, "apply-provisioner-destroy-removed") + p := testProvider("aws") + pr := testProvisioner() + p.PlanResourceChangeFn = testDiffFn + pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { + val := req.Config.GetAttr("command").AsString() + // The following is "destroy ${each.key} ${self.foo}" + if val != "destroy a bar" { + t.Fatalf("wrong value for command: %q", val) + } + + return + } + + state := states.NewState() + foo := state.EnsureModule(mustModuleInstance("module.foo")) + foo.SetResourceInstanceCurrent( + mustResourceInstanceAddr(`aws_instance.foo["a"]`).Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar","foo":"bar"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + Provisioners: map[string]provisioners.Factory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) + + plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -4761,9 +4996,9 @@ func TestContext2Apply_provisionerDestroyFail(t *testing.T) { }) plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.DestroyMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should error") } @@ -4827,9 +5062,9 @@ func TestContext2Apply_provisionerDestroyFailContinue(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -4894,9 +5129,9 @@ func TestContext2Apply_provisionerDestroyFailContinueFail(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("apply succeeded; wanted error from second provisioner") } @@ -4968,9 +5203,9 @@ func TestContext2Apply_provisionerDestroyTainted(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5019,9 +5254,9 @@ func TestContext2Apply_provisionerResourceRef(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5063,9 +5298,9 @@ func TestContext2Apply_provisionerSelfRef(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5114,9 +5349,9 @@ func TestContext2Apply_provisionerMultiSelfRef(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5172,9 +5407,9 @@ func TestContext2Apply_provisionerMultiSelfRefSingle(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5228,7 +5463,7 @@ func TestContext2Apply_provisionerExplicitSelfRef(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5256,7 +5491,7 @@ func TestContext2Apply_provisionerExplicitSelfRef(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5290,9 +5525,9 @@ func TestContext2Apply_provisionerForEachSelfRef(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5315,11 +5550,11 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { - logDiagnostics(t, diags) + tfdiags.LogDiagnostics(t, diags) t.Fatal("apply failed") } @@ -5360,11 +5595,11 @@ func TestContext2Apply_Provisioner_Diff(t *testing.T) { }) plan, diags = ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state2, diags := ctx.Apply(plan, m) + state2, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { - logDiagnostics(t, diags) + tfdiags.LogDiagnostics(t, diags) t.Fatal("apply failed") } @@ -5429,10 +5664,10 @@ func TestContext2Apply_outputDiffVars(t *testing.T) { //} plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) } func TestContext2Apply_destroyX(t *testing.T) { @@ -5449,9 +5684,9 @@ func TestContext2Apply_destroyX(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5468,9 +5703,9 @@ func TestContext2Apply_destroyX(t *testing.T) { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5504,9 +5739,9 @@ func TestContext2Apply_destroyOrder(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5525,9 +5760,9 @@ func TestContext2Apply_destroyOrder(t *testing.T) { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5562,9 +5797,9 @@ func TestContext2Apply_destroyModulePrefix(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5586,9 +5821,9 @@ func TestContext2Apply_destroyModulePrefix(t *testing.T) { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5623,9 +5858,9 @@ func TestContext2Apply_destroyNestedModule(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5661,9 +5896,9 @@ func TestContext2Apply_destroyDeeplyNestedModule(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -5696,7 +5931,7 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { t.Logf("Step 1 plan: %s", legacyDiffComparisonString(plan.Changes)) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errs: %s", diags.Err()) } @@ -5739,7 +5974,7 @@ func TestContext2Apply_destroyModuleWithAttrsReferencingResource(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } @@ -5768,9 +6003,9 @@ func TestContext2Apply_destroyWithModuleVariableAndCount(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } @@ -5810,7 +6045,7 @@ func TestContext2Apply_destroyWithModuleVariableAndCount(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } @@ -5840,9 +6075,9 @@ func TestContext2Apply_destroyTargetWithModuleVariableAndCount(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } @@ -5876,7 +6111,7 @@ func TestContext2Apply_destroyTargetWithModuleVariableAndCount(t *testing.T) { } // Destroy, targeting the module explicitly - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags) } @@ -5914,9 +6149,9 @@ func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } @@ -5954,7 +6189,7 @@ func TestContext2Apply_destroyWithModuleVariableAndCountNested(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } @@ -5992,9 +6227,9 @@ func TestContext2Apply_destroyOutputs(t *testing.T) { // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) @@ -6010,9 +6245,9 @@ func TestContext2Apply_destroyOutputs(t *testing.T) { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6031,9 +6266,9 @@ func TestContext2Apply_destroyOutputs(t *testing.T) { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatal(diags.Err()) } } @@ -6060,9 +6295,9 @@ func TestContext2Apply_destroyOrphan(t *testing.T) { p.PlanResourceChangeFn = testDiffFn plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6102,9 +6337,9 @@ func TestContext2Apply_destroyTaintedProvisioner(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6144,9 +6379,9 @@ func TestContext2Apply_error(t *testing.T) { p.PlanResourceChangeFn = testDiffFn plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should have error") } @@ -6162,7 +6397,7 @@ func TestContext2Apply_errorDestroy(t *testing.T) { m := testModule(t, "empty") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ @@ -6211,9 +6446,9 @@ func TestContext2Apply_errorDestroy(t *testing.T) { ) }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should have error") } @@ -6233,7 +6468,7 @@ func TestContext2Apply_errorCreateInvalidNew(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -6268,9 +6503,9 @@ func TestContext2Apply_errorCreateInvalidNew(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should have error") } @@ -6282,7 +6517,7 @@ func TestContext2Apply_errorCreateInvalidNew(t *testing.T) { if got, want := diags.Err().Error(), "forced error"; !strings.Contains(got, want) { t.Errorf("returned error does not contain %q, but it should\n%s", want, diags.Err()) } - if got, want := len(state.RootModule().Resources), 2; got != want { + if got, want := len(state.RootModule().Resources), 1; got != want { t.Errorf("%d resources in state before prune; should have %d\n%s", got, want, spew.Sdump(state)) } state.PruneResourceHusks() // aws_instance.bar with no instances gets left behind when we bail out, but that's okay @@ -6295,7 +6530,7 @@ func TestContext2Apply_errorUpdateNullNew(t *testing.T) { m := testModule(t, "apply-error") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -6343,9 +6578,9 @@ func TestContext2Apply_errorUpdateNullNew(t *testing.T) { ) }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("should have error") } @@ -6410,9 +6645,9 @@ func TestContext2Apply_errorPartial(t *testing.T) { p.PlanResourceChangeFn = testDiffFn plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags == nil { t.Fatal("should have error") } @@ -6442,9 +6677,9 @@ func TestContext2Apply_hook(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -6484,9 +6719,9 @@ func TestContext2Apply_hookOrphan(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -6514,9 +6749,9 @@ func TestContext2Apply_idAttr(t *testing.T) { p.ApplyResourceChangeFn = testApplyFn plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -6548,9 +6783,9 @@ func TestContext2Apply_outputBasic(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6574,9 +6809,9 @@ func TestContext2Apply_outputAdd(t *testing.T) { }) plan1, diags := ctx1.Plan(m1, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state1, diags := ctx1.Apply(plan1, m1) + state1, diags := ctx1.Apply(plan1, m1, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6592,9 +6827,9 @@ func TestContext2Apply_outputAdd(t *testing.T) { }) plan2, diags := ctx1.Plan(m2, state1, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state2, diags := ctx2.Apply(plan2, m2) + state2, diags := ctx2.Apply(plan2, m2, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6618,9 +6853,9 @@ func TestContext2Apply_outputList(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6644,9 +6879,9 @@ func TestContext2Apply_outputMulti(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6670,9 +6905,9 @@ func TestContext2Apply_outputMultiIndex(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6728,7 +6963,7 @@ func TestContext2Apply_taintX(t *testing.T) { t.Logf("plan: %s", legacyDiffComparisonString(plan.Changes)) } - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6783,7 +7018,7 @@ func TestContext2Apply_taintDep(t *testing.T) { t.Logf("plan: %s", legacyDiffComparisonString(plan.Changes)) } - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6834,7 +7069,7 @@ func TestContext2Apply_taintDepRequiresNew(t *testing.T) { t.Logf("plan: %s", legacyDiffComparisonString(plan.Changes)) } - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6865,9 +7100,9 @@ func TestContext2Apply_targeted(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6905,9 +7140,9 @@ func TestContext2Apply_targetedCount(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6947,9 +7182,9 @@ func TestContext2Apply_targetedCountIndex(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -6977,7 +7212,10 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) - root.SetOutputValue("out", cty.StringVal("bar"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "out"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) child := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) child.SetResourceInstanceCurrent( @@ -6995,7 +7233,7 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { }, }) - if diags := ctx.Validate(m); diags.HasErrors() { + if diags := ctx.Validate(m, nil); diags.HasErrors() { t.Fatalf("validate errors: %s", diags.Err()) } @@ -7007,9 +7245,9 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7036,8 +7274,8 @@ func TestContext2Apply_targetedDestroy(t *testing.T) { // TODO: Future refactoring may enable us to remove the output from state in // this case, and that would be Just Fine - this test can be modified to // expect 0 outputs. - if len(mod.OutputValues) != 1 { - t.Fatalf("expected 1 outputs, got: %#v", mod.OutputValues) + if len(state.RootOutputValues) != 1 { + t.Fatalf("expected 1 outputs, got: %#v", state.RootOutputValues) } // the module instance should remain @@ -7086,9 +7324,9 @@ func TestContext2Apply_targetedDestroyCountDeps(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7152,9 +7390,9 @@ func TestContext2Apply_targetedDestroyModule(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7238,9 +7476,9 @@ func TestContext2Apply_targetedDestroyCountIndex(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7278,9 +7516,9 @@ func TestContext2Apply_targetedModule(t *testing.T) { addrs.RootModuleInstance.Child("child", addrs.NoKey), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7335,11 +7573,12 @@ func TestContext2Apply_targetedModuleDep(t *testing.T) { t.Logf("Diff: %s", legacyDiffComparisonString(plan.Changes)) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } + // The output from module.child is copied into aws_instance.foo.foo checkStateString(t, state, ` aws_instance.foo: ID = foo @@ -7355,11 +7594,7 @@ module.child: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance - - Outputs: - - output = foo - `) +`) } // GH-10911 untargeted outputs should not be in the graph, and therefore @@ -7385,9 +7620,9 @@ func TestContext2Apply_targetedModuleUnrelatedOutputs(t *testing.T) { addrs.RootModuleInstance.Child("child2", addrs.NoKey), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7407,10 +7642,6 @@ module.child2: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] type = aws_instance - - Outputs: - - instance_id = foo `) } @@ -7433,9 +7664,9 @@ func TestContext2Apply_targetedModuleResource(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7486,9 +7717,9 @@ func TestContext2Apply_targetedResourceOrphanModule(t *testing.T) { ), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } } @@ -7505,7 +7736,7 @@ func TestContext2Apply_unknownAttribute(t *testing.T) { } p.ApplyResourceChangeFn = testApplyFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -7525,9 +7756,9 @@ func TestContext2Apply_unknownAttribute(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Error("should error, because attribute 'unknown' is still unknown after apply") } @@ -7560,7 +7791,7 @@ func TestContext2Apply_vars(t *testing.T) { ctx := testContext2(t, opts) m := fixture.Config - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if len(diags) != 0 { t.Fatalf("bad: %s", diags.ErrWithWarnings()) } @@ -7603,9 +7834,9 @@ func TestContext2Apply_vars(t *testing.T) { Mode: plans.NormalMode, SetVariables: variables, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -7623,7 +7854,7 @@ func TestContext2Apply_varsEnv(t *testing.T) { ctx := testContext2(t, opts) m := fixture.Config - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if len(diags) != 0 { t.Fatalf("bad: %s", diags.ErrWithWarnings()) } @@ -7654,9 +7885,9 @@ func TestContext2Apply_varsEnv(t *testing.T) { Mode: plans.NormalMode, SetVariables: variables, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -7727,16 +7958,16 @@ func TestContext2Apply_createBefore_depends(t *testing.T) { plan, diags := ctx.Plan(m, state, DefaultPlanOpts) if diags.HasErrors() { - logDiagnostics(t, diags) + tfdiags.LogDiagnostics(t, diags) t.Fatal("plan failed") } else { t.Logf("plan:\n%s", legacyDiffComparisonString(plan.Changes)) } h.Active = true - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { - logDiagnostics(t, diags) + tfdiags.LogDiagnostics(t, diags) t.Fatal("apply failed") } @@ -7854,10 +8085,10 @@ func TestContext2Apply_singleDestroy(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) h.Active = true - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -7871,7 +8102,7 @@ func TestContext2Apply_singleDestroy(t *testing.T) { func TestContext2Apply_issue7824(t *testing.T) { p := testProvider("template") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "template_file": { Attributes: map[string]*configschema.Attribute{ @@ -7912,7 +8143,7 @@ func TestContext2Apply_issue7824(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -7926,7 +8157,7 @@ func TestContext2Apply_issue5254(t *testing.T) { p := testProvider("template") p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "template_file": { Attributes: map[string]*configschema.Attribute{ @@ -7952,7 +8183,7 @@ func TestContext2Apply_issue5254(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -7986,7 +8217,7 @@ func TestContext2Apply_issue5254(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8063,7 +8294,7 @@ func TestContext2Apply_targetedWithTaintedInState(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8091,7 +8322,7 @@ func TestContext2Apply_ignoreChangesCreate(t *testing.T) { p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn - instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block + instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Body instanceSchema.Attributes["required_field"] = &configschema.Attribute{ Type: cty.String, Required: true, @@ -8107,10 +8338,10 @@ func TestContext2Apply_ignoreChangesCreate(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -8214,10 +8445,10 @@ func TestContext2Apply_ignoreChangesWithDep(t *testing.T) { }) plan, diags := ctx.Plan(m, state.DeepCopy(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + s, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) actual := strings.TrimSpace(s.String()) expected := strings.TrimSpace(state.String()) @@ -8232,7 +8463,7 @@ func TestContext2Apply_ignoreChangesAll(t *testing.T) { p.PlanResourceChangeFn = testDiffFn p.ApplyResourceChangeFn = testApplyFn - instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block + instanceSchema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Body instanceSchema.Attributes["required_field"] = &configschema.Attribute{ Type: cty.String, Required: true, @@ -8246,14 +8477,14 @@ func TestContext2Apply_ignoreChangesAll(t *testing.T) { plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) if diags.HasErrors() { - logDiagnostics(t, diags) + tfdiags.LogDiagnostics(t, diags) t.Fatal("plan failed") } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) mod := state.RootModule() if len(mod.Resources) != 1 { @@ -8290,9 +8521,9 @@ func TestContext2Apply_destroyNestedModuleWithAttrsReferencingResource(t *testin // First plan and apply a create operation plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply err: %s", diags.Err()) } @@ -8326,7 +8557,7 @@ func TestContext2Apply_destroyNestedModuleWithAttrsReferencingResource(t *testin t.Fatalf("err: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("destroy apply err: %s", diags.Err()) } @@ -8385,10 +8616,10 @@ resource "null_instance" "depends" { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) root := state.Module(addrs.RootModuleInstance) is := root.ResourceInstance(addrs.Resource{ @@ -8412,7 +8643,7 @@ resource "null_instance" "depends" { // run another plan to make sure the data source doesn't show as a change plan, diags = ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, c := range plan.Changes.Resources { if c.Action != plans.NoOp { @@ -8451,7 +8682,7 @@ resource "null_instance" "depends" { }) plan, diags = ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) expectedChanges := map[string]plans.Action{ "null_instance.write": plans.Update, @@ -8479,14 +8710,14 @@ func TestContext2Apply_terraformWorkspace(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } - actual := state.RootModule().OutputValues["output"] + actual := state.RootOutputValues["output"] expected := cty.StringVal("foo") if actual == nil || actual.Value != expected { t.Fatalf("wrong value\ngot: %#v\nwant: %#v", actual.Value, expected) @@ -8505,9 +8736,9 @@ func TestContext2Apply_multiRef(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8535,9 +8766,9 @@ func TestContext2Apply_targetedModuleRecursive(t *testing.T) { addrs.RootModuleInstance.Child("child", addrs.NoKey), }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8570,9 +8801,9 @@ func TestContext2Apply_localVal(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } @@ -8605,7 +8836,10 @@ func TestContext2Apply_destroyWithLocals(t *testing.T) { }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) - root.SetOutputValue("name", cty.StringVal("test-bar"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "name"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("test-bar"), false, + ) ctx := testContext2(t, &ContextOpts{ Providers: map[addrs.Provider]providers.Factory{ @@ -8616,9 +8850,9 @@ func TestContext2Apply_destroyWithLocals(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - s, diags := ctx.Apply(plan, m) + s, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } @@ -8653,9 +8887,9 @@ func TestContext2Apply_providerWithLocals(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8669,9 +8903,9 @@ func TestContext2Apply_providerWithLocals(t *testing.T) { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("err: %s", diags.Err()) } @@ -8724,9 +8958,9 @@ func TestContext2Apply_destroyWithProviders(t *testing.T) { plan, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("error during apply: %s", diags.Err()) } @@ -8824,7 +9058,7 @@ func TestContext2Apply_providersFromState(t *testing.T) { t.Fatal(diags.Err()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -8880,7 +9114,7 @@ func TestContext2Apply_plannedInterpolatedCount(t *testing.T) { } // Applying the plan should now succeed - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply failed: %s", diags.Err()) } @@ -8913,7 +9147,10 @@ func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) - root.SetOutputValue("out", cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foo")}), false) + state.SetOutputValue( + addrs.OutputValue{Name: "out"}.Absolute(addrs.RootModuleInstance), + cty.ListVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("foo")}), false, + ) ctx := testContext2(t, &ContextOpts{ Providers: providers, @@ -8939,7 +9176,7 @@ func TestContext2Apply_plannedDestroyInterpolatedCount(t *testing.T) { } // Applying the plan should now succeed - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply failed: %s", diags.Err()) } @@ -8990,7 +9227,7 @@ func TestContext2Apply_scaleInMultivarRef(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) { addr := mustResourceInstanceAddr("aws_instance.one[0]") change := plan.Changes.ResourceInstance(addr) @@ -9030,14 +9267,14 @@ func TestContext2Apply_scaleInMultivarRef(t *testing.T) { } // Applying the plan should now succeed - _, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) } func TestContext2Apply_inconsistentWithPlan(t *testing.T) { m := testModule(t, "apply-inconsistent-with-plan") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test": { Attributes: map[string]*configschema.Attribute{ @@ -9069,9 +9306,9 @@ func TestContext2Apply_inconsistentWithPlan(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatalf("apply succeeded; want error") } @@ -9088,7 +9325,7 @@ func TestContext2Apply_inconsistentWithPlan(t *testing.T) { func TestContext2Apply_issue19908(t *testing.T) { m := testModule(t, "apply-issue19908") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test": { Attributes: map[string]*configschema.Attribute{ @@ -9134,9 +9371,9 @@ func TestContext2Apply_issue19908(t *testing.T) { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatalf("apply succeeded; want error") } @@ -9168,7 +9405,7 @@ func TestContext2Apply_issue19908(t *testing.T) { func TestContext2Apply_invalidIndexRef(t *testing.T) { p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -9185,7 +9422,7 @@ func TestContext2Apply_invalidIndexRef(t *testing.T) { addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } @@ -9224,7 +9461,7 @@ func TestContext2Apply_moduleReplaceCycle(t *testing.T) { }, } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": instanceSchema, }, @@ -9301,7 +9538,7 @@ func TestContext2Apply_moduleReplaceCycle(t *testing.T) { }, }) - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ @@ -9343,10 +9580,12 @@ func TestContext2Apply_moduleReplaceCycle(t *testing.T) { Changes: changes, PriorState: state.DeepCopy(), PrevRunState: state.DeepCopy(), + Applyable: true, + Complete: true, } t.Run(mode, func(t *testing.T) { - _, diags := ctx.Apply(plan, m) + _, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -9471,7 +9710,7 @@ func TestContext2Apply_destroyDataCycle(t *testing.T) { return resp } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -9500,7 +9739,7 @@ func TestContext2Apply_taintedDestroyFailure(t *testing.T) { return testApplyFn(req) } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -9580,7 +9819,7 @@ func TestContext2Apply_taintedDestroyFailure(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("expected error") } @@ -9693,7 +9932,7 @@ func TestContext2Apply_plannedConnectionRefs(t *testing.T) { t.Fatalf("diags: %s", diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -9809,7 +10048,7 @@ func TestContext2Apply_cbdCycle(t *testing.T) { t.Fatalf("failed to create context for plan: %s", diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -9819,7 +10058,7 @@ func TestContext2Apply_ProviderMeta_apply_set(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -9854,10 +10093,10 @@ func TestContext2Apply_ProviderMeta_apply_set(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) if !p.ApplyResourceChangeCalled { t.Fatalf("ApplyResourceChange not called") @@ -9901,7 +10140,7 @@ func TestContext2Apply_ProviderMeta_apply_unset(t *testing.T) { m := testModule(t, "provider-meta-unset") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -9934,10 +10173,10 @@ func TestContext2Apply_ProviderMeta_apply_unset(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) if !p.ApplyResourceChangeCalled { t.Fatalf("ApplyResourceChange not called") @@ -9959,7 +10198,7 @@ func TestContext2Apply_ProviderMeta_apply_unset(t *testing.T) { func TestContext2Apply_ProviderMeta_plan_set(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -9983,7 +10222,7 @@ func TestContext2Apply_ProviderMeta_plan_set(t *testing.T) { }) _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.PlanResourceChangeCalled { t.Fatalf("PlanResourceChange not called") @@ -10026,7 +10265,7 @@ func TestContext2Apply_ProviderMeta_plan_set(t *testing.T) { func TestContext2Apply_ProviderMeta_plan_unset(t *testing.T) { m := testModule(t, "provider-meta-unset") p := testProvider("test") - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -10050,7 +10289,7 @@ func TestContext2Apply_ProviderMeta_plan_unset(t *testing.T) { }) _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.PlanResourceChangeCalled { t.Fatalf("PlanResourceChange not called") @@ -10111,7 +10350,7 @@ func TestContext2Apply_ProviderMeta_plan_setInvalid(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "quux": { @@ -10163,7 +10402,7 @@ func TestContext2Apply_ProviderMeta_refresh_set(t *testing.T) { m := testModule(t, "provider-meta-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -10175,7 +10414,7 @@ func TestContext2Apply_ProviderMeta_refresh_set(t *testing.T) { rrcPMs := map[string]cty.Value{} p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { rrcPMs[req.TypeName] = req.ProviderMeta - newState, err := p.GetProviderSchemaResponse.ResourceTypes[req.TypeName].Block.CoerceValue(req.PriorState) + newState, err := p.GetProviderSchemaResponse.ResourceTypes[req.TypeName].Body.CoerceValue(req.PriorState) if err != nil { panic(err) } @@ -10190,13 +10429,13 @@ func TestContext2Apply_ProviderMeta_refresh_set(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) _, diags = ctx.Refresh(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.ReadResourceCalled { t.Fatalf("ReadResource not called") @@ -10242,7 +10481,7 @@ func TestContext2Apply_ProviderMeta_refresh_setNoSchema(t *testing.T) { p.PlanResourceChangeFn = testDiffFn // we need a schema for plan/apply so they don't error - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -10259,10 +10498,10 @@ func TestContext2Apply_ProviderMeta_refresh_setNoSchema(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) // drop the schema before refresh, to test that it errors schema.ProviderMeta = nil @@ -10307,7 +10546,7 @@ func TestContext2Apply_ProviderMeta_refresh_setInvalid(t *testing.T) { p.PlanResourceChangeFn = testDiffFn // we need a matching schema for plan/apply so they don't error - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -10324,10 +10563,10 @@ func TestContext2Apply_ProviderMeta_refresh_setInvalid(t *testing.T) { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) // change the schema before refresh, to test that it errors schema.ProviderMeta = &configschema.Block{ @@ -10381,7 +10620,7 @@ func TestContext2Apply_ProviderMeta_refreshdata_set(t *testing.T) { m := testModule(t, "provider-meta-data-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -10425,13 +10664,13 @@ func TestContext2Apply_ProviderMeta_refreshdata_set(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) _, diags = ctx.Refresh(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.ReadDataSourceCalled { t.Fatalf("ReadDataSource not called") @@ -10475,7 +10714,7 @@ func TestContext2Apply_ProviderMeta_refreshdata_unset(t *testing.T) { m := testModule(t, "provider-meta-data-unset") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "baz": { @@ -10516,10 +10755,10 @@ func TestContext2Apply_ProviderMeta_refreshdata_unset(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + _, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) if !p.ReadDataSourceCalled { t.Fatalf("ReadDataSource not called") @@ -10586,7 +10825,7 @@ func TestContext2Apply_ProviderMeta_refreshdata_setInvalid(t *testing.T) { m := testModule(t, "provider-meta-data-set") p := testProvider("test") p.PlanResourceChangeFn = testDiffFn - schema := p.ProviderSchema() + schema := getProviderSchema(p) schema.ProviderMeta = &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "quux": { @@ -10652,6 +10891,14 @@ module "mod2" { source = "./mod" in = module.mod1["a"].out } + +output "mod1" { + value = module.mod1 +} + +output "mod2" { + value = module.mod2 +} `, "mod/main.tf": ` resource "aws_instance" "foo" { @@ -10683,22 +10930,23 @@ output "out" { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } expected := ` +Outputs: + +mod1 = {a:map[out:foo] } +mod2 = {out:foo } + module.mod1["a"]: aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] foo = default type = aws_instance - - Outputs: - - out = foo module.mod2: aws_instance.foo: ID = foo @@ -10742,7 +10990,7 @@ resource "aws_instance" "cbd" { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -10814,7 +11062,7 @@ func TestContext2Apply_moduleDependsOn(t *testing.T) { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -10871,7 +11119,7 @@ output "c" { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -10889,7 +11137,7 @@ output "c" { t.Fatal(diags.ErrWithWarnings()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -10938,7 +11186,7 @@ output "myoutput" { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -10956,7 +11204,7 @@ output "myoutput" { t.Fatal(diags.ErrWithWarnings()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -11130,13 +11378,13 @@ locals { } } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { log.Fatal(diags.ErrWithWarnings()) } // check the output, as those can't cause an error planning the value - out := state.RootModule().OutputValues["out"].Value.AsString() + out := state.RootOutputValues["out"].Value.AsString() if out != "a0" { t.Fatalf(`expected output "a0", got: %q`, out) } @@ -11185,13 +11433,13 @@ locals { } } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } // check the output, as those can't cause an error planning the value - out = state.RootModule().OutputValues["out"].Value.AsString() + out = state.RootOutputValues["out"].Value.AsString() if out != "" { t.Fatalf(`expected output "", got: %q`, out) } @@ -11200,36 +11448,10 @@ locals { // Ensure that we can destroy when a provider references a resource that will // also be destroyed func TestContext2Apply_destroyProviderReference(t *testing.T) { - m := testModuleInline(t, map[string]string{ - "main.tf": ` -provider "null" { - value = "" -} + m, snap := testModuleWithSnapshot(t, "apply-destroy-provisider-refs") -module "mod" { - source = "./mod" -} - -provider "test" { - value = module.mod.output -} - -resource "test_instance" "bar" { -} -`, - "mod/main.tf": ` -data "null_data_source" "foo" { - count = 1 -} - - -output "output" { - value = data.null_data_source.foo[0].output -} -`}) - - schemaFn := func(name string) *ProviderSchema { - return &ProviderSchema{ + schemaFn := func(name string) *providerSchema { + return &providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": { @@ -11269,7 +11491,7 @@ output "output" { } } - testP := new(MockProvider) + testP := new(testing_provider.MockProvider) testP.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } @@ -11294,7 +11516,7 @@ output "output" { } testP.PlanResourceChangeFn = testDiffFn - nullP := new(MockProvider) + nullP := new(testing_provider.MockProvider) nullP.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } @@ -11318,26 +11540,40 @@ output "output" { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } + providers := map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), + addrs.NewDefaultProvider("null"): testProviderFuncFixed(nullP), + } ctx = testContext2(t, &ContextOpts{ - Providers: map[addrs.Provider]providers.Factory{ - addrs.NewDefaultProvider("test"): testProviderFuncFixed(testP), - addrs.NewDefaultProvider("null"): testProviderFuncFixed(nullP), - }, + Providers: providers, }) plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + // We'll marshal and unmarshal the plan here, to ensure that we have + // a clean new context as would be created if we separately ran + // terraform plan -out=tfplan && terraform apply tfplan + ctxOpts, m, plan, err := contextOptsForPlanViaFile(t, snap, plan) + if err != nil { + t.Fatal(err) + } + ctxOpts.Providers = providers + ctx, diags = NewContext(ctxOpts) + + if diags.HasErrors() { + t.Fatalf("failed to create context for plan: %s", diags.Err()) + } + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("destroy apply errors: %s", diags.Err()) } } @@ -11411,9 +11647,9 @@ output "outputs" { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11428,9 +11664,9 @@ output "outputs" { plan, diags = ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("destroy apply errors: %s", diags.Err()) } @@ -11498,9 +11734,9 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11519,9 +11755,9 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11561,9 +11797,9 @@ resource "test_instance" "b" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11582,9 +11818,9 @@ resource "test_instance" "b" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11622,9 +11858,9 @@ resource "test_resource" "c" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11643,9 +11879,9 @@ resource "test_resource" "c" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11673,11 +11909,11 @@ resource "test_resource" "foo" { }`, }) - p := new(MockProvider) + p := new(testing_provider.MockProvider) p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { return providers.ReadResourceResponse{NewState: req.PriorState} } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "test_resource": { @@ -11715,9 +11951,9 @@ resource "test_resource" "foo" { }) plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11730,9 +11966,9 @@ resource "test_resource" "foo" { }) plan, diags = ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11753,9 +11989,9 @@ resource "test_resource" "foo" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11792,24 +12028,27 @@ resource "test_resource" "foo" { t.Fatalf("plan errors: %s", diags.Err()) } - verifySensitiveValue := func(pvms []cty.PathValueMarks) { - if len(pvms) != 1 { - t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) + verifySensitiveValue := func(paths []cty.Path) { + if len(paths) != 2 { + t.Fatalf("expected 2 sensitive paths, got %d", len(paths)) } - pvm := pvms[0] - if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) { - t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) - } - if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) + + for _, path := range paths { + switch { + case path.Equals(cty.GetAttrPath("value")): + case path.Equals(cty.GetAttrPath("sensitive_value")): + default: + t.Errorf("unexpected sensitive path: %#v", path) + return + } } } addr := mustResourceInstanceAddr("test_resource.foo") fooChangeSrc := plan.Changes.ResourceInstance(addr) - verifySensitiveValue(fooChangeSrc.AfterValMarks) + verifySensitiveValue(fooChangeSrc.AfterSensitivePaths) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11855,16 +12094,23 @@ resource "test_resource" "baz" { t.Fatalf("plan errors: %s", diags.Err()) } - verifySensitiveValue := func(pvms []cty.PathValueMarks) { - if len(pvms) != 1 { - t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) - } - pvm := pvms[0] - if gotPath, wantPath := pvm.Path, cty.GetAttrPath("value"); !gotPath.Equals(wantPath) { - t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) - } - if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) + wantSensitivePaths := []cty.Path{ + cty.GetAttrPath("value"), + cty.GetAttrPath("sensitive_value"), + cty.GetAttrPath("nesting_single").GetAttr("sensitive_value"), + } + verifySensitiveValue := func(gotSensitivePaths []cty.Path) { + for _, gotPath := range gotSensitivePaths { + wantSensitive := false + for _, wantPath := range wantSensitivePaths { + if wantPath.Equals(gotPath) { + wantSensitive = true + break + } + } + if !wantSensitive { + t.Errorf("unexpected sensitive path %s", tfdiags.FormatCtyPath(gotPath)) + } } } @@ -11873,13 +12119,13 @@ resource "test_resource" "baz" { // "bar" references sensitive resources in "foo" barAddr := mustResourceInstanceAddr("test_resource.bar") barChangeSrc := plan.Changes.ResourceInstance(barAddr) - verifySensitiveValue(barChangeSrc.AfterValMarks) + verifySensitiveValue(barChangeSrc.AfterSensitivePaths) bazAddr := mustResourceInstanceAddr("test_resource.baz") bazChangeSrc := plan.Changes.ResourceInstance(bazAddr) - verifySensitiveValue(bazChangeSrc.AfterValMarks) + verifySensitiveValue(bazChangeSrc.AfterSensitivePaths) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -11933,26 +12179,27 @@ resource "test_resource" "foo" { }) plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) addr := mustResourceInstanceAddr("test_resource.foo") - state, diags = ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags = ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) fooState := state.ResourceInstance(addr) - if len(fooState.Current.AttrSensitivePaths) != 1 { - t.Fatalf("wrong number of sensitive paths, expected 1, got, %v", len(fooState.Current.AttrSensitivePaths)) - } - got := fooState.Current.AttrSensitivePaths[0] - want := cty.PathValueMarks{ - Path: cty.GetAttrPath("value"), - Marks: cty.NewValueMarks(marks.Sensitive), + if len(fooState.Current.AttrSensitivePaths) != 2 { + t.Fatalf("wrong number of sensitive paths, expected 2, got, %v", len(fooState.Current.AttrSensitivePaths)) } - if !got.Equal(want) { - t.Fatalf("wrong value marks; got:\n%#v\n\nwant:\n%#v\n", got, want) + for _, path := range fooState.Current.AttrSensitivePaths { + switch { + case path.Equals(cty.GetAttrPath("value")): + case path.Equals(cty.GetAttrPath("sensitive_value")): + default: + t.Errorf("unexpected sensitive path: %#v", path) + return + } } m2 := testModuleInline(t, map[string]string{ @@ -11967,33 +12214,22 @@ resource "test_resource" "foo" { }`, }) - ctx2 := testContext2(t, &ContextOpts{ + ctx = testContext2(t, &ContextOpts{ Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) - // NOTE: Prior to our refactoring to make the state an explicit argument - // of Plan, as opposed to hidden state inside Context, this test was - // calling ctx.Apply instead of ctx2.Apply and thus using the previous - // plan instead of this new plan. "Fixing" it to use the new plan seems - // to break the test, so we've preserved that oddity here by saving the - // old plan as oldPlan and essentially discarding the new plan entirely, - // but this seems rather suspicious and we should ideally figure out what - // this test was originally intending to do and make it do that. - oldPlan := plan - _, diags = ctx2.Plan(m2, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) - stateWithoutSensitive, diags := ctx.Apply(oldPlan, m) - assertNoErrors(t, diags) + plan, diags = ctx.Plan(m2, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + stateWithoutSensitive, diags := ctx.Apply(plan, m2, nil) + tfdiags.AssertNoErrors(t, diags) fooState2 := stateWithoutSensitive.ResourceInstance(addr) - if len(fooState2.Current.AttrSensitivePaths) > 0 { - t.Fatalf( - "wrong number of sensitive paths, expected 0, got, %v\n%s", + if len(fooState2.Current.AttrSensitivePaths) != 1 { + t.Fatalf("wrong number of sensitive paths, expected 1, got, %v\n%#v\n", len(fooState2.Current.AttrSensitivePaths), - spew.Sdump(fooState2.Current.AttrSensitivePaths), - ) + fooState2.Current.AttrSensitivePaths) } } @@ -12038,12 +12274,11 @@ output "out" { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } - - got := state.RootModule().OutputValues["out"].Value + got := state.RootOutputValues["out"].Value want := cty.ObjectVal(map[string]cty.Value{ "required": cty.StringVal("boop"), @@ -12097,12 +12332,12 @@ output "out" { t.Fatal(diags.ErrWithWarnings()) } - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } - got := state.RootModule().OutputValues["out"].Value + got := state.RootOutputValues["out"].Value want := cty.ObjectVal(map[string]cty.Value{ "required": cty.StringVal("boop"), @@ -12119,76 +12354,220 @@ output "out" { } } -func TestContext2Apply_provisionerSensitive(t *testing.T) { - m := testModule(t, "apply-provisioner-sensitive") - p := testProvider("aws") +func TestContext2Apply_moduleVariableOptionalAttributesDefaultNull(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "in" { + type = object({ + required = string + optional = optional(string) + default = optional(bool, true) + }) + default = null +} - pr := testProvisioner() - pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { - if req.Config.ContainsMarked() { - t.Fatalf("unexpectedly marked config value: %#v", req.Config) - } - command := req.Config.GetAttr("command") - if command.IsMarked() { - t.Fatalf("unexpectedly marked command argument: %#v", command.Marks()) - } - req.UIOutput.Output(fmt.Sprintf("Executing: %q", command.AsString())) - return +# Wrap the input variable in a tuple because a null output value is elided from +# the plan, which prevents us from testing its type. +output "out" { + value = [var.in] +} +`}) + + ctx := testContext2(t, &ContextOpts{}) + + // We don't specify a value for the variable here, relying on its defined + // default. + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) } - p.PlanResourceChangeFn = testDiffFn - p.ApplyResourceChangeFn = testApplyFn - h := new(MockHook) - ctx := testContext2(t, &ContextOpts{ - Hooks: []Hook{h}, - Providers: map[addrs.Provider]providers.Factory{ - addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), - }, - Provisioners: map[string]provisioners.Factory{ - "shell": testProvisionerFuncFixed(pr), - }, + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + got := state.RootOutputValues["out"].Value + // The null default value should be bound, after type converting to the + // full object type + want := cty.TupleVal([]cty.Value{cty.NullVal(cty.Object(map[string]cty.Type{ + "required": cty.String, + "optional": cty.String, + "default": cty.Bool, + }))}) + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } +} + +func TestContext2Apply_moduleVariableOptionalAttributesDefaultChild(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "in" { + type = list(object({ + a = optional(set(string)) + })) + default = [ + { a = [ "foo" ] }, + { }, + ] +} + +module "child" { + source = "./child" + in = var.in +} + +output "out" { + value = module.child.out +} +`, + "child/main.tf": ` +variable "in" { + type = list(object({ + a = optional(set(string), []) + })) + default = [] +} + +output "out" { + value = var.in +} +`, }) - plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ - Mode: plans.NormalMode, - SetVariables: InputValues{ - "password": &InputValue{ - Value: cty.StringVal("secret"), - SourceType: ValueFromCaller, + ctx := testContext2(t, &ContextOpts{}) + + // We don't specify a value for the variable here, relying on its defined + // default. + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + state, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + got := state.RootOutputValues["out"].Value + want := cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "a": cty.SetVal([]cty.Value{cty.StringVal("foo")}), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.SetValEmpty(cty.String), + }), + }) + if !want.RawEquals(got) { + t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, want) + } +} + +func TestContext2Apply_provisionerMarks(t *testing.T) { + tcs := map[string]struct { + opts *ApplyOpts + want string + }{ + "apply-provisioner-sensitive": { + want: "output suppressed due to sensitive value", + }, + "apply-provisioner-sensitive-ephemeral": { + want: "output suppressed due to sensitive, ephemeral value", + opts: &ApplyOpts{ + SetVariables: InputValues{ + "password": &InputValue{ + Value: cty.StringVal("secret"), + SourceType: ValueFromCaller, + }, + }, + }, + }, + "apply-provisioner-ephemeral": { + want: "output suppressed due to ephemeral value", + opts: &ApplyOpts{ + SetVariables: InputValues{ + "password": &InputValue{ + Value: cty.StringVal("secret"), + SourceType: ValueFromCaller, + }, + }, }, }, - }) - assertNoErrors(t, diags) - - // "restart" provisioner - pr.CloseCalled = false - - state, diags := ctx.Apply(plan, m) - if diags.HasErrors() { - logDiagnostics(t, diags) - t.Fatal("apply failed") } + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + m := testModule(t, name) + p := testProvider("aws") - actual := strings.TrimSpace(state.String()) - expected := strings.TrimSpace(testTerraformApplyProvisionerSensitiveStr) - if actual != expected { - t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) - } + pr := testProvisioner() + pr.ProvisionResourceFn = func(req provisioners.ProvisionResourceRequest) (resp provisioners.ProvisionResourceResponse) { + if req.Config.ContainsMarked() { + t.Fatalf("unexpectedly marked config value: %#v", req.Config) + } + command := req.Config.GetAttr("command") + if command.IsMarked() { + t.Fatalf("unexpectedly marked command argument: %#v", command.Marks()) + } + req.UIOutput.Output(fmt.Sprintf("Executing: %q", command.AsString())) + return + } + p.PlanResourceChangeFn = testDiffFn + p.ApplyResourceChangeFn = testApplyFn - // Verify apply was invoked - if !pr.ProvisionResourceCalled { - t.Fatalf("provisioner was not called on apply") - } + h := new(MockHook) + ctx := testContext2(t, &ContextOpts{ + Hooks: []Hook{h}, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + Provisioners: map[string]provisioners.Factory{ + "shell": testProvisionerFuncFixed(pr), + }, + }) - // Verify output was suppressed - if !h.ProvisionOutputCalled { - t.Fatalf("ProvisionOutput hook not called") - } - if got, doNotWant := h.ProvisionOutputMessage, "secret"; strings.Contains(got, doNotWant) { - t.Errorf("sensitive value %q included in output:\n%s", doNotWant, got) - } - if got, want := h.ProvisionOutputMessage, "output suppressed"; !strings.Contains(got, want) { - t.Errorf("expected hook to be called with %q, but was:\n%s", want, got) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "password": &InputValue{ + Value: cty.StringVal("secret"), + SourceType: ValueFromCaller, + }, + }, + }) + tfdiags.AssertNoErrors(t, diags) + + // "restart" provisioner + pr.CloseCalled = false + + state, diags := ctx.Apply(plan, m, tc.opts) + if diags.HasErrors() { + tfdiags.LogDiagnostics(t, diags) + t.Fatal("apply failed") + } + + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testTerraformApplyProvisionerSensitiveStr) + if actual != expected { + t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) + } + + // Verify apply was invoked + if !pr.ProvisionResourceCalled { + t.Fatalf("provisioner was not called on apply") + } + + // Verify output was suppressed + if !h.ProvisionOutputCalled { + t.Fatalf("ProvisionOutput hook not called") + } + if got, doNotWant := h.ProvisionOutputMessage, "secret"; strings.Contains(got, doNotWant) { + t.Errorf("sensitive value %q included in output:\n%s", doNotWant, got) + } + if got, want := h.ProvisionOutputMessage, tc.want; !strings.Contains(got, want) { + t.Errorf("expected hook to be called with %q, but was:\n%s", want, got) + } + }) } } @@ -12216,9 +12595,9 @@ resource "test_resource" "foo" { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, diags := ctx.Apply(plan, m) + state, diags := ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } @@ -12245,7 +12624,7 @@ resource "test_instance" "a" { return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -12265,7 +12644,7 @@ resource "test_instance" "a" { t.Fatal(diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -12306,26 +12685,23 @@ func TestContext2Apply_dataSensitive(t *testing.T) { if diags.HasErrors() { t.Fatalf("diags: %s", diags.Err()) } else { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } - state, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) addr := mustResourceInstanceAddr("data.null_data_source.testing") dataSourceState := state.ResourceInstance(addr) - pvms := dataSourceState.Current.AttrSensitivePaths - if len(pvms) != 1 { - t.Fatalf("expected 1 sensitive path, got %d", len(pvms)) + sensitivePaths := dataSourceState.Current.AttrSensitivePaths + if len(sensitivePaths) != 1 { + t.Fatalf("expected 1 sensitive path, got %d", len(sensitivePaths)) } - pvm := pvms[0] - if gotPath, wantPath := pvm.Path, cty.GetAttrPath("foo"); !gotPath.Equals(wantPath) { + sensitivePath := sensitivePaths[0] + if gotPath, wantPath := sensitivePath, cty.GetAttrPath("foo"); !gotPath.Equals(wantPath) { t.Errorf("wrong path\n got: %#v\nwant: %#v", gotPath, wantPath) } - if gotMarks, wantMarks := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !gotMarks.Equal(wantMarks) { - t.Errorf("wrong marks\n got: %#v\nwant: %#v", gotMarks, wantMarks) - } } func TestContext2Apply_errorRestorePrivateData(t *testing.T) { @@ -12362,7 +12738,10 @@ func TestContext2Apply_errorRestorePrivateData(t *testing.T) { t.Fatal(diags.Err()) } - state, _ = ctx.Apply(plan, m) + state, _ = ctx.Apply(plan, m, nil) + if state.Empty() { + t.Fatal("no state at all") + } if string(state.ResourceInstance(addr).Current.Private) != "private" { t.Fatal("missing private data in state") } @@ -12407,7 +12786,10 @@ func TestContext2Apply_errorRestoreStatus(t *testing.T) { t.Fatal(diags.Err()) } - state, diags = ctx.Apply(plan, m) + state, diags = ctx.Apply(plan, m, nil) + if state.Empty() { + t.Fatal("no state at all") + } errString := diags.ErrWithWarnings().Error() if !strings.Contains(errString, "oops") || !strings.Contains(errString, "warned") { @@ -12469,7 +12851,7 @@ resource "test_object" "a" { t.Fatal(diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) errString := diags.ErrWithWarnings().Error() if !strings.Contains(errString, "oops") || !strings.Contains(errString, "warned") { t.Fatalf("error missing expected info: %q", errString) @@ -12505,7 +12887,7 @@ resource "test_object" "a" { t.Fatal(diags.Err()) } - _, diags = ctx.Apply(plan, m) + _, diags = ctx.Apply(plan, m, nil) if !diags.HasErrors() { t.Fatal("expected and error") } diff --git a/internal/terraform/context_eval.go b/internal/terraform/context_eval.go index f9d0f64933..c3f54df240 100644 --- a/internal/terraform/context_eval.go +++ b/internal/terraform/context_eval.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,12 +9,17 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) type EvalOpts struct { SetVariables InputValues + + // ExternalProviders is a set of pre-configured provider instances with + // the same purpose as [PlanOpts.ExternalProviders]. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface } // Eval produces a scope in which expressions can be evaluated for @@ -45,7 +53,10 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a state = state.DeepCopy() var walker *ContextGraphWalker - variables := opts.SetVariables + var variables InputValues + if opts != nil { + variables = opts.SetVariables + } // By the time we get here, we should have values defined for all of // the root module variables, even if some of them are "unknown". It's the @@ -57,13 +68,19 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a varDiags := checkInputVariables(config.Module.Variables, variables) diags = diags.Append(varDiags) + var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + if opts != nil { + externalProviderConfigs = opts.ExternalProviders + } + log.Printf("[DEBUG] Building and walking 'eval' graph") graph, moreDiags := (&EvalGraphBuilder{ - Config: config, - State: state, - RootVariableValues: variables, - Plugins: c.plugins, + Config: config, + State: state, + RootVariableValues: variables, + ExternalProviderConfigs: externalProviderConfigs, + Plugins: c.plugins, }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -83,14 +100,22 @@ func (c *Context) Eval(config *configs.Config, state *states.State, moduleAddr a // If we skipped walking the graph (due to errors) then we'll just // use a placeholder graph walker here, which'll refer to the // unmodified state. - walker = c.graphWalker(walkEval, walkOpts) + walker = c.graphWalker(graph, walkEval, walkOpts) } + return evalScopeFromGraphWalk(walker, moduleAddr), diags +} + +// evalScopeFromGraphWalk takes a [ContextGraphWalker] that was already used +// to run a graph walk and derives from it an evaluation scope which can +// evaluate expressions for a given module path in whatever mode makes sense +// for how the graph walker is configured. +func evalScopeFromGraphWalk(walker *ContextGraphWalker, moduleAddr addrs.ModuleInstance) *lang.Scope { // This is a bit weird since we don't normally evaluate outside of // the context of a walk, but we'll "re-enter" our desired path here // just to get hold of an EvalContext for it. ContextGraphWalker // caches its contexts, so we should get hold of the context that was // previously used for evaluation here, unless we skipped walking. - evalCtx := walker.EnterPath(moduleAddr) - return evalCtx.EvaluationScope(nil, EvalDataForNoInstanceKey), diags + evalCtx := walker.enterScope(evalContextModuleInstance{Addr: moduleAddr}) + return evalCtx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) } diff --git a/internal/terraform/context_eval_test.go b/internal/terraform/context_eval_test.go index 0bd752935e..d54cf678f3 100644 --- a/internal/terraform/context_eval_test.go +++ b/internal/terraform/context_eval_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,10 +8,16 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/hcl/v2/hcltest" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestContextEval(t *testing.T) { @@ -85,3 +94,284 @@ func TestContextEval(t *testing.T) { }) } } + +// ensure that we can execute a console when outputs have preconditions +func TestContextEval_outputsWithPreconditions(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "mod" { + source = "./mod" + input = "ok" +} + +output "out" { + value = module.mod.out +} +`, + + "./mod/main.tf": ` +variable "input" { + type = string +} + +output "out" { + value = var.input + + precondition { + condition = var.input != "" + error_message = "error" + } +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Eval(m, states.NewState(), addrs.RootModuleInstance, &EvalOpts{ + SetVariables: testInputValuesUnset(m.Module.Variables), + }) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContextPlanAndEval(t *testing.T) { + // This test actually performs a plan walk rather than an eval walk, but + // it's here because PlanAndEval is thematically related to the evaluation + // walk, with the same effect of producing a lang.Scope that the caller + // can use to evaluate arbitrary expressions. + + m := testModule(t, "planandeval-basic") + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_thing": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "arg": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, scope, diags := ctx.PlanAndEval(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "a": { + Value: cty.StringVal("a value"), + }, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // This test isn't really about whether the plan is correct, but we'll + // do some basic checks on it anyway because if the plan is incorrect + // then the evaluation scope will probably behave oddly too. + if plan.Errored { + t.Error("plan is marked as errored; want success") + } + riAddr := mustResourceInstanceAddr("test_thing.a") + if plan.Changes != nil { + if rc := plan.Changes.ResourceInstance(riAddr); rc == nil { + t.Errorf("plan does not include a change for test_thing.a") + } else if got, want := rc.Action, plans.Create; got != want { + t.Errorf("wrong planned action for test_thing.a\ngot: %s\nwant: %s", got, want) + } + if _, ok := plan.VariableValues["a"]; !ok { + t.Errorf("plan does not track value for var.a") + } + } else { + t.Fatalf("plan has no Changes") + } + if plan.PriorState == nil { + t.Fatalf("plan has no PriorState") + } + if plan.PrevRunState == nil { + t.Fatalf("plan has no PrevRunState") + } + + if scope == nil { + // It's okay for scope to be nil when there are errors, but if we + // successfully created a plan then it should always be set. + t.Fatal("PlanAndEval returned nil scope") + } + + t.Run("var.a", func(t *testing.T) { + expr := hcltest.MockExprTraversalSrc(`var.a`) + want := cty.StringVal("a value") + got, diags := scope.EvalExpr(expr, cty.String) + tfdiags.AssertNoDiagnostics(t, diags) + + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("test_thing.a", func(t *testing.T) { + expr := hcltest.MockExprTraversalSrc(`test_thing.a`) + want := cty.ObjectVal(map[string]cty.Value{ + "arg": cty.StringVal("a value"), + }) + got, diags := scope.EvalExpr(expr, cty.DynamicPseudoType) + tfdiags.AssertNoDiagnostics(t, diags) + + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) +} + +func TestContextApplyAndEval(t *testing.T) { + // This test actually performs plan and apply walks rather than an eval + // walk, but it's here because ApplyAndEval is thematically related to the + // evaluation walk, with the same effect of producing a lang.Scope that the + // caller can use to evaluate arbitrary expressions. + + m := testModule(t, "planandeval-basic") + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_thing": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "arg": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "a": { + Value: cty.StringVal("a value"), + }, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // This test isn't really about whether the plan is correct, but we'll + // do some basic checks on it anyway because if the plan is incorrect + // then the evaluation scope will probably behave oddly too. + if plan.Errored { + t.Error("plan is marked as errored; want success") + } + riAddr := mustResourceInstanceAddr("test_thing.a") + if plan.Changes != nil { + if rc := plan.Changes.ResourceInstance(riAddr); rc == nil { + t.Errorf("plan does not include a change for test_thing.a") + } else if got, want := rc.Action, plans.Create; got != want { + t.Errorf("wrong planned action for test_thing.a\ngot: %s\nwant: %s", got, want) + } + if _, ok := plan.VariableValues["a"]; !ok { + t.Errorf("plan does not track value for var.a") + } + } else { + t.Fatalf("plan has no Changes") + } + if plan.PriorState == nil { + t.Fatalf("plan has no PriorState") + } + if plan.PrevRunState == nil { + t.Fatalf("plan has no PrevRunState") + } + + finalState, scope, diags := ctx.ApplyAndEval(plan, m, nil) + tfdiags.AssertNoDiagnostics(t, diags) + if finalState == nil { + t.Fatalf("no final state") + } + + if scope == nil { + // It's okay for scope to be nil when there are errors, but if we + // successfully applied the plan then it should always be set. + t.Fatal("ApplyAndEval returned nil scope") + } + + t.Run("var.a", func(t *testing.T) { + expr := hcltest.MockExprTraversalSrc(`var.a`) + want := cty.StringVal("a value") + got, diags := scope.EvalExpr(expr, cty.String) + tfdiags.AssertNoDiagnostics(t, diags) + + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) + t.Run("test_thing.a", func(t *testing.T) { + expr := hcltest.MockExprTraversalSrc(`test_thing.a`) + want := cty.ObjectVal(map[string]cty.Value{ + "arg": cty.StringVal("a value"), + }) + got, diags := scope.EvalExpr(expr, cty.DynamicPseudoType) + tfdiags.AssertNoDiagnostics(t, diags) + + if !want.RawEquals(got) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) + } + }) +} + +func TestContextEval_ephemeralResource(t *testing.T) { + // make sure referenced to ephemeral resources are st least valid in the + // console, even if they are not known + m := testModuleInline(t, map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" {} + +locals { + composedString = "prefix-${ephemeral.ephem_resource.data.value}-suffix" +} + `, + }) + + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + EphemeralResourceTypes: map[string]providers.Schema{ + "ephem_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Eval(m, states.NewState(), addrs.RootModuleInstance, &EvalOpts{ + SetVariables: testInputValuesUnset(m.Module.Variables), + }) + tfdiags.AssertNoErrors(t, diags) +} diff --git a/internal/terraform/context_fixtures_test.go b/internal/terraform/context_fixtures_test.go index 2e9e9c2751..2c63a10489 100644 --- a/internal/terraform/context_fixtures_test.go +++ b/internal/terraform/context_fixtures_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/context_functions_test.go b/internal/terraform/context_functions_test.go new file mode 100644 index 0000000000..af174ffe4c --- /dev/null +++ b/internal/terraform/context_functions_test.go @@ -0,0 +1,388 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/msgpack" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestContext2Plan_providerFunctionBasic(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +locals { + input = { + key = "value" + } + + expected = { + key = "value" + } +} + +output "noop_equals" { + // The false branch will fail to evaluate entirely if our condition doesn't + // hold true. This is not a normal way to check a condition, but it's been + // seen in the wild, so adding it here for variety. + value = provider::test::noop(local.input) == local.expected ? "ok" : {}["fail"] +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionDecl{ + "noop": providers.FunctionDecl{ + Parameters: []providers.FunctionParam{ + { + Name: "any", + Type: cty.DynamicPseudoType, + }, + }, + ReturnType: cty.DynamicPseudoType, + }, + }, + } + p.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + resp.Result = req.Arguments[0] + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + expect, err := msgpack.Marshal(cty.StringVal("ok"), cty.DynamicPseudoType) + if err != nil { + t.Fatal(err) + } + + // there is exactly one output, which is a dynamically typed string + if !bytes.Equal(expect, plan.Changes.Outputs[0].After) { + t.Fatalf("got output dynamic value of %q", plan.Changes.Outputs[0].After) + } +} + +// check that provider functions called multiple times during validate and plan +// return consistent results +func TestContext2Plan_providerFunctionImpurePlan(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +output "first" { + value = provider::test::echo("input") +} + +output "second" { + value = provider::test::echo("input") +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionDecl{ + "echo": providers.FunctionDecl{ + Parameters: []providers.FunctionParam{ + { + Name: "arg", + Type: cty.String, + }, + }, + ReturnType: cty.String, + }, + }, + } + + inc := 0 + p.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + // this broken echo adds a counter to the argument + resp.Result = cty.StringVal(fmt.Sprintf("%s-%d", req.Arguments[0].AsString(), inc)) + inc++ + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + errs := diags.Err().Error() + if !strings.Contains(errs, "function returned an inconsistent result") { + t.Fatalf("expected error with %q, got %q", "provider function returned an inconsistent result", errs) + } + _, diags = ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + errs = diags.Err().Error() + if !strings.Contains(errs, "function returned an inconsistent result") { + t.Fatalf("expected error with %q, got %q", "provider function returned an inconsistent result", errs) + } +} + +// check that we catch provider functions which return inconsistent results +// during apply +func TestContext2Plan_providerFunctionImpureApply(t *testing.T) { + m, snap := testModuleWithSnapshot(t, "provider-function-echo") + + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + ResourceTypes: map[string]providers.Schema{ + "test_object": providers.Schema{Body: simpleTestSchema()}, + }, + DataSources: map[string]providers.Schema{ + "test_object": providers.Schema{Body: simpleTestSchema()}, + }, + Functions: map[string]providers.FunctionDecl{ + "echo": providers.FunctionDecl{ + Parameters: []providers.FunctionParam{ + { + Name: "arg", + Type: cty.String, + }, + }, + ReturnType: cty.String, + }, + }, + }, + } + + inc := 0 + p.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + // this broken echo adds a counter to the argument + resp.Result = cty.StringVal(fmt.Sprintf("%s-%d", req.Arguments[0].AsString(), inc)) + inc++ + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + // Write / Read plan to simulate running it through a Plan file + ctxOpts, m, plan, err := contextOptsForPlanViaFile(t, snap, plan) + if err != nil { + t.Fatalf("failed to round-trip through planfile: %s", err) + } + + ctxOpts.Providers = map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + } + ctx = testContext2(t, ctxOpts) + + _, diags = ctx.Apply(plan, m, nil) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + errs := diags.Err().Error() + if !strings.Contains(errs, "function returned an inconsistent result") { + t.Fatalf("expected error with %q, got %q", "provider function returned an inconsistent result", errs) + } +} + +// check that we can detect inconsistent results from filesystem functions during apply +func TestContext2Plan_filesystemFunctionImpureApply(t *testing.T) { + m, snap := testModuleWithSnapshot(t, "resource-fs-func") + + externalDataFile := filepath.Join(t.TempDir(), "testdata") + dataFile, err := os.Create(externalDataFile) + if err != nil { + t.Fatal(err) + } + defer dataFile.Close() + + if _, err := dataFile.WriteString("initial data"); err != nil { + t.Fatal(err) + } + + p := testProvider("test") + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "external_file": &InputValue{ + Value: cty.StringVal(externalDataFile), + SourceType: ValueFromCLIArg, + }, + }, + }) + tfdiags.AssertNoErrors(t, diags) + + // Write / Read plan to simulate running it through a Plan file + ctxOpts, m, plan, err := contextOptsForPlanViaFile(t, snap, plan) + if err != nil { + t.Fatalf("failed to round-trip through planfile: %s", err) + } + + ctxOpts.Providers = map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + } + ctx = testContext2(t, ctxOpts) + + // cause the external file data to change + if _, err := dataFile.WriteString("incorrect data"); err != nil { + t.Fatal(err) + } + + _, diags = ctx.Apply(plan, m, nil) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + errs := diags.Err().Error() + if !strings.Contains(errs, "function returned an inconsistent result") { + t.Fatalf("expected error with %q, got %q", "provider function returned an inconsistent result", errs) + } +} + +func TestContext2Validate_providerFunctionDiagnostics(t *testing.T) { + provider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + Functions: map[string]providers.FunctionDecl{ + "echo": providers.FunctionDecl{ + Parameters: []providers.FunctionParam{ + { + Name: "arg", + Type: cty.String, + }, + }, + ReturnType: cty.String, + }, + }, + }, + } + + tests := []struct { + name string + cfg *configs.Config + expectedDiag string + }{ + { + "missing provider", + testModuleInline(t, map[string]string{ + "main.tf": ` + output "first" { + value = provider::test::echo("input") + }`}), + `Ensure that provider name "test" is declared in this module's required_providers block, and that this provider offers a function named "echo"`, + }, + { + "invalid namespace", + testModuleInline(t, map[string]string{ + "main.tf": ` + output "first" { + value = test::echo("input") + }`}), + `The function namespace "test" is not valid. Provider function calls must use the "provider::" namespace prefix`, + }, + { + "missing namespace", + testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } + } + output "first" { + value = echo("input") + }`}), + `There is no function named "echo". Did you mean "provider::test::echo"?`, + }, + { + "no function from provider", + testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } + } + output "first" { + value = provider::test::missing("input") + }`}), + `Unknown provider function: The function "missing" is not available from the provider "test".`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(provider), + }, + }) + + diags := ctx.Validate(test.cfg, nil) + if !diags.HasErrors() { + t.Fatal("expected diagnsotics, got none") + } + got := diags.Err().Error() + if !strings.Contains(got, test.expectedDiag) { + t.Fatalf("expected %q, got %q", test.expectedDiag, got) + } + }) + } +} diff --git a/internal/terraform/context_import.go b/internal/terraform/context_import.go index ce5df08f5f..6629e8665f 100644 --- a/internal/terraform/context_import.go +++ b/internal/terraform/context_import.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -19,17 +22,20 @@ type ImportOpts struct { SetVariables InputValues } -// ImportTarget is a single resource to import. +// ImportTarget is a single resource to import, +// in legacy (CLI) import mode. type ImportTarget struct { - // Addr is the address for the resource instance that the new object should - // be imported into. - Addr addrs.AbsResourceInstance + // Config is the original import block for this import. This might be null + // if the import did not originate in config. + Config *configs.Import - // ID is the ID of the resource to import. This is resource-specific. - ID string + // LegacyAddr is the import address set from the command line arguments + // when using the import command. + LegacyAddr addrs.AbsResourceInstance - // ProviderAddr is the address of the provider that should handle the import. - ProviderAddr addrs.AbsProviderConfig + // LegacyID stores the ID from the command line arguments when using the + // import command. + LegacyID string } // Import takes already-created external resources and brings them @@ -82,6 +88,11 @@ func (c *Context) Import(config *configs.Config, prevRunState *states.State, opt return state, diags } + // Data sources which could not be read during the import plan will be + // unknown. We need to strip those objects out so that the state can be + // serialized. + walker.State.RemovePlannedResourceInstanceObjects() + newState := walker.State.Close() return newState, diags } diff --git a/internal/terraform/context_import_test.go b/internal/terraform/context_import_test.go index af8ff4b856..932eed2451 100644 --- a/internal/terraform/context_import_test.go +++ b/internal/terraform/context_import_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,11 +8,13 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) func TestContextImport_basic(t *testing.T) { @@ -35,10 +40,10 @@ func TestContextImport_basic(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -86,10 +91,10 @@ resource "aws_instance" "foo" { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.IntKey(0), ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -147,10 +152,10 @@ func TestContextImport_collision(t *testing.T) { state, diags := ctx.Import(m, state, &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -191,10 +196,10 @@ func TestContextImport_missingType(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -242,10 +247,10 @@ func TestContextImport_moduleProvider(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -297,10 +302,10 @@ func TestContextImport_providerModule(t *testing.T) { _, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey).ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -353,10 +358,10 @@ func TestContextImport_providerConfig(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, SetVariables: InputValues{ @@ -413,10 +418,10 @@ func TestContextImport_providerConfigResources(t *testing.T) { _, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -430,7 +435,24 @@ func TestContextImport_providerConfigResources(t *testing.T) { func TestContextImport_refresh(t *testing.T) { p := testProvider("aws") - m := testModule(t, "import-provider") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +provider "aws" { + foo = "bar" +} + +resource "aws_instance" "foo" { +} + + +// we are only importing aws_instance.foo, so these resources will be unknown +resource "aws_instance" "bar" { +} +data "aws_data_source" "bar" { + foo = aws_instance.bar.id +} +`}) + ctx := testContext2(t, &ContextOpts{ Providers: map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), @@ -448,6 +470,13 @@ func TestContextImport_refresh(t *testing.T) { }, } + p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("id"), + "foo": cty.UnknownVal(cty.String), + }), + } + p.ReadResourceFn = nil p.ReadResourceResponse = &providers.ReadResourceResponse{ @@ -460,10 +489,10 @@ func TestContextImport_refresh(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -471,6 +500,10 @@ func TestContextImport_refresh(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } + if d := state.ResourceInstance(mustResourceInstanceAddr("data.aws_data_source.bar")); d != nil { + t.Errorf("data.aws_data_source.bar has a status of ObjectPlanned and should not be in the state\ngot:%#v\n", d.Current) + } + actual := strings.TrimSpace(state.String()) expected := strings.TrimSpace(testImportRefreshStr) if actual != expected { @@ -507,10 +540,10 @@ func TestContextImport_refreshNil(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -548,10 +581,10 @@ func TestContextImport_module(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -589,10 +622,10 @@ func TestContextImport_moduleDepth2(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).Child("nested", addrs.NoKey).ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).Child("nested", addrs.NoKey).ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "baz", + LegacyID: "baz", }, }, }) @@ -630,10 +663,10 @@ func TestContextImport_moduleDiff(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.Child("child", addrs.IntKey(0)).ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "baz", + LegacyID: "baz", }, }, }) @@ -652,7 +685,7 @@ func TestContextImport_multiState(t *testing.T) { p := testProvider("aws") m := testModule(t, "import-provider") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, @@ -698,10 +731,10 @@ func TestContextImport_multiState(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -720,7 +753,7 @@ func TestContextImport_multiStateSame(t *testing.T) { p := testProvider("aws") m := testModule(t, "import-provider") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, @@ -772,10 +805,10 @@ func TestContextImport_multiStateSame(t *testing.T) { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, ), - ID: "bar", + LegacyID: "bar", }, }, }) @@ -829,7 +862,7 @@ resource "test_resource" "unused" { `, }) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, @@ -866,10 +899,10 @@ resource "test_resource" "unused" { state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ Targets: []*ImportTarget{ { - Addr: addrs.RootModuleInstance.ResourceInstance( + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( addrs.ManagedResourceMode, "test_resource", "test", addrs.NoKey, ), - ID: "test", + LegacyID: "test", }, }, }) @@ -887,12 +920,173 @@ resource "test_resource" "unused" { } } +// New resources in the config during import won't exist for evaluation +// purposes (until import is upgraded to using a complete plan). This means +// that references to them are unknown, but in the case of single instances, we +// can at least know the type of unknown value. +func TestContextImport_newResourceUnknown(t *testing.T) { + p := testProvider("aws") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "one" { +} + +resource "test_resource" "two" { + count = length(flatten([test_resource.one.id])) +} + +resource "test_resource" "test" { +} +`}) + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + }, + }, + }, + }) + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_resource", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("test"), + }), + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ + Targets: []*ImportTarget{ + { + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( + addrs.ManagedResourceMode, "test_resource", "test", addrs.NoKey, + ), + LegacyID: "test", + }, + }, + }) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + ri := state.ResourceInstance(mustResourceInstanceAddr("test_resource.test")) + expected := `{"id":"test"}` + if ri == nil || ri.Current == nil { + t.Fatal("no state is recorded for resource instance test_resource.test") + } + if string(ri.Current.AttrsJSON) != expected { + t.Fatalf("expected %q, got %q\n", expected, ri.Current.AttrsJSON) + } +} + +func TestContextImport_33572(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "issue-33572") + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_instance", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + }, + } + + state, diags := ctx.Import(m, states.NewState(), &ImportOpts{ + Targets: []*ImportTarget{ + { + LegacyAddr: addrs.RootModuleInstance.ResourceInstance( + addrs.ManagedResourceMode, "aws_instance", "foo", addrs.NoKey, + ), + LegacyID: "bar", + }, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + actual := strings.TrimSpace(state.String()) + expected := strings.TrimSpace(testImportStrWithDataSource) + if diff := cmp.Diff(actual, expected); len(diff) > 0 { + t.Fatalf("wrong final state\ngot:\n%s\nwant:\n%s\ndiff:\n%s", actual, expected, diff) + } +} + +// Missing import target should produce an error +func TestContextImport_missingModuleImport(t *testing.T) { + p := testProvider("test") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +locals { + xs = toset(["foo"]) +} + +module "a" { + for_each = local.xs + source = "./a" +} + +import { + to = module.WRONG.test_resource.x + id = "id" +} +`, + "a/main.tf": ` +resource "test_resource" "x" { + value = "z" +} +`, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatal("expected missing import target error") + } + if !strings.Contains(diags.Err().Error(), "Configuration for import target does not exist") { + t.Fatalf("incorrect error for missing import target: %s\n", diags.Err()) + } +} + const testImportStr = ` aws_instance.foo: ID = foo provider = provider["registry.terraform.io/hashicorp/aws"] ` +const testImportStrWithDataSource = ` +data.aws_data_source.bar: + ID = baz + provider = provider["registry.terraform.io/hashicorp/aws"] +aws_instance.foo: + ID = foo + provider = provider["registry.terraform.io/hashicorp/aws"] +` + const testImportCountIndexStr = ` aws_instance.foo.0: ID = foo diff --git a/internal/terraform/context_input.go b/internal/terraform/context_input.go index 153546d286..b4b560f2d3 100644 --- a/internal/terraform/context_input.go +++ b/internal/terraform/context_input.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/context_input_test.go b/internal/terraform/context_input_test.go index 5216efb596..add987b00b 100644 --- a/internal/terraform/context_input_test.go +++ b/internal/terraform/context_input_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -13,12 +16,13 @@ import ( "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestContext2Input_provider(t *testing.T) { m := testModule(t, "input-provider") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { @@ -71,9 +75,9 @@ func TestContext2Input_provider(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -85,7 +89,7 @@ func TestContext2Input_provider(t *testing.T) { func TestContext2Input_providerMulti(t *testing.T) { m := testModule(t, "input-provider-multi") - getProviderSchemaResponse := getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + getProviderSchemaResponse := getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { @@ -142,7 +146,7 @@ func TestContext2Input_providerMulti(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) providerFactory = func() (providers.Interface, error) { p := testProvider("aws") @@ -156,7 +160,7 @@ func TestContext2Input_providerMulti(t *testing.T) { return p, nil } - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -186,7 +190,7 @@ func TestContext2Input_providerId(t *testing.T) { m := testModule(t, "input-provider") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { @@ -230,9 +234,9 @@ func TestContext2Input_providerId(t *testing.T) { } plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -246,7 +250,7 @@ func TestContext2Input_providerOnly(t *testing.T) { m := testModule(t, "input-provider-vars") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": { @@ -304,9 +308,9 @@ func TestContext2Input_providerOnly(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - state, err := ctx.Apply(plan, m) + state, err := ctx.Apply(plan, m, nil) if err != nil { t.Fatalf("err: %s", err) } @@ -355,9 +359,9 @@ func TestContext2Input_providerVars(t *testing.T) { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) - if _, diags := ctx.Apply(plan, m); diags.HasErrors() { + if _, diags := ctx.Apply(plan, m, nil); diags.HasErrors() { t.Fatalf("apply errors: %s", diags.Err()) } @@ -406,7 +410,7 @@ func TestContext2Input_dataSourceRequiresRefresh(t *testing.T) { p := testProvider("null") m := testModule(t, "input-module-data-vars") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ DataSources: map[string]*configschema.Block{ "null_data_source": { Attributes: map[string]*configschema.Attribute{ diff --git a/internal/terraform/context_plan.go b/internal/terraform/context_plan.go index abd8d2adbe..db0571c1b7 100644 --- a/internal/terraform/context_plan.go +++ b/internal/terraform/context_plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,14 +9,19 @@ import ( "log" "sort" "strings" + "time" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/globalref" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/refactoring" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -33,6 +41,16 @@ type PlanOpts struct { // instance using its corresponding provider. SkipRefresh bool + // PreDestroyRefresh indicated that this is being passed to a plan used to + // refresh the state immediately before a destroy plan. + // FIXME: This is a temporary fix to allow the pre-destroy refresh to + // succeed. The refreshing operation during destroy must be a special case, + // which can allow for missing instances in the state, and avoid blocking + // on failing condition tests. The destroy plan itself should be + // responsible for this special case of refreshing, and the separate + // pre-destroy plan removed entirely. + PreDestroyRefresh bool + // SetVariables are the raw values for root module variables as provided // by the user who is requesting the run, prior to any normalization or // substitution of defaults. See the documentation for the InputValue @@ -59,17 +77,96 @@ type PlanOpts struct { // outside of Terraform), thereby hopefully replacing it with a // fully-functional new object. ForceReplace []addrs.AbsResourceInstance + + // DeferralAllowed specifies that the plan is allowed to defer some actions, + // so that a subset of the plan can be applied even if parts of it can't yet + // be planned at all. Plans that contain deferred actions can't converge in + // a single run, and their configuration must be planned again after the + // dependencies of their deferred objects are in a usable state. Various + // events can cause deferrals, including unknown values in count and + // for_each arguments, and deferral notices from providers. If + // DeferralAllowed is false, the plan will error upon encountering an object + // that would be unplannable until after the apply. + DeferralAllowed bool + + // ExternalReferences allows the external caller to pass in references to + // nodes that should not be pruned even if they are not referenced within + // the actual graph. + ExternalReferences []*addrs.Reference + + // ExternalDependencyDeferred, when set, indicates that the caller + // considers this configuration to depend on some other configuration + // that had at least one deferred change, and therefore everything in + // this configuration must have its changes deferred too so that the + // overall dependency ordering would be correct. + ExternalDependencyDeferred bool + + // Overrides provides a set of override objects that should be applied + // during this plan. + Overrides *mocking.Overrides + + // GenerateConfigPath tells Terraform where to write any generated + // configuration for any ImportTargets that do not have configuration + // already. + // + // If empty, then no config will be generated. + GenerateConfigPath string + + // ExternalProviders are clients for pre-configured providers that are + // treated as being passed into the root module from the caller. This + // is equivalent to writing a "providers" argument inside a "module" + // block in the Terraform language, but for the root module the caller + // is written in Go rather than the Terraform language. + // + // Terraform Core will NOT call ValidateProviderConfig or ConfigureProvider + // on any providers in this map; it's the caller's responsibility to + // configure these providers based on information outside the scope of + // the root module. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface + + // ForcePlanTimestamp, if not nil, will force the "plantimestamp" function + // to return the given value instead of the time when the plan operation + // started. + // + // This is here only to allow producing fixed results for tests. Don't + // use it for main code. + ForcePlanTimestamp *time.Time + + // Forget if set to true will cause the plan to forget all resources. This is + // only allowd in the context of a destroy plan. + Forget bool } -// Plan generates an execution plan for the given context, and returns the -// refreshed state. +// Plan generates an execution plan by comparing the given configuration +// with the given previous run state. // -// The execution plan encapsulates the context and can be stored -// in order to reinstantiate a context later for Apply. +// The given planning options allow control of various other details of the +// planning process that are not represented directly in the configuration. +// You can use terraform.DefaultPlanOpts to generate a normal plan with no +// special options. // -// Plan also updates the diff of this context to be the diff generated -// by the plan, so Apply can be called after. +// If the returned diagnostics contains no errors then the returned plan is +// applyable, although Terraform cannot guarantee that applying it will fully +// succeed. If the returned diagnostics contains errors but this method +// still returns a non-nil Plan then the plan describes the subset of actions +// planned so far, which is not safe to apply but could potentially be used +// by the UI layer to give extra context to support understanding of the +// returned error messages. func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { + plan, _, diags := c.PlanAndEval(config, prevRunState, opts) + return plan, diags +} + +// PlanAndEval is like [Context.Plan] except that it additionally makes a +// best effort to return a [lang.Scope] which can evaluate expressions in the +// root module based on the content of the generated plan. +// +// The scope will be nil if the planning process doesn't complete successfully +// enough to produce a valid evaluation scope. If the returned plan is nil +// then the scope will always be nil, but it's also possible for the scope +// to be nil even when the plan isn't, if the plan is not complete enough for +// the evaluation scope to produce consistent results. +func (c *Context) PlanAndEval(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, *lang.Scope, tfdiags.Diagnostics) { defer c.acquireRun("plan")() var diags tfdiags.Diagnostics @@ -90,11 +187,20 @@ func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts moreDiags := c.checkConfigDependencies(config) diags = diags.Append(moreDiags) + moreDiags = c.checkStateDependencies(prevRunState) + diags = diags.Append(moreDiags) + // If required dependencies are not available then we'll bail early since // otherwise we're likely to just see a bunch of other errors related to // incompatibilities, which could be overwhelming for the user. if diags.HasErrors() { - return nil, diags + return nil, nil, diags + } + + providerCfgDiags := checkExternalProviders(config, nil, prevRunState, opts.ExternalProviders) + diags = diags.Append(providerCfgDiags) + if providerCfgDiags.HasErrors() { + return nil, nil, diags } switch opts.Mode { @@ -109,7 +215,7 @@ func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts "Incompatible plan options", "Cannot skip refreshing in refresh-only mode. This is a bug in Terraform.", )) - return nil, diags + return nil, nil, diags } default: // The CLI layer (and other similar callers) should not try to @@ -119,7 +225,7 @@ func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts "Unsupported plan mode", fmt.Sprintf("Terraform Core doesn't know how to handle plan mode %s. This is a bug in Terraform.", opts.Mode), )) - return nil, diags + return nil, nil, diags } if len(opts.ForceReplace) > 0 && opts.Mode != plans.NormalMode { // The other modes don't generate no-op or update actions that we might @@ -129,7 +235,15 @@ func (c *Context) Plan(config *configs.Config, prevRunState *states.State, opts "Unsupported plan mode", "Forcing resource instance replacement (with -replace=...) is allowed only in normal planning mode.", )) - return nil, diags + return nil, nil, diags + } + if opts.Forget && opts.Mode != plans.DestroyMode { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unsupported plan mode", + "Forgetting all resources is only allowed in the context of a destroy plan. This is a bug in Terraform, please report it.", + )) + return nil, nil, diags } // By the time we get here, we should have values defined for all of @@ -154,32 +268,62 @@ The -target option is not for routine use, and is provided only for exceptional var plan *plans.Plan var planDiags tfdiags.Diagnostics + var evalScope *lang.Scope switch opts.Mode { case plans.NormalMode: - plan, planDiags = c.plan(config, prevRunState, opts) + plan, evalScope, planDiags = c.plan(config, prevRunState, opts) case plans.DestroyMode: - plan, planDiags = c.destroyPlan(config, prevRunState, opts) + plan, evalScope, planDiags = c.destroyPlan(config, prevRunState, opts) case plans.RefreshOnlyMode: - plan, planDiags = c.refreshOnlyPlan(config, prevRunState, opts) + plan, evalScope, planDiags = c.refreshOnlyPlan(config, prevRunState, opts) default: panic(fmt.Sprintf("unsupported plan mode %s", opts.Mode)) } diags = diags.Append(planDiags) - if diags.HasErrors() { - return nil, diags - } + // NOTE: We're intentionally not returning early when diags.HasErrors + // here because we'll still populate other metadata below on a best-effort + // basis to try to give the UI some extra context to return alongside the + // error messages. // convert the variables into the format expected for the plan varVals := make(map[string]plans.DynamicValue, len(opts.SetVariables)) + varMarks := make(map[string][]cty.PathValueMarks, len(opts.SetVariables)) + applyTimeVariables := collections.NewSetCmp[string]() for k, iv := range opts.SetVariables { + // If any input variables were declared as ephemeral and set to a + // non-null value then those variables must be provided again (possibly + // with _different_ non-null values) during the apply phase. + if vc, ok := config.Module.Variables[k]; ok && vc.Ephemeral { + // FIXME: We should actually do this based on the final value + // in the named values state, rather than the value as provided + // by the caller, so we can take into account the transforms + // done during variable evaluation. This is a plausible starting + // point for now, though. + if iv.Value != cty.NilVal && !iv.Value.IsNull() { + applyTimeVariables.Add(k) + } + continue + } + + // Non-ephemeral variables must remain unchanged between plan and + // apply, so we'll record their actual values. if iv.Value == cty.NilVal { continue // We only record values that the caller actually set } + // Root variable values arriving from the traditional CLI path are + // unmarked, as they are directly decoded from .tfvars, CLI arguments, + // or the environment. However, variable values arriving from other + // plans (via the coordination efforts of the stacks runtime) may have + // gathered marks during evaluation. We must separate the value from + // its marks here to maintain compatibility with plans.DynamicValue, + // which cannot represent marks. + value, pvm := iv.Value.UnmarkDeepWithPaths() + // We use cty.DynamicPseudoType here so that we'll save both the // value _and_ its dynamic type in the plan, so we can recover // exactly the same value later. - dv, err := plans.NewDynamicValue(iv.Value, cty.DynamicPseudoType) + dv, err := plans.NewDynamicValue(value, cty.DynamicPseudoType) if err != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -189,37 +333,56 @@ The -target option is not for routine use, and is provided only for exceptional continue } varVals[k] = dv + varMarks[k] = pvm } // insert the run-specific data from the context into the plan; variables, // targets and provider SHAs. if plan != nil { plan.VariableValues = varVals + plan.ApplyTimeVariables = applyTimeVariables + if len(varMarks) > 0 { + plan.VariableMarks = varMarks + } plan.TargetAddrs = opts.Targets } else if !diags.HasErrors() { panic("nil plan but no errors") } - relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, plan) - diags = diags.Append(rDiags) + if plan != nil { + relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, plan) + diags = diags.Append(rDiags) + plan.RelevantAttributes = relevantAttrs + } - plan.RelevantAttributes = relevantAttrs - diags = diags.Append(c.checkApplyGraph(plan, config)) + if diags.HasErrors() { + // We can't proceed further with an invalid plan, because an invalid + // plan isn't applyable by definition. + if plan != nil { + // We'll explicitly mark our plan as errored so that it can't + // be accidentally applied even though it's incomplete. + plan.Errored = true + } + return plan, evalScope, diags + } - return plan, diags + diags = diags.Append(c.checkApplyGraph(plan, config, opts)) + + return plan, evalScope, diags } // checkApplyGraph builds the apply graph out of the current plan to // check for any errors that may arise once the planned changes are added to // the graph. This allows terraform to report errors (mostly cycles) during // plan that would otherwise only crop up during apply -func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config) tfdiags.Diagnostics { +func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config, opts *PlanOpts) tfdiags.Diagnostics { if plan.Changes.Empty() { log.Println("[DEBUG] no planned changes, skipping apply graph check") return nil } log.Println("[DEBUG] building apply graph to check for errors") - _, _, diags := c.applyGraph(plan, config, true) + + _, _, diags := c.applyGraph(plan, config, opts.ApplyOpts(), true) return diags } @@ -246,39 +409,32 @@ func SimplePlanOpts(mode plans.Mode, setVariables InputValues) *PlanOpts { } } -func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { +func (c *Context) plan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, *lang.Scope, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - if opts.Mode != plans.NormalMode { panic(fmt.Sprintf("called Context.plan with %s", opts.Mode)) } - plan, walkDiags := c.planWalk(config, prevRunState, opts) + plan, evalScope, walkDiags := c.planWalk(config, prevRunState, opts) diags = diags.Append(walkDiags) - if diags.HasErrors() { - return nil, diags - } - // The refreshed state ends up with some placeholder objects in it for - // objects pending creation. We only really care about those being in - // the working state, since that's what we're going to use when applying, - // so we'll prune them all here. - plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() - - return plan, diags + return plan, evalScope, diags } -func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { +func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, *lang.Scope, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics if opts.Mode != plans.RefreshOnlyMode { panic(fmt.Sprintf("called Context.refreshOnlyPlan with %s", opts.Mode)) } - plan, walkDiags := c.planWalk(config, prevRunState, opts) + plan, evalScope, walkDiags := c.planWalk(config, prevRunState, opts) diags = diags.Append(walkDiags) if diags.HasErrors() { - return nil, diags + // Non-nil plan along with errors indicates a non-applyable partial + // plan that's only suitable to be shown to the user as extra context + // to help understand the errors. + return plan, evalScope, diags } // If the graph builder and graph nodes correctly obeyed our directive @@ -302,20 +458,15 @@ func (c *Context) refreshOnlyPlan(config *configs.Config, prevRunState *states.S )) } - // Prune out any placeholder objects we put in the state to represent - // objects that would need to be created. - plan.PriorState.SyncWrapper().RemovePlannedResourceInstanceObjects() - // We don't populate RelevantResources for a refresh-only plan, because // they never have any planned actions and so no resource can ever be // "relevant" per the intended meaning of that field. - return plan, diags + return plan, evalScope, diags } -func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { +func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, *lang.Scope, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - pendingPlan := &plans.Plan{} if opts.Mode != plans.DestroyMode { panic(fmt.Sprintf("called Context.destroyPlan with %s", opts.Mode)) @@ -334,11 +485,19 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State // must coordinate with this by taking that action only when c.skipRefresh // _is_ set. This coupling between the two is unfortunate but necessary // to work within our current structure. - if !opts.SkipRefresh { + if !opts.SkipRefresh && !prevRunState.Empty() { log.Printf("[TRACE] Context.destroyPlan: calling Context.plan to get the effect of refreshing the prior state") - normalOpts := *opts - normalOpts.Mode = plans.NormalMode - refreshPlan, refreshDiags := c.plan(config, prevRunState, &normalOpts) + refreshOpts := *opts + refreshOpts.Mode = plans.NormalMode + refreshOpts.PreDestroyRefresh = true + + // FIXME: A normal plan is required here to refresh the state, because + // the state and configuration may not match during a destroy, and a + // normal refresh plan can fail with evaluation errors. In the future + // the destroy plan should take care of refreshing instances itself, + // where the special cases of evaluation and skipping condition checks + // can be done. + refreshPlan, _, refreshDiags := c.plan(config, prevRunState, &refreshOpts) if refreshDiags.HasErrors() { // NOTE: Normally we'd append diagnostics regardless of whether // there are errors, just in case there are warnings we'd want to @@ -352,44 +511,46 @@ func (c *Context) destroyPlan(config *configs.Config, prevRunState *states.State // rather not show them here, because this non-destroy plan for // refreshing is largely an implementation detail.) diags = diags.Append(refreshDiags) - return nil, diags + return nil, nil, diags } - // insert the refreshed state into the destroy plan result, and ignore - // the changes recorded from the refresh. - pendingPlan.PriorState = refreshPlan.PriorState.DeepCopy() - pendingPlan.PrevRunState = refreshPlan.PrevRunState.DeepCopy() - log.Printf("[TRACE] Context.destroyPlan: now _really_ creating a destroy plan") - // We'll use the refreshed state -- which is the "prior state" from - // the perspective of this "pending plan" -- as the starting state + // the perspective of this "destroy plan" -- as the starting state // for our destroy-plan walk, so it can take into account if we // detected during refreshing that anything was already deleted outside // of Terraform. - priorState = pendingPlan.PriorState + priorState = refreshPlan.PriorState.DeepCopy() + + // The refresh plan may have upgraded state for some resources, make + // sure we store the new version. + prevRunState = refreshPlan.PrevRunState.DeepCopy() + log.Printf("[TRACE] Context.destroyPlan: now _really_ creating a destroy plan") } - destroyPlan, walkDiags := c.planWalk(config, priorState, opts) + destroyPlan, evalScope, walkDiags := c.planWalk(config, priorState, opts) diags = diags.Append(walkDiags) if walkDiags.HasErrors() { - return nil, diags + // Non-nil plan along with errors indicates a non-applyable partial + // plan that's only suitable to be shown to the user as extra context + // to help understand the errors. + return destroyPlan, evalScope, diags } if !opts.SkipRefresh { - // If we didn't skip refreshing then we want the previous run state - // prior state to be the one we originally fed into the c.plan call - // above, not the refreshed version we used for the destroy walk. - destroyPlan.PrevRunState = pendingPlan.PrevRunState + // If we didn't skip refreshing then we want the previous run state to + // be the one we originally fed into the c.refreshOnlyPlan call above, + // not the refreshed version we used for the destroy planWalk. + destroyPlan.PrevRunState = prevRunState } relevantAttrs, rDiags := c.relevantResourceAttrsForPlan(config, destroyPlan) diags = diags.Append(rDiags) destroyPlan.RelevantAttributes = relevantAttrs - return destroyPlan, diags + return destroyPlan, evalScope, diags } -func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State, targets []addrs.Targetable) ([]refactoring.MoveStatement, refactoring.MoveResults) { +func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState *states.State) ([]refactoring.MoveStatement, refactoring.MoveResults, tfdiags.Diagnostics) { explicitMoveStmts := refactoring.FindMoveStatements(config) implicitMoveStmts := refactoring.ImpliedMoveStatements(config, prevRunState, explicitMoveStmts) var moveStmts []refactoring.MoveStatement @@ -398,8 +559,8 @@ func (c *Context) prePlanFindAndApplyMoves(config *configs.Config, prevRunState moveStmts = append(moveStmts, explicitMoveStmts...) moveStmts = append(moveStmts, implicitMoveStmts...) } - moveResults := refactoring.ApplyMoves(moveStmts, prevRunState) - return moveStmts, moveResults + moveResults, diags := refactoring.ApplyMoves(moveStmts, prevRunState, c.plugins.ProviderFactories()) + return moveStmts, moveResults, diags } func (c *Context) prePlanVerifyTargetedMoves(moveResults refactoring.MoveResults, targets []addrs.Targetable) tfdiags.Diagnostics { @@ -461,7 +622,7 @@ func (c *Context) prePlanVerifyTargetedMoves(moveResults refactoring.MoveResults tfdiags.Error, "Moved resource instances excluded by targeting", fmt.Sprintf( - "Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances.\n\nTo create a valid plan, either remove your -target=... options altogether or add the following additional target options:%s\n\nNote that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.", + "Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options do not fully cover all of those resource instances.\n\nTo create a valid plan, either remove your -target=... options altogether or add the following additional target options:%s\n\nNote that adding these options may include further additional resource instances in your plan, in order to respect object dependencies.", listBuf.String(), ), )) @@ -474,12 +635,51 @@ func (c *Context) postPlanValidateMoves(config *configs.Config, stmts []refactor return refactoring.ValidateMoves(stmts, config, allInsts) } -func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, tfdiags.Diagnostics) { +// findImportTargets builds a list of import targets from any import blocks in +// config. +func (c *Context) findImportTargets(config *configs.Config) []*ImportTarget { + var importTargets []*ImportTarget + for _, ic := range config.Module.Import { + importTargets = append(importTargets, &ImportTarget{ + Config: ic, + }) + } + return importTargets +} + +// findForgetTargets builds a list of resources and a list of modules to be +// forgotten, based on any removed blocks in config. +func (c *Context) findForgetTargets(config *configs.Config) (forgetResources []addrs.ConfigResource, forgetModules []addrs.Module, diags tfdiags.Diagnostics) { + removeStmts, diags := refactoring.FindRemoveStatements(config) + if diags.HasErrors() { + return nil, nil, diags + } + for _, rst := range removeStmts.Values() { + if rst.Destroy { + // no-op + } else { + if fr, ok := rst.From.(addrs.ConfigResource); ok { + forgetResources = append(forgetResources, fr) + } else if fm, ok := rst.From.(addrs.Module); ok { + forgetModules = append(forgetModules, fm) + } else { + panic("Invalid ConfigMoveable type in remove statement") + } + } + } + return forgetResources, forgetModules, diags +} + +func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*plans.Plan, *lang.Scope, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics log.Printf("[DEBUG] Building and walking plan graph for %s", opts.Mode) prevRunState = prevRunState.DeepCopy() // don't modify the caller's object when we process the moves - moveStmts, moveResults := c.prePlanFindAndApplyMoves(config, prevRunState, opts.Targets) + moveStmts, moveResults, moveDiags := c.prePlanFindAndApplyMoves(config, prevRunState) + diags = diags.Append(moveDiags) + if moveDiags.HasErrors() { + return nil, nil, diags + } // If resource targeting is in effect then it might conflict with the // move result. @@ -488,27 +688,53 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o // We'll return early here, because if we have any moved resource // instances excluded by targeting then planning is likely to encounter // strange problems that may lead to confusing error messages. - return nil, diags + return nil, nil, diags } graph, walkOp, moreDiags := c.planGraph(config, prevRunState, opts) diags = diags.Append(moreDiags) if diags.HasErrors() { - return nil, diags + return nil, nil, diags + } + + timestamp := time.Now().UTC() + if opts.ForcePlanTimestamp != nil { + // Some tests use this to produce stable results to assert against. + timestamp = *opts.ForcePlanTimestamp + } + + var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + if opts != nil { + externalProviderConfigs = opts.ExternalProviders } // If we get here then we should definitely have a non-nil "graph", which // we can now walk. changes := plans.NewChanges() + + // Initialize the results table to validate provider function calls. + // Hold reference to this so we can store the table data in the plan file. + funcResults := lang.NewFunctionResultsTable(nil) + walker, walkDiags := c.walk(graph, walkOp, &graphWalkOpts{ - Config: config, - InputState: prevRunState, - Changes: changes, - MoveResults: moveResults, + Config: config, + InputState: prevRunState, + ExternalProviderConfigs: externalProviderConfigs, + DeferralAllowed: opts.DeferralAllowed, + ExternalDependencyDeferred: opts.ExternalDependencyDeferred, + Changes: changes, + MoveResults: moveResults, + Overrides: opts.Overrides, + PlanTimeTimestamp: timestamp, + FunctionResults: funcResults, + Forget: opts.Forget, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) - moveValidateDiags := c.postPlanValidateMoves(config, moveStmts, walker.InstanceExpander.AllInstances()) + + allInsts := walker.InstanceExpander.AllInstances() + + moveValidateDiags := c.postPlanValidateMoves(config, moveStmts, allInsts) if moveValidateDiags.HasErrors() { // If any of the move statements are invalid then those errors take // precedence over any other errors because an incomplete move graph @@ -516,7 +742,7 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o // comes from the fact that we need to apply the moves before we // actually validate them, because validation depends on the result // of first trying to plan. - return nil, moveValidateDiags + return nil, nil, moveValidateDiags } diags = diags.Append(moveValidateDiags) // might just contain warnings @@ -529,59 +755,196 @@ func (c *Context) planWalk(config *configs.Config, prevRunState *states.State, o diags = diags.Append(blockedMovesWarningDiag(moveResults)) } + // If we reach this point with error diagnostics then "changes" is a + // representation of the subset of changes we were able to plan before + // we encountered errors, which we'll return as part of a non-nil plan + // so that e.g. the UI can show what was planned so far in case that extra + // context helps the user to understand the error messages we're returning. prevRunState = walker.PrevRunState.Close() + + // The refreshed state may have data resource objects which were deferred + // to apply and cannot be serialized. + walker.RefreshState.RemovePlannedResourceInstanceObjects() priorState := walker.RefreshState.Close() + driftedResources, driftDiags := c.driftedResources(config, prevRunState, priorState, moveResults) diags = diags.Append(driftDiags) + deferredResources, deferredDiags := c.deferredResources(config, walker.Deferrals.GetDeferredChanges(), priorState) + diags = diags.Append(deferredDiags) + + var forgottenResources []string + for _, rc := range changes.Resources { + if rc.Action == plans.Forget { + // TODO KEM display resource ids + forgottenResources = append(forgottenResources, fmt.Sprintf(" - %s", rc.Addr)) + } + } + if len(forgottenResources) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Some objects will no longer be managed by Terraform", + fmt.Sprintf("If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:\n%s\n\nAfter applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.", strings.Join(forgottenResources, "\n")), + )) + } + schemas, schemaDiags := c.Schemas(config, prevRunState) + // We must finish building a plan object, and cannot return early here. + var changesSrc *plans.ChangesSrc + var err error + if !schemaDiags.HasErrors() { + changesSrc, err = changes.Encode(schemas) + if err != nil { + diags = diags.Append(err) + } + } + plan := &plans.Plan{ - UIMode: opts.Mode, - Changes: changes, - DriftedResources: driftedResources, - PrevRunState: prevRunState, - PriorState: priorState, - Checks: states.NewCheckResults(walker.Checks), + UIMode: opts.Mode, + Changes: changesSrc, + DriftedResources: driftedResources, + DeferredResources: deferredResources, + PrevRunState: prevRunState, + PriorState: priorState, + ExternalReferences: opts.ExternalReferences, + Overrides: opts.Overrides, + Checks: states.NewCheckResults(walker.Checks), + Timestamp: timestamp, + FunctionResults: funcResults.GetHashes(), // Other fields get populated by Context.Plan after we return } - return plan, diags + + // Our final rulings on whether the plan is "complete" and "applyable". + // See the documentation for these plan fields to learn what exactly they + // are intended to mean. + if !diags.HasErrors() { + if len(opts.Targets) == 0 && !walker.Deferrals.HaveAnyDeferrals() { + // A plan without any targets or deferred actions should be + // complete if we didn't encounter errors while producing it. + log.Println("[TRACE] Plan is complete") + plan.Complete = true + } else { + log.Println("[TRACE] Plan is incomplete") + } + if opts.Mode == plans.RefreshOnlyMode { + // In refresh-only mode we explicitly don't expect to propose any + // actions, but the plan is applyable if the state was changed + // in an interesting way by the refresh step. + plan.Applyable = !plan.PriorState.ManagedResourcesEqual(plan.PrevRunState) || !plan.PriorState.RootOutputValuesEqual(plan.PrevRunState) + } else { + // For other planning modes a plan is applyable if its "changes" + // are not considered empty (by whatever rules the plans package + // uses to decide that). + plan.Applyable = !plan.Changes.Empty() + } + if plan.Applyable { + log.Println("[TRACE] Plan is applyable") + } else { + log.Println("[TRACE] Plan is not applyable") + } + } else { + log.Println("[WARN] Planning encountered errors, so plan is not applyable") + } + + // The caller also gets access to an expression evaluation scope in the + // root module, in case it needs to extract other information using + // expressions, like in "terraform console" or the test harness. + evalScope := evalScopeFromGraphWalk(walker, addrs.RootModuleInstance) + + return plan, evalScope, diags +} + +func (c *Context) deferredResources(config *configs.Config, deferrals []*plans.DeferredResourceInstanceChange, state *states.State) ([]*plans.DeferredResourceInstanceChangeSrc, tfdiags.Diagnostics) { + var deferredResources []*plans.DeferredResourceInstanceChangeSrc + + schemas, diags := c.Schemas(config, state) + if diags.HasErrors() { + return deferredResources, diags + } + + for _, deferral := range deferrals { + + schema := schemas.ResourceTypeConfig( + deferral.Change.ProviderAddr.Provider, + deferral.Change.Addr.Resource.Resource.Mode, + deferral.Change.Addr.Resource.Resource.Type) + + deferralSrc, err := deferral.Encode(schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to prepare deferred resource for plan", + fmt.Sprintf("The deferred resource %q could not be serialized to store in the plan: %s.", deferral.Change.Addr, err))) + continue + } + + deferredResources = append(deferredResources, deferralSrc) + } + return deferredResources, diags } func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, opts *PlanOpts) (*Graph, walkOperation, tfdiags.Diagnostics) { + var externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + if opts != nil { + externalProviderConfigs = opts.ExternalProviders + } + switch mode := opts.Mode; mode { case plans.NormalMode: + // In Normal mode we need to pay attention to import and removed blocks + // in config so their targets can be added to the graph. + forgetResources, forgetModules, diags := c.findForgetTargets(config) + if diags.HasErrors() { + return nil, walkPlan, diags + } graph, diags := (&PlanGraphBuilder{ - Config: config, - State: prevRunState, - RootVariableValues: opts.SetVariables, - Plugins: c.plugins, - Targets: opts.Targets, - ForceReplace: opts.ForceReplace, - skipRefresh: opts.SkipRefresh, - Operation: walkPlan, + Config: config, + State: prevRunState, + RootVariableValues: opts.SetVariables, + ExternalProviderConfigs: externalProviderConfigs, + Plugins: c.plugins, + Targets: opts.Targets, + ForceReplace: opts.ForceReplace, + skipRefresh: opts.SkipRefresh, + preDestroyRefresh: opts.PreDestroyRefresh, + Operation: walkPlan, + ExternalReferences: opts.ExternalReferences, + Overrides: opts.Overrides, + ImportTargets: c.findImportTargets(config), + forgetResources: forgetResources, + forgetModules: forgetModules, + GenerateConfigPath: opts.GenerateConfigPath, + SkipGraphValidation: c.graphOpts.SkipGraphValidation, }).Build(addrs.RootModuleInstance) return graph, walkPlan, diags case plans.RefreshOnlyMode: graph, diags := (&PlanGraphBuilder{ - Config: config, - State: prevRunState, - RootVariableValues: opts.SetVariables, - Plugins: c.plugins, - Targets: opts.Targets, - skipRefresh: opts.SkipRefresh, - skipPlanChanges: true, // this activates "refresh only" mode. - Operation: walkPlan, + Config: config, + State: prevRunState, + RootVariableValues: opts.SetVariables, + ExternalProviderConfigs: externalProviderConfigs, + Plugins: c.plugins, + Targets: opts.Targets, + skipRefresh: opts.SkipRefresh, + skipPlanChanges: true, // this activates "refresh only" mode. + Operation: walkPlan, + ExternalReferences: opts.ExternalReferences, + Overrides: opts.Overrides, + SkipGraphValidation: c.graphOpts.SkipGraphValidation, }).Build(addrs.RootModuleInstance) return graph, walkPlan, diags case plans.DestroyMode: graph, diags := (&PlanGraphBuilder{ - Config: config, - State: prevRunState, - RootVariableValues: opts.SetVariables, - Plugins: c.plugins, - Targets: opts.Targets, - skipRefresh: opts.SkipRefresh, - Operation: walkPlanDestroy, + Config: config, + State: prevRunState, + RootVariableValues: opts.SetVariables, + ExternalProviderConfigs: externalProviderConfigs, + Plugins: c.plugins, + Targets: opts.Targets, + skipRefresh: opts.SkipRefresh, + Operation: walkPlanDestroy, + Overrides: opts.Overrides, + SkipGraphValidation: c.graphOpts.SkipGraphValidation, }).Build(addrs.RootModuleInstance) return graph, walkPlanDestroy, diags default: @@ -590,6 +953,12 @@ func (c *Context) planGraph(config *configs.Config, prevRunState *states.State, } } +// driftedResources is a best-effort attempt to compare the current and prior +// state. If we cannot decode the prior state for some reason, this should only +// return warnings to help the user correlate any missing resources in the +// report. This is known to happen when targeting a subset of resources, +// because the excluded instances will have been removed from the plan and +// not upgraded. func (c *Context) driftedResources(config *configs.Config, oldState, newState *states.State, moves refactoring.MoveResults) ([]*plans.ResourceInstanceChangeSrc, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics @@ -631,44 +1000,44 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s newIS := newState.ResourceInstance(addr) - schema, _ := schemas.ResourceTypeConfig( + schema := schemas.ResourceTypeConfig( provider, addr.Resource.Resource.Mode, addr.Resource.Resource.Type, ) - if schema == nil { - // This should never happen, but just in case - return nil, diags.Append(tfdiags.Sourceless( - tfdiags.Error, + if schema.Body == nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, "Missing resource schema from provider", - fmt.Sprintf("No resource schema found for %s.", addr.Resource.Resource.Type), + fmt.Sprintf("No resource schema found for %s when decoding prior state", addr.Resource.Resource.Type), )) + continue } - ty := schema.ImpliedType() - oldObj, err := oldIS.Current.Decode(ty) + oldObj, err := oldIS.Current.Decode(schema) if err != nil { - // This should also never happen - return nil, diags.Append(tfdiags.Sourceless( - tfdiags.Error, + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, "Failed to decode resource from state", - fmt.Sprintf("Error decoding %q from previous state: %s", addr.String(), err), + fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err), )) + continue } var newObj *states.ResourceInstanceObject if newIS != nil && newIS.Current != nil { - newObj, err = newIS.Current.Decode(ty) + newObj, err = newIS.Current.Decode(schema) if err != nil { - // This should also never happen - return nil, diags.Append(tfdiags.Sourceless( - tfdiags.Error, + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, "Failed to decode resource from state", fmt.Sprintf("Error decoding %q from prior state: %s", addr.String(), err), )) + continue } } + ty := schema.Body.ImpliedType() var oldVal, newVal cty.Value oldVal = oldObj.Value if newObj != nil { @@ -714,7 +1083,7 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s }, } - changeSrc, err := change.Encode(ty) + changeSrc, err := change.Encode(schema) if err != nil { diags = diags.Append(err) return nil, diags @@ -732,7 +1101,7 @@ func (c *Context) driftedResources(config *configs.Config, oldState, newState *s // (as opposed to graphs as an implementation detail) intended only for use // by the "terraform graph" command when asked to render a plan-time graph. // -// The result of this is intended only for rendering ot the user as a dot +// The result of this is intended only for rendering to the user as a dot // graph, and so may change in future in order to make the result more useful // in that context, even if drifts away from the physical graph that Terraform // Core currently uses as an implementation detail of planning. diff --git a/internal/terraform/context_plan2_test.go b/internal/terraform/context_plan2_test.go index 01cf74fe1b..fb14518797 100644 --- a/internal/terraform/context_plan2_test.go +++ b/internal/terraform/context_plan2_test.go @@ -1,24 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "bytes" "errors" "fmt" + "path/filepath" + "sort" "strings" "sync" "testing" + "time" "github.com/davecgh/go-spew/spew" "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty-debug/ctydebug" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) func TestContext2Plan_removedDuringRefresh(t *testing.T) { @@ -35,10 +45,10 @@ resource "test_object" "a" { p := simpleMockProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ "test_object": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "arg": {Type: cty.String, Optional: true}, }, @@ -80,7 +90,7 @@ resource "test_object" "a" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.UpgradeResourceStateCalled { t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") @@ -153,8 +163,8 @@ data "test_data_source" "foo" { `, }) - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ DataSources: map[string]*configschema.Block{ "test_data_source": { Attributes: map[string]*configschema.Attribute{ @@ -191,11 +201,8 @@ data "test_data_source" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"data_id", "foo":[{"bar":"baz"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("foo"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("foo"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -208,7 +215,7 @@ data "test_data_source" "foo" { }) plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, res := range plan.Changes.Resources { if res.Action != plans.NoOp { @@ -251,7 +258,7 @@ output "out" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) change, err := plan.Changes.Outputs[0].Decode() if err != nil { @@ -316,7 +323,7 @@ resource "test_object" "a" { }) _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) } func TestContext2Plan_dataReferencesResourceInModules(t *testing.T) { @@ -389,7 +396,7 @@ resource "test_resource" "b" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) oldMod := oldDataAddr.Module @@ -401,6 +408,129 @@ resource "test_resource" "b" { } } +func TestContext2Plan_resourceChecksInExpandedModule(t *testing.T) { + // When a resource is in a nested module we have two levels of expansion + // to do: first expand the module the resource is declared in, and then + // expand the resource itself. + // + // In earlier versions of Terraform we did that expansion as two levels + // of DynamicExpand, which led to a bug where we didn't have any central + // location from which to register all of the instances of a checkable + // resource. + // + // We now handle the full expansion all in one graph node and one dynamic + // subgraph, which avoids the problem. This is a regression test for the + // earlier bug. If this test is panicking with "duplicate checkable objects + // report" then that suggests the bug is reintroduced and we're now back + // to reporting each module instance separately again, which is incorrect. + + p := testProvider("test") + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{}, + }, + ResourceTypes: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{}, + }, + }, + } + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + resp.NewState = req.PriorState + return resp + } + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + resp.PlannedState = cty.EmptyObjectVal + return resp + } + p.ApplyResourceChangeFn = func(req providers.ApplyResourceChangeRequest) (resp providers.ApplyResourceChangeResponse) { + resp.NewState = req.PlannedState + return resp + } + + m := testModuleInline(t, map[string]string{ + "main.tf": ` + module "child" { + source = "./child" + count = 2 # must be at least 2 for this test to be valid + } + `, + "child/child.tf": ` + locals { + a = "a" + } + + resource "test" "test1" { + lifecycle { + postcondition { + # It doesn't matter what this checks as long as it + # passes, because if we don't handle expansion properly + # then we'll crash before we even get to evaluating this. + condition = local.a == local.a + error_message = "Postcondition failed." + } + } + } + + resource "test" "test2" { + count = 2 + + lifecycle { + postcondition { + # It doesn't matter what this checks as long as it + # passes, because if we don't handle expansion properly + # then we'll crash before we even get to evaluating this. + condition = local.a == local.a + error_message = "Postcondition failed." + } + } + } + `, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + priorState := states.NewState() + plan, diags := ctx.Plan(m, priorState, DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + resourceInsts := []addrs.AbsResourceInstance{ + mustResourceInstanceAddr("module.child[0].test.test1"), + mustResourceInstanceAddr("module.child[0].test.test2[0]"), + mustResourceInstanceAddr("module.child[0].test.test2[1]"), + mustResourceInstanceAddr("module.child[1].test.test1"), + mustResourceInstanceAddr("module.child[1].test.test2[0]"), + mustResourceInstanceAddr("module.child[1].test.test2[1]"), + } + + for _, instAddr := range resourceInsts { + t.Run(fmt.Sprintf("results for %s", instAddr), func(t *testing.T) { + if rc := plan.Changes.ResourceInstance(instAddr); rc != nil { + if got, want := rc.Action, plans.Create; got != want { + t.Errorf("wrong action for %s\ngot: %s\nwant: %s", instAddr, got, want) + } + if got, want := rc.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason for %s\ngot: %s\nwant: %s", instAddr, got, want) + } + } else { + t.Errorf("no planned change for %s", instAddr) + } + + if checkResult := plan.Checks.GetObjectResult(instAddr); checkResult != nil { + if got, want := checkResult.Status, checks.StatusPass; got != want { + t.Errorf("wrong check status for %s\ngot: %s\nwant: %s", instAddr, got, want) + } + } else { + t.Errorf("no check result for %s", instAddr) + } + }) + } +} + func TestContext2Plan_dataResourceChecksManagedResourceChange(t *testing.T) { // This tests the situation where the remote system contains data that // isn't valid per a data resource postcondition, but that the @@ -421,11 +551,11 @@ func TestContext2Plan_dataResourceChecksManagedResourceChange(t *testing.T) { p := testProvider("test") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{}, + Body: &configschema.Block{}, }, ResourceTypes: map[string]providers.Schema{ "test_resource": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -441,7 +571,7 @@ func TestContext2Plan_dataResourceChecksManagedResourceChange(t *testing.T) { }, DataSources: map[string]providers.Schema{ "test_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -551,7 +681,7 @@ data "test_data_source" "a" { }) plan, diags := ctx.Plan(m, priorState, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if rc := plan.Changes.ResourceInstance(dataAddr); rc != nil { if got, want := rc.Action, plans.Read; got != want { @@ -578,8 +708,8 @@ data "test_data_source" "a" { // This is primarily a plan-time test, since the special handling of // data resources is a plan-time concern, but we'll still try applying the // plan here just to make sure it's valid. - newState, diags := ctx.Apply(plan, m) - assertNoErrors(t, diags) + newState, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) if rs := newState.ResourceInstance(dataAddr); rs != nil { if !rs.HasCurrent() { @@ -620,11 +750,11 @@ func TestContext2Plan_managedResourceChecksOtherManagedResourceChange(t *testing p := testProvider("test") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{}, + Body: &configschema.Block{}, }, ResourceTypes: map[string]providers.Schema{ "test_resource": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -640,7 +770,7 @@ func TestContext2Plan_managedResourceChecksOtherManagedResourceChange(t *testing }, DataSources: map[string]providers.Schema{ "test_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { Type: cty.String, @@ -789,10 +919,10 @@ resource "test_object" "a" { p := simpleMockProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ "test_object": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "arg": {Type: cty.String, Optional: true}, }, @@ -866,7 +996,7 @@ resource "test_object" "a" { Mode: plans.DestroyMode, SkipRefresh: false, // the default }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !upgradeResourceStateCalled { t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") @@ -915,10 +1045,10 @@ resource "test_object" "a" { p := simpleMockProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ "test_object": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "arg": {Type: cty.String, Optional: true}, }, @@ -966,7 +1096,7 @@ resource "test_object" "a" { Mode: plans.DestroyMode, SkipRefresh: true, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.UpgradeResourceStateCalled { t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") @@ -1019,8 +1149,8 @@ output "result" { `, }) - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -1056,7 +1186,7 @@ output "result" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, res := range plan.Changes.Resources { if res.Action != plans.Create { @@ -1105,7 +1235,7 @@ provider "test" { _, diags := ctx.Plan(m, state, &PlanOpts{ Mode: plans.DestroyMode, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) } func TestContext2Plan_movedResourceBasic(t *testing.T) { @@ -1176,6 +1306,71 @@ func TestContext2Plan_movedResourceBasic(t *testing.T) { }) } +func TestContext2Plan_movedResourceMissingModule(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + addrB := mustResourceInstanceAddr("module.gone.test_object.b") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + moved { + from = test_object.a + to = module.gone.test_object.b + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) + } + }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + if got, want := instPlan.Addr, addrB; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Delete; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoMoveTarget; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + func TestContext2Plan_movedResourceCollision(t *testing.T) { addrNoKey := mustResourceInstanceAddr("test_object.a") addrZeroKey := mustResourceInstanceAddr("test_object.a[0]") @@ -1448,7 +1643,7 @@ The -target option is not for routine use, and is provided only for exceptional tfdiags.Sourceless( tfdiags.Error, "Moved resource instances excluded by targeting", - `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. + `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options do not fully cover all of those resource instances. To create a valid plan, either remove your -target=... options altogether or add the following additional target options: -target="test_object.a" @@ -1488,7 +1683,7 @@ The -target option is not for routine use, and is provided only for exceptional tfdiags.Sourceless( tfdiags.Error, "Moved resource instances excluded by targeting", - `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. + `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options do not fully cover all of those resource instances. To create a valid plan, either remove your -target=... options altogether or add the following additional target options: -target="test_object.b" @@ -1528,7 +1723,7 @@ The -target option is not for routine use, and is provided only for exceptional tfdiags.Sourceless( tfdiags.Error, "Moved resource instances excluded by targeting", - `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options to not fully cover all of those resource instances. + `Resource instances in your current state have moved to new addresses in the latest configuration. Terraform must include those resource instances while planning in order to ensure a correct result, but your -target=... options do not fully cover all of those resource instances. To create a valid plan, either remove your -target=... options altogether or add the following additional target options: -target="test_object.a" @@ -1559,11 +1754,6 @@ Note that adding these options may include further additional resource instances }) diags.Sort() - // We're semi-abusing "ForRPC" here just to get diagnostics that are - // more easily comparable than the various different diagnostics types - // tfdiags uses internally. The RPC-friendly diagnostics are also - // comparison-friendly, by discarding all of the dynamic type information. - gotDiags := diags.ForRPC() wantDiags := tfdiags.Diagnostics{ // Still get the warning about the -target option... tfdiags.Sourceless( @@ -1574,12 +1764,740 @@ Note that adding these options may include further additional resource instances The -target option is not for routine use, and is provided only for exceptional situations such as recovering from errors or mistakes, or when Terraform specifically suggests to use it as part of an error message.`, ), // ...but now we have no error about test_object.a - }.ForRPC() + } - if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { - t.Errorf("wrong diagnostics\n%s", diff) + tfdiags.AssertDiagnosticsMatch(t, wantDiags, diags) + }) +} + +func TestContext2Plan_movedResourceWithIdentity(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + addrB := mustResourceInstanceAddr("test_object.b") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_object" "b" { + } + + moved { + from = test_object.a + to = test_object.b + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{}`), + Status: states.ObjectReady, + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id": "before"}`), + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_object": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "test_object": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "test_object": 0, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) } }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + beforeIdentity, err := instPlan.BeforeIdentity.Decode(cty.Object(map[string]cty.Type{ + "id": cty.String, + })) + if err != nil { + t.Fatalf("unexpected error decoding before identity: %s", err) + } + + if beforeIdentity.IsNull() { + t.Fatalf("after identity is null") + } + expectedIdentity := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("before"), + }) + if !beforeIdentity.RawEquals(expectedIdentity) { + t.Fatalf("after identity doesn't match expected: expected %s, got %s", expectedIdentity.GoString(), beforeIdentity.GoString()) + } + }) +} + +func TestContext2Plan_crossResourceMoveBasic(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object_one.a") + addrB := mustResourceInstanceAddr("test_object_two.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_object_two" "a" { + } + + moved { + from = test_object_one.a + to = test_object_two.a + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"before"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object_one": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + "test_object_two": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + ServerCapabilities: providers.ServerCapabilities{ + MoveResourceState: true, + }, + } + p.MoveResourceStateResponse = &providers.MoveResourceStateResponse{ + TargetState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("after"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) + } + }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + if got, want := instPlan.Addr, addrB; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Update; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +func TestContext2Plan_crossProviderMove(t *testing.T) { + addrA := mustResourceInstanceAddr("one_object.a") + addrB := mustResourceInstanceAddr("two_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "two_object" "a" { + } + + moved { + from = one_object.a + to = two_object.a + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"before"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/one"]`)) + }) + + one := &testing_provider.MockProvider{} + one.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "one_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + + two := &testing_provider.MockProvider{} + two.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "two_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + ServerCapabilities: providers.ServerCapabilities{ + MoveResourceState: true, + }, + } + two.MoveResourceStateResponse = &providers.MoveResourceStateResponse{ + TargetState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("after"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("one"): testProviderFuncFixed(one), + addrs.NewDefaultProvider("two"): testProviderFuncFixed(two), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) + } + }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + if got, want := instPlan.Addr, addrB; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Update; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +func TestContext2Plan_crossProviderMoveWithIdentity(t *testing.T) { + addrA := mustResourceInstanceAddr("one_object.a") + addrB := mustResourceInstanceAddr("two_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "two_object" "a" { + } + + moved { + from = one_object.a + to = two_object.a + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"before"}`), + Status: states.ObjectReady, + IdentityJSON: []byte(`{"id":"42"}`), + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/one"]`)) + }) + + one := &testing_provider.MockProvider{} + one.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "one_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } + + two := &testing_provider.MockProvider{} + two.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "two_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "arn": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + ServerCapabilities: providers.ServerCapabilities{ + MoveResourceState: true, + }, + } + + expectedTargetIdentity := cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("arn:4223"), + }) + + var receivedSourceIdentity []byte + two.MoveResourceStateFn = func(req providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + receivedSourceIdentity = req.SourceIdentity + return providers.MoveResourceStateResponse{ + TargetState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("after"), + }), + TargetIdentity: expectedTargetIdentity, + } + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("one"): testProviderFuncFixed(one), + addrs.NewDefaultProvider("two"): testProviderFuncFixed(two), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) + } + + expectedSourceIdentity := `{"id":"42"}` + if string(receivedSourceIdentity) != string(expectedSourceIdentity) { + t.Errorf("wrong source identity\ngot: %s\nwant: %s", string(receivedSourceIdentity), string(expectedSourceIdentity)) + } + }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + if got, want := instPlan.Addr, addrB; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Update; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + + targetIdentity, err := instPlan.BeforeIdentity.Decode(cty.Object(map[string]cty.Type{ + "arn": cty.String, + })) + if err != nil { + t.Fatalf("failed to decode after identity: %s", err) + } + + if !targetIdentity.RawEquals(expectedTargetIdentity) { + t.Errorf("wrong target identity\ngot: %s\nwant: %s", targetIdentity.GoString(), expectedTargetIdentity.GoString()) + } + }) +} + +func TestContext2Plan_crossResourceMoveMissingConfig(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object_one.a") + addrB := mustResourceInstanceAddr("test_object_two.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + moved { + from = test_object_one.a + to = test_object_two.a + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"before"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{}, + ResourceTypes: map[string]providers.Schema{ + "test_object_one": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + "test_object_two": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + } + p.MoveResourceStateResponse = &providers.MoveResourceStateResponse{ + TargetState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("after"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) + } + }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + if got, want := instPlan.Addr, addrB; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Delete; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoMoveTarget; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +func TestContext2Plan_crossResourceMoveWithIdentity(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object_one.a") + addrB := mustResourceInstanceAddr("test_object_two.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_object_two" "a" { + } + + moved { + from = test_object_one.a + to = test_object_two.a + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + // The prior state tracks test_object.a, which we should treat as + // test_object.b because of the "moved" block in the config. + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"value":"before"}`), + Status: states.ObjectReady, + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"oneId": "before"}`), + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + expectedSourceIdentity := `{"oneId": "before"}` + expectedTargetIdentity := cty.ObjectVal(map[string]cty.Value{ + "twoId": cty.StringVal("after"), + }) + + p := &testing_provider.MockProvider{} + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_object_one": &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + "test_object_two": &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "test_object_one": { + Attributes: map[string]*configschema.Attribute{ + "oneId": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + + "test_object_two": { + Attributes: map[string]*configschema.Attribute{ + "twoId": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "test_object_one": 0, + "test_object_two": 2, + }, + }) + p.GetProviderSchemaResponse.ServerCapabilities = providers.ServerCapabilities{ + MoveResourceState: true, + } + + var sourceIdentity []byte + p.MoveResourceStateFn = func(req providers.MoveResourceStateRequest) providers.MoveResourceStateResponse { + sourceIdentity = req.SourceIdentity + return providers.MoveResourceStateResponse{ + TargetState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("after"), + }), + TargetIdentity: expectedTargetIdentity, + } + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + if string(expectedSourceIdentity) != string(sourceIdentity) { + t.Fatalf("unexpected source identity; expected %s, got %s", string(expectedSourceIdentity), string(sourceIdentity)) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan != nil { + t.Fatalf("unexpected plan for %s; should've moved to %s", addrA, addrB) + } + }) + t.Run(addrB.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrB) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrB) + } + + identity, err := instPlan.BeforeIdentity.Decode(cty.Object(map[string]cty.Type{ + "twoId": cty.String, + })) + if err != nil { + t.Fatalf("unexpected error decoding identity: %s", err) + } + + if !identity.RawEquals(expectedTargetIdentity) { + t.Fatalf("unexpected target identity; expected %s, got %s", expectedTargetIdentity.GoString(), identity.GoString()) + } + }) +} + +func TestContext2Plan_untargetedResourceSchemaChange(t *testing.T) { + // an untargeted resource which requires a schema migration should not + // block planning due external changes in the plan. + addrA := mustResourceInstanceAddr("test_object.a") + addrB := mustResourceInstanceAddr("test_object.b") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { +} +resource "test_object" "b" { +}`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(addrB, &states.ResourceInstanceObjectSrc{ + // old_list is no longer in the schema + AttrsJSON: []byte(`{"old_list":["used to be","a list here"]}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + + // external changes trigger a "drift report", but because test_object.b was + // not targeted, the state was not fixed to match the schema and cannot be + // deocded for the report. + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + obj := req.PriorState.AsValueMap() + // test_number changed externally + obj["test_number"] = cty.NumberIntVal(1) + resp.NewState = cty.ObjectVal(obj) + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + Targets: []addrs.Targetable{ + addrA, + }, + }) + // + tfdiags.AssertNoErrors(t, diags) } func TestContext2Plan_movedResourceRefreshOnly(t *testing.T) { @@ -1680,10 +2598,10 @@ func TestContext2Plan_refreshOnlyMode(t *testing.T) { p := simpleMockProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ "test_object": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "arg": {Type: cty.String, Optional: true}, }, @@ -1734,6 +2652,7 @@ func TestContext2Plan_refreshOnlyMode(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors\n%s", diags.Err().Error()) } + checkPlanCompleteAndApplyable(t, plan) if !p.UpgradeResourceStateCalled { t.Errorf("Provider's UpgradeResourceState wasn't called; should've been") @@ -1816,10 +2735,10 @@ func TestContext2Plan_refreshOnlyMode_deposed(t *testing.T) { p := simpleMockProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ "test_object": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "arg": {Type: cty.String, Optional: true}, }, @@ -1957,10 +2876,10 @@ func TestContext2Plan_refreshOnlyMode_orphan(t *testing.T) { p := simpleMockProvider() p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ "test_object": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "arg": {Type: cty.String, Optional: true}, }, @@ -2109,14 +3028,14 @@ data "test_data_source" "foo" { `, }) - p := new(MockProvider) + p := new(testing_provider.MockProvider) p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { resp.PlannedState = cty.ObjectVal(map[string]cty.Value{ "sensitive": cty.UnknownVal(cty.String), }) return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2157,11 +3076,8 @@ data "test_data_source" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"string":"data_id", "foo":[{"bar":"old"}]}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("foo"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("foo"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -2171,11 +3087,8 @@ data "test_data_source" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"sensitive":"old"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - { - Path: cty.GetAttrPath("sensitive"), - Marks: cty.NewValueMarks(marks.Sensitive), - }, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("sensitive"), }, }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), @@ -2188,7 +3101,7 @@ data "test_data_source" "foo" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, res := range plan.Changes.Resources { switch res.Addr.String() { @@ -2443,7 +3356,7 @@ output "output" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, res := range plan.Changes.Resources { // both existing data sources should be read during plan @@ -2452,7 +3365,7 @@ output "output" { } if res.Addr.Resource.Resource.Mode == addrs.DataResourceMode && res.Action != plans.NoOp { - t.Errorf("unexpected %s plan for %s", res.Action, res.Addr) + t.Errorf("unexpected %s/%s plan for %s", res.Action, res.ActionReason, res.Addr) } } } @@ -2584,7 +3497,7 @@ resource "test_resource" "a" { }) p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -2601,13 +3514,13 @@ resource "test_resource" "a" { }, }) - ctx := testContext2(t, &ContextOpts{ - Providers: map[addrs.Provider]providers.Factory{ - addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), - }, - }) - t.Run("conditions pass", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { m := req.ProposedNewState.AsValueMap() m["output"] = cty.StringVal("bar") @@ -2625,7 +3538,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, res := range plan.Changes.Resources { switch res.Addr.String() { case "test_resource.a": @@ -2639,6 +3552,12 @@ resource "test_resource" "a" { }) t.Run("precondition fail", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ Mode: plans.NormalMode, SetVariables: InputValues{ @@ -2660,6 +3579,12 @@ resource "test_resource" "a" { }) t.Run("precondition fail refresh-only", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`), @@ -2675,7 +3600,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if len(diags) == 0 { t.Fatalf("no diags, but should have warnings") } @@ -2688,6 +3613,12 @@ resource "test_resource" "a" { }) t.Run("postcondition fail", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { m := req.ProposedNewState.AsValueMap() m["output"] = cty.StringVal("") @@ -2696,7 +3627,7 @@ resource "test_resource" "a" { resp.LegacyTypeSystem = true return resp } - _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ Mode: plans.NormalMode, SetVariables: InputValues{ "boop": &InputValue{ @@ -2714,9 +3645,36 @@ resource "test_resource" "a" { if !p.PlanResourceChangeCalled { t.Errorf("Provider's PlanResourceChange wasn't called; should've been") } + + if !plan.Errored { + t.Errorf("plan is not marked as errored") + } + + // The plan should still include a proposed change for the resource + // instance whose postcondition failed, since its plan is valid in + // isolation even though the postcondition prevents any further + // planning downstream. This gives the UI the option of describing + // the planned change as additional context alongside the error + // message, although it's up to the UI to decide whether and how to + // do that. + changes := plan.Changes + changeSrc := changes.ResourceInstance(mustResourceInstanceAddr("test_resource.a")) + if changeSrc != nil { + if got, want := changeSrc.Action, plans.Create; got != want { + t.Errorf("wrong proposed change action\ngot: %s\nwant: %s", got, want) + } + } else { + t.Errorf("no planned change for test_resource.a") + } }) t.Run("postcondition fail refresh-only", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`), @@ -2748,7 +3706,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if len(diags) == 0 { t.Fatalf("no diags, but should have warnings") } @@ -2764,6 +3722,12 @@ resource "test_resource" "a" { }) t.Run("precondition and postcondition fail refresh-only", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + state := states.BuildState(func(s *states.SyncState) { s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_resource.a"), &states.ResourceInstanceObjectSrc{ AttrsJSON: []byte(`{"value":"boop","output":"blorp"}`), @@ -2795,7 +3759,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if got, want := len(diags), 2; got != want { t.Errorf("wrong number of warnings, got %d, want %d", got, want) } @@ -2846,7 +3810,7 @@ resource "test_resource" "a" { }) p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -2873,13 +3837,12 @@ resource "test_resource" "a" { }, }) - ctx := testContext2(t, &ContextOpts{ - Providers: map[addrs.Provider]providers.Factory{ - addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), - }, - }) - t.Run("conditions pass", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("boop"), @@ -2895,7 +3858,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) for _, res := range plan.Changes.Resources { switch res.Addr.String() { case "test_resource.a": @@ -2925,6 +3888,11 @@ resource "test_resource" "a" { }) t.Run("precondition fail", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ Mode: plans.NormalMode, SetVariables: InputValues{ @@ -2946,6 +3914,11 @@ resource "test_resource" "a" { }) t.Run("precondition fail refresh-only", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ Mode: plans.RefreshOnlyMode, SetVariables: InputValues{ @@ -2955,7 +3928,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if len(diags) == 0 { t.Fatalf("no diags, but should have warnings") } @@ -2979,6 +3952,11 @@ resource "test_resource" "a" { }) t.Run("postcondition fail", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("boop"), @@ -3006,6 +3984,11 @@ resource "test_resource" "a" { }) t.Run("postcondition fail refresh-only", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("boop"), @@ -3021,7 +4004,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if got, want := diags.ErrWithWarnings().Error(), "Resource postcondition failed: Results cannot be empty."; got != want { t.Fatalf("wrong error:\ngot: %s\nwant: %q", got, want) } @@ -3042,6 +4025,11 @@ resource "test_resource" "a" { }) t.Run("precondition and postcondition fail refresh-only", func(t *testing.T) { + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ State: cty.ObjectVal(map[string]cty.Value{ "foo": cty.StringVal("nope"), @@ -3057,7 +4045,7 @@ resource "test_resource" "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if got, want := len(diags), 2; got != want { t.Errorf("wrong number of warnings, got %d, want %d", got, want) } @@ -3109,7 +4097,7 @@ output "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) addr := addrs.RootModuleInstance.OutputValue("a") outputPlan := plan.Changes.OutputValue(addr) if outputPlan == nil { @@ -3161,7 +4149,7 @@ output "a" { }, }, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if len(diags) == 0 { t.Fatalf("no diags, but should have warnings") } @@ -3256,10 +4244,15 @@ func TestContext2Plan_preconditionErrors(t *testing.T) { `, tc.condition) m := testModuleInline(t, map[string]string{"main.tf": main}) - _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } + + if !plan.Errored { + t.Fatal("plan failed to record error") + } + diag := diags[0] if got, want := diag.Description().Summary, tc.wantSummary; got != want { t.Errorf("unexpected summary\n got: %s\nwant: %s", got, want) @@ -3267,6 +4260,13 @@ func TestContext2Plan_preconditionErrors(t *testing.T) { if got, want := diag.Description().Detail, tc.wantDetail; !strings.Contains(got, want) { t.Errorf("unexpected summary\ngot: %s\nwant to contain %q", got, want) } + + for _, kv := range plan.Checks.ConfigResults.Elements() { + // All these are configuration or evaluation errors + if kv.Value.Status != checks.StatusError { + t.Errorf("incorrect status, got %s", kv.Value.Status) + } + } }) } } @@ -3416,8 +4416,8 @@ data "test_object" "a" { `, }) - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ DataSources: map[string]*configschema.Block{ "test_object": { Attributes: map[string]*configschema.Attribute{ @@ -3459,7 +4459,7 @@ data "test_object" "a" { }) _, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) } func TestContext2Plan_applyGraphError(t *testing.T) { @@ -3518,3 +4518,2332 @@ resource "test_object" "b" { t.Fatalf("no cycle error found:\n got: %s\n", msg) } } + +// plan a destroy with no state where configuration could fail to evaluate +// expansion indexes. +func TestContext2Plan_emptyDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +locals { + enable = true + value = local.enable ? module.example[0].out : null +} + +module "example" { + count = local.enable ? 1 : 0 + source = "./example" +} +`, + "example/main.tf": ` +resource "test_resource" "x" { +} + +output "out" { + value = test_resource.x +} +`, + }) + + p := testProvider("test") + state := states.NewState() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + + tfdiags.AssertNoErrors(t, diags) + + // ensure that the given states are valid and can be serialized + if plan.PrevRunState == nil { + t.Fatal("nil plan.PrevRunState") + } + if plan.PriorState == nil { + t.Fatal("nil plan.PriorState") + } +} + +// A deposed instances which no longer exists during ReadResource creates NoOp +// change, which should not affect the plan. +func TestContext2Plan_deposedNoLongerExists(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "b" { + count = 1 + test_string = "updated" + lifecycle { + create_before_destroy = true + } +} +`, + }) + + p := simpleMockProvider() + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + s := req.PriorState.GetAttr("test_string").AsString() + if s == "current" { + resp.NewState = req.PriorState + return resp + } + // pretend the non-current instance has been deleted already + resp.NewState = cty.NullVal(req.PriorState.Type()) + return resp + } + + // Here we introduce a cycle via state which only shows up in the apply + // graph where the actual destroy instances are connected in the graph. + // This could happen for example when a user has an existing state with + // stored dependencies, and changes the config in such a way that + // contradicts the stored dependencies. + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceDeposed( + mustResourceInstanceAddr("test_object.a[0]").Resource, + states.DeposedKey("deposed"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"test_string":"old"}`), + Dependencies: []addrs.ConfigResource{}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a[0]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"test_string":"current"}`), + Dependencies: []addrs.ConfigResource{}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + tfdiags.AssertNoErrors(t, diags) +} + +// make sure there are no cycles with changes around a provider configured via +// managed resources. +func TestContext2Plan_destroyWithResourceConfiguredProvider(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + in = "a" +} + +provider "test" { + alias = "other" + in = test_object.a.out +} + +resource "test_object" "b" { + provider = test.other + in = "a" +} +`}) + + testProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "in": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "test_object": providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "in": { + Type: cty.String, + Optional: true, + }, + "out": { + Type: cty.Number, + Computed: true, + }, + }, + }, + }, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + }, + }) + + // plan+apply to create the initial state + opts := SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)) + plan, diags := ctx.Plan(m, states.NewState(), opts) + tfdiags.AssertNoErrors(t, diags) + state, diags := ctx.Apply(plan, m, nil) + tfdiags.AssertNoErrors(t, diags) + + // Resource changes which have dependencies across providers which + // themselves depend on resources can result in cycles. + // Because other_object transitively depends on the module resources + // through its provider, we trigger changes on both sides of this boundary + // to ensure we can create a valid plan. + // + // Try to replace both instances + addrA := mustResourceInstanceAddr("test_object.a") + addrB := mustResourceInstanceAddr(`test_object.b`) + opts.ForceReplace = []addrs.AbsResourceInstance{addrA, addrB} + + _, diags = ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Plan_destroyPartialState(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { +} + +output "out" { + value = module.mod.out +} + +module "mod" { + source = "./mod" +} +`, + + "./mod/main.tf": ` +resource "test_object" "a" { + count = 2 + + lifecycle { + precondition { + # test_object_b has already been destroyed, so referencing the first + # instance must not fail during a destroy plan. + condition = test_object.b[0].test_string == "invalid" + error_message = "should not block destroy" + } + precondition { + # this failing condition should bot block a destroy plan + condition = !local.continue + error_message = "should not block destroy" + } + } +} + +resource "test_object" "b" { + count = 2 +} + +locals { + continue = true +} + +output "out" { + # the reference to test_object.b[0] may not be valid during a destroy plan, + # but should not fail. + value = local.continue ? test_object.a[1].test_string != "invalid" && test_object.b[0].test_string != "invalid" : false + + precondition { + # test_object_b has already been destroyed, so referencing the first + # instance must not fail during a destroy plan. + condition = test_object.b[0].test_string == "invalid" + error_message = "should not block destroy" + } + precondition { + # this failing condition should bot block a destroy plan + condition = test_object.a[0].test_string == "invalid" + error_message = "should not block destroy" + } +} +`}) + + p := simpleMockProvider() + + // This state could be the result of a failed destroy, leaving only 2 + // remaining instances. We want to be able to continue the destroy to + // remove everything without blocking on invalid references or failing + // conditions. + state := states.NewState() + mod := state.EnsureModule(addrs.RootModuleInstance.Child("mod", addrs.NoKey)) + mod.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a[0]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"test_string":"current"}`), + Dependencies: []addrs.ConfigResource{}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + mod.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a[1]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectTainted, + AttrsJSON: []byte(`{"test_string":"current"}`), + Dependencies: []addrs.ConfigResource{}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Plan_destroyPartialStateLocalRef(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "already_destroyed" { + count = 1 + source = "./mod" +} + +locals { + eval_error = module.already_destroyed[0].out +} + +output "already_destroyed" { + value = local.eval_error +} + +`, + + "./mod/main.tf": ` +resource "test_object" "a" { +} + +output "out" { + value = test_object.a.test_string +} +`}) + + p := simpleMockProvider() + + state := states.NewState() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + tfdiags.AssertNoErrors(t, diags) +} + +// Make sure the data sources in the prior state are serializeable even if +// there were an error in the plan. +func TestContext2Plan_dataSourceReadPlanError(t *testing.T) { + m, snap := testModuleWithSnapshot(t, "data-source-read-with-plan-error") + awsProvider := testProvider("aws") + testProvider := testProvider("test") + + testProvider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + resp.PlannedState = req.ProposedNewState + resp.Diagnostics = resp.Diagnostics.Append(errors.New("oops")) + return resp + } + + state := states.NewState() + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(awsProvider), + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + }, + }) + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatalf("expected plan error") + } + + // make sure we can serialize the plan even if there were an error + _, _, _, err := contextOptsForPlanViaFile(t, snap, plan) + if err != nil { + t.Fatalf("failed to round-trip through planfile: %s", err) + } +} + +func TestContext2Plan_ignoredMarkedValue(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + map = { + prior = "value" + new = sensitive("ignored") + } +} +`}) + + testProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "map": { + Type: cty.Map(cty.String), + Optional: true, + }, + }, + }, + }, + }, + }, + } + + testProvider.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + // We're going to ignore any changes here and return the prior state. + resp.PlannedState = req.PriorState + return resp + } + + state := states.NewState() + root := state.RootModule() + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"map":{"prior":"value"}}`), + Dependencies: []addrs.ConfigResource{}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(testProvider), + }, + }) + + // plan+apply to create the initial state + opts := SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables)) + plan, diags := ctx.Plan(m, state, opts) + tfdiags.AssertNoErrors(t, diags) + + for _, c := range plan.Changes.Resources { + if c.Action != plans.NoOp { + t.Errorf("unexpected %s change for %s", c.Action, c.Addr) + } + } +} + +func TestContext2Plan_externalProvidersWithState(t *testing.T) { + // In this test we're going to use an external provider for a resource + // that is already in the state. Terraform should allow this, even though + // the provider isn't defined in the configuration. + + m := testModuleInline(t, map[string]string{ + "main.tf": ``, // no resources + }) + + state := states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent(mustResourceInstanceAddr("foo.a"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["hashicorp/foo"]`)) + }) + + fooProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "foo": { + Body: &configschema.Block{}, + }, + }, + }, + ConfigureProviderFn: func(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Pre-configured provider was reconfigured by the modules runtime", + "An externally-configured provider should not have its ConfigureProvider function called during planning.", + ), + }, + } + }, + } + + ctx, diags := NewContext(&ContextOpts{ + PreloadedProviderSchemas: map[addrs.Provider]providers.GetProviderSchemaResponse{ + addrs.MustParseProviderSourceString("hashicorp/foo"): *fooProvider.GetProviderSchemaResponse, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + fooProvider.ConfigureProviderCalled = true + _, diags = ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ExternalProviders: map[addrs.RootProviderConfig]providers.Interface{ + addrs.RootProviderConfig{ + Provider: addrs.MustParseProviderSourceString("hashicorp/foo"), + }: fooProvider, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) +} + +func TestContext2Plan_externalProviders(t *testing.T) { + // This test exercises the option for callers to pass in their own + // already-configured provider instances, instead of the modules runtime + // configuring the providers itself. This is for situations where the root + // module is actually a shared module and some external system is acting + // as the true root, such as in Stacks where the stack configuration is + // the root and each component is a separate module tree with external + // providers passed into it similar to what we're doing in this test. + + m := testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + bar = { + source = "terraform.io/builtin/bar" + } + baz = { + configuration_aliases = [ baz.beep ] + } + } + } + + resource "foo" "a" {} + resource "bar" "b" {} + resource "baz" "c" { + provider = baz.beep + } + `, + }) + + mustNotConfigure := func(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Pre-configured provider was reconfigured by the modules runtime", + "An externally-configured provider should not have its ConfigureProvider function called during planning.", + ), + }, + } + } + + fooAddr := addrs.MustParseProviderSourceString("hashicorp/foo") + fooConfigAddr := addrs.RootProviderConfig{ + Provider: fooAddr, + } + fooProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + // We have a required argument here just so that the + // plan will fail if the runtime erroneously tries + // to prepare a configuration for this provider; + // in the absense of an external provider instance + // the runtime would behave as if there were an empty + // provider "foo" block and try to decode its synthetic + // empty body against this schema, but that should not + // happen if we're assuming the provider is already + // configured by an external caller. + "reqd": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "foo": { + Body: &configschema.Block{}, + }, + }, + }, + ConfigureProviderFn: mustNotConfigure, + } + barAddr := addrs.MustParseProviderSourceString("terraform.io/builtin/bar") + barConfigAddr := addrs.RootProviderConfig{ + Provider: barAddr, + } + barProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "bar": { + Body: &configschema.Block{}, + }, + }, + }, + ConfigureProviderFn: mustNotConfigure, + } + bazAddr := addrs.MustParseProviderSourceString("hashicorp/baz") + bazConfigAddr := addrs.RootProviderConfig{ + Provider: bazAddr, + Alias: "beep", + } + bazProvider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "baz": { + Body: &configschema.Block{}, + }, + }, + }, + ConfigureProviderFn: mustNotConfigure, + } + + ctx, diags := NewContext(&ContextOpts{ + PreloadedProviderSchemas: map[addrs.Provider]providers.GetProviderSchemaResponse{ + fooAddr: *fooProvider.GetProviderSchemaResponse, + barAddr: *barProvider.GetProviderSchemaResponse, + bazAddr: *bazProvider.GetProviderSchemaResponse, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // Many of the MockProvider methods check whether Configure was called and + // return an error if not, and so we'll set that flag ahead of time to + // simulate the effect of the provider being pre-configured by an external + // caller. + fooProvider.ConfigureProviderCalled = true + barProvider.ConfigureProviderCalled = true + bazProvider.ConfigureProviderCalled = true + + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + ExternalProviders: map[addrs.RootProviderConfig]providers.Interface{ + fooConfigAddr: fooProvider, + barConfigAddr: barProvider, + bazConfigAddr: bazProvider, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // If everything has worked as intended, the Plan call should've skipped + // configuring and closing all of the providers, because that's the + // caller's job to deal with. + if fooProvider.GetProviderSchemaCalled { + t.Errorf("called GetProviderSchema for %s", fooAddr) + } + if fooProvider.CloseCalled { + t.Errorf("called Close for %s", fooAddr) + } + if barProvider.GetProviderSchemaCalled { + t.Errorf("called GetProviderSchema for %s", barAddr) + } + if barProvider.CloseCalled { + t.Errorf("called Close for %s", barAddr) + } + if bazProvider.GetProviderSchemaCalled { + t.Errorf("called GetProviderSchema for %s", bazAddr) + } + if bazProvider.CloseCalled { + t.Errorf("called Close for %s", bazAddr) + } + + // However, the Plan call _should_ have asked each of the providers to + // plan a resource change. + if !fooProvider.PlanResourceChangeCalled { + t.Errorf("did not call PlanResourceChange for %s", fooConfigAddr) + } + if !barProvider.PlanResourceChangeCalled { + t.Errorf("did not call PlanResourceChange for %s", barConfigAddr) + } + if !bazProvider.PlanResourceChangeCalled { + t.Errorf("did not call PlanResourceChange for %s", bazConfigAddr) + } +} + +func TestContext2Apply_externalDependencyDeferred(t *testing.T) { + // This test deals with the situation where the stacks runtime knows + // that an upstream component already has deferred actions and so + // it's telling us that we need to artifically treat everything in + // the current configuration as deferred. + + cfg := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test" "a" { + name = "a" + } + + resource "test" "b" { + name = "b" + upstream_names = [test.a.name] + } + + resource "test" "c" { + name = "c" + upstream_names = toset([ + test.a.name, + test.b.name, + ]) + } + `, + }) + + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + "upstream_names": { + Type: cty.Set(cty.String), + Optional: true, + }, + }, + }, + }, + }, + }, + PlanResourceChangeFn: func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + } + }, + } + resourceInstancesActionsInPlan := func(p *plans.Plan) map[string]plans.Action { + ret := make(map[string]plans.Action) + for _, cs := range p.Changes.Resources { + // Anything that was deferred will not appear in the result at + // all. Non-deferred actions that don't actually need to do anything + // _will_ appear, but with action set to [plans.NoOp]. + ret[cs.Addr.String()] = cs.Action + } + return ret + } + cmpOpts := cmp.Options{ + ctydebug.CmpOptions, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(cfg, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + DeferralAllowed: true, + ExternalDependencyDeferred: true, + }) + tfdiags.AssertNoDiagnostics(t, diags) + if plan.Applyable { + t.Fatal("plan is applyable; should not be, because there's nothing to do yet") + } + if plan.Complete { + t.Fatal("plan is complete; should have deferred actions") + } + + gotActions := resourceInstancesActionsInPlan(plan) + wantActions := map[string]plans.Action{ + // No actions at all, because everything was deferred! + } + if diff := cmp.Diff(wantActions, gotActions, cmpOpts); diff != "" { + t.Fatalf("wrong actions in plan\n%s", diff) + } + + if len(plan.DeferredResources) != 3 { + t.Fatalf("expected exactly 3 deferred resources, got %d", len(plan.DeferredResources)) + } + + for _, res := range plan.DeferredResources { + if res.DeferredReason != providers.DeferredReasonDeferredPrereq { + t.Fatalf("expected all resources to be deferred due to deferred prerequisites, but %s was not", res.ChangeSrc.Addr) + } + } +} + +func TestContext2Plan_removedResourceForgetBasic(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +removed { + from = test_object.a + lifecycle { + destroy = false + } +} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + // We should have a warning, though! We'll lightly abuse the "for RPC" + // feature of diagnostics to get some more-readily-comparable diagnostic + // values. + gotDiags := diags.ForRPC() + wantDiags := tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Some objects will no longer be managed by Terraform", + `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: + - test_object.a + +After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`, + ), + }.ForRPC() + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrA) + } + + if got, want := instPlan.Addr, addrA; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Forget; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +func TestContext2Plan_removedResourceDestroyBasic(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + // block is effectively a no-op, because the resource has already been + // removed from config. + "main.tf": ` + removed { + from = test_object.a + lifecycle { + destroy = true + } + } + `, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if len(diags) > 0 { + t.Fatalf("unexpected diags\n%s", diags.Err().Error()) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addrA) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrA) + } + + if got, want := instPlan.Addr, addrA; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Delete; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +// This test case imagines a situation in which a create_before_destroy resource +// was replaced during the last apply. The replace partially failed: the create +// step succeeded while the destroy step failed, so a deposed resource +// instance remains in state. +// Now the user has removed the "resource" block from config and replaced it +// with a "removed" block with destroy = false. +// In this case, the user signals their intent that any instances of the +// resource be forgotten, not destroyed, so the correct action is Forget. +func TestContext2Plan_removedResourceForgetDeposed(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + deposedKey := states.DeposedKey("gone") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +removed { + from = test_object.a + lifecycle { + destroy = false + } +} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceDeposed(addrA, deposedKey, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + // We should have a warning, though! We'll lightly abuse the "for RPC" + // feature of diagnostics to get some more-readily-comparable diagnostic + // values. + gotDiags := diags.ForRPC() + wantDiags := tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Warning, + "Some objects will no longer be managed by Terraform", + `If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them: + - test_object.a + +After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.`, + ), + }.ForRPC() + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } + + t.Run(addrA.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstanceDeposed(addrA, deposedKey) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addrA) + } + + if got, want := instPlan.Addr, addrA; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addrA; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Forget; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +func TestContext2Plan_removedResourceErrorStillInConfig(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" {} + +removed { + from = test_object.a + lifecycle { + destroy = false + } +} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + if got, want := diags.Err().Error(), "Removed resource still exists"; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} + +func TestContext2Plan_removedModuleRemovesDeeplyNestedResources(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +removed { + from = module.child + lifecycle { + destroy = false + } +} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("module.child.test_object.a"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("module.child.module.grandchild.test_object.a"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + + // We'll also check it works for deeply nested deposed objects too. + s.SetResourceInstanceDeposed(mustResourceInstanceAddr("module.child.module.grandchild.test_object.a"), "gone", &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + }) + if diags.HasErrors() { + t.Errorf("expected no errors but got %s", diags.Err()) + } + + resources := []string{"module.child.test_object.a", "module.child.module.grandchild.test_object.a"} + for _, resource := range resources { + t.Run(resource, func(t *testing.T) { + addr := mustResourceInstanceAddr(resource) + + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Forget; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceDeleteBecauseNoResourceConfig; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) + } + + t.Run("module.child.module.grandchild.test_object.a (deposed)", func(t *testing.T) { + addr := mustResourceInstanceAddr("module.child.module.grandchild.test_object.a") + + instPlan := plan.Changes.ResourceInstanceDeposed(addr, "gone") + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Forget; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + }) +} + +func TestContext2Plan_removedModuleErrorStillInConfigNested(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "a" { + source = "./mod" +} + +removed { + from = module.a.test_object.a + lifecycle { + destroy = false + } +} +`, + + "mod/main.tf": ` +resource "test_object" "a" {} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + if got, want := diags.Err().Error(), "Removed resource still exists"; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} + +func TestContext2Plan_removedModuleErrorStillInConfig(t *testing.T) { + addrA := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +module "a" { + source = "./mod" +} + +removed { + from = module.a + lifecycle { + destroy = false + } +} +`, + + "mod/main.tf": ` +resource "test_object" "a" {} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(addrA, &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"foo":"bar"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addrA, + }, + }) + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + if got, want := diags.Err().Error(), "Removed module still exists"; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} + +func TestContext2Plan_sensitiveInputVariableValue(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "boop" { + type = string + # this variable is not marked sensitive +} + +resource "test_resource" "a" { + value = var.boop +} + +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Build state with sensitive value in resource object + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_resource.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"value":"secret"}]}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + }, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + // Create a sensitive-marked value for the input variable. This is not + // possible through the normal CLI path, but is possible when the plan is + // created and modified by the stacks runtime. + secret := cty.StringVal("secret").Mark(marks.Sensitive) + plan, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.NormalMode, + SetVariables: InputValues{ + "boop": &InputValue{ + Value: secret, + SourceType: ValueFromUnknown, + }, + }, + }) + tfdiags.AssertNoErrors(t, diags) + for _, res := range plan.Changes.Resources { + switch res.Addr.String() { + case "test_resource.a": + if res.Action != plans.NoOp { + t.Errorf("unexpected %s change for %s", res.Action, res.Addr) + } + default: + t.Errorf("unexpected %s change for %s", res.Action, res.Addr) + } + } +} +func TestContext2Plan_dataSourceSensitiveRead(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +data "test_data_source" "foo" { +} + +resource "test_object" "obj" { + test_string = data.test_data_source.foo.bar +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + DataSources: map[string]*configschema.Block{ + "test_data_source": { + Attributes: map[string]*configschema.Attribute{ + "bar": { + Type: cty.String, + Computed: true, + Sensitive: true, + }, + }, + }, + }, + ResourceTypes: map[string]*configschema.Block{ + "test_object": simpleTestSchema(), + }, + }) + + p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "bar": cty.StringVal("data_id"), + }), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + ch := plan.Changes.ResourceInstance(mustResourceInstanceAddr("test_object.obj")) + if len(ch.AfterSensitivePaths) == 0 { + t.Fatal("expected marked values in test_object.obj") + } +} + +// When a schema declares that attributes nested within sets are sensitive, the +// resulting cty values will transfer those marks to the containing set. Verify +// that this does not present a change in the plan. +func TestContext2Plan_nestedSensitiveMarks(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "obj" { + set_block { + foo = "bar" + } +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_object": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "set_block": { + Nesting: configschema.NestingSet, + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Sensitive: true, + Optional: true, + }, + }, + }, + }, + }, + }, + }, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(mustResourceInstanceAddr("test_object.obj"), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"id":"z","set_block":[{"foo":"bar"}]}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("set_block"), + }, + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + ch := plan.Changes.ResourceInstance(mustResourceInstanceAddr("test_object.obj")) + if ch.Action != plans.NoOp { + t.Fatal("expected no change in plan") + } +} + +func TestContext2Plan_ephemeralInResource(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + beep = { + source = "terraform.io/builtin/beep" + } + } + } + + variable "in" { + type = string + ephemeral = true + } + + resource "beep" "boop" { + in = var.in + } + + data "beep" "boop" { + in = var.in + } + `, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "beep": { + Attributes: map[string]*configschema.Attribute{ + "in": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }) + p.GetProviderSchemaResponse.DataSources = p.GetProviderSchemaResponse.ResourceTypes + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("beep"): testProviderFuncFixed(p), + }, + }) + + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid use of ephemeral value", + Detail: `Ephemeral values are not valid for "in", because it is not a write-only attribute and must be persisted to state.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{ + Line: 16, Column: 10, Byte: 223, + }, + End: hcl.Pos{ + Line: 16, Column: 16, Byte: 229, + }, + }, + }) + wantDiags = wantDiags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid use of ephemeral value", + Detail: `Ephemeral values are not valid for "in", because it is not a write-only attribute and must be persisted to state.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{ + Line: 20, Column: 10, Byte: 269, + }, + End: hcl.Pos{ + Line: 20, Column: 16, Byte: 275, + }, + }, + }) + _, gotDiags := ctx.Plan( + m, states.NewState(), + SimplePlanOpts(plans.NormalMode, InputValues{ + "in": { + Value: cty.StringVal("hello"), + SourceType: ValueFromCaller, + }, + }), + ) + // We'll use the "for RPC" representation just as a convenient shortcut + // to not worry about exactly which diagnostic type Terraform Core chose + // to return here. + gotDiags = gotDiags.ForRPC() + gotDiags.Sort() + wantDiags = wantDiags.ForRPC() + wantDiags.Sort() + if diff := cmp.Diff(wantDiags, gotDiags); diff != "" { + t.Errorf("wrong diagnostics\n%s", diff) + } + +} + +func TestContext2Plan_ephemeralInProviderConfig(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + beep = { + source = "terraform.io/builtin/beep" + } + } + } + + variable "in" { + type = string + ephemeral = true + } + + provider "beep" { + in = var.in + } + + data "beep" "boop" { + } + `, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + Provider: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "in": { + Type: cty.String, + Optional: true, + }, + }, + }, + DataSources: map[string]*configschema.Block{ + "beep": {}, + }, + }) + p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ + State: cty.EmptyObjectVal, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("beep"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan( + m, states.NewState(), + SimplePlanOpts(plans.NormalMode, InputValues{ + "in": { + Value: cty.StringVal("hello"), + SourceType: ValueFromCaller, + }, + }), + ) + tfdiags.AssertNoDiagnostics(t, diags) + + if !p.ConfigureProviderCalled { + t.Fatal("ConfigureProvider was not called") + } + got := p.ConfigureProviderRequest.Config + want := cty.ObjectVal(map[string]cty.Value{ + // The value is not marked here, because Terraform Core unmarks it + // before calling the provider; ephemerality is Terraform Core's + // concern to deal with, not the provider's. + "in": cty.StringVal("hello"), + }) + if diff := cmp.Diff(want, got, ctydebug.CmpOptions); diff != "" { + t.Errorf("wrong provider configuration\n%s", diff) + } +} + +// This test explicitly reproduces the issue described in #34976. +func TestContext2Plan_34976(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +data "test_object" "obj" {} + +module "a" { + depends_on = [data.test_object.obj] + source = "./mod" +} + +output "value" { + value = try(module.a.notreal, null) +} +`, + "mod/main.tf": ``, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + DataSources: map[string]*configschema.Block{ + "test_object": {}, + }, + }) + p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + + // Just very small delay that means the output will be processed before + // the module. + time.Sleep(50 * time.Millisecond) + + return providers.ReadDataSourceResponse{ + State: req.Config, + } + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Just shouldn't crash. + _, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Plan_sensitiveRequiredReplace(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "obj" { + value = "changed" +} +`, + }) + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + Sensitive: true, + }, + }, + }, + }, + }, + } + p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("changed"), + }), + RequiresReplace: []cty.Path{ + cty.GetAttrPath("value"), + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.obj"), + &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustParseJson(map[string]any{ + "value": "original", + }), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + }, + Status: states.ObjectReady, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + _, diags := ctx.Plan(m, state, nil) + if len(diags) > 0 { + t.Errorf("unexpected diags\n%s", diags) + } +} + +func TestContext2Plan_writeOnlyRequiredReplace(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "obj" { + value = "changed" +} +`, + }) + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + RequiresReplace: []cty.Path{ + cty.GetAttrPath("value"), + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + rAddr := mustResourceInstanceAddr("test_object.obj") + pAddr := mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`) + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + rAddr, + &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustParseJson(map[string]any{ + "value": nil, + }), + Status: states.ObjectReady, + }, + pAddr) + }) + + plan, diags := ctx.Plan(m, state, nil) + tfdiags.AssertNoErrors(t, diags) + + if plan.Changes.Empty() { + t.Fatal("unexpected empty plan") + } + expectedChanges := &plans.Changes{ + Resources: []*plans.ResourceInstanceChange{ + { + Addr: rAddr, + PrevRunAddr: rAddr, + ProviderAddr: pAddr, + Change: plans.Change{ + Action: plans.DeleteThenCreate, + Before: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + BeforeIdentity: cty.NullVal(cty.EmptyObject), + After: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + AfterIdentity: cty.NullVal(cty.EmptyObject), + }, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + RequiredReplace: cty.NewPathSet(cty.GetAttrPath("value")), + }, + }, + } + + schemas, schemaDiags := ctx.Schemas(m, plan.PriorState) + tfdiags.AssertNoDiagnostics(t, schemaDiags) + + changes, err := plan.Changes.Decode(schemas) + if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(expectedChanges, changes, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected changes: %s", diff) + } +} + +func TestContext2Plan_writeOnlyRequiredReplace_createBeforeDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "obj" { + value = "changed" + lifecycle { + create_before_destroy = true + } +} +`, + }) + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Required: true, + WriteOnly: true, + }, + }, + }, + }, + }, + } + p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ + PlannedState: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + RequiresReplace: []cty.Path{ + cty.GetAttrPath("value"), + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + rAddr := mustResourceInstanceAddr("test_object.obj") + pAddr := mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`) + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + rAddr, + &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustParseJson(map[string]any{ + "value": nil, + }), + Status: states.ObjectReady, + }, + pAddr) + }) + + plan, diags := ctx.Plan(m, state, nil) + tfdiags.AssertNoErrors(t, diags) + + if plan.Changes.Empty() { + t.Fatal("unexpected empty plan") + } + expectedChanges := &plans.Changes{ + Resources: []*plans.ResourceInstanceChange{ + { + Addr: rAddr, + PrevRunAddr: rAddr, + ProviderAddr: pAddr, + Change: plans.Change{ + Action: plans.CreateThenDelete, + Before: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + BeforeIdentity: cty.NullVal(cty.EmptyObject), + After: cty.ObjectVal(map[string]cty.Value{ + "value": cty.NullVal(cty.String), + }), + AfterIdentity: cty.NullVal(cty.EmptyObject), + }, + ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate, + RequiredReplace: cty.NewPathSet(cty.GetAttrPath("value")), + }, + }, + } + + schemas, schemaDiags := ctx.Schemas(m, plan.PriorState) + tfdiags.AssertNoDiagnostics(t, schemaDiags) + + changes, err := plan.Changes.Decode(schemas) + if err != nil { + t.Fatal(err) + } + if diff := cmp.Diff(expectedChanges, changes, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected changes: %s", diff) + } +} + +func TestContext2Plan_selfReferences(t *testing.T) { + tcs := []struct { + attribute string + }{ + // Note here, the type returned by the lookup doesn't really matter as + // we should safely fail before we even get to type checking. + { + attribute: "count = test_object.a[0].test_string", + }, + { + attribute: "count = test_object.a[*].test_string", + }, + { + attribute: "for_each = test_object.a[0].test_string", + }, + { + attribute: "for_each = test_object.a[*].test_string", + }, + // Even though the can and try functions might normally allow some + // fairly crazy things, we're still going to put a stop to a self + // reference since it is more akin to a compilation error than some kind + // of dynamic exception. + { + attribute: "for_each = can(test_object.a[0].test_string) ? 0 : 1", + }, + { + attribute: "count = try(test_object.a[0].test_string, 0)", + }, + } + for _, tc := range tcs { + t.Run(tc.attribute, func(t *testing.T) { + tmpl := ` +resource "test_object" "a" { + %%attribute%% +} +` + module := strings.ReplaceAll(tmpl, "%%attribute%%", tc.attribute) + m := testModuleInline(t, map[string]string{ + "main.tf": module, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + // The providers never actually going to get called here, we should + // catch the error long before anything happens. + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + got, want := diags.Err().Error(), "Self-referential block: Configuration for test_object.a may not refer to itself." + if cmp.Diff(want, got) != "" { + t.Fatalf("unexpected error\n%s", cmp.Diff(want, got)) + } + }) + } + +} + +func TestContext2Plan_destroySkipsVariableValidations(t *testing.T) { + // this validation cannot block destroy, because we can't be sure arbitrary + // expressions can be evaluated at all during destroy. + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "input" { + type = string + + validation { + condition = var.input == "foo" + error_message = "bad input" + } +} + +resource "test_object" "a" { + test_string = var.input +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a"), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + }), &PlanOpts{ + Mode: plans.DestroyMode, + SetVariables: InputValues{ + "input": { + Value: cty.StringVal("foo"), + SourceType: ValueFromCLIArg, + SourceRange: tfdiags.SourceRange{}, + }, + }, + }) + if diags.HasErrors() { + t.Fatalf("expected no errors, but got %s", diags.ErrWithWarnings()) + } + + planResult := plan.Checks.GetObjectResult(addrs.AbsInputVariableInstance{ + Variable: addrs.InputVariable{ + Name: "input", + }, + Module: addrs.RootModuleInstance, + }) + + if planResult != nil && planResult.Status != checks.StatusUnknown { + // checks should not have been evaluated, because the variable is not required for destroy. + t.Errorf("expected checks to be pass but was %s", planResult.Status) + } +} + +func TestContext2Plan_orphanOutput(t *testing.T) { + // ensure the planned replacement of the data source is evaluated properly + m := testModuleInline(t, map[string]string{ + "main.tf": ` +output "staying" { + value = "foo" +} +`, + }) + + state := states.BuildState(func(s *states.SyncState) { + s.SetOutputValue(mustAbsOutputValue("output.old"), cty.StringVal("old_value"), false) + s.SetOutputValue(mustAbsOutputValue("output.staying"), cty.StringVal("foo"), false) + }) + + ctx := testContext2(t, &ContextOpts{}) + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + expectedChanges := &plans.Changes{ + Outputs: []*plans.OutputChange{ + { + Addr: mustAbsOutputValue("output.old"), + Change: plans.Change{ + Action: plans.Delete, + Before: cty.StringVal("old_value"), + BeforeIdentity: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.DynamicPseudoType), + AfterIdentity: cty.NullVal(cty.DynamicPseudoType), + }, + }, + { + Addr: mustAbsOutputValue("output.staying"), + Change: plans.Change{ + Action: plans.NoOp, + Before: cty.StringVal("foo"), + BeforeIdentity: cty.NullVal(cty.DynamicPseudoType), + After: cty.StringVal("foo"), + AfterIdentity: cty.NullVal(cty.DynamicPseudoType), + }, + }, + }, + } + changes, err := plan.Changes.Decode(nil) + if err != nil { + t.Fatal(err) + } + + sort.SliceStable(changes.Outputs, func(i, j int) bool { + return changes.Outputs[i].Addr.String() < changes.Outputs[j].Addr.String() + }) + + if diff := cmp.Diff(expectedChanges, changes, ctydebug.CmpOptions); diff != "" { + t.Fatalf("unexpected changes: %s", diff) + } +} + +func TestContext2Plan_multiInstanceSelfRef(t *testing.T) { + // The postcondition here references self, but because instances are + // processed concurrently some instances may not be registered yet during + // evaluation. This should still evaluate without error, because we know our + // self value exists. + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "test" { +} + +data "test_data_source" "foo" { + count = 100 + lifecycle { + postcondition { + condition = self.attr == null + error_message = "error" + } + } + depends_on = [test_resource.test] +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + DataSources: map[string]*configschema.Block{ + "test_data_source": { + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) +} + +func TestContext2Plan_upgradeState_WriteOnlyAttribute(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "aws_instance" "foo" { + wo_attr = "value" +} +`, + }) + + p := testProvider("aws") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "wo_attr": { + Type: cty.String, + Optional: true, + WriteOnly: true, + }, + }, + }, + }, + }) + p.UpgradeResourceStateFn = func(r providers.UpgradeResourceStateRequest) (resp providers.UpgradeResourceStateResponse) { + return providers.UpgradeResourceStateResponse{ + UpgradedState: cty.ObjectVal(map[string]cty.Value{ + "wo_attr": cty.StringVal("not-empty"), + }), + } + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + priorState := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + mustResourceInstanceAddr("aws_instance.foo"), + &states.ResourceInstanceObjectSrc{ + // The UpgradeResourceStateFn above does not care about specific prior + // state but it must not be empty for the function to be actually called + AttrsJSON: []byte(`{"wo_attr":null}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + }) + + // Plan should invoke state upgrade logic and trigger validation, given the mocks above + _, diags := ctx.Plan(m, priorState, DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatalf("expected errors but got none") + } + + if got, want := diags.Err().Error(), "Invalid resource state upgrade"; !strings.Contains(got, want) { + t.Errorf("unexpected error message\ngot: %s\nwant substring: %s", got, want) + } +} + +func TestContext2Plan_orphanUpdateInstance(t *testing.T) { + // ean orphaned instance should still reflect the refreshed state in the plan + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + for_each = {} +} +`, + }) + + p := simpleMockProvider() + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + state := req.PriorState.AsValueMap() + state["test_string"] = cty.StringVal("new") + resp.NewState = cty.ObjectVal(state) + return resp + } + + state := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent(mustResourceInstanceAddr(`test_object.a["old"]`), &states.ResourceInstanceObjectSrc{ + AttrsJSON: []byte(`{"test_string":"old"}`), + Status: states.ObjectReady, + }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`)) + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + tfdiags.AssertNoErrors(t, diags) + + resourceType := p.GetProviderSchemaResponse.ResourceTypes["test_object"] + change, err := plan.Changes.ResourceInstance(mustResourceInstanceAddr(`test_object.a["old"]`)).Decode(resourceType) + if err != nil { + t.Fatal(err) + } + if change.Before.GetAttr("test_string").AsString() != "new" { + t.Fatalf("resource before value not refreshed in plan: %#v\n", change.Before) + } +} + +func TestContext2Plan_dataResourceNestedUnknown(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "create" { +} + +data "test_data_source" "foo" { + nested_map = { + "key1" : { + required_attr = test_resource.create.id + } + } + nested_obj = { + required_attr = test_resource.create.id + } +} +`, + }) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + DataSources: map[string]*configschema.Block{ + "test_data_source": { + Attributes: map[string]*configschema.Attribute{ + "nested_map": &configschema.Attribute{ + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "required_attr": { + Type: cty.String, + Required: true, + }, + "computed_attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + "nested_obj": &configschema.Attribute{ + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "required_attr": { + Type: cty.String, + Required: true, + }, + "computed_attr": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), SimplePlanOpts(plans.NormalMode, testInputValuesUnset(m.Module.Variables))) + tfdiags.AssertNoErrors(t, diags) + + dataSchemaType := p.GetProviderSchemaResponse.DataSources["test_data_source"].Body.ImpliedType() + after, err := plan.Changes.ResourceInstance(mustResourceInstanceAddr("data.test_data_source.foo")).After.Decode(dataSchemaType) + if err != nil { + t.Fatal(err) + } + + // any computed attributes within configured objects should be unknown + expected := cty.ObjectVal(map[string]cty.Value{ + "nested_map": cty.MapVal(map[string]cty.Value{ + "key1": cty.ObjectVal(map[string]cty.Value{ + "required_attr": cty.UnknownVal(cty.String), + "computed_attr": cty.UnknownVal(cty.String), + }), + }), + "nested_obj": cty.ObjectVal(map[string]cty.Value{ + "required_attr": cty.UnknownVal(cty.String), + "computed_attr": cty.UnknownVal(cty.String), + }), + }) + + if !after.RawEquals(expected) { + t.Fatalf("\nexpected plan: %#v\n actual plan: %#v\n", expected, after) + + } +} diff --git a/internal/terraform/context_plan_ephemeral_test.go b/internal/terraform/context_plan_ephemeral_test.go new file mode 100644 index 0000000000..dd234e0fef --- /dev/null +++ b/internal/terraform/context_plan_ephemeral_test.go @@ -0,0 +1,912 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" +) + +func TestContext2Plan_ephemeralValues(t *testing.T) { + for name, tc := range map[string]struct { + toBeImplemented bool + module map[string]string + expectValidateDiagnostics func(m *configs.Config) tfdiags.Diagnostics + expectPlanDiagnostics func(m *configs.Config) tfdiags.Diagnostics + expectOpenEphemeralResourceCalled bool + expectValidateEphemeralResourceConfigCalled bool + expectCloseEphemeralResourceCalled bool + assertTestProviderConfigure func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) + assertPlan func(*testing.T, *plans.Plan) + inputs InputValues + }{ + "basic": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" { +} +`}, + expectOpenEphemeralResourceCalled: true, + expectValidateEphemeralResourceConfigCalled: true, + expectCloseEphemeralResourceCalled: true, + }, + + "terraform.applying": { + module: map[string]string{ + "child/main.tf": ` +output "value" { + value = terraform.applying + # Testing that this errors in the best way to ensure the symbol is ephemeral + ephemeral = false +} +`, + "main.tf": ` +module "child" { + source = "./child" +} +`, + }, + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "child", "main.tf"), + Start: hcl.Pos{Line: 3, Column: 13, Byte: 30}, + End: hcl.Pos{Line: 3, Column: 31, Byte: 48}, + }, + }) + }, + }, + + "provider reference": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" { +} + +provider "test" { + test_string = ephemeral.ephem_resource.data.value +} + +resource "test_object" "test" { +} +`, + }, + expectOpenEphemeralResourceCalled: true, + expectValidateEphemeralResourceConfigCalled: true, + expectCloseEphemeralResourceCalled: true, + assertTestProviderConfigure: func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + attr := req.Config.GetAttr("test_string") + if attr.AsString() != "test string" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("received config did not contain \"test string\", got %#v\n", req.Config)) + } + return resp + }, + }, + + "normal attribute": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" { +} + +resource "test_object" "test" { + test_string = ephemeral.ephem_resource.data.value +} +`, + }, + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid use of ephemeral value", + Detail: `Ephemeral values are not valid for "test_string", because it is not a write-only attribute and must be persisted to state.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 17, Byte: 88}, + End: hcl.Pos{Line: 6, Column: 52, Byte: 123}, + }, + }) + }, + }, + + "provider reference through module": { + module: map[string]string{ + "child/main.tf": ` +ephemeral "ephem_resource" "data" { +} + +output "value" { + value = ephemeral.ephem_resource.data.value + ephemeral = true +} +`, + "main.tf": ` +module "child" { + source = "./child" +} + +provider "test" { + test_string = module.child.value +} + +resource "test_object" "test" { +} +`, + }, + expectOpenEphemeralResourceCalled: true, + expectValidateEphemeralResourceConfigCalled: true, + expectCloseEphemeralResourceCalled: true, + assertTestProviderConfigure: func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + attr := req.Config.GetAttr("test_string") + if attr.AsString() != "test string" { + resp.Diagnostics = resp.Diagnostics.Append(fmt.Errorf("received config did not contain \"test string\", got %#v\n", req.Config)) + } + return resp + }, + }, + + "resource expansion - for_each": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" {} +resource "test_object" "test" { + for_each = toset(ephemeral.ephem_resource.data.list) + test_string = each.value +} +`, + }, + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: `The given "for_each" value is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify a resource's instance keys.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 4, Column: 14, Byte: 83}, + End: hcl.Pos{Line: 4, Column: 55, Byte: 124}, + }, + }) + }, + }, + + "resource expansion - count": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" {} +resource "test_object" "test" { + count = length(ephemeral.ephem_resource.data.list) + test_string = count.index +} +`, + }, + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: `The given "count" is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify the number of resource instances.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 4, Column: 11, Byte: 80}, + End: hcl.Pos{Line: 4, Column: 53, Byte: 122}, + }, + }) + }, + }, + + "module expansion - for_each": { + module: map[string]string{ + "child/main.tf": ` +output "value" { + value = "static value" +} +`, + "main.tf": ` +ephemeral "ephem_resource" "data" { +} +module "child" { + for_each = toset(ephemeral.ephem_resource.data.list) + source = "./child" +} +`, + }, + + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: `The given "for_each" value is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify a resource's instance keys.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 5, Column: 16, Byte: 71}, + End: hcl.Pos{Line: 5, Column: 57, Byte: 112}, + }, + }) + }, + }, + + "module expansion - count": { + module: map[string]string{ + "child/main.tf": ` +output "value" { + value = "static value" +} +`, + "main.tf": ` +ephemeral "ephem_resource" "data" {} +module "child" { + count = length(ephemeral.ephem_resource.data.list) + source = "./child" +} +`, + }, + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: `The given "count" is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify the number of resource instances.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 4, Column: 13, Byte: 67}, + End: hcl.Pos{Line: 4, Column: 55, Byte: 109}, + }, + }) + }, + }, + + "import expansion": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" {} + +import { + for_each = toset(ephemeral.ephem_resource.data.list) + id = each.value + to = test_object.test[each.value] +} + +resource "test_object" "test" { + for_each = toset(ephemeral.ephem_resource.data.list) + test_string = each.value +} +`, + }, + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: `The given "for_each" value is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify a resource's instance keys.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 11, Column: 16, Byte: 207}, + End: hcl.Pos{Line: 11, Column: 57, Byte: 248}, + }, + }, + ) + }, + }, + + "functions": { + module: map[string]string{ + "child/main.tf": ` +ephemeral "ephem_resource" "data" {} + +# We expect this to error since it should be an ephemeral value +output "value" { + value = max(42, length(ephemeral.ephem_resource.data.list)) +} +`, + "main.tf": ` +module "child" { + source = "./child" +} + `, + }, + + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "child", "main.tf"), + Start: hcl.Pos{Line: 6, Column: 13, Byte: 132}, + End: hcl.Pos{Line: 6, Column: 64, Byte: 183}, + }, + }) + }, + }, + + "provider-defined functions": { + module: map[string]string{ + "child/main.tf": ` + +terraform { + required_providers { + ephem = { + source = "hashicorp/ephem" + } + } +} +ephemeral "ephem_resource" "data" {} + +# We expect this to error since it should be an ephemeral value +output "value" { + value = provider::ephem::either(ephemeral.ephem_resource.data.value, "b") +} +`, + "main.tf": ` +module "child" { + source = "./child" +} + `, + }, + + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "child", "main.tf"), + Start: hcl.Pos{Line: 14, Column: 13, Byte: 245}, + End: hcl.Pos{Line: 14, Column: 78, Byte: 310}, + }, + }) + }, + }, + + "check blocks": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" {} + +check "check_using_ephemeral_value" { + assert { + condition = ephemeral.ephem_resource.data.bool == false + error_message = "Fine to persist" + } + assert { + condition = ephemeral.ephem_resource.data.bool == false + error_message = "Shall not be persisted ${ephemeral.ephem_resource.data.bool}" + } +} + `, + }, + expectOpenEphemeralResourceCalled: true, + expectValidateEphemeralResourceConfigCalled: true, + expectCloseEphemeralResourceCalled: true, + + assertPlan: func(t *testing.T, p *plans.Plan) { + key := addrs.ConfigCheck{ + Module: addrs.RootModule, + Check: addrs.Check{ + Name: "check_using_ephemeral_value", + }, + } + result, ok := p.Checks.ConfigResults.GetOk(key) + if !ok { + t.Fatalf("expected to find check result for %q", key) + } + objKey := addrs.AbsCheck{ + Module: addrs.RootModuleInstance, + Check: addrs.Check{ + Name: "check_using_ephemeral_value", + }, + } + obj, ok := result.ObjectResults.GetOk(objKey) + if !ok { + t.Fatalf("expected to find object for %q", objKey) + } + expectedMessages := []string{"Fine to persist"} + if diff := cmp.Diff(expectedMessages, obj.FailureMessages); diff != "" { + t.Fatalf("unexpected messages: %s", diff) + } + }, + expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Check block assertion failed", + Detail: "Fine to persist", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 17, Byte: 104}, + End: hcl.Pos{Line: 6, Column: 60, Byte: 147}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Check block assertion failed", + Detail: "This check failed, but has an invalid error message as described in the other accompanying messages.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 10, Column: 17, Byte: 217}, + End: hcl.Pos{Line: 10, Column: 60, Byte: 260}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Error message refers to ephemeral values", + Detail: "The error expression used to explain this condition refers to ephemeral values, so Terraform will not display the resulting message." + + "\n\nYou can correct this by removing references to ephemeral values, or by using the ephemeralasnull() function on the references to not reveal ephemeral data.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 11, Column: 21, Byte: 281}, + End: hcl.Pos{Line: 11, Column: 83, Byte: 343}, + }, + }) + return diags + }, + }, + + "function ephemeralasnull": { + module: map[string]string{ + "child/main.tf": ` +ephemeral "ephem_resource" "data" {} + +output "value" { + value = ephemeralasnull(ephemeral.ephem_resource.data.value) +} +`, + "main.tf": ` +module "child" { + source = "./child" +} + `, + }, + expectOpenEphemeralResourceCalled: true, + expectValidateEphemeralResourceConfigCalled: true, + expectCloseEphemeralResourceCalled: true, + }, + + "locals": { + module: map[string]string{ + "child/main.tf": ` +ephemeral "ephem_resource" "data" {} + +locals { + composedString = "prefix-${ephemeral.ephem_resource.data.value}-suffix" + composedList = ["a", ephemeral.ephem_resource.data.value, "c"] + composedObj = { + key = ephemeral.ephem_resource.data.value + foo = "bar" + } +} + +# We expect this to error since it should be an ephemeral value +output "composedString" { + value = local.composedString +} +output "composedList" { + value = local.composedList +} +output "composedObj" { + value = local.composedObj +} +`, + "main.tf": ` +module "child" { + source = "./child" +} + `, + }, + + expectValidateDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "child", "main.tf"), + Start: hcl.Pos{Line: 15, Column: 13, Byte: 376}, + End: hcl.Pos{Line: 15, Column: 33, Byte: 396}, + }, + }, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "child", "main.tf"), + Start: hcl.Pos{Line: 18, Column: 13, Byte: 435}, + End: hcl.Pos{Line: 18, Column: 31, Byte: 453}, + }, + }, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "child", "main.tf"), + Start: hcl.Pos{Line: 21, Column: 13, Byte: 491}, + End: hcl.Pos{Line: 21, Column: 30, Byte: 508}, + }, + }) + }, + }, + "resource precondition": { + module: map[string]string{ + "main.tf": ` +locals { + test_value = 2 +} +ephemeral "ephem_resource" "data" { + lifecycle { + precondition { + condition = local.test_value != 2 + error_message = "value should not be 2" + } + } +} +`, + }, + expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Resource precondition failed", + Detail: "value should not be 2", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 8, Column: 19, Byte: 116}, + End: hcl.Pos{Line: 8, Column: 40, Byte: 137}, + }, + }) + }, + }, + "resource postcondition": { + module: map[string]string{ + "main.tf": ` +locals { + test_value = 2 +} +ephemeral "ephem_resource" "data" { + lifecycle { + postcondition { + condition = self.value == "pass" + error_message = "value should be \"pass\"" + } + } +} +`, + }, + expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Resource postcondition failed", + Detail: `value should be "pass"`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 8, Column: 19, Byte: 117}, + End: hcl.Pos{Line: 8, Column: 39, Byte: 137}, + }, + }) + }, + }, + + "variable validation": { + module: map[string]string{ + "main.tf": ` +variable "ephem" { + type = string + ephemeral = true + + validation { + condition = length(var.ephem) > 4 + error_message = "This should fail but not show the value: ${var.ephem}" + } +} + +output "out" { + value = ephemeralasnull(var.ephem) +} +`, + }, + inputs: InputValues{ + "ephem": &InputValue{ + Value: cty.StringVal("ami"), + }, + }, + expectPlanDiagnostics: func(m *configs.Config) (diags tfdiags.Diagnostics) { + return diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid value for variable", + Detail: fmt.Sprintf(`The error message included a sensitive value, so it will not be displayed. + +This was checked by the validation rule at %s.`, m.Module.Variables["ephem"].Validations[0].DeclRange.String()), + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, + End: hcl.Pos{Line: 2, Column: 17, Byte: 17}, + }, + }).Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Error message refers to ephemeral values", + Detail: `The error expression used to explain this condition refers to ephemeral values. Terraform will not display the resulting message. + +You can correct this by removing references to ephemeral values, or by carefully using the ephemeralasnull() function if the expression will not reveal the ephemeral data.`, + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 8, Column: 21, Byte: 142}, + End: hcl.Pos{Line: 8, Column: 76, Byte: 197}, + }, + }) + }, + }, + + "write_only attribute": { + module: map[string]string{ + "main.tf": ` +ephemeral "ephem_resource" "data" { +} +resource "ephem_write_only" "test" { + write_only = ephemeral.ephem_resource.data.value +} +`, + }, + expectOpenEphemeralResourceCalled: true, + expectValidateEphemeralResourceConfigCalled: true, + expectCloseEphemeralResourceCalled: true, + }, + "write_only_sensitive_and_ephem": { + module: map[string]string{ + "main.tf": ` +variable "in" { + sensitive = true + ephemeral = true +} +resource "ephem_write_only" "test" { + write_only = var.in +} +`, + }, + inputs: InputValues{ + "in": &InputValue{ + Value: cty.StringVal("test"), + }, + }, + }, + } { + t.Run(name, func(t *testing.T) { + m := testModuleInline(t, tc.module) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + EphemeralResourceTypes: map[string]providers.Schema{ + "ephem_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + + "list": { + Type: cty.List(cty.String), + Computed: true, + }, + + "map": { + Type: cty.List(cty.Map(cty.String)), + Computed: true, + }, + + "bool": { + Type: cty.Bool, + Computed: true, + }, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "ephem_write_only": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "write_only": { + Type: cty.String, + WriteOnly: true, + Optional: true, + }, + }, + }, + }, + }, + Functions: map[string]providers.FunctionDecl{ + "either": { + Parameters: []providers.FunctionParam{ + { + Name: "a", + Type: cty.String, + }, + { + Name: "b", + Type: cty.String, + }, + }, + ReturnType: cty.String, + }, + }, + }, + } + + ephem.OpenEphemeralResourceFn = func(providers.OpenEphemeralResourceRequest) (resp providers.OpenEphemeralResourceResponse) { + resp.Result = cty.ObjectVal(map[string]cty.Value{ + "value": cty.StringVal("test string"), + "list": cty.ListVal([]cty.Value{cty.StringVal("test string 1"), cty.StringVal("test string 2")}), + "map": cty.ListVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "id": cty.StringVal("id-0"), + "to": cty.StringVal("aws_instance.a"), + }), + cty.MapVal(map[string]cty.Value{ + "id": cty.StringVal("id-1"), + "to": cty.StringVal("aws_instance.b"), + }), + }), + "bool": cty.True, + }) + return resp + } + + ephem.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + resp.Result = cty.StringVal(req.Arguments[0].AsString()) + return resp + } + + p := simpleMockProvider() + p.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + if tc.assertTestProviderConfigure != nil { + return tc.assertTestProviderConfigure(req) + } + return resp + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + // The providers never actually going to get called here, we should + // catch the error long before anything happens. + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if tc.expectValidateDiagnostics != nil { + tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectValidateDiagnostics(m)) + // If we expect diagnostics, we should not continue with the plan + // as it will fail. + return + } else { + tfdiags.AssertNoDiagnostics(t, diags) + } + + if tc.expectValidateEphemeralResourceConfigCalled { + if !ephem.ValidateEphemeralResourceConfigCalled { + t.Fatal("ValidateEphemeralResourceConfig not called") + } + } + + inputs := tc.inputs + if inputs == nil { + inputs = InputValues{} + } + + plan, diags := ctx.Plan(m, nil, SimplePlanOpts(plans.NormalMode, inputs)) + if tc.expectPlanDiagnostics != nil { + tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectPlanDiagnostics(m)) + } else { + tfdiags.AssertNoDiagnostics(t, diags) + } + + if tc.assertPlan != nil { + tc.assertPlan(t, plan) + } + + if tc.expectOpenEphemeralResourceCalled { + if !ephem.OpenEphemeralResourceCalled { + t.Fatal("OpenEphemeralResource not called") + } + } + + if tc.expectCloseEphemeralResourceCalled { + if !ephem.CloseEphemeralResourceCalled { + t.Fatal("CloseEphemeralResource not called") + } + } + }) + } +} + +func TestContext2Apply_ephemeralUnknownPlan(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_instance" "test" { +} + +ephemeral "ephem_resource" "data" { + input = test_instance.test.id + lifecycle { + postcondition { + condition = self.value != nil + error_message = "should return a value" + } + } +} + +locals { + value = ephemeral.ephem_resource.data.value +} + +// create a sink for the ephemeral value to test +provider "sink" { + test_string = local.value +} + +// we need a resource to ensure the sink provider is configured +resource "sink_object" "empty" { +} +`, + }) + + ephem := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + EphemeralResourceTypes: map[string]providers.Schema{ + "ephem_resource": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": { + Type: cty.String, + Computed: true, + }, + "input": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, + } + + sink := simpleMockProvider() + sink.GetProviderSchemaResponse.ResourceTypes = map[string]providers.Schema{ + "sink_object": {Body: simpleTestSchema()}, + } + sink.ConfigureProviderFn = func(req providers.ConfigureProviderRequest) (resp providers.ConfigureProviderResponse) { + if req.Config.GetAttr("test_string").IsKnown() { + t.Error("sink provider config should not be known in this test") + } + return resp + } + + p := testProvider("test") + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("ephem"): testProviderFuncFixed(ephem), + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + addrs.NewDefaultProvider("sink"): testProviderFuncFixed(sink), + }, + }) + + _, diags := ctx.Plan(m, nil, DefaultPlanOpts) + tfdiags.AssertNoDiagnostics(t, diags) + + if ephem.OpenEphemeralResourceCalled { + t.Error("OpenEphemeralResourceCalled called when config was not known") + } +} diff --git a/internal/terraform/context_plan_identity_test.go b/internal/terraform/context_plan_identity_test.go new file mode 100644 index 0000000000..1c6a573f70 --- /dev/null +++ b/internal/terraform/context_plan_identity_test.go @@ -0,0 +1,828 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestContext2Plan_resource_identity_refresh(t *testing.T) { + for name, tc := range map[string]struct { + StoredIdentitySchemaVersion uint64 + StoredIdentityJSON []byte + IdentitySchema providers.IdentitySchema + IdentityData cty.Value + ExpectedIdentity cty.Value + ExpectedError error + ExpectUpgradeResourceIdentityCalled bool + UpgradeResourceIdentityResponse providers.UpgradeResourceIdentityResponse + }{ + "no previous identity": { + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + "identity version mismatch": { + StoredIdentitySchemaVersion: 1, + StoredIdentityJSON: []byte(`{"id": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedError: fmt.Errorf("Resource instance managed by newer provider version: The current state of aws_instance.web was created by a newer provider version than is currently selected. Upgrade the aws provider to work with this state."), + }, + "identity type mismatch": { + StoredIdentitySchemaVersion: 0, + StoredIdentityJSON: []byte(`{"arn": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectedError: fmt.Errorf("failed to decode identity: unsupported attribute \"arn\". This is most likely a bug in the Provider, providers must not change the identity schema without updating the identity schema version"), + }, + "identity upgrade succeeds": { + StoredIdentitySchemaVersion: 1, + StoredIdentityJSON: []byte(`{"arn": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 2, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + UpgradeResourceIdentityResponse: providers.UpgradeResourceIdentityResponse{ + UpgradedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + ExpectUpgradeResourceIdentityCalled: true, + }, + "identity upgrade failed": { + StoredIdentitySchemaVersion: 1, + StoredIdentityJSON: []byte(`{"id": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 2, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "arn": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("arn:foo"), + }), + UpgradeResourceIdentityResponse: providers.UpgradeResourceIdentityResponse{ + UpgradedIdentity: cty.NilVal, + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "failed to upgrade resource identity", "provider was unable to do so"), + }, + }, + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("arn:foo"), + }), + ExpectUpgradeResourceIdentityCalled: true, + ExpectedError: fmt.Errorf("failed to upgrade resource identity: provider was unable to do so"), + }, + "identity sent to provider differs from returned one": { + // We don't throw an error here, because there are resource types with mutable identities + StoredIdentitySchemaVersion: 0, + StoredIdentityJSON: []byte(`{"id": "foo"}`), + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + ExpectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + "identity with unknowns": { + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + ExpectedError: fmt.Errorf("Provider produced invalid identity: Provider \"registry.terraform.io/hashicorp/aws\" returned an identity with unknown values for aws_instance.web. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + + "identity with marks": { + IdentitySchema: providers.IdentitySchema{ + Version: 0, + Body: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityData: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("marked value").Mark(marks.Sensitive), + }), + ExpectedError: fmt.Errorf("Provider produced invalid identity: Provider \"registry.terraform.io/hashicorp/aws\" returned an identity with marks for aws_instance.web. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + } { + t.Run(name, func(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "foo": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_instance": tc.IdentitySchema.Body, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "aws_instance": uint64(tc.IdentitySchema.Version), + }, + }) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("aws_instance.web").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`), + IdentitySchemaVersion: tc.StoredIdentitySchemaVersion, + IdentityJSON: tc.StoredIdentityJSON, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + + p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { + return providers.ReadResourceResponse{ + NewState: req.PriorState, + Identity: tc.IdentityData, + } + } + p.UpgradeResourceIdentityResponse = &tc.UpgradeResourceIdentityResponse + + s, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.RefreshOnlyMode}) + + // TODO: maybe move to comparing diagnostics instead + if tc.ExpectedError != nil { + if !diags.HasErrors() { + t.Fatal("expected error, got none") + } + if diags.Err().Error() != tc.ExpectedError.Error() { + t.Fatalf("unexpected error\nwant: %v\ngot: %v", tc.ExpectedError, diags.Err()) + } + + return + } else { + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + } + + if !p.ReadResourceCalled { + t.Fatal("ReadResource should be called") + } + + if tc.ExpectUpgradeResourceIdentityCalled && !p.UpgradeResourceIdentityCalled { + t.Fatal("UpgradeResourceIdentity should be called") + } + + mod := s.PriorState.RootModule() + fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) + if err != nil { + t.Fatal(err) + } + + if tc.ExpectedIdentity.Equals(fromState.Identity).False() { + t.Fatalf("wrong identity\nwant: %s\ngot: %s", tc.ExpectedIdentity.GoString(), fromState.Identity.GoString()) + } + }) + } +} + +// This test validates if a resource identity that is deposed and will be destroyed +// can be refreshed with an identity during the plan. +func TestContext2Plan_resource_identity_refresh_destroy_deposed(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "refresh-basic") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "foo": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "aws_instance": 0, + }, + }) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + + deposedKey := states.DeposedKey("00000001") + root.SetResourceInstanceDeposed( + mustResourceInstanceAddr("aws_instance.web").Resource, + deposedKey, + &states.ResourceInstanceObjectSrc{ // no identity recorded + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo","foo":"bar"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + ty := schema.Body.ImpliedType() + readState, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"id": "foo", "foo": "baz"}, ty) + if err != nil { + t.Fatal(err) + } + + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: readState, + Identity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + } + + s, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.RefreshOnlyMode}) + + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + if !p.ReadResourceCalled { + t.Fatal("ReadResource should be called") + } + + mod := s.PriorState.RootModule() + fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Deposed[deposedKey].Decode(schema) + if err != nil { + t.Fatal(err) + } + + newState, err := schema.Body.CoerceValue(fromState.Value) + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(readState, newState, valueComparer) { + t.Fatal(cmp.Diff(readState, newState, valueComparer, equateEmpty)) + } + expectedIdentity := cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }) + if expectedIdentity.Equals(fromState.Identity).False() { + t.Fatalf("wrong identity\nwant: %s\ngot: %s", expectedIdentity.GoString(), fromState.Identity.GoString()) + } + +} + +func TestContext2Plan_resource_identity_plan(t *testing.T) { + for name, tc := range map[string]struct { + mode plans.Mode + prevRunState *states.State + requiresReplace []cty.Path + identitySchemaVersion int64 + + readResourceIdentity cty.Value + upgradedIdentity cty.Value + + plannedIdentity cty.Value + expectedIdentity cty.Value + expectedPriorIdentity cty.Value + + expectDiagnostics tfdiags.Diagnostics + }{ + "create": { + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + "create - invalid planned identity schema": { + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.BoolVal(false), + }), + expectDiagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "Provider produced an identity that doesn't match the schema", "Provider \"registry.terraform.io/hashicorp/test\" returned an identity for test_resource.test that doesn't match the identity schema: .id: string required, but received bool. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + }, + "create - null planned identity schema": { + // We allow null values in identities + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + }), + }, + "update": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"bar"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + "delete": { + mode: plans.DestroyMode, + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"bar"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.NilVal, + expectedIdentity: cty.NullVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + }, + "replace": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + requiresReplace: []cty.Path{cty.GetAttrPath("id")}, + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + + "update - changing identity": { + // We don't throw an error here, because there are resource types with mutable identities + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"bar"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + + "update - updating identity schema version": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + upgradedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + identitySchemaVersion: 1, + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + + "update - downgrading identity schema version": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 2, + IdentityJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("arn:foo"), + }), + identitySchemaVersion: 1, + expectDiagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "Resource instance managed by newer provider version", "The current state of test_resource.test was created by a newer provider version than is currently selected. Upgrade the test provider to work with this state."), + }, + }, + + "read and update": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + readResourceIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + plannedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + + expectedPriorIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + expectedIdentity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("bar"), + }), + }, + "create with unknown identity": { + plannedIdentity: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + expectedIdentity: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + }, + "update with unknown identity": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"bar"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"bar"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + plannedIdentity: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + + expectDiagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless(tfdiags.Error, "Provider produced invalid identity", "Provider \"registry.terraform.io/hashicorp/test\" returned an identity with unknown values for test_resource.test. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker."), + }, + }, + "replace with unknown identity": { + prevRunState: states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "test", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + IdentitySchemaVersion: 0, + IdentityJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }), + requiresReplace: []cty.Path{cty.GetAttrPath("id")}, + plannedIdentity: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + + expectedIdentity: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + }, + } { + t.Run(name, func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_resource" "test" { + id = "newValue" + } + `, + }) + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "test_resource": uint64(tc.identitySchemaVersion), + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { + identity := req.CurrentIdentity + if !tc.readResourceIdentity.IsNull() { + identity = tc.readResourceIdentity + } + + return providers.ReadResourceResponse{ + NewState: req.PriorState, + Identity: identity, + } + } + var plannedPriorIdentity cty.Value + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + plannedPriorIdentity = req.PriorIdentity + return providers.PlanResourceChangeResponse{ + PlannedState: req.ProposedNewState, + PlannedIdentity: tc.plannedIdentity, + RequiresReplace: tc.requiresReplace, + } + } + + p.UpgradeResourceIdentityFn = func(req providers.UpgradeResourceIdentityRequest) providers.UpgradeResourceIdentityResponse { + + return providers.UpgradeResourceIdentityResponse{ + UpgradedIdentity: tc.upgradedIdentity, + } + } + + plan, diags := ctx.Plan(m, tc.prevRunState, &PlanOpts{Mode: plans.NormalMode}) + + if tc.expectDiagnostics != nil { + tfdiags.AssertDiagnosticsMatch(t, diags, tc.expectDiagnostics) + } else { + tfdiags.AssertNoDiagnostics(t, diags) + + if !tc.expectedPriorIdentity.IsNull() { + if !p.PlanResourceChangeCalled { + t.Fatal("PlanResourceChangeFn was not called") + } + + if !plannedPriorIdentity.RawEquals(tc.expectedPriorIdentity) { + t.Fatalf("wrong prior identity\nwant: %s\ngot: %s", tc.expectedPriorIdentity.GoString(), plannedPriorIdentity.GoString()) + } + } + + schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"] + + change, err := plan.Changes.Resources[0].Decode(schema) + + if err != nil { + t.Fatal(err) + } + + if !tc.expectedIdentity.RawEquals(change.AfterIdentity) { + t.Fatalf("wrong identity\nwant: %s\ngot: %s", tc.expectedIdentity.GoString(), change.AfterIdentity.GoString()) + } + } + }) + } +} diff --git a/internal/terraform/context_plan_import_test.go b/internal/terraform/context_plan_import_test.go new file mode 100644 index 0000000000..e71a087f05 --- /dev/null +++ b/internal/terraform/context_plan_import_test.go @@ -0,0 +1,2282 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "errors" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +func TestContext2Plan_importResourceBasic(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + hook := new(MockHook) + ctx := testContext2(t, &ContextOpts{ + Hooks: []Hook{hook}, + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing.ID != "123" { + t.Errorf("expected import change from \"123\", got non-import change") + } + + if !hook.PrePlanImportCalled { + t.Fatalf("PostPlanImport hook not called") + } + if addr, wantAddr := hook.PrePlanImportAddr, instPlan.Addr; !addr.Equal(wantAddr) { + t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) + } + + if !hook.PostPlanImportCalled { + t.Fatalf("PostPlanImport hook not called") + } + if addr, wantAddr := hook.PostPlanImportAddr, instPlan.Addr; !addr.Equal(wantAddr) { + t.Errorf("expected addr to be %s, but was %s", wantAddr, addr) + } + }) +} + +func TestContext2Plan_importResourceAlreadyInState(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing != nil { + t.Errorf("expected non-import change, got import change %#v", instPlan.Importing) + } + }) +} + +func TestContext2Plan_importResourceUpdate(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "bar" +} + +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.Update; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing.ID != "123" { + t.Errorf("expected import change from \"123\", got non-import change") + } + }) +} + +func TestContext2Plan_importResourceReplace(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "bar" +} + +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addr, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.DeleteThenCreate; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing.ID != "123" { + t.Errorf("expected import change from \"123\", got non-import change") + } + }) +} + +func TestContext2Plan_importRefreshOnce(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "bar" +} + +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + readCalled := 0 + p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { + readCalled++ + state, _ := simpleTestSchema().CoerceValue(cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + })) + + return providers.ReadResourceResponse{ + NewState: state, + } + } + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + ForceReplace: []addrs.AbsResourceInstance{ + addr, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + if readCalled > 1 { + t.Error("ReadResource called multiple times for import") + } +} + +func TestContext2Plan_importTargetWithKeyDoesNotExist(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + count = 1 + test_string = "bar" +} + +import { + to = test_object.a[42] + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + // while the counts are static, the indexes are not fully evaluated during validation + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatalf("expected error but got none") + } +} + +func TestContext2Plan_importIdVariable(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-id-variable") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_instance", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: InputValues{ + "the_id": &InputValue{ + // let var take its default value + Value: cty.NilVal, + }, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } +} + +func TestContext2Plan_importIdFunc(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-id-func") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_instance", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } +} + +func TestContext2Plan_importIdDataSource(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-id-data-source") + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_subnet": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + DataSources: map[string]*configschema.Block{ + "aws_subnet": { + Attributes: map[string]*configschema.Attribute{ + "vpc_id": { + Type: cty.String, + Required: true, + }, + "cidr_block": { + Type: cty.String, + Computed: true, + }, + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }) + p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "vpc_id": cty.StringVal("abc"), + "cidr_block": cty.StringVal("10.0.1.0/24"), + "id": cty.StringVal("123"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_subnet", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } +} + +func TestContext2Plan_importIdModule(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-id-module") + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }) + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_lb", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } +} + +func TestContext2Plan_importIdInvalidNull(t *testing.T) { + p := testProvider("test") + m := testModule(t, "import-id-invalid-null") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // input variables are not evaluated during validation + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: InputValues{ + "the_id": &InputValue{ + Value: cty.NullVal(cty.String), + }, + }, + }) + if !diags.HasErrors() { + t.Fatal("succeeded; want errors") + } + if got, want := diags.Err().Error(), "The import ID cannot be null"; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} + +func TestContext2Plan_importIdInvalidEmptyString(t *testing.T) { + p := testProvider("test") + m := testModule(t, "import-id-invalid-null") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: InputValues{ + "the_id": &InputValue{ + Value: cty.StringVal(""), + }, + }, + }) + if !diags.HasErrors() { + t.Fatal("succeeded; want errors") + } + if got, want := diags.Err().Error(), "The import ID value evaluates to an empty string, please provide a non-empty value."; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} + +func TestContext2Plan_importIdInvalidUnknown(t *testing.T) { + p := testProvider("test") + m := testModule(t, "import-id-invalid-unknown") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + }) + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse { + return providers.PlanResourceChangeResponse{ + PlannedState: cty.UnknownVal(cty.Object(map[string]cty.Type{ + "id": cty.String, + })), + } + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_resource", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatal("succeeded; want errors") + } + if got, want := diags.Err().Error(), `The import block "id" argument depends on resource attributes that cannot be determined until apply`; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} + +func TestContext2Plan_generateConfigWithNestedId(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = "foo" +} +`, + }) + + p := simpleMockProvider() + + p.GetProviderSchemaResponse.ResourceTypes = map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "test_id": { + Type: cty.String, + Required: true, + }, + "list_val": { + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_id": cty.StringVal("foo"), + "list_val": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("list_id"), + }), + }), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_id": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + // Actual plan doesn't matter, just want to make sure there are no errors. + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty. + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } +} + +func TestContext2Plan_importIntoModuleWithGeneratedConfig(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = "123" +} + +import { + to = module.mod.test_object.a + id = "456" +} + +module "mod" { + source = "./mod" +} +`, + "./mod/main.tf": ` +resource "test_object" "a" { + test_string = "bar" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty. + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + one := mustResourceInstanceAddr("test_object.a") + two := mustResourceInstanceAddr("module.mod.test_object.a") + + onePlan := plan.Changes.ResourceInstance(one) + twoPlan := plan.Changes.ResourceInstance(two) + + // This test is just to make sure things work e2e with modules and generated + // config, so we're not too careful about the actual responses - we're just + // happy nothing panicked. See the other import tests for actual validation + // of responses and the like. + if twoPlan.Action != plans.Update { + t.Errorf("expected nested item to be updated but was %s", twoPlan.Action) + } + + if len(onePlan.GeneratedConfig) == 0 { + t.Errorf("expected root item to generate config but it didn't") + } +} + +func TestContext2Plan_importResourceConfigGen(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty. + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing.ID != "123" { + t.Errorf("expected import change from \"123\", got non-import change") + } + + want := `resource "test_object" "a" { + test_bool = null + test_list = null + test_map = null + test_number = null + test_string = "foo" +}` + got := instPlan.GeneratedConfig + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } + }) +} + +func TestContext2Plan_importResourceConfigGenWithAlias(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +provider "test" { + alias = "backup" +} + +import { + provider = test.backup + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty. + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing.ID != "123" { + t.Errorf("expected import change from \"123\", got non-import change") + } + + want := `resource "test_object" "a" { + provider = test.backup + test_bool = null + test_list = null + test_map = null + test_number = null + test_string = "foo" +}` + got := instPlan.GeneratedConfig + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } + }) +} + +func TestContext2Plan_importResourceConfigGenExpandedResource(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a[0] + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if !diags.HasErrors() { + t.Fatalf("expected plan to error, but it did not") + } +} + +// config generation still succeeds even when planning fails +func TestContext2Plan_importResourceConfigGenWithError(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = "123" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.PlanResourceChangeResponse = &providers.PlanResourceChangeResponse{ + PlannedState: cty.NullVal(cty.DynamicPseudoType), + Diagnostics: tfdiags.Diagnostics(nil).Append(errors.New("plan failed")), + } + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", // Actual value here doesn't matter, as long as it is not empty. + }) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + want := `resource "test_object" "a" { + test_bool = null + test_list = null + test_map = null + test_number = null + test_string = "foo" +}` + got := instPlan.GeneratedConfig + if diff := cmp.Diff(want, got); len(diff) > 0 { + t.Errorf("got:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff) + } +} + +func TestContext2Plan_importForEach(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +locals { + things = { + first = "first_id" + second = "second_id" + } +} + +resource "test_object" "a" { + for_each = local.things + test_string = "foo" +} + +import { + for_each = local.things + to = test_object.a[each.key] + id = each.value +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + firstAddr := mustResourceInstanceAddr(`test_object.a["first"]`) + secondAddr := mustResourceInstanceAddr(`test_object.a["second"]`) + + for _, instPlan := range plan.Changes.Resources { + switch { + case instPlan.Addr.Equal(firstAddr): + if instPlan.Importing.ID != "first_id" { + t.Errorf("expected import ID of \"first_id\", got %q", instPlan.Importing.ID) + } + case instPlan.Addr.Equal(secondAddr): + if instPlan.Importing.ID != "second_id" { + t.Errorf("expected import ID of \"second_id\", got %q", instPlan.Importing.ID) + } + default: + t.Errorf("unexpected change for %s", instPlan.Addr) + } + + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + } +} + +func TestContext2Plan_importForEachmodule(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +locals { + things = { + brown = "brown_id" + blue = "blue_id" + } +} + +module "sub" { + for_each = local.things + source = "./sub" + things = local.things +} + +import { + for_each = [ + { + mod = "brown" + res = "brown" + id = "brown_brown_id" + }, + { + mod = "brown" + res = "blue" + id = "brown_blue_id" + }, + { + mod = "blue" + res = "brown" + id = "blue_brown_id" + }, + { + mod = "blue" + res = "blue" + id = "blue_blue_id" + }, + ] + to = module.sub[each.value.mod].test_object.a[each.value.res] + id = each.value.id +} +`, + + "./sub/main.tf": ` +variable things { + type = map(string) +} + +locals { + static_id = "foo" +} + +resource "test_object" "a" { + for_each = var.things + test_string = local.static_id +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + brownBlueAddr := mustResourceInstanceAddr(`module.sub["brown"].test_object.a["brown"]`) + brownBrownAddr := mustResourceInstanceAddr(`module.sub["brown"].test_object.a["blue"]`) + blueBlueAddr := mustResourceInstanceAddr(`module.sub["blue"].test_object.a["brown"]`) + blueBrownAddr := mustResourceInstanceAddr(`module.sub["blue"].test_object.a["blue"]`) + + for _, instPlan := range plan.Changes.Resources { + switch { + case instPlan.Addr.Equal(brownBlueAddr): + if instPlan.Importing.ID != "brown_brown_id" { + t.Errorf("expected import ID of \"brown_brown_id\", got %q", instPlan.Importing.ID) + } + case instPlan.Addr.Equal(brownBrownAddr): + if instPlan.Importing.ID != "brown_blue_id" { + t.Errorf("expected import ID of \"brown_blue_id\", got %q", instPlan.Importing.ID) + } + case instPlan.Addr.Equal(blueBlueAddr): + if instPlan.Importing.ID != "blue_brown_id" { + t.Errorf("expected import ID of \"blue_brown_id\", got %q", instPlan.Importing.ID) + } + case instPlan.Addr.Equal(blueBrownAddr): + if instPlan.Importing.ID != "blue_blue_id" { + t.Errorf("expected import ID of \"blue_blue_id\", got %q", instPlan.Importing.ID) + } + default: + t.Errorf("unexpected change for %s", instPlan.Addr) + } + + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + } +} + +func TestContext2Plan_importForEachPartial(t *testing.T) { + // one of the imported instances already exists in the state, which should + // result in a non-import, NoOp change + m := testModuleInline(t, map[string]string{ + "main.tf": ` +locals { + things = { + first = "first_id" + second = "second_id" + } +} + +resource "test_object" "a" { + for_each = local.things + test_string = "foo" +} + +import { + for_each = local.things + to = test_object.a[each.key] + id = each.value +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr(`test_object.a["first"]`).Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + firstAddr := mustResourceInstanceAddr(`test_object.a["first"]`) + secondAddr := mustResourceInstanceAddr(`test_object.a["second"]`) + + for _, instPlan := range plan.Changes.Resources { + switch { + case instPlan.Addr.Equal(firstAddr): + if instPlan.Importing != nil { + t.Errorf("expected no import for %s, got %#v", firstAddr, instPlan.Importing) + } + case instPlan.Addr.Equal(secondAddr): + if instPlan.Importing.ID != "second_id" { + t.Errorf("expected import ID of \"second_id\", got %q", instPlan.Importing.ID) + } + default: + t.Errorf("unexpected change for %s", instPlan.Addr) + } + + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + } +} + +func TestContext2Plan_importForEachFromData(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +data "test_object" "d" { +} + +resource "test_object" "a" { + count = 2 + test_string = "foo" +} + +import { + for_each = data.test_object.d.objects + to = test_object.a[each.key] + id = each.value +} +`, + }) + + p := simpleMockProvider() + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + ResourceTypes: map[string]providers.Schema{ + "test_object": providers.Schema{Body: simpleTestSchema()}, + }, + DataSources: map[string]providers.Schema{ + "test_object": providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "objects": { + Type: cty.List(cty.String), + Computed: true, + }, + }, + }, + }, + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "objects": cty.ListVal([]cty.Value{ + cty.StringVal("first_id"), cty.StringVal("second_id"), + }), + }), + } + + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + }, + }, + } + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + firstAddr := mustResourceInstanceAddr(`test_object.a[0]`) + secondAddr := mustResourceInstanceAddr(`test_object.a[1]`) + + for _, instPlan := range plan.Changes.Resources { + switch { + case instPlan.Addr.Equal(firstAddr): + if instPlan.Importing.ID != "first_id" { + t.Errorf("expected import ID of \"first_id\", got %q", instPlan.Importing.ID) + } + case instPlan.Addr.Equal(secondAddr): + if instPlan.Importing.ID != "second_id" { + t.Errorf("expected import ID of \"second_id\", got %q", instPlan.Importing.ID) + } + default: + t.Errorf("unexpected change for %s", instPlan.Addr) + } + + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + } +} + +func TestContext2Plan_importGenerateNone(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + for_each = [] + to = test_object.a + id = "81ba7c97" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if !diags.HasErrors() { + t.Fatal("expected errors, got none") + } +} + +// This is a test for the issue raised in #34992 +func TestContext2Plan_importWithSensitives(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = "123" +} +`, + }) + + p := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "sensitive_string": { + Type: cty.String, + Sensitive: true, + Optional: true, + }, + "sensitive_list": { + Type: cty.List(cty.String), + Sensitive: true, + Optional: true, + }, + }, + }, + }, + }, + }, + ImportResourceStateFn: func(request providers.ImportResourceStateRequest) providers.ImportResourceStateResponse { + return providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "sensitive_string": cty.StringVal("sensitive"), + "sensitive_list": cty.ListVal([]cty.Value{cty.StringVal("hello"), cty.StringVal("world")}), + }), + }, + }, + } + }, + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + // Just don't crash! + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + Mode: plans.NormalMode, + GenerateConfigPath: "generated.tf", + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } +} + +func TestContext2Plan_importDuringDestroy(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_object" "a" { + test_string = "foo" + } + + import { + to = test_object.a + id = "missing" + } + + resource "test_object" "b" { + test_string = "foo" + } + `, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.ReadResourceFn = func(req providers.ReadResourceRequest) (resp providers.ReadResourceResponse) { + // this resource has already been deleted, so return nothing during refresh + if req.PriorState.GetAttr("test_string").AsString() == "missing" { + resp.NewState = cty.NullVal(req.PriorState.Type()) + return resp + } + + resp.NewState = req.PriorState + return resp + } + + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("missing"), + }), + }, + }, + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.b").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + _, diags := ctx.Plan(m, state, &PlanOpts{ + Mode: plans.DestroyMode, + }) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } +} + +func TestContext2Plan_importSelfReference(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = test_object.a.test_string +} + +resource "test_object" "a" { + test_string = "foo" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + // The providers never actually going to get called here, we should + // catch the error long before anything happens. + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + // We're expecting exactly one diag, which is the self-reference error. + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + got, want := diags.Err().Error(), "Invalid import id argument: The import ID cannot reference the resource being imported." + if cmp.Diff(want, got) != "" { + t.Fatalf("unexpected error\n%s", cmp.Diff(want, got)) + } +} + +func TestContext2Plan_importSelfReferenceInstanceRef(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a + id = test_object.a[0].test_string +} + +resource "test_object" "a" { + count = 1 + test_string = "foo" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + // The providers never actually going to get called here, we should + // catch the error long before anything happens. + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + // We're expecting exactly one diag, which is the self-reference error. + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + got, want := diags.Err().Error(), "Invalid import id argument: The import ID cannot reference the resource being imported." + if cmp.Diff(want, got) != "" { + t.Fatalf("unexpected error\n%s", cmp.Diff(want, got)) + } +} + +func TestContext2Plan_importSelfReferenceInst(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = test_object.a[0] + id = test_object.a.test_string +} + +resource "test_object" "a" { + count = 1 + test_string = "foo" +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + // The providers never actually going to get called here, we should + // catch the error long before anything happens. + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + // We're expecting exactly one diag, which is the self-reference error. + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + got, want := diags.Err().Error(), "Invalid import id argument: The import ID cannot reference the resource being imported." + if cmp.Diff(want, got) != "" { + t.Fatalf("unexpected error\n%s", cmp.Diff(want, got)) + } +} + +func TestContext2Plan_importSelfReferenceInModule(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +import { + to = module.mod.test_object.a + id = module.mod.foo +} + +module "mod" { + source = "./mod" +} +`, + "mod/main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +output "foo" { + value = test_object.a.test_string +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + // The providers never actually going to get called here, we should + // catch the error long before anything happens. + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + // We're expecting exactly one diag, which is the self-reference error. + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + // Terraform detects this case as a cycle, and the order of rendering the + // cycle if non-deterministic, so we can't do a straight string match. + + got := diags.Err().Error() + if !strings.Contains(got, "Cycle:") { + t.Errorf("should have reported a cycle error, but got %s", got) + } + if !strings.Contains(got, "module.mod.output.foo") { + t.Errorf("should have reported the cycle to contain the module output, but got %s", got) + } + if !strings.Contains(got, "module.mod.test_object.a") { + t.Errorf("should have reported the cycle to contain the target resource, but got %s", got) + } +} + +// https://github.com/hashicorp/terraform/issues/36672 +func TestContext2Plan_importSelfReferenceInForEach(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + resource "test_resource" "a" { + count = 2 + } + + import { + # the block references the same resource it is importing into + for_each = { for _, v in test_resource.a : v => v } + to = test_resource.a[each.key] + id = concat("importable-", each.key) + } +`, + }) + + p := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + diags := ctx.Validate(m, &ValidateOpts{}) + + // We're expecting exactly one diag, which is the self-reference error. + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + got, want := diags.Err().Error(), "Invalid for_each argument: The for_each expression cannot reference the resource being imported." + if cmp.Diff(want, got) != "" { + t.Fatalf("unexpected error\n%s", cmp.Diff(want, got)) + } +} + +func TestContext2Plan_importIdentityModule(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-identity-module") + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }) + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_lb", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + Identity: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("bar"), + }), + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } +} + +func TestContext2Plan_importIdentityMissingRequired(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-identity-module") + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + + if len(diags) != 1 { + t.Fatalf("expected one diag, got %d: %s", len(diags), diags.ErrWithWarnings()) + } + + got := diags.Err().Error() + if !strings.Contains(got, "Invalid expression value:") { + t.Errorf("should have reported an invalid expression value, but got %s", got) + } +} + +func TestContext2Plan_importIdentityResourceAlreadyInState(t *testing.T) { + addr := mustResourceInstanceAddr("test_object.a") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_object" "a" { + test_string = "foo" +} + +import { + to = test_object.a + identity = { + id = "123" + } +} +`, + }) + + p := simpleMockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: simpleTestSchema()}, + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: simpleTestSchema(), + Identity: &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }, + } + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + Identity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("123"), + }), + } + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "test_object", + State: cty.ObjectVal(map[string]cty.Value{ + "test_string": cty.StringVal("foo"), + }), + Identity: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("123"), + }), + }, + }, + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"test_string":"foo"}`), + IdentityJSON: []byte(`{"id":"123"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + t.Run(addr.String(), func(t *testing.T) { + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + if got, want := instPlan.Addr, addr; !got.Equal(want) { + t.Errorf("wrong current address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.PrevRunAddr, addr; !got.Equal(want) { + t.Errorf("wrong previous run address\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.Action, plans.NoOp; got != want { + t.Errorf("wrong planned action\ngot: %s\nwant: %s", got, want) + } + if got, want := instPlan.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { + t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) + } + if instPlan.Importing != nil { + t.Errorf("expected non-import change, got import change %#v", instPlan.Importing) + } + }) +} + +func TestContext2Plan_importIdentityModuleWithOptional(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-identity-module") + + identitySchema := &configschema.Object{ + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + "something": { + Type: cty.Number, + Optional: true, + }, + }, + Nesting: configschema.NestingSingle, + } + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_lb": identitySchema, + }, + }) + wantIdentity := cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("bar"), + "something": cty.NumberIntVal(42), + }) + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_lb", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + Identity: wantIdentity, + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + t.Fatalf("unexpected errors: %s", diags.Err()) + } + + addr := mustResourceInstanceAddr("aws_lb.foo") + instPlan := plan.Changes.ResourceInstance(addr) + if instPlan == nil { + t.Fatalf("no plan for %s at all", addr) + } + + identity, err := instPlan.Importing.Identity.Decode(identitySchema.ImpliedType()) + if err != nil { + t.Fatalf("failed to decode identity: %s", err) + } + identityMatches := identity.Equals(wantIdentity) + if !identityMatches.True() { + t.Errorf("identity does not match\ngot: %s\nwant: %s", + tfdiags.ObjectToString(identity), + tfdiags.ObjectToString(wantIdentity)) + } +} + +func TestContext2Plan_importIdentityMissingResponse(t *testing.T) { + p := testProvider("aws") + m := testModule(t, "import-identity-module") + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "aws_lb": { + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + }) + p.ImportResourceStateResponse = &providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: "aws_lb", + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("foo"), + }), + // No identity returned + }, + }, + } + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, &ValidateOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected errors\n%s", diags.Err().Error()) + } + + _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if !diags.HasErrors() { + t.Fatal("succeeded; want errors") + } + if got, want := diags.Err().Error(), `import of aws_lb.foo didn't return an identity`; !strings.Contains(got, want) { + t.Fatalf("wrong error:\ngot: %s\nwant: message containing %q", got, want) + } +} diff --git a/internal/terraform/context_plan_test.go b/internal/terraform/context_plan_test.go index a04a7c05ce..d5e446771d 100644 --- a/internal/terraform/context_plan_test.go +++ b/internal/terraform/context_plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -22,6 +25,7 @@ import ( "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -40,15 +44,15 @@ func TestContext2Plan_basic(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanCompleteAndApplyable(t, plan) if l := len(plan.Changes.Resources); l < 2 { t.Fatalf("wrong number of resources %d; want fewer than two\n%s", l, spew.Sdump(plan.Changes.Resources)) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] for _, r := range plan.Changes.Resources { - ric, err := r.Decode(ty) + ric, err := r.Decode(schema) if err != nil { t.Fatal(err) } @@ -110,6 +114,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanCompleteAndApplyable(t, plan) // the state should still show one deposed expectedState := strings.TrimSpace(` @@ -123,8 +128,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { t.Fatalf("\nexpected: %q\ngot: %q\n", expectedState, plan.PriorState.String()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] type InstanceGen struct { Addr string @@ -155,7 +159,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { } { - ric, err := changes[InstanceGen{Addr: "aws_instance.foo"}].Decode(ty) + ric, err := changes[InstanceGen{Addr: "aws_instance.foo"}].Decode(schema) if err != nil { t.Fatal(err) } @@ -165,7 +169,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { } // the existing instance should only have an unchanged id - expected, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ + expected, err := schema.Body.CoerceValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("baz"), "type": cty.StringVal("aws_instance"), })) @@ -177,7 +181,7 @@ func TestContext2Plan_createBefore_deposed(t *testing.T) { } { - ric, err := changes[InstanceGen{Addr: "aws_instance.foo", DeposedKey: states.DeposedKey("00000001")}].Decode(ty) + ric, err := changes[InstanceGen{Addr: "aws_instance.foo", DeposedKey: states.DeposedKey("00000001")}].Decode(schema) if err != nil { t.Fatal(err) } @@ -284,15 +288,14 @@ func TestContext2Plan_escapedVar(t *testing.T) { t.Fatalf("expected resource creation, got %s", res.Action) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } - expected := objectVal(t, schema, map[string]cty.Value{ + expected := objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar-${baz}"), "type": cty.UnknownVal(cty.String), @@ -357,16 +360,15 @@ func TestContext2Plan_modules(t *testing.T) { t.Error("expected 3 resource in plan, got", len(plan.Changes.Resources)) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] - expectFoo := objectVal(t, schema, map[string]cty.Value{ + expectFoo := objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), "type": cty.UnknownVal(cty.String), }) - expectNum := objectVal(t, schema, map[string]cty.Value{ + expectNum := objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -376,7 +378,7 @@ func TestContext2Plan_modules(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -411,8 +413,7 @@ func TestContext2Plan_moduleExpand(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] expected := map[string]struct{}{ `aws_instance.foo["a"]`: {}, @@ -428,7 +429,7 @@ func TestContext2Plan_moduleExpand(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -448,7 +449,7 @@ func TestContext2Plan_moduleExpand(t *testing.T) { func TestContext2Plan_moduleCycle(t *testing.T) { m := testModule(t, "plan-module-cycle") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -471,8 +472,7 @@ func TestContext2Plan_moduleCycle(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -482,7 +482,7 @@ func TestContext2Plan_moduleCycle(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -490,12 +490,12 @@ func TestContext2Plan_moduleCycle(t *testing.T) { var expected cty.Value switch i := ric.Addr.String(); i { case "aws_instance.b": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }) case "aws_instance.c": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "some_input": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), @@ -525,19 +525,18 @@ func TestContext2Plan_moduleDeadlock(t *testing.T) { t.Fatalf("err: %s", err) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] for _, res := range plan.Changes.Resources { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } - expected := objectVal(t, schema, map[string]cty.Value{ + expected := objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }) @@ -569,8 +568,7 @@ func TestContext2Plan_moduleInput(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -580,7 +578,7 @@ func TestContext2Plan_moduleInput(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -589,13 +587,13 @@ func TestContext2Plan_moduleInput(t *testing.T) { switch i := ric.Addr.String(); i { case "aws_instance.bar": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), "type": cty.UnknownVal(cty.String), }) case "module.child.aws_instance.foo": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("42"), "type": cty.UnknownVal(cty.String), @@ -623,8 +621,7 @@ func TestContext2Plan_moduleInputComputed(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -634,21 +631,21 @@ func TestContext2Plan_moduleInputComputed(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), "compute": cty.StringVal("foo"), }), ric.After) case "module.child.aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), @@ -682,8 +679,7 @@ func TestContext2Plan_moduleInputFromVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -693,20 +689,20 @@ func TestContext2Plan_moduleInputFromVar(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.child.aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("52"), "type": cty.UnknownVal(cty.String), @@ -720,7 +716,7 @@ func TestContext2Plan_moduleInputFromVar(t *testing.T) { func TestContext2Plan_moduleMultiVar(t *testing.T) { m := testModule(t, "plan-module-multi-var") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -743,8 +739,7 @@ func TestContext2Plan_moduleMultiVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 5 { t.Fatal("expected 5 changes, got", len(plan.Changes.Resources)) @@ -755,32 +750,32 @@ func TestContext2Plan_moduleMultiVar(t *testing.T) { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.parent[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.parent[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), }), ric.After) case "module.child.aws_instance.bar[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "baz": cty.StringVal("baz"), }), ric.After) case "module.child.aws_instance.bar[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "baz": cty.StringVal("baz"), }), ric.After) case "module.child.aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("baz,baz"), }), ric.After) @@ -816,8 +811,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -825,7 +819,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -835,7 +829,7 @@ func TestContext2Plan_moduleOrphans(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -910,8 +904,7 @@ func TestContext2Plan_moduleOrphansWithProvisioner(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 3 { t.Error("expected 3 planned resources, got", len(plan.Changes.Resources)) @@ -919,7 +912,7 @@ func TestContext2Plan_moduleOrphansWithProvisioner(t *testing.T) { for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -975,7 +968,7 @@ func TestContext2Plan_moduleProviderInherit(t *testing.T) { defer l.Unlock() p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "from": {Type: cty.String, Optional: true}, @@ -1038,7 +1031,7 @@ func TestContext2Plan_moduleProviderInheritDeep(t *testing.T) { var from string p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "from": {Type: cty.String, Optional: true}, @@ -1092,7 +1085,7 @@ func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) { defer l.Unlock() p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "to": {Type: cty.String, Optional: true}, @@ -1155,7 +1148,7 @@ func TestContext2Plan_moduleProviderDefaultsVar(t *testing.T) { func TestContext2Plan_moduleProviderVar(t *testing.T) { m := testModule(t, "plan-module-provider-var") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, @@ -1181,8 +1174,7 @@ func TestContext2Plan_moduleProviderVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) @@ -1192,14 +1184,14 @@ func TestContext2Plan_moduleProviderVar(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "module.child.aws_instance.test": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "value": cty.StringVal("hello"), }), ric.After) default: @@ -1223,8 +1215,7 @@ func TestContext2Plan_moduleVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -1234,7 +1225,7 @@ func TestContext2Plan_moduleVar(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -1243,13 +1234,13 @@ func TestContext2Plan_moduleVar(t *testing.T) { switch i := ric.Addr.String(); i { case "aws_instance.bar": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), "type": cty.UnknownVal(cty.String), }) case "module.child.aws_instance.foo": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -1271,10 +1262,11 @@ func TestContext2Plan_moduleVarWrongTypeBasic(t *testing.T) { }, }) - _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) if !diags.HasErrors() { t.Fatalf("succeeded; want errors") } + checkPlanErrored(t, plan) } func TestContext2Plan_moduleVarWrongTypeNested(t *testing.T) { @@ -1286,10 +1278,11 @@ func TestContext2Plan_moduleVarWrongTypeNested(t *testing.T) { }, }) - _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) if !diags.HasErrors() { t.Fatalf("succeeded; want errors") } + checkPlanErrored(t, plan) } func TestContext2Plan_moduleVarWithDefaultValue(t *testing.T) { @@ -1301,10 +1294,11 @@ func TestContext2Plan_moduleVarWithDefaultValue(t *testing.T) { }, }) - _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanCompleteAndApplyable(t, plan) } func TestContext2Plan_moduleVarComputed(t *testing.T) { @@ -1321,8 +1315,7 @@ func TestContext2Plan_moduleVarComputed(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -1332,20 +1325,20 @@ func TestContext2Plan_moduleVarComputed(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.child.aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), @@ -1379,14 +1372,39 @@ func TestContext2Plan_preventDestroy_bad(t *testing.T) { }) plan, err := ctx.Plan(m, state, DefaultPlanOpts) + checkPlanErrored(t, plan) expectedErr := "aws_instance.foo has lifecycle.prevent_destroy" if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { if plan != nil { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } t.Fatalf("expected err would contain %q\nerr: %s", expectedErr, err) } + + // The plan should still include the proposal to replace the object + // that cannot be destroyed, since the change is valid in isolation, + // and this then allows Terraform CLI and HCP Terraform to still + // show the problematic change that caused the error as additional + // context. + changes := plan.Changes + changeSrc := changes.ResourceInstance(mustResourceInstanceAddr("aws_instance.foo")) + if changeSrc != nil { + if got, want := changeSrc.Action, plans.DeleteThenCreate; got != want { + t.Errorf("wrong proposed change action\ngot: %s\nwant: %s", got, want) + } + gotReqRep := changeSrc.RequiredReplace + if !gotReqRep.Has(cty.GetAttrPath("require_new")) { + t.Errorf("plan does not indicate that the require_new change forced replacement") + } + } else { + t.Errorf("no planned change for aws_instance.foo") + } + // The plan must also be marked as errored, so that Terraform will reject + // any attempts to apply the plan with the forbidden replace action. + if got, want := plan.Errored, true; got != want { + t.Errorf("plan is not marked as errored\ngot: %#v\nwant: %#v", got, want) + } } func TestContext2Plan_preventDestroy_good(t *testing.T) { @@ -1415,10 +1433,14 @@ func TestContext2Plan_preventDestroy_good(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanComplete(t, plan) if !plan.Changes.Empty() { t.Fatalf("expected no changes, got %#v\n", plan.Changes) } + if plan.Applyable { + t.Errorf("plan is applyable; should not be (because it has no changes)") + } } func TestContext2Plan_preventDestroy_countBad(t *testing.T) { @@ -1451,11 +1473,12 @@ func TestContext2Plan_preventDestroy_countBad(t *testing.T) { }) plan, err := ctx.Plan(m, state, DefaultPlanOpts) + checkPlanErrored(t, plan) expectedErr := "aws_instance.foo[1] has lifecycle.prevent_destroy" if !strings.Contains(fmt.Sprintf("%s", err), expectedErr) { if plan != nil { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } t.Fatalf("expected err would contain %q\nerr: %s", expectedErr, err) } @@ -1464,7 +1487,7 @@ func TestContext2Plan_preventDestroy_countBad(t *testing.T) { func TestContext2Plan_preventDestroy_countGood(t *testing.T) { m := testModule(t, "plan-prevent-destroy-count-good") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1504,17 +1527,17 @@ func TestContext2Plan_preventDestroy_countGood(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - if plan.Changes.Empty() { t.Fatalf("Expected non-empty plan, got %s", legacyDiffComparisonString(plan.Changes)) } + checkPlanCompleteAndApplyable(t, plan) } func TestContext2Plan_preventDestroy_countGoodNoChange(t *testing.T) { m := testModule(t, "plan-prevent-destroy-count-good") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1581,7 +1604,7 @@ func TestContext2Plan_preventDestroy_destroyPlan(t *testing.T) { expectedErr := "aws_instance.foo has lifecycle.prevent_destroy" if !strings.Contains(fmt.Sprintf("%s", diags.Err()), expectedErr) { if plan != nil { - t.Logf(legacyDiffComparisonString(plan.Changes)) + t.Log(legacyDiffComparisonString(plan.Changes)) } t.Fatalf("expected diagnostics would contain %q\nactual diags: %s", expectedErr, diags.Err()) } @@ -1621,8 +1644,7 @@ func TestContext2Plan_computed(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -1632,20 +1654,20 @@ func TestContext2Plan_computed(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), @@ -1661,7 +1683,7 @@ func TestContext2Plan_computed(t *testing.T) { func TestContext2Plan_blockNestingGroup(t *testing.T) { m := testModule(t, "plan-block-nesting-group") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test": { BlockTypes: map[string]*configschema.NestedBlock{ @@ -1723,6 +1745,10 @@ func TestContext2Plan_blockNestingGroup(t *testing.T) { "baz": cty.NullVal(cty.String), }), }), + ClientCapabilities: providers.ClientCapabilities{ + DeferralAllowed: false, + WriteOnlyAttributesAllowed: true, + }, } if !cmp.Equal(got, want, valueTrans) { t.Errorf("wrong PlanResourceChange request\n%s", cmp.Diff(got, want, valueTrans)) @@ -1732,7 +1758,7 @@ func TestContext2Plan_blockNestingGroup(t *testing.T) { func TestContext2Plan_computedDataResource(t *testing.T) { m := testModule(t, "plan-computed-data-resource") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1761,8 +1787,7 @@ func TestContext2Plan_computedDataResource(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.DataSources["aws_vpc"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.DataSources["aws_vpc"] if rc := plan.Changes.ResourceInstance(addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "aws_instance", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)); rc == nil { t.Fatalf("missing diff for aws_instance.foo") @@ -1776,7 +1801,7 @@ func TestContext2Plan_computedDataResource(t *testing.T) { t.Fatalf("missing diff for data.aws_vpc.bar") } - rc, err := rcs.Decode(ty) + rc, err := rcs.Decode(schema) if err != nil { t.Fatal(err) } @@ -1795,7 +1820,7 @@ func TestContext2Plan_computedDataResource(t *testing.T) { func TestContext2Plan_computedInFunction(t *testing.T) { m := testModule(t, "plan-computed-in-function") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1825,11 +1850,11 @@ func TestContext2Plan_computedInFunction(t *testing.T) { }, }) - diags := ctx.Validate(m) - assertNoErrors(t, diags) + diags := ctx.Validate(m, nil) + tfdiags.AssertNoErrors(t, diags) _, diags = ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if !p.ReadDataSourceCalled { t.Fatalf("ReadDataSource was not called on provider during plan; should've been called") @@ -1839,7 +1864,7 @@ func TestContext2Plan_computedInFunction(t *testing.T) { func TestContext2Plan_computedDataCountResource(t *testing.T) { m := testModule(t, "plan-computed-data-count") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1915,7 +1940,7 @@ func TestContext2Plan_dataResourceBecomesComputed(t *testing.T) { m := testModule(t, "plan-data-resource-becomes-computed") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1945,8 +1970,7 @@ func TestContext2Plan_dataResourceBecomesComputed(t *testing.T) { } } - schema := p.GetProviderSchemaResponse.DataSources["aws_data_source"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.DataSources["aws_data_source"] p.ReadDataSourceResponse = &providers.ReadDataSourceResponse{ // This should not be called, because the configuration for the @@ -1986,7 +2010,7 @@ func TestContext2Plan_dataResourceBecomesComputed(t *testing.T) { t.Fatalf("missing diff for data.aws_data_resource.foo") } - rc, err := rcs.Decode(ty) + rc, err := rcs.Decode(schema) if err != nil { t.Fatal(err) } @@ -2006,7 +2030,7 @@ func TestContext2Plan_computedList(t *testing.T) { m := testModule(t, "plan-computed-list") p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2030,9 +2054,7 @@ func TestContext2Plan_computedList(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() - + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } @@ -2041,18 +2063,18 @@ func TestContext2Plan_computedList(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "foo": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "list": cty.UnknownVal(cty.List(cty.String)), "num": cty.NumberIntVal(2), "compute": cty.StringVal("list.#"), @@ -2070,7 +2092,7 @@ func TestContext2Plan_computedMultiIndex(t *testing.T) { p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2093,8 +2115,7 @@ func TestContext2Plan_computedMultiIndex(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 3 { t.Fatal("expected 3 changes, got", len(plan.Changes.Resources)) @@ -2104,26 +2125,26 @@ func TestContext2Plan_computedMultiIndex(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "ip": cty.UnknownVal(cty.List(cty.String)), "foo": cty.NullVal(cty.List(cty.String)), "compute": cty.StringVal("ip.#"), }), ric.After) case "aws_instance.foo[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "ip": cty.UnknownVal(cty.List(cty.String)), "foo": cty.NullVal(cty.List(cty.String)), "compute": cty.StringVal("ip.#"), }), ric.After) case "aws_instance.bar[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "foo": cty.UnknownVal(cty.List(cty.String)), }), ric.After) default: @@ -2147,8 +2168,7 @@ func TestContext2Plan_count(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 6 { t.Fatal("expected 6 changes, got", len(plan.Changes.Resources)) @@ -2158,44 +2178,44 @@ func TestContext2Plan_count(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo,foo,foo,foo,foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[2]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[3]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[4]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2255,8 +2275,7 @@ func TestContext2Plan_countModuleStatic(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 3 { t.Fatal("expected 3 changes, got", len(plan.Changes.Resources)) @@ -2266,24 +2285,24 @@ func TestContext2Plan_countModuleStatic(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "module.child.aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.child.aws_instance.foo[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.child.aws_instance.foo[2]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) @@ -2308,8 +2327,7 @@ func TestContext2Plan_countModuleStaticGrandchild(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 3 { t.Fatal("expected 3 changes, got", len(plan.Changes.Resources)) @@ -2319,24 +2337,24 @@ func TestContext2Plan_countModuleStaticGrandchild(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "module.child.module.child.aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.child.module.child.aws_instance.foo[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.child.module.child.aws_instance.foo[2]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) @@ -2361,8 +2379,7 @@ func TestContext2Plan_countIndex(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -2372,20 +2389,20 @@ func TestContext2Plan_countIndex(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("0"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("1"), "type": cty.UnknownVal(cty.String), @@ -2418,8 +2435,7 @@ func TestContext2Plan_countVar(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 4 { t.Fatal("expected 4 changes, got", len(plan.Changes.Resources)) @@ -2429,32 +2445,32 @@ func TestContext2Plan_countVar(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo,foo,foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[2]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2468,7 +2484,7 @@ func TestContext2Plan_countVar(t *testing.T) { func TestContext2Plan_countZero(t *testing.T) { m := testModule(t, "plan-count-zero") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2495,8 +2511,7 @@ func TestContext2Plan_countZero(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) @@ -2507,7 +2522,7 @@ func TestContext2Plan_countZero(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -2536,8 +2551,7 @@ func TestContext2Plan_countOneIndex(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -2547,20 +2561,20 @@ func TestContext2Plan_countOneIndex(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), }), ric.After) case "aws_instance.foo[0]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2613,8 +2627,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 4 { t.Fatal("expected 4 changes, got", len(plan.Changes.Resources)) @@ -2622,7 +2635,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -2632,7 +2645,7 @@ func TestContext2Plan_countDecreaseToOne(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar"), "type": cty.UnknownVal(cty.String), @@ -2697,8 +2710,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 4 { t.Fatal("expected 4 changes, got", len(plan.Changes.Resources)) @@ -2706,7 +2718,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -2716,7 +2728,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar"), "type": cty.UnknownVal(cty.String), @@ -2729,7 +2741,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2738,7 +2750,7 @@ func TestContext2Plan_countIncreaseFromNotSet(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2774,16 +2786,14 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() - + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 4 { t.Fatal("expected 4 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -2793,7 +2803,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar"), "type": cty.UnknownVal(cty.String), @@ -2806,7 +2816,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2815,7 +2825,7 @@ func TestContext2Plan_countIncreaseFromOne(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2865,8 +2875,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 5 { t.Fatal("expected 5 changes, got", len(plan.Changes.Resources)) @@ -2874,7 +2883,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -2884,7 +2893,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar"), "type": cty.UnknownVal(cty.String), @@ -2901,7 +2910,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2910,7 +2919,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource create, got %s", res.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -2932,7 +2941,7 @@ func TestContext2Plan_countIncreaseFromOneCorrupted(t *testing.T) { func TestContext2Plan_countIncreaseWithSplatReference(t *testing.T) { m := testModule(t, "plan-count-splat-reference") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2990,15 +2999,14 @@ func TestContext2Plan_countIncreaseWithSplatReference(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 6 { t.Fatal("expected 6 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3044,8 +3052,7 @@ func TestContext2Plan_forEach(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 8 { t.Fatal("expected 8 changes, got", len(plan.Changes.Resources)) @@ -3055,7 +3062,7 @@ func TestContext2Plan_forEach(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - _, err := res.Decode(ty) + _, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3140,15 +3147,14 @@ func TestContext2Plan_destroy(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3201,15 +3207,14 @@ func TestContext2Plan_moduleDestroy(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3264,15 +3269,14 @@ func TestContext2Plan_moduleDestroyCycle(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3325,15 +3329,14 @@ func TestContext2Plan_moduleDestroyMultivar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3358,7 +3361,7 @@ func TestContext2Plan_pathVar(t *testing.T) { m := testModule(t, "plan-path-var") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -3381,15 +3384,14 @@ func TestContext2Plan_pathVar(t *testing.T) { t.Fatalf("err: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3399,7 +3401,7 @@ func TestContext2Plan_pathVar(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created", i) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "cwd": cty.StringVal(cwd + "/barpath"), "module": cty.StringVal(m.Module.SourceDir + "/foopath"), "root": cty.StringVal(m.Module.SourceDir + "/barpath"), @@ -3436,15 +3438,14 @@ func TestContext2Plan_diffVar(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3454,7 +3455,7 @@ func TestContext2Plan_diffVar(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created", i) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(3), "type": cty.UnknownVal(cty.String), @@ -3463,12 +3464,12 @@ func TestContext2Plan_diffVar(t *testing.T) { if res.Action != plans.Update { t.Fatalf("resource %s should be updated", i) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "num": cty.NumberIntVal(2), "type": cty.StringVal("aws_instance"), }), ric.Before) - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "num": cty.NumberIntVal(3), "type": cty.StringVal("aws_instance"), @@ -3551,15 +3552,14 @@ func TestContext2Plan_orphan(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3579,7 +3579,7 @@ func TestContext2Plan_orphan(t *testing.T) { if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -3635,15 +3635,14 @@ func TestContext2Plan_state(t *testing.T) { if len(plan.Changes.Resources) < 2 { t.Fatalf("bad: %#v", plan.Changes.Resources) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3656,7 +3655,7 @@ func TestContext2Plan_state(t *testing.T) { if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), "type": cty.UnknownVal(cty.String), @@ -3668,12 +3667,12 @@ func TestContext2Plan_state(t *testing.T) { if got, want := ric.ActionReason, plans.ResourceInstanceChangeNoReason; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "num": cty.NullVal(cty.Number), "type": cty.NullVal(cty.String), }), ric.Before) - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -3689,11 +3688,11 @@ func TestContext2Plan_requiresReplace(t *testing.T) { p := testProvider("test") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{}, + Body: &configschema.Block{}, }, ResourceTypes: map[string]providers.Schema{ "test_thing": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "v": { Type: cty.String, @@ -3735,8 +3734,7 @@ func TestContext2Plan_requiresReplace(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["test_thing"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["test_thing"] if got, want := len(plan.Changes.Resources), 1; got != want { t.Fatalf("got %d changes; want %d", got, want) @@ -3744,7 +3742,7 @@ func TestContext2Plan_requiresReplace(t *testing.T) { for _, res := range plan.Changes.Resources { t.Run(res.Addr.String(), func(t *testing.T) { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3757,7 +3755,7 @@ func TestContext2Plan_requiresReplace(t *testing.T) { if got, want := ric.ActionReason, plans.ResourceInstanceReplaceBecauseCannotUpdate; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "v": cty.StringVal("goodbye"), }), ric.After) default: @@ -3801,8 +3799,7 @@ func TestContext2Plan_taint(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -3810,7 +3807,7 @@ func TestContext2Plan_taint(t *testing.T) { for _, res := range plan.Changes.Resources { t.Run(res.Addr.String(), func(t *testing.T) { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3823,7 +3820,7 @@ func TestContext2Plan_taint(t *testing.T) { if got, want := res.ActionReason, plans.ResourceInstanceReplaceBecauseTainted; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("2"), "type": cty.UnknownVal(cty.String), @@ -3845,7 +3842,7 @@ func TestContext2Plan_taint(t *testing.T) { func TestContext2Plan_taintIgnoreChanges(t *testing.T) { m := testModule(t, "plan-taint-ignore-changes") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -3879,15 +3876,14 @@ func TestContext2Plan_taintIgnoreChanges(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3900,12 +3896,12 @@ func TestContext2Plan_taintIgnoreChanges(t *testing.T) { if got, want := res.ActionReason, plans.ResourceInstanceReplaceBecauseTainted; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("foo"), "vars": cty.StringVal("foo"), "type": cty.StringVal("aws_instance"), }), ric.Before) - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "vars": cty.StringVal("foo"), "type": cty.UnknownVal(cty.String), @@ -3960,15 +3956,14 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 3 { t.Fatal("expected 3 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -3981,11 +3976,11 @@ func TestContext2Plan_taintDestroyInterpolatedCountRace(t *testing.T) { if got, want := ric.ActionReason, plans.ResourceInstanceReplaceBecauseTainted; got != want { t.Errorf("wrong action reason\ngot: %s\nwant: %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "type": cty.StringVal("aws_instance"), }), ric.Before) - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) @@ -4021,15 +4016,19 @@ func TestContext2Plan_targeted(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + checkPlanApplyable(t, plan) + if plan.Complete { + t.Error("plan marked as complete; should not be because it used targeting") + } + + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4039,7 +4038,7 @@ func TestContext2Plan_targeted(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created", i) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -4071,15 +4070,19 @@ func TestContext2Plan_targetedCrossModule(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + checkPlanApplyable(t, plan) + if plan.Complete { + t.Error("plan marked as complete; should not be because it used targeting") + } + + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4088,13 +4091,13 @@ func TestContext2Plan_targetedCrossModule(t *testing.T) { } switch i := ric.Addr.String(); i { case "module.A.aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.StringVal("bar"), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.B.aws_instance.bar": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "foo": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), @@ -4108,7 +4111,7 @@ func TestContext2Plan_targetedCrossModule(t *testing.T) { func TestContext2Plan_targetedModuleWithProvider(t *testing.T) { m := testModule(t, "plan-targeted-module-with-provider") p := testProvider("null") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "key": {Type: cty.String, Optional: true}, @@ -4136,16 +4139,19 @@ func TestContext2Plan_targetedModuleWithProvider(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanApplyable(t, plan) + if plan.Complete { + t.Error("plan marked as complete; should not be because it used targeting") + } - schema := p.GetProviderSchemaResponse.ResourceTypes["null_resource"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["null_resource"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } res := plan.Changes.Resources[0] - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4195,16 +4201,19 @@ func TestContext2Plan_targetedOrphan(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanApplyable(t, plan) + if plan.Complete { + t.Error("plan marked as complete; should not be because it used targeting") + } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4261,16 +4270,19 @@ func TestContext2Plan_targetedModuleOrphan(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanApplyable(t, plan) + if plan.Complete { + t.Error("plan marked as complete; should not be because it used targeting") + } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } res := plan.Changes.Resources[0] - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4304,16 +4316,19 @@ func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanApplyable(t, plan) + if plan.Complete { + t.Error("plan marked as complete; should not be because it used targeting") + } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4322,12 +4337,12 @@ func TestContext2Plan_targetedModuleUntargetedVariable(t *testing.T) { } switch i := ric.Addr.String(); i { case "aws_instance.blue": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), }), ric.After) case "module.blue_mod.aws_instance.mod": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "value": cty.UnknownVal(cty.String), "type": cty.UnknownVal(cty.String), @@ -4410,11 +4425,10 @@ func TestContext2Plan_targetedOverTen(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4509,15 +4523,14 @@ func TestContext2Plan_ignoreChanges(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } res := plan.Changes.Resources[0] - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4526,7 +4539,7 @@ func TestContext2Plan_ignoreChanges(t *testing.T) { t.Fatalf("unexpected resource: %s", ric.Addr) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "ami": cty.StringVal("ami-abcd1234"), "type": cty.StringVal("aws_instance"), @@ -4536,7 +4549,20 @@ func TestContext2Plan_ignoreChanges(t *testing.T) { func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { m := testModule(t, "plan-ignore-changes-wildcard") p := testProvider("aws") - p.PlanResourceChangeFn = testDiffFn + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + // computed attributes should not be set in config + id := req.Config.GetAttr("id") + if !id.IsNull() { + t.Error("computed id set in plan config") + } + + foo := req.Config.GetAttr("foo") + if foo.IsNull() { + t.Error(`missing "foo" during plan, was set to "bar" in state and config`) + } + + return testDiffFn(req) + } state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) @@ -4544,7 +4570,7 @@ func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { mustResourceInstanceAddr("aws_instance.foo").Resource, &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, - AttrsJSON: []byte(`{"id":"bar","ami":"ami-abcd1234","instance":"t2.micro","type":"aws_instance"}`), + AttrsJSON: []byte(`{"id":"bar","ami":"ami-abcd1234","instance":"t2.micro","type":"aws_instance","foo":"bar"}`), }, mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), ) @@ -4571,6 +4597,10 @@ func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } + checkPlanComplete(t, plan) + if plan.Applyable { + t.Error("plan marked as applyable; should not be because all actions should be no-op") + } for _, res := range plan.Changes.Resources { if res.Action != plans.NoOp { @@ -4582,7 +4612,7 @@ func TestContext2Plan_ignoreChangesWildcard(t *testing.T) { func TestContext2Plan_ignoreChangesInMap(t *testing.T) { p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_ignore_changes_map": { Attributes: map[string]*configschema.Attribute{ @@ -4627,15 +4657,14 @@ func TestContext2Plan_ignoreChangesInMap(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["test_ignore_changes_map"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["test_ignore_changes_map"] if got, want := len(plan.Changes.Resources), 1; got != want { t.Fatalf("wrong number of changes %d; want %d", got, want) } res := plan.Changes.Resources[0] - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4647,7 +4676,7 @@ func TestContext2Plan_ignoreChangesInMap(t *testing.T) { t.Fatalf("unexpected resource address %s; want %s", got, want) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "tags": cty.MapVal(map[string]cty.Value{ "ignored": cty.StringVal("from state"), "other": cty.StringVal("from config"), @@ -4690,15 +4719,14 @@ func TestContext2Plan_ignoreChangesSensitive(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } res := plan.Changes.Resources[0] - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4707,7 +4735,7 @@ func TestContext2Plan_ignoreChangesSensitive(t *testing.T) { t.Fatalf("unexpected resource: %s", ric.Addr) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.StringVal("bar"), "ami": cty.StringVal("ami-abcd1234"), "type": cty.StringVal("aws_instance"), @@ -4717,7 +4745,7 @@ func TestContext2Plan_ignoreChangesSensitive(t *testing.T) { func TestContext2Plan_moduleMapLiteral(t *testing.T) { m := testModule(t, "plan-module-map-literal") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -4756,7 +4784,7 @@ func TestContext2Plan_moduleMapLiteral(t *testing.T) { func TestContext2Plan_computedValueInMap(t *testing.T) { m := testModule(t, "plan-computed-value-in-map") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -4799,9 +4827,9 @@ func TestContext2Plan_computedValueInMap(t *testing.T) { } for _, res := range plan.Changes.Resources { - schema := p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type].Block + schema := p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type] - ric, err := res.Decode(schema.ImpliedType()) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4812,11 +4840,11 @@ func TestContext2Plan_computedValueInMap(t *testing.T) { switch i := ric.Addr.String(); i { case "aws_computed_source.intermediates": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "computed_read_only": cty.UnknownVal(cty.String), }), ric.After) case "module.test_mod.aws_instance.inner2": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "looked_up": cty.UnknownVal(cty.String), }), ric.After) default: @@ -4828,7 +4856,7 @@ func TestContext2Plan_computedValueInMap(t *testing.T) { func TestContext2Plan_moduleVariableFromSplat(t *testing.T) { m := testModule(t, "plan-module-variable-from-splat") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -4854,9 +4882,9 @@ func TestContext2Plan_moduleVariableFromSplat(t *testing.T) { } for _, res := range plan.Changes.Resources { - schema := p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type].Block + schema := p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type] - ric, err := res.Decode(schema.ImpliedType()) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4870,7 +4898,7 @@ func TestContext2Plan_moduleVariableFromSplat(t *testing.T) { "module.mod1.aws_instance.test[1]", "module.mod2.aws_instance.test[0]", "module.mod2.aws_instance.test[1]": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "thing": cty.StringVal("doesnt"), }), ric.After) default: @@ -4882,7 +4910,7 @@ func TestContext2Plan_moduleVariableFromSplat(t *testing.T) { func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { m := testModule(t, "plan-cbd-depends-datasource") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -4933,15 +4961,15 @@ func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { seenAddrs := make(map[string]struct{}) for _, res := range plan.Changes.Resources { - var schema *configschema.Block + var schema providers.Schema switch res.Addr.Resource.Resource.Mode { case addrs.DataResourceMode: - schema = p.GetProviderSchemaResponse.DataSources[res.Addr.Resource.Resource.Type].Block + schema = p.GetProviderSchemaResponse.DataSources[res.Addr.Resource.Resource.Type] case addrs.ManagedResourceMode: - schema = p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type].Block + schema = p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type] } - ric, err := res.Decode(schema.ImpliedType()) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -4954,7 +4982,7 @@ func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created, got %s", ric.Addr, ric.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "num": cty.StringVal("2"), "computed": cty.StringVal("data_id"), }), ric.After) @@ -4962,7 +4990,7 @@ func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created, got %s", ric.Addr, ric.Action) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "num": cty.StringVal("2"), "computed": cty.StringVal("data_id"), }), ric.After) @@ -4985,7 +5013,7 @@ func TestContext2Plan_createBeforeDestroy_depends_datasource(t *testing.T) { func TestContext2Plan_listOrder(t *testing.T) { m := testModule(t, "plan-list-order") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -5029,7 +5057,7 @@ func TestContext2Plan_listOrder(t *testing.T) { func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { m := testModule(t, "plan-ignore-changes-with-flatmaps") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -5078,9 +5106,8 @@ func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { } res := plan.Changes.Resources[0] - schema := p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type].Block - - ric, err := res.Decode(schema.ImpliedType()) + schema := p.GetProviderSchemaResponse.ResourceTypes[res.Addr.Resource.Resource.Type] + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -5093,7 +5120,7 @@ func TestContext2Plan_ignoreChangesWithFlatmaps(t *testing.T) { t.Fatalf("unknown resource: %s", ric.Addr) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "lst": cty.ListVal([]cty.Value{ cty.StringVal("j"), cty.StringVal("k"), @@ -5182,7 +5209,7 @@ func TestContext2Plan_resourceNestedCount(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("validate errors: %s", diags.Err()) } @@ -5249,7 +5276,7 @@ func TestContext2Plan_computedAttrRefTypeMismatch(t *testing.T) { func TestContext2Plan_selfRef(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -5266,7 +5293,7 @@ func TestContext2Plan_selfRef(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } @@ -5285,7 +5312,7 @@ func TestContext2Plan_selfRef(t *testing.T) { func TestContext2Plan_selfRefMulti(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -5302,7 +5329,7 @@ func TestContext2Plan_selfRefMulti(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } @@ -5321,7 +5348,7 @@ func TestContext2Plan_selfRefMulti(t *testing.T) { func TestContext2Plan_selfRefMultiAll(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -5338,7 +5365,7 @@ func TestContext2Plan_selfRefMultiAll(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected validation failure: %s", diags.Err()) } @@ -5502,8 +5529,7 @@ func TestContext2Plan_variableSensitivity(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) @@ -5513,30 +5539,27 @@ func TestContext2Plan_variableSensitivity(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "foo": cty.StringVal("foo").Mark(marks.Sensitive), }), ric.After) - if len(res.ChangeSrc.BeforeValMarks) != 0 { - t.Errorf("unexpected BeforeValMarks: %#v", res.ChangeSrc.BeforeValMarks) + if len(res.ChangeSrc.BeforeSensitivePaths) != 0 { + t.Errorf("unexpected BeforeSensitivePaths: %#v", res.ChangeSrc.BeforeSensitivePaths) } - if len(res.ChangeSrc.AfterValMarks) != 1 { - t.Errorf("unexpected AfterValMarks: %#v", res.ChangeSrc.AfterValMarks) + if len(res.ChangeSrc.AfterSensitivePaths) != 1 { + t.Errorf("unexpected AfterSensitivePaths: %#v", res.ChangeSrc.AfterSensitivePaths) continue } - pvm := res.ChangeSrc.AfterValMarks[0] - if got, want := pvm.Path, cty.GetAttrPath("foo"); !got.Equals(want) { + sensitivePath := res.ChangeSrc.AfterSensitivePaths[0] + if got, want := sensitivePath, cty.GetAttrPath("foo"); !got.Equals(want) { t.Errorf("unexpected path for mark\n got: %#v\nwant: %#v", got, want) } - if got, want := pvm.Marks, cty.NewValueMarks(marks.Sensitive); !got.Equal(want) { - t.Errorf("unexpected value for mark\n got: %#v\nwant: %#v", got, want) - } default: t.Fatal("unknown instance:", i) } @@ -5571,8 +5594,7 @@ func TestContext2Plan_variableSensitivityModule(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) @@ -5582,40 +5604,38 @@ func TestContext2Plan_variableSensitivityModule(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } switch i := ric.Addr.String(); i { case "module.child.aws_instance.foo": - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "foo": cty.StringVal("foo").Mark(marks.Sensitive), "value": cty.StringVal("boop").Mark(marks.Sensitive), }), ric.After) - if len(res.ChangeSrc.BeforeValMarks) != 0 { - t.Errorf("unexpected BeforeValMarks: %#v", res.ChangeSrc.BeforeValMarks) + if len(res.ChangeSrc.BeforeSensitivePaths) != 0 { + t.Errorf("unexpected BeforeSensitivePaths: %#v", res.ChangeSrc.BeforeSensitivePaths) } - if len(res.ChangeSrc.AfterValMarks) != 2 { - t.Errorf("expected AfterValMarks to contain two elements: %#v", res.ChangeSrc.AfterValMarks) + if len(res.ChangeSrc.AfterSensitivePaths) != 2 { + t.Errorf("expected AfterSensitivePaths to contain two elements: %#v", res.ChangeSrc.AfterSensitivePaths) continue } // validate that the after marks have "foo" and "value" - contains := func(pvmSlice []cty.PathValueMarks, stepName string) bool { - for _, pvm := range pvmSlice { - if pvm.Path.Equals(cty.GetAttrPath(stepName)) { - if pvm.Marks.Equal(cty.NewValueMarks(marks.Sensitive)) { - return true - } + contains := func(paths []cty.Path, stepName string) bool { + for _, path := range paths { + if path.Equals(cty.GetAttrPath(stepName)) { + return true } } return false } - if !contains(res.ChangeSrc.AfterValMarks, "foo") { - t.Error("unexpected AfterValMarks to contain \"foo\" with sensitive mark") + if !contains(res.ChangeSrc.AfterSensitivePaths, "foo") { + t.Error("unexpected AfterSensitivePaths to contain \"foo\" with sensitive mark") } - if !contains(res.ChangeSrc.AfterValMarks, "value") { - t.Error("unexpected AfterValMarks to contain \"value\" with sensitive mark") + if !contains(res.ChangeSrc.AfterSensitivePaths, "value") { + t.Error("unexpected AfterSensitivePaths to contain \"value\" with sensitive mark") } default: t.Fatal("unknown instance:", i) @@ -5651,7 +5671,7 @@ func objectVal(t *testing.T, schema *configschema.Block, m map[string]cty.Value) func TestContext2Plan_requiredModuleOutput(t *testing.T) { m := testModule(t, "plan-required-output") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -5673,8 +5693,7 @@ func TestContext2Plan_requiredModuleOutput(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -5685,7 +5704,7 @@ func TestContext2Plan_requiredModuleOutput(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -5693,12 +5712,12 @@ func TestContext2Plan_requiredModuleOutput(t *testing.T) { var expected cty.Value switch i := ric.Addr.String(); i { case "test_resource.root": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "required": cty.UnknownVal(cty.String), }) case "module.mod.test_resource.for_output": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "required": cty.StringVal("val"), }) @@ -5714,7 +5733,7 @@ func TestContext2Plan_requiredModuleOutput(t *testing.T) { func TestContext2Plan_requiredModuleObject(t *testing.T) { m := testModule(t, "plan-required-whole-mod") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -5736,8 +5755,7 @@ func TestContext2Plan_requiredModuleObject(t *testing.T) { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["test_resource"] if len(plan.Changes.Resources) != 2 { t.Fatal("expected 2 changes, got", len(plan.Changes.Resources)) @@ -5748,7 +5766,7 @@ func TestContext2Plan_requiredModuleObject(t *testing.T) { if res.Action != plans.Create { t.Fatalf("expected resource creation, got %s", res.Action) } - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -5756,12 +5774,12 @@ func TestContext2Plan_requiredModuleObject(t *testing.T) { var expected cty.Value switch i := ric.Addr.String(); i { case "test_resource.root": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "required": cty.UnknownVal(cty.String), }) case "module.mod.test_resource.for_output": - expected = objectVal(t, schema, map[string]cty.Value{ + expected = objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "required": cty.StringVal("val"), }) @@ -6049,8 +6067,8 @@ data "test_data_source" "foo" {} `, }) - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ DataSources: map[string]*configschema.Block{ "test_data_source": { Attributes: map[string]*configschema.Attribute{ @@ -6150,7 +6168,7 @@ resource "test_instance" "b" { }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) t.Run("test_instance.a[0]", func(t *testing.T) { instAddr := mustResourceInstanceAddr("test_instance.a[0]") @@ -6205,15 +6223,14 @@ func TestContext2Plan_targetedModuleInstance(t *testing.T) { if diags.HasErrors() { t.Fatalf("unexpected errors: %s", diags.Err()) } - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] if len(plan.Changes.Resources) != 1 { t.Fatal("expected 1 changes, got", len(plan.Changes.Resources)) } for _, res := range plan.Changes.Resources { - ric, err := res.Decode(ty) + ric, err := res.Decode(schema) if err != nil { t.Fatal(err) } @@ -6223,7 +6240,7 @@ func TestContext2Plan_targetedModuleInstance(t *testing.T) { if res.Action != plans.Create { t.Fatalf("resource %s should be created", i) } - checkVals(t, objectVal(t, schema, map[string]cty.Value{ + checkVals(t, objectVal(t, schema.Body, map[string]cty.Value{ "id": cty.UnknownVal(cty.String), "num": cty.NumberIntVal(2), "type": cty.UnknownVal(cty.String), @@ -6319,7 +6336,7 @@ data "test_data_source" "e" { }) plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) rc := plan.Changes.ResourceInstance(addrs.Resource{ Mode: addrs.DataResourceMode, @@ -6440,7 +6457,7 @@ resource "test_instance" "a" { Mode: plans.NormalMode, SkipRefresh: true, }) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) if p.ReadResourceCalled { t.Fatal("Resource should not have been refreshed") @@ -6499,7 +6516,7 @@ data "test_data_source" "b" { }) _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) - assertNoErrors(t, diags) + tfdiags.AssertNoErrors(t, diags) // The change to data source a should not prevent data source b from being // read. @@ -6523,7 +6540,7 @@ resource "test_instance" "a" { return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -6579,7 +6596,7 @@ resource "test_instance" "a" { return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -6631,7 +6648,7 @@ resource "test_instance" "a" { }) p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -6676,6 +6693,70 @@ resource "test_instance" "a" { } } +func TestContext2Plan_legacyProviderIgnoreAll(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_instance" "a" { + lifecycle { + ignore_changes = all + } + data = "foo" +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_instance": { + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "data": {Type: cty.String, Optional: true}, + }, + }, + }, + }) + p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) { + plan := req.ProposedNewState.AsValueMap() + // Update both the computed id and the configured data. + // Legacy providers expect terraform to be able to ignore these. + + plan["id"] = cty.StringVal("updated") + plan["data"] = cty.StringVal("updated") + resp.PlannedState = cty.ObjectVal(plan) + resp.LegacyTypeSystem = true + return resp + } + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_instance.a").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"orig","data":"orig"}`), + Dependencies: []addrs.ConfigResource{}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + plan, diags := ctx.Plan(m, state, DefaultPlanOpts) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + for _, c := range plan.Changes.Resources { + if c.Action != plans.NoOp { + t.Fatalf("expected NoOp plan, got %s\n", c.Action) + } + } +} + func TestContext2Plan_dataRemovalNoProvider(t *testing.T) { m := testModuleInline(t, map[string]string{ "main.tf": ` @@ -6756,9 +6837,9 @@ resource "test_resource" "foo" { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "value":"hello", "sensitive_value":"hello"}`), - AttrSensitivePaths: []cty.PathValueMarks{ - {Path: cty.Path{cty.GetAttrStep{Name: "value"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - {Path: cty.Path{cty.GetAttrStep{Name: "sensitive_value"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("value"), + cty.GetAttrPath("sensitive_value"), }, }, addrs.AbsProviderConfig{ @@ -6779,6 +6860,120 @@ resource "test_resource" "foo" { } } +func TestContext2Plan_variableCustomValidationsSimple(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` + variable "a" { + type = string + + validation { + condition = var.a == "beep" + error_message = "Value must be beep." + } + } + `, + }) + + p := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Should be successful if we comply with the validation rules... + _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: map[string]*InputValue{ + "a": { + Value: cty.StringVal("beep"), + }, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected error\ngot: %s", diags.Err().Error()) + } + + // ...but should get an error if we violate the rule. + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: map[string]*InputValue{ + "a": { + Value: cty.StringVal("not beep"), + }, + }, + }) + if !diags.HasErrors() { + t.Fatalf("unexpected success") + } + gotDiags := diags.Err().Error() + wantDiagSubstr := "Value must be beep." + if !strings.Contains(gotDiags, wantDiagSubstr) { + t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", gotDiags, wantDiagSubstr) + } +} + +func TestContext2Plan_variableCustomValidationsCrossRef(t *testing.T) { + // This test is dealing with validation rules that refer to other objects + // in the same module. + m := testModuleInline(t, map[string]string{ + "main.tf": ` + variable "a" { + type = string + } + + variable "b" { + type = string + + validation { + condition = var.a == var.b + error_message = "Value must match the value of var.a." + } + } + `, + }) + + p := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // Should be successful if we comply with the validation rules... + _, diags := ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: map[string]*InputValue{ + "a": { + Value: cty.StringVal("beep"), + }, + "b": { + Value: cty.StringVal("beep"), + }, + }, + }) + if diags.HasErrors() { + t.Fatalf("unexpected error\ngot: %s", diags.Err().Error()) + } + + // ...but should get an error if we violate the rule. + _, diags = ctx.Plan(m, states.NewState(), &PlanOpts{ + SetVariables: map[string]*InputValue{ + "a": { + Value: cty.StringVal("beep"), + }, + "b": { + Value: cty.StringVal("not beep"), + }, + }, + }) + if !diags.HasErrors() { + t.Fatalf("unexpected success") + } + gotDiags := diags.Err().Error() + wantDiagSubstr := "Value must match the value of var.a." + if !strings.Contains(gotDiags, wantDiagSubstr) { + t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", gotDiags, wantDiagSubstr) + } +} + func TestContext2Plan_variableCustomValidationsSensitive(t *testing.T) { m := testModule(t, "validate-variable-custom-validations-child-sensitive") @@ -6810,8 +7005,10 @@ output "planned" { ctx := testContext2(t, &ContextOpts{}) state := states.BuildState(func(s *states.SyncState) { - r := s.Module(addrs.RootModuleInstance) - r.SetOutputValue("planned", cty.NullVal(cty.DynamicPseudoType), false) + s.SetOutputValue( + addrs.OutputValue{Name: "planned"}.Absolute(addrs.RootModuleInstance), + cty.NullVal(cty.DynamicPseudoType), false, + ) }) plan, diags := ctx.Plan(m, state, DefaultPlanOpts) if diags.HasErrors() { diff --git a/internal/terraform/context_plugins.go b/internal/terraform/context_plugins.go index 4b3071cf6d..883c64507f 100644 --- a/internal/terraform/context_plugins.go +++ b/internal/terraform/context_plugins.go @@ -1,209 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "fmt" - "log" - "sync" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" + "github.com/hashicorp/terraform/internal/schemarepo" + "github.com/hashicorp/terraform/internal/schemarepo/loadschemas" + "github.com/hashicorp/terraform/internal/states" ) -// contextPlugins represents a library of available plugins (providers and -// provisioners) which we assume will all be used with the same -// terraform.Context, and thus it'll be safe to cache certain information -// about the providers for performance reasons. -type contextPlugins struct { - providerFactories map[addrs.Provider]providers.Factory - provisionerFactories map[string]provisioners.Factory +// contextPlugins is a deprecated old name for loadschemas.Plugins +type contextPlugins = loadschemas.Plugins - // We memoize the schemas we've previously loaded in here, to avoid - // repeatedly paying the cost of activating the same plugins to access - // their schemas in various different spots. We use schemas for many - // purposes in Terraform, so there isn't a single choke point where - // it makes sense to preload all of them. - providerSchemas map[addrs.Provider]*ProviderSchema - provisionerSchemas map[string]*configschema.Block - schemasLock sync.Mutex +func newContextPlugins( + providerFactories map[addrs.Provider]providers.Factory, + provisionerFactories map[string]provisioners.Factory, + preloadedProviderSchemas map[addrs.Provider]providers.ProviderSchema, +) *loadschemas.Plugins { + return loadschemas.NewPlugins(providerFactories, provisionerFactories, preloadedProviderSchemas) } -func newContextPlugins(providerFactories map[addrs.Provider]providers.Factory, provisionerFactories map[string]provisioners.Factory) *contextPlugins { - ret := &contextPlugins{ - providerFactories: providerFactories, - provisionerFactories: provisionerFactories, - } - ret.init() - return ret -} - -func (cp *contextPlugins) init() { - cp.providerSchemas = make(map[addrs.Provider]*ProviderSchema, len(cp.providerFactories)) - cp.provisionerSchemas = make(map[string]*configschema.Block, len(cp.provisionerFactories)) -} - -func (cp *contextPlugins) HasProvider(addr addrs.Provider) bool { - _, ok := cp.providerFactories[addr] - return ok -} - -func (cp *contextPlugins) NewProviderInstance(addr addrs.Provider) (providers.Interface, error) { - f, ok := cp.providerFactories[addr] - if !ok { - return nil, fmt.Errorf("unavailable provider %q", addr.String()) - } - - return f() - -} - -func (cp *contextPlugins) HasProvisioner(typ string) bool { - _, ok := cp.provisionerFactories[typ] - return ok -} - -func (cp *contextPlugins) NewProvisionerInstance(typ string) (provisioners.Interface, error) { - f, ok := cp.provisionerFactories[typ] - if !ok { - return nil, fmt.Errorf("unavailable provisioner %q", typ) - } - - return f() -} - -// ProviderSchema uses a temporary instance of the provider with the given -// address to obtain the full schema for all aspects of that provider. -// -// ProviderSchema memoizes results by unique provider address, so it's fine -// to repeatedly call this method with the same address if various different -// parts of Terraform all need the same schema information. -func (cp *contextPlugins) ProviderSchema(addr addrs.Provider) (*ProviderSchema, error) { - cp.schemasLock.Lock() - defer cp.schemasLock.Unlock() - - if schema, ok := cp.providerSchemas[addr]; ok { - return schema, nil - } - - log.Printf("[TRACE] terraform.contextPlugins: Initializing provider %q to read its schema", addr) - - provider, err := cp.NewProviderInstance(addr) - if err != nil { - return nil, fmt.Errorf("failed to instantiate provider %q to obtain schema: %s", addr, err) - } - defer provider.Close() - - resp := provider.GetProviderSchema() - if resp.Diagnostics.HasErrors() { - return nil, fmt.Errorf("failed to retrieve schema from provider %q: %s", addr, resp.Diagnostics.Err()) - } - - s := &ProviderSchema{ - Provider: resp.Provider.Block, - ResourceTypes: make(map[string]*configschema.Block), - DataSources: make(map[string]*configschema.Block), - - ResourceTypeSchemaVersions: make(map[string]uint64), - } - - if resp.Provider.Version < 0 { - // We're not using the version numbers here yet, but we'll check - // for validity anyway in case we start using them in future. - return nil, fmt.Errorf("provider %s has invalid negative schema version for its configuration blocks,which is a bug in the provider ", addr) - } - - for t, r := range resp.ResourceTypes { - if err := r.Block.InternalValidate(); err != nil { - return nil, fmt.Errorf("provider %s has invalid schema for managed resource type %q, which is a bug in the provider: %q", addr, t, err) - } - s.ResourceTypes[t] = r.Block - s.ResourceTypeSchemaVersions[t] = uint64(r.Version) - if r.Version < 0 { - return nil, fmt.Errorf("provider %s has invalid negative schema version for managed resource type %q, which is a bug in the provider", addr, t) - } - } - - for t, d := range resp.DataSources { - if err := d.Block.InternalValidate(); err != nil { - return nil, fmt.Errorf("provider %s has invalid schema for data resource type %q, which is a bug in the provider: %q", addr, t, err) - } - s.DataSources[t] = d.Block - if d.Version < 0 { - // We're not using the version numbers here yet, but we'll check - // for validity anyway in case we start using them in future. - return nil, fmt.Errorf("provider %s has invalid negative schema version for data resource type %q, which is a bug in the provider", addr, t) - } - } - - if resp.ProviderMeta.Block != nil { - s.ProviderMeta = resp.ProviderMeta.Block - } - - cp.providerSchemas[addr] = s - return s, nil -} - -// ProviderConfigSchema is a helper wrapper around ProviderSchema which first -// reads the full schema of the given provider and then extracts just the -// provider's configuration schema, which defines what's expected in a -// "provider" block in the configuration when configuring this provider. -func (cp *contextPlugins) ProviderConfigSchema(providerAddr addrs.Provider) (*configschema.Block, error) { - providerSchema, err := cp.ProviderSchema(providerAddr) - if err != nil { - return nil, err - } - - return providerSchema.Provider, nil -} - -// ResourceTypeSchema is a helper wrapper around ProviderSchema which first -// reads the schema of the given provider and then tries to find the schema -// for the resource type of the given resource mode in that provider. -// -// ResourceTypeSchema will return an error if the provider schema lookup -// fails, but will return nil if the provider schema lookup succeeds but then -// the provider doesn't have a resource of the requested type. -// -// Managed resource types have versioned schemas, so the second return value -// is the current schema version number for the requested resource. The version -// is irrelevant for other resource modes. -func (cp *contextPlugins) ResourceTypeSchema(providerAddr addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (*configschema.Block, uint64, error) { - providerSchema, err := cp.ProviderSchema(providerAddr) - if err != nil { - return nil, 0, err - } - - schema, version := providerSchema.SchemaForResourceType(resourceMode, resourceType) - return schema, version, nil -} - -// ProvisionerSchema uses a temporary instance of the provisioner with the -// given type name to obtain the schema for that provisioner's configuration. -// -// ProvisionerSchema memoizes results by provisioner type name, so it's fine -// to repeatedly call this method with the same name if various different -// parts of Terraform all need the same schema information. -func (cp *contextPlugins) ProvisionerSchema(typ string) (*configschema.Block, error) { - cp.schemasLock.Lock() - defer cp.schemasLock.Unlock() - - if schema, ok := cp.provisionerSchemas[typ]; ok { - return schema, nil - } - - log.Printf("[TRACE] terraform.contextPlugins: Initializing provisioner %q to read its schema", typ) - provisioner, err := cp.NewProvisionerInstance(typ) - if err != nil { - return nil, fmt.Errorf("failed to instantiate provisioner %q to obtain schema: %s", typ, err) - } - defer provisioner.Close() - - resp := provisioner.GetSchema() - if resp.Diagnostics.HasErrors() { - return nil, fmt.Errorf("failed to retrieve schema from provisioner %q: %s", typ, resp.Diagnostics.Err()) - } - - cp.provisionerSchemas[typ] = resp.Provisioner - return resp.Provisioner, nil +// Schemas is a deprecated old name for schemarepo.Schemas +type Schemas = schemarepo.Schemas + +func loadSchemas(config *configs.Config, state *states.State, plugins *loadschemas.Plugins) (*schemarepo.Schemas, error) { + return loadschemas.LoadSchemas(config, state, plugins) } diff --git a/internal/terraform/context_plugins_test.go b/internal/terraform/context_plugins_test.go index 26813ceb0d..a6764a82e5 100644 --- a/internal/terraform/context_plugins_test.go +++ b/internal/terraform/context_plugins_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -7,6 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" + "github.com/hashicorp/terraform/internal/schemarepo/loadschemas" ) // simpleMockPluginLibrary returns a plugin library pre-configured with @@ -19,27 +23,26 @@ import ( // Each call to this function produces an entirely-separate set of objects, // so the caller can feel free to modify the returned value to further // customize the mocks contained within. -func simpleMockPluginLibrary() *contextPlugins { +func simpleMockPluginLibrary() *loadschemas.Plugins { // We create these out here, rather than in the factory functions below, // because we want each call to the factory to return the _same_ instance, // so that test code can customize it before passing this component // factory into real code under test. provider := simpleMockProvider() provisioner := simpleMockProvisioner() - ret := &contextPlugins{ - providerFactories: map[addrs.Provider]providers.Factory{ + return loadschemas.NewPlugins( + map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): func() (providers.Interface, error) { return provider, nil }, }, - provisionerFactories: map[string]provisioners.Factory{ + map[string]provisioners.Factory{ "test": func() (provisioners.Interface, error) { return provisioner, nil }, }, - } - ret.init() // prepare the internal cache data structures - return ret + nil, + ) } // simpleTestSchema returns a block schema that contains a few optional diff --git a/internal/terraform/context_refresh.go b/internal/terraform/context_refresh.go index cac5232b0d..467c709957 100644 --- a/internal/terraform/context_refresh.go +++ b/internal/terraform/context_refresh.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/context_refresh_test.go b/internal/terraform/context_refresh_test.go index aa2239dbed..f9f9c2cea3 100644 --- a/internal/terraform/context_refresh_test.go +++ b/internal/terraform/context_refresh_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -15,7 +18,9 @@ import ( "github.com/hashicorp/terraform/internal/configs/hcl2shim" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestContext2Refresh(t *testing.T) { @@ -39,8 +44,8 @@ func TestContext2Refresh(t *testing.T) { }, }) - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + ty := schema.Body.ImpliedType() readState, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"id": "foo", "foo": "baz"}, ty) if err != nil { t.Fatal(err) @@ -60,12 +65,12 @@ func TestContext2Refresh(t *testing.T) { } mod := s.RootModule() - fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(ty) + fromState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } - newState, err := schema.CoerceValue(fromState.Value) + newState, err := schema.Body.CoerceValue(fromState.Value) if err != nil { t.Fatal(err) } @@ -101,7 +106,7 @@ func TestContext2Refresh_dynamicAttr(t *testing.T) { }) p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -126,9 +131,7 @@ func TestContext2Refresh_dynamicAttr(t *testing.T) { }, }) - schema := p.GetProviderSchemaResponse.ResourceTypes["test_instance"].Block - ty := schema.ImpliedType() - + schema := p.GetProviderSchemaResponse.ResourceTypes["test_instance"] s, diags := ctx.Refresh(m, startingState, &PlanOpts{Mode: plans.NormalMode}) if diags.HasErrors() { t.Fatal(diags.Err()) @@ -139,7 +142,7 @@ func TestContext2Refresh_dynamicAttr(t *testing.T) { } mod := s.RootModule() - newState, err := mod.Resources["test_instance.foo"].Instances[addrs.NoKey].Current.Decode(ty) + newState, err := mod.Resources["test_instance.foo"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -163,7 +166,7 @@ func TestContext2Refresh_dataComputedModuleVar(t *testing.T) { return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { @@ -201,19 +204,19 @@ func TestContext2Refresh_dataComputedModuleVar(t *testing.T) { }, }) - s, diags := ctx.Refresh(m, states.NewState(), &PlanOpts{Mode: plans.NormalMode}) + plan, diags := ctx.Plan(m, states.NewState(), &PlanOpts{Mode: plans.RefreshOnlyMode}) if diags.HasErrors() { t.Fatalf("refresh errors: %s", diags.Err()) } - checkStateString(t, s, ` + checkStateString(t, plan.PriorState, ` `) } func TestContext2Refresh_targeted(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_elb": { @@ -293,7 +296,7 @@ func TestContext2Refresh_targeted(t *testing.T) { func TestContext2Refresh_targetedCount(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_elb": { @@ -383,7 +386,7 @@ func TestContext2Refresh_targetedCount(t *testing.T) { func TestContext2Refresh_targetedCountIndex(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_elb": { @@ -465,7 +468,7 @@ func TestContext2Refresh_targetedCountIndex(t *testing.T) { func TestContext2Refresh_moduleComputedVar(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { @@ -512,7 +515,7 @@ func TestContext2Refresh_delete(t *testing.T) { }) p.ReadResourceResponse = &providers.ReadResourceResponse{ - NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block.ImpliedType()), + NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Body.ImpliedType()), } s, diags := ctx.Refresh(m, state, &PlanOpts{Mode: plans.NormalMode}) @@ -626,7 +629,7 @@ func TestContext2Refresh_modules(t *testing.T) { func TestContext2Refresh_moduleInputComputedOutput(t *testing.T) { m := testModule(t, "refresh-module-input-computed-output") p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { @@ -694,7 +697,7 @@ func TestContext2Refresh_noState(t *testing.T) { func TestContext2Refresh_output(t *testing.T) { p := testProvider("aws") p.PlanResourceChangeFn = testDiffFn - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { @@ -718,7 +721,10 @@ func TestContext2Refresh_output(t *testing.T) { state := states.NewState() root := state.EnsureModule(addrs.RootModuleInstance) testSetResourceInstanceCurrent(root, "aws_instance.web", `{"id":"foo","foo":"bar"}`, `provider["registry.terraform.io/hashicorp/aws"]`) - root.SetOutputValue("foo", cty.StringVal("foo"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("foo"), false, + ) ctx := testContext2(t, &ContextOpts{ Providers: map[addrs.Provider]providers.Factory{ @@ -746,7 +752,7 @@ func TestContext2Refresh_outputPartial(t *testing.T) { // remote objects yet, to get stub values for interpolation. Therefore // we need to make DiffFn available to let that complete. - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { @@ -761,7 +767,7 @@ func TestContext2Refresh_outputPartial(t *testing.T) { }) p.ReadResourceResponse = &providers.ReadResourceResponse{ - NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block.ImpliedType()), + NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Body.ImpliedType()), } state := states.NewState() @@ -800,10 +806,8 @@ func TestContext2Refresh_stateBasic(t *testing.T) { }, }) - schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"].Block - ty := schema.ImpliedType() - - readStateVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ + schema := p.GetProviderSchemaResponse.ResourceTypes["aws_instance"] + readStateVal, err := schema.Body.CoerceValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), })) if err != nil { @@ -824,7 +828,7 @@ func TestContext2Refresh_stateBasic(t *testing.T) { } mod := s.RootModule() - newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(ty) + newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -844,7 +848,7 @@ func TestContext2Refresh_dataCount(t *testing.T) { resp.PlannedState = cty.ObjectVal(m) return resp } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test": { Attributes: map[string]*configschema.Attribute{ @@ -882,20 +886,22 @@ func TestContext2Refresh_dataCount(t *testing.T) { func TestContext2Refresh_dataState(t *testing.T) { m := testModule(t, "refresh-data-resource-basic") state := states.NewState() - schema := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "inputs": { - Type: cty.Map(cty.String), - Optional: true, + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "inputs": { + Type: cty.Map(cty.String), + Optional: true, + }, }, }, } p := testProvider("null") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, DataSources: map[string]*configschema.Block{ - "null_data_source": schema, + "null_data_source": schema.Body, }, }) @@ -927,7 +933,7 @@ func TestContext2Refresh_dataState(t *testing.T) { mod := s.RootModule() - newState, err := mod.Resources["data.null_data_source.testing"].Instances[addrs.NoKey].Current.Decode(schema.ImpliedType()) + newState, err := mod.Resources["data.null_data_source.testing"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -939,7 +945,7 @@ func TestContext2Refresh_dataState(t *testing.T) { func TestContext2Refresh_dataStateRefData(t *testing.T) { p := testProvider("null") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, DataSources: map[string]*configschema.Block{ "null_data_source": { @@ -1043,7 +1049,7 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { c, diags := NewContext(&ContextOpts{ Providers: map[addrs.Provider]providers.Factory{}, }) - assertNoDiagnostics(t, diags) + tfdiags.AssertNoDiagnostics(t, diags) _, diags = c.Refresh(m, states.NewState(), &PlanOpts{Mode: plans.NormalMode}) if !diags.HasErrors() { @@ -1058,22 +1064,24 @@ func TestContext2Refresh_unknownProvider(t *testing.T) { func TestContext2Refresh_vars(t *testing.T) { p := testProvider("aws") - schema := &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "ami": { - Type: cty.String, - Optional: true, - }, - "id": { - Type: cty.String, - Computed: true, + schema := providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "ami": { + Type: cty.String, + Optional: true, + }, + "id": { + Type: cty.String, + Computed: true, + }, }, }, } - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, - ResourceTypes: map[string]*configschema.Block{"aws_instance": schema}, + ResourceTypes: map[string]*configschema.Block{"aws_instance": schema.Body}, }) m := testModule(t, "refresh-vars") @@ -1087,7 +1095,7 @@ func TestContext2Refresh_vars(t *testing.T) { }, }) - readStateVal, err := schema.CoerceValue(cty.ObjectVal(map[string]cty.Value{ + readStateVal, err := schema.Body.CoerceValue(cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), })) if err != nil { @@ -1115,7 +1123,7 @@ func TestContext2Refresh_vars(t *testing.T) { mod := s.RootModule() - newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema.ImpliedType()) + newState, err := mod.Resources["aws_instance.web"].Instances[addrs.NoKey].Current.Decode(schema) if err != nil { t.Fatal(err) } @@ -1197,7 +1205,7 @@ func TestContext2Refresh_orphanModule(t *testing.T) { func TestContext2Validate(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{}, ResourceTypes: map[string]*configschema.Block{ "aws_instance": { @@ -1222,7 +1230,7 @@ func TestContext2Validate(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if len(diags) != 0 { t.Fatalf("unexpected error: %#v", diags.ErrWithWarnings()) } @@ -1261,7 +1269,7 @@ aws_instance.bar: func TestContext2Refresh_schemaUpgradeFlatmap(t *testing.T) { m := testModule(t, "refresh-schema-upgrade") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ @@ -1345,7 +1353,7 @@ test_thing.bar: func TestContext2Refresh_schemaUpgradeJSON(t *testing.T) { m := testModule(t, "refresh-schema-upgrade") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_thing": { Attributes: map[string]*configschema.Attribute{ @@ -1462,7 +1470,7 @@ data "aws_data_source" "foo" { func TestContext2Refresh_dataResourceDependsOn(t *testing.T) { m := testModule(t, "plan-data-depends-on") p := testProvider("test") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_resource": { Attributes: map[string]*configschema.Attribute{ @@ -1598,3 +1606,183 @@ func TestContext2Refresh_dataSourceOrphan(t *testing.T) { t.Fatal("orphaned data source instance should not be read") } } + +// Legacy providers may return invalid null values for blocks, causing noise in +// the diff output and unexpected behavior with ignore_changes. Make sure +// refresh fixes these up before storing the state. +func TestContext2Refresh_reifyNullBlock(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "test_resource" "foo" { +} +`, + }) + + p := new(testing_provider.MockProvider) + p.ReadResourceFn = func(req providers.ReadResourceRequest) providers.ReadResourceResponse { + // incorrectly return a null _set_block value + v := req.PriorState.AsValueMap() + v["set_block"] = cty.NullVal(v["set_block"].Type()) + return providers.ReadResourceResponse{NewState: cty.ObjectVal(v)} + } + + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + Provider: &configschema.Block{}, + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + }, + BlockTypes: map[string]*configschema.NestedBlock{ + "set_block": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "a": {Type: cty.String, Optional: true}, + }, + }, + Nesting: configschema.NestingSet, + }, + }, + }, + }, + }) + p.PlanResourceChangeFn = testDiffFn + + fooAddr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_resource", + Name: "foo", + }.Instance(addrs.NoKey) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + fooAddr, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo", "network_interface":[]}`), + Dependencies: []addrs.ConfigResource{}, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, state, &PlanOpts{Mode: plans.RefreshOnlyMode}) + if diags.HasErrors() { + t.Fatalf("refresh errors: %s", diags.Err()) + } + + jsonState := plan.PriorState.ResourceInstance(fooAddr.Absolute(addrs.RootModuleInstance)).Current.AttrsJSON + + // the set_block should still be an empty container, and not null + expected := `{"id":"foo","set_block":[]}` + if string(jsonState) != expected { + t.Fatalf("invalid state\nexpected: %s\ngot: %s\n", expected, jsonState) + } +} + +func TestContext2Refresh_identityUpgradeJSON(t *testing.T) { + m := testModule(t, "refresh-schema-upgrade") + p := testProvider("test") + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_thing": { + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + IdentityTypes: map[string]*configschema.Object{ + "test_thing": { + Attributes: map[string]*configschema.Attribute{ + "name": { + Type: cty.String, + Required: true, + }, + }, + Nesting: configschema.NestingSingle, + }, + }, + IdentityTypeSchemaVersions: map[string]uint64{ + "test_thing": 5, + }, + }) + p.UpgradeResourceIdentityResponse = &providers.UpgradeResourceIdentityResponse{ + UpgradedIdentity: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("foo"), + }), + } + + s := states.BuildState(func(s *states.SyncState) { + s.SetResourceInstanceCurrent( + addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_thing", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + SchemaVersion: 0, + AttrsJSON: []byte(`{"id":"foo"}`), + IdentitySchemaVersion: 3, + IdentityJSON: []byte(`{"id":"foo"}`), + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + }) + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + state, diags := ctx.Refresh(m, s, &PlanOpts{Mode: plans.NormalMode}) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + { + got := p.UpgradeResourceIdentityRequest + want := providers.UpgradeResourceIdentityRequest{ + TypeName: "test_thing", + Version: 3, + RawIdentityJSON: []byte(`{"id":"foo"}`), + } + if !cmp.Equal(got, want) { + t.Errorf("wrong identity upgrade request\n%s", cmp.Diff(want, got)) + } + } + + addr := mustResourceInstanceAddr("test_thing.bar") + res := state.ResourceInstance(addr) + if res == nil { + t.Fatalf("no resource in state for %s", addr) + } + + expectedIdentity := `{"name":"foo"}` + if string(res.Current.IdentityJSON) != expectedIdentity { + t.Fatalf("identity not updated in state\nexpected: %s\ngot: %s", expectedIdentity, res.Current.IdentityJSON) + } + expectedVersion := uint64(5) + if res.Current.IdentitySchemaVersion != expectedVersion { + t.Fatalf("identity schema version not updated in state\nexpected: %d\ngot: %d", expectedVersion, res.Current.IdentitySchemaVersion) + } +} diff --git a/internal/terraform/context_test.go b/internal/terraform/context_test.go index 12c376622b..358fb1e333 100644 --- a/internal/terraform/context_test.go +++ b/internal/terraform/context_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -13,6 +16,9 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/hashicorp/go-version" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configload" "github.com/hashicorp/terraform/internal/configs/configschema" @@ -20,12 +26,12 @@ import ( "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/planfile" "github.com/hashicorp/terraform/internal/providers" + provider_testing "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/states/statefile" "github.com/hashicorp/terraform/internal/tfdiags" tfversion "github.com/hashicorp/terraform/version" - "github.com/zclconf/go-cty/cty" ) var ( @@ -38,14 +44,12 @@ var ( func TestNewContextRequiredVersion(t *testing.T) { cases := []struct { Name string - Module string Version string Value string Err bool }{ { "no requirement", - "", "0.1.0", "", false, @@ -53,7 +57,6 @@ func TestNewContextRequiredVersion(t *testing.T) { { "doesn't match", - "", "0.1.0", "> 0.6.0", true, @@ -61,7 +64,6 @@ func TestNewContextRequiredVersion(t *testing.T) { { "matches", - "", "0.7.0", "> 0.6.0", false, @@ -69,7 +71,6 @@ func TestNewContextRequiredVersion(t *testing.T) { { "prerelease doesn't match with inequality", - "", "0.8.0", "> 0.7.0-beta", true, @@ -77,27 +78,10 @@ func TestNewContextRequiredVersion(t *testing.T) { { "prerelease doesn't match with equality", - "", "0.7.0", "0.7.0-beta", true, }, - - { - "module matches", - "context-required-version-module", - "0.5.0", - "", - false, - }, - - { - "module doesn't match", - "context-required-version-module", - "0.4.0", - "", - true, - }, } for i, tc := range cases { @@ -107,11 +91,7 @@ func TestNewContextRequiredVersion(t *testing.T) { tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) defer func() { tfversion.SemVer = old }() - name := "context-required-version" - if tc.Module != "" { - name = tc.Module - } - mod := testModule(t, name) + mod := testModule(t, "context-required-version") if tc.Value != "" { constraint, err := version.NewConstraint(tc.Value) if err != nil { @@ -126,7 +106,66 @@ func TestNewContextRequiredVersion(t *testing.T) { t.Fatalf("unexpected NewContext errors: %s", diags.Err()) } - diags = c.Validate(mod) + diags = c.Validate(mod, nil) + if diags.HasErrors() != tc.Err { + t.Fatalf("err: %s", diags.Err()) + } + }) + } +} + +func TestNewContextRequiredVersion_child(t *testing.T) { + mod := testModuleInline(t, map[string]string{ + "main.tf": ` +module "child" { + source = "./child" +} +`, + "child/main.tf": ` +terraform {} +`, + }) + + cases := map[string]struct { + Version string + Constraint string + Err bool + }{ + "matches": { + "0.5.0", + ">= 0.5.0", + false, + }, + "doesn't match": { + "0.4.0", + ">= 0.5.0", + true, + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + // Reset the version for the tests + old := tfversion.SemVer + tfversion.SemVer = version.Must(version.NewVersion(tc.Version)) + defer func() { tfversion.SemVer = old }() + + if tc.Constraint != "" { + constraint, err := version.NewConstraint(tc.Constraint) + if err != nil { + t.Fatalf("can't parse %q as version constraint", tc.Constraint) + } + child := mod.Children["child"] + child.Module.CoreVersionConstraints = append(child.Module.CoreVersionConstraints, configs.VersionConstraint{ + Required: constraint, + }) + } + c, diags := NewContext(&ContextOpts{}) + if diags.HasErrors() { + t.Fatalf("unexpected NewContext errors: %s", diags.Err()) + } + + diags = c.Validate(mod, nil) if diags.HasErrors() != tc.Err { t.Fatalf("err: %s", diags.Err()) } @@ -136,7 +175,7 @@ func TestNewContextRequiredVersion(t *testing.T) { func TestContext_missingPlugins(t *testing.T) { ctx, diags := NewContext(&ContextOpts{}) - assertNoDiagnostics(t, diags) + tfdiags.AssertNoDiagnostics(t, diags) configSrc := ` terraform { @@ -164,6 +203,29 @@ resource "implicit_thing" "b" { "main.tf": configSrc, }) + state := states.BuildState(func(state *states.SyncState) { + state.SetResourceInstanceCurrent(addrs.AbsResourceInstance{ + Resource: addrs.ResourceInstance{ + Resource: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "implicit3_thing", + Name: "c", + }, + }, + }, + &states.ResourceInstanceObjectSrc{ + AttrsJSON: mustParseJson(map[string]interface{}{}), + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Type: "implicit3", + Namespace: "hashicorp", + Hostname: "registry.terraform.io", + }, + }) + }) + // Validate and Plan are the two entry points where we explicitly verify // the available plugins match what the configuration needs. For other // operations we typically fail more deeply in Terraform Core, with @@ -171,8 +233,8 @@ resource "implicit_thing" "b" { // require doing some pretty weird things that aren't common enough to // be worth the complexity to check for them. - validateDiags := ctx.Validate(cfg) - _, planDiags := ctx.Plan(cfg, nil, DefaultPlanOpts) + validateDiags := ctx.Validate(cfg, nil) + _, planDiags := ctx.Plan(cfg, state, DefaultPlanOpts) tests := map[string]tfdiags.Diagnostics{ "validate": validateDiags, @@ -209,12 +271,69 @@ resource "implicit_thing" "b" { `This configuration requires provisioner plugin "nonexist", which isn't available. If you're intending to use an external provisioner plugin, you must install it manually into one of the plugin search directories before running Terraform.`, ), ) - assertDiagnosticsMatch(t, gotDiags, wantDiags) + + if testName == "plan" { + // the plan also validates the state + wantDiags = wantDiags.Append( + tfdiags.Sourceless( + tfdiags.Error, + "Missing required provider", + "This state requires provider registry.terraform.io/hashicorp/implicit3, but that provider isn't available. You may be able to install it automatically by running:\n terraform init", + ), + ) + } + tfdiags.AssertDiagnosticsMatch(t, gotDiags, wantDiags) }) } } -func testContext2(t *testing.T, opts *ContextOpts) *Context { +func TestContext_preloadedProviderSchemas(t *testing.T) { + var provider *provider_testing.MockProvider + { + var diags tfdiags.Diagnostics + diags = diags.Append(fmt.Errorf("mustn't really call GetProviderSchema")) + provider = &provider_testing.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Diagnostics: diags, + }, + } + } + + tfCore, err := NewContext(&ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewBuiltInProvider("blep"): func() (providers.Interface, error) { + return provider, nil + }, + }, + PreloadedProviderSchemas: map[addrs.Provider]providers.ProviderSchema{ + addrs.NewBuiltInProvider("blep"): providers.ProviderSchema{}, + }, + }) + if err != nil { + t.Fatal(err) + } + + cfg := testModuleInline(t, map[string]string{ + "main.tf": ` + terraform { + required_providers { + blep = { + source = "terraform.io/builtin/blep" + } + } + } + provider "blep" {} + `, + }) + _, diags := tfCore.Schemas(cfg, states.NewState()) + tfdiags.AssertNoDiagnostics(t, diags) + + if provider.GetProviderSchemaCalled { + t.Error("called GetProviderSchema even though a preloaded schema was provided") + } +} + +func testContext2(t testing.TB, opts *ContextOpts) *Context { t.Helper() ctx, diags := NewContext(opts) @@ -327,8 +446,8 @@ func testDiffFn(req providers.PlanResourceChangeRequest) (resp providers.PlanRes return } -func testProvider(prefix string) *MockProvider { - p := new(MockProvider) +func testProvider(prefix string) *provider_testing.MockProvider { + p := new(provider_testing.MockProvider) p.GetProviderSchemaResponse = testProviderSchema(prefix) return p @@ -390,7 +509,7 @@ func testCheckDeadlock(t *testing.T, f func()) { } func testProviderSchema(name string) *providers.GetProviderSchemaResponse { - return getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + return getProviderSchemaResponseFromProviderSchema(&providerSchema{ Provider: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "region": { @@ -715,7 +834,7 @@ func contextOptsForPlanViaFile(t *testing.T, configSnap *configload.Snapshot, pl // new plan and state types, and should not be used in new tests. Instead, use // a library like "cmp" to do a deep equality check and diff on the two // data structures. -func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { +func legacyPlanComparisonString(state *states.State, changes *plans.ChangesSrc) string { return fmt.Sprintf( "DIFF:\n\n%s\n\nSTATE:\n\n%s", legacyDiffComparisonString(changes), @@ -730,7 +849,7 @@ func legacyPlanComparisonString(state *states.State, changes *plans.Changes) str // This is here only for compatibility with existing tests that predate our // new plan types, and should not be used in new tests. Instead, use a library // like "cmp" to do a deep equality check and diff on the two data structures. -func legacyDiffComparisonString(changes *plans.Changes) string { +func legacyDiffComparisonString(changes *plans.ChangesSrc) string { // The old string representation of a plan was grouped by module, but // our new plan structure is not grouped in that way and so we'll need // to preprocess it in order to produce that grouping. @@ -895,80 +1014,56 @@ func legacyDiffComparisonString(changes *plans.Changes) string { return buf.String() } -// assertNoDiagnostics fails the test in progress (using t.Fatal) if the given -// diagnostics is non-empty. -func assertNoDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { - t.Helper() - if len(diags) == 0 { - return - } - logDiagnostics(t, diags) - t.FailNow() -} - -// assertNoDiagnostics fails the test in progress (using t.Fatal) if the given -// diagnostics has any errors. -func assertNoErrors(t *testing.T, diags tfdiags.Diagnostics) { - t.Helper() - if !diags.HasErrors() { - return - } - logDiagnostics(t, diags) - t.FailNow() -} - -// assertDiagnosticsMatch fails the test in progress (using t.Fatal) if the -// two sets of diagnostics don't match after being normalized using the -// "ForRPC" processing step, which eliminates the specific type information -// and HCL expression information of each diagnostic. +// checkPlanCompleteAndApplyable reports testing errors if the plan is not +// flagged as being both complete and applyable. // -// assertDiagnosticsMatch sorts the two sets of diagnostics in the usual way -// before comparing them, though diagnostics only have a partial order so that -// will not totally normalize the ordering of all diagnostics sets. -func assertDiagnosticsMatch(t *testing.T, got, want tfdiags.Diagnostics) { - got = got.ForRPC() - want = want.ForRPC() - got.Sort() - want.Sort() - if diff := cmp.Diff(want, got); diff != "" { - t.Fatalf("wrong diagnostics\n%s", diff) +// It does not prevent the test from continuing if those flags are not +// set, since downstream test assertions are likely to add extra context +// as to what went wrong. +func checkPlanCompleteAndApplyable(t *testing.T, plan *plans.Plan) { + t.Helper() + checkPlanComplete(t, plan) + checkPlanApplyable(t, plan) +} + +// checkPlanComplete reports a testing error if the plan is not flagged +// as being "complete". +// +// It does not prevent the test from continuing if that flag is not +// set, since downstream test assertions are likely to add extra context +// as to what went wrong. +func checkPlanComplete(t *testing.T, plan *plans.Plan) { + t.Helper() + if !plan.Complete { + t.Error("plan is incomplete; should be complete") } } -// logDiagnostics is a test helper that logs the given diagnostics to to the -// given testing.T using t.Log, in a way that is hopefully useful in debugging -// a test. It does not generate any errors or fail the test. See -// assertNoDiagnostics and assertNoErrors for more specific helpers that can -// also fail the test. -func logDiagnostics(t *testing.T, diags tfdiags.Diagnostics) { +// checkPlanApplyable reports a testing error if the plan is not flagged +// as being "applyable". +// +// It does not prevent the test from continuing if that flag is not +// set, since downstream test assertions are likely to add extra context +// as to what went wrong. +func checkPlanApplyable(t *testing.T, plan *plans.Plan) { t.Helper() - for _, diag := range diags { - desc := diag.Description() - rng := diag.Source() + if !plan.Applyable { + t.Error("plan is not applyable; should be applyable") + } +} - var severity string - switch diag.Severity() { - case tfdiags.Error: - severity = "ERROR" - case tfdiags.Warning: - severity = "WARN" - default: - severity = "???" // should never happen - } - - if subj := rng.Subject; subj != nil { - if desc.Detail == "" { - t.Logf("[%s@%s] %s", severity, subj.StartString(), desc.Summary) - } else { - t.Logf("[%s@%s] %s: %s", severity, subj.StartString(), desc.Summary, desc.Detail) - } - } else { - if desc.Detail == "" { - t.Logf("[%s] %s", severity, desc.Summary) - } else { - t.Logf("[%s] %s: %s", severity, desc.Summary, desc.Detail) - } - } +// checkPlanErrored reports a testing error if the plan is not flagged +// as "errored" and non-applyable. +// +// It does not prevent the test from continuing if those flags are not +// set, since downstream test assertions are likely to add extra context +// as to what went wrong. +func checkPlanErrored(t *testing.T, plan *plans.Plan) { + t.Helper() + if !plan.Errored { + t.Error("plan is not marked as errored; should be") + } else if plan.Applyable { + t.Error("plan is applyable; plans with errors should never be applyable") } } diff --git a/internal/terraform/context_validate.go b/internal/terraform/context_validate.go index aad884442a..5d22330e75 100644 --- a/internal/terraform/context_validate.go +++ b/internal/terraform/context_validate.go @@ -1,15 +1,41 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "log" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) +// ValidateOpts are the various options the affect the details of how Terraform +// will validate a configuration. +type ValidateOpts struct { + // ExternalProviders are clients for pre-configured providers that are + // treated as being passed into the root module from the caller. This + // is equivalent to writing a "providers" argument inside a "module" + // block in the Terraform language, but for the root module the caller + // is written in Go rather than the Terraform language. + // + // Note, that while Terraform Core will not call ValidateProviderConfig or + // ConfigureProvider on any providers in this map, as with the other context + // functions, the Validate function never calls ConfigureProvider anyway. + // + // Normally, the validate function would call the ValidateProviderConfig + // function on the provider, but the config may rely on variables that are + // not available to this function. Therefore, it is the responsibility of + // the caller to ensure that the provider configurations are valid. + ExternalProviders map[addrs.RootProviderConfig]providers.Interface +} + // Validate performs semantic validation of a configuration, and returns // any warnings or errors. // @@ -21,11 +47,18 @@ import ( // such as root module input variables. However, the Plan function includes // all of the same checks as Validate, in addition to the other work it does // to consider the previous run state and the planning options. -func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { +// +// The opts can be nil, and the ExternalProviders field of the opts can be nil. +func (c *Context) Validate(config *configs.Config, opts *ValidateOpts) tfdiags.Diagnostics { defer c.acquireRun("validate")() var diags tfdiags.Diagnostics + if opts == nil { + // Just make sure we don't get any nil pointer exceptions later. + opts = &ValidateOpts{} + } + moreDiags := c.checkConfigDependencies(config) diags = diags.Append(moreDiags) // If required dependencies are not available then we'll bail early since @@ -35,6 +68,15 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { return diags } + // There are some validation checks that happen when loading the provider + // schemas, and we can catch them early to ensure we are in a position to + // handle any errors. + _, moreDiags = c.Schemas(config, nil) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return diags + } + log.Printf("[DEBUG] Building and walking validate graph") // Validate is to check if the given module is valid regardless of @@ -56,11 +98,13 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { } graph, moreDiags := (&PlanGraphBuilder{ - Config: config, - Plugins: c.plugins, - State: states.NewState(), - RootVariableValues: varValues, - Operation: walkValidate, + Config: config, + Plugins: c.plugins, + State: states.NewState(), + RootVariableValues: varValues, + Operation: walkValidate, + ExternalProviderConfigs: opts.ExternalProviders, + ImportTargets: c.findImportTargets(config), }).Build(addrs.RootModuleInstance) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -68,7 +112,9 @@ func (c *Context) Validate(config *configs.Config) tfdiags.Diagnostics { } walker, walkDiags := c.walk(graph, walkValidate, &graphWalkOpts{ - Config: config, + Config: config, + FunctionResults: lang.NewFunctionResultsTable(nil), + ExternalProviderConfigs: opts.ExternalProviders, }) diags = diags.Append(walker.NonFatalDiagnostics) diags = diags.Append(walkDiags) diff --git a/internal/terraform/context_validate_test.go b/internal/terraform/context_validate_test.go index 47e71f404f..179a7db677 100644 --- a/internal/terraform/context_validate_test.go +++ b/internal/terraform/context_validate_test.go @@ -1,16 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "errors" "fmt" + "path/filepath" "strings" "testing" + "github.com/google/go-cmp/cmp" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -18,7 +25,7 @@ import ( func TestContext2Validate_badCount(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{}, @@ -33,7 +40,7 @@ func TestContext2Validate_badCount(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -41,7 +48,7 @@ func TestContext2Validate_badCount(t *testing.T) { func TestContext2Validate_badResource_reference(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{}, @@ -56,7 +63,7 @@ func TestContext2Validate_badResource_reference(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -64,7 +71,7 @@ func TestContext2Validate_badResource_reference(t *testing.T) { func TestContext2Validate_badVar(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -82,7 +89,7 @@ func TestContext2Validate_badVar(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -114,7 +121,7 @@ func TestContext2Validate_computedVar(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "value": {Type: cty.String, Optional: true}, }, @@ -122,7 +129,7 @@ func TestContext2Validate_computedVar(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -132,7 +139,7 @@ func TestContext2Validate_computedVar(t *testing.T) { pt.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, "value": {Type: cty.String, Optional: true}, @@ -159,7 +166,7 @@ func TestContext2Validate_computedVar(t *testing.T) { return } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -173,7 +180,7 @@ func TestContext2Validate_computedInFunction(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "attr": {Type: cty.Number, Optional: true}, }, @@ -182,7 +189,7 @@ func TestContext2Validate_computedInFunction(t *testing.T) { }, DataSources: map[string]providers.Schema{ "aws_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "optional_attr": {Type: cty.String, Optional: true}, "computed": {Type: cty.String, Computed: true}, @@ -199,7 +206,7 @@ func TestContext2Validate_computedInFunction(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -213,14 +220,14 @@ func TestContext2Validate_countComputed(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, }, DataSources: map[string]providers.Schema{ "aws_data_source": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "compute": {Type: cty.String, Optional: true}, "value": {Type: cty.String, Computed: true}, @@ -237,7 +244,7 @@ func TestContext2Validate_countComputed(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -248,7 +255,7 @@ func TestContext2Validate_countNegative(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -261,7 +268,7 @@ func TestContext2Validate_countNegative(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -272,7 +279,7 @@ func TestContext2Validate_countVariable(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -287,7 +294,7 @@ func TestContext2Validate_countVariable(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -299,7 +306,7 @@ func TestContext2Validate_countVariableNoDefault(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -312,7 +319,7 @@ func TestContext2Validate_countVariableNoDefault(t *testing.T) { addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) - assertNoDiagnostics(t, diags) + tfdiags.AssertNoDiagnostics(t, diags) _, diags = c.Plan(m, nil, &PlanOpts{}) if !diags.HasErrors() { @@ -326,7 +333,7 @@ func TestContext2Validate_moduleBadOutput(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -341,7 +348,7 @@ func TestContext2Validate_moduleBadOutput(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -352,7 +359,7 @@ func TestContext2Validate_moduleGood(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -367,7 +374,7 @@ func TestContext2Validate_moduleGood(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -379,7 +386,7 @@ func TestContext2Validate_moduleBadResource(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -396,7 +403,7 @@ func TestContext2Validate_moduleBadResource(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -408,7 +415,7 @@ func TestContext2Validate_moduleDepsShouldNotCycle(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Optional: true}, }, @@ -423,7 +430,7 @@ func TestContext2Validate_moduleDepsShouldNotCycle(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -434,7 +441,7 @@ func TestContext2Validate_moduleProviderVar(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -442,7 +449,7 @@ func TestContext2Validate_moduleProviderVar(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -464,7 +471,7 @@ func TestContext2Validate_moduleProviderVar(t *testing.T) { return } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -475,7 +482,7 @@ func TestContext2Validate_moduleProviderInheritUnused(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -483,7 +490,7 @@ func TestContext2Validate_moduleProviderInheritUnused(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -505,7 +512,7 @@ func TestContext2Validate_moduleProviderInheritUnused(t *testing.T) { return } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -516,7 +523,7 @@ func TestContext2Validate_orphans(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, "num": {Type: cty.String, Optional: true}, @@ -544,7 +551,7 @@ func TestContext2Validate_orphans(t *testing.T) { } } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -555,7 +562,7 @@ func TestContext2Validate_providerConfig_bad(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -563,7 +570,7 @@ func TestContext2Validate_providerConfig_bad(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -580,7 +587,7 @@ func TestContext2Validate_providerConfig_bad(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if len(diags) != 1 { t.Fatalf("wrong number of diagnostics %d; want %d", len(diags), 1) } @@ -594,7 +601,7 @@ func TestContext2Validate_providerConfig_skippedEmpty(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -602,7 +609,7 @@ func TestContext2Validate_providerConfig_skippedEmpty(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -619,7 +626,7 @@ func TestContext2Validate_providerConfig_skippedEmpty(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("should not be called")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -630,7 +637,7 @@ func TestContext2Validate_providerConfig_good(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -638,7 +645,7 @@ func TestContext2Validate_providerConfig_good(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -651,7 +658,7 @@ func TestContext2Validate_providerConfig_good(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -665,7 +672,7 @@ func TestContext2Validate_requiredProviderConfig(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "required_attribute": {Type: cty.String, Required: true}, }, @@ -673,7 +680,7 @@ func TestContext2Validate_requiredProviderConfig(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{}, }, }, @@ -686,7 +693,7 @@ func TestContext2Validate_requiredProviderConfig(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -698,7 +705,7 @@ func TestContext2Validate_provisionerConfig_bad(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -722,7 +729,7 @@ func TestContext2Validate_provisionerConfig_bad(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -734,7 +741,7 @@ func TestContext2Validate_badResourceConnection(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -754,7 +761,7 @@ func TestContext2Validate_badResourceConnection(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) t.Log(diags.Err()) if !diags.HasErrors() { t.Fatalf("succeeded; want error") @@ -767,7 +774,7 @@ func TestContext2Validate_badProvisionerConnection(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -787,7 +794,7 @@ func TestContext2Validate_badProvisionerConnection(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) t.Log(diags.Err()) if !diags.HasErrors() { t.Fatalf("succeeded; want error") @@ -799,7 +806,7 @@ func TestContext2Validate_provisionerConfig_good(t *testing.T) { p := testProvider("aws") p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -807,7 +814,7 @@ func TestContext2Validate_provisionerConfig_good(t *testing.T) { }, ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -836,7 +843,7 @@ func TestContext2Validate_provisionerConfig_good(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -848,7 +855,7 @@ func TestContext2Validate_requiredVar(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "ami": {Type: cty.String, Optional: true}, }, @@ -861,7 +868,7 @@ func TestContext2Validate_requiredVar(t *testing.T) { addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), }, }) - assertNoDiagnostics(t, diags) + tfdiags.AssertNoDiagnostics(t, diags) // NOTE: This test has grown idiosyncratic because originally Terraform // would (optionally) check variables during validation, and then in @@ -884,7 +891,7 @@ func TestContext2Validate_resourceConfig_bad(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -902,7 +909,7 @@ func TestContext2Validate_resourceConfig_bad(t *testing.T) { Diagnostics: tfdiags.Diagnostics{}.Append(fmt.Errorf("bad")), } - diags := c.Validate(m) + diags := c.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -914,7 +921,7 @@ func TestContext2Validate_resourceConfig_good(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -928,7 +935,7 @@ func TestContext2Validate_resourceConfig_good(t *testing.T) { }, }) - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -939,7 +946,7 @@ func TestContext2Validate_tainted(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, "num": {Type: cty.String, Optional: true}, @@ -966,7 +973,7 @@ func TestContext2Validate_tainted(t *testing.T) { } } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -979,7 +986,7 @@ func TestContext2Validate_targetedDestroy(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, "num": {Type: cty.String, Optional: true}, @@ -1003,7 +1010,7 @@ func TestContext2Validate_targetedDestroy(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1015,7 +1022,7 @@ func TestContext2Validate_varRefUnknown(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -1035,7 +1042,7 @@ func TestContext2Validate_varRefUnknown(t *testing.T) { return providers.ValidateResourceConfigResponse{} } - c.Validate(m) + c.Validate(m, nil) // Input variables are always unknown during the validate walk, because // we're checking for validity of all possible input values. Validity @@ -1055,7 +1062,7 @@ func TestContext2Validate_interpolateVar(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "template_file": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "template": {Type: cty.String, Optional: true}, }, @@ -1071,7 +1078,7 @@ func TestContext2Validate_interpolateVar(t *testing.T) { UIInput: input, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1087,7 +1094,7 @@ func TestContext2Validate_interpolateComputedModuleVarDef(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "attr": {Type: cty.String, Optional: true}, }, @@ -1103,7 +1110,7 @@ func TestContext2Validate_interpolateComputedModuleVarDef(t *testing.T) { UIInput: input, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1123,7 +1130,7 @@ func TestContext2Validate_interpolateMap(t *testing.T) { UIInput: input, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1173,7 +1180,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1204,7 +1211,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1240,7 +1247,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1275,7 +1282,7 @@ output "root" { ctx := testContext2(t, &ContextOpts{}) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1298,7 +1305,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1328,7 +1335,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1358,7 +1365,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1387,7 +1394,7 @@ resource "test_instance" "bar" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1419,7 +1426,7 @@ resource "test_instance" "bar" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1443,7 +1450,7 @@ func TestContext2Validate_variableCustomValidationsFail(t *testing.T) { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1477,7 +1484,7 @@ variable "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error\ngot: %s", diags.Err().Error()) } @@ -1538,7 +1545,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -1565,7 +1572,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1595,7 +1602,7 @@ resource "aws_instance" "foo" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1676,7 +1683,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -1703,7 +1710,7 @@ output "out" { `, }) - diags := testContext2(t, &ContextOpts{}).Validate(m) + diags := testContext2(t, &ContextOpts{}).Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1741,7 +1748,7 @@ output "out" { `, }) - diags := testContext2(t, &ContextOpts{}).Validate(m) + diags := testContext2(t, &ContextOpts{}).Validate(m, nil) if !diags.HasErrors() { t.Fatal("succeeded; want errors") } @@ -1771,7 +1778,7 @@ resource "test_instance" "a" { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "test_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": {Type: cty.String, Computed: true}, }, @@ -1789,7 +1796,7 @@ resource "test_instance" "a" { addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.Err()) } @@ -1812,7 +1819,7 @@ func TestContext2Validate_sensitiveProvisionerConfig(t *testing.T) { p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "foo": {Type: cty.String, Optional: true}, }, @@ -1839,7 +1846,7 @@ func TestContext2Validate_sensitiveProvisionerConfig(t *testing.T) { return pr.ValidateProvisionerConfigResponse } - diags := c.Validate(m) + diags := c.Validate(m, nil) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } @@ -1849,8 +1856,8 @@ func TestContext2Validate_sensitiveProvisionerConfig(t *testing.T) { } func TestContext2Plan_validateMinMaxDynamicBlock(t *testing.T) { - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -1933,7 +1940,7 @@ resource "test_instance" "c" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2000,15 +2007,15 @@ resource "test_object" "t" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } } func TestContext2Plan_lookupMismatchedObjectTypes(t *testing.T) { - p := new(MockProvider) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "test_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2057,7 +2064,7 @@ output "out" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2090,7 +2097,7 @@ func TestContext2Validate_nonNullableVariableDefaultValidation(t *testing.T) { ctx := testContext2(t, &ContextOpts{}) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2098,7 +2105,7 @@ func TestContext2Validate_nonNullableVariableDefaultValidation(t *testing.T) { func TestContext2Validate_precondition_good(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2133,7 +2140,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2141,7 +2148,7 @@ resource "aws_instance" "test" { func TestContext2Validate_precondition_badCondition(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2176,7 +2183,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2187,7 +2194,7 @@ resource "aws_instance" "test" { func TestContext2Validate_precondition_badErrorMessage(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2222,7 +2229,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2233,7 +2240,7 @@ resource "aws_instance" "test" { func TestContext2Validate_postcondition_good(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2263,7 +2270,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2271,7 +2278,7 @@ resource "aws_instance" "test" { func TestContext2Validate_postcondition_badCondition(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2312,7 +2319,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2323,7 +2330,7 @@ resource "aws_instance" "test" { func TestContext2Validate_postcondition_badErrorMessage(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2353,7 +2360,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if !diags.HasErrors() { t.Fatalf("succeeded; want error") } @@ -2364,7 +2371,7 @@ resource "aws_instance" "test" { func TestContext2Validate_precondition_count(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2399,7 +2406,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2407,7 +2414,7 @@ resource "aws_instance" "test" { func TestContext2Validate_postcondition_forEach(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2442,7 +2449,7 @@ resource "aws_instance" "test" { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) if diags.HasErrors() { t.Fatal(diags.ErrWithWarnings()) } @@ -2450,7 +2457,7 @@ resource "aws_instance" "test" { func TestContext2Validate_deprecatedAttr(t *testing.T) { p := testProvider("aws") - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&ProviderSchema{ + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ ResourceTypes: map[string]*configschema.Block{ "aws_instance": { Attributes: map[string]*configschema.Attribute{ @@ -2476,9 +2483,614 @@ locals { }, }) - diags := ctx.Validate(m) + diags := ctx.Validate(m, nil) warn := diags.ErrWithWarnings().Error() if !strings.Contains(warn, `The attribute "foo" is deprecated`) { t.Fatalf("expected deprecated warning, got: %q\n", warn) } } + +func TestContext2Validate_unknownForEach(t *testing.T) { + p := testProvider("aws") + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "aws_instance" "test" { +} + +locals { + follow = { + (aws_instance.test.id): "follow" + } +} + +resource "aws_instance" "follow" { + for_each = local.follow +} + `, + }) + c := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): testProviderFuncFixed(p), + }, + }) + + diags := c.Validate(m, nil) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } +} + +func TestContext2Validate_providerContributedFunctions(t *testing.T) { + mockProvider := func() *testing_provider.MockProvider { + p := testProvider("test") + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + Functions: map[string]providers.FunctionDecl{ + "count_e": { + ReturnType: cty.Number, + Parameters: []providers.FunctionParam{ + { + Name: "string", + Type: cty.String, + }, + }, + }, + }, + } + p.CallFunctionFn = func(req providers.CallFunctionRequest) (resp providers.CallFunctionResponse) { + if req.FunctionName != "count_e" { + resp.Err = fmt.Errorf("incorrect function name %q", req.FunctionName) + return resp + } + if len(req.Arguments) != 1 { + resp.Err = fmt.Errorf("wrong number of arguments %d", len(req.Arguments)) + return resp + } + if req.Arguments[0].Type() != cty.String { + resp.Err = fmt.Errorf("wrong argument type %#v", req.Arguments[0].Type()) + return resp + } + if !req.Arguments[0].IsKnown() { + resp.Err = fmt.Errorf("argument is unknown") + return resp + } + if req.Arguments[0].IsNull() { + resp.Err = fmt.Errorf("argument is null") + return resp + } + + str := req.Arguments[0].AsString() + count := strings.Count(str, "e") + resp.Result = cty.NumberIntVal(int64(count)) + return resp + } + return p + } + + t.Run("valid", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +locals { + result = provider::test::count_e("cheese") +} +output "result" { + value = local.result + precondition { + condition = (local.result == 3) + error_message = "Wrong number of Es in my cheese." + } +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + if !p.CallFunctionCalled { + t.Fatal("CallFunction was not called") + } + }) + t.Run("wrong name", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::cout_e("cheese") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), `Unknown provider function: The function "cout_e" is not available from the provider "test"`; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + + }) + t.Run("wrong namespace", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::toast::count_e("cheese") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), `Unknown provider function: There is no function named "provider::toast::count_e`; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("wrong argument type", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e([]) +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Invalid function argument: Invalid value for \"string\" parameter: string required."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("insufficient arguments", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e() +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Not enough function arguments: Function \"provider::test::count_e\" expects 1 argument(s). Missing value for \"string\"."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("too many arguments", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e("cheese", "louise") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Too many function arguments: Function \"provider::test::count_e\" expects only 1 argument(s)."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("unexpected null argument", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e(null) +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + if got, want := diags.Err().Error(), "Invalid function argument: Invalid value for \"string\" parameter: argument must not be null."; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) + t.Run("unhandled unknown argument", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} +output "result" { + value = provider::test::count_e(timestamp()) +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + // For this case, validation should succeed without calling the + // function yet, because the function doesn't declare that it handles + // unknown values and so we must defer validation until a later phase. + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + }) + t.Run("provider not declared", func(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + # Intentionally no declaration of local name "test" here + } +} +output "result" { + value = provider::test::count_e("cheese") +} +`, + }) + + p := mockProvider() + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + diags := ctx.Validate(m, nil) + if p.CallFunctionCalled { + t.Error("CallFunction was called, but should not have been") + } + if !diags.HasErrors() { + t.Fatal("unexpected success") + } + // Module author must declare a provider requirement in order to + // import a provider's functions. + if got, want := diags.Err().Error(), `Unknown provider function: There is no function named "provider::test::count_e"`; !strings.Contains(got, want) { + t.Errorf("wrong error message\nwant substring: %s\ngot: %s", want, got) + } + }) +} + +func TestContextValidate_externalProviders(t *testing.T) { + + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + bar = { + source = "hashicorp/bar" + } + } +} + +provider "bar" {} + +resource "bar_instance" "test" { + foo = "foo" # should be an int +} +`, + }) + + mustNotConfigure := func(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{ + Diagnostics: tfdiags.Diagnostics{ + tfdiags.Sourceless( + tfdiags.Error, + "Pre-configured provider was reconfigured by the modules runtime", + "An externally-configured provider should not have its ConfigureProvider function called during planning.", + ), + }, + } + } + + providerAddr := addrs.NewDefaultProvider("bar") + providerConfigAddr := addrs.RootProviderConfig{ + Provider: providerAddr, + } + + provider := &testing_provider.MockProvider{ + GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{ + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + // We have a required attribute that is not set, we're + // expecting this to not matter as we shouldn't validate + // the provider configuration as we're using an external + // provider. + "required": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + ResourceTypes: map[string]providers.Schema{ + "bar_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + // We should still validate this attribute as being + // incorrect, even though we have an external + // provider. + "foo": { + Type: cty.Number, + Required: true, + }, + }, + }, + }, + }, + }, + ConfigureProviderFn: mustNotConfigure, + } + + ctx, diags := NewContext(&ContextOpts{ + PreloadedProviderSchemas: map[addrs.Provider]providers.ProviderSchema{ + providerAddr: *provider.GetProviderSchemaResponse, + }, + }) + tfdiags.AssertNoDiagnostics(t, diags) + + // Many of the MockProvider methods check for this, so we'll set it to be + // true externally. + provider.ConfigureProviderCalled = true + + diags = ctx.Validate(m, &ValidateOpts{ + ExternalProviders: map[addrs.RootProviderConfig]providers.Interface{ + providerConfigAddr: provider, + }, + }) + + // We should have exactly one diagnostic, stating there was an error in the + // resource. But nothing complaining about the provider itself. + + if len(diags) != 1 { + t.Fatalf("expected exactly one diagnostic, got %d", len(diags)) + } + + if diff := cmp.Diff(diags[0].Description().Summary, "Incorrect attribute value type"); len(diff) > 0 { + t.Errorf("unexpected diagnostic summary: %s", diff) + } + if diff := cmp.Diff(diags[0].Description().Detail, "Inappropriate value for attribute \"foo\": a number is required."); len(diff) > 0 { + t.Errorf("unexpected diagnostic detail: %s", diff) + } +} + +func TestContext2Validate_providerSchemaError(t *testing.T) { + // validate module and output depends_on + m := testModuleInline(t, map[string]string{ + "main.tf": ` +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +output "foo" { + value = provider::test::func("foo") +} +`, + }) + + p := testProvider("test") + p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "test_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + }, + }, + }, + }, + Diagnostics: tfdiags.Diagnostics(nil).Append(errors.New("schema problem!")), + } + + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + diags := ctx.Validate(m, nil) + if !diags.HasErrors() { + t.Fatal("expected error") + } + + // while the function in the config doesn't exist, we should not have gotten + // that far and stopped at the schema error first + for _, d := range diags { + if detail := d.Description().Detail; !strings.Contains(detail, "schema problem!") { + t.Errorf("unexpected error: %s", detail) + } + } +} + +func TestContext2Validate_ephemeralOutput_root(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "main.tf": ` +variable "foo" { + ephemeral = true + default = "foo" +} +output "test" { + ephemeral = true + value = var.foo +} +`, + }) + + ctx := testContext2(t, &ContextOpts{}) + diags := ctx.Validate(m, &ValidateOpts{}) + var wantDiags tfdiags.Diagnostics + wantDiags = wantDiags.Append( + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral output not allowed", + Detail: "Ephemeral outputs are not allowed in context of a root module", + Subject: &hcl.Range{ + Filename: filepath.Join(m.Module.SourceDir, "main.tf"), + Start: hcl.Pos{Line: 6, Column: 1, Byte: 59}, + End: hcl.Pos{Line: 6, Column: 14, Byte: 72}, + }, + }, + ) + tfdiags.AssertDiagnosticsMatch(t, diags, wantDiags) +} + +func TestContext2Validate_ephemeralOutput_child(t *testing.T) { + m := testModuleInline(t, map[string]string{ + "child/main.tf": ` +variable "child-eph" { + ephemeral = true +} +output "out" { + ephemeral = true + value = var.child-eph +}`, + "main.tf": ` +variable "eph" { + ephemeral = true + default = "foo" +} + +module "child" { + source = "./child" + child-eph = var.eph +} +`, + }) + + ctx := testContext2(t, &ContextOpts{}) + diags := ctx.Validate(m, &ValidateOpts{}) + tfdiags.AssertNoDiagnostics(t, diags) +} diff --git a/internal/terraform/context_walk.go b/internal/terraform/context_walk.go index 523fa73854..b414603270 100644 --- a/internal/terraform/context_walk.go +++ b/internal/terraform/context_walk.go @@ -1,13 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "log" + "time" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/refactoring" + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -24,6 +35,26 @@ type graphWalkOpts struct { Changes *plans.Changes Config *configs.Config + // ExternalProviderConfigs is used for walks that make use of configured + // providers (e.g. plan and apply) to satisfy situations where the root + // module itself declares that it expects to have providers passed in + // from outside. + // + // This should not be populated for walks that use only unconfigured + // providers, such as validate. Populating it for those walks might cause + // strange things to happen, because our graph walking machinery doesn't + // always take into account what walk type it's dealing with. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + + // DeferralAlowed indicates that the current runtime supports deferred actions. + DeferralAllowed bool + + // ExternalDependencyDeferred indicates that something that this entire + // configuration depends on (outside the view of this modules runtime) + // has deferred changes, and therefore we must treat _all_ actions + // as deferred to produce the correct overall dependency ordering. + ExternalDependencyDeferred bool + // PlanTimeCheckResults should be populated during the apply phase with // the snapshot of check results that was generated during the plan step. // @@ -32,13 +63,28 @@ type graphWalkOpts struct { // the module and resource expansion. PlanTimeCheckResults *states.CheckResults + // PlanTimeTimestamp should be populated during the plan phase by retrieving + // the current UTC timestamp, and should be read from the plan file during + // the apply phase. + PlanTimeTimestamp time.Time + + // Overrides contains the set of overrides we should apply during this + // operation. + Overrides *mocking.Overrides + MoveResults refactoring.MoveResults + + FunctionResults *lang.FunctionResults + + // Forget if set to true will cause the plan to forget all resources. This is + // only allowd in the context of a destroy plan. + Forget bool } func (c *Context) walk(graph *Graph, operation walkOperation, opts *graphWalkOpts) (*ContextGraphWalker, tfdiags.Diagnostics) { log.Printf("[DEBUG] Starting graph walk: %s", operation.String()) - walker := c.graphWalker(operation, opts) + walker := c.graphWalker(graph, operation, opts) // Watch for a stop so we can call the provider Stop() API. watchStop, watchWait := c.watchStop(walker) @@ -53,7 +99,7 @@ func (c *Context) walk(graph *Graph, operation walkOperation, opts *graphWalkOpt return walker, diags } -func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *ContextGraphWalker { +func (c *Context) graphWalker(graph *Graph, operation walkOperation, opts *graphWalkOpts) *ContextGraphWalker { var state *states.SyncState var refreshState *states.SyncState var prevRunState *states.SyncState @@ -128,17 +174,30 @@ func (c *Context) graphWalker(operation walkOperation, opts *graphWalkOpts) *Con } } + deferred := deferring.NewDeferred(opts.DeferralAllowed) + if opts.ExternalDependencyDeferred { + deferred.SetExternalDependencyDeferred() + } + return &ContextGraphWalker{ - Context: c, - State: state, - Config: opts.Config, - RefreshState: refreshState, - PrevRunState: prevRunState, - Changes: changes.SyncWrapper(), - Checks: checkState, - InstanceExpander: instances.NewExpander(), - MoveResults: opts.MoveResults, - Operation: operation, - StopContext: c.runContext, + Context: c, + State: state, + Config: opts.Config, + RefreshState: refreshState, + Overrides: opts.Overrides, + PrevRunState: prevRunState, + Changes: changes.SyncWrapper(), + NamedValues: namedvals.NewState(), + EphemeralResources: ephemeral.NewResources(), + Deferrals: deferred, + Checks: checkState, + InstanceExpander: instances.NewExpander(opts.Overrides), + ExternalProviderConfigs: opts.ExternalProviderConfigs, + MoveResults: opts.MoveResults, + Operation: operation, + StopContext: c.runContext, + PlanTimestamp: opts.PlanTimeTimestamp, + functionResults: opts.FunctionResults, + Forget: opts.Forget, } } diff --git a/internal/terraform/diagnostics.go b/internal/terraform/diagnostics.go index 26f22f06ce..ac1ff905fe 100644 --- a/internal/terraform/diagnostics.go +++ b/internal/terraform/diagnostics.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -24,6 +27,23 @@ func (e diagnosticCausedByUnknown) DiagnosticCausedByUnknown() bool { return bool(e) } +// DiagnosticCausedByEphemeral is an implementation of +// tfdiags.DiagnosticExtraBecauseEphemeral which we can use in the "Extra" field +// of a diagnostic to indicate that the problem was caused by ephemeral values +// being involved in an expression evaluation. +// +// When using this, set the Extra to DiagnosticCausedByEphemeral(true) and also +// populate the EvalContext and Expression fields of the diagnostic so that +// the diagnostic renderer can use all of that information together to assist +// the user in understanding what was ephemeral. +type DiagnosticCausedByEphemeral bool + +var _ tfdiags.DiagnosticExtraBecauseEphemeral = DiagnosticCausedByEphemeral(true) + +func (e DiagnosticCausedByEphemeral) DiagnosticCausedByEphemeral() bool { + return bool(e) +} + // diagnosticCausedBySensitive is an implementation of // tfdiags.DiagnosticExtraBecauseSensitive which we can use in the "Extra" field // of a diagnostic to indicate that the problem was caused by sensitive values diff --git a/internal/terraform/eval_conditions.go b/internal/terraform/eval_conditions.go index 690188a670..6a221339e4 100644 --- a/internal/terraform/eval_conditions.go +++ b/internal/terraform/eval_conditions.go @@ -1,9 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" - "strings" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" @@ -14,7 +16,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" - "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -27,7 +29,7 @@ import ( // // If any of the rules do not pass, the returned diagnostics will contain // errors. Otherwise, it will either be empty or contain only warnings. -func evalCheckRules(typ addrs.CheckType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, diagSeverity tfdiags.Severity) tfdiags.Diagnostics { +func evalCheckRules(typ addrs.CheckRuleType, rules []*configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, diagSeverity tfdiags.Severity) tfdiags.Diagnostics { var diags tfdiags.Diagnostics checkState := ctx.Checks() @@ -48,7 +50,7 @@ func evalCheckRules(typ addrs.CheckType, rules []*configs.CheckRule, ctx EvalCon severity := diagSeverity.ToHCL() for i, rule := range rules { - result, ruleDiags := evalCheckRule(typ, rule, ctx, self, keyData, severity) + result, ruleDiags := evalCheckRule(addrs.NewCheckRule(self, typ, i), rule, ctx, keyData, severity) diags = diags.Append(ruleDiags) log.Printf("[TRACE] evalCheckRules: %s status is now %s", self, result.Status) @@ -67,45 +69,79 @@ type checkResult struct { FailureMessage string } -func evalCheckRule(typ addrs.CheckType, rule *configs.CheckRule, ctx EvalContext, self addrs.Checkable, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) { +func validateCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData) (string, *hcl.EvalContext, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - const errInvalidCondition = "Invalid condition result" - refs, moreDiags := lang.ReferencesInExpr(rule.Condition) + refs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRef, rule.Condition) diags = diags.Append(moreDiags) - moreRefs, moreDiags := lang.ReferencesInExpr(rule.ErrorMessage) + moreRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage) diags = diags.Append(moreDiags) refs = append(refs, moreRefs...) - var selfReference addrs.Referenceable - // Only resource postconditions can refer to self - if typ == addrs.ResourcePostcondition { - switch s := self.(type) { + var selfReference, sourceReference addrs.Referenceable + switch addr.Type { + case addrs.ResourcePostcondition: + switch s := addr.Container.(type) { case addrs.AbsResourceInstance: + // Only resource postconditions can refer to self selfReference = s.Resource default: - panic(fmt.Sprintf("Invalid self reference type %t", self)) + panic(fmt.Sprintf("Invalid self reference type %t", addr.Container)) + } + case addrs.CheckAssertion: + switch s := addr.Container.(type) { + case addrs.AbsCheck: + // Only check blocks have scoped resources so need to specify their + // source. + sourceReference = s.Check + default: + panic(fmt.Sprintf("Invalid source reference type %t", addr.Container)) } } - scope := ctx.EvaluationScope(selfReference, keyData) + scope := ctx.EvaluationScope(selfReference, sourceReference, keyData) hclCtx, moreDiags := scope.EvalContext(refs) diags = diags.Append(moreDiags) - resultVal, hclDiags := rule.Condition.Value(hclCtx) - diags = diags.Append(hclDiags) + errorMessage, moreDiags := lang.EvalCheckErrorMessage(rule.ErrorMessage, hclCtx, &addr) + diags = diags.Append(moreDiags) + return errorMessage, hclCtx, diags +} + +func evalCheckRule(addr addrs.CheckRule, rule *configs.CheckRule, ctx EvalContext, keyData instances.RepetitionData, severity hcl.DiagnosticSeverity) (checkResult, tfdiags.Diagnostics) { // NOTE: Intentionally not passing the caller's selected severity in here, // because this reports errors in the configuration itself, not the failure // of an otherwise-valid condition. - errorMessage, moreDiags := evalCheckErrorMessage(rule.ErrorMessage, hclCtx) - diags = diags.Append(moreDiags) + errorMessage, hclCtx, diags := validateCheckRule(addr, rule, ctx, keyData) + + const errInvalidCondition = "Invalid condition result" + + resultVal, hclDiags := rule.Condition.Value(hclCtx) + diags = diags.Append(hclDiags) if diags.HasErrors() { - log.Printf("[TRACE] evalCheckRule: %s: %s", typ, diags.Err().Error()) + log.Printf("[TRACE] evalCheckRule: %s: %s", addr.Type, diags.Err().Error()) + return checkResult{Status: checks.StatusError}, diags } if !resultVal.IsKnown() { + + // Check assertions warn if a status is unknown. + if addr.Type == addrs.CheckAssertion { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: fmt.Sprintf("%s known after apply", addr.Type.Description()), + Detail: "The condition could not be evaluated at this time, a result will be known when this plan is applied.", + Subject: rule.Condition.Range().Ptr(), + Expression: rule.Condition, + EvalContext: hclCtx, + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addr, + }, + }) + } + // We'll wait until we've learned more, then. return checkResult{Status: checks.StatusUnknown}, diags } @@ -160,11 +196,14 @@ func evalCheckRule(typ addrs.CheckType, rule *configs.CheckRule, ctx EvalContext // treat condition failures as warnings in the presence of // certain special planning options. Severity: severity, - Summary: fmt.Sprintf("%s failed", typ.Description()), + Summary: fmt.Sprintf("%s failed", addr.Type.Description()), Detail: errorMessageForDiags, Subject: rule.Condition.Range().Ptr(), Expression: rule.Condition, EvalContext: hclCtx, + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addr, + }, }) return checkResult{ @@ -172,66 +211,3 @@ func evalCheckRule(typ addrs.CheckType, rule *configs.CheckRule, ctx EvalContext FailureMessage: errorMessage, }, diags } - -// evalCheckErrorMessage makes a best effort to evaluate the given expression, -// as an error message string. -// -// It will either return a non-empty message string or it'll return diagnostics -// with either errors or warnings that explain why the given expression isn't -// acceptable. -func evalCheckErrorMessage(expr hcl.Expression, hclCtx *hcl.EvalContext) (string, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - - val, hclDiags := expr.Value(hclCtx) - diags = diags.Append(hclDiags) - if hclDiags.HasErrors() { - return "", diags - } - - val, err := convert.Convert(val, cty.String) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid error message", - Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)), - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, - }) - return "", diags - } - if !val.IsKnown() { - return "", diags - } - if val.IsNull() { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid error message", - Detail: "Unsuitable value for error message: must not be null.", - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, - }) - return "", diags - } - - val, valMarks := val.Unmark() - if _, sensitive := valMarks[marks.Sensitive]; sensitive { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Error message refers to sensitive values", - Detail: `The error expression used to explain this condition refers to sensitive values, so Terraform will not display the resulting message. - -You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`, - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, - }) - return "", diags - } - - // NOTE: We've discarded any other marks the string might have been carrying, - // aside from the sensitive mark. - - return strings.TrimSpace(val.AsString()), diags -} diff --git a/internal/terraform/eval_context.go b/internal/terraform/eval_context.go index fedf223051..bde1626ca3 100644 --- a/internal/terraform/eval_context.go +++ b/internal/terraform/eval_context.go @@ -1,26 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "context" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) +type hookFunc func(func(Hook) (HookAction, error)) error + // EvalContext is the interface that is given to eval nodes to execute. type EvalContext interface { - // Stopped returns a channel that is closed when evaluation is stopped - // via Terraform.Context.Stop() - Stopped() <-chan struct{} + // Stopped returns a context that is canceled when evaluation is stopped via + // Terraform.Context.Stop() + StopCtx() context.Context // Path is the current module path. Path() addrs.ModuleInstance @@ -38,7 +52,7 @@ type EvalContext interface { // It is an error to initialize the same provider more than once. This // method will panic if the module instance address of the given provider // configuration does not match the Path() of the EvalContext. - InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) + InitProvider(addr addrs.AbsProviderConfig, configs *configs.Provider) (providers.Interface, error) // Provider gets the provider instance with the given address (already // initialized) or returns nil if the provider isn't initialized. @@ -54,7 +68,7 @@ type EvalContext interface { // // This method expects an _absolute_ provider configuration address, since // resources in one module are able to use providers from other modules. - ProviderSchema(addrs.AbsProviderConfig) (*ProviderSchema, error) + ProviderSchema(addrs.AbsProviderConfig) (providers.ProviderSchema, error) // CloseProvider closes provider connections that aren't needed anymore. // @@ -87,8 +101,8 @@ type EvalContext interface { // InitProvisioner. ProvisionerSchema(string) (*configschema.Block, error) - // CloseProvisioner closes all provisioner plugins. - CloseProvisioners() error + // ClosePlugins closes all cached provisioner and provider plugins. + ClosePlugins() error // EvaluateBlock takes the given raw configuration block and associated // schema and evaluates it to produce a value of an object type that @@ -125,37 +139,21 @@ type EvalContext interface { // EvaluationScope returns a scope that can be used to evaluate reference // addresses in this context. - EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope + EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope - // SetRootModuleArgument defines the value for one variable of the root - // module. The caller must ensure that given value is a suitable - // "final value" for the variable, which means that it's already converted - // and validated to match any configured constraints and validation rules. - // - // Calling this function multiple times with the same variable address - // will silently overwrite the value provided by a previous call. - SetRootModuleArgument(addrs.InputVariable, cty.Value) + // LanguageExperimentActive returns true if the given experiment is + // active in the module associated with this EvalContext, or false + // otherwise. + LanguageExperimentActive(experiment experiments.Experiment) bool - // SetModuleCallArgument defines the value for one input variable of a - // particular child module call. The caller must ensure that the given - // value is a suitable "final value" for the variable, which means that - // it's already converted and validated to match any configured - // constraints and validation rules. - // - // Calling this function multiple times with the same variable address - // will silently overwrite the value provided by a previous call. - SetModuleCallArgument(addrs.ModuleCallInstance, addrs.InputVariable, cty.Value) + // EphemeralResources returns a helper object for tracking active + // instances of ephemeral resources declared in the configuration. + EphemeralResources() *ephemeral.Resources - // GetVariableValue returns the value provided for the input variable with - // the given address, or cty.DynamicVal if the variable hasn't been assigned - // a value yet. - // - // Most callers should deal with variable values only indirectly via - // EvaluationScope and the other expression evaluation functions, but - // this is provided because variables tend to be evaluated outside of - // the context of the module they belong to and so we sometimes need to - // override the normal expression evaluation behavior. - GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value + // NamedValues returns the object that tracks the gradual evaluation of + // all input variables, local values, and output values during a graph + // walk. + NamedValues() *namedvals.State // Changes returns the writer object that can be used to write new proposed // changes into the global changes set. @@ -188,6 +186,15 @@ type EvalContext interface { // EvalContext objects for a given configuration. InstanceExpander() *instances.Expander + // Deferrals returns a helper object for tracking deferred actions, which + // means that Terraform either cannot plan an action at all or cannot + // perform a planned action due to an upstream dependency being deferred. + Deferrals() *deferring.Deferred + + // ClientCapabilities returns the client capabilities sent to the providers + // for each request. They define what this terraform instance is capable of. + ClientCapabilities() providers.ClientCapabilities + // MoveResults returns a map describing the results of handling any // resource instance move statements prior to the graph walk, so that // the graph walk can then record that information appropriately in other @@ -198,7 +205,21 @@ type EvalContext interface { // objects accessible through it. MoveResults() refactoring.MoveResults - // WithPath returns a copy of the context with the internal path set to the - // path argument. - WithPath(path addrs.ModuleInstance) EvalContext + // Overrides contains the modules and resources we should mock as part of + // this execution. + Overrides() *mocking.Overrides + + // withScope derives a new EvalContext that has all of the same global + // context, but a new evaluation scope. + withScope(scope evalContextScope) EvalContext + + // Forget if set to true will cause the plan to forget all resources. This is + // only allowed in the context of a destroy plan. + Forget() bool +} + +func evalContextForModuleInstance(baseCtx EvalContext, addr addrs.ModuleInstance) EvalContext { + return baseCtx.withScope(evalContextModuleInstance{ + Addr: addr, + }) } diff --git a/internal/terraform/eval_context_builtin.go b/internal/terraform/eval_context_builtin.go index d66b9dd080..6a35b2e902 100644 --- a/internal/terraform/eval_context_builtin.go +++ b/internal/terraform/eval_context_builtin.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,91 +9,106 @@ import ( "log" "sync" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" - "github.com/hashicorp/terraform/version" - + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/tfdiags" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/version" ) // BuiltinEvalContext is an EvalContext implementation that is used by // Terraform by default. type BuiltinEvalContext struct { + // scope is the scope (module instance or set of possible module instances) + // that this context is operating within. + // + // Note: this can be evalContextGlobal (i.e. nil) when visiting a graph + // node that doesn't belong to a particular module, in which case any + // method using it will panic. + scope evalContextScope + // StopContext is the context used to track whether we're complete StopContext context.Context - // PathValue is the Path that this context is operating within. - PathValue addrs.ModuleInstance - - // pathSet indicates that this context was explicitly created for a - // specific path, and can be safely used for evaluation. This lets us - // differentiate between PathValue being unset, and the zero value which is - // equivalent to RootModuleInstance. Path and Evaluation methods will - // panic if this is not set. - pathSet bool - // Evaluator is used for evaluating expressions within the scope of this // eval context. Evaluator *Evaluator - // VariableValues contains the variable values across all modules. This - // structure is shared across the entire containing context, and so it - // may be accessed only when holding VariableValuesLock. - // The keys of the first level of VariableValues are the string - // representations of addrs.ModuleInstance values. The second-level keys - // are variable names within each module instance. - VariableValues map[string]map[string]cty.Value - VariableValuesLock *sync.Mutex + // NamedValuesValue is where we keep the values of already-evaluated input + // variables, local values, and output values. + NamedValuesValue *namedvals.State // Plugins is a library of plugin components (providers and provisioners) // available for use during a graph walk. Plugins *contextPlugins - Hooks []Hook - InputValue UIInput - ProviderCache map[string]providers.Interface - ProviderInputConfig map[string]map[string]cty.Value - ProviderLock *sync.Mutex - ProvisionerCache map[string]provisioners.Interface - ProvisionerLock *sync.Mutex - ChangesValue *plans.ChangesSync - StateValue *states.SyncState - ChecksValue *checks.State - RefreshStateValue *states.SyncState - PrevRunStateValue *states.SyncState - InstanceExpanderValue *instances.Expander - MoveResultsValue refactoring.MoveResults + // ExternalProviderConfigs are pre-configured provider instances passed + // in by the caller, for situations like Stack components where the + // root module isn't designed to be planned and applied in isolation and + // instead expects to recieve certain provider configurations from the + // stack configuration. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + + // DeferralsValue is the object returned by [BuiltinEvalContext.Deferrals]. + DeferralsValue *deferring.Deferred + + // forget if set to true will cause the plan to forget all resources. This is + // only allowd in the context of a destroy plan. + forget bool + + Hooks []Hook + InputValue UIInput + ProviderCache map[string]providers.Interface + ProviderFuncCache map[string]providers.Interface + FunctionResults *lang.FunctionResults + ProviderInputConfig map[string]map[string]cty.Value + ProviderLock *sync.Mutex + ProvisionerCache map[string]provisioners.Interface + ProvisionerLock *sync.Mutex + ChangesValue *plans.ChangesSync + StateValue *states.SyncState + ChecksValue *checks.State + EphemeralResourcesValue *ephemeral.Resources + RefreshStateValue *states.SyncState + PrevRunStateValue *states.SyncState + InstanceExpanderValue *instances.Expander + MoveResultsValue refactoring.MoveResults + OverrideValues *mocking.Overrides } // BuiltinEvalContext implements EvalContext var _ EvalContext = (*BuiltinEvalContext)(nil) -func (ctx *BuiltinEvalContext) WithPath(path addrs.ModuleInstance) EvalContext { +func (ctx *BuiltinEvalContext) withScope(scope evalContextScope) EvalContext { newCtx := *ctx - newCtx.pathSet = true - newCtx.PathValue = path + newCtx.scope = scope return &newCtx } -func (ctx *BuiltinEvalContext) Stopped() <-chan struct{} { +func (ctx *BuiltinEvalContext) StopCtx() context.Context { // This can happen during tests. During tests, we just block forever. if ctx.StopContext == nil { - return nil + return context.TODO() } - return ctx.StopContext.Done() + return ctx.StopContext } func (ctx *BuiltinEvalContext) Hook(fn func(Hook) (HookAction, error)) error { @@ -117,7 +135,7 @@ func (ctx *BuiltinEvalContext) Input() UIInput { return ctx.InputValue } -func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) { +func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig, config *configs.Provider) (providers.Interface, error) { // If we already initialized, it is an error if p := ctx.Provider(addr); p != nil { return nil, fmt.Errorf("%s is already initialized", addr) @@ -130,12 +148,38 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (provi key := addr.String() + if addr.Module.IsRoot() { + rootAddr := addrs.RootProviderConfig{ + Provider: addr.Provider, + Alias: addr.Alias, + } + if external, isExternal := ctx.ExternalProviderConfigs[rootAddr]; isExternal { + // External providers should always be pre-configured by the + // external caller, and so we'll wrap them in a type that + // makes operations like ConfigureProvider and Close be no-op. + wrapped := externalProviderWrapper{external} + ctx.ProviderCache[key] = wrapped + return wrapped, nil + } + } + p, err := ctx.Plugins.NewProviderInstance(addr.Provider) if err != nil { return nil, err } log.Printf("[TRACE] BuiltinEvalContext: Initialized %q provider for %s", addr.String(), addr) + + // The config might be nil, if there was no config block defined for this + // provider. + if config != nil && config.Mock { + log.Printf("[TRACE] BuiltinEvalContext: Mocked %q provider for %s", addr.String(), addr) + p = &providers.Mock{ + Provider: p, + Data: config.MockData, + } + } + ctx.ProviderCache[key] = p return p, nil @@ -148,7 +192,7 @@ func (ctx *BuiltinEvalContext) Provider(addr addrs.AbsProviderConfig) providers. return ctx.ProviderCache[addr.String()] } -func (ctx *BuiltinEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (*ProviderSchema, error) { +func (ctx *BuiltinEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (providers.ProviderSchema, error) { return ctx.Plugins.ProviderSchema(addr.Provider) } @@ -180,19 +224,13 @@ func (ctx *BuiltinEvalContext) ConfigureProvider(addr addrs.AbsProviderConfig, c return diags } - providerSchema, err := ctx.ProviderSchema(addr) - if err != nil { - diags = diags.Append(fmt.Errorf("failed to read schema for %s: %s", addr, err)) - return diags - } - if providerSchema == nil { - diags = diags.Append(fmt.Errorf("schema for %s is not available", addr)) - return diags - } - req := providers.ConfigureProviderRequest{ TerraformVersion: version.String(), Config: cfg, + ClientCapabilities: providers.ClientCapabilities{ + DeferralAllowed: ctx.Deferrals().DeferralAllowed(), + WriteOnlyAttributesAllowed: true, + }, } resp := p.ConfigureProvider(req) @@ -253,7 +291,7 @@ func (ctx *BuiltinEvalContext) ProvisionerSchema(n string) (*configschema.Block, return ctx.Plugins.ProvisionerSchema(n) } -func (ctx *BuiltinEvalContext) CloseProvisioners() error { +func (ctx *BuiltinEvalContext) ClosePlugins() error { var diags tfdiags.Diagnostics ctx.ProvisionerLock.Lock() defer ctx.ProvisionerLock.Unlock() @@ -263,6 +301,17 @@ func (ctx *BuiltinEvalContext) CloseProvisioners() error { if err != nil { diags = diags.Append(fmt.Errorf("provisioner.Close %s: %s", name, err)) } + delete(ctx.ProvisionerCache, name) + } + + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + for name, prov := range ctx.ProviderFuncCache { + err := prov.Close() + if err != nil { + diags = diags.Append(fmt.Errorf("provider.Close %s: %s", name, err)) + } + delete(ctx.ProviderFuncCache, name) } return diags.Err() @@ -270,7 +319,7 @@ func (ctx *BuiltinEvalContext) CloseProvisioners() error { func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema.Block, self addrs.Referenceable, keyData InstanceKeyEvalData) (cty.Value, hcl.Body, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - scope := ctx.EvaluationScope(self, keyData) + scope := ctx.EvaluationScope(self, nil, keyData) body, evalDiags := scope.ExpandBlock(body, schema) diags = diags.Append(evalDiags) val, evalDiags := scope.EvalBlock(body, schema) @@ -279,7 +328,7 @@ func (ctx *BuiltinEvalContext) EvaluateBlock(body hcl.Body, schema *configschema } func (ctx *BuiltinEvalContext) EvaluateExpr(expr hcl.Expression, wantType cty.Type, self addrs.Referenceable) (cty.Value, tfdiags.Diagnostics) { - scope := ctx.EvaluationScope(self, EvalDataForNoInstanceKey) + scope := ctx.EvaluationScope(self, nil, EvalDataForNoInstanceKey) return scope.EvalExpr(expr, wantType) } @@ -291,7 +340,7 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r return nil, false, diags } - var changes []*plans.ResourceInstanceChangeSrc + var changes []*plans.ResourceInstanceChange // store the address once we get it for validation var resourceAddr addrs.Resource @@ -304,7 +353,7 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r case addrs.ResourceInstance: resourceAddr = sub.ContainingResource() rc := sub.Absolute(ctx.Path()) - change := ctx.Changes().GetResourceInstanceChange(rc, states.CurrentGen) + change := ctx.Changes().GetResourceInstanceChange(rc, addrs.NotDeposed) if change != nil { // we'll generate an error below if there was no change changes = append(changes, change) @@ -312,7 +361,7 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r } // Do some validation to make sure we are expecting a change at all - cfg := ctx.Evaluator.Config.Descendent(ctx.Path().Module()) + cfg := ctx.Evaluator.Config.Descendant(ctx.Path().Module()) resCfg := cfg.Module.ResourceByAddr(resourceAddr) if resCfg == nil { diags = diags.Append(&hcl.Diagnostic{ @@ -334,7 +383,7 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r // for any change. if len(ref.Remaining) == 0 { for _, c := range changes { - switch c.ChangeSrc.Action { + switch c.Change.Action { // Only immediate changes to the resource will trigger replacement. case plans.Update, plans.DeleteThenCreate, plans.CreateThenDelete: return ref, true, diags @@ -351,41 +400,16 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r // Make sure the change is actionable. A create or delete action will have // a change in value, but are not valid for our purposes here. - switch change.ChangeSrc.Action { + switch change.Change.Action { case plans.Update, plans.DeleteThenCreate, plans.CreateThenDelete: // OK default: return nil, false, diags } - // Since we have a traversal after the resource reference, we will need to - // decode the changes, which means we need a schema. - providerAddr := change.ProviderAddr - schema, err := ctx.ProviderSchema(providerAddr) - if err != nil { - diags = diags.Append(err) - return nil, false, diags - } - - resAddr := change.Addr.ContainingResource().Resource - resSchema, _ := schema.SchemaForResourceType(resAddr.Mode, resAddr.Type) - ty := resSchema.ImpliedType() - - before, err := change.ChangeSrc.Before.Decode(ty) - if err != nil { - diags = diags.Append(err) - return nil, false, diags - } - - after, err := change.ChangeSrc.After.Decode(ty) - if err != nil { - diags = diags.Append(err) - return nil, false, diags - } - - path := traversalToPath(ref.Remaining) - attrBefore, _ := path.Apply(before) - attrAfter, _ := path.Apply(after) + path, _ := traversalToPath(ref.Remaining) + attrBefore, _ := path.Apply(change.Before) + attrAfter, _ := path.Apply(change.After) if attrBefore == cty.NilVal || attrAfter == cty.NilVal { replace := attrBefore != attrAfter @@ -397,82 +421,157 @@ func (ctx *BuiltinEvalContext) EvaluateReplaceTriggeredBy(expr hcl.Expression, r return ref, replace, diags } -func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, keyData instances.RepetitionData) *lang.Scope { - if !ctx.pathSet { - panic("context path not set") - } - data := &evaluationStateData{ - Evaluator: ctx.Evaluator, - ModulePath: ctx.PathValue, - InstanceKeyData: keyData, - Operation: ctx.Evaluator.Operation, - } - scope := ctx.Evaluator.Scope(data, self) +func (ctx *BuiltinEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { + switch scope := ctx.scope.(type) { + case evalContextModuleInstance: + data := &evaluationStateData{ + evaluationData: &evaluationData{ + Evaluator: ctx.Evaluator, + Module: scope.Addr.Module(), + }, + ModulePath: scope.Addr, + InstanceKeyData: keyData, + Operation: ctx.Evaluator.Operation, + } + evalScope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) - // ctx.PathValue is the path of the module that contains whatever - // expression the caller will be trying to evaluate, so this will - // activate only the experiments from that particular module, to - // be consistent with how experiment checking in the "configs" - // package itself works. The nil check here is for robustness in - // incompletely-mocked testing situations; mc should never be nil in - // real situations. - if mc := ctx.Evaluator.Config.DescendentForInstance(ctx.PathValue); mc != nil { - scope.SetActiveExperiments(mc.Module.ActiveExperiments) + // ctx.PathValue is the path of the module that contains whatever + // expression the caller will be trying to evaluate, so this will + // activate only the experiments from that particular module, to + // be consistent with how experiment checking in the "configs" + // package itself works. The nil check here is for robustness in + // incompletely-mocked testing situations; mc should never be nil in + // real situations. + if mc := ctx.Evaluator.Config.DescendantForInstance(scope.Addr); mc != nil { + evalScope.SetActiveExperiments(mc.Module.ActiveExperiments) + } + return evalScope + case evalContextPartialExpandedModule: + data := &evaluationPlaceholderData{ + evaluationData: &evaluationData{ + Evaluator: ctx.Evaluator, + Module: scope.Addr.Module(), + }, + ModulePath: scope.Addr, + CountAvailable: keyData.CountIndex != cty.NilVal, + EachAvailable: keyData.EachKey != cty.NilVal, + Operation: ctx.Evaluator.Operation, + } + evalScope := ctx.Evaluator.Scope(data, self, source, ctx.evaluationExternalFunctions()) + if mc := ctx.Evaluator.Config.Descendant(scope.Addr.Module()); mc != nil { + evalScope.SetActiveExperiments(mc.Module.ActiveExperiments) + } + return evalScope + default: + // This method is valid only for module-scoped EvalContext objects. + panic("no evaluation scope available: not in module context") } - return scope + +} + +// evaluationExternalFunctions is a helper for method EvaluationScope which +// determines the set of external functions that should be available for +// evaluation in this EvalContext, based on declarations in the configuration. +func (ctx *BuiltinEvalContext) evaluationExternalFunctions() lang.ExternalFuncs { + // The set of functions in scope includes the functions contributed by + // every provider that the current module has as a requirement. + // + // We expose them under the local name for each provider that was selected + // by the module author. + ret := lang.ExternalFuncs{} + + cfg := ctx.Evaluator.Config.Descendant(ctx.scope.evalContextScopeModule()) + if cfg == nil { + // It's weird to not have a configuration by this point, but we'll + // tolerate it for robustness and just return no functions at all. + return ret + } + if cfg.Module.ProviderRequirements == nil { + // A module with no provider requirements can't have any + // provider-contributed functions. + return ret + } + + reqs := cfg.Module.ProviderRequirements.RequiredProviders + ret.Provider = make(map[string]map[string]function.Function, len(reqs)) + + for localName, req := range reqs { + providerAddr := req.Type + funcDecls, err := ctx.Plugins.ProviderFunctionDecls(providerAddr) + if err != nil { + // If a particular provider can't return schema then we'll catch + // it in plenty other places where it's more reasonable for us + // to return an error, so here we'll just treat it as having + // no functions. + log.Printf("[WARN] Error loading schema for %s to determine its functions: %s", providerAddr, err) + continue + } + + ret.Provider[localName] = make(map[string]function.Function, len(funcDecls)) + funcs := ret.Provider[localName] + for name, decl := range funcDecls { + funcs[name] = decl.BuildFunction(providerAddr, name, ctx.FunctionResults, func() (providers.Interface, error) { + return ctx.functionProvider(providerAddr) + }) + } + } + + return ret +} + +// functionProvider fetches a running provider instance for evaluating +// functions from the cache, or starts a new instance and adds it to the cache. +func (ctx *BuiltinEvalContext) functionProvider(addr addrs.Provider) (providers.Interface, error) { + ctx.ProviderLock.Lock() + defer ctx.ProviderLock.Unlock() + + p, ok := ctx.ProviderFuncCache[addr.String()] + if ok { + return p, nil + } + + log.Printf("[TRACE] starting function provider instance for %s", addr) + p, err := ctx.Plugins.NewProviderInstance(addr) + if err == nil { + ctx.ProviderFuncCache[addr.String()] = p + } + + return p, err } func (ctx *BuiltinEvalContext) Path() addrs.ModuleInstance { - if !ctx.pathSet { - panic("context path not set") + if scope, ok := ctx.scope.(evalContextModuleInstance); ok { + return scope.Addr } - return ctx.PathValue + panic("not evaluating in the scope of a fully-expanded module") } -func (ctx *BuiltinEvalContext) SetRootModuleArgument(addr addrs.InputVariable, v cty.Value) { - ctx.VariableValuesLock.Lock() - defer ctx.VariableValuesLock.Unlock() - - log.Printf("[TRACE] BuiltinEvalContext: Storing final value for variable %s", addr.Absolute(addrs.RootModuleInstance)) - key := addrs.RootModuleInstance.String() - args := ctx.VariableValues[key] - if args == nil { - args = make(map[string]cty.Value) - ctx.VariableValues[key] = args +func (ctx *BuiltinEvalContext) LanguageExperimentActive(experiment experiments.Experiment) bool { + if ctx.Evaluator == nil || ctx.Evaluator.Config == nil { + // Should not get here in normal code, but might get here in test code + // if the context isn't fully populated. + return false } - args[addr.Name] = v + scope := ctx.scope + if scope == evalContextGlobal { + // If we're not associated with a specific module then there can't + // be any language experiments in play, because experiment activation + // is module-scoped. + return false + } + cfg := ctx.Evaluator.Config.Descendant(scope.evalContextScopeModule()) + if cfg == nil { + return false + } + return cfg.Module.ActiveExperiments.Has(experiment) } -func (ctx *BuiltinEvalContext) SetModuleCallArgument(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value) { - ctx.VariableValuesLock.Lock() - defer ctx.VariableValuesLock.Unlock() - - if !ctx.pathSet { - panic("context path not set") - } - - childPath := callAddr.ModuleInstance(ctx.PathValue) - log.Printf("[TRACE] BuiltinEvalContext: Storing final value for variable %s", varAddr.Absolute(childPath)) - key := childPath.String() - args := ctx.VariableValues[key] - if args == nil { - args = make(map[string]cty.Value) - ctx.VariableValues[key] = args - } - args[varAddr.Name] = v +func (ctx *BuiltinEvalContext) NamedValues() *namedvals.State { + return ctx.NamedValuesValue } -func (ctx *BuiltinEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value { - ctx.VariableValuesLock.Lock() - defer ctx.VariableValuesLock.Unlock() - - modKey := addr.Module.String() - modVars := ctx.VariableValues[modKey] - val, ok := modVars[addr.Variable.Name] - if !ok { - return cty.DynamicVal - } - return val +func (ctx *BuiltinEvalContext) Deferrals() *deferring.Deferred { + return ctx.DeferralsValue } func (ctx *BuiltinEvalContext) Changes() *plans.ChangesSync { @@ -502,3 +601,22 @@ func (ctx *BuiltinEvalContext) InstanceExpander() *instances.Expander { func (ctx *BuiltinEvalContext) MoveResults() refactoring.MoveResults { return ctx.MoveResultsValue } + +func (ctx *BuiltinEvalContext) Overrides() *mocking.Overrides { + return ctx.OverrideValues +} + +func (ctx *BuiltinEvalContext) Forget() bool { + return ctx.forget +} + +func (ctx *BuiltinEvalContext) EphemeralResources() *ephemeral.Resources { + return ctx.EphemeralResourcesValue +} + +func (ctx *BuiltinEvalContext) ClientCapabilities() providers.ClientCapabilities { + return providers.ClientCapabilities{ + DeferralAllowed: ctx.Deferrals().DeferralAllowed(), + WriteOnlyAttributesAllowed: true, + } +} diff --git a/internal/terraform/eval_context_builtin_test.go b/internal/terraform/eval_context_builtin_test.go index 0db0096a75..60ba0b84d2 100644 --- a/internal/terraform/eval_context_builtin_test.go +++ b/internal/terraform/eval_context_builtin_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,9 +8,12 @@ import ( "sync" "testing" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" ) func TestBuiltinEvalContextProviderInput(t *testing.T) { @@ -15,12 +21,12 @@ func TestBuiltinEvalContextProviderInput(t *testing.T) { cache := make(map[string]map[string]cty.Value) ctx1 := testBuiltinEvalContext(t) - ctx1 = ctx1.WithPath(addrs.RootModuleInstance).(*BuiltinEvalContext) + ctx1 = ctx1.withScope(evalContextModuleInstance{Addr: addrs.RootModuleInstance}).(*BuiltinEvalContext) ctx1.ProviderInputConfig = cache ctx1.ProviderLock = &lock ctx2 := testBuiltinEvalContext(t) - ctx2 = ctx2.WithPath(addrs.RootModuleInstance.Child("child", addrs.NoKey)).(*BuiltinEvalContext) + ctx2 = ctx2.withScope(evalContextModuleInstance{Addr: addrs.RootModuleInstance.Child("child", addrs.NoKey)}).(*BuiltinEvalContext) ctx2.ProviderInputConfig = cache ctx2.ProviderLock = &lock @@ -53,15 +59,15 @@ func TestBuiltinEvalContextProviderInput(t *testing.T) { func TestBuildingEvalContextInitProvider(t *testing.T) { var lock sync.Mutex - testP := &MockProvider{} + testP := &testing_provider.MockProvider{} ctx := testBuiltinEvalContext(t) - ctx = ctx.WithPath(addrs.RootModuleInstance).(*BuiltinEvalContext) + ctx = ctx.withScope(evalContextModuleInstance{Addr: addrs.RootModuleInstance}).(*BuiltinEvalContext) ctx.ProviderLock = &lock ctx.ProviderCache = make(map[string]providers.Interface) ctx.Plugins = newContextPlugins(map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): providers.FactoryFixed(testP), - }, nil) + }, nil, nil) providerAddrDefault := addrs.AbsProviderConfig{ Module: addrs.RootModule, @@ -72,15 +78,27 @@ func TestBuildingEvalContextInitProvider(t *testing.T) { Provider: addrs.NewDefaultProvider("test"), Alias: "foo", } + providerAddrMock := addrs.AbsProviderConfig{ + Module: addrs.RootModule, + Provider: addrs.NewDefaultProvider("test"), + Alias: "mock", + } - _, err := ctx.InitProvider(providerAddrDefault) + _, err := ctx.InitProvider(providerAddrDefault, nil) if err != nil { t.Fatalf("error initializing provider test: %s", err) } - _, err = ctx.InitProvider(providerAddrAlias) + _, err = ctx.InitProvider(providerAddrAlias, nil) if err != nil { t.Fatalf("error initializing provider test.foo: %s", err) } + + _, err = ctx.InitProvider(providerAddrMock, &configs.Provider{ + Mock: true, + }) + if err != nil { + t.Fatalf("error initializing provider test.mock: %s", err) + } } func testBuiltinEvalContext(t *testing.T) *BuiltinEvalContext { diff --git a/internal/terraform/eval_context_mock.go b/internal/terraform/eval_context_mock.go index 24159ef955..48b0a41fef 100644 --- a/internal/terraform/eval_context_mock.go +++ b/internal/terraform/eval_context_mock.go @@ -1,28 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "context" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcldec" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/experiments" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" ) // MockEvalContext is a mock version of EvalContext that can be used // for tests. type MockEvalContext struct { - StoppedCalled bool - StoppedValue <-chan struct{} + StopCtxCalled bool + StopCtxValue context.Context HookCalled bool HookHook Hook @@ -43,9 +55,14 @@ type MockEvalContext struct { ProviderSchemaCalled bool ProviderSchemaAddr addrs.AbsProviderConfig - ProviderSchemaSchema *ProviderSchema + ProviderSchemaSchema providers.ProviderSchema ProviderSchemaError error + ResourceIdentitySchemasCalled bool + ResourceIdentitySchemasAddr addrs.AbsProviderConfig + ResourceIdentitySchemasSchemas providers.ResourceIdentitySchemas + ResourceIdentitySchemasError error + CloseProviderCalled bool CloseProviderAddr addrs.AbsProviderConfig CloseProviderProvider providers.Interface @@ -75,7 +92,7 @@ type MockEvalContext struct { ProvisionerSchemaSchema *configschema.Block ProvisionerSchemaError error - CloseProvisionersCalled bool + ClosePluginsCalled bool EvaluateBlockCalled bool EvaluateBlockBody hcl.Body @@ -110,23 +127,15 @@ type MockEvalContext struct { EvaluationScopeScope *lang.Scope PathCalled bool - PathPath addrs.ModuleInstance + Scope evalContextScope - SetRootModuleArgumentCalled bool - SetRootModuleArgumentAddr addrs.InputVariable - SetRootModuleArgumentValue cty.Value - SetRootModuleArgumentFunc func(addr addrs.InputVariable, v cty.Value) + LanguageExperimentsActive experiments.Set - SetModuleCallArgumentCalled bool - SetModuleCallArgumentModuleCall addrs.ModuleCallInstance - SetModuleCallArgumentVariable addrs.InputVariable - SetModuleCallArgumentValue cty.Value - SetModuleCallArgumentFunc func(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value) + NamedValuesCalled bool + NamedValuesState *namedvals.State - GetVariableValueCalled bool - GetVariableValueAddr addrs.AbsInputVariableInstance - GetVariableValueValue cty.Value - GetVariableValueFunc func(addr addrs.AbsInputVariableInstance) cty.Value // supersedes GetVariableValueValue + DeferralsCalled bool + DeferralsState *deferring.Deferred ChangesCalled bool ChangesChanges *plans.ChangesSync @@ -148,14 +157,26 @@ type MockEvalContext struct { InstanceExpanderCalled bool InstanceExpanderExpander *instances.Expander + + EphemeralResourcesCalled bool + EphemeralResourcesResources *ephemeral.Resources + + OverridesCalled bool + OverrideValues *mocking.Overrides + + ForgetCalled bool + ForgetValues bool } // MockEvalContext implements EvalContext var _ EvalContext = (*MockEvalContext)(nil) -func (c *MockEvalContext) Stopped() <-chan struct{} { - c.StoppedCalled = true - return c.StoppedValue +func (c *MockEvalContext) StopCtx() context.Context { + c.StopCtxCalled = true + if c.StopCtxValue != nil { + return c.StopCtxValue + } + return context.TODO() } func (c *MockEvalContext) Hook(fn func(Hook) (HookAction, error)) error { @@ -174,7 +195,7 @@ func (c *MockEvalContext) Input() UIInput { return c.InputInput } -func (c *MockEvalContext) InitProvider(addr addrs.AbsProviderConfig) (providers.Interface, error) { +func (c *MockEvalContext) InitProvider(addr addrs.AbsProviderConfig, _ *configs.Provider) (providers.Interface, error) { c.InitProviderCalled = true c.InitProviderType = addr.String() c.InitProviderAddr = addr @@ -187,12 +208,18 @@ func (c *MockEvalContext) Provider(addr addrs.AbsProviderConfig) providers.Inter return c.ProviderProvider } -func (c *MockEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (*ProviderSchema, error) { +func (c *MockEvalContext) ProviderSchema(addr addrs.AbsProviderConfig) (providers.ProviderSchema, error) { c.ProviderSchemaCalled = true c.ProviderSchemaAddr = addr return c.ProviderSchemaSchema, c.ProviderSchemaError } +func (c *MockEvalContext) ResourceIdentitySchemas(addr addrs.AbsProviderConfig) (providers.ResourceIdentitySchemas, error) { + c.ResourceIdentitySchemasCalled = true + c.ResourceIdentitySchemasAddr = addr + return c.ResourceIdentitySchemasSchemas, c.ProviderSchemaError +} + func (c *MockEvalContext) CloseProvider(addr addrs.AbsProviderConfig) error { c.CloseProviderCalled = true c.CloseProviderAddr = addr @@ -234,8 +261,8 @@ func (c *MockEvalContext) ProvisionerSchema(n string) (*configschema.Block, erro return c.ProvisionerSchemaSchema, c.ProvisionerSchemaError } -func (c *MockEvalContext) CloseProvisioners() error { - c.CloseProvisionersCalled = true +func (c *MockEvalContext) ClosePlugins() error { + c.ClosePluginsCalled = true return nil } @@ -319,50 +346,49 @@ func (c *MockEvalContext) installSimpleEval() { } } -func (c *MockEvalContext) EvaluationScope(self addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { +func (c *MockEvalContext) EvaluationScope(self addrs.Referenceable, source addrs.Referenceable, keyData InstanceKeyEvalData) *lang.Scope { c.EvaluationScopeCalled = true c.EvaluationScopeSelf = self c.EvaluationScopeKeyData = keyData return c.EvaluationScopeScope } -func (c *MockEvalContext) WithPath(path addrs.ModuleInstance) EvalContext { +func (c *MockEvalContext) withScope(scope evalContextScope) EvalContext { newC := *c - newC.PathPath = path + newC.Scope = scope return &newC } func (c *MockEvalContext) Path() addrs.ModuleInstance { c.PathCalled = true - return c.PathPath + // This intentionally panics if scope isn't a module instance; callers + // should use this only for an eval context that's working in a + // fully-expanded module instance. + return c.Scope.(evalContextModuleInstance).Addr } -func (c *MockEvalContext) SetRootModuleArgument(addr addrs.InputVariable, v cty.Value) { - c.SetRootModuleArgumentCalled = true - c.SetRootModuleArgumentAddr = addr - c.SetRootModuleArgumentValue = v - if c.SetRootModuleArgumentFunc != nil { - c.SetRootModuleArgumentFunc(addr, v) - } +func (c *MockEvalContext) LanguageExperimentActive(experiment experiments.Experiment) bool { + // This particular function uses a live data structure so that tests can + // exercise different experiments being enabled; there is little reason + // to directly test whether this function was called since we use this + // function only temporarily while an experiment is active, and then + // remove the calls once the experiment is concluded. + return c.LanguageExperimentsActive.Has(experiment) } -func (c *MockEvalContext) SetModuleCallArgument(callAddr addrs.ModuleCallInstance, varAddr addrs.InputVariable, v cty.Value) { - c.SetModuleCallArgumentCalled = true - c.SetModuleCallArgumentModuleCall = callAddr - c.SetModuleCallArgumentVariable = varAddr - c.SetModuleCallArgumentValue = v - if c.SetModuleCallArgumentFunc != nil { - c.SetModuleCallArgumentFunc(callAddr, varAddr, v) - } +func (c *MockEvalContext) NamedValues() *namedvals.State { + c.NamedValuesCalled = true + return c.NamedValuesState } -func (c *MockEvalContext) GetVariableValue(addr addrs.AbsInputVariableInstance) cty.Value { - c.GetVariableValueCalled = true - c.GetVariableValueAddr = addr - if c.GetVariableValueFunc != nil { - return c.GetVariableValueFunc(addr) - } - return c.GetVariableValueValue +func (c *MockEvalContext) EphemeralResources() *ephemeral.Resources { + c.EphemeralResourcesCalled = true + return c.EphemeralResourcesResources +} + +func (c *MockEvalContext) Deferrals() *deferring.Deferred { + c.DeferralsCalled = true + return c.DeferralsState } func (c *MockEvalContext) Changes() *plans.ChangesSync { @@ -399,3 +425,20 @@ func (c *MockEvalContext) InstanceExpander() *instances.Expander { c.InstanceExpanderCalled = true return c.InstanceExpanderExpander } + +func (c *MockEvalContext) Overrides() *mocking.Overrides { + c.OverridesCalled = true + return c.OverrideValues +} + +func (c *MockEvalContext) Forget() bool { + c.ForgetCalled = true + return c.ForgetValues +} + +func (ctx *MockEvalContext) ClientCapabilities() providers.ClientCapabilities { + return providers.ClientCapabilities{ + DeferralAllowed: ctx.Deferrals().DeferralAllowed(), + WriteOnlyAttributesAllowed: true, + } +} diff --git a/internal/terraform/eval_context_scope.go b/internal/terraform/eval_context_scope.go new file mode 100644 index 0000000000..2e111e438e --- /dev/null +++ b/internal/terraform/eval_context_scope.go @@ -0,0 +1,93 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/collections" +) + +// evalContextScope represents the scope that an [EvalContext] (or rather, +// an [EvalContextBuiltin] is associated with. +// +// This is a closed interface representing a sum type, with three possible +// variants: +// +// - a nil value of this type represents a "global" evaluation context used +// for graph nodes that aren't considered to belong to any specific module +// instance. Some [EvalContext] methods are not appropriate for such a +// context, and so will panic on a global evaluation context. +// - [evalContextModuleInstance] is for an evaluation context used for +// graph nodes that implement [GraphNodeModuleInstance], meaning that +// they belong to a fully-expanded single module instance. +// - [evalContextPartialExpandedModule] is for an evaluation context used for +// graph nodes that implement [GraphNodeUnexpandedModule], meaning that +// they belong to an unbounded set of possible module instances sharing +// a common known prefix, in situations where a module call has an unknown +// value for its count or for_each argument. +type evalContextScope interface { + collections.UniqueKeyer[evalContextScope] + + // evalContextScopeModule returns the static module address of whatever + // fully- or partially-expanded module instance address this scope is + // associated with. + // + // A "global" evaluation context is a nil [evalContextScope], and so + // this method will panic for that scope. + evalContextScopeModule() addrs.Module + + String() string +} + +// evalContextGlobal is the nil [evalContextScope] used to represent an +// [EvalContext] that isn't associated with any module at all. +var evalContextGlobal evalContextScope + +// evalContextModuleInstance is an [evalContextScope] associated with a +// fully-expanded single module instance. +type evalContextModuleInstance struct { + Addr addrs.ModuleInstance +} + +func (s evalContextModuleInstance) evalContextScopeModule() addrs.Module { + return s.Addr.Module() +} + +func (s evalContextModuleInstance) String() string { + return s.Addr.String() +} + +func (s evalContextModuleInstance) UniqueKey() collections.UniqueKey[evalContextScope] { + return evalContextScopeUniqueKey{ + k: s.Addr.UniqueKey(), + } +} + +// evalContextPartialExpandedModule is an [evalContextScope] associated with +// an unbounded set of possible module instances that share a common known +// address prefix. +type evalContextPartialExpandedModule struct { + Addr addrs.PartialExpandedModule +} + +func (s evalContextPartialExpandedModule) evalContextScopeModule() addrs.Module { + return s.Addr.Module() +} + +func (s evalContextPartialExpandedModule) String() string { + return s.Addr.String() +} + +func (s evalContextPartialExpandedModule) UniqueKey() collections.UniqueKey[evalContextScope] { + return evalContextScopeUniqueKey{ + k: s.Addr.UniqueKey(), + } +} + +type evalContextScopeUniqueKey struct { + k addrs.UniqueKey +} + +// IsUniqueKey implements collections.UniqueKey. +func (evalContextScopeUniqueKey) IsUniqueKey(evalContextScope) {} diff --git a/internal/terraform/eval_count.go b/internal/terraform/eval_count.go index d4ab1a998f..aea7f92c17 100644 --- a/internal/terraform/eval_count.go +++ b/internal/terraform/eval_count.go @@ -1,12 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/gocty" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" ) // evaluateCountExpression is our standard mechanism for interpreting an @@ -17,9 +22,15 @@ import ( // evaluateCountExpression differs from evaluateCountExpressionValue by // returning an error if the count value is not known, and converting the // cty.Value to an integer. -func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags.Diagnostics) { +// +// If allowUnknown is false then this function will return error diagnostics +// whenever the expression returns an unknown value. Setting allowUnknown to +// true instead permits unknown values, indicating them by returning the +// placeholder value -1. Callers can assume that a return value of -1 without +// any error diagnostics represents a valid unknown value. +func evaluateCountExpression(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (int, tfdiags.Diagnostics) { countVal, diags := evaluateCountExpressionValue(expr, ctx) - if !countVal.IsKnown() { + if !allowUnknown && !countVal.IsKnown() { // Currently this is a rather bad outcome from a UX standpoint, since we have // no real mechanism to deal with this situation and all we can do is produce // an error message. @@ -40,6 +51,18 @@ func evaluateCountExpression(expr hcl.Expression, ctx EvalContext) (int, tfdiags }) } + // Ephemeral values are not allowed in count expressions. + if countVal.HasMark(marks.Ephemeral) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: `The given "count" value is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values here.`, + Subject: expr.Range().Ptr(), + Extra: DiagnosticCausedByEphemeral(true), + }) + return -1, diags + } + if countVal.IsNull() || !countVal.IsKnown() { return -1, diags } @@ -64,8 +87,26 @@ func evaluateCountExpressionValue(expr hcl.Expression, ctx EvalContext) (cty.Val return nullCount, diags } - // Unmark the count value, sensitive values are allowed in count but not for_each, - // as using it here will not disclose the sensitive value + if countVal.HasMark(marks.Ephemeral) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid count argument", + Detail: `The given "count" is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify the number of resource instances.`, + Subject: expr.Range().Ptr(), + + // TODO: Also populate Expression and EvalContext in here, but + // we can't easily do that right now because the hcl.EvalContext + // (which is not the same as the ctx we have in scope here) is + // hidden away inside ctx.EvaluateExpr. + Extra: DiagnosticCausedByEphemeral(true), + }) + } + + // Sensitive values are allowed in count but not for_each. This is a + // somewhat-dubious decision because the number of instances planned + // will disclose exactly what the value was, but in practice it's rare + // for a number alone to be sensitive and so this is pragmatic, along with + // being required for backward-compatibility. countVal, _ = countVal.Unmark() switch { diff --git a/internal/terraform/eval_count_test.go b/internal/terraform/eval_count_test.go index 8d3a51b488..307a507771 100644 --- a/internal/terraform/eval_count_test.go +++ b/internal/terraform/eval_count_test.go @@ -1,7 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "reflect" + "strings" "testing" "github.com/davecgh/go-spew/spew" @@ -20,7 +24,7 @@ func TestEvaluateCountExpression(t *testing.T) { hcltest.MockExprLiteral(cty.NumberIntVal(0)), 0, }, - "expression with marked value": { + "expression with sensitive value": { hcltest.MockExprLiteral(cty.NumberIntVal(8).Mark(marks.Sensitive)), 8, }, @@ -29,7 +33,7 @@ func TestEvaluateCountExpression(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - countVal, diags := evaluateCountExpression(test.Expr, ctx) + countVal, diags := evaluateCountExpression(test.Expr, ctx, false) if len(diags) != 0 { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) @@ -44,3 +48,52 @@ func TestEvaluateCountExpression(t *testing.T) { }) } } + +func TestEvaluateCountExpression_ephemeral(t *testing.T) { + expr := hcltest.MockExprLiteral(cty.NumberIntVal(8).Mark(marks.Ephemeral)) + ctx := &MockEvalContext{} + ctx.installSimpleEval() + _, diags := evaluateCountExpression(expr, ctx, false) + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + gotErrs := diags.Err().Error() + wantErr := `The given "count" is derived from an ephemeral value` + if !strings.Contains(gotErrs, wantErr) { + t.Errorf("missing expected error\ngot:\n%s\nwant substring: %s", gotErrs, wantErr) + } +} + +func TestEvaluateCountExpression_allowUnknown(t *testing.T) { + tests := map[string]struct { + Expr hcl.Expression + Count int + }{ + "unknown number": { + hcltest.MockExprLiteral(cty.UnknownVal(cty.Number)), + -1, + }, + "dynamicval": { + hcltest.MockExprLiteral(cty.DynamicVal), + -1, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := &MockEvalContext{} + ctx.installSimpleEval() + countVal, diags := evaluateCountExpression(test.Expr, ctx, true) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) + } + + if !reflect.DeepEqual(countVal, test.Count) { + t.Errorf( + "wrong result\ngot: %#v\nwant: %#v", + countVal, test.Count, + ) + } + }) + } +} diff --git a/internal/terraform/eval_for_each.go b/internal/terraform/eval_for_each.go index 3c80ebff01..b71cb824c5 100644 --- a/internal/terraform/eval_for_each.go +++ b/internal/terraform/eval_for_each.go @@ -1,155 +1,365 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/terraform/internal/lang" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) -// evaluateForEachExpression is our standard mechanism for interpreting an -// expression given for a "for_each" argument on a resource or a module. This -// should be called during expansion in order to determine the final keys and -// values. -// // evaluateForEachExpression differs from evaluateForEachExpressionValue by // returning an error if the count value is not known, and converting the // cty.Value to a map[string]cty.Value for compatibility with other calls. -func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext) (forEach map[string]cty.Value, diags tfdiags.Diagnostics) { - forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, false) - // forEachVal might be unknown, but if it is then there should already - // be an error about it in diags, which we'll return below. - - if forEachVal.IsNull() || !forEachVal.IsKnown() || markSafeLengthInt(forEachVal) == 0 { - // we check length, because an empty set return a nil map - return map[string]cty.Value{}, diags - } - - return forEachVal.AsValueMap(), diags +func evaluateForEachExpression(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (forEach map[string]cty.Value, known bool, diags tfdiags.Diagnostics) { + return newForEachEvaluator(expr, ctx, allowUnknown).ResourceValue() } -// evaluateForEachExpressionValue is like evaluateForEachExpression -// except that it returns a cty.Value map or set which can be unknown. -func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - nullMap := cty.NullVal(cty.Map(cty.DynamicPseudoType)) - - if expr == nil { - return nullMap, diags +// forEachEvaluator is the standard mechanism for interpreting an expression +// given for a "for_each" argument on a resource, module, or import. +func newForEachEvaluator(expr hcl.Expression, ctx EvalContext, allowUnknown bool) *forEachEvaluator { + if ctx == nil { + panic("nil EvalContext") } - refs, moreDiags := lang.ReferencesInExpr(expr) + return &forEachEvaluator{ + ctx: ctx, + expr: expr, + allowUnknown: allowUnknown, + } +} + +// forEachEvaluator is responsible for evaluating for_each expressions, using +// different rules depending on the desired context. +type forEachEvaluator struct { + // We bundle this functionality into a structure, because internal + // validation requires not only the resulting value, but also the original + // expression and the hcl EvalContext to build the corresponding + // diagnostic. Every method's dependency on all the evaluation pieces + // otherwise prevents refactoring and we end up with a single giant + // function. + ctx EvalContext + expr hcl.Expression + + // TEMP: If allowUnknown is set then we skip the usual restriction that + // unknown values are not allowed in for_each. A caller that sets this + // must therefore be ready to deal with the result being unknown. + // This will eventually become the default behavior, once we've updated + // the rest of this package to handle that situation in a reasonable way. + allowUnknown bool + + // internal + hclCtx *hcl.EvalContext +} + +// ResourceForEachValue returns a known for_each map[string]cty.Value +// appropriate for use within resource expansion. +func (ev *forEachEvaluator) ResourceValue() (map[string]cty.Value, bool, tfdiags.Diagnostics) { + res := map[string]cty.Value{} + + // no expression always results in an empty map + if ev.expr == nil { + return res, true, nil + } + + forEachVal, diags := ev.Value() + if diags.HasErrors() { + return res, false, diags + } + + // ensure our value is known for use in resource expansion + unknownDiags := ev.ensureKnownForResource(forEachVal) + if unknownDiags.HasErrors() { + if !ev.allowUnknown { + diags = diags.Append(unknownDiags) + } + return res, false, diags + } + + // validate the for_each value for use in resource expansion + diags = diags.Append(ev.validateResource(forEachVal)) + if diags.HasErrors() { + return res, false, diags + } + + if forEachVal.IsNull() || !forEachVal.IsKnown() || markSafeLengthInt(forEachVal) == 0 { + // we check length, because an empty set returns a nil map which will panic below + return res, true, diags + } + + if _, marks := forEachVal.Unmark(); len(marks) != 0 { + // Should not get here, because validateResource above should have + // rejected values that are marked. If we do get here then it's + // likely that we've added a new kind of mark that validateResource + // doesn't know about yet, and so we'll need to decide how for_each + // should react to that new mark. + diags = diags.Append(fmt.Errorf("for_each value is marked with %#v despite earlier validation; this is a bug in Terraform", marks)) + return res, false, diags + } + res = forEachVal.AsValueMap() + return res, true, diags +} + +// ImportValue returns the for_each map for use within an import block, +// enumerated as individual instances.RepetitionData values. +func (ev *forEachEvaluator) ImportValues() ([]instances.RepetitionData, bool, tfdiags.Diagnostics) { + var res []instances.RepetitionData + if ev.expr == nil { + return res, true, nil + } + + forEachVal, diags := ev.Value() + if diags.HasErrors() { + return res, false, diags + } + + // ensure our value is known for use in resource expansion + unknownDiags := diags.Append(ev.ensureKnownForImport(forEachVal)) + if unknownDiags.HasErrors() { + if !ev.allowUnknown { + diags = diags.Append(unknownDiags) + } + return res, false, diags + } + + // ensure the value is not ephemeral + diags = diags.Append(ev.ensureNotEphemeral(forEachVal)) + + if forEachVal.IsNull() { + return res, true, diags + } + + val, marks := forEachVal.Unmark() + + if !val.CanIterateElements() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: "The \"for_each\" expression must be a collection.", + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, + }) + return res, false, diags + } + + it := val.ElementIterator() + for it.Next() { + k, v := it.Element() + res = append(res, instances.RepetitionData{ + EachKey: k, + EachValue: v.WithMarks(marks), + }) + + } + + return res, true, diags +} + +// Value returns the raw cty.Value evaluated from the given for_each expression +func (ev *forEachEvaluator) Value() (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if ev.expr == nil { + // a nil expression always results in a null value + return cty.NullVal(cty.Map(cty.DynamicPseudoType)), nil + } + + refs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRef, ev.expr) diags = diags.Append(moreDiags) - scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) - var hclCtx *hcl.EvalContext + scope := ev.ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) if scope != nil { - hclCtx, moreDiags = scope.EvalContext(refs) + ev.hclCtx, moreDiags = scope.EvalContext(refs) } else { // This shouldn't happen in real code, but it can unfortunately arise // in unit tests due to incompletely-implemented mocks. :( - hclCtx = &hcl.EvalContext{} + ev.hclCtx = &hcl.EvalContext{} } + diags = diags.Append(moreDiags) if diags.HasErrors() { // Can't continue if we don't even have a valid scope - return nullMap, diags + return cty.DynamicVal, diags } - forEachVal, forEachDiags := expr.Value(hclCtx) + forEachVal, forEachDiags := ev.expr.Value(ev.hclCtx) diags = diags.Append(forEachDiags) - // If a whole map is marked, or a set contains marked values (which means the set is then marked) - // give an error diagnostic as this value cannot be used in for_each + return forEachVal, diags +} + +// ensureKnownForImport checks that the value is entirely known for use within +// import expansion. +func (ev *forEachEvaluator) ensureKnownForImport(forEachVal cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if !forEachVal.IsWhollyKnown() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: "The \"for_each\" expression includes values derived from other resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of values that might be used to import this resource.", + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, + Extra: diagnosticCausedByUnknown(true), + }) + } + return diags +} + +// ensureKnownForResource checks that the value is known within the rules of +// resource and module expansion. +func (ev *forEachEvaluator) ensureKnownForResource(forEachVal cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + ty := forEachVal.Type() + const errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge." + const errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge." + + if !forEachVal.IsKnown() { + var detailMsg string + switch { + case ty.IsSetType(): + detailMsg = errInvalidUnknownDetailSet + default: + detailMsg = errInvalidUnknownDetailMap + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: detailMsg, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, + Extra: diagnosticCausedByUnknown(true), + }) + return diags + } + + if ty.IsSetType() && !forEachVal.IsWhollyKnown() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: errInvalidUnknownDetailSet, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, + Extra: diagnosticCausedByUnknown(true), + }) + } + return diags +} + +// ensureNotEphemeral makes sure no ephemeral values are used in the for_each expression. +func (ev *forEachEvaluator) ensureNotEphemeral(forEachVal cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + // Ephemeral values are not allowed because instance keys persist from + // plan to apply and between plan/apply rounds, whereas ephemeral values + // do not. + if forEachVal.HasMark(marks.Ephemeral) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: `The given "for_each" value is derived from an ephemeral value, which means that Terraform cannot persist it between plan/apply rounds. Use only non-ephemeral values to specify a resource's instance keys.`, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, + Extra: DiagnosticCausedByEphemeral(true), + }) + } + + return diags +} + +// ValidateResourceValue is used from validation walks to verify the validity +// of the resource for_Each expression, while still allowing for unknown +// values. +func (ev *forEachEvaluator) ValidateResourceValue() tfdiags.Diagnostics { + val, diags := ev.Value() + if diags.HasErrors() { + return diags + } + + return diags.Append(ev.validateResource(val)) +} + +// validateResource validates the type and values of the forEachVal, while +// still allowing unknown values for use within the validation walk. +func (ev *forEachEvaluator) validateResource(forEachVal cty.Value) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // Sensitive values are not allowed because otherwise the sensitive keys + // would get exposed as part of the instance addresses. if forEachVal.HasMark(marks.Sensitive) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid for_each argument", Detail: "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.", - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, Extra: diagnosticCausedBySensitive(true), }) } + diags = diags.Append(ev.ensureNotEphemeral(forEachVal)) + if diags.HasErrors() { - return nullMap, diags + return diags } ty := forEachVal.Type() - const errInvalidUnknownDetailMap = "The \"for_each\" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge." - const errInvalidUnknownDetailSet = "The \"for_each\" set includes values derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.\n\nWhen working with unknown values in for_each, it's better to use a map value where the keys are defined statically in your configuration and where only the values contain apply-time results.\n\nAlternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to fully converge." - switch { case forEachVal.IsNull(): diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid for_each argument", Detail: `The given "for_each" argument value is unsuitable: the given "for_each" argument value is null. A map, or set of strings is allowed.`, - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, }) - return nullMap, diags - case !forEachVal.IsKnown(): - if !allowUnknown { - var detailMsg string - switch { - case ty.IsSetType(): - detailMsg = errInvalidUnknownDetailSet - default: - detailMsg = errInvalidUnknownDetailMap - } + return diags - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid for_each argument", - Detail: detailMsg, - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, - Extra: diagnosticCausedByUnknown(true), - }) - } - // ensure that we have a map, and not a DynamicValue - return cty.UnknownVal(cty.Map(cty.DynamicPseudoType)), diags + case forEachVal.Type() == cty.DynamicPseudoType: + // We may not have any type information if this is during validation, + // so we need to return early. During plan this can't happen because we + // validate for unknowns first. + return diags case !(ty.IsMapType() || ty.IsSetType() || ty.IsObjectType()): diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid for_each argument", Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: the "for_each" argument must be a map, or set of strings, and you have provided a value of type %s.`, ty.FriendlyName()), - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, }) - return nullMap, diags + return diags + + case !forEachVal.IsKnown(): + return diags case markSafeLengthInt(forEachVal) == 0: // If the map is empty ({}), return an empty map, because cty will // return nil when representing {} AsValueMap. This also covers an empty // set (toset([])) - return forEachVal, diags + return diags } if ty.IsSetType() { // since we can't use a set values that are unknown, we treat the // entire set as unknown if !forEachVal.IsWhollyKnown() { - if !allowUnknown { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid for_each argument", - Detail: errInvalidUnknownDetailSet, - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, - Extra: diagnosticCausedByUnknown(true), - }) - } - return cty.UnknownVal(ty), diags + return diags } if ty.ElementType() != cty.String { @@ -157,11 +367,11 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU Severity: hcl.DiagError, Summary: "Invalid for_each set argument", Detail: fmt.Sprintf(`The given "for_each" argument value is unsuitable: "for_each" supports maps and sets of strings, but you have provided a set containing type %s.`, forEachVal.Type().ElementType().FriendlyName()), - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, }) - return cty.NullVal(ty), diags + return diags } // A set of strings may contain null, which makes it impossible to @@ -174,16 +384,16 @@ func evaluateForEachExpressionValue(expr hcl.Expression, ctx EvalContext, allowU Severity: hcl.DiagError, Summary: "Invalid for_each set argument", Detail: `The given "for_each" argument value is unsuitable: "for_each" sets must not contain null values.`, - Subject: expr.Range().Ptr(), - Expression: expr, - EvalContext: hclCtx, + Subject: ev.expr.Range().Ptr(), + Expression: ev.expr, + EvalContext: ev.hclCtx, }) - return cty.NullVal(ty), diags + return diags } } } - return forEachVal, nil + return diags } // markSafeLengthInt allows calling LengthInt on marked values safely diff --git a/internal/terraform/eval_for_each_test.go b/internal/terraform/eval_for_each_test.go index 05dba9cabc..81017804e5 100644 --- a/internal/terraform/eval_for_each_test.go +++ b/internal/terraform/eval_for_each_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -63,13 +66,23 @@ func TestEvaluateForEachExpression_valid(t *testing.T) { "b": cty.BoolVal(false), }, }, + "map containing ephemeral values": { + hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ + "a": cty.BoolVal(true).Mark(marks.Ephemeral), + "b": cty.BoolVal(false), + })), + map[string]cty.Value{ + "a": cty.BoolVal(true).Mark(marks.Ephemeral), + "b": cty.BoolVal(false), + }, + }, } for name, test := range tests { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - forEachMap, diags := evaluateForEachExpression(test.Expr, ctx) + forEachMap, _, diags := evaluateForEachExpression(test.Expr, ctx, false) if len(diags) != 0 { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) @@ -88,84 +101,99 @@ func TestEvaluateForEachExpression_valid(t *testing.T) { func TestEvaluateForEachExpression_errors(t *testing.T) { tests := map[string]struct { - Expr hcl.Expression - Summary, DetailSubstring string - CausedByUnknown, CausedBySensitive bool + Expr hcl.Expression + Summary, DetailSubstring string + CausedByUnknown, CausedByEphemeral, CausedBySensitive bool }{ "null set": { hcltest.MockExprLiteral(cty.NullVal(cty.Set(cty.String))), "Invalid for_each argument", `the given "for_each" argument value is null`, - false, false, + false, false, false, }, "string": { hcltest.MockExprLiteral(cty.StringVal("i am definitely a set")), "Invalid for_each argument", "must be a map, or set of strings, and you have provided a value of type string", - false, false, + false, false, false, }, "list": { hcltest.MockExprLiteral(cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("a")})), "Invalid for_each argument", "must be a map, or set of strings, and you have provided a value of type list", - false, false, + false, false, false, }, "tuple": { hcltest.MockExprLiteral(cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")})), "Invalid for_each argument", "must be a map, or set of strings, and you have provided a value of type tuple", - false, false, + false, false, false, }, "unknown string set": { hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), "Invalid for_each argument", "set includes values derived from resource attributes that cannot be determined until apply", - true, false, + true, false, false, }, "unknown map": { hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))), "Invalid for_each argument", "map includes keys derived from resource attributes that cannot be determined until apply", - true, false, + true, false, false, }, - "marked map": { + "sensitive map": { hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ "a": cty.BoolVal(true), "b": cty.BoolVal(false), }).Mark(marks.Sensitive)), "Invalid for_each argument", "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.", - false, true, + false, false, true, + }, + "ephemeral map": { + hcltest.MockExprLiteral(cty.MapVal(map[string]cty.Value{ + "a": cty.BoolVal(true), + "b": cty.BoolVal(false), + }).Mark(marks.Ephemeral)), + "Invalid for_each argument", + `The given "for_each" value is derived from an ephemeral value`, + false, true, false, }, "set containing booleans": { hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.BoolVal(true)})), "Invalid for_each set argument", "supports maps and sets of strings, but you have provided a set containing type bool", - false, false, + false, false, false, }, "set containing null": { hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.NullVal(cty.String)})), "Invalid for_each set argument", "must not contain null values", - false, false, + false, false, false, }, "set containing unknown value": { hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})), "Invalid for_each argument", "set includes values derived from resource attributes that cannot be determined until apply", - true, false, + true, false, false, }, "set containing dynamic unknown value": { hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})), "Invalid for_each argument", "set includes values derived from resource attributes that cannot be determined until apply", - true, false, + true, false, false, }, - "set containing marked values": { + "set containing sensitive values": { hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("beep").Mark(marks.Sensitive), cty.StringVal("boop")})), "Invalid for_each argument", "Sensitive values, or values derived from sensitive values, cannot be used as for_each arguments. If used, the sensitive value could be exposed as a resource instance key.", - false, true, + false, false, true, + }, + "set containing ephemeral values": { + hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.StringVal("beep").Mark(marks.Ephemeral), cty.StringVal("boop")})), + "Invalid for_each argument", + `The given "for_each" value is derived from an ephemeral value`, + false, true, false, }, } @@ -173,10 +201,10 @@ func TestEvaluateForEachExpression_errors(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - _, diags := evaluateForEachExpression(test.Expr, ctx) + _, _, diags := evaluateForEachExpression(test.Expr, ctx, false) if len(diags) != 1 { - t.Fatalf("got %d diagnostics; want 1", diags) + t.Fatalf("got %d diagnostics; want 1", len(diags)) } if got, want := diags[0].Severity(), tfdiags.Error; got != want { t.Errorf("wrong diagnostic severity %#v; want %#v", got, want) @@ -201,6 +229,9 @@ func TestEvaluateForEachExpression_errors(t *testing.T) { if got, want := tfdiags.DiagnosticCausedByUnknown(diags[0]), test.CausedByUnknown; got != want { t.Errorf("wrong result from tfdiags.DiagnosticCausedByUnknown\ngot: %#v\nwant: %#v", got, want) } + if got, want := tfdiags.DiagnosticCausedByEphemeral(diags[0]), test.CausedByEphemeral; got != want { + t.Errorf("wrong result from tfdiags.DiagnosticCausedByEphemeral\ngot: %#v\nwant: %#v", got, want) + } if got, want := tfdiags.DiagnosticCausedBySensitive(diags[0]), test.CausedBySensitive; got != want { t.Errorf("wrong result from tfdiags.DiagnosticCausedBySensitive\ngot: %#v\nwant: %#v", got, want) } @@ -208,6 +239,41 @@ func TestEvaluateForEachExpression_errors(t *testing.T) { } } +func TestEvaluateForEachExpression_allowUnknown(t *testing.T) { + tests := map[string]struct { + Expr hcl.Expression + }{ + "unknown string set": { + hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), + }, + "unknown map": { + hcltest.MockExprLiteral(cty.UnknownVal(cty.Map(cty.Bool))), + }, + "set containing unknown value": { + hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)})), + }, + "set containing dynamic unknown value": { + hcltest.MockExprLiteral(cty.SetVal([]cty.Value{cty.UnknownVal(cty.DynamicPseudoType)})), + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + ctx := &MockEvalContext{} + ctx.installSimpleEval() + _, known, diags := evaluateForEachExpression(test.Expr, ctx, true) + + // With allowUnknown set, all of these expressions should be treated + // as valid for_each values. + tfdiags.AssertNoDiagnostics(t, diags) + + if known { + t.Errorf("result is known; want unknown") + } + }) + } +} + func TestEvaluateForEachExpressionKnown(t *testing.T) { tests := map[string]hcl.Expression{ "unknown string set": hcltest.MockExprLiteral(cty.UnknownVal(cty.Set(cty.String))), @@ -218,15 +284,11 @@ func TestEvaluateForEachExpressionKnown(t *testing.T) { t.Run(name, func(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - forEachVal, diags := evaluateForEachExpressionValue(expr, ctx, true) + diags := newForEachEvaluator(expr, ctx, false).ValidateResourceValue() if len(diags) != 0 { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) } - - if forEachVal.IsKnown() { - t.Error("got known, want unknown") - } }) } } diff --git a/internal/terraform/eval_import.go b/internal/terraform/eval_import.go new file mode 100644 index 0000000000..7af58b3f6a --- /dev/null +++ b/internal/terraform/eval_import.go @@ -0,0 +1,275 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// evaluateImportIdExpression evaluates the given expression to determine the +// import Id for a resource. It should evaluate to a non-empty string. +// +// The given expression must be non-nil or the function will panic. +func evaluateImportIdExpression(expr hcl.Expression, ctx EvalContext, keyData instances.RepetitionData, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // import blocks only exist in the root module, and must be evaluated in + // that context. + ctx = evalContextForModuleInstance(ctx, addrs.RootModuleInstance) + scope := ctx.EvaluationScope(nil, nil, keyData) + importIdVal, evalDiags := scope.EvalExpr(expr, cty.String) + diags = diags.Append(evalDiags) + + if importIdVal.IsNull() { + return cty.NilVal, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import id argument", + Detail: "The import ID cannot be null.", + Subject: expr.Range().Ptr(), + }) + } + if !allowUnknown && !importIdVal.IsKnown() { + return cty.NilVal, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import id argument", + Detail: `The import block "id" argument depends on resource attributes that cannot be determined until apply, so Terraform cannot plan to import this resource.`, // FIXME and what should I do about that? + Subject: expr.Range().Ptr(), + // Expression: + // EvalContext: + Extra: diagnosticCausedByUnknown(true), + }) + } + + // Import data may have marks, which we can discard because the id is only + // sent to the provider. + importIdVal, _ = importIdVal.Unmark() + + if importIdVal.Type() != cty.String { + return cty.NilVal, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import id argument", + Detail: "The import ID value is unsuitable: not a string.", + Subject: expr.Range().Ptr(), + }) + } + + if importIdVal.IsKnown() && importIdVal.AsString() == "" { + return cty.NilVal, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import id argument", + Detail: "The import ID value evaluates to an empty string, please provide a non-empty value.", + Subject: expr.Range().Ptr(), + }) + } + + return importIdVal, diags +} + +// evaluateImportIdentityExpression evaluates the given expression to determine the +// import identity for a resource. It uses the resource identity schema to validate +// the structure of the object.. +// +// The given expression must be non-nil or the function will panic. +func evaluateImportIdentityExpression(expr hcl.Expression, identity *configschema.Object, ctx EvalContext, keyData instances.RepetitionData, allowUnknown bool) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // import blocks only exist in the root module, and must be evaluated in + // that context. + ctx = evalContextForModuleInstance(ctx, addrs.RootModuleInstance) + scope := ctx.EvaluationScope(nil, nil, keyData) + importIdentityVal, evalDiags := scope.EvalExpr(expr, identity.ConfigType()) + if evalDiags.HasErrors() { + // TODO? Do we need to improve the error message? + return cty.NilVal, evalDiags + } + + if importIdentityVal.IsNull() { + return cty.NilVal, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import identity argument", + Detail: "The import identity cannot be null.", + Subject: expr.Range().Ptr(), + }) + } + if !allowUnknown && !importIdentityVal.IsWhollyKnown() { + return cty.NilVal, diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import identity argument", + Detail: `The import block "identity" argument depends on resource attributes that cannot be determined until apply, so Terraform cannot plan to import this resource.`, // FIXME and what should I do about that? + Subject: expr.Range().Ptr(), + Extra: diagnosticCausedByUnknown(true), + }) + } + + // Import data may have marks, which we can discard because the id is only + // sent to the provider. + importIdentityVal, _ = importIdentityVal.Unmark() + + return importIdentityVal, diags +} + +func evalImportToExpression(expr hcl.Expression, keyData instances.RepetitionData) (addrs.AbsResourceInstance, tfdiags.Diagnostics) { + var res addrs.AbsResourceInstance + var diags tfdiags.Diagnostics + + traversal, diags := importToExprToTraversal(expr, keyData) + if diags.HasErrors() { + return res, diags + } + + target, targetDiags := addrs.ParseTarget(traversal) + diags = diags.Append(targetDiags) + if diags.HasErrors() { + return res, targetDiags + } + + switch sub := target.Subject.(type) { + case addrs.AbsResource: + res = sub.Instance(addrs.NoKey) + case addrs.AbsResourceInstance: + res = sub + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import 'to' expression", + Detail: fmt.Sprintf("The import block 'to' argument %s does not resolve to a single resource instance.", sub), + Subject: expr.Range().Ptr(), + }) + } + + return res, diags +} + +func evalImportUnknownToExpression(expr hcl.Expression) (addrs.PartialExpandedResource, tfdiags.Diagnostics) { + var per addrs.PartialExpandedResource + var diags tfdiags.Diagnostics + + traversal, diags := importToExprToTraversal(expr, instances.UnknownForEachRepetitionData(cty.DynamicPseudoType)) + if diags.HasErrors() { + return per, diags + } + + per, moreDiags := parseImportToPartialAddress(traversal) + diags = diags.Append(moreDiags) + return per, diags +} + +// trggersExprToTraversal takes an hcl expression limited to the syntax allowed +// in replace_triggered_by, and converts it to a static traversal. The +// RepetitionData contains the data necessary to evaluate the only allowed +// variables in the expression, count.index and each.key. +func importToExprToTraversal(expr hcl.Expression, keyData instances.RepetitionData) (hcl.Traversal, tfdiags.Diagnostics) { + var trav hcl.Traversal + var diags tfdiags.Diagnostics + + switch e := expr.(type) { + case *hclsyntax.RelativeTraversalExpr: + t, d := importToExprToTraversal(e.Source, keyData) + diags = diags.Append(d) + trav = append(trav, t...) + trav = append(trav, e.Traversal...) + + case *hclsyntax.ScopeTraversalExpr: + // a static reference, we can just append the traversal + trav = append(trav, e.Traversal...) + + case *hclsyntax.IndexExpr: + // Get the collection from the index expression + t, d := importToExprToTraversal(e.Collection, keyData) + diags = diags.Append(d) + if diags.HasErrors() { + return nil, diags + } + trav = append(trav, t...) + + // The index key is the only place where we could have variables that + // reference count and each, so we need to parse those independently. + idx, hclDiags := parseImportToKeyExpression(e.Key, keyData) + diags = diags.Append(hclDiags) + + trav = append(trav, idx) + + default: + // if we don't recognise the expression type (which means we are likely + // dealing with a test mock), try and interpret this as an absolute + // traversal + t, d := hcl.AbsTraversalForExpr(e) + diags = diags.Append(d) + trav = append(trav, t...) + } + + return trav, diags +} + +// parseImportToKeyExpression takes an hcl.Expression and parses it as an index key, while +// evaluating any references to count.index or each.key. +func parseImportToKeyExpression(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, hcl.Diagnostics) { + idx := hcl.TraverseIndex{ + SrcRange: expr.Range(), + } + + ctx := &hcl.EvalContext{ + Variables: map[string]cty.Value{ + "each": cty.ObjectVal(map[string]cty.Value{ + "key": keyData.EachKey, + "value": keyData.EachValue, + }), + }, + } + + val, diags := expr.Value(ctx) + if diags.HasErrors() { + // catch the most common case of an unsupported variable and try to + // give the user a slightly more helpful error + for i := range diags { + if diags[i].Summary == "Unknown variable" { + diags[i].Detail += "Only \"each.key\" and \"each.value\" can be used in import address index expressions." + } + } + + return idx, diags + } + + if val.HasMark(marks.Sensitive) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid index expression", + Detail: "Import address index expression cannot be sensitive.", + Subject: expr.Range().Ptr(), + }) + return idx, diags + } + + idx.Key = val + return idx, nil + +} + +func parseImportToPartialAddress(traversal hcl.Traversal) (addrs.PartialExpandedResource, tfdiags.Diagnostics) { + partial, rest, diags := addrs.ParsePartialExpandedResource(traversal) + if diags.HasErrors() { + return addrs.PartialExpandedResource{}, diags + } + + if len(rest) > 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import 'to' expression", + Detail: "The import block 'to' argument does not resolve to a single resource instance.", + Subject: traversal.SourceRange().Ptr(), + }) + } + + return partial, diags +} diff --git a/internal/terraform/eval_provider.go b/internal/terraform/eval_provider.go index a97f347e40..cb3a26922c 100644 --- a/internal/terraform/eval_provider.go +++ b/internal/terraform/eval_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -40,20 +43,21 @@ func buildProviderConfig(ctx EvalContext, addr addrs.AbsProviderConfig, config * } // getProvider returns the providers.Interface and schema for a given provider. -func getProvider(ctx EvalContext, addr addrs.AbsProviderConfig) (providers.Interface, *ProviderSchema, error) { +func getProvider(ctx EvalContext, addr addrs.AbsProviderConfig) (providers.Interface, providers.ProviderSchema, error) { if addr.Provider.Type == "" { // Should never happen panic("GetProvider used with uninitialized provider configuration address") } provider := ctx.Provider(addr) if provider == nil { - return nil, &ProviderSchema{}, fmt.Errorf("provider %s not initialized", addr) + return nil, providers.ProviderSchema{}, fmt.Errorf("provider %s not initialized", addr) } // Not all callers require a schema, so we will leave checking for a nil // schema to the callers. schema, err := ctx.ProviderSchema(addr) if err != nil { - return nil, &ProviderSchema{}, fmt.Errorf("failed to read schema for provider %s: %w", addr, err) + return nil, providers.ProviderSchema{}, fmt.Errorf("failed to read schema for provider %s: %w", addr, err) } + return provider, schema, nil } diff --git a/internal/terraform/eval_provider_test.go b/internal/terraform/eval_provider_test.go index 0a1aeca703..842658bfd1 100644 --- a/internal/terraform/eval_provider_test.go +++ b/internal/terraform/eval_provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/eval_variable.go b/internal/terraform/eval_variable.go index 67a777430f..2bcbc11ec7 100644 --- a/internal/terraform/eval_variable.go +++ b/internal/terraform/eval_variable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -7,12 +10,15 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/lang/marks" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" ) func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *InputValue, cfg *configs.Variable) (cty.Value, tfdiags.Diagnostics) { @@ -90,8 +96,11 @@ func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *In given = defaultVal // must be set, because we checked above that the variable isn't required } - // Apply defaults from the variable's type constraint to the given value - if cfg.TypeDefaults != nil { + // Apply defaults from the variable's type constraint to the converted value, + // unless the converted value is null. We do not apply defaults to top-level + // null values, as doing so could prevent assigning null to a nullable + // variable. + if cfg.TypeDefaults != nil && !given.IsNull() { given = cfg.TypeDefaults.Apply(given) } @@ -180,6 +189,36 @@ func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *In } } + if cfg.Ephemeral { + // An ephemeral input variable always has an ephemeral value inside the + // module, even if the value assigned to it from outside is not. This + // is a useful simplification so that module authors can be explicit + // about what guarantees they are intending to make (regardless of + // current implementation details). Changing the ephemerality of an + // input variable is a breaking change to a module's API. + val = val.Mark(marks.Ephemeral) + } else { + if marks.Contains(val, marks.Ephemeral) { + var subject hcl.Range + if raw.HasSourceRange() { + subject = raw.SourceRange.ToHCL() + } else { + // We shouldn't typically get here for ephemeral values, because + // all of the source types that can represent expressions that + // could potentially produce ephemeral values are those which + // have source locations. This is just here for robustness. + subject = cfg.DeclRange + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This input variable is not declared as accepting a ephemeral values, so it cannot be set to a result derived from an ephemeral value.", + Subject: subject.Ptr(), + }) + } + } + return val, diags } @@ -188,204 +227,291 @@ func prepareFinalInputVariableValue(addr addrs.AbsInputVariableInstance, raw *In // // This must be used only after any side-effects that make the value of the // variable available for use in expression evaluation, such as -// EvalModuleCallArgument for variables in descendent modules. -func evalVariableValidations(addr addrs.AbsInputVariableInstance, config *configs.Variable, expr hcl.Expression, ctx EvalContext) (diags tfdiags.Diagnostics) { - if config == nil || len(config.Validations) == 0 { +// EvalModuleCallArgument for variables in descendant modules. +func evalVariableValidations(addr addrs.AbsInputVariableInstance, ctx EvalContext, rules []*configs.CheckRule, valueRng hcl.Range, validateWalk bool) (diags tfdiags.Diagnostics) { + if len(rules) == 0 { log.Printf("[TRACE] evalVariableValidations: no validation rules declared for %s, so skipping", addr) return nil } log.Printf("[TRACE] evalVariableValidations: validating %s", addr) - // Variable nodes evaluate in the parent module to where they were declared - // because the value expression (n.Expr, if set) comes from the calling - // "module" block in the parent module. - // - // Validation expressions are statically validated (during configuration - // loading) to refer only to the variable being validated, so we can - // bypass our usual evaluation machinery here and just produce a minimal - // evaluation context containing just the required value, and thus avoid - // the problem that ctx's evaluation functions refer to the wrong module. - val := ctx.GetVariableValue(addr) - if val == cty.NilVal { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "No final value for variable", - Detail: fmt.Sprintf("Terraform doesn't have a final value for %s during validation. This is a bug in Terraform; please report it!", addr), - }) + checkState := ctx.Checks() + if !checkState.ConfigHasChecks(addr.ConfigCheckable()) { + // We have nothing to do if this object doesn't have any checks, + // but the "rules" slice should agree that we don't. + if ct := len(rules); ct != 0 { + panic(fmt.Sprintf("check state says that %s should have no rules, but it has %d", addr, ct)) + } return diags } - hclCtx := &hcl.EvalContext{ - Variables: map[string]cty.Value{ - "var": cty.ObjectVal(map[string]cty.Value{ - config.Name: val, - }), - }, - Functions: ctx.EvaluationScope(nil, EvalDataForNoInstanceKey).Functions(), + + // We'll build just one evaluation context covering the data needed by + // all of the rules together, since that'll minimize lock contention + // on the state, plan, etc. + scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) + var refs []*addrs.Reference + for _, rule := range rules { + condRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRef, rule.Condition) + diags = diags.Append(moreDiags) + msgRefs, moreDiags := langrefs.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage) + diags = diags.Append(moreDiags) + refs = append(refs, condRefs...) + refs = append(refs, msgRefs...) + } + if diags.HasErrors() { + // If any of the references were invalid then evaluating the expressions + // will duplicate those errors, so we'll bail out early. + return diags + } + hclCtx, moreDiags := scope.EvalContext(refs) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return diags } - for _, validation := range config.Validations { - const errInvalidCondition = "Invalid variable validation result" - const errInvalidValue = "Invalid value for variable" - var ruleDiags tfdiags.Diagnostics + // HACK: Historically we manually built a very constrained hcl.EvalContext + // here, which only included the value of the one specific input variable + // we're validating, since we didn't yet support referring to anything + // else. That accidentally bypassed our rule that input variables are + // always unknown during the validate walk, and thus accidentally created + // a useful behavior of actually checking constant-only values against + // their validation rules just during "terraform validate", rather than + // having to run "terraform plan". + // + // Although that behavior was accidental, it makes simple validation rules + // more useful and is protected by compatibility promises, and so we'll + // fake it here by overwriting the unknown value that scope.EvalContext + // will have inserted with a possibly-more-known value using the same + // strategy our special code used to use. + ourVal := ctx.NamedValues().GetInputVariableValue(addr) + if ourVal != cty.NilVal { + // (it would be weird for ourVal to be nil here, but we'll tolerate it + // because it was scope.EvalContext's responsibility to check for the + // absent final value, and even if it didn't we'll just get an + // evaluation error when evaluating the expressions below anyway.) - result, moreDiags := validation.Condition.Value(hclCtx) - ruleDiags = ruleDiags.Append(moreDiags) - errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx) - - // The following error handling is a workaround to preserve backwards - // compatibility. Due to an implementation quirk, all prior versions of - // Terraform would treat error messages specified using JSON - // configuration syntax (.tf.json) as string literals, even if they - // contained the "${" template expression operator. This behaviour did - // not match that of HCL configuration syntax, where a template - // expression would result in a validation error. - // - // As a result, users writing or generating JSON configuration syntax - // may have specified error messages which are invalid template - // expressions. As we add support for error message expressions, we are - // unable to perfectly distinguish between these two cases. - // - // To ensure that we don't break backwards compatibility, we have the - // below fallback logic if the error message fails to evaluate. This - // should only have any effect for JSON configurations. The gohcl - // DecodeExpression function behaves differently when the source of the - // expression is a JSON configuration file and a nil context is passed. - if errorDiags.HasErrors() { - // Attempt to decode the expression as a string literal. Passing - // nil as the context forces a JSON syntax string value to be - // interpreted as a string literal. - var errorString string - moreErrorDiags := gohcl.DecodeExpression(validation.ErrorMessage, nil, &errorString) - if !moreErrorDiags.HasErrors() { - // Decoding succeeded, meaning that this is a JSON syntax - // string value. We rewrap that as a cty value to allow later - // decoding to succeed. - errorValue = cty.StringVal(errorString) - - // This warning diagnostic explains this odd behaviour, while - // giving us an escape hatch to change this to a hard failure - // in some future Terraform 1.x version. - errorDiags = hcl.Diagnostics{ - &hcl.Diagnostic{ - Severity: hcl.DiagWarning, - Summary: "Validation error message expression is invalid", - Detail: fmt.Sprintf("The error message provided could not be evaluated as an expression, so Terraform is interpreting it as a string literal.\n\nIn future versions of Terraform, this will be considered an error. Please file a GitHub issue if this would break your workflow.\n\n%s", errorDiags.Error()), - Subject: validation.ErrorMessage.Range().Ptr(), - Context: validation.DeclRange.Ptr(), - Expression: validation.ErrorMessage, - EvalContext: hclCtx, - }, - } - } - - // We want to either report the original diagnostics if the - // fallback failed, or the warning generated above if it succeeded. - ruleDiags = ruleDiags.Append(errorDiags) + // Our goal here is to make sure that a reference to the variable + // we're checking will evaluate to ourVal, regardless of what else + // scope.EvalContext might have put in the variables table. + if hclCtx.Variables == nil { + hclCtx.Variables = make(map[string]cty.Value) } + if varsVal, ok := hclCtx.Variables["var"]; ok { + // Unfortunately we need to unpack and repack the object here, + // because cty values are immutable. + attrs := varsVal.AsValueMap() + attrs[addr.Variable.Name] = ourVal + hclCtx.Variables["var"] = cty.ObjectVal(attrs) + } else { + hclCtx.Variables["var"] = cty.ObjectVal(map[string]cty.Value{ + addr.Variable.Name: ourVal, + }) + } + } + for ix, validation := range rules { + result, ruleDiags := evalVariableValidation(validation, hclCtx, valueRng, addr, ix, validateWalk) diags = diags.Append(ruleDiags) - if ruleDiags.HasErrors() { - log.Printf("[TRACE] evalVariableValidations: %s rule %s check rule evaluation failed: %s", addr, validation.DeclRange, ruleDiags.Err().Error()) - } - if !result.IsKnown() { - log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange) - continue // We'll wait until we've learned more, then. - } - if result.IsNull() { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: errInvalidCondition, - Detail: "Validation condition expression must return either true or false, not null.", - Subject: validation.Condition.Range().Ptr(), - Expression: validation.Condition, - EvalContext: hclCtx, - }) - continue - } - var err error - result, err = convert.Convert(result, cty.Bool) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: errInvalidCondition, - Detail: fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)), - Subject: validation.Condition.Range().Ptr(), - Expression: validation.Condition, - EvalContext: hclCtx, - }) - continue - } - - // Validation condition may be marked if the input variable is bound to - // a sensitive value. This is irrelevant to the validation process, so - // we discard the marks now. - result, _ = result.Unmark() - - if result.True() { - continue - } - - var errorMessage string - if !errorDiags.HasErrors() && errorValue.IsKnown() && !errorValue.IsNull() { - var err error - errorValue, err = convert.Convert(errorValue, cty.String) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid error message", - Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)), - Subject: validation.ErrorMessage.Range().Ptr(), - Expression: validation.ErrorMessage, - EvalContext: hclCtx, - }) - } else { - if marks.Has(errorValue, marks.Sensitive) { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - - Summary: "Error message refers to sensitive values", - Detail: `The error expression used to explain this condition refers to sensitive values. Terraform will not display the resulting message. - -You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`, - - Subject: validation.ErrorMessage.Range().Ptr(), - Expression: validation.ErrorMessage, - EvalContext: hclCtx, - }) - errorMessage = "The error message included a sensitive value, so it will not be displayed." - } else { - errorMessage = strings.TrimSpace(errorValue.AsString()) - } - } - } - if errorMessage == "" { - errorMessage = "Failed to evaluate condition error message." - } - - if expr != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: errInvalidValue, - Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()), - Subject: expr.Range().Ptr(), - Expression: validation.Condition, - EvalContext: hclCtx, - }) + log.Printf("[TRACE] evalVariableValidations: %s status is now %s", addr, result.Status) + if result.Status == checks.StatusFail { + checkState.ReportCheckFailure(addr, addrs.InputValidation, ix, result.FailureMessage) } else { - // Since we don't have a source expression for a root module - // variable, we'll just report the error from the perspective - // of the variable declaration itself. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: errInvalidValue, - Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()), - Subject: config.DeclRange.Ptr(), - Expression: validation.Condition, - EvalContext: hclCtx, - }) + checkState.ReportCheckResult(addr, addrs.InputValidation, ix, result.Status) } } return diags } + +func evalVariableValidation(validation *configs.CheckRule, hclCtx *hcl.EvalContext, valueRng hcl.Range, addr addrs.AbsInputVariableInstance, ix int, validateWalk bool) (checkResult, tfdiags.Diagnostics) { + const errInvalidCondition = "Invalid variable validation result" + const errInvalidValue = "Invalid value for variable" + var diags tfdiags.Diagnostics + + result, moreDiags := validation.Condition.Value(hclCtx) + diags = diags.Append(moreDiags) + errorValue, errorDiags := validation.ErrorMessage.Value(hclCtx) + + // The following error handling is a workaround to preserve backwards + // compatibility. Due to an implementation quirk, all prior versions of + // Terraform would treat error messages specified using JSON + // configuration syntax (.tf.json) as string literals, even if they + // contained the "${" template expression operator. This behaviour did + // not match that of HCL configuration syntax, where a template + // expression would result in a validation error. + // + // As a result, users writing or generating JSON configuration syntax + // may have specified error messages which are invalid template + // expressions. As we add support for error message expressions, we are + // unable to perfectly distinguish between these two cases. + // + // To ensure that we don't break backwards compatibility, we have the + // below fallback logic if the error message fails to evaluate. This + // should only have any effect for JSON configurations. The gohcl + // DecodeExpression function behaves differently when the source of the + // expression is a JSON configuration file and a nil context is passed. + if errorDiags.HasErrors() { + // Attempt to decode the expression as a string literal. Passing + // nil as the context forces a JSON syntax string value to be + // interpreted as a string literal. + var errorString string + moreErrorDiags := gohcl.DecodeExpression(validation.ErrorMessage, nil, &errorString) + if !moreErrorDiags.HasErrors() { + // Decoding succeeded, meaning that this is a JSON syntax + // string value. We rewrap that as a cty value to allow later + // decoding to succeed. + errorValue = cty.StringVal(errorString) + + // This warning diagnostic explains this odd behaviour, while + // giving us an escape hatch to change this to a hard failure + // in some future Terraform 1.x version. + errorDiags = hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Validation error message expression is invalid", + Detail: fmt.Sprintf("The error message provided could not be evaluated as an expression, so Terraform is interpreting it as a string literal.\n\nIn future versions of Terraform, this will be considered an error. Please file a GitHub issue if this would break your workflow.\n\n%s", errorDiags.Error()), + Subject: validation.ErrorMessage.Range().Ptr(), + Context: validation.DeclRange.Ptr(), + Expression: validation.ErrorMessage, + EvalContext: hclCtx, + }, + } + } + + // We want to either report the original diagnostics if the + // fallback failed, or the warning generated above if it succeeded. + diags = diags.Append(errorDiags) + } + + if diags.HasErrors() { + log.Printf("[TRACE] evalVariableValidations: %s rule %s check rule evaluation failed: %s", addr, validation.DeclRange, diags.Err().Error()) + } + if !result.IsKnown() { + log.Printf("[TRACE] evalVariableValidations: %s rule %s condition value is unknown, so skipping validation for now", addr, validation.DeclRange) + + return checkResult{Status: checks.StatusUnknown}, diags // We'll wait until we've learned more, then. + } + if result.IsNull() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errInvalidCondition, + Detail: "Validation condition expression must return either true or false, not null.", + Subject: validation.Condition.Range().Ptr(), + Expression: validation.Condition, + EvalContext: hclCtx, + }) + return checkResult{Status: checks.StatusError}, diags + } + var err error + result, err = convert.Convert(result, cty.Bool) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errInvalidCondition, + Detail: fmt.Sprintf("Invalid validation condition result value: %s.", tfdiags.FormatError(err)), + Subject: validation.Condition.Range().Ptr(), + Expression: validation.Condition, + EvalContext: hclCtx, + }) + return checkResult{Status: checks.StatusError}, diags + } + + // Validation condition may be marked if the input variable is bound to + // a sensitive value. This is irrelevant to the validation process, so + // we discard the marks now. + result, _ = result.Unmark() + status := checks.StatusForCtyValue(result) + + if status != checks.StatusFail { + return checkResult{Status: status}, diags + } + + if !errorValue.IsKnown() { + if validateWalk { + log.Printf("[DEBUG] evalVariableValidations: %s rule %s error_message value is unknown, so skipping validation for now", addr, validation.DeclRange) + return checkResult{Status: checks.StatusUnknown}, diags + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid error message", + Detail: "Unsuitable value for error message: expression refers to values that won't be known until the apply phase.", + Subject: validation.ErrorMessage.Range().Ptr(), + Expression: validation.ErrorMessage, + EvalContext: hclCtx, + Extra: diagnosticCausedByUnknown(true), + }) + return checkResult{ + Status: checks.StatusError, + }, diags + } + + var errorMessage string + if !errorDiags.HasErrors() && !errorValue.IsNull() { + var err error + errorValue, err = convert.Convert(errorValue, cty.String) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid error message", + Detail: fmt.Sprintf("Unsuitable value for error message: %s.", tfdiags.FormatError(err)), + Subject: validation.ErrorMessage.Range().Ptr(), + Expression: validation.ErrorMessage, + EvalContext: hclCtx, + }) + } else { + if marks.Has(errorValue, marks.Sensitive) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + + Summary: "Error message refers to sensitive values", + Detail: `The error expression used to explain this condition refers to sensitive values. Terraform will not display the resulting message. + +You can correct this by removing references to sensitive values, or by carefully using the nonsensitive() function if the expression will not reveal the sensitive data.`, + + Subject: validation.ErrorMessage.Range().Ptr(), + Expression: validation.ErrorMessage, + EvalContext: hclCtx, + }) + errorMessage = "The error message included a sensitive value, so it will not be displayed." + } else if marks.Has(errorValue, marks.Ephemeral) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + + Summary: "Error message refers to ephemeral values", + Detail: `The error expression used to explain this condition refers to ephemeral values. Terraform will not display the resulting message. + +You can correct this by removing references to ephemeral values, or by carefully using the ephemeralasnull() function if the expression will not reveal the ephemeral data.`, + + Subject: validation.ErrorMessage.Range().Ptr(), + Expression: validation.ErrorMessage, + EvalContext: hclCtx, + }) + errorMessage = "The error message included a sensitive value, so it will not be displayed." + } else { + errorMessage = strings.TrimSpace(errorValue.AsString()) + } + } + } + if errorMessage == "" { + errorMessage = "Failed to evaluate condition error message." + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: errInvalidValue, + Detail: fmt.Sprintf("%s\n\nThis was checked by the validation rule at %s.", errorMessage, validation.DeclRange.String()), + Subject: valueRng.Ptr(), + Expression: validation.Condition, + EvalContext: hclCtx, + Extra: &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addr.CheckRule(addrs.InputValidation, ix), + }, + }) + + return checkResult{ + Status: status, + FailureMessage: errorMessage, + }, diags +} diff --git a/internal/terraform/eval_variable_test.go b/internal/terraform/eval_variable_test.go index cb6c1bb2b8..910bbd4626 100644 --- a/internal/terraform/eval_variable_test.go +++ b/internal/terraform/eval_variable_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,11 +9,15 @@ import ( "testing" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcltest" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -68,6 +75,145 @@ func TestPrepareFinalInputVariableValue(t *testing.T) { nullable = false type = string } + variable "complex_type_with_nested_default_optional" { + type = set(object({ + name = string + schedules = set(object({ + name = string + cold_storage_after = optional(number, 10) + })) + })) + } + variable "complex_type_with_nested_complex_types" { + type = object({ + name = string + nested_object = object({ + name = string + value = optional(string, "foo") + }) + nested_object_with_default = optional(object({ + name = string + value = optional(string, "bar") + }), { + name = "nested_object_with_default" + }) + }) + } + // https://github.com/hashicorp/terraform/issues/32152 + // This variable was originally added to test that optional attribute + // metadata is stripped from empty default collections. Essentially, you + // should be able to mix and match custom and default values for the + // optional_list attribute. + variable "complex_type_with_empty_default_and_nested_optional" { + type = list(object({ + name = string + optional_list = optional(list(object({ + string = string + optional_string = optional(string) + })), []) + })) + } + // https://github.com/hashicorp/terraform/issues/32160#issuecomment-1302783910 + // These variables were added to test the specific use case from this + // GitHub comment. + variable "empty_object_with_optional_nested_object_with_optional_bool" { + type = object({ + thing = optional(object({ + flag = optional(bool, false) + })) + }) + default = {} + } + variable "populated_object_with_optional_nested_object_with_optional_bool" { + type = object({ + thing = optional(object({ + flag = optional(bool, false) + })) + }) + default = { + thing = {} + } + } + variable "empty_object_with_default_nested_object_with_optional_bool" { + type = object({ + thing = optional(object({ + flag = optional(bool, false) + }), {}) + }) + default = {} + } + // https://github.com/hashicorp/terraform/issues/32160 + // This variable was originally added to test that optional objects do + // get created containing only their defaults. Instead they should be + // left empty. We do not expect nested_object to be created just because + // optional_string has a default value. + variable "object_with_nested_object_with_required_and_optional_attributes" { + type = object({ + nested_object = optional(object({ + string = string + optional_string = optional(string, "optional") + })) + }) + } + // https://github.com/hashicorp/terraform/issues/32157 + // Similar to above, we want to see that merging combinations of the + // nested_object into a single collection doesn't crash because of + // inconsistent elements. + variable "list_with_nested_object_with_required_and_optional_attributes" { + type = list(object({ + nested_object = optional(object({ + string = string + optional_string = optional(string, "optional") + })) + })) + } + // https://github.com/hashicorp/terraform/issues/32109 + // This variable was originally introduced to test the behaviour of + // the dynamic type constraint. You should be able to use the 'any' + // constraint and introduce empty, null, and populated values into the + // list. + variable "list_with_nested_list_of_any" { + type = list(object({ + a = string + b = optional(list(any)) + })) + default = [ + { + a = "a" + }, + { + a = "b" + b = [1] + } + ] + } + // https://github.com/hashicorp/terraform/issues/32396 + // This variable was originally introduced to test the behaviour of the + // dynamic type constraint. You should be able to set primitive types in + // the list consistently. + variable "list_with_nested_collections_dynamic_with_default" { + type = list( + object({ + name = optional(string, "default") + taints = optional(list(map(any)), []) + }) + ) + } + // https://github.com/hashicorp/terraform/issues/32752 + // This variable was introduced to make sure the evaluation doesn't + // crash even when the types are wrong. + variable "invalid_nested_type" { + type = map( + object({ + rules = map( + object({ + destination_addresses = optional(list(string), []) + }) + ) + }) + ) + default = {} + } ` cfg := testModuleInline(t, map[string]string{ "main.tf": cfgSrc, @@ -398,6 +544,292 @@ func TestPrepareFinalInputVariableValue(t *testing.T) { cty.UnknownVal(cty.String), ``, }, + { + "list_with_nested_collections_dynamic_with_default", + cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("default"), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("complex"), + "taints": cty.ListVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("my_key"), + "value": cty.StringVal("my_value"), + }), + }), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("default"), + "taints": cty.ListValEmpty(cty.Map(cty.String)), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("complex"), + "taints": cty.ListVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("my_key"), + "value": cty.StringVal("my_value"), + }), + }), + }), + }), + ``, + }, + + // complex types + + { + "complex_type_with_nested_default_optional", + cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test1"), + "schedules": cty.SetVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "name": cty.StringVal("daily"), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test2"), + "schedules": cty.SetVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "name": cty.StringVal("daily"), + }), + cty.MapVal(map[string]cty.Value{ + "name": cty.StringVal("weekly"), + "cold_storage_after": cty.StringVal("0"), + }), + }), + }), + }), + cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test1"), + "schedules": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("daily"), + "cold_storage_after": cty.NumberIntVal(10), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test2"), + "schedules": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("daily"), + "cold_storage_after": cty.NumberIntVal(10), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("weekly"), + "cold_storage_after": cty.NumberIntVal(0), + }), + }), + }), + }), + ``, + }, + { + "complex_type_with_nested_complex_types", + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("object"), + "nested_object": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("nested_object"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("object"), + "nested_object": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("nested_object"), + "value": cty.StringVal("foo"), + }), + "nested_object_with_default": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("nested_object_with_default"), + "value": cty.StringVal("bar"), + }), + }), + ``, + }, + { + "complex_type_with_empty_default_and_nested_optional", + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("abc"), + "optional_list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("child"), + "optional_string": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("def"), + "optional_list": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "string": cty.String, + "optional_string": cty.String, + }))), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("abc"), + "optional_list": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("child"), + "optional_string": cty.NullVal(cty.String), + }), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("def"), + "optional_list": cty.ListValEmpty(cty.Object(map[string]cty.Type{ + "string": cty.String, + "optional_string": cty.String, + })), + }), + }), + ``, + }, + { + "object_with_nested_object_with_required_and_optional_attributes", + cty.EmptyObjectVal, + cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ + "string": cty.String, + "optional_string": cty.String, + })), + }), + ``, + }, + { + "empty_object_with_optional_nested_object_with_optional_bool", + cty.NilVal, + cty.ObjectVal(map[string]cty.Value{ + "thing": cty.NullVal(cty.Object(map[string]cty.Type{ + "flag": cty.Bool, + })), + }), + ``, + }, + { + "populated_object_with_optional_nested_object_with_optional_bool", + cty.NilVal, + cty.ObjectVal(map[string]cty.Value{ + "thing": cty.ObjectVal(map[string]cty.Value{ + "flag": cty.False, + }), + }), + ``, + }, + { + "empty_object_with_default_nested_object_with_optional_bool", + cty.NilVal, + cty.ObjectVal(map[string]cty.Value{ + "thing": cty.ObjectVal(map[string]cty.Value{ + "flag": cty.False, + }), + }), + ``, + }, + { + "list_with_nested_object_with_required_and_optional_attributes", + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("string"), + "optional_string": cty.NullVal(cty.String), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ + "string": cty.String, + "optional_string": cty.String, + })), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("string"), + "optional_string": cty.StringVal("optional"), + }), + }), + cty.ObjectVal(map[string]cty.Value{ + "nested_object": cty.NullVal(cty.Object(map[string]cty.Type{ + "string": cty.String, + "optional_string": cty.String, + })), + }), + }), + ``, + }, + { + "list_with_nested_list_of_any", + cty.NilVal, + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("a"), + "b": cty.NullVal(cty.List(cty.Number)), + }), + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "b": cty.ListVal([]cty.Value{ + cty.NumberIntVal(1), + }), + }), + }), + ``, + }, + { + "list_with_nested_collections_dynamic_with_default", + cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("default"), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("complex"), + "taints": cty.ListVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("my_key"), + "value": cty.StringVal("my_value"), + }), + }), + }), + }), + cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("default"), + "taints": cty.ListValEmpty(cty.Map(cty.String)), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("complex"), + "taints": cty.ListVal([]cty.Value{ + cty.MapVal(map[string]cty.Value{ + "key": cty.StringVal("my_key"), + "value": cty.StringVal("my_value"), + }), + }), + }), + }), + ``, + }, + { + "invalid_nested_type", + cty.MapVal(map[string]cty.Value{ + "mysql": cty.ObjectVal(map[string]cty.Value{ + "rules": cty.ObjectVal(map[string]cty.Value{ + "destination_addresses": cty.ListVal([]cty.Value{cty.StringVal("192.168.0.1")}), + }), + }), + }), + cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{ + "rules": cty.Map(cty.Object(map[string]cty.Type{ + "destination_addresses": cty.List(cty.String), + })), + }))), + `Invalid value for input variable: Unsuitable value for var.invalid_nested_type set from outside of the configuration: incorrect map element type: attribute "rules": element "destination_addresses": object required.`, + }, // sensitive { @@ -679,12 +1111,14 @@ func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { given cty.Value wantErr []string wantWarn []string + status checks.Status }{ // Valid variable validation declaration, assigned value which passes // the condition generates no diagnostics. { varName: "valid", given: cty.StringVal("foo"), + status: checks.StatusPass, }, // Assigning a value which fails the condition generates an error // message with the expression successfully evaluated. @@ -695,6 +1129,7 @@ func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { "Invalid value for variable", "Valid template string bar", }, + status: checks.StatusFail, }, // Invalid variable validation declaration due to an unparseable // template string. Assigning a value which passes the condition @@ -706,6 +1141,7 @@ func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { "Validation error message expression is invalid", "Missing expression; Expected the start of an expression, but found the end of the file.", }, + status: checks.StatusPass, }, // Assigning a value which fails the condition generates an error // message including the configured string interpreted as a literal @@ -721,6 +1157,7 @@ func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { "Validation error message expression is invalid", "Missing expression; Expected the start of an expression, but found the end of the file.", }, + status: checks.StatusFail, }, } @@ -738,18 +1175,26 @@ func TestEvalVariableValidations_jsonErrorMessageEdgeCase(t *testing.T) { // We need a minimal scope to allow basic functions to be passed to // the HCL scope - ctx.EvaluationScopeScope = &lang.Scope{} - ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { - if got, want := addr.String(), varAddr.String(); got != want { - t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) - } - return test.given + ctx.EvaluationScopeScope = &lang.Scope{ + Data: &fakeEvaluationData{ + inputVariables: map[addrs.InputVariable]cty.Value{ + varAddr.Variable: test.given, + }, + }, } + ctx.NamedValuesState = namedvals.NewState() + ctx.NamedValuesState.SetInputVariableValue(varAddr, test.given) + ctx.ChecksState = checks.NewState(cfg) + ctx.ChecksState.ReportCheckableObjects(varAddr.ConfigCheckable(), addrs.MakeSet[addrs.Checkable](varAddr)) gotDiags := evalVariableValidations( - varAddr, varCfg, nil, ctx, + varAddr, ctx, varCfg.Validations, varCfg.DeclRange, false, ) + if ctx.ChecksState.ObjectCheckStatus(varAddr) != test.status { + t.Errorf("expected check result %s but instead %s", test.status, ctx.ChecksState.ObjectCheckStatus(varAddr)) + } + if len(test.wantErr) == 0 && len(test.wantWarn) == 0 { if len(gotDiags) > 0 { t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) @@ -830,12 +1275,14 @@ variable "bar" { varName string given cty.Value wantErr []string + status checks.Status }{ // Validations pass on a sensitive variable with an error message which // would generate a sensitive value { varName: "foo", given: cty.StringVal("boop"), + status: checks.StatusPass, }, // Assigning a value which fails the condition generates a sensitive // error message, which is elided and generates another error @@ -847,12 +1294,14 @@ variable "bar" { "The error message included a sensitive value, so it will not be displayed.", "Error message refers to sensitive values", }, + status: checks.StatusFail, }, // Validations pass on a sensitive variable with a correctly defined // error message { varName: "bar", given: cty.StringVal("boop"), + status: checks.StatusPass, }, // Assigning a value which fails the condition generates a nonsensitive // error message, which is displayed @@ -863,6 +1312,7 @@ variable "bar" { "Invalid value for variable", "Bar must be 4 characters, not 3.", }, + status: checks.StatusFail, }, } @@ -880,22 +1330,30 @@ variable "bar" { // We need a minimal scope to allow basic functions to be passed to // the HCL scope - ctx.EvaluationScopeScope = &lang.Scope{} - ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { - if got, want := addr.String(), varAddr.String(); got != want { - t.Errorf("incorrect argument to GetVariableValue: got %s, want %s", got, want) - } - if varCfg.Sensitive { - return test.given.Mark(marks.Sensitive) - } else { - return test.given - } + varVal := test.given + if varCfg.Sensitive { + varVal = varVal.Mark(marks.Sensitive) } + ctx.EvaluationScopeScope = &lang.Scope{ + Data: &fakeEvaluationData{ + inputVariables: map[addrs.InputVariable]cty.Value{ + varAddr.Variable: varVal, + }, + }, + } + ctx.NamedValuesState = namedvals.NewState() + ctx.NamedValuesState.SetInputVariableValue(varAddr, varVal) + ctx.ChecksState = checks.NewState(cfg) + ctx.ChecksState.ReportCheckableObjects(varAddr.ConfigCheckable(), addrs.MakeSet[addrs.Checkable](varAddr)) gotDiags := evalVariableValidations( - varAddr, varCfg, nil, ctx, + varAddr, ctx, varCfg.Validations, varCfg.DeclRange, false, ) + if ctx.ChecksState.ObjectCheckStatus(varAddr) != test.status { + t.Errorf("expected check result %s but instead %s", test.status, ctx.ChecksState.ObjectCheckStatus(varAddr)) + } + if len(test.wantErr) == 0 { if len(gotDiags) > 0 { t.Errorf("no diags expected, got %s", gotDiags.Err().Error()) @@ -918,3 +1376,57 @@ variable "bar" { }) } } + +func TestEvalVariableValidation_unknownErrorMessage(t *testing.T) { + t.Run("known condition, unknown error_message", func(t *testing.T) { + rule := &configs.CheckRule{ + Condition: hcltest.MockExprLiteral(cty.False), + ErrorMessage: hcltest.MockExprLiteral(cty.UnknownVal(cty.String)), + } + hclCtx := &hcl.EvalContext{} + varAddr := addrs.AbsInputVariableInstance{ + Module: addrs.RootModuleInstance, + Variable: addrs.InputVariable{Name: "foo"}, + } + + // this should not produce any error when validationWalk is true + result, diags := evalVariableValidation(rule, hclCtx, hcl.Range{}, varAddr, 0, true) + if got, want := result.Status, checks.StatusUnknown; got != want { + t.Errorf("wrong result.Status\ngot: %s\nwant: %s", got, want) + } + if diags.HasErrors() { + t.Fatal(diags.ErrWithWarnings()) + } + + // any other time this should result in an error + result, diags = evalVariableValidation(rule, hclCtx, hcl.Range{}, varAddr, 0, false) + if got, want := result.Status, checks.StatusError; got != want { + t.Errorf("wrong result.Status\ngot: %s\nwant: %s", got, want) + } + if !diags.HasErrors() { + t.Fatalf("unexpected success; want error") + } + found := false + hasCorrectExtra := false + wantDesc := tfdiags.Description{ + Summary: "Invalid error message", + Detail: "Unsuitable value for error message: expression refers to values that won't be known until the apply phase.", + } + for _, diag := range diags { + gotDesc := diag.Description() + if diag.Severity() == tfdiags.Error && gotDesc.Summary == wantDesc.Summary && gotDesc.Detail == wantDesc.Detail { + found = true + hasCorrectExtra = tfdiags.DiagnosticCausedByUnknown(diag) + break + } + } + if !found { + t.Errorf("missing expected error diagnostic\nwant: %s: %s\ngot: %s", + wantDesc.Summary, wantDesc.Detail, + diags.Err().Error(), + ) + } else if !hasCorrectExtra { + t.Errorf("diagnostic is not marked as being 'caused by unknown'") + } + }) +} diff --git a/internal/terraform/evaluate.go b/internal/terraform/evaluate.go index f8df6a2dc0..d1a8797d60 100644 --- a/internal/terraform/evaluate.go +++ b/internal/terraform/evaluate.go @@ -1,23 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" - "os" - "path/filepath" - "sync" + "time" - "github.com/agext/levenshtein" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/didyoumean" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -35,15 +39,26 @@ type Evaluator struct { // Config is the root node in the configuration tree. Config *configs.Config - // VariableValues is a map from variable names to their associated values, - // within the module indicated by ModulePath. VariableValues is modified - // concurrently, and so it must be accessed only while holding - // VariableValuesLock. - // - // The first map level is string representations of addr.ModuleInstance - // values, while the second level is variable names. - VariableValues map[string]map[string]cty.Value - VariableValuesLock *sync.Mutex + // Instances tracks the dynamic instances that are associated with each + // module call or resource. The graph walk gradually registers the + // set of instances for each object within the graph nodes for those + // objects, and so as long as the graph has been built correctly the + // set of instances for an object should always be available by the time + // we're evaluating expressions that refer to it. + Instances *instances.Expander + + // NamedValues is where we keep the values of already-evaluated input + // variables, local values, and output values. + NamedValues *namedvals.State + + // EphemeralResources tracks the currently-open instances of any ephemeral + // resources. + EphemeralResources *ephemeral.Resources + + // Deferrals tracks resources and modules that have had either their + // expansion or their specific planned actions deferred to a future + // plan/apply round. + Deferrals *deferring.Deferred // Plugins is the library of available plugin components (providers and // provisioners) that we have available to help us evaluate expressions @@ -60,6 +75,13 @@ type Evaluator struct { // Changes is the set of proposed changes, embedded in a wrapper that // ensures they can be safely accessed and modified concurrently. Changes *plans.ChangesSync + + // FunctionResults carries forward the global cache of function results to + // be used when building out all the builtin functions returned in the + // Scope. + FunctionResults *lang.FunctionResults + + PlanTimestamp time.Time } // Scope creates an evaluation scope for the given module path and optional @@ -68,19 +90,24 @@ type Evaluator struct { // If the "self" argument is nil then the "self" object is not available // in evaluated expressions. Otherwise, it behaves as an alias for the given // address. -func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable) *lang.Scope { +func (e *Evaluator) Scope(data lang.Data, self addrs.Referenceable, source addrs.Referenceable, extFuncs lang.ExternalFuncs) *lang.Scope { return &lang.Scope{ - Data: data, - SelfAddr: self, - PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval, - BaseDir: ".", // Always current working directory for now. + Data: data, + ParseRef: addrs.ParseRef, + SelfAddr: self, + SourceAddr: source, + PureOnly: e.Operation != walkApply && e.Operation != walkDestroy && e.Operation != walkEval, + BaseDir: ".", // Always current working directory for now. + PlanTimestamp: e.PlanTimestamp, + ExternalFuncs: extFuncs, + FunctionResults: e.FunctionResults, } } // evaluationStateData is an implementation of lang.Data that resolves // references primarily (but not exclusively) using information from a State. type evaluationStateData struct { - Evaluator *Evaluator + *evaluationData // ModulePath is the path through the dynamic module tree to the module // that references will be resolved relative to. @@ -134,6 +161,13 @@ var EvalDataForNoInstanceKey = InstanceKeyEvalData{} // evaluationStateData must implement lang.Data var _ lang.Data = (*evaluationStateData)(nil) +// StaticValidateReferences calls [Evaluator.StaticValidateReferences] on +// the evaluator embedded in this data object, using this data object's +// static module path. +func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { + return d.Evaluator.StaticValidateReferences(refs, d.ModulePath.Module(), self, source) +} + func (d *evaluationStateData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics switch addr.Name { @@ -176,7 +210,7 @@ func (d *evaluationStateData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `each.value cannot be used in this context`, - Detail: `A reference to "each.value" has been used in a context in which it unavailable, such as when the configuration no longer contains the value in its "for_each" expression. Remove this reference to each.value in your configuration to work around this error.`, + Detail: `A reference to "each.value" has been used in a context in which it is unavailable, such as when the configuration no longer contains the value in its "for_each" expression. Remove this reference to each.value in your configuration to work around this error.`, Subject: rng.ToHCL().Ptr(), }) return cty.UnknownVal(cty.DynamicPseudoType), diags @@ -208,7 +242,7 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd // First we'll make sure the requested value is declared in configuration, // so we can produce a nice message if not. - moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + moduleConfig := d.Evaluator.Config.DescendantForInstance(d.ModulePath) if moduleConfig == nil { // should never happen, since we can't be evaluating in a module // that wasn't mentioned in configuration. @@ -221,7 +255,7 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd for k := range moduleConfig.Module.Variables { suggestions = append(suggestions, k) } - suggestion := nameSuggestion(addr.Name, suggestions) + suggestion := didyoumean.NameSuggestion(addr.Name, suggestions) if suggestion != "" { suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) } else { @@ -236,8 +270,6 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd }) return cty.DynamicVal, diags } - d.Evaluator.VariableValuesLock.Lock() - defer d.Evaluator.VariableValuesLock.Unlock() // During the validate walk, input variables are always unknown so // that we are validating the configuration for all possible input values @@ -253,48 +285,27 @@ func (d *evaluationStateData) GetInputVariable(addr addrs.InputVariable, rng tfd // being liberal in what it accepts because the subsequent plan walk has // more information available and so can be more conservative. if d.Operation == walkValidate { - // Ensure variable sensitivity is captured in the validate walk + // We should still capture the statically-configured marks during + // the validate walk. + ret := cty.UnknownVal(config.Type) if config.Sensitive { - return cty.UnknownVal(config.Type).Mark(marks.Sensitive), diags + ret = ret.Mark(marks.Sensitive) } - return cty.UnknownVal(config.Type), diags + if config.Ephemeral { + ret = ret.Mark(marks.Ephemeral) + } + return ret, diags } - moduleAddrStr := d.ModulePath.String() - vals := d.Evaluator.VariableValues[moduleAddrStr] - if vals == nil { - return cty.UnknownVal(config.Type), diags - } + val := d.Evaluator.NamedValues.GetInputVariableValue(d.ModulePath.InputVariable(addr.Name)) - // d.Evaluator.VariableValues should always contain valid "final values" - // for variables, which is to say that they have already had type - // conversions, validations, and default value handling applied to them. - // Those are the responsibility of the graph notes representing the - // variable declarations. Therefore here we just trust that we already - // have a correct value. - - val, isSet := vals[addr.Name] - if !isSet { - // We should not be able to get here without having a valid value - // for every variable, so this always indicates a bug in either - // the graph builder (not including all the needed nodes) or in - // the graph nodes representing variables. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Reference to unresolved input variable`, - Detail: fmt.Sprintf( - `The final value for %s is missing in Terraform's evaluation context. This is a bug in Terraform; please report it!`, - addr.Absolute(d.ModulePath), - ), - Subject: rng.ToHCL().Ptr(), - }) - val = cty.UnknownVal(config.Type) - } - - // Mark if sensitive + // Mark if sensitive and/or ephemeral if config.Sensitive { val = val.Mark(marks.Sensitive) } + if config.Ephemeral { + val = val.Mark(marks.Ephemeral) + } return val, diags } @@ -304,7 +315,7 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S // First we'll make sure the requested value is declared in configuration, // so we can produce a nice message if not. - moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + moduleConfig := d.Evaluator.Config.DescendantForInstance(d.ModulePath) if moduleConfig == nil { // should never happen, since we can't be evaluating in a module // that wasn't mentioned in configuration. @@ -317,7 +328,7 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S for k := range moduleConfig.Module.Locals { suggestions = append(suggestions, k) } - suggestion := nameSuggestion(addr.Name, suggestions) + suggestion := didyoumean.NameSuggestion(addr.Name, suggestions) if suggestion != "" { suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) } @@ -331,12 +342,7 @@ func (d *evaluationStateData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.S return cty.DynamicVal, diags } - val := d.Evaluator.State.LocalValue(addr.Absolute(d.ModulePath)) - if val == cty.NilVal { - // Not evaluated yet? - val = cty.DynamicVal - } - + val := d.Evaluator.NamedValues.GetLocalValue(addr.Absolute(d.ModulePath)) return val, diags } @@ -346,8 +352,9 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc // Output results live in the module that declares them, which is one of // the child module instances of our current module path. moduleAddr := d.ModulePath.Module().Child(addr.Name) + absAddr := addr.Absolute(d.ModulePath) - parentCfg := d.Evaluator.Config.DescendentForInstance(d.ModulePath) + parentCfg := d.Evaluator.Config.DescendantForInstance(d.ModulePath) callConfig, ok := parentCfg.Module.ModuleCalls[addr.Name] if !ok { diags = diags.Append(&hcl.Diagnostic{ @@ -362,7 +369,7 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc // We'll consult the configuration to see what output names we are // expecting, so we can ensure the resulting object is of the expected // type even if our data is incomplete for some reason. - moduleConfig := d.Evaluator.Config.Descendent(moduleAddr) + moduleConfig := d.Evaluator.Config.Descendant(moduleAddr) if moduleConfig == nil { // should never happen, since we have a valid module call above, this // should be caught during static validation. @@ -370,260 +377,123 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc } outputConfigs := moduleConfig.Module.Outputs - // Collect all the relevant outputs that current exist in the state. - // We know the instance path up to this point, and the child module name, - // so we only need to store these by instance key. - stateMap := map[addrs.InstanceKey]map[string]cty.Value{} - for _, output := range d.Evaluator.State.ModuleOutputs(d.ModulePath, addr) { - _, callInstance := output.Addr.Module.CallInstance() - instance, ok := stateMap[callInstance.Key] - if !ok { - instance = map[string]cty.Value{} - stateMap[callInstance.Key] = instance - } - - instance[output.Addr.OutputValue.Name] = output.Value - } - - // Get all changes that reside for this module call within our path. - // The change contains the full addr, so we can key these with strings. - changesMap := map[addrs.InstanceKey]map[string]*plans.OutputChangeSrc{} - for _, change := range d.Evaluator.Changes.GetOutputChanges(d.ModulePath, addr) { - _, callInstance := change.Addr.Module.CallInstance() - instance, ok := changesMap[callInstance.Key] - if !ok { - instance = map[string]*plans.OutputChangeSrc{} - changesMap[callInstance.Key] = instance - } - - instance[change.Addr.OutputValue.Name] = change - } - - // Build up all the module objects, creating a map of values for each - // module instance. - moduleInstances := map[addrs.InstanceKey]map[string]cty.Value{} - - // create a dummy object type for validation below - unknownMap := map[string]cty.Type{} - - // the structure is based on the configuration, so iterate through all the - // defined outputs, and add any instance state or changes we find. - for _, cfg := range outputConfigs { - // record the output names for validation - unknownMap[cfg.Name] = cty.DynamicPseudoType - - // get all instance output for this path from the state - for key, states := range stateMap { - outputState, ok := states[cfg.Name] - if !ok { - continue - } - - instance, ok := moduleInstances[key] - if !ok { - instance = map[string]cty.Value{} - moduleInstances[key] = instance - } - - instance[cfg.Name] = outputState - - if cfg.Sensitive { - instance[cfg.Name] = outputState.Mark(marks.Sensitive) - } - } - - // any pending changes override the state state values - for key, changes := range changesMap { - changeSrc, ok := changes[cfg.Name] - if !ok { - continue - } - - instance, ok := moduleInstances[key] - if !ok { - instance = map[string]cty.Value{} - moduleInstances[key] = instance - } - - change, err := changeSrc.Decode() - if err != nil { - // This should happen only if someone has tampered with a plan - // file, so we won't bother with a pretty error for it. - diags = diags.Append(fmt.Errorf("planned change for %s could not be decoded: %s", addr, err)) - instance[cfg.Name] = cty.DynamicVal - continue - } - - instance[cfg.Name] = change.After - - if change.Sensitive { - instance[cfg.Name] = change.After.Mark(marks.Sensitive) - } - } - } - - var ret cty.Value - - // compile the outputs into the correct value type for the each mode - switch { - case callConfig.Count != nil: - // figure out what the last index we have is - length := -1 - for key := range moduleInstances { - intKey, ok := key.(addrs.IntKey) - if !ok { - // old key from state which is being dropped - continue - } - if int(intKey) >= length { - length = int(intKey) + 1 - } - } - - if length > 0 { - vals := make([]cty.Value, length) - for key, instance := range moduleInstances { - intKey, ok := key.(addrs.IntKey) - if !ok { - // old key from state which is being dropped - continue - } - - vals[int(intKey)] = cty.ObjectVal(instance) - } - - // Insert unknown values where there are any missing instances - for i, v := range vals { - if v.IsNull() { - vals[i] = cty.DynamicVal - continue - } - } - ret = cty.TupleVal(vals) - } else { - ret = cty.EmptyTupleVal - } - - case callConfig.ForEach != nil: - vals := make(map[string]cty.Value) - for key, instance := range moduleInstances { - strKey, ok := key.(addrs.StringKey) - if !ok { - continue - } - - vals[string(strKey)] = cty.ObjectVal(instance) - } - - if len(vals) > 0 { - ret = cty.ObjectVal(vals) - } else { - ret = cty.EmptyObjectVal - } - - default: - val, ok := moduleInstances[addrs.NoKey] - if !ok { - // create the object if there wasn't one known - val = map[string]cty.Value{} - for k := range outputConfigs { - val[k] = cty.DynamicVal - } - } - - ret = cty.ObjectVal(val) - } - - // The module won't be expanded during validation, so we need to return an - // unknown value. This will ensure the types looks correct, since we built - // the objects based on the configuration. + // We don't do instance expansion during validation, and so we need to + // return an unknown value. Technically we should always return + // cty.DynamicVal here because the final value during plan will always + // be an object or tuple type with unpredictable attributes/elements, + // but because we never actually carry values forward from validation to + // planning we lie a little here and return unknown list and map types, + // just to give us more opportunities to catch author mistakes during + // validation. + // + // This means that in practice any expression that refers to a module + // call must be written to be valid for either a collection type or + // structural type of similar kind, so that it can be considered as + // valid during both the validate and plan walks. if d.Operation == walkValidate { - // While we know the type here and it would be nice to validate whether - // indexes are valid or not, because tuples and objects have fixed - // numbers of elements we can't simply return an unknown value of the - // same type since we have not expanded any instances during - // validation. - // - // In order to validate the expression a little precisely, we'll create - // an unknown map or list here to get more type information. - ty := cty.Object(unknownMap) + atys := make(map[string]cty.Type, len(outputConfigs)) + for name := range outputConfigs { + atys[name] = cty.DynamicPseudoType // output values are dynamically-typed + } + instTy := cty.Object(atys) + switch { case callConfig.Count != nil: - ret = cty.UnknownVal(cty.List(ty)) + return cty.UnknownVal(cty.List(instTy)), diags case callConfig.ForEach != nil: - ret = cty.UnknownVal(cty.Map(ty)) + return cty.UnknownVal(cty.Map(instTy)), diags default: - ret = cty.UnknownVal(ty) + return cty.UnknownVal(instTy), diags } } - return ret, diags -} + // For all other walk types, we proceed to dynamic evaluation of individual + // instances, using the global instance expander. An earlier graph node + // should always have registered the expansion of this module call before + // we get here, unless there's a bug in the graph builders. + allInstances := d.Evaluator.Instances + instKeyType, instKeys, known := allInstances.ExpandAbsModuleCall(absAddr) + if !known { + // If we don't know which instances exist then we can't really predict + // anything at all. We can't even predict the return type based on + // instKeyType because output values are dynamically-typed and so + // our final result will always be an object or tuple type whose + // attribute/element count we cannot predict. + return cty.DynamicVal, diags + } -func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { - var diags tfdiags.Diagnostics - switch addr.Name { + instanceObjVal := func(instKey addrs.InstanceKey) (cty.Value, tfdiags.Diagnostics) { + // This function must always return a valid value, even if it's + // just a cty.DynamicVal placeholder accompanying error diagnostics. + var diags tfdiags.Diagnostics - case "cwd": - var err error - var wd string - if d.Evaluator.Meta != nil { - // Meta is always non-nil in the normal case, but some test cases - // are not so realistic. - wd = d.Evaluator.Meta.OriginalWorkingDir - } - if wd == "" { - wd, err = os.Getwd() - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Failed to get working directory`, - Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags + namedVals := d.Evaluator.NamedValues + moduleInstAddr := absAddr.Instance(instKey) + attrs := make(map[string]cty.Value, len(outputConfigs)) + for name := range outputConfigs { + outputAddr := moduleInstAddr.OutputValue(name) + + // Although we do typically expect the graph dependencies to + // ensure that values get registered before they are needed, + // we track depedencies with specific output values where + // possible, instead of with entire module calls, and so + // in this specific case it's valid for some of this call's + // output values to not be known yet, with the graph builder + // being responsible for making sure that no expression + // in the configuration can actually observe that. + if !namedVals.HasOutputValue(outputAddr) { + attrs[name] = cty.DynamicVal + continue } - } - // The current working directory should always be absolute, whether we - // just looked it up or whether we were relying on ContextMeta's - // (possibly non-normalized) path. - wd, err = filepath.Abs(wd) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Failed to get working directory`, - Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags + outputVal := namedVals.GetOutputValue(outputAddr) + attrs[name] = outputVal } - return cty.StringVal(filepath.ToSlash(wd)), diags + return cty.ObjectVal(attrs), diags + } - case "module": - moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) - if moduleConfig == nil { - // should never happen, since we can't be evaluating in a module - // that wasn't mentioned in configuration. - panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.ModulePath)) + switch instKeyType { + + case addrs.NoKeyType: + // In this case we should always have exactly one instance that + // is addrs.NoKey. If not then there's a bug in the [instances.Expander] + // implementation. + if len(instKeys) != 1 { + panic(fmt.Sprintf("module call has no instance key type but has %d instances (should be 1)", len(instKeys))) } - sourceDir := moduleConfig.Module.SourceDir - return cty.StringVal(filepath.ToSlash(sourceDir)), diags + ret, moreDiags := instanceObjVal(instKeys[0]) + diags = diags.Append(moreDiags) + return ret, diags - case "root": - sourceDir := d.Evaluator.Config.Module.SourceDir - return cty.StringVal(filepath.ToSlash(sourceDir)), diags + case addrs.IntKeyType: + // We can assume that the instance keys are in ascending numerical order + // and are consecutive, per the contract of allInstances.ExpandModuleCall. + elems := make([]cty.Value, 0, len(instKeys)) + for _, instKey := range instKeys { + instVal, moreDiags := instanceObjVal(instKey) + elems = append(elems, instVal) + diags = diags.Append(moreDiags) + } + return cty.TupleVal(elems), diags + + case addrs.StringKeyType: + attrs := make(map[string]cty.Value, len(instKeys)) + for _, instKey := range instKeys { + instVal, moreDiags := instanceObjVal(instKey) + attrs[string(instKey.(addrs.StringKey))] = instVal + diags = diags.Append(moreDiags) + } + return cty.ObjectVal(attrs), diags default: - suggestion := nameSuggestion(addr.Name, []string{"cwd", "module", "root"}) - if suggestion != "" { - suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) - } diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, - Summary: `Invalid "path" attribute`, - Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion), - Subject: rng.ToHCL().Ptr(), + Summary: `Unsupported instance key type`, + Detail: fmt.Sprintf( + `Module call %s has instance key type %#v, which is not supported by the expression evaluator. This is a bug in Terraform.`, + absAddr, instKeyType, + ), + Subject: rng.ToHCL().Ptr(), }) return cty.DynamicVal, diags } @@ -634,7 +504,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc // First we'll consult the configuration to see if an resource of this // name is declared at all. moduleAddr := d.ModulePath - moduleConfig := d.Evaluator.Config.DescendentForInstance(moduleAddr) + moduleConfig := d.Evaluator.Config.DescendantForInstance(moduleAddr) if moduleConfig == nil { // should never happen, since we can't be evaluating in a module // that wasn't mentioned in configuration. @@ -652,12 +522,47 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc return cty.DynamicVal, diags } + // Much of this function was written before we had factored out the handling + // of instance keys into the separate instance expander model, and so it + // does a bunch of instance-related work itself below. + // + // Currently, unknown instance keys are only possible when planning with + // DeferralAllowed set to true in the PlanOpts, which should only be the + // case in the stacks runtime (not the "normal terraform" modules runtime). + // Thus, we have some amount of duplicated code remaining, to be more + // certain that stacks-specific behaviors won't leak out into the standard + // runtime. + // + // TODO: When deferred actions are more stable and robust in stacks, it + // would be nice to rework this function to rely on the ResourceInstanceKeys + // result for _all_ of its work, rather than continuing to duplicate a bunch + // of the logic we've tried to encapsulate over ther already. + if d.Operation == walkPlan || d.Operation == walkApply { + if _, _, hasUnknownKeys := d.Evaluator.Instances.ResourceInstanceKeys(addr.Absolute(moduleAddr)); hasUnknownKeys { + // There really isn't anything interesting we can do in this situation, + // because it means we have an unknown for_each/count, in which case + // we can't even predict what the result type will be because it + // would be either an object or tuple type decided based on the instance + // keys. + // (We can't get in here for a single-instance resource because in that + // case we would know that there's only one key and it's addrs.NoKey, + // so we'll fall through to the other logic below.) + unknownVal := cty.DynamicVal + + // If an ephemeral resource is deferred we need to mark the returned unknown value as ephemeral + if addr.Mode == addrs.EphemeralResourceMode { + unknownVal = unknownVal.Mark(marks.Ephemeral) + } + return unknownVal, diags + } + } + // Build the provider address from configuration, since we may not have // state available in all cases. // We need to build an abs provider address, but we can use a default // instance since we're only interested in the schema. schema := d.getResourceSchema(addr, config.Provider) - if schema == nil { + if schema.Body == nil { // This shouldn't happen, since validation before we get here should've // taken care of it, but we'll show a reasonable error message anyway. diags = diags.Append(&hcl.Diagnostic{ @@ -668,7 +573,18 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc }) return cty.DynamicVal, diags } - ty := schema.ImpliedType() + ty := schema.Body.ImpliedType() + + if addr.Mode == addrs.EphemeralResourceMode { + // FIXME: This does not yet work with deferrals, and it would be nice to + // find some way to refactor this so that the following code is not so + // tethered to the current implementation details. Instead we should + // have an abstract idea of first determining what instances the + // resource has (using d.Evaluator.Instances.ResourceInstanceKeys) and + // then retrieving the value for each instance to assemble into the + // result, using some per-resource-mode logic maintained elsewhere. + return d.getEphemeralResource(addr, rng) + } rs := d.Evaluator.State.Resource(addr.Absolute(d.ModulePath)) @@ -695,18 +611,58 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc return cty.DynamicVal, diags } + case walkImport: + // Import does not yet plan resource changes, so new resources from + // config are not going to be found here. Once walkImport fully + // plans resources, this case should not longer be needed. + // In the single instance case, we can return a typed unknown value + // for the instance to better satisfy other expressions using the + // value. This of course will not help if statically known + // attributes are expected to be known elsewhere, but reduces the + // number of problematic configs for now. + // Unlike in plan and apply above we can't be sure the count or + // for_each instances are empty, so we return a DynamicVal. We + // don't really have a good value to return otherwise -- empty + // values will fail for direct index expressions, and unknown + // Lists and Maps could fail in some type unifications. + switch { + case config.Count != nil: + return cty.DynamicVal, diags + case config.ForEach != nil: + return cty.DynamicVal, diags + default: + return cty.UnknownVal(ty), diags + } + default: - // We should only end up here during the validate walk, - // since later walks should have at least partial states populated - // for all resources in the configuration. + // We should only end up here during the validate walk (or + // console/eval), since later walks should have at least partial + // states populated for all resources in the configuration. return cty.DynamicVal, diags } } - // Decode all instances in the current state + // Now, we're going to build up a value that represents the resource + // or resources that are in the state. instances := map[addrs.InstanceKey]cty.Value{} - pendingDestroy := d.Evaluator.Changes.IsFullDestroy() + + // First, we're going to load any instances that we have written into the + // deferrals system. A deferred resource overrides anything that might be + // in the state for the resource, so we do this first. + for key, value := range d.Evaluator.Deferrals.GetDeferredResourceInstances(addr.Absolute(d.ModulePath)) { + instances[key] = value + } + + // Decode all instances in the current state + pendingDestroy := d.Operation == walkDestroy for key, is := range rs.Instances { + if _, ok := instances[key]; ok { + // Then we've already loaded this instance from the deferrals so + // we'll just ignore it being in state. + continue + } + // Otherwise, we'll load the instance from state. + if is == nil || is.Current == nil { // Assume we're dealing with an instance that hasn't been created yet. instances[key] = cty.UnknownVal(ty) @@ -714,8 +670,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc } instAddr := addr.Instance(key).Absolute(d.ModulePath) - - change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, states.CurrentGen) + change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, addrs.NotDeposed) if change != nil { // Don't take any resources that are yet to be deleted into account. // If the referenced resource is CreateBeforeDestroy, then orphaned @@ -732,39 +687,21 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc // and need to be replaced by the planned value here. if is.Current.Status == states.ObjectPlanned { if change == nil { - // If the object is in planned status then we should not get - // here, since we should have found a pending value in the plan - // above instead. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Missing pending object in plan", - Detail: fmt.Sprintf("Instance %s is marked as having a change pending but that change is not recorded in the plan. This is a bug in Terraform; please report it.", instAddr), - Subject: &config.DeclRange, - }) + // FIXME: This is usually an unfortunate case where we need to + // lookup an individual instance referenced via "self" for + // postconditions which we know exists, but because evaluation + // must always get the resource in aggregate some instance + // changes may not yet be registered. + instances[key] = cty.DynamicVal + // log the problem for debugging, since it may be a legitimate error we can't catch + log.Printf("[WARN] instance %s is marked as having a change pending but that change is not recorded in the plan", instAddr) continue } - val, err := change.After.Decode(ty) - if err != nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid resource instance data in plan", - Detail: fmt.Sprintf("Instance %s data could not be decoded from the plan: %s.", instAddr, err), - Subject: &config.DeclRange, - }) - continue - } - - // If our provider schema contains sensitive values, mark those as sensitive - afterMarks := change.AfterValMarks - if schema.ContainsSensitive() { - afterMarks = append(afterMarks, schema.ValueMarks(val, nil)...) - } - - instances[key] = val.MarkWithPaths(afterMarks) + instances[key] = change.After continue } - ios, err := is.Current.Decode(ty) + ios, err := is.Current.Decode(schema) if err != nil { // This shouldn't happen, since by the time we get here we // should have upgraded the state data already. @@ -779,16 +716,6 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc val := ios.Value - // If our schema contains sensitive values, mark those as sensitive. - // Since decoding the instance object can also apply sensitivity marks, - // we must remove and combine those before remarking to avoid a double- - // mark error. - if schema.ContainsSensitive() { - var marks []cty.PathValueMarks - val, marks = val.UnmarkDeepWithPaths() - marks = append(marks, schema.ValueMarks(val, nil)...) - val = val.MarkWithPaths(marks) - } instances[key] = val } @@ -866,65 +793,179 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc return ret, diags } -func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.Provider) *configschema.Block { - schema, _, err := d.Evaluator.Plugins.ResourceTypeSchema(providerAddr, addr.Mode, addr.Type) +func (d *evaluationStateData) getEphemeralResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if d.Operation == walkValidate || d.Operation == walkEval { + // Ephemeral instances are never live during the validate walk. Eval is + // similarly offline, and since there is no value stored we can't return + // anything other than dynamic. + return cty.DynamicVal.Mark(marks.Ephemeral), diags + } + + // Now, we're going to build up a value that represents the resource + // or resources that are in the state. + instances := map[addrs.InstanceKey]cty.Value{} + + // First, we're going to load any instances that we have written into the + // deferrals system. A deferred resource overrides anything that might be + // in the state for the resource, so we do this first. + for key, value := range d.Evaluator.Deferrals.GetDeferredResourceInstances(addr.Absolute(d.ModulePath)) { + instances[key] = value + } + + absAddr := addr.Absolute(d.ModulePath) + keyType, keys, haveUnknownKeys := d.Evaluator.Instances.ResourceInstanceKeys(absAddr) + if haveUnknownKeys { + // We can probably do better than totally unknown at least for a + // single-instance resource, but we'll just keep it simple for now. + // Result must be marked as ephemeral so that we can still catch + // attempts to use the results in non-ephemeral locations, so that + // the operator doesn't end up trapped with an error on a subsequent + // plan/apply round. + return cty.DynamicVal.Mark(marks.Ephemeral), diags + } + + ephems := d.Evaluator.EphemeralResources + getInstValue := func(addr addrs.AbsResourceInstance) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // If we have a deferred instance with this key we don't need to check if it is live or not, + // it has not been created so we can just return the deferred value. + if v, ok := instances[addr.Resource.Key]; ok { + return v, diags + } + + val, isLive := ephems.InstanceValue(addr) + if !isLive { + // If the instance is no longer "live" by the time we're accessing + // it then that suggests that it needed renewal and renewal has + // failed, and so the object's value is no longer usable. We'll + // still return the value in case it's somehow useful for diagnosis, + // but we return an error to prevent further evaluation of whatever + // other expression depended on the liveness of this object. + // + // This error message is written on the assumption that it will + // always appear alongside the provider's renewal error, but that'll + // be exposed only once the (now-zombied) ephemeral resource is + // eventually closed, so that we can avoid returning the same error + // multiple times. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral resource instance has expired", + Detail: fmt.Sprintf( + "The remote object for %s is no longer available due to a renewal error, so Terraform cannot evaluate this expression.", + addr, + ), + Subject: rng.ToHCL().Ptr(), + }) + } + if val == cty.NilVal { + val = cty.DynamicVal.Mark(marks.Ephemeral) + } + return val, diags + } + + switch keyType { + case addrs.NoKeyType: + // For "no key" we're returning just a single object representing + // the single instance of this resource. + instVal, moreDiags := getInstValue(absAddr.Instance(addrs.NoKey)) + diags = diags.Append(moreDiags) + return instVal, diags + case addrs.IntKeyType: + // For integer keys we're returning a tuple-typed value whose + // indices are the keys. + elems := make([]cty.Value, len(keys)) + for _, key := range keys { + idx := int(key.(addrs.IntKey)) + instAddr := absAddr.Instance(key) + instVal, moreDiags := getInstValue(instAddr) + diags = diags.Append(moreDiags) + elems[idx] = instVal + } + return cty.TupleVal(elems), diags + case addrs.StringKeyType: + // For string keys we're returning an object-typed value whose + // attributes are the keys. + attrs := make(map[string]cty.Value, len(keys)) + for _, key := range keys { + attrName := string(key.(addrs.StringKey)) + instAddr := absAddr.Instance(key) + instVal, moreDiags := getInstValue(instAddr) + diags = diags.Append(moreDiags) + attrs[attrName] = instVal + } + return cty.ObjectVal(attrs), diags + default: + panic(fmt.Sprintf("unhandled instance key type %#v", keyType)) + } +} + +func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.Provider) providers.Schema { + schema, err := d.Evaluator.Plugins.ResourceTypeSchema(providerAddr, addr.Mode, addr.Type) if err != nil { // We have plently other codepaths that will detect and report // schema lookup errors before we'd reach this point, so we'll just // treat a failure here the same as having no schema. - return nil + return providers.Schema{} } return schema } -func (d *evaluationStateData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { +func (d *evaluationStateData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - switch addr.Name { - case "workspace": - workspaceName := d.Evaluator.Meta.Env - return cty.StringVal(workspaceName), diags - - case "env": - // Prior to Terraform 0.12 there was an attribute "env", which was - // an alias name for "workspace". This was deprecated and is now - // removed. - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "terraform" attribute`, - Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`, - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags - - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: `Invalid "terraform" attribute`, - Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attribute is terraform.workspace, the name of the currently-selected workspace.`, addr.Name), - Subject: rng.ToHCL().Ptr(), - }) - return cty.DynamicVal, diags + // First we'll make sure the requested value is declared in configuration, + // so we can produce a nice message if not. + moduleConfig := d.Evaluator.Config.DescendantForInstance(d.ModulePath) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("output value read from %s, which has no configuration", d.ModulePath)) } -} -// nameSuggestion tries to find a name from the given slice of suggested names -// that is close to the given name and returns it if found. If no suggestion -// is close enough, returns the empty string. -// -// The suggestions are tried in order, so earlier suggestions take precedence -// if the given string is similar to two or more suggestions. -// -// This function is intended to be used with a relatively-small number of -// suggestions. It's not optimized for hundreds or thousands of them. -func nameSuggestion(given string, suggestions []string) string { - for _, suggestion := range suggestions { - dist := levenshtein.Distance(given, suggestion, nil) - if dist < 3 { // threshold determined experimentally - return suggestion + config := moduleConfig.Module.Outputs[addr.Name] + if config == nil { + var suggestions []string + for k := range moduleConfig.Module.Outputs { + suggestions = append(suggestions, k) } + suggestion := didyoumean.NameSuggestion(addr.Name, suggestions) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to undeclared output value`, + Detail: fmt.Sprintf(`An output value with the name %q has not been declared.%s`, addr.Name, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags } - return "" + + output := d.Evaluator.State.OutputValue(addr.Absolute(d.ModulePath)) + if output == nil { + // Then the output itself returned null, so we'll package that up and + // pass it on. + output = &states.OutputValue{ + Addr: addr.Absolute(d.ModulePath), + Value: cty.NilVal, + Sensitive: config.Sensitive, + } + } else if output.Value == cty.NilVal || output.Value.IsNull() { + // Then we did get a value but Terraform itself thought it was NilVal + // so we treat this as if the value isn't yet known. + output.Value = cty.DynamicVal + } + + val := output.Value + if output.Sensitive { + val = val.Mark(marks.Sensitive) + } + + return val, diags } // moduleDisplayAddr returns a string describing the given module instance diff --git a/internal/terraform/evaluate_data.go b/internal/terraform/evaluate_data.go new file mode 100644 index 0000000000..1ba113f334 --- /dev/null +++ b/internal/terraform/evaluate_data.go @@ -0,0 +1,183 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/didyoumean" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// evaluationData is the base struct for evaluating data from within Terraform +// Core. It contains some common data and functions shared by the various +// implemented evaluators. +type evaluationData struct { + Evaluator *Evaluator + + // Module is the unexpanded module that this data is being evaluated within. + Module addrs.Module +} + +// GetPathAttr implements lang.Data. +func (d *evaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch addr.Name { + + case "cwd": + var err error + var wd string + if d.Evaluator.Meta != nil { + // Meta is always non-nil in the normal case, but some test cases + // are not so realistic. + wd = d.Evaluator.Meta.OriginalWorkingDir + } + if wd == "" { + wd, err = os.Getwd() + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Failed to get working directory`, + Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + } + // The current working directory should always be absolute, whether we + // just looked it up or whether we were relying on ContextMeta's + // (possibly non-normalized) path. + wd, err = filepath.Abs(wd) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Failed to get working directory`, + Detail: fmt.Sprintf(`The value for path.cwd cannot be determined due to a system error: %s`, err), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + + return cty.StringVal(filepath.ToSlash(wd)), diags + + case "module": + moduleConfig := d.Evaluator.Config.Descendant(d.Module) + if moduleConfig == nil { + // should never happen, since we can't be evaluating in a module + // that wasn't mentioned in configuration. + panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.Module)) + } + sourceDir := moduleConfig.Module.SourceDir + return cty.StringVal(filepath.ToSlash(sourceDir)), diags + + case "root": + sourceDir := d.Evaluator.Config.Module.SourceDir + return cty.StringVal(filepath.ToSlash(sourceDir)), diags + + default: + suggestion := didyoumean.NameSuggestion(addr.Name, []string{"cwd", "module", "root"}) + if suggestion != "" { + suggestion = fmt.Sprintf(" Did you mean %q?", suggestion) + } + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "path" attribute`, + Detail: fmt.Sprintf(`The "path" object does not have an attribute named %q.%s`, addr.Name, suggestion), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// GetTerraformAttr implements lang.Data. +func (d *evaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch addr.Name { + + case "workspace": + // The absence of an "env" (really: workspace) name suggests that + // we're running in a non-workspace context, such as in a component + // of a stack. terraform.workspace is a legacy thing from workspaces + // mode that isn't carried forward to stacks, because stack + // configurations can instead vary their behavior based on input + // variables provided in the deployment configuration. + if d.Evaluator.Meta == nil || d.Evaluator.Meta.Env == "" { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid reference`, + Detail: `The terraform.workspace attribute is only available for modules used in Terraform workspaces. Use input variables instead to create variations between different instances of this module.`, + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } + workspaceName := d.Evaluator.Meta.Env + return cty.StringVal(workspaceName), diags + + // terraform.applying is an ephemeral boolean value that's set to true + // during an apply walk or false in any other situation. This is + // intended to allow, for example, using a more privileged auth role + // in a provider configuration during the apply phase but a more + // constrained role for other situations. + case "applying": + return cty.BoolVal(d.Evaluator.Operation == walkApply).Mark(marks.Ephemeral), nil + + case "env": + // Prior to Terraform 0.12 there was an attribute "env", which was + // an alias name for "workspace". This was deprecated and is now + // removed. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "terraform" attribute`, + Detail: `The terraform.env attribute was deprecated in v0.10 and removed in v0.12. The "state environment" concept was renamed to "workspace" in v0.12, and so the workspace name can now be accessed using the terraform.workspace attribute.`, + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "terraform" attribute`, + Detail: fmt.Sprintf(`The "terraform" object does not have an attribute named %q. The only supported attributes are terraform.workspace, the name of the currently-selected workspace, and terraform.applying, a boolean which is true only during apply.`, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// StaticValidateReferences implements lang.Data. +func (d *evaluationData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { + return d.Evaluator.StaticValidateReferences(refs, d.Module, self, source) +} + +// GetRunBlock implements lang.Data. +func (d *evaluationData) GetRunBlock(addrs.Run, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + // We should not get here because any scope that has an [evaluationPlaceholderData] + // as its Data should have a reference parser that doesn't accept addrs.Run + // addresses. + panic("GetRunBlock called on non-test evaluation dataset") +} + +func (d *evaluationData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + // For now, check blocks don't contain any meaningful data and can only + // be referenced from the testing scope within an expect_failures attribute. + // + // We've added them into the scope explicitly since they are referencable, + // but we'll actually just return an error message saying they can't be + // referenced in this context. + var diags tfdiags.Diagnostics + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Reference to \"check\" in invalid context", + Detail: "The \"check\" object can only be referenced from an \"expect_failures\" attribute within a Terraform testing \"run\" block.", + Subject: rng.ToHCL().Ptr(), + }) + return cty.NilVal, diags +} diff --git a/internal/terraform/evaluate_placeholder.go b/internal/terraform/evaluate_placeholder.go new file mode 100644 index 0000000000..620b64cece --- /dev/null +++ b/internal/terraform/evaluate_placeholder.go @@ -0,0 +1,224 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// evaluationPlaceholderData is an implementation of lang.Data that deals +// with resolving references inside module prefixes whose full expansion +// isn't known yet, and thus returns placeholder values that represent +// only what we know to be true for all possible final module instances +// that could exist for the prefix. +type evaluationPlaceholderData struct { + *evaluationData + + // ModulePath is the partially-expanded path through the dynamic module + // tree to a set of possible module instances that share a common known + // prefix. + ModulePath addrs.PartialExpandedModule + + // CountAvailable is true if this data object is representing an evaluation + // scope where the "count" symbol would be available. + CountAvailable bool + + // EachAvailable is true if this data object is representing an evaluation + // scope where the "each" symbol would be available. + EachAvailable bool + + // Operation records the type of walk the evaluationStateData is being used + // for. + Operation walkOperation +} + +// TODO: Historically we were inconsistent about whether static validation +// logic is implemented in Evaluator.StaticValidateReference or inline in +// methods of evaluationStateData, because the dedicated static validator +// came later. +// +// Some validation rules (and their associated error messages) have therefore +// ended up being duplicated between evaluationPlaceholderData and +// evaluationStateData. We've accepted that for now to avoid creating a bunch +// of churn in pre-existing code while adding support for partial expansion +// placeholders, but one day it would be nice to refactor this a little so +// that the division between these three units is a little clearer and so +// that all of the error checks are implemented in only one place each. + +var _ lang.Data = (*evaluationPlaceholderData)(nil) + +// GetCountAttr implements lang.Data. +func (d *evaluationPlaceholderData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + switch addr.Name { + + case "index": + if !d.CountAvailable { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to "count" in non-counted context`, + Detail: `The "count" object can only be used in "module", "resource", and "data" blocks, and only when the "count" argument is set.`, + Subject: rng.ToHCL().Ptr(), + }) + } + // When we're under a partially-expanded prefix, the leaf instance + // keys are never known because otherwise we'd be under a fully-known + // prefix by definition. We do know it's always >= 0 and not null, + // though. + return cty.UnknownVal(cty.Number).Refine(). + NumberRangeLowerBound(cty.Zero, true). + NotNull(). + NewValue(), diags + + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "count" attribute`, + Detail: fmt.Sprintf(`The "count" object does not have an attribute named %q. The only supported attribute is count.index, which is the index of each instance of a resource block that has the "count" argument set.`, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// GetForEachAttr implements lang.Data. +func (d *evaluationPlaceholderData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // When we're under a partially-expanded prefix, the leaf instance + // keys are never known because otherwise we'd be under a fully-known + // prefix by definition. Therefore all return paths here produce unknown + // values. + + if !d.EachAvailable { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to "each" in context without for_each`, + Detail: `The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.`, + Subject: rng.ToHCL().Ptr(), + }) + return cty.UnknownVal(cty.DynamicPseudoType), diags + } + + switch addr.Name { + + case "key": + // each.key is always a string and is never null + return cty.UnknownVal(cty.String).RefineNotNull(), diags + case "value": + return cty.DynamicVal, diags + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Invalid "each" attribute`, + Detail: fmt.Sprintf(`The "each" object does not have an attribute named %q. The supported attributes are each.key and each.value, the current key and value pair of the "for_each" attribute set.`, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + return cty.DynamicVal, diags + } +} + +// GetInputVariable implements lang.Data. +func (d *evaluationPlaceholderData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + namedVals := d.Evaluator.NamedValues + absAddr := addrs.ObjectInPartialExpandedModule(d.ModulePath, addr) + return namedVals.GetInputVariablePlaceholder(absAddr), nil +} + +// GetLocalValue implements lang.Data. +func (d *evaluationPlaceholderData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + namedVals := d.Evaluator.NamedValues + absAddr := addrs.ObjectInPartialExpandedModule(d.ModulePath, addr) + return namedVals.GetLocalValuePlaceholder(absAddr), nil +} + +// GetModule implements lang.Data. +func (d *evaluationPlaceholderData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + // We'll reuse the evaluator's "static evaluate" logic to check that the + // module call being referred to is even declared in the configuration, + // since it returns a good-quality error message for that case that + // we don't want to have to duplicate here. + diags := d.Evaluator.StaticValidateReference(&addrs.Reference{ + Subject: addr, + SourceRange: rng, + }, d.ModulePath.Module(), nil, nil) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + + callerCfg := d.Evaluator.Config.Descendant(d.ModulePath.Module()) + if callerCfg == nil { + // Strange! The above StaticValidateReference should've failed if + // the module we're in isn't even declared. But we'll just tolerate + // it and return a very general placeholder. + return cty.DynamicVal, diags + } + callCfg := callerCfg.Module.ModuleCalls[addr.Name] + if callCfg == nil { + // Again strange, for the same reason as just above. + return cty.DynamicVal, diags + } + + // Any module call under an unexpanded prefix has an unknown set of instance + // keys itself by definition, unless that call isn't using count or for_each + // at all and thus we know it has exactly one "no-key" instance. + // + // If we don't know the instance keys then we cannot predict anything about + // the result, because module calls with repetition appear as either + // object or tuple types and we cannot predict those types here. + if callCfg.Count != nil || callCfg.ForEach != nil { + return cty.DynamicVal, diags + } + + // If we get down here then we know we have a single-instance module, and + // so we can return a more specific placeholder object that has all of + // the child module's declared output values represented, which could + // then potentially allow detecting a downstream error referring to + // an output value that doesn't actually exist. + calledCfg := d.Evaluator.Config.Descendant(d.ModulePath.Module().Child(addr.Name)) + if calledCfg == nil { + // This suggests that the config wasn't constructed correctly, since + // there should always be a child config node for any module call, + // but that's a "package configs" problem and so we'll just tolerate + // it here for robustness. + return cty.DynamicVal, diags + } + + attrs := make(map[string]cty.Value, len(calledCfg.Module.Outputs)) + for name := range calledCfg.Module.Outputs { + // Module output values are dynamically-typed, so we cannot + // predict anything about their results until finalized. + attrs[name] = cty.DynamicVal + } + return cty.ObjectVal(attrs), diags +} + +// GetOutput implements lang.Data. +func (d *evaluationPlaceholderData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + namedVals := d.Evaluator.NamedValues + absAddr := addrs.ObjectInPartialExpandedModule(d.ModulePath, addr) + return namedVals.GetOutputValuePlaceholder(absAddr), nil + +} + +// GetResource implements lang.Data. +func (d *evaluationPlaceholderData) GetResource(addrs.Resource, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + // TODO: Once we've implemented the evaluation of placeholders for + // deferred resources during the graph walk, we should return such + // placeholders here where possible. + // + // However, for resources that use count or for_each we'd not be able + // to predict anything more than cty.DynamicVal here anyway, since + // we don't know the instance keys, and so that improvement would only + // really help references to single-instance resources. + return cty.DynamicVal, nil +} diff --git a/internal/terraform/evaluate_test.go b/internal/terraform/evaluate_test.go index 765efded68..db984755b7 100644 --- a/internal/terraform/evaluate_test.go +++ b/internal/terraform/evaluate_test.go @@ -1,7 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "sync" + "fmt" "testing" "github.com/davecgh/go-spew/spew" @@ -10,8 +13,13 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -21,11 +29,14 @@ func TestEvaluatorGetTerraformAttr(t *testing.T) { Meta: &ContextMeta{ Env: "foo", }, + NamedValues: namedvals.NewState(), } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) t.Run("workspace", func(t *testing.T) { want := cty.StringVal("foo") @@ -51,11 +62,14 @@ func TestEvaluatorGetPathAttr(t *testing.T) { SourceDir: "bar/baz", }, }, + NamedValues: namedvals.NewState(), } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) t.Run("module", func(t *testing.T) { want := cty.StringVal("bar/baz") @@ -84,9 +98,83 @@ func TestEvaluatorGetPathAttr(t *testing.T) { }) } +func TestEvaluatorGetOutputValue(t *testing.T) { + evaluator := &Evaluator{ + Meta: &ContextMeta{ + Env: "foo", + }, + Config: &configs.Config{ + Module: &configs.Module{ + Outputs: map[string]*configs.Output{ + "some_output": { + Name: "some_output", + Sensitive: true, + }, + "some_other_output": { + Name: "some_other_output", + }, + }, + }, + }, + State: states.BuildState(func(state *states.SyncState) { + state.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{ + Name: "some_output", + }, + }, cty.StringVal("first"), true) + state.SetOutputValue(addrs.AbsOutputValue{ + Module: addrs.RootModuleInstance, + OutputValue: addrs.OutputValue{ + Name: "some_other_output", + }, + }, cty.StringVal("second"), false) + }).SyncWrapper(), + } + + data := &evaluationStateData{ + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, + } + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) + + want := cty.StringVal("first").Mark(marks.Sensitive) + got, diags := scope.Data.GetOutput(addrs.OutputValue{ + Name: "some_output", + }, tfdiags.SourceRange{}) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) + } + if !got.RawEquals(want) { + t.Errorf("wrong result %#v; want %#v", got, want) + } + + want = cty.StringVal("second") + got, diags = scope.Data.GetOutput(addrs.OutputValue{ + Name: "some_other_output", + }, tfdiags.SourceRange{}) + + if len(diags) != 0 { + t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) + } + if !got.RawEquals(want) { + t.Errorf("wrong result %#v; want %#v", got, want) + } +} + // This particularly tests that a sensitive attribute in config // results in a value that has a "sensitive" cty Mark func TestEvaluatorGetInputVariable(t *testing.T) { + namedValues := namedvals.NewState() + namedValues.SetInputVariableValue( + addrs.RootModuleInstance.InputVariable("some_var"), cty.StringVal("bar"), + ) + namedValues.SetInputVariableValue( + addrs.RootModuleInstance.InputVariable("some_other_var"), cty.StringVal("boop").Mark(marks.Sensitive), + ) + evaluator := &Evaluator{ Meta: &ContextMeta{ Env: "foo", @@ -112,19 +200,15 @@ func TestEvaluatorGetInputVariable(t *testing.T) { }, }, }, - VariableValues: map[string]map[string]cty.Value{ - "": { - "some_var": cty.StringVal("bar"), - "some_other_var": cty.StringVal("boop").Mark(marks.Sensitive), - }, - }, - VariableValuesLock: &sync.Mutex{}, + NamedValues: namedValues, } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) want := cty.StringVal("bar").Mark(marks.Sensitive) got, diags := scope.Data.GetInputVariable(addrs.InputVariable{ @@ -162,6 +246,14 @@ func TestEvaluatorGetResource(t *testing.T) { &states.ResourceInstanceObjectSrc{ Status: states.ObjectReady, AttrsJSON: []byte(`{"id":"foo", "nesting_list": [{"sensitive_value":"abc"}], "nesting_map": {"foo":{"foo":"x"}}, "nesting_set": [{"baz":"abc"}], "nesting_single": {"boop":"abc"}, "nesting_nesting": {"nesting_list":[{"sensitive_value":"abc"}]}, "value":"hello"}`), + AttrSensitivePaths: []cty.Path{ + cty.GetAttrPath("nesting_list").IndexInt(0).GetAttr("sensitive_value"), + cty.GetAttrPath("nesting_map").IndexString("foo").GetAttr("foo"), + cty.GetAttrPath("nesting_nesting").GetAttr("nesting_list").IndexInt(0).GetAttr("sensitive_value"), + cty.GetAttrPath("nesting_set"), + cty.GetAttrPath("nesting_single").GetAttr("boop"), + cty.GetAttrPath("value"), + }, }, addrs.AbsProviderConfig{ Provider: addrs.NewDefaultProvider("test"), @@ -196,72 +288,75 @@ func TestEvaluatorGetResource(t *testing.T) { }, }, }, - State: stateSync, - Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]*ProviderSchema{ + State: stateSync, + NamedValues: namedvals.NewState(), + Deferrals: deferring.NewDeferred(false), + Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]providers.ProviderSchema{ addrs.NewDefaultProvider("test"): { - Provider: &configschema.Block{}, - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_resource": { - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Computed: true, - }, - "value": { - Type: cty.String, - Computed: true, - Sensitive: true, - }, - }, - BlockTypes: map[string]*configschema.NestedBlock{ - "nesting_list": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "value": {Type: cty.String, Optional: true}, - "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, - }, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, }, - Nesting: configschema.NestingList, - }, - "nesting_map": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foo": {Type: cty.String, Optional: true, Sensitive: true}, - }, + "value": { + Type: cty.String, + Computed: true, + Sensitive: true, }, - Nesting: configschema.NestingMap, }, - "nesting_set": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "baz": {Type: cty.String, Optional: true, Sensitive: true}, - }, - }, - Nesting: configschema.NestingSet, - }, - "nesting_single": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "boop": {Type: cty.String, Optional: true, Sensitive: true}, - }, - }, - Nesting: configschema.NestingSingle, - }, - "nesting_nesting": { - Block: configschema.Block{ - BlockTypes: map[string]*configschema.NestedBlock{ - "nesting_list": { - Block: configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "value": {Type: cty.String, Optional: true}, - "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, - }, - }, - Nesting: configschema.NestingList, + BlockTypes: map[string]*configschema.NestedBlock{ + "nesting_list": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, }, }, + Nesting: configschema.NestingList, + }, + "nesting_map": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingMap, + }, + "nesting_set": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "baz": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingSet, + }, + "nesting_single": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "boop": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingSingle, + }, + "nesting_nesting": { + Block: configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "nesting_list": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "value": {Type: cty.String, Optional: true}, + "sensitive_value": {Type: cty.String, Optional: true, Sensitive: true}, + }, + }, + Nesting: configschema.NestingList, + }, + }, + }, + Nesting: configschema.NestingSingle, }, - Nesting: configschema.NestingSingle, }, }, }, @@ -271,9 +366,11 @@ func TestEvaluatorGetResource(t *testing.T) { } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) want := cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), @@ -358,39 +455,40 @@ func TestEvaluatorGetResource_changes(t *testing.T) { After: cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), "to_mark_val": cty.StringVal("pizza").Mark(marks.Sensitive), - "sensitive_value": cty.StringVal("abc"), + "sensitive_value": cty.StringVal("abc").Mark(marks.Sensitive), "sensitive_collection": cty.MapVal(map[string]cty.Value{ "boop": cty.StringVal("beep"), - }), + }).Mark(marks.Sensitive), }), }, } // Set up our schemas schemas := &Schemas{ - Providers: map[addrs.Provider]*ProviderSchema{ + Providers: map[addrs.Provider]providers.ProviderSchema{ addrs.NewDefaultProvider("test"): { - Provider: &configschema.Block{}, - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "test_resource": { - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Computed: true, - }, - "to_mark_val": { - Type: cty.String, - Computed: true, - }, - "sensitive_value": { - Type: cty.String, - Computed: true, - Sensitive: true, - }, - "sensitive_collection": { - Type: cty.Map(cty.String), - Computed: true, - Sensitive: true, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, + "to_mark_val": { + Type: cty.String, + Computed: true, + }, + "sensitive_value": { + Type: cty.String, + Computed: true, + Sensitive: true, + }, + "sensitive_collection": { + Type: cty.Map(cty.String), + Computed: true, + Sensitive: true, + }, }, }, }, @@ -405,10 +503,8 @@ func TestEvaluatorGetResource_changes(t *testing.T) { Type: "test_resource", Name: "foo", } - schema, _ := schemas.ResourceTypeConfig(addrs.NewDefaultProvider("test"), addr.Mode, addr.Type) - // This encoding separates out the After's marks into its AfterValMarks - csrc, _ := change.Encode(schema.ImpliedType()) - changesSync.AppendResourceInstanceChange(csrc) + + changesSync.AppendResourceInstanceChange(change) evaluator := &Evaluator{ Meta: &ContextMeta{ @@ -431,14 +527,18 @@ func TestEvaluatorGetResource_changes(t *testing.T) { }, }, }, - State: stateSync, - Plugins: schemaOnlyProvidersForTesting(schemas.Providers), + State: stateSync, + NamedValues: namedvals.NewState(), + Deferrals: deferring.NewDeferred(false), + Plugins: schemaOnlyProvidersForTesting(schemas.Providers), } data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) want := cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("foo"), @@ -461,19 +561,18 @@ func TestEvaluatorGetResource_changes(t *testing.T) { } func TestEvaluatorGetModule(t *testing.T) { - // Create a new evaluator with an existing state - stateSync := states.BuildState(func(ss *states.SyncState) { - ss.SetOutputValue( - addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}), - cty.StringVal("bar"), - true, - ) - }).SyncWrapper() - evaluator := evaluatorForModule(stateSync, plans.NewChanges().SyncWrapper()) + evaluator := evaluatorForModule(states.NewState().SyncWrapper(), plans.NewChanges().SyncWrapper()) + evaluator.Instances.SetModuleSingle(addrs.RootModuleInstance, addrs.ModuleCall{Name: "mod"}) + evaluator.NamedValues.SetOutputValue( + addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}), + cty.StringVal("bar").Mark(marks.Sensitive), + ) data := &evaluationStateData{ - Evaluator: evaluator, + evaluationData: &evaluationData{ + Evaluator: evaluator, + }, } - scope := evaluator.Scope(data, nil) + scope := evaluator.Scope(data, nil, nil, lang.ExternalFuncs{}) want := cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("bar").Mark(marks.Sensitive)}) got, diags := scope.Data.GetModule(addrs.ModuleCall{ Name: "mod", @@ -483,53 +582,7 @@ func TestEvaluatorGetModule(t *testing.T) { t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) } if !got.RawEquals(want) { - t.Errorf("wrong result %#v; want %#v", got, want) - } - - // Changes should override the state value - changesSync := plans.NewChanges().SyncWrapper() - change := &plans.OutputChange{ - Addr: addrs.OutputValue{Name: "out"}.Absolute(addrs.ModuleInstance{addrs.ModuleInstanceStep{Name: "mod"}}), - Sensitive: true, - Change: plans.Change{ - After: cty.StringVal("baz"), - }, - } - cs, _ := change.Encode() - changesSync.AppendOutputChange(cs) - evaluator = evaluatorForModule(stateSync, changesSync) - data = &evaluationStateData{ - Evaluator: evaluator, - } - scope = evaluator.Scope(data, nil) - want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)}) - got, diags = scope.Data.GetModule(addrs.ModuleCall{ - Name: "mod", - }, tfdiags.SourceRange{}) - - if len(diags) != 0 { - t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) - } - if !got.RawEquals(want) { - t.Errorf("wrong result %#v; want %#v", got, want) - } - - // Test changes with empty state - evaluator = evaluatorForModule(states.NewState().SyncWrapper(), changesSync) - data = &evaluationStateData{ - Evaluator: evaluator, - } - scope = evaluator.Scope(data, nil) - want = cty.ObjectVal(map[string]cty.Value{"out": cty.StringVal("baz").Mark(marks.Sensitive)}) - got, diags = scope.Data.GetModule(addrs.ModuleCall{ - Name: "mod", - }, tfdiags.SourceRange{}) - - if len(diags) != 0 { - t.Errorf("unexpected diagnostics %s", spew.Sdump(diags)) - } - if !got.RawEquals(want) { - t.Errorf("wrong result %#v; want %#v", got, want) + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, want) } } @@ -560,7 +613,108 @@ func evaluatorForModule(stateSync *states.SyncState, changesSync *plans.ChangesS }, }, }, - State: stateSync, - Changes: changesSync, + State: stateSync, + Changes: changesSync, + Instances: instances.NewExpander(nil), + NamedValues: namedvals.NewState(), } } + +// fakeEvaluationData is an implementation of [lang.Data] that answers most +// questions just by returning data directly from the maps stored inside it. +type fakeEvaluationData struct { + checkBlocks map[addrs.Check]cty.Value + countAttrs map[addrs.CountAttr]cty.Value + forEachAttrs map[addrs.ForEachAttr]cty.Value + inputVariables map[addrs.InputVariable]cty.Value + localValues map[addrs.LocalValue]cty.Value + modules map[addrs.ModuleCall]cty.Value + outputValues map[addrs.OutputValue]cty.Value + pathAttrs map[addrs.PathAttr]cty.Value + resources map[addrs.Resource]cty.Value + runBlocks map[addrs.Run]cty.Value + terraformAttrs map[addrs.TerraformAttr]cty.Value + + // staticValidateRefs optionally implements [lang.Data.StaticValidateReferences], + // but can be left as nil to just skip static validation altogether. + staticValidateRefs func(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics +} + +var _ lang.Data = (*fakeEvaluationData)(nil) + +func fakeEvaluationDataLookup[Addr interface { + comparable + addrs.Referenceable +}](addr Addr, _ tfdiags.SourceRange, table map[Addr]cty.Value) (cty.Value, tfdiags.Diagnostics) { + ret, ok := table[addr] + if !ok { + var diags tfdiags.Diagnostics + diags = diags.Append(fmt.Errorf("fakeEvaluationData does not know about %s", addr)) + return cty.DynamicVal, diags + } + return ret, nil +} + +// GetCheckBlock implements lang.Data. +func (d *fakeEvaluationData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.checkBlocks) +} + +// GetCountAttr implements lang.Data. +func (d *fakeEvaluationData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.countAttrs) +} + +// GetForEachAttr implements lang.Data. +func (d *fakeEvaluationData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.forEachAttrs) +} + +// GetInputVariable implements lang.Data. +func (d *fakeEvaluationData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.inputVariables) +} + +// GetLocalValue implements lang.Data. +func (d *fakeEvaluationData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.localValues) +} + +// GetModule implements lang.Data. +func (d *fakeEvaluationData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.modules) +} + +// GetOutput implements lang.Data. +func (d *fakeEvaluationData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.outputValues) +} + +// GetPathAttr implements lang.Data. +func (d *fakeEvaluationData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.pathAttrs) +} + +// GetResource implements lang.Data. +func (d *fakeEvaluationData) GetResource(addr addrs.Resource, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.resources) +} + +// GetRunBlock implements lang.Data. +func (d *fakeEvaluationData) GetRunBlock(addr addrs.Run, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.runBlocks) +} + +// GetTerraformAttr implements lang.Data. +func (d *fakeEvaluationData) GetTerraformAttr(addr addrs.TerraformAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) { + return fakeEvaluationDataLookup(addr, rng, d.terraformAttrs) +} + +// StaticValidateReferences implements lang.Data. +func (d *fakeEvaluationData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { + if d.staticValidateRefs == nil { + // By default we just skip static validation + return nil + } + return d.staticValidateRefs(refs, self, source) +} diff --git a/internal/terraform/evaluate_triggers.go b/internal/terraform/evaluate_triggers.go index 31fd80e16b..1d615d025d 100644 --- a/internal/terraform/evaluate_triggers.go +++ b/internal/terraform/evaluate_triggers.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -57,7 +60,7 @@ func triggersExprToTraversal(expr hcl.Expression, keyData instances.RepetitionDa // The index key is the only place where we could have variables that // reference count and each, so we need to parse those independently. - idx, hclDiags := parseIndexKeyExpr(e.Key, keyData) + idx, hclDiags := parseReplaceTriggeredByKeyExpr(e.Key, keyData) diags = diags.Append(hclDiags) trav = append(trav, idx) @@ -77,9 +80,9 @@ func triggersExprToTraversal(expr hcl.Expression, keyData instances.RepetitionDa return trav, diags } -// parseIndexKeyExpr takes an hcl.Expression and parses it as an index key, while +// parseReplaceTriggeredByKeyExpr takes an hcl.Expression and parses it as an index key, while // evaluating any references to count.index or each.key. -func parseIndexKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, hcl.Diagnostics) { +func parseReplaceTriggeredByKeyExpr(expr hcl.Expression, keyData instances.RepetitionData) (hcl.TraverseIndex, hcl.Diagnostics) { idx := hcl.TraverseIndex{ SrcRange: expr.Range(), } diff --git a/internal/terraform/evaluate_triggers_test.go b/internal/terraform/evaluate_triggers_test.go index d51b1c2be6..5d79d39cc2 100644 --- a/internal/terraform/evaluate_triggers_test.go +++ b/internal/terraform/evaluate_triggers_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/evaluate_valid.go b/internal/terraform/evaluate_valid.go index 1d43cc4fce..b33d17b52e 100644 --- a/internal/terraform/evaluate_valid.go +++ b/internal/terraform/evaluate_valid.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -28,24 +31,27 @@ import ( // // The result may include warning diagnostics if, for example, deprecated // features are referenced. -func (d *evaluationStateData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics { +func (e *Evaluator) StaticValidateReferences(refs []*addrs.Reference, modAddr addrs.Module, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { var diags tfdiags.Diagnostics for _, ref := range refs { - moreDiags := d.staticValidateReference(ref, self) + moreDiags := e.StaticValidateReference(ref, modAddr, self, source) diags = diags.Append(moreDiags) } return diags } -func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self addrs.Referenceable) tfdiags.Diagnostics { - modCfg := d.Evaluator.Config.DescendentForInstance(d.ModulePath) +func (e *Evaluator) StaticValidateReference(ref *addrs.Reference, modAddr addrs.Module, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { + modCfg := e.Config.Descendant(modAddr) if modCfg == nil { // This is a bug in the caller rather than a problem with the // reference, but rather than crashing out here in an unhelpful way // we'll just ignore it and trust a different layer to catch it. return nil } + return e.staticValidateReference(ref, modCfg, self, source) +} +func (e *Evaluator) staticValidateReference(ref *addrs.Reference, modCfg *configs.Config, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics { if ref.Subject == addrs.Self { // The "self" address is a special alias for the address given as // our self parameter here, if present. @@ -77,20 +83,20 @@ func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self // staticValidateMultiResourceReference respectively. case addrs.Resource: var diags tfdiags.Diagnostics - diags = diags.Append(d.staticValidateSingleResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) - diags = diags.Append(d.staticValidateResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) + diags = diags.Append(staticValidateSingleResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) + diags = diags.Append(staticValidateResourceReference(modCfg, addr, source, e.Plugins, ref.Remaining, ref.SourceRange)) return diags case addrs.ResourceInstance: var diags tfdiags.Diagnostics - diags = diags.Append(d.staticValidateMultiResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) - diags = diags.Append(d.staticValidateResourceReference(modCfg, addr.ContainingResource(), ref.Remaining, ref.SourceRange)) + diags = diags.Append(staticValidateMultiResourceReference(modCfg, addr, ref.Remaining, ref.SourceRange)) + diags = diags.Append(staticValidateResourceReference(modCfg, addr.ContainingResource(), source, e.Plugins, ref.Remaining, ref.SourceRange)) return diags // We also handle all module call references the same way, disregarding index. case addrs.ModuleCall: - return d.staticValidateModuleCallReference(modCfg, addr, ref.Remaining, ref.SourceRange) + return staticValidateModuleCallReference(modCfg, addr, ref.Remaining, ref.SourceRange) case addrs.ModuleCallInstance: - return d.staticValidateModuleCallReference(modCfg, addr.Call, ref.Remaining, ref.SourceRange) + return staticValidateModuleCallReference(modCfg, addr.Call, ref.Remaining, ref.SourceRange) case addrs.ModuleCallInstanceOutput: // This one is a funny one because we will take the output name referenced // and use it to fake up a "remaining" that would make sense for the @@ -106,7 +112,7 @@ func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self // but is close enough for our purposes. SrcRange: ref.SourceRange.ToHCL(), } - return d.staticValidateModuleCallReference(modCfg, addr.Call.Call, remain, ref.SourceRange) + return staticValidateModuleCallReference(modCfg, addr.Call.Call, remain, ref.SourceRange) default: // Anything else we'll just permit through without any static validation @@ -115,7 +121,7 @@ func (d *evaluationStateData) staticValidateReference(ref *addrs.Reference, self } } -func (d *evaluationStateData) staticValidateSingleResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { +func staticValidateSingleResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { // If we have at least one step in "remain" and this resource has // "count" set then we know for sure this in invalid because we have // something like: @@ -160,7 +166,7 @@ func (d *evaluationStateData) staticValidateSingleResourceReference(modCfg *conf return diags } -func (d *evaluationStateData) staticValidateMultiResourceReference(modCfg *configs.Config, addr addrs.ResourceInstance, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { +func staticValidateMultiResourceReference(modCfg *configs.Config, addr addrs.ResourceInstance, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { var diags tfdiags.Diagnostics cfg := modCfg.Module.ResourceByAddr(addr.ContainingResource()) @@ -172,7 +178,7 @@ func (d *evaluationStateData) staticValidateMultiResourceReference(modCfg *confi if addr.Key == addrs.NoKey { // This is a different path into staticValidateSingleResourceReference - return d.staticValidateSingleResourceReference(modCfg, addr.ContainingResource(), remain, rng) + return staticValidateSingleResourceReference(modCfg, addr.ContainingResource(), remain, rng) } else { if cfg.Count == nil && cfg.ForEach == nil { diags = diags.Append(&hcl.Diagnostic{ @@ -187,15 +193,19 @@ func (d *evaluationStateData) staticValidateMultiResourceReference(modCfg *confi return diags } -func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { +func staticValidateResourceReference(modCfg *configs.Config, addr addrs.Resource, source addrs.Referenceable, plugins *contextPlugins, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { var diags tfdiags.Diagnostics var modeAdjective string + modeArticleUpper := "A" switch addr.Mode { case addrs.ManagedResourceMode: modeAdjective = "managed" case addrs.DataResourceMode: modeAdjective = "data" + case addrs.EphemeralResourceMode: + modeAdjective = "ephemeral" + modeArticleUpper = "An" default: // should never happen modeAdjective = "" @@ -217,14 +227,29 @@ func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Co diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `Reference to undeclared resource`, - Detail: fmt.Sprintf(`A %s resource %q %q has not been declared in %s.%s`, modeAdjective, addr.Type, addr.Name, moduleConfigDisplayAddr(modCfg.Path), suggestion), - Subject: rng.ToHCL().Ptr(), + Detail: fmt.Sprintf( + `%s %s resource %q %q has not been declared in %s.%s`, + modeArticleUpper, modeAdjective, + addr.Type, addr.Name, + moduleConfigDisplayAddr(modCfg.Path), + suggestion, + ), + Subject: rng.ToHCL().Ptr(), }) return diags } + if cfg.Container != nil && (source == nil || !cfg.Container.Accessible(source)) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: `Reference to scoped resource`, + Detail: fmt.Sprintf(`The referenced %s resource %q %q is not available from this context.`, modeAdjective, addr.Type, addr.Name), + Subject: rng.ToHCL().Ptr(), + }) + } + providerFqn := modCfg.Module.ProviderForLocalConfig(cfg.ProviderConfigAddr()) - schema, _, err := d.Evaluator.Plugins.ResourceTypeSchema(providerFqn, addr.Mode, addr.Type) + schema, err := plugins.ResourceTypeSchema(providerFqn, addr.Mode, addr.Type) if err != nil { // Prior validation should've taken care of a schema lookup error, // so we should never get here but we'll handle it here anyway for @@ -237,15 +262,20 @@ func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Co }) } - if schema == nil { + if schema.Body == nil { // Prior validation should've taken care of a resource block with an // unsupported type, so we should never get here but we'll handle it // here anyway for robustness. diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: `Invalid resource type`, - Detail: fmt.Sprintf(`A %s resource type %q is not supported by provider %q.`, modeAdjective, addr.Type, providerFqn.String()), - Subject: rng.ToHCL().Ptr(), + Detail: fmt.Sprintf( + `%s %s resource type %q is not supported by provider %q.`, + modeArticleUpper, modeAdjective, + addr.Type, + providerFqn.String(), + ), + Subject: rng.ToHCL().Ptr(), }) return diags } @@ -268,13 +298,13 @@ func (d *evaluationStateData) staticValidateResourceReference(modCfg *configs.Co // If we got this far then we'll try to validate the remaining traversal // steps against our schema. - moreDiags := schema.StaticValidateTraversal(remain) + moreDiags := schema.Body.StaticValidateTraversal(remain) diags = diags.Append(moreDiags) return diags } -func (d *evaluationStateData) staticValidateModuleCallReference(modCfg *configs.Config, addr addrs.ModuleCall, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { +func staticValidateModuleCallReference(modCfg *configs.Config, addr addrs.ModuleCall, remain hcl.Traversal, rng tfdiags.SourceRange) tfdiags.Diagnostics { var diags tfdiags.Diagnostics // For now, our focus here is just in testing that the referenced module diff --git a/internal/terraform/evaluate_valid_test.go b/internal/terraform/evaluate_valid_test.go index cfdfdea1f5..4e37f5f5dd 100644 --- a/internal/terraform/evaluate_valid_test.go +++ b/internal/terraform/evaluate_valid_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,50 +8,53 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/providers" ) func TestStaticValidateReferences(t *testing.T) { tests := []struct { Ref string + Src addrs.Referenceable WantErr string }{ { - "aws_instance.no_count", - ``, + Ref: "aws_instance.no_count", + WantErr: ``, }, { - "aws_instance.count", - ``, + Ref: "aws_instance.count", + WantErr: ``, }, { - "aws_instance.count[0]", - ``, + Ref: "aws_instance.count[0]", + WantErr: ``, }, { - "aws_instance.nonexist", - `Reference to undeclared resource: A managed resource "aws_instance" "nonexist" has not been declared in the root module.`, + Ref: "aws_instance.nonexist", + WantErr: `Reference to undeclared resource: A managed resource "aws_instance" "nonexist" has not been declared in the root module.`, }, { - "beep.boop", - `Reference to undeclared resource: A managed resource "beep" "boop" has not been declared in the root module. + Ref: "beep.boop", + WantErr: `Reference to undeclared resource: A managed resource "beep" "boop" has not been declared in the root module. Did you mean the data resource data.beep.boop?`, }, { - "aws_instance.no_count[0]", - `Unexpected resource instance key: Because aws_instance.no_count does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource.`, + Ref: "aws_instance.no_count[0]", + WantErr: `Unexpected resource instance key: Because aws_instance.no_count does not have "count" or "for_each" set, references to it must not include an index key. Remove the bracketed index to refer to the single instance of this resource.`, }, { - "aws_instance.count.foo", + Ref: "aws_instance.count.foo", // In this case we return two errors that are somewhat redundant with // one another, but we'll accept that because they both report the // problem from different perspectives and so give the user more // opportunity to understand what's going on here. - `2 problems: + WantErr: `2 problems: - Missing resource instance key: Because aws_instance.count has "count" set, its attributes must be accessed on specific instances. @@ -57,28 +63,74 @@ For example, to correlate with indices of a referring resource, use: - Unsupported attribute: This object has no argument, nested block, or exported attribute named "foo".`, }, { - "boop_instance.yep", - ``, + Ref: "boop_instance.yep", + WantErr: ``, }, { - "boop_whatever.nope", - `Invalid resource type: A managed resource type "boop_whatever" is not supported by provider "registry.terraform.io/foobar/beep".`, + Ref: "boop_whatever.nope", + WantErr: `Invalid resource type: A managed resource type "boop_whatever" is not supported by provider "registry.terraform.io/foobar/beep".`, + }, + { + Ref: "data.boop_data.boop_nested", + WantErr: `Reference to scoped resource: The referenced data resource "boop_data" "boop_nested" is not available from this context.`, + }, + { + Ref: "ephemeral.beep.boop", + WantErr: ``, + }, + { + Ref: "ephemeral.beep.nonexistant", + WantErr: `Reference to undeclared resource: An ephemeral resource "beep" "nonexistant" has not been declared in the root module.`, + }, + { + Ref: "data.boop_data.boop_nested", + WantErr: ``, + Src: addrs.Check{Name: "foo"}, + }, + { + Ref: "run.zero", + // This one resembles a reference to a previous run in a .tftest.hcl + // file, but when inside a .tf file it must be understood as a + // reference to a resource of type "run", just in case such a + // resource type exists in some provider somewhere. + WantErr: `Reference to undeclared resource: A managed resource "run" "zero" has not been declared in the root module.`, }, } cfg := testModule(t, "static-validate-refs") evaluator := &Evaluator{ Config: cfg, - Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]*ProviderSchema{ + Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]providers.ProviderSchema{ addrs.NewDefaultProvider("aws"): { - ResourceTypes: map[string]*configschema.Block{ - "aws_instance": {}, + ResourceTypes: map[string]providers.Schema{ + "aws_instance": { + Body: &configschema.Block{}, + }, }, }, addrs.MustParseProviderSourceString("foobar/beep"): { - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ // intentional mismatch between resource type prefix and provider type - "boop_instance": {}, + "boop_instance": { + Body: &configschema.Block{}, + }, + }, + DataSources: map[string]providers.Schema{ + "boop_data": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }, + }, + }, + EphemeralResourceTypes: map[string]providers.Schema{ + "beep": { + Body: &configschema.Block{}, + }, }, }, }), @@ -91,16 +143,12 @@ For example, to correlate with indices of a referring resource, use: t.Fatal(hclDiags.Error()) } - refs, diags := lang.References([]hcl.Traversal{traversal}) + refs, diags := langrefs.References(addrs.ParseRef, []hcl.Traversal{traversal}) if diags.HasErrors() { t.Fatal(diags.Err()) } - data := &evaluationStateData{ - Evaluator: evaluator, - } - - diags = data.StaticValidateReferences(refs, nil) + diags = evaluator.StaticValidateReferences(refs, addrs.RootModule, nil, test.Src) if diags.HasErrors() { if test.WantErr == "" { t.Fatalf("Unexpected diagnostics: %s", diags.Err()) diff --git a/internal/terraform/execute.go b/internal/terraform/execute.go index 8c3a6fe15e..fe6f49e63e 100644 --- a/internal/terraform/execute.go +++ b/internal/terraform/execute.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "github.com/hashicorp/terraform/internal/tfdiags" diff --git a/internal/terraform/features.go b/internal/terraform/features.go index 97c77bdbd0..18c1bfb7de 100644 --- a/internal/terraform/features.go +++ b/internal/terraform/features.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "os" diff --git a/internal/terraform/graph.go b/internal/terraform/graph.go index 9e2f195531..d641fb57b4 100644 --- a/internal/terraform/graph.go +++ b/internal/terraform/graph.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "fmt" "log" "strings" @@ -47,6 +51,14 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { log.Printf("[TRACE] vertex %q: starting visit (%T)", dag.VertexName(v), v) defer func() { + if r := recover(); r != nil { + // If the walkFn panics, we get confusing logs about how the + // visit was complete. To stop this, we'll catch the panic log + // that the vertex panicked without finishing and re-panic. + log.Printf("[ERROR] vertex %q panicked", dag.VertexName(v)) + panic(r) // re-panic + } + if diags.HasErrors() { for _, diag := range diags { if diag.Severity() == tfdiags.Error { @@ -60,13 +72,70 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { } }() + haveOverrides := !ctx.Overrides().Empty() + + // If the graph node is overridable, we'll check our overrides to see + // if we need to apply any overrides to the node. + if overridable, ok := v.(GraphNodeOverridable); ok && haveOverrides { + // It'd be nice if we could just pass the overrides directly into + // the nodes, but the way the AbstractNodeResource is created is + // complicated and it's not easy to make sure that every + // implementation sets the overrides correctly. Instead, we just + // do it from this single location to keep things simple. + // + // See the output node for an example of providing the overrides + // directly to the node. + if override, ok := ctx.Overrides().GetResourceOverride(overridable.ResourceInstanceAddr(), overridable.ConfigProvider()); ok { + overridable.SetOverride(override) + } + } + + if provider, ok := v.(GraphNodeProvider); ok && haveOverrides { + // If we find a legacy provider within an overridden module, we + // can't evaluate the config so we have to skip it. We do this here + // for the similar reasons as the resource overrides above, and to + // keep all the override logic together. + addr := provider.ProviderAddr() + // UnkeyedInstanceShim is used by legacy provider configs within a + // module to return an instance of that module, since they can never + // exist within an expanded instance. + if ctx.Overrides().IsOverridden(addr.Module.UnkeyedInstanceShim()) { + log.Printf("[DEBUG] skipping provider %s found within overridden module", addr) + return + } + } + // vertexCtx is the context that we use when evaluating. This - // is normally the context of our graph but can be overridden - // with a GraphNodeModuleInstance impl. + // is normally the global context but can be overridden + // with either a GraphNodeModuleInstance, GraphNodePartialExpandedModule, + // or graphNodeEvalContextScope implementation. (These interfaces are + // all intentionally mutually-exclusive by having the same method + // name but different signatures, since a node can only belong to + // one context at a time.) vertexCtx := ctx - if pn, ok := v.(GraphNodeModuleInstance); ok { - vertexCtx = walker.EnterPath(pn.Path()) - defer walker.ExitPath(pn.Path()) + if pn, ok := v.(graphNodeEvalContextScope); ok { + scope := pn.Path() + log.Printf("[TRACE] vertex %q: belongs to %s", dag.VertexName(v), scope) + vertexCtx = walker.enterScope(scope) + defer walker.exitScope(scope) + } else if pn, ok := v.(GraphNodeModuleInstance); ok { + moduleAddr := pn.Path() // An addrs.ModuleInstance + log.Printf("[TRACE] vertex %q: belongs to %s", dag.VertexName(v), moduleAddr) + scope := evalContextModuleInstance{ + Addr: moduleAddr, + } + vertexCtx = walker.enterScope(scope) + defer walker.exitScope(scope) + } else if pn, ok := v.(GraphNodePartialExpandedModule); ok { + moduleAddr := pn.Path() // An addrs.PartialExpandedModule + log.Printf("[TRACE] vertex %q: belongs to all of %s", dag.VertexName(v), moduleAddr) + scope := evalContextPartialExpandedModule{ + Addr: moduleAddr, + } + vertexCtx = walker.enterScope(scope) + defer walker.exitScope(scope) + } else { + log.Printf("[TRACE] vertex %q: does not belong to any module instance", dag.VertexName(v)) } // If the node is exec-able, then execute it. @@ -81,12 +150,35 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { if ev, ok := v.(GraphNodeDynamicExpandable); ok { log.Printf("[TRACE] vertex %q: expanding dynamic subgraph", dag.VertexName(v)) - g, err := ev.DynamicExpand(vertexCtx) - if err != nil { - diags = diags.Append(err) + g, moreDiags := ev.DynamicExpand(vertexCtx) + diags = diags.Append(moreDiags) + if diags.HasErrors() { + log.Printf("[TRACE] vertex %q: failed expanding dynamic subgraph: %s", dag.VertexName(v), diags.Err()) return } if g != nil { + // The subgraph should always be valid, per our normal acyclic + // graph validation rules. + if err := g.Validate(); err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Graph node has invalid dynamic subgraph", + fmt.Sprintf("The internal logic for %q generated an invalid dynamic subgraph: %s.\n\nThis is a bug in Terraform. Please report it!", dag.VertexName(v), err), + )) + return + } + // If we passed validation then there is exactly one root node. + // That root node should always be "rootNode", the singleton + // root node value. + if n, err := g.Root(); err != nil || n != dag.Vertex(rootNode) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Graph node has invalid dynamic subgraph", + fmt.Sprintf("The internal logic for %q generated an invalid dynamic subgraph: the root node is %T, which is not a suitable root node type.\n\nThis is a bug in Terraform. Please report it!", dag.VertexName(v), n), + )) + return + } + // Walk the subgraph log.Printf("[TRACE] vertex %q: entering dynamic subgraph", dag.VertexName(v)) subDiags := g.walk(walker) @@ -109,3 +201,77 @@ func (g *Graph) walk(walker GraphWalker) tfdiags.Diagnostics { return g.AcyclicGraph.Walk(walkFn) } + +// ResourceGraph derives a graph containing addresses of only the nodes in the +// receiver which implement [GraphNodeConfigResource], describing the +// relationships between all of their [addrs.ConfigResource] addresses. +// +// Nodes that do not have resource addresses are discarded but the +// result preserves correct dependency relationships for the nodes that are +// left, still taking into account any indirect dependencies through nodes +// that were discarded. +func (g *Graph) ResourceGraph() addrs.DirectedGraph[addrs.ConfigResource] { + // For now we're doing this in a kinda-janky way, by first constructing + // a reduced graph containing only GraphNodeConfigResource implementations + // and then using that temporary graph to construct the final graph to + // return. + + log.Printf("[TRACE] ResourceGraph: copying source graph\n") + tmpG := Graph{} + tmpG.Subsume(&g.Graph) + log.Printf("[TRACE] ResourceGraph: reducing graph\n") + tmpG.reducePreservingRelationships(func(n dag.Vertex) bool { + _, ret := n.(GraphNodeConfigResource) + return ret + }) + log.Printf("[TRACE] ResourceGraph: TransitiveReduction\n") + + // The resulting graph could have many more edges now, but alternate paths + // are not a problem for the deferral system, so we may choose not to run + // this as it may be very time consuming. The reducePreservingRelationships + // method also doesn't add many (if any) redundant new edges to most graphs. + tmpG.TransitiveReduction() + + log.Printf("[TRACE] ResourceGraph: creating address graph\n") + ret := addrs.NewDirectedGraph[addrs.ConfigResource]() + for _, n := range tmpG.Vertices() { + sourceR := n.(GraphNodeConfigResource) + sourceAddr := sourceR.ResourceAddr() + ret.Add(sourceAddr) + for _, dn := range tmpG.DownEdges(n) { + targetR := dn.(GraphNodeConfigResource) + + ret.AddDependency(sourceAddr, targetR.ResourceAddr()) + } + } + log.Printf("[TRACE] ResourceGraph: completed with %d nodes\n", len(ret.AllNodes())) + return ret +} + +// reducePreservingRelationships modifies the receiver in-place so that it only +// contains the nodes for which keepNode returns true, but also adds new +// edges to preserve the dependency relationships for all of the nodes +// that still remain. +func (g *Graph) reducePreservingRelationships(keepNode func(dag.Vertex) bool) { + for _, n := range g.Vertices() { + if keepNode(n) { + continue + } + + // If we're not going to keep this node then we need to connect + // all of its dependents to all of its dependencies so that the + // ordering is still preserved for those nodes that remain. + // However, this will often generate more edges than are strictly + // required and so it could be productive to run a transitive + // reduction afterwards. + dependents := g.UpEdges(n) + dependencies := g.DownEdges(n) + for dependent := range dependents { + for dependency := range dependencies { + edge := dag.BasicEdge(dependent, dependency) + g.Connect(edge) + } + } + g.Remove(n) + } +} diff --git a/internal/terraform/graph_builder.go b/internal/terraform/graph_builder.go index 1c69ee41f8..4b7d6f781b 100644 --- a/internal/terraform/graph_builder.go +++ b/internal/terraform/graph_builder.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -24,6 +27,10 @@ type BasicGraphBuilder struct { Steps []GraphTransformer // Optional name to add to the graph debug log Name string + + // SkipGraphValidation indicates whether the graph validation (enabled by default) + // should be skipped after the graph is built. + SkipGraphValidation bool } func (b *BasicGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Diagnostics) { @@ -48,6 +55,9 @@ func (b *BasicGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Di if err != nil { if nf, isNF := err.(tfdiags.NonFatalError); isNF { diags = diags.Append(nf.Diagnostics) + } else if diag, isDiag := err.(tfdiags.DiagnosticsAsError); isDiag { + diags = diags.Append(diag.Diagnostics) + return g, diags } else { diags = diags.Append(err) return g, diags @@ -55,6 +65,13 @@ func (b *BasicGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Di } } + // Return early if the graph validation is skipped + // This behavior is currently only used by the graph command + // which only wants to display the dot representation of the graph + if b.SkipGraphValidation { + return g, diags + } + if err := g.Validate(); err != nil { log.Printf("[ERROR] Graph validation failed. Graph:\n\n%s", g.String()) diags = diags.Append(err) diff --git a/internal/terraform/graph_builder_apply.go b/internal/terraform/graph_builder_apply.go index 619e2e14ff..ee871eb0e5 100644 --- a/internal/terraform/graph_builder_apply.go +++ b/internal/terraform/graph_builder_apply.go @@ -1,10 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -21,7 +26,11 @@ type ApplyGraphBuilder struct { Config *configs.Config // Changes describes the changes that we need apply. - Changes *plans.Changes + Changes *plans.ChangesSrc + + // DeferredChanges describes the changes that were deferred during the plan + // and should not be applied. + DeferredChanges []*plans.DeferredResourceInstanceChangeSrc // State is the current state State *states.State @@ -31,6 +40,12 @@ type ApplyGraphBuilder struct { // to get a consistent result. RootVariableValues InputValues + // ExternalProviderConfigs are pre-initialized root module provider + // configurations that the graph builder should assume will be available + // immediately during the subsequent plan walk, without any explicit + // initialization step. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + // Plugins is a library of the plug-in components (providers and // provisioners) available for use. Plugins *contextPlugins @@ -46,13 +61,30 @@ type ApplyGraphBuilder struct { // The apply step refers to these as part of verifying that the planned // actions remain consistent between plan and apply. ForceReplace []addrs.AbsResourceInstance + + // Plan Operation this graph will be used for. + Operation walkOperation + + // ExternalReferences allows the external caller to pass in references to + // nodes that should not be pruned even if they are not referenced within + // the actual graph. + ExternalReferences []*addrs.Reference + + // Overrides provides the set of overrides supplied by the testing + // framework. + Overrides *mocking.Overrides + + // SkipGraphValidation indicates whether the graph builder should skip + // validation of the graph. + SkipGraphValidation bool } // See GraphBuilder func (b *ApplyGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Diagnostics) { return (&BasicGraphBuilder{ - Steps: b.Steps(), - Name: "ApplyGraphBuilder", + Steps: b.Steps(), + Name: "ApplyGraphBuilder", + SkipGraphValidation: b.SkipGraphValidation, }).Build(path) } @@ -89,10 +121,22 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { }, // Add dynamic values - &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues}, - &ModuleVariableTransformer{Config: b.Config}, + &RootVariableTransformer{ + Config: b.Config, + RawValues: b.RootVariableValues, + DestroyApply: b.Operation == walkDestroy, + }, + &ModuleVariableTransformer{ + Config: b.Config, + DestroyApply: b.Operation == walkDestroy, + }, + &variableValidationTransformer{}, &LocalTransformer{Config: b.Config}, - &OutputTransformer{Config: b.Config, Changes: b.Changes}, + &OutputTransformer{ + Config: b.Config, + Destroying: b.Operation == walkDestroy, + Overrides: b.Overrides, + }, // Creates all the resource instances represented in the diff, along // with dependency edges against the whole-resource nodes added by @@ -101,6 +145,19 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { Concrete: concreteResourceInstance, State: b.State, Changes: b.Changes, + Config: b.Config, + }, + + // Creates nodes for all the deferred changes. + &DeferredTransformer{ + DeferredChanges: b.DeferredChanges, + }, + + // Add nodes and edges for check block assertions. Check block data + // sources were added earlier. + &checkTransformer{ + Config: b.Config, + Operation: b.Operation, }, // Attach the state @@ -113,7 +170,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { &AttachResourceConfigTransformer{Config: b.Config}, // add providers - transformProviders(concreteProvider, b.Config), + transformProviders(concreteProvider, b.Config, b.ExternalProviderConfigs), // Remove modules no longer present in the config &RemovedModuleTransformer{Config: b.Config, State: b.State}, @@ -127,29 +184,51 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer { // objects that can belong to modules. &ModuleExpansionTransformer{Config: b.Config}, + // Plug in any external references. + &ExternalReferenceTransformer{ + ExternalReferences: b.ExternalReferences, + }, + // Connect references so ordering is correct &ReferenceTransformer{}, &AttachDependenciesTransformer{}, + // Nested data blocks should be loaded after every other resource has + // done its thing. + &checkStartTransformer{Config: b.Config, Operation: b.Operation}, + // Detect when create_before_destroy must be forced on for a particular // node due to dependency edges, to avoid graph cycles during apply. + // + // FIXME: this should not need to be recalculated during apply. + // Currently however, the instance object which stores the planned + // information is lost for newly created instances because it contains + // no state value, and we end up recalculating CBD for all nodes. &ForcedCBDTransformer{}, // Destruction ordering - &DestroyEdgeTransformer{}, + &DestroyEdgeTransformer{ + Changes: b.Changes, + Operation: b.Operation, + }, &CBDEdgeTransformer{ Config: b.Config, State: b.State, }, - // We need to remove configuration nodes that are not used at all, as - // they may not be able to evaluate, especially during destroy. - // These include variables, locals, and instance expanders. - &pruneUnusedNodesTransformer{}, + // In a destroy, we need to remove configuration nodes that are not used + // at all, as they may not be able to evaluate. These include variables, + // locals, and instance expanders. + &pruneUnusedNodesTransformer{ + skip: b.Operation != walkDestroy, + }, // Target &TargetsTransformer{Targets: b.Targets}, + // Close any ephemeral resource instances. + &ephemeralResourceCloseTransformer{}, + // Close opened plugin connections &CloseProviderTransformer{}, diff --git a/internal/terraform/graph_builder_apply_test.go b/internal/terraform/graph_builder_apply_test.go index 88ebcfefd1..f573e7e6d8 100644 --- a/internal/terraform/graph_builder_apply_test.go +++ b/internal/terraform/graph_builder_apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -10,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" ) @@ -18,7 +22,7 @@ func TestApplyGraphBuilder_impl(t *testing.T) { } func TestApplyGraphBuilder(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.create"), @@ -72,7 +76,7 @@ func TestApplyGraphBuilder(t *testing.T) { // This tests the ordering of two resources where a non-CBD depends // on a CBD. GH-11349. func TestApplyGraphBuilder_depCbd(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A"), @@ -165,7 +169,7 @@ func TestApplyGraphBuilder_depCbd(t *testing.T) { // This tests the ordering of two resources that are both CBD that // require destroy/create. func TestApplyGraphBuilder_doubleCBD(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A"), @@ -237,7 +241,7 @@ func TestApplyGraphBuilder_doubleCBD(t *testing.T) { // This tests the ordering of two resources being destroyed that depend // on each other from only state. GH-11749 func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("module.child.test_object.A"), @@ -299,7 +303,7 @@ func TestApplyGraphBuilder_destroyStateOnly(t *testing.T) { // This tests the ordering of destroying a single count of a resource. func TestApplyGraphBuilder_destroyCount(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A[1]"), @@ -361,7 +365,7 @@ func TestApplyGraphBuilder_destroyCount(t *testing.T) { } func TestApplyGraphBuilder_moduleDestroy(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("module.A.test_object.foo"), @@ -419,7 +423,7 @@ func TestApplyGraphBuilder_moduleDestroy(t *testing.T) { } func TestApplyGraphBuilder_targetModule(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.foo"), @@ -463,14 +467,14 @@ func TestApplyGraphBuilder_updateFromOrphan(t *testing.T) { cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b_id"), "test_string": cty.StringVal("a_id"), - }), instanceSchema.ImpliedType()) + }), instanceSchema.Body.ImpliedType()) bAfter, _ := plans.NewDynamicValue( cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b_id"), "test_string": cty.StringVal("changed"), - }), instanceSchema.ImpliedType()) + }), instanceSchema.Body.ImpliedType()) - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.a"), @@ -568,14 +572,14 @@ func TestApplyGraphBuilder_updateFromCBDOrphan(t *testing.T) { cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b_id"), "test_string": cty.StringVal("a_id"), - }), instanceSchema.ImpliedType()) + }), instanceSchema.Body.ImpliedType()) bAfter, _ := plans.NewDynamicValue( cty.ObjectVal(map[string]cty.Value{ "id": cty.StringVal("b_id"), "test_string": cty.StringVal("changed"), - }), instanceSchema.ImpliedType()) + }), instanceSchema.Body.ImpliedType()) - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.a"), @@ -660,7 +664,7 @@ test_object.b // The orphan clean up node should not be connected to a provider func TestApplyGraphBuilder_orphanedWithProvider(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A"), @@ -699,6 +703,90 @@ func TestApplyGraphBuilder_orphanedWithProvider(t *testing.T) { testGraphNotContains(t, g, "provider.test") } +func TestApplyGraphBuilder_withChecks(t *testing.T) { + awsProvider := mockProviderWithResourceTypeSchema("aws_instance", simpleTestSchema()) + + changes := &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("aws_instance.foo"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: mustResourceInstanceAddr("aws_instance.baz"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Create, + }, + }, + { + Addr: mustResourceInstanceAddr("data.aws_data_source.bar"), + ChangeSrc: plans.ChangeSrc{ + Action: plans.Read, + }, + ActionReason: plans.ResourceInstanceReadBecauseCheckNested, + }, + }, + } + + plugins := newContextPlugins(map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider), + }, nil, nil) + + b := &ApplyGraphBuilder{ + Config: testModule(t, "apply-with-checks"), + Changes: changes, + Plugins: plugins, + State: states.NewState(), + Operation: walkApply, + } + + g, err := b.Build(addrs.RootModuleInstance) + if err != nil { + t.Fatalf("err: %s", err) + } + + if g.Path.String() != addrs.RootModuleInstance.String() { + t.Fatalf("wrong path %q", g.Path.String()) + } + + got := strings.TrimSpace(g.String()) + // We're especially looking for the edge here, where aws_instance.bat + // has a dependency on aws_instance.boo + want := strings.TrimSpace(testPlanWithCheckGraphBuilderStr) + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("\ngot:\n%s\n\nwant:\n%s\n\ndiff:\n%s", got, want, diff) + } + +} + +const testPlanWithCheckGraphBuilderStr = ` +(execute checks) + aws_instance.baz +aws_instance.baz + aws_instance.baz (expand) +aws_instance.baz (expand) + aws_instance.foo +aws_instance.foo + aws_instance.foo (expand) +aws_instance.foo (expand) + provider["registry.terraform.io/hashicorp/aws"] +check.my_check (expand) + data.aws_data_source.bar +data.aws_data_source.bar + (execute checks) + data.aws_data_source.bar (expand) +data.aws_data_source.bar (expand) + provider["registry.terraform.io/hashicorp/aws"] +provider["registry.terraform.io/hashicorp/aws"] +provider["registry.terraform.io/hashicorp/aws"] (close) + data.aws_data_source.bar +root + check.my_check (expand) + provider["registry.terraform.io/hashicorp/aws"] (close) +` + const testApplyGraphBuilderStr = ` module.child (close) module.child.test_object.other @@ -709,11 +797,9 @@ module.child.test_object.create (expand) module.child (expand) provider["registry.terraform.io/hashicorp/test"] module.child.test_object.other - module.child.test_object.create module.child.test_object.other (expand) module.child.test_object.other (expand) - module.child (expand) - provider["registry.terraform.io/hashicorp/test"] + module.child.test_object.create provider["registry.terraform.io/hashicorp/test"] provider["registry.terraform.io/hashicorp/test"] (close) module.child.test_object.other @@ -726,10 +812,9 @@ test_object.create test_object.create (expand) provider["registry.terraform.io/hashicorp/test"] test_object.other - test_object.create test_object.other (expand) test_object.other (expand) - provider["registry.terraform.io/hashicorp/test"] + test_object.create ` const testApplyGraphBuilderDestroyCountStr = ` @@ -743,9 +828,8 @@ test_object.A (expand) test_object.A[1] (destroy) provider["registry.terraform.io/hashicorp/test"] test_object.B - test_object.A (expand) test_object.A[1] (destroy) test_object.B (expand) test_object.B (expand) - provider["registry.terraform.io/hashicorp/test"] + test_object.A (expand) ` diff --git a/internal/terraform/graph_builder_eval.go b/internal/terraform/graph_builder_eval.go index 4e205045f7..697b66fee0 100644 --- a/internal/terraform/graph_builder_eval.go +++ b/internal/terraform/graph_builder_eval.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -35,6 +39,12 @@ type EvalGraphBuilder struct { // of the plan walk. RootVariableValues InputValues + // ExternalProviderConfigs are pre-initialized root module provider + // configurations that the graph builder should assume will be available + // immediately during the subsequent plan walk, without any explicit + // initialization step. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + // Plugins is a library of plug-in components (providers and // provisioners) available for use. Plugins *contextPlugins @@ -64,10 +74,14 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer { }, // Add dynamic values - &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues}, - &ModuleVariableTransformer{Config: b.Config}, + &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues, Planning: true}, + &ModuleVariableTransformer{Config: b.Config, Planning: true}, + &variableValidationTransformer{}, &LocalTransformer{Config: b.Config}, - &OutputTransformer{Config: b.Config}, + &OutputTransformer{ + Config: b.Config, + Planning: true, + }, // Attach the configuration to any resources &AttachResourceConfigTransformer{Config: b.Config}, @@ -75,7 +89,7 @@ func (b *EvalGraphBuilder) Steps() []GraphTransformer { // Attach the state &AttachStateTransformer{State: b.State}, - transformProviders(concreteProvider, b.Config), + transformProviders(concreteProvider, b.Config, b.ExternalProviderConfigs), // Must attach schemas before ReferenceTransformer so that we can // analyze the configuration to find references. diff --git a/internal/terraform/graph_builder_plan.go b/internal/terraform/graph_builder_plan.go index 2f0a9f2d87..22c267afaa 100644 --- a/internal/terraform/graph_builder_plan.go +++ b/internal/terraform/graph_builder_plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,6 +9,8 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -34,6 +39,12 @@ type PlanGraphBuilder struct { // of the plan walk. RootVariableValues InputValues + // ExternalProviderConfigs are pre-initialized root module provider + // configurations that the graph builder should assume will be available + // immediately during the subsequent plan walk, without any explicit + // initialization step. + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + // Plugins is a library of plug-in components (providers and // provisioners) available for use. Plugins *contextPlugins @@ -49,6 +60,11 @@ type PlanGraphBuilder struct { // skipRefresh indicates that we should skip refreshing managed resources skipRefresh bool + // preDestroyRefresh indicates that we are executing the refresh which + // happens immediately before a destroy plan, which happens to use the + // normal planing mode so skipPlanChanges cannot be set. + preDestroyRefresh bool + // skipPlanChanges indicates that we should skip the step of comparing // prior state with configuration and generating planned changes to // resource instances. (This is for the "refresh only" planning mode, @@ -65,16 +81,44 @@ type PlanGraphBuilder struct { // Plan Operation this graph will be used for. Operation walkOperation + // ExternalReferences allows the external caller to pass in references to + // nodes that should not be pruned even if they are not referenced within + // the actual graph. + ExternalReferences []*addrs.Reference + + // Overrides provides the set of overrides supplied by the testing + // framework. + Overrides *mocking.Overrides + // ImportTargets are the list of resources to import. ImportTargets []*ImportTarget + + // forgetResources lists the resources that are to be forgotten, i.e. removed + // from state without destroying. + forgetResources []addrs.ConfigResource + + // forgetModules lists the modules that are to be forgotten, i.e. removed + // from state without destroying. + forgetModules []addrs.Module + + // GenerateConfig tells Terraform where to write and generated config for + // any import targets that do not already have configuration. + // + // If empty, then config will not be generated. + GenerateConfigPath string + + // SkipGraphValidation indicates whether the graph builder should skip + // validation of the graph. + SkipGraphValidation bool } // See GraphBuilder func (b *PlanGraphBuilder) Build(path addrs.ModuleInstance) (*Graph, tfdiags.Diagnostics) { log.Printf("[TRACE] building graph for %s", b.Operation) return (&BasicGraphBuilder{ - Steps: b.Steps(), - Name: "PlanGraphBuilder", + Steps: b.Steps(), + Name: "PlanGraphBuilder", + SkipGraphValidation: b.SkipGraphValidation, }).Build(path) } @@ -98,21 +142,49 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { &ConfigTransformer{ Concrete: b.ConcreteResource, Config: b.Config, - - // Resources are not added from the config on destroy. - skip: b.Operation == walkPlanDestroy, + destroy: b.Operation == walkDestroy || b.Operation == walkPlanDestroy, importTargets: b.ImportTargets, + + // We only want to generate config during a plan operation. + generateConfigPathForImportTargets: b.GenerateConfigPath, }, // Add dynamic values - &RootVariableTransformer{Config: b.Config, RawValues: b.RootVariableValues}, - &ModuleVariableTransformer{Config: b.Config}, + &RootVariableTransformer{ + Config: b.Config, + RawValues: b.RootVariableValues, + Planning: true, + DestroyApply: false, // always false for planning + }, + &ModuleVariableTransformer{ + Config: b.Config, + Planning: true, + DestroyApply: false, // always false for planning + }, + &variableValidationTransformer{ + validateWalk: b.Operation == walkValidate, + }, &LocalTransformer{Config: b.Config}, &OutputTransformer{ - Config: b.Config, - RefreshOnly: b.skipPlanChanges, - removeRootOutputs: b.Operation == walkPlanDestroy, + Config: b.Config, + RefreshOnly: b.skipPlanChanges || b.preDestroyRefresh, + Destroying: b.Operation == walkPlanDestroy, + Overrides: b.Overrides, + + // NOTE: We currently treat anything built with the plan graph + // builder as "planning" for our purposes here, because we share + // the same graph node implementation between all of the walk + // types and so the pre-planning walks still think they are + // producing a plan even though we immediately discard it. + Planning: true, + }, + + // Add nodes and edges for the check block assertions. Check block data + // sources were added earlier. + &checkTransformer{ + Config: b.Config, + Operation: b.Operation, }, // Add orphan resources @@ -138,13 +210,17 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { &AttachStateTransformer{State: b.State}, // Create orphan output nodes - &OrphanOutputTransformer{Config: b.Config, State: b.State}, + &OrphanOutputTransformer{ + Config: b.Config, + State: b.State, + Planning: true, + }, // Attach the configuration to any resources &AttachResourceConfigTransformer{Config: b.Config}, // add providers - transformProviders(b.ConcreteProvider, b.Config), + transformProviders(b.ConcreteProvider, b.Config, b.ExternalProviderConfigs), // Remove modules no longer present in the config &RemovedModuleTransformer{Config: b.Config, State: b.State}, @@ -158,6 +234,11 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // objects that can belong to modules. &ModuleExpansionTransformer{Concrete: b.ConcreteModule, Config: b.Config}, + // Plug in any external references. + &ExternalReferenceTransformer{ + ExternalReferences: b.ExternalReferences, + }, + &ReferenceTransformer{}, &AttachDependenciesTransformer{}, @@ -168,7 +249,13 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // DestroyEdgeTransformer is only required during a plan so that the // TargetsTransformer can determine which nodes to keep in the graph. - &DestroyEdgeTransformer{}, + &DestroyEdgeTransformer{ + Operation: b.Operation, + }, + + &pruneUnusedNodesTransformer{ + skip: b.Operation != walkPlanDestroy, + }, // Target &TargetsTransformer{Targets: b.Targets}, @@ -177,6 +264,9 @@ func (b *PlanGraphBuilder) Steps() []GraphTransformer { // node due to dependency edges, to avoid graph cycles during apply. &ForcedCBDTransformer{}, + // Close any ephemeral resource instances. + &ephemeralResourceCloseTransformer{skip: b.Operation == walkValidate}, + // Close opened plugin connections &CloseProviderTransformer{}, @@ -203,6 +293,7 @@ func (b *PlanGraphBuilder) initPlan() { NodeAbstractResource: a, skipRefresh: b.skipRefresh, skipPlanChanges: b.skipPlanChanges, + preDestroyRefresh: b.preDestroyRefresh, forceReplace: b.ForceReplace, } } @@ -212,6 +303,8 @@ func (b *PlanGraphBuilder) initPlan() { NodeAbstractResourceInstance: a, skipRefresh: b.skipRefresh, skipPlanChanges: b.skipPlanChanges, + forgetResources: b.forgetResources, + forgetModules: b.forgetModules, } } @@ -222,6 +315,8 @@ func (b *PlanGraphBuilder) initPlan() { skipRefresh: b.skipRefresh, skipPlanChanges: b.skipPlanChanges, + forgetResources: b.forgetResources, + forgetModules: b.forgetModules, } } } diff --git a/internal/terraform/graph_builder_plan_test.go b/internal/terraform/graph_builder_plan_test.go index 8775e1f175..ce2e4b9b19 100644 --- a/internal/terraform/graph_builder_plan_test.go +++ b/internal/terraform/graph_builder_plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -10,6 +13,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" ) func TestPlanGraphBuilder_impl(t *testing.T) { @@ -17,13 +21,13 @@ func TestPlanGraphBuilder_impl(t *testing.T) { } func TestPlanGraphBuilder(t *testing.T) { - awsProvider := &MockProvider{ + awsProvider := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ - "aws_security_group": {Block: simpleTestSchema()}, - "aws_instance": {Block: simpleTestSchema()}, - "aws_load_balancer": {Block: simpleTestSchema()}, + "aws_security_group": {Body: simpleTestSchema()}, + "aws_instance": {Body: simpleTestSchema()}, + "aws_load_balancer": {Body: simpleTestSchema()}, }, }, } @@ -31,7 +35,7 @@ func TestPlanGraphBuilder(t *testing.T) { plugins := newContextPlugins(map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider), addrs.NewDefaultProvider("openstack"): providers.FactoryFixed(openstackProvider), - }, nil) + }, nil, nil) b := &PlanGraphBuilder{ Config: testModule(t, "graph-builder-plan-basic"), @@ -53,6 +57,40 @@ func TestPlanGraphBuilder(t *testing.T) { if diff := cmp.Diff(want, got); diff != "" { t.Fatalf("wrong result\n%s", diff) } + + // We should also be able to derive a graph of the relationships between + // just the resource addresses, taking into account indirect dependencies + // through nodes that don't represent resources. + t.Run("ResourceGraph", func(t *testing.T) { + resAddrGraph := g.ResourceGraph() + got := strings.TrimSpace(resAddrGraph.StringForComparison()) + want := strings.TrimSpace(` +aws_instance.web + aws_security_group.firewall +aws_load_balancer.weblb + aws_instance.web +aws_security_group.firewall + openstack_floating_ip.random +openstack_floating_ip.random +`) + // HINT: aws_security_group.firewall depends on openstack_floating_ip.random + // because the aws provider configuration refers to it, and all of the + // aws_-prefixed resource types depend on their provider configuration. + // We collapse these indirect deps into direct deps as part of lowering + // into a graph of just resources. + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("wrong result\n%s", diff) + } + + // Building the resource graph should not have damaged the original graph. + { + got := strings.TrimSpace(g.String()) + want := strings.TrimSpace(testPlanGraphBuilderStr) + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("g.ResourceGraph has changed g (should not have modified it)\n%s", diff) + } + } + }) } func TestPlanGraphBuilder_dynamicBlock(t *testing.T) { @@ -74,7 +112,7 @@ func TestPlanGraphBuilder_dynamicBlock(t *testing.T) { }) plugins := newContextPlugins(map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): providers.FactoryFixed(provider), - }, nil) + }, nil, nil) b := &PlanGraphBuilder{ Config: testModule(t, "graph-builder-plan-dynblock"), @@ -130,7 +168,7 @@ func TestPlanGraphBuilder_attrAsBlocks(t *testing.T) { }) plugins := newContextPlugins(map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("test"): providers.FactoryFixed(provider), - }, nil) + }, nil, nil) b := &PlanGraphBuilder{ Config: testModule(t, "graph-builder-plan-attr-as-blocks"), @@ -195,7 +233,7 @@ func TestPlanGraphBuilder_forEach(t *testing.T) { plugins := newContextPlugins(map[addrs.Provider]providers.Factory{ addrs.NewDefaultProvider("aws"): providers.FactoryFixed(awsProvider), - }, nil) + }, nil, nil) b := &PlanGraphBuilder{ Config: testModule(t, "plan-for-each"), diff --git a/internal/terraform/graph_builder_test.go b/internal/terraform/graph_builder_test.go index 414fc0b8d7..08b34de7dc 100644 --- a/internal/terraform/graph_builder_test.go +++ b/internal/terraform/graph_builder_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/graph_dot.go b/internal/terraform/graph_dot.go index 22e701bcc1..05a7fb18f3 100644 --- a/internal/terraform/graph_dot.go +++ b/internal/terraform/graph_dot.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "github.com/hashicorp/terraform/internal/dag" diff --git a/internal/terraform/graph_dot_test.go b/internal/terraform/graph_dot_test.go index 5042534b4e..7bb307818a 100644 --- a/internal/terraform/graph_dot_test.go +++ b/internal/terraform/graph_dot_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/graph_interface_subgraph.go b/internal/terraform/graph_interface_subgraph.go index 6aa2206dfd..8f197591a6 100644 --- a/internal/terraform/graph_interface_subgraph.go +++ b/internal/terraform/graph_interface_subgraph.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -15,3 +18,35 @@ type GraphNodeModuleInstance interface { type GraphNodeModulePath interface { ModulePath() addrs.Module } + +// GraphNodePartialExpandedModule says that a node represents an unbounded +// set of objects within an unbounded set of module instances that happen +// to share a known address prefix. +// +// Nodes of this type typically produce placeholder data to support partial +// evaluation despite the full analysis of a module being deferred to a future +// plan when more information will be available. They might also perform +// checks and raise errors when something can be proven to be definitely +// invalid regardless of what the final set of module instances turns out to +// be. +// +// Node types implementing this interface cannot also implement +// [GraphNodeModuleInstance], because it is not possible to evaluate a +// node in two different contexts at once. +type GraphNodePartialExpandedModule interface { + Path() addrs.PartialExpandedModule +} + +// graphNodeEvalContextScope is essentially a combination of +// [GraphNodeModuleInstance] and [GraphNodePartialExpandedModule] for when +// the decision between the two must be made dynamically. +// +// When a graph node implements this interface, the [EvalContext] passed +// to its DynamicExpand and/or Execute method will be associated with whatever +// scope is returned by method Path. +type graphNodeEvalContextScope interface { + // Path must return a _non-nil_ evalContextScope value, which therefore + // describes either a fully-expanded module instance address or a + // partial-expanded module address. + Path() evalContextScope +} diff --git a/internal/terraform/graph_test.go b/internal/terraform/graph_test.go index 5e163a0213..03ee59d15c 100644 --- a/internal/terraform/graph_test.go +++ b/internal/terraform/graph_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -37,13 +40,8 @@ func testGraphHappensBefore(t *testing.T, g *Graph, A, B string) { } // Look at ancestors - deps, err := g.Ancestors(vertexB) - if err != nil { - t.Fatalf("Error: %s in graph:\n\n%s", err, g.String()) - } - // Make sure B is in there - for _, v := range deps.List() { + for _, v := range g.Ancestors(vertexB) { if dag.VertexName(v) == A { // Success return diff --git a/internal/terraform/graph_walk.go b/internal/terraform/graph_walk.go index 5a0041cb4f..09886fc043 100644 --- a/internal/terraform/graph_walk.go +++ b/internal/terraform/graph_walk.go @@ -1,7 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -9,8 +11,8 @@ import ( // with Graph.Walk will invoke the given callbacks under certain events. type GraphWalker interface { EvalContext() EvalContext - EnterPath(addrs.ModuleInstance) EvalContext - ExitPath(addrs.ModuleInstance) + enterScope(evalContextScope) EvalContext + exitScope(evalContextScope) Execute(EvalContext, GraphNodeExecutable) tfdiags.Diagnostics } @@ -20,6 +22,6 @@ type GraphWalker interface { type NullGraphWalker struct{} func (NullGraphWalker) EvalContext() EvalContext { return new(MockEvalContext) } -func (NullGraphWalker) EnterPath(addrs.ModuleInstance) EvalContext { return new(MockEvalContext) } -func (NullGraphWalker) ExitPath(addrs.ModuleInstance) {} +func (NullGraphWalker) enterScope(evalContextScope) EvalContext { return new(MockEvalContext) } +func (NullGraphWalker) exitScope(evalContextScope) {} func (NullGraphWalker) Execute(EvalContext, GraphNodeExecutable) tfdiags.Diagnostics { return nil } diff --git a/internal/terraform/graph_walk_context.go b/internal/terraform/graph_walk_context.go index 8060954059..6c1083d333 100644 --- a/internal/terraform/graph_walk_context.go +++ b/internal/terraform/graph_walk_context.go @@ -1,20 +1,28 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "context" "sync" - - "github.com/zclconf/go-cty/cty" + "time" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/refactoring" + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -25,48 +33,64 @@ type ContextGraphWalker struct { NullGraphWalker // Configurable values - Context *Context - State *states.SyncState // Used for safe concurrent access to state - RefreshState *states.SyncState // Used for safe concurrent access to state - PrevRunState *states.SyncState // Used for safe concurrent access to state - Changes *plans.ChangesSync // Used for safe concurrent writes to changes - Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results - InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances - MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements - Operation walkOperation - StopContext context.Context - RootVariableValues InputValues - Config *configs.Config + Context *Context + State *states.SyncState // Used for safe concurrent access to state + RefreshState *states.SyncState // Used for safe concurrent access to state + PrevRunState *states.SyncState // Used for safe concurrent access to state + Changes *plans.ChangesSync // Used for safe concurrent writes to changes + Checks *checks.State // Used for safe concurrent writes of checkable objects and their check results + NamedValues *namedvals.State // Tracks evaluation of input variables, local values, and output values + InstanceExpander *instances.Expander // Tracks our gradual expansion of module and resource instances + Deferrals *deferring.Deferred // Tracks any deferred actions + EphemeralResources *ephemeral.Resources // Tracks active instances of ephemeral resources + Imports []configs.Import + MoveResults refactoring.MoveResults // Read-only record of earlier processing of move statements + Operation walkOperation + StopContext context.Context + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface + Config *configs.Config + PlanTimestamp time.Time + Overrides *mocking.Overrides + // Forget if set to true will cause the plan to forget all resources. This is + // only allowd in the context of a destroy plan. + Forget bool // This is an output. Do not set this, nor read it while a graph walk // is in progress. NonFatalDiagnostics tfdiags.Diagnostics once sync.Once - contexts map[string]*BuiltinEvalContext + contexts collections.Map[evalContextScope, *BuiltinEvalContext] contextLock sync.Mutex - variableValues map[string]map[string]cty.Value - variableValuesLock sync.Mutex providerCache map[string]providers.Interface - providerSchemas map[string]*ProviderSchema + providerFuncCache map[string]providers.Interface + functionResults *lang.FunctionResults + providerSchemas map[string]providers.ProviderSchema providerLock sync.Mutex provisionerCache map[string]provisioners.Interface provisionerSchemas map[string]*configschema.Block provisionerLock sync.Mutex } -func (w *ContextGraphWalker) EnterPath(path addrs.ModuleInstance) EvalContext { +var _ GraphWalker = (*ContextGraphWalker)(nil) + +// enterScope provides an EvalContext associated with the given scope. +func (w *ContextGraphWalker) enterScope(scope evalContextScope) EvalContext { + if scope == nil { + // Just want a global EvalContext then, presumably. + return w.EvalContext() + } + w.contextLock.Lock() defer w.contextLock.Unlock() - // If we already have a context for this path cached, use that - key := path.String() - if ctx, ok := w.contexts[key]; ok { + // We might already have a context for this scope. + if ctx, ok := w.contexts.GetOk(scope); ok { return ctx } - ctx := w.EvalContext().WithPath(path) - w.contexts[key] = ctx.(*BuiltinEvalContext) + ctx := w.EvalContext().withScope(scope).(*BuiltinEvalContext) + w.contexts.Put(scope, ctx) return ctx } @@ -82,50 +106,53 @@ func (w *ContextGraphWalker) EvalContext() EvalContext { Operation: w.Operation, State: w.State, Changes: w.Changes, + EphemeralResources: w.EphemeralResources, Plugins: w.Context.plugins, - VariableValues: w.variableValues, - VariableValuesLock: &w.variableValuesLock, + Instances: w.InstanceExpander, + NamedValues: w.NamedValues, + Deferrals: w.Deferrals, + PlanTimestamp: w.PlanTimestamp, + FunctionResults: w.functionResults, } ctx := &BuiltinEvalContext{ - StopContext: w.StopContext, - Hooks: w.Context.hooks, - InputValue: w.Context.uiInput, - InstanceExpanderValue: w.InstanceExpander, - Plugins: w.Context.plugins, - MoveResultsValue: w.MoveResults, - ProviderCache: w.providerCache, - ProviderInputConfig: w.Context.providerInputConfig, - ProviderLock: &w.providerLock, - ProvisionerCache: w.provisionerCache, - ProvisionerLock: &w.provisionerLock, - ChangesValue: w.Changes, - ChecksValue: w.Checks, - StateValue: w.State, - RefreshStateValue: w.RefreshState, - PrevRunStateValue: w.PrevRunState, - Evaluator: evaluator, - VariableValues: w.variableValues, - VariableValuesLock: &w.variableValuesLock, + StopContext: w.StopContext, + Hooks: w.Context.hooks, + InputValue: w.Context.uiInput, + EphemeralResourcesValue: w.EphemeralResources, + InstanceExpanderValue: w.InstanceExpander, + Plugins: w.Context.plugins, + ExternalProviderConfigs: w.ExternalProviderConfigs, + MoveResultsValue: w.MoveResults, + ProviderCache: w.providerCache, + ProviderFuncCache: w.providerFuncCache, + FunctionResults: w.functionResults, + ProviderInputConfig: w.Context.providerInputConfig, + ProviderLock: &w.providerLock, + ProvisionerCache: w.provisionerCache, + ProvisionerLock: &w.provisionerLock, + ChangesValue: w.Changes, + ChecksValue: w.Checks, + NamedValuesValue: w.NamedValues, + DeferralsValue: w.Deferrals, + StateValue: w.State, + RefreshStateValue: w.RefreshState, + PrevRunStateValue: w.PrevRunState, + Evaluator: evaluator, + OverrideValues: w.Overrides, + forget: w.Forget, } return ctx } func (w *ContextGraphWalker) init() { - w.contexts = make(map[string]*BuiltinEvalContext) + w.contexts = collections.NewMap[evalContextScope, *BuiltinEvalContext]() w.providerCache = make(map[string]providers.Interface) - w.providerSchemas = make(map[string]*ProviderSchema) + w.providerFuncCache = make(map[string]providers.Interface) + w.providerSchemas = make(map[string]providers.ProviderSchema) w.provisionerCache = make(map[string]provisioners.Interface) w.provisionerSchemas = make(map[string]*configschema.Block) - w.variableValues = make(map[string]map[string]cty.Value) - - // Populate root module variable values. Other modules will be populated - // during the graph walk. - w.variableValues[""] = make(map[string]cty.Value) - for k, iv := range w.RootVariableValues { - w.variableValues[""][k] = iv.Value - } } func (w *ContextGraphWalker) Execute(ctx EvalContext, n GraphNodeExecutable) tfdiags.Diagnostics { diff --git a/internal/terraform/graph_walk_operation.go b/internal/terraform/graph_walk_operation.go index 798ff20e13..9100c8b881 100644 --- a/internal/terraform/graph_walk_operation.go +++ b/internal/terraform/graph_walk_operation.go @@ -1,6 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform -//go:generate go run golang.org/x/tools/cmd/stringer -type=walkOperation graph_walk_operation.go +//go:generate go tool golang.org/x/tools/cmd/stringer -type=walkOperation graph_walk_operation.go // walkOperation is an enum which tells the walkContext what to do. type walkOperation byte diff --git a/internal/terraform/graph_walk_test.go b/internal/terraform/graph_walk_test.go index 88b52a7481..3901c3965f 100644 --- a/internal/terraform/graph_walk_test.go +++ b/internal/terraform/graph_walk_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/hook.go b/internal/terraform/hook.go index 7e927e8a5d..4a6e4365fc 100644 --- a/internal/terraform/hook.go +++ b/internal/terraform/hook.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -22,6 +25,14 @@ const ( HookActionHalt ) +// HookResourceIdentity is passed to Hook interface methods to fully identify +// the resource instance being operated on. It currently includes the resource +// address and the provider address. +type HookResourceIdentity struct { + Addr addrs.AbsResourceInstance + ProviderAddr addrs.Provider +} + // Hook is the interface that must be implemented to hook into various // parts of Terraform, allowing you to inspect or change behavior at runtime. // @@ -33,14 +44,14 @@ type Hook interface { // PreApply and PostApply are called before and after an action for a // single instance is applied. The error argument in PostApply is the // error, if any, that was returned from the provider Apply call itself. - PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) - PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) + PreApply(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) + PostApply(id HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (HookAction, error) // PreDiff and PostDiff are called before and after a provider is given // the opportunity to customize the proposed new state to produce the // planned new state. - PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) - PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) + PreDiff(id HookResourceIdentity, dk addrs.DeposedKey, priorState, proposedNewState cty.Value) (HookAction, error) + PostDiff(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) // The provisioning hooks signal both the overall start end end of // provisioning for a particular instance and of each of the individual @@ -60,25 +71,59 @@ type Hook interface { // This will be called multiple times as output comes in, with each call // representing one line of output. It cannot control whether the // provisioner continues running. - PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) - PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) - PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) - PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) - ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) + PreProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) + PostProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) + PreProvisionInstanceStep(id HookResourceIdentity, typeName string) (HookAction, error) + PostProvisionInstanceStep(id HookResourceIdentity, typeName string, err error) (HookAction, error) + ProvisionOutput(id HookResourceIdentity, typeName string, line string) // PreRefresh and PostRefresh are called before and after a single // resource state is refreshed, respectively. - PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) - PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) + PreRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (HookAction, error) + PostRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (HookAction, error) // PreImportState and PostImportState are called before and after - // (respectively) each state import operation for a given resource address. - PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) - PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (HookAction, error) + // (respectively) each state import operation for a given resource address when + // using the legacy import command. + PreImportState(id HookResourceIdentity, importID string) (HookAction, error) + PostImportState(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) - // PostStateUpdate is called each time the state is updated. It receives - // a deep copy of the state, which it may therefore access freely without - // any need for locks to protect from concurrent writes from the caller. + // PrePlanImport and PostPlanImport are called during a plan before and after planning to import + // a new resource using the configuration-driven import workflow. + PrePlanImport(id HookResourceIdentity, importTarget cty.Value) (HookAction, error) + PostPlanImport(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) + + // PreApplyImport and PostApplyImport are called during an apply for each imported resource when + // using the configuration-driven import workflow. + PreApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) + PostApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) + + // PreEphemeralOp and PostEphemeralOp are called during an operation on ephemeral resource + // such as opening, renewal or closing + PreEphemeralOp(id HookResourceIdentity, action plans.Action) (HookAction, error) + PostEphemeralOp(id HookResourceIdentity, action plans.Action, opErr error) (HookAction, error) + + // Stopping is called if an external signal requests that Terraform + // gracefully abort an operation in progress. + // + // This notification might suggest that the user wants Terraform to exit + // ASAP and in that case it's possible that if Terraform runs for too much + // longer then it'll get killed un-gracefully, and so this hook could be + // an opportunity to persist any transient data that would be lost under + // a subsequent kill signal. However, implementations must take care to do + // so in a way that won't cause corruption if the process _is_ killed while + // this hook is still running. + // + // This hook cannot control whether Terraform continues, because the + // graceful shutdown process is typically already running by the time this + // function is called. + Stopping() + + // PostStateUpdate is called each time the state is updated. The caller must + // coordinate a lock for the state if necessary, such that the Hook may + // access it freely without any need for additional locks to protect from + // concurrent writes. Implementations which modify or retain the state after + // the call has returned must copy the state. PostStateUpdate(new *states.State) (HookAction, error) } @@ -89,57 +134,85 @@ type NilHook struct{} var _ Hook = (*NilHook)(nil) -func (*NilHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (*NilHook) PreApply(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { +func (*NilHook) PostApply(id HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { +func (*NilHook) PreDiff(id HookResourceIdentity, dk addrs.DeposedKey, priorState, proposedNewState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (*NilHook) PostDiff(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (*NilHook) PreProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (*NilHook) PostProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { +func (*NilHook) PreProvisionInstanceStep(id HookResourceIdentity, typeName string) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { +func (*NilHook) PostProvisionInstanceStep(id HookResourceIdentity, typeName string, err error) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { +func (*NilHook) ProvisionOutput(id HookResourceIdentity, typeName string, line string) { } -func (*NilHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { +func (*NilHook) PreRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { +func (*NilHook) PostRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { +func (*NilHook) PreImportState(id HookResourceIdentity, importID string) (HookAction, error) { return HookActionContinue, nil } -func (*NilHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (HookAction, error) { +func (*NilHook) PostImportState(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { return HookActionContinue, nil } +func (h *NilHook) PrePlanImport(id HookResourceIdentity, importTarget cty.Value) (HookAction, error) { + return HookActionContinue, nil +} + +func (h *NilHook) PostPlanImport(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { + return HookActionContinue, nil +} + +func (h *NilHook) PreApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + return HookActionContinue, nil +} + +func (h *NilHook) PostApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + return HookActionContinue, nil +} + +func (h *NilHook) PreEphemeralOp(id HookResourceIdentity, action plans.Action) (HookAction, error) { + return HookActionContinue, nil +} + +func (h *NilHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action, opErr error) (HookAction, error) { + return HookActionContinue, nil +} + +func (*NilHook) Stopping() { + // Does nothing at all by default +} + func (*NilHook) PostStateUpdate(new *states.State) (HookAction, error) { return HookActionContinue, nil } diff --git a/internal/terraform/hook_mock.go b/internal/terraform/hook_mock.go index 0511a57805..b15ae345de 100644 --- a/internal/terraform/hook_mock.go +++ b/internal/terraform/hook_mock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -18,7 +21,7 @@ type MockHook struct { PreApplyCalled bool PreApplyAddr addrs.AbsResourceInstance - PreApplyGen states.Generation + PreApplyGen addrs.DeposedKey PreApplyAction plans.Action PreApplyPriorState cty.Value PreApplyPlannedState cty.Value @@ -27,16 +30,16 @@ type MockHook struct { PostApplyCalled bool PostApplyAddr addrs.AbsResourceInstance - PostApplyGen states.Generation + PostApplyGen addrs.DeposedKey PostApplyNewState cty.Value PostApplyError error PostApplyReturn HookAction PostApplyReturnError error - PostApplyFn func(addrs.AbsResourceInstance, states.Generation, cty.Value, error) (HookAction, error) + PostApplyFn func(addrs.AbsResourceInstance, addrs.DeposedKey, cty.Value, error) (HookAction, error) PreDiffCalled bool PreDiffAddr addrs.AbsResourceInstance - PreDiffGen states.Generation + PreDiffGen addrs.DeposedKey PreDiffPriorState cty.Value PreDiffProposedState cty.Value PreDiffReturn HookAction @@ -44,7 +47,7 @@ type MockHook struct { PostDiffCalled bool PostDiffAddr addrs.AbsResourceInstance - PostDiffGen states.Generation + PostDiffGen addrs.DeposedKey PostDiffAction plans.Action PostDiffPriorState cty.Value PostDiffPlannedState cty.Value @@ -83,14 +86,14 @@ type MockHook struct { PreRefreshCalled bool PreRefreshAddr addrs.AbsResourceInstance - PreRefreshGen states.Generation + PreRefreshGen addrs.DeposedKey PreRefreshPriorState cty.Value PreRefreshReturn HookAction PreRefreshError error PostRefreshCalled bool PostRefreshAddr addrs.AbsResourceInstance - PostRefreshGen states.Generation + PostRefreshGen addrs.DeposedKey PostRefreshPriorState cty.Value PostRefreshNewState cty.Value PostRefreshReturn HookAction @@ -108,6 +111,39 @@ type MockHook struct { PostImportStateReturn HookAction PostImportStateError error + PrePlanImportCalled bool + PrePlanImportAddr addrs.AbsResourceInstance + PrePlanImportReturn HookAction + PrePlanImportError error + + PostPlanImportAddr addrs.AbsResourceInstance + PostPlanImportCalled bool + PostPlanImportReturn HookAction + PostPlanImportError error + + PreApplyImportCalled bool + PreApplyImportAddr addrs.AbsResourceInstance + PreApplyImportReturn HookAction + PreApplyImportError error + + PostApplyImportCalled bool + PostApplyImportAddr addrs.AbsResourceInstance + PostApplyImportReturn HookAction + PostApplyImportError error + + PreEphemeralOpCalled bool + PreEphemeralOpAddr addrs.AbsResourceInstance + PreEphemeralOpReturn HookAction + PreEphemeralOpReturnError error + + PostEphemeralOpCalled bool + PostEphemeralOpAddr addrs.AbsResourceInstance + PostEphemeralOpError error + PostEphemeralOpReturn HookAction + PostEphemeralOpReturnError error + + StoppingCalled bool + PostStateUpdateCalled bool PostStateUpdateState *states.State PostStateUpdateReturn HookAction @@ -116,154 +152,207 @@ type MockHook struct { var _ Hook = (*MockHook)(nil) -func (h *MockHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *MockHook) PreApply(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreApplyCalled = true - h.PreApplyAddr = addr - h.PreApplyGen = gen + h.PreApplyAddr = id.Addr + h.PreApplyGen = dk h.PreApplyAction = action h.PreApplyPriorState = priorState h.PreApplyPlannedState = plannedNewState return h.PreApplyReturn, h.PreApplyError } -func (h *MockHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { +func (h *MockHook) PostApply(id HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (HookAction, error) { h.Lock() defer h.Unlock() h.PostApplyCalled = true - h.PostApplyAddr = addr - h.PostApplyGen = gen + h.PostApplyAddr = id.Addr + h.PostApplyGen = dk h.PostApplyNewState = newState h.PostApplyError = err if h.PostApplyFn != nil { - return h.PostApplyFn(addr, gen, newState, err) + return h.PostApplyFn(id.Addr, dk, newState, err) } return h.PostApplyReturn, h.PostApplyReturnError } -func (h *MockHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { +func (h *MockHook) PreDiff(id HookResourceIdentity, dk addrs.DeposedKey, priorState, proposedNewState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreDiffCalled = true - h.PreDiffAddr = addr - h.PreDiffGen = gen + h.PreDiffAddr = id.Addr + h.PreDiffGen = dk h.PreDiffPriorState = priorState h.PreDiffProposedState = proposedNewState return h.PreDiffReturn, h.PreDiffError } -func (h *MockHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *MockHook) PostDiff(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PostDiffCalled = true - h.PostDiffAddr = addr - h.PostDiffGen = gen + h.PostDiffAddr = id.Addr + h.PostDiffGen = dk h.PostDiffAction = action h.PostDiffPriorState = priorState h.PostDiffPlannedState = plannedNewState return h.PostDiffReturn, h.PostDiffError } -func (h *MockHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (h *MockHook) PreProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreProvisionInstanceCalled = true - h.PreProvisionInstanceAddr = addr + h.PreProvisionInstanceAddr = id.Addr h.PreProvisionInstanceState = state return h.PreProvisionInstanceReturn, h.PreProvisionInstanceError } -func (h *MockHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (h *MockHook) PostProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PostProvisionInstanceCalled = true - h.PostProvisionInstanceAddr = addr + h.PostProvisionInstanceAddr = id.Addr h.PostProvisionInstanceState = state return h.PostProvisionInstanceReturn, h.PostProvisionInstanceError } -func (h *MockHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { +func (h *MockHook) PreProvisionInstanceStep(id HookResourceIdentity, typeName string) (HookAction, error) { h.Lock() defer h.Unlock() h.PreProvisionInstanceStepCalled = true - h.PreProvisionInstanceStepAddr = addr + h.PreProvisionInstanceStepAddr = id.Addr h.PreProvisionInstanceStepProvisionerType = typeName return h.PreProvisionInstanceStepReturn, h.PreProvisionInstanceStepError } -func (h *MockHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { +func (h *MockHook) PostProvisionInstanceStep(id HookResourceIdentity, typeName string, err error) (HookAction, error) { h.Lock() defer h.Unlock() h.PostProvisionInstanceStepCalled = true - h.PostProvisionInstanceStepAddr = addr + h.PostProvisionInstanceStepAddr = id.Addr h.PostProvisionInstanceStepProvisionerType = typeName h.PostProvisionInstanceStepErrorArg = err return h.PostProvisionInstanceStepReturn, h.PostProvisionInstanceStepError } -func (h *MockHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { +func (h *MockHook) ProvisionOutput(id HookResourceIdentity, typeName string, line string) { h.Lock() defer h.Unlock() h.ProvisionOutputCalled = true - h.ProvisionOutputAddr = addr + h.ProvisionOutputAddr = id.Addr h.ProvisionOutputProvisionerType = typeName h.ProvisionOutputMessage = line } -func (h *MockHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { +func (h *MockHook) PreRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PreRefreshCalled = true - h.PreRefreshAddr = addr - h.PreRefreshGen = gen + h.PreRefreshAddr = id.Addr + h.PreRefreshGen = dk h.PreRefreshPriorState = priorState return h.PreRefreshReturn, h.PreRefreshError } -func (h *MockHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { +func (h *MockHook) PostRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (HookAction, error) { h.Lock() defer h.Unlock() h.PostRefreshCalled = true - h.PostRefreshAddr = addr + h.PostRefreshAddr = id.Addr h.PostRefreshPriorState = priorState h.PostRefreshNewState = newState return h.PostRefreshReturn, h.PostRefreshError } -func (h *MockHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { +func (h *MockHook) PreImportState(id HookResourceIdentity, importID string) (HookAction, error) { h.Lock() defer h.Unlock() h.PreImportStateCalled = true - h.PreImportStateAddr = addr + h.PreImportStateAddr = id.Addr h.PreImportStateID = importID return h.PreImportStateReturn, h.PreImportStateError } -func (h *MockHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (HookAction, error) { +func (h *MockHook) PostImportState(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { h.Lock() defer h.Unlock() h.PostImportStateCalled = true - h.PostImportStateAddr = addr + h.PostImportStateAddr = id.Addr h.PostImportStateNewStates = imported return h.PostImportStateReturn, h.PostImportStateError } +func (h *MockHook) PrePlanImport(id HookResourceIdentity, importTarget cty.Value) (HookAction, error) { + h.PrePlanImportCalled = true + h.PrePlanImportAddr = id.Addr + return h.PrePlanImportReturn, h.PrePlanImportError +} + +func (h *MockHook) PostPlanImport(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { + h.PostPlanImportCalled = true + h.PostPlanImportAddr = id.Addr + return h.PostPlanImportReturn, h.PostPlanImportError +} + +func (h *MockHook) PreApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + h.PreApplyImportCalled = true + h.PreApplyImportAddr = id.Addr + return h.PreApplyImportReturn, h.PreApplyImportError +} + +func (h *MockHook) PostApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + h.Lock() + defer h.Unlock() + + h.PostApplyImportCalled = true + h.PostApplyImportAddr = id.Addr + return h.PostApplyImportReturn, h.PostApplyImportError +} + +func (h *MockHook) PreEphemeralOp(id HookResourceIdentity, action plans.Action) (HookAction, error) { + h.Lock() + defer h.Unlock() + + h.PreEphemeralOpCalled = true + h.PreEphemeralOpAddr = id.Addr + return h.PreEphemeralOpReturn, h.PreEphemeralOpReturnError +} + +func (h *MockHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action, opErr error) (HookAction, error) { + h.Lock() + defer h.Unlock() + + h.PostEphemeralOpCalled = true + h.PostEphemeralOpAddr = id.Addr + h.PostEphemeralOpError = opErr + return h.PostEphemeralOpReturn, h.PostEphemeralOpReturnError +} + +func (h *MockHook) Stopping() { + h.Lock() + defer h.Unlock() + + h.StoppingCalled = true +} + func (h *MockHook) PostStateUpdate(new *states.State) (HookAction, error) { h.Lock() defer h.Unlock() diff --git a/internal/terraform/hook_stop.go b/internal/terraform/hook_stop.go index 2d4144e560..11a38aacbe 100644 --- a/internal/terraform/hook_stop.go +++ b/internal/terraform/hook_stop.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -20,57 +23,83 @@ type stopHook struct { var _ Hook = (*stopHook)(nil) -func (h *stopHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *stopHook) PreApply(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { +func (h *stopHook) PostApply(id HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (HookAction, error) { return h.hook() } -func (h *stopHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { +func (h *stopHook) PreDiff(id HookResourceIdentity, dk addrs.DeposedKey, priorState, proposedNewState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *stopHook) PostDiff(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (h *stopHook) PreProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (h *stopHook) PostProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { +func (h *stopHook) PreProvisionInstanceStep(id HookResourceIdentity, typeName string) (HookAction, error) { return h.hook() } -func (h *stopHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { +func (h *stopHook) PostProvisionInstanceStep(id HookResourceIdentity, typeName string, err error) (HookAction, error) { return h.hook() } -func (h *stopHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { +func (h *stopHook) ProvisionOutput(id HookResourceIdentity, typeName string, line string) { } -func (h *stopHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { +func (h *stopHook) PreRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { +func (h *stopHook) PostRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (HookAction, error) { return h.hook() } -func (h *stopHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { +func (h *stopHook) PreImportState(id HookResourceIdentity, importID string) (HookAction, error) { return h.hook() } -func (h *stopHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (HookAction, error) { +func (h *stopHook) PostImportState(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { return h.hook() } +func (h *stopHook) PrePlanImport(id HookResourceIdentity, importTarget cty.Value) (HookAction, error) { + return h.hook() +} + +func (h *stopHook) PostPlanImport(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { + return h.hook() +} + +func (h *stopHook) PreApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + return h.hook() +} + +func (h *stopHook) PostApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + return h.hook() +} + +func (h *stopHook) PreEphemeralOp(id HookResourceIdentity, action plans.Action) (HookAction, error) { + return h.hook() +} + +func (h *stopHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action, opErr error) (HookAction, error) { + return h.hook() +} + +func (h *stopHook) Stopping() {} + func (h *stopHook) PostStateUpdate(new *states.State) (HookAction, error) { return h.hook() } diff --git a/internal/terraform/hook_stop_test.go b/internal/terraform/hook_stop_test.go index 2c30231f96..a659ccd38d 100644 --- a/internal/terraform/hook_stop_test.go +++ b/internal/terraform/hook_stop_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/hook_test.go b/internal/terraform/hook_test.go index 0d5267dafa..d4230fcf44 100644 --- a/internal/terraform/hook_test.go +++ b/internal/terraform/hook_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -34,96 +37,144 @@ type testHookCall struct { InstanceID string } -func (h *testHook) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *testHook) PreApply(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PreApply", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PreApply", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostApply(addr addrs.AbsResourceInstance, gen states.Generation, newState cty.Value, err error) (HookAction, error) { +func (h *testHook) PostApply(id HookResourceIdentity, dk addrs.DeposedKey, newState cty.Value, err error) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PostApply", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PostApply", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreDiff(addr addrs.AbsResourceInstance, gen states.Generation, priorState, proposedNewState cty.Value) (HookAction, error) { +func (h *testHook) PreDiff(id HookResourceIdentity, dk addrs.DeposedKey, priorState, proposedNewState cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PreDiff", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PreDiff", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostDiff(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *testHook) PostDiff(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PostDiff", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PostDiff", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (h *testHook) PreProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstance", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstance", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostProvisionInstance(addr addrs.AbsResourceInstance, state cty.Value) (HookAction, error) { +func (h *testHook) PostProvisionInstance(id HookResourceIdentity, state cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstance", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstance", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string) (HookAction, error) { +func (h *testHook) PreProvisionInstanceStep(id HookResourceIdentity, typeName string) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstanceStep", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PreProvisionInstanceStep", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostProvisionInstanceStep(addr addrs.AbsResourceInstance, typeName string, err error) (HookAction, error) { +func (h *testHook) PostProvisionInstanceStep(id HookResourceIdentity, typeName string, err error) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstanceStep", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PostProvisionInstanceStep", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) ProvisionOutput(addr addrs.AbsResourceInstance, typeName string, line string) { +func (h *testHook) ProvisionOutput(id HookResourceIdentity, typeName string, line string) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"ProvisionOutput", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"ProvisionOutput", id.Addr.String()}) } -func (h *testHook) PreRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value) (HookAction, error) { +func (h *testHook) PreRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PreRefresh", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PreRefresh", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostRefresh(addr addrs.AbsResourceInstance, gen states.Generation, priorState cty.Value, newState cty.Value) (HookAction, error) { +func (h *testHook) PostRefresh(id HookResourceIdentity, dk addrs.DeposedKey, priorState cty.Value, newState cty.Value) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PostRefresh", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PostRefresh", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PreImportState(addr addrs.AbsResourceInstance, importID string) (HookAction, error) { +func (h *testHook) PreImportState(id HookResourceIdentity, importID string) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PreImportState", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PreImportState", id.Addr.String()}) return HookActionContinue, nil } -func (h *testHook) PostImportState(addr addrs.AbsResourceInstance, imported []providers.ImportedResource) (HookAction, error) { +func (h *testHook) PostImportState(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() - h.Calls = append(h.Calls, &testHookCall{"PostImportState", addr.String()}) + h.Calls = append(h.Calls, &testHookCall{"PostImportState", id.Addr.String()}) return HookActionContinue, nil } +func (h *testHook) PrePlanImport(id HookResourceIdentity, importTarget cty.Value) (HookAction, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"PrePlanImport", id.Addr.String()}) + return HookActionContinue, nil +} + +func (h *testHook) PostPlanImport(id HookResourceIdentity, imported []providers.ImportedResource) (HookAction, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"PostPlanImport", id.Addr.String()}) + return HookActionContinue, nil +} + +func (h *testHook) PreApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"PreApplyImport", id.Addr.String()}) + return HookActionContinue, nil +} + +func (h *testHook) PostApplyImport(id HookResourceIdentity, importing plans.ImportingSrc) (HookAction, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"PostApplyImport", id.Addr.String()}) + return HookActionContinue, nil +} + +func (h *testHook) PreEphemeralOp(id HookResourceIdentity, action plans.Action) (HookAction, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"PreEphemeralOp", id.Addr.String()}) + return HookActionContinue, nil +} + +func (h *testHook) PostEphemeralOp(id HookResourceIdentity, action plans.Action, err error) (HookAction, error) { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"PostEphemeralOp", id.Addr.String()}) + return HookActionContinue, nil +} + +func (h *testHook) Stopping() { + h.mu.Lock() + defer h.mu.Unlock() + h.Calls = append(h.Calls, &testHookCall{"Stopping", ""}) +} + func (h *testHook) PostStateUpdate(new *states.State) (HookAction, error) { h.mu.Lock() defer h.mu.Unlock() diff --git a/internal/terraform/instance_expanders.go b/internal/terraform/instance_expanders.go index b3733afb0a..c720464926 100644 --- a/internal/terraform/instance_expanders.go +++ b/internal/terraform/instance_expanders.go @@ -1,7 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform -// graphNodeExpandsInstances is implemented by nodes that causes instances to -// be registered in the instances.Expander. -type graphNodeExpandsInstances interface { - expandsInstances() +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/instances" +) + +// forEachModuleInstance is a helper to deal with the common need of doing +// some action for every dynamic module instance associated with a static +// module path. +// +// Many of our plan graph nodes represent configuration constructs that need +// to produce a dynamic subgraph based on the expansion of whatever module +// they are declared inside, and this helper deals with enumerating those +// dynamic addresses so that callers can just focus on building a graph node +// for each one and registering it in the subgraph. +// +// Both of the two callbacks will be called for each instance or set of +// unknown instances. knownCb receives fully-known instance addresses, +// while unknownCb receives partially-expanded addresses. Callers typically +// create a different graph node type in each callback, because +// partially-expanded prefixes conceptually represent an infinite set of +// possible module instance addresses and therefore need quite different +// treatment than a single concrete module instance address. +func forEachModuleInstance(insts *instances.Expander, modAddr addrs.Module, includeOverrides bool, knownCb func(addrs.ModuleInstance), unknownCb func(addrs.PartialExpandedModule)) { + for _, instAddr := range insts.ExpandModule(modAddr, includeOverrides) { + knownCb(instAddr) + } + for _, instsAddr := range insts.UnknownModuleInstances(modAddr, includeOverrides) { + unknownCb(instsAddr) + } } diff --git a/internal/terraform/marks.go b/internal/terraform/marks.go index 8e2a326072..65c32b98a1 100644 --- a/internal/terraform/marks.go +++ b/internal/terraform/marks.go @@ -1,39 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "fmt" - "sort" - + "github.com/hashicorp/terraform/internal/lang/marks" "github.com/zclconf/go-cty/cty" ) -// marksEqual compares 2 unordered sets of PathValue marks for equality, with -// the comparison using the cty.PathValueMarks.Equal method. -func marksEqual(a, b []cty.PathValueMarks) bool { - if len(a) == 0 && len(b) == 0 { - return true - } - - if len(a) != len(b) { - return false - } - - less := func(s []cty.PathValueMarks) func(i, j int) bool { - return func(i, j int) bool { - // the sort only needs to be consistent, so use the GoString format - // to get a comparable value - return fmt.Sprintf("%#v", s[i]) < fmt.Sprintf("%#v", s[j]) - } - } - - sort.Slice(a, less(a)) - sort.Slice(b, less(b)) - - for i := 0; i < len(a); i++ { - if !a[i].Equal(b[i]) { - return false - } - } - - return true +// valueMarksEqual compares the marks of 2 cty.Values for equality. +func valueMarksEqual(a, b cty.Value) bool { + _, aMarks := a.UnmarkDeepWithPaths() + _, bMarks := b.UnmarkDeepWithPaths() + return marks.MarksEqual(aMarks, bMarks) } diff --git a/internal/terraform/marks_test.go b/internal/terraform/marks_test.go deleted file mode 100644 index d3f4491877..0000000000 --- a/internal/terraform/marks_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package terraform - -import ( - "fmt" - "testing" - - "github.com/hashicorp/terraform/internal/lang/marks" - "github.com/zclconf/go-cty/cty" -) - -func TestMarksEqual(t *testing.T) { - for i, tc := range []struct { - a, b []cty.PathValueMarks - equal bool - }{ - { - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - true, - }, - { - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "A"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - false, - }, - { - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "b"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "c"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "b"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "c"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - true, - }, - { - []cty.PathValueMarks{ - cty.PathValueMarks{ - Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - cty.PathValueMarks{ - Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "c"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - []cty.PathValueMarks{ - cty.PathValueMarks{ - Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "c"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - cty.PathValueMarks{ - Path: cty.Path{cty.GetAttrStep{Name: "a"}, cty.GetAttrStep{Name: "b"}}, - Marks: cty.NewValueMarks(marks.Sensitive), - }, - }, - true, - }, - { - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "b"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - false, - }, - { - nil, - nil, - true, - }, - { - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - nil, - false, - }, - { - nil, - []cty.PathValueMarks{ - cty.PathValueMarks{Path: cty.Path{cty.GetAttrStep{Name: "a"}}, Marks: cty.NewValueMarks(marks.Sensitive)}, - }, - false, - }, - } { - t.Run(fmt.Sprint(i), func(t *testing.T) { - if marksEqual(tc.a, tc.b) != tc.equal { - t.Fatalf("marksEqual(\n%#v,\n%#v,\n) != %t\n", tc.a, tc.b, tc.equal) - } - }) - } -} diff --git a/internal/terraform/node_check.go b/internal/terraform/node_check.go new file mode 100644 index 0000000000..309bd495ab --- /dev/null +++ b/internal/terraform/node_check.go @@ -0,0 +1,207 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/hcl/v2/hclsyntax" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +var ( + _ GraphNodeModulePath = (*nodeReportCheck)(nil) + _ GraphNodeExecutable = (*nodeReportCheck)(nil) +) + +// nodeReportCheck calls the ReportCheckableObjects function for our assertions +// within the check blocks. +// +// We need this to happen before the checks are actually verified and before any +// nested data blocks, so the creator of this structure should make sure this +// node is a parent of any nested data blocks. +// +// This needs to be separate to nodeExpandCheck, because the actual checks +// should happen after referenced data blocks rather than before. +type nodeReportCheck struct { + addr addrs.ConfigCheck +} + +func (n *nodeReportCheck) ModulePath() addrs.Module { + return n.addr.Module +} + +func (n *nodeReportCheck) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + exp := ctx.InstanceExpander() + modInsts := exp.ExpandModule(n.ModulePath(), false) + + instAddrs := addrs.MakeSet[addrs.Checkable]() + for _, modAddr := range modInsts { + instAddrs.Add(n.addr.Check.Absolute(modAddr)) + } + ctx.Checks().ReportCheckableObjects(n.addr, instAddrs) + return nil +} + +func (n *nodeReportCheck) Name() string { + return n.addr.String() + " (report)" +} + +var ( + _ GraphNodeModulePath = (*nodeExpandCheck)(nil) + _ GraphNodeDynamicExpandable = (*nodeExpandCheck)(nil) + _ GraphNodeReferencer = (*nodeExpandCheck)(nil) +) + +// nodeExpandCheck creates child nodes that actually execute the assertions for +// a given check block. +// +// This must happen after any other nodes/resources/data sources that are +// referenced, so we implement GraphNodeReferencer. +// +// This needs to be separate to nodeReportCheck as nodeReportCheck must happen +// first, while nodeExpandCheck must execute after any referenced blocks. +type nodeExpandCheck struct { + addr addrs.ConfigCheck + config *configs.Check + + makeInstance func(addrs.AbsCheck, *configs.Check) dag.Vertex +} + +func (n *nodeExpandCheck) ModulePath() addrs.Module { + return n.addr.Module +} + +func (n *nodeExpandCheck) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { + exp := ctx.InstanceExpander() + + var g Graph + forEachModuleInstance(exp, n.ModulePath(), false, func(modAddr addrs.ModuleInstance) { + testAddr := n.addr.Check.Absolute(modAddr) + log.Printf("[TRACE] nodeExpandCheck: Node for %s", testAddr) + g.Add(n.makeInstance(testAddr, n.config)) + }, func(pem addrs.PartialExpandedModule) { + // TODO: Graph node to check the placeholder values for all possible module instances in this prefix. + testAddr := addrs.ObjectInPartialExpandedModule(pem, n.addr) + log.Printf("[WARN] nodeExpandCheck: not yet doing placeholder-check for all %s", testAddr) + }) + addRootNodeToGraph(&g) + + return &g, nil +} + +func (n *nodeExpandCheck) References() []*addrs.Reference { + var refs []*addrs.Reference + for _, assert := range n.config.Asserts { + // Check blocks reference anything referenced by conditions or messages + // in their check rules. + condition, _ := langrefs.ReferencesInExpr(addrs.ParseRef, assert.Condition) + message, _ := langrefs.ReferencesInExpr(addrs.ParseRef, assert.ErrorMessage) + refs = append(refs, condition...) + refs = append(refs, message...) + } + if n.config.DataResource != nil { + // We'll also always reference our nested data block if it exists, as + // there is nothing enforcing that it has to also be referenced by our + // conditions or messages. + // + // We don't need to make this addr absolute, because the check block and + // the data resource are always within the same module/instance. + traversal, _ := hclsyntax.ParseTraversalAbs( + []byte(n.config.DataResource.Addr().String()), + n.config.DataResource.DeclRange.Filename, + n.config.DataResource.DeclRange.Start) + ref, _ := addrs.ParseRef(traversal) + refs = append(refs, ref) + } + return refs +} + +func (n *nodeExpandCheck) Name() string { + return n.addr.String() + " (expand)" +} + +var ( + _ GraphNodeModuleInstance = (*nodeCheckAssert)(nil) + _ GraphNodeExecutable = (*nodeCheckAssert)(nil) +) + +type nodeCheckAssert struct { + addr addrs.AbsCheck + config *configs.Check + + // We only want to actually execute the checks during the plan and apply + // operations, but we still want to validate our config during + // other operations. + executeChecks bool +} + +func (n *nodeCheckAssert) ModulePath() addrs.Module { + return n.Path().Module() +} + +func (n *nodeCheckAssert) Path() addrs.ModuleInstance { + return n.addr.Module +} + +func (n *nodeCheckAssert) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + + // We only want to actually execute the checks during specific + // operations, such as plan and applies. + if n.executeChecks { + if status := ctx.Checks().ObjectCheckStatus(n.addr); status == checks.StatusFail || status == checks.StatusError { + // This check is already failing, so we won't try and evaluate it. + // This typically means there was an error in a data block within + // the check block. + return nil + } + + return evalCheckRules( + addrs.CheckAssertion, + n.config.Asserts, + ctx, + n.addr, + EvalDataForNoInstanceKey, + tfdiags.Warning) + + } + + // Otherwise let's still validate the config and references and return + // diagnostics if references do not exist etc. + var diags tfdiags.Diagnostics + for ix, assert := range n.config.Asserts { + _, _, moreDiags := validateCheckRule(addrs.NewCheckRule(n.addr, addrs.CheckAssertion, ix), assert, ctx, EvalDataForNoInstanceKey) + diags = diags.Append(moreDiags) + } + return diags +} + +func (n *nodeCheckAssert) Name() string { + return n.addr.String() + " (assertions)" +} + +var ( + _ GraphNodeExecutable = (*nodeCheckStart)(nil) +) + +// We need to ensure that any nested data sources execute after all other +// resource changes have been applied. This node acts as a single point of +// dependency that can enforce this ordering. +type nodeCheckStart struct{} + +func (n *nodeCheckStart) Execute(context EvalContext, operation walkOperation) tfdiags.Diagnostics { + // This node doesn't actually do anything, except simplify the underlying + // graph structure. + return nil +} + +func (n *nodeCheckStart) Name() string { + return "(execute checks)" +} diff --git a/internal/terraform/node_data_destroy.go b/internal/terraform/node_data_destroy.go index 0e81bb9c45..e68874a7c7 100644 --- a/internal/terraform/node_data_destroy.go +++ b/internal/terraform/node_data_destroy.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/node_data_destroy_test.go b/internal/terraform/node_data_destroy_test.go index f399ee4183..5097675dcd 100644 --- a/internal/terraform/node_data_destroy_test.go +++ b/internal/terraform/node_data_destroy_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/node_external_reference.go b/internal/terraform/node_external_reference.go new file mode 100644 index 0000000000..8050d15e3f --- /dev/null +++ b/internal/terraform/node_external_reference.go @@ -0,0 +1,50 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "sort" + "strings" + + "github.com/hashicorp/terraform/internal/addrs" +) + +// nodeExternalReference allows external callers (such as the testing framework) +// to provide the list of references they are making into the graph. This +// ensures that Terraform will not remove any nodes from the graph that might +// not be referenced from within a module but are referenced by the currently +// executing test file. +// +// This should only be added to the graph if we are executing the +// `terraform test` command. +type nodeExternalReference struct { + ExternalReferences []*addrs.Reference +} + +var ( + _ GraphNodeReferencer = (*nodeExternalReference)(nil) +) + +// GraphNodeModulePath +func (n *nodeExternalReference) ModulePath() addrs.Module { + // The external references are always made from test files, which currently + // execute as if they are in the root module. + return addrs.RootModule +} + +// GraphNodeReferencer +func (n *nodeExternalReference) References() []*addrs.Reference { + return n.ExternalReferences +} + +// Name implements dag.NamedVertex +func (n *nodeExternalReference) Name() string { + names := make([]string, len(n.ExternalReferences)) + for i, ref := range n.ExternalReferences { + names[i] = ref.DisplayString() + } + sort.Strings(names) + return fmt.Sprintf("", strings.Join(names, ", ")) +} diff --git a/internal/terraform/node_local.go b/internal/terraform/node_local.go index 79b4757682..dd25a98ab6 100644 --- a/internal/terraform/node_local.go +++ b/internal/terraform/node_local.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,12 +8,13 @@ import ( "log" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // nodeExpandLocal represents a named local value in a configuration module, @@ -26,11 +30,8 @@ var ( _ GraphNodeReferencer = (*nodeExpandLocal)(nil) _ GraphNodeDynamicExpandable = (*nodeExpandLocal)(nil) _ graphNodeTemporaryValue = (*nodeExpandLocal)(nil) - _ graphNodeExpandsInstances = (*nodeExpandLocal)(nil) ) -func (n *nodeExpandLocal) expandsInstances() {} - // graphNodeTemporaryValue func (n *nodeExpandLocal) temporaryValue() bool { return true @@ -58,21 +59,29 @@ func (n *nodeExpandLocal) ReferenceableAddrs() []addrs.Referenceable { // GraphNodeReferencer func (n *nodeExpandLocal) References() []*addrs.Reference { - refs, _ := lang.ReferencesInExpr(n.Config.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.Config.Expr) return refs } -func (n *nodeExpandLocal) DynamicExpand(ctx EvalContext) (*Graph, error) { +func (n *nodeExpandLocal) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { var g Graph expander := ctx.InstanceExpander() - for _, module := range expander.ExpandModule(n.Module) { + forEachModuleInstance(expander, n.Module, false, func(module addrs.ModuleInstance) { o := &NodeLocal{ Addr: n.Addr.Absolute(module), Config: n.Config, } log.Printf("[TRACE] Expanding local: adding %s as %T", o.Addr.String(), o) g.Add(o) - } + }, func(pem addrs.PartialExpandedModule) { + o := &nodeLocalInPartialModule{ + Addr: addrs.ObjectInPartialExpandedModule(pem, n.Addr), + Config: n.Config, + } + log.Printf("[TRACE] Expanding local: adding placeholder for all %s as %T", o.Addr.String(), o) + g.Add(o) + }) + addRootNodeToGraph(&g) return &g, nil } @@ -120,7 +129,7 @@ func (n *NodeLocal) ReferenceableAddrs() []addrs.Referenceable { // GraphNodeReferencer func (n *NodeLocal) References() []*addrs.Reference { - refs, _ := lang.ReferencesInExpr(n.Config.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.Config.Expr) return refs } @@ -129,41 +138,9 @@ func (n *NodeLocal) References() []*addrs.Reference { // expression for a local value and writes it into a transient part of // the state. func (n *NodeLocal) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { - expr := n.Config.Expr - addr := n.Addr.LocalValue - - // We ignore diags here because any problems we might find will be found - // again in EvaluateExpr below. - refs, _ := lang.ReferencesInExpr(expr) - for _, ref := range refs { - if ref.Subject == addr { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Self-referencing local value", - Detail: fmt.Sprintf("Local value %s cannot use its own result as part of its expression.", addr), - Subject: ref.SourceRange.ToHCL().Ptr(), - Context: expr.Range().Ptr(), - }) - } - } - if diags.HasErrors() { - return diags - } - - val, moreDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) - diags = diags.Append(moreDiags) - if moreDiags.HasErrors() { - return diags - } - - state := ctx.State() - if state == nil { - diags = diags.Append(fmt.Errorf("cannot write local value to nil state")) - return diags - } - - state.SetLocalValue(addr.Absolute(ctx.Path()), val) - + namedVals := ctx.NamedValues() + val, diags := evaluateLocalValue(n.Config, n.Addr.LocalValue, n.Addr.String(), ctx) + namedVals.SetLocalValue(n.Addr, val) return diags } @@ -177,3 +154,85 @@ func (n *NodeLocal) DotNode(name string, opts *dag.DotOpts) *dag.DotNode { }, } } + +// nodeLocalInPartialModule represents an infinite set of possible local value +// instances beneath a partially-expanded module instance prefix. +// +// Its job is to find a suitable placeholder value that approximates the +// values of all of those possible instances. Ideally that's a concrete +// known value if all instances would have the same value, an unknown value +// of a specific type if the definition produces a known type, or a +// totally-unknown value of unknown type in the worst case. +type nodeLocalInPartialModule struct { + Addr addrs.InPartialExpandedModule[addrs.LocalValue] + Config *configs.Local +} + +// Path implements [GraphNodePartialExpandedModule], meaning that the +// Execute method receives an [EvalContext] that's set up for partial-expanded +// evaluation instead of full evaluation. +func (n *nodeLocalInPartialModule) Path() addrs.PartialExpandedModule { + return n.Addr.Module +} + +func (n *nodeLocalInPartialModule) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + // Our job here is to make sure that the local value definition is + // valid for all instances of this local value across all of the possible + // module instances under our partially-expanded prefix, and to record + // a placeholder value that captures as precisely as possible what all + // of those results have in common. In the worst case where they have + // absolutely nothing in common cty.DynamicVal is the ultimate fallback, + // but we should try to do better when possible to give operators earlier + // feedback about any problems they would definitely encounter on a + // subsequent plan where the local values get evaluated concretely. + + namedVals := ctx.NamedValues() + val, diags := evaluateLocalValue(n.Config, n.Addr.Local, n.Addr.String(), ctx) + namedVals.SetLocalValuePlaceholder(n.Addr, val) + return diags +} + +// evaluateLocalValue is the common evaluation logic shared between +// [NodeLocal] and [nodeLocalInPartialModule]. +// +// The overall validation and evaluation process is the same for each, with +// the differences encapsulated inside the given [EvalContext], which is +// configured in a different way when doing partial-expanded evaluation. +// +// the addrStr argument should be the canonical string representation of the +// anbsolute address of the object being evaluated, which should either be an +// [addrs.AbsLocalValue] or an [addrs.InPartialEvaluatedModule[addrs.LocalValue]] +// depending on which of the two callers are calling this function. +// +// localAddr should match the local portion of the address that was stringified +// for addrStr, describing the local value relative to the module it's declared +// inside. +func evaluateLocalValue(config *configs.Local, localAddr addrs.LocalValue, addrStr string, ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + expr := config.Expr + + // We ignore diags here because any problems we might find will be found + // again in EvaluateExpr below. + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr) + for _, ref := range refs { + if ref.Subject == localAddr { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Self-referencing local value", + Detail: fmt.Sprintf("Local value %s cannot use its own result as part of its expression.", addrStr), + Subject: ref.SourceRange.ToHCL().Ptr(), + Context: expr.Range().Ptr(), + }) + } + } + if diags.HasErrors() { + return cty.DynamicVal, diags + } + + val, moreDiags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) + diags = diags.Append(moreDiags) + if val == cty.NilVal { + val = cty.DynamicVal + } + return val, diags +} diff --git a/internal/terraform/node_local_test.go b/internal/terraform/node_local_test.go index c79f05eabf..b3ed692cde 100644 --- a/internal/terraform/node_local_test.go +++ b/internal/terraform/node_local_test.go @@ -1,39 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "reflect" "testing" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/hcl2shim" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/states" ) func TestNodeLocalExecute(t *testing.T) { tests := []struct { Value string - Want interface{} + Want cty.Value Err bool }{ { "hello!", - "hello!", + cty.StringVal("hello!"), false, }, { "", - "", + cty.StringVal(""), false, }, { "Hello, ${local.foo}", - nil, + cty.DynamicVal, true, // self-referencing }, } @@ -45,16 +46,18 @@ func TestNodeLocalExecute(t *testing.T) { t.Fatal(diags.Error()) } + localAddr := addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance) n := &NodeLocal{ - Addr: addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + Addr: localAddr, Config: &configs.Local{ Expr: expr, }, } ctx := &MockEvalContext{ - StateState: states.NewState().SyncWrapper(), + StateState: states.NewState().SyncWrapper(), + NamedValuesState: namedvals.NewState(), - EvaluateExprResult: hcl2shim.HCL2ValueFromConfigValue(test.Want), + EvaluateExprResult: test.Want, } err := n.Execute(ctx, walkApply) @@ -66,18 +69,13 @@ func TestNodeLocalExecute(t *testing.T) { } } - ms := ctx.StateState.Module(addrs.RootModuleInstance) - gotLocals := ms.LocalValues - wantLocals := map[string]cty.Value{} - if test.Want != nil { - wantLocals["foo"] = hcl2shim.HCL2ValueFromConfigValue(test.Want) + if !ctx.NamedValues().HasLocalValue(localAddr) { + t.Fatalf("no value for %s", localAddr) } - - if !reflect.DeepEqual(gotLocals, wantLocals) { - t.Errorf( - "wrong locals after Eval\ngot: %swant: %s", - spew.Sdump(gotLocals), spew.Sdump(wantLocals), - ) + got := ctx.NamedValues().GetLocalValue(localAddr) + want := test.Want + if !want.RawEquals(got) { + t.Errorf("wrong value for %s\ngot: %#v\nwant: %#v", localAddr, got, want) } }) } diff --git a/internal/terraform/node_module_expand.go b/internal/terraform/node_module_expand.go index 49389ac654..4ab2e30282 100644 --- a/internal/terraform/node_module_expand.go +++ b/internal/terraform/node_module_expand.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,7 +9,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -23,13 +26,11 @@ type nodeExpandModule struct { var ( _ GraphNodeExecutable = (*nodeExpandModule)(nil) + _ GraphNodeReferenceable = (*nodeExpandModule)(nil) _ GraphNodeReferencer = (*nodeExpandModule)(nil) _ GraphNodeReferenceOutside = (*nodeExpandModule)(nil) - _ graphNodeExpandsInstances = (*nodeExpandModule)(nil) ) -func (n *nodeExpandModule) expandsInstances() {} - func (n *nodeExpandModule) Name() string { return n.Addr.String() + " (expand)" } @@ -61,16 +62,24 @@ func (n *nodeExpandModule) References() []*addrs.Reference { // child module instances we might expand to during our evaluation. if n.ModuleCall.Count != nil { - countRefs, _ := lang.ReferencesInExpr(n.ModuleCall.Count) + countRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.Count) refs = append(refs, countRefs...) } if n.ModuleCall.ForEach != nil { - forEachRefs, _ := lang.ReferencesInExpr(n.ModuleCall.ForEach) + forEachRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.ModuleCall.ForEach) refs = append(refs, forEachRefs...) } return refs } +func (n *nodeExpandModule) ReferenceableAddrs() []addrs.Referenceable { + // Anything referencing this module must do so after the ExpandModule call + // has been made to the expander, so we return the module call address as + // the only referenceable address. + _, call := n.Addr.Call() + return []addrs.Referenceable{call} +} + func (n *nodeExpandModule) DependsOn() []*addrs.Reference { if n.ModuleCall == nil { return nil @@ -95,35 +104,52 @@ func (n *nodeExpandModule) DependsOn() []*addrs.Reference { // GraphNodeReferenceOutside func (n *nodeExpandModule) ReferenceOutside() (selfPath, referencePath addrs.Module) { - return n.Addr, n.Addr.Parent() + return n.Addr.Parent(), n.Addr.Parent() } // GraphNodeExecutable -func (n *nodeExpandModule) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { - expander := ctx.InstanceExpander() +func (n *nodeExpandModule) Execute(globalCtx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { + expander := globalCtx.InstanceExpander() _, call := n.Addr.Call() + // Allowing unknown values in count and for_each is a top-level plan option. + // + // If this is false then the codepaths that handle unknown values below + // become unreachable, because the evaluate functions will reject unknown + // values as an error. + allowUnknown := globalCtx.Deferrals().DeferralAllowed() + // nodeExpandModule itself does not have visibility into how its ancestors // were expanded, so we use the expander here to provide all possible paths // to our module, and register module instances with each of them. - for _, module := range expander.ExpandModule(n.Addr.Parent()) { - ctx = ctx.WithPath(module) + for _, module := range expander.ExpandModule(n.Addr.Parent(), false) { + moduleCtx := evalContextForModuleInstance(globalCtx, module) + switch { case n.ModuleCall.Count != nil: - count, ctDiags := evaluateCountExpression(n.ModuleCall.Count, ctx) + count, ctDiags := evaluateCountExpression(n.ModuleCall.Count, moduleCtx, allowUnknown) diags = diags.Append(ctDiags) if diags.HasErrors() { return diags } - expander.SetModuleCount(module, call, count) + if count >= 0 { + expander.SetModuleCount(module, call, count) + } else { + // -1 represents "unknown" + expander.SetModuleCountUnknown(module, call) + } case n.ModuleCall.ForEach != nil: - forEach, feDiags := evaluateForEachExpression(n.ModuleCall.ForEach, ctx) + forEach, known, feDiags := evaluateForEachExpression(n.ModuleCall.ForEach, moduleCtx, allowUnknown) diags = diags.Append(feDiags) if diags.HasErrors() { return diags } - expander.SetModuleForEach(module, call, forEach) + if known { + expander.SetModuleForEach(module, call, forEach) + } else { + expander.SetModuleForEachUnknown(module, call) + } default: expander.SetModuleSingle(module, call) @@ -176,22 +202,23 @@ func (n *nodeCloseModule) Name() string { } func (n *nodeCloseModule) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { - if n.Addr.IsRoot() { - // If this is the root module, we are cleaning up the walk, so close - // any running provisioners - diags = diags.Append(ctx.CloseProvisioners()) + if !n.Addr.IsRoot() { + return } + // If this is the root module, we are cleaning up the walk, so close + // any running plugins + diags = diags.Append(ctx.ClosePlugins()) + + // We also close up the ephemeral resource manager + diags = diags.Append(ctx.EphemeralResources().Close(ctx.StopCtx())) + switch op { case walkApply, walkDestroy: state := ctx.State().Lock() defer ctx.State().Unlock() for modKey, mod := range state.Modules { - if !n.Addr.Equal(mod.Addr.Module()) { - continue - } - // clean out any empty resources for resKey, res := range mod.Resources { if len(res.Instances) == 0 { @@ -199,8 +226,18 @@ func (n *nodeCloseModule) Execute(ctx EvalContext, op walkOperation) (diags tfdi } } + // we don't ever remove a module that's been overridden - it will + // have outputs that have been set by the user and wouldn't be + // removed during normal operations as the module would have created + // resources. Overrides are only set during tests, and stop the + // module creating resources but we still care about the outputs. + overridden := false + if overrides := ctx.Overrides(); !overrides.Empty() { + _, overridden = overrides.GetModuleOverride(mod.Addr) + } + // empty child modules are always removed - if len(mod.Resources) == 0 && !mod.Addr.IsRoot() { + if len(mod.Resources) == 0 && !mod.Addr.IsRoot() && !overridden { delete(state.Modules, modKey) } } @@ -220,31 +257,31 @@ type nodeValidateModule struct { var _ GraphNodeExecutable = (*nodeValidateModule)(nil) // GraphNodeEvalable -func (n *nodeValidateModule) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { +func (n *nodeValidateModule) Execute(globalCtx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { _, call := n.Addr.Call() - expander := ctx.InstanceExpander() + expander := globalCtx.InstanceExpander() // Modules all evaluate to single instances during validation, only to // create a proper context within which to evaluate. All parent modules // will be a single instance, but still get our address in the expected // manner anyway to ensure they've been registered correctly. - for _, module := range expander.ExpandModule(n.Addr.Parent()) { - ctx = ctx.WithPath(module) + for _, module := range expander.ExpandModule(n.Addr.Parent(), false) { + moduleCtx := evalContextForModuleInstance(globalCtx, module) // Validate our for_each and count expressions at a basic level // We skip validation on known, because there will be unknown values before // a full expansion, presuming these errors will be caught in later steps switch { case n.ModuleCall.Count != nil: - _, countDiags := evaluateCountExpressionValue(n.ModuleCall.Count, ctx) + _, countDiags := evaluateCountExpressionValue(n.ModuleCall.Count, moduleCtx) diags = diags.Append(countDiags) case n.ModuleCall.ForEach != nil: - _, forEachDiags := evaluateForEachExpressionValue(n.ModuleCall.ForEach, ctx, true) + forEachDiags := newForEachEvaluator(n.ModuleCall.ForEach, moduleCtx, false).ValidateResourceValue() diags = diags.Append(forEachDiags) } - diags = diags.Append(validateDependsOn(ctx, n.ModuleCall.DependsOn)) + diags = diags.Append(validateDependsOn(moduleCtx, n.ModuleCall.DependsOn)) // now set our own mode to single expander.SetModuleSingle(module, call) diff --git a/internal/terraform/node_module_expand_test.go b/internal/terraform/node_module_expand_test.go index 42eb91a6dc..84252fb539 100644 --- a/internal/terraform/node_module_expand_test.go +++ b/internal/terraform/node_module_expand_test.go @@ -1,19 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "testing" "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/resources/ephemeral" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) func TestNodeExpandModuleExecute(t *testing.T) { ctx := &MockEvalContext{ - InstanceExpanderExpander: instances.NewExpander(), + InstanceExpanderExpander: instances.NewExpander(nil), } ctx.installSimpleEval() @@ -39,7 +44,8 @@ func TestNodeCloseModuleExecute(t *testing.T) { state := states.NewState() state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) ctx := &MockEvalContext{ - StateState: state.SyncWrapper(), + StateState: state.SyncWrapper(), + EphemeralResourcesResources: ephemeral.NewResources(), } node := nodeCloseModule{addrs.Module{"child"}} diags := node.Execute(ctx, walkApply) @@ -47,6 +53,18 @@ func TestNodeCloseModuleExecute(t *testing.T) { t.Fatalf("unexpected error: %s", diags.Err()) } + // Since module.child has no resources, it should be removed + if _, ok := state.Modules["module.child"]; !ok { + t.Fatal("module.child should not be removed from state yet") + } + + // the root module should do all the module cleanup + node = nodeCloseModule{addrs.RootModule} + diags = node.Execute(ctx, walkApply) + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err()) + } + // Since module.child has no resources, it should be removed if _, ok := state.Modules["module.child"]; ok { t.Fatal("module.child was not removed from state") @@ -58,7 +76,8 @@ func TestNodeCloseModuleExecute(t *testing.T) { state := states.NewState() state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey)) ctx := &MockEvalContext{ - StateState: state.SyncWrapper(), + StateState: state.SyncWrapper(), + EphemeralResourcesResources: ephemeral.NewResources(), } node := nodeCloseModule{addrs.Module{"child"}} @@ -75,7 +94,7 @@ func TestNodeCloseModuleExecute(t *testing.T) { func TestNodeValidateModuleExecute(t *testing.T) { t.Run("success", func(t *testing.T) { ctx := &MockEvalContext{ - InstanceExpanderExpander: instances.NewExpander(), + InstanceExpanderExpander: instances.NewExpander(nil), } ctx.installSimpleEval() node := nodeValidateModule{ @@ -95,7 +114,7 @@ func TestNodeValidateModuleExecute(t *testing.T) { t.Run("invalid count", func(t *testing.T) { ctx := &MockEvalContext{ - InstanceExpanderExpander: instances.NewExpander(), + InstanceExpanderExpander: instances.NewExpander(nil), } ctx.installSimpleEval() node := nodeValidateModule{ diff --git a/internal/terraform/node_module_variable.go b/internal/terraform/node_module_variable.go index c5e2294eaa..d8f9830cfe 100644 --- a/internal/terraform/node_module_variable.go +++ b/internal/terraform/node_module_variable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,13 +8,14 @@ import ( "log" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/instances" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // nodeExpandModuleVariable is the placeholder for an variable that has not yet had @@ -21,6 +25,14 @@ type nodeExpandModuleVariable struct { Module addrs.Module Config *configs.Variable Expr hcl.Expression + + // Planning must be set to true when building a planning graph, and must be + // false when building an apply graph. + Planning bool + + // DestroyApply must be set to true when planning or applying a destroy + // operation, and false otherwise. + DestroyApply bool } var ( @@ -29,27 +41,59 @@ var ( _ GraphNodeReferenceable = (*nodeExpandModuleVariable)(nil) _ GraphNodeReferencer = (*nodeExpandModuleVariable)(nil) _ graphNodeTemporaryValue = (*nodeExpandModuleVariable)(nil) - _ graphNodeExpandsInstances = (*nodeExpandModuleVariable)(nil) ) -func (n *nodeExpandModuleVariable) expandsInstances() {} - func (n *nodeExpandModuleVariable) temporaryValue() bool { return true } -func (n *nodeExpandModuleVariable) DynamicExpand(ctx EvalContext) (*Graph, error) { +func (n *nodeExpandModuleVariable) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { var g Graph + + // If this variable has preconditions, we need to report these checks now. + // + // We should only do this during planning as the apply phase starts with + // all the same checkable objects that were registered during the plan. + var checkableAddrs addrs.Set[addrs.Checkable] + if n.Planning { + if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(n.Module)) { + checkableAddrs = addrs.MakeSet[addrs.Checkable]() + } + } + expander := ctx.InstanceExpander() - for _, module := range expander.ExpandModule(n.Module) { + forEachModuleInstance(expander, n.Module, false, func(module addrs.ModuleInstance) { + addr := n.Addr.Absolute(module) + if checkableAddrs != nil { + log.Printf("[TRACE] nodeExpandModuleVariable: found checkable object %s", addr) + checkableAddrs.Add(addr) + } + o := &nodeModuleVariable{ - Addr: n.Addr.Absolute(module), + Addr: addr, Config: n.Config, Expr: n.Expr, ModuleInstance: module, + DestroyApply: n.DestroyApply, } g.Add(o) + }, func(pem addrs.PartialExpandedModule) { + addr := addrs.ObjectInPartialExpandedModule(pem, n.Addr) + o := &nodeModuleVariableInPartialModule{ + Addr: addr, + Config: n.Config, + Expr: n.Expr, + ModuleInstance: pem, + DestroyApply: n.DestroyApply, + } + g.Add(o) + }) + addRootNodeToGraph(&g) + + if checkableAddrs != nil { + ctx.Checks().ReportCheckableObjects(n.Addr.InModule(n.Module), checkableAddrs) } + return &g, nil } @@ -86,7 +130,7 @@ func (n *nodeExpandModuleVariable) References() []*addrs.Reference { // where our associated variable was declared, which is correct because // our value expression is assigned within a "module" block in the parent // module. - refs, _ := lang.ReferencesInExpr(n.Expr) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, n.Expr) return refs } @@ -100,6 +144,25 @@ func (n *nodeExpandModuleVariable) ReferenceableAddrs() []addrs.Referenceable { return []addrs.Referenceable{n.Addr} } +// variableValidationRules implements [graphNodeValidatableVariable]. +func (n *nodeExpandModuleVariable) variableValidationRules() (addrs.ConfigInputVariable, []*configs.CheckRule, hcl.Range) { + var defnRange hcl.Range + if n.Expr != nil { // should always be set in real calls, but not always in tests + defnRange = n.Expr.Range() + } + if n.DestroyApply { + // We don't perform any variable validation during the apply phase + // of a destroy, because validation rules typically aren't prepared + // for dealing with things already having been destroyed. + return n.Addr.InModule(n.Module), nil, defnRange + } + var rules []*configs.CheckRule + if n.Config != nil { // always in normal code, but sometimes not in unit tests + rules = n.Config.Validations + } + return n.Addr.InModule(n.Module), rules, defnRange +} + // nodeModuleVariable represents a module variable input during // the apply step. type nodeModuleVariable struct { @@ -109,6 +172,15 @@ type nodeModuleVariable struct { // ModuleInstance in order to create the appropriate context for evaluating // ModuleCallArguments, ex. so count.index and each.key can resolve ModuleInstance addrs.ModuleInstance + + // ModuleCallConfig is the module call that the expression in field Expr + // came from, which helps decide what [instances.RepetitionData] we should + // use when evaluating Expr. + ModuleCallConfig *configs.ModuleCall + + // DestroyApply must be set to true when applying a destroy operation and + // false otherwise. + DestroyApply bool } // Ensure that we are implementing all of the interfaces we think we are @@ -161,10 +233,12 @@ func (n *nodeModuleVariable) Execute(ctx EvalContext, op walkOperation) (diags t // Set values for arguments of a child module call, for later retrieval // during expression evaluation. - _, call := n.Addr.Module.CallInstance() - ctx.SetModuleCallArgument(call, n.Addr.Variable, val) + ctx.NamedValues().SetInputVariableValue(n.Addr, val) - return evalVariableValidations(n.Addr, n.Config, n.Expr, ctx) + // Custom validation rules are handled by a separate graph node of type + // nodeVariableValidation, added by variableValidationTransformer. + + return diags } // dag.GraphNodeDotter impl. @@ -181,12 +255,6 @@ func (n *nodeModuleVariable) DotNode(name string, opts *dag.DotOpts) *dag.DotNod // evalModuleVariable produces the value for a particular variable as will // be used by a child module instance. // -// The result is written into a map, with its key set to the local name of the -// variable, disregarding the module instance address. A map is returned instead -// of a single value as a result of trying to be convenient for use with -// EvalContext.SetModuleCallArguments, which expects a map to merge in with any -// existing arguments. -// // validateOnly indicates that this evaluation is only for config // validation, and we will not have any expansion module instance // repetition data. @@ -201,11 +269,10 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo case validateOnly: // the instance expander does not track unknown expansion values, so we // have to assume all RepetitionData is unknown. - moduleInstanceRepetitionData = instances.RepetitionData{ - CountIndex: cty.UnknownVal(cty.Number), - EachKey: cty.UnknownVal(cty.String), - EachValue: cty.DynamicVal, - } + // TODO: Ideally we should vary the placeholder we use here based + // on how the module call repetition was configured, but we don't + // have enough information here to decide that. + moduleInstanceRepetitionData = instances.TotallyUnknownRepetitionData default: // Get the repetition data for this module instance, @@ -213,7 +280,7 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo moduleInstanceRepetitionData = ctx.InstanceExpander().GetModuleInstanceRepetitionData(n.ModuleInstance) } - scope := ctx.EvaluationScope(nil, moduleInstanceRepetitionData) + scope := ctx.EvaluationScope(nil, nil, moduleInstanceRepetitionData) val, moreDiags := scope.EvalExpr(expr, cty.DynamicPseudoType) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { @@ -241,3 +308,59 @@ func (n *nodeModuleVariable) evalModuleVariable(ctx EvalContext, validateOnly bo return finalVal, diags.ErrWithWarnings() } + +// nodeModuleVariableInPartialModule represents an infinite set of possible +// input variable instances beneath a partially-expanded module instance prefix. +// +// Its job is to find a suitable placeholder value that approximates the +// values of all of those possible instances. Ideally that's a concrete +// known value if all instances would have the same value, an unknown value +// of a specific type if the definition produces a known type, or a +// totally-unknown value of unknown type in the worst case. +type nodeModuleVariableInPartialModule struct { + Addr addrs.InPartialExpandedModule[addrs.InputVariable] + Config *configs.Variable // Config is the var in the config + Expr hcl.Expression // Expr is the value expression given in the call + // ModuleInstance in order to create the appropriate context for evaluating + // ModuleCallArguments, ex. so count.index and each.key can resolve + ModuleInstance addrs.PartialExpandedModule + + // DestroyApply must be set to true when applying a destroy operation and + // false otherwise. + DestroyApply bool +} + +func (n *nodeModuleVariableInPartialModule) Path() addrs.PartialExpandedModule { + return n.Addr.Module +} + +func (n *nodeModuleVariableInPartialModule) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + // Our job here is to make sure that the input variable definition is + // valid for all instances of this input variable across all of the possible + // module instances under our partially-expanded prefix, and to record + // a placeholder value that captures as precisely as possible what all + // of those results have in common. In the worst case where they have + // absolutely nothing in common cty.DynamicVal is the ultimate fallback, + // but we should try to do better when possible to give operators earlier + // feedback about any problems they would definitely encounter on a + // subsequent plan where the input variables get evaluated concretely. + + namedVals := ctx.NamedValues() + + // TODO: Ideally we should vary the placeholder we use here based + // on how the module call repetition was configured, but we don't + // have enough information here to decide that. + moduleInstanceRepetitionData := instances.TotallyUnknownRepetitionData + + // NOTE WELL: Input variables are a little strange in that they announce + // themselves as belonging to the caller of the module they are declared + // in, because that's where their definition expressions get evaluated. + // Therefore this [EvalContext] is in the scope of the parent module, + // while n.Addr describes an object in the child module (where the + // variable declaration appeared). + scope := ctx.EvaluationScope(nil, nil, moduleInstanceRepetitionData) + val, diags := scope.EvalExpr(n.Expr, cty.DynamicPseudoType) + + namedVals.SetInputVariablePlaceholder(n.Addr, val) + return diags +} diff --git a/internal/terraform/node_module_variable_test.go b/internal/terraform/node_module_variable_test.go index e2b458cdbb..e313959876 100644 --- a/internal/terraform/node_module_variable_test.go +++ b/internal/terraform/node_module_variable_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/node_output.go b/internal/terraform/node_output.go index 5ce32c2445..46121f15c6 100644 --- a/internal/terraform/node_output.go +++ b/internal/terraform/node_output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -10,9 +13,12 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/namedvals" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -23,34 +29,45 @@ type nodeExpandOutput struct { Addr addrs.OutputValue Module addrs.Module Config *configs.Output - Destroy bool + Destroying bool RefreshOnly bool + + // Planning is set to true when this node is in a graph that was produced + // by the plan graph builder, as opposed to the apply graph builder. + // This quirk is just because we share the same node type between both + // phases but in practice there are a few small differences in the actions + // we need to take between plan and apply. See method DynamicExpand for + // details. + Planning bool + + // Overrides is the set of overrides applied by the testing framework. We + // may need to override the value for this output and if we do the value + // comes from here. + Overrides *mocking.Overrides + + Dependencies []addrs.ConfigResource } var ( - _ GraphNodeReferenceable = (*nodeExpandOutput)(nil) - _ GraphNodeReferencer = (*nodeExpandOutput)(nil) - _ GraphNodeReferenceOutside = (*nodeExpandOutput)(nil) - _ GraphNodeDynamicExpandable = (*nodeExpandOutput)(nil) - _ graphNodeTemporaryValue = (*nodeExpandOutput)(nil) - _ graphNodeExpandsInstances = (*nodeExpandOutput)(nil) + _ GraphNodeReferenceable = (*nodeExpandOutput)(nil) + _ GraphNodeReferencer = (*nodeExpandOutput)(nil) + _ GraphNodeReferenceOutside = (*nodeExpandOutput)(nil) + _ GraphNodeDynamicExpandable = (*nodeExpandOutput)(nil) + _ graphNodeTemporaryValue = (*nodeExpandOutput)(nil) + _ GraphNodeAttachDependencies = (*nodeExpandOutput)(nil) ) -func (n *nodeExpandOutput) expandsInstances() {} - func (n *nodeExpandOutput) temporaryValue() bool { // non root outputs are temporary return !n.Module.IsRoot() } -func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, error) { - if n.Destroy { - // if we're planning a destroy, we only need to handle the root outputs. - // The destroy plan doesn't evaluate any other config, so we can skip - // the rest of the outputs. - return n.planDestroyRootOutput(ctx) - } +// GraphNodeAttachDependencies +func (n *nodeExpandOutput) AttachDependencies(resources []addrs.ConfigResource) { + n.Dependencies = resources +} +func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { expander := ctx.InstanceExpander() changes := ctx.Changes() @@ -59,43 +76,85 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, error) { // wants to know the addresses of the checkable objects so that it can // treat them as unknown status if we encounter an error before actually // visiting the checks. + // + // We must do this only during planning, because the apply phase will start + // with all of the same checkable objects that were registered during the + // planning phase. Consumers of our JSON plan and state formats expect + // that the set of checkable objects will be consistent between the plan + // and any state snapshots created during apply, and that only the statuses + // of those objects will have changed. var checkableAddrs addrs.Set[addrs.Checkable] - if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(n.Module)) { - checkableAddrs = addrs.MakeSet[addrs.Checkable]() + if n.Planning { + if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(n.Module)) { + checkableAddrs = addrs.MakeSet[addrs.Checkable]() + } } var g Graph - for _, module := range expander.ExpandModule(n.Module) { - absAddr := n.Addr.Absolute(module) - if checkableAddrs != nil { - checkableAddrs.Add(absAddr) - } - - // Find any recorded change for this output - var change *plans.OutputChangeSrc - var outputChanges []*plans.OutputChangeSrc - if module.IsRoot() { - outputChanges = changes.GetRootOutputChanges() - } else { - parent, call := module.Call() - outputChanges = changes.GetOutputChanges(parent, call) - } - for _, c := range outputChanges { - if c.Addr.String() == absAddr.String() { - change = c - break + forEachModuleInstance( + expander, n.Module, true, + func(module addrs.ModuleInstance) { + absAddr := n.Addr.Absolute(module) + if checkableAddrs != nil { + checkableAddrs.Add(absAddr) } - } - o := &NodeApplyableOutput{ - Addr: absAddr, - Config: n.Config, - Change: change, - RefreshOnly: n.RefreshOnly, - } - log.Printf("[TRACE] Expanding output: adding %s as %T", o.Addr.String(), o) - g.Add(o) - } + // Find any recorded change for this output + var change *plans.OutputChange + var outputChanges []*plans.OutputChange + if module.IsRoot() { + outputChanges = changes.GetRootOutputChanges() + } else { + parent, call := module.Call() + outputChanges = changes.GetOutputChanges(parent, call) + } + for _, c := range outputChanges { + if c.Addr.String() == absAddr.String() { + change = c + break + } + } + + var node dag.Vertex + switch { + case module.IsRoot() && n.Destroying: + node = &NodeDestroyableOutput{ + Addr: absAddr, + Planning: n.Planning, + } + + default: + node = &NodeApplyableOutput{ + Addr: absAddr, + Config: n.Config, + Change: change, + RefreshOnly: n.RefreshOnly, + DestroyApply: n.Destroying, + Planning: n.Planning, + Override: n.getOverrideValue(absAddr.Module), + Dependencies: n.Dependencies, + } + } + + log.Printf("[TRACE] Expanding output: adding %s as %T", absAddr.String(), node) + g.Add(node) + }, + func(pem addrs.PartialExpandedModule) { + absAddr := addrs.ObjectInPartialExpandedModule(pem, n.Addr) + node := &nodeOutputInPartialModule{ + Addr: absAddr, + Config: n.Config, + RefreshOnly: n.RefreshOnly, + } + // We don't need to handle the module.IsRoot() && n.Destroying case + // seen in the fully-expanded case above, because the root module + // instance is always "fully expanded" (it's always a singleton) + // and so we can't get here for output values in the root module. + log.Printf("[TRACE] Expanding output: adding placeholder for all %s as %T", absAddr.String(), node) + g.Add(node) + }, + ) + addRootNodeToGraph(&g) if checkableAddrs != nil { checkState := ctx.Checks() @@ -105,27 +164,6 @@ func (n *nodeExpandOutput) DynamicExpand(ctx EvalContext) (*Graph, error) { return &g, nil } -// if we're planing a destroy operation, add a destroy node for any root output -func (n *nodeExpandOutput) planDestroyRootOutput(ctx EvalContext) (*Graph, error) { - if !n.Module.IsRoot() { - return nil, nil - } - state := ctx.State() - if state == nil { - return nil, nil - } - - var g Graph - o := &NodeDestroyableOutput{ - Addr: n.Addr.Absolute(addrs.RootModuleInstance), - Config: n.Config, - } - log.Printf("[TRACE] Expanding output: adding %s as %T", o.Addr.String(), o) - g.Add(o) - - return &g, nil -} - func (n *nodeExpandOutput) Name() string { path := n.Module.String() addr := n.Addr.String() + " (expand)" @@ -174,22 +212,74 @@ func (n *nodeExpandOutput) ReferenceOutside() (selfPath, referencePath addrs.Mod // GraphNodeReferencer func (n *nodeExpandOutput) References() []*addrs.Reference { - // root outputs might be destroyable, and may not reference anything in - // that case + // DestroyNodes do not reference anything. + if n.Module.IsRoot() && n.Destroying { + return nil + } + return referencesForOutput(n.Config) } +func (n *nodeExpandOutput) getOverrideValue(inst addrs.ModuleInstance) cty.Value { + // First check if we have any overrides at all, this is a shorthand for + // "are we running terraform test". + if n.Overrides.Empty() { + // cty.NilVal means no override + return cty.NilVal + } + + // We have overrides, let's see if we have one for this module instance. + if override, ok := n.Overrides.GetModuleOverride(inst); ok { + + output := n.Addr.Name + values := override.Values + + // The values.Type() should be an object type, but it might have + // been set to nil by a test or something. We can handle it in the + // same way as the attribute just not being specified. It's + // functionally the same for us and not something we need to raise + // alarms about. + if values.Type().IsObjectType() && values.Type().HasAttribute(output) { + return values.GetAttr(output) + } + + // If we don't have a value provided for an output, then we'll + // just set it to be null. + // + // TODO(liamcervante): Can we generate a value here? Probably + // not as we don't know the type. + return cty.NullVal(cty.DynamicPseudoType) + } + + // cty.NilVal indicates no override. + return cty.NilVal +} + // NodeApplyableOutput represents an output that is "applyable": // it is ready to be applied. type NodeApplyableOutput struct { Addr addrs.AbsOutputValue Config *configs.Output // Config is the output in the config // If this is being evaluated during apply, we may have a change recorded already - Change *plans.OutputChangeSrc + Change *plans.OutputChange // Refresh-only mode means that any failing output preconditions are // reported as warnings rather than errors RefreshOnly bool + + // DestroyApply indicates that we are applying a destroy plan, and do not + // need to account for conditional blocks. + DestroyApply bool + + Planning bool + + // Override provides the value to use for this output, if any. This can be + // set by testing framework when a module is overridden. + Override cty.Value + + // Dependencies is the full set of resources that are referenced by this + // output. + Dependencies []addrs.ConfigResource } var ( @@ -261,19 +351,21 @@ func (n *NodeApplyableOutput) ReferenceableAddrs() []addrs.Referenceable { } func referencesForOutput(c *configs.Output) []*addrs.Reference { - impRefs, _ := lang.ReferencesInExpr(c.Expr) - expRefs, _ := lang.References(c.DependsOn) - l := len(impRefs) + len(expRefs) - if l == 0 { - return nil - } - refs := make([]*addrs.Reference, 0, l) + var refs []*addrs.Reference + + impRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, c.Expr) + expRefs, _ := langrefs.References(addrs.ParseRef, c.DependsOn) + refs = append(refs, impRefs...) refs = append(refs, expRefs...) + for _, check := range c.Preconditions { - checkRefs, _ := lang.ReferencesInExpr(check.Condition) - refs = append(refs, checkRefs...) + condRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, check.Condition) + refs = append(refs, condRefs...) + errRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage) + refs = append(refs, errRefs...) } + return refs } @@ -296,57 +388,75 @@ func (n *NodeApplyableOutput) Execute(ctx EvalContext, op walkOperation) (diags // we we have a change recorded, we don't need to re-evaluate if the value // was known if changeRecorded { - change, err := n.Change.Decode() - diags = diags.Append(err) - if err == nil { - val = change.After - } + val = n.Change.After } - checkRuleSeverity := tfdiags.Error - if n.RefreshOnly { - checkRuleSeverity = tfdiags.Warning + if n.Addr.Module.IsRoot() && n.Config.Ephemeral { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral output not allowed", + Detail: "Ephemeral outputs are not allowed in context of a root module", + Subject: n.Config.DeclRange.Ptr(), + }) + return } - checkDiags := evalCheckRules( - addrs.OutputPrecondition, - n.Config.Preconditions, - ctx, n.Addr, EvalDataForNoInstanceKey, - checkRuleSeverity, - ) - diags = diags.Append(checkDiags) - if diags.HasErrors() { - return diags // failed preconditions prevent further evaluation + + // Checks are not evaluated during a destroy. The checks may fail, may not + // be valid, or may not have been registered at all. + // We also don't evaluate checks for overridden outputs. This is because + // any references within the checks will likely not have been created. + if !n.DestroyApply && n.Override == cty.NilVal { + checkRuleSeverity := tfdiags.Error + if n.RefreshOnly { + checkRuleSeverity = tfdiags.Warning + } + checkDiags := evalCheckRules( + addrs.OutputPrecondition, + n.Config.Preconditions, + ctx, n.Addr, EvalDataForNoInstanceKey, + checkRuleSeverity, + ) + diags = diags.Append(checkDiags) + if diags.HasErrors() { + return diags // failed preconditions prevent further evaluation + } } // If there was no change recorded, or the recorded change was not wholly // known, then we need to re-evaluate the output if !changeRecorded || !val.IsWhollyKnown() { - // This has to run before we have a state lock, since evaluation also - // reads the state - var evalDiags tfdiags.Diagnostics - val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil) - diags = diags.Append(evalDiags) - // We'll handle errors below, after we have loaded the module. - // Outputs don't have a separate mode for validation, so validate - // depends_on expressions here too - diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) + // First, we check if we have an overridden value. If we do, then we + // use that and we don't try and evaluate the underlying expression. + val = n.Override + if val == cty.NilVal { + // This has to run before we have a state lock, since evaluation also + // reads the state + var evalDiags tfdiags.Diagnostics + val, evalDiags = ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil) + diags = diags.Append(evalDiags) - // For root module outputs in particular, an output value must be - // statically declared as sensitive in order to dynamically return - // a sensitive result, to help avoid accidental exposure in the state - // of a sensitive value that the user doesn't want to include there. - if n.Addr.Module.IsRoot() { - if !n.Config.Sensitive && marks.Contains(val, marks.Sensitive) { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Output refers to sensitive values", - Detail: `To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data be explicitly marked as sensitive, to confirm your intent. + // We'll handle errors below, after we have loaded the module. + // Outputs don't have a separate mode for validation, so validate + // depends_on expressions here too + diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) + + // For root module outputs in particular, an output value must be + // statically declared as sensitive in order to dynamically return + // a sensitive result, to help avoid accidental exposure in the state + // of a sensitive value that the user doesn't want to include there. + if n.Addr.Module.IsRoot() { + if !n.Config.Sensitive && marks.Contains(val, marks.Sensitive) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Output refers to sensitive values", + Detail: `To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data be explicitly marked as sensitive, to confirm your intent. If you do intend to export this data, annotate the output value as sensitive by adding the following argument: sensitive = true`, - Subject: n.Config.DeclRange.Ptr(), - }) + Subject: n.Config.DeclRange.Ptr(), + }) + } } } } @@ -358,7 +468,7 @@ If you do intend to export this data, annotate the output value as sensitive by // if we're continuing, make sure the output is included, and // marked as unknown. If the evaluator was able to find a type // for the value in spite of the error then we'll use it. - n.setValue(state, changes, cty.UnknownVal(val.Type())) + n.setValue(ctx.NamedValues(), state, changes, ctx.Deferrals(), cty.UnknownVal(val.Type())) // Keep existing warnings, while converting errors to warnings. // This is not meant to be the normal path, so there no need to @@ -378,12 +488,39 @@ If you do intend to export this data, annotate the output value as sensitive by } return diags } - n.setValue(state, changes, val) + + // The checks below this point are intentionally not opted out by + // "flagWarnOutputErrors", because they relate to features that were added + // more recently than the historical change to treat invalid output values + // as errors rather than warnings. + if n.Config.Ephemeral && !marks.Has(val, marks.Ephemeral) { + // An ephemeral output value must always be ephemeral + // This is to prevent accidental persistence upstream + // from here. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Value not allowed in ephemeral output", + Detail: "This output value is declared as returning an ephemeral value, so it can only be set to an ephemeral value.", + Subject: n.Config.Expr.Range().Ptr(), + }) + return diags + } else if !n.Config.Ephemeral && marks.Contains(val, marks.Ephemeral) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Ephemeral value not allowed", + Detail: "This output value is not declared as returning an ephemeral value, so it cannot be set to a result derived from an ephemeral value.", + Subject: n.Config.Expr.Range().Ptr(), + }) + return diags + } + + n.setValue(ctx.NamedValues(), state, changes, ctx.Deferrals(), val) // If we were able to evaluate a new value, we can update that in the // refreshed state as well. if state = ctx.RefreshState(); state != nil && val.IsWhollyKnown() { - n.setValue(state, changes, val) + // we only need to update the state, do not pass in the changes again + n.setValue(nil, state, nil, ctx.Deferrals(), val) } return diags @@ -400,11 +537,66 @@ func (n *NodeApplyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.DotNo } } +// nodeOutputInPartialModule represents an infinite set of possible output value +// instances beneath a partially-expanded module instance prefix. +// +// Its job is to find a suitable placeholder value that approximates the +// values of all of those possible instances. Ideally that's a concrete +// known value if all instances would have the same value, an unknown value +// of a specific type if the definition produces a known type, or a +// totally-unknown value of unknown type in the worst case. +type nodeOutputInPartialModule struct { + Addr addrs.InPartialExpandedModule[addrs.OutputValue] + Config *configs.Output + + // Refresh-only mode means that any failing output preconditions are + // reported as warnings rather than errors + RefreshOnly bool +} + +// Path implements [GraphNodePartialExpandedModule], meaning that the +// Execute method receives an [EvalContext] that's set up for partial-expanded +// evaluation instead of full evaluation. +func (n *nodeOutputInPartialModule) Path() addrs.PartialExpandedModule { + return n.Addr.Module +} + +func (n *nodeOutputInPartialModule) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + // Our job here is to make sure that the output value definition is + // valid for all instances of this output value across all of the possible + // module instances under our partially-expanded prefix, and to record + // a placeholder value that captures as precisely as possible what all + // of those results have in common. In the worst case where they have + // absolutely nothing in common cty.DynamicVal is the ultimate fallback, + // but we should try to do better when possible to give operators earlier + // feedback about any problems they would definitely encounter on a + // subsequent plan where the output values get evaluated concretely. + + namedVals := ctx.NamedValues() + + // this "ctx" is preconfigured to evaluate in terms of other placeholder + // values generated in the same unexpanded module prefix, rather than + // from the active state/plan, so this result is likely to be derived + // from unknown value placeholders itself. + val, diags := ctx.EvaluateExpr(n.Config.Expr, cty.DynamicPseudoType, nil) + if val == cty.NilVal { + val = cty.DynamicVal + } + + // We'll also check that the depends_on argument is valid, since that's + // a static concern anyway and so cannot vary between instances of the + // same module. + diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) + + namedVals.SetOutputValuePlaceholder(n.Addr, val) + return diags +} + // NodeDestroyableOutput represents an output that is "destroyable": // its application will remove the output from the state. type NodeDestroyableOutput struct { - Addr addrs.AbsOutputValue - Config *configs.Output // Config is the output in the config + Addr addrs.AbsOutputValue + Planning bool } var ( @@ -439,19 +631,22 @@ func (n *NodeDestroyableOutput) Execute(ctx EvalContext, op walkOperation) tfdia before := cty.NullVal(cty.DynamicPseudoType) mod := state.Module(n.Addr.Module) if n.Addr.Module.IsRoot() && mod != nil { - if o, ok := mod.OutputValues[n.Addr.OutputValue.Name]; ok { + s := state.Lock() + rootOutputs := s.RootOutputValues + if o, ok := rootOutputs[n.Addr.OutputValue.Name]; ok { sensitiveBefore = o.Sensitive before = o.Value } else { // If the output was not in state, a delete change would // be meaningless, so exit early. + state.Unlock() return nil - } + state.Unlock() } changes := ctx.Changes() - if changes != nil { + if changes != nil && n.Planning { change := &plans.OutputChange{ Addr: n.Addr, Sensitive: sensitiveBefore, @@ -462,17 +657,11 @@ func (n *NodeDestroyableOutput) Execute(ctx EvalContext, op walkOperation) tfdia }, } - cs, err := change.Encode() - if err != nil { - // Should never happen, since we just constructed this right above - panic(fmt.Sprintf("planned change for %s could not be encoded: %s", n.Addr, err)) - } - log.Printf("[TRACE] NodeDestroyableOutput: Saving %s change for %s in changeset", change.Action, n.Addr) changes.RemoveOutputChange(n.Addr) // remove any existing planned change, if present - changes.AppendOutputChange(cs) // add the new planned change + changes.AppendOutputChange(change) // add the new planned change } - state.RemoveOutputValue(n.Addr) + return nil } @@ -487,12 +676,8 @@ func (n *NodeDestroyableOutput) DotNode(name string, opts *dag.DotOpts) *dag.Dot } } -func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.ChangesSync, val cty.Value) { - // If we have an active changeset then we'll first replicate the value in - // there and lookup the prior value in the state. This is used in - // preference to the state where present, since it *is* able to represent - // unknowns, while the state cannot. - if changes != nil { +func (n *NodeApplyableOutput) setValue(namedVals *namedvals.State, state *states.SyncState, changes *plans.ChangesSync, deferred *deferring.Deferred, val cty.Value) { + if changes != nil && n.Planning { // if this is a root module, try to get a before value from the state for // the diff sensitiveBefore := false @@ -503,7 +688,9 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C mod := state.Module(n.Addr.Module) if n.Addr.Module.IsRoot() && mod != nil { - for name, o := range mod.OutputValues { + s := state.Lock() + rootOutputs := s.RootOutputValues + for name, o := range rootOutputs { if name == n.Addr.OutputValue.Name { before = o.Value sensitiveBefore = o.Sensitive @@ -511,10 +698,11 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C break } } + state.Unlock() } - // We will not show the value is either the before or after are marked - // as sensitivity. We can show the value again once sensitivity is + // We will not show the value if either the before or after are marked + // as sensitive. We can show the value again once sensitivity is // removed from both the config and the state. sensitiveChange := sensitiveBefore || n.Config.Sensitive @@ -542,37 +730,70 @@ func (n *NodeApplyableOutput) setValue(state *states.SyncState, changes *plans.C action = plans.NoOp } - change := &plans.OutputChange{ - Addr: n.Addr, - Sensitive: sensitiveChange, - Change: plans.Change{ - Action: action, - Before: before, - After: val, - }, - } + // Non-ephemeral output values get their changes recorded in the plan + if !n.Config.Ephemeral { + change := &plans.OutputChange{ + Addr: n.Addr, + Sensitive: sensitiveChange, + Change: plans.Change{ + Action: action, + Before: before, + After: val, + }, + } - cs, err := change.Encode() - if err != nil { - // Should never happen, since we just constructed this right above - panic(fmt.Sprintf("planned change for %s could not be encoded: %s", n.Addr, err)) + log.Printf("[TRACE] setValue: Saving %s change for %s in changeset", change.Action, n.Addr) + changes.AppendOutputChange(change) // add the new planned change } - log.Printf("[TRACE] setValue: Saving %s change for %s in changeset", change.Action, n.Addr) - changes.RemoveOutputChange(n.Addr) // remove any existing planned change, if present - changes.AppendOutputChange(cs) // add the new planned change } - if val.IsKnown() && !val.IsNull() { - // The state itself doesn't represent unknown values, so we null them - // out here and then we'll save the real unknown value in the planned - // changeset below, if we have one on this graph walk. - log.Printf("[TRACE] setValue: Saving value for %s in state", n.Addr) - unmarkedVal, _ := val.UnmarkDeep() - stateVal := cty.UnknownAsNull(unmarkedVal) - state.SetOutputValue(n.Addr, stateVal, n.Config.Sensitive) - } else { + if changes != nil && !n.Planning { + // During apply there is no longer any change to track, so we must + // ensure the state is updated and not overridden by a change. + changes.RemoveOutputChange(n.Addr) + } + + // Null outputs must be saved for modules so that they can still be + // evaluated. Null root outputs are removed entirely, which is always fine + // because they can't be referenced by anything else in the configuration. + if n.Addr.Module.IsRoot() && val.IsNull() { log.Printf("[TRACE] setValue: Removing %s from state (it is now null)", n.Addr) state.RemoveOutputValue(n.Addr) + return } + // caller leaves namedVals nil if they've already called this function + // with a different state, since we only have one namedVals regardless + // of how many states are involved in an operation. + if namedVals != nil { + saveVal := val + if n.Config.Ephemeral { + // Downstream uses of this output value must propagate the + // ephemerality. + saveVal = saveVal.Mark(marks.Ephemeral) + } + namedVals.SetOutputValue(n.Addr, saveVal) + } + + // Non-ephemeral output values get saved in the state too + if !n.Config.Ephemeral { + // The state itself doesn't represent unknown values, so we null them + // out here and then we'll save the real unknown value in the planned + // changeset, if we have one on this graph walk. + log.Printf("[TRACE] setValue: Saving value for %s in state", n.Addr) + // non-root outputs need to keep sensitive marks for evaluation, but are + // not serialized. + if n.Addr.Module.IsRoot() { + val, _ = val.UnmarkDeep() + if deferred.DependenciesDeferred(n.Dependencies) { + // If the output is from deferred resources then we return a + // simple null value representing that the value is really + // unknown as the dependencies were not properly computed. + val = cty.NullVal(val.Type()) + } else { + val = cty.UnknownAsNull(val) + } + } + } + state.SetOutputValue(n.Addr, val, n.Config.Sensitive) } diff --git a/internal/terraform/node_output_test.go b/internal/terraform/node_output_test.go index 80d60539e8..c5e30f2c07 100644 --- a/internal/terraform/node_output_test.go +++ b/internal/terraform/node_output_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -11,6 +14,7 @@ import ( "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/states" ) @@ -19,6 +23,7 @@ func TestNodeApplyableOutputExecute_knownValue(t *testing.T) { ctx.StateState = states.NewState().SyncWrapper() ctx.RefreshStateState = states.NewState().SyncWrapper() ctx.ChecksState = checks.NewState(nil) + ctx.DeferralsState = deferring.NewDeferred(false) config := &configs.Output{Name: "map-output"} addr := addrs.OutputValue{Name: config.Name}.Absolute(addrs.RootModuleInstance) @@ -121,6 +126,7 @@ func TestNodeApplyableOutputExecute_sensitiveValueAndOutput(t *testing.T) { ctx := new(MockEvalContext) ctx.StateState = states.NewState().SyncWrapper() ctx.ChecksState = checks.NewState(nil) + ctx.DeferralsState = deferring.NewDeferred(false) config := &configs.Output{ Name: "map-output", @@ -150,7 +156,10 @@ func TestNodeDestroyableOutputExecute(t *testing.T) { outputAddr := addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance) state := states.NewState() - state.Module(addrs.RootModuleInstance).SetOutputValue("foo", cty.StringVal("bar"), false) + state.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("bar"), false, + ) state.OutputValue(outputAddr) ctx := &MockEvalContext{ diff --git a/internal/terraform/node_overridable.go b/internal/terraform/node_overridable.go new file mode 100644 index 0000000000..de8da889fb --- /dev/null +++ b/internal/terraform/node_overridable.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +// GraphNodeOverridable represents a node in the graph that can be overridden +// by the testing framework. +type GraphNodeOverridable interface { + GraphNodeResourceInstance + + ConfigProvider() addrs.AbsProviderConfig + SetOverride(override *configs.Override) +} diff --git a/internal/terraform/node_provider.go b/internal/terraform/node_provider.go index c5b09136cb..85f5b8eda6 100644 --- a/internal/terraform/node_provider.go +++ b/internal/terraform/node_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,10 +8,11 @@ import ( "log" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // NodeApplyableProvider represents a provider during an apply. @@ -22,7 +26,7 @@ var ( // GraphNodeExecutable func (n *NodeApplyableProvider) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { - _, err := ctx.InitProvider(n.Addr) + _, err := ctx.InitProvider(n.Addr, n.Config) diags = diags.Append(err) if diags.HasErrors() { return diags @@ -65,7 +69,7 @@ func (n *NodeApplyableProvider) ValidateProvider(ctx EvalContext, provider provi return diags } - configSchema := schemaResp.Provider.Block + configSchema := schemaResp.Provider.Body if configSchema == nil { // Should never happen in real code, but often comes up in tests where // mock schemas are being used that tend to be incomplete. @@ -107,10 +111,20 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov return diags } - configSchema := resp.Provider.Block + configSchema := resp.Provider.Body configVal, configBody, evalDiags := ctx.EvaluateBlock(configBody, configSchema, nil, EvalDataForNoInstanceKey) diags = diags.Append(evalDiags) if evalDiags.HasErrors() { + if config == nil { + // The error messages from the above evaluation will be confusing + // if there isn't an explicit "provider" block in the configuration. + // Add some detail to the error message in this case. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid provider configuration", + fmt.Sprintf(providerConfigErr, n.Addr.Provider), + )) + } return diags } @@ -175,5 +189,39 @@ func (n *NodeApplyableProvider) ConfigureProvider(ctx EvalContext, provider prov return diags } +// nodeExternalProvider is used instead of [NodeApplyableProvider] when an +// already-configured provider instance has been provided by an external caller, +// and therefore we don't need to do anything to get the provider ready to +// use. +type nodeExternalProvider struct { + *NodeAbstractProvider +} + +var ( + _ GraphNodeExecutable = (*nodeExternalProvider)(nil) +) + +// Execute implements GraphNodeExecutable. +func (n *nodeExternalProvider) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + log.Printf("[TRACE] nodeExternalProvider: using externally-configured instance for %s", n.Addr) + var diags tfdiags.Diagnostics + + // Due to how the EvalContext provider cache works, we need to just poke + // this method with our provider address so that a subsequent call + // to ctx.Provider will return it successfully. + // In this case the "config" argument is always ignored, so we leave it + // set to nil to represent that. + _, err := ctx.InitProvider(n.Addr, nil) + if err != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to initialize externally-configured provider", + fmt.Sprintf("Despite it having been pre-initialized by an external caller, %s somehow failed to initialize. This is a bug in Terraform.", n.Addr), + )) + } + + return diags +} + const providerConfigErr = `Provider %q requires explicit configuration. Add a provider block to the root module and configure the provider's required arguments as described in the provider documentation. ` diff --git a/internal/terraform/node_provider_abstract.go b/internal/terraform/node_provider_abstract.go index 09bdd95b40..ceeffa0544 100644 --- a/internal/terraform/node_provider_abstract.go +++ b/internal/terraform/node_provider_abstract.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/node_provider_eval.go b/internal/terraform/node_provider_eval.go index fba47ddb96..87dd299ede 100644 --- a/internal/terraform/node_provider_eval.go +++ b/internal/terraform/node_provider_eval.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "github.com/hashicorp/terraform/internal/tfdiags" @@ -14,6 +17,6 @@ var _ GraphNodeExecutable = (*NodeEvalableProvider)(nil) // GraphNodeExecutable func (n *NodeEvalableProvider) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { - _, err := ctx.InitProvider(n.Addr) + _, err := ctx.InitProvider(n.Addr, n.Config) return diags.Append(err) } diff --git a/internal/terraform/node_provider_test.go b/internal/terraform/node_provider_test.go index fe8a80d11b..921d0b2166 100644 --- a/internal/terraform/node_provider_test.go +++ b/internal/terraform/node_provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,13 +9,15 @@ import ( "testing" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) func TestNodeApplyableProviderExecute(t *testing.T) { @@ -346,6 +351,19 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { } return } + + // requiredProvider matches provider, but its attributes are required + // explicitly. This means we can simulate an earlier failure in the + // config validation. + requiredProvider := mockProviderWithConfigSchema(&configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "region": { + Type: cty.String, + Required: true, + }, + }, + }) + ctx := &MockEvalContext{ProviderProvider: provider} ctx.installSimpleEval() @@ -407,6 +425,43 @@ func TestNodeApplyableProvider_ConfigProvider(t *testing.T) { } }) + t.Run("missing schema-required config (no config at all)", func(t *testing.T) { + node := NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + }, + } + + diags := node.ConfigureProvider(ctx, requiredProvider, false) + if !diags.HasErrors() { + t.Fatal("missing expected error with nil config") + } + if !strings.Contains(diags.Err().Error(), "requires explicit configuration") { + t.Errorf("diagnostic is missing \"requires explicit configuration\" message: %s", diags.Err()) + } + }) + + t.Run("missing schema-required config", func(t *testing.T) { + config := &configs.Provider{ + Name: "test", + Config: hcl.EmptyBody(), + } + node := NodeApplyableProvider{ + NodeAbstractProvider: &NodeAbstractProvider{ + Addr: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + Config: config, + }, + } + + diags := node.ConfigureProvider(ctx, requiredProvider, false) + if !diags.HasErrors() { + t.Fatal("missing expected error with invalid config") + } + if !strings.Contains(diags.Err().Error(), "The argument \"region\" is required, but was not set.") { + t.Errorf("wrong diagnostic: %s", diags.Err()) + } + }) + } // This test is similar to TestNodeApplyableProvider_ConfigProvider, but tests responses from the providers.ConfigureProviderRequest @@ -498,7 +553,7 @@ func TestNodeApplyableProvider_ConfigProvider_config_fn_err(t *testing.T) { } func TestGetSchemaError(t *testing.T) { - provider := &MockProvider{ + provider := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Diagnostics: tfdiags.Diagnostics.Append(nil, tfdiags.WholeContainingBody(tfdiags.Error, "oops", "error")), }, diff --git a/internal/terraform/node_resource_abstract.go b/internal/terraform/node_resource_abstract.go index af9583a28d..84f977486f 100644 --- a/internal/terraform/node_resource_abstract.go +++ b/internal/terraform/node_resource_abstract.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -8,7 +11,8 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -50,9 +54,12 @@ type NodeAbstractResource struct { // interfaces if you're running those transforms, but also be explicitly // set if you already have that information. - Schema *configschema.Block // Schema for processing the configuration body - SchemaVersion uint64 // Schema version of "Schema", as decided by the provider - Config *configs.Resource // Config is the resource in the config + Schema *providers.Schema // Schema for processing the configuration body + + // Config and RemovedConfig are mutally-exclusive, because a + // resource can't be both declared and removed at the same time. + Config *configs.Resource // Config is the resource in the config, if any + RemovedConfig *configs.Removed // RemovedConfig is the "removed" block for this resource, if any // ProviderMetas is the provider_meta configs for the module this resource belongs to ProviderMetas map[addrs.Provider]*configs.ProviderMeta @@ -63,19 +70,29 @@ type NodeAbstractResource struct { Targets []addrs.Targetable // Set from AttachDataResourceDependsOn - dependsOn []addrs.ConfigResource - forceDependsOn bool + dependsOn []addrs.ConfigResource // The address of the provider this resource will use ResolvedProvider addrs.AbsProviderConfig + // storedProviderConfig is the provider address retrieved from the + // state. This is defined here for access within the ProvidedBy method, but + // will be set from the embedding instance type when the state is attached. + storedProviderConfig addrs.AbsProviderConfig // This resource may expand into instances which need to be imported. importTargets []*ImportTarget + + // generateConfigPath tells this node which file to write generated config + // into. If empty, then config should not be generated. + generateConfigPath string + + forceCreateBeforeDestroy bool } var ( _ GraphNodeReferenceable = (*NodeAbstractResource)(nil) _ GraphNodeReferencer = (*NodeAbstractResource)(nil) + _ GraphNodeImportReferencer = (*NodeAbstractResource)(nil) _ GraphNodeProviderConsumer = (*NodeAbstractResource)(nil) _ GraphNodeProvisionerConsumer = (*NodeAbstractResource)(nil) _ GraphNodeConfigResource = (*NodeAbstractResource)(nil) @@ -86,6 +103,7 @@ var ( _ GraphNodeTargetable = (*NodeAbstractResource)(nil) _ graphNodeAttachDataResourceDependsOn = (*NodeAbstractResource)(nil) _ dag.GraphNodeDotter = (*NodeAbstractResource)(nil) + _ GraphNodeDestroyerCBD = (*NodeAbstractResource)(nil) ) // NewNodeAbstractResource creates an abstract resource graph node for @@ -110,6 +128,7 @@ var ( _ GraphNodeAttachProvisionerSchema = (*NodeAbstractResourceInstance)(nil) _ GraphNodeAttachProviderMetaConfigs = (*NodeAbstractResourceInstance)(nil) _ GraphNodeTargetable = (*NodeAbstractResourceInstance)(nil) + _ GraphNodeOverridable = (*NodeAbstractResourceInstance)(nil) _ dag.GraphNodeDotter = (*NodeAbstractResourceInstance)(nil) ) @@ -127,16 +146,29 @@ func (n *NodeAbstractResource) ReferenceableAddrs() []addrs.Referenceable { return []addrs.Referenceable{n.Addr.Resource} } -func (n *NodeAbstractResource) Import(addr *ImportTarget) { +// CreateBeforeDestroy returns this node's CreateBeforeDestroy status. +func (n *NodeAbstractResource) CreateBeforeDestroy() bool { + if n.forceCreateBeforeDestroy { + return n.forceCreateBeforeDestroy + } + if n.Config != nil && n.Config.Managed != nil { + return n.Config.Managed.CreateBeforeDestroy + } + + return false +} + +func (n *NodeAbstractResource) ModifyCreateBeforeDestroy(v bool) error { + n.forceCreateBeforeDestroy = v + return nil } // GraphNodeReferencer func (n *NodeAbstractResource) References() []*addrs.Reference { + var result []*addrs.Reference // If we have a config then we prefer to use that. if c := n.Config; c != nil { - var result []*addrs.Reference - result = append(result, n.DependsOn()...) if n.Schema == nil { @@ -145,25 +177,25 @@ func (n *NodeAbstractResource) References() []*addrs.Reference { log.Printf("[WARN] no schema is attached to %s, so config references cannot be detected", n.Name()) } - refs, _ := lang.ReferencesInExpr(c.Count) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, c.Count) result = append(result, refs...) - refs, _ = lang.ReferencesInExpr(c.ForEach) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, c.ForEach) result = append(result, refs...) for _, expr := range c.TriggersReplacement { - refs, _ = lang.ReferencesInExpr(expr) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, expr) result = append(result, refs...) } // ReferencesInBlock() requires a schema if n.Schema != nil { - refs, _ = lang.ReferencesInBlock(c.Config, n.Schema) + refs, _ = langrefs.ReferencesInBlock(addrs.ParseRef, c.Config, n.Schema.Body) result = append(result, refs...) } if c.Managed != nil { if c.Managed.Connection != nil { - refs, _ = lang.ReferencesInBlock(c.Managed.Connection.Config, connectionBlockSupersetSchema) + refs, _ = langrefs.ReferencesInBlock(addrs.ParseRef, c.Managed.Connection.Config, connectionBlockSupersetSchema) result = append(result, refs...) } @@ -172,7 +204,7 @@ func (n *NodeAbstractResource) References() []*addrs.Reference { continue } if p.Connection != nil { - refs, _ = lang.ReferencesInBlock(p.Connection.Config, connectionBlockSupersetSchema) + refs, _ = langrefs.ReferencesInBlock(addrs.ParseRef, p.Connection.Config, connectionBlockSupersetSchema) result = append(result, refs...) } @@ -180,29 +212,44 @@ func (n *NodeAbstractResource) References() []*addrs.Reference { if schema == nil { log.Printf("[WARN] no schema for provisioner %q is attached to %s, so provisioner block references cannot be detected", p.Type, n.Name()) } - refs, _ = lang.ReferencesInBlock(p.Config, schema) + refs, _ = langrefs.ReferencesInBlock(addrs.ParseRef, p.Config, schema) result = append(result, refs...) } } for _, check := range c.Preconditions { - refs, _ := lang.ReferencesInExpr(check.Condition) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, check.Condition) result = append(result, refs...) - refs, _ = lang.ReferencesInExpr(check.ErrorMessage) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage) result = append(result, refs...) } for _, check := range c.Postconditions { - refs, _ := lang.ReferencesInExpr(check.Condition) + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, check.Condition) result = append(result, refs...) - refs, _ = lang.ReferencesInExpr(check.ErrorMessage) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, check.ErrorMessage) result = append(result, refs...) } - - return result } - // Otherwise, we have no references. - return nil + return result +} + +func (n *NodeAbstractResource) ImportReferences() []*addrs.Reference { + var result []*addrs.Reference + for _, importTarget := range n.importTargets { + // legacy import won't have any config + if importTarget.Config == nil { + continue + } + + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ID) + result = append(result, refs...) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, importTarget.Config.Identity) + result = append(result, refs...) + refs, _ = langrefs.ReferencesInExpr(addrs.ParseRef, importTarget.Config.ForEach) + result = append(result, refs...) + } + return result } func (n *NodeAbstractResource) DependsOn() []*addrs.Reference { @@ -231,6 +278,11 @@ func (n *NodeAbstractResource) SetProvider(p addrs.AbsProviderConfig) { // GraphNodeProviderConsumer func (n *NodeAbstractResource) ProvidedBy() (addrs.ProviderConfig, bool) { + // Once the provider is fully resolved, we can return the known value. + if n.ResolvedProvider.Provider.Type != "" { + return n.ResolvedProvider, true + } + // If we have a config we prefer that above all else if n.Config != nil { relAddr := n.Config.ProviderConfigAddr() @@ -240,6 +292,29 @@ func (n *NodeAbstractResource) ProvidedBy() (addrs.ProviderConfig, bool) { }, false } + // See if we have a valid provider config from the state. + if n.storedProviderConfig.Provider.Type != "" { + // An address from the state must match exactly, since we must ensure + // we refresh/destroy a resource with the same provider configuration + // that created it. + return n.storedProviderConfig, true + } + + // We might have an import target that is providing a specific provider, + // this is okay as we know there is nothing else potentially providing a + // provider configuration. + if len(n.importTargets) > 0 { + // The import targets should either all be defined via config or none + // of them should be. They should also all have the same provider, so it + // shouldn't matter which we check here, as they'll all give the same. + if n.importTargets[0].Config != nil && n.importTargets[0].Config.ProviderConfigRef != nil { + return addrs.LocalProviderConfig{ + LocalName: n.importTargets[0].Config.ProviderConfigRef.Name, + Alias: n.importTargets[0].Config.ProviderConfigRef.Alias, + }, false + } + } + // No provider configuration found; return a default address return addrs.AbsProviderConfig{ Provider: n.Provider(), @@ -252,6 +327,19 @@ func (n *NodeAbstractResource) Provider() addrs.Provider { if n.Config != nil { return n.Config.Provider } + if n.storedProviderConfig.Provider.Type != "" { + return n.storedProviderConfig.Provider + } + + if len(n.importTargets) > 0 { + // The import targets should either all be defined via config or none + // of them should be. They should also all have the same provider, so it + // shouldn't matter which we check here, as they'll all give the same. + if n.importTargets[0].Config != nil { + return n.importTargets[0].Config.Provider + } + } + return addrs.ImpliedProviderForUnqualifiedType(n.Addr.Resource.ImpliedProvider()) } @@ -291,20 +379,19 @@ func (n *NodeAbstractResource) SetTargets(targets []addrs.Targetable) { } // graphNodeAttachDataResourceDependsOn -func (n *NodeAbstractResource) AttachDataResourceDependsOn(deps []addrs.ConfigResource, force bool) { +func (n *NodeAbstractResource) AttachDataResourceDependsOn(deps []addrs.ConfigResource) { n.dependsOn = deps - n.forceDependsOn = force } // GraphNodeAttachResourceConfig -func (n *NodeAbstractResource) AttachResourceConfig(c *configs.Resource) { +func (n *NodeAbstractResource) AttachResourceConfig(c *configs.Resource, rc *configs.Removed) { n.Config = c + n.RemovedConfig = rc } // GraphNodeAttachResourceSchema impl -func (n *NodeAbstractResource) AttachResourceSchema(schema *configschema.Block, version uint64) { +func (n *NodeAbstractResource) AttachResourceSchema(schema *providers.Schema) { n.Schema = schema - n.SchemaVersion = version } // GraphNodeAttachProviderMetaConfigs impl @@ -323,16 +410,10 @@ func (n *NodeAbstractResource) DotNode(name string, opts *dag.DotOpts) *dag.DotN } } -// writeResourceState ensures that a suitable resource-level state record is -// present in the state, if that's required for the "each mode" of that -// resource. -// -// This is important primarily for the situation where count = 0, since this -// eval is the only change we get to set the resource "each mode" to list -// in that case, allowing expression evaluation to see it as a zero-element list -// rather than as not set at all. -func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.AbsResource) (diags tfdiags.Diagnostics) { - state := ctx.State() +// recordResourceData records some metadata for the resource as a whole in +// various locations. This currently includes adding resource expansion info to +// the instance expander, and recording the provider used in the state. +func (n *NodeAbstractResource) recordResourceData(ctx EvalContext, addr addrs.AbsResource) (diags tfdiags.Diagnostics) { // We'll record our expansion decision in the shared "expander" object // so that later operations (i.e. DynamicExpand and expression evaluation) @@ -340,19 +421,30 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab // to expand the module here to create all resources. expander := ctx.InstanceExpander() + // Allowing unknown values in count and for_each is a top-level plan option. + // + // If this is false then the codepaths that handle unknown values below + // become unreachable, because the evaluate functions will reject unknown + // values as an error. + allowUnknown := ctx.Deferrals().DeferralAllowed() + switch { - case n.Config.Count != nil: - count, countDiags := evaluateCountExpression(n.Config.Count, ctx) + case n.Config != nil && n.Config.Count != nil: + count, countDiags := evaluateCountExpression(n.Config.Count, ctx, allowUnknown) diags = diags.Append(countDiags) if countDiags.HasErrors() { return diags } - state.SetResourceProvider(addr, n.ResolvedProvider) - expander.SetResourceCount(addr.Module, n.Addr.Resource, count) + if count >= 0 { + expander.SetResourceCount(addr.Module, n.Addr.Resource, count) + } else { + // -1 represents "unknown" + expander.SetResourceCountUnknown(addr.Module, n.Addr.Resource) + } - case n.Config.ForEach != nil: - forEach, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx) + case n.Config != nil && n.Config.ForEach != nil: + forEach, known, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx, allowUnknown) diags = diags.Append(forEachDiags) if forEachDiags.HasErrors() { return diags @@ -360,14 +452,24 @@ func (n *NodeAbstractResource) writeResourceState(ctx EvalContext, addr addrs.Ab // This method takes care of all of the business logic of updating this // while ensuring that any existing instances are preserved, etc. - state.SetResourceProvider(addr, n.ResolvedProvider) - expander.SetResourceForEach(addr.Module, n.Addr.Resource, forEach) + if known { + expander.SetResourceForEach(addr.Module, n.Addr.Resource, forEach) + } else { + expander.SetResourceForEachUnknown(addr.Module, n.Addr.Resource) + } default: - state.SetResourceProvider(addr, n.ResolvedProvider) expander.SetResourceSingle(addr.Module, n.Addr.Resource) } + if addr.Resource.Mode == addrs.EphemeralResourceMode { + // ephemeral resources are not included in the state + return diags + } + + state := ctx.State() + state.SetResourceProvider(addr, n.ResolvedProvider) + return diags } @@ -383,19 +485,19 @@ func (n *NodeAbstractResource) readResourceInstanceState(ctx EvalContext, addr a log.Printf("[TRACE] readResourceInstanceState: reading state for %s", addr) - src := ctx.State().ResourceInstanceObject(addr, states.CurrentGen) + src := ctx.State().ResourceInstanceObject(addr, addrs.NotDeposed) if src == nil { // Presumably we only have deposed objects, then. log.Printf("[TRACE] readResourceInstanceState: no state present for %s", addr) return nil, nil } - schema, currentVersion := (providerSchema).SchemaForResourceAddr(addr.Resource.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.Resource.ContainingResource()) + if schema.Body == nil { // Shouldn't happen since we should've failed long ago if no schema is present return nil, diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", addr)) } - src, upgradeDiags := upgradeResourceState(addr, provider, src, schema, currentVersion) + src, upgradeDiags := upgradeResourceState(addr, provider, src, schema) if n.Config != nil { upgradeDiags = upgradeDiags.InConfigBody(n.Config.Config, addr.String()) } @@ -404,7 +506,13 @@ func (n *NodeAbstractResource) readResourceInstanceState(ctx EvalContext, addr a return nil, diags } - obj, err := src.Decode(schema.ImpliedType()) + src, upgradeDiags = upgradeResourceIdentity(addr, provider, src, schema) + diags = diags.Append(upgradeDiags) + if diags.HasErrors() { + return nil, diags + } + + obj, err := src.Decode(schema) if err != nil { diags = diags.Append(err) } @@ -435,14 +543,14 @@ func (n *NodeAbstractResource) readResourceInstanceStateDeposed(ctx EvalContext, return nil, diags } - schema, currentVersion := (providerSchema).SchemaForResourceAddr(addr.Resource.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.Resource.ContainingResource()) + if schema.Body == nil { // Shouldn't happen since we should've failed long ago if no schema is present return nil, diags.Append(fmt.Errorf("no schema available for %s while reading state; this is a bug in Terraform and should be reported", addr)) } - src, upgradeDiags := upgradeResourceState(addr, provider, src, schema, currentVersion) + src, upgradeDiags := upgradeResourceState(addr, provider, src, schema) if n.Config != nil { upgradeDiags = upgradeDiags.InConfigBody(n.Config.Config, addr.String()) } @@ -455,7 +563,13 @@ func (n *NodeAbstractResource) readResourceInstanceStateDeposed(ctx EvalContext, return nil, diags } - obj, err := src.Decode(schema.ImpliedType()) + src, upgradeDiags = upgradeResourceIdentity(addr, provider, src, schema) + diags = diags.Append(upgradeDiags) + if diags.HasErrors() { + return nil, diags + } + + obj, err := src.Decode(schema) if err != nil { diags = diags.Append(err) } diff --git a/internal/terraform/node_resource_abstract_instance.go b/internal/terraform/node_resource_abstract_instance.go index 7ffc310d42..6ae8f812cd 100644 --- a/internal/terraform/node_resource_abstract_instance.go +++ b/internal/terraform/node_resource_abstract_instance.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,13 +9,20 @@ import ( "strings" "github.com/hashicorp/hcl/v2" + tfaddr "github.com/hashicorp/terraform-registry-address" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/ephemeral" + "github.com/hashicorp/terraform/internal/lang/format" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/moduletest/mocking" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/plans/objchange" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" @@ -31,12 +41,17 @@ type NodeAbstractResourceInstance struct { // These are set via the AttachState method. instanceState *states.ResourceInstance - // storedProviderConfig is the provider address retrieved from the - // state, but since it is only stored in the whole Resource rather than the - // ResourceInstance, we extract it out here. - storedProviderConfig addrs.AbsProviderConfig Dependencies []addrs.ConfigResource + + preDestroyRefresh bool + + // During import we may generate configuration for a resource, which needs + // to be stored in the final change. + generatedConfigHCL string + + // override is set by the graph itself, just before this node executes. + override *configs.Override } // NewNodeAbstractResourceInstance creates an abstract resource instance graph @@ -62,6 +77,13 @@ func (n *NodeAbstractResourceInstance) Path() addrs.ModuleInstance { return n.Addr.Module } +func (n *NodeAbstractResourceInstance) HookResourceIdentity() HookResourceIdentity { + return HookResourceIdentity{ + Addr: n.Addr, + ProviderAddr: n.ResolvedProvider.Provider, + } +} + // GraphNodeReferenceable func (n *NodeAbstractResourceInstance) ReferenceableAddrs() []addrs.Referenceable { addr := n.ResourceInstanceAddr() @@ -96,52 +118,21 @@ func (n *NodeAbstractResourceInstance) References() []*addrs.Reference { return nil } -// StateDependencies returns the dependencies saved in the state. +// StateDependencies returns the dependencies which will be saved in the state +// for managed resources, or the most current dependencies for data resources. func (n *NodeAbstractResourceInstance) StateDependencies() []addrs.ConfigResource { + // Managed resources prefer the stored dependencies, to avoid possible + // conflicts in ordering when refactoring configuration. if s := n.instanceState; s != nil { if s.Current != nil { return s.Current.Dependencies } } - return nil -} - -// GraphNodeProviderConsumer -func (n *NodeAbstractResourceInstance) ProvidedBy() (addrs.ProviderConfig, bool) { - // If we have a config we prefer that above all else - if n.Config != nil { - relAddr := n.Config.ProviderConfigAddr() - return addrs.LocalProviderConfig{ - LocalName: relAddr.LocalName, - Alias: relAddr.Alias, - }, false - } - - // See if we have a valid provider config from the state. - if n.storedProviderConfig.Provider.Type != "" { - // An address from the state must match exactly, since we must ensure - // we refresh/destroy a resource with the same provider configuration - // that created it. - return n.storedProviderConfig, true - } - - // No provider configuration found; return a default address - return addrs.AbsProviderConfig{ - Provider: n.Provider(), - Module: n.ModulePath(), - }, false -} - -// GraphNodeProviderConsumer -func (n *NodeAbstractResourceInstance) Provider() addrs.Provider { - if n.Config != nil { - return n.Config.Provider - } - if n.storedProviderConfig.Provider.Type != "" { - return n.storedProviderConfig.Provider - } - return addrs.ImpliedProviderForUnqualifiedType(n.Addr.Resource.ContainingResource().ImpliedProvider()) + // If there are no stored dependencies, this is either a newly created + // managed resource, or a data source, and we can use the most recently + // calculated dependencies. + return n.Dependencies } // GraphNodeResourceInstance @@ -160,36 +151,36 @@ func (n *NodeAbstractResourceInstance) AttachResourceState(s *states.Resource) { n.storedProviderConfig = s.ProviderConfig } +// GraphNodeOverridable +func (n *NodeAbstractResourceInstance) ConfigProvider() addrs.AbsProviderConfig { + return n.ResolvedProvider +} + +// GraphNodeOverridable +func (n *NodeAbstractResourceInstance) SetOverride(override *configs.Override) { + n.override = override +} + // readDiff returns the planned change for a particular resource instance // object. -func (n *NodeAbstractResourceInstance) readDiff(ctx EvalContext, providerSchema *ProviderSchema) (*plans.ResourceInstanceChange, error) { +func (n *NodeAbstractResourceInstance) readDiff(ctx EvalContext, providerSchema providers.ProviderSchema) (*plans.ResourceInstanceChange, error) { changes := ctx.Changes() addr := n.ResourceInstanceAddr() - schema, _ := providerSchema.SchemaForResourceAddr(addr.Resource.Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.Resource.Resource) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here return nil, fmt.Errorf("provider does not support resource type %q", addr.Resource.Resource.Type) } - gen := states.CurrentGen - csrc := changes.GetResourceInstanceChange(addr, gen) - if csrc == nil { - log.Printf("[TRACE] readDiff: No planned change recorded for %s", n.Addr) - return nil, nil - } - - change, err := csrc.Decode(schema.ImpliedType()) - if err != nil { - return nil, fmt.Errorf("failed to decode planned changes for %s: %s", n.Addr, err) - } + change := changes.GetResourceInstanceChange(addr, addrs.NotDeposed) log.Printf("[TRACE] readDiff: Read %s change from plan for %s", change.Action, n.Addr) return change, nil } -func (n *NodeAbstractResourceInstance) checkPreventDestroy(change *plans.ResourceInstanceChange) error { +func (n *NodeAbstractResourceInstance) checkPreventDestroy(change *plans.ResourceInstanceChange) tfdiags.Diagnostics { if change == nil || n.Config == nil || n.Config.Managed == nil { return nil } @@ -202,12 +193,12 @@ func (n *NodeAbstractResourceInstance) checkPreventDestroy(change *plans.Resourc Severity: hcl.DiagError, Summary: "Instance cannot be destroyed", Detail: fmt.Sprintf( - "Resource %s has lifecycle.prevent_destroy set, but the plan calls for this resource to be destroyed. To avoid this error and continue with the plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan using the -target flag.", + "Resource %s has lifecycle.prevent_destroy set, but the plan calls for this resource to be destroyed. To avoid this error and continue with the plan, either disable lifecycle.prevent_destroy or reduce the scope of the plan using the -target option.", n.Addr.String(), ), Subject: &n.Config.DeclRange, }) - return diags.Err() + return diags } return nil @@ -227,7 +218,7 @@ func (n *NodeAbstractResourceInstance) preApplyHook(ctx EvalContext, change *pla plannedNewState := change.After diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreApply(n.Addr, change.DeposedKey.Generation(), change.Action, priorState, plannedNewState) + return h.PreApply(n.HookResourceIdentity(), change.DeposedKey, change.Action, priorState, plannedNewState) })) if diags.HasErrors() { return diags @@ -250,7 +241,7 @@ func (n *NodeAbstractResourceInstance) postApplyHook(ctx EvalContext, state *sta newState = cty.NullVal(cty.DynamicPseudoType) } diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostApply(n.Addr, nil, newState, err) + return h.PostApply(n.HookResourceIdentity(), addrs.NotDeposed, newState, err) })) } @@ -265,7 +256,7 @@ const ( prevRunState ) -//go:generate go run golang.org/x/tools/cmd/stringer -type phaseState +//go:generate go tool golang.org/x/tools/cmd/stringer -type phaseState // writeResourceInstanceState saves the given object as the current object for // the selected resource instance. @@ -348,26 +339,17 @@ func (n *NodeAbstractResourceInstance) writeResourceInstanceStateImpl(ctx EvalCo return nil } - if providerSchema == nil { - // Should never happen, unless our state object is nil - panic("writeResourceInstanceStateImpl used with nil ProviderSchema") - } + log.Printf("[TRACE] %s: writing state object for %s", logFuncName, absAddr) - if obj != nil { - log.Printf("[TRACE] %s: writing state object for %s", logFuncName, absAddr) - } else { - log.Printf("[TRACE] %s: removing state object for %s", logFuncName, absAddr) - } - - schema, currentVersion := (*providerSchema).SchemaForResourceAddr(absAddr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) + if schema.Body == nil { // It shouldn't be possible to get this far in any real scenario // without a schema, but we might end up here in contrived tests that // fail to set up their world properly. return fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr) } - src, err := obj.Encode(schema.ImpliedType(), currentVersion) + src, err := obj.Encode(schema) if err != nil { return fmt.Errorf("failed to encode %s in state: %s", absAddr, err) } @@ -377,11 +359,13 @@ func (n *NodeAbstractResourceInstance) writeResourceInstanceStateImpl(ctx EvalCo } // planDestroy returns a plain destroy diff. -func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState *states.ResourceInstanceObject, deposedKey states.DeposedKey) (*plans.ResourceInstanceChange, tfdiags.Diagnostics) { +func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState *states.ResourceInstanceObject, deposedKey states.DeposedKey) (*plans.ResourceInstanceChange, *providers.Deferred, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + var deferred *providers.Deferred var plan *plans.ResourceInstanceChange absAddr := n.Addr + deferralAllowed := ctx.Deferrals().DeferralAllowed() if n.ResolvedProvider.Provider.Type == "" { if deposedKey == "" { @@ -409,7 +393,25 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState }, ProviderAddr: n.ResolvedProvider, } - return noop, nil + return noop, deferred, nil + } + + _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) + if err != nil { + return plan, deferred, diags.Append(err) + } + schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type)) + return nil, deferred, diags + } + + // If we are in a context where we forget instead of destroying, we can + // just return the forget change without consulting the provider. + if ctx.Forget() { + forget, diags := n.planForget(ctx, currentState, deposedKey) + return forget, deferred, diags } unmarkedPriorVal, _ := currentState.Value.UnmarkDeep() @@ -420,48 +422,74 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState provider, _, err := getProvider(ctx, n.ResolvedProvider) if err != nil { - return plan, diags.Append(err) + return plan, deferred, diags.Append(err) } metaConfigVal, metaDiags := n.providerMetas(ctx) diags = diags.Append(metaDiags) if diags.HasErrors() { - return plan, diags + return plan, deferred, diags } - // Allow the provider to check the destroy plan, and insert any necessary - // private data. - resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: nullVal, - PriorState: unmarkedPriorVal, - ProposedNewState: nullVal, - PriorPrivate: currentState.Private, - ProviderMeta: metaConfigVal, - }) + var resp providers.PlanResourceChangeResponse + if n.override != nil { + // If we have an overridden value from the test framework, that means + // this value was created without consulting the provider previously. + // We can just set the planned state to deleted without consulting the + // provider. + resp = providers.PlanResourceChangeResponse{ + PlannedState: nullVal, + } + } else { + // Allow the provider to check the destroy plan, and insert any + // necessary private data. + resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + Config: nullVal, + PriorState: unmarkedPriorVal, + ProposedNewState: nullVal, + PriorPrivate: currentState.Private, + ProviderMeta: metaConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), + PriorIdentity: currentState.Identity, + }) + deferred = resp.Deferred - // We may not have a config for all destroys, but we want to reference it in - // the diagnostics if we do. - if n.Config != nil { - resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()) - } - diags = diags.Append(resp.Diagnostics) - if diags.HasErrors() { - return plan, diags - } + // If we don't support deferrals, but the provider reports a deferral and does not + // emit any error level diagnostics, we should emit an error. + if resp.Deferred != nil && !deferralAllowed && !resp.Diagnostics.HasErrors() { + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(n.Addr)) + } - // Check that the provider returned a null value here, since that is the - // only valid value for a destroy plan. - if !resp.PlannedState.IsNull() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced invalid plan", - fmt.Sprintf( - "Provider %q planned a non-null destroy value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider.Provider, n.Addr), - ), - ) - return plan, diags + if !resp.PlannedIdentity.IsNull() { + // Destroying is an operation where we allow identity changes. + diags = diags.Append(n.validateIdentityKnown(resp.PlannedIdentity)) + diags = diags.Append(n.validateIdentity(resp.PlannedIdentity, schema.Identity)) + } + + // We may not have a config for all destroys, but we want to reference + // it in the diagnostics if we do. + if n.Config != nil { + resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()) + } + diags = diags.Append(resp.Diagnostics) + if diags.HasErrors() { + return plan, deferred, diags + } + + // Check that the provider returned a null value here, since that is the + // only valid value for a destroy plan. + if !resp.PlannedState.IsNull() && deferred == nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q planned a non-null destroy value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr), + ), + ) + return plan, deferred, diags + } } // Plan is always the same for a destroy. @@ -470,14 +498,63 @@ func (n *NodeAbstractResourceInstance) planDestroy(ctx EvalContext, currentState PrevRunAddr: n.prevRunAddr(ctx), DeposedKey: deposedKey, Change: plans.Change{ - Action: plans.Delete, - Before: currentState.Value, - After: cty.NullVal(cty.DynamicPseudoType), + Action: plans.Delete, + Before: currentState.Value, + BeforeIdentity: currentState.Identity, + After: nullVal, + AfterIdentity: resp.PlannedIdentity, }, Private: resp.PlannedPrivate, ProviderAddr: n.ResolvedProvider, } + return plan, deferred, diags +} + +// planForget returns a Forget change. +func (n *NodeAbstractResourceInstance) planForget(ctx EvalContext, currentState *states.ResourceInstanceObject, deposedKey states.DeposedKey) (*plans.ResourceInstanceChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + var plan *plans.ResourceInstanceChange + + absAddr := n.Addr + + // If there is no state or our attributes object is null then the resource + // is already removed. + if currentState == nil || currentState.Value.IsNull() { + // We still need to generate a NoOp change, because that allows + // outside consumers of the plan to distinguish between us affirming + // that we checked something and concluded no changes were needed + // vs. that something being entirely excluded e.g. due to -target. + noop := &plans.ResourceInstanceChange{ + Addr: absAddr, + PrevRunAddr: n.prevRunAddr(ctx), + DeposedKey: deposedKey, + Change: plans.Change{ + Action: plans.NoOp, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.NullVal(cty.DynamicPseudoType), + }, + ProviderAddr: n.ResolvedProvider, + } + return noop, nil + } + + unmarkedPriorVal, _ := currentState.Value.UnmarkDeep() + nullVal := cty.NullVal(unmarkedPriorVal.Type()) + + // Plan is always the same for a forget. + plan = &plans.ResourceInstanceChange{ + Addr: absAddr, + PrevRunAddr: n.prevRunAddr(ctx), + DeposedKey: deposedKey, + Change: plans.Change{ + Action: plans.Forget, + Before: currentState.Value, + After: nullVal, + }, + ProviderAddr: n.ResolvedProvider, + } + return plan, diags } @@ -489,19 +566,10 @@ func (n *NodeAbstractResourceInstance) writeChange(ctx EvalContext, change *plan if change == nil { // Caller sets nil to indicate that we need to remove a change from // the set of changes. - gen := states.CurrentGen - if deposedKey != states.NotDeposed { - gen = deposedKey - } - changes.RemoveResourceInstanceChange(n.Addr, gen) + changes.RemoveResourceInstanceChange(n.Addr, deposedKey) return nil } - _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) - if err != nil { - return err - } - if change.Addr.String() != n.Addr.String() || change.DeposedKey != deposedKey { // Should never happen, and indicates a bug in the caller. panic("inconsistent address and/or deposed key in writeChange") @@ -517,19 +585,7 @@ func (n *NodeAbstractResourceInstance) writeChange(ctx EvalContext, change *plan panic("unpopulated ResourceInstanceChange.PrevRunAddr in writeChange") } - ri := n.Addr.Resource - schema, _ := providerSchema.SchemaForResourceAddr(ri.Resource) - if schema == nil { - // Should be caught during validation, so we don't bother with a pretty error here - return fmt.Errorf("provider does not support resource type %q", ri.Resource.Type) - } - - csrc, err := change.Encode(schema.ImpliedType()) - if err != nil { - return fmt.Errorf("failed to encode planned changes for %s: %s", n.Addr, err) - } - - changes.AppendResourceInstanceChange(csrc) + changes.AppendResourceInstanceChange(change) if deposedKey == states.NotDeposed { log.Printf("[TRACE] writeChange: recorded %s change for %s", change.Action, n.Addr) } else { @@ -540,8 +596,10 @@ func (n *NodeAbstractResourceInstance) writeChange(ctx EvalContext, change *plan } // refresh does a refresh for a resource -func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey states.DeposedKey, state *states.ResourceInstanceObject) (*states.ResourceInstanceObject, tfdiags.Diagnostics) { +// if the second return value is non-nil, the refresh is deferred +func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey states.DeposedKey, state *states.ResourceInstanceObject, deferralAllowed bool) (*states.ResourceInstanceObject, *providers.Deferred, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics + var deferred *providers.Deferred absAddr := n.Addr if deposedKey == states.NotDeposed { log.Printf("[TRACE] NodeAbstractResourceInstance.refresh for %s", absAddr) @@ -550,64 +608,81 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state } provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) if err != nil { - return state, diags.Append(err) + return state, deferred, diags.Append(err) } // If we have no state, we don't do any refreshing if state == nil { log.Printf("[DEBUG] refresh: %s: no state, so not refreshing", absAddr) - return state, diags + return state, deferred, diags } - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.Resource.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.ContainingResource()) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type)) - return state, diags + return state, deferred, diags } metaConfigVal, metaDiags := n.providerMetas(ctx) diags = diags.Append(metaDiags) if diags.HasErrors() { - return state, diags - } - - hookGen := states.CurrentGen - if deposedKey != states.NotDeposed { - hookGen = deposedKey + return state, deferred, diags } // Call pre-refresh hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreRefresh(absAddr, hookGen, state.Value) + return h.PreRefresh(n.HookResourceIdentity(), deposedKey, state.Value) })) if diags.HasErrors() { - return state, diags + return state, deferred, diags } // Refresh! priorVal := state.Value // Unmarked before sending to provider - var priorPaths []cty.PathValueMarks - if priorVal.ContainsMarked() { - priorVal, priorPaths = priorVal.UnmarkDeepWithPaths() - } + var priorMarks []cty.PathValueMarks + priorVal, priorMarks = priorVal.UnmarkDeepWithPaths() - providerReq := providers.ReadResourceRequest{ - TypeName: n.Addr.Resource.Resource.Type, - PriorState: priorVal, - Private: state.Private, - ProviderMeta: metaConfigVal, - } + var resp providers.ReadResourceResponse + if n.override != nil { + // If we have an override set for this resource, we don't want to talk + // to the provider so we'll just return whatever was in state. + resp = providers.ReadResourceResponse{ + NewState: priorVal, + Identity: state.Identity, + } + } else { + resp = provider.ReadResource(providers.ReadResourceRequest{ + TypeName: n.Addr.Resource.Resource.Type, + PriorState: priorVal, + Private: state.Private, + ProviderMeta: metaConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), + CurrentIdentity: state.Identity, + }) - resp := provider.ReadResource(providerReq) + // If we don't support deferrals, but the provider reports a deferral and does not + // emit any error level diagnostics, we should emit an error. + if resp.Deferred != nil && !deferralAllowed && !resp.Diagnostics.HasErrors() { + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(n.Addr)) + } + + if !resp.Identity.IsNull() { + diags = diags.Append(n.validateIdentityKnown(resp.Identity)) + diags = diags.Append(n.validateIdentity(resp.Identity, schema.Identity)) + } + if resp.Deferred != nil { + deferred = resp.Deferred + } + } if n.Config != nil { resp.Diagnostics = resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String()) } diags = diags.Append(resp.Diagnostics) if diags.HasErrors() { - return state, diags + return state, deferred, diags } if resp.NewState == cty.NilVal { @@ -617,51 +692,86 @@ func (n *NodeAbstractResourceInstance) refresh(ctx EvalContext, deposedKey state panic("new state is cty.NilVal") } - for _, err := range resp.NewState.Type().TestConformance(schema.ImpliedType()) { + // If we have deferred the refresh, we expect the new state not to be wholly known + // and callers should be prepared to handle this. + if !resp.NewState.IsWhollyKnown() && deferred == nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid object", fmt.Sprintf( "Provider %q planned an invalid value for %s during refresh: %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider.Provider.String(), absAddr, tfdiags.FormatError(err), + n.ResolvedProvider.Provider, absAddr, "The returned state contains unknown values", + ), + )) + } + + for _, err := range resp.NewState.Type().TestConformance(schema.Body.ImpliedType()) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid object", + fmt.Sprintf( + "Provider %q planned an invalid value for %s during refresh: %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, absAddr, tfdiags.FormatError(err), ), )) } if diags.HasErrors() { - return state, diags + return state, deferred, diags } - // We have no way to exempt provider using the legacy SDK from this check, - // so we can only log inconsistencies with the updated state values. - // In most cases these are not errors anyway, and represent "drift" from - // external changes which will be handled by the subsequent plan. - if errs := objchange.AssertObjectCompatible(schema, priorVal, resp.NewState); len(errs) > 0 { - var buf strings.Builder - fmt.Fprintf(&buf, "[WARN] Provider %q produced an unexpected new value for %s during refresh.", n.ResolvedProvider.Provider.String(), absAddr) - for _, err := range errs { - fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) - } - log.Print(buf.String()) + // Providers are supposed to return null values for all write-only attributes + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( + "Provider produced invalid object", + func(path cty.Path) string { + return fmt.Sprintf( + "Provider %q returned a value for the write-only attribute \"%s%s\" during refresh. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, tfdiags.FormatCtyPath(path), + ) + }, + resp.NewState, + schema.Body, + ) + diags = diags.Append(writeOnlyDiags) + + if writeOnlyDiags.HasErrors() { + return state, deferred, diags + } + if diags.HasErrors() { + return state, deferred, diags + } + + newState := objchange.NormalizeObjectFromLegacySDK(resp.NewState, schema.Body) + if !newState.RawEquals(resp.NewState) { + // We had to fix up this object in some way, and we still need to + // accept any changes for compatibility, so all we can do is log a + // warning about the change. + log.Printf("[WARN] Provider %q produced an invalid new value containing null blocks for %q during refresh\n", n.ResolvedProvider.Provider, n.Addr) } ret := state.DeepCopy() - ret.Value = resp.NewState + ret.Value = newState ret.Private = resp.Private + ret.Identity = resp.Identity // Call post-refresh hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostRefresh(absAddr, hookGen, priorVal, ret.Value) + return h.PostRefresh(n.HookResourceIdentity(), deposedKey, priorVal, ret.Value) })) if diags.HasErrors() { - return ret, diags + return ret, deferred, diags } - // Mark the value if necessary - if len(priorPaths) > 0 { - ret.Value = ret.Value.MarkWithPaths(priorPaths) + // Mark the value with any prior marks from the state, and the marks from + // the schema. This ensures we capture any marks from the last + // configuration, as well as any marks from the schema which were not in + // the prior state. New marks may appear when the prior state was from an + // import operation, or if the provider added new marks to the schema. + ret.Value = ret.Value.MarkWithPaths(priorMarks) + if moreSensitivePaths := schema.Body.SensitivePaths(ret.Value, nil); len(moreSensitivePaths) != 0 { + ret.Value = marks.MarkPaths(ret.Value, marks.Sensitive, moreSensitivePaths) } - return ret, diags + return ret, deferred, diags } func (n *NodeAbstractResourceInstance) plan( @@ -669,17 +779,46 @@ func (n *NodeAbstractResourceInstance) plan( plannedChange *plans.ResourceInstanceChange, currentState *states.ResourceInstanceObject, createBeforeDestroy bool, - forceReplace []addrs.AbsResourceInstance) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) { + forceReplace []addrs.AbsResourceInstance, +) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, *providers.Deferred, instances.RepetitionData, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - var state *states.ResourceInstanceObject - var plan *plans.ResourceInstanceChange var keyData instances.RepetitionData + var deferred *providers.Deferred - config := *n.Config resource := n.Addr.Resource.Resource + deferralAllowed := ctx.Deferrals().DeferralAllowed() + provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) if err != nil { - return plan, state, keyData, diags.Append(err) + return nil, nil, deferred, keyData, diags.Append(err) + } + + schema := providerSchema.SchemaForResourceAddr(resource) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append(fmt.Errorf("provider does not support resource type %q", resource.Type)) + return nil, nil, deferred, keyData, diags + } + + // If we're importing and generating config, generate it now. + if n.Config == nil { + // This shouldn't happen. A node that isn't generating config should + // have embedded config, and the rest of Terraform should enforce this. + // If, however, we didn't do things correctly the next line will panic, + // so let's not do that and return an error message with more context. + + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource has no configuration", + fmt.Sprintf("Terraform attempted to process a resource at %s that has no configuration. This is a bug in Terraform; please report it!", n.Addr.String()))) + return nil, nil, deferred, keyData, diags + } + + config := *n.Config + + checkRuleSeverity := tfdiags.Error + if n.preDestroyRefresh { + checkRuleSeverity = tfdiags.Warning } if plannedChange != nil { @@ -687,20 +826,8 @@ func (n *NodeAbstractResourceInstance) plan( createBeforeDestroy = plannedChange.Action == plans.CreateThenDelete } - if providerSchema == nil { - diags = diags.Append(fmt.Errorf("provider schema is unavailable for %s", n.Addr)) - return plan, state, keyData, diags - } - // Evaluate the configuration - schema, _ := providerSchema.SchemaForResourceAddr(resource) - if schema == nil { - // Should be caught during validation, so we don't bother with a pretty error here - diags = diags.Append(fmt.Errorf("provider does not support resource type %q", resource.Type)) - return plan, state, keyData, diags - } - - forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) @@ -708,32 +835,44 @@ func (n *NodeAbstractResourceInstance) plan( addrs.ResourcePrecondition, n.Config.Preconditions, ctx, n.Addr, keyData, - tfdiags.Error, + checkRuleSeverity, ) diags = diags.Append(checkDiags) if diags.HasErrors() { - return plan, state, keyData, diags // failed preconditions prevent further evaluation + return nil, nil, deferred, keyData, diags // failed preconditions prevent further evaluation } - origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + // If we have a previous plan and the action was a noop, then the only + // reason we're in this method was to evaluate the preconditions. There's + // no need to re-plan this resource. + if plannedChange != nil && plannedChange.Action == plans.NoOp { + return plannedChange, currentState.DeepCopy(), deferred, keyData, diags + } + + origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return plan, state, keyData, diags + diags = diags.Append( + validateResourceForbiddenEphemeralValues(ctx, origConfigVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), + ) + if diags.HasErrors() { + return nil, nil, deferred, keyData, diags } metaConfigVal, metaDiags := n.providerMetas(ctx) diags = diags.Append(metaDiags) if diags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } var priorVal cty.Value var priorValTainted cty.Value var priorPrivate []byte + var priorIdentity cty.Value if currentState != nil { if currentState.Status != states.ObjectTainted { priorVal = currentState.Value priorPrivate = currentState.Private + priorIdentity = currentState.Identity } else { // If the prior state is tainted then we'll proceed below like // we're creating an entirely new object, but then turn it into @@ -741,10 +880,10 @@ func (n *NodeAbstractResourceInstance) plan( // result as if the provider had marked at least one argument // change as "requires replacement". priorValTainted = currentState.Value - priorVal = cty.NullVal(schema.ImpliedType()) + priorVal = cty.NullVal(schema.Body.ImpliedType()) } } else { - priorVal = cty.NullVal(schema.ImpliedType()) + priorVal = cty.NullVal(schema.Body.ImpliedType()) } log.Printf("[TRACE] Re-validating config for %q", n.Addr) @@ -761,13 +900,14 @@ func (n *NodeAbstractResourceInstance) plan( unmarkedConfigVal, _ := origConfigVal.UnmarkDeep() validateResp := provider.ValidateResourceConfig( providers.ValidateResourceConfigRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: unmarkedConfigVal, + TypeName: n.Addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), }, ) diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } // ignore_changes is meant to only apply to the configuration, so it must @@ -777,97 +917,159 @@ func (n *NodeAbstractResourceInstance) plan( // starting values. // Here we operate on the marked values, so as to revert any changes to the // marks as well as the value. - configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal) + configValIgnored, ignoreChangeDiags := n.processIgnoreChanges(priorVal, origConfigVal, schema.Body) diags = diags.Append(ignoreChangeDiags) if ignoreChangeDiags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } // Create an unmarked version of our config val and our prior val. // Store the paths for the config val to re-mark after we've sent things // over the wire. unmarkedConfigVal, unmarkedPaths := configValIgnored.UnmarkDeepWithPaths() - unmarkedPriorVal, priorPaths := priorVal.UnmarkDeepWithPaths() + unmarkedPriorVal, _ := priorVal.UnmarkDeepWithPaths() - proposedNewVal := objchange.ProposedNew(schema, unmarkedPriorVal, unmarkedConfigVal) + proposedNewVal := objchange.ProposedNew(schema.Body, unmarkedPriorVal, unmarkedConfigVal) // Call pre-diff hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreDiff(n.Addr, states.CurrentGen, priorVal, proposedNewVal) + return h.PreDiff(n.HookResourceIdentity(), addrs.NotDeposed, priorVal, proposedNewVal) })) if diags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } - resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: unmarkedConfigVal, - PriorState: unmarkedPriorVal, - ProposedNewState: proposedNewVal, - PriorPrivate: priorPrivate, - ProviderMeta: metaConfigVal, - }) + var resp providers.PlanResourceChangeResponse + if n.override != nil { + // Then we have an override to apply for this change. But, overrides + // only matter when we are creating a resource for the first time as we + // only apply computed values. + if priorVal.IsNull() { + // Then we are actually creating something, so let's populate the + // computed values from our override value. + override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: !n.override.UseForPlan, + }, schema.Body) + resp = providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema.Body), + Diagnostics: overrideDiags, + } + } else { + // This is an update operation, and we don't actually have any + // computed values that need to be applied. + resp = providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(proposedNewVal, schema.Body), + } + } + } else { + resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + PriorState: unmarkedPriorVal, + ProposedNewState: proposedNewVal, + PriorPrivate: priorPrivate, + ProviderMeta: metaConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), + PriorIdentity: priorIdentity, + }) + // If we don't support deferrals, but the provider reports a deferral and does not + // emit any error level diagnostics, we should emit an error. + if resp.Deferred != nil && !deferralAllowed && !resp.Diagnostics.HasErrors() { + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(n.Addr)) + } + } diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags + } + + // We mark this node as deferred at a later point when we know the complete change + if resp.Deferred != nil { + deferred = resp.Deferred } plannedNewVal := resp.PlannedState plannedPrivate := resp.PlannedPrivate + plannedIdentity := resp.PlannedIdentity - if plannedNewVal == cty.NilVal { - // Should never happen. Since real-world providers return via RPC a nil - // is always a bug in the client-side stub. This is more likely caused - // by an incompletely-configured mock provider in tests, though. - panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", n.Addr)) - } + // These checks are only relevant if the provider is not deferring the + // change. + if deferred == nil { + if plannedNewVal == cty.NilVal { + // Should never happen. Since real-world providers return via RPC a nil + // is always a bug in the client-side stub. This is more likely caused + // by an incompletely-configured mock provider in tests, though. + panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", n.Addr)) + } - // We allow the planned new value to disagree with configuration _values_ - // here, since that allows the provider to do special logic like a - // DiffSuppressFunc, but we still require that the provider produces - // a value whose type conforms to the schema. - for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, + // Providers are supposed to return null values for all write-only attributes + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( "Provider produced invalid plan", - fmt.Sprintf( - "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider.Provider, tfdiags.FormatErrorPrefixed(err, n.Addr.String()), - ), - )) - } - if diags.HasErrors() { - return plan, state, keyData, diags - } + func(path cty.Path) string { + return fmt.Sprintf( + "Provider %q returned a value for the write-only attribute \"%s%s\" during planning. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, tfdiags.FormatCtyPath(path), + ) + }, + plannedNewVal, + schema.Body, + ) + diags = diags.Append(writeOnlyDiags) - if errs := objchange.AssertPlanValid(schema, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { - if resp.LegacyTypeSystem { - // The shimming of the old type system in the legacy SDK is not precise - // enough to pass this consistency check, so we'll give it a pass here, - // but we will generate a warning about it so that we are more likely - // to notice in the logs if an inconsistency beyond the type system - // leads to a downstream provider failure. - var buf strings.Builder - fmt.Fprintf(&buf, - "[WARN] Provider %q produced an invalid plan for %s, but we are tolerating it because it is using the legacy plugin SDK.\n The following problems may be the cause of any confusing errors from downstream operations:", - n.ResolvedProvider.Provider, n.Addr, - ) - for _, err := range errs { - fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) + if writeOnlyDiags.HasErrors() { + return nil, nil, deferred, keyData, diags + } + + // We allow the planned new value to disagree with configuration _values_ + // here, since that allows the provider to do special logic like a + // DiffSuppressFunc, but we still require that the provider produces + // a value whose type conforms to the schema. + for _, err := range plannedNewVal.Type().TestConformance(schema.Body.ImpliedType()) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, tfdiags.FormatErrorPrefixed(err, n.Addr.String()), + ), + )) + } + + if diags.HasErrors() { + return nil, nil, deferred, keyData, diags + } + + if errs := objchange.AssertPlanValid(schema.Body, unmarkedPriorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { + if resp.LegacyTypeSystem { + // The shimming of the old type system in the legacy SDK is not precise + // enough to pass this consistency check, so we'll give it a pass here, + // but we will generate a warning about it so that we are more likely + // to notice in the logs if an inconsistency beyond the type system + // leads to a downstream provider failure. + var buf strings.Builder + fmt.Fprintf(&buf, + "[WARN] Provider %q produced an invalid plan for %s, but we are tolerating it because it is using the legacy plugin SDK.\n The following problems may be the cause of any confusing errors from downstream operations:", + n.ResolvedProvider.Provider, n.Addr, + ) + for _, err := range errs { + fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) + } + log.Print(buf.String()) + } else { + for _, err := range errs { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, tfdiags.FormatErrorPrefixed(err, n.Addr.String()), + ), + )) + } + return nil, nil, deferred, keyData, diags } - log.Print(buf.String()) - } else { - for _, err := range errs { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced invalid plan", - fmt.Sprintf( - "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider.Provider, tfdiags.FormatErrorPrefixed(err, n.Addr.String()), - ), - )) - } - return plan, state, keyData, diags } } @@ -881,129 +1083,50 @@ func (n *NodeAbstractResourceInstance) plan( // providers that we must accommodate the behavior for now, so for // ignore_changes to work at all on these values, we will revert the // ignored values once more. - plannedNewVal, ignoreChangeDiags = n.processIgnoreChanges(unmarkedPriorVal, plannedNewVal) + // A nil schema is passed to processIgnoreChanges to indicate that we + // don't want to fixup a config value according to the schema when + // ignoring "all", rather we are reverting provider imposed changes. + plannedNewVal, ignoreChangeDiags = n.processIgnoreChanges(unmarkedPriorVal, plannedNewVal, nil) diags = diags.Append(ignoreChangeDiags) if ignoreChangeDiags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } } - // Add the marks back to the planned new value -- this must happen after ignore changes - // have been processed + // Add the marks back to the planned new value -- this must happen after + // ignore changes have been processed. We add in the schema marks as well, + // to ensure that provider defined private attributes are marked correctly + // here. We remove the ephemeral marks, the provider is expected to return null + // for write-only attributes (the only place where ephemeral values are allowed). + // This is verified in objchange.AssertPlanValid already. unmarkedPlannedNewVal := plannedNewVal - if len(unmarkedPaths) > 0 { - plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + unmarkedPaths = marks.RemoveAll(unmarkedPaths, marks.Ephemeral) + + plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.Body.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { + plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths) } - // The provider produces a list of paths to attributes whose changes mean - // that we must replace rather than update an existing remote object. - // However, we only need to do that if the identified attributes _have_ - // actually changed -- particularly after we may have undone some of the - // changes in processIgnoreChanges -- so now we'll filter that list to - // include only where changes are detected. - reqRep := cty.NewPathSet() - if len(resp.RequiresReplace) > 0 { - for _, path := range resp.RequiresReplace { - if priorVal.IsNull() { - // If prior is null then we don't expect any RequiresReplace at all, - // because this is a Create action. - continue - } + writeOnlyPaths := schema.Body.WriteOnlyPaths(plannedNewVal, nil) - priorChangedVal, priorPathDiags := hcl.ApplyPath(unmarkedPriorVal, path, nil) - plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil) - if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() { - // This means the path was invalid in both the prior and new - // values, which is an error with the provider itself. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced invalid plan", - fmt.Sprintf( - "Provider %q has indicated \"requires replacement\" on %s for a non-existent attribute path %#v.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider.Provider, n.Addr, path, - ), - )) - continue - } - - // Make sure we have valid Values for both values. - // Note: if the opposing value was of the type - // cty.DynamicPseudoType, the type assigned here may not exactly - // match the schema. This is fine here, since we're only going to - // check for equality, but if the NullVal is to be used, we need to - // check the schema for th true type. - switch { - case priorChangedVal == cty.NilVal && plannedChangedVal == cty.NilVal: - // this should never happen without ApplyPath errors above - panic("requires replace path returned 2 nil values") - case priorChangedVal == cty.NilVal: - priorChangedVal = cty.NullVal(plannedChangedVal.Type()) - case plannedChangedVal == cty.NilVal: - plannedChangedVal = cty.NullVal(priorChangedVal.Type()) - } - - // Unmark for this value for the equality test. If only sensitivity has changed, - // this does not require an Update or Replace - unmarkedPlannedChangedVal, _ := plannedChangedVal.UnmarkDeep() - eqV := unmarkedPlannedChangedVal.Equals(priorChangedVal) - if !eqV.IsKnown() || eqV.False() { - reqRep.Add(path) - } - } - if diags.HasErrors() { - return plan, state, keyData, diags - } + reqRep, reqRepDiags := getRequiredReplaces(unmarkedPriorVal, unmarkedPlannedNewVal, writeOnlyPaths, resp.RequiresReplace, n.ResolvedProvider.Provider, n.Addr) + diags = diags.Append(reqRepDiags) + if diags.HasErrors() { + return nil, nil, deferred, keyData, diags } - // The user might also ask us to force replacing a particular resource - // instance, regardless of whether the provider thinks it needs replacing. - // For example, users typically do this if they learn a particular object - // has become degraded in an immutable infrastructure scenario and so - // replacing it with a new object is a viable repair path. - matchedForceReplace := false - for _, candidateAddr := range forceReplace { - if candidateAddr.Equal(n.Addr) { - matchedForceReplace = true - break + woPathSet := cty.NewPathSet(writeOnlyPaths...) + action, actionReason := getAction(n.Addr, unmarkedPriorVal, unmarkedPlannedNewVal, createBeforeDestroy, woPathSet, forceReplace, reqRep) + + if !plannedIdentity.IsNull() { + if !action.IsReplace() && action != plans.Create { + diags = diags.Append(n.validateIdentityKnown(plannedIdentity)) } - // For "force replace" purposes we require an exact resource instance - // address to match. If a user forgets to include the instance key - // for a multi-instance resource then it won't match here, but we - // have an earlier check in NodePlannableResource.Execute that should - // prevent us from getting here in that case. + diags = diags.Append(n.validateIdentity(plannedIdentity, schema.Identity)) } - - // Unmark for this test for value equality. - eqV := unmarkedPlannedNewVal.Equals(unmarkedPriorVal) - eq := eqV.IsKnown() && eqV.True() - - var action plans.Action - var actionReason plans.ResourceInstanceChangeActionReason - switch { - case priorVal.IsNull(): - action = plans.Create - case eq && !matchedForceReplace: - action = plans.NoOp - case matchedForceReplace || !reqRep.Empty(): - // If the user "forced replace" of this instance of if there are any - // "requires replace" paths left _after our filtering above_ then this - // is a replace action. - if createBeforeDestroy { - action = plans.CreateThenDelete - } else { - action = plans.DeleteThenCreate - } - switch { - case matchedForceReplace: - actionReason = plans.ResourceInstanceReplaceByRequest - case !reqRep.Empty(): - actionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate - } - default: - action = plans.Update - // "Delete" is never chosen here, because deletion plans are always - // created more directly elsewhere, such as in "orphan" handling. + if diags.HasErrors() { + return nil, nil, deferred, keyData, diags } if action.IsReplace() { @@ -1018,7 +1141,7 @@ func (n *NodeAbstractResourceInstance) plan( // The resulting change should show any computed attributes changing // from known prior values to unknown values, unless the provider is // able to predict new values for any of these computed attributes. - nullPriorVal := cty.NullVal(schema.ImpliedType()) + nullPriorVal := cty.NullVal(schema.Body.ImpliedType()) // Since there is no prior state to compare after replacement, we need // a new unmarked config from our original with no ignored values. @@ -1028,16 +1151,43 @@ func (n *NodeAbstractResourceInstance) plan( } // create a new proposed value from the null state and the config - proposedNewVal = objchange.ProposedNew(schema, nullPriorVal, unmarkedConfigVal) + proposedNewVal = objchange.ProposedNew(schema.Body, nullPriorVal, unmarkedConfigVal) - resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - Config: unmarkedConfigVal, - PriorState: nullPriorVal, - ProposedNewState: proposedNewVal, - PriorPrivate: plannedPrivate, - ProviderMeta: metaConfigVal, - }) + if n.override != nil { + // In this case, we are always creating the resource so we don't + // do any validation, and just call out to the mocking library. + override, overrideDiags := mocking.PlanComputedValuesForResource(proposedNewVal, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: !n.override.UseForPlan, + }, schema.Body) + resp = providers.PlanResourceChangeResponse{ + PlannedState: ephemeral.StripWriteOnlyAttributes(override, schema.Body), + Diagnostics: overrideDiags, + } + } else { + resp = provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + PriorState: nullPriorVal, + ProposedNewState: proposedNewVal, + PriorPrivate: plannedPrivate, + ProviderMeta: metaConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), + PriorIdentity: plannedIdentity, + }) + + // If we don't support deferrals, but the provider reports a deferral and does not + // emit any error level diagnostics, we should emit an error. + if resp.Deferred != nil && !deferralAllowed && !resp.Diagnostics.HasErrors() { + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(n.Addr)) + } + + if !resp.PlannedIdentity.IsNull() { + // On replace the identity is allowed to change and be unknown. + diags = diags.Append(n.validateIdentity(resp.PlannedIdentity, schema.Identity)) + } + } // We need to tread carefully here, since if there are any warnings // in here they probably also came out of our previous call to // PlanResourceChange above, and so we don't want to repeat them. @@ -1045,16 +1195,22 @@ func (n *NodeAbstractResourceInstance) plan( // append these new diagnostics if there's at least one error inside. if resp.Diagnostics.HasErrors() { diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } + + if deferred == nil && resp.Deferred != nil { + deferred = resp.Deferred + } + plannedNewVal = resp.PlannedState plannedPrivate = resp.PlannedPrivate + plannedIdentity = resp.PlannedIdentity if len(unmarkedPaths) > 0 { plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) } - for _, err := range plannedNewVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range plannedNewVal.Type().TestConformance(schema.Body.ImpliedType()) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid plan", @@ -1065,7 +1221,25 @@ func (n *NodeAbstractResourceInstance) plan( )) } if diags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags + } + + // Providers are supposed to return null values for all write-only attributes + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( + "Provider produced invalid plan", + func(path cty.Path) string { + return fmt.Sprintf( + "Provider %q returned a value for the write-only attribute \"%s%s\" during planning. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, tfdiags.FormatCtyPath(path), + ) + }, + plannedNewVal, + schema.Body, + ) + diags = diags.Append(writeOnlyDiags) + + if writeOnlyDiags.HasErrors() { + return nil, nil, deferred, keyData, diags } } @@ -1082,9 +1256,15 @@ func (n *NodeAbstractResourceInstance) plan( actionReason = plans.ResourceInstanceReplaceBecauseTainted } - // If we plan to write or delete sensitive paths from state, - // this is an Update action - if action == plans.NoOp && !marksEqual(unmarkedPaths, priorPaths) { + // If we plan to change the sensitivity on some portion of the value, this + // is an Update action even when the values are otherwise equal. + // + // The marks should be normalized by being applied to the new value. We + // don't try to compare the marks we had collected from the config and + // schema, because the act of applying marks to a value may result in + // slightly different marks. For example marks within a set transfer to the + // entire set, and are not saved on the individual elements. + if action == plans.NoOp && !valueMarksEqual(plannedNewVal, priorVal) { action = plans.Update } @@ -1105,62 +1285,91 @@ func (n *NodeAbstractResourceInstance) plan( // Call post-refresh hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostDiff(n.Addr, states.CurrentGen, action, priorVal, plannedNewVal) + return h.PostDiff(n.HookResourceIdentity(), addrs.NotDeposed, action, priorVal, plannedNewVal) })) if diags.HasErrors() { - return plan, state, keyData, diags + return nil, nil, deferred, keyData, diags } // Update our return plan - plan = &plans.ResourceInstanceChange{ + plan := &plans.ResourceInstanceChange{ Addr: n.Addr, PrevRunAddr: n.prevRunAddr(ctx), Private: plannedPrivate, ProviderAddr: n.ResolvedProvider, Change: plans.Change{ - Action: action, - Before: priorVal, + Action: action, + Before: priorVal, + BeforeIdentity: priorIdentity, // Pass the marked planned value through in our change // to propogate through evaluation. // Marks will be removed when encoding. - After: plannedNewVal, + After: plannedNewVal, + AfterIdentity: plannedIdentity, + GeneratedConfig: n.generatedConfigHCL, }, ActionReason: actionReason, RequiredReplace: reqRep, } // Update our return state - state = &states.ResourceInstanceObject{ + state := &states.ResourceInstanceObject{ // We use the special "planned" status here to note that this // object's value is not yet complete. Objects with this status // cannot be used during expression evaluation, so the caller // must _also_ record the returned change in the active plan, // which the expression evaluator will use in preference to this // incomplete value recorded in the state. - Status: states.ObjectPlanned, - Value: plannedNewVal, - Private: plannedPrivate, + Status: states.ObjectPlanned, + Value: plannedNewVal, + Private: plannedPrivate, + Identity: resp.PlannedIdentity, } - return plan, state, keyData, diags + return plan, state, deferred, keyData, diags } -func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value) (cty.Value, tfdiags.Diagnostics) { +func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { // ignore_changes only applies when an object already exists, since we // can't ignore changes to a thing we've not created yet. if prior.IsNull() { return config, nil } - ignoreChanges := traversalsToPaths(n.Config.Managed.IgnoreChanges) + ignoreChanges, keys := traversalsToPaths(n.Config.Managed.IgnoreChanges) ignoreAll := n.Config.Managed.IgnoreAllChanges if len(ignoreChanges) == 0 && !ignoreAll { return config, nil } + if ignoreAll { - return prior, nil + log.Printf("[TRACE] processIgnoreChanges: Ignoring all changes for %s", n.Addr) + + // Legacy providers need up to clean up their invalid plans and ensure + // no changes are passed though, but that also means making an invalid + // config with computed values. In that case we just don't supply a + // schema and return the prior val directly. + if schema == nil { + return prior, nil + } + + // If we are trying to ignore all attribute changes, we must filter + // computed attributes out from the prior state to avoid sending them + // to the provider as if they were included in the configuration. + ret, _ := cty.Transform(prior, func(path cty.Path, v cty.Value) (cty.Value, error) { + attr := schema.AttributeByPath(path) + if attr != nil && attr.Computed && !attr.Optional { + return cty.NullVal(v.Type()), nil + } + + return v, nil + }) + + return ret, nil } + log.Printf("[TRACE] processIgnoreChanges: Ignoring changes for %s at [%s]", n.Addr, strings.Join(keys, ", ")) + if prior.IsNull() || config.IsNull() { // Ignore changes doesn't apply when we're creating for the first time. // Proposed should never be null here, but if it is then we'll just let it be. @@ -1174,36 +1383,49 @@ func (n *NodeAbstractResource) processIgnoreChanges(prior, config cty.Value) (ct // Convert the hcl.Traversal values we get form the configuration to the // cty.Path values we need to operate on the cty.Values -func traversalsToPaths(traversals []hcl.Traversal) []cty.Path { +func traversalsToPaths(traversals []hcl.Traversal) ([]cty.Path, []string) { paths := make([]cty.Path, len(traversals)) + keys := make([]string, len(traversals)) for i, traversal := range traversals { - path := traversalToPath(traversal) + path, key := traversalToPath(traversal) paths[i] = path + keys[i] = key } - return paths + return paths, keys } -func traversalToPath(traversal hcl.Traversal) cty.Path { +func traversalToPath(traversal hcl.Traversal) (cty.Path, string) { path := make(cty.Path, len(traversal)) + var key strings.Builder for si, step := range traversal { switch ts := step.(type) { case hcl.TraverseRoot: path[si] = cty.GetAttrStep{ Name: ts.Name, } + key.WriteString(ts.Name) case hcl.TraverseAttr: path[si] = cty.GetAttrStep{ Name: ts.Name, } + key.WriteString(".") + key.WriteString(ts.Name) case hcl.TraverseIndex: path[si] = cty.IndexStep{ Key: ts.Key, } + if ts.Key.Type().IsPrimitiveType() { + key.WriteString("[") + key.WriteString(tfdiags.CompactValueStr(ts.Key)) + key.WriteString("]") + } else { + key.WriteString("[...]") + } default: panic(fmt.Sprintf("unsupported traversal step %#v", step)) } } - return path + return path, key.String() } func processIgnoreChangesIndividual(prior, config cty.Value, ignoreChangesPath []cty.Path) (cty.Value, tfdiags.Diagnostics) { @@ -1364,32 +1586,30 @@ func processIgnoreChangesIndividual(prior, config cty.Value, ignoreChangesPath [ // readDataSource handles everything needed to call ReadDataSource on the provider. // A previously evaluated configVal can be passed in, or a new one is generated // from the resource configuration. -func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal cty.Value) (cty.Value, tfdiags.Diagnostics) { +func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal cty.Value) (cty.Value, *providers.Deferred, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var newVal cty.Value + var deferred *providers.Deferred config := *n.Config + deferralAllowed := ctx.Deferrals().DeferralAllowed() provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) diags = diags.Append(err) if diags.HasErrors() { - return newVal, diags + return newVal, deferred, diags } - if providerSchema == nil { - diags = diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr)) - return newVal, diags - } - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type)) - return newVal, diags + return newVal, deferred, diags } metaConfigVal, metaDiags := n.providerMetas(ctx) diags = diags.Append(metaDiags) if diags.HasErrors() { - return newVal, diags + return newVal, deferred, diags } // Unmark before sending to provider, will re-mark before returning @@ -1405,7 +1625,7 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal ) diags = diags.Append(validateResp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { - return newVal, diags + return newVal, deferred, diags } // If we get down here then our configuration is complete and we're read @@ -1413,81 +1633,108 @@ func (n *NodeAbstractResourceInstance) readDataSource(ctx EvalContext, configVal log.Printf("[TRACE] readDataSource: %s configuration is complete, so reading from provider", n.Addr) diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreApply(n.Addr, states.CurrentGen, plans.Read, cty.NullVal(configVal.Type()), configVal) + return h.PreApply(n.HookResourceIdentity(), addrs.NotDeposed, plans.Read, cty.NullVal(configVal.Type()), configVal) })) if diags.HasErrors() { - return newVal, diags + return newVal, deferred, diags } - resp := provider.ReadDataSource(providers.ReadDataSourceRequest{ - TypeName: n.Addr.ContainingResource().Resource.Type, - Config: configVal, - ProviderMeta: metaConfigVal, - }) + var resp providers.ReadDataSourceResponse + if n.override != nil { + override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: false, + }, schema.Body) + resp = providers.ReadDataSourceResponse{ + State: ephemeral.StripWriteOnlyAttributes(override, schema.Body), + Diagnostics: overrideDiags, + } + } else { + resp = provider.ReadDataSource(providers.ReadDataSourceRequest{ + TypeName: n.Addr.ContainingResource().Resource.Type, + Config: configVal, + ProviderMeta: metaConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), + }) + + // If we don't support deferrals, but the provider reports a deferral and does not + // emit any error level diagnostics, we should emit an error. + if resp.Deferred != nil && !deferralAllowed && !resp.Diagnostics.HasErrors() { + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(n.Addr)) + } + + if resp.Deferred != nil { + deferred = resp.Deferred + } + } diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, n.Addr.String())) if diags.HasErrors() { - return newVal, diags + return newVal, deferred, diags } newVal = resp.State if newVal == cty.NilVal { // This can happen with incompletely-configured mocks. We'll allow it // and treat it as an alias for a properly-typed null value. - newVal = cty.NullVal(schema.ImpliedType()) + newVal = cty.NullVal(schema.Body.ImpliedType()) } - for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced invalid object", - fmt.Sprintf( - "Provider %q produced an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider, tfdiags.FormatErrorPrefixed(err, n.Addr.String()), - ), - )) - } - if diags.HasErrors() { - return newVal, diags - } + // We don't want to run the checks if the data source read is deferred + if deferred == nil { + for _, err := range newVal.Type().TestConformance(schema.Body.ImpliedType()) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid object", + fmt.Sprintf( + "Provider %q produced an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, tfdiags.FormatErrorPrefixed(err, n.Addr.String()), + ), + )) + } + if diags.HasErrors() { + return newVal, deferred, diags + } - if newVal.IsNull() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced null object", - fmt.Sprintf( - "Provider %q produced a null value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider, n.Addr, - ), - )) + if newVal.IsNull() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced null object", + fmt.Sprintf( + "Provider %q produced a null value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, + ), + )) + } + + if !newVal.IsNull() && !newVal.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid object", + fmt.Sprintf( + "Provider %q produced a value for %s that is not wholly known.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, + ), + )) + + // We'll still save the object, but we need to eliminate any unknown + // values first because we can't serialize them in the state file. + // Note that this may cause set elements to be coalesced if they + // differed only by having unknown values, but we don't worry about + // that here because we're saving the value only for inspection + // purposes; the error we added above will halt the graph walk. + newVal = cty.UnknownAsNull(newVal) + } } - - if !newVal.IsNull() && !newVal.IsWhollyKnown() { - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Error, - "Provider produced invalid object", - fmt.Sprintf( - "Provider %q produced a value for %s that is not wholly known.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", - n.ResolvedProvider, n.Addr, - ), - )) - - // We'll still save the object, but we need to eliminate any unknown - // values first because we can't serialize them in the state file. - // Note that this may cause set elements to be coalesced if they - // differed only by having unknown values, but we don't worry about - // that here because we're saving the value only for inspection - // purposes; the error we added above will halt the graph walk. - newVal = cty.UnknownAsNull(newVal) - } - - if len(pvm) > 0 { - newVal = newVal.MarkWithPaths(pvm) + newVal = newVal.MarkWithPaths(pvm) + if sensitivePaths := schema.Body.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) } diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostApply(n.Addr, states.CurrentGen, newVal, diags.Err()) + return h.PostApply(n.HookResourceIdentity(), addrs.NotDeposed, newVal, diags.Err()) })) - return newVal, diags + return newVal, deferred, diags } func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value, tfdiags.Diagnostics) { @@ -1498,13 +1745,10 @@ func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value if err != nil { return metaConfigVal, diags.Append(err) } - if providerSchema == nil { - return metaConfigVal, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr)) - } if n.ProviderMetas != nil { if m, ok := n.ProviderMetas[n.ResolvedProvider.Provider]; ok && m != nil { // if the provider doesn't support this feature, throw an error - if providerSchema.ProviderMeta == nil { + if providerSchema.ProviderMeta.Body == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", n.ResolvedProvider.Provider.String()), @@ -1513,7 +1757,7 @@ func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value }) } else { var configDiags tfdiags.Diagnostics - metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, providerSchema.ProviderMeta, nil, EvalDataForNoInstanceKey) + metaConfigVal, _, configDiags = ctx.EvaluateBlock(m.Config, providerSchema.ProviderMeta.Body, nil, EvalDataForNoInstanceKey) diags = diags.Append(configDiags) } } @@ -1530,31 +1774,48 @@ func (n *NodeAbstractResourceInstance) providerMetas(ctx EvalContext) (cty.Value // value, but it still matches the previous state, then we can record a NoNop // change. If the states don't match then we record a Read change so that the // new value is applied to the state. -func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRuleSeverity tfdiags.Severity) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) { +// +// The cases where a data source will generate a planned change instead +// of finishing during the plan are: +// +// - Its config has unknown values or it depends on a resource with pending changes. +// (Note that every data source that is DeferredPrereq should also fit this description.) +// - We attempted a read request, but the provider says we're deferred. +// - It's nested in a check block, and should always read again during apply. +func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRuleSeverity tfdiags.Severity, skipPlanChanges, dependencyDeferred bool) (*plans.ResourceInstanceChange, *states.ResourceInstanceObject, *providers.Deferred, instances.RepetitionData, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics var keyData instances.RepetitionData var configVal cty.Value + var deferred *providers.Deferred + if dependencyDeferred { + // If a dependency of this data source was deferred, then we're going + // to end up deferring this whatever happens. So, our default status + // is deferred. If the provider indicates this resource should be + // deferred for another reason, that reason should take priority over + // this one. + deferred = &providers.Deferred{ + Reason: providers.DeferredReasonDeferredPrereq, + } + } + _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) if err != nil { - return nil, nil, keyData, diags.Append(err) - } - if providerSchema == nil { - return nil, nil, keyData, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr)) + return nil, nil, deferred, keyData, diags.Append(err) } config := *n.Config - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type)) - return nil, nil, keyData, diags + return nil, nil, deferred, keyData, diags } - objTy := schema.ImpliedType() + objTy := schema.Body.ImpliedType() priorVal := cty.NullVal(objTy) - forEach, _ := evaluateForEachExpression(config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(config.ForEach, ctx, false) keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) checkDiags := evalCheckRules( @@ -1565,17 +1826,45 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule ) diags = diags.Append(checkDiags) if diags.HasErrors() { - return nil, nil, keyData, diags // failed preconditions prevent further evaluation + return nil, nil, deferred, keyData, diags // failed preconditions prevent further evaluation } var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema, nil, keyData) + configVal, _, configDiags = ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) - if configDiags.HasErrors() { - return nil, nil, keyData, diags + diags = diags.Append( + validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), + ) + if diags.HasErrors() { + return nil, nil, deferred, keyData, diags } + unmarkedConfigVal, unmarkedPaths := configVal.UnmarkDeepWithPaths() - unmarkedConfigVal, configMarkPaths := configVal.UnmarkDeepWithPaths() + check, nested := n.nestedInCheckBlock() + if nested { + // Going forward from this point, the only reason we will fail is + // that the data source fails to load its data. Normally, this would + // cancel the entire plan and this error message would bubble its way + // back up to the user. + // + // But, if we are in a check block then we don't want this data block to + // cause the plan to fail. We also need to report a status on the data + // block so the check processing later on knows whether to attempt to + // process the checks. Either we'll report the data block as failed + // if/when we load the data block later, or we want to report it as a + // success overall. + // + // Therefore, we create a deferred function here that will check if the + // status for the check has been updated yet, and if not we will set it + // to be StatusPass. The rest of this function will only update the + // status if it should be StatusFail. + defer func() { + status := ctx.Checks().ObjectCheckStatus(check.Addr().Absolute(n.Addr.Module)) + if status == checks.StatusUnknown { + ctx.Checks().ReportCheckResult(check.Addr().Absolute(n.Addr.Module), addrs.CheckDataResource, 0, checks.StatusPass) + } + }() + } configKnown := configVal.IsWhollyKnown() depsPending := n.dependenciesHavePendingChanges(ctx) @@ -1584,6 +1873,17 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule // producing a "Read" change for this resource, and a placeholder value for // it in the state. if depsPending || !configKnown { + // We can't plan any changes if we're only refreshing, so the only + // value we can set here is whatever was in state previously. + if skipPlanChanges { + plannedNewState := &states.ResourceInstanceObject{ + Value: priorVal, + Status: states.ObjectReady, + } + + return nil, plannedNewState, deferred, keyData, diags + } + var reason plans.ResourceInstanceChangeActionReason switch { case !configKnown: @@ -1597,8 +1897,15 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule reason = plans.ResourceInstanceReadBecauseDependencyPending } - proposedNewVal := objchange.PlannedDataResourceObject(schema, unmarkedConfigVal) - proposedNewVal = proposedNewVal.MarkWithPaths(configMarkPaths) + proposedNewVal := objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) + + // even though we are only returning the config value because we can't + // yet read the data source, we need to incorporate the schema marks so + // that downstream consumers can detect them when planning. + proposedNewVal = proposedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.Body.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { + proposedNewVal = marks.MarkPaths(proposedNewVal, marks.Sensitive, sensitivePaths) + } // Apply detects that the data source will need to be read by the After // value containing unknowns from PlanDataResourceObject. @@ -1620,26 +1927,136 @@ func (n *NodeAbstractResourceInstance) planDataSource(ctx EvalContext, checkRule } diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostDiff(n.Addr, states.CurrentGen, plans.Read, priorVal, proposedNewVal) + return h.PostDiff(n.HookResourceIdentity(), addrs.NotDeposed, plans.Read, priorVal, proposedNewVal) })) - return plannedChange, plannedNewState, keyData, diags + return plannedChange, plannedNewState, deferred, keyData, diags } // We have a complete configuration with no dependencies to wait on, so we // can read the data source into the state. - newVal, readDiags := n.readDataSource(ctx, configVal) + // newVal is fully marked by the readDataSource method. + newVal, readDeferred, readDiags := n.readDataSource(ctx, configVal) + + if readDeferred != nil { + // This will either be null or a value that indicates we're deferred + // because of a dependency. In both cases we're happy to just overwrite + // that with the more relevant information directly from the provider. + deferred = readDeferred + } + + // Now we've loaded the data, and diags tells us whether we were successful + // or not, we are going to create our plannedChange and our + // proposedNewState. + var plannedChange *plans.ResourceInstanceChange + var plannedNewState *states.ResourceInstanceObject + + // If we are a nested block, then we want to create a plannedChange that + // tells Terraform to reload the data block during the apply stage even if + // we managed to get the data now. + // Another consideration is that if we failed to load the data, we need to + // disguise that for a nested block. Nested blocks will report the overall + // check as failed but won't affect the rest of the plan operation or block + // an apply operation. + + if nested { + addr := check.Addr().Absolute(n.Addr.Module) + + // Let's fix things up for a nested data block. + // + // A nested data block doesn't error, and creates a planned change. So, + // if we encountered an error we'll tidy up newVal so it makes sense + // and handle the error. We'll also create the plannedChange if + // appropriate. + + if readDiags.HasErrors() { + // If we had errors, then we can cover that up by marking the new + // state as unknown. + newVal = objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) + + // not only do we want to ensure this synthetic value has the marks, + // but since this is the value being returned from the data source + // we need to ensure the schema marks are added as well. + newVal = newVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.Body.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) + } + + // We still want to report the check as failed even if we are still + // letting it run again during the apply stage. + ctx.Checks().ReportCheckFailure(addr, addrs.CheckDataResource, 0, readDiags.Err().Error()) + } + + // Any warning or error diagnostics we'll wrap with some special checks + // diagnostics. This is so we can identify them later, and so they'll + // only report as warnings. + readDiags = tfdiags.OverrideAll(readDiags, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addr, addrs.CheckDataResource, 0), + } + }) + + // refreshOnly plans cannot produce planned changes, so we only do + // this if skipPlanChanges is false. Conversely, provider-deferred data + // sources always generate a planned change with a different ActionReason. + if !skipPlanChanges && deferred == nil { + plannedChange = &plans.ResourceInstanceChange{ + Addr: n.Addr, + PrevRunAddr: n.prevRunAddr(ctx), + ProviderAddr: n.ResolvedProvider, + Change: plans.Change{ + Action: plans.Read, + Before: priorVal, + After: newVal, + }, + ActionReason: plans.ResourceInstanceReadBecauseCheckNested, + } + } + } + + // Provider-deferred data sources always generate a planned change. + if deferred != nil { + plannedChange = &plans.ResourceInstanceChange{ + Addr: n.Addr, + PrevRunAddr: n.prevRunAddr(ctx), + ProviderAddr: n.ResolvedProvider, + Change: plans.Change{ + Action: plans.Read, + Before: priorVal, + After: newVal, + }, + // The caller should be more interested in the deferral reason, but this + // action reason is a reasonable description of what's happening. + ActionReason: plans.ResourceInstanceReadBecauseDependencyPending, + } + + plannedNewState = &states.ResourceInstanceObject{ + Value: newVal, + Status: states.ObjectPlanned, + } + } + diags = diags.Append(readDiags) - if diags.HasErrors() { - return nil, nil, keyData, diags + if !diags.HasErrors() && deferred == nil { + // Finally, let's make our new state. + plannedNewState = &states.ResourceInstanceObject{ + Value: newVal, + Status: states.ObjectReady, + } } - plannedNewState := &states.ResourceInstanceObject{ - Value: newVal, - Status: states.ObjectReady, - } + return plannedChange, plannedNewState, deferred, keyData, diags +} - return nil, plannedNewState, keyData, diags +// nestedInCheckBlock determines if this resource is nested in a Check config +// block. If so, this resource will be loaded during both plan and apply +// operations to make sure the check is always giving the latest information. +func (n *NodeAbstractResourceInstance) nestedInCheckBlock() (*configs.Check, bool) { + if n.Config.Container != nil { + check, ok := n.Config.Container.(*configs.Check) + return check, ok + } + return nil, false } // dependenciesHavePendingChanges determines whether any managed resource the @@ -1707,10 +2124,6 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned if err != nil { return nil, keyData, diags.Append(err) } - if providerSchema == nil { - return nil, keyData, diags.Append(fmt.Errorf("provider schema not available for %s", n.Addr)) - } - if planned != nil && planned.Action != plans.Read && planned.Action != plans.NoOp { // If any other action gets in here then that's always a bug; this // EvalNode only deals with reading. @@ -1722,14 +2135,14 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned } config := *n.Config - schema, _ := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(n.Addr.ContainingResource().Resource) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here diags = diags.Append(fmt.Errorf("provider %q does not support data source %q", n.ResolvedProvider, n.Addr.ContainingResource().Resource.Type)) return nil, keyData, diags } - forEach, _ := evaluateForEachExpression(config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(config.ForEach, ctx, false) keyData = EvalDataForInstanceKey(n.Addr.Resource.Key, forEach) checkDiags := evalCheckRules( @@ -1741,7 +2154,7 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned diags = diags.Append(checkDiags) if diags.HasErrors() { diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostApply(n.Addr, states.CurrentGen, planned.Before, diags.Err()) + return h.PostApply(n.HookResourceIdentity(), addrs.NotDeposed, planned.Before, diags.Err()) })) return nil, keyData, diags // failed preconditions prevent further evaluation } @@ -1753,15 +2166,49 @@ func (n *NodeAbstractResourceInstance) applyDataSource(ctx EvalContext, planned return nil, keyData, diags } - configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData) + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { return nil, keyData, diags } - newVal, readDiags := n.readDataSource(ctx, configVal) + newVal, readDeferred, readDiags := n.readDataSource(ctx, configVal) + if check, nested := n.nestedInCheckBlock(); nested { + addr := check.Addr().Absolute(n.Addr.Module) + + // We're just going to jump in here and hide away any errors for nested + // data blocks. + if readDiags.HasErrors() { + ctx.Checks().ReportCheckFailure(addr, addrs.CheckDataResource, 0, readDiags.Err().Error()) + diags = diags.Append(tfdiags.OverrideAll(readDiags, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addr, addrs.CheckDataResource, 0), + } + })) + return nil, keyData, diags + } + + // Even though we know there are no errors here, we still want to + // identify these diags has having been generated from a check block. + readDiags = tfdiags.OverrideAll(readDiags, tfdiags.Warning, func() tfdiags.DiagnosticExtraWrapper { + return &addrs.CheckRuleDiagnosticExtra{ + CheckRule: addrs.NewCheckRule(addr, addrs.CheckDataResource, 0), + } + }) + + // If no errors, just remember to report this as a success and continue + // as normal. + ctx.Checks().ReportCheckResult(addr, addrs.CheckDataResource, 0, checks.StatusPass) + } + diags = diags.Append(readDiags) - if diags.HasErrors() { + if readDiags.HasErrors() { + return nil, keyData, diags + } + + if readDeferred != nil { + // Just skip data sources that are being deferred. Nothing, that + // references them should be calling them. return nil, keyData, diags } @@ -1795,7 +2242,15 @@ func (n *NodeAbstractResourceInstance) evalApplyProvisioners(ctx EvalContext, st return nil } - provs := filterProvisioners(n.Config, when) + var allProvs []*configs.Provisioner + switch { + case n.Config != nil && n.Config.Managed != nil: + allProvs = n.Config.Managed.Provisioners + case n.RemovedConfig != nil && n.RemovedConfig.Managed != nil: + allProvs = n.RemovedConfig.Managed.Provisioners + } + + provs := filterProvisioners(allProvs, when) if len(provs) == 0 { // We have no provisioners, so don't do anything return nil @@ -1803,7 +2258,7 @@ func (n *NodeAbstractResourceInstance) evalApplyProvisioners(ctx EvalContext, st // Call pre hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreProvisionInstance(n.Addr, state.Value) + return h.PreProvisionInstance(n.HookResourceIdentity(), state.Value) })) if diags.HasErrors() { return diags @@ -1819,24 +2274,19 @@ func (n *NodeAbstractResourceInstance) evalApplyProvisioners(ctx EvalContext, st // Call post hook return diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostProvisionInstance(n.Addr, state.Value) + return h.PostProvisionInstance(n.HookResourceIdentity(), state.Value) })) } // filterProvisioners filters the provisioners on the resource to only // the provisioners specified by the "when" option. -func filterProvisioners(config *configs.Resource, when configs.ProvisionerWhen) []*configs.Provisioner { - // Fast path the zero case - if config == nil || config.Managed == nil { +func filterProvisioners(configured []*configs.Provisioner, when configs.ProvisionerWhen) []*configs.Provisioner { + if len(configured) == 0 { return nil } - if len(config.Managed.Provisioners) == 0 { - return nil - } - - result := make([]*configs.Provisioner, 0, len(config.Managed.Provisioners)) - for _, p := range config.Managed.Provisioners { + result := make([]*configs.Provisioner, 0, len(configured)) + for _, p := range configured { if p.When == when { result = append(result, p) } @@ -1865,8 +2315,11 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state // then it'll serve as a base connection configuration for all of the // provisioners. var baseConn hcl.Body - if n.Config.Managed != nil && n.Config.Managed.Connection != nil { + switch { + case n.Config != nil && n.Config.Managed != nil && n.Config.Managed.Connection != nil: baseConn = n.Config.Managed.Connection.Config + case n.RemovedConfig != nil && n.RemovedConfig.Managed != nil && n.RemovedConfig.Managed.Connection != nil: + baseConn = n.RemovedConfig.Managed.Connection.Config } for _, prov := range provs { @@ -1929,7 +2382,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state { // Call pre hook err := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreProvisionInstanceStep(n.Addr, prov.Type) + return h.PreProvisionInstanceStep(n.HookResourceIdentity(), prov.Type) }) if err != nil { return diags.Append(err) @@ -1939,7 +2392,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state // The output function outputFn := func(msg string) { ctx.Hook(func(h Hook) (HookAction, error) { - h.ProvisionOutput(n.Addr, prov.Type, msg) + h.ProvisionOutput(n.HookResourceIdentity(), prov.Type, msg) return HookActionContinue, nil }) } @@ -1955,10 +2408,28 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state // provisioner logging, so we conservatively suppress all output in // this case. This should not apply to connection info values, which // provisioners ought not to be logging anyway. - if len(configMarks) > 0 { + _, sensitive := configMarks[marks.Sensitive] + _, ephemeral := configMarks[marks.Ephemeral] + + switch { + case sensitive && ephemeral: outputFn = func(msg string) { ctx.Hook(func(h Hook) (HookAction, error) { - h.ProvisionOutput(n.Addr, prov.Type, "(output suppressed due to sensitive value in config)") + h.ProvisionOutput(n.HookResourceIdentity(), prov.Type, "(output suppressed due to sensitive, ephemeral value in config)") + return HookActionContinue, nil + }) + } + case sensitive: + outputFn = func(msg string) { + ctx.Hook(func(h Hook) (HookAction, error) { + h.ProvisionOutput(n.HookResourceIdentity(), prov.Type, "(output suppressed due to sensitive value in config)") + return HookActionContinue, nil + }) + } + case ephemeral: + outputFn = func(msg string) { + ctx.Hook(func(h Hook) (HookAction, error) { + h.ProvisionOutput(n.HookResourceIdentity(), prov.Type, "(output suppressed due to ephemeral value in config)") return HookActionContinue, nil }) } @@ -1974,7 +2445,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state // Call post hook hookErr := ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostProvisionInstanceStep(n.Addr, prov.Type, applyDiags.Err()) + return h.PostProvisionInstanceStep(n.HookResourceIdentity(), prov.Type, applyDiags.Err()) }) switch prov.OnFailure { @@ -2005,7 +2476,7 @@ func (n *NodeAbstractResourceInstance) applyProvisioners(ctx EvalContext, state func (n *NodeAbstractResourceInstance) evalProvisionerConfig(ctx EvalContext, body hcl.Body, self cty.Value, schema *configschema.Block) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - forEach, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, forEachDiags := evaluateForEachExpression(n.Config.ForEach, ctx, false) diags = diags.Append(forEachDiags) keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) @@ -2026,7 +2497,7 @@ func (n *NodeAbstractResourceInstance) evalDestroyProvisionerConfig(ctx EvalCont // destroy-time provisioners. keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, nil) - evalScope := ctx.EvaluationScope(n.ResourceInstanceAddr().Resource, keyData) + evalScope := ctx.EvaluationScope(n.ResourceInstanceAddr().Resource, nil, keyData) config, evalDiags := evalScope.EvalSelfBlock(body, self, schema, keyData) diags = diags.Append(evalDiags) @@ -2034,44 +2505,38 @@ func (n *NodeAbstractResourceInstance) evalDestroyProvisionerConfig(ctx EvalCont } // apply accepts an applyConfig, instead of using n.Config, so destroy plans can -// send a nil config. Most of the errors generated in apply are returned as -// diagnostics, but if provider.ApplyResourceChange itself fails, that error is -// returned as an error and nil diags are returned. +// send a nil config. The keyData information can be empty if the config is +// nil, since it is only used to evaluate the configuration. func (n *NodeAbstractResourceInstance) apply( ctx EvalContext, state *states.ResourceInstanceObject, change *plans.ResourceInstanceChange, applyConfig *configs.Resource, - createBeforeDestroy bool) (*states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) { + keyData instances.RepetitionData, + createBeforeDestroy bool) (*states.ResourceInstanceObject, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - var keyData instances.RepetitionData if state == nil { state = &states.ResourceInstanceObject{} } - if applyConfig != nil { - forEach, _ := evaluateForEachExpression(applyConfig.ForEach, ctx) - keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) - } - if change.Action == plans.NoOp { // If this is a no-op change then we don't want to actually change // anything, so we'll just echo back the state we were given and // let our internal checks and updates proceed. log.Printf("[TRACE] NodeAbstractResourceInstance.apply: skipping %s because it has no planned action", n.Addr) - return state, keyData, diags + return state, diags } provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) if err != nil { - return nil, keyData, diags.Append(err) + return nil, diags.Append(err) } - schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type)) - return nil, keyData, diags + return nil, diags } log.Printf("[INFO] Starting apply for %s", n.Addr) @@ -2079,10 +2544,10 @@ func (n *NodeAbstractResourceInstance) apply( configVal := cty.NullVal(cty.DynamicPseudoType) if applyConfig != nil { var configDiags tfdiags.Diagnostics - configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData) + configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema.Body, nil, keyData) diags = diags.Append(configDiags) if configDiags.HasErrors() { - return nil, keyData, diags + return nil, diags } } @@ -2093,7 +2558,7 @@ func (n *NodeAbstractResourceInstance) apply( var unknownPaths []string cty.Transform(configVal, func(p cty.Path, v cty.Value) (cty.Value, error) { if !v.IsKnown() { - unknownPaths = append(unknownPaths, fmt.Sprintf("%#v", p)) + unknownPaths = append(unknownPaths, format.CtyPath(p)) } return v, nil }) @@ -2107,13 +2572,13 @@ func (n *NodeAbstractResourceInstance) apply( strings.Join(unknownPaths, "\n"), ), )) - return nil, keyData, diags + return nil, diags } metaConfigVal, metaDiags := n.providerMetas(ctx) diags = diags.Append(metaDiags) if diags.HasErrors() { - return nil, keyData, diags + return nil, diags } log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr, change.Action) @@ -2132,7 +2597,7 @@ func (n *NodeAbstractResourceInstance) apply( // persisted. eqV := unmarkedBefore.Equals(unmarkedAfter) eq := eqV.IsKnown() && eqV.True() - if change.Action == plans.Update && eq && !marksEqual(beforePaths, afterPaths) { + if change.Action == plans.Update && eq && !marks.MarksEqual(beforePaths, afterPaths) { // Copy the previous state, changing only the value newState := &states.ResourceInstanceObject{ CreateBeforeDestroy: state.CreateBeforeDestroy, @@ -2141,17 +2606,45 @@ func (n *NodeAbstractResourceInstance) apply( Status: state.Status, Value: change.After, } - return newState, keyData, diags + return newState, diags } - resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ - TypeName: n.Addr.Resource.Resource.Type, - PriorState: unmarkedBefore, - Config: unmarkedConfigVal, - PlannedState: unmarkedAfter, - PlannedPrivate: change.Private, - ProviderMeta: metaConfigVal, - }) + var resp providers.ApplyResourceChangeResponse + if n.override != nil { + // As with the planning stage, we only need to worry about computed + // values the first time the object is created. Otherwise, we're happy + // to just apply whatever the user asked for. + if change.Action == plans.Create { + override, overrideDiags := mocking.ApplyComputedValuesForResource(unmarkedAfter, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: false, + }, schema.Body) + resp = providers.ApplyResourceChangeResponse{ + NewState: override, + Diagnostics: overrideDiags, + } + } else { + resp = providers.ApplyResourceChangeResponse{ + NewState: unmarkedAfter, + } + } + } else { + resp = provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{ + TypeName: n.Addr.Resource.Resource.Type, + PriorState: unmarkedBefore, + Config: unmarkedConfigVal, + PlannedState: unmarkedAfter, + PlannedPrivate: change.Private, + ProviderMeta: metaConfigVal, + PlannedIdentity: change.AfterIdentity, + }) + + if !resp.NewIdentity.IsNull() { + diags = diags.Append(n.validateIdentityKnown(resp.NewIdentity)) + diags = diags.Append(n.validateIdentity(resp.NewIdentity, schema.Identity)) + } + } applyDiags := resp.Diagnostics if applyConfig != nil { applyDiags = applyDiags.InConfigBody(applyConfig.Config, n.Addr.String()) @@ -2165,11 +2658,6 @@ func (n *NodeAbstractResourceInstance) apply( // incomplete. newVal := resp.NewState - // If we have paths to mark, mark those on this new value - if len(afterPaths) > 0 { - newVal = newVal.MarkWithPaths(afterPaths) - } - if newVal == cty.NilVal { // Providers are supposed to return a partial new value even when errors // occur, but sometimes they don't and so in that case we'll patch that up @@ -2181,7 +2669,7 @@ func (n *NodeAbstractResourceInstance) apply( // we were trying to execute a delete, because the provider in this case // probably left the newVal unset intending it to be interpreted as "null". if change.After.IsNull() { - newVal = cty.NullVal(schema.ImpliedType()) + newVal = cty.NullVal(schema.Body.ImpliedType()) } if !diags.HasErrors() { @@ -2197,7 +2685,7 @@ func (n *NodeAbstractResourceInstance) apply( } var conformDiags tfdiags.Diagnostics - for _, err := range newVal.Type().TestConformance(schema.ImpliedType()) { + for _, err := range newVal.Type().TestConformance(schema.Body.ImpliedType()) { conformDiags = conformDiags.Append(tfdiags.Sourceless( tfdiags.Error, "Provider produced invalid object", @@ -2212,7 +2700,25 @@ func (n *NodeAbstractResourceInstance) apply( // Bail early in this particular case, because an object that doesn't // conform to the schema can't be saved in the state anyway -- the // serializer will reject it. - return nil, keyData, diags + return nil, diags + } + + // Providers are supposed to return null values for all write-only attributes + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( + "Provider produced invalid object", + func(path cty.Path) string { + return fmt.Sprintf( + "Provider %q returned a value for the write-only attribute \"%s%s\" after apply. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, tfdiags.FormatCtyPath(path), + ) + }, + newVal, + schema.Body, + ) + diags = diags.Append(writeOnlyDiags) + + if writeOnlyDiags.HasErrors() { + return nil, diags } // After this point we have a type-conforming result object and so we @@ -2255,6 +2761,15 @@ func (n *NodeAbstractResourceInstance) apply( newVal = cty.UnknownAsNull(newVal) } + // If we have paths to mark, mark those on this new value we need to + // re-check the value against the schema, because nested computed values + // won't be included in afterPaths, which are only what was read from the + // After plan value. + newVal = newVal.MarkWithPaths(afterPaths) + if sensitivePaths := schema.Body.SensitivePaths(newVal, nil); len(sensitivePaths) != 0 { + newVal = marks.MarkPaths(newVal, marks.Sensitive, sensitivePaths) + } + if change.Action != plans.Delete && !diags.HasErrors() { // Only values that were marked as unknown in the planned value are allowed // to change during the apply operation. (We do this after the unknown-ness @@ -2265,7 +2780,7 @@ func (n *NodeAbstractResourceInstance) apply( // a pass since the other errors are usually the explanation for // this one and so it's more helpful to let the user focus on the // root cause rather than distract with this extra problem. - if errs := objchange.AssertObjectCompatible(schema, change.After, newVal); len(errs) > 0 { + if errs := objchange.AssertObjectCompatible(schema.Body, change.After, newVal); len(errs) > 0 { if resp.LegacyTypeSystem { // The shimming of the old type system in the legacy SDK is not precise // enough to pass this consistency check, so we'll give it a pass here, @@ -2338,7 +2853,7 @@ func (n *NodeAbstractResourceInstance) apply( // prior state as the new value, making this effectively a no-op. If // the item really _has_ been deleted then our next refresh will detect // that and fix it up. - return state.DeepCopy(), keyData, diags + return state.DeepCopy(), diags case diags.HasErrors() && !newVal.IsNull(): // if we have an error, make sure we restore the object status in the new state @@ -2355,7 +2870,7 @@ func (n *NodeAbstractResourceInstance) apply( newState.Dependencies = state.Dependencies } - return newState, keyData, diags + return newState, diags case !newVal.IsNull(): // Non error case with a new state @@ -2364,12 +2879,13 @@ func (n *NodeAbstractResourceInstance) apply( Value: newVal, Private: resp.Private, CreateBeforeDestroy: createBeforeDestroy, + Identity: resp.NewIdentity, } - return newState, keyData, diags + return newState, diags default: // Non error case, were the object was deleted - return nil, keyData, diags + return nil, diags } } @@ -2377,7 +2893,199 @@ func (n *NodeAbstractResourceInstance) prevRunAddr(ctx EvalContext) addrs.AbsRes return resourceInstancePrevRunAddr(ctx, n.Addr) } +func (n *NodeAbstractResourceInstance) validateIdentityKnown(newIdentity cty.Value) (diags tfdiags.Diagnostics) { + if !newIdentity.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q returned an identity with unknown values for %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr, + ), + )) + } + + return diags +} + +func (n *NodeAbstractResourceInstance) validateIdentity(newIdentity cty.Value, identitySchema *configschema.Object) (diags tfdiags.Diagnostics) { + if _, marks := newIdentity.UnmarkDeep(); len(marks) > 0 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q returned an identity with marks for %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr, + ), + )) + } + + // The identity schema is always a single object, so we can check the + // nesting type here. + if identitySchema.Nesting != configschema.NestingSingle { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid identity", + fmt.Sprintf( + "Provider %q returned an identity with a nesting type of %s for %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, identitySchema.Nesting, n.Addr, + ), + )) + } + + newType := newIdentity.Type() + currentType := identitySchema.ImpliedType() + if errs := newType.TestConformance(currentType); len(errs) > 0 { + for _, err := range errs { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced an identity that doesn't match the schema", + fmt.Sprintf( + "Provider %q returned an identity for %s that doesn't match the identity schema: %s. \n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider.Provider, n.Addr, tfdiags.FormatError(err), + ), + )) + } + return diags + } + + return diags +} + func resourceInstancePrevRunAddr(ctx EvalContext, currentAddr addrs.AbsResourceInstance) addrs.AbsResourceInstance { table := ctx.MoveResults() return table.OldAddr(currentAddr) } + +func getAction(addr addrs.AbsResourceInstance, priorVal, plannedNewVal cty.Value, createBeforeDestroy bool, writeOnly cty.PathSet, forceReplace []addrs.AbsResourceInstance, reqRep cty.PathSet) (action plans.Action, actionReason plans.ResourceInstanceChangeActionReason) { + // The user might also ask us to force replacing a particular resource + // instance, regardless of whether the provider thinks it needs replacing. + // For example, users typically do this if they learn a particular object + // has become degraded in an immutable infrastructure scenario and so + // replacing it with a new object is a viable repair path. + matchedForceReplace := false + for _, candidateAddr := range forceReplace { + if candidateAddr.Equal(addr) { + matchedForceReplace = true + break + } + + // For "force replace" purposes we require an exact resource instance + // address to match. If a user forgets to include the instance key + // for a multi-instance resource then it won't match here, but we + // have an earlier check in NodePlannableResource.Execute that should + // prevent us from getting here in that case. + } + + // Unmark for this test for value equality. + eqV := plannedNewVal.Equals(priorVal) + eq := eqV.IsKnown() && eqV.True() + + switch { + case priorVal.IsNull(): + action = plans.Create + case matchedForceReplace || !reqRep.Empty() || !writeOnly.Intersection(reqRep).Empty(): + // If the user "forced replace" of this instance of if there are any + // "requires replace" paths left _after our filtering above_ then this + // is a replace action. + if createBeforeDestroy { + action = plans.CreateThenDelete + } else { + action = plans.DeleteThenCreate + } + switch { + case matchedForceReplace: + actionReason = plans.ResourceInstanceReplaceByRequest + case !reqRep.Empty(): + actionReason = plans.ResourceInstanceReplaceBecauseCannotUpdate + } + case eq && !matchedForceReplace: + action = plans.NoOp + default: + action = plans.Update + // "Delete" is never chosen here, because deletion plans are always + // created more directly elsewhere, such as in "orphan" handling. + } + + return +} + +// getRequiredReplaces returns a list of paths to attributes whose changes mean +// that we must replace rather than update an existing remote object. +// +// The provider produces a list of paths to attributes whose changes mean +// that we must replace rather than update an existing remote object. +// However, we only need to do that if the identified attributes _have_ +// actually changed -- particularly after we may have undone some of the +// changes in processIgnoreChanges -- so now we'll filter that list to +// include only where changes are detected. +// +// Both the priorVal and plannedNewVal should be unmarked before calling this +// function. This function exposes nothing about the priorVal or plannedVal +// except for the paths that require replacement which can be deduced from the +// type with or without marks. +func getRequiredReplaces(priorVal, plannedNewVal cty.Value, writeOnly []cty.Path, requiredReplaces []cty.Path, providerAddr tfaddr.Provider, addr addrs.AbsResourceInstance) (cty.PathSet, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + reqRep := cty.NewPathSet() + if len(requiredReplaces) > 0 { + for _, path := range requiredReplaces { + if priorVal.IsNull() { + // If prior is null then we don't expect any RequiresReplace at all, + // because this is a Create action. + continue + } + + priorChangedVal, priorPathDiags := hcl.ApplyPath(priorVal, path, nil) + plannedChangedVal, plannedPathDiags := hcl.ApplyPath(plannedNewVal, path, nil) + if plannedPathDiags.HasErrors() && priorPathDiags.HasErrors() { + // This means the path was invalid in both the prior and new + // values, which is an error with the provider itself. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q has indicated \"requires replacement\" on %s for a non-existent attribute path %#v.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + providerAddr, addr, path, + ), + )) + continue + } + + // Make sure we have valid Values for both values. + // Note: if the opposing value was of the type + // cty.DynamicPseudoType, the type assigned here may not exactly + // match the schema. This is fine here, since we're only going to + // check for equality, but if the NullVal is to be used, we need to + // check the schema for th true type. + switch { + case priorChangedVal == cty.NilVal && plannedChangedVal == cty.NilVal: + // this should never happen without ApplyPath errors above + panic("requires replace path returned 2 nil values") + case priorChangedVal == cty.NilVal: + priorChangedVal = cty.NullVal(plannedChangedVal.Type()) + case plannedChangedVal == cty.NilVal: + plannedChangedVal = cty.NullVal(priorChangedVal.Type()) + } + + eqV := plannedChangedVal.Equals(priorChangedVal) + + // if attribute/path is writeOnly we have no values to compare + // but still respect the required replacement + isWriteOnly := false + for _, woPath := range writeOnly { + if path.Equals(woPath) { + isWriteOnly = true + } + } + if !eqV.IsKnown() || eqV.False() || isWriteOnly { + reqRep.Add(path) + } + } + if diags.HasErrors() { + return reqRep, diags + } + } + + return reqRep, diags +} diff --git a/internal/terraform/node_resource_abstract_instance_test.go b/internal/terraform/node_resource_abstract_instance_test.go index dca1cb52f0..79810b81a1 100644 --- a/internal/terraform/node_resource_abstract_instance_test.go +++ b/internal/terraform/node_resource_abstract_instance_test.go @@ -1,14 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) func TestNodeAbstractResourceInstanceProvider(t *testing.T) { @@ -125,9 +131,10 @@ func TestNodeAbstractResourceInstanceProvider(t *testing.T) { // function. (This would not be valid for some other functions.) Addr: test.Addr, NodeAbstractResource: NodeAbstractResource{ - Config: test.Config, + Addr: test.Addr.ConfigResource(), + Config: test.Config, + storedProviderConfig: test.StoredProviderConfig, }, - storedProviderConfig: test.StoredProviderConfig, } got := node.Provider() if got != test.Want { @@ -141,7 +148,7 @@ func TestNodeAbstractResourceInstance_WriteResourceInstanceState(t *testing.T) { state := states.NewState() ctx := new(MockEvalContext) ctx.StateState = state.SyncWrapper() - ctx.PathPath = addrs.RootModuleInstance + ctx.Scope = evalContextModuleInstance{Addr: addrs.RootModuleInstance} mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -167,7 +174,7 @@ func TestNodeAbstractResourceInstance_WriteResourceInstanceState(t *testing.T) { }, } ctx.ProviderProvider = mockProvider - ctx.ProviderSchemaSchema = mockProvider.ProviderSchema() + ctx.ProviderSchemaSchema = mockProvider.GetProviderSchema() err := node.writeResourceInstanceState(ctx, obj, workingState) if err != nil { @@ -180,3 +187,66 @@ aws_instance.foo: provider = provider["registry.terraform.io/hashicorp/aws"] `) } + +func TestNodeAbstractResourceInstance_refresh_with_deferred_read(t *testing.T) { + state := states.NewState() + evalCtx := &MockEvalContext{} + evalCtx.StateState = state.SyncWrapper() + evalCtx.Scope = evalContextModuleInstance{Addr: addrs.RootModuleInstance} + + mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Optional: true, + }, + }, + }) + mockProvider.ConfigureProviderCalled = true + + mockProvider.ReadResourceFn = func(providers.ReadResourceRequest) providers.ReadResourceResponse { + return providers.ReadResourceResponse{ + NewState: cty.ObjectVal(map[string]cty.Value{ + "id": cty.UnknownVal(cty.String), + }), + Deferred: &providers.Deferred{ + Reason: providers.DeferredReasonAbsentPrereq, + }, + } + } + + obj := &states.ResourceInstanceObject{ + Value: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("i-abc123"), + }), + Status: states.ObjectReady, + } + + node := &NodeAbstractResourceInstance{ + Addr: mustResourceInstanceAddr("aws_instance.foo"), + NodeAbstractResource: NodeAbstractResource{ + ResolvedProvider: mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + }, + } + evalCtx.ProviderProvider = mockProvider + evalCtx.ProviderSchemaSchema = mockProvider.GetProviderSchema() + evalCtx.DeferralsState = deferring.NewDeferred(true) + + rio, deferred, diags := node.refresh(evalCtx, states.NotDeposed, obj, true) + if diags.HasErrors() { + t.Fatal(diags.Err()) + } + + value := rio.Value + if value.IsWhollyKnown() { + t.Fatalf("value was known: %v", value) + } + + if deferred == nil { + t.Fatalf("expected deferral to be present") + } + + if deferred.Reason != providers.DeferredReasonAbsentPrereq { + t.Fatalf("expected deferral to be AbsentPrereq, got %s", deferred.Reason) + } +} diff --git a/internal/terraform/node_resource_abstract_test.go b/internal/terraform/node_resource_abstract_test.go index 352cc1bb6a..cd196128f2 100644 --- a/internal/terraform/node_resource_abstract_test.go +++ b/internal/terraform/node_resource_abstract_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -112,6 +115,73 @@ func TestNodeAbstractResourceProvider(t *testing.T) { } } +// Make sure ProvideBy returns the final resolved provider +func TestNodeAbstractResourceSetProvider(t *testing.T) { + node := &NodeAbstractResource{ + + // Just enough NodeAbstractResource for the Provider function. + // (This would not be valid for some other functions.) + Addr: addrs.Resource{ + Mode: addrs.DataResourceMode, + Type: "terraform_remote_state", + Name: "baz", + }.InModule(addrs.RootModule), + Config: &configs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "terraform_remote_state", + Name: "baz", + // Just enough configs.Resource for the Provider method. Not + // actually valid for general use. + Provider: addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "awesomecorp", + Type: "happycloud", + }, + }, + } + + p, exact := node.ProvidedBy() + if exact { + t.Fatalf("no exact provider should be found from this confniguration, got %q\n", p) + } + + // the implied non-exact provider should be "terraform" + lpc, ok := p.(addrs.LocalProviderConfig) + if !ok { + t.Fatalf("expected LocalProviderConfig, got %#v\n", p) + } + + if lpc.LocalName != "terraform" { + t.Fatalf("expected non-exact provider of 'terraform', got %q", lpc.LocalName) + } + + // now set a resolved provider for the resource + resolved := addrs.AbsProviderConfig{ + Provider: addrs.Provider{ + Hostname: addrs.DefaultProviderRegistryHost, + Namespace: "awesomecorp", + Type: "happycloud", + }, + Module: addrs.RootModule, + Alias: "test", + } + + node.SetProvider(resolved) + p, exact = node.ProvidedBy() + if !exact { + t.Fatalf("exact provider should be found, got %q\n", p) + } + + apc, ok := p.(addrs.AbsProviderConfig) + if !ok { + t.Fatalf("expected AbsProviderConfig, got %#v\n", p) + } + + if apc.String() != resolved.String() { + t.Fatalf("incorrect resolved config: got %#v, wanted %#v\n", apc, resolved) + } +} + func TestNodeAbstractResource_ReadResourceInstanceState(t *testing.T) { mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ Attributes: map[string]*configschema.Attribute{ @@ -159,8 +229,8 @@ func TestNodeAbstractResource_ReadResourceInstanceState(t *testing.T) { t.Run(k, func(t *testing.T) { ctx := new(MockEvalContext) ctx.StateState = test.State.SyncWrapper() - ctx.PathPath = addrs.RootModuleInstance - ctx.ProviderSchemaSchema = mockProvider.ProviderSchema() + ctx.Scope = evalContextModuleInstance{Addr: addrs.RootModuleInstance} + ctx.ProviderSchemaSchema = mockProvider.GetProviderSchema() ctx.ProviderProvider = providers.Interface(mockProvider) @@ -224,8 +294,8 @@ func TestNodeAbstractResource_ReadResourceInstanceStateDeposed(t *testing.T) { t.Run(k, func(t *testing.T) { ctx := new(MockEvalContext) ctx.StateState = test.State.SyncWrapper() - ctx.PathPath = addrs.RootModuleInstance - ctx.ProviderSchemaSchema = mockProvider.ProviderSchema() + ctx.Scope = evalContextModuleInstance{Addr: addrs.RootModuleInstance} + ctx.ProviderSchemaSchema = mockProvider.GetProviderSchema() ctx.ProviderProvider = providers.Interface(mockProvider) key := states.DeposedKey("00000001") // shim from legacy state assigns 0th deposed index this key diff --git a/internal/terraform/node_resource_apply.go b/internal/terraform/node_resource_apply.go index 3928bea0fd..e587ecc1a3 100644 --- a/internal/terraform/node_resource_apply.go +++ b/internal/terraform/node_resource_apply.go @@ -1,11 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "log" - "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -15,97 +15,148 @@ import ( // NodeApplyableResource nodes into their respective modules. type nodeExpandApplyableResource struct { *NodeAbstractResource + + PartialExpansions []addrs.PartialExpandedResource } var ( - _ GraphNodeDynamicExpandable = (*nodeExpandApplyableResource)(nil) _ GraphNodeReferenceable = (*nodeExpandApplyableResource)(nil) _ GraphNodeReferencer = (*nodeExpandApplyableResource)(nil) _ GraphNodeConfigResource = (*nodeExpandApplyableResource)(nil) _ GraphNodeAttachResourceConfig = (*nodeExpandApplyableResource)(nil) - _ graphNodeExpandsInstances = (*nodeExpandApplyableResource)(nil) _ GraphNodeTargetable = (*nodeExpandApplyableResource)(nil) + _ GraphNodeDynamicExpandable = (*nodeExpandApplyableResource)(nil) ) -func (n *nodeExpandApplyableResource) expandsInstances() { -} - func (n *nodeExpandApplyableResource) References() []*addrs.Reference { - return (&NodeApplyableResource{NodeAbstractResource: n.NodeAbstractResource}).References() + refs := n.NodeAbstractResource.References() + + // The expand node needs to connect to the individual resource instances it + // references, but cannot refer to it's own instances without causing + // cycles. It would be preferable to entirely disallow self references + // without the `self` identifier, but those were allowed in provisioners + // for compatibility with legacy configuration. We also can't always just + // filter them out for all resource node types, because the only method we + // have for catching certain invalid configurations are the cycles that + // result from these inter-instance references. + return filterSelfRefs(n.Addr.Resource, refs) } func (n *nodeExpandApplyableResource) Name() string { return n.NodeAbstractResource.Name() + " (expand)" } -func (n *nodeExpandApplyableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { +func (n *nodeExpandApplyableResource) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { + if n.Addr.Resource.Mode == addrs.EphemeralResourceMode { + return n.dynamicExpandEphemeral(ctx) + } + + var diags tfdiags.Diagnostics + expander := ctx.InstanceExpander() + moduleInstances := expander.ExpandModule(n.Addr.Module, false) + for _, module := range moduleInstances { + moduleCtx := evalContextForModuleInstance(ctx, module) + diags = diags.Append(n.recordResourceData(moduleCtx, n.Addr.Resource.Absolute(module))) + } + + return nil, diags +} + +// We need to expand the ephemeral resources mostly the same as we do during +// planning. There a lot of options than happen during planning which aren't +// applicable to apply however, and we have to make sure we don't re-register +// checks which already recorded in the plan, so we create a pared down version +// of the plan expansion here. +func (n *nodeExpandApplyableResource) dynamicExpandEphemeral(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics var g Graph expander := ctx.InstanceExpander() - moduleInstances := expander.ExpandModule(n.Addr.Module) + moduleInstances := expander.ExpandModule(n.Addr.Module, false) + for _, module := range moduleInstances { - g.Add(&NodeApplyableResource{ - NodeAbstractResource: n.NodeAbstractResource, - Addr: n.Addr.Resource.Absolute(module), - }) + resAddr := n.Addr.Resource.Absolute(module) + expDiags := n.expandEphemeralResourceInstances(ctx, resAddr, &g) + diags = diags.Append(expDiags) } - return &g, nil + addRootNodeToGraph(&g) + + return &g, diags } -// NodeApplyableResource represents a resource that is "applyable": -// it may need to have its record in the state adjusted to match configuration. -// -// Unlike in the plan walk, this resource node does not DynamicExpand. Instead, -// it should be inserted into the same graph as any instances of the nodes -// with dependency edges ensuring that the resource is evaluated before any -// of its instances, which will turn ensure that the whole-resource record -// in the state is suitably prepared to receive any updates to instances. -type NodeApplyableResource struct { - *NodeAbstractResource +func (n *nodeExpandApplyableResource) expandEphemeralResourceInstances(globalCtx EvalContext, resAddr addrs.AbsResource, g *Graph) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics - Addr addrs.AbsResource -} + // The rest of our work here needs to know which module instance it's + // working in, so that it can evaluate expressions in the appropriate scope. + moduleCtx := evalContextForModuleInstance(globalCtx, resAddr.Module) -var ( - _ GraphNodeModuleInstance = (*NodeApplyableResource)(nil) - _ GraphNodeConfigResource = (*NodeApplyableResource)(nil) - _ GraphNodeExecutable = (*NodeApplyableResource)(nil) - _ GraphNodeProviderConsumer = (*NodeApplyableResource)(nil) - _ GraphNodeAttachResourceConfig = (*NodeApplyableResource)(nil) - _ GraphNodeReferencer = (*NodeApplyableResource)(nil) -) - -func (n *NodeApplyableResource) Path() addrs.ModuleInstance { - return n.Addr.Module -} - -func (n *NodeApplyableResource) References() []*addrs.Reference { - if n.Config == nil { - log.Printf("[WARN] NodeApplyableResource %q: no configuration, so can't determine References", dag.VertexName(n)) - return nil + // writeResourceState is responsible for informing the expander of what + // repetition mode this resource has, which allows expander.ExpandResource + // to work below. + moreDiags := n.recordResourceData(moduleCtx, resAddr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return diags } - var result []*addrs.Reference + expander := moduleCtx.InstanceExpander() + instanceAddrs := expander.ExpandResource(resAddr) - // Since this node type only updates resource-level metadata, we only - // need to worry about the parts of the configuration that affect - // our "each mode": the count and for_each meta-arguments. - refs, _ := lang.ReferencesInExpr(n.Config.Count) - result = append(result, refs...) - refs, _ = lang.ReferencesInExpr(n.Config.ForEach) - result = append(result, refs...) - - return result + instG, instDiags := n.ephemeralResourceInstanceSubgraph(resAddr, instanceAddrs) + if instDiags.HasErrors() { + diags = diags.Append(instDiags) + return diags + } + g.Subsume(&instG.AcyclicGraph.Graph) + return diags } -// GraphNodeExecutable -func (n *NodeApplyableResource) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { - if n.Config == nil { - // Nothing to do, then. - log.Printf("[TRACE] NodeApplyableResource: no configuration present for %s", n.Name()) - return nil +func (n *nodeExpandApplyableResource) ephemeralResourceInstanceSubgraph(addr addrs.AbsResource, instanceAddrs []addrs.AbsResourceInstance) (*Graph, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + concreteEphemeral := func(a *NodeAbstractResourceInstance) dag.Vertex { + a.Config = n.Config + a.ResolvedProvider = n.ResolvedProvider + a.Schema = n.Schema + a.ProvisionerSchemas = n.ProvisionerSchemas + a.ProviderMetas = n.ProviderMetas + a.dependsOn = n.dependsOn + + // we still need the Plannable resource instance + return &NodeApplyableResourceInstance{ + NodeAbstractResourceInstance: a, + } } - return n.writeResourceState(ctx, n.Addr) + // Start creating the steps + steps := []GraphTransformer{ + // Expand the count or for_each (if present) + &ResourceCountTransformer{ + Concrete: concreteEphemeral, + Schema: n.Schema, + Addr: n.ResourceAddr(), + InstanceAddrs: instanceAddrs, + }, + + // Targeting + &TargetsTransformer{Targets: n.Targets}, + + // Connect references so ordering is correct + &ReferenceTransformer{}, + + // Make sure there is a single root + &RootTransformer{}, + } + + // Build the graph + b := &BasicGraphBuilder{ + Steps: steps, + Name: "nodeExpandApplyEphemeralResource", + } + graph, graphDiags := b.Build(addr.Module) + diags = diags.Append(graphDiags) + + return graph, diags } diff --git a/internal/terraform/node_resource_apply_deferred.go b/internal/terraform/node_resource_apply_deferred.go new file mode 100644 index 0000000000..1a51172784 --- /dev/null +++ b/internal/terraform/node_resource_apply_deferred.go @@ -0,0 +1,80 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// nodeApplyableDeferredInstance is a node that represents a deferred instance +// in the apply graph. This node is targetable and helps maintain the correct +// ordering of the apply graph. +// +// When executed during the apply phase, this transfers the planned change we +// got from the plan's deferrals *back* into the EvalContext's Deferred struct, +// so downstream references to deferred objects can get partial values (of +// varying quality). +type nodeApplyableDeferredInstance struct { + *NodeAbstractResourceInstance + + Reason providers.DeferredReason + ChangeSrc *plans.ResourceInstanceChangeSrc +} + +func (n *nodeApplyableDeferredInstance) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + if n.Schema == nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to decode", "Terraform failed to decode a deferred change due to the schema not being present. This is a bug in Terraform; please report it!")) + return diags + } + + change, err := n.ChangeSrc.Decode(*n.Schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to decode ", fmt.Sprintf("Terraform failed to decode a deferred change: %v\n\nThis is a bug in Terraform; please report it!", err))) + } + + switch n.Addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, n.Reason, change) + case addrs.DataResourceMode: + ctx.Deferrals().ReportDataSourceInstanceDeferred(n.Addr, n.Reason, change) + } + return diags +} + +// nodeApplyableDeferredPartialInstance is a node that represents a deferred +// partial instance in the apply graph. This simply adds a method to get the +// partial address on top of the regular behaviour of +// nodeApplyableDeferredInstance. +type nodeApplyableDeferredPartialInstance struct { + *nodeApplyableDeferredInstance + + PartialAddr addrs.PartialExpandedResource +} + +func (n *nodeApplyableDeferredPartialInstance) Execute(ctx EvalContext, _ walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + if n.Schema == nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to decode", "Terraform failed to decode a deferred change due to the schema not being present. This is a bug in Terraform; please report it!")) + return diags + } + + change, err := n.ChangeSrc.Decode(*n.Schema) + if err != nil { + diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "Failed to decode ", fmt.Sprintf("Terraform failed to decode a deferred change: %v\n\nThis is a bug in Terraform; please report it!", err))) + } + + switch n.Addr.Resource.Resource.Mode { + case addrs.ManagedResourceMode: + ctx.Deferrals().ReportResourceExpansionDeferred(n.PartialAddr, change) + case addrs.DataResourceMode: + ctx.Deferrals().ReportDataSourceExpansionDeferred(n.PartialAddr, change) + } + return diags +} diff --git a/internal/terraform/node_resource_apply_instance.go b/internal/terraform/node_resource_apply_instance.go index 7fc6e6fc2e..b5670f971e 100644 --- a/internal/terraform/node_resource_apply_instance.go +++ b/internal/terraform/node_resource_apply_instance.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,8 +9,10 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/plans/objchange" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -24,10 +29,6 @@ type NodeApplyableResourceInstance struct { graphNodeDeposer // implementation of GraphNodeDeposerConfig - // If this node is forced to be CreateBeforeDestroy, we need to record that - // in the state to. - ForceCreateBeforeDestroy bool - // forceReplace are resource instance addresses where the user wants to // force generating a replace action. This set isn't pre-filtered, so // it might contain addresses that have nothing to do with the resource @@ -45,24 +46,6 @@ var ( _ GraphNodeAttachDependencies = (*NodeApplyableResourceInstance)(nil) ) -// CreateBeforeDestroy returns this node's CreateBeforeDestroy status. -func (n *NodeApplyableResourceInstance) CreateBeforeDestroy() bool { - if n.ForceCreateBeforeDestroy { - return n.ForceCreateBeforeDestroy - } - - if n.Config != nil && n.Config.Managed != nil { - return n.Config.Managed.CreateBeforeDestroy - } - - return false -} - -func (n *NodeApplyableResourceInstance) ModifyCreateBeforeDestroy(v bool) error { - n.ForceCreateBeforeDestroy = v - return nil -} - // GraphNodeCreator func (n *NodeApplyableResourceInstance) CreateAddr() *addrs.AbsResourceInstance { addr := n.ResourceInstanceAddr() @@ -113,11 +96,16 @@ func (n *NodeApplyableResourceInstance) Execute(ctx EvalContext, op walkOperatio addr := n.ResourceInstanceAddr() if n.Config == nil { - // This should not be possible, but we've got here in at least one - // case as discussed in the following issue: - // https://github.com/hashicorp/terraform/issues/21258 - // To avoid an outright crash here, we'll instead return an explicit - // error. + // If there is no config, and there is no change, then we have nothing + // to do and the change was left in the plan for informational + // purposes only. + changes := ctx.Changes() + csrc := changes.GetResourceInstanceChange(n.ResourceInstanceAddr(), addrs.NotDeposed) + if csrc == nil || csrc.Action == plans.NoOp { + log.Printf("[DEBUG] NodeApplyableResourceInstance: No config or planned change recorded for %s", n.Addr) + return nil + } + diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Resource node has no configuration attached", @@ -135,11 +123,23 @@ func (n *NodeApplyableResourceInstance) Execute(ctx EvalContext, op walkOperatio return n.managedResourceExecute(ctx) case addrs.DataResourceMode: return n.dataResourceExecute(ctx) + case addrs.EphemeralResourceMode: + return n.ephemeralResourceExecute(ctx) default: panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode)) } } +func (n *NodeApplyableResourceInstance) ephemeralResourceExecute(ctx EvalContext) tfdiags.Diagnostics { + _, diags := ephemeralResourceOpen(ctx, ephemeralResourceInput{ + addr: n.Addr, + config: n.Config, + providerConfig: n.ResolvedProvider, + }) + + return diags +} + func (n *NodeApplyableResourceInstance) dataResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) diags = diags.Append(err) @@ -265,12 +265,23 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) // Make a new diff, in case we've learned new values in the state // during apply which we can now incorporate. - diffApply, _, _, planDiags := n.plan(ctx, diff, state, false, n.forceReplace) + diffApply, _, deferred, repeatData, planDiags := n.plan(ctx, diff, state, false, n.forceReplace) diags = diags.Append(planDiags) if diags.HasErrors() { return diags } + if deferred != nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource deferred during apply, but not during plan", + fmt.Sprintf( + "Terraform has encountered a bug where a provider would mark the resource %q as deferred during apply, but not during plan. This is most likely a bug in the provider. Please file an issue with the provider.", n.Addr, + ), + )) + return diags + } + // Compare the diffs diags = diags.Append(n.checkPlannedChange(ctx, diff, diffApply, providerSchema)) if diags.HasErrors() { @@ -290,7 +301,14 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) return diags } - state, repeatData, applyDiags := n.apply(ctx, state, diffApply, n.Config, n.CreateBeforeDestroy()) + // If there is no change, there was nothing to apply, and we don't need to + // re-write the state, but we do need to re-evaluate postconditions. + if diffApply.Action == plans.NoOp { + return diags.Append(n.managedResourcePostconditions(ctx, repeatData)) + } + + state, applyDiags := n.apply(ctx, state, diffApply, n.Config, repeatData, n.CreateBeforeDestroy()) + diags = diags.Append(applyDiags) // We clear the change out here so that future nodes don't see a change @@ -365,15 +383,18 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) // _after_ writing the state because we want to check against // the result of the operation, and to fail on future operations // until the user makes the condition succeed. + return diags.Append(n.managedResourcePostconditions(ctx, repeatData)) +} + +func (n *NodeApplyableResourceInstance) managedResourcePostconditions(ctx EvalContext, repeatData instances.RepetitionData) (diags tfdiags.Diagnostics) { + checkDiags := evalCheckRules( addrs.ResourcePostcondition, n.Config.Postconditions, ctx, n.ResourceInstanceAddr(), repeatData, tfdiags.Error, ) - diags = diags.Append(checkDiags) - - return diags + return diags.Append(checkDiags) } // checkPlannedChange produces errors if the _actual_ expected value is not @@ -382,12 +403,12 @@ func (n *NodeApplyableResourceInstance) managedResourceExecute(ctx EvalContext) // Errors here are most often indicative of a bug in the provider, so our error // messages will report with that in mind. It's also possible that there's a bug // in Terraform's Core's own "proposed new value" code in EvalDiff. -func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plannedChange, actualChange *plans.ResourceInstanceChange, providerSchema *ProviderSchema) tfdiags.Diagnostics { +func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plannedChange, actualChange *plans.ResourceInstanceChange, providerSchema providers.ProviderSchema) tfdiags.Diagnostics { var diags tfdiags.Diagnostics addr := n.ResourceInstanceAddr().Resource - schema, _ := providerSchema.SchemaForResourceAddr(addr.ContainingResource()) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(addr.ContainingResource()) + if schema.Body == nil { // Should be caught during validation, so we don't bother with a pretty error here diags = diags.Append(fmt.Errorf("provider does not support %q", addr.Resource.Type)) return diags @@ -429,7 +450,7 @@ func (n *NodeApplyableResourceInstance) checkPlannedChange(ctx EvalContext, plan } } - errs := objchange.AssertObjectCompatible(schema, plannedChange.After, actualChange.After) + errs := objchange.AssertObjectCompatible(schema.Body, plannedChange.After, actualChange.After) for _, err := range errs { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, diff --git a/internal/terraform/node_resource_apply_test.go b/internal/terraform/node_resource_apply_test.go index 4fd9949544..def3461efb 100644 --- a/internal/terraform/node_resource_apply_test.go +++ b/internal/terraform/node_resource_apply_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -9,33 +12,40 @@ import ( "github.com/hashicorp/terraform/internal/states" ) -func TestNodeApplyableResourceExecute(t *testing.T) { +func TestNodeExpandApplyableResourceExecute(t *testing.T) { state := states.NewState() - ctx := &MockEvalContext{ - StateState: state.SyncWrapper(), - InstanceExpanderExpander: instances.NewExpander(), - } - t.Run("no config", func(t *testing.T) { - node := NodeApplyableResource{ + ctx := &MockEvalContext{ + StateState: state.SyncWrapper(), + InstanceExpanderExpander: instances.NewExpander(nil), + } + + node := &nodeExpandApplyableResource{ NodeAbstractResource: &NodeAbstractResource{ + Addr: mustConfigResourceAddr("test_instance.foo"), Config: nil, }, - Addr: mustAbsResourceAddr("test_instance.foo"), } - diags := node.Execute(ctx, walkApply) + _, diags := node.DynamicExpand(ctx) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } + + state.PruneResourceHusks() if !state.Empty() { t.Fatalf("expected no state, got:\n %s", state.String()) } }) t.Run("simple", func(t *testing.T) { + ctx := &MockEvalContext{ + StateState: state.SyncWrapper(), + InstanceExpanderExpander: instances.NewExpander(nil), + } - node := NodeApplyableResource{ + node := &nodeExpandApplyableResource{ NodeAbstractResource: &NodeAbstractResource{ + Addr: mustConfigResourceAddr("test_instance.foo"), Config: &configs.Resource{ Mode: addrs.ManagedResourceMode, Type: "test_instance", @@ -46,12 +56,12 @@ func TestNodeApplyableResourceExecute(t *testing.T) { Module: addrs.RootModule, }, }, - Addr: mustAbsResourceAddr("test_instance.foo"), } - diags := node.Execute(ctx, walkApply) + _, diags := node.DynamicExpand(ctx) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } + if state.Empty() { t.Fatal("expected resources in state, got empty state") } diff --git a/internal/terraform/node_resource_destroy.go b/internal/terraform/node_resource_destroy.go index d0cc7276bb..5e037a4aaf 100644 --- a/internal/terraform/node_resource_destroy.go +++ b/internal/terraform/node_resource_destroy.go @@ -1,9 +1,13 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" + "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/tfdiags" @@ -16,11 +20,6 @@ import ( // destroyed. type NodeDestroyResourceInstance struct { *NodeAbstractResourceInstance - - // If DeposedKey is set to anything other than states.NotDeposed then - // this node destroys a deposed object of the associated instance - // rather than its current object. - DeposedKey states.DeposedKey } var ( @@ -37,9 +36,6 @@ var ( ) func (n *NodeDestroyResourceInstance) Name() string { - if n.DeposedKey != states.NotDeposed { - return fmt.Sprintf("%s (destroy deposed %s)", n.ResourceInstanceAddr(), n.DeposedKey) - } return n.ResourceInstanceAddr().String() + " (destroy)" } @@ -61,7 +57,7 @@ func (n *NodeDestroyResourceInstance) DestroyAddr() *addrs.AbsResourceInstance { func (n *NodeDestroyResourceInstance) CreateBeforeDestroy() bool { // State takes precedence during destroy. // If the resource was removed, there is no config to check. - // If CBD was forced from descendent, it should be saved in the state + // If CBD was forced from descendant, it should be saved in the state // already. if s := n.instanceState; s != nil { if s.Current != nil { @@ -210,7 +206,7 @@ func (n *NodeDestroyResourceInstance) managedResourceExecute(ctx EvalContext) (d // Managed resources need to be destroyed, while data sources // are only removed from state. // we pass a nil configuration to apply because we are destroying - s, _, d := n.apply(ctx, state, changeApply, nil, false) + s, d := n.apply(ctx, state, changeApply, nil, instances.RepetitionData{}, false) state, diags = s, diags.Append(d) // we don't return immediately here on error, so that the state can be // finalized diff --git a/internal/terraform/node_resource_destroy_deposed.go b/internal/terraform/node_resource_destroy_deposed.go index 2c042386a3..2fdf6fccfa 100644 --- a/internal/terraform/node_resource_destroy_deposed.go +++ b/internal/terraform/node_resource_destroy_deposed.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,7 +9,9 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -22,11 +27,11 @@ type GraphNodeDeposedResourceInstanceObject interface { // NodePlanDeposedResourceInstanceObject represents deposed resource // instance objects during plan. These are distinct from the primary object -// for each resource instance since the only valid operation to do with them -// is to destroy them. +// for each resource instance since they must be either destroyed or forgotten. // // This node type is also used during the refresh walk to ensure that the -// record of a deposed object is up-to-date before we plan to destroy it. +// record of a deposed object is up to date before we plan to destroy or forget +// it. type NodePlanDeposedResourceInstanceObject struct { *NodeAbstractResourceInstance DeposedKey states.DeposedKey @@ -37,6 +42,14 @@ type NodePlanDeposedResourceInstanceObject struct { // skipPlanChanges indicates we should skip trying to plan change actions // for any instances. skipPlanChanges bool + + // forgetResources lists resources that should not be destroyed, only removed + // from state. + forgetResources []addrs.ConfigResource + + // forgetModules lists modules that should not be destroyed, only removed + // from state. + forgetModules []addrs.Module } var ( @@ -48,8 +61,13 @@ var ( _ GraphNodeExecutable = (*NodePlanDeposedResourceInstanceObject)(nil) _ GraphNodeProviderConsumer = (*NodePlanDeposedResourceInstanceObject)(nil) _ GraphNodeProvisionerConsumer = (*NodePlanDeposedResourceInstanceObject)(nil) + _ GraphNodeDestroyer = (*NodePlanDeposedResourceInstanceObject)(nil) ) +func (n *NodePlanDeposedResourceInstanceObject) DestroyAddr() *addrs.AbsResourceInstance { + return &n.Addr +} + func (n *NodePlanDeposedResourceInstanceObject) Name() string { return fmt.Sprintf("%s (deposed %s)", n.ResourceInstanceAddr().String(), n.DeposedKey) } @@ -75,6 +93,8 @@ func (n *NodePlanDeposedResourceInstanceObject) References() []*addrs.Reference func (n *NodePlanDeposedResourceInstanceObject) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { log.Printf("[TRACE] NodePlanDeposedResourceInstanceObject: planning %s deposed object %s", n.Addr, n.DeposedKey) + var deferred *providers.Deferred + // Read the state for the deposed resource instance state, err := n.readResourceInstanceStateDeposed(ctx, n.Addr, n.DeposedKey) diags = diags.Append(err) @@ -95,49 +115,84 @@ func (n *NodePlanDeposedResourceInstanceObject) Execute(ctx EvalContext, op walk return diags } + var forget bool + for _, ft := range n.forgetResources { + if ft.Equal(n.ResourceAddr()) { + forget = true + } + } + for _, fm := range n.forgetModules { + if fm.TargetContains(n.Addr) { + forget = true + } + } + // We don't refresh during the planDestroy walk, since that is only adding // the destroy changes to the plan and the provider will not be configured // at this point. The other nodes use separate types for plan and destroy, - // while deposed instances are always a destroy operation, so the logic - // here is a bit overloaded. - if !n.skipRefresh && op != walkPlanDestroy { - // Refresh this object even though it is going to be destroyed, in + // while deposed instances are always a destroy or forget operation, so the + // logic here is a bit overloaded. + // + // We also don't refresh when forgetting instances, as it is unnecessary. + if !n.skipRefresh && op != walkPlanDestroy && !forget { + // Refresh this object even though it may be destroyed, in // case it's already been deleted outside of Terraform. If this is a // normal plan, providers expect a Read request to remove missing // resources from the plan before apply, and may not handle a missing // resource during Delete correctly. If this is a simple refresh, // Terraform is expected to remove the missing resource from the state // entirely - refreshedState, refreshDiags := n.refresh(ctx, n.DeposedKey, state) + refreshedState, refreshDeferred, refreshDiags := n.refresh(ctx, n.DeposedKey, state, ctx.Deferrals().DeferralAllowed()) diags = diags.Append(refreshDiags) if diags.HasErrors() { return diags } - diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, refreshedState, refreshState)) - if diags.HasErrors() { - return diags - } + state = refreshedState // If we refreshed then our subsequent planning should be in terms of // the new object, not the original object. - state = refreshedState + if refreshDeferred == nil { + diags = diags.Append(n.writeResourceInstanceStateDeposed(ctx, n.DeposedKey, refreshedState, refreshState)) + if diags.HasErrors() { + return diags + } + } else { + deferred = refreshDeferred + } } if !n.skipPlanChanges { var change *plans.ResourceInstanceChange - change, destroyPlanDiags := n.planDestroy(ctx, state, n.DeposedKey) - diags = diags.Append(destroyPlanDiags) + var pDiags tfdiags.Diagnostics + var planDeferred *providers.Deferred + if forget { + change, pDiags = n.planForget(ctx, state, n.DeposedKey) + } else { + change, planDeferred, pDiags = n.planDestroy(ctx, state, n.DeposedKey) + } + diags = diags.Append(pDiags) if diags.HasErrors() { return diags } + if deferred == nil { + // Only set the plan deferral if it wasn't already due to be + // deferred from the refresh step. + deferred = planDeferred + } + + if deferred != nil { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, deferred.Reason, change) + return diags + } + // NOTE: We don't check prevent_destroy for deposed objects, even // though we would do so here for a "current" object, because // if we've reached a point where an object is already deposed then // we've already planned and partially-executed a create_before_destroy // replace and we would've checked prevent_destroy at that point. We're - // now just need to get the deposed object destroyed, because there + // now just need to get the deposed object removed, because there // should be a new object already serving as its replacement. diags = diags.Append(n.writeChange(ctx, change, n.DeposedKey)) @@ -176,6 +231,7 @@ var ( _ GraphNodeExecutable = (*NodeDestroyDeposedResourceInstanceObject)(nil) _ GraphNodeProviderConsumer = (*NodeDestroyDeposedResourceInstanceObject)(nil) _ GraphNodeProvisionerConsumer = (*NodeDestroyDeposedResourceInstanceObject)(nil) + _ GraphNodeDestroyer = (*NodeDestroyDeposedResourceInstanceObject)(nil) ) func (n *NodeDestroyDeposedResourceInstanceObject) Name() string { @@ -236,12 +292,20 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w return diags } - change, destroyPlanDiags := n.planDestroy(ctx, state, n.DeposedKey) + change, deferred, destroyPlanDiags := n.planDestroy(ctx, state, n.DeposedKey) diags = diags.Append(destroyPlanDiags) if diags.HasErrors() { return diags } + if deferred != nil { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, deferred.Reason, change) + return diags + } else if ctx.Deferrals().ShouldDeferResourceInstanceChanges(n.Addr, n.Dependencies) { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, providers.DeferredReasonDeferredPrereq, change) + return diags + } + // Call pre-apply hook diags = diags.Append(n.preApplyHook(ctx, change)) if diags.HasErrors() { @@ -249,7 +313,7 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w } // we pass a nil configuration to apply because we are destroying - state, _, applyDiags := n.apply(ctx, state, change, nil, false) + state, applyDiags := n.apply(ctx, state, change, nil, instances.RepetitionData{}, false) diags = diags.Append(applyDiags) // don't return immediately on errors, we need to handle the state @@ -267,6 +331,78 @@ func (n *NodeDestroyDeposedResourceInstanceObject) Execute(ctx EvalContext, op w return diags.Append(updateStateHook(ctx)) } +// NodeForgetDeposedResourceInstanceObject represents deposed resource +// instance objects during apply. Nodes of this type are inserted by +// DiffTransformer when the planned changeset contains "forget" changes for +// deposed instance objects, and its only supported operation is to forget the +// associated object. +type NodeForgetDeposedResourceInstanceObject struct { + *NodeAbstractResourceInstance + DeposedKey states.DeposedKey +} + +var ( + _ GraphNodeDeposedResourceInstanceObject = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeConfigResource = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeResourceInstance = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeReferenceable = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeReferencer = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeExecutable = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeProviderConsumer = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeProvisionerConsumer = (*NodeForgetDeposedResourceInstanceObject)(nil) + _ GraphNodeDestroyer = (*NodeForgetDeposedResourceInstanceObject)(nil) +) + +func (n *NodeForgetDeposedResourceInstanceObject) Name() string { + return fmt.Sprintf("%s (forget deposed %s)", n.ResourceInstanceAddr(), n.DeposedKey) +} + +func (n *NodeForgetDeposedResourceInstanceObject) DestroyAddr() *addrs.AbsResourceInstance { + return &n.Addr +} + +func (n *NodeForgetDeposedResourceInstanceObject) DeposedInstanceObjectKey() states.DeposedKey { + return n.DeposedKey +} + +// GraphNodeReferenceable implementation, overriding the one from NodeAbstractResourceInstance +func (n *NodeForgetDeposedResourceInstanceObject) ReferenceableAddrs() []addrs.Referenceable { + // Deposed objects don't participate in references. + return nil +} + +// GraphNodeReferencer implementation, overriding the one from NodeAbstractResourceInstance +func (n *NodeForgetDeposedResourceInstanceObject) References() []*addrs.Reference { + // We don't evaluate configuration for deposed objects, so they effectively + // make no references. + return nil +} + +// GraphNodeExecutable impl. +func (n *NodeForgetDeposedResourceInstanceObject) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { + // Read the state for the deposed resource instance + state, err := n.readResourceInstanceStateDeposed(ctx, n.Addr, n.DeposedKey) + if err != nil { + return diags.Append(err) + } + + if state == nil { + diags = diags.Append(fmt.Errorf("missing deposed state for %s (%s)", n.Addr, n.DeposedKey)) + return diags + } + + _, forgetPlanDiags := n.planForget(ctx, state, n.DeposedKey) + diags = diags.Append(forgetPlanDiags) + if diags.HasErrors() { + return diags + } + + ctx.State().ForgetResourceInstanceDeposed(n.Addr, n.DeposedKey) + + diags = diags.Append(updateStateHook(ctx)) + return diags +} + // GraphNodeDeposer is an optional interface implemented by graph nodes that // might create a single new deposed object for a specific associated resource // instance, allowing a caller to optionally pre-allocate a DeposedKey for @@ -310,19 +446,16 @@ func (n *NodeDestroyDeposedResourceInstanceObject) writeResourceInstanceState(ct if err != nil { return err } - if providerSchema == nil { - // Should never happen, unless our state object is nil - panic("writeResourceInstanceStateDeposed used with no ProviderSchema object") - } - schema, currentVersion := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) - if schema == nil { + schema := providerSchema.SchemaForResourceAddr(absAddr.ContainingResource().Resource) + if schema.Body == nil { // It shouldn't be possible to get this far in any real scenario // without a schema, but we might end up here in contrived tests that // fail to set up their world properly. return fmt.Errorf("failed to encode %s in state: no resource type schema available", absAddr) } - src, err := obj.Encode(schema.ImpliedType(), currentVersion) + + src, err := obj.Encode(schema) if err != nil { return fmt.Errorf("failed to encode %s in state: %s", absAddr, err) } diff --git a/internal/terraform/node_resource_destroy_deposed_test.go b/internal/terraform/node_resource_destroy_deposed_test.go index f173002a28..681f62c4c2 100644 --- a/internal/terraform/node_resource_destroy_deposed_test.go +++ b/internal/terraform/node_resource_destroy_deposed_test.go @@ -1,14 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) { @@ -37,13 +42,15 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) { PrevRunStateState: state.DeepCopy().SyncWrapper(), RefreshStateState: state.DeepCopy().SyncWrapper(), ProviderProvider: p, - ProviderSchemaSchema: &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + ProviderSchemaSchema: providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Computed: true, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, }, }, }, @@ -74,7 +81,7 @@ func TestNodePlanDeposedResourceInstanceObject_Execute(t *testing.T) { } change := ctx.Changes().GetResourceInstanceChange(absResource, deposedKey) - if got, want := change.ChangeSrc.Action, plans.Delete; got != want { + if got, want := change.Change.Action, plans.Delete; got != want { t.Fatalf("wrong planned action\ngot: %s\nwant: %s", got, want) } } @@ -93,13 +100,15 @@ func TestNodeDestroyDeposedResourceInstanceObject_Execute(t *testing.T) { mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), ) - schema := &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + schema := providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "test_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Computed: true, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, }, }, }, @@ -108,7 +117,7 @@ func TestNodeDestroyDeposedResourceInstanceObject_Execute(t *testing.T) { p := testProvider("test") p.ConfigureProvider(providers.ConfigureProviderRequest{}) - p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(schema) + p.GetProviderSchemaResponse = &schema p.UpgradeResourceStateResponse = &providers.UpgradeResourceStateResponse{ UpgradedState: cty.ObjectVal(map[string]cty.Value{ @@ -120,6 +129,7 @@ func TestNodeDestroyDeposedResourceInstanceObject_Execute(t *testing.T) { ProviderProvider: p, ProviderSchemaSchema: schema, ChangesChanges: plans.NewChanges().SyncWrapper(), + DeferralsState: deferring.NewDeferred(false), } node := NodeDestroyDeposedResourceInstanceObject{ @@ -146,7 +156,7 @@ func TestNodeDestroyDeposedResourceInstanceObject_WriteResourceInstanceState(t * state := states.NewState() ctx := new(MockEvalContext) ctx.StateState = state.SyncWrapper() - ctx.PathPath = addrs.RootModuleInstance + ctx.Scope = evalContextModuleInstance{Addr: addrs.RootModuleInstance} mockProvider := mockProviderWithResourceTypeSchema("aws_instance", &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "id": { @@ -156,7 +166,7 @@ func TestNodeDestroyDeposedResourceInstanceObject_WriteResourceInstanceState(t * }, }) ctx.ProviderProvider = mockProvider - ctx.ProviderSchemaSchema = mockProvider.ProviderSchema() + ctx.ProviderSchemaSchema = mockProvider.GetProviderSchema() obj := &states.ResourceInstanceObject{ Value: cty.ObjectVal(map[string]cty.Value{ @@ -191,7 +201,7 @@ func TestNodeDestroyDeposedResourceInstanceObject_ExecuteMissingState(t *testing ctx := &MockEvalContext{ StateState: states.NewState().SyncWrapper(), ProviderProvider: simpleMockProvider(), - ProviderSchemaSchema: p.ProviderSchema(), + ProviderSchemaSchema: p.GetProviderSchema(), ChangesChanges: plans.NewChanges().SyncWrapper(), } diff --git a/internal/terraform/node_resource_ephemeral.go b/internal/terraform/node_resource_ephemeral.go new file mode 100644 index 0000000000..7910326978 --- /dev/null +++ b/internal/terraform/node_resource_ephemeral.go @@ -0,0 +1,303 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "context" + "fmt" + "log" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/objchange" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/resources/ephemeral" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +type ephemeralResourceInput struct { + addr addrs.AbsResourceInstance + config *configs.Resource + providerConfig addrs.AbsProviderConfig +} + +// ephemeralResourceOpen implements the "open" step of the ephemeral resource +// instance lifecycle, which behaves the same way in both the plan and apply +// walks. +func ephemeralResourceOpen(ctx EvalContext, inp ephemeralResourceInput) (*providers.Deferred, tfdiags.Diagnostics) { + log.Printf("[TRACE] ephemeralResourceOpen: opening %s", inp.addr) + var diags tfdiags.Diagnostics + + provider, providerSchema, err := getProvider(ctx, inp.providerConfig) + if err != nil { + diags = diags.Append(err) + return nil, diags + } + + config := inp.config + schema := providerSchema.SchemaForResourceAddr(inp.addr.ContainingResource().Resource) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append( + fmt.Errorf("provider %q does not support ephemeral resource %q", + inp.providerConfig, inp.addr.ContainingResource().Resource.Type, + ), + ) + return nil, diags + } + + rId := HookResourceIdentity{ + Addr: inp.addr, + ProviderAddr: inp.providerConfig.Provider, + } + + ephemerals := ctx.EphemeralResources() + allInsts := ctx.InstanceExpander() + keyData := allInsts.GetResourceInstanceRepetitionData(inp.addr) + + checkDiags := evalCheckRules( + addrs.ResourcePrecondition, + config.Preconditions, + ctx, inp.addr, keyData, + tfdiags.Error, + ) + diags = diags.Append(checkDiags) + if diags.HasErrors() { + return nil, diags // failed preconditions prevent further evaluation + } + + configVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema.Body, nil, keyData) + diags = diags.Append(configDiags) + if diags.HasErrors() { + return nil, diags + } + unmarkedConfigVal, configMarks := configVal.UnmarkDeepWithPaths() + + if !unmarkedConfigVal.IsWhollyKnown() { + log.Printf("[DEBUG] ehpemeralResourceOpen: configuration for %s contains unknown values, cannot open resource", inp.addr) + + // We don't know what the result will be, but we need to keep the + // configured attributes for consistent evaluation. We can use the same + // technique we used for data sources to create the plan-time value. + unknownResult := objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) + // add back any configured marks + unknownResult = unknownResult.MarkWithPaths(configMarks) + // and mark the entire value as ephemeral, since it's coming from an ephemeral context. + unknownResult = unknownResult.Mark(marks.Ephemeral) + + // The state of ephemerals all comes from the registered instances, so + // we still need to register something so evaluation doesn't fail. + ephemerals.RegisterInstance(ctx.StopCtx(), inp.addr, ephemeral.ResourceInstanceRegistration{ + Value: unknownResult, + ConfigBody: config.Config, + }) + + ctx.Hook(func(h Hook) (HookAction, error) { + // ephemeral resources aren't stored in the plan, so use a hook to + // give some feedback to the user that this can't be opened + return h.PreEphemeralOp(rId, plans.Read) + }) + + return nil, diags + } + + validateResp := provider.ValidateEphemeralResourceConfig(providers.ValidateEphemeralResourceConfigRequest{ + TypeName: inp.addr.Resource.Resource.Type, + Config: unmarkedConfigVal, + }) + + diags = diags.Append(validateResp.Diagnostics) + if diags.HasErrors() { + return nil, diags + } + + ctx.Hook(func(h Hook) (HookAction, error) { + return h.PreEphemeralOp(rId, plans.Open) + }) + resp := provider.OpenEphemeralResource(providers.OpenEphemeralResourceRequest{ + TypeName: inp.addr.ContainingResource().Resource.Type, + Config: unmarkedConfigVal, + }) + ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostEphemeralOp(rId, plans.Open, resp.Diagnostics.Err()) + }) + diags = diags.Append(resp.Diagnostics.InConfigBody(config.Config, inp.addr.String())) + if diags.HasErrors() { + return nil, diags + } + if resp.Deferred != nil { + return resp.Deferred, diags + } + resultVal := resp.Result.MarkWithPaths(configMarks) + + errs := objchange.AssertPlanValid(schema.Body, cty.NullVal(schema.Body.ImpliedType()), configVal, resultVal) + for _, err := range errs { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Provider produced invalid ephemeral resource instance", + fmt.Sprintf( + "The provider for %s produced an inconsistent result: %s.", + inp.addr.Resource.Resource.Type, + tfdiags.FormatError(err), + ), + nil, + )).InConfigBody(config.Config, inp.addr.String()) + } + if diags.HasErrors() { + return nil, diags + } + + // We are going to wholesale mark the entire resource as ephemeral. This + // simplifies the model as any references to ephemeral resources can be + // considered as such. Any input values that don't need to be ephemeral can + // be referenced directly. + resultVal = resultVal.Mark(marks.Ephemeral) + + impl := &ephemeralResourceInstImpl{ + addr: inp.addr, + providerCfg: inp.providerConfig, + provider: provider, + hook: ctx.Hook, + internal: resp.Private, + } + + ephemerals.RegisterInstance(ctx.StopCtx(), inp.addr, ephemeral.ResourceInstanceRegistration{ + Value: resultVal, + ConfigBody: config.Config, + Impl: impl, + RenewAt: resp.RenewAt, + Private: resp.Private, + }) + + // Postconditions for ephemerals validate only what is returned by + // OpenEphemeralResource. These will block downstream dependency operations + // if an error is returned, but don't prevent renewal or closing of the + // resource. + checkDiags = evalCheckRules( + addrs.ResourcePostcondition, + config.Postconditions, + ctx, inp.addr, keyData, + tfdiags.Error, + ) + diags = diags.Append(checkDiags) + + return nil, diags +} + +// nodeEphemeralResourceClose is the node type for closing the previously-opened +// instances of a particular ephemeral resource. +// +// Although ephemeral resource instances will always all get closed once a +// graph walk has completed anyway, the inclusion of explicit nodes for this +// allows closing ephemeral resource instances more promptly after all work +// that uses them has been completed, rather than always just waiting until +// the end of the graph walk. +// +// This is scoped to config-level resources rather than dynamic resource +// instances as a concession to allow using the same node type in both the plan +// and apply graphs, where the former only deals in whole resources while the +// latter contains individual instances. +type nodeEphemeralResourceClose struct { + // The provider must remain active for the lifetime of the value. Proxy the + // provider methods from the original resource to ensure the references are + // create correctly. + resourceNode GraphNodeProviderConsumer + addr addrs.ConfigResource +} + +var _ GraphNodeExecutable = (*nodeEphemeralResourceClose)(nil) +var _ GraphNodeModulePath = (*nodeEphemeralResourceClose)(nil) +var _ GraphNodeProviderConsumer = (*nodeEphemeralResourceClose)(nil) + +func (n *nodeEphemeralResourceClose) Name() string { + return n.addr.String() + " (close)" +} + +// ModulePath implements GraphNodeModulePath. +func (n *nodeEphemeralResourceClose) ModulePath() addrs.Module { + return n.addr.Module +} + +// Execute implements GraphNodeExecutable. +func (n *nodeEphemeralResourceClose) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + log.Printf("[TRACE] nodeEphemeralResourceClose: closing all instances of %s", n.addr) + resources := ctx.EphemeralResources() + return resources.CloseInstances(ctx.StopCtx(), n.addr) +} + +func (n *nodeEphemeralResourceClose) ProvidedBy() (addrs.ProviderConfig, bool) { + return n.resourceNode.ProvidedBy() +} + +func (n *nodeEphemeralResourceClose) Provider() addrs.Provider { + return n.resourceNode.Provider() +} + +func (n *nodeEphemeralResourceClose) SetProvider(provider addrs.AbsProviderConfig) { + // the provider should not be set through this proxy node +} + +// ephemeralResourceInstImpl implements ephemeral.ResourceInstance as an +// adapter to the relevant provider API calls. +type ephemeralResourceInstImpl struct { + addr addrs.AbsResourceInstance + providerCfg addrs.AbsProviderConfig + provider providers.Interface + hook hookFunc + internal []byte +} + +var _ ephemeral.ResourceInstance = (*ephemeralResourceInstImpl)(nil) + +// Close implements ephemeral.ResourceInstance. +func (impl *ephemeralResourceInstImpl) Close(ctx context.Context) tfdiags.Diagnostics { + log.Printf("[TRACE] ephemeralResourceInstImpl: closing %s", impl.addr) + rId := HookResourceIdentity{ + Addr: impl.addr, + ProviderAddr: impl.providerCfg.Provider, + } + impl.hook(func(h Hook) (HookAction, error) { + return h.PreEphemeralOp(rId, plans.Close) + }) + resp := impl.provider.CloseEphemeralResource(providers.CloseEphemeralResourceRequest{ + TypeName: impl.addr.Resource.Resource.Type, + Private: impl.internal, + }) + impl.hook(func(h Hook) (HookAction, error) { + return h.PostEphemeralOp(rId, plans.Close, resp.Diagnostics.Err()) + }) + return resp.Diagnostics +} + +// Renew implements ephemeral.ResourceInstance. +func (impl *ephemeralResourceInstImpl) Renew(ctx context.Context, req providers.EphemeralRenew) (nextRenew *providers.EphemeralRenew, diags tfdiags.Diagnostics) { + log.Printf("[TRACE] ephemeralResourceInstImpl: renewing %s", impl.addr) + + rId := HookResourceIdentity{ + Addr: impl.addr, + ProviderAddr: impl.providerCfg.Provider, + } + impl.hook(func(h Hook) (HookAction, error) { + return h.PreEphemeralOp(rId, plans.Renew) + }) + resp := impl.provider.RenewEphemeralResource(providers.RenewEphemeralResourceRequest{ + TypeName: impl.addr.Resource.Resource.Type, + Private: req.Private, + }) + impl.hook(func(h Hook) (HookAction, error) { + return h.PostEphemeralOp(rId, plans.Renew, resp.Diagnostics.Err()) + }) + if !resp.RenewAt.IsZero() { + nextRenew = &providers.EphemeralRenew{ + RenewAt: resp.RenewAt, + Private: resp.Private, + } + } + + return nextRenew, resp.Diagnostics +} diff --git a/internal/terraform/node_resource_forget.go b/internal/terraform/node_resource_forget.go new file mode 100644 index 0000000000..8118ee2d3d --- /dev/null +++ b/internal/terraform/node_resource_forget.go @@ -0,0 +1,88 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/tfdiags" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/states" +) + +// NodeForgetResourceInstance represents a resource instance that is to be +// removed from state. +type NodeForgetResourceInstance struct { + *NodeAbstractResourceInstance +} + +var ( + _ GraphNodeModuleInstance = (*NodeForgetResourceInstance)(nil) + _ GraphNodeConfigResource = (*NodeForgetResourceInstance)(nil) + _ GraphNodeResourceInstance = (*NodeForgetResourceInstance)(nil) + _ GraphNodeReferencer = (*NodeForgetResourceInstance)(nil) + _ GraphNodeExecutable = (*NodeForgetResourceInstance)(nil) + _ GraphNodeProviderConsumer = (*NodeForgetResourceInstance)(nil) + _ GraphNodeProvisionerConsumer = (*NodeForgetResourceInstance)(nil) + _ GraphNodeDestroyer = (*NodeForgetResourceInstance)(nil) +) + +func (n *NodeForgetResourceInstance) DestroyAddr() *addrs.AbsResourceInstance { + return &n.Addr +} + +func (n *NodeForgetResourceInstance) Name() string { + return n.ResourceInstanceAddr().String() + " (forget)" +} + +func (n *NodeForgetResourceInstance) ProvidedBy() (addr addrs.ProviderConfig, exact bool) { + if n.Addr.Resource.Resource.Mode == addrs.DataResourceMode { + // Indicate that this node does not require a configured provider + return nil, true + } + return n.NodeAbstractResourceInstance.ProvidedBy() +} + +// GraphNodeExecutable +func (n *NodeForgetResourceInstance) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { + addr := n.ResourceInstanceAddr() + + is := n.instanceState + if is == nil { + log.Printf("[WARN] NodeForgetResourceInstance for %s with no state", addr) + } + + var changeApply *plans.ResourceInstanceChange + var state *states.ResourceInstanceObject + + _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) + diags = diags.Append(err) + if diags.HasErrors() { + return diags + } + + changeApply, err = n.readDiff(ctx, providerSchema) + diags = diags.Append(err) + if changeApply == nil || diags.HasErrors() { + return diags + } + + state, readDiags := n.readResourceInstanceState(ctx, addr) + diags = diags.Append(readDiags) + if diags.HasErrors() { + return diags + } + + // Exit early if state is already null + if state == nil || state.Value.IsNull() { + return diags + } + + ctx.State().ForgetResourceInstanceCurrent(n.Addr) + + diags = diags.Append(updateStateHook(ctx)) + return diags +} diff --git a/internal/terraform/node_resource_import.go b/internal/terraform/node_resource_import.go index ecf39a07e0..82a269cb84 100644 --- a/internal/terraform/node_resource_import.go +++ b/internal/terraform/node_resource_import.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,9 +8,11 @@ import ( "log" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/zclconf/go-cty/cty" ) type graphNodeImportState struct { @@ -70,32 +75,65 @@ func (n *graphNodeImportState) Execute(ctx EvalContext, op walkOperation) (diags // Reset our states n.states = nil - provider, _, err := getProvider(ctx, n.ResolvedProvider) + provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) diags = diags.Append(err) if diags.HasErrors() { return diags } + schema := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type)) + return diags + } + // import state absAddr := n.Addr.Resource.Absolute(ctx.Path()) + hookResourceID := HookResourceIdentity{ + Addr: absAddr, + ProviderAddr: n.ResolvedProvider.Provider, + } // Call pre-import hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PreImportState(absAddr, n.ID) + return h.PreImportState(hookResourceID, n.ID) })) if diags.HasErrors() { return diags } resp := provider.ImportResourceState(providers.ImportResourceStateRequest{ - TypeName: n.Addr.Resource.Resource.Type, - ID: n.ID, + TypeName: n.Addr.Resource.Resource.Type, + ID: n.ID, + ClientCapabilities: ctx.ClientCapabilities(), }) diags = diags.Append(resp.Diagnostics) if diags.HasErrors() { return diags } + // Providers are supposed to return null values for all write-only attributes + var writeOnlyDiags tfdiags.Diagnostics + for _, imported := range resp.ImportedResources { + writeOnlyDiags = ephemeral.ValidateWriteOnlyAttributes( + "Import returned a non-null value for a write-only attribute", + func(path cty.Path) string { + return fmt.Sprintf( + "Provider %q returned a value for the write-only attribute \"%s%s\" during import. Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.ResolvedProvider, n.Addr, tfdiags.FormatCtyPath(path), + ) + }, + imported.State, + schema.Body, + ) + diags = diags.Append(writeOnlyDiags) + } + + if writeOnlyDiags.HasErrors() { + return diags + } + imported := resp.ImportedResources for _, obj := range imported { log.Printf("[TRACE] graphNodeImportState: import %s %q produced instance object of type %s", absAddr.String(), n.ID, obj.TypeName) @@ -104,7 +142,7 @@ func (n *graphNodeImportState) Execute(ctx EvalContext, op walkOperation) (diags // Call post-import hook diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { - return h.PostImportState(absAddr, imported) + return h.PostImportState(hookResourceID, imported) })) return diags } @@ -115,7 +153,7 @@ func (n *graphNodeImportState) Execute(ctx EvalContext, op walkOperation) (diags // and state inserts we need to do for our import state. Since they're new // resources they don't depend on anything else and refreshes are isolated // so this is nearly a perfect use case for dynamic expand. -func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) { +func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics g := &Graph{Path: ctx.Path()} @@ -161,7 +199,7 @@ func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) { } if diags.HasErrors() { // Bail out early, then. - return nil, diags.Err() + return nil, diags } // For each of the states, we add a node to handle the refresh/add to state. @@ -176,14 +214,10 @@ func (n *graphNodeImportState) DynamicExpand(ctx EvalContext) (*Graph, error) { }) } - // Root transform for a single root - t := &RootTransformer{} - if err := t.Transform(g); err != nil { - return nil, err - } + addRootNodeToGraph(g) // Done! - return g, diags.Err() + return g, diags } // graphNodeImportStateSub is the sub-node of graphNodeImportState @@ -216,7 +250,7 @@ func (n *graphNodeImportStateSub) Execute(ctx EvalContext, op walkOperation) (di return diags } - state := n.State.AsInstanceObject() + state := states.NewResourceInstanceObjectFromIR(n.State) // Refresh riNode := &NodeAbstractResourceInstance{ @@ -225,29 +259,47 @@ func (n *graphNodeImportStateSub) Execute(ctx EvalContext, op walkOperation) (di ResolvedProvider: n.ResolvedProvider, }, } - state, refreshDiags := riNode.refresh(ctx, states.NotDeposed, state) + state, deferred, refreshDiags := riNode.refresh(ctx, states.NotDeposed, state, false) diags = diags.Append(refreshDiags) if diags.HasErrors() { return diags } - // Verify the existance of the imported resource - if state.Value.IsNull() { - var diags tfdiags.Diagnostics + // If the refresh is deferred we will need to do another cycle to import the resource + if deferred != nil { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, - "Cannot import non-existent remote object", + "Cannot import deferred remote object", fmt.Sprintf( "While attempting to import an existing object to %q, "+ - "the provider detected that no object exists with the given id. "+ - "Only pre-existing objects can be imported; check that the id "+ - "is correct and that it is associated with the provider's "+ - "configured region or endpoint, or use \"terraform apply\" to "+ - "create a new remote object for this resource.", + "the provider deferred reading the resource. "+ + "This is a bug in the provider since deferrals are not supported when importing through the CLI, please file an issue."+ + "Please either use an import block for importing this resource "+ + "or remove the to be imported resource from your configuration, "+ + "apply the configuration using \"terraform apply\", "+ + "add the to be imported resource again, and retry the import operation.", n.TargetAddr, ), )) - return diags + } else { + // Verify the existance of the imported resource + if state.Value.IsNull() { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot import non-existent remote object", + fmt.Sprintf( + "While attempting to import an existing object to %q, "+ + "the provider detected that no object exists with the given id. "+ + "Only pre-existing objects can be imported; check that the id "+ + "is correct and that it is associated with the provider's "+ + "configured region or endpoint, or use \"terraform apply\" to "+ + "create a new remote object for this resource.", + n.TargetAddr, + ), + )) + return diags + } } diags = diags.Append(riNode.writeResourceInstanceState(ctx, state, workingState)) diff --git a/internal/terraform/node_resource_partial_plan.go b/internal/terraform/node_resource_partial_plan.go new file mode 100644 index 0000000000..9c4c4d86fa --- /dev/null +++ b/internal/terraform/node_resource_partial_plan.go @@ -0,0 +1,387 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// This file is a temporary split from the node_resource_plan.go file. We handle +// the unknown instances branch of execution within here, while this is still +// being developed. +// +// We have split the files to make structuring the code easier, eventually once +// the functions within this file are production ready, we will merge them back +// into the node_resource_plan.go file. + +// dynamicExpandPartial is a variant of dynamicExpand that we use when deferred +// actions are enabled for the current plan. +// +// Once deferred actions are more stable and robust in the stacks runtime, it +// would be nice to integrate this logic a little better with the main +// DynamicExpand logic, but it's separate for now to minimize the risk of +// stacks-specific behavior impacting configurations that are not opted into it. +func (n *nodeExpandPlannableResource) dynamicExpandPartial(ctx EvalContext, knownModules []addrs.ModuleInstance, partialModules addrs.Set[addrs.PartialExpandedModule], knownImports addrs.Map[addrs.AbsResourceInstance, cty.Value], unknownImports addrs.Map[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]]) (*Graph, tfdiags.Diagnostics) { + var g Graph + var diags tfdiags.Diagnostics + + knownResources := addrs.MakeSet[addrs.AbsResourceInstance]() + partialResources := addrs.MakeSet[addrs.PartialExpandedResource]() + maybeOrphanResources := addrs.MakeSet[addrs.AbsResourceInstance]() + + for _, moduleAddr := range knownModules { + resourceAddr := n.Addr.Resource.Absolute(moduleAddr) + resources, partials, maybeOrphans, moreDiags := n.expandKnownModule(ctx, resourceAddr, knownImports, unknownImports, &g) + diags = diags.Append(moreDiags) + + // Track all the resources we know about. + knownResources = knownResources.Union(resources) + partialResources = partialResources.Union(partials) + maybeOrphanResources = maybeOrphanResources.Union(maybeOrphans) + } + + for _, moduleAddr := range partialModules { + resourceAddr := moduleAddr.Resource(n.Addr.Resource) + partialResources.Add(resourceAddr) + + // And add a node to the graph for this resource. + g.Add(&nodePlannablePartialExpandedResource{ + addr: resourceAddr, + config: n.Config, + resolvedProvider: n.ResolvedProvider, + skipPlanChanges: n.skipPlanChanges, + preDestroyRefresh: n.preDestroyRefresh, + }) + } + + func() { + + ss := ctx.PrevRunState() + if ss == nil { + return // No previous state, so nothing to do here. + } + state := ss.Lock() + defer ss.Unlock() + + Resources: + for _, res := range state.Resources(n.Addr) { + + for _, knownModule := range knownModules { + if knownModule.Equal(res.Addr.Module) { + // Then we handled this resource as part of the known + // modules processing. + continue Resources + } + } + + for _, partialResource := range partialResources { + if partialResource.MatchesResource(res.Addr) { + + for key := range res.Instances { + // Then each of the instances is a "maybe orphan" + // instance, and we need to add a node for that. + maybeOrphanResources.Add(res.Addr.Instance(key)) + g.Add(n.concreteResource(ctx, addrs.MakeMap[addrs.AbsResourceInstance, cty.Value](), addrs.MakeMap[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]](), true)(NewNodeAbstractResourceInstance(res.Addr.Instance(key)))) + } + + // Move onto the next resource. + continue Resources + } + } + + // Otherwise, everything in here is just a simple orphaned instance. + + for key := range res.Instances { + inst := res.Addr.Instance(key) + abs := NewNodeAbstractResourceInstance(inst) + abs.AttachResourceState(res) + g.Add(n.concreteResourceOrphan(abs)) + } + + } + + }() + + // We need to ensure that all of the expanded import targets are actually + // present in the configuration, because we can't import something that + // doesn't exist. + // + // See the validateExpandedImportTargets function for the equivalent of + // this for the known resources path. +ImportValidationKnown: + for _, addr := range knownImports.Keys() { + if knownResources.Has(addr) { + // Simple case, this is known to be in the configuration so we + // skip it. + continue + } + + for _, partialAddr := range partialResources { + if partialAddr.MatchesInstance(addr) { + // This is a partial-expanded address, so we can't yet know + // whether it's in the configuration or not, and so we'll + // defer dealing with it to a future round. + continue ImportValidationKnown + } + } + + if maybeOrphanResources.Has(addr) { + // This is in the previous state but we can't yet know whether + // it's still desired, so we'll defer dealing with it to a future + // round. + continue + } + + // If we get here then the import target is not in the configuration + // at all, and so we'll report an error. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Configuration for import target does not exist", + fmt.Sprintf("The configuration for the given import %s does not exist. All target instances must have an associated configuration to be imported.", addr), + )) + } + + // We'll also perform the same kind of validation on our unknown imports. + // This will be less precise because we don't have the full state to + // compare against, but we can at least check that the import targets are + // in the configuration. +ImportValidationUnknown: + for _, elem := range unknownImports.Elems { + unknownImport := elem.Key + + for _, resource := range knownResources { + if unknownImport.MatchesInstance(resource) { + // This is in the configuration so we can skip it. + continue ImportValidationUnknown + } + } + + for _, partialResource := range partialResources { + // If the partial resource is a subset of the unknown import, or + // vice versa, then it *might* match up one day once everything + // is resolved so we'll allow it for now. + if partialResource.MatchesPartial(unknownImport) { + continue ImportValidationUnknown + } + if unknownImport.MatchesPartial(partialResource) { + continue ImportValidationUnknown + } + } + + for _, maybeOrphan := range maybeOrphanResources { + if unknownImport.MatchesInstance(maybeOrphan) { + // This is in the previous state but we can't yet know whether + // it's still desired, so we'll defer dealing with it to a + // future round. + continue ImportValidationUnknown + } + + } + + // If we get here then the import target is not in the configuration + // at all, and so we'll report an error. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Configuration for import target does not exist", + fmt.Sprintf("The configuration for the given import %s does not exist. All target instances must have an associated configuration to be imported.", unknownImport), + )) + + } + + // If this is a resource that participates in custom condition checks + // (i.e. it has preconditions or postconditions) then the check state + // wants to know the addresses of the checkable objects so that it can + // treat them as unknown status if we encounter an error before actually + // visiting the checks. + if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.NodeAbstractResource.Addr) { + checkables := addrs.MakeSet[addrs.Checkable]() + for _, addr := range knownResources { + checkables.Add(addr) + } + for _, addr := range maybeOrphanResources { + checkables.Add(addr) + } + + checkState.ReportCheckableObjects(n.NodeAbstractResource.Addr, checkables) + } + + addRootNodeToGraph(&g) + return &g, diags +} + +func (n *nodeExpandPlannableResource) expandKnownModule(globalCtx EvalContext, resAddr addrs.AbsResource, knownImports addrs.Map[addrs.AbsResourceInstance, cty.Value], unknownImports addrs.Map[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]], g *Graph) (addrs.Set[addrs.AbsResourceInstance], addrs.Set[addrs.PartialExpandedResource], addrs.Set[addrs.AbsResourceInstance], tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + moduleCtx := evalContextForModuleInstance(globalCtx, resAddr.Module) + + moreDiags := n.recordResourceData(moduleCtx, resAddr) + diags = diags.Append(moreDiags) + if moreDiags.HasErrors() { + return nil, nil, nil, diags + } + + expander := moduleCtx.InstanceExpander() + _, knownInstKeys, haveUnknownKeys := expander.ResourceInstanceKeys(resAddr) + + knownResources := addrs.MakeSet[addrs.AbsResourceInstance]() + partialResources := addrs.MakeSet[addrs.PartialExpandedResource]() + + for _, key := range knownInstKeys { + knownResources.Add(resAddr.Instance(key)) + } + if haveUnknownKeys { + partialResources.Add(resAddr.Module.UnexpandedResource(resAddr.Resource)) + } + + mustHaveIndex := len(knownInstKeys) != 1 || haveUnknownKeys + if len(knownInstKeys) == 1 && knownInstKeys[0] != addrs.NoKey { + mustHaveIndex = true + } + if mustHaveIndex { + var instanceAddrs []addrs.AbsResourceInstance + for _, key := range knownInstKeys { + instanceAddrs = append(instanceAddrs, resAddr.Instance(key)) + } + diags = diags.Append(n.validForceReplaceTargets(instanceAddrs)) + } + + instGraph, maybeOrphanResources, instDiags := n.knownModuleSubgraph(moduleCtx, resAddr, knownInstKeys, haveUnknownKeys, knownImports, unknownImports) + diags = diags.Append(instDiags) + if instDiags.HasErrors() { + return nil, nil, nil, diags + } + g.Subsume(&instGraph.AcyclicGraph.Graph) + return knownResources, partialResources, maybeOrphanResources, diags +} + +func (n *nodeExpandPlannableResource) knownModuleSubgraph(ctx EvalContext, addr addrs.AbsResource, knownInstKeys []addrs.InstanceKey, haveUnknownKeys bool, knownImports addrs.Map[addrs.AbsResourceInstance, cty.Value], unknownImports addrs.Map[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]]) (*Graph, addrs.Set[addrs.AbsResourceInstance], tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + if n.Config == nil && n.generateConfigPath != "" && knownImports.Len() == 0 { + // We're generating configuration, but there's nothing to import, which + // means the import block must have expanded to zero instances. + // the instance expander will always return a single instance because + // we have assumed there will eventually be a configuration for this + // resource, so return here before we add that to the graph. + return &Graph{}, nil, diags + } + + // Our graph transformers require access to the full state, so we'll + // temporarily lock it while we work on this. + state := ctx.State().Lock() + defer ctx.State().Unlock() + + maybeOrphans := addrs.MakeSet[addrs.AbsResourceInstance]() + + steps := []GraphTransformer{ + + DynamicTransformer(func(graph *Graph) error { + // We'll add a node for all the known instance keys. + for _, key := range knownInstKeys { + graph.Add(n.concreteResource(ctx, knownImports, unknownImports, n.skipPlanChanges)(NewNodeAbstractResourceInstance(addr.Instance(key)))) + } + return nil + }), + + DynamicTransformer(func(graph *Graph) error { + // We'll add a node if there are unknown instance keys. + if haveUnknownKeys { + addr := addr.Module.UnexpandedResource(addr.Resource) + + graph.Add(&nodePlannablePartialExpandedResource{ + addr: addr, + config: n.Config, + resolvedProvider: n.ResolvedProvider, + skipPlanChanges: n.skipPlanChanges, + preDestroyRefresh: n.preDestroyRefresh, + }) + } + return nil + }), + + DynamicTransformer(func(graph *Graph) error { + // Ephemeral resources don't need to be accounted for in this transform, + // since they are not in the state. + if addr.Resource.Mode == addrs.EphemeralResourceMode { + return nil + } + + // We'll add nodes for any orphaned resources. + rs := state.Resource(addr) + if rs == nil { + return nil + } + Instances: + for key, inst := range rs.Instances { + if inst.Current == nil { + continue + } + + for _, knownKey := range knownInstKeys { + if knownKey == key { + // Then we have a known instance, so we can skip this + // one - it's definitely not an orphan. + continue Instances + } + } + + if haveUnknownKeys { + // Then this is a "maybe orphan" instance. It isn't mapped + // to a known instance but we have unknown keys so we don't + // know for sure that it's been deleted. + maybeOrphans.Add(addr.Instance(key)) + graph.Add(n.concreteResource(ctx, addrs.MakeMap[addrs.AbsResourceInstance, cty.Value](), addrs.MakeMap[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]](), true)(NewNodeAbstractResourceInstance(addr.Instance(key)))) + continue + } + + // If none of the above, then this is definitely an orphan. + graph.Add(n.concreteResourceOrphan(NewNodeAbstractResourceInstance(addr.Instance(key)))) + } + + return nil + }), + + // Attach the state + &AttachStateTransformer{State: state}, + + // Targeting + &TargetsTransformer{Targets: n.Targets}, + + // Connect references so ordering is correct + &ReferenceTransformer{}, + + // Make sure there is a single root + &RootTransformer{}, + } + + b := &BasicGraphBuilder{ + Steps: steps, + Name: "nodeExpandPlannableResource", + } + graph, graphDiags := b.Build(addr.Module) + diags = diags.Append(graphDiags) + return graph, maybeOrphans, diags +} + +// transformDynamic is a helper struct that wraps a single function, allowing +// us to transform a graph dynamically. +type transformDynamic struct { + Transformer func(*Graph) error +} + +// DynamicTransformer returns a GraphTransformer that will apply the given +// function to the graph during the dynamic expansion phase. +func DynamicTransformer(f func(*Graph) error) GraphTransformer { + return &transformDynamic{Transformer: f} +} + +// implements GraphTransformer +func (t *transformDynamic) Transform(g *Graph) error { + return t.Transformer(g) +} diff --git a/internal/terraform/node_resource_plan.go b/internal/terraform/node_resource_plan.go index c99654e3b0..2a6e3590bd 100644 --- a/internal/terraform/node_resource_plan.go +++ b/internal/terraform/node_resource_plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,16 +8,18 @@ import ( "log" "strings" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) -// nodeExpandPlannableResource handles the first layer of resource -// expansion. We need this extra layer so DynamicExpand is called twice for -// the resource, the first to expand the Resource for each module instance, and -// the second to expand each ResourceInstance for the expanded Resources. +// nodeExpandPlannableResource represents an addrs.ConfigResource and implements +// DynamicExpand to a subgraph containing all of the addrs.AbsResourceInstance +// resulting from both the containing module and resource-specific expansion. type nodeExpandPlannableResource struct { *NodeAbstractResource @@ -26,6 +31,8 @@ type nodeExpandPlannableResource struct { // skipRefresh indicates that we should skip refreshing individual instances skipRefresh bool + preDestroyRefresh bool + // skipPlanChanges indicates we should skip trying to plan change actions // for any instances. skipPlanChanges bool @@ -49,6 +56,7 @@ var ( _ GraphNodeDynamicExpandable = (*nodeExpandPlannableResource)(nil) _ GraphNodeReferenceable = (*nodeExpandPlannableResource)(nil) _ GraphNodeReferencer = (*nodeExpandPlannableResource)(nil) + _ GraphNodeImportReferencer = (*nodeExpandPlannableResource)(nil) _ GraphNodeConfigResource = (*nodeExpandPlannableResource)(nil) _ GraphNodeAttachResourceConfig = (*nodeExpandPlannableResource)(nil) _ GraphNodeAttachDependencies = (*nodeExpandPlannableResource)(nil) @@ -84,31 +92,261 @@ func (n *nodeExpandPlannableResource) ModifyCreateBeforeDestroy(v bool) error { return nil } -func (n *nodeExpandPlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { - var g Graph +func (n *nodeExpandPlannableResource) DynamicExpand(ctx EvalContext) (*Graph, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics - expander := ctx.InstanceExpander() - moduleInstances := expander.ExpandModule(n.Addr.Module) - - // Add the current expanded resource to the graph - for _, module := range moduleInstances { - resAddr := n.Addr.Resource.Absolute(module) - g.Add(&NodePlannableResource{ - NodeAbstractResource: n.NodeAbstractResource, - Addr: resAddr, - ForceCreateBeforeDestroy: n.ForceCreateBeforeDestroy, - dependencies: n.dependencies, - skipRefresh: n.skipRefresh, - skipPlanChanges: n.skipPlanChanges, - forceReplace: n.forceReplace, - }) + // First, make sure the count and the foreach don't refer to the same + // resource. The config maybe nil if we are generating configuration, or + // deleting a resource. + if n.Config != nil { + diags = diags.Append(validateMetaSelfRef(n.Addr.Resource, n.Config.Count)) + diags = diags.Append(validateMetaSelfRef(n.Addr.Resource, n.Config.ForEach)) + if diags.HasErrors() { + return nil, diags + } } - // Lock the state while we inspect it - state := ctx.State().Lock() - defer ctx.State().Unlock() + // Expand the current module. + expander := ctx.InstanceExpander() + moduleInstances := expander.ExpandModule(n.Addr.Module, false) + + // The possibility of partial-expanded modules and resources is guarded by a + // top-level option for the whole plan, so that we can preserve mainline + // behavior for the modules runtime. So, we currently branch off into an + // entirely-separate codepath in those situations, at the expense of + // duplicating some of the logic for behavior this method would normally + // handle. + if ctx.Deferrals().DeferralAllowed() { // Expand the imports for this resource. + knownImports, unknownImports, importDiags := n.expandResourceImports(ctx, true) + diags = diags.Append(importDiags) + + pem := expander.UnknownModuleInstances(n.Addr.Module, false) + g, expandDiags := n.dynamicExpandPartial(ctx, moduleInstances, pem, knownImports, unknownImports) + diags = diags.Append(expandDiags) + return g, diags + } + + // Expand the imports for this resource. + imports, unknownImports, importDiags := n.expandResourceImports(ctx, false) + diags = diags.Append(importDiags) + + // Since allowUnknown was set to false in expandResourceImports, we should + // not have any unknown imports. + if unknownImports.Len() > 0 { + panic("unexpected unknown imports") + } + + g, expandDiags := n.dynamicExpand(ctx, moduleInstances, imports) + diags = diags.Append(expandDiags) + return g, diags +} + +// Import blocks are expanded in conjunction with their associated resource block. +func (n *nodeExpandPlannableResource) expandResourceImports(ctx EvalContext, allowUnknown bool) (addrs.Map[addrs.AbsResourceInstance, cty.Value], addrs.Map[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]], tfdiags.Diagnostics) { + // Imports maps the target address to an import ID. + knownImports := addrs.MakeMap[addrs.AbsResourceInstance, cty.Value]() + unknownImports := addrs.MakeMap[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]]() + var diags tfdiags.Diagnostics + + if len(n.importTargets) == 0 { + return knownImports, unknownImports, diags + } + + // Import blocks are only valid within the root module, and must be + // evaluated within that context + ctx = evalContextForModuleInstance(ctx, addrs.RootModuleInstance) + + state := ctx.State() + + for _, imp := range n.importTargets { + if imp.Config == nil { + // if we have a legacy addr, it was supplied on the commandline so + // there is nothing to expand + if !imp.LegacyAddr.Equal(addrs.AbsResourceInstance{}) { + knownImports.Put(imp.LegacyAddr, cty.StringVal(imp.LegacyID)) + return knownImports, unknownImports, diags + } + + // legacy import tests may have no configuration + log.Printf("[WARN] no configuration for import target %#v", imp) + continue + } + + if imp.Config.ForEach == nil { + traversal, hds := hcl.AbsTraversalForExpr(imp.Config.To) + diags = diags.Append(hds) + to, tds := addrs.ParseAbsResourceInstance(traversal) + diags = diags.Append(tds) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + + diags = diags.Append(validateImportTargetExpansion(n.Config, to, imp.Config.To)) + + var importID cty.Value + var evalDiags tfdiags.Diagnostics + if imp.Config.ID != nil { + importID, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, EvalDataForNoInstanceKey, allowUnknown) + } else if imp.Config.Identity != nil { + providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) + if err != nil { + diags = diags.Append(err) + return knownImports, unknownImports, diags + } + schema := providerSchema.SchemaForResourceAddr(to.Resource.Resource) + + importID, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, EvalDataForNoInstanceKey, allowUnknown) + } else { + // Should never happen + return knownImports, unknownImports, diags + } + + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + + knownImports.Put(to, importID) + + log.Printf("[TRACE] expandResourceImports: found single import target %s", to) + continue + } + + forEachData, known, forEachDiags := newForEachEvaluator(imp.Config.ForEach, ctx, allowUnknown).ImportValues() + diags = diags.Append(forEachDiags) + if forEachDiags.HasErrors() { + return knownImports, unknownImports, diags + } + + if !known { + // Then we need to parse the target address as a PartialResource + // instead of a known resource. + addr, evalDiags := evalImportUnknownToExpression(imp.Config.To) + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + + // We're going to work out which instances this import block might + // target actually already exist. + knownInstances := addrs.MakeSet[addrs.AbsResourceInstance]() + + cfg := addr.ConfigResource() + modInsts := state.ModuleInstances(cfg.Module) + for _, modInst := range modInsts { + abs := cfg.Absolute(modInst) + resource := state.Resource(cfg.Absolute(modInst)) + if resource == nil { + // Then we are creating every instance of this resource. + continue + } + + for inst := range resource.Instances { + knownInstances.Add(abs.Instance(inst)) + } + } + + unknownImports.Put(addr, knownInstances) + continue + } + + for _, keyData := range forEachData { + var evalDiags tfdiags.Diagnostics + res, evalDiags := evalImportToExpression(imp.Config.To, keyData) + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + + diags = diags.Append(validateImportTargetExpansion(n.Config, res, imp.Config.To)) + + var importID cty.Value + if imp.Config.ID != nil { + importID, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, keyData, allowUnknown) + } else if imp.Config.Identity != nil { + providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) + if err != nil { + diags = diags.Append(err) + return knownImports, unknownImports, diags + } + schema := providerSchema.SchemaForResourceAddr(res.Resource.Resource) + + importID, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, keyData, allowUnknown) + } else { + // Should never happen + return knownImports, unknownImports, diags + } + + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return knownImports, unknownImports, diags + } + + knownImports.Put(res, importID) + log.Printf("[TRACE] expandResourceImports: expanded import target %s", res) + } + } + + // filter out any known import which already exist in state + for _, el := range knownImports.Elements() { + if state.ResourceInstance(el.Key) != nil { + log.Printf("[DEBUG] expandResourceImports: skipping import address %s already in state", el.Key) + knownImports.Remove(el.Key) + } + } + + return knownImports, unknownImports, diags +} + +// validateExpandedImportTargets checks that all expanded imports correspond to +// a configured instance. +// +// This function is only called from within the dynamicExpand method, the +// import validation is inlined within the dynamicExpandPartial method for the +// alternate code path. +func (n *nodeExpandPlannableResource) validateExpandedImportTargets(expandedImports addrs.Map[addrs.AbsResourceInstance, cty.Value], expandedInstances addrs.Set[addrs.Checkable]) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + for _, addr := range expandedImports.Keys() { + if !expandedInstances.Has(addr) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Configuration for import target does not exist", + fmt.Sprintf("The configuration for the given import %s does not exist. All target instances must have an associated configuration to be imported.", addr), + )) + return diags + } + + if n.Config == nil && n.generateConfigPath == "" && len(n.importTargets) == 1 { + // we have no config and aren't generating any. This isn't caught during + // validation because generateConfigPath is only a plan option. If we + // got this far however, it means this node is eligible for config + // generation, so suggest it to the user. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Configuration for import target does not exist", + Detail: fmt.Sprintf("The configuration for the given import target %s does not exist. If you wish to automatically generate config for this resource, use the -generate-config-out option within terraform plan. Otherwise, make sure the target resource exists within your configuration. For example:\n\n terraform plan -generate-config-out=generated.tf", n.Addr), + Subject: n.importTargets[0].Config.To.Range().Ptr(), + }) + return diags + } + } + + return diags +} + +func (n *nodeExpandPlannableResource) findOrphans(ctx EvalContext, moduleInstances []addrs.ModuleInstance) []*states.Resource { + if n.Addr.Resource.Mode == addrs.EphemeralResourceMode { + // ephemeral resources don't exist in state + return nil + } var orphans []*states.Resource + + // Lock the state while we inspect it + sMgr := ctx.State() + state := sMgr.Lock() + for _, res := range state.Resources(n.Addr) { found := false for _, m := range moduleInstances { @@ -117,105 +355,96 @@ func (n *nodeExpandPlannableResource) DynamicExpand(ctx EvalContext) (*Graph, er break } } - // Address form state was not found in the current config + // The module instance of the resource in the state doesn't exist + // in the current config, so this whole resource is orphaned. if !found { orphans = append(orphans, res) } } + sMgr.Unlock() - // The concrete resource factory we'll use for orphans - concreteResourceOrphan := func(a *NodeAbstractResourceInstance) *NodePlannableResourceInstanceOrphan { - // Add the config and state since we don't do that via transforms - a.Config = n.Config - a.ResolvedProvider = n.ResolvedProvider - a.Schema = n.Schema - a.ProvisionerSchemas = n.ProvisionerSchemas - a.ProviderMetas = n.ProviderMetas - a.Dependencies = n.dependencies + return orphans +} - return &NodePlannableResourceInstanceOrphan{ - NodeAbstractResourceInstance: a, - skipRefresh: n.skipRefresh, - skipPlanChanges: n.skipPlanChanges, - } - } +func (n *nodeExpandPlannableResource) dynamicExpand(ctx EvalContext, moduleInstances []addrs.ModuleInstance, imports addrs.Map[addrs.AbsResourceInstance, cty.Value]) (*Graph, tfdiags.Diagnostics) { + var g Graph + var diags tfdiags.Diagnostics + + orphans := n.findOrphans(ctx, moduleInstances) for _, res := range orphans { for key := range res.Instances { addr := res.Addr.Instance(key) abs := NewNodeAbstractResourceInstance(addr) abs.AttachResourceState(res) - n := concreteResourceOrphan(abs) + n := n.concreteResourceOrphan(abs) g.Add(n) } } - return &g, nil + // The above dealt with the expansion of the containing module, so now + // we need to deal with the expansion of the resource itself across all + // instances of the module. + // + // We'll gather up all of the leaf instances we learn about along the way + // so that we can inform the checks subsystem of which instances it should + // be expecting check results for, below. + + expandedInstances := addrs.MakeSet[addrs.Checkable]() + for _, module := range moduleInstances { + resAddr := n.Addr.Resource.Absolute(module) + instances, err := n.expandResourceInstances(ctx, resAddr, imports, &g) + diags = diags.Append(err) + for _, instance := range instances { + expandedInstances.Add(instance) + } + } + if diags.HasErrors() { + return nil, diags + } + + diags = diags.Append(n.validateExpandedImportTargets(imports, expandedInstances)) + + // If this is a resource that participates in custom condition checks + // (i.e. it has preconditions or postconditions) then the check state + // wants to know the addresses of the checkable objects so that it can + // treat them as unknown status if we encounter an error before actually + // visiting the checks. + if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.NodeAbstractResource.Addr) { + checkState.ReportCheckableObjects(n.NodeAbstractResource.Addr, expandedInstances) + } + + addRootNodeToGraph(&g) + + return &g, diags } -// NodePlannableResource represents a resource that is "plannable": -// it is ready to be planned in order to create a diff. -type NodePlannableResource struct { - *NodeAbstractResource - - Addr addrs.AbsResource - - // ForceCreateBeforeDestroy might be set via our GraphNodeDestroyerCBD - // during graph construction, if dependencies require us to force this - // on regardless of what the configuration says. - ForceCreateBeforeDestroy *bool - - // skipRefresh indicates that we should skip refreshing individual instances - skipRefresh bool - - // skipPlanChanges indicates we should skip trying to plan change actions - // for any instances. - skipPlanChanges bool - - // forceReplace are resource instance addresses where the user wants to - // force generating a replace action. This set isn't pre-filtered, so - // it might contain addresses that have nothing to do with the resource - // that this node represents, which the node itself must therefore ignore. - forceReplace []addrs.AbsResourceInstance - - dependencies []addrs.ConfigResource -} - -var ( - _ GraphNodeModuleInstance = (*NodePlannableResource)(nil) - _ GraphNodeDestroyerCBD = (*NodePlannableResource)(nil) - _ GraphNodeDynamicExpandable = (*NodePlannableResource)(nil) - _ GraphNodeReferenceable = (*NodePlannableResource)(nil) - _ GraphNodeReferencer = (*NodePlannableResource)(nil) - _ GraphNodeConfigResource = (*NodePlannableResource)(nil) - _ GraphNodeAttachResourceConfig = (*NodePlannableResource)(nil) -) - -func (n *NodePlannableResource) Path() addrs.ModuleInstance { - return n.Addr.Module -} - -func (n *NodePlannableResource) Name() string { - return n.Addr.String() -} - -// GraphNodeExecutable -func (n *NodePlannableResource) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { +// expandResourceInstances calculates the dynamic expansion for the resource +// itself in the context of a particular module instance. +// +// It has several side-effects: +// - Adds a node to Graph g for each leaf resource instance it discovers, whether present or orphaned. +// - Registers the expansion of the resource in the "expander" object embedded inside EvalContext globalCtx. +// - Adds each present (non-orphaned) resource instance address to checkableAddrs (guaranteed to always be addrs.AbsResourceInstance, despite being declared as addrs.Checkable). +// +// After calling this for each of the module instances the resource appears +// within, the caller must register the final superset instAddrs with the +// checks subsystem so that it knows the fully expanded set of checkable +// object instances for this resource instance. +func (n *nodeExpandPlannableResource) expandResourceInstances(globalCtx EvalContext, resAddr addrs.AbsResource, imports addrs.Map[addrs.AbsResourceInstance, cty.Value], g *Graph) ([]addrs.AbsResourceInstance, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - if n.Config == nil { - // Nothing to do, then. - log.Printf("[TRACE] NodeApplyableResource: no configuration present for %s", n.Name()) - return diags - } + // The rest of our work here needs to know which module instance it's + // working in, so that it can evaluate expressions in the appropriate scope. + moduleCtx := evalContextForModuleInstance(globalCtx, resAddr.Module) // writeResourceState is responsible for informing the expander of what // repetition mode this resource has, which allows expander.ExpandResource // to work below. - moreDiags := n.writeResourceState(ctx, n.Addr) + moreDiags := n.recordResourceData(moduleCtx, resAddr) diags = diags.Append(moreDiags) if moreDiags.HasErrors() { - return diags + return nil, diags } // Before we expand our resource into potentially many resource instances, @@ -223,8 +452,8 @@ func (n *NodePlannableResource) Execute(ctx EvalContext, op walkOperation) tfdia // consistent with the repetition mode of the resource. In other words, // we're aiming to catch a situation where naming a particular resource // instance would require an instance key but the given address has none. - expander := ctx.InstanceExpander() - instanceAddrs := expander.ExpandResource(n.ResourceAddr().Absolute(ctx.Path())) + expander := moduleCtx.InstanceExpander() + instanceAddrs := expander.ExpandResource(resAddr) // If there's a number of instances other than 1 then we definitely need // an index. @@ -235,160 +464,49 @@ func (n *NodePlannableResource) Execute(ctx EvalContext, op walkOperation) tfdia mustHaveIndex = true } if mustHaveIndex { - for _, candidateAddr := range n.forceReplace { - if candidateAddr.Resource.Key == addrs.NoKey { - if n.Addr.Resource.Equal(candidateAddr.Resource.Resource) { - switch { - case len(instanceAddrs) == 0: - // In this case there _are_ no instances to replace, so - // there isn't any alternative address for us to suggest. - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - "Incompletely-matched force-replace resource instance", - fmt.Sprintf( - "Your force-replace request for %s doesn't match any resource instances because this resource doesn't have any instances.", - candidateAddr, - ), - )) - case len(instanceAddrs) == 1: - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - "Incompletely-matched force-replace resource instance", - fmt.Sprintf( - "Your force-replace request for %s doesn't match any resource instances because it lacks an instance key.\n\nTo force replacement of the single declared instance, use the following option instead:\n -replace=%q", - candidateAddr, instanceAddrs[0], - ), - )) - default: - var possibleValidOptions strings.Builder - for _, addr := range instanceAddrs { - fmt.Fprintf(&possibleValidOptions, "\n -replace=%q", addr) - } - - diags = diags.Append(tfdiags.Sourceless( - tfdiags.Warning, - "Incompletely-matched force-replace resource instance", - fmt.Sprintf( - "Your force-replace request for %s doesn't match any resource instances because it lacks an instance key.\n\nTo force replacement of particular instances, use one or more of the following options instead:%s", - candidateAddr, possibleValidOptions.String(), - ), - )) - } - } - } - } + diags = diags.Append(n.validForceReplaceTargets(instanceAddrs)) } // NOTE: The actual interpretation of n.forceReplace to produce replace - // actions is in NodeAbstractResourceInstance.plan, because we must do so - // on a per-instance basis rather than for the whole resource. + // actions is in the per-instance function we're about to call, because + // we need to evaluate it on a per-instance basis. - return diags -} - -// GraphNodeDestroyerCBD -func (n *NodePlannableResource) CreateBeforeDestroy() bool { - if n.ForceCreateBeforeDestroy != nil { - return *n.ForceCreateBeforeDestroy + // Our graph builder mechanism expects to always be constructing new + // graphs rather than adding to existing ones, so we'll first + // construct a subgraph just for this individual modules's instances and + // then we'll steal all of its nodes and edges to incorporate into our + // main graph which contains all of the resource instances together. + instG, instDiags := n.resourceInstanceSubgraph(moduleCtx, resAddr, instanceAddrs, imports) + if instDiags.HasErrors() { + diags = diags.Append(instDiags) + return nil, diags } + g.Subsume(&instG.AcyclicGraph.Graph) - // If we have no config, we just assume no - if n.Config == nil || n.Config.Managed == nil { - return false - } - - return n.Config.Managed.CreateBeforeDestroy + return instanceAddrs, diags } -// GraphNodeDestroyerCBD -func (n *NodePlannableResource) ModifyCreateBeforeDestroy(v bool) error { - n.ForceCreateBeforeDestroy = &v - return nil -} - -// GraphNodeDynamicExpandable -func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { +func (n *nodeExpandPlannableResource) resourceInstanceSubgraph(ctx EvalContext, addr addrs.AbsResource, instanceAddrs []addrs.AbsResourceInstance, imports addrs.Map[addrs.AbsResourceInstance, cty.Value]) (*Graph, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - // Our instance expander should already have been informed about the - // expansion of this resource and of all of its containing modules, so - // it can tell us which instance addresses we need to process. - expander := ctx.InstanceExpander() - instanceAddrs := expander.ExpandResource(n.ResourceAddr().Absolute(ctx.Path())) + if n.Config == nil && n.generateConfigPath != "" && imports.Len() == 0 { + // We're generating configuration, but there's nothing to import, which + // means the import block must have expanded to zero instances. + // the instance expander will always return a single instance because + // we have assumed there will eventually be a configuration for this + // resource, so return here before we add that to the graph. + return &Graph{}, diags + } // Our graph transformers require access to the full state, so we'll // temporarily lock it while we work on this. state := ctx.State().Lock() defer ctx.State().Unlock() - // If this is a resource that participates in custom condition checks - // (i.e. it has preconditions or postconditions) then the check state - // wants to know the addresses of the checkable objects so that it can - // treat them as unknown status if we encounter an error before actually - // visiting the checks. - if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.NodeAbstractResource.Addr) { - checkableAddrs := addrs.MakeSet[addrs.Checkable]() - for _, addr := range instanceAddrs { - checkableAddrs.Add(addr) - } - checkState.ReportCheckableObjects(n.NodeAbstractResource.Addr, checkableAddrs) - } - - // The concrete resource factory we'll use - concreteResource := func(a *NodeAbstractResourceInstance) dag.Vertex { - // check if this node is being imported first - for _, importTarget := range n.importTargets { - if importTarget.Addr.Equal(a.Addr) { - return &graphNodeImportState{ - Addr: importTarget.Addr, - ID: importTarget.ID, - ResolvedProvider: n.ResolvedProvider, - } - } - } - - // Add the config and state since we don't do that via transforms - a.Config = n.Config - a.ResolvedProvider = n.ResolvedProvider - a.Schema = n.Schema - a.ProvisionerSchemas = n.ProvisionerSchemas - a.ProviderMetas = n.ProviderMetas - a.dependsOn = n.dependsOn - a.Dependencies = n.dependencies - - return &NodePlannableResourceInstance{ - NodeAbstractResourceInstance: a, - - // By the time we're walking, we've figured out whether we need - // to force on CreateBeforeDestroy due to dependencies on other - // nodes that have it. - ForceCreateBeforeDestroy: n.CreateBeforeDestroy(), - skipRefresh: n.skipRefresh, - skipPlanChanges: n.skipPlanChanges, - forceReplace: n.forceReplace, - } - } - - // The concrete resource factory we'll use for orphans - concreteResourceOrphan := func(a *NodeAbstractResourceInstance) dag.Vertex { - // Add the config and state since we don't do that via transforms - a.Config = n.Config - a.ResolvedProvider = n.ResolvedProvider - a.Schema = n.Schema - a.ProvisionerSchemas = n.ProvisionerSchemas - a.ProviderMetas = n.ProviderMetas - - return &NodePlannableResourceInstanceOrphan{ - NodeAbstractResourceInstance: a, - skipRefresh: n.skipRefresh, - skipPlanChanges: n.skipPlanChanges, - } - } - // Start creating the steps steps := []GraphTransformer{ // Expand the count or for_each (if present) &ResourceCountTransformer{ - Concrete: concreteResource, + Concrete: n.concreteResource(ctx, imports, addrs.MakeMap[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]](), n.skipPlanChanges), Schema: n.Schema, Addr: n.ResourceAddr(), InstanceAddrs: instanceAddrs, @@ -396,8 +514,8 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { // Add the count/for_each orphans &OrphanResourceInstanceCountTransformer{ - Concrete: concreteResourceOrphan, - Addr: n.Addr, + Concrete: n.concreteResourceOrphan, + Addr: addr, InstanceAddrs: instanceAddrs, State: state, }, @@ -418,8 +536,148 @@ func (n *NodePlannableResource) DynamicExpand(ctx EvalContext) (*Graph, error) { // Build the graph b := &BasicGraphBuilder{ Steps: steps, - Name: "NodePlannableResource", + Name: "nodeExpandPlannableResource", } - graph, diags := b.Build(ctx.Path()) - return graph, diags.ErrWithWarnings() + graph, graphDiags := b.Build(addr.Module) + diags = diags.Append(graphDiags) + + return graph, diags +} + +func (n *nodeExpandPlannableResource) concreteResource(ctx EvalContext, knownImports addrs.Map[addrs.AbsResourceInstance, cty.Value], unknownImports addrs.Map[addrs.PartialExpandedResource, addrs.Set[addrs.AbsResourceInstance]], skipPlanChanges bool) func(*NodeAbstractResourceInstance) dag.Vertex { + return func(a *NodeAbstractResourceInstance) dag.Vertex { + var m *NodePlannableResourceInstance + + // If we're in legacy import mode (the import CLI command), we only need + // to return the import node, not a plannable resource node. + for _, importTarget := range n.importTargets { + if importTarget.LegacyAddr.Equal(a.Addr) { + + // If we're in the legacy import mode, then we should never + // see unknown imports. So, it's fine to just look at the known + // imports here. + idValue := knownImports.Get(importTarget.LegacyAddr) + + return &graphNodeImportState{ + Addr: importTarget.LegacyAddr, + ID: idValue.AsString(), + ResolvedProvider: n.ResolvedProvider, + } + } + } + + // Add the config and state since we don't do that via transforms + a.Config = n.Config + a.ResolvedProvider = n.ResolvedProvider + a.Schema = n.Schema + a.ProvisionerSchemas = n.ProvisionerSchemas + a.ProviderMetas = n.ProviderMetas + a.dependsOn = n.dependsOn + a.Dependencies = n.dependencies + a.preDestroyRefresh = n.preDestroyRefresh + a.generateConfigPath = n.generateConfigPath + + m = &NodePlannableResourceInstance{ + NodeAbstractResourceInstance: a, + + // By the time we're walking, we've figured out whether we need + // to force on CreateBeforeDestroy due to dependencies on other + // nodes that have it. + ForceCreateBeforeDestroy: n.CreateBeforeDestroy(), + skipRefresh: n.skipRefresh, + skipPlanChanges: skipPlanChanges, + forceReplace: n.forceReplace, + } + + if importID, ok := knownImports.GetOk(a.Addr); ok { + m.importTarget = importID + } else { + // We're going to check now if this resource instance *might* be + // targeted by one of the unknown imports. If it is, we'll set the + // import target to an unknown value so that the import operation + // will be deferred. + for _, unknownImport := range unknownImports.Elems { + if unknownImport.Key.MatchesInstance(a.Addr) { + if unknownImport.Value.Has(a.Addr) { + // This means that this particular instance already + // exists within the state. `import` blocks that target + // instances that already exist are ignored by + // Terraform. This means that even if this unknown + // import does eventually resolve to this instance then + // it would be ignored anyway. So for this instance we + // won't set the import target. + continue + } + + m.importTarget = cty.UnknownVal(cty.String) + } + } + } + + return m + } +} + +func (n *nodeExpandPlannableResource) concreteResourceOrphan(a *NodeAbstractResourceInstance) dag.Vertex { + // Add the config and state since we don't do that via transforms + a.Config = n.Config + a.ResolvedProvider = n.ResolvedProvider + a.Schema = n.Schema + a.ProvisionerSchemas = n.ProvisionerSchemas + a.ProviderMetas = n.ProviderMetas + + return &NodePlannableResourceInstanceOrphan{ + NodeAbstractResourceInstance: a, + skipRefresh: n.skipRefresh, + skipPlanChanges: n.skipPlanChanges, + } +} + +func (n *nodeExpandPlannableResource) validForceReplaceTargets(instanceAddrs []addrs.AbsResourceInstance) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + for _, candidateAddr := range n.forceReplace { + if candidateAddr.Resource.Key == addrs.NoKey { + if n.Addr.Resource.Equal(candidateAddr.Resource.Resource) { + switch { + case len(instanceAddrs) == 0: + // In this case there _are_ no instances to replace, so + // there isn't any alternative address for us to suggest. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Incompletely-matched force-replace resource instance", + fmt.Sprintf( + "Your force-replace request for %s doesn't match any resource instances because this resource doesn't have any instances.", + candidateAddr, + ), + )) + case len(instanceAddrs) == 1: + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Incompletely-matched force-replace resource instance", + fmt.Sprintf( + "Your force-replace request for %s doesn't match any resource instances because it lacks an instance key.\n\nTo force replacement of the single declared instance, use the following option instead:\n -replace=%q", + candidateAddr, instanceAddrs[0], + ), + )) + default: + var possibleValidOptions strings.Builder + for _, addr := range instanceAddrs { + fmt.Fprintf(&possibleValidOptions, "\n -replace=%q", addr) + } + + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Warning, + "Incompletely-matched force-replace resource instance", + fmt.Sprintf( + "Your force-replace request for %s doesn't match any resource instances because it lacks an instance key.\n\nTo force replacement of particular instances, use one or more of the following options instead:%s", + candidateAddr, possibleValidOptions.String(), + ), + )) + } + } + } + } + + return diags } diff --git a/internal/terraform/node_resource_plan_destroy.go b/internal/terraform/node_resource_plan_destroy.go index dd8216445d..0962ce8fa8 100644 --- a/internal/terraform/node_resource_plan_destroy.go +++ b/internal/terraform/node_resource_plan_destroy.go @@ -1,13 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // NodePlanDestroyableResourceInstance represents a resource that is ready @@ -32,6 +37,10 @@ var ( _ GraphNodeProviderConsumer = (*NodePlanDestroyableResourceInstance)(nil) ) +func (n *NodePlanDestroyableResourceInstance) Name() string { + return n.NodeAbstractResource.Name() + " (destroy)" +} + // GraphNodeDestroyer func (n *NodePlanDestroyableResourceInstance) DestroyAddr() *addrs.AbsResourceInstance { addr := n.ResourceInstanceAddr() @@ -87,18 +96,32 @@ func (n *NodePlanDestroyableResourceInstance) managedResourceExecute(ctx EvalCon } } - change, destroyPlanDiags := n.planDestroy(ctx, state, "") + change, deferred, destroyPlanDiags := n.planDestroy(ctx, state, "") diags = diags.Append(destroyPlanDiags) if diags.HasErrors() { return diags } + if deferred != nil { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, deferred.Reason, change) + return diags + } else if ctx.Deferrals().ShouldDeferResourceInstanceChanges(n.Addr, n.Dependencies) { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, providers.DeferredReasonDeferredPrereq, change) + return diags + } + + // We intentionally write the change before the subsequent checks, because + // all of the checks below this point are for problems caused by the + // context surrounding the change, rather than the change itself, and + // so it's helpful to still include the valid-in-isolation change as + // part of the plan as additional context in our error output. + diags = diags.Append(n.writeChange(ctx, change, "")) + diags = diags.Append(n.checkPreventDestroy(change)) if diags.HasErrors() { return diags } - diags = diags.Append(n.writeChange(ctx, change, "")) return diags } diff --git a/internal/terraform/node_resource_plan_instance.go b/internal/terraform/node_resource_plan_instance.go index 14bded923a..b0e3702404 100644 --- a/internal/terraform/node_resource_plan_instance.go +++ b/internal/terraform/node_resource_plan_instance.go @@ -1,17 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" + "path/filepath" "sort" - "github.com/hashicorp/terraform/internal/instances" - "github.com/hashicorp/terraform/internal/plans" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/genconfig" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/ephemeral" + "github.com/hashicorp/terraform/internal/moduletest/mocking" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) // NodePlannableResourceInstance represents a _single_ resource @@ -37,6 +50,10 @@ type NodePlannableResourceInstance struct { // replaceTriggeredBy stores references from replace_triggered_by which // triggered this instance to be replaced. replaceTriggeredBy []*addrs.Reference + + // importTarget, if populated, contains the information necessary to plan + // an import of this resource. + importTarget cty.Value } var ( @@ -60,6 +77,8 @@ func (n *NodePlannableResourceInstance) Execute(ctx EvalContext, op walkOperatio return n.managedResourceExecute(ctx) case addrs.DataResourceMode: return n.dataResourceExecute(ctx) + case addrs.EphemeralResourceMode: + return n.ephemeralResourceExecute(ctx) default: panic(fmt.Errorf("unsupported resource mode %s", n.Config.Mode)) } @@ -83,40 +102,77 @@ func (n *NodePlannableResourceInstance) dataResourceExecute(ctx EvalContext) (di } checkRuleSeverity := tfdiags.Error - if n.skipPlanChanges { + if n.skipPlanChanges || n.preDestroyRefresh { checkRuleSeverity = tfdiags.Warning } - change, state, repeatData, planDiags := n.planDataSource(ctx, checkRuleSeverity) + deferrals := ctx.Deferrals() + change, state, deferred, repeatData, planDiags := n.planDataSource(ctx, checkRuleSeverity, n.skipPlanChanges, deferrals.ShouldDeferResourceInstanceChanges(addr, n.Dependencies)) diags = diags.Append(planDiags) if diags.HasErrors() { return diags } - // write the data source into both the refresh state and the - // working state - diags = diags.Append(n.writeResourceInstanceState(ctx, state, refreshState)) - if diags.HasErrors() { - return diags - } - diags = diags.Append(n.writeResourceInstanceState(ctx, state, workingState)) - if diags.HasErrors() { - return diags + // A nil change here indicates that Terraform is deciding NOT to make a + // change at all. In which case even if we wanted to try and defer it + // (because of a dependency) we can't as there is no change to defer. + // + // The most common case for this is when the data source is being refreshed + // but depends on unknown values or dependencies which means we just skip + // refreshing the data source. We maintain that behaviour here. + if change != nil && deferred != nil { + // Then this data source got deferred by the provider during planning. + deferrals.ReportDataSourceInstanceDeferred(addr, deferred.Reason, change) + } else { + // Not deferred; business as usual. + + // write the data source into both the refresh state and the + // working state + diags = diags.Append(n.writeResourceInstanceState(ctx, state, refreshState)) + if diags.HasErrors() { + return diags + } + diags = diags.Append(n.writeResourceInstanceState(ctx, state, workingState)) + if diags.HasErrors() { + return diags + } + + diags = diags.Append(n.writeChange(ctx, change, "")) + + // Post-conditions might block further progress. We intentionally do this + // _after_ writing the state/diff because we want to check against + // the result of the operation, and to fail on future operations + // until the user makes the condition succeed. + checkDiags := evalCheckRules( + addrs.ResourcePostcondition, + n.Config.Postconditions, + ctx, addr, repeatData, + checkRuleSeverity, + ) + diags = diags.Append(checkDiags) } - diags = diags.Append(n.writeChange(ctx, change, "")) + return diags +} - // Post-conditions might block further progress. We intentionally do this - // _after_ writing the state/diff because we want to check against - // the result of the operation, and to fail on future operations - // until the user makes the condition succeed. - checkDiags := evalCheckRules( - addrs.ResourcePostcondition, - n.Config.Postconditions, - ctx, addr, repeatData, - checkRuleSeverity, - ) - diags = diags.Append(checkDiags) +func (n *NodePlannableResourceInstance) ephemeralResourceExecute(ctx EvalContext) (diags tfdiags.Diagnostics) { + deferrals := ctx.Deferrals() + // For deferred ephemeral resources, we don't need to do anything here. + if deferrals.ShouldDeferResourceInstanceChanges(n.Addr, n.Dependencies) { + deferrals.ReportEphemeralResourceInstanceDeferred(n.Addr, providers.DeferredReasonDeferredPrereq) + return nil + } + + deferred, diags := ephemeralResourceOpen(ctx, ephemeralResourceInput{ + addr: n.Addr, + config: n.Config, + providerConfig: n.ResolvedProvider, + }) + + if deferred != nil { + // Then this ephemeral resource has been deferred while opening. + deferrals.ReportEphemeralResourceInstanceDeferred(n.Addr, deferred.Reason) + } return diags } @@ -125,62 +181,127 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) config := n.Config addr := n.ResourceInstanceAddr() - var change *plans.ResourceInstanceChange var instanceRefreshState *states.ResourceInstanceObject - _, providerSchema, err := getProvider(ctx, n.ResolvedProvider) + checkRuleSeverity := tfdiags.Error + if n.skipPlanChanges || n.preDestroyRefresh { + checkRuleSeverity = tfdiags.Warning + } + + provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider) diags = diags.Append(err) if diags.HasErrors() { return diags } - diags = diags.Append(validateSelfRef(addr.Resource, config.Config, providerSchema)) - if diags.HasErrors() { - return diags - } - - instanceRefreshState, readDiags := n.readResourceInstanceState(ctx, addr) - diags = diags.Append(readDiags) - if diags.HasErrors() { - return diags - } - - // We'll save a snapshot of what we just read from the state into the - // prevRunState before we do anything else, since this will capture the - // result of any schema upgrading that readResourceInstanceState just did, - // but not include any out-of-band changes we might detect in in the - // refresh step below. - diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, prevRunState)) - if diags.HasErrors() { - return diags - } - // Also the refreshState, because that should still reflect schema upgrades - // even if it doesn't reflect upstream changes. - diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) - if diags.HasErrors() { - return diags - } - - // In 0.13 we could be refreshing a resource with no config. - // We should be operating on managed resource, but check here to be certain - if n.Config == nil || n.Config.Managed == nil { - log.Printf("[WARN] managedResourceExecute: no Managed config value found in instance state for %q", n.Addr) - } else { - if instanceRefreshState != nil { - instanceRefreshState.CreateBeforeDestroy = n.Config.Managed.CreateBeforeDestroy || n.ForceCreateBeforeDestroy + if config != nil { + diags = diags.Append(validateSelfRef(addr.Resource, config.Config, providerSchema)) + if diags.HasErrors() { + return diags } } + importing := n.importTarget != cty.NilVal && !n.preDestroyRefresh + + var deferred *providers.Deferred + + // If the resource is to be imported, we now ask the provider for an Import + // and a Refresh, and save the resulting state to instanceRefreshState. + + if importing { + if n.importTarget.IsWhollyKnown() { + var importDiags tfdiags.Diagnostics + instanceRefreshState, deferred, importDiags = n.importState(ctx, addr, n.importTarget, provider, providerSchema) + diags = diags.Append(importDiags) + } else { + // Otherwise, just mark the resource as deferred without trying to + // import it. + deferred = &providers.Deferred{ + Reason: providers.DeferredReasonResourceConfigUnknown, + } + if n.Config == nil && len(n.generateConfigPath) > 0 { + // Then we're supposed to be generating configuration for this + // resource, but we can't because the configuration is unknown. + // + // Normally, the rest of this function would just be about + // planning the known configuration to make sure everything we + // do know about it is correct, but we can't even do that here. + // + // What we'll do is write out the address as being deferred with + // an entirely unknown value. Then we'll skip the rest of this + // function. (a) We're going to panic later when it complains + // about having no configuration, and (b) the rest of the + // function isn't doing anything as there is no configuration + // to validate. + + impliedType := providerSchema.ResourceTypes[addr.Resource.Resource.Type].Body.ImpliedType() + ctx.Deferrals().ReportResourceInstanceDeferred(addr, providers.DeferredReasonResourceConfigUnknown, &plans.ResourceInstanceChange{ + Addr: addr, + PrevRunAddr: addr, + ProviderAddr: n.ResolvedProvider, + Change: plans.Change{ + Action: plans.NoOp, // assume we'll get the config generation correct. + Before: cty.NullVal(impliedType), + After: cty.UnknownVal(impliedType), + Importing: &plans.Importing{ + Target: n.importTarget, + }, + }, + }) + return diags + } + } + } else { + var readDiags tfdiags.Diagnostics + instanceRefreshState, readDiags = n.readResourceInstanceState(ctx, addr) + diags = diags.Append(readDiags) + if diags.HasErrors() { + return diags + } + } + + if deferred == nil { + // We'll save a snapshot of what we just read from the state into the + // prevRunState before we do anything else, since this will capture the + // result of any schema upgrading that readResourceInstanceState just did, + // but not include any out-of-band changes we might detect in in the + // refresh step below. + diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, prevRunState)) + if diags.HasErrors() { + return diags + } + // Also the refreshState, because that should still reflect schema upgrades + // even if it doesn't reflect upstream changes. + diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) + if diags.HasErrors() { + return diags + } + } + + // we may need to detect a change in CreateBeforeDestroy to ensure it's + // stored when we are not refreshing + updatedCBD := false + if n.Config != nil && n.Config.Managed != nil && instanceRefreshState != nil { + newCBD := n.Config.Managed.CreateBeforeDestroy || n.ForceCreateBeforeDestroy + updatedCBD = instanceRefreshState.CreateBeforeDestroy != newCBD + instanceRefreshState.CreateBeforeDestroy = newCBD + } + + var refreshDeferred *providers.Deferred + // This is the state of the resource before we refresh the value, we need to keep track + // of this to report this as the before value if the refresh is deferred. + priorInstanceRefreshState := instanceRefreshState + // Refresh, maybe - if !n.skipRefresh { - s, refreshDiags := n.refresh(ctx, states.NotDeposed, instanceRefreshState) + // The import process handles its own refresh + if !n.skipRefresh && !importing { + var refreshDiags tfdiags.Diagnostics + instanceRefreshState, refreshDeferred, refreshDiags = n.refresh(ctx, states.NotDeposed, instanceRefreshState, ctx.Deferrals().DeferralAllowed()) diags = diags.Append(refreshDiags) if diags.HasErrors() { return diags } - instanceRefreshState = s - if instanceRefreshState != nil { // When refreshing we start by merging the stored dependencies and // the configured dependencies. The configured dependencies will be @@ -190,6 +311,23 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) instanceRefreshState.Dependencies = mergeDeps(n.Dependencies, instanceRefreshState.Dependencies) } + if deferred == nil && refreshDeferred != nil { + deferred = refreshDeferred + } + + if deferred == nil { + diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) + } + + if diags.HasErrors() { + return diags + } + } + + if n.skipRefresh && !importing && updatedCBD { + // CreateBeforeDestroy must be set correctly in the state which is used + // to create the apply graph, so if we did not refresh the state make + // sure we still update any changes to CreateBeforeDestroy. diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) if diags.HasErrors() { return diags @@ -215,14 +353,50 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) return diags } - change, instancePlanState, repeatData, planDiags := n.plan( - ctx, change, instanceRefreshState, n.ForceCreateBeforeDestroy, n.forceReplace, + change, instancePlanState, planDeferred, repeatData, planDiags := n.plan( + ctx, nil, instanceRefreshState, n.ForceCreateBeforeDestroy, n.forceReplace, ) diags = diags.Append(planDiags) if diags.HasErrors() { + // If we are importing and generating a configuration, we need to + // ensure the change is written out so the configuration can be + // captured. + if planDeferred == nil && len(n.generateConfigPath) > 0 { + // Update our return plan + change := &plans.ResourceInstanceChange{ + Addr: n.Addr, + PrevRunAddr: n.prevRunAddr(ctx), + ProviderAddr: n.ResolvedProvider, + Change: plans.Change{ + // we only need a placeholder, so this will be a NoOp + Action: plans.NoOp, + Before: instanceRefreshState.Value, + After: instanceRefreshState.Value, + GeneratedConfig: n.generatedConfigHCL, + }, + } + diags = diags.Append(n.writeChange(ctx, change, "")) + } + return diags } + if deferred == nil && planDeferred != nil { + deferred = planDeferred + } + + if importing { + // There is a subtle difference between the import by identity + // and the import by ID. When importing by identity, we need to + // make sure to use the complete identity return by the provider + // instead of the (potential) incomplete one from the configuration. + if n.importTarget.Type().IsObjectType() { + change.Importing = &plans.Importing{Target: instanceRefreshState.Identity} + } else { + change.Importing = &plans.Importing{Target: n.importTarget} + } + } + // FIXME: here we udpate the change to reflect the reason for // replacement, but we still overload forceReplace to get the correct // change planned. @@ -230,59 +404,86 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) change.ActionReason = plans.ResourceInstanceReplaceByTriggers } - diags = diags.Append(n.checkPreventDestroy(change)) - if diags.HasErrors() { - return diags - } - - // FIXME: it is currently important that we write resource changes to - // the plan (n.writeChange) before we write the corresponding state - // (n.writeResourceInstanceState). - // - // This is because the planned resource state will normally have the - // status of states.ObjectPlanned, which causes later logic to refer to - // the contents of the plan to retrieve the resource data. Because - // there is no shared lock between these two data structures, reversing - // the order of these writes will cause a brief window of inconsistency - // which can lead to a failed safety check. - // - // Future work should adjust these APIs such that it is impossible to - // update these two data structures incorrectly through any objects - // reachable via the terraform.EvalContext API. - diags = diags.Append(n.writeChange(ctx, change, "")) - - diags = diags.Append(n.writeResourceInstanceState(ctx, instancePlanState, workingState)) - if diags.HasErrors() { - return diags - } - - // If this plan resulted in a NoOp, then apply won't have a chance to make - // any changes to the stored dependencies. Since this is a NoOp we know - // that the stored dependencies will have no effect during apply, and we can - // write them out now. - if change.Action == plans.NoOp && !depsEqual(instanceRefreshState.Dependencies, n.Dependencies) { - // the refresh state will be the final state for this resource, so - // finalize the dependencies here if they need to be updated. - instanceRefreshState.Dependencies = n.Dependencies - diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) + deferrals := ctx.Deferrals() + if deferred != nil { + // Then this resource has been deferred either during the import, + // refresh or planning stage. We'll report the deferral and + // store what we could produce in the deferral tracker. + deferrals.ReportResourceInstanceDeferred(addr, deferred.Reason, change) + } else if !deferrals.ShouldDeferResourceInstanceChanges(n.Addr, n.Dependencies) { + // We intentionally write the change before the subsequent checks, because + // all of the checks below this point are for problems caused by the + // context surrounding the change, rather than the change itself, and + // so it's helpful to still include the valid-in-isolation change as + // part of the plan as additional context in our error output. + // + // FIXME: it is currently important that we write resource changes to + // the plan (n.writeChange) before we write the corresponding state + // (n.writeResourceInstanceState). + // + // This is because the planned resource state will normally have the + // status of states.ObjectPlanned, which causes later logic to refer to + // the contents of the plan to retrieve the resource data. Because + // there is no shared lock between these two data structures, reversing + // the order of these writes will cause a brief window of inconsistency + // which can lead to a failed safety check. + // + // Future work should adjust these APIs such that it is impossible to + // update these two data structures incorrectly through any objects + // reachable via the terraform.EvalContext API. + diags = diags.Append(n.writeChange(ctx, change, "")) + if diags.HasErrors() { + return diags + } + diags = diags.Append(n.writeResourceInstanceState(ctx, instancePlanState, workingState)) if diags.HasErrors() { return diags } - } - // Post-conditions might block completion. We intentionally do this - // _after_ writing the state/diff because we want to check against - // the result of the operation, and to fail on future operations - // until the user makes the condition succeed. - // (Note that some preconditions will end up being skipped during - // planning, because their conditions depend on values not yet known.) - checkDiags := evalCheckRules( - addrs.ResourcePostcondition, - n.Config.Postconditions, - ctx, n.ResourceInstanceAddr(), repeatData, - tfdiags.Error, - ) - diags = diags.Append(checkDiags) + diags = diags.Append(n.checkPreventDestroy(change)) + if diags.HasErrors() { + return diags + } + + // If this plan resulted in a NoOp, then apply won't have a chance to make + // any changes to the stored dependencies. Since this is a NoOp we know + // that the stored dependencies will have no effect during apply, and we can + // write them out now. + if change.Action == plans.NoOp && !depsEqual(instanceRefreshState.Dependencies, n.Dependencies) { + // the refresh state will be the final state for this resource, so + // finalize the dependencies here if they need to be updated. + instanceRefreshState.Dependencies = n.Dependencies + diags = diags.Append(n.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) + if diags.HasErrors() { + return diags + } + } + + // Post-conditions might block completion. We intentionally do this + // _after_ writing the state/diff because we want to check against + // the result of the operation, and to fail on future operations + // until the user makes the condition succeed. + // (Note that some preconditions will end up being skipped during + // planning, because their conditions depend on values not yet known.) + checkDiags := evalCheckRules( + addrs.ResourcePostcondition, + n.Config.Postconditions, + ctx, n.ResourceInstanceAddr(), repeatData, + checkRuleSeverity, + ) + diags = diags.Append(checkDiags) + } else { + // The deferrals tracker says that we must defer changes for + // this resource instance, presumably due to a dependency on an + // upstream object that was already deferred. Therefore we just + // report our own deferral (capturing a placeholder value in the + // deferral tracker) and don't add anything to the plan or + // working state. + // In this case, the expression evaluator should use the placeholder + // value registered here as the value of this resource instance, + // instead of using the plan. + deferrals.ReportResourceInstanceDeferred(n.Addr, providers.DeferredReasonDeferredPrereq, change) + } } else { // In refresh-only mode we need to evaluate the for-each expression in // order to supply the value to the pre- and post-condition check @@ -291,14 +492,14 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) // values, which could result in a post-condition check relying on that // value being inaccurate. Unless we decide to store the value of the // for-each expression in state, this is unavoidable. - forEach, _ := evaluateForEachExpression(n.Config.ForEach, ctx) + forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) repeatData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) checkDiags := evalCheckRules( addrs.ResourcePrecondition, n.Config.Preconditions, ctx, addr, repeatData, - tfdiags.Warning, + checkRuleSeverity, ) diags = diags.Append(checkDiags) @@ -321,9 +522,24 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) addrs.ResourcePostcondition, n.Config.Postconditions, ctx, addr, repeatData, - tfdiags.Warning, + checkRuleSeverity, ) diags = diags.Append(checkDiags) + + // In this case we skipped planning changes and therefore need to report the deferral + // here, if there was one. + if refreshDeferred != nil { + ctx.Deferrals().ReportResourceInstanceDeferred(addr, deferred.Reason, &plans.ResourceInstanceChange{ + Addr: n.Addr, + PrevRunAddr: n.Addr, + ProviderAddr: n.ResolvedProvider, + Change: plans.Change{ + Action: plans.Read, + Before: priorInstanceRefreshState.Value, + After: instanceRefreshState.Value, + }, + }) + } } return diags @@ -334,6 +550,9 @@ func (n *NodePlannableResourceInstance) managedResourceExecute(ctx EvalContext) // instance address is added to forceReplace func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repData instances.RepetitionData) tfdiags.Diagnostics { var diags tfdiags.Diagnostics + if n.Config == nil { + return diags + } for _, expr := range n.Config.TriggersReplacement { ref, replace, evalDiags := ctx.EvaluateReplaceTriggeredBy(expr, repData) @@ -359,6 +578,340 @@ func (n *NodePlannableResourceInstance) replaceTriggered(ctx EvalContext, repDat return diags } +func (n *NodePlannableResourceInstance) importState(ctx EvalContext, addr addrs.AbsResourceInstance, importTarget cty.Value, provider providers.Interface, providerSchema providers.ProviderSchema) (*states.ResourceInstanceObject, *providers.Deferred, tfdiags.Diagnostics) { + deferralAllowed := ctx.Deferrals().DeferralAllowed() + var diags tfdiags.Diagnostics + absAddr := addr.Resource.Absolute(ctx.Path()) + hookResourceID := HookResourceIdentity{ + Addr: absAddr, + ProviderAddr: n.ResolvedProvider.Provider, + } + + var deferred *providers.Deferred + + diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { + return h.PrePlanImport(hookResourceID, importTarget) + })) + if diags.HasErrors() { + return nil, deferred, diags + } + + schema := providerSchema.SchemaForResourceAddr(n.Addr.Resource.Resource) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append(fmt.Errorf("provider does not support resource type for %q", n.Addr)) + return nil, deferred, diags + } + + var resp providers.ImportResourceStateResponse + if n.override != nil { + // For overriding resources that are being imported, we cheat a little + // bit and look ahead at the configuration the user has provided and + // we'll use that as the basis for the resource we're going to make up + // that is due to be overridden. + + // Note, we know we have configuration as it's impossible to enable + // config generation during tests, and the validation that config exists + // if configuration generation is off has already happened. + if n.Config == nil { + // But, just in case we change this at some point in the future, + // let's add a specific error message here we can test for to + // document the expectation somewhere. This shouldn't happen in + // production, so we don't bother with a pretty error. + diags = diags.Append(fmt.Errorf("override blocks do not support config generation")) + return nil, deferred, diags + } + + forEach, _, _ := evaluateForEachExpression(n.Config.ForEach, ctx, false) + keyData := EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach) + configVal, _, configDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) + if configDiags.HasErrors() { + // We have an overridden resource so we're definitely in a test and + // the users config is not valid. So give up and just report the + // problems in the users configuration. Normally, we'd import the + // resource before giving up but for a test it doesn't matter, the + // test fails in the same way and the state is just lost anyway. + // + // If there were only warnings from the config then we'll duplicate + // them if we include them (as the config will be loaded again + // later), so only add the configDiags into the main diags if we + // found actual errors. + diags = diags.Append(configDiags) + return nil, deferred, diags + } + configVal, _ = configVal.UnmarkDeep() + + // Let's pretend we're reading the value as a data source so we + // pre-compute values now as if the resource has already been created. + override, overrideDiags := mocking.ComputedValuesForDataSource(configVal, &mocking.MockedData{ + Value: n.override.Values, + Range: n.override.Range, + ComputedAsUnknown: false, + }, schema.Body) + resp = providers.ImportResourceStateResponse{ + ImportedResources: []providers.ImportedResource{ + { + TypeName: addr.Resource.Resource.Type, + State: ephemeral.StripWriteOnlyAttributes(override, schema.Body), + }, + }, + Diagnostics: overrideDiags.InConfigBody(n.Config.Config, absAddr.String()), + } + } else { + if importTarget.Type().IsObjectType() { + // Identity-based import + resp = provider.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: addr.Resource.Resource.Type, + Identity: importTarget, + ClientCapabilities: ctx.ClientCapabilities(), + }) + } else { + // ID-based/string import + resp = provider.ImportResourceState(providers.ImportResourceStateRequest{ + TypeName: addr.Resource.Resource.Type, + ID: importTarget.AsString(), + ClientCapabilities: ctx.ClientCapabilities(), + }) + } + } + // If we don't support deferrals, but the provider reports a deferral and does not + // emit any error level diagnostics, we should emit an error. + if resp.Deferred != nil && !deferralAllowed && !resp.Diagnostics.HasErrors() { + diags = diags.Append(deferring.UnexpectedProviderDeferralDiagnostic(n.Addr)) + } + diags = diags.Append(resp.Diagnostics) + deferred = resp.Deferred + if diags.HasErrors() { + return nil, deferred, diags + } + + importType := "ID" + var importValue string + if importTarget.Type().IsObjectType() { + importType = "Identity" + importValue = tfdiags.ObjectToString(importTarget) + } else { + importValue = importTarget.AsString() + } + + imported := resp.ImportedResources + + if len(imported) > 1 { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Multiple import states not supported", + fmt.Sprintf("While attempting to import with %s %s, the provider "+ + "returned multiple resource instance states. This "+ + "is not currently supported.", + importType, importValue, + ), + )) + } + + if len(imported) == 0 { + + // Sanity check against the providers. If the provider defers the response, it may not have been able to return a state, so we'll only error if no deferral was returned. + if deferred == nil { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Import returned no resources", + fmt.Sprintf("While attempting to import with %s %s, the provider"+ + "returned no instance states.", + importType, importValue, + ), + )) + return nil, deferred, diags + } + + // If we were deferred, then let's make up a resource to represent the + // state we're going to import. + state := providers.ImportedResource{ + TypeName: addr.Resource.Resource.Type, + State: cty.NullVal(schema.Body.ImpliedType()), + } + + // We skip the read and further validation since we make up the state + // of the imported resource anyways. + return states.NewResourceInstanceObjectFromIR(state), deferred, diags + } + + for _, obj := range imported { + log.Printf("[TRACE] graphNodeImportState: import %s %q produced instance object of type %s", absAddr.String(), importValue, obj.TypeName) + } + + // We can only call the hooks and validate the imported state if we have + // actually done the import. + if resp.Deferred == nil { + // call post-import hook + diags = diags.Append(ctx.Hook(func(h Hook) (HookAction, error) { + return h.PostPlanImport(hookResourceID, imported) + })) + } + + if imported[0].TypeName == "" { + diags = diags.Append(fmt.Errorf("import of %s didn't set type", n.Addr.String())) + return nil, deferred, diags + } + + // Providers are supposed to return an identity when importing by identity + if importTarget.Type().IsObjectType() && imported[0].Identity.IsNull() { + diags = diags.Append(fmt.Errorf("import of %s didn't return an identity", n.Addr.String())) + return nil, deferred, diags + } + + // Providers are supposed to return null values for all write-only attributes + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( + "Import returned a non-null value for a write-only attribute", + func(path cty.Path) string { + return fmt.Sprintf( + "While attempting to import with %s %s, the provider %q returned a value for the write-only attribute \"%s%s\". Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + importType, importValue, n.ResolvedProvider, n.Addr, tfdiags.FormatCtyPath(path), + ) + }, + imported[0].State, + schema.Body, + ) + diags = diags.Append(writeOnlyDiags) + + if writeOnlyDiags.HasErrors() { + return nil, deferred, diags + } + + importedState := states.NewResourceInstanceObjectFromIR(imported[0]) + if deferred == nil && !importTarget.Type().IsObjectType() && importedState.Value.IsNull() { + // It's actually okay for a deferred import to have returned a null. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Import returned null resource", + fmt.Sprintf("While attempting to import with %s %s, the provider"+ + "returned an instance with no state.", + importType, importValue, + ), + )) + } + + // refresh + riNode := &NodeAbstractResourceInstance{ + Addr: n.Addr, + NodeAbstractResource: NodeAbstractResource{ + ResolvedProvider: n.ResolvedProvider, + }, + override: n.override, + } + instanceRefreshState, refreshDeferred, refreshDiags := riNode.refresh(ctx, states.NotDeposed, importedState, ctx.Deferrals().DeferralAllowed()) + diags = diags.Append(refreshDiags) + if diags.HasErrors() { + return instanceRefreshState, deferred, diags + } + + // report the refresh was deferred, we don't need to error since the import step succeeded + if deferred == nil && refreshDeferred != nil { + deferred = refreshDeferred + } + + // verify the existence of the imported resource + if refreshDeferred == nil && instanceRefreshState.Value.IsNull() { + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Cannot import non-existent remote object", + fmt.Sprintf( + "While attempting to import an existing object to %q, "+ + "the provider detected that no object exists with the given id. "+ + "Only pre-existing objects can be imported; check that the id "+ + "is correct and that it is associated with the provider's "+ + "configured region or endpoint, or use \"terraform apply\" to "+ + "create a new remote object for this resource.", + n.Addr, + ), + )) + return instanceRefreshState, deferred, diags + } + + // If we're importing and generating config, generate it now. We only + // generate config if the import isn't being deferred. We should generate + // the configuration in the plan that the import is actually happening in. + if deferred == nil && len(n.generateConfigPath) > 0 { + if n.Config != nil { + return instanceRefreshState, nil, diags.Append(fmt.Errorf("tried to generate config for %s, but it already exists", n.Addr)) + } + + // Generate the HCL string first, then parse the HCL body from it. + // First we generate the contents of the resource block for use within + // the planning node. Then we wrap it in an enclosing resource block to + // pass into the plan for rendering. + generatedHCLAttributes, generatedDiags := n.generateHCLStringAttributes(n.Addr, instanceRefreshState, schema.Body) + diags = diags.Append(generatedDiags) + + n.generatedConfigHCL = genconfig.WrapResourceContents(n.Addr, generatedHCLAttributes) + + // parse the "file" as HCL to get the hcl.Body + synthHCLFile, hclDiags := hclsyntax.ParseConfig([]byte(generatedHCLAttributes), filepath.Base(n.generateConfigPath), hcl.Pos{Byte: 0, Line: 1, Column: 1}) + diags = diags.Append(hclDiags) + if hclDiags.HasErrors() { + return instanceRefreshState, nil, diags + } + + // We have to do a kind of mini parsing of the content here to correctly + // mark attributes like 'provider' as hidden. We only care about the + // resulting content, so it's remain that gets passed into the resource + // as the config. + _, remain, resourceDiags := synthHCLFile.Body.PartialContent(configs.ResourceBlockSchema) + diags = diags.Append(resourceDiags) + if resourceDiags.HasErrors() { + return instanceRefreshState, nil, diags + } + + n.Config = &configs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: n.Addr.Resource.Resource.Type, + Name: n.Addr.Resource.Resource.Name, + Config: remain, + Managed: &configs.ManagedResource{}, + Provider: n.ResolvedProvider.Provider, + } + } + + if deferred == nil { + // Only write the state if the change isn't being deferred. We're also + // reporting the deferred status to the caller, so they should know + // not to read from the state. + diags = diags.Append(riNode.writeResourceInstanceState(ctx, instanceRefreshState, refreshState)) + } + return instanceRefreshState, deferred, diags +} + +// generateHCLStringAttributes produces a string in HCL format for the given +// resource state and schema without the surrounding block. +func (n *NodePlannableResourceInstance) generateHCLStringAttributes(addr addrs.AbsResourceInstance, state *states.ResourceInstanceObject, schema *configschema.Block) (string, tfdiags.Diagnostics) { + filteredSchema := schema.Filter( + configschema.FilterOr( + configschema.FilterReadOnlyAttribute, + configschema.FilterDeprecatedAttribute, + + // The legacy SDK adds an Optional+Computed "id" attribute to the + // resource schema even if not defined in provider code. + // During validation, however, the presence of an extraneous "id" + // attribute in config will cause an error. + // Remove this attribute so we do not generate an "id" attribute + // where there is a risk that it is not in the real resource schema. + // + // TRADEOFF: Resources in which there actually is an + // Optional+Computed "id" attribute in the schema will have that + // attribute missing from generated config. + configschema.FilterHelperSchemaIdAttribute, + ), + configschema.FilterDeprecatedBlock, + ) + + providerAddr := addrs.LocalProviderConfig{ + LocalName: n.ResolvedProvider.Provider.Type, + Alias: n.ResolvedProvider.Alias, + } + + return genconfig.GenerateResourceContents(addr, filteredSchema, providerAddr, state.Value) +} + // mergeDeps returns the union of 2 sets of dependencies func mergeDeps(a, b []addrs.ConfigResource) []addrs.ConfigResource { switch { diff --git a/internal/terraform/node_resource_plan_orphan.go b/internal/terraform/node_resource_plan_orphan.go index 94aa18f980..51364049f2 100644 --- a/internal/terraform/node_resource_plan_orphan.go +++ b/internal/terraform/node_resource_plan_orphan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,6 +9,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" ) @@ -21,6 +25,14 @@ type NodePlannableResourceInstanceOrphan struct { // skipPlanChanges indicates we should skip trying to plan change actions // for any instances. skipPlanChanges bool + + // forgetResources lists resources that should not be destroyed, only removed + // from state. + forgetResources []addrs.ConfigResource + + // forgetModules lists modules that should not be destroyed, only removed + // from state. + forgetModules []addrs.Module } var ( @@ -98,65 +110,109 @@ func (n *NodePlannableResourceInstanceOrphan) managedResourceExecute(ctx EvalCon return diags } - if !n.skipRefresh { + var forget bool + for _, ft := range n.forgetResources { + if ft.Equal(n.ResourceAddr()) { + forget = true + } + } + for _, fm := range n.forgetModules { + if fm.TargetContains(n.Addr) { + forget = true + } + } + + if !n.skipRefresh && !forget { // Refresh this instance even though it is going to be destroyed, in // order to catch missing resources. If this is a normal plan, // providers expect a Read request to remove missing resources from the // plan before apply, and may not handle a missing resource during // Delete correctly. If this is a simple refresh, Terraform is // expected to remove the missing resource from the state entirely - refreshedState, refreshDiags := n.refresh(ctx, states.NotDeposed, oldState) + refreshedState, refreshDeferred, refreshDiags := n.refresh(ctx, states.NotDeposed, oldState, ctx.Deferrals().DeferralAllowed()) diags = diags.Append(refreshDiags) if diags.HasErrors() { return diags } - diags = diags.Append(n.writeResourceInstanceState(ctx, refreshedState, refreshState)) - if diags.HasErrors() { - return diags - } - - // If we refreshed then our subsequent planning should be in terms of - // the new object, not the original object. oldState = refreshedState + + if refreshDeferred == nil { + // only update the state if we're not deferring the change + diags = diags.Append(n.writeResourceInstanceState(ctx, refreshedState, refreshState)) + if diags.HasErrors() { + return diags + } + } } - if !n.skipPlanChanges { - var change *plans.ResourceInstanceChange - change, destroyPlanDiags := n.planDestroy(ctx, oldState, "") - diags = diags.Append(destroyPlanDiags) - if diags.HasErrors() { - return diags - } + shouldDefer := ctx.Deferrals().ShouldDeferResourceInstanceChanges(n.Addr, n.Dependencies) + var change *plans.ResourceInstanceChange + var pDiags tfdiags.Diagnostics + var deferred *providers.Deferred + if forget { + change, pDiags = n.planForget(ctx, oldState, "") + diags = diags.Append(pDiags) + } else { + change, deferred, pDiags = n.planDestroy(ctx, oldState, "") + diags = diags.Append(pDiags) + } + if diags.HasErrors() { + return diags + } + + // We might be able to offer an approximate reason for why we are + // planning to delete this object. (This is best-effort; we might + // sometimes not have a reason.) + change.ActionReason = n.deleteActionReason(ctx) + + if deferred != nil { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, deferred.Reason, change) + return diags + } else if shouldDefer { + ctx.Deferrals().ReportResourceInstanceDeferred(n.Addr, providers.DeferredReasonDeferredPrereq, change) + return diags + } + + // If we're skipping planning, all we need to do is write the state. If the + // refresh indicates the instance no longer exists, there is also nothing + // to plan because there is no longer any state and it doesn't exist in the + // config. + if n.skipPlanChanges || oldState == nil || oldState.Value.IsNull() { + return diags.Append(n.writeResourceInstanceState(ctx, oldState, workingState)) + } + + // We intentionally write the change before the subsequent checks, because + // all of the checks below this point are for problems caused by the + // context surrounding the change, rather than the change itself, and + // so it's helpful to still include the valid-in-isolation change as + // part of the plan as additional context in our error output. + diags = diags.Append(n.writeChange(ctx, change, "")) + if diags.HasErrors() { + return diags + } + + if !forget { diags = diags.Append(n.checkPreventDestroy(change)) if diags.HasErrors() { return diags } - - // We might be able to offer an approximate reason for why we are - // planning to delete this object. (This is best-effort; we might - // sometimes not have a reason.) - change.ActionReason = n.deleteActionReason(ctx) - - diags = diags.Append(n.writeChange(ctx, change, "")) - if diags.HasErrors() { - return diags - } - - diags = diags.Append(n.writeResourceInstanceState(ctx, nil, workingState)) - } else { - // The working state should at least be updated with the result - // of upgrading and refreshing from above. - diags = diags.Append(n.writeResourceInstanceState(ctx, oldState, workingState)) } - return diags + return diags.Append(n.writeResourceInstanceState(ctx, nil, workingState)) } func (n *NodePlannableResourceInstanceOrphan) deleteActionReason(ctx EvalContext) plans.ResourceInstanceChangeActionReason { cfg := n.Config if cfg == nil { + if !n.Addr.Equal(n.prevRunAddr(ctx)) { + // This means the resource was moved - see also + // ResourceInstanceChange.Moved() which calculates + // this the same way. + return plans.ResourceInstanceDeleteBecauseNoMoveTarget + } + return plans.ResourceInstanceDeleteBecauseNoResourceConfig } @@ -238,7 +294,7 @@ func (n *NodePlannableResourceInstanceOrphan) deleteActionReason(ctx EvalContext // First we'll check whether our containing module instance still // exists, so we can talk about that differently in the reason. declared := false - for _, inst := range expander.ExpandModule(n.Addr.Module.Module()) { + for _, inst := range expander.ExpandModule(n.Addr.Module.Module(), false) { if n.Addr.Module.Equal(inst) { declared = true break diff --git a/internal/terraform/node_resource_plan_orphan_test.go b/internal/terraform/node_resource_plan_orphan_test.go index f46c7a7091..8f2215289d 100644 --- a/internal/terraform/node_resource_plan_orphan_test.go +++ b/internal/terraform/node_resource_plan_orphan_test.go @@ -1,15 +1,19 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "testing" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/instances" "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/deferring" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" - "github.com/zclconf/go-cty/cty" ) func TestNodeResourcePlanOrphanExecute(t *testing.T) { @@ -38,14 +42,17 @@ func TestNodeResourcePlanOrphanExecute(t *testing.T) { StateState: state.SyncWrapper(), RefreshStateState: state.DeepCopy().SyncWrapper(), PrevRunStateState: state.DeepCopy().SyncWrapper(), - InstanceExpanderExpander: instances.NewExpander(), + InstanceExpanderExpander: instances.NewExpander(nil), ProviderProvider: p, - ProviderSchemaSchema: &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "test_object": simpleTestSchema(), + ProviderSchemaSchema: providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: simpleTestSchema(), + }, }, }, ChangesChanges: plans.NewChanges().SyncWrapper(), + DeferralsState: deferring.NewDeferred(false), } node := NodePlannableResourceInstanceOrphan{ @@ -96,20 +103,23 @@ func TestNodeResourcePlanOrphanExecute_alreadyDeleted(t *testing.T) { p := simpleMockProvider() p.ConfigureProvider(providers.ConfigureProviderRequest{}) p.ReadResourceResponse = &providers.ReadResourceResponse{ - NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_string"].Block.ImpliedType()), + NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_string"].Body.ImpliedType()), } ctx := &MockEvalContext{ StateState: state.SyncWrapper(), RefreshStateState: refreshState.SyncWrapper(), PrevRunStateState: prevRunState.SyncWrapper(), - InstanceExpanderExpander: instances.NewExpander(), + InstanceExpanderExpander: instances.NewExpander(nil), ProviderProvider: p, - ProviderSchemaSchema: &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "test_object": simpleTestSchema(), + ProviderSchemaSchema: providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: simpleTestSchema(), + }, }, }, ChangesChanges: changes.SyncWrapper(), + DeferralsState: deferring.NewDeferred(false), } node := NodePlannableResourceInstanceOrphan{ @@ -137,12 +147,77 @@ func TestNodeResourcePlanOrphanExecute_alreadyDeleted(t *testing.T) { if got := refreshState.ResourceInstance(addr); got != nil { t.Errorf("refresh state has entry for %s; should've been removed", addr) } - if got := changes.ResourceInstance(addr); got == nil { - t.Errorf("no entry for %s in the planned changes; should have a NoOp change", addr) - } else { - if got, want := got.Action, plans.NoOp; got != want { - t.Errorf("planned change for %s has wrong action\ngot: %s\nwant: %s", addr, got, want) - } + if got := changes.ResourceInstance(addr); got != nil { + t.Errorf("there should be no change for the %s instance, got %s", addr, got.Action) + } +} + +// This test describes a situation which should not be possible, as this node +// should never work on deposed instances. However, a bug elsewhere resulted in +// this code path being exercised and triggered a panic. As a result, the +// assertions at the end of the test are minimal, as the behaviour (aside from +// not panicking) is unspecified. +func TestNodeResourcePlanOrphanExecute_deposed(t *testing.T) { + addr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_object", + Name: "foo", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance) + + state := states.NewState() + state.Module(addrs.RootModuleInstance).SetResourceInstanceDeposed( + addr.Resource, + states.NewDeposedKey(), + &states.ResourceInstanceObjectSrc{ + AttrsFlat: map[string]string{ + "test_string": "foo", + }, + Status: states.ObjectReady, + }, + addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + ) + refreshState := state.DeepCopy() + prevRunState := state.DeepCopy() + changes := plans.NewChanges() + + p := simpleMockProvider() + p.ConfigureProvider(providers.ConfigureProviderRequest{}) + p.ReadResourceResponse = &providers.ReadResourceResponse{ + NewState: cty.NullVal(p.GetProviderSchemaResponse.ResourceTypes["test_string"].Body.ImpliedType()), + } + ctx := &MockEvalContext{ + StateState: state.SyncWrapper(), + RefreshStateState: refreshState.SyncWrapper(), + PrevRunStateState: prevRunState.SyncWrapper(), + InstanceExpanderExpander: instances.NewExpander(nil), + ProviderProvider: p, + ProviderSchemaSchema: providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ + "test_object": { + Body: simpleTestSchema(), + }, + }, + }, + ChangesChanges: changes.SyncWrapper(), + DeferralsState: deferring.NewDeferred(false), } + node := NodePlannableResourceInstanceOrphan{ + NodeAbstractResourceInstance: &NodeAbstractResourceInstance{ + NodeAbstractResource: NodeAbstractResource{ + ResolvedProvider: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("test"), + Module: addrs.RootModule, + }, + }, + Addr: mustResourceInstanceAddr("test_object.foo"), + }, + } + diags := node.Execute(ctx, walkPlan) + if diags.HasErrors() { + t.Fatalf("unexpected error: %s", diags.Err()) + } } diff --git a/internal/terraform/node_resource_plan_partialexp.go b/internal/terraform/node_resource_plan_partialexp.go new file mode 100644 index 0000000000..d2675b0449 --- /dev/null +++ b/internal/terraform/node_resource_plan_partialexp.go @@ -0,0 +1,405 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "log" + "strings" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/instances" + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/plans/objchange" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// nodePlannablePartialExpandedResource is a graph node that stands in for +// an unbounded set of potential resource instances that we don't yet know. +// +// Its job is to check the configuration as much as we can with the information +// that's available (so we can raise an error early if something is clearly +// wrong across _all_ potential instances) and to record a placeholder value +// for use when evaluating other objects that refer to this resource. +// +// This is the partial-expanded equivalent of NodePlannableResourceInstance. +type nodePlannablePartialExpandedResource struct { + addr addrs.PartialExpandedResource + config *configs.Resource + resolvedProvider addrs.AbsProviderConfig + skipPlanChanges bool + preDestroyRefresh bool +} + +var ( + _ graphNodeEvalContextScope = (*nodePlannablePartialExpandedResource)(nil) + _ GraphNodeConfigResource = (*nodePlannablePartialExpandedResource)(nil) + _ GraphNodeExecutable = (*nodePlannablePartialExpandedResource)(nil) +) + +// Name implements [dag.NamedVertex]. +func (n *nodePlannablePartialExpandedResource) Name() string { + return n.addr.String() +} + +// Path implements graphNodeEvalContextScope. +func (n *nodePlannablePartialExpandedResource) Path() evalContextScope { + if moduleAddr, ok := n.addr.ModuleInstance(); ok { + return evalContextModuleInstance{Addr: moduleAddr} + } else if moduleAddr, ok := n.addr.PartialExpandedModule(); ok { + return evalContextPartialExpandedModule{Addr: moduleAddr} + } else { + // Should not get here: at least one of the two cases above + // should always be true for any valid addrs.PartialExpandedResource + panic("addrs.PartialExpandedResource has neither a partial-expanded or a fully-expanded module instance address") + } +} + +// ResourceAddr implements GraphNodeConfigResource. +func (n *nodePlannablePartialExpandedResource) ResourceAddr() addrs.ConfigResource { + return n.addr.ConfigResource() +} + +// Execute implements GraphNodeExecutable. +func (n *nodePlannablePartialExpandedResource) Execute(ctx EvalContext, op walkOperation) tfdiags.Diagnostics { + // Because this node type implements [graphNodeEvalContextScope], the + // given EvalContext could either be for a fully-expanded module instance + // or an unbounded set of potential module instances sharing a common + // prefix. The logic here doesn't need to vary between the two because + // the differences are encapsulated in the EvalContext abstraction, + // but if you're unsure which of the two is being used then look for + // the following line in the logs to see if there's a [*] marker on + // any of the module instance steps, or if the [*] is applied only to + // the resource itself. + // + // Fully-expanded module example: + // + // module.foo["a"].type.name[*] + // + // Partial-expanded module example: + // + // module.foo[*].type.name[*] + // + log.Printf("[TRACE] nodePlannablePartialExpandedResource: checking all of %s", n.addr.String()) + + switch op { + case walkPlanDestroy: + // During destroy plans, we never include partial-expanded resources. + // We're only interested in fully-expanded resources that we know we + // need to destroy. + return nil + case walkPlan: + if n.preDestroyRefresh || n.skipPlanChanges { + // During any kind of refresh, we also don't really care about + // partial resources. We only care about the fully-expanded resources + // already in state, so we don't need to plan partial resources. + return nil + } + + default: + // Continue with the normal planning process + } + + var diags tfdiags.Diagnostics + switch n.addr.Resource().Mode { + case addrs.ManagedResourceMode: + change, changeDiags := n.managedResourceExecute(ctx) + diags = diags.Append(changeDiags) + ctx.Deferrals().ReportResourceExpansionDeferred(n.addr, change) + case addrs.DataResourceMode: + change, changeDiags := n.dataResourceExecute(ctx) + diags = diags.Append(changeDiags) + ctx.Deferrals().ReportDataSourceExpansionDeferred(n.addr, change) + case addrs.EphemeralResourceMode: + ctx.Deferrals().ReportEphemeralResourceExpansionDeferred(n.addr) + default: + panic(fmt.Errorf("unsupported resource mode %s", n.config.Mode)) + } + + // Registering this allows downstream resources that depend on this one + // to know that they need to defer themselves too, in order to preserve + // correct dependency order. + return diags +} + +// Logic here mirrors (*NodePlannableResourceInstance).managedResourceExecute. +func (n *nodePlannablePartialExpandedResource) managedResourceExecute(ctx EvalContext) (*plans.ResourceInstanceChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // We cannot fully plan partial-expanded resources because we don't know + // what addresses they will have, but in this function we'll still go + // through many of the familiar motions of planning so that we can give + // feedback sooner if we can prove that the configuration is already + // invalid even with the partial information we have here. This is just + // to shorten the iterative journey, so nothing here actually contributes + // new actions to the plan. + + // We'll make a basic change for us to use as a placeholder for the time + // being, and we'll populate it as we get more info. + change := plans.ResourceInstanceChange{ + Addr: n.addr.UnknownResourceInstance(), + ProviderAddr: n.resolvedProvider, + Change: plans.Change{ + // We don't actually know the action, but we simulate the plan later + // as a create action so we'll use that here too. + Action: plans.Create, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.DynamicVal, // This will be populated later + }, + } + + provider, providerSchema, err := getProvider(ctx, n.resolvedProvider) + diags = diags.Append(err) + if diags.HasErrors() { + return &change, diags + } + + diags = diags.Append(validateSelfRef(n.addr.Resource(), n.config.Config, providerSchema)) + if diags.HasErrors() { + return &change, diags + } + + schema := providerSchema.SchemaForResourceAddr(n.addr.Resource()) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.addr.Resource().Type)) + return &change, diags + } + + // TODO: Normal managed resource planning + // (in [NodePlannableResourceInstance.managedResourceExecute]) deals with + // some additional things that we're just ignoring here for now. We should + // confirm whether it's really okay to ignore them here or if we ought + // to be partial-populating some results. + // + // Including but not necessarily limited to: + // - Somehow reacting if one or more of the possible resource instances + // is affected by an import block + // - Evaluating the preconditions/postconditions to see if they produce + // a definitive fail result even with the partial information. + + if n.skipPlanChanges { + // If we're supposed to be making a refresh-only plan then there's + // not really anything else to do here, since we can only refresh + // specific known resource instances (which another graph node should + // handle), so we'll just return early. + return &change, diags + } + + keyData := n.keyData() + + configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, schema.Body, nil, keyData) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return &change, diags + } + + unmarkedConfigVal, _ := configVal.UnmarkDeep() + log.Printf("[TRACE] Validating partially expanded config for %q", n.addr) + validateResp := provider.ValidateResourceConfig( + providers.ValidateResourceConfigRequest{ + TypeName: n.addr.Resource().Type, + Config: unmarkedConfigVal, + }, + ) + diags = diags.Append(validateResp.Diagnostics.InConfigBody(n.config.Config, n.addr.String())) + if diags.HasErrors() { + return &change, diags + } + + unmarkedConfigVal, unmarkedPaths := configVal.UnmarkDeepWithPaths() + priorVal := cty.NullVal(schema.Body.ImpliedType()) // we don't have any specific prior value to use + proposedNewVal := objchange.ProposedNew(schema.Body, priorVal, unmarkedConfigVal) + + // The provider now gets to plan an imaginary substitute that represents + // all of the possible resource instances together. Correctly-implemented + // providers should handle the extra unknown values here just as if they + // had been unknown an individual instance's configuration, but we can + // still find out if any of the known values are somehow invalid and + // learn a subset of the "computed" attribute values to save as part + // of our placeholder value for downstream checks. + resp := provider.PlanResourceChange(providers.PlanResourceChangeRequest{ + TypeName: n.addr.Resource().Type, + Config: unmarkedConfigVal, + PriorState: priorVal, + ProposedNewState: proposedNewVal, + // TODO: Should we send "ProviderMeta" here? We don't have the + // necessary data for that wired through here right now, but + // we might need to do that before stabilizing support for unknown + // resource instance expansion. + }) + diags = diags.Append(resp.Diagnostics.InConfigBody(n.config.Config, n.addr.String())) + if diags.HasErrors() { + return &change, diags + } + + plannedNewVal := resp.PlannedState + if plannedNewVal == cty.NilVal { + // Should never happen. Since real-world providers return via RPC a nil + // is always a bug in the client-side stub. This is more likely caused + // by an incompletely-configured mock provider in tests, though. + panic(fmt.Sprintf("PlanResourceChange of %s produced nil value", n.addr.String())) + } + + for _, err := range plannedNewVal.Type().TestConformance(schema.Body.ImpliedType()) { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.resolvedProvider.Provider, tfdiags.FormatErrorPrefixed(err, n.addr.String()), + ), + )) + } + if diags.HasErrors() { + return &change, diags + } + + if errs := objchange.AssertPlanValid(schema.Body, priorVal, unmarkedConfigVal, plannedNewVal); len(errs) > 0 { + if resp.LegacyTypeSystem { + // The shimming of the old type system in the legacy SDK is not precise + // enough to pass this consistency check, so we'll give it a pass here, + // but we will generate a warning about it so that we are more likely + // to notice in the logs if an inconsistency beyond the type system + // leads to a downstream provider failure. + var buf strings.Builder + fmt.Fprintf(&buf, + "[WARN] Provider %q produced an invalid plan for %s, but we are tolerating it because it is using the legacy plugin SDK.\n The following problems may be the cause of any confusing errors from downstream operations:", + n.resolvedProvider.Provider, n.addr.String(), + ) + for _, err := range errs { + fmt.Fprintf(&buf, "\n - %s", tfdiags.FormatError(err)) + } + log.Print(buf.String()) + } else { + for _, err := range errs { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Provider produced invalid plan", + fmt.Sprintf( + "Provider %q planned an invalid value for %s.\n\nThis is a bug in the provider, which should be reported in the provider's own issue tracker.", + n.resolvedProvider.Provider, tfdiags.FormatErrorPrefixed(err, n.addr.String()), + ), + )) + } + return &change, diags + } + } + + // We need to combine the dynamic marks with the static marks implied by + // the provider's schema. + plannedNewVal = plannedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.Body.SensitivePaths(plannedNewVal, nil); len(sensitivePaths) != 0 { + plannedNewVal = marks.MarkPaths(plannedNewVal, marks.Sensitive, sensitivePaths) + } + + change.After = plannedNewVal + change.Private = resp.PlannedPrivate + return &change, diags +} + +// Logic here mirrors a combination of (*NodePlannableResourceInstance).dataResourceExecute +// and (*NodeAbstractResourceInstance).planDataSource. +func (n *nodePlannablePartialExpandedResource) dataResourceExecute(ctx EvalContext) (*plans.ResourceInstanceChange, tfdiags.Diagnostics) { + var diags tfdiags.Diagnostics + + // Start with a basic change, then attempt to fill in the After value. + change := plans.ResourceInstanceChange{ + Addr: n.addr.UnknownResourceInstance(), + ProviderAddr: n.resolvedProvider, + Change: plans.Change{ + // Data sources can only Read. + Action: plans.Read, + Before: cty.NullVal(cty.DynamicPseudoType), + After: cty.DynamicVal, // hoping to fill this in + }, + // For now, this is the default reason for deferred data source reads. + // It's _basically_ the truth! + ActionReason: plans.ResourceInstanceReadBecauseConfigUnknown, + } + + // Unlike with the managed path, we don't ask the provider to *do* anything. + _, providerSchema, err := getProvider(ctx, n.resolvedProvider) + diags = diags.Append(err) + if diags.HasErrors() { + return &change, diags + } + + diags = diags.Append(validateSelfRef(n.addr.Resource(), n.config.Config, providerSchema)) + if diags.HasErrors() { + return &change, diags + } + + // This is the point where we switch to mirroring logic from + // NodeAbstractResourceInstance's planDataSource. If you were curious. + + schema := providerSchema.SchemaForResourceAddr(n.addr.Resource()) + if schema.Body == nil { + // Should be caught during validation, so we don't bother with a pretty error here + diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.addr.Resource().Type)) + return &change, diags + } + + keyData := n.keyData() + + configVal, _, configDiags := ctx.EvaluateBlock(n.config.Config, schema.Body, nil, keyData) + diags = diags.Append(configDiags) + if configDiags.HasErrors() { + return &change, diags + } + + // Note: We're deliberately not doing anything special for nested-in-a-check + // data sources. (*NodeAbstractResourceInstance).planDataSource has some + // special handling for these, but it's founded on the assumption that we're + // going to be able to actually read the data source. (Specifically: it + // blocks propagation of errors on read during plan, and ensures that we get + // a planned Read to execute during apply even if the data source would have + // been readable earlier.) But we're getting deferred anyway, so none of + // that is relevant on this path. 👍🏼 + + // Unlike the managed path, we don't call provider.ValidateResourceConfig; + // Terraform handles planning for data sources without hands-on input from + // the provider. BTW, this is about where we start mirroring planDataSource's + // logic for a data source with unknown config, which is sort of what we + // are, after all. + unmarkedConfigVal, unmarkedPaths := configVal.UnmarkDeepWithPaths() + proposedNewVal := objchange.PlannedDataResourceObject(schema.Body, unmarkedConfigVal) + proposedNewVal = proposedNewVal.MarkWithPaths(unmarkedPaths) + if sensitivePaths := schema.Body.SensitivePaths(proposedNewVal, nil); len(sensitivePaths) != 0 { + proposedNewVal = marks.MarkPaths(proposedNewVal, marks.Sensitive, sensitivePaths) + } + // yay we made it + change.After = proposedNewVal + return &change, diags +} + +// keyData returns suitable unknown values for count.index, each.key, and +// each.value, based on what we know of the resource config. When evaluating +// with this unknown key data, anything that varies between the instances will +// be unknown but we can still check the arguments that they all have in common. +func (n *nodePlannablePartialExpandedResource) keyData() instances.RepetitionData { + switch { + case n.config.ForEach != nil: + // We don't actually know the `for_each` type here, but we do at least + // know it's for_each. + return instances.UnknownForEachRepetitionData(cty.DynamicPseudoType) + case n.config.Count != nil: + return instances.UnknownCountRepetitionData + default: + // If we get here then we're presumably a single-instance resource + // inside a multi-instance module whose instances aren't known yet, + // and so we'll evaluate without any of the repetition symbols to + // still generate the usual errors if someone tries to use them here. + return instances.RepetitionData{ + CountIndex: cty.NilVal, + EachKey: cty.NilVal, + EachValue: cty.NilVal, + } + } +} diff --git a/internal/terraform/node_resource_plan_test.go b/internal/terraform/node_resource_plan_test.go deleted file mode 100644 index 78aa83079a..0000000000 --- a/internal/terraform/node_resource_plan_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package terraform - -import ( - "testing" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/instances" - "github.com/hashicorp/terraform/internal/states" -) - -func TestNodePlannableResourceExecute(t *testing.T) { - state := states.NewState() - ctx := &MockEvalContext{ - StateState: state.SyncWrapper(), - InstanceExpanderExpander: instances.NewExpander(), - } - - t.Run("no config", func(t *testing.T) { - node := NodePlannableResource{ - NodeAbstractResource: &NodeAbstractResource{ - Config: nil, - }, - Addr: mustAbsResourceAddr("test_instance.foo"), - } - diags := node.Execute(ctx, walkApply) - if diags.HasErrors() { - t.Fatalf("unexpected error: %s", diags.Err()) - } - if !state.Empty() { - t.Fatalf("expected no state, got:\n %s", state.String()) - } - }) - - t.Run("simple", func(t *testing.T) { - - node := NodePlannableResource{ - NodeAbstractResource: &NodeAbstractResource{ - Config: &configs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_instance", - Name: "foo", - }, - ResolvedProvider: addrs.AbsProviderConfig{ - Provider: addrs.NewDefaultProvider("test"), - Module: addrs.RootModule, - }, - }, - Addr: mustAbsResourceAddr("test_instance.foo"), - } - diags := node.Execute(ctx, walkApply) - if diags.HasErrors() { - t.Fatalf("unexpected error: %s", diags.Err()) - } - if state.Empty() { - t.Fatal("expected resources in state, got empty state") - } - r := state.Resource(mustAbsResourceAddr("test_instance.foo")) - if r == nil { - t.Fatal("test_instance.foo not found in state") - } - }) -} diff --git a/internal/terraform/node_resource_validate.go b/internal/terraform/node_resource_validate.go index a70bcdc4bc..f0dad0c2b5 100644 --- a/internal/terraform/node_resource_validate.go +++ b/internal/terraform/node_resource_validate.go @@ -1,20 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" + "log" "strings" "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/didyoumean" "github.com/hashicorp/terraform/internal/instances" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/ephemeral" + "github.com/hashicorp/terraform/internal/lang/format" + "github.com/hashicorp/terraform/internal/lang/langrefs" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // NodeValidatableResource represents a resource that is used for validation @@ -41,6 +48,13 @@ func (n *NodeValidatableResource) Path() addrs.ModuleInstance { // GraphNodeEvalable func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) { + // this is done first since there may not be config if we are generating it + diags = diags.Append(n.validateImportTargets(ctx)) + + if n.Config == nil { + return diags + } + diags = diags.Append(n.validateResource(ctx)) diags = diags.Append(n.validateCheckRules(ctx, n.Config)) @@ -48,14 +62,7 @@ func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (di if managed := n.Config.Managed; managed != nil { // Validate all the provisioners for _, p := range managed.Provisioners { - if p.Connection == nil { - p.Connection = n.Config.Managed.Connection - } else if n.Config.Managed.Connection != nil { - p.Connection.Config = configs.MergeBodies(n.Config.Managed.Connection.Config, p.Connection.Config) - } - - // Validate Provisioner Config - diags = diags.Append(n.validateProvisioner(ctx, p)) + diags = diags.Append(n.validateProvisioner(ctx, p, n.Config.Managed.Connection)) if diags.HasErrors() { return diags } @@ -67,7 +74,7 @@ func (n *NodeValidatableResource) Execute(ctx EvalContext, op walkOperation) (di // validateProvisioner validates the configuration of a provisioner belonging to // a resource. The provisioner config is expected to contain the merged // connection configurations. -func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *configs.Provisioner) tfdiags.Diagnostics { +func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *configs.Provisioner, baseConn *configs.Connection) tfdiags.Diagnostics { var diags tfdiags.Diagnostics provisioner, err := ctx.Provisioner(p.Type) @@ -112,8 +119,21 @@ func (n *NodeValidatableResource) validateProvisioner(ctx EvalContext, p *config // configuration keys that are not valid for *any* communicator, catching // typos early rather than waiting until we actually try to run one of // the resource's provisioners. - _, _, connDiags := n.evaluateBlock(ctx, p.Connection.Config, connectionBlockSupersetSchema) + + cfg := p.Connection.Config + if baseConn != nil { + // Merge the local config into the base connection config, if we + // both specified. + cfg = configs.MergeBodies(baseConn.Config, cfg) + } + + _, _, connDiags := n.evaluateBlock(ctx, cfg, connectionBlockSupersetSchema) diags = diags.Append(connDiags) + } else if baseConn != nil { + // Just validate the baseConn directly. + _, _, connDiags := n.evaluateBlock(ctx, baseConn.Config, connectionBlockSupersetSchema) + diags = diags.Append(connDiags) + } return diags } @@ -268,10 +288,6 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag if diags.HasErrors() { return diags } - if providerSchema == nil { - diags = diags.Append(fmt.Errorf("validateResource has nil schema for %s", n.Addr)) - return diags - } keyData := EvalDataForNoInstanceKey @@ -286,7 +302,7 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag // Basic type-checking of the count argument. More complete validation // of this will happen when we DynamicExpand during the plan walk. - countDiags := validateCount(ctx, n.Config.Count) + _, countDiags := evaluateCountExpressionValue(n.Config.Count, ctx) diags = diags.Append(countDiags) case n.Config.ForEach != nil: @@ -296,55 +312,21 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag } // Evaluate the for_each expression here so we can expose the diagnostics - forEachDiags := validateForEach(ctx, n.Config.ForEach) + forEachDiags := newForEachEvaluator(n.Config.ForEach, ctx, false).ValidateResourceValue() diags = diags.Append(forEachDiags) } diags = diags.Append(validateDependsOn(ctx, n.Config.DependsOn)) - // Validate the provider_meta block for the provider this resource - // belongs to, if there is one. - // - // Note: this will return an error for every resource a provider - // uses in a module, if the provider_meta for that module is - // incorrect. The only way to solve this that we've found is to - // insert a new ProviderMeta graph node in the graph, and make all - // that provider's resources in the module depend on the node. That's - // an awful heavy hammer to swing for this feature, which should be - // used only in limited cases with heavy coordination with the - // Terraform team, so we're going to defer that solution for a future - // enhancement to this functionality. - /* - if n.ProviderMetas != nil { - if m, ok := n.ProviderMetas[n.ProviderAddr.ProviderConfig.Type]; ok && m != nil { - // if the provider doesn't support this feature, throw an error - if (*n.ProviderSchema).ProviderMeta == nil { - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: fmt.Sprintf("Provider %s doesn't support provider_meta", cfg.ProviderConfigAddr()), - Detail: fmt.Sprintf("The resource %s belongs to a provider that doesn't support provider_meta blocks", n.Addr), - Subject: &m.ProviderRange, - }) - } else { - _, _, metaDiags := ctx.EvaluateBlock(m.Config, (*n.ProviderSchema).ProviderMeta, nil, EvalDataForNoInstanceKey) - diags = diags.Append(metaDiags) - } - } - } - */ - // BUG(paddy): we're not validating provider_meta blocks on EvalValidate right now - // because the ProviderAddr for the resource isn't available on the EvalValidate - // struct. - // Provider entry point varies depending on resource mode, because // managed resources and data resources are two distinct concepts // in the provider abstraction. switch n.Config.Mode { case addrs.ManagedResourceMode: - schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) + if schema.Body == nil { var suggestion string - if dSchema, _ := providerSchema.SchemaForResourceType(addrs.DataResourceMode, n.Config.Type); dSchema != nil { + if dSchema := providerSchema.SchemaForResourceType(addrs.DataResourceMode, n.Config.Type); dSchema.Body != nil { suggestion = fmt.Sprintf("\n\nDid you intend to use the data source %q? If so, declare this using a \"data\" block instead of a \"resource\" block.", n.Config.Type) } else if len(providerSchema.ResourceTypes) > 0 { suggestions := make([]string, 0, len(providerSchema.ResourceTypes)) @@ -365,16 +347,19 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) + configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return diags } + diags = diags.Append( + validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), + ) if n.Config.Managed != nil { // can be nil only in tests with poorly-configured mocks for _, traversal := range n.Config.Managed.IgnoreChanges { // validate the ignore_changes traversals apply. - moreDiags := schema.StaticValidateTraversal(traversal) + moreDiags := schema.Body.StaticValidateTraversal(traversal) diags = diags.Append(moreDiags) // ignore_changes cannot be used for Computed attributes, @@ -383,9 +368,9 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag // use that to check whether the Attribute is Computed and // non-Optional. if !diags.HasErrors() { - path := traversalToPath(traversal) + path, _ := traversalToPath(traversal) - attrSchema := schema.AttributeByPath(path) + attrSchema := schema.Body.AttributeByPath(path) if attrSchema != nil && !attrSchema.Optional && attrSchema.Computed { // ignore_changes uses absolute traversal syntax in config despite @@ -406,19 +391,21 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() + log.Printf("[TRACE] Validating config for %q", n.Addr) req := providers.ValidateResourceConfigRequest{ - TypeName: n.Config.Type, - Config: unmarkedConfigVal, + TypeName: n.Config.Type, + Config: unmarkedConfigVal, + ClientCapabilities: ctx.ClientCapabilities(), } resp := provider.ValidateResourceConfig(req) diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) case addrs.DataResourceMode: - schema, _ := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) - if schema == nil { + schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) + if schema.Body == nil { var suggestion string - if dSchema, _ := providerSchema.SchemaForResourceType(addrs.ManagedResourceMode, n.Config.Type); dSchema != nil { + if dSchema := providerSchema.SchemaForResourceType(addrs.ManagedResourceMode, n.Config.Type); dSchema.Body != nil { suggestion = fmt.Sprintf("\n\nDid you intend to use the managed resource type %q? If so, declare this using a \"resource\" block instead of a \"data\" block.", n.Config.Type) } else if len(providerSchema.DataSources) > 0 { suggestions := make([]string, 0, len(providerSchema.DataSources)) @@ -439,11 +426,14 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag return diags } - configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema, nil, keyData) + configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) diags = diags.Append(valDiags) if valDiags.HasErrors() { return diags } + diags = diags.Append( + validateResourceForbiddenEphemeralValues(ctx, configVal, schema.Body).InConfigBody(n.Config.Config, n.Addr.String()), + ) // Use unmarked value for validate request unmarkedConfigVal, _ := configVal.UnmarkDeep() @@ -454,6 +444,32 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag resp := provider.ValidateDataResourceConfig(req) diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) + case addrs.EphemeralResourceMode: + schema := providerSchema.SchemaForResourceType(n.Config.Mode, n.Config.Type) + if schema.Body == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid ephemeral resource", + Detail: fmt.Sprintf("The provider %s does not support ephemeral resource %q.", n.Provider().ForDisplay(), n.Config.Type), + Subject: &n.Config.TypeRange, + }) + return diags + } + + configVal, _, valDiags := ctx.EvaluateBlock(n.Config.Config, schema.Body, nil, keyData) + diags = diags.Append(valDiags) + if valDiags.HasErrors() { + return diags + } + // Use unmarked value for validate request + unmarkedConfigVal, _ := configVal.UnmarkDeep() + req := providers.ValidateEphemeralResourceConfigRequest{ + TypeName: n.Config.Type, + Config: unmarkedConfigVal, + } + + resp := provider.ValidateEphemeralResourceConfig(req) + diags = diags.Append(resp.Diagnostics.InConfigBody(n.Config.Config, n.Addr.String())) } return diags @@ -462,10 +478,10 @@ func (n *NodeValidatableResource) validateResource(ctx EvalContext) tfdiags.Diag func (n *NodeValidatableResource) evaluateExpr(ctx EvalContext, expr hcl.Expression, wantTy cty.Type, self addrs.Referenceable, keyData instances.RepetitionData) (cty.Value, tfdiags.Diagnostics) { var diags tfdiags.Diagnostics - refs, refDiags := lang.ReferencesInExpr(expr) + refs, refDiags := langrefs.ReferencesInExpr(addrs.ParseRef, expr) diags = diags.Append(refDiags) - scope := ctx.EvaluationScope(self, keyData) + scope := ctx.EvaluationScope(self, nil, keyData) hclCtx, moreDiags := scope.EvalContext(refs) diags = diags.Append(moreDiags) @@ -534,33 +550,203 @@ func (n *NodeValidatableResource) validateCheckRules(ctx EvalContext, config *co return diags } -func validateCount(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { - val, countDiags := evaluateCountExpressionValue(expr, ctx) - // If the value isn't known then that's the best we can do for now, but - // we'll check more thoroughly during the plan walk - if !val.IsKnown() { +// validateImportTargets checks that the import block expressions are valid. +func (n *NodeValidatableResource) validateImportTargets(ctx EvalContext) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if len(n.importTargets) == 0 { return diags } - if countDiags.HasErrors() { - diags = diags.Append(countDiags) + diags = diags.Append(n.validateConfigGen(ctx)) + + // Import blocks are only valid within the root module, and must be + // evaluated within that context + ctx = evalContextForModuleInstance(ctx, addrs.RootModuleInstance) + + for _, imp := range n.importTargets { + if imp.Config == nil { + // if we have a legacy addr, it will be supplied on the command line so + // there is nothing to check now and we need to wait for plan. + continue + } + + diags = diags.Append(validateImportSelfRef(n.Addr.Resource, imp.Config.ID)) + if diags.HasErrors() { + return diags + } + + if imp.Config.ForEach != nil { + diags = diags.Append(validateImportForEachRef(n.Addr.Resource, imp.Config.ForEach)) + if diags.HasErrors() { + return diags + } + + forEachData, _, forEachDiags := newForEachEvaluator(imp.Config.ForEach, ctx, true).ImportValues() + diags = diags.Append(forEachDiags) + if forEachDiags.HasErrors() { + return diags + } + + for _, keyData := range forEachData { + var evalDiags tfdiags.Diagnostics + to, evalDiags := evalImportToExpression(imp.Config.To, keyData) + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return diags + } + diags = diags.Append(validateImportTargetExpansion(n.Config, to, imp.Config.To)) + if diags.HasErrors() { + return diags + } + + if imp.Config.ID != nil { + _, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, keyData, true) + } else if imp.Config.Identity != nil { + providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) + if err != nil { + diags = diags.Append(err) + return diags + } + schema := providerSchema.SchemaForResourceAddr(to.Resource.Resource) + + _, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, keyData, true) + } + + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return diags + } + } + } else { + traversal, hds := hcl.AbsTraversalForExpr(imp.Config.To) + diags = diags.Append(hds) + to, tds := addrs.ParseAbsResourceInstance(traversal) + diags = diags.Append(tds) + if diags.HasErrors() { + return diags + } + + diags = diags.Append(validateImportTargetExpansion(n.Config, to, imp.Config.To)) + if diags.HasErrors() { + return diags + } + + var evalDiags tfdiags.Diagnostics + if imp.Config.ID != nil { + _, evalDiags = evaluateImportIdExpression(imp.Config.ID, ctx, EvalDataForNoInstanceKey, true) + } else if imp.Config.Identity != nil { + providerSchema, err := ctx.ProviderSchema(n.ResolvedProvider) + if err != nil { + diags = diags.Append(err) + return diags + } + schema := providerSchema.SchemaForResourceAddr(to.Resource.Resource) + + _, evalDiags = evaluateImportIdentityExpression(imp.Config.Identity, schema.Identity, ctx, EvalDataForNoInstanceKey, true) + } + + diags = diags.Append(evalDiags) + if diags.HasErrors() { + return diags + } + } } return diags } -func validateForEach(ctx EvalContext, expr hcl.Expression) (diags tfdiags.Diagnostics) { - val, forEachDiags := evaluateForEachExpressionValue(expr, ctx, true) - // If the value isn't known then that's the best we can do for now, but - // we'll check more thoroughly during the plan walk - if !val.IsKnown() { +// validateImportTargetExpansion ensures that the To address key and resource expansion mode both agree. +func validateImportTargetExpansion(cfg *configs.Resource, to addrs.AbsResourceInstance, toExpr hcl.Expression) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + forEach := cfg != nil && cfg.ForEach != nil + count := cfg != nil && cfg.Count != nil + + switch to.Resource.Key.(type) { + case addrs.StringKey: + if !forEach { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import 'to' expression", + Detail: "The target resource does not use for_each.", + Subject: toExpr.Range().Ptr(), + }) + } + case addrs.IntKey: + if !count { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import 'to' expression", + Detail: "The target resource does not use count.", + Subject: toExpr.Range().Ptr(), + }) + } + default: + if forEach { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import 'to' expression", + Detail: "The target resource is using for_each.", + Subject: toExpr.Range().Ptr(), + }) + } + + if count { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import 'to' expression", + Detail: "The target resource is using count.", + Subject: toExpr.Range().Ptr(), + }) + } + } + + return diags +} + +// validate imports with no config for possible config generation +func (n *NodeValidatableResource) validateConfigGen(ctx EvalContext) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + if n.Config != nil { return diags } - if forEachDiags.HasErrors() { - diags = diags.Append(forEachDiags) - } + // We won't have the config generation output path during validate, so only + // check if generation is at all possible. + for _, imp := range n.importTargets { + if !n.Addr.Module.IsRoot() { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Configuration for import target does not exist", + Detail: fmt.Sprintf("Resource %s not found. Only resources within the root module are eligible for config generation.", n.Addr), + Subject: imp.Config.To.Range().Ptr(), + }) + continue + } + + var toDiags tfdiags.Diagnostics + traversal, hd := hcl.AbsTraversalForExpr(imp.Config.To) + toDiags = toDiags.Append(hd) + to, td := addrs.ParseAbsResourceInstance(traversal) + toDiags = toDiags.Append(td) + + if toDiags.HasErrors() { + // these will be caught elsewhere with better context + continue + } + + if to.Resource.Key != addrs.NoKey || imp.Config.ForEach != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Configuration for import target does not exist", + Detail: "The given import block is not compatible with config generation. The -generate-config-out option cannot be used with import blocks which use for_each, or resources which use for_each or count.", + Subject: imp.Config.To.Range().Ptr(), + }) + } + } return diags } @@ -581,7 +767,7 @@ func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiag // we'll just eval it and count on the fact that our evaluator will // detect references to non-existent objects. if !diags.HasErrors() { - scope := ctx.EvaluationScope(nil, EvalDataForNoInstanceKey) + scope := ctx.EvaluationScope(nil, nil, EvalDataForNoInstanceKey) if scope != nil { // sometimes nil in tests, due to incomplete mocks _, refDiags = scope.EvalReference(ref, cty.DynamicPseudoType) diags = diags.Append(refDiags) @@ -590,3 +776,49 @@ func validateDependsOn(ctx EvalContext, dependsOn []hcl.Traversal) (diags tfdiag } return diags } + +// validateResourceForbiddenEphemeralValues returns an error diagnostic for each +// value anywhere inside the given value that is marked as ephemeral, for +// situations where ephemeral values are not permitted. +// +// All returned diagnostics are contextual diagnostics that must be finalized +// by calling [tfdiags.Diagnostics.InConfigBody] before returning them to +// any caller that expects fully-resolved diagnostics. +func validateResourceForbiddenEphemeralValues(ctx EvalContext, value cty.Value, schema *configschema.Block) (diags tfdiags.Diagnostics) { + for _, path := range ephemeral.EphemeralValuePaths(value) { + attr := schema.AttributeByPath(path) + + // If this attribute doesn't exist (usually through a dynamic type) or + // itself isn't write-only, it may be nested within a more complex type + // which is write-only. We need to walk upwards through the path + // segments and see of something wrapping this path is write-only. + for (attr == nil || !attr.WriteOnly) && len(path) > 1 { + path = path[:len(path)-1] + attr = schema.AttributeByPath((path)) + } + + // We know the config decoded, so the "attribute" exists in the + // schema somehow. If the ephemeral mark ended up being hoisted into + // a container however, especially if that container is a block, + // it's not actually an assignable attribute so we need to make a + // generic sounding error with a little more context because the + // AttributeValue diagnostic won't point to anything except the + // resource block. + if attr == nil { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid use of ephemeral value", + fmt.Sprintf("Ephemeral values are not valid for %q, because it is not an assignable attribute.", strings.TrimPrefix(format.CtyPath(path), ".")), + path, + )) + } else if !attr.WriteOnly { + diags = diags.Append(tfdiags.AttributeValue( + tfdiags.Error, + "Invalid use of ephemeral value", + fmt.Sprintf("Ephemeral values are not valid for %q, because it is not a write-only attribute and must be persisted to state.", strings.TrimPrefix(format.CtyPath(path), ".")), + path, + )) + } + } + return diags +} diff --git a/internal/terraform/node_resource_validate_test.go b/internal/terraform/node_resource_validate_test.go index b5b2af74cd..f48cdae003 100644 --- a/internal/terraform/node_resource_validate_test.go +++ b/internal/terraform/node_resource_validate_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -7,14 +10,16 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/lang/marks" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) func TestNodeValidatableResource_ValidateProvisioner_valid(t *testing.T) { @@ -51,7 +56,7 @@ func TestNodeValidatableResource_ValidateProvisioner_valid(t *testing.T) { }, } - diags := node.validateProvisioner(ctx, pc) + diags := node.validateProvisioner(ctx, pc, nil) if diags.HasErrors() { t.Fatalf("node.Eval failed: %s", diags.Err()) } @@ -96,7 +101,7 @@ func TestNodeValidatableResource_ValidateProvisioner__warning(t *testing.T) { } } - diags := node.validateProvisioner(ctx, pc) + diags := node.validateProvisioner(ctx, pc, nil) if len(diags) != 1 { t.Fatalf("wrong number of diagnostics in %s; want one warning", diags.ErrWithWarnings()) } @@ -141,7 +146,57 @@ func TestNodeValidatableResource_ValidateProvisioner__connectionInvalid(t *testi }, } - diags := node.validateProvisioner(ctx, pc) + diags := node.validateProvisioner(ctx, pc, nil) + if !diags.HasErrors() { + t.Fatalf("node.Eval succeeded; want error") + } + if len(diags) != 3 { + t.Fatalf("wrong number of diagnostics; want two errors\n\n%s", diags.Err()) + } + + errStr := diags.Err().Error() + if !(strings.Contains(errStr, "bananananananana") && strings.Contains(errStr, "bazaz")) { + t.Fatalf("wrong errors %q; want something about each of our invalid connInfo keys", errStr) + } +} + +func TestNodeValidatableResource_ValidateProvisioner_baseConnInvalid(t *testing.T) { + ctx := &MockEvalContext{} + ctx.installSimpleEval() + mp := &MockProvisioner{} + ps := &configschema.Block{} + ctx.ProvisionerSchemaSchema = ps + ctx.ProvisionerProvisioner = mp + + pc := &configs.Provisioner{ + Type: "baz", + Config: hcl.EmptyBody(), + } + + baseConn := &configs.Connection{ + Config: configs.SynthBody("", map[string]cty.Value{ + "type": cty.StringVal("ssh"), + "bananananananana": cty.StringVal("foo"), + "bazaz": cty.StringVal("bar"), + }), + } + + rc := &configs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "test_foo", + Name: "bar", + Config: configs.SynthBody("", map[string]cty.Value{}), + Managed: &configs.ManagedResource{}, + } + + node := NodeValidatableResource{ + NodeAbstractResource: &NodeAbstractResource{ + Addr: mustConfigResourceAddr("test_foo.bar"), + Config: rc, + }, + } + + diags := node.validateProvisioner(ctx, pc, baseConn) if !diags.HasErrors() { t.Fatalf("node.Eval succeeded; want error") } @@ -190,7 +245,7 @@ func TestNodeValidatableResource_ValidateResource_managedResource(t *testing.T) ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p err := node.validateResource(ctx) @@ -220,7 +275,7 @@ func TestNodeValidatableResource_ValidateResource_managedResourceCount(t *testin ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p tests := []struct { @@ -304,7 +359,7 @@ func TestNodeValidatableResource_ValidateResource_dataSource(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p diags := node.validateResource(ctx) @@ -340,7 +395,7 @@ func TestNodeValidatableResource_ValidateResource_valid(t *testing.T) { ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p diags := node.validateResource(ctx) @@ -377,7 +432,7 @@ func TestNodeValidatableResource_ValidateResource_warningsAndErrorsPassedThrough ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p diags := node.validateResource(ctx) @@ -440,7 +495,7 @@ func TestNodeValidatableResource_ValidateResource_invalidDependsOn(t *testing.T) ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p diags := node.validateResource(ctx) @@ -524,7 +579,7 @@ func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesNonexisten ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p diags := node.validateResource(ctx) @@ -565,11 +620,11 @@ func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesComputed(t }, } - mp := &MockProvider{ + mp := &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: ms}, + Provider: providers.Schema{Body: ms}, ResourceTypes: map[string]providers.Schema{ - "test_object": providers.Schema{Block: ms}, + "test_object": providers.Schema{Body: ms}, }, }, } @@ -607,7 +662,7 @@ func TestNodeValidatableResource_ValidateResource_invalidIgnoreChangesComputed(t ctx := &MockEvalContext{} ctx.installSimpleEval() - ctx.ProviderSchemaSchema = mp.ProviderSchema() + ctx.ProviderSchemaSchema = mp.GetProviderSchema() ctx.ProviderProvider = p diags := node.validateResource(ctx) @@ -633,3 +688,349 @@ The attribute computed_string is decided by the provider alone and therefore the t.Fatalf("wrong error\ngot: %s\nwant: Message containing %q", got, want) } } + +func Test_validateResourceForbiddenEphemeralValues(t *testing.T) { + simpleAttrs := map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + "input_wo": {Type: cty.String, Optional: true, WriteOnly: true}, + } + + dynAttrs := map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + "input_wo": {Type: cty.String, Optional: true, WriteOnly: true}, + "dyn": {Type: cty.DynamicPseudoType, Optional: true}, + "dyn_wo": {Type: cty.DynamicPseudoType, Optional: true, WriteOnly: true}, + } + + allAttrs := map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + "input_wo": {Type: cty.String, Optional: true, WriteOnly: true}, + "dyn": {Type: cty.DynamicPseudoType, Optional: true}, + "dyn_wo": {Type: cty.DynamicPseudoType, Optional: true, WriteOnly: true}, + "nested_single_attr": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: dynAttrs, + }, + Optional: true, + }, + "nested_list_attr": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: dynAttrs, + }, + Optional: true, + }, + "nested_set_attr": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + }, + }, + Optional: true, + }, + "nested_single_attr_wo": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: simpleAttrs, + }, + Optional: true, + WriteOnly: true, + }, + "nested_list_attr_wo": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: dynAttrs, + }, + Optional: true, + WriteOnly: true, + }, + "nested_set_attr_wo": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + }, + }, + Optional: true, + WriteOnly: true, + }, + } + + schema := &configschema.Block{ + Attributes: allAttrs, + BlockTypes: map[string]*configschema.NestedBlock{ + "single": { + Block: configschema.Block{ + Attributes: dynAttrs, + }, + Nesting: configschema.NestingSingle, + }, + "list": { + Block: configschema.Block{ + Attributes: dynAttrs, + }, + Nesting: configschema.NestingList, + }, + "set": { + Block: configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "input": {Type: cty.String, Optional: true}, + }, + }, + Nesting: configschema.NestingSet, + }, + "map": { + Block: configschema.Block{ + Attributes: simpleAttrs, + }, + Nesting: configschema.NestingMap, + }, + }, + } + + if err := schema.InternalValidate(); err != nil { + t.Fatal(err) + } + + type testCase struct { + obj cty.Value + valid bool + } + + tests := map[string]testCase{ + "wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "input_wo": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + valid: true, + }, + "not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + valid: false, + }, + "dyn_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "dyn_wo": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + valid: true, + }, + "dyn_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "dyn": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + valid: false, + }, + "nested_dyn_wo": { + // an ephemeral mark within a dynamic attribute is valid if the entire + // attr is write-only + obj: cty.ObjectVal(map[string]cty.Value{ + "dyn_wo": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: true, + }, + "nested_nested_dyn_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "dyn_wo": cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + }), + valid: true, + }, + "nested_dyn_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "dyn": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: false, + }, + "nested_single_attr_attr_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_single_attr": cty.ObjectVal(map[string]cty.Value{ + "input_wo": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: true, + }, + "nested_single_attr_attr_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_single_attr": cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: false, + }, + "nested_single_attr_wo_not_wo_attr": { + // we can assign an ephemeral to input because the outer + // nested_single_attr_wo attribute is write-only + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_single_attr_wo": cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: true, + }, + "nested_set_attr": { + // there is no possible input_wo because the schema validated that + // it cannot exist + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_set_attr": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: false, + }, + "nested_set_attr_wo": { + // assigning an ephemeral to input is valid, because the outer set is write-only + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_set_attr_wo": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: true, + }, + "nested_list_attr_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_list_attr": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: false, + }, + "nested_list_attr_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "nested_list_attr_wo": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: true, + }, + + "single_block_attr_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "single": cty.ObjectVal(map[string]cty.Value{ + "input_wo": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: true, + }, + "single_block_attr_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "single": cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + valid: false, + }, + "single_block_dyn_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "single": cty.ObjectVal(map[string]cty.Value{ + "dyn_wo": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + }), + valid: true, + }, + "single_block_dyn_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "single": cty.ObjectVal(map[string]cty.Value{ + "dyn": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + }), + valid: false, + }, + "list_block_attr_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "list": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input_wo": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: true, + }, + "list_block_attr_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "list": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: false, + }, + "list_block_dyn_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "list": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "dyn_wo": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + })}), + }), + valid: true, + }, + "list_block_dyn_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "list": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "dyn": cty.ObjectVal(map[string]cty.Value{ + "ephem": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + })}), + }), + valid: false, + }, + "set_block_attr_wo": { + // the ephemeral value within a set will always transfer the mark to + // the outer set, but set blocks cannot be write-only + obj: cty.ObjectVal(map[string]cty.Value{ + "set": cty.SetVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + })}), + }), + valid: false, + }, + "map_block_attr_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "test": cty.ObjectVal(map[string]cty.Value{ + "input_wo": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + }), + valid: true, + }, + "map_block_attr_not_wo": { + obj: cty.ObjectVal(map[string]cty.Value{ + "map": cty.MapVal(map[string]cty.Value{ + "test": cty.ObjectVal(map[string]cty.Value{ + "input": cty.StringVal("wo").Mark(marks.Ephemeral), + }), + }), + }), + valid: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + val, err := schema.CoerceValue(tc.obj) + if err != nil { + t.Fatal(err) + } + diags := validateResourceForbiddenEphemeralValues(nil, val, schema) + switch { + case tc.valid && diags.HasErrors(): + t.Fatal("unexpected diags:", diags.ErrWithWarnings()) + case !tc.valid && !diags.HasErrors(): + t.Fatal("expected diagnostics, got none") + } + }) + } +} diff --git a/internal/terraform/node_root_variable.go b/internal/terraform/node_root_variable.go index 33f439d7cd..3255cd211c 100644 --- a/internal/terraform/node_root_variable.go +++ b/internal/terraform/node_root_variable.go @@ -1,13 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "log" + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/tfdiags" - "github.com/zclconf/go-cty/cty" ) // NodeRootVariable represents a root variable input. @@ -21,6 +26,14 @@ type NodeRootVariable struct { // converted or validated, and can be nil for a variable that isn't // set at all. RawValue *InputValue + + // Planning must be set to true when building a planning graph, and must be + // false when building an apply graph. + Planning bool + + // DestroyApply must be set to true when applying a destroy operation and + // false otherwise. + DestroyApply bool } var ( @@ -79,6 +92,14 @@ func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Di } } + if n.Planning { + if checkState := ctx.Checks(); checkState.ConfigHasChecks(n.Addr.InModule(addrs.RootModule)) { + ctx.Checks().ReportCheckableObjects( + n.Addr.InModule(addrs.RootModule), + addrs.MakeSet[addrs.Checkable](n.Addr.Absolute(addrs.RootModuleInstance))) + } + } + finalVal, moreDiags := prepareFinalInputVariableValue( addr, givenVal, @@ -91,15 +112,11 @@ func (n *NodeRootVariable) Execute(ctx EvalContext, op walkOperation) tfdiags.Di return diags } - ctx.SetRootModuleArgument(addr.Variable, finalVal) + ctx.NamedValues().SetInputVariableValue(addr, finalVal) + + // Custom validation rules are handled by a separate graph node of type + // nodeVariableValidation, added by variableValidationTransformer. - moreDiags = evalVariableValidations( - addrs.RootModuleInstance.InputVariable(n.Addr.Name), - n.Config, - nil, // not set for root module variables - ctx, - ) - diags = diags.Append(moreDiags) return diags } @@ -113,3 +130,30 @@ func (n *NodeRootVariable) DotNode(name string, opts *dag.DotOpts) *dag.DotNode }, } } + +// variableValidationRules implements [graphNodeValidatableVariable]. +func (n *NodeRootVariable) variableValidationRules() (addrs.ConfigInputVariable, []*configs.CheckRule, hcl.Range) { + var defnRange hcl.Range + if n.RawValue != nil && n.RawValue.SourceType.HasSourceRange() { + defnRange = n.RawValue.SourceRange.ToHCL() + } else if n.Config != nil { // always in normal code, but sometimes not in unit tests + // For non-configuration-based definitions, such as environment + // variables or CLI arguments, we'll use the declaration as the + // "definition range" instead, since it's better than not indicating + // any source range at all. + defnRange = n.Config.DeclRange + } + + if n.DestroyApply { + // We don't perform any variable validation during the apply phase + // of a destroy, because validation rules typically aren't prepared + // for dealing with things already having been destroyed. + return n.Addr.InModule(addrs.RootModule), nil, defnRange + } + + var rules []*configs.CheckRule + if n.Config != nil { // always in normal code, but sometimes not in unit tests + rules = n.Config.Validations + } + return n.Addr.InModule(addrs.RootModule), rules, defnRange +} diff --git a/internal/terraform/node_root_variable_test.go b/internal/terraform/node_root_variable_test.go index 537cecce9f..d22d15cc8d 100644 --- a/internal/terraform/node_root_variable_test.go +++ b/internal/terraform/node_root_variable_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -8,8 +11,10 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/checks" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/namedvals" ) func TestNodeRootVariableExecute(t *testing.T) { @@ -29,18 +34,18 @@ func TestNodeRootVariableExecute(t *testing.T) { }, } + ctx.NamedValuesState = namedvals.NewState() + diags := n.Execute(ctx, walkApply) if diags.HasErrors() { t.Fatalf("unexpected error: %s", diags.Err()) } - if !ctx.SetRootModuleArgumentCalled { - t.Fatalf("ctx.SetRootModuleArgument wasn't called") + absAddr := addrs.RootModuleInstance.InputVariable(n.Addr.Name) + if !ctx.NamedValues().HasInputVariableValue(absAddr) { + t.Fatalf("no result was registered") } - if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want { - t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want) - } - if got, want := ctx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) { + if got, want := ctx.NamedValues().GetInputVariableValue(absAddr), cty.StringVal("true"); !want.RawEquals(got) { // NOTE: The given value was cty.Bool but the type constraint was // cty.String, so it was NodeRootVariable's responsibility to convert // as part of preparing the "final value". @@ -50,58 +55,63 @@ func TestNodeRootVariableExecute(t *testing.T) { t.Run("validation", func(t *testing.T) { ctx := new(MockEvalContext) - // The variable validation function gets called with Terraform's - // built-in functions available, so we need a minimal scope just for - // it to get the functions from. - ctx.EvaluationScopeScope = &lang.Scope{} + // Validation is actually handled by a separate node of type + // nodeVariableValidation, so this test will combine NodeRootVariable + // and nodeVariableValidation to check that they work together + // correctly in integration. - // We need to reimplement a _little_ bit of EvalContextBuiltin logic - // here to get a similar effect with EvalContextMock just to get the - // value to flow through here in a realistic way that'll make this test - // useful. - var finalVal cty.Value - ctx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) { - if addr.Name == "foo" { - t.Logf("set %s to %#v", addr.String(), v) - finalVal = v - } - } - ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value { - if addr.String() != "var.foo" { - return cty.NilVal - } - t.Logf("reading final val for %s (%#v)", addr.String(), finalVal) - return finalVal + ctx.NamedValuesState = namedvals.NewState() + + // We need a minimal scope that knows just enough to complete evaluation + // of this input variable. + varAddr := addrs.InputVariable{Name: "foo"} + varValue := cty.StringVal("5") + ctx.EvaluationScopeScope = &lang.Scope{ + Data: &fakeEvaluationData{ + inputVariables: map[addrs.InputVariable]cty.Value{ + // Nothing here to start, since in realistic use it + // would be NodeRootVariable that decides the final + // value to populate in here. + }, + }, } n := &NodeRootVariable{ - Addr: addrs.InputVariable{Name: "foo"}, + Addr: varAddr, Config: &configs.Variable{ - Name: "foo", + Name: varAddr.Name, Type: cty.Number, ConstraintType: cty.Number, Validations: []*configs.CheckRule{ { - Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { - // This returns true only if the given variable value - // is exactly cty.Number, which allows us to verify - // that we were given the value _after_ type - // conversion. - // This had previously not been handled correctly, - // as reported in: - // https://github.com/hashicorp/terraform/issues/29899 - vars := ctx.Variables["var"] - if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") { - t.Logf("var.foo isn't available") - return cty.False, nil - } - val := vars.GetAttr("foo") - if val == cty.NilVal || val.Type() != cty.Number { - t.Logf("var.foo is %#v; want a number", val) - return cty.False, nil - } - return cty.True, nil - }), + Condition: fakeHCLExpression( + []hcl.Traversal{ + { + hcl.TraverseRoot{Name: "var"}, + hcl.TraverseAttr{Name: varAddr.Name}, + }, + }, + func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { + // This returns true only if the given variable value + // is exactly cty.Number, which allows us to verify + // that we were given the value _after_ type + // conversion. + // This had previously not been handled correctly, + // as reported in: + // https://github.com/hashicorp/terraform/issues/29899 + vars := ctx.Variables["var"] + if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute(varAddr.Name) { + t.Logf("%s isn't available", varAddr) + return cty.False, nil + } + val := vars.GetAttr(varAddr.Name) + if val == cty.NilVal || val.Type() != cty.Number { + t.Logf("%s is %#v; want a number", varAddr, val) + return cty.False, nil + } + return cty.True, nil + }, + ), ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")), }, }, @@ -109,27 +119,59 @@ func TestNodeRootVariableExecute(t *testing.T) { RawValue: &InputValue{ // Note: This is a string, but the variable's type constraint // is number so it should be converted before use. - Value: cty.StringVal("5"), + Value: varValue, SourceType: ValueFromUnknown, }, + Planning: true, } + configAddr, validationRules, defnRange := n.variableValidationRules() + validateN := &nodeVariableValidation{ + configAddr: configAddr, + rules: validationRules, + defnRange: defnRange, + } + + ctx.ChecksState = checks.NewState(&configs.Config{ + Module: &configs.Module{ + Variables: map[string]*configs.Variable{ + varAddr.Name: n.Config, + }, + }, + }) diags := n.Execute(ctx, walkApply) if diags.HasErrors() { - t.Fatalf("unexpected error: %s", diags.Err()) + t.Fatalf("unexpected error from NodeRootVariable: %s", diags.Err()) } - if !ctx.SetRootModuleArgumentCalled { - t.Fatalf("ctx.SetRootModuleArgument wasn't called") + // We should now have a final value for the variable, pending validation. + absAddr := varAddr.Absolute(addrs.RootModuleInstance) + if !ctx.NamedValues().HasInputVariableValue(absAddr) { + t.Fatalf("no result value for input variable") } - if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want { - t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot: %s\nwant: %s", got, want) - } - if got, want := ctx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) { + if got, want := ctx.NamedValues().GetInputVariableValue(absAddr), cty.NumberIntVal(5); !want.RawEquals(got) { // NOTE: The given value was cty.Bool but the type constraint was // cty.String, so it was NodeRootVariable's responsibility to convert // as part of preparing the "final value". - t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want) + t.Fatalf("wrong value for ctx.SetRootModuleArgument\ngot: %#v\nwant: %#v", got, want) + } else { + // Our evaluation scope would now, if using the _real_ + // evaluationStateData implementation, include that final value. + // + // There are also integration tests covering the fully-integrated + // form of this test, using the real evaluation data implementation: + // TestContext2Plan_variableCustomValidationsSimple + // TestContext2Plan_variableCustomValidationsCrossRef + ctx.EvaluationScopeScope.Data.(*fakeEvaluationData).inputVariables[varAddr] = got + } + + diags = validateN.Execute(ctx, walkApply) + if diags.HasErrors() { + t.Fatalf("unexpected error from nodeVariableValidation: %s", diags.Err()) + } + + if status := ctx.Checks().ObjectCheckStatus(n.Addr.Absolute(addrs.RootModuleInstance)); status != checks.StatusPass { + t.Errorf("expected checks to pass but go %s instead", status) } }) } @@ -165,3 +207,32 @@ func (f fakeHCLExpressionFunc) Range() hcl.Range { func (f fakeHCLExpressionFunc) StartRange() hcl.Range { return f.Range() } + +// fakeHCLExpressionFuncWithTraversals extends [fakeHCLExpressionFunc] with +// a set of traversals that it reports from the [hcl.Expression.Variables] +// method, thereby allowing the expression to also ask Terraform to include +// specific data in the evaluation context that'll eventually be passed +// to the callback function. +type fakeHCLExpressionFuncWithTraversals struct { + fakeHCLExpressionFunc + traversals []hcl.Traversal +} + +// fakeHCLExpression returns a [fakeHCLExpressionFuncWithTraversals] that +// announces that it requires the traversals given in required, and then +// calls the eval callback when asked to evaluate itself. +// +// If the evaluation callback expects to find any variables in the given +// HCL evaluation context then the corresponding traversals MUST be given +// in "required", because Terraform typically populates the context only +// with the minimum required data for a given expression. +func fakeHCLExpression(required []hcl.Traversal, eval fakeHCLExpressionFunc) fakeHCLExpressionFuncWithTraversals { + return fakeHCLExpressionFuncWithTraversals{ + fakeHCLExpressionFunc: eval, + traversals: required, + } +} + +func (f fakeHCLExpressionFuncWithTraversals) Variables() []hcl.Traversal { + return f.traversals +} diff --git a/internal/terraform/node_value.go b/internal/terraform/node_value.go index 62a6e6ae83..d884e431bd 100644 --- a/internal/terraform/node_value.go +++ b/internal/terraform/node_value.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform // graphNodeTemporaryValue is implemented by nodes that may represent temporary diff --git a/internal/terraform/node_variable_validation.go b/internal/terraform/node_variable_validation.go new file mode 100644 index 0000000000..88d678394e --- /dev/null +++ b/internal/terraform/node_variable_validation.go @@ -0,0 +1,136 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "slices" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// nodeVariableValidation checks the author-specified validation rules against +// the final value of all expanded instances of a given input variable. +// +// A node of this type should always depend on another node that's responsible +// for deciding the final values for the nominated variable and registering +// them in the current "named values" state. [variableValidationTransformer] +// is the one responsible for inserting nodes of this type and ensuring that +// they each depend on the node that will register the final variable value. +type nodeVariableValidation struct { + configAddr addrs.ConfigInputVariable + rules []*configs.CheckRule + + // defnRange is whatever source range we consider to best represent + // the definition of the variable, which should ideally cover the + // source code of the expression that was assigned to the variable. + // When that's not possible -- for example, if the variable was + // set from a non-configuration location like an environment variable -- + // it's acceptable to use the declaration location instead. + defnRange hcl.Range + + // validateWalk is set to true during a validation walk, where any input + // variables are set to unknown values. Since we may have unknown values + // which will be known during plan, we need to be more lenient about what + // can be unknown in variable validation expressions. + validateWalk bool +} + +var _ GraphNodeModulePath = (*nodeVariableValidation)(nil) +var _ GraphNodeReferenceable = (*nodeVariableValidation)(nil) +var _ GraphNodeReferencer = (*nodeVariableValidation)(nil) +var _ GraphNodeExecutable = (*nodeVariableValidation)(nil) +var _ graphNodeTemporaryValue = (*nodeVariableValidation)(nil) + +func (n *nodeVariableValidation) Name() string { + return fmt.Sprintf("%s (validation)", n.configAddr.String()) +} + +// ModulePath implements [GraphNodeModulePath]. +func (n *nodeVariableValidation) ModulePath() addrs.Module { + return n.configAddr.Module +} + +// ReferenceableAddrs implements [GraphNodeReferenceable], announcing that +// this node contributes to the value for the input variable that it's +// validating, and must therefore run before any nodes that refer to it. +func (n *nodeVariableValidation) ReferenceableAddrs() []addrs.Referenceable { + return []addrs.Referenceable{n.configAddr.Variable} +} + +// nodeVariableValidation must act as if it's part of the associated variable +// node, and that means mirroring all that node's graph behavior. Root module +// variable are not temporary however, but because during a destroy we can't +// ensure that all references can be evaluated, we must skip validation unless +// absolutely necessary to avoid blocking the destroy from proceeding. +func (n *nodeVariableValidation) temporaryValue() bool { + return true +} + +// References implements [GraphNodeReferencer], announcing anything that +// the check rules refer to, other than the variable that's being validated +// (which gets its dependency connected by [variableValidationTransformer] +// instead). +func (n *nodeVariableValidation) References() []*addrs.Reference { + var ret []*addrs.Reference + for _, rule := range n.rules { + // We ignore all diagnostics here because if an expression contains + // invalid references then we'll catch them once we visit the + // node (method Execute). + condRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, rule.Condition) + msgRefs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, rule.ErrorMessage) + ret = n.appendRefsFilterSelf(ret, condRefs...) + ret = n.appendRefsFilterSelf(ret, msgRefs...) + } + return ret +} + +// appendRefsFilterSelf is a specialized version of builtin [append] that +// ignores any new references to the input variable represented by the +// reciever. +func (n *nodeVariableValidation) appendRefsFilterSelf(to []*addrs.Reference, new ...*addrs.Reference) []*addrs.Reference { + // We need to filter out any self-references, because those would + // make the resulting graph invalid and we don't need them because + // variableValidationTransformer should've arranged for us to + // already depend on whatever node provides the final value for + // this variable. + ret := slices.Grow(to, len(new)) + ourAddr := n.configAddr.Variable + for _, ref := range new { + if refAddr, ok := ref.Subject.(addrs.InputVariable); ok { + if refAddr == ourAddr { + continue + } + } + ret = append(ret, ref) + } + return ret +} + +func (n *nodeVariableValidation) Execute(globalCtx EvalContext, op walkOperation) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + // We need to perform validation work separately for each instance of + // the variable across expanded modules, because each one could potentially + // have a different value assigned to it and other different data in scope. + expander := globalCtx.InstanceExpander() + for _, modInst := range expander.ExpandModule(n.configAddr.Module, false) { + addr := n.configAddr.Variable.Absolute(modInst) + moduleCtx := globalCtx.withScope(evalContextModuleInstance{Addr: addr.Module}) + diags = diags.Append(evalVariableValidations( + addr, + moduleCtx, + n.rules, + n.defnRange, + n.validateWalk, + )) + } + + return diags +} diff --git a/internal/terraform/providers.go b/internal/terraform/providers.go new file mode 100644 index 0000000000..3272e5ba95 --- /dev/null +++ b/internal/terraform/providers.go @@ -0,0 +1,141 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" +) + +// checkExternalProviders verifies that all of the explicitly-declared +// external provider configuration requirements in the root module are +// satisfied by the given instances, and also that all of the given +// instances belong to providers that the overall configuration at least +// uses somewhere. +// +// At the moment we only use external provider configurations for module +// trees acting as Stack components and most other use will not offer any +// externally-configured providers at all, and so the errors returned +// here are somewhat vague to accommodate being used both to describe +// an invalid component configuration and the problem of trying to plan and +// apply a module that wasn't intended to be a root module. +func checkExternalProviders(rootCfg *configs.Config, plan *plans.Plan, state *states.State, got map[addrs.RootProviderConfig]providers.Interface) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + allowedProviders := make(map[addrs.Provider]bool) + for _, addr := range rootCfg.ProviderTypes() { + allowedProviders[addr] = true + } + if state != nil { + for _, addr := range state.ProviderAddrs() { + allowedProviders[addr.Provider] = true + } + } + if plan != nil { + for _, addr := range plan.ProviderAddrs() { + allowedProviders[addr.Provider] = true + } + } + requiredConfigs := rootCfg.EffectiveRequiredProviderConfigs().Keys() + + // Passed-in provider configurations can only be for providers that this + // configuration actually contains some use of. + // (This is an imprecise way of rejecting undeclared provider configs; + // we can't be precise because Terraform permits implicit default provider + // configurations.) + for cfgAddr := range got { + if !allowedProviders[cfgAddr.Provider] { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected provider configuration", + fmt.Sprintf("The plan options include a configuration for provider %s, which is not used anywhere in this configuration.", cfgAddr.Provider), + )) + } else if cfgAddr.Alias != "" && !requiredConfigs.Has(cfgAddr) { + // Additional (aliased) provider configurations must always be + // explicitly declared. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected provider configuration", + fmt.Sprintf("The plan options include a configuration for provider %s with alias %q, which is not declared by the root module.", cfgAddr.Provider, cfgAddr.Alias), + )) + } + } + + // The caller _must_ pass external provider configurations for any address + // that's been explicitly declared as required in the required_providers + // block. + for _, cfgAddr := range requiredConfigs { + if _, defined := got[cfgAddr]; !defined { + if cfgAddr.Alias == "" { + // We can't actually return an error here because it's valid + // to leave a default provider configuration implied as long + // as the provider itself will accept an all-null configuration, + // which we won't know until we actually start evaluating. + continue + } else { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Undefined provider configuration", + fmt.Sprintf( + "The root module declares that it requires the caller to pass a configuration for provider %s with alias %q.", + cfgAddr.Provider, cfgAddr.Alias, + ), + )) + } + } + } + + // It isn't valid to pass in a provider for an address that is associated + // with an explicit "provider" block in the root module, since that would + // make it ambiguous whether we're using the passed in one or the declared + // one. + for _, pc := range rootCfg.Module.ProviderConfigs { + absAddr := rootCfg.ResolveAbsProviderAddr(pc.Addr(), addrs.RootModule) + rootAddr := addrs.RootProviderConfig{ + Provider: absAddr.Provider, + Alias: absAddr.Alias, + } + if _, defined := got[rootAddr]; defined { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Unexpected provider configuration", + fmt.Sprintf("The plan options include provider configuration %s, but that conflicts with the explicitly-defined provider configuration at %s.", rootAddr, pc.DeclRange.String()), + )) + } + } + + return diags +} + +// externalProviderWrapper is a wrapper around a provider instance that +// intercepts methods that don't make sense to call on a provider instance +// passed in by an external caller which we assume is owned by the caller +// and pre-configured. +// +// This is a kinda-hacky way to deal with the fact that Terraform Core +// logic tends to assume it is responsible for the full lifecycle of a +// provider instance, which isn't true for externally-provided ones. +type externalProviderWrapper struct { + providers.Interface +} + +var _ providers.Interface = externalProviderWrapper{} + +// ConfigureProvider does nothing because external providers are supposed to +// be pre-configured before passing them to Terraform Core. +func (pw externalProviderWrapper) ConfigureProvider(providers.ConfigureProviderRequest) providers.ConfigureProviderResponse { + return providers.ConfigureProviderResponse{} +} + +// Close does nothing because the caller which provided an external provider +// client is the one responsible for eventually closing it. +func (pw externalProviderWrapper) Close() error { + return nil +} diff --git a/internal/terraform/provisioner_mock.go b/internal/terraform/provisioner_mock.go index fe76157a2d..b0bfc6b4b3 100644 --- a/internal/terraform/provisioner_mock.go +++ b/internal/terraform/provisioner_mock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/provisioner_mock_test.go b/internal/terraform/provisioner_mock_test.go index a9d9d7bf3d..1696d99a36 100644 --- a/internal/terraform/provisioner_mock_test.go +++ b/internal/terraform/provisioner_mock_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/reduce_plan.go b/internal/terraform/reduce_plan.go index 17a58eff5c..8fc2cfc682 100644 --- a/internal/terraform/reduce_plan.go +++ b/internal/terraform/reduce_plan.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/reduce_plan_test.go b/internal/terraform/reduce_plan_test.go index f32101aaf5..142782ec97 100644 --- a/internal/terraform/reduce_plan_test.go +++ b/internal/terraform/reduce_plan_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -430,7 +433,8 @@ func TestProcessIgnoreChangesIndividual(t *testing.T) { ignore[i] = trav } - ret, diags := processIgnoreChangesIndividual(test.Old, test.New, traversalsToPaths(ignore)) + paths, _ := traversalsToPaths(ignore) + ret, diags := processIgnoreChangesIndividual(test.Old, test.New, paths) if diags.HasErrors() { t.Fatal(diags.Err()) } diff --git a/internal/terraform/resource_eval_bench_test.go b/internal/terraform/resource_eval_bench_test.go new file mode 100644 index 0000000000..6b1a85baad --- /dev/null +++ b/internal/terraform/resource_eval_bench_test.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/states" + "github.com/zclconf/go-cty/cty" +) + +// Benchmark that stresses resource instance evaluation during plan. The +// references to resources with large count values force the evaluator to +// repeated return all instances, so accessing those changes should be made as +// efficient as possible. +func BenchmarkPlanLargeCountRefs(b *testing.B) { + m := testModuleInline(b, map[string]string{ + "main.tf": ` +resource "test_resource" "a" { + count = 512 + input = "ok" +} + +resource "test_resource" "b" { + count = length(test_resource.a) + input = test_resource.a +} + +module "mod" { + count = length(test_resource.a) + source = "./mod" + in = [test_resource.a[count.index].id, test_resource.b[count.index].id] +} + +output out { + value = module.mod +}`, + "./mod/main.tf": ` +variable "in" { +} + +output "out" { + value = var.in +} +`}) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "input": {Type: cty.DynamicPseudoType, Optional: true}, + }, + }, + }, + }) + + ctx := testContext2(b, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + b.ResetTimer() + for range b.N { + _, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + b.Fatal(diags.Err()) + } + } +} + +// Similar to PlanLargeCountRefs, this runs through Apply to benchmark the +// caching of decoded state values. +func BenchmarkApplyLargeCountRefs(b *testing.B) { + m := testModuleInline(b, map[string]string{ + "main.tf": ` +resource "test_resource" "a" { + count = 512 + input = "ok" +} + +resource "test_resource" "b" { + count = length(test_resource.a) + input = test_resource.a +} + +module "mod" { + count = length(test_resource.a) + source = "./mod" + in = [test_resource.a[count.index].id, test_resource.b[count.index].id] +} + +output out { + value = module.mod +}`, + "./mod/main.tf": ` +variable "in" { +} + +output "out" { + value = var.in +} +`}) + + p := new(testing_provider.MockProvider) + p.GetProviderSchemaResponse = getProviderSchemaResponseFromProviderSchema(&providerSchema{ + ResourceTypes: map[string]*configschema.Block{ + "test_resource": { + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Computed: true}, + "input": {Type: cty.DynamicPseudoType, Optional: true}, + }, + }, + }, + }) + + ctx := testContext2(b, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + plan, diags := ctx.Plan(m, states.NewState(), DefaultPlanOpts) + if diags.HasErrors() { + b.Fatal(diags.Err()) + } + + b.ResetTimer() + for range b.N { + ctx := testContext2(b, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + _, diags := ctx.Apply(plan, m, nil) + if diags.HasErrors() { + b.Fatal(diags.Err()) + } + } +} diff --git a/internal/terraform/resource_provider_mock_test.go b/internal/terraform/resource_provider_mock_test.go index 6592b0a960..45da48cbf1 100644 --- a/internal/terraform/resource_provider_mock_test.go +++ b/internal/terraform/resource_provider_mock_test.go @@ -1,28 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/zclconf/go-cty/cty" ) // mockProviderWithConfigSchema is a test helper to concisely create a mock // provider with the given schema for its own configuration. -func mockProviderWithConfigSchema(schema *configschema.Block) *MockProvider { - return &MockProvider{ +func mockProviderWithConfigSchema(schema *configschema.Block) *testing_provider.MockProvider { + return &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: schema}, + Provider: providers.Schema{Body: schema}, }, } } // mockProviderWithResourceTypeSchema is a test helper to concisely create a mock // provider with a schema containing a single resource type. -func mockProviderWithResourceTypeSchema(name string, schema *configschema.Block) *MockProvider { - return &MockProvider{ +func mockProviderWithResourceTypeSchema(name string, schema *configschema.Block) *testing_provider.MockProvider { + return &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ - Block: &configschema.Block{ + Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ "string": { Type: cty.String, @@ -40,36 +45,12 @@ func mockProviderWithResourceTypeSchema(name string, schema *configschema.Block) }, }, ResourceTypes: map[string]providers.Schema{ - name: providers.Schema{Block: schema}, + name: providers.Schema{Body: schema}, }, }, } } -// getProviderSchemaResponseFromProviderSchema is a test helper to convert a -// ProviderSchema to a GetProviderSchemaResponse for use when building a mock provider. -func getProviderSchemaResponseFromProviderSchema(providerSchema *ProviderSchema) *providers.GetProviderSchemaResponse { - resp := &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: providerSchema.Provider}, - ProviderMeta: providers.Schema{Block: providerSchema.ProviderMeta}, - ResourceTypes: map[string]providers.Schema{}, - DataSources: map[string]providers.Schema{}, - } - - for name, schema := range providerSchema.ResourceTypes { - resp.ResourceTypes[name] = providers.Schema{ - Block: schema, - Version: int64(providerSchema.ResourceTypeSchemaVersions[name]), - } - } - - for name, schema := range providerSchema.DataSources { - resp.DataSources[name] = providers.Schema{Block: schema} - } - - return resp -} - // simpleMockProvider returns a MockProvider that is pre-configured // with schema for its own config, for a resource type called "test_object" and // for a data source also called "test_object". @@ -87,16 +68,94 @@ func getProviderSchemaResponseFromProviderSchema(providerSchema *ProviderSchema) // the default schema stored in the field GetSchemaReturn. Each new call to // simpleTestProvider produces entirely new instances of all of the nested // objects so that callers can mutate without affecting mock objects. -func simpleMockProvider() *MockProvider { - return &MockProvider{ +func simpleMockProvider() *testing_provider.MockProvider { + return &testing_provider.MockProvider{ GetProviderSchemaResponse: &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{Block: simpleTestSchema()}, + Provider: providers.Schema{Body: simpleTestSchema()}, ResourceTypes: map[string]providers.Schema{ - "test_object": providers.Schema{Block: simpleTestSchema()}, + "test_object": providers.Schema{Body: simpleTestSchema()}, }, DataSources: map[string]providers.Schema{ - "test_object": providers.Schema{Block: simpleTestSchema()}, + "test_object": providers.Schema{Body: simpleTestSchema()}, }, }, } } + +// getProviderSchema is a helper to convert from the internal +// GetProviderSchemaResponse to a providerSchema. +func getProviderSchema(p *testing_provider.MockProvider) *providerSchema { + if p.GetProviderSchemaResponse == nil { + // Then just return an empty provider schema. + return &providerSchema{ + ResourceTypes: make(map[string]*configschema.Block), + ResourceTypeSchemaVersions: make(map[string]uint64), + DataSources: make(map[string]*configschema.Block), + } + } + + resp := p.GetProviderSchemaResponse + + schema := &providerSchema{ + Provider: resp.Provider.Body, + ProviderMeta: resp.ProviderMeta.Body, + ResourceTypes: map[string]*configschema.Block{}, + DataSources: map[string]*configschema.Block{}, + ResourceTypeSchemaVersions: map[string]uint64{}, + } + + for resType, s := range resp.ResourceTypes { + schema.ResourceTypes[resType] = s.Body + schema.ResourceTypeSchemaVersions[resType] = uint64(s.Version) + } + + for dataSource, s := range resp.DataSources { + schema.DataSources[dataSource] = s.Body + } + + return schema +} + +// the type was refactored out with all the functionality handled within the +// provider package, but we keep this here for a shim in existing tests. +type providerSchema struct { + Provider *configschema.Block + ProviderMeta *configschema.Block + ResourceTypes map[string]*configschema.Block + ResourceTypeSchemaVersions map[string]uint64 + DataSources map[string]*configschema.Block + IdentityTypes map[string]*configschema.Object + IdentityTypeSchemaVersions map[string]uint64 +} + +// getProviderSchemaResponseFromProviderSchema is a test helper to convert a +// providerSchema to a GetProviderSchemaResponse for use when building a mock provider. +func getProviderSchemaResponseFromProviderSchema(providerSchema *providerSchema) *providers.GetProviderSchemaResponse { + resp := &providers.GetProviderSchemaResponse{ + Provider: providers.Schema{Body: providerSchema.Provider}, + ProviderMeta: providers.Schema{Body: providerSchema.ProviderMeta}, + ResourceTypes: map[string]providers.Schema{}, + DataSources: map[string]providers.Schema{}, + } + + for name, schema := range providerSchema.ResourceTypes { + ps := providers.Schema{ + Body: schema, + Version: int64(providerSchema.ResourceTypeSchemaVersions[name]), + } + + id, ok := providerSchema.IdentityTypes[name] + if ok { + ps.Identity = id + ps.IdentityVersion = int64(providerSchema.IdentityTypeSchemaVersions[name]) + } + + resp.ResourceTypes[name] = ps + } + + for name, schema := range providerSchema.DataSources { + resp.DataSources[name] = providers.Schema{Body: schema} + } + + return resp +} diff --git a/internal/terraform/schemas.go b/internal/terraform/schemas.go deleted file mode 100644 index 24edeb85aa..0000000000 --- a/internal/terraform/schemas.go +++ /dev/null @@ -1,187 +0,0 @@ -package terraform - -import ( - "fmt" - "log" - - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/providers" - "github.com/hashicorp/terraform/internal/states" - "github.com/hashicorp/terraform/internal/tfdiags" -) - -// ProviderSchema is an alias for providers.Schemas, which is the new location -// for what we originally called terraform.ProviderSchema but which has -// moved out as part of ongoing refactoring to shrink down the main "terraform" -// package. -type ProviderSchema = providers.Schemas - -// Schemas is a container for various kinds of schema that Terraform needs -// during processing. -type Schemas struct { - Providers map[addrs.Provider]*providers.Schemas - Provisioners map[string]*configschema.Block -} - -// ProviderSchema returns the entire ProviderSchema object that was produced -// by the plugin for the given provider, or nil if no such schema is available. -// -// It's usually better to go use the more precise methods offered by type -// Schemas to handle this detail automatically. -func (ss *Schemas) ProviderSchema(provider addrs.Provider) *providers.Schemas { - if ss.Providers == nil { - return nil - } - return ss.Providers[provider] -} - -// ProviderConfig returns the schema for the provider configuration of the -// given provider type, or nil if no such schema is available. -func (ss *Schemas) ProviderConfig(provider addrs.Provider) *configschema.Block { - ps := ss.ProviderSchema(provider) - if ps == nil { - return nil - } - return ps.Provider -} - -// ResourceTypeConfig returns the schema for the configuration of a given -// resource type belonging to a given provider type, or nil of no such -// schema is available. -// -// In many cases the provider type is inferrable from the resource type name, -// but this is not always true because users can override the provider for -// a resource using the "provider" meta-argument. Therefore it's important to -// always pass the correct provider name, even though it many cases it feels -// redundant. -func (ss *Schemas) ResourceTypeConfig(provider addrs.Provider, resourceMode addrs.ResourceMode, resourceType string) (block *configschema.Block, schemaVersion uint64) { - ps := ss.ProviderSchema(provider) - if ps == nil || ps.ResourceTypes == nil { - return nil, 0 - } - return ps.SchemaForResourceType(resourceMode, resourceType) -} - -// ProvisionerConfig returns the schema for the configuration of a given -// provisioner, or nil of no such schema is available. -func (ss *Schemas) ProvisionerConfig(name string) *configschema.Block { - return ss.Provisioners[name] -} - -// loadSchemas searches the given configuration, state and plan (any of which -// may be nil) for constructs that have an associated schema, requests the -// necessary schemas from the given component factory (which must _not_ be nil), -// and returns a single object representing all of the necessary schemas. -// -// If an error is returned, it may be a wrapped tfdiags.Diagnostics describing -// errors across multiple separate objects. Errors here will usually indicate -// either misbehavior on the part of one of the providers or of the provider -// protocol itself. When returned with errors, the returned schemas object is -// still valid but may be incomplete. -func loadSchemas(config *configs.Config, state *states.State, plugins *contextPlugins) (*Schemas, error) { - schemas := &Schemas{ - Providers: map[addrs.Provider]*providers.Schemas{}, - Provisioners: map[string]*configschema.Block{}, - } - var diags tfdiags.Diagnostics - - newDiags := loadProviderSchemas(schemas.Providers, config, state, plugins) - diags = diags.Append(newDiags) - newDiags = loadProvisionerSchemas(schemas.Provisioners, config, plugins) - diags = diags.Append(newDiags) - - return schemas, diags.Err() -} - -func loadProviderSchemas(schemas map[addrs.Provider]*providers.Schemas, config *configs.Config, state *states.State, plugins *contextPlugins) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - ensure := func(fqn addrs.Provider) { - name := fqn.String() - - if _, exists := schemas[fqn]; exists { - return - } - - log.Printf("[TRACE] LoadSchemas: retrieving schema for provider type %q", name) - schema, err := plugins.ProviderSchema(fqn) - if err != nil { - // We'll put a stub in the map so we won't re-attempt this on - // future calls, which would then repeat the same error message - // multiple times. - schemas[fqn] = &providers.Schemas{} - diags = diags.Append( - tfdiags.Sourceless( - tfdiags.Error, - "Failed to obtain provider schema", - fmt.Sprintf("Could not load the schema for provider %s: %s.", fqn, err), - ), - ) - return - } - - schemas[fqn] = schema - } - - if config != nil { - for _, fqn := range config.ProviderTypes() { - ensure(fqn) - } - } - - if state != nil { - needed := providers.AddressedTypesAbs(state.ProviderAddrs()) - for _, typeAddr := range needed { - ensure(typeAddr) - } - } - - return diags -} - -func loadProvisionerSchemas(schemas map[string]*configschema.Block, config *configs.Config, plugins *contextPlugins) tfdiags.Diagnostics { - var diags tfdiags.Diagnostics - - ensure := func(name string) { - if _, exists := schemas[name]; exists { - return - } - - log.Printf("[TRACE] LoadSchemas: retrieving schema for provisioner %q", name) - schema, err := plugins.ProvisionerSchema(name) - if err != nil { - // We'll put a stub in the map so we won't re-attempt this on - // future calls, which would then repeat the same error message - // multiple times. - schemas[name] = &configschema.Block{} - diags = diags.Append( - tfdiags.Sourceless( - tfdiags.Error, - "Failed to obtain provisioner schema", - fmt.Sprintf("Could not load the schema for provisioner %q: %s.", name, err), - ), - ) - return - } - - schemas[name] = schema - } - - if config != nil { - for _, rc := range config.Module.ManagedResources { - for _, pc := range rc.Managed.Provisioners { - ensure(pc.Type) - } - } - - // Must also visit our child modules, recursively. - for _, cc := range config.Children { - childDiags := loadProvisionerSchemas(schemas, cc, plugins) - diags = diags.Append(childDiags) - } - } - - return diags -} diff --git a/internal/terraform/schemas_test.go b/internal/terraform/schemas_test.go index 044b795a50..71e447ce0f 100644 --- a/internal/terraform/schemas_test.go +++ b/internal/terraform/schemas_test.go @@ -1,18 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/providers/testing" + "github.com/hashicorp/terraform/internal/schemarepo" + "github.com/hashicorp/terraform/internal/schemarepo/loadschemas" ) -func simpleTestSchemas() *Schemas { +func simpleTestSchemas() *schemarepo.Schemas { provider := simpleMockProvider() provisioner := simpleMockProvisioner() return &Schemas{ - Providers: map[addrs.Provider]*ProviderSchema{ - addrs.NewDefaultProvider("test"): provider.ProviderSchema(), + Providers: map[addrs.Provider]providers.ProviderSchema{ + addrs.NewDefaultProvider("test"): provider.GetProviderSchema(), }, Provisioners: map[string]*configschema.Block{ "test": provisioner.GetSchemaResponse.Provisioner, @@ -28,32 +34,14 @@ func simpleTestSchemas() *Schemas { // The intended use for this is in testing components that use schemas to // drive other behavior, such as reference analysis during graph construction, // but that don't actually need to interact with providers otherwise. -func schemaOnlyProvidersForTesting(schemas map[addrs.Provider]*ProviderSchema) *contextPlugins { +func schemaOnlyProvidersForTesting(schemas map[addrs.Provider]providers.ProviderSchema) *loadschemas.Plugins { factories := make(map[addrs.Provider]providers.Factory, len(schemas)) for providerAddr, schema := range schemas { + schema := schema - resp := &providers.GetProviderSchemaResponse{ - Provider: providers.Schema{ - Block: schema.Provider, - }, - ResourceTypes: make(map[string]providers.Schema), - DataSources: make(map[string]providers.Schema), - } - for t, tSchema := range schema.ResourceTypes { - resp.ResourceTypes[t] = providers.Schema{ - Block: tSchema, - Version: int64(schema.ResourceTypeSchemaVersions[t]), - } - } - for t, tSchema := range schema.DataSources { - resp.DataSources[t] = providers.Schema{ - Block: tSchema, - } - } - - provider := &MockProvider{ - GetProviderSchemaResponse: resp, + provider := &testing.MockProvider{ + GetProviderSchemaResponse: &schema, } factories[providerAddr] = func() (providers.Interface, error) { @@ -61,5 +49,5 @@ func schemaOnlyProvidersForTesting(schemas map[addrs.Provider]*ProviderSchema) * } } - return newContextPlugins(factories, nil) + return newContextPlugins(factories, nil, nil) } diff --git a/internal/legacy/terraform/util.go b/internal/terraform/semaphore.go similarity index 61% rename from internal/legacy/terraform/util.go rename to internal/terraform/semaphore.go index 7966b58dd2..681f1dd300 100644 --- a/internal/legacy/terraform/util.go +++ b/internal/terraform/semaphore.go @@ -1,8 +1,7 @@ -package terraform +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 -import ( - "sort" -) +package terraform // Semaphore is a wrapper around a channel to provide // utility methods to clarify that we are treating the @@ -45,31 +44,3 @@ func (s Semaphore) Release() { panic("release without an acquire") } } - -// strSliceContains checks if a given string is contained in a slice -// When anybody asks why Go needs generics, here you go. -func strSliceContains(haystack []string, needle string) bool { - for _, s := range haystack { - if s == needle { - return true - } - } - return false -} - -// deduplicate a slice of strings -func uniqueStrings(s []string) []string { - if len(s) < 2 { - return s - } - - sort.Strings(s) - result := make([]string, 1, len(s)) - result[0] = s[0] - for i := 1; i < len(s); i++ { - if s[i] != result[len(result)-1] { - result = append(result, s[i]) - } - } - return result -} diff --git a/internal/terraform/semaphore_test.go b/internal/terraform/semaphore_test.go new file mode 100644 index 0000000000..a094ba3026 --- /dev/null +++ b/internal/terraform/semaphore_test.go @@ -0,0 +1,36 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "testing" + "time" +) + +func TestSemaphore(t *testing.T) { + s := NewSemaphore(2) + timer := time.AfterFunc(time.Second, func() { + panic("deadlock") + }) + defer timer.Stop() + + s.Acquire() + if !s.TryAcquire() { + t.Fatalf("should acquire") + } + if s.TryAcquire() { + t.Fatalf("should not acquire") + } + s.Release() + s.Release() + + // This release should panic + defer func() { + r := recover() + if r == nil { + t.Fatalf("should panic") + } + }() + s.Release() +} diff --git a/internal/terraform/terraform_test.go b/internal/terraform/terraform_test.go index 4192459583..9ca5ef48e5 100644 --- a/internal/terraform/terraform_test.go +++ b/internal/terraform/terraform_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -19,6 +22,7 @@ import ( "github.com/hashicorp/terraform/internal/initwd" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" + testing_provider "github.com/hashicorp/terraform/internal/providers/testing" "github.com/hashicorp/terraform/internal/provisioners" "github.com/hashicorp/terraform/internal/registry" "github.com/hashicorp/terraform/internal/states" @@ -63,8 +67,8 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths // in a JSON file so that we can load them below. - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) - _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), dir, "tests", true, false, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } @@ -85,10 +89,13 @@ func testModuleWithSnapshot(t *testing.T, name string) (*configs.Config, *config // testModuleInline takes a map of path -> config strings and yields a config // structure with those files loaded from disk -func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { +func testModuleInline(t testing.TB, sources map[string]string) *configs.Config { t.Helper() - cfgPath := t.TempDir() + cfgPath, err := filepath.EvalSymlinks(t.TempDir()) + if err != nil { + t.Fatal(err) + } for path, configStr := range sources { dir := filepath.Dir(path) @@ -120,8 +127,8 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { // Test modules usually do not refer to remote sources, and for local // sources only this ultimately just records all of the module paths // in a JSON file so that we can load them below. - inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) - _, instDiags := inst.InstallModules(context.Background(), cfgPath, true, initwd.ModuleInstallHooksImpl{}) + inst := initwd.NewModuleInstaller(loader.ModulesDir(), loader, registry.NewClient(nil, nil)) + _, instDiags := inst.InstallModules(context.Background(), cfgPath, "tests", true, false, initwd.ModuleInstallHooksImpl{}) if instDiags.HasErrors() { t.Fatal(instDiags.Err()) } @@ -132,7 +139,7 @@ func testModuleInline(t *testing.T, sources map[string]string) *configs.Config { t.Fatalf("failed to refresh modules after installation: %s", err) } - config, diags := loader.LoadConfig(cfgPath) + config, diags := loader.LoadConfigWithTests(cfgPath, "tests") if diags.HasErrors() { t.Fatal(diags.Error()) } @@ -167,32 +174,33 @@ func testSetResourceInstanceTainted(module *states.Module, resource, attrsJson, } func testProviderFuncFixed(rp providers.Interface) providers.Factory { - return func() (providers.Interface, error) { - if p, ok := rp.(*MockProvider); ok { - // make sure none of the methods were "called" on this new instance - p.GetProviderSchemaCalled = false - p.ValidateProviderConfigCalled = false - p.ValidateResourceConfigCalled = false - p.ValidateDataResourceConfigCalled = false - p.UpgradeResourceStateCalled = false - p.ConfigureProviderCalled = false - p.StopCalled = false - p.ReadResourceCalled = false - p.PlanResourceChangeCalled = false - p.ApplyResourceChangeCalled = false - p.ImportResourceStateCalled = false - p.ReadDataSourceCalled = false - p.CloseCalled = false - } + if p, ok := rp.(*testing_provider.MockProvider); ok { + // make sure none of the methods were "called" on this new instance + p.GetProviderSchemaCalled = false + p.ValidateProviderConfigCalled = false + p.ValidateResourceConfigCalled = false + p.ValidateDataResourceConfigCalled = false + p.UpgradeResourceStateCalled = false + p.ConfigureProviderCalled = false + p.StopCalled = false + p.ReadResourceCalled = false + p.PlanResourceChangeCalled = false + p.ApplyResourceChangeCalled = false + p.ImportResourceStateCalled = false + p.ReadDataSourceCalled = false + p.CloseCalled = false + } + return func() (providers.Interface, error) { return rp, nil } } func testProvisionerFuncFixed(rp *MockProvisioner) provisioners.Factory { + // make sure this provisioner has has not been closed + rp.CloseCalled = false + return func() (provisioners.Interface, error) { - // make sure this provisioner has has not been closed - rp.CloseCalled = false return rp, nil } } @@ -221,6 +229,14 @@ func mustAbsResourceAddr(s string) addrs.AbsResource { return addr } +func mustAbsOutputValue(s string) addrs.AbsOutputValue { + p, diags := addrs.ParseAbsOutputValueStr(s) + if diags.HasErrors() { + panic(diags.Err()) + } + return p +} + func mustProviderConfig(s string) addrs.AbsProviderConfig { p, diags := addrs.ParseAbsProviderConfigStr(s) if diags.HasErrors() { @@ -229,6 +245,22 @@ func mustProviderConfig(s string) addrs.AbsProviderConfig { return p } +func mustReference(s string) *addrs.Reference { + p, diags := addrs.ParseRefStr(s) + if diags.HasErrors() { + panic(diags.Err()) + } + return p +} + +func mustModuleInstance(s string) addrs.ModuleInstance { + p, diags := addrs.ParseModuleInstanceStr(s) + if diags.HasErrors() { + panic(diags.Err()) + } + return p +} + // HookRecordApplyOrder is a test hook that records the order of applies // by recording the PreApply event. type HookRecordApplyOrder struct { @@ -243,7 +275,7 @@ type HookRecordApplyOrder struct { l sync.Mutex } -func (h *HookRecordApplyOrder) PreApply(addr addrs.AbsResourceInstance, gen states.Generation, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { +func (h *HookRecordApplyOrder) PreApply(id HookResourceIdentity, dk addrs.DeposedKey, action plans.Action, priorState, plannedNewState cty.Value) (HookAction, error) { if plannedNewState.RawEquals(priorState) { return HookActionContinue, nil } @@ -252,7 +284,7 @@ func (h *HookRecordApplyOrder) PreApply(addr addrs.AbsResourceInstance, gen stat h.l.Lock() defer h.l.Unlock() - h.IDs = append(h.IDs, addr.String()) + h.IDs = append(h.IDs, id.Addr.String()) h.Diffs = append(h.Diffs, &plans.Change{ Action: action, Before: priorState, @@ -628,13 +660,6 @@ module.child: aws_instance.foo ` -const testTerraformApplyOutputOrphanStr = ` - -Outputs: - -foo = bar -` - const testTerraformApplyOutputOrphanModuleStr = ` ` diff --git a/internal/terraform/testdata/apply-destroy-provisider-refs/main.tf b/internal/terraform/testdata/apply-destroy-provisider-refs/main.tf new file mode 100644 index 0000000000..e7ef6f0b5d --- /dev/null +++ b/internal/terraform/testdata/apply-destroy-provisider-refs/main.tf @@ -0,0 +1,15 @@ +provider "null" { + value = "" +} + +module "mod" { + source = "./mod" +} + +provider "test" { + value = module.mod.output +} + +resource "test_instance" "bar" { +} + diff --git a/internal/terraform/testdata/apply-destroy-provisider-refs/mod/main.tf b/internal/terraform/testdata/apply-destroy-provisider-refs/mod/main.tf new file mode 100644 index 0000000000..c9ecec002b --- /dev/null +++ b/internal/terraform/testdata/apply-destroy-provisider-refs/mod/main.tf @@ -0,0 +1,9 @@ +data "null_data_source" "foo" { + count = 1 +} + + +output "output" { + value = data.null_data_source.foo[0].output +} + diff --git a/internal/terraform/testdata/apply-provisioner-destroy-removed/main.tf b/internal/terraform/testdata/apply-provisioner-destroy-removed/main.tf new file mode 100644 index 0000000000..c3a793c39b --- /dev/null +++ b/internal/terraform/testdata/apply-provisioner-destroy-removed/main.tf @@ -0,0 +1,12 @@ +removed { + from = module.foo.aws_instance.foo + + provisioner "shell" { + when = "destroy" + + // Capture that we can reference either count.index or each.key from a + // removed block, and it's up to the user to ensure the provisioner is + // correct for the now removed resources. + command = "destroy ${try(count.index, each.key)} ${self.foo}" + } +} diff --git a/internal/terraform/testdata/apply-provisioner-ephemeral/main.tf b/internal/terraform/testdata/apply-provisioner-ephemeral/main.tf new file mode 100644 index 0000000000..815fbd30cf --- /dev/null +++ b/internal/terraform/testdata/apply-provisioner-ephemeral/main.tf @@ -0,0 +1,18 @@ +variable "password" { + type = string + ephemeral = true +} + +resource "aws_instance" "foo" { + connection { + host = "localhost" + type = "telnet" + user = "superuser" + port = 2222 + password = "password" + } + + provisioner "shell" { + command = "echo ${var.password} > secrets" + } +} diff --git a/internal/terraform/testdata/apply-provisioner-sensitive-ephemeral/main.tf b/internal/terraform/testdata/apply-provisioner-sensitive-ephemeral/main.tf new file mode 100644 index 0000000000..d096fe6b80 --- /dev/null +++ b/internal/terraform/testdata/apply-provisioner-sensitive-ephemeral/main.tf @@ -0,0 +1,19 @@ +variable "password" { + type = string + ephemeral = true + sensitive = true +} + +resource "aws_instance" "foo" { + connection { + host = "localhost" + type = "telnet" + user = "superuser" + port = 2222 + password = "password" + } + + provisioner "shell" { + command = "echo ${var.password} > secrets" + } +} diff --git a/internal/terraform/testdata/apply-stop/apply-stop.tf b/internal/terraform/testdata/apply-stop/apply-stop.tf new file mode 100644 index 0000000000..003ca67b3b --- /dev/null +++ b/internal/terraform/testdata/apply-stop/apply-stop.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + indefinite = { + source = "terraform.io/test/indefinite" + } + } +} + +# The TestContext2Apply_stop test arranges for "indefinite"'s +# ApplyResourceChange to just block indefinitely until the operation +# is cancelled using Context.Stop. +resource "indefinite" "foo" { +} + +resource "indefinite" "bar" { + # Should never get here during apply because we're going to interrupt the + # run during indefinite.foo's ApplyResourceChange. + depends_on = [indefinite.foo] +} + +output "result" { + value = indefinite.foo.result +} diff --git a/internal/terraform/testdata/apply-with-checks/main.tf b/internal/terraform/testdata/apply-with-checks/main.tf new file mode 100644 index 0000000000..0064a4b4ae --- /dev/null +++ b/internal/terraform/testdata/apply-with-checks/main.tf @@ -0,0 +1,20 @@ + +resource "aws_instance" "foo" { + test_string = "Hello, world!" +} + +resource "aws_instance" "baz" { + test_string = aws_instance.foo.test_string +} + +check "my_check" { + data "aws_data_source" "bar" { + id = "UI098L" + } + + assert { + condition = data.aws_data_source.bar.foo == "valid value" + error_message = "invalid value" + } + +} diff --git a/internal/terraform/testdata/context-required-version-module/child/main.tf b/internal/terraform/testdata/context-required-version-module/child/main.tf deleted file mode 100644 index 3b52ffab91..0000000000 --- a/internal/terraform/testdata/context-required-version-module/child/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -terraform { - required_version = ">= 0.5.0" -} diff --git a/internal/terraform/testdata/context-required-version-module/main.tf b/internal/terraform/testdata/context-required-version-module/main.tf deleted file mode 100644 index 0f6991c536..0000000000 --- a/internal/terraform/testdata/context-required-version-module/main.tf +++ /dev/null @@ -1,3 +0,0 @@ -module "child" { - source = "./child" -} diff --git a/internal/terraform/testdata/data-source-read-with-plan-error/main.tf b/internal/terraform/testdata/data-source-read-with-plan-error/main.tf new file mode 100644 index 0000000000..2559406f7a --- /dev/null +++ b/internal/terraform/testdata/data-source-read-with-plan-error/main.tf @@ -0,0 +1,12 @@ +resource "aws_instance" "foo" { +} + +// this will be postponed until apply +data "aws_data_source" "foo" { + foo = aws_instance.foo.id +} + +// this will cause an error in the final plan +resource "test_instance" "bar" { + foo = "error" +} diff --git a/internal/terraform/testdata/import-id-data-source/main.tf b/internal/terraform/testdata/import-id-data-source/main.tf new file mode 100644 index 0000000000..ad524fe5fa --- /dev/null +++ b/internal/terraform/testdata/import-id-data-source/main.tf @@ -0,0 +1,11 @@ +data "aws_subnet" "bar" { + vpc_id = "abc" + cidr_block = "10.0.1.0/24" +} + +import { + to = aws_subnet.bar + id = data.aws_subnet.bar.id +} + +resource "aws_subnet" "bar" {} diff --git a/internal/terraform/testdata/import-id-func/main.tf b/internal/terraform/testdata/import-id-func/main.tf new file mode 100644 index 0000000000..f861fae069 --- /dev/null +++ b/internal/terraform/testdata/import-id-func/main.tf @@ -0,0 +1,8 @@ +import { + to = aws_instance.foo + id = substr("hmm123", "2", "3") +} + +resource "aws_instance" "foo" { + +} diff --git a/internal/terraform/testdata/import-id-invalid-null/main.tf b/internal/terraform/testdata/import-id-invalid-null/main.tf new file mode 100644 index 0000000000..5de128d1ec --- /dev/null +++ b/internal/terraform/testdata/import-id-invalid-null/main.tf @@ -0,0 +1,10 @@ +variable "the_id" { + type = string +} + +import { + to = test_resource.foo + id = var.the_id +} + +resource "test_resource" "foo" {} diff --git a/internal/terraform/testdata/import-id-invalid-unknown/main.tf b/internal/terraform/testdata/import-id-invalid-unknown/main.tf new file mode 100644 index 0000000000..317e99f006 --- /dev/null +++ b/internal/terraform/testdata/import-id-invalid-unknown/main.tf @@ -0,0 +1,12 @@ +resource "test_resource" "foo" { + +} + +import { + to = test_resource.bar + id = test_resource.foo.id +} + +resource "test_resource" "bar" { + +} diff --git a/internal/terraform/testdata/import-id-module/child/main.tf b/internal/terraform/testdata/import-id-module/child/main.tf new file mode 100644 index 0000000000..251f582ae5 --- /dev/null +++ b/internal/terraform/testdata/import-id-module/child/main.tf @@ -0,0 +1,3 @@ +output "lb_id" { + value = 1 +} diff --git a/internal/terraform/testdata/import-id-module/main.tf b/internal/terraform/testdata/import-id-module/main.tf new file mode 100644 index 0000000000..ef75c78ec1 --- /dev/null +++ b/internal/terraform/testdata/import-id-module/main.tf @@ -0,0 +1,10 @@ +module "child" { + source = "./child" +} + +import { + to = aws_lb.foo + id = module.child.lb_id +} + +resource "aws_lb" "foo" {} diff --git a/internal/terraform/testdata/import-id-variable/main.tf b/internal/terraform/testdata/import-id-variable/main.tf new file mode 100644 index 0000000000..db1d33aed1 --- /dev/null +++ b/internal/terraform/testdata/import-id-variable/main.tf @@ -0,0 +1,21 @@ +variable "the_id" { + default = "123" +} + +import { + to = aws_instance.foo + id = var.the_id +} + +resource "aws_instance" "foo" { +} + +module "test" { + source = "./mod" +} + +import { + to = module.test.aws_instance.foo + id = var.the_id +} + diff --git a/internal/terraform/testdata/import-id-variable/mod/main.tf b/internal/terraform/testdata/import-id-variable/mod/main.tf new file mode 100644 index 0000000000..b626e60c82 --- /dev/null +++ b/internal/terraform/testdata/import-id-variable/mod/main.tf @@ -0,0 +1,2 @@ +resource "aws_instance" "foo" { +} diff --git a/internal/terraform/testdata/import-identity-module/child/main.tf b/internal/terraform/testdata/import-identity-module/child/main.tf new file mode 100644 index 0000000000..251f582ae5 --- /dev/null +++ b/internal/terraform/testdata/import-identity-module/child/main.tf @@ -0,0 +1,3 @@ +output "lb_id" { + value = 1 +} diff --git a/internal/terraform/testdata/import-identity-module/main.tf b/internal/terraform/testdata/import-identity-module/main.tf new file mode 100644 index 0000000000..e85efe5e64 --- /dev/null +++ b/internal/terraform/testdata/import-identity-module/main.tf @@ -0,0 +1,12 @@ +module "child" { + source = "./child" +} + +import { + to = aws_lb.foo + identity = { + name = "bar" + } +} + +resource "aws_lb" "foo" {} diff --git a/internal/terraform/testdata/import-provider-resources/main.tf b/internal/terraform/testdata/import-provider-resources/main.tf index cb92470fab..a99ee5e941 100644 --- a/internal/terraform/testdata/import-provider-resources/main.tf +++ b/internal/terraform/testdata/import-provider-resources/main.tf @@ -1,5 +1,5 @@ provider "aws" { - value = "${test_instance.bar.value}" + value = "${test_instance.bar.id}" } resource "aws_instance" "foo" { diff --git a/internal/terraform/testdata/issue-33572/main.tf b/internal/terraform/testdata/issue-33572/main.tf new file mode 100644 index 0000000000..2718b62996 --- /dev/null +++ b/internal/terraform/testdata/issue-33572/main.tf @@ -0,0 +1,14 @@ +provider "aws" {} + +resource "aws_instance" "foo" {} + +check "aws_instance_exists" { + data "aws_data_source" "bar" { + id = "baz" + } + + assert { + condition = data.aws_data_source.bar.foo == "Hello, world!" + error_message = "incorrect value" + } +} diff --git a/internal/terraform/testdata/plan-ignore-changes-wildcard/main.tf b/internal/terraform/testdata/plan-ignore-changes-wildcard/main.tf index d4e55a8858..ac594a9eb8 100644 --- a/internal/terraform/testdata/plan-ignore-changes-wildcard/main.tf +++ b/internal/terraform/testdata/plan-ignore-changes-wildcard/main.tf @@ -5,6 +5,7 @@ variable "bar" {} resource "aws_instance" "foo" { ami = "${var.foo}" instance = "${var.bar}" + foo = "bar" lifecycle { ignore_changes = all diff --git a/internal/terraform/testdata/planandeval-basic/planandeval-basic.tf b/internal/terraform/testdata/planandeval-basic/planandeval-basic.tf new file mode 100644 index 0000000000..4211fae086 --- /dev/null +++ b/internal/terraform/testdata/planandeval-basic/planandeval-basic.tf @@ -0,0 +1,8 @@ + +variable "a" { + type = string +} + +resource "test_thing" "a" { + arg = var.a +} diff --git a/internal/terraform/testdata/provider-function-echo/main.tf b/internal/terraform/testdata/provider-function-echo/main.tf new file mode 100644 index 0000000000..02ee302772 --- /dev/null +++ b/internal/terraform/testdata/provider-function-echo/main.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +resource "test_object" "test" { + test_string = provider::test::echo("input") +} diff --git a/internal/terraform/testdata/resource-fs-func/main.tf b/internal/terraform/testdata/resource-fs-func/main.tf new file mode 100644 index 0000000000..ad30746695 --- /dev/null +++ b/internal/terraform/testdata/resource-fs-func/main.tf @@ -0,0 +1,6 @@ +variable "external_file" { +} + +resource "test_resource" "test" { + value = filebase64(var.external_file) +} diff --git a/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf b/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf index 3667a4e11f..54b2343d37 100644 --- a/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf +++ b/internal/terraform/testdata/static-validate-refs/static-validate-refs.tf @@ -21,3 +21,16 @@ resource "boop_whatever" "nope" { data "beep" "boop" { } + +ephemeral "beep" "boop" { + provider = boop +} + +check "foo" { + data "boop_data" "boop_nested" {} + + assert { + condition = data.boop_data.boop_nested.id == null + error_message = "check failed" + } +} diff --git a/internal/terraform/transform.go b/internal/terraform/transform.go index 2cc812ffe4..dc070218e6 100644 --- a/internal/terraform/transform.go +++ b/internal/terraform/transform.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_attach_config_provider.go b/internal/terraform/transform_attach_config_provider.go index 95153eaced..eb8245b046 100644 --- a/internal/terraform/transform_attach_config_provider.go +++ b/internal/terraform/transform_attach_config_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_attach_config_provider_meta.go b/internal/terraform/transform_attach_config_provider_meta.go index d79df26fa0..bdeaa96346 100644 --- a/internal/terraform/transform_attach_config_provider_meta.go +++ b/internal/terraform/transform_attach_config_provider_meta.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_attach_config_resource.go b/internal/terraform/transform_attach_config_resource.go index e2468a0099..2436e3a044 100644 --- a/internal/terraform/transform_attach_config_resource.go +++ b/internal/terraform/transform_attach_config_resource.go @@ -1,8 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "log" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" ) @@ -12,8 +16,11 @@ import ( type GraphNodeAttachResourceConfig interface { GraphNodeConfigResource - // Sets the configuration - AttachResourceConfig(*configs.Resource) + // Sets the configuration, either to a present resource block or to + // a "removed" block commemorating a resource that has since been + // removed. Callers should always leave at least one of these + // arguments set to nil. + AttachResourceConfig(*configs.Resource, *configs.Removed) } // AttachResourceConfigTransformer goes through the graph and attaches @@ -27,6 +34,22 @@ type AttachResourceConfigTransformer struct { } func (t *AttachResourceConfigTransformer) Transform(g *Graph) error { + // Collect removed blocks to attach to any resources. These are collected + // independently because removed blocks may live in a parent module of the + // resource referenced. + removed := addrs.MakeMap[addrs.ConfigResource, *configs.Removed]() + + t.Config.DeepEach(func(c *configs.Config) { + for _, rem := range c.Module.Removed { + resAddr, ok := rem.From.RelSubject.(addrs.ConfigResource) + if !ok { + // Not for a resource at all, so can't possibly match. + // Non-resource removed targets have nothing to attach. + continue + } + removed.Put(resAddr, rem) + } + }) // Go through and find GraphNodeAttachResource for _, v := range g.Vertices() { @@ -39,69 +62,30 @@ func (t *AttachResourceConfigTransformer) Transform(g *Graph) error { // Determine what we're looking for addr := arn.ResourceAddr() + // Check for a removed block first, since that would preclude any resource config. + if remCfg, ok := removed.GetOk(addr); ok { + log.Printf("[TRACE] AttachResourceConfigTransformer: attaching to %q (%T) removed block from %#v", dag.VertexName(v), v, remCfg.DeclRange) + arn.AttachResourceConfig(nil, remCfg) + } + // Get the configuration. - config := t.Config.Descendent(addr.Module) + config := t.Config.Descendant(addr.Module) + if config == nil { log.Printf("[TRACE] AttachResourceConfigTransformer: %q (%T) has no configuration available", dag.VertexName(v), v) continue } - for _, r := range config.Module.ManagedResources { - rAddr := r.Addr() - - if rAddr != addr.Resource { - // Not the same resource - continue - } - - log.Printf("[TRACE] AttachResourceConfigTransformer: attaching to %q (%T) config from %s", dag.VertexName(v), v, r.DeclRange) - arn.AttachResourceConfig(r) - - // attach the provider_meta info - if gnapmc, ok := v.(GraphNodeAttachProviderMetaConfigs); ok { - log.Printf("[TRACE] AttachResourceConfigTransformer: attaching provider meta configs to %s", dag.VertexName(v)) - if config == nil { - log.Printf("[TRACE] AttachResourceConfigTransformer: no config set on the transformer for %s", dag.VertexName(v)) - continue - } - if config.Module == nil { - log.Printf("[TRACE] AttachResourceConfigTransformer: no module in config for %s", dag.VertexName(v)) - continue - } - if config.Module.ProviderMetas == nil { - log.Printf("[TRACE] AttachResourceConfigTransformer: no provider metas defined for %s", dag.VertexName(v)) - continue - } - gnapmc.AttachProviderMetaConfigs(config.Module.ProviderMetas) - } - } - for _, r := range config.Module.DataResources { - rAddr := r.Addr() - - if rAddr != addr.Resource { - // Not the same resource - continue - } - + if r := config.Module.ResourceByAddr(addr.Resource); r != nil { log.Printf("[TRACE] AttachResourceConfigTransformer: attaching to %q (%T) config from %#v", dag.VertexName(v), v, r.DeclRange) - arn.AttachResourceConfig(r) - - // attach the provider_meta info + arn.AttachResourceConfig(r, nil) if gnapmc, ok := v.(GraphNodeAttachProviderMetaConfigs); ok { log.Printf("[TRACE] AttachResourceConfigTransformer: attaching provider meta configs to %s", dag.VertexName(v)) - if config == nil { - log.Printf("[TRACE] AttachResourceConfigTransformer: no config set on the transformer for %s", dag.VertexName(v)) - continue + if config.Module.ProviderMetas != nil { + gnapmc.AttachProviderMetaConfigs(config.Module.ProviderMetas) + } else { + log.Printf("[TRACE] AttachResourceConfigTransformer: no provider meta configs available to attach to %s", dag.VertexName(v)) } - if config.Module == nil { - log.Printf("[TRACE] AttachResourceConfigTransformer: no module in config for %s", dag.VertexName(v)) - continue - } - if config.Module.ProviderMetas == nil { - log.Printf("[TRACE] AttachResourceConfigTransformer: no provider metas defined for %s", dag.VertexName(v)) - continue - } - gnapmc.AttachProviderMetaConfigs(config.Module.ProviderMetas) } } } diff --git a/internal/terraform/transform_attach_schema.go b/internal/terraform/transform_attach_schema.go index 8f7a590833..3a56b036cc 100644 --- a/internal/terraform/transform_attach_schema.go +++ b/internal/terraform/transform_attach_schema.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -7,6 +10,7 @@ import ( "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/providers" ) // GraphNodeAttachResourceSchema is an interface implemented by node types @@ -15,7 +19,7 @@ type GraphNodeAttachResourceSchema interface { GraphNodeConfigResource GraphNodeProviderConsumer - AttachResourceSchema(schema *configschema.Block, version uint64) + AttachResourceSchema(schema *providers.Schema) } // GraphNodeAttachProviderConfigSchema is an interface implemented by node types @@ -62,16 +66,16 @@ func (t *AttachSchemaTransformer) Transform(g *Graph) error { typeName := addr.Resource.Type providerFqn := tv.Provider() - schema, version, err := t.Plugins.ResourceTypeSchema(providerFqn, mode, typeName) + schema, err := t.Plugins.ResourceTypeSchema(providerFqn, mode, typeName) if err != nil { return fmt.Errorf("failed to read schema for %s in %s: %s", addr, providerFqn, err) } - if schema == nil { + if schema.Body == nil { log.Printf("[ERROR] AttachSchemaTransformer: No resource schema available for %s", addr) continue } log.Printf("[TRACE] AttachSchemaTransformer: attaching resource schema to %s", dag.VertexName(v)) - tv.AttachResourceSchema(schema, version) + tv.AttachResourceSchema(&schema) } if tv, ok := v.(GraphNodeAttachProviderConfigSchema); ok { diff --git a/internal/terraform/transform_attach_state.go b/internal/terraform/transform_attach_state.go index 13694718c2..762da2d873 100644 --- a/internal/terraform/transform_attach_state.go +++ b/internal/terraform/transform_attach_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_check.go b/internal/terraform/transform_check.go new file mode 100644 index 0000000000..b72e1e4882 --- /dev/null +++ b/internal/terraform/transform_check.go @@ -0,0 +1,145 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" +) + +type checkTransformer struct { + // Config for the entire module. + Config *configs.Config + + // Operation is the current operation this node will be part of. + Operation walkOperation +} + +var _ GraphTransformer = (*checkTransformer)(nil) + +func (t *checkTransformer) Transform(graph *Graph) error { + return t.transform(graph, t.Config, graph.Vertices()) +} + +func (t *checkTransformer) transform(g *Graph, cfg *configs.Config, allNodes []dag.Vertex) error { + + if t.Operation == walkDestroy || t.Operation == walkPlanDestroy { + // Don't include anything about checks during destroy operations. + // + // For other plan and normal apply operations we do everything, for + // destroy operations we do nothing. For any other operations we still + // include the check nodes, but we don't actually execute the checks + // instead we still validate their references and make sure their + // conditions make sense etc. + return nil + } + + moduleAddr := cfg.Path + + for _, check := range cfg.Module.Checks { + configAddr := check.Addr().InModule(moduleAddr) + + // We want to create a node for each check block. This node will execute + // after anything it references, and will update the checks object + // embedded in the plan and/or state. + + log.Printf("[TRACE] checkTransformer: Nodes and edges for %s", configAddr) + expand := &nodeExpandCheck{ + addr: configAddr, + config: check, + makeInstance: func(addr addrs.AbsCheck, cfg *configs.Check) dag.Vertex { + return &nodeCheckAssert{ + addr: addr, + config: cfg, + executeChecks: t.ExecuteChecks(), + } + }, + } + g.Add(expand) + + // We also need to report the checks we are going to execute before we + // try and execute them. + if t.ReportChecks() { + report := &nodeReportCheck{ + addr: configAddr, + } + g.Add(report) + + // Make sure we report our checks before we start executing the + // actual checks. + g.Connect(dag.BasicEdge(expand, report)) + + if check.DataResource != nil { + // If we have a nested data source, we need to make sure we + // also report the check before the data source executes. + // + // We loop through all the nodes in the graph to find the one + // that contains our data source and connect it. + for _, other := range allNodes { + if resource, isResource := other.(GraphNodeConfigResource); isResource { + resourceAddr := resource.ResourceAddr() + if !resourceAddr.Module.Equal(moduleAddr) { + // This resource isn't in the same module as our check + // so skip it. + continue + } + + resourceCfg := cfg.Module.ResourceByAddr(resourceAddr.Resource) + if resourceCfg != nil && resourceCfg.Container != nil && resourceCfg.Container.Accessible(check.Addr()) { + // Make sure we report our checks before we execute any + // embedded data resource. + g.Connect(dag.BasicEdge(other, report)) + + // There's at most one embedded data source, and + // we've found it so stop looking. + break + } + } + } + } + } + } + + for _, child := range cfg.Children { + if err := t.transform(g, child, allNodes); err != nil { + return err + } + } + + return nil +} + +// ReportChecks returns true if this operation should report any check blocks +// that it is about to execute. +// +// This is true for planning operations, as apply operations recreate the +// expected checks from the plan. +// +// We'll also report the checks during an import operation. We still execute +// our check blocks during an import operation so they need to be reported +// first. +func (t *checkTransformer) ReportChecks() bool { + return t.Operation == walkPlan || t.Operation == walkImport +} + +// ExecuteChecks returns true if this operation should actually execute any +// check blocks in the config. +// +// If this returns false we will still create and execute check nodes in the +// graph, but they will only validate things like references and syntax. +func (t *checkTransformer) ExecuteChecks() bool { + switch t.Operation { + case walkPlan, walkApply, walkImport: + // We only actually execute the checks for plan and apply operations. + return true + default: + // For everything else, we still want to validate the checks make sense + // logically and syntactically, but we won't actually resolve the check + // conditions. + return false + } +} diff --git a/internal/terraform/transform_check_starter.go b/internal/terraform/transform_check_starter.go new file mode 100644 index 0000000000..44b2b0b7b7 --- /dev/null +++ b/internal/terraform/transform_check_starter.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" +) + +var _ GraphTransformer = (*checkStartTransformer)(nil) + +// checkStartTransformer checks if the configuration has any data blocks nested +// within check blocks, and if it does then it introduces a nodeCheckStart +// vertex that ensures all resources have been applied before it starts loading +// the nested data sources. +type checkStartTransformer struct { + // Config for the entire module. + Config *configs.Config + + // Operation is the current operation this node will be part of. + Operation walkOperation +} + +func (s *checkStartTransformer) Transform(graph *Graph) error { + if s.Operation != walkApply && s.Operation != walkPlan { + // We only actually execute the checks during plan apply operations + // so if we are doing something else we can just skip this and + // leave the graph alone. + return nil + } + + var resources []dag.Vertex + var nested []dag.Vertex + + // We're going to step through all the vertices and pull out the relevant + // resources and data sources. + for _, vertex := range graph.Vertices() { + if node, isResource := vertex.(GraphNodeCreator); isResource { + addr := node.CreateAddr() + + if addr.Resource.Resource.Mode == addrs.ManagedResourceMode { + // This is a resource, so we want to make sure it executes + // before any nested data sources. + + // We can reduce the number of additional edges we write into + // the graph by only including "leaf" resources, that is + // resources that aren't referenced by other resources. If a + // resource is referenced by another resource then we know that + // it will execute before that resource so we only need to worry + // about the referencing resource. + + leafResource := true + for _, other := range graph.UpEdges(vertex) { + if otherResource, isResource := other.(GraphNodeCreator); isResource { + otherAddr := otherResource.CreateAddr() + if otherAddr.Resource.Resource.Mode == addrs.ManagedResourceMode { + // Then this resource is being referenced so skip + // it. + leafResource = false + break + } + } + } + + if leafResource { + resources = append(resources, vertex) + } + + // We've handled the resource so move to the next vertex. + continue + } + + // Now, we know we are processing a data block. + + config := s.Config + if !addr.Module.IsRoot() { + config = s.Config.Descendant(addr.Module.Module()) + } + if config == nil { + // might have been deleted, so it won't be subject to any checks + // anyway. + continue + } + + resource := config.Module.ResourceByAddr(addr.Resource.Resource) + if resource == nil { + // might have been deleted, so it won't be subject to any checks + // anyway. + continue + } + + if _, ok := resource.Container.(*configs.Check); ok { + // Then this is a data source within a check block, so let's + // make a note of it. + nested = append(nested, vertex) + } + + // Otherwise, it's just a normal data source. From a check block we + // don't really care when Terraform is loading non-nested data + // sources so we'll just forget about it and move on. + } + } + + if len(nested) > 0 { + + // We don't need to do any of this if we don't have any nested data + // sources, so we check that first. + // + // Otherwise we introduce a vertex that can act as a pauser between + // our nested data sources and leaf resources. + + check := &nodeCheckStart{} + graph.Add(check) + + // Finally, connect everything up so it all executes in order. + + for _, vertex := range nested { + graph.Connect(dag.BasicEdge(vertex, check)) + } + + for _, vertex := range resources { + graph.Connect(dag.BasicEdge(check, vertex)) + } + } + + return nil +} diff --git a/internal/terraform/transform_config.go b/internal/terraform/transform_config.go index 59fa1eeea9..aa2ee0b74b 100644 --- a/internal/terraform/transform_config.go +++ b/internal/terraform/transform_config.go @@ -1,11 +1,17 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "fmt" "log" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/tfdiags" ) // ConfigTransformer is a GraphTransformer that adds all the resources @@ -29,23 +35,33 @@ type ConfigTransformer struct { ModeFilter bool Mode addrs.ResourceMode - // Do not apply this transformer. - skip bool + // some actions are skipped during the destroy process + destroy bool - // configuration resources that are to be imported + // importTargets specifies a slice of addresses that will have state + // imported for them. importTargets []*ImportTarget + + // generateConfigPathForImportTargets tells the graph where to write any + // generated config for import targets that are not contained within config. + // + // If this is empty and an import target has no config, the graph will + // simply import the state for the target and any follow-up operations will + // try to delete the imported resource unless the config is updated + // manually. + generateConfigPathForImportTargets string } func (t *ConfigTransformer) Transform(g *Graph) error { - if t.skip { - return nil - } - // If no configuration is available, we don't do anything if t.Config == nil { return nil } + if err := t.validateImportTargets(); err != nil { + return err + } + // Start the transformation process return t.transform(g, t.Config) } @@ -61,7 +77,7 @@ func (t *ConfigTransformer) transform(g *Graph, config *configs.Config) error { return err } - // Transform all the children. + // Transform all the children without generating config. for _, c := range config.Children { if err := t.transform(g, c); err != nil { return err @@ -76,12 +92,36 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er module := config.Module log.Printf("[TRACE] ConfigTransformer: Starting for path: %v", path) - allResources := make([]*configs.Resource, 0, len(module.ManagedResources)+len(module.DataResources)) - for _, r := range module.ManagedResources { + var allResources []*configs.Resource + if !t.destroy { + for _, r := range module.ManagedResources { + allResources = append(allResources, r) + } + for _, r := range module.DataResources { + allResources = append(allResources, r) + } + } + + // ephemeral resources act like temporary values and must be added to the + // graph even during destroy operations. + for _, r := range module.EphemeralResources { allResources = append(allResources, r) } - for _, r := range module.DataResources { - allResources = append(allResources, r) + + // Take a copy of the import targets, so we can edit them as we go. + // Only include import targets that are targeting the current module. + var importTargets []*ImportTarget + for _, target := range t.importTargets { + switch { + case target.Config == nil: + if target.LegacyAddr.Module.Module().Equal(config.Path) { + importTargets = append(importTargets, target) + } + default: + if target.Config.ToResource.Module.Equal(config.Path) { + importTargets = append(importTargets, target) + } + } } for _, r := range allResources { @@ -96,12 +136,33 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er // filter them down to the applicable addresses. var imports []*ImportTarget configAddr := relAddr.InModule(path) - for _, i := range t.importTargets { - if target := i.Addr.ContainingResource().Config(); target.Equal(configAddr) { + + var matchedIndices []int + for ix, i := range importTargets { + if i.LegacyAddr.ConfigResource().Equal(configAddr) { + matchedIndices = append(matchedIndices, ix) + imports = append(imports, i) + + } + if i.Config != nil && i.Config.ToResource.Equal(configAddr) { + // This import target has been claimed by an actual resource, + // let's make a note of this to remove it from the targets. + matchedIndices = append(matchedIndices, ix) imports = append(imports, i) } } + for ix := len(matchedIndices) - 1; ix >= 0; ix-- { + tIx := matchedIndices[ix] + + // We do this backwards, since it means we don't have to adjust the + // later indices as we change the length of import targets. + // + // We need to do this separately, as a single resource could match + // multiple import targets. + importTargets = append(importTargets[:tIx], importTargets[tIx+1:]...) + } + abstract := &NodeAbstractResource{ Addr: addrs.ConfigResource{ Resource: relAddr, @@ -118,5 +179,56 @@ func (t *ConfigTransformer) transformSingle(g *Graph, config *configs.Config) er g.Add(node) } + // If any import targets were not claimed by resources we may be + // generating configuration. Add them to the graph for validation. + for _, i := range importTargets { + log.Printf("[DEBUG] ConfigTransformer: adding config generation node for %s", i.Config.ToResource) + + // TODO: if config generation is ever supported for for_each + // resources, this will add multiple nodes for the same + // resource + abstract := &NodeAbstractResource{ + Addr: i.Config.ToResource, + importTargets: []*ImportTarget{i}, + generateConfigPath: t.generateConfigPathForImportTargets, + } + var node dag.Vertex = abstract + if f := t.Concrete; f != nil { + node = f(abstract) + } + + g.Add(node) + } return nil } + +// validateImportTargets ensures that the import target module exists in the +// configuration. Individual resources will be check by the validation node. +func (t *ConfigTransformer) validateImportTargets() error { + if t.destroy { + return nil + } + var diags tfdiags.Diagnostics + + for _, i := range t.importTargets { + var toResource addrs.ConfigResource + switch { + case i.Config != nil: + toResource = i.Config.ToResource + default: + toResource = i.LegacyAddr.ConfigResource() + } + + moduleCfg := t.Config.Root.Descendant(toResource.Module) + if moduleCfg == nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Configuration for import target does not exist", + Detail: fmt.Sprintf("The configuration for the given import target %s does not exist. All target instances must have an associated configuration to be imported.", i.Config.ToResource), + Subject: i.Config.To.Range().Ptr(), + }) + } + } + + return diags.Err() +} diff --git a/internal/terraform/transform_config_test.go b/internal/terraform/transform_config_test.go index ceed2bf356..fac8e24f4c 100644 --- a/internal/terraform/transform_config_test.go +++ b/internal/terraform/transform_config_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_deferred.go b/internal/terraform/transform_deferred.go new file mode 100644 index 0000000000..e0c0e88a7a --- /dev/null +++ b/internal/terraform/transform_deferred.go @@ -0,0 +1,100 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" +) + +// DeferredTransformer is a GraphTransformer that adds graph nodes representing +// each of the deferred changes to the graph. +// +// Deferred changes are not executed during the apply phase, but they are +// tracked in the graph to ensure that the correct ordering is maintained and +// the target flags are correctly applied. +type DeferredTransformer struct { + DeferredChanges []*plans.DeferredResourceInstanceChangeSrc +} + +func (t *DeferredTransformer) Transform(g *Graph) error { + if len(t.DeferredChanges) == 0 { + return nil + } + + // As with the DiffTransformer, DeferredTransformer creates resource + // instance nodes. If there are any whole-resource nodes already in the + // graph, we must ensure they get evaluated before any of the corresponding + // instances by creating dependency edges. + resourceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeConfigResource]() + for _, node := range g.Vertices() { + rn, ok := node.(GraphNodeConfigResource) + if !ok { + continue + } + // We ignore any instances that _also_ implement + // GraphNodeResourceInstance, since in the unlikely event that they + // do exist we'd probably end up creating cycles by connecting them. + if _, ok := node.(GraphNodeResourceInstance); ok { + continue + } + + rAddr := rn.ResourceAddr() + resourceNodes.Put(rAddr, append(resourceNodes.Get(rAddr), rn)) + } + + for _, change := range t.DeferredChanges { + node := &nodeApplyableDeferredInstance{ + NodeAbstractResourceInstance: NewNodeAbstractResourceInstance(change.ChangeSrc.Addr), + Reason: change.DeferredReason, + ChangeSrc: change.ChangeSrc, + } + + // Create a special node for partial instances, that handles the + // addresses a little differently. + if change.DeferredReason == providers.DeferredReasonInstanceCountUnknown { + per := change.ChangeSrc.Addr.PartialResource() + + // This is a partial instance, so we need to create a partial node + // instead of a full instance node. + node := &nodeApplyableDeferredPartialInstance{ + nodeApplyableDeferredInstance: node, + PartialAddr: per, + } + g.Add(node) + + // Now we want to find the expansion node that would be applied for + // this resource, and tell it that it is performing a partial + // expansion. + for _, v := range g.Vertices() { + if n, ok := v.(*nodeExpandApplyableResource); ok { + if per.ConfigResource().Equal(n.Addr) { + n.PartialExpansions = append(n.PartialExpansions, per) + } + } + } + + // Also connect the deferred instance node to the underlying + // resource node to make sure any expansion happens first. + for _, resourceNode := range resourceNodes.Get(node.Addr.ConfigResource()) { + g.Connect(dag.BasicEdge(node, resourceNode)) + } + + continue + } + + // Otherwise, just add the normal deferred instance node. + g.Add(node) + + // Still connect the deferred instance node to the underlying resource + // node. + for _, resourceNode := range resourceNodes.Get(node.Addr.ConfigResource()) { + g.Connect(dag.BasicEdge(node, resourceNode)) + } + } + + return nil +} diff --git a/internal/terraform/transform_destroy_cbd.go b/internal/terraform/transform_destroy_cbd.go index 19dadbe5ae..2b09c7a80a 100644 --- a/internal/terraform/transform_destroy_cbd.go +++ b/internal/terraform/transform_destroy_cbd.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -42,14 +45,14 @@ func (t *ForcedCBDTransformer) Transform(g *Graph) error { } if !dn.CreateBeforeDestroy() { - // If there are no CBD decendent (dependent nodes), then we + // If there are no CBD decendant (dependent nodes), then we // do nothing here. - if !t.hasCBDDescendent(g, v) { - log.Printf("[TRACE] ForcedCBDTransformer: %q (%T) has no CBD descendent, so skipping", dag.VertexName(v), v) + if !t.hasCBDDescendant(g, v) { + log.Printf("[TRACE] ForcedCBDTransformer: %q (%T) has no CBD descendant, so skipping", dag.VertexName(v), v) continue } - // If this isn't naturally a CBD node, this means that an descendent is + // If this isn't naturally a CBD node, this means that an descendant is // and we need to auto-upgrade this node to CBD. We do this because // a CBD node depending on non-CBD will result in cycles. To avoid this, // we always attempt to upgrade it. @@ -68,28 +71,18 @@ func (t *ForcedCBDTransformer) Transform(g *Graph) error { return nil } -// hasCBDDescendent returns true if any descendent (node that depends on this) +// hasCBDDescendant returns true if any descendant (node that depends on this) // has CBD set. -func (t *ForcedCBDTransformer) hasCBDDescendent(g *Graph, v dag.Vertex) bool { - s, _ := g.Descendents(v) - if s == nil { - return true - } - - for _, ov := range s { +func (t *ForcedCBDTransformer) hasCBDDescendant(g *Graph, v dag.Vertex) bool { + return g.MatchDescendant(v, func(ov dag.Vertex) bool { dn, ok := ov.(GraphNodeDestroyerCBD) - if !ok { - continue - } - - if dn.CreateBeforeDestroy() { - // some descendent is CreateBeforeDestroy, so we need to follow suit - log.Printf("[TRACE] ForcedCBDTransformer: %q has CBD descendent %q", dag.VertexName(v), dag.VertexName(ov)) + if ok && dn.CreateBeforeDestroy() { + // some descendant is CreateBeforeDestroy, so we need to follow suit + log.Printf("[TRACE] ForcedCBDTransformer: %q has CBD descendant %q", dag.VertexName(v), dag.VertexName(ov)) return true } - } - - return false + return false + }) } // CBDEdgeTransformer modifies the edges of create-before-destroy ("CBD") nodes @@ -112,9 +105,16 @@ func (t *ForcedCBDTransformer) hasCBDDescendent(g *Graph, v dag.Vertex) bool { // DiffTransformer when building the apply graph. type CBDEdgeTransformer struct { // Module and State are only needed to look up dependencies in - // any way possible. Either can be nil if not availabile. + // any way possible. Either can be nil if not available. Config *configs.Config State *states.State + + // FIXME: This should optimally be decided entirely during plan, and then we + // can rely on the planned changes to determine the CreateBeforeDestroy + // status. This would require very careful auditing however, since not all + // nodes are represented exactly in the changes, and the way + // CreateBeforeDestroy propagates through the graph is extremely important + // for correctness and to prevent cycles. } func (t *CBDEdgeTransformer) Transform(g *Graph) error { diff --git a/internal/terraform/transform_destroy_cbd_test.go b/internal/terraform/transform_destroy_cbd_test.go index 8f5712b574..86f0c0c5da 100644 --- a/internal/terraform/transform_destroy_cbd_test.go +++ b/internal/terraform/transform_destroy_cbd_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,11 +9,12 @@ import ( "testing" "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" ) -func cbdTestGraph(t *testing.T, mod string, changes *plans.Changes, state *states.State) *Graph { +func cbdTestGraph(t *testing.T, mod string, changes *plans.ChangesSrc, state *states.State) *Graph { module := testModule(t, mod) applyBuilder := &ApplyGraphBuilder{ @@ -58,6 +62,13 @@ func cbdTestSteps(steps []GraphTransformer) []GraphTransformer { func filterInstances(g *Graph) *Graph { for _, v := range g.Vertices() { if _, ok := v.(GraphNodeResourceInstance); !ok { + // connect around the node to remove it without breaking deps + for _, down := range g.DownEdges(v) { + for _, up := range g.UpEdges(v) { + g.Connect(dag.BasicEdge(up, down)) + } + } + g.Remove(v) } @@ -66,7 +77,7 @@ func filterInstances(g *Graph) *Graph { } func TestCBDEdgeTransformer(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A"), @@ -121,7 +132,7 @@ test_object.B } func TestCBDEdgeTransformerMulti(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A"), @@ -197,7 +208,7 @@ test_object.C } func TestCBDEdgeTransformer_depNonCBDCount(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A"), @@ -268,7 +279,7 @@ test_object.B\[1\] } func TestCBDEdgeTransformer_depNonCBDCountBoth(t *testing.T) { - changes := &plans.Changes{ + changes := &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: mustResourceInstanceAddr("test_object.A[0]"), diff --git a/internal/terraform/transform_destroy_edge.go b/internal/terraform/transform_destroy_edge.go index 12028ba0d1..ed2d547fed 100644 --- a/internal/terraform/transform_destroy_edge.go +++ b/internal/terraform/transform_destroy_edge.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,6 +8,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/plans" ) // GraphNodeDestroyer must be implemented by nodes that destroy resources. @@ -37,7 +41,100 @@ type GraphNodeCreator interface { // dependent resources will block parent resources from deleting. Concrete // example: VPC with subnets, the VPC can't be deleted while there are // still subnets. -type DestroyEdgeTransformer struct{} +type DestroyEdgeTransformer struct { + // FIXME: GraphNodeCreators are not always applying changes, and should not + // participate in the destroy graph if there are no operations which could + // interract with destroy nodes. We need Changes for now to detect the + // action type, but perhaps this should be indicated somehow by the + // DiffTransformer which was intended to be the only transformer operating + // from the change set. + Changes *plans.ChangesSrc + + // FIXME: Operation will not be needed here one we can better track + // inter-provider dependencies and remove the cycle checks in + // tryInterProviderDestroyEdge. + Operation walkOperation +} + +// tryInterProviderDestroyEdge checks if we're inserting a destroy edge +// across a provider boundary, and only adds the edge if it results in no cycles. +// +// FIXME: The cycles can arise in valid configurations when a provider depends +// on resources from another provider. In the future we may want to inspect +// the dependencies of the providers themselves, to avoid needing to use the +// blunt hammer of checking for cycles. +// +// A reduced example of this dependency problem looks something like: +/* + +createA <- createB + | \ / | + | providerB <- | + v \ v +destroyA -------------> destroyB + +*/ +// +// The edge from destroyA to destroyB would be skipped in this case, but there +// are still other combinations of changes which could connect the A and B +// groups around providerB in various ways. +// +// The most difficult problem here happens during a full destroy operation. +// That creates a special case where resources on which a provider depends must +// exist for evaluation before they are destroyed. This means that any provider +// dependencies must wait until all that provider's resources have first been +// destroyed. This is where these cross-provider edges are still required to +// ensure the correct order. +func (t *DestroyEdgeTransformer) tryInterProviderDestroyEdge(g *Graph, from, to dag.Vertex) { + e := dag.BasicEdge(from, to) + g.Connect(e) + + // If this is a complete destroy operation, then there are no create/update + // nodes to worry about and we can accept the edge without deeper inspection. + if t.Operation == walkDestroy || t.Operation == walkPlanDestroy { + return + } + + // getComparableProvider inspects the node to try and get the most precise + // description of the provider being used to help determine if 2 nodes are + // from the same provider instance. + getComparableProvider := func(pc GraphNodeProviderConsumer) string { + ps := pc.Provider().String() + + // we don't care about `exact` here, since we're only looking for any + // clue that the providers may differ. + p, _ := pc.ProvidedBy() + switch p := p.(type) { + case addrs.AbsProviderConfig: + ps = p.String() + case addrs.LocalProviderConfig: + ps = p.String() + } + + return ps + } + + pc, ok := from.(GraphNodeProviderConsumer) + if !ok { + return + } + fromProvider := getComparableProvider(pc) + + pc, ok = to.(GraphNodeProviderConsumer) + if !ok { + return + } + toProvider := getComparableProvider(pc) + + // Check for cycles, and back out the edge if there are any. + // The cycles we are looking for only appears between providers, so don't + // waste time checking for cycles if both nodes use the same provider. + if fromProvider != toProvider && g.Ancestors(to).Include(from) { + log.Printf("[DEBUG] DestroyEdgeTransformer: skipping inter-provider edge %s->%s which creates a cycle", + dag.VertexName(from), dag.VertexName(to)) + g.RemoveEdge(e) + } +} func (t *DestroyEdgeTransformer) Transform(g *Graph) error { // Build a map of what is being destroyed (by address string) to @@ -70,8 +167,20 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { resAddr := addr.ContainingResource().Config().String() destroyersByResource[resAddr] = append(destroyersByResource[resAddr], n) case GraphNodeCreator: - addr := n.CreateAddr().ContainingResource().Config().String() - creators[addr] = append(creators[addr], n) + addr := n.CreateAddr() + cfgAddr := addr.ContainingResource().Config().String() + + if t.Changes == nil { + // unit tests may not have changes + creators[cfgAddr] = append(creators[cfgAddr], n) + break + } + + // NoOp changes should not participate in the destroy dependencies. + rc := t.Changes.ResourceInstance(*addr) + if rc != nil && rc.Action != plans.NoOp { + creators[cfgAddr] = append(creators[cfgAddr], n) + } } } @@ -81,60 +190,6 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { return nil } - // Connect destroy dependencies as stored in the state - for _, ds := range destroyers { - for _, des := range ds { - ri, ok := des.(GraphNodeResourceInstance) - if !ok { - continue - } - - for _, resAddr := range ri.StateDependencies() { - for _, desDep := range destroyersByResource[resAddr.String()] { - if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(desDep, des) { - log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(desDep), dag.VertexName(des)) - g.Connect(dag.BasicEdge(desDep, des)) - } else { - log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(desDep), dag.VertexName(des)) - } - } - - // We can have some create or update nodes which were - // dependents of the destroy node. If they have no destroyer - // themselves, make the connection directly from the creator. - for _, createDep := range creators[resAddr.String()] { - if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(createDep, des) { - log.Printf("[DEBUG] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(createDep), dag.VertexName(des)) - g.Connect(dag.BasicEdge(createDep, des)) - } else { - log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(createDep), dag.VertexName(des)) - } - } - } - } - } - - // connect creators to any destroyers on which they may depend - for _, cs := range creators { - for _, c := range cs { - ri, ok := c.(GraphNodeResourceInstance) - if !ok { - continue - } - - for _, resAddr := range ri.StateDependencies() { - for _, desDep := range destroyersByResource[resAddr.String()] { - if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(c, desDep) { - log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(c), dag.VertexName(desDep)) - g.Connect(dag.BasicEdge(c, desDep)) - } else { - log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(c), dag.VertexName(desDep)) - } - } - } - } - } - // Go through and connect creators to destroyers. Going along with // our example, this makes: A_d => A for _, v := range g.Vertices() { @@ -161,97 +216,138 @@ func (t *DestroyEdgeTransformer) Transform(g *Graph) error { } } - return nil -} + // connect creators to any destroyers on which they may depend + for _, cs := range creators { + for _, c := range cs { + ri, ok := c.(GraphNodeResourceInstance) + if !ok { + continue + } -// Remove any nodes that aren't needed when destroying modules. -// Variables, outputs, locals, and expanders may not be able to evaluate -// correctly, so we can remove these if nothing depends on them. The module -// closers also need to disable their use of expansion if the module itself is -// no longer present. -type pruneUnusedNodesTransformer struct { -} - -func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error { - // We need a reverse depth first walk of modules, processing them in order - // from the leaf modules to the root. This allows us to remove unneeded - // dependencies from child modules, freeing up nodes in the parent module - // to also be removed. - - nodes := g.Vertices() - - for removed := true; removed; { - removed = false - - for i := 0; i < len(nodes); i++ { - // run this in a closure, so we can return early rather than - // dealing with complex looping and labels - func() { - n := nodes[i] - switch n := n.(type) { - case graphNodeTemporaryValue: - // root module outputs indicate they are not temporary by - // returning false here. - if !n.temporaryValue() { - return + for _, resAddr := range ri.StateDependencies() { + for _, desDep := range destroyersByResource[resAddr.String()] { + if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(c, desDep) { + log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(c), dag.VertexName(desDep)) + g.Connect(dag.BasicEdge(c, desDep)) + } else { + log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(c), dag.VertexName(desDep)) } + } + } + } + } - // temporary values, which consist of variables, locals, - // and outputs, must be kept if anything refers to them. - for _, v := range g.UpEdges(n) { - // keep any value which is connected through a - // reference - if _, ok := v.(GraphNodeReferencer); ok { - return - } + // Connect destroy dependencies as stored in the state + for _, ds := range destroyers { + for _, des := range ds { + ri, ok := des.(GraphNodeResourceInstance) + if !ok { + continue + } + + for _, resAddr := range ri.StateDependencies() { + for _, desDep := range destroyersByResource[resAddr.String()] { + if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(desDep, des) { + log.Printf("[TRACE] DestroyEdgeTransformer: %s has stored dependency of %s\n", dag.VertexName(desDep), dag.VertexName(des)) + t.tryInterProviderDestroyEdge(g, desDep, des) + } else { + log.Printf("[TRACE] DestroyEdgeTransformer: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(desDep), dag.VertexName(des)) } - - case graphNodeExpandsInstances: - // Any nodes that expand instances are kept when their - // instances may need to be evaluated. - for _, v := range g.UpEdges(n) { - switch v.(type) { - case graphNodeExpandsInstances: - // Root module output values (which the following - // condition matches) are exempt because we know - // there is only ever exactly one instance of the - // root module, and so it's not actually important - // to expand it and so this lets us do a bit more - // pruning than we'd be able to do otherwise. - if tmp, ok := v.(graphNodeTemporaryValue); ok && !tmp.temporaryValue() { - continue - } - - // expanders can always depend on module expansion - // themselves - return - case GraphNodeResourceInstance: - // resource instances always depend on their - // resource node, which is an expander - return - } - } - - case GraphNodeProvider: - // Providers that may have been required by expansion nodes - // that we no longer need can also be removed. - if g.UpEdges(n).Len() > 0 { - return - } - - default: - return } - log.Printf("[DEBUG] pruneUnusedNodes: %s is no longer needed, removing", dag.VertexName(n)) - g.Remove(n) - removed = true - - // remove the node from our iteration as well - last := len(nodes) - 1 - nodes[i], nodes[last] = nodes[last], nodes[i] - nodes = nodes[:last] - }() + // We can have some create or update nodes which were + // dependents of the destroy node. If they have no destroyer + // themselves, make the connection directly from the creator. + for _, createDep := range creators[resAddr.String()] { + if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(createDep, des) { + log.Printf("[DEBUG] DestroyEdgeTransformer2: %s has stored dependency of %s\n", dag.VertexName(createDep), dag.VertexName(des)) + t.tryInterProviderDestroyEdge(g, createDep, des) + } else { + log.Printf("[TRACE] DestroyEdgeTransformer2: skipping %s => %s inter-module-instance dependency\n", dag.VertexName(createDep), dag.VertexName(des)) + } + } + } + } + } + + return nil +} + +// Remove nodes that aren't needed, when planning or applying a full destroy. +// Specifically, we want to remove any temporary values (variables, outputs, +// locals) that aren't ultimately referenced by a provider, as well as any +// expanders whose instances are never relevant. This is necessary because of +// some interacting behaviors: +// +// - In a destroy, we create nodes from the full config, but then try to *act* +// like we're using a config with all the resources removed. So we have many +// nodes for temporary values that *would not exist* in the stipulated +// "resources gone" config. (This is why we *can* prune some nodes.) +// +// - We still need provider configurations to destroy anything. Provider configs +// must be re-evaluated during apply (they aren't cached in the state or plan), +// and they might refer to temporary values like locals or variables. (This is +// why we can't just prune *all* the temporary value nodes.) +// +// - Any node referenced by an in-use provider should end up properly anchored +// in the destroy graph ordering. But other temporary values are more randomly +// ordered (because we don't bother fixing edges to guarantee they happen before +// relevant destructions), so they might be impossible to evaluate properly +// during a destroy, especially if we already performed a partial destroy and +// got interrupted. (This is why we *must* prune some nodes.) +// +// - The first and third points above aren't relevant in a normal run that +// happens to perform some destructions, because any temporary value that +// references a destroyed resource will get ordered after the creation of the +// resource's replacement. (This is why we only prune for destroys.) +type pruneUnusedNodesTransformer struct { + // Both the plan and apply graph builders will skip this transformer except + // during a full destroy. + skip bool +} + +func (t *pruneUnusedNodesTransformer) Transform(g *Graph) error { + if t.skip { + return nil + } + + // we need to track nodes to keep, because the dependency trees can overlap, + // so we can't just remove all dependencies of nodes we don't want. + keep := make(dag.Set) + + // Only keep destroyers, their providers, and anything the providers need + // for configuration. Since the destroyer should already be hooked up to the + // provider, keeping all the destroyer dependencies should suffice. + for _, n := range g.Vertices() { + // a special case of destroyer, is that by convention Terraform expects + // root outputs to be "destroyed", and the output node is what writes + // the nil state. A root module output currently identifies itself as a + // temporary value which is not temporary for that reason. + if tmp, ok := n.(graphNodeTemporaryValue); ok && !tmp.temporaryValue() { + log.Printf("[TRACE] pruneUnusedNodesTransformer: keeping root output %s", dag.VertexName(n)) + keep.Add(n) + continue + } + + // from here we only search for managed resource destroy nodes + n, ok := n.(GraphNodeDestroyer) + if !ok { + continue + } + + log.Printf("[TRACE] pruneUnusedNodesTransformer: keeping destroy node %s", dag.VertexName(n)) + keep.Add(n) + + for _, anc := range g.Ancestors(n) { + log.Printf("[TRACE] pruneUnusedNodesTransformer: keeping %s as dependency of %s", dag.VertexName(anc), dag.VertexName(n)) + keep.Add(anc) + } + } + + for _, n := range g.Vertices() { + if !keep.Include(n) { + log.Printf("[TRACE] pruneUnusedNodesTransformer: removing %s", dag.VertexName(n)) + g.Remove(n) } } diff --git a/internal/terraform/transform_destroy_edge_test.go b/internal/terraform/transform_destroy_edge_test.go index a369099401..778fe74ede 100644 --- a/internal/terraform/transform_destroy_edge_test.go +++ b/internal/terraform/transform_destroy_edge_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -12,6 +15,7 @@ import ( "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestDestroyEdgeTransformer_basic(t *testing.T) { @@ -364,8 +368,8 @@ func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) { providerCfgAddr, ) }) - changes := plans.NewChanges() - changes.SyncWrapper().AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ + changes := plans.NewChangesSrc() + changes.AppendResourceInstanceChange(&plans.ResourceInstanceChangeSrc{ Addr: resourceInstAddr, PrevRunAddr: resourceInstAddr, ProviderAddr: providerCfgAddr, @@ -383,8 +387,7 @@ func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) { Config: config, }, &OutputTransformer{ - Config: config, - Changes: changes, + Config: config, }, &DiffTransformer{ Concrete: concreteResourceInstance, @@ -398,7 +401,7 @@ func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) { }, } graph, diags := builder.Build(addrs.RootModuleInstance) - assertNoDiagnostics(t, diags) + tfdiags.AssertNoDiagnostics(t, diags) // At this point, thanks to pruneUnusedNodesTransformer, we should still // have the node for the output value, but the "test.a (expand)" node @@ -441,6 +444,123 @@ func TestPruneUnusedNodesTransformer_rootModuleOutputValues(t *testing.T) { } } +// NoOp changes should not be participating in the destroy sequence +func TestDestroyEdgeTransformer_noOp(t *testing.T) { + g := Graph{Path: addrs.RootModuleInstance} + g.Add(testDestroyNode("test_object.A")) + g.Add(testUpdateNode("test_object.B")) + g.Add(testDestroyNode("test_object.C")) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.A").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"A"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.B").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"B","test_string":"x"}`), + Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A")}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.C").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"C","test_string":"x"}`), + Dependencies: []addrs.ConfigResource{mustConfigResourceAddr("test_object.A"), + mustConfigResourceAddr("test_object.B")}, + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { + t.Fatal(err) + } + + tf := &DestroyEdgeTransformer{ + // We only need a minimal object to indicate GraphNodeCreator change is + // a NoOp here. + Changes: &plans.ChangesSrc{ + Resources: []*plans.ResourceInstanceChangeSrc{ + { + Addr: mustResourceInstanceAddr("test_object.B"), + ChangeSrc: plans.ChangeSrc{Action: plans.NoOp}, + }, + }, + }, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + expected := strings.TrimSpace(` +test_object.A (destroy) + test_object.C (destroy) +test_object.B +test_object.C (destroy)`) + + actual := strings.TrimSpace(g.String()) + if actual != expected { + t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) + } +} + +func TestDestroyEdgeTransformer_dataDependsOn(t *testing.T) { + g := Graph{Path: addrs.RootModuleInstance} + + addrA := mustResourceInstanceAddr("test_object.A") + instA := NewNodeAbstractResourceInstance(addrA) + a := &NodeDestroyResourceInstance{NodeAbstractResourceInstance: instA} + g.Add(a) + + // B here represents a data sources, which is effectively an update during + // apply, but won't have dependencies stored in the state. + addrB := mustResourceInstanceAddr("test_object.B") + instB := NewNodeAbstractResourceInstance(addrB) + instB.Dependencies = append(instB.Dependencies, addrA.ConfigResource()) + b := &NodeApplyableResourceInstance{NodeAbstractResourceInstance: instB} + + g.Add(b) + + state := states.NewState() + root := state.EnsureModule(addrs.RootModuleInstance) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("test_object.A").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"A"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`), + ) + + if err := (&AttachStateTransformer{State: state}).Transform(&g); err != nil { + t.Fatal(err) + } + + tf := &DestroyEdgeTransformer{} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(` +test_object.A (destroy) +test_object.B + test_object.A (destroy) +`) + if actual != expected { + t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) + } +} + func testDestroyNode(addrString string) GraphNodeDestroyer { instAddr := mustResourceInstanceAddr(addrString) inst := NewNodeAbstractResourceInstance(instAddr) diff --git a/internal/terraform/transform_diff.go b/internal/terraform/transform_diff.go index 894db4c519..90df9a9d86 100644 --- a/internal/terraform/transform_diff.go +++ b/internal/terraform/transform_diff.go @@ -1,9 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" "log" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/states" @@ -15,7 +20,29 @@ import ( type DiffTransformer struct { Concrete ConcreteResourceInstanceNodeFunc State *states.State - Changes *plans.Changes + Changes *plans.ChangesSrc + Config *configs.Config +} + +// return true if the given resource instance has either Preconditions or +// Postconditions defined in the configuration. +func (t *DiffTransformer) hasConfigConditions(addr addrs.AbsResourceInstance) bool { + // unit tests may have no config + if t.Config == nil { + return false + } + + cfg := t.Config.DescendantForInstance(addr.Module) + if cfg == nil { + return false + } + + res := cfg.Module.ResourceByAddr(addr.ConfigResource().Resource) + if res == nil { + return false + } + + return len(res.Preconditions) > 0 || len(res.Postconditions) > 0 } func (t *DiffTransformer) Transform(g *Graph) error { @@ -36,7 +63,7 @@ func (t *DiffTransformer) Transform(g *Graph) error { // get evaluated before any of the corresponding instances by creating // dependency edges, so we'll do some prep work here to ensure we'll only // create connections to nodes that existed before we started here. - resourceNodes := map[string][]GraphNodeConfigResource{} + resourceNodes := addrs.MakeMap[addrs.ConfigResource, []GraphNodeConfigResource]() for _, node := range g.Vertices() { rn, ok := node.(GraphNodeConfigResource) if !ok { @@ -49,8 +76,8 @@ func (t *DiffTransformer) Transform(g *Graph) error { continue } - addr := rn.ResourceAddr().String() - resourceNodes[addr] = append(resourceNodes[addr], rn) + rAddr := rn.ResourceAddr() + resourceNodes.Put(rAddr, append(resourceNodes.Get(rAddr), rn)) } for _, rc := range changes.Resources { @@ -62,29 +89,34 @@ func (t *DiffTransformer) Transform(g *Graph) error { // Depending on the action we'll need some different combinations of // nodes, because destroying uses a special node type separate from // other actions. - var update, delete, createBeforeDestroy bool + var update, delete, forget, createBeforeDestroy bool switch rc.Action { case plans.NoOp: // For a no-op change we don't take any action but we still // run any condition checks associated with the object, to // make sure that they still hold when considering the // results of other changes. - update = true + update = t.hasConfigConditions(addr) case plans.Delete: delete = true case plans.DeleteThenCreate, plans.CreateThenDelete: update = true delete = true createBeforeDestroy = (rc.Action == plans.CreateThenDelete) + case plans.Forget: + forget = true default: update = true } - if dk != states.NotDeposed && update { + // A deposed instance may only have a change of Delete, Forget, or NoOp. + // A NoOp can happen if the provider shows it no longer exists during + // the most recent ReadResource operation. + if dk != states.NotDeposed && !(rc.Action == plans.Delete || rc.Action == plans.Forget || rc.Action == plans.NoOp) { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, "Invalid planned change for deposed object", - fmt.Sprintf("The plan contains a non-delete change for %s deposed object %s. The only valid action for a deposed object is to destroy it, so this is a bug in Terraform.", addr, dk), + fmt.Sprintf("The plan contains a non-remove change for %s deposed object %s. The only valid actions for a deposed object are to destroy it or remove it from state, so this is a bug in Terraform.", addr, dk), )) continue } @@ -148,8 +180,7 @@ func (t *DiffTransformer) Transform(g *Graph) error { } g.Add(node) - rsrcAddr := addr.ContainingResource().String() - for _, rsrcNode := range resourceNodes[rsrcAddr] { + for _, rsrcNode := range resourceNodes.Get(addr.ConfigResource()) { g.Connect(dag.BasicEdge(node, rsrcNode)) } } @@ -163,7 +194,6 @@ func (t *DiffTransformer) Transform(g *Graph) error { if dk == states.NotDeposed { node = &NodeDestroyResourceInstance{ NodeAbstractResourceInstance: abstract, - DeposedKey: dk, } } else { node = &NodeDestroyDeposedResourceInstanceObject{ @@ -179,6 +209,24 @@ func (t *DiffTransformer) Transform(g *Graph) error { g.Add(node) } + if forget { + var node GraphNodeResourceInstance + abstract := NewNodeAbstractResourceInstance(addr) + if dk == states.NotDeposed { + node = &NodeForgetResourceInstance{ + NodeAbstractResourceInstance: abstract, + } + } else { + node = &NodeForgetDeposedResourceInstanceObject{ + NodeAbstractResourceInstance: abstract, + DeposedKey: dk, + } + } + + log.Printf("[TRACE] DiffTransformer: %s will be represented for forgetting by %s", addr, dag.VertexName(node)) + g.Add(node) + } + } log.Printf("[TRACE] DiffTransformer complete") diff --git a/internal/terraform/transform_diff_test.go b/internal/terraform/transform_diff_test.go index c7cddc452f..9549b3c167 100644 --- a/internal/terraform/transform_diff_test.go +++ b/internal/terraform/transform_diff_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -35,7 +38,7 @@ func TestDiffTransformer(t *testing.T) { } tf := &DiffTransformer{ - Changes: &plans.Changes{ + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ @@ -81,6 +84,26 @@ func TestDiffTransformer_noOpChange(t *testing.T) { // results changed even if the resource instance they are attached to // didn't actually change directly itself. + // aws_instance.foo has a precondition, so should be included in the final + // graph. aws_instance.bar has no conditions, so there is nothing to + // execute during apply and it should not be included in the graph. + m := testModuleInline(t, map[string]string{ + "main.tf": ` +resource "aws_instance" "bar" { +} + +resource "aws_instance" "foo" { + test_string = "ok" + + lifecycle { + precondition { + condition = self.test_string != "" + error_message = "resource error" + } + } +} +`}) + g := Graph{Path: addrs.RootModuleInstance} beforeVal, err := plans.NewDynamicValue(cty.StringVal(""), cty.String) @@ -89,7 +112,8 @@ func TestDiffTransformer_noOpChange(t *testing.T) { } tf := &DiffTransformer{ - Changes: &plans.Changes{ + Config: m, + Changes: &plans.ChangesSrc{ Resources: []*plans.ResourceInstanceChangeSrc{ { Addr: addrs.Resource{ @@ -109,6 +133,24 @@ func TestDiffTransformer_noOpChange(t *testing.T) { After: beforeVal, }, }, + { + Addr: addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "bar", + }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), + ProviderAddr: addrs.AbsProviderConfig{ + Provider: addrs.NewDefaultProvider("aws"), + Module: addrs.RootModule, + }, + ChangeSrc: plans.ChangeSrc{ + // A "no-op" change has the no-op action and has the + // same object as both Before and After. + Action: plans.NoOp, + Before: beforeVal, + After: beforeVal, + }, + }, }, }, } diff --git a/internal/terraform/transform_ephemeral_resource_close.go b/internal/terraform/transform_ephemeral_resource_close.go new file mode 100644 index 0000000000..adec22343d --- /dev/null +++ b/internal/terraform/transform_ephemeral_resource_close.go @@ -0,0 +1,84 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/dag" +) + +// ephemeralResourceCloseTransformer is a graph transformer that inserts a +// nodeEphemeralResourceClose node for each ephemeral resource, and arranges for +// the close node to depend on any other node that consumes the relevant +// ephemeral resource. +type ephemeralResourceCloseTransformer struct { + // This does not need to run during validate walks since the ephemeral + // resources will never be opened. + skip bool +} + +func (t *ephemeralResourceCloseTransformer) Transform(g *Graph) error { + if t.skip { + // Nothing to do if ephemeral resources are not opened + return nil + } + + verts := g.Vertices() + for _, v := range verts { + // find any ephemeral resource nodes + v, ok := v.(GraphNodeConfigResource) + if !ok { + continue + } + addr := v.ResourceAddr() + if addr.Resource.Mode != addrs.EphemeralResourceMode { + continue + } + + closeNode := &nodeEphemeralResourceClose{ + // the node must also be a ProviderConsumer + resourceNode: v.(GraphNodeProviderConsumer), + addr: addr, + } + log.Printf("[TRACE] ephemeralResourceCloseTransformer: adding close node for %s", addr) + g.Add(closeNode) + g.Connect(dag.BasicEdge(closeNode, v)) + + // Now we have an ephemeral resource, and we need to depend on all + // dependents of that resource. Rather than connect directly to them all + // however, we'll only connect to leaf nodes by finding those that have + // no up edges. + lastReferences := g.FirstDescendantsWith(v, func(v dag.Vertex) bool { + // We want something which is both a referencer and has no incoming + // edges from referencers. While it wouldn't be incorrect to just + // check for all leaf nodes, we are trying to connect to the end of + // evaluation chain, otherwise we may just as well wait until the end + // of the walk and close everything together. We technically don't + // know if these nodes are connected because they reference the + // ephemeral value, or if they are connected for some other + // dependency reason, but this generally shouldn't matter as we can + // count any dependency as a reason to maintain the ephemeral value. + if _, ok := v.(GraphNodeReferencer); !ok { + return false + } + + up := g.UpEdges(v) + up = up.Filter(func(v any) bool { + _, ok := v.(GraphNodeReferencer) + return ok + }) + + // if there are no references connected to this node, then we can be + // sure it's the last referencer in the chain. + return len(up) == 0 + }) + + for last := range lastReferences.List() { + g.Connect(dag.BasicEdge(closeNode, last)) + } + } + return nil +} diff --git a/internal/terraform/transform_expand.go b/internal/terraform/transform_expand.go index dca71b630f..38d7ababc1 100644 --- a/internal/terraform/transform_expand.go +++ b/internal/terraform/transform_expand.go @@ -1,9 +1,24 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform +import ( + "github.com/hashicorp/terraform/internal/tfdiags" +) + // GraphNodeDynamicExpandable is an interface that nodes can implement // to signal that they can be expanded at eval-time (hence dynamic). // These nodes are given the eval context and are expected to return // a new subgraph. type GraphNodeDynamicExpandable interface { - DynamicExpand(EvalContext) (*Graph, error) + // DynamicExpand returns a new graph which will be treated as the dynamic + // subgraph of the receiving node. + // + // The second return value is of type error for historical reasons; + // it's valid (and most ideal) for DynamicExpand to return the result + // of calling ErrWithWarnings on a tfdiags.Diagnostics value instead, + // in which case the caller will unwrap it and gather the individual + // diagnostics. + DynamicExpand(EvalContext) (*Graph, tfdiags.Diagnostics) } diff --git a/internal/terraform/transform_external_reference.go b/internal/terraform/transform_external_reference.go new file mode 100644 index 0000000000..ae104dd312 --- /dev/null +++ b/internal/terraform/transform_external_reference.go @@ -0,0 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import "github.com/hashicorp/terraform/internal/addrs" + +// ExternalReferenceTransformer will add a GraphNodeReferencer into the graph +// that makes no changes to the graph itself but, by referencing the addresses +// within ExternalReferences, ensures that any temporary nodes that are required +// by an external caller, such as the terraform testing framework, are not +// skipped because they are not referenced from within the module. +type ExternalReferenceTransformer struct { + ExternalReferences []*addrs.Reference +} + +func (t *ExternalReferenceTransformer) Transform(g *Graph) error { + if len(t.ExternalReferences) == 0 { + return nil + } + + g.Add(&nodeExternalReference{ + ExternalReferences: t.ExternalReferences, + }) + return nil +} diff --git a/internal/terraform/transform_import_state_test.go b/internal/terraform/transform_import_state_test.go index 919f09d84b..eb28feff16 100644 --- a/internal/terraform/transform_import_state_test.go +++ b/internal/terraform/transform_import_state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -27,8 +30,23 @@ func TestGraphNodeImportStateExecute(t *testing.T) { provider.ConfigureProvider(providers.ConfigureProviderRequest{}) ctx := &MockEvalContext{ + Scope: evalContextModuleInstance{Addr: addrs.RootModuleInstance}, StateState: state.SyncWrapper(), ProviderProvider: provider, + ProviderSchemaSchema: providers.GetProviderSchemaResponse{ + ResourceTypes: map[string]providers.Schema{ + "aws_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + }, + }, + }, + }, + }, } // Import a new aws_instance.foo, this time with ID=bar. The original @@ -69,13 +87,15 @@ func TestGraphNodeImportStateSubExecute(t *testing.T) { ctx := &MockEvalContext{ StateState: state.SyncWrapper(), ProviderProvider: provider, - ProviderSchemaSchema: &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + ProviderSchemaSchema: providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Computed: true, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, }, }, }, @@ -129,13 +149,15 @@ func TestGraphNodeImportStateSubExecuteNull(t *testing.T) { ctx := &MockEvalContext{ StateState: state.SyncWrapper(), ProviderProvider: provider, - ProviderSchemaSchema: &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ + ProviderSchemaSchema: providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Attributes: map[string]*configschema.Attribute{ - "id": { - Type: cty.String, - Computed: true, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Computed: true, + }, }, }, }, diff --git a/internal/terraform/transform_local.go b/internal/terraform/transform_local.go index 667d6f917e..7ab2ae9a01 100644 --- a/internal/terraform/transform_local.go +++ b/internal/terraform/transform_local.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_module_expansion.go b/internal/terraform/transform_module_expansion.go index a4d45d963c..4dd2eb2354 100644 --- a/internal/terraform/transform_module_expansion.go +++ b/internal/terraform/transform_module_expansion.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -29,7 +32,7 @@ type ModuleExpansionTransformer struct { func (t *ModuleExpansionTransformer) Transform(g *Graph) error { t.closers = make(map[string]*nodeCloseModule) // The root module is always a singleton and so does not need expansion - // processing, but any descendent modules do. We'll process them + // processing, but any descendant modules do. We'll process them // recursively using t.transform. for _, cfg := range t.Config.Children { err := t.transform(g, cfg, nil) diff --git a/internal/terraform/transform_module_variable.go b/internal/terraform/transform_module_variable.go index a9fa02c4e1..6da5c4bc6e 100644 --- a/internal/terraform/transform_module_variable.go +++ b/internal/terraform/transform_module_variable.go @@ -1,13 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "fmt" - "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/tfdiags" "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/tfdiags" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/configs" ) @@ -23,6 +28,14 @@ import ( // steps for validating module blocks, separate from this transform. type ModuleVariableTransformer struct { Config *configs.Config + + // Planning must be set to true when building a planning graph, and must be + // false when building an apply graph. + Planning bool + + // DestroyApply must be set to true when applying a destroy operation and + // false otherwise. + DestroyApply bool } func (t *ModuleVariableTransformer) Transform(g *Graph) error { @@ -101,9 +114,11 @@ func (t *ModuleVariableTransformer) transformSingle(g *Graph, parent, c *configs Addr: addrs.InputVariable{ Name: v.Name, }, - Module: c.Path, - Config: v, - Expr: expr, + Module: c.Path, + Config: v, + Expr: expr, + Planning: t.Planning, + DestroyApply: t.DestroyApply, } g.Add(node) } diff --git a/internal/terraform/transform_module_variable_test.go b/internal/terraform/transform_module_variable_test.go index 363d141ae7..470548f369 100644 --- a/internal/terraform/transform_module_variable_test.go +++ b/internal/terraform/transform_module_variable_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_orphan_count.go b/internal/terraform/transform_orphan_count.go index 5b9f3c75c7..a6a46a11c9 100644 --- a/internal/terraform/transform_orphan_count.go +++ b/internal/terraform/transform_orphan_count.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -33,7 +36,13 @@ func (t *OrphanResourceInstanceCountTransformer) Transform(g *Graph) error { // number of instances of a single resource ought to always be small in any // reasonable Terraform configuration. Have: - for key := range rs.Instances { + for key, inst := range rs.Instances { + // Instances which have no current objects (only one or more + // deposed objects) will be taken care of separately + if inst.Current == nil { + continue + } + thisAddr := rs.Addr.Instance(key) for _, wantAddr := range t.InstanceAddrs { if wantAddr.Equal(thisAddr) { diff --git a/internal/terraform/transform_orphan_count_test.go b/internal/terraform/transform_orphan_count_test.go index a777631f09..d57e09d38e 100644 --- a/internal/terraform/transform_orphan_count_test.go +++ b/internal/terraform/transform_orphan_count_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -161,6 +164,66 @@ func TestOrphanResourceCountTransformer_oneIndex(t *testing.T) { } } +func TestOrphanResourceCountTransformer_deposed(t *testing.T) { + state := states.NewState() + root := state.RootModule() + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("aws_instance.web").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("aws_instance.foo[0]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + root.SetResourceInstanceCurrent( + mustResourceInstanceAddr("aws_instance.foo[1]").Resource, + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + root.SetResourceInstanceDeposed( + mustResourceInstanceAddr("aws_instance.foo[2]").Resource, + states.NewDeposedKey(), + &states.ResourceInstanceObjectSrc{ + Status: states.ObjectReady, + AttrsJSON: []byte(`{"id":"foo"}`), + }, + mustProviderConfig(`provider["registry.terraform.io/hashicorp/aws"]`), + ) + + g := Graph{Path: addrs.RootModuleInstance} + + { + tf := &OrphanResourceInstanceCountTransformer{ + Concrete: testOrphanResourceConcreteFunc, + Addr: addrs.RootModuleInstance.Resource( + addrs.ManagedResourceMode, "aws_instance", "foo", + ), + InstanceAddrs: []addrs.AbsResourceInstance{mustResourceInstanceAddr("aws_instance.foo[0]")}, + State: state, + } + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformOrphanResourceCountDeposedStr) + if actual != expected { + t.Fatalf("bad:\n\n%s", actual) + } +} + // When converting from a NoEach mode to an EachMap via a switch to for_each, // an edge is necessary to ensure that the map-key'd instances // are evaluated after the NoKey resource, because the final instance evaluated @@ -236,6 +299,10 @@ const testTransformOrphanResourceCountOneIndexStr = ` aws_instance.foo[1] (orphan) ` +const testTransformOrphanResourceCountDeposedStr = ` +aws_instance.foo[1] (orphan) +` + const testTransformOrphanResourceForEachStr = ` aws_instance.foo (orphan) aws_instance.foo["bar"] (orphan) diff --git a/internal/terraform/transform_orphan_output.go b/internal/terraform/transform_orphan_output.go index 320c91fbe1..866d6e9dd9 100644 --- a/internal/terraform/transform_orphan_output.go +++ b/internal/terraform/transform_orphan_output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -12,8 +15,9 @@ import ( // in the given config that are in the state and adds them to the graph // for deletion. type OrphanOutputTransformer struct { - Config *configs.Config // Root of config tree - State *states.State // State is the root state + Config *configs.Config // Root of config tree + State *states.State // State is the root state + Planning bool } func (t *OrphanOutputTransformer) Transform(g *Graph) error { @@ -22,37 +26,14 @@ func (t *OrphanOutputTransformer) Transform(g *Graph) error { return nil } - for _, ms := range t.State.Modules { - if err := t.transform(g, ms); err != nil { - return err - } - } - return nil -} - -func (t *OrphanOutputTransformer) transform(g *Graph, ms *states.Module) error { - if ms == nil { - return nil - } - - moduleAddr := ms.Addr - - // Get the config for this path, which is nil if the entire module has been - // removed. - var outputs map[string]*configs.Output - if c := t.Config.DescendentForInstance(moduleAddr); c != nil { - outputs = c.Module.Outputs - } - - // An output is "orphaned" if it's present in the state but not declared - // in the configuration. - for name := range ms.OutputValues { - if _, exists := outputs[name]; exists { + cfgs := t.Config.Module.Outputs + for name := range t.State.RootOutputValues { + if _, exists := cfgs[name]; exists { continue } - g.Add(&NodeDestroyableOutput{ - Addr: addrs.OutputValue{Name: name}.Absolute(moduleAddr), + Addr: addrs.OutputValue{Name: name}.Absolute(addrs.RootModuleInstance), + Planning: t.Planning, }) } diff --git a/internal/terraform/transform_orphan_resource.go b/internal/terraform/transform_orphan_resource.go index 974fdf0de3..0d9a6bb0a3 100644 --- a/internal/terraform/transform_orphan_resource.go +++ b/internal/terraform/transform_orphan_resource.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -68,7 +71,7 @@ func (t *OrphanResourceInstanceTransformer) transform(g *Graph, ms *states.Modul // nil if the module was removed from the configuration. This is okay, // this just means that every resource is an orphan. var m *configs.Module - if c := t.Config.DescendentForInstance(moduleAddr); c != nil { + if c := t.Config.DescendantForInstance(moduleAddr); c != nil { m = c.Module } @@ -87,7 +90,8 @@ func (t *OrphanResourceInstanceTransformer) transform(g *Graph, ms *states.Modul } for key, inst := range rs.Instances { - // deposed instances will be taken care of separately + // Instances which have no current objects (only one or more + // deposed objects) will be taken care of separately if inst.Current == nil { continue } diff --git a/internal/terraform/transform_orphan_resource_test.go b/internal/terraform/transform_orphan_resource_test.go index f44f081525..41638166e8 100644 --- a/internal/terraform/transform_orphan_resource_test.go +++ b/internal/terraform/transform_orphan_resource_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_output.go b/internal/terraform/transform_output.go index afc9ab09ec..3bcfa5558f 100644 --- a/internal/terraform/transform_output.go +++ b/internal/terraform/transform_output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,8 +8,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" - "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/moduletest/mocking" ) // OutputTransformer is a GraphTransformer that adds all the outputs @@ -16,16 +18,23 @@ import ( // aren't changing since there is no downside: the state will be available // even if the dependent items aren't changing. type OutputTransformer struct { - Config *configs.Config - Changes *plans.Changes - - // If this is a planned destroy, root outputs are still in the configuration - // so we need to record that we wish to remove them - removeRootOutputs bool + Config *configs.Config // Refresh-only mode means that any failing output preconditions are // reported as warnings rather than errors RefreshOnly bool + + // Planning must be set to true only when we're building a planning graph. + // It must be set to false whenever we're building an apply graph. + Planning bool + + // If this is a planned destroy, root outputs are still in the configuration + // so we need to record that we wish to remove them. + Destroying bool + + // Overrides supplies the values for any output variables that should be + // overridden by the testing framework. + Overrides *mocking.Overrides } func (t *OutputTransformer) Transform(g *Graph) error { @@ -47,49 +56,17 @@ func (t *OutputTransformer) transform(g *Graph, c *configs.Config) error { } } - // Add outputs to the graph, which will be dynamically expanded - // into NodeApplyableOutputs to reflect possible expansion - // through the presence of "count" or "for_each" on the modules. - - var changes []*plans.OutputChangeSrc - if t.Changes != nil { - changes = t.Changes.Outputs - } - for _, o := range c.Module.Outputs { addr := addrs.OutputValue{Name: o.Name} - var rootChange *plans.OutputChangeSrc - for _, c := range changes { - if c.Addr.Module.IsRoot() && c.Addr.OutputValue.Name == o.Name { - rootChange = c - } - } - - destroy := t.removeRootOutputs - if rootChange != nil { - destroy = rootChange.Action == plans.Delete - } - - // If this is a root output and we're destroying, we add the destroy - // node directly, as there is no need to expand. - - var node dag.Vertex - switch { - case c.Path.IsRoot() && destroy: - node = &NodeDestroyableOutput{ - Addr: addr.Absolute(addrs.RootModuleInstance), - Config: o, - } - - default: - node = &nodeExpandOutput{ - Addr: addr, - Module: c.Path, - Config: o, - Destroy: t.removeRootOutputs, - RefreshOnly: t.RefreshOnly, - } + node := &nodeExpandOutput{ + Addr: addr, + Module: c.Path, + Config: o, + Destroying: t.Destroying, + RefreshOnly: t.RefreshOnly, + Planning: t.Planning, + Overrides: t.Overrides, } log.Printf("[TRACE] OutputTransformer: adding %s as %T", o.Name, node) diff --git a/internal/terraform/transform_provider.go b/internal/terraform/transform_provider.go index 2e1f8bd5f3..a34b51ca6f 100644 --- a/internal/terraform/transform_provider.go +++ b/internal/terraform/transform_provider.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -8,11 +11,16 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" ) -func transformProviders(concrete ConcreteProviderNodeFunc, config *configs.Config) GraphTransformer { +func transformProviders(concrete ConcreteProviderNodeFunc, config *configs.Config, externalProviderConfigs map[addrs.RootProviderConfig]providers.Interface) GraphTransformer { return GraphTransformMulti( + // Add placeholder nodes for any externally-configured providers + &externalProviderTransformer{ + ExternalProviderConfigs: externalProviderConfigs, + }, // Add providers from the config &ProviderConfigTransformer{ Config: config, @@ -254,28 +262,48 @@ func (t *CloseProviderTransformer) Transform(g *Graph) error { for _, p := range pm { key := p.ProviderAddr().String() - // get the close provider of this type if we alread created it - closer := cpm[key] - - if closer == nil { - // create a closer for this provider type - closer = &graphNodeCloseProvider{Addr: p.ProviderAddr()} - g.Add(closer) - cpm[key] = closer + // make sure we haven't created the closer node already + _, ok := cpm[key] + if ok { + log.Printf("[ERROR] CloseProviderTransformer: already created close node for %s", key) + continue } + // create a closer for this provider type + closer := &graphNodeCloseProvider{Addr: p.ProviderAddr()} + g.Add(closer) + cpm[key] = closer + // Close node depends on the provider itself // this is added unconditionally, so it will connect to all instances // of the provider. Extra edges will be removed by transitive // reduction. g.Connect(dag.BasicEdge(closer, p)) + } - // connect all the provider's resources to the close node - for _, s := range g.UpEdges(p) { - if _, ok := s.(GraphNodeProviderConsumer); ok { - g.Connect(dag.BasicEdge(closer, s)) - } + // Now look for all provider consumers and connect them to the appropriate closers. + for _, v := range g.Vertices() { + pc, ok := v.(GraphNodeProviderConsumer) + if !ok { + continue } + + p, exact := pc.ProvidedBy() + if p == nil && exact { + // this node does not require a provider + continue + } + + provider, ok := p.(addrs.AbsProviderConfig) + if !ok { + return fmt.Errorf("%s failed to return a provider reference", dag.VertexName(pc)) + } + + closer, ok := cpm[provider.String()] + if !ok { + return fmt.Errorf("no graphNodeCloseProvider for %s", provider) + } + g.Connect(dag.BasicEdge(closer, v)) } return err @@ -491,7 +519,9 @@ func (t *ProviderConfigTransformer) Transform(g *Graph) error { return nil } - t.providers = make(map[string]GraphNodeProvider) + // We'll start with any provider nodes that are already in the graph, + // just so we can avoid creating any duplicates. + t.providers = providerVertexMap(g) t.proxiable = make(map[string]bool) // Start the transformation process @@ -707,7 +737,7 @@ func (t *ProviderConfigTransformer) attachProviderConfigs(g *Graph) error { addr := apn.ProviderAddr() // Get the configuration. - mc := t.Config.Descendent(addr.Module) + mc := t.Config.Descendant(addr.Module) if mc == nil { log.Printf("[TRACE] ProviderConfigTransformer: no configuration available for %s", addr.String()) continue @@ -728,3 +758,38 @@ func (t *ProviderConfigTransformer) attachProviderConfigs(g *Graph) error { return nil } + +// externalProviderTransformer adds placeholder graph nodes for any providers +// that were already instantiated and configured by the external caller. +// +// This should typically run before any other transformers that can add +// nodes representing provider configurations, so that the others can notice +// that a node is already present and therefore skip adding a duplicate. +type externalProviderTransformer struct { + ExternalProviderConfigs map[addrs.RootProviderConfig]providers.Interface +} + +func (t *externalProviderTransformer) Transform(g *Graph) error { + existing := providerVertexMap(g) + + for rootAddr := range t.ExternalProviderConfigs { + absAddr := rootAddr.AbsProviderConfig() + if existing, exists := existing[absAddr.String()]; exists { + // We must not allow a non-external graph node to exist for + // an externally-configured provider, because that would + // cause strange things to happen. We shouldn't get here in + // practice because externalProviderTransformer should be + // the first transformer that introduces graph nodes representing + // provider configurations. + return fmt.Errorf("conflicting %T node for externally-configured provider %s (this is a bug in Terraform)", existing, absAddr) + } + abstract := &NodeAbstractProvider{ + Addr: absAddr, + } + concrete := &nodeExternalProvider{ + NodeAbstractProvider: abstract, + } + g.Add(concrete) + } + return nil +} diff --git a/internal/terraform/transform_provider_test.go b/internal/terraform/transform_provider_test.go index ff21685710..25351593ed 100644 --- a/internal/terraform/transform_provider_test.go +++ b/internal/terraform/transform_provider_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -176,7 +179,7 @@ func TestMissingProviderTransformer_grandchildMissing(t *testing.T) { g := testProviderTransformerGraph(t, mod) { - transform := transformProviders(concrete, mod) + transform := transformProviders(concrete, mod, nil) if err := transform.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -241,7 +244,7 @@ func TestProviderConfigTransformer_parentProviders(t *testing.T) { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod) + tf := transformProviders(concrete, mod, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -261,7 +264,7 @@ func TestProviderConfigTransformer_grandparentProviders(t *testing.T) { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod) + tf := transformProviders(concrete, mod, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -295,7 +298,7 @@ resource "test_object" "a" { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod) + tf := transformProviders(concrete, mod, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } @@ -373,7 +376,7 @@ resource "test_object" "a" { g := testProviderTransformerGraph(t, mod) { - tf := transformProviders(concrete, mod) + tf := transformProviders(concrete, mod, nil) if err := tf.Transform(g); err != nil { t.Fatalf("err: %s", err) } diff --git a/internal/terraform/transform_provisioner.go b/internal/terraform/transform_provisioner.go index 38e3a8ed71..a343c5abe7 100644 --- a/internal/terraform/transform_provisioner.go +++ b/internal/terraform/transform_provisioner.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform // GraphNodeProvisionerConsumer is an interface that nodes that require diff --git a/internal/terraform/transform_reference.go b/internal/terraform/transform_reference.go index 7c45955b86..dbba5855ad 100644 --- a/internal/terraform/transform_reference.go +++ b/internal/terraform/transform_reference.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,10 +9,11 @@ import ( "sort" "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/dag" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" ) // GraphNodeReferenceable must be implemented by any node that represents @@ -38,8 +42,16 @@ type GraphNodeReferencer interface { References() []*addrs.Reference } +// GraphNodeReferencer must be implemented by nodes that import resources. +type GraphNodeImportReferencer interface { + GraphNodeReferencer + + // ImportReferences returns a list of references made by this node's + // associated import block. + ImportReferences() []*addrs.Reference +} + type GraphNodeAttachDependencies interface { - GraphNodeConfigResource AttachDependencies([]addrs.ConfigResource) } @@ -66,12 +78,7 @@ type graphNodeAttachDataResourceDependsOn interface { // AttachDataResourceDependsOn stores the discovered dependencies in the // resource node for evaluation later. - // - // The force parameter indicates that even if there are no dependencies, - // force the data source to act as though there are for refresh purposes. - // This is needed because yet-to-be-created resources won't be in the - // initial refresh graph, but may still be referenced through depends_on. - AttachDataResourceDependsOn(deps []addrs.ConfigResource, force bool) + AttachDataResourceDependsOn(deps []addrs.ConfigResource) } // GraphNodeReferenceOutside is an interface that can optionally be implemented. @@ -136,7 +143,7 @@ func (t *ReferenceTransformer) Transform(g *Graph) error { if !graphNodesAreResourceInstancesInDifferentInstancesOfSameModule(v, parent) { g.Connect(dag.BasicEdge(v, parent)) } else { - log.Printf("[TRACE] ReferenceTransformer: skipping %s => %s inter-module-instance dependency", v, parent) + log.Printf("[TRACE] ReferenceTransformer: skipping %s => %s inter-module-instance dependency", dag.VertexName(v), dag.VertexName(parent)) } } @@ -192,7 +199,7 @@ func (t attachDataResourceDependsOnTransformer) Transform(g *Graph) error { // depMap will only add resource references then dedupe deps := make(depMap) - dependsOnDeps, fromModule := refMap.dependsOn(g, depender) + dependsOnDeps := refMap.dependsOn(g, depender) for _, dep := range dependsOnDeps { // any the dependency deps.add(dep) @@ -204,7 +211,7 @@ func (t attachDataResourceDependsOnTransformer) Transform(g *Graph) error { } log.Printf("[TRACE] attachDataDependenciesTransformer: %s depends on %s", depender.ResourceAddr(), res) - depender.AttachDataResourceDependsOn(res, fromModule) + depender.AttachDataResourceDependsOn(res) } return nil @@ -222,17 +229,34 @@ func (t AttachDependenciesTransformer) Transform(g *Graph) error { if !ok { continue } - selfAddr := attacher.ResourceAddr() - ans, err := g.Ancestors(v) - if err != nil { - return err + // We'll check if the node is a config resource already, in which case + // we want to make sure it is not referencing itself. + + // matchesSelf is a function that returns true if the given address + // matches the address of the node itself. + matchesSelf := func(addrs.ConfigResource) bool { + // Default case is to always return false. + return false + } + + self, ok := v.(GraphNodeConfigResource) + if ok { + matchesSelf = func(addr addrs.ConfigResource) bool { + // If we know the node is a config resource, we can compare + // the addresses directly. + return addr.Equal(self.ResourceAddr()) + } } // dedupe addrs when there's multiple instances involved, or // multiple paths in the un-reduced graph depMap := map[string]addrs.ConfigResource{} - for _, d := range ans { + + // since we need to type-switch over the nodes anyway, we're going to + // insert the address directly into depMap and forget about the returned + // set. + for _, d := range g.Ancestors(v) { var addr addrs.ConfigResource switch d := d.(type) { @@ -245,9 +269,10 @@ func (t AttachDependenciesTransformer) Transform(g *Graph) error { continue } - if addr.Equal(selfAddr) { + if matchesSelf(addr) { continue } + depMap[addr.String()] = addr } @@ -259,7 +284,7 @@ func (t AttachDependenciesTransformer) Transform(g *Graph) error { return deps[i].String() < deps[j].String() }) - log.Printf("[TRACE] AttachDependenciesTransformer: %s depends on %s", attacher.ResourceAddr(), deps) + log.Printf("[TRACE] AttachDependenciesTransformer: %s depends on %s", dag.VertexName(v), deps) attacher.AttachDependencies(deps) } @@ -284,37 +309,23 @@ type ReferenceMap map[string][]dag.Vertex // References returns the set of vertices that the given vertex refers to, // and any referenced addresses that do not have corresponding vertices. func (m ReferenceMap) References(v dag.Vertex) []dag.Vertex { - rn, ok := v.(GraphNodeReferencer) - if !ok { - return nil + var matches []dag.Vertex + var referenceKeys []string + + if rn, ok := v.(GraphNodeReferencer); ok { + for _, ref := range rn.References() { + referenceKeys = append(referenceKeys, m.referenceMapKey(vertexReferencePath(v), ref.Subject)) + } } - var matches []dag.Vertex - - for _, ref := range rn.References() { - subject := ref.Subject - - key := m.referenceMapKey(v, subject) - if _, exists := m[key]; !exists { - // If what we were looking for was a ResourceInstance then we - // might be in a resource-oriented graph rather than an - // instance-oriented graph, and so we'll see if we have the - // resource itself instead. - switch ri := subject.(type) { - case addrs.ResourceInstance: - subject = ri.ContainingResource() - case addrs.ResourceInstancePhase: - subject = ri.ContainingResource() - case addrs.ModuleCallInstanceOutput: - subject = ri.ModuleCallOutput() - case addrs.ModuleCallInstance: - subject = ri.Call - default: - log.Printf("[INFO] ReferenceTransformer: reference not found: %q", subject) - continue - } - key = m.referenceMapKey(v, subject) + if rn, ok := v.(GraphNodeImportReferencer); ok { + for _, ref := range rn.ImportReferences() { + // import block references are always in the root module scope + referenceKeys = append(referenceKeys, m.referenceMapKey(addrs.RootModule, ref.Subject)) } + } + + for _, key := range referenceKeys { vertices := m[key] for _, rv := range vertices { // don't include self-references @@ -324,31 +335,26 @@ func (m ReferenceMap) References(v dag.Vertex) []dag.Vertex { matches = append(matches, rv) } } - return matches } // dependsOn returns the set of vertices that the given vertex refers to from -// the configured depends_on. The bool return value indicates if depends_on was -// found in a parent module configuration. -func (m ReferenceMap) dependsOn(g *Graph, depender graphNodeDependsOn) ([]dag.Vertex, bool) { - var res []dag.Vertex - fromModule := false +// the configured depends_on. This is only used to calculate depends_on for +// data sources. No other resource type changes it's behavior based on how +// dependencies are declared, hence everything else is resolved via the normal +// reference mechanism. +func (m ReferenceMap) dependsOn(g *Graph, depender graphNodeDependsOn) []dag.Vertex { + res := make(dag.Set) refs := depender.DependsOn() // get any implied dependencies for data sources refs = append(refs, m.dataDependsOn(depender)...) - // This is where we record that a module has depends_on configured. - if _, ok := depender.(*nodeExpandModule); ok && len(refs) > 0 { - fromModule = true - } - for _, ref := range refs { subject := ref.Subject - key := m.referenceMapKey(depender, subject) + key := m.referenceMapKey(vertexReferencePath(depender), subject) vertices, ok := m[key] if !ok { // the ReferenceMap generates all possible keys, so any warning @@ -360,30 +366,40 @@ func (m ReferenceMap) dependsOn(g *Graph, depender graphNodeDependsOn) ([]dag.Ve if rv == depender { continue } - res = append(res, rv) + res.Add(rv) - // Check any ancestors for transitive dependencies when we're - // not pointed directly at a resource. We can't be much more - // precise here, since in order to maintain our guarantee that data - // sources will wait for explicit dependencies, if those dependencies - // happen to be a module, output, or variable, we have to find some - // upstream managed resource in order to check for a planned - // change. + // Check any ancestors for transitive dependencies when we're not + // pointed directly at a resource. We can't be much more precise + // here, since in order to maintain our guarantee that data sources + // will wait for explicit dependencies, if those dependencies happen + // to be a module, output, or variable, we have to find some + // upstream managed resource in order to check for a planned change. + // We need to descend through all ancestors here, because data + // sources aren't just tracking this for graph edges, but rather + // they need to look for changes during the plan. if _, ok := rv.(GraphNodeConfigResource); !ok { - ans, _ := g.Ancestors(rv) - for _, v := range ans { + for _, v := range g.Ancestors(rv) { if isDependableResource(v) { - res = append(res, v) + res.Add(v) } } } } } - parentDeps, fromParentModule := m.parentModuleDependsOn(g, depender) - res = append(res, parentDeps...) + parentDeps := m.parentModuleDependsOn(g, depender) + // dag.Set doesn't have an insert/union method, but they are simple maps + for k, v := range parentDeps { + res[k] = v + } - return res, fromModule || fromParentModule + // Now we need to convert the set back to our slice type, because Set.List() + // returns []any. + vertices := make([]dag.Vertex, 0, res.Len()) + for _, v := range res { + vertices = append(vertices, v) + } + return vertices } // Return extra depends_on references if this is a data source. @@ -421,11 +437,9 @@ func (m ReferenceMap) dataDependsOn(depender graphNodeDependsOn) []*addrs.Refere } // parentModuleDependsOn returns the set of vertices that a data sources parent -// module references through the module call's depends_on. The bool return -// value indicates if depends_on was found in a parent module configuration. -func (m ReferenceMap) parentModuleDependsOn(g *Graph, depender graphNodeDependsOn) ([]dag.Vertex, bool) { - var res []dag.Vertex - fromModule := false +// module references through the module call's depends_on. +func (m ReferenceMap) parentModuleDependsOn(g *Graph, depender graphNodeDependsOn) dag.Set { + res := make(dag.Set) // Look for containing modules with DependsOn. // This should be connected directly to the module node, so we only need to @@ -437,23 +451,24 @@ func (m ReferenceMap) parentModuleDependsOn(g *Graph, depender graphNodeDependsO continue } - deps, fromParentModule := m.dependsOn(g, mod) + deps := m.dependsOn(g, mod) for _, dep := range deps { - // add the dependency - res = append(res, dep) - - // and check any transitive resource dependencies for more resources - ans, _ := g.Ancestors(dep) - for _, v := range ans { - if isDependableResource(v) { - res = append(res, v) - } + if isDependableResource(dep) { + res.Add(dep) + } + } + + // We need to descend through all ancestors here, because data sources + // aren't just tracking this for graph edges, but rather they need to + // look for changes during the plan. + for _, v := range g.Ancestors(deps...) { + if isDependableResource(v) { + res.Add(v) } } - fromModule = fromModule || fromParentModule } - return res, fromModule + return res } func (m *ReferenceMap) mapKey(path addrs.Module, addr addrs.Referenceable) string { @@ -517,9 +532,59 @@ func vertexReferencePath(v dag.Vertex) addrs.Module { // // Only GraphNodeModulePath implementations can be referrers, so this method will // panic if the given vertex does not implement that interface. -func (m *ReferenceMap) referenceMapKey(referrer dag.Vertex, addr addrs.Referenceable) string { - path := vertexReferencePath(referrer) - return m.mapKey(path, addr) +func (m ReferenceMap) referenceMapKey(path addrs.Module, addr addrs.Referenceable) string { + key := m.mapKey(path, addr) + if _, exists := m[key]; !exists { + // If what we were looking for was a ResourceInstance then we + // might be in a resource-oriented graph rather than an + // instance-oriented graph, and so we'll see if we have the + // resource itself instead. + + if ri, ok := addr.(addrs.ResourceInstance); ok { + return m.mapKey(path, ri.ContainingResource()) + } + + if rip, ok := addr.(addrs.ResourceInstancePhase); ok { + return m.mapKey(path, rip.ContainingResource()) + } + + if mcio, ok := addr.(addrs.ModuleCallInstanceOutput); ok { + + // A module call instance output is a reference to an output of a + // specific module call. If we can't find that, we'll look first + // for the general non-instanced output. + + key = m.mapKey(path, mcio.ModuleCallOutput()) + if _, exists := m[key]; exists { + // We found it, so we can just use that. + return key + } + + // Otherwise we'll look just for the instanced module call itself. + + key = m.mapKey(path, mcio.Call) + if _, exists := m[key]; exists { + // We found it, so we can just use that. + return key + } + + // If we still can't find it, then we'll look for the non-instanced + // module call. This is the same as we'd do if the original call had + // just been for a ModuleCallInstance, so we'll let that fall + // through. + + addr = mcio.Call + + } + + if mci, ok := addr.(addrs.ModuleCallInstance); ok { + return m.mapKey(path, mci.Call) + } + + // If nothing matched, then we'll just return the original key + // unchanged. + } + return key } // NewReferenceMap is used to create a new reference map for the @@ -552,6 +617,6 @@ func ReferencesFromConfig(body hcl.Body, schema *configschema.Block) []*addrs.Re if body == nil { return nil } - refs, _ := lang.ReferencesInBlock(body, schema) + refs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, body, schema) return refs } diff --git a/internal/terraform/transform_reference_test.go b/internal/terraform/transform_reference_test.go index 50e47e19b8..df1cd00725 100644 --- a/internal/terraform/transform_reference_test.go +++ b/internal/terraform/transform_reference_test.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "fmt" "reflect" "sort" "strings" @@ -8,6 +12,10 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/plans" + "github.com/hashicorp/terraform/internal/providers" + "github.com/hashicorp/terraform/internal/states" + "github.com/hashicorp/terraform/internal/tfdiags" ) func TestReferenceTransformer_simple(t *testing.T) { @@ -317,3 +325,77 @@ child.A child.B child.A ` + +// attachDataResourceDependsOnTransformer makes sure data resources with +// `depends_on` wait for all dependencies of `depends_on` arguments, and +// everything referenced by any parent module's depends_on arguments. +func TestAttachDataResourceDependsOnTransformer(t *testing.T) { + cfg := testModuleInline(t, map[string]string{ + "main.tf": ` +module "moda" { + source = "./moda" + depends_on = [module.modb] +} + +module "modb" { + source = "./modb" + in = test_resource.root.id +} + +resource "test_resource" "root" { +} +`, + "./moda/main.tf": ` +data "test_data_source" "in_moda" { +}`, + + "./modb/main.tf": ` +variable "in" { +} + +resource "test_resource" "in_modb" { +} + +module "modc" { + source = "../modc" + in = var.in +} +`, + "./modc/main.tf": ` +variable "in" { +} + +resource "test_resource" "in_modc" { + value = var.in +}`, + }) + + p := testProvider("test") + ctx := testContext2(t, &ContextOpts{ + Providers: map[addrs.Provider]providers.Factory{ + addrs.NewDefaultProvider("test"): testProviderFuncFixed(p), + }, + }) + + g, _, diags := ctx.planGraph(cfg, states.NewState(), &PlanOpts{Mode: plans.NormalMode}) + tfdiags.AssertNoErrors(t, diags) + + // find the data resource node + for _, v := range g.Vertices() { + data, ok := v.(*nodeExpandPlannableResource) + if !ok || data.Addr.Resource.Mode != addrs.DataResourceMode { + continue + } + + sort.Slice(data.dependsOn, func(i, j int) bool { + return data.dependsOn[i].String() < data.dependsOn[j].String() + }) + + expected := `["module.modb.module.modc.test_resource.in_modc" "module.modb.test_resource.in_modb" "test_resource.root"]` + got := fmt.Sprintf("%q", data.dependsOn) + if got != expected { + t.Fatalf("expected dependsOn: %s\ngot: %s", expected, got) + } + } + +} diff --git a/internal/terraform/transform_removed_modules.go b/internal/terraform/transform_removed_modules.go index 090582ce20..e19b0f8648 100644 --- a/internal/terraform/transform_removed_modules.go +++ b/internal/terraform/transform_removed_modules.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -24,7 +27,7 @@ func (t *RemovedModuleTransformer) Transform(g *Graph) error { removed := map[string]addrs.Module{} for _, m := range t.State.Modules { - cc := t.Config.DescendentForInstance(m.Addr) + cc := t.Config.DescendantForInstance(m.Addr) if cc != nil { continue } diff --git a/internal/terraform/transform_resource_count.go b/internal/terraform/transform_resource_count.go index 4d853593eb..1b9706d0a9 100644 --- a/internal/terraform/transform_resource_count.go +++ b/internal/terraform/transform_resource_count.go @@ -1,11 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( "log" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/dag" + "github.com/hashicorp/terraform/internal/providers" ) // ResourceCountTransformer is a GraphTransformer that expands the count @@ -14,7 +17,7 @@ import ( // This assumes that the count is already interpolated. type ResourceCountTransformer struct { Concrete ConcreteResourceInstanceNodeFunc - Schema *configschema.Block + Schema *providers.Schema Addr addrs.ConfigResource InstanceAddrs []addrs.AbsResourceInstance diff --git a/internal/terraform/transform_root.go b/internal/terraform/transform_root.go index 27804ff024..56ad00422b 100644 --- a/internal/terraform/transform_root.go +++ b/internal/terraform/transform_root.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -10,31 +13,48 @@ const rootNodeName = "root" type RootTransformer struct{} func (t *RootTransformer) Transform(g *Graph) error { - // If we already have a good root, we're done - if _, err := g.Root(); err == nil { - return nil - } + addRootNodeToGraph(g) + return nil +} - // Add a root - var root graphNodeRoot - g.Add(root) +// addRootNodeToGraph modifies the given graph in-place so that it has a root +// node if it didn't already have one and so that any other node which doesn't +// already depend on something will depend on that root node. +// +// After this function returns, the graph will have only one node that doesn't +// depend on any other nodes. +func addRootNodeToGraph(g *Graph) { + // We always add the root node. This is a singleton so if it's already + // in the graph this will do nothing and just retain the existing root node. + // + // Note that rootNode is intentionally added by value and not by pointer + // so that all root nodes will be equal to one another and therefore + // coalesce when two valid graphs get merged together into a single graph. + g.Add(rootNode) - // Connect the root to all the edges that need it + // Everything that doesn't already depend on at least one other node will + // depend on the root node, except the root node itself. for _, v := range g.Vertices() { - if v == root { + if v == dag.Vertex(rootNode) { continue } if g.UpEdges(v).Len() == 0 { - g.Connect(dag.BasicEdge(root, v)) + g.Connect(dag.BasicEdge(rootNode, v)) } } - - return nil } type graphNodeRoot struct{} +// rootNode is the singleton value representing all root graph nodes. +// +// The root node for all graphs should be this value directly, and in particular +// _not_ a pointer to this value. Using the value directly here means that +// multiple root nodes will always coalesce together when subsuming one graph +// into another. +var rootNode graphNodeRoot + func (n graphNodeRoot) Name() string { return rootNodeName } diff --git a/internal/terraform/transform_root_test.go b/internal/terraform/transform_root_test.go index 4a426b5e7c..ba27d97b20 100644 --- a/internal/terraform/transform_root_test.go +++ b/internal/terraform/transform_root_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -8,50 +11,78 @@ import ( ) func TestRootTransformer(t *testing.T) { - mod := testModule(t, "transform-root-basic") + t.Run("many nodes", func(t *testing.T) { + mod := testModule(t, "transform-root-basic") - g := Graph{Path: addrs.RootModuleInstance} - { - tf := &ConfigTransformer{Config: mod} - if err := tf.Transform(&g); err != nil { + g := Graph{Path: addrs.RootModuleInstance} + { + tf := &ConfigTransformer{Config: mod} + if err := tf.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &MissingProviderTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &ProviderTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + { + transform := &RootTransformer{} + if err := transform.Transform(&g); err != nil { + t.Fatalf("err: %s", err) + } + } + + actual := strings.TrimSpace(g.String()) + expected := strings.TrimSpace(testTransformRootBasicStr) + if actual != expected { + t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) + } + + root, err := g.Root() + if err != nil { t.Fatalf("err: %s", err) } - } - - { - transform := &MissingProviderTransformer{} - if err := transform.Transform(&g); err != nil { - t.Fatalf("err: %s", err) + if _, ok := root.(graphNodeRoot); !ok { + t.Fatalf("bad: %#v", root) } - } + }) - { - transform := &ProviderTransformer{} - if err := transform.Transform(&g); err != nil { - t.Fatalf("err: %s", err) + t.Run("only one initial node", func(t *testing.T) { + g := Graph{Path: addrs.RootModuleInstance} + g.Add("foo") + addRootNodeToGraph(&g) + got := strings.TrimSpace(g.String()) + want := strings.TrimSpace(` +foo +root + foo +`) + if got != want { + t.Errorf("wrong final graph\ngot:\n%s\nwant:\n%s", got, want) } - } + }) - { - transform := &RootTransformer{} - if err := transform.Transform(&g); err != nil { - t.Fatalf("err: %s", err) + t.Run("graph initially empty", func(t *testing.T) { + g := Graph{Path: addrs.RootModuleInstance} + addRootNodeToGraph(&g) + got := strings.TrimSpace(g.String()) + want := `root` + if got != want { + t.Errorf("wrong final graph\ngot:\n%s\nwant:\n%s", got, want) } - } + }) - actual := strings.TrimSpace(g.String()) - expected := strings.TrimSpace(testTransformRootBasicStr) - if actual != expected { - t.Fatalf("wrong result\n\ngot:\n%s\n\nwant:\n%s", actual, expected) - } - - root, err := g.Root() - if err != nil { - t.Fatalf("err: %s", err) - } - if _, ok := root.(graphNodeRoot); !ok { - t.Fatalf("bad: %#v", root) - } } const testTransformRootBasicStr = ` diff --git a/internal/terraform/transform_state.go b/internal/terraform/transform_state.go index 1ca060a88a..47340e192a 100644 --- a/internal/terraform/transform_state.go +++ b/internal/terraform/transform_state.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_targets.go b/internal/terraform/transform_targets.go index e603bcedb4..ca71b82b67 100644 --- a/internal/terraform/transform_targets.go +++ b/internal/terraform/transform_targets.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -61,8 +64,7 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targeta tn.SetTargets(addrs) } - deps, _ := g.Ancestors(v) - for _, d := range deps { + for _, d := range g.Ancestors(v) { targetedNodes.Add(d) } } @@ -88,7 +90,7 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targeta // If this output is descended only from targeted resources, then we // will keep it - deps, _ := g.Ancestors(v) + deps := g.Ancestors(v) found := 0 for _, d := range deps { switch d.(type) { @@ -123,13 +125,30 @@ func (t *TargetsTransformer) selectTargetedNodes(g *Graph, addrs []addrs.Targeta func (t *TargetsTransformer) nodeIsTarget(v dag.Vertex, targets []addrs.Targetable) bool { var vertexAddr addrs.Targetable switch r := v.(type) { + case *nodeApplyableDeferredPartialInstance: + + // Partial instances are not targeted directly, but they might be + // targeted after they have been expanded so we need to perform a custom + // check for them here. + // + // The other types of nodes can be targeted directly, and are handled + // together. + + for _, targetAddr := range targets { + if r.PartialAddr.IsTargetedBy(targetAddr) { + return true + } + } + return false + case GraphNodeResourceInstance: vertexAddr = r.ResourceInstanceAddr() case GraphNodeConfigResource: vertexAddr = r.ResourceAddr() default: - // Only resource and resource instance nodes can be targeted. + // Only partial nodes and resource and resource instance nodes can be + // targeted. return false } diff --git a/internal/terraform/transform_targets_test.go b/internal/terraform/transform_targets_test.go index 0ed95c0809..5edaabd66f 100644 --- a/internal/terraform/transform_targets_test.go +++ b/internal/terraform/transform_targets_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_transitive_reduction.go b/internal/terraform/transform_transitive_reduction.go index 0bb6cb3773..bf9be4b166 100644 --- a/internal/terraform/transform_transitive_reduction.go +++ b/internal/terraform/transform_transitive_reduction.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform // TransitiveReductionTransformer is a GraphTransformer that diff --git a/internal/terraform/transform_transitive_reduction_test.go b/internal/terraform/transform_transitive_reduction_test.go index 1339d071fe..0ac6eafb6d 100644 --- a/internal/terraform/transform_transitive_reduction_test.go +++ b/internal/terraform/transform_transitive_reduction_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,6 +9,7 @@ import ( "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" "github.com/zclconf/go-cty/cty" ) @@ -30,18 +34,20 @@ func TestTransitiveReductionTransformer(t *testing.T) { { transform := &AttachSchemaTransformer{ - Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]*ProviderSchema{ + Plugins: schemaOnlyProvidersForTesting(map[addrs.Provider]providers.ProviderSchema{ addrs.NewDefaultProvider("aws"): { - ResourceTypes: map[string]*configschema.Block{ + ResourceTypes: map[string]providers.Schema{ "aws_instance": { - Attributes: map[string]*configschema.Attribute{ - "A": { - Type: cty.String, - Optional: true, - }, - "B": { - Type: cty.String, - Optional: true, + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "A": { + Type: cty.String, + Optional: true, + }, + "B": { + Type: cty.String, + Optional: true, + }, }, }, }, diff --git a/internal/terraform/transform_variable.go b/internal/terraform/transform_variable.go index 4262ea3d6d..2d9365d5fe 100644 --- a/internal/terraform/transform_variable.go +++ b/internal/terraform/transform_variable.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -15,6 +18,14 @@ type RootVariableTransformer struct { Config *configs.Config RawValues InputValues + + // Planning must be set to true when building a planning graph, and must be + // false when building an apply graph. + Planning bool + + // DestroyApply must be set to true when applying a destroy operation and + // false otherwise. + DestroyApply bool } func (t *RootVariableTransformer) Transform(g *Graph) error { @@ -33,8 +44,10 @@ func (t *RootVariableTransformer) Transform(g *Graph) error { Addr: addrs.InputVariable{ Name: v.Name, }, - Config: v, - RawValue: t.RawValues[v.Name], + Config: v, + RawValue: t.RawValues[v.Name], + Planning: t.Planning, + DestroyApply: t.DestroyApply, } g.Add(node) } diff --git a/internal/terraform/transform_variable_validation.go b/internal/terraform/transform_variable_validation.go new file mode 100644 index 0000000000..f2b81a2944 --- /dev/null +++ b/internal/terraform/transform_variable_validation.go @@ -0,0 +1,86 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "log" + + "github.com/hashicorp/hcl/v2" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" + "github.com/hashicorp/terraform/internal/dag" +) + +// graphNodeValidatableVariable is implemented by nodes that represent +// input variables, and which must therefore have variable validation +// nodes created alongside them to verify that the final value matches +// the author's validation rules. +type graphNodeValidatableVariable interface { + // variableValidationRules returns the information required to validate + // the final value produced by the implementing node. + // + // configAddr is the address of the static declaration of the variable + // that is to be validated. + // + // rules is the set of validation rules to use to check the final value + // of the variable. + // + // defnRange is the source range to "blame" for any problems. This + // should ideally cover the source code of the expression that was evaluated + // to produce the variable's value, but if there is no such expression -- + // for example, if the value came from an environment variable -- then + // the location of the variable declaration is a plausible substitute. + variableValidationRules() (configAddr addrs.ConfigInputVariable, rules []*configs.CheckRule, defnRange hcl.Range) +} + +// Correct behavior requires both of the input variable node types to +// announce themselves as producing final input variable values that need +// to be validated. +var _ graphNodeValidatableVariable = (*NodeRootVariable)(nil) +var _ graphNodeValidatableVariable = (*nodeExpandModuleVariable)(nil) + +// variableValidationTransformer searches the given graph for any nodes +// that implement [graphNodeValidatableVariable]. For each one found, it +// inserts a new [nodeVariableValidation] and makes it depend on the original +// node, to cause the validation action to happen only after the variable's +// final value has been registered. +// +// This transformer should run after any transformer that might insert a +// node that implements [graphNodeValidatableVariable], and before the +// [ReferenceTransformer] because references like "var.foo" must be connected +// with the new [nodeVariableValidation] nodes to prevent downstream nodes +// from relying on unvalidated values. +type variableValidationTransformer struct { + validateWalk bool +} + +var _ GraphTransformer = (*variableValidationTransformer)(nil) + +func (t *variableValidationTransformer) Transform(g *Graph) error { + log.Printf("[TRACE] variableValidationTransformer: adding validation nodes for any existing variable evaluation nodes") + for _, v := range g.Vertices() { + v, ok := v.(graphNodeValidatableVariable) + if !ok { + continue // irrelevant node + } + + configAddr, rules, defnRange := v.variableValidationRules() + newV := &nodeVariableValidation{ + configAddr: configAddr, + rules: rules, + defnRange: defnRange, + validateWalk: t.validateWalk, + } + + if len(rules) != 0 { + log.Printf("[TRACE] variableValidationTransformer: %s has %d validation rule(s)", configAddr, len(rules)) + g.Add(newV) + g.Connect(dag.BasicEdge(newV, v)) + } else { + log.Printf("[TRACE] variableValidationTransformer: %s has no validation rules", configAddr) + } + } + return nil +} diff --git a/internal/terraform/transform_variable_validation_test.go b/internal/terraform/transform_variable_validation_test.go new file mode 100644 index 0000000000..bc9881a970 --- /dev/null +++ b/internal/terraform/transform_variable_validation_test.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package terraform + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hcltest" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" + "github.com/hashicorp/terraform/internal/configs" +) + +func TestVariableValidationTransformer(t *testing.T) { + // This is a unit test focused on just the validation transformer's + // behavior, which assumes that the caller correctly arranges for + // its invariants to be met: + // 1. all input variable evaluation nodes must already be present + // in the graph before running the transformer. + // 2. the reference transformer must run after this transformer. + // + // To avoid this depending on transformers other than the one we're + // testing we'll arrange for those to be met in a rather artificial + // way. Our integration tests complement this by verifying that + // the variable validation feature as a whole is working. For + // example: [TestContext2Plan_variableValidation]. + + g := &Graph{} + fooNode := &nodeTestOnlyInputVariable{ + configAddr: addrs.InputVariable{Name: "foo"}.InModule(addrs.RootModule), + rules: []*configs.CheckRule{ + { + // The condition contains a self-reference, which is required + // for a realistic input variable validation because otherwise + // it wouldn't actually be checking the variable it's + // supposed to be validating. (This transformer is not the + // one responsible for validating that though, so it's + // okay for the examples below to not meet that requirement.) + Condition: hcltest.MockExprTraversalSrc("var.foo"), + ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("wrong")), + }, + }, + } + barNode := &nodeTestOnlyInputVariable{ + configAddr: addrs.InputVariable{Name: "bar"}.InModule(addrs.RootModule), + rules: []*configs.CheckRule{ + { + // The condition of this one refers to var.foo + Condition: hcltest.MockExprTraversalSrc("var.foo"), + ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("wrong")), + }, + }, + } + bazNode := &nodeTestOnlyInputVariable{ + configAddr: addrs.InputVariable{Name: "baz"}.InModule(addrs.RootModule), + rules: []*configs.CheckRule{ + { + // The error message of this one refers to var.foo + Condition: hcltest.MockExprLiteral(cty.False), + ErrorMessage: hcltest.MockExprTraversalSrc("var.foo"), + }, + }, + } + g.Add(fooNode) + g.Add(barNode) + g.Add(bazNode) + + transformer := &variableValidationTransformer{} + err := transformer.Transform(g) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + gotStr := strings.TrimSpace(g.String()) + wantStr := strings.TrimSpace(` +var.bar (test fake) +var.bar (validation) + var.bar (test fake) +var.baz (test fake) +var.baz (validation) + var.baz (test fake) +var.foo (test fake) +var.foo (validation) + var.foo (test fake) +`) + if diff := cmp.Diff(wantStr, gotStr); diff != "" { + t.Errorf("wrong graph after transform\n%s", diff) + } + + // This transformer is not responsible for wiring up dependencies based + // on references -- that's ReferenceTransformer's job -- but we'll + // verify that the nodes that were added by this transformer do at least + // report the references we expect them to report, in the way that + // ReferenceTransformer would expect. + gotRefs := map[string]map[string]struct{}{} + for _, v := range g.Vertices() { + v, ok := v.(*nodeVariableValidation) // the type of all nodes that this transformer adds + if !ok { + continue + } + var _ GraphNodeReferencer = v // static assertion just to make sure we'll fail to compile if GraphNodeReferencer changes later + + refs := v.References() + gotRefs[v.Name()] = map[string]struct{}{} + for _, ref := range refs { + gotRefs[v.Name()][ref.Subject.String()] = struct{}{} + } + } + wantRefs := map[string]map[string]struct{}{ + "var.bar (validation)": { + "var.foo": struct{}{}, + }, + "var.baz (validation)": { + "var.foo": struct{}{}, + }, + "var.foo (validation)": {}, + } + if diff := cmp.Diff(wantRefs, gotRefs); diff != "" { + t.Errorf("wrong references for the added nodes\n%s", diff) + } +} + +type nodeTestOnlyInputVariable struct { + configAddr addrs.ConfigInputVariable + rules []*configs.CheckRule +} + +var _ graphNodeValidatableVariable = (*nodeTestOnlyInputVariable)(nil) + +func (n *nodeTestOnlyInputVariable) Name() string { + return fmt.Sprintf("%s (test fake)", n.configAddr) +} + +// variableValidationRules implements [graphNodeValidatableVariable]. +func (n *nodeTestOnlyInputVariable) variableValidationRules() (addrs.ConfigInputVariable, []*configs.CheckRule, hcl.Range) { + return n.configAddr, n.rules, hcl.Range{ + Filename: "test", + Start: hcl.InitialPos, + End: hcl.InitialPos, + } +} diff --git a/internal/terraform/transform_vertex.go b/internal/terraform/transform_vertex.go index 6dd2f98dce..f223efb1d6 100644 --- a/internal/terraform/transform_vertex.go +++ b/internal/terraform/transform_vertex.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/transform_vertex_test.go b/internal/terraform/transform_vertex_test.go index 21d5d914a7..9bd875be39 100644 --- a/internal/terraform/transform_vertex_test.go +++ b/internal/terraform/transform_vertex_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/ui_input.go b/internal/terraform/ui_input.go index 688bcf71e4..1d780a2d06 100644 --- a/internal/terraform/ui_input.go +++ b/internal/terraform/ui_input.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "context" diff --git a/internal/terraform/ui_input_mock.go b/internal/terraform/ui_input_mock.go index e2d9c38481..9476e0038f 100644 --- a/internal/terraform/ui_input_mock.go +++ b/internal/terraform/ui_input_mock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "context" diff --git a/internal/terraform/ui_input_prefix.go b/internal/terraform/ui_input_prefix.go index b5d32b1e85..a911eb7966 100644 --- a/internal/terraform/ui_input_prefix.go +++ b/internal/terraform/ui_input_prefix.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/ui_input_prefix_test.go b/internal/terraform/ui_input_prefix_test.go index dff42c39c5..0adc688e1b 100644 --- a/internal/terraform/ui_input_prefix_test.go +++ b/internal/terraform/ui_input_prefix_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/ui_output.go b/internal/terraform/ui_output.go index 84427c63de..c68ad6a1f4 100644 --- a/internal/terraform/ui_output.go +++ b/internal/terraform/ui_output.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform // UIOutput is the interface that must be implemented to output diff --git a/internal/terraform/ui_output_callback.go b/internal/terraform/ui_output_callback.go index 135a91c5f0..f122aa00d8 100644 --- a/internal/terraform/ui_output_callback.go +++ b/internal/terraform/ui_output_callback.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform type CallbackUIOutput struct { diff --git a/internal/terraform/ui_output_callback_test.go b/internal/terraform/ui_output_callback_test.go index 1dd5ccddf9..6886702445 100644 --- a/internal/terraform/ui_output_callback_test.go +++ b/internal/terraform/ui_output_callback_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/ui_output_mock.go b/internal/terraform/ui_output_mock.go index d828c921ca..998439bf81 100644 --- a/internal/terraform/ui_output_mock.go +++ b/internal/terraform/ui_output_mock.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import "sync" diff --git a/internal/terraform/ui_output_mock_test.go b/internal/terraform/ui_output_mock_test.go index 0a23c2e234..8306770293 100644 --- a/internal/terraform/ui_output_mock_test.go +++ b/internal/terraform/ui_output_mock_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/ui_output_provisioner.go b/internal/terraform/ui_output_provisioner.go deleted file mode 100644 index 22e5670cbd..0000000000 --- a/internal/terraform/ui_output_provisioner.go +++ /dev/null @@ -1,19 +0,0 @@ -package terraform - -import ( - "github.com/hashicorp/terraform/internal/addrs" -) - -// ProvisionerUIOutput is an implementation of UIOutput that calls a hook -// for the output so that the hooks can handle it. -type ProvisionerUIOutput struct { - InstanceAddr addrs.AbsResourceInstance - ProvisionerType string - Hooks []Hook -} - -func (o *ProvisionerUIOutput) Output(msg string) { - for _, h := range o.Hooks { - h.ProvisionOutput(o.InstanceAddr, o.ProvisionerType, msg) - } -} diff --git a/internal/terraform/ui_output_provisioner_test.go b/internal/terraform/ui_output_provisioner_test.go deleted file mode 100644 index baadd31817..0000000000 --- a/internal/terraform/ui_output_provisioner_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package terraform - -import ( - "testing" - - "github.com/hashicorp/terraform/internal/addrs" -) - -func TestProvisionerUIOutput_impl(t *testing.T) { - var _ UIOutput = new(ProvisionerUIOutput) -} - -func TestProvisionerUIOutputOutput(t *testing.T) { - hook := new(MockHook) - output := &ProvisionerUIOutput{ - InstanceAddr: addrs.Resource{ - Mode: addrs.ManagedResourceMode, - Type: "test_thing", - Name: "test", - }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), - ProvisionerType: "foo", - Hooks: []Hook{hook}, - } - - output.Output("bar") - - if !hook.ProvisionOutputCalled { - t.Fatal("hook.ProvisionOutput was not called, and should've been") - } - if got, want := hook.ProvisionOutputProvisionerType, "foo"; got != want { - t.Fatalf("wrong provisioner type\ngot: %q\nwant: %q", got, want) - } - if got, want := hook.ProvisionOutputMessage, "bar"; got != want { - t.Fatalf("wrong output message\ngot: %q\nwant: %q", got, want) - } -} diff --git a/internal/terraform/update_state_hook.go b/internal/terraform/update_state_hook.go index c2ed76e8ec..35dc4d82cd 100644 --- a/internal/terraform/update_state_hook.go +++ b/internal/terraform/update_state_hook.go @@ -1,14 +1,14 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform // updateStateHook calls the PostStateUpdate hook with the current state. func updateStateHook(ctx EvalContext) error { - // In principle we could grab the lock here just long enough to take a - // deep copy and then pass that to our hooks below, but we'll instead - // hold the hook for the duration to avoid the potential confusing - // situation of us racing to call PostStateUpdate concurrently with - // different state snapshots. + // PostStateUpdate requires that the state be locked and safe to read for + // the duration of the call. stateSync := ctx.State() - state := stateSync.Lock().DeepCopy() + state := stateSync.Lock() defer stateSync.Unlock() // Call the hook diff --git a/internal/terraform/update_state_hook_test.go b/internal/terraform/update_state_hook_test.go index ac3e33f55d..307aa23f22 100644 --- a/internal/terraform/update_state_hook_test.go +++ b/internal/terraform/update_state_hook_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -14,7 +17,10 @@ func TestUpdateStateHook(t *testing.T) { mockHook := new(MockHook) state := states.NewState() - state.Module(addrs.RootModuleInstance).SetLocalValue("foo", cty.StringVal("hello")) + state.SetOutputValue( + addrs.OutputValue{Name: "foo"}.Absolute(addrs.RootModuleInstance), + cty.StringVal("hello"), false, + ) ctx := new(MockEvalContext) ctx.HookHook = mockHook @@ -27,7 +33,7 @@ func TestUpdateStateHook(t *testing.T) { if !mockHook.PostStateUpdateCalled { t.Fatal("should call PostStateUpdate") } - if mockHook.PostStateUpdateState.LocalValue(addrs.LocalValue{Name: "foo"}.Absolute(addrs.RootModuleInstance)) != cty.StringVal("hello") { + if mockHook.PostStateUpdateState.RootOutputValues["foo"].Value != cty.StringVal("hello") { t.Fatalf("wrong state passed to hook: %s", spew.Sdump(mockHook.PostStateUpdateState)) } } diff --git a/internal/terraform/upgrade_resource_state.go b/internal/terraform/upgrade_resource_state.go index 906898e281..5529fc5574 100644 --- a/internal/terraform/upgrade_resource_state.go +++ b/internal/terraform/upgrade_resource_state.go @@ -1,12 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( + "bytes" "encoding/json" "fmt" "log" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/lang/ephemeral" "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/states" "github.com/hashicorp/terraform/internal/tfdiags" @@ -20,7 +24,7 @@ import ( // // If any errors occur during upgrade, error diagnostics are returned. In that // case it is not safe to proceed with using the original state object. -func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema *configschema.Block, currentVersion uint64) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) { +func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema providers.Schema) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) { if addr.Resource.Resource.Mode != addrs.ManagedResourceMode { // We only do state upgrading for managed resources. // This was a part of the normal workflow in older versions and @@ -36,16 +40,16 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int // Legacy flatmap state is already taken care of during conversion. // If the schema version is be changed, then allow the provider to handle // removed attributes. - if len(src.AttrsJSON) > 0 && src.SchemaVersion == currentVersion { - src.AttrsJSON = stripRemovedStateAttributes(src.AttrsJSON, currentSchema.ImpliedType()) + if len(src.AttrsJSON) > 0 && src.SchemaVersion == uint64(currentSchema.Version) { + src.AttrsJSON = stripRemovedStateAttributes(src.AttrsJSON, currentSchema.Body.ImpliedType()) } stateIsFlatmap := len(src.AttrsJSON) == 0 // TODO: This should eventually use a proper FQN. providerType := addr.Resource.Resource.ImpliedProvider() - if src.SchemaVersion > currentVersion { - log.Printf("[TRACE] upgradeResourceState: can't downgrade state for %s from version %d to %d", addr, src.SchemaVersion, currentVersion) + if src.SchemaVersion > uint64(currentSchema.Version) { + log.Printf("[TRACE] upgradeResourceState: can't downgrade state for %s from version %d to %d", addr, src.SchemaVersion, currentSchema.Version) var diags tfdiags.Diagnostics diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -64,10 +68,10 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int // v0.12, this also includes translating from legacy flatmap to new-style // representation, since only the provider has enough information to // understand a flatmap built against an older schema. - if src.SchemaVersion != currentVersion { - log.Printf("[TRACE] upgradeResourceState: upgrading state for %s from version %d to %d using provider %q", addr, src.SchemaVersion, currentVersion, providerType) + if src.SchemaVersion != uint64(currentSchema.Version) { + log.Printf("[TRACE] upgradeResourceState: upgrading state for %s from version %d to %d using provider %q", addr, src.SchemaVersion, currentSchema.Version, providerType) } else { - log.Printf("[TRACE] upgradeResourceState: schema version of %s is still %d; calling provider %q for any other minor fixups", addr, currentVersion, providerType) + log.Printf("[TRACE] upgradeResourceState: schema version of %s is still %d; calling provider %q for any other minor fixups", addr, currentSchema.Version, providerType) } req := providers.UpgradeResourceStateRequest{ @@ -93,12 +97,20 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int return nil, diags } + if !resp.UpgradedState.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid resource state upgrade", + fmt.Sprintf("The %s provider upgraded the state for %s from a previous version, but produced an invalid result: The returned state contains unknown values.", providerType, addr), + )) + } + // After upgrading, the new value must conform to the current schema. When // going over RPC this is actually already ensured by the // marshaling/unmarshaling of the new value, but we'll check it here // anyway for robustness, e.g. for in-process providers. newValue := resp.UpgradedState - if errs := newValue.Type().TestConformance(currentSchema.ImpliedType()); len(errs) > 0 { + if errs := newValue.Type().TestConformance(currentSchema.Body.ImpliedType()); len(errs) > 0 { for _, err := range errs { diags = diags.Append(tfdiags.Sourceless( tfdiags.Error, @@ -109,7 +121,25 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int return nil, diags } - new, err := src.CompleteUpgrade(newValue, currentSchema.ImpliedType(), uint64(currentVersion)) + // Check for any write-only attributes that have non-null values + writeOnlyDiags := ephemeral.ValidateWriteOnlyAttributes( + "Invalid resource state upgrade", + func(path cty.Path) string { + return fmt.Sprintf( + "While attempting to upgrade state of resource %s, the provider %q returned a value for the write-only attribute \"%s%s\". Write-only attributes cannot be read back from the provider. This is a bug in the provider, which should be reported in the provider's own issue tracker.", + addr, providerType, addr, tfdiags.FormatCtyPath(path), + ) + }, + newValue, + currentSchema.Body, + ) + diags = diags.Append(writeOnlyDiags) + + if writeOnlyDiags.HasErrors() { + return nil, diags + } + + new, err := src.CompleteUpgrade(newValue, currentSchema.Body.ImpliedType(), uint64(currentSchema.Version)) if err != nil { // We already checked for type conformance above, so getting into this // codepath should be rare and is probably a bug somewhere under CompleteUpgrade. @@ -122,11 +152,92 @@ func upgradeResourceState(addr addrs.AbsResourceInstance, provider providers.Int return new, diags } +func upgradeResourceIdentity(addr addrs.AbsResourceInstance, provider providers.Interface, src *states.ResourceInstanceObjectSrc, currentSchema providers.Schema) (*states.ResourceInstanceObjectSrc, tfdiags.Diagnostics) { + // TODO: This should eventually use a proper FQN. + providerType := addr.Resource.Resource.ImpliedProvider() + if src.IdentitySchemaVersion > uint64(currentSchema.IdentityVersion) { + log.Printf("[TRACE] upgradeResourceIdentity: can't downgrade identity for %s from version %d to %d", addr, src.IdentitySchemaVersion, currentSchema.IdentityVersion) + var diags tfdiags.Diagnostics + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Resource instance managed by newer provider version", + // This is not a very good error message, but we don't retain enough + // information in state to give good feedback on what provider + // version might be required here. :( + fmt.Sprintf("The current state of %s was created by a newer provider version than is currently selected. Upgrade the %s provider to work with this state.", addr, providerType), + )) + return nil, diags + } + + // We don't need to do anything if the identity schema version is already up-to-date. + if src.IdentitySchemaVersion == uint64(currentSchema.IdentityVersion) { + return src, nil + } + + req := providers.UpgradeResourceIdentityRequest{ + TypeName: addr.Resource.Resource.Type, + + // TODO: The internal schema version representations are all using + // uint64 instead of int64, but unsigned integers aren't friendly + // to all protobuf target languages so in practice we use int64 + // on the wire. In future we will change all of our internal + // representations to int64 too. + Version: int64(src.IdentitySchemaVersion), + RawIdentityJSON: src.IdentityJSON, + } + + resp := provider.UpgradeResourceIdentity(req) + diags := resp.Diagnostics + if diags.HasErrors() { + return nil, diags + } + + if !resp.UpgradedIdentity.IsWhollyKnown() { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid resource identity upgrade", + fmt.Sprintf("The %s provider upgraded the identity for %s from a previous version, but produced an invalid result: The returned state contains unknown values.", providerType, addr), + )) + return nil, diags + } + + newIdentity := resp.UpgradedIdentity + newType := newIdentity.Type() + currentType := currentSchema.Identity.ImpliedType() + if errs := newType.TestConformance(currentType); len(errs) > 0 { + for _, err := range errs { + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Invalid resource identity upgrade", + fmt.Sprintf("The %s provider upgraded the identity for %s from a previous version, but produced an invalid result: %s.", providerType, addr, tfdiags.FormatError(err)), + )) + } + return nil, diags + } + + new, err := src.CompleteIdentityUpgrade(newIdentity, currentSchema) + if err != nil { + // We already checked for type conformance above, so getting into this + // codepath should be rare and is probably a bug somewhere under CompleteUpgrade. + diags = diags.Append(tfdiags.Sourceless( + tfdiags.Error, + "Failed to encode result of resource identity upgrade", + fmt.Sprintf("Failed to encode state for %s after resource identity schema upgrade: %s.", addr, tfdiags.FormatError(err)), + )) + } + + return new, diags +} + // stripRemovedStateAttributes deletes any attributes no longer present in the // schema, so that the json can be correctly decoded. func stripRemovedStateAttributes(state []byte, ty cty.Type) []byte { + // we must use json.Number to avoid changing the precision of cty.Number values + decoder := json.NewDecoder(bytes.NewReader(state)) + decoder.UseNumber() + jsonMap := map[string]interface{}{} - err := json.Unmarshal(state, &jsonMap) + err := decoder.Decode(&jsonMap) if err != nil { // we just log any errors here, and let the normal decode process catch // invalid JSON. diff --git a/internal/terraform/upgrade_resource_state_test.go b/internal/terraform/upgrade_resource_state_test.go index 11ef77b5f3..ce998cf65e 100644 --- a/internal/terraform/upgrade_resource_state_test.go +++ b/internal/terraform/upgrade_resource_state_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,6 +8,7 @@ import ( "testing" "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" ) func TestStripRemovedStateAttributes(t *testing.T) { @@ -43,6 +47,20 @@ func TestStripRemovedStateAttributes(t *testing.T) { }), true, }, + { + "has large number", + map[string]interface{}{ + "a": "ok", + "b": nil, + }, + map[string]interface{}{ + "a": "ok", + }, + cty.Object(map[string]cty.Type{ + "a": cty.String, + }), + true, + }, { "removed nested string", map[string]interface{}{ @@ -146,3 +164,47 @@ func TestStripRemovedStateAttributes(t *testing.T) { }) } } + +func TestStripRemovedStateAttributesDecoder(t *testing.T) { + cases := []struct { + name string + state string + expect cty.Value + }{ + { + "removed string", + `{"a": "ok","b": "gone"}`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("ok"), + }), + }, + { + "removed null", + `{"a": "ok","b": "gone"}`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("ok"), + }), + }, + { + "with large number", + `{"a": 123456789123456789.123456789,"b": "gone"}`, + cty.ObjectVal(map[string]cty.Value{ + "a": cty.MustParseNumberVal("123456789123456789.123456789"), + }), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + upgraded := stripRemovedStateAttributes([]byte(tc.state), tc.expect.Type()) + got, err := ctyjson.Unmarshal(upgraded, tc.expect.Type()) + if err != nil { + t.Fatal(err) + } + + if !tc.expect.RawEquals(got) { + t.Fatalf("expected: %#v\n got: %#v\n", tc.expect, got) + } + }) + } +} diff --git a/internal/terraform/util.go b/internal/terraform/util.go deleted file mode 100644 index 7966b58dd2..0000000000 --- a/internal/terraform/util.go +++ /dev/null @@ -1,75 +0,0 @@ -package terraform - -import ( - "sort" -) - -// Semaphore is a wrapper around a channel to provide -// utility methods to clarify that we are treating the -// channel as a semaphore -type Semaphore chan struct{} - -// NewSemaphore creates a semaphore that allows up -// to a given limit of simultaneous acquisitions -func NewSemaphore(n int) Semaphore { - if n <= 0 { - panic("semaphore with limit <=0") - } - ch := make(chan struct{}, n) - return Semaphore(ch) -} - -// Acquire is used to acquire an available slot. -// Blocks until available. -func (s Semaphore) Acquire() { - s <- struct{}{} -} - -// TryAcquire is used to do a non-blocking acquire. -// Returns a bool indicating success -func (s Semaphore) TryAcquire() bool { - select { - case s <- struct{}{}: - return true - default: - return false - } -} - -// Release is used to return a slot. Acquire must -// be called as a pre-condition. -func (s Semaphore) Release() { - select { - case <-s: - default: - panic("release without an acquire") - } -} - -// strSliceContains checks if a given string is contained in a slice -// When anybody asks why Go needs generics, here you go. -func strSliceContains(haystack []string, needle string) bool { - for _, s := range haystack { - if s == needle { - return true - } - } - return false -} - -// deduplicate a slice of strings -func uniqueStrings(s []string) []string { - if len(s) < 2 { - return s - } - - sort.Strings(s) - result := make([]string, 1, len(s)) - result[0] = s[0] - for i := 1; i < len(s); i++ { - if s[i] != result[len(result)-1] { - result = append(result, s[i]) - } - } - return result -} diff --git a/internal/terraform/util_test.go b/internal/terraform/util_test.go deleted file mode 100644 index 8b3907e236..0000000000 --- a/internal/terraform/util_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package terraform - -import ( - "fmt" - "reflect" - "testing" - "time" -) - -func TestSemaphore(t *testing.T) { - s := NewSemaphore(2) - timer := time.AfterFunc(time.Second, func() { - panic("deadlock") - }) - defer timer.Stop() - - s.Acquire() - if !s.TryAcquire() { - t.Fatalf("should acquire") - } - if s.TryAcquire() { - t.Fatalf("should not acquire") - } - s.Release() - s.Release() - - // This release should panic - defer func() { - r := recover() - if r == nil { - t.Fatalf("should panic") - } - }() - s.Release() -} - -func TestStrSliceContains(t *testing.T) { - if strSliceContains(nil, "foo") { - t.Fatalf("Bad") - } - if strSliceContains([]string{}, "foo") { - t.Fatalf("Bad") - } - if strSliceContains([]string{"bar"}, "foo") { - t.Fatalf("Bad") - } - if !strSliceContains([]string{"bar", "foo"}, "foo") { - t.Fatalf("Bad") - } -} - -func TestUniqueStrings(t *testing.T) { - cases := []struct { - Input []string - Expected []string - }{ - { - []string{}, - []string{}, - }, - { - []string{"x"}, - []string{"x"}, - }, - { - []string{"a", "b", "c"}, - []string{"a", "b", "c"}, - }, - { - []string{"a", "a", "a"}, - []string{"a"}, - }, - { - []string{"a", "b", "a", "b", "a", "a"}, - []string{"a", "b"}, - }, - { - []string{"c", "b", "a", "c", "b"}, - []string{"a", "b", "c"}, - }, - } - - for i, tc := range cases { - t.Run(fmt.Sprintf("unique-%d", i), func(t *testing.T) { - actual := uniqueStrings(tc.Input) - if !reflect.DeepEqual(tc.Expected, actual) { - t.Fatalf("Expected: %q\nGot: %q", tc.Expected, actual) - } - }) - } -} diff --git a/internal/terraform/validate_selfref.go b/internal/terraform/validate_selfref.go index ff00cded75..fc22028929 100644 --- a/internal/terraform/validate_selfref.go +++ b/internal/terraform/validate_selfref.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -6,14 +9,14 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/configs/configschema" - "github.com/hashicorp/terraform/internal/lang" + "github.com/hashicorp/terraform/internal/lang/langrefs" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/terraform/internal/tfdiags" ) // validateSelfRef checks to ensure that expressions within a particular // referencable block do not reference that same block. -func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema *ProviderSchema) tfdiags.Diagnostics { +func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema providers.ProviderSchema) tfdiags.Diagnostics { var diags tfdiags.Diagnostics addrStrs := make([]string, 0, 1) @@ -24,25 +27,20 @@ func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema * addrStrs = append(addrStrs, tAddr.ContainingResource().String()) } - if providerSchema == nil { - diags = diags.Append(fmt.Errorf("provider schema unavailable while validating %s for self-references; this is a bug in Terraform and should be reported", addr)) - return diags - } - - var schema *configschema.Block + var schema providers.Schema switch tAddr := addr.(type) { case addrs.Resource: - schema, _ = providerSchema.SchemaForResourceAddr(tAddr) + schema = providerSchema.SchemaForResourceAddr(tAddr) case addrs.ResourceInstance: - schema, _ = providerSchema.SchemaForResourceAddr(tAddr.ContainingResource()) + schema = providerSchema.SchemaForResourceAddr(tAddr.ContainingResource()) } - if schema == nil { + if schema.Body == nil { diags = diags.Append(fmt.Errorf("no schema available for %s to validate for self-references; this is a bug in Terraform and should be reported", addr)) return diags } - refs, _ := lang.ReferencesInBlock(config, schema) + refs, _ := langrefs.ReferencesInBlock(addrs.ParseRef, config, schema.Body) for _, ref := range refs { for _, addrStr := range addrStrs { if ref.Subject.String() == addrStr { @@ -58,3 +56,110 @@ func validateSelfRef(addr addrs.Referenceable, config hcl.Body, providerSchema * return diags } + +// validateMetaSelfRef checks to ensure that a specific meta expression (count / +// for_each) does not reference the resource it is attached to. The behaviour +// is slightly different from validateSelfRef in that this function is only ever +// called from static contexts (ie. before expansion) and as such the address is +// always a Resource. +// +// This also means that often the references will be to instances of the +// resource, so we need to unpack these to the containing resource to compare +// against the static resource. From the perspective of this function +// `test_resource.foo[4]` is considered to be a self reference to +// `test_resource.foo`, in which is a significant behaviour change to +// validateSelfRef. +func validateMetaSelfRef(addr addrs.Resource, expr hcl.Expression) tfdiags.Diagnostics { + return validateSelfRefFromExprInner(addr, expr, func(ref *addrs.Reference) *hcl.Diagnostic { + return &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Self-referential block", + Detail: fmt.Sprintf("Configuration for %s may not refer to itself.", addr.String()), + Subject: ref.SourceRange.ToHCL().Ptr(), + } + }) +} + +// validateImportSelfRef is similar to validateMetaSelfRef except it +// tweaks the error message slightly to reflect the self-reference is coming +// from an import block instead of directly from the resource. All the same +// caveats apply as validateMetaSelfRef. +func validateImportSelfRef(addr addrs.Resource, expr hcl.Expression) tfdiags.Diagnostics { + return validateSelfRefFromExprInner(addr, expr, func(ref *addrs.Reference) *hcl.Diagnostic { + return &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid import id argument", + Detail: "The import ID cannot reference the resource being imported.", + Subject: ref.SourceRange.ToHCL().Ptr(), + } + }) +} + +func validateImportForEachRef(addr addrs.Resource, expr hcl.Expression) tfdiags.Diagnostics { + return validateSelfRefFromExprInner(addr, expr, func(ref *addrs.Reference) *hcl.Diagnostic { + return &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid for_each argument", + Detail: "The for_each expression cannot reference the resource being imported.", + Subject: ref.SourceRange.ToHCL().Ptr(), + } + }) +} + +// validateSelfRefFromExprInner is a helper function that takes an address and +// an expression and returns diagnostics for self-references in the expression. +// +// This should only be called via validateMetaSelfRef and validateImportSelfRef, +// do not access this function directly. +func validateSelfRefFromExprInner(addr addrs.Resource, expr hcl.Expression, diag func(ref *addrs.Reference) *hcl.Diagnostic) tfdiags.Diagnostics { + var diags tfdiags.Diagnostics + + refs, _ := langrefs.ReferencesInExpr(addrs.ParseRef, expr) + for _, ref := range refs { + var target addrs.Resource + switch t := ref.Subject.(type) { + case addrs.ResourceInstance: + // Automatically unpack an instance reference to its containing + // resource, since we're only comparing against the static resource. + target = t.Resource + case addrs.Resource: + target = t + default: + // Anything else cannot be a self-reference. + continue + } + + if target.Equal(addr) { + diags = diags.Append(diag(ref)) + } + } + + return diags +} + +// Legacy provisioner configurations may refer to single instances using the +// resource address. We need to filter these out from the reported references +// to prevent cycles. +func filterSelfRefs(self addrs.Resource, refs []*addrs.Reference) []*addrs.Reference { + for i := 0; i < len(refs); i++ { + ref := refs[i] + + var subject addrs.Resource + switch subj := ref.Subject.(type) { + case addrs.Resource: + subject = subj + case addrs.ResourceInstance: + subject = subj.ContainingResource() + default: + continue + } + + if self.Equal(subject) { + tail := len(refs) - 1 + + refs[i], refs[tail] = refs[tail], refs[i] + refs = refs[:tail] + } + } + return refs +} diff --git a/internal/terraform/validate_selfref_test.go b/internal/terraform/validate_selfref_test.go index 73fda25d0a..f139e4eaf1 100644 --- a/internal/terraform/validate_selfref_test.go +++ b/internal/terraform/validate_selfref_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -5,11 +8,13 @@ import ( "testing" "github.com/hashicorp/terraform/internal/configs/configschema" + "github.com/hashicorp/terraform/internal/providers" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hcltest" - "github.com/hashicorp/terraform/internal/addrs" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/addrs" ) func TestValidateSelfRef(t *testing.T) { @@ -79,19 +84,23 @@ func TestValidateSelfRef(t *testing.T) { }, }) - ps := &ProviderSchema{ - ResourceTypes: map[string]*configschema.Block{ - "aws_instance": &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "foo": { - Type: cty.String, - Required: true, + ps := providers.ProviderSchema{ + ResourceTypes: map[string]providers.Schema{ + "aws_instance": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "foo": { + Type: cty.String, + Required: true, + }, }, }, }, }, } + // First test the expression within the context of the configuration + // body. diags := validateSelfRef(test.Addr, body, ps) if diags.HasErrors() != test.Err { if test.Err { @@ -103,3 +112,59 @@ func TestValidateSelfRef(t *testing.T) { }) } } + +func TestValidateSelfInExpr(t *testing.T) { + rAddr := addrs.Resource{ + Mode: addrs.ManagedResourceMode, + Type: "aws_instance", + Name: "foo", + } + + tests := []struct { + Name string + Addr addrs.Resource + Expr hcl.Expression + Err bool + }{ + { + "no references at all", + rAddr, + hcltest.MockExprLiteral(cty.StringVal("bar")), + false, + }, + + { + "non self reference", + rAddr, + hcltest.MockExprTraversalSrc("aws_instance.bar.id"), + false, + }, + + { + "self reference", + rAddr, + hcltest.MockExprTraversalSrc("aws_instance.foo.id"), + true, + }, + + { + "self reference other index", + rAddr, + hcltest.MockExprTraversalSrc("aws_instance.foo[4].id"), + true, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d-%s", i, test.Name), func(t *testing.T) { + diags := validateMetaSelfRef(test.Addr, test.Expr) + if diags.HasErrors() != test.Err { + if test.Err { + t.Errorf("unexpected success; want error") + } else { + t.Errorf("unexpected error\n\n%s", diags.Err()) + } + } + }) + } +} diff --git a/internal/terraform/variables.go b/internal/terraform/variables.go index a60f187003..278f73dd7f 100644 --- a/internal/terraform/variables.go +++ b/internal/terraform/variables.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( @@ -134,7 +137,28 @@ func (v ValueSourceType) GoString() string { return fmt.Sprintf("terraform.%s", v) } -//go:generate go run golang.org/x/tools/cmd/stringer -type ValueSourceType +func (v ValueSourceType) DiagnosticLabel() string { + switch v { + case ValueFromConfig: + return "set by the default value in configuration" + case ValueFromAutoFile: + return "set by an automatically loaded .tfvars file" + case ValueFromNamedFile: + return "set by a .tfvars file passed through -var-file argument" + case ValueFromCLIArg: + return "set by a CLI argument" + case ValueFromEnvVar: + return "set by an environment variable" + case ValueFromInput: + return "set by an interactive input" + case ValueFromPlan: + return "set by the plan" + default: + return "unknown" + } +} + +//go:generate go tool golang.org/x/tools/cmd/stringer -type ValueSourceType // InputValues is a map of InputValue instances. type InputValues map[string]*InputValue diff --git a/internal/terraform/variables_test.go b/internal/terraform/variables_test.go index 6e53a95750..746c078add 100644 --- a/internal/terraform/variables_test.go +++ b/internal/terraform/variables_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( diff --git a/internal/terraform/version_required.go b/internal/terraform/version_required.go index 1861050b95..4c9873113c 100644 --- a/internal/terraform/version_required.go +++ b/internal/terraform/version_required.go @@ -1,14 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package terraform import ( - "fmt" - - "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/tfdiags" "github.com/hashicorp/terraform/internal/configs" - - tfversion "github.com/hashicorp/terraform/version" ) // CheckCoreVersionRequirements visits each of the modules in the given @@ -24,62 +22,7 @@ func CheckCoreVersionRequirements(config *configs.Config) tfdiags.Diagnostics { } var diags tfdiags.Diagnostics - module := config.Module - - for _, constraint := range module.CoreVersionConstraints { - // Before checking if the constraints are met, check that we are not using any prerelease fields as these - // are not currently supported. - var prereleaseDiags tfdiags.Diagnostics - for _, required := range constraint.Required { - if required.Prerelease() { - prereleaseDiags = prereleaseDiags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Invalid required_version constraint", - Detail: fmt.Sprintf( - "Prerelease version constraints are not supported: %s. Remove the prerelease information from the constraint. Prerelease versions of terraform will match constraints using their version core only.", - required.String()), - Subject: constraint.DeclRange.Ptr(), - }) - } - } - - if len(prereleaseDiags) > 0 { - // There were some prerelease fields in the constraints. Don't check the constraints as they will - // fail, and populate the diagnostics for these constraints with the prerelease diagnostics. - diags = diags.Append(prereleaseDiags) - continue - } - - if !constraint.Required.Check(tfversion.SemVer) { - switch { - case len(config.Path) == 0: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported Terraform Core version", - Detail: fmt.Sprintf( - "This configuration does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", - tfversion.String(), - ), - Subject: constraint.DeclRange.Ptr(), - }) - default: - diags = diags.Append(&hcl.Diagnostic{ - Severity: hcl.DiagError, - Summary: "Unsupported Terraform Core version", - Detail: fmt.Sprintf( - "Module %s (from %s) does not support Terraform version %s. To proceed, either choose another supported Terraform version or update this version constraint. Version constraints are normally set for good reason, so updating the constraint may lead to other errors or unexpected behavior.", - config.Path, config.SourceAddr, tfversion.String(), - ), - Subject: constraint.DeclRange.Ptr(), - }) - } - } - } - - for _, c := range config.Children { - childDiags := CheckCoreVersionRequirements(c) - diags = diags.Append(childDiags) - } + diags = diags.Append(config.CheckCoreVersionRequirements()) return diags } diff --git a/internal/tfdiags/compare.go b/internal/tfdiags/compare.go new file mode 100644 index 0000000000..8a7cc297d0 --- /dev/null +++ b/internal/tfdiags/compare.go @@ -0,0 +1,32 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package tfdiags + +import "github.com/google/go-cmp/cmp" + +// DiagnosticComparer returns a cmp.Option that can be used with +// the package github.com/google/go-cmp/cmp. +// +// The comparer relies on the underlying Diagnostic implementing +// [ComparableDiagnostic]. +// +// Example usage: +// +// cmp.Diff(diag1, diag2, tfdiags.DiagnosticComparer) +var DiagnosticComparer cmp.Option = cmp.Comparer(diagnosticComparerSimple) + +// diagnosticComparerSimple returns false when a difference is identified between +// the two Diagnostic arguments. +func diagnosticComparerSimple(l, r Diagnostic) bool { + ld, ok := l.(ComparableDiagnostic) + if !ok { + return false + } + + rd, ok := r.(ComparableDiagnostic) + if !ok { + return false + } + + return ld.Equals(rd) +} diff --git a/internal/tfdiags/compare_test.go b/internal/tfdiags/compare_test.go new file mode 100644 index 0000000000..760679d567 --- /dev/null +++ b/internal/tfdiags/compare_test.go @@ -0,0 +1,112 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package tfdiags + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl/v2" +) + +func TestDiagnosticComparer(t *testing.T) { + + baseError := hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error", + Detail: "this is an error", + Subject: &hcl.Range{ + Filename: "foobar.tf", + Start: hcl.Pos{ + Line: 0, + Column: 0, + Byte: 0, + }, + End: hcl.Pos{ + Line: 1, + Column: 1, + Byte: 1, + }, + }, + } + + cases := map[string]struct { + diag1 Diagnostic + diag2 Diagnostic + expectDiff bool + }{ + // Correctly identifying things that match + "reports that identical diagnostics match": { + diag1: hclDiagnostic{&baseError}, + diag2: hclDiagnostic{&baseError}, + expectDiff: false, + }, + // Correctly identifies when things don't match + "reports that diagnostics don't match if the concrete type differs": { + diag1: hclDiagnostic{&baseError}, + diag2: makeRPCFriendlyDiag(hclDiagnostic{&baseError}), + expectDiff: true, + }, + "reports that diagnostics don't match if severity differs": { + diag1: hclDiagnostic{&baseError}, + diag2: func() Diagnostic { + d := baseError + d.Severity = hcl.DiagWarning + return hclDiagnostic{&d} + }(), + expectDiff: true, + }, + "reports that diagnostics don't match if summary differs": { + diag1: hclDiagnostic{&baseError}, + diag2: func() Diagnostic { + d := baseError + d.Summary = "altered summary" + return hclDiagnostic{&d} + }(), + expectDiff: true, + }, + "reports that diagnostics don't match if detail differs": { + diag1: hclDiagnostic{&baseError}, + diag2: func() Diagnostic { + d := baseError + d.Detail = "altered detail" + return hclDiagnostic{&d} + }(), + expectDiff: true, + }, + "reports that diagnostics don't match if attribute path differs": { + diag1: func() Diagnostic { + return AttributeValue(Error, "summary here", "detail here", cty.Path{cty.GetAttrStep{Name: "foobar1"}}) + }(), + diag2: func() Diagnostic { + return AttributeValue(Error, "summary here", "detail here", cty.Path{cty.GetAttrStep{Name: "foobar2"}}) + }(), + expectDiff: true, + }, + "reports that diagnostics don't match if attribute path is missing from one": { + diag1: func() Diagnostic { + return AttributeValue(Error, "summary here", "detail here", cty.Path{cty.GetAttrStep{Name: "foobar1"}}) + }(), + diag2: func() Diagnostic { + return AttributeValue(Error, "summary here", "detail here", cty.Path{}) + }(), + expectDiff: true, + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + output := cmp.Diff(tc.diag1, tc.diag2, DiagnosticComparer) + + diffFound := output != "" + if diffFound && !tc.expectDiff { + t.Fatalf("unexpected diff detected:\n%s", output) + } + if !diffFound && tc.expectDiff { + t.Fatal("expected a diff but none was detected") + } + }) + } +} diff --git a/internal/tfdiags/config_traversals.go b/internal/tfdiags/config_traversals.go index 8e41f46ed2..531daf391d 100644 --- a/internal/tfdiags/config_traversals.go +++ b/internal/tfdiags/config_traversals.go @@ -1,68 +1,26 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( - "bytes" - "fmt" - "strconv" - - "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/terraform/internal/lang/format" ) +// These functions have been moved to the format package to allow for imports +// which would otherwise cause cycles. + // FormatCtyPath is a helper function to produce a user-friendly string // representation of a cty.Path. The result uses a syntax similar to the // HCL expression language in the hope of it being familiar to users. -func FormatCtyPath(path cty.Path) string { - var buf bytes.Buffer - for _, step := range path { - switch ts := step.(type) { - case cty.GetAttrStep: - fmt.Fprintf(&buf, ".%s", ts.Name) - case cty.IndexStep: - buf.WriteByte('[') - key := ts.Key - keyTy := key.Type() - switch { - case key.IsNull(): - buf.WriteString("null") - case !key.IsKnown(): - buf.WriteString("(not yet known)") - case keyTy == cty.Number: - bf := key.AsBigFloat() - buf.WriteString(bf.Text('g', -1)) - case keyTy == cty.String: - buf.WriteString(strconv.Quote(key.AsString())) - default: - buf.WriteString("...") - } - buf.WriteByte(']') - } - } - return buf.String() -} +var FormatCtyPath = format.CtyPath // FormatError is a helper function to produce a user-friendly string // representation of certain special error types that we might want to // include in diagnostic messages. -// -// This currently has special behavior only for cty.PathError, where a -// non-empty path is rendered in a HCL-like syntax as context. -func FormatError(err error) string { - perr, ok := err.(cty.PathError) - if !ok || len(perr.Path) == 0 { - return err.Error() - } - - return fmt.Sprintf("%s: %s", FormatCtyPath(perr.Path), perr.Error()) -} +var FormatError = format.ErrorDiag // FormatErrorPrefixed is like FormatError except that it presents any path // information after the given prefix string, which is assumed to contain // an HCL syntax representation of the value that errors are relative to. -func FormatErrorPrefixed(err error, prefix string) string { - perr, ok := err.(cty.PathError) - if !ok || len(perr.Path) == 0 { - return fmt.Sprintf("%s: %s", prefix, err.Error()) - } - - return fmt.Sprintf("%s%s: %s", prefix, FormatCtyPath(perr.Path), perr.Error()) -} +var FormatErrorPrefixed = format.ErrorDiagPrefixed diff --git a/internal/tfdiags/consolidate_warnings.go b/internal/tfdiags/consolidate_warnings.go index 08d36d60b6..329e36222a 100644 --- a/internal/tfdiags/consolidate_warnings.go +++ b/internal/tfdiags/consolidate_warnings.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import "fmt" @@ -43,6 +46,12 @@ func (diags Diagnostics) ConsolidateWarnings(threshold int) Diagnostics { continue } + if DoNotConsolidateDiagnostic(diag) { + // Then do not consolidate this diagnostic. + newDiags = newDiags.Append(diag) + continue + } + desc := diag.Description() summary := desc.Summary if g, ok := warningGroups[summary]; ok { diff --git a/internal/tfdiags/consolidate_warnings_test.go b/internal/tfdiags/consolidate_warnings_test.go index df94d4af8f..8cebc0a300 100644 --- a/internal/tfdiags/consolidate_warnings_test.go +++ b/internal/tfdiags/consolidate_warnings_test.go @@ -1,10 +1,12 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( "fmt" "testing" - "github.com/google/go-cmp/cmp" "github.com/hashicorp/hcl/v2" ) @@ -51,9 +53,45 @@ func TestConsolidateWarnings(t *testing.T) { }, }) - // We're using ForRPC here to force the diagnostics to be of a consistent - // type that we can easily assert against below. - got := diags.ConsolidateWarnings(2).ForRPC() + // Finally, we'll just add a set of diags that should not be consolidated. + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "do not consolidate", + Detail: "warning 1, I should not have been consolidated", + Subject: &hcl.Range{ + Filename: "bar.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + }, + Extra: doNotConsolidate(true), + }) + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "do not consolidate", + Detail: "warning 2, I should not have been consolidated", + Subject: &hcl.Range{ + Filename: "bar.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + }, + Extra: doNotConsolidate(true), + }) + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "do not consolidate", + Detail: "warning 3, I should not have been consolidated", + Subject: &hcl.Range{ + Filename: "bar.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + }, + Extra: doNotConsolidate(true), + }) + + got := diags.ConsolidateWarnings(2) want := Diagnostics{ // First set &rpcFriendlyDiag{ @@ -171,9 +209,48 @@ func TestConsolidateWarnings(t *testing.T) { End: SourcePos{Line: 1, Column: 1, Byte: 0}, }, }, + + // The final set of warnings should not have been consolidated because + // of our filter function. + &rpcFriendlyDiag{ + Severity_: Warning, + Summary_: "do not consolidate", + Detail_: "warning 1, I should not have been consolidated", + Subject_: &SourceRange{ + Filename: "bar.tf", + Start: SourcePos{Line: 1, Column: 1, Byte: 0}, + End: SourcePos{Line: 1, Column: 1, Byte: 0}, + }, + }, + &rpcFriendlyDiag{ + Severity_: Warning, + Summary_: "do not consolidate", + Detail_: "warning 2, I should not have been consolidated", + Subject_: &SourceRange{ + Filename: "bar.tf", + Start: SourcePos{Line: 1, Column: 1, Byte: 0}, + End: SourcePos{Line: 1, Column: 1, Byte: 0}, + }, + }, + &rpcFriendlyDiag{ + Severity_: Warning, + Summary_: "do not consolidate", + Detail_: "warning 3, I should not have been consolidated", + Subject_: &SourceRange{ + Filename: "bar.tf", + Start: SourcePos{Line: 1, Column: 1, Byte: 0}, + End: SourcePos{Line: 1, Column: 1, Byte: 0}, + }, + }, } - if diff := cmp.Diff(want, got); diff != "" { - t.Errorf("wrong result\n%s", diff) - } + AssertDiagnosticsMatch(t, want, got) +} + +type doNotConsolidate bool + +var _ DiagnosticExtraDoNotConsolidate = doNotConsolidate(true) + +func (d doNotConsolidate) DoNotConsolidateDiagnostic() bool { + return bool(d) } diff --git a/internal/tfdiags/contextual.go b/internal/tfdiags/contextual.go index c59280f342..7f2bd5e5df 100644 --- a/internal/tfdiags/contextual.go +++ b/internal/tfdiags/contextual.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( @@ -202,6 +205,31 @@ func (d *attributeDiagnostic) ElaborateFromConfigBody(body hcl.Body, addr string return &ret } +func (d *attributeDiagnostic) Equals(otherDiag ComparableDiagnostic) bool { + od, ok := otherDiag.(*attributeDiagnostic) + if !ok { + return false + } + if d.severity != od.severity { + return false + } + if d.summary != od.summary { + return false + } + if d.detail != od.detail { + return false + } + if !d.attrPath.Equals(od.attrPath) { + return false + } + + // address can differ between and after expansion + // even though it represents the same attribute + // so we avoid comparing it here + + return sourceRangeEquals(d.subject, od.subject) +} + func traversePathSteps(traverse []cty.PathStep, body hcl.Body) hcl.Body { for i := 0; i < len(traverse); i++ { step := traverse[i] @@ -383,3 +411,41 @@ func (d *wholeBodyDiagnostic) Source() Source { Subject: d.subject, } } + +func (d *wholeBodyDiagnostic) Equals(otherDiag ComparableDiagnostic) bool { + od, ok := otherDiag.(*wholeBodyDiagnostic) + if !ok { + return false + } + if d.severity != od.severity { + return false + } + if d.summary != od.summary { + return false + } + if d.detail != od.detail { + return false + } + + // address can differ between and after expansion + // even though it represents the same attribute + // so we avoid comparing it here + + return sourceRangeEquals(d.subject, od.subject) +} + +func sourceRangeEquals(l, r *SourceRange) bool { + if l == nil || r == nil { + return l == r + } + if l.Filename != r.Filename { + return false + } + if l.Start.Byte != r.Start.Byte { + return false + } + if l.End.Byte != r.End.Byte { + return false + } + return true +} diff --git a/internal/tfdiags/contextual_test.go b/internal/tfdiags/contextual_test.go index e29712a7b6..7395e731ee 100644 --- a/internal/tfdiags/contextual_test.go +++ b/internal/tfdiags/contextual_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( diff --git a/internal/tfdiags/diagnostic.go b/internal/tfdiags/diagnostic.go index f988f398b6..fbead195e1 100644 --- a/internal/tfdiags/diagnostic.go +++ b/internal/tfdiags/diagnostic.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( @@ -24,9 +27,13 @@ type Diagnostic interface { ExtraInfo() interface{} } +type ComparableDiagnostic interface { + Equals(otherDiag ComparableDiagnostic) bool +} + type Severity rune -//go:generate go run golang.org/x/tools/cmd/stringer -type=Severity +//go:generate go tool golang.org/x/tools/cmd/stringer -type=Severity const ( Error Severity = 'E' diff --git a/internal/tfdiags/diagnostic_base.go b/internal/tfdiags/diagnostic_base.go index 88495290e7..6c0db08280 100644 --- a/internal/tfdiags/diagnostic_base.go +++ b/internal/tfdiags/diagnostic_base.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags // diagnosticBase can be embedded in other diagnostic structs to get diff --git a/internal/tfdiags/diagnostic_extra.go b/internal/tfdiags/diagnostic_extra.go index 97a14746c4..2bf25af830 100644 --- a/internal/tfdiags/diagnostic_extra.go +++ b/internal/tfdiags/diagnostic_extra.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags // This "Extra" idea is something we've inherited from HCL's diagnostic model, @@ -102,6 +105,14 @@ type DiagnosticExtraUnwrapper interface { UnwrapDiagnosticExtra() interface{} } +// DiagnosticExtraWrapper is an interface implemented by values that can be +// dynamically updated to wrap other extra info. +type DiagnosticExtraWrapper interface { + // WrapDiagnosticExtra accepts an ExtraInfo that it should add within the + // current ExtraInfo. + WrapDiagnosticExtra(inner interface{}) +} + // DiagnosticExtraBecauseUnknown is an interface implemented by values in // the Extra field of Diagnostic when the diagnostic is potentially caused by // the presence of unknown values in an expression evaluation. @@ -136,6 +147,40 @@ func DiagnosticCausedByUnknown(diag Diagnostic) bool { return maybe.DiagnosticCausedByUnknown() } +// DiagnosticExtraBecauseEphemeral is an interface implemented by values in +// the Extra field of Diagnostic when the diagnostic is potentially caused by +// the presence of ephemeral values in an expression evaluation. +// +// Just implementing this interface is not sufficient signal, though. Callers +// must also call the DiagnosticCausedByEphemeral method in order to confirm +// the result, or use the package-level function DiagnosticCausedByEphemeral +// as a convenient wrapper. +type DiagnosticExtraBecauseEphemeral interface { + // DiagnosticCausedByEphemeral returns true if the associated diagnostic + // was caused by the presence of ephemeral values during an expression + // evaluation, or false otherwise. + // + // Callers might use this to tailor what contextual information they show + // alongside an error report in the UI, to avoid potential confusion + // caused by talking about the presence of deferred values if that was + // immaterial to the error. + DiagnosticCausedByEphemeral() bool +} + +// DiagnosticCausedByEphemeral returns true if the given diagnostic has an +// indication that it was caused by the presence of deferred values during +// an expression evaluation. +// +// This is a wrapper around checking if the diagnostic's extra info implements +// interface DiagnosticExtraBecauseDeferred and then calling its method if so. +func DiagnosticCausedByEphemeral(diag Diagnostic) bool { + maybe := ExtraInfo[DiagnosticExtraBecauseEphemeral](diag) + if maybe == nil { + return false + } + return maybe.DiagnosticCausedByEphemeral() +} + // DiagnosticExtraBecauseSensitive is an interface implemented by values in // the Extra field of Diagnostic when the diagnostic is potentially caused by // the presence of sensitive values in an expression evaluation. @@ -169,3 +214,49 @@ func DiagnosticCausedBySensitive(diag Diagnostic) bool { } return maybe.DiagnosticCausedBySensitive() } + +// DiagnosticExtraDoNotConsolidate tells the Diagnostics.ConsolidateWarnings +// function not to consolidate this diagnostic if it otherwise would. +type DiagnosticExtraDoNotConsolidate interface { + // DoNotConsolidateDiagnostic returns true if the associated diagnostic + // should not be consolidated by the Diagnostics.ConsolidateWarnings + // function. + DoNotConsolidateDiagnostic() bool +} + +// DoNotConsolidateDiagnostic returns true if the given diagnostic should not +// be consolidated by the Diagnostics.ConsolidateWarnings function. +func DoNotConsolidateDiagnostic(diag Diagnostic) bool { + maybe := ExtraInfo[DiagnosticExtraDoNotConsolidate](diag) + if maybe == nil { + return false + } + return maybe.DoNotConsolidateDiagnostic() +} + +// DiagnosticExtraCausedByTestFailure is an interface implemented by +// values in the Extra field of Diagnostic when the diagnostic is caused by a +// failing assertion in a run block during the `test` command. +// +// Just implementing this interface is not sufficient signal, though. Callers +// must also call the DiagnosticCausedByTestFailure method in order to +// confirm the result, or use the package-level function +// DiagnosticCausedByTestFailure as a convenient wrapper. +type DiagnosticExtraCausedByTestFailure interface { + // DiagnosticCausedByTestFailure returns true if the associated + // diagnostic is the result of a failed assertion in a run block. + DiagnosticCausedByTestFailure() bool + + // IsTestVerboseMode returns true if the test was executed in verbose mode. + IsTestVerboseMode() bool +} + +// DiagnosticCausedByTestFailure returns true if the given diagnostic +// is the result of a failed assertion in a run block. +func DiagnosticCausedByTestFailure(diag Diagnostic) bool { + maybe := ExtraInfo[DiagnosticExtraCausedByTestFailure](diag) + if maybe == nil { + return false + } + return maybe.DiagnosticCausedByTestFailure() +} diff --git a/internal/tfdiags/diagnostics.go b/internal/tfdiags/diagnostics.go index b4d71bf3c8..5e549de8f8 100644 --- a/internal/tfdiags/diagnostics.go +++ b/internal/tfdiags/diagnostics.go @@ -1,14 +1,16 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( "bytes" + "errors" "fmt" "path/filepath" "sort" "strings" - "github.com/hashicorp/errwrap" - multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/hcl/v2" ) @@ -55,7 +57,7 @@ func (diags Diagnostics) Append(new ...interface{}) Diagnostics { diags = append(diags, ti) case Diagnostics: diags = append(diags, ti...) // flatten - case diagnosticsAsError: + case DiagnosticsAsError: diags = diags.Append(ti.Diagnostics) // unwrap case NonFatalError: diags = diags.Append(ti.Diagnostics) // unwrap @@ -65,23 +67,8 @@ func (diags Diagnostics) Append(new ...interface{}) Diagnostics { } case *hcl.Diagnostic: diags = append(diags, hclDiagnostic{ti}) - case *multierror.Error: - for _, err := range ti.Errors { - diags = append(diags, nativeError{err}) - } case error: - switch { - case errwrap.ContainsType(ti, Diagnostics(nil)): - // If we have an errwrap wrapper with a Diagnostics hiding - // inside then we'll unpick it here to get access to the - // individual diagnostics. - diags = diags.Append(errwrap.GetType(ti, Diagnostics(nil))) - case errwrap.ContainsType(ti, hcl.Diagnostics(nil)): - // Likewise, if we have HCL diagnostics we'll unpick that too. - diags = diags.Append(errwrap.GetType(ti, hcl.Diagnostics(nil))) - default: - diags = append(diags, nativeError{ti}) - } + diags = append(diags, diagnosticsForError(ti)...) default: panic(fmt.Errorf("can't construct diagnostic(s) from %T", item)) } @@ -96,6 +83,116 @@ func (diags Diagnostics) Append(new ...interface{}) Diagnostics { return diags } +// ContainsDiagnostic returns true of a given diagnostic is contained +// within the Diagnostics slice. +// Comparisons are done via [ComparableDiagnostic]. +func (diags Diagnostics) ContainsDiagnostic(diag ComparableDiagnostic) bool { + for _, d := range diags { + if cd, ok := d.(ComparableDiagnostic); ok && diag.Equals(cd) { + return true + } + } + return false +} + +// AppendWithoutDuplicates appends a Diagnostic unless one is already contained +// according to [ContainsDiagnostic], i.e. based on [ComparableDiagnostic]. +func (diags Diagnostics) AppendWithoutDuplicates(newDiags ...Diagnostic) Diagnostics { + for _, newItem := range newDiags { + if newItem == nil { + continue + } + + cd, ok := newItem.(ComparableDiagnostic) + if !ok { + // append what we cannot compare + diags = diags.Append(newItem) + continue + } + if diags.ContainsDiagnostic(cd) { + continue + } + + diags = diags.Append(newItem) + } + + if len(diags) == 0 { + return nil + } + + return diags +} + +func diagnosticsForError(err error) []Diagnostic { + if err == nil { + return nil + } + + // This is the interface implemented by the result of the + // standard library errors.Join function, which combines + // multiple errors together into a single error value. + type UnwrapJoined interface { + Unwrap() []error + } + if err, ok := err.(UnwrapJoined); ok { + errs := err.Unwrap() + if len(errs) == 0 { // weird, but harmless! + return nil + } + // We'll start with the assumption of 1:1 relationship between + // errors and diagnostics, but we'll grow this if one of + // the wrapped errors becomes multiple diagnostics itself. + ret := make([]Diagnostic, 0, len(errs)) + for _, err := range errs { + ret = append(ret, diagnosticsForError(err)...) + } + return ret + } + + // If we've wrapped a Diagnostics in an error then we'll unwrap + // it and add it directly. + var asErr DiagnosticsAsError + if errors.As(err, &asErr) { + return asErr.Diagnostics + } + + // We also support wrapping diagnostics in a special kind of error + // that might contain only warnings, in special cases where the + // caller and callee are both aware of that convention. + var asErrWithWarnings NonFatalError + if errors.As(err, &asErrWithWarnings) { + return asErrWithWarnings.Diagnostics + } + + // Finally, HCL's own Diagnostics type implements error and so we + // might have been given HCL diagnostics directly. + var asHCLDiags hcl.Diagnostics + if errors.As(err, &asHCLDiags) { + ret := make([]Diagnostic, len(asHCLDiags)) + for i, hclDiag := range asHCLDiags { + ret[i] = hclDiagnostic{hclDiag} + } + return ret + } + + // If none of the special treatments above applied then we'll just + // wrap the given error as a single (low-quality) diagnostic. + return []Diagnostic{ + nativeError{err}, + } +} + +// Warnings returns a Diagnostics list containing only diagnostics with a severity of Warning. +func (diags Diagnostics) Warnings() Diagnostics { + var warns = Diagnostics{} + for _, diag := range diags { + if diag.Severity() == Warning { + warns = append(warns, diag) + } + } + return warns +} + // HasErrors returns true if any of the diagnostics in the list have // a severity of Error. func (diags Diagnostics) HasErrors() bool { @@ -107,6 +204,17 @@ func (diags Diagnostics) HasErrors() bool { return false } +// HasWarnings returns true if any of the diagnostics in the list have +// a severity of Warning. +func (diags Diagnostics) HasWarnings() bool { + for _, diag := range diags { + if diag.Severity() == Warning { + return true + } + } + return false +} + // ForRPC returns a version of the receiver that has been simplified so that // it is friendly to RPC protocols. // @@ -138,7 +246,7 @@ func (diags Diagnostics) Err() error { if !diags.HasErrors() { return nil } - return diagnosticsAsError{diags} + return DiagnosticsAsError{diags} } // ErrWithWarnings is similar to Err except that it will also return a non-nil @@ -189,11 +297,12 @@ func (diags Diagnostics) Sort() { sort.Stable(sortDiagnostics(diags)) } -type diagnosticsAsError struct { +// DiagnosticsAsError embeds diagnostics, and satisfies the error interface. +type DiagnosticsAsError struct { Diagnostics } -func (dae diagnosticsAsError) Error() string { +func (dae DiagnosticsAsError) Error() string { diags := dae.Diagnostics switch { case len(diags) == 0: @@ -223,7 +332,7 @@ func (dae diagnosticsAsError) Error() string { // WrappedErrors is an implementation of errwrap.Wrapper so that an error-wrapped // diagnostics object can be picked apart by errwrap-aware code. -func (dae diagnosticsAsError) WrappedErrors() []error { +func (dae DiagnosticsAsError) WrappedErrors() []error { var errs []error for _, diag := range dae.Diagnostics { if wrapper, isErr := diag.(nativeError); isErr { diff --git a/internal/tfdiags/diagnostics_test.go b/internal/tfdiags/diagnostics_test.go index c2f50d8e85..1cac152ef0 100644 --- a/internal/tfdiags/diagnostics_test.go +++ b/internal/tfdiags/diagnostics_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( @@ -7,8 +10,6 @@ import ( "strings" "testing" - "github.com/hashicorp/go-multierror" - "github.com/davecgh/go-spew/spew" "github.com/hashicorp/hcl/v2" ) @@ -122,10 +123,10 @@ func TestBuild(t *testing.T) { }, }, }, - "multierror.Error": { + "errors.Join": { func(diags Diagnostics) Diagnostics { - err := multierror.Append(nil, errors.New("bad thing A")) - err = multierror.Append(err, errors.New("bad thing B")) + err := errors.Join(nil, errors.New("bad thing A")) + err = errors.Join(err, errors.New("bad thing B")) diags = diags.Append(err) return diags }, @@ -158,6 +159,42 @@ func TestBuild(t *testing.T) { }, }, }, + "Diagnostics.Err": { + func(diags Diagnostics) Diagnostics { + var moreDiags Diagnostics + moreDiags = moreDiags.Append(errors.New("bad thing A")) + moreDiags = moreDiags.Append(errors.New("bad thing B")) + return diags.Append(moreDiags.Err()) + }, + []diagFlat{ + { + Severity: Error, + Summary: "bad thing A", + }, + { + Severity: Error, + Summary: "bad thing B", + }, + }, + }, + "Diagnostics.ErrWithWarnings": { + func(diags Diagnostics) Diagnostics { + var moreDiags Diagnostics + moreDiags = moreDiags.Append(SimpleWarning("Don't forget your toothbrush!")) + moreDiags = moreDiags.Append(SimpleWarning("Always make sure you know where your towel is")) + return diags.Append(moreDiags.ErrWithWarnings()) + }, + []diagFlat{ + { + Severity: Warning, + Summary: "Don't forget your toothbrush!", + }, + { + Severity: Warning, + Summary: "Always make sure you know where your towel is", + }, + }, + }, "single Diagnostic": { func(diags Diagnostics) Diagnostics { return diags.Append(SimpleWarning("Don't forget your toothbrush!")) @@ -437,3 +474,319 @@ func TestDiagnosticsNonFatalErr(t *testing.T) { } }) } + +func TestWarnings(t *testing.T) { + errorDiag := &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "something bad happened", + Detail: "details of the error", + } + + warnDiag := &hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "something bad happened", + Detail: "details of the warning", + } + + cases := map[string]struct { + diags Diagnostics + expected Diagnostics + }{ + "empty diags": { + diags: Diagnostics{}, + expected: Diagnostics{}, + }, + "nil diags": { + diags: nil, + expected: Diagnostics{}, + }, + "all error diags": { + diags: func() Diagnostics { + var d Diagnostics + d = d.Append(errorDiag, errorDiag, errorDiag) + return d + }(), + expected: Diagnostics{}, + }, + "mixture of error and warning diags": { + diags: func() Diagnostics { + var d Diagnostics + d = d.Append(errorDiag, errorDiag, warnDiag) + return d + }(), + expected: func() Diagnostics { + var d Diagnostics + d = d.Append(warnDiag) + return d + }(), + }, + "empty error diags": { + diags: func() Diagnostics { + var d Diagnostics + d = d.Append(warnDiag, warnDiag) + return d + }(), + expected: func() Diagnostics { + var d Diagnostics + d = d.Append(warnDiag, warnDiag) + return d + }(), + }, + } + + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + warnings := tc.diags.Warnings() + + AssertDiagnosticsMatch(t, tc.expected, warnings) + }) + } +} + +func TestAppendWithoutDuplicates(t *testing.T) { + type diagFlat struct { + Severity Severity + Summary string + Detail string + Subject *SourceRange + Context *SourceRange + } + + tests := map[string]struct { + Cons func(Diagnostics) Diagnostics + Want []diagFlat + }{ + "nil": { + func(diags Diagnostics) Diagnostics { + return nil + }, + nil, + }, + "errors.New": { + // these could be from different locations, so we can't dedupe them + func(diags Diagnostics) Diagnostics { + return diags.Append( + errors.New("oh no bad"), + errors.New("oh no bad"), + ) + }, + []diagFlat{ + { + Severity: Error, + Summary: "oh no bad", + }, + { + Severity: Error, + Summary: "oh no bad", + }, + }, + }, + "hcl.Diagnostic": { + func(diags Diagnostics) Diagnostics { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 25}, + }, + Context: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 30}, + }, + }) + // exact same diag + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 25}, + }, + Context: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 1, Column: 1, Byte: 0}, + End: hcl.Pos{Line: 3, Column: 1, Byte: 30}, + }, + }) + // same diag as prev, different location + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 4, Column: 10, Byte: 40}, + End: hcl.Pos{Line: 5, Column: 3, Byte: 55}, + }, + Context: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 4, Column: 1, Byte: 40}, + End: hcl.Pos{Line: 6, Column: 1, Byte: 60}, + }, + }) + return diags + }, + []diagFlat{ + { + Severity: Error, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &SourceRange{ + Filename: "foo.tf", + Start: SourcePos{Line: 1, Column: 10, Byte: 9}, + End: SourcePos{Line: 2, Column: 3, Byte: 25}, + }, + Context: &SourceRange{ + Filename: "foo.tf", + Start: SourcePos{Line: 1, Column: 1, Byte: 0}, + End: SourcePos{Line: 3, Column: 1, Byte: 30}, + }, + }, + { + Severity: Error, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &SourceRange{ + Filename: "foo.tf", + Start: SourcePos{Line: 4, Column: 10, Byte: 40}, + End: SourcePos{Line: 5, Column: 3, Byte: 55}, + }, + Context: &SourceRange{ + Filename: "foo.tf", + Start: SourcePos{Line: 4, Column: 1, Byte: 40}, + End: SourcePos{Line: 6, Column: 1, Byte: 60}, + }, + }, + }, + }, + "simple warning": { + func(diags Diagnostics) Diagnostics { + diags = diags.Append(SimpleWarning("Don't forget your toothbrush!")) + diags = diags.Append(SimpleWarning("Don't forget your toothbrush!")) + return diags + }, + []diagFlat{ + { + Severity: Warning, + Summary: "Don't forget your toothbrush!", + }, + { + Severity: Warning, + Summary: "Don't forget your toothbrush!", + }, + }, + }, + "hcl.Diagnostic extra": { + // Extra can contain anything, and we don't know how to compare + // those values, so we can't dedupe them + func(diags Diagnostics) Diagnostics { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Extra: 42, + Subject: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 25}, + }, + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Extra: 38, + Subject: &hcl.Range{ + Filename: "foo.tf", + Start: hcl.Pos{Line: 1, Column: 10, Byte: 9}, + End: hcl.Pos{Line: 2, Column: 3, Byte: 25}, + }, + }) + return diags + }, + []diagFlat{ + { + Severity: Error, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &SourceRange{ + Filename: "foo.tf", + Start: SourcePos{Line: 1, Column: 10, Byte: 9}, + End: SourcePos{Line: 2, Column: 3, Byte: 25}, + }, + }, + { + Severity: Error, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + Subject: &SourceRange{ + Filename: "foo.tf", + Start: SourcePos{Line: 1, Column: 10, Byte: 9}, + End: SourcePos{Line: 2, Column: 3, Byte: 25}, + }, + }, + }, + }, + "hcl.Diagnostic no-location": { + // Extra can contain anything, and we don't know how to compare + // those values, so we can't dedupe them + func(diags Diagnostics) Diagnostics { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + }) + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + }) + return diags + }, + []diagFlat{ + { + Severity: Error, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + }, + { + Severity: Error, + Summary: "Something bad happened", + Detail: "It was really, really bad.", + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var deduped Diagnostics + + diags := test.Cons(nil) + deduped = deduped.AppendWithoutDuplicates(diags...) + + var got []diagFlat + for _, item := range deduped { + desc := item.Description() + source := item.Source() + got = append(got, diagFlat{ + Severity: item.Severity(), + Summary: desc.Summary, + Detail: desc.Detail, + Subject: source.Subject, + Context: source.Context, + }) + } + + if !reflect.DeepEqual(got, test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/internal/tfdiags/doc.go b/internal/tfdiags/doc.go index c427879ebc..6c39d06fe2 100644 --- a/internal/tfdiags/doc.go +++ b/internal/tfdiags/doc.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // Package tfdiags is a utility package for representing errors and // warnings in a manner that allows us to produce good messages for the // user. diff --git a/internal/tfdiags/error.go b/internal/tfdiags/error.go index 1e26bf9680..cb388e593d 100644 --- a/internal/tfdiags/error.go +++ b/internal/tfdiags/error.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags // nativeError is a Diagnostic implementation that wraps a normal Go error diff --git a/internal/tfdiags/format.go b/internal/tfdiags/format.go new file mode 100644 index 0000000000..9ac2a8e7e4 --- /dev/null +++ b/internal/tfdiags/format.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tfdiags + +import ( + "bytes" + "encoding/json" + "fmt" + + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/lang/marks" +) + +// CompactValueStr produces a compact, single-line summary of a given value +// that is suitable for display in the UI. +// +// For primitives it returns a full representation, while for more complex +// types it instead summarizes the type, size, etc to produce something +// that is hopefully still somewhat useful but not as verbose as a rendering +// of the entire data structure. +func CompactValueStr(val cty.Value) string { + // This is a specialized subset of value rendering tailored to producing + // helpful but concise messages in diagnostics. It is not comprehensive + // nor intended to be used for other purposes. + + val, valMarks := val.Unmark() + for mark := range valMarks { + switch mark { + case marks.Sensitive: + // We check this in here just to make sure, but note that the caller + // of compactValueStr ought to have already checked this and skipped + // calling into compactValueStr anyway, so this shouldn't actually + // be reachable. + return "(sensitive value)" + case marks.Ephemeral: + // A non-sensitive ephemeral value is fine to show in the UI. Values + // that are both ephemeral and sensitive should have both markings + // and should therefore get caught by the marks.Sensitive case + // above. + return "(ephemeral value)" + default: + // We don't know about any other marks, so we'll be conservative. + // This shouldn't actually reachable since the caller should've + // checked this and skipped calling compactValueStr anyway. + return "value with unrecognized marks (this is a bug in Terraform)" + } + } + + // WARNING: We've only checked that the value isn't sensitive _shallowly_ + // here, and so we must never show any element values from complex types + // in here. However, it's fine to show map keys and attribute names because + // those are never sensitive in isolation: the entire value would be + // sensitive in that case. + + ty := val.Type() + switch { + case val.IsNull(): + return "null" + case !val.IsKnown(): + // Should never happen here because we should filter before we get + // in here, but we'll do something reasonable rather than panic. + return "(not yet known)" + case ty == cty.Bool: + if val.True() { + return "true" + } + return "false" + case ty == cty.Number: + bf := val.AsBigFloat() + return bf.Text('g', 10) + case ty == cty.String: + // Go string syntax is not exactly the same as HCL native string syntax, + // but we'll accept the minor edge-cases where this is different here + // for now, just to get something reasonable here. + return fmt.Sprintf("%q", val.AsString()) + case ty.IsCollectionType() || ty.IsTupleType(): + l := val.LengthInt() + switch l { + case 0: + return "empty " + ty.FriendlyName() + case 1: + return ty.FriendlyName() + " with 1 element" + default: + return fmt.Sprintf("%s with %d elements", ty.FriendlyName(), l) + } + case ty.IsObjectType(): + atys := ty.AttributeTypes() + l := len(atys) + switch l { + case 0: + return "object with no attributes" + case 1: + var name string + for k := range atys { + name = k + } + return fmt.Sprintf("object with 1 attribute %q", name) + default: + return fmt.Sprintf("object with %d attributes", l) + } + default: + return ty.FriendlyName() + } +} + +// TraversalStr produces a representation of an HCL traversal that is compact, +// resembles HCL native syntax, and is suitable for display in the UI. +func TraversalStr(traversal hcl.Traversal) string { + // This is a specialized subset of traversal rendering tailored to + // producing helpful contextual messages in diagnostics. It is not + // comprehensive nor intended to be used for other purposes. + + var buf bytes.Buffer + for _, step := range traversal { + switch tStep := step.(type) { + case hcl.TraverseRoot: + buf.WriteString(tStep.Name) + case hcl.TraverseAttr: + buf.WriteByte('.') + buf.WriteString(tStep.Name) + case hcl.TraverseIndex: + buf.WriteByte('[') + if keyTy := tStep.Key.Type(); keyTy.IsPrimitiveType() { + buf.WriteString(CompactValueStr(tStep.Key)) + } else { + // We'll just use a placeholder for more complex values, + // since otherwise our result could grow ridiculously long. + buf.WriteString("...") + } + buf.WriteByte(']') + } + } + return buf.String() +} + +// FormatValueStr produces a JSON-compatible, human-readable representation of a +// cty.Value that is suitable for display in the UI. +// +// The full representation of the value is produced, but with some redaction to +// nodes within the value sensitive and ephemeral marks. +// e.g {"a": "10", "b": "password"} => {"a": "10", "b": "(sensitive value)"} +func FormatValueStr(val cty.Value) (string, error) { + var buf bytes.Buffer + + val, err := cty.Transform(val, func(path cty.Path, val cty.Value) (cty.Value, error) { + // If a value is sensitive or ephemeral or unknown, we redact it, otherwise + // we return the value as is. + if val.HasMark(marks.Sensitive) || val.HasMark(marks.Ephemeral) || !val.IsKnown() { + return cty.StringVal(CompactValueStr(val)), nil + } + return val, nil + }) + if err != nil { + return "", fmt.Errorf("unexpected error transforming value: %s", err) + } + + jsonVal, err := ctyjson.Marshal(val, val.Type()) + if err != nil { + return "", fmt.Errorf("unexpected error marshalling value: %s", err) + } + + // indent the JSON output for better readability + if err := json.Indent(&buf, jsonVal, "", " "); err != nil { + return "", fmt.Errorf("unexpected error formatting JSON: %s", err) + } + + return buf.String(), nil +} diff --git a/internal/tfdiags/format_test.go b/internal/tfdiags/format_test.go new file mode 100644 index 0000000000..b88e6b886b --- /dev/null +++ b/internal/tfdiags/format_test.go @@ -0,0 +1,110 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tfdiags + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/internal/lang/marks" + "github.com/zclconf/go-cty/cty" +) + +func TestCompactValueStr(t *testing.T) { + tests := []struct { + Val cty.Value + Want string + }{ + { + cty.NullVal(cty.DynamicPseudoType), + "null", + }, + { + cty.UnknownVal(cty.DynamicPseudoType), + "(not yet known)", + }, + { + cty.False, + "false", + }, + { + cty.True, + "true", + }, + { + cty.NumberIntVal(5), + "5", + }, + { + cty.NumberFloatVal(5.2), + "5.2", + }, + { + cty.StringVal(""), + `""`, + }, + { + cty.StringVal("hello"), + `"hello"`, + }, + { + cty.ListValEmpty(cty.String), + "empty list of string", + }, + { + cty.SetValEmpty(cty.String), + "empty set of string", + }, + { + cty.EmptyTupleVal, + "empty tuple", + }, + { + cty.MapValEmpty(cty.String), + "empty map of string", + }, + { + cty.EmptyObjectVal, + "object with no attributes", + }, + { + cty.ListVal([]cty.Value{cty.StringVal("a")}), + "list of string with 1 element", + }, + { + cty.ListVal([]cty.Value{cty.StringVal("a"), cty.StringVal("b")}), + "list of string with 2 elements", + }, + { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + }), + `object with 1 attribute "a"`, + }, + { + cty.ObjectVal(map[string]cty.Value{ + "a": cty.StringVal("b"), + "c": cty.StringVal("d"), + }), + "object with 2 attributes", + }, + { + cty.StringVal("a sensitive value").Mark(marks.Sensitive), + "(sensitive value)", + }, + { + cty.StringVal("an ephemeral value").Mark(marks.Ephemeral), + "(ephemeral value)", + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("%#v", test.Val), func(t *testing.T) { + got := CompactValueStr(test.Val) + if got != test.Want { + t.Errorf("wrong result\nvalue: %#v\ngot: %s\nwant: %s", test.Val, got, test.Want) + } + }) + } +} diff --git a/internal/tfdiags/hcl.go b/internal/tfdiags/hcl.go index edf16b5b4d..3531237477 100644 --- a/internal/tfdiags/hcl.go +++ b/internal/tfdiags/hcl.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( @@ -54,6 +57,49 @@ func (d hclDiagnostic) ExtraInfo() interface{} { return d.diag.Extra } +func (d hclDiagnostic) Equals(otherDiag ComparableDiagnostic) bool { + od, ok := otherDiag.(hclDiagnostic) + if !ok { + return false + } + if d.diag.Severity != od.diag.Severity { + return false + } + if d.diag.Summary != od.diag.Summary { + return false + } + if d.diag.Detail != od.diag.Detail { + return false + } + if !hclRangeEquals(d.diag.Subject, od.diag.Subject) { + return false + } + + // we can't compare extra values without knowing what they are + if d.ExtraInfo() != nil || od.ExtraInfo() != nil { + return false + } + + return true +} + +func hclRangeEquals(l, r *hcl.Range) bool { + if l == nil || r == nil { + return false + } + + if l.Filename != r.Filename { + return false + } + if l.Start.Byte != r.Start.Byte { + return false + } + if l.End.Byte != r.End.Byte { + return false + } + return true +} + // SourceRangeFromHCL constructs a SourceRange from the corresponding range // type within the HCL package. func SourceRangeFromHCL(hclRange hcl.Range) SourceRange { diff --git a/internal/tfdiags/hcl_test.go b/internal/tfdiags/hcl_test.go index 784562f9bd..c4ed287d76 100644 --- a/internal/tfdiags/hcl_test.go +++ b/internal/tfdiags/hcl_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( diff --git a/internal/tfdiags/object.go b/internal/tfdiags/object.go new file mode 100644 index 0000000000..6e46d3480a --- /dev/null +++ b/internal/tfdiags/object.go @@ -0,0 +1,77 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package tfdiags + +import ( + "fmt" + "strings" + + "github.com/zclconf/go-cty/cty" +) + +// ObjectToString is a helper function that converts a go-cty object to a string representation +func ObjectToString(obj cty.Value) string { + if obj.IsNull() { + return "" + } + + if !obj.IsWhollyKnown() { + return "" + } + + if obj.Type().IsObjectType() && len(obj.Type().AttributeTypes()) == 0 { + return "" + } + + if obj.Type().IsObjectType() { + result := "" + it := obj.ElementIterator() + for it.Next() { + key, val := it.Element() + keyStr := key.AsString() + + if result != "" { + result += "," + } + + if val.IsNull() { + result += fmt.Sprintf("%s=", keyStr) + continue + } + + switch val.Type() { + case cty.Bool: + result += fmt.Sprintf("%s=%t", keyStr, val.True()) + case cty.Number: + result += fmt.Sprintf("%s=%s", keyStr, val.AsBigFloat().String()) + case cty.String: + result += fmt.Sprintf("%s=%s", keyStr, val.AsString()) + case cty.List(cty.Bool): + elements := val.AsValueSlice() + parts := make([]string, len(elements)) + for i, element := range elements { + parts[i] = fmt.Sprintf("%t", element.True()) + } + result += fmt.Sprintf("%s=[%s]", keyStr, strings.Join(parts, ",")) + case cty.List(cty.Number): + elements := val.AsValueSlice() + parts := make([]string, len(elements)) + for i, element := range elements { + parts[i] = element.AsBigFloat().String() + } + result += fmt.Sprintf("%s=[%s]", keyStr, strings.Join(parts, ",")) + case cty.List(cty.String): + elements := val.AsValueSlice() + parts := make([]string, len(elements)) + for i, element := range elements { + parts[i] = element.AsString() + } + result += fmt.Sprintf("%s=[%s]", keyStr, strings.Join(parts, ",")) + } + } + + return result + } + + panic("not an object") +} diff --git a/internal/tfdiags/object_test.go b/internal/tfdiags/object_test.go new file mode 100644 index 0000000000..926617eb98 --- /dev/null +++ b/internal/tfdiags/object_test.go @@ -0,0 +1,72 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package tfdiags + +import ( + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func Test_ObjectToString(t *testing.T) { + testCases := []struct { + name string + value cty.Value + expected string + }{ + { + name: "null", + value: cty.NullVal(cty.Object(map[string]cty.Type{})), + expected: "", + }, + { + name: "unknown", + value: cty.UnknownVal(cty.Object(map[string]cty.Type{})), + expected: "", + }, + { + name: "empty", + value: cty.EmptyObjectVal, + expected: "", + }, + { + name: "primitive", + value: cty.ObjectVal(map[string]cty.Value{ + "number": cty.NumberIntVal(42), + "string": cty.StringVal("hello"), + "bool": cty.BoolVal(true), + }), + expected: "bool=true,number=42,string=hello", + }, + { + name: "list", + value: cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "list": cty.ListVal([]cty.Value{ + cty.StringVal("a"), + cty.StringVal("b"), + cty.StringVal("c"), + }), + }), + expected: "list=[a,b,c],string=hello", + }, + { + name: "with null value", + value: cty.ObjectVal(map[string]cty.Value{ + "string": cty.StringVal("hello"), + "null": cty.NullVal(cty.String), + }), + expected: "null=,string=hello", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + actual := ObjectToString(tc.value) + + if actual != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, actual) + } + }) + } +} diff --git a/internal/tfdiags/override.go b/internal/tfdiags/override.go new file mode 100644 index 0000000000..f88bc6ff30 --- /dev/null +++ b/internal/tfdiags/override.go @@ -0,0 +1,75 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tfdiags + +// overriddenDiagnostic implements the Diagnostic interface by wrapping another +// Diagnostic while overriding the severity of the original Diagnostic. +type overriddenDiagnostic struct { + original Diagnostic + severity Severity + extra interface{} +} + +var _ Diagnostic = overriddenDiagnostic{} + +// OverrideAll accepts a set of Diagnostics and wraps them with a new severity +// and, optionally, a new ExtraInfo. +func OverrideAll(originals Diagnostics, severity Severity, createExtra func() DiagnosticExtraWrapper) Diagnostics { + var diags Diagnostics + for _, diag := range originals { + diags = diags.Append(Override(diag, severity, createExtra)) + } + return diags +} + +// Override matches OverrideAll except it operates over a single Diagnostic +// rather than multiple Diagnostics. +func Override(original Diagnostic, severity Severity, createExtra func() DiagnosticExtraWrapper) Diagnostic { + extra := original.ExtraInfo() + if createExtra != nil { + nw := createExtra() + nw.WrapDiagnosticExtra(extra) + extra = nw + } + + return overriddenDiagnostic{ + original: original, + severity: severity, + extra: extra, + } +} + +// UndoOverride will return the original diagnostic that was overridden within +// the OverrideAll function. +// +// If the provided Diagnostic was never overridden then it is simply returned +// unchanged. +func UndoOverride(diag Diagnostic) Diagnostic { + if override, ok := diag.(overriddenDiagnostic); ok { + return override.original + } + + // Then it wasn't overridden, so we'll just return the diag unchanged. + return diag +} + +func (o overriddenDiagnostic) Severity() Severity { + return o.severity +} + +func (o overriddenDiagnostic) Description() Description { + return o.original.Description() +} + +func (o overriddenDiagnostic) Source() Source { + return o.original.Source() +} + +func (o overriddenDiagnostic) FromExpr() *FromExpr { + return o.original.FromExpr() +} + +func (o overriddenDiagnostic) ExtraInfo() interface{} { + return o.extra +} diff --git a/internal/tfdiags/override_test.go b/internal/tfdiags/override_test.go new file mode 100644 index 0000000000..046c5960bd --- /dev/null +++ b/internal/tfdiags/override_test.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package tfdiags + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" +) + +func TestOverride_UpdatesSeverity(t *testing.T) { + original := Sourceless(Error, "summary", "detail") + override := Override(original, Warning, nil) + + if override.Severity() != Warning { + t.Errorf("expected warning but was %s", override.Severity()) + } +} + +func TestOverride_MaintainsExtra(t *testing.T) { + original := hclDiagnostic{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "summary", + Detail: "detail", + Extra: "extra", + }} + override := Override(original, Warning, nil) + + if override.ExtraInfo().(string) != "extra" { + t.Errorf("invalid extra info %v", override.ExtraInfo()) + } +} + +func TestOverride_WrapsExtra(t *testing.T) { + original := hclDiagnostic{&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "summary", + Detail: "detail", + Extra: "extra", + }} + override := Override(original, Warning, func() DiagnosticExtraWrapper { + return &extraWrapper{ + mine: "mine", + } + }) + + wrapper := override.ExtraInfo().(*extraWrapper) + if wrapper.mine != "mine" { + t.Errorf("invalid extra info %v", override.ExtraInfo()) + } + if wrapper.original.(string) != "extra" { + t.Errorf("invalid wrapped extra info %v", override.ExtraInfo()) + } +} + +func TestUndoOverride(t *testing.T) { + original := Sourceless(Error, "summary", "detail") + override := Override(original, Warning, nil) + restored := UndoOverride(override) + + if restored.Severity() != Error { + t.Errorf("expected warning but was %s", restored.Severity()) + } +} + +func TestUndoOverride_NotOverridden(t *testing.T) { + original := Sourceless(Error, "summary", "detail") + restored := UndoOverride(original) // Shouldn't do anything bad. + + if restored.Severity() != Error { + t.Errorf("expected warning but was %s", restored.Severity()) + } +} + +type extraWrapper struct { + mine string + original interface{} +} + +func (e *extraWrapper) WrapDiagnosticExtra(inner interface{}) { + e.original = inner +} diff --git a/internal/tfdiags/rpc_friendly.go b/internal/tfdiags/rpc_friendly.go index 4c627bf98a..223a21aa60 100644 --- a/internal/tfdiags/rpc_friendly.go +++ b/internal/tfdiags/rpc_friendly.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( @@ -48,6 +51,27 @@ func (d *rpcFriendlyDiag) Source() Source { } } +func (d *rpcFriendlyDiag) Equals(otherDiag ComparableDiagnostic) bool { + od, ok := otherDiag.(*rpcFriendlyDiag) + if !ok { + return false + } + if d.Severity_ != od.Severity_ { + return false + } + if d.Summary_ != od.Summary_ { + return false + } + if d.Detail_ != od.Detail_ { + return false + } + if !sourceRangeEquals(d.Subject_, od.Subject_) { + return false + } + + return true +} + func (d rpcFriendlyDiag) FromExpr() *FromExpr { // RPC-friendly diagnostics cannot preserve expression information because // expressions themselves are not RPC-friendly. diff --git a/internal/tfdiags/rpc_friendly_test.go b/internal/tfdiags/rpc_friendly_test.go index bf51707539..15810f6d97 100644 --- a/internal/tfdiags/rpc_friendly_test.go +++ b/internal/tfdiags/rpc_friendly_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( diff --git a/internal/tfdiags/simple_warning.go b/internal/tfdiags/simple_warning.go index 3c18f19247..7ecc5d35b3 100644 --- a/internal/tfdiags/simple_warning.go +++ b/internal/tfdiags/simple_warning.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags type simpleWarning string diff --git a/internal/tfdiags/source_range.go b/internal/tfdiags/source_range.go index 3031168d6a..7e99cc318a 100644 --- a/internal/tfdiags/source_range.go +++ b/internal/tfdiags/source_range.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags import ( diff --git a/internal/tfdiags/sourceless.go b/internal/tfdiags/sourceless.go index eaa27373db..b820f97ac7 100644 --- a/internal/tfdiags/sourceless.go +++ b/internal/tfdiags/sourceless.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package tfdiags // Sourceless creates and returns a diagnostic with no source location diff --git a/internal/tfdiags/testing.go b/internal/tfdiags/testing.go new file mode 100644 index 0000000000..0d2bb4d10f --- /dev/null +++ b/internal/tfdiags/testing.go @@ -0,0 +1,124 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package tfdiags + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +// AssertDiagnosticsMatch fails the test in progress (using t.Fatal) if the +// two sets of diagnostics don't match after being normalized using the +// "ForRPC" processing step, which eliminates the specific type information +// and HCL expression information of each diagnostic. +// +// AssertDiagnosticsMatch sorts the two sets of diagnostics in the usual way +// before comparing them, though diagnostics only have a partial order so that +// will not totally normalize the ordering of all diagnostics sets. +func AssertDiagnosticsMatch(t *testing.T, got, want Diagnostics) { + t.Helper() + + if diff := assertDiagnosticsMatch(got, want); diff != "" { + t.Fatalf("unexpected diagnostics difference:\n%s", diff) + } +} + +func assertDiagnosticsMatch(got, want Diagnostics) string { + got = got.ForRPC() + want = want.ForRPC() + + got.Sort() + want.Sort() + + return cmp.Diff(want, got, DiagnosticComparer) +} + +// AssertDiagnosticMatch fails the test in progress (using t.Fatal) if the +// two (singular) diagnostics don't match after being normalized to an +// "RPC-friendly" diagnostic, which eliminates the specific type information +// and HCL expression information of each diagnostic. +func AssertDiagnosticMatch(t *testing.T, got, want Diagnostic) { + t.Helper() + + if diff := assertDiagnosticMatch(want, got); diff != "" { + t.Fatalf("unexpected diagnostics difference:\n%s", diff) + } +} + +func assertDiagnosticMatch(got, want Diagnostic) string { + + got = makeRPCFriendlyDiag(got) + want = makeRPCFriendlyDiag(want) + + return cmp.Diff(want, got, DiagnosticComparer) +} + +// AssertNoDiagnostics will fail a test if any diagnostics are present. +// If diagnostics are present, they will each be logged. +func AssertNoDiagnostics(t *testing.T, diags Diagnostics) { + t.Helper() + AssertDiagnosticCount(t, diags, 0) +} + +// AssertDiagnosticCount will fail a test if the number of diagnostics present +// doesn't match the expected number. +// If an incorrect number of diagnostics are present, they will each be logged. +func AssertDiagnosticCount(t *testing.T, diags Diagnostics, want int) { + t.Helper() + if len(diags) != want { + t.Errorf("wrong number of diagnostics %d; want %d", len(diags), want) + for _, diag := range diags { + t.Logf("- %#v", diag) + } + t.FailNow() + } +} + +// tfdiags.AssertNoDiagnostics fails the test in progress (using t.FailNow) if the given +// diagnostics has any errors. +func AssertNoErrors(t *testing.T, diags Diagnostics) { + t.Helper() + if !diags.HasErrors() { + return + } + LogDiagnostics(t, diags) + t.FailNow() +} + +// LogDiagnostics is a test helper that logs the given diagnostics to to the +// given testing.T using t.Log, in a way that is hopefully useful in debugging +// a test. It does not generate any errors or fail the test. See +// tfdiags.AssertNoDiagnostics and tfdiags.AssertNoErrors for more specific helpers that can +// also fail the test. +func LogDiagnostics(t *testing.T, diags Diagnostics) { + t.Helper() + for _, diag := range diags { + desc := diag.Description() + rng := diag.Source() + + var severity string + switch diag.Severity() { + case Error: + severity = "ERROR" + case Warning: + severity = "WARN" + default: + severity = "???" // should never happen + } + + if subj := rng.Subject; subj != nil { + if desc.Detail == "" { + t.Logf("[%s@%s] %s", severity, subj.StartString(), desc.Summary) + } else { + t.Logf("[%s@%s] %s: %s", severity, subj.StartString(), desc.Summary, desc.Detail) + } + } else { + if desc.Detail == "" { + t.Logf("[%s] %s", severity, desc.Summary) + } else { + t.Logf("[%s] %s: %s", severity, desc.Summary, desc.Detail) + } + } + } +} diff --git a/internal/tfdiags/testing_test.go b/internal/tfdiags/testing_test.go new file mode 100644 index 0000000000..9291fd4a2d --- /dev/null +++ b/internal/tfdiags/testing_test.go @@ -0,0 +1,101 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 +package tfdiags + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" +) + +// These tests are to ensure that the normalisation of the diagnostics' concrete +// types doesn't impact how the diagnostics are compared. +// +// Full tests of the comparison logic in DiagnosticComparer and +// DiagnosticComparerWithSource are in compare_test.go + +func Test_assertDiagnosticMatch_differentConcreteTypes(t *testing.T) { + baseError := hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error", + Detail: "this is an error", + } + + cases := map[string]struct { + diag1 Diagnostic + diag2 Diagnostic + expectDiff bool + }{ + "diagnostics match but are different concrete types": { + expectDiff: false, + diag1: hclDiagnostic{&baseError}, + diag2: makeRPCFriendlyDiag(hclDiagnostic{&baseError}), + }, + "diagnostics don't match and are different concrete types": { + expectDiff: true, + diag1: hclDiagnostic{&baseError}, + diag2: func() Diagnostic { + d := baseError + d.Severity = hcl.DiagWarning // Altered severity level + return makeRPCFriendlyDiag(hclDiagnostic{&d}) + }(), + }, + } + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + // This should show no diff as, internally, the two diags are transformed into the same + // concrete type + diff := assertDiagnosticMatch(tc.diag1, tc.diag2) + + if !tc.expectDiff && len(diff) > 0 { + t.Fatalf("unexpected diff:\n%s", diff) + } + if tc.expectDiff && len(diff) == 0 { + t.Fatalf("expected a diff but got none") + } + }) + } +} + +func Test_assertDiagnosticsMatch_differentConcreteTypes(t *testing.T) { + baseError := hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "error", + Detail: "this is an error", + } + + cases := map[string]struct { + diags1 Diagnostics + diags2 Diagnostics + expectDiff bool + }{ + "diagnostics match but are different concrete types": { + expectDiff: false, + diags1: Diagnostics{hclDiagnostic{&baseError}}, + diags2: Diagnostics{makeRPCFriendlyDiag(hclDiagnostic{&baseError})}, + }, + "diagnostics don't match and are different concrete types": { + expectDiff: true, + diags1: Diagnostics{hclDiagnostic{&baseError}}, + diags2: func() Diagnostics { + d := baseError + d.Severity = hcl.DiagWarning // Altered severity level + return Diagnostics{makeRPCFriendlyDiag(hclDiagnostic{&d})} + }(), + }, + } + for tn, tc := range cases { + t.Run(tn, func(t *testing.T) { + // This should show no diff as, internally, the two diags are transformed into the same + // concrete type + diff := assertDiagnosticsMatch(tc.diags1, tc.diags2) + + if !tc.expectDiff && len(diff) > 0 { + t.Fatalf("unexpected diff:\n%s", diff) + } + if tc.expectDiff && len(diff) == 0 { + t.Fatalf("expected a diff but got none") + } + }) + } +} diff --git a/internal/tfplugin5/.copywrite.hcl b/internal/tfplugin5/.copywrite.hcl new file mode 100644 index 0000000000..2c144ddb7d --- /dev/null +++ b/internal/tfplugin5/.copywrite.hcl @@ -0,0 +1,6 @@ +schema_version = 1 + +project { + license = "MPL-2.0" + copyright_year = 2024 +} diff --git a/internal/tfplugin5/LICENSE b/internal/tfplugin5/LICENSE new file mode 100644 index 0000000000..e25da5fad9 --- /dev/null +++ b/internal/tfplugin5/LICENSE @@ -0,0 +1,355 @@ +Copyright (c) 2014 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. diff --git a/internal/tfplugin5/tfplugin5.pb.go b/internal/tfplugin5/tfplugin5.pb.go index 85ab54eab6..8dfe1f1b24 100644 --- a/internal/tfplugin5/tfplugin5.pb.go +++ b/internal/tfplugin5/tfplugin5.pb.go @@ -1,13 +1,15 @@ -// Terraform Plugin RPC protocol version 5.3 +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Terraform Plugin RPC protocol version 5.9 // -// This file defines version 5.3 of the RPC protocol. To implement a plugin +// This file defines version 5.9 of the RPC protocol. To implement a plugin // against this protocol, copy this definition into your own codebase and // use protoc to generate stubs for your target language. // -// This file will not be updated. Any minor versions of protocol 5 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. +// Any minor versions of protocol 5 to follow should modify this file while +// maintaining backwards compatibility. Breaking changes, if any are required, +// will come in a subsequent major version with its own separate proto definition. // // Note that only the proto files included in a release tag of Terraform are // official protocol releases. Proto files taken from other commits may include @@ -19,7 +21,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 +// protoc-gen-go v1.36.5 // protoc v3.15.6 // source: tfplugin5.proto @@ -32,8 +34,10 @@ import ( status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -193,27 +197,83 @@ func (x Schema_NestedBlock_NestingMode) Number() protoreflect.EnumNumber { // Deprecated: Use Schema_NestedBlock_NestingMode.Descriptor instead. func (Schema_NestedBlock_NestingMode) EnumDescriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{5, 2, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{8, 2, 0} +} + +// Reason is the reason for deferring the change. +type Deferred_Reason int32 + +const ( + // UNKNOWN is the default value, and should not be used. + Deferred_UNKNOWN Deferred_Reason = 0 + // RESOURCE_CONFIG_UNKNOWN is used when the config is partially unknown and the real + // values need to be known before the change can be planned. + Deferred_RESOURCE_CONFIG_UNKNOWN Deferred_Reason = 1 + // PROVIDER_CONFIG_UNKNOWN is used when parts of the provider configuration + // are unknown, e.g. the provider configuration is only known after the apply is done. + Deferred_PROVIDER_CONFIG_UNKNOWN Deferred_Reason = 2 + // ABSENT_PREREQ is used when a hard dependency has not been satisfied. + Deferred_ABSENT_PREREQ Deferred_Reason = 3 +) + +// Enum value maps for Deferred_Reason. +var ( + Deferred_Reason_name = map[int32]string{ + 0: "UNKNOWN", + 1: "RESOURCE_CONFIG_UNKNOWN", + 2: "PROVIDER_CONFIG_UNKNOWN", + 3: "ABSENT_PREREQ", + } + Deferred_Reason_value = map[string]int32{ + "UNKNOWN": 0, + "RESOURCE_CONFIG_UNKNOWN": 1, + "PROVIDER_CONFIG_UNKNOWN": 2, + "ABSENT_PREREQ": 3, + } +) + +func (x Deferred_Reason) Enum() *Deferred_Reason { + p := new(Deferred_Reason) + *p = x + return p +} + +func (x Deferred_Reason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Deferred_Reason) Descriptor() protoreflect.EnumDescriptor { + return file_tfplugin5_proto_enumTypes[3].Descriptor() +} + +func (Deferred_Reason) Type() protoreflect.EnumType { + return &file_tfplugin5_proto_enumTypes[3] +} + +func (x Deferred_Reason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Deferred_Reason.Descriptor instead. +func (Deferred_Reason) EnumDescriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{12, 0} } // DynamicValue is an opaque encoding of terraform data, with the field name // indicating the encoding scheme used. type DynamicValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` + Json []byte `protobuf:"bytes,2,opt,name=json,proto3" json:"json,omitempty"` unknownFields protoimpl.UnknownFields - - Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` - Json []byte `protobuf:"bytes,2,opt,name=json,proto3" json:"json,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DynamicValue) Reset() { *x = DynamicValue{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DynamicValue) String() string { @@ -224,7 +284,7 @@ func (*DynamicValue) ProtoMessage() {} func (x *DynamicValue) ProtoReflect() protoreflect.Message { mi := &file_tfplugin5_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -254,23 +314,20 @@ func (x *DynamicValue) GetJson() []byte { } type Diagnostic struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Severity Diagnostic_Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=tfplugin5.Diagnostic_Severity" json:"severity,omitempty"` + Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` + Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` + Attribute *AttributePath `protobuf:"bytes,4,opt,name=attribute,proto3" json:"attribute,omitempty"` unknownFields protoimpl.UnknownFields - - Severity Diagnostic_Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=tfplugin5.Diagnostic_Severity" json:"severity,omitempty"` - Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` - Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` - Attribute *AttributePath `protobuf:"bytes,4,opt,name=attribute,proto3" json:"attribute,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Diagnostic) Reset() { *x = Diagnostic{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Diagnostic) String() string { @@ -281,7 +338,7 @@ func (*Diagnostic) ProtoMessage() {} func (x *Diagnostic) ProtoReflect() protoreflect.Message { mi := &file_tfplugin5_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -324,21 +381,72 @@ func (x *Diagnostic) GetAttribute() *AttributePath { return nil } -type AttributePath struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +type FunctionError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + // The optional function_argument records the index position of the + // argument which caused the error. + FunctionArgument *int64 `protobuf:"varint,2,opt,name=function_argument,json=functionArgument,proto3,oneof" json:"function_argument,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - Steps []*AttributePath_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` +func (x *FunctionError) Reset() { + *x = FunctionError{} + mi := &file_tfplugin5_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FunctionError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionError) ProtoMessage() {} + +func (x *FunctionError) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FunctionError.ProtoReflect.Descriptor instead. +func (*FunctionError) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{2} +} + +func (x *FunctionError) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *FunctionError) GetFunctionArgument() int64 { + if x != nil && x.FunctionArgument != nil { + return *x.FunctionArgument + } + return 0 +} + +type AttributePath struct { + state protoimpl.MessageState `protogen:"open.v1"` + Steps []*AttributePath_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttributePath) Reset() { *x = AttributePath{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AttributePath) String() string { @@ -348,8 +456,8 @@ func (x *AttributePath) String() string { func (*AttributePath) ProtoMessage() {} func (x *AttributePath) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[3] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -361,7 +469,7 @@ func (x *AttributePath) ProtoReflect() protoreflect.Message { // Deprecated: Use AttributePath.ProtoReflect.Descriptor instead. func (*AttributePath) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{2} + return file_tfplugin5_proto_rawDescGZIP(), []int{3} } func (x *AttributePath) GetSteps() []*AttributePath_Step { @@ -372,18 +480,16 @@ func (x *AttributePath) GetSteps() []*AttributePath_Step { } type Stop struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Stop) Reset() { *x = Stop{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Stop) String() string { @@ -393,8 +499,8 @@ func (x *Stop) String() string { func (*Stop) ProtoMessage() {} func (x *Stop) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[4] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -406,28 +512,25 @@ func (x *Stop) ProtoReflect() protoreflect.Message { // Deprecated: Use Stop.ProtoReflect.Descriptor instead. func (*Stop) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{3} + return file_tfplugin5_proto_rawDescGZIP(), []int{4} } // RawState holds the stored state for a resource to be upgraded by the // provider. It can be in one of two formats, the current json encoded format // in bytes, or the legacy flatmap format as a map of strings. type RawState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Json []byte `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"` + Flatmap map[string]string `protobuf:"bytes,2,rep,name=flatmap,proto3" json:"flatmap,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Json []byte `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"` - Flatmap map[string]string `protobuf:"bytes,2,rep,name=flatmap,proto3" json:"flatmap,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *RawState) Reset() { *x = RawState{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawState) String() string { @@ -437,8 +540,8 @@ func (x *RawState) String() string { func (*RawState) ProtoMessage() {} func (x *RawState) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[5] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -450,7 +553,7 @@ func (x *RawState) ProtoReflect() protoreflect.Message { // Deprecated: Use RawState.ProtoReflect.Descriptor instead. func (*RawState) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{4} + return file_tfplugin5_proto_rawDescGZIP(), []int{5} } func (x *RawState) GetJson() []byte { @@ -467,27 +570,145 @@ func (x *RawState) GetFlatmap() map[string]string { return nil } +// ResourceIdentitySchema represents the structure and types of data used to identify +// a managed resource type. Effectively, resource identity is a versioned object +// that can be used to compare resources, whether already managed and/or being +// discovered. +type ResourceIdentitySchema struct { + state protoimpl.MessageState `protogen:"open.v1"` + // version is the identity version and separate from the Schema version. + // Any time the structure or format of identity_attributes changes, this version + // should be incremented. Versioning implicitly starts at 0 and by convention + // should be incremented by 1 each change. + // + // When comparing identity_attributes data, differing versions should always be treated + // as inequal. + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // identity_attributes are the individual value definitions which define identity data + // for a managed resource type. This information is used to decode DynamicValue of + // identity data. + // + // These attributes are intended for permanent identity data and must be wholly + // representative of all data necessary to compare two managed resource instances + // with no other data. This generally should include account, endpoint, location, + // and automatically generated identifiers. For some resources, this may include + // configuration-based data, such as a required name which must be unique. + IdentityAttributes []*ResourceIdentitySchema_IdentityAttribute `protobuf:"bytes,2,rep,name=identity_attributes,json=identityAttributes,proto3" json:"identity_attributes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceIdentitySchema) Reset() { + *x = ResourceIdentitySchema{} + mi := &file_tfplugin5_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceIdentitySchema) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceIdentitySchema) ProtoMessage() {} + +func (x *ResourceIdentitySchema) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceIdentitySchema.ProtoReflect.Descriptor instead. +func (*ResourceIdentitySchema) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{6} +} + +func (x *ResourceIdentitySchema) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ResourceIdentitySchema) GetIdentityAttributes() []*ResourceIdentitySchema_IdentityAttribute { + if x != nil { + return x.IdentityAttributes + } + return nil +} + +type ResourceIdentityData struct { + state protoimpl.MessageState `protogen:"open.v1"` + // identity_data is the resource identity data for the given definition. It should + // be decoded using the identity schema. + // + // This data is considered permanent for the identity version and suitable for + // longer-term storage. + IdentityData *DynamicValue `protobuf:"bytes,1,opt,name=identity_data,json=identityData,proto3" json:"identity_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceIdentityData) Reset() { + *x = ResourceIdentityData{} + mi := &file_tfplugin5_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceIdentityData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceIdentityData) ProtoMessage() {} + +func (x *ResourceIdentityData) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceIdentityData.ProtoReflect.Descriptor instead. +func (*ResourceIdentityData) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{7} +} + +func (x *ResourceIdentityData) GetIdentityData() *DynamicValue { + if x != nil { + return x.IdentityData + } + return nil +} + // Schema is the configuration schema for a Resource, Provider, or Provisioner. type Schema struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The version of the schema. // Schemas are versioned, so that providers can upgrade a saved resource // state when the schema is changed. Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` // Block is the top level configuration block for this schema. - Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema) Reset() { *x = Schema{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema) String() string { @@ -497,8 +718,8 @@ func (x *Schema) String() string { func (*Schema) ProtoMessage() {} func (x *Schema) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[8] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -510,7 +731,7 @@ func (x *Schema) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema.ProtoReflect.Descriptor instead. func (*Schema) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{5} + return file_tfplugin5_proto_rawDescGZIP(), []int{8} } func (x *Schema) GetVersion() int64 { @@ -527,19 +748,333 @@ func (x *Schema) GetBlock() *Schema_Block { return nil } -type GetProviderSchema struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// ServerCapabilities allows providers to communicate extra information +// regarding supported protocol features. This is used to indicate +// availability of certain forward-compatible changes which may be optional +// in a major protocol version, but cannot be tested for directly. +type ServerCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The plan_destroy capability signals that a provider expects a call + // to PlanResourceChange when a resource is going to be destroyed. + PlanDestroy bool `protobuf:"varint,1,opt,name=plan_destroy,json=planDestroy,proto3" json:"plan_destroy,omitempty"` + // The get_provider_schema_optional capability indicates that this + // provider does not require calling GetProviderSchema to operate + // normally, and the caller can used a cached copy of the provider's + // schema. + GetProviderSchemaOptional bool `protobuf:"varint,2,opt,name=get_provider_schema_optional,json=getProviderSchemaOptional,proto3" json:"get_provider_schema_optional,omitempty"` + // The move_resource_state capability signals that a provider supports the + // MoveResourceState RPC. + MoveResourceState bool `protobuf:"varint,3,opt,name=move_resource_state,json=moveResourceState,proto3" json:"move_resource_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerCapabilities) Reset() { + *x = ServerCapabilities{} + mi := &file_tfplugin5_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerCapabilities) ProtoMessage() {} + +func (x *ServerCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerCapabilities.ProtoReflect.Descriptor instead. +func (*ServerCapabilities) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{9} +} + +func (x *ServerCapabilities) GetPlanDestroy() bool { + if x != nil { + return x.PlanDestroy + } + return false +} + +func (x *ServerCapabilities) GetGetProviderSchemaOptional() bool { + if x != nil { + return x.GetProviderSchemaOptional + } + return false +} + +func (x *ServerCapabilities) GetMoveResourceState() bool { + if x != nil { + return x.MoveResourceState + } + return false +} + +// ClientCapabilities allows Terraform to publish information regarding +// supported protocol features. This is used to indicate availability of +// certain forward-compatible changes which may be optional in a major +// protocol version, but cannot be tested for directly. +type ClientCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The deferral_allowed capability signals that the client is able to + // handle deferred responses from the provider. + DeferralAllowed bool `protobuf:"varint,1,opt,name=deferral_allowed,json=deferralAllowed,proto3" json:"deferral_allowed,omitempty"` + // The write_only_attributes_allowed capability signals that the client + // is able to handle write_only attributes for managed resources. + WriteOnlyAttributesAllowed bool `protobuf:"varint,2,opt,name=write_only_attributes_allowed,json=writeOnlyAttributesAllowed,proto3" json:"write_only_attributes_allowed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientCapabilities) Reset() { + *x = ClientCapabilities{} + mi := &file_tfplugin5_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientCapabilities) ProtoMessage() {} + +func (x *ClientCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientCapabilities.ProtoReflect.Descriptor instead. +func (*ClientCapabilities) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{10} +} + +func (x *ClientCapabilities) GetDeferralAllowed() bool { + if x != nil { + return x.DeferralAllowed + } + return false +} + +func (x *ClientCapabilities) GetWriteOnlyAttributesAllowed() bool { + if x != nil { + return x.WriteOnlyAttributesAllowed + } + return false +} + +type Function struct { + state protoimpl.MessageState `protogen:"open.v1"` + // parameters is the ordered list of positional function parameters. + Parameters []*Function_Parameter `protobuf:"bytes,1,rep,name=parameters,proto3" json:"parameters,omitempty"` + // variadic_parameter is an optional final parameter which accepts + // zero or more argument values, in which Terraform will send an + // ordered list of the parameter type. + VariadicParameter *Function_Parameter `protobuf:"bytes,2,opt,name=variadic_parameter,json=variadicParameter,proto3" json:"variadic_parameter,omitempty"` + // Return is the function return parameter. + Return *Function_Return `protobuf:"bytes,3,opt,name=return,proto3" json:"return,omitempty"` + // summary is the human-readable shortened documentation for the function. + Summary string `protobuf:"bytes,4,opt,name=summary,proto3" json:"summary,omitempty"` + // description is human-readable documentation for the function. + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // description_kind is the formatting of the description. + DescriptionKind StringKind `protobuf:"varint,6,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin5.StringKind" json:"description_kind,omitempty"` + // deprecation_message is human-readable documentation if the + // function is deprecated. + DeprecationMessage string `protobuf:"bytes,7,opt,name=deprecation_message,json=deprecationMessage,proto3" json:"deprecation_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Function) Reset() { + *x = Function{} + mi := &file_tfplugin5_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Function) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Function) ProtoMessage() {} + +func (x *Function) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Function.ProtoReflect.Descriptor instead. +func (*Function) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{11} +} + +func (x *Function) GetParameters() []*Function_Parameter { + if x != nil { + return x.Parameters + } + return nil +} + +func (x *Function) GetVariadicParameter() *Function_Parameter { + if x != nil { + return x.VariadicParameter + } + return nil +} + +func (x *Function) GetReturn() *Function_Return { + if x != nil { + return x.Return + } + return nil +} + +func (x *Function) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *Function) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Function) GetDescriptionKind() StringKind { + if x != nil { + return x.DescriptionKind + } + return StringKind_PLAIN +} + +func (x *Function) GetDeprecationMessage() string { + if x != nil { + return x.DeprecationMessage + } + return "" +} + +// Deferred is a message that indicates that change is deferred for a reason. +type Deferred struct { + state protoimpl.MessageState `protogen:"open.v1"` + // reason is the reason for deferring the change. + Reason Deferred_Reason `protobuf:"varint,1,opt,name=reason,proto3,enum=tfplugin5.Deferred_Reason" json:"reason,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Deferred) Reset() { + *x = Deferred{} + mi := &file_tfplugin5_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Deferred) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Deferred) ProtoMessage() {} + +func (x *Deferred) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Deferred.ProtoReflect.Descriptor instead. +func (*Deferred) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{12} +} + +func (x *Deferred) GetReason() Deferred_Reason { + if x != nil { + return x.Reason + } + return Deferred_UNKNOWN +} + +type GetMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata) Reset() { + *x = GetMetadata{} + mi := &file_tfplugin5_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata) ProtoMessage() {} + +func (x *GetMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13} +} + +type GetProviderSchema struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderSchema) Reset() { *x = GetProviderSchema{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProviderSchema) String() string { @@ -549,8 +1084,8 @@ func (x *GetProviderSchema) String() string { func (*GetProviderSchema) ProtoMessage() {} func (x *GetProviderSchema) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[14] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -562,22 +1097,20 @@ func (x *GetProviderSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderSchema.ProtoReflect.Descriptor instead. func (*GetProviderSchema) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{6} + return file_tfplugin5_proto_rawDescGZIP(), []int{14} } type PrepareProviderConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PrepareProviderConfig) Reset() { *x = PrepareProviderConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PrepareProviderConfig) String() string { @@ -587,8 +1120,8 @@ func (x *PrepareProviderConfig) String() string { func (*PrepareProviderConfig) ProtoMessage() {} func (x *PrepareProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[15] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -600,22 +1133,20 @@ func (x *PrepareProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use PrepareProviderConfig.ProtoReflect.Descriptor instead. func (*PrepareProviderConfig) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{7} + return file_tfplugin5_proto_rawDescGZIP(), []int{15} } type UpgradeResourceState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpgradeResourceState) Reset() { *x = UpgradeResourceState{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpgradeResourceState) String() string { @@ -625,8 +1156,8 @@ func (x *UpgradeResourceState) String() string { func (*UpgradeResourceState) ProtoMessage() {} func (x *UpgradeResourceState) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[16] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -638,22 +1169,92 @@ func (x *UpgradeResourceState) ProtoReflect() protoreflect.Message { // Deprecated: Use UpgradeResourceState.ProtoReflect.Descriptor instead. func (*UpgradeResourceState) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{8} + return file_tfplugin5_proto_rawDescGZIP(), []int{16} +} + +type GetResourceIdentitySchemas struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResourceIdentitySchemas) Reset() { + *x = GetResourceIdentitySchemas{} + mi := &file_tfplugin5_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceIdentitySchemas) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceIdentitySchemas) ProtoMessage() {} + +func (x *GetResourceIdentitySchemas) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceIdentitySchemas.ProtoReflect.Descriptor instead. +func (*GetResourceIdentitySchemas) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{17} +} + +type UpgradeResourceIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpgradeResourceIdentity) Reset() { + *x = UpgradeResourceIdentity{} + mi := &file_tfplugin5_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpgradeResourceIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpgradeResourceIdentity) ProtoMessage() {} + +func (x *UpgradeResourceIdentity) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpgradeResourceIdentity.ProtoReflect.Descriptor instead. +func (*UpgradeResourceIdentity) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{18} } type ValidateResourceTypeConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateResourceTypeConfig) Reset() { *x = ValidateResourceTypeConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateResourceTypeConfig) String() string { @@ -663,8 +1264,8 @@ func (x *ValidateResourceTypeConfig) String() string { func (*ValidateResourceTypeConfig) ProtoMessage() {} func (x *ValidateResourceTypeConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[19] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -676,22 +1277,20 @@ func (x *ValidateResourceTypeConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResourceTypeConfig.ProtoReflect.Descriptor instead. func (*ValidateResourceTypeConfig) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{9} + return file_tfplugin5_proto_rawDescGZIP(), []int{19} } type ValidateDataSourceConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateDataSourceConfig) Reset() { *x = ValidateDataSourceConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateDataSourceConfig) String() string { @@ -701,8 +1300,8 @@ func (x *ValidateDataSourceConfig) String() string { func (*ValidateDataSourceConfig) ProtoMessage() {} func (x *ValidateDataSourceConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[20] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -714,22 +1313,56 @@ func (x *ValidateDataSourceConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateDataSourceConfig.ProtoReflect.Descriptor instead. func (*ValidateDataSourceConfig) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{10} + return file_tfplugin5_proto_rawDescGZIP(), []int{20} +} + +type ValidateEphemeralResourceConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEphemeralResourceConfig) Reset() { + *x = ValidateEphemeralResourceConfig{} + mi := &file_tfplugin5_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEphemeralResourceConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEphemeralResourceConfig) ProtoMessage() {} + +func (x *ValidateEphemeralResourceConfig) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEphemeralResourceConfig.ProtoReflect.Descriptor instead. +func (*ValidateEphemeralResourceConfig) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{21} } type Configure struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Configure) Reset() { *x = Configure{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Configure) String() string { @@ -739,8 +1372,8 @@ func (x *Configure) String() string { func (*Configure) ProtoMessage() {} func (x *Configure) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[22] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -752,22 +1385,20 @@ func (x *Configure) ProtoReflect() protoreflect.Message { // Deprecated: Use Configure.ProtoReflect.Descriptor instead. func (*Configure) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{11} + return file_tfplugin5_proto_rawDescGZIP(), []int{22} } type ReadResource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadResource) Reset() { *x = ReadResource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadResource) String() string { @@ -777,8 +1408,8 @@ func (x *ReadResource) String() string { func (*ReadResource) ProtoMessage() {} func (x *ReadResource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[23] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -790,22 +1421,20 @@ func (x *ReadResource) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResource.ProtoReflect.Descriptor instead. func (*ReadResource) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{12} + return file_tfplugin5_proto_rawDescGZIP(), []int{23} } type PlanResourceChange struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PlanResourceChange) Reset() { *x = PlanResourceChange{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PlanResourceChange) String() string { @@ -815,8 +1444,8 @@ func (x *PlanResourceChange) String() string { func (*PlanResourceChange) ProtoMessage() {} func (x *PlanResourceChange) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[24] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -828,22 +1457,20 @@ func (x *PlanResourceChange) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanResourceChange.ProtoReflect.Descriptor instead. func (*PlanResourceChange) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{13} + return file_tfplugin5_proto_rawDescGZIP(), []int{24} } type ApplyResourceChange struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ApplyResourceChange) Reset() { *x = ApplyResourceChange{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ApplyResourceChange) String() string { @@ -853,8 +1480,8 @@ func (x *ApplyResourceChange) String() string { func (*ApplyResourceChange) ProtoMessage() {} func (x *ApplyResourceChange) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[25] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -866,22 +1493,20 @@ func (x *ApplyResourceChange) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyResourceChange.ProtoReflect.Descriptor instead. func (*ApplyResourceChange) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{14} + return file_tfplugin5_proto_rawDescGZIP(), []int{25} } type ImportResourceState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState) Reset() { *x = ImportResourceState{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState) String() string { @@ -891,8 +1516,8 @@ func (x *ImportResourceState) String() string { func (*ImportResourceState) ProtoMessage() {} func (x *ImportResourceState) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[26] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -904,22 +1529,56 @@ func (x *ImportResourceState) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportResourceState.ProtoReflect.Descriptor instead. func (*ImportResourceState) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{15} + return file_tfplugin5_proto_rawDescGZIP(), []int{26} +} + +type MoveResourceState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MoveResourceState) Reset() { + *x = MoveResourceState{} + mi := &file_tfplugin5_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MoveResourceState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoveResourceState) ProtoMessage() {} + +func (x *MoveResourceState) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoveResourceState.ProtoReflect.Descriptor instead. +func (*MoveResourceState) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{27} } type ReadDataSource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadDataSource) Reset() { *x = ReadDataSource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadDataSource) String() string { @@ -929,8 +1588,8 @@ func (x *ReadDataSource) String() string { func (*ReadDataSource) ProtoMessage() {} func (x *ReadDataSource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[28] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -942,22 +1601,20 @@ func (x *ReadDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadDataSource.ProtoReflect.Descriptor instead. func (*ReadDataSource) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{16} + return file_tfplugin5_proto_rawDescGZIP(), []int{28} } type GetProvisionerSchema struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProvisionerSchema) Reset() { *x = GetProvisionerSchema{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProvisionerSchema) String() string { @@ -967,8 +1624,8 @@ func (x *GetProvisionerSchema) String() string { func (*GetProvisionerSchema) ProtoMessage() {} func (x *GetProvisionerSchema) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[29] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -980,22 +1637,20 @@ func (x *GetProvisionerSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProvisionerSchema.ProtoReflect.Descriptor instead. func (*GetProvisionerSchema) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{17} + return file_tfplugin5_proto_rawDescGZIP(), []int{29} } type ValidateProvisionerConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateProvisionerConfig) Reset() { *x = ValidateProvisionerConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateProvisionerConfig) String() string { @@ -1005,8 +1660,8 @@ func (x *ValidateProvisionerConfig) String() string { func (*ValidateProvisionerConfig) ProtoMessage() {} func (x *ValidateProvisionerConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[30] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1018,22 +1673,20 @@ func (x *ValidateProvisionerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateProvisionerConfig.ProtoReflect.Descriptor instead. func (*ValidateProvisionerConfig) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{18} + return file_tfplugin5_proto_rawDescGZIP(), []int{30} } type ProvisionResource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProvisionResource) Reset() { *x = ProvisionResource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ProvisionResource) String() string { @@ -1043,8 +1696,8 @@ func (x *ProvisionResource) String() string { func (*ProvisionResource) ProtoMessage() {} func (x *ProvisionResource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[31] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1056,29 +1709,206 @@ func (x *ProvisionResource) ProtoReflect() protoreflect.Message { // Deprecated: Use ProvisionResource.ProtoReflect.Descriptor instead. func (*ProvisionResource) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{19} + return file_tfplugin5_proto_rawDescGZIP(), []int{31} +} + +type OpenEphemeralResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenEphemeralResource) Reset() { + *x = OpenEphemeralResource{} + mi := &file_tfplugin5_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenEphemeralResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenEphemeralResource) ProtoMessage() {} + +func (x *OpenEphemeralResource) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenEphemeralResource.ProtoReflect.Descriptor instead. +func (*OpenEphemeralResource) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{32} +} + +type RenewEphemeralResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewEphemeralResource) Reset() { + *x = RenewEphemeralResource{} + mi := &file_tfplugin5_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewEphemeralResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewEphemeralResource) ProtoMessage() {} + +func (x *RenewEphemeralResource) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewEphemeralResource.ProtoReflect.Descriptor instead. +func (*RenewEphemeralResource) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{33} +} + +type CloseEphemeralResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseEphemeralResource) Reset() { + *x = CloseEphemeralResource{} + mi := &file_tfplugin5_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseEphemeralResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseEphemeralResource) ProtoMessage() {} + +func (x *CloseEphemeralResource) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseEphemeralResource.ProtoReflect.Descriptor instead. +func (*CloseEphemeralResource) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{34} +} + +type GetFunctions struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFunctions) Reset() { + *x = GetFunctions{} + mi := &file_tfplugin5_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFunctions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFunctions) ProtoMessage() {} + +func (x *GetFunctions) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFunctions.ProtoReflect.Descriptor instead. +func (*GetFunctions) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{35} +} + +type CallFunction struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallFunction) Reset() { + *x = CallFunction{} + mi := &file_tfplugin5_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallFunction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallFunction) ProtoMessage() {} + +func (x *CallFunction) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallFunction.ProtoReflect.Descriptor instead. +func (*CallFunction) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{36} } type AttributePath_Step struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Selector: + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selector: // // *AttributePath_Step_AttributeName // *AttributePath_Step_ElementKeyString // *AttributePath_Step_ElementKeyInt - Selector isAttributePath_Step_Selector `protobuf_oneof:"selector"` + Selector isAttributePath_Step_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttributePath_Step) Reset() { *x = AttributePath_Step{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[20] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AttributePath_Step) String() string { @@ -1088,8 +1918,8 @@ func (x *AttributePath_Step) String() string { func (*AttributePath_Step) ProtoMessage() {} func (x *AttributePath_Step) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[20] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[37] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1101,33 +1931,39 @@ func (x *AttributePath_Step) ProtoReflect() protoreflect.Message { // Deprecated: Use AttributePath_Step.ProtoReflect.Descriptor instead. func (*AttributePath_Step) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{2, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{3, 0} } -func (m *AttributePath_Step) GetSelector() isAttributePath_Step_Selector { - if m != nil { - return m.Selector +func (x *AttributePath_Step) GetSelector() isAttributePath_Step_Selector { + if x != nil { + return x.Selector } return nil } func (x *AttributePath_Step) GetAttributeName() string { - if x, ok := x.GetSelector().(*AttributePath_Step_AttributeName); ok { - return x.AttributeName + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_AttributeName); ok { + return x.AttributeName + } } return "" } func (x *AttributePath_Step) GetElementKeyString() string { - if x, ok := x.GetSelector().(*AttributePath_Step_ElementKeyString); ok { - return x.ElementKeyString + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_ElementKeyString); ok { + return x.ElementKeyString + } } return "" } func (x *AttributePath_Step) GetElementKeyInt() int64 { - if x, ok := x.GetSelector().(*AttributePath_Step_ElementKeyInt); ok { - return x.ElementKeyInt + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_ElementKeyInt); ok { + return x.ElementKeyInt + } } return 0 } @@ -1159,18 +1995,16 @@ func (*AttributePath_Step_ElementKeyString) isAttributePath_Step_Selector() {} func (*AttributePath_Step_ElementKeyInt) isAttributePath_Step_Selector() {} type Stop_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Stop_Request) Reset() { *x = Stop_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Stop_Request) String() string { @@ -1180,8 +2014,8 @@ func (x *Stop_Request) String() string { func (*Stop_Request) ProtoMessage() {} func (x *Stop_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[38] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1193,24 +2027,21 @@ func (x *Stop_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Stop_Request.ProtoReflect.Descriptor instead. func (*Stop_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{3, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{4, 0} } type Stop_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Error string `protobuf:"bytes,1,opt,name=Error,proto3" json:"Error,omitempty"` unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=Error,proto3" json:"Error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Stop_Response) Reset() { *x = Stop_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Stop_Response) String() string { @@ -1220,8 +2051,8 @@ func (x *Stop_Response) String() string { func (*Stop_Response) ProtoMessage() {} func (x *Stop_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[39] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1233,7 +2064,7 @@ func (x *Stop_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Stop_Response.ProtoReflect.Descriptor instead. func (*Stop_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{3, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{4, 1} } func (x *Stop_Response) GetError() string { @@ -1243,26 +2074,109 @@ func (x *Stop_Response) GetError() string { return "" } -type Schema_Block struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// IdentityAttribute represents one value of data within resource identity. These +// are always used in resource identity comparisons. +type ResourceIdentitySchema_IdentityAttribute struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the identity attribute name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // type is the identity attribute type + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // required_for_import when enabled signifies that this attribute must be + // defined for ImportResourceState to complete successfully + RequiredForImport bool `protobuf:"varint,3,opt,name=required_for_import,json=requiredForImport,proto3" json:"required_for_import,omitempty"` + // optional_for_import when enabled signifies that this attribute is not + // required for ImportResourceState, because it can be supplied by the + // provider. It is still possible to supply this attribute during import. + OptionalForImport bool `protobuf:"varint,4,opt,name=optional_for_import,json=optionalForImport,proto3" json:"optional_for_import,omitempty"` + // description is a human-readable description of the attribute in Markdown + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - Attributes []*Schema_Attribute `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes,omitempty"` - BlockTypes []*Schema_NestedBlock `protobuf:"bytes,3,rep,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"` - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - DescriptionKind StringKind `protobuf:"varint,5,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin5.StringKind" json:"description_kind,omitempty"` - Deprecated bool `protobuf:"varint,6,opt,name=deprecated,proto3" json:"deprecated,omitempty"` +func (x *ResourceIdentitySchema_IdentityAttribute) Reset() { + *x = ResourceIdentitySchema_IdentityAttribute{} + mi := &file_tfplugin5_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceIdentitySchema_IdentityAttribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceIdentitySchema_IdentityAttribute) ProtoMessage() {} + +func (x *ResourceIdentitySchema_IdentityAttribute) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[41] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceIdentitySchema_IdentityAttribute.ProtoReflect.Descriptor instead. +func (*ResourceIdentitySchema_IdentityAttribute) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{6, 0} +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetRequiredForImport() bool { + if x != nil { + return x.RequiredForImport + } + return false +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetOptionalForImport() bool { + if x != nil { + return x.OptionalForImport + } + return false +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type Schema_Block struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Attributes []*Schema_Attribute `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes,omitempty"` + BlockTypes []*Schema_NestedBlock `protobuf:"bytes,3,rep,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + DescriptionKind StringKind `protobuf:"varint,5,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin5.StringKind" json:"description_kind,omitempty"` + Deprecated bool `protobuf:"varint,6,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_Block) Reset() { *x = Schema_Block{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_Block) String() string { @@ -1272,8 +2186,8 @@ func (x *Schema_Block) String() string { func (*Schema_Block) ProtoMessage() {} func (x *Schema_Block) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[42] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1285,7 +2199,7 @@ func (x *Schema_Block) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_Block.ProtoReflect.Descriptor instead. func (*Schema_Block) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{5, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{8, 0} } func (x *Schema_Block) GetVersion() int64 { @@ -1331,28 +2245,26 @@ func (x *Schema_Block) GetDeprecated() bool { } type Schema_Attribute struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - Optional bool `protobuf:"varint,5,opt,name=optional,proto3" json:"optional,omitempty"` - Computed bool `protobuf:"varint,6,opt,name=computed,proto3" json:"computed,omitempty"` - Sensitive bool `protobuf:"varint,7,opt,name=sensitive,proto3" json:"sensitive,omitempty"` - DescriptionKind StringKind `protobuf:"varint,8,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin5.StringKind" json:"description_kind,omitempty"` - Deprecated bool `protobuf:"varint,9,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + Optional bool `protobuf:"varint,5,opt,name=optional,proto3" json:"optional,omitempty"` + Computed bool `protobuf:"varint,6,opt,name=computed,proto3" json:"computed,omitempty"` + Sensitive bool `protobuf:"varint,7,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + DescriptionKind StringKind `protobuf:"varint,8,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin5.StringKind" json:"description_kind,omitempty"` + Deprecated bool `protobuf:"varint,9,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + WriteOnly bool `protobuf:"varint,10,opt,name=write_only,json=writeOnly,proto3" json:"write_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_Attribute) Reset() { *x = Schema_Attribute{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_Attribute) String() string { @@ -1362,8 +2274,8 @@ func (x *Schema_Attribute) String() string { func (*Schema_Attribute) ProtoMessage() {} func (x *Schema_Attribute) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[43] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1375,7 +2287,7 @@ func (x *Schema_Attribute) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_Attribute.ProtoReflect.Descriptor instead. func (*Schema_Attribute) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{5, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{8, 1} } func (x *Schema_Attribute) GetName() string { @@ -1441,25 +2353,29 @@ func (x *Schema_Attribute) GetDeprecated() bool { return false } -type Schema_NestedBlock struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *Schema_Attribute) GetWriteOnly() bool { + if x != nil { + return x.WriteOnly + } + return false +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` - Nesting Schema_NestedBlock_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=tfplugin5.Schema_NestedBlock_NestingMode" json:"nesting,omitempty"` - MinItems int64 `protobuf:"varint,4,opt,name=min_items,json=minItems,proto3" json:"min_items,omitempty"` - MaxItems int64 `protobuf:"varint,5,opt,name=max_items,json=maxItems,proto3" json:"max_items,omitempty"` +type Schema_NestedBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + Nesting Schema_NestedBlock_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=tfplugin5.Schema_NestedBlock_NestingMode" json:"nesting,omitempty"` + MinItems int64 `protobuf:"varint,4,opt,name=min_items,json=minItems,proto3" json:"min_items,omitempty"` + MaxItems int64 `protobuf:"varint,5,opt,name=max_items,json=maxItems,proto3" json:"max_items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_NestedBlock) Reset() { *x = Schema_NestedBlock{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_NestedBlock) String() string { @@ -1469,8 +2385,8 @@ func (x *Schema_NestedBlock) String() string { func (*Schema_NestedBlock) ProtoMessage() {} func (x *Schema_NestedBlock) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[26] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[44] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1482,7 +2398,7 @@ func (x *Schema_NestedBlock) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_NestedBlock.ProtoReflect.Descriptor instead. func (*Schema_NestedBlock) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{5, 2} + return file_tfplugin5_proto_rawDescGZIP(), []int{8, 2} } func (x *Schema_NestedBlock) GetTypeName() string { @@ -1520,19 +2436,455 @@ func (x *Schema_NestedBlock) GetMaxItems() int64 { return 0 } -type GetProviderSchema_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type Function_Parameter struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the human-readable display name for the parameter. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // type is the type constraint for the parameter. + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // allow_null_value when enabled denotes that a null argument value can + // be passed to the provider. When disabled, Terraform returns an error + // if the argument value is null. + AllowNullValue bool `protobuf:"varint,3,opt,name=allow_null_value,json=allowNullValue,proto3" json:"allow_null_value,omitempty"` + // allow_unknown_values when enabled denotes that only wholly known + // argument values will be passed to the provider. When disabled, + // Terraform skips the function call entirely and assumes an unknown + // value result from the function. + AllowUnknownValues bool `protobuf:"varint,4,opt,name=allow_unknown_values,json=allowUnknownValues,proto3" json:"allow_unknown_values,omitempty"` + // description is human-readable documentation for the parameter. + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // description_kind is the formatting of the description. + DescriptionKind StringKind `protobuf:"varint,6,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin5.StringKind" json:"description_kind,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Function_Parameter) Reset() { + *x = Function_Parameter{} + mi := &file_tfplugin5_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Function_Parameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Function_Parameter) ProtoMessage() {} + +func (x *Function_Parameter) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Function_Parameter.ProtoReflect.Descriptor instead. +func (*Function_Parameter) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{11, 0} +} + +func (x *Function_Parameter) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Function_Parameter) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +func (x *Function_Parameter) GetAllowNullValue() bool { + if x != nil { + return x.AllowNullValue + } + return false +} + +func (x *Function_Parameter) GetAllowUnknownValues() bool { + if x != nil { + return x.AllowUnknownValues + } + return false +} + +func (x *Function_Parameter) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Function_Parameter) GetDescriptionKind() StringKind { + if x != nil { + return x.DescriptionKind + } + return StringKind_PLAIN +} + +type Function_Return struct { + state protoimpl.MessageState `protogen:"open.v1"` + // type is the type constraint for the function result. + Type []byte `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Function_Return) Reset() { + *x = Function_Return{} + mi := &file_tfplugin5_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Function_Return) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Function_Return) ProtoMessage() {} + +func (x *Function_Return) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Function_Return.ProtoReflect.Descriptor instead. +func (*Function_Return) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{11, 1} +} + +func (x *Function_Return) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +type GetMetadata_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_Request) Reset() { + *x = GetMetadata_Request{} + mi := &file_tfplugin5_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_Request) ProtoMessage() {} + +func (x *GetMetadata_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_Request.ProtoReflect.Descriptor instead. +func (*GetMetadata_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13, 0} +} + +type GetMetadata_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServerCapabilities *ServerCapabilities `protobuf:"bytes,1,opt,name=server_capabilities,json=serverCapabilities,proto3" json:"server_capabilities,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + DataSources []*GetMetadata_DataSourceMetadata `protobuf:"bytes,3,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` + Resources []*GetMetadata_ResourceMetadata `protobuf:"bytes,4,rep,name=resources,proto3" json:"resources,omitempty"` + // functions returns metadata for any functions. + Functions []*GetMetadata_FunctionMetadata `protobuf:"bytes,5,rep,name=functions,proto3" json:"functions,omitempty"` + EphemeralResources []*GetMetadata_EphemeralMetadata `protobuf:"bytes,6,rep,name=ephemeral_resources,json=ephemeralResources,proto3" json:"ephemeral_resources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_Response) Reset() { + *x = GetMetadata_Response{} + mi := &file_tfplugin5_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_Response) ProtoMessage() {} + +func (x *GetMetadata_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_Response.ProtoReflect.Descriptor instead. +func (*GetMetadata_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13, 1} +} + +func (x *GetMetadata_Response) GetServerCapabilities() *ServerCapabilities { + if x != nil { + return x.ServerCapabilities + } + return nil +} + +func (x *GetMetadata_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *GetMetadata_Response) GetDataSources() []*GetMetadata_DataSourceMetadata { + if x != nil { + return x.DataSources + } + return nil +} + +func (x *GetMetadata_Response) GetResources() []*GetMetadata_ResourceMetadata { + if x != nil { + return x.Resources + } + return nil +} + +func (x *GetMetadata_Response) GetFunctions() []*GetMetadata_FunctionMetadata { + if x != nil { + return x.Functions + } + return nil +} + +func (x *GetMetadata_Response) GetEphemeralResources() []*GetMetadata_EphemeralMetadata { + if x != nil { + return x.EphemeralResources + } + return nil +} + +type GetMetadata_EphemeralMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_EphemeralMetadata) Reset() { + *x = GetMetadata_EphemeralMetadata{} + mi := &file_tfplugin5_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_EphemeralMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_EphemeralMetadata) ProtoMessage() {} + +func (x *GetMetadata_EphemeralMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_EphemeralMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_EphemeralMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13, 2} +} + +func (x *GetMetadata_EphemeralMetadata) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +type GetMetadata_FunctionMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the function name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_FunctionMetadata) Reset() { + *x = GetMetadata_FunctionMetadata{} + mi := &file_tfplugin5_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_FunctionMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_FunctionMetadata) ProtoMessage() {} + +func (x *GetMetadata_FunctionMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_FunctionMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_FunctionMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13, 3} +} + +func (x *GetMetadata_FunctionMetadata) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetMetadata_DataSourceMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_DataSourceMetadata) Reset() { + *x = GetMetadata_DataSourceMetadata{} + mi := &file_tfplugin5_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_DataSourceMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_DataSourceMetadata) ProtoMessage() {} + +func (x *GetMetadata_DataSourceMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[51] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_DataSourceMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_DataSourceMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13, 4} +} + +func (x *GetMetadata_DataSourceMetadata) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +type GetMetadata_ResourceMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_ResourceMetadata) Reset() { + *x = GetMetadata_ResourceMetadata{} + mi := &file_tfplugin5_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_ResourceMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_ResourceMetadata) ProtoMessage() {} + +func (x *GetMetadata_ResourceMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[52] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_ResourceMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_ResourceMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{13, 5} +} + +func (x *GetMetadata_ResourceMetadata) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +type GetProviderSchema_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderSchema_Request) Reset() { *x = GetProviderSchema_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[53] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProviderSchema_Request) String() string { @@ -1542,8 +2894,8 @@ func (x *GetProviderSchema_Request) String() string { func (*GetProviderSchema_Request) ProtoMessage() {} func (x *GetProviderSchema_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[27] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[53] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1555,29 +2907,28 @@ func (x *GetProviderSchema_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderSchema_Request.ProtoReflect.Descriptor instead. func (*GetProviderSchema_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{6, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{14, 0} } type GetProviderSchema_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Provider *Schema `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - ResourceSchemas map[string]*Schema `protobuf:"bytes,2,rep,name=resource_schemas,json=resourceSchemas,proto3" json:"resource_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - DataSourceSchemas map[string]*Schema `protobuf:"bytes,3,rep,name=data_source_schemas,json=dataSourceSchemas,proto3" json:"data_source_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` - ProviderMeta *Schema `protobuf:"bytes,5,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` - ServerCapabilities *GetProviderSchema_ServerCapabilities `protobuf:"bytes,6,opt,name=server_capabilities,json=serverCapabilities,proto3" json:"server_capabilities,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Provider *Schema `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + ResourceSchemas map[string]*Schema `protobuf:"bytes,2,rep,name=resource_schemas,json=resourceSchemas,proto3" json:"resource_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + DataSourceSchemas map[string]*Schema `protobuf:"bytes,3,rep,name=data_source_schemas,json=dataSourceSchemas,proto3" json:"data_source_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Functions map[string]*Function `protobuf:"bytes,7,rep,name=functions,proto3" json:"functions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + EphemeralResourceSchemas map[string]*Schema `protobuf:"bytes,8,rep,name=ephemeral_resource_schemas,json=ephemeralResourceSchemas,proto3" json:"ephemeral_resource_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + ProviderMeta *Schema `protobuf:"bytes,5,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ServerCapabilities *ServerCapabilities `protobuf:"bytes,6,opt,name=server_capabilities,json=serverCapabilities,proto3" json:"server_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderSchema_Response) Reset() { *x = GetProviderSchema_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[28] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[54] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProviderSchema_Response) String() string { @@ -1587,8 +2938,8 @@ func (x *GetProviderSchema_Response) String() string { func (*GetProviderSchema_Response) ProtoMessage() {} func (x *GetProviderSchema_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[28] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[54] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1600,7 +2951,7 @@ func (x *GetProviderSchema_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderSchema_Response.ProtoReflect.Descriptor instead. func (*GetProviderSchema_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{6, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{14, 1} } func (x *GetProviderSchema_Response) GetProvider() *Schema { @@ -1624,6 +2975,20 @@ func (x *GetProviderSchema_Response) GetDataSourceSchemas() map[string]*Schema { return nil } +func (x *GetProviderSchema_Response) GetFunctions() map[string]*Function { + if x != nil { + return x.Functions + } + return nil +} + +func (x *GetProviderSchema_Response) GetEphemeralResourceSchemas() map[string]*Schema { + if x != nil { + return x.EphemeralResourceSchemas + } + return nil +} + func (x *GetProviderSchema_Response) GetDiagnostics() []*Diagnostic { if x != nil { return x.Diagnostics @@ -1638,81 +3003,25 @@ func (x *GetProviderSchema_Response) GetProviderMeta() *Schema { return nil } -func (x *GetProviderSchema_Response) GetServerCapabilities() *GetProviderSchema_ServerCapabilities { +func (x *GetProviderSchema_Response) GetServerCapabilities() *ServerCapabilities { if x != nil { return x.ServerCapabilities } return nil } -// ServerCapabilities allows providers to communicate extra information -// regarding supported protocol features. This is used to indicate -// availability of certain forward-compatible changes which may be optional -// in a major protocol version, but cannot be tested for directly. -type GetProviderSchema_ServerCapabilities struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The plan_destroy capability signals that a provider expects a call - // to PlanResourceChange when a resource is going to be destroyed. - PlanDestroy bool `protobuf:"varint,1,opt,name=plan_destroy,json=planDestroy,proto3" json:"plan_destroy,omitempty"` -} - -func (x *GetProviderSchema_ServerCapabilities) Reset() { - *x = GetProviderSchema_ServerCapabilities{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[29] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetProviderSchema_ServerCapabilities) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetProviderSchema_ServerCapabilities) ProtoMessage() {} - -func (x *GetProviderSchema_ServerCapabilities) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[29] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetProviderSchema_ServerCapabilities.ProtoReflect.Descriptor instead. -func (*GetProviderSchema_ServerCapabilities) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{6, 2} -} - -func (x *GetProviderSchema_ServerCapabilities) GetPlanDestroy() bool { - if x != nil { - return x.PlanDestroy - } - return false -} - type PrepareProviderConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields - - Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + sizeCache protoimpl.SizeCache } func (x *PrepareProviderConfig_Request) Reset() { *x = PrepareProviderConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PrepareProviderConfig_Request) String() string { @@ -1722,8 +3031,8 @@ func (x *PrepareProviderConfig_Request) String() string { func (*PrepareProviderConfig_Request) ProtoMessage() {} func (x *PrepareProviderConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[32] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[59] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1735,7 +3044,7 @@ func (x *PrepareProviderConfig_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use PrepareProviderConfig_Request.ProtoReflect.Descriptor instead. func (*PrepareProviderConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{7, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{15, 0} } func (x *PrepareProviderConfig_Request) GetConfig() *DynamicValue { @@ -1746,21 +3055,18 @@ func (x *PrepareProviderConfig_Request) GetConfig() *DynamicValue { } type PrepareProviderConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - PreparedConfig *DynamicValue `protobuf:"bytes,1,opt,name=prepared_config,json=preparedConfig,proto3" json:"prepared_config,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + PreparedConfig *DynamicValue `protobuf:"bytes,1,opt,name=prepared_config,json=preparedConfig,proto3" json:"prepared_config,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PrepareProviderConfig_Response) Reset() { *x = PrepareProviderConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PrepareProviderConfig_Response) String() string { @@ -1770,8 +3076,8 @@ func (x *PrepareProviderConfig_Response) String() string { func (*PrepareProviderConfig_Response) ProtoMessage() {} func (x *PrepareProviderConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[33] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[60] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1783,7 +3089,7 @@ func (x *PrepareProviderConfig_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use PrepareProviderConfig_Response.ProtoReflect.Descriptor instead. func (*PrepareProviderConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{7, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{15, 1} } func (x *PrepareProviderConfig_Response) GetPreparedConfig() *DynamicValue { @@ -1800,12 +3106,18 @@ func (x *PrepareProviderConfig_Response) GetDiagnostics() []*Diagnostic { return nil } +// Request is the message that is sent to the provider during the +// UpgradeResourceState RPC. +// +// This message intentionally does not include configuration data as any +// configuration-based or configuration-conditional changes should occur +// during the PlanResourceChange RPC. Additionally, the configuration is +// not guaranteed to exist (in the case of resource destruction), be wholly +// known, nor match the given prior state, which could lead to unexpected +// provider behaviors for practitioners. type UpgradeResourceState_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` // version is the schema_version number recorded in the state file Version int64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // raw_state is the raw states as stored for the resource. Core does @@ -1813,16 +3125,16 @@ type UpgradeResourceState_Request struct { // provider's responsibility to interpret this value using the // appropriate older schema. The raw_state will be the json encoded // state, or a legacy flat-mapped format. - RawState *RawState `protobuf:"bytes,3,opt,name=raw_state,json=rawState,proto3" json:"raw_state,omitempty"` + RawState *RawState `protobuf:"bytes,3,opt,name=raw_state,json=rawState,proto3" json:"raw_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpgradeResourceState_Request) Reset() { *x = UpgradeResourceState_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpgradeResourceState_Request) String() string { @@ -1832,8 +3144,8 @@ func (x *UpgradeResourceState_Request) String() string { func (*UpgradeResourceState_Request) ProtoMessage() {} func (x *UpgradeResourceState_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[34] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[61] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1845,7 +3157,7 @@ func (x *UpgradeResourceState_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use UpgradeResourceState_Request.ProtoReflect.Descriptor instead. func (*UpgradeResourceState_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{8, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{16, 0} } func (x *UpgradeResourceState_Request) GetTypeName() string { @@ -1870,10 +3182,7 @@ func (x *UpgradeResourceState_Request) GetRawState() *RawState { } type UpgradeResourceState_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // new_state is a msgpack-encoded data structure that, when interpreted with // the _current_ schema for this resource type, is functionally equivalent to // that which was given in prior_state_raw. @@ -1881,16 +3190,16 @@ type UpgradeResourceState_Response struct { // diagnostics describes any errors encountered during migration that could not // be safely resolved, and warnings about any possibly-risky assumptions made // in the upgrade process. - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpgradeResourceState_Response) Reset() { *x = UpgradeResourceState_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpgradeResourceState_Response) String() string { @@ -1900,8 +3209,8 @@ func (x *UpgradeResourceState_Response) String() string { func (*UpgradeResourceState_Response) ProtoMessage() {} func (x *UpgradeResourceState_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[35] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[62] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1913,7 +3222,7 @@ func (x *UpgradeResourceState_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use UpgradeResourceState_Response.ProtoReflect.Descriptor instead. func (*UpgradeResourceState_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{8, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{16, 1} } func (x *UpgradeResourceState_Response) GetUpgradedState() *DynamicValue { @@ -1930,22 +3239,230 @@ func (x *UpgradeResourceState_Response) GetDiagnostics() []*Diagnostic { return nil } -type ValidateResourceTypeConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type GetResourceIdentitySchemas_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` +func (x *GetResourceIdentitySchemas_Request) Reset() { + *x = GetResourceIdentitySchemas_Request{} + mi := &file_tfplugin5_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceIdentitySchemas_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceIdentitySchemas_Request) ProtoMessage() {} + +func (x *GetResourceIdentitySchemas_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceIdentitySchemas_Request.ProtoReflect.Descriptor instead. +func (*GetResourceIdentitySchemas_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{17, 0} +} + +type GetResourceIdentitySchemas_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // identity_schemas is a mapping of resource type names to their identity schemas. + IdentitySchemas map[string]*ResourceIdentitySchema `protobuf:"bytes,1,rep,name=identity_schemas,json=identitySchemas,proto3" json:"identity_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // diagnostics is the collection of warning and error diagnostics for this request. + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResourceIdentitySchemas_Response) Reset() { + *x = GetResourceIdentitySchemas_Response{} + mi := &file_tfplugin5_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceIdentitySchemas_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceIdentitySchemas_Response) ProtoMessage() {} + +func (x *GetResourceIdentitySchemas_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceIdentitySchemas_Response.ProtoReflect.Descriptor instead. +func (*GetResourceIdentitySchemas_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{17, 1} +} + +func (x *GetResourceIdentitySchemas_Response) GetIdentitySchemas() map[string]*ResourceIdentitySchema { + if x != nil { + return x.IdentitySchemas + } + return nil +} + +func (x *GetResourceIdentitySchemas_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type UpgradeResourceIdentity_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // type_name is the managed resource type name + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + // version is the version of the resource identity data to upgrade + Version int64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // raw_identity is the raw identity as stored for the resource. Core does + // not have access to the identity schema of prior_version, so it's the + // provider's responsibility to interpret this value using the + // appropriate older schema. The raw_identity will be json encoded. + RawIdentity *RawState `protobuf:"bytes,3,opt,name=raw_identity,json=rawIdentity,proto3" json:"raw_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpgradeResourceIdentity_Request) Reset() { + *x = UpgradeResourceIdentity_Request{} + mi := &file_tfplugin5_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpgradeResourceIdentity_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpgradeResourceIdentity_Request) ProtoMessage() {} + +func (x *UpgradeResourceIdentity_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpgradeResourceIdentity_Request.ProtoReflect.Descriptor instead. +func (*UpgradeResourceIdentity_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{18, 0} +} + +func (x *UpgradeResourceIdentity_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *UpgradeResourceIdentity_Request) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *UpgradeResourceIdentity_Request) GetRawIdentity() *RawState { + if x != nil { + return x.RawIdentity + } + return nil +} + +type UpgradeResourceIdentity_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // upgraded_identity returns the upgraded resource identity data + UpgradedIdentity *ResourceIdentityData `protobuf:"bytes,1,opt,name=upgraded_identity,json=upgradedIdentity,proto3" json:"upgraded_identity,omitempty"` + // diagnostics is the collection of warning and error diagnostics for this request + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpgradeResourceIdentity_Response) Reset() { + *x = UpgradeResourceIdentity_Response{} + mi := &file_tfplugin5_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpgradeResourceIdentity_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpgradeResourceIdentity_Response) ProtoMessage() {} + +func (x *UpgradeResourceIdentity_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpgradeResourceIdentity_Response.ProtoReflect.Descriptor instead. +func (*UpgradeResourceIdentity_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{18, 1} +} + +func (x *UpgradeResourceIdentity_Response) GetUpgradedIdentity() *ResourceIdentityData { + if x != nil { + return x.UpgradedIdentity + } + return nil +} + +func (x *UpgradeResourceIdentity_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type ValidateResourceTypeConfig_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateResourceTypeConfig_Request) Reset() { *x = ValidateResourceTypeConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateResourceTypeConfig_Request) String() string { @@ -1955,8 +3472,8 @@ func (x *ValidateResourceTypeConfig_Request) String() string { func (*ValidateResourceTypeConfig_Request) ProtoMessage() {} func (x *ValidateResourceTypeConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[36] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[68] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1968,7 +3485,7 @@ func (x *ValidateResourceTypeConfig_Request) ProtoReflect() protoreflect.Message // Deprecated: Use ValidateResourceTypeConfig_Request.ProtoReflect.Descriptor instead. func (*ValidateResourceTypeConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{9, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{19, 0} } func (x *ValidateResourceTypeConfig_Request) GetTypeName() string { @@ -1985,21 +3502,25 @@ func (x *ValidateResourceTypeConfig_Request) GetConfig() *DynamicValue { return nil } -type ValidateResourceTypeConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ValidateResourceTypeConfig_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ValidateResourceTypeConfig_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateResourceTypeConfig_Response) Reset() { *x = ValidateResourceTypeConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateResourceTypeConfig_Response) String() string { @@ -2009,8 +3530,8 @@ func (x *ValidateResourceTypeConfig_Response) String() string { func (*ValidateResourceTypeConfig_Response) ProtoMessage() {} func (x *ValidateResourceTypeConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[37] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[69] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2022,7 +3543,7 @@ func (x *ValidateResourceTypeConfig_Response) ProtoReflect() protoreflect.Messag // Deprecated: Use ValidateResourceTypeConfig_Response.ProtoReflect.Descriptor instead. func (*ValidateResourceTypeConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{9, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{19, 1} } func (x *ValidateResourceTypeConfig_Response) GetDiagnostics() []*Diagnostic { @@ -2033,21 +3554,18 @@ func (x *ValidateResourceTypeConfig_Response) GetDiagnostics() []*Diagnostic { } type ValidateDataSourceConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields - - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateDataSourceConfig_Request) Reset() { *x = ValidateDataSourceConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateDataSourceConfig_Request) String() string { @@ -2057,8 +3575,8 @@ func (x *ValidateDataSourceConfig_Request) String() string { func (*ValidateDataSourceConfig_Request) ProtoMessage() {} func (x *ValidateDataSourceConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[38] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[70] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2070,7 +3588,7 @@ func (x *ValidateDataSourceConfig_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateDataSourceConfig_Request.ProtoReflect.Descriptor instead. func (*ValidateDataSourceConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{10, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{20, 0} } func (x *ValidateDataSourceConfig_Request) GetTypeName() string { @@ -2088,20 +3606,17 @@ func (x *ValidateDataSourceConfig_Request) GetConfig() *DynamicValue { } type ValidateDataSourceConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` unknownFields protoimpl.UnknownFields - - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateDataSourceConfig_Response) Reset() { *x = ValidateDataSourceConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateDataSourceConfig_Response) String() string { @@ -2111,8 +3626,8 @@ func (x *ValidateDataSourceConfig_Response) String() string { func (*ValidateDataSourceConfig_Response) ProtoMessage() {} func (x *ValidateDataSourceConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[39] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[71] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2124,7 +3639,7 @@ func (x *ValidateDataSourceConfig_Response) ProtoReflect() protoreflect.Message // Deprecated: Use ValidateDataSourceConfig_Response.ProtoReflect.Descriptor instead. func (*ValidateDataSourceConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{10, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{20, 1} } func (x *ValidateDataSourceConfig_Response) GetDiagnostics() []*Diagnostic { @@ -2134,22 +3649,116 @@ func (x *ValidateDataSourceConfig_Response) GetDiagnostics() []*Diagnostic { return nil } -type Configure_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type ValidateEphemeralResourceConfig_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - TerraformVersion string `protobuf:"bytes,1,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` +func (x *ValidateEphemeralResourceConfig_Request) Reset() { + *x = ValidateEphemeralResourceConfig_Request{} + mi := &file_tfplugin5_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEphemeralResourceConfig_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEphemeralResourceConfig_Request) ProtoMessage() {} + +func (x *ValidateEphemeralResourceConfig_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[72] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEphemeralResourceConfig_Request.ProtoReflect.Descriptor instead. +func (*ValidateEphemeralResourceConfig_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{21, 0} +} + +func (x *ValidateEphemeralResourceConfig_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *ValidateEphemeralResourceConfig_Request) GetConfig() *DynamicValue { + if x != nil { + return x.Config + } + return nil +} + +type ValidateEphemeralResourceConfig_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEphemeralResourceConfig_Response) Reset() { + *x = ValidateEphemeralResourceConfig_Response{} + mi := &file_tfplugin5_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEphemeralResourceConfig_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEphemeralResourceConfig_Response) ProtoMessage() {} + +func (x *ValidateEphemeralResourceConfig_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[73] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEphemeralResourceConfig_Response.ProtoReflect.Descriptor instead. +func (*ValidateEphemeralResourceConfig_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{21, 1} +} + +func (x *ValidateEphemeralResourceConfig_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type Configure_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TerraformVersion string `protobuf:"bytes,1,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Configure_Request) Reset() { *x = Configure_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Configure_Request) String() string { @@ -2159,8 +3768,8 @@ func (x *Configure_Request) String() string { func (*Configure_Request) ProtoMessage() {} func (x *Configure_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[40] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[74] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2172,7 +3781,7 @@ func (x *Configure_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use Configure_Request.ProtoReflect.Descriptor instead. func (*Configure_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{11, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{22, 0} } func (x *Configure_Request) GetTerraformVersion() string { @@ -2189,21 +3798,25 @@ func (x *Configure_Request) GetConfig() *DynamicValue { return nil } -type Configure_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *Configure_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type Configure_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Configure_Response) Reset() { *x = Configure_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Configure_Response) String() string { @@ -2213,8 +3826,8 @@ func (x *Configure_Response) String() string { func (*Configure_Response) ProtoMessage() {} func (x *Configure_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[41] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[75] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2226,7 +3839,7 @@ func (x *Configure_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use Configure_Response.ProtoReflect.Descriptor instead. func (*Configure_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{11, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{22, 1} } func (x *Configure_Response) GetDiagnostics() []*Diagnostic { @@ -2236,24 +3849,31 @@ func (x *Configure_Response) GetDiagnostics() []*Diagnostic { return nil } +// Request is the message that is sent to the provider during the +// ReadResource RPC. +// +// This message intentionally does not include configuration data as any +// configuration-based or configuration-conditional changes should occur +// during the PlanResourceChange RPC. Additionally, the configuration is +// not guaranteed to be wholly known nor match the given prior state, which +// could lead to unexpected provider behaviors for practitioners. type ReadResource_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - CurrentState *DynamicValue `protobuf:"bytes,2,opt,name=current_state,json=currentState,proto3" json:"current_state,omitempty"` - Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,4,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + CurrentState *DynamicValue `protobuf:"bytes,2,opt,name=current_state,json=currentState,proto3" json:"current_state,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,4,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,5,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + CurrentIdentity *ResourceIdentityData `protobuf:"bytes,6,opt,name=current_identity,json=currentIdentity,proto3" json:"current_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadResource_Request) Reset() { *x = ReadResource_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadResource_Request) String() string { @@ -2263,8 +3883,8 @@ func (x *ReadResource_Request) String() string { func (*ReadResource_Request) ProtoMessage() {} func (x *ReadResource_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[42] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[76] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2276,7 +3896,7 @@ func (x *ReadResource_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResource_Request.ProtoReflect.Descriptor instead. func (*ReadResource_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{12, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{23, 0} } func (x *ReadResource_Request) GetTypeName() string { @@ -2307,23 +3927,38 @@ func (x *ReadResource_Request) GetProviderMeta() *DynamicValue { return nil } -type ReadResource_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ReadResource_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` - Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` +func (x *ReadResource_Request) GetCurrentIdentity() *ResourceIdentityData { + if x != nil { + return x.CurrentIdentity + } + return nil +} + +type ReadResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,4,opt,name=deferred,proto3" json:"deferred,omitempty"` + NewIdentity *ResourceIdentityData `protobuf:"bytes,5,opt,name=new_identity,json=newIdentity,proto3" json:"new_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadResource_Response) Reset() { *x = ReadResource_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadResource_Response) String() string { @@ -2333,8 +3968,8 @@ func (x *ReadResource_Response) String() string { func (*ReadResource_Response) ProtoMessage() {} func (x *ReadResource_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[43] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[77] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2346,7 +3981,7 @@ func (x *ReadResource_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResource_Response.ProtoReflect.Descriptor instead. func (*ReadResource_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{12, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{23, 1} } func (x *ReadResource_Response) GetNewState() *DynamicValue { @@ -2370,26 +4005,39 @@ func (x *ReadResource_Response) GetPrivate() []byte { return nil } -type PlanResourceChange_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ReadResource_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` - ProposedNewState *DynamicValue `protobuf:"bytes,3,opt,name=proposed_new_state,json=proposedNewState,proto3" json:"proposed_new_state,omitempty"` - Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` - PriorPrivate []byte `protobuf:"bytes,5,opt,name=prior_private,json=priorPrivate,proto3" json:"prior_private,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` +func (x *ReadResource_Response) GetNewIdentity() *ResourceIdentityData { + if x != nil { + return x.NewIdentity + } + return nil +} + +type PlanResourceChange_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` + ProposedNewState *DynamicValue `protobuf:"bytes,3,opt,name=proposed_new_state,json=proposedNewState,proto3" json:"proposed_new_state,omitempty"` + Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` + PriorPrivate []byte `protobuf:"bytes,5,opt,name=prior_private,json=priorPrivate,proto3" json:"prior_private,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,7,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + PriorIdentity *ResourceIdentityData `protobuf:"bytes,8,opt,name=prior_identity,json=priorIdentity,proto3" json:"prior_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PlanResourceChange_Request) Reset() { *x = PlanResourceChange_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PlanResourceChange_Request) String() string { @@ -2399,8 +4047,8 @@ func (x *PlanResourceChange_Request) String() string { func (*PlanResourceChange_Request) ProtoMessage() {} func (x *PlanResourceChange_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[44] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[78] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2412,7 +4060,7 @@ func (x *PlanResourceChange_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanResourceChange_Request.ProtoReflect.Descriptor instead. func (*PlanResourceChange_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{13, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{24, 0} } func (x *PlanResourceChange_Request) GetTypeName() string { @@ -2457,15 +4105,26 @@ func (x *PlanResourceChange_Request) GetProviderMeta() *DynamicValue { return nil } -type PlanResourceChange_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *PlanResourceChange_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - PlannedState *DynamicValue `protobuf:"bytes,1,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` - RequiresReplace []*AttributePath `protobuf:"bytes,2,rep,name=requires_replace,json=requiresReplace,proto3" json:"requires_replace,omitempty"` - PlannedPrivate []byte `protobuf:"bytes,3,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +func (x *PlanResourceChange_Request) GetPriorIdentity() *ResourceIdentityData { + if x != nil { + return x.PriorIdentity + } + return nil +} + +type PlanResourceChange_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + PlannedState *DynamicValue `protobuf:"bytes,1,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` + RequiresReplace []*AttributePath `protobuf:"bytes,2,rep,name=requires_replace,json=requiresReplace,proto3" json:"requires_replace,omitempty"` + PlannedPrivate []byte `protobuf:"bytes,3,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` // This may be set only by the helper/schema "SDK" in the main Terraform // repository, to request that Terraform Core >=0.12 permit additional // inconsistencies that can result from the legacy SDK type system @@ -2478,15 +4137,19 @@ type PlanResourceChange_Response struct { // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== // ==== DO NOT USE THIS ==== LegacyTypeSystem bool `protobuf:"varint,5,opt,name=legacy_type_system,json=legacyTypeSystem,proto3" json:"legacy_type_system,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,6,opt,name=deferred,proto3" json:"deferred,omitempty"` + PlannedIdentity *ResourceIdentityData `protobuf:"bytes,7,opt,name=planned_identity,json=plannedIdentity,proto3" json:"planned_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PlanResourceChange_Response) Reset() { *x = PlanResourceChange_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PlanResourceChange_Response) String() string { @@ -2496,8 +4159,8 @@ func (x *PlanResourceChange_Response) String() string { func (*PlanResourceChange_Response) ProtoMessage() {} func (x *PlanResourceChange_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[45] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[79] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2509,7 +4172,7 @@ func (x *PlanResourceChange_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanResourceChange_Response.ProtoReflect.Descriptor instead. func (*PlanResourceChange_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{13, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{24, 1} } func (x *PlanResourceChange_Response) GetPlannedState() *DynamicValue { @@ -2547,26 +4210,38 @@ func (x *PlanResourceChange_Response) GetLegacyTypeSystem() bool { return false } -type ApplyResourceChange_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *PlanResourceChange_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` - PlannedState *DynamicValue `protobuf:"bytes,3,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` - Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` - PlannedPrivate []byte `protobuf:"bytes,5,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` +func (x *PlanResourceChange_Response) GetPlannedIdentity() *ResourceIdentityData { + if x != nil { + return x.PlannedIdentity + } + return nil +} + +type ApplyResourceChange_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` + PlannedState *DynamicValue `protobuf:"bytes,3,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` + Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` + PlannedPrivate []byte `protobuf:"bytes,5,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + PlannedIdentity *ResourceIdentityData `protobuf:"bytes,7,opt,name=planned_identity,json=plannedIdentity,proto3" json:"planned_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ApplyResourceChange_Request) Reset() { *x = ApplyResourceChange_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ApplyResourceChange_Request) String() string { @@ -2576,8 +4251,8 @@ func (x *ApplyResourceChange_Request) String() string { func (*ApplyResourceChange_Request) ProtoMessage() {} func (x *ApplyResourceChange_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[46] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[80] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2589,7 +4264,7 @@ func (x *ApplyResourceChange_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyResourceChange_Request.ProtoReflect.Descriptor instead. func (*ApplyResourceChange_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{14, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{25, 0} } func (x *ApplyResourceChange_Request) GetTypeName() string { @@ -2634,14 +4309,18 @@ func (x *ApplyResourceChange_Request) GetProviderMeta() *DynamicValue { return nil } -type ApplyResourceChange_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ApplyResourceChange_Request) GetPlannedIdentity() *ResourceIdentityData { + if x != nil { + return x.PlannedIdentity + } + return nil +} - NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` - Private []byte `protobuf:"bytes,2,opt,name=private,proto3" json:"private,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,3,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ApplyResourceChange_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + Private []byte `protobuf:"bytes,2,opt,name=private,proto3" json:"private,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,3,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` // This may be set only by the helper/schema "SDK" in the main Terraform // repository, to request that Terraform Core >=0.12 permit additional // inconsistencies that can result from the legacy SDK type system @@ -2653,16 +4332,17 @@ type ApplyResourceChange_Response struct { // ==== DO NOT USE THIS ==== // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== // ==== DO NOT USE THIS ==== - LegacyTypeSystem bool `protobuf:"varint,4,opt,name=legacy_type_system,json=legacyTypeSystem,proto3" json:"legacy_type_system,omitempty"` + LegacyTypeSystem bool `protobuf:"varint,4,opt,name=legacy_type_system,json=legacyTypeSystem,proto3" json:"legacy_type_system,omitempty"` + NewIdentity *ResourceIdentityData `protobuf:"bytes,5,opt,name=new_identity,json=newIdentity,proto3" json:"new_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ApplyResourceChange_Response) Reset() { *x = ApplyResourceChange_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ApplyResourceChange_Response) String() string { @@ -2672,8 +4352,8 @@ func (x *ApplyResourceChange_Response) String() string { func (*ApplyResourceChange_Response) ProtoMessage() {} func (x *ApplyResourceChange_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[47] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[81] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2685,7 +4365,7 @@ func (x *ApplyResourceChange_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyResourceChange_Response.ProtoReflect.Descriptor instead. func (*ApplyResourceChange_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{14, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{25, 1} } func (x *ApplyResourceChange_Response) GetNewState() *DynamicValue { @@ -2716,22 +4396,28 @@ func (x *ApplyResourceChange_Response) GetLegacyTypeSystem() bool { return false } -type ImportResourceState_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ApplyResourceChange_Response) GetNewIdentity() *ResourceIdentityData { + if x != nil { + return x.NewIdentity + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` +type ImportResourceState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + Identity *ResourceIdentityData `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState_Request) Reset() { *x = ImportResourceState_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState_Request) String() string { @@ -2741,8 +4427,8 @@ func (x *ImportResourceState_Request) String() string { func (*ImportResourceState_Request) ProtoMessage() {} func (x *ImportResourceState_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[48] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[82] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2754,7 +4440,7 @@ func (x *ImportResourceState_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportResourceState_Request.ProtoReflect.Descriptor instead. func (*ImportResourceState_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{15, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{26, 0} } func (x *ImportResourceState_Request) GetTypeName() string { @@ -2771,23 +4457,35 @@ func (x *ImportResourceState_Request) GetId() string { return "" } -type ImportResourceState_ImportedResource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ImportResourceState_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - State *DynamicValue `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` +func (x *ImportResourceState_Request) GetIdentity() *ResourceIdentityData { + if x != nil { + return x.Identity + } + return nil +} + +type ImportResourceState_ImportedResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + State *DynamicValue `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` + Identity *ResourceIdentityData `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState_ImportedResource) Reset() { *x = ImportResourceState_ImportedResource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState_ImportedResource) String() string { @@ -2797,8 +4495,8 @@ func (x *ImportResourceState_ImportedResource) String() string { func (*ImportResourceState_ImportedResource) ProtoMessage() {} func (x *ImportResourceState_ImportedResource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[49] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[83] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2810,7 +4508,7 @@ func (x *ImportResourceState_ImportedResource) ProtoReflect() protoreflect.Messa // Deprecated: Use ImportResourceState_ImportedResource.ProtoReflect.Descriptor instead. func (*ImportResourceState_ImportedResource) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{15, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{26, 1} } func (x *ImportResourceState_ImportedResource) GetTypeName() string { @@ -2834,22 +4532,29 @@ func (x *ImportResourceState_ImportedResource) GetPrivate() []byte { return nil } -type ImportResourceState_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ImportResourceState_ImportedResource) GetIdentity() *ResourceIdentityData { + if x != nil { + return x.Identity + } + return nil +} +type ImportResourceState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` ImportedResources []*ImportResourceState_ImportedResource `protobuf:"bytes,1,rep,name=imported_resources,json=importedResources,proto3" json:"imported_resources,omitempty"` Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,3,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState_Response) Reset() { *x = ImportResourceState_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState_Response) String() string { @@ -2859,8 +4564,8 @@ func (x *ImportResourceState_Response) String() string { func (*ImportResourceState_Response) ProtoMessage() {} func (x *ImportResourceState_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[50] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[84] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2872,7 +4577,7 @@ func (x *ImportResourceState_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportResourceState_Response.ProtoReflect.Descriptor instead. func (*ImportResourceState_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{15, 2} + return file_tfplugin5_proto_rawDescGZIP(), []int{26, 2} } func (x *ImportResourceState_Response) GetImportedResources() []*ImportResourceState_ImportedResource { @@ -2889,23 +4594,213 @@ func (x *ImportResourceState_Response) GetDiagnostics() []*Diagnostic { return nil } -type ReadDataSource_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ImportResourceState_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,3,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` +type MoveResourceState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The address of the provider the resource is being moved from. + SourceProviderAddress string `protobuf:"bytes,1,opt,name=source_provider_address,json=sourceProviderAddress,proto3" json:"source_provider_address,omitempty"` + // The resource type that the resource is being moved from. + SourceTypeName string `protobuf:"bytes,2,opt,name=source_type_name,json=sourceTypeName,proto3" json:"source_type_name,omitempty"` + // The schema version of the resource type that the resource is being + // moved from. + SourceSchemaVersion int64 `protobuf:"varint,3,opt,name=source_schema_version,json=sourceSchemaVersion,proto3" json:"source_schema_version,omitempty"` + // The raw state of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + SourceState *RawState `protobuf:"bytes,4,opt,name=source_state,json=sourceState,proto3" json:"source_state,omitempty"` + // The resource type that the resource is being moved to. + TargetTypeName string `protobuf:"bytes,5,opt,name=target_type_name,json=targetTypeName,proto3" json:"target_type_name,omitempty"` + // The private state of the resource being moved. + SourcePrivate []byte `protobuf:"bytes,6,opt,name=source_private,json=sourcePrivate,proto3" json:"source_private,omitempty"` + // The raw identity of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + SourceIdentity *RawState `protobuf:"bytes,7,opt,name=source_identity,json=sourceIdentity,proto3" json:"source_identity,omitempty"` + // The identity schema version of the resource type that the resource + // is being moved from. + SourceIdentitySchemaVersion int64 `protobuf:"varint,8,opt,name=source_identity_schema_version,json=sourceIdentitySchemaVersion,proto3" json:"source_identity_schema_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MoveResourceState_Request) Reset() { + *x = MoveResourceState_Request{} + mi := &file_tfplugin5_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MoveResourceState_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoveResourceState_Request) ProtoMessage() {} + +func (x *MoveResourceState_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[85] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoveResourceState_Request.ProtoReflect.Descriptor instead. +func (*MoveResourceState_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{27, 0} +} + +func (x *MoveResourceState_Request) GetSourceProviderAddress() string { + if x != nil { + return x.SourceProviderAddress + } + return "" +} + +func (x *MoveResourceState_Request) GetSourceTypeName() string { + if x != nil { + return x.SourceTypeName + } + return "" +} + +func (x *MoveResourceState_Request) GetSourceSchemaVersion() int64 { + if x != nil { + return x.SourceSchemaVersion + } + return 0 +} + +func (x *MoveResourceState_Request) GetSourceState() *RawState { + if x != nil { + return x.SourceState + } + return nil +} + +func (x *MoveResourceState_Request) GetTargetTypeName() string { + if x != nil { + return x.TargetTypeName + } + return "" +} + +func (x *MoveResourceState_Request) GetSourcePrivate() []byte { + if x != nil { + return x.SourcePrivate + } + return nil +} + +func (x *MoveResourceState_Request) GetSourceIdentity() *RawState { + if x != nil { + return x.SourceIdentity + } + return nil +} + +func (x *MoveResourceState_Request) GetSourceIdentitySchemaVersion() int64 { + if x != nil { + return x.SourceIdentitySchemaVersion + } + return 0 +} + +type MoveResourceState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The state of the resource after it has been moved. + TargetState *DynamicValue `protobuf:"bytes,1,opt,name=target_state,json=targetState,proto3" json:"target_state,omitempty"` + // Any diagnostics that occurred during the move. + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + // The private state of the resource after it has been moved. + TargetPrivate []byte `protobuf:"bytes,3,opt,name=target_private,json=targetPrivate,proto3" json:"target_private,omitempty"` + TargetIdentity *ResourceIdentityData `protobuf:"bytes,4,opt,name=target_identity,json=targetIdentity,proto3" json:"target_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MoveResourceState_Response) Reset() { + *x = MoveResourceState_Response{} + mi := &file_tfplugin5_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MoveResourceState_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoveResourceState_Response) ProtoMessage() {} + +func (x *MoveResourceState_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[86] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoveResourceState_Response.ProtoReflect.Descriptor instead. +func (*MoveResourceState_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{27, 1} +} + +func (x *MoveResourceState_Response) GetTargetState() *DynamicValue { + if x != nil { + return x.TargetState + } + return nil +} + +func (x *MoveResourceState_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *MoveResourceState_Response) GetTargetPrivate() []byte { + if x != nil { + return x.TargetPrivate + } + return nil +} + +func (x *MoveResourceState_Response) GetTargetIdentity() *ResourceIdentityData { + if x != nil { + return x.TargetIdentity + } + return nil +} + +type ReadDataSource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,3,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,4,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadDataSource_Request) Reset() { *x = ReadDataSource_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[51] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadDataSource_Request) String() string { @@ -2915,8 +4810,8 @@ func (x *ReadDataSource_Request) String() string { func (*ReadDataSource_Request) ProtoMessage() {} func (x *ReadDataSource_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[51] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[87] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2928,7 +4823,7 @@ func (x *ReadDataSource_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadDataSource_Request.ProtoReflect.Descriptor instead. func (*ReadDataSource_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{16, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{28, 0} } func (x *ReadDataSource_Request) GetTypeName() string { @@ -2952,22 +4847,29 @@ func (x *ReadDataSource_Request) GetProviderMeta() *DynamicValue { return nil } -type ReadDataSource_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ReadDataSource_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - State *DynamicValue `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ReadDataSource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *DynamicValue `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,3,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadDataSource_Response) Reset() { *x = ReadDataSource_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[52] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadDataSource_Response) String() string { @@ -2977,8 +4879,8 @@ func (x *ReadDataSource_Response) String() string { func (*ReadDataSource_Response) ProtoMessage() {} func (x *ReadDataSource_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[52] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[88] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2990,7 +4892,7 @@ func (x *ReadDataSource_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadDataSource_Response.ProtoReflect.Descriptor instead. func (*ReadDataSource_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{16, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{28, 1} } func (x *ReadDataSource_Response) GetState() *DynamicValue { @@ -3007,19 +4909,24 @@ func (x *ReadDataSource_Response) GetDiagnostics() []*Diagnostic { return nil } +func (x *ReadDataSource_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + type GetProvisionerSchema_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProvisionerSchema_Request) Reset() { *x = GetProvisionerSchema_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[53] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProvisionerSchema_Request) String() string { @@ -3029,8 +4936,8 @@ func (x *GetProvisionerSchema_Request) String() string { func (*GetProvisionerSchema_Request) ProtoMessage() {} func (x *GetProvisionerSchema_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[53] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[89] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3042,25 +4949,22 @@ func (x *GetProvisionerSchema_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProvisionerSchema_Request.ProtoReflect.Descriptor instead. func (*GetProvisionerSchema_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{17, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{29, 0} } type GetProvisionerSchema_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Provisioner *Schema `protobuf:"bytes,1,opt,name=provisioner,proto3" json:"provisioner,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` unknownFields protoimpl.UnknownFields - - Provisioner *Schema `protobuf:"bytes,1,opt,name=provisioner,proto3" json:"provisioner,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GetProvisionerSchema_Response) Reset() { *x = GetProvisionerSchema_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[54] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProvisionerSchema_Response) String() string { @@ -3070,8 +4974,8 @@ func (x *GetProvisionerSchema_Response) String() string { func (*GetProvisionerSchema_Response) ProtoMessage() {} func (x *GetProvisionerSchema_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[54] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[90] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3083,7 +4987,7 @@ func (x *GetProvisionerSchema_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProvisionerSchema_Response.ProtoReflect.Descriptor instead. func (*GetProvisionerSchema_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{17, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{29, 1} } func (x *GetProvisionerSchema_Response) GetProvisioner() *Schema { @@ -3101,20 +5005,17 @@ func (x *GetProvisionerSchema_Response) GetDiagnostics() []*Diagnostic { } type ValidateProvisionerConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields - - Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateProvisionerConfig_Request) Reset() { *x = ValidateProvisionerConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[55] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateProvisionerConfig_Request) String() string { @@ -3124,8 +5025,8 @@ func (x *ValidateProvisionerConfig_Request) String() string { func (*ValidateProvisionerConfig_Request) ProtoMessage() {} func (x *ValidateProvisionerConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[55] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[91] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3137,7 +5038,7 @@ func (x *ValidateProvisionerConfig_Request) ProtoReflect() protoreflect.Message // Deprecated: Use ValidateProvisionerConfig_Request.ProtoReflect.Descriptor instead. func (*ValidateProvisionerConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{18, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{30, 0} } func (x *ValidateProvisionerConfig_Request) GetConfig() *DynamicValue { @@ -3148,20 +5049,17 @@ func (x *ValidateProvisionerConfig_Request) GetConfig() *DynamicValue { } type ValidateProvisionerConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` unknownFields protoimpl.UnknownFields - - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateProvisionerConfig_Response) Reset() { *x = ValidateProvisionerConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[56] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateProvisionerConfig_Response) String() string { @@ -3171,8 +5069,8 @@ func (x *ValidateProvisionerConfig_Response) String() string { func (*ValidateProvisionerConfig_Response) ProtoMessage() {} func (x *ValidateProvisionerConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[56] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[92] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3184,7 +5082,7 @@ func (x *ValidateProvisionerConfig_Response) ProtoReflect() protoreflect.Message // Deprecated: Use ValidateProvisionerConfig_Response.ProtoReflect.Descriptor instead. func (*ValidateProvisionerConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{18, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{30, 1} } func (x *ValidateProvisionerConfig_Response) GetDiagnostics() []*Diagnostic { @@ -3195,21 +5093,18 @@ func (x *ValidateProvisionerConfig_Response) GetDiagnostics() []*Diagnostic { } type ProvisionResource_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Connection *DynamicValue `protobuf:"bytes,2,opt,name=connection,proto3" json:"connection,omitempty"` unknownFields protoimpl.UnknownFields - - Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` - Connection *DynamicValue `protobuf:"bytes,2,opt,name=connection,proto3" json:"connection,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ProvisionResource_Request) Reset() { *x = ProvisionResource_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[57] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ProvisionResource_Request) String() string { @@ -3219,8 +5114,8 @@ func (x *ProvisionResource_Request) String() string { func (*ProvisionResource_Request) ProtoMessage() {} func (x *ProvisionResource_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[57] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[93] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3232,7 +5127,7 @@ func (x *ProvisionResource_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ProvisionResource_Request.ProtoReflect.Descriptor instead. func (*ProvisionResource_Request) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{19, 0} + return file_tfplugin5_proto_rawDescGZIP(), []int{31, 0} } func (x *ProvisionResource_Request) GetConfig() *DynamicValue { @@ -3250,21 +5145,18 @@ func (x *ProvisionResource_Request) GetConnection() *DynamicValue { } type ProvisionResource_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` unknownFields protoimpl.UnknownFields - - Output string `protobuf:"bytes,1,opt,name=output,proto3" json:"output,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ProvisionResource_Response) Reset() { *x = ProvisionResource_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin5_proto_msgTypes[58] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin5_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ProvisionResource_Response) String() string { @@ -3274,8 +5166,8 @@ func (x *ProvisionResource_Response) String() string { func (*ProvisionResource_Response) ProtoMessage() {} func (x *ProvisionResource_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin5_proto_msgTypes[58] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin5_proto_msgTypes[94] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3287,7 +5179,7 @@ func (x *ProvisionResource_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ProvisionResource_Response.ProtoReflect.Descriptor instead. func (*ProvisionResource_Response) Descriptor() ([]byte, []int) { - return file_tfplugin5_proto_rawDescGZIP(), []int{19, 1} + return file_tfplugin5_proto_rawDescGZIP(), []int{31, 1} } func (x *ProvisionResource_Response) GetOutput() string { @@ -3304,147 +5196,848 @@ func (x *ProvisionResource_Response) GetDiagnostics() []*Diagnostic { return nil } +type OpenEphemeralResource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenEphemeralResource_Request) Reset() { + *x = OpenEphemeralResource_Request{} + mi := &file_tfplugin5_proto_msgTypes[95] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenEphemeralResource_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenEphemeralResource_Request) ProtoMessage() {} + +func (x *OpenEphemeralResource_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[95] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenEphemeralResource_Request.ProtoReflect.Descriptor instead. +func (*OpenEphemeralResource_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{32, 0} +} + +func (x *OpenEphemeralResource_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *OpenEphemeralResource_Request) GetConfig() *DynamicValue { + if x != nil { + return x.Config + } + return nil +} + +func (x *OpenEphemeralResource_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} + +type OpenEphemeralResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + RenewAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=renew_at,json=renewAt,proto3,oneof" json:"renew_at,omitempty"` + Result *DynamicValue `protobuf:"bytes,3,opt,name=result,proto3" json:"result,omitempty"` + Private []byte `protobuf:"bytes,4,opt,name=private,proto3,oneof" json:"private,omitempty"` + Deferred *Deferred `protobuf:"bytes,5,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenEphemeralResource_Response) Reset() { + *x = OpenEphemeralResource_Response{} + mi := &file_tfplugin5_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenEphemeralResource_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenEphemeralResource_Response) ProtoMessage() {} + +func (x *OpenEphemeralResource_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[96] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenEphemeralResource_Response.ProtoReflect.Descriptor instead. +func (*OpenEphemeralResource_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{32, 1} +} + +func (x *OpenEphemeralResource_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetRenewAt() *timestamppb.Timestamp { + if x != nil { + return x.RenewAt + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetResult() *DynamicValue { + if x != nil { + return x.Result + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +type RenewEphemeralResource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Private []byte `protobuf:"bytes,2,opt,name=private,proto3,oneof" json:"private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewEphemeralResource_Request) Reset() { + *x = RenewEphemeralResource_Request{} + mi := &file_tfplugin5_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewEphemeralResource_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewEphemeralResource_Request) ProtoMessage() {} + +func (x *RenewEphemeralResource_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[97] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewEphemeralResource_Request.ProtoReflect.Descriptor instead. +func (*RenewEphemeralResource_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{33, 0} +} + +func (x *RenewEphemeralResource_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *RenewEphemeralResource_Request) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +type RenewEphemeralResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + RenewAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=renew_at,json=renewAt,proto3,oneof" json:"renew_at,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3,oneof" json:"private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewEphemeralResource_Response) Reset() { + *x = RenewEphemeralResource_Response{} + mi := &file_tfplugin5_proto_msgTypes[98] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewEphemeralResource_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewEphemeralResource_Response) ProtoMessage() {} + +func (x *RenewEphemeralResource_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[98] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewEphemeralResource_Response.ProtoReflect.Descriptor instead. +func (*RenewEphemeralResource_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{33, 1} +} + +func (x *RenewEphemeralResource_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *RenewEphemeralResource_Response) GetRenewAt() *timestamppb.Timestamp { + if x != nil { + return x.RenewAt + } + return nil +} + +func (x *RenewEphemeralResource_Response) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +type CloseEphemeralResource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Private []byte `protobuf:"bytes,2,opt,name=private,proto3,oneof" json:"private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseEphemeralResource_Request) Reset() { + *x = CloseEphemeralResource_Request{} + mi := &file_tfplugin5_proto_msgTypes[99] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseEphemeralResource_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseEphemeralResource_Request) ProtoMessage() {} + +func (x *CloseEphemeralResource_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[99] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseEphemeralResource_Request.ProtoReflect.Descriptor instead. +func (*CloseEphemeralResource_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{34, 0} +} + +func (x *CloseEphemeralResource_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *CloseEphemeralResource_Request) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +type CloseEphemeralResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseEphemeralResource_Response) Reset() { + *x = CloseEphemeralResource_Response{} + mi := &file_tfplugin5_proto_msgTypes[100] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseEphemeralResource_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseEphemeralResource_Response) ProtoMessage() {} + +func (x *CloseEphemeralResource_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[100] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseEphemeralResource_Response.ProtoReflect.Descriptor instead. +func (*CloseEphemeralResource_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{34, 1} +} + +func (x *CloseEphemeralResource_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type GetFunctions_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFunctions_Request) Reset() { + *x = GetFunctions_Request{} + mi := &file_tfplugin5_proto_msgTypes[101] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFunctions_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFunctions_Request) ProtoMessage() {} + +func (x *GetFunctions_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[101] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFunctions_Request.ProtoReflect.Descriptor instead. +func (*GetFunctions_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{35, 0} +} + +type GetFunctions_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // functions is a mapping of function names to definitions. + Functions map[string]*Function `protobuf:"bytes,1,rep,name=functions,proto3" json:"functions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // diagnostics is any warnings or errors. + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFunctions_Response) Reset() { + *x = GetFunctions_Response{} + mi := &file_tfplugin5_proto_msgTypes[102] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFunctions_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFunctions_Response) ProtoMessage() {} + +func (x *GetFunctions_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[102] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFunctions_Response.ProtoReflect.Descriptor instead. +func (*GetFunctions_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{35, 1} +} + +func (x *GetFunctions_Response) GetFunctions() map[string]*Function { + if x != nil { + return x.Functions + } + return nil +} + +func (x *GetFunctions_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type CallFunction_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Arguments []*DynamicValue `protobuf:"bytes,2,rep,name=arguments,proto3" json:"arguments,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallFunction_Request) Reset() { + *x = CallFunction_Request{} + mi := &file_tfplugin5_proto_msgTypes[104] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallFunction_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallFunction_Request) ProtoMessage() {} + +func (x *CallFunction_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[104] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallFunction_Request.ProtoReflect.Descriptor instead. +func (*CallFunction_Request) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{36, 0} +} + +func (x *CallFunction_Request) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CallFunction_Request) GetArguments() []*DynamicValue { + if x != nil { + return x.Arguments + } + return nil +} + +type CallFunction_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *DynamicValue `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Error *FunctionError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallFunction_Response) Reset() { + *x = CallFunction_Response{} + mi := &file_tfplugin5_proto_msgTypes[105] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallFunction_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallFunction_Response) ProtoMessage() {} + +func (x *CallFunction_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin5_proto_msgTypes[105] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallFunction_Response.ProtoReflect.Descriptor instead. +func (*CallFunction_Response) Descriptor() ([]byte, []int) { + return file_tfplugin5_proto_rawDescGZIP(), []int{36, 1} +} + +func (x *CallFunction_Response) GetResult() *DynamicValue { + if x != nil { + return x.Result + } + return nil +} + +func (x *CallFunction_Response) GetError() *FunctionError { + if x != nil { + return x.Error + } + return nil +} + var File_tfplugin5_proto protoreflect.FileDescriptor -var file_tfplugin5_proto_rawDesc = []byte{ +var file_tfplugin5_proto_rawDesc = string([]byte{ 0x0a, 0x0f, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x22, 0x3c, 0x0a, 0x0c, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, - 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0xe3, 0x01, 0x0a, 0x0a, 0x44, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x12, 0x3a, 0x0a, 0x08, 0x73, 0x65, 0x76, - 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, - 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x36, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x50, 0x61, 0x74, 0x68, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, - 0x2f, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x49, - 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, - 0x22, 0xdc, 0x01, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, - 0x74, 0x68, 0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, - 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x95, 0x01, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, - 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x4b, 0x65, 0x79, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x48, 0x00, 0x52, 0x0d, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, - 0x49, 0x6e, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, - 0x33, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x20, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, - 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x07, 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x35, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x46, 0x6c, 0x61, 0x74, - 0x6d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, - 0x70, 0x1a, 0x3a, 0x0a, 0x0c, 0x46, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xcc, 0x07, - 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, - 0x6b, 0x1a, 0xa2, 0x02, 0x0a, 0x05, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, - 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, - 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x6f, 0x12, 0x09, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3c, 0x0a, + 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, + 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0xe3, 0x01, 0x0a, 0x0a, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x12, 0x3a, 0x0a, 0x08, 0x73, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x36, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x22, 0x2f, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, + 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, + 0x02, 0x22, 0x6b, 0x0a, 0x0d, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x30, 0x0a, 0x11, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x48, 0x00, 0x52, 0x10, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x72, 0x67, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x66, 0x75, 0x6e, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0xdc, + 0x01, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, + 0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, + 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x95, 0x01, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, + 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6c, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, + 0x79, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6c, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x48, 0x00, 0x52, 0x0d, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x49, 0x6e, + 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x33, 0x0a, + 0x04, 0x53, 0x74, 0x6f, 0x70, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x20, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, + 0x73, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x07, 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x46, 0x6c, 0x61, 0x74, 0x6d, 0x61, + 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x1a, + 0x3a, 0x0a, 0x0c, 0x46, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, + 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd8, 0x02, 0x0a, 0x16, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x64, 0x0a, 0x13, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x52, 0x12, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x1a, 0xbd, 0x01, 0x0a, 0x11, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, + 0x74, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, + 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x11, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, + 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x11, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x46, 0x6f, 0x72, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x54, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12, 0x3c, + 0x0a, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x22, 0xeb, 0x07, 0x0a, + 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x2d, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, + 0x1a, 0xa2, 0x02, 0x0a, 0x05, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, + 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, + 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, + 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xc8, 0x02, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, + 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, + 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, + 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x12, + 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, + 0x69, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, + 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, + 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, + 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4f, 0x6e, 0x6c, 0x79, + 0x1a, 0xa7, 0x02, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, + 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, + 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, + 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x43, 0x0a, 0x07, + 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x4e, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, + 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1b, + 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x4d, 0x0a, 0x0b, 0x4e, + 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, + 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, + 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, 0x07, 0x0a, + 0x03, 0x53, 0x45, 0x54, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x10, 0x04, 0x12, + 0x09, 0x0a, 0x05, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x05, 0x22, 0xa8, 0x01, 0x0a, 0x12, 0x53, + 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x6c, 0x61, 0x6e, 0x44, 0x65, 0x73, + 0x74, 0x72, 0x6f, 0x79, 0x12, 0x3f, 0x0a, 0x1c, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x19, 0x67, 0x65, 0x74, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x2e, 0x0a, 0x13, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x11, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x12, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x29, 0x0a, 0x10, + 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x61, 0x6c, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x61, 0x6c, + 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x72, 0x69, 0x74, 0x65, + 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x1a, + 0x77, 0x72, 0x69, 0x74, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x22, 0x8e, 0x05, 0x0a, 0x08, 0x46, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x4c, 0x0a, 0x12, 0x76, 0x61, 0x72, 0x69, 0x61, 0x64, + 0x69, 0x63, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x46, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x52, 0x11, 0x76, 0x61, 0x72, 0x69, 0x61, 0x64, 0x69, 0x63, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6e, + 0x52, 0x06, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, + 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, - 0x61, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, - 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xa9, 0x02, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x20, 0x0a, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, - 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, - 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, - 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, - 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, - 0x64, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, - 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, - 0x65, 0x64, 0x1a, 0xa7, 0x02, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x2d, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x43, - 0x0a, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x4e, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6e, 0x65, 0x73, 0x74, - 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, - 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x4d, 0x0a, - 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, - 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, - 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, - 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x10, - 0x04, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x05, 0x22, 0xeb, 0x05, 0x0a, - 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x91, 0x05, - 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x10, 0x72, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, - 0x12, 0x6c, 0x0a, 0x13, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, - 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, + 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x2f, 0x0a, 0x13, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0xf3, 0x01, 0x0a, 0x09, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, + 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4e, 0x75, + 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x5f, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x6e, 0x6b, 0x6e, + 0x6f, 0x77, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x10, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x1a, 0x1c, 0x0a, + 0x06, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa2, 0x01, 0x0a, 0x08, + 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x2e, 0x52, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x62, 0x0a, 0x06, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, + 0x4e, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, + 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x01, + 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, + 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x02, 0x12, 0x11, 0x0a, + 0x0d, 0x41, 0x42, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, 0x45, 0x52, 0x45, 0x51, 0x10, 0x03, + 0x22, 0xa3, 0x05, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0xca, 0x03, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x13, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, + 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x12, 0x4c, 0x0a, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, + 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, + 0x45, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, + 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x09, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x59, 0x0a, + 0x13, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x2e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x30, 0x0a, 0x11, 0x45, 0x70, 0x68, 0x65, + 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, + 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x26, 0x0a, 0x10, 0x46, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, + 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x1a, 0x31, 0x0a, 0x12, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x2f, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, + 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xab, 0x08, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x09, 0x0a, 0x07, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x8a, 0x08, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x64, 0x61, 0x74, - 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x37, - 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x36, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, - 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, - 0x60, 0x0a, 0x13, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, - 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, + 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x6c, 0x0a, 0x13, 0x64, 0x61, + 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x64, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x52, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x81, 0x01, 0x0a, + 0x1a, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x43, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, + 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, + 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x18, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, + 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, + 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x36, 0x0a, 0x0d, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, + 0x61, 0x12, 0x4e, 0x0a, 0x13, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x55, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, @@ -3458,69 +6051,109 @@ var file_tfplugin5_proto_rawDesc = []byte{ 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x37, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, - 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x5f, - 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, - 0x6c, 0x61, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x22, 0xdb, 0x01, 0x0a, 0x15, 0x50, - 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x3a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, - 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x1a, 0x85, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, - 0x0f, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, - 0x0e, 0x70, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, - 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, - 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, - 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x90, 0x02, 0x0a, 0x14, 0x55, 0x70, 0x67, - 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x1a, 0x72, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x01, 0x1a, 0x51, 0x0a, 0x0e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5e, 0x0a, 0x1d, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, + 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0xdb, 0x01, 0x0a, 0x15, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x3a, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x85, 0x01, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0f, 0x70, 0x72, 0x65, 0x70, 0x61, + 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0e, 0x70, 0x72, 0x65, 0x70, 0x61, + 0x72, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x22, 0x90, 0x02, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x72, 0x0a, 0x07, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, + 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, + 0x09, 0x72, 0x61, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x61, 0x77, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x08, 0x72, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, + 0x83, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0e, + 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x75, + 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xc4, 0x02, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x9a, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x10, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x37, 0x0a, 0x0b, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x1a, 0x65, 0x0a, 0x14, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa7, 0x02, 0x0a, + 0x17, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0x78, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0c, 0x72, 0x61, + 0x77, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x61, 0x77, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x72, 0x61, 0x77, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x1a, 0x91, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x4c, 0x0a, 0x11, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x10, 0x75, 0x70, 0x67, + 0x72, 0x61, 0x64, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x37, 0x0a, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x8b, 0x02, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0xa7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, + 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, + 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, + 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, + 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, + 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x18, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x72, 0x61, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x35, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x08, 0x72, 0x61, 0x77, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x83, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, - 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xba, 0x01, 0x0a, 0x1a, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, - 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, - 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, - 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xb8, 0x01, 0x0a, 0x18, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, - 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, - 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, - 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, - 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x73, 0x22, 0xb9, 0x01, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x65, 0x1a, 0x67, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x11, - 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, - 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, @@ -3528,30 +6161,76 @@ var file_tfplugin5_proto_rawDesc = []byte{ 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, - 0xe3, 0x02, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x1a, 0xbc, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, - 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x63, 0x75, 0x72, - 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, - 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, - 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0xbf, 0x01, 0x0a, 0x1f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, + 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x22, 0x8a, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x1a, + 0xb7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x74, + 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, + 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x1a, - 0x93, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, - 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xf4, + 0x04, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, + 0xd8, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, + 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, - 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, - 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xf2, 0x04, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0xbb, 0x02, 0x0a, + 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, + 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x4e, + 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x4a, + 0x0a, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x63, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x74, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0x88, 0x02, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x12, 0x42, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x87, 0x07, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0xd3, 0x03, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, @@ -3571,418 +6250,800 @@ var file_tfplugin5_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x9d, 0x02, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x6e, - 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x43, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, - 0x73, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, - 0x72, 0x65, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x6c, - 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, - 0x63, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, - 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2c, 0x0a, 0x12, - 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, - 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x54, 0x79, 0x70, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, 0x92, 0x04, 0x0a, 0x13, 0x41, - 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, - 0x67, 0x65, 0x1a, 0xb6, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x70, - 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, - 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x6f, 0x72, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, - 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, - 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, - 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, - 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0xc1, 0x01, 0x0a, 0x08, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, - 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, - 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, - 0xed, 0x02, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x36, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, - 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x1a, - 0x78, 0x0a, 0x10, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x46, 0x0a, 0x0e, 0x70, 0x72, + 0x69, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x1a, 0x9a, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x3c, 0x0a, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x43, 0x0a, + 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, + 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x61, + 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x10, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x79, 0x73, 0x74, + 0x65, 0x6d, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, + 0x72, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x10, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0f, + 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, + 0xa2, 0x05, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x82, 0x03, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, + 0x70, 0x72, 0x69, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x6c, + 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, + 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x4a, 0x0a, 0x10, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0f, 0x70, 0x6c, 0x61, + 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0x85, 0x02, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, 0x6e, 0x65, 0x77, + 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, + 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, + 0x12, 0x42, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x6e, 0x65, 0x77, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x22, 0xea, 0x04, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0xc3, 0x01, 0x0a, + 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x1a, 0xb5, 0x01, 0x0a, 0x10, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x3b, 0x0a, + 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, + 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0xd4, 0x01, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x12, 0x69, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, + 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x52, 0x11, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, + 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x22, 0xb4, 0x05, 0x0a, 0x11, 0x4d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0xab, 0x03, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0c, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x61, 0x77, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x50, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x56, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xf0, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, + 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x48, + 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x9e, 0x03, 0x0a, 0x0e, 0x52, 0x65, 0x61, + 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0xe5, 0x01, 0x0a, 0x07, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, + 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, + 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x69, 0x65, 0x73, 0x1a, 0xa3, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x1a, 0xa3, 0x01, 0x0a, 0x08, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x12, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x49, - 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x52, 0x11, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, - 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, - 0x9c, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x1a, 0x95, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, - 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, - 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0d, - 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x72, 0x0a, 0x08, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, + 0x72, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, + 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x22, 0x9b, 0x01, 0x0a, 0x14, 0x47, 0x65, + 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x78, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, 0x0a, 0x0b, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x37, + 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x9c, 0x01, 0x0a, 0x19, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x3a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x73, 0x0a, 0x07, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x37, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x1a, 0x5b, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, + 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, + 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, - 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x9b, - 0x01, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x78, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x33, - 0x0a, 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x0b, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, - 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, - 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x9c, 0x01, 0x0a, - 0x19, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x3a, 0x0a, 0x07, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, - 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xe5, 0x01, 0x0a, 0x11, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x1a, 0x73, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x06, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x37, 0x0a, - 0x0a, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xdd, + 0x03, 0x0a, 0x15, 0x4f, 0x70, 0x65, 0x6e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0xa7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, - 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x5b, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, - 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x73, 0x2a, 0x25, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, - 0x64, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, - 0x4d, 0x41, 0x52, 0x4b, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x32, 0x97, 0x09, 0x0a, 0x08, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x58, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, - 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x6c, 0x0a, 0x15, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, - 0x2e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x7b, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x18, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x1a, 0x99, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x72, 0x65, 0x6e, 0x65, + 0x77, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x41, + 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x5f, + 0x61, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xa5, + 0x02, 0x0a, 0x16, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, + 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x51, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x48, 0x00, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, + 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x1a, 0xb7, 0x01, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x5f, 0x61, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1d, + 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, + 0x01, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x5f, 0x61, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, + 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x1a, 0x51, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x07, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x81, 0x02, 0x0a, 0x0c, 0x47, 0x65, + 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0xe5, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x1a, 0x51, 0x0a, 0x0e, 0x46, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd1, 0x01, + 0x0a, 0x0c, 0x43, 0x61, 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x54, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, + 0x09, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x61, 0x72, 0x67, 0x75, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x1a, 0x6b, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2f, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x46, 0x75, 0x6e, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x2a, 0x25, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x12, + 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x41, + 0x52, 0x4b, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x32, 0xbd, 0x11, 0x0a, 0x08, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x4e, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, + 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x6c, 0x0a, 0x15, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x28, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, + 0x72, 0x65, 0x70, 0x61, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, + 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x75, 0x0a, 0x18, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x53, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, - 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x69, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x55, + 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x1a, + 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x2d, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, + 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, 0x0a, 0x17, 0x55, 0x70, 0x67, + 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x12, 0x2a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, - 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, - 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x55, 0x70, 0x67, + 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x48, 0x0a, + 0x09, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x50, - 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x12, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x6c, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x66, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x70, 0x70, 0x6c, - 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, - 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x49, 0x6d, 0x70, 0x6f, - 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x35, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x57, 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x12, 0x21, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, - 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x35, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x04, 0x53, 0x74, 0x6f, - 0x70, 0x12, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, - 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x86, 0x03, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x12, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, - 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, - 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x19, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x2c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, + 0x12, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x6c, 0x61, + 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x66, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x26, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x60, 0x0a, 0x11, 0x4d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, + 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x57, 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x21, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8a, 0x01, 0x0a, 0x1f, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x32, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x15, 0x4f, 0x70, 0x65, 0x6e, 0x45, + 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x28, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x4f, 0x70, 0x65, + 0x6e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x45, 0x70, 0x68, 0x65, 0x6d, + 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, + 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, + 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x6e, 0x65, + 0x77, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, 0x68, 0x65, + 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x45, + 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x6f, + 0x73, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x45, 0x70, 0x68, + 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x46, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x43, 0x61, + 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, + 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x86, 0x03, 0x0a, 0x0b, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, 0x5e, 0x0a, 0x09, 0x47, 0x65, 0x74, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x19, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x62, - 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x30, 0x01, 0x12, 0x39, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x17, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, - 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, - 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, - 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x35, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x35, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x62, 0x0a, 0x11, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x39, 0x0a, 0x04, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x17, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x35, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, + 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x35, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_tfplugin5_proto_rawDescOnce sync.Once - file_tfplugin5_proto_rawDescData = file_tfplugin5_proto_rawDesc + file_tfplugin5_proto_rawDescData []byte ) func file_tfplugin5_proto_rawDescGZIP() []byte { file_tfplugin5_proto_rawDescOnce.Do(func() { - file_tfplugin5_proto_rawDescData = protoimpl.X.CompressGZIP(file_tfplugin5_proto_rawDescData) + file_tfplugin5_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tfplugin5_proto_rawDesc), len(file_tfplugin5_proto_rawDesc))) }) return file_tfplugin5_proto_rawDescData } -var file_tfplugin5_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_tfplugin5_proto_msgTypes = make([]protoimpl.MessageInfo, 59) -var file_tfplugin5_proto_goTypes = []interface{}{ - (StringKind)(0), // 0: tfplugin5.StringKind - (Diagnostic_Severity)(0), // 1: tfplugin5.Diagnostic.Severity - (Schema_NestedBlock_NestingMode)(0), // 2: tfplugin5.Schema.NestedBlock.NestingMode - (*DynamicValue)(nil), // 3: tfplugin5.DynamicValue - (*Diagnostic)(nil), // 4: tfplugin5.Diagnostic - (*AttributePath)(nil), // 5: tfplugin5.AttributePath - (*Stop)(nil), // 6: tfplugin5.Stop - (*RawState)(nil), // 7: tfplugin5.RawState - (*Schema)(nil), // 8: tfplugin5.Schema - (*GetProviderSchema)(nil), // 9: tfplugin5.GetProviderSchema - (*PrepareProviderConfig)(nil), // 10: tfplugin5.PrepareProviderConfig - (*UpgradeResourceState)(nil), // 11: tfplugin5.UpgradeResourceState - (*ValidateResourceTypeConfig)(nil), // 12: tfplugin5.ValidateResourceTypeConfig - (*ValidateDataSourceConfig)(nil), // 13: tfplugin5.ValidateDataSourceConfig - (*Configure)(nil), // 14: tfplugin5.Configure - (*ReadResource)(nil), // 15: tfplugin5.ReadResource - (*PlanResourceChange)(nil), // 16: tfplugin5.PlanResourceChange - (*ApplyResourceChange)(nil), // 17: tfplugin5.ApplyResourceChange - (*ImportResourceState)(nil), // 18: tfplugin5.ImportResourceState - (*ReadDataSource)(nil), // 19: tfplugin5.ReadDataSource - (*GetProvisionerSchema)(nil), // 20: tfplugin5.GetProvisionerSchema - (*ValidateProvisionerConfig)(nil), // 21: tfplugin5.ValidateProvisionerConfig - (*ProvisionResource)(nil), // 22: tfplugin5.ProvisionResource - (*AttributePath_Step)(nil), // 23: tfplugin5.AttributePath.Step - (*Stop_Request)(nil), // 24: tfplugin5.Stop.Request - (*Stop_Response)(nil), // 25: tfplugin5.Stop.Response - nil, // 26: tfplugin5.RawState.FlatmapEntry - (*Schema_Block)(nil), // 27: tfplugin5.Schema.Block - (*Schema_Attribute)(nil), // 28: tfplugin5.Schema.Attribute - (*Schema_NestedBlock)(nil), // 29: tfplugin5.Schema.NestedBlock - (*GetProviderSchema_Request)(nil), // 30: tfplugin5.GetProviderSchema.Request - (*GetProviderSchema_Response)(nil), // 31: tfplugin5.GetProviderSchema.Response - (*GetProviderSchema_ServerCapabilities)(nil), // 32: tfplugin5.GetProviderSchema.ServerCapabilities - nil, // 33: tfplugin5.GetProviderSchema.Response.ResourceSchemasEntry - nil, // 34: tfplugin5.GetProviderSchema.Response.DataSourceSchemasEntry - (*PrepareProviderConfig_Request)(nil), // 35: tfplugin5.PrepareProviderConfig.Request - (*PrepareProviderConfig_Response)(nil), // 36: tfplugin5.PrepareProviderConfig.Response - (*UpgradeResourceState_Request)(nil), // 37: tfplugin5.UpgradeResourceState.Request - (*UpgradeResourceState_Response)(nil), // 38: tfplugin5.UpgradeResourceState.Response - (*ValidateResourceTypeConfig_Request)(nil), // 39: tfplugin5.ValidateResourceTypeConfig.Request - (*ValidateResourceTypeConfig_Response)(nil), // 40: tfplugin5.ValidateResourceTypeConfig.Response - (*ValidateDataSourceConfig_Request)(nil), // 41: tfplugin5.ValidateDataSourceConfig.Request - (*ValidateDataSourceConfig_Response)(nil), // 42: tfplugin5.ValidateDataSourceConfig.Response - (*Configure_Request)(nil), // 43: tfplugin5.Configure.Request - (*Configure_Response)(nil), // 44: tfplugin5.Configure.Response - (*ReadResource_Request)(nil), // 45: tfplugin5.ReadResource.Request - (*ReadResource_Response)(nil), // 46: tfplugin5.ReadResource.Response - (*PlanResourceChange_Request)(nil), // 47: tfplugin5.PlanResourceChange.Request - (*PlanResourceChange_Response)(nil), // 48: tfplugin5.PlanResourceChange.Response - (*ApplyResourceChange_Request)(nil), // 49: tfplugin5.ApplyResourceChange.Request - (*ApplyResourceChange_Response)(nil), // 50: tfplugin5.ApplyResourceChange.Response - (*ImportResourceState_Request)(nil), // 51: tfplugin5.ImportResourceState.Request - (*ImportResourceState_ImportedResource)(nil), // 52: tfplugin5.ImportResourceState.ImportedResource - (*ImportResourceState_Response)(nil), // 53: tfplugin5.ImportResourceState.Response - (*ReadDataSource_Request)(nil), // 54: tfplugin5.ReadDataSource.Request - (*ReadDataSource_Response)(nil), // 55: tfplugin5.ReadDataSource.Response - (*GetProvisionerSchema_Request)(nil), // 56: tfplugin5.GetProvisionerSchema.Request - (*GetProvisionerSchema_Response)(nil), // 57: tfplugin5.GetProvisionerSchema.Response - (*ValidateProvisionerConfig_Request)(nil), // 58: tfplugin5.ValidateProvisionerConfig.Request - (*ValidateProvisionerConfig_Response)(nil), // 59: tfplugin5.ValidateProvisionerConfig.Response - (*ProvisionResource_Request)(nil), // 60: tfplugin5.ProvisionResource.Request - (*ProvisionResource_Response)(nil), // 61: tfplugin5.ProvisionResource.Response +var file_tfplugin5_proto_enumTypes = make([]protoimpl.EnumInfo, 4) +var file_tfplugin5_proto_msgTypes = make([]protoimpl.MessageInfo, 106) +var file_tfplugin5_proto_goTypes = []any{ + (StringKind)(0), // 0: tfplugin5.StringKind + (Diagnostic_Severity)(0), // 1: tfplugin5.Diagnostic.Severity + (Schema_NestedBlock_NestingMode)(0), // 2: tfplugin5.Schema.NestedBlock.NestingMode + (Deferred_Reason)(0), // 3: tfplugin5.Deferred.Reason + (*DynamicValue)(nil), // 4: tfplugin5.DynamicValue + (*Diagnostic)(nil), // 5: tfplugin5.Diagnostic + (*FunctionError)(nil), // 6: tfplugin5.FunctionError + (*AttributePath)(nil), // 7: tfplugin5.AttributePath + (*Stop)(nil), // 8: tfplugin5.Stop + (*RawState)(nil), // 9: tfplugin5.RawState + (*ResourceIdentitySchema)(nil), // 10: tfplugin5.ResourceIdentitySchema + (*ResourceIdentityData)(nil), // 11: tfplugin5.ResourceIdentityData + (*Schema)(nil), // 12: tfplugin5.Schema + (*ServerCapabilities)(nil), // 13: tfplugin5.ServerCapabilities + (*ClientCapabilities)(nil), // 14: tfplugin5.ClientCapabilities + (*Function)(nil), // 15: tfplugin5.Function + (*Deferred)(nil), // 16: tfplugin5.Deferred + (*GetMetadata)(nil), // 17: tfplugin5.GetMetadata + (*GetProviderSchema)(nil), // 18: tfplugin5.GetProviderSchema + (*PrepareProviderConfig)(nil), // 19: tfplugin5.PrepareProviderConfig + (*UpgradeResourceState)(nil), // 20: tfplugin5.UpgradeResourceState + (*GetResourceIdentitySchemas)(nil), // 21: tfplugin5.GetResourceIdentitySchemas + (*UpgradeResourceIdentity)(nil), // 22: tfplugin5.UpgradeResourceIdentity + (*ValidateResourceTypeConfig)(nil), // 23: tfplugin5.ValidateResourceTypeConfig + (*ValidateDataSourceConfig)(nil), // 24: tfplugin5.ValidateDataSourceConfig + (*ValidateEphemeralResourceConfig)(nil), // 25: tfplugin5.ValidateEphemeralResourceConfig + (*Configure)(nil), // 26: tfplugin5.Configure + (*ReadResource)(nil), // 27: tfplugin5.ReadResource + (*PlanResourceChange)(nil), // 28: tfplugin5.PlanResourceChange + (*ApplyResourceChange)(nil), // 29: tfplugin5.ApplyResourceChange + (*ImportResourceState)(nil), // 30: tfplugin5.ImportResourceState + (*MoveResourceState)(nil), // 31: tfplugin5.MoveResourceState + (*ReadDataSource)(nil), // 32: tfplugin5.ReadDataSource + (*GetProvisionerSchema)(nil), // 33: tfplugin5.GetProvisionerSchema + (*ValidateProvisionerConfig)(nil), // 34: tfplugin5.ValidateProvisionerConfig + (*ProvisionResource)(nil), // 35: tfplugin5.ProvisionResource + (*OpenEphemeralResource)(nil), // 36: tfplugin5.OpenEphemeralResource + (*RenewEphemeralResource)(nil), // 37: tfplugin5.RenewEphemeralResource + (*CloseEphemeralResource)(nil), // 38: tfplugin5.CloseEphemeralResource + (*GetFunctions)(nil), // 39: tfplugin5.GetFunctions + (*CallFunction)(nil), // 40: tfplugin5.CallFunction + (*AttributePath_Step)(nil), // 41: tfplugin5.AttributePath.Step + (*Stop_Request)(nil), // 42: tfplugin5.Stop.Request + (*Stop_Response)(nil), // 43: tfplugin5.Stop.Response + nil, // 44: tfplugin5.RawState.FlatmapEntry + (*ResourceIdentitySchema_IdentityAttribute)(nil), // 45: tfplugin5.ResourceIdentitySchema.IdentityAttribute + (*Schema_Block)(nil), // 46: tfplugin5.Schema.Block + (*Schema_Attribute)(nil), // 47: tfplugin5.Schema.Attribute + (*Schema_NestedBlock)(nil), // 48: tfplugin5.Schema.NestedBlock + (*Function_Parameter)(nil), // 49: tfplugin5.Function.Parameter + (*Function_Return)(nil), // 50: tfplugin5.Function.Return + (*GetMetadata_Request)(nil), // 51: tfplugin5.GetMetadata.Request + (*GetMetadata_Response)(nil), // 52: tfplugin5.GetMetadata.Response + (*GetMetadata_EphemeralMetadata)(nil), // 53: tfplugin5.GetMetadata.EphemeralMetadata + (*GetMetadata_FunctionMetadata)(nil), // 54: tfplugin5.GetMetadata.FunctionMetadata + (*GetMetadata_DataSourceMetadata)(nil), // 55: tfplugin5.GetMetadata.DataSourceMetadata + (*GetMetadata_ResourceMetadata)(nil), // 56: tfplugin5.GetMetadata.ResourceMetadata + (*GetProviderSchema_Request)(nil), // 57: tfplugin5.GetProviderSchema.Request + (*GetProviderSchema_Response)(nil), // 58: tfplugin5.GetProviderSchema.Response + nil, // 59: tfplugin5.GetProviderSchema.Response.ResourceSchemasEntry + nil, // 60: tfplugin5.GetProviderSchema.Response.DataSourceSchemasEntry + nil, // 61: tfplugin5.GetProviderSchema.Response.FunctionsEntry + nil, // 62: tfplugin5.GetProviderSchema.Response.EphemeralResourceSchemasEntry + (*PrepareProviderConfig_Request)(nil), // 63: tfplugin5.PrepareProviderConfig.Request + (*PrepareProviderConfig_Response)(nil), // 64: tfplugin5.PrepareProviderConfig.Response + (*UpgradeResourceState_Request)(nil), // 65: tfplugin5.UpgradeResourceState.Request + (*UpgradeResourceState_Response)(nil), // 66: tfplugin5.UpgradeResourceState.Response + (*GetResourceIdentitySchemas_Request)(nil), // 67: tfplugin5.GetResourceIdentitySchemas.Request + (*GetResourceIdentitySchemas_Response)(nil), // 68: tfplugin5.GetResourceIdentitySchemas.Response + nil, // 69: tfplugin5.GetResourceIdentitySchemas.Response.IdentitySchemasEntry + (*UpgradeResourceIdentity_Request)(nil), // 70: tfplugin5.UpgradeResourceIdentity.Request + (*UpgradeResourceIdentity_Response)(nil), // 71: tfplugin5.UpgradeResourceIdentity.Response + (*ValidateResourceTypeConfig_Request)(nil), // 72: tfplugin5.ValidateResourceTypeConfig.Request + (*ValidateResourceTypeConfig_Response)(nil), // 73: tfplugin5.ValidateResourceTypeConfig.Response + (*ValidateDataSourceConfig_Request)(nil), // 74: tfplugin5.ValidateDataSourceConfig.Request + (*ValidateDataSourceConfig_Response)(nil), // 75: tfplugin5.ValidateDataSourceConfig.Response + (*ValidateEphemeralResourceConfig_Request)(nil), // 76: tfplugin5.ValidateEphemeralResourceConfig.Request + (*ValidateEphemeralResourceConfig_Response)(nil), // 77: tfplugin5.ValidateEphemeralResourceConfig.Response + (*Configure_Request)(nil), // 78: tfplugin5.Configure.Request + (*Configure_Response)(nil), // 79: tfplugin5.Configure.Response + (*ReadResource_Request)(nil), // 80: tfplugin5.ReadResource.Request + (*ReadResource_Response)(nil), // 81: tfplugin5.ReadResource.Response + (*PlanResourceChange_Request)(nil), // 82: tfplugin5.PlanResourceChange.Request + (*PlanResourceChange_Response)(nil), // 83: tfplugin5.PlanResourceChange.Response + (*ApplyResourceChange_Request)(nil), // 84: tfplugin5.ApplyResourceChange.Request + (*ApplyResourceChange_Response)(nil), // 85: tfplugin5.ApplyResourceChange.Response + (*ImportResourceState_Request)(nil), // 86: tfplugin5.ImportResourceState.Request + (*ImportResourceState_ImportedResource)(nil), // 87: tfplugin5.ImportResourceState.ImportedResource + (*ImportResourceState_Response)(nil), // 88: tfplugin5.ImportResourceState.Response + (*MoveResourceState_Request)(nil), // 89: tfplugin5.MoveResourceState.Request + (*MoveResourceState_Response)(nil), // 90: tfplugin5.MoveResourceState.Response + (*ReadDataSource_Request)(nil), // 91: tfplugin5.ReadDataSource.Request + (*ReadDataSource_Response)(nil), // 92: tfplugin5.ReadDataSource.Response + (*GetProvisionerSchema_Request)(nil), // 93: tfplugin5.GetProvisionerSchema.Request + (*GetProvisionerSchema_Response)(nil), // 94: tfplugin5.GetProvisionerSchema.Response + (*ValidateProvisionerConfig_Request)(nil), // 95: tfplugin5.ValidateProvisionerConfig.Request + (*ValidateProvisionerConfig_Response)(nil), // 96: tfplugin5.ValidateProvisionerConfig.Response + (*ProvisionResource_Request)(nil), // 97: tfplugin5.ProvisionResource.Request + (*ProvisionResource_Response)(nil), // 98: tfplugin5.ProvisionResource.Response + (*OpenEphemeralResource_Request)(nil), // 99: tfplugin5.OpenEphemeralResource.Request + (*OpenEphemeralResource_Response)(nil), // 100: tfplugin5.OpenEphemeralResource.Response + (*RenewEphemeralResource_Request)(nil), // 101: tfplugin5.RenewEphemeralResource.Request + (*RenewEphemeralResource_Response)(nil), // 102: tfplugin5.RenewEphemeralResource.Response + (*CloseEphemeralResource_Request)(nil), // 103: tfplugin5.CloseEphemeralResource.Request + (*CloseEphemeralResource_Response)(nil), // 104: tfplugin5.CloseEphemeralResource.Response + (*GetFunctions_Request)(nil), // 105: tfplugin5.GetFunctions.Request + (*GetFunctions_Response)(nil), // 106: tfplugin5.GetFunctions.Response + nil, // 107: tfplugin5.GetFunctions.Response.FunctionsEntry + (*CallFunction_Request)(nil), // 108: tfplugin5.CallFunction.Request + (*CallFunction_Response)(nil), // 109: tfplugin5.CallFunction.Response + (*timestamppb.Timestamp)(nil), // 110: google.protobuf.Timestamp } var file_tfplugin5_proto_depIdxs = []int32{ - 1, // 0: tfplugin5.Diagnostic.severity:type_name -> tfplugin5.Diagnostic.Severity - 5, // 1: tfplugin5.Diagnostic.attribute:type_name -> tfplugin5.AttributePath - 23, // 2: tfplugin5.AttributePath.steps:type_name -> tfplugin5.AttributePath.Step - 26, // 3: tfplugin5.RawState.flatmap:type_name -> tfplugin5.RawState.FlatmapEntry - 27, // 4: tfplugin5.Schema.block:type_name -> tfplugin5.Schema.Block - 28, // 5: tfplugin5.Schema.Block.attributes:type_name -> tfplugin5.Schema.Attribute - 29, // 6: tfplugin5.Schema.Block.block_types:type_name -> tfplugin5.Schema.NestedBlock - 0, // 7: tfplugin5.Schema.Block.description_kind:type_name -> tfplugin5.StringKind - 0, // 8: tfplugin5.Schema.Attribute.description_kind:type_name -> tfplugin5.StringKind - 27, // 9: tfplugin5.Schema.NestedBlock.block:type_name -> tfplugin5.Schema.Block - 2, // 10: tfplugin5.Schema.NestedBlock.nesting:type_name -> tfplugin5.Schema.NestedBlock.NestingMode - 8, // 11: tfplugin5.GetProviderSchema.Response.provider:type_name -> tfplugin5.Schema - 33, // 12: tfplugin5.GetProviderSchema.Response.resource_schemas:type_name -> tfplugin5.GetProviderSchema.Response.ResourceSchemasEntry - 34, // 13: tfplugin5.GetProviderSchema.Response.data_source_schemas:type_name -> tfplugin5.GetProviderSchema.Response.DataSourceSchemasEntry - 4, // 14: tfplugin5.GetProviderSchema.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 8, // 15: tfplugin5.GetProviderSchema.Response.provider_meta:type_name -> tfplugin5.Schema - 32, // 16: tfplugin5.GetProviderSchema.Response.server_capabilities:type_name -> tfplugin5.GetProviderSchema.ServerCapabilities - 8, // 17: tfplugin5.GetProviderSchema.Response.ResourceSchemasEntry.value:type_name -> tfplugin5.Schema - 8, // 18: tfplugin5.GetProviderSchema.Response.DataSourceSchemasEntry.value:type_name -> tfplugin5.Schema - 3, // 19: tfplugin5.PrepareProviderConfig.Request.config:type_name -> tfplugin5.DynamicValue - 3, // 20: tfplugin5.PrepareProviderConfig.Response.prepared_config:type_name -> tfplugin5.DynamicValue - 4, // 21: tfplugin5.PrepareProviderConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 7, // 22: tfplugin5.UpgradeResourceState.Request.raw_state:type_name -> tfplugin5.RawState - 3, // 23: tfplugin5.UpgradeResourceState.Response.upgraded_state:type_name -> tfplugin5.DynamicValue - 4, // 24: tfplugin5.UpgradeResourceState.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 25: tfplugin5.ValidateResourceTypeConfig.Request.config:type_name -> tfplugin5.DynamicValue - 4, // 26: tfplugin5.ValidateResourceTypeConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 27: tfplugin5.ValidateDataSourceConfig.Request.config:type_name -> tfplugin5.DynamicValue - 4, // 28: tfplugin5.ValidateDataSourceConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 29: tfplugin5.Configure.Request.config:type_name -> tfplugin5.DynamicValue - 4, // 30: tfplugin5.Configure.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 31: tfplugin5.ReadResource.Request.current_state:type_name -> tfplugin5.DynamicValue - 3, // 32: tfplugin5.ReadResource.Request.provider_meta:type_name -> tfplugin5.DynamicValue - 3, // 33: tfplugin5.ReadResource.Response.new_state:type_name -> tfplugin5.DynamicValue - 4, // 34: tfplugin5.ReadResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 35: tfplugin5.PlanResourceChange.Request.prior_state:type_name -> tfplugin5.DynamicValue - 3, // 36: tfplugin5.PlanResourceChange.Request.proposed_new_state:type_name -> tfplugin5.DynamicValue - 3, // 37: tfplugin5.PlanResourceChange.Request.config:type_name -> tfplugin5.DynamicValue - 3, // 38: tfplugin5.PlanResourceChange.Request.provider_meta:type_name -> tfplugin5.DynamicValue - 3, // 39: tfplugin5.PlanResourceChange.Response.planned_state:type_name -> tfplugin5.DynamicValue - 5, // 40: tfplugin5.PlanResourceChange.Response.requires_replace:type_name -> tfplugin5.AttributePath - 4, // 41: tfplugin5.PlanResourceChange.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 42: tfplugin5.ApplyResourceChange.Request.prior_state:type_name -> tfplugin5.DynamicValue - 3, // 43: tfplugin5.ApplyResourceChange.Request.planned_state:type_name -> tfplugin5.DynamicValue - 3, // 44: tfplugin5.ApplyResourceChange.Request.config:type_name -> tfplugin5.DynamicValue - 3, // 45: tfplugin5.ApplyResourceChange.Request.provider_meta:type_name -> tfplugin5.DynamicValue - 3, // 46: tfplugin5.ApplyResourceChange.Response.new_state:type_name -> tfplugin5.DynamicValue - 4, // 47: tfplugin5.ApplyResourceChange.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 48: tfplugin5.ImportResourceState.ImportedResource.state:type_name -> tfplugin5.DynamicValue - 52, // 49: tfplugin5.ImportResourceState.Response.imported_resources:type_name -> tfplugin5.ImportResourceState.ImportedResource - 4, // 50: tfplugin5.ImportResourceState.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 51: tfplugin5.ReadDataSource.Request.config:type_name -> tfplugin5.DynamicValue - 3, // 52: tfplugin5.ReadDataSource.Request.provider_meta:type_name -> tfplugin5.DynamicValue - 3, // 53: tfplugin5.ReadDataSource.Response.state:type_name -> tfplugin5.DynamicValue - 4, // 54: tfplugin5.ReadDataSource.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 8, // 55: tfplugin5.GetProvisionerSchema.Response.provisioner:type_name -> tfplugin5.Schema - 4, // 56: tfplugin5.GetProvisionerSchema.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 57: tfplugin5.ValidateProvisionerConfig.Request.config:type_name -> tfplugin5.DynamicValue - 4, // 58: tfplugin5.ValidateProvisionerConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 3, // 59: tfplugin5.ProvisionResource.Request.config:type_name -> tfplugin5.DynamicValue - 3, // 60: tfplugin5.ProvisionResource.Request.connection:type_name -> tfplugin5.DynamicValue - 4, // 61: tfplugin5.ProvisionResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic - 30, // 62: tfplugin5.Provider.GetSchema:input_type -> tfplugin5.GetProviderSchema.Request - 35, // 63: tfplugin5.Provider.PrepareProviderConfig:input_type -> tfplugin5.PrepareProviderConfig.Request - 39, // 64: tfplugin5.Provider.ValidateResourceTypeConfig:input_type -> tfplugin5.ValidateResourceTypeConfig.Request - 41, // 65: tfplugin5.Provider.ValidateDataSourceConfig:input_type -> tfplugin5.ValidateDataSourceConfig.Request - 37, // 66: tfplugin5.Provider.UpgradeResourceState:input_type -> tfplugin5.UpgradeResourceState.Request - 43, // 67: tfplugin5.Provider.Configure:input_type -> tfplugin5.Configure.Request - 45, // 68: tfplugin5.Provider.ReadResource:input_type -> tfplugin5.ReadResource.Request - 47, // 69: tfplugin5.Provider.PlanResourceChange:input_type -> tfplugin5.PlanResourceChange.Request - 49, // 70: tfplugin5.Provider.ApplyResourceChange:input_type -> tfplugin5.ApplyResourceChange.Request - 51, // 71: tfplugin5.Provider.ImportResourceState:input_type -> tfplugin5.ImportResourceState.Request - 54, // 72: tfplugin5.Provider.ReadDataSource:input_type -> tfplugin5.ReadDataSource.Request - 24, // 73: tfplugin5.Provider.Stop:input_type -> tfplugin5.Stop.Request - 56, // 74: tfplugin5.Provisioner.GetSchema:input_type -> tfplugin5.GetProvisionerSchema.Request - 58, // 75: tfplugin5.Provisioner.ValidateProvisionerConfig:input_type -> tfplugin5.ValidateProvisionerConfig.Request - 60, // 76: tfplugin5.Provisioner.ProvisionResource:input_type -> tfplugin5.ProvisionResource.Request - 24, // 77: tfplugin5.Provisioner.Stop:input_type -> tfplugin5.Stop.Request - 31, // 78: tfplugin5.Provider.GetSchema:output_type -> tfplugin5.GetProviderSchema.Response - 36, // 79: tfplugin5.Provider.PrepareProviderConfig:output_type -> tfplugin5.PrepareProviderConfig.Response - 40, // 80: tfplugin5.Provider.ValidateResourceTypeConfig:output_type -> tfplugin5.ValidateResourceTypeConfig.Response - 42, // 81: tfplugin5.Provider.ValidateDataSourceConfig:output_type -> tfplugin5.ValidateDataSourceConfig.Response - 38, // 82: tfplugin5.Provider.UpgradeResourceState:output_type -> tfplugin5.UpgradeResourceState.Response - 44, // 83: tfplugin5.Provider.Configure:output_type -> tfplugin5.Configure.Response - 46, // 84: tfplugin5.Provider.ReadResource:output_type -> tfplugin5.ReadResource.Response - 48, // 85: tfplugin5.Provider.PlanResourceChange:output_type -> tfplugin5.PlanResourceChange.Response - 50, // 86: tfplugin5.Provider.ApplyResourceChange:output_type -> tfplugin5.ApplyResourceChange.Response - 53, // 87: tfplugin5.Provider.ImportResourceState:output_type -> tfplugin5.ImportResourceState.Response - 55, // 88: tfplugin5.Provider.ReadDataSource:output_type -> tfplugin5.ReadDataSource.Response - 25, // 89: tfplugin5.Provider.Stop:output_type -> tfplugin5.Stop.Response - 57, // 90: tfplugin5.Provisioner.GetSchema:output_type -> tfplugin5.GetProvisionerSchema.Response - 59, // 91: tfplugin5.Provisioner.ValidateProvisionerConfig:output_type -> tfplugin5.ValidateProvisionerConfig.Response - 61, // 92: tfplugin5.Provisioner.ProvisionResource:output_type -> tfplugin5.ProvisionResource.Response - 25, // 93: tfplugin5.Provisioner.Stop:output_type -> tfplugin5.Stop.Response - 78, // [78:94] is the sub-list for method output_type - 62, // [62:78] is the sub-list for method input_type - 62, // [62:62] is the sub-list for extension type_name - 62, // [62:62] is the sub-list for extension extendee - 0, // [0:62] is the sub-list for field type_name + 1, // 0: tfplugin5.Diagnostic.severity:type_name -> tfplugin5.Diagnostic.Severity + 7, // 1: tfplugin5.Diagnostic.attribute:type_name -> tfplugin5.AttributePath + 41, // 2: tfplugin5.AttributePath.steps:type_name -> tfplugin5.AttributePath.Step + 44, // 3: tfplugin5.RawState.flatmap:type_name -> tfplugin5.RawState.FlatmapEntry + 45, // 4: tfplugin5.ResourceIdentitySchema.identity_attributes:type_name -> tfplugin5.ResourceIdentitySchema.IdentityAttribute + 4, // 5: tfplugin5.ResourceIdentityData.identity_data:type_name -> tfplugin5.DynamicValue + 46, // 6: tfplugin5.Schema.block:type_name -> tfplugin5.Schema.Block + 49, // 7: tfplugin5.Function.parameters:type_name -> tfplugin5.Function.Parameter + 49, // 8: tfplugin5.Function.variadic_parameter:type_name -> tfplugin5.Function.Parameter + 50, // 9: tfplugin5.Function.return:type_name -> tfplugin5.Function.Return + 0, // 10: tfplugin5.Function.description_kind:type_name -> tfplugin5.StringKind + 3, // 11: tfplugin5.Deferred.reason:type_name -> tfplugin5.Deferred.Reason + 47, // 12: tfplugin5.Schema.Block.attributes:type_name -> tfplugin5.Schema.Attribute + 48, // 13: tfplugin5.Schema.Block.block_types:type_name -> tfplugin5.Schema.NestedBlock + 0, // 14: tfplugin5.Schema.Block.description_kind:type_name -> tfplugin5.StringKind + 0, // 15: tfplugin5.Schema.Attribute.description_kind:type_name -> tfplugin5.StringKind + 46, // 16: tfplugin5.Schema.NestedBlock.block:type_name -> tfplugin5.Schema.Block + 2, // 17: tfplugin5.Schema.NestedBlock.nesting:type_name -> tfplugin5.Schema.NestedBlock.NestingMode + 0, // 18: tfplugin5.Function.Parameter.description_kind:type_name -> tfplugin5.StringKind + 13, // 19: tfplugin5.GetMetadata.Response.server_capabilities:type_name -> tfplugin5.ServerCapabilities + 5, // 20: tfplugin5.GetMetadata.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 55, // 21: tfplugin5.GetMetadata.Response.data_sources:type_name -> tfplugin5.GetMetadata.DataSourceMetadata + 56, // 22: tfplugin5.GetMetadata.Response.resources:type_name -> tfplugin5.GetMetadata.ResourceMetadata + 54, // 23: tfplugin5.GetMetadata.Response.functions:type_name -> tfplugin5.GetMetadata.FunctionMetadata + 53, // 24: tfplugin5.GetMetadata.Response.ephemeral_resources:type_name -> tfplugin5.GetMetadata.EphemeralMetadata + 12, // 25: tfplugin5.GetProviderSchema.Response.provider:type_name -> tfplugin5.Schema + 59, // 26: tfplugin5.GetProviderSchema.Response.resource_schemas:type_name -> tfplugin5.GetProviderSchema.Response.ResourceSchemasEntry + 60, // 27: tfplugin5.GetProviderSchema.Response.data_source_schemas:type_name -> tfplugin5.GetProviderSchema.Response.DataSourceSchemasEntry + 61, // 28: tfplugin5.GetProviderSchema.Response.functions:type_name -> tfplugin5.GetProviderSchema.Response.FunctionsEntry + 62, // 29: tfplugin5.GetProviderSchema.Response.ephemeral_resource_schemas:type_name -> tfplugin5.GetProviderSchema.Response.EphemeralResourceSchemasEntry + 5, // 30: tfplugin5.GetProviderSchema.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 12, // 31: tfplugin5.GetProviderSchema.Response.provider_meta:type_name -> tfplugin5.Schema + 13, // 32: tfplugin5.GetProviderSchema.Response.server_capabilities:type_name -> tfplugin5.ServerCapabilities + 12, // 33: tfplugin5.GetProviderSchema.Response.ResourceSchemasEntry.value:type_name -> tfplugin5.Schema + 12, // 34: tfplugin5.GetProviderSchema.Response.DataSourceSchemasEntry.value:type_name -> tfplugin5.Schema + 15, // 35: tfplugin5.GetProviderSchema.Response.FunctionsEntry.value:type_name -> tfplugin5.Function + 12, // 36: tfplugin5.GetProviderSchema.Response.EphemeralResourceSchemasEntry.value:type_name -> tfplugin5.Schema + 4, // 37: tfplugin5.PrepareProviderConfig.Request.config:type_name -> tfplugin5.DynamicValue + 4, // 38: tfplugin5.PrepareProviderConfig.Response.prepared_config:type_name -> tfplugin5.DynamicValue + 5, // 39: tfplugin5.PrepareProviderConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 9, // 40: tfplugin5.UpgradeResourceState.Request.raw_state:type_name -> tfplugin5.RawState + 4, // 41: tfplugin5.UpgradeResourceState.Response.upgraded_state:type_name -> tfplugin5.DynamicValue + 5, // 42: tfplugin5.UpgradeResourceState.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 69, // 43: tfplugin5.GetResourceIdentitySchemas.Response.identity_schemas:type_name -> tfplugin5.GetResourceIdentitySchemas.Response.IdentitySchemasEntry + 5, // 44: tfplugin5.GetResourceIdentitySchemas.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 10, // 45: tfplugin5.GetResourceIdentitySchemas.Response.IdentitySchemasEntry.value:type_name -> tfplugin5.ResourceIdentitySchema + 9, // 46: tfplugin5.UpgradeResourceIdentity.Request.raw_identity:type_name -> tfplugin5.RawState + 11, // 47: tfplugin5.UpgradeResourceIdentity.Response.upgraded_identity:type_name -> tfplugin5.ResourceIdentityData + 5, // 48: tfplugin5.UpgradeResourceIdentity.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 49: tfplugin5.ValidateResourceTypeConfig.Request.config:type_name -> tfplugin5.DynamicValue + 14, // 50: tfplugin5.ValidateResourceTypeConfig.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 5, // 51: tfplugin5.ValidateResourceTypeConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 52: tfplugin5.ValidateDataSourceConfig.Request.config:type_name -> tfplugin5.DynamicValue + 5, // 53: tfplugin5.ValidateDataSourceConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 54: tfplugin5.ValidateEphemeralResourceConfig.Request.config:type_name -> tfplugin5.DynamicValue + 5, // 55: tfplugin5.ValidateEphemeralResourceConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 56: tfplugin5.Configure.Request.config:type_name -> tfplugin5.DynamicValue + 14, // 57: tfplugin5.Configure.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 5, // 58: tfplugin5.Configure.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 59: tfplugin5.ReadResource.Request.current_state:type_name -> tfplugin5.DynamicValue + 4, // 60: tfplugin5.ReadResource.Request.provider_meta:type_name -> tfplugin5.DynamicValue + 14, // 61: tfplugin5.ReadResource.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 11, // 62: tfplugin5.ReadResource.Request.current_identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 63: tfplugin5.ReadResource.Response.new_state:type_name -> tfplugin5.DynamicValue + 5, // 64: tfplugin5.ReadResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 16, // 65: tfplugin5.ReadResource.Response.deferred:type_name -> tfplugin5.Deferred + 11, // 66: tfplugin5.ReadResource.Response.new_identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 67: tfplugin5.PlanResourceChange.Request.prior_state:type_name -> tfplugin5.DynamicValue + 4, // 68: tfplugin5.PlanResourceChange.Request.proposed_new_state:type_name -> tfplugin5.DynamicValue + 4, // 69: tfplugin5.PlanResourceChange.Request.config:type_name -> tfplugin5.DynamicValue + 4, // 70: tfplugin5.PlanResourceChange.Request.provider_meta:type_name -> tfplugin5.DynamicValue + 14, // 71: tfplugin5.PlanResourceChange.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 11, // 72: tfplugin5.PlanResourceChange.Request.prior_identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 73: tfplugin5.PlanResourceChange.Response.planned_state:type_name -> tfplugin5.DynamicValue + 7, // 74: tfplugin5.PlanResourceChange.Response.requires_replace:type_name -> tfplugin5.AttributePath + 5, // 75: tfplugin5.PlanResourceChange.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 16, // 76: tfplugin5.PlanResourceChange.Response.deferred:type_name -> tfplugin5.Deferred + 11, // 77: tfplugin5.PlanResourceChange.Response.planned_identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 78: tfplugin5.ApplyResourceChange.Request.prior_state:type_name -> tfplugin5.DynamicValue + 4, // 79: tfplugin5.ApplyResourceChange.Request.planned_state:type_name -> tfplugin5.DynamicValue + 4, // 80: tfplugin5.ApplyResourceChange.Request.config:type_name -> tfplugin5.DynamicValue + 4, // 81: tfplugin5.ApplyResourceChange.Request.provider_meta:type_name -> tfplugin5.DynamicValue + 11, // 82: tfplugin5.ApplyResourceChange.Request.planned_identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 83: tfplugin5.ApplyResourceChange.Response.new_state:type_name -> tfplugin5.DynamicValue + 5, // 84: tfplugin5.ApplyResourceChange.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 11, // 85: tfplugin5.ApplyResourceChange.Response.new_identity:type_name -> tfplugin5.ResourceIdentityData + 14, // 86: tfplugin5.ImportResourceState.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 11, // 87: tfplugin5.ImportResourceState.Request.identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 88: tfplugin5.ImportResourceState.ImportedResource.state:type_name -> tfplugin5.DynamicValue + 11, // 89: tfplugin5.ImportResourceState.ImportedResource.identity:type_name -> tfplugin5.ResourceIdentityData + 87, // 90: tfplugin5.ImportResourceState.Response.imported_resources:type_name -> tfplugin5.ImportResourceState.ImportedResource + 5, // 91: tfplugin5.ImportResourceState.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 16, // 92: tfplugin5.ImportResourceState.Response.deferred:type_name -> tfplugin5.Deferred + 9, // 93: tfplugin5.MoveResourceState.Request.source_state:type_name -> tfplugin5.RawState + 9, // 94: tfplugin5.MoveResourceState.Request.source_identity:type_name -> tfplugin5.RawState + 4, // 95: tfplugin5.MoveResourceState.Response.target_state:type_name -> tfplugin5.DynamicValue + 5, // 96: tfplugin5.MoveResourceState.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 11, // 97: tfplugin5.MoveResourceState.Response.target_identity:type_name -> tfplugin5.ResourceIdentityData + 4, // 98: tfplugin5.ReadDataSource.Request.config:type_name -> tfplugin5.DynamicValue + 4, // 99: tfplugin5.ReadDataSource.Request.provider_meta:type_name -> tfplugin5.DynamicValue + 14, // 100: tfplugin5.ReadDataSource.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 4, // 101: tfplugin5.ReadDataSource.Response.state:type_name -> tfplugin5.DynamicValue + 5, // 102: tfplugin5.ReadDataSource.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 16, // 103: tfplugin5.ReadDataSource.Response.deferred:type_name -> tfplugin5.Deferred + 12, // 104: tfplugin5.GetProvisionerSchema.Response.provisioner:type_name -> tfplugin5.Schema + 5, // 105: tfplugin5.GetProvisionerSchema.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 106: tfplugin5.ValidateProvisionerConfig.Request.config:type_name -> tfplugin5.DynamicValue + 5, // 107: tfplugin5.ValidateProvisionerConfig.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 108: tfplugin5.ProvisionResource.Request.config:type_name -> tfplugin5.DynamicValue + 4, // 109: tfplugin5.ProvisionResource.Request.connection:type_name -> tfplugin5.DynamicValue + 5, // 110: tfplugin5.ProvisionResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 4, // 111: tfplugin5.OpenEphemeralResource.Request.config:type_name -> tfplugin5.DynamicValue + 14, // 112: tfplugin5.OpenEphemeralResource.Request.client_capabilities:type_name -> tfplugin5.ClientCapabilities + 5, // 113: tfplugin5.OpenEphemeralResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 110, // 114: tfplugin5.OpenEphemeralResource.Response.renew_at:type_name -> google.protobuf.Timestamp + 4, // 115: tfplugin5.OpenEphemeralResource.Response.result:type_name -> tfplugin5.DynamicValue + 16, // 116: tfplugin5.OpenEphemeralResource.Response.deferred:type_name -> tfplugin5.Deferred + 5, // 117: tfplugin5.RenewEphemeralResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 110, // 118: tfplugin5.RenewEphemeralResource.Response.renew_at:type_name -> google.protobuf.Timestamp + 5, // 119: tfplugin5.CloseEphemeralResource.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 107, // 120: tfplugin5.GetFunctions.Response.functions:type_name -> tfplugin5.GetFunctions.Response.FunctionsEntry + 5, // 121: tfplugin5.GetFunctions.Response.diagnostics:type_name -> tfplugin5.Diagnostic + 15, // 122: tfplugin5.GetFunctions.Response.FunctionsEntry.value:type_name -> tfplugin5.Function + 4, // 123: tfplugin5.CallFunction.Request.arguments:type_name -> tfplugin5.DynamicValue + 4, // 124: tfplugin5.CallFunction.Response.result:type_name -> tfplugin5.DynamicValue + 6, // 125: tfplugin5.CallFunction.Response.error:type_name -> tfplugin5.FunctionError + 51, // 126: tfplugin5.Provider.GetMetadata:input_type -> tfplugin5.GetMetadata.Request + 57, // 127: tfplugin5.Provider.GetSchema:input_type -> tfplugin5.GetProviderSchema.Request + 63, // 128: tfplugin5.Provider.PrepareProviderConfig:input_type -> tfplugin5.PrepareProviderConfig.Request + 72, // 129: tfplugin5.Provider.ValidateResourceTypeConfig:input_type -> tfplugin5.ValidateResourceTypeConfig.Request + 74, // 130: tfplugin5.Provider.ValidateDataSourceConfig:input_type -> tfplugin5.ValidateDataSourceConfig.Request + 65, // 131: tfplugin5.Provider.UpgradeResourceState:input_type -> tfplugin5.UpgradeResourceState.Request + 67, // 132: tfplugin5.Provider.GetResourceIdentitySchemas:input_type -> tfplugin5.GetResourceIdentitySchemas.Request + 70, // 133: tfplugin5.Provider.UpgradeResourceIdentity:input_type -> tfplugin5.UpgradeResourceIdentity.Request + 78, // 134: tfplugin5.Provider.Configure:input_type -> tfplugin5.Configure.Request + 80, // 135: tfplugin5.Provider.ReadResource:input_type -> tfplugin5.ReadResource.Request + 82, // 136: tfplugin5.Provider.PlanResourceChange:input_type -> tfplugin5.PlanResourceChange.Request + 84, // 137: tfplugin5.Provider.ApplyResourceChange:input_type -> tfplugin5.ApplyResourceChange.Request + 86, // 138: tfplugin5.Provider.ImportResourceState:input_type -> tfplugin5.ImportResourceState.Request + 89, // 139: tfplugin5.Provider.MoveResourceState:input_type -> tfplugin5.MoveResourceState.Request + 91, // 140: tfplugin5.Provider.ReadDataSource:input_type -> tfplugin5.ReadDataSource.Request + 76, // 141: tfplugin5.Provider.ValidateEphemeralResourceConfig:input_type -> tfplugin5.ValidateEphemeralResourceConfig.Request + 99, // 142: tfplugin5.Provider.OpenEphemeralResource:input_type -> tfplugin5.OpenEphemeralResource.Request + 101, // 143: tfplugin5.Provider.RenewEphemeralResource:input_type -> tfplugin5.RenewEphemeralResource.Request + 103, // 144: tfplugin5.Provider.CloseEphemeralResource:input_type -> tfplugin5.CloseEphemeralResource.Request + 105, // 145: tfplugin5.Provider.GetFunctions:input_type -> tfplugin5.GetFunctions.Request + 108, // 146: tfplugin5.Provider.CallFunction:input_type -> tfplugin5.CallFunction.Request + 42, // 147: tfplugin5.Provider.Stop:input_type -> tfplugin5.Stop.Request + 93, // 148: tfplugin5.Provisioner.GetSchema:input_type -> tfplugin5.GetProvisionerSchema.Request + 95, // 149: tfplugin5.Provisioner.ValidateProvisionerConfig:input_type -> tfplugin5.ValidateProvisionerConfig.Request + 97, // 150: tfplugin5.Provisioner.ProvisionResource:input_type -> tfplugin5.ProvisionResource.Request + 42, // 151: tfplugin5.Provisioner.Stop:input_type -> tfplugin5.Stop.Request + 52, // 152: tfplugin5.Provider.GetMetadata:output_type -> tfplugin5.GetMetadata.Response + 58, // 153: tfplugin5.Provider.GetSchema:output_type -> tfplugin5.GetProviderSchema.Response + 64, // 154: tfplugin5.Provider.PrepareProviderConfig:output_type -> tfplugin5.PrepareProviderConfig.Response + 73, // 155: tfplugin5.Provider.ValidateResourceTypeConfig:output_type -> tfplugin5.ValidateResourceTypeConfig.Response + 75, // 156: tfplugin5.Provider.ValidateDataSourceConfig:output_type -> tfplugin5.ValidateDataSourceConfig.Response + 66, // 157: tfplugin5.Provider.UpgradeResourceState:output_type -> tfplugin5.UpgradeResourceState.Response + 68, // 158: tfplugin5.Provider.GetResourceIdentitySchemas:output_type -> tfplugin5.GetResourceIdentitySchemas.Response + 71, // 159: tfplugin5.Provider.UpgradeResourceIdentity:output_type -> tfplugin5.UpgradeResourceIdentity.Response + 79, // 160: tfplugin5.Provider.Configure:output_type -> tfplugin5.Configure.Response + 81, // 161: tfplugin5.Provider.ReadResource:output_type -> tfplugin5.ReadResource.Response + 83, // 162: tfplugin5.Provider.PlanResourceChange:output_type -> tfplugin5.PlanResourceChange.Response + 85, // 163: tfplugin5.Provider.ApplyResourceChange:output_type -> tfplugin5.ApplyResourceChange.Response + 88, // 164: tfplugin5.Provider.ImportResourceState:output_type -> tfplugin5.ImportResourceState.Response + 90, // 165: tfplugin5.Provider.MoveResourceState:output_type -> tfplugin5.MoveResourceState.Response + 92, // 166: tfplugin5.Provider.ReadDataSource:output_type -> tfplugin5.ReadDataSource.Response + 77, // 167: tfplugin5.Provider.ValidateEphemeralResourceConfig:output_type -> tfplugin5.ValidateEphemeralResourceConfig.Response + 100, // 168: tfplugin5.Provider.OpenEphemeralResource:output_type -> tfplugin5.OpenEphemeralResource.Response + 102, // 169: tfplugin5.Provider.RenewEphemeralResource:output_type -> tfplugin5.RenewEphemeralResource.Response + 104, // 170: tfplugin5.Provider.CloseEphemeralResource:output_type -> tfplugin5.CloseEphemeralResource.Response + 106, // 171: tfplugin5.Provider.GetFunctions:output_type -> tfplugin5.GetFunctions.Response + 109, // 172: tfplugin5.Provider.CallFunction:output_type -> tfplugin5.CallFunction.Response + 43, // 173: tfplugin5.Provider.Stop:output_type -> tfplugin5.Stop.Response + 94, // 174: tfplugin5.Provisioner.GetSchema:output_type -> tfplugin5.GetProvisionerSchema.Response + 96, // 175: tfplugin5.Provisioner.ValidateProvisionerConfig:output_type -> tfplugin5.ValidateProvisionerConfig.Response + 98, // 176: tfplugin5.Provisioner.ProvisionResource:output_type -> tfplugin5.ProvisionResource.Response + 43, // 177: tfplugin5.Provisioner.Stop:output_type -> tfplugin5.Stop.Response + 152, // [152:178] is the sub-list for method output_type + 126, // [126:152] is the sub-list for method input_type + 126, // [126:126] is the sub-list for extension type_name + 126, // [126:126] is the sub-list for extension extendee + 0, // [0:126] is the sub-list for field type_name } func init() { file_tfplugin5_proto_init() } @@ -3990,692 +7051,23 @@ func file_tfplugin5_proto_init() { if File_tfplugin5_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_tfplugin5_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DynamicValue); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Diagnostic); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AttributePath); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Stop); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RawState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PrepareProviderConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpgradeResourceState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateResourceTypeConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateDataSourceConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Configure); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadResource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceChange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyResourceChange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadDataSource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProvisionerSchema); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateProvisionerConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProvisionResource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AttributePath_Step); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Stop_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Stop_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_Block); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_Attribute); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_NestedBlock); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema_ServerCapabilities); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PrepareProviderConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PrepareProviderConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpgradeResourceState_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpgradeResourceState_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateResourceTypeConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateResourceTypeConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateDataSourceConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateDataSourceConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Configure_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Configure_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadResource_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadResource_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceChange_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceChange_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyResourceChange_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyResourceChange_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState_ImportedResource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[51].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadDataSource_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[52].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadDataSource_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[53].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProvisionerSchema_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[54].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProvisionerSchema_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[55].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateProvisionerConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[56].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateProvisionerConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[57].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProvisionResource_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin5_proto_msgTypes[58].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ProvisionResource_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_tfplugin5_proto_msgTypes[20].OneofWrappers = []interface{}{ + file_tfplugin5_proto_msgTypes[2].OneofWrappers = []any{} + file_tfplugin5_proto_msgTypes[37].OneofWrappers = []any{ (*AttributePath_Step_AttributeName)(nil), (*AttributePath_Step_ElementKeyString)(nil), (*AttributePath_Step_ElementKeyInt)(nil), } + file_tfplugin5_proto_msgTypes[96].OneofWrappers = []any{} + file_tfplugin5_proto_msgTypes[97].OneofWrappers = []any{} + file_tfplugin5_proto_msgTypes[98].OneofWrappers = []any{} + file_tfplugin5_proto_msgTypes[99].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_tfplugin5_proto_rawDesc, - NumEnums: 3, - NumMessages: 59, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tfplugin5_proto_rawDesc), len(file_tfplugin5_proto_rawDesc)), + NumEnums: 4, + NumMessages: 106, NumExtensions: 0, NumServices: 2, }, @@ -4685,7 +7077,6 @@ func file_tfplugin5_proto_init() { MessageInfos: file_tfplugin5_proto_msgTypes, }.Build() File_tfplugin5_proto = out.File - file_tfplugin5_proto_rawDesc = nil file_tfplugin5_proto_goTypes = nil file_tfplugin5_proto_depIdxs = nil } @@ -4702,12 +7093,25 @@ const _ = grpc.SupportPackageIsVersion6 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type ProviderClient interface { - // ////// Information about what a provider supports/expects + // GetMetadata returns upfront information about server capabilities and + // supported resource types without requiring the server to instantiate all + // schema information, which may be memory intensive. This RPC is optional, + // where clients may receive an unimplemented RPC error. Clients should + // ignore the error and call the GetSchema RPC as a fallback. + GetMetadata(ctx context.Context, in *GetMetadata_Request, opts ...grpc.CallOption) (*GetMetadata_Response, error) + // GetSchema returns schema information for the provider, data resources, + // and managed resources. GetSchema(ctx context.Context, in *GetProviderSchema_Request, opts ...grpc.CallOption) (*GetProviderSchema_Response, error) PrepareProviderConfig(ctx context.Context, in *PrepareProviderConfig_Request, opts ...grpc.CallOption) (*PrepareProviderConfig_Response, error) ValidateResourceTypeConfig(ctx context.Context, in *ValidateResourceTypeConfig_Request, opts ...grpc.CallOption) (*ValidateResourceTypeConfig_Response, error) ValidateDataSourceConfig(ctx context.Context, in *ValidateDataSourceConfig_Request, opts ...grpc.CallOption) (*ValidateDataSourceConfig_Response, error) UpgradeResourceState(ctx context.Context, in *UpgradeResourceState_Request, opts ...grpc.CallOption) (*UpgradeResourceState_Response, error) + // GetResourceIdentitySchemas returns the identity schemas for all managed + // resources. + GetResourceIdentitySchemas(ctx context.Context, in *GetResourceIdentitySchemas_Request, opts ...grpc.CallOption) (*GetResourceIdentitySchemas_Response, error) + // UpgradeResourceIdentityData should return the upgraded resource identity + // data for a managed resource type. + UpgradeResourceIdentity(ctx context.Context, in *UpgradeResourceIdentity_Request, opts ...grpc.CallOption) (*UpgradeResourceIdentity_Response, error) // ////// One-time initialization, called before other functions below Configure(ctx context.Context, in *Configure_Request, opts ...grpc.CallOption) (*Configure_Response, error) // ////// Managed Resource Lifecycle @@ -4715,7 +7119,17 @@ type ProviderClient interface { PlanResourceChange(ctx context.Context, in *PlanResourceChange_Request, opts ...grpc.CallOption) (*PlanResourceChange_Response, error) ApplyResourceChange(ctx context.Context, in *ApplyResourceChange_Request, opts ...grpc.CallOption) (*ApplyResourceChange_Response, error) ImportResourceState(ctx context.Context, in *ImportResourceState_Request, opts ...grpc.CallOption) (*ImportResourceState_Response, error) + MoveResourceState(ctx context.Context, in *MoveResourceState_Request, opts ...grpc.CallOption) (*MoveResourceState_Response, error) ReadDataSource(ctx context.Context, in *ReadDataSource_Request, opts ...grpc.CallOption) (*ReadDataSource_Response, error) + // ////// Ephemeral Resource Lifecycle + ValidateEphemeralResourceConfig(ctx context.Context, in *ValidateEphemeralResourceConfig_Request, opts ...grpc.CallOption) (*ValidateEphemeralResourceConfig_Response, error) + OpenEphemeralResource(ctx context.Context, in *OpenEphemeralResource_Request, opts ...grpc.CallOption) (*OpenEphemeralResource_Response, error) + RenewEphemeralResource(ctx context.Context, in *RenewEphemeralResource_Request, opts ...grpc.CallOption) (*RenewEphemeralResource_Response, error) + CloseEphemeralResource(ctx context.Context, in *CloseEphemeralResource_Request, opts ...grpc.CallOption) (*CloseEphemeralResource_Response, error) + // GetFunctions returns the definitions of all functions. + GetFunctions(ctx context.Context, in *GetFunctions_Request, opts ...grpc.CallOption) (*GetFunctions_Response, error) + // ////// Provider-contributed Functions + CallFunction(ctx context.Context, in *CallFunction_Request, opts ...grpc.CallOption) (*CallFunction_Response, error) // ////// Graceful Shutdown Stop(ctx context.Context, in *Stop_Request, opts ...grpc.CallOption) (*Stop_Response, error) } @@ -4728,6 +7142,15 @@ func NewProviderClient(cc grpc.ClientConnInterface) ProviderClient { return &providerClient{cc} } +func (c *providerClient) GetMetadata(ctx context.Context, in *GetMetadata_Request, opts ...grpc.CallOption) (*GetMetadata_Response, error) { + out := new(GetMetadata_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/GetMetadata", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) GetSchema(ctx context.Context, in *GetProviderSchema_Request, opts ...grpc.CallOption) (*GetProviderSchema_Response, error) { out := new(GetProviderSchema_Response) err := c.cc.Invoke(ctx, "/tfplugin5.Provider/GetSchema", in, out, opts...) @@ -4773,6 +7196,24 @@ func (c *providerClient) UpgradeResourceState(ctx context.Context, in *UpgradeRe return out, nil } +func (c *providerClient) GetResourceIdentitySchemas(ctx context.Context, in *GetResourceIdentitySchemas_Request, opts ...grpc.CallOption) (*GetResourceIdentitySchemas_Response, error) { + out := new(GetResourceIdentitySchemas_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/GetResourceIdentitySchemas", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) UpgradeResourceIdentity(ctx context.Context, in *UpgradeResourceIdentity_Request, opts ...grpc.CallOption) (*UpgradeResourceIdentity_Response, error) { + out := new(UpgradeResourceIdentity_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/UpgradeResourceIdentity", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) Configure(ctx context.Context, in *Configure_Request, opts ...grpc.CallOption) (*Configure_Response, error) { out := new(Configure_Response) err := c.cc.Invoke(ctx, "/tfplugin5.Provider/Configure", in, out, opts...) @@ -4818,6 +7259,15 @@ func (c *providerClient) ImportResourceState(ctx context.Context, in *ImportReso return out, nil } +func (c *providerClient) MoveResourceState(ctx context.Context, in *MoveResourceState_Request, opts ...grpc.CallOption) (*MoveResourceState_Response, error) { + out := new(MoveResourceState_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/MoveResourceState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) ReadDataSource(ctx context.Context, in *ReadDataSource_Request, opts ...grpc.CallOption) (*ReadDataSource_Response, error) { out := new(ReadDataSource_Response) err := c.cc.Invoke(ctx, "/tfplugin5.Provider/ReadDataSource", in, out, opts...) @@ -4827,6 +7277,60 @@ func (c *providerClient) ReadDataSource(ctx context.Context, in *ReadDataSource_ return out, nil } +func (c *providerClient) ValidateEphemeralResourceConfig(ctx context.Context, in *ValidateEphemeralResourceConfig_Request, opts ...grpc.CallOption) (*ValidateEphemeralResourceConfig_Response, error) { + out := new(ValidateEphemeralResourceConfig_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/ValidateEphemeralResourceConfig", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) OpenEphemeralResource(ctx context.Context, in *OpenEphemeralResource_Request, opts ...grpc.CallOption) (*OpenEphemeralResource_Response, error) { + out := new(OpenEphemeralResource_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/OpenEphemeralResource", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) RenewEphemeralResource(ctx context.Context, in *RenewEphemeralResource_Request, opts ...grpc.CallOption) (*RenewEphemeralResource_Response, error) { + out := new(RenewEphemeralResource_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/RenewEphemeralResource", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) CloseEphemeralResource(ctx context.Context, in *CloseEphemeralResource_Request, opts ...grpc.CallOption) (*CloseEphemeralResource_Response, error) { + out := new(CloseEphemeralResource_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/CloseEphemeralResource", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) GetFunctions(ctx context.Context, in *GetFunctions_Request, opts ...grpc.CallOption) (*GetFunctions_Response, error) { + out := new(GetFunctions_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/GetFunctions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) CallFunction(ctx context.Context, in *CallFunction_Request, opts ...grpc.CallOption) (*CallFunction_Response, error) { + out := new(CallFunction_Response) + err := c.cc.Invoke(ctx, "/tfplugin5.Provider/CallFunction", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) Stop(ctx context.Context, in *Stop_Request, opts ...grpc.CallOption) (*Stop_Response, error) { out := new(Stop_Response) err := c.cc.Invoke(ctx, "/tfplugin5.Provider/Stop", in, out, opts...) @@ -4838,12 +7342,25 @@ func (c *providerClient) Stop(ctx context.Context, in *Stop_Request, opts ...grp // ProviderServer is the server API for Provider service. type ProviderServer interface { - // ////// Information about what a provider supports/expects + // GetMetadata returns upfront information about server capabilities and + // supported resource types without requiring the server to instantiate all + // schema information, which may be memory intensive. This RPC is optional, + // where clients may receive an unimplemented RPC error. Clients should + // ignore the error and call the GetSchema RPC as a fallback. + GetMetadata(context.Context, *GetMetadata_Request) (*GetMetadata_Response, error) + // GetSchema returns schema information for the provider, data resources, + // and managed resources. GetSchema(context.Context, *GetProviderSchema_Request) (*GetProviderSchema_Response, error) PrepareProviderConfig(context.Context, *PrepareProviderConfig_Request) (*PrepareProviderConfig_Response, error) ValidateResourceTypeConfig(context.Context, *ValidateResourceTypeConfig_Request) (*ValidateResourceTypeConfig_Response, error) ValidateDataSourceConfig(context.Context, *ValidateDataSourceConfig_Request) (*ValidateDataSourceConfig_Response, error) UpgradeResourceState(context.Context, *UpgradeResourceState_Request) (*UpgradeResourceState_Response, error) + // GetResourceIdentitySchemas returns the identity schemas for all managed + // resources. + GetResourceIdentitySchemas(context.Context, *GetResourceIdentitySchemas_Request) (*GetResourceIdentitySchemas_Response, error) + // UpgradeResourceIdentityData should return the upgraded resource identity + // data for a managed resource type. + UpgradeResourceIdentity(context.Context, *UpgradeResourceIdentity_Request) (*UpgradeResourceIdentity_Response, error) // ////// One-time initialization, called before other functions below Configure(context.Context, *Configure_Request) (*Configure_Response, error) // ////// Managed Resource Lifecycle @@ -4851,7 +7368,17 @@ type ProviderServer interface { PlanResourceChange(context.Context, *PlanResourceChange_Request) (*PlanResourceChange_Response, error) ApplyResourceChange(context.Context, *ApplyResourceChange_Request) (*ApplyResourceChange_Response, error) ImportResourceState(context.Context, *ImportResourceState_Request) (*ImportResourceState_Response, error) + MoveResourceState(context.Context, *MoveResourceState_Request) (*MoveResourceState_Response, error) ReadDataSource(context.Context, *ReadDataSource_Request) (*ReadDataSource_Response, error) + // ////// Ephemeral Resource Lifecycle + ValidateEphemeralResourceConfig(context.Context, *ValidateEphemeralResourceConfig_Request) (*ValidateEphemeralResourceConfig_Response, error) + OpenEphemeralResource(context.Context, *OpenEphemeralResource_Request) (*OpenEphemeralResource_Response, error) + RenewEphemeralResource(context.Context, *RenewEphemeralResource_Request) (*RenewEphemeralResource_Response, error) + CloseEphemeralResource(context.Context, *CloseEphemeralResource_Request) (*CloseEphemeralResource_Response, error) + // GetFunctions returns the definitions of all functions. + GetFunctions(context.Context, *GetFunctions_Request) (*GetFunctions_Response, error) + // ////// Provider-contributed Functions + CallFunction(context.Context, *CallFunction_Request) (*CallFunction_Response, error) // ////// Graceful Shutdown Stop(context.Context, *Stop_Request) (*Stop_Response, error) } @@ -4860,6 +7387,9 @@ type ProviderServer interface { type UnimplementedProviderServer struct { } +func (*UnimplementedProviderServer) GetMetadata(context.Context, *GetMetadata_Request) (*GetMetadata_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMetadata not implemented") +} func (*UnimplementedProviderServer) GetSchema(context.Context, *GetProviderSchema_Request) (*GetProviderSchema_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method GetSchema not implemented") } @@ -4875,6 +7405,12 @@ func (*UnimplementedProviderServer) ValidateDataSourceConfig(context.Context, *V func (*UnimplementedProviderServer) UpgradeResourceState(context.Context, *UpgradeResourceState_Request) (*UpgradeResourceState_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method UpgradeResourceState not implemented") } +func (*UnimplementedProviderServer) GetResourceIdentitySchemas(context.Context, *GetResourceIdentitySchemas_Request) (*GetResourceIdentitySchemas_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetResourceIdentitySchemas not implemented") +} +func (*UnimplementedProviderServer) UpgradeResourceIdentity(context.Context, *UpgradeResourceIdentity_Request) (*UpgradeResourceIdentity_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpgradeResourceIdentity not implemented") +} func (*UnimplementedProviderServer) Configure(context.Context, *Configure_Request) (*Configure_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method Configure not implemented") } @@ -4890,9 +7426,30 @@ func (*UnimplementedProviderServer) ApplyResourceChange(context.Context, *ApplyR func (*UnimplementedProviderServer) ImportResourceState(context.Context, *ImportResourceState_Request) (*ImportResourceState_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method ImportResourceState not implemented") } +func (*UnimplementedProviderServer) MoveResourceState(context.Context, *MoveResourceState_Request) (*MoveResourceState_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method MoveResourceState not implemented") +} func (*UnimplementedProviderServer) ReadDataSource(context.Context, *ReadDataSource_Request) (*ReadDataSource_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method ReadDataSource not implemented") } +func (*UnimplementedProviderServer) ValidateEphemeralResourceConfig(context.Context, *ValidateEphemeralResourceConfig_Request) (*ValidateEphemeralResourceConfig_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ValidateEphemeralResourceConfig not implemented") +} +func (*UnimplementedProviderServer) OpenEphemeralResource(context.Context, *OpenEphemeralResource_Request) (*OpenEphemeralResource_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenEphemeralResource not implemented") +} +func (*UnimplementedProviderServer) RenewEphemeralResource(context.Context, *RenewEphemeralResource_Request) (*RenewEphemeralResource_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method RenewEphemeralResource not implemented") +} +func (*UnimplementedProviderServer) CloseEphemeralResource(context.Context, *CloseEphemeralResource_Request) (*CloseEphemeralResource_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseEphemeralResource not implemented") +} +func (*UnimplementedProviderServer) GetFunctions(context.Context, *GetFunctions_Request) (*GetFunctions_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetFunctions not implemented") +} +func (*UnimplementedProviderServer) CallFunction(context.Context, *CallFunction_Request) (*CallFunction_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CallFunction not implemented") +} func (*UnimplementedProviderServer) Stop(context.Context, *Stop_Request) (*Stop_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method Stop not implemented") } @@ -4901,6 +7458,24 @@ func RegisterProviderServer(s *grpc.Server, srv ProviderServer) { s.RegisterService(&_Provider_serviceDesc, srv) } +func _Provider_GetMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMetadata_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).GetMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/GetMetadata", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).GetMetadata(ctx, req.(*GetMetadata_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_GetSchema_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetProviderSchema_Request) if err := dec(in); err != nil { @@ -4991,6 +7566,42 @@ func _Provider_UpgradeResourceState_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _Provider_GetResourceIdentitySchemas_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceIdentitySchemas_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).GetResourceIdentitySchemas(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/GetResourceIdentitySchemas", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).GetResourceIdentitySchemas(ctx, req.(*GetResourceIdentitySchemas_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_UpgradeResourceIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpgradeResourceIdentity_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).UpgradeResourceIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/UpgradeResourceIdentity", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).UpgradeResourceIdentity(ctx, req.(*UpgradeResourceIdentity_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_Configure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Configure_Request) if err := dec(in); err != nil { @@ -5081,6 +7692,24 @@ func _Provider_ImportResourceState_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _Provider_MoveResourceState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MoveResourceState_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).MoveResourceState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/MoveResourceState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).MoveResourceState(ctx, req.(*MoveResourceState_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_ReadDataSource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ReadDataSource_Request) if err := dec(in); err != nil { @@ -5099,6 +7728,114 @@ func _Provider_ReadDataSource_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Provider_ValidateEphemeralResourceConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateEphemeralResourceConfig_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).ValidateEphemeralResourceConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/ValidateEphemeralResourceConfig", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).ValidateEphemeralResourceConfig(ctx, req.(*ValidateEphemeralResourceConfig_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_OpenEphemeralResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenEphemeralResource_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).OpenEphemeralResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/OpenEphemeralResource", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).OpenEphemeralResource(ctx, req.(*OpenEphemeralResource_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_RenewEphemeralResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RenewEphemeralResource_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).RenewEphemeralResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/RenewEphemeralResource", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).RenewEphemeralResource(ctx, req.(*RenewEphemeralResource_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_CloseEphemeralResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseEphemeralResource_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).CloseEphemeralResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/CloseEphemeralResource", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).CloseEphemeralResource(ctx, req.(*CloseEphemeralResource_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_GetFunctions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetFunctions_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).GetFunctions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/GetFunctions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).GetFunctions(ctx, req.(*GetFunctions_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_CallFunction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CallFunction_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).CallFunction(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin5.Provider/CallFunction", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).CallFunction(ctx, req.(*CallFunction_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Stop_Request) if err := dec(in); err != nil { @@ -5121,6 +7858,10 @@ var _Provider_serviceDesc = grpc.ServiceDesc{ ServiceName: "tfplugin5.Provider", HandlerType: (*ProviderServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "GetMetadata", + Handler: _Provider_GetMetadata_Handler, + }, { MethodName: "GetSchema", Handler: _Provider_GetSchema_Handler, @@ -5141,6 +7882,14 @@ var _Provider_serviceDesc = grpc.ServiceDesc{ MethodName: "UpgradeResourceState", Handler: _Provider_UpgradeResourceState_Handler, }, + { + MethodName: "GetResourceIdentitySchemas", + Handler: _Provider_GetResourceIdentitySchemas_Handler, + }, + { + MethodName: "UpgradeResourceIdentity", + Handler: _Provider_UpgradeResourceIdentity_Handler, + }, { MethodName: "Configure", Handler: _Provider_Configure_Handler, @@ -5161,10 +7910,38 @@ var _Provider_serviceDesc = grpc.ServiceDesc{ MethodName: "ImportResourceState", Handler: _Provider_ImportResourceState_Handler, }, + { + MethodName: "MoveResourceState", + Handler: _Provider_MoveResourceState_Handler, + }, { MethodName: "ReadDataSource", Handler: _Provider_ReadDataSource_Handler, }, + { + MethodName: "ValidateEphemeralResourceConfig", + Handler: _Provider_ValidateEphemeralResourceConfig_Handler, + }, + { + MethodName: "OpenEphemeralResource", + Handler: _Provider_OpenEphemeralResource_Handler, + }, + { + MethodName: "RenewEphemeralResource", + Handler: _Provider_RenewEphemeralResource_Handler, + }, + { + MethodName: "CloseEphemeralResource", + Handler: _Provider_CloseEphemeralResource_Handler, + }, + { + MethodName: "GetFunctions", + Handler: _Provider_GetFunctions_Handler, + }, + { + MethodName: "CallFunction", + Handler: _Provider_CallFunction_Handler, + }, { MethodName: "Stop", Handler: _Provider_Stop_Handler, diff --git a/internal/tfplugin5/tfplugin5.proto b/internal/tfplugin5/tfplugin5.proto index 0bdfe34554..83573e6a2b 120000 --- a/internal/tfplugin5/tfplugin5.proto +++ b/internal/tfplugin5/tfplugin5.proto @@ -1 +1 @@ -../../docs/plugin-protocol/tfplugin5.3.proto \ No newline at end of file +../../docs/plugin-protocol/tfplugin5.proto \ No newline at end of file diff --git a/internal/tfplugin6/.copywrite.hcl b/internal/tfplugin6/.copywrite.hcl new file mode 100644 index 0000000000..2c144ddb7d --- /dev/null +++ b/internal/tfplugin6/.copywrite.hcl @@ -0,0 +1,6 @@ +schema_version = 1 + +project { + license = "MPL-2.0" + copyright_year = 2024 +} diff --git a/internal/tfplugin6/LICENSE b/internal/tfplugin6/LICENSE new file mode 100644 index 0000000000..e25da5fad9 --- /dev/null +++ b/internal/tfplugin6/LICENSE @@ -0,0 +1,355 @@ +Copyright (c) 2014 HashiCorp, Inc. + +Mozilla Public License, version 2.0 + +1. Definitions + +1.1. “Contributor” + + means each individual or legal entity that creates, contributes to the + creation of, or owns Covered Software. + +1.2. “Contributor Version” + + means the combination of the Contributions of others (if any) used by a + Contributor and that particular Contributor’s Contribution. + +1.3. “Contribution” + + means Covered Software of a particular Contributor. + +1.4. “Covered Software” + + means Source Code Form to which the initial Contributor has attached the + notice in Exhibit A, the Executable Form of such Source Code Form, and + Modifications of such Source Code Form, in each case including portions + thereof. + +1.5. “Incompatible With Secondary Licenses” + means + + a. that the initial Contributor has attached the notice described in + Exhibit B to the Covered Software; or + + b. that the Covered Software was made available under the terms of version + 1.1 or earlier of the License, but not also under the terms of a + Secondary License. + +1.6. “Executable Form” + + means any form of the work other than Source Code Form. + +1.7. “Larger Work” + + means a work that combines Covered Software with other material, in a separate + file or files, that is not Covered Software. + +1.8. “License” + + means this document. + +1.9. “Licensable” + + means having the right to grant, to the maximum extent possible, whether at the + time of the initial grant or subsequently, any and all of the rights conveyed by + this License. + +1.10. “Modifications” + + means any of the following: + + a. any file in Source Code Form that results from an addition to, deletion + from, or modification of the contents of Covered Software; or + + b. any new file in Source Code Form that contains any Covered Software. + +1.11. “Patent Claims” of a Contributor + + means any patent claim(s), including without limitation, method, process, + and apparatus claims, in any patent Licensable by such Contributor that + would be infringed, but for the grant of the License, by the making, + using, selling, offering for sale, having made, import, or transfer of + either its Contributions or its Contributor Version. + +1.12. “Secondary License” + + means either the GNU General Public License, Version 2.0, the GNU Lesser + General Public License, Version 2.1, the GNU Affero General Public + License, Version 3.0, or any later versions of those licenses. + +1.13. “Source Code Form” + + means the form of the work preferred for making modifications. + +1.14. “You” (or “Your”) + + means an individual or a legal entity exercising rights under this + License. For legal entities, “You” includes any entity that controls, is + controlled by, or is under common control with You. For purposes of this + definition, “control” means (a) the power, direct or indirect, to cause + the direction or management of such entity, whether by contract or + otherwise, or (b) ownership of more than fifty percent (50%) of the + outstanding shares or beneficial ownership of such entity. + + +2. License Grants and Conditions + +2.1. Grants + + Each Contributor hereby grants You a world-wide, royalty-free, + non-exclusive license: + + a. under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or as + part of a Larger Work; and + + b. under Patent Claims of such Contributor to make, use, sell, offer for + sale, have made, import, and otherwise transfer either its Contributions + or its Contributor Version. + +2.2. Effective Date + + The licenses granted in Section 2.1 with respect to any Contribution become + effective for each Contribution on the date the Contributor first distributes + such Contribution. + +2.3. Limitations on Grant Scope + + The licenses granted in this Section 2 are the only rights granted under this + License. No additional rights or licenses will be implied from the distribution + or licensing of Covered Software under this License. Notwithstanding Section + 2.1(b) above, no patent license is granted by a Contributor: + + a. for any code that a Contributor has removed from Covered Software; or + + b. for infringements caused by: (i) Your and any other third party’s + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + + c. under Patent Claims infringed by Covered Software in the absence of its + Contributions. + + This License does not grant any rights in the trademarks, service marks, or + logos of any Contributor (except as may be necessary to comply with the + notice requirements in Section 3.4). + +2.4. Subsequent Licenses + + No Contributor makes additional grants as a result of Your choice to + distribute the Covered Software under a subsequent version of this License + (see Section 10.2) or under the terms of a Secondary License (if permitted + under the terms of Section 3.3). + +2.5. Representation + + Each Contributor represents that the Contributor believes its Contributions + are its original creation(s) or it has sufficient rights to grant the + rights to its Contributions conveyed by this License. + +2.6. Fair Use + + This License is not intended to limit any rights You have under applicable + copyright doctrines of fair use, fair dealing, or other equivalents. + +2.7. Conditions + + Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in + Section 2.1. + + +3. Responsibilities + +3.1. Distribution of Source Form + + All distribution of Covered Software in Source Code Form, including any + Modifications that You create or to which You contribute, must be under the + terms of this License. You must inform recipients that the Source Code Form + of the Covered Software is governed by the terms of this License, and how + they can obtain a copy of this License. You may not attempt to alter or + restrict the recipients’ rights in the Source Code Form. + +3.2. Distribution of Executable Form + + If You distribute Covered Software in Executable Form then: + + a. such Covered Software must also be made available in Source Code Form, + as described in Section 3.1, and You must inform recipients of the + Executable Form how they can obtain a copy of such Source Code Form by + reasonable means in a timely manner, at a charge no more than the cost + of distribution to the recipient; and + + b. You may distribute such Executable Form under the terms of this License, + or sublicense it under different terms, provided that the license for + the Executable Form does not attempt to limit or alter the recipients’ + rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + + You may create and distribute a Larger Work under terms of Your choice, + provided that You also comply with the requirements of this License for the + Covered Software. If the Larger Work is a combination of Covered Software + with a work governed by one or more Secondary Licenses, and the Covered + Software is not Incompatible With Secondary Licenses, this License permits + You to additionally distribute such Covered Software under the terms of + such Secondary License(s), so that the recipient of the Larger Work may, at + their option, further distribute the Covered Software under the terms of + either this License or such Secondary License(s). + +3.4. Notices + + You may not remove or alter the substance of any license notices (including + copyright notices, patent notices, disclaimers of warranty, or limitations + of liability) contained within the Source Code Form of the Covered + Software, except that You may alter any license notices to the extent + required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + + You may choose to offer, and to charge a fee for, warranty, support, + indemnity or liability obligations to one or more recipients of Covered + Software. However, You may do so only on Your own behalf, and not on behalf + of any Contributor. You must make it absolutely clear that any such + warranty, support, indemnity, or liability obligation is offered by You + alone, and You hereby agree to indemnify every Contributor for any + liability incurred by such Contributor as a result of warranty, support, + indemnity or liability terms You offer. You may include additional + disclaimers of warranty and limitations of liability specific to any + jurisdiction. + +4. Inability to Comply Due to Statute or Regulation + + If it is impossible for You to comply with any of the terms of this License + with respect to some or all of the Covered Software due to statute, judicial + order, or regulation then You must: (a) comply with the terms of this License + to the maximum extent possible; and (b) describe the limitations and the code + they affect. Such description must be placed in a text file included with all + distributions of the Covered Software under this License. Except to the + extent prohibited by statute or regulation, such description must be + sufficiently detailed for a recipient of ordinary skill to be able to + understand it. + +5. Termination + +5.1. The rights granted under this License will terminate automatically if You + fail to comply with any of its terms. However, if You become compliant, + then the rights granted under this License from a particular Contributor + are reinstated (a) provisionally, unless and until such Contributor + explicitly and finally terminates Your grants, and (b) on an ongoing basis, + if such Contributor fails to notify You of the non-compliance by some + reasonable means prior to 60 days after You have come back into compliance. + Moreover, Your grants from a particular Contributor are reinstated on an + ongoing basis if such Contributor notifies You of the non-compliance by + some reasonable means, this is the first time You have received notice of + non-compliance with this License from such Contributor, and You become + compliant prior to 30 days after Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent + infringement claim (excluding declaratory judgment actions, counter-claims, + and cross-claims) alleging that a Contributor Version directly or + indirectly infringes any patent, then the rights granted to You by any and + all Contributors for the Covered Software under Section 2.1 of this License + shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user + license agreements (excluding distributors and resellers) which have been + validly granted by You or Your distributors under this License prior to + termination shall survive termination. + +6. Disclaimer of Warranty + + Covered Software is provided under this License on an “as is” basis, without + warranty of any kind, either expressed, implied, or statutory, including, + without limitation, warranties that the Covered Software is free of defects, + merchantable, fit for a particular purpose or non-infringing. The entire + risk as to the quality and performance of the Covered Software is with You. + Should any Covered Software prove defective in any respect, You (not any + Contributor) assume the cost of any necessary servicing, repair, or + correction. This disclaimer of warranty constitutes an essential part of this + License. No use of any Covered Software is authorized under this License + except under this disclaimer. + +7. Limitation of Liability + + Under no circumstances and under no legal theory, whether tort (including + negligence), contract, or otherwise, shall any Contributor, or anyone who + distributes Covered Software as permitted above, be liable to You for any + direct, indirect, special, incidental, or consequential damages of any + character including, without limitation, damages for lost profits, loss of + goodwill, work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses, even if such party shall have been + informed of the possibility of such damages. This limitation of liability + shall not apply to liability for death or personal injury resulting from such + party’s negligence to the extent applicable law prohibits such limitation. + Some jurisdictions do not allow the exclusion or limitation of incidental or + consequential damages, so this exclusion and limitation may not apply to You. + +8. Litigation + + Any litigation relating to this License may be brought only in the courts of + a jurisdiction where the defendant maintains its principal place of business + and such litigation shall be governed by laws of that jurisdiction, without + reference to its conflict-of-law provisions. Nothing in this Section shall + prevent a party’s ability to bring cross-claims or counter-claims. + +9. Miscellaneous + + This License represents the complete agreement concerning the subject matter + hereof. If any provision of this License is held to be unenforceable, such + provision shall be reformed only to the extent necessary to make it + enforceable. Any law or regulation which provides that the language of a + contract shall be construed against the drafter shall not be used to construe + this License against a Contributor. + + +10. Versions of the License + +10.1. New Versions + + Mozilla Foundation is the license steward. Except as provided in Section + 10.3, no one other than the license steward has the right to modify or + publish new versions of this License. Each version will be given a + distinguishing version number. + +10.2. Effect of New Versions + + You may distribute the Covered Software under the terms of the version of + the License under which You originally received the Covered Software, or + under the terms of any subsequent version published by the license + steward. + +10.3. Modified Versions + + If you create software not governed by this License, and you want to + create a new license for such software, you may create and use a modified + version of this License if you rename the license and remove any + references to the name of the license steward (except to note that such + modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + If You choose to distribute Source Code Form that is Incompatible With + Secondary Licenses under the terms of this version of the License, the + notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the + terms of the Mozilla Public License, v. + 2.0. If a copy of the MPL was not + distributed with this file, You can + obtain one at + http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular file, then +You may include the notice in a location (such as a LICENSE file in a relevant +directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is “Incompatible + With Secondary Licenses”, as defined by + the Mozilla Public License, v. 2.0. diff --git a/internal/tfplugin6/tfplugin6.pb.go b/internal/tfplugin6/tfplugin6.pb.go index 6e101cd21b..965661e552 100644 --- a/internal/tfplugin6/tfplugin6.pb.go +++ b/internal/tfplugin6/tfplugin6.pb.go @@ -1,13 +1,15 @@ -// Terraform Plugin RPC protocol version 6.3 +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Terraform Plugin RPC protocol version 6.9 // -// This file defines version 6.3 of the RPC protocol. To implement a plugin +// This file defines version 6.9 of the RPC protocol. To implement a plugin // against this protocol, copy this definition into your own codebase and // use protoc to generate stubs for your target language. // -// This file will not be updated. Any minor versions of protocol 6 to follow -// should copy this file and modify the copy while maintaing backwards -// compatibility. Breaking changes, if any are required, will come -// in a subsequent major version with its own separate proto definition. +// Any minor versions of protocol 6 to follow should modify this file while +// maintaining backwards compatibility. Breaking changes, if any are required, +// will come in a subsequent major version with its own separate proto definition. // // Note that only the proto files included in a release tag of Terraform are // official protocol releases. Proto files taken from other commits may include @@ -19,7 +21,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.27.1 +// protoc-gen-go v1.36.5 // protoc v3.15.6 // source: tfplugin6.proto @@ -32,8 +34,10 @@ import ( status "google.golang.org/grpc/status" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" reflect "reflect" sync "sync" + unsafe "unsafe" ) const ( @@ -193,7 +197,7 @@ func (x Schema_NestedBlock_NestingMode) Number() protoreflect.EnumNumber { // Deprecated: Use Schema_NestedBlock_NestingMode.Descriptor instead. func (Schema_NestedBlock_NestingMode) EnumDescriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5, 2, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{8, 2, 0} } type Schema_Object_NestingMode int32 @@ -248,27 +252,83 @@ func (x Schema_Object_NestingMode) Number() protoreflect.EnumNumber { // Deprecated: Use Schema_Object_NestingMode.Descriptor instead. func (Schema_Object_NestingMode) EnumDescriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5, 3, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{8, 3, 0} +} + +// Reason is the reason for deferring the change. +type Deferred_Reason int32 + +const ( + // UNKNOWN is the default value, and should not be used. + Deferred_UNKNOWN Deferred_Reason = 0 + // RESOURCE_CONFIG_UNKNOWN is used when the config is partially unknown and the real + // values need to be known before the change can be planned. + Deferred_RESOURCE_CONFIG_UNKNOWN Deferred_Reason = 1 + // PROVIDER_CONFIG_UNKNOWN is used when parts of the provider configuration + // are unknown, e.g. the provider configuration is only known after the apply is done. + Deferred_PROVIDER_CONFIG_UNKNOWN Deferred_Reason = 2 + // ABSENT_PREREQ is used when a hard dependency has not been satisfied. + Deferred_ABSENT_PREREQ Deferred_Reason = 3 +) + +// Enum value maps for Deferred_Reason. +var ( + Deferred_Reason_name = map[int32]string{ + 0: "UNKNOWN", + 1: "RESOURCE_CONFIG_UNKNOWN", + 2: "PROVIDER_CONFIG_UNKNOWN", + 3: "ABSENT_PREREQ", + } + Deferred_Reason_value = map[string]int32{ + "UNKNOWN": 0, + "RESOURCE_CONFIG_UNKNOWN": 1, + "PROVIDER_CONFIG_UNKNOWN": 2, + "ABSENT_PREREQ": 3, + } +) + +func (x Deferred_Reason) Enum() *Deferred_Reason { + p := new(Deferred_Reason) + *p = x + return p +} + +func (x Deferred_Reason) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Deferred_Reason) Descriptor() protoreflect.EnumDescriptor { + return file_tfplugin6_proto_enumTypes[4].Descriptor() +} + +func (Deferred_Reason) Type() protoreflect.EnumType { + return &file_tfplugin6_proto_enumTypes[4] +} + +func (x Deferred_Reason) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Deferred_Reason.Descriptor instead. +func (Deferred_Reason) EnumDescriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{12, 0} } // DynamicValue is an opaque encoding of terraform data, with the field name // indicating the encoding scheme used. type DynamicValue struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` + Json []byte `protobuf:"bytes,2,opt,name=json,proto3" json:"json,omitempty"` unknownFields protoimpl.UnknownFields - - Msgpack []byte `protobuf:"bytes,1,opt,name=msgpack,proto3" json:"msgpack,omitempty"` - Json []byte `protobuf:"bytes,2,opt,name=json,proto3" json:"json,omitempty"` + sizeCache protoimpl.SizeCache } func (x *DynamicValue) Reset() { *x = DynamicValue{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *DynamicValue) String() string { @@ -279,7 +339,7 @@ func (*DynamicValue) ProtoMessage() {} func (x *DynamicValue) ProtoReflect() protoreflect.Message { mi := &file_tfplugin6_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -309,23 +369,20 @@ func (x *DynamicValue) GetJson() []byte { } type Diagnostic struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Severity Diagnostic_Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=tfplugin6.Diagnostic_Severity" json:"severity,omitempty"` + Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` + Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` + Attribute *AttributePath `protobuf:"bytes,4,opt,name=attribute,proto3" json:"attribute,omitempty"` unknownFields protoimpl.UnknownFields - - Severity Diagnostic_Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=tfplugin6.Diagnostic_Severity" json:"severity,omitempty"` - Summary string `protobuf:"bytes,2,opt,name=summary,proto3" json:"summary,omitempty"` - Detail string `protobuf:"bytes,3,opt,name=detail,proto3" json:"detail,omitempty"` - Attribute *AttributePath `protobuf:"bytes,4,opt,name=attribute,proto3" json:"attribute,omitempty"` + sizeCache protoimpl.SizeCache } func (x *Diagnostic) Reset() { *x = Diagnostic{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Diagnostic) String() string { @@ -336,7 +393,7 @@ func (*Diagnostic) ProtoMessage() {} func (x *Diagnostic) ProtoReflect() protoreflect.Message { mi := &file_tfplugin6_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -379,21 +436,72 @@ func (x *Diagnostic) GetAttribute() *AttributePath { return nil } -type AttributePath struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +type FunctionError struct { + state protoimpl.MessageState `protogen:"open.v1"` + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` + // The optional function_argument records the index position of the + // argument which caused the error. + FunctionArgument *int64 `protobuf:"varint,2,opt,name=function_argument,json=functionArgument,proto3,oneof" json:"function_argument,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - Steps []*AttributePath_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` +func (x *FunctionError) Reset() { + *x = FunctionError{} + mi := &file_tfplugin6_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FunctionError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FunctionError) ProtoMessage() {} + +func (x *FunctionError) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FunctionError.ProtoReflect.Descriptor instead. +func (*FunctionError) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{2} +} + +func (x *FunctionError) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *FunctionError) GetFunctionArgument() int64 { + if x != nil && x.FunctionArgument != nil { + return *x.FunctionArgument + } + return 0 +} + +type AttributePath struct { + state protoimpl.MessageState `protogen:"open.v1"` + Steps []*AttributePath_Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttributePath) Reset() { *x = AttributePath{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AttributePath) String() string { @@ -403,8 +511,8 @@ func (x *AttributePath) String() string { func (*AttributePath) ProtoMessage() {} func (x *AttributePath) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[3] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -416,7 +524,7 @@ func (x *AttributePath) ProtoReflect() protoreflect.Message { // Deprecated: Use AttributePath.ProtoReflect.Descriptor instead. func (*AttributePath) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{2} + return file_tfplugin6_proto_rawDescGZIP(), []int{3} } func (x *AttributePath) GetSteps() []*AttributePath_Step { @@ -427,18 +535,16 @@ func (x *AttributePath) GetSteps() []*AttributePath_Step { } type StopProvider struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StopProvider) Reset() { *x = StopProvider{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StopProvider) String() string { @@ -448,8 +554,8 @@ func (x *StopProvider) String() string { func (*StopProvider) ProtoMessage() {} func (x *StopProvider) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[4] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -461,28 +567,25 @@ func (x *StopProvider) ProtoReflect() protoreflect.Message { // Deprecated: Use StopProvider.ProtoReflect.Descriptor instead. func (*StopProvider) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{3} + return file_tfplugin6_proto_rawDescGZIP(), []int{4} } // RawState holds the stored state for a resource to be upgraded by the // provider. It can be in one of two formats, the current json encoded format // in bytes, or the legacy flatmap format as a map of strings. type RawState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Json []byte `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"` + Flatmap map[string]string `protobuf:"bytes,2,rep,name=flatmap,proto3" json:"flatmap,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` unknownFields protoimpl.UnknownFields - - Json []byte `protobuf:"bytes,1,opt,name=json,proto3" json:"json,omitempty"` - Flatmap map[string]string `protobuf:"bytes,2,rep,name=flatmap,proto3" json:"flatmap,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + sizeCache protoimpl.SizeCache } func (x *RawState) Reset() { *x = RawState{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[4] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *RawState) String() string { @@ -492,8 +595,8 @@ func (x *RawState) String() string { func (*RawState) ProtoMessage() {} func (x *RawState) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[4] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[5] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -505,7 +608,7 @@ func (x *RawState) ProtoReflect() protoreflect.Message { // Deprecated: Use RawState.ProtoReflect.Descriptor instead. func (*RawState) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{4} + return file_tfplugin6_proto_rawDescGZIP(), []int{5} } func (x *RawState) GetJson() []byte { @@ -522,27 +625,145 @@ func (x *RawState) GetFlatmap() map[string]string { return nil } +// ResourceIdentitySchema represents the structure and types of data used to identify +// a managed resource type. Effectively, resource identity is a versioned object +// that can be used to compare resources, whether already managed and/or being +// discovered. +type ResourceIdentitySchema struct { + state protoimpl.MessageState `protogen:"open.v1"` + // version is the identity version and separate from the Schema version. + // Any time the structure or format of identity_attributes changes, this version + // should be incremented. Versioning implicitly starts at 0 and by convention + // should be incremented by 1 each change. + // + // When comparing identity_attributes data, differing versions should always be treated + // as inequal. + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + // identity_attributes are the individual value definitions which define identity data + // for a managed resource type. This information is used to decode DynamicValue of + // identity data. + // + // These attributes are intended for permanent identity data and must be wholly + // representative of all data necessary to compare two managed resource instances + // with no other data. This generally should include account, endpoint, location, + // and automatically generated identifiers. For some resources, this may include + // configuration-based data, such as a required name which must be unique. + IdentityAttributes []*ResourceIdentitySchema_IdentityAttribute `protobuf:"bytes,2,rep,name=identity_attributes,json=identityAttributes,proto3" json:"identity_attributes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceIdentitySchema) Reset() { + *x = ResourceIdentitySchema{} + mi := &file_tfplugin6_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceIdentitySchema) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceIdentitySchema) ProtoMessage() {} + +func (x *ResourceIdentitySchema) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceIdentitySchema.ProtoReflect.Descriptor instead. +func (*ResourceIdentitySchema) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{6} +} + +func (x *ResourceIdentitySchema) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ResourceIdentitySchema) GetIdentityAttributes() []*ResourceIdentitySchema_IdentityAttribute { + if x != nil { + return x.IdentityAttributes + } + return nil +} + +type ResourceIdentityData struct { + state protoimpl.MessageState `protogen:"open.v1"` + // identity_data is the resource identity data for the given definition. It should + // be decoded using the identity schema. + // + // This data is considered permanent for the identity version and suitable for + // longer-term storage. + IdentityData *DynamicValue `protobuf:"bytes,1,opt,name=identity_data,json=identityData,proto3" json:"identity_data,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ResourceIdentityData) Reset() { + *x = ResourceIdentityData{} + mi := &file_tfplugin6_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceIdentityData) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceIdentityData) ProtoMessage() {} + +func (x *ResourceIdentityData) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceIdentityData.ProtoReflect.Descriptor instead. +func (*ResourceIdentityData) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{7} +} + +func (x *ResourceIdentityData) GetIdentityData() *DynamicValue { + if x != nil { + return x.IdentityData + } + return nil +} + // Schema is the configuration schema for a Resource or Provider. type Schema struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // The version of the schema. // Schemas are versioned, so that providers can upgrade a saved resource // state when the schema is changed. Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` // Block is the top level configuration block for this schema. - Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema) Reset() { *x = Schema{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[5] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema) String() string { @@ -552,8 +773,8 @@ func (x *Schema) String() string { func (*Schema) ProtoMessage() {} func (x *Schema) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[5] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[8] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -565,7 +786,7 @@ func (x *Schema) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema.ProtoReflect.Descriptor instead. func (*Schema) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5} + return file_tfplugin6_proto_rawDescGZIP(), []int{8} } func (x *Schema) GetVersion() int64 { @@ -582,19 +803,333 @@ func (x *Schema) GetBlock() *Schema_Block { return nil } -type GetProviderSchema struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type Function struct { + state protoimpl.MessageState `protogen:"open.v1"` + // parameters is the ordered list of positional function parameters. + Parameters []*Function_Parameter `protobuf:"bytes,1,rep,name=parameters,proto3" json:"parameters,omitempty"` + // variadic_parameter is an optional final parameter which accepts + // zero or more argument values, in which Terraform will send an + // ordered list of the parameter type. + VariadicParameter *Function_Parameter `protobuf:"bytes,2,opt,name=variadic_parameter,json=variadicParameter,proto3" json:"variadic_parameter,omitempty"` + // Return is the function return parameter. + Return *Function_Return `protobuf:"bytes,3,opt,name=return,proto3" json:"return,omitempty"` + // summary is the human-readable shortened documentation for the function. + Summary string `protobuf:"bytes,4,opt,name=summary,proto3" json:"summary,omitempty"` + // description is human-readable documentation for the function. + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // description_kind is the formatting of the description. + DescriptionKind StringKind `protobuf:"varint,6,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin6.StringKind" json:"description_kind,omitempty"` + // deprecation_message is human-readable documentation if the + // function is deprecated. + DeprecationMessage string `protobuf:"bytes,7,opt,name=deprecation_message,json=deprecationMessage,proto3" json:"deprecation_message,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Function) Reset() { + *x = Function{} + mi := &file_tfplugin6_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Function) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Function) ProtoMessage() {} + +func (x *Function) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Function.ProtoReflect.Descriptor instead. +func (*Function) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{9} +} + +func (x *Function) GetParameters() []*Function_Parameter { + if x != nil { + return x.Parameters + } + return nil +} + +func (x *Function) GetVariadicParameter() *Function_Parameter { + if x != nil { + return x.VariadicParameter + } + return nil +} + +func (x *Function) GetReturn() *Function_Return { + if x != nil { + return x.Return + } + return nil +} + +func (x *Function) GetSummary() string { + if x != nil { + return x.Summary + } + return "" +} + +func (x *Function) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Function) GetDescriptionKind() StringKind { + if x != nil { + return x.DescriptionKind + } + return StringKind_PLAIN +} + +func (x *Function) GetDeprecationMessage() string { + if x != nil { + return x.DeprecationMessage + } + return "" +} + +// ServerCapabilities allows providers to communicate extra information +// regarding supported protocol features. This is used to indicate +// availability of certain forward-compatible changes which may be optional +// in a major protocol version, but cannot be tested for directly. +type ServerCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The plan_destroy capability signals that a provider expects a call + // to PlanResourceChange when a resource is going to be destroyed. + PlanDestroy bool `protobuf:"varint,1,opt,name=plan_destroy,json=planDestroy,proto3" json:"plan_destroy,omitempty"` + // The get_provider_schema_optional capability indicates that this + // provider does not require calling GetProviderSchema to operate + // normally, and the caller can used a cached copy of the provider's + // schema. + GetProviderSchemaOptional bool `protobuf:"varint,2,opt,name=get_provider_schema_optional,json=getProviderSchemaOptional,proto3" json:"get_provider_schema_optional,omitempty"` + // The move_resource_state capability signals that a provider supports the + // MoveResourceState RPC. + MoveResourceState bool `protobuf:"varint,3,opt,name=move_resource_state,json=moveResourceState,proto3" json:"move_resource_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ServerCapabilities) Reset() { + *x = ServerCapabilities{} + mi := &file_tfplugin6_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ServerCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerCapabilities) ProtoMessage() {} + +func (x *ServerCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ServerCapabilities.ProtoReflect.Descriptor instead. +func (*ServerCapabilities) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{10} +} + +func (x *ServerCapabilities) GetPlanDestroy() bool { + if x != nil { + return x.PlanDestroy + } + return false +} + +func (x *ServerCapabilities) GetGetProviderSchemaOptional() bool { + if x != nil { + return x.GetProviderSchemaOptional + } + return false +} + +func (x *ServerCapabilities) GetMoveResourceState() bool { + if x != nil { + return x.MoveResourceState + } + return false +} + +// ClientCapabilities allows Terraform to publish information regarding +// supported protocol features. This is used to indicate availability of +// certain forward-compatible changes which may be optional in a major +// protocol version, but cannot be tested for directly. +type ClientCapabilities struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The deferral_allowed capability signals that the client is able to + // handle deferred responses from the provider. + DeferralAllowed bool `protobuf:"varint,1,opt,name=deferral_allowed,json=deferralAllowed,proto3" json:"deferral_allowed,omitempty"` + // The write_only_attributes_allowed capability signals that the client + // is able to handle write_only attributes for managed resources. + WriteOnlyAttributesAllowed bool `protobuf:"varint,2,opt,name=write_only_attributes_allowed,json=writeOnlyAttributesAllowed,proto3" json:"write_only_attributes_allowed,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientCapabilities) Reset() { + *x = ClientCapabilities{} + mi := &file_tfplugin6_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientCapabilities) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientCapabilities) ProtoMessage() {} + +func (x *ClientCapabilities) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientCapabilities.ProtoReflect.Descriptor instead. +func (*ClientCapabilities) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{11} +} + +func (x *ClientCapabilities) GetDeferralAllowed() bool { + if x != nil { + return x.DeferralAllowed + } + return false +} + +func (x *ClientCapabilities) GetWriteOnlyAttributesAllowed() bool { + if x != nil { + return x.WriteOnlyAttributesAllowed + } + return false +} + +// Deferred is a message that indicates that change is deferred for a reason. +type Deferred struct { + state protoimpl.MessageState `protogen:"open.v1"` + // reason is the reason for deferring the change. + Reason Deferred_Reason `protobuf:"varint,1,opt,name=reason,proto3,enum=tfplugin6.Deferred_Reason" json:"reason,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Deferred) Reset() { + *x = Deferred{} + mi := &file_tfplugin6_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Deferred) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Deferred) ProtoMessage() {} + +func (x *Deferred) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Deferred.ProtoReflect.Descriptor instead. +func (*Deferred) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{12} +} + +func (x *Deferred) GetReason() Deferred_Reason { + if x != nil { + return x.Reason + } + return Deferred_UNKNOWN +} + +type GetMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata) Reset() { + *x = GetMetadata{} + mi := &file_tfplugin6_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata) ProtoMessage() {} + +func (x *GetMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13} +} + +type GetProviderSchema struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderSchema) Reset() { *x = GetProviderSchema{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[6] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProviderSchema) String() string { @@ -604,8 +1139,8 @@ func (x *GetProviderSchema) String() string { func (*GetProviderSchema) ProtoMessage() {} func (x *GetProviderSchema) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[6] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[14] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -617,22 +1152,20 @@ func (x *GetProviderSchema) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderSchema.ProtoReflect.Descriptor instead. func (*GetProviderSchema) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{6} + return file_tfplugin6_proto_rawDescGZIP(), []int{14} } type ValidateProviderConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateProviderConfig) Reset() { *x = ValidateProviderConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateProviderConfig) String() string { @@ -642,8 +1175,8 @@ func (x *ValidateProviderConfig) String() string { func (*ValidateProviderConfig) ProtoMessage() {} func (x *ValidateProviderConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[15] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -655,22 +1188,20 @@ func (x *ValidateProviderConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateProviderConfig.ProtoReflect.Descriptor instead. func (*ValidateProviderConfig) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{7} + return file_tfplugin6_proto_rawDescGZIP(), []int{15} } type UpgradeResourceState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpgradeResourceState) Reset() { *x = UpgradeResourceState{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[8] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpgradeResourceState) String() string { @@ -680,8 +1211,8 @@ func (x *UpgradeResourceState) String() string { func (*UpgradeResourceState) ProtoMessage() {} func (x *UpgradeResourceState) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[8] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[16] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -693,22 +1224,92 @@ func (x *UpgradeResourceState) ProtoReflect() protoreflect.Message { // Deprecated: Use UpgradeResourceState.ProtoReflect.Descriptor instead. func (*UpgradeResourceState) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{8} + return file_tfplugin6_proto_rawDescGZIP(), []int{16} +} + +type GetResourceIdentitySchemas struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResourceIdentitySchemas) Reset() { + *x = GetResourceIdentitySchemas{} + mi := &file_tfplugin6_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceIdentitySchemas) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceIdentitySchemas) ProtoMessage() {} + +func (x *GetResourceIdentitySchemas) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceIdentitySchemas.ProtoReflect.Descriptor instead. +func (*GetResourceIdentitySchemas) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{17} +} + +type UpgradeResourceIdentity struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpgradeResourceIdentity) Reset() { + *x = UpgradeResourceIdentity{} + mi := &file_tfplugin6_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpgradeResourceIdentity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpgradeResourceIdentity) ProtoMessage() {} + +func (x *UpgradeResourceIdentity) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpgradeResourceIdentity.ProtoReflect.Descriptor instead. +func (*UpgradeResourceIdentity) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{18} } type ValidateResourceConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateResourceConfig) Reset() { *x = ValidateResourceConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[9] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateResourceConfig) String() string { @@ -718,8 +1319,8 @@ func (x *ValidateResourceConfig) String() string { func (*ValidateResourceConfig) ProtoMessage() {} func (x *ValidateResourceConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[9] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[19] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -731,22 +1332,20 @@ func (x *ValidateResourceConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResourceConfig.ProtoReflect.Descriptor instead. func (*ValidateResourceConfig) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{9} + return file_tfplugin6_proto_rawDescGZIP(), []int{19} } type ValidateDataResourceConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateDataResourceConfig) Reset() { *x = ValidateDataResourceConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[10] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateDataResourceConfig) String() string { @@ -756,8 +1355,8 @@ func (x *ValidateDataResourceConfig) String() string { func (*ValidateDataResourceConfig) ProtoMessage() {} func (x *ValidateDataResourceConfig) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[10] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[20] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -769,22 +1368,56 @@ func (x *ValidateDataResourceConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateDataResourceConfig.ProtoReflect.Descriptor instead. func (*ValidateDataResourceConfig) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{10} + return file_tfplugin6_proto_rawDescGZIP(), []int{20} +} + +type ValidateEphemeralResourceConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEphemeralResourceConfig) Reset() { + *x = ValidateEphemeralResourceConfig{} + mi := &file_tfplugin6_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEphemeralResourceConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEphemeralResourceConfig) ProtoMessage() {} + +func (x *ValidateEphemeralResourceConfig) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEphemeralResourceConfig.ProtoReflect.Descriptor instead. +func (*ValidateEphemeralResourceConfig) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{21} } type ConfigureProvider struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConfigureProvider) Reset() { *x = ConfigureProvider{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[11] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ConfigureProvider) String() string { @@ -794,8 +1427,8 @@ func (x *ConfigureProvider) String() string { func (*ConfigureProvider) ProtoMessage() {} func (x *ConfigureProvider) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[11] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[22] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -807,22 +1440,20 @@ func (x *ConfigureProvider) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProvider.ProtoReflect.Descriptor instead. func (*ConfigureProvider) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{11} + return file_tfplugin6_proto_rawDescGZIP(), []int{22} } type ReadResource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadResource) Reset() { *x = ReadResource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[12] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadResource) String() string { @@ -832,8 +1463,8 @@ func (x *ReadResource) String() string { func (*ReadResource) ProtoMessage() {} func (x *ReadResource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[12] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[23] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -845,22 +1476,20 @@ func (x *ReadResource) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResource.ProtoReflect.Descriptor instead. func (*ReadResource) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{12} + return file_tfplugin6_proto_rawDescGZIP(), []int{23} } type PlanResourceChange struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PlanResourceChange) Reset() { *x = PlanResourceChange{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[13] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PlanResourceChange) String() string { @@ -870,8 +1499,8 @@ func (x *PlanResourceChange) String() string { func (*PlanResourceChange) ProtoMessage() {} func (x *PlanResourceChange) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[13] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[24] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -883,22 +1512,20 @@ func (x *PlanResourceChange) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanResourceChange.ProtoReflect.Descriptor instead. func (*PlanResourceChange) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{13} + return file_tfplugin6_proto_rawDescGZIP(), []int{24} } type ApplyResourceChange struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ApplyResourceChange) Reset() { *x = ApplyResourceChange{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[14] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ApplyResourceChange) String() string { @@ -908,8 +1535,8 @@ func (x *ApplyResourceChange) String() string { func (*ApplyResourceChange) ProtoMessage() {} func (x *ApplyResourceChange) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[14] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[25] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -921,22 +1548,20 @@ func (x *ApplyResourceChange) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyResourceChange.ProtoReflect.Descriptor instead. func (*ApplyResourceChange) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{14} + return file_tfplugin6_proto_rawDescGZIP(), []int{25} } type ImportResourceState struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState) Reset() { *x = ImportResourceState{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[15] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState) String() string { @@ -946,8 +1571,8 @@ func (x *ImportResourceState) String() string { func (*ImportResourceState) ProtoMessage() {} func (x *ImportResourceState) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[15] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[26] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -959,22 +1584,56 @@ func (x *ImportResourceState) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportResourceState.ProtoReflect.Descriptor instead. func (*ImportResourceState) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{15} + return file_tfplugin6_proto_rawDescGZIP(), []int{26} +} + +type MoveResourceState struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MoveResourceState) Reset() { + *x = MoveResourceState{} + mi := &file_tfplugin6_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MoveResourceState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoveResourceState) ProtoMessage() {} + +func (x *MoveResourceState) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoveResourceState.ProtoReflect.Descriptor instead. +func (*MoveResourceState) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{27} } type ReadDataSource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadDataSource) Reset() { *x = ReadDataSource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[16] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadDataSource) String() string { @@ -984,8 +1643,8 @@ func (x *ReadDataSource) String() string { func (*ReadDataSource) ProtoMessage() {} func (x *ReadDataSource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[16] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[28] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -997,29 +1656,206 @@ func (x *ReadDataSource) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadDataSource.ProtoReflect.Descriptor instead. func (*ReadDataSource) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{16} + return file_tfplugin6_proto_rawDescGZIP(), []int{28} +} + +type OpenEphemeralResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenEphemeralResource) Reset() { + *x = OpenEphemeralResource{} + mi := &file_tfplugin6_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenEphemeralResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenEphemeralResource) ProtoMessage() {} + +func (x *OpenEphemeralResource) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenEphemeralResource.ProtoReflect.Descriptor instead. +func (*OpenEphemeralResource) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{29} +} + +type RenewEphemeralResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewEphemeralResource) Reset() { + *x = RenewEphemeralResource{} + mi := &file_tfplugin6_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewEphemeralResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewEphemeralResource) ProtoMessage() {} + +func (x *RenewEphemeralResource) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewEphemeralResource.ProtoReflect.Descriptor instead. +func (*RenewEphemeralResource) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{30} +} + +type CloseEphemeralResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseEphemeralResource) Reset() { + *x = CloseEphemeralResource{} + mi := &file_tfplugin6_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseEphemeralResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseEphemeralResource) ProtoMessage() {} + +func (x *CloseEphemeralResource) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseEphemeralResource.ProtoReflect.Descriptor instead. +func (*CloseEphemeralResource) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{31} +} + +type GetFunctions struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFunctions) Reset() { + *x = GetFunctions{} + mi := &file_tfplugin6_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFunctions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFunctions) ProtoMessage() {} + +func (x *GetFunctions) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFunctions.ProtoReflect.Descriptor instead. +func (*GetFunctions) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{32} +} + +type CallFunction struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallFunction) Reset() { + *x = CallFunction{} + mi := &file_tfplugin6_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallFunction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallFunction) ProtoMessage() {} + +func (x *CallFunction) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallFunction.ProtoReflect.Descriptor instead. +func (*CallFunction) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{33} } type AttributePath_Step struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // Types that are assignable to Selector: + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Selector: // // *AttributePath_Step_AttributeName // *AttributePath_Step_ElementKeyString // *AttributePath_Step_ElementKeyInt - Selector isAttributePath_Step_Selector `protobuf_oneof:"selector"` + Selector isAttributePath_Step_Selector `protobuf_oneof:"selector"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *AttributePath_Step) Reset() { *x = AttributePath_Step{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[17] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *AttributePath_Step) String() string { @@ -1029,8 +1865,8 @@ func (x *AttributePath_Step) String() string { func (*AttributePath_Step) ProtoMessage() {} func (x *AttributePath_Step) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[17] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[34] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1042,33 +1878,39 @@ func (x *AttributePath_Step) ProtoReflect() protoreflect.Message { // Deprecated: Use AttributePath_Step.ProtoReflect.Descriptor instead. func (*AttributePath_Step) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{2, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{3, 0} } -func (m *AttributePath_Step) GetSelector() isAttributePath_Step_Selector { - if m != nil { - return m.Selector +func (x *AttributePath_Step) GetSelector() isAttributePath_Step_Selector { + if x != nil { + return x.Selector } return nil } func (x *AttributePath_Step) GetAttributeName() string { - if x, ok := x.GetSelector().(*AttributePath_Step_AttributeName); ok { - return x.AttributeName + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_AttributeName); ok { + return x.AttributeName + } } return "" } func (x *AttributePath_Step) GetElementKeyString() string { - if x, ok := x.GetSelector().(*AttributePath_Step_ElementKeyString); ok { - return x.ElementKeyString + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_ElementKeyString); ok { + return x.ElementKeyString + } } return "" } func (x *AttributePath_Step) GetElementKeyInt() int64 { - if x, ok := x.GetSelector().(*AttributePath_Step_ElementKeyInt); ok { - return x.ElementKeyInt + if x != nil { + if x, ok := x.Selector.(*AttributePath_Step_ElementKeyInt); ok { + return x.ElementKeyInt + } } return 0 } @@ -1100,18 +1942,16 @@ func (*AttributePath_Step_ElementKeyString) isAttributePath_Step_Selector() {} func (*AttributePath_Step_ElementKeyInt) isAttributePath_Step_Selector() {} type StopProvider_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *StopProvider_Request) Reset() { *x = StopProvider_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[18] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StopProvider_Request) String() string { @@ -1121,8 +1961,8 @@ func (x *StopProvider_Request) String() string { func (*StopProvider_Request) ProtoMessage() {} func (x *StopProvider_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[18] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[35] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1134,24 +1974,21 @@ func (x *StopProvider_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use StopProvider_Request.ProtoReflect.Descriptor instead. func (*StopProvider_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{3, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{4, 0} } type StopProvider_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Error string `protobuf:"bytes,1,opt,name=Error,proto3" json:"Error,omitempty"` unknownFields protoimpl.UnknownFields - - Error string `protobuf:"bytes,1,opt,name=Error,proto3" json:"Error,omitempty"` + sizeCache protoimpl.SizeCache } func (x *StopProvider_Response) Reset() { *x = StopProvider_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[19] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *StopProvider_Response) String() string { @@ -1161,8 +1998,8 @@ func (x *StopProvider_Response) String() string { func (*StopProvider_Response) ProtoMessage() {} func (x *StopProvider_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[19] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[36] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1174,7 +2011,7 @@ func (x *StopProvider_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use StopProvider_Response.ProtoReflect.Descriptor instead. func (*StopProvider_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{3, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{4, 1} } func (x *StopProvider_Response) GetError() string { @@ -1184,26 +2021,109 @@ func (x *StopProvider_Response) GetError() string { return "" } -type Schema_Block struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +// IdentityAttribute represents one value of data within resource identity. These +// are always used in resource identity comparisons. +type ResourceIdentitySchema_IdentityAttribute struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the identity attribute name + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // type is the identity attribute type + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // required_for_import when enabled signifies that this attribute must be + // defined for ImportResourceState to complete successfully + RequiredForImport bool `protobuf:"varint,3,opt,name=required_for_import,json=requiredForImport,proto3" json:"required_for_import,omitempty"` + // optional_for_import when enabled signifies that this attribute is not + // required for ImportResourceState, because it can be supplied by the + // provider. It is still possible to supply this attribute during import. + OptionalForImport bool `protobuf:"varint,4,opt,name=optional_for_import,json=optionalForImport,proto3" json:"optional_for_import,omitempty"` + // description is a human-readable description of the attribute in Markdown + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` - Attributes []*Schema_Attribute `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes,omitempty"` - BlockTypes []*Schema_NestedBlock `protobuf:"bytes,3,rep,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"` - Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` - DescriptionKind StringKind `protobuf:"varint,5,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin6.StringKind" json:"description_kind,omitempty"` - Deprecated bool `protobuf:"varint,6,opt,name=deprecated,proto3" json:"deprecated,omitempty"` +func (x *ResourceIdentitySchema_IdentityAttribute) Reset() { + *x = ResourceIdentitySchema_IdentityAttribute{} + mi := &file_tfplugin6_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ResourceIdentitySchema_IdentityAttribute) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ResourceIdentitySchema_IdentityAttribute) ProtoMessage() {} + +func (x *ResourceIdentitySchema_IdentityAttribute) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ResourceIdentitySchema_IdentityAttribute.ProtoReflect.Descriptor instead. +func (*ResourceIdentitySchema_IdentityAttribute) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{6, 0} +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetRequiredForImport() bool { + if x != nil { + return x.RequiredForImport + } + return false +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetOptionalForImport() bool { + if x != nil { + return x.OptionalForImport + } + return false +} + +func (x *ResourceIdentitySchema_IdentityAttribute) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type Schema_Block struct { + state protoimpl.MessageState `protogen:"open.v1"` + Version int64 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Attributes []*Schema_Attribute `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes,omitempty"` + BlockTypes []*Schema_NestedBlock `protobuf:"bytes,3,rep,name=block_types,json=blockTypes,proto3" json:"block_types,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + DescriptionKind StringKind `protobuf:"varint,5,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin6.StringKind" json:"description_kind,omitempty"` + Deprecated bool `protobuf:"varint,6,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_Block) Reset() { *x = Schema_Block{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[21] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_Block) String() string { @@ -1213,8 +2133,8 @@ func (x *Schema_Block) String() string { func (*Schema_Block) ProtoMessage() {} func (x *Schema_Block) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[21] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[39] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1226,7 +2146,7 @@ func (x *Schema_Block) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_Block.ProtoReflect.Descriptor instead. func (*Schema_Block) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{8, 0} } func (x *Schema_Block) GetVersion() int64 { @@ -1272,29 +2192,27 @@ func (x *Schema_Block) GetDeprecated() bool { } type Schema_Attribute struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - NestedType *Schema_Object `protobuf:"bytes,10,opt,name=nested_type,json=nestedType,proto3" json:"nested_type,omitempty"` - Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` - Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` - Optional bool `protobuf:"varint,5,opt,name=optional,proto3" json:"optional,omitempty"` - Computed bool `protobuf:"varint,6,opt,name=computed,proto3" json:"computed,omitempty"` - Sensitive bool `protobuf:"varint,7,opt,name=sensitive,proto3" json:"sensitive,omitempty"` - DescriptionKind StringKind `protobuf:"varint,8,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin6.StringKind" json:"description_kind,omitempty"` - Deprecated bool `protobuf:"varint,9,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + NestedType *Schema_Object `protobuf:"bytes,10,opt,name=nested_type,json=nestedType,proto3" json:"nested_type,omitempty"` + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + Required bool `protobuf:"varint,4,opt,name=required,proto3" json:"required,omitempty"` + Optional bool `protobuf:"varint,5,opt,name=optional,proto3" json:"optional,omitempty"` + Computed bool `protobuf:"varint,6,opt,name=computed,proto3" json:"computed,omitempty"` + Sensitive bool `protobuf:"varint,7,opt,name=sensitive,proto3" json:"sensitive,omitempty"` + DescriptionKind StringKind `protobuf:"varint,8,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin6.StringKind" json:"description_kind,omitempty"` + Deprecated bool `protobuf:"varint,9,opt,name=deprecated,proto3" json:"deprecated,omitempty"` + WriteOnly bool `protobuf:"varint,11,opt,name=write_only,json=writeOnly,proto3" json:"write_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_Attribute) Reset() { *x = Schema_Attribute{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[22] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_Attribute) String() string { @@ -1304,8 +2222,8 @@ func (x *Schema_Attribute) String() string { func (*Schema_Attribute) ProtoMessage() {} func (x *Schema_Attribute) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[22] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[40] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1317,7 +2235,7 @@ func (x *Schema_Attribute) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_Attribute.ProtoReflect.Descriptor instead. func (*Schema_Attribute) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{8, 1} } func (x *Schema_Attribute) GetName() string { @@ -1390,25 +2308,29 @@ func (x *Schema_Attribute) GetDeprecated() bool { return false } -type Schema_NestedBlock struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *Schema_Attribute) GetWriteOnly() bool { + if x != nil { + return x.WriteOnly + } + return false +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` - Nesting Schema_NestedBlock_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=tfplugin6.Schema_NestedBlock_NestingMode" json:"nesting,omitempty"` - MinItems int64 `protobuf:"varint,4,opt,name=min_items,json=minItems,proto3" json:"min_items,omitempty"` - MaxItems int64 `protobuf:"varint,5,opt,name=max_items,json=maxItems,proto3" json:"max_items,omitempty"` +type Schema_NestedBlock struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Block *Schema_Block `protobuf:"bytes,2,opt,name=block,proto3" json:"block,omitempty"` + Nesting Schema_NestedBlock_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=tfplugin6.Schema_NestedBlock_NestingMode" json:"nesting,omitempty"` + MinItems int64 `protobuf:"varint,4,opt,name=min_items,json=minItems,proto3" json:"min_items,omitempty"` + MaxItems int64 `protobuf:"varint,5,opt,name=max_items,json=maxItems,proto3" json:"max_items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_NestedBlock) Reset() { *x = Schema_NestedBlock{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[23] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[41] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_NestedBlock) String() string { @@ -1418,8 +2340,8 @@ func (x *Schema_NestedBlock) String() string { func (*Schema_NestedBlock) ProtoMessage() {} func (x *Schema_NestedBlock) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[23] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[41] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1431,7 +2353,7 @@ func (x *Schema_NestedBlock) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_NestedBlock.ProtoReflect.Descriptor instead. func (*Schema_NestedBlock) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5, 2} + return file_tfplugin6_proto_rawDescGZIP(), []int{8, 2} } func (x *Schema_NestedBlock) GetTypeName() string { @@ -1470,28 +2392,25 @@ func (x *Schema_NestedBlock) GetMaxItems() int64 { } type Schema_Object struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` Attributes []*Schema_Attribute `protobuf:"bytes,1,rep,name=attributes,proto3" json:"attributes,omitempty"` Nesting Schema_Object_NestingMode `protobuf:"varint,3,opt,name=nesting,proto3,enum=tfplugin6.Schema_Object_NestingMode" json:"nesting,omitempty"` // MinItems and MaxItems were never used in the protocol, and have no // effect on validation. // - // Deprecated: Do not use. + // Deprecated: Marked as deprecated in tfplugin6.proto. MinItems int64 `protobuf:"varint,4,opt,name=min_items,json=minItems,proto3" json:"min_items,omitempty"` - // Deprecated: Do not use. - MaxItems int64 `protobuf:"varint,5,opt,name=max_items,json=maxItems,proto3" json:"max_items,omitempty"` + // Deprecated: Marked as deprecated in tfplugin6.proto. + MaxItems int64 `protobuf:"varint,5,opt,name=max_items,json=maxItems,proto3" json:"max_items,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *Schema_Object) Reset() { *x = Schema_Object{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[24] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[42] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *Schema_Object) String() string { @@ -1501,8 +2420,8 @@ func (x *Schema_Object) String() string { func (*Schema_Object) ProtoMessage() {} func (x *Schema_Object) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[24] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[42] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1514,7 +2433,7 @@ func (x *Schema_Object) ProtoReflect() protoreflect.Message { // Deprecated: Use Schema_Object.ProtoReflect.Descriptor instead. func (*Schema_Object) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{5, 3} + return file_tfplugin6_proto_rawDescGZIP(), []int{8, 3} } func (x *Schema_Object) GetAttributes() []*Schema_Attribute { @@ -1531,7 +2450,7 @@ func (x *Schema_Object) GetNesting() Schema_Object_NestingMode { return Schema_Object_INVALID } -// Deprecated: Do not use. +// Deprecated: Marked as deprecated in tfplugin6.proto. func (x *Schema_Object) GetMinItems() int64 { if x != nil { return x.MinItems @@ -1539,7 +2458,7 @@ func (x *Schema_Object) GetMinItems() int64 { return 0 } -// Deprecated: Do not use. +// Deprecated: Marked as deprecated in tfplugin6.proto. func (x *Schema_Object) GetMaxItems() int64 { if x != nil { return x.MaxItems @@ -1547,19 +2466,455 @@ func (x *Schema_Object) GetMaxItems() int64 { return 0 } -type GetProviderSchema_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type Function_Parameter struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the human-readable display name for the parameter. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // type is the type constraint for the parameter. + Type []byte `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + // allow_null_value when enabled denotes that a null argument value can + // be passed to the provider. When disabled, Terraform returns an error + // if the argument value is null. + AllowNullValue bool `protobuf:"varint,3,opt,name=allow_null_value,json=allowNullValue,proto3" json:"allow_null_value,omitempty"` + // allow_unknown_values when enabled denotes that only wholly known + // argument values will be passed to the provider. When disabled, + // Terraform skips the function call entirely and assumes an unknown + // value result from the function. + AllowUnknownValues bool `protobuf:"varint,4,opt,name=allow_unknown_values,json=allowUnknownValues,proto3" json:"allow_unknown_values,omitempty"` + // description is human-readable documentation for the parameter. + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + // description_kind is the formatting of the description. + DescriptionKind StringKind `protobuf:"varint,6,opt,name=description_kind,json=descriptionKind,proto3,enum=tfplugin6.StringKind" json:"description_kind,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Function_Parameter) Reset() { + *x = Function_Parameter{} + mi := &file_tfplugin6_proto_msgTypes[43] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Function_Parameter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Function_Parameter) ProtoMessage() {} + +func (x *Function_Parameter) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[43] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Function_Parameter.ProtoReflect.Descriptor instead. +func (*Function_Parameter) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{9, 0} +} + +func (x *Function_Parameter) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Function_Parameter) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +func (x *Function_Parameter) GetAllowNullValue() bool { + if x != nil { + return x.AllowNullValue + } + return false +} + +func (x *Function_Parameter) GetAllowUnknownValues() bool { + if x != nil { + return x.AllowUnknownValues + } + return false +} + +func (x *Function_Parameter) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Function_Parameter) GetDescriptionKind() StringKind { + if x != nil { + return x.DescriptionKind + } + return StringKind_PLAIN +} + +type Function_Return struct { + state protoimpl.MessageState `protogen:"open.v1"` + // type is the type constraint for the function result. + Type []byte `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Function_Return) Reset() { + *x = Function_Return{} + mi := &file_tfplugin6_proto_msgTypes[44] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Function_Return) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Function_Return) ProtoMessage() {} + +func (x *Function_Return) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[44] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Function_Return.ProtoReflect.Descriptor instead. +func (*Function_Return) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{9, 1} +} + +func (x *Function_Return) GetType() []byte { + if x != nil { + return x.Type + } + return nil +} + +type GetMetadata_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_Request) Reset() { + *x = GetMetadata_Request{} + mi := &file_tfplugin6_proto_msgTypes[45] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_Request) ProtoMessage() {} + +func (x *GetMetadata_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[45] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_Request.ProtoReflect.Descriptor instead. +func (*GetMetadata_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13, 0} +} + +type GetMetadata_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + ServerCapabilities *ServerCapabilities `protobuf:"bytes,1,opt,name=server_capabilities,json=serverCapabilities,proto3" json:"server_capabilities,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + DataSources []*GetMetadata_DataSourceMetadata `protobuf:"bytes,3,rep,name=data_sources,json=dataSources,proto3" json:"data_sources,omitempty"` + Resources []*GetMetadata_ResourceMetadata `protobuf:"bytes,4,rep,name=resources,proto3" json:"resources,omitempty"` + // functions returns metadata for any functions. + Functions []*GetMetadata_FunctionMetadata `protobuf:"bytes,5,rep,name=functions,proto3" json:"functions,omitempty"` + EphemeralResources []*GetMetadata_EphemeralMetadata `protobuf:"bytes,6,rep,name=ephemeral_resources,json=ephemeralResources,proto3" json:"ephemeral_resources,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_Response) Reset() { + *x = GetMetadata_Response{} + mi := &file_tfplugin6_proto_msgTypes[46] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_Response) ProtoMessage() {} + +func (x *GetMetadata_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[46] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_Response.ProtoReflect.Descriptor instead. +func (*GetMetadata_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13, 1} +} + +func (x *GetMetadata_Response) GetServerCapabilities() *ServerCapabilities { + if x != nil { + return x.ServerCapabilities + } + return nil +} + +func (x *GetMetadata_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *GetMetadata_Response) GetDataSources() []*GetMetadata_DataSourceMetadata { + if x != nil { + return x.DataSources + } + return nil +} + +func (x *GetMetadata_Response) GetResources() []*GetMetadata_ResourceMetadata { + if x != nil { + return x.Resources + } + return nil +} + +func (x *GetMetadata_Response) GetFunctions() []*GetMetadata_FunctionMetadata { + if x != nil { + return x.Functions + } + return nil +} + +func (x *GetMetadata_Response) GetEphemeralResources() []*GetMetadata_EphemeralMetadata { + if x != nil { + return x.EphemeralResources + } + return nil +} + +type GetMetadata_EphemeralMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_EphemeralMetadata) Reset() { + *x = GetMetadata_EphemeralMetadata{} + mi := &file_tfplugin6_proto_msgTypes[47] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_EphemeralMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_EphemeralMetadata) ProtoMessage() {} + +func (x *GetMetadata_EphemeralMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[47] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_EphemeralMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_EphemeralMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13, 2} +} + +func (x *GetMetadata_EphemeralMetadata) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +type GetMetadata_FunctionMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // name is the function name. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_FunctionMetadata) Reset() { + *x = GetMetadata_FunctionMetadata{} + mi := &file_tfplugin6_proto_msgTypes[48] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_FunctionMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_FunctionMetadata) ProtoMessage() {} + +func (x *GetMetadata_FunctionMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[48] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_FunctionMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_FunctionMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13, 3} +} + +func (x *GetMetadata_FunctionMetadata) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetMetadata_DataSourceMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_DataSourceMetadata) Reset() { + *x = GetMetadata_DataSourceMetadata{} + mi := &file_tfplugin6_proto_msgTypes[49] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_DataSourceMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_DataSourceMetadata) ProtoMessage() {} + +func (x *GetMetadata_DataSourceMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[49] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_DataSourceMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_DataSourceMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13, 4} +} + +func (x *GetMetadata_DataSourceMetadata) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +type GetMetadata_ResourceMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMetadata_ResourceMetadata) Reset() { + *x = GetMetadata_ResourceMetadata{} + mi := &file_tfplugin6_proto_msgTypes[50] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMetadata_ResourceMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMetadata_ResourceMetadata) ProtoMessage() {} + +func (x *GetMetadata_ResourceMetadata) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[50] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMetadata_ResourceMetadata.ProtoReflect.Descriptor instead. +func (*GetMetadata_ResourceMetadata) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{13, 5} +} + +func (x *GetMetadata_ResourceMetadata) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +type GetProviderSchema_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderSchema_Request) Reset() { *x = GetProviderSchema_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[25] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[51] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProviderSchema_Request) String() string { @@ -1569,8 +2924,8 @@ func (x *GetProviderSchema_Request) String() string { func (*GetProviderSchema_Request) ProtoMessage() {} func (x *GetProviderSchema_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[25] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[51] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1582,29 +2937,28 @@ func (x *GetProviderSchema_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderSchema_Request.ProtoReflect.Descriptor instead. func (*GetProviderSchema_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{6, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{14, 0} } type GetProviderSchema_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Provider *Schema `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` - ResourceSchemas map[string]*Schema `protobuf:"bytes,2,rep,name=resource_schemas,json=resourceSchemas,proto3" json:"resource_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - DataSourceSchemas map[string]*Schema `protobuf:"bytes,3,rep,name=data_source_schemas,json=dataSourceSchemas,proto3" json:"data_source_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` - ProviderMeta *Schema `protobuf:"bytes,5,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` - ServerCapabilities *GetProviderSchema_ServerCapabilities `protobuf:"bytes,6,opt,name=server_capabilities,json=serverCapabilities,proto3" json:"server_capabilities,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Provider *Schema `protobuf:"bytes,1,opt,name=provider,proto3" json:"provider,omitempty"` + ResourceSchemas map[string]*Schema `protobuf:"bytes,2,rep,name=resource_schemas,json=resourceSchemas,proto3" json:"resource_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + DataSourceSchemas map[string]*Schema `protobuf:"bytes,3,rep,name=data_source_schemas,json=dataSourceSchemas,proto3" json:"data_source_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Functions map[string]*Function `protobuf:"bytes,7,rep,name=functions,proto3" json:"functions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + EphemeralResourceSchemas map[string]*Schema `protobuf:"bytes,8,rep,name=ephemeral_resource_schemas,json=ephemeralResourceSchemas,proto3" json:"ephemeral_resource_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + ProviderMeta *Schema `protobuf:"bytes,5,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ServerCapabilities *ServerCapabilities `protobuf:"bytes,6,opt,name=server_capabilities,json=serverCapabilities,proto3" json:"server_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *GetProviderSchema_Response) Reset() { *x = GetProviderSchema_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[26] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[52] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GetProviderSchema_Response) String() string { @@ -1614,8 +2968,8 @@ func (x *GetProviderSchema_Response) String() string { func (*GetProviderSchema_Response) ProtoMessage() {} func (x *GetProviderSchema_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[26] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[52] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1627,7 +2981,7 @@ func (x *GetProviderSchema_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use GetProviderSchema_Response.ProtoReflect.Descriptor instead. func (*GetProviderSchema_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{6, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{14, 1} } func (x *GetProviderSchema_Response) GetProvider() *Schema { @@ -1651,6 +3005,20 @@ func (x *GetProviderSchema_Response) GetDataSourceSchemas() map[string]*Schema { return nil } +func (x *GetProviderSchema_Response) GetFunctions() map[string]*Function { + if x != nil { + return x.Functions + } + return nil +} + +func (x *GetProviderSchema_Response) GetEphemeralResourceSchemas() map[string]*Schema { + if x != nil { + return x.EphemeralResourceSchemas + } + return nil +} + func (x *GetProviderSchema_Response) GetDiagnostics() []*Diagnostic { if x != nil { return x.Diagnostics @@ -1665,81 +3033,25 @@ func (x *GetProviderSchema_Response) GetProviderMeta() *Schema { return nil } -func (x *GetProviderSchema_Response) GetServerCapabilities() *GetProviderSchema_ServerCapabilities { +func (x *GetProviderSchema_Response) GetServerCapabilities() *ServerCapabilities { if x != nil { return x.ServerCapabilities } return nil } -// ServerCapabilities allows providers to communicate extra information -// regarding supported protocol features. This is used to indicate -// availability of certain forward-compatible changes which may be optional -// in a major protocol version, but cannot be tested for directly. -type GetProviderSchema_ServerCapabilities struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // The plan_destroy capability signals that a provider expects a call - // to PlanResourceChange when a resource is going to be destroyed. - PlanDestroy bool `protobuf:"varint,1,opt,name=plan_destroy,json=planDestroy,proto3" json:"plan_destroy,omitempty"` -} - -func (x *GetProviderSchema_ServerCapabilities) Reset() { - *x = GetProviderSchema_ServerCapabilities{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[27] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *GetProviderSchema_ServerCapabilities) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*GetProviderSchema_ServerCapabilities) ProtoMessage() {} - -func (x *GetProviderSchema_ServerCapabilities) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[27] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use GetProviderSchema_ServerCapabilities.ProtoReflect.Descriptor instead. -func (*GetProviderSchema_ServerCapabilities) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{6, 2} -} - -func (x *GetProviderSchema_ServerCapabilities) GetPlanDestroy() bool { - if x != nil { - return x.PlanDestroy - } - return false -} - type ValidateProviderConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields - - Config *DynamicValue `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateProviderConfig_Request) Reset() { *x = ValidateProviderConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[30] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[57] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateProviderConfig_Request) String() string { @@ -1749,8 +3061,8 @@ func (x *ValidateProviderConfig_Request) String() string { func (*ValidateProviderConfig_Request) ProtoMessage() {} func (x *ValidateProviderConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[30] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[57] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1762,7 +3074,7 @@ func (x *ValidateProviderConfig_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateProviderConfig_Request.ProtoReflect.Descriptor instead. func (*ValidateProviderConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{7, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{15, 0} } func (x *ValidateProviderConfig_Request) GetConfig() *DynamicValue { @@ -1773,20 +3085,17 @@ func (x *ValidateProviderConfig_Request) GetConfig() *DynamicValue { } type ValidateProviderConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` unknownFields protoimpl.UnknownFields - - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateProviderConfig_Response) Reset() { *x = ValidateProviderConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[31] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[58] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateProviderConfig_Response) String() string { @@ -1796,8 +3105,8 @@ func (x *ValidateProviderConfig_Response) String() string { func (*ValidateProviderConfig_Response) ProtoMessage() {} func (x *ValidateProviderConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[31] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[58] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1809,7 +3118,7 @@ func (x *ValidateProviderConfig_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateProviderConfig_Response.ProtoReflect.Descriptor instead. func (*ValidateProviderConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{7, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{15, 1} } func (x *ValidateProviderConfig_Response) GetDiagnostics() []*Diagnostic { @@ -1819,12 +3128,18 @@ func (x *ValidateProviderConfig_Response) GetDiagnostics() []*Diagnostic { return nil } +// Request is the message that is sent to the provider during the +// UpgradeResourceState RPC. +// +// This message intentionally does not include configuration data as any +// configuration-based or configuration-conditional changes should occur +// during the PlanResourceChange RPC. Additionally, the configuration is +// not guaranteed to exist (in the case of resource destruction), be wholly +// known, nor match the given prior state, which could lead to unexpected +// provider behaviors for practitioners. type UpgradeResourceState_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` // version is the schema_version number recorded in the state file Version int64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // raw_state is the raw states as stored for the resource. Core does @@ -1832,16 +3147,16 @@ type UpgradeResourceState_Request struct { // provider's responsibility to interpret this value using the // appropriate older schema. The raw_state will be the json encoded // state, or a legacy flat-mapped format. - RawState *RawState `protobuf:"bytes,3,opt,name=raw_state,json=rawState,proto3" json:"raw_state,omitempty"` + RawState *RawState `protobuf:"bytes,3,opt,name=raw_state,json=rawState,proto3" json:"raw_state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpgradeResourceState_Request) Reset() { *x = UpgradeResourceState_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[32] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[59] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpgradeResourceState_Request) String() string { @@ -1851,8 +3166,8 @@ func (x *UpgradeResourceState_Request) String() string { func (*UpgradeResourceState_Request) ProtoMessage() {} func (x *UpgradeResourceState_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[32] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[59] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1864,7 +3179,7 @@ func (x *UpgradeResourceState_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use UpgradeResourceState_Request.ProtoReflect.Descriptor instead. func (*UpgradeResourceState_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{8, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{16, 0} } func (x *UpgradeResourceState_Request) GetTypeName() string { @@ -1889,10 +3204,7 @@ func (x *UpgradeResourceState_Request) GetRawState() *RawState { } type UpgradeResourceState_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - + state protoimpl.MessageState `protogen:"open.v1"` // new_state is a msgpack-encoded data structure that, when interpreted with // the _current_ schema for this resource type, is functionally equivalent to // that which was given in prior_state_raw. @@ -1900,16 +3212,16 @@ type UpgradeResourceState_Response struct { // diagnostics describes any errors encountered during migration that could not // be safely resolved, and warnings about any possibly-risky assumptions made // in the upgrade process. - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UpgradeResourceState_Response) Reset() { *x = UpgradeResourceState_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[33] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[60] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UpgradeResourceState_Response) String() string { @@ -1919,8 +3231,8 @@ func (x *UpgradeResourceState_Response) String() string { func (*UpgradeResourceState_Response) ProtoMessage() {} func (x *UpgradeResourceState_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[33] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[60] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1932,7 +3244,7 @@ func (x *UpgradeResourceState_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use UpgradeResourceState_Response.ProtoReflect.Descriptor instead. func (*UpgradeResourceState_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{8, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{16, 1} } func (x *UpgradeResourceState_Response) GetUpgradedState() *DynamicValue { @@ -1949,22 +3261,230 @@ func (x *UpgradeResourceState_Response) GetDiagnostics() []*Diagnostic { return nil } -type ValidateResourceConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type GetResourceIdentitySchemas_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` +func (x *GetResourceIdentitySchemas_Request) Reset() { + *x = GetResourceIdentitySchemas_Request{} + mi := &file_tfplugin6_proto_msgTypes[61] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceIdentitySchemas_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceIdentitySchemas_Request) ProtoMessage() {} + +func (x *GetResourceIdentitySchemas_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[61] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceIdentitySchemas_Request.ProtoReflect.Descriptor instead. +func (*GetResourceIdentitySchemas_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{17, 0} +} + +type GetResourceIdentitySchemas_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // identity_schemas is a mapping of resource type names to their identity schemas. + IdentitySchemas map[string]*ResourceIdentitySchema `protobuf:"bytes,1,rep,name=identity_schemas,json=identitySchemas,proto3" json:"identity_schemas,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // diagnostics is the collection of warning and error diagnostics for this request. + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetResourceIdentitySchemas_Response) Reset() { + *x = GetResourceIdentitySchemas_Response{} + mi := &file_tfplugin6_proto_msgTypes[62] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetResourceIdentitySchemas_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetResourceIdentitySchemas_Response) ProtoMessage() {} + +func (x *GetResourceIdentitySchemas_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[62] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetResourceIdentitySchemas_Response.ProtoReflect.Descriptor instead. +func (*GetResourceIdentitySchemas_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{17, 1} +} + +func (x *GetResourceIdentitySchemas_Response) GetIdentitySchemas() map[string]*ResourceIdentitySchema { + if x != nil { + return x.IdentitySchemas + } + return nil +} + +func (x *GetResourceIdentitySchemas_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type UpgradeResourceIdentity_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // type_name is the managed resource type name + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + // version is the version of the resource identity data to upgrade + Version int64 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // raw_identity is the raw identity as stored for the resource. Core does + // not have access to the identity schema of prior_version, so it's the + // provider's responsibility to interpret this value using the + // appropriate older schema. The raw_identity will be json encoded. + RawIdentity *RawState `protobuf:"bytes,3,opt,name=raw_identity,json=rawIdentity,proto3" json:"raw_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpgradeResourceIdentity_Request) Reset() { + *x = UpgradeResourceIdentity_Request{} + mi := &file_tfplugin6_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpgradeResourceIdentity_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpgradeResourceIdentity_Request) ProtoMessage() {} + +func (x *UpgradeResourceIdentity_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpgradeResourceIdentity_Request.ProtoReflect.Descriptor instead. +func (*UpgradeResourceIdentity_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{18, 0} +} + +func (x *UpgradeResourceIdentity_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *UpgradeResourceIdentity_Request) GetVersion() int64 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *UpgradeResourceIdentity_Request) GetRawIdentity() *RawState { + if x != nil { + return x.RawIdentity + } + return nil +} + +type UpgradeResourceIdentity_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // upgraded_identity returns the upgraded resource identity data + UpgradedIdentity *ResourceIdentityData `protobuf:"bytes,1,opt,name=upgraded_identity,json=upgradedIdentity,proto3" json:"upgraded_identity,omitempty"` + // diagnostics is the collection of warning and error diagnostics for this request + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpgradeResourceIdentity_Response) Reset() { + *x = UpgradeResourceIdentity_Response{} + mi := &file_tfplugin6_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpgradeResourceIdentity_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpgradeResourceIdentity_Response) ProtoMessage() {} + +func (x *UpgradeResourceIdentity_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpgradeResourceIdentity_Response.ProtoReflect.Descriptor instead. +func (*UpgradeResourceIdentity_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{18, 1} +} + +func (x *UpgradeResourceIdentity_Response) GetUpgradedIdentity() *ResourceIdentityData { + if x != nil { + return x.UpgradedIdentity + } + return nil +} + +func (x *UpgradeResourceIdentity_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type ValidateResourceConfig_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateResourceConfig_Request) Reset() { *x = ValidateResourceConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[34] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateResourceConfig_Request) String() string { @@ -1974,8 +3494,8 @@ func (x *ValidateResourceConfig_Request) String() string { func (*ValidateResourceConfig_Request) ProtoMessage() {} func (x *ValidateResourceConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[34] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[66] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -1987,7 +3507,7 @@ func (x *ValidateResourceConfig_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResourceConfig_Request.ProtoReflect.Descriptor instead. func (*ValidateResourceConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{9, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{19, 0} } func (x *ValidateResourceConfig_Request) GetTypeName() string { @@ -2004,21 +3524,25 @@ func (x *ValidateResourceConfig_Request) GetConfig() *DynamicValue { return nil } -type ValidateResourceConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ValidateResourceConfig_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ValidateResourceConfig_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ValidateResourceConfig_Response) Reset() { *x = ValidateResourceConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[35] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateResourceConfig_Response) String() string { @@ -2028,8 +3552,8 @@ func (x *ValidateResourceConfig_Response) String() string { func (*ValidateResourceConfig_Response) ProtoMessage() {} func (x *ValidateResourceConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[35] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[67] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2041,7 +3565,7 @@ func (x *ValidateResourceConfig_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResourceConfig_Response.ProtoReflect.Descriptor instead. func (*ValidateResourceConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{9, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{19, 1} } func (x *ValidateResourceConfig_Response) GetDiagnostics() []*Diagnostic { @@ -2052,21 +3576,18 @@ func (x *ValidateResourceConfig_Response) GetDiagnostics() []*Diagnostic { } type ValidateDataResourceConfig_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields - - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateDataResourceConfig_Request) Reset() { *x = ValidateDataResourceConfig_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[36] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateDataResourceConfig_Request) String() string { @@ -2076,8 +3597,8 @@ func (x *ValidateDataResourceConfig_Request) String() string { func (*ValidateDataResourceConfig_Request) ProtoMessage() {} func (x *ValidateDataResourceConfig_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[36] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[68] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2089,7 +3610,7 @@ func (x *ValidateDataResourceConfig_Request) ProtoReflect() protoreflect.Message // Deprecated: Use ValidateDataResourceConfig_Request.ProtoReflect.Descriptor instead. func (*ValidateDataResourceConfig_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{10, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{20, 0} } func (x *ValidateDataResourceConfig_Request) GetTypeName() string { @@ -2107,20 +3628,17 @@ func (x *ValidateDataResourceConfig_Request) GetConfig() *DynamicValue { } type ValidateDataResourceConfig_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` unknownFields protoimpl.UnknownFields - - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ValidateDataResourceConfig_Response) Reset() { *x = ValidateDataResourceConfig_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[37] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ValidateDataResourceConfig_Response) String() string { @@ -2130,8 +3648,8 @@ func (x *ValidateDataResourceConfig_Response) String() string { func (*ValidateDataResourceConfig_Response) ProtoMessage() {} func (x *ValidateDataResourceConfig_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[37] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[69] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2143,7 +3661,7 @@ func (x *ValidateDataResourceConfig_Response) ProtoReflect() protoreflect.Messag // Deprecated: Use ValidateDataResourceConfig_Response.ProtoReflect.Descriptor instead. func (*ValidateDataResourceConfig_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{10, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{20, 1} } func (x *ValidateDataResourceConfig_Response) GetDiagnostics() []*Diagnostic { @@ -2153,22 +3671,116 @@ func (x *ValidateDataResourceConfig_Response) GetDiagnostics() []*Diagnostic { return nil } -type ConfigureProvider_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache +type ValidateEphemeralResourceConfig_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} - TerraformVersion string `protobuf:"bytes,1,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` +func (x *ValidateEphemeralResourceConfig_Request) Reset() { + *x = ValidateEphemeralResourceConfig_Request{} + mi := &file_tfplugin6_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEphemeralResourceConfig_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEphemeralResourceConfig_Request) ProtoMessage() {} + +func (x *ValidateEphemeralResourceConfig_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEphemeralResourceConfig_Request.ProtoReflect.Descriptor instead. +func (*ValidateEphemeralResourceConfig_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{21, 0} +} + +func (x *ValidateEphemeralResourceConfig_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *ValidateEphemeralResourceConfig_Request) GetConfig() *DynamicValue { + if x != nil { + return x.Config + } + return nil +} + +type ValidateEphemeralResourceConfig_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEphemeralResourceConfig_Response) Reset() { + *x = ValidateEphemeralResourceConfig_Response{} + mi := &file_tfplugin6_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEphemeralResourceConfig_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEphemeralResourceConfig_Response) ProtoMessage() {} + +func (x *ValidateEphemeralResourceConfig_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEphemeralResourceConfig_Response.ProtoReflect.Descriptor instead. +func (*ValidateEphemeralResourceConfig_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{21, 1} +} + +func (x *ValidateEphemeralResourceConfig_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type ConfigureProvider_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TerraformVersion string `protobuf:"bytes,1,opt,name=terraform_version,json=terraformVersion,proto3" json:"terraform_version,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConfigureProvider_Request) Reset() { *x = ConfigureProvider_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[38] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[72] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ConfigureProvider_Request) String() string { @@ -2178,8 +3790,8 @@ func (x *ConfigureProvider_Request) String() string { func (*ConfigureProvider_Request) ProtoMessage() {} func (x *ConfigureProvider_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[38] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[72] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2191,7 +3803,7 @@ func (x *ConfigureProvider_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProvider_Request.ProtoReflect.Descriptor instead. func (*ConfigureProvider_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{11, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{22, 0} } func (x *ConfigureProvider_Request) GetTerraformVersion() string { @@ -2208,21 +3820,25 @@ func (x *ConfigureProvider_Request) GetConfig() *DynamicValue { return nil } -type ConfigureProvider_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ConfigureProvider_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ConfigureProvider_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ConfigureProvider_Response) Reset() { *x = ConfigureProvider_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[39] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[73] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ConfigureProvider_Response) String() string { @@ -2232,8 +3848,8 @@ func (x *ConfigureProvider_Response) String() string { func (*ConfigureProvider_Response) ProtoMessage() {} func (x *ConfigureProvider_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[39] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[73] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2245,7 +3861,7 @@ func (x *ConfigureProvider_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigureProvider_Response.ProtoReflect.Descriptor instead. func (*ConfigureProvider_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{11, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{22, 1} } func (x *ConfigureProvider_Response) GetDiagnostics() []*Diagnostic { @@ -2255,24 +3871,31 @@ func (x *ConfigureProvider_Response) GetDiagnostics() []*Diagnostic { return nil } +// Request is the message that is sent to the provider during the +// ReadResource RPC. +// +// This message intentionally does not include configuration data as any +// configuration-based or configuration-conditional changes should occur +// during the PlanResourceChange RPC. Additionally, the configuration is +// not guaranteed to be wholly known nor match the given prior state, which +// could lead to unexpected provider behaviors for practitioners. type ReadResource_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - CurrentState *DynamicValue `protobuf:"bytes,2,opt,name=current_state,json=currentState,proto3" json:"current_state,omitempty"` - Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,4,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + CurrentState *DynamicValue `protobuf:"bytes,2,opt,name=current_state,json=currentState,proto3" json:"current_state,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,4,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,5,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + CurrentIdentity *ResourceIdentityData `protobuf:"bytes,6,opt,name=current_identity,json=currentIdentity,proto3" json:"current_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadResource_Request) Reset() { *x = ReadResource_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[40] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[74] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadResource_Request) String() string { @@ -2282,8 +3905,8 @@ func (x *ReadResource_Request) String() string { func (*ReadResource_Request) ProtoMessage() {} func (x *ReadResource_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[40] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[74] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2295,7 +3918,7 @@ func (x *ReadResource_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResource_Request.ProtoReflect.Descriptor instead. func (*ReadResource_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{12, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{23, 0} } func (x *ReadResource_Request) GetTypeName() string { @@ -2326,23 +3949,38 @@ func (x *ReadResource_Request) GetProviderMeta() *DynamicValue { return nil } -type ReadResource_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ReadResource_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` - Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` +func (x *ReadResource_Request) GetCurrentIdentity() *ResourceIdentityData { + if x != nil { + return x.CurrentIdentity + } + return nil +} + +type ReadResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,4,opt,name=deferred,proto3" json:"deferred,omitempty"` + NewIdentity *ResourceIdentityData `protobuf:"bytes,5,opt,name=new_identity,json=newIdentity,proto3" json:"new_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadResource_Response) Reset() { *x = ReadResource_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[41] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[75] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadResource_Response) String() string { @@ -2352,8 +3990,8 @@ func (x *ReadResource_Response) String() string { func (*ReadResource_Response) ProtoMessage() {} func (x *ReadResource_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[41] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[75] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2365,7 +4003,7 @@ func (x *ReadResource_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadResource_Response.ProtoReflect.Descriptor instead. func (*ReadResource_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{12, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{23, 1} } func (x *ReadResource_Response) GetNewState() *DynamicValue { @@ -2389,26 +4027,39 @@ func (x *ReadResource_Response) GetPrivate() []byte { return nil } -type PlanResourceChange_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ReadResource_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` - ProposedNewState *DynamicValue `protobuf:"bytes,3,opt,name=proposed_new_state,json=proposedNewState,proto3" json:"proposed_new_state,omitempty"` - Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` - PriorPrivate []byte `protobuf:"bytes,5,opt,name=prior_private,json=priorPrivate,proto3" json:"prior_private,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` +func (x *ReadResource_Response) GetNewIdentity() *ResourceIdentityData { + if x != nil { + return x.NewIdentity + } + return nil +} + +type PlanResourceChange_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` + ProposedNewState *DynamicValue `protobuf:"bytes,3,opt,name=proposed_new_state,json=proposedNewState,proto3" json:"proposed_new_state,omitempty"` + Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` + PriorPrivate []byte `protobuf:"bytes,5,opt,name=prior_private,json=priorPrivate,proto3" json:"prior_private,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,7,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + PriorIdentity *ResourceIdentityData `protobuf:"bytes,8,opt,name=prior_identity,json=priorIdentity,proto3" json:"prior_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PlanResourceChange_Request) Reset() { *x = PlanResourceChange_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[42] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[76] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PlanResourceChange_Request) String() string { @@ -2418,8 +4069,8 @@ func (x *PlanResourceChange_Request) String() string { func (*PlanResourceChange_Request) ProtoMessage() {} func (x *PlanResourceChange_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[42] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[76] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2431,7 +4082,7 @@ func (x *PlanResourceChange_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanResourceChange_Request.ProtoReflect.Descriptor instead. func (*PlanResourceChange_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{13, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{24, 0} } func (x *PlanResourceChange_Request) GetTypeName() string { @@ -2476,15 +4127,26 @@ func (x *PlanResourceChange_Request) GetProviderMeta() *DynamicValue { return nil } -type PlanResourceChange_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *PlanResourceChange_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - PlannedState *DynamicValue `protobuf:"bytes,1,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` - RequiresReplace []*AttributePath `protobuf:"bytes,2,rep,name=requires_replace,json=requiresReplace,proto3" json:"requires_replace,omitempty"` - PlannedPrivate []byte `protobuf:"bytes,3,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +func (x *PlanResourceChange_Request) GetPriorIdentity() *ResourceIdentityData { + if x != nil { + return x.PriorIdentity + } + return nil +} + +type PlanResourceChange_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + PlannedState *DynamicValue `protobuf:"bytes,1,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` + RequiresReplace []*AttributePath `protobuf:"bytes,2,rep,name=requires_replace,json=requiresReplace,proto3" json:"requires_replace,omitempty"` + PlannedPrivate []byte `protobuf:"bytes,3,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,4,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` // This may be set only by the helper/schema "SDK" in the main Terraform // repository, to request that Terraform Core >=0.12 permit additional // inconsistencies that can result from the legacy SDK type system @@ -2497,15 +4159,19 @@ type PlanResourceChange_Response struct { // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== // ==== DO NOT USE THIS ==== LegacyTypeSystem bool `protobuf:"varint,5,opt,name=legacy_type_system,json=legacyTypeSystem,proto3" json:"legacy_type_system,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,6,opt,name=deferred,proto3" json:"deferred,omitempty"` + PlannedIdentity *ResourceIdentityData `protobuf:"bytes,7,opt,name=planned_identity,json=plannedIdentity,proto3" json:"planned_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *PlanResourceChange_Response) Reset() { *x = PlanResourceChange_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[43] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[77] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *PlanResourceChange_Response) String() string { @@ -2515,8 +4181,8 @@ func (x *PlanResourceChange_Response) String() string { func (*PlanResourceChange_Response) ProtoMessage() {} func (x *PlanResourceChange_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[43] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[77] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2528,7 +4194,7 @@ func (x *PlanResourceChange_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use PlanResourceChange_Response.ProtoReflect.Descriptor instead. func (*PlanResourceChange_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{13, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{24, 1} } func (x *PlanResourceChange_Response) GetPlannedState() *DynamicValue { @@ -2566,26 +4232,38 @@ func (x *PlanResourceChange_Response) GetLegacyTypeSystem() bool { return false } -type ApplyResourceChange_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *PlanResourceChange_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` - PlannedState *DynamicValue `protobuf:"bytes,3,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` - Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` - PlannedPrivate []byte `protobuf:"bytes,5,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` +func (x *PlanResourceChange_Response) GetPlannedIdentity() *ResourceIdentityData { + if x != nil { + return x.PlannedIdentity + } + return nil +} + +type ApplyResourceChange_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + PriorState *DynamicValue `protobuf:"bytes,2,opt,name=prior_state,json=priorState,proto3" json:"prior_state,omitempty"` + PlannedState *DynamicValue `protobuf:"bytes,3,opt,name=planned_state,json=plannedState,proto3" json:"planned_state,omitempty"` + Config *DynamicValue `protobuf:"bytes,4,opt,name=config,proto3" json:"config,omitempty"` + PlannedPrivate []byte `protobuf:"bytes,5,opt,name=planned_private,json=plannedPrivate,proto3" json:"planned_private,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,6,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + PlannedIdentity *ResourceIdentityData `protobuf:"bytes,7,opt,name=planned_identity,json=plannedIdentity,proto3" json:"planned_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ApplyResourceChange_Request) Reset() { *x = ApplyResourceChange_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[44] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[78] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ApplyResourceChange_Request) String() string { @@ -2595,8 +4273,8 @@ func (x *ApplyResourceChange_Request) String() string { func (*ApplyResourceChange_Request) ProtoMessage() {} func (x *ApplyResourceChange_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[44] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[78] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2608,7 +4286,7 @@ func (x *ApplyResourceChange_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyResourceChange_Request.ProtoReflect.Descriptor instead. func (*ApplyResourceChange_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{14, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{25, 0} } func (x *ApplyResourceChange_Request) GetTypeName() string { @@ -2653,14 +4331,18 @@ func (x *ApplyResourceChange_Request) GetProviderMeta() *DynamicValue { return nil } -type ApplyResourceChange_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ApplyResourceChange_Request) GetPlannedIdentity() *ResourceIdentityData { + if x != nil { + return x.PlannedIdentity + } + return nil +} - NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` - Private []byte `protobuf:"bytes,2,opt,name=private,proto3" json:"private,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,3,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ApplyResourceChange_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + NewState *DynamicValue `protobuf:"bytes,1,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + Private []byte `protobuf:"bytes,2,opt,name=private,proto3" json:"private,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,3,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` // This may be set only by the helper/schema "SDK" in the main Terraform // repository, to request that Terraform Core >=0.12 permit additional // inconsistencies that can result from the legacy SDK type system @@ -2672,16 +4354,17 @@ type ApplyResourceChange_Response struct { // ==== DO NOT USE THIS ==== // ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ==== // ==== DO NOT USE THIS ==== - LegacyTypeSystem bool `protobuf:"varint,4,opt,name=legacy_type_system,json=legacyTypeSystem,proto3" json:"legacy_type_system,omitempty"` + LegacyTypeSystem bool `protobuf:"varint,4,opt,name=legacy_type_system,json=legacyTypeSystem,proto3" json:"legacy_type_system,omitempty"` + NewIdentity *ResourceIdentityData `protobuf:"bytes,5,opt,name=new_identity,json=newIdentity,proto3" json:"new_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ApplyResourceChange_Response) Reset() { *x = ApplyResourceChange_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[45] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ApplyResourceChange_Response) String() string { @@ -2691,8 +4374,8 @@ func (x *ApplyResourceChange_Response) String() string { func (*ApplyResourceChange_Response) ProtoMessage() {} func (x *ApplyResourceChange_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[45] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[79] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2704,7 +4387,7 @@ func (x *ApplyResourceChange_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyResourceChange_Response.ProtoReflect.Descriptor instead. func (*ApplyResourceChange_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{14, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{25, 1} } func (x *ApplyResourceChange_Response) GetNewState() *DynamicValue { @@ -2735,22 +4418,28 @@ func (x *ApplyResourceChange_Response) GetLegacyTypeSystem() bool { return false } -type ImportResourceState_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ApplyResourceChange_Response) GetNewIdentity() *ResourceIdentityData { + if x != nil { + return x.NewIdentity + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` +type ImportResourceState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + Identity *ResourceIdentityData `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState_Request) Reset() { *x = ImportResourceState_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[46] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState_Request) String() string { @@ -2760,8 +4449,8 @@ func (x *ImportResourceState_Request) String() string { func (*ImportResourceState_Request) ProtoMessage() {} func (x *ImportResourceState_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[46] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[80] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2773,7 +4462,7 @@ func (x *ImportResourceState_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportResourceState_Request.ProtoReflect.Descriptor instead. func (*ImportResourceState_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{15, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{26, 0} } func (x *ImportResourceState_Request) GetTypeName() string { @@ -2790,23 +4479,35 @@ func (x *ImportResourceState_Request) GetId() string { return "" } -type ImportResourceState_ImportedResource struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ImportResourceState_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - State *DynamicValue `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` - Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` +func (x *ImportResourceState_Request) GetIdentity() *ResourceIdentityData { + if x != nil { + return x.Identity + } + return nil +} + +type ImportResourceState_ImportedResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + State *DynamicValue `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3" json:"private,omitempty"` + Identity *ResourceIdentityData `protobuf:"bytes,4,opt,name=identity,proto3" json:"identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState_ImportedResource) Reset() { *x = ImportResourceState_ImportedResource{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[47] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState_ImportedResource) String() string { @@ -2816,8 +4517,8 @@ func (x *ImportResourceState_ImportedResource) String() string { func (*ImportResourceState_ImportedResource) ProtoMessage() {} func (x *ImportResourceState_ImportedResource) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[47] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[81] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2829,7 +4530,7 @@ func (x *ImportResourceState_ImportedResource) ProtoReflect() protoreflect.Messa // Deprecated: Use ImportResourceState_ImportedResource.ProtoReflect.Descriptor instead. func (*ImportResourceState_ImportedResource) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{15, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{26, 1} } func (x *ImportResourceState_ImportedResource) GetTypeName() string { @@ -2853,22 +4554,29 @@ func (x *ImportResourceState_ImportedResource) GetPrivate() []byte { return nil } -type ImportResourceState_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ImportResourceState_ImportedResource) GetIdentity() *ResourceIdentityData { + if x != nil { + return x.Identity + } + return nil +} +type ImportResourceState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` ImportedResources []*ImportResourceState_ImportedResource `protobuf:"bytes,1,rep,name=imported_resources,json=importedResources,proto3" json:"imported_resources,omitempty"` Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,3,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ImportResourceState_Response) Reset() { *x = ImportResourceState_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[48] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ImportResourceState_Response) String() string { @@ -2878,8 +4586,8 @@ func (x *ImportResourceState_Response) String() string { func (*ImportResourceState_Response) ProtoMessage() {} func (x *ImportResourceState_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[48] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[82] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2891,7 +4599,7 @@ func (x *ImportResourceState_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportResourceState_Response.ProtoReflect.Descriptor instead. func (*ImportResourceState_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{15, 2} + return file_tfplugin6_proto_rawDescGZIP(), []int{26, 2} } func (x *ImportResourceState_Response) GetImportedResources() []*ImportResourceState_ImportedResource { @@ -2908,23 +4616,213 @@ func (x *ImportResourceState_Response) GetDiagnostics() []*Diagnostic { return nil } -type ReadDataSource_Request struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ImportResourceState_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} - TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` - Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` - ProviderMeta *DynamicValue `protobuf:"bytes,3,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` +type MoveResourceState_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The address of the provider the resource is being moved from. + SourceProviderAddress string `protobuf:"bytes,1,opt,name=source_provider_address,json=sourceProviderAddress,proto3" json:"source_provider_address,omitempty"` + // The resource type that the resource is being moved from. + SourceTypeName string `protobuf:"bytes,2,opt,name=source_type_name,json=sourceTypeName,proto3" json:"source_type_name,omitempty"` + // The schema version of the resource type that the resource is being + // moved from. + SourceSchemaVersion int64 `protobuf:"varint,3,opt,name=source_schema_version,json=sourceSchemaVersion,proto3" json:"source_schema_version,omitempty"` + // The raw state of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + SourceState *RawState `protobuf:"bytes,4,opt,name=source_state,json=sourceState,proto3" json:"source_state,omitempty"` + // The resource type that the resource is being moved to. + TargetTypeName string `protobuf:"bytes,5,opt,name=target_type_name,json=targetTypeName,proto3" json:"target_type_name,omitempty"` + // The private state of the resource being moved. + SourcePrivate []byte `protobuf:"bytes,6,opt,name=source_private,json=sourcePrivate,proto3" json:"source_private,omitempty"` + // The raw identity of the resource being moved. Only the json field is + // populated, as there should be no legacy providers using the flatmap + // format that support newly introduced RPCs. + SourceIdentity *RawState `protobuf:"bytes,7,opt,name=source_identity,json=sourceIdentity,proto3" json:"source_identity,omitempty"` + // The identity schema version of the resource type that the resource + // is being moved from. + SourceIdentitySchemaVersion int64 `protobuf:"varint,8,opt,name=source_identity_schema_version,json=sourceIdentitySchemaVersion,proto3" json:"source_identity_schema_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MoveResourceState_Request) Reset() { + *x = MoveResourceState_Request{} + mi := &file_tfplugin6_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MoveResourceState_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoveResourceState_Request) ProtoMessage() {} + +func (x *MoveResourceState_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[83] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoveResourceState_Request.ProtoReflect.Descriptor instead. +func (*MoveResourceState_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{27, 0} +} + +func (x *MoveResourceState_Request) GetSourceProviderAddress() string { + if x != nil { + return x.SourceProviderAddress + } + return "" +} + +func (x *MoveResourceState_Request) GetSourceTypeName() string { + if x != nil { + return x.SourceTypeName + } + return "" +} + +func (x *MoveResourceState_Request) GetSourceSchemaVersion() int64 { + if x != nil { + return x.SourceSchemaVersion + } + return 0 +} + +func (x *MoveResourceState_Request) GetSourceState() *RawState { + if x != nil { + return x.SourceState + } + return nil +} + +func (x *MoveResourceState_Request) GetTargetTypeName() string { + if x != nil { + return x.TargetTypeName + } + return "" +} + +func (x *MoveResourceState_Request) GetSourcePrivate() []byte { + if x != nil { + return x.SourcePrivate + } + return nil +} + +func (x *MoveResourceState_Request) GetSourceIdentity() *RawState { + if x != nil { + return x.SourceIdentity + } + return nil +} + +func (x *MoveResourceState_Request) GetSourceIdentitySchemaVersion() int64 { + if x != nil { + return x.SourceIdentitySchemaVersion + } + return 0 +} + +type MoveResourceState_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The state of the resource after it has been moved. + TargetState *DynamicValue `protobuf:"bytes,1,opt,name=target_state,json=targetState,proto3" json:"target_state,omitempty"` + // Any diagnostics that occurred during the move. + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + // The private state of the resource after it has been moved. + TargetPrivate []byte `protobuf:"bytes,3,opt,name=target_private,json=targetPrivate,proto3" json:"target_private,omitempty"` + TargetIdentity *ResourceIdentityData `protobuf:"bytes,4,opt,name=target_identity,json=targetIdentity,proto3" json:"target_identity,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MoveResourceState_Response) Reset() { + *x = MoveResourceState_Response{} + mi := &file_tfplugin6_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MoveResourceState_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MoveResourceState_Response) ProtoMessage() {} + +func (x *MoveResourceState_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[84] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MoveResourceState_Response.ProtoReflect.Descriptor instead. +func (*MoveResourceState_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{27, 1} +} + +func (x *MoveResourceState_Response) GetTargetState() *DynamicValue { + if x != nil { + return x.TargetState + } + return nil +} + +func (x *MoveResourceState_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *MoveResourceState_Response) GetTargetPrivate() []byte { + if x != nil { + return x.TargetPrivate + } + return nil +} + +func (x *MoveResourceState_Response) GetTargetIdentity() *ResourceIdentityData { + if x != nil { + return x.TargetIdentity + } + return nil +} + +type ReadDataSource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ProviderMeta *DynamicValue `protobuf:"bytes,3,opt,name=provider_meta,json=providerMeta,proto3" json:"provider_meta,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,4,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadDataSource_Request) Reset() { *x = ReadDataSource_Request{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[49] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadDataSource_Request) String() string { @@ -2934,8 +4832,8 @@ func (x *ReadDataSource_Request) String() string { func (*ReadDataSource_Request) ProtoMessage() {} func (x *ReadDataSource_Request) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[49] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[85] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -2947,7 +4845,7 @@ func (x *ReadDataSource_Request) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadDataSource_Request.ProtoReflect.Descriptor instead. func (*ReadDataSource_Request) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{16, 0} + return file_tfplugin6_proto_rawDescGZIP(), []int{28, 0} } func (x *ReadDataSource_Request) GetTypeName() string { @@ -2971,22 +4869,29 @@ func (x *ReadDataSource_Request) GetProviderMeta() *DynamicValue { return nil } -type ReadDataSource_Response struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields +func (x *ReadDataSource_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} - State *DynamicValue `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` - Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` +type ReadDataSource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *DynamicValue `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + // deferred is set if the provider is deferring the change. If set the caller + // needs to handle the deferral. + Deferred *Deferred `protobuf:"bytes,3,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ReadDataSource_Response) Reset() { *x = ReadDataSource_Response{} - if protoimpl.UnsafeEnabled { - mi := &file_tfplugin6_proto_msgTypes[50] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_tfplugin6_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ReadDataSource_Response) String() string { @@ -2996,8 +4901,8 @@ func (x *ReadDataSource_Response) String() string { func (*ReadDataSource_Response) ProtoMessage() {} func (x *ReadDataSource_Response) ProtoReflect() protoreflect.Message { - mi := &file_tfplugin6_proto_msgTypes[50] - if protoimpl.UnsafeEnabled && x != nil { + mi := &file_tfplugin6_proto_msgTypes[86] + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -3009,7 +4914,7 @@ func (x *ReadDataSource_Response) ProtoReflect() protoreflect.Message { // Deprecated: Use ReadDataSource_Response.ProtoReflect.Descriptor instead. func (*ReadDataSource_Response) Descriptor() ([]byte, []int) { - return file_tfplugin6_proto_rawDescGZIP(), []int{16, 1} + return file_tfplugin6_proto_rawDescGZIP(), []int{28, 1} } func (x *ReadDataSource_Response) GetState() *DynamicValue { @@ -3026,168 +4931,876 @@ func (x *ReadDataSource_Response) GetDiagnostics() []*Diagnostic { return nil } +func (x *ReadDataSource_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +type OpenEphemeralResource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Config *DynamicValue `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + ClientCapabilities *ClientCapabilities `protobuf:"bytes,3,opt,name=client_capabilities,json=clientCapabilities,proto3" json:"client_capabilities,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenEphemeralResource_Request) Reset() { + *x = OpenEphemeralResource_Request{} + mi := &file_tfplugin6_proto_msgTypes[87] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenEphemeralResource_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenEphemeralResource_Request) ProtoMessage() {} + +func (x *OpenEphemeralResource_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[87] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenEphemeralResource_Request.ProtoReflect.Descriptor instead. +func (*OpenEphemeralResource_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{29, 0} +} + +func (x *OpenEphemeralResource_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *OpenEphemeralResource_Request) GetConfig() *DynamicValue { + if x != nil { + return x.Config + } + return nil +} + +func (x *OpenEphemeralResource_Request) GetClientCapabilities() *ClientCapabilities { + if x != nil { + return x.ClientCapabilities + } + return nil +} + +type OpenEphemeralResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + RenewAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=renew_at,json=renewAt,proto3,oneof" json:"renew_at,omitempty"` + Result *DynamicValue `protobuf:"bytes,3,opt,name=result,proto3" json:"result,omitempty"` + Private []byte `protobuf:"bytes,4,opt,name=private,proto3,oneof" json:"private,omitempty"` + Deferred *Deferred `protobuf:"bytes,5,opt,name=deferred,proto3" json:"deferred,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OpenEphemeralResource_Response) Reset() { + *x = OpenEphemeralResource_Response{} + mi := &file_tfplugin6_proto_msgTypes[88] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OpenEphemeralResource_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OpenEphemeralResource_Response) ProtoMessage() {} + +func (x *OpenEphemeralResource_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[88] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OpenEphemeralResource_Response.ProtoReflect.Descriptor instead. +func (*OpenEphemeralResource_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{29, 1} +} + +func (x *OpenEphemeralResource_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetRenewAt() *timestamppb.Timestamp { + if x != nil { + return x.RenewAt + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetResult() *DynamicValue { + if x != nil { + return x.Result + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +func (x *OpenEphemeralResource_Response) GetDeferred() *Deferred { + if x != nil { + return x.Deferred + } + return nil +} + +type RenewEphemeralResource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Private []byte `protobuf:"bytes,2,opt,name=private,proto3,oneof" json:"private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewEphemeralResource_Request) Reset() { + *x = RenewEphemeralResource_Request{} + mi := &file_tfplugin6_proto_msgTypes[89] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewEphemeralResource_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewEphemeralResource_Request) ProtoMessage() {} + +func (x *RenewEphemeralResource_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[89] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewEphemeralResource_Request.ProtoReflect.Descriptor instead. +func (*RenewEphemeralResource_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{30, 0} +} + +func (x *RenewEphemeralResource_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *RenewEphemeralResource_Request) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +type RenewEphemeralResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + RenewAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=renew_at,json=renewAt,proto3,oneof" json:"renew_at,omitempty"` + Private []byte `protobuf:"bytes,3,opt,name=private,proto3,oneof" json:"private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenewEphemeralResource_Response) Reset() { + *x = RenewEphemeralResource_Response{} + mi := &file_tfplugin6_proto_msgTypes[90] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenewEphemeralResource_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenewEphemeralResource_Response) ProtoMessage() {} + +func (x *RenewEphemeralResource_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[90] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenewEphemeralResource_Response.ProtoReflect.Descriptor instead. +func (*RenewEphemeralResource_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{30, 1} +} + +func (x *RenewEphemeralResource_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +func (x *RenewEphemeralResource_Response) GetRenewAt() *timestamppb.Timestamp { + if x != nil { + return x.RenewAt + } + return nil +} + +func (x *RenewEphemeralResource_Response) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +type CloseEphemeralResource_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + TypeName string `protobuf:"bytes,1,opt,name=type_name,json=typeName,proto3" json:"type_name,omitempty"` + Private []byte `protobuf:"bytes,2,opt,name=private,proto3,oneof" json:"private,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseEphemeralResource_Request) Reset() { + *x = CloseEphemeralResource_Request{} + mi := &file_tfplugin6_proto_msgTypes[91] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseEphemeralResource_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseEphemeralResource_Request) ProtoMessage() {} + +func (x *CloseEphemeralResource_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[91] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseEphemeralResource_Request.ProtoReflect.Descriptor instead. +func (*CloseEphemeralResource_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{31, 0} +} + +func (x *CloseEphemeralResource_Request) GetTypeName() string { + if x != nil { + return x.TypeName + } + return "" +} + +func (x *CloseEphemeralResource_Request) GetPrivate() []byte { + if x != nil { + return x.Private + } + return nil +} + +type CloseEphemeralResource_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Diagnostics []*Diagnostic `protobuf:"bytes,1,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseEphemeralResource_Response) Reset() { + *x = CloseEphemeralResource_Response{} + mi := &file_tfplugin6_proto_msgTypes[92] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseEphemeralResource_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseEphemeralResource_Response) ProtoMessage() {} + +func (x *CloseEphemeralResource_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[92] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseEphemeralResource_Response.ProtoReflect.Descriptor instead. +func (*CloseEphemeralResource_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{31, 1} +} + +func (x *CloseEphemeralResource_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type GetFunctions_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFunctions_Request) Reset() { + *x = GetFunctions_Request{} + mi := &file_tfplugin6_proto_msgTypes[93] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFunctions_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFunctions_Request) ProtoMessage() {} + +func (x *GetFunctions_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[93] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFunctions_Request.ProtoReflect.Descriptor instead. +func (*GetFunctions_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{32, 0} +} + +type GetFunctions_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + // functions is a mapping of function names to definitions. + Functions map[string]*Function `protobuf:"bytes,1,rep,name=functions,proto3" json:"functions,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // diagnostics is any warnings or errors. + Diagnostics []*Diagnostic `protobuf:"bytes,2,rep,name=diagnostics,proto3" json:"diagnostics,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetFunctions_Response) Reset() { + *x = GetFunctions_Response{} + mi := &file_tfplugin6_proto_msgTypes[94] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetFunctions_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetFunctions_Response) ProtoMessage() {} + +func (x *GetFunctions_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[94] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetFunctions_Response.ProtoReflect.Descriptor instead. +func (*GetFunctions_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{32, 1} +} + +func (x *GetFunctions_Response) GetFunctions() map[string]*Function { + if x != nil { + return x.Functions + } + return nil +} + +func (x *GetFunctions_Response) GetDiagnostics() []*Diagnostic { + if x != nil { + return x.Diagnostics + } + return nil +} + +type CallFunction_Request struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Arguments []*DynamicValue `protobuf:"bytes,2,rep,name=arguments,proto3" json:"arguments,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallFunction_Request) Reset() { + *x = CallFunction_Request{} + mi := &file_tfplugin6_proto_msgTypes[96] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallFunction_Request) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallFunction_Request) ProtoMessage() {} + +func (x *CallFunction_Request) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[96] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallFunction_Request.ProtoReflect.Descriptor instead. +func (*CallFunction_Request) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{33, 0} +} + +func (x *CallFunction_Request) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CallFunction_Request) GetArguments() []*DynamicValue { + if x != nil { + return x.Arguments + } + return nil +} + +type CallFunction_Response struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *DynamicValue `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Error *FunctionError `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CallFunction_Response) Reset() { + *x = CallFunction_Response{} + mi := &file_tfplugin6_proto_msgTypes[97] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CallFunction_Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CallFunction_Response) ProtoMessage() {} + +func (x *CallFunction_Response) ProtoReflect() protoreflect.Message { + mi := &file_tfplugin6_proto_msgTypes[97] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CallFunction_Response.ProtoReflect.Descriptor instead. +func (*CallFunction_Response) Descriptor() ([]byte, []int) { + return file_tfplugin6_proto_rawDescGZIP(), []int{33, 1} +} + +func (x *CallFunction_Response) GetResult() *DynamicValue { + if x != nil { + return x.Result + } + return nil +} + +func (x *CallFunction_Response) GetError() *FunctionError { + if x != nil { + return x.Error + } + return nil +} + var File_tfplugin6_proto protoreflect.FileDescriptor -var file_tfplugin6_proto_rawDesc = []byte{ +var file_tfplugin6_proto_rawDesc = string([]byte{ 0x0a, 0x0f, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x09, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x22, 0x3c, 0x0a, 0x0c, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x6d, - 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0xe3, 0x01, 0x0a, 0x0a, 0x44, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x12, 0x3a, 0x0a, 0x08, 0x73, 0x65, 0x76, - 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x74, 0x66, - 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, - 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x36, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, - 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, - 0x50, 0x61, 0x74, 0x68, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x22, - 0x2f, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x49, - 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, - 0x52, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, - 0x22, 0xdc, 0x01, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, - 0x74, 0x68, 0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, - 0x52, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x95, 0x01, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, - 0x12, 0x27, 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, - 0x4b, 0x65, 0x79, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6c, 0x65, - 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x48, 0x00, 0x52, 0x0d, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, - 0x49, 0x6e, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, - 0x3b, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x1a, - 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x0a, 0x08, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x96, 0x01, 0x0a, - 0x08, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, - 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, - 0x07, 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x2e, 0x46, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x1a, 0x3a, 0x0a, 0x0c, 0x46, 0x6c, 0x61, - 0x74, 0x6d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x95, 0x0a, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x05, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, - 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x1a, 0xa2, 0x02, 0x0a, 0x05, 0x42, 0x6c, - 0x6f, 0x63, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, - 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, - 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x0a, - 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x6f, 0x12, 0x09, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x1a, 0x1f, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x3c, 0x0a, + 0x0c, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, + 0x6d, 0x73, 0x67, 0x70, 0x61, 0x63, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0xe3, 0x01, 0x0a, 0x0a, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x12, 0x3a, 0x0a, 0x08, 0x73, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x12, 0x16, 0x0a, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x12, 0x36, 0x0a, 0x09, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x09, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x22, 0x2f, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x0b, 0x0a, 0x07, + 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, + 0x02, 0x22, 0x6b, 0x0a, 0x0d, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x30, 0x0a, 0x11, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x03, 0x48, 0x00, 0x52, 0x10, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x72, 0x67, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x66, 0x75, 0x6e, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0xdc, + 0x01, 0x0a, 0x0d, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, + 0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x65, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x2e, 0x53, 0x74, 0x65, 0x70, 0x52, 0x05, + 0x73, 0x74, 0x65, 0x70, 0x73, 0x1a, 0x95, 0x01, 0x0a, 0x04, 0x53, 0x74, 0x65, 0x70, 0x12, 0x27, + 0x0a, 0x0e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6c, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x10, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, + 0x79, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x12, 0x28, 0x0a, 0x0f, 0x65, 0x6c, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, + 0x48, 0x00, 0x52, 0x0d, 0x65, 0x6c, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x49, 0x6e, + 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x73, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x3b, 0x0a, + 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x1a, 0x09, 0x0a, + 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x96, 0x01, 0x0a, 0x08, 0x52, + 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x12, 0x3a, 0x0a, 0x07, 0x66, + 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x2e, 0x46, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, + 0x66, 0x6c, 0x61, 0x74, 0x6d, 0x61, 0x70, 0x1a, 0x3a, 0x0a, 0x0c, 0x46, 0x6c, 0x61, 0x74, 0x6d, + 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0xd8, 0x02, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x18, + 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x64, 0x0a, 0x13, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x12, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x1a, 0xbd, + 0x01, 0x0a, 0x11, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x13, + 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x69, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x72, 0x65, 0x71, 0x75, 0x69, + 0x72, 0x65, 0x64, 0x46, 0x6f, 0x72, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x2e, 0x0a, 0x13, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x6f, 0x72, 0x5f, 0x69, 0x6d, 0x70, + 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x61, 0x6c, 0x46, 0x6f, 0x72, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x20, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x54, + 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12, 0x3c, 0x0a, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x44, 0x61, 0x74, 0x61, 0x22, 0xb4, 0x0a, 0x0a, 0x06, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a, 0x05, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, + 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x1a, 0xa2, 0x02, 0x0a, 0x05, 0x42, 0x6c, 0x6f, + 0x63, 0x6b, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3b, 0x0a, 0x0a, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x0b, 0x62, 0x6c, 0x6f, + 0x63, 0x6b, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x0a, 0x62, + 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x10, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1e, 0x0a, + 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0x83, 0x03, + 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x52, 0x0a, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x20, + 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, + 0x75, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x76, + 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x76, 0x65, 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, + 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, + 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, + 0x61, 0x74, 0x65, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x77, 0x72, 0x69, 0x74, 0x65, 0x5f, 0x6f, 0x6e, + 0x6c, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4f, + 0x6e, 0x6c, 0x79, 0x1a, 0xa7, 0x02, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, + 0x6f, 0x63, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x2d, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, + 0x43, 0x0a, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, + 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6e, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, 0x65, 0x6d, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x49, 0x74, 0x65, 0x6d, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x4d, + 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, + 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, + 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, + 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, + 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x05, 0x1a, 0x8b, 0x02, + 0x0a, 0x06, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x3b, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6e, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, 0x65, + 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x6d, 0x69, + 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1f, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x74, + 0x65, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x6d, + 0x61, 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x42, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x69, + 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, + 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, + 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, + 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x10, 0x04, 0x22, 0x8e, 0x05, 0x0a, 0x08, + 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3d, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x52, 0x0a, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x4c, 0x0a, 0x12, 0x76, 0x61, 0x72, 0x69, 0x61, + 0x64, 0x69, 0x63, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, + 0x65, 0x72, 0x52, 0x11, 0x76, 0x61, 0x72, 0x69, 0x61, 0x64, 0x69, 0x63, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x74, 0x75, 0x72, + 0x6e, 0x52, 0x06, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x6d, + 0x6d, 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, + 0x61, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x72, 0x69, + 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x2f, 0x0a, 0x13, 0x64, 0x65, 0x70, 0x72, 0x65, + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0xf3, 0x01, 0x0a, 0x09, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x28, + 0x0a, 0x10, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x6e, 0x75, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x4e, + 0x75, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x61, 0x6c, 0x6c, 0x6f, + 0x77, 0x5f, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x6e, 0x6b, + 0x6e, 0x6f, 0x77, 0x6e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, - 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1e, - 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xe4, - 0x02, 0x0a, 0x09, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, - 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x52, 0x0a, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x1a, 0x0a, - 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x08, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x63, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x76, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x65, 0x6e, 0x73, 0x69, 0x74, - 0x69, 0x76, 0x65, 0x12, 0x40, 0x0a, 0x10, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, - 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x0f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, - 0x74, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x72, 0x65, - 0x63, 0x61, 0x74, 0x65, 0x64, 0x1a, 0xa7, 0x02, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, - 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, - 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, - 0x6b, 0x12, 0x43, 0x0a, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, - 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x42, 0x6c, 0x6f, 0x63, - 0x6b, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, 0x6e, - 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, 0x74, - 0x65, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x69, 0x6e, 0x49, 0x74, - 0x65, 0x6d, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x73, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6d, 0x61, 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, - 0x22, 0x4d, 0x0a, 0x0b, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, - 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, - 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, - 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x53, 0x45, 0x54, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, - 0x41, 0x50, 0x10, 0x04, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x52, 0x4f, 0x55, 0x50, 0x10, 0x05, 0x1a, - 0x8b, 0x02, 0x0a, 0x06, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x3b, 0x0a, 0x0a, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, - 0x61, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x52, 0x0a, 0x61, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x3e, 0x0a, 0x07, 0x6e, 0x65, 0x73, 0x74, 0x69, - 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4f, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x2e, 0x4e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x52, 0x07, - 0x6e, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x12, 0x1f, 0x0a, 0x09, 0x6d, 0x69, 0x6e, 0x5f, 0x69, - 0x74, 0x65, 0x6d, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, - 0x6d, 0x69, 0x6e, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x12, 0x1f, 0x0a, 0x09, 0x6d, 0x61, 0x78, 0x5f, - 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x42, 0x02, 0x18, 0x01, 0x52, - 0x08, 0x6d, 0x61, 0x78, 0x49, 0x74, 0x65, 0x6d, 0x73, 0x22, 0x42, 0x0a, 0x0b, 0x4e, 0x65, 0x73, - 0x74, 0x69, 0x6e, 0x67, 0x4d, 0x6f, 0x64, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x49, 0x4e, 0x56, 0x41, - 0x4c, 0x49, 0x44, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x10, - 0x01, 0x12, 0x08, 0x0a, 0x04, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x53, - 0x45, 0x54, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x41, 0x50, 0x10, 0x04, 0x22, 0xeb, 0x05, - 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, - 0x65, 0x6d, 0x61, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x91, - 0x05, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x70, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x10, 0x72, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x73, 0x12, 0x6c, 0x0a, 0x13, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x4b, 0x69, 0x6e, 0x64, 0x1a, 0x1c, + 0x0a, 0x06, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa8, 0x01, 0x0a, + 0x12, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x69, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x5f, 0x64, 0x65, 0x73, 0x74, + 0x72, 0x6f, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x70, 0x6c, 0x61, 0x6e, 0x44, + 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x12, 0x3f, 0x0a, 0x1c, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x6f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x19, 0x67, 0x65, + 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x12, 0x2e, 0x0a, 0x13, 0x6d, 0x6f, 0x76, 0x65, 0x5f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x6d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x12, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x29, + 0x0a, 0x10, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x61, 0x6c, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, + 0x61, 0x6c, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x12, 0x41, 0x0a, 0x1d, 0x77, 0x72, 0x69, + 0x74, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x5f, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x1a, 0x77, 0x72, 0x69, 0x74, 0x65, 0x4f, 0x6e, 0x6c, 0x79, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x22, 0xa2, 0x01, 0x0a, + 0x08, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x06, 0x72, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x2e, 0x52, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0x62, 0x0a, + 0x06, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, + 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, + 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x01, 0x12, 0x1b, 0x0a, 0x17, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x44, 0x45, 0x52, 0x5f, 0x43, 0x4f, + 0x4e, 0x46, 0x49, 0x47, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x02, 0x12, 0x11, + 0x0a, 0x0d, 0x41, 0x42, 0x53, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x52, 0x45, 0x52, 0x45, 0x51, 0x10, + 0x03, 0x22, 0xa3, 0x05, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0xca, 0x03, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x13, 0x73, 0x65, 0x72, + 0x76, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x12, 0x4c, 0x0a, 0x0c, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, + 0x12, 0x45, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x09, 0x72, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x59, + 0x0a, 0x13, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x2e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x12, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x30, 0x0a, 0x11, 0x45, 0x70, 0x68, + 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, + 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x26, 0x0a, 0x10, 0x46, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x1a, 0x31, 0x0a, 0x12, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, + 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x1a, 0x2f, 0x0a, 0x10, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, + 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, + 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xab, 0x08, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x1a, 0x09, 0x0a, + 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x8a, 0x08, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x10, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x64, 0x61, - 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, - 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, - 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x36, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, - 0x12, 0x60, 0x0a, 0x13, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, - 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, - 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x6c, 0x0a, 0x13, 0x64, + 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, + 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x11, 0x64, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x52, 0x0a, 0x09, 0x66, 0x75, 0x6e, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x81, 0x01, + 0x0a, 0x1a, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, + 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, + 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x18, 0x65, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, + 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x36, 0x0a, 0x0d, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, + 0x74, 0x61, 0x12, 0x4e, 0x0a, 0x13, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x63, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x55, 0x0a, 0x14, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, @@ -3201,97 +5814,183 @@ var file_tfplugin6_proto_rawDesc = []byte{ 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x37, 0x0a, 0x12, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x61, 0x70, 0x61, - 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x6c, 0x61, 0x6e, - 0x5f, 0x64, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, - 0x70, 0x6c, 0x61, 0x6e, 0x44, 0x65, 0x73, 0x74, 0x72, 0x6f, 0x79, 0x22, 0x99, 0x01, 0x0a, 0x16, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x3a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, - 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, - 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, - 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x90, 0x02, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, - 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x1a, 0x72, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, - 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, - 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x09, 0x72, 0x61, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x36, 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x08, 0x72, 0x61, 0x77, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x1a, 0x83, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x38, 0x01, 0x1a, 0x51, 0x0a, 0x0e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5e, 0x0a, 0x1d, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, + 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x99, 0x01, 0x0a, 0x16, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x1a, 0x3a, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x06, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x22, 0x90, 0x02, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x72, 0x0a, 0x07, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, + 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x09, + 0x72, 0x61, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x61, 0x77, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x08, 0x72, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x83, + 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x0e, 0x75, + 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0d, 0x75, 0x70, + 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x22, 0xc4, 0x02, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x73, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x9a, + 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x10, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x43, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0f, 0x69, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x1a, 0x65, 0x0a, 0x14, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, + 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xa7, 0x02, 0x0a, 0x17, + 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0x78, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x0c, 0x72, 0x61, 0x77, + 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x61, 0x77, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x72, 0x61, 0x77, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x1a, 0x91, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, + 0x0a, 0x11, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x10, 0x75, 0x70, 0x67, 0x72, + 0x61, 0x64, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x37, 0x0a, 0x0b, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x87, 0x02, 0x0a, 0x16, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x1a, 0xa7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x0d, 0x75, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, - 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xb6, 0x01, 0x0a, 0x16, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, + 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, + 0xba, 0x01, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x57, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, + 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, + 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0xbf, 0x01, 0x0a, + 0x1f, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, + 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, + 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, + 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x92, + 0x02, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x1a, 0xb7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, 0x72, + 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, - 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, + 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4e, + 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, + 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x73, 0x22, 0xba, 0x01, 0x0a, 0x1a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x1a, 0x57, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, - 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, 0x43, 0x0a, 0x08, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, - 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, - 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, - 0x22, 0xc1, 0x01, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x1a, 0x67, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x5f, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x74, 0x65, - 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2f, - 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x1a, - 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, - 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, - 0x74, 0x69, 0x63, 0x73, 0x22, 0xe3, 0x02, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0xbc, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3c, - 0x0a, 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, - 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, - 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, - 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x4d, 0x65, 0x74, 0x61, 0x1a, 0x93, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x34, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, + 0x69, 0x63, 0x73, 0x22, 0xf4, 0x04, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x1a, 0xd8, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x3c, 0x0a, + 0x0d, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, - 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, - 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, - 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, - 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xf2, 0x04, 0x0a, 0x12, 0x50, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x63, + 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, + 0x65, 0x74, 0x61, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, + 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x69, 0x65, 0x73, 0x12, 0x4a, 0x0a, 0x10, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0f, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, + 0x88, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x09, + 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, + 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x6e, + 0x65, 0x77, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x87, 0x07, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x1a, 0xbb, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, + 0x65, 0x1a, 0xd3, 0x03, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, @@ -3310,343 +6009,725 @@ var file_tfplugin6_proto_rawDesc = []byte{ 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x1a, - 0x9d, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, - 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, - 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x6c, - 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x43, 0x0a, 0x10, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x02, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, - 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, - 0x27, 0x0a, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, - 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, - 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, - 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, - 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, - 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x22, - 0x92, 0x04, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0xb6, 0x02, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0a, - 0x70, 0x72, 0x69, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x6c, - 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, - 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, - 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x6c, 0x61, - 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x05, 0x20, 0x01, - 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, - 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, - 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, - 0x1a, 0xc1, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, - 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, - 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, - 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x03, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, - 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x10, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, 0x79, - 0x73, 0x74, 0x65, 0x6d, 0x22, 0xed, 0x02, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0x36, 0x0a, 0x07, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, - 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x1a, 0x78, 0x0a, 0x10, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, + 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, + 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, + 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, + 0x46, 0x0a, 0x0e, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0x9a, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, + 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, + 0x74, 0x65, 0x12, 0x43, 0x0a, 0x10, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x72, + 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x50, 0x61, 0x74, 0x68, 0x52, 0x0f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, + 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, + 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, + 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, + 0x61, 0x63, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, + 0x65, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, + 0x72, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, + 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x4a, 0x0a, 0x10, 0x70, 0x6c, 0x61, 0x6e, + 0x6e, 0x65, 0x64, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, + 0x61, 0x74, 0x61, 0x52, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x74, 0x79, 0x22, 0xa2, 0x05, 0x0a, 0x13, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x1a, 0x82, 0x03, 0x0a, + 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, - 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, + 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x5f, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x6f, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x3c, 0x0a, 0x0d, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0c, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x0a, + 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x27, + 0x0a, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, + 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, + 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, + 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x4a, 0x0a, 0x10, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, + 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, + 0x52, 0x0f, 0x70, 0x6c, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x1a, 0x85, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, + 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x37, + 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, + 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6c, 0x65, 0x67, 0x61, 0x63, + 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x10, 0x6c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x53, + 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x42, 0x0a, 0x0c, 0x6e, 0x65, 0x77, 0x5f, 0x69, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0b, 0x6e, 0x65, + 0x77, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0xea, 0x04, 0x0a, 0x13, 0x49, 0x6d, + 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x1a, 0xc3, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, + 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, + 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, + 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, + 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, + 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x69, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, 0xb5, 0x01, 0x0a, 0x10, 0x49, 0x6d, 0x70, 0x6f, + 0x72, 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x12, 0x3b, 0x0a, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, + 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x08, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x1a, + 0xd4, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x12, + 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, + 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x11, 0x69, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0b, + 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, + 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, + 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x22, 0xb4, 0x05, 0x0a, 0x11, 0x4d, 0x6f, 0x76, 0x65, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x1a, 0xab, 0x03, 0x0a, + 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x36, 0x0a, 0x17, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, + 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x15, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x28, 0x0a, 0x10, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x13, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x36, + 0x0a, 0x0c, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x52, 0x61, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x54, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x3c, 0x0a, 0x0f, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x61, 0x77, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, + 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x1e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xf0, 0x01, 0x0a, 0x08, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, + 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0b, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, + 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x25, 0x0a, 0x0e, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x12, 0x48, 0x0a, 0x0f, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x69, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x44, 0x61, 0x74, 0x61, 0x52, 0x0e, 0x74, + 0x61, 0x72, 0x67, 0x65, 0x74, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x22, 0x9e, 0x03, + 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x1a, 0xe5, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, + 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, + 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x1a, 0xa3, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x1a, 0xa3, - 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x5e, 0x0a, 0x12, 0x69, - 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x36, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x11, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x65, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, - 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, - 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, - 0x74, 0x69, 0x63, 0x73, 0x22, 0x9c, 0x02, 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, - 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x95, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, + 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, + 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x2f, 0x0a, + 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x65, 0x66, 0x65, + 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x22, 0xdd, + 0x03, 0x0a, 0x15, 0x4f, 0x70, 0x65, 0x6e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0xa7, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x2f, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, + 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x4e, 0x0a, 0x13, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x12, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x1a, 0x99, 0x02, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x72, 0x65, 0x6e, 0x65, + 0x77, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x41, + 0x74, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x72, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x44, 0x65, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x52, 0x08, 0x64, 0x65, 0x66, + 0x65, 0x72, 0x72, 0x65, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x5f, + 0x61, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xa5, + 0x02, 0x0a, 0x16, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, + 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x1a, 0x51, 0x0a, 0x07, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x48, 0x00, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, + 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x1a, 0xb7, 0x01, 0x0a, + 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, + 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, + 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, + 0x63, 0x73, 0x12, 0x3a, 0x0a, 0x08, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x5f, 0x61, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1d, + 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, + 0x01, 0x52, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x72, 0x65, 0x6e, 0x65, 0x77, 0x5f, 0x61, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, + 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x1a, 0x51, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x09, + 0x74, 0x79, 0x70, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x74, 0x79, 0x70, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x07, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x1a, 0x43, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, + 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x22, 0x81, 0x02, 0x0a, 0x0c, 0x47, 0x65, + 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x09, 0x0a, 0x07, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0xe5, 0x01, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x66, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, + 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x1a, 0x51, 0x0a, 0x0e, 0x46, 0x75, + 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x29, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd1, 0x01, + 0x0a, 0x0c, 0x43, 0x61, 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x54, + 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, + 0x09, 0x61, 0x72, 0x67, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, - 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x3c, 0x0a, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x6d, 0x65, - 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, - 0x65, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x1a, - 0x72, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, - 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, - 0x6c, 0x75, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x37, 0x0a, 0x0b, 0x64, 0x69, - 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x15, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x69, 0x61, 0x67, - 0x6e, 0x6f, 0x73, 0x74, 0x69, 0x63, 0x52, 0x0b, 0x64, 0x69, 0x61, 0x67, 0x6e, 0x6f, 0x73, 0x74, - 0x69, 0x63, 0x73, 0x2a, 0x25, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, - 0x64, 0x12, 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, - 0x4d, 0x41, 0x52, 0x4b, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x32, 0xcc, 0x09, 0x0a, 0x08, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x60, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x24, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, - 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, - 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, - 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x56, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x09, 0x61, 0x72, 0x67, 0x75, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x1a, 0x6b, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x2f, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x44, 0x79, 0x6e, + 0x61, 0x6d, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x12, 0x2e, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x18, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x46, 0x75, 0x6e, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x2a, 0x25, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4b, 0x69, 0x6e, 0x64, 0x12, + 0x09, 0x0a, 0x05, 0x50, 0x4c, 0x41, 0x49, 0x4e, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x4d, 0x41, + 0x52, 0x4b, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x01, 0x32, 0xf2, 0x11, 0x0a, 0x08, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x4e, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x1e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x47, 0x65, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, + 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x56, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x12, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x2a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, + 0x67, 0x12, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x1a, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x74, 0x66, 0x70, 0x6c, - 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, - 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, - 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, - 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x55, 0x70, 0x67, - 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x66, 0x70, 0x6c, + 0x69, 0x67, 0x12, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7b, 0x0a, 0x1a, 0x56, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x44, 0x61, 0x74, 0x61, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x14, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, + 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x27, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, + 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x7b, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x12, + 0x2d, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, + 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, + 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x53, 0x63, + 0x68, 0x65, 0x6d, 0x61, 0x73, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x72, + 0x0a, 0x17, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x2a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, - 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x75, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, - 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x55, 0x70, 0x67, 0x72, 0x61, 0x64, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x60, 0x0a, 0x11, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x65, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x61, 0x64, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, - 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x25, - 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x36, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, - 0x13, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x12, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, - 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x26, 0x2e, 0x74, - 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, - 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, - 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, - 0x21, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x61, 0x64, - 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, - 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, - 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2e, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, - 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, - 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x12, 0x50, 0x6c, 0x61, 0x6e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x25, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x50, 0x6c, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x13, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x12, 0x26, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, + 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x79, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x66, 0x0a, 0x13, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x26, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x11, + 0x4d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x24, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x4d, 0x6f, + 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x4d, 0x6f, 0x76, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, + 0x0a, 0x0e, 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x12, 0x21, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x61, + 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, + 0x52, 0x65, 0x61, 0x64, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x8a, 0x01, 0x0a, 0x1f, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x32, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x33, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x56, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x15, 0x4f, 0x70, 0x65, 0x6e, 0x45, 0x70, 0x68, 0x65, + 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x28, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x45, 0x70, + 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x4f, 0x70, 0x65, 0x6e, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, + 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, 0x68, 0x65, 0x6d, + 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x29, 0x2e, 0x74, + 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, + 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x52, 0x65, 0x6e, 0x65, 0x77, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, + 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6f, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x45, 0x70, 0x68, 0x65, + 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x29, 0x2e, + 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x45, + 0x70, 0x68, 0x65, 0x6d, 0x65, 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x45, 0x70, 0x68, 0x65, 0x6d, 0x65, + 0x72, 0x61, 0x6c, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, + 0x2e, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, + 0x36, 0x2e, 0x47, 0x65, 0x74, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x43, 0x61, 0x6c, 0x6c, 0x46, + 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, + 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, 0x70, 0x6c, 0x75, + 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x43, 0x61, 0x6c, 0x6c, 0x46, 0x75, 0x6e, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x0c, 0x53, 0x74, + 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x74, 0x66, 0x70, + 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x66, + 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x36, 0x2e, 0x53, 0x74, 0x6f, 0x70, 0x50, 0x72, 0x6f, 0x76, + 0x69, 0x64, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x33, 0x5a, + 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, + 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x74, 0x65, 0x72, 0x72, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x66, 0x70, 0x6c, 0x75, 0x67, 0x69, + 0x6e, 0x36, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +}) var ( file_tfplugin6_proto_rawDescOnce sync.Once - file_tfplugin6_proto_rawDescData = file_tfplugin6_proto_rawDesc + file_tfplugin6_proto_rawDescData []byte ) func file_tfplugin6_proto_rawDescGZIP() []byte { file_tfplugin6_proto_rawDescOnce.Do(func() { - file_tfplugin6_proto_rawDescData = protoimpl.X.CompressGZIP(file_tfplugin6_proto_rawDescData) + file_tfplugin6_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_tfplugin6_proto_rawDesc), len(file_tfplugin6_proto_rawDesc))) }) return file_tfplugin6_proto_rawDescData } -var file_tfplugin6_proto_enumTypes = make([]protoimpl.EnumInfo, 4) -var file_tfplugin6_proto_msgTypes = make([]protoimpl.MessageInfo, 51) -var file_tfplugin6_proto_goTypes = []interface{}{ - (StringKind)(0), // 0: tfplugin6.StringKind - (Diagnostic_Severity)(0), // 1: tfplugin6.Diagnostic.Severity - (Schema_NestedBlock_NestingMode)(0), // 2: tfplugin6.Schema.NestedBlock.NestingMode - (Schema_Object_NestingMode)(0), // 3: tfplugin6.Schema.Object.NestingMode - (*DynamicValue)(nil), // 4: tfplugin6.DynamicValue - (*Diagnostic)(nil), // 5: tfplugin6.Diagnostic - (*AttributePath)(nil), // 6: tfplugin6.AttributePath - (*StopProvider)(nil), // 7: tfplugin6.StopProvider - (*RawState)(nil), // 8: tfplugin6.RawState - (*Schema)(nil), // 9: tfplugin6.Schema - (*GetProviderSchema)(nil), // 10: tfplugin6.GetProviderSchema - (*ValidateProviderConfig)(nil), // 11: tfplugin6.ValidateProviderConfig - (*UpgradeResourceState)(nil), // 12: tfplugin6.UpgradeResourceState - (*ValidateResourceConfig)(nil), // 13: tfplugin6.ValidateResourceConfig - (*ValidateDataResourceConfig)(nil), // 14: tfplugin6.ValidateDataResourceConfig - (*ConfigureProvider)(nil), // 15: tfplugin6.ConfigureProvider - (*ReadResource)(nil), // 16: tfplugin6.ReadResource - (*PlanResourceChange)(nil), // 17: tfplugin6.PlanResourceChange - (*ApplyResourceChange)(nil), // 18: tfplugin6.ApplyResourceChange - (*ImportResourceState)(nil), // 19: tfplugin6.ImportResourceState - (*ReadDataSource)(nil), // 20: tfplugin6.ReadDataSource - (*AttributePath_Step)(nil), // 21: tfplugin6.AttributePath.Step - (*StopProvider_Request)(nil), // 22: tfplugin6.StopProvider.Request - (*StopProvider_Response)(nil), // 23: tfplugin6.StopProvider.Response - nil, // 24: tfplugin6.RawState.FlatmapEntry - (*Schema_Block)(nil), // 25: tfplugin6.Schema.Block - (*Schema_Attribute)(nil), // 26: tfplugin6.Schema.Attribute - (*Schema_NestedBlock)(nil), // 27: tfplugin6.Schema.NestedBlock - (*Schema_Object)(nil), // 28: tfplugin6.Schema.Object - (*GetProviderSchema_Request)(nil), // 29: tfplugin6.GetProviderSchema.Request - (*GetProviderSchema_Response)(nil), // 30: tfplugin6.GetProviderSchema.Response - (*GetProviderSchema_ServerCapabilities)(nil), // 31: tfplugin6.GetProviderSchema.ServerCapabilities - nil, // 32: tfplugin6.GetProviderSchema.Response.ResourceSchemasEntry - nil, // 33: tfplugin6.GetProviderSchema.Response.DataSourceSchemasEntry - (*ValidateProviderConfig_Request)(nil), // 34: tfplugin6.ValidateProviderConfig.Request - (*ValidateProviderConfig_Response)(nil), // 35: tfplugin6.ValidateProviderConfig.Response - (*UpgradeResourceState_Request)(nil), // 36: tfplugin6.UpgradeResourceState.Request - (*UpgradeResourceState_Response)(nil), // 37: tfplugin6.UpgradeResourceState.Response - (*ValidateResourceConfig_Request)(nil), // 38: tfplugin6.ValidateResourceConfig.Request - (*ValidateResourceConfig_Response)(nil), // 39: tfplugin6.ValidateResourceConfig.Response - (*ValidateDataResourceConfig_Request)(nil), // 40: tfplugin6.ValidateDataResourceConfig.Request - (*ValidateDataResourceConfig_Response)(nil), // 41: tfplugin6.ValidateDataResourceConfig.Response - (*ConfigureProvider_Request)(nil), // 42: tfplugin6.ConfigureProvider.Request - (*ConfigureProvider_Response)(nil), // 43: tfplugin6.ConfigureProvider.Response - (*ReadResource_Request)(nil), // 44: tfplugin6.ReadResource.Request - (*ReadResource_Response)(nil), // 45: tfplugin6.ReadResource.Response - (*PlanResourceChange_Request)(nil), // 46: tfplugin6.PlanResourceChange.Request - (*PlanResourceChange_Response)(nil), // 47: tfplugin6.PlanResourceChange.Response - (*ApplyResourceChange_Request)(nil), // 48: tfplugin6.ApplyResourceChange.Request - (*ApplyResourceChange_Response)(nil), // 49: tfplugin6.ApplyResourceChange.Response - (*ImportResourceState_Request)(nil), // 50: tfplugin6.ImportResourceState.Request - (*ImportResourceState_ImportedResource)(nil), // 51: tfplugin6.ImportResourceState.ImportedResource - (*ImportResourceState_Response)(nil), // 52: tfplugin6.ImportResourceState.Response - (*ReadDataSource_Request)(nil), // 53: tfplugin6.ReadDataSource.Request - (*ReadDataSource_Response)(nil), // 54: tfplugin6.ReadDataSource.Response +var file_tfplugin6_proto_enumTypes = make([]protoimpl.EnumInfo, 5) +var file_tfplugin6_proto_msgTypes = make([]protoimpl.MessageInfo, 98) +var file_tfplugin6_proto_goTypes = []any{ + (StringKind)(0), // 0: tfplugin6.StringKind + (Diagnostic_Severity)(0), // 1: tfplugin6.Diagnostic.Severity + (Schema_NestedBlock_NestingMode)(0), // 2: tfplugin6.Schema.NestedBlock.NestingMode + (Schema_Object_NestingMode)(0), // 3: tfplugin6.Schema.Object.NestingMode + (Deferred_Reason)(0), // 4: tfplugin6.Deferred.Reason + (*DynamicValue)(nil), // 5: tfplugin6.DynamicValue + (*Diagnostic)(nil), // 6: tfplugin6.Diagnostic + (*FunctionError)(nil), // 7: tfplugin6.FunctionError + (*AttributePath)(nil), // 8: tfplugin6.AttributePath + (*StopProvider)(nil), // 9: tfplugin6.StopProvider + (*RawState)(nil), // 10: tfplugin6.RawState + (*ResourceIdentitySchema)(nil), // 11: tfplugin6.ResourceIdentitySchema + (*ResourceIdentityData)(nil), // 12: tfplugin6.ResourceIdentityData + (*Schema)(nil), // 13: tfplugin6.Schema + (*Function)(nil), // 14: tfplugin6.Function + (*ServerCapabilities)(nil), // 15: tfplugin6.ServerCapabilities + (*ClientCapabilities)(nil), // 16: tfplugin6.ClientCapabilities + (*Deferred)(nil), // 17: tfplugin6.Deferred + (*GetMetadata)(nil), // 18: tfplugin6.GetMetadata + (*GetProviderSchema)(nil), // 19: tfplugin6.GetProviderSchema + (*ValidateProviderConfig)(nil), // 20: tfplugin6.ValidateProviderConfig + (*UpgradeResourceState)(nil), // 21: tfplugin6.UpgradeResourceState + (*GetResourceIdentitySchemas)(nil), // 22: tfplugin6.GetResourceIdentitySchemas + (*UpgradeResourceIdentity)(nil), // 23: tfplugin6.UpgradeResourceIdentity + (*ValidateResourceConfig)(nil), // 24: tfplugin6.ValidateResourceConfig + (*ValidateDataResourceConfig)(nil), // 25: tfplugin6.ValidateDataResourceConfig + (*ValidateEphemeralResourceConfig)(nil), // 26: tfplugin6.ValidateEphemeralResourceConfig + (*ConfigureProvider)(nil), // 27: tfplugin6.ConfigureProvider + (*ReadResource)(nil), // 28: tfplugin6.ReadResource + (*PlanResourceChange)(nil), // 29: tfplugin6.PlanResourceChange + (*ApplyResourceChange)(nil), // 30: tfplugin6.ApplyResourceChange + (*ImportResourceState)(nil), // 31: tfplugin6.ImportResourceState + (*MoveResourceState)(nil), // 32: tfplugin6.MoveResourceState + (*ReadDataSource)(nil), // 33: tfplugin6.ReadDataSource + (*OpenEphemeralResource)(nil), // 34: tfplugin6.OpenEphemeralResource + (*RenewEphemeralResource)(nil), // 35: tfplugin6.RenewEphemeralResource + (*CloseEphemeralResource)(nil), // 36: tfplugin6.CloseEphemeralResource + (*GetFunctions)(nil), // 37: tfplugin6.GetFunctions + (*CallFunction)(nil), // 38: tfplugin6.CallFunction + (*AttributePath_Step)(nil), // 39: tfplugin6.AttributePath.Step + (*StopProvider_Request)(nil), // 40: tfplugin6.StopProvider.Request + (*StopProvider_Response)(nil), // 41: tfplugin6.StopProvider.Response + nil, // 42: tfplugin6.RawState.FlatmapEntry + (*ResourceIdentitySchema_IdentityAttribute)(nil), // 43: tfplugin6.ResourceIdentitySchema.IdentityAttribute + (*Schema_Block)(nil), // 44: tfplugin6.Schema.Block + (*Schema_Attribute)(nil), // 45: tfplugin6.Schema.Attribute + (*Schema_NestedBlock)(nil), // 46: tfplugin6.Schema.NestedBlock + (*Schema_Object)(nil), // 47: tfplugin6.Schema.Object + (*Function_Parameter)(nil), // 48: tfplugin6.Function.Parameter + (*Function_Return)(nil), // 49: tfplugin6.Function.Return + (*GetMetadata_Request)(nil), // 50: tfplugin6.GetMetadata.Request + (*GetMetadata_Response)(nil), // 51: tfplugin6.GetMetadata.Response + (*GetMetadata_EphemeralMetadata)(nil), // 52: tfplugin6.GetMetadata.EphemeralMetadata + (*GetMetadata_FunctionMetadata)(nil), // 53: tfplugin6.GetMetadata.FunctionMetadata + (*GetMetadata_DataSourceMetadata)(nil), // 54: tfplugin6.GetMetadata.DataSourceMetadata + (*GetMetadata_ResourceMetadata)(nil), // 55: tfplugin6.GetMetadata.ResourceMetadata + (*GetProviderSchema_Request)(nil), // 56: tfplugin6.GetProviderSchema.Request + (*GetProviderSchema_Response)(nil), // 57: tfplugin6.GetProviderSchema.Response + nil, // 58: tfplugin6.GetProviderSchema.Response.ResourceSchemasEntry + nil, // 59: tfplugin6.GetProviderSchema.Response.DataSourceSchemasEntry + nil, // 60: tfplugin6.GetProviderSchema.Response.FunctionsEntry + nil, // 61: tfplugin6.GetProviderSchema.Response.EphemeralResourceSchemasEntry + (*ValidateProviderConfig_Request)(nil), // 62: tfplugin6.ValidateProviderConfig.Request + (*ValidateProviderConfig_Response)(nil), // 63: tfplugin6.ValidateProviderConfig.Response + (*UpgradeResourceState_Request)(nil), // 64: tfplugin6.UpgradeResourceState.Request + (*UpgradeResourceState_Response)(nil), // 65: tfplugin6.UpgradeResourceState.Response + (*GetResourceIdentitySchemas_Request)(nil), // 66: tfplugin6.GetResourceIdentitySchemas.Request + (*GetResourceIdentitySchemas_Response)(nil), // 67: tfplugin6.GetResourceIdentitySchemas.Response + nil, // 68: tfplugin6.GetResourceIdentitySchemas.Response.IdentitySchemasEntry + (*UpgradeResourceIdentity_Request)(nil), // 69: tfplugin6.UpgradeResourceIdentity.Request + (*UpgradeResourceIdentity_Response)(nil), // 70: tfplugin6.UpgradeResourceIdentity.Response + (*ValidateResourceConfig_Request)(nil), // 71: tfplugin6.ValidateResourceConfig.Request + (*ValidateResourceConfig_Response)(nil), // 72: tfplugin6.ValidateResourceConfig.Response + (*ValidateDataResourceConfig_Request)(nil), // 73: tfplugin6.ValidateDataResourceConfig.Request + (*ValidateDataResourceConfig_Response)(nil), // 74: tfplugin6.ValidateDataResourceConfig.Response + (*ValidateEphemeralResourceConfig_Request)(nil), // 75: tfplugin6.ValidateEphemeralResourceConfig.Request + (*ValidateEphemeralResourceConfig_Response)(nil), // 76: tfplugin6.ValidateEphemeralResourceConfig.Response + (*ConfigureProvider_Request)(nil), // 77: tfplugin6.ConfigureProvider.Request + (*ConfigureProvider_Response)(nil), // 78: tfplugin6.ConfigureProvider.Response + (*ReadResource_Request)(nil), // 79: tfplugin6.ReadResource.Request + (*ReadResource_Response)(nil), // 80: tfplugin6.ReadResource.Response + (*PlanResourceChange_Request)(nil), // 81: tfplugin6.PlanResourceChange.Request + (*PlanResourceChange_Response)(nil), // 82: tfplugin6.PlanResourceChange.Response + (*ApplyResourceChange_Request)(nil), // 83: tfplugin6.ApplyResourceChange.Request + (*ApplyResourceChange_Response)(nil), // 84: tfplugin6.ApplyResourceChange.Response + (*ImportResourceState_Request)(nil), // 85: tfplugin6.ImportResourceState.Request + (*ImportResourceState_ImportedResource)(nil), // 86: tfplugin6.ImportResourceState.ImportedResource + (*ImportResourceState_Response)(nil), // 87: tfplugin6.ImportResourceState.Response + (*MoveResourceState_Request)(nil), // 88: tfplugin6.MoveResourceState.Request + (*MoveResourceState_Response)(nil), // 89: tfplugin6.MoveResourceState.Response + (*ReadDataSource_Request)(nil), // 90: tfplugin6.ReadDataSource.Request + (*ReadDataSource_Response)(nil), // 91: tfplugin6.ReadDataSource.Response + (*OpenEphemeralResource_Request)(nil), // 92: tfplugin6.OpenEphemeralResource.Request + (*OpenEphemeralResource_Response)(nil), // 93: tfplugin6.OpenEphemeralResource.Response + (*RenewEphemeralResource_Request)(nil), // 94: tfplugin6.RenewEphemeralResource.Request + (*RenewEphemeralResource_Response)(nil), // 95: tfplugin6.RenewEphemeralResource.Response + (*CloseEphemeralResource_Request)(nil), // 96: tfplugin6.CloseEphemeralResource.Request + (*CloseEphemeralResource_Response)(nil), // 97: tfplugin6.CloseEphemeralResource.Response + (*GetFunctions_Request)(nil), // 98: tfplugin6.GetFunctions.Request + (*GetFunctions_Response)(nil), // 99: tfplugin6.GetFunctions.Response + nil, // 100: tfplugin6.GetFunctions.Response.FunctionsEntry + (*CallFunction_Request)(nil), // 101: tfplugin6.CallFunction.Request + (*CallFunction_Response)(nil), // 102: tfplugin6.CallFunction.Response + (*timestamppb.Timestamp)(nil), // 103: google.protobuf.Timestamp } var file_tfplugin6_proto_depIdxs = []int32{ - 1, // 0: tfplugin6.Diagnostic.severity:type_name -> tfplugin6.Diagnostic.Severity - 6, // 1: tfplugin6.Diagnostic.attribute:type_name -> tfplugin6.AttributePath - 21, // 2: tfplugin6.AttributePath.steps:type_name -> tfplugin6.AttributePath.Step - 24, // 3: tfplugin6.RawState.flatmap:type_name -> tfplugin6.RawState.FlatmapEntry - 25, // 4: tfplugin6.Schema.block:type_name -> tfplugin6.Schema.Block - 26, // 5: tfplugin6.Schema.Block.attributes:type_name -> tfplugin6.Schema.Attribute - 27, // 6: tfplugin6.Schema.Block.block_types:type_name -> tfplugin6.Schema.NestedBlock - 0, // 7: tfplugin6.Schema.Block.description_kind:type_name -> tfplugin6.StringKind - 28, // 8: tfplugin6.Schema.Attribute.nested_type:type_name -> tfplugin6.Schema.Object - 0, // 9: tfplugin6.Schema.Attribute.description_kind:type_name -> tfplugin6.StringKind - 25, // 10: tfplugin6.Schema.NestedBlock.block:type_name -> tfplugin6.Schema.Block - 2, // 11: tfplugin6.Schema.NestedBlock.nesting:type_name -> tfplugin6.Schema.NestedBlock.NestingMode - 26, // 12: tfplugin6.Schema.Object.attributes:type_name -> tfplugin6.Schema.Attribute - 3, // 13: tfplugin6.Schema.Object.nesting:type_name -> tfplugin6.Schema.Object.NestingMode - 9, // 14: tfplugin6.GetProviderSchema.Response.provider:type_name -> tfplugin6.Schema - 32, // 15: tfplugin6.GetProviderSchema.Response.resource_schemas:type_name -> tfplugin6.GetProviderSchema.Response.ResourceSchemasEntry - 33, // 16: tfplugin6.GetProviderSchema.Response.data_source_schemas:type_name -> tfplugin6.GetProviderSchema.Response.DataSourceSchemasEntry - 5, // 17: tfplugin6.GetProviderSchema.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 9, // 18: tfplugin6.GetProviderSchema.Response.provider_meta:type_name -> tfplugin6.Schema - 31, // 19: tfplugin6.GetProviderSchema.Response.server_capabilities:type_name -> tfplugin6.GetProviderSchema.ServerCapabilities - 9, // 20: tfplugin6.GetProviderSchema.Response.ResourceSchemasEntry.value:type_name -> tfplugin6.Schema - 9, // 21: tfplugin6.GetProviderSchema.Response.DataSourceSchemasEntry.value:type_name -> tfplugin6.Schema - 4, // 22: tfplugin6.ValidateProviderConfig.Request.config:type_name -> tfplugin6.DynamicValue - 5, // 23: tfplugin6.ValidateProviderConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 8, // 24: tfplugin6.UpgradeResourceState.Request.raw_state:type_name -> tfplugin6.RawState - 4, // 25: tfplugin6.UpgradeResourceState.Response.upgraded_state:type_name -> tfplugin6.DynamicValue - 5, // 26: tfplugin6.UpgradeResourceState.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 27: tfplugin6.ValidateResourceConfig.Request.config:type_name -> tfplugin6.DynamicValue - 5, // 28: tfplugin6.ValidateResourceConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 29: tfplugin6.ValidateDataResourceConfig.Request.config:type_name -> tfplugin6.DynamicValue - 5, // 30: tfplugin6.ValidateDataResourceConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 31: tfplugin6.ConfigureProvider.Request.config:type_name -> tfplugin6.DynamicValue - 5, // 32: tfplugin6.ConfigureProvider.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 33: tfplugin6.ReadResource.Request.current_state:type_name -> tfplugin6.DynamicValue - 4, // 34: tfplugin6.ReadResource.Request.provider_meta:type_name -> tfplugin6.DynamicValue - 4, // 35: tfplugin6.ReadResource.Response.new_state:type_name -> tfplugin6.DynamicValue - 5, // 36: tfplugin6.ReadResource.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 37: tfplugin6.PlanResourceChange.Request.prior_state:type_name -> tfplugin6.DynamicValue - 4, // 38: tfplugin6.PlanResourceChange.Request.proposed_new_state:type_name -> tfplugin6.DynamicValue - 4, // 39: tfplugin6.PlanResourceChange.Request.config:type_name -> tfplugin6.DynamicValue - 4, // 40: tfplugin6.PlanResourceChange.Request.provider_meta:type_name -> tfplugin6.DynamicValue - 4, // 41: tfplugin6.PlanResourceChange.Response.planned_state:type_name -> tfplugin6.DynamicValue - 6, // 42: tfplugin6.PlanResourceChange.Response.requires_replace:type_name -> tfplugin6.AttributePath - 5, // 43: tfplugin6.PlanResourceChange.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 44: tfplugin6.ApplyResourceChange.Request.prior_state:type_name -> tfplugin6.DynamicValue - 4, // 45: tfplugin6.ApplyResourceChange.Request.planned_state:type_name -> tfplugin6.DynamicValue - 4, // 46: tfplugin6.ApplyResourceChange.Request.config:type_name -> tfplugin6.DynamicValue - 4, // 47: tfplugin6.ApplyResourceChange.Request.provider_meta:type_name -> tfplugin6.DynamicValue - 4, // 48: tfplugin6.ApplyResourceChange.Response.new_state:type_name -> tfplugin6.DynamicValue - 5, // 49: tfplugin6.ApplyResourceChange.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 50: tfplugin6.ImportResourceState.ImportedResource.state:type_name -> tfplugin6.DynamicValue - 51, // 51: tfplugin6.ImportResourceState.Response.imported_resources:type_name -> tfplugin6.ImportResourceState.ImportedResource - 5, // 52: tfplugin6.ImportResourceState.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 4, // 53: tfplugin6.ReadDataSource.Request.config:type_name -> tfplugin6.DynamicValue - 4, // 54: tfplugin6.ReadDataSource.Request.provider_meta:type_name -> tfplugin6.DynamicValue - 4, // 55: tfplugin6.ReadDataSource.Response.state:type_name -> tfplugin6.DynamicValue - 5, // 56: tfplugin6.ReadDataSource.Response.diagnostics:type_name -> tfplugin6.Diagnostic - 29, // 57: tfplugin6.Provider.GetProviderSchema:input_type -> tfplugin6.GetProviderSchema.Request - 34, // 58: tfplugin6.Provider.ValidateProviderConfig:input_type -> tfplugin6.ValidateProviderConfig.Request - 38, // 59: tfplugin6.Provider.ValidateResourceConfig:input_type -> tfplugin6.ValidateResourceConfig.Request - 40, // 60: tfplugin6.Provider.ValidateDataResourceConfig:input_type -> tfplugin6.ValidateDataResourceConfig.Request - 36, // 61: tfplugin6.Provider.UpgradeResourceState:input_type -> tfplugin6.UpgradeResourceState.Request - 42, // 62: tfplugin6.Provider.ConfigureProvider:input_type -> tfplugin6.ConfigureProvider.Request - 44, // 63: tfplugin6.Provider.ReadResource:input_type -> tfplugin6.ReadResource.Request - 46, // 64: tfplugin6.Provider.PlanResourceChange:input_type -> tfplugin6.PlanResourceChange.Request - 48, // 65: tfplugin6.Provider.ApplyResourceChange:input_type -> tfplugin6.ApplyResourceChange.Request - 50, // 66: tfplugin6.Provider.ImportResourceState:input_type -> tfplugin6.ImportResourceState.Request - 53, // 67: tfplugin6.Provider.ReadDataSource:input_type -> tfplugin6.ReadDataSource.Request - 22, // 68: tfplugin6.Provider.StopProvider:input_type -> tfplugin6.StopProvider.Request - 30, // 69: tfplugin6.Provider.GetProviderSchema:output_type -> tfplugin6.GetProviderSchema.Response - 35, // 70: tfplugin6.Provider.ValidateProviderConfig:output_type -> tfplugin6.ValidateProviderConfig.Response - 39, // 71: tfplugin6.Provider.ValidateResourceConfig:output_type -> tfplugin6.ValidateResourceConfig.Response - 41, // 72: tfplugin6.Provider.ValidateDataResourceConfig:output_type -> tfplugin6.ValidateDataResourceConfig.Response - 37, // 73: tfplugin6.Provider.UpgradeResourceState:output_type -> tfplugin6.UpgradeResourceState.Response - 43, // 74: tfplugin6.Provider.ConfigureProvider:output_type -> tfplugin6.ConfigureProvider.Response - 45, // 75: tfplugin6.Provider.ReadResource:output_type -> tfplugin6.ReadResource.Response - 47, // 76: tfplugin6.Provider.PlanResourceChange:output_type -> tfplugin6.PlanResourceChange.Response - 49, // 77: tfplugin6.Provider.ApplyResourceChange:output_type -> tfplugin6.ApplyResourceChange.Response - 52, // 78: tfplugin6.Provider.ImportResourceState:output_type -> tfplugin6.ImportResourceState.Response - 54, // 79: tfplugin6.Provider.ReadDataSource:output_type -> tfplugin6.ReadDataSource.Response - 23, // 80: tfplugin6.Provider.StopProvider:output_type -> tfplugin6.StopProvider.Response - 69, // [69:81] is the sub-list for method output_type - 57, // [57:69] is the sub-list for method input_type - 57, // [57:57] is the sub-list for extension type_name - 57, // [57:57] is the sub-list for extension extendee - 0, // [0:57] is the sub-list for field type_name + 1, // 0: tfplugin6.Diagnostic.severity:type_name -> tfplugin6.Diagnostic.Severity + 8, // 1: tfplugin6.Diagnostic.attribute:type_name -> tfplugin6.AttributePath + 39, // 2: tfplugin6.AttributePath.steps:type_name -> tfplugin6.AttributePath.Step + 42, // 3: tfplugin6.RawState.flatmap:type_name -> tfplugin6.RawState.FlatmapEntry + 43, // 4: tfplugin6.ResourceIdentitySchema.identity_attributes:type_name -> tfplugin6.ResourceIdentitySchema.IdentityAttribute + 5, // 5: tfplugin6.ResourceIdentityData.identity_data:type_name -> tfplugin6.DynamicValue + 44, // 6: tfplugin6.Schema.block:type_name -> tfplugin6.Schema.Block + 48, // 7: tfplugin6.Function.parameters:type_name -> tfplugin6.Function.Parameter + 48, // 8: tfplugin6.Function.variadic_parameter:type_name -> tfplugin6.Function.Parameter + 49, // 9: tfplugin6.Function.return:type_name -> tfplugin6.Function.Return + 0, // 10: tfplugin6.Function.description_kind:type_name -> tfplugin6.StringKind + 4, // 11: tfplugin6.Deferred.reason:type_name -> tfplugin6.Deferred.Reason + 45, // 12: tfplugin6.Schema.Block.attributes:type_name -> tfplugin6.Schema.Attribute + 46, // 13: tfplugin6.Schema.Block.block_types:type_name -> tfplugin6.Schema.NestedBlock + 0, // 14: tfplugin6.Schema.Block.description_kind:type_name -> tfplugin6.StringKind + 47, // 15: tfplugin6.Schema.Attribute.nested_type:type_name -> tfplugin6.Schema.Object + 0, // 16: tfplugin6.Schema.Attribute.description_kind:type_name -> tfplugin6.StringKind + 44, // 17: tfplugin6.Schema.NestedBlock.block:type_name -> tfplugin6.Schema.Block + 2, // 18: tfplugin6.Schema.NestedBlock.nesting:type_name -> tfplugin6.Schema.NestedBlock.NestingMode + 45, // 19: tfplugin6.Schema.Object.attributes:type_name -> tfplugin6.Schema.Attribute + 3, // 20: tfplugin6.Schema.Object.nesting:type_name -> tfplugin6.Schema.Object.NestingMode + 0, // 21: tfplugin6.Function.Parameter.description_kind:type_name -> tfplugin6.StringKind + 15, // 22: tfplugin6.GetMetadata.Response.server_capabilities:type_name -> tfplugin6.ServerCapabilities + 6, // 23: tfplugin6.GetMetadata.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 54, // 24: tfplugin6.GetMetadata.Response.data_sources:type_name -> tfplugin6.GetMetadata.DataSourceMetadata + 55, // 25: tfplugin6.GetMetadata.Response.resources:type_name -> tfplugin6.GetMetadata.ResourceMetadata + 53, // 26: tfplugin6.GetMetadata.Response.functions:type_name -> tfplugin6.GetMetadata.FunctionMetadata + 52, // 27: tfplugin6.GetMetadata.Response.ephemeral_resources:type_name -> tfplugin6.GetMetadata.EphemeralMetadata + 13, // 28: tfplugin6.GetProviderSchema.Response.provider:type_name -> tfplugin6.Schema + 58, // 29: tfplugin6.GetProviderSchema.Response.resource_schemas:type_name -> tfplugin6.GetProviderSchema.Response.ResourceSchemasEntry + 59, // 30: tfplugin6.GetProviderSchema.Response.data_source_schemas:type_name -> tfplugin6.GetProviderSchema.Response.DataSourceSchemasEntry + 60, // 31: tfplugin6.GetProviderSchema.Response.functions:type_name -> tfplugin6.GetProviderSchema.Response.FunctionsEntry + 61, // 32: tfplugin6.GetProviderSchema.Response.ephemeral_resource_schemas:type_name -> tfplugin6.GetProviderSchema.Response.EphemeralResourceSchemasEntry + 6, // 33: tfplugin6.GetProviderSchema.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 13, // 34: tfplugin6.GetProviderSchema.Response.provider_meta:type_name -> tfplugin6.Schema + 15, // 35: tfplugin6.GetProviderSchema.Response.server_capabilities:type_name -> tfplugin6.ServerCapabilities + 13, // 36: tfplugin6.GetProviderSchema.Response.ResourceSchemasEntry.value:type_name -> tfplugin6.Schema + 13, // 37: tfplugin6.GetProviderSchema.Response.DataSourceSchemasEntry.value:type_name -> tfplugin6.Schema + 14, // 38: tfplugin6.GetProviderSchema.Response.FunctionsEntry.value:type_name -> tfplugin6.Function + 13, // 39: tfplugin6.GetProviderSchema.Response.EphemeralResourceSchemasEntry.value:type_name -> tfplugin6.Schema + 5, // 40: tfplugin6.ValidateProviderConfig.Request.config:type_name -> tfplugin6.DynamicValue + 6, // 41: tfplugin6.ValidateProviderConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 10, // 42: tfplugin6.UpgradeResourceState.Request.raw_state:type_name -> tfplugin6.RawState + 5, // 43: tfplugin6.UpgradeResourceState.Response.upgraded_state:type_name -> tfplugin6.DynamicValue + 6, // 44: tfplugin6.UpgradeResourceState.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 68, // 45: tfplugin6.GetResourceIdentitySchemas.Response.identity_schemas:type_name -> tfplugin6.GetResourceIdentitySchemas.Response.IdentitySchemasEntry + 6, // 46: tfplugin6.GetResourceIdentitySchemas.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 11, // 47: tfplugin6.GetResourceIdentitySchemas.Response.IdentitySchemasEntry.value:type_name -> tfplugin6.ResourceIdentitySchema + 10, // 48: tfplugin6.UpgradeResourceIdentity.Request.raw_identity:type_name -> tfplugin6.RawState + 12, // 49: tfplugin6.UpgradeResourceIdentity.Response.upgraded_identity:type_name -> tfplugin6.ResourceIdentityData + 6, // 50: tfplugin6.UpgradeResourceIdentity.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 5, // 51: tfplugin6.ValidateResourceConfig.Request.config:type_name -> tfplugin6.DynamicValue + 16, // 52: tfplugin6.ValidateResourceConfig.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 6, // 53: tfplugin6.ValidateResourceConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 5, // 54: tfplugin6.ValidateDataResourceConfig.Request.config:type_name -> tfplugin6.DynamicValue + 6, // 55: tfplugin6.ValidateDataResourceConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 5, // 56: tfplugin6.ValidateEphemeralResourceConfig.Request.config:type_name -> tfplugin6.DynamicValue + 6, // 57: tfplugin6.ValidateEphemeralResourceConfig.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 5, // 58: tfplugin6.ConfigureProvider.Request.config:type_name -> tfplugin6.DynamicValue + 16, // 59: tfplugin6.ConfigureProvider.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 6, // 60: tfplugin6.ConfigureProvider.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 5, // 61: tfplugin6.ReadResource.Request.current_state:type_name -> tfplugin6.DynamicValue + 5, // 62: tfplugin6.ReadResource.Request.provider_meta:type_name -> tfplugin6.DynamicValue + 16, // 63: tfplugin6.ReadResource.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 12, // 64: tfplugin6.ReadResource.Request.current_identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 65: tfplugin6.ReadResource.Response.new_state:type_name -> tfplugin6.DynamicValue + 6, // 66: tfplugin6.ReadResource.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 17, // 67: tfplugin6.ReadResource.Response.deferred:type_name -> tfplugin6.Deferred + 12, // 68: tfplugin6.ReadResource.Response.new_identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 69: tfplugin6.PlanResourceChange.Request.prior_state:type_name -> tfplugin6.DynamicValue + 5, // 70: tfplugin6.PlanResourceChange.Request.proposed_new_state:type_name -> tfplugin6.DynamicValue + 5, // 71: tfplugin6.PlanResourceChange.Request.config:type_name -> tfplugin6.DynamicValue + 5, // 72: tfplugin6.PlanResourceChange.Request.provider_meta:type_name -> tfplugin6.DynamicValue + 16, // 73: tfplugin6.PlanResourceChange.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 12, // 74: tfplugin6.PlanResourceChange.Request.prior_identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 75: tfplugin6.PlanResourceChange.Response.planned_state:type_name -> tfplugin6.DynamicValue + 8, // 76: tfplugin6.PlanResourceChange.Response.requires_replace:type_name -> tfplugin6.AttributePath + 6, // 77: tfplugin6.PlanResourceChange.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 17, // 78: tfplugin6.PlanResourceChange.Response.deferred:type_name -> tfplugin6.Deferred + 12, // 79: tfplugin6.PlanResourceChange.Response.planned_identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 80: tfplugin6.ApplyResourceChange.Request.prior_state:type_name -> tfplugin6.DynamicValue + 5, // 81: tfplugin6.ApplyResourceChange.Request.planned_state:type_name -> tfplugin6.DynamicValue + 5, // 82: tfplugin6.ApplyResourceChange.Request.config:type_name -> tfplugin6.DynamicValue + 5, // 83: tfplugin6.ApplyResourceChange.Request.provider_meta:type_name -> tfplugin6.DynamicValue + 12, // 84: tfplugin6.ApplyResourceChange.Request.planned_identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 85: tfplugin6.ApplyResourceChange.Response.new_state:type_name -> tfplugin6.DynamicValue + 6, // 86: tfplugin6.ApplyResourceChange.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 12, // 87: tfplugin6.ApplyResourceChange.Response.new_identity:type_name -> tfplugin6.ResourceIdentityData + 16, // 88: tfplugin6.ImportResourceState.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 12, // 89: tfplugin6.ImportResourceState.Request.identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 90: tfplugin6.ImportResourceState.ImportedResource.state:type_name -> tfplugin6.DynamicValue + 12, // 91: tfplugin6.ImportResourceState.ImportedResource.identity:type_name -> tfplugin6.ResourceIdentityData + 86, // 92: tfplugin6.ImportResourceState.Response.imported_resources:type_name -> tfplugin6.ImportResourceState.ImportedResource + 6, // 93: tfplugin6.ImportResourceState.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 17, // 94: tfplugin6.ImportResourceState.Response.deferred:type_name -> tfplugin6.Deferred + 10, // 95: tfplugin6.MoveResourceState.Request.source_state:type_name -> tfplugin6.RawState + 10, // 96: tfplugin6.MoveResourceState.Request.source_identity:type_name -> tfplugin6.RawState + 5, // 97: tfplugin6.MoveResourceState.Response.target_state:type_name -> tfplugin6.DynamicValue + 6, // 98: tfplugin6.MoveResourceState.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 12, // 99: tfplugin6.MoveResourceState.Response.target_identity:type_name -> tfplugin6.ResourceIdentityData + 5, // 100: tfplugin6.ReadDataSource.Request.config:type_name -> tfplugin6.DynamicValue + 5, // 101: tfplugin6.ReadDataSource.Request.provider_meta:type_name -> tfplugin6.DynamicValue + 16, // 102: tfplugin6.ReadDataSource.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 5, // 103: tfplugin6.ReadDataSource.Response.state:type_name -> tfplugin6.DynamicValue + 6, // 104: tfplugin6.ReadDataSource.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 17, // 105: tfplugin6.ReadDataSource.Response.deferred:type_name -> tfplugin6.Deferred + 5, // 106: tfplugin6.OpenEphemeralResource.Request.config:type_name -> tfplugin6.DynamicValue + 16, // 107: tfplugin6.OpenEphemeralResource.Request.client_capabilities:type_name -> tfplugin6.ClientCapabilities + 6, // 108: tfplugin6.OpenEphemeralResource.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 103, // 109: tfplugin6.OpenEphemeralResource.Response.renew_at:type_name -> google.protobuf.Timestamp + 5, // 110: tfplugin6.OpenEphemeralResource.Response.result:type_name -> tfplugin6.DynamicValue + 17, // 111: tfplugin6.OpenEphemeralResource.Response.deferred:type_name -> tfplugin6.Deferred + 6, // 112: tfplugin6.RenewEphemeralResource.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 103, // 113: tfplugin6.RenewEphemeralResource.Response.renew_at:type_name -> google.protobuf.Timestamp + 6, // 114: tfplugin6.CloseEphemeralResource.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 100, // 115: tfplugin6.GetFunctions.Response.functions:type_name -> tfplugin6.GetFunctions.Response.FunctionsEntry + 6, // 116: tfplugin6.GetFunctions.Response.diagnostics:type_name -> tfplugin6.Diagnostic + 14, // 117: tfplugin6.GetFunctions.Response.FunctionsEntry.value:type_name -> tfplugin6.Function + 5, // 118: tfplugin6.CallFunction.Request.arguments:type_name -> tfplugin6.DynamicValue + 5, // 119: tfplugin6.CallFunction.Response.result:type_name -> tfplugin6.DynamicValue + 7, // 120: tfplugin6.CallFunction.Response.error:type_name -> tfplugin6.FunctionError + 50, // 121: tfplugin6.Provider.GetMetadata:input_type -> tfplugin6.GetMetadata.Request + 56, // 122: tfplugin6.Provider.GetProviderSchema:input_type -> tfplugin6.GetProviderSchema.Request + 62, // 123: tfplugin6.Provider.ValidateProviderConfig:input_type -> tfplugin6.ValidateProviderConfig.Request + 71, // 124: tfplugin6.Provider.ValidateResourceConfig:input_type -> tfplugin6.ValidateResourceConfig.Request + 73, // 125: tfplugin6.Provider.ValidateDataResourceConfig:input_type -> tfplugin6.ValidateDataResourceConfig.Request + 64, // 126: tfplugin6.Provider.UpgradeResourceState:input_type -> tfplugin6.UpgradeResourceState.Request + 66, // 127: tfplugin6.Provider.GetResourceIdentitySchemas:input_type -> tfplugin6.GetResourceIdentitySchemas.Request + 69, // 128: tfplugin6.Provider.UpgradeResourceIdentity:input_type -> tfplugin6.UpgradeResourceIdentity.Request + 77, // 129: tfplugin6.Provider.ConfigureProvider:input_type -> tfplugin6.ConfigureProvider.Request + 79, // 130: tfplugin6.Provider.ReadResource:input_type -> tfplugin6.ReadResource.Request + 81, // 131: tfplugin6.Provider.PlanResourceChange:input_type -> tfplugin6.PlanResourceChange.Request + 83, // 132: tfplugin6.Provider.ApplyResourceChange:input_type -> tfplugin6.ApplyResourceChange.Request + 85, // 133: tfplugin6.Provider.ImportResourceState:input_type -> tfplugin6.ImportResourceState.Request + 88, // 134: tfplugin6.Provider.MoveResourceState:input_type -> tfplugin6.MoveResourceState.Request + 90, // 135: tfplugin6.Provider.ReadDataSource:input_type -> tfplugin6.ReadDataSource.Request + 75, // 136: tfplugin6.Provider.ValidateEphemeralResourceConfig:input_type -> tfplugin6.ValidateEphemeralResourceConfig.Request + 92, // 137: tfplugin6.Provider.OpenEphemeralResource:input_type -> tfplugin6.OpenEphemeralResource.Request + 94, // 138: tfplugin6.Provider.RenewEphemeralResource:input_type -> tfplugin6.RenewEphemeralResource.Request + 96, // 139: tfplugin6.Provider.CloseEphemeralResource:input_type -> tfplugin6.CloseEphemeralResource.Request + 98, // 140: tfplugin6.Provider.GetFunctions:input_type -> tfplugin6.GetFunctions.Request + 101, // 141: tfplugin6.Provider.CallFunction:input_type -> tfplugin6.CallFunction.Request + 40, // 142: tfplugin6.Provider.StopProvider:input_type -> tfplugin6.StopProvider.Request + 51, // 143: tfplugin6.Provider.GetMetadata:output_type -> tfplugin6.GetMetadata.Response + 57, // 144: tfplugin6.Provider.GetProviderSchema:output_type -> tfplugin6.GetProviderSchema.Response + 63, // 145: tfplugin6.Provider.ValidateProviderConfig:output_type -> tfplugin6.ValidateProviderConfig.Response + 72, // 146: tfplugin6.Provider.ValidateResourceConfig:output_type -> tfplugin6.ValidateResourceConfig.Response + 74, // 147: tfplugin6.Provider.ValidateDataResourceConfig:output_type -> tfplugin6.ValidateDataResourceConfig.Response + 65, // 148: tfplugin6.Provider.UpgradeResourceState:output_type -> tfplugin6.UpgradeResourceState.Response + 67, // 149: tfplugin6.Provider.GetResourceIdentitySchemas:output_type -> tfplugin6.GetResourceIdentitySchemas.Response + 70, // 150: tfplugin6.Provider.UpgradeResourceIdentity:output_type -> tfplugin6.UpgradeResourceIdentity.Response + 78, // 151: tfplugin6.Provider.ConfigureProvider:output_type -> tfplugin6.ConfigureProvider.Response + 80, // 152: tfplugin6.Provider.ReadResource:output_type -> tfplugin6.ReadResource.Response + 82, // 153: tfplugin6.Provider.PlanResourceChange:output_type -> tfplugin6.PlanResourceChange.Response + 84, // 154: tfplugin6.Provider.ApplyResourceChange:output_type -> tfplugin6.ApplyResourceChange.Response + 87, // 155: tfplugin6.Provider.ImportResourceState:output_type -> tfplugin6.ImportResourceState.Response + 89, // 156: tfplugin6.Provider.MoveResourceState:output_type -> tfplugin6.MoveResourceState.Response + 91, // 157: tfplugin6.Provider.ReadDataSource:output_type -> tfplugin6.ReadDataSource.Response + 76, // 158: tfplugin6.Provider.ValidateEphemeralResourceConfig:output_type -> tfplugin6.ValidateEphemeralResourceConfig.Response + 93, // 159: tfplugin6.Provider.OpenEphemeralResource:output_type -> tfplugin6.OpenEphemeralResource.Response + 95, // 160: tfplugin6.Provider.RenewEphemeralResource:output_type -> tfplugin6.RenewEphemeralResource.Response + 97, // 161: tfplugin6.Provider.CloseEphemeralResource:output_type -> tfplugin6.CloseEphemeralResource.Response + 99, // 162: tfplugin6.Provider.GetFunctions:output_type -> tfplugin6.GetFunctions.Response + 102, // 163: tfplugin6.Provider.CallFunction:output_type -> tfplugin6.CallFunction.Response + 41, // 164: tfplugin6.Provider.StopProvider:output_type -> tfplugin6.StopProvider.Response + 143, // [143:165] is the sub-list for method output_type + 121, // [121:143] is the sub-list for method input_type + 121, // [121:121] is the sub-list for extension type_name + 121, // [121:121] is the sub-list for extension extendee + 0, // [0:121] is the sub-list for field type_name } func init() { file_tfplugin6_proto_init() } @@ -3654,596 +6735,23 @@ func file_tfplugin6_proto_init() { if File_tfplugin6_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_tfplugin6_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DynamicValue); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Diagnostic); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AttributePath); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StopProvider); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RawState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateProviderConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpgradeResourceState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateResourceConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateDataResourceConfig); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ConfigureProvider); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadResource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceChange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyResourceChange); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadDataSource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AttributePath_Step); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StopProvider_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*StopProvider_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_Block); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_Attribute); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_NestedBlock); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Schema_Object); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GetProviderSchema_ServerCapabilities); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateProviderConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateProviderConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpgradeResourceState_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[33].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UpgradeResourceState_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateResourceConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateResourceConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[36].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateDataResourceConfig_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[37].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ValidateDataResourceConfig_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[38].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ConfigureProvider_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[39].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ConfigureProvider_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[40].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadResource_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[41].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadResource_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[42].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceChange_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[43].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PlanResourceChange_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[44].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyResourceChange_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[45].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ApplyResourceChange_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[46].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[47].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState_ImportedResource); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[48].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportResourceState_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[49].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadDataSource_Request); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tfplugin6_proto_msgTypes[50].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ReadDataSource_Response); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - file_tfplugin6_proto_msgTypes[17].OneofWrappers = []interface{}{ + file_tfplugin6_proto_msgTypes[2].OneofWrappers = []any{} + file_tfplugin6_proto_msgTypes[34].OneofWrappers = []any{ (*AttributePath_Step_AttributeName)(nil), (*AttributePath_Step_ElementKeyString)(nil), (*AttributePath_Step_ElementKeyInt)(nil), } + file_tfplugin6_proto_msgTypes[88].OneofWrappers = []any{} + file_tfplugin6_proto_msgTypes[89].OneofWrappers = []any{} + file_tfplugin6_proto_msgTypes[90].OneofWrappers = []any{} + file_tfplugin6_proto_msgTypes[91].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_tfplugin6_proto_rawDesc, - NumEnums: 4, - NumMessages: 51, + RawDescriptor: unsafe.Slice(unsafe.StringData(file_tfplugin6_proto_rawDesc), len(file_tfplugin6_proto_rawDesc)), + NumEnums: 5, + NumMessages: 98, NumExtensions: 0, NumServices: 1, }, @@ -4253,7 +6761,6 @@ func file_tfplugin6_proto_init() { MessageInfos: file_tfplugin6_proto_msgTypes, }.Build() File_tfplugin6_proto = out.File - file_tfplugin6_proto_rawDesc = nil file_tfplugin6_proto_goTypes = nil file_tfplugin6_proto_depIdxs = nil } @@ -4270,12 +6777,25 @@ const _ = grpc.SupportPackageIsVersion6 // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type ProviderClient interface { - // ////// Information about what a provider supports/expects + // GetMetadata returns upfront information about server capabilities and + // supported resource types without requiring the server to instantiate all + // schema information, which may be memory intensive. This RPC is optional, + // where clients may receive an unimplemented RPC error. Clients should + // ignore the error and call the GetProviderSchema RPC as a fallback. + GetMetadata(ctx context.Context, in *GetMetadata_Request, opts ...grpc.CallOption) (*GetMetadata_Response, error) + // GetSchema returns schema information for the provider, data resources, + // and managed resources. GetProviderSchema(ctx context.Context, in *GetProviderSchema_Request, opts ...grpc.CallOption) (*GetProviderSchema_Response, error) ValidateProviderConfig(ctx context.Context, in *ValidateProviderConfig_Request, opts ...grpc.CallOption) (*ValidateProviderConfig_Response, error) ValidateResourceConfig(ctx context.Context, in *ValidateResourceConfig_Request, opts ...grpc.CallOption) (*ValidateResourceConfig_Response, error) ValidateDataResourceConfig(ctx context.Context, in *ValidateDataResourceConfig_Request, opts ...grpc.CallOption) (*ValidateDataResourceConfig_Response, error) UpgradeResourceState(ctx context.Context, in *UpgradeResourceState_Request, opts ...grpc.CallOption) (*UpgradeResourceState_Response, error) + // GetResourceIdentitySchemas returns the identity schemas for all managed + // resources. + GetResourceIdentitySchemas(ctx context.Context, in *GetResourceIdentitySchemas_Request, opts ...grpc.CallOption) (*GetResourceIdentitySchemas_Response, error) + // UpgradeResourceIdentityData should return the upgraded resource identity + // data for a managed resource type. + UpgradeResourceIdentity(ctx context.Context, in *UpgradeResourceIdentity_Request, opts ...grpc.CallOption) (*UpgradeResourceIdentity_Response, error) // ////// One-time initialization, called before other functions below ConfigureProvider(ctx context.Context, in *ConfigureProvider_Request, opts ...grpc.CallOption) (*ConfigureProvider_Response, error) // ////// Managed Resource Lifecycle @@ -4283,7 +6803,17 @@ type ProviderClient interface { PlanResourceChange(ctx context.Context, in *PlanResourceChange_Request, opts ...grpc.CallOption) (*PlanResourceChange_Response, error) ApplyResourceChange(ctx context.Context, in *ApplyResourceChange_Request, opts ...grpc.CallOption) (*ApplyResourceChange_Response, error) ImportResourceState(ctx context.Context, in *ImportResourceState_Request, opts ...grpc.CallOption) (*ImportResourceState_Response, error) + MoveResourceState(ctx context.Context, in *MoveResourceState_Request, opts ...grpc.CallOption) (*MoveResourceState_Response, error) ReadDataSource(ctx context.Context, in *ReadDataSource_Request, opts ...grpc.CallOption) (*ReadDataSource_Response, error) + // ////// Ephemeral Resource Lifecycle + ValidateEphemeralResourceConfig(ctx context.Context, in *ValidateEphemeralResourceConfig_Request, opts ...grpc.CallOption) (*ValidateEphemeralResourceConfig_Response, error) + OpenEphemeralResource(ctx context.Context, in *OpenEphemeralResource_Request, opts ...grpc.CallOption) (*OpenEphemeralResource_Response, error) + RenewEphemeralResource(ctx context.Context, in *RenewEphemeralResource_Request, opts ...grpc.CallOption) (*RenewEphemeralResource_Response, error) + CloseEphemeralResource(ctx context.Context, in *CloseEphemeralResource_Request, opts ...grpc.CallOption) (*CloseEphemeralResource_Response, error) + // GetFunctions returns the definitions of all functions. + GetFunctions(ctx context.Context, in *GetFunctions_Request, opts ...grpc.CallOption) (*GetFunctions_Response, error) + // ////// Provider-contributed Functions + CallFunction(ctx context.Context, in *CallFunction_Request, opts ...grpc.CallOption) (*CallFunction_Response, error) // ////// Graceful Shutdown StopProvider(ctx context.Context, in *StopProvider_Request, opts ...grpc.CallOption) (*StopProvider_Response, error) } @@ -4296,6 +6826,15 @@ func NewProviderClient(cc grpc.ClientConnInterface) ProviderClient { return &providerClient{cc} } +func (c *providerClient) GetMetadata(ctx context.Context, in *GetMetadata_Request, opts ...grpc.CallOption) (*GetMetadata_Response, error) { + out := new(GetMetadata_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/GetMetadata", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) GetProviderSchema(ctx context.Context, in *GetProviderSchema_Request, opts ...grpc.CallOption) (*GetProviderSchema_Response, error) { out := new(GetProviderSchema_Response) err := c.cc.Invoke(ctx, "/tfplugin6.Provider/GetProviderSchema", in, out, opts...) @@ -4341,6 +6880,24 @@ func (c *providerClient) UpgradeResourceState(ctx context.Context, in *UpgradeRe return out, nil } +func (c *providerClient) GetResourceIdentitySchemas(ctx context.Context, in *GetResourceIdentitySchemas_Request, opts ...grpc.CallOption) (*GetResourceIdentitySchemas_Response, error) { + out := new(GetResourceIdentitySchemas_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/GetResourceIdentitySchemas", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) UpgradeResourceIdentity(ctx context.Context, in *UpgradeResourceIdentity_Request, opts ...grpc.CallOption) (*UpgradeResourceIdentity_Response, error) { + out := new(UpgradeResourceIdentity_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/UpgradeResourceIdentity", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) ConfigureProvider(ctx context.Context, in *ConfigureProvider_Request, opts ...grpc.CallOption) (*ConfigureProvider_Response, error) { out := new(ConfigureProvider_Response) err := c.cc.Invoke(ctx, "/tfplugin6.Provider/ConfigureProvider", in, out, opts...) @@ -4386,6 +6943,15 @@ func (c *providerClient) ImportResourceState(ctx context.Context, in *ImportReso return out, nil } +func (c *providerClient) MoveResourceState(ctx context.Context, in *MoveResourceState_Request, opts ...grpc.CallOption) (*MoveResourceState_Response, error) { + out := new(MoveResourceState_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/MoveResourceState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) ReadDataSource(ctx context.Context, in *ReadDataSource_Request, opts ...grpc.CallOption) (*ReadDataSource_Response, error) { out := new(ReadDataSource_Response) err := c.cc.Invoke(ctx, "/tfplugin6.Provider/ReadDataSource", in, out, opts...) @@ -4395,6 +6961,60 @@ func (c *providerClient) ReadDataSource(ctx context.Context, in *ReadDataSource_ return out, nil } +func (c *providerClient) ValidateEphemeralResourceConfig(ctx context.Context, in *ValidateEphemeralResourceConfig_Request, opts ...grpc.CallOption) (*ValidateEphemeralResourceConfig_Response, error) { + out := new(ValidateEphemeralResourceConfig_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/ValidateEphemeralResourceConfig", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) OpenEphemeralResource(ctx context.Context, in *OpenEphemeralResource_Request, opts ...grpc.CallOption) (*OpenEphemeralResource_Response, error) { + out := new(OpenEphemeralResource_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/OpenEphemeralResource", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) RenewEphemeralResource(ctx context.Context, in *RenewEphemeralResource_Request, opts ...grpc.CallOption) (*RenewEphemeralResource_Response, error) { + out := new(RenewEphemeralResource_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/RenewEphemeralResource", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) CloseEphemeralResource(ctx context.Context, in *CloseEphemeralResource_Request, opts ...grpc.CallOption) (*CloseEphemeralResource_Response, error) { + out := new(CloseEphemeralResource_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/CloseEphemeralResource", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) GetFunctions(ctx context.Context, in *GetFunctions_Request, opts ...grpc.CallOption) (*GetFunctions_Response, error) { + out := new(GetFunctions_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/GetFunctions", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *providerClient) CallFunction(ctx context.Context, in *CallFunction_Request, opts ...grpc.CallOption) (*CallFunction_Response, error) { + out := new(CallFunction_Response) + err := c.cc.Invoke(ctx, "/tfplugin6.Provider/CallFunction", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *providerClient) StopProvider(ctx context.Context, in *StopProvider_Request, opts ...grpc.CallOption) (*StopProvider_Response, error) { out := new(StopProvider_Response) err := c.cc.Invoke(ctx, "/tfplugin6.Provider/StopProvider", in, out, opts...) @@ -4406,12 +7026,25 @@ func (c *providerClient) StopProvider(ctx context.Context, in *StopProvider_Requ // ProviderServer is the server API for Provider service. type ProviderServer interface { - // ////// Information about what a provider supports/expects + // GetMetadata returns upfront information about server capabilities and + // supported resource types without requiring the server to instantiate all + // schema information, which may be memory intensive. This RPC is optional, + // where clients may receive an unimplemented RPC error. Clients should + // ignore the error and call the GetProviderSchema RPC as a fallback. + GetMetadata(context.Context, *GetMetadata_Request) (*GetMetadata_Response, error) + // GetSchema returns schema information for the provider, data resources, + // and managed resources. GetProviderSchema(context.Context, *GetProviderSchema_Request) (*GetProviderSchema_Response, error) ValidateProviderConfig(context.Context, *ValidateProviderConfig_Request) (*ValidateProviderConfig_Response, error) ValidateResourceConfig(context.Context, *ValidateResourceConfig_Request) (*ValidateResourceConfig_Response, error) ValidateDataResourceConfig(context.Context, *ValidateDataResourceConfig_Request) (*ValidateDataResourceConfig_Response, error) UpgradeResourceState(context.Context, *UpgradeResourceState_Request) (*UpgradeResourceState_Response, error) + // GetResourceIdentitySchemas returns the identity schemas for all managed + // resources. + GetResourceIdentitySchemas(context.Context, *GetResourceIdentitySchemas_Request) (*GetResourceIdentitySchemas_Response, error) + // UpgradeResourceIdentityData should return the upgraded resource identity + // data for a managed resource type. + UpgradeResourceIdentity(context.Context, *UpgradeResourceIdentity_Request) (*UpgradeResourceIdentity_Response, error) // ////// One-time initialization, called before other functions below ConfigureProvider(context.Context, *ConfigureProvider_Request) (*ConfigureProvider_Response, error) // ////// Managed Resource Lifecycle @@ -4419,7 +7052,17 @@ type ProviderServer interface { PlanResourceChange(context.Context, *PlanResourceChange_Request) (*PlanResourceChange_Response, error) ApplyResourceChange(context.Context, *ApplyResourceChange_Request) (*ApplyResourceChange_Response, error) ImportResourceState(context.Context, *ImportResourceState_Request) (*ImportResourceState_Response, error) + MoveResourceState(context.Context, *MoveResourceState_Request) (*MoveResourceState_Response, error) ReadDataSource(context.Context, *ReadDataSource_Request) (*ReadDataSource_Response, error) + // ////// Ephemeral Resource Lifecycle + ValidateEphemeralResourceConfig(context.Context, *ValidateEphemeralResourceConfig_Request) (*ValidateEphemeralResourceConfig_Response, error) + OpenEphemeralResource(context.Context, *OpenEphemeralResource_Request) (*OpenEphemeralResource_Response, error) + RenewEphemeralResource(context.Context, *RenewEphemeralResource_Request) (*RenewEphemeralResource_Response, error) + CloseEphemeralResource(context.Context, *CloseEphemeralResource_Request) (*CloseEphemeralResource_Response, error) + // GetFunctions returns the definitions of all functions. + GetFunctions(context.Context, *GetFunctions_Request) (*GetFunctions_Response, error) + // ////// Provider-contributed Functions + CallFunction(context.Context, *CallFunction_Request) (*CallFunction_Response, error) // ////// Graceful Shutdown StopProvider(context.Context, *StopProvider_Request) (*StopProvider_Response, error) } @@ -4428,6 +7071,9 @@ type ProviderServer interface { type UnimplementedProviderServer struct { } +func (*UnimplementedProviderServer) GetMetadata(context.Context, *GetMetadata_Request) (*GetMetadata_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMetadata not implemented") +} func (*UnimplementedProviderServer) GetProviderSchema(context.Context, *GetProviderSchema_Request) (*GetProviderSchema_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method GetProviderSchema not implemented") } @@ -4443,6 +7089,12 @@ func (*UnimplementedProviderServer) ValidateDataResourceConfig(context.Context, func (*UnimplementedProviderServer) UpgradeResourceState(context.Context, *UpgradeResourceState_Request) (*UpgradeResourceState_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method UpgradeResourceState not implemented") } +func (*UnimplementedProviderServer) GetResourceIdentitySchemas(context.Context, *GetResourceIdentitySchemas_Request) (*GetResourceIdentitySchemas_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetResourceIdentitySchemas not implemented") +} +func (*UnimplementedProviderServer) UpgradeResourceIdentity(context.Context, *UpgradeResourceIdentity_Request) (*UpgradeResourceIdentity_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpgradeResourceIdentity not implemented") +} func (*UnimplementedProviderServer) ConfigureProvider(context.Context, *ConfigureProvider_Request) (*ConfigureProvider_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method ConfigureProvider not implemented") } @@ -4458,9 +7110,30 @@ func (*UnimplementedProviderServer) ApplyResourceChange(context.Context, *ApplyR func (*UnimplementedProviderServer) ImportResourceState(context.Context, *ImportResourceState_Request) (*ImportResourceState_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method ImportResourceState not implemented") } +func (*UnimplementedProviderServer) MoveResourceState(context.Context, *MoveResourceState_Request) (*MoveResourceState_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method MoveResourceState not implemented") +} func (*UnimplementedProviderServer) ReadDataSource(context.Context, *ReadDataSource_Request) (*ReadDataSource_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method ReadDataSource not implemented") } +func (*UnimplementedProviderServer) ValidateEphemeralResourceConfig(context.Context, *ValidateEphemeralResourceConfig_Request) (*ValidateEphemeralResourceConfig_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method ValidateEphemeralResourceConfig not implemented") +} +func (*UnimplementedProviderServer) OpenEphemeralResource(context.Context, *OpenEphemeralResource_Request) (*OpenEphemeralResource_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method OpenEphemeralResource not implemented") +} +func (*UnimplementedProviderServer) RenewEphemeralResource(context.Context, *RenewEphemeralResource_Request) (*RenewEphemeralResource_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method RenewEphemeralResource not implemented") +} +func (*UnimplementedProviderServer) CloseEphemeralResource(context.Context, *CloseEphemeralResource_Request) (*CloseEphemeralResource_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CloseEphemeralResource not implemented") +} +func (*UnimplementedProviderServer) GetFunctions(context.Context, *GetFunctions_Request) (*GetFunctions_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetFunctions not implemented") +} +func (*UnimplementedProviderServer) CallFunction(context.Context, *CallFunction_Request) (*CallFunction_Response, error) { + return nil, status.Errorf(codes.Unimplemented, "method CallFunction not implemented") +} func (*UnimplementedProviderServer) StopProvider(context.Context, *StopProvider_Request) (*StopProvider_Response, error) { return nil, status.Errorf(codes.Unimplemented, "method StopProvider not implemented") } @@ -4469,6 +7142,24 @@ func RegisterProviderServer(s *grpc.Server, srv ProviderServer) { s.RegisterService(&_Provider_serviceDesc, srv) } +func _Provider_GetMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMetadata_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).GetMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/GetMetadata", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).GetMetadata(ctx, req.(*GetMetadata_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_GetProviderSchema_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetProviderSchema_Request) if err := dec(in); err != nil { @@ -4559,6 +7250,42 @@ func _Provider_UpgradeResourceState_Handler(srv interface{}, ctx context.Context return interceptor(ctx, in, info, handler) } +func _Provider_GetResourceIdentitySchemas_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetResourceIdentitySchemas_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).GetResourceIdentitySchemas(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/GetResourceIdentitySchemas", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).GetResourceIdentitySchemas(ctx, req.(*GetResourceIdentitySchemas_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_UpgradeResourceIdentity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpgradeResourceIdentity_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).UpgradeResourceIdentity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/UpgradeResourceIdentity", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).UpgradeResourceIdentity(ctx, req.(*UpgradeResourceIdentity_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_ConfigureProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ConfigureProvider_Request) if err := dec(in); err != nil { @@ -4649,6 +7376,24 @@ func _Provider_ImportResourceState_Handler(srv interface{}, ctx context.Context, return interceptor(ctx, in, info, handler) } +func _Provider_MoveResourceState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MoveResourceState_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).MoveResourceState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/MoveResourceState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).MoveResourceState(ctx, req.(*MoveResourceState_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_ReadDataSource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ReadDataSource_Request) if err := dec(in); err != nil { @@ -4667,6 +7412,114 @@ func _Provider_ReadDataSource_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Provider_ValidateEphemeralResourceConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateEphemeralResourceConfig_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).ValidateEphemeralResourceConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/ValidateEphemeralResourceConfig", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).ValidateEphemeralResourceConfig(ctx, req.(*ValidateEphemeralResourceConfig_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_OpenEphemeralResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(OpenEphemeralResource_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).OpenEphemeralResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/OpenEphemeralResource", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).OpenEphemeralResource(ctx, req.(*OpenEphemeralResource_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_RenewEphemeralResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RenewEphemeralResource_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).RenewEphemeralResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/RenewEphemeralResource", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).RenewEphemeralResource(ctx, req.(*RenewEphemeralResource_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_CloseEphemeralResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseEphemeralResource_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).CloseEphemeralResource(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/CloseEphemeralResource", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).CloseEphemeralResource(ctx, req.(*CloseEphemeralResource_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_GetFunctions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetFunctions_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).GetFunctions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/GetFunctions", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).GetFunctions(ctx, req.(*GetFunctions_Request)) + } + return interceptor(ctx, in, info, handler) +} + +func _Provider_CallFunction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CallFunction_Request) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ProviderServer).CallFunction(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/tfplugin6.Provider/CallFunction", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ProviderServer).CallFunction(ctx, req.(*CallFunction_Request)) + } + return interceptor(ctx, in, info, handler) +} + func _Provider_StopProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(StopProvider_Request) if err := dec(in); err != nil { @@ -4689,6 +7542,10 @@ var _Provider_serviceDesc = grpc.ServiceDesc{ ServiceName: "tfplugin6.Provider", HandlerType: (*ProviderServer)(nil), Methods: []grpc.MethodDesc{ + { + MethodName: "GetMetadata", + Handler: _Provider_GetMetadata_Handler, + }, { MethodName: "GetProviderSchema", Handler: _Provider_GetProviderSchema_Handler, @@ -4709,6 +7566,14 @@ var _Provider_serviceDesc = grpc.ServiceDesc{ MethodName: "UpgradeResourceState", Handler: _Provider_UpgradeResourceState_Handler, }, + { + MethodName: "GetResourceIdentitySchemas", + Handler: _Provider_GetResourceIdentitySchemas_Handler, + }, + { + MethodName: "UpgradeResourceIdentity", + Handler: _Provider_UpgradeResourceIdentity_Handler, + }, { MethodName: "ConfigureProvider", Handler: _Provider_ConfigureProvider_Handler, @@ -4729,10 +7594,38 @@ var _Provider_serviceDesc = grpc.ServiceDesc{ MethodName: "ImportResourceState", Handler: _Provider_ImportResourceState_Handler, }, + { + MethodName: "MoveResourceState", + Handler: _Provider_MoveResourceState_Handler, + }, { MethodName: "ReadDataSource", Handler: _Provider_ReadDataSource_Handler, }, + { + MethodName: "ValidateEphemeralResourceConfig", + Handler: _Provider_ValidateEphemeralResourceConfig_Handler, + }, + { + MethodName: "OpenEphemeralResource", + Handler: _Provider_OpenEphemeralResource_Handler, + }, + { + MethodName: "RenewEphemeralResource", + Handler: _Provider_RenewEphemeralResource_Handler, + }, + { + MethodName: "CloseEphemeralResource", + Handler: _Provider_CloseEphemeralResource_Handler, + }, + { + MethodName: "GetFunctions", + Handler: _Provider_GetFunctions_Handler, + }, + { + MethodName: "CallFunction", + Handler: _Provider_CallFunction_Handler, + }, { MethodName: "StopProvider", Handler: _Provider_StopProvider_Handler, diff --git a/internal/tfplugin6/tfplugin6.proto b/internal/tfplugin6/tfplugin6.proto index a7cf631c59..a0a088e8c7 120000 --- a/internal/tfplugin6/tfplugin6.proto +++ b/internal/tfplugin6/tfplugin6.proto @@ -1 +1 @@ -../../docs/plugin-protocol/tfplugin6.3.proto \ No newline at end of file +../../docs/plugin-protocol/tfplugin6.proto \ No newline at end of file diff --git a/internal/typeexpr/defaults.go b/internal/typeexpr/defaults.go deleted file mode 100644 index 851c72fbfc..0000000000 --- a/internal/typeexpr/defaults.go +++ /dev/null @@ -1,157 +0,0 @@ -package typeexpr - -import ( - "github.com/zclconf/go-cty/cty" -) - -// Defaults represents a type tree which may contain default values for -// optional object attributes at any level. This is used to apply nested -// defaults to an input value before converting it to the concrete type. -type Defaults struct { - // Type of the node for which these defaults apply. This is necessary in - // order to determine how to inspect the Defaults and Children collections. - Type cty.Type - - // DefaultValues contains the default values for each object attribute, - // indexed by attribute name. - DefaultValues map[string]cty.Value - - // Children is a map of Defaults for elements contained in this type. This - // only applies to structural and collection types. - // - // The map is indexed by string instead of cty.Value because cty.Number - // instances are non-comparable, due to embedding a *big.Float. - // - // Collections have a single element type, which is stored at key "". - Children map[string]*Defaults -} - -// Apply walks the given value, applying specified defaults wherever optional -// attributes are missing. The input and output values may have different -// types, and the result may still require type conversion to the final desired -// type. -// -// This function is permissive and does not report errors, assuming that the -// caller will have better context to report useful type conversion failure -// diagnostics. -func (d *Defaults) Apply(val cty.Value) cty.Value { - val, err := cty.TransformWithTransformer(val, &defaultsTransformer{defaults: d}) - - // The transformer should never return an error. - if err != nil { - panic(err) - } - - return val -} - -// defaultsTransformer implements cty.Transformer, as a pre-order traversal, -// applying defaults as it goes. The pre-order traversal allows us to specify -// defaults more loosely for structural types, as the defaults for the types -// will be applied to the default value later in the walk. -type defaultsTransformer struct { - defaults *Defaults -} - -var _ cty.Transformer = (*defaultsTransformer)(nil) - -func (t *defaultsTransformer) Enter(p cty.Path, v cty.Value) (cty.Value, error) { - // Cannot apply defaults to an unknown value - if !v.IsKnown() { - return v, nil - } - - // Look up the defaults for this path. - defaults := t.defaults.traverse(p) - - // If we have no defaults, nothing to do. - if len(defaults) == 0 { - return v, nil - } - - // Ensure we are working with an object or map. - vt := v.Type() - if !vt.IsObjectType() && !vt.IsMapType() { - // Cannot apply defaults because the value type is incompatible. - // We'll ignore this and let the later conversion stage display a - // more useful diagnostic. - return v, nil - } - - // Unmark the value and reapply the marks later. - v, valMarks := v.Unmark() - - // Convert the given value into an attribute map (if it's non-null and - // non-empty). - attrs := make(map[string]cty.Value) - if !v.IsNull() && v.LengthInt() > 0 { - attrs = v.AsValueMap() - } - - // Apply defaults where attributes are missing, constructing a new - // value with the same marks. - for attr, defaultValue := range defaults { - if attrValue, ok := attrs[attr]; !ok || attrValue.IsNull() { - attrs[attr] = defaultValue - } - } - - // We construct an object even if the input value was a map, as the - // type of an attribute's default value may be incompatible with the - // map element type. - return cty.ObjectVal(attrs).WithMarks(valMarks), nil -} - -func (t *defaultsTransformer) Exit(p cty.Path, v cty.Value) (cty.Value, error) { - return v, nil -} - -// traverse walks the abstract defaults structure for a given path, returning -// a set of default values (if any are present) or nil (if not). This operation -// differs from applying a path to a value because we need to customize the -// traversal steps for collection types, where a single set of defaults can be -// applied to an arbitrary number of elements. -func (d *Defaults) traverse(path cty.Path) map[string]cty.Value { - if len(path) == 0 { - return d.DefaultValues - } - - switch s := path[0].(type) { - case cty.GetAttrStep: - if d.Type.IsObjectType() { - // Attribute path steps are normally applied to objects, where each - // attribute may have different defaults. - return d.traverseChild(s.Name, path) - } else if d.Type.IsMapType() { - // Literal values for maps can result in attribute path steps, in which - // case we need to disregard the attribute name, as maps can have only - // one child. - return d.traverseChild("", path) - } - - return nil - case cty.IndexStep: - if d.Type.IsTupleType() { - // Tuples can have different types for each element, so we look - // up the defaults based on the index key. - return d.traverseChild(s.Key.AsBigFloat().String(), path) - } else if d.Type.IsCollectionType() { - // Defaults for collection element types are stored with a blank - // key, so we disregard the index key. - return d.traverseChild("", path) - } - return nil - default: - // At time of writing there are no other path step types. - return nil - } -} - -// traverseChild continues the traversal for a given child key, and mutually -// recurses with traverse. -func (d *Defaults) traverseChild(name string, path cty.Path) map[string]cty.Value { - if child, ok := d.Children[name]; ok { - return child.traverse(path[1:]) - } - return nil -} diff --git a/internal/typeexpr/defaults_test.go b/internal/typeexpr/defaults_test.go deleted file mode 100644 index a4da6bb6b2..0000000000 --- a/internal/typeexpr/defaults_test.go +++ /dev/null @@ -1,504 +0,0 @@ -package typeexpr - -import ( - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/zclconf/go-cty/cty" -) - -var ( - valueComparer = cmp.Comparer(cty.Value.RawEquals) -) - -func TestDefaults_Apply(t *testing.T) { - simpleObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Bool, - }, []string{"b"}) - nestedObject := cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "c": simpleObject, - "d": cty.Number, - }, []string{"c"}) - - testCases := map[string]struct { - defaults *Defaults - value cty.Value - want cty.Value - }{ - // Nothing happens when there are no default values and no children. - "no defaults": { - defaults: &Defaults{ - Type: cty.Map(cty.String), - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("bar"), - }), - want: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("bar"), - }), - }, - // Passing a map which does not include one of the attributes with a - // default results in the default being applied to the output. Output - // is always an object. - "simple object with defaults applied": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - }, - // Unknown values may be assigned to root modules during validation, - // and we cannot apply defaults at that time. - "simple object with defaults but unknown value": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.UnknownVal(cty.Map(cty.String)), - want: cty.UnknownVal(cty.Map(cty.String)), - }, - // Defaults do not override attributes which are present in the given - // value. - "simple object with optional attributes specified": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("false"), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.StringVal("false"), - }), - }, - // Defaults will replace explicit nulls. - "object with explicit null for attribute with default": { - defaults: &Defaults{ - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.NullVal(cty.String), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - }, - // Defaults can be specified at any level of depth and will be applied - // so long as there is a parent value to populate. - "nested object with defaults applied": { - defaults: &Defaults{ - Type: nestedObject, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.False, - }, - }, - }, - }, - value: cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.False, - }), - "d": cty.NumberIntVal(5), - }), - }, - // Testing traversal of collections. - "map of objects with defaults applied": { - defaults: &Defaults{ - Type: cty.Map(simpleObject), - Children: map[string]*Defaults{ - "": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - value: cty.MapVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - }), - }), - want: cty.MapVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - "b": cty.True, - }), - }), - }, - // A map variable value specified in a tfvars file will be an object, - // in which case we must still traverse the defaults structure - // correctly. - "map of objects with defaults applied, given object instead of map": { - defaults: &Defaults{ - Type: cty.Map(simpleObject), - Children: map[string]*Defaults{ - "": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - value: cty.ObjectVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - }), - }), - want: cty.ObjectVal(map[string]cty.Value{ - "f": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "b": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - "b": cty.True, - }), - }), - }, - // Another example of a collection type, this time exercising the code - // processing a tuple input. - "list of objects with defaults applied": { - defaults: &Defaults{ - Type: cty.List(simpleObject), - Children: map[string]*Defaults{ - "": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("bar"), - "b": cty.True, - }), - }), - }, - // Unlike collections, tuple variable types can have defaults for - // multiple element types. - "tuple of objects with defaults applied": { - defaults: &Defaults{ - Type: cty.Tuple([]cty.Type{simpleObject, nestedObject}), - Children: map[string]*Defaults{ - "0": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.False, - }, - }, - "1": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("default"), - "b": cty.True, - }), - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(5), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.False, - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("default"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - }), - }, - // More complex cases with deeply nested defaults, testing the "default - // within a default" edges. - "set of nested objects, no default sub-object": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - // No default value for "c" specified, so none applied. The - // convert stage will fill in a null. - "d": cty.NumberIntVal(7), - }), - }), - }, - "set of nested objects, empty default sub-object": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - // This is a convenient shorthand which causes a - // missing sub-object to be filled with an object - // with all of the default values specified in the - // sub-object's type. - "c": cty.EmptyObjectVal, - }, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - // Default value for "b" is applied to the empty object - // specified as the default for "c" - "b": cty.True, - }), - "d": cty.NumberIntVal(7), - }), - }), - }, - "set of nested objects, overriding default sub-object": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - // If no value is given for "c", we use this object - // of non-default values instead. These take - // precedence over the default values specified in - // the child type. - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("fallback"), - "b": cty.False, - }), - }, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - // The default value for "b" is not applied, as the - // default value for "c" includes a non-default value - // already. - "a": cty.StringVal("fallback"), - "b": cty.False, - }), - "d": cty.NumberIntVal(7), - }), - }), - }, - "set of nested objects, nulls in default sub-object overridden": { - defaults: &Defaults{ - Type: cty.Set(nestedObject), - Children: map[string]*Defaults{ - "": { - Type: nestedObject, - DefaultValues: map[string]cty.Value{ - // The default value for "c" is used to prepopulate - // the nested object's value if not specified, but - // the null default for its "b" attribute will be - // overridden by the default specified in the child - // type. - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("fallback"), - "b": cty.NullVal(cty.Bool), - }), - }, - Children: map[string]*Defaults{ - "c": { - Type: simpleObject, - DefaultValues: map[string]cty.Value{ - "b": cty.True, - }, - }, - }, - }, - }, - }, - value: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "d": cty.NumberIntVal(7), - }), - }), - want: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - "a": cty.StringVal("foo"), - "b": cty.True, - }), - "d": cty.NumberIntVal(5), - }), - cty.ObjectVal(map[string]cty.Value{ - "c": cty.ObjectVal(map[string]cty.Value{ - // The default value for "b" overrides the explicit - // null in the default value for "c". - "a": cty.StringVal("fallback"), - "b": cty.True, - }), - "d": cty.NumberIntVal(7), - }), - }), - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - got := tc.defaults.Apply(tc.value) - if !cmp.Equal(tc.want, got, valueComparer) { - t.Errorf("wrong result\n%s", cmp.Diff(tc.want, got, valueComparer)) - } - }) - } -} diff --git a/internal/typeexpr/doc.go b/internal/typeexpr/doc.go deleted file mode 100644 index 9a62984a35..0000000000 --- a/internal/typeexpr/doc.go +++ /dev/null @@ -1,10 +0,0 @@ -// Package typeexpr is a fork of github.com/hashicorp/hcl/v2/ext/typeexpr -// which has additional experimental support for optional attributes. -// -// This is here as part of the module_variable_optional_attrs experiment. -// If that experiment is successful, the changes here may be upstreamed into -// HCL itself or, if we deem it to be Terraform-specific, we should at least -// update this documentation to reflect that this is now the primary -// Terraform-specific type expression implementation, separate from the -// upstream HCL one. -package typeexpr diff --git a/internal/typeexpr/get_type_test.go b/internal/typeexpr/get_type_test.go deleted file mode 100644 index 2dca23d27e..0000000000 --- a/internal/typeexpr/get_type_test.go +++ /dev/null @@ -1,669 +0,0 @@ -package typeexpr - -import ( - "fmt" - "testing" - - "github.com/hashicorp/hcl/v2/gohcl" - - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/hclsyntax" - "github.com/hashicorp/hcl/v2/json" - "github.com/zclconf/go-cty/cty" -) - -var ( - typeComparer = cmp.Comparer(cty.Type.Equals) -) - -func TestGetType(t *testing.T) { - tests := []struct { - Source string - Constraint bool - Want cty.Type - WantError string - }{ - // keywords - { - `bool`, - false, - cty.Bool, - "", - }, - { - `number`, - false, - cty.Number, - "", - }, - { - `string`, - false, - cty.String, - "", - }, - { - `any`, - false, - cty.DynamicPseudoType, - `The keyword "any" cannot be used in this type specification: an exact type is required.`, - }, - { - `any`, - true, - cty.DynamicPseudoType, - "", - }, - { - `list`, - false, - cty.DynamicPseudoType, - "The list type constructor requires one argument specifying the element type.", - }, - { - `map`, - false, - cty.DynamicPseudoType, - "The map type constructor requires one argument specifying the element type.", - }, - { - `set`, - false, - cty.DynamicPseudoType, - "The set type constructor requires one argument specifying the element type.", - }, - { - `object`, - false, - cty.DynamicPseudoType, - "The object type constructor requires one argument specifying the attribute types and values as a map.", - }, - { - `tuple`, - false, - cty.DynamicPseudoType, - "The tuple type constructor requires one argument specifying the element types as a list.", - }, - - // constructors - { - `bool()`, - false, - cty.DynamicPseudoType, - `Primitive type keyword "bool" does not expect arguments.`, - }, - { - `number()`, - false, - cty.DynamicPseudoType, - `Primitive type keyword "number" does not expect arguments.`, - }, - { - `string()`, - false, - cty.DynamicPseudoType, - `Primitive type keyword "string" does not expect arguments.`, - }, - { - `any()`, - false, - cty.DynamicPseudoType, - `Type constraint keyword "any" does not expect arguments.`, - }, - { - `any()`, - true, - cty.DynamicPseudoType, - `Type constraint keyword "any" does not expect arguments.`, - }, - { - `list(string)`, - false, - cty.List(cty.String), - ``, - }, - { - `set(string)`, - false, - cty.Set(cty.String), - ``, - }, - { - `map(string)`, - false, - cty.Map(cty.String), - ``, - }, - { - `list()`, - false, - cty.DynamicPseudoType, - `The list type constructor requires one argument specifying the element type.`, - }, - { - `list(string, string)`, - false, - cty.DynamicPseudoType, - `The list type constructor requires one argument specifying the element type.`, - }, - { - `list(any)`, - false, - cty.List(cty.DynamicPseudoType), - `The keyword "any" cannot be used in this type specification: an exact type is required.`, - }, - { - `list(any)`, - true, - cty.List(cty.DynamicPseudoType), - ``, - }, - { - `object({})`, - false, - cty.EmptyObject, - ``, - }, - { - `object({name=string})`, - false, - cty.Object(map[string]cty.Type{"name": cty.String}), - ``, - }, - { - `object({"name"=string})`, - false, - cty.EmptyObject, - `Object constructor map keys must be attribute names.`, - }, - { - `object({name=nope})`, - false, - cty.Object(map[string]cty.Type{"name": cty.DynamicPseudoType}), - `The keyword "nope" is not a valid type specification.`, - }, - { - `object()`, - false, - cty.DynamicPseudoType, - `The object type constructor requires one argument specifying the attribute types and values as a map.`, - }, - { - `object(string)`, - false, - cty.DynamicPseudoType, - `Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.`, - }, - { - `tuple([])`, - false, - cty.EmptyTuple, - ``, - }, - { - `tuple([string, bool])`, - false, - cty.Tuple([]cty.Type{cty.String, cty.Bool}), - ``, - }, - { - `tuple([nope])`, - false, - cty.Tuple([]cty.Type{cty.DynamicPseudoType}), - `The keyword "nope" is not a valid type specification.`, - }, - { - `tuple()`, - false, - cty.DynamicPseudoType, - `The tuple type constructor requires one argument specifying the element types as a list.`, - }, - { - `tuple(string)`, - false, - cty.DynamicPseudoType, - `Tuple type constructor requires a list of element types.`, - }, - { - `shwoop(string)`, - false, - cty.DynamicPseudoType, - `Keyword "shwoop" is not a valid type constructor.`, - }, - { - `list("string")`, - false, - cty.List(cty.DynamicPseudoType), - `A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).`, - }, - - // More interesting combinations - { - `list(object({}))`, - false, - cty.List(cty.EmptyObject), - ``, - }, - { - `list(map(tuple([])))`, - false, - cty.List(cty.Map(cty.EmptyTuple)), - ``, - }, - - // Optional modifier - { - `object({name=string,age=optional(number)})`, - true, - cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "name": cty.String, - "age": cty.Number, - }, []string{"age"}), - ``, - }, - { - `object({name=string,meta=optional(any)})`, - true, - cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "name": cty.String, - "meta": cty.DynamicPseudoType, - }, []string{"meta"}), - ``, - }, - { - `object({name=string,age=optional(number)})`, - false, - cty.Object(map[string]cty.Type{ - "name": cty.String, - "age": cty.Number, - }), - `Optional attribute modifier is only for type constraints, not for exact types.`, - }, - { - `object({name=string,meta=optional(any)})`, - false, - cty.Object(map[string]cty.Type{ - "name": cty.String, - "meta": cty.DynamicPseudoType, - }), - `Optional attribute modifier is only for type constraints, not for exact types.`, - }, - { - `object({name=string,meta=optional()})`, - true, - cty.Object(map[string]cty.Type{ - "name": cty.String, - }), - `Optional attribute modifier requires the attribute type as its argument.`, - }, - { - `object({name=string,meta=optional(string, "hello")})`, - true, - cty.Object(map[string]cty.Type{ - "name": cty.String, - "meta": cty.String, - }), - `Optional attribute modifier expects only one argument: the attribute type.`, - }, - { - `optional(string)`, - false, - cty.DynamicPseudoType, - `Keyword "optional" is valid only as a modifier for object type attributes.`, - }, - { - `optional`, - false, - cty.DynamicPseudoType, - `The keyword "optional" is not a valid type specification.`, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%s (constraint=%v)", test.Source, test.Constraint), func(t *testing.T) { - expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1}) - if diags.HasErrors() { - t.Fatalf("failed to parse: %s", diags) - } - - got, _, diags := getType(expr, test.Constraint, false) - if test.WantError == "" { - for _, diag := range diags { - t.Error(diag) - } - } else { - found := false - for _, diag := range diags { - t.Log(diag) - if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { - found = true - } - } - if !found { - t.Errorf("missing expected error detail message: %s", test.WantError) - } - } - - if !got.Equals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) - } - }) - } -} - -func TestGetTypeJSON(t *testing.T) { - // We have fewer test cases here because we're mainly exercising the - // extra indirection in the JSON syntax package, which ultimately calls - // into the native syntax parser (which we tested extensively in - // TestGetType). - tests := []struct { - Source string - Constraint bool - Want cty.Type - WantError string - }{ - { - `{"expr":"bool"}`, - false, - cty.Bool, - "", - }, - { - `{"expr":"list(bool)"}`, - false, - cty.List(cty.Bool), - "", - }, - { - `{"expr":"list"}`, - false, - cty.DynamicPseudoType, - "The list type constructor requires one argument specifying the element type.", - }, - } - - for _, test := range tests { - t.Run(test.Source, func(t *testing.T) { - file, diags := json.Parse([]byte(test.Source), "") - if diags.HasErrors() { - t.Fatalf("failed to parse: %s", diags) - } - - type TestContent struct { - Expr hcl.Expression `hcl:"expr"` - } - var content TestContent - diags = gohcl.DecodeBody(file.Body, nil, &content) - if diags.HasErrors() { - t.Fatalf("failed to decode: %s", diags) - } - - got, _, diags := getType(content.Expr, test.Constraint, false) - if test.WantError == "" { - for _, diag := range diags { - t.Error(diag) - } - } else { - found := false - for _, diag := range diags { - t.Log(diag) - if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { - found = true - } - } - if !found { - t.Errorf("missing expected error detail message: %s", test.WantError) - } - } - - if !got.Equals(test.Want) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) - } - }) - } -} - -func TestGetTypeDefaults(t *testing.T) { - tests := []struct { - Source string - Want *Defaults - WantError string - }{ - // primitive types have nil defaults - { - `bool`, - nil, - "", - }, - { - `number`, - nil, - "", - }, - { - `string`, - nil, - "", - }, - { - `any`, - nil, - "", - }, - - // complex structures with no defaults have nil defaults - { - `map(string)`, - nil, - "", - }, - { - `set(number)`, - nil, - "", - }, - { - `tuple([number, string])`, - nil, - "", - }, - { - `object({ a = string, b = number })`, - nil, - "", - }, - { - `map(list(object({ a = string, b = optional(number) })))`, - nil, - "", - }, - - // object optional attribute with defaults - { - `object({ a = string, b = optional(number, 5) })`, - &Defaults{ - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - "", - }, - - // nested defaults - { - `object({ a = optional(object({ b = optional(number, 5) }), {}) })`, - &Defaults{ - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "b": cty.Number, - }, []string{"b"}), - }, []string{"a"}), - DefaultValues: map[string]cty.Value{ - "a": cty.EmptyObjectVal, - }, - Children: map[string]*Defaults{ - "a": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - - // collections of objects with defaults - { - `map(object({ a = string, b = optional(number, 5) }))`, - &Defaults{ - Type: cty.Map(cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"})), - Children: map[string]*Defaults{ - "": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - { - `list(object({ a = string, b = optional(number, 5) }))`, - &Defaults{ - Type: cty.List(cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"})), - Children: map[string]*Defaults{ - "": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - { - `set(object({ a = string, b = optional(number, 5) }))`, - &Defaults{ - Type: cty.Set(cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"})), - Children: map[string]*Defaults{ - "": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - - // tuples containing objects with defaults work differently from - // collections - { - `tuple([string, bool, object({ a = string, b = optional(number, 5) })])`, - &Defaults{ - Type: cty.Tuple([]cty.Type{ - cty.String, - cty.Bool, - cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - }), - Children: map[string]*Defaults{ - "2": { - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"b"}), - DefaultValues: map[string]cty.Value{ - "b": cty.NumberIntVal(5), - }, - }, - }, - }, - "", - }, - - // incompatible default value causes an error - { - `object({ a = optional(string, "hello"), b = optional(number, true) })`, - &Defaults{ - Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{ - "a": cty.String, - "b": cty.Number, - }, []string{"a", "b"}), - DefaultValues: map[string]cty.Value{ - "a": cty.StringVal("hello"), - }, - }, - "This default value is not compatible with the attribute's type constraint: number required.", - }, - - // Too many arguments - { - `object({name=string,meta=optional(string, "hello", "world")})`, - nil, - `Optional attribute modifier expects at most two arguments: the attribute type, and a default value.`, - }, - } - - for _, test := range tests { - t.Run(test.Source, func(t *testing.T) { - expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1}) - if diags.HasErrors() { - t.Fatalf("failed to parse: %s", diags) - } - - _, got, diags := getType(expr, true, true) - if test.WantError == "" { - for _, diag := range diags { - t.Error(diag) - } - } else { - found := false - for _, diag := range diags { - t.Log(diag) - if diag.Severity == hcl.DiagError && diag.Detail == test.WantError { - found = true - } - } - if !found { - t.Errorf("missing expected error detail message: %s", test.WantError) - } - } - - if !cmp.Equal(test.Want, got, valueComparer, typeComparer) { - t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got, valueComparer, typeComparer)) - } - }) - } -} diff --git a/internal/typeexpr/public.go b/internal/typeexpr/public.go deleted file mode 100644 index 82f215c097..0000000000 --- a/internal/typeexpr/public.go +++ /dev/null @@ -1,143 +0,0 @@ -package typeexpr - -import ( - "bytes" - "fmt" - "sort" - - "github.com/hashicorp/hcl/v2/hclsyntax" - - "github.com/hashicorp/hcl/v2" - "github.com/zclconf/go-cty/cty" -) - -// Type attempts to process the given expression as a type expression and, if -// successful, returns the resulting type. If unsuccessful, error diagnostics -// are returned. -func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) { - ty, _, diags := getType(expr, false, false) - return ty, diags -} - -// TypeConstraint attempts to parse the given expression as a type constraint -// and, if successful, returns the resulting type. If unsuccessful, error -// diagnostics are returned. -// -// A type constraint has the same structure as a type, but it additionally -// allows the keyword "any" to represent cty.DynamicPseudoType, which is often -// used as a wildcard in type checking and type conversion operations. -func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) { - ty, _, diags := getType(expr, true, false) - return ty, diags -} - -// TypeConstraintWithDefaults attempts to parse the given expression as a type -// constraint which may include default values for object attributes. If -// successful both the resulting type and corresponding defaults are returned. -// If unsuccessful, error diagnostics are returned. -// -// When using this function, defaults should be applied to the input value -// before type conversion, to ensure that objects with missing attributes have -// default values populated. -func TypeConstraintWithDefaults(expr hcl.Expression) (cty.Type, *Defaults, hcl.Diagnostics) { - return getType(expr, true, true) -} - -// TypeString returns a string rendering of the given type as it would be -// expected to appear in the HCL native syntax. -// -// This is primarily intended for showing types to the user in an application -// that uses typexpr, where the user can be assumed to be familiar with the -// type expression syntax. In applications that do not use typeexpr these -// results may be confusing to the user and so type.FriendlyName may be -// preferable, even though it's less precise. -// -// TypeString produces reasonable results only for types like what would be -// produced by the Type and TypeConstraint functions. In particular, it cannot -// support capsule types. -func TypeString(ty cty.Type) string { - // Easy cases first - switch ty { - case cty.String: - return "string" - case cty.Bool: - return "bool" - case cty.Number: - return "number" - case cty.DynamicPseudoType: - return "any" - } - - if ty.IsCapsuleType() { - panic("TypeString does not support capsule types") - } - - if ty.IsCollectionType() { - ety := ty.ElementType() - etyString := TypeString(ety) - switch { - case ty.IsListType(): - return fmt.Sprintf("list(%s)", etyString) - case ty.IsSetType(): - return fmt.Sprintf("set(%s)", etyString) - case ty.IsMapType(): - return fmt.Sprintf("map(%s)", etyString) - default: - // Should never happen because the above is exhaustive - panic("unsupported collection type") - } - } - - if ty.IsObjectType() { - var buf bytes.Buffer - buf.WriteString("object({") - atys := ty.AttributeTypes() - names := make([]string, 0, len(atys)) - for name := range atys { - names = append(names, name) - } - sort.Strings(names) - first := true - for _, name := range names { - aty := atys[name] - if !first { - buf.WriteByte(',') - } - if !hclsyntax.ValidIdentifier(name) { - // Should never happen for any type produced by this package, - // but we'll do something reasonable here just so we don't - // produce garbage if someone gives us a hand-assembled object - // type that has weird attribute names. - // Using Go-style quoting here isn't perfect, since it doesn't - // exactly match HCL syntax, but it's fine for an edge-case. - buf.WriteString(fmt.Sprintf("%q", name)) - } else { - buf.WriteString(name) - } - buf.WriteByte('=') - buf.WriteString(TypeString(aty)) - first = false - } - buf.WriteString("})") - return buf.String() - } - - if ty.IsTupleType() { - var buf bytes.Buffer - buf.WriteString("tuple([") - etys := ty.TupleElementTypes() - first := true - for _, ety := range etys { - if !first { - buf.WriteByte(',') - } - buf.WriteString(TypeString(ety)) - first = false - } - buf.WriteString("])") - return buf.String() - } - - // Should never happen because we covered all cases above. - panic(fmt.Errorf("unsupported type %#v", ty)) -} diff --git a/internal/typeexpr/type_string_test.go b/internal/typeexpr/type_string_test.go deleted file mode 100644 index fbdf3f481d..0000000000 --- a/internal/typeexpr/type_string_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package typeexpr - -import ( - "testing" - - "github.com/zclconf/go-cty/cty" -) - -func TestTypeString(t *testing.T) { - tests := []struct { - Type cty.Type - Want string - }{ - { - cty.DynamicPseudoType, - "any", - }, - { - cty.String, - "string", - }, - { - cty.Number, - "number", - }, - { - cty.Bool, - "bool", - }, - { - cty.List(cty.Number), - "list(number)", - }, - { - cty.Set(cty.Bool), - "set(bool)", - }, - { - cty.Map(cty.String), - "map(string)", - }, - { - cty.EmptyObject, - "object({})", - }, - { - cty.Object(map[string]cty.Type{"foo": cty.Bool}), - "object({foo=bool})", - }, - { - cty.Object(map[string]cty.Type{"foo": cty.Bool, "bar": cty.String}), - "object({bar=string,foo=bool})", - }, - { - cty.EmptyTuple, - "tuple([])", - }, - { - cty.Tuple([]cty.Type{cty.Bool}), - "tuple([bool])", - }, - { - cty.Tuple([]cty.Type{cty.Bool, cty.String}), - "tuple([bool,string])", - }, - { - cty.List(cty.DynamicPseudoType), - "list(any)", - }, - { - cty.Tuple([]cty.Type{cty.DynamicPseudoType}), - "tuple([any])", - }, - { - cty.Object(map[string]cty.Type{"foo": cty.DynamicPseudoType}), - "object({foo=any})", - }, - { - // We don't expect to find attributes that aren't valid identifiers - // because we only promise to support types that this package - // would've created, but we allow this situation during rendering - // just because it's convenient for applications trying to produce - // error messages about mismatched types. Note that the quoted - // attribute name is not actually accepted by our Type and - // TypeConstraint functions, so this is one situation where the - // TypeString result cannot be re-parsed by those functions. - cty.Object(map[string]cty.Type{"foo bar baz": cty.String}), - `object({"foo bar baz"=string})`, - }, - } - - for _, test := range tests { - t.Run(test.Type.GoString(), func(t *testing.T) { - got := TypeString(test.Type) - if got != test.Want { - t.Errorf("wrong result\ntype: %#v\ngot: %s\nwant: %s", test.Type, got, test.Want) - } - }) - } -} diff --git a/internal/typeexpr/type_type.go b/internal/typeexpr/type_type.go deleted file mode 100644 index e72bf6beff..0000000000 --- a/internal/typeexpr/type_type.go +++ /dev/null @@ -1,119 +0,0 @@ -package typeexpr - -import ( - "fmt" - "reflect" - - "github.com/hashicorp/hcl/v2" - "github.com/hashicorp/hcl/v2/ext/customdecode" - "github.com/zclconf/go-cty/cty" - "github.com/zclconf/go-cty/cty/convert" - "github.com/zclconf/go-cty/cty/function" -) - -// TypeConstraintType is a cty capsule type that allows cty type constraints to -// be used as values. -// -// If TypeConstraintType is used in a context supporting the -// customdecode.CustomExpressionDecoder extension then it will implement -// expression decoding using the TypeConstraint function, thus allowing -// type expressions to be used in contexts where value expressions might -// normally be expected, such as in arguments to function calls. -var TypeConstraintType cty.Type - -// TypeConstraintVal constructs a cty.Value whose type is -// TypeConstraintType. -func TypeConstraintVal(ty cty.Type) cty.Value { - return cty.CapsuleVal(TypeConstraintType, &ty) -} - -// TypeConstraintFromVal extracts the type from a cty.Value of -// TypeConstraintType that was previously constructed using TypeConstraintVal. -// -// If the given value isn't a known, non-null value of TypeConstraintType -// then this function will panic. -func TypeConstraintFromVal(v cty.Value) cty.Type { - if !v.Type().Equals(TypeConstraintType) { - panic("value is not of TypeConstraintType") - } - ptr := v.EncapsulatedValue().(*cty.Type) - return *ptr -} - -// ConvertFunc is a cty function that implements type conversions. -// -// Its signature is as follows: -// -// convert(value, type_constraint) -// -// ...where type_constraint is a type constraint expression as defined by -// typeexpr.TypeConstraint. -// -// It relies on HCL's customdecode extension and so it's not suitable for use -// in non-HCL contexts or if you are using a HCL syntax implementation that -// does not support customdecode for function arguments. However, it _is_ -// supported for function calls in the HCL native expression syntax. -var ConvertFunc function.Function - -func init() { - TypeConstraintType = cty.CapsuleWithOps("type constraint", reflect.TypeOf(cty.Type{}), &cty.CapsuleOps{ - ExtensionData: func(key interface{}) interface{} { - switch key { - case customdecode.CustomExpressionDecoder: - return customdecode.CustomExpressionDecoderFunc( - func(expr hcl.Expression, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { - ty, diags := TypeConstraint(expr) - if diags.HasErrors() { - return cty.NilVal, diags - } - return TypeConstraintVal(ty), nil - }, - ) - default: - return nil - } - }, - TypeGoString: func(_ reflect.Type) string { - return "typeexpr.TypeConstraintType" - }, - GoString: func(raw interface{}) string { - tyPtr := raw.(*cty.Type) - return fmt.Sprintf("typeexpr.TypeConstraintVal(%#v)", *tyPtr) - }, - RawEquals: func(a, b interface{}) bool { - aPtr := a.(*cty.Type) - bPtr := b.(*cty.Type) - return (*aPtr).Equals(*bPtr) - }, - }) - - ConvertFunc = function.New(&function.Spec{ - Params: []function.Parameter{ - { - Name: "value", - Type: cty.DynamicPseudoType, - AllowNull: true, - AllowDynamicType: true, - }, - { - Name: "type", - Type: TypeConstraintType, - }, - }, - Type: func(args []cty.Value) (cty.Type, error) { - wantTypePtr := args[1].EncapsulatedValue().(*cty.Type) - got, err := convert.Convert(args[0], *wantTypePtr) - if err != nil { - return cty.NilType, function.NewArgError(0, err) - } - return got.Type(), nil - }, - Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { - v, err := convert.Convert(args[0], retType) - if err != nil { - return cty.NilVal, function.NewArgError(0, err) - } - return v, nil - }, - }) -} diff --git a/internal/typeexpr/type_type_test.go b/internal/typeexpr/type_type_test.go deleted file mode 100644 index 2286a2e1a5..0000000000 --- a/internal/typeexpr/type_type_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package typeexpr - -import ( - "fmt" - "testing" - - "github.com/zclconf/go-cty/cty" -) - -func TestTypeConstraintType(t *testing.T) { - tyVal1 := TypeConstraintVal(cty.String) - tyVal2 := TypeConstraintVal(cty.String) - tyVal3 := TypeConstraintVal(cty.Number) - - if !tyVal1.RawEquals(tyVal2) { - t.Errorf("tyVal1 not equal to tyVal2\ntyVal1: %#v\ntyVal2: %#v", tyVal1, tyVal2) - } - if tyVal1.RawEquals(tyVal3) { - t.Errorf("tyVal1 equal to tyVal2, but should not be\ntyVal1: %#v\ntyVal3: %#v", tyVal1, tyVal3) - } - - if got, want := TypeConstraintFromVal(tyVal1), cty.String; !got.Equals(want) { - t.Errorf("wrong type extracted from tyVal1\ngot: %#v\nwant: %#v", got, want) - } - if got, want := TypeConstraintFromVal(tyVal3), cty.Number; !got.Equals(want) { - t.Errorf("wrong type extracted from tyVal3\ngot: %#v\nwant: %#v", got, want) - } -} - -func TestConvertFunc(t *testing.T) { - // This is testing the convert function directly, skipping over the HCL - // parsing and evaluation steps that would normally lead there. There is - // another test in the "integrationtest" package called TestTypeConvertFunc - // that exercises the full path to this function via the hclsyntax parser. - - tests := []struct { - val, ty cty.Value - want cty.Value - wantErr string - }{ - // The goal here is not an exhaustive set of conversions, since that's - // already covered in cty/convert, but rather exercising different - // permutations of success and failure to make sure the function - // handles all of the results in a reasonable way. - { - cty.StringVal("hello"), - TypeConstraintVal(cty.String), - cty.StringVal("hello"), - ``, - }, - { - cty.True, - TypeConstraintVal(cty.String), - cty.StringVal("true"), - ``, - }, - { - cty.StringVal("hello"), - TypeConstraintVal(cty.Bool), - cty.NilVal, - `a bool is required`, - }, - { - cty.UnknownVal(cty.Bool), - TypeConstraintVal(cty.Bool), - cty.UnknownVal(cty.Bool), - ``, - }, - { - cty.DynamicVal, - TypeConstraintVal(cty.Bool), - cty.UnknownVal(cty.Bool), - ``, - }, - { - cty.NullVal(cty.Bool), - TypeConstraintVal(cty.Bool), - cty.NullVal(cty.Bool), - ``, - }, - { - cty.NullVal(cty.DynamicPseudoType), - TypeConstraintVal(cty.Bool), - cty.NullVal(cty.Bool), - ``, - }, - { - cty.StringVal("hello").Mark(1), - TypeConstraintVal(cty.String), - cty.StringVal("hello").Mark(1), - ``, - }, - } - - for _, test := range tests { - t.Run(fmt.Sprintf("%#v to %#v", test.val, test.ty), func(t *testing.T) { - got, err := ConvertFunc.Call([]cty.Value{test.val, test.ty}) - - if err != nil { - if test.wantErr != "" { - if got, want := err.Error(), test.wantErr; got != want { - t.Errorf("wrong error\ngot: %s\nwant: %s", got, want) - } - } else { - t.Errorf("unexpected error\ngot: %s\nwant: ", err) - } - return - } - if test.wantErr != "" { - t.Errorf("wrong error\ngot: \nwant: %s", test.wantErr) - } - - if !test.want.RawEquals(got) { - t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.want) - } - }) - } -} diff --git a/main.go b/main.go index c807bb5a40..cce3f5f998 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,10 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( + "context" "encoding/json" "fmt" "log" @@ -10,6 +14,8 @@ import ( "runtime" "strings" + "github.com/apparentlymart/go-shquot/shquot" + "github.com/hashicorp/cli" "github.com/hashicorp/go-plugin" "github.com/hashicorp/terraform-svchost/disco" "github.com/hashicorp/terraform/internal/addrs" @@ -21,8 +27,8 @@ import ( "github.com/hashicorp/terraform/internal/terminal" "github.com/hashicorp/terraform/version" "github.com/mattn/go-shellwords" - "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" + "go.opentelemetry.io/otel/trace" backendInit "github.com/hashicorp/terraform/internal/backend/init" ) @@ -63,6 +69,24 @@ func realMain() int { var err error + err = openTelemetryInit() + if err != nil { + // openTelemetryInit can only fail if Terraform was run with an + // explicit environment variable to enable telemetry collection, + // so in typical use we cannot get here. + Ui.Error(fmt.Sprintf("Could not initialize telemetry: %s", err)) + Ui.Error(fmt.Sprintf("Unset environment variable %s if you don't intend to collect telemetry from Terraform.", openTelemetryExporterEnvVar)) + return 1 + } + var ctx context.Context + var otelSpan trace.Span + { + // At minimum we emit a span covering the entire command execution. + _, displayArgs := shquot.POSIXShellSplit(os.Args) + ctx, otelSpan = tracer.Start(context.Background(), fmt.Sprintf("terraform %s", displayArgs)) + defer otelSpan.End() + } + tmpLogPath := os.Getenv(envTmpLogPath) if tmpLogPath != "" { f, err := os.OpenFile(tmpLogPath, os.O_RDWR|os.O_APPEND, 0666) @@ -222,11 +246,11 @@ func realMain() int { // in case they need to refer back to it for any special reason, though // they should primarily be working with the override working directory // that we've now switched to above. - initCommands(originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders) + initCommands(ctx, originalWd, streams, config, services, providerSrc, providerDevOverrides, unmanagedProviders) } // Run checkpoint - go runCheckpoint(config) + go runCheckpoint(ctx, config) // Make sure we clean up any managed plugins at the end of this defer plugin.CleanupClients() @@ -335,8 +359,13 @@ func mergeEnvArgs(envName string, cmd string, args []string) ([]string, error) { return args, nil } + swParser := &shellwords.Parser{ + ParseEnv: false, + ParseBacktick: false, + } + log.Printf("[INFO] %s value: %q", envName, v) - extra, err := shellwords.Parse(v) + extra, err := swParser.Parse(v) if err != nil { return nil, fmt.Errorf( "Error parsing extra CLI args from %s: %s", diff --git a/main_test.go b/main_test.go index 15a09f283c..addd07dfeb 100644 --- a/main_test.go +++ b/main_test.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -6,7 +9,7 @@ import ( "reflect" "testing" - "github.com/mitchellh/cli" + "github.com/hashicorp/cli" ) func TestMain_cliArgsFromEnv(t *testing.T) { @@ -31,7 +34,7 @@ func TestMain_cliArgsFromEnv(t *testing.T) { cases := []struct { Name string Args []string - Value string + EnvValue string Expected []string Err bool }{ @@ -46,8 +49,8 @@ func TestMain_cliArgsFromEnv(t *testing.T) { { "both env var and CLI", []string{testCommandName, "foo", "bar"}, - "-foo bar", - []string{"-foo", "bar", "foo", "bar"}, + "-foo baz", + []string{"-foo", "baz", "foo", "bar"}, false, }, @@ -108,19 +111,36 @@ func TestMain_cliArgsFromEnv(t *testing.T) { []string{"-foo", "'bar baz'", "foo"}, false, }, + + { + "backticks taken literally", + // The shellwords library we use to parse the environment variables + // has the option to automatically execute commands written in + // backticks. This test is here to make sure we don't accidentally + // enable that. + []string{testCommandName, "foo"}, + "-foo `echo nope`", + []string{"-foo", "`echo nope`", "foo"}, + false, + }, + + { + "no nested environment variable expansion", + // The shellwords library we use to parse the environment variables + // has the option to automatically expand sequences that appear + // to be environment variable interpolations. This test is here to + // make sure we don't accidentally enable that. + []string{testCommandName, "foo"}, + "-foo $OTHER_ENV", + []string{"-foo", "$OTHER_ENV", "foo"}, + false, + }, } for i, tc := range cases { t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) { - os.Unsetenv(EnvCLI) - defer os.Unsetenv(EnvCLI) - - // Set the env var value - if tc.Value != "" { - if err := os.Setenv(EnvCLI, tc.Value); err != nil { - t.Fatalf("err: %s", err) - } - } + t.Setenv(EnvCLI, tc.EnvValue) + t.Setenv("OTHER_ENV", "placeholder") // Set up the args args := make([]string, len(tc.Args)+1) @@ -140,7 +160,7 @@ func TestMain_cliArgsFromEnv(t *testing.T) { // Verify if !reflect.DeepEqual(testCommand.Args, tc.Expected) { - t.Fatalf("bad: %#v", testCommand.Args) + t.Fatalf("expected args %#v but got %#v", tc.Expected, testCommand.Args) } }) } diff --git a/provider_source.go b/provider_source.go index f27ca54b2a..50111b3c01 100644 --- a/provider_source.go +++ b/provider_source.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( @@ -90,7 +93,7 @@ func implicitProviderSource(services *disco.Disco) getproviders.Source { // The local search directories we use for implicit configuration are: // - The "terraform.d/plugins" directory in the current working directory, // which we've historically documented as a place to put plugins as a - // way to include them in bundles uploaded to Terraform Cloud, where + // way to include them in bundles uploaded to HCP Terraform, where // there has historically otherwise been no way to use custom providers. // - The "plugins" subdirectory of the CLI config search directory. // (thats ~/.terraform.d/plugins on Unix systems, equivalents elsewhere) diff --git a/scripts/build.sh b/scripts/build.sh index a7b2002b6e..8ac354ea3e 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + # # This script builds the application from source for multiple platforms. @@ -8,7 +11,7 @@ while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" # Change into that directory -cd "$DIR" +cd "$DIR" || exit # Determine the arch/os combos we're building for XC_ARCH=${XC_ARCH:-"386 amd64 arm"} @@ -38,9 +41,10 @@ export CGO_ENABLED=0 # Set module download mode to readonly to not implicitly update go.mod export GOFLAGS="-mod=readonly" -# In release mode we don't want debug information in the binary +# In release mode we don't want debug information in the binary and we don't +# want the -dev version marker if [[ -n "${TF_RELEASE}" ]]; then - LD_FLAGS="-s -w" + LD_FLAGS="-s -w -X 'github.com/hashicorp/terraform/version.dev=no'" fi # Ensure all remote modules are downloaded and cached before build so that diff --git a/scripts/changelog-links.sh b/scripts/changelog-links.sh index 34645f36cf..1f20c2c570 100755 --- a/scripts/changelog-links.sh +++ b/scripts/changelog-links.sh @@ -1,4 +1,7 @@ #!/bin/bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + # This script rewrites [GH-nnnn]-style references in the CHANGELOG.md file to # be Markdown links to the given github issues. diff --git a/scripts/changelog.sh b/scripts/changelog.sh new file mode 100755 index 0000000000..68b7e995be --- /dev/null +++ b/scripts/changelog.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + + +set -uo pipefail + +CHANGIE_VERSION="${CHANGIE_VERSION:-1.21.0}" +SEMVER_VERSION="${SEMVER_VERSION:-7.6.3}" + +function usage { + cat <<-'EOF' +Usage: ./changelog.sh [] + +Description: + This script will update CHANGELOG.md with the given version and date and add the changelog entries. + It will also set the version/VERSION file to the correct version. + In general this script should handle most release related tasks within this repository. + +Commands: + generate + generate will create a new section in the CHANGELOG.md file for the given release + type. The release type should be one of "dev", "alpha", "rc", "release", or "patch". + `dev`: will update the changelog with the latest unreleased changes. + `alpha`: will generate a new section with an alpha version for today. + `beta`: will generate a new beta release. + `rc`: will generate a new rc release. + `release`: will make the initial minor release for this branch. + `patch`: will generate a new patch release + + nextminor: + Run on main branch: Updates the minor version. + + listIssuesInRelease: + Lists all issues in the release passed as an argument. +EOF +} + +function generate { + RELEASE_TYPE="${1:-}" + + if [[ -z "$RELEASE_TYPE" ]]; then + echo "missing argument" + usage + exit 1 + fi + + FOOTER_FILE='footer.md' + + case "$RELEASE_TYPE" in + + dev) + FOOTER_FILE='footer-with-experiments.md' + LATEST_VERSION=$(npx -y changie@$CHANGIE_VERSION latest -r --skip-prereleases) + + # Check if we already released this version already + if git tag -l "v$LATEST_VERSION" | grep -q "v$LATEST_VERSION"; then + LATEST_VERSION=$(npx -y semver@$SEMVER_VERSION -i patch $LATEST_VERSION) + fi + + COMPLETE_VERSION="$LATEST_VERSION-dev" + + npx -y changie@$CHANGIE_VERSION merge -u "## $LATEST_VERSION (Unreleased)" + + # If we have no changes yet, the changelog is empty now, so we need to add a header + if ! grep -q "## $LATEST_VERSION" CHANGELOG.md; then + CURRENT_CHANGELOG=$(cat CHANGELOG.md) + echo "## $LATEST_VERSION (Unreleased)" > CHANGELOG.md + echo "" >> CHANGELOG.md + echo "$CURRENT_CHANGELOG" >> CHANGELOG.md + fi + ;; + + alpha) + FOOTER_FILE='footer-with-experiments.md' + PRERELEASE_VERSION=$(date +"alpha%Y%m%d") + LATEST_VERSION=$(npx -y changie@$CHANGIE_VERSION latest -r --skip-prereleases) + HUMAN_DATE=$(date +"%B %d, %Y") # Date in Janurary 1st, 2022 format + COMPLETE_VERSION="$LATEST_VERSION-$PRERELEASE_VERSION" + + npx -y changie@$CHANGIE_VERSION merge -u "## $COMPLETE_VERSION ($HUMAN_DATE)" + ;; + + beta) + LATEST_VERSION=$(npx -y changie@$CHANGIE_VERSION latest -r --skip-prereleases) + # We need to check if this is the first RC of the version + BETA_NUMBER=$(git tag -l "v$LATEST_VERSION-beta*" | wc -l) + BETA_NUMBER=$((BETA_NUMBER + 1)) + HUMAN_DATE=$(date +"%B %d, %Y") # Date in Janurary 1st, 2022 format + COMPLETE_VERSION="$LATEST_VERSION-beta$BETA_NUMBER" + + npx -y changie@$CHANGIE_VERSION merge -u "## $COMPLETE_VERSION ($HUMAN_DATE)" + ;; + + rc) + LATEST_VERSION=$(npx -y changie@$CHANGIE_VERSION latest -r --skip-prereleases) + # We need to check if this is the first RC of the version + RC_NUMBER=$(git tag -l "v$LATEST_VERSION-rc*" | wc -l) + RC_NUMBER=$((RC_NUMBER + 1)) + HUMAN_DATE=$(date +"%B %d, %Y") # Date in Janurary 1st, 2022 format + COMPLETE_VERSION="$LATEST_VERSION-rc$RC_NUMBER" + + npx -y changie@$CHANGIE_VERSION merge -u "## $COMPLETE_VERSION ($HUMAN_DATE)" + ;; + + patch) + COMPLETE_VERSION=$(npx -y changie@$CHANGIE_VERSION next patch) + npx -y changie@$CHANGIE_VERSION batch patch + npx -y changie@$CHANGIE_VERSION merge + ;; + + release) + # This is the first release of the branch, releasing the new minor version + COMPLETE_VERSION=$(npx -y changie@$CHANGIE_VERSION latest -r --skip-prereleases) + # We currently keep a file that looks like this release to ensure the alphas and dev versions are generated correctly + rm ./.changes/$COMPLETE_VERSION.md + + npx -y changie@$CHANGIE_VERSION batch $COMPLETE_VERSION + npx -y changie@$CHANGIE_VERSION merge + ;; + + *) + echo "invalid argument" + usage + exit 1 + + ;; + esac + + # Set version/VERSION to the to be released version + echo "$COMPLETE_VERSION" > version/VERSION + + # Add footer to the changelog + cat ./.changes/$FOOTER_FILE >> CHANGELOG.md + echo "" >> CHANGELOG.md + cat ./.changes/previous-releases.md >> CHANGELOG.md +} + +# This function expects the current branch to be main. Run it if you want to set main to the next +# minor version. +function nextminor { + # Prepend the latest version to the previous releases + LATEST_VERSION=$(npx -y changie@$CHANGIE_VERSION latest -r --skip-prereleases) + LATEST_VERSION=${LATEST_VERSION%.*} # Remove the patch version + CURRENT_FILE_CONTENT=$(cat ./.changes/previous-releases.md) + echo "- [v$LATEST_VERSION](https://github.com/hashicorp/terraform/blob/v$LATEST_VERSION/CHANGELOG.md)" > ./.changes/previous-releases.md + echo "$CURRENT_FILE_CONTENT" >> ./.changes/previous-releases.md + + NEXT_VERSION=$(npx -y changie@$CHANGIE_VERSION next minor) + # Remove all existing per-release changelogs + rm ./.changes/*.*.*.md + + # Remove all old changelog entries + rm ./.changes/v*/*.yaml + + + + # Create a new empty version file for the next minor version + touch ./.changes/$NEXT_VERSION.md + + LATEST_MAJOR_MINOR=$(echo $LATEST_VERSION | awk -F. '{print $1"."$2}') + NEXT_MAJOR_MINOR=$(echo $NEXT_VERSION | awk -F. '{print $1"."$2}') + + # Create a new changes directory for the next minor version + mkdir ./.changes/v$NEXT_MAJOR_MINOR + touch ./.changes/v$NEXT_MAJOR_MINOR/.gitkeep + + # Set changies changes dir to the new version + awk "{sub(/unreleasedDir: v$LATEST_MAJOR_MINOR/, \"unreleasedDir: v$NEXT_MAJOR_MINOR\")}1" ./.changie.yaml > temp && mv temp ./.changie.yaml + generate "dev" +} + +function listIssuesInRelease() { + RELEASE_MAJOR_MINOR="${1:-}" + if [ -z "$RELEASE_MAJOR_MINOR" ]; then + echo "No release version specified" + exit 1 + fi + + # Check if yq is installed + if ! command -v yq &> /dev/null; then + echo "yq could not be found" + exit 1 + fi + + echo "Listing issues in release $RELEASE_MAJOR_MINOR" + # Loop through files in .changes/v$RELEASE_MAJOR_MINOR + for file in ./.changes/v$RELEASE_MAJOR_MINOR/*.yaml; do + ISSUE=$(cat "$file" | yq '.custom.Issue') + echo "- https://github.com/hashicorp/terraform/issues/$ISSUE" + done +} + +function main { + case "$1" in + generate) + generate "${@:2}" + ;; + + nextminor) + nextminor "${@:2}" + ;; + + listIssuesInRelease) + listIssuesInRelease "${@:2}" + ;; + + *) + usage + exit 1 + + ;; + esac +} + +main "$@" +exit $? diff --git a/scripts/copyright.sh b/scripts/copyright.sh new file mode 100755 index 0000000000..b896cddc49 --- /dev/null +++ b/scripts/copyright.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# This script checks that all files have the appropriate copyright headers, +# according to their nearest .copywrite.hcl config file. The copyright tool +# does not natively support repos with multiple licenses, so we have to +# script this ourselves. + +set -euo pipefail + +# Find all directories containing a .copywrite.hcl config file +directories=$(find . -type f -name '.copywrite.hcl' -execdir pwd \;) +args=${1:-} + +for dir in $directories; do + cd $dir && pwd && go tool github.com/hashicorp/copywrite headers $args +done diff --git a/scripts/exhaustive.sh b/scripts/exhaustive.sh index 0bf10356ff..a88f9e891d 100755 --- a/scripts/exhaustive.sh +++ b/scripts/exhaustive.sh @@ -1,7 +1,10 @@ #!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + echo "==> Checking for switch statement exhaustiveness..." # For now we're only checking a handful of packages, rather than defaulting to # everything with a skip list. -go run github.com/nishanths/exhaustive/cmd/exhaustive ./internal/command/views/json +go tool github.com/nishanths/exhaustive/cmd/exhaustive ./internal/command/views/json diff --git a/scripts/gofmtcheck.sh b/scripts/gofmtcheck.sh index 00b81a8bef..64465d9e40 100755 --- a/scripts/gofmtcheck.sh +++ b/scripts/gofmtcheck.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + # Check go fmt echo "==> Checking that code complies with go fmt requirements..." diff --git a/scripts/gogetcookie.sh b/scripts/gogetcookie.sh index 31a0d5586c..08d5189e49 100755 --- a/scripts/gogetcookie.sh +++ b/scripts/gogetcookie.sh @@ -1,3 +1,6 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + touch ~/.gitcookies chmod 0600 ~/.gitcookies diff --git a/scripts/goimportscheck.sh b/scripts/goimportscheck.sh index e53cafe54d..3aefb0b0dc 100755 --- a/scripts/goimportscheck.sh +++ b/scripts/goimportscheck.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + set -euo pipefail @@ -29,7 +32,7 @@ fi # "IFS" environment variable, but the primary place we want to run this right # now is in our "quick checks" workflow and that _does_ have a reasonably # modern version of Bash. -readarray -t target_files < <(git diff --name-only ${base_branch} --diff-filter=MA | grep "\.go" | grep -v ".pb.go") +readarray -t target_files < <(git diff --name-only ${base_branch} --diff-filter=MA | grep "\.go" | grep -v ".pb.go" | grep -v ".go-version") # NOTE: The above intentionally excludes .pb.go files because those are # generated by a tool (protoc-gen-go) which itself doesn't produce @@ -50,7 +53,7 @@ for filename in "${target_files[@]}"; do continue fi - output=$(go run golang.org/x/tools/cmd/goimports -l "${filename}") + output=$(go tool golang.org/x/tools/cmd/goimports -l "${filename}") if [[ $? -ne 0 ]]; then echo >&2 goimports failed for "$filename" exit 1 @@ -70,7 +73,7 @@ if [[ "${#incorrect_files[@]}" -gt 1 ]]; then echo >&2 ' - ' "${filename}" done - echo >&2 'Use `go run golang.org/x/tools/cmd/goimports -w -l` on each of these files to update these files.' + echo >&2 'Use `go tool golang.org/x/tools/cmd/goimports -w -l` on each of these files to update these files.' exit 1 fi diff --git a/scripts/staticcheck.sh b/scripts/staticcheck.sh index 2ef394280f..2ec786a9de 100755 --- a/scripts/staticcheck.sh +++ b/scripts/staticcheck.sh @@ -1,4 +1,7 @@ #!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + echo "==> Checking that code complies with static analysis requirements..." # Skip legacy code which is frozen, and can be removed once we can refactor the @@ -10,9 +13,6 @@ skip=$skip"|internal/planproto|internal/tfplugin5|internal/tfplugin6" packages=$(go list ./... | egrep -v ${skip}) -# We are skipping style-related checks, since terraform intentionally breaks -# some of these. The goal here is to find issues that reduce code clarity, or -# may result in bugs. We also disable fucntion deprecation checks (SA1019) -# because our policy is to update deprecated calls locally while making other -# nearby changes, rather than to make cross-cutting changes to update them all. -go run honnef.co/go/tools/cmd/staticcheck -checks 'all,-SA1019,-ST*' ${packages} +# Note that we globally disable some checks. The list is controlled by the +# top-level staticcheck.conf file in this repo. +go tool honnef.co/go/tools/cmd/staticcheck ${packages} diff --git a/scripts/syncdeps.sh b/scripts/syncdeps.sh new file mode 100755 index 0000000000..0a8548e1a3 --- /dev/null +++ b/scripts/syncdeps.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +# This repository contains one logical codebase but as an implementation detail +# it's split into multiple modules at the boundaries of code ownership, so +# that we can keep track of which dependency changes might affect which +# components. +# +# This script runs "go mod tidy" in each module to synchronize any dependency +# updates that were made in any one of the modules. + +set -eufo pipefail + +# We'll do our work in the root of the repository, which is the parent +# directory of where this script is. +cd "$( dirname "${BASH_SOURCE[0]}" )/.." + +# We need to make sure the root go.mod is synchronized first, because otherwise +# the "go list" command below might fail. +go mod tidy + +# Each of the modules starting at our root gets its go.mod and go.sum +# synchronized, so that we can see which components are affected by an +# update and therefore which codeowners might be interested in the change. +for dir in $(go list -m -f '{{.Dir}}' github.com/hashicorp/terraform/...); do + (cd $dir && go mod tidy) +done diff --git a/scripts/version-bump.sh b/scripts/version-bump.sh new file mode 100755 index 0000000000..b75843d4af --- /dev/null +++ b/scripts/version-bump.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + + +set -uo pipefail + +function usage { + cat <<-'EOF' +Usage: ./version-bump.sh + +Description: + This script will update the version/VERSION file with the given version. +EOF +} + +function update_version { + VERSION="${1:-}" + + if [[ -z "$VERSION" ]]; then + echo "missing at least one of [] arguments" + usage + exit 1 + fi + + echo "$VERSION" > version/VERSION +} + +update_version "$@" +exit $? diff --git a/signal_unix.go b/signal_unix.go index 5e742b34ba..77003db909 100644 --- a/signal_unix.go +++ b/signal_unix.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build !windows // +build !windows diff --git a/signal_windows.go b/signal_windows.go index d5d2a8a80b..219e261c02 100644 --- a/signal_windows.go +++ b/signal_windows.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build windows // +build windows diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000000..20851da807 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,7 @@ +# Our goal in using staticcheck is to find issues that reduce code clarity, or +# may result in bugs. We are skipping style-related checks, since terraform +# intentionally breaks some of these. We also disable function deprecation +# checks (SA1019) because our policy is to update deprecated calls locally while +# making other nearby changes, rather than to make cross-cutting changes to +# update them all. +checks = ["all", "-SA1019", "-ST*"] diff --git a/telemetry.go b/telemetry.go new file mode 100644 index 0000000000..e19bcde87a --- /dev/null +++ b/telemetry.go @@ -0,0 +1,90 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + "context" + "os" + + "github.com/hashicorp/terraform/version" + "go.opentelemetry.io/contrib/exporters/autoexport" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.opentelemetry.io/otel/trace" +) + +// If this environment variable is set to "otlp" when running Terraform CLI +// then we'll enable an experimental OTLP trace exporter. +// +// BEWARE! This is not a committed external interface. +// +// Everything about this is experimental and subject to change in future +// releases. Do not depend on anything about the structure of this output. +// This mechanism might be removed altogether if a different strategy seems +// better based on experience with this experiment. +const openTelemetryExporterEnvVar = "OTEL_TRACES_EXPORTER" + +// tracer is the OpenTelemetry tracer to use for traces in package main only. +var tracer trace.Tracer + +func init() { + tracer = otel.Tracer("github.com/hashicorp/terraform") +} + +// openTelemetryInit initializes the optional OpenTelemetry exporter. +// +// By default we don't export telemetry information at all, since Terraform is +// a CLI tool and so we don't assume we're running in an environment with +// a telemetry collector available. +// +// However, for those running Terraform in automation we allow setting +// the standard OpenTelemetry environment variable OTEL_TRACES_EXPORTER=otlp +// to enable an OTLP exporter, which is in turn configured by all of the +// standard OTLP exporter environment variables: +// +// https://opentelemetry.io/docs/specs/otel/protocol/exporter/#configuration-options +// +// We don't currently support any other telemetry export protocols, because +// OTLP has emerged as a de-facto standard and each other exporter we support +// means another relatively-heavy external dependency. OTLP happens to use +// protocol buffers and gRPC, which Terraform would depend on for other reasons +// anyway. +func openTelemetryInit() error { + // We'll check the environment variable ourselves first, because the + // "autoexport" helper we're about to use is built under the assumption + // that exporting should always be enabled and so will expect to find + // an OTLP server on localhost if no environment variables are set at all. + if os.Getenv(openTelemetryExporterEnvVar) != "otlp" { + return nil // By default we just discard all telemetry calls + } + + otelResource := resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNameKey.String("Terraform CLI"), + semconv.ServiceVersionKey.String(version.Version), + ) + + // If the environment variable was set to explicitly enable telemetry + // then we'll enable it, using the "autoexport" library to automatically + // handle the details based on the other OpenTelemetry standard environment + // variables. + exp, err := autoexport.NewSpanExporter(context.Background()) + if err != nil { + return err + } + sp := sdktrace.NewSimpleSpanProcessor(exp) + provider := sdktrace.NewTracerProvider( + sdktrace.WithSpanProcessor(sp), + sdktrace.WithResource(otelResource), + ) + otel.SetTracerProvider(provider) + + pgtr := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) + otel.SetTextMapPropagator(pgtr) + + return nil +} diff --git a/testing/equivalence-tests/README.md b/testing/equivalence-tests/README.md new file mode 100644 index 0000000000..e6e812f7e7 --- /dev/null +++ b/testing/equivalence-tests/README.md @@ -0,0 +1,46 @@ +# Equivalence testing + +This directory contains the test cases for the equivalence testing. The +Terraform equivalence tests are E2E tests that are used to verify that the +output of Terraform commands doesn't change in unexpected ways. The tests are +run by comparing the output of the Terraform commands before and after a change +to the codebase. + +## Running the tests + +The equivalence tests are executed by the Terraform equivalence testing +framework. This is built in [github.com/hashicorp/terraform-equivalence-testing](https://github.com/hashicorp/terraform-equivalence-testing). + +To execute the tests you must download the `terraform-equivalence-testing` +binary and execute either the `diff` or `update` command. The `diff` command +will run the tests and output the differences between the current and previous +run. The `update` command will run the tests and update the reference output +files. + +You can also execute the tests directly using the `equivalence-tests-manual` +GitHub action. This action will run the tests against a given branch and +open a PR with the results. + +## Automated testing + +The equivalence tests are run automatically by the Terraform CI system. The +tests are run when every pull request is opened and when every pull request +is closed. + +When pull requests are opened, the tests run the diff command and will comment +on the PR with the results. PR authors should validate any changes to the output +files and make sure that the changes are expected. + +When pull requests are closed, the tests run the update command and open a new +PR with the updated reference output files. PR authors should review the changes +and make sure that the changes are expected before merging the automated PR. + +If the framework detects no changes, the process should be invisible to the PR +author. No comments will be made on the PR and no new PRs will be opened. + +## Writing new tests + +New tests should be written into the `tests` directory. Each test should be +written in a separate directory and should follow the guidelines in the +equivalence testing framework documentation. Any tests added to this directory +will be picked up the CI system and run automatically. diff --git a/testing/equivalence-tests/outputs/basic_json_string_update/apply.json b/testing/equivalence-tests/outputs/basic_json_string_update/apply.json new file mode 100644 index 0000000000..b710e86a09 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_json_string_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_simple_resource.json: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_simple_resource.json", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.json", + "resource_key": null, + "resource_name": "json", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.json: Modifying... [id=5a3fd9b3-e852-8956-8c0a-255d47eda645]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "resource": { + "addr": "tfcoremock_simple_resource.json", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.json", + "resource_key": null, + "resource_name": "json", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "resource": { + "addr": "tfcoremock_simple_resource.json", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.json", + "resource_key": null, + "resource_name": "json", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_json_string_update/plan b/testing/equivalence-tests/outputs/basic_json_string_update/plan new file mode 100644 index 0000000000..fb5ba9f58c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_json_string_update/plan @@ -0,0 +1,37 @@ +tfcoremock_simple_resource.json: Refreshing state... [id=5a3fd9b3-e852-8956-8c0a-255d47eda645] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.json will be updated in-place + ~ resource "tfcoremock_simple_resource" "json" { + id = "5a3fd9b3-e852-8956-8c0a-255d47eda645" + ~ string = jsonencode( + ~ { + ~ list-attribute = [ + "one", + ~ "two" -> "four", + "three", + ] + ~ object-attribute = { + + key_four = "value_three" + ~ key_three = "value_three" -> "value_two" + - key_two = "value_two" + # (1 unchanged attribute hidden) + } + ~ string-attribute = "string" -> "a new string" + } + ) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_json_string_update/plan.json b/testing/equivalence-tests/outputs/basic_json_string_update/plan.json new file mode 100644 index 0000000000..851574f779 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_json_string_update/plan.json @@ -0,0 +1,114 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.json", + "expressions": { + "string": { + "constant_value": "{\"list-attribute\":[\"one\",\"four\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_three\":\"value_two\", \"key_four\":\"value_three\"},\"string-attribute\":\"a new string\"}" + } + }, + "mode": "managed", + "name": "json", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.json", + "mode": "managed", + "name": "json", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "integer": null, + "number": null, + "string": "{\"list-attribute\":[\"one\",\"four\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_three\":\"value_two\", \"key_four\":\"value_three\"},\"string-attribute\":\"a new string\"}" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.json", + "mode": "managed", + "name": "json", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "integer": null, + "number": null, + "string": "{\"list-attribute\":[\"one\",\"two\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_two\":\"value_two\",\"key_three\":\"value_three\"},\"string-attribute\":\"string\"}" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.json", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "integer": null, + "number": null, + "string": "{\"list-attribute\":[\"one\",\"four\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_three\":\"value_two\", \"key_four\":\"value_three\"},\"string-attribute\":\"a new string\"}" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "integer": null, + "number": null, + "string": "{\"list-attribute\":[\"one\",\"two\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_two\":\"value_two\",\"key_three\":\"value_three\"},\"string-attribute\":\"string\"}" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "json", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_json_string_update/state b/testing/equivalence-tests/outputs/basic_json_string_update/state new file mode 100644 index 0000000000..73038c1f72 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_json_string_update/state @@ -0,0 +1,19 @@ +# tfcoremock_simple_resource.json: +resource "tfcoremock_simple_resource" "json" { + id = "5a3fd9b3-e852-8956-8c0a-255d47eda645" + string = jsonencode( + { + list-attribute = [ + "one", + "four", + "three", + ] + object-attribute = { + key_four = "value_three" + key_one = "value_one" + key_three = "value_two" + } + string-attribute = "a new string" + } + ) +} diff --git a/testing/equivalence-tests/outputs/basic_json_string_update/state.json b/testing/equivalence-tests/outputs/basic_json_string_update/state.json new file mode 100644 index 0000000000..67820818ef --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_json_string_update/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.json", + "mode": "managed", + "name": "json", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "integer": null, + "number": null, + "string": "{\"list-attribute\":[\"one\",\"four\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_three\":\"value_two\", \"key_four\":\"value_three\"},\"string-attribute\":\"a new string\"}" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list/apply.json b/testing/equivalence-tests/outputs/basic_list/apply.json new file mode 100644 index 0000000000..bafeb55dd4 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_list.list: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_list.list: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list/plan b/testing/equivalence-tests/outputs/basic_list/plan new file mode 100644 index 0000000000..dee4008613 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list/plan @@ -0,0 +1,25 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_list.list will be created + + resource "tfcoremock_list" "list" { + + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + + list = [ + + "9C2BE420-042D-440A-96E9-75565341C994", + + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + ] + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_list/plan.json b/testing/equivalence-tests/outputs/basic_list/plan.json new file mode 100644 index 0000000000..84be55a5fa --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list/plan.json @@ -0,0 +1,100 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "expressions": { + "id": { + "constant_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + }, + "list": { + "constant_value": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + } + }, + "mode": "managed", + "name": "list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_list.list", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "after_sensitive": { + "list": [ + false, + false, + false + ] + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list/state b/testing/equivalence-tests/outputs/basic_list/state new file mode 100644 index 0000000000..a4d698867c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list/state @@ -0,0 +1,9 @@ +# tfcoremock_list.list: +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + list = [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + ] +} diff --git a/testing/equivalence-tests/outputs/basic_list/state.json b/testing/equivalence-tests/outputs/basic_list/state.json new file mode 100644 index 0000000000..4e3ccc9084 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list/state.json @@ -0,0 +1,32 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_empty/apply.json b/testing/equivalence-tests/outputs/basic_list_empty/apply.json new file mode 100644 index 0000000000..a89c63e935 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_empty/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_list.list: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_list.list: Modifying... [id=985820B3-ACF9-4F00-94AD-F81C5EA33663]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_empty/plan b/testing/equivalence-tests/outputs/basic_list_empty/plan new file mode 100644 index 0000000000..8f4de24a1d --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_empty/plan @@ -0,0 +1,26 @@ +tfcoremock_list.list: Refreshing state... [id=985820B3-ACF9-4F00-94AD-F81C5EA33663] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_list.list will be updated in-place + ~ resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + ~ list = [ + - "9C2BE420-042D-440A-96E9-75565341C994", + - "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + - "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_list_empty/plan.json b/testing/equivalence-tests/outputs/basic_list_empty/plan.json new file mode 100644 index 0000000000..ca5704a833 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_empty/plan.json @@ -0,0 +1,125 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "expressions": { + "id": { + "constant_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + }, + "list": { + "constant_value": [] + } + }, + "mode": "managed", + "name": "list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_list.list", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [] + }, + "after_sensitive": { + "list": [] + }, + "after_unknown": {}, + "before": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "before_sensitive": { + "list": [ + false, + false, + false + ] + } + }, + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_empty/state b/testing/equivalence-tests/outputs/basic_list_empty/state new file mode 100644 index 0000000000..71e4d83d37 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_empty/state @@ -0,0 +1,5 @@ +# tfcoremock_list.list: +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + list = [] +} diff --git a/testing/equivalence-tests/outputs/basic_list_empty/state.json b/testing/equivalence-tests/outputs/basic_list_empty/state.json new file mode 100644 index 0000000000..c538681578 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_empty/state.json @@ -0,0 +1,24 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_null/apply.json b/testing/equivalence-tests/outputs/basic_list_null/apply.json new file mode 100644 index 0000000000..a89c63e935 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_null/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_list.list: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_list.list: Modifying... [id=985820B3-ACF9-4F00-94AD-F81C5EA33663]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_null/plan b/testing/equivalence-tests/outputs/basic_list_null/plan new file mode 100644 index 0000000000..a9f8617c0d --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_null/plan @@ -0,0 +1,26 @@ +tfcoremock_list.list: Refreshing state... [id=985820B3-ACF9-4F00-94AD-F81C5EA33663] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_list.list will be updated in-place + ~ resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + - list = [ + - "9C2BE420-042D-440A-96E9-75565341C994", + - "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + - "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + ] -> null + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_list_null/plan.json b/testing/equivalence-tests/outputs/basic_list_null/plan.json new file mode 100644 index 0000000000..80e9438539 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_null/plan.json @@ -0,0 +1,118 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "expressions": { + "id": { + "constant_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + } + }, + "mode": "managed", + "name": "list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": null + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_list.list", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": null + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "before_sensitive": { + "list": [ + false, + false, + false + ] + } + }, + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_null/state b/testing/equivalence-tests/outputs/basic_list_null/state new file mode 100644 index 0000000000..5e0291df7b --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_null/state @@ -0,0 +1,4 @@ +# tfcoremock_list.list: +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" +} diff --git a/testing/equivalence-tests/outputs/basic_list_null/state.json b/testing/equivalence-tests/outputs/basic_list_null/state.json new file mode 100644 index 0000000000..79251ad8f4 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_null/state.json @@ -0,0 +1,22 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_update/apply.json b/testing/equivalence-tests/outputs/basic_list_update/apply.json new file mode 100644 index 0000000000..a89c63e935 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_list.list: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_list.list: Modifying... [id=985820B3-ACF9-4F00-94AD-F81C5EA33663]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_update/plan b/testing/equivalence-tests/outputs/basic_list_update/plan new file mode 100644 index 0000000000..e17c34087f --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_update/plan @@ -0,0 +1,26 @@ +tfcoremock_list.list: Refreshing state... [id=985820B3-ACF9-4F00-94AD-F81C5EA33663] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_list.list will be updated in-place + ~ resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + ~ list = [ + "9C2BE420-042D-440A-96E9-75565341C994", + ~ "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1" -> "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + ~ "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" -> "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0", + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_list_update/plan.json b/testing/equivalence-tests/outputs/basic_list_update/plan.json new file mode 100644 index 0000000000..1b8c00fa0a --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_update/plan.json @@ -0,0 +1,145 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "expressions": { + "id": { + "constant_value": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + }, + "list": { + "constant_value": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0" + ] + } + }, + "mode": "managed", + "name": "list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0" + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_list.list", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0" + ] + }, + "after_sensitive": { + "list": [ + false, + false, + false + ] + }, + "after_unknown": {}, + "before": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "before_sensitive": { + "list": [ + false, + false, + false + ] + } + }, + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_list_update/state b/testing/equivalence-tests/outputs/basic_list_update/state new file mode 100644 index 0000000000..4df3a39a2d --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_update/state @@ -0,0 +1,9 @@ +# tfcoremock_list.list: +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + list = [ + "9C2BE420-042D-440A-96E9-75565341C994", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0", + ] +} diff --git a/testing/equivalence-tests/outputs/basic_list_update/state.json b/testing/equivalence-tests/outputs/basic_list_update/state.json new file mode 100644 index 0000000000..e6c1ae48d1 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_list_update/state.json @@ -0,0 +1,32 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + false, + false, + false + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0" + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map/apply.json b/testing/equivalence-tests/outputs/basic_map/apply.json new file mode 100644 index 0000000000..2a4475249b --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_map.map: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_map.map: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map/plan b/testing/equivalence-tests/outputs/basic_map/plan new file mode 100644 index 0000000000..d4b18d5471 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map/plan @@ -0,0 +1,25 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_map.map will be created + + resource "tfcoremock_map" "map" { + + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + + map = { + + "one" = "682672C7-0918-4448-8342-887BAE01062A" + + "two" = "212FFBF6-40FE-4862-B708-E6AA508E84E0" + + "zero" = "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_map/plan.json b/testing/equivalence-tests/outputs/basic_map/plan.json new file mode 100644 index 0000000000..0cb676d4db --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map/plan.json @@ -0,0 +1,92 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "expressions": { + "id": { + "constant_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "constant_value": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + }, + "mode": "managed", + "name": "map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_map.map", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "after_sensitive": { + "map": {} + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map/state b/testing/equivalence-tests/outputs/basic_map/state new file mode 100644 index 0000000000..3ef5b140c1 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map/state @@ -0,0 +1,9 @@ +# tfcoremock_map.map: +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + map = { + "one" = "682672C7-0918-4448-8342-887BAE01062A" + "two" = "212FFBF6-40FE-4862-B708-E6AA508E84E0" + "zero" = "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } +} diff --git a/testing/equivalence-tests/outputs/basic_map/state.json b/testing/equivalence-tests/outputs/basic_map/state.json new file mode 100644 index 0000000000..57d858cd11 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map/state.json @@ -0,0 +1,28 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_empty/apply.json b/testing/equivalence-tests/outputs/basic_map_empty/apply.json new file mode 100644 index 0000000000..f5ae7b1d60 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_empty/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_map.map: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_map.map: Modifying... [id=50E1A46E-E64A-4C1F-881C-BA85A5440964]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_empty/plan b/testing/equivalence-tests/outputs/basic_map_empty/plan new file mode 100644 index 0000000000..0eae681e96 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_empty/plan @@ -0,0 +1,26 @@ +tfcoremock_map.map: Refreshing state... [id=50E1A46E-E64A-4C1F-881C-BA85A5440964] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_map.map will be updated in-place + ~ resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + ~ map = { + - "one" = "682672C7-0918-4448-8342-887BAE01062A" -> null + - "two" = "212FFBF6-40FE-4862-B708-E6AA508E84E0" -> null + - "zero" = "6B044AF7-172B-495B-BE11-B9546C12C3BD" -> null + } + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_map_empty/plan.json b/testing/equivalence-tests/outputs/basic_map_empty/plan.json new file mode 100644 index 0000000000..58ccb7db9a --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_empty/plan.json @@ -0,0 +1,117 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "expressions": { + "id": { + "constant_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "constant_value": {} + } + }, + "mode": "managed", + "name": "map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": {} + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_map.map", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": {} + }, + "after_sensitive": { + "map": {} + }, + "after_unknown": {}, + "before": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "before_sensitive": { + "map": {} + } + }, + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_empty/state b/testing/equivalence-tests/outputs/basic_map_empty/state new file mode 100644 index 0000000000..c658bfa75a --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_empty/state @@ -0,0 +1,5 @@ +# tfcoremock_map.map: +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + map = {} +} diff --git a/testing/equivalence-tests/outputs/basic_map_empty/state.json b/testing/equivalence-tests/outputs/basic_map_empty/state.json new file mode 100644 index 0000000000..28d17fd2a9 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_empty/state.json @@ -0,0 +1,24 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": {} + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_null/apply.json b/testing/equivalence-tests/outputs/basic_map_null/apply.json new file mode 100644 index 0000000000..f5ae7b1d60 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_null/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_map.map: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_map.map: Modifying... [id=50E1A46E-E64A-4C1F-881C-BA85A5440964]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_null/plan b/testing/equivalence-tests/outputs/basic_map_null/plan new file mode 100644 index 0000000000..4fbdf38143 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_null/plan @@ -0,0 +1,26 @@ +tfcoremock_map.map: Refreshing state... [id=50E1A46E-E64A-4C1F-881C-BA85A5440964] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_map.map will be updated in-place + ~ resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + - map = { + - "one" = "682672C7-0918-4448-8342-887BAE01062A" + - "two" = "212FFBF6-40FE-4862-B708-E6AA508E84E0" + - "zero" = "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } -> null + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_map_null/plan.json b/testing/equivalence-tests/outputs/basic_map_null/plan.json new file mode 100644 index 0000000000..c8aeeab72c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_null/plan.json @@ -0,0 +1,110 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "expressions": { + "id": { + "constant_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + } + }, + "mode": "managed", + "name": "map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": null + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_map.map", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": null + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "before_sensitive": { + "map": {} + } + }, + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_null/state b/testing/equivalence-tests/outputs/basic_map_null/state new file mode 100644 index 0000000000..99c5a94887 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_null/state @@ -0,0 +1,4 @@ +# tfcoremock_map.map: +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" +} diff --git a/testing/equivalence-tests/outputs/basic_map_null/state.json b/testing/equivalence-tests/outputs/basic_map_null/state.json new file mode 100644 index 0000000000..440dc72720 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_null/state.json @@ -0,0 +1,22 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_update/apply.json b/testing/equivalence-tests/outputs/basic_map_update/apply.json new file mode 100644 index 0000000000..f5ae7b1d60 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_map.map: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_map.map: Modifying... [id=50E1A46E-E64A-4C1F-881C-BA85A5440964]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_update/plan b/testing/equivalence-tests/outputs/basic_map_update/plan new file mode 100644 index 0000000000..122d000556 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_update/plan @@ -0,0 +1,26 @@ +tfcoremock_map.map: Refreshing state... [id=50E1A46E-E64A-4C1F-881C-BA85A5440964] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_map.map will be updated in-place + ~ resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + ~ map = { + + "four" = "D820D482-7C2C-4EF3-8935-863168A193F9" + - "one" = "682672C7-0918-4448-8342-887BAE01062A" -> null + # (2 unchanged elements hidden) + } + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_map_update/plan.json b/testing/equivalence-tests/outputs/basic_map_update/plan.json new file mode 100644 index 0000000000..39ecd07d4c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_update/plan.json @@ -0,0 +1,129 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "expressions": { + "id": { + "constant_value": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "constant_value": { + "four": "D820D482-7C2C-4EF3-8935-863168A193F9", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + }, + "mode": "managed", + "name": "map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "four": "D820D482-7C2C-4EF3-8935-863168A193F9", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_map.map", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "four": "D820D482-7C2C-4EF3-8935-863168A193F9", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "after_sensitive": { + "map": {} + }, + "after_unknown": {}, + "before": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "before_sensitive": { + "map": {} + } + }, + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_map_update/state b/testing/equivalence-tests/outputs/basic_map_update/state new file mode 100644 index 0000000000..e58fcff075 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_update/state @@ -0,0 +1,9 @@ +# tfcoremock_map.map: +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + map = { + "four" = "D820D482-7C2C-4EF3-8935-863168A193F9" + "two" = "212FFBF6-40FE-4862-B708-E6AA508E84E0" + "zero" = "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } +} diff --git a/testing/equivalence-tests/outputs/basic_map_update/state.json b/testing/equivalence-tests/outputs/basic_map_update/state.json new file mode 100644 index 0000000000..ee8cdb6019 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_map_update/state.json @@ -0,0 +1,28 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": {} + }, + "type": "tfcoremock_map", + "values": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "four": "D820D482-7C2C-4EF3-8935-863168A193F9", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_multiline_string_update/apply.json b/testing/equivalence-tests/outputs/basic_multiline_string_update/apply.json new file mode 100644 index 0000000000..28219a446c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_multiline_string_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_simple_resource.multiline: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_simple_resource.multiline", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.multiline", + "resource_key": null, + "resource_name": "multiline", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.multiline: Modifying... [id=69fe5233-e77a-804f-0dac-115c949540bc]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "69fe5233-e77a-804f-0dac-115c949540bc", + "resource": { + "addr": "tfcoremock_simple_resource.multiline", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.multiline", + "resource_key": null, + "resource_name": "multiline", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "69fe5233-e77a-804f-0dac-115c949540bc", + "resource": { + "addr": "tfcoremock_simple_resource.multiline", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.multiline", + "resource_key": null, + "resource_name": "multiline", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_multiline_string_update/plan b/testing/equivalence-tests/outputs/basic_multiline_string_update/plan new file mode 100644 index 0000000000..665d430b20 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_multiline_string_update/plan @@ -0,0 +1,31 @@ +tfcoremock_simple_resource.multiline: Refreshing state... [id=69fe5233-e77a-804f-0dac-115c949540bc] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.multiline will be updated in-place + ~ resource "tfcoremock_simple_resource" "multiline" { + id = "69fe5233-e77a-804f-0dac-115c949540bc" + ~ string = <<-EOT + one + - two + three + + two + four + - five + + six + + seven + EOT + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_multiline_string_update/plan.json b/testing/equivalence-tests/outputs/basic_multiline_string_update/plan.json new file mode 100644 index 0000000000..dcf819199d --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_multiline_string_update/plan.json @@ -0,0 +1,114 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.multiline", + "expressions": { + "string": { + "constant_value": "one\nthree\ntwo\nfour\nsix\nseven" + } + }, + "mode": "managed", + "name": "multiline", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.multiline", + "mode": "managed", + "name": "multiline", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "69fe5233-e77a-804f-0dac-115c949540bc", + "integer": null, + "number": null, + "string": "one\nthree\ntwo\nfour\nsix\nseven" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.multiline", + "mode": "managed", + "name": "multiline", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "69fe5233-e77a-804f-0dac-115c949540bc", + "integer": null, + "number": null, + "string": "one\ntwo\nthree\nfour\nfive" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.multiline", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "69fe5233-e77a-804f-0dac-115c949540bc", + "integer": null, + "number": null, + "string": "one\nthree\ntwo\nfour\nsix\nseven" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "69fe5233-e77a-804f-0dac-115c949540bc", + "integer": null, + "number": null, + "string": "one\ntwo\nthree\nfour\nfive" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "multiline", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_multiline_string_update/state b/testing/equivalence-tests/outputs/basic_multiline_string_update/state new file mode 100644 index 0000000000..18891b11f0 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_multiline_string_update/state @@ -0,0 +1,12 @@ +# tfcoremock_simple_resource.multiline: +resource "tfcoremock_simple_resource" "multiline" { + id = "69fe5233-e77a-804f-0dac-115c949540bc" + string = <<-EOT + one + three + two + four + six + seven + EOT +} diff --git a/testing/equivalence-tests/outputs/basic_multiline_string_update/state.json b/testing/equivalence-tests/outputs/basic_multiline_string_update/state.json new file mode 100644 index 0000000000..1ee0ace0bc --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_multiline_string_update/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.multiline", + "mode": "managed", + "name": "multiline", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "69fe5233-e77a-804f-0dac-115c949540bc", + "integer": null, + "number": null, + "string": "one\nthree\ntwo\nfour\nsix\nseven" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set/apply.json b/testing/equivalence-tests/outputs/basic_set/apply.json new file mode 100644 index 0000000000..467e2696de --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_set.set: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_set.set: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set/plan b/testing/equivalence-tests/outputs/basic_set/plan new file mode 100644 index 0000000000..f6629e419f --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set/plan @@ -0,0 +1,25 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_set.set will be created + + resource "tfcoremock_set" "set" { + + id = "046952C9-B832-4106-82C0-C217F7C73E18" + + set = [ + + "41471135-E14C-4946-BFA4-2626C7E2A94A", + + "C04762B9-D07B-40FE-A92B-B72AD342658D", + + "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + ] + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_set/plan.json b/testing/equivalence-tests/outputs/basic_set/plan.json new file mode 100644 index 0000000000..eddd4b5e39 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set/plan.json @@ -0,0 +1,100 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "expressions": { + "id": { + "constant_value": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "constant_value": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + }, + "mode": "managed", + "name": "set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_set.set", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "after_sensitive": { + "set": [ + false, + false, + false + ] + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set/state b/testing/equivalence-tests/outputs/basic_set/state new file mode 100644 index 0000000000..7e2b27775b --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set/state @@ -0,0 +1,9 @@ +# tfcoremock_set.set: +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + set = [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + ] +} diff --git a/testing/equivalence-tests/outputs/basic_set/state.json b/testing/equivalence-tests/outputs/basic_set/state.json new file mode 100644 index 0000000000..b869f57555 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set/state.json @@ -0,0 +1,32 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_empty/apply.json b/testing/equivalence-tests/outputs/basic_set_empty/apply.json new file mode 100644 index 0000000000..41bc5375d2 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_empty/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_set.set: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_set.set: Modifying... [id=046952C9-B832-4106-82C0-C217F7C73E18]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_empty/plan b/testing/equivalence-tests/outputs/basic_set_empty/plan new file mode 100644 index 0000000000..9953cff989 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_empty/plan @@ -0,0 +1,26 @@ +tfcoremock_set.set: Refreshing state... [id=046952C9-B832-4106-82C0-C217F7C73E18] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_set.set will be updated in-place + ~ resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + ~ set = [ + - "41471135-E14C-4946-BFA4-2626C7E2A94A", + - "C04762B9-D07B-40FE-A92B-B72AD342658D", + - "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_set_empty/plan.json b/testing/equivalence-tests/outputs/basic_set_empty/plan.json new file mode 100644 index 0000000000..297996972e --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_empty/plan.json @@ -0,0 +1,125 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "expressions": { + "id": { + "constant_value": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "constant_value": [] + } + }, + "mode": "managed", + "name": "set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_set.set", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [] + }, + "after_sensitive": { + "set": [] + }, + "after_unknown": {}, + "before": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "before_sensitive": { + "set": [ + false, + false, + false + ] + } + }, + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_empty/state b/testing/equivalence-tests/outputs/basic_set_empty/state new file mode 100644 index 0000000000..dc1835e81c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_empty/state @@ -0,0 +1,5 @@ +# tfcoremock_set.set: +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + set = [] +} diff --git a/testing/equivalence-tests/outputs/basic_set_empty/state.json b/testing/equivalence-tests/outputs/basic_set_empty/state.json new file mode 100644 index 0000000000..3ad076ed83 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_empty/state.json @@ -0,0 +1,24 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_null/apply.json b/testing/equivalence-tests/outputs/basic_set_null/apply.json new file mode 100644 index 0000000000..41bc5375d2 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_null/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_set.set: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_set.set: Modifying... [id=046952C9-B832-4106-82C0-C217F7C73E18]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_null/plan b/testing/equivalence-tests/outputs/basic_set_null/plan new file mode 100644 index 0000000000..37c9921403 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_null/plan @@ -0,0 +1,26 @@ +tfcoremock_set.set: Refreshing state... [id=046952C9-B832-4106-82C0-C217F7C73E18] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_set.set will be updated in-place + ~ resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + - set = [ + - "41471135-E14C-4946-BFA4-2626C7E2A94A", + - "C04762B9-D07B-40FE-A92B-B72AD342658D", + - "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + ] -> null + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_set_null/plan.json b/testing/equivalence-tests/outputs/basic_set_null/plan.json new file mode 100644 index 0000000000..929b2f33ed --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_null/plan.json @@ -0,0 +1,118 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "expressions": { + "id": { + "constant_value": "046952C9-B832-4106-82C0-C217F7C73E18" + } + }, + "mode": "managed", + "name": "set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": null + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_set.set", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": null + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "before_sensitive": { + "set": [ + false, + false, + false + ] + } + }, + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_null/state b/testing/equivalence-tests/outputs/basic_set_null/state new file mode 100644 index 0000000000..192cb7097e --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_null/state @@ -0,0 +1,4 @@ +# tfcoremock_set.set: +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" +} diff --git a/testing/equivalence-tests/outputs/basic_set_null/state.json b/testing/equivalence-tests/outputs/basic_set_null/state.json new file mode 100644 index 0000000000..299974c474 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_null/state.json @@ -0,0 +1,22 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_update/apply.json b/testing/equivalence-tests/outputs/basic_set_update/apply.json new file mode 100644 index 0000000000..41bc5375d2 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_set.set: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_set.set: Modifying... [id=046952C9-B832-4106-82C0-C217F7C73E18]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "046952C9-B832-4106-82C0-C217F7C73E18", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_update/plan b/testing/equivalence-tests/outputs/basic_set_update/plan new file mode 100644 index 0000000000..ad8fbb7e9c --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_update/plan @@ -0,0 +1,26 @@ +tfcoremock_set.set: Refreshing state... [id=046952C9-B832-4106-82C0-C217F7C73E18] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_set.set will be updated in-place + ~ resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + ~ set = [ + - "C04762B9-D07B-40FE-A92B-B72AD342658D", + + "1769B76E-12F0-4214-A864-E843EB23B64E", + # (2 unchanged elements hidden) + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/basic_set_update/plan.json b/testing/equivalence-tests/outputs/basic_set_update/plan.json new file mode 100644 index 0000000000..85be31fa3f --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_update/plan.json @@ -0,0 +1,145 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "expressions": { + "id": { + "constant_value": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "constant_value": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + "1769B76E-12F0-4214-A864-E843EB23B64E" + ] + } + }, + "mode": "managed", + "name": "set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "1769B76E-12F0-4214-A864-E843EB23B64E", + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_set.set", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "1769B76E-12F0-4214-A864-E843EB23B64E", + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "after_sensitive": { + "set": [ + false, + false, + false + ] + }, + "after_unknown": {}, + "before": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "before_sensitive": { + "set": [ + false, + false, + false + ] + } + }, + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/basic_set_update/state b/testing/equivalence-tests/outputs/basic_set_update/state new file mode 100644 index 0000000000..a1afc79fd2 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_update/state @@ -0,0 +1,9 @@ +# tfcoremock_set.set: +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + set = [ + "1769B76E-12F0-4214-A864-E843EB23B64E", + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + ] +} diff --git a/testing/equivalence-tests/outputs/basic_set_update/state.json b/testing/equivalence-tests/outputs/basic_set_update/state.json new file mode 100644 index 0000000000..ba74173e52 --- /dev/null +++ b/testing/equivalence-tests/outputs/basic_set_update/state.json @@ -0,0 +1,32 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + false, + false, + false + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "1769B76E-12F0-4214-A864-E843EB23B64E", + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/data_read/plan b/testing/equivalence-tests/outputs/data_read/plan new file mode 100644 index 0000000000..5d01597a9f --- /dev/null +++ b/testing/equivalence-tests/outputs/data_read/plan @@ -0,0 +1,45 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + <= read (data resources) + +Terraform will perform the following actions: + + # data.tfcoremock_simple_resource.read will be read during apply + # (config refers to values not yet known) + <= data "tfcoremock_simple_resource" "read" { + + id = (known after apply) + } + + # tfcoremock_simple_resource.create will be created + + resource "tfcoremock_simple_resource" "create" { + + id = (known after apply) + } + + # module.create.local_file.data_file will be created + + resource "local_file" "data_file" { + + content = (known after apply) + + directory_permission = "0777" + + file_permission = "0777" + + filename = (known after apply) + + id = (known after apply) + } + + # module.create.random_integer.random will be created + + resource "random_integer" "random" { + + id = (known after apply) + + max = 9999999 + + min = 1000000 + + result = (known after apply) + + seed = "F78CB410-BA01-44E1-82E1-37D61F7CB158" + } + +Plan: 3 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/data_read/plan.json b/testing/equivalence-tests/outputs/data_read/plan.json new file mode 100644 index 0000000000..e348cb1318 --- /dev/null +++ b/testing/equivalence-tests/outputs/data_read/plan.json @@ -0,0 +1,332 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "local": { + "full_name": "registry.terraform.io/hashicorp/local", + "name": "local", + "version_constraint": "2.2.3" + }, + "random": { + "full_name": "registry.terraform.io/hashicorp/random", + "name": "random", + "version_constraint": "3.4.3" + }, + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "module_calls": { + "create": { + "expressions": { + "contents": { + "constant_value": "hello, world!" + } + }, + "module": { + "outputs": { + "id": { + "expression": { + "references": [ + "random_integer.random.id", + "random_integer.random" + ] + } + } + }, + "resources": [ + { + "address": "local_file.data_file", + "expressions": { + "content": { + "references": [ + "local.contents" + ] + }, + "filename": { + "references": [ + "random_integer.random.id", + "random_integer.random" + ] + } + }, + "mode": "managed", + "name": "data_file", + "provider_config_key": "local", + "schema_version": 0, + "type": "local_file" + }, + { + "address": "random_integer.random", + "expressions": { + "max": { + "constant_value": 9999999 + }, + "min": { + "constant_value": 1000000 + }, + "seed": { + "constant_value": "F78CB410-BA01-44E1-82E1-37D61F7CB158" + } + }, + "mode": "managed", + "name": "random", + "provider_config_key": "random", + "schema_version": 0, + "type": "random_integer" + } + ], + "variables": { + "contents": {} + } + }, + "source": "./create" + } + }, + "resources": [ + { + "address": "tfcoremock_simple_resource.create", + "expressions": { + "string": { + "references": [ + "data.tfcoremock_simple_resource.read.string", + "data.tfcoremock_simple_resource.read" + ] + } + }, + "mode": "managed", + "name": "create", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + }, + { + "address": "data.tfcoremock_simple_resource.read", + "depends_on": [ + "module.create" + ], + "expressions": { + "id": { + "references": [ + "module.create.id", + "module.create" + ] + } + }, + "mode": "data", + "name": "read", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "child_modules": [ + { + "address": "module.create", + "resources": [ + { + "address": "module.create.local_file.data_file", + "mode": "managed", + "name": "data_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": { + "sensitive_content": true + }, + "type": "local_file", + "values": { + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "sensitive_content": null, + "source": null + } + }, + { + "address": "module.create.random_integer.random", + "mode": "managed", + "name": "random", + "provider_name": "registry.terraform.io/hashicorp/random", + "schema_version": 0, + "sensitive_values": {}, + "type": "random_integer", + "values": { + "keepers": null, + "max": 9999999, + "min": 1000000, + "seed": "F78CB410-BA01-44E1-82E1-37D61F7CB158" + } + } + ] + } + ], + "resources": [ + { + "address": "data.tfcoremock_simple_resource.read", + "mode": "data", + "name": "read", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "integer": null, + "number": null, + "string": null + } + }, + { + "address": "tfcoremock_simple_resource.create", + "mode": "managed", + "name": "create", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "integer": null, + "number": null, + "string": null + } + } + ] + } + }, + "relevant_attributes": [ + { + "attribute": [ + "string" + ], + "resource": "data.tfcoremock_simple_resource.read" + }, + { + "attribute": [ + "id" + ], + "resource": "module.create.random_integer.random" + } + ], + "resource_changes": [ + { + "action_reason": "read_because_config_unknown", + "address": "data.tfcoremock_simple_resource.read", + "change": { + "actions": [ + "read" + ], + "after": { + "bool": null, + "float": null, + "integer": null, + "number": null, + "string": null + }, + "after_sensitive": {}, + "after_unknown": { + "id": true + }, + "before": null, + "before_sensitive": false + }, + "mode": "data", + "name": "read", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + }, + { + "address": "tfcoremock_simple_resource.create", + "change": { + "actions": [ + "create" + ], + "after": { + "bool": null, + "float": null, + "integer": null, + "number": null, + "string": null + }, + "after_sensitive": {}, + "after_unknown": { + "id": true + }, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "create", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + }, + { + "address": "module.create.local_file.data_file", + "change": { + "actions": [ + "create" + ], + "after": { + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "sensitive_content": null, + "source": null + }, + "after_sensitive": { + "sensitive_content": true + }, + "after_unknown": { + "content": true, + "filename": true, + "id": true + }, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "module_address": "module.create", + "name": "data_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "type": "local_file" + }, + { + "address": "module.create.random_integer.random", + "change": { + "actions": [ + "create" + ], + "after": { + "keepers": null, + "max": 9999999, + "min": 1000000, + "seed": "F78CB410-BA01-44E1-82E1-37D61F7CB158" + }, + "after_sensitive": {}, + "after_unknown": { + "id": true, + "result": true + }, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "module_address": "module.create", + "name": "random", + "provider_name": "registry.terraform.io/hashicorp/random", + "type": "random_integer" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_refresh_only/apply.json b/testing/equivalence-tests/outputs/drift_refresh_only/apply.json new file mode 100644 index 0000000000..42438bd2f7 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_refresh_only/apply.json @@ -0,0 +1,22 @@ +[ + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_refresh_only/plan b/testing/equivalence-tests/outputs/drift_refresh_only/plan new file mode 100644 index 0000000000..b03117055c --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_refresh_only/plan @@ -0,0 +1,25 @@ +tfcoremock_simple_resource.drift: Refreshing state... [id=cb79269e-dc39-1e68-0a9c-63cb392afda9] + +Note: Objects have changed outside of Terraform + +Terraform detected the following changes made outside of Terraform since the +last "terraform apply" which may have affected this plan: + + # tfcoremock_simple_resource.drift has changed + ~ resource "tfcoremock_simple_resource" "drift" { + id = "cb79269e-dc39-1e68-0a9c-63cb392afda9" + ~ string = "Hello, world!" -> "Hello, drift!" + } + + +This is a refresh-only plan, so Terraform will not take any actions to undo +these. If you were expecting these changes then you can apply this plan to +record the updated values in the Terraform state without changing any remote +objects. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/drift_refresh_only/plan.json b/testing/equivalence-tests/outputs/drift_refresh_only/plan.json new file mode 100644 index 0000000000..1ae0755bdd --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_refresh_only/plan.json @@ -0,0 +1,94 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "expressions": { + "string": { + "constant_value": "Hello, world!" + } + }, + "mode": "managed", + "name": "drift", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": {} + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "cb79269e-dc39-1e68-0a9c-63cb392afda9", + "integer": null, + "number": null, + "string": "Hello, drift!" + } + } + ] + } + } + }, + "resource_drift": [ + { + "address": "tfcoremock_simple_resource.drift", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "cb79269e-dc39-1e68-0a9c-63cb392afda9", + "integer": null, + "number": null, + "string": "Hello, drift!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "cb79269e-dc39-1e68-0a9c-63cb392afda9", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_refresh_only/state.json b/testing/equivalence-tests/outputs/drift_refresh_only/state.json new file mode 100644 index 0000000000..d91cc99a75 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_refresh_only/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "cb79269e-dc39-1e68-0a9c-63cb392afda9", + "integer": null, + "number": null, + "string": "Hello, drift!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_relevant_attributes/apply.json b/testing/equivalence-tests/outputs/drift_relevant_attributes/apply.json new file mode 100644 index 0000000000..463280a087 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_relevant_attributes/apply.json @@ -0,0 +1,136 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_simple_resource.base: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_simple_resource.base", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base", + "resource_key": null, + "resource_name": "base", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.dependent: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_simple_resource.dependent", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.dependent", + "resource_key": null, + "resource_name": "dependent", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.base: Modifying... [id=f6f74ca6-e8ef-e51f-522c-433b9ed5038f]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "resource": { + "addr": "tfcoremock_simple_resource.base", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base", + "resource_key": null, + "resource_name": "base", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "resource": { + "addr": "tfcoremock_simple_resource.base", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base", + "resource_key": null, + "resource_name": "base", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.dependent: Modifying... [id=1b17b502-96c9-fcc3-3b09-2af1c3de6ad8]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "resource": { + "addr": "tfcoremock_simple_resource.dependent", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.dependent", + "resource_key": null, + "resource_name": "dependent", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "resource": { + "addr": "tfcoremock_simple_resource.dependent", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.dependent", + "resource_key": null, + "resource_name": "dependent", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 2 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 2, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_relevant_attributes/plan b/testing/equivalence-tests/outputs/drift_relevant_attributes/plan new file mode 100644 index 0000000000..7e4738aafa --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_relevant_attributes/plan @@ -0,0 +1,49 @@ +tfcoremock_simple_resource.base: Refreshing state... [id=f6f74ca6-e8ef-e51f-522c-433b9ed5038f] +tfcoremock_simple_resource.dependent: Refreshing state... [id=1b17b502-96c9-fcc3-3b09-2af1c3de6ad8] + +Note: Objects have changed outside of Terraform + +Terraform detected the following changes made outside of Terraform since the +last "terraform apply" which may have affected this plan: + + # tfcoremock_simple_resource.base has changed + ~ resource "tfcoremock_simple_resource" "base" { + id = "f6f74ca6-e8ef-e51f-522c-433b9ed5038f" + ~ string = "Hello, world!" -> "Hello, drift!" + # (1 unchanged attribute hidden) + } + + +Unless you have made equivalent changes to your configuration, or ignored the +relevant attributes using ignore_changes, the following plan may include +actions to undo or respond to these changes. + +───────────────────────────────────────────────────────────────────────────── + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.base will be updated in-place + ~ resource "tfcoremock_simple_resource" "base" { + id = "f6f74ca6-e8ef-e51f-522c-433b9ed5038f" + ~ number = 1 -> 0 + ~ string = "Hello, drift!" -> "Hello, change!" + } + + # tfcoremock_simple_resource.dependent will be updated in-place + ~ resource "tfcoremock_simple_resource" "dependent" { + id = "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8" + ~ string = "Hello, world!" -> "Hello, change!" + } + +Plan: 0 to add, 2 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/drift_relevant_attributes/plan.json b/testing/equivalence-tests/outputs/drift_relevant_attributes/plan.json new file mode 100644 index 0000000000..253ad1eb2b --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_relevant_attributes/plan.json @@ -0,0 +1,242 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base", + "expressions": { + "number": { + "constant_value": 0 + }, + "string": { + "constant_value": "Hello, change!" + } + }, + "mode": "managed", + "name": "base", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + }, + { + "address": "tfcoremock_simple_resource.dependent", + "expressions": { + "string": { + "references": [ + "tfcoremock_simple_resource.base.string", + "tfcoremock_simple_resource.base" + ] + } + }, + "mode": "managed", + "name": "dependent", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base", + "mode": "managed", + "name": "base", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 0, + "string": "Hello, change!" + } + }, + { + "address": "tfcoremock_simple_resource.dependent", + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "integer": null, + "number": null, + "string": "Hello, change!" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base", + "mode": "managed", + "name": "base", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 1, + "string": "Hello, drift!" + } + }, + { + "address": "tfcoremock_simple_resource.dependent", + "depends_on": [ + "tfcoremock_simple_resource.base" + ], + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } + }, + "relevant_attributes": [ + { + "attribute": [ + "string" + ], + "resource": "tfcoremock_simple_resource.base" + } + ], + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.base", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 0, + "string": "Hello, change!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 1, + "string": "Hello, drift!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "base", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + }, + { + "address": "tfcoremock_simple_resource.dependent", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "integer": null, + "number": null, + "string": "Hello, change!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ], + "resource_drift": [ + { + "address": "tfcoremock_simple_resource.base", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 1, + "string": "Hello, drift!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 0, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "base", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_relevant_attributes/state b/testing/equivalence-tests/outputs/drift_relevant_attributes/state new file mode 100644 index 0000000000..dce67889e8 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_relevant_attributes/state @@ -0,0 +1,12 @@ +# tfcoremock_simple_resource.base: +resource "tfcoremock_simple_resource" "base" { + id = "f6f74ca6-e8ef-e51f-522c-433b9ed5038f" + number = 0 + string = "Hello, change!" +} + +# tfcoremock_simple_resource.dependent: +resource "tfcoremock_simple_resource" "dependent" { + id = "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8" + string = "Hello, change!" +} diff --git a/testing/equivalence-tests/outputs/drift_relevant_attributes/state.json b/testing/equivalence-tests/outputs/drift_relevant_attributes/state.json new file mode 100644 index 0000000000..1d9f6ad0d3 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_relevant_attributes/state.json @@ -0,0 +1,46 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base", + "mode": "managed", + "name": "base", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 0, + "string": "Hello, change!" + } + }, + { + "address": "tfcoremock_simple_resource.dependent", + "depends_on": [ + "tfcoremock_simple_resource.base" + ], + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "integer": null, + "number": null, + "string": "Hello, change!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_simple/apply.json b/testing/equivalence-tests/outputs/drift_simple/apply.json new file mode 100644 index 0000000000..11057cf563 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_simple/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_simple_resource.drift: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_simple_resource.drift", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.drift", + "resource_key": null, + "resource_name": "drift", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.drift: Modifying... [id=f3c6ddc5-37d5-0170-64ff-518ad421385a]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "resource": { + "addr": "tfcoremock_simple_resource.drift", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.drift", + "resource_key": null, + "resource_name": "drift", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "resource": { + "addr": "tfcoremock_simple_resource.drift", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.drift", + "resource_key": null, + "resource_name": "drift", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_simple/plan b/testing/equivalence-tests/outputs/drift_simple/plan new file mode 100644 index 0000000000..266a142e10 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_simple/plan @@ -0,0 +1,22 @@ +tfcoremock_simple_resource.drift: Refreshing state... [id=f3c6ddc5-37d5-0170-64ff-518ad421385a] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.drift will be updated in-place + ~ resource "tfcoremock_simple_resource" "drift" { + id = "f3c6ddc5-37d5-0170-64ff-518ad421385a" + ~ string = "Hello, drift!" -> "Hello, world!" + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/drift_simple/plan.json b/testing/equivalence-tests/outputs/drift_simple/plan.json new file mode 100644 index 0000000000..fea4e9309d --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_simple/plan.json @@ -0,0 +1,147 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "expressions": { + "string": { + "constant_value": "Hello, world!" + } + }, + "mode": "managed", + "name": "drift", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, drift!" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.drift", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, drift!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ], + "resource_drift": [ + { + "address": "tfcoremock_simple_resource.drift", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, drift!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/drift_simple/state b/testing/equivalence-tests/outputs/drift_simple/state new file mode 100644 index 0000000000..8715e62595 --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_simple/state @@ -0,0 +1,5 @@ +# tfcoremock_simple_resource.drift: +resource "tfcoremock_simple_resource" "drift" { + id = "f3c6ddc5-37d5-0170-64ff-518ad421385a" + string = "Hello, world!" +} diff --git a/testing/equivalence-tests/outputs/drift_simple/state.json b/testing/equivalence-tests/outputs/drift_simple/state.json new file mode 100644 index 0000000000..8029de066e --- /dev/null +++ b/testing/equivalence-tests/outputs/drift_simple/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.drift", + "mode": "managed", + "name": "drift", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex/apply.json b/testing/equivalence-tests/outputs/fully_populated_complex/apply.json new file mode 100644 index 0000000000..5f89da2e52 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex/plan b/testing/equivalence-tests/outputs/fully_populated_complex/plan new file mode 100644 index 0000000000..86fb3ae3ed --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex/plan @@ -0,0 +1,228 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_complex_resource.complex will be created + + resource "tfcoremock_complex_resource" "complex" { + + bool = true + + float = 987654321 + + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + + integer = 987654321 + + list = [ + + { + + string = "this is my first entry in the list, and doesn't contain anything interesting" + }, + + { + + string = <<-EOT + this is my second entry in the list + I am a bit more interesting + and contain multiple lines + EOT + }, + + { + + list = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + + { + + number = 2 + }, + ] + + string = "this is my third entry, and I actually have a nested list" + }, + + { + + set = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + ] + + string = "this is my fourth entry, and I actually have a nested set" + }, + ] + + map = { + + "key_four" = { + + set = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + ] + + string = "this is my fourth entry, and I actually have a nested set" + }, + + "key_one" = { + + string = "this is my first entry in the map, and doesn't contain anything interesting" + }, + + "key_three" = { + + list = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + + { + + number = 2 + }, + ] + + string = "this is my third entry, and I actually have a nested list" + }, + + "key_two" = { + + string = <<-EOT + this is my second entry in the map + I am a bit more interesting + and contain multiple lines + EOT + }, + } + + number = 123456789 + + object = { + + bool = false + + number = 0 + + object = { + + bool = true + + number = 1 + + string = "i am a nested nested object" + } + + string = "i am a nested object" + } + + set = [ + + { + + list = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + + { + + number = 2 + }, + ] + + string = "this is my third entry, and I actually have a nested list" + }, + + { + + set = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + ] + + string = "this is my fourth entry, and I actually have a nested set" + }, + + { + + string = "this is my first entry in the set, and doesn't contain anything interesting" + }, + + { + + string = <<-EOT + this is my second entry in the set + I am a bit more interesting + and contain multiple lines + EOT + }, + ] + + string = "a not very long or complex string" + + + list_block { + + string = jsonencode( + { + + index = 0 + } + ) + } + + list_block { + + list = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + + { + + number = 2 + }, + ] + + string = jsonencode( + { + + index = 1 + } + ) + } + + list_block { + + set = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + ] + + string = jsonencode( + { + + index = 2 + } + ) + } + + + set_block { + + list = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + + { + + number = 2 + }, + ] + + string = jsonencode( + { + + index = 1 + } + ) + } + + set_block { + + set = [ + + { + + number = 0 + }, + + { + + number = 1 + }, + ] + + string = jsonencode( + { + + index = 2 + } + ) + } + + set_block { + + string = jsonencode( + { + + index = 0 + } + ) + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/fully_populated_complex/plan.json b/testing/equivalence-tests/outputs/fully_populated_complex/plan.json new file mode 100644 index 0000000000..adae4e8682 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex/plan.json @@ -0,0 +1,1547 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "expressions": { + "bool": { + "constant_value": true + }, + "float": { + "constant_value": 987654321 + }, + "id": { + "constant_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + }, + "integer": { + "constant_value": 987654321 + }, + "list": { + "constant_value": [ + { + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + { + "set": [ + { + "number": 0 + }, + { + "number": 1 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ] + }, + "list_block": [ + { + "string": { + "constant_value": "{\"index\":0}" + } + }, + { + "list": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ] + }, + "string": { + "constant_value": "{\"index\":1}" + } + }, + { + "set": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + } + ] + }, + "string": { + "constant_value": "{\"index\":2}" + } + } + ], + "map": { + "constant_value": { + "key_four": { + "set": [ + { + "number": 0 + }, + { + "number": 1 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + } + }, + "number": { + "constant_value": 123456789 + }, + "object": { + "constant_value": { + "bool": false, + "number": 0, + "object": { + "bool": true, + "number": 1, + "string": "i am a nested nested object" + }, + "string": "i am a nested object" + } + }, + "set": { + "constant_value": [ + { + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + }, + { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + { + "set": [ + { + "number": 0 + }, + { + "number": 1 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ] + }, + "set_block": [ + { + "string": { + "constant_value": "{\"index\":0}" + } + }, + { + "list": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ] + }, + "string": { + "constant_value": "{\"index\":1}" + } + }, + { + "set": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + } + ] + }, + "string": { + "constant_value": "{\"index\":2}" + } + } + ], + "string": { + "constant_value": "a not very long or complex string" + } + }, + "mode": "managed", + "name": "complex", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_complex_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "type": "tfcoremock_complex_resource", + "values": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_complex_resource.complex", + "change": { + "actions": [ + "create" + ], + "after": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + }, + "after_sensitive": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_complex_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex/state b/testing/equivalence-tests/outputs/fully_populated_complex/state new file mode 100644 index 0000000000..de3bff4f52 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex/state @@ -0,0 +1,212 @@ +# tfcoremock_complex_resource.complex: +resource "tfcoremock_complex_resource" "complex" { + bool = true + float = 987654321 + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + integer = 987654321 + list = [ + { + string = "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + string = <<-EOT + this is my second entry in the list + I am a bit more interesting + and contain multiple lines + EOT + }, + { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = "this is my third entry, and I actually have a nested list" + }, + { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = "this is my fourth entry, and I actually have a nested set" + }, + ] + map = { + "key_four" = { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = "this is my fourth entry, and I actually have a nested set" + }, + "key_one" = { + string = "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three" = { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = "this is my third entry, and I actually have a nested list" + }, + "key_two" = { + string = <<-EOT + this is my second entry in the map + I am a bit more interesting + and contain multiple lines + EOT + }, + } + number = 123456789 + object = { + bool = false + number = 0 + object = { + bool = true + number = 1 + string = "i am a nested nested object" + } + string = "i am a nested object" + } + set = [ + { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = "this is my third entry, and I actually have a nested list" + }, + { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = "this is my fourth entry, and I actually have a nested set" + }, + { + string = "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + string = <<-EOT + this is my second entry in the set + I am a bit more interesting + and contain multiple lines + EOT + }, + ] + string = "a not very long or complex string" + + list_block { + string = jsonencode( + { + index = 0 + } + ) + } + list_block { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = jsonencode( + { + index = 1 + } + ) + } + list_block { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = jsonencode( + { + index = 2 + } + ) + } + + set_block { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = jsonencode( + { + index = 1 + } + ) + } + set_block { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = jsonencode( + { + index = 2 + } + ) + } + set_block { + string = jsonencode( + { + index = 0 + } + ) + } +} diff --git a/testing/equivalence-tests/outputs/fully_populated_complex/state.json b/testing/equivalence-tests/outputs/fully_populated_complex/state.json new file mode 100644 index 0000000000..e7f3c06711 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex/state.json @@ -0,0 +1,653 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "type": "tfcoremock_complex_resource", + "values": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_destroy/apply.json b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/apply.json new file mode 100644 index 0000000000..278bb8077e --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/apply.json @@ -0,0 +1,121 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Refreshing state... [id=64564E36-BFCB-458B-9405-EBBF6A3CAC7A]", + "@module": "terraform.ui", + "hook": { + "id_key": "id", + "id_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "refresh_start" + }, + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Refresh complete [id=64564E36-BFCB-458B-9405-EBBF6A3CAC7A]", + "@module": "terraform.ui", + "hook": { + "id_key": "id", + "id_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "refresh_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Plan to delete", + "@module": "terraform.ui", + "change": { + "action": "delete", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "Plan: 0 to add, 0 to change, 1 to destroy.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "plan", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Destroying... [id=64564E36-BFCB-458B-9405-EBBF6A3CAC7A]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Destroy complete! Resources: 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "destroy", + "remove": 1 + }, + "type": "change_summary" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_destroy/plan b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/plan new file mode 100644 index 0000000000..194ca98269 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/plan @@ -0,0 +1,229 @@ +tfcoremock_complex_resource.complex: Refreshing state... [id=64564E36-BFCB-458B-9405-EBBF6A3CAC7A] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + - destroy + +Terraform will perform the following actions: + + # tfcoremock_complex_resource.complex will be destroyed + - resource "tfcoremock_complex_resource" "complex" { + - bool = true -> null + - float = 987654321 -> null + - id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" -> null + - integer = 987654321 -> null + - list = [ + - { + - string = "this is my first entry in the list, and doesn't contain anything interesting" -> null + }, + - { + - string = <<-EOT + this is my second entry in the list + I am a bit more interesting + and contain multiple lines + EOT -> null + }, + - { + - list = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + - { + - number = 2 -> null + }, + ] -> null + - string = "this is my third entry, and I actually have a nested list" -> null + }, + - { + - set = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + ] -> null + - string = "this is my fourth entry, and I actually have a nested set" -> null + }, + ] -> null + - map = { + - "key_four" = { + - set = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + ] -> null + - string = "this is my fourth entry, and I actually have a nested set" -> null + }, + - "key_one" = { + - string = "this is my first entry in the map, and doesn't contain anything interesting" -> null + }, + - "key_three" = { + - list = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + - { + - number = 2 -> null + }, + ] -> null + - string = "this is my third entry, and I actually have a nested list" -> null + }, + - "key_two" = { + - string = <<-EOT + this is my second entry in the map + I am a bit more interesting + and contain multiple lines + EOT -> null + }, + } -> null + - number = 123456789 -> null + - object = { + - bool = false -> null + - number = 0 -> null + - object = { + - bool = true -> null + - number = 1 -> null + - string = "i am a nested nested object" -> null + } -> null + - string = "i am a nested object" -> null + } -> null + - set = [ + - { + - list = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + - { + - number = 2 -> null + }, + ] -> null + - string = "this is my third entry, and I actually have a nested list" -> null + }, + - { + - set = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + ] -> null + - string = "this is my fourth entry, and I actually have a nested set" -> null + }, + - { + - string = "this is my first entry in the set, and doesn't contain anything interesting" -> null + }, + - { + - string = <<-EOT + this is my second entry in the set + I am a bit more interesting + and contain multiple lines + EOT -> null + }, + ] -> null + - string = "a not very long or complex string" -> null + + - list_block { + - string = jsonencode( + { + - index = 0 + } + ) -> null + } + - list_block { + - list = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + - { + - number = 2 -> null + }, + ] -> null + - string = jsonencode( + { + - index = 1 + } + ) -> null + } + - list_block { + - set = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + ] -> null + - string = jsonencode( + { + - index = 2 + } + ) -> null + } + + - set_block { + - list = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + - { + - number = 2 -> null + }, + ] -> null + - string = jsonencode( + { + - index = 1 + } + ) -> null + } + - set_block { + - set = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + ] -> null + - string = jsonencode( + { + - index = 2 + } + ) -> null + } + - set_block { + - string = jsonencode( + { + - index = 0 + } + ) -> null + } + } + +Plan: 0 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_destroy/plan.json b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/plan.json new file mode 100644 index 0000000000..a3d72e0398 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/plan.json @@ -0,0 +1,1548 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "expressions": { + "bool": { + "constant_value": true + }, + "float": { + "constant_value": 123456789 + }, + "id": { + "constant_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + }, + "integer": { + "constant_value": 123456789 + }, + "list": { + "constant_value": [ + { + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 3 + }, + { + "number": 4 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + { + "set": [ + { + "number": 0 + }, + { + "number": 2 + } + ], + "string": "this is my fourth entry, and I actually have a nested set and I edited my test" + } + ] + }, + "list_block": [ + { + "string": { + "constant_value": "{\"index\":0}" + } + }, + { + "list": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ] + }, + "string": { + "constant_value": "{\"index\":1}" + } + } + ], + "map": { + "constant_value": { + "key_four": { + "set": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 3 + }, + { + "number": 4 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "list": [ + { + "number": 0 + }, + { + "number": 3 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + } + }, + "number": { + "constant_value": 987654321 + }, + "object": { + "constant_value": { + "number": 0, + "object": { + "bool": true, + "string": "i am a nested nested object" + }, + "string": "i am a nested object" + } + }, + "set": { + "constant_value": [ + { + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + }, + { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + { + "set": [ + { + "number": 0 + }, + { + "number": 1 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ] + }, + "set_block": [ + { + "list": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ] + }, + "string": { + "constant_value": "{\"index\":1}" + } + }, + { + "set": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + } + ] + }, + "string": { + "constant_value": "{\"index\":2}" + } + }, + { + "string": { + "constant_value": "{\"index\":3}" + } + } + ], + "string": { + "constant_value": "a not very long or complex string" + } + }, + "mode": "managed", + "name": "complex", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_complex_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": {} + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "type": "tfcoremock_complex_resource", + "values": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_complex_resource.complex", + "change": { + "actions": [ + "delete" + ], + "after": null, + "after_sensitive": false, + "after_unknown": {}, + "before": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + }, + "before_sensitive": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + } + }, + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_complex_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_destroy/state.json b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/state.json new file mode 100644 index 0000000000..00f8f1ca36 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_destroy/state.json @@ -0,0 +1,3 @@ +{ + "format_version": "1.0" +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_update/apply.json b/testing/equivalence-tests/outputs/fully_populated_complex_update/apply.json new file mode 100644 index 0000000000..f248c26af7 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_complex_resource.complex: Modifying... [id=64564E36-BFCB-458B-9405-EBBF6A3CAC7A]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "resource": { + "addr": "tfcoremock_complex_resource.complex", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_complex_resource.complex", + "resource_key": null, + "resource_name": "complex", + "resource_type": "tfcoremock_complex_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_update/plan b/testing/equivalence-tests/outputs/fully_populated_complex_update/plan new file mode 100644 index 0000000000..3d00db8d87 --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_update/plan @@ -0,0 +1,131 @@ +tfcoremock_complex_resource.complex: Refreshing state... [id=64564E36-BFCB-458B-9405-EBBF6A3CAC7A] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_complex_resource.complex will be updated in-place + ~ resource "tfcoremock_complex_resource" "complex" { + ~ float = 987654321 -> 123456789 + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + ~ integer = 987654321 -> 123456789 + ~ list = [ + ~ { + ~ string = <<-EOT + this is my second entry in the list + I am a bit more interesting + and contain multiple lines + + but I've been edited + EOT + }, + ~ { + ~ list = [ + ~ { + ~ number = 2 -> 3 + }, + + { + + number = 4 + }, + # (2 unchanged elements hidden) + ] + # (1 unchanged attribute hidden) + }, + ~ { + ~ set = [ + - { + - number = 1 -> null + }, + + { + + number = 2 + }, + # (1 unchanged element hidden) + ] + ~ string = "this is my fourth entry, and I actually have a nested set" -> "this is my fourth entry, and I actually have a nested set and I edited my test" + }, + # (1 unchanged element hidden) + ] + ~ map = { + ~ "key_four" = { + ~ set = [ + + { + + number = 3 + }, + + { + + number = 4 + }, + # (2 unchanged elements hidden) + ] + # (1 unchanged attribute hidden) + }, + ~ "key_three" = { + ~ list = [ + ~ { + ~ number = 1 -> 3 + }, + ~ { + ~ number = 2 -> 1 + }, + + { + + number = 2 + }, + # (1 unchanged element hidden) + ] + # (1 unchanged attribute hidden) + }, + # (2 unchanged elements hidden) + } + ~ number = 123456789 -> 987654321 + ~ object = { + - bool = false -> null + ~ object = { + - number = 1 -> null + # (2 unchanged attributes hidden) + } + # (2 unchanged attributes hidden) + } + # (3 unchanged attributes hidden) + + - list_block { + - set = [ + - { + - number = 0 -> null + }, + - { + - number = 1 -> null + }, + ] -> null + - string = jsonencode( + { + - index = 2 + } + ) -> null + } + + - set_block { + - string = jsonencode( + { + - index = 0 + } + ) -> null + } + + set_block { + + string = jsonencode( + { + + index = 3 + } + ) + } + + # (4 unchanged blocks hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_update/plan.json b/testing/equivalence-tests/outputs/fully_populated_complex_update/plan.json new file mode 100644 index 0000000000..f8b41bf7df --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_update/plan.json @@ -0,0 +1,2837 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "expressions": { + "bool": { + "constant_value": true + }, + "float": { + "constant_value": 123456789 + }, + "id": { + "constant_value": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + }, + "integer": { + "constant_value": 123456789 + }, + "list": { + "constant_value": [ + { + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 3 + }, + { + "number": 4 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + { + "set": [ + { + "number": 0 + }, + { + "number": 2 + } + ], + "string": "this is my fourth entry, and I actually have a nested set and I edited my test" + } + ] + }, + "list_block": [ + { + "string": { + "constant_value": "{\"index\":0}" + } + }, + { + "list": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ] + }, + "string": { + "constant_value": "{\"index\":1}" + } + } + ], + "map": { + "constant_value": { + "key_four": { + "set": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 3 + }, + { + "number": 4 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "list": [ + { + "number": 0 + }, + { + "number": 3 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + } + }, + "number": { + "constant_value": 987654321 + }, + "object": { + "constant_value": { + "number": 0, + "object": { + "bool": true, + "string": "i am a nested nested object" + }, + "string": "i am a nested object" + } + }, + "set": { + "constant_value": [ + { + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + }, + { + "list": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ], + "string": "this is my third entry, and I actually have a nested list" + }, + { + "set": [ + { + "number": 0 + }, + { + "number": 1 + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ] + }, + "set_block": [ + { + "list": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + }, + { + "number": 2 + } + ] + }, + "string": { + "constant_value": "{\"index\":1}" + } + }, + { + "set": { + "constant_value": [ + { + "number": 0 + }, + { + "number": 1 + } + ] + }, + "string": { + "constant_value": "{\"index\":2}" + } + }, + { + "string": { + "constant_value": "{\"index\":3}" + } + } + ], + "string": { + "constant_value": "a not very long or complex string" + } + }, + "mode": "managed", + "name": "complex", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_complex_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {}, + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "type": "tfcoremock_complex_resource", + "values": { + "bool": true, + "float": 123456789, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 123456789, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 4, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set and I edited my test" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 4, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 987654321, + "object": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":3}" + } + ], + "string": "a not very long or complex string" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "type": "tfcoremock_complex_resource", + "values": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_complex_resource.complex", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": true, + "float": 123456789, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 123456789, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 4, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set and I edited my test" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 4, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 987654321, + "object": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":3}" + } + ], + "string": "a not very long or complex string" + }, + "after_sensitive": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {}, + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "after_unknown": {}, + "before": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + }, + "before_sensitive": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + } + }, + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_complex_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_update/state b/testing/equivalence-tests/outputs/fully_populated_complex_update/state new file mode 100644 index 0000000000..4b1061fd1e --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_update/state @@ -0,0 +1,208 @@ +# tfcoremock_complex_resource.complex: +resource "tfcoremock_complex_resource" "complex" { + bool = true + float = 123456789 + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + integer = 123456789 + list = [ + { + string = "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + string = <<-EOT + this is my second entry in the list + I am a bit more interesting + and contain multiple lines + but I've been edited + EOT + }, + { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 3 + }, + { + number = 4 + }, + ] + string = "this is my third entry, and I actually have a nested list" + }, + { + set = [ + { + number = 0 + }, + { + number = 2 + }, + ] + string = "this is my fourth entry, and I actually have a nested set and I edited my test" + }, + ] + map = { + "key_four" = { + set = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 3 + }, + { + number = 4 + }, + ] + string = "this is my fourth entry, and I actually have a nested set" + }, + "key_one" = { + string = "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three" = { + list = [ + { + number = 0 + }, + { + number = 3 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = "this is my third entry, and I actually have a nested list" + }, + "key_two" = { + string = <<-EOT + this is my second entry in the map + I am a bit more interesting + and contain multiple lines + EOT + }, + } + number = 987654321 + object = { + number = 0 + object = { + bool = true + string = "i am a nested nested object" + } + string = "i am a nested object" + } + set = [ + { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = "this is my third entry, and I actually have a nested list" + }, + { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = "this is my fourth entry, and I actually have a nested set" + }, + { + string = "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + string = <<-EOT + this is my second entry in the set + I am a bit more interesting + and contain multiple lines + EOT + }, + ] + string = "a not very long or complex string" + + list_block { + string = jsonencode( + { + index = 0 + } + ) + } + list_block { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = jsonencode( + { + index = 1 + } + ) + } + + set_block { + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + }, + ] + string = jsonencode( + { + index = 1 + } + ) + } + set_block { + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + string = jsonencode( + { + index = 2 + } + ) + } + set_block { + string = jsonencode( + { + index = 3 + } + ) + } +} diff --git a/testing/equivalence-tests/outputs/fully_populated_complex_update/state.json b/testing/equivalence-tests/outputs/fully_populated_complex_update/state.json new file mode 100644 index 0000000000..95a967d84f --- /dev/null +++ b/testing/equivalence-tests/outputs/fully_populated_complex_update/state.json @@ -0,0 +1,657 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_complex_resource.complex", + "mode": "managed", + "name": "complex", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + { + "list": [ + {}, + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + } + ], + "list_block": [ + { + "list_block": [], + "set_block": [] + }, + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + } + ], + "map": { + "key_four": { + "set": [ + {}, + {}, + {}, + {} + ] + }, + "key_one": {}, + "key_three": { + "list": [ + {}, + {}, + {}, + {} + ] + }, + "key_two": {} + }, + "object": { + "object": {} + }, + "set": [ + { + "list": [ + {}, + {}, + {} + ] + }, + { + "set": [ + {}, + {} + ] + }, + {}, + {} + ], + "set_block": [ + { + "list": [ + {}, + {}, + {} + ], + "list_block": [], + "set_block": [] + }, + { + "list_block": [], + "set": [ + {}, + {} + ], + "set_block": [] + }, + { + "list_block": [], + "set_block": [] + } + ] + }, + "type": "tfcoremock_complex_resource", + "values": { + "bool": true, + "float": 123456789, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 123456789, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 4, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set and I edited my test" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 4, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 3, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 987654321, + "object": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":3}" + } + ], + "string": "a not very long or complex string" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_basic/apply.json b/testing/equivalence-tests/outputs/local_provider_basic/apply.json new file mode 100644 index 0000000000..c8bf1054f2 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_basic/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "local_file.local_file: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "local_file.local_file: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_basic/output.json b/testing/equivalence-tests/outputs/local_provider_basic/output.json new file mode 100644 index 0000000000..eeedd40076 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_basic/output.json @@ -0,0 +1,3 @@ +{ + "hello": "world" +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_basic/plan b/testing/equivalence-tests/outputs/local_provider_basic/plan new file mode 100644 index 0000000000..ab85756411 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_basic/plan @@ -0,0 +1,28 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # local_file.local_file will be created + + resource "local_file" "local_file" { + + content = jsonencode( + { + + hello = "world" + } + ) + + directory_permission = "0777" + + file_permission = "0777" + + filename = "output.json" + + id = (known after apply) + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/local_provider_basic/plan.json b/testing/equivalence-tests/outputs/local_provider_basic/plan.json new file mode 100644 index 0000000000..df3c40c9e0 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_basic/plan.json @@ -0,0 +1,94 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "local": { + "full_name": "registry.terraform.io/hashicorp/local", + "name": "local", + "version_constraint": "2.2.3" + } + }, + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "expressions": { + "content": { + "references": [ + "local.contents" + ] + }, + "filename": { + "constant_value": "output.json" + } + }, + "mode": "managed", + "name": "local_file", + "provider_config_key": "local", + "schema_version": 0, + "type": "local_file" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": { + "sensitive_content": true + }, + "type": "local_file", + "values": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "sensitive_content": null, + "source": null + } + } + ] + } + }, + "resource_changes": [ + { + "address": "local_file.local_file", + "change": { + "actions": [ + "create" + ], + "after": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "sensitive_content": null, + "source": null + }, + "after_sensitive": { + "sensitive_content": true + }, + "after_unknown": { + "id": true + }, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "type": "local_file" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_basic/state b/testing/equivalence-tests/outputs/local_provider_basic/state new file mode 100644 index 0000000000..cd74916e90 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_basic/state @@ -0,0 +1,12 @@ +# local_file.local_file: +resource "local_file" "local_file" { + content = jsonencode( + { + hello = "world" + } + ) + directory_permission = "0777" + file_permission = "0777" + filename = "output.json" + id = "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e" +} diff --git a/testing/equivalence-tests/outputs/local_provider_basic/state.json b/testing/equivalence-tests/outputs/local_provider_basic/state.json new file mode 100644 index 0000000000..7963a7ac53 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_basic/state.json @@ -0,0 +1,30 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": { + "sensitive_content": true + }, + "type": "local_file", + "values": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_delete/apply.json b/testing/equivalence-tests/outputs/local_provider_delete/apply.json new file mode 100644 index 0000000000..57022c17d9 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_delete/apply.json @@ -0,0 +1,78 @@ +[ + { + "@level": "info", + "@message": "local_file.local_file: Plan to delete", + "@module": "terraform.ui", + "change": { + "action": "delete", + "reason": "delete_because_no_resource_config", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "local_file.local_file: Destroying... [id=2248ee2fa0aaaad99178531f924bf00b4b0a8f4e]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_delete/plan b/testing/equivalence-tests/outputs/local_provider_delete/plan new file mode 100644 index 0000000000..dedcf5cd7c --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_delete/plan @@ -0,0 +1,30 @@ +local_file.local_file: Refreshing state... [id=2248ee2fa0aaaad99178531f924bf00b4b0a8f4e] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + - destroy + +Terraform will perform the following actions: + + # local_file.local_file will be destroyed + # (because local_file.local_file is not in configuration) + - resource "local_file" "local_file" { + - content = jsonencode( + { + - hello = "world" + } + ) -> null + - directory_permission = "0777" -> null + - file_permission = "0777" -> null + - filename = "output.json" -> null + - id = "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e" -> null + } + +Plan: 0 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/local_provider_delete/plan.json b/testing/equivalence-tests/outputs/local_provider_delete/plan.json new file mode 100644 index 0000000000..6cdf4199a3 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_delete/plan.json @@ -0,0 +1,121 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "local": { + "full_name": "registry.terraform.io/hashicorp/local", + "name": "local", + "version_constraint": "2.2.3" + } + }, + "root_module": {} + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": {} + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": { + "sensitive_content": true + }, + "type": "local_file", + "values": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "delete_because_no_resource_config", + "address": "local_file.local_file", + "change": { + "actions": [ + "delete" + ], + "after": null, + "after_sensitive": false, + "after_unknown": {}, + "before": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "before_sensitive": { + "sensitive_content": true + } + }, + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "type": "local_file" + } + ], + "resource_drift": [ + { + "address": "local_file.local_file", + "change": { + "actions": [ + "update" + ], + "after": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "after_sensitive": { + "sensitive_content": true + }, + "after_unknown": {}, + "before": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "before_sensitive": { + "sensitive_content": true + } + }, + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "type": "local_file" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_delete/state b/testing/equivalence-tests/outputs/local_provider_delete/state new file mode 100644 index 0000000000..222a60b6eb --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_delete/state @@ -0,0 +1 @@ +The state file is empty. No resources are represented. diff --git a/testing/equivalence-tests/outputs/local_provider_delete/state.json b/testing/equivalence-tests/outputs/local_provider_delete/state.json new file mode 100644 index 0000000000..00f8f1ca36 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_delete/state.json @@ -0,0 +1,3 @@ +{ + "format_version": "1.0" +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_update/apply.json b/testing/equivalence-tests/outputs/local_provider_update/apply.json new file mode 100644 index 0000000000..5f5d08bc25 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_update/apply.json @@ -0,0 +1,115 @@ +[ + { + "@level": "info", + "@message": "local_file.local_file: Plan to replace", + "@module": "terraform.ui", + "change": { + "action": "replace", + "reason": "cannot_update", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "local_file.local_file: Destroying... [id=2248ee2fa0aaaad99178531f924bf00b4b0a8f4e]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "local_file.local_file: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "648a5452054fca119f95b07f9ea992cc6d9681df", + "resource": { + "addr": "local_file.local_file", + "implied_provider": "local", + "module": "", + "resource": "local_file.local_file", + "resource_key": null, + "resource_name": "local_file", + "resource_type": "local_file" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_update/output.json b/testing/equivalence-tests/outputs/local_provider_update/output.json new file mode 100644 index 0000000000..9eab206c84 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_update/output.json @@ -0,0 +1,3 @@ +{ + "goodbye": "world" +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_update/plan b/testing/equivalence-tests/outputs/local_provider_update/plan new file mode 100644 index 0000000000..6cadae46cc --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_update/plan @@ -0,0 +1,28 @@ +local_file.local_file: Refreshing state... [id=2248ee2fa0aaaad99178531f924bf00b4b0a8f4e] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # local_file.local_file must be replaced +-/+ resource "local_file" "local_file" { + ~ content = jsonencode( + ~ { + + goodbye = "world" + - hello = "world" + } # forces replacement + ) + ~ id = "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e" -> (known after apply) + # (3 unchanged attributes hidden) + } + +Plan: 1 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/local_provider_update/plan.json b/testing/equivalence-tests/outputs/local_provider_update/plan.json new file mode 100644 index 0000000000..e4f137d61a --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_update/plan.json @@ -0,0 +1,181 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "local": { + "full_name": "registry.terraform.io/hashicorp/local", + "name": "local", + "version_constraint": "2.2.3" + } + }, + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "expressions": { + "content": { + "references": [ + "local.contents" + ] + }, + "filename": { + "constant_value": "output.json" + } + }, + "mode": "managed", + "name": "local_file", + "provider_config_key": "local", + "schema_version": 0, + "type": "local_file" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": {}, + "type": "local_file", + "values": { + "content": "{\"goodbye\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "sensitive_content": null, + "source": null + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": { + "sensitive_content": true + }, + "type": "local_file", + "values": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "replace_because_cannot_update", + "address": "local_file.local_file", + "change": { + "actions": [ + "delete", + "create" + ], + "after": { + "content": "{\"goodbye\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "sensitive_content": null, + "source": null + }, + "after_sensitive": { + "sensitive_content": true + }, + "after_unknown": { + "id": true + }, + "before": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "before_sensitive": { + "sensitive_content": true + }, + "replace_paths": [ + [ + "content" + ] + ] + }, + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "type": "local_file" + } + ], + "resource_drift": [ + { + "address": "local_file.local_file", + "change": { + "actions": [ + "update" + ], + "after": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "after_sensitive": { + "sensitive_content": true + }, + "after_unknown": {}, + "before": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "before_sensitive": { + "sensitive_content": true + } + }, + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "type": "local_file" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/local_provider_update/state b/testing/equivalence-tests/outputs/local_provider_update/state new file mode 100644 index 0000000000..4f14ea50ef --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_update/state @@ -0,0 +1,12 @@ +# local_file.local_file: +resource "local_file" "local_file" { + content = jsonencode( + { + goodbye = "world" + } + ) + directory_permission = "0777" + file_permission = "0777" + filename = "output.json" + id = "648a5452054fca119f95b07f9ea992cc6d9681df" +} diff --git a/testing/equivalence-tests/outputs/local_provider_update/state.json b/testing/equivalence-tests/outputs/local_provider_update/state.json new file mode 100644 index 0000000000..e8c649c255 --- /dev/null +++ b/testing/equivalence-tests/outputs/local_provider_update/state.json @@ -0,0 +1,30 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "local_file.local_file", + "mode": "managed", + "name": "local_file", + "provider_name": "registry.terraform.io/hashicorp/local", + "schema_version": 0, + "sensitive_values": { + "sensitive_content": true + }, + "type": "local_file", + "values": { + "content": "{\"goodbye\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "648a5452054fca119f95b07f9ea992cc6d9681df", + "sensitive_content": null, + "source": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_simple/apply.json b/testing/equivalence-tests/outputs/moved_simple/apply.json new file mode 100644 index 0000000000..42438bd2f7 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_simple/apply.json @@ -0,0 +1,22 @@ +[ + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_simple/plan b/testing/equivalence-tests/outputs/moved_simple/plan new file mode 100644 index 0000000000..083308b345 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_simple/plan @@ -0,0 +1,18 @@ +tfcoremock_simple_resource.second: Refreshing state... [id=70c47571-66c3-b1dc-2474-47a74b9c7886] + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.first has moved to tfcoremock_simple_resource.second + resource "tfcoremock_simple_resource" "second" { + id = "70c47571-66c3-b1dc-2474-47a74b9c7886" + # (1 unchanged attribute hidden) + } + +Plan: 0 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/moved_simple/plan.json b/testing/equivalence-tests/outputs/moved_simple/plan.json new file mode 100644 index 0000000000..2aefad14ee --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_simple/plan.json @@ -0,0 +1,115 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "expressions": { + "string": { + "constant_value": "Hello, world!" + } + }, + "mode": "managed", + "name": "second", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "mode": "managed", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "mode": "managed", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.second", + "change": { + "actions": [ + "no-op" + ], + "after": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "second", + "previous_address": "tfcoremock_simple_resource.first", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_simple/state b/testing/equivalence-tests/outputs/moved_simple/state new file mode 100644 index 0000000000..f336bbede8 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_simple/state @@ -0,0 +1,5 @@ +# tfcoremock_simple_resource.second: +resource "tfcoremock_simple_resource" "second" { + id = "70c47571-66c3-b1dc-2474-47a74b9c7886" + string = "Hello, world!" +} diff --git a/testing/equivalence-tests/outputs/moved_simple/state.json b/testing/equivalence-tests/outputs/moved_simple/state.json new file mode 100644 index 0000000000..84cdbee6fd --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_simple/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "mode": "managed", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_drift/apply.json b/testing/equivalence-tests/outputs/moved_with_drift/apply.json new file mode 100644 index 0000000000..6dcc3b03c4 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_drift/apply.json @@ -0,0 +1,145 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_simple_resource.base_after: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "previous_resource": { + "addr": "tfcoremock_simple_resource.base_before", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base_before", + "resource_key": null, + "resource_name": "base_before", + "resource_type": "tfcoremock_simple_resource" + }, + "resource": { + "addr": "tfcoremock_simple_resource.base_after", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base_after", + "resource_key": null, + "resource_name": "base_after", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.dependent: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_simple_resource.dependent", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.dependent", + "resource_key": null, + "resource_name": "dependent", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.base_after: Modifying... [id=e450ef2f-b80f-0cce-8bdb-14d88f48649c]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "resource": { + "addr": "tfcoremock_simple_resource.base_after", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base_after", + "resource_key": null, + "resource_name": "base_after", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "resource": { + "addr": "tfcoremock_simple_resource.base_after", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base_after", + "resource_key": null, + "resource_name": "base_after", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.dependent: Modifying... [id=2ecc718c-8d04-5774-5c36-7d69bf77d34e]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "resource": { + "addr": "tfcoremock_simple_resource.dependent", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.dependent", + "resource_key": null, + "resource_name": "dependent", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "resource": { + "addr": "tfcoremock_simple_resource.dependent", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.dependent", + "resource_key": null, + "resource_name": "dependent", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 2 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 2, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_drift/plan b/testing/equivalence-tests/outputs/moved_with_drift/plan new file mode 100644 index 0000000000..0293566d57 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_drift/plan @@ -0,0 +1,49 @@ +tfcoremock_simple_resource.base_after: Refreshing state... [id=e450ef2f-b80f-0cce-8bdb-14d88f48649c] +tfcoremock_simple_resource.dependent: Refreshing state... [id=2ecc718c-8d04-5774-5c36-7d69bf77d34e] + +Note: Objects have changed outside of Terraform + +Terraform detected the following changes made outside of Terraform since the +last "terraform apply" which may have affected this plan: + + # tfcoremock_simple_resource.base_after has changed + # (moved from tfcoremock_simple_resource.base_before) + ~ resource "tfcoremock_simple_resource" "base_after" { + id = "e450ef2f-b80f-0cce-8bdb-14d88f48649c" + ~ string = "Hello, world!" -> "Hello, drift!" + } + + +Unless you have made equivalent changes to your configuration, or ignored the +relevant attributes using ignore_changes, the following plan may include +actions to undo or respond to these changes. + +───────────────────────────────────────────────────────────────────────────── + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.base_after will be updated in-place + # (moved from tfcoremock_simple_resource.base_before) + ~ resource "tfcoremock_simple_resource" "base_after" { + id = "e450ef2f-b80f-0cce-8bdb-14d88f48649c" + ~ string = "Hello, drift!" -> "Hello, change!" + } + + # tfcoremock_simple_resource.dependent will be updated in-place + ~ resource "tfcoremock_simple_resource" "dependent" { + id = "2ecc718c-8d04-5774-5c36-7d69bf77d34e" + ~ string = "Hello, world!" -> "Hello, change!" + } + +Plan: 0 to add, 2 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/moved_with_drift/plan.json b/testing/equivalence-tests/outputs/moved_with_drift/plan.json new file mode 100644 index 0000000000..f0411ed537 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_drift/plan.json @@ -0,0 +1,242 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base_after", + "expressions": { + "string": { + "constant_value": "Hello, change!" + } + }, + "mode": "managed", + "name": "base_after", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + }, + { + "address": "tfcoremock_simple_resource.dependent", + "expressions": { + "string": { + "references": [ + "tfcoremock_simple_resource.base_after.string", + "tfcoremock_simple_resource.base_after" + ] + } + }, + "mode": "managed", + "name": "dependent", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base_after", + "mode": "managed", + "name": "base_after", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, change!" + } + }, + { + "address": "tfcoremock_simple_resource.dependent", + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "integer": null, + "number": null, + "string": "Hello, change!" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base_after", + "mode": "managed", + "name": "base_after", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, drift!" + } + }, + { + "address": "tfcoremock_simple_resource.dependent", + "depends_on": [ + "tfcoremock_simple_resource.base_after", + "tfcoremock_simple_resource.base_before" + ], + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } + }, + "relevant_attributes": [ + { + "attribute": [ + "string" + ], + "resource": "tfcoremock_simple_resource.base_after" + } + ], + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.base_after", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, change!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, drift!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "base_after", + "previous_address": "tfcoremock_simple_resource.base_before", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + }, + { + "address": "tfcoremock_simple_resource.dependent", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "integer": null, + "number": null, + "string": "Hello, change!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ], + "resource_drift": [ + { + "address": "tfcoremock_simple_resource.base_after", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, drift!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "base_after", + "previous_address": "tfcoremock_simple_resource.base_before", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_drift/state b/testing/equivalence-tests/outputs/moved_with_drift/state new file mode 100644 index 0000000000..0ad1268362 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_drift/state @@ -0,0 +1,11 @@ +# tfcoremock_simple_resource.base_after: +resource "tfcoremock_simple_resource" "base_after" { + id = "e450ef2f-b80f-0cce-8bdb-14d88f48649c" + string = "Hello, change!" +} + +# tfcoremock_simple_resource.dependent: +resource "tfcoremock_simple_resource" "dependent" { + id = "2ecc718c-8d04-5774-5c36-7d69bf77d34e" + string = "Hello, change!" +} diff --git a/testing/equivalence-tests/outputs/moved_with_drift/state.json b/testing/equivalence-tests/outputs/moved_with_drift/state.json new file mode 100644 index 0000000000..e1303bed58 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_drift/state.json @@ -0,0 +1,46 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.base_after", + "mode": "managed", + "name": "base_after", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, change!" + } + }, + { + "address": "tfcoremock_simple_resource.dependent", + "depends_on": [ + "tfcoremock_simple_resource.base_after" + ], + "mode": "managed", + "name": "dependent", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "integer": null, + "number": null, + "string": "Hello, change!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_refresh_only/apply.json b/testing/equivalence-tests/outputs/moved_with_refresh_only/apply.json new file mode 100644 index 0000000000..42438bd2f7 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_refresh_only/apply.json @@ -0,0 +1,22 @@ +[ + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_refresh_only/plan b/testing/equivalence-tests/outputs/moved_with_refresh_only/plan new file mode 100644 index 0000000000..441ec4994c --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_refresh_only/plan @@ -0,0 +1,18 @@ +tfcoremock_simple_resource.second: Refreshing state... [id=70c47571-66c3-b1dc-2474-47a74b9c7886] + +Note: Objects have changed outside of Terraform + +Terraform detected the following changes made outside of Terraform since the +last "terraform apply" which may have affected this plan: + + # tfcoremock_simple_resource.first has moved to tfcoremock_simple_resource.second + resource "tfcoremock_simple_resource" "second" { + id = "70c47571-66c3-b1dc-2474-47a74b9c7886" + # (1 unchanged attribute hidden) + } + + +This is a refresh-only plan, so Terraform will not take any actions to undo +these. If you were expecting these changes then you can apply this plan to +record the updated values in the Terraform state without changing any remote +objects. diff --git a/testing/equivalence-tests/outputs/moved_with_refresh_only/plan.json b/testing/equivalence-tests/outputs/moved_with_refresh_only/plan.json new file mode 100644 index 0000000000..cf7d4969f0 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_refresh_only/plan.json @@ -0,0 +1,95 @@ +{ + "applyable": false, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "expressions": { + "string": { + "constant_value": "Hello, world!" + } + }, + "mode": "managed", + "name": "second", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": {} + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "mode": "managed", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } + }, + "resource_drift": [ + { + "address": "tfcoremock_simple_resource.second", + "change": { + "actions": [ + "no-op" + ], + "after": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "second", + "previous_address": "tfcoremock_simple_resource.first", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_refresh_only/state.json b/testing/equivalence-tests/outputs/moved_with_refresh_only/state.json new file mode 100644 index 0000000000..84cdbee6fd --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_refresh_only/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.second", + "mode": "managed", + "name": "second", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_update/apply.json b/testing/equivalence-tests/outputs/moved_with_update/apply.json new file mode 100644 index 0000000000..668ec98b79 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_update/apply.json @@ -0,0 +1,88 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_simple_resource.moved: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "previous_resource": { + "addr": "tfcoremock_simple_resource.base", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.base", + "resource_key": null, + "resource_name": "base", + "resource_type": "tfcoremock_simple_resource" + }, + "resource": { + "addr": "tfcoremock_simple_resource.moved", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.moved", + "resource_key": null, + "resource_name": "moved", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_simple_resource.moved: Modifying... [id=7da63aeb-f908-a112-9886-f29a0b0bd4ad]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "resource": { + "addr": "tfcoremock_simple_resource.moved", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.moved", + "resource_key": null, + "resource_name": "moved", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "resource": { + "addr": "tfcoremock_simple_resource.moved", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_simple_resource.moved", + "resource_key": null, + "resource_name": "moved", + "resource_type": "tfcoremock_simple_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_update/plan b/testing/equivalence-tests/outputs/moved_with_update/plan new file mode 100644 index 0000000000..eab173770c --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_update/plan @@ -0,0 +1,23 @@ +tfcoremock_simple_resource.moved: Refreshing state... [id=7da63aeb-f908-a112-9886-f29a0b0bd4ad] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_simple_resource.moved will be updated in-place + # (moved from tfcoremock_simple_resource.base) + ~ resource "tfcoremock_simple_resource" "moved" { + id = "7da63aeb-f908-a112-9886-f29a0b0bd4ad" + ~ string = "Hello, world!" -> "Hello, change!" + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/moved_with_update/plan.json b/testing/equivalence-tests/outputs/moved_with_update/plan.json new file mode 100644 index 0000000000..b588fdcba8 --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_update/plan.json @@ -0,0 +1,115 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.moved", + "expressions": { + "string": { + "constant_value": "Hello, change!" + } + }, + "mode": "managed", + "name": "moved", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_simple_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.moved", + "mode": "managed", + "name": "moved", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "integer": null, + "number": null, + "string": "Hello, change!" + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.moved", + "mode": "managed", + "name": "moved", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "integer": null, + "number": null, + "string": "Hello, world!" + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_simple_resource.moved", + "change": { + "actions": [ + "update" + ], + "after": { + "bool": null, + "float": null, + "id": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "integer": null, + "number": null, + "string": "Hello, change!" + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "bool": null, + "float": null, + "id": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "moved", + "previous_address": "tfcoremock_simple_resource.base", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_simple_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/moved_with_update/state b/testing/equivalence-tests/outputs/moved_with_update/state new file mode 100644 index 0000000000..5394de2afe --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_update/state @@ -0,0 +1,5 @@ +# tfcoremock_simple_resource.moved: +resource "tfcoremock_simple_resource" "moved" { + id = "7da63aeb-f908-a112-9886-f29a0b0bd4ad" + string = "Hello, change!" +} diff --git a/testing/equivalence-tests/outputs/moved_with_update/state.json b/testing/equivalence-tests/outputs/moved_with_update/state.json new file mode 100644 index 0000000000..406616c3db --- /dev/null +++ b/testing/equivalence-tests/outputs/moved_with_update/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_simple_resource.moved", + "mode": "managed", + "name": "moved", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_simple_resource", + "values": { + "bool": null, + "float": null, + "id": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "integer": null, + "number": null, + "string": "Hello, change!" + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/multiple_block_types/apply.json b/testing/equivalence-tests/outputs/multiple_block_types/apply.json new file mode 100644 index 0000000000..42d7fb3eca --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_multiple_blocks.multiple_blocks: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_multiple_blocks.multiple_blocks", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_multiple_blocks.multiple_blocks", + "resource_key": null, + "resource_name": "multiple_blocks", + "resource_type": "tfcoremock_multiple_blocks" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_multiple_blocks.multiple_blocks: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_multiple_blocks.multiple_blocks", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_multiple_blocks.multiple_blocks", + "resource_key": null, + "resource_name": "multiple_blocks", + "resource_type": "tfcoremock_multiple_blocks" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "resource": { + "addr": "tfcoremock_multiple_blocks.multiple_blocks", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_multiple_blocks.multiple_blocks", + "resource_key": null, + "resource_name": "multiple_blocks", + "resource_type": "tfcoremock_multiple_blocks" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/multiple_block_types/plan b/testing/equivalence-tests/outputs/multiple_block_types/plan new file mode 100644 index 0000000000..4d8be6b642 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types/plan @@ -0,0 +1,37 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_multiple_blocks.multiple_blocks will be created + + resource "tfcoremock_multiple_blocks" "multiple_blocks" { + + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + + first_block { + + id = "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + } + + first_block { + + id = "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + + first_block { + + id = "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + + + second_block { + + id = "157660A9-D590-469E-BE28-83B8526428CA" + } + + second_block { + + id = "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/multiple_block_types/plan.json b/testing/equivalence-tests/outputs/multiple_block_types/plan.json new file mode 100644 index 0000000000..392c7e0420 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types/plan.json @@ -0,0 +1,158 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "expressions": { + "first_block": [ + { + "id": { + "constant_value": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + } + }, + { + "id": { + "constant_value": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + }, + { + "id": { + "constant_value": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + } + ], + "id": { + "constant_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + }, + "second_block": [ + { + "id": { + "constant_value": "157660A9-D590-469E-BE28-83B8526428CA" + } + }, + { + "id": { + "constant_value": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + } + ] + }, + "mode": "managed", + "name": "multiple_blocks", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_multiple_blocks" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "type": "tfcoremock_multiple_blocks", + "values": { + "first_block": [ + { + "id": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "157660A9-D590-469E-BE28-83B8526428CA" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "change": { + "actions": [ + "create" + ], + "after": { + "first_block": [ + { + "id": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "157660A9-D590-469E-BE28-83B8526428CA" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + }, + "after_sensitive": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_multiple_blocks" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/multiple_block_types/state b/testing/equivalence-tests/outputs/multiple_block_types/state new file mode 100644 index 0000000000..e09b6e9678 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types/state @@ -0,0 +1,21 @@ +# tfcoremock_multiple_blocks.multiple_blocks: +resource "tfcoremock_multiple_blocks" "multiple_blocks" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + first_block { + id = "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + } + first_block { + id = "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + first_block { + id = "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + + second_block { + id = "157660A9-D590-469E-BE28-83B8526428CA" + } + second_block { + id = "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } +} diff --git a/testing/equivalence-tests/outputs/multiple_block_types/state.json b/testing/equivalence-tests/outputs/multiple_block_types/state.json new file mode 100644 index 0000000000..c3cc2418d9 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types/state.json @@ -0,0 +1,50 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "type": "tfcoremock_multiple_blocks", + "values": { + "first_block": [ + { + "id": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "157660A9-D590-469E-BE28-83B8526428CA" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/multiple_block_types_update/apply.json b/testing/equivalence-tests/outputs/multiple_block_types_update/apply.json new file mode 100644 index 0000000000..793f3ad180 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_multiple_blocks.multiple_blocks: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_multiple_blocks.multiple_blocks", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_multiple_blocks.multiple_blocks", + "resource_key": null, + "resource_name": "multiple_blocks", + "resource_type": "tfcoremock_multiple_blocks" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_multiple_blocks.multiple_blocks: Modifying... [id=DA051126-BAD6-4EB2-92E5-F0250DAF0B92]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "resource": { + "addr": "tfcoremock_multiple_blocks.multiple_blocks", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_multiple_blocks.multiple_blocks", + "resource_key": null, + "resource_name": "multiple_blocks", + "resource_type": "tfcoremock_multiple_blocks" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "resource": { + "addr": "tfcoremock_multiple_blocks.multiple_blocks", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_multiple_blocks.multiple_blocks", + "resource_key": null, + "resource_name": "multiple_blocks", + "resource_type": "tfcoremock_multiple_blocks" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/multiple_block_types_update/plan b/testing/equivalence-tests/outputs/multiple_block_types_update/plan new file mode 100644 index 0000000000..f65e01a932 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types_update/plan @@ -0,0 +1,31 @@ +tfcoremock_multiple_blocks.multiple_blocks: Refreshing state... [id=DA051126-BAD6-4EB2-92E5-F0250DAF0B92] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_multiple_blocks.multiple_blocks will be updated in-place + ~ resource "tfcoremock_multiple_blocks" "multiple_blocks" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + ~ first_block { + ~ id = "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" -> "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + } + + ~ second_block { + ~ id = "157660A9-D590-469E-BE28-83B8526428CA" -> "91640A80-A65F-4BEF-925B-684E4517A04D" + } + + # (3 unchanged blocks hidden) + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/multiple_block_types_update/plan.json b/testing/equivalence-tests/outputs/multiple_block_types_update/plan.json new file mode 100644 index 0000000000..e6b0578ba8 --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types_update/plan.json @@ -0,0 +1,239 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "expressions": { + "first_block": [ + { + "id": { + "constant_value": "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + } + }, + { + "id": { + "constant_value": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + }, + { + "id": { + "constant_value": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + } + ], + "id": { + "constant_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + }, + "second_block": [ + { + "id": { + "constant_value": "91640A80-A65F-4BEF-925B-684E4517A04D" + } + }, + { + "id": { + "constant_value": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + } + ] + }, + "mode": "managed", + "name": "multiple_blocks", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_multiple_blocks" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "type": "tfcoremock_multiple_blocks", + "values": { + "first_block": [ + { + "id": "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "91640A80-A65F-4BEF-925B-684E4517A04D" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "type": "tfcoremock_multiple_blocks", + "values": { + "first_block": [ + { + "id": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "157660A9-D590-469E-BE28-83B8526428CA" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "change": { + "actions": [ + "update" + ], + "after": { + "first_block": [ + { + "id": "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "91640A80-A65F-4BEF-925B-684E4517A04D" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + }, + "after_sensitive": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "after_unknown": {}, + "before": { + "first_block": [ + { + "id": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "157660A9-D590-469E-BE28-83B8526428CA" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + }, + "before_sensitive": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + } + }, + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_multiple_blocks" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/multiple_block_types_update/state b/testing/equivalence-tests/outputs/multiple_block_types_update/state new file mode 100644 index 0000000000..eda4b8112a --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types_update/state @@ -0,0 +1,21 @@ +# tfcoremock_multiple_blocks.multiple_blocks: +resource "tfcoremock_multiple_blocks" "multiple_blocks" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + first_block { + id = "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + } + first_block { + id = "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + first_block { + id = "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + + second_block { + id = "91640A80-A65F-4BEF-925B-684E4517A04D" + } + second_block { + id = "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } +} diff --git a/testing/equivalence-tests/outputs/multiple_block_types_update/state.json b/testing/equivalence-tests/outputs/multiple_block_types_update/state.json new file mode 100644 index 0000000000..554a13613c --- /dev/null +++ b/testing/equivalence-tests/outputs/multiple_block_types_update/state.json @@ -0,0 +1,50 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_multiple_blocks.multiple_blocks", + "mode": "managed", + "name": "multiple_blocks", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "first_block": [ + {}, + {}, + {} + ], + "second_block": [ + {}, + {} + ] + }, + "type": "tfcoremock_multiple_blocks", + "values": { + "first_block": [ + { + "id": "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "91640A80-A65F-4BEF-925B-684E4517A04D" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_list/apply.json b/testing/equivalence-tests/outputs/nested_list/apply.json new file mode 100644 index 0000000000..8b4de18a0d --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_list.nested_list: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_list.nested_list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_list.nested_list", + "resource_key": null, + "resource_name": "nested_list", + "resource_type": "tfcoremock_nested_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_list.nested_list: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_list.nested_list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_list.nested_list", + "resource_key": null, + "resource_name": "nested_list", + "resource_type": "tfcoremock_nested_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "resource": { + "addr": "tfcoremock_nested_list.nested_list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_list.nested_list", + "resource_key": null, + "resource_name": "nested_list", + "resource_type": "tfcoremock_nested_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_list/plan b/testing/equivalence-tests/outputs/nested_list/plan new file mode 100644 index 0000000000..eca0a0954a --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list/plan @@ -0,0 +1,30 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_nested_list.nested_list will be created + + resource "tfcoremock_nested_list" "nested_list" { + + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + lists = [ + + [], + + [ + + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982", + ], + + [ + + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD", + ], + ] + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_list/plan.json b/testing/equivalence-tests/outputs/nested_list/plan.json new file mode 100644 index 0000000000..9848f3f663 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list/plan.json @@ -0,0 +1,125 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "expressions": { + "id": { + "constant_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + }, + "lists": { + "constant_value": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + } + }, + "mode": "managed", + "name": "nested_list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "lists": [ + [], + [ + false + ], + [ + false, + false + ] + ] + }, + "type": "tfcoremock_nested_list", + "values": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_list.nested_list", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + }, + "after_sensitive": { + "lists": [ + [], + [ + false + ], + [ + false, + false + ] + ] + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_list/state b/testing/equivalence-tests/outputs/nested_list/state new file mode 100644 index 0000000000..2b6cb15f20 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list/state @@ -0,0 +1,14 @@ +# tfcoremock_nested_list.nested_list: +resource "tfcoremock_nested_list" "nested_list" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + lists = [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982", + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD", + ], + ] +} diff --git a/testing/equivalence-tests/outputs/nested_list/state.json b/testing/equivalence-tests/outputs/nested_list/state.json new file mode 100644 index 0000000000..6ce6b63512 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list/state.json @@ -0,0 +1,42 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "lists": [ + [], + [ + false + ], + [ + false, + false + ] + ] + }, + "type": "tfcoremock_nested_list", + "values": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_list_update/apply.json b/testing/equivalence-tests/outputs/nested_list_update/apply.json new file mode 100644 index 0000000000..2f71377612 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_list.nested_list: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_nested_list.nested_list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_list.nested_list", + "resource_key": null, + "resource_name": "nested_list", + "resource_type": "tfcoremock_nested_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_list.nested_list: Modifying... [id=DA051126-BAD6-4EB2-92E5-F0250DAF0B92]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "resource": { + "addr": "tfcoremock_nested_list.nested_list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_list.nested_list", + "resource_key": null, + "resource_name": "nested_list", + "resource_type": "tfcoremock_nested_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "resource": { + "addr": "tfcoremock_nested_list.nested_list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_list.nested_list", + "resource_key": null, + "resource_name": "nested_list", + "resource_type": "tfcoremock_nested_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_list_update/plan b/testing/equivalence-tests/outputs/nested_list_update/plan new file mode 100644 index 0000000000..be5e788baa --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list_update/plan @@ -0,0 +1,33 @@ +tfcoremock_nested_list.nested_list: Refreshing state... [id=DA051126-BAD6-4EB2-92E5-F0250DAF0B92] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_nested_list.nested_list will be updated in-place + ~ resource "tfcoremock_nested_list" "nested_list" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + ~ lists = [ + ~ [ + + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982", + ], + ~ [ + ~ "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" -> "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD", + ], + ~ [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + - "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD", + ], + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_list_update/plan.json b/testing/equivalence-tests/outputs/nested_list_update/plan.json new file mode 100644 index 0000000000..0409d03658 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list_update/plan.json @@ -0,0 +1,195 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "expressions": { + "id": { + "constant_value": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + }, + "lists": { + "constant_value": [ + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F" + ] + ] + } + }, + "mode": "managed", + "name": "nested_list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "lists": [ + [ + false + ], + [ + false + ], + [ + false + ] + ] + }, + "type": "tfcoremock_nested_list", + "values": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F" + ] + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "lists": [ + [], + [ + false + ], + [ + false, + false + ] + ] + }, + "type": "tfcoremock_nested_list", + "values": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_list.nested_list", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F" + ] + ] + }, + "after_sensitive": { + "lists": [ + [ + false + ], + [ + false + ], + [ + false + ] + ] + }, + "after_unknown": {}, + "before": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + }, + "before_sensitive": { + "lists": [ + [], + [ + false + ], + [ + false, + false + ] + ] + } + }, + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_list_update/state b/testing/equivalence-tests/outputs/nested_list_update/state new file mode 100644 index 0000000000..23e036afd7 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list_update/state @@ -0,0 +1,15 @@ +# tfcoremock_nested_list.nested_list: +resource "tfcoremock_nested_list" "nested_list" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + lists = [ + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982", + ], + [ + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD", + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + ], + ] +} diff --git a/testing/equivalence-tests/outputs/nested_list_update/state.json b/testing/equivalence-tests/outputs/nested_list_update/state.json new file mode 100644 index 0000000000..f8f1cc2710 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_list_update/state.json @@ -0,0 +1,44 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_list.nested_list", + "mode": "managed", + "name": "nested_list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "lists": [ + [ + false + ], + [ + false + ], + [ + false + ] + ] + }, + "type": "tfcoremock_nested_list", + "values": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F" + ] + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_map/apply.json b/testing/equivalence-tests/outputs/nested_map/apply.json new file mode 100644 index 0000000000..ca3a11106c --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_map.nested_map: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_map.nested_map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_map.nested_map", + "resource_key": null, + "resource_name": "nested_map", + "resource_type": "tfcoremock_nested_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_map.nested_map: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_map.nested_map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_map.nested_map", + "resource_key": null, + "resource_name": "nested_map", + "resource_type": "tfcoremock_nested_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "502B0348-B796-4F6A-8694-A5A397237B85", + "resource": { + "addr": "tfcoremock_nested_map.nested_map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_map.nested_map", + "resource_key": null, + "resource_name": "nested_map", + "resource_type": "tfcoremock_nested_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_map/plan b/testing/equivalence-tests/outputs/nested_map/plan new file mode 100644 index 0000000000..d0bcf5f7f0 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map/plan @@ -0,0 +1,30 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_nested_map.nested_map will be created + + resource "tfcoremock_nested_map" "nested_map" { + + id = "502B0348-B796-4F6A-8694-A5A397237B85" + + maps = { + + "first_nested_map" = { + + "first_key" = "9E858021-953F-4DD3-8842-F2C782780422" + + "second_key" = "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + } + + "second_nested_map" = { + + "first_key" = "6E80C701-A823-43FE-A520-699851EF9052" + + "second_key" = "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_map/plan.json b/testing/equivalence-tests/outputs/nested_map/plan.json new file mode 100644 index 0000000000..2d4783e708 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map/plan.json @@ -0,0 +1,113 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "expressions": { + "id": { + "constant_value": "502B0348-B796-4F6A-8694-A5A397237B85" + }, + "maps": { + "constant_value": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + } + }, + "mode": "managed", + "name": "nested_map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "type": "tfcoremock_nested_map", + "values": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_map.nested_map", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + }, + "after_sensitive": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_map/state b/testing/equivalence-tests/outputs/nested_map/state new file mode 100644 index 0000000000..6cd8e2205d --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map/state @@ -0,0 +1,14 @@ +# tfcoremock_nested_map.nested_map: +resource "tfcoremock_nested_map" "nested_map" { + id = "502B0348-B796-4F6A-8694-A5A397237B85" + maps = { + "first_nested_map" = { + "first_key" = "9E858021-953F-4DD3-8842-F2C782780422" + "second_key" = "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + } + "second_nested_map" = { + "first_key" = "6E80C701-A823-43FE-A520-699851EF9052" + "second_key" = "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } +} diff --git a/testing/equivalence-tests/outputs/nested_map/state.json b/testing/equivalence-tests/outputs/nested_map/state.json new file mode 100644 index 0000000000..7c2a9c603c --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map/state.json @@ -0,0 +1,36 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "type": "tfcoremock_nested_map", + "values": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_map_update/apply.json b/testing/equivalence-tests/outputs/nested_map_update/apply.json new file mode 100644 index 0000000000..bf5bf71021 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_map.nested_map: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_nested_map.nested_map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_map.nested_map", + "resource_key": null, + "resource_name": "nested_map", + "resource_type": "tfcoremock_nested_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_map.nested_map: Modifying... [id=502B0348-B796-4F6A-8694-A5A397237B85]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "502B0348-B796-4F6A-8694-A5A397237B85", + "resource": { + "addr": "tfcoremock_nested_map.nested_map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_map.nested_map", + "resource_key": null, + "resource_name": "nested_map", + "resource_type": "tfcoremock_nested_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "502B0348-B796-4F6A-8694-A5A397237B85", + "resource": { + "addr": "tfcoremock_nested_map.nested_map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_map.nested_map", + "resource_key": null, + "resource_name": "nested_map", + "resource_type": "tfcoremock_nested_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_map_update/plan b/testing/equivalence-tests/outputs/nested_map_update/plan new file mode 100644 index 0000000000..ef2185de58 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map_update/plan @@ -0,0 +1,32 @@ +tfcoremock_nested_map.nested_map: Refreshing state... [id=502B0348-B796-4F6A-8694-A5A397237B85] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_nested_map.nested_map will be updated in-place + ~ resource "tfcoremock_nested_map" "nested_map" { + id = "502B0348-B796-4F6A-8694-A5A397237B85" + ~ maps = { + ~ "first_nested_map" = { + ~ "first_key" = "9E858021-953F-4DD3-8842-F2C782780422" -> "6E80C701-A823-43FE-A520-699851EF9052" + + "third_key" = "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + # (1 unchanged element hidden) + } + ~ "second_nested_map" = { + ~ "first_key" = "6E80C701-A823-43FE-A520-699851EF9052" -> "9E858021-953F-4DD3-8842-F2C782780422" + - "second_key" = "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" -> null + } + } + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_map_update/plan.json b/testing/equivalence-tests/outputs/nested_map_update/plan.json new file mode 100644 index 0000000000..208f839c59 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map_update/plan.json @@ -0,0 +1,166 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "expressions": { + "id": { + "constant_value": "502B0348-B796-4F6A-8694-A5A397237B85" + }, + "maps": { + "constant_value": { + "first_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0", + "third_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + }, + "second_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422" + } + } + } + }, + "mode": "managed", + "name": "nested_map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "type": "tfcoremock_nested_map", + "values": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0", + "third_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + }, + "second_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422" + } + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "type": "tfcoremock_nested_map", + "values": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_map.nested_map", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0", + "third_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + }, + "second_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422" + } + } + }, + "after_sensitive": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "after_unknown": {}, + "before": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + }, + "before_sensitive": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + } + }, + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_map_update/state b/testing/equivalence-tests/outputs/nested_map_update/state new file mode 100644 index 0000000000..cbd0c4f5d6 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map_update/state @@ -0,0 +1,14 @@ +# tfcoremock_nested_map.nested_map: +resource "tfcoremock_nested_map" "nested_map" { + id = "502B0348-B796-4F6A-8694-A5A397237B85" + maps = { + "first_nested_map" = { + "first_key" = "6E80C701-A823-43FE-A520-699851EF9052" + "second_key" = "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + "third_key" = "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + "second_nested_map" = { + "first_key" = "9E858021-953F-4DD3-8842-F2C782780422" + } + } +} diff --git a/testing/equivalence-tests/outputs/nested_map_update/state.json b/testing/equivalence-tests/outputs/nested_map_update/state.json new file mode 100644 index 0000000000..39383246e2 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_map_update/state.json @@ -0,0 +1,36 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_map.nested_map", + "mode": "managed", + "name": "nested_map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "maps": { + "first_nested_map": {}, + "second_nested_map": {} + } + }, + "type": "tfcoremock_nested_map", + "values": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0", + "third_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + }, + "second_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422" + } + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_objects/apply.json b/testing/equivalence-tests/outputs/nested_objects/apply.json new file mode 100644 index 0000000000..d20e3a175e --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_object.nested_object: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_object.nested_object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_object.nested_object", + "resource_key": null, + "resource_name": "nested_object", + "resource_type": "tfcoremock_nested_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_object.nested_object: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_object.nested_object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_object.nested_object", + "resource_key": null, + "resource_name": "nested_object", + "resource_type": "tfcoremock_nested_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "resource": { + "addr": "tfcoremock_nested_object.nested_object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_object.nested_object", + "resource_key": null, + "resource_name": "nested_object", + "resource_type": "tfcoremock_nested_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_objects/plan b/testing/equivalence-tests/outputs/nested_objects/plan new file mode 100644 index 0000000000..bb70728ffc --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects/plan @@ -0,0 +1,30 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_nested_object.nested_object will be created + + resource "tfcoremock_nested_object" "nested_object" { + + id = "B2491EF0-9361-40FD-B25A-0332A1A5E052" + + parent_object = { + + first_nested_object = { + + attribute_one = "09AE7244-7BFB-476B-912C-D1AB4E7E9622" + + attribute_two = "5425587C-49EF-4C1E-A906-1DC923A12725" + } + + second_nested_object = { + + attribute_one = "63712BFE-78F8-42D3-A074-A78249E5E25E" + + attribute_two = "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_objects/plan.json b/testing/equivalence-tests/outputs/nested_objects/plan.json new file mode 100644 index 0000000000..20dfa2bffb --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects/plan.json @@ -0,0 +1,113 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "expressions": { + "id": { + "constant_value": "B2491EF0-9361-40FD-B25A-0332A1A5E052" + }, + "parent_object": { + "constant_value": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + } + }, + "mode": "managed", + "name": "nested_object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "type": "tfcoremock_nested_object", + "values": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_object.nested_object", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + }, + "after_sensitive": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_objects/state b/testing/equivalence-tests/outputs/nested_objects/state new file mode 100644 index 0000000000..f6da6e389b --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects/state @@ -0,0 +1,14 @@ +# tfcoremock_nested_object.nested_object: +resource "tfcoremock_nested_object" "nested_object" { + id = "B2491EF0-9361-40FD-B25A-0332A1A5E052" + parent_object = { + first_nested_object = { + attribute_one = "09AE7244-7BFB-476B-912C-D1AB4E7E9622" + attribute_two = "5425587C-49EF-4C1E-A906-1DC923A12725" + } + second_nested_object = { + attribute_one = "63712BFE-78F8-42D3-A074-A78249E5E25E" + attribute_two = "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } +} diff --git a/testing/equivalence-tests/outputs/nested_objects/state.json b/testing/equivalence-tests/outputs/nested_objects/state.json new file mode 100644 index 0000000000..f7bb2f41f8 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects/state.json @@ -0,0 +1,36 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "type": "tfcoremock_nested_object", + "values": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_objects_update/apply.json b/testing/equivalence-tests/outputs/nested_objects_update/apply.json new file mode 100644 index 0000000000..5f8f28d336 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_object.nested_object: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_nested_object.nested_object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_object.nested_object", + "resource_key": null, + "resource_name": "nested_object", + "resource_type": "tfcoremock_nested_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_object.nested_object: Modifying... [id=B2491EF0-9361-40FD-B25A-0332A1A5E052]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "resource": { + "addr": "tfcoremock_nested_object.nested_object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_object.nested_object", + "resource_key": null, + "resource_name": "nested_object", + "resource_type": "tfcoremock_nested_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "resource": { + "addr": "tfcoremock_nested_object.nested_object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_object.nested_object", + "resource_key": null, + "resource_name": "nested_object", + "resource_type": "tfcoremock_nested_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_objects_update/plan b/testing/equivalence-tests/outputs/nested_objects_update/plan new file mode 100644 index 0000000000..fe763de498 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects_update/plan @@ -0,0 +1,31 @@ +tfcoremock_nested_object.nested_object: Refreshing state... [id=B2491EF0-9361-40FD-B25A-0332A1A5E052] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_nested_object.nested_object will be updated in-place + ~ resource "tfcoremock_nested_object" "nested_object" { + id = "B2491EF0-9361-40FD-B25A-0332A1A5E052" + ~ parent_object = { + ~ first_nested_object = { + ~ attribute_two = "5425587C-49EF-4C1E-A906-1DC923A12725" -> "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + # (1 unchanged attribute hidden) + } + ~ second_nested_object = { + ~ attribute_two = "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" -> "5425587C-49EF-4C1E-A906-1DC923A12725" + # (1 unchanged attribute hidden) + } + } + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_objects_update/plan.json b/testing/equivalence-tests/outputs/nested_objects_update/plan.json new file mode 100644 index 0000000000..6fa46265d9 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects_update/plan.json @@ -0,0 +1,166 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "expressions": { + "id": { + "constant_value": "B2491EF0-9361-40FD-B25A-0332A1A5E052" + }, + "parent_object": { + "constant_value": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } + } + }, + "mode": "managed", + "name": "nested_object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "type": "tfcoremock_nested_object", + "values": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "type": "tfcoremock_nested_object", + "values": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_object.nested_object", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } + }, + "after_sensitive": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "after_unknown": {}, + "before": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + }, + "before_sensitive": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + } + }, + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_objects_update/state b/testing/equivalence-tests/outputs/nested_objects_update/state new file mode 100644 index 0000000000..b677f2c173 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects_update/state @@ -0,0 +1,14 @@ +# tfcoremock_nested_object.nested_object: +resource "tfcoremock_nested_object" "nested_object" { + id = "B2491EF0-9361-40FD-B25A-0332A1A5E052" + parent_object = { + first_nested_object = { + attribute_one = "09AE7244-7BFB-476B-912C-D1AB4E7E9622" + attribute_two = "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + second_nested_object = { + attribute_one = "63712BFE-78F8-42D3-A074-A78249E5E25E" + attribute_two = "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } +} diff --git a/testing/equivalence-tests/outputs/nested_objects_update/state.json b/testing/equivalence-tests/outputs/nested_objects_update/state.json new file mode 100644 index 0000000000..bedf6f9537 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_objects_update/state.json @@ -0,0 +1,36 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_object.nested_object", + "mode": "managed", + "name": "nested_object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "parent_object": { + "first_nested_object": {}, + "second_nested_object": {} + } + }, + "type": "tfcoremock_nested_object", + "values": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_set/apply.json b/testing/equivalence-tests/outputs/nested_set/apply.json new file mode 100644 index 0000000000..896d850dea --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_set.nested_set: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_set.nested_set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_set.nested_set", + "resource_key": null, + "resource_name": "nested_set", + "resource_type": "tfcoremock_nested_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_set.nested_set: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_nested_set.nested_set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_set.nested_set", + "resource_key": null, + "resource_name": "nested_set", + "resource_type": "tfcoremock_nested_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "510598F6-83FE-4090-8986-793293E90480", + "resource": { + "addr": "tfcoremock_nested_set.nested_set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_set.nested_set", + "resource_key": null, + "resource_name": "nested_set", + "resource_type": "tfcoremock_nested_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_set/plan b/testing/equivalence-tests/outputs/nested_set/plan new file mode 100644 index 0000000000..819b7bf805 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set/plan @@ -0,0 +1,30 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_nested_set.nested_set will be created + + resource "tfcoremock_nested_set" "nested_set" { + + id = "510598F6-83FE-4090-8986-793293E90480" + + sets = [ + + [ + + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + + "7E90963C-BE32-4411-B9DD-B02E7FE75766", + ], + + [ + + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE", + ], + + [], + ] + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_set/plan.json b/testing/equivalence-tests/outputs/nested_set/plan.json new file mode 100644 index 0000000000..aa90c453a9 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set/plan.json @@ -0,0 +1,125 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "expressions": { + "id": { + "constant_value": "510598F6-83FE-4090-8986-793293E90480" + }, + "sets": { + "constant_value": [ + [], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [ + "7E90963C-BE32-4411-B9DD-B02E7FE75766", + "29B6824A-5CB6-4C25-A359-727BAFEF25EB" + ] + ] + } + }, + "mode": "managed", + "name": "nested_set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "sets": [ + [ + false, + false + ], + [ + false + ], + [] + ] + }, + "type": "tfcoremock_nested_set", + "values": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [] + ] + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_set.nested_set", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [] + ] + }, + "after_sensitive": { + "sets": [ + [ + false, + false + ], + [ + false + ], + [] + ] + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_set/state b/testing/equivalence-tests/outputs/nested_set/state new file mode 100644 index 0000000000..7c47e96839 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set/state @@ -0,0 +1,14 @@ +# tfcoremock_nested_set.nested_set: +resource "tfcoremock_nested_set" "nested_set" { + id = "510598F6-83FE-4090-8986-793293E90480" + sets = [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766", + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE", + ], + [], + ] +} diff --git a/testing/equivalence-tests/outputs/nested_set/state.json b/testing/equivalence-tests/outputs/nested_set/state.json new file mode 100644 index 0000000000..1cef8af085 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set/state.json @@ -0,0 +1,42 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "sets": [ + [ + false, + false + ], + [ + false + ], + [] + ] + }, + "type": "tfcoremock_nested_set", + "values": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [] + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_set_update/apply.json b/testing/equivalence-tests/outputs/nested_set_update/apply.json new file mode 100644 index 0000000000..57e4e8c8f0 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_nested_set.nested_set: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_nested_set.nested_set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_set.nested_set", + "resource_key": null, + "resource_name": "nested_set", + "resource_type": "tfcoremock_nested_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_nested_set.nested_set: Modifying... [id=510598F6-83FE-4090-8986-793293E90480]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "510598F6-83FE-4090-8986-793293E90480", + "resource": { + "addr": "tfcoremock_nested_set.nested_set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_set.nested_set", + "resource_key": null, + "resource_name": "nested_set", + "resource_type": "tfcoremock_nested_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "510598F6-83FE-4090-8986-793293E90480", + "resource": { + "addr": "tfcoremock_nested_set.nested_set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_nested_set.nested_set", + "resource_key": null, + "resource_name": "nested_set", + "resource_type": "tfcoremock_nested_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_set_update/plan b/testing/equivalence-tests/outputs/nested_set_update/plan new file mode 100644 index 0000000000..643ad107f1 --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set_update/plan @@ -0,0 +1,35 @@ +tfcoremock_nested_set.nested_set: Refreshing state... [id=510598F6-83FE-4090-8986-793293E90480] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_nested_set.nested_set will be updated in-place + ~ resource "tfcoremock_nested_set" "nested_set" { + id = "510598F6-83FE-4090-8986-793293E90480" + ~ sets = [ + - [ + - "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + - "7E90963C-BE32-4411-B9DD-B02E7FE75766", + ], + - [], + + [ + + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + ], + + [ + + "7E90963C-BE32-4411-B9DD-B02E7FE75766", + ], + # (1 unchanged element hidden) + ] + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/nested_set_update/plan.json b/testing/equivalence-tests/outputs/nested_set_update/plan.json new file mode 100644 index 0000000000..8317feb74d --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set_update/plan.json @@ -0,0 +1,195 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "expressions": { + "id": { + "constant_value": "510598F6-83FE-4090-8986-793293E90480" + }, + "sets": { + "constant_value": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [ + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ] + ] + } + }, + "mode": "managed", + "name": "nested_set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_nested_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "sets": [ + [ + false + ], + [ + false + ], + [ + false + ] + ] + }, + "type": "tfcoremock_nested_set", + "values": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB" + ], + [ + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ] + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "sets": [ + [ + false, + false + ], + [ + false + ], + [] + ] + }, + "type": "tfcoremock_nested_set", + "values": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [] + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_nested_set.nested_set", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB" + ], + [ + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ] + ] + }, + "after_sensitive": { + "sets": [ + [ + false + ], + [ + false + ], + [ + false + ] + ] + }, + "after_unknown": {}, + "before": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [] + ] + }, + "before_sensitive": { + "sets": [ + [ + false, + false + ], + [ + false + ], + [] + ] + } + }, + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_nested_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/nested_set_update/state b/testing/equivalence-tests/outputs/nested_set_update/state new file mode 100644 index 0000000000..8e93cbe52a --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set_update/state @@ -0,0 +1,15 @@ +# tfcoremock_nested_set.nested_set: +resource "tfcoremock_nested_set" "nested_set" { + id = "510598F6-83FE-4090-8986-793293E90480" + sets = [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + ], + [ + "7E90963C-BE32-4411-B9DD-B02E7FE75766", + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE", + ], + ] +} diff --git a/testing/equivalence-tests/outputs/nested_set_update/state.json b/testing/equivalence-tests/outputs/nested_set_update/state.json new file mode 100644 index 0000000000..a9d25b08cb --- /dev/null +++ b/testing/equivalence-tests/outputs/nested_set_update/state.json @@ -0,0 +1,44 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_nested_set.nested_set", + "mode": "managed", + "name": "nested_set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "sets": [ + [ + false + ], + [ + false + ], + [ + false + ] + ] + }, + "type": "tfcoremock_nested_set", + "values": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB" + ], + [ + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ] + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/null_provider_delete/apply.json b/testing/equivalence-tests/outputs/null_provider_delete/apply.json new file mode 100644 index 0000000000..d766ed1273 --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_delete/apply.json @@ -0,0 +1,78 @@ +[ + { + "@level": "info", + "@message": "null_resource.null_resource: Plan to delete", + "@module": "terraform.ui", + "change": { + "action": "delete", + "reason": "delete_because_no_resource_config", + "resource": { + "addr": "null_resource.null_resource", + "implied_provider": "null", + "module": "", + "resource": "null_resource.null_resource", + "resource_key": null, + "resource_name": "null_resource", + "resource_type": "null_resource" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "null_resource.null_resource: Destroying... [id=7115293105928418144]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "7115293105928418144", + "resource": { + "addr": "null_resource.null_resource", + "implied_provider": "null", + "module": "", + "resource": "null_resource.null_resource", + "resource_key": null, + "resource_name": "null_resource", + "resource_type": "null_resource" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "null_resource.null_resource", + "implied_provider": "null", + "module": "", + "resource": "null_resource.null_resource", + "resource_key": null, + "resource_name": "null_resource", + "resource_type": "null_resource" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/null_provider_delete/plan b/testing/equivalence-tests/outputs/null_provider_delete/plan new file mode 100644 index 0000000000..4c799e4876 --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_delete/plan @@ -0,0 +1,22 @@ +null_resource.null_resource: Refreshing state... [id=7115293105928418144] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + - destroy + +Terraform will perform the following actions: + + # null_resource.null_resource will be destroyed + # (because null_resource.null_resource is not in configuration) + - resource "null_resource" "null_resource" { + - id = "7115293105928418144" -> null + } + +Plan: 0 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/null_provider_delete/plan.json b/testing/equivalence-tests/outputs/null_provider_delete/plan.json new file mode 100644 index 0000000000..72ca35c485 --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_delete/plan.json @@ -0,0 +1,64 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "null": { + "full_name": "registry.terraform.io/hashicorp/null", + "name": "null", + "version_constraint": "3.1.1" + } + }, + "root_module": {} + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": {} + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "null_resource.null_resource", + "mode": "managed", + "name": "null_resource", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "sensitive_values": {}, + "type": "null_resource", + "values": { + "id": "7115293105928418144", + "triggers": null + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "delete_because_no_resource_config", + "address": "null_resource.null_resource", + "change": { + "actions": [ + "delete" + ], + "after": null, + "after_sensitive": false, + "after_unknown": {}, + "before": { + "id": "7115293105928418144", + "triggers": null + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "null_resource", + "provider_name": "registry.terraform.io/hashicorp/null", + "type": "null_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/null_provider_delete/state b/testing/equivalence-tests/outputs/null_provider_delete/state new file mode 100644 index 0000000000..222a60b6eb --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_delete/state @@ -0,0 +1 @@ +The state file is empty. No resources are represented. diff --git a/testing/equivalence-tests/outputs/null_provider_delete/state.json b/testing/equivalence-tests/outputs/null_provider_delete/state.json new file mode 100644 index 0000000000..00f8f1ca36 --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_delete/state.json @@ -0,0 +1,3 @@ +{ + "format_version": "1.0" +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/null_provider_update/apply.json b/testing/equivalence-tests/outputs/null_provider_update/apply.json new file mode 100644 index 0000000000..42438bd2f7 --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_update/apply.json @@ -0,0 +1,22 @@ +[ + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/null_provider_update/plan b/testing/equivalence-tests/outputs/null_provider_update/plan new file mode 100644 index 0000000000..c2ee48084b --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_update/plan @@ -0,0 +1,6 @@ +null_resource.null_resource: Refreshing state... [id=3637779521417605172] + +No changes. Your infrastructure matches the configuration. + +Terraform has compared your real infrastructure against your configuration +and found no differences, so no changes are needed. diff --git a/testing/equivalence-tests/outputs/null_provider_update/plan.json b/testing/equivalence-tests/outputs/null_provider_update/plan.json new file mode 100644 index 0000000000..ef949f810c --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_update/plan.json @@ -0,0 +1,93 @@ +{ + "applyable": false, + "complete": true, + "configuration": { + "provider_config": { + "null": { + "full_name": "registry.terraform.io/hashicorp/null", + "name": "null", + "version_constraint": "3.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "null_resource.null_resource", + "mode": "managed", + "name": "null_resource", + "provider_config_key": "null", + "schema_version": 0, + "type": "null_resource" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "null_resource.null_resource", + "mode": "managed", + "name": "null_resource", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "sensitive_values": {}, + "type": "null_resource", + "values": { + "id": "3637779521417605172", + "triggers": null + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "null_resource.null_resource", + "mode": "managed", + "name": "null_resource", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "sensitive_values": {}, + "type": "null_resource", + "values": { + "id": "3637779521417605172", + "triggers": null + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "null_resource.null_resource", + "change": { + "actions": [ + "no-op" + ], + "after": { + "id": "3637779521417605172", + "triggers": null + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "id": "3637779521417605172", + "triggers": null + }, + "before_sensitive": {} + }, + "mode": "managed", + "name": "null_resource", + "provider_name": "registry.terraform.io/hashicorp/null", + "type": "null_resource" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/null_provider_update/state b/testing/equivalence-tests/outputs/null_provider_update/state new file mode 100644 index 0000000000..651823f4ed --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_update/state @@ -0,0 +1,4 @@ +# null_resource.null_resource: +resource "null_resource" "null_resource" { + id = "3637779521417605172" +} diff --git a/testing/equivalence-tests/outputs/null_provider_update/state.json b/testing/equivalence-tests/outputs/null_provider_update/state.json new file mode 100644 index 0000000000..77ad52e453 --- /dev/null +++ b/testing/equivalence-tests/outputs/null_provider_update/state.json @@ -0,0 +1,22 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "null_resource.null_resource", + "mode": "managed", + "name": "null_resource", + "provider_name": "registry.terraform.io/hashicorp/null", + "schema_version": 0, + "sensitive_values": {}, + "type": "null_resource", + "values": { + "id": "3637779521417605172", + "triggers": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_list/apply.json b/testing/equivalence-tests/outputs/replace_within_list/apply.json new file mode 100644 index 0000000000..a423983d36 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_list/apply.json @@ -0,0 +1,115 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_list.list: Plan to replace", + "@module": "terraform.ui", + "change": { + "action": "replace", + "reason": "cannot_update", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_list.list: Destroying... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_list.list: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_list.list", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_list.list", + "resource_key": null, + "resource_name": "list", + "resource_type": "tfcoremock_list" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_list/plan b/testing/equivalence-tests/outputs/replace_within_list/plan new file mode 100644 index 0000000000..e5c73a3fe3 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_list/plan @@ -0,0 +1,27 @@ +tfcoremock_list.list: Refreshing state... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # tfcoremock_list.list must be replaced +-/+ resource "tfcoremock_list" "list" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + ~ list = [ + ~ { + ~ id = "6A8C6A29-D417-480A-BE19-12D7398B3178" -> "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" # forces replacement + }, + # (2 unchanged elements hidden) + ] + } + +Plan: 1 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/replace_within_list/plan.json b/testing/equivalence-tests/outputs/replace_within_list/plan.json new file mode 100644 index 0000000000..2b954ce8de --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_list/plan.json @@ -0,0 +1,184 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "expressions": { + "id": { + "constant_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "list": { + "constant_value": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + }, + "mode": "managed", + "name": "list", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_list" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + {} + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "list": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + {} + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "list": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "6A8C6A29-D417-480A-BE19-12D7398B3178" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "replace_because_cannot_update", + "address": "tfcoremock_list.list", + "change": { + "actions": [ + "delete", + "create" + ], + "after": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "list": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + }, + "after_sensitive": { + "list": [ + {}, + {}, + {} + ] + }, + "after_unknown": {}, + "before": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "list": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "6A8C6A29-D417-480A-BE19-12D7398B3178" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + }, + "before_sensitive": { + "list": [ + {}, + {}, + {} + ] + }, + "replace_paths": [ + [ + "list", + 1, + "id" + ] + ] + }, + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_list" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_list/state b/testing/equivalence-tests/outputs/replace_within_list/state new file mode 100644 index 0000000000..6f162b176a --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_list/state @@ -0,0 +1,15 @@ +# tfcoremock_list.list: +resource "tfcoremock_list" "list" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + list = [ + { + id = "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + id = "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + ] +} diff --git a/testing/equivalence-tests/outputs/replace_within_list/state.json b/testing/equivalence-tests/outputs/replace_within_list/state.json new file mode 100644 index 0000000000..b4a486baac --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_list/state.json @@ -0,0 +1,38 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_list.list", + "mode": "managed", + "name": "list", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "list": [ + {}, + {}, + {} + ] + }, + "type": "tfcoremock_list", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "list": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_map/apply.json b/testing/equivalence-tests/outputs/replace_within_map/apply.json new file mode 100644 index 0000000000..46bd9bc939 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_map/apply.json @@ -0,0 +1,115 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_map.map: Plan to replace", + "@module": "terraform.ui", + "change": { + "action": "replace", + "reason": "cannot_update", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_map.map: Destroying... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_map.map: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_map.map", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_map.map", + "resource_key": null, + "resource_name": "map", + "resource_type": "tfcoremock_map" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_map/plan b/testing/equivalence-tests/outputs/replace_within_map/plan new file mode 100644 index 0000000000..46ab58c665 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_map/plan @@ -0,0 +1,27 @@ +tfcoremock_map.map: Refreshing state... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # tfcoremock_map.map must be replaced +-/+ resource "tfcoremock_map" "map" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + ~ map = { + ~ "key_two" = { + ~ id = "56C7E07F-B9DF-4799-AF62-E703D1167A51" -> "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" # forces replacement + }, + # (2 unchanged elements hidden) + } + } + +Plan: 1 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/replace_within_map/plan.json b/testing/equivalence-tests/outputs/replace_within_map/plan.json new file mode 100644 index 0000000000..bd795d7f48 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_map/plan.json @@ -0,0 +1,184 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "expressions": { + "id": { + "constant_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "map": { + "constant_value": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + } + }, + "mode": "managed", + "name": "map", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_map" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": { + "key_one": {}, + "key_three": {}, + "key_two": {} + } + }, + "type": "tfcoremock_map", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "map": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": { + "key_one": {}, + "key_three": {}, + "key_two": {} + } + }, + "type": "tfcoremock_map", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "map": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "replace_because_cannot_update", + "address": "tfcoremock_map.map", + "change": { + "actions": [ + "delete", + "create" + ], + "after": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "map": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + }, + "after_sensitive": { + "map": { + "key_one": {}, + "key_three": {}, + "key_two": {} + } + }, + "after_unknown": {}, + "before": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "map": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + }, + "before_sensitive": { + "map": { + "key_one": {}, + "key_three": {}, + "key_two": {} + } + }, + "replace_paths": [ + [ + "map", + "key_two", + "id" + ] + ] + }, + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_map" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_map/state b/testing/equivalence-tests/outputs/replace_within_map/state new file mode 100644 index 0000000000..f6f1b2cc0f --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_map/state @@ -0,0 +1,15 @@ +# tfcoremock_map.map: +resource "tfcoremock_map" "map" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + map = { + "key_one" = { + id = "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three" = { + id = "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two" = { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + } +} diff --git a/testing/equivalence-tests/outputs/replace_within_map/state.json b/testing/equivalence-tests/outputs/replace_within_map/state.json new file mode 100644 index 0000000000..02fae77fe6 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_map/state.json @@ -0,0 +1,38 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_map.map", + "mode": "managed", + "name": "map", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "map": { + "key_one": {}, + "key_three": {}, + "key_two": {} + } + }, + "type": "tfcoremock_map", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "map": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_object/apply.json b/testing/equivalence-tests/outputs/replace_within_object/apply.json new file mode 100644 index 0000000000..301a5dcc72 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_object/apply.json @@ -0,0 +1,115 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_object.object: Plan to replace", + "@module": "terraform.ui", + "change": { + "action": "replace", + "reason": "cannot_update", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Destroying... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_object/plan b/testing/equivalence-tests/outputs/replace_within_object/plan new file mode 100644 index 0000000000..2df539c6e4 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_object/plan @@ -0,0 +1,24 @@ +tfcoremock_object.object: Refreshing state... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # tfcoremock_object.object must be replaced +-/+ resource "tfcoremock_object" "object" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + ~ object = { + ~ id = "56C7E07F-B9DF-4799-AF62-E703D1167A51" -> "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" # forces replacement + } + } + +Plan: 1 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/replace_within_object/plan.json b/testing/equivalence-tests/outputs/replace_within_object/plan.json new file mode 100644 index 0000000000..e85424ca7d --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_object/plan.json @@ -0,0 +1,127 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "expressions": { + "id": { + "constant_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "object": { + "constant_value": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + }, + "mode": "managed", + "name": "object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "object": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "object": { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "replace_because_cannot_update", + "address": "tfcoremock_object.object", + "change": { + "actions": [ + "delete", + "create" + ], + "after": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "object": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + }, + "after_sensitive": { + "object": {} + }, + "after_unknown": {}, + "before": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "object": { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + }, + "before_sensitive": { + "object": {} + }, + "replace_paths": [ + [ + "object", + "id" + ] + ] + }, + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_object/state b/testing/equivalence-tests/outputs/replace_within_object/state new file mode 100644 index 0000000000..14a420edad --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_object/state @@ -0,0 +1,7 @@ +# tfcoremock_object.object: +resource "tfcoremock_object" "object" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + object = { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } +} diff --git a/testing/equivalence-tests/outputs/replace_within_object/state.json b/testing/equivalence-tests/outputs/replace_within_object/state.json new file mode 100644 index 0000000000..a2f7433ee5 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_object/state.json @@ -0,0 +1,26 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "object": { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_set/apply.json b/testing/equivalence-tests/outputs/replace_within_set/apply.json new file mode 100644 index 0000000000..75768b0572 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_set/apply.json @@ -0,0 +1,115 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_set.set: Plan to replace", + "@module": "terraform.ui", + "change": { + "action": "replace", + "reason": "cannot_update", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_set.set: Destroying... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_set.set: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "resource": { + "addr": "tfcoremock_set.set", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_set.set", + "resource_key": null, + "resource_name": "set", + "resource_type": "tfcoremock_set" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_set/plan b/testing/equivalence-tests/outputs/replace_within_set/plan new file mode 100644 index 0000000000..252d0b4648 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_set/plan @@ -0,0 +1,30 @@ +tfcoremock_set.set: Refreshing state... [id=F40F2AB4-100C-4AE8-BFD0-BF332A158415] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # tfcoremock_set.set must be replaced +-/+ resource "tfcoremock_set" "set" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + ~ set = [ + - { # forces replacement + - id = "56C7E07F-B9DF-4799-AF62-E703D1167A51" -> null + }, + + { # forces replacement + + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + # (2 unchanged elements hidden) + ] + } + +Plan: 1 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/replace_within_set/plan.json b/testing/equivalence-tests/outputs/replace_within_set/plan.json new file mode 100644 index 0000000000..68f81b341f --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_set/plan.json @@ -0,0 +1,182 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "expressions": { + "id": { + "constant_value": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "set": { + "constant_value": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + }, + "mode": "managed", + "name": "set", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_set" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + {}, + {}, + {} + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "set": [ + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + {}, + {}, + {} + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "set": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + ] + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "replace_because_cannot_update", + "address": "tfcoremock_set.set", + "change": { + "actions": [ + "delete", + "create" + ], + "after": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "set": [ + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + }, + "after_sensitive": { + "set": [ + {}, + {}, + {} + ] + }, + "after_unknown": {}, + "before": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "set": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + ] + }, + "before_sensitive": { + "set": [ + {}, + {}, + {} + ] + }, + "replace_paths": [ + [ + "set" + ] + ] + }, + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_set" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/replace_within_set/state b/testing/equivalence-tests/outputs/replace_within_set/state new file mode 100644 index 0000000000..1eabcb4128 --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_set/state @@ -0,0 +1,15 @@ +# tfcoremock_set.set: +resource "tfcoremock_set" "set" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + set = [ + { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + id = "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + id = "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + ] +} diff --git a/testing/equivalence-tests/outputs/replace_within_set/state.json b/testing/equivalence-tests/outputs/replace_within_set/state.json new file mode 100644 index 0000000000..992019e97d --- /dev/null +++ b/testing/equivalence-tests/outputs/replace_within_set/state.json @@ -0,0 +1,38 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_set.set", + "mode": "managed", + "name": "set", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "set": [ + {}, + {}, + {} + ] + }, + "type": "tfcoremock_set", + "values": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "set": [ + { + "id": "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object/apply.json b/testing/equivalence-tests/outputs/simple_object/apply.json new file mode 100644 index 0000000000..033815ef89 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object/apply.json @@ -0,0 +1,77 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_object.object: Plan to create", + "@module": "terraform.ui", + "change": { + "action": "create", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "AF9833AE-3434-4D0B-8B69-F4B992565D9F", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object/plan b/testing/equivalence-tests/outputs/simple_object/plan new file mode 100644 index 0000000000..9d5ef63405 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object/plan @@ -0,0 +1,25 @@ + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # tfcoremock_object.object will be created + + resource "tfcoremock_object" "object" { + + id = "AF9833AE-3434-4D0B-8B69-F4B992565D9F" + + object = { + + boolean = true + + number = 10 + + string = "Hello, world!" + } + } + +Plan: 1 to add, 0 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/simple_object/plan.json b/testing/equivalence-tests/outputs/simple_object/plan.json new file mode 100644 index 0000000000..230245c9c8 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object/plan.json @@ -0,0 +1,92 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "expressions": { + "id": { + "constant_value": "AF9833AE-3434-4D0B-8B69-F4B992565D9F" + }, + "object": { + "constant_value": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + }, + "mode": "managed", + "name": "object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "AF9833AE-3434-4D0B-8B69-F4B992565D9F", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + }, + "resource_changes": [ + { + "address": "tfcoremock_object.object", + "change": { + "actions": [ + "create" + ], + "after": { + "id": "AF9833AE-3434-4D0B-8B69-F4B992565D9F", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "after_sensitive": { + "object": {} + }, + "after_unknown": {}, + "before": null, + "before_sensitive": false + }, + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object/state b/testing/equivalence-tests/outputs/simple_object/state new file mode 100644 index 0000000000..d3c0e21cc6 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object/state @@ -0,0 +1,9 @@ +# tfcoremock_object.object: +resource "tfcoremock_object" "object" { + id = "AF9833AE-3434-4D0B-8B69-F4B992565D9F" + object = { + boolean = true + number = 10 + string = "Hello, world!" + } +} diff --git a/testing/equivalence-tests/outputs/simple_object/state.json b/testing/equivalence-tests/outputs/simple_object/state.json new file mode 100644 index 0000000000..5f29eade26 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object/state.json @@ -0,0 +1,28 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "AF9833AE-3434-4D0B-8B69-F4B992565D9F", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_empty/apply.json b/testing/equivalence-tests/outputs/simple_object_empty/apply.json new file mode 100644 index 0000000000..2f970be655 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_empty/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_object.object: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Modifying... [id=00e14fba-4d56-6cc5-b685-633555376e3f]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "00e14fba-4d56-6cc5-b685-633555376e3f", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "00e14fba-4d56-6cc5-b685-633555376e3f", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_empty/plan b/testing/equivalence-tests/outputs/simple_object_empty/plan new file mode 100644 index 0000000000..2144d18d06 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_empty/plan @@ -0,0 +1,26 @@ +tfcoremock_object.object: Refreshing state... [id=00e14fba-4d56-6cc5-b685-633555376e3f] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_object.object will be updated in-place + ~ resource "tfcoremock_object" "object" { + id = "00e14fba-4d56-6cc5-b685-633555376e3f" + ~ object = { + - boolean = true -> null + - number = 10 -> null + - string = "Hello, world!" -> null + } + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/simple_object_empty/plan.json b/testing/equivalence-tests/outputs/simple_object_empty/plan.json new file mode 100644 index 0000000000..98c76c3eab --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_empty/plan.json @@ -0,0 +1,122 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "expressions": { + "object": { + "constant_value": {} + } + }, + "mode": "managed", + "name": "object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": null, + "number": null, + "string": null + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_object.object", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": null, + "number": null, + "string": null + } + }, + "after_sensitive": { + "object": {} + }, + "after_unknown": {}, + "before": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "before_sensitive": { + "object": {} + } + }, + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_empty/state b/testing/equivalence-tests/outputs/simple_object_empty/state new file mode 100644 index 0000000000..f31cb96ea0 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_empty/state @@ -0,0 +1,5 @@ +# tfcoremock_object.object: +resource "tfcoremock_object" "object" { + id = "00e14fba-4d56-6cc5-b685-633555376e3f" + object = {} +} diff --git a/testing/equivalence-tests/outputs/simple_object_empty/state.json b/testing/equivalence-tests/outputs/simple_object_empty/state.json new file mode 100644 index 0000000000..70d840591e --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_empty/state.json @@ -0,0 +1,28 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": null, + "number": null, + "string": null + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_null/apply.json b/testing/equivalence-tests/outputs/simple_object_null/apply.json new file mode 100644 index 0000000000..2f970be655 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_null/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_object.object: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Modifying... [id=00e14fba-4d56-6cc5-b685-633555376e3f]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "00e14fba-4d56-6cc5-b685-633555376e3f", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "00e14fba-4d56-6cc5-b685-633555376e3f", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_null/plan b/testing/equivalence-tests/outputs/simple_object_null/plan new file mode 100644 index 0000000000..35d37fc9ce --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_null/plan @@ -0,0 +1,26 @@ +tfcoremock_object.object: Refreshing state... [id=00e14fba-4d56-6cc5-b685-633555376e3f] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_object.object will be updated in-place + ~ resource "tfcoremock_object" "object" { + id = "00e14fba-4d56-6cc5-b685-633555376e3f" + - object = { + - boolean = true -> null + - number = 10 -> null + - string = "Hello, world!" -> null + } -> null + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/simple_object_null/plan.json b/testing/equivalence-tests/outputs/simple_object_null/plan.json new file mode 100644 index 0000000000..df7695adcf --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_null/plan.json @@ -0,0 +1,105 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": null + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_object.object", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": null + }, + "after_sensitive": {}, + "after_unknown": {}, + "before": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "before_sensitive": { + "object": {} + } + }, + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_null/state b/testing/equivalence-tests/outputs/simple_object_null/state new file mode 100644 index 0000000000..af3a89c560 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_null/state @@ -0,0 +1,4 @@ +# tfcoremock_object.object: +resource "tfcoremock_object" "object" { + id = "00e14fba-4d56-6cc5-b685-633555376e3f" +} diff --git a/testing/equivalence-tests/outputs/simple_object_null/state.json b/testing/equivalence-tests/outputs/simple_object_null/state.json new file mode 100644 index 0000000000..6101209870 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_null/state.json @@ -0,0 +1,22 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": {}, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": null + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_replace/apply.json b/testing/equivalence-tests/outputs/simple_object_replace/apply.json new file mode 100644 index 0000000000..992a074d33 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_replace/apply.json @@ -0,0 +1,115 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_object.object: Plan to replace", + "@module": "terraform.ui", + "change": { + "action": "replace", + "reason": "cannot_update", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Destroying... [id=a0ed13ec-116b-14c4-7437-418e217d3659]", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "id_key": "id", + "id_value": "a0ed13ec-116b-14c4-7437-418e217d3659", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "delete", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Creating...", + "@module": "terraform.ui", + "hook": { + "action": "create", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "create", + "id_key": "id", + "id_value": "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 1 added, 0 changed, 1 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 1, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 1 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_replace/plan b/testing/equivalence-tests/outputs/simple_object_replace/plan new file mode 100644 index 0000000000..cbd97123b8 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_replace/plan @@ -0,0 +1,22 @@ +tfcoremock_object.object: Refreshing state... [id=a0ed13ec-116b-14c4-7437-418e217d3659] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: +-/+ destroy and then create replacement + +Terraform will perform the following actions: + + # tfcoremock_object.object must be replaced +-/+ resource "tfcoremock_object" "object" { + ~ id = "a0ed13ec-116b-14c4-7437-418e217d3659" -> "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3" # forces replacement + # (1 unchanged attribute hidden) + } + +Plan: 1 to add, 0 to change, 1 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/simple_object_replace/plan.json b/testing/equivalence-tests/outputs/simple_object_replace/plan.json new file mode 100644 index 0000000000..5dc83cdaad --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_replace/plan.json @@ -0,0 +1,136 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "expressions": { + "id": { + "constant_value": "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3" + }, + "object": { + "constant_value": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + }, + "mode": "managed", + "name": "object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "a0ed13ec-116b-14c4-7437-418e217d3659", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "action_reason": "replace_because_cannot_update", + "address": "tfcoremock_object.object", + "change": { + "actions": [ + "delete", + "create" + ], + "after": { + "id": "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "after_sensitive": { + "object": {} + }, + "after_unknown": {}, + "before": { + "id": "a0ed13ec-116b-14c4-7437-418e217d3659", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "before_sensitive": { + "object": {} + }, + "replace_paths": [ + [ + "id" + ] + ] + }, + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_replace/state b/testing/equivalence-tests/outputs/simple_object_replace/state new file mode 100644 index 0000000000..454092883a --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_replace/state @@ -0,0 +1,9 @@ +# tfcoremock_object.object: +resource "tfcoremock_object" "object" { + id = "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3" + object = { + boolean = true + number = 10 + string = "Hello, world!" + } +} diff --git a/testing/equivalence-tests/outputs/simple_object_replace/state.json b/testing/equivalence-tests/outputs/simple_object_replace/state.json new file mode 100644 index 0000000000..abdc14aa07 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_replace/state.json @@ -0,0 +1,28 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_update/apply.json b/testing/equivalence-tests/outputs/simple_object_update/apply.json new file mode 100644 index 0000000000..2f970be655 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_update/apply.json @@ -0,0 +1,79 @@ +[ + { + "@level": "info", + "@message": "tfcoremock_object.object: Plan to update", + "@module": "terraform.ui", + "change": { + "action": "update", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "planned_change" + }, + { + "@level": "info", + "@message": "tfcoremock_object.object: Modifying... [id=00e14fba-4d56-6cc5-b685-633555376e3f]", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "00e14fba-4d56-6cc5-b685-633555376e3f", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_start" + }, + { + "@level": "info", + "@module": "terraform.ui", + "hook": { + "action": "update", + "id_key": "id", + "id_value": "00e14fba-4d56-6cc5-b685-633555376e3f", + "resource": { + "addr": "tfcoremock_object.object", + "implied_provider": "tfcoremock", + "module": "", + "resource": "tfcoremock_object.object", + "resource_key": null, + "resource_name": "object", + "resource_type": "tfcoremock_object" + } + }, + "type": "apply_complete" + }, + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 1 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 1, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 0", + "@module": "terraform.ui", + "outputs": {}, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_update/plan b/testing/equivalence-tests/outputs/simple_object_update/plan new file mode 100644 index 0000000000..0e1e6e1726 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_update/plan @@ -0,0 +1,26 @@ +tfcoremock_object.object: Refreshing state... [id=00e14fba-4d56-6cc5-b685-633555376e3f] + +Terraform used the selected providers to generate the following execution +plan. Resource actions are indicated with the following symbols: + ~ update in-place + +Terraform will perform the following actions: + + # tfcoremock_object.object will be updated in-place + ~ resource "tfcoremock_object" "object" { + id = "00e14fba-4d56-6cc5-b685-633555376e3f" + ~ object = { + ~ boolean = true -> false + ~ number = 10 -> 2 + ~ string = "Hello, world!" -> "Hello, a totally different world!" + } + } + +Plan: 0 to add, 1 to change, 0 to destroy. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/simple_object_update/plan.json b/testing/equivalence-tests/outputs/simple_object_update/plan.json new file mode 100644 index 0000000000..76bc1bcc22 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_update/plan.json @@ -0,0 +1,126 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "provider_config": { + "tfcoremock": { + "full_name": "registry.terraform.io/hashicorp/tfcoremock", + "name": "tfcoremock", + "version_constraint": "0.1.1" + } + }, + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "expressions": { + "object": { + "constant_value": { + "boolean": false, + "number": 2, + "string": "Hello, a totally different world!" + } + } + }, + "mode": "managed", + "name": "object", + "provider_config_key": "tfcoremock", + "schema_version": 0, + "type": "tfcoremock_object" + } + ] + } + }, + "errored": false, + "format_version": "1.2", + "planned_values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": false, + "number": 2, + "string": "Hello, a totally different world!" + } + } + } + ] + } + }, + "prior_state": { + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + } + } + ] + } + } + }, + "resource_changes": [ + { + "address": "tfcoremock_object.object", + "change": { + "actions": [ + "update" + ], + "after": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": false, + "number": 2, + "string": "Hello, a totally different world!" + } + }, + "after_sensitive": { + "object": {} + }, + "after_unknown": {}, + "before": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "before_sensitive": { + "object": {} + } + }, + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "type": "tfcoremock_object" + } + ] +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/simple_object_update/state b/testing/equivalence-tests/outputs/simple_object_update/state new file mode 100644 index 0000000000..efa6c14033 --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_update/state @@ -0,0 +1,9 @@ +# tfcoremock_object.object: +resource "tfcoremock_object" "object" { + id = "00e14fba-4d56-6cc5-b685-633555376e3f" + object = { + boolean = false + number = 2 + string = "Hello, a totally different world!" + } +} diff --git a/testing/equivalence-tests/outputs/simple_object_update/state.json b/testing/equivalence-tests/outputs/simple_object_update/state.json new file mode 100644 index 0000000000..8640460c2e --- /dev/null +++ b/testing/equivalence-tests/outputs/simple_object_update/state.json @@ -0,0 +1,28 @@ +{ + "format_version": "1.0", + "values": { + "root_module": { + "resources": [ + { + "address": "tfcoremock_object.object", + "mode": "managed", + "name": "object", + "provider_name": "registry.terraform.io/hashicorp/tfcoremock", + "schema_version": 0, + "sensitive_values": { + "object": {} + }, + "type": "tfcoremock_object", + "values": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": false, + "number": 2, + "string": "Hello, a totally different world!" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/variables_and_outputs/apply.json b/testing/equivalence-tests/outputs/variables_and_outputs/apply.json new file mode 100644 index 0000000000..2f267f861d --- /dev/null +++ b/testing/equivalence-tests/outputs/variables_and_outputs/apply.json @@ -0,0 +1,124 @@ +[ + { + "@level": "info", + "@message": "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.", + "@module": "terraform.ui", + "changes": { + "add": 0, + "change": 0, + "import": 0, + "operation": "apply", + "remove": 0 + }, + "type": "change_summary" + }, + { + "@level": "info", + "@message": "Outputs: 5", + "@module": "terraform.ui", + "outputs": { + "list_empty_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [] + }, + "list_no_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [ + { + "optional_attribute": null, + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + "optional_attribute": "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + "optional_attribute": "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + "optional_attribute_with_default": "92E855B2-A444-49DF-AFCA-2B5B017451B4", + "required_attribute": "9F9922C4-B426-4648-96AE-804A6F52F778" + } + ] + }, + "nested_optional_object": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": null + } + }, + "nested_optional_object_with_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + }, + "nested_optional_object_with_embedded_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + } + }, + "type": "outputs" + } +] \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/variables_and_outputs/plan b/testing/equivalence-tests/outputs/variables_and_outputs/plan new file mode 100644 index 0000000000..8b8a9f0a74 --- /dev/null +++ b/testing/equivalence-tests/outputs/variables_and_outputs/plan @@ -0,0 +1,43 @@ + +Changes to Outputs: + + list_empty_default = [] + + list_no_default = [ + + { + + optional_attribute = null + + optional_attribute_with_default = "Hello, world!" + + required_attribute = "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + + { + + optional_attribute = "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487" + + optional_attribute_with_default = "Hello, world!" + + required_attribute = "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + + { + + optional_attribute = "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C" + + optional_attribute_with_default = "92E855B2-A444-49DF-AFCA-2B5B017451B4" + + required_attribute = "9F9922C4-B426-4648-96AE-804A6F52F778" + }, + ] + + nested_optional_object = { + + nested_object = null + } + + nested_optional_object_with_default = { + + nested_object = { + + flag = false + } + } + + nested_optional_object_with_embedded_default = { + + nested_object = { + + flag = false + } + } + +You can apply this plan to save these new output values to the Terraform +state, without changing any real infrastructure. + +───────────────────────────────────────────────────────────────────────────── + +Saved the plan to: equivalence_test_plan + +To perform exactly these actions, run the following command to apply: + terraform apply "equivalence_test_plan" diff --git a/testing/equivalence-tests/outputs/variables_and_outputs/plan.json b/testing/equivalence-tests/outputs/variables_and_outputs/plan.json new file mode 100644 index 0000000000..8560bac370 --- /dev/null +++ b/testing/equivalence-tests/outputs/variables_and_outputs/plan.json @@ -0,0 +1,405 @@ +{ + "applyable": true, + "complete": true, + "configuration": { + "root_module": { + "outputs": { + "list_empty_default": { + "expression": { + "references": [ + "var.list_empty_default" + ] + } + }, + "list_no_default": { + "expression": { + "references": [ + "var.list_no_default" + ] + } + }, + "nested_optional_object": { + "expression": { + "references": [ + "var.nested_optional_object" + ] + } + }, + "nested_optional_object_with_default": { + "expression": { + "references": [ + "var.nested_optional_object_with_default" + ] + } + }, + "nested_optional_object_with_embedded_default": { + "expression": { + "references": [ + "var.nested_optional_object_with_embedded_default" + ] + } + } + }, + "variables": { + "list_empty_default": { + "default": [] + }, + "list_no_default": {}, + "nested_optional_object": { + "default": { + "nested_object": null + } + }, + "nested_optional_object_with_default": { + "default": { + "nested_object": { + "flag": false + } + } + }, + "nested_optional_object_with_embedded_default": { + "default": { + "nested_object": { + "flag": false + } + } + } + } + } + }, + "errored": false, + "format_version": "1.2", + "output_changes": { + "list_empty_default": { + "actions": [ + "create" + ], + "after": [], + "after_sensitive": false, + "after_unknown": false, + "before": null, + "before_sensitive": false + }, + "list_no_default": { + "actions": [ + "create" + ], + "after": [ + { + "optional_attribute": null, + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + "optional_attribute": "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + "optional_attribute": "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + "optional_attribute_with_default": "92E855B2-A444-49DF-AFCA-2B5B017451B4", + "required_attribute": "9F9922C4-B426-4648-96AE-804A6F52F778" + } + ], + "after_sensitive": false, + "after_unknown": false, + "before": null, + "before_sensitive": false + }, + "nested_optional_object": { + "actions": [ + "create" + ], + "after": { + "nested_object": null + }, + "after_sensitive": false, + "after_unknown": false, + "before": null, + "before_sensitive": false + }, + "nested_optional_object_with_default": { + "actions": [ + "create" + ], + "after": { + "nested_object": { + "flag": false + } + }, + "after_sensitive": false, + "after_unknown": false, + "before": null, + "before_sensitive": false + }, + "nested_optional_object_with_embedded_default": { + "actions": [ + "create" + ], + "after": { + "nested_object": { + "flag": false + } + }, + "after_sensitive": false, + "after_unknown": false, + "before": null, + "before_sensitive": false + } + }, + "planned_values": { + "outputs": { + "list_empty_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [] + }, + "list_no_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [ + { + "optional_attribute": null, + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + "optional_attribute": "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + "optional_attribute": "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + "optional_attribute_with_default": "92E855B2-A444-49DF-AFCA-2B5B017451B4", + "required_attribute": "9F9922C4-B426-4648-96AE-804A6F52F778" + } + ] + }, + "nested_optional_object": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": null + } + }, + "nested_optional_object_with_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + }, + "nested_optional_object_with_embedded_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + } + }, + "root_module": {} + }, + "prior_state": { + "format_version": "1.0", + "values": { + "outputs": { + "list_empty_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [] + }, + "list_no_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [ + { + "optional_attribute": null, + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + "optional_attribute": "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + "optional_attribute": "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + "optional_attribute_with_default": "92E855B2-A444-49DF-AFCA-2B5B017451B4", + "required_attribute": "9F9922C4-B426-4648-96AE-804A6F52F778" + } + ] + }, + "nested_optional_object": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": null + } + }, + "nested_optional_object_with_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + }, + "nested_optional_object_with_embedded_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + } + }, + "root_module": {} + } + }, + "variables": { + "list_empty_default": { + "value": [] + }, + "list_no_default": { + "value": [ + { + "required_attribute": "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + "optional_attribute": "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + "required_attribute": "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + "optional_attribute": "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + "optional_attribute_with_default": "92E855B2-A444-49DF-AFCA-2B5B017451B4", + "required_attribute": "9F9922C4-B426-4648-96AE-804A6F52F778" + } + ] + }, + "nested_optional_object": { + "value": { + "nested_object": null + } + }, + "nested_optional_object_with_default": { + "value": { + "nested_object": { + "flag": false + } + } + }, + "nested_optional_object_with_embedded_default": { + "value": { + "nested_object": { + "flag": false + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/outputs/variables_and_outputs/state b/testing/equivalence-tests/outputs/variables_and_outputs/state new file mode 100644 index 0000000000..330b898624 --- /dev/null +++ b/testing/equivalence-tests/outputs/variables_and_outputs/state @@ -0,0 +1,32 @@ + + +Outputs: + +list_empty_default = [] +list_no_default = [ + { + optional_attribute_with_default = "Hello, world!" + required_attribute = "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + optional_attribute = "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487" + optional_attribute_with_default = "Hello, world!" + required_attribute = "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + optional_attribute = "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C" + optional_attribute_with_default = "92E855B2-A444-49DF-AFCA-2B5B017451B4" + required_attribute = "9F9922C4-B426-4648-96AE-804A6F52F778" + }, +] +nested_optional_object = {} +nested_optional_object_with_default = { + nested_object = { + flag = false + } +} +nested_optional_object_with_embedded_default = { + nested_object = { + flag = false + } +} diff --git a/testing/equivalence-tests/outputs/variables_and_outputs/state.json b/testing/equivalence-tests/outputs/variables_and_outputs/state.json new file mode 100644 index 0000000000..fc76ce8529 --- /dev/null +++ b/testing/equivalence-tests/outputs/variables_and_outputs/state.json @@ -0,0 +1,109 @@ +{ + "format_version": "1.0", + "values": { + "outputs": { + "list_empty_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [] + }, + "list_no_default": { + "sensitive": false, + "type": [ + "list", + [ + "object", + { + "optional_attribute": "string", + "optional_attribute_with_default": "string", + "required_attribute": "string" + } + ] + ], + "value": [ + { + "optional_attribute": null, + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "D92053D5-948A-4E5E-80BF-E53F0DB33EB5" + }, + { + "optional_attribute": "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + "optional_attribute_with_default": "Hello, world!", + "required_attribute": "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5" + }, + { + "optional_attribute": "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + "optional_attribute_with_default": "92E855B2-A444-49DF-AFCA-2B5B017451B4", + "required_attribute": "9F9922C4-B426-4648-96AE-804A6F52F778" + } + ] + }, + "nested_optional_object": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": null + } + }, + "nested_optional_object_with_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + }, + "nested_optional_object_with_embedded_default": { + "sensitive": false, + "type": [ + "object", + { + "nested_object": [ + "object", + { + "flag": "bool" + } + ] + } + ], + "value": { + "nested_object": { + "flag": false + } + } + } + }, + "root_module": {} + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_json_string_update/.terraform.lock.hcl b/testing/equivalence-tests/tests/basic_json_string_update/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/basic_json_string_update/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/basic_json_string_update/main.tf b/testing/equivalence-tests/tests/basic_json_string_update/main.tf new file mode 100644 index 0000000000..37e3cadacb --- /dev/null +++ b/testing/equivalence-tests/tests/basic_json_string_update/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "json" { + string = "{\"list-attribute\":[\"one\",\"four\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_three\":\"value_two\", \"key_four\":\"value_three\"},\"string-attribute\":\"a new string\"}" +} diff --git a/testing/equivalence-tests/tests/basic_json_string_update/spec.json b/testing/equivalence-tests/tests/basic_json_string_update/spec.json new file mode 100644 index 0000000000..ef42c1a35e --- /dev/null +++ b/testing/equivalence-tests/tests/basic_json_string_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating a JSON compatible string", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_json_string_update/terraform.resource/5a3fd9b3-e852-8956-8c0a-255d47eda645.json b/testing/equivalence-tests/tests/basic_json_string_update/terraform.resource/5a3fd9b3-e852-8956-8c0a-255d47eda645.json new file mode 100644 index 0000000000..5604f6c2f2 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_json_string_update/terraform.resource/5a3fd9b3-e852-8956-8c0a-255d47eda645.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "5a3fd9b3-e852-8956-8c0a-255d47eda645" + }, + "string": { + "string": "{\"list-attribute\":[\"one\",\"two\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_two\":\"value_two\",\"key_three\":\"value_three\"},\"string-attribute\":\"string\"}" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_json_string_update/terraform.tfstate b/testing/equivalence-tests/tests/basic_json_string_update/terraform.tfstate new file mode 100644 index 0000000000..24089bcfde --- /dev/null +++ b/testing/equivalence-tests/tests/basic_json_string_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "4a3a27e3-e5b3-3db6-bc4c-6b762a030232", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "json", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "5a3fd9b3-e852-8956-8c0a-255d47eda645", + "integer": null, + "number": null, + "string": "{\"list-attribute\":[\"one\",\"two\",\"three\"],\"object-attribute\":{\"key_one\":\"value_one\",\"key_two\":\"value_two\",\"key_three\":\"value_three\"},\"string-attribute\":\"string\"}" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_list/dynamic_resources.json b/testing/equivalence-tests/tests/basic_list/dynamic_resources.json new file mode 100644 index 0000000000..78b194e7a1 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_list": { + "attributes": { + "list": { + "type": "list", + "optional": true, + "list": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_list/main.tf b/testing/equivalence-tests/tests/basic_list/main.tf new file mode 100644 index 0000000000..e2e5a6b647 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + list = [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + ] +} + diff --git a/testing/equivalence-tests/tests/basic_list/spec.json b/testing/equivalence-tests/tests/basic_list/spec.json new file mode 100644 index 0000000000..1a085f69cf --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list/spec.json @@ -0,0 +1,5 @@ +{ + "description": "basic test covering creation of a single list", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_list_empty/dynamic_resources.json b/testing/equivalence-tests/tests/basic_list_empty/dynamic_resources.json new file mode 100644 index 0000000000..7311d1b939 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_empty/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_list": { + "attributes": { + "list": { + "type": "list", + "optional": true, + "list": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_empty/main.tf b/testing/equivalence-tests/tests/basic_list_empty/main.tf new file mode 100644 index 0000000000..520ea046ba --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_empty/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + list = [] +} diff --git a/testing/equivalence-tests/tests/basic_list_empty/spec.json b/testing/equivalence-tests/tests/basic_list_empty/spec.json new file mode 100644 index 0000000000..99bfdb40d6 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_empty/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests removing all elements from a simple list", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_list_empty/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json b/testing/equivalence-tests/tests/basic_list_empty/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json new file mode 100644 index 0000000000..6328bc9db6 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_empty/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + }, + "list": { + "list": [ + { + "string": "9C2BE420-042D-440A-96E9-75565341C994" + }, + { + "string": "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1" + }, + { + "string": "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_empty/terraform.tfstate b/testing/equivalence-tests/tests/basic_list_empty/terraform.tfstate new file mode 100644 index 0000000000..db4167ebef --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_empty/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "10d37a1e-be83-d81f-dc89-30b6ec90e835", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_list", + "name": "list", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_list_null/dynamic_resources.json b/testing/equivalence-tests/tests/basic_list_null/dynamic_resources.json new file mode 100644 index 0000000000..7311d1b939 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_list": { + "attributes": { + "list": { + "type": "list", + "optional": true, + "list": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_null/main.tf b/testing/equivalence-tests/tests/basic_list_null/main.tf new file mode 100644 index 0000000000..dd4ea8dbef --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" +} diff --git a/testing/equivalence-tests/tests/basic_list_null/spec.json b/testing/equivalence-tests/tests/basic_list_null/spec.json new file mode 100644 index 0000000000..779888c48f --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests deleting a simple list from a resource", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_list_null/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json b/testing/equivalence-tests/tests/basic_list_null/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json new file mode 100644 index 0000000000..1bae638877 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "set": [ + { + "string": "41471135-E14C-4946-BFA4-2626C7E2A94A" + }, + { + "string": "C04762B9-D07B-40FE-A92B-B72AD342658D" + }, + { + "string": "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_null/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json b/testing/equivalence-tests/tests/basic_list_null/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json new file mode 100644 index 0000000000..4ed206e78c --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "map": { + "one": { + "string": "682672C7-0918-4448-8342-887BAE01062A" + }, + "two": { + "string": "212FFBF6-40FE-4862-B708-E6AA508E84E0" + }, + "zero": { + "string": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_null/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json b/testing/equivalence-tests/tests/basic_list_null/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json new file mode 100644 index 0000000000..6328bc9db6 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + }, + "list": { + "list": [ + { + "string": "9C2BE420-042D-440A-96E9-75565341C994" + }, + { + "string": "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1" + }, + { + "string": "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_null/terraform.tfstate b/testing/equivalence-tests/tests/basic_list_null/terraform.tfstate new file mode 100644 index 0000000000..db4167ebef --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_null/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "10d37a1e-be83-d81f-dc89-30b6ec90e835", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_list", + "name": "list", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_list_update/dynamic_resources.json b/testing/equivalence-tests/tests/basic_list_update/dynamic_resources.json new file mode 100644 index 0000000000..78b194e7a1 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_update/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_list": { + "attributes": { + "list": { + "type": "list", + "optional": true, + "list": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_list_update/main.tf b/testing/equivalence-tests/tests/basic_list_update/main.tf new file mode 100644 index 0000000000..95e5dbae69 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_update/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_list" "list" { + id = "985820B3-ACF9-4F00-94AD-F81C5EA33663" + list = [ + "9C2BE420-042D-440A-96E9-75565341C994", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6", + "9B9F3ADF-8AD4-4E8C-AFE4-7BC2413E9AC0", + ] +} diff --git a/testing/equivalence-tests/tests/basic_list_update/spec.json b/testing/equivalence-tests/tests/basic_list_update/spec.json new file mode 100644 index 0000000000..bf5bca6265 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests adding and removing elements from a simple list", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_list_update/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json b/testing/equivalence-tests/tests/basic_list_update/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json new file mode 100644 index 0000000000..6328bc9db6 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_update/terraform.resource/985820B3-ACF9-4F00-94AD-F81C5EA33663.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "985820B3-ACF9-4F00-94AD-F81C5EA33663" + }, + "list": { + "list": [ + { + "string": "9C2BE420-042D-440A-96E9-75565341C994" + }, + { + "string": "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1" + }, + { + "string": "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_list_update/terraform.tfstate b/testing/equivalence-tests/tests/basic_list_update/terraform.tfstate new file mode 100644 index 0000000000..db4167ebef --- /dev/null +++ b/testing/equivalence-tests/tests/basic_list_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "10d37a1e-be83-d81f-dc89-30b6ec90e835", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_list", + "name": "list", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "985820B3-ACF9-4F00-94AD-F81C5EA33663", + "list": [ + "9C2BE420-042D-440A-96E9-75565341C994", + "3EC6EB1F-E372-46C3-A069-00D6E82EC1E1", + "D01290F6-2D3A-45FA-B006-DAA80F6D31F6" + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_map/dynamic_resources.json b/testing/equivalence-tests/tests/basic_map/dynamic_resources.json new file mode 100644 index 0000000000..2b2d959bde --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_map": { + "attributes": { + "map": { + "type": "map", + "optional": true, + "map": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_map/main.tf b/testing/equivalence-tests/tests/basic_map/main.tf new file mode 100644 index 0000000000..5622c5c084 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + map = { + "zero" : "6B044AF7-172B-495B-BE11-B9546C12C3BD", + "one" : "682672C7-0918-4448-8342-887BAE01062A", + "two" : "212FFBF6-40FE-4862-B708-E6AA508E84E0", + } +} diff --git a/testing/equivalence-tests/tests/basic_map/spec.json b/testing/equivalence-tests/tests/basic_map/spec.json new file mode 100644 index 0000000000..91eeb9ea4d --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map/spec.json @@ -0,0 +1,5 @@ +{ + "description": "basic test covering creation of a simple map", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_map_empty/dynamic_resources.json b/testing/equivalence-tests/tests/basic_map_empty/dynamic_resources.json new file mode 100644 index 0000000000..2b2d959bde --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_empty/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_map": { + "attributes": { + "map": { + "type": "map", + "optional": true, + "map": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_map_empty/main.tf b/testing/equivalence-tests/tests/basic_map_empty/main.tf new file mode 100644 index 0000000000..aebd593122 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_empty/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + map = {} +} diff --git a/testing/equivalence-tests/tests/basic_map_empty/spec.json b/testing/equivalence-tests/tests/basic_map_empty/spec.json new file mode 100644 index 0000000000..678b5cddf6 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_empty/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests removing all elements from a simple map", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_map_empty/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json b/testing/equivalence-tests/tests/basic_map_empty/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json new file mode 100644 index 0000000000..4ed206e78c --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_empty/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "map": { + "one": { + "string": "682672C7-0918-4448-8342-887BAE01062A" + }, + "two": { + "string": "212FFBF6-40FE-4862-B708-E6AA508E84E0" + }, + "zero": { + "string": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_map_empty/terraform.tfstate b/testing/equivalence-tests/tests/basic_map_empty/terraform.tfstate new file mode 100644 index 0000000000..c98571045e --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_empty/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "6d4501ec-0c29-0728-bfb9-296c7dc09b9f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_map", + "name": "map", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_map_null/dynamic_resources.json b/testing/equivalence-tests/tests/basic_map_null/dynamic_resources.json new file mode 100644 index 0000000000..2b2d959bde --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_null/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_map": { + "attributes": { + "map": { + "type": "map", + "optional": true, + "map": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_map_null/main.tf b/testing/equivalence-tests/tests/basic_map_null/main.tf new file mode 100644 index 0000000000..6bac607214 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_null/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" +} diff --git a/testing/equivalence-tests/tests/basic_map_null/spec.json b/testing/equivalence-tests/tests/basic_map_null/spec.json new file mode 100644 index 0000000000..3eb24d9855 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_null/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests deleting a simple map from a resource", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_map_null/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json b/testing/equivalence-tests/tests/basic_map_null/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json new file mode 100644 index 0000000000..4ed206e78c --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_null/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "map": { + "one": { + "string": "682672C7-0918-4448-8342-887BAE01062A" + }, + "two": { + "string": "212FFBF6-40FE-4862-B708-E6AA508E84E0" + }, + "zero": { + "string": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_map_null/terraform.tfstate b/testing/equivalence-tests/tests/basic_map_null/terraform.tfstate new file mode 100644 index 0000000000..c98571045e --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_null/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "6d4501ec-0c29-0728-bfb9-296c7dc09b9f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_map", + "name": "map", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_map_update/dynamic_resources.json b/testing/equivalence-tests/tests/basic_map_update/dynamic_resources.json new file mode 100644 index 0000000000..2b2d959bde --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_update/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_map": { + "attributes": { + "map": { + "type": "map", + "optional": true, + "map": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_map_update/main.tf b/testing/equivalence-tests/tests/basic_map_update/main.tf new file mode 100644 index 0000000000..3446f95ef5 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_update/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_map" "map" { + id = "50E1A46E-E64A-4C1F-881C-BA85A5440964" + map = { + "zero" : "6B044AF7-172B-495B-BE11-B9546C12C3BD", + "two" : "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "four" : "D820D482-7C2C-4EF3-8935-863168A193F9", + } +} diff --git a/testing/equivalence-tests/tests/basic_map_update/spec.json b/testing/equivalence-tests/tests/basic_map_update/spec.json new file mode 100644 index 0000000000..332a515865 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "basic test coverting updating a simple map", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_map_update/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json b/testing/equivalence-tests/tests/basic_map_update/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json new file mode 100644 index 0000000000..4ed206e78c --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_update/terraform.resource/50E1A46E-E64A-4C1F-881C-BA85A5440964.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "50E1A46E-E64A-4C1F-881C-BA85A5440964" + }, + "map": { + "map": { + "one": { + "string": "682672C7-0918-4448-8342-887BAE01062A" + }, + "two": { + "string": "212FFBF6-40FE-4862-B708-E6AA508E84E0" + }, + "zero": { + "string": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_map_update/terraform.tfstate b/testing/equivalence-tests/tests/basic_map_update/terraform.tfstate new file mode 100644 index 0000000000..c98571045e --- /dev/null +++ b/testing/equivalence-tests/tests/basic_map_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "6d4501ec-0c29-0728-bfb9-296c7dc09b9f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_map", + "name": "map", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "50E1A46E-E64A-4C1F-881C-BA85A5440964", + "map": { + "one": "682672C7-0918-4448-8342-887BAE01062A", + "two": "212FFBF6-40FE-4862-B708-E6AA508E84E0", + "zero": "6B044AF7-172B-495B-BE11-B9546C12C3BD" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_multiline_string_update/.terraform.lock.hcl b/testing/equivalence-tests/tests/basic_multiline_string_update/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/basic_multiline_string_update/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/basic_multiline_string_update/main.tf b/testing/equivalence-tests/tests/basic_multiline_string_update/main.tf new file mode 100644 index 0000000000..c3bbaaa8bf --- /dev/null +++ b/testing/equivalence-tests/tests/basic_multiline_string_update/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "multiline" { + string = "one\nthree\ntwo\nfour\nsix\nseven" +} diff --git a/testing/equivalence-tests/tests/basic_multiline_string_update/spec.json b/testing/equivalence-tests/tests/basic_multiline_string_update/spec.json new file mode 100644 index 0000000000..16347a0d92 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_multiline_string_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests handling of multiline strings when updating", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_multiline_string_update/terraform.resource/69fe5233-e77a-804f-0dac-115c949540bc.json b/testing/equivalence-tests/tests/basic_multiline_string_update/terraform.resource/69fe5233-e77a-804f-0dac-115c949540bc.json new file mode 100644 index 0000000000..8ad35d41d6 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_multiline_string_update/terraform.resource/69fe5233-e77a-804f-0dac-115c949540bc.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "69fe5233-e77a-804f-0dac-115c949540bc" + }, + "string": { + "string": "one\ntwo\nthree\nfour\nfive" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_multiline_string_update/terraform.tfstate b/testing/equivalence-tests/tests/basic_multiline_string_update/terraform.tfstate new file mode 100644 index 0000000000..050b25962f --- /dev/null +++ b/testing/equivalence-tests/tests/basic_multiline_string_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "1e8cb645-9856-bc39-5053-f58ec18be73d", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "multiline", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "69fe5233-e77a-804f-0dac-115c949540bc", + "integer": null, + "number": null, + "string": "one\ntwo\nthree\nfour\nfive" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_set/dynamic_resources.json b/testing/equivalence-tests/tests/basic_set/dynamic_resources.json new file mode 100644 index 0000000000..93eb3901b8 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_set": { + "attributes": { + "set": { + "type": "set", + "optional": true, + "set": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_set/main.tf b/testing/equivalence-tests/tests/basic_set/main.tf new file mode 100644 index 0000000000..a327a27c16 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + set = [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + ] +} diff --git a/testing/equivalence-tests/tests/basic_set/spec.json b/testing/equivalence-tests/tests/basic_set/spec.json new file mode 100644 index 0000000000..5ac9530a80 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set/spec.json @@ -0,0 +1,5 @@ +{ + "description": "basic test coverting creation of a simple set", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_set_empty/dynamic_resources.json b/testing/equivalence-tests/tests/basic_set_empty/dynamic_resources.json new file mode 100644 index 0000000000..93eb3901b8 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_empty/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_set": { + "attributes": { + "set": { + "type": "set", + "optional": true, + "set": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_set_empty/main.tf b/testing/equivalence-tests/tests/basic_set_empty/main.tf new file mode 100644 index 0000000000..aa7d0d2007 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_empty/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + set = [] +} diff --git a/testing/equivalence-tests/tests/basic_set_empty/spec.json b/testing/equivalence-tests/tests/basic_set_empty/spec.json new file mode 100644 index 0000000000..ed8691da37 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_empty/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests removing all elements from a simple set", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_set_empty/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json b/testing/equivalence-tests/tests/basic_set_empty/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json new file mode 100644 index 0000000000..1bae638877 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_empty/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "set": [ + { + "string": "41471135-E14C-4946-BFA4-2626C7E2A94A" + }, + { + "string": "C04762B9-D07B-40FE-A92B-B72AD342658D" + }, + { + "string": "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_set_empty/terraform.tfstate b/testing/equivalence-tests/tests/basic_set_empty/terraform.tfstate new file mode 100644 index 0000000000..556b2adc89 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_empty/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "b6909641-99a4-0628-bbc1-110b6361ba80", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_set", + "name": "set", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_set_null/dynamic_resources.json b/testing/equivalence-tests/tests/basic_set_null/dynamic_resources.json new file mode 100644 index 0000000000..93eb3901b8 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_null/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_set": { + "attributes": { + "set": { + "type": "set", + "optional": true, + "set": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_set_null/main.tf b/testing/equivalence-tests/tests/basic_set_null/main.tf new file mode 100644 index 0000000000..aa7594d68d --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_null/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" +} diff --git a/testing/equivalence-tests/tests/basic_set_null/spec.json b/testing/equivalence-tests/tests/basic_set_null/spec.json new file mode 100644 index 0000000000..07aa574b6c --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_null/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests deleting a simple set", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_set_null/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json b/testing/equivalence-tests/tests/basic_set_null/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json new file mode 100644 index 0000000000..1bae638877 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_null/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "set": [ + { + "string": "41471135-E14C-4946-BFA4-2626C7E2A94A" + }, + { + "string": "C04762B9-D07B-40FE-A92B-B72AD342658D" + }, + { + "string": "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_set_null/terraform.tfstate b/testing/equivalence-tests/tests/basic_set_null/terraform.tfstate new file mode 100644 index 0000000000..556b2adc89 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_null/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "b6909641-99a4-0628-bbc1-110b6361ba80", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_set", + "name": "set", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/basic_set_update/dynamic_resources.json b/testing/equivalence-tests/tests/basic_set_update/dynamic_resources.json new file mode 100644 index 0000000000..93eb3901b8 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_update/dynamic_resources.json @@ -0,0 +1,13 @@ +{ + "tfcoremock_set": { + "attributes": { + "set": { + "type": "set", + "optional": true, + "set": { + "type": "string" + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/basic_set_update/main.tf b/testing/equivalence-tests/tests/basic_set_update/main.tf new file mode 100644 index 0000000000..dfbd4b2830 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_update/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_set" "set" { + id = "046952C9-B832-4106-82C0-C217F7C73E18" + set = [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B", + "1769B76E-12F0-4214-A864-E843EB23B64E", + ] +} diff --git a/testing/equivalence-tests/tests/basic_set_update/spec.json b/testing/equivalence-tests/tests/basic_set_update/spec.json new file mode 100644 index 0000000000..7df5cf3da0 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests adding and removing elements from a simple set", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/basic_set_update/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json b/testing/equivalence-tests/tests/basic_set_update/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json new file mode 100644 index 0000000000..1bae638877 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_update/terraform.resource/046952C9-B832-4106-82C0-C217F7C73E18.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "046952C9-B832-4106-82C0-C217F7C73E18" + }, + "set": { + "set": [ + { + "string": "41471135-E14C-4946-BFA4-2626C7E2A94A" + }, + { + "string": "C04762B9-D07B-40FE-A92B-B72AD342658D" + }, + { + "string": "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/basic_set_update/terraform.tfstate b/testing/equivalence-tests/tests/basic_set_update/terraform.tfstate new file mode 100644 index 0000000000..556b2adc89 --- /dev/null +++ b/testing/equivalence-tests/tests/basic_set_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.6", + "serial": 1, + "lineage": "b6909641-99a4-0628-bbc1-110b6361ba80", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_set", + "name": "set", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "046952C9-B832-4106-82C0-C217F7C73E18", + "set": [ + "41471135-E14C-4946-BFA4-2626C7E2A94A", + "C04762B9-D07B-40FE-A92B-B72AD342658D", + "D8F7EA80-9E25-4DD7-8D97-797D2080952B" + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/data_read/create/main.tf b/testing/equivalence-tests/tests/data_read/create/main.tf new file mode 100644 index 0000000000..5cb4ba343e --- /dev/null +++ b/testing/equivalence-tests/tests/data_read/create/main.tf @@ -0,0 +1,32 @@ + +variable "contents" { + type = string +} + +resource "random_integer" "random" { + min = 1000000 + max = 9999999 + seed = "F78CB410-BA01-44E1-82E1-37D61F7CB158" +} + +locals { + contents = jsonencode({ + values = { + id = { + string = random_integer.random.id + } + string = { + string = var.contents + } + } + }) +} + +resource "local_file" "data_file" { + filename = "terraform.data/${random_integer.random.id}.json" + content = local.contents +} + +output "id" { + value = random_integer.random.id +} diff --git a/testing/equivalence-tests/tests/data_read/main.tf b/testing/equivalence-tests/tests/data_read/main.tf new file mode 100644 index 0000000000..67c3f11a65 --- /dev/null +++ b/testing/equivalence-tests/tests/data_read/main.tf @@ -0,0 +1,39 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + local = { + source = "hashicorp/local" + version = "2.2.3" + } + random = { + source = "hashicorp/random" + version = "3.4.3" + } + } +} + +provider "tfcoremock" {} + +provider "local" {} + +provider "random" {} + +module "create" { + source = "./create" + contents = "hello, world!" +} + +data "tfcoremock_simple_resource" "read" { + id = module.create.id + + depends_on = [ + module.create + ] +} + +resource "tfcoremock_simple_resource" "create" { + string = data.tfcoremock_simple_resource.read.string +} diff --git a/testing/equivalence-tests/tests/data_read/spec.json b/testing/equivalence-tests/tests/data_read/spec.json new file mode 100644 index 0000000000..7931b8ce19 --- /dev/null +++ b/testing/equivalence-tests/tests/data_read/spec.json @@ -0,0 +1,27 @@ +{ + "description": "tests reading data from data sources using only a plan", + "include_files": [], + "ignore_fields": {}, + "commands": [ + { + "name": "init", + "arguments": ["init"], + "capture_output": false + }, + { + "name": "plan", + "arguments": ["plan", "-out=equivalence_test_plan", "-no-color"], + "capture_output": true, + "output_file_name": "plan", + "has_json_output": false + }, + { + "name": "show_plan", + "arguments": ["show", "-json", "equivalence_test_plan"], + "capture_output": true, + "output_file_name": "plan.json", + "has_json_output": true, + "streams_json_output": false + } + ] +} diff --git a/testing/equivalence-tests/tests/drift_refresh_only/.terraform.lock.hcl b/testing/equivalence-tests/tests/drift_refresh_only/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/drift_refresh_only/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/drift_refresh_only/main.tf b/testing/equivalence-tests/tests/drift_refresh_only/main.tf new file mode 100644 index 0000000000..77e891392b --- /dev/null +++ b/testing/equivalence-tests/tests/drift_refresh_only/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "drift" { + string = "Hello, world!" +} diff --git a/testing/equivalence-tests/tests/drift_refresh_only/spec.json b/testing/equivalence-tests/tests/drift_refresh_only/spec.json new file mode 100644 index 0000000000..810211a4cb --- /dev/null +++ b/testing/equivalence-tests/tests/drift_refresh_only/spec.json @@ -0,0 +1,43 @@ +{ + "description": "tests drift in a refresh only plan, so has a custom set of commands", + "include_files": [], + "ignore_fields": {}, + "commands": [ + { + "name": "init", + "arguments": ["init"], + "capture_output": false + }, + { + "name": "plan", + "arguments": ["plan", "-out=equivalence_test_plan", "-no-color", "-refresh-only"], + "capture_output": true, + "output_file_name": "plan", + "has_json_output": false + }, + { + "name": "apply", + "arguments": ["apply", "-json", "equivalence_test_plan"], + "capture_output": true, + "output_file_name": "apply.json", + "has_json_output": true, + "streams_json_output": true + }, + { + "name": "show_state", + "arguments": ["show", "-json"], + "capture_output": true, + "output_file_name": "state.json", + "has_json_output": true, + "streams_json_output": false + }, + { + "name": "show_plan", + "arguments": ["show", "-json", "equivalence_test_plan"], + "capture_output": true, + "output_file_name": "plan.json", + "has_json_output": true, + "streams_json_output": false + } + ] +} diff --git a/testing/equivalence-tests/tests/drift_refresh_only/terraform.resource/cb79269e-dc39-1e68-0a9c-63cb392afda9.json b/testing/equivalence-tests/tests/drift_refresh_only/terraform.resource/cb79269e-dc39-1e68-0a9c-63cb392afda9.json new file mode 100644 index 0000000000..4446f2c11f --- /dev/null +++ b/testing/equivalence-tests/tests/drift_refresh_only/terraform.resource/cb79269e-dc39-1e68-0a9c-63cb392afda9.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "cb79269e-dc39-1e68-0a9c-63cb392afda9" + }, + "string": { + "string": "Hello, drift!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/drift_refresh_only/terraform.tfstate b/testing/equivalence-tests/tests/drift_refresh_only/terraform.tfstate new file mode 100644 index 0000000000..b0ae387993 --- /dev/null +++ b/testing/equivalence-tests/tests/drift_refresh_only/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "4ce8adfd-57a0-aba7-118d-834394462086", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "drift", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "cb79269e-dc39-1e68-0a9c-63cb392afda9", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/drift_relevant_attributes/.terraform.lock.hcl b/testing/equivalence-tests/tests/drift_relevant_attributes/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/drift_relevant_attributes/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/drift_relevant_attributes/main.tf b/testing/equivalence-tests/tests/drift_relevant_attributes/main.tf new file mode 100644 index 0000000000..a73a09797d --- /dev/null +++ b/testing/equivalence-tests/tests/drift_relevant_attributes/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "base" { + string = "Hello, change!" + number = 0 +} + +resource "tfcoremock_simple_resource" "dependent" { + string = tfcoremock_simple_resource.base.string +} diff --git a/testing/equivalence-tests/tests/drift_relevant_attributes/spec.json b/testing/equivalence-tests/tests/drift_relevant_attributes/spec.json new file mode 100644 index 0000000000..273ef1b49d --- /dev/null +++ b/testing/equivalence-tests/tests/drift_relevant_attributes/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests that relevant attributes are applied when dependent resources are updated by drift", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.resource/1b17b502-96c9-fcc3-3b09-2af1c3de6ad8.json b/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.resource/1b17b502-96c9-fcc3-3b09-2af1c3de6ad8.json new file mode 100644 index 0000000000..cd2b86e856 --- /dev/null +++ b/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.resource/1b17b502-96c9-fcc3-3b09-2af1c3de6ad8.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8" + }, + "string": { + "string": "Hello, world!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.resource/f6f74ca6-e8ef-e51f-522c-433b9ed5038f.json b/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.resource/f6f74ca6-e8ef-e51f-522c-433b9ed5038f.json new file mode 100644 index 0000000000..1e1a7ef4bf --- /dev/null +++ b/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.resource/f6f74ca6-e8ef-e51f-522c-433b9ed5038f.json @@ -0,0 +1,13 @@ +{ + "values": { + "id": { + "string": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f" + }, + "number": { + "number": "1" + }, + "string": { + "string": "Hello, drift!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.tfstate b/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.tfstate new file mode 100644 index 0000000000..9ad07bf00f --- /dev/null +++ b/testing/equivalence-tests/tests/drift_relevant_attributes/terraform.tfstate @@ -0,0 +1,53 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 5, + "lineage": "f521f089-8673-e4a9-93a7-4f01f72fbc15", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "base", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "f6f74ca6-e8ef-e51f-522c-433b9ed5038f", + "integer": null, + "number": 0, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "dependent", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "1b17b502-96c9-fcc3-3b09-2af1c3de6ad8", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [], + "dependencies": [ + "tfcoremock_simple_resource.base" + ] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/drift_simple/.terraform.lock.hcl b/testing/equivalence-tests/tests/drift_simple/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/drift_simple/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/drift_simple/main.tf b/testing/equivalence-tests/tests/drift_simple/main.tf new file mode 100644 index 0000000000..77e891392b --- /dev/null +++ b/testing/equivalence-tests/tests/drift_simple/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "drift" { + string = "Hello, world!" +} diff --git a/testing/equivalence-tests/tests/drift_simple/spec.json b/testing/equivalence-tests/tests/drift_simple/spec.json new file mode 100644 index 0000000000..71d921b3b8 --- /dev/null +++ b/testing/equivalence-tests/tests/drift_simple/spec.json @@ -0,0 +1,5 @@ +{ + "description": "a simple test that models drift in a single resource by updating an existing resource outside of Terraform", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/drift_simple/terraform.resource/f3c6ddc5-37d5-0170-64ff-518ad421385a.json b/testing/equivalence-tests/tests/drift_simple/terraform.resource/f3c6ddc5-37d5-0170-64ff-518ad421385a.json new file mode 100644 index 0000000000..14976bf5aa --- /dev/null +++ b/testing/equivalence-tests/tests/drift_simple/terraform.resource/f3c6ddc5-37d5-0170-64ff-518ad421385a.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "f3c6ddc5-37d5-0170-64ff-518ad421385a" + }, + "string": { + "string": "Hello, drift!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/drift_simple/terraform.tfstate b/testing/equivalence-tests/tests/drift_simple/terraform.tfstate new file mode 100644 index 0000000000..2d652ab565 --- /dev/null +++ b/testing/equivalence-tests/tests/drift_simple/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "3ee5327f-66cc-dd24-f2f1-95ef63c0bcb8", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "drift", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "f3c6ddc5-37d5-0170-64ff-518ad421385a", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/fully_populated_complex/main.tf b/testing/equivalence-tests/tests/fully_populated_complex/main.tf new file mode 100644 index 0000000000..64335c742e --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex/main.tf @@ -0,0 +1,209 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_complex_resource" "complex" { + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + + number = 123456789.0 + integer = 987654321 + float = 987654321.0 + + string = "a not very long or complex string" + + bool = true + + list = [ + { + string = "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + string = "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + ] + + object = { + string = "i am a nested object" + + number = 0 + bool = false + + object = { + string = "i am a nested nested object" + number = 1 + bool = true + } + } + + map = { + "key_one" = { + string = "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_two" = { + string = "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + }, + "key_three" = { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + "key_four" = { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + } + + set = [ + { + string = "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + string = "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + }, + { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + ] + + list_block { + string = "{\"index\":0}" + } + + list_block { + string = "{\"index\":1}" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + } + + list_block { + string = "{\"index\":2}" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + + set_block { + string = "{\"index\":0}" + } + + set_block { + string = "{\"index\":1}" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + } + + set_block { + string = "{\"index\":2}" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/fully_populated_complex/spec.json b/testing/equivalence-tests/tests/fully_populated_complex/spec.json new file mode 100644 index 0000000000..8fc0b7b188 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex/spec.json @@ -0,0 +1,6 @@ +{ + "description": "this test creates an almost fully populated tfcoremock_complex_resource, which should cover most basic Terraform use cases", + "include_files": [], + "ignore_fields": {} +} + diff --git a/testing/equivalence-tests/tests/fully_populated_complex_destroy/.terraform.lock.hcl b/testing/equivalence-tests/tests/fully_populated_complex_destroy/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_destroy/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/fully_populated_complex_destroy/main.tf b/testing/equivalence-tests/tests/fully_populated_complex_destroy/main.tf new file mode 100644 index 0000000000..f5b590fde2 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_destroy/main.tf @@ -0,0 +1,206 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_complex_resource" "complex" { + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + + number = 987654321.0 + integer = 123456789 + float = 123456789.0 + + string = "a not very long or complex string" + + bool = true + + list = [ + { + string = "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + string = "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 3 + }, + { + number = 4 + } + ] + }, + { + string = "this is my fourth entry, and I actually have a nested set and I edited my test" + + set = [ + { + number = 0 + }, + { + number = 2 + }, + ] + } + ] + + object = { + string = "i am a nested object" + + number = 0 + + object = { + string = "i am a nested nested object" + bool = true + } + } + + map = { + "key_one" = { + string = "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_two" = { + string = "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + }, + "key_three" = { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 3 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + "key_four" = { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 3 + }, + { + number = 4 + }, + ] + } + } + + set = [ + { + string = "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + string = "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + }, + { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + ] + + list_block { + string = "{\"index\":0}" + } + + list_block { + string = "{\"index\":1}" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + } + + set_block { + string = "{\"index\":1}" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + } + + set_block { + string = "{\"index\":2}" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + + set_block { + string = "{\"index\":3}" + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/fully_populated_complex_destroy/spec.json b/testing/equivalence-tests/tests/fully_populated_complex_destroy/spec.json new file mode 100644 index 0000000000..e91bd2cd06 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_destroy/spec.json @@ -0,0 +1,42 @@ +{ + "include_files": [], + "ignore_fields": {}, + "commands": [ + { + "name": "init", + "arguments": ["init"], + "capture_output": false + }, + { + "name": "plan", + "arguments": ["plan", "-out=equivalence_test_plan", "-no-color", "-destroy"], + "capture_output": true, + "output_file_name": "plan", + "has_json_output": false + }, + { + "name": "apply", + "arguments": ["destroy", "-json", "-auto-approve"], + "capture_output": true, + "output_file_name": "apply.json", + "has_json_output": true, + "streams_json_output": true + }, + { + "name": "show_state", + "arguments": ["show", "-json"], + "capture_output": true, + "output_file_name": "state.json", + "has_json_output": true, + "streams_json_output": false + }, + { + "name": "show_plan", + "arguments": ["show", "-json", "equivalence_test_plan"], + "capture_output": true, + "output_file_name": "plan.json", + "has_json_output": true, + "streams_json_output": false + } + ] +} diff --git a/testing/equivalence-tests/tests/fully_populated_complex_destroy/terraform.resource/64564E36-BFCB-458B-9405-EBBF6A3CAC7A.json b/testing/equivalence-tests/tests/fully_populated_complex_destroy/terraform.resource/64564E36-BFCB-458B-9405-EBBF6A3CAC7A.json new file mode 100644 index 0000000000..49aa8d9c66 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_destroy/terraform.resource/64564E36-BFCB-458B-9405-EBBF6A3CAC7A.json @@ -0,0 +1,445 @@ +{ + "values": { + "bool": { + "boolean": true + }, + "float": { + "number": "9.87654321e+08" + }, + "id": { + "string": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + }, + "integer": { + "number": "9.87654321e+08" + }, + "list": { + "list": [ + { + "object": { + "string": { + "string": "this is my first entry in the list, and doesn't contain anything interesting" + } + } + }, + { + "object": { + "string": { + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + } + } + }, + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "string": { + "string": "this is my third entry, and I actually have a nested list" + } + } + }, + { + "object": { + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "string": { + "string": "this is my fourth entry, and I actually have a nested set" + } + } + } + ] + }, + "list_block": { + "list": [ + { + "object": { + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":0}" + } + } + }, + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":1}" + } + } + }, + { + "object": { + "list_block": { + "list": [] + }, + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":2}" + } + } + } + ] + }, + "map": { + "map": { + "key_four": { + "object": { + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "string": { + "string": "this is my fourth entry, and I actually have a nested set" + } + } + }, + "key_one": { + "object": { + "string": { + "string": "this is my first entry in the map, and doesn't contain anything interesting" + } + } + }, + "key_three": { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "string": { + "string": "this is my third entry, and I actually have a nested list" + } + } + }, + "key_two": { + "object": { + "string": { + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + } + } + } + }, + "number": { + "number": "1.23456789e+08" + }, + "object": { + "object": { + "bool": { + "boolean": false + }, + "number": { + "number": "0" + }, + "object": { + "object": { + "bool": { + "boolean": true + }, + "number": { + "number": "1" + }, + "string": { + "string": "i am a nested nested object" + } + } + }, + "string": { + "string": "i am a nested object" + } + } + }, + "set": { + "set": [ + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "string": { + "string": "this is my third entry, and I actually have a nested list" + } + } + }, + { + "object": { + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "string": { + "string": "this is my fourth entry, and I actually have a nested set" + } + } + }, + { + "object": { + "string": { + "string": "this is my first entry in the set, and doesn't contain anything interesting" + } + } + }, + { + "object": { + "string": { + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + } + } + ] + }, + "set_block": { + "set": [ + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":1}" + } + } + }, + { + "object": { + "list_block": { + "list": [] + }, + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":2}" + } + } + }, + { + "object": { + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":0}" + } + } + } + ] + }, + "string": { + "string": "a not very long or complex string" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/fully_populated_complex_destroy/terraform.tfstate b/testing/equivalence-tests/tests/fully_populated_complex_destroy/terraform.tfstate new file mode 100644 index 0000000000..6945ad2d99 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_destroy/terraform.tfstate @@ -0,0 +1,556 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "bc319b50-c252-a9b5-3ce3-3128618500d6", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_complex_resource", + "name": "complex", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/fully_populated_complex_update/.terraform.lock.hcl b/testing/equivalence-tests/tests/fully_populated_complex_update/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_update/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/fully_populated_complex_update/main.tf b/testing/equivalence-tests/tests/fully_populated_complex_update/main.tf new file mode 100644 index 0000000000..f5b590fde2 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_update/main.tf @@ -0,0 +1,206 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_complex_resource" "complex" { + id = "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + + number = 987654321.0 + integer = 123456789 + float = 123456789.0 + + string = "a not very long or complex string" + + bool = true + + list = [ + { + string = "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + string = "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines\nbut I've been edited" + }, + { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 3 + }, + { + number = 4 + } + ] + }, + { + string = "this is my fourth entry, and I actually have a nested set and I edited my test" + + set = [ + { + number = 0 + }, + { + number = 2 + }, + ] + } + ] + + object = { + string = "i am a nested object" + + number = 0 + + object = { + string = "i am a nested nested object" + bool = true + } + } + + map = { + "key_one" = { + string = "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_two" = { + string = "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + }, + "key_three" = { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 3 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + "key_four" = { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 3 + }, + { + number = 4 + }, + ] + } + } + + set = [ + { + string = "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + string = "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + }, + { + string = "this is my third entry, and I actually have a nested list" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + }, + { + string = "this is my fourth entry, and I actually have a nested set" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + ] + + list_block { + string = "{\"index\":0}" + } + + list_block { + string = "{\"index\":1}" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + } + + set_block { + string = "{\"index\":1}" + + list = [ + { + number = 0 + }, + { + number = 1 + }, + { + number = 2 + } + ] + } + + set_block { + string = "{\"index\":2}" + + set = [ + { + number = 0 + }, + { + number = 1 + }, + ] + } + + set_block { + string = "{\"index\":3}" + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/fully_populated_complex_update/spec.json b/testing/equivalence-tests/tests/fully_populated_complex_update/spec.json new file mode 100644 index 0000000000..cc62355313 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_update/spec.json @@ -0,0 +1,5 @@ +{ + "include_files": [], + "ignore_fields": {} +} + diff --git a/testing/equivalence-tests/tests/fully_populated_complex_update/terraform.resource/64564E36-BFCB-458B-9405-EBBF6A3CAC7A.json b/testing/equivalence-tests/tests/fully_populated_complex_update/terraform.resource/64564E36-BFCB-458B-9405-EBBF6A3CAC7A.json new file mode 100644 index 0000000000..49aa8d9c66 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_update/terraform.resource/64564E36-BFCB-458B-9405-EBBF6A3CAC7A.json @@ -0,0 +1,445 @@ +{ + "values": { + "bool": { + "boolean": true + }, + "float": { + "number": "9.87654321e+08" + }, + "id": { + "string": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A" + }, + "integer": { + "number": "9.87654321e+08" + }, + "list": { + "list": [ + { + "object": { + "string": { + "string": "this is my first entry in the list, and doesn't contain anything interesting" + } + } + }, + { + "object": { + "string": { + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + } + } + }, + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "string": { + "string": "this is my third entry, and I actually have a nested list" + } + } + }, + { + "object": { + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "string": { + "string": "this is my fourth entry, and I actually have a nested set" + } + } + } + ] + }, + "list_block": { + "list": [ + { + "object": { + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":0}" + } + } + }, + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":1}" + } + } + }, + { + "object": { + "list_block": { + "list": [] + }, + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":2}" + } + } + } + ] + }, + "map": { + "map": { + "key_four": { + "object": { + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "string": { + "string": "this is my fourth entry, and I actually have a nested set" + } + } + }, + "key_one": { + "object": { + "string": { + "string": "this is my first entry in the map, and doesn't contain anything interesting" + } + } + }, + "key_three": { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "string": { + "string": "this is my third entry, and I actually have a nested list" + } + } + }, + "key_two": { + "object": { + "string": { + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + } + } + } + }, + "number": { + "number": "1.23456789e+08" + }, + "object": { + "object": { + "bool": { + "boolean": false + }, + "number": { + "number": "0" + }, + "object": { + "object": { + "bool": { + "boolean": true + }, + "number": { + "number": "1" + }, + "string": { + "string": "i am a nested nested object" + } + } + }, + "string": { + "string": "i am a nested object" + } + } + }, + "set": { + "set": [ + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "string": { + "string": "this is my third entry, and I actually have a nested list" + } + } + }, + { + "object": { + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "string": { + "string": "this is my fourth entry, and I actually have a nested set" + } + } + }, + { + "object": { + "string": { + "string": "this is my first entry in the set, and doesn't contain anything interesting" + } + } + }, + { + "object": { + "string": { + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + } + } + ] + }, + "set_block": { + "set": [ + { + "object": { + "list": { + "list": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + }, + { + "object": { + "number": { + "number": "2" + } + } + } + ] + }, + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":1}" + } + } + }, + { + "object": { + "list_block": { + "list": [] + }, + "set": { + "set": [ + { + "object": { + "number": { + "number": "0" + } + } + }, + { + "object": { + "number": { + "number": "1" + } + } + } + ] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":2}" + } + } + }, + { + "object": { + "list_block": { + "list": [] + }, + "set_block": { + "set": [] + }, + "string": { + "string": "{\"index\":0}" + } + } + } + ] + }, + "string": { + "string": "a not very long or complex string" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/fully_populated_complex_update/terraform.tfstate b/testing/equivalence-tests/tests/fully_populated_complex_update/terraform.tfstate new file mode 100644 index 0000000000..6945ad2d99 --- /dev/null +++ b/testing/equivalence-tests/tests/fully_populated_complex_update/terraform.tfstate @@ -0,0 +1,556 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "bc319b50-c252-a9b5-3ce3-3128618500d6", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_complex_resource", + "name": "complex", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": true, + "float": 987654321, + "id": "64564E36-BFCB-458B-9405-EBBF6A3CAC7A", + "integer": 987654321, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the list, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the list\nI am a bit more interesting\nand contain multiple lines" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + } + ], + "list_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + } + ], + "map": { + "key_four": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + "key_one": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the map, and doesn't contain anything interesting" + }, + "key_three": { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + "key_two": { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the map\nI am a bit more interesting\nand contain multiple lines" + } + }, + "number": 123456789, + "object": { + "bool": false, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": { + "bool": true, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": "i am a nested nested object" + }, + "set": null, + "string": "i am a nested object" + }, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my third entry, and I actually have a nested list" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "string": "this is my fourth entry, and I actually have a nested set" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my first entry in the set, and doesn't contain anything interesting" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": null, + "object": null, + "set": null, + "string": "this is my second entry in the set\nI am a bit more interesting\nand contain multiple lines" + } + ], + "set_block": [ + { + "bool": null, + "float": null, + "integer": null, + "list": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 2, + "object": null, + "set": null, + "string": null + } + ], + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":1}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": [ + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 0, + "object": null, + "set": null, + "string": null + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "map": null, + "number": 1, + "object": null, + "set": null, + "string": null + } + ], + "set_block": [], + "string": "{\"index\":2}" + }, + { + "bool": null, + "float": null, + "integer": null, + "list": null, + "list_block": [], + "map": null, + "number": null, + "object": null, + "set": null, + "set_block": [], + "string": "{\"index\":0}" + } + ], + "string": "a not very long or complex string" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/local_provider_basic/main.tf b/testing/equivalence-tests/tests/local_provider_basic/main.tf new file mode 100644 index 0000000000..7640a43ae2 --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_basic/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + local = { + source = "hashicorp/local" + version = "2.2.3" + } + } +} + +locals { + contents = jsonencode({ + "hello" = "world" + }) +} + +provider "local" {} + +resource "local_file" "local_file" { + filename = "output.json" + content = local.contents +} diff --git a/testing/equivalence-tests/tests/local_provider_basic/spec.json b/testing/equivalence-tests/tests/local_provider_basic/spec.json new file mode 100644 index 0000000000..e47d6d23ee --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_basic/spec.json @@ -0,0 +1,7 @@ +{ + "description": "tests creating a local file using the local provider", + "include_files": [ + "output.json" + ], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/local_provider_delete/.terraform.lock.hcl b/testing/equivalence-tests/tests/local_provider_delete/.terraform.lock.hcl new file mode 100644 index 0000000000..7379b9c53b --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_delete/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/local" { + version = "2.2.3" + constraints = "2.2.3" + hashes = [ + "h1:FvRIEgCmAezgZUqb2F+PZ9WnSSnR5zbEM2ZI+GLmbMk=", + "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0", + "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa", + "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797", + "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb", + "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3", + "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c", + "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8", + "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e", + "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9", + "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd", + ] +} diff --git a/testing/equivalence-tests/tests/local_provider_delete/main.tf b/testing/equivalence-tests/tests/local_provider_delete/main.tf new file mode 100644 index 0000000000..4b08d8bbc8 --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_delete/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + local = { + source = "hashicorp/local" + version = "2.2.3" + } + } +} + +locals { + contents = jsonencode({ + "goodbye" = "world" + }) +} + +provider "local" {} diff --git a/testing/equivalence-tests/tests/local_provider_delete/output.json b/testing/equivalence-tests/tests/local_provider_delete/output.json new file mode 100755 index 0000000000..3f3571faa3 --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_delete/output.json @@ -0,0 +1 @@ +{"hello":"world"} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/local_provider_delete/spec.json b/testing/equivalence-tests/tests/local_provider_delete/spec.json new file mode 100644 index 0000000000..e3a25780ec --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_delete/spec.json @@ -0,0 +1,5 @@ +{ + "description": "test deleting a file created by the local provider", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/local_provider_delete/terraform.tfstate b/testing/equivalence-tests/tests/local_provider_delete/terraform.tfstate new file mode 100644 index 0000000000..abdfed08ae --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_delete/terraform.tfstate @@ -0,0 +1,33 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "e2a94970-ee0e-0eb7-16a5-67e94860dc8e", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "local_file", + "name": "local_file", + "provider": "provider[\"registry.terraform.io/hashicorp/local\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "sensitive_attributes": [], + "private": "bnVsbA==" + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/local_provider_update/.terraform.lock.hcl b/testing/equivalence-tests/tests/local_provider_update/.terraform.lock.hcl new file mode 100644 index 0000000000..7379b9c53b --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_update/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/local" { + version = "2.2.3" + constraints = "2.2.3" + hashes = [ + "h1:FvRIEgCmAezgZUqb2F+PZ9WnSSnR5zbEM2ZI+GLmbMk=", + "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0", + "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa", + "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797", + "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb", + "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3", + "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c", + "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8", + "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e", + "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9", + "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd", + ] +} diff --git a/testing/equivalence-tests/tests/local_provider_update/main.tf b/testing/equivalence-tests/tests/local_provider_update/main.tf new file mode 100644 index 0000000000..e47de3cfb2 --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_update/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + local = { + source = "hashicorp/local" + version = "2.2.3" + } + } +} + +locals { + contents = jsonencode({ + "goodbye" = "world" + }) +} + +provider "local" {} + +resource "local_file" "local_file" { + filename = "output.json" + content = local.contents +} diff --git a/testing/equivalence-tests/tests/local_provider_update/output.json b/testing/equivalence-tests/tests/local_provider_update/output.json new file mode 100755 index 0000000000..3f3571faa3 --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_update/output.json @@ -0,0 +1 @@ +{"hello":"world"} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/local_provider_update/spec.json b/testing/equivalence-tests/tests/local_provider_update/spec.json new file mode 100644 index 0000000000..17b0f32774 --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_update/spec.json @@ -0,0 +1,7 @@ +{ + "description": "tests updating a file using the local provider", + "include_files": [ + "output.json" + ], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/local_provider_update/terraform.tfstate b/testing/equivalence-tests/tests/local_provider_update/terraform.tfstate new file mode 100644 index 0000000000..abdfed08ae --- /dev/null +++ b/testing/equivalence-tests/tests/local_provider_update/terraform.tfstate @@ -0,0 +1,33 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "e2a94970-ee0e-0eb7-16a5-67e94860dc8e", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "local_file", + "name": "local_file", + "provider": "provider[\"registry.terraform.io/hashicorp/local\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "content": "{\"hello\":\"world\"}", + "content_base64": null, + "directory_permission": "0777", + "file_permission": "0777", + "filename": "output.json", + "id": "2248ee2fa0aaaad99178531f924bf00b4b0a8f4e", + "sensitive_content": null, + "source": null + }, + "sensitive_attributes": [], + "private": "bnVsbA==" + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/moved_simple/.terraform.lock.hcl b/testing/equivalence-tests/tests/moved_simple/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/moved_simple/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/moved_simple/main.tf b/testing/equivalence-tests/tests/moved_simple/main.tf new file mode 100644 index 0000000000..356b980f9b --- /dev/null +++ b/testing/equivalence-tests/tests/moved_simple/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + + +resource "tfcoremock_simple_resource" "second" { + string = "Hello, world!" +} + +moved { + from = tfcoremock_simple_resource.first + to = tfcoremock_simple_resource.second +} diff --git a/testing/equivalence-tests/tests/moved_simple/spec.json b/testing/equivalence-tests/tests/moved_simple/spec.json new file mode 100644 index 0000000000..d9c1fbd05b --- /dev/null +++ b/testing/equivalence-tests/tests/moved_simple/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests an unchanged resource being moved", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/moved_simple/terraform.resource/70c47571-66c3-b1dc-2474-47a74b9c7886.json b/testing/equivalence-tests/tests/moved_simple/terraform.resource/70c47571-66c3-b1dc-2474-47a74b9c7886.json new file mode 100644 index 0000000000..6c63be352d --- /dev/null +++ b/testing/equivalence-tests/tests/moved_simple/terraform.resource/70c47571-66c3-b1dc-2474-47a74b9c7886.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "70c47571-66c3-b1dc-2474-47a74b9c7886" + }, + "string": { + "string": "Hello, world!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/moved_simple/terraform.tfstate b/testing/equivalence-tests/tests/moved_simple/terraform.tfstate new file mode 100644 index 0000000000..89f82ba712 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_simple/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "4a0f03a7-03fd-9357-9fd2-b3405139fa1d", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "first", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/moved_with_drift/.terraform.lock.hcl b/testing/equivalence-tests/tests/moved_with_drift/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_drift/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/moved_with_drift/main.tf b/testing/equivalence-tests/tests/moved_with_drift/main.tf new file mode 100644 index 0000000000..e060e2bbd0 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_drift/main.tf @@ -0,0 +1,23 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "base_after" { + string = "Hello, change!" +} + +resource "tfcoremock_simple_resource" "dependent" { + string = tfcoremock_simple_resource.base_after.string +} + +moved { + from = tfcoremock_simple_resource.base_before + to = tfcoremock_simple_resource.base_after +} diff --git a/testing/equivalence-tests/tests/moved_with_drift/spec.json b/testing/equivalence-tests/tests/moved_with_drift/spec.json new file mode 100644 index 0000000000..f2ae347047 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_drift/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests using the moved block combined with simulated drift", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/moved_with_drift/terraform.resource/2ecc718c-8d04-5774-5c36-7d69bf77d34e.json b/testing/equivalence-tests/tests/moved_with_drift/terraform.resource/2ecc718c-8d04-5774-5c36-7d69bf77d34e.json new file mode 100644 index 0000000000..2adef56f2b --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_drift/terraform.resource/2ecc718c-8d04-5774-5c36-7d69bf77d34e.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "2ecc718c-8d04-5774-5c36-7d69bf77d34e" + }, + "string": { + "string": "Hello, world!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/moved_with_drift/terraform.resource/e450ef2f-b80f-0cce-8bdb-14d88f48649c.json b/testing/equivalence-tests/tests/moved_with_drift/terraform.resource/e450ef2f-b80f-0cce-8bdb-14d88f48649c.json new file mode 100644 index 0000000000..ea5cd38510 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_drift/terraform.resource/e450ef2f-b80f-0cce-8bdb-14d88f48649c.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "e450ef2f-b80f-0cce-8bdb-14d88f48649c" + }, + "string": { + "string": "Hello, drift!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/moved_with_drift/terraform.tfstate b/testing/equivalence-tests/tests/moved_with_drift/terraform.tfstate new file mode 100644 index 0000000000..97ea08cd35 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_drift/terraform.tfstate @@ -0,0 +1,53 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 3, + "lineage": "6cdc8ae0-8355-2447-7fb8-a9e9c2243e8f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "base_before", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "e450ef2f-b80f-0cce-8bdb-14d88f48649c", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + }, + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "dependent", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "2ecc718c-8d04-5774-5c36-7d69bf77d34e", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [], + "dependencies": [ + "tfcoremock_simple_resource.base_before" + ] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/moved_with_refresh_only/.terraform.lock.hcl b/testing/equivalence-tests/tests/moved_with_refresh_only/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_refresh_only/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/moved_with_refresh_only/main.tf b/testing/equivalence-tests/tests/moved_with_refresh_only/main.tf new file mode 100644 index 0000000000..356b980f9b --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_refresh_only/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + + +resource "tfcoremock_simple_resource" "second" { + string = "Hello, world!" +} + +moved { + from = tfcoremock_simple_resource.first + to = tfcoremock_simple_resource.second +} diff --git a/testing/equivalence-tests/tests/moved_with_refresh_only/spec.json b/testing/equivalence-tests/tests/moved_with_refresh_only/spec.json new file mode 100644 index 0000000000..8cae386da0 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_refresh_only/spec.json @@ -0,0 +1,43 @@ +{ + "description": "tests displaying a moved resource within a refresh only plan", + "include_files": [], + "ignore_fields": {}, + "commands": [ + { + "name": "init", + "arguments": ["init"], + "capture_output": false + }, + { + "name": "plan", + "arguments": ["plan", "-out=equivalence_test_plan", "-no-color", "-refresh-only"], + "capture_output": true, + "output_file_name": "plan", + "has_json_output": false + }, + { + "name": "apply", + "arguments": ["apply", "-json", "equivalence_test_plan"], + "capture_output": true, + "output_file_name": "apply.json", + "has_json_output": true, + "streams_json_output": true + }, + { + "name": "show_state", + "arguments": ["show", "-json"], + "capture_output": true, + "output_file_name": "state.json", + "has_json_output": true, + "streams_json_output": false + }, + { + "name": "show_plan", + "arguments": ["show", "-json", "equivalence_test_plan"], + "capture_output": true, + "output_file_name": "plan.json", + "has_json_output": true, + "streams_json_output": false + } + ] +} diff --git a/testing/equivalence-tests/tests/moved_with_refresh_only/terraform.resource/70c47571-66c3-b1dc-2474-47a74b9c7886.json b/testing/equivalence-tests/tests/moved_with_refresh_only/terraform.resource/70c47571-66c3-b1dc-2474-47a74b9c7886.json new file mode 100644 index 0000000000..6c63be352d --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_refresh_only/terraform.resource/70c47571-66c3-b1dc-2474-47a74b9c7886.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "70c47571-66c3-b1dc-2474-47a74b9c7886" + }, + "string": { + "string": "Hello, world!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/moved_with_refresh_only/terraform.tfstate b/testing/equivalence-tests/tests/moved_with_refresh_only/terraform.tfstate new file mode 100644 index 0000000000..89f82ba712 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_refresh_only/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "4a0f03a7-03fd-9357-9fd2-b3405139fa1d", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "first", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "70c47571-66c3-b1dc-2474-47a74b9c7886", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/moved_with_update/.terraform.lock.hcl b/testing/equivalence-tests/tests/moved_with_update/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_update/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/moved_with_update/main.tf b/testing/equivalence-tests/tests/moved_with_update/main.tf new file mode 100644 index 0000000000..a727802429 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_update/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_simple_resource" "moved" { + string = "Hello, change!" +} + +moved { + from = tfcoremock_simple_resource.base + to = tfcoremock_simple_resource.moved +} diff --git a/testing/equivalence-tests/tests/moved_with_update/spec.json b/testing/equivalence-tests/tests/moved_with_update/spec.json new file mode 100644 index 0000000000..dceb24c2b3 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "this test updates a resource that has also been moved", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/moved_with_update/terraform.resource/7da63aeb-f908-a112-9886-f29a0b0bd4ad.json b/testing/equivalence-tests/tests/moved_with_update/terraform.resource/7da63aeb-f908-a112-9886-f29a0b0bd4ad.json new file mode 100644 index 0000000000..dc908dc3c8 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_update/terraform.resource/7da63aeb-f908-a112-9886-f29a0b0bd4ad.json @@ -0,0 +1,10 @@ +{ + "values": { + "id": { + "string": "7da63aeb-f908-a112-9886-f29a0b0bd4ad" + }, + "string": { + "string": "Hello, world!" + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/moved_with_update/terraform.tfstate b/testing/equivalence-tests/tests/moved_with_update/terraform.tfstate new file mode 100644 index 0000000000..b338b65286 --- /dev/null +++ b/testing/equivalence-tests/tests/moved_with_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 1, + "lineage": "74f354aa-d7c6-5524-9573-cfdb625cb511", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_simple_resource", + "name": "base", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "bool": null, + "float": null, + "id": "7da63aeb-f908-a112-9886-f29a0b0bd4ad", + "integer": null, + "number": null, + "string": "Hello, world!" + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/multiple_block_types/dynamic_resources.json b/testing/equivalence-tests/tests/multiple_block_types/dynamic_resources.json new file mode 100644 index 0000000000..d51252e2c7 --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_multiple_blocks": { + "blocks": { + "first_block": { + "attributes": { + "id": { + "type": "string", + "required": true + } + }, + "mode": "list" + }, + "second_block": { + "attributes": { + "id": { + "type": "string", + "required": true + } + }, + "mode": "list" + } + } + } +} diff --git a/testing/equivalence-tests/tests/multiple_block_types/main.tf b/testing/equivalence-tests/tests/multiple_block_types/main.tf new file mode 100644 index 0000000000..370ada5f25 --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types/main.tf @@ -0,0 +1,34 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_multiple_blocks" "multiple_blocks" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + first_block { + id = "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + } + + first_block { + id = "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + + first_block { + id = "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + + second_block { + id = "157660A9-D590-469E-BE28-83B8526428CA" + } + + second_block { + id = "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } +} diff --git a/testing/equivalence-tests/tests/multiple_block_types/spec.json b/testing/equivalence-tests/tests/multiple_block_types/spec.json new file mode 100644 index 0000000000..675c10ef05 --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types/spec.json @@ -0,0 +1,5 @@ +{ + "description": "basic test case covering interaction between multiple blocks within a resource", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/multiple_block_types_update/dynamic_resources.json b/testing/equivalence-tests/tests/multiple_block_types_update/dynamic_resources.json new file mode 100644 index 0000000000..d51252e2c7 --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types_update/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_multiple_blocks": { + "blocks": { + "first_block": { + "attributes": { + "id": { + "type": "string", + "required": true + } + }, + "mode": "list" + }, + "second_block": { + "attributes": { + "id": { + "type": "string", + "required": true + } + }, + "mode": "list" + } + } + } +} diff --git a/testing/equivalence-tests/tests/multiple_block_types_update/main.tf b/testing/equivalence-tests/tests/multiple_block_types_update/main.tf new file mode 100644 index 0000000000..df3b5c0f93 --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types_update/main.tf @@ -0,0 +1,34 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_multiple_blocks" "multiple_blocks" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + first_block { + id = "B27FB8BE-52D4-4CEB-ACE9-5E7FB3968F2B" + } + + first_block { + id = "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + + first_block { + id = "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + + second_block { + id = "91640A80-A65F-4BEF-925B-684E4517A04D" + } + + second_block { + id = "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } +} diff --git a/testing/equivalence-tests/tests/multiple_block_types_update/spec.json b/testing/equivalence-tests/tests/multiple_block_types_update/spec.json new file mode 100644 index 0000000000..4fa45babba --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "basic test covering interaction between multiple blocks within a resource, while updating", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/multiple_block_types_update/terraform.resource/DA051126-BAD6-4EB2-92E5-F0250DAF0B92.json b/testing/equivalence-tests/tests/multiple_block_types_update/terraform.resource/DA051126-BAD6-4EB2-92E5-F0250DAF0B92.json new file mode 100644 index 0000000000..23f6818d1b --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types_update/terraform.resource/DA051126-BAD6-4EB2-92E5-F0250DAF0B92.json @@ -0,0 +1,50 @@ +{ + "values": { + "first_block": { + "list": [ + { + "object": { + "id": { + "string": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + } + } + }, + { + "object": { + "id": { + "string": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + } + } + }, + { + "object": { + "id": { + "string": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + } + } + ] + }, + "id": { + "string": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + }, + "second_block": { + "list": [ + { + "object": { + "id": { + "string": "157660A9-D590-469E-BE28-83B8526428CA" + } + } + }, + { + "object": { + "id": { + "string": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/multiple_block_types_update/terraform.tfstate b/testing/equivalence-tests/tests/multiple_block_types_update/terraform.tfstate new file mode 100644 index 0000000000..90829eb9e9 --- /dev/null +++ b/testing/equivalence-tests/tests/multiple_block_types_update/terraform.tfstate @@ -0,0 +1,44 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "a5c97830-8a8e-bf77-515e-481367d1e17e", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_multiple_blocks", + "name": "multiple_blocks", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "first_block": [ + { + "id": "D35E88DA-BC3B-46D7-9E0B-4ED4582FA65A" + }, + { + "id": "E60148A2-04D1-4EF8-90A2-45CAFC02C60D" + }, + { + "id": "717C64FB-6A93-4763-A1EF-FE4C5B341488" + } + ], + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "second_block": [ + { + "id": "157660A9-D590-469E-BE28-83B8526428CA" + }, + { + "id": "D080F298-2BA4-4DFA-A367-2C5FB0EA7BFE" + } + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/nested_list/dynamic_resources.json b/testing/equivalence-tests/tests/nested_list/dynamic_resources.json new file mode 100644 index 0000000000..5a9d24839d --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list/dynamic_resources.json @@ -0,0 +1,16 @@ +{ + "tfcoremock_nested_list": { + "attributes": { + "lists": { + "type": "list", + "required": true, + "list": { + "type": "list", + "list": { + "type": "string" + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_list/main.tf b/testing/equivalence-tests/tests/nested_list/main.tf new file mode 100644 index 0000000000..23652adafe --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_list" "nested_list" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + lists = [ + [], + ["44E1C623-7B70-4D78-B4D3-D9CFE8A6D982"], + ["13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD"], + ] +} diff --git a/testing/equivalence-tests/tests/nested_list/spec.json b/testing/equivalence-tests/tests/nested_list/spec.json new file mode 100644 index 0000000000..c60dbb18e6 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests creating lists within lists", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_list_update/dynamic_resources.json b/testing/equivalence-tests/tests/nested_list_update/dynamic_resources.json new file mode 100644 index 0000000000..5a9d24839d --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list_update/dynamic_resources.json @@ -0,0 +1,16 @@ +{ + "tfcoremock_nested_list": { + "attributes": { + "lists": { + "type": "list", + "required": true, + "list": { + "type": "list", + "list": { + "type": "string" + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_list_update/main.tf b/testing/equivalence-tests/tests/nested_list_update/main.tf new file mode 100644 index 0000000000..bc1808e14e --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list_update/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_list" "nested_list" { + id = "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + + lists = [ + ["44E1C623-7B70-4D78-B4D3-D9CFE8A6D982"], + ["8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD"], + ["13E3B154-7B85-4EAA-B3D0-E295E7D71D7F"], + ] +} diff --git a/testing/equivalence-tests/tests/nested_list_update/spec.json b/testing/equivalence-tests/tests/nested_list_update/spec.json new file mode 100644 index 0000000000..24a0a6de69 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating a nested list attribute within a resource", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_list_update/terraform.resource/DA051126-BAD6-4EB2-92E5-F0250DAF0B92.json b/testing/equivalence-tests/tests/nested_list_update/terraform.resource/DA051126-BAD6-4EB2-92E5-F0250DAF0B92.json new file mode 100644 index 0000000000..da4dd86e0f --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list_update/terraform.resource/DA051126-BAD6-4EB2-92E5-F0250DAF0B92.json @@ -0,0 +1,31 @@ +{ + "values": { + "id": { + "string": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92" + }, + "lists": { + "list": [ + { + "list": [] + }, + { + "list": [ + { + "string": "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + } + ] + }, + { + "list": [ + { + "string": "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F" + }, + { + "string": "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/nested_list_update/terraform.tfstate b/testing/equivalence-tests/tests/nested_list_update/terraform.tfstate new file mode 100644 index 0000000000..7aed46c268 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_list_update/terraform.tfstate @@ -0,0 +1,35 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "a370201d-6899-c596-f68f-01dfdf622d44", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_nested_list", + "name": "nested_list", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "DA051126-BAD6-4EB2-92E5-F0250DAF0B92", + "lists": [ + [], + [ + "44E1C623-7B70-4D78-B4D3-D9CFE8A6D982" + ], + [ + "13E3B154-7B85-4EAA-B3D0-E295E7D71D7F", + "8B031CD1-01F7-422C-BBE6-FF8A0E18CDFD" + ] + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/nested_map/dynamic_resources.json b/testing/equivalence-tests/tests/nested_map/dynamic_resources.json new file mode 100644 index 0000000000..0bab718428 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map/dynamic_resources.json @@ -0,0 +1,16 @@ +{ + "tfcoremock_nested_map": { + "attributes": { + "maps": { + "type": "map", + "required": true, + "map": { + "type": "map", + "map": { + "type": "string" + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_map/main.tf b/testing/equivalence-tests/tests/nested_map/main.tf new file mode 100644 index 0000000000..1db6d160fa --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_map" "nested_map" { + id = "502B0348-B796-4F6A-8694-A5A397237B85" + + maps = { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } +} diff --git a/testing/equivalence-tests/tests/nested_map/spec.json b/testing/equivalence-tests/tests/nested_map/spec.json new file mode 100644 index 0000000000..0590e12714 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests creating maps within maps", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_map_update/dynamic_resources.json b/testing/equivalence-tests/tests/nested_map_update/dynamic_resources.json new file mode 100644 index 0000000000..0bab718428 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map_update/dynamic_resources.json @@ -0,0 +1,16 @@ +{ + "tfcoremock_nested_map": { + "attributes": { + "maps": { + "type": "map", + "required": true, + "map": { + "type": "map", + "map": { + "type": "string" + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_map_update/main.tf b/testing/equivalence-tests/tests/nested_map_update/main.tf new file mode 100644 index 0000000000..91e57e037b --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map_update/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_map" "nested_map" { + id = "502B0348-B796-4F6A-8694-A5A397237B85" + + maps = { + "first_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + "third_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + }, + "second_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + } + } +} diff --git a/testing/equivalence-tests/tests/nested_map_update/spec.json b/testing/equivalence-tests/tests/nested_map_update/spec.json new file mode 100644 index 0000000000..b313e21f2f --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating a map nested within another map", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_map_update/terraform.resource/502B0348-B796-4F6A-8694-A5A397237B85.json b/testing/equivalence-tests/tests/nested_map_update/terraform.resource/502B0348-B796-4F6A-8694-A5A397237B85.json new file mode 100644 index 0000000000..c41344752f --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map_update/terraform.resource/502B0348-B796-4F6A-8694-A5A397237B85.json @@ -0,0 +1,31 @@ +{ + "values": { + "id": { + "string": "502B0348-B796-4F6A-8694-A5A397237B85" + }, + "maps": { + "map": { + "first_nested_map": { + "map": { + "first_key": { + "string": "9E858021-953F-4DD3-8842-F2C782780422" + }, + "second_key": { + "string": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + } + } + }, + "second_nested_map": { + "map": { + "first_key": { + "string": "6E80C701-A823-43FE-A520-699851EF9052" + }, + "second_key": { + "string": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/nested_map_update/terraform.tfstate b/testing/equivalence-tests/tests/nested_map_update/terraform.tfstate new file mode 100644 index 0000000000..b64ef7d258 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_map_update/terraform.tfstate @@ -0,0 +1,35 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "f6b2f8a8-d060-337c-a45a-25c038eb196f", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_nested_map", + "name": "nested_map", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "502B0348-B796-4F6A-8694-A5A397237B85", + "maps": { + "first_nested_map": { + "first_key": "9E858021-953F-4DD3-8842-F2C782780422", + "second_key": "D55D0E1E-51D9-4BCE-9021-7D201906D3C0" + }, + "second_nested_map": { + "first_key": "6E80C701-A823-43FE-A520-699851EF9052", + "second_key": "79CBEBB1-1192-480A-B4A8-E816A1A9D2FC" + } + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/nested_objects/dynamic_resources.json b/testing/equivalence-tests/tests/nested_objects/dynamic_resources.json new file mode 100644 index 0000000000..30f8c82e19 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects/dynamic_resources.json @@ -0,0 +1,40 @@ +{ + "tfcoremock_nested_object": { + "attributes": { + "parent_object": { + "type": "object", + "required": true, + "object": { + "first_nested_object": { + "type": "object", + "required": true, + "object": { + "attribute_one": { + "type": "string", + "required": true + }, + "attribute_two": { + "type": "string", + "required": true + } + } + }, + "second_nested_object": { + "type": "object", + "required": true, + "object": { + "attribute_one": { + "type": "string", + "required": true + }, + "attribute_two": { + "type": "string", + "required": true + } + } + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_objects/main.tf b/testing/equivalence-tests/tests/nested_objects/main.tf new file mode 100644 index 0000000000..b8bd36bdbe --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_object" "nested_object" { + id = "B2491EF0-9361-40FD-B25A-0332A1A5E052" + + parent_object = { + first_nested_object = { + attribute_one = "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + attribute_two = "5425587C-49EF-4C1E-A906-1DC923A12725" + } + second_nested_object = { + attribute_one = "63712BFE-78F8-42D3-A074-A78249E5E25E", + attribute_two = "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } +} diff --git a/testing/equivalence-tests/tests/nested_objects/spec.json b/testing/equivalence-tests/tests/nested_objects/spec.json new file mode 100644 index 0000000000..52a765cc68 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests creating objects within objects", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_objects_update/dynamic_resources.json b/testing/equivalence-tests/tests/nested_objects_update/dynamic_resources.json new file mode 100644 index 0000000000..30f8c82e19 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects_update/dynamic_resources.json @@ -0,0 +1,40 @@ +{ + "tfcoremock_nested_object": { + "attributes": { + "parent_object": { + "type": "object", + "required": true, + "object": { + "first_nested_object": { + "type": "object", + "required": true, + "object": { + "attribute_one": { + "type": "string", + "required": true + }, + "attribute_two": { + "type": "string", + "required": true + } + } + }, + "second_nested_object": { + "type": "object", + "required": true, + "object": { + "attribute_one": { + "type": "string", + "required": true + }, + "attribute_two": { + "type": "string", + "required": true + } + } + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_objects_update/main.tf b/testing/equivalence-tests/tests/nested_objects_update/main.tf new file mode 100644 index 0000000000..c351eb15d0 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects_update/main.tf @@ -0,0 +1,25 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_object" "nested_object" { + id = "B2491EF0-9361-40FD-B25A-0332A1A5E052" + + parent_object = { + first_nested_object = { + attribute_one = "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + attribute_two = "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + second_nested_object = { + attribute_one = "63712BFE-78F8-42D3-A074-A78249E5E25E", + attribute_two = "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } +} diff --git a/testing/equivalence-tests/tests/nested_objects_update/spec.json b/testing/equivalence-tests/tests/nested_objects_update/spec.json new file mode 100644 index 0000000000..cd8465f2f0 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating objects that are nested within other objects", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_objects_update/terraform.resource/B2491EF0-9361-40FD-B25A-0332A1A5E052.json b/testing/equivalence-tests/tests/nested_objects_update/terraform.resource/B2491EF0-9361-40FD-B25A-0332A1A5E052.json new file mode 100644 index 0000000000..58c69da2ba --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects_update/terraform.resource/B2491EF0-9361-40FD-B25A-0332A1A5E052.json @@ -0,0 +1,31 @@ +{ + "values": { + "id": { + "string": "B2491EF0-9361-40FD-B25A-0332A1A5E052" + }, + "parent_object": { + "object": { + "first_nested_object": { + "object": { + "attribute_one": { + "string": "09AE7244-7BFB-476B-912C-D1AB4E7E9622" + }, + "attribute_two": { + "string": "5425587C-49EF-4C1E-A906-1DC923A12725" + } + } + }, + "second_nested_object": { + "object": { + "attribute_one": { + "string": "63712BFE-78F8-42D3-A074-A78249E5E25E" + }, + "attribute_two": { + "string": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/nested_objects_update/terraform.tfstate b/testing/equivalence-tests/tests/nested_objects_update/terraform.tfstate new file mode 100644 index 0000000000..765933a117 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_objects_update/terraform.tfstate @@ -0,0 +1,35 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "2fd492f4-7a34-53d6-de72-96debd114238", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_nested_object", + "name": "nested_object", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "B2491EF0-9361-40FD-B25A-0332A1A5E052", + "parent_object": { + "first_nested_object": { + "attribute_one": "09AE7244-7BFB-476B-912C-D1AB4E7E9622", + "attribute_two": "5425587C-49EF-4C1E-A906-1DC923A12725" + }, + "second_nested_object": { + "attribute_one": "63712BFE-78F8-42D3-A074-A78249E5E25E", + "attribute_two": "FB350D92-4AAE-48C6-A408-BFFAFAD46B04" + } + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/nested_set/dynamic_resources.json b/testing/equivalence-tests/tests/nested_set/dynamic_resources.json new file mode 100644 index 0000000000..478a0d7677 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set/dynamic_resources.json @@ -0,0 +1,16 @@ +{ + "tfcoremock_nested_set": { + "attributes": { + "sets": { + "type": "set", + "required": true, + "set": { + "type": "set", + "set": { + "type": "string" + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_set/main.tf b/testing/equivalence-tests/tests/nested_set/main.tf new file mode 100644 index 0000000000..9c486dfa18 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_set" "nested_set" { + id = "510598F6-83FE-4090-8986-793293E90480" + + sets = [ + [], + ["9373D62D-1BF0-4F17-B100-7C0FBE368ADE"], + ["7E90963C-BE32-4411-B9DD-B02E7FE75766", "29B6824A-5CB6-4C25-A359-727BAFEF25EB"], + ] +} diff --git a/testing/equivalence-tests/tests/nested_set/spec.json b/testing/equivalence-tests/tests/nested_set/spec.json new file mode 100644 index 0000000000..967ec8580b --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests creating sets within sets", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_set_update/dynamic_resources.json b/testing/equivalence-tests/tests/nested_set_update/dynamic_resources.json new file mode 100644 index 0000000000..478a0d7677 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set_update/dynamic_resources.json @@ -0,0 +1,16 @@ +{ + "tfcoremock_nested_set": { + "attributes": { + "sets": { + "type": "set", + "required": true, + "set": { + "type": "set", + "set": { + "type": "string" + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/nested_set_update/main.tf b/testing/equivalence-tests/tests/nested_set_update/main.tf new file mode 100644 index 0000000000..e3f7b747ce --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set_update/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_nested_set" "nested_set" { + id = "510598F6-83FE-4090-8986-793293E90480" + + sets = [ + ["29B6824A-5CB6-4C25-A359-727BAFEF25EB"], + ["9373D62D-1BF0-4F17-B100-7C0FBE368ADE"], + ["7E90963C-BE32-4411-B9DD-B02E7FE75766"], + ] +} diff --git a/testing/equivalence-tests/tests/nested_set_update/spec.json b/testing/equivalence-tests/tests/nested_set_update/spec.json new file mode 100644 index 0000000000..a1d1c9ab19 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating sets when they are nested within other sets", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/nested_set_update/terraform.resource/510598F6-83FE-4090-8986-793293E90480.json b/testing/equivalence-tests/tests/nested_set_update/terraform.resource/510598F6-83FE-4090-8986-793293E90480.json new file mode 100644 index 0000000000..67986905e8 --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set_update/terraform.resource/510598F6-83FE-4090-8986-793293E90480.json @@ -0,0 +1,31 @@ +{ + "values": { + "id": { + "string": "510598F6-83FE-4090-8986-793293E90480" + }, + "sets": { + "set": [ + { + "set": [ + { + "string": "29B6824A-5CB6-4C25-A359-727BAFEF25EB" + }, + { + "string": "7E90963C-BE32-4411-B9DD-B02E7FE75766" + } + ] + }, + { + "set": [ + { + "string": "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + } + ] + }, + { + "set": [] + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/nested_set_update/terraform.tfstate b/testing/equivalence-tests/tests/nested_set_update/terraform.tfstate new file mode 100644 index 0000000000..3d89c4983e --- /dev/null +++ b/testing/equivalence-tests/tests/nested_set_update/terraform.tfstate @@ -0,0 +1,35 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "68208952-1936-3604-26c2-bcd453e7d1ad", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_nested_set", + "name": "nested_set", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "510598F6-83FE-4090-8986-793293E90480", + "sets": [ + [ + "29B6824A-5CB6-4C25-A359-727BAFEF25EB", + "7E90963C-BE32-4411-B9DD-B02E7FE75766" + ], + [ + "9373D62D-1BF0-4F17-B100-7C0FBE368ADE" + ], + [] + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/null_provider_delete/.terraform.lock.hcl b/testing/equivalence-tests/tests/null_provider_delete/.terraform.lock.hcl new file mode 100644 index 0000000000..6ed19d1db2 --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_delete/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", + ] +} diff --git a/testing/equivalence-tests/tests/null_provider_delete/main.tf b/testing/equivalence-tests/tests/null_provider_delete/main.tf new file mode 100644 index 0000000000..f4234bfebb --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_delete/main.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.1" + } + } +} + +provider "null" {} diff --git a/testing/equivalence-tests/tests/null_provider_delete/spec.json b/testing/equivalence-tests/tests/null_provider_delete/spec.json new file mode 100644 index 0000000000..0ab998cb3f --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_delete/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests deleting a resource created by the null provider", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/null_provider_delete/terraform.tfstate b/testing/equivalence-tests/tests/null_provider_delete/terraform.tfstate new file mode 100644 index 0000000000..ca15ed9df5 --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_delete/terraform.tfstate @@ -0,0 +1,27 @@ +{ + "version": 4, + "terraform_version": "1.3.0", + "serial": 2, + "lineage": "bc759d94-5aca-e092-1b90-cb90e6227c62", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "null_resource", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "7115293105928418144", + "triggers": null + }, + "sensitive_attributes": [], + "private": "bnVsbA==" + } + ] + } + ], + "check_results": [] +} diff --git a/testing/equivalence-tests/tests/null_provider_update/.terraform.lock.hcl b/testing/equivalence-tests/tests/null_provider_update/.terraform.lock.hcl new file mode 100644 index 0000000000..6ed19d1db2 --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_update/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/null" { + version = "3.1.1" + constraints = "3.1.1" + hashes = [ + "h1:YvH6gTaQzGdNv+SKTZujU1O0bO+Pw6vJHOPhqgN8XNs=", + "zh:063466f41f1d9fd0dd93722840c1314f046d8760b1812fa67c34de0afcba5597", + "zh:08c058e367de6debdad35fc24d97131c7cf75103baec8279aba3506a08b53faf", + "zh:73ce6dff935150d6ddc6ac4a10071e02647d10175c173cfe5dca81f3d13d8afe", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8fdd792a626413502e68c195f2097352bdc6a0df694f7df350ed784741eb587e", + "zh:976bbaf268cb497400fd5b3c774d218f3933271864345f18deebe4dcbfcd6afa", + "zh:b21b78ca581f98f4cdb7a366b03ae9db23a73dfa7df12c533d7c19b68e9e72e5", + "zh:b7fc0c1615dbdb1d6fd4abb9c7dc7da286631f7ca2299fb9cd4664258ccfbff4", + "zh:d1efc942b2c44345e0c29bc976594cb7278c38cfb8897b344669eafbc3cddf46", + "zh:e356c245b3cd9d4789bab010893566acace682d7db877e52d40fc4ca34a50924", + "zh:ea98802ba92fcfa8cf12cbce2e9e7ebe999afbf8ed47fa45fc847a098d89468b", + "zh:eff8872458806499889f6927b5d954560f3d74bf20b6043409edf94d26cd906f", + ] +} diff --git a/testing/equivalence-tests/tests/null_provider_update/main.tf b/testing/equivalence-tests/tests/null_provider_update/main.tf new file mode 100644 index 0000000000..31d6ee217a --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_update/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + null = { + source = "hashicorp/null" + version = "3.1.1" + } + } +} + +provider "null" {} + +resource "null_resource" "null_resource" {} diff --git a/testing/equivalence-tests/tests/null_provider_update/spec.json b/testing/equivalence-tests/tests/null_provider_update/spec.json new file mode 100644 index 0000000000..fcd4cd67bc --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests creating a simple resource created by the null provider", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/null_provider_update/terraform.tfstate b/testing/equivalence-tests/tests/null_provider_update/terraform.tfstate new file mode 100644 index 0000000000..c3b48aedfa --- /dev/null +++ b/testing/equivalence-tests/tests/null_provider_update/terraform.tfstate @@ -0,0 +1,27 @@ +{ + "version": 4, + "terraform_version": "1.10.0", + "serial": 1, + "lineage": "afca9e36-5040-2a11-9225-a64c8caa4605", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "null_resource", + "name": "null_resource", + "provider": "provider[\"registry.terraform.io/hashicorp/null\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "3637779521417605172", + "triggers": null + }, + "sensitive_attributes": [], + "private": "bnVsbA==" + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/replace_within_list/.terraform.lock.hcl b/testing/equivalence-tests/tests/replace_within_list/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_list/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/replace_within_list/dynamic_resources.json b/testing/equivalence-tests/tests/replace_within_list/dynamic_resources.json new file mode 100644 index 0000000000..5d8e42bdaa --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_list/dynamic_resources.json @@ -0,0 +1,20 @@ +{ + "tfcoremock_list": { + "attributes": { + "list": { + "type": "list", + "required": true, + "list": { + "type": "object", + "object": { + "id": { + "type": "string", + "replace": true, + "required": true + } + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/replace_within_list/main.tf b/testing/equivalence-tests/tests/replace_within_list/main.tf new file mode 100644 index 0000000000..3f4d34b617 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_list/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_list" "list" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + + list = [ + { + id = "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + id = "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + ] +} diff --git a/testing/equivalence-tests/tests/replace_within_list/spec.json b/testing/equivalence-tests/tests/replace_within_list/spec.json new file mode 100644 index 0000000000..002a35bddd --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_list/spec.json @@ -0,0 +1,6 @@ +{ + "description": "tests the behaviour of an attribute within a list causing a resource to be replaced", + "include_files": [], + "ignore_fields": {} +} + diff --git a/testing/equivalence-tests/tests/replace_within_list/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json b/testing/equivalence-tests/tests/replace_within_list/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json new file mode 100644 index 0000000000..b1c22ca9df --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_list/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json @@ -0,0 +1,32 @@ +{ + "values": { + "id": { + "string": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "list": { + "list": [ + { + "object": { + "id": { + "string": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + } + } + }, + { + "object": { + "id": { + "string": "6A8C6A29-D417-480A-BE19-12D7398B3178" + } + } + }, + { + "object": { + "id": { + "string": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/replace_within_list/terraform.tfstate b/testing/equivalence-tests/tests/replace_within_list/terraform.tfstate new file mode 100644 index 0000000000..9526deff96 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_list/terraform.tfstate @@ -0,0 +1,36 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 4, + "lineage": "a0e7bfac-ce3d-9720-1b4e-d6e6c58cf620", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_list", + "name": "list", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "list": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "6A8C6A29-D417-480A-BE19-12D7398B3178" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/replace_within_map/.terraform.lock.hcl b/testing/equivalence-tests/tests/replace_within_map/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_map/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/replace_within_map/dynamic_resources.json b/testing/equivalence-tests/tests/replace_within_map/dynamic_resources.json new file mode 100644 index 0000000000..6034bef4a4 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_map/dynamic_resources.json @@ -0,0 +1,20 @@ +{ + "tfcoremock_map": { + "attributes": { + "map": { + "type": "map", + "required": true, + "map": { + "type": "object", + "object": { + "id": { + "type": "string", + "replace": true, + "required": true + } + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/replace_within_map/main.tf b/testing/equivalence-tests/tests/replace_within_map/main.tf new file mode 100644 index 0000000000..aafa092a33 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_map/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_map" "map" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + + map = { + "key_one" = { + id = "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_two" = { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + "key_three" = { + id = "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + } +} diff --git a/testing/equivalence-tests/tests/replace_within_map/spec.json b/testing/equivalence-tests/tests/replace_within_map/spec.json new file mode 100644 index 0000000000..602fcd52ce --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_map/spec.json @@ -0,0 +1,6 @@ +{ + "description": "tests the behaviour of an attribute within a map causing a resource to be replaced", + "include_files": [], + "ignore_fields": {} +} + diff --git a/testing/equivalence-tests/tests/replace_within_map/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json b/testing/equivalence-tests/tests/replace_within_map/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json new file mode 100644 index 0000000000..86c7de1ea7 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_map/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json @@ -0,0 +1,32 @@ +{ + "values": { + "id": { + "string": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "map": { + "map": { + "key_one": { + "object": { + "id": { + "string": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + } + } + }, + "key_three": { + "object": { + "id": { + "string": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + } + }, + "key_two": { + "object": { + "id": { + "string": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/replace_within_map/terraform.tfstate b/testing/equivalence-tests/tests/replace_within_map/terraform.tfstate new file mode 100644 index 0000000000..5ca2114e26 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_map/terraform.tfstate @@ -0,0 +1,36 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 4, + "lineage": "761a9430-9f20-5cf3-d66e-c3e345115ed1", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_map", + "name": "map", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "map": { + "key_one": { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + "key_three": { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + "key_two": { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/replace_within_object/.terraform.lock.hcl b/testing/equivalence-tests/tests/replace_within_object/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_object/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/replace_within_object/dynamic_resources.json b/testing/equivalence-tests/tests/replace_within_object/dynamic_resources.json new file mode 100644 index 0000000000..641f64640c --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_object/dynamic_resources.json @@ -0,0 +1,17 @@ +{ + "tfcoremock_object": { + "attributes": { + "object": { + "type": "object", + "required": true, + "object": { + "id": { + "type": "string", + "replace": true, + "required": true + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/replace_within_object/main.tf b/testing/equivalence-tests/tests/replace_within_object/main.tf new file mode 100644 index 0000000000..9cda772181 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_object/main.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_object" "object" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + + object = { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + } +} diff --git a/testing/equivalence-tests/tests/replace_within_object/spec.json b/testing/equivalence-tests/tests/replace_within_object/spec.json new file mode 100644 index 0000000000..e1601f23bc --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_object/spec.json @@ -0,0 +1,6 @@ +{ + "description": "tests the behaviour of an attribute within an object causing a resource to be replaced", + "include_files": [], + "ignore_fields": {} +} + diff --git a/testing/equivalence-tests/tests/replace_within_object/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json b/testing/equivalence-tests/tests/replace_within_object/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json new file mode 100644 index 0000000000..49395abd35 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_object/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json @@ -0,0 +1,14 @@ +{ + "values": { + "id": { + "string": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "object": { + "object": { + "id": { + "string": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/replace_within_object/terraform.tfstate b/testing/equivalence-tests/tests/replace_within_object/terraform.tfstate new file mode 100644 index 0000000000..1162e6a266 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_object/terraform.tfstate @@ -0,0 +1,28 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 4, + "lineage": "02b0a9ca-8da4-ad32-a144-3cc984a1d395", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_object", + "name": "object", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "object": { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/replace_within_set/.terraform.lock.hcl b/testing/equivalence-tests/tests/replace_within_set/.terraform.lock.hcl new file mode 100644 index 0000000000..d2d6193ccb --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_set/.terraform.lock.hcl @@ -0,0 +1,22 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tfcoremock" { + version = "0.1.1" + constraints = "0.1.1" + hashes = [ + "h1:K5ImnTbl0eD02BQgVe6pNn5w2wW07j9t7iDUwqPvcy0=", + "zh:0219ffa02c6f2cf1f519ea42a89615cfbab8219803bcdb4a3614793952b1f5a8", + "zh:13915c1eb4f2ad384c6ce343eebe108ddfb0a103e19f1d8b199c0af8e57bc74a", + "zh:1c265814efa730540a475f76a8045d90a70c22010d6f2eee45f69e973ce10580", + "zh:34d58f6bf64afc491359ad1455007e1eb9aef60be17909096ec88a802b6f72b2", + "zh:7a7d709aeb7b2945f5a9a0497976a85bceb07b2cbf236c8c1bb0b7b079b839ab", + "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", + "zh:965fbb6042acd6fbf77a9b5c3321edbfa0aa6bf0359394afbf0a7e1392245324", + "zh:9e4ac8eae03d1243b9283b202132c007d5c4ee533d1c5efc690403caaaaa7aac", + "zh:a51833ca1c6983e32a937ea3864b6037c1ccb97a4917cafb38771a3aa583cc77", + "zh:ac2eba5efca9f0bf4ecca3b23c2a89c4318ef7cda45e374811e42cced0aa660b", + "zh:c77663f4c0e4ca799e5f1abfa94cfe15f36e9879e637a9196ea01fcaeba13286", + "zh:dc0ab43f1affe80e117cea108780afe53d7c97357566ded87f53221828c875de", + ] +} diff --git a/testing/equivalence-tests/tests/replace_within_set/dynamic_resources.json b/testing/equivalence-tests/tests/replace_within_set/dynamic_resources.json new file mode 100644 index 0000000000..25aa1369a7 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_set/dynamic_resources.json @@ -0,0 +1,20 @@ +{ + "tfcoremock_set": { + "attributes": { + "set": { + "type": "set", + "required": true, + "set": { + "type": "object", + "object": { + "id": { + "type": "string", + "replace": true, + "required": true + } + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/replace_within_set/main.tf b/testing/equivalence-tests/tests/replace_within_set/main.tf new file mode 100644 index 0000000000..64a599b9d6 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_set/main.tf @@ -0,0 +1,26 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_set" "set" { + id = "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + + set = [ + { + id = "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + id = "07F887E2-FDFF-4B2E-9BFB-B6AA4A05EDB9" + }, + { + id = "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + ] +} diff --git a/testing/equivalence-tests/tests/replace_within_set/spec.json b/testing/equivalence-tests/tests/replace_within_set/spec.json new file mode 100644 index 0000000000..3a08ac22e4 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_set/spec.json @@ -0,0 +1,6 @@ +{ + "description": "tests the behaviour of an attribute within a set causing a resource to be replaced", + "include_files": [], + "ignore_fields": {} +} + diff --git a/testing/equivalence-tests/tests/replace_within_set/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json b/testing/equivalence-tests/tests/replace_within_set/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json new file mode 100644 index 0000000000..b8f76cc177 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_set/terraform.resource/F40F2AB4-100C-4AE8-BFD0-BF332A158415.json @@ -0,0 +1,32 @@ +{ + "values": { + "id": { + "string": "F40F2AB4-100C-4AE8-BFD0-BF332A158415" + }, + "set": { + "set": [ + { + "object": { + "id": { + "string": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + } + } + }, + { + "object": { + "id": { + "string": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + } + } + }, + { + "object": { + "id": { + "string": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + } + } + ] + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/replace_within_set/terraform.tfstate b/testing/equivalence-tests/tests/replace_within_set/terraform.tfstate new file mode 100644 index 0000000000..8c33aca3f0 --- /dev/null +++ b/testing/equivalence-tests/tests/replace_within_set/terraform.tfstate @@ -0,0 +1,36 @@ +{ + "version": 4, + "terraform_version": "1.3.7", + "serial": 4, + "lineage": "a0f911d6-cc86-a0d6-4d4c-967c11b80966", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_set", + "name": "set", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "F40F2AB4-100C-4AE8-BFD0-BF332A158415", + "set": [ + { + "id": "3BFC1A84-023F-44FA-A8EE-EFD88E18B8F7" + }, + { + "id": "4B7178A8-AB9D-4FF4-8B3D-48B754DE537B" + }, + { + "id": "56C7E07F-B9DF-4799-AF62-E703D1167A51" + } + ] + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/simple_object/dynamic_resources.json b/testing/equivalence-tests/tests/simple_object/dynamic_resources.json new file mode 100644 index 0000000000..dfda8f3e13 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_object": { + "attributes": { + "object": { + "type": "object", + "optional": true, + "object": { + "string": { + "type": "string", + "optional": true + }, + "boolean": { + "type": "boolean", + "optional": true + }, + "number": { + "type": "number", + "optional": true + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/simple_object/main.tf b/testing/equivalence-tests/tests/simple_object/main.tf new file mode 100644 index 0000000000..7881c59081 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_object" "object" { + id = "AF9833AE-3434-4D0B-8B69-F4B992565D9F" + object = { + string = "Hello, world!" + boolean = true + number = 10 + } +} diff --git a/testing/equivalence-tests/tests/simple_object/spec.json b/testing/equivalence-tests/tests/simple_object/spec.json new file mode 100644 index 0000000000..e9b771e0a2 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests creating a simple object with primitive attributes", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/simple_object_empty/dynamic_resources.json b/testing/equivalence-tests/tests/simple_object_empty/dynamic_resources.json new file mode 100644 index 0000000000..dfda8f3e13 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_empty/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_object": { + "attributes": { + "object": { + "type": "object", + "optional": true, + "object": { + "string": { + "type": "string", + "optional": true + }, + "boolean": { + "type": "boolean", + "optional": true + }, + "number": { + "type": "number", + "optional": true + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/simple_object_empty/main.tf b/testing/equivalence-tests/tests/simple_object_empty/main.tf new file mode 100644 index 0000000000..c0e38ad82b --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_empty/main.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_object" "object" { + object = {} +} diff --git a/testing/equivalence-tests/tests/simple_object_empty/spec.json b/testing/equivalence-tests/tests/simple_object_empty/spec.json new file mode 100644 index 0000000000..97195acb61 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_empty/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests removing all attributes from an object", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/simple_object_empty/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json b/testing/equivalence-tests/tests/simple_object_empty/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json new file mode 100644 index 0000000000..cea8724dee --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_empty/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "00e14fba-4d56-6cc5-b685-633555376e3f" + }, + "object": { + "object": { + "boolean": { + "boolean": true + }, + "number": { + "number": "10" + }, + "string": { + "string": "Hello, world!" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/simple_object_empty/terraform.tfstate b/testing/equivalence-tests/tests/simple_object_empty/terraform.tfstate new file mode 100644 index 0000000000..cfee84dac8 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_empty/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.5", + "serial": 1, + "lineage": "daaaeb37-0157-6c8c-2de9-7687bf0a6040", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_object", + "name": "object", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/simple_object_null/dynamic_resources.json b/testing/equivalence-tests/tests/simple_object_null/dynamic_resources.json new file mode 100644 index 0000000000..dfda8f3e13 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_null/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_object": { + "attributes": { + "object": { + "type": "object", + "optional": true, + "object": { + "string": { + "type": "string", + "optional": true + }, + "boolean": { + "type": "boolean", + "optional": true + }, + "number": { + "type": "number", + "optional": true + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/simple_object_null/main.tf b/testing/equivalence-tests/tests/simple_object_null/main.tf new file mode 100644 index 0000000000..62c0f194a5 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_null/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_object" "object" {} diff --git a/testing/equivalence-tests/tests/simple_object_null/spec.json b/testing/equivalence-tests/tests/simple_object_null/spec.json new file mode 100644 index 0000000000..9f4e3080da --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_null/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests setting an object within a resource to null by removing it from the config", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/simple_object_null/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json b/testing/equivalence-tests/tests/simple_object_null/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json new file mode 100644 index 0000000000..cea8724dee --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_null/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "00e14fba-4d56-6cc5-b685-633555376e3f" + }, + "object": { + "object": { + "boolean": { + "boolean": true + }, + "number": { + "number": "10" + }, + "string": { + "string": "Hello, world!" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/simple_object_null/terraform.tfstate b/testing/equivalence-tests/tests/simple_object_null/terraform.tfstate new file mode 100644 index 0000000000..cfee84dac8 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_null/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.5", + "serial": 1, + "lineage": "daaaeb37-0157-6c8c-2de9-7687bf0a6040", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_object", + "name": "object", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/simple_object_replace/dynamic_resources.json b/testing/equivalence-tests/tests/simple_object_replace/dynamic_resources.json new file mode 100644 index 0000000000..dfda8f3e13 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_replace/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_object": { + "attributes": { + "object": { + "type": "object", + "optional": true, + "object": { + "string": { + "type": "string", + "optional": true + }, + "boolean": { + "type": "boolean", + "optional": true + }, + "number": { + "type": "number", + "optional": true + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/simple_object_replace/main.tf b/testing/equivalence-tests/tests/simple_object_replace/main.tf new file mode 100644 index 0000000000..0b82b8e2dc --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_replace/main.tf @@ -0,0 +1,20 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_object" "object" { + id = "63A9E8E8-71BC-4DAE-A66C-48CE393CCBD3" + + object = { + string = "Hello, world!" + boolean = true + number = 10 + } +} diff --git a/testing/equivalence-tests/tests/simple_object_replace/spec.json b/testing/equivalence-tests/tests/simple_object_replace/spec.json new file mode 100644 index 0000000000..4cc29f5773 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_replace/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating an attribute that forces the overall resource to be replaced", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/simple_object_replace/terraform.resource/a0ed13ec-116b-14c4-7437-418e217d3659.json b/testing/equivalence-tests/tests/simple_object_replace/terraform.resource/a0ed13ec-116b-14c4-7437-418e217d3659.json new file mode 100644 index 0000000000..ccd29f182e --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_replace/terraform.resource/a0ed13ec-116b-14c4-7437-418e217d3659.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "a0ed13ec-116b-14c4-7437-418e217d3659" + }, + "object": { + "object": { + "boolean": { + "boolean": true + }, + "number": { + "number": "10" + }, + "string": { + "string": "Hello, world!" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/simple_object_replace/terraform.tfstate b/testing/equivalence-tests/tests/simple_object_replace/terraform.tfstate new file mode 100644 index 0000000000..c31363da93 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_replace/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.5", + "serial": 1, + "lineage": "d6a2102b-030b-3b20-a00c-6d0256541c88", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_object", + "name": "object", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "a0ed13ec-116b-14c4-7437-418e217d3659", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/simple_object_update/dynamic_resources.json b/testing/equivalence-tests/tests/simple_object_update/dynamic_resources.json new file mode 100644 index 0000000000..dfda8f3e13 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_update/dynamic_resources.json @@ -0,0 +1,24 @@ +{ + "tfcoremock_object": { + "attributes": { + "object": { + "type": "object", + "optional": true, + "object": { + "string": { + "type": "string", + "optional": true + }, + "boolean": { + "type": "boolean", + "optional": true + }, + "number": { + "type": "number", + "optional": true + } + } + } + } + } +} diff --git a/testing/equivalence-tests/tests/simple_object_update/main.tf b/testing/equivalence-tests/tests/simple_object_update/main.tf new file mode 100644 index 0000000000..8d48cc5294 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_update/main.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + tfcoremock = { + source = "hashicorp/tfcoremock" + version = "0.1.1" + } + } +} + +provider "tfcoremock" {} + +resource "tfcoremock_object" "object" { + object = { + string = "Hello, a totally different world!" + boolean = false + number = 2 + } +} diff --git a/testing/equivalence-tests/tests/simple_object_update/spec.json b/testing/equivalence-tests/tests/simple_object_update/spec.json new file mode 100644 index 0000000000..37fb69faad --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_update/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests updating objects when they are nested in other objects", + "include_files": [], + "ignore_fields": {} +} diff --git a/testing/equivalence-tests/tests/simple_object_update/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json b/testing/equivalence-tests/tests/simple_object_update/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json new file mode 100644 index 0000000000..cea8724dee --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_update/terraform.resource/00e14fba-4d56-6cc5-b685-633555376e3f.json @@ -0,0 +1,20 @@ +{ + "values": { + "id": { + "string": "00e14fba-4d56-6cc5-b685-633555376e3f" + }, + "object": { + "object": { + "boolean": { + "boolean": true + }, + "number": { + "number": "10" + }, + "string": { + "string": "Hello, world!" + } + } + } + } +} \ No newline at end of file diff --git a/testing/equivalence-tests/tests/simple_object_update/terraform.tfstate b/testing/equivalence-tests/tests/simple_object_update/terraform.tfstate new file mode 100644 index 0000000000..cfee84dac8 --- /dev/null +++ b/testing/equivalence-tests/tests/simple_object_update/terraform.tfstate @@ -0,0 +1,30 @@ +{ + "version": 4, + "terraform_version": "1.3.5", + "serial": 1, + "lineage": "daaaeb37-0157-6c8c-2de9-7687bf0a6040", + "outputs": {}, + "resources": [ + { + "mode": "managed", + "type": "tfcoremock_object", + "name": "object", + "provider": "provider[\"registry.terraform.io/hashicorp/tfcoremock\"]", + "instances": [ + { + "schema_version": 0, + "attributes": { + "id": "00e14fba-4d56-6cc5-b685-633555376e3f", + "object": { + "boolean": true, + "number": 10, + "string": "Hello, world!" + } + }, + "sensitive_attributes": [] + } + ] + } + ], + "check_results": null +} diff --git a/testing/equivalence-tests/tests/variables_and_outputs/main.auto.tfvars b/testing/equivalence-tests/tests/variables_and_outputs/main.auto.tfvars new file mode 100644 index 0000000000..c9ca489f8a --- /dev/null +++ b/testing/equivalence-tests/tests/variables_and_outputs/main.auto.tfvars @@ -0,0 +1,17 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +list_no_default = [ + { + required_attribute = "D92053D5-948A-4E5E-80BF-E53F0DB33EB5", + }, + { + required_attribute = "E6DA6176-49FB-46D6-9ECD-401B3F46A3E5", + optional_attribute = "8AC4B9EE-9E05-4AE0-AA35-6D7636AEA487", + }, + { + required_attribute = "9F9922C4-B426-4648-96AE-804A6F52F778", + optional_attribute = "E68C1EB0-3D3D-4DB0-A41D-0F8C334E181C", + optional_attribute_with_default = "92E855B2-A444-49DF-AFCA-2B5B017451B4", + }, +] diff --git a/testing/equivalence-tests/tests/variables_and_outputs/main.tf b/testing/equivalence-tests/tests/variables_and_outputs/main.tf new file mode 100644 index 0000000000..63accd8b3b --- /dev/null +++ b/testing/equivalence-tests/tests/variables_and_outputs/main.tf @@ -0,0 +1,66 @@ +variable "list_empty_default" { + type = list(object({ + required_attribute = string, + optional_attribute = optional(string), + optional_attribute_with_default = optional(string, "Hello, world!"), + })) + default = [] +} + +variable "list_no_default" { + type = list(object({ + required_attribute = string, + optional_attribute = optional(string), + optional_attribute_with_default = optional(string, "Hello, world!"), + })) +} + +variable "nested_optional_object" { + type = object({ + nested_object = optional(object({ + flag = optional(bool, false) + })) + }) + default = {} +} + +variable "nested_optional_object_with_default" { + type = object({ + nested_object = optional(object({ + flag = optional(bool, false) + })) + }) + default = { + nested_object = {} + } +} + +variable "nested_optional_object_with_embedded_default" { + type = object({ + nested_object = optional(object({ + flag = optional(bool, false) + }), {}) + }) + default = {} +} + + +output "list_empty_default" { + value = var.list_empty_default +} + +output "list_no_default" { + value = var.list_no_default +} + +output "nested_optional_object" { + value = var.nested_optional_object +} + +output "nested_optional_object_with_default" { + value = var.nested_optional_object_with_default +} + +output "nested_optional_object_with_embedded_default" { + value = var.nested_optional_object_with_embedded_default +} diff --git a/testing/equivalence-tests/tests/variables_and_outputs/spec.json b/testing/equivalence-tests/tests/variables_and_outputs/spec.json new file mode 100644 index 0000000000..406d082514 --- /dev/null +++ b/testing/equivalence-tests/tests/variables_and_outputs/spec.json @@ -0,0 +1,5 @@ +{ + "description": "tests a set of basic variables and outputs", + "include_files": [], + "ignore_fields": {} +} diff --git a/tools.go b/tools.go deleted file mode 100644 index 7963816585..0000000000 --- a/tools.go +++ /dev/null @@ -1,14 +0,0 @@ -//go:build tools -// +build tools - -package tools - -// This file tracks some external tools we use during development and release -// processes. These are not used at runtime but having them here allows the -// Go toolchain to see that we need to include them in go.mod and go.sum. - -import ( - _ "github.com/nishanths/exhaustive/cmd/exhaustive" - _ "golang.org/x/tools/cmd/stringer" - _ "honnef.co/go/tools/cmd/staticcheck" -) diff --git a/tools/loggraphdiff/loggraphdiff.go b/tools/loggraphdiff/loggraphdiff.go index 8b25eea7cf..e0609287a3 100644 --- a/tools/loggraphdiff/loggraphdiff.go +++ b/tools/loggraphdiff/loggraphdiff.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // loggraphdiff is a tool for interpreting changes to the Terraform graph // based on the simple graph printing format used in the TF_LOG=trace log // output from Terraform, which looks like this: diff --git a/tools/protobuf-compile/protobuf-compile.go b/tools/protobuf-compile/protobuf-compile.go index b1efe07890..b8cba69172 100644 --- a/tools/protobuf-compile/protobuf-compile.go +++ b/tools/protobuf-compile/protobuf-compile.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // protobuf-compile is a helper tool for running protoc against all of the // .proto files in this repository using specific versions of protoc and // protoc-gen-go, to ensure consistent results across all development @@ -50,11 +53,67 @@ var protocSteps = []protocStep{ "internal/tfplugin6", []string{"--go_out=paths=source_relative,plugins=grpc:.", "./tfplugin6.proto"}, }, + { + "terraform1 (Terraform Core RPC API)", + "internal/rpcapi/terraform1", + []string{"--go_out=paths=source_relative,plugins=grpc:.", "--go_opt=Mterraform1.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1", "./terraform1.proto"}, + }, + { + "terraform1 (Terraform Core RPC API) setup", + "internal/rpcapi/terraform1/setup", + []string{"--go_out=paths=source_relative,plugins=grpc:.", "--go_opt=Msetup.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1/setup", "./setup.proto"}, + }, + { + "terraform1 (Terraform Core RPC API) dependencies", + "internal/rpcapi/terraform1/dependencies", + []string{ + "--go_out=paths=source_relative,plugins=grpc:.", + "--go_opt=Mterraform1.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1", + "--go_opt=Mdependencies.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1/dependencies", + "-I.", + "-I..", + "./dependencies.proto", + }, + }, + { + "terraform1 (Terraform Core RPC API) stacks", + "internal/rpcapi/terraform1/stacks", + []string{ + "--go_out=paths=source_relative,plugins=grpc:.", + "--go_opt=Mterraform1.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1", + "--go_opt=Mstacks.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1/stacks", + "-I.", + "-I..", + "./stacks.proto", + }, + }, + { + "terraform1 (Terraform Core RPC API) packages", + "internal/rpcapi/terraform1/packages", + []string{ + "--go_out=paths=source_relative,plugins=grpc:.", + "--go_opt=Mterraform1.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1", + "--go_opt=Mpackages.proto=github.com/hashicorp/terraform/internal/rpcapi/terraform1/packages", + "-I.", + "-I..", + "./packages.proto", + }, + }, { "tfplan (plan file serialization)", - "internal/plans/internal/planproto", + "internal/plans/planproto", []string{"--go_out=paths=source_relative:.", "planfile.proto"}, }, + { + "tfstackdata1 (Internal data formats for Stack state and plan)", + "internal/stacks/tfstackdata1", + []string{"--go_out=paths=source_relative:.", "--go_opt=Mtfstackdata1.proto=github.com/hashicorp/terraform/internal/stacks/tfstackdata1", "-I.", "-I../../plans/planproto", "./tfstackdata1.proto"}, + }, + { + "cloudproto1 (cloud protocol version 1)", + "internal/cloudplugin/cloudproto1", + []string{"--go_out=paths=source_relative,plugins=grpc:.", "cloudproto1.proto"}, + }, } func main() { diff --git a/tools/terraform-bundle/README.md b/tools/terraform-bundle/README.md index 4df28c5a01..87ff8d672d 100644 --- a/tools/terraform-bundle/README.md +++ b/tools/terraform-bundle/README.md @@ -8,17 +8,17 @@ change to distribute providers separately from Terraform CLI. The Terraform v0.13 series introduced our intended longer-term solutions to this need: -* [Alternative provider installation methods](https://www.terraform.io/docs/cli/config/config-file.html#provider-installation), +* [Alternative provider installation methods](https://developer.hashicorp.com/terraform/cli/config/config-file#provider-installation), including the possibility of running server containing a local mirror of providers you intend to use which Terraform can then use instead of the origin registry. -* [The `terraform providers mirror` command](https://www.terraform.io/docs/cli/commands/providers/mirror.html), +* [The `terraform providers mirror` command](https://developer.hashicorp.com/terraform/cli/commands/providers/mirror), built in to Terraform v0.13.0 and later, can automatically construct a suitable directory structure to serve from a local mirror based on your current Terraform configuration, serving a similar (though not identical) purpose than `terraform-bundle` had served. -For those using Terraform CLI alone, without Terraform Cloud, we recommend +For those using Terraform CLI alone, without HCP Terraform or Terraform Enterprise, we recommend planning to transition to the above features instead of using `terraform-bundle`. @@ -53,7 +53,7 @@ Terraform v0.13's introduction of automatic third-party provider installation. ## Terraform Enterprise Users If you use Terraform Enterprise, the self-hosted distribution of -Terraform Cloud, you can use `terraform-bundle` as described above to build +HCP Terraform, you can use `terraform-bundle` as described above to build custom Terraform packages with bundled provider plugins. For more information, see diff --git a/tools/tools.go b/tools/tools.go index bd54b0e5ec..3e79bfde83 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -1,12 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + //go:build tools // +build tools package tools import ( - _ "github.com/golang/mock/mockgen" _ "github.com/mitchellh/gox" _ "github.com/nishanths/exhaustive" + _ "go.uber.org/mock/mockgen" _ "golang.org/x/tools/cmd/cover" _ "golang.org/x/tools/cmd/stringer" _ "google.golang.org/grpc/cmd/protoc-gen-go-grpc" diff --git a/version.go b/version.go index a543316474..c36135581a 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package main import ( diff --git a/version/VERSION b/version/VERSION new file mode 100644 index 0000000000..a4ab692a5f --- /dev/null +++ b/version/VERSION @@ -0,0 +1 @@ +1.13.0-dev diff --git a/version/dependencies.go b/version/dependencies.go index 155d7eafc3..aed62bea37 100644 --- a/version/dependencies.go +++ b/version/dependencies.go @@ -1,16 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + package version import "runtime/debug" -// See the docs for InterestingDependencyVersions to understand what -// "interesting" is intended to mean here. We should keep this set relatively -// small to avoid bloating the logs too much. +// See the docs for InterestingDependencies to understand what "interesting" is +// intended to mean here. We should keep this set relatively small to avoid +// bloating the logs too much. var interestingDependencies = map[string]struct{}{ - "github.com/hashicorp/hcl/v2": {}, - "github.com/zclconf/go-cty": {}, - "github.com/hashicorp/go-tfe": {}, - "github.com/hashicorp/terraform-config-inspect": {}, - "github.com/hashicorp/terraform-svchost": {}, + "github.com/hashicorp/hcl/v2": {}, + "github.com/zclconf/go-cty": {}, + "github.com/hashicorp/go-tfe": {}, + "github.com/hashicorp/terraform-svchost": {}, } // InterestingDependencies returns the compiled-in module version info for diff --git a/version/version.go b/version/version.go index aca727e93b..f44575c5e8 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,6 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + // The version package provides a location to set the release versions for all // packages to consume, without creating import cycles. // @@ -5,26 +8,45 @@ package version import ( + _ "embed" "fmt" + "strings" version "github.com/hashicorp/go-version" ) -// The main version number that is being run at the moment. -var Version = "1.3.0" +// rawVersion is the current version as a string, as read from the VERSION +// file. This must be a valid semantic version. +// +//go:embed VERSION +var rawVersion string -// 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 -// such as "dev" (in development), "beta", "rc1", etc. -var Prerelease = "dev" +// dev determines whether the -dev prerelease marker will +// be included in version info. It is expected to be set to "no" using +// linker flags when building binaries for release. +var dev string = "yes" -// SemVer is an instance of version.Version. This has the secondary -// benefit of verifying during tests and init time that our version is a -// proper semantic version, which should always be the case. +// The main version number that is being run at the moment, populated from the raw version. +var Version string + +// A pre-release marker for the version, populated using a combination of the raw version +// and the dev flag. +var Prerelease string + +// SemVer is an instance of version.Version representing the main version +// without any prerelease information. var SemVer *version.Version func init() { - SemVer = version.Must(version.NewVersion(Version)) + semVerFull := version.Must(version.NewVersion(strings.TrimSpace(rawVersion))) + SemVer = semVerFull.Core() + Version = SemVer.String() + + if dev == "no" { + Prerelease = semVerFull.Prerelease() + } else { + Prerelease = "dev" + } } // Header is the header name used to send the current terraform version diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 0000000000..182b323ed9 --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package version + +import ( + "regexp" + "strings" + "testing" +) + +// Smoke test to validate that the version file can be read correctly and all exported +// variables include the expected information. +func TestVersion(t *testing.T) { + if match, _ := regexp.MatchString("[^\\d+\\.]", Version); match != false { + t.Fatalf("Version should contain only the main version") + } + + if match, _ := regexp.MatchString("[^a-z\\d]", Prerelease); match != false { + t.Fatalf("Prerelease should contain only letters and numbers") + } + + if SemVer.Prerelease() != "" { + t.Fatalf("SemVer should not include prerelease information") + } + + if !strings.Contains(String(), Prerelease) { + t.Fatalf("Full version string should include prerelease information") + } +} diff --git a/website/Makefile b/website/Makefile new file mode 100644 index 0000000000..fe76940c45 --- /dev/null +++ b/website/Makefile @@ -0,0 +1,60 @@ +###################################################### +# NOTE: This file is managed by the Digital Team's # +# Terraform configuration @ hashicorp/mktg-terraform # +###################################################### + +.DEFAULT_GOAL := website + +# Set the preview mode for the website shell to "developer" or "io" +PREVIEW_MODE ?= developer +REPO ?= terraform + +# Enable setting alternate docker tool, e.g. 'make DOCKER_CMD=podman' +DOCKER_CMD ?= docker + +CURRENT_GIT_BRANCH=$$(git rev-parse --abbrev-ref HEAD) +LOCAL_CONTENT_DIR=../docs +PWD=$$(pwd) + +DOCKER_IMAGE="hashicorp/dev-portal" +DOCKER_IMAGE_LOCAL="dev-portal-local" +DOCKER_RUN_FLAGS=-it \ + --publish "3000:3000" \ + --rm \ + --tty \ + --volume "$(PWD)/docs:/app/docs" \ + --volume "$(PWD)/img:/app/public" \ + --volume "$(PWD)/data:/app/data" \ + --volume "$(PWD)/redirects.js:/app/redirects.js" \ + --volume "next-dir:/app/website-preview/.next" \ + --volume "$(PWD)/.env:/app/.env" \ + --volume "$(PWD)/.env.development:/app/website-preview/.env.development" \ + --volume "$(PWD)/.env.local:/app/website-preview/.env.local" \ + -e "REPO=$(REPO)" \ + -e "PREVIEW_FROM_REPO=$(REPO)" \ + -e "IS_CONTENT_PREVIEW=true" \ + -e "LOCAL_CONTENT_DIR=$(LOCAL_CONTENT_DIR)" \ + -e "CURRENT_GIT_BRANCH=$(CURRENT_GIT_BRANCH)" \ + -e "PREVIEW_MODE=$(PREVIEW_MODE)" + +# Default: run this if working on the website locally to run in watch mode. +.PHONY: website +website: + @echo "==> Downloading latest Docker image..." + @$(DOCKER_CMD) pull $(DOCKER_IMAGE) + @echo "==> Starting website..." + @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE) + +# Use this if you have run `website/build-local` to use the locally built image. +.PHONY: website/local +website/local: + @echo "==> Starting website from local image..." + @$(DOCKER_CMD) run $(DOCKER_RUN_FLAGS) $(DOCKER_IMAGE_LOCAL) + +# Run this to generate a new local Docker image. +.PHONY: website/build-local +website/build-local: + @echo "==> Building local Docker image" + @$(DOCKER_CMD) build https://github.com/hashicorp/dev-portal.git\#main \ + -t $(DOCKER_IMAGE_LOCAL) + diff --git a/website/README.md b/website/README.md index eb9aead414..dc5932ec63 100644 --- a/website/README.md +++ b/website/README.md @@ -11,11 +11,22 @@ You can [submit an issue](https://github.com/hashicorp/terraform/issues/new/choo Click **Edit this page** at the bottom of any Terraform website page to go directly to the associated markdown file in GitHub. +## Validating Content + +Content changes are automatically validated against a set of rules as part of the pull request process. If you want to run these checks locally to validate your content before committing your changes, you can run the following command: + +``` +npm run content-check +``` + +If the validation fails, actionable error messages will be displayed to help you address detected issues. + ## Modifying Sidebar Navigation -You must update the the sidebar navigation when you add or delete documentation .mdx files. If you do not update the navigation, the website deploy preview fails. +You must update the sidebar navigation when you add or delete documentation .mdx files. If you do not update the navigation, the website deploy preview fails. To update the sidebar navigation, you must edit the appropriate `nav-data.json` file. This repository contains the sidebar navigation files for the following documentation sets: + - Terraform Language: [`language-nav-data.json`](https://github.com/hashicorp/terraform/blob/main/website/data/language-nav-data.json) - Terraform CLI: [`cli-nav-data.json`](https://github.com/hashicorp/terraform/blob/main/website/data/cli-nav-data.json) - Introduction to Terraform: [`intro-nav-data.json`](https://github.com/hashicorp/terraform/blob/update-readme/website/data/intro-nav-data.json) @@ -24,7 +35,7 @@ For more details about how to update the sidebar navigation, refer to [Editing N ## Adding Redirects -You must add a redirect when you move, rename, or delete documentation pages. Refer to https://github.com/hashicorp/terraform-website#redirects for details. +You must add a redirect when you move, rename, or delete documentation pages. Refer to https://github.com/hashicorp/terraform-docs-common#redirects for details. ## Previewing Changes @@ -48,7 +59,6 @@ You should preview all of your changes locally before creating a pull request. T 1. Open `http://localhost:3000` in your web browser. While the preview is running, you can edit pages and Next.js automatically rebuilds them. 1. Press `ctrl-C` in your terminal to stop the server and end the preview. - ## Deploying Changes Merging a PR to `main` queues up documentation changes for the next minor product release. Your changes are not immediately available on the website. @@ -57,21 +67,20 @@ The website generates versioned documentation by pointing to the HEAD of the rel ### Backporting -**Important:** Editing old versions (not latest) should be rare. We backport to old versions when there is an egregious error. Egregious errors include inaccuracies that could cause security vulnerabilities or extreme inconvenience for users. +**Important:** Editing old versions (not latest) should be rare. We backport to old versions when there is an egregious error. Egregious errors include inaccuracies that could cause security vulnerabilities or extreme inconvenience for users. -Backporting involves cherry-picking commits to one or more release branches within a docs repository. You can backport (cherry-pick) commits to a version branch by adding the associated backport label to your pull request. For example, if you need to add a security warning to the v1.1 documentation, you must add the `1.1-backport` label. When you merge a pull request with one or more backport labels, GitHub Actions opens a backport PR to cherry-pick your changes to the associated release branches. You must manually merge the backport PR to finish backporting the changes. +Backporting involves cherry-picking commits to one or more release branches within a docs repository. You can backport (cherry-pick) commits to a version branch by adding the associated backport label to your pull request. For example, if you need to add a security warning to the v1.1 documentation, you must add the `1.1-backport` label. When you merge a pull request with one or more backport labels, GitHub Actions opens a backport PR to cherry-pick your changes to the associated release branches. You must manually merge the backport PR to finish backporting the changes. To make your changes available on the latest docs version: 1. Add the backport label for the latest version. Screen Shot 2022-08-09 at 11 06 17 AM - + 1. Merge the pull request. GitHub Actions autogenerates a backport pull request, linked to the original. Screen Shot 2022-08-09 at 11 08 52 AM - -1. Merge the auto-generated backport pull request. +1. Merge the auto-generated backport pull request. You can review and merge your own backport pull request without waiting for another review if the changes in the backport pull request are effectively equivalent to the original. You can make minor adjustments to resolve merge conflicts, but you should not merge a backport PR that contains major content or functionality changes from the original, approved pull request. If you are not sure whether it is okay to merge a backport pull request, post a comment on the original pull request to discuss with the team. diff --git a/website/data/cli-nav-data.json b/website/data/cli-nav-data.json index 7309ffdafb..ab233710c0 100644 --- a/website/data/cli-nav-data.json +++ b/website/data/cli-nav-data.json @@ -33,15 +33,7 @@ { "title": "Overview", "path": "code" }, { "title": "console", "href": "/cli/commands/console" }, { "title": "fmt", "href": "/cli/commands/fmt" }, - { "title": "validate", "href": "/cli/commands/validate" }, - { - "title": "0.13upgrade", - "href": "/cli/commands/0.13upgrade" - }, - { - "title": "0.12upgrade", - "href": "/cli/commands/0.12upgrade" - } + { "title": "validate", "href": "/cli/commands/validate" } ] }, { @@ -62,22 +54,18 @@ ] }, { - "title": "Importing Infrastructure", + "title": "Import Infrastructure", "routes": [ { "title": "Overview", "path": "import" }, + { "title": "Import existing resources", "path": "import/usage" }, { - "title": "import", + "title": "Reference", "href": "/cli/commands/import" - }, - { "title": "Usage Tips", "path": "import/usage" }, - { - "title": "Resource Importability", - "path": "import/importability" } ] }, { - "title": "Manipulating State", + "title": "Manually Update State", "routes": [ { "title": "Overview", "path": "state" }, { @@ -229,20 +217,23 @@ ] }, { - "title": "Using Terraform Cloud", + "title": "Using HCP Terraform", "routes": [ { "title": "Overview", "path": "cloud" }, - { "title": "Terraform Cloud Settings", "path": "cloud/settings" }, - { - "title": "Initializing and Migrating", - "path": "cloud/migrating" - }, + { "title": "Connect to HCP Terraform", "path": "cloud/settings" }, { "title": "Command Line Arguments", "path": "cloud/command-line-arguments" } ] }, + { + "title": "Testing Terraform", + "routes": [ + { "title": "Overview", "path": "test" }, + { "title": "test", "href": "/cli/commands/test"} + ] + }, { "title": "Automating Terraform", "routes": [ @@ -263,7 +254,6 @@ { "title": "apply", "href": "/cli/commands/apply" }, { "title": "console", "href": "/cli/commands/console" }, { "title": "destroy", "href": "/cli/commands/destroy" }, - { "title": "env", "href": "/cli/commands/env" }, { "title": "fmt", "href": "/cli/commands/fmt" }, { "title": "force-unlock", @@ -275,6 +265,7 @@ { "title": "init", "href": "/cli/commands/init" }, { "title": "login", "href": "/cli/commands/login" }, { "title": "logout", "href": "/cli/commands/logout" }, + { "title": "modules", "href": "/cli/commands/modules" }, { "title": "output", "href": "/cli/commands/output" }, { "title": "plan", "href": "/cli/commands/plan" }, { "title": "providers", "href": "/cli/commands/providers" }, @@ -290,10 +281,6 @@ "title": "providers schema", "href": "/cli/commands/providers/schema" }, - { - "title": "push (deprecated)", - "href": "/cli/commands/push" - }, { "title": "refresh", "href": "/cli/commands/refresh" }, { "title": "show", "href": "/cli/commands/show" }, { "title": "state", "href": "/cli/commands/state" }, @@ -320,10 +307,7 @@ "href": "/cli/commands/state/show" }, { "title": "taint", "href": "/cli/commands/taint" }, - { - "title": "test (deprecated)", - "href": "/cli/commands/test" - }, + { "title": "test", "href": "/cli/commands/test" }, { "title": "untaint", "href": "/cli/commands/untaint" }, { "title": "validate", "href": "/cli/commands/validate" }, { "title": "version", "href": "/cli/commands/version" }, @@ -349,11 +333,11 @@ "href": "/cli/commands/workspace/show" }, { - "title": "0.12upgrade", + "title": "0.12upgrade", "href": "/cli/commands/0.12upgrade" }, { - "title": "0.13upgrade", + "title": "0.13upgrade", "href": "/cli/commands/0.13upgrade" } ] @@ -366,7 +350,6 @@ { "title": "apply", "path": "commands/apply" }, { "title": "console", "path": "commands/console" }, { "title": "destroy", "path": "commands/destroy" }, - { "title": "env", "path": "commands/env" }, { "title": "fmt", "path": "commands/fmt" }, { "title": "force-unlock", "path": "commands/force-unlock" }, { "title": "get", "path": "commands/get" }, @@ -375,6 +358,7 @@ { "title": "init", "path": "commands/init" }, { "title": "login", "path": "commands/login" }, { "title": "logout", "path": "commands/logout" }, + { "title": "modules", "path": "commands/modules" }, { "title": "output", "path": "commands/output" }, { "title": "plan", "path": "commands/plan" }, { @@ -386,7 +370,6 @@ { "title": "providers schema", "path": "commands/providers/schema" } ] }, - { "title": "push (deprecated)", "path": "commands/push" }, { "title": "refresh", "path": "commands/refresh" }, { "title": "show", "path": "commands/show" }, { @@ -406,7 +389,7 @@ ] }, { "title": "taint", "path": "commands/taint" }, - { "title": "test (deprecated)", "path": "commands/test", "hidden": true }, + { "title": "test", "path": "commands/test" }, { "title": "untaint", "path": "commands/untaint" }, { "title": "validate", "path": "commands/validate" }, { "title": "version", "path": "commands/version" }, @@ -424,25 +407,16 @@ { "title": "workspace show", "path": "commands/workspace/show" } ] }, - { "title": "0.12upgrade", "path": "commands/0.12upgrade" }, - { "title": "0.13upgrade", "path": "commands/0.13upgrade" } - ] - }, - { - "title": "Installation", - "hidden": true, - "routes": [ { - "title": "APT Packages for Debian and Ubuntu", - "path": "install/apt" + "title": "0.12upgrade", + "path": "commands/0.12upgrade" }, { - "title": "Yum Packages for Red Hat Enterprise Linux, Fedora, and Amazon Linux", - "path": "install/yum" + "title": "0.13upgrade", + "path": "commands/0.13upgrade" } ] }, { "divider": true }, - { "title": "Terraform Internals", "href": "/internals" }, - { "divider": true } + { "title": "Terraform Internals", "href": "/internals" } ] diff --git a/website/data/configuration-nav-data.json b/website/data/configuration-nav-data.json deleted file mode 100644 index b264d9bab9..0000000000 --- a/website/data/configuration-nav-data.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "title": "Expressions Landing Page", - "path": "expressions" - }, - { "title": "Modules Landing Page", "path": "modules" }, - { "title": "Resources Landing Page", "path": "resources" } -] diff --git a/website/data/internals-nav-data.json b/website/data/internals-nav-data.json index 283682218f..f9291d8991 100644 --- a/website/data/internals-nav-data.json +++ b/website/data/internals-nav-data.json @@ -1,5 +1,9 @@ [ { "heading": "Terraform Internals" }, + { + "title": "Overview", + "path": "" + }, { "title": "Credentials Helpers", "path": "credentials-helpers" @@ -21,13 +25,9 @@ "path": "provider-registry-protocol" }, { - "title": "Resource Graph", + "title": "Dependency Graph", "path": "graph" }, - { - "title": "Resource Lifecycle", - "path": "lifecycle" - }, { "title": "Login Protocol", "path": "login-protocol" @@ -44,10 +44,13 @@ "title": "Provider Metadata", "path": "provider-meta" }, + { + "title": "Functions Metadata", + "path": "functions-meta" + }, { "title": "Machine Readable UI", - "path": "machine-readable-ui", - "hidden": true + "path": "machine-readable-ui" }, { "title": "Archiving", @@ -57,6 +60,5 @@ { "divider": true }, { "title": "Terraform CLI", "href": "/cli" }, { "divider": true }, - { "title": "Configuration Language", "href": "/language" }, - { "divider": true } + { "title": "Configuration Language", "href": "/language" } ] diff --git a/website/data/intro-nav-data.json b/website/data/intro-nav-data.json index 120e29a1aa..4d0da480af 100644 --- a/website/data/intro-nav-data.json +++ b/website/data/intro-nav-data.json @@ -8,6 +8,16 @@ }, { "title": "Terraform Editions", "path": "terraform-editions" }, { "title": "The Core Terraform Workflow", "path": "core-workflow" }, + { + "title": "Phases of Terraform Adoption", + "routes": [ + {"title": "Overview", "path": "phases"}, + {"title": "Adopt", "path": "phases/adopt"}, + {"title": "Collaborate", "path": "phases/collaborate"}, + {"title": "Scale", "path": "phases/scale"}, + {"title": "Govern", "path": "phases/govern"} + ] + }, { "title": "Terraform vs. Alternatives", "routes": [ diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index 876ee26733..7a4164f687 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -6,20 +6,54 @@ "path": "attr-as-blocks", "hidden": true }, + { + "title": "Style Guide", + "path": "style" + }, { - "title": "Terraform v1.0 Compatibility Promises", - "path": "v1-compatibility-promises", - "hidden": true + "title": "Stacks", + "badge": { + "text": "BETA", + "type": "outlined", + "color": "neutral" + }, + "routes": [ + { "title": "Overview", "path": "stacks" }, + { "title": "Use cases", "path": "stacks/use-cases" }, + { "title": "Design a Stack", "path": "stacks/design" }, + { + "title": "Create a Stack", + "routes": [ + { "title": "Define configuration", "path": "stacks/create/config" }, + { "title": "Declare providers", "path": "stacks/create/declare-providers" } + ] + }, + { + "title": "Define deployments", + "routes": [ + { "title": "Define configuration", "path": "stacks/deploy/config" }, + { "title": "Set conditions for deployment plans", "path": "stacks/deploy/conditions" }, + { "title": "Authenticate a Stack", "path": "stacks/deploy/authenticate" }, + { "title": "Pass data from one Stack to another", "path": "stacks/deploy/pass-data" } + ] + }, + { + "title": "Reference", + "routes": [ + { "title": "Stack configuration file", "path": "stacks/reference/tfstack" }, + { "title": "Deployment configuration file", "path": "stacks/reference/tfdeploy" }, + { "title": "Tfstacks CLI", "path": "stacks/reference/tfstacks-cli" } + ] + } + ] }, { "title": "Files and Directories", "routes": [ { "title": "Overview", "path": "files" }, { "title": "Override Files", "path": "files/override" }, - { - "title": "Dependency Lock File", - "path": "files/dependency-lock" - } + { "title": "Dependency Lock File", "path": "files/dependency-lock" }, + { "title": "Test Files", "path": "files/tests" } ] }, { @@ -33,8 +67,7 @@ { "title": "JSON Configuration Syntax", "path": "syntax/json" - }, - { "title": "Style Conventions", "path": "syntax/style" } + } ] }, { @@ -97,23 +130,27 @@ { "title": "remote-exec", "path": "resources/provisioners/remote-exec" - }, - { "divider": true }, + } + ] + }, + { + "title": "The terraform_data Resource Type", + "path": "resources/terraform-data" + }, + { + "title": "Ephemerality in Resources", + "routes": [ { - "title": "chef", - "path": "resources/provisioners/chef" + "title": "Overview", + "path": "resources/ephemeral" }, { - "title": "habitat", - "path": "resources/provisioners/habitat" + "title": "Ephemeral block", + "path": "resources/ephemeral/reference" }, { - "title": "puppet", - "path": "resources/provisioners/puppet" - }, - { - "title": "salt-masterless", - "path": "resources/provisioners/salt-masterless" + "title": "Use write-only arguments", + "path": "resources/ephemeral/write-only" } ] } @@ -231,14 +268,85 @@ "path": "modules/develop/refactoring" } ] + } + ] + }, + { + "title": "Moved block", + "path": "moved" + }, + { + "title": "Terraform block", + "path": "terraform" + }, + { + "title": "Backend block", + "routes": [ + { + "title": "Overview", + "path": "backend" }, { - "title": "Module Testing Experiment", - "path": "modules/testing-experiment", - "hidden": true + "title": "local", + "path": "backend/local" + }, + { + "title": "remote", + "path": "backend/remote" + }, + { + "title": "azurerm", + "path": "backend/azurerm" + }, + { + "title": "consul", + "path": "backend/consul" + }, + { + "title": "cos", + "path": "backend/cos" + }, + { + "title": "gcs", + "path": "backend/gcs" + }, + { + "title": "http", + "path": "backend/http" + }, + { + "title": "Kubernetes", + "path": "backend/kubernetes" + }, + { + "title": "oci", + "path": "backend/oci" + }, + { + "title": "oss", + "path": "backend/oss" + }, + { + "title": "pg", + "path": "backend/pg" + }, + { + "title": "s3", + "path": "backend/s3" } ] }, + { "title": "Checks", "path": "checks" }, + { + "title": "Import", + "routes": [ + { "title": "Overview", "path": "import" }, + { + "title": "Generating Configuration", + "path": "import/generating-configuration" + } + ] + }, { "title": "Expressions", "routes": [ @@ -272,7 +380,7 @@ "path": "expressions/dynamic-blocks" }, { - "title": "Custom Condition Checks", + "title": "Custom Conditions", "path": "expressions/custom-conditions" }, { @@ -360,6 +468,10 @@ "title": "startswith", "href": "/language/functions/startswith" }, + { + "title": "strcontains", + "href": "/language/functions/strcontains" + }, { "title": "strrev", "href": "/language/functions/strrev" @@ -368,6 +480,10 @@ "title": "substr", "href": "/language/functions/substr" }, + { + "title": "templatestring", + "href": "/language/functions/templatestring" + }, { "title": "title", "href": "/language/functions/title" @@ -597,6 +713,10 @@ "title": "formatdate", "href": "/language/functions/formatdate" }, + { + "title": "plantimestamp", + "href": "/language/functions/plantimestamp" + }, { "title": "timeadd", "href": "/language/functions/timeadd" @@ -696,6 +816,11 @@ "title": "Type Conversion Functions", "routes": [ { "title": "can", "href": "/language/functions/can" }, + { "title": "ephemeralasnull", "href": "/language/functions/ephemeralasnull" }, + { + "title": "issensitive", + "href": "/language/functions/issensitive" + }, { "title": "nonsensitive", "href": "/language/functions/nonsensitive" @@ -732,6 +857,15 @@ { "title": "type", "href": "/language/functions/type" } ] }, + { + "title": "Terraform-specific Functions", + "routes": [ + { "title": "provider::terraform::encode_tfvars", "href": "/language/functions/terraform-encode_tfvars" }, + { "title": "provider::terraform::decode_tfvars", "href": "/language/functions/terraform-decode_tfvars" }, + { "title": "provider::terraform::encode_expr", "href": "/language/functions/terraform-encode_expr" }, + { "title": "terraform.applying", "href": "/language/functions/terraform-applying" } + ] + }, { "title": "abs", "path": "functions/abs", "hidden": true }, { "title": "abspath", "path": "functions/abspath", "hidden": true }, { "title": "alltrue", "path": "functions/alltrue", "hidden": true }, @@ -789,6 +923,7 @@ { "title": "distinct", "path": "functions/distinct", "hidden": true }, { "title": "element", "path": "functions/element", "hidden": true }, { "title": "endswith", "path": "functions/endswith", "hidden": true }, + { "title": "ephemeralasnull", "path": "functions/ephemeralasnull", "hidden": true }, { "title": "file", "path": "functions/file", "hidden": true }, { "title": "filebase64", "path": "functions/filebase64", "hidden": true }, { @@ -814,6 +949,11 @@ { "title": "formatlist", "path": "functions/formatlist", "hidden": true }, { "title": "indent", "path": "functions/indent", "hidden": true }, { "title": "index", "path": "functions/index_function", "hidden": true }, + { + "title": "issensitive", + "path": "functions/issensitive", + "hidden": true + }, { "title": "join", "path": "functions/join", "hidden": true }, { "title": "jsondecode", "path": "functions/jsondecode", "hidden": true }, { "title": "jsonencode", "path": "functions/jsonencode", "hidden": true }, @@ -837,6 +977,7 @@ { "title": "one", "path": "functions/one", "hidden": true }, { "title": "parseint", "path": "functions/parseint", "hidden": true }, { "title": "pathexpand", "path": "functions/pathexpand", "hidden": true }, + { "title": "plantimestamp", "path": "functions/plantimestamp", "hidden": true }, { "title": "pow", "path": "functions/pow", "hidden": true }, { "title": "range", "path": "functions/range", "hidden": true }, { "title": "regex", "path": "functions/regex", "hidden": true }, @@ -865,6 +1006,7 @@ { "title": "sort", "path": "functions/sort", "hidden": true }, { "title": "split", "path": "functions/split", "hidden": true }, { "title": "startswith", "path": "functions/startswith", "hidden": true }, + { "title": "strcontains", "path": "functions/strcontains", "hidden": true}, { "title": "strrev", "path": "functions/strrev", "hidden": true }, { "title": "substr", "path": "functions/substr", "hidden": true }, { "title": "sum", "path": "functions/sum", "hidden": true }, @@ -873,6 +1015,19 @@ "path": "functions/templatefile", "hidden": true }, + { + "title": "templatestring", + "path": "functions/templatestring", + "hidden": true + }, + { + "title": "terraform.applying", + "path": "functions/terraform-applying", + "hidden": true + }, + { "title": "terraform-encode_tfvars", "path": "functions/terraform-encode_tfvars", "hidden": true }, + { "title": "terraform-decode_tfvars", "path": "functions/terraform-decode_tfvars", "hidden": true }, + { "title": "terraform-encode_expr", "path": "functions/terraform-encode_expr", "hidden": true }, { "title": "textdecodebase64", "path": "functions/textdecodebase64", @@ -910,171 +1065,6 @@ { "title": "zipmap", "path": "functions/zipmap", "hidden": true } ] }, - { - "title": "Terraform Settings", - "routes": [ - { "title": "Overview", "path": "settings" }, - { "title": "Terraform Cloud", "path": "settings/terraform-cloud" }, - { - "title": "Backends", - "routes": [ - { - "title": "Backend Configuration", - "path": "settings/backends/configuration" - }, - { - "title": "Available Backends", - "routes": [ - { - "title": "local", - "href": "/language/settings/backends/local" - }, - { - "title": "remote", - "href": "/language/settings/backends/remote" - }, - { - "title": "artifactory", - "href": "/language/settings/backends/artifactory" - }, - { - "title": "azurerm", - "href": "/language/settings/backends/azurerm" - }, - { - "title": "consul", - "href": "/language/settings/backends/consul" - }, - { - "title": "cos", - "href": "/language/settings/backends/cos" - }, - { - "title": "etcd", - "href": "/language/settings/backends/etcd" - }, - { - "title": "etcdv3", - "href": "/language/settings/backends/etcdv3" - }, - { - "title": "gcs", - "href": "/language/settings/backends/gcs" - }, - { - "title": "http", - "href": "/language/settings/backends/http" - }, - { - "title": "Kubernetes", - "href": "/language/settings/backends/kubernetes" - }, - { - "title": "manta", - "href": "/language/settings/backends/manta" - }, - { - "title": "oss", - "href": "/language/settings/backends/oss" - }, - { - "title": "pg", - "href": "/language/settings/backends/pg" - }, - { - "title": "s3", - "href": "/language/settings/backends/s3" - }, - { - "title": "swift", - "href": "/language/settings/backends/swift" - } - ] - }, - { - "title": "local", - "hidden": true, - "path": "settings/backends/local" - }, - { - "title": "remote", - "hidden": true, - "path": "settings/backends/remote" - }, - { - "title": "artifactory", - "hidden": true, - "path": "settings/backends/artifactory" - }, - { - "title": "azurerm", - "hidden": true, - "path": "settings/backends/azurerm" - }, - { - "title": "consul", - "hidden": true, - "path": "settings/backends/consul" - }, - { - "title": "cos", - "hidden": true, - "path": "settings/backends/cos" - }, - { - "title": "etcd", - "hidden": true, - "path": "settings/backends/etcd" - }, - { - "title": "etcdv3", - "hidden": true, - "path": "settings/backends/etcdv3" - }, - { - "title": "gcs", - "hidden": true, - "path": "settings/backends/gcs" - }, - { - "title": "http", - "hidden": true, - "path": "settings/backends/http" - }, - { - "title": "Kubernetes", - "hidden": true, - "path": "settings/backends/kubernetes" - }, - { - "title": "manta", - "hidden": true, - "path": "settings/backends/manta" - }, - { - "title": "oss", - "hidden": true, - "path": "settings/backends/oss" - }, - { - "title": "pg", - "hidden": true, - "path": "settings/backends/pg" - }, - { - "title": "s3", - "hidden": true, - "path": "settings/backends/s3" - }, - { - "title": "swift", - "hidden": true, - "path": "settings/backends/swift" - } - ] - } - ] - }, { "title": "State", "routes": [ @@ -1092,6 +1082,10 @@ "title": "Import Existing Resources", "path": "state/import" }, + { + "title": "Refactor state", + "path": "state/refactor" + }, { "title": "Locking", "path": "state/locking" }, { "title": "Workspaces", "path": "state/workspaces" }, { "title": "Remote State", "path": "state/remote" }, @@ -1102,64 +1096,26 @@ ] }, { - "title": "Upgrade Guides", + "title": "Tests", "routes": [ - { "title": "Overview", "path": "upgrade-guides" }, { - "title": "Upgrading to Terraform v1.2", - "path": "upgrade-guides/1-2" + "title": "Overview", + "path": "tests" }, { - "title": "Upgrading to Terraform v1.1", - "path": "upgrade-guides/1-1" - }, - { - "title": "Upgrading to Terraform v1.0", - "path": "upgrade-guides/1-0" - }, - { - "title": "v1.0 Compatibility Promises", - "href": "/language/v1-compatibility-promises" - }, - { - "title": "Upgrading to Terraform v0.15", - "path": "upgrade-guides/0-15" - }, - { - "title": "Upgrading to Terraform v0.14", - "path": "upgrade-guides/0-14" - }, - { - "title": "Upgrading to Terraform v0.13", - "path": "upgrade-guides/0-13" - }, - { - "title": "Upgrading to Terraform v0.12", - "path": "upgrade-guides/0-12" - }, - { - "title": "Upgrading to Terraform v0.11", - "path": "upgrade-guides/0-11" - }, - { - "title": "Upgrading to Terraform v0.10", - "path": "upgrade-guides/0-10" - }, - { - "title": "Upgrading to Terraform v0.9", - "path": "upgrade-guides/0-9" - }, - { - "title": "Upgrading to Terraform v0.8", - "path": "upgrade-guides/0-8" - }, - { - "title": "Upgrading to Terraform v0.7", - "path": "upgrade-guides/0-7" + "title": "Mocks", + "path": "tests/mocking" } ] }, + { + "title": "Upgrading to Terraform v1.10", + "path": "upgrade-guides" + }, + { + "title": "v1.x Compatibility Promises", + "path": "v1-compatibility-promises" + }, { "divider": true }, - { "title": "Terraform Internals", "href": "/internals" }, - { "divider": true } + { "title": "Terraform Internals", "href": "/internals" } ] diff --git a/website/docs/cli/auth/index.mdx b/website/docs/cli/auth/index.mdx index 0b2247365f..dc783d43a7 100644 --- a/website/docs/cli/auth/index.mdx +++ b/website/docs/cli/auth/index.mdx @@ -1,31 +1,33 @@ --- -page_title: Authentication - Terraform CLI +page_title: Get an API token for HCP Terraform or Terraform Enterprise description: >- - Documentation about the login and logout commands that help automate getting - an API token for your Terraform Cloud account. + Use the `terraform login` and `terraform logout` commands get + an API token for your HCP Terraform or Terraform Enterprise account. --- -# CLI Authentication +# Get an API token for HCP Terraform and Terraform Enterprise -> **Hands-on:** Try the [Authenticate the CLI with Terraform Cloud](https://learn.hashicorp.com/tutorials/terraform/cloud-login?in=terraform/cloud&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +This topic describes how to use the `terraform login` and `terraform logout` to authenticate with HCP Terraform and Terraform Enterprise. -[Terraform Cloud](/cloud) and -[Terraform Enterprise](/enterprise) are platforms that perform +> **Hands-on:** Try the [Authenticate the CLI with HCP Terraform](/terraform/tutorials/cloud/cloud-login?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. + +## Overview + +[HCP Terraform](https://cloud.hashicorp.com/products/terraform) and +[Terraform Enterprise](/terraform/enterprise) are platforms that perform Terraform runs to provision infrastructure, offering a collaboration-focused -environment that makes it easier for teams to use Terraform together. (For -expediency, the content below refers to both products as "Terraform Cloud.") +environment that makes it easier for teams to use Terraform together. -Terraform CLI integrates with Terraform Cloud in several ways — it can be a -front-end for [CLI-driven runs](/cloud-docs/run/cli) in Terraform Cloud, -and can also use Terraform Cloud as a state backend and a private module -registry. All of these integrations require you to authenticate Terraform CLI -with your Terraform Cloud account. +You can integrate the Terraform CLI with HCP Terraform and Terraform Enterprise in the following ways: -The best way to handle CLI authentication is with the `login` and `logout` -commands, which help automate the process of getting an API token for your -Terraform Cloud user account. +- Use the Terraform CLI as a front-end for [CLI-driven runs](/terraform/cloud-docs/run/cli) in HCP Terraform +- Use HCP Terraform or Terraform Enterprise as a state backend and a private module registry. -For details, see: +These integrations require you to authenticate the Terraform CLI +with your HCP Terraform account. -- [The `terraform login` command](/cli/commands/login) -- [The `terraform logout` command](/cli/commands/logout) +## Authentication + +Run the `terraform login` command to generate an API token for your HCP Terraform user account. Refer to the [`terraform login` command](/terraform/cli/commands/login) reference documentation for details. + +Run the `terraform logout` command to end your HCP Terraform or Terraform Enterprise session. Refer to the [`terraform logout` command](/terraform/cli/commands/logout) reference documentation for details. diff --git a/website/docs/cli/cloud/command-line-arguments.mdx b/website/docs/cli/cloud/command-line-arguments.mdx index 81897cfe66..8483a8adf2 100644 --- a/website/docs/cli/cloud/command-line-arguments.mdx +++ b/website/docs/cli/cloud/command-line-arguments.mdx @@ -1,9 +1,9 @@ --- -page_title: Command Line Arguments -description: Command Line Arguments +page_title: -ignore-remote-version reference +description: Use the -ignore-remote-version flag to override CLI-driven commands for HCP Terraform runs. --- -# Command Line Arguments +# `-ignore-remote-version` reference When your configuration includes a `cloud` block, commands that make local modifications to Terraform state and then push them back up to the remote workspace @@ -20,5 +20,5 @@ accept the following option to modify that behavior: remote execution environment cannot decode. We recommend against using this option unless absolutely necessary. Overriding this check can result - in a Terraform Cloud workspace that is no longer able to complete remote operations with the currently + in an HCP Terraform workspace that is no longer able to complete remote operations with the currently selected version of Terraform. diff --git a/website/docs/cli/cloud/index.mdx b/website/docs/cli/cloud/index.mdx index 0ab3a80f20..a8c93cb532 100644 --- a/website/docs/cli/cloud/index.mdx +++ b/website/docs/cli/cloud/index.mdx @@ -1,26 +1,25 @@ --- -page_title: Using Terraform Cloud - Terraform CLI +page_title: Use HCP Terraform or Terraform Enterprise with the Terraform CLI +description: >- + Learn how to use HCP Terraform and Terraform Enterprise on the command line with the Terraform CLI. --- -# Using Terraform Cloud with Terraform CLI +# Use HCP Terraform with the Terraform CLI -The Terraform CLI integration with Terraform Cloud lets you use Terraform Cloud and Terraform Enterprise on the command line. In the documentation Terraform Cloud instructions also apply to Terraform Enterprise, except where explicitly stated. +The Terraform CLI integration with HCP Terraform lets you use HCP Terraform and Terraform Enterprise on the command line. In the documentation HCP Terraform instructions also apply to Terraform Enterprise, except where explicitly stated. -Using Terraform Cloud through the command line is called the [CLI-driven run workflow](/cloud-docs/run/cli). When you use the CLI workflow, operations like `terraform plan` or `terraform apply` are remotely executed in Terraform Cloud's run environment by default, with log output streaming to the local terminal. This lets you use Terraform Cloud features within the familiar Terraform CLI workflow, including variables encrypted at rest in a Terraform Cloud workspace, cost estimates, and policy checking. +Using HCP Terraform through the command line is called the [CLI-driven run workflow](/terraform/cloud-docs/run/cli). When you use the CLI workflow, operations like `terraform plan` or `terraform apply` are remotely executed in HCP Terraform's run environment by default, with log output streaming to the local terminal. This lets you use HCP Terraform features within the familiar Terraform CLI workflow, including variables encrypted at rest in an HCP Terraform workspace, cost estimates, and policy checking. -> **Hands On:** Try the [Migrate State to Terraform Cloud](https://learn.hashicorp.com/tutorials/terraform/cloud-migrate) tutorial on HashiCorp Learn. +> **Hands On:** Try the [Migrate State to HCP Terraform](/terraform/tutorials/cloud/cloud-migrate) tutorial. -Workspaces can also be configured for local execution, in which case only state is stored in -Terraform Cloud. In this mode, Terraform Cloud behaves just like a standard state backend. +Workspaces can also be configured for local execution, in which case HCP Terraform only stores state. In this mode, HCP Terraform behaves just like a standard state backend. --> **Note:** The CLI integration is available in Terraform 1.1.0 and later, and Terraform Enterprise 202201-1 and later. Previous versions can use the [`remote` backend](/language/settings/backends/remote). Refer to [Migrating from the remote -backend](/cli/cloud/migrating) for details about switching to the CLI integration. +-> **Note:** The CLI integration is available in Terraform 1.1.0 and later, and Terraform Enterprise 202201-1 and later. Previous versions can use the [`remote` backend](/terraform/language/backend/remote). Refer to [Migrating from the remote +backend](/terraform/cli/cloud/settings#migrate-state-data) for details about switching to the CLI integration. ## Documentation Summary -- [Terraform Cloud Settings](/cli/cloud/settings) documents the `cloud` block that you must add to your configuration to enable Terraform Cloud support. -- [Initializing and Migrating](/cli/cloud/migrating) describes - how to start using Terraform Cloud with a working directory that already has state data. -- [Command Line Arguments](/cli/cloud/command-line-arguments) lists the Terraform command flags that are specific to using Terraform with Terraform Cloud. +- [Connect to HCP Terraform](/terraform/cli/cloud/settings) documents the `cloud` block that you must add to your configuration to enable HCP Terraform support. +- [Command Line Arguments](/terraform/cli/cloud/command-line-arguments) lists the Terraform command flags that are specific to using Terraform with HCP Terraform. -Refer to the [CLI-driven Run Workflow](/cloud-docs/run/cli) for more details about how to use Terraform Cloud from the command line. +Refer to the [CLI-driven Run Workflow](/terraform/cloud-docs/run/cli) for more details about how to use HCP Terraform from the command line. diff --git a/website/docs/cli/cloud/migrating.mdx b/website/docs/cli/cloud/migrating.mdx deleted file mode 100644 index 7aa5572c37..0000000000 --- a/website/docs/cli/cloud/migrating.mdx +++ /dev/null @@ -1,84 +0,0 @@ ---- -page_title: Initializing and Migrating to Terraform Cloud - Terraform CLI ---- - -# Initializing and Migrating - -After [configuring Terraform Cloud settings](/cli/cloud/settings) for a working directory, you must run `terraform init` to finish setting up. If the working directory has no existing Terraform state, you can start using Terraform with Terraform Cloud right away. Refer to [CLI-driven run workflow](/cloud-docs/run/cli) for more details. - -When you run `terraform init` in the following scenarios, Terraform will ask you to choose whether or not to migrate state from any existing workspaces. - -1. [**Migrating from local state or state backends:**](#migrating-from-local-state-or-state-backends) If the working directory already has state data in one or more workspaces, Terraform will ask if you would like to migrate that state to new Terraform Cloud workspaces. - -1. [**Migrating from the `remote` backend:**](#migrating-from-the-remote-backend) If the working directory was already connected to Terraform Cloud with the `remote` backend, Terraform can continue using the same Terraform Cloud workspaces. You will need to switch the `remote` backend block to the `cloud` block. - -## Migrating from Local State or State Backends - -> **Hands On:** Try the [Migrate State to Terraform Cloud](https://learn.hashicorp.com/tutorials/terraform/cloud-migrate) tutorial on HashiCorp Learn. - -If the working directory already has state data available (using either local state or a [state -backend](/language/settings/backends)), Terraform will ask your approval to migrate -that state to Terraform Cloud. You will need permission to manage workspaces in the destination Terraform Cloud organization. This process is interactive and self-documenting, and resembles -moving between state backends. - -Terraform may also prompt you to rename your workspaces during the migration, to either give a name to -the unnamed `default` workspace (Terraform Cloud requires all workspaces to have a name) or give -your workspace names more contextual information. Unlike Terraform CLI-only workspaces, which represent -multiple environments associated with the same configuration (e.g. production, staging, development), -Terraform Cloud workspaces can represent totally independent configurations, and must have unique names within the Terraform Cloud organization. - -Because of this, Terraform will prompt you to rename the working directory's workspaces -according to a pattern relative to their existing names. This can indicate the fact that these specific workspaces share configuration. A typical strategy is -`--` (e.g., `networking-prod-us-east`, -`networking-staging-us-east`). Refer to [Workspace -Naming](/cloud-docs/workspaces/naming) in the Terraform Cloud documentation for more detail. - -## Migrating from the `remote` Backend - -If the working directory was already connected to Terraform Cloud with the `remote` backend, Terraform can continue using the same Terraform Cloud workspaces. The local names shown for those workspaces will change to match their remote names. - -The [`remote` backend](/language/settings/backends/remote) was the primary implementation of Terraform Cloud's [CLI-driven run workflow](/cloud-docs/run/cli) for Terraform versions 0.11.13 through 1.0.x. We recommend using the native `cloud` integration for Terraform versions 1.1 or later, as it provides an improved user experience and various enhancements. - -### Block Replacement - -When switching from the `remote` backend to a `cloud` block, Terraform will continue using the same -set of Terraform Cloud workspaces. Replace your `backend "remote"` block with an equivalent `cloud` -block. - -#### Single Workspace - -If you were using a single workspace with the `name` argument, change the block -label to `cloud`. - -```diff -terraform { -- backend "remote" { -+ cloud { - organization = "my-org" - - workspaces { - name = "my-app-prod" - } - } - } -``` - -#### Multiple Workspaces - -If you were using multiple workspaces with the `prefix` argument, replace it with a `cloud` block that uses the `tags` argument. You may specify any number of tags to distinguish the workspaces for your working directory, but a good starting point may be to use whatever the prefix was before. - -The tags you configure do not need to be present on the existing workspaces. When you initialize, Terraform will add the specified tags to the workspaces if necessary. - -```diff -terraform { -- backend "remote" { -+ cloud { - organization = "my-org" - - workspaces { -- prefix = "my-app-" -+ tags = ["app:mine"] - } - } - } -``` diff --git a/website/docs/cli/cloud/settings.mdx b/website/docs/cli/cloud/settings.mdx index fdf1d0530f..24f3229333 100644 --- a/website/docs/cli/cloud/settings.mdx +++ b/website/docs/cli/cloud/settings.mdx @@ -1,32 +1,53 @@ --- -page_title: Terraform Cloud Settings - Terraform CLI +page_title: Connect to HCP Terraform description: >- - Configure the Terraform Cloud CLI integration. + Learn how to configure the Terraform CLI to connect to HCP Terraform. --- -# Terraform Cloud Settings +# Connect to HCP Terraform -Terraform CLI can integrate with Terraform Cloud, acting as a client for Terraform Cloud's -[CLI-driven run workflow](/cloud-docs/run/cli). +This topic describes how to connect the Terraform CLI to HCP Terraform. Integrating the CLI with HCP Terraform enables the CLI to act as a client for CLI-drive workflows. Refer to [CLI-driven Run Workflow](/terraform/cloud-docs/run/cli) for additional information. -> **Hands On:** Try the [Migrate State to Terraform Cloud](https://learn.hashicorp.com/tutorials/terraform/cloud-migrate) tutorial on HashiCorp Learn. +> **Hands On:** Complete the [Migrate State to HCP Terraform](/terraform/tutorials/cloud/cloud-migrate) tutorial to learn more about integrating the CLI with HCP Terraform. -You must configure the following settings to use Terraform Cloud for a particular working directory: +## Overview -- Provide credentials to access Terraform Cloud, preferably by using the - [`terraform login`](/cli/commands/login) command. -- Add a `cloud` block to the directory's Terraform configuration, to specify - which organization and workspace(s) to use. -- Optionally, use a `.terraformignore` file to specify files that shouldn't be - uploaded with the Terraform configuration when running plans and applies. +Connecting the Terraform CLI to HCP Terraform links the working directory that contains your Terraform configurations to one or more HCP Terraform workspaces. This allows team members with access to the workspace to provision and manage infrastructure using HCP Terraform. Additionally, HCP Terraform manages state data so that you do not have to maintain remote state objects. Refer to the following topics for additional information: -After adding or changing a `cloud` block, you must run `terraform init`. +- [State overview](/terraform/language/state) in the Terraform configuration language reference. +- [Terraform State in HCP Terraform](/terraform/cloud-docs/workspaces/state) in the HCP Terraform documentation. -## The `cloud` Block +Complete the following steps to connect to HCP Terraform: -The `cloud` block is a nested block within the top-level `terraform` settings -block. It specifies which Terraform Cloud workspaces to use for the current -working directory. +1. Provide credentials to HCP Terraform. +1. Define connection settings in your Terraform configuration. +1. Initialize the working directory. +1. Migrate state data. This step is optional. + +## Requirements + +You must have a user profile in HCP Terraform with permissions to create a workspace. Refer to [Workspace Permissions](/terraform/cloud-docs/users-teams-organizations/permissions) in the HCP Terraform documentation for additional information. + +## Provide credentials + +You must provide credentials to access HCP Terraform. We recommend using the +[`terraform login`](/terraform/cli/commands/login) command to log into Terraform. You can also provide a user token in the Terraform configuration. Refer to the [`token`](/terraform/language/terraform#terraform-cloud-token) attribute in the Terraform configuration reference for additional information. + + +## Define connection settings + +Add a `cloud` block to your Terraform configuration and configure the connection settings to link the working directory to an HCP Terraform workspace. The `cloud` block is a member of the `terraform` block. Refer to the [`terraform` block reference](/terraform/language/terraform) for additional information. + +Specify the following settings in the `cloud` block: + +- `organization`: Specifies the name of an HCP Terraform organization to connect to. +- `workspaces.tags`: Specifies either a map of tag strings or a list of key-only string tags (legacy style). Terraform links the working directory to existing workspaces in the organization that have matching tags. If there are no existing workspaces with matching tags, the Terraform CLI prompts you to create a new workspace that applies the tags you specify in this field when you initialize the configuration. +- `workspaces.name`: You can specify the name of an existing workspace to associate with the Terraform configuration instead of using tags. If you configure the `name`, you cannot use the `tags` configuration. +- `workspaces.project`: You can specify the name of an existing project. Terraform associates the configuration with workspaces in the project that match the `name` or `tags`. + +Refer to the [`cloud` block reference](/terraform/language/terraform#terraform-cloud) for details about configuring the `cloud` block. + +In the following example, the configuration links the working directory to all workspaces tagged with `networking` and `source:cli` in the `networking-development` project: ```hcl terraform { @@ -35,87 +56,103 @@ terraform { hostname = "app.terraform.io" # Optional; defaults to app.terraform.io workspaces { - tags = ["networking", "source:cli"] + project = "networking-development" + + tags = { + layer = "networking" + source = "cli" + } } } } ``` -The `cloud` block also has some special restrictions: +## Initialize the working directory -- A configuration can only provide one `cloud` block. -- A `cloud` block cannot be used with [state backends](/language/settings/backends). - A configuration can use one or the other, but not both. -- A `cloud` block cannot refer to named values (like input variables, locals, or - data source attributes). +After adding or changing a `cloud` block, run the [`terraform init` command](/terraform/cli/commands/init) to complete the set up. -The `cloud` block only affects Terraform CLI's behavior. When Terraform Cloud uses a configuration -that contains a cloud block - for example, when a workspace is configured to use a VCS provider -directly - it ignores the block and behaves according to its own workspace settings. +By default, Terraform uploads a copy of Terraform configurations stored in the working directory when you run the `terraform plan` or `terraform apply` command, but you can add a `.terraformignore` file to the directory and specify files that you do not want to upload to HCP Terraform. Refer to [Exclude files](#exclude-files) for additional information. -### Arguments +If the working directory does not have an existing Terraform state file, you can immediately start using Terraform with HCP Terraform. Refer to [CLI-driven run workflow](/terraform/cloud-docs/run/cli) for additional information. -The `cloud` block supports the following configuration arguments: +If the directory has an existing state file associated with a `backend` configuration, Terraform prompts you to migrate state from any existing workspaces. Refer to [Migrate state data](#migrate-state-data) for next steps. -- `organization` - (Required) The name of the organization containing the - workspace(s) the current configuration should use. +## Migrate state data -- `workspaces` - (Required) A nested block that specifies which remote Terraform Cloud workspaces to - use for the current configuration. The `workspaces` block must contain **exactly one** of the - following arguments, each denoting a strategy for how workspaces should be mapped: +Complete the data migration process when prompted according to one of the following scenarios: - - `tags` - (Optional) A set of Terraform Cloud workspace tags. You will be able to use - this working directory with any workspaces that have all of the specified tags, - and can use [the `terraform workspace` commands](/cli/workspaces) - to switch between them or create new workspaces. New workspaces will automatically have - the specified tags. This option conflicts with `name`. +State is stored in a [local or state backend](#local-and-state-backend-migration): If the working directory already has state data in one or more workspaces, Terraform prompts you to migrate the state to new HCP Terraform workspaces. +State is stored in a [remote backend](#remote-backend-migration): If the working directory is already connected to HCP Terraform with the remote backend, Terraform can continue using the same HCP Terraform workspaces. Change the `backend "remote"` configuration to a `cloud` block in this scenario. - - `name` - (Optional) The name of a single Terraform Cloud workspace. You will - only be able to use the workspace specified in the configuration with this working - directory, and cannot manage workspaces from the CLI (e.g. `terraform workspace select` or - `terraform workspace new`). This option conflicts with `tags`. +### Migrate local state -- `hostname` - (Optional) The hostname of a Terraform Enterprise installation, if using Terraform - Enterprise. Defaults to Terraform Cloud (app.terraform.io). +Run the `terraform init` command and follow the CLI prompts to migrate state data stored in a local or state backend. -- `token` - (Optional) The token used to authenticate with Terraform Cloud. - We recommend omitting the token from the configuration, and instead using - [`terraform login`](/cli/commands/login) or manually configuring - `credentials` in the - [CLI config file](/cli/config/config-file#credentials). +HCP Terraform requires all workspaces to have a name. As a result, Terraform may also prompt you to rename your workspaces during the migration. -### Environment Variables +Terraform CLI-only workspaces represent multiple environments associated with the same configuration, such as `production`, `staging`, and `development`, but HCP Terraform workspaces can represent completely independent configurations and must have unique names within the HCP Terraform organization. --> **Note:** CLI integration environment variables are supported in Terraform v1.2.0 and later. +As a result, Terraform prompts you to rename workspaces according to a pattern relative to their existing names. The pattern is intended to indicate that the workspaces share configuration. A common strategy is `--`, for example `networking-prod-us-east` and `networking-staging-us-east`. Refer to [Workspace Naming](/terraform/cloud-docs/workspaces/naming) in the HCP Terraform documentation for additional information. -You can use environment variables to configure one or more `cloud` block attributes. This is helpful when you want to configure Terraform as part of a Continuous Integration (CI) pipeline. Terraform only reads these variables if the corresponding attribute is omitted from your configuration file. If you choose to configure the `cloud` block entirely through environment variables, you must still add an empty `cloud` block in your configuration file. +### Migrate remote backend -~> **Warning:** Remote execution with non-interactive workflows requires auto-approved deployments. Minimize risk of unpredictable infrastructure changes and configuration drift by making sure that no one can change your infrastructure outside of your automated build pipeline. Refer to [Non-Interactive Workflows](/cloud-docs/run/cli#non-interactive-workflows) for details. +In the `terraform` block or `terraform.tf` file, replace `backend "remote"` with `cloud`. Terraform will continue to use the same ste of HCP Terraform workspaces. -Use the following environment variables to configure the `cloud` block: +The following example migrates the state data for a single workspace named `my-app-prod` to an HCP Terraform organization named `my-org`. -- `TF_CLOUD_ORGANIZATION` - The name of the organization. Terraform reads this variable when `organization` omitted from the `cloud` block`. If both are specified, the configuration takes precedence. +```hcl +terraform { +- backend "remote" { ++ cloud { + organization = "my-org" -- `TF_CLOUD_HOSTNAME` - The hostname of a Terraform Enterprise installation. Terraform reads this when `hostname` is omitted from the `cloud` block. If both are specified, the configuration takes precendence. + workspaces { + name = "my-app-prod" + } + } + } +} +``` -- `TF_WORKSPACE` - The name of a single Terraform Cloud workspace. Terraform reads this when `workspaces` is omitted from the `cloud` block. Terraform Cloud will not create a new workspace from this variable; the workspace must exist in the specified organization. You can set `TF_WORKSPACE` if the `cloud` block uses tags. However, the value of `TF_WORKSPACE` must be included in the set of tags. This variable also selects the workspace in your local environment. Refer to [TF_WORKSPACE](https://www.terraform.io/cli/config/environment-variables#tf_workspace) for details. +If the `terraform` block or `terraform.tf` file uses the `prefix` argument to connect to multiple workspaces, you can specify a list of key-value string tags in the `tags` argument instead of using the `name` argument. During `terraform plan` or `terraform apply` operations, Terraform associates the configuration with workspaces that match the specified tags. -## Excluding Files from Upload with .terraformignore +The following example replaces the `my-app-` prefix with the `app=mine` tag: -When executing a remote `plan` or `apply` in a [CLI-driven run](/cloud-docs/run/cli), -a copy of your configuration directory is uploaded to Terraform Cloud. You can define +```hcl +terraform { +- backend "remote" { ++ cloud { + organization = "my-org" + + workspaces { +- prefix = "my-app-" ++ tags = { ++ app = "mine" ++ } + } + } + } + +``` + +Note that because the `cloud` block does not support the `prefix` argument, after you migrate your workspaces to HCP Terraform, you must refer to them by their full name when you use the Terraform CLI. For example, instead of running the `terraform workspace select prod` command, you would run `terraform workspace select my-app-prod` instead. + +## Exclude files + +When executing a remote `plan` or `apply` in a [CLI-driven run](/terraform/cloud-docs/run/cli), +a copy of your configuration directory is uploaded to HCP Terraform. You can define paths to exclude from upload by adding a `.terraformignore` file at the root of your -configuration directory. If this file is not present, the upload will exclude -the following by default: +configuration directory. If this file is not present, Terraform still excludes the following directories by default: - `.git/` directories - `.terraform/` directories (exclusive of `.terraform/modules`) -The rules in `.terraformignore` file resemble the rules allowed in a -[.gitignore file](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring): +The rules for defining `.terraformignore` are based on +[.gitignore files](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository#_ignoring): -- Comments (starting with `#`) or blank lines are ignored. +- Terraform ignores comments starting with `#` +- Terraform ignores blank lines. - End a pattern with a forward slash `/` to specify a directory. -- Negate a pattern by starting it with an exclamation point `!`. +- Negate a pattern by starting it with an exclamation point `!`. When ignoring large directories, negation patterns can impact performance. Place negation rules as early as possible within `.terraformignore` or avoid using them if possible. --> **Note:** Unlike `.gitignore`, only the `.terraformignore` at the root of the configuration directory is considered. +Terraform parses the `.terraformignore` at the root of the configuration directory. diff --git a/website/docs/cli/code/index.mdx b/website/docs/cli/code/index.mdx index ec06e20abe..bc681b34e5 100644 --- a/website/docs/cli/code/index.mdx +++ b/website/docs/cli/code/index.mdx @@ -1,31 +1,36 @@ --- -page_title: Writing and Modifying Code - Terraform CLI +page_title: Format and validate Terraform configuration using the Terraform CLI description: >- - Learn commands that help validate, format, and upgrade code written in the - Terraform Configuration Language. + Learn about the Terraform commands that validate, format, and upgrade code written in HCL. --- -# Writing and Modifying Terraform Code +# Write and modify Terrafrom configuration from the CLI -The [Terraform language](/language) is Terraform's primary +This topic provides an overview of the Terraform CLI commands you can use to develop, format, and validate your Terraform configuration. + +## Introduction + +The [Terraform language](/terraform/language) is Terraform's primary user interface, and all of Terraform's workflows rely on configurations written in the Terraform language. +## Workflows + Terraform CLI includes several commands to make Terraform code more convenient to work with. Integrating these commands into your editing workflow can potentially save you time and effort. -- [The `terraform console` command](/cli/commands/console) starts an +- [The `terraform console` command](/terraform/cli/commands/console) starts an interactive shell for evaluating Terraform - [expressions](/language/expressions), which can be a faster way + [expressions](/terraform/language/expressions), which can be a faster way to verify that a particular resource argument results in the value you expect. -- [The `terraform fmt` command](/cli/commands/fmt) rewrites Terraform +- [The `terraform fmt` command](/terraform/cli/commands/fmt) rewrites Terraform configuration files to a canonical format and style, so you don't have to waste time making minor adjustments for readability and consistency. It works well as a pre-commit hook in your version control system. -- [The `terraform validate` command](/cli/commands/validate) validates the +- [The `terraform validate` command](/terraform/cli/commands/validate) validates the syntax and arguments of the Terraform configuration files in a directory, including argument and attribute names and types for resources and modules. The `plan` and `apply` commands automatically validate a configuration before @@ -33,12 +38,12 @@ potentially save you time and effort. workflow, but it can be very useful as a pre-commit hook or as part of a continuous integration pipeline. -- [The `0.13upgrade` command](/cli/commands/0.13upgrade) and - [the `0.12upgrade` command](/cli/commands/0.12upgrade) can automatically +- [The `0.13upgrade` command](/terraform/cli/commands/0.13upgrade) and + [the `0.12upgrade` command](/terraform/cli/commands/0.12upgrade) can automatically modify the configuration files in a Terraform module to help deal with major syntax changes that occurred in the 0.13 and 0.12 releases of Terraform. Both of these commands are only available in the Terraform version they are associated with, and you are expected to upgrade older code to be compatible with 0.12 before attempting to make it compatible with 0.13. For more detailed information about updating code for new Terraform versions, see the [upgrade - guides](/language/upgrade-guides) in the Terraform language docs. + guides](/terraform/language/upgrade-guides) in the Terraform language docs. diff --git a/website/docs/cli/commands/0.12upgrade.mdx b/website/docs/cli/commands/0.12upgrade.mdx index 73068ad71b..ef4cebb75e 100644 --- a/website/docs/cli/commands/0.12upgrade.mdx +++ b/website/docs/cli/commands/0.12upgrade.mdx @@ -1,17 +1,17 @@ --- -page_title: 'Command: 0.12upgrade' +page_title: terraform 0.12upgrade command reference description: >- - The 0.12upgrade subcommand automatically rewrites existing configurations for + The `terraform 0.12upgrade` command automatically rewrites existing configurations for Terraform 0.12 compatibility. --- -# Command: 0.12upgrade +# `terraform 0.12upgrade` command The `terraform 0.12upgrade` command applies several automatic upgrade rules to help prepare a module that was written for Terraform v0.11 to be used with Terraform v0.12. --> **This command is available only in Terraform v0.12 releases.** For more information, see [the Terraform v0.12 upgrade guide](/language/upgrade-guides/0-12). +-> **This command is available only in Terraform v0.12 releases.** For more information, see [the Terraform v0.12 upgrade guide](/terraform/language/v1.1.x/upgrade-guides/0-12). ## Usage @@ -70,13 +70,13 @@ the change. Once upgraded the configuration will no longer be compatible with Terraform v0.11 and earlier. When upgrading a shared module that is called from multiple configurations, you may need to -[fix existing configurations to a previous version](/language/modules/syntax#version) +[fix existing configurations to a previous version](/terraform/language/modules/syntax#version) to allow for a gradual upgrade. If the module is published via -[a Terraform registry](/registry), assign a new _major_ version number +[a Terraform registry](/terraform/registry), assign a new _major_ version number to the upgraded module source to represent the fact that this is a breaking change for v0.11 callers. If a module is installed directly from a version control system such as Git, -[use specific revisions](/language/modules/sources#selecting-a-revision) +[use specific revisions](/terraform/language/modules/sources#selecting-a-revision) to control which version is used by which caller. The command-line options are all optional. The available options are: @@ -114,4 +114,4 @@ prompt, so be sure you have a clean work tree before running it. Because upgrading requires access to the configuration's provider plugins, all of the directories must be initialized with `terraform init` prior to -running the above. +running the above. \ No newline at end of file diff --git a/website/docs/cli/commands/0.13upgrade.mdx b/website/docs/cli/commands/0.13upgrade.mdx index 7322129aa9..0cc459b42b 100644 --- a/website/docs/cli/commands/0.13upgrade.mdx +++ b/website/docs/cli/commands/0.13upgrade.mdx @@ -1,17 +1,17 @@ --- -page_title: 'Command: 0.13upgrade' +page_title: terraform 0.13upgrade command reference description: >- - The 0.13upgrade subcommand updates existing configurations to use the new + The `terraform 0.13upgrade` command updates existing configurations to use the new provider source features from Terraform 0.13. --- -# Command: 0.13upgrade +# `terraform 0.13upgrade` command The `terraform 0.13upgrade` command updates existing configuration to add an explicit `source` attribute for each provider used in a given module. The provider source settings are stored in a `required_providers` block. --> **This command is available only in Terraform v0.13 releases.** For more information, see [the Terraform v0.13 upgrade guide](/language/upgrade-guides/0-13). +-> **This command is available only in Terraform v0.13 releases.** For more information, see [the Terraform v0.13 upgrade guide](/terraform/language/v1.1.x/upgrade-guides/0-13). ## Usage @@ -22,7 +22,7 @@ providers are in use for a module, detect the source address for those providers where possible, and record this information in a [`required_providers` block][required-providers]. -[required-providers]: /language/providers/requirements +[required-providers]: /terraform/language/providers/requirements ~> Note: the command ignores `.tf.json` files and override files in the module. @@ -86,4 +86,4 @@ Select-Object -Unique | ForEach-Object { terraform 0.13upgrade -yes $_.FullName ``` Note that the above commands include the `-yes` option to override the -interactive prompt, so be sure you have a clean work tree before running it. +interactive prompt, so be sure you have a clean work tree before running it. \ No newline at end of file diff --git a/website/docs/cli/commands/apply.mdx b/website/docs/cli/commands/apply.mdx index 2b71592b89..2eb5a9a8c6 100644 --- a/website/docs/cli/commands/apply.mdx +++ b/website/docs/cli/commands/apply.mdx @@ -1,27 +1,24 @@ --- -page_title: 'Command: apply' -description: >- - The terraform apply command executes the actions proposed in a Terraform plan +page_title: terraform apply command reference +description: The `terraform apply` command executes the actions proposed in a Terraform plan to create, update, or destroy infrastructure. --- -# Command: apply +# `terraform apply` command The `terraform apply` command executes the actions proposed in a Terraform plan. -> **Hands On:** Try the [Apply Terraform Configuration](https://learn.hashicorp.com/tutorials/terraform/apply?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial to learn how Terraform applies a configuration, how Terraform recovers from errors during apply, and common ways to use this command. +> **Hands On:** Try the [Apply Terraform Configuration](/terraform/tutorials/cli/apply?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial to learn how Terraform applies a configuration, how Terraform recovers from errors during apply, and common ways to use this command. ## Usage Usage: `terraform apply [options] [plan file]` - - ### Automatic Plan Mode -When you run `terraform apply` without passing a saved plan file, Terraform automatically creates a new execution plan as if you had run [`terraform plan`](/cli/commands/plan), prompts you to approve that plan, and takes the indicated actions. You can use all of the [planning modes](/cli/commands/plan#planning-modes) and -[planning options](/cli/commands/plan#planning-options) to customize how Terraform will create the plan. +When you run `terraform apply` without passing a saved plan file, Terraform automatically creates a new execution plan as if you had run [`terraform plan`](/terraform/cli/commands/plan), prompts you to approve that plan, and takes the indicated actions. You can use all of the [planning modes](/terraform/cli/commands/plan#planning-modes) and +[planning options](/terraform/cli/commands/plan#planning-options) to customize how Terraform will create the plan. You can pass the `-auto-approve` option to instruct Terraform to apply the plan without asking for confirmation. @@ -29,9 +26,9 @@ You can pass the `-auto-approve` option to instruct Terraform to apply the plan ### Saved Plan Mode -When you pass a [saved plan file](/cli/commands/plan#out-filename) to `terraform apply`, Terraform takes the actions in the saved plan without prompting you for confirmation. You may want to use this two-step workflow when [running Terraform in automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). +When you pass a [saved plan file](/terraform/cli/commands/plan#out-filename) to `terraform apply`, Terraform takes the actions in the saved plan without prompting you for confirmation. You may want to use this two-step workflow when [running Terraform in automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). -Use [`terraform show`](/cli/commands/show) to inspect a saved plan file before applying it. +Use [`terraform show`](/terraform/cli/commands/show) to inspect a saved plan file before applying it. When using a saved plan, you cannot specify any additional planning modes or options. These options only affect Terraform's decisions about which actions to take, and the plan file contains the final results of those @@ -41,17 +38,16 @@ decisions. Without a saved plan file, `terraform apply` supports all planning modes and planning options available for `terraform plan`. -- **[Planning Modes](/cli/commands/plan#planning-modes):** These include `-destroy`, which creates a plan to destroy all remote objects, and `-refresh-only`, which creates a plan to update Terraform state and root module output values. -- **[Planning Options](/cli/commands/plan#planning-options):** These include specifying which resource instances Terraform should replace, setting Terraform input variables, etc. +- **[Planning Modes](/terraform/cli/commands/plan#planning-modes):** These include `-destroy`, which creates a plan to destroy all remote objects, and `-refresh-only`, which creates a plan to update Terraform state and root module output values. +- **[Planning Options](/terraform/cli/commands/plan#planning-options):** These include specifying which resource instances Terraform should replace (`-replace`), setting Terraform input variables (`-var` and `-var-file`), etc. ### Apply Options The following options change how the apply command executes and reports on the apply operation. -- `-auto-approve` - Skips interactive approval of plan before applying. This - option is ignored when you pass a previously-saved plan file, because - Terraform considers you passing the plan file as the approval and so - will never prompt in that case. +- `-auto-approve` - Skips interactive approval of the plan before applying. Terraform ignores this + option when you pass a previously-saved plan file. This is because + Terraform interprets the act of passing the plan file as the approval. - `-compact-warnings` - Shows any warning messages in a compact form which includes only the summary messages, unless the warnings are accompanied by @@ -63,7 +59,7 @@ The following options change how the apply command executes and reports on the a plan, so Terraform will conservatively assume that you do not wish to apply the plan, causing the operation to fail. If you wish to run Terraform in a non-interactive context, see - [Running Terraform in Automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) for some + [Running Terraform in Automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) for some different approaches. - `-json` - Enables the [machine readable JSON UI][machine-readable-ui] output. @@ -71,7 +67,7 @@ The following options change how the apply command executes and reports on the a variable values to continue. To enable this flag, you must also either enable the `-auto-approve` flag or specify a previously-saved plan. - [machine-readable-ui]: /internals/machine-readable-ui + [machine-readable-ui]: /terraform/internals/machine-readable-ui - `-lock=false` - Don't hold a state lock during the operation. This is dangerous if others might concurrently run commands against the same @@ -86,18 +82,18 @@ The following options change how the apply command executes and reports on the a if you are running Terraform in a context where its output will be rendered by a system that cannot interpret terminal formatting. -- `-parallelism=n` - Limit the number of concurrent operation as Terraform - [walks the graph](/internals/graph#walking-the-graph). Defaults to +- `-parallelism=n` - Limit the number of concurrent operations as Terraform + [walks the graph](/terraform/internals/graph#walking-the-graph). Defaults to 10\. -- All [planning modes](/cli/commands/plan#planning-modes) and -[planning options](/cli/commands/plan#planning-options) for +- All [planning modes](/terraform/cli/commands/plan#planning-modes) and +[planning options](/terraform/cli/commands/plan#planning-options) for `terraform plan` - Customize how Terraform will create the plan. Only available when you run `terraform apply` without a saved plan file. For configurations using -[the `local` backend](/language/settings/backends/local) only, +[the `local` backend](/terraform/language/backend/local) only, `terraform apply` also accepts the legacy options -[`-state`, `-state-out`, and `-backup`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, and `-backup`](/terraform/language/backend/local#command-line-arguments). ## Passing a Different Configuration Directory @@ -107,7 +103,7 @@ that directory as the root module instead of the current working directory. That usage was deprecated in Terraform v0.14 and removed in Terraform v0.15. If your workflow relies on overriding the root module directory, use -[the `-chdir` global option](/cli/commands/#switching-working-directory-with-chdir) +[the `-chdir` global option](/terraform/cli/commands#switching-working-directory-with-chdir) instead, which works across all commands and makes Terraform consistently look in the given directory for all files it would normally read or write in the current working directory. @@ -115,6 +111,6 @@ current working directory. If your previous use of this legacy pattern was also relying on Terraform writing the `.terraform` subdirectory into the current working directory even though the root module directory was overridden, use -[the `TF_DATA_DIR` environment variable](/cli/config/environment-variables#tf_data_dir) +[the `TF_DATA_DIR` environment variable](/terraform/cli/config/environment-variables#tf_data_dir) to direct Terraform to write the `.terraform` directory to a location other than the current working directory. diff --git a/website/docs/cli/commands/console.mdx b/website/docs/cli/commands/console.mdx index 40e3b5e6fd..f740b2e6d5 100644 --- a/website/docs/cli/commands/console.mdx +++ b/website/docs/cli/commands/console.mdx @@ -1,33 +1,33 @@ --- -page_title: 'Command: console' +page_title: terraform console command reference description: >- - The terraform console command provides an interactive console for evaluating + The `terraform console` command opens an interactive console for evaluating expressions. --- -# Command: console +# `terraform console` command -The `terraform console` command provides an interactive console for -evaluating [expressions](/language/expressions). +The `terraform console` command opens an interactive console for +evaluating [expressions](/terraform/language/expressions). ## Usage Usage: `terraform console [options]` This command provides an interactive command-line console for evaluating and -experimenting with [expressions](/language/expressions). +experimenting with [expressions](/terraform/language/expressions). You can use it to test interpolations before using them in configurations and to interact with any values currently saved in -[state](/language/state). If the current state is empty or has not yet been created, you can use the console to experiment with the expression syntax and -[built-in functions](/language/functions). The console holds a [lock on the state](/language/state/locking), and you will not be able to use the console while performing other actions that modify state. +[state](/terraform/language/state). If the current state is empty or has not yet been created, you can use the console to experiment with the expression syntax and +[built-in functions](/terraform/language/functions). The console holds a [lock on the state](/terraform/language/state/locking), and you will not be able to use the console while performing other actions that modify state. To close the console, enter the `exit` command or press Control-C or Control-D. For configurations using -[the `local` backend](/language/settings/backends/local) only, +[the `local` backend](/terraform/language/backend/local) only, `terraform console` accepts the legacy command line option -[`-state`](/language/settings/backends/local#command-line-arguments). +[`-state`](/terraform/language/backend/local#command-line-arguments). ## Scripting @@ -48,10 +48,35 @@ tolist([ ## Remote State -If [remote state](/language/state/remote) is used by the current backend, +If [remote state](/terraform/language/state/remote) is used by the current backend, Terraform will read the state for the current workspace from the backend before evaluating any expressions. +## Evaluation against a Plan + +By default, `terraform console` evaluates expressions against the current +Terraform state, and so the results are typically very limited for resource +instances that haven't yet been created by applying a plan. + +You can use the `-plan` option to instead generate an execution plan first, +as if running `terraform plan`, and then evaluate against the _planned_ state +to describe the values Terraform expects will be correct after the plan is +applied. This typically causes a longer delay before the console prompt +appears, but in return there will be a more complete set of values available in +the expression scope. + +For well-behaved configurations the planning phase should not make any +modifications to real remote objects, but it _is_ possible to write a +configuration that can take significant actions while planning. For example, a +configuration which uses the `hashicorp/external` provider's +[`external` data source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external) +is likely to run the configured external command during the plan phase, which +means it would be run by `terraform console -plan` too. + +We don't recommend that you write configurations that make changes during the +plan phase. If you do write such a configuration despite that recommendation, +take care when using the console in plan mode against that configuration. + ## Examples The `terraform console` command will read the Terraform configuration in the diff --git a/website/docs/cli/commands/destroy.mdx b/website/docs/cli/commands/destroy.mdx index beb9782ebf..376942ce39 100644 --- a/website/docs/cli/commands/destroy.mdx +++ b/website/docs/cli/commands/destroy.mdx @@ -1,14 +1,13 @@ --- -page_title: 'Command: destroy' +page_title: terraform destroy command reference description: >- - The terraform destroy command destroys all objects managed by a Terraform + The `terraform destroy` command deprovisions all objects managed by a Terraform configuration. --- -# Command: destroy +# `terraform destroy` command -The `terraform destroy` command is a convenient way to destroy all remote -objects managed by a particular Terraform configuration. +The `terraform destroy` command deprovisions all objects managed by a Terraform configuration. While you will typically not want to destroy long-lived objects in a production environment, Terraform is sometimes used to manage ephemeral infrastructure @@ -27,7 +26,7 @@ terraform apply -destroy ``` For that reason, this command accepts most of the options that -[`terraform apply`](/cli/commands/apply) accepts, although it does +[`terraform apply`](/terraform/cli/commands/apply) accepts, although it does not accept a plan file argument and forces the selection of the "destroy" planning mode. @@ -38,9 +37,14 @@ destroying would be, by running the following command: terraform plan -destroy ``` -This will run [`terraform plan`](/cli/commands/plan) in _destroy_ mode, showing +This will run [`terraform plan`](/terraform/cli/commands/plan) in _destroy_ mode, showing you the proposed destroy changes without executing them. -> **Note:** The `-destroy` option to `terraform apply` exists only in Terraform v0.15.2 and later. For earlier versions, you _must_ use `terraform destroy` to get the effect of `terraform apply -destroy`. + +### Target a specific resource + +You can use the `-target` option to destroy a particular resource and its dependencies: + diff --git a/website/docs/cli/commands/env.mdx b/website/docs/cli/commands/env.mdx deleted file mode 100644 index 611354068a..0000000000 --- a/website/docs/cli/commands/env.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -page_title: 'Command: env' -description: >- - The terraform env command is a deprecated form of the terraform workspace - command. ---- - -# Command: env - -The `terraform env` command is deprecated. -[The `terraform workspace` command](/cli/commands/workspace) -should be used instead. diff --git a/website/docs/cli/commands/fmt.mdx b/website/docs/cli/commands/fmt.mdx index ef03d40193..871f800eb5 100644 --- a/website/docs/cli/commands/fmt.mdx +++ b/website/docs/cli/commands/fmt.mdx @@ -1,33 +1,32 @@ --- -page_title: 'Command: fmt' +page_title: terraform fmt command reference description: >- - The terraform fmt command rewrites configuration files to a canonical format + The `terraform fmt` command formats Terraform configuration contents so that it matches the canonical format and style. --- -# Command: fmt +# `terraform fmt` command -The `terraform fmt` command is used to rewrite Terraform configuration files -to a canonical format and style. This command applies a subset of -the [Terraform language style conventions](/language/syntax/style), +The `terraform fmt` command formats Terraform configuration file contents so that it matches the canonical format and style. This command applies a subset of +the [Terraform language style conventions](/terraform/language/style#code-formatting), along with other minor adjustments for readability. -Other Terraform commands that generate Terraform configuration will produce -configuration files that conform to the style imposed by `terraform fmt`, so -using this style in your own files will ensure consistency. +## Introduction +Terraform commands that generate Terraform configuration produce +configuration files that conform to the style imposed by `terraform fmt`. Follow this style in your files to ensure consistency. The canonical format may change in minor ways between Terraform versions, so after upgrading Terraform we recommend to proactively run `terraform fmt` on your modules along with any other changes you are making to adopt the new version. -We don't consider new formatting rules in `terraform fmt` to be a breaking -change in new versions of Terraform, but we do aim to minimize changes for -configurations that are already following the style examples shown in the -Terraform documentation. When adding new formatting rules, they will usually -aim to apply more of the rules already shown in the configuration examples -in the documentation, and so we recommend following the documented style even -for decisions that `terraform fmt` doesn't yet apply automatically. +We do not consider new formatting rules in `terraform fmt` to be a breaking +change in new versions of Terraform, but we minimize changes for +configurations that already follow the style examples shown in the +Terraform documentation. New formatting rules programmed into the `terraform fmt` +command are usually expansions to include existing rules from the documentation. +We recommend following the documented style even for decisions that `terraform fmt` +does not yet apply automatically. Formatting decisions are always subjective and so you might disagree with the decisions that `terraform fmt` makes. This command is intentionally opinionated @@ -47,16 +46,17 @@ and the generated files. Usage: `terraform fmt [options] [target...]` -By default, `fmt` scans the current directory for configuration files. If you -provide a directory for the `target` argument, then `fmt` will scan that -directory instead. If you provide a file, then `fmt` will process just that -file. If you provide a single dash (`-`), then `fmt` will read from standard -input (STDIN). +By default, the `terraform fmt` command scans your current directory for configuration files. You can also provide a `target` argument to tell `terraform fmt` to scan: +* A directory +* A specific file +* Standard input by supplying a single dash (`-`). -The command-line flags are all optional. The following flags are available: +The `terraform fmt` command accepts the following arguments. -* `-list=false` - Don't list the files containing formatting inconsistencies. -* `-write=false` - Don't overwrite the input files. (This is implied by `-check` or when the input is STDIN.) -* `-diff` - Display diffs of formatting changes -* `-check` - Check if the input is formatted. Exit status will be 0 if all input is properly formatted. If not, exit status will be non-zero and the command will output a list of filenames whose files are not properly formatted. -* `-recursive` - Also process files in subdirectories. By default, only the given directory (or current directory) is processed. +| Flag | Description | Required | +| :---- | :---- | :---- | +| `-list=false` | Prevents the command from listing the files containing formatting inconsistencies. | Optional | +| `-diff` | Displays the diffs of formatting changes. | Optional | +| `-write=false` | Prevents the command from overwriting files. This behavior is implied by the `-check` flag or if the input is from `STDIN`. | Optional | +| `-check` | Checks if the input is formatted. The exit status is `0` if the command's input is properly formatted. Otherwise, the exit status is non-zero, and the command outputs a list of improperly formatted file names. | Optional | +| `-recursive` | Processes files in subdirectories in addition to the current directory. By default, the command only processes the specified, or current, directory. | Optional | diff --git a/website/docs/cli/commands/force-unlock.mdx b/website/docs/cli/commands/force-unlock.mdx index 705d04427a..b26eb3e1b8 100644 --- a/website/docs/cli/commands/force-unlock.mdx +++ b/website/docs/cli/commands/force-unlock.mdx @@ -1,18 +1,17 @@ --- -page_title: 'Command: force-unlock' +page_title: terraform force-unlock command reference description: >- - The terraform force-unlock command unlocks the state for a configuration. It + The `terraform force-unlock` command unlocks the state for a configuration. It does not modify your infrastructure. --- -# Command: force-unlock +# `terraform force-unlock` command -Manually unlock the state for the defined configuration. +This topic provides reference information about the `terraform force-unlock` command. This command manually unlocks the state for the defined configuration. -This will not modify your infrastructure. This command removes the lock on the -state for the current configuration. The behavior of this lock is dependent +This command removes the lock on the state for the current configuration. The behavior of this lock is dependent on the backend being used. Local state files cannot be unlocked by another -process. +process. The `terraform force-unlock` command does not modify your infrastructure. ## Usage @@ -27,4 +26,4 @@ process. Options: -* `-force` - Don't ask for input for unlock confirmation. +- `-force` - Don't ask for input for unlock confirmation. diff --git a/website/docs/cli/commands/get.mdx b/website/docs/cli/commands/get.mdx index bf9b9f9705..9b0254331c 100644 --- a/website/docs/cli/commands/get.mdx +++ b/website/docs/cli/commands/get.mdx @@ -1,16 +1,16 @@ --- -page_title: 'Command: get' -description: The terraform get command downloads and updates modules. +page_title: terraform get command reference +description: The `terraform get` command downloads and updates modules. --- -# Command: get +# `terraform get` command -The `terraform get` command is used to download and update -[modules](/language/modules/develop) mentioned in the root module. +Run the `terraform get` command to download and update +[modules](/terraform/language/modules/develop) declared in the root module. ## Usage -Usage: `terraform get [options] PATH` +Usage: `terraform get [options]` The modules are downloaded into a `.terraform` subdirectory of the current working directory. Don't commit this directory to your version control diff --git a/website/docs/cli/commands/graph.mdx b/website/docs/cli/commands/graph.mdx index 969c6e5dbe..12a471977f 100644 --- a/website/docs/cli/commands/graph.mdx +++ b/website/docs/cli/commands/graph.mdx @@ -1,57 +1,54 @@ --- -page_title: 'Command: graph' +page_title: terraform graph command reference description: >- - The terraform graph command generates a visual representation of a + The `terraform graph` command generates a visual representation of a configuration or execution plan that you can use to generate charts. --- -# Command: graph +# `terraform graph` command -The `terraform graph` command is used to generate a visual -representation of either a configuration or execution plan. -The output is in the DOT format, which can be used by -[GraphViz](http://www.graphviz.org) to generate charts. +The `terraform graph` command generates a visual representation of a configuration or execution plan that you can use to generate charts. This command uses the DOT language generate graphs. Refer to the [GraphViz documentation](https://graphviz.org/doc/info/lang.html) for additional information. ## Usage Usage: `terraform graph [options]` -Outputs the visual execution graph of Terraform resources according to -either the current configuration or an execution plan. +By default the result is a simplified graph which describes only the dependency +ordering of the resources (`resource` and `data` blocks) in the configuration. -The graph is outputted in DOT format. The typical program that can -read this format is GraphViz, but many web services are also available -to read this format. - -The `-type` flag can be used to control the type of graph shown. Terraform -creates different graphs for different operations. See the options below -for the list of types supported. The default type is "plan" if a -configuration is given, and "apply" if a plan file is passed as an -argument. +The `-type=...` option optionally selects from a number of other graph types +which have more detail, at the expense of also exposing some of the +implementation details of the Terraform language runtime. Options: -* `-plan=tfplan` - Render graph using the specified plan file instead of the - configuration in the current directory. +* `-plan=tfplan` - Produce a graph for applying the given plan. Implies `-type=apply`. -* `-draw-cycles` - Highlight any cycles in the graph with colored edges. - This helps when diagnosing cycle errors. +* `-draw-cycles` - Highlight any cycles in the graph with colored edges. + This helps when diagnosing cycle errors. This option is supported only when + selecting one of the real graph operaton types using the `-type=...` + option. -* `-type=plan` - Type of graph to output. Can be: `plan`, `plan-destroy`, `apply`, - `validate`, `input`, `refresh`. - -* `-module-depth=n` - (deprecated) In prior versions of Terraform, specified the - depth of modules to show in the output. +* `-type=...` - Selects a specific operation type to show the graph of, instead + of the default resources-only simplified graph. + Can be: `plan`, `plan-refresh-only`, `plan-destroy`, or `apply`. ## Generating Images -The output of `terraform graph` is in the DOT format, which can -easily be converted to an image by making use of `dot` provided -by GraphViz: +The graph output uses +[the DOT language](https://en.wikipedia.org/wiki/DOT_(graph_description_language)), +which is a machine-readable graph description language which originated in +[Graphviz](https://graphviz.org/). You can use the Graphviz `dot` command +to present the resulting graph description as an image. There are also various +third-party online graph rendering services which accept this format. + +If you have the Graphviz `dot` command already installed, you can render +a PNG image by piping into that command: ```shellsession -$ terraform graph | dot -Tsvg > graph.svg +$ terraform graph -type=plan | dot -Tpng >graph.png ``` -Here is an example graph output: -![Graph Example](/img/docs/graph-example.png) +The following is an example result: + +![A visualization of the plan graph of a hypothetical Terraform configuration, produced by dot](/img/docs/graph-example.png) diff --git a/website/docs/cli/commands/import.mdx b/website/docs/cli/commands/import.mdx index 46b712b21f..c3097e7394 100644 --- a/website/docs/cli/commands/import.mdx +++ b/website/docs/cli/commands/import.mdx @@ -1,15 +1,15 @@ --- -page_title: 'Command: import' -description: The terraform import command brings existing resources into Terraform state. +page_title: terraform import command reference +description: The `terraform import` command imports existing resources into Terraform state. --- -# Command: import +# `terraform import` command reference -> **Hands-on:** Try the [Import Terraform Configuration](https://learn.hashicorp.com/tutorials/terraform/state-import?in=terraform/state&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +The `terraform import` command imports existing resources into Terraform. Refer to [Import](/terraform/cli/import) for additional information. + + +> **Hands-on:** Try the [Import Terraform Configuration](/terraform/tutorials/state/state-import?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -The `terraform import` command is used to -[import existing resources](/cli/import) -into Terraform. ## Usage @@ -18,7 +18,7 @@ Usage: `terraform import [options] ADDRESS ID` Import will find the existing resource from ID and import it into your Terraform state at the given ADDRESS. -ADDRESS must be a valid [resource address](/cli/state/resource-addressing). +ADDRESS must be a valid [resource address](/terraform/cli/state/resource-addressing). Because any resource address is valid, the import command can import resources into modules as well as directly into the root of your state. @@ -34,7 +34,9 @@ itself having created all objects. If you import existing objects into Terraform be careful to import each remote object to only one Terraform resource address. If you import the same object multiple times, Terraform may exhibit unwanted behavior. For more information on this assumption, see -[the State section](/language/state). +[the State section](/terraform/language/state). + +Instead of manually importing resources, you can add the `import` block to your Terraform configurations so that Terraform imports resources when you run the `terraform apply` command. Using Terraform configurations lets you automate resource imports as part of your CI/CD pipelines. Refer to the [`import` block reference documentation](/terraform/language/import) for additional information. The command-line flags are all optional. The following flags are available: @@ -53,8 +55,8 @@ The command-line flags are all optional. The following flags are available: - `-no-color` - If specified, output won't contain any color. -- `-parallelism=n` - Limit the number of concurrent operation as Terraform - [walks the graph](/internals/graph#walking-the-graph). Defaults +- `-parallelism=n` - Limit the number of concurrent operations as Terraform + [walks the graph](/terraform/internals/graph#walking-the-graph). Defaults to 10. - `-provider=provider` - **Deprecated** Override the provider configuration to @@ -63,27 +65,27 @@ The command-line flags are all optional. The following flags are available: - `-var 'foo=bar'` - Set a variable in the Terraform configuration. This flag can be set multiple times. Variable values are interpreted as - [literal expressions](/language/expressions/types) in the + [literal expressions](/terraform/language/expressions/types) in the Terraform language, so list and map values can be specified via this flag. - `-var-file=foo` - Set variables in the Terraform configuration from - a [variable file](/language/values/variables#variable-definitions-tfvars-files). If - a `terraform.tfvars` or any `.auto.tfvars` files are present in the current - directory, they will be automatically loaded. `terraform.tfvars` is loaded + a [variable file](/terraform/language/values/variables#variable-definitions-tfvars-files). If + `terraform.tfvars` or any `.auto.tfvars` files are present in the current + directory, they are automatically loaded. Terraform loads `terraform.tfvars` first and the `.auto.tfvars` files after in alphabetical order. Any files specified by `-var-file` override any values set automatically from files in the working directory. This flag can be used multiple times. This is only useful with the `-config` flag. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform import` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). For configurations using -[the `local` backend](/language/settings/backends/local) only, +[the `local` backend](/terraform/language/backend/local) only, `terraform import` also accepts the legacy options -[`-state`, `-state-out`, and `-backup`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, and `-backup`](/terraform/language/backend/local#command-line-arguments). ## Provider Configuration @@ -107,8 +109,8 @@ variable "access_key" {} variable "secret_key" {} provider "aws" { - access_key = "${var.access_key}" - secret_key = "${var.secret_key}" + access_key = var.access_key + secret_key = var.secret_key } ``` @@ -131,7 +133,7 @@ $ terraform import module.foo.aws_instance.bar i-abcd1234 ## Example: Import into Resource configured with count The example below will import an AWS instance into the first instance of the `aws_instance` resource named `baz` configured with -[`count`](/language/meta-arguments/count): +[`count`](/terraform/language/meta-arguments/count): ```shell $ terraform import 'aws_instance.baz[0]' i-abcd1234 @@ -140,7 +142,7 @@ $ terraform import 'aws_instance.baz[0]' i-abcd1234 ## Example: Import into Resource configured with for_each The example below will import an AWS instance into the `"example"` instance of the `aws_instance` resource named `baz` configured with -[`for_each`](/language/meta-arguments/for_each): +[`for_each`](/terraform/language/meta-arguments/for_each): Linux, Mac OS, and UNIX: diff --git a/website/docs/cli/commands/index.mdx b/website/docs/cli/commands/index.mdx index f7dd9c74fc..3dfffc566c 100644 --- a/website/docs/cli/commands/index.mdx +++ b/website/docs/cli/commands/index.mdx @@ -1,22 +1,25 @@ --- -page_title: Basic CLI Features -description: An introduction to the terraform command and its available subcommands. +page_title: Terraform CLI overview +description: The Terrafrom CLI includes commands for provisioning infrastructure as code and managing the infrastructure lifecycle. Learn about Terraform CLI features. --- -# Basic CLI Features +# Terraform CLI Overview -> **Hands-on:** Try the [Terraform: Get Started](https://learn.hashicorp.com/collections/terraform/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +This topic provides an overview of the Terraform command line interface. -The command line interface to Terraform is via the `terraform` command, which +> **Hands-on:** Try the [Terraform: Get Started](/terraform/tutorials/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. + + +## Introduction + +The command line interface to Terraform is the `terraform` command, which accepts a variety of subcommands such as `terraform init` or `terraform plan`. -A full list of all of the supported subcommands is in the navigation section -of this page. We refer to the `terraform` command line tool as "Terraform CLI" elsewhere in the documentation. This terminology is often used to distinguish it from other components you might use in the Terraform product family, such as -[Terraform Cloud](/cloud-docs) or -the various [Terraform providers](/language/providers), which +[HCP Terraform](/terraform/cloud-docs) or +the various [Terraform providers](/terraform/language/providers), which are developed and released separately from Terraform CLI. To view a list of the commands available in your current Terraform version, @@ -45,6 +48,8 @@ All other commands: import Associate existing infrastructure with a Terraform resource login Obtain and save credentials for a remote host logout Remove locally-stored credentials for a remote host + metadata Metadata related commands + modules Show all declared modules in a working directory output Show output values from your root module providers Show the providers required for this configuration refresh Update the state to match remote systems @@ -58,7 +63,7 @@ All other commands: Global options (use these before the subcommand, if any): -chdir=DIR Switch to a different working directory before executing the given subcommand. - -help Show this help output, or the help for a specified subcommand. + -help Show this help output or the help for a specified subcommand. -version An alias for the "version" subcommand. ``` @@ -69,10 +74,9 @@ To get specific help for any specific command, use the `-help` option with the relevant subcommand. For example, to see help about the "validate" subcommand you can run `terraform validate -help`. -The inline help built in to Terraform CLI describes the most important +The inline help built into Terraform CLI describes the most important characteristics of each command. For more detailed information, refer to each -command's section of this documentation, available in the navigation -section of this page. +command's page for details. ## Switching working directory with `-chdir` @@ -98,7 +102,7 @@ will be read or written in the given directory instead. There are two exceptions where Terraform will use the original working directory even when you specify `-chdir=...`: -* Settings in the [CLI Configuration](/cli/config/config-file) are not for a specific +* Settings in the [CLI Configuration](/terraform/cli/config/config-file) are not for a specific subcommand and Terraform processes them before acting on the `-chdir` option. @@ -110,8 +114,7 @@ even when you specify `-chdir=...`: ## Shell Tab-completion If you use either `bash` or `zsh` as your command shell, Terraform can provide -tab-completion support for all command names and (at this time) _some_ command -arguments. +tab-completion support for all command names and some command arguments. To add the necessary commands to your shell profile, run the following command: @@ -129,14 +132,11 @@ manually in the shell profile, run the following command: terraform -uninstall-autocomplete ``` -Currently not all of Terraform's subcommands have full tab-completion support -for all arguments. We plan to improve tab-completion coverage over time. - ## Upgrade and Security Bulletin Checks The Terraform CLI commands interact with the HashiCorp service [Checkpoint](https://checkpoint.hashicorp.com/) to check for the availability -of new versions and for critical security bulletins about the current version. +of new versions and critical security bulletins about the current version. One place where the effect of this can be seen is in `terraform version`, where it is used by default to indicate in the output when a newer version is @@ -151,7 +151,7 @@ Checkpoint itself can be entirely disabled for all HashiCorp products by setting the environment variable `CHECKPOINT_DISABLE` to any non-empty value. Alternatively, settings in -[the CLI configuration file](/cli/config/config-file) can be used to +[the CLI configuration file](/terraform/cli/config/config-file) can be used to disable checkpoint features. The following checkpoint-related settings are supported in this file: diff --git a/website/docs/cli/commands/init.mdx b/website/docs/cli/commands/init.mdx index ba975042f8..08604186b8 100644 --- a/website/docs/cli/commands/init.mdx +++ b/website/docs/cli/commands/init.mdx @@ -1,19 +1,19 @@ --- -page_title: 'Command: init' +page_title: terraform init command reference description: >- - The terraform init command initializes a working directory containing + The `terraform init` command initializes a working directory containing configuration files and installs plugins for required providers. --- -# Command: init +# `terraform init` command -> **Hands-on:** Try the [Terraform: Get Started](https://learn.hashicorp.com/collections/terraform/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. For more in-depth details on the `init` command, check out the [Initialize Terraform Configuration tutorial](https://learn.hashicorp.com/tutorials/terraform/init?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). - -The `terraform init` command is used to initialize a working directory -containing Terraform configuration files. This is the first command that should -be run after writing a new Terraform configuration or cloning an existing one +The `terraform init` command initializes a working directory +containing Terraform configuration files. This is the first command you should +run after writing a new Terraform configuration or cloning an existing configuration from version control. It is safe to run this command multiple times. +> **Hands-on:** Try the [Terraform: Get Started](/terraform/tutorials/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. For more in-depth details on the `init` command, check out the [Initialize Terraform Configuration tutorial](/terraform/tutorials/cli/init?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). + ## Usage Usage: `terraform init [options]` @@ -74,7 +74,7 @@ activating credentials) before running `terraform init`. ## Backend Initialization During init, the root configuration directory is consulted for -[backend configuration](/language/settings/backends/configuration) and the chosen backend +[backend configuration](/terraform/language/backend) and the chosen backend is initialized using the given configuration settings. Re-running init with an already-initialized backend will update the working @@ -96,14 +96,14 @@ when the working directory was already previously initialized for a particular backend. The `-backend-config=...` option can be used for -[partial backend configuration](/language/settings/backends/configuration#partial-configuration), +[partial backend configuration](/terraform/language/backend#partial-configuration), in situations where the backend settings are dynamic or sensitive and so cannot be statically specified in the configuration file. ## Child Module Installation During init, the configuration is searched for `module` blocks, and the source -code for referenced [modules](/language/modules/develop) is retrieved from the locations +code for referenced [modules](/terraform/language/modules/develop) is retrieved from the locations given in their `source` arguments. Re-running init with modules already installed will install the sources for @@ -128,13 +128,13 @@ third-party provider registry, `terraform init` will automatically find, download, and install the necessary provider plugins. If you cannot or do not wish to install providers from their origin registries, you can customize how Terraform installs providers using -[the provider installation settings in the CLI configuration](/cli/config/config-file#provider-installation). +[the provider installation settings in the CLI configuration](/terraform/cli/config/config-file#provider-installation). For more information about specifying which providers are required for each -of your modules, see [Provider Requirements](/language/providers/requirements). +of your modules, see [Provider Requirements](/terraform/language/providers/requirements). After successful installation, Terraform writes information about the selected -providers to [the dependency lock file](/language/files/dependency-lock). +providers to [the dependency lock file](/terraform/language/files/dependency-lock). You should commit this file to your version control system to ensure that when you run `terraform init` again in future Terraform will select exactly the same provider versions. Use the `-upgrade` option if you want Terraform @@ -150,15 +150,15 @@ You can modify `terraform init`'s plugin behavior with the following options: * `-get-plugins=false` — Skip plugin installation. -> Note: Since Terraform 0.13, this option has been superseded by the - [`provider_installation`](/cli/config/config-file#provider-installation) and - [`plugin_cache_dir`](/cli/config/config-file#plugin_cache_dir) settings. + [`provider_installation`](/terraform/cli/config/config-file#provider-installation) and + [`plugin_cache_dir`](/terraform/cli/config/config-file#plugin_cache_dir) settings. It should not be used in Terraform versions 0.13+, and this option was removed in Terraform 0.15. * `-plugin-dir=PATH` — Force plugin installation to read plugins _only_ from the specified directory, as if it had been configured as a `filesystem_mirror` in the CLI configuration. If you intend to routinely use a particular filesystem mirror then we recommend - [configuring Terraform's installation methods globally](/cli/config/config-file#provider-installation). + [configuring Terraform's installation methods globally](/terraform/cli/config/config-file#provider-installation). You can use `-plugin-dir` as a one-time override for exceptional situations, such as if you are testing a local build of a provider plugin you are currently developing. @@ -181,7 +181,7 @@ other interesting features such as integration with version control hooks. There are some special concerns when running `init` in such an environment, including optionally making plugins available locally to avoid repeated re-installation. For more information, see -the [Running Terraform in Automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +the [Running Terraform in Automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. ## Passing a Different Configuration Directory @@ -189,10 +189,10 @@ Terraform v0.13 and earlier also accepted a directory path in place of the plan file argument to `terraform apply`, in which case Terraform would use that directory as the root module instead of the current working directory. -That usage is still supported in Terraform v0.14, but is now deprecated and we -plan to remove it in Terraform v0.15. If your workflow relies on overriding +That usage is still supported in Terraform v0.14, but is now deprecated and removed in +Terraform v0.15. If your workflow relies on overriding the root module directory, use -[the `-chdir` global option](/cli/commands/#switching-working-directory-with-chdir) +[the `-chdir` global option](/terraform/cli/commands#switching-working-directory-with-chdir) instead, which works across all commands and makes Terraform consistently look in the given directory for all files it would normally read or write in the current working directory. @@ -200,6 +200,6 @@ current working directory. If your previous use of this legacy pattern was also relying on Terraform writing the `.terraform` subdirectory into the current working directory even though the root module directory was overridden, use -[the `TF_DATA_DIR` environment variable](/cli/config/environment-variables#tf_data_dir) +[the `TF_DATA_DIR` environment variable](/terraform/cli/config/environment-variables#tf_data_dir) to direct Terraform to write the `.terraform` directory to a location other than the current working directory. diff --git a/website/docs/cli/commands/login.mdx b/website/docs/cli/commands/login.mdx index 86e1fcfee9..9a4cb535a7 100644 --- a/website/docs/cli/commands/login.mdx +++ b/website/docs/cli/commands/login.mdx @@ -1,28 +1,26 @@ --- -page_title: 'Command: login' +page_title: terraform login command reference description: >- - The terraform login command can be used to automatically obtain and save an - API token for Terraform Cloud, Terraform Enterprise, or any other host that + The `terraform login` command obtains an API token for HCP Terraform, Terraform Enterprise, or other host that offers Terraform services. --- -# Command: login +# `terraform login` command -The `terraform login` command can be used to automatically obtain and save an -API token for Terraform Cloud, Terraform Enterprise, or any other host that offers Terraform services. +The `terraform login` command obtains an API token for HCP Terraform, Terraform Enterprise, or other host that + offers Terraform services. --> **Note:** This command is suitable only for use in interactive scenarios -where it is possible to launch a web browser on the same host where Terraform +You can only use this command in interactive scenarios because the command launches a web browser on the same host where Terraform is running. If you are running Terraform in an unattended automation scenario, you can -[configure credentials manually in the CLI configuration](/cli/config/config-file#credentials). +[configure credentials manually in the CLI configuration](/terraform/cli/config/config-file#credentials). ## Usage Usage: `terraform login [hostname]` If you don't provide an explicit hostname, Terraform will assume you want to -log in to Terraform Cloud at `app.terraform.io`. +log in to HCP Terraform at `app.terraform.io`. ## Credentials Storage @@ -32,14 +30,14 @@ local CLI configuration file called `credentials.tfrc.json`. When you run the API token and give you a chance to cancel if the current configuration is not as desired. -If you don't wish to store your API token in the default location, you can +If you do not wish to store your API token in the default location, you can optionally configure a -[credentials helper program](/cli/config/config-file#credentials-helpers) which knows +[credentials helper program](/terraform/cli/config/config-file#credentials-helpers) that knows how to store and later retrieve credentials in some other system, such as your organization's existing secrets management system. ## Login Server Support The `terraform login` command works with any server supporting the -[login protocol](/internals/login-protocol), including Terraform Cloud +[login protocol](/terraform/internals/login-protocol), including HCP Terraform and Terraform Enterprise. diff --git a/website/docs/cli/commands/logout.mdx b/website/docs/cli/commands/logout.mdx index dbec4bc757..8351a6a232 100644 --- a/website/docs/cli/commands/logout.mdx +++ b/website/docs/cli/commands/logout.mdx @@ -1,14 +1,18 @@ --- -page_title: 'Command: logout' +page_title: terraform logout command reference description: >- - The terraform logout command is used to remove credentials stored by terraform - login. + The `terraform logout` command removes credentials stored after using the terraform + login command. --- -# Command: logout +# `terraform logout` command -The `terraform logout` command is used to remove credentials stored by -`terraform login`. These credentials are API tokens for Terraform Cloud, +This topic provides reference information about the `terraform logout` command. + +## Introduction + +Use the `terraform logout` command to remove credentials stored after running the +`terraform login` command. These credentials are API tokens for HCP Terraform, Terraform Enterprise, or any other host that offers Terraform services. ## Usage @@ -16,7 +20,7 @@ Terraform Enterprise, or any other host that offers Terraform services. Usage: `terraform logout [hostname]` If you don't provide an explicit hostname, Terraform will assume you want to -log out of Terraform Cloud at `app.terraform.io`. +log out of HCP Terraform at `app.terraform.io`. -> **Note:** the API token is only removed from local storage, not destroyed on the remote server, so it will remain valid until manually revoked. @@ -25,5 +29,5 @@ the remote server, so it will remain valid until manually revoked. By default, Terraform will remove the token stored in plain text in a local CLI configuration file called `credentials.tfrc.json`. If you have configured a -[credentials helper program](/cli/config/config-file#credentials-helpers), Terraform +[credentials helper program](/terraform/cli/config/config-file#credentials-helpers), Terraform will use the helper's `forget` command to remove it. diff --git a/website/docs/cli/commands/modules.mdx b/website/docs/cli/commands/modules.mdx new file mode 100644 index 0000000000..2039a3bce9 --- /dev/null +++ b/website/docs/cli/commands/modules.mdx @@ -0,0 +1,88 @@ +--- +page_title: terraform modules command reference +description: >- + The `terraform modules` command prints source and version data about all declared + modules in Terraform configuration. +--- + +# Command: modules + +The `terraform modules` command provides a holistic view of all Terraform modules +declared in Terraform configuration for the current working directory. +This command can be useful for auditing or setting policies on module consumption. +The output of `terraform modules` includes a list of each declared module's +key, source, and version. + +-> **Note**: The `terraform modules` command requires **Terraform v1.10.0 or later**. + +## Usage + +Usage: `terraform modules [options]` + +The following optional flags are available: + +- `-json` - Displays the module declarations in a machine-readable, JSON format. + +The output of `terraform modules -json` includes a `format_version` key, which is set to the value of `"1.0"` in Terraform 1.10.0. The semantics of this version are: + +- For minor versions, such as `"1.1"`, changes or additions will be backward-compatible. + Ignore object properties that are unrecognized to remain forward-compatible + with future minor versions. +- For major versions, such as `"2.0"`, changes will not be backward-compatible. Reject any input which reports an unsupported major version. + +We will introduce new major versions only within the bounds of +[the Terraform 1.0 Compatibility Promises](/terraform/language/v1-compatibility-promises). + +## Output Format + +The `terraform modules` command returns a nested structure, representing module composition and hierarchy within your configuration. + +The following example demonstrates what the `terraform modules` command returns without any additional flags: + +```shell-session +Modules declared by configuration: + +. +├── "my_private_registry_module"[app.terraform.io/hashicorp/label/null] 1.0.0 (>=1.0.0, < 2.0.1) +├── "my_public_registry_module"[terraform-aws-modules/iam/aws] 5.47.1 (>5.0.1) +└── "my_local_module_a"[./path/to/local/module_a] + └── "my_local_module_b"[./path/to/local/module_a/module_b] + └── "my_local_module_c"[./path/to/local/module/module_a/module_b/module_c] +``` + +The following example is a representation of the JSON output format that `terraform modules -json` returns. + +```javascript +{ + "format_version": "1.0", + "modules": [ + { + "key": "my_private_registry_module", + "source": "app.terraform.io/hashicorp/label/null", + "version": "1.0.0" + }, + { + "key": "my_public_registry_module", + "source": "terraform-aws-modules/iam/aws", + "version": "5.47.1" + }, + { + "key": "my_local_module_a", + "source": "./path/to/local/module_a", + "version": "" + }, + { + "key": "my_local_module_b", + "source": "./path/to/local/module_a/module_b", + "version": "" + }, + { + "key": "my_local_module_c", + "source": "./path/to/local/module/module_a/module_b/module_c", + "version": "" + }, + ] +} +``` + + diff --git a/website/docs/cli/commands/output.mdx b/website/docs/cli/commands/output.mdx index 39ef516cd2..cfaa719704 100644 --- a/website/docs/cli/commands/output.mdx +++ b/website/docs/cli/commands/output.mdx @@ -1,14 +1,12 @@ --- -page_title: 'Command: output' +page_title: terraform output command reference description: >- - The `terraform output` command is used to extract the value of an output - variable from the state file. + The `terraform output` command extracts the value of an output variable from the state file. --- -# Command: output +# `terraform output` command -The `terraform output` command is used to extract the value of -an output variable from the state file. +The `terraform output` command extracts the value of an output variable from the state file. ## Usage @@ -30,11 +28,10 @@ The command-line flags are all optional. The following flags are available: for processing complex data types. * `-no-color` - If specified, output won't contain any color. * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". - Ignored when [remote state](/language/state/remote) is used. + Ignored when [remote state](/terraform/language/state/remote) is used. --> **Note:** When using the `-json` or `-raw` command-line flag, any sensitive -values in Terraform state will be displayed in plain text. For more information, -see [Sensitive Data in State](/language/state/sensitive-data). +-> **Note:** When using the `-json` or `-raw` command-line flags, Terraform displays `sensitive` values in plain text. For more information, +refer to [Sensitive data in state](/terraform/language/state/sensitive-data). ## Examples @@ -51,7 +48,7 @@ output "lb_address" { output "password" { sensitive = true - value = var.secret_password + value = var.secret_password } ``` @@ -68,13 +65,15 @@ lb_address = "my-app-alb-1657023003.us-east-1.elb.amazonaws.com" password = ``` -Note that outputs with the `sensitive` attribute will be redacted: +Note that Terraform does not redact `sensitive` values when you specify the output by name: ```shellsession $ terraform output password -password = +password = notasecurepassword ``` +However, Terraform completely omits any `ephemeral` values, even if you specify an output by name. Ephemeral values are never stored in state or included in Terraform plans. For more information, refer to [Sensitive data in state](/terraform/language/state/sensitive-data). + To query for the DNS address of the load balancer: ```shellsession diff --git a/website/docs/cli/commands/plan.mdx b/website/docs/cli/commands/plan.mdx index d801d5d810..4354cfacdc 100644 --- a/website/docs/cli/commands/plan.mdx +++ b/website/docs/cli/commands/plan.mdx @@ -1,15 +1,17 @@ --- -page_title: 'Command: plan' +page_title: terraform plan command reference description: >- - The terraform plan command creates an execution plan with a preview of the + The `terraform plan` command creates an execution plan with a preview of the changes that Terraform will make to your infrastructure. --- -# Command: plan +# `terraform plan` command The `terraform plan` command creates an execution plan, which lets you preview -the changes that Terraform plans to make to your infrastructure. By default, -when Terraform creates a plan it: +the changes that Terraform plans to make to your infrastructure. + +## Introduction +By default, Terraform performs the following operations when it creates a plan: * Reads the current state of any already-existing remote objects to make sure that the Terraform state is up-to-date. @@ -18,10 +20,9 @@ when Terraform creates a plan it: * Proposes a set of change actions that should, if applied, make the remote objects match the configuration. -> **Hands-on:** Try the [Terraform: Get Started](https://learn.hashicorp.com/collections/terraform/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. For more in-depth details on the `plan` command, check out the [Create a Terraform Plan tutorial](https://learn.hashicorp.com/tutorials/terraform/plan?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). +> **Hands-on:** Try the [Terraform: Get Started](/terraform/tutorials/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. For more in-depth details on the `plan` command, check out the [Create a Terraform Plan tutorial](/terraform/tutorials/cli/plan?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). -The plan command alone will not actually carry out the proposed changes, and -so you can use this command to check whether the proposed changes match what +The `plan` command alone does not actually carry out the proposed changes You can use this command to check whether the proposed changes match what you expected before you apply the changes or share your changes with your team for broader review. @@ -31,14 +32,14 @@ to be taken. If you are using Terraform directly in an interactive terminal and you expect to apply the changes Terraform proposes, you can alternatively run -[`terraform apply`](/cli/commands/apply) directly. By default, the "apply" command -automatically generates a new plan and prompts for you to approve it. +[`terraform apply`](/terraform/cli/commands/apply) directly. By default, the "apply" command +automatically generates a new plan and prompts you to approve it. You can use the optional `-out=FILE` option to save the generated plan to a file on disk, which you can later execute by passing the file to -[`terraform apply`](/cli/commands/apply) as an extra argument. This two-step workflow +[`terraform apply`](/terraform/cli/commands/apply) as an extra argument. This two-step workflow is primarily intended for when -[running Terraform in automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). +[running Terraform in automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). If you run `terraform plan` without the `-out=FILE` option then it will create a _speculative plan_, which is a description of the effect of the plan but @@ -84,10 +85,10 @@ The remaining sections on this page describe the various options: The previous section describes Terraform's default planning behavior, which changes the remote system to match the changes you make to -your configuration. Terraform has two alternative planning modes, each of which creates a plan with a different intended outcome. These options are available for both `terraform plan` and [`terraform apply`](/cli/commands/apply). +your configuration. Terraform has two alternative planning modes, each of which creates a plan with a different intended outcome. These options are available for both `terraform plan` and [`terraform apply`](/terraform/cli/commands/apply). * **Destroy mode:** creates a plan whose goal is to destroy all remote objects - that currently exist, leaving an empty Terraform state. It is the same as running [`terraform destroy`](/cli/commands/destroy). Destroy mode can be useful for situations like transient development environments, where the managed objects cease to be useful once the development task is complete. + that currently exist, leaving an empty Terraform state. It is the same as running [`terraform destroy`](/terraform/cli/commands/destroy). Destroy mode can be useful for situations like transient development environments, where the managed objects cease to be useful once the development task is complete. Activate destroy mode using the `-destroy` command line option. @@ -113,55 +114,55 @@ one alternative mode at the same time. -> **Note:** In Terraform v0.15 and earlier, the `-destroy` option is supported only by the `terraform plan` command, and not by the `terraform apply` command. To create and apply a plan in destroy mode in -earlier versions you must run [`terraform destroy`](/cli/commands/destroy). +earlier versions you must run [`terraform destroy`](/terraform/cli/commands/destroy). -> **Note:** The `-refresh-only` option is available only in Terraform v0.15.4 and later. -> **Hands-on:** Try the [Use Refresh-Only Mode to Sync Terraform State](https://learn.hashicorp.com/tutorials/terraform/refresh) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Use Refresh-Only Mode to Sync Terraform State](/terraform/tutorials/state/refresh) tutorial. ## Planning Options -In addition to alternate [planning modes](#planning-modes), there are several options that can modify planning behavior. These options are available for both `terraform plan` and [`terraform apply`](/cli/commands/apply). +In addition to alternate [planning modes](#planning-modes), there are several options that can modify planning behavior. These options are available for both `terraform plan` and [`terraform apply`](/terraform/cli/commands/apply). - `-refresh=false` - Disables the default behavior of synchronizing the Terraform state with remote objects before checking for configuration changes. This can make the planning operation faster by reducing the number of remote API requests. However, setting `refresh=false` causes Terraform to ignore external changes, which could result in an incomplete or incorrect plan. You cannot use `refresh=false` in refresh-only planning mode because it would effectively disable the entirety of the planning operation. - `-replace=ADDRESS` - Instructs Terraform to plan to replace the - resource instance with the given address. This is helpful when one or more remote objects have become degraded, and you can use replacement objects with the same configuratation to align with immutable infrastructure patterns. Terraform will use a "replace" action if the specified resource would normally cause an "update" action or no action at all. Include this option multiple times to replace several objects at once. You cannot use `-replace` with the `-destroy` option, and it is only available from Terraform v0.15.2 onwards. For earlier versions, use [`terraform taint`](/cli/commands/taint) to achieve a similar result. + resource instance with the given address. This is helpful when one or more remote objects have become degraded, and you can use replacement objects with the same configuration to align with immutable infrastructure patterns. Terraform will use a "replace" action if the specified resource would normally cause an "update" action or no action at all. Include this option multiple times to replace several objects at once. You cannot use `-replace` with the `-destroy` option, and it is only available from Terraform v0.15.2 onwards. For earlier versions, use [`terraform taint`](/terraform/cli/commands/taint) to achieve a similar result. - `-target=ADDRESS` - Instructs Terraform to focus its planning efforts only - on resource instances which match the given address and on any objects that + on resource instances that match the given address and on any objects that those instances depend on. -> **Note:** Use `-target=ADDRESS` in exceptional circumstances only, such as recovering from mistakes or working around Terraform limitations. Refer to [Resource Targeting](#resource-targeting) for more details. - `-var 'NAME=VALUE'` - Sets a value for a single - [input variable](/language/values/variables) declared in the + [input variable](/terraform/language/values/variables) declared in the root module of the configuration. Use this option multiple times to set more than one variable. Refer to [Input Variables on the Command Line](#input-variables-on-the-command-line) for more information. - `-var-file=FILENAME` - Sets values for potentially many - [input variables](/language/values/variables) declared in the + [input variables](/terraform/language/values/variables) declared in the root module of the configuration, using definitions from a - ["tfvars" file](/language/values/variables#variable-definitions-tfvars-files). + ["tfvars" file](/terraform/language/values/variables#variable-definitions-tfvars-files). Use this option multiple times to include values from more than one file. There are several other ways to set values for input variables in the root module, aside from the `-var` and `-var-file` options. Refer to -[Assigning Values to Root Module Variables](/language/values/variables#assigning-values-to-root-module-variables) for more information. +[Assigning Values to Root Module Variables](/terraform/language/values/variables#assigning-values-to-root-module-variables) for more information. ### Input Variables on the Command Line You can use the `-var` command line option to specify values for -[input variables](/language/values/variables) declared in your +[input variables](/terraform/language/values/variables) declared in your root module. However, to do so will require writing a command line that is parsable both by your chosen command line shell _and_ Terraform, which can be complicated -for expressions involving lots of quotes and escape sequences. In most cases -we recommend using the `-var-file` option instead, and write your actual values +for expressions involving lots of quotes and escape sequences. In most cases, +we recommend using the `-var-file` option instead, and writing your actual values in a separate file so that Terraform can parse them directly, rather than interpreting the result of your shell's parsing. @@ -204,7 +205,7 @@ so we do not recommend using Terraform with PowerShell when you are on Windows. Use Windows Command Prompt instead. The appropriate syntax for writing the variable value is different depending -on the variable's [type constraint](/language/expressions/type-constraints). +on the variable's [type constraint](/terraform/language/expressions/type-constraints). The primitive types `string`, `number`, and `bool` all expect a direct string value with no special punctuation except that required by your shell, as shown in the above examples. For all other type constraints, including list, @@ -224,15 +225,15 @@ terraform plan -var "name=[\"a\", \"b\", \"c\"]" Similar constraints apply when setting input variables using environment variables. For more information on the various methods for setting root module input variables, see -[Assigning Values to Root Module Variables](/language/values/variables#assigning-values-to-root-module-variables). +[Assigning Values to Root Module Variables](/terraform/language/values/variables#assigning-values-to-root-module-variables). ### Resource Targeting -> **Hands-on:** Try the [Target resources](https://learn.hashicorp.com/tutorials/terraform/resource-targeting?in=terraform/state&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Target resources](/terraform/tutorials/state/resource-targeting?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. You can use the `-target` option to focus Terraform's attention on only a subset of resources. -You can use [resource address syntax](/cli/state/resource-addressing) +You can use [resource address syntax](/terraform/cli/state/resource-addressing) to specify the constraint. Terraform interprets the resource address as follows: * If the given address identifies one specific resource instance, Terraform @@ -264,7 +265,7 @@ of resources relates to configuration. Instead of using `-target` as a means to operate on isolated portions of very large configurations, prefer instead to break large configurations into several smaller configurations that can each be independently applied. -[Data sources](/language/data-sources) can be used to access +[Data sources](/terraform/language/data-sources) can be used to access information about resources created in other configurations, allowing a complex system architecture to be broken down into more manageable parts that can be updated independently. @@ -291,6 +292,8 @@ The available options are: * 1 = Error * 2 = Succeeded with non-empty diff (changes present) +- `-generate-config-out=PATH` - (Experimental) If `import` blocks are present in configuration, instructs Terraform to generate HCL for any imported resources not already present. The configuration is written to a new file at PATH, which must not already exist, or Terraform will error. If the plan fails for another reason, Terraform may still attempt to write configuration. + * `-input=false` - Disables Terraform's default behavior of prompting for input for root module input variables that have not otherwise been assigned a value. This option is particularly useful when running Terraform in @@ -300,7 +303,7 @@ The available options are: This implies `-input=false`, so the configuration must have no unassigned variable values to continue. - [machine-readable-ui]: /internals/machine-readable-ui + [machine-readable-ui]: /terraform/internals/machine-readable-ui * `-lock=false` - Don't hold a state lock during the operation. This is dangerous if others might concurrently run commands against the same @@ -335,13 +338,13 @@ The available options are: saved plan files as potentially-sensitive artifacts. * `-parallelism=n` - Limit the number of concurrent operations as Terraform - [walks the graph](/internals/graph#walking-the-graph). Defaults + [walks the graph](/terraform/internals/graph#walking-the-graph). Defaults to 10. For configurations using -[the `local` backend](/language/settings/backends/local) only, +[the `local` backend](/terraform/language/backend/local) only, `terraform plan` accepts the legacy command line option -[`-state`](/language/settings/backends/local#command-line-arguments). +[`-state`](/terraform/language/backend/local#command-line-arguments). ### Passing a Different Configuration Directory @@ -351,7 +354,7 @@ module instead of the current working directory. That usage was deprecated in Terraform v0.14 and removed in Terraform v0.15. If your workflow relies on overriding the root module directory, use -[the `-chdir` global option](/cli/commands/#switching-working-directory-with-chdir) +[the `-chdir` global option](/terraform/cli/commands#switching-working-directory-with-chdir) instead, which works across all commands and makes Terraform consistently look in the given directory for all files it would normally read or write in the current working directory. @@ -359,6 +362,6 @@ current working directory. If your previous use of this legacy pattern was also relying on Terraform writing the `.terraform` subdirectory into the current working directory even though the root module directory was overridden, use -[the `TF_DATA_DIR` environment variable](/cli/config/environment-variables#tf_data_dir) +[the `TF_DATA_DIR` environment variable](/terraform/cli/config/environment-variables#tf_data_dir) to direct Terraform to write the `.terraform` directory to a location other than the current working directory. diff --git a/website/docs/cli/commands/providers.mdx b/website/docs/cli/commands/providers.mdx index 9d2b6c7221..c1a2f33158 100644 --- a/website/docs/cli/commands/providers.mdx +++ b/website/docs/cli/commands/providers.mdx @@ -1,19 +1,18 @@ --- page_title: 'Command: providers' description: >- - The terraform providers command prints information about the providers + The `terraform providers` command prints information about the providers required in the current configuration. --- # Command: providers The `terraform providers` command shows information about the -[provider requirements](/language/providers/requirements) of the +[provider requirements](/terraform/language/providers/requirements) of the configuration in the current working directory, as an aid to understanding where each requirement was detected from. -This command also has several subcommands with different purposes, which -are listed in the navigation bar. +This command also has several subcommands with different purposes. ## Usage diff --git a/website/docs/cli/commands/providers/lock.mdx b/website/docs/cli/commands/providers/lock.mdx index 9a6de3ff9d..4ea85060ab 100644 --- a/website/docs/cli/commands/providers/lock.mdx +++ b/website/docs/cli/commands/providers/lock.mdx @@ -1,23 +1,26 @@ --- -page_title: 'Command: providers lock' +page_title: terraform providers lock command reference description: |- The `terraform providers lock` command adds new provider selection information to the dependency lock file without initializing the referenced providers. --- -# Command: terraform providers lock +# `terraform providers lock` command -The `terraform providers lock` consults upstream registries (by default) in -order to write provider dependency information into -[the dependency lock file](/language/files/dependency-lock). +The `terraform providers lock` adds new provider selection information to the dependency lock file without initializing the referenced providers. + +## Introduction + +When you run the command, Terraform consults upstream registries and writes provider dependency information into the +the dependency lock file. Refer to [Dependency Lock File](/terraform/language/files/dependency-lock) in the Terraform configuration language reference documentation for additional information about the lock file. The common way to update the dependency lock file is as a side-effect of normal provider installation during -[`terraform init`](/cli/commands/init), but there are several situations where that +[`terraform init`](/terraform/cli/commands/init), but there are several situations where that automatic approach may not be sufficient: * If you are running Terraform in an environment that uses - [alternative provider installation methods](/cli/config/config-file#provider-installation), + [alternative provider installation methods](/terraform/cli/config/config-file#provider-installation), such as filesystem or network mirrors, normal provider installation will not access the origin registry for a provider and therefore Terraform will not be able to populate all of the possible package checksums for the selected @@ -33,12 +36,10 @@ automatic approach may not be sufficient: on both Windows and Linux) and the upstream registry for a provider is unable to provide signed checksums using the latest hashing scheme, subsequent runs of Terraform on other platforms may - [add additional checksums to the lock file](/language/files/dependency-lock#new-provider-package-checksums). + [add additional checksums to the lock file](/terraform/language/files/dependency-lock#new-provider-package-checksums). You can avoid that by pre-populating hashes for all of the platforms you intend to use, using the `terraform providers lock` command. --> `terraform providers lock` is available only in Terraform v0.14 or later. - ## Usage Usage: `terraform providers lock [options] [providers...]` @@ -47,7 +48,7 @@ With no additional command line arguments, `terraform providers lock` will analyze the configuration in the current working directory to find all of the providers it depends on, and it will fetch the necessary data about those providers from their origin registries and then update -[the dependency lock file](/language/files/dependency-lock) to +[the dependency lock file](/terraform/language/files/dependency-lock) to include a selected version for each provider and all of the package checksums that are covered by the provider developer's cryptographic signature. @@ -72,7 +73,7 @@ You can customize the default behavior using the following additional option: * `-net-mirror=URL` - Direct Terraform to look for provider packages in the given network mirror service, instead of in upstream registries. The given URL must implement - [the Terraform provider network mirror protocol](/internals/provider-network-mirror-protocol). + [the Terraform provider network mirror protocol](/terraform/internals/provider-network-mirror-protocol). * `-platform=OS_ARCH` - Specify a platform you intend to use to work with this Terraform configuration. Terraform will ensure that the providers are all @@ -88,6 +89,13 @@ You can customize the default behavior using the following additional option: There is more detail on this option in the following section. +* `-enable-plugin-cache` - Enable the usage of the [globally configured plugin cache](/terraform/cli/config/config-file#provider-plugin-cache). + This will speed up the locking process. This is not enabled by default since + the plugin cache is not an authoritative source. As the + `terraform provider lock` command is used to ensure no untrusted provider + versions can be used installing the plugins from the cache is considered risky. + + ## Specifying Target Platforms In your environment you may, for example, have both developers who work with @@ -148,7 +156,7 @@ multiple times and specify a different subset of your providers each time. The `-fs-mirror` and `-net-mirror` options have the same meaning as `filesystem_mirror` and `network_mirror` blocks in -[the provider installation methods configuration](/cli/config/config-file#provider-installation), +[the provider installation methods configuration](/terraform/cli/config/config-file#provider-installation), but specify only a single method in order to be explicit about where you intend to derive the package checksum information from. @@ -165,4 +173,4 @@ If you wish, you can publish your in-house providers via an in-house provider registry, which will then allow locking and installation of those providers without any special options or additional CLI configuration. For more information, see -[the provider registry protocol](/internals/provider-registry-protocol). +[the provider registry protocol](/terraform/internals/provider-registry-protocol). diff --git a/website/docs/cli/commands/providers/mirror.mdx b/website/docs/cli/commands/providers/mirror.mdx index d6f9c8c8c8..0ef8bc47cb 100644 --- a/website/docs/cli/commands/providers/mirror.mdx +++ b/website/docs/cli/commands/providers/mirror.mdx @@ -1,12 +1,12 @@ --- -page_title: 'Command: providers mirror' +page_title: terraform providers mirror command reference description: |- The `terraform providers mirror` command downloads the providers required for the current configuration and copies them into a directory in the local filesystem. --- -# Command: terraform providers mirror +# `terraform providers mirror` command The `terraform providers mirror` command downloads the providers required for the current configuration and copies them into a directory in the local @@ -17,7 +17,7 @@ from provider registries as part of initializing the current working directory. Sometimes Terraform is running in an environment where that isn't possible, such as on an isolated network without access to the Terraform Registry. In that case, -[explicit installation method configuration](/cli/config/config-file#explicit-installation-method-configuration) +[explicit installation method configuration](/terraform/cli/config/config-file#explicit-installation-method-configuration) allows you to configure Terraform, when running on a particular system, to consult only a local filesystem directory where you've created a local mirror of the necessary plugins, and to skip accessing the upstream registry at all. @@ -39,7 +39,7 @@ themselves. Terraform will also generate various `.json` index files which contain suitable responses to implement -[the network mirror protocol](/internals/provider-network-mirror-protocol), +[the network mirror protocol](/terraform/internals/provider-network-mirror-protocol), if you upload the resulting directory to a static website host. Terraform ignores those index files when using the directory as a filesystem mirror, because the directory entries themselves are authoritative in that case. @@ -55,6 +55,10 @@ This command supports the following additional option: architecture. For example, `linux_amd64` selects the Linux operating system running on an AMD64 or x86_64 CPU. +* `-lock-file=false` - Ignore the provider lock file when fetching providers. +By default the mirror command will use the version info in the lock file if the +configuration directory has been previously initialized. + You can run `terraform providers mirror` again on an existing mirror directory to update it with new packages. For example, you can add packages for a new target platform by re-running the command with the desired new `-platform=...` diff --git a/website/docs/cli/commands/providers/schema.mdx b/website/docs/cli/commands/providers/schema.mdx index 868f210c0d..af0cf6eb3a 100644 --- a/website/docs/cli/commands/providers/schema.mdx +++ b/website/docs/cli/commands/providers/schema.mdx @@ -1,40 +1,30 @@ --- -page_title: 'Command: providers schema' +page_title: terraform providers schema command description: >- - The `terraform providers schema` command prints detailed schemas for the - providers used - - in the current configuration. + The `terraform providers schema` command prints detailed schemas for the providers declared in the configuration. --- -# Command: terraform providers schema +# `terraform providers schema` command -The `terraform providers schema` command is used to print detailed schemas for the providers used in the current configuration. - --> `terraform providers schema` requires **Terraform v0.12 or later**. +The `terraform providers schema` command print detailed schemas for the providers used in the current configuration. ## Usage -Usage: `terraform providers schema [options]` +``` +$ terraform providers schema [options] +``` The following flags are available: -- `-json` - Displays the schemas in a machine-readable, JSON format. +- `-json` - Displays the schemas in a machine-readable JSON format. The `-json` flag is required. -Please note that, at this time, the `-json` flag is a _required_ option. In future releases, this command will be extended to allow for additional options. + The output includes a `format_version` key, which has a default value of `"1.0"`. The semantics of this version are: -The output includes a `format_version` key, which as of Terraform 1.1.0 has -value `"1.0"`. The semantics of this version are: - -- We will increment the minor version, e.g. `"1.1"`, for backward-compatible - changes or additions. Ignore any object properties with unrecognized names to + - Versions between `1.0` and `2.0` are backward-compatible. You should ignore any object properties with unrecognized names to remain forward-compatible with future minor versions. -- We will increment the major version, e.g. `"2.0"`, for changes that are not - backward-compatible. Reject any input which reports an unsupported major + - Major versions are not backward-compatible to older versions. You should reject any input that reports an unsupported major version. - -We will introduce new major versions only within the bounds of -[the Terraform 1.0 Compatibility Promises](/language/v1-compatibility-promises). + - Refer to [Terraform 1.0 Compatibility Promises](/terraform/language/v1-compatibility-promises) for additional information about version support. ## Format Summary @@ -45,8 +35,10 @@ To avoid excessive repetition, we've split the complete format into several disc The JSON output format consists of the following objects and sub-objects: - [Providers Schema Representation](#providers-schema-representation) - the top-level object returned by `terraform providers schema -json` -- [Schema Representation](#schema-representation) - a sub-object of providers, resources, and data sources that describes their schema +- [Schema Representation](#schema-representation) - a sub-object of providers, resources, and data sources that describes their schema, along with function signatures - [Block Representation](#block-representation) - a sub-object of schemas that describes attributes and nested blocks +- [Function Representation](#function-representation) - a sub-object of functions that describes parameters, the return, and additional documentation +- [Parameter Representation](#parameter-representation) - a sub-object of function signatures that describes their type and additional documentation ## Providers Schema Representation @@ -71,6 +63,17 @@ The JSON output format consists of the following objects and sub-objects: // data source's schema "data_source_schemas": { "example_datasource_name": , + }, + + // "ephemeral_resource_schemas" map the resource type name to the + // resource's schema + "ephemeral_resource_schemas": { + "example_resource_name": , + }, + + // "functions" map the provider function name to the function definition + "functions": { + "example_function": } }, "example_provider_two": { … } @@ -137,15 +140,74 @@ A block representation contains "attributes" and "block_types" (which represent // list // set // map - "nesting_mode": "list", - "block": , + "nesting_mode": "list", + "block": , - // "min_items" and "max_items" set lower and upper - // limits on the number of child blocks allowed for - // the list and set modes. These are - // omitted for other modes. - "min_items": 1, - "max_items": 3 + // "min_items" and "max_items" set lower and upper + // limits on the number of child blocks allowed for + // the list and set modes. These are + // omitted for other modes. + "min_items": 1, + "max_items": 3 + } } } ``` + +## Function Representation + +A function representation describes the definition of a function. + +```javascript +{ + // "summary" is a shortened English-language description of + // the purpose of the function in Markdown. + "summary": "string", + + // "description" is a longer English-language description of + // the purpose and usage of the function in Markdown. + "description": "string", + + // "deprecation_message" when present signals that the function is deprecated + // and the message contains practitioner-facing actions for the deprecation. + "deprecation_message": "string", + + // "return_type" is a representation of a type specification + // that the function returns. + "return_type": "string", + + // "parameters" is an optional list of the positional parameters + // that the function accepts. + "parameters": [ + , + // ... + ], + + // "variadic_parameter" is an optional representation of the + // additional arguments that the function accepts after those + // matching with the fixed parameters. + "variadic_parameter": +} +``` + +## Parameter Representation + +A parameter representation describes a parameter to a function. + +```javascript +{ + // "name" is the internal name of the parameter + "name": "string", + + // "description" is an optional English-language description of + // the purpose and usage of the parameter in Markdown. + "description": "string", + + // "is_nullable" is true if null is acceptable value for the argument + "is_nullable": bool, + + // "type" is a representation of a type specification + // that the parameter's value must conform to. + "type": "string" +} +``` diff --git a/website/docs/cli/commands/push.mdx b/website/docs/cli/commands/push.mdx deleted file mode 100644 index 0e15e1590a..0000000000 --- a/website/docs/cli/commands/push.mdx +++ /dev/null @@ -1,14 +0,0 @@ ---- -page_title: 'Command: push' -description: >- - DISCONTINUED. Terraform Cloud and the modern "remote" backend have replaced - the old `terraform push` command. ---- - -# Command: push - -!> **Important:** The `terraform push` command is no longer functional. We recommend the [Terraform Cloud CLI integration](/cli/cloud) instead, which allows you to run remote operations in Terraform Cloud directly from the command line. - -The `terraform push` command was an early implementation of remote Terraform runs. It allowed teams to push a configuration to a remote run environment in a discontinued version of Terraform Enterprise. - -The legacy Terraform Enterprise version that supported `terraform push` is no longer available, and there are no remaining instances of that version in operation. Without a service to push to, the command is now completely non-functional. diff --git a/website/docs/cli/commands/refresh.mdx b/website/docs/cli/commands/refresh.mdx index 398e53b615..d58b385f7b 100644 --- a/website/docs/cli/commands/refresh.mdx +++ b/website/docs/cli/commands/refresh.mdx @@ -1,33 +1,19 @@ --- -page_title: 'Command: refresh' +page_title: terraform refresh command reference description: |- The `terraform refresh` command reads the current settings from all managed remote objects and updates the Terraform state to match. --- -# Command: refresh - -> **Hands-on:** Try the [Use Refresh-Only Mode to Sync Terraform State](https://learn.hashicorp.com/tutorials/terraform/refresh) tutorial on HashiCorp Learn. +# `terraform refresh` command The `terraform refresh` command reads the current settings from all managed -remote objects and updates the Terraform state to match. +remote objects and updates the Terraform state to match. This command is deprecated. Instead, add the `-refresh-only` flag to [`terraform apply`](/terraform/cli/commands/apply) and [`terraform plan`](/terraform/cli/commands/plan) commands. -~> _Warning:_ This command is deprecated, because its default behavior is -unsafe if you have misconfigured credentials for any of your providers. -See below for more information and recommended alternatives. +This does not modify your real remote objects, but it modifies the +[Terraform state](/terraform/language/state). -This won't modify your real remote objects, but it will modify the -[Terraform state](/language/state). - -You shouldn't typically need to use this command, because Terraform -automatically performs the same refreshing actions as a part of creating -a plan in both the -[`terraform plan`](/cli/commands/plan) -and -[`terraform apply`](/cli/commands/apply) -commands. This command is here primarily for backward compatibility, but -we don't recommend using it because it provides no opportunity to review -the effects of the operation before updating the state. +> **Hands-on:** Try the [Use Refresh-Only Mode to Sync Terraform State](/terraform/tutorials/state/refresh) tutorial. ## Usage @@ -40,7 +26,7 @@ terraform apply -refresh-only -auto-approve ``` Consequently, it supports all of the same options as -[`terraform apply`](/cli/commands/apply) except that it does not accept a saved +[`terraform apply`](/terraform/cli/commands/apply) except that it does not accept a saved plan file, it doesn't allow selecting a planning mode other than "refresh only", and `-auto-approve` is always enabled. diff --git a/website/docs/cli/commands/show.mdx b/website/docs/cli/commands/show.mdx index f956db19ba..cee62369cf 100644 --- a/website/docs/cli/commands/show.mdx +++ b/website/docs/cli/commands/show.mdx @@ -1,41 +1,37 @@ --- -page_title: 'Command: show' -description: >- - The `terraform show` command is used to provide human-readable output from a - state or plan file. This can be used to inspect a plan to ensure that the - planned operations are expected, or to inspect the current state as Terraform - sees it. +page_title: terraform show command reference +description: The `terraform show` command provides human-readable output from a state or plan file. --- -# Command: show +# `terraform show` command -The `terraform show` command is used to provide human-readable output -from a state or plan file. This can be used to inspect a plan to ensure +The `terraform show` command provides human-readable output +from a state or plan file. Use the command to inspect a plan to ensure that the planned operations are expected, or to inspect the current state as Terraform sees it. -Machine-readable output is generated by adding the `-json` command-line -flag. -> **Note:** When using the `-json` command-line flag, any sensitive values in Terraform state will be displayed in plain text. For more information, see -[Sensitive Data in State](/language/state/sensitive-data). +[Sensitive Data in State](/terraform/language/state/sensitive-data). ## JSON Output -For Terraform state files (including when no path is provided), -`terraform show -json` will show a JSON representation of the state. +Add the `-json` command-line flag to generate machine-readable output. -For Terraform plan files, `terraform show -json` will show a JSON representation +For Terraform state files, including when no path is provided, +`terraform show -json` shows a JSON representation of the state. + +For Terraform plan files, `terraform show -json` shows a JSON representation of the plan, configuration, and current state. -If you've updated providers which contain new schema versions since the state -was written, the state needs to be upgraded before it can be displayed with +If you updated providers that contain new schema versions since the state +was written, upgrade the state before so that Terraform can display it with `show -json`. If you are viewing a plan, it must be created without `-refresh=false`. If you are viewing a state file, run `terraform refresh` first. -The output format is covered in detail in [JSON Output Format](/internals/json-format). +The output format is covered in detail in [JSON Output Format](/terraform/internals/json-format). ## Usage diff --git a/website/docs/cli/commands/state/index.mdx b/website/docs/cli/commands/state/index.mdx index 7b2f10ac8a..0ba25c3bab 100644 --- a/website/docs/cli/commands/state/index.mdx +++ b/website/docs/cli/commands/state/index.mdx @@ -1,24 +1,28 @@ --- -page_title: 'Command: state' -description: The `terraform state` command is used for advanced state management. +page_title: terraform state commands reference +description: The `terraform state` group of commands enable advanced Terraform state management. --- -# State Command +# `terraform state` commands -The `terraform state` command is used for advanced state management. -As your Terraform usage becomes more advanced, there are some cases where -you may need to modify the [Terraform state](/language/state). -Rather than modify the state directly, the `terraform state` commands can -be used in many cases instead. +The `terraform state` commands enable advanced state management. -This command is a nested subcommand, meaning that it has further subcommands. -These subcommands are listed to the left. +## Introduction + +You can use the `terraform state` commands to modify the [Terraform state](/terraform/language/state) instead modifying the state directly. ## Usage Usage: `terraform state [options] [args]` -Please click a subcommand to the left for more information. +Refer to the following subcommands for additional information: + +- [`terraform state list`](/terraform/cli/commands/state/list) +- [`terraform state mv`](/terraform/cli/commands/state/mv) +- [`terraform state pull`](/terraform/cli/commands/state/pull) +- [`terraform state replace-provider`](/terraform/cli/commands/state/replace-provider) +- [`terraform state rm`](/terraform/cli/commands/state/rm) +- [`terraform state show`](/terraform/cli/commands/state/show) ## Remote State @@ -32,7 +36,7 @@ written to disk and the CLI usage is the same as if it were local state. All `terraform state` subcommands that modify the state write backup files. The path of these backup file can be controlled with `-backup`. -Subcommands that are read-only (such as [list](/cli/commands/state/list)) +Subcommands that are read-only (such as [list](/terraform/cli/commands/state/list)) do not write any backup files since they aren't modifying the state. Note that backups for state modification _can not be disabled_. Due to diff --git a/website/docs/cli/commands/state/list.mdx b/website/docs/cli/commands/state/list.mdx index 7bf09baa4f..3da2091d90 100644 --- a/website/docs/cli/commands/state/list.mdx +++ b/website/docs/cli/commands/state/list.mdx @@ -1,14 +1,14 @@ --- -page_title: 'Command: state list' +page_title: terraform state list command reference description: >- - The terraform state list command is used to list resources within a Terraform + The terraform state list command lists resources within a Terraform state. --- -# Command: state list +# `terraform state list` command -The `terraform state list` command is used to list resources within a -[Terraform state](/language/state). +The `terraform state list` command lists resources within a +[Terraform state](/terraform/language/state). ## Usage @@ -18,18 +18,18 @@ The command will list all resources in the state file matching the given addresses (if any). If no addresses are given, all resources are listed. The resources listed are sorted according to module depth order followed -by alphabetical. This means that resources that are in your immediate +alphabetically. This means that resources that are in your immediate configuration are listed first, and resources that are more deeply nested within modules are listed last. For complex infrastructures, the state can contain thousands of resources. To filter these, provide one or more patterns to the command. Patterns are -in [resource addressing format](/cli/state/resource-addressing). +in [resource addressing format](/terraform/cli/state/resource-addressing). The command-line flags are all optional. The following flags are available: * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". - Ignored when [remote state](/language/state/remote) is used. + Ignored when [remote state](/terraform/language/state/remote) is used. * `-id=id` - ID of resources to show. Ignored when unset. ## Example: All Resources diff --git a/website/docs/cli/commands/state/mv.mdx b/website/docs/cli/commands/state/mv.mdx index ed2159b145..4c29c01572 100644 --- a/website/docs/cli/commands/state/mv.mdx +++ b/website/docs/cli/commands/state/mv.mdx @@ -1,22 +1,18 @@ --- -page_title: 'Command: state mv' +page_title: terraform state mv command reference description: >- - The `terraform state mv` command changes bindings in Terraform state, - associating existing remote objects with new resource instances. + The `terraform state mv` command changes bindings in Terraform state so that existing remote objects bind to new resource instances. --- -# Command: state mv +# `terraform state mv` command -The main function of [Terraform state](/language/state) is -to track the bindings between resource instance addresses in your configuration -and the remote objects they represent. Normally Terraform automatically -updates the state in response to actions taken when applying a plan, such as -removing a binding for an remote object that has now been deleted. +The `terraform state mv` command changes bindings in Terraform state so that existing remote objects bind to new resource instances. -You can use `terraform state mv` in the less common situation where you wish +## Introduction + +Terraform automatically updates the state when you apply a plan, but you can use `terraform state mv` to retain an existing remote object but track it as a different resource -instance address in Terraform, such as if you have renamed a resource block -or you have moved it into a different module in your configuration. +instance address in Terraform. ## Usage @@ -28,7 +24,7 @@ remote objects currently associated with the source to be tracked instead by the destination. Both the source and destination addresses must use -[resource address syntax](/cli/state/resource-addressing), and +[resource address syntax](/terraform/cli/state/resource-addressing), and they must both refer to the same kind of object: you can only move a resource instance to another resource instance, a whole module instance to another whole module instance, etc. Furthermore, if you are moving a resource or @@ -66,22 +62,22 @@ This command also accepts the following options: returning an error. The duration syntax is a number followed by a time unit letter, such as "3s" for three seconds. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform state mv` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). -The legacy options [`-backup` and `-backup-out`](/language/settings/backends/local#command-line-arguments) +The legacy options [`-backup` and `-backup-out`](/terraform/language/backend/local#command-line-arguments) operate on a local state file only. Configurations using -[the `remote` backend](/language/settings/backends/remote) -must specify a local state file with the [`-state`](/language/settings/backends/local#command-line-arguments) -option in order to use the [`-backup` and `-backup-out`](/language/settings/backends/local#command-line-arguments) +[the `remote` backend](/terraform/language/backend/remote) +must specify a local state file with the [`-state`](/terraform/language/backend/local#command-line-arguments) +option in order to use the [`-backup` and `-backup-out`](/terraform/language/backend/local#command-line-arguments) options. For configurations using -[the `local` state mv](/language/settings/backends/local) only, +[the `local` state mv](/terraform/language/backend/local) only, `terraform state mv` also accepts the legacy options -[`-state`, `-state-out`, `-backup`, and `-backup-out`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, `-backup`, and `-backup-out`](/terraform/language/backend/local#command-line-arguments). ## Example: Rename a Resource @@ -133,7 +129,7 @@ terraform state mv module.app module.parent.module.app ## Example: Move a Particular Instance of a Resource using `count` -A resource defined with [the `count` meta-argument](/language/meta-arguments/count) +A resource defined with [the `count` meta-argument](/terraform/language/meta-arguments/count) has multiple instances that are each identified by an integer. You can select a particular instance by including an explicit index in your given address: @@ -158,7 +154,7 @@ The above examples show the typical quoting syntax for Unix-style shells. ## Example: Move a Resource configured with for_each -A resource defined with [the `for_each` meta-argument](/language/meta-arguments/for_each) +A resource defined with [the `for_each` meta-argument](/terraform/language/meta-arguments/for_each) has multiple instances that are each identified by an string. You can select a particular instance by including an explicit key in your given address. diff --git a/website/docs/cli/commands/state/pull.mdx b/website/docs/cli/commands/state/pull.mdx index 15c372e81a..839b3d85b3 100644 --- a/website/docs/cli/commands/state/pull.mdx +++ b/website/docs/cli/commands/state/pull.mdx @@ -1,15 +1,12 @@ --- -page_title: 'Command: state pull' +page_title: terraform state pull command reference description: >- - The `terraform state pull` command is used to manually download and output the - state from remote state. + The `terraform state pull` command downloads and outputs state information from a remote state or local state. --- -# Command: state pull +# `terraform state pull` command -The `terraform state pull` command is used to manually download and output -the state from [remote state](/language/state/remote). This command also -works with local state. +The `terraform state pull` downloads and outputs state information from a [remote state](/terraform/language/state/remote) or local state. ## Usage @@ -27,4 +24,4 @@ You cannot use this command to inspect the Terraform version of the remote state, as it will always be converted to the current Terraform version before output. --> **Note:** Terraform state files must be in UTF-8 format without a byte order mark (BOM). For PowerShell on Windows, use `Set-Content` to automatically encode files in UTF-8 format. For example, run `terraform state pull | sc terraform.tfstate`. +-> **Note:** Terraform state files must be in UTF-8 format without a byte order mark (BOM). For PowerShell on Windows, use `Set-Content` to automatically encode files in UTF-8 format. For example, run `terraform state pull | Set-Content terraform.tfstate`. diff --git a/website/docs/cli/commands/state/push.mdx b/website/docs/cli/commands/state/push.mdx index 8ce576c17c..5acf7962ae 100644 --- a/website/docs/cli/commands/state/push.mdx +++ b/website/docs/cli/commands/state/push.mdx @@ -1,29 +1,24 @@ --- -page_title: 'Command: state push' -description: The `terraform state push` command pushes items to the Terraform state. +page_title: terraform state push command reference +description: The `terraform state push` command uploads a state file to the Terraform state. --- -# Command: state push +# `terraform state push` command -The `terraform state push` command is used to manually upload a local -state file to [remote state](/language/state/remote). This command also -works with local state. - -This command should rarely be used. It is meant only as a utility in case -manual intervention is necessary with the remote state. +The `terraform state push` command uploads a local state file to [remote state](/terraform/language/state/remote) or a local state. We only recommend using this command when you must manually modify the remote state. ## Usage Usage: `terraform state push [options] PATH` This command pushes the state specified by PATH to the currently -configured [backend](/language/settings/backends). +configured [backend](/terraform/language/backend). If PATH is "-" then the state data to push is read from stdin. This data is loaded completely into memory and verified prior to being written to the destination state. --> **Note:** Terraform state files must be in UTF-8 format without a byte order mark (BOM). For PowerShell on Windows, use `Set-Content` to automatically encode files in UTF-8 format. For example, run `terraform state push | sc terraform.tfstate`. +-> **Note:** Terraform state files must be in UTF-8 format without a byte order mark (BOM). For PowerShell on Windows, use `Set-Content` to automatically encode files in UTF-8 format. For example, run `terraform state push | Set-Content terraform.tfstate`. Terraform will perform a number of safety checks to prevent you from making changes that appear to be unsafe: @@ -42,7 +37,7 @@ Both of these safety checks can be disabled with the `-force` flag. **This is not recommended.** If you disable the safety checks and are pushing state, the destination state will be overwritten. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform state push` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). diff --git a/website/docs/cli/commands/state/replace-provider.mdx b/website/docs/cli/commands/state/replace-provider.mdx index 63fc4a75ef..690277a049 100644 --- a/website/docs/cli/commands/state/replace-provider.mdx +++ b/website/docs/cli/commands/state/replace-provider.mdx @@ -1,14 +1,14 @@ --- -page_title: 'Command: state replace-provider' +page_title: terraform state replace-provider command reference description: >- The `terraform state replace-provider` command replaces the provider for resources in the Terraform state. --- -# Command: state replace-provider +# `terraform state replace-provider` command -The `terraform state replace-provider` command is used to replace the provider -for resources in a [Terraform state](/language/state). +The `terraform state replace-provider` command replaces the provider +for resources in a [Terraform state](/terraform/language/state). ## Usage @@ -32,15 +32,15 @@ This command also accepts the following options: - `-lock-timeout=0s` - Duration to retry a state lock. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform state replace-provider` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). For configurations using -[the `local` state rm](/language/settings/backends/local) only, +[the `local` state](/terraform/language/backend/local) only, `terraform state replace-provider` also accepts the legacy options -[`-state`, `-state-out`, and `-backup`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, and `-backup`](/terraform/language/backend/local#command-line-arguments). ## Example diff --git a/website/docs/cli/commands/state/rm.mdx b/website/docs/cli/commands/state/rm.mdx index e7031865d4..92cd4ad36c 100644 --- a/website/docs/cli/commands/state/rm.mdx +++ b/website/docs/cli/commands/state/rm.mdx @@ -1,35 +1,28 @@ --- -page_title: 'Command: state rm' +page_title: terraform state rm command reference description: >- - The `terraform state rm` command removes bindings from the Terraform state, - causing Terraform to "forget about" existing objects. + The `terraform state rm` command removes bindings between resource instances defined in the Terraform configuration and corresponding remote objects. --- -# Command: state rm +# `terraform state rm` command -The main function of [Terraform state](/language/state) is -to track the bindings between resource instance addresses in your configuration -and the remote objects they represent. Normally Terraform automatically -updates the state in response to actions taken when applying a plan, such as -removing a binding for a remote object that has now been deleted. +The `terraform state rm` command removes the binding to an existing remote object without first destroying it. The remote object continues +to exist but is no longer managed by Terraform. -You can use `terraform state rm` in the less common situation where you wish -to remove a binding to an existing remote object without first destroying it, -which will effectively make Terraform "forget" the object while it continues -to exist in the remote system. +Instead of using the `terraform state rm` command, you can use `removed` blocks to remove resources. You can remove more than one resource at a time, and you can review removals as part of your normal plan and apply workflow. [Learn more about using `removed` blocks with resources](/terraform/language/resources/syntax#removing-resources) and [using `removed` blocks with modules](/terraform/language/modules/syntax#removing-modules). ## Usage Usage: `terraform state rm [options] ADDRESS...` Terraform will search the state for any instances matching the given -[resource address](/cli/state/resource-addressing), and remove +[resource address](/terraform/cli/state/resource-addressing), and remove the record of each one so that Terraform will no longer be tracking the corresponding remote objects. This means that although the objects will still continue to exist in the remote system, a subsequent -[`terraform plan`](/cli/commands/plan) +[`terraform plan`](/terraform/cli/commands/plan) will include an action to create a new object for each of the "forgotten" instances. Depending on the constraints imposed by the remote system, creating those objects might fail if their names or other identifiers conflict with @@ -49,15 +42,15 @@ This command also accepts the following options: returning an error. The duration syntax is a number followed by a time unit letter, such as "3s" for three seconds. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform state rm` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). For configurations using -[the `local` state rm](/language/settings/backends/local) only, +[the `local` state rm](/terraform/language/backend/local) only, `terraform state rm` also accepts the legacy options -[`-state`, `-state-out`, and `-backup`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, and `-backup`](/terraform/language/backend/local#command-line-arguments). ## Example: Remove all Instances of a Resource @@ -92,7 +85,7 @@ $ terraform state rm 'module.foo' ## Example: Remove a Particular Instance of a Resource using `count` -A resource defined with [the `count` meta-argument](/language/meta-arguments/count) +A resource defined with [the `count` meta-argument](/terraform/language/meta-arguments/count) has multiple instances that are each identified by an integer. You can select a particular instance by including an explicit index in your given address: @@ -107,7 +100,7 @@ The above shows the typical quoting syntax for Unix-style shells. ## Example: Remove a Particular Instance of a Resource using `for_each` -A resource defined with [the `for_each` meta-argument](/language/meta-arguments/for_each) +A resource defined with [the `for_each` meta-argument](/terraform/language/meta-arguments/for_each) has multiple instances that are each identified by an string. You can select a particular instance by including an explicit key in your given address. diff --git a/website/docs/cli/commands/state/show.mdx b/website/docs/cli/commands/state/show.mdx index 547aa83c74..30c9036d32 100644 --- a/website/docs/cli/commands/state/show.mdx +++ b/website/docs/cli/commands/state/show.mdx @@ -1,15 +1,15 @@ --- -page_title: 'Command: state show' +page_title: terraform state show command reference description: >- - The `terraform state show` command is used to show the attributes of a single + The `terraform state show` command shows the attributes of a single resource in the Terraform state. --- -# Command: state show +# `terraform state show` command -The `terraform state show` command is used to show the attributes of a +The `terraform state show` command shows the attributes of a single resource in the -[Terraform state](/language/state). +[Terraform state](/terraform/language/state). ## Usage @@ -20,16 +20,16 @@ state file that matches the given address. This command requires an address that points to a single resource in the state. Addresses are -in [resource addressing format](/cli/state/resource-addressing). +in [resource addressing format](/terraform/cli/state/resource-addressing). The command-line flags are all optional. The following flags are available: * `-state=path` - Path to the state file. Defaults to "terraform.tfstate". - Ignored when [remote state](/language/state/remote) is used. + Ignored when [remote state](/terraform/language/state/remote) is used. The output of `terraform state show` is intended for human consumption, not programmatic consumption. To extract state data for use in other software, use -[`terraform show -json`](/cli/commands/show#json-output) and decode the result +[`terraform show -json`](/terraform/cli/commands/show#json-output) and decode the result using the documented structure. ## Example: Show a Resource @@ -60,7 +60,7 @@ $ terraform state show 'module.foo.packet_device.worker' ## Example: Show a Resource configured with count The example below shows the first instance of a `packet_device` resource named `worker` configured with -[`count`](/language/meta-arguments/count): +[`count`](/terraform/language/meta-arguments/count): ```shell $ terraform state show 'packet_device.worker[0]' @@ -68,8 +68,7 @@ $ terraform state show 'packet_device.worker[0]' ## Example: Show a Resource configured with for_each -The example below shows the `"example"` instance of a `packet_device` resource named `worker` configured with -[`for_each`](/language/meta-arguments/for_each): +The following example shows the `"example"` instance of a `packet_device` resource named `worker` configured with the [`for_each`](/terraform/language/meta-arguments/for_each) meta-argument. You must place the resource name in single quotes when it contains special characters like double quotes. Linux, Mac OS, and UNIX: diff --git a/website/docs/cli/commands/taint.mdx b/website/docs/cli/commands/taint.mdx index b37a34b3eb..eede6b8d18 100644 --- a/website/docs/cli/commands/taint.mdx +++ b/website/docs/cli/commands/taint.mdx @@ -1,22 +1,18 @@ --- -page_title: 'Command: taint' +page_title: terraform taint command reference description: |- - The `terraform taint` command informs Terraform that a particular object - is damaged or degraded. + The `terraform taint` command marks specified objects in the Terraform state as tainted. --- -# Command: taint +# `terraform taint` command -The `terraform taint` command informs Terraform that a particular object has -become degraded or damaged. Terraform represents this by marking the -object as "tainted" in the Terraform state, and Terraform will -propose to replace it in the next plan you create. +The `terraform taint` command marks specified objects in the Terraform state as tainted. Use the `terraform taint` command when objects become degraded or damaged. Terraform prompts you to replace the tainted objects in the next plan you create. -~> **Warning:** This command is deprecated. For Terraform v0.15.2 and later, we recommend using the `-replace` option with `terraform apply` instead (details below). +This command is deprecated. Instead, add the `-replace` option to your [`terraform apply` command](/terraform/cli/commands/apply). ## Recommended Alternative -For Terraform v0.15.2 and later, we recommend using the [`-replace` option](/cli/commands/plan#replace-address) with `terraform apply` to force Terraform to replace an object even though there are no configuration changes that would require it. +For Terraform v0.15.2 and later, we recommend using the [`-replace` option](/terraform/cli/commands/plan#replace-address) with `terraform apply` to force Terraform to replace an object even though there are no configuration changes that would require it. ``` $ terraform apply -replace="aws_instance.example[0]" @@ -32,7 +28,7 @@ $ terraform taint [options]
The `address` argument is the address of the resource to mark as tainted. The address is in -[the resource address syntax](/cli/state/resource-addressing) syntax, +[the resource address syntax](/terraform/cli/state/resource-addressing), as shown in the output from other commands, such as: - `aws_instance.foo` @@ -55,11 +51,11 @@ This command accepts the following options: returning an error. The duration syntax is a number followed by a time unit letter, such as "3s" for three seconds. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) only, `terraform taint` +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform taint` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). For configurations using -[the `local` backend](/language/settings/backends/local) only, +[the `local` backend](/terraform/language/backend/local) only, `terraform taint` also accepts the legacy options -[`-state`, `-state-out`, and `-backup`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, and `-backup`](/terraform/language/backend/local#command-line-arguments). diff --git a/website/docs/cli/commands/test.mdx b/website/docs/cli/commands/test.mdx index 282c217105..1e1e2bed7a 100644 --- a/website/docs/cli/commands/test.mdx +++ b/website/docs/cli/commands/test.mdx @@ -1,13 +1,229 @@ --- -page_title: 'Command: test' -description: Part of the ongoing design research for module integration testing. +page_title: terraform test command reference +description: >- + The `terraform test` command loads and executes Terraform testing files. --- -# Command: test +# `terraform test` command -The `terraform test` command is currently serving as part of -[the module integration testing experiment](/language/modules/testing-experiment). +The `terraform test` command loads and exectures Terraform testing files. -It's not ready for routine use, but if you'd be interested in trying the -prototype functionality then we'd love to hear your feedback. See the -experiment details page linked above for more information. +## Introduction + +The `terraform test` command and the test file syntax help module authors validate and test their shared modules. You can also use the `terraform test` command to validate root modules. + +## Usage + +Usage: `terraform test [options]` + +This command searches the current directory and the specified testing directory for Terraform testing files and executes the specified tests. By default, the directory containing test files is named `tests`. Refer to [Tests](/terraform/language/tests) for more details on test files. + +Terraform then executes a series of Terraform plan or apply commands according to the test files' specifications, and also validates the relevant plan and state files according to the test files' specifications. + +!> **Warning:** The Terraform test command can create real infrastructure than can cost you money. Refer to the [Terraform Test Cleanup](#terraform-test-cleanup) section for best practices on ensuring created infrastructure is destroyed. + +## General Options + +The following options apply to the Terraform `terraform test` command: + +* `-cloud-run=` - This test run executes remotely on HCP Terraform within the specified Terraform [private registry](/terraform/language/modules/sources#terraform-registry) module. + +* `-filter=testfile` - Limits the `terraform test` operation to the specified test files. + +* `-json` - Displays machine-readable JSON output for your testing results. + +* `-junit-xml=` - Saves a test report in JUnit XML format to the specified file. This is currently incompatible with remote test execution using the the `-cloud-run` option. The file path must be relative or absolute. + +* `-test-directory=` - Overrides the directory that Terraform looks into for test files. Note that Terraform always loads testing files within the main configuration directory. The default testing directory is `tests`. + +* `-verbose` - Prints out the plan or state for each `run` block within a test file, based on the `command` attribute of each `run` block. + +* `-parallelism=` - Specifies the number of plan/apply operations to execute in parallel within a single test run. The default is 10. + +## State Management + +Each Terraform test file will maintain all Terraform state it requires within memory as it executes, starting empty. This state is entirely separate from any existing state for the configuration under test, so you can safely execute Terraform test commands without affecting any live infrastructure. + +### Terraform Test Cleanup + +The Terraform `terraform test` command creates _real_ infrastructure. Once Terraform fully executes each test file, Terraform attempts to destroy any remaining infrastructure. If it cannot do this, Terraform reports a list of resources it created but could not destroy. + +You should monitor the output of the test command closely to ensure Terraform removes the infrastructure it created or perform manual cleanup if not. We recommend creating dedicated testing accounts within the target providers that you can routinely and safely purge to ensure any accidental and costly resources aren't left behind. + +Terraform also provides diagnostics explaining why it could not automatically clean up. You should review these diagnostics to ensure that future clean-up operations are successful. + +## HCP Terraform execution + +You can execute tests remotely on HCP Terraform using the `-cloud-run` option. + +The `-cloud-run` option accepts a [private registry module source](/terraform/language/modules/sources#terraform-registry). This option associates the test run with your specified private module within the HCP Terraform user interface. + +You must provide a module from a _private_ registry, not the public Terraform registry. + +You must execute [`terraform login`](/terraform/cli/commands/login) before using this option, and ensure that your `hostname` argument matches the private registry hostname of your target module. + +## Example: Test Directory Structure and Commands + +The following directory structure represents an example directory tree for a Terraform module with tests and a setup module. + +``` +project/ +|-- main.tf +|-- outputs.tf +|-- terraform.tf +|-- variables.tf +|-- tests/ +| |-- validations.tftest.hcl +| |-- outputs.tftest.hcl +|-- testing/ + |-- setup/ + |-- main.tf + |-- outputs.tf + |-- terraform.tf + |-- variables.tf +``` + +At the root directory of the project, there are some typical Terraform configuration files: `main.tf`, `outputs.tf`, `terraform.tf`, and `variables.tf`. The test files, `validations.tftest.hcl` and `outputs.tftest.hcl`, are within the default tests directory: `tests`. + +In addition, a [setup module](/terraform/language/tests#modules) for the tests exists within the `testing` directory. + +In order to execute the tests you should run `terraform test` from the root configuration directory as if running `terraform plan` or `terraform apply`. Despite the actual test files being in the nested `tests` directory, Terraform executes from the main configuration directory. + +Specific test files can be executed using the `-filter` option. + +Linux, Mac OS, and UNIX: + +```shell +terraform test -filter=tests/validations.tftest.hcl +``` + +PowerShell: + +```shell +terraform test -filter='tests\validations.tftest.hcl' +``` + +Windows `cmd.exe`: + +```shell +terraform test -filter=tests\validations.tftest.hcl +``` + +### Alternate Test Directories + +In the above example the tests are in the default testing directory of `tests`. Test files can also be included directly within the main configuration directory: + +``` +project/ +|-- main.tf +|-- outputs.tf +|-- terraform.tf +|-- variables.tf +|-- validations.tftest.hcl +|-- outputs.tftest.hcl +|-- testing/ + |-- setup/ + |-- main.tf + |-- outputs.tf + |-- terraform.tf + |-- variables.tf +``` + +The location of the testing files does not affect the operation of `terraform test`. All references to, and absolute file paths within, the testing files should be relative to the main configuration directory. + +You can also use the `-test-directory` argument to change the location of the testing files. For example, `terraform test -test-directory=testing` would instruct Terraform to load tests from the directory `testing` instead of `tests`. + +The testing directory must be beneath the main configuration directory, but it can be nested many times. + +> Note: Test files within the root configuration directory are always loaded, regardless of the `-test-directory` value. + +We do not recommend changing the default test directory. The option for customization is included for configuration authors who may have included a `tests` submodule in their configuration before the `terraform test` command was released. In general, the default test directory of `tests` should always be used. + + +## Example: Test Output Format Options + +Below is a contrived example of Terraform testing that makes assertions about the values of local variables `true` and `false`. +There are two test files: one contains a passing test, and one contains a failing test. + +```hcl +# main.tf +locals { + true = "true" + false = "true" # incorrect, should be "false"! +} +``` + +The assertion that `local.true == "true"` in `example_1.tftest.hcl` will pass: + +```hcl +# example_1.tftest.hcl +run "true_is_true" { + assert { + condition = local.true == "true" + error_message = "local.true did not match expected value" + } +} +``` + +The assertion that `local.false == "false"` in `example_2.tftest.hcl` will fail: + +```hcl +# example_2.tftest.hcl +run "false_is_false" { + assert { + condition = local.false == "false" + error_message = "local.false did not match expected value" + } +} +``` + +### Test output in JUnit XML format, saved to file + +Below is the output of the `terraform test -junit-xml=./output.xml` command using the example files above. +The test output is: + +* Printed to the terminal in the default, human-readable format. +* Also saved in JUnit XML format in the file specified by the flag. + +Below is the contents of the resulting `output.xml` file: + +```xml + + + + + + + + + + +``` + +If a run block contains a failing assertion, the `` element will contain a `` element that includes the error message and further details. + +A `` element could contain a `` element that includes details about why a test was skipped. This could either be due to an error causing remaining run blocks to be skipped, or due to the command being interrupted. + +#### Mapping Terraform test command concepts to JUnit XML format + +The test report generated when using `-junit-xml` maps Terraform test command concepts to JUnit XML format according to the table below: + +| Terraform test concept | Element in JUnit XML output | +| ----------------------------------| --------------------------------------------| +| Test directory | `` | +| Test file | `` | +| Run block | `` | +| Run block assertion | None; details are included only on failure | +| Test failure | `` | +| Test was skipped | `` | +| Test stopped due to error | `` | +| Any unhandled warnings or errors | `` | \ No newline at end of file diff --git a/website/docs/cli/commands/untaint.mdx b/website/docs/cli/commands/untaint.mdx index 7d247638ea..bd2163ae39 100644 --- a/website/docs/cli/commands/untaint.mdx +++ b/website/docs/cli/commands/untaint.mdx @@ -1,14 +1,16 @@ --- -page_title: 'Command: untaint' +page_title: terraform untaint command reference description: |- - The `terraform untaint` command tells Terraform that an object is functioning - correctly, even though its creation failed or it was previously manually - marked as degraded. + The `terraform untaint` command removes the `tainted` status from infrastructure objects tracked in the Terraform state data. --- -# Command: untaint +# `terraform untaint` command -Terraform has a marker called "tainted" which it uses to track that an object +This topic provides reference information about the `terraform untaint` command. + +## Introduction + +Terraform has a marker called `tainted` which it uses to track that an object might be damaged and so a future Terraform plan ought to replace it. Terraform automatically marks an object as "tainted" if an error occurs during @@ -16,7 +18,7 @@ a multi-step "create" action, because Terraform can't be sure that the object was left in a fully-functional state. You can also manually mark an object as "tainted" using the deprecated command -[`terraform taint`](/cli/commands/taint), although we no longer recommend that +[`terraform taint`](/terraform/cli/commands/taint), although we no longer recommend that workflow. If Terraform currently considers a particular object as tainted but you've @@ -38,7 +40,7 @@ terraform apply -replace="aws_instance.example[0]" Usage: `terraform untaint [options] address` -The `address` argument is a [resource address](/cli/state/resource-addressing) +The `address` argument is a [resource address](/terraform/cli/state/resource-addressing) identifying a particular resource instance which is currently tainted. This command also accepts the following options: @@ -61,12 +63,12 @@ This command also accepts the following options: if you are running Terraform in a context where its output will be rendered by a system that cannot interpret terminal formatting. -For configurations using the [Terraform Cloud CLI integration](/cli/cloud) or the [`remote` backend](/language/settings/backends/remote) +For configurations using the [HCP Terraform CLI integration](/terraform/cli/cloud) or the [`remote` backend](/terraform/language/backend/remote) only, `terraform untaint` also accepts the option -[`-ignore-remote-version`](/cli/cloud/command-line-arguments#ignore-remote-version). +[`-ignore-remote-version`](/terraform/cli/cloud/command-line-arguments#ignore-remote-version). For configurations using -[the `local` backend](/language/settings/backends/local) only, +[the `local` backend](/terraform/language/backend/local) only, `terraform untaint` also accepts the legacy options -[`-state`, `-state-out`, and `-backup`](/language/settings/backends/local#command-line-arguments). +[`-state`, `-state-out`, and `-backup`](/terraform/language/backend/local#command-line-arguments). diff --git a/website/docs/cli/commands/validate.mdx b/website/docs/cli/commands/validate.mdx index f662ed6245..962875f0fa 100644 --- a/website/docs/cli/commands/validate.mdx +++ b/website/docs/cli/commands/validate.mdx @@ -1,15 +1,15 @@ --- -page_title: 'Command: validate' +page_title: terraform validate command reference description: >- - The `terraform validate` command is used to validate the syntax of the - terraform files. + The `terraform validate` command validates the syntax of Terraform configuration files in a directory. --- -# Command: validate +# `terraform validate` command The `terraform validate` command validates the configuration files in a -directory, referring only to the configuration and not accessing any remote -services such as remote state, provider APIs, etc. +directory. It does not validate remote services, such as remote state or provider APIs. + +## Introduction Validate runs checks that verify whether a configuration is syntactically valid and internally consistent, regardless of any provided variables or @@ -17,7 +17,7 @@ existing state. It is thus primarily useful for general verification of reusable modules, including correctness of attribute names and value types. It is safe to run this command automatically, for example as a post-save -check in a text editor or as a test step for a re-usable module in a CI +check in a text editor or as a test step for a reusable module in a CI system. Validation requires an initialized working directory with any referenced plugins and modules installed. To initialize a working directory for validation without accessing any configured backend, use: @@ -26,7 +26,7 @@ Validation requires an initialized working directory with any referenced plugins $ terraform init -backend=false ``` -To verify configuration in the context of a particular run (a particular +To verify the configuration in the context of a particular run (a particular target workspace, input variable values, etc), use the `terraform plan` command instead, which includes an implied validation check. @@ -65,7 +65,7 @@ value `"1.0"`. The semantics of this version are: version. We will introduce new major versions only within the bounds of -[the Terraform 1.0 Compatibility Promises](/language/v1-compatibility-promises). +[the Terraform 1.0 Compatibility Promises](/terraform/language/v1-compatibility-promises). In the normal case, Terraform will print a JSON object to the standard output stream. The top-level JSON object will have the following properties: @@ -89,7 +89,7 @@ stream. The top-level JSON object will have the following properties: The nested objects in `diagnostics` have the following properties: -- `severity` (string): A string keyword, currently either `"error"` or +- `severity` (string): A string keyword, either `"error"` or `"warning"`, indicating the diagnostic severity. The presence of errors causes Terraform to consider a configuration to be @@ -107,7 +107,7 @@ The nested objects in `diagnostics` have the following properties: Summaries are typically short, single sentences, but can sometimes be longer as a result of returning errors from subsystems that are not designed to - return full diagnostics, where the entire error message therefore becomes the + return full diagnostics, where the entire error message becomes the summary. In those cases, the summary might include newline characters which a renderer should honor when presenting the message visually to a user. @@ -119,26 +119,24 @@ The nested objects in `diagnostics` have the following properties: reference. Detail messages are often multiple paragraphs and possibly interspersed with - non-paragraph lines, so tools which aim to present detail messages to the + non-paragraph lines, so tools that aim to present detailed messages to the user should distinguish between lines without leading spaces, treating them as paragraphs, and lines with leading spaces, treating them as preformatted text. Renderers should then soft-wrap the paragraphs to fit the width of the rendering container, but leave the preformatted lines unwrapped. - Some Terraform detail messages currently contain an approximation of bullet - lists using ASCII characters to mark the bullets. This is not currently a - contractural formatting convention and so renderers should avoid depending on + Some Terraform detail messages contain an approximation of bullet + lists using ASCII characters to mark the bullets. This is not a + contractual formatting convention, so renderers should avoid depending on it and should instead treat those lines as either paragraphs or preformatted - text per the rules above. A future version of this format may define some - additional rules for processing other text conventions, but will do so within - the bounds of the rules above to achieve backward-compatibility. + text. - `range` (object): An optional object referencing a portion of the configuration source code that the diagnostic message relates to. For errors, this will typically indicate the bounds of the specific block header, attribute, or expression which was detected as invalid. - A source range is an object with a property `filename` which gives the + A source range is an object with a property `filename` that gives the filename as a relative path from the current working directory, and then two properties `start` and `end` which are both themselves objects describing source positions, as described below. @@ -154,7 +152,7 @@ The nested objects in `diagnostics` have the following properties: - `context` (string): An optional summary of the root context of the diagnostic. For example, this might be the resource block containing the - expression which triggered the diagnostic. For some diagnostics this + expression that triggered the diagnostic. For some diagnostics, this information is not available, and then this property will be `null`. - `code` (string): A snippet of Terraform configuration including the @@ -195,13 +193,11 @@ object, has the following properties: A `start` position is inclusive while an `end` position is exclusive. The exact positions used for particular error messages are intended for human -interpretation only and subject to change in future versions of Terraform due -either to improvements to the error reporting or changes in implementation -details of the language parser/evaluator. +interpretation only. ### Expression Value -An expression value object gives additional information about a value which is +An expression value object gives additional information about a value that is part of the expression which triggered the diagnostic. This is especially useful when using `for_each` or similar constructs, in order to identify exactly which values are responsible for an error. The object has two properties: @@ -209,8 +205,7 @@ exactly which values are responsible for an error. The object has two properties - `traversal` (string): An HCL-like traversal string, such as `var.instance_count`. Complex index key values may be elided, so this will not always be valid, parseable HCL. The contents of this string are intended - to be human-readable and are subject to change in future versions of - Terraform. + to be human-readable. - `statement` (string): A short English-language fragment describing the value of the expression when the diagnostic was triggered. The contents of this diff --git a/website/docs/cli/commands/version.mdx b/website/docs/cli/commands/version.mdx index bcfa5f0b76..d2f0d3a4e8 100644 --- a/website/docs/cli/commands/version.mdx +++ b/website/docs/cli/commands/version.mdx @@ -1,29 +1,28 @@ --- -page_title: 'Command: version' +page_title: terraform version command reference description: >- - The terraform version command displays the Terraform version and the version + The terraform version command prints the Terraform version and the version of all installed plugins. --- -# Command: version +# `terraform version` command -The `terraform version` displays the current version of Terraform and all +The `terraform version` command prints the current version of the Terraform binary and all installed plugins. ## Usage Usage: `terraform version [options]` -With no additional arguments, `version` will display the version of Terraform, -the platform it's installed on, installed providers, and the results of upgrade -and security checks [unless disabled](/cli/commands#upgrade-and-security-bulletin-checks). +With no additional arguments, `version` displays the version of Terraform, +the platform it is installed on, installed providers, and the results of upgrade +and security checks unless disabled. Refer to [Upgrade and Security Bulletin Checks](/terraform/cli/commands#upgrade-and-security-bulletin-checks) for additional information. + +## Flags This command has one optional flag: -* `-json` - If specified, the version information is formatted as a JSON object, - and no upgrade or security information is included. - --> **Note:** Platform information was added to the `version` command in Terraform 0.15. +* `-json` - Formats version information as a JSON object. No upgrade or security information is included. ## Example @@ -36,7 +35,7 @@ on darwin_amd64 + provider registry.terraform.io/hashicorp/null v3.0.0 Your version of Terraform is out of date! The latest version -is X.Y.Z. You can update by downloading from https://www.terraform.io/downloads.html +is X.Y.Z. You can update by downloading from https://developer.hashicorp.com/terraform/install ``` As JSON: diff --git a/website/docs/cli/commands/workspace/delete.mdx b/website/docs/cli/commands/workspace/delete.mdx index cadfa6459e..4b4f3f3c6c 100644 --- a/website/docs/cli/commands/workspace/delete.mdx +++ b/website/docs/cli/commands/workspace/delete.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Command: workspace delete' -description: The terraform workspace delete command is used to delete a workspace. +page_title: terraform workspace delete command reference +description: The terraform workspace delete command deletes the specified workspace. --- -# Command: workspace delete +# `terraform workspace delete` command -The `terraform workspace delete` command is used to delete an existing workspace. +The `terraform workspace delete` command deletes the specified workspace. ## Usage @@ -13,20 +13,23 @@ Usage: `terraform workspace delete [OPTIONS] NAME [DIR]` This command will delete the specified workspace. -To delete an workspace, it must already exist, it must have an empty state, -and it must not be your current workspace. If the workspace state is not empty, +To delete a workspace, it must already exist, it must not be tracking resources, +and it must not be your current workspace. If the workspace is tracking resources, Terraform will not allow you to delete it unless the `-force` flag is specified. -If you delete a workspace with a non-empty state (via `-force`), then resources +Additionally, different [backends](/terraform/language/backend#backend-types) may implement other +restrictions on whether a workspace is considered safe to delete without the `-force` flag, such as whether the workspace is locked. + +If you delete a workspace which is tracking resources (via `-force`), then resources may become "dangling". These are resources that physically exist but that -Terraform can no longer manage. This is sometimes preferred: you want -Terraform to stop managing resources so they can be managed some other way. +Terraform can no longer manage. This is sometimes preferred: you may want +Terraform to stop managing resources, so they can be managed some other way. Most of the time, however, this is not intended and so Terraform protects you from getting into this situation. -The command-line flags are all optional. The only supported flag is: +The command-line flags are all optional. The only supported flags are: -* `-force` - Delete the workspace even if its state is not empty. Defaults to false. +* `-force` - Delete the workspace even if it is tracking resources. After deletion, Terraform can no longer track or manage the workspace's infrastructure. Defaults to false. * `-lock=false` - Don't hold a state lock during the operation. This is dangerous if others might concurrently run commands against the same workspace. diff --git a/website/docs/cli/commands/workspace/index.mdx b/website/docs/cli/commands/workspace/index.mdx index 2d67b1d7c5..c7d3dba497 100644 --- a/website/docs/cli/commands/workspace/index.mdx +++ b/website/docs/cli/commands/workspace/index.mdx @@ -1,18 +1,16 @@ --- -page_title: 'Command: workspace' -description: The workspace command helps you manage workspaces. +page_title: terraform workspace command reference +description: The terraform workspace command helps you manage workspaces. --- -# Command: workspace +# `terraform workspace` command -The `terraform workspace` command is used to manage -[workspaces](/language/state/workspaces). +The `terraform workspace` command group helps you manage [workspaces](/terraform/language/state/workspaces). -This command is a container for further subcommands. These subcommands are -listed in the navigation bar. +This command is a container for further subcommands that each have their own page in the documentation. ## Usage Usage: `terraform workspace [options] [args]` -Please choose a subcommand from the navigation for more information. +Choose a subcommand page for more information. diff --git a/website/docs/cli/commands/workspace/list.mdx b/website/docs/cli/commands/workspace/list.mdx index d7d2e6eee2..74a0b266e2 100644 --- a/website/docs/cli/commands/workspace/list.mdx +++ b/website/docs/cli/commands/workspace/list.mdx @@ -1,11 +1,11 @@ --- -page_title: 'Command: workspace list' -description: The terraform workspace list command is used to list all existing workspaces. +page_title: terraform workspace list command reference +description: The terraform workspace list command lists all existing workspaces. --- -# Command: workspace list +# `terraform workspace list` command -The `terraform workspace list` command is used to list all existing workspaces. +The `terraform workspace list` command lists all existing workspaces. ## Usage diff --git a/website/docs/cli/commands/workspace/new.mdx b/website/docs/cli/commands/workspace/new.mdx index 28b4d1c30d..5b07fae6de 100644 --- a/website/docs/cli/commands/workspace/new.mdx +++ b/website/docs/cli/commands/workspace/new.mdx @@ -1,9 +1,9 @@ --- -page_title: 'Command: workspace new' -description: The terraform workspace new command is used to create a new workspace. +page_title: terraform workspace new command reference +description: The terraform workspace new command creates a new workspace with the specified name. --- -# Command: workspace new +# `terraform workspace new` command The `terraform workspace new` command is used to create a new workspace. diff --git a/website/docs/cli/commands/workspace/select.mdx b/website/docs/cli/commands/workspace/select.mdx index f22f7aabeb..17816e23be 100644 --- a/website/docs/cli/commands/workspace/select.mdx +++ b/website/docs/cli/commands/workspace/select.mdx @@ -1,12 +1,11 @@ --- -page_title: 'Command: workspace select' -description: The terraform workspace select command is used to choose a workspace. +page_title: terraform workspace select` command reference +description: The terraform workspace select command selects a workspace. --- -# Command: workspace select +# `terraform workspace select` command -The `terraform workspace select` command is used to choose a different -workspace to use for further operations. +The `terraform workspace select` selects a different workspace to use for further operations. ## Usage @@ -15,6 +14,10 @@ Usage: `terraform workspace select NAME [DIR]` This command will select another workspace. The named workspace must already exist. +The supported flags are: + +* `-or-create` - If the workspace that is being selected does not exist, create it. Default is `false`. + ## Example ``` diff --git a/website/docs/cli/commands/workspace/show.mdx b/website/docs/cli/commands/workspace/show.mdx index c6063f22fd..548573ebba 100644 --- a/website/docs/cli/commands/workspace/show.mdx +++ b/website/docs/cli/commands/workspace/show.mdx @@ -1,17 +1,17 @@ --- -page_title: 'Command: workspace show' -description: The terraform workspace show command is used to output the current workspace. +page_title: terraform workspace show command reference +description: The terraform workspace show command outputs the current workspace. --- -# Command: workspace show +# `terraform workspace show` command -The `terraform workspace show` command is used to output the current workspace. +The `terraform workspace show` command outputs the current workspace. ## Usage Usage: `terraform workspace show` -The command will display the current workspace. +The command displays the current workspace. ## Example diff --git a/website/docs/cli/config/config-file.mdx b/website/docs/cli/config/config-file.mdx index 761baec6d7..bbf7200792 100644 --- a/website/docs/cli/config/config-file.mdx +++ b/website/docs/cli/config/config-file.mdx @@ -1,15 +1,20 @@ --- -page_title: CLI Configuration +page_title: Create a Terraform CLI configuration file description: >- - Learn to use the CLI configuration file to customize your CLI settings, - including credentials, plugin caching, provider installation methods, etc. + Learn how to create a `.terraformrc` or `terraform.rc` file to define Terraform CLI settings, including credentials, plugin caching, and provider installation. --- -# CLI Configuration File (`.terraformrc` or `terraform.rc`) +# Create a Terraform CLI configuration file + +This topic describes how create a configuration file to customize the behavior of the Terraform CLI. + +## Introduction The CLI configuration file configures per-user settings for CLI behaviors, which apply across all Terraform working directories. This is separate from -[your infrastructure configuration](/language). +[your infrastructure configuration](/terraform/language). +You can define custom configurations in file called `.terraformrc` or `terraform.rc` +depending on the host operating system as explained below. ## Locations @@ -31,7 +36,7 @@ as just `terraform.rc`. Use `dir` from PowerShell or Command Prompt to confirm the filename. The location of the Terraform CLI configuration file can also be specified -using the `TF_CLI_CONFIG_FILE` [environment variable](/cli/config/environment-variables). +using the `TF_CLI_CONFIG_FILE` [environment variable](/terraform/cli/config/environment-variables). Any such file should follow the naming pattern `*.tfrc`. ## Configuration File Syntax @@ -50,16 +55,16 @@ disable_checkpoint = true The following settings can be set in the CLI configuration file: -* `credentials` - configures credentials for use with Terraform Cloud or +* `credentials` - configures credentials for use with HCP Terraform or Terraform Enterprise. See [Credentials](#credentials) below for more information. * `credentials_helper` - configures an external helper program for the storage - and retrieval of credentials for Terraform Cloud or Terraform Enterprise. + and retrieval of credentials for HCP Terraform or Terraform Enterprise. See [Credentials Helpers](#credentials-helpers) below for more information. * `disable_checkpoint` — when set to `true`, disables - [upgrade and security bulletin checks](/cli/commands#upgrade-and-security-bulletin-checks) + [upgrade and security bulletin checks](/terraform/cli/commands#upgrade-and-security-bulletin-checks) that require reaching out to HashiCorp-provided network services. * `disable_checkpoint_signature` — when set to `true`, allows the upgrade and @@ -76,12 +81,12 @@ The following settings can be set in the CLI configuration file: ## Credentials -[Terraform Cloud](/cloud) provides a number of remote network +[HCP Terraform](https://cloud.hashicorp.com/products/terraform) provides a number of remote network services for use with Terraform, and -[Terraform Enterprise](/enterprise) allows hosting those +[Terraform Enterprise](/terraform/enterprise) allows hosting those services inside your own infrastructure. For example, these systems offer both -[remote operations](/cloud-docs/run/cli) and a -[private module registry](/cloud-docs/registry). +[remote operations](/terraform/cloud-docs/run/cli) and a +[private module registry](/terraform/cloud-docs/registry). When interacting with Terraform-specific network services, Terraform expects to find API tokens in CLI configuration files in `credentials` blocks: @@ -92,27 +97,27 @@ credentials "app.terraform.io" { } ``` -If you are running the Terraform CLI interactively on a computer with a web browser, you can use [the `terraform login` command](/cli/commands/login) +If you are running the Terraform CLI interactively on a computer with a web browser, you can use [the `terraform login` command](/terraform/cli/commands/login) to get credentials and automatically save them in the CLI configuration. If not, you can manually write `credentials` blocks. You can have multiple `credentials` blocks if you regularly use services from multiple hosts. Many users will configure only one, for either -Terraform Cloud (at `app.terraform.io`) or for their organization's own +HCP Terraform (at `app.terraform.io`) or for their organization's own Terraform Enterprise host. Each `credentials` block contains a `token` argument giving the API token to use for that host. -~> **Important:** If you are using Terraform Cloud or Terraform Enterprise, +~> **Important:** If you are using HCP Terraform or Terraform Enterprise, the token provided must be either a -[user token](/cloud-docs/users-teams-organizations/users#api-tokens) +[user token](/terraform/cloud-docs/users-teams-organizations/users#api-tokens) or a -[team token](/cloud-docs/users-teams-organizations/api-tokens#team-api-tokens); +[team token](/terraform/cloud-docs/users-teams-organizations/api-tokens#team-api-tokens); organization tokens cannot be used for command-line Terraform actions. -> **Note:** The credentials hostname must match the hostname in your module sources and/or backend configuration. If your Terraform Enterprise instance is available at multiple hostnames, use only one of them consistently. -Terraform Cloud responds to API calls at both its current hostname +HCP Terraform responds to API calls at both its current hostname `app.terraform.io`, and its historical hostname `atlas.hashicorp.com`. ### Environment Variable Credentials @@ -161,7 +166,7 @@ for a specific hostname by writing a `credentials` block alongside the Terraform does not include any credentials helpers in the main distribution. To learn how to write and install your own credentials helpers to integrate with existing in-house credentials management systems, see -[the guide to Credentials Helper internals](/internals/credentials-helpers). +[the guide to Credentials Helper internals](/terraform/internals/credentials-helpers). ### Credentials Source Priority Order @@ -270,7 +275,7 @@ The following are the two supported installation method types: use the `https:` scheme and end with a trailing slash. Terraform expects the given URL to be a base URL for an implementation of - [the provider network mirror protocol](/internals/provider-network-mirror-protocol), + [the provider network mirror protocol](/terraform/internals/provider-network-mirror-protocol), which is designed to be relatively easy to implement using typical static website hosting mechanisms. @@ -387,6 +392,57 @@ grow to contain several unused versions which you must delete manually. safe. The provider installer's behavior in environments with multiple `terraform init` calls is undefined. +### Allowing the Provider Plugin Cache to break the dependency lock file + +~> **Note:** The option described in is for unusual and exceptional situations +only. Do not set this option unless you are sure you need it and you fully +understand the consequences of enabling it. + +By default Terraform will use packages from the global cache directory only +if they match at least one of the checksums recorded in the +[dependency lock file](/terraform/language/files/dependency-lock) +for that provider. This ensures that Terraform can always +generate a complete and correct dependency lock file entry the first time you +use a new provider in a particular configuration. + +However, we know that in some special situations teams have been unable to use +the dependency lock file as intended, and so they don't include it in their +version control as recommended and instead let Terraform re-generate it each +time it installs providers. + +For those teams that don't preserve the dependency lock file in their version +control systems between runs, Terraform allows an additional CLI Configuration +setting which tells Terraform to always treat a package in the cache directory +as valid even if there isn't already an entry in the dependency lock file +to confirm it: + +```hcl +plugin_cache_may_break_dependency_lock_file = true +``` + +Alternatively, you can set the environment variable +`TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE` to any value other than the +empty string or `0`, which is equivalent to the above setting. + +Setting this option gives Terraform CLI permission to create an incomplete +dependency lock file entry for a provider if that would allow Terraform to +use the cache to install that provider. In that situation the dependency lock +file will be valid for use on the current system but may not be valid for use on +another computer with a different operating system or CPU architecture, because +it will include only a checksum of the package in the global cache. + +We recommend that most users leave this option unset, in which case Terraform +will always install a provider from upstream the first time you use it with +a particular configuration, but can then re-use the cache entry on later runs +once the dependency lock file records valid checksums for the provider package. + +~> **Note:** The Terraform team intends to improve the dependency lock file +mechanism in future versions so that it will be usable in more situations. At +that time this option will become silently ignored. If your workflow relies on +the use of this option, please open a GitHub issue to share details about your +situation so that we can consider how to support it without breaking the +dependency lock file. + ### Development Overrides for Provider Developers -> **Note:** Development overrides work only in Terraform v0.14 and later. @@ -429,7 +485,7 @@ provider_installation { With development overrides in effect, the `terraform init` command will still attempt to select a suitable published version of your provider to install and record in -[the dependency lock file](/language/files/dependency-lock) +[the dependency lock file](/terraform/language/files/dependency-lock) for future use, but other commands like `terraform apply` will disregard the lock file's entry for `hashicorp/null` and will use the given directory instead. Once your new changes are included in a diff --git a/website/docs/cli/config/environment-variables.mdx b/website/docs/cli/config/environment-variables.mdx index f03b4f6335..70144c6fde 100644 --- a/website/docs/cli/config/environment-variables.mdx +++ b/website/docs/cli/config/environment-variables.mdx @@ -1,17 +1,20 @@ --- -page_title: Environment Variables +page_title: Terraform CLI environment variables reference description: >- - Learn to use environment variables to change Terraform's default behavior. - Configure log content and output, set variables, and more. + Terraform environment variables let you customize the Terraform CLI's default behavior. + Learn about the Terraform CLI environment variables. --- -# Environment Variables +# Terraform CLI environment variables reference + +This topic contains reference information about the environment variables you can use with the Terraform CLI. + +## Introduction Terraform refers to a number of environment variables to customize various aspects of its behavior. None of these environment variables are required -when using Terraform, but they can be used to change some of Terraform's -default behaviors in unusual situations, or to increase output verbosity -for debugging. +when using Terraform, but you can use them to change some of Terraform's +default behaviors or to increase output verbosity for debugging. ## TF_LOG @@ -27,7 +30,7 @@ To disable, either unset it, or set it to `off`. For example: export TF_LOG=off ``` -For more on debugging Terraform, check out the section on [Debugging](/internals/debugging). +For more on debugging Terraform, check out the section on [Debugging](/terraform/internals/debugging). ## TF_LOG_PATH @@ -37,7 +40,7 @@ This specifies where the log should persist its output to. Note that even when ` export TF_LOG_PATH=./terraform.log ``` -For more on debugging Terraform, check out the section on [Debugging](/internals/debugging). +For more on debugging Terraform, check out the section on [Debugging](/terraform/internals/debugging). ## TF_INPUT @@ -58,7 +61,7 @@ export TF_VAR_alist='[1,2,3]' export TF_VAR_amap='{ foo = "bar", baz = "qux" }' ``` -For more on how to use `TF_VAR_name` in context, check out the section on [Variable Configuration](/language/values/variables). +For more on how to use `TF_VAR_name` in context, check out the section on [Variable Configuration](/terraform/language/values/variables). ## TF_CLI_ARGS and TF_CLI_ARGS_name @@ -114,7 +117,7 @@ export TF_WORKSPACE=your_workspace Using this environment variable is recommended only for non-interactive usage, since in a local shell environment it can be easy to forget the variable is set and apply changes to the wrong state. -For more information regarding workspaces, check out the section on [Using Workspaces](/language/state/workspaces). +For more information regarding workspaces, check out the section on [Using Workspaces](/terraform/language/state/workspaces). ## TF_IN_AUTOMATION @@ -127,7 +130,7 @@ applications. This is a purely cosmetic change to Terraform's human-readable output, and the exact output differences can change between minor Terraform versions. -For more details, see [Running Terraform in Automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). +For more details, see [Running Terraform in Automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS). ## TF_REGISTRY_DISCOVERY_RETRY @@ -137,32 +140,38 @@ the remote registry client will attempt for client connection errors or ## TF_REGISTRY_CLIENT_TIMEOUT -The default client timeout for requests to the remote registry is 10s. `TF_REGISTRY_CLIENT_TIMEOUT` can be configured and increased during extraneous circumstances. +The default client timeout for requests to the remote registry is 10s. `TF_REGISTRY_CLIENT_TIMEOUT` can be configured and increased during exceptional circumstances. ```shell export TF_REGISTRY_CLIENT_TIMEOUT=15 ``` +## TF_STATE_PERSIST_INTERVAL + +The interval in seconds that Terraform attempts to persist state to a remote backend during an apply operation. The default minimum interval for all remote backends is 20 seconds. Backends may override the default minimum value. If the value of `TF_STATE_PERSIST_INTERVAL` is lower than the default interval specified by a remote backend, the default interval will be used. + +```shell +export TF_STATE_PERSIST_INTERVAL=100 +``` + ## TF_CLI_CONFIG_FILE -The location of the [Terraform CLI configuration file](/cli/config/config-file). +The location of the [Terraform CLI configuration file](/terraform/cli/config/config-file). ```shell export TF_CLI_CONFIG_FILE="$HOME/.terraformrc-custom" ``` -## TF_IGNORE +Note that `TERRAFORM_CONFIG` is a deprecated alias for the `TF_CLI_CONFIG_FILE` variable. We recommend using `TF_CLI_CONFIG_FILE` instead of the deprecated `TERRAFORM_CONFIG` variable. -If `TF_IGNORE` is set to "trace", Terraform will output debug messages to display ignored files and folders. This is useful when debugging large repositories with `.terraformignore` files. +## TF_PLUGIN_CACHE_DIR -```shell -export TF_IGNORE=trace -``` +The `TF_PLUGIN_CACHE_DIR` environment variable is an alternative way to set [the `plugin_cache_dir` setting in the CLI configuration](/terraform/cli/config/config-file#provider-plugin-cache). -For more details on `.terraformignore`, please see [Excluding Files from Upload with .terraformignore](/language/settings/backends/remote#excluding-files-from-upload-with-terraformignore). +You can also use `TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE` to activate [the transitional compatibility setting `plugin_cache_may_break_dependency_lock_file`](/terraform/cli/config/config-file#allowing-the-provider-plugin-cache-to-break-the-dependency-lock-file). -## Terraform Cloud CLI Integration +## HCP Terraform CLI Integration -The CLI integration with Terraform Cloud lets you use Terraform Cloud and Terraform Enterprise on the command line. The integration requires including a `cloud` block in your Terraform configuration. You can define its arguments directly in your configuration file or supply them through environment variables, which can be useful for non-interactive workflows like Continuous Integration (CI). +The CLI integration with HCP Terraform lets you use HCP Terraform and Terraform Enterprise on the command line. The integration requires including a `cloud` block in your Terraform configuration. You can define its arguments directly in your configuration file or supply them through environment variables, which can be useful for non-interactive workflows like Continuous Integration (CI). -Refer to [Terraform Cloud Settings](/cli/cloud/settings#environment-variables) for a full list of `cloud` block environment variables. +Refer to [HCP Terraform Settings](/terraform/cli/cloud/settings#environment-variables) for a full list of `cloud` block environment variables. diff --git a/website/docs/cli/config/index.mdx b/website/docs/cli/config/index.mdx index 188e7ad614..e329f751bd 100644 --- a/website/docs/cli/config/index.mdx +++ b/website/docs/cli/config/index.mdx @@ -1,24 +1,22 @@ --- -page_title: CLI Configuration - Terraform CLI +page_title: Terraform CLI configuration overview description: >- - Find documentation about the CLI config file and customizing Terraform - environment variables. + The CLI configuration file and supported Terraform environment variables let you customize Terraform CLI behavior. --- -# CLI Configuration +# Terraform CLI configuration overview -Terraform CLI can be configured with some global settings, which are separate +You can configure the Terraform CLI in global settings, which are separate from any Terraform configuration and which apply across all working directories. -We've designed Terraform such that an average user running Terraform CLI -interactively will not need to interact with any of these settings. As a result, +The default behavior of the Terraform CLI is suitable in most cases. As a result, most of the global settings relate to advanced or automated workflows, or -unusual environmental conditions like running Terraform on an airgapped +unusual environmental conditions, such as running Terraform on an air-gapped instance. -- The [CLI config file](/cli/config/config-file) configures provider +- The [CLI config file](/terraform/cli/config/config-file) configures provider installation and security features. -- Several [environment variables](/cli/config/environment-variables) can +- Several [environment variables](/terraform/cli/config/environment-variables) can configure Terraform's inputs and outputs; this includes some alternate ways to provide information that is usually passed on the command line or read from the state of the shell. diff --git a/website/docs/cli/import/importability.mdx b/website/docs/cli/import/importability.mdx deleted file mode 100644 index 88a200a4e1..0000000000 --- a/website/docs/cli/import/importability.mdx +++ /dev/null @@ -1,22 +0,0 @@ ---- -page_title: 'Import: Resource Importability' -description: |- - Each resource in Terraform must implement some basic logic to become - importable. As a result, not all Terraform resources are currently importable. ---- - -# Resource Importability - -Each resource in Terraform must implement some basic logic to become -importable. As a result, not all Terraform resources are currently importable. -For those resources that support import, they are documented at the bottom of -each resource documentation page, under the Import heading. If you find a -resource that you want to import and Terraform reports that it is not -importable, please report an issue in the relevant provider repository. - -Converting a resource to be importable is also relatively simple, so if -you're interested in contributing that functionality, the Terraform team -would be grateful. - -To make a resource importable, please see -[Extending Terraform: Resources — Import](/plugin/sdkv2/resources/import). diff --git a/website/docs/cli/import/index.mdx b/website/docs/cli/import/index.mdx index d9503e0b46..6d1e7b13a3 100644 --- a/website/docs/cli/import/index.mdx +++ b/website/docs/cli/import/index.mdx @@ -1,43 +1,42 @@ --- -page_title: Import +page_title: Import existing infrastructure resources description: >- - Terraform can import and manage existing infrastructure. This can help you - transition your infrastructure to Terraform. + Terraform lets you import existing infrastructure into state so that you can begin managing your infrastructure as code. --- -# Import +# Import existing resources overview -> **Hands-on:** Try the [Import Terraform Configuration](https://learn.hashicorp.com/tutorials/terraform/state-import?in=terraform/state&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +This topic provides an overview of the Terraform commands that let you import existing infrastructure resources so that you can manage them with Terraform. -Terraform is able to import existing infrastructure. This allows you take -resources you've created by some other means and bring it under Terraform -management. +> **Hands-on:** Try the [Import Terraform Configuration](/terraform/tutorials/state/state-import?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -This is a great way to slowly transition infrastructure to Terraform, or -to be able to be confident that you can use Terraform in the future if it -potentially doesn't support every feature you need today. -~> Warning: Terraform expects that each remote object it is managing will be -bound to only one resource address, which is normally guaranteed by Terraform -itself having created all objects. If you import existing objects into Terraform, -be careful to import each remote object to only one Terraform resource address. -If you import the same object multiple times, Terraform may exhibit unwanted -behavior. For more information on this assumption, see -[the State section](/language/state). +## Workflows -## Currently State Only +You can import an existing resource to state from the Terraform CLI. You can also perform import operations using HCP Terraform. To import multiple resources, use the `import` block. -The current implementation of Terraform import can only import resources -into the [state](/language/state). It does not generate configuration. A future -version of Terraform will also generate configuration. +### Import to state -Because of this, prior to running `terraform import` it is necessary to write -manually a `resource` configuration block for the resource, to which the -imported object will be mapped. +Before you run `terraform import` you must manually write a `resource` configuration block for the resource. The resource block describes where Terraform should map the imported object. -While this may seem tedious, it still gives Terraform users an avenue for -importing existing resources. +The `terraform import` CLI command can only import resources into the [state](/terraform/language/state). Importing via the CLI does _not_ generate configuration. If you want to generate the accompanying configuration for imported resources, [use the `import` block instead](/terraform/language/import). -## Terraform Cloud +Terraform expects each remote object to be bound to a single resource address. You should import each remote object to one Terraform resource address. Importing the same object multiple times may result in unwanted behavior. Refer to [State](/terraform/language/state) for more details. -When you use Terraform on the command line with Terraform Cloud, many commands (e.g., `apply`) run inside your Terraform Cloud environment. However, the `import` command runs locally, so it will not have access to information from Terraform Cloud. To successfully perform an import, you may need to set local variables equivalent to any remote workspace variables in Terraform Cloud. +### HCP Terraform + +When you use Terraform on the command line with HCP Terraform, commands such as `apply` run inside your HCP Terraform environment. However, the `import` command runs locally, so it does not have access to information from HCP Terraform. To successfully perform an import, you may need to set local variables equivalent to any remote workspace variables in HCP Terraform. + +### Import multiple resources + +You can specify multiple resources in the `import` block to import more than one resource at a time. You can also review imports as part of your normal plan and apply workflow. Refer to the [`import` block reference ](/terraform/language/import) in the Terraform configuration language documentation for addtitional information. + +## Resource importability + +Each resource in Terraform must implement some basic logic to become +importable. As a result, you cannot import all Terraform resources. + +The resources that you can import are documented at the bottom of +each resource documentation page in the [Terraform Registry](https://registry.terraform.io/). If you have issues importing a resource, report an issue in the relevant provider repository. + +To make a resource importable, refer to [Extending Terraform: Resources — Import](/terraform/plugin/sdkv2/resources/import). \ No newline at end of file diff --git a/website/docs/cli/import/usage.mdx b/website/docs/cli/import/usage.mdx index 522dd3c83a..ea9ddc2def 100644 --- a/website/docs/cli/import/usage.mdx +++ b/website/docs/cli/import/usage.mdx @@ -1,15 +1,23 @@ --- -page_title: 'Import: Usage' -description: The `terraform import` command is used to import existing infrastructure. +page_title: Import existing resources +description: Learn now to use the `terraform import` command to import existing infrastructure resources. --- -# Import Usage +# Import existing resources -> **Hands-on:** Try the [Import Terraform Configuration](https://learn.hashicorp.com/tutorials/terraform/state-import?in=terraform/state&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +This topic describes how to use the `terraform import` command to import existing infrastructure resources so that you can manage them as code. -Use the `terraform import` command to import existing infrastructure to Terraform state. +> **Hands-on:** Try the [Import Terraform Configuration](/terraform/tutorials/state/state-import?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. + +## Overview + +Use the `terraform import` command to import existing infrastructure to Terraform state. The `terraform import` command can only import one resource at a time. It cannot simultaneously import an entire collection of resources, such as an AWS VPC. + +Complete the following steps to import resources: + +1. Add the resource you want to manage with Terraform to your Terraform configuration. +1. Run the `terraform import` command. -The `terraform import` command can only import one resource at a time. It cannot simultaneously import an entire collection of resources, like an AWS VPC. ~> Warning: Terraform expects that each remote object it is managing will be bound to only one resource address, which is normally guaranteed by Terraform @@ -17,26 +25,26 @@ itself having created all objects. If you import existing objects into Terraform be careful to import each remote object to only one Terraform resource address. If you import the same object multiple times, Terraform may exhibit unwanted behavior. For more information on this assumption, see -[the State section](/language/state). +[the State section](/terraform/language/state). -To import a resource, first write a resource block for it in your -configuration, establishing the name by which it will be known to Terraform: +## Add the resource to your configuration -``` +Write a resource block for the resource you want to import in your configuration. +Provide a name for the resource, which is a unique ID that you can use to reference the resource elsewhere in the configuration. + +In the following example, the imported resource is an AWS instance named `example`: + +```hcl resource "aws_instance" "example" { # ...instance configuration... } ``` -The name "example" here is local to the module where it is declared and is -chosen by the configuration author. This is distinct from any ID issued by -the remote system, which may change over time while the resource name -remains constant. +You do not have to complete the body of the resource block. Instead, you can finish defining arguments after the instance is imported. -If desired, you can leave the body of the resource block blank for now and -return to fill it in once the instance is imported. +## Run the `terraform import` command -Now `terraform import` can be run to attach an existing instance to this +Run `terraform import` to attach an existing instance to the resource configuration: ```shell @@ -45,13 +53,13 @@ $ terraform import aws_instance.example i-abcd1234 This command locates the AWS EC2 instance with ID `i-abcd1234`. Then it attaches the existing settings of the instance, as described by the EC2 API, to the -name `aws_instance.example` of a module. In this example the module path +name `aws_instance.example` of a module. In this example, the module path implies that the root module is used. Finally, the mapping is saved in the Terraform state. It is also possible to import to resources in child modules, using their paths, and to single instances of a resource with `count` or `for_each` set. See -[_Resource Addressing_](/cli/state/resource-addressing) for more +[_Resource Addressing_](/terraform/cli/state/resource-addressing) for more details on how to specify a target resource. The syntax of the given ID is dependent on the resource type being imported. @@ -72,9 +80,9 @@ multiple resources are imported. For example, an AWS network ACL imports an `aws_network_acl` but also one `aws_network_acl_rule` for each rule. In this scenario, the secondary resources will not already exist in -configuration, so it is necessary to consult the import output and create -a `resource` block in configuration for each secondary resource. If this is +the configuration, so it is necessary to consult the import output and create +a `resource` block in the configuration for each secondary resource. If this is not done, Terraform will plan to destroy the imported objects on the next run. If you want to rename or otherwise move the imported resources, the -[state management commands](/cli/commands/state) can be used. +[state management commands](/terraform/cli/commands/state) can be used. diff --git a/website/docs/cli/index.mdx b/website/docs/cli/index.mdx index e59efd750e..18bc093082 100644 --- a/website/docs/cli/index.mdx +++ b/website/docs/cli/index.mdx @@ -1,19 +1,19 @@ --- page_title: Terraform CLI Documentation description: >- - Learn how to use Terraform's CLI-based workflows. You can use the CLI alone or - in conjunction with Terraform Cloud or Terraform Enterprise. + Learn Terraform's CLI-based workflows. You can use the CLI alone or + with HCP Terraform or Terraform Enterprise. --- # Terraform CLI Documentation -> **Hands-on:** Try the [Terraform: Get Started](https://learn.hashicorp.com/collections/terraform/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Terraform: Get Started](/terraform/tutorials/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. -This is the documentation for Terraform CLI. It is relevant to anyone working -with Terraform's CLI-based workflows; this includes people who use Terraform CLI -by itself, as well as those who use Terraform CLI in conjunction with Terraform -Cloud or Terraform Enterprise. +This documentation provides reference information about Terraform CLI commands, +as well as instructions for using commands to provision infrastructure and manage the +infrastructure lifecyle. It is relevant to anyone working with Terraform's CLI-based +workflows, including people who use Terraform CLI by itself, as well as those who +use Terraform CLI in conjunction with HCTP Terraform or Terraform Enterprise. -Notably, this documentation does not cover the syntax and usage of the Terraform -language. For that, see the -[Terraform Language Documentation](/language). +For information about the Terraform configuration language syntax and coding patters, refer to the +[Terraform configuration language documentation](/terraform/language). diff --git a/website/docs/cli/init/index.mdx b/website/docs/cli/init/index.mdx index 133527e971..f91c7c0897 100644 --- a/website/docs/cli/init/index.mdx +++ b/website/docs/cli/init/index.mdx @@ -1,16 +1,14 @@ --- -page_title: Initializing Working Directories - Terraform CLI +page_title: Initialize the Terraform working directory description: >- - Working directories contain configurations, settings, cached plugins and - modules, and state data. Learn how to initialize and manage working - directories. + Learn how to initialize the working directory with the terraform init command, which installs plugins and modules defined in the configuration and retrieves state data. --- -# Initializing Working Directories +# Initialize the Working Directory Terraform expects to be invoked from a working directory that contains configuration files written in -[the Terraform language](/language). Terraform uses +[the Terraform language](/terraform/language). Terraform uses configuration content from this directory, and also uses the directory to store settings, cached plugins and modules, and sometimes state data. @@ -25,14 +23,13 @@ A Terraform working directory typically contains: configuration is expected to change over time. - A hidden `.terraform` directory, which Terraform uses to manage cached provider plugins and modules, record which - [workspace](/cli/workspaces) is currently active, and + [workspace](/terraform/cli/workspaces) is currently active, and record the last known backend configuration in case it needs to migrate state on the next run. This directory is automatically managed by Terraform, and is created during initialization. -- State data, if the configuration uses the default `local` backend. This is - managed by Terraform in a `terraform.tfstate` file (if the directory only uses - the default workspace) or a `terraform.tfstate.d` directory (if the directory - uses multiple workspaces). +- State data when the configuration uses the default `local` backend. Terraform manages state in a `terraform.tfstate` file when the directory only uses + the default workspace or a `terraform.tfstate.d` directory when the directory + uses multiple workspaces. ## Initialization @@ -50,7 +47,7 @@ plugins, and downloading modules. Under some conditions (usually when changing from one backend to another), it might ask the user for guidance or confirmation. -For details, see [the `terraform init` command](/cli/commands/init). +For details, see [the `terraform init` command](/terraform/cli/commands/init). ## Reinitialization diff --git a/website/docs/cli/inspect/index.mdx b/website/docs/cli/inspect/index.mdx index 2337764dea..cd323c1724 100644 --- a/website/docs/cli/inspect/index.mdx +++ b/website/docs/cli/inspect/index.mdx @@ -1,11 +1,11 @@ --- -page_title: Inspecting Infrastructure - Terraform CLI +page_title: Inspect infrastructure description: >- - Learn commands to inspect dependency information, outputs, etc. Use them for - integration or to understand your infrastructure. + The terraform inspect commands return dependency information and outputs. Learn how to use terraform inspect commands + to understand your infrastructure. --- -# Inspecting Infrastructure +# Inspect Infrastructure Commands Overview Terraform configurations and state data include some highly structured information about the resources they manage; this includes dependency @@ -17,19 +17,19 @@ Terraform CLI includes some commands for inspecting or transforming this data. You can use these to integrate other tools with Terraform's infrastructure data, or just to gain a deeper or more holistic understanding of your infrastructure. -- [The `terraform graph` command](/cli/commands/graph) creates a visual +- [The `terraform graph` command](/terraform/cli/commands/graph) creates a visual representation of a configuration or a set of planned changes. -- [The `terraform output` command](/cli/commands/output) can get the - values for the top-level [output values](/language/values/outputs) of +- [The `terraform output` command](/terraform/cli/commands/output) can get the + values for the top-level [output values](/terraform/language/values/outputs) of a configuration, which are often helpful when making use of the infrastructure Terraform has provisioned. -- [The `terraform show` command](/cli/commands/show) can generate +- [The `terraform show` command](/terraform/cli/commands/show) can generate human-readable versions of a state file or plan file, or generate machine-readable versions that can be integrated with other tools. -- [The `terraform state list` command](/cli/commands/state/list) can list +- [The `terraform state list` command](/terraform/cli/commands/state/list) can list the resources being managed by the current working directory and workspace, providing a complete or filtered list. -- [The `terraform state show` command](/cli/commands/state/show) can print +- [The `terraform state show` command](/terraform/cli/commands/state/show) can print all of the attributes of a given resource being managed by the current working directory and workspace, including generated read-only attributes like the unique ID assigned by the cloud provider. diff --git a/website/docs/cli/install/apt.mdx b/website/docs/cli/install/apt.mdx deleted file mode 100644 index bd2744cd22..0000000000 --- a/website/docs/cli/install/apt.mdx +++ /dev/null @@ -1,144 +0,0 @@ ---- -page_title: APT Packages for Debian and Ubuntu -description: >- - The HashiCorp APT repositories contain distribution-specific Terraform - packages for both Debian and Ubuntu systems. ---- - -# APT Packages for Debian and Ubuntu - -The primary distribution packages for Terraform are `.zip` archives containing -single executable files that you can extract anywhere on your system. However, -for easier integration with configuration management tools and other systematic -system configuration strategies, we also offer package repositories for -Debian and Ubuntu systems, which allow you to install Terraform using the -`apt install` command or any other APT frontend. - -If you are instead using Red Hat Enterprise Linux, CentOS, or Fedora, you -might prefer to [install Terraform from our Yum repositories](/cli/install/yum). - --> **Note:** The APT repositories discussed on this page are generic HashiCorp -repositories that contain packages for a variety of different HashiCorp -products, rather than just Terraform. Adding these repositories to your -system will, by default, therefore make several other non-Terraform -packages available for installation. That might then mask some packages that -are available for some HashiCorp products in the main Debian and Ubuntu -package repositories. - -## Repository Configuration - -The Terraform packages are signed using a private key controlled by HashiCorp, so you must configure your system to trust that HashiCorp key for package authentication. - -To configure your repository: - -1. Download the signing key to a new keyring. - - ```bash - $ wget -O- https://apt.releases.hashicorp.com/gpg | \ - gpg --dearmor | \ - sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg - -1. Verify the key's fingerprint. - - ```bash - $ gpg --no-default-keyring \ - --keyring /usr/share/keyrings/hashicorp-archive-keyring.gpg \ - --fingerprint - ``` - The fingerprint must match `E8A0 32E0 94D8 EB4E A189 D270 DA41 8C88 A321 9F7B`. You can also verify the key on [Security at HashiCorp](https://www.hashicorp.com/security) under **Linux Package Checksum Verification**. - -1. Add the official HashiCorp repository to your system. The `lsb_release -cs` command finds the distribution release codename for your current system, such as `buster`, `groovy`, or `sid`. - - ```bash - $ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ - https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \ - sudo tee /etc/apt/sources.list.d/hashicorp.list - ``` - -1. Download the package information from HashiCorp. - - ```bash - $ sudo apt update - ``` - -1. Install Terraform from the new repository. - - ```bash - $ sudo apt install terraform - ``` - -## Supported Architectures - -The HashiCorp APT server currently has packages only for the `amd64` -architecture, which is also sometimes known as `x86_64`. - -There are no official packages available for other architectures, such as -`arm64`. If you wish to use Terraform on a non-`amd64` system, -[download a normal release `.zip` file](/downloads) instead. - -## Supported Debian and Ubuntu Releases - -The HashiCorp APT server currently contains release repositories for the -following distribution releases: - -* Debian 8 (`jessie`) -* Debian 9 (`stretch`) -* Debian 10 (`buster`) -* Debian 11 (`bullseye`) -* Ubuntu 16.04 (`xenial`) -* Ubuntu 18.04 (`bionic`) -* Ubuntu 19.10 (`eoam`) -* Ubuntu 20.04 (`focal`) -* Ubuntu 20.10 (`groovy`) -* Ubuntu 21.04 (`hirsute`) -* Ubuntu 21.10 (`impish`) - -No repositories are available for other Debian or Ubuntu versions or -any other APT-based Linux distributions. If you add the repository using -the above commands on other systems then `apt update` will report the -repository index as missing. - -Terraform executables are statically linked and so they depend only on the -Linux system call interface, not on any system libraries. Because of that, -you may be able to use one of the above release codenames when adding a -repository to your system, even if that codename doesn't match your current -distribution release. - -Over time we will change the set of supported distributions, including both -adding support for new releases and ceasing to publish new Terraform versions -under older releases. - -## Choosing Terraform Versions - -The HashiCorp APT repositories contain multiple versions of Terraform, but -because the packages are all named `terraform` it is impossible to install -more than one version at a time, and `apt install` will default to selecting -the latest version. - -It's often necessary to match your Terraform version with what a particular -configuration is currently expecting. You can use the following command to -see which versions are currently available in the repository index: - -```bash -apt policy terraform -``` - -There may be multiple package releases for a particular Terraform version if -we need to publish an updated package for any reason. In that case, the -subsequent releases will have an additional suffix, like `0.13.4-2`. In these -cases, the Terraform executable inside the package should be unchanged, but its -metadata and other contents may be different. - -You can select a specific version to install by including it in the -`apt install` command line, as follows: - -```bash -sudo apt install terraform=0.14.0 -``` - -If your workflow requires using multiple versions of Terraform at the same -time, for example when working through a gradual upgrade where not all -of your configurations are upgraded yet, we recommend that you use the -official release `.zip` files instead of the APT packages, so you can install -multiple versions at once and then select which to use for each command you -run. diff --git a/website/docs/cli/install/yum.mdx b/website/docs/cli/install/yum.mdx deleted file mode 100644 index accc2d3f3d..0000000000 --- a/website/docs/cli/install/yum.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -page_title: 'Yum Packages for Red Hat Enterprise Linux, Fedora, and Amazon Linux' -description: >- - The HashiCorp Yum repositories contain distribution-specific Terraform - packages for Red Hat Enterprise Linux, Fedora, and Amazon Linux systems. ---- - -# Yum/DNF Packages for RHEL, CentOS, and Fedora - -The primary distribution packages for Terraform are `.zip` archives containing -single executable files that you can extract anywhere on your system. However, -for easier integration with configuration management tools and other systematic -system configuration strategies, we also offer package repositories for -RedHat Enterprise Linux, Fedora, and Amazon Linux systems, which allow you to -install Terraform using the `yum install` or `dnf install` commands. - -If you are instead using Debian or Ubuntu, you -might prefer to [install Terraform from our APT repositories](/cli/install/apt). - --> **Note:** The Yum repositories discussed on this page are generic HashiCorp -repositories that contain packages for a variety of different HashiCorp -products, rather than just Terraform. Adding these repositories to your -system will, by default, therefore make a number of other non-Terraform -packages available for installation. That might then mask the packages that are -available for some HashiCorp products in the main distribution repositories. - -## Repository Configuration - -Before adding a repository you must determine which distribution you are using. -The following command lines refer to a placeholder variable `$release` which -you must replace with the appropriate value from the following list: - -* Red Hat Enterprise Linux: `RHEL` -* Fedora: `fedora` -* Amazon Linux: `AmazonLinux` - -If you are using a Yum-based distribution, add the repository using -`yum-config-manager` as follows: - -```bash -sudo yum install -y yum-utils -sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/$release/hashicorp.repo -``` - -If you are using a DNF-based distribution, add the repository using -`dnf config-manager` as follows: - -```bash -sudo dnf install -y dnf-plugins-core -sudo dnf config-manager --add-repo https://rpm.releases.hashicorp.com/$release/hashicorp.repo -``` - -In both cases, the Terraform package name is `terraform`. For example: - -```bash -yum install terraform -``` - -## Supported Architectures - -The HashiCorp Yum/DNF server currently has packages only for the `x86_64` -architecture, which is also sometimes known as `amd64`. - -There are no official packages available for other architectures, such as -`aarch64`. If you wish to use Terraform on a non-`x86_64` system, -[download a normal release `.zip` file](/downloads) instead. - -## Supported Distribution Releases - -The HashiCorp Yum server currently contains release repositories for the -following distribution releases: - -* AmazonLinux 2 -* Fedora 29 -* Fedora 30 -* Fedora 31 -* Fedora 32 -* Fedora 33 -* RHEL 7 (and CentOS 7) -* RHEL 8 (and CentOS 8) - -No repositories are available for other versions of these distributions or for -any other RPM-based Linux distributions. If you add the repository using -the above commands on other systems then you will see a 404 Not Found error. - -Over time we will change the set of supported distributions, including both -adding support for new releases and ceasing to publish new Terraform versions -under older releases. - -## Choosing Terraform Versions - -The HashiCorp Yum repositories contain multiple versions of Terraform, but -because the packages are all named `terraform` it is impossible to install -more than one version at a time, and `yum install` or `dnf install` will -default to selecting the latest version. - -It's often necessary to match your Terraform version with what a particular -configuration is currently expecting. You can use the following command to -see which versions are currently available in the repository index: - -```bash -yum --showduplicate list terraform -``` - -You can select a specific version to install by including it in the -`yum install` command line, as follows: - -```bash -yum install terraform-0.14.0-2.x86_64 -``` - -If you are using a DNF-based distribution, similar use `dnf` instead of `yum` -when following the above steps. - -If your workflow requires using multiple versions of Terraform at the same -time, for example when working through a gradual upgrade where not all -of your configurations are upgraded yet, we recommend that you use the -official release `.zip` files instead of the Yum packages, so you can install -multiple versions at once and then select which to use for each command you -run. diff --git a/website/docs/cli/plugins/index.mdx b/website/docs/cli/plugins/index.mdx index 22965ea07e..82f8bb3203 100644 --- a/website/docs/cli/plugins/index.mdx +++ b/website/docs/cli/plugins/index.mdx @@ -1,60 +1,56 @@ --- -page_title: Managing Plugins - Terraform CLI +page_title: Manage Terraform plugins description: >- - Commands to install, configure, and show information about providers. Also - commands to reduce install effort in air-gapped environments. + Providers are types of plugins for Terraform that manage infrastructure resources. Learn about managing plugins using the Terraform CLI. --- -# Managing Plugins +# Manage plugins overview -Terraform relies on plugins called "providers" in order to manage various types -of resources. (For more information about providers, see -[Providers](/language/providers) in the Terraform -language docs.) +This topic provides an overview of the how to manage plugins that Terraform relies on to manage various types +of resources. Providers are the only plugin type Terraform users interact with. Refer to [Providers](/terraform/language/providers) in the Terraform +language docs for additional information about providers. --> **Note:** Providers are currently the only plugin type most Terraform users -will interact with. Terraform also supports third-party provisioner plugins, but -we discourage their use. +## Workflow -Terraform downloads and/or installs any providers -[required](/language/providers/requirements) by a configuration -when [initializing](/cli/init) a working directory. By default, -this works without any additional interaction but requires network access to +When you initialize a working directory, Terraform installs any providers the Terraform configuration requires. Refer to +[Provider Requirements](/terraform/language/providers/requirements) in the Terraform configuration language information about requiring providers. Refer to the [`terraform init` command documentation](/terraform/cli/init) for additional information about how initialize the working directory. + +By default, Terraform initializes the working directory without any additional interaction, but you must have network access to download providers from their source registry. You can configure Terraform's provider installation behavior to limit or skip -network access, and to enable use of providers that aren't available via a -networked source. Terraform also includes some commands to show information -about providers and to reduce the effort of installing providers in airgapped +network access, and to enable use of providers that are not available through a +networked source. Terraform also includes commands that show information +about providers and commands that reduce the effort of installing providers in air-gapped environments. ## Configuring Plugin Installation Terraform's configuration file includes options for caching downloaded plugins, or explicitly specifying a local or HTTPS mirror to install plugins from. For -more information, see [CLI Config File](/cli/config/config-file). +more information, see [CLI Config File](/terraform/cli/config/config-file). ## Getting Plugin Information -Use the [`terraform providers`](/cli/commands/providers) command to get information +Use the [`terraform providers`](/terraform/cli/commands/providers) command to get information about the providers required by the current working directory's configuration. -Use the [`terraform version`](/cli/commands/version) command (or +Use the [`terraform version`](/terraform/cli/commands/version) command (or `terraform -version`) to show the specific provider versions installed for the current working directory. -Use the [`terraform providers schema`](/cli/commands/providers/schema) command to +Use the [`terraform providers schema`](/terraform/cli/commands/providers/schema) command to get machine-readable information about the resources and configuration options offered by each provider. ## Managing Plugin Installation -Use the [`terraform providers mirror`](/cli/commands/providers/mirror) command to +Use the [`terraform providers mirror`](/terraform/cli/commands/providers/mirror) command to download local copies of every provider required by the current working -directory's configuration. This directory will use the nested directory layout +directory's configuration. The directory uses the nested directory layout that Terraform expects when installing plugins from a local source, so you can -transfer it directly to an airgapped system that runs Terraform. +transfer it directly to an air-gapped system that runs Terraform. -Use the [`terraform providers lock`](/cli/commands/providers/lock) command +Use the [`terraform providers lock`](/terraform/cli/commands/providers/lock) command to update the lock file that Terraform uses to ensure predictable runs when using ambiguous provider version constraints. diff --git a/website/docs/cli/plugins/signing.mdx b/website/docs/cli/plugins/signing.mdx index cdcd546d68..bb2e69e92d 100644 --- a/website/docs/cli/plugins/signing.mdx +++ b/website/docs/cli/plugins/signing.mdx @@ -1,27 +1,29 @@ --- -page_title: Plugin Signing +page_title: Plugin signatures description: >- - Learn about the types of signatures providers can have on the Terraform - Registry. + Signatures help you determine the authenticity of the plugins you want to install. Learn about the types of signatures providers can have on the Terraform registry. --- -# Plugin Signing +# Plugin signatures -~> **Note** Currently only provider plugins fetched from a registry are authenticated. +This topic provides information about the types of signatures that can be built into plugins you install. Terraform only authenticates provider plugins fetched from a registry. -Terraform providers installed from the Registry are cryptographically signed, and the signature is verified at time of installation. There are three types of provider signatures, each with different trust implications: +## Types of plugin signatures -* **Signed by HashiCorp** - are built, signed, and supported by HashiCorp. -* **Signed by Trusted Partners** - are built, signed, and supported by a third party. HashiCorp has - verified the ownership of the private key and we provide a chain of trust to the CLI to verify this - programatically. -* **Self-signed** - are built, signed, and supported by a third party. HashiCorp does not provide a +Terraform providers installed from the registry are cryptographically signed. Terraform verifies the signature during installation. There are three types of signatures: + +* Providers signed by HashiCorp: HashiCorp builds, signs, and supports these providers. +* Providers signed by trusted partners: A third party builds, signs, and supports these providers. HashiCorp verifies the ownership of the private key and provides a chain of trust to the CLI to verify ownership programatically. +* Self-signed providers: A third party builds, signs, and supports these providers. HashiCorp does not provide a verification or chain of trust for the signature. You may obtain and validate fingerprints manually if you want to ensure you are using a binary you can trust. -Terraform does **NOT** support fetching and using unsigned binaries, but you can manually install -unsigned binaries. You should take extreme care when doing so as no programatic authentication is performed. +## Unsigned binaries -Usage of plugins from the registry is subject to the Registry's [Terms of Use](https://registry.terraform.io/terms). +You cannot fetch and use unsigned binaries from the registry, but you can manually install unsigned binaries. We strongly recommend that you thoroughly vetting providers that you manually install so that these providers do not programatically authenticate. + +## Registry terms of use + +Use of plugins from the registry is subject to the registry's [terms of use](https://registry.terraform.io/terms). diff --git a/website/docs/cli/run/index.mdx b/website/docs/cli/run/index.mdx index 0ce5238200..bc57c38cd6 100644 --- a/website/docs/cli/run/index.mdx +++ b/website/docs/cli/run/index.mdx @@ -1,25 +1,28 @@ --- -page_title: Provisioning Infrastructure - Terraform CLI -description: 'Learn about commands for core provisioning tasks: plan, apply, and destroy.' +page_title: Terraform workflow for provisioning infrastructure +description: Learn how to use the Terraform CLI to provision infrastructure. --- -# Provisioning Infrastructure with Terraform +# Terraform workflow for provisioning infrastructure -Terraform's primary function is to create, modify, and destroy infrastructure +This topic provides overview information about the Terraform workflow for provisioning infrastructure using the Terraform CLI. + +## Workflows + +You can use Terraform to create, modify, and destroy infrastructure resources to match the desired state described in a -[Terraform configuration](/language). +[Terraform configuration](/terraform/language). The +Terraform binary includes commands and subcommands for a wide variety of infrastructure lifecycle management +actions, but the following commands provide basic provisioning tasks: -When people refer to "running Terraform," they generally mean performing these -provisioning actions in order to affect real infrastructure objects. The -Terraform binary has many other subcommands for a wide variety of administrative -actions, but these basic provisioning tasks are the core of Terraform. +- `terrafrom plan` +- `terraform apply` +- `terraform destroy` -Terraform's provisioning workflow relies on three commands: `plan`, `apply`, and -`destroy`. All of these commands require an -[initialized](/cli/init) working directory, and all of them act -only upon the currently selected [workspace](/cli/workspaces). +All of these commands require an [initialized](/terraform/cli/init) working directory, and all of them act +only upon the currently selected [workspace](/terraform/cli/workspaces). -## Planning +### Plan The `terraform plan` command evaluates a Terraform configuration to determine the desired state of all the resources it declares, then compares that desired @@ -38,9 +41,9 @@ resulting actions are as expected. However, `terraform plan` can also save its plan as a runnable artifact, which `terraform apply` can use to carry out those exact changes. -For details, see [the `terraform plan` command](/cli/commands/plan). +For details, see [the `terraform plan` command](/terraform/cli/commands/plan). -## Applying +### Apply The `terraform apply` command performs a plan just like `terraform plan` does, but then actually carries out the planned changes to each resource using the @@ -54,9 +57,9 @@ running a new plan. You can use this to reliably perform an exact set of pre-approved changes, even if the configuration or the state of the real infrastructure has changed in the minutes since the original plan was created. -For details, see [the `terraform apply` command](/cli/commands/apply). +For details, see [the `terraform apply` command](/terraform/cli/commands/apply). -## Destroying +### Destroy The `terraform destroy` command destroys all of the resources being managed by the current working directory and workspace, using state data to determine which @@ -68,4 +71,4 @@ and then running an apply, except that it doesn't require editing the configuration. This is more convenient if you intend to provision similar resources at a later date. -For details, see [the `terraform destroy` command](/cli/commands/destroy). +For details, see [the `terraform destroy` command](/terraform/cli/commands/destroy). diff --git a/website/docs/cli/state/index.mdx b/website/docs/cli/state/index.mdx index b3c11b7de3..1e419491d8 100644 --- a/website/docs/cli/state/index.mdx +++ b/website/docs/cli/state/index.mdx @@ -1,34 +1,32 @@ --- -page_title: Manipulating State - Terraform CLI +page_title: Update Terraform state manually description: >- - State data tracks which real-world object corresponds to each resource. - Inspect state, move or import resources, and more. + State data is the record of how real-world objects map to resources in the Terraform configuration. Learn how to manually update with state data. --- -# Manipulating Terraform State +# Update Terraform state manually overview -> **Hands-on:** Try the [Manage Resources in Terraform State](https://learn.hashicorp.com/tutorials/terraform/state-cli?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +This topic provides overview information about how to manually update state in Terraform. -Terraform uses [state data](/language/state) to remember which -real-world object corresponds to each resource in the configuration; -this allows it to modify an existing object when its resource declaration -changes. +> **Hands-on:** Try the [Manage Resources in Terraform State](/terraform/tutorials/state/state-cli?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -Terraform updates state automatically during plans and applies. However, it's -sometimes necessary to make deliberate adjustments to Terraform's state data, -usually to compensate for changes to the configuration or the real managed -infrastructure. +## Introduction -Terraform CLI supports several workflows for interacting with state: +Terraform stores information about real-world object that correspond to resources in the configuration as [state data](/terraform/language/state). +Doing so allows Terraform to modify an existing object when its resource declaration changes. + +Terraform automatically updates state when you run the `terraform plan` and `terraform apply` commands, but you may need to manually adjustment state data as a result of changes to the configuration or the real managed infrastructure. + +## Workflow + +Modifying state data outside of normal `terraform plan` or `terraform apply` operations can cause Terraform to lose track of managed resources, leading to increased costs, reduced productivity, or compromised security. Make sure to keep backups of your state data if you choose to manually modify state. + +You can use the Terraform CLI to perform the following state interations: + +- [Inspect state](/terraform/cli/state/inspect) +- [Re-create resources](/terraform/cli/state/taint) +- [Move resources](/terraform/cli/state/move) +- [Import existing resources](/terraform/cli/import) +- [Recover state from backup](/terraform/cli/state/recover) -- [Inspecting State](/cli/state/inspect) -- [Forcing Re-creation](/cli/state/taint) -- [Moving Resources](/cli/state/move) -- Importing Pre-existing Resources (documented in the - [Importing Infrastructure](/cli/import) section) -- [Disaster Recovery](/cli/state/recover) -~> **Important:** Modifying state data outside a normal plan or apply can cause -Terraform to lose track of managed resources, which might waste money, annoy -your colleagues, or even compromise the security of your operations. Make sure -to keep backups of your state data when modifying state out-of-band. diff --git a/website/docs/cli/state/inspect.mdx b/website/docs/cli/state/inspect.mdx index 54767bb834..74f200ca50 100644 --- a/website/docs/cli/state/inspect.mdx +++ b/website/docs/cli/state/inspect.mdx @@ -1,21 +1,21 @@ --- -page_title: Inspecting State - Terraform CLI -description: Commands that allow you to read and update state. +page_title: Inspect Terraform state +description: The `terraform state` group of commands help you inspect Terraform state. Learn how inspecting Terraform state can help you read and update state. --- -# Inspecting State +# Inspect Terraform State Overview Terraform includes some commands for reading and updating state without taking any other actions. -- [The `terraform state list` command](/cli/commands/state/list) +- [The `terraform state list` command](/terraform/cli/commands/state/list) shows the resource addresses for every resource Terraform knows about in a configuration, optionally filtered by partial resource address. -- [The `terraform state show` command](/cli/commands/state/show) +- [The `terraform state show` command](/terraform/cli/commands/state/show) displays detailed state data about one resource. -- [The `terraform refresh` command](/cli/commands/refresh) updates +- [The `terraform refresh` command](/terraform/cli/commands/refresh) updates state data to match the real-world condition of the managed resources. This is done automatically during plans and applies, but not when interacting with state directly. diff --git a/website/docs/cli/state/move.mdx b/website/docs/cli/state/move.mdx index c69280a902..252184d103 100644 --- a/website/docs/cli/state/move.mdx +++ b/website/docs/cli/state/move.mdx @@ -1,14 +1,13 @@ --- -page_title: Moving Resources - Terraform CLI +page_title: Move resources description: >- - Commands that allow you to manage the way that resources are tracked in state. - They are helpful when you move or change resources. + Terraform state commands can move and remove resources and transfer existing resources to a different provider. Learn how about changing or moving resources. --- -# Moving Resources +# Move Resources Terraform's state associates each real-world object with a configured resource -at a specific [resource address](/cli/state/resource-addressing). This +at a specific [resource address](/terraform/cli/state/resource-addressing). This is seamless when changing a resource's attributes, but Terraform will lose track of a resource if you change its name, move it to a different module, or change its provider. @@ -22,27 +21,26 @@ can explicitly tell Terraform to associate it with a different configured resource. For most cases we recommend using -[the Terraform language's refactoring features](/language/modules/develop/refactoring) +[the Terraform language's refactoring features](/terraform/language/modules/develop/refactoring) to document in your module exactly how the resource names have changed over -time. Terraform will react to this information automatically during planning, -and thus users of your module will not need to take any unusual extra steps. +time. Terraform reacts to this information automatically during planning, so users of your module do not need to take any unusual extra steps. -> **Hands On:** Try the [Use Configuration to Move Resources](https://learn.hashicorp.com/tutorials/terraform/move-config) on HashiCorp Learn. +> **Hands On:** Try the [Use Configuration to Move Resources](/terraform/tutorials/configuration-language/move-config) tutorial. There are some other situations which require explicit state modifications, though. For those, consider the following Terraform commands: -- [The `terraform state mv` command](/cli/commands/state/mv) changes +- [The `terraform state mv` command](/terraform/cli/commands/state/mv) changes which resource address in your configuration is associated with a particular real-world object. Use this to preserve an object when renaming a resource, or when moving a resource into or out of a child module. -- [The `terraform state rm` command](/cli/commands/state/rm) tells +- [The `terraform state rm` command](/terraform/cli/commands/state/rm) tells Terraform to stop managing a resource as part of the current working directory and workspace, _without_ destroying the corresponding real-world object. (You can later use `terraform import` to start managing that resource in a different workspace or a different Terraform configuration.) -- [The `terraform state replace-provider` command](/cli/commands/state/replace-provider) +- [The `terraform state replace-provider` command](/terraform/cli/commands/state/replace-provider) transfers existing resources to a new provider without requiring them to be re-created. diff --git a/website/docs/cli/state/recover.mdx b/website/docs/cli/state/recover.mdx index f3b124cb2d..8189731d88 100644 --- a/website/docs/cli/state/recover.mdx +++ b/website/docs/cli/state/recover.mdx @@ -1,25 +1,19 @@ --- -page_title: Recovering from State Disasters - Terraform CLI -descriptin: >- - Commands that allow you to restore state backups and override Terraform state - protections. +page_title: Recover state from backup +description: >- + Learn how to restore state backups and override Terraform state protections to fix state errors with the Terraform CLI. --- -# Recovering from State Disasters +# Recover state from backup overview -If something has gone horribly wrong (possibly due to accidents when performing -other state manipulation actions), you might need to take drastic actions with -your state data. +This topic provides overview information about recovering Terraform state from a backup after a disaster, such as an accident when performing +other state manipulation actions. -- [The `terraform force-unlock` command](/cli/commands/force-unlock) can - override the protections Terraform uses to prevent two processes from - modifying state at the same time. You might need this if a Terraform process - (like a normal apply) is unexpectedly terminated (like by the complete - destruction of the VM it's running in) before it can release its lock on the - state backend. Do not run this until you are completely certain what happened - to the process that caused the lock to get stuck. +## Workflow -- [The `terraform state pull` command](/cli/commands/state/pull) and - [the `terraform state push` command](/cli/commands/state/push) can - directly read and write entire state files from and to the configured backend. - You might need this for obtaining or restoring a state backup. +1. **Unlock Terraform**: You may need to unlock Terraform when a `terraform apply` or other process unexpectedly terminates before Terraform can release its lock on the state backend. Unlocking Terraform overrides protectionsthat prevent two processes from modifying state at the same time. We do not recommend unlocking until you determine what caused the lock to get stuck. + + Refer to the [`terraform force-unlock` command](/terraform/cli/commands/force-unlock) documentation for additional information. + +1. **Read state data**: Run the [`terraform state pull` command](/terraform/cli/commands/state/pull) to read the state files from the configured backend. +1. **Write state data**: Run the [`terraform state push` command](/terraform/cli/commands/state/push) to write state files to the configured backend. \ No newline at end of file diff --git a/website/docs/cli/state/resource-addressing.mdx b/website/docs/cli/state/resource-addressing.mdx index 07f3039c06..04e4b756de 100644 --- a/website/docs/cli/state/resource-addressing.mdx +++ b/website/docs/cli/state/resource-addressing.mdx @@ -1,11 +1,13 @@ --- -page_title: 'Internals: Resource Address' -description: |- - A resource address is a string that identifies zero or more resource - instances in your overall configuration. +page_title: Resource address reference +description: Use the resource address to reference specific instances of resources elsewhere in the configuration. Learn how Terraform creates addresses for resources. --- -# Resource Addressing +# Resource Address Reference + +This topic provides reference information about resource addresses in Terraform. + +## Syntax A _resource address_ is a string that identifies zero or more resource instances in your overall configuration. @@ -71,7 +73,7 @@ resource_type.resource_name[instance index] -> In Terraform v0.12 and later, a resource spec without a module path prefix matches only resources in the root module. In earlier versions, a resource spec without a module path prefix would match resources with the same type and name -in any descendent module. +in any descendant module. ## Index values for Modules and Resources @@ -118,12 +120,12 @@ Given a Terraform config that includes: ```hcl resource "aws_instance" "web" { # ... - for_each = { + for_each = tomap({ "terraform": "value1", "resource": "value2", "indexing": "value3", "example": "value4", - } + }) } ``` @@ -133,4 +135,4 @@ An address like this: aws_instance.web["example"] ``` -Refers to only the "example" instance in the config. +Refers to only the "example" instance in the config, and resolves to "value4". diff --git a/website/docs/cli/state/taint.mdx b/website/docs/cli/state/taint.mdx index c4c46ca2f0..cb3e06d7dc 100644 --- a/website/docs/cli/state/taint.mdx +++ b/website/docs/cli/state/taint.mdx @@ -1,28 +1,38 @@ --- -page_title: Forcing Re-creation of Resources - Terraform CLI -description: Commands that allow you to destroy and re-create resources manually. +page_title: Recreate resources +description: The -replace flag and taint command help you replace infrastructure objects. Learn how the -replace flag and taint command can help you recreate resources. --- -# Forcing Re-creation of Resources +# Recreate resources overview -During planning, by default Terraform retrieves the latest state of each -existing object and compares it with the current configuration, planning -actions only against objects whose current state does not match the -configuration. +This topic provides an overview of how to recreate resources in Terraform. -However, in some cases a remote object may become damaged or degraded in a -way that Terraform cannot automatically detect. For example, if software -running inside a virtual machine crashes but the virtual machine itself is -still running then Terraform will typically have no way to detect and respond -to the problem, because Terraform only directly manages the machine as a whole. +## Introduction -If you know that an object is damaged, or if you want to force Terraform to -replace it for any other reason, you can override Terraform's default behavior -using [the `-replace=...` planning option](/cli/commands/plan#replace-address) -when you run either `terraform plan` or `terraform apply`: +By default, Terraform retrieves the latest state of each existing object and compares it with the current configuration when you run the `terraform apply` command. Terraform only takes action on objects that do not match the configuration. + +When remote objects become damaged or degraded, such as when software +running inside a virtual machine crashes but the virtual machine is +still running, Terraform does not have no way to detect and respond +to the problem. This is because Terraform only directly manages the machine as a whole. + +In some cases, Terraform can automatically infer that an object is in an +incomplete or degraded state. For example, when a complex object is partially created in the remote system or +when a provisioner step failed. When this occurs, Terraform automatically flags resources to recreate. + +You can manually replace objects when Terraform is unable to infer that an object should be replaced. + +## Workflows + +When you meed to replace an object, you can use the following methods. + +### Manually replace resources + +Add the [`-replace` flag](/terraform/cli/commands/plan#replace-address) +to your `terraform plan` or `terraform apply` command: ```shellsession -$ terraform apply -replace=aws_instance.example +$ terraform apply -replace="aws_instance.example" # ... # aws_instance.example will be replaced, as requested @@ -31,18 +41,9 @@ $ terraform apply -replace=aws_instance.example } ``` -## The "tainted" status +### Replace resource in `tainted` state -Sometimes Terraform is able to infer automatically that an object is in an -incomplete or degraded state. For example, if creation of a complex object -fails in such a way that parts of it already exist in the remote system, or -if object creation succeeded but a provisioner step subsequently failed, -Terraform must remember that the object exists but may not be fully-functional. - -Terraform represents this situation by marking an object in the state as -"tainted". When an object is marked with this status, the next plan will force -replacing that object in a similar way to if you had specified that object's -address using `-replace=...` as described above. +Terraform applies the `tainted` status to objects in the state data when Terraform is able to infer that the object is in a degraded or damaged state. This status indicates that the object exists but may not be fully-functional. Terraform replaces objects in a `tainted` states during the next `plan` or `apply` operation. ``` # aws_instance.example is tainted, so must be replaced @@ -53,11 +54,11 @@ address using `-replace=...` as described above. If Terraform has marked an object as tainted but you consider it to be working correctly and do not want to replace it, you can override Terraform's -determination using [the `terraform untaint` command](/cli/commands/untaint), +determination using [the `terraform untaint` command](/terraform/cli/commands/untaint), after which Terraform will consider the object to be ready for use by any downstream resource declarations. -You can also _force_ Terraform to mark a particular object as tainted using -[the `terraform taint` command](/cli/commands/taint), but that approach is +You can force Terraform to mark a particular object as tainted using +[the `terraform taint` command](/terraform/cli/commands/taint), but that approach is deprecated in favor of the `-replace=...` option, which avoids the need to create an interim state snapshot with a tainted object. diff --git a/website/docs/cli/test/index.mdx b/website/docs/cli/test/index.mdx new file mode 100644 index 0000000000..08177edce9 --- /dev/null +++ b/website/docs/cli/test/index.mdx @@ -0,0 +1,54 @@ +--- +page_title: Testing features in Terraform +description: >- + Learn about the terraform test command, which runs structured tests and validations for your configuration to ensure + correctness in your infrastructure. +--- + +# Testing features in Terraform overview + +This topic provides an overview of the testing features in Terraform to help you validate your infrastructure. + +## Introduction + +Terraform provides the following types of testing capabilities: + +1. Configuration and infrastructure validation as part of your regular Terraform operations. Refer to [Custom Conditions](/terraform/language/expressions/custom-conditions) and [Checks](/terraform/language/checks) to learn more about these types of testing capabilities. +1. Traditional unit and integration testing on your configuration. Refer to the [`terraform test` command](/terraform/cli/commands/test) documentation to learn more about this testing capability. + +### Additional testing and validation features + +- [Input Variable Validation](/terraform/language/expressions/custom-conditions#input-variable-validation) +- [Pre and Post-conditions](/terraform/language/expressions/custom-conditions#preconditions-and-postconditions) +- [Checks](/terraform/language/checks) + +## How the `terraform test` command works + +The `test` command performs the following actions: + +- Locates Terraform testing files within your configuration directory. +- Provisions the infrastructure within your configuration as specified by each testing file. +- Runs the assertions from the test file against the provisioned infrastructure. +- Destroys the provisioned infrastructure at the end of the test. + +For details about using the `test` command, refer to the [`test` command reference documentation](/terraform/cli/commands/test). + +### Write configuration for tests + +Terraform test files [have their own configuration syntax](/terraform/language/tests). This test file syntax focuses on customizing Terraform executions for the current configuration and overriding variables and providers to test different behaviors. + +## Validations + +Validations allow you to verify aspects of your configuration and infrastructure as it is applied and created. HCP Terraform also supports automated [continuous validation](/terraform/cloud-docs/workspaces/health#continuous-validation). + +The Terraform `test` command also executes any validations within your configuration as part of the tests it executes. For more information on the available validation, refer to [Checks](/terraform/language/checks) and [Custom Conditions](/terraform/language/expressions/custom-conditions). + +## Tests versus validations + +You can write many validations as test assertions, but there are specific use cases for both. + +Validations are executed during Terraform plan and apply operations, and the Terraform `test` command also runs validations while executing tests. Therefore, use validations to validate aspects of your configuration that should always be true and could impact the valid execution of your infrastructure. + +Module authors should note that validations are executed and exposed to module users, so if they fail, ensure the failure messages are understandable and actionable. + +In contrast, Terraform only executes tests when you run `terraform test`. Use tests to assert the correctness of any logical operations or specific behavior within your configuration. For example, you can test that Terraform creates conditional resources based on an input by setting the input controlling those resources to a certain value then verifying the resources Terraform creates. \ No newline at end of file diff --git a/website/docs/cli/workspaces/index.mdx b/website/docs/cli/workspaces/index.mdx index 7a6b11f9e7..3baca17bd4 100644 --- a/website/docs/cli/workspaces/index.mdx +++ b/website/docs/cli/workspaces/index.mdx @@ -1,78 +1,85 @@ --- -page_title: Managing Workspaces - Terraform CLI +page_title: Manage workspaces description: >- - Commands to list, select, create, and output workspaces. Workspaces help - manage different groups of resources with one configuration. + Workspaces are separate instances of Terraform state data. Learn commands for managing workspaces. --- -# Managing Workspaces +# Manage Workspaces Overview -In Terraform CLI, _workspaces_ are separate instances of -[state data](/language/state) that can be used from the same working -directory. You can use workspaces to manage multiple non-overlapping groups of -resources with the same configuration. +Workspaces in the Terraform CLI refer to separate instances of [state data](/terraform/language/state) inside the same Terraform working directory. They are distinctly different from [workspaces in HCP Terraform](/terraform/cloud-docs/workspaces), which each have their own Terraform configuration and function as separate working directories. -- Every [initialized working directory](/cli/init) has at least - one workspace. (If you haven't created other workspaces, it is a workspace - named `default`.) -- For a given working directory, only one workspace can be _selected_ at a time. -- Most Terraform commands (including [provisioning](/cli/run) - and [state manipulation](/cli/state) commands) only interact - with the currently selected workspace. -- Use [the `terraform workspace select` command](/cli/commands/workspace/select) - to change the currently selected workspace. -- Use the [`terraform workspace list`](/cli/commands/workspace/list), - [`terraform workspace new`](/cli/commands/workspace/new), and - [`terraform workspace delete`](/cli/commands/workspace/delete) commands - to manage the available workspaces in the current working directory. +Terraform relies on state to associate resources with real-world objects. When you run the same configuration multiple times with separate state data, Terraform can manage multiple sets of non-overlapping resources. --> **Note:** Terraform Cloud and Terraform CLI both have features called -"workspaces," but they're slightly different. Terraform Cloud's workspaces -behave more like completely separate working directories. +Workspaces can be helpful for specific [use cases](#use-cases), but they are not required to use the Terraform CLI. We recommend using [alternative approaches](#alternatives-to-workspaces) for complex deployments requiring separate credentials and access controls. -## The Purpose of Workspaces -Since most of the resources you can manage with Terraform don't include a unique -name as part of their configuration, it's common to use the same Terraform -configuration to provision multiple groups of similar resources. +## Managing CLI Workspaces -Terraform relies on [state](/language/state) to associate resources with -real-world objects, so if you run the same configuration multiple times with -completely separate state data, Terraform can manage many non-overlapping groups -of resources. In some cases you'll want to change -[variable values](/language/values/variables) for these different -resource collections (like when specifying differences between staging and -production deployments), and in other cases you might just want many instances -of a particular infrastructure pattern. +Every [initialized working directory](/terraform/cli/init) starts with one workspace named `default`. -The simplest way to maintain multiple instances of a configuration with -completely separate state data is to use multiple -[working directories](/cli/init) (with different -[backend](/language/settings/backends/configuration) configurations per directory, if you -aren't using the default `local` backend). +Use the [`terraform workspace list`](/terraform/cli/commands/workspace/list), [`terraform workspace new`](/terraform/cli/commands/workspace/new), and [`terraform workspace delete`](/terraform/cli/commands/workspace/delete) commands to manage the available workspaces in the current working directory. -However, this isn't always the most _convenient_ way to handle separate states. -Terraform installs a separate cache of plugins and modules for each working -directory, so maintaining multiple directories can waste bandwidth and disk -space. You must also update your configuration code from version control -separately for each directory, reinitialize each directory separately when -changing the configuration, etc. +Use [the `terraform workspace select` command](/terraform/cli/commands/workspace/select) to change the currently selected workspace. For a given working directory, you can only select one workspace at a time. Most Terraform commands only interact with the currently selected workspace. This includes [provisioning](/terraform/cli/run) and [state manipulation](/terraform/cli/state). -Workspaces allow you to use the same working copy of your configuration and the -same plugin and module caches, while still keeping separate states for each -collection of resources you manage. +When you provision infrastructure in each workspace, you usually need to manually specify different [input variables](/terraform/language/values/variables) to differentiate each collection. For example, you might deploy test infrastructure to a different region. -## Interactions with Terraform Cloud Workspaces -Terraform Cloud organizes infrastructure using workspaces, but its workspaces -act more like completely separate working directories; each Terraform Cloud +## Use Cases + +You can create multiple [working directories](/terraform/cli/init) to maintain multiple instances of a configuration with completely separate state data. However, Terraform installs a separate cache of plugins and modules for each working directory, so maintaining multiple directories can waste bandwidth and disk space. This approach also requires extra tasks like updating configuration from version control for each directory separately and reinitializing each directory when you change the configuration. Workspaces are convenient because they let you create different sets of infrastructure with the same working copy of your configuration and the same plugin and module caches. + +A common use for multiple workspaces is to create a parallel, distinct copy of +a set of infrastructure to test a set of changes before modifying production infrastructure. + +Non-default workspaces are often related to feature branches in version control. +The default workspace might correspond to the `main` or `trunk` branch, which describes the intended state of production infrastructure. When a developer creates a feature branch for a change, they might also create a corresponding workspace and deploy into it a temporary copy of the main infrastructure. They can then test changes on the copy without affecting the production infrastructure. Once the change is merged and deployed to the default workspace, they destroy the test infrastructure and delete the temporary workspace. + + +### When Not to Use Multiple Workspaces + +Workspaces let you quickly switch between multiple instances of a **single configuration** within its **single backend**. They are not designed to solve all problems. + +When using Terraform to manage larger systems, you should create separate Terraform configurations that correspond to architectural boundaries within the system. This lets teams manage different components separately. Workspaces alone are not a suitable tool for system decomposition because each subsystem should have its own separate configuration and backend. + +In particular, organizations commonly want to create a strong separation +between multiple deployments of the same infrastructure serving different +development stages or different internal teams. In this case, the backend for each deployment often has different credentials and access controls. CLI workspaces within a working directory use the same backend, so they are not a suitable isolation mechanism for this scenario. + +## Alternatives to Workspaces + +Instead of creating CLI workspaces, you can use one or more [re-usable modules](/terraform/language/modules/develop) to represent the common elements and then represent each instance as a separate configuration that instantiates those common elements in the context of a different [backend](/terraform/language/backend). The root module of each configuration consists only of a backend configuration and a small number of `module` blocks with arguments describing any small differences between the deployments. + +When multiple configurations represent distinct system components rather than multiple deployments, you can pass data from one component to another using paired resources types and data sources. + +- When a shared [Consul](https://www.consul.io/) cluster is available, use [`consul_key_prefix`](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/resources/key_prefix) to publish to the key/value store and [`consul_keys`](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/data-sources/keys) to retrieve those values in other configurations. + +- In systems that support user-defined labels or tags, use a tagging convention to make resources automatically discoverable. For example, use [the `aws_vpc` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) to assign suitable tags and then [the `aws_vpc` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) to query by those tags in other configurations. + +- For server addresses, use a provider-specific resource to create a DNS record with a predictable name. Then you can either use that name directly or use [the `dns` provider](https://registry.terraform.io/providers/hashicorp/dns/latest/docs) to retrieve the published addresses in other configurations. + +- If you store a Terraform state for one configuration in a remote backend that other configurations can access, then the other configurations can use [`terraform_remote_state`](/terraform/language/state/remote-state-data) to directly consume its root module outputs. This setup creates a tighter coupling between configurations, and the root configuration does not need to publish its results in a separate system. + + +## Interactions with HCP Terraform Workspaces + +HCP Terraform organizes infrastructure using workspaces, but its workspaces +act more like completely separate working directories. Each HCP Terraform workspace has its own Terraform configuration, set of variable values, state data, run history, and settings. -These two kinds of workspaces are different, but related. When [using Terraform -CLI as a frontend for Terraform Cloud](/cli/cloud), you can associate the current working -directory with one or more remote workspaces. If you associate the -directory with multiple workspaces (using workspace tags), you can use the -`terraform workspace` commands to select which remote workspace to use. +When you [integrate Terraform CLI with HCP Terraform](/terraform/cli/cloud), you can associate the current CLI working directory with one or more remote HCP Terraform workspaces. Then, use the `terraform workspace` commands to select the remote workspace you want to use for each run. -Refer to [CLI-driven Runs](/cloud-docs/run/cli) in the Terraform Cloud documentation for more details about using Terraform CLI with Terraform Cloud. +Refer to [CLI-driven Runs](/terraform/cloud-docs/run/cli) in the HCP Terraform documentation for more details. + + +## Workspace Internals + +Workspaces are technically equivalent to renaming your state file. Terraform then includes a set of protections and support for remote state. + +Workspaces are also meant to be a shared resource. They are not private, unless you use purely local state and do not commit your state to version control. + +For local state, Terraform stores the workspace states in a directory called `terraform.tfstate.d`. This directory should be treated similarly to local-only `terraform.tfstate`. Some teams commit these files to version control, but we recommend using a remote backend instead when there are multiple collaborators. + +For [remote state](/terraform/language/state/remote), the workspaces are stored directly in the configured [backend](/terraform/language/backend). For example, if you use [Consul](/terraform/language/backend/consul), the workspaces are stored by appending the workspace name to the state path. To ensure that workspace names are stored correctly and safely in all backends, the name must be valid to use in a URL path segment without escaping. + +Terraform stores the current workspace name locally in the ignored `.terraform` directory. This allows multiple team members to work on different workspaces concurrently. Workspace names are also attached to associated remote workspaces in HCP Terraform. For more details about workspace names in HCP Terraform, refer to the [CLI Integration (recommended)](/terraform/cli/cloud/settings#arguments) and [remote backend](/terraform/language/backend/remote#workspaces) and documentation. diff --git a/website/docs/configuration/expressions.mdx b/website/docs/configuration/expressions.mdx deleted file mode 100644 index f9ac614688..0000000000 --- a/website/docs/configuration/expressions.mdx +++ /dev/null @@ -1,121 +0,0 @@ ---- -page_title: Expressions Landing Page - Configuration Language ---- - -# Expressions Landing Page - -To improve navigation, we've split the old Expressions page into several smaller -pages. - - - -## Types and Values, Literal Expressions, Indices and Attributes - -Terraform's types are `string`, `number`, `bool`, `list`, `tuple`, `map`, -`object`, and `null`. - -This information has moved to -[Types and Values](/language/expressions/types). - -
- - - -## References to Named Values (Resource Attributes, Variables, etc.) - -You can refer to certain values by name, like `var.some_variable` or -`aws_instance.example.ami`. - -This information has moved to -[References to Values](/language/expressions/references). - -
- - - -## Arithmetic and Logical Operators - -Operators are expressions that transform other expressions, like adding two -numbers (`+`) or comparing two values to get a bool (`==`, `>=`, etc.). - -This information has moved to -[Operators](/language/expressions/operators). - -
- -## Conditional Expressions - -The `condition ? true_val : false_val` expression chooses between two -expressions based on a bool condition. - -This information has moved to -[Conditional Expressions](/language/expressions/conditionals). - -
- - - -## Function Calls - -Terraform's functions can be called like `function_name(arg1, arg2)`. - -This information has moved to -[Function Calls](/language/expressions/function-calls). - -
- - - -## `for` Expressions - -Expressions like `[for s in var.list : upper(s)]` can transform a complex type -value into another complex type value. - -This information has moved to -[For Expressions](/language/expressions/for). - -
- - - -## Splat Expressions - -Expressions like `var.list[*].id` can extract simpler collections from complex -collections. - -This information has moved to -[Splat Expressions](/language/expressions/splat). - -
- - - -## `dynamic` Blocks - -The special `dynamic` block type serves the same purpose as a `for` expression, -except it creates multiple repeatable nested blocks instead of a complex value. - -This information has moved to -[Dynamic Blocks](/language/expressions/dynamic-blocks). - -
- - - -## String Literals and String Templates - -Strings can be `"double-quoted"` or - -```hcl -< diff --git a/website/docs/configuration/index.mdx b/website/docs/configuration/index.mdx deleted file mode 100644 index 046abeac20..0000000000 --- a/website/docs/configuration/index.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -page_title: Terraform Configuration ---- - -# Terraform Configuration diff --git a/website/docs/configuration/modules.mdx b/website/docs/configuration/modules.mdx deleted file mode 100644 index b6a0adeed5..0000000000 --- a/website/docs/configuration/modules.mdx +++ /dev/null @@ -1,39 +0,0 @@ ---- -page_title: Modules Landing Page - Configuration Language ---- - -# Modules Landing Page - -To improve navigation, we've split the old Modules page into several smaller -pages. - - - -## Syntax and Elements of Module Blocks - -This information has moved to -[Module Blocks](/language/modules/syntax). - -
- - - -## Multiple Instances with `count` and `for_each` - -This information has moved to -[`count`](/language/meta-arguments/count) and -[`for_each`](/language/meta-arguments/for_each). - -
- - - -## Handling Provider Configurations in Re-usable Modules - -This information has moved to -[The `providers` Meta-Argument](/language/meta-arguments/module-providers) -(for users of re-usable modules) and -[Providers Within Modules](/language/modules/develop/providers) -(for module developers). - -
diff --git a/website/docs/configuration/resources.mdx b/website/docs/configuration/resources.mdx deleted file mode 100644 index 7f9489afd6..0000000000 --- a/website/docs/configuration/resources.mdx +++ /dev/null @@ -1,93 +0,0 @@ ---- -page_title: Resources Landing Page - Configuration Language ---- - -# Resources Landing Page - -To improve navigation, we've split the old Resources page into several smaller -pages. - - - -## Syntax and Elements of Resource Blocks - -This information has moved to -[Resource Blocks](/language/resources/syntax). - -
- - - -## Details of Resource Behavior - -This information has moved to -[Resource Behavior](/language/resources/behavior). - -
- -## Resource Meta-Arguments - -Each resource meta-argument has moved to its own page: - -- [`depends_on`](/language/meta-arguments/depends_on) -- [`count`](/language/meta-arguments/count) -- [`for_each`](/language/meta-arguments/for_each) -- [`provider`](/language/meta-arguments/resource-provider) -- [`lifecycle`](/language/meta-arguments/lifecycle) -- [Provisioners](/language/resources/provisioners) - -
- - - -### `depends_on` - -This information has moved to -[`depends_on`](/language/meta-arguments/depends_on). - -
- - - -### `count` - -This information has moved to -[`count`](/language/meta-arguments/count). - -
- - - -### `for_each` - -This information has moved to -[`for_each`](/language/meta-arguments/for_each). - -
- - - -### `provider` - -This information has moved to -[`provider`](/language/meta-arguments/resource-provider). - -
- - - -### `lifecycle` - -This information has moved to -[`lifecycle`](/language/meta-arguments/lifecycle). - -
- - - -### Provisioners - -This information has moved to -[Provisioners](/language/resources/provisioners). - -
diff --git a/website/docs/internals/archiving.mdx b/website/docs/internals/archiving.mdx index 5c71326b7d..354bcb3750 100644 --- a/website/docs/internals/archiving.mdx +++ b/website/docs/internals/archiving.mdx @@ -7,7 +7,7 @@ description: >- --- # Archiving Providers @@ -23,6 +23,6 @@ What does archiving mean? 1. No new releases will be published. 1. Nightly acceptance tests may not be run. -HashiCorp may archive a provider when we or the community are not able to support it at a level consistent with our open source guidelines and community expectations. +HashiCorp may archive a provider when we or the community are not able to support it at a level consistent with our guidelines and community expectations. -Archiving is reversible. If anyone from the community is willing to maintain an archived provider, please reach out to the [Terraform Provider Development Program](/docs/partnerships) at __. +Archiving is reversible. If anyone from the community is willing to maintain an archived provider, please reach out to the [Terraform Provider Development Program](/terraform/docs/partnerships) at __. diff --git a/website/docs/internals/credentials-helpers.mdx b/website/docs/internals/credentials-helpers.mdx index f2be17c1ff..a65d63921b 100644 --- a/website/docs/internals/credentials-helpers.mdx +++ b/website/docs/internals/credentials-helpers.mdx @@ -1,26 +1,28 @@ --- -page_title: Credentials Helpers +page_title: Create Credentials Helpers description: >- - Credentials helpers are external programs that know how to store and retrieve - API tokens for remote Terraform services. + Credentials helpers are external programs that can store and retrieve + API tokens for remote Terraform services. Learn how to create credentials helpers. --- -# Credentials Helpers +# Create Credentials Helpers + +This topic describes how to write and install a credentials helper so that you can customize how Terraform obtains credentials. To learn +how to configure a credentials helper that was already installed, refer to +[Credentials Helpers](/terraform/cli/config/config-file#credentials-helpers) in the Terraform CLI documentation. + +## Introduction For Terraform-specific features that interact with remote network services, -such as [module registries](/registry) and -[remote operations](/cloud-docs/run/cli), Terraform by default looks for +such as [module registries](/terraform/registry) and +[remote operations](/terraform/cloud-docs/run/cli), Terraform by default looks for API credentials to use in these calls in -[the CLI configuration](/cli/config/config-file). +[the CLI configuration](/terraform/cli/config/config-file). Credentials helpers offer an alternative approach that allows you to customize how Terraform obtains credentials using an external program, which can then directly access an existing secrets management system in your organization. -This page is about how to write and install a credentials helper. To learn -how to configure a credentials helper that was already installed, see -[the CLI config Credentials Helpers section](/cli/config/config-file#credentials-helpers). - ## How Terraform finds Credentials Helpers A credentials helper is a normal executable program that is installed in a @@ -29,7 +31,7 @@ particular location and whose name follows a specific naming convention. A credentials helper called "credstore", for example, would be implemented as an executable program named `terraform-credentials-credstore` (with an `.exe` extension on Windows only), and installed in one of the -[default plugin search locations](/plugin/how-terraform-works#plugin-locations). +[default plugin search locations](/terraform/plugin/how-terraform-works#plugin-locations). ## How Terraform runs Credentials Helpers @@ -40,7 +42,7 @@ block in the CLI configuration. For the following examples, we'll assume a "credstore" credentials helper configured as follows: -``` +```hcl credentials_helper "credstore" { args = ["--host=credstore.example.com"] } @@ -56,7 +58,7 @@ The current set of verbs are: To represent credentials, the credentials helper protocol uses a JSON object whose contents correspond with the contents of -[`credentials` blocks in the CLI configuration](/cli/config/config-file#credentials). +[`credentials` blocks in the CLI configuration](/terraform/cli/config/config-file#credentials). To represent an API token, the object contains a property called "token" whose value is the token string: @@ -140,8 +142,8 @@ stream and then exiting with a non-zero status code. ## Handling Unsupported Credentials Object Properties -Currently Terraform defines only the `token` property within JSON credentials -objects, but this format might be extended in future. +Terraform defines only the `token` property within JSON credentials +objects. If a credentials helper is asked to store an object that has any properties other than `token` and if it is not able to faithfully retain them then it @@ -159,7 +161,7 @@ other properties as described above. Terraform does not have any automatic installation mechanism for credentials helpers. Instead, the user must extract the helper program executable into -one of the [default plugin search locations](/plugin/how-terraform-works#plugin-locations). +one of the [default plugin search locations](/terraform/plugin/how-terraform-works#plugin-locations). If you are packaging a credentials helper for distribution, place it in an named with the expected naming scheme (`terraform-credentials-example`) and, diff --git a/website/docs/internals/debugging.mdx b/website/docs/internals/debugging.mdx index d7777bdbf2..31e9ba4d75 100644 --- a/website/docs/internals/debugging.mdx +++ b/website/docs/internals/debugging.mdx @@ -1,16 +1,18 @@ --- -page_title: Debugging +page_title: Enable logs to debug Terraform description: >- - Terraform has detailed logs which can be enabled by setting the TF_LOG - environment variable to any value. This will cause detailed logs to appear on - stderr + Enable Terraform to generate logs so that you can debug unexpected behaviors. --- -# Debugging Terraform +# Enable Terraform logs -> **Hands-on:** Try the [Create Dynamic Expressions](https://learn.hashicorp.com/tutorials/terraform/troubleshooting-workflow#bug-reporting-best-practices?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +This topic describes how to enable Terraform logs so that you can debug unexpected behaviors. -Terraform has detailed logs which can be enabled by setting the `TF_LOG` environment variable to any value. This will cause detailed logs to appear on stderr. +> **Hands-on:** Try the [Create Dynamic Expressions](/terraform/tutorials/configuration-language/troubleshooting-workflow#bug-reporting-best-practices?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. + +## Set the `TF_LOG` variable + +Terraform has detailed logs that you can enable by setting the `TF_LOG` environment variable to any value. Enabling this setting causes detailed logs to appear on `stderr`. You can set `TF_LOG` to one of the log levels (in order of decreasing verbosity) `TRACE`, `DEBUG`, `INFO`, `WARN` or `ERROR` to change the verbosity of the logs. @@ -18,7 +20,9 @@ Setting `TF_LOG` to `JSON` outputs logs at the `TRACE` level or higher, and uses ~> **Warning:** The JSON encoding of log files is not considered a stable interface. It may change at any time, without warning. It is meant to support tooling that will be forthcoming, and that tooling is the only supported way to interact with JSON formatted logs. -Logging can be enabled separately for terraform itself and the provider plugins +## Set the `TF_LOG_CORE` variable + +Logging can be enabled separately for Terraform itself and the provider plugins using the `TF_LOG_CORE` or `TF_LOG_PROVIDER` environment variables. These take the same level arguments as `TF_LOG`, but only activate a subset of the logs. diff --git a/website/docs/internals/functions-meta.mdx b/website/docs/internals/functions-meta.mdx new file mode 100644 index 0000000000..b227843f72 --- /dev/null +++ b/website/docs/internals/functions-meta.mdx @@ -0,0 +1,101 @@ +--- +page_title: terraform metadata functions command reference +description: >- + The `terraform metadata functions` command prints signatures for all the + functions available in the current Terraform version. +--- + +# `terraform metadata functions` command + +The `terraform metadata functions` command prints signatures for the functions available in the current Terraform version. + +-> `terraform metadata functions` requires **Terraform v1.4 or later**. + +## Usage + +Usage: `terraform metadata functions [options]` + +The following flags are available: + +- `-json` - Displays the function signatures in a machine-readable, JSON format. + +Please note that, at this time, the `-json` flag is a _required_ option. In future releases, this command will be extended to allow for additional options. + +The output includes a `format_version` key, which as of Terraform 1.4.0 has +value `"1.0"`. The semantics of this version are: + +- We will increment the minor version, e.g. `"1.1"`, for backward-compatible + changes or additions. Ignore any object properties with unrecognized names to + remain forward-compatible with future minor versions. +- We will increment the major version, e.g. `"2.0"`, for changes that are not + backward-compatible. Reject any input which reports an unsupported major + version. + +We will introduce new major versions only within the bounds of +[the Terraform 1.0 Compatibility Promises](/terraform/language/v1-compatibility-promises). + +## Format Summary + +The following sections describe the JSON output format by example, using a pseudo-JSON notation. +Important elements are described with comments, which are prefixed with `//`. +To avoid excessive repetition, we've split the complete format into several discrete sub-objects, described under separate headers. References wrapped in angle brackets (like ``) are placeholders which, in the real output, would be replaced by an instance of the specified sub-object. + +The JSON output format consists of the following objects and sub-objects: + +- [Function Signature Representation](#function-signature-representation) - the top-level object returned by `terraform metadata functions -json` +- [Parameter Representation](#parameter-representation) - a sub-object of signatures that describes their parameters + +## Function Signature Representation + +```javascript +{ + "format_version": "1.0", + + // "function_signatures" describes the signatures for all + // available functions. + "function_signatures": { + // keys in this map are the function names, such as "abs" + "example_function": { + // "description" is an English-language description of + // the purpose and usage of the function in Markdown. + "description": "string", + + // "return_type" is a representation of a type specification + // that the function returns. + "return_type": "string", + + // "parameters" is an optional list of the positional parameters + // that the function accepts. + "parameters": [ + , + … + ], + + // "variadic_parameter" is an optional representation of the + // additional arguments that the function accepts after those + // matching with the fixed parameters. + "variadic_parameter": + }, + "example_function_two": { … } + } +} +``` + +## Parameter Representation + +A parameter representation describes a parameter to a function. + +```javascript +{ + // "name" is the internal name of the parameter + "name": "string", + + // "description" is an optional English-language description of + // the purpose and usage of the parameter in Markdown. + "description": "string", + + // "type" is a representation of a type specification + // that the parameter's value must conform to. + "type": "string" +} +``` diff --git a/website/docs/internals/graph.mdx b/website/docs/internals/graph.mdx index 0f9c3f2795..087be693da 100644 --- a/website/docs/internals/graph.mdx +++ b/website/docs/internals/graph.mdx @@ -1,38 +1,24 @@ --- -page_title: Resource Graph +page_title: Dependency Graph description: >- - Terraform builds a dependency graph from the Terraform configurations, and - walks this graph to generate plans, refresh state, and more. This page - documents the details of what are contained in this graph, what types of nodes - there are, and how the edges of the graph are determined. + Learn how Terraform builds a dependency graph from the Terraform configurations and + uses the graph to generate plans, refresh state, perform other operations. --- -# Resource Graph +# Dependency Graph -Terraform builds a -[dependency graph](https://en.wikipedia.org/wiki/Dependency_graph) -from the Terraform configurations, and walks this graph to -generate plans, refresh state, and more. This page documents -the details of what are contained in this graph, what types -of nodes there are, and how the edges of the graph are determined. +This topic explains the dependency graph Terraform builds from Terraform configurations. This is an advanced topic and not required to understand how to use Terraform. -~> **Advanced Topic!** This page covers technical details -of Terraform. You don't need to understand these details to -effectively use Terraform. The details are documented here for -those who wish to learn about them without having to go -spelunking through the source code. +## Introduction -For some background on graph theory, and a summary of how -Terraform applies it, see the HashiCorp 2016 presentation +Terraform builds a dependency graph and uses it to perform operations, such as generate plans and refresh state. +For background on graph theory and a summary of how +Terraform applies it, refer the HashiCorp 2016 presentation [_Applying Graph Theory to Infrastructure as Code_](https://www.youtube.com/watch?v=Ce3RNfRbdZ0). -This presentation also covers some similar ideas to the following -guide. ## Graph Nodes -There are only a handful of node types that can exist within the -graph. We'll cover these first before explaining how they're -determined and built: +The following node types can exist within the graph: - **Resource Node** - Represents a single resource. If you have the `count` metaparameter set, then there will be one resource @@ -110,8 +96,8 @@ The amount of parallelism is limited using a semaphore to prevent too many concurrent operations from overwhelming the resources of the machine running Terraform. By default, up to 10 nodes in the graph will be processed concurrently. This number can be set using the `-parallelism` flag on the -[plan](/cli/commands/plan), [apply](/cli/commands/apply), and -[destroy](/cli/commands/destroy) commands. +[plan](/terraform/cli/commands/plan), [apply](/terraform/cli/commands/apply), and +[destroy](/terraform/cli/commands/destroy) commands. Setting `-parallelism` is considered an advanced operation and should not be necessary for normal usage of Terraform. It may be helpful in certain special diff --git a/website/docs/internals/index.mdx b/website/docs/internals/index.mdx index 5f36d50016..f025c4b7d0 100644 --- a/website/docs/internals/index.mdx +++ b/website/docs/internals/index.mdx @@ -1,18 +1,11 @@ --- -page_title: Internals +page_title: Terraform internals description: >- - Learn the technical details of how Terraform generates and executes - infrastructure plans, works with plugins, obtains credentials, etc. + Learn about internal Terraform processes, such as generating the resource dependency graph. --- -# Terraform Internals +# Terraform internals overview -This section covers the internals of Terraform and explains how -plans are generated, the lifecycle of a provider, etc. The goal -of this section is to remove any notion of "magic" from Terraform. -We want you to be able to trust and understand what Terraform is -doing to function. - --> **Note:** Knowledge of Terraform internals is not -required to use Terraform. If you aren't interested in the internals -of Terraform, you may safely skip this section. +This topic provides overview information about the internals of Terraform. +Topics in this section explain how Terraform generates plans and describe the provider lifecycle. +You do not have to understand Terraform internals to use Terraform. diff --git a/website/docs/internals/json-format.mdx b/website/docs/internals/json-format.mdx index a09adf5036..815f89afab 100644 --- a/website/docs/internals/json-format.mdx +++ b/website/docs/internals/json-format.mdx @@ -1,19 +1,20 @@ --- -page_title: 'Internals: JSON Output Format' +page_title: JSON output format description: >- - Terraform provides a machine-readable JSON representation of state, - configuration and plan. + Learn how to configure Terraform to print JSON-formatted details about state, configuration, and proposed infrastructure plans. --- -# JSON Output Format +# JSON Output Format Overview --> **Note:** This format is available in Terraform 0.12 and later. +This topic provides overview information about the JSON output Terraform prints to the terminal. + +## Introduction When Terraform plans to make changes, it prints a human-readable summary to the terminal. It can also, when run with `-out=`, write a much more detailed binary plan file, which can later be used to apply those changes. -Since the format of plan files isn't suited for use with external tools (and likely never will be), Terraform can output a machine-readable JSON representation of a plan file's changes. It can also convert state files to the same format, to simplify data loading and provide better long-term compatibility. +Terraform can output a machine-readable JSON representation of a plan file's changes. It can also convert state files to the same format, to simplify data loading and provide better long-term compatibility. -Use `terraform show -json ` to generate a JSON representation of a plan or state file. See [the `terraform show` documentation](/cli/commands/show) for more details. +Use `terraform show -json ` to generate a JSON representation of a plan or state file. See [the `terraform show` documentation](/terraform/cli/commands/show) for more details. The output includes a `format_version` key, which as of Terraform 1.1.0 has value `"1.0"`. The semantics of this version are: @@ -25,9 +26,6 @@ value `"1.0"`. The semantics of this version are: backward-compatible. Reject any input which reports an unsupported major version. -We will introduce new major versions only within the bounds of -[the Terraform 1.0 Compatibility Promises](/language/v1-compatibility-promises). - ## Format Summary The following sections describe the JSON output format by example, using a pseudo-JSON notation. @@ -49,7 +47,7 @@ The JSON output format consists of the following objects and sub-objects: ## State Representation -Because state does not currently have any significant metadata not covered by the common values representation ([described below](#values-representation)), the `` is straightforward: +State does not have any significant metadata not included in the common [values representation](#values-representation), so the `` uses the following format: ```javascript { @@ -61,8 +59,6 @@ Because state does not currently have any significant metadata not covered by th } ``` -The extra wrapping object here will allow for any extension we may need to add in future versions of this format. - ## Plan Representation A plan consists of a prior state, the configuration that is being applied to that state, and the set of changes Terraform plans to make to achieve that. @@ -77,6 +73,34 @@ For ease of consumption by callers, the plan representation includes a partial r // being applied to, using the state representation described above. "prior_state": , + // "applyable" indicates that it would make sense for a wrapping automation + // to try to apply this plan, possibly after asking a human operator for + // approval. + // + // Other attributes may give additional context about why the plan is not + // applyable, but wrapping automations should use this flag as their + // primary condition to accommodate potential changes to the exact definition + // of "applyable" in future Terraform versions. + "applyable": true, + + // "complete" indicates that Terraform expects that after applying this + // plan the actual state will match the desired state. + // + // An incomplete plan is expected to require at least one additional + // plan/apply round to achieve convergence, and so wrapping automations + // should ideally either automatically start a new plan/apply round after + // this plan is applied, or prompt the operator that they should do so. + // + // Other attributes may give additional context about why the plan is not + // complete, but wrapping automations should use this flag as their + // primary condition to accommodate potential changes to the exact definition + // of "complete" in future Terraform versions. + "complete": true, + + // "errored" indicates whether planning failed. An errored plan cannot be applied, + // but the actions planned before failure may help to understand the error. + "errored": false, + // "configuration" is a representation of the configuration being applied to the // prior state, using the configuration representation described above. "configuration": , @@ -184,7 +208,7 @@ For ease of consumption by callers, the plan representation includes a partial r // // If there is no special reason to note, Terraform will omit this // property altogether. - action_reason: "replace_because_tainted" + "action_reason": "replace_because_tainted" } ], @@ -246,7 +270,7 @@ The following example illustrates the structure of a ``: ```javascript { // "outputs" describes the outputs from the root module. Outputs from - // descendent modules are not available because they are not retained in all + // descendant modules are not available because they are not retained in all // of the underlying structures we will build this values representation from. "outputs": { "private_ip": { @@ -340,7 +364,7 @@ The following example illustrates the structure of a ``: } ``` -The translation of attribute and output values is the same intuitive mapping from HCL types to JSON types used by Terraform's [`jsonencode`](/language/functions/jsonencode) function. This mapping does lose some information: lists, sets, and tuples all lower to JSON arrays while maps and objects both lower to JSON objects. Unknown values and null values are both treated as absent or null. +The translation of attribute and output values is the same intuitive mapping from HCL types to JSON types used by Terraform's [`jsonencode`](/terraform/language/functions/jsonencode) function. This mapping does lose some information: lists, sets, and tuples all lower to JSON arrays while maps and objects both lower to JSON objects. Unknown values and null values are both treated as absent or null. Output values include a `"type"` field, which is a [serialization of the value's type](https://pkg.go.dev/github.com/zclconf/go-cty/cty#Type.MarshalJSON). For primitive types this is a string value, such as `"number"` or `"bool"`. Complex types are represented as a nested JSON array, such as `["map","string"]` or `["object",{"a":"number"}]`. This can be used to reconstruct the output value with the correct type. @@ -378,7 +402,7 @@ Because the configuration models are produced at a stage prior to expression eva "alias": "foo", // "module_address" is included only for provider configurations that are - // declared in a descendent module, and gives the opaque address for the + // declared in a descendant module, and gives the opaque address for the // module that contains the provider configuration. "module_address": "module.child", @@ -390,7 +414,7 @@ Because the configuration models are produced at a stage prior to expression eva }, // "root_module" describes the root module in the configuration, and serves - // as the root of a tree of similar objects describing descendent modules. + // as the root of a tree of similar objects describing descendant modules. "root_module": { // "outputs" describes the output value configurations in the module. @@ -608,13 +632,20 @@ A `` describes the change to the indicated object. // replacement (for example, if the resource was tainted). Each path // consists of one or more steps, each of which will be a number or a // string. - "replace_paths": [["triggers"]] + "replace_paths": [["triggers"]], + + // "importing" is present only when the object is being imported as part + // of this change. + "importing": { + // "id" is the import ID of the object being imported. + "id": "foo" + } } ``` ## Checks Representation -~> **Warning:** The JSON representation of "checks" is currently experimental +~> **Warning:** The JSON representation of checks is experimental and some details may change in future Terraform versions based on feedback, even in minor releases of Terraform CLI. @@ -629,8 +660,7 @@ A `` describes the current state of a checkable object in // "kind" specifies what kind of checkable object this is. Different // kinds of object will have different additional properties inside the // address object, but all kinds include both "kind" and "to_display". - // Currently the two valid kinds are "resource" and "output_value", but - // additional kinds may be added in future Terraform versions. + // The two valid kinds are "resource" and "output_value". "kind": "resource", // "to_display" contains an opaque string representation of the address diff --git a/website/docs/internals/lifecycle.mdx b/website/docs/internals/lifecycle.mdx deleted file mode 100644 index 1cb489a51a..0000000000 --- a/website/docs/internals/lifecycle.mdx +++ /dev/null @@ -1,59 +0,0 @@ ---- -page_title: Resource Lifecycle -description: >- - Resources have a strict lifecycle, and can be thought of as basic state - machines. Understanding this lifecycle can help better understand how - Terraform generates an execution plan, how it safely executes that plan, and - what the resource provider is doing throughout all of this. ---- - -# Resource Lifecycle - -Resources have a strict lifecycle, and can be thought of as basic -state machines. Understanding this lifecycle can help better understand -how Terraform generates an execution plan, how it safely executes that -plan, and what the resource provider is doing throughout all of this. - -~> **Advanced Topic!** This page covers technical details -of Terraform. You don't need to understand these details to -effectively use Terraform. The details are documented here for -those who wish to learn about them without having to go -spelunking through the source code. - -## Lifecycle - -A resource roughly follows the steps below: - -1. `ValidateResource` is called to do a high-level structural - validation of a resource's configuration. The configuration - at this point is raw and the interpolations have not been processed. - The value of any key is not guaranteed and is just meant to be - a quick structural check. - -1. `Diff` is called with the current state and the configuration. - The resource provider inspects this and returns a diff, outlining - all the changes that need to occur to the resource. The diff includes - details such as whether or not the resource is being destroyed, what - attribute necessitates the destroy, old values and new values, whether - a value is computed, etc. It is up to the resource provider to - have this knowledge. - -1. `Apply` is called with the current state and the diff. Apply does - not have access to the configuration. This is a safety mechanism - that limits the possibility that a provider changes a diff on the - fly. `Apply` must apply a diff as prescribed and do nothing else - to remain true to the Terraform execution plan. Apply returns the - new state of the resource (or nil if the resource was destroyed). - -1. If a resource was just created and did not exist before, and the - apply succeeded without error, then the provisioners are executed - in sequence. If any provisioner errors, the resource is marked as - _tainted_, so that it will be destroyed on the next apply. - -## Partial State and Error Handling - -If an error happens at any stage in the lifecycle of a resource, -Terraform stores a partial state of the resource. This behavior is -critical for Terraform to ensure that you don't end up with any -_zombie_ resources: resources that were created by Terraform but -no longer managed by Terraform due to a loss of state. diff --git a/website/docs/internals/login-protocol.mdx b/website/docs/internals/login-protocol.mdx index e558a6eb43..c3394866de 100644 --- a/website/docs/internals/login-protocol.mdx +++ b/website/docs/internals/login-protocol.mdx @@ -1,28 +1,27 @@ --- -page_title: Login Protocol +page_title: Login protocol reference description: >- - The login protocol is used for authenticating Terraform against servers - providing Terraform-native services. + The login protocol authenticates Terraform against servers that provide Terraform-native services. Learn about the login protocol. --- -# Server-side Login Protocol +# Login Protocol Reference -~> **Note:** You don't need to read these docs to _use_ -[`terraform login`](/cli/commands/login). The information below is for -anyone intending to implement the server side of `terraform login` in order to -offer Terraform-native services in a third-party system. +This topic provides reference information about the login protocol Terraform uses for authentication against servers that profide Terraform-native services. You can use this reference information to offer Terraform-native services in a third-party system. + +## Introduction The `terraform login` command supports performing an OAuth 2.0 authorization request using configuration provided by the target host. You may wish to implement this protocol if you are producing a third-party implementation of -any [Terraform-native services](/internals/remote-service-discovery), +any [Terraform-native services](/terraform/internals/remote-service-discovery), such as a Terraform module registry. -First, Terraform uses -[remote service discovery](/internals/remote-service-discovery) to +## OAuth Configuration + +Terraform uses [remote service discovery](/terraform/internals/remote-service-discovery) to find the OAuth configuration for the host. The host must support the service name `login.v1` and define for it an object containing OAuth client -configuration values, like this: +configuration values. The following example describes a client that authenticates at ports `10000` and `10010`: ```json { @@ -51,14 +50,12 @@ The properties within the discovery object are as follows: specific mechanism by which an OAuth server authenticates the request and issues an authorization token. - Terraform CLI currently only supports a single grant type: + Terraform CLI supports a single grant type: * `authz_code`: [authorization code grant](https://tools.ietf.org/html/rfc6749#section-4.1). Both the `authz` and `token` properties are required when `authz_code` is present. - Other grant types may be supported in future versions of Terraform CLI, - and may impose different requirements on the `authz` and `token` properties. If not specified, `grant_types` defaults to `["authz_code"]`. * `authz` (Required if needed for a given grant type): the server's @@ -107,7 +104,7 @@ authorization errors once the token expires, after which the user can run -> **Note:** As a special case, Terraform can use a [Resource Owner Password Credentials Grant](https://tools.ietf.org/html/rfc6749#section-4.3) -only when interacting with `app.terraform.io` ([Terraform Cloud](/cloud)), +only when interacting with `app.terraform.io` ([HCP Terraform](https://cloud.hashicorp.com/products/terraform)), under the recommendation in the OAuth specification to use this grant type only when the client and server are closely related. The `password` grant type is not supported for any other hostname and will be ignored. diff --git a/website/docs/internals/machine-readable-ui.mdx b/website/docs/internals/machine-readable-ui.mdx index bbb3025d8a..05f4c20fb2 100644 --- a/website/docs/internals/machine-readable-ui.mdx +++ b/website/docs/internals/machine-readable-ui.mdx @@ -1,17 +1,18 @@ --- -page_title: 'Internals: Machine-Readable UI' +page_title: Machine-readable output reference description: >- - Terraform provides a machine-readable streaming JSON UI output for plan, - apply, and refresh operations. + Terraform streams machine-readable output in JSON format, letting you use third-party tools to monitor operations. --- -# Machine-Readable UI +# Machine-readable UI Output Reference --> **Note:** This format is available in Terraform 0.15.3 and later. +This topic provides reference information about the machine-readable output Terraform prints. -By default, many Terraform commands display UI output as unstructured text, intended to be read by a user via a terminal emulator. This text stream is not a stable interface for integrations. Some commands support a `-json` flag, which enables a structured JSON output mode with a defined interface. +## Introduction -For long-running commands such as `plan`, `apply`, and `refresh`, the `-json` flag outputs a stream of JSON UI messages, one per line. These can be processed one message at a time, with integrating software filtering, combining, or modifying the output as desired. +By default, many Terraform commands display UI output as unstructured text that is intended to be read in a terminal emulator. This text stream is not a stable interface for integrations. Some commands support a `-json` flag, which enables a structured JSON output mode with a defined interface. + +For long-running commands such as `plan`, `apply`, `refresh`, and `test`, the `-json` flag outputs a stream of JSON UI messages. The ouptut messages appear one per line so that you can process the messages by integrating software filtering, combining, or modifying the output as desired. The first message output has type `version`, and includes a `ui` key, which as of Terraform 1.1.0 has value `"1.0"`. The semantics of this version are: @@ -23,8 +24,8 @@ value `"1.0"`. The semantics of this version are: backward-compatible. Reject any input which reports an unsupported major version. -We will introduce new major versions only within the bounds of -[the Terraform 1.0 Compatibility Promises](/language/v1-compatibility-promises). +Refer to +[Terraform 1.0 Compatibility Promises](/terraform/language/v1-compatibility-promises) for additional information. ## Sample JSON Output @@ -60,7 +61,7 @@ The following message types are supported: - `version`: information about the Terraform version and the version of the schema used for the following messages - `log`: unstructured human-readable log lines -- `diagnostic`: diagnostic warning or error messages; [see the `terraform validate` docs for more details on the format](/cli/commands/validate#json) +- `diagnostic`: diagnostic warning or error messages; [see the `terraform validate` docs for more details on the format](/terraform/cli/commands/validate#json) ### Operation Results @@ -75,6 +76,17 @@ The following message types are supported: - `provision_start`, `provision_progress`, `provision_complete`, `provision_errored`: sequence of messages indicating progress of a single provisioner step - `refresh_start`, `refresh_complete`: sequence of messages indicating progress of a single resource through refresh +### Test Results + +- `test_abstract`: describes the test operation that Terraform executes +- `test_file`: describes the status of a completed test file +- `test_run`: describes the status of a completed `run` block within a test file +- `test_cleanup`: describes the result of the test cleanup after a completed test file +- `test_summary`: describes the overall status of the completed test operation +- `test_plan`: describes the output of a given `run` block when `command = plan` +- `test_state`: describes the output of a given `run` block when `command = apply` +- `test_interrupt`: describes the result of an interrupted test operation + ## Version Message A machine-readable UI command output will always begin with a `version` message. The following message-specific keys are defined: @@ -103,7 +115,7 @@ If drift is detected during planning, Terraform will emit a `resource_drift` mes - `resource`: object describing the address of the resource to be changed; see [resource object](#resource-object) below for details - `action`: the action planned to be taken for the resource. Values: `update`, `delete`. -This message does not include details about the exact changes which caused the change to be planned. That information is available in [the JSON plan output](/internals/json-format). +This message does not include details about the exact changes which caused the change to be planned. That information is available in [the JSON plan output](/terraform/internals/json-format). ### Example @@ -135,8 +147,8 @@ At the end of a plan or before an apply, Terraform will emit a `planned_change` - `resource`: object describing the address of the resource to be changed; see [resource object](#resource-object) below for details - `previous_resource`: object describing the previous address of the resource, if this change includes a configuration-driven move -- `action`: the action planned to be taken for the resource. Values: `noop`, `create`, `read`, `update`, `replace`, `delete`, `move`. -- `reason`: an optional reason for the change, currently only used when the action is `replace` or `delete`. Values: +- `action`: the action planned to be taken for the resource. Values: `noop`, `create`, `read`, `update`, `replace`, `delete`, `move`, `import`. +- `reason`: an optional reason for the change, only used when the action is `replace` or `delete`. Values: - `tainted`: resource was marked as tainted - `requested`: user requested that the resource be replaced, for example via the `-replace` plan flag - `cannot_update`: changes to configuration force the resource to be deleted and created rather than updated @@ -146,7 +158,7 @@ At the end of a plan or before an apply, Terraform will emit a `planned_change` - `delete_because_each_key`: resource instance key is not included in the `for_each` argument - `delete_because_no_module`: enclosing module instance is not in configuration -This message does not include details about the exact changes which caused the change to be planned. That information is available in [the JSON plan output](/internals/json-format). +This message does not include details about the exact changes which caused the change to be planned. That information is available in [the JSON plan output](/terraform/internals/json-format). ### Example @@ -243,6 +255,10 @@ Performing Terraform operations to a resource will often result in several messa - `provision_errored`: when an error is enountered during provisioning - `refresh_start`: when reading a resource during refresh - `refresh_complete`: on successful refresh +- `ephemeral_op_start`: when starting an ephemeral resource operation +- `ephemeral_op_progress`: periodically showing elapsed time output during ephemeral resource operation +- `ephemeral_op_complete`: on successful ephemeral resource operation completion +- `ephemeral_op_errored`: when an error is encountered during ephemeral resource operation Each of these messages has a `hook` object, which has different fields for each type. All hooks have a [`resource` object](#resource-object) which identifies which resource is the subject of the operation. @@ -574,6 +590,138 @@ The `refresh_complete` message `hook` object has the following keys: } ``` +## Ephemeral Operation Start + +The `ephemeral_op_start` message `hook` object has the following keys: + +- `resource`: a [`resource` object](#resource-object) identifying the resource +- `action`: the action the ephemeral resource is going through. Values: `open`, `renew`, `close` + +### Example + +```json +{ + "@level": "info", + "@message": "ephemeral.random_password.example: Opening...", + "@module": "terraform.ui", + "@timestamp": "2024-10-30T10:34:26.222465-00:00", + "hook": { + "resource": { + "addr": "ephemeral.random_password.example", + "module": "", + "resource": "ephemeral.random_password.example", + "implied_provider": "random", + "resource_type": "random_password", + "resource_name": "example", + "resource_key": null + }, + "action": "open" + }, + "type": "ephemeral_op_start" +} +``` + +## Ephemeral Operation Progress + +The `ephemeral_op_progress` message `hook` object has the following keys: + +- `resource`: a [`resource` object](#resource-object) identifying the resource +- `action`: the action the ephemeral resource is going through. Values: `open`, `renew`, `close` +- `elapsed_seconds`: time elapsed since the operation started, expressed as an integer number of seconds + +### Example + +```json +{ + "@level": "info", + "@message": "ephemeral.random_password.example: Closing... [3s elapsed]", + "@module": "terraform.ui", + "@timestamp": "2024-10-30T10:34:26.222465-00:00", + "hook": { + "resource": { + "addr": "ephemeral.random_password.example", + "module": "", + "resource": "ephemeral.random_password.example", + "implied_provider": "random", + "resource_type": "random_password", + "resource_name": "example", + "resource_key": null + }, + "action": "close", + "elapsed_seconds": 3 + }, + "type": "ephemeral_op_progress" +} +``` + +## Ephemeral Operation Complete + +The `ephemeral_op_start` message `hook` object has the following keys: + +- `resource`: a [`resource` object](#resource-object) identifying the resource +- `action`: the action the ephemeral resource is going through. Values: `open`, `renew`, `close` +- `elapsed_seconds`: time elapsed since the operation started, expressed as an integer number of seconds + +### Example + +```json +{ + "@level": "info", + "@message": "ephemeral.random_password.example: Opening complete after 1s", + "@module": "terraform.ui", + "@timestamp": "2024-10-30T10:34:26.222465-00:00", + "hook": { + "resource": { + "addr": "ephemeral.random_password.example", + "module": "", + "resource": "ephemeral.random_password.example", + "implied_provider": "random", + "resource_type": "random_password", + "resource_name": "example", + "resource_key": null + }, + "action": "open", + "elapsed_seconds": 1 + }, + "type": "ephemeral_op_complete" +} +``` + +## Ephemeral Operation Errored + +The `ephemeral_op_start` message `hook` object has the following keys: + +- `resource`: a [`resource` object](#resource-object) identifying the resource +- `action`: the action the ephemeral resource is going through. Values: `open`, `renew`, `close` +- `elapsed_seconds`: time elapsed since the operation started, expressed as an integer number of seconds + +The exact detail of the error will be rendered as a separate `diagnostic` message. + +### Example + +```json +{ + "@level": "info", + "@message": "ephemeral.random_password.example: Opening errored after 2s", + "@module": "terraform.ui", + "@timestamp": "2024-10-30T10:34:26.222465-00:00", + "hook": { + "resource": { + "addr": "ephemeral.random_password.example", + "module": "", + "resource": "ephemeral.random_password.example", + "implied_provider": "random", + "resource_type": "random_password", + "resource_name": "example", + "resource_key": null + }, + "action": "open", + "elapsed_seconds": 2 + }, + "type": "ephemeral_op_errored" +} +``` + ## Resource Object The `resource` object is a decomposed structure representing a resource address in configuration, which is used to identify which resource a given message is associated with. The object has the following keys: @@ -599,3 +747,320 @@ The `resource` object is a decomposed structure representing a resource address "resource_key": "friend" } ``` + +## Test Abstract + +The `terraform test` command emits a `test_abstract` message describing the test files and `run` blocks that Terraform executes during the upcoming operation. + +### Example + +```json +{ + "@level": "info", + "@message": "Found 1 file and 2 run blocks", + "@module": "terraform.ui", + "@timestamp": "2023-08-09T16:12:30.325582+02:00", + "test_abstract": { + "validation.tftest.hcl": [ + "passed_validation", + "failed_validatation" + ] + }, + "type": "test_abstract" +} +``` + +## Test File + +Throughout a `terraform test` execution, Terraform will produce several progress updates for each test file. The progress field can be `starting`, `teardown`, or `complete`. Each test file will emit each progress update exactly once. When a test file emits the `complete` progress update, it will also include a status field containing one of `pass`, `error`, `fail`, or `skip` denoting the overall status of the completed test file. + +### Example + +```json +{ + "@level": "info", + "@message": "main.tftest.hcl... pass", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@timestamp": "2023-08-09T16:12:30.724368+02:00", + "test_file": { + "path": "validation.tftest.hcl", + "progress": "complete", + "status": "pass" + }, + "type": "test_file" +} +``` + +## Test Run + +While executing `run` blocks within a test file, Terraform will also produce several status updates for each `run` block. The progress field for a `run` block progress update can be `starting`, `running`, `teardown`, and `complete`. The `starting` and `complete` progress updates will be emitted exactly once. While the `running` and `teardown` progress updates can be emitted multiple times. + +The `starting`, `running` and `teardown` updates will also include an `elapsed` field indicating the number of milliseconds the current test operation (for `starting` and `running`) or the current destroy operation (for `teardown`) has been in progress. + +The `complete` progress update will also include a `status` field containing one of `pass`, `error`, `fail`, or `skip` denoting the overall status of the completed `run` block`. + +Not every `run` block will emit `teardown` progress updates, as only the most recently executed `run` blocks reference the latest in-memory state files that need to be torn down. In addition, the `run` block tear down process is only initiated once the overall test file has already emitted its `teardown` status update. This means you can expect the `complete` progress update to be issued for a `run` block before any `teardown` updates are provided. There will always be a `complete` progress update issued by the enclosing test file when the tear down process for all `run` blocks is complete. + +### Examples + +```json +{ + "@level": "info", + "@message": " \"successful_validation\"... pass", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@testrun": "successful_validation", + "@timestamp": "2023-08-09T16:12:30.724407+02:00", + "test_run": { + "path": "main.tftest.hcl", + "run": "successful_validation", + "progress": "complete", + "status": "pass" + }, + "type": "test_run" +} +``` + +```json +{ + "@level": "info", + "@message": " \"successful_validation\"... in progress", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@testrun": "successful_validation", + "@timestamp": "2023-08-09T16:12:30.724407+02:00", + "test_run": { + "path": "main.tftest.hcl", + "run": "successful_validation", + "progress": "running", + "elapsed": 2024 + }, + "type": "test_run" +} +``` + +## Test Cleanup + +After Terraform completes the execution of each test file, `terraform test` may emit a series of `test_cleanup` messages detailing any state it could not destroy. You must locate and destroy the resources listed in these resources manually. + +As the test framework can manage multiple state files for each test file, you can see multiple versions of this message for each state file that holds resources that Terraform could not destroy. Using the `@testrun` field, you can pinpoint the run block that last updated the state file to locate the resources that Terraform could not destroy. + +### Example + +```json +{ + "@level": "info", + "@message": "Terraform left some resources in state after executing main.tftest.hcl, they need to be cleaned up manually.", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@testrun": "successful_validation", + "@timestamp": "2023-08-09T16:12:30.724407+02:00", + "test_cleanup": { + "failed_resources": [ + { + "instance": "aws_instance.primary" + }, + { + "instance": "aws_instance.secondary" + } + ] + }, + "type": "test_cleanup" +} +``` + +## Test Summary + +After the test operation has completed all test files, `terraform test` emits a `test_summary` message with the status of the overall test operation. + +### Example + +```json +{ + "@level": "info", + "@message": "Success! 2 passed, 0 failed.", + "@module": "terraform.ui", + "@timestamp": "2023-08-09T16:26:45.482070+02:00", + "test_summary": { + "status": "pass", + "passed": 2, + "failed": 0, + "errored": 0, + "skipped": 0 + }, + "type": "test_summary" +} +``` + +## Test Plan + +In `-verbose` mode, `terraform test` emits a `test_plan` message for all `run` blocks that executed a plan operation. The `test_plan` message contains a subset of the attributes from the `terraform show -json ` command [output](/terraform/internals/json-format#plan-representation) and the `terraform providers schema -json` command [output](/terraform/cli/commands/providers/schema#providers-schema-representation). + +The message contains the following fields representing the plan: `plan_format_version`, `output_changes`, `resource_changes`, `resource_drift` and `relevant_attributes`. These match the respective fields in the [JSON Plan Representation](/terraform/internals/json-format#plan-representation). + +The message contains the following fields representing the provider schemas: `provider_format_version` and `provider_schemas`. These match the respective fields in the [JSON Providers Schema Representation](/terraform/cli/commands/providers/schema#providers-schema-representation). + +### Example + +```json +{ + "@level": "info", + "@message": "-verbose flag enabled, printing plan", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@testrun": "successful_validation", + "@timestamp": "2023-08-09T17:10:06.211942+02:00", + "test_plan": { + "plan_format_version": "1.2", + "resource_changes": [ + { + "address": "aws_instance.primary", + "mode": "managed", + "type": "aws_instance", + "name": "primary", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "ami": "af84f887-e3eb-9e52-5f8b-8a2803734fd0" + }, + "after_unknown": {}, + "before_sensitive": false, + "after_sensitive": {} + } + } + ], + "provider_format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/aws": { + "provider": { + "version": 0 + }, + "resource_schemas": { + "aws_instance": { + "version": 0, + "block": { + "attributes": { + "ami": { + "type": "string", + "description_kind": "plain", + "required": true + } + }, + "description_kind": "plain" + } + } + } + } + } + }, + "type": "test_plan" +} +``` + +## Test State + +In `-verbose` mode, `terraform test` emits a `test_state` message for all `run` blocks that executed an apply operation. The `test_state` message contains a subset of the `terraform show -json` command [output](/terraform/internals/json-format#state-representation) and the `terraform providers schema -json` command [output](/terraform/cli/commands/providers/schema#providers-schema-representation). + +The message contains the following fields representing the state: `state_format_version`, `root_module`, and `outputs`. These match the respective fields in the [JSON Values Representation](/terraform/internals/json-format#values-representation) that are embedded in complete JSON state representation. + +The message contains the following fields representing the provider schemas: `provider_format_version` and `provider_schemas`. These match the respective fields in the [JSON Providers Schema Representation](/terraform/cli/commands/providers/schema#providers-schema-representation). + +### Example + +```json +{ + "@level": "info", + "@message": "-verbose flag enabled, printing state", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@testrun": "successful_validation", + "@timestamp": "2023-08-09T17:18:21.173008+02:00", + "test_state": { + "state_format_version": "1.0", + "root_module": { + "resources": [ + { + "address": "aws_instance.primary", + "mode": "managed", + "type": "aws_instance", + "name": "primary", + "provider_name": "registry.terraform.io/hashicorp/aws", + "schema_version": 0, + "values": { + "ami": "af84f887-e3eb-9e52-5f8b-8a2803734fd0" + }, + "sensitive_values": {} + } + ] + }, + "provider_format_version": "1.0", + "provider_schemas": { + "registry.terraform.io/hashicorp/aws": { + "provider": { + "version": 0 + }, + "resource_schemas": { + "aws_instance": { + "version": 0, + "block": { + "attributes": { + "ami": { + "type": "string", + "description_kind": "plain", + "required": true + } + }, + "description_kind": "plain" + } + } + } + } + } + }, + "type": "test_state" +} +``` + +## Test Interrupt + +When `terraform test` is impeded or canceled, it emits a `test_interrupt` message that describes the interrupted operation. This message includes any state that Terraform could not destroy before exiting and any leftover resources that Terraform did not finish creating. + +As with the [Test Cleanup](#test-cleanup) message, Terraform might have been maintaining multiple state files. The `state` field contains the resources from the main state for the test configuration, while the `states` field contains a map of `run` block names to the resources that each `run` block created, which Terraform could not destroy. + +Finally, the `planned` field contains any resources that were in the process of being created by the interrupted `run` block, which you can identify using the `@testrun` field. + +### Example + +```json +{ + "@level": "info", + "@message": "-verbose flag enabled, printing state", + "@module": "terraform.ui", + "@testfile": "validation.tftest.hcl", + "@testrun": "successful_validation", + "@timestamp": "2023-08-09T17:18:21.173008+02:00", + "test_interrupt": { + "state": [ + { + "instance": "aws_instance.primary" + } + ], + "states": { + "unsuccessful_validation": [ + { + "instance": "aws_instance.secondary" + } + ] + }, + "planned": [ + "aws_instance.secondary" + ] + }, + "type": "test_interrupt" +} +``` diff --git a/website/docs/internals/module-registry-protocol.mdx b/website/docs/internals/module-registry-protocol.mdx index 48310bbe82..71ceff93a9 100644 --- a/website/docs/internals/module-registry-protocol.mdx +++ b/website/docs/internals/module-registry-protocol.mdx @@ -1,16 +1,18 @@ --- -page_title: Module Registry Protocol +page_title: Module registry protocol reference description: |- - The module registry protocol is implemented by a host intending to be the - host of one or more Terraform modules, specifying which modules are available - and where to find their distribution packages. + Terraform discovers modules available for installation using the module registry protocol. Learn about the module registry protocol so consumers can find your modules. --- -# Module Registry Protocol +# Module Registry Protocol Reference + +This topic provides reference information about the module registry protocol. -> Third-party module registries are supported only in Terraform CLI 0.11 and later. Prior versions do not support this protocol. -The module registry protocol is what Terraform CLI uses to discover metadata +## Introduction + +The Terraform CLI uses the module registry protocol to discover metadata about modules available for installation and to locate the distribution package for a selected module. @@ -23,7 +25,7 @@ publishing them on the public Terraform Registry. The public Terraform Registry implements a superset of the API described on this page, in order to capture additional information used in the registry UI. For information on those extensions, see -[Terraform Registry HTTP API](/registry/api-docs). Third-party registry +[Terraform Registry HTTP API](/terraform/registry/api-docs). Third-party registry implementations may choose to implement those extensions if desired, but Terraform CLI itself does not use them. @@ -81,7 +83,7 @@ blocks have the same source address. ## Service Discovery The module registry protocol begins with Terraform CLI using -[Terraform's remote service discovery protocol](/internals/remote-service-discovery), +[Terraform's remote service discovery protocol](/terraform/internals/remote-service-discovery), with the hostname in the module address acting as the "User-facing Hostname". The service identifier for the module registry protocol is `modules.v1`. @@ -147,9 +149,7 @@ $ curl 'https://registry.terraform.io/v1/modules/hashicorp/consul/aws/versions' The `modules` array in the response always includes the requested module as the first element. -Other elements of this list are not currently used. Third-party implementations -should always use a single-element list for forward compatiblity with possible -future extensions to the protocol. +Terraform does not use the other elements of this list. However, third-party implementations should always use a single-element list for forward compatiblity. Each returned module has an array of available versions, which Terraform matches against any version constraints given in configuration. @@ -211,10 +211,10 @@ A successful response has no body, and includes the location from which the module version's source can be downloaded in the `X-Terraform-Get` header. The value of this header accepts the same values as the `source` argument in a `module` block in Terraform configuration, as described in -[Module Sources](/language/modules/sources), +[Module Sources](/terraform/language/modules/sources), except that it may not recursively refer to another module registry address. The value of `X-Terraform-Get` may instead be a relative URL, indicated by beginning with `/`, `./` or `../`, in which case it is resolved relative to the full URL of the download endpoint to produce -[an HTTP URL module source](/language/modules/sources#http-urls). +[an HTTP URL module source](/terraform/language/modules/sources#http-urls). diff --git a/website/docs/internals/provider-meta.mdx b/website/docs/internals/provider-meta.mdx index 0e25a8f412..50bb31dbf3 100644 --- a/website/docs/internals/provider-meta.mdx +++ b/website/docs/internals/provider-meta.mdx @@ -1,39 +1,36 @@ --- -page_title: Provider Metadata +page_title: Collect Provider Metadata description: >- - For advanced use cases, modules can provide some pre-defined metadata for - providers. + Provider developers can enable the modules created for the their providers to collect provider metadata. --- -# Provider Metadata +# Collect Provider Metadata -In some situations it's beneficial for a provider to offer an interface +This topic describes how to create an inferface in the providers you develop that allows you to collect metadata that is unrelated to the resources in the module, such as usage statistics. This is an advanced topic and is not required to use Terraform. + +## Introduction + +In some situations it is beneficial for a provider to offer an interface through which modules can pass it information unrelated to the resources in the module, but scoped on a per-module basis. -Provider Metadata allows a provider to declare metadata fields it expects, +Provider metadata allows a provider to declare metadata fields it expects, which individual modules can then populate independently of any provider configuration. While provider configurations are often shared between modules, provider metadata is always module-specific. -Provider Metadata is intended primarily for the situation where an official +Provider metadata is intended primarily for the situation where an official module is developed by the same vendor that produced the provider it is intended to work with, to allow the vendor to indirectly obtain usage statistics for each module via the provider. For that reason, this documentation is presented from the perspective of the provider developer rather than the module developer. -~> **Advanced Topic!** This page covers technical details -of Terraform. You don't need to understand these details to -effectively use Terraform. The details are documented here for -module authors and provider developers working on advanced -functionality. -~> **Experimental Feature!** This functionality is still considered -experimental, and anyone taking advantage of it should [coordinate +~> **Experimental Feature:** This functionality is +experimental. You should [coordinate with the Terraform team](https://github.com/hashicorp/terraform/issues/new) -to help the team understand how the feature is being used and to make -sure their use case is taken into account as the feature develops. +so that they can understand how you are using this functionality. Doing so ensures that your use cases are taken into account as the feature evolves. ## Defining the Schema diff --git a/website/docs/internals/provider-network-mirror-protocol.mdx b/website/docs/internals/provider-network-mirror-protocol.mdx index b52fd154b2..7178de4725 100644 --- a/website/docs/internals/provider-network-mirror-protocol.mdx +++ b/website/docs/internals/provider-network-mirror-protocol.mdx @@ -1,22 +1,19 @@ --- -page_title: Provider Network Mirror Protocol +page_title: Provider network mirror protocol reference description: |- - The provider network mirror protocol is implemented by a server intending - to provide a mirror or read-through caching proxy for Terraform providers, - as an alternative distribution source from the provider's origin provider - registry. + The provider network mirror protocol lets you provide an alternate installation source for your providers. Learn about the provider network mirror protocol. --- -# Provider Network Mirror Protocol +# Provider Network Mirror Protocol Reference + +This topic provides reference information about the provider network mirror protocol. You can implement an alternative installation source for Terraform providers and make the source available over the provider network mirror protocol, regardless of their origin registries. -> Provider network mirrors are supported only in Terraform CLI v0.13.2 and later. Prior versions do not support this protocol. -The provider network mirror protocol is an optional protocol which you can -implement to provide an alternative installation source for Terraform providers, -regardless of their origin registries. +## Introduction Terraform uses network mirrors only if you activate them explicitly in -[the CLI configuration's `provider_installation` block](/cli/config/config-file#provider-installation). +[the CLI configuration's `provider_installation` block](/terraform/cli/config/config-file#provider-installation). When enabled, a network mirror can serve providers belonging to any registry hostname, which can allow an organization to serve all of the Terraform providers they intend to use from an internal server, rather than from each @@ -26,7 +23,7 @@ This is _not_ the protocol that should be implemented by a host intending to serve as an origin registry for Terraform Providers. To provide an origin registry (whose hostname would then be included in the source addresses of the providers it hosts), implement -[the provider registry protocol](/internals/provider-registry-protocol) +[the provider registry protocol](/terraform/internals/provider-registry-protocol) instead. ## Provider Addresses @@ -34,7 +31,7 @@ instead. Each Terraform provider has an associated address which uniquely identifies it within Terraform. A provider address has the syntax `hostname/namespace/type`, which is described in more detail in -[the Provider Requirements documentation](/language/providers/requirements). +[the Provider Requirements documentation](/terraform/language/providers/requirements). By default, the `hostname` portion of a provider address serves both as part of its unique identifier _and_ as the location of the registry to retrieve it @@ -52,7 +49,7 @@ the hostname where the provider network mirror is deployed. ## Protocol Base URL Most Terraform-native services use -[the remote service discovery protocol](/internals/remote-service-discovery) so +[the remote service discovery protocol](/terraform/internals/remote-service-discovery) so that the physical location of the endpoints can potentially be separated from the hostname used in identifiers. The Provider Network Mirror protocol does _not_ use the service discovery indirection, because a network mirror location @@ -94,7 +91,7 @@ base URL from the above CLI configuration example. ### Authentication If the CLI configuration includes -[credentials](/cli/config/config-file#credentials) for the hostname +[credentials](/terraform/cli/config/config-file#credentials) for the hostname given in the network mirror base URL, Terraform will include those credentials in its requests for operations described below. @@ -149,11 +146,7 @@ A successful result is a JSON object containing a single property `versions`, which must be a JSON object. Each of the property names of the `versions` object represents an available -version number. The property values must be objects, but no properties are -currently defined for those objects. Future versions of this protocol may -define optional per-version properties for Terraform to use as installation -hints, so implementations of the current version should leave those objects -empty. +version number. The property values must be objects, but no properties are defined for those objects. We recommend leaving those objects empty for forward compatibility. Return `404 Not Found` to signal that the mirror does not have a provider with the given address. @@ -262,7 +255,7 @@ in the appropriate nested subdirectories, and ensure that your system is configured to serve `.json` files with the `application/json` media type. As a convenience, Terraform CLI includes -[the `terraform providers mirror` subcommand](/cli/commands/providers/mirror), +[the `terraform providers mirror` subcommand](/terraform/cli/commands/providers/mirror), which will analyze the current configuration for the providers it requires, download the packages for those providers from their origin registries, and place them into a local directory suitable for use as a mirror. diff --git a/website/docs/internals/provider-registry-protocol.mdx b/website/docs/internals/provider-registry-protocol.mdx index dc260e41d5..a8acc4e948 100644 --- a/website/docs/internals/provider-registry-protocol.mdx +++ b/website/docs/internals/provider-registry-protocol.mdx @@ -1,18 +1,14 @@ --- -page_title: Provider Registry Protocol +page_title: Provider registry protocol reference description: |- - The provider registry protocol is implemented by a host intending to be the - origin host for one or more Terraform providers, specifying which providers - are available and where to find their distribution packages. + Use the provider registry protocol to enable Terraform to discover metadata about available providers and locate their distribution packages. --- -# Provider Registry Protocol +# Provider Registry Protocol Reference --> Third-party provider registries are supported only in Terraform CLI 0.13 and later. Prior versions do not support this protocol. +This topic provides reference information about the provider registry protocol. The protocol allows the Terraform CLI to discover metadata about the providers available for installation and to locate the distribution packages for a selected provider. -The provider registry protocol is what Terraform CLI uses to discover metadata -about providers available for installation and to locate the distribution -packages for a selected provider. +## Introduction The primary implementation of this protocol is the public [Terraform Registry](https://registry.terraform.io/) at `registry.terraform.io`. @@ -40,7 +36,7 @@ where: * `hostname` is the registry host that the provider is considered to have originated from, and the default location Terraform will consult for information about the provider - [unless overridden in the CLI configuration](/cli/config/config-file#provider-installation). + [unless overridden in the CLI configuration](/terraform/cli/config/config-file#provider-installation). * `namespace` is the name of a namespace, unique on a particular hostname, that can contain one or more providers that are somehow related. On the public Terraform Registry the "namespace" represents the organization that is @@ -76,7 +72,7 @@ to see it as an entirely separate provider that will _not_ be usable by modules that declare a dependency on `hashicorp/azurerm`. If your goal is to create an alternative local distribution source for an existing provider -- that is, a _mirror_ of the provider -- refer to -[the provider installation method configuration](/cli/config/config-file#provider-installation) +[the provider installation method configuration](/terraform/cli/config/config-file#provider-installation) instead. ## Provider Versions @@ -96,7 +92,7 @@ version selection. ## Service Discovery The providers protocol begins with Terraform CLI using -[Terraform's remote service discovery protocol](/internals/remote-service-discovery), +[Terraform's remote service discovery protocol](/terraform/internals/remote-service-discovery), with the hostname in the provider address acting as the "User-facing Hostname". The service identifier for the provider registry protocol is `providers.v1`. @@ -294,11 +290,11 @@ A successful result is a JSON object with the following properties: _required_ for describing an individual provider package so that Terraform CLI can avoid downloading a package that will not be compatible with it. -* `os` (required): this must currently echo back the `os` parameter from the - request. Other possibilities may come in later versions of this protocol. +* `os` (required): this must echo back the `os` parameter from the + request. -* `arch` (required): this must currently echo back the `arch` parameter from the - request. Other possibilities may come in later versions of this protocol. +* `arch` (required): this must echo back the `arch` parameter from the + request. * `filename` (required): the filename for this provider's zip archive as recorded in the "shasums" document, so that Terraform CLI can determine which @@ -319,6 +315,9 @@ A successful result is a JSON object with the following properties: * `shasums_signature_url` (required): a URL from which Terraform can retrieve a binary, detached GPG signature for the document at `shasums_url`, signed by one of the keys indicated in the `signing_keys` property. + +* `shasum` (required): the SHA256 checksum for this provider's zip archive as + recorded in the shasums document. * `signing_keys` (required): an object describing signing keys for this provider package, one of which must have been used to produce the signature diff --git a/website/docs/internals/remote-service-discovery.mdx b/website/docs/internals/remote-service-discovery.mdx index 1a652106e1..5aeb17888c 100644 --- a/website/docs/internals/remote-service-discovery.mdx +++ b/website/docs/internals/remote-service-discovery.mdx @@ -1,22 +1,23 @@ --- -page_title: 'Internals: Remote Service Discovery' +page_title: Remote service discovery protocol reference description: |- - Remote service discovery is a protocol used to locate Terraform-native - services provided at a user-friendly hostname. + The remote service discovery protocol presents Terraform-native services at a human-readable hostname. Learn about the remote service discovery protocol. --- -# Remote Service Discovery +# Remote Service Discovery Protocol Reference -Terraform implements much of its functionality in terms of remote services. -While in many cases these are generic third-party services that are useful -to many applications, some of these services are tailored specifically to -Terraform's needs. We call these _Terraform-native services_, and Terraform -interacts with them via the remote service discovery protocol described below. +This topic provides reference information about the remote service discovery protocol in Terraform. + +## Introduction + +Terraform implements much of its functionality as remote services. +Some of these are native services that Terraform +interacts with through the remote service discovery protocol. ## User-facing Hostname -Terraform-native services are provided, from a user's perspective, at a -user-facing "friendly hostname" which serves as the key for configuration and +Terraform provides native services at a +human-readable hostname. The hostname is the key for configuration and for any authentication credentials required. The discovery protocol's purpose is to map from a user-provided hostname to @@ -83,14 +84,14 @@ version 1 of the module registry protocol: At present, the following service identifiers are in use: -* `login.v1`: [login protocol version 1](/cli/commands/login) -* `modules.v1`: [module registry API version 1](/internals/module-registry-protocol) -* `providers.v1`: [provider registry API version 1](/internals/provider-registry-protocol) +* `login.v1`: [login protocol version 1](/terraform/cli/commands/login) +* `modules.v1`: [module registry API version 1](/terraform/internals/module-registry-protocol) +* `providers.v1`: [provider registry API version 1](/terraform/internals/provider-registry-protocol) ## Authentication If credentials for the given hostname are available in -[the CLI config](/cli/config/config-file#Credentials) through a `credentials_helper` or a host-specific environment variable, then they will be included in the request for the discovery document. +[the CLI config](/terraform/cli/config/config-file#Credentials) through a `credentials_helper` or a host-specific environment variable, then they will be included in the request for the discovery document. The credentials may also be provided to endpoints declared in the discovery document, depending on the requirements of the service in question. diff --git a/website/docs/intro/core-workflow.mdx b/website/docs/intro/core-workflow.mdx index e73cb326d2..3d49b7db39 100644 --- a/website/docs/intro/core-workflow.mdx +++ b/website/docs/intro/core-workflow.mdx @@ -1,9 +1,9 @@ --- -page_title: The Core Terraform Workflow - Guides -description: 'An overview of how individuals, teams, and organizations can use Terraform. ' +page_title: Overview of the core Terraform workflow +description: Learn how to provision and manage infrastructure using the core Terraform workflow for individuals, teams, and organizations. --- -# The Core Terraform Workflow +# Core Terraform Workflow Overview The core Terraform workflow has three steps: @@ -13,7 +13,7 @@ The core Terraform workflow has three steps: This guide walks through how each of these three steps plays out in the context of working as an individual practitioner, how they evolve when a team is -collaborating on infrastructure, and how Terraform Cloud enables this +collaborating on infrastructure, and how HCP Terraform enables this workflow to run smoothly for entire organizations. ## Working as an Individual Practitioner @@ -152,7 +152,7 @@ Terraform operations are executed in a shared Continuous Integration (CI) environment. The work needed to create such a CI environment is nontrivial, and is outside the scope of this core workflow overview, but a full deep dive on this topic can be found in our -[Running Terraform in Automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) +[Running Terraform in Automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) guide. This longer iteration cycle of committing changes to version control and then @@ -215,37 +215,44 @@ Just like the workflow for individuals, the core workflow for teams is a loop that plays out for each change. For some teams this loop happens a few times a week, for others, many times a day. -## The Core Workflow Enhanced by Terraform Cloud +## The Core Workflow Enhanced by HCP Terraform While the above described workflows enable the safe, predictable, and reproducible creating or changing of infrastructure, there are multiple collaboration points that can be streamlined, especially as teams and -organizations scale. We designed Terraform Cloud to support and enhance +organizations scale. We designed HCP Terraform to support and enhance the core Terraform workflow for anyone collaborating on infrastructure, from -small teams to large organizations. Let's look at how Terraform Cloud makes +small teams to large organizations. Let's look at how HCP Terraform makes for a better experience at each step. ### Write -Terraform Cloud provides a centralized and secure location for storing +HCP Terraform provides a centralized and secure location for storing input variables and state while also bringing back a tight feedback loop for speculative plans for config authors. Terraform configuration can interact with -Terraform Cloud through the [CLI integration](/cli/cloud). +HCP Terraform through the [CLI integration](/terraform/cli/cloud). -``` +```hcl terraform { cloud { organization = "my-org" hostname = "app.terraform.io" # Optional; defaults to app.terraform.io workspaces { - tags = ["networking", "source:cli"] + tags = { + layer = "networking" + source = "cli" + } + + # For terraform versions below 1.10, you must specify key-only tags + # using a list of strings. Example: + # tags = ["networking", "source:cli"] } } } ``` -After you configure the integration, a Terraform Cloud API key is all your team members need to edit config and run speculative plans +After you configure the integration, an HCP Terraform API key is all your team members need to edit config and run speculative plans against the latest version of the state file using all the remotely stored input variables. @@ -273,7 +280,7 @@ authoring config until it is ready to propose as a change via a pull request. ### Plan -Once a pull request is ready for review, Terraform Cloud makes the process +Once a pull request is ready for review, HCP Terraform makes the process of reviewing a speculative plan easier for team members. First, the plan is automatically run when the pull request is created. Status updates to the pull request indicate while the plan is in progress. @@ -285,17 +292,17 @@ changes in the speculative plan, right from the pull request view. For certain types of changes, this information is all that's needed for a team member to be able to approve the pull request. When a teammate needs to do a -full review of the plan, clicking the link to Terraform Cloud brings up a +full review of the plan, clicking the link to HCP Terraform brings up a view that allows them to quickly analyze the full plan details. -![Screenshot of Pull Request run in Terraform Cloud](/img/docs/pr-plan.png) +![Screenshot of Pull Request run in HCP Terraform](/img/docs/pr-plan.png) This page allows the reviewer to quickly determine if the plan is matching the config author's intent and evaluate the risk of the change. ### Apply -After merge, Terraform Cloud presents the concrete plan to the team for +After merge, HCP Terraform presents the concrete plan to the team for review and approval. ![Screenshot of concrete plan](/img/docs/concrete-plan.png) @@ -303,9 +310,9 @@ review and approval. The team can discuss any outstanding questions about the plan before the change is made. -![Screenshot of back-and-forth in Terraform Cloud comments](/img/docs/plan-comments.png) +![Screenshot of back-and-forth in HCP Terraform comments](/img/docs/plan-comments.png) -Once the Apply is confirmed, Terraform Cloud displays the progress live +Once the Apply is confirmed, HCP Terraform displays the progress live to anyone who'd like to watch. ![Screenshot of in-progress Apply](/img/docs/in-progress-apply.png) @@ -327,5 +334,5 @@ There are many different ways to use Terraform: as an individual user, a single team, or an entire organization at scale. Choosing the best approach for the density of collaboration needed will provide the most return on your investment in the core Terraform workflow. For organizations using Terraform at scale, -Terraform Cloud introduces new layers that build on this core workflow to +HCP Terraform introduces new layers that build on this core workflow to solve problems unique to teams and organizations. diff --git a/website/docs/intro/index.mdx b/website/docs/intro/index.mdx index df01b7a992..50df0b90fc 100644 --- a/website/docs/intro/index.mdx +++ b/website/docs/intro/index.mdx @@ -1,6 +1,6 @@ --- layout: "intro" -page_title: "What is Terraform" +page_title: What is Terraform sidebar_current: "what" description: |- Terraform is an infrastructure as code tool that lets you build, change, and version cloud and on-prem resources safely and efficiently. @@ -9,14 +9,14 @@ description: |- HashiCorp Terraform is an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to provision and manage all of your infrastructure throughout its lifecycle. Terraform can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features. -> **Hands On:** Try the Get Started tutorials on HashiCorp Learn to start managing infrastructure on popular cloud providers: [Amazon Web Services](https://learn.hashicorp.com/collections/terraform/aws-get-started), [Azure](https://learn.hashicorp.com/collections/terraform/azure-get-started), [Google Cloud Platform](https://learn.hashicorp.com/collections/terraform/gcp-get-started), [Oracle Cloud Infrastructure](https://learn.hashicorp.com/collections/terraform/oci-get-started), and [Docker](https://learn.hashicorp.com/collections/terraform/docker-get-started). +> **Hands On:** Try the Get Started tutorials to start managing infrastructure on popular cloud providers: [Amazon Web Services](/terraform/tutorials/aws-get-started), [Azure](/terraform/tutorials/azure-get-started), [Google Cloud Platform](/terraform/tutorials/gcp-get-started), [Oracle Cloud Infrastructure](/terraform/tutorials/oci-get-started), and [Docker](/terraform/tutorials/docker-get-started). ## How does Terraform work? Terraform creates and manages resources on cloud platforms and other services through their application programming interfaces (APIs). Providers enable Terraform to work with virtually any platform or service with an accessible API. ![Terraform creates and manages cloud platforms and services through their APIs](/img/docs/intro-terraform-apis.png) -HashiCorp and the Terraform community have already written **more than 1700 providers** to manage thousands of different types of resources and services, and this number continues to grow. You can find all publicly available providers on the [Terraform Registry](https://registry.terraform.io/), including Amazon Web Services (AWS), Azure, Google Cloud Platform (GCP), Kubernetes, Helm, GitHub, Splunk, DataDog, and many more. +HashiCorp and the Terraform community have already written **thousands of providers** to manage many different types of resources and services. You can find all publicly available providers on the [Terraform Registry](https://registry.terraform.io/), including Amazon Web Services (AWS), Azure, Google Cloud Platform (GCP), Kubernetes, Helm, GitHub, Splunk, DataDog, and many more. The core Terraform workflow consists of three stages: @@ -31,16 +31,15 @@ The core Terraform workflow consists of three stages: HashiCorp co-founder and CTO Armon Dadgar explains how Terraform solves infrastructure challenges. - - + ### Manage any infrastructure -Find providers for many of the platforms and services you already use in the [Terraform Registry](https://registry.terraform.io/). You can also [write your own](/plugin). Terraform takes an [immutable approach to infrastructure](https://www.hashicorp.com/resources/what-is-mutable-vs-immutable-infrastructure), reducing the complexity of upgrading or modifying your services and infrastructure. +Find providers for many of the platforms and services you already use in the [Terraform Registry](https://registry.terraform.io/). You can also [write your own](/terraform/plugin). Terraform takes an [immutable approach to infrastructure](https://www.hashicorp.com/resources/what-is-mutable-vs-immutable-infrastructure), reducing the complexity of upgrading or modifying your services and infrastructure. ### Track your infrastructure -Terraform generates a plan and prompts you for your approval before modifying your infrastructure. It also keeps track of your real infrastructure in a [state file](/language/state), which acts as a source of truth for your environment. Terraform uses the state file to determine the changes to make to your infrastructure so that it will match your configuration. +Terraform generates a plan and prompts you for your approval before modifying your infrastructure. It also keeps track of your real infrastructure in a [state file](/terraform/language/state), which acts as a source of truth for your environment. Terraform uses the state file to determine the changes to make to your infrastructure so that it will match your configuration. ### Automate changes @@ -48,13 +47,13 @@ Terraform configuration files are declarative, meaning that they describe the en ### Standardize configurations -Terraform supports reusable configuration components called [modules](/docs/language/modules) that define configurable collections of infrastructure, saving time and encouraging best practices. You can use publicly available modules from the Terraform Registry, or write your own. +Terraform supports reusable configuration components called [modules](/terraform/language/modules) that define configurable collections of infrastructure, saving time and encouraging best practices. You can use publicly available modules from the Terraform Registry, or write your own. ### Collaborate -Since your configuration is written in a file, you can commit it to a Version Control System (VCS) and use [Terraform Cloud](/intro/terraform-editions#terraform-cloud) to efficiently manage Terraform workflows across teams. Terraform Cloud runs Terraform in a consistent, reliable environment and provides secure access to shared state and secret data, role-based access controls, a private registry for sharing both modules and providers, and more. +Since your configuration is written in a file, you can commit it to a Version Control System (VCS) and use [HCP Terraform](/terraform/intro/terraform-editions#hcp-terraform) to efficiently manage Terraform workflows across teams. HCP Terraform runs Terraform in a consistent, reliable environment and provides secure access to shared state and secret data, role-based access controls, a private registry for sharing both modules and providers, and more. --> **Tip:** Learn more about [Terraform use cases](/intro/use-cases) and [how Terraform compares to alternatives](/intro/vs). +-> **Tip:** Learn more about [Terraform use cases](/terraform/intro/use-cases) and [how Terraform compares to alternatives](/terraform/intro/vs). ## Community diff --git a/website/docs/intro/phases/adopt.mdx b/website/docs/intro/phases/adopt.mdx new file mode 100644 index 0000000000..f2ff29b4df --- /dev/null +++ b/website/docs/intro/phases/adopt.mdx @@ -0,0 +1,55 @@ +--- +page_title: Adopt Terraform +description: Establish strong foundational practices that support future scale and make Terraform operations predictable and secure. +--- + +# Adopt Terraform + +An individual practitioner can establish strong foundational practices that support future scale and make Terraform operations predictable and secure. + +## Use version control + +Store your Terraform configuration in a version control system, such as Git, just as you would with your application code. Terraform configuration files are code, and will benefit from the same features as your application in a version control repository such as versioning and easier code reviews. + + + +Do not store [`terraform.tfstate` state files](/terraform/language/state), provider credentials, or sensitive values in version control. Use a [gitignore file](https://github.com/github/gitignore/blob/main/Terraform.gitignore) to avoid accidentally committing sensitive files. + + + +You can [connect your VCS provider to HCP Terraform](/terraform/cloud-docs/vcs) to automatically initiate Terraform runs and view [speculative plans that let you preview your infrastructure changes](/terraform/cloud-docs/run/ui#speculative-plans-on-pull-requests) in your pull requests. + +## Reuse code with modules + +Terraform modules group resources that you usually deploy together, letting you define reusable units of infrastructure code. For example, when you create a VPC in AWS, you may also need to create subnets, the route table, the internet gateway, security groups, and more. Instead of defining the individual resources and configuring the relationships between them every time you need a new VPC, you can use the [VPC module](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest), which you can customize using input variables to quickly create the required infrastructure. The [public Terraform module registry](https://registry.terraform.io/browse/modules) offers many modules that encode best practices for common use cases. + +You can also create your own modules to deploy the specific infrastructure required by your services. Even a small three-tier application may require many Terraform-managed resources. A module lets you contain that complexity, turning each deployment of the application stack into a short, readable, and reusable configuration. The following Terraform configuration references a local module stored at `./modules/appstack` that takes in two arguments named `web_instance_count` and `api_instance_count`: + +```hcl +module "appstack" { + source = "./modules/appstack" + + web_instance_count = 2 + api_instance_count = 1 +} + +output "web_instance_ips" { + value = module.appstack.web_ips +} +``` + +[Follow our tutorials to learn how to use and develop modules](/terraform/tutorials/modules/module) and explore the [public Terraform module registry](https://registry.terraform.io/browse/modules). + +## Use secrets storage + +Your configuration may rely on sensitive values, such as provider credentials. Although you can mark certain variables as sensitive to prevent displaying them as plaintext in run output, a more robust solution is to use secrets storage such as [HashiCorp Vault](/vault) + +Vault securely stores sensitive information such as credentials and provides granular access control. You can integrate Vault into your Terraform configuration using the [Vault provider](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/data-sources/generic_secret). If you deploy your infrastructure to a major cloud provider, such as AWS, you can also [generate short-lived credentials with Vault](/terraform/tutorials/secrets/secrets-vault) or use [dynamic provider credentials](/terraform/cloud-docs/workspaces/dynamic-provider-credentials), which prevents having to store credentials. + +Vault also integrates into many popular CI/CD solutions such as [GitHub, Jenkins, and CircleCI](/well-architected-framework/security/security-cicd-vault). Vault provides a central system to store and access data, which lets CI/CD pipelines push and pull secrets programmatically. + +## Next steps + +Multiple developers working on the same codebase introduces a new set of challenges, but solutions such as remote state backends help ease collaboration and coordinate execution. + +[Learn how to collaborate with Terraform](/terraform/intro/phases/collaborate). \ No newline at end of file diff --git a/website/docs/intro/phases/collaborate.mdx b/website/docs/intro/phases/collaborate.mdx new file mode 100644 index 0000000000..4b39110509 --- /dev/null +++ b/website/docs/intro/phases/collaborate.mdx @@ -0,0 +1,50 @@ +--- +page_title: Collaborate with Terraform +description: Ease collaboration and coordinate execution across your team. +--- + +# Collaborate with Terraform + +Multiple developers working on the same codebase introduces a new set of challenges, but solutions such as remote state backends help ease collaboration and coordinate execution. + +## Use remote state storage + +As more team members work on Terraform configuration, you should implement remote state storage to support collaboration. HCP Terraform and remote backends implement several features to help you safely manage your Terraform state: + +- **Storage:** Remote state storage lets you manage infrastructure collaboratively and securely. Different state stores may also support additional features for state management, such as encryption, versioning, automated backups, redundancy, and more. +- **Locking:** Some remote state storage options support [state locking](/terraform/language/state/locking). State locking prevents concurrent Terraform operations on single state files. +- **Execution:** HCP Terraform and Terraform Enterprise support executing Terraform operations in stable, remote environments. + +Since state files may contain sensitive data, refer to your backend documentation and, if supported, use [state encryption](/well-architected-framework/security/security-sensitive-data). [HCP Terraform and Terraform Enterprise](/terraform/cloud-docs/architectural-details/data-security#data-security) both automatically encrypt state, and [AWS, GCP, and Azure](/well-architected-framework/security/security-sensitive-data#storing-terraform-state) backends can implement encryption as well. + +As your team grows, you may run into the risk of concurrent operations on state files. If supported by your remote storage solution, use [state locking](/terraform/language/state/locking) to prevent unpredictable outcomes or corrupted data. [HCP Terraform and Terraform Enterprise](/terraform/cli/cloud/settings) support state locking by default, but other state storage implementations require additional configuration. For example, the [AWS S3 remote backend](/terraform/language/backend/s3) requires that a [DynamoDB table](/terraform/language/backend/s3#dynamodb-table-permissions) for state locking. + +| | Storage | Locking | Execution | +|------------------------------|---------|--------------|-----------| +| HCP Terraform / Enterprise | Yes | Yes | Yes | +| Amazon S3 | Yes | via DynamoDB | No | +| Azure Storage | Yes | Yes | No | +| Google Cloud Storage | Yes | Yes | No | + +[Get started with HCP Terraform](/terraform/tutorials/cloud-get-started) and learn how to [securely store your Terraform state](/well-architected-framework/security/security-sensitive-data#storing-terraform-state). + +## Implement code reviews + +Implement good code practices for your Terraform configuration, including using pull requests for code changes and performing proper code reviews. +Code reviews can prevent introducing errors into your infrastructure configuration. They also help team members share their knowledge of the code base and enforce coding standards. + +Use the integrations offered by your version control system to help with your code reviews. For example, HCP Terraform's VCS integration [generates speculative plans](/terraform/cloud-docs/run/ui#speculative-plans-on-pull-requests) for each pull request, showing the exact changes that Terraform will make to your infrastructure. + +## Automate deployments with CI/CD + +A CI/CD pipeline offers a consistent process for shipping new features and fixes. By storing your Terraform configuration in version control, you define a single source of truth for your infrastructure configuration and can automate your deployments. You can configure a CI pipeline to automatically start a Terraform plan and apply operation for any changes to your code. + +Terraform [integrates](/terraform/tutorials/automation/automate-terraform) with many automation solutions. If you do not have an existing CI/CD workflow, HashiCorp's [Setup Terraform GitHub action](/terraform/tutorials/automation/github-actions) sets up and configures the Terraform CLI in your Github Actions workflow. + +## Next steps + +As Terraform usage expands across your organization, you will need to decide how to define boundaries of infrastructure ownership. + +You will also need to decide on a cloud deployment strategy based on your organization's practices and needs. Possible approaches include using a single account in a single cloud provider, a hybrid or multi-cloud approach, or to divide up resources across accounts by environment. Regardless of your implementation, Terraform lets you manage your infrastructure with a consistent workflow. + +[Learn how to scale Terraform](/terraform/intro/phases/scale). \ No newline at end of file diff --git a/website/docs/intro/phases/govern.mdx b/website/docs/intro/phases/govern.mdx new file mode 100644 index 0000000000..d5ed5748a5 --- /dev/null +++ b/website/docs/intro/phases/govern.mdx @@ -0,0 +1,22 @@ +--- +page_title: Govern Terraform +description: Use codified, automated policy enforcement to govern your organization's standards and best practices. +--- + +# Govern Terraform + +As your teams grow, a common operational challenge is deciding how to enforce your organization's standards and practices. Using codified, automated policy enforcement with Sentinel or OPA ensures consistent application of your standards. + +## Govern infrastructure through policy + +You can use policy as code to ensure your infrastructure meets your organization's security, governance, and cost requirements. You can configure your workflows to automatically run policy checks as part of your Terraform operations and set conditions for how to handle policy failures. Soft enforcement lets prompts a user to approve an operation that fails a policy check, and hard enforcement blocks the operation entirely. + +You can define policies that set standards for both your infrastructure configuration itself, and for the workflows around configuration deployment. Some examples of policy rules you can define include which ports are open in a firewall, the permitted sizes of virtual machines, or that deployments cannot take place on Fridays. In HCP Terraform and Terraform Enterprise you can use either [OPA](/terraform/cloud-docs/policy-enforcement/opa) or [Sentinel](https://www.hashicorp.com/sentinel) for your policy definitions. + +Learn how to [write a Sentinel policy for a Terraform Deployment](/terraform/tutorials/policy/sentinel-policy) and how to [detect infrastructure drift and enforce OPA policies](/terraform/tutorials/cloud/drift-and-opa). + +## Next steps + +This guide introduces considerations to keep in mind as your organization adopts Terraform, but there are many more topics to explore. [HCP Terraform](/terraform/tutorials/cloud-get-started) provides a place to get started with many of these topics, and you can [get started for free](https://app.terraform.io/public/signup/account). + +The [HashiCorp Well-Architected Framework](/well-architected-framework) provides more in-depth information on how to adopt and scale your use of Terraform. \ No newline at end of file diff --git a/website/docs/intro/phases/index.mdx b/website/docs/intro/phases/index.mdx new file mode 100644 index 0000000000..b4f9fad2b3 --- /dev/null +++ b/website/docs/intro/phases/index.mdx @@ -0,0 +1,40 @@ +--- +page_title: Phases of Terraform Adoption +description: Evolve your Terraform strategy as adoption grows within your organization +--- + +# Phases of Terraform Adoption + +As more of your organization adopts Terraform, your infrastructure provisioning workflows will need to change and adapt. The workflows that are suitable for individual practitioners may not scale to larger enterprises. This guide will help you plan your organization's Terraform adoption strategy and presents workflow considerations that you should keep in mind to support future scale. This guide focuses on challenges faced by larger organizations, but we recommend implementing each practice as early as you can to help you scale smoothly. + +## Adopt + +An individual practitioner can establish strong foundational practices that support future scale and make Terraform operations predictable and secure. + +[Learn how to adopt Terraform](/terraform/intro/phases/adopt) + +## Collaborate + +Multiple developers working on the same codebase introduces a new set of challenges, but solutions such as remote state backends help ease collaboration and coordinate execution. + +[Learn how to collaborate with Terraform](/terraform/intro/phases/collaborate). + +## Scale + +As Terraform usage expands across your organization, you will need to decide how to define boundaries of infrastructure ownership. + +You will also need to decide on a cloud deployment strategy based on your organization's practices and needs. Possible approaches include using a single account in a single cloud provider, a hybrid or multi-cloud approach, or to divide up resources across accounts by environment. Regardless of your implementation, Terraform lets you manage your infrastructure with a consistent workflow. + +[Learn how to scale Terraform](/terraform/intro/phases/scale). + +## Govern + +As your teams grow, a common operational challenge is deciding how to enforce your organization's standards and practices. Using codified, automated policy enforcement with Sentinel or OPA ensures consistent application of your standards. + +[Learn how to govern your organization's best practices](/terraform/intro/phases/govern). + +## Next steps + +This guide introduces considerations to keep in mind as your organization adopts Terraform, but there are many more topics to explore. To learn more Terraform best practices, refer to [Terraform style guide](/terraform/language/style). The [HashiCorp Well-Architected Framework](/well-architected-framework) provides more in-depth information on how to adopt and scale your use of Terraform. + +[HCP Terraform](/terraform/tutorials/cloud-get-started) provides a place to get started with many of these topics, and you can [get started for free](https://app.terraform.io/public/signup/account). \ No newline at end of file diff --git a/website/docs/intro/phases/scale.mdx b/website/docs/intro/phases/scale.mdx new file mode 100644 index 0000000000..857c15807c --- /dev/null +++ b/website/docs/intro/phases/scale.mdx @@ -0,0 +1,40 @@ +--- +page_title: Scale Terraform +description: Define boundaries of infrastructure ownership across your team with Terraform. +--- + +# Scale Terraform + +As Terraform usage expands across your organization, you will need to decide how to define boundaries of infrastructure ownership. + +You will also need to decide on a cloud deployment strategy based on your organization's practices and needs. Possible approaches include using a single account in a single cloud provider, a hybrid or multi-cloud approach, or to divide up resources across accounts by environment. Regardless of your implementation, Terraform lets you manage your infrastructure with a consistent workflow. + +## Adopt modules across your organization + +We recommend using modules early in your Terraform adoption process to support consistent infrastructure configuration. As your Terraform usage scales, a central module registry helps teams find and use your modules rather than rewriting the same code. + +Terraform supports [multiple module distribution options](/terraform/language/modules/sources), but we recommend that you use a native Terraform module registry such as HCP Terraform or Terraform Enterprise. These both use the [module registry protocol](/terraform/internals/module-registry-protocol), which is the Terraform-specific protocol to discover metadata about modules available for installation and to locate the distribution package for a selected module. + +If you cannot use a native module registry, there are other source options such as [Git repositories](/terraform/language/modules/sources#generic-git-repository) or [AWS S3](/terraform/language/modules/sources#s3-bucket). + +Modules also help teams establish infrastructure configuration standards. For example, you can write a module to create a database used by your application that includes all of the defaults that your architecture requires. The module can define the database size, type, and handle all of the required networking. This ensures that module consumers provision infrastructure in line with your organization standards and requirements. + +Since modules define their own inputs, you can decide which parameters are configurable by the user. For example, you might want to allow them to change the size of the cluster, but not let them change the engine type. + +Read the [recommended patterns for creating modules](/terraform/tutorials/modules/pattern-module-creation). + +## Divide infrastructure responsibility + +It is common for different teams to focus on different parts of your organization's infrastructure. For example, the networking team may manage the VPCs, while the application team only needs to know where to deploy their application and focuses on configuring servers and databases. In this scenario, there is a division of responsibilities but the application team still needs to access data about the networking resources for their own configuration. + +Terraform lets you [reference data about other resources](/terraform/language/state/remote#delegation-and-teamwork) in your configuration without having to manage them in the same state file, allowing you to maintain distinct areas of ownership and infrastructure decoupling. You can use data sources to query a provider for more data about a particular resource, or reference output values from another state file using the remote state data source. HCP Terraform lets you explicitly grant access to your workspace state file to only the workspaces that need it, reducing access to potentially sensitive data. You can also use the [tfe_outputs](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) data source to access the outputs of another HCP Terraform workspace. + +## Consider multiple IaaS accounts + +Many Terraform users start by deploying to a single account in their cloud provider. This makes sense when you are managing only a few resources. As your Terraform adoption matures, managing thousands of resources across several cloud providers can become very complex, slow, and hard to secure. One strategy is to split your managed resources into multiple accounts in a way that makes sense to your organization. For example, you may want an account per deployment environment, such as one for development and one for production. + +## Next steps + +As your teams grow, a common operational challenge is deciding how to enforce your organization's standards and practices. Using codified, automated policy enforcement with Sentinel or OPA ensures consistent application of your standards. + +[Learn how to govern your organization's best practices](/terraform/intro/phases/govern). \ No newline at end of file diff --git a/website/docs/intro/terraform-editions.mdx b/website/docs/intro/terraform-editions.mdx index d33d661fd4..0d3962b153 100644 --- a/website/docs/intro/terraform-editions.mdx +++ b/website/docs/intro/terraform-editions.mdx @@ -3,20 +3,20 @@ layout: "intro" page_title: "Terraform Editions" sidebar_current: "what" description: |- - Terraform Open Source, Terraform Cloud, and Terraform Enterprise solve increasingly complex infrastructure and collaboration challenges. + Learn how Terraform Community Edition, HCP Terraform, and Terraform Enterprise solve increasingly complex infrastructure and collaboration challenges. --- -# Terraform Editions +# Terraform Editions Overview As your organization adopts infrastructure as code (IaC), you will encounter increasingly complex technical and collaboration challenges. We offer three Terraform editions designed to help you solve them. -## Terraform Open Source +## Terraform Community Edition -Terraform open source is a free, downloadable tool that you interact with on the command line. It lets you provision infrastructure on any cloud provider and manages configuration, plugins, infrastructure, and state. +Terraform Community Edition is a free, downloadable tool that you interact with on the command line. It lets you provision infrastructure on any cloud provider and manages configuration, plugins, infrastructure, and state. -### Why Terraform Open Source? +### Why Terraform Community Edition? -Terraform open source lets you: +Terraform Community Edition lets you: - Adopt infrastructure as code and use a common configuration language to provision thousands of different types of resources and services. - Codify your infrastructure so that you can check configuration files into a version control system (VCS) to safely manage contributions. Manually pull the most up-to-date version to perform Terraform operations. @@ -25,46 +25,46 @@ Terraform open source lets you: ### Resources -- Get Started collections on HashiCorp Learn for popular providers: [Amazon Web Services](https://learn.hashicorp.com/collections/terraform/aws-get-started), [Azure](https://learn.hashicorp.com/collections/terraform/azure-get-started), [Google Cloud Platform](https://learn.hashicorp.com/collections/terraform/gcp-get-started), [Oracle Cloud Infrastructure](https://learn.hashicorp.com/collections/terraform/oci-get-started), and [Docker](https://learn.hashicorp.com/collections/terraform/docker-get-started) -- [What is Terraform?](/intro) -- [Configuration Language Documentation](/docs/language/index.html) -- [CLI Documentation](/docs/cli/index.html) +- Get Started tutorials for popular providers: [Amazon Web Services](/terraform/tutorials/aws-get-started), [Azure](/terraform/tutorials/azure-get-started), [Google Cloud Platform](/terraform/tutorials/gcp-get-started), [Oracle Cloud Infrastructure](/terraform/tutorials/oci-get-started), and [Docker](/terraform/tutorials/docker-get-started) +- [What is Terraform?](/terraform/intro) +- [Configuration Language Documentation](/terraform/language) +- [CLI Documentation](/terraform/cli) -## Terraform Cloud +## HCP Terraform -Terraform Cloud is a SaaS application that runs Terraform in a stable, remote environment and securely stores state and secrets. It includes a rich user interface that helps you better understand your Terraform operations and resources, allows you to define role-based access controls, and offers a private registry for sharing modules and providers. Terraform Cloud also integrates with the Terraform CLI and connects to common version control systems (VCS) like GitHub, GitLab, and Bitbucket. When you connect a Terraform Cloud workspace to a VCS repository, new commits and changes can automatically trigger Terraform plans. Terraform Cloud also offers an API, allowing you to integrate it into existing workflows. +HCP Terraform is a SaaS application that runs Terraform in a stable, remote environment and securely stores state and secrets. It includes a rich user interface that helps you better understand your Terraform operations and resources, allows you to define role-based access controls, and offers a private registry for sharing modules and providers. HCP Terraform also integrates with the Terraform CLI and connects to common version control systems (VCS) like GitHub, GitLab, and Bitbucket. When you connect an HCP Terraform workspace to a VCS repository, new commits and changes can automatically trigger Terraform plans. HCP Terraform also offers an API, allowing you to integrate it into existing workflows. -Many Terraform Cloud features are free for small teams; we offer paid plans for larger organizations with additional collaboration and governance features. +Many HCP Terraform features are free for small teams; we offer paid plans for larger organizations with additional collaboration and governance features. -### Why Terraform Cloud? +### Why HCP Terraform? -Terraform Cloud lets you: +HCP Terraform lets you: -- Run Terraform from the local CLI or in a remote environment, trigger operations through your version control system, or use an API to integrate Terraform Cloud into your existing workflows. -- Ensure that only approved teams can access, edit, and provision infrastructure with Terraform Cloud workspaces, single sign-on, and role-based access controls. +- Run Terraform from the local CLI or in a remote environment, trigger operations through your version control system, or use an API to integrate HCP Terraform into your existing workflows. +- Ensure that only approved teams can access, edit, and provision infrastructure with HCP Terraform workspaces, single sign-on, and role-based access controls. - Securely store and version Terraform state remotely, with encryption at rest. Versioned state files allow you to access state file history. -- Publish configuration modules in the Terraform Cloud private registry that define approved infrastructure patterns. For example, a module may allow users to choose the cloud provider on which to deploy their Java application. This allows consumers to implement your organization’s best practices without becoming infrastructure or cloud experts. +- Publish configuration modules in the HCP Terraform private registry that define approved infrastructure patterns. For example, a module may allow users to choose the cloud provider on which to deploy their Java application. This allows consumers to implement your organization’s best practices without becoming infrastructure or cloud experts. - Enforce best practices and security rules with the Sentinel embedded policy as code framework. For example, policies may restrict regions for production deployments. ### Resources -- [Create a Terraform Cloud Account](https://app.terraform.io/signup/account) -- [Terraform Cloud Documentation](/docs/cloud/index.html) -- [Sentinel Documentation](/cloud-docs/sentinel) -- [Get Started - Terraform Cloud](https://learn.hashicorp.com/collections/terraform/cloud-get-started) tutorials show you how to manage infrastructure using Terraform Cloud's VCS integration +- [Create an HCP Terraform Account](https://app.terraform.io/public/signup/account) +- [HCP Terraform Documentation](/terraform/cloud-docs) +- [Sentinel Documentation](/terraform/cloud-docs/policy-enforcement) +- [Get Started - HCP Terraform](/terraform/tutorials/cloud-get-started) tutorials show you how to manage infrastructure using HCP Terraform's VCS integration ## Terraform Enterprise -Terraform Enterprise allows you to set up a self-hosted distribution of Terraform Cloud. It offers customizable resource limits and is ideal for organizations with strict security and compliance requirements. +Terraform Enterprise allows you to set up a self-hosted distribution of HCP Terraform. It offers customizable resource limits and is ideal for organizations with strict security and compliance requirements. ### Why Terraform Enterprise? Terraform Enterprise lets you: -- Set up a private instance of Terraform Cloud with dedicated support from HashiCorp. +- Set up a private instance of HCP Terraform with dedicated support from HashiCorp. - Accommodate advanced security and compliance requirements. Terraform Enterprise supports several types of installations, including air gapped and active/active architecture, and allows private networking and job scaling for better performance. ### Resources - [Terraform Pricing](https://www.hashicorp.com/products/terraform/pricing) -- [Terraform Enterprise Documentation](/docs/enterprise/index.html) -- [Recommended Enterprise Patterns](https://learn.hashicorp.com/collections/terraform/recommended-patterns) guides +- [Terraform Enterprise Documentation](/terraform/enterprise) +- [Recommended Enterprise Patterns](/terraform/tutorials/recommended-patterns) guides diff --git a/website/docs/intro/use-cases.mdx b/website/docs/intro/use-cases.mdx index 41600af6c7..b858238e2e 100644 --- a/website/docs/intro/use-cases.mdx +++ b/website/docs/intro/use-cases.mdx @@ -3,12 +3,12 @@ layout: "intro" page_title: "Use Cases" sidebar_current: "use-cases" description: |- - Learn how Terraform enables multi-cloud deployments, application management, policy compliance, and self-service infrastructure. + Learn about Terraform use cases, such as enabling multi-cloud deployments, application management, policy compliance, and self-service infrastructure. --- -# Use Cases +# Terraform Use Cases -[HashiCorp Terraform](/intro/index.html) is an infrastructure as code tool that lets you define infrastructure resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to safely and efficiently provision and manage your infrastructure throughout its lifecycle. +[HashiCorp Terraform](/terraform/intro) is an infrastructure as code tool that lets you define infrastructure resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to safely and efficiently provision and manage your infrastructure throughout its lifecycle. This page describes popular Terraform use cases and provides related resources that you can use to create Terraform configurations and workflows. @@ -17,7 +17,7 @@ Provisioning infrastructure across multiple clouds increases fault-tolerance, al ### Resources -- Try our [Deploy Federated Multi-Cloud Kubernetes Clusters](https://learn.hashicorp.com/tutorials/terraform/multicloud-kubernetes) tutorial to provision Kubernetes clusters in both Azure and AWS environments, configure Consul federation with mesh gateways across the two clusters, and deploy microservices across the two clusters to verify federation. +- Try our [Deploy Federated Multi-Cloud Kubernetes Clusters](/terraform/tutorials/networking/multicloud-kubernetes) tutorial to provision Kubernetes clusters in both Azure and AWS environments, configure Consul federation with mesh gateways across the two clusters, and deploy microservices across the two clusters to verify federation. - Browse the [Terraform Registry](https://registry.terraform.io/browse/providers) to find thousands of publicly available providers. @@ -27,30 +27,30 @@ You can use Terraform to efficiently deploy, release, scale, and monitor infrast ### Resources -- Try our [Automate Monitoring with the Terraform Datadog Provider](https://learn.hashicorp.com/tutorials/terraform/datadog-provider?in=terraform/applications) tutorial to deploy a demo Nginx application to a Kubernetes cluster with Helm and install the Datadog agent across the cluster. The Datadog agent reports the cluster health back to your Datadog dashboard. -- Try our [Use Application Load Balancers for Blue-Green and Canary Deployments](https://learn.hashicorp.com/tutorials/terraform/blue-green-canary-tests-deployments) tutorial. You will provision the blue and green environments, add feature toggles to your Terraform configuration to define a list of potential deployment strategies, conduct a canary test, and incrementally promote your green environment. +- Try our [Automate Monitoring with the Terraform Datadog Provider](/terraform/tutorials/applications/datadog-provider) tutorial to deploy a demo Nginx application to a Kubernetes cluster with Helm and install the Datadog agent across the cluster. The Datadog agent reports the cluster health back to your Datadog dashboard. +- Try our [Use Application Load Balancers for Blue-Green and Canary Deployments](/terraform/tutorials/aws/blue-green-canary-tests-deployments) tutorial. You will provision the blue and green environments, add feature toggles to your Terraform configuration to define a list of potential deployment strategies, conduct a canary test, and incrementally promote your green environment. ## Self-Service Clusters -At a large organization, your centralized operations team may get many repetitive infrastructure requests. You can use Terraform to build a "self-serve" infrastructure model that lets product teams manage their own infrastructure independently. You can create and use Terraform modules that codify the standards for deploying and managing services in your organization, allowing teams to efficiently deploy services in compliance with your organization’s practices. Terraform Cloud can also integrate with ticketing systems like ServiceNow to automatically generate new infrastructure requests. +At a large organization, your centralized operations team may get many repetitive infrastructure requests. You can use Terraform to build a "self-serve" infrastructure model that lets product teams manage their own infrastructure independently. You can create and use Terraform modules that codify the standards for deploying and managing services in your organization, allowing teams to efficiently deploy services in compliance with your organization’s practices. HCP Terraform can also integrate with ticketing systems like ServiceNow to automatically generate new infrastructure requests. ### Resources -- Try the [Use Modules from the Registry](https://learn.hashicorp.com/tutorials/terraform/module-use?in=terraform/modules) tutorial to get started using public modules in your Terraform configuration. -Try the [Build and Use a Local Module](https://learn.hashicorp.com/tutorials/terraform/module-create?in=terraform/modules) tutorial on HashiCorp Learn to create a module to manage AWS S3 buckets. -- Follow these [ServiceNow Service Catalog Integration Setup Instructions](/cloud-docs/integrations/service-now) to connect ServiceNow to Terraform Cloud. +- Try the [Use Modules from the Registry](/terraform/tutorials/modules/module-use) tutorial to get started using public modules in your Terraform configuration. +Try the [Build and Use a Local Module](/terraform/tutorials/modules/module-create) tutorial to create a module to manage AWS S3 buckets. +- Follow these [ServiceNow Service Catalog Integration Setup Instructions](/terraform/cloud-docs/integrations/service-now) to connect ServiceNow to HCP Terraform. ## Policy Compliance and Management -Terraform can help you enforce policies on the types of resources teams can provision and use. Ticket-based review processes are a bottleneck that can slow down development. Instead, you can use Sentinel, a policy-as-code framework, to automatically enforce compliance and governance policies before Terraform makes infrastructure changes. Sentinel is available with the [Terraform Cloud team and governance](https://www.hashicorp.com/products/terraform/pricing) tier. +Terraform can help you enforce policies on the types of resources teams can provision and use. Ticket-based review processes are a bottleneck that can slow down development. Instead, you can use Sentinel, a policy-as-code framework, to automatically enforce compliance and governance policies before Terraform makes infrastructure changes. Sentinel policies are available in Terraform Enterprise and [HCP Terraform](https://www.hashicorp.com/products/terraform/pricing). ### Resources -- Try the [Control Costs with Policies](https://learn.hashicorp.com/tutorials/terraform/cost-estimation) tutorial on HashiCorp Learn to estimate the cost of infrastructure changes and define policy to limit it. +- Try the [Control Costs with Policies](/terraform/tutorials/cloud-get-started/cost-estimation) tutorial to estimate the cost of infrastructure changes and define policy to limit it. -- The [Sentinel documentation](/docs/cloud/sentinel/index.html) provides more in-depth information and a list of example policies that you can adapt for your use cases. +- The [Sentinel documentation](/terraform/cloud-docs/policy-enforcement) provides more in-depth information and a list of example policies that you can adapt for your use cases. ## PaaS Application Setup @@ -58,29 +58,29 @@ Platform as a Service (PaaS) vendors like Heroku allow you to create web applic ### Resources -Try the [Deploy, Manage, and Scale an Application on Heroku](https://learn.hashicorp.com/tutorials/terraform/heroku-provider?in=terraform/applications) tutorial on HashiCorp Learn manage an application’s lifecycle with Terraform. +Try the [Deploy, Manage, and Scale an Application on Heroku](/terraform/tutorials/applications/heroku-provider) tutorial to manage an application’s lifecycle with Terraform. ## Software Defined Networking Terraform can interact with Software Defined Networks (SDNs) to automatically configure the network according to the needs of the applications running in it. This lets you move from a ticket-based workflow to an automated one, reducing deployment times. -For example, when a service registers with [HashiCorp Consul](https://www.consul.io/), [Consul-Terraform-Sync](https://www.consul.io/docs/nia) can automatically generate Terraform configuration to expose appropriate ports and adjust network settings for any SDN that has an associated Terraform provider. Network Infrastructure Automation (NIA) allows you to safely approve the changes that your applications require without having to manually translate tickets from developers into the changes you think their applications need. +For example, when a service registers with [HashiCorp Consul](https://www.consul.io/), [Consul-Terraform-Sync](/consul/docs/nia) can automatically generate Terraform configuration to expose appropriate ports and adjust network settings for any SDN that has an associated Terraform provider. Network Infrastructure Automation (NIA) allows you to safely approve the changes that your applications require without having to manually translate tickets from developers into the changes you think their applications need. ### Resources -- Try the [Network Infrastructure Automation with Consul-Terraform-Sync Intro](https://learn.hashicorp.com/tutorials/consul/consul-terraform-sync-intro?in=consul/network-infrastructure-automation) tutorial on HashiCorp Learn to install Consul-Terraform-Sync on a node. You will then configure it to communicate with a Consul datacenter, react to service changes, and execute an example task. -- Try the [Consul-Terraform-Sync and Terraform Enterprise/Cloud Integration](https://learn.hashicorp.com/tutorials/consul/consul-terraform-sync-terraform-enterprise?in=consul/network-infrastructure-automation) tutorial on HashiCorp Learn to configure Consul-Terraform-Sync to interact with Terraform Enterprise and Terraform Cloud. +- Try the [Network Infrastructure Automation with Consul-Terraform-Sync Intro](/consul/tutorials/network-infrastructure-automation/consul-terraform-sync-intro) tutorial to install Consul-Terraform-Sync on a node. You will then configure it to communicate with a Consul datacenter, react to service changes, and execute an example task. +- Try the [Consul-Terraform-Sync and Terraform Enterprise/Cloud Integration](/consul/tutorials/network-infrastructure-automation/consul-terraform-sync-terraform-enterprise) tutorial to configure Consul-Terraform-Sync to interact with Terraform Enterprise and HCP Terraform. ## Kubernetes -Kubernetes is an open-source workload scheduler for containerized applications. Terraform lets you both deploy a Kubernetes cluster and manage its resources (e.g., pods, deployments, services, etc.). You can also use the [Kubernetes Operator for Terraform](https://github.com/hashicorp/terraform-k8s) to manage cloud and on-prem infrastructure through a Kubernetes Custom Resource Definition (CRD) and Terraform Cloud. +Kubernetes is an open-source workload scheduler for containerized applications. Terraform lets you both deploy a Kubernetes cluster and manage its resources (e.g., pods, deployments, services, etc.). You can also use the [Kubernetes Operator for Terraform](https://github.com/hashicorp/terraform-k8s) to manage cloud and on-prem infrastructure through a Kubernetes Custom Resource Definition (CRD) and HCP Terraform. ### Resources -- Try the [Manage Kubernetes Resources via Terraform](https://learn.hashicorp.com/tutorials/terraform/kubernetes-provider?in=terraform/kubernetes) tutorial on HashiCorp Learn. You will use Terraform to schedule and expose a NGINX deployment on a Kubernetes cluster. -- Try the [Deploy Infrastructure with the Terraform Cloud Operator for Kubernetes](https://learn.hashicorp.com/tutorials/terraform/kubernetes-operator) tutorial on HashiCorp Learn. You will configure and deploy the Operator to a Kubernetes cluster and use it to create a Terraform Cloud workspace and provision a message queue for an example application. +- Try the [Manage Kubernetes Resources via Terraform](/terraform/tutorials/kubernetes/kubernetes-provider) tutorial. You will use Terraform to schedule and expose a NGINX deployment on a Kubernetes cluster. +- Try the [Deploy Infrastructure with the HCP Terraform Operator for Kubernetes](/terraform/tutorials/kubernetes/kubernetes-operator) tutorial. You will configure and deploy the Operator to a Kubernetes cluster and use it to create an HCP Terraform workspace and provision a message queue for an example application. ## Parallel Environments diff --git a/website/docs/intro/vs/boto.mdx b/website/docs/intro/vs/boto.mdx index de008b8dce..698e10a28d 100644 --- a/website/docs/intro/vs/boto.mdx +++ b/website/docs/intro/vs/boto.mdx @@ -1,9 +1,9 @@ --- -page_title: 'Terraform vs. Boto, Fog, etc.' -description: 'How Terraform compares to cloud provider client libraries like Boto and Fog. ' +page_title: Terraform versus Boto, Fog, and other cloud provider client libraries +description: Learn how Terraform's syntax compares to Boto, Flog, and other cloud provider client libraries. --- -# Terraform vs. Boto, Fog, etc. +# Terraform versus Boto, Fog, and other cloud provider client libraries Libraries like Boto, Fog, etc. are used to provide native access to cloud providers and services by using their APIs. Some diff --git a/website/docs/intro/vs/cloudformation.mdx b/website/docs/intro/vs/cloudformation.mdx index a677ec089a..eaa57524da 100644 --- a/website/docs/intro/vs/cloudformation.mdx +++ b/website/docs/intro/vs/cloudformation.mdx @@ -1,15 +1,13 @@ --- -page_title: 'Terraform vs. CloudFormation, Heat, etc.' +page_title: Terraform versus CloudFormation, Heat, and other infrastructure as code tools description: >- - How Terraform compares to other infrastructure as code tools like - CloudFormation and Heat. Terraform can simultaneously manage multiple cloud - providers (AWS, OpenStack, etc.) and services (Cloudflare, DNSimple, etc.). + Learn how Terraform manages various cloud providers and services, such as AWS, OpenStack, Cloudflare, and DNSimple, versus CloudFormation, Heat, and other tools. --- -# Terraform vs. CloudFormation, Heat, etc. +# Terraform versus CloudFormation, Heat, and other infrastructure as code tools -Tools like CloudFormation, Heat, etc. allow the details of an infrastructure -to be codified into a configuration file. The configuration files allow +CloudFormation, Heat, and other infrastructure as code tools allow you to codify the details of infrastructure +into a configuration file. The configuration files allow the infrastructure to be elastically created, modified and destroyed. Terraform is inspired by the problems they solve. diff --git a/website/docs/intro/vs/custom.mdx b/website/docs/intro/vs/custom.mdx index 8b381c6dfe..32b18d6eb8 100644 --- a/website/docs/intro/vs/custom.mdx +++ b/website/docs/intro/vs/custom.mdx @@ -1,11 +1,10 @@ --- -page_title: Terraform vs. Custom Solutions +page_title: Terraform versus Custom Solutions description: >- - Why Terraform is easier to use and maintain than custom, internal - infrastructure solutions. + Learn how the Terraform syntax enables to Terraform binary to overcome challenges of custom infrastructure as code solutions. --- -# Terraform vs. Custom Solutions +# Terraform versus Custom Solutions Most organizations start by manually managing infrastructure through simple scripts or web-based interfaces. As the infrastructure grows, @@ -30,9 +29,9 @@ do not need to remember and reason about them. Removing the burden of building the tool allows operators to focus on their infrastructure and not the tooling. -Furthermore, Terraform is an open source tool. In addition to +Furthermore, Terraform is a free, source-available tool. In addition to HashiCorp, the community around Terraform helps to extend its features, fix bugs and document new use cases. Terraform helps solve a problem that exists in every organization and provides a standard that can be adopted to avoid reinventing the wheel between and within organizations. -Its open source nature ensures it will be around in the long term. +Its source code license ensures it will be around in the long term. diff --git a/website/docs/intro/vs/index.mdx b/website/docs/intro/vs/index.mdx index 6d5bec374f..3504ffacb4 100644 --- a/website/docs/intro/vs/index.mdx +++ b/website/docs/intro/vs/index.mdx @@ -1,9 +1,9 @@ --- -page_title: Terraform vs. Alternatives -description: An overview of how Terraform compares to alternative software and tools. +page_title: Terraform versus alternatives overview +description: Terraform lets you define infrastructure as code and automate infrastructure lifecycle management. Learn how Terraform compares to other cloud infrastructure tools. --- -# Terraform vs. Alternatives +# Terraform versus Alternatives Overview Terraform provides a flexible abstraction of resources and providers. This model allows for representing everything from physical hardware, virtual machines, and @@ -16,7 +16,7 @@ entire datacenter. Learn how Terraform compares to: -- [Chef, Puppet, etc.](/intro/vs/chef-puppet) -- [CloudFormation, Heat, etc.](/intro/vs/cloudformation) -- [Boto, Fog, etc.](/intro/vs/boto) -- [Custom Solutions](/intro/vs/custom) +- [Chef, Puppet, etc.](/terraform/intro/vs/chef-puppet) +- [CloudFormation, Heat, etc.](/terraform/intro/vs/cloudformation) +- [Boto, Fog, etc.](/terraform/intro/vs/boto) +- [Custom Solutions](/terraform/intro/vs/custom) diff --git a/website/docs/language/attr-as-blocks.mdx b/website/docs/language/attr-as-blocks.mdx index 0806682798..86b53ebe60 100644 --- a/website/docs/language/attr-as-blocks.mdx +++ b/website/docs/language/attr-as-blocks.mdx @@ -9,9 +9,7 @@ description: >- # Attributes as Blocks --> **Note:** This page is an appendix to the Terraform documentation, and is -outside the normal navigation hierarchy. Most users do not need to know the full -details of the behavior described below. +-> **Note:** This page is an appendix to the Terraform documentation. Most users do not need to know the full details of this behavior. ## Summary @@ -25,14 +23,14 @@ is set to an empty list (` = []`). Most users do not need to know any further details of this "nested block or empty list" behavior. However, read further if you need to: -- Use Terraform's [JSON syntax](/language/syntax/json) with this +- Use Terraform's [JSON syntax](/terraform/language/syntax/json) with this type of resource. - Create a reusable module that wraps this type of resource. ## Details In Terraform v0.12 and later, the language makes a distinction between -[argument syntax and nested block syntax](/language/syntax/configuration#arguments-and-blocks) +[argument syntax and nested block syntax](/terraform/language/syntax/configuration#arguments-and-blocks) within blocks: - Argument syntax sets a named argument for the containing object. If the @@ -46,7 +44,7 @@ within blocks: merging in with any explicitly-defined arguments. The distinction between these is particularly important for -[JSON syntax](/language/syntax/json) +[JSON syntax](/terraform/language/syntax/json) because the same primitive JSON constructs (lists and objects) will be interpreted differently depending on whether a particular name is an argument or a nested block type. @@ -153,7 +151,7 @@ example = [ For the arguments that use the attributes-as-blocks usage mode, the above is a better pattern than using -[`dynamic` blocks](/language/expressions/dynamic-blocks) +[`dynamic` blocks](/terraform/language/expressions/dynamic-blocks) because the case where the caller provides an empty list will result in explicitly assigning an empty list value, rather than assigning no value at all and thus retaining and @@ -163,7 +161,7 @@ dynamically-generating _normal_ nested blocks, though. ## In JSON syntax Arguments that use this special mode are specified in JSON syntax always using -the [JSON expression mapping](/language/syntax/json#expression-mapping) +the [JSON expression mapping](/terraform/language/syntax/json#expression-mapping) to produce a list of objects. The interpretation of these values in JSON syntax is, therefore, equivalent diff --git a/website/docs/language/backend/azurerm.mdx b/website/docs/language/backend/azurerm.mdx new file mode 100644 index 0000000000..6237ed3f61 --- /dev/null +++ b/website/docs/language/backend/azurerm.mdx @@ -0,0 +1,581 @@ +--- +page_title: 'Backend Type: azurerm' +description: Terraform can store state remotely in Azure Blob Storage. +--- + +# azurerm + +Stores the state as a Blob with the given Key within the Blob Container within [the Blob Storage Account](https://docs.microsoft.com/en-us/azure/storage/common/storage-introduction). + +This backend supports state locking and consistency checking with Azure Blob Storage native capabilities. + +## Authentication + +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. + +The `azurerm` backend needs to authenticate to the storage account data plane in order to manipulate the state file blob in the storage account container. In order to do that, it needs to authenticate and to know the data plane URI for the storage account. + +The `azurerm` backend supports 5 methods to authenticate to the storage account data plane: + +- [Azure Active Directory](#azure-active-directory) **(Recommended)** +- [SAS Token](#sas-token) *(Not recommended for new workloads)* +- [Access Key](#access-key) *(Not recommended for new workloads)* +- [Access Key Lookup](#access-key-lookup) *(Not recommended for new workloads)* + +### Azure Active Directory and Access Key Lookup Authentication Types + +There are 5 types of Azure Active Directory authentication supported, which apply to the Azure Active Directory and Access Key Lookup methods. + +- OpenID Connect / Workload identity federation **(Recommended)** + - User Assigned Managed Identity with Federated Credentials **(Recommended)** + - Service Princial / App Registration with Federated Credentials +- User or System Assigned Managed Identity + - User Assigned Managed Identity attached to Azure compute instance (agent / runner) + - System Assigned Managed Identity attached to Azure compute instance (agent / runner) +- Service Principal / App Registration with Client Secret +- Service Principal / App REgistration with Client Certificate +- User Account with Azure CLI only (for local development cycle) + +These types can be supplied via inputs or via a pre-authenticated Azure CLI. We cover them in more depth in the following sections. + +### Data Plane URI + +In most cases, you can infer the data plane URI from the `storage_account_name` and `container_name`. Refer to the [storage account overview documentation](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#standard-endpoints) for more information on the standard endpoints. + +If you are using the ['Azure DNS zone endpoints' feature](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-overview#azure-dns-zone-endpoints-preview), the backend will need to lookup the data plane URI from the management plane. This requires that you set the `lookup_blob_endpoint` configuration option to `true` and the `Reader` role assignment on the storage account. + +## Azure Active Directory + +This method requires a valid Azure Active Directory principal and a predictable storage account data plane URI. + +### Required Configuration Options + +The following configuration options are always required for this method: + +- `use_azuread_auth` - Set to `true` to use Azure Active Directory authentication to the storage account data plane. This can also be set via the `ARM_USE_AZUREAD` environment variable. +- `tenant_id` - The tenant ID of the Azure Active Directory principal is required to authenticate to the storage account data plane. If using Azure CLI, this can be inferred from the CLI session. This can also be set via the `ARM_TENANT_ID` environment variable. +- `storage_account_name` - The name of the storage account to write the state file blob to. +- `container_name` - The name of the storage account container to write the state file blob to. +- `key` - The name of the blob within the storage account container to write the state file to. + +### Optional Inputs + +These optional configuration options apply when [looking up the data plane URI](#data-plane-uri) from the management plane. They are not required when the data plane URI can be inferred from the `storage_account_name` and `container_name`. + +- `lookup_blob_endpoint` - Set to `true` to lookup the storage account data plane URI from the management plane. This is required if you are using the 'Azure DNS zone endpoints' feature. Defaults to `false`. This value can also be sourced from the `ARM_USE_DNS_ZONE_ENDPOINT` environment variable. +- `subscription_id` - The subscription ID of the storage account is required to query the management plane. This is only required if `lookup_blob_endpoint` is set to `true`. If using Azure CLI, this can be inferred from the CLI session. This can also be set via the `ARM_SUBSCRIPTION_ID` environment variable. +- `resource_group_name` - The resource group name of the storage account is required to query the management plane. This is only required if `lookup_blob_endpoint` is set to `true`. + +### Storage Account Required Role Assignments + +The recommended data plane role assignments required for this method are either one of: + +- `Storage Blob Data Owner` on the storage account container (Recommended) +- `Storage Blob Data Contributor` on the storage account + +The recommended management plane role assignments required for this method are: + +- `Reader` on the storage account *(Only required if `lookup_blob_endpoint` is set to `true`)* + +### Azure Active Directory with OpenID Connect / Workload identity federation + +#### Required Configuration Options + +The following additional configuration options are always required for this sub-type: + +- `use_oidc` - Set to `true` to use OpenID Connect / Workload identity federation to authenticate to the storage account data plane. This can also be set via the `ARM_USE_OIDC` environment variable. +- `client_id` - The client ID of the Azure Active Directory Service Principal / App Registration or User Assigned Managed Identity is required to authenticate to the storage account data plane. This can also be set via the `ARM_CLIENT_ID` environment variable. + +#### Example Configuration for GitHub + +With GitHub, the ID Token environment variables are automatically found, so no further settings are required. + +```hcl +terraform { + backend "azurerm" { + use_oidc = true # Can also be set via `ARM_USE_OIDC` environment variable. + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +#### Example Configuration for Azure DevOps + +With Azure DevOps, the ID Token endpoint environment variables are automatically found, but you need to supply the service connection ID in `oidc_azure_service_connection_id`. If you are using the `AzureCLI` or `AzurePowerShell` tasks, the service connection ID is automatically set to the `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` environment variable. + +```hcl +terraform { + backend "azurerm" { + use_oidc = true # Can also be set via `ARM_USE_OIDC` environment variable. + oidc_azure_service_connection_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID` environment variable. + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Azure Active Directory with Compute Attached Managed Identity + +#### Required Configuration Options + +The following additional configuration options are always required for this sub-type: + +- `use_msi` - Set to `true` to use the managed identity to authenticate to the storage account data plane. This can also be set via the `ARM_USE_MSI` environment variable. + +#### Optional Configuration Options + +The following additional configuration options are optional for this sub-type: + +- `client_id` - The client ID of the User Assigned Managed Identity is required to authenticate to the storage account data plane. This is not required for System Assigned Managed Identity. This can also be set via the `ARM_CLIENT_ID` environment variable. + +#### Example Configuration + +```hcl +terraform { + backend "azurerm" { + use_msi = true # Can also be set via `ARM_USE_MSI` environment variable. + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. Not required for System Assigned Managed Identity. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Azure Active Directory with Azure CLI + +You must have a pre-authenticated Azure CLI session using any supported method. + +#### Required Configuration Options + +The following additional configuration options are always required for this sub-type: + +- `use_cli` - Set to `true` to use the Azure CLI session authenticate to the storage account data plane. This can also be set via the `ARM_USE_CLI` environment variable. + +#### Example Configuration + +```hcl +terraform { + backend "azurerm" { + use_cli = true # Can also be set via `ARM_USE_CLI` environment variable. + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. Azure CLI will fallback to use the connected tenant ID if not supplied. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Azure Active Directory with Client Secret + +Terraform retains this method for backwards compatibility only, do not use it for any new workloads. + +~> **Warning!** This method requires you to manage and rotate a secret. Use OpenID Connect / Workload identity federation as a more secure approach. + +#### Required Inputs + +The following additional configuration options are always required for this sub-type: + +- `client_id` - The client ID of the Azure Active Directory Service Principal / App Registration is required to authenticate to the storage account data plane. This can also be set via the `ARM_CLIENT_ID` environment variable. +- `client_secret` - The client secret of the Azure Active Directory Service Principal / App Registration is required to authenticate to the storage account data plane. This can also be set via the `ARM_CLIENT_SECRET` environment variable. + +#### Example Configuration + +```hcl +terraform { + backend "azurerm" { + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + client_secret = "************************************" # Can also be set via `ARM_CLIENT_SECRET` environment variable. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Azure Active Directory with Client Certificate + +Terraform retains this method for backwards compatibility only, do not use it for any new workloads. + +~> **Warning!** This method requires you to manage and rotate a secret. Use OpenID Connect / Workload identity federation as a more secure approach. + +#### Required Inputs + +The following additional configuration options are always required for this sub-type: + +- `client_id` - The client ID of the Azure Active Directory Service Principal / App Registration is required to authenticate to the storage account data plane. This can also be set via the `ARM_CLIENT_ID` environment variable. +- `client_certificate_path` - The path to the client certificate bundle is required to authenticate to the storage account data plane. This can also be set via the `ARM_CLIENT_CERTIFICATE_PATH` environment variable. +- `client_certificate_password` - The password for the client certificate bundle is required to authenticate to the storage account data plane. This can also be set via the `ARM_CLIENT_CERTIFICATE_PASSWORD` environment variable. + +#### Example Configuration + +```hcl +terraform { + backend "azurerm" { + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + client_certificate_path = "/path/to/bundle.pfx" # Can also be set via `ARM_CLIENT_CERTIFICATE_PATH` environment variable. + client_certificate_password = "************************************" # Can also be set via `ARM_CLIENT_CERTIFICATE_PASSWORD` environment variable. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +## Access Key + +This method requires you find the Access Key for the storage account and supply it to the backend configuration. + +The Access Key is then used to directly authenticate to the storage account data plane. + +Terraform retains this method for backwards compatibility, we do not recommend it for new workloads. + +### Required Configuration Options + +The following configuration options are always required for this method: + +- `access_key` - The Access Key of the storage account is required to authenticate to the storage account data plane. This can also be set via the `ARM_ACCESS_KEY` environment variable. +- `storage_account_name` - The name of the storage account to write the state file blob to. +- `container_name` - The name of the storage account container to write the state file blob to. +- `key` - The name of the blob within the storage account container to write the state file to. + +### Storage Account Required Role Assignments + +There are no role assignments required on the storage account for this method as the Access Key is used to authenticate to the data plane. + +### Example Configuration + +~> **Warning!** This method requires you to manage and rotate a secret. Consider using OIDC as a more secure approach. + +```hcl +terraform { + backend "azurerm" { + access_key = "abcdefghijklmnopqrstuvwxyz0123456789..." # Can also be set via `ARM_ACCESS_KEY` environment variable. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +## SAS Token + +This method requires you generate a SAS Token for the storage account container or blob and supply it to the backend configuration. + +The SAS Token is then used to directly authenticate to the storage account data plane. + +Terraform retains this method for backwards compatibility, we do not recommend it for new workloads. + +### Required Configuration Options + +The following configuration options are always required for this method: + +- `sas_token` - The SAS Token for the storage account container or blob is required to authenticate to the storage account data plane. This can also be set via the `ARM_SAS_TOKEN` environment variable. +- `storage_account_name` - The name of the storage account to write the state file blob to. +- `container_name` - The name of the storage account container to write the state file blob to. +- `key` - The name of the blob within the storage account container to write the state file to. + +### Storage Account Required Permissions + +The SAS Token requires `write` and `list` permissions on the container or blob. + +### Example Configuration + +~> **Warning!** This method requires you to manage and rotate a secret. Consider using OIDC as a more secure approach. + +```hcl +terraform { + backend "azurerm" { + sas_token = "abcdefghijklmnopqrstuvwxyz0123456789..." # Can also be set via `ARM_SAS_TOKEN` environment variable. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +## Access Key Lookup + +This method requires a valid Azure Active Directory principal and is a fallback for when Azure Active Directory authentication cannot be used on the storage account data plane. + +This method queries the management plane to get the storage account Access Key and then uses that Access Key to authenticate to the storage account data plane. It requires elevated permissions on the storage account. + +Terraform retains this method for backwards compatibility, we do not recommend it for new workloads. + +### Required Configuration Options + +The following configuration options are always required for this method: + +- `tenant_id` - The tenant ID of the Azure Active Directory principal is required to authenticate to the storage account management and data plane. If using Azure CLI, this can be inferred from the CLI session. This can also be set via the `ARM_TENANT_ID` environment variable. +- `subscription_id` - The subscription ID of the storage account is required to query the management plane. If using Azure CLI, this can be inferred from the CLI session. This can also be set via the `ARM_SUBSCRIPTION_ID` environment variable. +- `resource_group_name` - The resource group name of the storage account is required to query the management plane. +- `storage_account_name` - The name of the storage account to write the state file blob to. +- `container_name` - The name of the storage account container to write the state file blob to. +- `key` - The name of the blob within the storage account container to write the state file to. + +### Optional Configuration Options + +These optional configuration options apply when [looking up the data plane URI](#data-plane-uri) from the management plane. They are not required when the data plane URI can be inferred from the `storage_account_name` and `container_name`. + +- `lookup_blob_endpoint` - Set to `true` to lookup the storage account data plane URI from the management plane. This is required if you are using the 'Azure DNS zone endpoints' feature. Defaults to `false`. This value can also be sourced from the `ARM_USE_DNS_ZONE_ENDPOINT` environment variable. + +### Storage Account Required Role Assignments + +The recommended data plane role assignments required for this method are either one of: + +- `Storage Blob Data Owner` on the storage account container (Recommended) +- `Storage Blob Data Contributor` on the storage account + +The recommended management plane role assignments required for this method are: + +- `Reader` on the storage account +- `Storage Account Key Operator Service Role` on the storage account + +### Access Key Lookup with OpenID Connect / Workload identity federation + +OpenID Connect / Workload identity federation is the recommended method for this scenario. + +#### Required Configuration Options + +The following additional configuration options are always required for this sub-type: + +- `use_oidc` - Set to `true` to use OpenID Connect / Workload identity federation to authenticate to the storage account management and data plane. This can also be set via the `ARM_USE_OIDC` environment variable. +- `client_id` - The client ID of the Azure Active Directory Service Principal / App Registration or User Assigned Managed Identity is required to authenticate to the storage account management and data plane. This can also be set via the `ARM_CLIENT_ID` environment variable. + +#### Example Configuration for GitHub + +With GitHub, the ID Token environment variables are automatically found, so no further settings are required. + +```hcl +terraform { + backend "azurerm" { + use_oidc = true # Can also be set via `ARM_USE_OIDC` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + subscription_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_SUBSCRIPTION_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + resource_group_name = "StorageAccount-ResourceGroup" # Can be passed via `-backend-config=`"resource_group_name="` in the `init` command. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +#### Example Configuration for Azure DevOps + +With Azure DevOps, the ID Token endpoint environment variables are automatically found, but you need to supply the service connection ID in `oidc_azure_service_connection_id`. If you are using the `AzureCLI` or `AzurePowerShell` tasks, the service connection ID is automatically set to the `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` environment variable. + +```hcl +terraform { + backend "azurerm" { + use_oidc = true # Can also be set via `ARM_USE_OIDC` environment variable. + oidc_azure_service_connection_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + subscription_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_SUBSCRIPTION_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + resource_group_name = "StorageAccount-ResourceGroup" # Can be passed via `-backend-config=`"resource_group_name="` in the `init` command. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Access Key Lookup with Compute Attached Managed Identity + +#### Required Configuration Options + +The following additional configuration options are always required for this sub-type: + +- `use_msi` - Set to `true` to use the managed identity to authenticate to the storage account data plane. This can also be set via the `ARM_USE_MSI` environment variable. + +#### Optional Configuration Options + +The following additional configuration options are optional for this sub-type: + +- `client_id` - The client ID of the User Assigned Managed Identity is required to authenticate to the storage account data plane. This is not required for System Assigned Managed Identity. This can also be set via the `ARM_CLIENT_ID` environment variable. + +#### Example Configuration + +```hcl +terraform { + backend "azurerm" { + use_msi = true # Can also be set via `ARM_USE_MSI` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + subscription_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_SUBSCRIPTION_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. Not required for System Assigned Managed Identity. + resource_group_name = "StorageAccount-ResourceGroup" # Can be passed via `-backend-config=`"resource_group_name="` in the `init` command. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Access Key Lookup with Azure CLI + +You must have a pre-authenticated Azure CLI session using any supported method. + +```hcl +terraform { + backend "azurerm" { + use_cli = true # Can also be set via `ARM_USE_CLI` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. Azure CLI will fallback to use the connected tenant ID if not supplied. + subscription_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_SUBSCRIPTION_ID` environment variable. Azure CLI will fallback to use the connected subscription ID if not supplied. + resource_group_name = "StorageAccount-ResourceGroup" # Can be passed via `-backend-config=`"resource_group_name="` in the `init` command. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Access Key Lookup with Client Secret + +Terraform retains this method for backwards compatibility only, do not use it for any new workloads. + +~> **Warning!** This method requires you to manage and rotate a secret. Use OpenID Connect / Workload identity federation as a more secure approach. + +```hcl +terraform { + backend "azurerm" { + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + subscription_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_SUBSCRIPTION_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + client_secret = "************************************" # Can also be set via `ARM_CLIENT_SECRET` environment variable. + resource_group_name = "StorageAccount-ResourceGroup" # Can be passed via `-backend-config=`"resource_group_name="` in the `init` command. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +### Access Key Lookup with Client Certificate + +Terraform retains this method for backwards compatibility only, do not use it for any new workloads. + +~> **Warning!** This method requires you to manage and rotate a secret. Use OpenID Connect / Workload identity federation as a more secure approach. + +```hcl +terraform { + backend "azurerm" { + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + subscription_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_SUBSCRIPTION_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + client_certificate_path = "/path/to/bundle.pfx" # Can also be set via `ARM_CLIENT_CERTIFICATE_PATH` environment variable. + client_certificate_password = "************************************" # Can also be set via `ARM_CLIENT_CERTIFICATE_PASSWORD` environment variable. + resource_group_name = "StorageAccount-ResourceGroup" # Can be passed via `-backend-config=`"resource_group_name="` in the `init` command. + storage_account_name = "abcd1234" # Can be passed via `-backend-config=`"storage_account_name="` in the `init` command. + container_name = "tfstate" # Can be passed via `-backend-config=`"container_name="` in the `init` command. + key = "prod.terraform.tfstate" # Can be passed via `-backend-config=`"key="` in the `init` command. + } +} +``` + +## terraform_remote_state Data Source + +To use the `terraform_remote_state` data source with the `azurerm` backend, you must use the exact same configuration as you would for the `backend` block in your configuration. + +For example to use [Direct Azure Active Directory authentication with OpenID Connect / Workload identity federation for GitHub](#azure-active-directory-with-openid-connect--workload-identity-federation) you would use the following configuration: + +```hcl +data "terraform_remote_state" "foo" { + backend = "azurerm" + config = { + use_oidc = true # Can also be set via `ARM_USE_OIDC` environment variable. + use_azuread_auth = true # Can also be set via `ARM_USE_AZUREAD` environment variable. + tenant_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_TENANT_ID` environment variable. + client_id = "00000000-0000-0000-0000-000000000000" # Can also be set via `ARM_CLIENT_ID` environment variable. + storage_account_name = "abcd1234" # There is not environment variable support for this input. + container_name = "tfstate" # There is not environment variable support for this input. + key = "prod.terraform.tfstate" # There is not environment variable support for this input. + } +} +``` + +## Configuration Variables + +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. + +For more information on when each of these configuration settings is required, refer to the previous sections of this page. + +The following configuration options are supported: + +* `storage_account_name` - (Required) The Name of [the Storage Account](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account). + +* `container_name` - (Required) The Name of [the Storage Container](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container) within the Storage Account. + +* `key` - (Required) The name of the Blob used to retrieve/store Terraform's State file inside the Storage Container. + +* `environment` - (Optional) The Azure Environment which should be used. This can also be sourced from the `ARM_ENVIRONMENT` environment variable. Possible values are `public`, `china` and `usgovernment`. Defaults to `public`. + +* `metadata_host` - (Optional) The Hostname of the Azure Metadata Service (for example `management.azure.com`), used to obtain the Cloud Environment when using a Custom Azure Environment. This can also be sourced from the `ARM_METADATA_HOSTNAME` Environment Variable. + +* `lookup_blob_endpoint` - (Optional) Set to `true` to look up the Storage Account Data Plane URI from the Management Plane. Defaults to `false`. This value can also be sourced from the `ARM_USE_DNS_ZONE_ENDPOINT` environment variable. + +* `snapshot` - (Optional) Set to `true` to snapshot the Blob used to store the Terraform state file before use. Defaults to `false`. This value can also be sourced from the `ARM_SNAPSHOT` environment variable. + +* `tenant_id` - (Optional) The Tenant ID of the principal. This can also be sourced from the `ARM_TENANT_ID` environment variable. + +* `use_azuread_auth` - (Optional) Whether Azure Active Directory Authentication for storage account data plane authentication. This can also be sourced from the `ARM_USE_AZUREAD` environment variable. + +* `subscription_id` - (Optional) The Subscription ID of the storage account required for management plane authentication. This can also be sourced from the `ARM_SUBSCRIPTION_ID` environment variable. + +* `resource_group_name` - (Optional) The Name of the Resource Group in which the Storage Account exists required for management plane authentication. + +* `access_key` - (Optional) The Access Key used to authenticate to the storage account data plane with the [Direct Access Key](#direct-access-key) authenticaton method. This can also be sourced from the `ARM_ACCESS_KEY` environment variable. + +* `sas_token` - (Optional) The SAS Token used to authenticate to the storage account data plane with the [Direct SAS Token](#direct-sas-token) authentication method. This can also be sourced from the `ARM_SAS_TOKEN` environment variable. + +* `use_cli` - (Optional) Set to `true` to use the Azure CLI session for authentication to the storage account management and data plane. This value can also be sourced from the `ARM_USE_CLI` environment variable. + +* `use_oidc` - (Optional) Set to `true` to use OpenID Connect / Workload identity federation authentication for authentication to the storage account management and data plane. This can also be sourced from the `ARM_USE_OIDC` environment variable. + +* `client_id` - (Optional) The Client ID of the Azure Active Directory Principal required for some authentication sub-types. This can also be sourced from the `ARM_CLIENT_ID` environment variable. + +* `ado_pipeline_service_connection_id` - (Optional) The Azure DevOps Pipeline Service Connection ID required for Open ID Connect / Workload identity federation authentication with Azure DevOps. This can also be sourced from the `ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID` or `ARM_OIDC_AZURE_SERVICE_CONNECTION_ID` environment variables. The provider will look for values in this order and use the first it finds configured. + +* `oidc_request_url` - (Optional) The URL for the Open ID Connect provider from which to request an ID token. This is only required for advanced scenarios or third party integrations. This can also be sourced from the `ARM_OIDC_REQUEST_URL`, `ACTIONS_ID_TOKEN_REQUEST_URL` or `SYSTEM_OIDCREQUESTURI` environment variables. The provider will look for values in this order and use the first it finds configured. + +* `oidc_request_token` - (Optional) The bearer token for the request to the Open ID Connect provider. This is only required for advanced scenarios or third party integrations. This can also be sourced from the `ARM_OIDC_REQUEST_TOKEN`, `ACTIONS_ID_TOKEN_REQUEST_TOKEN` or `SYSTEM_ACCESSTOKEN` environment variables. The provider will look for values in this order and use the first it finds configured. + +* `oidc_token` - (Optional) The ID Token when authenticating using OpenID Connect. This is only required for advanced scenarios or third party integrations. This can also be sourced from the `ARM_OIDC_TOKEN` environment variable. + +* `oidc_token_file_path` - (Optional) The path to a file containing an ID token when authenticating using OpenID Connect. This is only required for advanced scenarios or third party integrations. This can also be sourced from the `ARM_OIDC_TOKEN_FILE_PATH` environment variable. + +* `use_aks_workload_identity` (Optional) Set to `true` to use Azure AKS Workload Identity for authentication. This is only required for advanced scenarios or third party integrations. This can also be sourced from the `ARM_USE_AKS_WORKLOAD_IDENTITY` environment variable. + +* `use_msi` - (Optional) Set to `true` to use a Compute Attached Managed Service Identity for authentication to the storage account management and data planes. This can also be sourced from the `ARM_USE_MSI` environment variable. + +* `msi_endpoint` - (Optional) The path to a custom Managed Service Identity endpoint which is automatically determined if not specified. This can also be sourced from the `ARM_MSI_ENDPOINT` environment variable. + +* `client_id_file_path` (Optional) The path to a file containing the Client ID which should be used. This can also be sourced from the `ARM_CLIENT_ID_FILE_PATH` Environment Variable. + +* `client_certificate_password` - (Optional) The password associated with the Client Certificate specified in `client_certificate_path`. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PASSWORD` environment variable. + +* `client_certificate_path` - (Optional) The path to the PFX file used as the Client Certificate when authenticating as a Service Principal. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PATH` environment variable. + +* `client_certificate` - (Optional) Base64 encoded PKCS#12 certificate bundle to use when authenticating as a Service Principal using a Client Certificate. This can also be sourced from the `ARM_CLIENT_CERTIFICATE` environment variable. + +* `client_id_file_path` (Optional) The path to a file containing the Client ID which should be used. This can also be sourced from the `ARM_CLIENT_ID_FILE_PATH` Environment Variable. + +* `client_secret` - (Optional) The Client Secret of the Service Principal. This can also be sourced from the `ARM_CLIENT_SECRET` environment variable. + +* `client_secret_file_path` - (Optional) The path to a file containing the Client Secret which should be used. This can also be sourced from the `ARM_CLIENT_SECRET_FILE_PATH` Environment Variable. diff --git a/website/docs/language/settings/backends/consul.mdx b/website/docs/language/backend/consul.mdx similarity index 90% rename from website/docs/language/settings/backends/consul.mdx rename to website/docs/language/backend/consul.mdx index 603fcfb2cd..f6c60f01e2 100644 --- a/website/docs/language/settings/backends/consul.mdx +++ b/website/docs/language/backend/consul.mdx @@ -7,7 +7,7 @@ description: Terraform can store state in Consul. Stores the state in the [Consul](https://www.consul.io/) KV store at a given path. -This backend supports [state locking](/language/state/locking). +This backend supports [state locking](/terraform/language/state/locking). ## Example Configuration @@ -22,7 +22,7 @@ terraform { ``` Note that for the access credentials we recommend using a -[partial configuration](/language/settings/backends/configuration#partial-configuration). +[partial configuration](/terraform/language/backend#partial-configuration). ## Data Source Configuration @@ -37,7 +37,7 @@ data "terraform_remote_state" "foo" { ## Configuration Variables -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. The following configuration options / environment variables are supported: diff --git a/website/docs/language/backend/cos.mdx b/website/docs/language/backend/cos.mdx new file mode 100644 index 0000000000..80551ad7ab --- /dev/null +++ b/website/docs/language/backend/cos.mdx @@ -0,0 +1,223 @@ +--- +page_title: 'Backend Type: cos' +description: >- + Terraform can store the state remotely, making it easier to version and work + with in a team. +--- + +# COS + +Stores the state as an object in a configurable prefix in a given bucket on [Tencent Cloud Object Storage](https://intl.cloud.tencent.com/product/cos) (COS). + +This backend supports [state locking](/terraform/language/state/locking). Storing your state in a COS bucket requires the following permissions: + +- `CreateTag`, `DeleteTag`, and `DescribeTags` on the tag key `tencentcloud-terraform-lock` +- `Put`, `Get`, and `Delete` files for the specified bucket's prefix + +~> **Warning!** It is highly recommended that you enable [Object Versioning](https://intl.cloud.tencent.com/document/product/436/19883) +on the COS bucket to allow for state recovery in the case of accidental deletions and human error. + +## Example Configuration + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-1258798060" + prefix = "terraform/state" + } +} +``` + +This assumes we have a [COS Bucket](https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs/resources/cos_bucket) created named `bucket-for-terraform-state-1258798060`, +Terraform state will be written into the file `terraform/state/terraform.tfstate`. + +## Data Source Configuration + +To make use of the COS remote state in another configuration, use the [`terraform_remote_state` data source](/terraform/language/state/remote-state-data). + +```hcl +data "terraform_remote_state" "foo" { + backend = "cos" + + config = { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-1258798060" + prefix = "terraform/state" + } +} +``` + +## Configuration Variables + +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backends#credentials-and-sensitive-data) for details. + +The following configuration options or environment variables are supported: + +- `secret_id` - (Optional) Secret id of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_ID`. +- `secret_key` - (Optional) Secret key of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_KEY`. +- `security_token` - (Optional) TencentCloud Security Token of temporary access credentials. It supports environment variables `TENCENTCLOUD_SECURITY_TOKEN`. +- `region` - (Optional) The region of the COS bucket. It supports environment variables `TENCENTCLOUD_REGION`. +- `bucket` - (Required) The name of the COS bucket. You shall manually create it first. +- `prefix` - (Optional) The directory for saving the state file in bucket. Default to "env:". +- `key` - (Optional) The path for saving the state file in bucket. Defaults to `terraform.tfstate`. +- `encrypt` - (Optional) Whether to enable server side encryption of the state file. If it is true, COS will use 'AES256' encryption algorithm to encrypt state file. +- `acl` - (Optional) Object ACL to be applied to the state file, allows `private` and `public-read`. Defaults to `private`. +- `accelerate` - (Optional) Whether to enable global Acceleration. Defaults to `false`. +- `endpoint` - (Optional) The Custom Endpoint for the COS backend. It supports the environment variable `TENCENTCLOUD_ENDPOINT`. +- `domain` - (Optional) The root domain of the API request. Defaults to `tencentcloudapi.com`. It supports the environment variable `TENCENTCLOUD_DOMAIN`. + +### Assume Role +If provided with an assume role, Terraform will attempt to assume this role using the supplied credentials. +Assume role can be provided by adding an `assume_role` block in the cos backend block. + +- `assume_role` - (Optional) The `assume_role` block. If provided, terraform will attempt to assume this role using the supplied credentials. + +The details of `assume_role` block as following: +- `role_arn` - (Required) The ARN of the role to assume. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_ARN`. +- `session_name` - (Required) The session name to use when making the AssumeRole call. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME`. +- `session_duration` - (Required) The duration of the session when making the AssumeRole call. Its value ranges from 0 to 43200(seconds), and default is 7200 seconds. It can be sourced from the `TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION`. +- `policy` - (Optional) A more restrictive policy when making the AssumeRole call. Its content must not contains `principal` elements. Notice: more syntax references, please refer to: [policies syntax logic](https://intl.cloud.tencent.com/document/product/598/10603). + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + assume_role { + role_arn = "qcs::cam::uin/xxx:roleName/yyy" + session_name = "my-session-name" + session_duration = 7200 + } + } +} +``` + +In addition, these `assume_role` configurations can also be provided by environment variables. + +Usage: + +```shell +$ export TENCENTCLOUD_SECRET_ID="my-secret-id" +$ export TENCENTCLOUD_SECRET_KEY="my-secret-key" +$ export TENCENTCLOUD_REGION="ap-guangzhou" +$ export TENCENTCLOUD_ASSUME_ROLE_ARN="qcs::cam::uin/xxx:roleName/yyy" +$ export TENCENTCLOUD_ASSUME_ROLE_SESSION_NAME="my-session-name" +$ export TENCENTCLOUD_ASSUME_ROLE_SESSION_DURATION=7200 +$ terraform plan +``` + +### Shared credentials +You can use [Tencent Cloud credentials](https://www.tencentcloud.com/document/product/1013/33464) to specify your credentials. The default location is `$HOME/.tccli` on Linux and macOS, And `"%USERPROFILE%\.tccli"` on Windows. You can optionally specify a different location in the Terraform configuration by providing the `shared_credentials_dir` argument or using the `TENCENTCLOUD_SHARED_CREDENTIALS_DIR` environment variable. This method also supports a `profile` configuration and matching `TENCENTCLOUD_PROFILE` environment variable: + +- `shared_credentials_dir` - (Optional) The directory of the shared credentials. It can also be sourced from the `TENCENTCLOUD_SHARED_CREDENTIALS_DIR` environment variable. If not set this defaults to ~/.tccli. +- `profile` - (Optional) The profile name as set in the shared credentials. It can also be sourced from the `TENCENTCLOUD_PROFILE` environment variable. If not set, the default profile created with `tccli configure` will be used. + +Usage: + +On Linux/MacOS + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + shared_credentials_dir = "/Users/tf_user/.tccli" + profile = "default" + } +} +``` + +On Windows + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + shared_credentials_dir = "C:\\Users\\tf_user\\.tccli" + profile = "default" + } +} +``` + +In addition, these `shared_credentials_dir`, `profile` configurations can also be provided by environment variables. + +Usage: + +```shell +$ export PROVIDER_SHARED_CREDENTIALS_DIR="/Users/tf_user/.tccli" +$ export PROVIDER_PROFILE="default" +$ terraform plan +``` + +### Cam role name +If provided with a Cam role name, Terraform will just access the metadata URL: `http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials/` to obtain the STS credential. The CVM Instance Role also can be set using the `TENCENTCLOUD_CAM_ROLE_NAME` environment variables. + +- `cam_role_name` - (Optional) The name of the CVM instance CAM role. It can be sourced from the `TENCENTCLOUD_CAM_ROLE_NAME` environment variable. + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + cam_role_name = "my-cam-role-name" + } +} +``` + +It can also be authenticated together with method Assume role. Authentication process: Perform CAM authentication first, then proceed with Assume role authentication. + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-{appid}" + prefix = "terraform/state" + cam_role_name = "my-cam-role-name" + assume_role { + role_arn = "qcs::cam::uin/xxx:roleName/yyy" + session_name = "my-session-name" + session_duration = 7200 + external_id = "my-external-id" + } + } +} +``` + +In addition, these `cam_role_name` configurations can also be provided by environment variables. + +Usage: + +```shell +$ export PROVIDER_CAM_ROLE_NAME="my-cam-role-name" +$ terraform plan +``` + +### Endpoint +If provided with an endpoint URL, Terraform will attempt to access the COS backend by the `endpoint` configuration or the environment variable `TENCENTCLOUD_ENDPOINT`. + +A typical endpoint looks like this: `http://cos-internal.{Region}.tencentcos.cn`. Both HTTP and HTTPS are accepted. + +Usage: + +```hcl +terraform { + backend "cos" { + region = "ap-guangzhou" + bucket = "bucket-for-terraform-state-1258798060" + prefix = "terraform/state" + endpoint = "http://cos-internal.ap-guangzhou.tencentcos.cn" + } +} +``` \ No newline at end of file diff --git a/website/docs/language/backend/gcs.mdx b/website/docs/language/backend/gcs.mdx new file mode 100644 index 0000000000..7647c8c098 --- /dev/null +++ b/website/docs/language/backend/gcs.mdx @@ -0,0 +1,136 @@ +--- +page_title: 'Backend Type: gcs' +description: >- + Terraform can store the state remotely, making it easier to version and work + with in a team. +--- + +# gcs + +Stores the state as an object in a configurable prefix in a pre-existing bucket on [Google Cloud Storage](https://cloud.google.com/storage/) (GCS). +The bucket must exist prior to configuring the backend. + +This backend supports [state locking](/terraform/language/state/locking). + +~> **Warning!** It is highly recommended that you enable +[Object Versioning](https://cloud.google.com/storage/docs/object-versioning) +on the GCS bucket to allow for state recovery in the case of accidental deletions and human error. + +## Example Configuration + +```hcl +terraform { + backend "gcs" { + bucket = "tf-state-prod" + prefix = "terraform/state" + } +} +``` + +## Data Source Configuration + +```hcl +data "terraform_remote_state" "foo" { + backend = "gcs" + config = { + bucket = "terraform-state" + prefix = "prod" + } +} + +# Terraform >= 0.12 +resource "local_file" "foo" { + content = data.terraform_remote_state.foo.outputs.greeting + filename = "${path.module}/outputs.txt" +} + +# Terraform <= 0.11 +resource "local_file" "foo" { + content = "${data.terraform_remote_state.foo.greeting}" + filename = "${path.module}/outputs.txt" +} +``` + +## Authentication + +IAM Changes to buckets are [eventually consistent](https://cloud.google.com/storage/docs/consistency#eventually_consistent_operations) and may take upto a few minutes to take effect. Terraform will return 403 errors till it is eventually consistent. + +### Running Terraform on your workstation. + +If you are using terraform on your workstation, you will need to install the Google Cloud SDK and authenticate using [User Application Default +Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default). + +User ADCs do [expire](https://developers.google.com/identity/protocols/oauth2#expiration) and you can refresh them by running `gcloud auth application-default login`. + +### Running Terraform on Google Cloud + +If you are running terraform on Google Cloud, you can configure that instance or cluster to use a [Google Service +Account](https://cloud.google.com/compute/docs/authentication). This will allow Terraform to authenticate to Google Cloud without having to bake in a separate +credential/authentication file. Make sure that the scope of the VM/Cluster is set to cloud-platform. + +### Running Terraform outside of Google Cloud + +If you are running terraform outside of Google Cloud, generate a service account key and set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to +the path of the service account key. Terraform will use that key for authentication. + +### Impersonating Service Accounts + +Terraform can impersonate a Google Service Account as described [here](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials). A valid credential must be provided as mentioned in the earlier section and that identity must have the `roles/iam.serviceAccountTokenCreator` role on the service account you are impersonating. + +## Encryption + +!> **Warning:** Take care of your encryption keys because state data encrypted with a lost or deleted key is not recoverable. If you use customer-supplied encryption keys, you must securely manage your keys and ensure you do not lose them. You must not delete customer-managed encryption keys in Cloud KMS used to encrypt state. However, if you accidentally delete a key, there is a time window where [you can recover it](https://cloud.google.com/kms/docs/destroy-restore#restore). + +### Customer-supplied encryption keys + +To get started, follow this guide: [Use customer-supplied encryption keys](https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys) + +If you want to remove customer-supplied keys from your backend configuration or change to a different customer-supplied key, Terraform cannot perform a state migration automatically and manual intervention is necessary instead. This intervention is necessary because Google does not store customer-supplied encryption keys, any requests sent to the Cloud Storage API must supply them instead (see [Customer-supplied Encryption Keys](https://cloud.google.com/storage/docs/encryption/customer-supplied-keys)). At the time of state migration, the backend configuration loses the old key's details and Terraform cannot use the key during the migration process. + +~> **Important:** To migrate your state away from using customer-supplied encryption keys or change the key used by your backend, you need to perform a [rewrite (gsutil CLI)](https://cloud.google.com/storage/docs/gsutil/commands/rewrite) or [cp (gcloud CLI)](https://cloud.google.com/sdk/gcloud/reference/storage/cp#--decryption-keys) operation to remove use of the old customer-supplied encryption key on your state file. Once you remove the encryption, you can successfully run `terraform init -migrate-state` with your new backend configuration. + +### Customer-managed encryption keys (Cloud KMS) + +To get started, follow this guide: [Use customer-managed encryption keys](https://cloud.google.com/storage/docs/encryption/using-customer-managed-keys) + +If you want to remove customer-managed keys from your backend configuration or change to a different customer-managed key, Terraform _can_ manage a state migration without manual intervention. This ability is because GCP stores customer-managed encryption keys and are accessible during the state migration process. However, these changes do not fully come into effect until the first write operation occurs on the state file after state migration occurs. In the first write operation after state migration, the file decrypts with the old key and then writes with the new encryption method. This method is equivalent to the [rewrite](https://cloud.google.com/storage/docs/gsutil/commands/rewrite) operation described in the customer-supplied encryption keys section. Because of the importance of the first write to state after state migration, you should not delete old KMS keys until any state file(s) encrypted with that key update. + +Customer-managed keys do not need to be sent in requests to read files from GCS buckets because decryption occurs automatically within GCS. This process means that if you use the `terraform_remote_state` [data source](/terraform/language/state/remote-state-data) to access KMS-encrypted state, you do not need to specify the KMS key in the data source's `config` object. + +~> **Important:** To use customer-managed encryption keys, you need to create a key and give your project's GCS service agent permission to use it with the Cloud KMS CryptoKey Encrypter/Decrypter predefined role. + +## Configuration Variables + +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform includes these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. + +The following configuration options are supported: + +- `bucket` - (Required) The name of the GCS bucket. This name must be + globally unique. For more information, see [Bucket Naming + Guidelines](https://cloud.google.com/storage/docs/bucketnaming.html#requirements). +- `credentials` / `GOOGLE_BACKEND_CREDENTIALS` / `GOOGLE_CREDENTIALS` - + (Optional) Local path to Google Cloud Platform account credentials in JSON + format. If unset, the path uses [Google Application Default Credentials](https://developers.google.com/identity/protocols/application-default-credentials). The provided credentials must have the Storage Object Admin role on the bucket. + **Warning**: if using the Google Cloud Platform provider as well, it will + also pick up the `GOOGLE_CREDENTIALS` environment variable. +- `impersonate_service_account` / `GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT` / `GOOGLE_IMPERSONATE_SERVICE_ACCOUNT` - (Optional) The service account to impersonate for accessing the State Bucket. + You must have `roles/iam.serviceAccountTokenCreator` role on that account for the impersonation to succeed. + If you are using a delegation chain, you can specify that using the `impersonate_service_account_delegates` field. +- `impersonate_service_account_delegates` - (Optional) The delegation chain for an impersonating a service account as described [here](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-delegated). +- `access_token` - (Optional) A temporary \[OAuth 2.0 access token] obtained + from the Google Authorization server, i.e. the `Authorization: Bearer` token + used to authenticate HTTP requests to GCP APIs. This is an alternative to + `credentials`. If both are specified, `access_token` will be used over the + `credentials` field. +- `prefix` - (Optional) GCS prefix inside the bucket. Named states for + workspaces are stored in an object called `/.tfstate`. +- `encryption_key` / `GOOGLE_ENCRYPTION_KEY` - (Optional) A 32 byte base64 + encoded 'customer-supplied encryption key' used when reading and writing state files in the bucket. For + more information see [Customer-supplied Encryption + Keys](https://cloud.google.com/storage/docs/encryption/customer-supplied-keys). +- `kms_encryption_key` / `GOOGLE_KMS_ENCRYPTION_KEY` - (Optional) A Cloud KMS key ('customer-managed encryption key') + used when reading and writing state files in the bucket. + Format should be `projects/{{project}}/locations/{{location}}/keyRings/{{keyRing}}/cryptoKeys/{{name}}`. + For more information, including IAM requirements, see [Customer-managed Encryption + Keys](https://cloud.google.com/storage/docs/encryption/customer-managed-keys). +- `storage_custom_endpoint` / `GOOGLE_BACKEND_STORAGE_CUSTOM_ENDPOINT` / `GOOGLE_STORAGE_CUSTOM_ENDPOINT` - (Optional) A URL containing three parts: the protocol, the DNS name pointing to a Private Service Connect endpoint, and the path for the Cloud Storage API (`/storage/v1/b`, [see here](https://cloud.google.com/storage/docs/json_api/v1/buckets/get#http-request)). You can either use [a DNS name automatically made by the Service Directory](https://cloud.google.com/vpc/docs/configure-private-service-connect-apis#configure-p-dns) or a [custom DNS name](https://cloud.google.com/vpc/docs/configure-private-service-connect-apis#configure-dns-default) made by you. For example, if you create an endpoint called `xyz` and want to use the automatically-created DNS name, you should set the field value as `https://storage-xyz.p.googleapis.com/storage/v1/b`. For help creating a Private Service Connect endpoint using Terraform, [see this guide](https://cloud.google.com/vpc/docs/configure-private-service-connect-apis#terraform_1). diff --git a/website/docs/language/settings/backends/http.mdx b/website/docs/language/backend/http.mdx similarity index 78% rename from website/docs/language/settings/backends/http.mdx rename to website/docs/language/backend/http.mdx index aed01a9db2..787f669cca 100644 --- a/website/docs/language/settings/backends/http.mdx +++ b/website/docs/language/backend/http.mdx @@ -9,7 +9,7 @@ Stores the state using a simple [REST](https://en.wikipedia.org/wiki/Representat State will be fetched via GET, updated via POST, and purged with DELETE. The method used for updating is configurable. -This backend optionally supports [state locking](/language/state/locking). When locking +This backend optionally supports [state locking](/terraform/language/state/locking). When locking support is enabled it will use LOCK and UNLOCK requests providing the lock info in the body. The endpoint should return a 423: Locked or 409: Conflict with the holding lock info when it's already taken, 200: OK for success. Any other status will be considered an error. The ID of the holding lock @@ -40,7 +40,7 @@ data "terraform_remote_state" "foo" { ## Configuration Variables -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. The following configuration options / environment variables are supported: @@ -67,3 +67,9 @@ The following configuration options / environment variables are supported: seconds to wait between HTTP request attempts. Defaults to `1`. - `retry_wait_max` / `TF_HTTP_RETRY_WAIT_MAX` – (Optional) The maximum time in seconds to wait between HTTP request attempts. Defaults to `30`. + +For mTLS authentication, the following three options may be set: + + - `client_certificate_pem` / `TF_HTTP_CLIENT_CERTIFICATE_PEM` - (Optional) A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication. + - `client_private_key_pem` /`TF_HTTP_CLIENT_PRIVATE_KEY_PEM` - (Optional) A PEM-encoded private key, required if client_certificate_pem is specified. + - `client_ca_certificate_pem` / `TF_HTTP_CLIENT_CA_CERTIFICATE_PEM` - (Optional) A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication. diff --git a/website/docs/language/settings/backends/configuration.mdx b/website/docs/language/backend/index.mdx similarity index 53% rename from website/docs/language/settings/backends/configuration.mdx rename to website/docs/language/backend/index.mdx index 83c850a819..b77d670674 100644 --- a/website/docs/language/settings/backends/configuration.mdx +++ b/website/docs/language/backend/index.mdx @@ -1,27 +1,20 @@ --- -page_title: Backend Configuration - Configuration Language +page_title: Backend block configuration overview +description: >- + Use the `backend` block to control where Terraform stores state. Learn about the available state backends, the backend block, initializing backends, partial backend configuration, changing backend configuration, and unconfiguring a backend. --- -# Backend Configuration +# Backend block configuration overview -A backend defines where Terraform stores its [state](/language/state) data files. +This topic provides an overview of how to configure the `backend` block in your Terraform configuration. The `backend` defines where Terraform stores its [state](/terraform/language/state) data files. -Terraform uses persisted state data to keep track of the resources it manages. Most non-trivial Terraform configurations either [integrate with Terraform Cloud](/language/settings/terraform-cloud) or use a backend to store state remotely. This lets multiple people access the state data and work together on that collection of infrastructure resources. +## Overview -This page describes how to configure a backend by adding the [`backend` block](#using-a-backend-block) to your configuration. +Terraform uses persisted state data to keep track of the resources it manages. You can either [integrate with HCP Terraform](/terraform/language/terraform#terraform-cloud) to store state data or define a `backend` block to store state in a remote object. This lets multiple people access the state data and work together on that collection of infrastructure resources. --> **Note:** In Terraform versions before 1.1.0, we classified backends as standard or enhanced. The enhanced label differentiated the [`remote` backend](/language/settings/backends/remote), which could both store state and perform Terraform operations. This classification has been removed. Refer to [Using Terraform Cloud](/cli/cloud) for details about storing state, executing remote operations, and using Terraform Cloud directly from Terraform. +## Define a `backend` block -## Available Backends - -By default, Terraform uses a backend called [`local`](/language/settings/backends/local), which stores state as a local file on disk. You can also configure one of the built-in backends listed in the documentation sidebar. - -Some of these backends act like plain remote disks for state files, while others support locking the state while operations are being performed. This helps prevent conflicts and inconsistencies. The built-in backends listed are the only backends. You cannot load additional backends as plugins. - -## Using a Backend Block - -You do not need to configure a backend when using Terraform Cloud because -Terraform Cloud automatically manages state in the workspaces associated with your configuration. If your configuration includes a [`cloud` block](/language/settings/terraform-cloud), it cannot include a `backend` block. +Do not configure a backend when connecting your configuration to workspaces in HCP Terraform or Terraform Enterprise. These systems automatically manage state in the workspaces associated with your configuration. If your configuration includes a [`cloud` block](/terraform/language/terraform#terraform-cloud), it cannot include a `backend` block. To configure a backend, add a nested `backend` block within the top-level `terraform` block. The following example configures the `remote` backend. @@ -42,8 +35,36 @@ There are some important limitations on backend configuration: - A configuration can only provide one backend block. - A backend block cannot refer to named values (like input variables, locals, or data source attributes). +- You cannot reference values declared within backend blocks elsewhere in the configuration. Refer to [References to Resource Attributes](/terraform/language/expressions/references#references-to-resource-attributes) for more details. -### Credentials and Sensitive Data +### Default backend + +Terraform uses a backend called [`local`](/terraform/language/backend/local) by default. The `local` backend type stores state as a local file on disk. + +### Backend types + +Terraform ships with several built-in backend types. Some backends function as remote disks for state files, while others support locking the state during Terraform operations to prevent conflicts and inconsistencies. You cannot load additional backends as plugins. + +Specify the backend type as the `backend` block label. The following example instructs Terraform to use a remote backend: + +```hcl +backend "remote" { + organization = "example_corp" + . . . +} +``` + +The specified backend must be available in the version of Terraform you are using. + +### Backend arguments + +The arguments in the `backend` block body are specific to the backend type. They specify where and how the backend stores configuration state. Some backend types allow you to configure additional behaviors. Refer to the documentation for your backend for additional information. + +Some backends allow you to provide access credentials as part of the configuration, but we do not recommend including access credentials directly in the configuration. Instead, leave credential-related arguments unset and provide them using the credentials files or environment variables that are conventional for the target system. + +Refer to the page for each backend type for full details and that type's configuration arguments. + +### Credentials and sensitive data Backends store state in a remote service, which allows multiple people to access it. Accessing remote state generally requires access credentials, since state data contains extremely sensitive information. @@ -55,36 +76,24 @@ Terraform writes the backend configuration in plain text in two separate files. When applying a plan that you previously saved to a file, Terraform uses the backend configuration stored in that file instead of the current backend settings. If that configuration contains time-limited credentials, they may expire before you finish applying the plan. Use environment variables to pass credentials when you need to use different values between the plan and apply steps. -### Backend Types +## Initialize the backend -The block label of the backend block (`"remote"`, in the example above) indicates which backend type to use. Terraform has a built-in selection of backends, and the configured backend must be available in the version of Terraform you are using. - -The arguments used in the block's body are specific to the chosen backend type; they configure where and how the backend will store the configuration's state, and in some cases configure other behavior. - -Some backends allow providing access credentials directly as part of the configuration for use in unusual situations, for pragmatic reasons. However, in normal use we _do not_ recommend including access credentials as part of the backend configuration. Instead, leave those arguments completely unset and provide credentials via the credentials files or environment variables that are conventional for the target system, as described in the documentation for each backend. - -Refer to the list of backend types in the navigation sidebar for details about each supported backend type and its configuration arguments. - -### Default Backend - -If a configuration includes no backend block, Terraform defaults to using the `local` backend, which stores state as a plain file in the current working directory. - -## Initialization - -Whenever a configuration's backend changes, you must run `terraform init` again +When you change a backend's configuration, you must run `terraform init` again to validate and configure the backend before you can perform any plans, applies, or state operations. -When changing backends, Terraform will give you the option to migrate +After you initialize, Terraform creates a `.terraform/` directory locally. This directory contains the most recent backend configuration, including any authentication parameters you provided to the Terraform CLI. Do not check this directory into Git, as it may contain sensitive credentials for your remote backend. + +The local backend configuration is different and entirely separate from the `terraform.tfstate` file that contains [state data](/terraform/language/state) about your real-world infrastructure. Terraform stores the `terraform.tfstate` file in your remote backend. + +When you change backends, Terraform gives you the option to migrate your state to the new backend. This lets you adopt backends without losing any existing state. -To be extra careful, we always recommend manually backing up your state -as well. You can do this by simply copying your `terraform.tfstate` file -to another location. The initialization process should create a backup -as well, but it never hurts to be safe! +~> **Important:** Before migrating to a new backend, we strongly recommend manually backing up your state by copying your `terraform.tfstate` file +to another location. -## Partial Configuration +## Partial configuration You do not need to specify every required argument in the backend configuration. Omitting certain arguments may be desirable if some arguments are provided @@ -92,13 +101,38 @@ automatically by an automation script running Terraform. When some or all of the arguments are omitted, we call this a _partial configuration_. With a partial configuration, the remaining configuration arguments must be -provided as part of [the initialization process](/cli/init). +provided as part of [the initialization process](/terraform/cli/init). There are several ways to supply the remaining arguments: - **File**: A configuration file may be specified via the `init` command line. To specify a file, use the `-backend-config=PATH` option when running - `terraform init`. If the file contains secrets it may be kept in + `terraform init`. The partial configuration must have a `backend` block containing keys set to empty values. When you run the `terraform init -backend-config=""` command, Terraform populates the keys in the partial `backend` configuration with matching key values from the specified configuration file. In the following example, the keys defined in the `backend` block of the `state.tf` configuration file are populated with values from the `state.config` configuration: + ``` + $ `terraform init -backend-config="./state.config"` + ``` + + ```hcl + # state.tf + terraform { + backend "s3" { + bucket = "" + key = "" + region = "" + profile= "" + } + } + ``` + + ```hcl + # state.config + bucket = "your-bucket" + key = "your-state.tfstate" + region = "eu-central-1" + profile= "Your_Profile" + ``` + + When your configuration file contains secrets, you can store them in a secure data store, such as [Vault](https://www.vaultproject.io/), in which case it must be downloaded to the local disk before running Terraform. @@ -168,31 +202,25 @@ or `CONSUL_HTTP_AUTH` environment variables. See the documentation of your chosen backend to learn how to provide credentials to it outside of its main configuration. -## Changing Configuration +## Change configuration You can change your backend configuration at any time. You can change both the configuration itself as well as the type of backend (for example from "consul" to "s3"). Terraform will automatically detect any changes in your configuration -and request a [reinitialization](/cli/init). As part of +and request a [reinitialization](/terraform/cli/init). As part of the reinitialization process, Terraform will ask if you'd like to migrate your existing state to the new configuration. This allows you to easily switch from one backend to another. -If you're using multiple [workspaces](/language/state/workspaces), +If you're using multiple [workspaces](/terraform/language/state/workspaces), Terraform can copy all workspaces to the destination. If Terraform detects you have multiple workspaces, it will ask if this is what you want to do. If you're just reconfiguring the same backend, Terraform will still ask if you want to migrate your state. You can respond "no" in this scenario. -## Unconfiguring a Backend +## Remove a backend configuration -If you no longer want to use any backend, you can simply remove the -configuration from the file. Terraform will detect this like any other -change and prompt you to [reinitialize](/cli/init). - -As part of the reinitialization, Terraform will ask if you'd like to migrate -your state back down to normal local state. Once this is complete then -Terraform is back to behaving as it does by default. +Remove the `backend` block from your configuration and [reinitialize](/terraform/cli/init) the directory when prompted. Terraform also prompts you to migrate the state to the default `local` backend. diff --git a/website/docs/language/settings/backends/kubernetes.mdx b/website/docs/language/backend/kubernetes.mdx similarity index 87% rename from website/docs/language/settings/backends/kubernetes.mdx rename to website/docs/language/backend/kubernetes.mdx index 1dd16537f3..937c461a75 100644 --- a/website/docs/language/settings/backends/kubernetes.mdx +++ b/website/docs/language/backend/kubernetes.mdx @@ -5,11 +5,9 @@ description: Terraform can store state remotely in Kubernetes and lock that stat # kubernetes --> **Note:** This backend is limited by Kubernetes' maximum Secret size of 1MB. See [Secret restrictions](https://kubernetes.io/docs/concepts/configuration/secret/#restrictions) for details. - Stores the state in a [Kubernetes secret](https://kubernetes.io/docs/concepts/configuration/secret/). -This backend supports [state locking](/language/state/locking), with locking done using a Lease resource. +This backend supports [state locking](/terraform/language/state/locking), with locking done using a Lease resource. ## Example Configuration @@ -30,7 +28,7 @@ If the `in_cluster_config` flag is set the backend will attempt to use a [servic For most use cases either `in_cluster_config`, `config_path`, or `config_paths` will need to be set. If all flags are set the configuration at `config_path` will be used. -Note that for the access credentials we recommend using a [partial configuration](/language/settings/backends/configuration#partial-configuration). +Note that for the access credentials we recommend using a [partial configuration](/terraform/language/backend#partial-configuration). ## Example Referencing @@ -46,11 +44,11 @@ data "terraform_remote_state" "foo" { ## Configuration Variables -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. The following configuration options are supported: -* `secret_suffix` - (Required) Suffix used when creating secrets. Secrets will be named in the format: `tfstate-{workspace}-{secret_suffix}`. +* `secret_suffix` - (Required) Suffix used when creating the secret. The secret will be named in the format: `tfstate-{workspace}-{secret_suffix}`. Note that the backend may append its own numeric index to the secret name when chunking large state files into multiple secrets. In this case, there will be multiple secrets named in the format: `tfstate-{workspace}-{secret_suffix}-{index}`. * `labels` - (Optional) Map of additional labels to be applied to the secret and lease. * `namespace` - (Optional) Namespace to store the secret and lease in. Can be sourced from `KUBE_NAMESPACE`. * `in_cluster_config` - (Optional) Used to authenticate to the cluster from inside a pod. Can be sourced from `KUBE_IN_CLUSTER_CONFIG`. diff --git a/website/docs/language/settings/backends/local.mdx b/website/docs/language/backend/local.mdx similarity index 95% rename from website/docs/language/settings/backends/local.mdx rename to website/docs/language/backend/local.mdx index d2f5d18a0a..26fdfa6bd8 100644 --- a/website/docs/language/settings/backends/local.mdx +++ b/website/docs/language/backend/local.mdx @@ -77,7 +77,7 @@ the three arguments would typically all be paths within a temporary directory used just for one operation. Because these old workflows predate the introduction of the possibility of -[multiple workspaces](/language/state/workspaces), setting them +[multiple workspaces](/terraform/language/state/workspaces), setting them overrides Terraform's usual behavior of selecting a different state filename based on the selected workspace. If you use all three of these options then the selected workspace has no effect on which filenames Terraform will select @@ -89,7 +89,7 @@ backend type selected. We do not recommend using these options in new systems, even if you are running Terraform in automation. Instead, -[select a different backend which supports remote state](/language/settings/backends) and configure it +[select a different backend which supports remote state](/terraform/language/backend) and configure it within your root module, which ensures that everyone working on your configuration will automatically retrieve and store state in the correct shared location without any special command line options. diff --git a/website/docs/language/backend/oci.mdx b/website/docs/language/backend/oci.mdx new file mode 100644 index 0000000000..2996c07c88 --- /dev/null +++ b/website/docs/language/backend/oci.mdx @@ -0,0 +1,200 @@ +--- +page_title: 'Backend Type: oci' +description: Terraform can store and lock state remotely in OCI object storage. +--- + +# oci + +The oci backend stores the Terraform state file in [Oracle Cloud Infrastructure (OCI) Object Storage](https://docs.oracle.com/en-us/iaas/Content/Object/Concepts/objectstorageoverview.htm#overview), allowing multiple users to collaborate using a shared remote state and benefit from features such as [state locking](https://developer.hashicorp.com/terraform/language/v1.12.x/state/locking) and [workspaces](https://developer.hashicorp.com/terraform/language/v1.12.x/state/workspaces). + +> ⚠️ **Warning:** It is highly recommended that you enable +> [Bucket Versioning](https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingversioning_topic-To_enable_versioning_during_bucket_creation.htm) +> on the object storage bucket to allow for state recovery in the case of accidental deletions and human error. + +## Example Configuration + +```hcl +terraform { + backend "oci" { + # Required + bucket = "mybucket" + namespace = "my-namespace" + # Optional + tenancy_ocid = "ocid1.tenancy.oc1..xxxxxxx" + user_ocid = "ocid1.user.oc1..xxxxxxxx" + fingerprint = "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx" + private_key_path = "~/.oci/oci_api_key.pem" + region = "us-ashburn-1" + key = "path/to/my/key" + workspace_key_prefix = "envs/" + kms_key_id = "ocid1.key.oc1.iad.xxxxxxxxxxxxxx" + auth = "APIKey" + config_file_profile = "DEFAULT" + } +} +``` + +This assumes we have a bucket created called `mybucket`. The Terraform state is written to the key `path/to/my/key`. + +Note that for the access credentials we recommend using a [partial configuration](/terraform/language/backend#partial-configuration). + +## State Storage + +The oci backend stores Terraform state files in Oracle Cloud Infrastructure (OCI) Object Storage at the path defined by the `key` parameter, inside the bucket specified by the `bucket` parameter. + +Using the example shown above, the state file would be stored at: +`path/to/my/key` in the bucket `mybucket`. + +When using Terraform workspaces, the state for the default workspace is stored exactly at the path described above. +For non-default workspaces, the state is stored using the following path format: + `//` + + - The default workspace_key_prefix is `tf-state-env`, but it can be customized using the `workspace_key_prefix` backend parameter. + + - For example, with workspace `development`, the state would be stored at: `tf-state-env/development/path/to/my/key` + +## State Locking +- The oci backend supports state locking by leveraging the `If-None-Match: *` header capability of OCI Object Storage. +- When a user initiates a `terraform plan/apply/destroy`, the backend creates a lock object in the same bucket as the state file. +- For example, the lock file for the `development` workspace would be stored at: `tf-state-env/development/path/to/my/key.lock` + +This locking mechanism helps prevent concurrent operations on the same state file, reducing the risk of corruption or conflicting changes. + +## Permissions Required + +### OCI Object Storage Bucket Permissions +Terraform requires the following [OCI IAM permissions](https://docs.oracle.com/en-us/iaas/Content/Identity/Reference/policyreference.htm) on the target backend bucket to manage the state file and associated lock files: + +- OBJECT_INSPECT (includes HeadObject) +- OBJECT_CREATE (includes PutObject, CreateMultipartUpload, UploadPart, and CommitMultipartUpload) +- OBJECT_DELETE (includes DeleteObject) +- OBJECT_READ (includes GetObject) + +Alternatively, you can specify the exact operations if you're using fine-grained policies: + +- GetObject +- PutObject +- DeleteObject +- HeadObject +- CreateMultipartUpload +- UploadPart +- CommitMultipart + +Note: These permissions should be granted on the specific bucket being used for Terraform state storage. +### OCI IAM Policy Reference +> [OCI IAM Policy Reference](https://docs.oracle.com/en-us/iaas/Content/Identity/Reference/policyreference.htm) This resource provides comprehensive details on the required permissions and how to structure your policies to grant Terraform the appropriate access to Object Storage resources. + +## Data Source Configuration + +To make use of the oci remote state in another configuration, +use the [`terraform_remote_state` data source](/terraform/language/state/remote-state-data). + +```hcl +data "terraform_remote_state" "mystate" { + backend = "oci" + config = { + bucket = "mybucket" + key = "path/to/my/key" + namespace = "my-namespace" + tenancy_ocid = "ocid1.tenancy.oc1..xxxxxxx" + user_ocid = "ocid1.user.oc1..xxxxxxxx" + fingerprint = "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx" + private_key_path = "~/.oci/oci_api_key.pem" + region = "us-ashburn-1" + } +} +``` + +The `terraform_remote_state` data source will return all of the root module +outputs defined in the referenced remote state (but not any outputs from +nested modules unless they are explicitly output again in the root). An +example output might look like: + +``` +# data.terraform_remote_state.mystate: +data "terraform_remote_state" "mystate" { + backend = "oci" + config = { + bucket = "mybucket" + key = "path/to/my/key" + namespace = "my-namespace" + tenancy_ocid = "ocid1.tenancy.oc1..xxxxxxx" + user_ocid = "ocid1.user.oc1..xxxxxxxx" + fingerprint = "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx" + private_key_path = "~/.oci/oci_api_key.pem" + region = "us-ashburn-1" + } +} +``` + +## Configuration + +The oci backend requires the configuration of the OCI namespace and Object Storage bucket where the Terraform state file will be stored. +### Credentials and Shared Configuration + +> ⚠️ **Warning:** +> We strongly recommend using **environment variables** to supply credentials and other sensitive data. +> Avoid hardcoding these values directly in your configuration or using the `-backend-config` flag with plaintext secrets. +> +> Sensitive values passed this way may be saved in: +> - The `.terraform` subdirectory +> - Terraform plan files (`*.tfplan`) +> +> For more guidance, refer to the [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) documentation. + +## Attribute reference + +### Required Properties + +| Name | Description | +|-----------|-----------------------------------------------------------------------------| +| `bucket` | The name of the OCI Object Storage bucket where the state file will be stored. | +| `namespace` | The namespace of the OCI Object Storage. | + +### Optional Properties + +| Name | Description | +|------------------------|-------------------------------------------------------------------------------------------------| +| `key` | The name of the state file stored in the backend. Defaults to `terraform.tfstate`. | +| `tenancy_ocid` | The OCID of the tenancy. Required for API key authentication. | +| `user_ocid` | The OCID of the user. Required for API key authentication. | +| `fingerprint` | The fingerprint of the user's API key. Required for API key authentication. | +| `private_key_path` | Path to the private key file for API authentication. Required for API key authentication. | +| `region` | The OCI region where the bucket is located. Required for most configurations. | +| `kms_key_id` | The OCID of a master encryption key used for encrypting the state file. | +| `workspace_key_prefix` | A prefix applied to the non-default state path inside the bucket. Defaults to `tf-state-env`. | +| `auth` | Authentication method (e.g., `APIKey`, `InstancePrincipal`, `ResourcePrincipal`, `InstancePrincipalWithCerts`, `SecurityToken`,`OKEWorkloadIdentity`). | +| `config_file_profile` | Profile name from the OCI config file (default `~/.oci/config`). | +| `sse_customer_key` | The customer-managed encryption key used for server-side encryption. | +| `sse_customer_key_sha256` | The SHA256 hash of the customer-managed encryption key. | +| `sse_customer_algorithm` | The algorithm used for server-side encryption. Supported values: `AES256` | + +### 🔧 Setting Backend Configuration via Environment Variables +You can configure the OCI Terraform backend using environment variables. This provides flexibility and simplifies automation workflows. + +For any backend attribute (e.g., region), you can set its value using one of the following environment variable formats: + +#### 🧩 Supported Environment Variable Prefixes (in order of priority): +1. `OCI_` – OCI SDK-compatible environment variable. Can also be used for Terraform Provider OCI credentials. + ```bash + export OCI_region=us-ashburn-1 + ``` +2. No prefix – Generic (lowest priority) + ```bash + export region=us-ashburn-1 + ``` +Terraform will resolve the attribute value using the first match it finds based on the above priority order. + +### Custom encryption key + +To use a custom encryption key, you can generate a new key and its SHA256 hash using the following commands: + +1. Generate a 256-bit (32-byte) base64-encoded AES key: +```bash + openssl rand -base64 32 +``` +2. Generate the SHA-256 hash of the decoded key: +```bash + generate using command `echo -n | base64 -d | openssl dgst -sha256 -binary | base64` +``` +Replace with the value generated in step 1. \ No newline at end of file diff --git a/website/docs/language/settings/backends/oss.mdx b/website/docs/language/backend/oss.mdx similarity index 59% rename from website/docs/language/settings/backends/oss.mdx rename to website/docs/language/backend/oss.mdx index 1fbd9c7f30..94740e9d7e 100644 --- a/website/docs/language/settings/backends/oss.mdx +++ b/website/docs/language/backend/oss.mdx @@ -11,7 +11,7 @@ This backend also supports state locking and consistency checking via [Alibaba Cloud Table Store](https://www.alibabacloud.com/help/doc-detail/27280.htm), which can be enabled by setting the `tablestore_table` field to an existing TableStore table name. -This backend supports [state locking](/language/state/locking) via TableStore. +This backend supports [state locking](/terraform/language/state/locking) via TableStore. -> **Note:** The OSS backend is available from terraform version 0.12.2. @@ -39,7 +39,7 @@ Terraform state will be written into the file `path/mystate/version-1.tfstate`. To make use of the OSS remote state in another configuration, use the [`terraform_remote_state` data -source](/language/state/remote-state-data). +source](/terraform/language/state/remote-state-data). ```hcl terraform { @@ -55,7 +55,7 @@ terraform { The `terraform_remote_state` data source will return all of the root outputs defined in the referenced remote state, an example output might look like: -``` +```hcl data "terraform_remote_state" "network" { backend = "oss" config = { @@ -71,21 +71,21 @@ data "terraform_remote_state" "network" { ## Configuration Variables -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. The following configuration options or environment variables are supported: -* `access_key` - (Optional) Alibaba Cloud access key. It supports environment variables `ALICLOUD_ACCESS_KEY` and `ALICLOUD_ACCESS_KEY_ID`. +* `access_key` - (Optional) Alibaba Cloud access key. It supports environment variables `ALICLOUD_ACCESS_KEY` and `ALIBABA_CLOUD_ACCESS_KEY_ID`(Recommended). -* `secret_key` - (Optional) Alibaba Cloud secret access key. It supports environment variables `ALICLOUD_SECRET_KEY` and `ALICLOUD_ACCESS_KEY_SECRET`. +* `secret_key` - (Optional) Alibaba Cloud secret access key. It supports environment variables `ALICLOUD_SECRET_KEY` and `ALIBABA_CLOUD_ACCESS_KEY_SECRET`(Recommended). -* `security_token` - (Optional) STS access token. It supports environment variable `ALICLOUD_SECURITY_TOKEN`. +* `security_token` - (Optional) STS access token. It supports environment variable `ALICLOUD_SECURITY_TOKEN` and `ALIBABA_CLOUD_SECURITY_TOKEN`(Recommended). -* `ecs_role_name` - (Optional, Available in 0.12.14+) The RAM Role Name attached on a ECS instance for API operations. You can retrieve this from the 'Access Control' section of the Alibaba Cloud console. +* `ecs_role_name` - (Optional) The RAM Role Name attached on a ECS instance for API operations. You can retrieve this from the 'Access Control' section of the Alibaba Cloud console. -* `region` - (Optional) The region of the OSS bucket. It supports environment variables `ALICLOUD_REGION` and `ALICLOUD_DEFAULT_REGION`. +* `region` - (Optional) The region of the OSS bucket. It supports environment variables `ALICLOUD_REGION` and `ALIBABA_CLOUD_REGION`(Recommended). -* `endpoint` - (Optional) A custom endpoint for the OSS API. It supports environment variables `ALICLOUD_OSS_ENDPOINT` and `OSS_ENDPOINT`. +* `endpoint` - (Optional) A custom endpoint for the OSS API. It supports environment variables `ALICLOUD_OSS_ENDPOINT` and `ALIBABA_CLOUD_OSS_ENDPOINT`(Recommended). * `bucket` - (Required) The name of the OSS bucket. @@ -93,11 +93,14 @@ The following configuration options or environment variables are supported: * `key` - (Optional) The name of the state file. Defaults to `terraform.tfstate`. -* `tablestore_endpoint` / `ALICLOUD_TABLESTORE_ENDPOINT` - (Optional) A custom endpoint for the TableStore API. +* `tablestore_endpoint` - (Optional) A custom endpoint for the TableStore API. It supports environment variables `ALICLOUD_TABLESTORE_ENDPOINT` and `ALIBABA_CLOUD_TABLESTORE_ENDPOINT`(Recommended). + +* `tablestore_instance_name` - (Optional) Specifies the name of an instance that `TableStore` belongs to. By default, Terraform parses the name from `tablestore_endpoint`. + You should set the access URL explicitly when the `tablestore` endpoint is a VPC access URL. * `tablestore_table` - (Optional) A TableStore table for state locking and consistency. The table must have a primary key named `LockID` of type `String`. -* `sts_endpoint` - (Optional, Available in 1.0.11+) Custom endpoint for the AliCloud Security Token Service (STS) API. It supports environment variable `ALICLOUD_STS_ENDPOINT`. +* `sts_endpoint` - (Optional) Custom endpoint for the AliCloud Security Token Service (STS) API. It supports environment variable `ALICLOUD_STS_ENDPOINT` and `ALIBABA_CLOUD_STS_ENDPOINT`(Recommended). * `encrypt` - (Optional) Whether to enable server side encryption of the state file. If it is true, OSS will use 'AES256' encryption algorithm to encrypt state file. @@ -106,24 +109,24 @@ The following configuration options or environment variables are supported: ACL](https://www.alibabacloud.com/help/doc-detail/52284.htm) to be applied to the state file. -* `shared_credentials_file` - (Optional, Available in 0.12.8+) This is the path to the shared credentials file. It can also be sourced from the `ALICLOUD_SHARED_CREDENTIALS_FILE` environment variable. If this is not set and a profile is specified, `~/.aliyun/config.json` will be used. +* `shared_credentials_file` - (Optional) This is the path to the shared credentials file. It can also be sourced from the `ALICLOUD_SHARED_CREDENTIALS_FILE` or `ALIBABA_CLOUD_CREDENTIALS_FILE`(Recommended) environment variable. If this is not set and a profile is specified, `~/.aliyun/config.json` will be used. -* `profile` - (Optional, Available in 0.12.8+) This is the Alibaba Cloud profile name as set in the shared credentials file. It can also be sourced from the `ALICLOUD_PROFILE` environment variable. +* `profile` - (Optional) This is the Alibaba Cloud profile name as set in the shared credentials file. It supports environment variable `ALICLOUD_PROFILE` and `ALIBABA_CLOUD_PROFILE`(Recommended). -* `assume_role_role_arn` - (Optional, Available in 1.1.0+) The ARN of the role to assume. If ARN is set to an empty string, it does not perform role switching. It supports the environment variable `ALICLOUD_ASSUME_ROLE_ARN`. +* `assume_role_role_arn` - (Optional) The ARN of the role to assume. If ARN is set to an empty string, it does not perform role switching. It supports the environment variable `ALICLOUD_ASSUME_ROLE_ARN` and `ALIBABA_CLOUD_ROLE_ARN`(Recommended). Terraform executes configuration on account with provided credentials. -* `assume_role_policy` - (Optional, Available in 1.1.0+) A more restrictive policy to apply to the temporary credentials. This gives you a way to further restrict the permissions for the resulting temporary security credentials. You cannot use this policy to grant permissions that exceed those of the role that is being assumed. +* `assume_role_policy` - (Optional) A more restrictive policy to apply to the temporary credentials. This gives you a way to further restrict the permissions for the resulting temporary security credentials. You cannot use this policy to grant permissions that exceed those of the role that is being assumed. -* `assume_role_session_name` - (Optional, Available in 1.1.0+) The session name to use when assuming the role. If omitted, 'terraform' is passed to the AssumeRole call as session name. It supports environment variable `ALICLOUD_ASSUME_ROLE_SESSION_NAME`. +* `assume_role_session_name` - (Optional) The session name to use when assuming the role. If omitted, 'terraform' is passed to the AssumeRole call as session name. It supports environment variable `ALICLOUD_ASSUME_ROLE_SESSION_NAME` and `ALIBABA_CLOUD_ROLE_SESSION_NAME`(Recommended). -* `assume_role_session_expiration` - (Optional, Available in 1.1.0+) The time after which the established session for assuming role expires. Valid value range: \[900-3600] seconds. Default to 3600 (in this case Alibaba Cloud uses its own default value). It supports environment variable `ALICLOUD_ASSUME_ROLE_SESSION_EXPIRATION`. +* `assume_role_session_expiration` - (Optional) The time after which the established session for assuming role expires. Valid value range: \[900-3600] seconds. Default to 3600 (in this case Alibaba Cloud uses its own default value). It supports environment variable `ALICLOUD_ASSUME_ROLE_SESSION_EXPIRATION`. -* `assume_role` - (**Deprecated as of 1.1.0+**, Available in 0.12.6+) If provided with a role ARN, will attempt to assume this role using the supplied credentials. It will be ignored when `assume_role_role_arn` is specified. +* `assume_role` - (**Deprecated as of 1.1.0+**) If provided with a role ARN, will attempt to assume this role using the supplied credentials. It will be ignored when `assume_role_role_arn` is specified. **Deprecated in favor of flattening assume_role_\* options** - * `role_arn` - (Required) The ARN of the role to assume. If ARN is set to an empty string, it does not perform role switching. It supports the environment variable `ALICLOUD_ASSUME_ROLE_ARN`. + * `role_arn` - (Required) The ARN of the role to assume. If ARN is set to an empty string, it does not perform role switching. It supports the environment variable `ALICLOUD_ASSUME_ROLE_ARN` and `ALIBABA_CLOUD_ROLE_ARN`(Recommended). Terraform executes configuration on account with provided credentials. * `policy` - (Optional) A more restrictive policy to apply to the temporary credentials. This gives you a way to further restrict the permissions for the resulting temporary security credentials. You cannot use this policy to grant permissions that exceed those of the role that is being assumed. diff --git a/website/docs/language/backend/pg.mdx b/website/docs/language/backend/pg.mdx new file mode 100644 index 0000000000..fa231b962b --- /dev/null +++ b/website/docs/language/backend/pg.mdx @@ -0,0 +1,107 @@ +--- +page_title: 'Backend Type: pg' +description: Terraform can store state remotely in a Postgres database with locking. +--- + +# pg + +Stores the state in a [Postgres database](https://www.postgresql.org) version 10 or newer. + +This backend supports [state locking](/terraform/language/state/locking). + +## Example Configuration + +```hcl +terraform { + backend "pg" { + conn_str = "postgres://user:pass@db.example.com/terraform_backend" + } +} +``` + +Before initializing the backend with `terraform init`, the database must already exist: + +``` +createdb terraform_backend +``` + +This `createdb` command is found in [Postgres client applications](https://www.postgresql.org/docs/10/reference-client.html) which are installed along with the database server. + + +### Using environment variables + +We recommend using environment variables to configure the `pg` backend in order +not to have sensitive credentials written to disk and committed to source +control. + +The `pg` backend supports the standard [`libpq` environment variables](https://www.postgresql.org/docs/current/libpq-envars.html). + +The backend can be configured either by giving the whole configuration as an +environment variable: + +```hcl +terraform { + backend "pg" {} +} +``` + +```shellsession +$ export PG_CONN_STR=postgres://user:pass@db.example.com/terraform_backend +$ terraform init +``` + +or just the sensitive parameters: + +```hcl +terraform { + backend "pg" { + conn_str = "postgres://db.example.com/terraform_backend" + } +} +``` + +```shellsession +$ export PGUSER=user +$ read -s PGPASSWORD +$ export PGPASSWORD +$ terraform init +``` + +## Data Source Configuration + +To make use of the pg remote state in another configuration, use the [`terraform_remote_state` data source](/terraform/language/state/remote-state-data). + +```hcl +data "terraform_remote_state" "network" { + backend = "pg" + config = { + conn_str = "postgres://localhost/terraform_backend" + } +} +``` + +## Configuration Variables + +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. + +The following configuration options or environment variables are supported: + +- `conn_str` - Postgres connection string; a `postgres://` URL. The `PG_CONN_STR` and [standard `libpq`](https://www.postgresql.org/docs/current/libpq-envars.html) environment variables can also be used to indicate how to connect to the PostgreSQL database. +- `schema_name` - Name of the automatically-managed Postgres schema, default to `terraform_remote_state`. Can also be set using the `PG_SCHEMA_NAME` environment variable. +- `skip_schema_creation` - If set to `true`, the Postgres schema must already exist. Can also be set using the `PG_SKIP_SCHEMA_CREATION` environment variable. Terraform won't try to create the schema, this is useful when it has already been created by a database administrator. +- `skip_table_creation` - If set to `true`, the Postgres table must already exist. Can also be set using the `PG_SKIP_TABLE_CREATION` environment variable. Terraform won't try to create the table, this is useful when it has already been created by a database administrator. +- `skip_index_creation` - If set to `true`, the Postgres index must already exist. Can also be set using the `PG_SKIP_INDEX_CREATION` environment variable. Terraform won't try to create the index, this is useful when it has already been created by a database administrator. + +## Technical Design + +This backend creates one table **states** in the automatically-managed Postgres schema configured by the `schema_name` variable. + +The table is keyed by the [workspace](/terraform/language/state/workspaces) name. If workspaces are not in use, the name `default` is used. + +Locking is supported using [Postgres advisory locks](https://www.postgresql.org/docs/9.5/explicit-locking.html#ADVISORY-LOCKS). [`force-unlock`](/terraform/cli/commands/force-unlock) is not supported, because these database-native locks will automatically unlock when the session is aborted or the connection fails. To see outstanding locks in a Postgres server, use the [`pg_locks` system view](https://www.postgresql.org/docs/9.5/view-pg-locks.html). + +The **states** table contains: + +- a serial integer `id`, used as the key for advisory locks +- the workspace `name` key as _text_ with a unique index +- the Terraform state `data` as _text_ diff --git a/website/docs/language/settings/backends/remote.mdx b/website/docs/language/backend/remote.mdx similarity index 68% rename from website/docs/language/settings/backends/remote.mdx rename to website/docs/language/backend/remote.mdx index 9611b115b1..e9ea55281a 100644 --- a/website/docs/language/settings/backends/remote.mdx +++ b/website/docs/language/backend/remote.mdx @@ -7,18 +7,18 @@ description: >- # remote --> **Note:** The remote backend was introduced in Terraform v0.11.13 and Terraform Enterprise v201809-1. As of Terraform v1.1.0 and Terraform Enterprise v202201-1, **we recommend using the Terraform Cloud's built-in [`cloud` integration](/language/settings/terraform-cloud)** instead of this backend. The `cloud` option includes an improved user experience and more features. +-> **Note:** We introduced the remote backend in Terraform v0.11.13 and Terraform Enterprise v201809-1. As of Terraform v1.1.0 and Terraform Enterprise v202201-1, we recommend using HCP Terraform's built-in [`cloud` integration](/terraform/cli/cloud/settings) instead. The `cloud` option supports an improved user experience and more features, such as [structured run output mode](/terraform/cloud-docs/workspaces/settings#user-interface), which displays your plan and apply results in a human-readable format. -The remote backend is unique among all other Terraform backends because it can both store state snapshots and execute operations for Terraform Cloud's [CLI-driven run workflow](/cloud-docs/run/cli). It used to be called an "enhanced" backend. +The remote backend is unique among all other Terraform backends because it can both store state snapshots and execute operations for HCP Terraform's [CLI-driven run workflow](/terraform/cloud-docs/run/cli). It used to be called an "enhanced" backend. When using full remote operations, operations like `terraform plan` or `terraform apply` can be executed in Terraform -Cloud's run environment, with log output streaming to the local terminal. Remote plans and applies use variable values from the associated Terraform Cloud workspace. +Cloud's run environment, with log output streaming to the local terminal. Remote plans and applies use variable values from the associated HCP Terraform workspace. -You can also use Terraform Cloud with local operations, in which case only state is stored in the Terraform Cloud backend. +You can also use HCP Terraform with local operations, in which case only state is stored in the HCP Terraform backend. ## Command Support -Currently the remote backend supports the following Terraform commands: +The remote backend supports the following Terraform commands: - `apply` - `console` (supported in Terraform >= v0.11.12) @@ -41,18 +41,18 @@ Currently the remote backend supports the following Terraform commands: ## Workspaces -The remote backend can work with either a single remote Terraform Cloud workspace, or with multiple similarly-named remote workspaces (like `networking-dev` and `networking-prod`). The `workspaces` block of the backend configuration +The remote backend can work with either a single remote HCP Terraform workspace, or with multiple similarly-named remote workspaces (like `networking-dev` and `networking-prod`). The `workspaces` block of the backend configuration determines which mode it uses: -- To use a single remote Terraform Cloud workspace, set `workspaces.name` to the +- To use a single remote HCP Terraform workspace, set `workspaces.name` to the remote workspace's full name (like `networking-prod`). - To use multiple remote workspaces, set `workspaces.prefix` to a prefix used in all of the desired remote workspace names. For example, set - `prefix = "networking-"` to use Terraform cloud workspaces with + `prefix = "networking-"` to use HCP Terraform workspaces with names like `networking-dev` and `networking-prod`. This is helpful when - mapping multiple Terraform CLI [workspaces](/language/state/workspaces) - used in a single Terraform configuration to multiple Terraform Cloud + mapping multiple Terraform CLI [workspaces](/terraform/language/state/workspaces) + used in a single Terraform configuration to multiple HCP Terraform workspaces. @@ -62,37 +62,37 @@ setting both results in a configuration error. If previous state is present when you run `terraform init` and the corresponding remote workspaces are empty or absent, Terraform will create workspaces and update the remote state accordingly. However, if your workspace requires variables or a specific version of Terraform for remote operations, we -recommend that you create your remote workspaces on Terraform Cloud before +recommend that you create your remote workspaces on HCP Terraform before running any remote operations against them. ### Workspace Names -Terraform uses shortened names without the common prefix to interact with workspaces on the command line. For example, if `prefix = "networking-"`, use `terraform workspace select prod` to switch to the Terraform CLI workspace `prod` within the current configuration. However, remote Terraform operations such as `plan` and `apply` for that Terraform CLI workspace will take place in the Terraform Cloud workspace `networking-prod`. +Terraform uses shortened names without the common prefix to interact with workspaces on the command line. For example, if `prefix = "networking-"`, use `terraform workspace select prod` to switch to the Terraform CLI workspace `prod` within the current configuration. However, remote Terraform operations such as `plan` and `apply` for that Terraform CLI workspace will take place in the HCP Terraform workspace `networking-prod`. -Because of this, the [`terraform.workspace`](/language/state/workspaces#current-workspace-interpolation) interpolation expression produces different results depending on whether a remote workspace is configured to perform operations locally or remotely. For example, in a remote workspace called `networking-prod` created with `prefix = "networking-"` the expression produces the following: +Because of this, the [`terraform.workspace`](/terraform/language/state/workspaces#current-workspace-interpolation) interpolation expression produces different results depending on whether a remote workspace is configured to perform operations locally or remotely. For example, in a remote workspace called `networking-prod` created with `prefix = "networking-"` the expression produces the following: - For local operations, `terraform.workspace` = `prod` - For remote operations, `terraform.workspace`= `networking-prod` -Prior to Terraform version 1.1.0, Terraform Cloud workspaces used only the single `default` Terraform CLI workspace internally. So if a Terraform configuration used `terraform.workspace` to return `dev` or `prod`, remote runs in Terraform Cloud would always evaluate it as `default`, regardless of -which workspace you set with the `terraform workspace select` command. Therefore, we do not recommend using `terraform.workspace` in Terraform configurations that use Terraform 1.0.x or earlier and run remote operations against Terraform Cloud workspaces. +Prior to Terraform version 1.1.0, HCP Terraform workspaces used only the single `default` Terraform CLI workspace internally. So if a Terraform configuration used `terraform.workspace` to return `dev` or `prod`, remote runs in HCP Terraform would always evaluate it as `default`, regardless of +which workspace you set with the `terraform workspace select` command. Therefore, we do not recommend using `terraform.workspace` in Terraform configurations that use Terraform 1.0.x or earlier and run remote operations against HCP Terraform workspaces. ### Determining Run Environment -If you need to determine whether a run is local or remote in your Terraform configuration, we recommend using [Terraform Cloud run environment variables](/cloud-docs/run/run-environment#environment-variables). The example below uses `TFC_RUN_ID`. +If you need to determine whether a run is local or remote in your Terraform configuration, we recommend using [HCP Terraform run environment variables](/terraform/cloud-docs/run/run-environment#environment-variables). The example below uses `HCP_TERRAFORM_RUN_ID`. -``` +```hcl output "current_workspace_name" { value = terraform.workspace } -variable "TFC_RUN_ID" { +variable "HCP_TERRAFORM_RUN_ID" { type = string default = "" } output "remote_execution_determine" { - value = "Remote run environment? %{if var.TFC_RUN_ID != ""}Yes%{else}No this is local%{endif}!" + value = "Remote run environment? %{if var.HCP_TERRAFORM_RUN_ID != ""}Yes%{else}No this is local%{endif}!" } ``` @@ -100,8 +100,8 @@ output "remote_execution_determine" { ## Example Configurations -> **Note:** We recommend omitting the token from the configuration, and instead using -[`terraform login`](/cli/commands/login) or manually configuring -`credentials` in the [CLI config file](/cli/config/config-file#credentials). +[`terraform login`](/terraform/cli/commands/login) or manually configuring +`credentials` in the [CLI config file](/terraform/cli/config/config-file#credentials). ### Basic Configuration @@ -175,7 +175,7 @@ data "terraform_remote_state" "foo" { ## Configuration Variables -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. The following configuration options are supported: @@ -185,9 +185,9 @@ The following configuration options are supported: targeted workspace(s). - `token` - (Optional) The token used to authenticate with the remote backend. We recommend omitting the token from the configuration, and instead using - [`terraform login`](/cli/commands/login) or manually configuring + [`terraform login`](/terraform/cli/commands/login) or manually configuring `credentials` in the - [CLI config file](/cli/config/config-file#credentials). + [CLI config file](/terraform/cli/config/config-file#credentials). - `workspaces` - (Required) A block specifying which remote workspace(s) to use. The `workspaces` block supports the following keys: @@ -195,12 +195,12 @@ The following configuration options are supported: only the default workspace can be used. This option conflicts with `prefix`. - `prefix` - (Optional) A prefix used in the names of one or more remote workspaces, all of which can be used with this configuration. The full - workspace names are used in Terraform Cloud, and the short names + workspace names are used in HCP Terraform, and the short names (minus the prefix) are used on the command line for Terraform CLI workspaces. If omitted, only the default workspace can be used. This option conflicts with `name`. -> **Note:** You must use the `name` key when configuring a `terraform_remote_state` -data source that retrieves state from another Terraform Cloud workspace. The `prefix` key is only +data source that retrieves state from another HCP Terraform workspace. The `prefix` key is only intended for use when configuring an instance of the remote backend. ## Command Line Arguments @@ -219,7 +219,7 @@ the remote workspace accept the following option to modify that behavior: local operation creating a new state snapshot which the workspace's remote execution environment would then be unable to decode. - Overriding this check can result in a Terraform Cloud workspace that is + Overriding this check can result in an HCP Terraform workspace that is no longer able to complete remote operations, so we recommend against using this option. @@ -227,8 +227,8 @@ the remote workspace accept the following option to modify that behavior: -> **Version note:** `.terraformignore` support was added in Terraform 0.12.11. -When executing a remote `plan` or `apply` in a [CLI-driven run](/cloud-docs/run/cli), -an archive of your configuration directory is uploaded to Terraform Cloud. You can define +When executing a remote `plan` or `apply` in a [CLI-driven run](/terraform/cloud-docs/run/cli), +an archive of your configuration directory is uploaded to HCP Terraform. You can define paths to ignore from upload via a `.terraformignore` file at the root of your configuration directory. If this file is not present, the archive will exclude the following by default: - `.git/` directories diff --git a/website/docs/language/backend/s3.mdx b/website/docs/language/backend/s3.mdx new file mode 100644 index 0000000000..77794f44cd --- /dev/null +++ b/website/docs/language/backend/s3.mdx @@ -0,0 +1,617 @@ +--- +page_title: 'Backend Type: s3' +description: Terraform can store and lock state remotely in Amazon S3. +--- + +# S3 + +Stores the state as a given key in a given bucket on [Amazon S3](https://aws.amazon.com/s3/). +This backend also supports state locking which can be enabled by setting the `use_lockfile` argument to `true`. + +~> **Warning!** It is highly recommended that you enable +[Bucket Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/manage-versioning-examples.html) +on the S3 bucket to allow for state recovery in the case of accidental deletions and human error. + +## Example Configuration + +```hcl +terraform { + backend "s3" { + bucket = "mybucket" + key = "path/to/my/key" + region = "us-east-1" + } +} +``` + +This assumes we have a bucket created called `mybucket`. The Terraform state is written to the key `path/to/my/key`. + +Note that for the access credentials we recommend using a [partial configuration](/terraform/language/backend#partial-configuration). + +## State Storage + +The S3 backend stores state data in an S3 object at the path set by the `key` parameter in the S3 bucket indicated by the `bucket` parameter. +Using the example shown above, the state would be stored at the path `path/to/my/key` in the bucket `mybucket`. + +When using [workspaces](/terraform/language/state/workspaces), the state for the `default` workspace is stored at the location described above. +Other workspaces are stored using the path `//`. +The default workspace key prefix is `env:` and it can be configured using the parameter `workspace_key_prefix`. +Using the example above, the state for the workspace `development` would be stored at the path `env:/development/path/to/my/key`. + + +### State Locking + +State locking is an opt-in feature of the S3 backend. + +Locking can be enabled via S3 or DynamoDB. However, **DynamoDB-based locking is deprecated** and will be removed in a future minor version. To support migration from older versions of Terraform that only support DynamoDB-based locking, the S3 and DynamoDB arguments can be configured simultaneously. + +#### Enabling S3 State Locking + +To enable S3 state locking, use the following optional argument: + +- `use_lockfile` - (Optional) Whether to use a lockfile for locking the state file. Defaults to `false`. + +#### Enabling DynamoDB State Locking (Deprecated) + +To enable DynamoDB state locking, use the following optional arguments: + +- `dynamodb_endpoint` - (Optional, **Deprecated**) Custom endpoint URL for the AWS DynamoDB API. Use `endpoints.dynamodb` instead. +- `dynamodb_table` - (Optional, **Deprecated**) Name of the DynamoDB Table to use for state locking and consistency. The table must have a partition key named `LockID` with a type of `String`. + +## Permissions Required + +### S3 Bucket Permissions + +When not using [workspaces](/terraform/language/state/workspaces)(or when only using the `default` workspace), Terraform will need the following AWS IAM permissions on the target backend bucket: + +* `s3:ListBucket` on `arn:aws:s3:::mybucket`. At a minimum, this must be able to list the path where the state is stored. +* `s3:GetObject` on `arn:aws:s3:::mybucket/path/to/my/key` +* `s3:PutObject` on `arn:aws:s3:::mybucket/path/to/my/key` + +-> **Note:** If `use_lockfile` is set, `s3:GetObject`, `s3:PutObject`, +and `s3:DeleteObject` are required on the lock file, e.g., +`arn:aws:s3:::mybucket/path/to/my/key.tflock`. + +-> **Note:** `s3:DeleteObject` is not required on the state file, as Terraform does not delete it. + +This is seen in the following AWS IAM Statement: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::mybucket", + "Condition": { + "StringEquals": { + "s3:prefix": "path/to/my/key" + } + } + }, + { + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObject"], + "Resource": [ + "arn:aws:s3:::mybucket/path/to/my/key" + ] + }, + { + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], + "Resource": [ + "arn:aws:s3:::mybucket/path/to/my/key.tflock" + ] + } + ] +} +``` + +When using [workspaces](/terraform/language/state/workspaces), Terraform will also need permissions to create, list, read, update, and delete the workspace state file: + +* `s3:ListBucket` on `arn:aws:s3:::mybucket`. At a minumum, this must be able to list the path where the `default` workspace is stored as well as the other workspaces. +* `s3:GetObject` on `arn:aws:s3:::mybucket/path/to/my/key`, `arn:aws:s3:::mybucket//*/path/to/my/key` +* `s3:PutObject` on `arn:aws:s3:::mybucket/path/to/my/key`, `arn:aws:s3:::mybucket//*/path/to/my/key` +* `s3:DeleteObject` on `arn:aws:s3:::mybucket//*/path/to/my/key` + +-> **Note:** If `use_lockfile` is set, `s3:GetObject`, `s3:PutObject`, +and `s3:DeleteObject` are required on the lock file, e.g., +`arn:aws:s3:::mybucket//*/path/to/my/key.tflock`. + +-> **Note:** AWS can control access to S3 buckets with either IAM policies +attached to users/groups/roles (like the example above) or resource policies +attached to bucket objects (which look similar but also require a `Principal` to +indicate which entity has those permissions). For more details, see Amazon's +documentation about +[S3 access control](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html). + +### DynamoDB Table Permissions + +If you are using the deprecated DynamoDB-based locking mechanism, Terraform will need the following AWS IAM +permissions on the DynamoDB table (`arn:aws:dynamodb:::table/mytable`): + +* `dynamodb:DescribeTable` +* `dynamodb:GetItem` +* `dynamodb:PutItem` +* `dynamodb:DeleteItem` + +This is seen in the following AWS IAM Statement: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "dynamodb:DescribeTable", + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:DeleteItem" + ], + "Resource": "arn:aws:dynamodb:*:*:table/mytable" + } + ] +} +``` + +## Data Source Configuration + +To make use of the S3 remote state in another configuration, +use the [`terraform_remote_state` data source](/terraform/language/state/remote-state-data). + +```hcl +data "terraform_remote_state" "network" { + backend = "s3" + config = { + bucket = "terraform-state-prod" + key = "network/terraform.tfstate" + region = "us-east-1" + } +} +``` + +The `terraform_remote_state` data source will return all of the root module +outputs defined in the referenced remote state (but not any outputs from +nested modules unless they are explicitly output again in the root). An +example output might look like: + +``` +data.terraform_remote_state.network: + id = 2016-10-29 01:57:59.780010914 +0000 UTC + addresses.# = 2 + addresses.0 = 52.207.220.222 + addresses.1 = 54.196.78.166 + backend = s3 + config.% = 3 + config.bucket = terraform-state-prod + config.key = network/terraform.tfstate + config.region = us-east-1 + elb_address = web-elb-790251200.us-east-1.elb.amazonaws.com + public_subnet_id = subnet-1e05dd33 +``` + +## Configuration + +This backend requires the configuration of the AWS Region and S3 state storage. Other configuration, such as enabling state locking, is optional. + +### Credentials and Shared Configuration + +!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/terraform/language/backend#credentials-and-sensitive-data) for details. + +The following configuration is required: + +* `region` - (Required) AWS Region of the S3 Bucket and DynamoDB Table (if used). This can also be sourced from the `AWS_DEFAULT_REGION` and `AWS_REGION` environment variables. + +The following configuration is optional: + +* `use_lockfile` - (Optional) Whether to use a lockfile for locking the state file. Defaults to `false`. +* `access_key` - (Optional) AWS access key. If configured, must also configure `secret_key`. This can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable, AWS shared credentials file (e.g. `~/.aws/credentials`), or AWS shared configuration file (e.g. `~/.aws/config`). +* `allowed_account_ids` - (Optional) List of allowed AWS account IDs to prevent potential destruction of a live environment. Conflicts with `forbidden_account_ids`. +* `custom_ca_bundle` - (Optional) File containing custom root and intermediate certificates. Can also be set using the `AWS_CA_BUNDLE` environment variable. Setting ca_bundle in the shared config file is not supported. +* `ec2_metadata_service_endpoint` - (Optional) Custom endpoint URL for the EC2 Instance Metadata Service (IMDS) API. + Can also be set with the `AWS_EC2_METADATA_SERVICE_ENDPOINT` environment variable. +* `ec2_metadata_service_endpoint_mode` - (Optional) Mode to use in communicating with the metadata service. + Valid values are `IPv4` and `IPv6`. + Can also be set with the `AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE` environment variable. +* `forbidden_account_ids` - (Optional) List of forbidden AWS account IDs to prevent potential destruction of a live environment. Conflicts with `allowed_account_ids`. +* `http_proxy` - (Optional) URL of a proxy to use for HTTP requests when accessing the AWS API. + Can also be set using the `HTTP_PROXY` or `http_proxy` environment variables. +* `https_proxy` - (Optional) URL of a proxy to use for HTTPS requests when accessing the AWS API. + Can also be set using the `HTTPS_PROXY` or `https_proxy` environment variables. +* `iam_endpoint` - (Optional, **Deprecated**) Custom endpoint URL for the AWS Identity and Access Management (IAM) API. + Use `endpoints.iam` instead. +* `insecure` - (Optional) Whether to explicitly allow the backend to perform "insecure" SSL requests. If omitted, the default value is `false`. +* `no_proxy` - (Optional) Comma-separated list of hosts that should not use HTTP or HTTPS proxies. + Each value can be one of: + * A domain name + * An IP address + * A CIDR address + * An asterisk (`*`), to indicate that no proxying should be performed + Domain name and IP address values can also include a port number. + Can also be set using the `NO_PROXY` or `no_proxy` environment variables. +* `max_retries` - (Optional) The maximum number of times an AWS API request is retried on retryable failure. Defaults to 5. +* `profile` - (Optional) Name of AWS profile in AWS shared credentials file (e.g. `~/.aws/credentials`) or AWS shared configuration file (e.g. `~/.aws/config`) to use for credentials and/or configuration. This can also be sourced from the `AWS_PROFILE` environment variable. +* `retry_mode` - (Optional) Specifies how retries are attempted. Valid values are `standard` and `adaptive`. Can also be configured using the `AWS_RETRY_MODE` environment variable or the shared config file parameter `retry_mode`. +* `secret_key` - (Optional) AWS access key. If configured, must also configure `access_key`. This can also be sourced from the `AWS_SECRET_ACCESS_KEY` environment variable, AWS shared credentials file (e.g. `~/.aws/credentials`), or AWS shared configuration file (e.g. `~/.aws/config`). +* `shared_config_files` - (Optional) List of paths to AWS shared configuration files. Defaults to `~/.aws/config`. +* `shared_credentials_file` - (Optional, **Deprecated**, use `shared_credentials_files` instead) Path to the AWS shared credentials file. Defaults to `~/.aws/credentials`. +* `shared_credentials_files` - (Optional) List of paths to AWS shared credentials files. Defaults to `~/.aws/credentials`. +* `skip_credentials_validation` - (Optional) Skip credentials validation via the STS API. + Useful for testing and for AWS API implementations that do not have STS available. +* `skip_region_validation` - (Optional) Skip validation of provided region name. +* `skip_requesting_account_id` - (Optional) Whether to skip requesting the account ID. + Useful for AWS API implementations that do not have the IAM, STS API, or metadata API. +* `skip_metadata_api_check` - (Optional) Skip usage of EC2 Metadata API. +* `skip_s3_checksum` - (Optional) Do not include checksum when uploading S3 Objects. + Useful for some S3-Compatible APIs. +* `sts_endpoint` - (Optional, **Deprecated**) Custom endpoint URL for the AWS Security Token Service (STS) API. + Use `endpoints.sts` instead. +* `sts_region` - (Optional) AWS region for STS. If unset, AWS will use the same region for STS as other non-STS operations. +* `token` - (Optional) Multi-Factor Authentication (MFA) token. This can also be sourced from the `AWS_SESSION_TOKEN` environment variable. +* `use_dualstack_endpoint` - (Optional) Force the backend to resolve endpoints with DualStack capability. Can also be set with the `AWS_USE_DUALSTACK_ENDPOINT` environment variable or in a shared config file (`use_dualstack_endpoint`). +* `use_fips_endpoint` - (Optional) Force the backend to resolve endpoints with FIPS capability. Can also be set with the `AWS_USE_FIPS_ENDPOINT` environment variable or in a shared config file (`use_fips_endpoint`). + +#### Overriding AWS API endpoints + +The optional argument `endpoints` contains the following arguments: + +* `dynamodb` - (Optional, **Deprecated**) Custom endpoint URL for the AWS DynamoDB API. + This can also be sourced from the environment variable `AWS_ENDPOINT_URL_DYNAMODB` or the deprecated environment variable `AWS_DYNAMODB_ENDPOINT`. +* `iam` - (Optional) Custom endpoint URL for the AWS IAM API. + This can also be sourced from the environment variable `AWS_ENDPOINT_URL_IAM` or the deprecated environment variable `AWS_IAM_ENDPOINT`. +* `s3` - (Optional) Custom endpoint URL for the AWS S3 API. + This can also be sourced from the environment variable `AWS_ENDPOINT_URL_S3` or the deprecated environment variable `AWS_S3_ENDPOINT`. +* `sso` - (Optional) Custom endpoint URL for the AWS IAM Identity Center (formerly known as AWS SSO) API. + This can also be sourced from the environment variable `AWS_ENDPOINT_URL_SSO`. +* `sts` - (Optional) Custom endpoint URL for the AWS STS API. + This can also be sourced from the environment variable `AWS_ENDPOINT_URL_STS` or the deprecated environment variable `AWS_STS_ENDPOINT`. + +The environment variable `AWS_ENDPOINT_URL` can be used to set a base endpoint URL for all services. + +Endpoints can also be overridden using the [AWS shared configuration file](https://docs.aws.amazon.com/sdkref/latest/guide/feature-ss-endpoints.html#ss-endpoints-config). +Setting the parameter `endpoint_url` on a profile will set that endpoint for all services. +To set endpoints for specific services, create a `services` section and set the `endpoint_url` parameters for each desired service. +Endpoints set for specific services will override the base endpoint configured in the profile. + +#### Assume Role Configuration + +The argument `assume_role` contains the following arguments: + +* `role_arn` - (Required) Amazon Resource Name (ARN) of the IAM Role to assume. +* `duration` - (Optional) The duration individual credentials will be valid. + Credentials are automatically renewed up to the maximum defined by the AWS account. + Specified using the format `hms` with any unit being optional. + For example, an hour and a half can be specified as `1h30m` or `90m`. + Must be between 15 minutes (15m) and 12 hours (12h). +* `external_id` - (Optional) External identifier to use when assuming the role. +* `policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed. +* `policy_arns` - (Optional) Set of Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed. +* `session_name` - (Optional) Session name to use when assuming the role. +* `source_identity` - (Optional) Source identity specified by the principal assuming the role. +* `tags` - (Optional) Map of assume role session tags. +* `transitive_tag_keys` - (Optional) Set of assume role session tag keys to pass to any subsequent sessions. + +Multiple `assume_role` values can be specified, and the roles will be assumed in order. + +```terraform +terraform { + backend "s3" { + bucket = "example-bucket" + key = "path/to/state" + region = "us-east-1" + assume_role = { + role_arn = "arn:aws:iam::PRODUCTION-ACCOUNT-ID:role/Terraform" + } + } +} +``` + +#### Assume Role With Web Identity Configuration + +The following `assume_role_with_web_identity` configuration block is optional: + +* `role_arn` - (Required) Amazon Resource Name (ARN) of the IAM Role to assume. + Can also be set with the `AWS_ROLE_ARN` environment variable. +* `duration` - (Optional) The duration individual credentials will be valid. + Credentials are automatically renewed up to the maximum defined by the AWS account. + Specified using the format `hms` with any unit being optional. + For example, an hour and a half can be specified as `1h30m` or `90m`. + Must be between 15 minutes (15m) and 12 hours (12h). +* `policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed. +* `policy_arns` - (Optional) Set of Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed. +* `session_name` - (Optional) Session name to use when assuming the role. + Can also be set with the `AWS_ROLE_SESSION_NAME` environment variable. +* `web_identity_token` - (Optional) The value of a web identity token from an OpenID Connect (OIDC) or OAuth provider. + One of `web_identity_token` or `web_identity_token_file` is required. +* `web_identity_token_file` - (Optional) File containing a web identity token from an OpenID Connect (OIDC) or OAuth provider. + One of `web_identity_token_file` or `web_identity_token` is required. + Can also be set with the `AWS_WEB_IDENTITY_TOKEN_FILE` environment variable. + +```terraform +terraform { + backend "s3" { + bucket = "example-bucket" + key = "path/to/state" + region = "us-east-1" + assume_role_with_web_identity = { + role_arn = "arn:aws:iam::PRODUCTION-ACCOUNT-ID:role/Terraform" + web_identity_token = "" + } + } +} +``` + +### S3 State Storage + +The following configuration is required: + +* `bucket` - (Required) Name of the S3 Bucket. +* `key` - (Required) Path to the state file inside the S3 Bucket. When using a non-default [workspace](/terraform/language/state/workspaces), the state path will be `//` (see also the `workspace_key_prefix` configuration). + +The following configuration is optional: + +* `acl` - (Optional) [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl) to be applied to the state and lock files. +* `encrypt` - (Optional) Enable [server side encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html) of the state and lock files. +* `endpoint` - (Optional, **Deprecated**) Custom endpoint URL for the AWS S3 API. + Use `endpoints.s3` instead. +* `force_path_style` - (Optional, **Deprecated**) Enable path-style S3 URLs (`https:///` instead of `https://.`). +* `kms_key_id` - (Optional) Amazon Resource Name (ARN) of a Key Management Service (KMS) Key to use for encrypting the state and lock files. Note that if this value is specified, Terraform will need `kms:Encrypt`, `kms:Decrypt` and `kms:GenerateDataKey` permissions on this KMS key. +* `sse_customer_key` - (Optional) The key to use for encrypting state and lock files with [Server-Side Encryption with Customer-Provided Keys (SSE-C)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html). This is the base64-encoded value of the key, which must decode to 256 bits. This can also be sourced from the `AWS_SSE_CUSTOMER_KEY` environment variable, which is recommended due to the sensitivity of the value. Setting it inside a terraform file will cause it to be persisted to disk in `terraform.tfstate`. +* `use_path_style` - (Optional) Enable path-style S3 URLs (`https:///` instead of `https://.`). +* `workspace_key_prefix` - (Optional) Prefix applied to the state path inside the bucket. This is only relevant when using a non-default workspace. Defaults to `env:`. + +## Multi-account AWS Architecture + +A common architectural pattern is for an organization to use a number of +separate AWS accounts to isolate different teams and environments. For example, +a "staging" system will often be deployed into a separate AWS account than +its corresponding "production" system, to minimize the risk of the staging +environment affecting production infrastructure, whether via rate limiting, +misconfigured access controls, or other unintended interactions. + +The S3 backend can be used in a number of different ways that make different +tradeoffs between convenience, security, and isolation in such an organization. +This section describes one such approach that aims to find a good compromise +between these tradeoffs, allowing use of +[Terraform's workspaces feature](/terraform/language/state/workspaces) to switch +conveniently between multiple isolated deployments of the same configuration. + +Use this section as a starting-point for your approach, but note that +you will probably need to make adjustments for the unique standards and +regulations that apply to your organization. You will also need to make some +adjustments to this approach to account for _existing_ practices within your +organization, if for example other tools have previously been used to manage +infrastructure. + +Terraform is an administrative tool that manages your infrastructure, and so +ideally the infrastructure that is used by Terraform should exist outside of +the infrastructure that Terraform manages. This can be achieved by creating a +separate _administrative_ AWS account which contains the user accounts used by +human operators and any infrastructure and tools used to manage the other +accounts. Isolating shared administrative tools from your main environments +has a number of advantages, such as avoiding accidentally damaging the +administrative infrastructure while changing the target infrastructure, and +reducing the risk that an attacker might abuse production infrastructure to +gain access to the (usually more privileged) administrative infrastructure. + +### Administrative Account Setup + +Your administrative AWS account will contain at least the following items: + +* One or more [IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html) + for system administrators that will log in to maintain infrastructure in + the other accounts. +* Optionally, one or more [IAM groups](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_groups.html) + to differentiate between different groups of users that have different + levels of access to the other AWS accounts. +* An [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingBucket.html) + that will contain the Terraform state files for each workspace. + +Provide the S3 bucket name to Terraform in the S3 backend configuration +using the `bucket` argument. Set `use_lockfile` to true to enable state locking. +Configure a suitable `workspace_key_prefix` to manage states of workspaces that +will be created for this configuration. + +### Environment Account Setup + +For the sake of this section, the term "environment account" refers to one +of the accounts whose contents are managed by Terraform, separate from the +administrative account described above. + +Your environment accounts will eventually contain your own product-specific +infrastructure. Along with this it must contain one or more +[IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) +that grant sufficient access for Terraform to perform the desired management +tasks. + +### Delegating Access + +Each Administrator will run Terraform using credentials for their IAM user +in the administrative account. +[IAM Role Delegation](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html) +is used to grant these users access to the roles created in each environment +account. + +Full details on role delegation are covered in the AWS documentation linked +above. The most important details are: + +* Each role's _Assume Role Policy_ must grant access to the administrative AWS + account, which creates a trust relationship with the administrative AWS + account so that its users may assume the role. +* The users or groups within the administrative account must also have a + policy that creates the converse relationship, allowing these users or groups + to assume that role. + +Since the purpose of the administrative account is only to host tools for +managing other accounts, it is useful to give the administrative accounts +restricted access only to the specific operations needed to assume the +environment account role and access the Terraform state. By blocking all +other access, you remove the risk that user error will lead to staging or +production resources being created in the administrative account by mistake. + +When configuring Terraform, use either environment variables or the standard +credentials file `~/.aws/credentials` to provide the administrator user's +IAM credentials within the administrative account to both the S3 backend _and_ +to Terraform's AWS provider. + +Use conditional configuration to pass a different `assume_role` value to +the AWS provider depending on the selected workspace. For example: + +```hcl +variable "workspace_iam_roles" { + default = { + staging = "arn:aws:iam::STAGING-ACCOUNT-ID:role/Terraform" + production = "arn:aws:iam::PRODUCTION-ACCOUNT-ID:role/Terraform" + } +} + +provider "aws" { + # No credentials explicitly set here because they come from either the + # environment or the global credentials file. + + assume_role = { + role_arn = var.workspace_iam_roles[terraform.workspace] + } +} +``` + +If workspace IAM roles are centrally managed and shared across many separate +Terraform configurations, the role ARNs could also be obtained via a data +source such as [`terraform_remote_state`](/terraform/language/state/remote-state-data) +to avoid repeating these values. + +### Creating and Selecting Workspaces + +With the necessary objects created and the backend configured, run +`terraform init` to initialize the backend and establish an initial workspace +called "default". This workspace will not be used, but is created automatically +by Terraform as a convenience for users who are not using the workspaces +feature. + +Create a workspace corresponding to each key given in the `workspace_iam_roles` +variable value above: + +``` +$ terraform workspace new staging +Created and switched to workspace "staging"! + +... + +$ terraform workspace new production +Created and switched to workspace "production"! + +... +``` + +Due to the `assume_role` setting in the AWS provider configuration, any +management operations for AWS resources will be performed via the configured +role in the appropriate environment AWS account. The backend operations, such +as reading and writing the state from S3, will be performed directly as the +administrator's own user within the administrative account. + +``` +$ terraform workspace select staging +$ terraform apply +... +``` + +### Running Terraform in Amazon EC2 + +Teams that make extensive use of Terraform for infrastructure management +often [run Terraform in automation](/terraform/tutorials/automation/automate-terraform?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) +to ensure a consistent operating environment and to limit access to the +various secrets and other sensitive information that Terraform configurations +tend to require. + +When running Terraform in an automation tool running on an Amazon EC2 instance, +consider running this instance in the administrative account and using an +[instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) +in place of the various administrator IAM users suggested above. An IAM +instance profile can also be granted cross-account delegation access via +an IAM policy, giving this instance the access it needs to run Terraform. + +To isolate access to different environment accounts, use a separate EC2 +instance for each target account so that its access can be limited only to +the single account. + +Similar approaches can be taken with equivalent features in other AWS compute +services, such as ECS. + +### Protecting Access to Workspace State + +In a simple implementation of the pattern described earlier, +all users can read and write states for all workspaces. +In many cases, it is desirable to apply precise access controls +to the Terraform state objects stored in S3. For example, only +trusted administrators should modify the production state. +It is also important to control access to _reading_ the state file. +If state locking is enabled, the lock file (`.tflock`) +must also be included in the access controls. + +Amazon S3 supports fine-grained access control on a per-object-path basis +using IAM policy. A full description of S3's access control mechanism is +beyond the scope of this guide, but an example IAM policy granting access +to only a single state object within an S3 bucket is shown below: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::example-bucket", + "Condition": { + "StringEquals": { + "s3:prefix": "path/to/state" + } + } + }, + { + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObject"], + "Resource": [ + "arn:aws:s3:::example-bucket/myapp/production/tfstate", + ] + }, + { + "Effect": "Allow", + "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], + "Resource": [ + "arn:aws:s3:::example-bucket/myapp/production/tfstate.tflock" + ] + } + ] +} +``` + +The example backend configuration below documents the corresponding `bucket`, `key` and `use_lockfile` arguments: + +```hcl +terraform { + backend "s3" { + bucket = "example-bucket" + key = "path/to/state" + use_lockfile = true + region = "us-east-1" + } +} +``` +Refer to the [AWS documentation on S3 access control](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html) for more details. + +### Configuring Custom User-Agent Information + +Note this feature is optional and only available in Terraform v0.13.1+. + +By default, the underlying AWS client used by the Terraform AWS Provider creates requests with User-Agent headers including information about Terraform and AWS Go SDK versions. To provide additional information in the User-Agent headers, the `TF_APPEND_USER_AGENT` environment variable can be set and its value will be directly added to HTTP requests. e.g. + +```sh +$ export TF_APPEND_USER_AGENT="JenkinsAgent/i-12345678 BuildID/1234 (Optional Extra Information)" +``` + +## Support for "S3 Compatible" Storage Providers + +Support for S3 Compatible storage providers is offered as “best effort”. +HashiCorp only tests the `s3` backend against Amazon S3, so cannot offer any guarantees when using an alternate provider. diff --git a/website/docs/language/checks/index.mdx b/website/docs/language/checks/index.mdx new file mode 100644 index 0000000000..021ee582c1 --- /dev/null +++ b/website/docs/language/checks/index.mdx @@ -0,0 +1,121 @@ +--- +page_title: Checks - Configuration Language +description: >- + Check customized infrastructure requirements to provide ongoing and continuous verification. +--- + +# Checks + +-> **Note:** Check blocks are only available in Terraform v1.5.0 and later. + +The `check` block can validate your infrastructure outside the usual resource lifecycle. Check blocks address a gap between post-apply and functional validation of infrastructure. + +> **Hands-on:** Try the [Validate Infrastructure Using Checks](/terraform/tutorials/configuration-language/checks) tutorial. + +Check blocks allow you to define [custom conditions](/terraform/language/expressions/custom-conditions) that execute on every Terraform plan or apply operation without affecting the overall status of an operation. Check blocks execute as the last step of a plan or apply after Terraform has planned or provisioned your infrastructure. + +## Syntax + +You can declare a `check` block with a local name, zero-to-one scoped [data sources](#scoped-data-sources), and one-to-many [assertions](#assertions). + +The following example loads the Terraform website and validates that it returns the expected status code `200`. + +```hcl +check "health_check" { + data "http" "terraform_io" { + url = "https://www.terraform.io" + } + + assert { + condition = data.http.terraform_io.status_code == 200 + error_message = "${data.http.terraform_io.url} returned an unhealthy status code" + } +} +``` + +### Scoped data sources + +You can use any data source from any provider as a scoped data source within a `check` block. + +A `check` block can optionally contain a nested (a.k.a. scoped) data source. This `data` block behaves like an external [data source](/terraform/language/data-sources), except you can not reference it outside its enclosing `check` block. Additionally, if a scoped data source's provider raises any errors, they are masked as warnings and do not prevent Terraform from continuing operation execution. + +You can use a scoped data source to validate the status of a piece of infrastructure outside of the usual Terraform resource lifecycle. [In the above example](#checks-syntax), if the `terraform_io` data source fails to load, you receive a warning instead of a blocking error, which would occur if you declared this data source outside of a `check` block. + +#### Meta-Arguments + +Scoped data sources support the `depends_on` and `provider` [meta-arguments](/terraform/language/resources/syntax#meta-arguments). Scoped data sources do not support the `count` or`for_each` meta-arguments. + +##### `depends_on` + +The `depends_on` meta-argument can be particularly powerful when used within scoped data sources. + +The first time Terraform creates the _initial_ plan for our [previous example](#checks-syntax), the plan fails because Terraform has not applied its configuration yet. Meaning this test fails because Terraform must still create the resources to make this website exist. Therefore, the first time Terraform runs this check, it always throws a potentially distracting error message. + +You can fix this by adding [`depends_on`](/terraform/language/meta-arguments/depends_on) to your scoped data source, ensuring it depends on an essential piece of your site's infrastructure, such as the load balancer. The check returns `known after apply` until that crucial piece of your website is ready. This strategy avoids producing unnecessary warnings during setup, and the check executes during subsequent plans and applies. + +One problem with this strategy is that if the resource your scoped data source `depends_on` changes, the check block returns `known after apply` until Terraform has updated that resource. Depending on your use case, this behavior could be acceptable or problematic. + +We recommend implementing the `depends_on` meta-argument if your scoped data source depends on the existence of another resource without referencing it directly. + +### Assertions + +Check blocks validate your custom assertions using `assert` blocks. Each `check` block must have at least one, but potentially many, `assert` blocks. Each `assert` block has a [`condition` attribute](/terraform/language/expressions/custom-conditions#condition-expressions) and an [`error_message` attribute](/terraform/language/expressions/custom-conditions#error-messages). + +Unlike other [custom conditions](/terraform/language/expressions/custom-conditions), assertions do not affect Terraform's execution of an operation. A failed assertion reports a warning without halting the ongoing operation. This contrasts with other custom conditions, such as a postcondition, where Terraform produces an error immediately, halting the operation and blocking the application or planning of future resources. + +Condition arguments within `assert` blocks can refer to scoped data sources within the enclosing `check` block and any variables, resources, data sources, or module outputs within the current module. + +[Learn more about assertions](/terraform/language/expressions/custom-conditions#checks-with-assertions). + +### Meta-Arguments + +Check blocks do not currently support [meta-arguments](/terraform/language/resources/syntax#meta-arguments). We are still collecting feedback on this feature, so if your use case would benefit from check blocks supporting meta-arguments, please [let us know](https://github.com/hashicorp/terraform/issues/new/choose). + +## Continuous validation in HCP Terraform + +HCP Terraform can automatically validate whether checks in a workspace’s configuration continue to pass after Terraform provisions new infrastructure. See [Continuous Validation](/terraform/cloud-docs/workspaces/health#continuous-validation) for details. + +## Choosing Checks or other Custom Conditions + +Check blocks offer the most _flexible_ validation solution within Terraform. You can reference outputs, variables, resources, and data sources within check assertions. You can also use checks to model every alternate [Custom Condition](/terraform/language/expressions/custom-conditions). However, that does not mean you should replace all your custom conditions with check blocks. + +There are major behavioral differences between check block assertions and other custom conditions, the main one being that check blocks do _not_ affect Terraform's execution of an operation. You can use this non-blocking behavior to decide the best type of validation for your use case. + +### Outputs and variables + +[Output postconditions](/terraform/language/expressions/custom-conditions#outputs) and [variable validations](/terraform/language/expressions/custom-conditions#input-variable-validation) both make assertions around inputs and outputs. + +This is one of the cases where you might want Terraform to block further execution. + +For example, it is not helpful for Terraform to warn that an input variable is invalid after it applies an entire configuration with that input variable. In this case, a check block would warn of the invalid input variable _without_ interrupting the operation. A validation block for the same input variable would alert you of the invalid variable and halt the plan or apply operation. + +### Resource Preconditions and Postconditions + +The difference between [preconditions and postconditions](/terraform/language/expressions/custom-conditions#preconditions-and-postconditions) and check blocks is more nuanced. + +Preconditions are unique amongst the custom conditions in that they execute _before_ a resource change is applied or planned. [Choosing Between Preconditions and Postconditions](/terraform/language/expressions/custom-conditions#choosing-between-preconditions-and-postconditions) offers guidance on choosing between a precondition and a postcondition, and the same topics also apply to choosing between a precondition and a check block. + +You can often use postconditions interchangeably with check blocks to validate resources and data sources. + +For example, you can [rewrite the above `check` block example](#syntax) to use a postcondition instead. The below code uses a `postcondition` block to validate that the Terraform website returns the expected status code of `200`. + +```hcl +data "http" "terraform_io" { + url = "https://www.terraform.io" + + lifecycle { + postcondition { + condition = self.status_code == 200 + error_message = "${self.url} returned an unhealthy status code" + } + } +} +``` + +Both the `check` and `postcondition` block examples validate that the Terraform website returns a `200` status code during a plan or an apply operation. The difference between the two blocks is how each handles failure. + +If a `postcondition` block fails, it _blocks_ Terraform from executing the current operation. If a `check` block fails, it _does not_ block Terraform from executing an operation. + +If the above example's postcondition fails, it is impossible to recover from. Terraform blocks any future plan or apply operations if your postcondition is unsatisfied during the planning stage. This problem occurs because the postcondition does not directly depend on Terraform configuration, but instead on the complex interactions between multiple resources. + +We recommend using check blocks to validate the status of infrastructure as a whole. We only recommend using postconditions when you want a guarantee on a single resource based on that resource's configuration. diff --git a/website/docs/language/data-sources/index.mdx b/website/docs/language/data-sources/index.mdx index d9ea8257fc..f8d6522395 100644 --- a/website/docs/language/data-sources/index.mdx +++ b/website/docs/language/data-sources/index.mdx @@ -11,10 +11,10 @@ description: >- _Data sources_ allow Terraform to use information defined outside of Terraform, defined by another separate Terraform configuration, or modified by functions. -> **Hands-on:** Try the [Query Data Sources](https://learn.hashicorp.com/tutorials/terraform/data-sources) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Query Data Sources](/terraform/tutorials/configuration-language/data-sources) tutorial. -Each [provider](/language/providers) may offer data sources -alongside its set of [resource](/language/resources) +Each [provider](/terraform/language/providers) may offer data sources +alongside its set of [resource](/terraform/language/resources) types. ## Using Data Sources @@ -61,14 +61,14 @@ Each data resource is associated with a single data source, which determines the kind of object (or objects) it reads and what query constraint arguments are available. -Each data source in turn belongs to a [provider](/language/providers), +Each data source in turn belongs to a [provider](/terraform/language/providers), which is a plugin for Terraform that offers a collection of resource types and data sources that most often belong to a single cloud or on-premises infrastructure platform. Most of the items within the body of a `data` block are defined by and specific to the selected data source, and these arguments can make full -use of [expressions](/language/expressions) and other dynamic +use of [expressions](/terraform/language/expressions) and other dynamic Terraform language features. However, there are some "meta-arguments" that are defined by Terraform itself @@ -115,7 +115,7 @@ operation, and is re-calculated each time a new plan is created. ## Data Resource Dependencies Data resources have the same dependency resolution behavior -[as defined for managed resources](/language/resources/behavior#resource-dependencies). +[as defined for managed resources](/terraform/language/resources/behavior#resource-dependencies). Setting the `depends_on` meta-argument within `data` blocks defers reading of the data source until after all changes to the dependencies have been applied. @@ -149,13 +149,13 @@ data "aws_ami" "example" { Custom conditions can help capture assumptions, helping future maintainers understand the configuration design and intent. They also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. -Refer to [Custom Condition Checks](/language/expressions/custom-conditions#preconditions-and-postconditions) for more details. +Refer to [Custom Condition Checks](/terraform/language/expressions/custom-conditions#preconditions-and-postconditions) for more details. ## Multiple Resource Instances -Data resources support [`count`](/language/meta-arguments/count) -and [`for_each`](/language/meta-arguments/for_each) +Data resources support [`count`](/terraform/language/meta-arguments/count) +and [`for_each`](/terraform/language/meta-arguments/for_each) meta-arguments as defined for managed resources, with the same syntax and behavior. As with managed resources, when `count` or `for_each` is present it is important to @@ -165,14 +165,16 @@ own variant of the constraint arguments, producing an indexed result. ## Selecting a Non-default Provider Configuration -Data resources support [the `provider` meta-argument](/language/meta-arguments/resource-provider) +Data resources support [the `provider` meta-argument](/terraform/language/meta-arguments/resource-provider) as defined for managed resources, with the same syntax and behavior. ## Lifecycle Customizations -Data resources do not currently have any customization settings available -for their lifecycle, but the `lifecycle` nested block is reserved in case -any are added in future versions. +Data resources do not have any customization settings available +for their lifecycle. Only the `precondition` and `postcondition` +blocks are allowed in the data resource `lifecycle` block. + +Refer to [Custom Condition Checks](#custom-condition-checks) for more details. ## Example @@ -203,7 +205,7 @@ and name must be unique. Within the block (the `{ }`) is configuration for the data instance. The configuration is dependent on the type; as with -[resources](/language/resources), each provider on the +[resources](/terraform/language/resources), each provider on the [Terraform Registry](https://registry.terraform.io/browse/providers) has its own documentation for configuring and using the data types it provides. @@ -221,13 +223,13 @@ resource "aws_instance" "web" { ## Meta-Arguments As data sources are essentially a read only subset of resources, they also -support the same [meta-arguments](/language/resources/syntax#meta-arguments) of resources +support the same [meta-arguments](/terraform/language/resources/syntax#meta-arguments) of resources with the exception of the -[`lifecycle` configuration block](/language/meta-arguments/lifecycle). +[`lifecycle` configuration block](/terraform/language/meta-arguments/lifecycle). ### Non-Default Provider Configurations -Similarly to [resources](/language/resources), when +Similarly to [resources](/terraform/language/resources), when a module has multiple configurations for the same provider you can specify which configuration to use with the `provider` meta-argument: @@ -240,7 +242,7 @@ data "aws_ami" "web" { ``` See -[The Resource `provider` Meta-Argument](/language/meta-arguments/resource-provider) +[The Resource `provider` Meta-Argument](/terraform/language/meta-arguments/resource-provider) for more information. ## Data Source Lifecycle diff --git a/website/docs/language/expressions/conditionals.mdx b/website/docs/language/expressions/conditionals.mdx index 90e08bbaaf..28ccc148b3 100644 --- a/website/docs/language/expressions/conditionals.mdx +++ b/website/docs/language/expressions/conditionals.mdx @@ -10,7 +10,7 @@ description: >- A _conditional expression_ uses the value of a boolean expression to select one of two values. -> **Hands-on:** Try the [Create Dynamic Expressions](https://learn.hashicorp.com/tutorials/terraform/expressions?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Create Dynamic Expressions](/terraform/tutorials/configuration-language/expressions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. ## Syntax @@ -26,8 +26,8 @@ If `condition` is `true` then the result is `true_val`. If `condition` is A common use of conditional expressions is to define defaults to replace invalid values: -``` -var.a != "" ? var.a : "default-a" +```hcl +var.a == "" ? "default-a" : var.a ``` If `var.a` is an empty string then the result is `"default-a"`, but otherwise @@ -45,7 +45,7 @@ You can create conditions that produce custom error messages for several types o Custom conditions can help capture assumptions, helping future maintainers understand the configuration design and intent. They also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. -Refer to [Custom Condition Checks](/language/expressions/custom-conditions#input-variable-validation) for details. +Refer to [Custom Condition Checks](/terraform/language/expressions/custom-conditions#input-variable-validation) for details. ## Result Types @@ -72,7 +72,7 @@ be some uncertainty about the expected result type. The following example is contrived because it would be easier to write the constant `"12"` instead of the type conversion in this case, but shows how to -use [`tostring`](/language/functions/tostring) to explicitly convert a number to +use [`tostring`](/terraform/language/functions/tostring) to explicitly convert a number to a string. ```hcl diff --git a/website/docs/language/expressions/custom-conditions.mdx b/website/docs/language/expressions/custom-conditions.mdx index b032126b2c..6f658ad36b 100644 --- a/website/docs/language/expressions/custom-conditions.mdx +++ b/website/docs/language/expressions/custom-conditions.mdx @@ -1,27 +1,37 @@ --- -page_title: Custom Condition Checks - Configuration Language +page_title: Custom Conditions - Configuration Language description: >- Check custom requirements for variables, outputs, data sources, and resources and provide better error messages in context. --- -# Custom Condition Checks +# Custom Conditions You can create conditions that produce custom error messages for several types of objects in a configuration. For example, you can add a condition to an input variable that checks whether incoming image IDs are formatted properly. Custom conditions can capture assumptions, helping future maintainers understand the configuration design and intent. They also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. -> **Hands On:** Try the [Validate Modules with Custom Conditions](https://learn.hashicorp.com/tutorials/terraform/custom-conditions?in=terraform/configuration-language) tutorial on HashiCorp Learn. +> **Hands On:** Try the [Validate Infrastructure Using Checks](/terraform/tutorials/configuration-language/checks) tutorial to learn how to use `check` blocks. Try the [Validate Modules with Custom Conditions](/terraform/tutorials/configuration-language/custom-conditions) tutorial to learn how to use other custom conditions. This page explains the following: + - Creating checks with [assertions](#checks-with-assertions) to verify your infrastructure as a whole (Terraform v1.5.0 and later) - Creating [validation conditions](#input-variable-validation) for input variables (Terraform v0.13.0 and later) - Creating [preconditions and postconditions](#preconditions-and-postconditions) for resources, data sources, and outputs (Terraform v1.2.0 and later) - Writing effective [condition expressions](#condition-expressions) and [error messages](#error-messages) - When Terraform [evaluates custom conditions](#conditions-checked-only-during-apply) during the plan and apply cycle +## Selecting a Custom Condition for your use case + +Terraform's different custom conditions are best suited to various situations. Use the following broad guidelines to select the best custom condition for your use case: +1. [Check blocks with assertions](#checks-with-assertions) validate your infrastructure as a whole. Additionally, check blocks do not prevent or block the overall execution of Terraform operations. +1. [Validation conditions](#input-variable-validation) or [output postconditions](#preconditions-and-postconditions) can ensure your configuration's inputs and outputs meet specific requirements. +1. Resource [preconditions and postconditions](#preconditions-and-postconditions) can validate that Terraform produces your configuration with predictable results. + +For more information on when to use certain custom conditions, see [Choosing Between Preconditions and Postconditions](#choosing-between-preconditions-and-postconditions) and [Choosing Checks or Other Custom Conditions](/terraform/language/checks#choosing-checks-or-other-custom-conditions). + ## Input Variable Validation --> **Note:** Input variable validation is available in Terraform v0.13.0 and later. +-> **Note:** Input variable validation is available in Terraform v0.13.0 and later. Before Terraform v1.9.0, validation rules can refer only to the variable being validated, and not to any other variables. -Add one or more `validation` blocks within the `variable` block to specify custom conditions. Each validation requires a [`condition` argument](#condition-expressions), an expression that must use the value of the variable to return `true` if the value is valid, or `false` if it is invalid. The expression can refer only to the containing variable and must not produce errors. +Add one or more `validation` blocks within the `variable` block to specify custom conditions. Each validation requires a [`condition` argument](#condition-expressions), an expression that must use the value of the variable to return `true` if the value is valid, or `false` if it is invalid. The expression must not cause errors directly itself. If the condition evaluates to `false`, Terraform produces an [error message](#error-messages) that includes the result of the `error_message` expression. If you declare multiple validations, Terraform returns error messages for all failed conditions. @@ -39,7 +49,7 @@ variable "image_id" { } ``` -If the failure of an expression determines the validation decision, use the [`can` function](/language/functions/can) as demonstrated in the following example. +If the failure of an expression determines the validation decision, use the [`can` function](/terraform/language/functions/can) as demonstrated in the following example. ```hcl variable "image_id" { @@ -71,7 +81,7 @@ If the condition evaluates to `false`, Terraform will produce an [error message] The following example uses a postcondition to detect if the caller accidentally provided an AMI intended for the wrong system component. -``` hcl +```hcl data "aws_ami" "example" { id = var.aws_ami_id @@ -111,13 +121,22 @@ The following example shows use cases for preconditions and postconditions. The - **The EC2 instance must be allocated a public DNS hostname.** In Amazon Web Services, EC2 instances are assigned public DNS hostnames only if they belong to a virtual network configured in a certain way. The postcondition would detect if the selected virtual network is not configured correctly, prompting the user to debug the network settings. -- **The EC2 instance will have an encrypted root volume.** The precondition ensures that the root volume is encrypted, even though the software running in this EC2 instance would probably still operate as expected on an unencrypted volume. This lets Terraform produce an error immediately, before any other components rely on the new EC2 instance. +- **The EC2 instance will have an encrypted root volume.** The postcondition ensures that the root volume is encrypted, even though the software running in this EC2 instance would probably still operate as expected on an unencrypted volume. This lets Terraform produce an error immediately, before any other components rely on the new EC2 instance. ```hcl +data "aws_ami" "example" { + owners = ["amazon"] + + filter { + name = "image-id" + values = ["ami-abc123"] + } +} + resource "aws_instance" "example" { - instance_type = "t2.micro" - ami = "ami-abc123" + instance_type = "t3.micro" + ami = data.aws_ami.example.id lifecycle { # The AMI ID must refer to an AMI that contains an operating system @@ -144,7 +163,7 @@ data "aws_ebs_volume" "example" { filter { name = "volume-id" - values = [aws_instance.example.root_block_device.volume_id] + values = [aws_instance.example.root_block_device[0].volume_id] } # Whenever a data resource is verifying the result of a managed resource @@ -190,10 +209,38 @@ You should also consider the following questions when creating preconditions and - Which approach is more convenient? If a particular resource has many dependencies that all make an assumption about that resource, it can be pragmatic to declare that once as a post-condition of the resource, rather than declaring it many times as preconditions on each of the dependencies. - Is it helpful to declare the same or similar conditions as both preconditions and postconditions? This can be useful if the postcondition is in a different module than the precondition because it lets the modules verify one another as they evolve independently. +## Checks with Assertions + +-> **Note:** Check blocks and their assertions are only available in Terraform v1.5.0 and later. + +[Check blocks](/terraform/language/checks) can validate your infrastructure outside the usual resource lifecycle. You can add custom conditions via `assert` blocks, which execute at the end of the plan and apply stages and produce warnings to notify you of problems within your infrastructure. + +You can add one or more `assert` blocks within a `check` block to verify custom conditions. Each assertion requires a [`condition` argument](#condition-expressions), a boolean expression that should return `true` if the intended assumption or guarantee is fulfilled or `false` if it does not. Your `condition` expression can refer to any resource, data source, or variable available to the surrounding `check` block. + +The following example uses a check block with an assertion to verify the Terraform website is healthy. + +```hcl +check "health_check" { + data "http" "terraform_io" { + url = "https://www.terraform.io" + } + + assert { + condition = data.http.terraform_io.status_code == 200 + error_message = "${data.http.terraform_io.url} returned an unhealthy status code" + } +} +``` + +If the condition evaluates to `false`, Terraform produces an [error message](#error-messages) that includes the result of the `error_message` expression. If you declare multiple assertions, Terraform returns error messages for all failed conditions. + +### Continuous Validation in HCP Terraform + +HCP Terraform can automatically check whether the checks in a workspace’s configuration continue to pass after Terraform provisions the infrastructure. For example, you can write a `check` to continuously monitor the validity of an API gateway certificate. Continuous validation alerts you when the condition fails, so you can update the certificate and avoid errors the next time you want to update your infrastructure. Refer to [Continuous Validation](/terraform/cloud-docs/workspaces/health#continuous-validation) in the HCP Terraform documentation for details. ## Condition Expressions -Input variable validation, preconditions, and postconditions all require a `condition` argument. This is a boolean expression that should return `true` if the intended assumption or guarantee is fulfilled or `false` if it does not. +Check assertions, input variable validation, preconditions, and postconditions all require a `condition` argument. This is a boolean expression that should return `true` if the intended assumption or guarantee is fulfilled or `false` if it does not. You can use any of Terraform's built-in functions or language operators in a condition as long as the expression is valid and returns a boolean result. The following language features are particularly useful when writing condition expressions. @@ -206,11 +253,11 @@ Use the logical operators `&&` (AND), `||` (OR), and `!` (NOT) to combine multip condition = var.name != "" && lower(var.name) == var.name ``` -You can also use arithmetic operators (e.g. `a + b`), equality operators (eg., `a == b`) and comparison operators (e.g., `a < b`). Refer to [Arithmetic and Logical Operators](/language/expressions/operators) for details. +You can also use arithmetic operators (e.g. `a + b`), equality operators (eg., `a == b`) and comparison operators (e.g., `a < b`). Refer to [Arithmetic and Logical Operators](/terraform/language/expressions/operators) for details. ### `contains` Function -Use the [`contains` function](/language/functions/contains) to test whether a given value is one of a set of predefined valid values. +Use the [`contains` function](/terraform/language/functions/contains) to test whether a given value is one of a set of predefined valid values. ```hcl condition = contains(["STAGE", "PROD"], var.environment) @@ -218,7 +265,7 @@ Use the [`contains` function](/language/functions/contains) to test whether a gi ### `length` Function -Use the [`length` function](/language/functions/length) to test a collection's length and require a non-empty list or map. +Use the [`length` function](/terraform/language/functions/length) to test a collection's length and require a non-empty list or map. ```hcl condition = length(var.items) != 0 @@ -227,7 +274,7 @@ This is a better approach than directly comparing with another collection using ### `for` Expressions -Use [`for` expressions](/language/expressions/for) in conjunction with the functions `alltrue` and `anytrue` to test whether a condition holds for all or for any elements of a collection. +Use [`for` expressions](/terraform/language/expressions/for) in conjunction with the functions `alltrue` and `anytrue` to test whether a condition holds for all or for any elements of a collection. ```hcl condition = alltrue([ @@ -237,7 +284,7 @@ Use [`for` expressions](/language/expressions/for) in conjunction with the funct ### `can` Function -Use the [`can` function](/language/functions/can) to concisely use the validity of an expression as a condition. It returns `true` if its given expression evaluates successfully and `false` if it returns any error, so you can use various other functions that typically return errors as a part of your condition expressions. +Use the [`can` function](/terraform/language/functions/can) to concisely use the validity of an expression as a condition. It returns `true` if its given expression evaluates successfully and `false` if it returns any error, so you can use various other functions that typically return errors as a part of your condition expressions. For example, you can use `can` with `regex` to test if a string matches a particular pattern because `regex` returns an error when given a non-matching string. @@ -295,7 +342,7 @@ resource "aws_instance" "example" { ### `each` and `count` Objects -In blocks where [`for_each`](/language/meta-arguments/for_each) or [`count`](/language/meta-arguments/count) are set, use `each` and `count` objects to refer to other resources that are expanded in a chain. +In blocks where [`for_each`](/terraform/language/meta-arguments/for_each) or [`count`](/terraform/language/meta-arguments/count) are set, use `each` and `count` objects to refer to other resources that are expanded in a chain. ```hcl variable "vpc_cidrs" { @@ -341,7 +388,7 @@ The selected AMI must be tagged with the Component value "nomad-server". ``` The `error_message` argument can be any expression that evaluates to a string. -This includes literal strings, heredocs, and template expressions. You can use the [`format` function](/language/functions/format) to convert items of `null`, `list`, or `map` types into a formatted string. Multi-line +This includes literal strings, heredocs, and template expressions. You can use the [`format` function](/terraform/language/functions/format) to convert items of `null`, `list`, or `map` types into a formatted string. Multi-line error messages are supported, and lines with leading whitespace will not be word wrapped. @@ -354,7 +401,7 @@ external values included in the condition expression. Terraform evaluates custom conditions as early as possible. -Input variable validations can only refer to the variable value, so Terraform always evaluates them immediately. When Terraform evaluates preconditions and postconditions depends on whether the value(s) associated with the condition are known before or after applying the configuration. +Input variable validations can only refer to the variable value, so Terraform always evaluates them immediately. Check assertions, preconditions, and postconditions depend on Terraform evaluating whether the value(s) associated with the condition are known before or after applying the configuration. - **Known before apply:** Terraform checks the condition during the planning phase. For example, Terraform can know the value of an image ID during planning as long as it is not generated from another resource. - **Known after apply:** Terraform delays checking that condition until the apply phase. For example, AWS only assigns the root volume ID when it starts an EC2 instance, so Terraform cannot know this value until apply. diff --git a/website/docs/language/expressions/dynamic-blocks.mdx b/website/docs/language/expressions/dynamic-blocks.mdx index 0ab3e3f595..ce30ce9549 100644 --- a/website/docs/language/expressions/dynamic-blocks.mdx +++ b/website/docs/language/expressions/dynamic-blocks.mdx @@ -30,7 +30,7 @@ special `dynamic` block type, which is supported inside `resource`, `data`, ```hcl resource "aws_elastic_beanstalk_environment" "tfenvtest" { name = "tf-test-name" - application = "${aws_elastic_beanstalk_application.tftest.name}" + application = aws_elastic_beanstalk_application.tftest.name solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6" dynamic "setting" { @@ -44,7 +44,7 @@ resource "aws_elastic_beanstalk_environment" "tfenvtest" { } ``` -A `dynamic` block acts much like a [`for` expression](/language/expressions/for), but produces +A `dynamic` block acts much like a [`for` expression](/terraform/language/expressions/for), but produces nested blocks instead of a complex typed value. It iterates over a given complex value, and generates a nested block for each element of that complex value. @@ -84,9 +84,9 @@ nested block. If you need to declare resource instances based on a nested data structure or combinations of elements from multiple data structures you can use Terraform expressions and functions to derive a suitable value. For some common examples of such situations, see the -[`flatten`](/language/functions/flatten) +[`flatten`](/terraform/language/functions/flatten) and -[`setproduct`](/language/functions/setproduct) +[`setproduct`](/terraform/language/functions/setproduct) functions. ## Multi-level Nested Block Structures @@ -151,5 +151,5 @@ nested blocks using directly-corresponding attributes from an input variable then that might suggest that your module is not creating a useful abstraction. It may be better for the calling module to define the resource itself then pass information about it into your module. For more information on this design -tradeoff, see [When to Write a Module](/language/modules/develop#when-to-write-a-module) -and [Module Composition](/language/modules/develop/composition). +tradeoff, see [When to Write a Module](/terraform/language/modules/develop#when-to-write-a-module) +and [Module Composition](/terraform/language/modules/develop/composition). diff --git a/website/docs/language/expressions/for.mdx b/website/docs/language/expressions/for.mdx index b59f805bd7..80a883ff54 100644 --- a/website/docs/language/expressions/for.mdx +++ b/website/docs/language/expressions/for.mdx @@ -86,7 +86,7 @@ A `for` expression can also include an optional `if` clause to filter elements from the source collection, producing a value with fewer elements than the source value: -``` +```hcl [for s in var.list : upper(s) if s != ""] ``` @@ -127,11 +127,7 @@ using lexical sorting. For sets of strings, Terraform sorts the elements by their value, using lexical sorting. -For sets of other types, Terraform uses an arbitrary ordering that may change -in future versions of Terraform. For that reason, we recommend converting the -result of such an expression to itself be a set so that it's clear elsewhere -in the configuration that the result is unordered. You can use -[the `toset` function](/language/functions/toset) +For sets of other types, Terraform uses an arbitrary ordering that may change in future versions. We recommend converting the expression result into a set to make it clear elsewhere in the configuration that the result is unordered. You can use [the `toset` function](/terraform/language/functions/toset) to concisely convert a `for` expression result to be of a set type. ```hcl @@ -208,4 +204,4 @@ Some resource types also define _nested block types_, which typically represent separate objects that belong to the containing resource in some way. You can't dynamically generate nested blocks using `for` expressions, but you _can_ generate nested blocks for a resource dynamically using -[`dynamic` blocks](/language/expressions/dynamic-blocks). +[`dynamic` blocks](/terraform/language/expressions/dynamic-blocks). diff --git a/website/docs/language/expressions/function-calls.mdx b/website/docs/language/expressions/function-calls.mdx index 844a603c4e..b12f667413 100644 --- a/website/docs/language/expressions/function-calls.mdx +++ b/website/docs/language/expressions/function-calls.mdx @@ -7,10 +7,10 @@ description: >- # Function Calls -> **Hands-on:** Try the [Perform Dynamic Operations with Functions](https://learn.hashicorp.com/tutorials/terraform/functions?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Perform Dynamic Operations with Functions](/terraform/tutorials/configuration-language/functions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. The Terraform language has a number of -[built-in functions](/language/functions) that can be used +[built-in functions](/terraform/language/functions) that can be used in expressions to transform and combine values. These are similar to the operators but all follow a common syntax: @@ -35,7 +35,7 @@ A function call expression evaluates to the function's return value. ## Available Functions For a full list of available functions, see -[the function reference](/language/functions). +[the function reference](/terraform/language/functions). ## Expanding Function Arguments @@ -52,8 +52,8 @@ The expansion symbol is three periods (`...`), not a Unicode ellipsis character ## Using Sensitive Data as Function Arguments -When using sensitive data, such as [an input variable](/language/values/variables#suppressing-values-in-cli-output) -or [an output defined](/language/values/outputs#sensitive-suppressing-values-in-cli-output) as sensitive +When using sensitive data, such as [an input variable](/terraform/language/values/variables#suppressing-values-in-cli-output) +or [an output defined](/terraform/language/values/outputs#sensitive-suppressing-values-in-cli-output) as sensitive as function arguments, the result of the function call will be marked as sensitive. This is a conservative behavior that is true irrespective of the function being @@ -63,11 +63,11 @@ the `keys()` function will result in a list that is sensitive: ```shell > local.baz { - "a" = (sensitive) + "a" = (sensitive value) "b" = "dog" } > keys(local.baz) -(sensitive) +(sensitive value) ``` ## When Terraform Calls Functions @@ -82,10 +82,10 @@ those it can be helpful to know when Terraform will call them in relation to other events that occur in a Terraform run. The small set of special functions includes -[`file`](/language/functions/file), -[`templatefile`](/language/functions/templatefile), -[`timestamp`](/language/functions/timestamp), -and [`uuid`](/language/functions/uuid). +[`file`](/terraform/language/functions/file), +[`templatefile`](/terraform/language/functions/templatefile), +[`timestamp`](/terraform/language/functions/timestamp), +and [`uuid`](/terraform/language/functions/uuid). If you are not working with these functions then you don't need to read this section, although the information here may still be interesting background information. @@ -104,7 +104,7 @@ both cause the final configuration during the apply step not to match the actions shown in the plan, which violates the Terraform execution model. For that reason, Terraform arranges for both of those functions to produce -[unknown value](/language/expressions/references#values-not-yet-known) results during the +[unknown value](/terraform/language/expressions/references#values-not-yet-known) results during the plan step, with the real result being decided only during the apply step. For `timestamp` in particular, this means that the recorded time will be the instant when Terraform began applying the change, rather than when diff --git a/website/docs/language/expressions/index.mdx b/website/docs/language/expressions/index.mdx index 8dff315093..8817589cd8 100644 --- a/website/docs/language/expressions/index.mdx +++ b/website/docs/language/expressions/index.mdx @@ -7,9 +7,9 @@ description: >- # Expressions -> **Hands-on:** Try the [Create Dynamic Expressions](https://learn.hashicorp.com/tutorials/terraform/expressions?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Create Dynamic Expressions](/terraform/tutorials/configuration-language/expressions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -_Expressions_ are used to refer to or compute values within a configuration. +_Expressions_ refer to or compute values within a configuration. The simplest expressions are just literal values, like `"hello"` or `5`, but the Terraform language also allows more complex expressions such as references to data exported by resources, arithmetic, conditional evaluation, @@ -18,54 +18,54 @@ and a number of built-in functions. Expressions can be used in a number of places in the Terraform language, but some contexts limit which expression constructs are allowed, such as requiring a literal value of a particular type or forbidding -[references to resource attributes](/language/expressions/references#references-to-resource-attributes). +[references to resource attributes](/terraform/language/expressions/references#references-to-resource-attributes). Each language feature's documentation describes any restrictions it places on expressions. You can experiment with the behavior of Terraform's expressions from the Terraform expression console, by running -[the `terraform console` command](/cli/commands/console). +[the `terraform console` command](/terraform/cli/commands/console). The other pages in this section describe the features of Terraform's expression syntax. -- [Types and Values](/language/expressions/types) +- [Types and Values](/terraform/language/expressions/types) documents the data types that Terraform expressions can resolve to, and the literal syntaxes for values of those types. -- [Strings and Templates](/language/expressions/strings) +- [Strings and Templates](/terraform/language/expressions/strings) documents the syntaxes for string literals, including interpolation sequences and template directives. -- [References to Values](/language/expressions/references) +- [References to Values](/terraform/language/expressions/references) documents how to refer to named values like variables and resource attributes. -- [Operators](/language/expressions/operators) +- [Operators](/terraform/language/expressions/operators) documents the arithmetic, comparison, and logical operators. -- [Function Calls](/language/expressions/function-calls) +- [Function Calls](/terraform/language/expressions/function-calls) documents the syntax for calling Terraform's built-in functions. -- [Conditional Expressions](/language/expressions/conditionals) +- [Conditional Expressions](/terraform/language/expressions/conditionals) documents the ` ? : ` expression, which chooses between two values based on a bool condition. -- [For Expressions](/language/expressions/for) +- [For Expressions](/terraform/language/expressions/for) documents expressions like `[for s in var.list : upper(s)]`, which can transform a complex type value into another complex type value. -- [Splat Expressions](/language/expressions/splat) +- [Splat Expressions](/terraform/language/expressions/splat) documents expressions like `var.list[*].id`, which can extract simpler collections from more complicated expressions. -- [Dynamic Blocks](/language/expressions/dynamic-blocks) +- [Dynamic Blocks](/terraform/language/expressions/dynamic-blocks) documents a way to create multiple repeatable nested blocks within a resource or other construct. -- [Type Constraints](/language/expressions/type-constraints) +- [Type Constraints](/terraform/language/expressions/type-constraints) documents the syntax for referring to a type, rather than a value of that type. Input variables expect this syntax in their `type` argument. -- [Version Constraints](/language/expressions/version-constraints) +- [Version Constraints](/terraform/language/expressions/version-constraints) documents the syntax of special strings that define a set of allowed software versions. Terraform uses version constraints in several places. diff --git a/website/docs/language/expressions/operators.mdx b/website/docs/language/expressions/operators.mdx index cb177e8cff..4c77aca58f 100644 --- a/website/docs/language/expressions/operators.mdx +++ b/website/docs/language/expressions/operators.mdx @@ -42,6 +42,8 @@ given values to be of a particular type. Terraform will attempt to convert values to the required type automatically, or will produce an error message if automatic conversion is impossible. +The `?` character combined with the `:` character is part of a conditional expression in Terraform and is not considered an operator. For more information, refer to [Conditional Expressions](/terraform/language/expressions/conditionals#conditional-expressions). + ## Arithmetic Operators The arithmetic operators all expect number values and produce number values @@ -56,9 +58,9 @@ as results: * `-a` returns the result of multiplying `a` by `-1`. Terraform supports some other less-common numeric operations as -[functions](/language/expressions/function-calls). For example, you can calculate exponents +[functions](/terraform/language/expressions/function-calls). For example, you can calculate exponents using -[the `pow` function](/language/functions/pow). +[the `pow` function](/terraform/language/functions/pow). ## Equality Operators @@ -103,5 +105,3 @@ The logical operators all expect bool values and produce bool values as results. Terraform does not have an operator for the "exclusive OR" operation. If you know that both operators are boolean values then exclusive OR is equivalent to the `!=` ("not equal") operator. - -The logical operators in Terraform do not short-circuit, meaning `var.foo || var.foo.bar` will produce an error message if `var.foo` is `null` because both `var.foo` and `var.foo.bar` are evaluated. diff --git a/website/docs/language/expressions/references.mdx b/website/docs/language/expressions/references.mdx index 75b3bc41bb..f3febc16e0 100644 --- a/website/docs/language/expressions/references.mdx +++ b/website/docs/language/expressions/references.mdx @@ -8,10 +8,10 @@ description: >- # References to Named Values -> **Hands-on:** Try the [Create Dynamic Expressions](https://learn.hashicorp.com/tutorials/terraform/expressions?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Create Dynamic Expressions](/terraform/tutorials/configuration-language/expressions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. Terraform makes several kinds of named values available. Each of these names is -an expression that references the associated value; you can use them as +an expression that references the associated value. You can use them as standalone expressions, or combine them with other expressions to compute new values. @@ -30,7 +30,7 @@ The main kinds of named values available in Terraform are: The sections below explain each kind of named value in detail. Although many of these names use dot-separated paths that resemble -[attribute notation](/language/expressions/types#indices-and-attributes) for elements of object values, they are not +[attribute notation](/terraform/language/expressions/types#indices-and-attributes) for elements of object values, they are not implemented as real objects. This means you must use them exactly as written: you cannot use square-bracket notation to replace the dot-separated paths, and you cannot iterate over the "parent object" of a named entity; for example, you @@ -39,7 +39,7 @@ instance resource. ### Resources -`.` represents a [managed resource](/language/resources) of +`.` represents a [managed resource](/terraform/language/resources) of the given type and name. The value of a resource reference can vary, depending on whether the resource @@ -47,7 +47,7 @@ uses `count` or `for_each`: * If the resource doesn't use `count` or `for_each`, the reference's value is an object. The resource's attributes are elements of the object, and you can - access them using [dot or square bracket notation](/language/expressions/types#indices-and-attributes). + access them using [dot or square bracket notation](/terraform/language/expressions/types#indices-and-attributes). * If the resource has the `count` argument set, the reference's value is a _list_ of objects representing its instances. * If the resource has the `for_each` argument set, the reference's value is a @@ -61,7 +61,7 @@ For more information about how to use resource references, see ### Input Variables -`var.` is the value of the [input variable](/language/values/variables) of the given name. +`var.` is the value of the [input variable](/terraform/language/values/variables) of the given name. If the variable has a type constraint (`type` argument) as part of its declaration, Terraform will automatically convert the caller's given value @@ -79,7 +79,7 @@ constraint all of the attributes you intend to use elsewhere in your module. ### Local Values -`local.` is the value of the [local value](/language/values/locals) of the given name. +`local.` is the value of the [local value](/terraform/language/values/locals) of the given name. Local values can refer to other local values, even within the same `locals` block, as long as you don't introduce circular dependencies. @@ -87,12 +87,12 @@ block, as long as you don't introduce circular dependencies. ### Child Module Outputs `module.` is an value representing the results of -[a `module` block](/language/modules/syntax). +[a `module` block](/terraform/language/modules/syntax). If the corresponding `module` block does not have either `count` nor `for_each` set then the value will be an object with one attribute for each output value defined in the child module. To access one of the module's -[output values](/language/values/outputs), use `module..`. +[output values](/terraform/language/values/outputs), use `module..`. If the corresponding `module` uses `for_each` then the value will be a map of objects whose keys correspond with the keys in the `for_each` expression, @@ -106,7 +106,7 @@ elements, each one representing one module instance. ### Data Sources `data..` is an object representing a -[data resource](/language/data-sources) of the given data +[data resource](/terraform/language/data-sources) of the given data source type and name. If the resource has the `count` argument set, the value is a list of objects representing its instances. If the resource has the `for_each` argument set, the value is a map of objects representing its instances. @@ -134,7 +134,7 @@ The following values are available: directory. We recommend using `path.root` or `path.module` over `path.cwd` where possible. - `terraform.workspace` is the name of the currently selected - [workspace](/language/state/workspaces). + [workspace](/terraform/language/state/workspaces). Use the values in this section carefully, because they include information about the context in which a configuration is being applied and so may @@ -174,19 +174,19 @@ These local names are described in the documentation for the specific contexts where they appear. Some of most common local names are: * `count.index`, in resources that use - [the `count` meta-argument](/language/meta-arguments/count). + [the `count` meta-argument](/terraform/language/meta-arguments/count). * `each.key` / `each.value`, in resources that use - [the `for_each` meta-argument](/language/meta-arguments/for_each). -* `self`, in [provisioner](/language/resources/provisioners/syntax) and - [connection](/language/resources/provisioners/connection) blocks. + [the `for_each` meta-argument](/terraform/language/meta-arguments/for_each). +* `self`, in [provisioner](/terraform/language/resources/provisioners/syntax) and + [connection](/terraform/language/resources/provisioners/connection) blocks. -> **Note:** Local names are often referred to as _variables_ or _temporary variables_ in their documentation. These are not [input -variables](/language/values/variables); they are just arbitrary names +variables](/terraform/language/values/variables); they are just arbitrary names that temporarily represent a value. The names in this section relate to top-level configuration blocks only. -If you use [`dynamic` blocks](/language/expressions/dynamic-blocks) to dynamically generate +If you use [`dynamic` blocks](/terraform/language/expressions/dynamic-blocks) to dynamically generate resource-type-specific _nested_ blocks within `resource` and `data` blocks then you'll refer to the key and value of each element differently. See the `dynamic` blocks documentation for details. @@ -235,7 +235,7 @@ for use in references, as follows: * The `id` attribute exported by this resource type can be read using the same syntax, giving `aws_instance.example.id`. * The arguments of the `ebs_block_device` nested blocks can be accessed using - a [splat expression](/language/expressions/splat). For example, to obtain a list of + a [splat expression](/terraform/language/expressions/splat). For example, to obtain a list of all of the `device_name` values, use `aws_instance.example.ebs_block_device[*].device_name`. * The nested blocks in this particular resource type do not have any exported @@ -261,24 +261,24 @@ for use in references, as follows: as `aws_instance.example.device["foo"].size`. To obtain a map of values of a particular argument for _labelled_ nested - block types, use a [`for` expression](/language/expressions/for): + block types, use a [`for` expression](/terraform/language/expressions/for): `{for k, device in aws_instance.example.device : k => device.size}`. When a resource has the -[`count`](/language/meta-arguments/count) +[`count`](/terraform/language/meta-arguments/count) argument set, the resource itself becomes a _list_ of instance objects rather than a single object. In that case, access the attributes of the instances using -either [splat expressions](/language/expressions/splat) or index syntax: +either [splat expressions](/terraform/language/expressions/splat) or index syntax: * `aws_instance.example[*].id` returns a list of all of the ids of each of the instances. * `aws_instance.example[0].id` returns just the id of the first instance. When a resource has the -[`for_each`](/language/meta-arguments/for_each) +[`for_each`](/terraform/language/meta-arguments/for_each) argument set, the resource itself becomes a _map_ of instance objects rather than a single object, and attributes of instances must be specified by key, or can -be accessed using a [`for` expression](/language/expressions/for). +be accessed using a [`for` expression](/terraform/language/expressions/for). * `aws_instance.example["a"].id` returns the id of the "a"-keyed resource. * `[for value in aws_instance.example: value.id]` returns a list of all of the ids @@ -292,25 +292,25 @@ Note that unlike `count`, splat expressions are _not_ directly applicable to res When defining the schema for a resource type, a provider developer can mark certain attributes as _sensitive_, in which case Terraform will show a -placeholder marker `(sensitive)` instead of the actual value when rendering +placeholder marker `(sensitive value)` instead of the actual value when rendering a plan involving that attribute. -A provider attribute marked as sensitive behaves similarly to an -[an input variable declared as sensitive](/language/values/variables#suppressing-values-in-cli-output), +A provider attribute marked as sensitive behaves similarly to +[an input variable declared as sensitive](/terraform/language/values/variables#suppressing-values-in-cli-output), where Terraform will hide the value in the plan and apply messages and will also hide any other values you derive from it as sensitive. However, there are some limitations to that behavior as described in -[Cases where Terraform may disclose a sensitive variable](/language/values/variables#cases-where-terraform-may-disclose-a-sensitive-variable). +[Cases where Terraform may disclose a sensitive variable](/terraform/language/values/variables#cases-where-terraform-may-disclose-a-sensitive-variable). If you use a sensitive value from a resource attribute as part of an -[output value](/language/values/outputs) then Terraform will require +[output value](/terraform/language/values/outputs) then Terraform will require you to also mark the output value itself as sensitive, to confirm that you intended to export it. -Terraform will still record sensitive values in the [state](/language/state), +Terraform will still record sensitive values in the [state](/terraform/language/state), and so anyone who can access the state data will have access to the sensitive values in cleartext. For more information, see -[_Sensitive Data in State_](/language/state/sensitive-data). +[_Sensitive Data in State_](/terraform/language/state/sensitive-data). -> **Note:** Treating values derived from a sensitive resource attribute as sensitive themselves was introduced in Terraform v0.15. Earlier versions of @@ -326,12 +326,10 @@ values are decided dynamically by the remote system. For example, if a particular remote object type is assigned a generated unique id on creation, Terraform cannot predict the value of this id until the object has been created. -To allow expressions to still be evaluated during the plan phase, Terraform -uses special "unknown value" placeholders for these results. In most cases you -don't need to do anything special to deal with these, since the Terraform -language automatically handles unknown values during expressions, so that -for example adding a known value to an unknown value automatically produces -an unknown value as the result. +Terraform uses special unknown value placeholders for information that it +cannot predict during the plan phase. The Terraform language automatically +handles unknown values in expressions. For example, adding a known value to an +unknown value automatically produces an unknown value as a result. However, there are some situations where unknown values _do_ have a significant effect: @@ -357,4 +355,4 @@ effect: types where possible, but incorrect use of such values may not be detected until the apply phase, causing the apply to fail. -Unknown values appear in the `terraform plan` output as `(not yet known)`. +Unknown values appear in the `terraform plan` output as `(known after apply)`. diff --git a/website/docs/language/expressions/splat.mdx b/website/docs/language/expressions/splat.mdx index 36c0fdf5c5..e7d246253e 100644 --- a/website/docs/language/expressions/splat.mdx +++ b/website/docs/language/expressions/splat.mdx @@ -7,7 +7,7 @@ description: >- # Splat Expressions -> **Hands-on:** Try the [Create Dynamic Expressions](https://learn.hashicorp.com/tutorials/terraform/expressions?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Create Dynamic Expressions](/terraform/tutorials/configuration-language/expressions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. A _splat expression_ provides a more concise way to express a common operation that could otherwise be performed with a `for` expression. @@ -45,12 +45,12 @@ The above expression is equivalent to the following `for` expression: The splat expression patterns shown above apply only to lists, sets, and tuples. To get a similar result with a map or object value you must use -[`for` expressions](/language/expressions/for). +[`for` expressions](/terraform/language/expressions/for). Resources that use the `for_each` argument will appear in expressions as a map of objects, so you can't use splat expressions with those resources. For more information, see -[Referring to Resource Instances](/language/meta-arguments/for_each#referring-to-instances). +[Referring to Resource Instances](/terraform/language/meta-arguments/for_each#referring-to-instances). ## Single Values as Lists @@ -65,7 +65,7 @@ empty tuple. This special behavior can be useful for modules that accept optional input variables whose default value is `null` to represent the absence of any value. This allows the module to adapt the variable value for Terraform language features designed to work with collections. For example: -``` +```hcl variable "website_setting" { type = object({ index_document = string @@ -87,7 +87,7 @@ resource "aws_s3_bucket" "example" { } ``` -The above example uses a [`dynamic` block](/language/expressions/dynamic-blocks), which +The above example uses a [`dynamic` block](/terraform/language/expressions/dynamic-blocks), which generates zero or more nested blocks based on a collection value. The input variable `var.website_setting` is defined as a single object that might be null, so the `dynamic` block's `for_each` expression uses `[*]` to ensure that diff --git a/website/docs/language/expressions/strings.mdx b/website/docs/language/expressions/strings.mdx index aead7f2313..99eb6775d2 100644 --- a/website/docs/language/expressions/strings.mdx +++ b/website/docs/language/expressions/strings.mdx @@ -77,8 +77,8 @@ allowed, but conventionally this identifier is in all-uppercase and begins with ### Generating JSON or YAML Don't use "heredoc" strings to generate JSON or YAML. Instead, use -[the `jsonencode` function](/language/functions/jsonencode) or -[the `yamlencode` function](/language/functions/yamlencode) so that Terraform +[the `jsonencode` function](/terraform/language/functions/jsonencode) or +[the `yamlencode` function](/terraform/language/functions/yamlencode) so that Terraform can be responsible for guaranteeing valid JSON or YAML syntax. ```hcl @@ -180,7 +180,7 @@ The following directives are supported: ```hcl < **Note:** Although colons are valid delimiters between keys and values, `terraform fmt` ignores them. In contrast, `terraform fmt` attempts to vertically align equals signs. + * `set(...)`: a collection of unique values that do not have any secondary identifiers or ordering. @@ -153,7 +154,7 @@ like the following: The Terraform language has literal expressions for creating tuple and object values, which are described in -[Expressions: Literal Expressions](/language/expressions/types#literal-expressions) as +[Expressions: Literal Expressions](/terraform/language/expressions/types#literal-expressions) as "list/tuple" literals and "map/object" literals, respectively. Terraform does _not_ provide any way to directly represent lists, maps, or sets. @@ -209,18 +210,56 @@ will raise a type mismatch error, since a tuple cannot be converted to a string. ## Dynamic Types: The "any" Constraint +~> **Warning:** `any` is very rarely the correct type constraint to use. +**Do not use `any` just to avoid specifying a type constraint**. Always write an +exact type constraint unless you are truly handling dynamic data. + The keyword `any` is a special construct that serves as a placeholder for a type yet to be decided. `any` is not _itself_ a type: when interpreting a value against a type constraint containing `any`, Terraform will attempt to find a single actual type that could replace the `any` keyword to produce a valid result. +The only situation where it's appropriate to use `any` is if you will pass +the given value directly to some other system without directly accessing its +contents. For example, it's okay to use a variable of type `any` if you use +it only with `jsonencode` to pass the full value directly to a resource, as +shown in the following example: + +```hcl +variable "settings" { + type = any +} + +resource "aws_s3_object" "example" { + # ... + + # This is a reasonable use of "any" because this module + # just writes any given data to S3 as JSON, without + # inspecting it further or applying any constraints + # to its type or value. + content = jsonencode(var.settings) +} +``` + +If any part of your module accesses elements or attributes of the value, or +expects it to be a string or number, or any other non-opaque treatment, it +is _incorrect_ to use `any`. Write the exact type that your module is expecting +instead. + +### `any` with Collection Types + +All of the elements of a collection must have the same type, so if you use +`any` as the placeholder for the element type of a collection then Terraform +will attempt to find a single exact element type to use for the resulting +collection. + For example, given the type constraint `list(any)`, Terraform will examine the given value and try to choose a replacement for the `any` that would make the result valid. If the given value were `["a", "b", "c"]` -- whose physical type is -`tuple([string, string, string])`, Terraform analyzes this as follows: +`tuple([string, string, string])` -- Terraform analyzes this as follows: * Tuple types and list types are _similar_ per the previous section, so the tuple-to-list conversion rule applies. @@ -229,10 +268,9 @@ If the given value were `["a", "b", "c"]` -- whose physical type is * Therefore in this case the `any` argument is replaced with `string`, and the final concrete value type is `list(string)`. -All of the elements of a collection must have the same type, so conversion -to `list(any)` requires that all of the given elements must be convertible -to a common type. This implies some other behaviors that result from the -conversion rules described in earlier sections. +If the elements of the given tuple are not all of the same type then Terraform +will attempt to find a single type that they can all convert to. Terraform +will consider various conversion rules as described in earlier sections. * If the given value were instead `["a", 1, "b"]` then Terraform would still select `list(string)`, because of the primitive type conversion rules, and @@ -246,22 +284,8 @@ conversion rules described in earlier sections. Although the above examples use `list(any)`, a similar principle applies to `map(any)` and `set(any)`. -If you wish to apply absolutely no constraint to the given value, the `any` -keyword can be used in isolation: - -```hcl -variable "no_type_constraint" { - type = any -} -``` - -In this case, Terraform will replace `any` with the exact type of the given -value and thus perform no type conversion whatsoever. - ## Optional Object Type Attributes --> **Note:** Optional object type attributes are supported only in Terraform v1.3 and later. - Terraform typically returns an error when it does not receive a value for specified object attributes. When you mark an attribute as optional, Terraform instead inserts a default value for the missing attribute. This allows the receiving module to describe an appropriate fallback behavior. To mark attributes as optional, use the `optional` modifier in the object type constraint. The following example creates optional attribute `b` and optional attribute with a default value `c`. diff --git a/website/docs/language/expressions/types.mdx b/website/docs/language/expressions/types.mdx index 0d49007bb4..a93fd7abc6 100644 --- a/website/docs/language/expressions/types.mdx +++ b/website/docs/language/expressions/types.mdx @@ -21,13 +21,16 @@ The Terraform language uses the following types for its values: numbers like `15` and fractional values like `6.283185`. * `bool`: a boolean value, either `true` or `false`. `bool` values can be used in conditional logic. -* `list` (or `tuple`): a sequence of values, like - `["us-west-1a", "us-west-1c"]`. Elements in a list or tuple are identified by - consecutive whole numbers, starting with zero. +* `list` (or `tuple`): a sequence of values, like `["us-west-1a", "us-west-1c"]`. Identify elements in a list with consecutive whole numbers, starting with zero. +* `set`: a collection of unique values that do not have any secondary identifiers or ordering. * `map` (or `object`): a group of values identified by named labels, like `{name = "Mabel", age = 52}`. -Strings, numbers, and bools are sometimes called _primitive types._ Lists/tuples and maps/objects are sometimes called _complex types,_ _structural types,_ or _collection types._ +Strings, numbers, and bools are sometimes called _primitive types._ +Lists/tuples and maps/objects are sometimes called _complex types,_ _structural +types,_ or _collection types._ See +[Type Constraints](/terraform/language/expressions/type-constraints) for a more detailed +description of complex types. Finally, there is one special value that has _no_ type: @@ -51,7 +54,7 @@ characters, `"like this"`. There is also a "heredoc" syntax for more complex strings. String literals are the most complex kind of literal expression in -Terraform, and have their own page of documentation. See [Strings](/language/expressions/strings) +Terraform, and have their own page of documentation. See [Strings](/terraform/language/expressions/strings) for information about escape sequences, the heredoc syntax, interpolation, and template directives. @@ -77,6 +80,41 @@ List literals can be split into multiple lines for readability, but always require a comma between values. A comma after the final value is allowed, but not required. Values in a list can be arbitrary expressions. +Lists and tuples each have different constraints on the types they allow. For more information on the types that each allows, refer to [tuples](/terraform/language/expressions/type-constraints#tuple) and [lists](/terraform/language/expressions/type-constraints#list). + +### Sets + +Terraform does not support directly accessing elements of a set by index because sets are unordered collections. To access elements in a set by index, first convert the set to a list. + + +1. Define a set. The following example specifies a set name `example_set`: + + ```hcl + variable "example_set" { + type = set(string) + default = ["foo", "bar"] + } + ``` + +1. Use the `tolist` function to convert the set to a list. The following example stores the converted list as a local variable called `example_list`: + + ```hcl + locals { + example_list = tolist(var.example_set) + } + ``` + +1. You can then reference an element in the list: + + ```hcl + output "first_element" { + value = local.example_list[0] + } + output "second_element" { + value = local.example_list[1] + } + ``` + ### Maps/Objects Maps/objects are represented by a pair of curly braces containing a series of @@ -95,7 +133,7 @@ The values in a map can be arbitrary expressions. The keys in a map must be strings; they can be left unquoted if -they are a valid [identifier](/language/syntax/configuration#identifiers), but must be quoted +they are a valid [identifier](/terraform/language/syntax/configuration#identifiers), but must be quoted otherwise. You can use a non-literal string expression as a key by wrapping it in parentheses, like `(var.business_unit_tag_name) = "SRE"`. @@ -126,7 +164,7 @@ offer different ways to restrict the allowed values for input variables and resource arguments. For complete details about these types (and an explanation of why the difference -usually doesn't matter), see [Type Constraints](/language/expressions/type-constraints). +usually doesn't matter), see [Type Constraints](/terraform/language/expressions/type-constraints). ## Type Conversion diff --git a/website/docs/language/expressions/version-constraints.mdx b/website/docs/language/expressions/version-constraints.mdx index 0537bec33f..9aa3c9217e 100644 --- a/website/docs/language/expressions/version-constraints.mdx +++ b/website/docs/language/expressions/version-constraints.mdx @@ -5,87 +5,89 @@ description: >- providers, and Terraform itself. Learn version constraint syntax and behavior. --- -# Version Constraints +# Version constraints -Anywhere that Terraform lets you specify a range of acceptable versions for -something, it expects a specially formatted string known as a version -constraint. Version constraints are used when configuring: +This topic provides reference information about the version constraints syntax in Terraform configuration language. -- [Modules](/language/modules) -- [Provider requirements](/language/providers/requirements) -- [The `required_version` setting](/language/settings#specifying-a-required-terraform-version) in the `terraform` block. +## Introduction -## Version Constraint Syntax +Terraform lets you specify a range of acceptable versions for +components you define in the configuration. Terraform expects a specially-formatted string to constrain the versions of the component. You can specify version constraints when configuring the following components: -Terraform's syntax for version constraints is very similar to the syntax used by -other dependency management systems like Bundler and NPM. +- [Modules](/terraform/language/modules) +- [Provider requirements](/terraform/language/providers/requirements) +- [The `required_version` setting](/terraform/language/terraform#terraform-required_version) in the `terraform` block. + +## Version constraint syntax + +A version constraint is a [string literal](/terraform/language/expressions/strings) +containing one or more conditions separated by commas. + +Each condition consists of an operator and a version number. + +Version numbers are a series of numbers separated by periods, for example `1.2.0`. It is optional, but you can include a suffix to indicate a beta release. Refer to [Specify a pre-release version](#specify-a-pre-release-version) for additional information. + +Use the following syntax to specify version constraints: + +```hcl +version = " " +``` + +In the following example, Terraform installs a versions `1.2.0` and newer, as well as version older than `2.0.0`: ```hcl version = ">= 1.2.0, < 2.0.0" ``` -A version constraint is a [string literal](/language/expressions/strings) -containing one or more conditions, which are separated by commas. +## Operators -Each condition consists of an operator and a version number. +The following table describes the operators you can use to configure version constraints: -Version numbers should be a series of numbers separated by periods (like -`1.2.0`), optionally with a suffix to indicate a beta release. +| Operator | Description | +| --- | --- | +| `=`,
no operator | Allows only one exact version number. Cannot be combined with other conditions. | +| `!=` | Excludes an exact version number. | +| `>`,
`>=`,
`<`,
`<=` | Compares to a specified version. Terraform allows versions that resolve to `true`. The `>` and `>=` operators request newer versions. The `<` and `<=` operators request older versions. | +| `~>` | Allows only the right-most version component to increment. Examples:
  • `~> 1.0.4`: Allows Terraform to install `1.0.5` and `1.0.10` but not `1.1.0`.
  • `~> 1.1`: Allows Terraform to install `1.2` and `1.10` but not `2.0`.
| -The following operators are valid: +## Version constraint behavior -- `=` (or no operator): Allows only one exact version number. Cannot be combined - with other conditions. - -- `!=`: Excludes an exact version number. - -- `>`, `>=`, `<`, `<=`: Comparisons against a specified version, allowing - versions for which the comparison is true. "Greater-than" requests newer - versions, and "less-than" requests older versions. - -- `~>`: Allows only the _rightmost_ version component to increment. For example, - to allow new patch releases within a specific minor release, use the full - version number: `~> 1.0.4` will allow installation of `1.0.5` and `1.0.10` - but not `1.1.0`. This is usually called the pessimistic constraint operator. - -## Version Constraint Behavior - -A version number that meets every applicable constraint is considered acceptable. +Terraform uses versions that meet all applicable constraints. Terraform consults version constraints to determine whether it has acceptable versions of itself, any required provider plugins, and any required modules. For -plugins and modules, it will use the newest installed version that meets the +plugins and modules, Terraform uses the newest installed version that meets the applicable constraints. -If Terraform doesn't have an acceptable version of a required plugin or module, -it will attempt to download the newest version that meets the applicable +When Terraform does not have an acceptable version of a required plugin or module, +it attempts to download the newest version that meets the applicable constraints. -If Terraform isn't able to obtain acceptable versions of external dependencies, -or if it doesn't have an acceptable version of itself, it won't proceed with any -plans, applies, or state manipulation actions. +When Terraform is unable to obtain acceptable versions of external dependencies +or if it does not have an acceptable version of itself, then it does not proceed with any +`terraform plan`, `terraform apply`, or `terraform state` operations. -Both the root module and any child module can constrain the acceptable versions -of Terraform and any providers they use. Terraform considers these constraints -equal, and will only proceed if all of them can be met. +The root module and any child modules can constrain the Terraform version and any provider versions the modules use. Terraform considers these constraints +equal, and only proceeds if all are met. -A prerelease version is a version number that contains a suffix introduced by -a dash, like `1.2.0-beta`. A prerelease version can be selected only by an -_exact_ version constraint (the `=` operator or no operator). Prerelease -versions do not match inexact operators such as `>=`, `~>`, etc. +### Specify a pre-release version -## Best Practices +A pre-release version is a version number that contains a suffix introduced by +a dash, for example `1.2.0-beta`. To configure Terraform to select a pre-release version, set the exact version number using the `=` operator. You can also omit the operator and specify the exact pre-release version. Terraform does not match pre-release versions on `>`, `>=`, `<`, `<=`, or `~>` operators. -### Module Versions +## Best practices -- When depending on third-party modules, require specific versions to ensure - that updates only happen when convenient to you. +We recommend implementing the following best practices when configuration version constraints. -- For modules maintained within your organization, specifying version ranges - may be appropriate if semantic versioning is used consistently or if there is - a well-defined release process that avoids unwanted updates. +### Module versions -### Terraform Core and Provider Versions +- Require specific versions to ensure that updates only happen when convenient to you when your infrastructure depends on third-party modules. + +- Specify version ranges when your organization consistently uses semantic versioning for modules it maintains. + +- Specify version ranges when your organization follows a well-defined release process that avoids unwanted updates. + +### Terraform core and provider versions - Reusable modules should constrain only their minimum allowed versions of Terraform and providers, such as `>= 0.12.0`. This helps avoid known diff --git a/website/docs/language/files/dependency-lock.mdx b/website/docs/language/files/dependency-lock.mdx index d439acc08b..99aeb2bcdb 100644 --- a/website/docs/language/files/dependency-lock.mdx +++ b/website/docs/language/files/dependency-lock.mdx @@ -11,14 +11,14 @@ description: >- versions of Terraform did not track dependency selections at all, so the information here is not relevant to those versions. -> **Hands-on:** Try the [Lock and Upgrade Provider Versions](https://learn.hashicorp.com/tutorials/terraform/provider-versioning?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Lock and Upgrade Provider Versions](/terraform/tutorials/configuration-language/provider-versioning?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. A Terraform configuration may refer to two different kinds of external dependency that come from outside of its own codebase: -- [Providers](/language/providers/requirements), which are plugins for Terraform +- [Providers](/terraform/language/providers/requirements), which are plugins for Terraform that extend it with support for interacting with various external systems. -- [Modules](/language/modules), which allow +- [Modules](/terraform/language/modules), which allow splitting out groups of Terraform configuration constructs (written in the Terraform language) into reusable abstractions. @@ -28,7 +28,7 @@ reason, Terraform must determine which versions of those dependencies are potentially compatible with the current configuration and which versions are currently selected for use. -[Version constraints](/language/expressions/version-constraints) within the configuration +[Version constraints](/terraform/language/expressions/version-constraints) within the configuration itself determine which versions of dependencies are _potentially_ compatible, but after selecting a specific version of each dependency Terraform remembers the decisions it made in a _dependency lock file_ so that it can (by default) @@ -53,7 +53,7 @@ to signify that it is a lock file for various items that Terraform caches in the `.terraform` subdirectory of your working directory. Terraform automatically creates or updates the dependency lock file each time -you run [the `terraform init` command](/cli/commands/init). You should +you run [the `terraform init` command](/terraform/cli/commands/init). You should include this file in your version control repository so that you can discuss potential changes to your external dependencies via code review, just as you would discuss potential changes to your configuration itself. @@ -141,7 +141,7 @@ There are two special considerations with the "trust on first use" model: To avoid this problem you can pre-populate checksums for a variety of different platforms in your lock file using - [the `terraform providers lock` command](/cli/commands/providers/lock), + [the `terraform providers lock` command](/terraform/cli/commands/providers/lock), which will then allow future calls to `terraform init` to verify that the packages available in your chosen mirror match the official packages from the provider's origin registry. @@ -159,7 +159,7 @@ proposed changes. The following sections will describe these common situations. ### Dependency on a new provider If you add a new entry to the -[provider requirements](/language/providers/requirements) for any module in your +[provider requirements](/terraform/language/providers/requirements) for any module in your configuration, or if you add an external module that includes a new provider dependency itself, `terraform init` will respond to that by selecting the newest version of that provider which meets all of the version constraints @@ -306,7 +306,7 @@ The two hashing schemes currently supported are: packages indexed in the origin registry. This is an effective scheme for verifying the official release packages when installed from a registry, but it's not suitable for verifying packages that come from other - [provider installation methods](/cli/config/config-file#provider-installation), + [provider installation methods](/terraform/cli/config/config-file#provider-installation), such as filesystem mirrors using the unpacked directory layout. - `h1:`: a mnemonic for "hash scheme 1", which is the current preferred hashing @@ -347,7 +347,7 @@ your configuration on new target platforms, or if you are installing providers from a mirror that therefore can't provide official signed checksums, you can ask Terraform to pre-populate hashes for a chosen set of platforms using -[the `terraform providers lock` command](/cli/commands/providers/lock): +[the `terraform providers lock` command](/terraform/cli/commands/providers/lock): ``` terraform providers lock \ diff --git a/website/docs/language/files/index.mdx b/website/docs/language/files/index.mdx index b5808a013b..5e861a8c55 100644 --- a/website/docs/language/files/index.mdx +++ b/website/docs/language/files/index.mdx @@ -11,7 +11,7 @@ description: >- Code in the Terraform language is stored in plain text files with the `.tf` file extension. There is also -[a JSON-based variant of the language](/language/syntax/json) that is named with +[a JSON-based variant of the language](/terraform/language/syntax/json) that is named with the `.tf.json` file extension. Files containing Terraform code are often called _configuration files._ @@ -36,7 +36,7 @@ treating the entire module as a single document. Separating various blocks into different files is purely for the convenience of readers and maintainers, and has no effect on the module's behavior. -A Terraform module can use [module calls](/language/modules) to +A Terraform module can use [module calls](/terraform/language/modules) to explicitly include other modules into the configuration. These child modules can come from local directories (nested in the parent module's directory, or anywhere else on disk), or from external sources like the @@ -51,8 +51,8 @@ called by those modules, etc.). - In Terraform CLI, the root module is the working directory where Terraform is invoked. (You can use command line options to specify a root module outside - the working directory, but in practice this is rare. ) -- In Terraform Cloud and Terraform Enterprise, the root module for a workspace + the working directory, but in practice this is rare.) +- In HCP Terraform and Terraform Enterprise, the root module for a workspace defaults to the top level of the configuration directory (supplied via version control repository or direct upload), but the workspace settings can specify a subdirectory to use instead. diff --git a/website/docs/language/files/tests.mdx b/website/docs/language/files/tests.mdx new file mode 100644 index 0000000000..370fdc4f8d --- /dev/null +++ b/website/docs/language/files/tests.mdx @@ -0,0 +1,21 @@ +--- +page_title: Test Files - Configuration Language +description: >- + Learn how to write test files for validating your configuration and modules. +--- + +# Test Files + +Specific Terraform commands, such as `test`, `init`, and `validate`, load Terraform test files for your configuration. + +Test files contain specifications for Terraform test executions. For more information about the Terraform `test` command, refer to [Command: `test`](/terraform/cli/commands/test). For more information about the syntax and Terraform test file language, refer to [Tests](/terraform/language/tests). + +## File Extension + +Terraform test files use the file extensions `.tftest.hcl` and `.tftest.json`. + +## Test File Locations + +Terraform loads all test files within your root configuration directory. + +Terraform also loads the test files within the testing directory. You can override the location of the testing directory by appending the `-test-directory` flag to commands that load test files. The default testing directory is `tests` relative to your configuration directory. diff --git a/website/docs/language/functions/abs.mdx b/website/docs/language/functions/abs.mdx index 49ceef9c93..5d40f25b5f 100644 --- a/website/docs/language/functions/abs.mdx +++ b/website/docs/language/functions/abs.mdx @@ -3,19 +3,49 @@ page_title: abs - Functions - Configuration Language description: The abs function returns the absolute value of the given number. --- -# `abs` Function +# `abs` function reference -`abs` returns the absolute value of the given number. In other words, if the -number is zero or positive then it is returned as-is, but if it is negative -then it is multiplied by -1 to make it positive before returning it. +This topic provides reference information about the `abs` function. The `abs` function returns the absolute value of the given number. -## Examples +## Introduction +The `abs` function returns the absolute value of the given number. If the number is zero or positive, the function returns the value as-is, but if it is negative, it is multiplied by -1 to make it positive before returning it. + +## Syntax + +Use the `abs` function with the following syntax: + +```hcl +abs(number) ``` -> abs(23) + +The `number` argument is the number you want the absolute value of. + +In the following example, the function returns the absolute value of `23`, `0`, and `-12.4`. + +```hcl hideClipboard +$ abs(23) 23 -> abs(0) +$ abs(0) 0 -> abs(-12.4) +$ abs(-12.4) 12.4 ``` + +## Example use case + +The following example defines a variable `num` that is negative. The function outputs the absolute value of `num`, which is `10`. + +```hcl +variable "num" { + default = -10 +} + +output "absolute_value" { + value = abs(var.num) +} +``` + +## Related Functions + +- [`signum`](/terraform/language/functions/signum) determines the sign of a number, returning a number between -1 and 1 to represent the sign diff --git a/website/docs/language/functions/abspath.mdx b/website/docs/language/functions/abspath.mdx index 482bc9fc13..cf6da3b9a6 100644 --- a/website/docs/language/functions/abspath.mdx +++ b/website/docs/language/functions/abspath.mdx @@ -12,7 +12,7 @@ with the current working directory. Referring directly to filesystem paths in resource arguments may cause spurious diffs if the same configuration is applied from multiple systems or on different host operating systems. We recommend using filesystem paths only -for transient values, such as the argument to [`file`](/language/functions/file) (where +for transient values, such as the argument to [`file`](/terraform/language/functions/file) (where only the contents are then stored) or in `connection` and `provisioner` blocks. ## Examples diff --git a/website/docs/language/functions/base64decode.mdx b/website/docs/language/functions/base64decode.mdx index 8cb5a46c0a..0e0ddf1574 100644 --- a/website/docs/language/functions/base64decode.mdx +++ b/website/docs/language/functions/base64decode.mdx @@ -24,7 +24,7 @@ most cases. Various other functions with names containing "base64" can generate or manipulate Base64 data directly. `base64decode` is, in effect, a shorthand for calling -[`textdecodebase64`](/language/functions/textdecodebase64) with the encoding name set to +[`textdecodebase64`](/terraform/language/functions/textdecodebase64) with the encoding name set to `UTF-8`. ## Examples @@ -36,11 +36,11 @@ Hello World ## Related Functions -* [`base64encode`](/language/functions/base64encode) performs the opposite operation, +* [`base64encode`](/terraform/language/functions/base64encode) performs the opposite operation, encoding the UTF-8 bytes for a string as Base64. -* [`textdecodebase64`](/language/functions/textdecodebase64) is a more general function that +* [`textdecodebase64`](/terraform/language/functions/textdecodebase64) is a more general function that supports character encodings other than UTF-8. -* [`base64gzip`](/language/functions/base64gzip) applies gzip compression to a string +* [`base64gzip`](/terraform/language/functions/base64gzip) applies gzip compression to a string and returns the result with Base64 encoding. -* [`filebase64`](/language/functions/filebase64) reads a file from the local filesystem +* [`filebase64`](/terraform/language/functions/filebase64) reads a file from the local filesystem and returns its raw bytes with Base64 encoding. diff --git a/website/docs/language/functions/base64encode.mdx b/website/docs/language/functions/base64encode.mdx index 04482c917e..1c4d6fda8b 100644 --- a/website/docs/language/functions/base64encode.mdx +++ b/website/docs/language/functions/base64encode.mdx @@ -25,7 +25,7 @@ Base64 themselves, and so this function exists primarily to allow string data to be easily provided to resource types that expect Base64 bytes. `base64encode` is, in effect, a shorthand for calling -[`textencodebase64`](/language/functions/textencodebase64) with the encoding name set to +[`textencodebase64`](/terraform/language/functions/textencodebase64) with the encoding name set to `UTF-8`. ## Examples @@ -37,12 +37,12 @@ SGVsbG8gV29ybGQ= ## Related Functions -* [`base64decode`](/language/functions/base64decode) performs the opposite operation, +* [`base64decode`](/terraform/language/functions/base64decode) performs the opposite operation, decoding Base64 data and interpreting it as a UTF-8 string. -* [`textencodebase64`](/language/functions/textencodebase64) is a more general function that +* [`textencodebase64`](/terraform/language/functions/textencodebase64) is a more general function that supports character encodings other than UTF-8. -* [`base64gzip`](/language/functions/base64gzip) applies gzip compression to a string +* [`base64gzip`](/terraform/language/functions/base64gzip) applies gzip compression to a string and returns the result with Base64 encoding all in one operation. -* [`filebase64`](/language/functions/filebase64) reads a file from the local filesystem +* [`filebase64`](/terraform/language/functions/filebase64) reads a file from the local filesystem and returns its raw bytes with Base64 encoding, without creating an intermediate Unicode string. diff --git a/website/docs/language/functions/base64gzip.mdx b/website/docs/language/functions/base64gzip.mdx index cb3eefd28f..66fe36e03f 100644 --- a/website/docs/language/functions/base64gzip.mdx +++ b/website/docs/language/functions/base64gzip.mdx @@ -1,31 +1,56 @@ --- -page_title: base64gzip - Functions - Configuration Language +page_title: base64gzip function reference - Functions - Configuration Language description: |- - The base64encode function compresses the given string with gzip and then - encodes the result in Base64. + The base64encode function compresses an HCL string using gzip, and then encodes it using Base64 encoding. --- -# `base64gzip` Function +# `base64gzip` function reference -`base64gzip` compresses a string with gzip and then encodes the result in -Base64 encoding. +This topic provides reference information about the `base64gzip` function. +The `base64gzip` function compresses an HCL string using gzip and then encodes the string using Base64 encoding. -Terraform uses the "standard" Base64 alphabet as defined in -[RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). +## Introduction -Strings in the Terraform language are sequences of unicode characters rather -than bytes, so this function will first encode the characters from the string -as UTF-8, then apply gzip compression, and then finally apply Base64 encoding. +You can use the `base64gzip` function to compress an HCL string and then encode it in the Base64 format. +Terraform uses the standard Base64 alphabet that is defined in [RFC 4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4). -While we do not recommend manipulating large, raw binary data in the Terraform -language, this function can be used to compress reasonably sized text strings -generated within the Terraform language. For example, the result of this -function can be used to create a compressed object in Amazon S3 as part of -an S3 website. +While HashiCorp does not recommend manipulating large, raw binary data in HCL, Base64 encoding can be an effective way to represent small binary objects in memory when you need to pass them as values, rather than referring to files on disk. +For example, you could use the `base64gzip` function to compress a large JSON string so that you can upload it to S3. -## Related Functions +Because HCL strings are sequences of unicode characters rather than bytes, `base64gzip` first encodes the characters in the string as UTF-8. +Then it applies gzip compression and encodes the string using Base64 format. -* [`base64encode`](/language/functions/base64encode) applies Base64 encoding _without_ - gzip compression. -* [`filebase64`](/language/functions/filebase64) reads a file from the local filesystem - and returns its raw bytes with Base64 encoding. +## Syntax + +Use the `base64gzip` function with the following syntax: + +```hcl +base64gzip(ARGS) +``` + +The argument is the string that you want to compress and encode. + +In the following example, the function compresses the string at `local.my_data` and encodes it using the Base64 format. + +```hcl +base64gzip(local.my_data) +``` + +## Example use case + +The following example defines a local value `my_data` that contains the string you want to compress and encode. +The `base64gzip` function compresses and encodes the string, and then it is used to populate an S3 bucket. + +```hcl +resource "aws_s3_object" "example" { + bucket = "my_bucket" + key = "example.txt" + content_base64 = base64gzip(local.my_data) + content_encoding = "gzip" +} +``` + +## Related functions + +* [`base64encode`](/terraform/language/functions/base64encode) applies Base64 encoding to an HCL string without using gzip compression. +* [`filebase64`](/terraform/language/functions/filebase64) reads a file from the local filesystem and encodes its raw bits using the Base64 format. diff --git a/website/docs/language/functions/base64sha256.mdx b/website/docs/language/functions/base64sha256.mdx index ca2116a656..7a1b340a5f 100644 --- a/website/docs/language/functions/base64sha256.mdx +++ b/website/docs/language/functions/base64sha256.mdx @@ -25,7 +25,7 @@ uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek= ## Related Functions -* [`filebase64sha256`](/language/functions/filebase64sha256) calculates the same hash from +* [`filebase64sha256`](/terraform/language/functions/filebase64sha256) calculates the same hash from the contents of a file rather than from a string value. -* [`sha256`](/language/functions/sha256) calculates the same hash but returns the result +* [`sha256`](/terraform/language/functions/sha256) calculates the same hash but returns the result in a more-verbose hexadecimal encoding. diff --git a/website/docs/language/functions/base64sha512.mdx b/website/docs/language/functions/base64sha512.mdx index 3aaa4366ae..7b426283ac 100644 --- a/website/docs/language/functions/base64sha512.mdx +++ b/website/docs/language/functions/base64sha512.mdx @@ -25,7 +25,7 @@ MJ7MSJwS1utMxA9QyQLytNDtd+5RGnx6m808qG1M2G+YndNbxf9JlnDaNCVbRbDP2DDoH2Bdz33FVC6T ## Related Functions -* [`filebase64sha512`](/language/functions/filebase64sha512) calculates the same hash from +* [`filebase64sha512`](/terraform/language/functions/filebase64sha512) calculates the same hash from the contents of a file rather than from a string value. -* [`sha512`](/language/functions/sha512) calculates the same hash but returns the result +* [`sha512`](/terraform/language/functions/sha512) calculates the same hash but returns the result in a more-verbose hexadecimal encoding. diff --git a/website/docs/language/functions/basename.mdx b/website/docs/language/functions/basename.mdx index aed14fc814..8f9262ad94 100644 --- a/website/docs/language/functions/basename.mdx +++ b/website/docs/language/functions/basename.mdx @@ -24,7 +24,7 @@ it uses backslash `\` as the path segment separator. On Unix systems, the slash Referring directly to filesystem paths in resource arguments may cause spurious diffs if the same configuration is applied from multiple systems or on different host operating systems. We recommend using filesystem paths only -for transient values, such as the argument to [`file`](/language/functions/file) (where +for transient values, such as the argument to [`file`](/terraform/language/functions/file) (where only the contents are then stored) or in `connection` and `provisioner` blocks. ## Examples @@ -36,6 +36,6 @@ baz.txt ## Related Functions -* [`dirname`](/language/functions/dirname) returns all of the segments of a filesystem path +* [`dirname`](/terraform/language/functions/dirname) returns all of the segments of a filesystem path _except_ the last, discarding the portion that would be returned by `basename`. diff --git a/website/docs/language/functions/can.mdx b/website/docs/language/functions/can.mdx index 9f58f93cc8..c02f082071 100644 --- a/website/docs/language/functions/can.mdx +++ b/website/docs/language/functions/can.mdx @@ -12,15 +12,15 @@ whether the expression produced a result without any errors. This is a special function that is able to catch errors produced when evaluating its argument. For most situations where you could use `can` it's better to use -[`try`](/language/functions/try) instead, because it allows for more concise definition of +[`try`](/terraform/language/functions/try) instead, because it allows for more concise definition of fallback values for failing expressions. The primary purpose of `can` is to turn an error condition into a boolean validation result when writing -[custom variable validation rules](/language/values/variables#custom-validation-rules). +[custom variable validation rules](/terraform/language/values/variables#custom-validation-rules). For example: -``` +```hcl variable "timestamp" { type = string @@ -41,7 +41,7 @@ as a malformed resource reference. variable validation rules. Although it can technically accept any sort of expression and be used elsewhere in the configuration, we recommend against using it in other contexts. For error handling elsewhere in the configuration, -prefer to use [`try`](/language/functions/try). +prefer to use [`try`](/terraform/language/functions/try). ## Examples @@ -70,5 +70,5 @@ A local value with the name "nonexist" has not been declared. ## Related Functions -* [`try`](/language/functions/try), which tries evaluating a sequence of expressions and +* [`try`](/terraform/language/functions/try), which tries evaluating a sequence of expressions and returns the result of the first one that succeeds. diff --git a/website/docs/language/functions/ceil.mdx b/website/docs/language/functions/ceil.mdx index 0f0956de0f..0245a82ac2 100644 --- a/website/docs/language/functions/ceil.mdx +++ b/website/docs/language/functions/ceil.mdx @@ -21,5 +21,5 @@ given value, which may be a fraction. ## Related Functions -* [`floor`](/language/functions/floor), which rounds to the nearest whole number _less than_ +* [`floor`](/terraform/language/functions/floor), which rounds to the nearest whole number _less than_ or equal. diff --git a/website/docs/language/functions/chomp.mdx b/website/docs/language/functions/chomp.mdx index cded57e7b0..ff1b046c4c 100644 --- a/website/docs/language/functions/chomp.mdx +++ b/website/docs/language/functions/chomp.mdx @@ -23,5 +23,5 @@ hello ## Related Functions -* [`trimspace`](/language/functions/trimspace), which removes all types of whitespace from +* [`trimspace`](/terraform/language/functions/trimspace), which removes all types of whitespace from both the start and the end of a string. diff --git a/website/docs/language/functions/cidrhost.mdx b/website/docs/language/functions/cidrhost.mdx index f49fcebc5b..83b3762326 100644 --- a/website/docs/language/functions/cidrhost.mdx +++ b/website/docs/language/functions/cidrhost.mdx @@ -19,9 +19,13 @@ cidrhost(prefix, hostnum) `hostnum` is a whole number that can be represented as a binary integer with no more than the number of digits remaining in the address after the given -prefix. For more details on how this function interprets CIDR prefixes and +prefix. If `hostnum` is negative, the count starts from the end of the range. +For example, `cidrhost("10.0.0.0/8", 2)` returns `10.0.0.2` and +`cidrhost("10.0.0.0/8", -2)` returns `10.255.255.254`. + +For more details on how this function interprets CIDR prefixes and populates host numbers, see the worked example for -[`cidrsubnet`](/language/functions/cidrsubnet). +[`cidrsubnet`](/terraform/language/functions/cidrsubnet). Conventionally host number zero is used to represent the address of the network itself and the host number that would fill all the host bits with @@ -50,5 +54,5 @@ fd00:fd12:3456:7890::22 ## Related Functions -* [`cidrsubnet`](/language/functions/cidrsubnet) calculates a subnet address under a given +* [`cidrsubnet`](/terraform/language/functions/cidrsubnet) calculates a subnet address under a given network address prefix. diff --git a/website/docs/language/functions/cidrnetmask.mdx b/website/docs/language/functions/cidrnetmask.mdx index f8d5d71321..f66dc34c9e 100644 --- a/website/docs/language/functions/cidrnetmask.mdx +++ b/website/docs/language/functions/cidrnetmask.mdx @@ -20,8 +20,8 @@ cidrnetmask(prefix) The result is a subnet address formatted in the conventional dotted-decimal IPv4 address syntax, as expected by some software. -CIDR notation is the only valid notation for IPv6 addresses, so `cidrnetmask` -produces an error if given an IPv6 address. +The `cidrnetmask` function only accepts IPv4 addresses in CIDR notation. +If you use an IPv6 address, `cidrnetmask` returns an error. -> **Note:** As a historical accident, this function interprets IPv4 address octets that have leading zeros as decimal numbers, which is contrary to some diff --git a/website/docs/language/functions/cidrsubnet.mdx b/website/docs/language/functions/cidrsubnet.mdx index 70ec016012..8ee8b1cd8f 100644 --- a/website/docs/language/functions/cidrsubnet.mdx +++ b/website/docs/language/functions/cidrsubnet.mdx @@ -27,7 +27,7 @@ additional bits added to the prefix. This function accepts both IPv6 and IPv4 prefixes, and the result always uses the same addressing scheme as the given prefix. -Unlike the related function [`cidrsubnets`](/language/functions/cidrsubnets), `cidrsubnet` +Unlike the related function [`cidrsubnets`](/terraform/language/functions/cidrsubnets), `cidrsubnet` allows you to give a specific network number to use. `cidrsubnets` can allocate multiple network addresses at once, but numbers them automatically starting with zero. @@ -93,7 +93,7 @@ This gives us some additional information but also confirms (using a slightly different notation) the conversion from decimal to binary and shows the range of possible host addresses in this network. -While [`cidrhost`](/language/functions/cidrhost) allows calculating single host IP addresses, +While [`cidrhost`](/terraform/language/functions/cidrhost) allows calculating single host IP addresses, `cidrsubnet` on the other hand creates a new network prefix _within_ the given network prefix. In other words, it creates a subnet. @@ -148,7 +148,7 @@ Hosts/Net: 14 Class A, Private Internet The new subnet has four bits available for host numbering, which means that there are 14 host addresses available for assignment once we subtract the network's own address and the broadcast address. You can thus use -[`cidrhost`](/language/functions/cidrhost) function to calculate those host addresses by +[`cidrhost`](/terraform/language/functions/cidrhost) function to calculate those host addresses by providing it a value between 1 and 14: ``` @@ -163,9 +163,9 @@ For more information on CIDR notation and subnetting, see ## Related Functions -* [`cidrhost`](/language/functions/cidrhost) calculates the IP address for a single host +* [`cidrhost`](/terraform/language/functions/cidrhost) calculates the IP address for a single host within a given network address prefix. -* [`cidrnetmask`](/language/functions/cidrnetmask) converts an IPv4 network prefix in CIDR +* [`cidrnetmask`](/terraform/language/functions/cidrnetmask) converts an IPv4 network prefix in CIDR notation into netmask notation. -* [`cidrsubnets`](/language/functions/cidrsubnets) can allocate multiple consecutive +* [`cidrsubnets`](/terraform/language/functions/cidrsubnets) can allocate multiple consecutive addresses under a prefix at once, numbering them automatically. diff --git a/website/docs/language/functions/cidrsubnets.mdx b/website/docs/language/functions/cidrsubnets.mdx index d8e63f6eb2..0dfd573bd4 100644 --- a/website/docs/language/functions/cidrsubnets.mdx +++ b/website/docs/language/functions/cidrsubnets.mdx @@ -23,7 +23,7 @@ value is therefore a list with one element per `newbits` argument, each a string containing an address range in CIDR notation. For more information on IP addressing concepts, see the documentation for the -related function [`cidrsubnet`](/language/functions/cidrsubnet). `cidrsubnet` calculates +related function [`cidrsubnet`](/terraform/language/functions/cidrsubnet). `cidrsubnet` calculates a single subnet address within a prefix while allowing you to specify its subnet number, while `cidrsubnets` can calculate many at once, potentially of different sizes, and assigns subnet numbers automatically. @@ -69,7 +69,7 @@ platforms. ``` You can use nested `cidrsubnets` calls with -[`for` expressions](/language/expressions/for) +[`for` expressions](/terraform/language/expressions/for) to concisely allocate groups of network address blocks: ``` @@ -96,9 +96,9 @@ to concisely allocate groups of network address blocks: ## Related Functions -* [`cidrhost`](/language/functions/cidrhost) calculates the IP address for a single host +* [`cidrhost`](/terraform/language/functions/cidrhost) calculates the IP address for a single host within a given network address prefix. -* [`cidrnetmask`](/language/functions/cidrnetmask) converts an IPv4 network prefix in CIDR +* [`cidrnetmask`](/terraform/language/functions/cidrnetmask) converts an IPv4 network prefix in CIDR notation into netmask notation. -* [`cidrsubnet`](/language/functions/cidrsubnet) calculates a single subnet address, allowing +* [`cidrsubnet`](/terraform/language/functions/cidrsubnet) calculates a single subnet address, allowing you to specify its network number. diff --git a/website/docs/language/functions/coalesce.mdx b/website/docs/language/functions/coalesce.mdx index 1c005ea40c..fcbb6463cd 100644 --- a/website/docs/language/functions/coalesce.mdx +++ b/website/docs/language/functions/coalesce.mdx @@ -52,5 +52,5 @@ Call to function "coalesce" failed: all arguments must have the same type. ## Related Functions -* [`coalescelist`](/language/functions/coalescelist) performs a similar operation with +* [`coalescelist`](/terraform/language/functions/coalescelist) performs a similar operation with list arguments rather than individual arguments. diff --git a/website/docs/language/functions/coalescelist.mdx b/website/docs/language/functions/coalescelist.mdx index 4c43162124..9d53dc05fb 100644 --- a/website/docs/language/functions/coalescelist.mdx +++ b/website/docs/language/functions/coalescelist.mdx @@ -38,5 +38,5 @@ symbol to expand the outer list as arguments: ## Related Functions -* [`coalesce`](/language/functions/coalesce) performs a similar operation with string +* [`coalesce`](/terraform/language/functions/coalesce) performs a similar operation with string arguments rather than list arguments. diff --git a/website/docs/language/functions/compact.mdx b/website/docs/language/functions/compact.mdx index b76d0e811b..92321d62d6 100644 --- a/website/docs/language/functions/compact.mdx +++ b/website/docs/language/functions/compact.mdx @@ -1,17 +1,17 @@ --- page_title: compact - Functions - Configuration Language -description: The compact function removes empty string elements from a list. +description: The compact function removes null or empty string elements from a list. --- # `compact` Function -`compact` takes a list of strings and returns a new list with any empty string +`compact` takes a list of strings and returns a new list with any null or empty string elements removed. ## Examples ``` -> compact(["a", "", "b", "c"]) +> compact(["a", "", "b", null, "c"]) [ "a", "b", diff --git a/website/docs/language/functions/concat.mdx b/website/docs/language/functions/concat.mdx index 89c74960f5..7709fe6cc0 100644 --- a/website/docs/language/functions/concat.mdx +++ b/website/docs/language/functions/concat.mdx @@ -17,4 +17,15 @@ description: The concat function combines two or more lists into a single list. "b", "c", ] + +# Can do multiple lists of mixed types, arguments can also be empty lists +concat([], [1, "a"], [[3], "c"]) +[ + 1, + "a", + [ + 3, + ], + "c", +] ``` diff --git a/website/docs/language/functions/contains.mdx b/website/docs/language/functions/contains.mdx index 2b6239c3b9..b6c16d9f97 100644 --- a/website/docs/language/functions/contains.mdx +++ b/website/docs/language/functions/contains.mdx @@ -5,8 +5,10 @@ description: The contains function determines whether a list or set contains a g # `contains` Function -`contains` determines whether a given list or set contains a given single value -as one of its elements. +`contains` determines whether the list, tuple, or set given in its first +argument contains at least one element that is equal to the value in the second +argument, using the same definition of equality as the `==` operator described in +[Equality Operators](/terraform/language/expressions/operators#equality-operators). ```hcl contains(list, value) diff --git a/website/docs/language/functions/csvdecode.mdx b/website/docs/language/functions/csvdecode.mdx index 5d8859df38..c6262c4558 100644 --- a/website/docs/language/functions/csvdecode.mdx +++ b/website/docs/language/functions/csvdecode.mdx @@ -39,7 +39,7 @@ number of fields, or this function will produce an error. ## Use with the `for_each` meta-argument You can use the result of `csvdecode` with -[the `for_each` meta-argument](/language/meta-arguments/for_each) +[the `for_each` meta-argument](/terraform/language/meta-arguments/for_each) to describe a collection of similar objects whose differences are described by the rows in the given CSV file. @@ -63,7 +63,7 @@ locals { } resource "aws_instance" "example" { - for_each = { for inst in local.instances : inst.local_id => inst } + for_each = tomap({ for inst in local.instances : inst.local_id => inst }) instance_type = each.value.instance_type ami = each.value.ami @@ -87,7 +87,7 @@ create or destroy associated instances as appropriate. If there is no reasonable value you can use as a unique identifier in your CSV then you could instead use -[the `count` meta-argument](/language/meta-arguments/count) +[the `count` meta-argument](/terraform/language/meta-arguments/count) to define an object for each CSV row, with each one identified by its index into the list returned by `csvdecode`. However, in that case any future updates to the CSV may be disruptive if they change the positions of particular objects in diff --git a/website/docs/language/functions/dirname.mdx b/website/docs/language/functions/dirname.mdx index c26edbd8ca..1b4ab32eaf 100644 --- a/website/docs/language/functions/dirname.mdx +++ b/website/docs/language/functions/dirname.mdx @@ -23,7 +23,7 @@ any slashes in the given path will be replaced by backslashes before returning. Referring directly to filesystem paths in resource arguments may cause spurious diffs if the same configuration is applied from multiple systems or on different host operating systems. We recommend using filesystem paths only -for transient values, such as the argument to [`file`](/language/functions/file) (where +for transient values, such as the argument to [`file`](/terraform/language/functions/file) (where only the contents are then stored) or in `connection` and `provisioner` blocks. ## Examples @@ -35,5 +35,5 @@ foo/bar ## Related Functions -* [`basename`](/language/functions/basename) returns _only_ the last portion of a filesystem +* [`basename`](/terraform/language/functions/basename) returns _only_ the last portion of a filesystem path, discarding the portion that would be returned by `dirname`. diff --git a/website/docs/language/functions/element.mdx b/website/docs/language/functions/element.mdx index ca81aa6115..49d8039a7e 100644 --- a/website/docs/language/functions/element.mdx +++ b/website/docs/language/functions/element.mdx @@ -12,7 +12,7 @@ element(list, index) ``` The index is zero-based. This function produces an error if used with an -empty list. The index must be a non-negative integer. +empty list. The index can be a negative integer. Use the built-in index syntax `list[index]` in most cases. Use this function only for the special additional "wrap-around" behavior described below. @@ -21,7 +21,7 @@ only for the special additional "wrap-around" behavior described below. ``` > element(["a", "b", "c"], 1) -b +"b" ``` If the given index is greater than the length of the list then the index is @@ -29,19 +29,17 @@ If the given index is greater than the length of the list then the index is ``` > element(["a", "b", "c"], 3) -a +"a" ``` -To get the last element from the list use [`length`](/language/functions/length) to find -the size of the list (minus 1 as the list is zero-based) and then pick the -last element: +To get the last element from the list use the index `-1`: ``` -> element(["a", "b", "c"], length(["a", "b", "c"])-1) -c +> element(["a", "b", "c"], -1) +"c" ``` ## Related Functions -* [`index`](/language/functions/index_function) finds the index for a particular element value. -* [`lookup`](/language/functions/lookup) retrieves a value from a _map_ given its _key_. +* [`index`](/terraform/language/functions/index_function) finds the index for a particular element value. +* [`lookup`](/terraform/language/functions/lookup) retrieves a value from a _map_ given its _key_. diff --git a/website/docs/language/functions/endswith.mdx b/website/docs/language/functions/endswith.mdx index d96cc88aad..d6e5601f31 100644 --- a/website/docs/language/functions/endswith.mdx +++ b/website/docs/language/functions/endswith.mdx @@ -24,4 +24,4 @@ false ## Related Functions -- [`startswith`](/language/functions/startswith) takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix. +- [`startswith`](/terraform/language/functions/startswith) takes two values: a string to check and a prefix string. The function returns true if the string begins with that exact prefix. diff --git a/website/docs/language/functions/ephemeralasnull.mdx b/website/docs/language/functions/ephemeralasnull.mdx new file mode 100644 index 0000000000..7e2092e63e --- /dev/null +++ b/website/docs/language/functions/ephemeralasnull.mdx @@ -0,0 +1,85 @@ +--- +page_title: ephemeralasnull function reference - Functions - Configuration Language +description: |- + The `ephemeralasnull` function accepts an ephemeral value and returns null. +--- + +# `ephemeralasnull` function reference + +-> **Note**: The `ephemeralasnull` function is available in Terraform v1.10 and later. + +This topic provides reference information about the `ephemeralasnull` function. The `ephemeralasnull` function accepts an ephemeral value and returns `null`. + +## Introduction + +You can use the `ephemeralasnull` function to nullify ephemeral values. For example, if you pass an object with a nested ephemeral value to `ephemeralasnull`,it nullifies any ephemeral values within that object. + +## Syntax + +Use the `ephemeralasnull` function with the following syntax: + +```hcl +ephemeralasnull(var.my_ephemeral_value) +``` + +In the following example, you can pass the `ephemeralasnull` function an ephemeral value, and the function returns `null`: + +```hcl +variable "example" { + type = string + default = "test" + ephemeral = true +} + +# This output returns null. +output "example_output" { + value = ephemeralasnull(var.example) +} +``` + +For more information on which Terraform blocks can be ephemeral, refer to ephemeral [inputs](/terraform/language/values/variables#exclude-values-from-state), [outputs](/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files), and [resources](/terraform/language/resources/ephemeral). + +## Example use case + +The following example shows how you can use the `ephemeralasnull` function to intake a map that contains some ephemeral values, and output the values that are not ephemeral. + +```hcl +variable "session_token" { + type = string + default = "test" + ephemeral = true +} + +variable "configuration" { + type = map(string) + default = { + "env" = "development" + } +} + +# This is a contrived example, but this pattern works with any object that is a mix of ephemeral and non-ephemeral values. +locals { + configuration_with_token = merge( + var.configuration, + { "session_token" = var.session_token } + ) + ephemeral = true +} + +output "configuration_settings" { +# Using ephemeralasnull enables you to output the non-ephemeral values. + value = ephemeralasnull(local.things_with_token) + description = "Environment setting." +} +``` + +When you apply the above configuration, Terraform returns the `ephemeral` values in `locals.configuration_settings` as `null`. This lets you output the non-ephemeral value `env`. + +```hcl +Outputs: + +configuration_settings = { + "env" = "development" + "session_token" = tostring(null) +} +``` \ No newline at end of file diff --git a/website/docs/language/functions/file.mdx b/website/docs/language/functions/file.mdx index 0dd177f683..1c9872ba18 100644 --- a/website/docs/language/functions/file.mdx +++ b/website/docs/language/functions/file.mdx @@ -37,10 +37,10 @@ Hello World ## Related Functions -* [`filebase64`](/language/functions/filebase64) also reads the contents of a given file, +* [`filebase64`](/terraform/language/functions/filebase64) also reads the contents of a given file, but returns the raw bytes in that file Base64-encoded, rather than interpreting the contents as UTF-8 text. -* [`fileexists`](/language/functions/fileexists) determines whether a file exists +* [`fileexists`](/terraform/language/functions/fileexists) determines whether a file exists at a given path. -* [`templatefile`](/language/functions/templatefile) renders using a file from disk as a +* [`templatefile`](/terraform/language/functions/templatefile) renders using a file from disk as a template. diff --git a/website/docs/language/functions/filebase64.mdx b/website/docs/language/functions/filebase64.mdx index 7e5cede2c3..15c46ac4bd 100644 --- a/website/docs/language/functions/filebase64.mdx +++ b/website/docs/language/functions/filebase64.mdx @@ -38,9 +38,9 @@ SGVsbG8gV29ybGQ= ## Related Functions -* [`file`](/language/functions/file) also reads the contents of a given file, +* [`file`](/terraform/language/functions/file) also reads the contents of a given file, but interprets the data as UTF-8 text and returns the result directly as a string, without any further encoding. -* [`base64decode`](/language/functions/base64decode) can decode a Base64 string representing +* [`base64decode`](/terraform/language/functions/base64decode) can decode a Base64 string representing bytes in UTF-8, but in practice `base64decode(filebase64(...))` is equivalent to the shorter expression `file(...)`. diff --git a/website/docs/language/functions/filebase64sha256.mdx b/website/docs/language/functions/filebase64sha256.mdx index 7e8ba0da15..2cdee344f0 100644 --- a/website/docs/language/functions/filebase64sha256.mdx +++ b/website/docs/language/functions/filebase64sha256.mdx @@ -7,9 +7,9 @@ description: |- # `filebase64sha256` Function -`filebase64sha256` is a variant of [`base64sha256`](/language/functions/base64sha256) +`filebase64sha256` is a variant of [`base64sha256`](/terraform/language/functions/base64sha256) that hashes the contents of a given file rather than a literal string. This is similar to `base64sha256(file(filename))`, but -because [`file`](/language/functions/file) accepts only UTF-8 text it cannot be used to +because [`file`](/terraform/language/functions/file) accepts only UTF-8 text it cannot be used to create hashes for binary files. diff --git a/website/docs/language/functions/filebase64sha512.mdx b/website/docs/language/functions/filebase64sha512.mdx index cae825b310..0fd09cbdd3 100644 --- a/website/docs/language/functions/filebase64sha512.mdx +++ b/website/docs/language/functions/filebase64sha512.mdx @@ -7,9 +7,9 @@ description: |- # `filebase64sha512` Function -`filebase64sha512` is a variant of [`base64sha512`](/language/functions/base64sha512) +`filebase64sha512` is a variant of [`base64sha512`](/terraform/language/functions/base64sha512) that hashes the contents of a given file rather than a literal string. This is similar to `base64sha512(file(filename))`, but -because [`file`](/language/functions/file) accepts only UTF-8 text it cannot be used to +because [`file`](/terraform/language/functions/file) accepts only UTF-8 text it cannot be used to create hashes for binary files. diff --git a/website/docs/language/functions/fileexists.mdx b/website/docs/language/functions/fileexists.mdx index 47ceca0298..b95585f777 100644 --- a/website/docs/language/functions/fileexists.mdx +++ b/website/docs/language/functions/fileexists.mdx @@ -31,4 +31,4 @@ fileexists("custom-section.sh") ? file("custom-section.sh") : local.default_cont ## Related Functions -* [`file`](/language/functions/file) reads the contents of a file at a given path +* [`file`](/terraform/language/functions/file) reads the contents of a file at a given path diff --git a/website/docs/language/functions/filemd5.mdx b/website/docs/language/functions/filemd5.mdx index de2894d028..5b8525557f 100644 --- a/website/docs/language/functions/filemd5.mdx +++ b/website/docs/language/functions/filemd5.mdx @@ -7,9 +7,14 @@ description: |- # `filemd5` Function -`filemd5` is a variant of [`md5`](/language/functions/md5) +`filemd5` is a variant of [`md5`](/terraform/language/functions/md5) that hashes the contents of a given file rather than a literal string. This is similar to `md5(file(filename))`, but -because [`file`](/language/functions/file) accepts only UTF-8 text it cannot be used to +because [`file`](/terraform/language/functions/file) accepts only UTF-8 text it cannot be used to create hashes for binary files. + +Collision attacks have been successfully performed against this hashing +function. Before using this function for anything security-sensitive, refer to +[RFC 6151](https://tools.ietf.org/html/rfc6151) for updated security +considerations applying to the MD5 algorithm. diff --git a/website/docs/language/functions/fileset.mdx b/website/docs/language/functions/fileset.mdx index 115d8bf6f6..3a1661c83a 100644 --- a/website/docs/language/functions/fileset.mdx +++ b/website/docs/language/functions/fileset.mdx @@ -66,7 +66,7 @@ before Terraform takes any actions. ``` A common use of `fileset` is to create one resource instance per matched file, using -[the `for_each` meta-argument](/language/meta-arguments/for_each): +[the `for_each` meta-argument](/terraform/language/meta-arguments/for_each): ```hcl resource "example_thing" "example" { diff --git a/website/docs/language/functions/filesha1.mdx b/website/docs/language/functions/filesha1.mdx index 271884d697..7ae46bb0df 100644 --- a/website/docs/language/functions/filesha1.mdx +++ b/website/docs/language/functions/filesha1.mdx @@ -1,15 +1,48 @@ --- page_title: filesha1 - Functions - Configuration Language -description: |- - The filesha1 function computes the SHA1 hash of the contents of - a given file and encodes it as hex. +description: The filesha1 function computes the SHA1 hash of the contents of a given file and encodes it as hex. --- -# `filesha1` Function +# `filesha1` function reference +This topic provides reference information about the `filesha1` function, which calculates the SHA-1 hash of a file's contents. -`filesha1` is a variant of [`sha1`](/language/functions/sha1) -that hashes the contents of a given file rather than a literal string. +## Introduction -This is similar to `sha1(file(filename))`, but -because [`file`](/language/functions/file) accepts only UTF-8 text it cannot be used to -create hashes for binary files. +The `filesha1` is a variant of [`sha1`](/terraform/language/functions/sha1) that hashes the contents of a given file rather than a literal string. + +Use the `filesha1` function instead of wrapping the `file` function in a `sha1` function, for example `sha1(file(filename))`, because [`file`](/terraform/language/functions/file) accepts only UTF-8 text. As a result, you cannot use `sha1(file(filename))` to create hashes for binary files. + +!> **Security warning**: This hashing function is susceptible to collision attacks. Before using this function for anything security-sensitive, review relevant literature to understand the security implications. + +## Syntax + +Use the filesha1 function with the following syntax: + +```hcl +filesha1(path) +``` + +The `path` is the relative or absolute file path to the file whose SHA-1 hash you want to compute. + +In the following example, the function returns the SHA-1 value. + +```hcl +$ filesha1("example.txt") +d3486ae9136e7856bc42212385ea797094475802 +``` + +## Example use case + +In the following example the `filesha1` function computes the SHA-1 hash of the file `example.txt` located in the current module's directory. The result is a 40-character hexadecimal string representing the SHA-1 hash. + +```hcl +output "file_hash" { + value = filesha1("${path.module}/example.txt") +} +``` + +## Related functions + +- [`sha1`](/terraform/language/functions/sha1) computes the SHA-1 hash of a given string and encodes it with hexadecimal digits. +- [`filesha256`](/terraform/language/functions/filesha256) computes the SHA-256 hash of a given file and encodes it with hexadecimal digits. +- [`filesha512`](/terraform/language/functions/filesha512) computes the SHA-512 hash of a given file and encodes it with hexadecimal digits. diff --git a/website/docs/language/functions/filesha256.mdx b/website/docs/language/functions/filesha256.mdx index 80918ba05c..2be52d37b3 100644 --- a/website/docs/language/functions/filesha256.mdx +++ b/website/docs/language/functions/filesha256.mdx @@ -5,11 +5,46 @@ description: |- a given file and encodes it as hex. --- -# `filesha256` Function +# `filesha256` function reference -`filesha256` is a variant of [`sha256`](/language/functions/sha256) +This topic provides reference information about the `filesha256` function, which calculates the SHA-256 hash of a file's contents. + +## Introduction + +The `filesha256` is a variant of [`sha256`](/terraform/language/functions/sha256) that hashes the contents of a given file rather than a literal string. -This is similar to `sha256(file(filename))`, but -because [`file`](/language/functions/file) accepts only UTF-8 text it cannot be used to -create hashes for binary files. +Use the `filesha1` function instead of wrapping the `file` function in a `sha1` function, for example `sha1(file(filename))`, because [`file`](/terraform/language/functions/file) accepts only UTF-8 text. As a result, you cannot use `sha1(file(filename))` to create hashes for binary files. + +## Syntax + +Use the `filesha256` function with the following syntax: + +```hcl +filesha256(path) +``` + +The `path` is the relative or absolute file path to the file whose SHA-256 hash you want to compute. + +In the following example, the function returns the SHA-256 value of `example.txt`. + +```hcl +$ filesha512("example.txt") +a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b0b571d0f6a26f2bb +``` + +## Example use case + +In the following example, the `filesha256` function computes the SHA-256 hash of the file `example.txt` located in the current module's directory. + +```hcl +output "file_hash" { + value = filesha256("example.txt") +} +``` + +## Related functions + +- [`sha256`](/terraform/language/functions/sha256) computes the SHA-1 hash of a given string and encodes it with hexadecimal digits. +- [`filesha512`](/terraform/language/functions/filesha512) computes the SHA-512 hash of a given file and encodes it with hexadecimal digits. +- [`filesha1`](/terraform/language/functions/filesha1)computes the SHA-1 hash of a given file and encodes it with hexadecimal digits. diff --git a/website/docs/language/functions/filesha512.mdx b/website/docs/language/functions/filesha512.mdx index 10b0147186..cb9aadd1d0 100644 --- a/website/docs/language/functions/filesha512.mdx +++ b/website/docs/language/functions/filesha512.mdx @@ -5,11 +5,48 @@ description: |- a given file and encodes it as hex. --- -# `filesha512` Function +# `filesha512` function reference -`filesha512` is a variant of [`sha512`](/language/functions/sha512) +This topic provides reference information about the `filesha512` function, which calculates the SHA-512 hash of a file's contents. + +## Introduction + +The `filesha512` is a variant of [`sha512`](/terraform/language/functions/sha512) that hashes the contents of a given file rather than a literal string. This is similar to `sha512(file(filename))`, but -because [`file`](/language/functions/file) accepts only UTF-8 text it cannot be used to +because [`file`](/terraform/language/functions/file) accepts only UTF-8 text it cannot be used to create hashes for binary files. + +## Syntax + +Use the `filesha512` function with the following syntax: + +```hcl +filesha512(path) +``` + +The `path` is the relative or absolute file path to the file whose SHA-512 hash you want to compute. + +In the following example, the function returns the SHA-512 value of `example.txt`. + +```hcl +$ filesha512("example.txt") +861844d6704e8573fec34d967e20bcfe6e061b3348b3b0c7e9b7c6a482f6f15a48bffd0fb928fd8c9f9196f7a8596d5e32b45d5a25488a8499396a67442c1d76 +``` + +## Example use case + +In the following example, the `filesha512` function computes the SHA-512 hash of the file `example.txt` located in the current module's directory. + +```hcl +output "file_hash" { + value = filesha512("example.txt") +} +``` + +## Related functions + +- [`sha512`](/terraform/language/functions/sha512) computes the SHA-512 hash of a given string and encodes it with hexadecimal digits. +- [`filesha256`](/terraform/language/functions/filesha256) computes the SHA-1 hash of a given file and encodes it with hexadecimal digits. +- [`filesha1`](/terraform/language/functions/filesha1)computes the SHA-1 hash of a given file and encodes it with hexadecimal digits. diff --git a/website/docs/language/functions/flatten.mdx b/website/docs/language/functions/flatten.mdx index 27f675370f..461f44f343 100644 --- a/website/docs/language/functions/flatten.mdx +++ b/website/docs/language/functions/flatten.mdx @@ -28,9 +28,9 @@ Indirectly-nested lists, such as those in maps, are _not_ flattened. ## Flattening nested structures for `for_each` The -[resource `for_each`](/language/meta-arguments/for_each) +[resource `for_each`](/terraform/language/meta-arguments/for_each) and -[`dynamic` block](/language/expressions/dynamic-blocks) +[`dynamic` block](/terraform/language/expressions/dynamic-blocks) language features both require a collection value that has one element for each repetition. @@ -46,6 +46,38 @@ variable "networks" { cidr_block = string subnets = map(object({ cidr_block = string })) })) + default = { + "private" = { + cidr_block = "10.1.0.0/16" + subnets = { + "db1" = { + cidr_block = "10.1.0.1/16" + } + "db2" = { + cidr_block = "10.1.0.2/16" + } + } + }, + "public" = { + cidr_block = "10.1.1.0/16" + subnets = { + "webserver" = { + cidr_block = "10.1.1.1/16" + } + "email_server" = { + cidr_block = "10.1.1.2/16" + } + } + } + "dmz" = { + cidr_block = "10.1.2.0/16" + subnets = { + "firewall" = { + cidr_block = "10.1.2.1/16" + } + } + } + } } ``` @@ -86,9 +118,9 @@ resource "aws_subnet" "example" { # local.network_subnets is a list, so we must now project it into a map # where each key is unique. We'll combine the network and subnet keys to # produce a single unique key per instance. - for_each = { + for_each = tomap({ for subnet in local.network_subnets : "${subnet.network_key}.${subnet.subnet_key}" => subnet - } + }) vpc_id = each.value.network_id availability_zone = each.value.subnet_key @@ -101,6 +133,6 @@ the associations between the subnets and their containing networks. ## Related Functions -* [`setproduct`](/language/functions/setproduct) finds all of the combinations of multiple +* [`setproduct`](/terraform/language/functions/setproduct) finds all of the combinations of multiple lists or sets of values, which can also be useful when preparing collections for use with `for_each` constructs. diff --git a/website/docs/language/functions/floor.mdx b/website/docs/language/functions/floor.mdx index 97e57d9aff..c40b0e5f5a 100644 --- a/website/docs/language/functions/floor.mdx +++ b/website/docs/language/functions/floor.mdx @@ -21,5 +21,5 @@ given value, which may be a fraction. ## Related Functions -* [`ceil`](/language/functions/ceil), which rounds to the nearest whole number _greater than_ +* [`ceil`](/terraform/language/functions/ceil), which rounds to the nearest whole number _greater than_ or equal. diff --git a/website/docs/language/functions/format.mdx b/website/docs/language/functions/format.mdx index be1ad663b8..0bf30208d5 100644 --- a/website/docs/language/functions/format.mdx +++ b/website/docs/language/functions/format.mdx @@ -34,7 +34,14 @@ Hello, Valentina! Hello, Valentina! ``` -The formatting verb `%#v` accepts a value of any type and presents it using JSON encoding, similar to jsonencode. This can be useful for describing the values given to a module in [custom condition check](/language/expressions/custom-conditions#error-messages) error messages. +We can specify an argument position number + +``` +> format("%[1]s%[2]s%[1]s%[3]s", "/", "path", "file.tf") +"/path/file.tf" +``` + +The formatting verb `%#v` accepts a value of any type and presents it using JSON encoding, similar to jsonencode. This can be useful for describing the values given to a module in [custom condition check](/terraform/language/expressions/custom-conditions#error-messages) error messages. ``` > format("%#v", "hello") @@ -139,7 +146,7 @@ Use the following symbols immediately after the `%` symbol to set additional for ## Related Functions -* [`formatdate`](/language/functions/formatdate) is a specialized formatting function for +* [`formatdate`](/terraform/language/functions/formatdate) is a specialized formatting function for human-readable timestamps. -* [`formatlist`](/language/functions/formatlist) uses the same specification syntax to +* [`formatlist`](/terraform/language/functions/formatlist) uses the same specification syntax to produce a list of strings. diff --git a/website/docs/language/functions/formatdate.mdx b/website/docs/language/functions/formatdate.mdx index 07e555f762..358536d794 100644 --- a/website/docs/language/functions/formatdate.mdx +++ b/website/docs/language/functions/formatdate.mdx @@ -99,7 +99,7 @@ configuration as needed: ## Related Functions -- [`format`](/language/functions/format) is a more general formatting function for arbitrary +- [`format`](/terraform/language/functions/format) is a more general formatting function for arbitrary data. -- [`timestamp`](/language/functions/timestamp) returns the current date and time in a format +- [`timestamp`](/terraform/language/functions/timestamp) returns the current date and time in a format suitable for input to `formatdate`. diff --git a/website/docs/language/functions/formatlist.mdx b/website/docs/language/functions/formatlist.mdx index c8ee47910d..3d96865aa7 100644 --- a/website/docs/language/functions/formatlist.mdx +++ b/website/docs/language/functions/formatlist.mdx @@ -15,7 +15,7 @@ formatlist(spec, values...) ``` The specification string uses -[the same syntax as `format`](/language/functions/format#specification-syntax). +[the same syntax as `format`](/terraform/language/functions/format#specification-syntax). The given values can be a mixture of list and non-list arguments. Any given lists must be the same length, which decides the length of the resulting list. @@ -45,5 +45,5 @@ once per element of the list arguments. ## Related Functions -* [`format`](/language/functions/format) defines the specification syntax used by this +* [`format`](/terraform/language/functions/format) defines the specification syntax used by this function and produces a single string as its result. diff --git a/website/docs/language/functions/indent.mdx b/website/docs/language/functions/indent.mdx index 0f7934027e..bed6e96f03 100644 --- a/website/docs/language/functions/indent.mdx +++ b/website/docs/language/functions/indent.mdx @@ -1,31 +1,35 @@ --- page_title: indent - Functions - Configuration Language description: |- - The indent function adds a number of spaces to the beginnings of all but the - first line of a given multi-line string. + The indent function adds spaces to the beginning of each line but the first in a multi-line string. --- -# `indent` Function +# `indent` function reference -`indent` adds a given number of spaces to the beginnings of all but the first -line in a given multi-line string. +This topic provides reference information about the `indent` function. +You can use the `indent` function to add indentation to the beginning of each line, except the first, in a multi-line string. + +## Introduction + +The `indent` function adds a specified number of spaces to the beginning of each line in a multi-line string, except for the first line. +You can use the `indent` function to help ensure that complex strings are properly formatted, consistent, and readable. +The function can be especially useful when you work with YAML, JSON, Kubernetes, or other formats that require complex, structured text. + +## Syntax + +Use the `indent` function with the following syntax: ```hcl indent(num_spaces, string) ``` -## Examples +- The first argument is numeric. It specifies the number of spaces you want to add to each line except the first. +- The second argument is a string. It specifies the multi-line string to which you want to add spaces. -This function is useful for inserting a multi-line string into an -already-indented context in another string: +In the following example, the `indent` function adds two spaces to the beginning of each line of the `description` variable to make it easier to read: +```hcl +output "formatted_description" { + value = indent(2, var.description) +} ``` -> " items: ${indent(2, "[\n foo,\n bar,\n]\n")}" - items: [ - foo, - bar, - ] -``` - -The first line of the string is not indented so that, as above, it can be -placed after an introduction sequence that has already begun the line. diff --git a/website/docs/language/functions/index.mdx b/website/docs/language/functions/index.mdx index 617d1091df..a8f151986d 100644 --- a/website/docs/language/functions/index.mdx +++ b/website/docs/language/functions/index.mdx @@ -7,7 +7,7 @@ description: >- # Built-in Functions -> **Hands-on:** Try the [Perform Dynamic Operations with Functions](https://learn.hashicorp.com/tutorials/terraform/functions?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Perform Dynamic Operations with Functions](/terraform/tutorials/configuration-language/functions?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. The Terraform language includes a number of built-in functions that you can call from within expressions to transform and combine values. The general @@ -19,16 +19,15 @@ max(5, 12, 9) ``` For more details on syntax, see -[_Function Calls_](/language/expressions/function-calls) +[_Function Calls_](/terraform/language/expressions/function-calls) in the Expressions section. The Terraform language does not support user-defined functions, and so only -the functions built in to the language are available for use. The navigation -for this section includes a list of all of the available built-in functions. +the functions built in to the language are available for use. The documentation includes a page for all of the available built-in functions. You can experiment with the behavior of Terraform's built-in functions from the Terraform expression console, by running -[the `terraform console` command](/cli/commands/console): +[the `terraform console` command](/terraform/cli/commands/console): ``` > max(5, 12, 9) diff --git a/website/docs/language/functions/index_function.mdx b/website/docs/language/functions/index_function.mdx index f0c1001cdd..0839e0cbbf 100644 --- a/website/docs/language/functions/index_function.mdx +++ b/website/docs/language/functions/index_function.mdx @@ -5,7 +5,7 @@ description: The index function finds the element index for a given value in a l # `index` Function -`index` finds the element index for a given value in a list. +`index` finds the first element index for a given value in a list. ```hcl index(list, value) @@ -21,7 +21,14 @@ value is not present in the list. 1 ``` +In this example, the index of the first occurence of `"two"` is returned. + +``` +> index(["one", "two", "two"], "two") +1 +``` + ## Related Functions -* [`element`](/language/functions/element) retrieves a particular element from a list given +* [`element`](/terraform/language/functions/element) retrieves a particular element from a list given its index. diff --git a/website/docs/language/functions/issensitive.mdx b/website/docs/language/functions/issensitive.mdx new file mode 100644 index 0000000000..913edaab45 --- /dev/null +++ b/website/docs/language/functions/issensitive.mdx @@ -0,0 +1,27 @@ +--- +page_title: issensitive - Functions - Configuration Language +description: The issensitive function true if the value passed is marked as sensitive +--- + +# `issensitive` Function + +-> **Note:** This function is only available in Terraform v1.8 and later. + +`issensitive` takes any value and returns true if Terraform +treats it as sensitive, with the same meaning and behavior as for +[sensitive input variables](/terraform/language/values/variables#suppressing-values-in-cli-output). + +If a value not marked as sensitive is passed the function returns false. + +See [`sensitive`](/terraform/language/functions/sensitive), [`nonsensitive`](/terraform/language/functions/nonsensitive), and [sensitive input variables](/terraform/language/values/variables#suppressing-values-in-cli-output) for more information on sensitive values. + +## Examples + +``` +> issensitive(sensitive("secret")) +true +> issensitive("hello") +false +> issensitive(var.my-var-with-sensitive-set-to-true) +true +``` diff --git a/website/docs/language/functions/join.mdx b/website/docs/language/functions/join.mdx index 6d8bd70c63..a917553f2b 100644 --- a/website/docs/language/functions/join.mdx +++ b/website/docs/language/functions/join.mdx @@ -7,8 +7,8 @@ description: |- # `join` Function -`join` produces a string by concatenating together all elements of a given -list of strings with the given delimiter. +`join` produces a string by concatenating all of the elements of the specified +list of strings with the specified separator. ```hcl join(separator, list) @@ -17,6 +17,8 @@ join(separator, list) ## Examples ``` +> join("-", ["foo", "bar", "baz"]) +"foo-bar-baz" > join(", ", ["foo", "bar", "baz"]) foo, bar, baz > join(", ", ["foo"]) @@ -25,5 +27,5 @@ foo ## Related Functions -* [`split`](/language/functions/split) performs the opposite operation: producing a list +* [`split`](/terraform/language/functions/split) performs the opposite operation: producing a list by separating a single string using a given delimiter. diff --git a/website/docs/language/functions/jsondecode.mdx b/website/docs/language/functions/jsondecode.mdx index fe16e2bd25..e34322b1d1 100644 --- a/website/docs/language/functions/jsondecode.mdx +++ b/website/docs/language/functions/jsondecode.mdx @@ -13,7 +13,7 @@ of the result of decoding that string. The JSON encoding is defined in [RFC 7159](https://tools.ietf.org/html/rfc7159). This function maps JSON values to -[Terraform language values](/language/expressions/types) +[Terraform language values](/terraform/language/expressions/types) in the following way: | JSON type | Terraform type | @@ -42,5 +42,5 @@ true ## Related Functions -* [`jsonencode`](/language/functions/jsonencode) performs the opposite operation, _encoding_ +* [`jsonencode`](/terraform/language/functions/jsonencode) performs the opposite operation, _encoding_ a value as JSON. diff --git a/website/docs/language/functions/jsonencode.mdx b/website/docs/language/functions/jsonencode.mdx index 5c4fb5c133..4afa3bcf07 100644 --- a/website/docs/language/functions/jsonencode.mdx +++ b/website/docs/language/functions/jsonencode.mdx @@ -10,7 +10,7 @@ description: The jsonencode function encodes a given value as a JSON string. The JSON encoding is defined in [RFC 7159](https://tools.ietf.org/html/rfc7159). This function maps -[Terraform language values](/language/expressions/types) +[Terraform language values](/terraform/language/expressions/types) to JSON values in the following way: | Terraform type | JSON type | @@ -46,5 +46,5 @@ The `jsonencode` command outputs a minified representation of the input. ## Related Functions -* [`jsondecode`](/language/functions/jsondecode) performs the opposite operation, _decoding_ +* [`jsondecode`](/terraform/language/functions/jsondecode) performs the opposite operation, _decoding_ a JSON string to obtain its represented value. diff --git a/website/docs/language/functions/keys.mdx b/website/docs/language/functions/keys.mdx index e97083730b..96a1553888 100644 --- a/website/docs/language/functions/keys.mdx +++ b/website/docs/language/functions/keys.mdx @@ -23,4 +23,4 @@ be identical as long as the keys in the map don't change. ## Related Functions -* [`values`](/language/functions/values) returns a list of the _values_ from a map. +* [`values`](/terraform/language/functions/values) returns a list of the _values_ from a map. diff --git a/website/docs/language/functions/list.mdx b/website/docs/language/functions/list.mdx index 0fb9a13c8f..c62663e66e 100644 --- a/website/docs/language/functions/list.mdx +++ b/website/docs/language/functions/list.mdx @@ -11,16 +11,16 @@ but Terraform v0.12 introduced a new first-class syntax. To update an expression like `list(a, b, c)`, write the following instead: -``` +```hcl tolist([a, b, c]) ``` The `[ ... ]` brackets construct a tuple value, and then the `tolist` function then converts it to a list. For more information on the value types in the -Terraform language, see [Type Constraints](/language/expressions/types). +Terraform language, see [Type Constraints](/terraform/language/expressions/types). ## Related Functions -* [`concat`](/language/functions/concat) produces a new list by concatenating together the +* [`concat`](/terraform/language/functions/concat) produces a new list by concatenating together the elements from other lists. -* [`tolist`](/language/functions/tolist) converts a set or tuple value to a list. +* [`tolist`](/terraform/language/functions/tolist) converts a set or tuple value to a list. diff --git a/website/docs/language/functions/lookup.mdx b/website/docs/language/functions/lookup.mdx index 55a92fe0e4..f08ffc9ada 100644 --- a/website/docs/language/functions/lookup.mdx +++ b/website/docs/language/functions/lookup.mdx @@ -8,7 +8,7 @@ description: The lookup function retrieves an element value from a map given its `lookup` retrieves the value of a single element from a map, given its key. If the given key does not exist, the given default value is returned instead. -``` +```hcl lookup(map, key, default) ``` @@ -27,4 +27,4 @@ what? ## Related Functions -* [`element`](/language/functions/element) retrieves a value from a _list_ given its _index_. +* [`element`](/terraform/language/functions/element) retrieves a value from a _list_ given its _index_. diff --git a/website/docs/language/functions/lower.mdx b/website/docs/language/functions/lower.mdx index 638cb349ed..2e999ebd04 100644 --- a/website/docs/language/functions/lower.mdx +++ b/website/docs/language/functions/lower.mdx @@ -14,13 +14,13 @@ description: >- ``` > lower("HELLO") hello -> lower("АЛЛО!") -алло! +> lower("ΓΕΙΑ ΣΟΥ") +"γεια σου" ``` This function uses Unicode's definition of letters and of upper- and lowercase. ## Related Functions -* [`upper`](/language/functions/upper) converts letters in a string to _uppercase_. -* [`title`](/language/functions/title) converts the first letter of each word in a string to uppercase. +* [`upper`](/terraform/language/functions/upper) converts letters in a string to _uppercase_. +* [`title`](/terraform/language/functions/title) converts the first letter of each word in a string to uppercase. diff --git a/website/docs/language/functions/map.mdx b/website/docs/language/functions/map.mdx index ae05bf5476..85d603c727 100644 --- a/website/docs/language/functions/map.mdx +++ b/website/docs/language/functions/map.mdx @@ -11,7 +11,7 @@ but Terraform v0.12 introduced a new first-class syntax. To update an expression like `map("a", "b", "c", "d")`, write the following instead: -``` +```hcl tomap({ a = "b" c = "d" @@ -20,10 +20,10 @@ tomap({ The `{ ... }` braces construct an object value, and then the `tomap` function then converts it to a map. For more information on the value types in the -Terraform language, see [Type Constraints](/language/expressions/types). +Terraform language, see [Type Constraints](/terraform/language/expressions/types). ## Related Functions -* [`tomap`](/language/functions/tomap) converts an object value to a map. -* [`zipmap`](/language/functions/zipmap) constructs a map dynamically, by taking keys from +* [`tomap`](/terraform/language/functions/tomap) converts an object value to a map. +* [`zipmap`](/terraform/language/functions/zipmap) constructs a map dynamically, by taking keys from one list and values from another list. diff --git a/website/docs/language/functions/max.mdx b/website/docs/language/functions/max.mdx index bb453cc5f7..05a4ee6b07 100644 --- a/website/docs/language/functions/max.mdx +++ b/website/docs/language/functions/max.mdx @@ -24,4 +24,4 @@ to individual arguments: ## Related Functions -* [`min`](/language/functions/min), which returns the _smallest_ number from a set. +* [`min`](/terraform/language/functions/min), which returns the _smallest_ number from a set. diff --git a/website/docs/language/functions/md5.mdx b/website/docs/language/functions/md5.mdx index fdaf0b9723..97e61f1960 100644 --- a/website/docs/language/functions/md5.mdx +++ b/website/docs/language/functions/md5.mdx @@ -14,7 +14,8 @@ The given string is first encoded as UTF-8 and then the MD5 algorithm is applied as defined in [RFC 1321](https://tools.ietf.org/html/rfc1321). The raw hash is then encoded to lowercase hexadecimal digits before returning. -Before using this function for anything security-sensitive, refer to +Collision attacks have been successfully performed against this hashing +function. Before using this function for anything security-sensitive, refer to [RFC 6151](https://tools.ietf.org/html/rfc6151) for updated security considerations applying to the MD5 algorithm. @@ -27,5 +28,5 @@ considerations applying to the MD5 algorithm. ## Related Functions -* [`filemd5`](/language/functions/filemd5) calculates the same hash from +* [`filemd5`](/terraform/language/functions/filemd5) calculates the same hash from the contents of a file rather than from a string value. diff --git a/website/docs/language/functions/merge.mdx b/website/docs/language/functions/merge.mdx index dfc70aaa28..6a3d230a73 100644 --- a/website/docs/language/functions/merge.mdx +++ b/website/docs/language/functions/merge.mdx @@ -39,7 +39,7 @@ type structure of the attributes after the merging rules have been applied. } ``` -The following example uses the expansion symbol (...) to transform the value into separate arguments. Refer to [Expanding Function Argument](/language/expressions/function-calls#expanding-function-arguments) for details. +The following example uses the expansion symbol (...) to transform the value into separate arguments. Refer to [Expanding Function Argument](/terraform/language/expressions/function-calls#expanding-function-arguments) for details. ``` > merge([{a="b", c="d"}, {}, {e="f", c="z"}]...) diff --git a/website/docs/language/functions/min.mdx b/website/docs/language/functions/min.mdx index d58765c272..f5388b2cc0 100644 --- a/website/docs/language/functions/min.mdx +++ b/website/docs/language/functions/min.mdx @@ -24,4 +24,4 @@ to individual arguments: ## Related Functions -* [`max`](/language/functions/max), which returns the _greatest_ number from a set. +* [`max`](/terraform/language/functions/max), which returns the _greatest_ number from a set. diff --git a/website/docs/language/functions/nonsensitive.mdx b/website/docs/language/functions/nonsensitive.mdx index df3186d18d..c25ba18432 100644 --- a/website/docs/language/functions/nonsensitive.mdx +++ b/website/docs/language/functions/nonsensitive.mdx @@ -73,10 +73,8 @@ due to an inappropriate call to `nonsensitive` in your module, that's a bug in your module and not a bug in Terraform itself. **Use this function sparingly and only with due care.** -`nonsensitive` will return an error if you pass a value that isn't marked -as sensitive, because such a call would be redundant and potentially confusing -or misleading to a future maintainer of your module. Use `nonsensitive` only -after careful consideration and with definite intent. +`nonsensitive` will make no changes to values that aren't marked as sensitive, even though such a call may be redundant and potentially confusing. +Use `nonsensitive` only after careful consideration and with definite intent. Consider including a comment adjacent to your call to explain to future maintainers what makes the usage safe and thus what invariants they must take @@ -91,11 +89,11 @@ the local value `mixed_content`, with a valid JSON string assigned to ``` > var.mixed_content_json -(sensitive) +(sensitive value) > local.mixed_content -(sensitive) +(sensitive value) > local.mixed_content["password"] -(sensitive) +(sensitive value) > nonsensitive(local.mixed_content["username"]) "zqb" > nonsensitive("clear") diff --git a/website/docs/language/functions/one.mdx b/website/docs/language/functions/one.mdx index fca597dfaf..ceb6a16164 100644 --- a/website/docs/language/functions/one.mdx +++ b/website/docs/language/functions/one.mdx @@ -47,7 +47,7 @@ no instances were created. ## Relationship to the "Splat" Operator The Terraform language has a built-in operator `[*]`, known as -[the _splat_ operator](/language/expressions/splat), and one of its functions +[the _splat_ operator](/terraform/language/expressions/splat), and one of its functions is to translate a primitive value that might be null into a list of either zero or one elements: diff --git a/website/docs/language/functions/parseint.mdx b/website/docs/language/functions/parseint.mdx index e7ecb94011..952a147a0a 100644 --- a/website/docs/language/functions/parseint.mdx +++ b/website/docs/language/functions/parseint.mdx @@ -46,5 +46,5 @@ Invalid value for "number" parameter: cannot parse "12" as a base 2 integer. ## Related Functions -* [`format`](/language/functions/format) can format numbers and other values into strings, +* [`format`](/terraform/language/functions/format) can format numbers and other values into strings, with optional zero padding, alignment, etc. diff --git a/website/docs/language/functions/plantimestamp.mdx b/website/docs/language/functions/plantimestamp.mdx new file mode 100644 index 0000000000..7b3a41e0d0 --- /dev/null +++ b/website/docs/language/functions/plantimestamp.mdx @@ -0,0 +1,54 @@ +--- +page_title: plantimestamp - Functions - Configuration Language +description: |- + The plantimestamp functions a string representation of the date and time + during the plan. +--- + +# `plantimestamp` Function + +-> **Note:** This function is only available in Terraform v1.5 and later. + +`plantimestamp` returns a UTC timestamp string in [RFC 3339](https://tools.ietf.org/html/rfc3339) format. + +In the Terraform language, timestamps are conventionally represented as +strings using [RFC 3339](https://tools.ietf.org/html/rfc3339) +"Date and Time format" syntax, and so `plantimestamp` returns a string +in this format. + +The result of this function will change for every plan operation. It is intended +for use within [Custom Conditions](/terraform/language/expressions/custom-conditions) +as a way to validate time sensitive resources such as TLS certificates. + +There are circumstances, such as during a Terraform [Refresh-only](/terraform/cli/commands/plan#planning-modes) plan, where +the value for this function will be recomputed but not propagated to resources +defined within the configuration. As such, it is recommended that this function +only be used to compare against timestamps exported by providers and not against +timestamps generated in the configuration. + +The `plantimestamp` function is not available within the Terraform console. + +## Examples + +``` +> plantimestamp() +2018-05-13T07:44:12Z +``` + +```terraform +check "terraform_io_certificate" { + data "tls_certificate" "terraform_io" { + url = "https://www.terraform.io/" + } + + assert { + condition = timecmp(plantimestamp(), data.tls_certificate.terraform_io.certificates[0].not_after) < 0 + error_message = "terraform.io certificate has expired" + } +} +``` + +## Related Functions + +* [`timestamp`](/terraform/language/functions/timestamp) returns the current timestamp when it is evaluated +during the apply step. \ No newline at end of file diff --git a/website/docs/language/functions/regex.mdx b/website/docs/language/functions/regex.mdx index 08086e9234..98edcdf616 100644 --- a/website/docs/language/functions/regex.mdx +++ b/website/docs/language/functions/regex.mdx @@ -30,7 +30,7 @@ It's not valid to mix both named and unnamed capture groups in the same pattern. If the given pattern does not match at all, the `regex` raises an error. To _test_ whether a given pattern matches a string, use -[`regexall`](/language/functions/regexall) and test that the result has length greater than +[`regexall`](/terraform/language/functions/regexall) and test that the result has length greater than zero. The pattern is a string containing a mixture of literal characters and special @@ -71,7 +71,7 @@ of the pattern must be escaped as `\\`. | `\PN` | The opposite of `\pN` | | `\P{Greek}` | The opposite of `\p{Greek}` | | `xy` | `x` followed immediately by `y` | -| x\|y | either `x` or `y`, preferring `x` | +| `x|y` | either `x` or `y`, preferring `x` | | `x*` | zero or more `x`, preferring more | | `x*?` | zero or more `x`, preferring fewer | | `x+` | one or more `x`, preferring more | @@ -86,7 +86,7 @@ of the pattern must be escaped as `\\`. | `(x)` | unnamed capture group for sub-pattern `x` | | `(?Px)` | named capture group, named `name`, for sub-pattern `x` | | `(?:x)` | non-capturing sub-pattern `x` | -| `\*` | Literal `*` for any punctuation character `*` | +| `\*` | Literal `*` for any punctuation character `*` For example, `\.` is a literal `.` | | `\Q...\E` | Literal `...` for any text `...` as long as it does not include literally `\E` | In addition to the above matching operators that consume the characters they @@ -152,8 +152,8 @@ string. ## Related Functions -- [`regexall`](/language/functions/regexall) searches for potentially multiple matches of a given pattern in a string. -- [`replace`](/language/functions/replace) replaces a substring of a string with another string, optionally matching using the same regular expression syntax as `regex`. +- [`regexall`](/terraform/language/functions/regexall) searches for potentially multiple matches of a given pattern in a string. +- [`replace`](/terraform/language/functions/replace) replaces a substring of a string with another string, optionally matching using the same regular expression syntax as `regex`. If Terraform already has a more specialized function to parse the syntax you are trying to match, prefer to use that function instead. Regular expressions diff --git a/website/docs/language/functions/regexall.mdx b/website/docs/language/functions/regexall.mdx index 886052ef4a..ea794af23b 100644 --- a/website/docs/language/functions/regexall.mdx +++ b/website/docs/language/functions/regexall.mdx @@ -15,7 +15,7 @@ to a string and returns a list of all matches. regexall(pattern, string) ``` -`regexall` is a variant of [`regex`](/language/functions/regex) and uses the same pattern +`regexall` is a variant of [`regex`](/terraform/language/functions/regex) and uses the same pattern syntax. For any given input to `regex`, `regexall` returns a list of whatever type `regex` would've returned, with one element per match. That is: @@ -48,7 +48,7 @@ false ## Related Functions -- [`regex`](/language/functions/regex) searches for a single match of a given pattern, and +- [`regex`](/terraform/language/functions/regex) searches for a single match of a given pattern, and returns an error if no match is found. If Terraform already has a more specialized function to parse the syntax you diff --git a/website/docs/language/functions/replace.mdx b/website/docs/language/functions/replace.mdx index 9ffd958325..e4e76a1c68 100644 --- a/website/docs/language/functions/replace.mdx +++ b/website/docs/language/functions/replace.mdx @@ -16,7 +16,7 @@ replace(string, substring, replacement) If `substring` is wrapped in forward slashes, it is treated as a regular expression, using the same pattern syntax as -[`regex`](/language/functions/regex). If using a regular expression for the substring +[`regex`](/terraform/language/functions/regex). If using a regular expression for the substring argument, the `replacement` string can incorporate captured strings from the input by using an `$n` sequence, where `n` is the index or name of a capture group. @@ -33,5 +33,5 @@ hello everybody ## Related Functions -- [`regex`](/language/functions/regex) searches a given string for a substring matching a +- [`regex`](/terraform/language/functions/regex) searches a given string for a substring matching a given regular expression pattern. diff --git a/website/docs/language/functions/reverse.mdx b/website/docs/language/functions/reverse.mdx index 257202c426..c46350a256 100644 --- a/website/docs/language/functions/reverse.mdx +++ b/website/docs/language/functions/reverse.mdx @@ -21,4 +21,4 @@ with all of the same elements as the given sequence but in reverse order. ## Related Functions -* [`strrev`](/language/functions/strrev) reverses a string. +* [`strrev`](/terraform/language/functions/strrev) reverses a string. diff --git a/website/docs/language/functions/sensitive.mdx b/website/docs/language/functions/sensitive.mdx index b3a3bf220d..2aeb2da177 100644 --- a/website/docs/language/functions/sensitive.mdx +++ b/website/docs/language/functions/sensitive.mdx @@ -9,7 +9,7 @@ description: The sensitive function marks a value as being sensitive. `sensitive` takes any value and returns a copy of it marked so that Terraform will treat it as sensitive, with the same meaning and behavior as for -[sensitive input variables](/language/values/variables#suppressing-values-in-cli-output). +[sensitive input variables](/terraform/language/values/variables#suppressing-values-in-cli-output). Wherever possible we recommend marking your input variable and/or output value declarations as sensitive directly, instead of using this function, because in @@ -20,7 +20,7 @@ The `sensitive` function might be useful in some less-common situations where a sensitive value arises from a definition _within_ your module, such as if you've loaded sensitive data from a file on disk as part of your configuration: -``` +```hcl locals { sensitive_content = sensitive(file("${path.module}/sensitive.txt")) } @@ -34,9 +34,9 @@ because they may be exposed in other ways outside of Terraform's control. ``` > sensitive(1) -(sensitive) +(sensitive value) > sensitive("hello") -(sensitive) +(sensitive value) > sensitive([]) -(sensitive) +(sensitive value) ``` diff --git a/website/docs/language/functions/setintersection.mdx b/website/docs/language/functions/setintersection.mdx index 296c6a185c..74c7de92a7 100644 --- a/website/docs/language/functions/setintersection.mdx +++ b/website/docs/language/functions/setintersection.mdx @@ -1,39 +1,75 @@ --- -page_title: setintersection - Functions - Configuration Language +page_title: setintersection function reference - Functions - Configuration Language description: |- - The setintersection function takes multiple sets and produces a single set - containing only the elements that all of the given sets have in common. + The setintersection function takes multiple arrays and produces a single array + containing only the elements that all of the given arrays have in common. --- -# `setintersection` Function +# `setintersection` function reference + +This topic provides reference information about the `setintersection` function, +which computes the intersection of the specified sets. + +## Introduction The `setintersection` function takes multiple sets and produces a single set containing only the elements that all of the given sets have in common. -In other words, it computes the -[intersection](https://en.wikipedia.org/wiki/Intersection_\(set_theory\)) of the sets. +In other words, `setintersection` computes the intersection of the sets. +Refer to Wikipedia's [Intersection (set +theory)](https://en.wikipedia.org/wiki/Intersection_\(set_theory\)) article +for a mathematical explanantion of set theory intersection. + +## Syntax + +Use the `setintersection` function with the following syntax: ```hcl setintersection(sets...) ``` -## Examples +Replace `sets...` with a comma-delimited list of sets such as `["a","b"], ["a","c","g"]`. The elements of +the different sets do not have to be the same type. -``` +The `setintersection` result is an unordered set. + +## Example use cases + +This example passes in sets of strings and returns a set with one element. + +```hcl > setintersection(["a", "b"], ["b", "c"], ["b", "d"]) [ "b", ] ``` -The given arguments are converted to sets, so the result is also a set and -the ordering of the given elements is not preserved. +This example passes in number sets of different sizes and returns a set with two elements. + +```hcl +> setintersection([3,3.3,4], [4,3.3,65,99], [4.0,3.3]) +toset([ + 3.3, + 4, +]) +``` + +This examples pass in sets of different lengths and element types. +The result is a set of two string elements. + +```hcl +> setintersection(["bob","jane",3], ["jane",3,"ajax",10], ["3","jane",26,"nomad"]) +toset([ + "3", + "jane", +]) +``` ## Related Functions -* [`contains`](/language/functions/contains) tests whether a given list or set contains +* [`contains`](/terraform/language/functions/contains) tests whether a given list or set contains a given element value. -* [`setproduct`](/language/functions/setproduct) computes the _Cartesian product_ of multiple +* [`setproduct`](/terraform/language/functions/setproduct) computes the _Cartesian product_ of multiple sets. -* [`setsubtract`](/language/functions/setsubtract) computes the _relative complement_ of two sets -* [`setunion`](/language/functions/setunion) computes the _union_ of +* [`setsubtract`](/terraform/language/functions/setsubtract) computes the _relative complement_ of two sets +* [`setunion`](/terraform/language/functions/setunion) computes the _union_ of multiple sets. diff --git a/website/docs/language/functions/setproduct.mdx b/website/docs/language/functions/setproduct.mdx index 5d1469f7eb..39298bbcb2 100644 --- a/website/docs/language/functions/setproduct.mdx +++ b/website/docs/language/functions/setproduct.mdx @@ -115,9 +115,9 @@ elements all have a consistent type: ## Finding combinations for `for_each` The -[resource `for_each`](/language/meta-arguments/for_each) +[resource `for_each`](/terraform/language/meta-arguments/for_each) and -[`dynamic` block](/language/expressions/dynamic-blocks) +[`dynamic` block](/terraform/language/expressions/dynamic-blocks) language features both require a collection value that has one element for each repetition. @@ -190,9 +190,9 @@ resource "aws_subnet" "example" { # local.network_subnets is a list, so project it into a map # where each key is unique. Combine the network and subnet keys to # produce a single unique key per instance. - for_each = { + for_each = tomap({ for subnet in local.network_subnets : "${subnet.network_key}.${subnet.subnet_key}" => subnet - } + }) vpc_id = each.value.network_id availability_zone = each.value.subnet_key @@ -224,7 +224,7 @@ subnets = { } ``` -The `nework_subnets` output would look similar to the following: +The `network_subnets` output would look similar to the following: ```hcl [ @@ -269,13 +269,13 @@ The `nework_subnets` output would look similar to the following: ## Related Functions -- [`contains`](/language/functions/contains) tests whether a given list or set contains +- [`contains`](/terraform/language/functions/contains) tests whether a given list or set contains a given element value. -- [`flatten`](/language/functions/flatten) is useful for flattening hierarchical data +- [`flatten`](/terraform/language/functions/flatten) is useful for flattening hierarchical data into a single list, for situations where the relationships between two object types are defined explicitly. -- [`setintersection`](/language/functions/setintersection) computes the _intersection_ of +- [`setintersection`](/terraform/language/functions/setintersection) computes the _intersection_ of multiple sets. -- [`setsubtract`](/language/functions/setsubtract) computes the _relative complement_ of two sets -- [`setunion`](/language/functions/setunion) computes the _union_ of multiple +- [`setsubtract`](/terraform/language/functions/setsubtract) computes the _relative complement_ of two sets +- [`setunion`](/terraform/language/functions/setunion) computes the _union_ of multiple sets. diff --git a/website/docs/language/functions/setsubtract.mdx b/website/docs/language/functions/setsubtract.mdx index dafeca4231..5397ed8168 100644 --- a/website/docs/language/functions/setsubtract.mdx +++ b/website/docs/language/functions/setsubtract.mdx @@ -18,25 +18,25 @@ setsubtract(a, b) ``` > setsubtract(["a", "b", "c"], ["a", "c"]) -[ +toset([ "b", -] +]) ``` ### Set Difference (Symmetric Difference) ``` > setunion(setsubtract(["a", "b", "c"], ["a", "c", "d"]), setsubtract(["a", "c", "d"], ["a", "b", "c"])) -[ +toset([ "b", "d", -] +]) ``` ## Related Functions -* [`setintersection`](/language/functions/setintersection) computes the _intersection_ of multiple sets -* [`setproduct`](/language/functions/setproduct) computes the _Cartesian product_ of multiple +* [`setintersection`](/terraform/language/functions/setintersection) computes the _intersection_ of multiple sets +* [`setproduct`](/terraform/language/functions/setproduct) computes the _Cartesian product_ of multiple sets. -* [`setunion`](/language/functions/setunion) computes the _union_ of +* [`setunion`](/terraform/language/functions/setunion) computes the _union_ of multiple sets. diff --git a/website/docs/language/functions/setunion.mdx b/website/docs/language/functions/setunion.mdx index bbd36c6fe6..85874bd030 100644 --- a/website/docs/language/functions/setunion.mdx +++ b/website/docs/language/functions/setunion.mdx @@ -33,10 +33,10 @@ the ordering of the given elements is not preserved. ## Related Functions -* [`contains`](/language/functions/contains) tests whether a given list or set contains +* [`contains`](/terraform/language/functions/contains) tests whether a given list or set contains a given element value. -* [`setintersection`](/language/functions/setintersection) computes the _intersection_ of +* [`setintersection`](/terraform/language/functions/setintersection) computes the _intersection_ of multiple sets. -* [`setproduct`](/language/functions/setproduct) computes the _Cartesian product_ of multiple +* [`setproduct`](/terraform/language/functions/setproduct) computes the _Cartesian product_ of multiple sets. -* [`setsubtract`](/language/functions/setsubtract) computes the _relative complement_ of two sets +* [`setsubtract`](/terraform/language/functions/setsubtract) computes the _relative complement_ of two sets diff --git a/website/docs/language/functions/sha1.mdx b/website/docs/language/functions/sha1.mdx index f5e2e04c62..dde0142fd3 100644 --- a/website/docs/language/functions/sha1.mdx +++ b/website/docs/language/functions/sha1.mdx @@ -27,5 +27,5 @@ relevant literature to understand the security implications. ## Related Functions -* [`filesha1`](/language/functions/filesha1) calculates the same hash from +* [`filesha1`](/terraform/language/functions/filesha1) calculates the same hash from the contents of a file rather than from a string value. diff --git a/website/docs/language/functions/sha256.mdx b/website/docs/language/functions/sha256.mdx index 9dee5324b8..a4bba263e2 100644 --- a/website/docs/language/functions/sha256.mdx +++ b/website/docs/language/functions/sha256.mdx @@ -25,7 +25,7 @@ b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 ## Related Functions -* [`filesha256`](/language/functions/filesha256) calculates the same hash from +* [`filesha256`](/terraform/language/functions/filesha256) calculates the same hash from the contents of a file rather than from a string value. -* [`base64sha256`](/language/functions/base64sha256) calculates the same hash but returns +* [`base64sha256`](/terraform/language/functions/base64sha256) calculates the same hash but returns the result in a more-compact Base64 encoding. diff --git a/website/docs/language/functions/sha512.mdx b/website/docs/language/functions/sha512.mdx index 37e68753d9..83f55882f2 100644 --- a/website/docs/language/functions/sha512.mdx +++ b/website/docs/language/functions/sha512.mdx @@ -23,7 +23,7 @@ then encoded to lowercase hexadecimal digits before returning. ## Related Functions -* [`filesha512`](/language/functions/filesha512) calculates the same hash from +* [`filesha512`](/terraform/language/functions/filesha512) calculates the same hash from the contents of a file rather than from a string value. -* [`base64sha512`](/language/functions/base64sha512) calculates the same hash but returns +* [`base64sha512`](/terraform/language/functions/base64sha512) calculates the same hash but returns the result in a more-compact Base64 encoding. diff --git a/website/docs/language/functions/slice.mdx b/website/docs/language/functions/slice.mdx index dfc0ddd5c1..570c282dd5 100644 --- a/website/docs/language/functions/slice.mdx +++ b/website/docs/language/functions/slice.mdx @@ -27,5 +27,5 @@ list. ## Related Functions -* [`substr`](/language/functions/substr) performs a similar function for characters in a +* [`substr`](/terraform/language/functions/substr) performs a similar function for characters in a string, although it uses a length instead of an end index. diff --git a/website/docs/language/functions/split.mdx b/website/docs/language/functions/split.mdx index 50036ed804..2d22e03a24 100644 --- a/website/docs/language/functions/split.mdx +++ b/website/docs/language/functions/split.mdx @@ -35,5 +35,5 @@ split(separator, string) ## Related Functions -* [`join`](/language/functions/join) performs the opposite operation: producing a string +* [`join`](/terraform/language/functions/join) performs the opposite operation: producing a string joining together a list of strings with a given separator. diff --git a/website/docs/language/functions/startswith.mdx b/website/docs/language/functions/startswith.mdx index d49f7aa0ba..d9ff41c912 100644 --- a/website/docs/language/functions/startswith.mdx +++ b/website/docs/language/functions/startswith.mdx @@ -1,5 +1,5 @@ --- -page_title: startsswith - Functions - Configuration Language +page_title: startswith - Functions - Configuration Language description: |- The startswith function takes two values: a string to check and a prefix string. It returns true if the string begins with that exact prefix. --- @@ -24,4 +24,4 @@ false ## Related Functions -- [`endswith`](/language/functions/endswith) takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix. \ No newline at end of file +- [`endswith`](/terraform/language/functions/endswith) takes two values: a string to check and a suffix string. The function returns true if the first string ends with that exact suffix. \ No newline at end of file diff --git a/website/docs/language/functions/strcontains.mdx b/website/docs/language/functions/strcontains.mdx new file mode 100644 index 0000000000..6e684a6f6a --- /dev/null +++ b/website/docs/language/functions/strcontains.mdx @@ -0,0 +1,25 @@ +--- +page_title: strcontains - Functions - Configuration Language +description: |- + The strcontains function checks whether a given string can be found within another string. +--- + +# `strcontains` Function + +`strcontains` function checks whether a substring is within another string. + +```hcl +strcontains(string, substr) +``` + +## Examples + +``` +> strcontains("hello world", "wor") +true +``` + +``` +> strcontains("hello world", "wod") +false +``` \ No newline at end of file diff --git a/website/docs/language/functions/strrev.mdx b/website/docs/language/functions/strrev.mdx index b94949c2c0..f5e1e36a2a 100644 --- a/website/docs/language/functions/strrev.mdx +++ b/website/docs/language/functions/strrev.mdx @@ -23,4 +23,4 @@ olleh ## Related Functions -* [`reverse`](/language/functions/reverse) reverses a sequence. +* [`reverse`](/terraform/language/functions/reverse) reverses a sequence. diff --git a/website/docs/language/functions/sum.mdx b/website/docs/language/functions/sum.mdx index 2528240b9f..b8bf154da1 100644 --- a/website/docs/language/functions/sum.mdx +++ b/website/docs/language/functions/sum.mdx @@ -9,6 +9,8 @@ description: |- `sum` takes a list or set of numbers and returns the sum of those numbers. +`sum` fails when given an empty list or set. + ## Examples ``` diff --git a/website/docs/language/functions/templatefile.mdx b/website/docs/language/functions/templatefile.mdx index 674002550c..6d79bff054 100644 --- a/website/docs/language/functions/templatefile.mdx +++ b/website/docs/language/functions/templatefile.mdx @@ -15,13 +15,13 @@ templatefile(path, vars) ``` The template syntax is the same as for -[string templates](/language/expressions/strings#string-templates) +[string templates](/terraform/language/expressions/strings#string-templates) in the main Terraform language, including interpolation sequences delimited with `${` ... `}`. This function just allows longer template sequences to be factored out into a separate file for readability. -The "vars" argument must be a map. Within the template file, each of the keys -in the map is available as a variable for interpolation. The template may +The "vars" argument must be an object. Within the template file, each of the +keys in the map is available as a variable for interpolation. The template may also use any other function available in the Terraform language, except that recursive calls to `templatefile` are not permitted. Variable names must each start with a letter, followed by zero or more letters, digits, or @@ -60,7 +60,6 @@ The `templatefile` function renders the template: > templatefile("${path.module}/backends.tftpl", { port = 8080, ip_addrs = ["10.0.0.1", "10.0.0.2"] }) backend 10.0.0.1:8080 backend 10.0.0.2:8080 - ``` ### Maps @@ -99,9 +98,9 @@ YAML that will be interpreted correctly when using lots of individual interpolation sequences and directives. Instead, you can write a template that consists only of a single interpolated -call to either [`jsonencode`](/language/functions/jsonencode) or -[`yamlencode`](/language/functions/yamlencode), specifying the value to encode using -[normal Terraform expression syntax](/language/expressions) +call to either [`jsonencode`](/terraform/language/functions/jsonencode) or +[`yamlencode`](/terraform/language/functions/yamlencode), specifying the value to encode using +[normal Terraform expression syntax](/terraform/language/expressions) as in the following examples: ``` @@ -121,9 +120,9 @@ this will produce a valid JSON or YAML representation of the given data structure, without the need to manually handle escaping or delimiters. In the latest examples above, the repetition based on elements of `ip_addrs` is achieved by using a -[`for` expression](/language/expressions/for) +[`for` expression](/terraform/language/expressions/for) rather than by using -[template directives](/language/expressions/strings#directives). +[template directives](/terraform/language/expressions/strings#directives). ```json {"backends":["10.0.0.1:8080","10.0.0.2:8080"]} @@ -142,9 +141,10 @@ locals { ``` For more information, see the main documentation for -[`jsonencode`](/language/functions/jsonencode) and [`yamlencode`](/language/functions/yamlencode). +[`jsonencode`](/terraform/language/functions/jsonencode) and [`yamlencode`](/terraform/language/functions/yamlencode). ## Related Functions -* [`file`](/language/functions/file) reads a file from disk and returns its literal contents +* [`file`](/terraform/language/functions/file) reads a file from disk and returns its literal contents without any template interpretation. +* [`templatestring`](/terraform/language/functions/templatestring) takes a simple reference to a string value containing the template and renders its content. diff --git a/website/docs/language/functions/templatestring.mdx b/website/docs/language/functions/templatestring.mdx new file mode 100644 index 0000000000..843325c92d --- /dev/null +++ b/website/docs/language/functions/templatestring.mdx @@ -0,0 +1,68 @@ +--- +page_title: templatestring - Functions - Configuration Language +description: |- + The templatestring function takes a string from elsewhere in the module and renders its content as a template using a supplied set of template variables. +--- + +# `templatestring` function reference + +This topic provides reference information about the `templatestring` function. The `templatestring` function renders a string defined elsewhere in the module as a template using a set of variables. + +## Introduction + +The primary use case for the `templatestring` function is to render templates fetched as a single string from remote locations. The function enables advanced use cases where a [string template expression](/terraform/language/expressions/strings#string-templates) is insufficient, such as when the template is available from a named object declared in the current module. Refer to [Dynamic template construction](#dynamic-template-construction) for additional information. + +To render a template from a file, use the [`templatefile` function](/terraform/language/functions/templatefile). + +## Syntax + +Write `templatestring` functions using the following syntax: + +```hcl +templatestring(ARG1, ARG2, . . .) +``` + +Specify the following arguments: + +- The first argument is always a reference to an object defined in the module. You cannot supply the template expression directly as the first argument. +- The second argument is an object that specifies a variable to use for rendering the template. +- You can specify additional arguments to use multiple variables for the template. + +In the following example, the function renders the string value located at `data.aws_s3_object.example.body` as the template: + +```hcl +templatestring(data.aws_s3_object.example.body, { + name = var.name +}) +``` + +For information about the syntax you can use for the variables arguments, refer to [Strings and Templates](/terraform/language/expressions/strings). + +## Example use case + +The following example retrieves a template from S3 and dynamically renders it: + +```hcl +data "aws_s3_object" "example" { + bucket = "example-example" + key = "example.tmpl" +} + +output "example" { + value = templatestring(data.aws_s3_object.example.body, { + name = var.name + }) +} +``` + +For more examples of how to use templates, refer to the documentation for [the `templatefile` function](/terraform/language/functions/templatefile#Examples). + +## Dynamic template construction + +You can write an expression that builds a template dynamically and then assigns it to a [local value](/terraform/language/values/locals). You can then use a reference to that local value as the first argument to the `templatestring` function. + +Note that you should only dynamically construct templates in this way when no other alternative is feasible. This is because the result can be difficult to understand and maintain and is susceptible to unexpected inputs. Built-in Terraform functions may interact with the local filesystem. As a result, the inputs may produce a template that includes data from arbitrary files on the system where Terraform is running. + +## Related Functions + +* [`templatefile`](/terraform/language/functions/templatefile) reads a file from disk and renders its content as a template. diff --git a/website/docs/language/functions/terraform-applying.mdx b/website/docs/language/functions/terraform-applying.mdx new file mode 100644 index 0000000000..7cd525d3d0 --- /dev/null +++ b/website/docs/language/functions/terraform-applying.mdx @@ -0,0 +1,44 @@ +--- +page_title: terraform.applying reference - Functions - Configuration Language +description: |- + The terraform.applying symbol enables you to determine if Terraform is currently running an apply operation. +--- + +# The `terraform.applying` symbol + +-> **Note**: The `terraform.applying` symbol is available in Terraform v1.10 and later. + +You can use the `terraform.applying` symbol in your configuration to determine if Terraform is currently running an apply operation. + +Terraform automatically sets `terraform.applying` to `true` when you run an [apply](/terraform/cli/commands/apply) operation, and `false` during any other operation. The [planning mode](/terraform/cli/commands/plan#planning-modes) you run `terraform apply` in does not affect `terraform.applying`, meaning that even in destroy mode, `terraform.applying` is still `true`. + +You can use `terraform.applying` to change Terraform behavior during apply operations. In the following example, Terraform uses your read-only credentials when running a plan operation but uses your write credentials when you run an apply operation: + +```hcl +locals { + aws_read_role_arn = "arn:aws:iam::XXXXX:role/terraform-read" + aws_write_role_arn = "arn:aws:iam::XXXXX:role/terraform-full" + + role_arn = terraform.applying ? local.aws_write_role_arn : local.aws_read_role_arn +} + +provider "aws" { + region = "us-west-2" + + assume_role { + role_arn = local.role_arn + } +} + +``` + + +The `terraform.applying` symbol is an ephemeral value and is only available during Terraform operations. Terraform does not write ephemeral values to plan or state files. Additionally, you can only reference `terraform.applying` in the following ephemeral contexts: + +- In a [write-only argument](/terraform/language/resources/ephemeral/write-only) +- In [ephemeral variables](/terraform/language/values/variables#exclude-values-from-state) +- In [local values](/terraform/language/values/locals#ephemeral-values) +- In [ephemeral resources](/terraform/language/resources/ephemeral) +- In [ephemeral outputs](/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files) +- Configuring providers in the `provider` block +- In [provisioner](/terraform/language/resources/provisioners/syntax) and [connection](/terraform/language/resources/provisioners/connection) blocks diff --git a/website/docs/language/functions/terraform-decode_tfvars.mdx b/website/docs/language/functions/terraform-decode_tfvars.mdx new file mode 100644 index 0000000000..6bd7d6c8c4 --- /dev/null +++ b/website/docs/language/functions/terraform-decode_tfvars.mdx @@ -0,0 +1,70 @@ +--- +page_title: provider::terraform::decode_tfvars - Functions - Configuration Language +description: >- + The decode_tfvars function parses a string containing syntax like that used + in a ".tfvars" file. +--- + +# `provider::terraform::decode_tfvars` Function + +-> **Note:** This function is supported only in Terraform v1.8 and later. + +`provider::terraform::decode_tfvars` is a rarely-needed function which takes +a string containing the content of a +[`.tfvars` file](/terraform/language/values/variables#variable-definitions-tfvars-files) +and returns an object describing the raw variable values it defines. + +To use this function, your module must declare a dependency on the built-in +`terraform` provider, which contains this function: + +```hcl +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} +``` + +Elsewhere in your module you can then call this function: + +```hcl +provider::terraform::decode_tfvars( + <- + The encode_expr function produces a string representation of an arbitrary value + using Terraform expression syntax. +--- + +# `provider::terraform::encode_expr` Function + +-> **Note:** This function is supported only in Terraform v1.8 and later. + +`provider::terraform::encode_expr` is a rarely-needed function which takes +any value and produces a string containing Terraform language expression syntax +approximating that value. + +To use this function, your module must declare a dependency on the built-in +`terraform` provider, which contains this function: + +```hcl +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} +``` + +The primary use for this function is in conjunction with the `hashicorp/tfe` +provider's resource type +[`tfe_variable`](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/resources/variable), +which expects variable values to be provided in Terraform expression syntax. + +For example, the following concisely declares multiple input variables for +a particular HCP Terraform workspace: + +```hcl +locals { + workspace_vars = { + example1 = "Hello" + example2 = ["A", "B"] + } +} + +resource "tfe_variable" "test" { + for_each = local.workspace_vars + + category = "terraform" + workspace_id = tfe_workspace.example.id + + key = each.key + value = provider::terraform::encode_expr(each.value) + hcl = true +} +``` + +When using this pattern, always set `hcl = true` in the resource declaration +to ensure that HCP Terraform will expect `value` to be given as Terraform +expression syntax. + +We do not recommend using this function in any other situation. + +~> **Warning:** The exact syntax used to encode certain values may change +in future versions of Terraform to follow idiomatic style. Avoid using the +results of this function in any context where such changes might be disruptive +when upgrading Terraform in future. + +## Related Functions + +* [`encode_tfvars`](/terraform/language/functions/terraform-encode_tfvars) + produces expression strings for many different values at once, in `.tfvars` + syntax. diff --git a/website/docs/language/functions/terraform-encode_tfvars.mdx b/website/docs/language/functions/terraform-encode_tfvars.mdx new file mode 100644 index 0000000000..c7e36d1c4b --- /dev/null +++ b/website/docs/language/functions/terraform-encode_tfvars.mdx @@ -0,0 +1,72 @@ +--- +page_title: provider::terraform::encode_tfvars - Functions - Configuration Language +description: >- + The encode_tfvars function produces a string representation of an object + using the same syntax as for ".tfvars" files used in Terraform CLI. +--- + +# `provider::terraform::encode_tfvars` Function + +-> **Note:** This function is supported only in Terraform v1.8 and later. + +`provider::terraform::encode_tfvars` is a rarely-needed function which takes +an object value and produces a string containing a description of that object +using the same syntax as Terraform CLI would expect in a +[`.tfvars` file](/terraform/language/values/variables#variable-definitions-tfvars-files). + +In most cases it's better to pass data between Terraform configurations using +[Data Sources](/terraform/language/data-sources), +instead of writing generated `.tfvars` files to disk. Use this function only as +a last resort. + +To use this function, your module must declare a dependency on the built-in +`terraform` provider, which contains this function: + +```hcl +terraform { + required_providers { + terraform = { + source = "terraform.io/builtin/terraform" + } + } +} +``` + +Elsewhere in your module you can then call this function: + +```hcl +provider::terraform::encode_tfvars({ + example = "Hello!" +}) +``` + +The call above would produce the following result: + +```hcl +example = "Hello!" +``` + +Due to Terraform's requirements for the `.tfvars` format, all of the attributes +of the given object must be valid Terraform variable names, as would be +accepted in an +[input variable declaration](/terraform/language/values/variables#declaring-an-input-variable). + +The `.tfvars` format is specific to Terraform and so we do not recommend using +it as a general serialization format. +Use [`jsonencode`](/terraform/language/functions/jsonencode) or +[`yamlencode`](/terraform/language/functions/yamlencode) instead to produce +formats that are supported by other software. + +~> **Warning:** The exact syntax used to encode certain values may change +in future versions of Terraform to follow idiomatic style. Avoid using the +results of this function in any context where such changes might be disruptive +when upgrading Terraform in future. + +## Related Functions + +* [`decode_tfvars`](/terraform/language/functions/terraform-decode_tfvars) + performs the opposite operation: parsing `.tfvars` content to obtain + the variable values declared inside. +* [`encode_expr`](/terraform/language/functions/terraform-encode_expr) + encodes a single value as a plain expression, without the `.tfvars` + container around it. diff --git a/website/docs/language/functions/textdecodebase64.mdx b/website/docs/language/functions/textdecodebase64.mdx index 12ede76aab..2ee2007dc1 100644 --- a/website/docs/language/functions/textdecodebase64.mdx +++ b/website/docs/language/functions/textdecodebase64.mdx @@ -25,7 +25,7 @@ Terraform supports only a subset of the registered encodings, and the encoding support may vary between Terraform versions. Terraform accepts the encoding name `UTF-8`, which will produce the same result -as [`base64decode`](/language/functions/base64decode). +as [`base64decode`](/terraform/language/functions/base64decode). ## Examples @@ -36,7 +36,7 @@ Hello World ## Related Functions -* [`textencodebase64`](/language/functions/textencodebase64) performs the opposite operation, +* [`textencodebase64`](/terraform/language/functions/textencodebase64) performs the opposite operation, applying target encoding and then Base64 to a string. -* [`base64decode`](/language/functions/base64decode) is effectively a shorthand for +* [`base64decode`](/terraform/language/functions/base64decode) is effectively a shorthand for `textdecodebase64` where the character encoding is fixed as `UTF-8`. diff --git a/website/docs/language/functions/textencodebase64.mdx b/website/docs/language/functions/textencodebase64.mdx index 387a749667..1fae4fcce1 100644 --- a/website/docs/language/functions/textencodebase64.mdx +++ b/website/docs/language/functions/textencodebase64.mdx @@ -16,7 +16,7 @@ specified character encoding, returning the result base64 encoded because Terraform language strings are always sequences of unicode characters. ```hcl -substr(string, encoding_name) +textencodebase64(string, encoding_name) ``` Terraform uses the "standard" Base64 alphabet as defined in @@ -31,7 +31,7 @@ support may vary between Terraform versions. In particular Terraform supports therefore sometimes expected by Windows-originated software such as PowerShell. Terraform also accepts the encoding name `UTF-8`, which will produce the same -result as [`base64encode`](/language/functions/base64encode). +result as [`base64encode`](/terraform/language/functions/base64encode). ## Examples @@ -42,10 +42,10 @@ SABlAGwAbABvACAAVwBvAHIAbABkAA== ## Related Functions -* [`textdecodebase64`](/language/functions/textdecodebase64) performs the opposite operation, +* [`textdecodebase64`](/terraform/language/functions/textdecodebase64) performs the opposite operation, decoding Base64 data and interpreting it as a particular character encoding. -* [`base64encode`](/language/functions/base64encode) applies Base64 encoding of the UTF-8 +* [`base64encode`](/terraform/language/functions/base64encode) applies Base64 encoding of the UTF-8 encoding of a string. -* [`filebase64`](/language/functions/filebase64) reads a file from the local filesystem +* [`filebase64`](/terraform/language/functions/filebase64) reads a file from the local filesystem and returns its raw bytes with Base64 encoding, without creating an intermediate Unicode string. diff --git a/website/docs/language/functions/timeadd.mdx b/website/docs/language/functions/timeadd.mdx index 86c4ebbd0c..4a96720d3c 100644 --- a/website/docs/language/functions/timeadd.mdx +++ b/website/docs/language/functions/timeadd.mdx @@ -1,38 +1,67 @@ --- -page_title: timeadd - Functions - Configuration Language +page_title: timeadd function reference - Functions - Configuration Language description: |- The timeadd function adds a duration to a timestamp, returning a new timestamp. --- -# `timeadd` Function +# `timeadd` function reference +This topic provices reference information about the `timeadd` function. `timeadd` adds a duration to a timestamp, returning a new timestamp. +## Introduction + +The Terraform language represents timestamps as strings using [RFC +3339][rfc3339]'s Date and Time format. +`timeadd` requires that the `timestamp` argument is a string conforming to the Date and Time syntax. + +## Syntax + +Use the `timeadd` function with the following syntax: + + ```hcl timeadd(timestamp, duration) ``` -In the Terraform language, timestamps are conventionally represented as -strings using [RFC 3339](https://tools.ietf.org/html/rfc3339) -"Date and Time format" syntax. `timeadd` requires the `timestamp` argument -to be a string conforming to this syntax. +- `timestamp` is a string representation of a date in RFC 3339 format. Refer to the + external RFC 3339's [Internet Date/Time Format section][date-time-format] for how to construct a timestamp string. +- `duration` is a string representation of a time difference. This string consists of +sequences of number and unit pairs, such as `"1.5h"` or `"1h30m"`. You may use +the following units: -`duration` is a string representation of a time difference, consisting of -sequences of number and unit pairs, like `"1.5h"` or `"1h30m"`. The accepted -units are `"ns"`, `"us"` (or `"µs"`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first -number may be negative to indicate a negative duration, like `"-2h5m"`. + - `ns`: nanosecond + - `us` or `µs`: microsecond + - `ms`: millisecond + - `s`: second + - `m`: minute + - `h`: hour -The result is a string, also in RFC 3339 format, representing the result -of adding the given direction to the given timestamp. + To indicate a negative duration, make the first number negative, such as `"-2h5m"`. -## Examples +The `timeadd` result is a string, also in RFC 3339 format, representing the result +of adding the given duration to the given timestamp. +## Example use case + +This example adds ten minutes. + +```hcl +> timeadd("2024-08-16T12:45:05Z", "10m") +"2024-08-16T12:55:05Z" ``` -> timeadd("2017-11-22T00:00:00Z", "10m") -2017-11-22T00:10:00Z + +This example subtracts ten minutes by using a negative duration. + +```hcl +> timeadd("2024-08-16T12:45:05Z", "-10m") +"2024-08-16T12:35:05Z" ``` # Related Functions -* [`timecmp`](./timecmp) determines an ordering for two timestamps. +* [`timecmp`](/terraform/language/functions/timecmp) determines an ordering for two timestamps. + +[rfc3339]: https://tools.ietf.org/html/rfc3339 +[date-time-format]: https://datatracker.ietf.org/doc/html/rfc3339#section-5.6 diff --git a/website/docs/language/functions/timecmp.mdx b/website/docs/language/functions/timecmp.mdx index 5e215022d5..2c078a9b4e 100644 --- a/website/docs/language/functions/timecmp.mdx +++ b/website/docs/language/functions/timecmp.mdx @@ -44,7 +44,7 @@ both be strings conforming to this syntax. ``` `timecmp` can be particularly useful in defining -[custom condition checks](/language/expressions/custom-conditions) that +[custom condition checks](/terraform/language/expressions/custom-conditions) that involve a specified timestamp being within a particular range. For example, the following resource postcondition would raise an error if a TLS certificate (or other expiring object) expires sooner than 30 days from the time of @@ -61,7 +61,6 @@ the "apply" step: ## Related Functions -* [`timestamp`](./timestamp) returns the current timestamp when it is evaluated +* [`timestamp`](/terraform/language/functions/timestamp) returns the current timestamp when it is evaluated during the apply step. -* [`timeadd`](./timeadd) can perform arithmetic on timestamps by adding or - removing a specified duration. +* [`timeadd`](/terraform/language/functions/timeadd) can perform arithmetic on timestamps by adding or removing a specified duration. diff --git a/website/docs/language/functions/timestamp.mdx b/website/docs/language/functions/timestamp.mdx index 206e80f02b..ad9f8be08a 100644 --- a/website/docs/language/functions/timestamp.mdx +++ b/website/docs/language/functions/timestamp.mdx @@ -18,9 +18,9 @@ The result of this function will change every second, so using this function directly with resource attributes will cause a diff to be detected on every Terraform run. We do not recommend using this function in resource attributes, but in rare cases it can be used in conjunction with -[the `ignore_changes` lifecycle meta-argument](/language/meta-arguments/lifecycle#ignore_changes) +[the `ignore_changes` lifecycle meta-argument](/terraform/language/meta-arguments/lifecycle#ignore_changes) to take the timestamp only on initial creation of the resource. For more stable -time handling, see the [Time Provider](https://registry.terraform.io/providers/hashicorp/time/). +time handling, see the [Time Provider](https://registry.terraform.io/providers/hashicorp/time). Due to the constantly changing return value, the result of this function cannot be predicted during Terraform's planning phase, and so the timestamp will be @@ -35,5 +35,7 @@ taken only once the plan is being applied. ## Related Functions -* [`formatdate`](/language/functions/formatdate) can convert the resulting timestamp to +* [`formatdate`](/terraform/language/functions/formatdate) can convert the resulting timestamp to other date and time formats. +* [`plantimestamp`](/terraform/language/functions/plantimestamp) will return a consistent timestamp + representing the date and time during the plan. diff --git a/website/docs/language/functions/title.mdx b/website/docs/language/functions/title.mdx index 2723d79462..ba7a6d98bb 100644 --- a/website/docs/language/functions/title.mdx +++ b/website/docs/language/functions/title.mdx @@ -20,5 +20,5 @@ This function uses Unicode's definition of letters and of upper- and lowercase. ## Related Functions -* [`upper`](/language/functions/upper) converts _all_ letters in a string to uppercase. -* [`lower`](/language/functions/lower) converts all letters in a string to lowercase. +* [`upper`](/terraform/language/functions/upper) converts _all_ letters in a string to uppercase. +* [`lower`](/terraform/language/functions/lower) converts all letters in a string to lowercase. diff --git a/website/docs/language/functions/tostring.mdx b/website/docs/language/functions/tostring.mdx index 0b3db29b8c..5df650aa2b 100644 --- a/website/docs/language/functions/tostring.mdx +++ b/website/docs/language/functions/tostring.mdx @@ -12,19 +12,19 @@ convert types automatically where required. Use the explicit type conversion functions only to normalize types returned in module outputs. Only the primitive types (string, number, and bool) and `null` can be converted to string. -All other values will produce an error. +`tostring(null)` produces a `null` value of type `string`. All other values produce an error. ## Examples ``` > tostring("hello") -hello +"hello" > tostring(1) -1 +"1" > tostring(true) -true +"true" > tostring(null) -null +tostring(null) > tostring([]) Error: Invalid function argument diff --git a/website/docs/language/functions/trim.mdx b/website/docs/language/functions/trim.mdx index 96583bd154..7cdd03283e 100644 --- a/website/docs/language/functions/trim.mdx +++ b/website/docs/language/functions/trim.mdx @@ -34,7 +34,7 @@ and end of the string specified in the first argument. ## Related Functions -* [`trimprefix`](/language/functions/trimprefix) removes a word from the start of a string. -* [`trimsuffix`](/language/functions/trimsuffix) removes a word from the end of a string. -* [`trimspace`](/language/functions/trimspace) removes all types of whitespace from +* [`trimprefix`](/terraform/language/functions/trimprefix) removes a word from the start of a string. +* [`trimsuffix`](/terraform/language/functions/trimsuffix) removes a word from the end of a string. +* [`trimspace`](/terraform/language/functions/trimspace) removes all types of whitespace from both the start and the end of a string. diff --git a/website/docs/language/functions/trimprefix.mdx b/website/docs/language/functions/trimprefix.mdx index 9b141ee7c7..195cacd455 100644 --- a/website/docs/language/functions/trimprefix.mdx +++ b/website/docs/language/functions/trimprefix.mdx @@ -7,7 +7,7 @@ description: |- # `trimprefix` Function -`trimprefix` removes the specified prefix from the start of the given string. If the string does not start with the prefix, the string is returned unchanged. +`trimprefix` removes the specified prefix from the start of the given string, but only once. If the string does not begin with the prefix, the original string is returned unchanged. ## Examples @@ -21,9 +21,14 @@ world helloworld ``` +``` +> trimprefix("--hello", "-") +-hello +``` + ## Related Functions -* [`trim`](/language/functions/trim) removes characters at the start and end of a string. -* [`trimsuffix`](/language/functions/trimsuffix) removes a word from the end of a string. -* [`trimspace`](/language/functions/trimspace) removes all types of whitespace from +* [`trim`](/terraform/language/functions/trim) removes characters at the start and end of a string. +* [`trimsuffix`](/terraform/language/functions/trimsuffix) removes a word from the end of a string. +* [`trimspace`](/terraform/language/functions/trimspace) removes all types of whitespace from both the start and the end of a string. diff --git a/website/docs/language/functions/trimspace.mdx b/website/docs/language/functions/trimspace.mdx index 950e3f935d..a1eafaba91 100644 --- a/website/docs/language/functions/trimspace.mdx +++ b/website/docs/language/functions/trimspace.mdx @@ -23,5 +23,5 @@ hello ## Related Functions -* [`chomp`](/language/functions/chomp) removes just line ending characters from the _end_ of +* [`chomp`](/terraform/language/functions/chomp) removes just line ending characters from the _end_ of a string. diff --git a/website/docs/language/functions/trimsuffix.mdx b/website/docs/language/functions/trimsuffix.mdx index d7aa7a296f..272d0cc48e 100644 --- a/website/docs/language/functions/trimsuffix.mdx +++ b/website/docs/language/functions/trimsuffix.mdx @@ -7,7 +7,7 @@ description: |- # `trimsuffix` Function -`trimsuffix` removes the specified suffix from the end of the given string. +`trimsuffix` removes the specified suffix from the end of the given string, but only once, even if the suffix appears multiple times. If the suffix does not appear at the very end of the string, the original string is returned unchanged. ## Examples @@ -16,9 +16,19 @@ description: |- hello ``` +``` +> trimsuffix("helloworld", "cat") +helloworld +``` + +``` +> trimsuffix("hello--", "-") +hello- +``` + ## Related Functions -* [`trim`](/language/functions/trim) removes characters at the start and end of a string. -* [`trimprefix`](/language/functions/trimprefix) removes a word from the start of a string. -* [`trimspace`](/language/functions/trimspace) removes all types of whitespace from +* [`trim`](/terraform/language/functions/trim) removes characters at the start and end of a string. +* [`trimprefix`](/terraform/language/functions/trimprefix) removes a word from the start of a string. +* [`trimspace`](/terraform/language/functions/trimspace) removes all types of whitespace from both the start and the end of a string. diff --git a/website/docs/language/functions/try.mdx b/website/docs/language/functions/try.mdx index a23d1def50..fffc932f0a 100644 --- a/website/docs/language/functions/try.mdx +++ b/website/docs/language/functions/try.mdx @@ -107,5 +107,5 @@ A local value with the name "nonexist" has not been declared. ## Related Functions -* [`can`](/language/functions/can), which tries evaluating an expression and returns a +* [`can`](/terraform/language/functions/can), which tries evaluating an expression and returns a boolean value indicating whether it succeeded. diff --git a/website/docs/language/functions/upper.mdx b/website/docs/language/functions/upper.mdx index 134a93b03b..874e40fe5b 100644 --- a/website/docs/language/functions/upper.mdx +++ b/website/docs/language/functions/upper.mdx @@ -22,5 +22,5 @@ This function uses Unicode's definition of letters and of upper- and lowercase. ## Related Functions -* [`lower`](/language/functions/lower) converts letters in a string to _lowercase_. -* [`title`](/language/functions/title) converts the first letter of each word in a string to uppercase. +* [`lower`](/terraform/language/functions/lower) converts letters in a string to _lowercase_. +* [`title`](/terraform/language/functions/title) converts the first letter of each word in a string to uppercase. diff --git a/website/docs/language/functions/uuid.mdx b/website/docs/language/functions/uuid.mdx index b203060226..08a3471e52 100644 --- a/website/docs/language/functions/uuid.mdx +++ b/website/docs/language/functions/uuid.mdx @@ -5,22 +5,19 @@ description: The uuid function generates a unique id. # `uuid` Function -`uuid` generates a unique identifier string. +`uuid` generates UUID-format strings using random bytes. -The id is a generated and formatted as required by -[RFC 4122 section 4.4](https://tools.ietf.org/html/rfc4122#section-4.4), -producing a Version 4 UUID. The result is a UUID generated only from -pseudo-random numbers. +The function generates a well-understood string representation of a 128-bit value, but the output is not RFC-compliant. This function produces a new value each time it is called, and so using it directly in resource arguments will result in spurious diffs. We do not recommend using the `uuid` function in resource configurations, but it can be used with care in conjunction with -[the `ignore_changes` lifecycle meta-argument](/language/meta-arguments/lifecycle#ignore_changes). +[the `ignore_changes` lifecycle meta-argument](/terraform/language/meta-arguments/lifecycle#ignore_changes). In most cases we recommend using [the `random` provider](https://registry.terraform.io/providers/hashicorp/random/latest/docs) instead, since it allows the one-time generation of random values that are -then retained in the Terraform [state](/language/state) for use by +then retained in the Terraform [state](/terraform/language/state) for use by future operations. In particular, [`random_id`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) can generate results with equivalent randomness to the `uuid` function. @@ -34,4 +31,4 @@ b5ee72a3-54dd-c4b8-551c-4bdc0204cedb ## Related Functions -* [`uuidv5`](/language/functions/uuidv5), which generates name-based UUIDs. +* [`uuidv5`](/terraform/language/functions/uuidv5), which generates name-based UUIDs. diff --git a/website/docs/language/functions/uuidv5.mdx b/website/docs/language/functions/uuidv5.mdx index e80e5dc1f4..0b45bb57cc 100644 --- a/website/docs/language/functions/uuidv5.mdx +++ b/website/docs/language/functions/uuidv5.mdx @@ -16,7 +16,7 @@ uuidv5(namespace, name) ``` Unlike the pseudo-random UUIDs generated by -[`uuid`](/language/functions/uuid), name-based UUIDs derive from namespace and an name, +[`uuid`](/terraform/language/functions/uuid), name-based UUIDs derive from namespace and an name, producing the same UUID value every time if the namespace and name are unchanged. @@ -77,4 +77,4 @@ defined it. ## Related Functions -* [`uuid`](/language/functions/uuid), which generates pseudorandom UUIDs. +* [`uuid`](/terraform/language/functions/uuid), which generates pseudorandom UUIDs. diff --git a/website/docs/language/functions/values.mdx b/website/docs/language/functions/values.mdx index 40a6d6549d..b7362ab5d2 100644 --- a/website/docs/language/functions/values.mdx +++ b/website/docs/language/functions/values.mdx @@ -10,7 +10,7 @@ in that map. The values are returned in lexicographical order by their corresponding _keys_, so the values will be returned in the same order as their keys would be -returned from [`keys`](/language/functions/keys). +returned from [`keys`](/terraform/language/functions/keys). ## Examples @@ -25,4 +25,4 @@ returned from [`keys`](/language/functions/keys). ## Related Functions -* [`keys`](/language/functions/keys) returns a list of the _keys_ from a map. +* [`keys`](/terraform/language/functions/keys) returns a list of the _keys_ from a map. diff --git a/website/docs/language/functions/yamldecode.mdx b/website/docs/language/functions/yamldecode.mdx index 5f894e5a32..d1fa41eb33 100644 --- a/website/docs/language/functions/yamldecode.mdx +++ b/website/docs/language/functions/yamldecode.mdx @@ -14,7 +14,7 @@ This function supports a subset of [YAML 1.2](https://yaml.org/spec/1.2/spec.htm as described below. This function maps YAML values to -[Terraform language values](/language/expressions/types) +[Terraform language values](/terraform/language/expressions/types) in the following way: | YAML type | Terraform type | @@ -93,7 +93,7 @@ Call to function "yamldecode" failed: unsupported tag "!not-supported". ## Related Functions -- [`jsondecode`](/language/functions/jsondecode) is a similar operation using JSON instead +- [`jsondecode`](/terraform/language/functions/jsondecode) is a similar operation using JSON instead of YAML. -- [`yamlencode`](/language/functions/yamlencode) performs the opposite operation, _encoding_ +- [`yamlencode`](/terraform/language/functions/yamlencode) performs the opposite operation, _encoding_ a value as YAML. diff --git a/website/docs/language/functions/yamlencode.mdx b/website/docs/language/functions/yamlencode.mdx index 7ceddb746c..c06012492e 100644 --- a/website/docs/language/functions/yamlencode.mdx +++ b/website/docs/language/functions/yamlencode.mdx @@ -8,25 +8,8 @@ description: The yamlencode function encodes a given value as a YAML string. `yamlencode` encodes a given value to a string using [YAML 1.2](https://yaml.org/spec/1.2/spec.html) block syntax. -~> **Warning:** This function is currently **experimental** and its exact -result format may change in future versions of Terraform, based on feedback. -Do not use `yamldecode` to construct a value for any resource argument where -changes to the result would be disruptive. To get a consistent string -representation of a value use [`jsonencode`](/language/functions/jsonencode) instead; its -results are also valid YAML because YAML is a JSON superset. - - - This function maps -[Terraform language values](/language/expressions/types) +[Terraform language values](/terraform/language/expressions/types) to YAML tags in the following way: | Terraform type | YAML type | @@ -49,6 +32,15 @@ types, passing the `yamlencode` result to `yamldecode` will not produce an identical value, but the Terraform language automatic type conversion rules mean that this is rarely a problem in practice. +YAML is a superset of JSON, and so where possible we recommend generating +JSON using [`jsonencode`](/terraform/language/functions/jsonencode) instead, even if +a remote system supports YAML. JSON syntax is equivalent to flow-style YAML +and Terraform can present detailed structural change information for JSON +values in plans, whereas Terraform will treat block-style YAML just as a normal +multi-line string. However, generating YAML may improve readability if the +resulting value will be directly read or modified in the remote system by +humans. + ## Examples ``` @@ -74,12 +66,12 @@ mean that this is rarely a problem in practice. `yamlencode` always uses YAML's "block style" for mappings and sequences, unless the mapping or sequence is empty. To generate flow-style YAML, use -[`jsonencode`](/language/functions/jsonencode) instead: YAML flow-style is a superset +[`jsonencode`](/terraform/language/functions/jsonencode) instead: YAML flow-style is a superset of JSON syntax. ## Related Functions -- [`jsonencode`](/language/functions/jsonencode) is a similar operation using JSON instead +- [`jsonencode`](/terraform/language/functions/jsonencode) is a similar operation using JSON instead of YAML. -- [`yamldecode`](/language/functions/yamldecode) performs the opposite operation, _decoding_ +- [`yamldecode`](/terraform/language/functions/yamldecode) performs the opposite operation, _decoding_ a YAML string to obtain its represented value. diff --git a/website/docs/language/import/generating-configuration.mdx b/website/docs/language/import/generating-configuration.mdx new file mode 100644 index 0000000000..e717036a63 --- /dev/null +++ b/website/docs/language/import/generating-configuration.mdx @@ -0,0 +1,145 @@ +--- +page_title: Import - Generating Configuration +description: >- + Generate configuration and manage existing resources with Terraform using configuration-driven import. +--- + +# Generating configuration + +~> **Experimental:** Configuration generation is available in Terraform v1.5 as an experimental feature. Later minor versions may contain changes to the formatting of generated configuration and behavior of the `terraform plan` command using the `-generate-config-out` flag. + +Terraform can generate code for the resources you define in [`import` blocks](/terraform/language/import) that do not already exist in your configuration. Terraform produces HCL to act as a template that contains Terraform's best guess at the appropriate value for each resource argument. + +Starting with Terraform's generated HCL, we recommend iterating to find your ideal configuration by removing some attributes, adjusting the value of others, and rearranging `resource` blocks into files and modules as appropriate. + +To generate configuration, run `terraform plan` with the `-generate-config-out` flag and supply a new file path. Do not supply a path to an existing file, or Terraform throws an error. + +```shell +$ terraform plan -generate-config-out="generated_resources.tf" +``` + +If any resources targeted by an `import` block do not exist in your configuration, Terraform then generates and writes configuration for those resources in `generated_resources.tf`. + +## Workflow + +The workflow for generating configuration is similar to the [`import` block workflow](/terraform/language/import#plan-and-apply-an-import), with the extra step of generating configuration during the planning stage. You can then review and modify the generated configuration before applying. + +### 1. Add the `import` block + +Add an `import` block to your configuration. This `import` block can be in a separate file (e.g., `import.tf`) or an existing configuration file. + +```hcl +import { + to = aws_iot_thing.bar + id = "foo" +} +``` + +The import block's `to` argument points to the address a `resource` will have in your state file. If a resource address in your state matches an `import` block's `to` argument, Terraform attempts to import into that resource. In future planning, Terraform knows it doesn't need to generate configuration for resources that already exist in your state. + +The import block's `id` argument uses that resource's [import ID](/terraform/language/import#import-id). + +If your configuration does not contain other resources for your selected provider, you must add a `provider` block to inform Terraform which provider it should use to generate configuration. Otherwise, Terraform displays an error if it can not determine which provider to use. +If you add a new `provider` block to your configuration, you must run `terraform init` again. + +### 2. Plan and generate configuration + +To instruct Terraform to generate configuration for the `import` blocks you defined, run `terraform plan` with the `-generate-config-out=` flag and a new file path. Terraform displays its plan for importing your resource and the file where Terraform generated configuration based on this plan. + +```shell +$ terraform plan -generate-config-out=generated.tf + +aws_iot_thing.bar: Preparing import... [id=foo] +aws_iot_thing.bar: Refreshing state... [id=foo] + +Terraform will perform the following actions: + + # aws_iot_thing.bar will be imported + # (config will be generated) + resource "aws_iot_thing" "bar" { + arn = "arn:aws:iot:eu-west-1:1234567890:thing/foo" + attributes = {} + default_client_id = "foo" + id = "foo" + name = "foo" + version = 1 + } + +Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. + +╷ +│ Warning: Config generation is experimental +│ +│ Generating configuration during import is currently experimental, and the generated configuration format may change in future versions. +╵ + +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Terraform has generated configuration and written it to generated.tf. Please review the configuration and edit it as necessary before adding it to version control. +``` + +### 3. Review generated configuration + +The example above instructs Terraform to generate configuration in a file named `generated.tf`. The below code is an example of a `generated.tf` file. + +```hcl +resource "aws_iot_thing" "bar" { + name = "foo" +} +``` + +Review the generated configuration and update it as needed. You may wish to move the generated configuration to another file, add or remove resource arguments, or update it to reference input variables or other resources in your configuration. + +### 4. Apply + +Run `terraform apply` to import your infrastructure. + +```shell +$ terraform apply + +aws_iot_thing.bar: Preparing import... [id=foo] +aws_iot_thing.bar: Refreshing state... [id=foo] + +Terraform will perform the following actions: + + # aws_iot_thing.bar will be imported + resource "aws_iot_thing" "bar" { + arn = "arn:aws:iot:eu-west-1:1234567890:thing/foo" + attributes = {} + default_client_id = "foo" + id = "foo" + name = "foo" + version = 1 + } + +Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. +aws_iot_thing.bar: Importing... [id=foo] +aws_iot_thing.bar: Import complete [id=foo] + +Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed. +``` + +Commit your new resource configuration to your version control system. + +## Limitations + +### Conflicting resource arguments + +Terraform generates configuration for importable resources during a plan by requesting values for resource attributes from the provider. For certain resources with complex schemas, Terraform may not be able to construct a valid configuration from these values. + +Terraform will display an error like the one below if it does not receive values for resource attributes while generating configuration. + +```shell +$ terraform plan -generate-config-out="generated.tf" +╷ +│ Error: Conflicting configuration arguments +│ +│ with aws_instance.ubuntu, +│ on g.tf line 20, in resource "aws_instance" "ubuntu": +│ 20: ipv6_address_count = 0 +│ +│ "ipv6_address_count": conflicts with ipv6_addresses +╵ +``` + +In the example above, Terraform still generates configuration and writes it to `generated.tf`. This error stems from a conflict between the `ipv6_address_count` and `ipv6_addresses` arguments. The resource supports both of these arguments, but you must choose only one when configuring the resource. You could fix the error by removing one of these two arguments, then running `terraform plan` again to check that there are no further issues. diff --git a/website/docs/language/import/index.mdx b/website/docs/language/import/index.mdx new file mode 100644 index 0000000000..8390af81aa --- /dev/null +++ b/website/docs/language/import/index.mdx @@ -0,0 +1,208 @@ +--- +page_title: Import - Configuration Language +description: >- + Import and manage existing resources with Terraform using configuration-driven import. +--- + +# Import + +-> **Note:** Import blocks are only available in Terraform v1.5.0 and later. + +~> **Experimental:** While we do not expect to make backwards-incompatible changes to syntax, the `-generate-config-out` flag and how Terraform processes imports during the plan stage and generates configuration may change in future releases. + +Use the `import` block to import existing infrastructure resources into Terraform, bringing them under Terraform's management. Unlike the `terraform import` command, configuration-driven import using `import` blocks is predictable, works with CICD pipelines, and lets you preview an import operation before modifying state. + +Once imported, Terraform tracks the resource in your state file. You can then manage the imported resource like any other, updating its attributes and destroying it as part of a standard resource lifecycle. + +The `import` block records that Terraform imported the resource and did not create it. After importing, you can optionally remove import blocks from your configuration or leave them as a record of the resource's origin. + +## Syntax + +You can add an `import` block to any Terraform configuration file. A common pattern is to create an `imports.tf` file, or to place each `import` block beside the `resource` block it imports into. + +```hcl +import { + to = aws_instance.example + id = "i-abcd1234" +} + +resource "aws_instance" "example" { + name = "hashi" + # (other resource arguments...) +} +``` + +The above `import` block defines an import of the AWS instance with the ID "i-abcd1234" into the `aws_instance.example` resource in the root module. + +The `import` block has the following arguments: + - `to` - The instance address this resource will have in your state file. + - `id` - A string with the [import ID](#import-id) of the resource, or an expression that evaluates to a string. + - `provider` (optional) - An optional custom resource provider, see [The Resource provider Meta-Argument](/terraform/language/meta-arguments/resource-provider) for details. + +If you do not set the `provider` argument, Terraform attempts to import from the default provider. + +### Import ID + +The import block's `id` argument can be a literal string of your resource's import ID, or an expression that evaluates to a string, such as `var.instance_id`. Terraform needs this import ID to locate the resource you want to import. + +The import ID must be known at plan time for planning to succeed. If the value of `id` is only known after apply, `terraform plan` will fail with an error. + +The identifier you use for a resource's import ID is resource-specific. You can find the required ID in the [provider documentation](https://registry.terraform.io/browse/providers) for the resource you wish to import. + +### Import multiple instances with `for_each` + +Multiple resource instances can be imported via a single `import` block using the `for_each` argument. The `for_each` argument accepts a collection to iterate over, and results in an `each` iterator in the same manner as it does for [`dynamic` blocks](/terraform/language/expressions/dynamic-blocks). The `for_each` argument must be entirely known for the import plan to succeed, and `for_each` cannot be used when [generating configuration](/terraform/language/import/generating-configuration). + +The resulting `each.key` and `each.value` values can be used both in the `id` expression, and within index expressions in the `to` argument. This allows the mapping of multiple instances to expanded resources: + +```hcl +locals { + buckets = { + "staging" = "bucket1" + "uat" = "bucket2" + "prod" = "bucket3" + } +} + +import { + for_each = local.buckets + to = aws_s3_bucket.this[each.key] + id = each.value +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets +} +``` + +The same process can also be used to expand the imports across multiple module instances: + +```hcl +locals { + buckets = [ + { + group = "one" + key = "bucket1" + id = "one_1" + }, + { + group = "one" + key = "bucket2" + id = "one_2" + }, + { + group = "two" + key = "bucket1" + id = "two_1" + }, + { + group = "two" + key = "bucket2" + id = "two_2" + }, + ] +} + +import { + for_each = local.buckets + id = each.value.id + to = module.group[each.value.group].aws_s3_bucket.this[each.value.key] +} +``` + + +## Plan and apply an import + +Terraform processes the `import` block during the plan stage. Once a plan is approved, Terraform imports the resource into its state during the subsequent apply stage. + +To import a resource using `import` blocks, you must: +1. Define an `import` block for the resource(s). +1. Add a corresponding `resource` block to your configuration , or [generate configuration](/terraform/language/import/generating-configuration) for that resource. +1. Run `terraform plan` to review how Terraform will import the resource(s). +1. Apply the configuration to import the resources and update your Terraform state. + +> **Hands-on:** Try the [State Import](/terraform/tutorials/state/state-import?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. + +The `import` block is [_idempotent_](https://en.wikipedia.org/wiki/Idempotence), meaning that applying an import action and running another plan will not generate another import action as long as that resource remains in your state. + +Terraform only needs to import a given resource once. Attempting to import a resource into the same address again is a harmless no-op. You can remove `import` blocks after completing the import or safely leave them in your configuration as a record of the resource's origin for future module maintainers. For more information on maintaining configurations over time, see [Refactoring](/terraform/language/modules/develop/refactoring). + +## Resource configuration + +Before importing, you must add configuration for every resource you want Terraform to import. Otherwise, Terraform throws an error during planning, insisting you add resource configuration before it can successfully import. You can create resource configuration manually or [generate it using Terraform](/terraform/language/import/generating-configuration). + +We recommend writing a `resource` block if you know what most of the [resource's arguments](/terraform/language/resources/syntax#resource-arguments) will be. For example, your configuration may already contain a similar resource whose configuration you can copy and modify. + +We recommend [generating configuration](/terraform/language/import/generating-configuration) when importing multiple resources or a single complex resource that you do not already have the configuration for. + +### Add a `resource` block + +Add a `resource` block for the resource to import. The resource address must match the import block's `to` argument. + +```hcl +import { + to = aws_instance.example + id = "i-abcd1234" +} + +resource "aws_instance" "example" { + name = "renderer" +} +``` + +### Generate configuration + +Terraform can generate HCL for resources that do not already exist in configuration. +For more details, see [Generating Configuration](/terraform/language/import/generating-configuration). + +## Examples + +The following example demonstrates how to import into a module. + +```hcl +import { + to = module.instances.aws_instance.example + id = "i-abcd1234" +} +``` + +The below example shows how to use the value of a variable as the import ID. + +```hcl +import { + to = aws_instance.example + id = var.instance_id +} +``` + +The below example shows how to import a resource that includes [`count`](/terraform/language/meta-arguments/count). + +```hcl +import { + to = aws_instance.example[0] + id = "i-abcd1234" +} +``` + +The below example shows how to import a resource that includes [`for_each`](/terraform/language/meta-arguments/for_each). +```hcl +import { + to = aws_instance.example["foo"] + id = "i-abcd1234" +} +``` + +Finally, the below example demonstrates how to import from a custom resource provider. + +```hcl +provider "aws" { + alias = "europe" + region = "eu-west-1" +} + +import { + provider = aws.europe + to = aws_instance.example["foo"] + id = "i-abcd1234" +} +``` diff --git a/website/docs/language/index.mdx b/website/docs/language/index.mdx index 8efe473ee5..fdaef99afc 100644 --- a/website/docs/language/index.mdx +++ b/website/docs/language/index.mdx @@ -1,28 +1,27 @@ --- page_title: Overview - Configuration Language description: >- - You can use the Terraform language to write configuration files that tell - Terraform how to manage a collection of infrastructure. + Use the Terraform configuration language to describe the infrastructure that Terraform manages. --- # Terraform Language Documentation This is the documentation for Terraform's configuration language. It is relevant -to users of [Terraform CLI](/cli), -[Terraform Cloud](/cloud), and -[Terraform Enterprise](/enterprise). Terraform's language is +to users of [Terraform CLI](/terraform/cli), +[HCP Terraform](https://cloud.hashicorp.com/products/terraform), and +[Terraform Enterprise](/terraform/enterprise). Terraform's language is its primary user interface. Configuration files you write in Terraform language tell Terraform what plugins to install, what infrastructure to create, and what data to fetch. Terraform language also lets you define dependencies between resources and create multiple similar resources from a single configuration block. -> **Hands-on:** Try the [Write Terraform Configuration](https://learn.hashicorp.com/collections/terraform/configuration-language) tutorials on HashiCorp Learn. +> **Hands-on:** Try the [Write Terraform Configuration](/terraform/tutorials/configuration-language) tutorials. ## About the Terraform Language The main purpose of the Terraform language is declaring -[resources](/language/resources), which represent infrastructure objects. All other +[resources](/terraform/language/resources), which represent infrastructure objects. All other language features exist only to make the definition of resources more flexible and convenient. diff --git a/website/docs/language/meta-arguments/count.mdx b/website/docs/language/meta-arguments/count.mdx index 7b051c8f1b..f08b3f7c11 100644 --- a/website/docs/language/meta-arguments/count.mdx +++ b/website/docs/language/meta-arguments/count.mdx @@ -12,16 +12,16 @@ previous versions can only use it with resources. -> **Note:** A given resource or module block cannot use both `count` and `for_each`. -> **Hands-on:** Try the [Manage Similar Resources With Count](https://learn.hashicorp.com/tutorials/terraform/count?in=terraform/0-13&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Manage Similar Resources With Count](/terraform/tutorials/0-13/count?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -By default, a [resource block](/language/resources/syntax) configures one real +By default, a [resource block](/terraform/language/resources/syntax) configures one real infrastructure object. (Similarly, a -[module block](/language/modules/syntax) includes a +[module block](/terraform/language/modules/syntax) includes a child module's contents into the configuration one time.) However, sometimes you want to manage several similar objects (like a fixed pool of compute instances) without writing a separate block for each one. Terraform has two ways to do this: -`count` and [`for_each`](/language/meta-arguments/for_each). +`count` and [`for_each`](/terraform/language/meta-arguments/for_each). If a resource or module block includes a `count` argument whose value is a whole number, Terraform will create that many instances. @@ -60,7 +60,7 @@ This object has one attribute: ## Using Expressions in `count` -The `count` meta-argument accepts numeric [expressions](/language/expressions). +The `count` meta-argument accepts numeric [expressions](/terraform/language/expressions). However, unlike most arguments, the `count` value must be known _before_ Terraform performs any remote resource actions. This means `count` can't refer to any resource attributes that aren't known until after a diff --git a/website/docs/language/meta-arguments/depends_on.mdx b/website/docs/language/meta-arguments/depends_on.mdx index 9edccf29cd..5f12751b53 100644 --- a/website/docs/language/meta-arguments/depends_on.mdx +++ b/website/docs/language/meta-arguments/depends_on.mdx @@ -14,11 +14,11 @@ Use the `depends_on` meta-argument to handle hidden resource or module dependenc ## Processing and Planning Consequences -The `depends_on` meta-argument instructs Terraform to complete all actions on the dependency object (including Read actions) before performing actions on the object declaring the dependency. When the dependency object is an entire module, `depends_on` affects the order in which Terraform processes all of the resources and data sources associated with that module. Refer to [Resource Dependencies](/language/resources/behavior#resource-dependencies) and [Data Resource Dependencies](/language/data-sources#data-resource-dependencies) for more details. +The `depends_on` meta-argument instructs Terraform to complete all actions on the dependency object (including Read actions) before performing actions on the object declaring the dependency. When the dependency object is an entire module, `depends_on` affects the order in which Terraform processes all of the resources and data sources associated with that module. Refer to [Resource Dependencies](/terraform/language/resources/behavior#resource-dependencies) and [Data Resource Dependencies](/terraform/language/data-sources#data-resource-dependencies) for more details. You should use `depends_on` as a last resort because it can cause Terraform to create more conservative plans that replace more resources than necessary. For example, Terraform may treat more values as unknown “(known after apply)” because it is uncertain what changes will occur on the upstream object. This is especially likely when you use `depends_on` for modules. -Instead of `depends_on`, we recommend using [expression references](/language/expressions/references) to imply dependencies when possible. Expression references let Terraform understand which value the reference derives from and avoid planning changes if that particular value hasn’t changed, even if other parts of the upstream object have planned changes. +Instead of `depends_on`, we recommend using [expression references](/terraform/language/expressions/references) to imply dependencies when possible. Expression references let Terraform understand which value the reference derives from and avoid planning changes if that particular value hasn’t changed, even if other parts of the upstream object have planned changes. ## Usage diff --git a/website/docs/language/meta-arguments/for_each.mdx b/website/docs/language/meta-arguments/for_each.mdx index 48dbc67b2a..7001fd08fd 100644 --- a/website/docs/language/meta-arguments/for_each.mdx +++ b/website/docs/language/meta-arguments/for_each.mdx @@ -7,19 +7,19 @@ description: >- # The `for_each` Meta-Argument -By default, a [resource block](/language/resources/syntax) configures one real +By default, a [resource block](/terraform/language/resources/syntax) configures one real infrastructure object (and similarly, a -[module block](/language/modules/syntax) includes a +[module block](/terraform/language/modules/syntax) includes a child module's contents into the configuration one time). However, sometimes you want to manage several similar objects (like a fixed pool of compute instances) without writing a separate block for each one. Terraform has two ways to do this: -[`count`](/language/meta-arguments/count) and `for_each`. +[`count`](/terraform/language/meta-arguments/count) and `for_each`. -> **Hands-on:** Try the [Manage Similar Resources With For Each](https://learn.hashicorp.com/tutorials/terraform/for-each?in=terraform/configuration-language) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Manage Similar Resources With For Each](/terraform/tutorials/configuration-language/for-each) tutorial. If a resource or module block includes a `for_each` argument whose value is a map or -a set of strings, Terraform will create one instance for each member of +a set of strings, Terraform creates one instance for each member of that map or set. -> **Version note:** `for_each` was added in Terraform 0.12.6. Module support @@ -42,10 +42,10 @@ Map: ```hcl resource "azurerm_resource_group" "rg" { - for_each = { - a_group = "eastus" + for_each = tomap({ + a_group = "eastus" another_group = "westus2" - } + }) name = each.key location = each.value } @@ -55,7 +55,7 @@ Set of strings: ```hcl resource "aws_iam_user" "the-accounts" { - for_each = toset( ["Todd", "James", "Alice", "Dottie"] ) + for_each = toset(["Todd", "James", "Alice", "Dottie"]) name = each.key } ``` @@ -109,16 +109,16 @@ that cannot be determined before apply, and a `-target` may be needed. including `uuid`, `bcrypt`, or `timestamp`, as their evaluation is deferred during the main evaluation step. -Sensitive values, such as [sensitive input variables](/language/values/variables#suppressing-values-in-cli-output), -[sensitive outputs](/language/values/outputs#sensitive-suppressing-values-in-cli-output), -or [sensitive resource attributes](/language/expressions/references#sensitive-resource-attributes), +Sensitive values, such as [sensitive input variables](/terraform/language/values/variables#suppressing-values-in-cli-output), +[sensitive outputs](/terraform/language/values/outputs#sensitive-suppressing-values-in-cli-output), +or [sensitive resource attributes](/terraform/language/expressions/references#sensitive-resource-attributes), cannot be used as arguments to `for_each`. The value used in `for_each` is used to identify the resource instance and will always be disclosed in UI output, which is why sensitive values are not allowed. Attempts to use sensitive values as `for_each` arguments will result in an error. If you transform a value containing sensitive data into an argument to be used in `for_each`, be aware that -[most functions in Terraform will return a sensitive result if given an argument with any sensitive content](/language/expressions/function-calls#using-sensitive-data-as-function-arguments). +[most functions in Terraform will return a sensitive result if given an argument with any sensitive content](/terraform/language/expressions/function-calls#using-sensitive-data-as-function-arguments). In many cases, you can achieve similar results to a function used for this purpose by using a `for` expression. For example, if you would like to call `keys(local.map)`, where `local.map` is an object with sensitive values (but non-sensitive keys), you can create a @@ -126,28 +126,28 @@ value to pass to `for_each` with `toset([for k,v in local.map : k])`. ## Using Expressions in `for_each` -The `for_each` meta-argument accepts map or set [expressions](/language/expressions). +The `for_each` meta-argument accepts map or set [expressions](/terraform/language/expressions). However, unlike most arguments, the `for_each` value must be known _before_ Terraform performs any remote resource actions. This means `for_each` can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created). -The `for_each` value must be a map or set with one element per desired -resource instance. When providing a set, you must use an expression that -explicitly returns a set value, like the [`toset`](/language/functions/toset) -function; to prevent unwanted surprises during conversion, the `for_each` -argument does not implicitly convert lists or tuples to sets. +The `for_each` value must be a map or set with one element per desired resource +instance. To use a sequence as the `for_each` value, you must use an expression +that explicitly returns a set value, like the [toset](/terraform/language/functions/toset) +function. To prevent unwanted surprises during conversion, the `for_each` argument +does not implicitly convert lists or tuples to sets. If you need to declare resource instances based on a nested data structure or combinations of elements from multiple data structures you can use Terraform expressions and functions to derive a suitable value. For example: - Transform a multi-level nested structure into a flat list by - [using nested `for` expressions with the `flatten` function](/language/functions/flatten#flattening-nested-structures-for-for_each). + [using nested `for` expressions with the `flatten` function](/terraform/language/functions/flatten#flattening-nested-structures-for-for_each). - Produce an exhaustive list of combinations of elements from two or more collections by - [using the `setproduct` function inside a `for` expression](/language/functions/setproduct#finding-combinations-for-for_each). + [using the `setproduct` function inside a `for` expression](/terraform/language/functions/setproduct#finding-combinations-for-for_each). ### Chaining `for_each` Between Resources @@ -227,7 +227,7 @@ as a whole. ## Using Sets The Terraform language doesn't have a literal syntax for -[set values](/language/expressions/type-constraints#collection-types), but you can use the `toset` +[set values](/terraform/language/expressions/type-constraints#collection-types), but you can use the `toset` function to explicitly convert a list of strings to a set: ```hcl @@ -256,7 +256,7 @@ removes any duplicate elements. `toset(["b", "a", "b"])` will produce a set containing only `"a"` and `"b"` in no particular order; the second `"b"` is discarded. -If you are writing a module with an [input variable](/language/values/variables) that +If you are writing a module with an [input variable](/terraform/language/values/variables) that will be used as a set of strings for `for_each`, you can set its type to `set(string)` to avoid the need for an explicit type conversion: diff --git a/website/docs/language/meta-arguments/lifecycle.mdx b/website/docs/language/meta-arguments/lifecycle.mdx index a6e093a1a6..b62bb5131f 100644 --- a/website/docs/language/meta-arguments/lifecycle.mdx +++ b/website/docs/language/meta-arguments/lifecycle.mdx @@ -7,10 +7,9 @@ description: >- # The `lifecycle` Meta-Argument -> **Hands-on:** Try the [Lifecycle Management](https://learn.hashicorp.com/tutorials/terraform/resource-lifecycle?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Lifecycle Management](/terraform/tutorials/state/resource-lifecycle?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -The general lifecycle for resources is described in the -[Resource Behavior](/language/resources/behavior) page. Some details of +The [Resource Behavior](/terraform/language/resources/behavior) page describes the general lifecycle for resources. Some details of that behavior can be customized using the special nested `lifecycle` block within a resource block body: @@ -50,8 +49,13 @@ The arguments available within a `lifecycle` block are `create_before_destroy`, such features, so you must understand the constraints for each resource type before using `create_before_destroy` with it. - Destroy provisioners of this resource will not run if `create_before_destroy` - is set to `true`. We may address this in the future, and this [GitHub issue](https://github.com/hashicorp/terraform/issues/13549) contains more details. + Note that Terraform propagates and applies the `create_before_destroy` meta-attribute + behaviour to all resource dependencies. For example, if `create_before_destroy` is enabled on resource A but not on resource B, but resource A is dependent on resource B, then Terraform enables `create_before_destroy` for resource B + implicitly by default and stores it to the state file. You cannot override `create_before_destroy` + to `false` on resource B because that would imply dependency cycles in the graph. + + Destroy provisioners of this resource do not run if `create_before_destroy` + is set to `true`. * `prevent_destroy` (bool) - This meta-argument, when set to `true`, will cause Terraform to reject with an error any plan that would destroy the @@ -145,6 +149,8 @@ The arguments available within a `lifecycle` block are `create_before_destroy`, } ``` + `replace_triggered_by` allows only resource addresses because the decision is based on the planned actions for all of the given resources. Plain values such as local values or input variables do not have planned actions of their own, but you can treat them with a resource-like lifecycle by using them with [the `terraform_data` resource type](/terraform/language/resources/terraform-data). + ## Custom Condition Checks You can add `precondition` and `postcondition` blocks with a `lifecycle` block to specify assumptions and guarantees about how resources and data sources operate. The following examples creates a precondition that checks whether the AMI is properly configured. @@ -167,7 +173,7 @@ resource "aws_instance" "example" { Custom conditions can help capture assumptions, helping future maintainers understand the configuration design and intent. They also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. -Refer to [Custom Conditions](/language/expressions/custom-conditions#preconditions-and-postconditions) for more details. +Refer to [Custom Conditions](/terraform/language/expressions/custom-conditions#preconditions-and-postconditions) for more details. ## Literal Values Only diff --git a/website/docs/language/meta-arguments/module-providers.mdx b/website/docs/language/meta-arguments/module-providers.mdx index 2b7f7d9a9e..3b7339cfdb 100644 --- a/website/docs/language/meta-arguments/module-providers.mdx +++ b/website/docs/language/meta-arguments/module-providers.mdx @@ -7,9 +7,9 @@ description: >- # The Module `providers` Meta-Argument -In a [module call](/language/modules/syntax) block, the +In a [module call](/terraform/language/modules/syntax) block, the optional `providers` meta-argument specifies which -[provider configurations](/language/providers/configuration) from the parent +[provider configurations](/terraform/language/providers/configuration) from the parent module will be available inside the child module. ```hcl @@ -38,7 +38,7 @@ module "example" { ## Default Behavior: Inherit Default Providers -If the child module does not declare any [configuration aliases](/language/modules/develop/providers#provider-aliases-within-modules), +If the child module does not declare any [configuration aliases](/terraform/language/modules/develop/providers#provider-aliases-within-modules), the `providers` argument is optional. If you omit it, a child module inherits all of the _default_ provider configurations from its parent module. (Default provider configurations are ones that don't use the `alias` argument.) @@ -123,4 +123,4 @@ names it needs. For more details and guidance about working with providers inside a re-usable child module, see -[Module Development: Providers Within Modules](/language/modules/develop/providers). +[Module Development: Providers Within Modules](/terraform/language/modules/develop/providers). diff --git a/website/docs/language/meta-arguments/resource-provider.mdx b/website/docs/language/meta-arguments/resource-provider.mdx index 683eaa3e01..a4b72a0b64 100644 --- a/website/docs/language/meta-arguments/resource-provider.mdx +++ b/website/docs/language/meta-arguments/resource-provider.mdx @@ -11,7 +11,7 @@ The `provider` meta-argument specifies which provider configuration to use for a overriding Terraform's default behavior of selecting one based on the resource type name. Its value should be an unquoted `.` reference. -As described in [Provider Configuration](/language/providers/configuration), you can optionally +As described in [Provider Configuration](/terraform/language/providers/configuration), you can optionally create multiple configurations for a single provider (usually to manage resources in different regions of multi-region services). Each provider can have one default configuration, and any number of alternate configurations that @@ -53,7 +53,7 @@ ensure that the provider is fully configured before any resource actions are taken. The `provider` meta-argument expects -[a `.` reference](/language/providers/configuration#referring-to-alternate-provider-configurations), +[a `.` reference](/terraform/language/providers/configuration#referring-to-alternate-provider-configurations), which does not need to be quoted. Arbitrary expressions are not permitted for `provider` because it must be resolved while Terraform is constructing the dependency graph, before it is safe to evaluate expressions. diff --git a/website/docs/language/modules/develop/composition.mdx b/website/docs/language/modules/develop/composition.mdx index e9758b58ac..d50679f411 100644 --- a/website/docs/language/modules/develop/composition.mdx +++ b/website/docs/language/modules/develop/composition.mdx @@ -193,7 +193,7 @@ Every module has implicit assumptions and guarantees that define what data it ex - **Assumption:** A condition that must be true in order for the configuration of a particular resource to be usable. For example, an `aws_instance` configuration can have the assumption that the given AMI will always be configured for the `x86_64` CPU architecture. - **Guarantee:** A characteristic or behavior of an object that the rest of the configuration should be able to rely on. For example, an `aws_instance` configuration can have the guarantee that an EC2 instance will be running in a network that assigns it a private DNS record. -We recommend using [custom conditions](/language/expressions/custom-conditions) help capture and test for assumptions and guarantees. This helps future maintainers understand the configuration design and intent. Custom conditions also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. +We recommend using [custom conditions](/terraform/language/expressions/custom-conditions) to help capture and test for assumptions and guarantees. This helps future maintainers understand the configuration design and intent. Custom conditions also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. The following examples creates a precondition that checks whether the EC2 instance has an encrypted root volume. @@ -331,7 +331,7 @@ Most modules contain `resource` blocks and thus describe infrastructure to be created and managed. It may sometimes be useful to write modules that do not describe any new infrastructure at all, but merely retrieve information about existing infrastructure that was created elsewhere using -[data sources](/language/data-sources). +[data sources](/terraform/language/data-sources). As with conventional modules, we suggest using this technique only when the module raises the level of abstraction in some way, in this case by @@ -344,7 +344,7 @@ we might write a shared module called `join-network-aws` which can be called by any configuration that needs information about the shared network when deployed in AWS: -``` +```hcl module "network" { source = "./modules/join-network-aws" @@ -367,7 +367,7 @@ data sources, or it could read saved information from a Consul cluster using [`consul_keys`](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/data-sources/keys), or it might read the outputs directly from the state of the configuration that manages the network using -[`terraform_remote_state`](/language/state/remote-state-data). +[`terraform_remote_state`](/terraform/language/state/remote-state-data). The key benefit of this approach is that the source of this information can change over time without updating every configuration that depends on it. diff --git a/website/docs/language/modules/develop/index.mdx b/website/docs/language/modules/develop/index.mdx index 760b5ea7a8..aee54ee9e5 100644 --- a/website/docs/language/modules/develop/index.mdx +++ b/website/docs/language/modules/develop/index.mdx @@ -7,34 +7,34 @@ description: >- # Creating Modules -> **Hands-on:** Try the [Reuse Configuration with Modules](https://learn.hashicorp.com/collections/terraform/modules?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Reuse Configuration with Modules](/terraform/tutorials/modules?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. A _module_ is a container for multiple resources that are used together. -Modules can be used to create lightweight abstractions, so that you can +You can use modules to create lightweight abstractions, so that you can describe your infrastructure in terms of its architecture, rather than directly in terms of physical objects. -The `.tf` files in your working directory when you run [`terraform plan`](/cli/commands/plan) -or [`terraform apply`](/cli/commands/apply) together form the _root_ -module. That module may [call other modules](/language/modules/syntax#calling-a-child-module) +The `.tf` files in your working directory when you run [`terraform plan`](/terraform/cli/commands/plan) +or [`terraform apply`](/terraform/cli/commands/apply) together form the _root_ +module. That module may [call other modules](/terraform/language/modules/syntax#calling-a-child-module) and connect them together by passing output values from one to input values of another. -To learn how to _use_ modules, see [the Modules configuration section](/language/modules). +To learn how to _use_ modules, see [the Modules configuration section](/terraform/language/modules). This section is about _creating_ re-usable modules that other configurations can include using `module` blocks. ## Module structure Re-usable modules are defined using all of the same -[configuration language](/language) concepts we use in root modules. +[configuration language](/terraform/language) concepts we use in root modules. Most commonly, modules use: -* [Input variables](/language/values/variables) to accept values from +* [Input variables](/terraform/language/values/variables) to accept values from the calling module. -* [Output values](/language/values/outputs) to return results to the +* [Output values](/terraform/language/values/outputs) to return results to the calling module, which it can then use to populate arguments elsewhere. -* [Resources](/language/resources) to define one or more +* [Resources](/terraform/language/resources) to define one or more infrastructure objects that the module will manage. To define a module, create a new directory for it and place one or more `.tf` @@ -44,7 +44,7 @@ be re-used by lots of configurations you may wish to place it in its own version control repository. Modules can also call other modules using a `module` block, but we recommend -keeping the module tree relatively flat and using [module composition](/language/modules/develop/composition) +keeping the module tree relatively flat and using [module composition](/terraform/language/modules/develop/composition) as an alternative to a deeply-nested tree of modules, because this makes the individual modules easier to re-use in different combinations. @@ -70,9 +70,15 @@ your module is not creating any new abstraction and so the module is adding unnecessary complexity. Just use the resource type directly in the calling module instead. +### No-Code Provisioning in HCP Terraform + +You can also create no-code ready modules to enable the no-code provisioning workflow in HCP Terraform. No-code provisioning lets users deploy a module's resources in HCP Terraform without writing any Terraform configuration. + +No-code ready modules have additional requirements and considerations. Refer to [Designing No-Code Ready Modules](/terraform/cloud-docs/no-code-provisioning/module-design) in the HCP Terraform documentation for details. + ## Refactoring module resources -You can include [refactoring blocks](/language/modules/develop/refactoring) to record how resource +You can include [refactoring blocks](/terraform/language/modules/develop/refactoring) to record how resource names and module structure have changed from previous module versions. Terraform uses that information during planning to reinterpret existing objects as if they had been created at the corresponding new addresses, eliminating a diff --git a/website/docs/language/modules/develop/providers.mdx b/website/docs/language/modules/develop/providers.mdx index 717ef16618..df1a88df83 100644 --- a/website/docs/language/modules/develop/providers.mdx +++ b/website/docs/language/modules/develop/providers.mdx @@ -1,5 +1,7 @@ --- page_title: Providers Within Modules - Configuration Language +description: >- + Use providers within Terraform modules. Learn about version constraints, aliases, implicit inheritance, and passing providers to Terraform modules. --- # Providers Within Modules @@ -15,7 +17,7 @@ Terraform, are global to an entire Terraform configuration and can be shared across module boundaries. Provider configurations can be defined only in a root Terraform module. -Providers can be passed down to descendent modules in two ways: either +Providers can be passed down to descendant modules in two ways: either _implicitly_ through inheritance, or _explicitly_ via the `providers` argument within a `module` block. These two options are discussed in more detail in the following sections. @@ -44,10 +46,10 @@ to reintroduce the provider configuration. ## Provider Version Constraints in Modules Although provider _configurations_ are shared between modules, each module must -declare its own [provider requirements](/language/providers/requirements), so that +declare its own [provider requirements](/terraform/language/providers/requirements), so that Terraform can ensure that there is a single version of the provider that is compatible with all modules in the configuration and to specify the -[source address](/language/providers/requirements#source-addresses) that serves as +[source address](/terraform/language/providers/requirements#source-addresses) that serves as the global (module-agnostic) identifier for a provider. To declare that a module requires particular versions of a specific provider, @@ -70,7 +72,7 @@ however, specify any of the configuration settings that determine what remote endpoints the provider will access, such as an AWS region; configuration settings come from provider _configurations_, and a particular overall Terraform configuration can potentially have -[several different configurations for the same provider](/language/providers/configuration#alias-multiple-provider-configurations). +[several different configurations for the same provider](/terraform/language/providers/configuration#alias-multiple-provider-configurations). ## Provider Aliases Within Modules @@ -102,7 +104,7 @@ features are needed by other parts of their overall configuration. ## Implicit Provider Inheritance For convenience in simple configurations, a child module automatically inherits -[default provider configurations](/language/providers/configuration#default-provider-configurations) from its parent. This means that +[default provider configurations](/terraform/language/providers/configuration#default-provider-configurations) from its parent. This means that explicit `provider` blocks appear only in the root module, and downstream modules can simply declare resources for that provider and have them automatically associated with the root provider configurations. @@ -132,10 +134,10 @@ resource "aws_s3_bucket" "example" { We recommend using this approach when a single configuration for each provider is sufficient for an entire configuration. -~> **Note:** Only provider configurations are inherited by child modules, not provider source or version requirements. Each module must [declare its own provider requirements](/language/providers/requirements). This is especially important for non-HashiCorp providers. +~> **Note:** Only provider configurations are inherited by child modules, not provider source or version requirements. Each module must [declare its own provider requirements](/terraform/language/providers/requirements). This is especially important for non-HashiCorp providers. In more complex situations there may be -[multiple provider configurations](/language/providers/configuration#alias-multiple-provider-configurations), +[multiple provider configurations](/terraform/language/providers/configuration#alias-multiple-provider-configurations), or a child module may need to use different provider settings than its parent. For such situations, you must pass providers explicitly. @@ -172,7 +174,7 @@ module "example" { ``` The `providers` argument within a `module` block is similar to -[the `provider` argument](/language/meta-arguments/resource-provider) +[the `provider` argument](/terraform/language/meta-arguments/resource-provider) within a resource, but is a map rather than a single string because a module may contain resources from many different providers. @@ -332,7 +334,7 @@ provider "aws" { provider "google" { alias = "usw1" - credentials = "${file("account.json")}" + credentials = file("account.json") project = "my-project-id" region = "us-west1" zone = "us-west1-a" @@ -340,7 +342,7 @@ provider "google" { provider "google" { alias = "usw2" - credentials = "${file("account.json")}" + credentials = file("account.json") project = "my-project-id" region = "us-west2" zone = "us-west2-a" @@ -350,7 +352,7 @@ module "bucket_w1" { source = "./publish_bucket" providers = { aws.src = aws.usw1 - google.src = google.usw2 + google.src = google.usw1 } } diff --git a/website/docs/language/modules/develop/publish.mdx b/website/docs/language/modules/develop/publish.mdx index 6a5e0992d6..c904a0a23a 100644 --- a/website/docs/language/modules/develop/publish.mdx +++ b/website/docs/language/modules/develop/publish.mdx @@ -6,12 +6,12 @@ description: A module is a container for multiple resources that are used togeth # Publishing Modules If you've built a module that you intend to be reused, we recommend -[publishing the module](/registry/modules/publish) on the +[publishing the module](/terraform/registry/modules/publish) on the [Terraform Registry](https://registry.terraform.io). This will version your module, generate documentation, and more. Published modules can be easily consumed by Terraform, and users can -[constrain module versions](/language/modules/syntax#version) +[constrain module versions](/terraform/language/modules/syntax#version) for safe and predictable updates. The following example shows how a caller might use a module from the Terraform Registry: @@ -22,7 +22,7 @@ module "consul" { ``` If you do not wish to publish your modules in the public registry, you can -instead use a [private registry](/registry/private) to get +instead use a [private registry](/terraform/registry/private) to get the same benefits. We welcome contributions of Terraform modules from our community members, partners, and customers. Our ecosystem is made richer by each new module created or an existing one updated, as they reflect the wide range of experience and technical requirements of the community that uses them. Our cloud provider partners often seek to develop specific modules for popular or challenging use cases on their platform and utilize them as valuable learning experiences to empathize with their users. Similarly, our community module developers incorporate a variety of opinions and use cases from the broader Terraform community. Both types of modules have their place in the Terraform registry, accessible to practitioners who can decide which modules best fit their requirements. @@ -31,11 +31,11 @@ We welcome contributions of Terraform modules from our community members, partne Although the registry is the native mechanism for distributing re-usable modules, Terraform can also install modules from -[various other sources](/language/modules/sources). The alternative sources +[various other sources](/terraform/language/modules/sources). The alternative sources do not support the first-class versioning mechanism, but some sources have their own mechanisms for selecting particular VCS commits, etc. We recommend that modules distributed via other protocols still use the -[standard module structure](/language/modules/develop/structure) so that they can +[standard module structure](/terraform/language/modules/develop/structure) so that they can be used in a similar way as a registry module or be published on the registry at a later time. diff --git a/website/docs/language/modules/develop/refactoring.mdx b/website/docs/language/modules/develop/refactoring.mdx index 2831404161..1344b0d791 100644 --- a/website/docs/language/modules/develop/refactoring.mdx +++ b/website/docs/language/modules/develop/refactoring.mdx @@ -6,7 +6,7 @@ description: How to make backward-compatible changes to modules already in use. # Refactoring -> **Note:** Explicit refactoring declarations with `moved` blocks is available in Terraform v1.1 and later. For earlier Terraform versions or for refactoring actions too complex to express as `moved` blocks, you can -use the [`terraform state mv` CLI command](/cli/commands/state/mv) +use the [`terraform state mv` CLI command](/terraform/cli/commands/state/mv) as a separate step. In shared modules and long-lived configurations, you may eventually outgrow @@ -23,7 +23,7 @@ When you add `moved` blocks in your configuration to record where you've historically moved or renamed an object, Terraform treats an existing object at the old address as if it now belongs to the new address. -> **Hands On:** Try the [Use Configuration to Move Resources](https://learn.hashicorp.com/tutorials/terraform/move-config) tutorial on HashiCorp Learn. +> **Hands On:** Try the [Use Configuration to Move Resources](/terraform/tutorials/configuration-language/move-config) tutorial. ## `moved` Block Syntax @@ -103,11 +103,13 @@ so Terraform recognizes the move for all instances of the resource. That is, it covers both `aws_instance.a[0]` and `aws_instance.a[1]` without the need to identify each one separately. -Each resource type has a separate schema and so objects of different types -are not compatible. Therefore, although you can use `moved` to change the name -of a resource, you _cannot_ use `moved` to change to a different resource type -or to change a managed resource (a `resource` block) into a data resource -(a `data` block). +Each resource type has a separate schema so objects of different types +are not typically compatible. You can always use the `moved` block to change +the name of a resource, but some providers also let you change an object from +one resource type to another. Refer to the provider documentation for details +on which resources can move between types. You _cannot_ use the `moved` +block to change a managed resource (a `resource` block) into a data +resource (a `data` block). ## Enabling `count` or `for_each` For a Resource @@ -122,7 +124,7 @@ resource "aws_instance" "a" { Applying this configuration would lead to Terraform creating an object bound to the address `aws_instance.a`. -Later, you use [`for_each`](/language/meta-arguments/for_each) with this +Later, you use [`for_each`](/terraform/language/meta-arguments/for_each) with this resource to systematically declare multiple instances. To preserve an object that was previously associated with `aws_instance.a` alone, you must add a `moved` block to specify which instance key the object will take in the new @@ -261,7 +263,7 @@ Applying this configuration would cause Terraform to create objects whose addresses begin with `module.a`. In later module versions, you may need to use -[`count`](/language/meta-arguments/count) with this resource to systematically +[`count`](/terraform/language/meta-arguments/count) with this resource to systematically declare multiple instances. To preserve an object that was previously associated with `aws_instance.a` alone, you can add a `moved` block to specify which instance key that object will take in the new configuration: @@ -398,7 +400,7 @@ typical rule that a parent module sees its child module as a "closed box", unaware of exactly which resources are declared inside it. This compromise assumes that all three of these modules are maintained by the same people and distributed together in a single -[module package](/language/modules/sources#modules-in-package-sub-directories). +[module package](/terraform/language/modules/sources#modules-in-package-sub-directories). Terraform resolves module references in `moved` blocks relative to the module instance they are defined in. For example, if the original module above were diff --git a/website/docs/language/modules/develop/structure.mdx b/website/docs/language/modules/develop/structure.mdx index dbe2db33c3..20ba5c9458 100644 --- a/website/docs/language/modules/develop/structure.mdx +++ b/website/docs/language/modules/develop/structure.mdx @@ -1,5 +1,7 @@ --- page_title: Standard Module Structure +description: >- + Learn about the recommended file and directory structure for developing reusable modules distributed as separate repositories. --- # Standard Module Structure @@ -53,8 +55,8 @@ don't need to do any extra work to follow the standard structure. * **Variables and outputs should have descriptions.** All variables and outputs should have one or two sentence descriptions that explain their purpose. This is used for documentation. See the documentation for - [variable configuration](/language/values/variables) and - [output configuration](/language/values/outputs) for more details. + [variable configuration](/terraform/language/values/variables) and + [output configuration](/terraform/language/values/outputs) for more details. * **Nested modules**. Nested modules should exist under the `modules/` subdirectory. Any nested module with a `README.md` is considered usable @@ -74,7 +76,7 @@ don't need to do any extra work to follow the standard structure. again separately. If a repository or package contains multiple nested modules, they should - ideally be [composable](/language/modules/develop/composition) by the caller, rather than + ideally be [composable](/terraform/language/modules/develop/composition) by the caller, rather than calling directly to each other and creating a deeply-nested tree of modules. * **Examples**. Examples of using the module should exist under the diff --git a/website/docs/language/modules/index.mdx b/website/docs/language/modules/index.mdx index bbfad4e8e1..9898b34c10 100644 --- a/website/docs/language/modules/index.mdx +++ b/website/docs/language/modules/index.mdx @@ -7,7 +7,7 @@ description: >- # Modules -> **Hands-on:** Try the [Reuse Configuration with Modules](https://learn.hashicorp.com/collections/terraform/modules?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Reuse Configuration with Modules](/terraform/tutorials/modules?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. _Modules_ are containers for multiple resources that are used together. A module consists of a collection of `.tf` and/or `.tf.json` files kept together in a @@ -44,27 +44,27 @@ download them automatically if you specify the appropriate source and version in a module call block. Also, members of your organization might produce modules specifically crafted -for your own infrastructure needs. [Terraform Cloud](/cloud) and -[Terraform Enterprise](/enterprise) both include a private +for your own infrastructure needs. [HCP Terraform](https://cloud.hashicorp.com/products/terraform) and +[Terraform Enterprise](/terraform/enterprise) both include a private module registry for sharing modules internally within your organization. ## Using Modules -- [Module Blocks](/language/modules/syntax) documents the syntax for +- [Module Blocks](/terraform/language/modules/syntax) documents the syntax for calling a child module from a parent module, including meta-arguments like `for_each`. -- [Module Sources](/language/modules/sources) documents what kinds of paths, +- [Module Sources](/terraform/language/modules/sources) documents what kinds of paths, addresses, and URIs can be used in the `source` argument of a module block. - The Meta-Arguments section documents special arguments that can be used with every module, including - [`providers`](/language/meta-arguments/module-providers), - [`depends_on`](/language/meta-arguments/depends_on), - [`count`](/language/meta-arguments/count), - and [`for_each`](/language/meta-arguments/for_each). + [`providers`](/terraform/language/meta-arguments/module-providers), + [`depends_on`](/terraform/language/meta-arguments/depends_on), + [`count`](/terraform/language/meta-arguments/count), + and [`for_each`](/terraform/language/meta-arguments/for_each). ## Developing Modules For information about developing reusable modules, see -[Module Development](/language/modules/develop). +[Module Development](/terraform/language/modules/develop). diff --git a/website/docs/language/modules/sources.mdx b/website/docs/language/modules/sources.mdx index ea06f0dea7..7144fd7eca 100644 --- a/website/docs/language/modules/sources.mdx +++ b/website/docs/language/modules/sources.mdx @@ -8,19 +8,16 @@ description: >- # Module Sources -The `source` argument in [a `module` block](/language/modules/syntax) +The `source` argument in [a `module` block](/terraform/language/modules/syntax) tells Terraform where to find the source code for the desired child module. Terraform uses this during the module installation step of `terraform init` -to download the source code to a directory on local disk so that it can be -used by other Terraform commands. +to download the source code to a directory on local disk so that other Terraform commands can use it. -> **Hands-on:** Try our HashiCorp Learn tutorials to use modules from [the -> registry](https://learn.hashicorp.com/tutorials/terraform/module-use) -> or [locally](https://learn.hashicorp.com/tutorials/terraform/module-create). +> **Hands-on:** Try the [Use Modules From the Registry](/terraform/tutorials/modules/module-use) or [Build and Use a Local Module](/terraform/tutorials/modules/module-create) tutorials. The module installer supports installation from a number of different source -types, as listed below. +types. - [Local paths](#local-paths) @@ -100,10 +97,10 @@ to get started with Terraform and find modules created by others in the community. You can also use a -[private registry](/registry/private), either -via the built-in feature from Terraform Cloud, or by running a custom +[private registry](/terraform/registry/private), either +via the built-in feature from HCP Terraform, or by running a custom service that implements -[the module registry protocol](/registry/api-docs). +[the module registry protocol](/terraform/registry/api-docs). Modules on the public Terraform Registry can be referenced using a registry source address of the form `//`, with each @@ -131,23 +128,25 @@ module "consul" { } ``` -If you are using the SaaS version of Terraform Cloud, its private +If you are using the SaaS version of HCP Terraform, its private registry hostname is `app.terraform.io`. If you use a self-hosted Terraform Enterprise instance, its private registry hostname is the same as the host where you'd access the web UI and the host you'd use when configuring -the [Terraform Cloud CLI integration](/cli/cloud). +the [HCP Terraform CLI integration](/terraform/cli/cloud). + +Both HCP Terraform and self-hosted Terraform Enterprise support a [generic hostname](/terraform/cloud-docs/registry/using#generic-hostname-terraform-cloud-and-terraform-enterprise) `localterraform.com`. Registry modules support versioning. You can provide a specific version as shown in the above examples, or use flexible -[version constraints](/language/modules/syntax#version). +[version constraints](/terraform/language/modules/syntax#version). You can learn more about the registry at the -[Terraform Registry documentation](/registry/modules/use#using-modules). +[Terraform Registry documentation](/terraform/registry/modules/use#using-modules). To access modules from a private registry, you may need to configure an access -token [in the CLI config](/cli/config/config-file#credentials). Use the +token [in the CLI config](/terraform/cli/config/config-file#credentials). Use the same hostname as used in the module source string. For a private registry -within Terraform Cloud, use the same authentication token as you would +within HCP Terraform, use the same authentication token as you would use with the Enterprise API or command-line clients. ## GitHub @@ -230,9 +229,9 @@ username/password credentials, configure [Git Credentials Storage](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage) to select a suitable source of credentials for your environment. -If your Terraform configuration will be used within [Terraform Cloud](https://www.hashicorp.com/products/terraform), +If your Terraform configuration will be used within [HCP Terraform](https://www.hashicorp.com/products/terraform), only SSH key authentication is supported, and -[keys can be configured on a per-workspace basis](/cloud-docs/workspaces/settings/ssh-keys). +[keys can be configured on a per-workspace basis](/terraform/cloud-docs/workspaces/settings/ssh-keys). ### Selecting a Revision @@ -263,8 +262,14 @@ to reduce the time taken to retrieve the remote repository. The `depth` URL argument corresponds to [the `--depth` argument to `git clone`](https://git-scm.com/docs/git-clone#Documentation/git-clone.txt---depthltdepthgt), -telling Git to create a shallow clone with the history truncated to only -the specified number of commits. +which instructs Git to create a shallow clone that includes +only the specified number of commits in the history. +Setting `depth` to `1` is suitable for most cases. This is because Terraform only uses the most recently selected commit to find the source. +```hcl +module "vpc" { + source = "git::https://example.com/vpc.git?depth=1&ref=v1.2.0" +} +``` However, because shallow clone requires different Git protocol behavior, setting the `depth` argument makes Terraform pass your [`ref` argument](#selecting-a-revision), @@ -273,9 +278,6 @@ if any, to instead. That means it must specify a named branch or tag known to the remote repository, and that raw commit IDs are not acceptable. -Because Terraform only uses the most recent selected commit to find the source -code of your specified module, it is not typically useful to set `depth` -to any value other than `1`. ### "scp-like" address syntax @@ -319,9 +321,9 @@ automatically. This is the most common way to access non-public Mercurial repositories from automated systems because it allows access to private repositories without interactive prompts. -If your Terraform configuration will be used within [Terraform Cloud](https://www.hashicorp.com/products/terraform), +If your Terraform configuration will be used within [HCP Terraform](https://www.hashicorp.com/products/terraform), only SSH key authentication is supported, and -[keys can be configured on a per-workspace basis](/cloud-docs/workspaces/settings/ssh-keys). +[keys can be configured on a per-workspace basis](/terraform/cloud-docs/workspaces/settings/ssh-keys). ### Selecting a Revision @@ -359,8 +361,9 @@ In either case, the result is interpreted as another module source address using one of the forms documented elsewhere on this page. If an HTTP/HTTPS URL requires authentication credentials, use a `.netrc` -file in your home directory to configure these. For information on this format, -see [the documentation for using it in `curl`](https://everything.curl.dev/usingcurl/netrc). +file to configure the credentials. By default, Terraform searches for the `.netrc` file +in your HOME directory. However, you can override the default filesystem location by setting the `NETRC` environment variable. For information on the `.netrc` format, +refer to [the documentation for using it in `curl`](https://everything.curl.dev/usingcurl/netrc). ### Fetching archives over HTTP @@ -378,9 +381,9 @@ module "vpc" { The extensions that Terraform recognizes for this special behavior are: - `zip` -- `tar.bz2` and `tbz2` -- `tar.gz` and `tgz` -- `tar.xz` and `txz` +- `bz2`, `tar.bz2`, `tar.tbz2`, and `tbz2` +- `gz`, `tar.gz`, and `tgz` +- `xz`, `tar.xz`, and `txz` If your URL _doesn't_ have one of these extensions but refers to an archive anyway, use the `archive` argument to force this interpretation: @@ -458,7 +461,7 @@ When the source of a module is a version control repository or archive file (generically, a "package"), the module itself may be in a sub-directory relative to the root of the package. -A special double-slash syntax is interpreted by Terraform to indicate that +A special double-slash syntax `//` is interpreted by Terraform to indicate that the remaining path after that point is a sub-directory within the package. For example: diff --git a/website/docs/language/modules/syntax.mdx b/website/docs/language/modules/syntax.mdx index 87064e9d35..e23e26ada9 100644 --- a/website/docs/language/modules/syntax.mdx +++ b/website/docs/language/modules/syntax.mdx @@ -8,7 +8,7 @@ description: >- # Module Blocks -> **Hands-on:** Try the [Reuse Configuration with Modules](https://learn.hashicorp.com/collections/terraform/modules?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Reuse Configuration with Modules](/terraform/tutorials/modules?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. A _module_ is a container for multiple resources that are used together. @@ -23,13 +23,13 @@ in separate configurations, allowing resource configurations to be packaged and re-used. This page describes how to call one module from another. For more information -about creating re-usable child modules, see [Module Development](/language/modules/develop). +about creating re-usable child modules, see [Module Development](/terraform/language/modules/develop). ## Calling a Child Module To _call_ a module means to include the contents of that module into the configuration with specific values for its -[input variables](/language/values/variables). Modules are called +[input variables](/terraform/language/values/variables). Modules are called from within other modules using `module` blocks: ```hcl @@ -53,7 +53,7 @@ Module calls use the following kinds of arguments: - The `version` argument is recommended for modules from a registry. -- Most other arguments correspond to [input variables](/language/values/variables) +- Most other arguments correspond to [input variables](/terraform/language/values/variables) defined by the module. (The `servers` argument in the example above is one of these.) @@ -67,7 +67,7 @@ Terraform. Its value is either the path to a local directory containing the module's configuration files, or a remote module source that Terraform should download and use. This value must be a literal string with no template sequences; arbitrary expressions are not allowed. For more information on -possible values for this argument, see [Module Sources](/language/modules/sources). +possible values for this argument, see [Module Sources](/terraform/language/modules/sources). The same source address can be specified in multiple `module` blocks to create multiple copies of the resources defined within, possibly with different @@ -95,14 +95,14 @@ module "consul" { } ``` -The `version` argument accepts a [version constraint string](/language/expressions/version-constraints). +The `version` argument accepts a [version constraint string](/terraform/language/expressions/version-constraints). Terraform will use the newest installed version of the module that meets the constraint; if no acceptable versions are installed, it will download the newest version that meets the constraint. Version constraints are supported only for modules installed from a module registry, such as the public [Terraform Registry](https://registry.terraform.io/) -or [Terraform Cloud's private module registry](/cloud-docs/registry). +or [HCP Terraform's private module registry](/terraform/cloud-docs/registry). Other module sources can provide their own versioning mechanisms within the source string itself, or might not support versions at all. In particular, modules sourced from local file paths do not support `version`; since @@ -116,32 +116,31 @@ optional meta-arguments that have special meaning across all modules, described in more detail in the following pages: - `count` - Creates multiple instances of a module from a single `module` block. - See [the `count` page](/language/meta-arguments/count) + See [the `count` page](/terraform/language/meta-arguments/count) for details. - `for_each` - Creates multiple instances of a module from a single `module` block. See - [the `for_each` page](/language/meta-arguments/for_each) + [the `for_each` page](/terraform/language/meta-arguments/for_each) for details. - `providers` - Passes provider configurations to a child module. See - [the `providers` page](/language/meta-arguments/module-providers) + [the `providers` page](/terraform/language/meta-arguments/module-providers) for details. If not specified, the child module inherits all of the default (un-aliased) provider configurations from the calling module. - `depends_on` - Creates explicit dependencies between the entire module and the listed targets. See - [the `depends_on` page](/language/meta-arguments/depends_on) + [the `depends_on` page](/terraform/language/meta-arguments/depends_on) for details. -In addition to the above, the `lifecycle` argument is not currently used by -Terraform but is reserved for planned future features. +Terraform does not use the `lifecycle` argument. However, the `lifecycle` block is reserved for future versions. ## Accessing Module Output Values The resources defined in a module are encapsulated, so the calling module cannot access their attributes directly. However, the child module can -declare [output values](/language/values/outputs) to selectively +declare [output values](/terraform/language/values/outputs) to selectively export certain values to be accessed by the calling module. For example, if the `./app-cluster` module referenced in the example above @@ -157,7 +156,7 @@ resource "aws_elb" "example" { ``` For more information about referring to named values, see -[Expressions](/language/expressions). +[Expressions](/terraform/language/expressions). ## Transferring Resource State Into Modules @@ -167,7 +166,7 @@ result, Terraform plans to destroy all resource instances at the old address and create new instances at the new address. To preserve existing objects, you can use -[refactoring blocks](/language/modules/develop/refactoring) to record the old and new +[refactoring blocks](/terraform/language/modules/develop/refactoring) to record the old and new addresses for each resource instance. This directs Terraform to treat existing objects at the old addresses as if they had originally been created at the corresponding new addresses. @@ -177,7 +176,7 @@ corresponding new addresses. You may have an object that needs to be replaced with a new object for a reason that isn't automatically visible to Terraform, such as if a particular virtual machine is running on degraded underlying hardware. In this case, you can use -[the `-replace=...` planning option](/cli/commands/plan#replace-address) +[the `-replace=...` planning option](/terraform/cli/commands/plan#replace-address) to force Terraform to propose replacing that object. If the object belongs to a resource within a nested module, specify the full @@ -194,3 +193,29 @@ The above selects a `resource "aws_instance" "example"` declared inside a Because replacing is a very disruptive action, Terraform only allows selecting individual resource instances. There is no syntax to force replacing _all_ resource instances belonging to a particular module. + +## Removing Modules + +-> **Note:** The `removed` block is available in Terraform v1.7 and later. For earlier Terraform versions, you can use the [`terraform state rm` CLI command](/terraform/cli/commands/state/rm) as a separate step. + +To remove a module from Terraform, simply delete the module call from your Terraform configuration. + +By default, after you remove the `module` block, Terraform will plan to destroy any resources it is managing that were declared in that module. This is because when you remove the module call, that module's configuration is no longer included in your Terraform configuration. + +Sometimes you may wish to remove a module from your Terraform configuration without destroying the real infrastructure objects it manages. In this case, the resources will be removed from the [Terraform state](/terraform/language/state), but the real infrastructure objects will not be destroyed. + +To declare that a module was removed from Terraform configuration but that its managed objects should not be destroyed, remove the `module` block from your configuration and replace it with a `removed` block: + +```hcl +removed { + from = module.example + + lifecycle { + destroy = false + } +} +``` + +The `from` argument is the address of the module you want to remove, without any instance keys (such as "module.example[1]"). + +The `lifecycle` block is required. The `destroy` argument determines whether Terraform will attempt to destroy the objects managed by the module or not. A value of `false` means that Terraform will remove the resources from state without destroying them. diff --git a/website/docs/language/modules/testing-experiment.mdx b/website/docs/language/modules/testing-experiment.mdx deleted file mode 100644 index f230b85b99..0000000000 --- a/website/docs/language/modules/testing-experiment.mdx +++ /dev/null @@ -1,322 +0,0 @@ ---- -page_title: Module Testing Experiment - Configuration Language ---- - -# Module Testing Experiment - -This page is about some experimental features available in recent versions of -Terraform CLI related to integration testing of shared modules. - -The Terraform team is aiming to use these features to gather feedback as part -of ongoing research into different strategies for testing Terraform modules. -These features are likely to change significantly in future releases based on -feedback. - -## Current Research Goals - -Our initial area of research is into the question of whether it's helpful and -productive to write module integration tests in the Terraform language itself, -or whether it's better to handle that as a separate concern orchestrated by -code written in other languages. - -Some existing efforts have piloted both approaches: - -* [Terratest](https://terratest.gruntwork.io/) and - [kitchen-terraform](https://github.com/newcontext-oss/kitchen-terraform) - both pioneered the idea of writing tests for Terraform modules with explicit - orchestration written in the Go and Ruby programming languages, respectively. - -* The Terraform provider - [`apparentlymart/testing`](https://registry.terraform.io/providers/apparentlymart/testing/latest) - introduced the idea of writing Terraform module tests in the Terraform - language itself, using a special provider that can evaluate assertions - and fail `terraform apply` if they don't pass. - -Both of these approaches have both advantages and disadvantages, and so it's -likely that both will coexist for different situations, but the community -efforts have already explored the external-language testing model quite deeply -while the Terraform-integrated testing model has not yet been widely trialled. -For that reason, the current iteration of the module testing experiment is -aimed at trying to make the Terraform-integrated approach more accessible so -that more module authors can hopefully try it and share their experiences. - -## Current Experimental Features - --> This page describes the incarnation of the experimental features introduced -in **Terraform CLI v0.15.0**. If you are using an earlier version of Terraform -then you'll need to upgrade to v0.15.0 or later to use the experimental features -described here, though you only need to use v0.15.0 or later for running tests; -your module itself can remain compatible with earlier Terraform versions, if -needed. - -Our current area of interest is in what sorts of tests can and cannot be -written using features integrated into the Terraform language itself. As a -means to investigate that without invasive, cross-cutting changes to Terraform -Core we're using a special built-in Terraform provider as a placeholder for -potential new features. - -If this experiment is successful then we expect to run a second round of -research and design about exactly what syntax is most ergonomic for writing -tests, but for the moment we're interested less in the specific syntax and more -in the capabilities of this approach. - -The temporary extensions to Terraform for this experiment consist of the -following parts: - -* A temporary experimental provider `terraform.io/builtin/test`, which acts as - a placeholder for potential new language features related to test assertions. - -* A `terraform test` command for more conveniently running multiple tests in - a single action. - -* An experimental convention of placing test configurations in subdirectories - of a `tests` directory within your module, which `terraform test` will then - discover and run. - -We would like to invite adventurous module authors to try writing integration -tests for their modules using these mechanisms, and ideally also share the -tests you write (in a temporary VCS branch, if necessary) so we can see what -you were able to test, along with anything you felt unable to test in this way. - -If you're interested in giving this a try, see the following sections for -usage details. Because these features are temporary experimental extensions, -there's some boilerplate required to activate and make use of it which would -likely not be required in a final design. - -### Writing Tests for a Module - -For the purposes of the current experiment, module tests are arranged into -_test suites_, each of which is a root Terraform module which includes a -`module` block calling the module under test, and ideally also a number of -test assertions to verify that the module outputs match expectations. - -In the same directory where you keep your module's `.tf` and/or `.tf.json` -source files, create a subdirectory called `tests`. Under that directory, -make another directory which will serve as your first test suite, with a -directory name that concisely describes what the suite is aiming to test. - -Here's an example directory structure of a typical module directory layout -with the addition of a test suite called `defaults`: - -``` -main.tf -outputs.tf -providers.tf -variables.tf -versions.tf -tests/ - defaults/ - test_defaults.tf -``` - -The `tests/defaults/test_defaults.tf` file will contain a call to the -main module with a suitable set of arguments and hopefully also one or more -resources that will, for the sake of the experiment, serve as the temporary -syntax for defining test assertions. For example: - -```hcl -terraform { - required_providers { - # Because we're currently using a built-in provider as - # a substitute for dedicated Terraform language syntax - # for now, test suite modules must always declare a - # dependency on this provider. This provider is only - # available when running tests, so you shouldn't use it - # in non-test modules. - test = { - source = "terraform.io/builtin/test" - } - - # This example also uses the "http" data source to - # verify the behavior of the hypothetical running - # service, so we should declare that too. - http = { - source = "hashicorp/http" - } - } -} - -module "main" { - # source is always ../.. for test suite configurations, - # because they are placed two subdirectories deep under - # the main module directory. - source = "../.." - - # This test suite is aiming to test the "defaults" for - # this module, so it doesn't set any input variables - # and just lets their default values be selected instead. -} - -# As with all Terraform modules, we can use local values -# to do any necessary post-processing of the results from -# the module in preparation for writing test assertions. -locals { - # This expression also serves as an implicit assertion - # that the base URL uses URL syntax; the test suite - # will fail if this function fails. - api_url_parts = regex( - "^(?:(?P[^:/?#]+):)?(?://(?P[^/?#]*))?", - module.main.api_url, - ) -} - -# The special test_assertions resource type, which belongs -# to the test provider we required above, is a temporary -# syntax for writing out explicit test assertions. -resource "test_assertions" "api_url" { - # "component" serves as a unique identifier for this - # particular set of assertions in the test results. - component = "api_url" - - # equal and check blocks serve as the test assertions. - # the labels on these blocks are unique identifiers for - # the assertions, to allow more easily tracking changes - # in success between runs. - - equal "scheme" { - description = "default scheme is https" - got = local.api_url_parts.scheme - want = "https" - } - - check "port_number" { - description = "default port number is 8080" - condition = can(regex(":8080$", local.api_url_parts.authority)) - } -} - -# We can also use data resources to respond to the -# behavior of the real remote system, rather than -# just to values within the Terraform configuration. -data "http" "api_response" { - depends_on = [ - # make sure the syntax assertions run first, so - # we'll be sure to see if it was URL syntax errors - # that let to this data resource also failing. - test_assertions.api_url, - ] - - url = module.main.api_url -} - -resource "test_assertions" "api_response" { - component = "api_response" - - check "valid_json" { - description = "base URL responds with valid JSON" - condition = can(jsondecode(data.http.api_response.body)) - } -} -``` - -If you like, you can create additional directories alongside -the `default` directory to define additional test suites that -pass different variable values into the main module, and -then include assertions that verify that the result has changed -in the expected way. - -### Running Your Tests - -The `terraform test` command aims to make it easier to exercise all of your -defined test suites at once, and see only the output related to any test -failures or errors. - -The current experimental incarnation of this command expects to be run from -your main module directory. In our example directory structure above, -that was the directory containing `main.tf` etc, and _not_ the specific test -suite directory containing `test_defaults.tf`. - -Because these test suites are integration tests rather than unit tests, you'll -need to set up any credentials files or environment variables needed by the -providers your module uses before running `terraform test`. The test command -will, for each suite: - -* Install the providers and any external modules the test configuration depends - on. -* Create an execution plan to create the objects declared in the module. -* Apply that execution plan to create the objects in the real remote system. -* Collect all of the test results from the apply step, which would also have - "created" the `test_assertions` resources. -* Destroy all of the objects recorded in the temporary test state, as if running - `terraform destroy` against the test configuration. - -```shellsession -$ terraform test -─── Failed: defaults.api_url.scheme (default scheme is https) ─────────────── -wrong value - got: "http" - want: "https" -───────────────────────────────────────────────────────────────────────────── -``` - -In this case, it seems like the module returned an `http` rather than an -`https` URL in the default case, and so the `defaults.api_url.scheme` -assertion failed, and the `terraform test` command detected and reported it. - -The `test_assertions` resource captures any assertion failures but does not -return an error, because that can then potentially allow downstream -assertions to also run and thus capture as much context as possible. -However, if Terraform encounters any _errors_ while processing the test -configuration it will halt processing, which may cause some of the test -assertions to be skipped. - -## Known Limitations - -The design above is very much a prototype aimed at gathering more experience -with the possibilities of testing inside the Terraform language. We know it's -currently somewhat non-ergonomic, and hope to improve on that in later phases -of research and design, but the main focus of this iteration is on available -functionality and so with that in mind there are some specific possibilities -that we know the current prototype doesn't support well: - -* Testing of subsequent updates to an existing deployment of a module. - Currently tests written in this way can only exercise the create and destroy - behaviors. - -* Assertions about expected errors. For a module that includes variable - validation rules and data resources that function as assertion checks, - the current prototype doesn't have any way to express that a particular - set of inputs is _expected_ to produce an error, and thus report a test - failure if it doesn't. We'll hopefully be able to improve on this in a future - iteration with the test assertions better integrated into the language. - -* Capturing context about failures. Due to this prototype using a provider as - an approximation for new assertion syntax, the `terraform test` command is - limited in how much context it's able to gather about failures. A design - more integrated into the language could potentially capture the source - expressions and input values to give better feedback about what went wrong, - similar to what Terraform typically returns from expression evaluation errors - in the main language. - -* Unit testing without creating real objects. Although we do hope to spend more - time researching possibilities for unit testing against fake test doubles in - the future, we've decided to focus on integration testing to start because - it feels like the better-defined problem. - -## Sending Feedback - -The sort of feedback we'd most like to see at this stage of the experiment is -to see the source code of any tests you've written against real modules using -the features described above, along with notes about anything that you -attempted to test but were blocked from doing so by limitations of the above -features. The most ideal way to share that would be to share a link to a -version control branch where you've added such tests, if your module is open -source. - -If you've previously written or attempted to write tests in an external -language, using a system like Terratest or kitchen-terraform, we'd also be -interested to hear about comparative differences between the two: what worked -well in each and what didn't work so well. - -Our ultimate goal is to work towards an integration testing methodology which -strikes the best compromise between the capabilities of these different -approaches, ideally avoiding a hard requirement on any particular external -language and fitting well into the Terraform workflow. - -Since this is still early work and likely to lead to unstructured discussion, -we'd like to gather feedback primarily via new topics in -[the community forum](https://discuss.hashicorp.com/c/terraform-core/27). That -way we can have some more freedom to explore different ideas and approaches -without the structural requirements we typically impose on GitHub issues. - -Any feedback you'd like to share would be very welcome! diff --git a/website/docs/language/moved.mdx b/website/docs/language/moved.mdx new file mode 100644 index 0000000000..b5dbc79ab5 --- /dev/null +++ b/website/docs/language/moved.mdx @@ -0,0 +1,53 @@ +--- +page_title: moved block configuration reference +description: Learn about the `moved` block that you can specify in Terraform configurations. The `moved` block programmatically changes the location of a resource. +--- + +# `moved` block reference + +This topic provides reference information for the `moved` block. Use this block to programmatically change the address of a resource. Refer to [Refactoring](/terraform/language/modules/develop/refactoring) for details about how to use the `moved` block in your Terraform configurations. + +## Configuration model + +The `moved` block supports the following arguments: + +- [`moved`](#moved):   block + - [`from`](#moved):   reference | required + - [`to`](#moved):   reference | required + +## Complete configuration + +The following `moved` block defines all of the supported built-in arguments you can set: + +```hcl +moved { + from = + to = +} +``` + +## Specification + +A `moved` block supports the following configuration. + +### `moved` + +The `moved` block specifies the previous address and the new address for the resource. The following table describes the arguments you can set in the `moved` block. + +| Argument | Description | Type | Required | +| --- | --- | --- | --- | +| `from` | Specifies a resource's previous address. The syntax allows Terraform to select modules, resources, and resources inside child modules. | string | required | +| `to` | Specifies the new address to relocate the resource to. The syntax allows Terraform to select modules, resources, and resources inside child modules. | string | required | + +Before creating a new plan for the resource specified in the `to` field, Terraform checks the state for an existing object at the address specified in the `from` field. Terraform renames existing objects to the string specified in the `to` field and then creates a plan. The plan directs Terraform to provision the resource specified in the `from` field as the resource specified in the `to` field. As a result, Terraform does not destroy the resource during the Terraform run. + +## Example + +The following example moves an AWS instance from address `aws_instance.a` to `aws_instance.b`: + +```hcl +moved { + from = aws_instance.a + to = aws_instance.b +} +``` \ No newline at end of file diff --git a/website/docs/language/providers/configuration.mdx b/website/docs/language/providers/configuration.mdx index 4513338fb0..f3cce651ed 100644 --- a/website/docs/language/providers/configuration.mdx +++ b/website/docs/language/providers/configuration.mdx @@ -16,7 +16,7 @@ configure settings for providers. Additionally, all Terraform configurations must declare which providers they require so that Terraform can install and use them. The -[Provider Requirements](/language/providers/requirements) +[Provider Requirements](/terraform/language/providers/requirements) page documents how to declare providers so Terraform can install them. ## Provider Configuration @@ -24,8 +24,8 @@ page documents how to declare providers so Terraform can install them. Provider configurations belong in the root module of a Terraform configuration. (Child modules receive their provider configurations from the root module; for more information, see -[The Module `providers` Meta-Argument](/language/meta-arguments/module-providers) -and [Module Development: Providers Within Modules](/language/modules/develop/providers).) +[The Module `providers` Meta-Argument](/terraform/language/meta-arguments/module-providers) +and [Module Development: Providers Within Modules](/terraform/language/modules/develop/providers).) A provider configuration is created using a `provider` block: @@ -37,7 +37,7 @@ provider "google" { ``` The name given in the block header (`"google"` in this example) is the -[local name](/language/providers/requirements#local-names) of the provider to +[local name](/terraform/language/providers/requirements#local-names) of the provider to configure. This provider should already be included in a `required_providers` block. @@ -46,7 +46,7 @@ the provider. Most arguments in this section are defined by the provider itself; in this example both `project` and `region` are specific to the `google` provider. -You can use [expressions](/language/expressions) in the values of these +You can use [expressions](/terraform/language/expressions) in the values of these configuration arguments, but can only reference values that are known before the configuration is applied. This means you can safely reference input variables, but not attributes exported by resources (with an exception for resource @@ -68,7 +68,7 @@ and available for all `provider` blocks: - [`alias`, for using the same provider with different configurations for different resources][inpage-alias] - [`version`, which we no longer recommend][inpage-versions] (use - [provider requirements](/language/providers/requirements) instead) + [provider requirements](/terraform/language/providers/requirements) instead) Unlike many other objects in the Terraform language, a `provider` block may be omitted if its contents would otherwise be empty. Terraform assumes an @@ -175,7 +175,7 @@ module "aws_vpc" { ``` Modules have some special requirements when passing in providers; see -[The Module `providers` Meta-Argument](/language/meta-arguments/module-providers) +[The Module `providers` Meta-Argument](/terraform/language/meta-arguments/module-providers) for more details. In most cases, only _root modules_ should define provider configurations, with all child modules obtaining their provider configurations from their parents. @@ -188,11 +188,11 @@ from their parents. The `version` meta-argument specifies a version constraint for a provider, and works the same way as the `version` argument in a -[`required_providers` block](/language/providers/requirements). The version +[`required_providers` block](/terraform/language/providers/requirements). The version constraint in a provider configuration is only used if `required_providers` does not include one for that provider. -**The `version` argument in provider configurations is deprecated.** +~**Warning:** The `version` argument in provider configurations is deprecated, and we will remove it in a future Terraform version. + In Terraform 0.13 and later, always declare provider version constraints in -[the `required_providers` block](/language/providers/requirements). The `version` -argument will be removed in a future version of Terraform. +[the `required_providers` block](/terraform/language/providers/requirements). diff --git a/website/docs/language/providers/index.mdx b/website/docs/language/providers/index.mdx index 6dab350864..49b68ad6b6 100644 --- a/website/docs/language/providers/index.mdx +++ b/website/docs/language/providers/index.mdx @@ -7,9 +7,9 @@ description: >- # Providers -> **Hands-on:** Try the [Perform CRUD Operations with Providers](https://learn.hashicorp.com/tutorials/terraform/provider-use?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Perform CRUD Operations with Providers](/terraform/tutorials/configuration-language/provider-use?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. -Terraform relies on plugins called "providers" to interact with cloud providers, +Terraform relies on plugins called providers to interact with cloud providers, SaaS providers, and other APIs. Terraform configurations must declare which providers they require so that @@ -18,8 +18,8 @@ configuration (like endpoint URLs or cloud regions) before they can be used. ## What Providers Do -Each provider adds a set of [resource types](/language/resources) -and/or [data sources](/language/data-sources) that Terraform can +Each provider adds a set of [resource types](/terraform/language/resources) +and/or [data sources](/terraform/language/data-sources) that Terraform can manage. Every resource type is implemented by a provider; without providers, Terraform @@ -51,45 +51,68 @@ Provider documentation in the Registry is versioned; you can use the version menu in the header to change which version you're viewing. For details about writing, generating, and previewing provider documentation, -see the [provider publishing documentation](/registry/providers/docs). +see the [provider publishing documentation](/terraform/registry/providers/docs). ## How to Use Providers +Providers are released separately from Terraform itself and have their own version numbers. In production we recommend constraining the acceptable provider versions in the configuration's provider requirements block, to make sure that `terraform init` does not install newer versions of the provider that are incompatible with the configuration. + To use resources from a given provider, you need to include some information about it in your configuration. See the following pages for details: -- [Provider Requirements](/language/providers/requirements) +- [Provider Requirements](/terraform/language/providers/requirements) documents how to declare providers so Terraform can install them. -- [Provider Configuration](/language/providers/configuration) +- [Provider Configuration](/terraform/language/providers/configuration) documents how to configure settings for providers. -- [Dependency Lock File](/language/files/dependency-lock) +- [Dependency Lock File](/terraform/language/files/dependency-lock) documents an additional HCL file that can be included with a configuration, which tells Terraform to always use a specific set of provider versions. ## Provider Installation -- Terraform Cloud and Terraform Enterprise install providers as part of every run. +- HCP Terraform and Terraform Enterprise install providers as part of every run. - Terraform CLI finds and installs providers when - [initializing a working directory](/cli/init). It can + [initializing a working directory](/terraform/cli/init). It can automatically download providers from a Terraform registry, or load them from a local mirror or cache. If you are using a persistent working directory, you must reinitialize whenever you change a configuration's providers. To save time and bandwidth, Terraform CLI supports an optional plugin cache. You can enable the cache using the `plugin_cache_dir` setting in - [the CLI configuration file](/cli/config/config-file). + [the CLI configuration file](/terraform/cli/config/config-file). To ensure Terraform always installs the same provider versions for a given configuration, you can use Terraform CLI to create a -[dependency lock file](/language/files/dependency-lock) +[dependency lock file](/terraform/language/files/dependency-lock) and commit it to version control along with your configuration. If a lock file -is present, Terraform Cloud, CLI, and Enterprise will all obey it when +is present, HCP Terraform, CLI, and Enterprise will all obey it when installing providers. -> **Hands-on:** Try the [Lock and Upgrade Provider Versions](https://learn.hashicorp.com/tutorials/terraform/provider-versioning?in=terraform/configuration-language&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Lock and Upgrade Provider Versions](/terraform/tutorials/configuration-language/provider-versioning?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. + +### Private Providers + +If you are using a provider that is not in a Hashicorp-hosted registry, you may +need to attach additional credentials to your requests to external registries. +You do not need these credentials if your provider is in the Terraform public +registry or the HCP Terraform private registry. + +By default, Terraform only authenticates the opening request from a provider to +the registry. The registry responds with +[follow-up URLs](/terraform/internals/provider-registry-protocol#find-a-provider-package) +that Terraform makes requests to, such as telling Terraform to download the +provider or the `SHASUMS` file. Hashicorp-hosted registries do not require +additional authentication for these follow-up requests. If your registry does +require additional credentials for follow-up requests, you can use a `.netrc` +file to provide those credentials. + +By default, Terraform searches for the `.netrc` file in your `HOME` directory. +However, you can override the default filesystem location by setting the `NETRC` +environment variable. For information on the format of`.netrc`, refer to the +[`curl` documentation](https://everything.curl.dev/usingcurl/netrc). ## How to Find Providers @@ -109,6 +132,5 @@ develops and maintains a given provider. Providers are written in Go, using the Terraform Plugin SDK. For more information on developing providers, see: -- The [Plugin Development](/plugin) documentation -- The [Call APIs with Terraform Providers](https://learn.hashicorp.com/collections/terraform/providers?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) - collection on HashiCorp Learn +- The [Plugin Development](/terraform/plugin) documentation +- The [Call APIs with Terraform Providers](/terraform/tutorials/providers-plugin-framework?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials diff --git a/website/docs/language/providers/requirements.mdx b/website/docs/language/providers/requirements.mdx index f83d930399..171aa9294d 100644 --- a/website/docs/language/providers/requirements.mdx +++ b/website/docs/language/providers/requirements.mdx @@ -12,11 +12,9 @@ Terraform configurations must declare which providers they require, so that Terraform can install and use them. This page documents how to declare providers so Terraform can install them. -> **Hands-on:** Try the [Perform CRUD Operations with Providers](https://learn.hashicorp.com/tutorials/terraform/provider-use) tutorial on HashiCorp Learn. - Additionally, some providers require configuration (like endpoint URLs or cloud regions) before they can be used. The [Provider -Configuration](/language/providers/configuration) page documents how +Configuration](/terraform/language/providers/configuration) page documents how to configure settings for providers. -> **Note:** This page is about a feature of Terraform 0.13 and later; it also @@ -44,7 +42,7 @@ terraform { ``` The `required_providers` block must be nested inside the top-level -[`terraform` block](/language/settings) (which can also contain other settings). +[`terraform` block](/terraform/language/terraform) (which can also contain other settings). Each argument in the `required_providers` block enables one provider. The key determines the provider's [local name](#local-names) (its unique identifier @@ -81,7 +79,7 @@ Local names must be unique per-module. Outside of the `required_providers` block, Terraform configurations always refer to providers by their local names. For example, the following configuration declares `mycloud` as the local name for `mycorp/mycloud`, then uses that local -name when [configuring the provider](/language/providers/configuration): +name when [configuring the provider](/terraform/language/providers/configuration): ```hcl terraform { @@ -119,13 +117,17 @@ follows: `[/]/` +Examples of valid provider source address formats include: +- `NAMESPACE/TYPE` +- `HOSTNAME/NAMESPACE/TYPE` + * **Hostname** (optional): The hostname of the Terraform registry that distributes the provider. If omitted, this defaults to `registry.terraform.io`, the hostname of [the public Terraform Registry](https://registry.terraform.io/). * **Namespace:** An organizational namespace within the specified registry. - For the public Terraform Registry and for Terraform Cloud's private registry, + For the public Terraform Registry and for HCP Terraform's private registry, this represents the organization that publishes the provider. This field may have other meanings for other registry hosts. @@ -210,7 +212,7 @@ avoiding typing. Each provider plugin has its own set of available versions, allowing the functionality of the provider to evolve over time. Each provider dependency you -declare should have a [version constraint](/language/expressions/version-constraints) given in +declare should have a [version constraint](/terraform/language/expressions/version-constraints) given in the `version` argument so Terraform can select a single version per provider that all modules are compatible with. @@ -220,12 +222,12 @@ a version constraint for every provider your module depends on. To ensure Terraform always installs the same provider versions for a given configuration, you can use Terraform CLI to create a -[dependency lock file](/language/files/dependency-lock) +[dependency lock file](/terraform/language/files/dependency-lock) and commit it to version control along with your configuration. If a lock file -is present, Terraform Cloud, CLI, and Enterprise will all obey it when +is present, HCP Terraform, CLI, and Enterprise will all obey it when installing providers. -> **Hands-on:** Try the [Lock and Upgrade Provider Versions](https://learn.hashicorp.com/tutorials/terraform/provider-versioning) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Lock and Upgrade Provider Versions](/terraform/tutorials/configuration-language/provider-versioning) tutorial. ### Best Practices for Provider Versions @@ -248,7 +250,7 @@ directory where you'd run `terraform apply` — should also specify the _maximum_ provider version it is intended to work with, to avoid accidental upgrades to incompatible new versions. The `~>` operator is a convenient shorthand for allowing the rightmost component of a version to increment. The -following example uses the operator to allow only patch releases within a +following example uses the operator to allow only patch releases within a specific minor release: ```hcl @@ -271,12 +273,11 @@ incompatibilities, and let the root module manage the maximum version. ## Built-in Providers -While most Terraform providers are distributed separately as plugins, there -is currently one provider that is built in to Terraform itself, which -provides -[the `terraform_remote_state` data source](/language/state/remote-state-data). +Most Terraform providers are distributed separately as plugins, but there +is one provider that is built into Terraform itself. This provider enables +[the `terraform_remote_state` data source](/terraform/language/state/remote-state-data). -Because this provider is built in to Terraform, you don't need to declare it +Because this provider is built into Terraform, you don't need to declare it in the `required_providers` block in order to use its features. However, for consistency it _does_ have a special provider source address, which is `terraform.io/builtin/terraform`. This address may sometimes appear in @@ -293,9 +294,8 @@ compatible with Terraform v0.11 or later and should never be declared in a ## In-house Providers Anyone can develop and distribute their own Terraform providers. See -the [Call APIs with Terraform Providers](https://learn.hashicorp.com/collections/terraform/providers) -collection on HashiCorp Learn for more -about provider development. +the [Call APIs with Terraform Providers](/terraform/tutorials/providers) +tutorials for more about provider development. Some organizations develop their own providers to configure proprietary systems, and wish to use these providers from Terraform without @@ -303,11 +303,11 @@ publishing them on the public Terraform Registry. One option for distributing such a provider is to run an in-house _private_ registry, by implementing -[the provider registry protocol](/internals/provider-registry-protocol). +[the provider registry protocol](/terraform/internals/provider-registry-protocol). Running an additional service just to distribute a single provider internally may be undesirable, so Terraform also supports -[other provider installation methods](/cli/config/config-file#provider-installation), +[other provider installation methods](/terraform/cli/config/config-file#provider-installation), including placing provider plugins directly in specific directories in the local filesystem, via _filesystem mirrors_. @@ -336,7 +336,7 @@ terraform { To make version 1.0.0 of this provider available for installation from the local filesystem, choose one of the -[implied local mirror directories](/cli/config/config-file#implied-local-mirror-directories) +[implied local mirror directories](/terraform/cli/config/config-file#implied-local-mirror-directories) and create a directory structure under it like this: ``` diff --git a/website/docs/language/resources/behavior.mdx b/website/docs/language/resources/behavior.mdx index 8f3f91f1db..c65de9d1f4 100644 --- a/website/docs/language/resources/behavior.mdx +++ b/website/docs/language/resources/behavior.mdx @@ -20,7 +20,7 @@ match the configuration. When Terraform creates a new infrastructure object represented by a `resource` block, the identifier for that real object is saved in Terraform's -[state](/language/state), allowing it to be updated and destroyed +[state](/terraform/language/state), allowing it to be updated and destroyed in response to future changes. For resource blocks that already have an associated infrastructure object in the state, Terraform compares the actual configuration of the object with the arguments given in the @@ -44,7 +44,7 @@ customized on a per-resource basis. ## Accessing Resource Attributes -[Expressions](/language/expressions) within a Terraform module can access +[Expressions](/terraform/language/expressions) within a Terraform module can access information about resources in the same module, and you can use that information to help configure other resources. Use the `..` syntax to reference a resource attribute in an expression. @@ -54,7 +54,7 @@ read-only attributes with information obtained from the remote API; this often includes things that can't be known until the resource is created, like the resource's unique random ID. -Many providers also include [data sources](/language/data-sources), +Many providers also include [data sources](/terraform/language/data-sources), which are a special type of resource used only for looking up information. For a list of the attributes a resource or data source type provides, consult @@ -62,7 +62,7 @@ its documentation; these are generally included in a second list below its list of configurable arguments. For more information about referencing resource attributes in expressions, see -[Expressions: References to Resource Attributes](/language/expressions/references#references-to-resource-attributes). +[Expressions: References to Resource Attributes](/terraform/language/expressions/references#references-to-resource-attributes). ## Resource Dependencies @@ -75,7 +75,7 @@ resource's configuration just requires information generated by another resource. Most resource dependencies are handled automatically. Terraform analyses any -[expressions](/language/expressions) within a `resource` block to find references +[expressions](/terraform/language/expressions) within a `resource` block to find references to other objects, and treats those references as implicit ordering requirements when creating, updating, or destroying resources. Since most resources with behavioral dependencies on other resources also refer to those resources' data, @@ -86,10 +86,10 @@ example, if Terraform must manage access control policies _and_ take actions that require those policies to be present, there is a hidden dependency between the access policy and a resource whose creation depends on it. In these rare cases, -[the `depends_on` meta-argument](/language/meta-arguments/depends_on) +[the `depends_on` meta-argument](/terraform/language/meta-arguments/depends_on) can explicitly specify a dependency. -You can also use the [`replace_triggered_by` meta-argument](/language/meta-arguments/lifecycle#replace_triggered_by) to add dependencies between otherwise independent resources. It forces Terraform to replace the parent resource when there is a change to a referenced resource or resource attribute. +You can also use the [`replace_triggered_by` meta-argument](/terraform/language/meta-arguments/lifecycle#replace_triggered_by) to add dependencies between otherwise independent resources. It forces Terraform to replace the parent resource when there is a change to a referenced resource or resource attribute. ## Local-only Resources diff --git a/website/docs/language/resources/ephemeral/index.mdx b/website/docs/language/resources/ephemeral/index.mdx new file mode 100644 index 0000000000..687d613f00 --- /dev/null +++ b/website/docs/language/resources/ephemeral/index.mdx @@ -0,0 +1,81 @@ +--- +page_title: Ephemeral resources +description: Learn how to keep sensitive resource data out of state and plan files in Terraform with ephemeral resource blocks and write-only arguments. +--- + +# Ephemerality in resources + +Managing infrastructure often requires creating and handling sensitive values that you may not want Terraform to persist outside of the current operation. Terraform provides two tools for resources to manage data you do not want to store in state or plan files: the `ephemeral` resource block and ephemeral write-only arguments on specific resources. + +## Ephemeral resources + +Ephemeral resources are Terraform resources that are essentially temporary. Ephemeral resources have a unique lifecycle, and Terraform does not store information about ephemeral resources in state or plan files. Each `ephemeral` block describes one or more ephemeral resources, such as a temporary password or connection to another system. + +In your configuration, you can only reference an `ephemeral` block in [other ephemeral contexts](/terraform/language/resources/ephemeral/reference#reference-ephemeral-resources). + +### Lifecycle + +The lifecycle of an `ephemeral` resource is different from other resources and data sources. When Terraform provisions ephemeral resources, it performs the following steps: + +1. If Terraform needs to access the result of an ephemeral resource, it opens +that ephemeral resource. For example, if Terraform opens an ephemeral resource for a Vault secret, the Vault provider obtains a lease and returns a secret. + +1. If Terraform needs access to the ephemeral resource for longer than the +remote system's enforced expiration time, Terraform asks the provider +to periodically renew it. For example, if Terraform renews a Vault secret `ephemeral` resource, the Vault provider calls Vault's lease renewal API endpoint to extend the expiration time. + +1. Once Terraform no longer needs an ephemeral resource, Terraform closes +it. This happens after the providers that depend on an ephemeral resource +complete all of their work for the current Terraform run phase. For example, closing a Vault secret ephemeral resource means the Vault provider explicitly ends the lease, allowing Vault to immediately revoke the associated credentials. + +Terraform follows these lifecycle steps for each instance of an ephemeral +resource in a given configuration. + +### Configuration model + +To learn more about the `ephemeral` resource block, refer to the [Ephemeral resource reference](/terraform/language/resources/ephemeral/reference). + +## Write-only arguments + +Terraform's managed resources, defined by `resource` blocks, can include ephemeral arguments, called **write-only arguments**. Write-only arguments are only available during the current Terraform operation, and Terraform does not store them in state or plan files. + +Use write-only arguments to securely pass temporary values to resources during a Terraform operation without worrying about Terraform persisting those values. For example, the `aws_db_instance` resource has a write-only `password_wo` argument that accepts a database password: + + + +```hcl +ephemeral "random_password" "db_password" { + length = 16 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret" "db_password" { + name = "db_password" +} + +resource "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret.db_password.id + secret_string_wo = ephemeral.random_password.db_password.result + secret_string_wo_version = 1 +} + +ephemeral "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret_version.db_password.secret_id +} + +resource "aws_db_instance" "example" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + skip_final_snapshot = true + password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string + password_wo_version = aws_secretsmanager_secret_version.db_password.secret_string_wo_version +} +``` + + + +When Terraform creates the `aws_db_instance` resource, Terraform sends the `password_wo` value to the `aws` provider. The `aws` provider then uses the `password_wo` value to create the database instance, and then Terraform discards the password value without ever storing it. + +To learn more about this example and using write-only arguments, refer to the [Use write-only arguments](/terraform/language/resources/ephemeral/write-only). \ No newline at end of file diff --git a/website/docs/language/resources/ephemeral/reference.mdx b/website/docs/language/resources/ephemeral/reference.mdx new file mode 100644 index 0000000000..8c2324c854 --- /dev/null +++ b/website/docs/language/resources/ephemeral/reference.mdx @@ -0,0 +1,95 @@ +--- +page_title: Ephemeral block configuration reference +description: Learn to define ephemeral blocks in Terraform configurations to keep temporary and sensitive information out of Terraform state and plan files. +--- + +# Ephemeral block configuration reference + +This topic provides reference information for the `ephemeral` block. + +-> **Note**: Ephemeral resources are available in Terraform v1.10 and later. + +## Introduction + +Ephemeral resources are Terraform resources that are essentially temporary. Ephemeral resources have a unique lifecycle, and Terraform does not store them in its state. Each `ephemeral` block describes one or more ephemeral resources, such as a temporary password or connection to another system. + +## Dependency graph + +Ephemeral resources form nodes in Terraform's dependency graph, which interact similarly as resources and data sources. For example, when a resource or data source depends on an attribute of an ephemeral resource, Terraform automatically provisions the ephemeral resource first. + +## Defer provisioning + +If an input argument of an ephemeral resource references a value that Terraform does not know yet, but can learn during or after a plan, Terraform defers executing that resource until the apply stage. Deferring execution lets Terraform ensure it has all of the information it needs to properly provision the ephemeral resource. + +## Configuration model + +An `ephemeral` block declares an ephemeral resource of a specific type with a +specific local name, much like a `resource` block. Terraform uses an ephemeral resource's name to refer to that resource in the same module, but an ephemeral resource's name has no meaning outside that module's scope. + +Most of the arguments within the body of an `ephemeral` block are specific to the ephemeral resource you are defining. As with resources and data sources, each provider in the [Terraform Registry](https://registry.terraform.io/browse/providers) includes documentation for the ephemeral resources it supports, if any. An ephemeral resource type's documentation lists which arguments are available and how you should format your resource's values. + +The following list outlines general field hierarchy, language-specific data types, and requirements in the `ephemeral` block. + +- [`ephemeral`](#ephemeral): map + - [`attributes`](#ephemeral) + - [`meta-arguments`](#ephemeral) + +## Complete configuration + +An `ephemeral` block has the following form: + +```hcl +ephemeral "" "" { + + +} +``` + +## Reference ephemeral resources + +You can only reference ephemeral resources in specific ephemeral contexts or +Terraform throws an error. The following are valid contexts for referencing +ephemeral resources: + +* In a [write-only argument](/terraform/language/resources/ephemeral/write-only) +* In another ephemeral resource +* In [local values](/terraform/language/values/locals#ephemeral-values) +* In [ephemeral variables](/terraform/language/values/variables#exclude-values-from-state) +* In [ephemeral outputs](/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files) +* Configuring providers in the `provider` block +* In [provisioner](/terraform/language/resources/provisioners/syntax) and [connection](/terraform/language/resources/provisioners/connection) blocks + +## Meta-arguments + +You can use the following meta-arguments with ephemeral resources to change the behavior of those resources. The following meta-arguments work the same way for resources, data sources, and ephemeral +resources: + +- [`depends_on`, for specifying hidden dependencies](/terraform/language/meta-arguments/depends_on) +- [`count`, for creating multiple resource instances according to a count](/terraform/language/meta-arguments/count) +- [`for_each`, to create multiple instances according to a map or set of strings](/terraform/language/meta-arguments/for_each) +- [`provider`, for selecting a non-default provider configuration](/terraform/language/meta-arguments/resource-provider) +- [`lifecycle`, for lifecycle customizations](/terraform/language/meta-arguments/lifecycle) + +Ephemeral resources do not support the `provisioner` meta-argument. + +## Example + +The following example configures the `postgresql` provider with credentials from +an ephemeral resource. Since these credentials are managed by an ephemeral resource, Terraform does not store them in your state or plan files. + +```hcl +ephemeral "aws_secretsmanager_secret_version" "db_master" { + secret_id = aws_secretsmanager_secret_version.db_password.secret_id +} + +locals { + credentials = jsondecode(ephemeral.aws_secretsmanager_secret_version.db_master.secret_string) +} + +provider "postgresql" { + host = aws_db_instance.example.address + port = aws_db_instance.example.port + username = local.credentials["username"] + password = local.credentials["password"] +} +``` diff --git a/website/docs/language/resources/ephemeral/write-only.mdx b/website/docs/language/resources/ephemeral/write-only.mdx new file mode 100644 index 0000000000..730af8abd8 --- /dev/null +++ b/website/docs/language/resources/ephemeral/write-only.mdx @@ -0,0 +1,250 @@ +--- +page_title: Use write-only arguments +description: Learn how to use write-only arguments to set temporary values that are not stored in Terraform's state or plan files. +--- + +# Use write-only arguments + +Write-only arguments let you securely pass temporary values to Terraform's managed resources during an operation without persisting those values to state or plan files. Use write-only arguments to handle sensitive data such as passwords, API tokens, and other secrets. + +## Background + +Write-only arguments complement [other ephemeral values](/terraform/language/resources/ephemeral/reference#reference-ephemeral-resources) in Terraform, letting you securely pass sensitive data throughout your configuration without ever storing it in Terraform's artifacts. For example, you can generate a random password using an `ephemeral` resource then pass it to a write-only argument on another `resource` block. The provider uses the write-only argument value to configure the resource, then Terraform discards the value without storing it. + +> **Hands-on**: Declare a write-only argument in the [Upgrade RDS major version](/terraform/tutorials/aws/rds-upgrade) tutorial. + +Unlike other ephemeral constructs in Terraform, such as ephemeral resources or variables, write-only arguments accept both ephemeral and non-ephemeral values. + +## Requirements + +To use write-only arguments, you must use Terraform v.1.11 or later and use a resource that supports write-only arguments. + +## Declare a write-only argument + +Providers indicate in the Terraform registry whether an argument is write-only. For example, the `aws` provider's `aws_db_instance` resource has a write-only `password_wo` argument. The `password_wo` argument accepts a value to use as the database password: + + + +```hcl +resource "aws_db_instance" "test" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + skip_final_snapshot = true + password_wo = + password_wo_version = 1 +} +``` + + + +Write-only arguments accept both ephemeral and non-ephemeral values. For example, you could also use a string as the value of a write-only argument: + +```hcl +resource "aws_db_instance" "test" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + skip_final_snapshot = true + password_wo = "my-password-here" + password_wo_version = 1 +} +``` + +However, we recommend using write-only arguments for passing ephemeral values to resources. For example, you can use an `ephemeral` resource to generate a random password and pass it to the `password_wo` write-only argument: + + + +```hcl +ephemeral "random_password" "db_password" { + length = 16 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_db_instance" "example" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + skip_final_snapshot = true + password_wo = ephemeral.random_password.db_password.result + password_wo_version = 1 +} +``` + + + +During a Terraform operation, the provider uses the `password_wo` value to create the database instance, and then Terraform discards that value without storing it in the plan or state file. + +Note that Terraform does not store the generated value for `password_wo`, but you can capture it in another resource or output. For an example of generating, storing, retrieving, and using an ephemeral password as a write-only argument, refer to the [Examples](#examples). + +## Update write-only arguments with versions + +Terraform does not store write-only arguments in state files, so Terraform has no way of knowing if a write-only argument value has changed. Because Terraform cannot track write-only argument values, it sends write-only arguments to the provider during every operation. + +Terraform also cannot create plan diffs for write-only arguments because it does not store those values in plan files. However, providers typically include version arguments alongside write-only arguments. Terraform stores version arguments in state, and can track if a version argument changes. + +Providers implement version arguments to let practitioners track write-only argument values and control when a provider uses those write-only arguments. The implementation of write-only arguments and their version arguments is provider-specific, so consult the Registry for more details about your specific provider. + +For example, the `aws_db_instance` resource has an accompanying `password_wo_version` argument for the `password_wo` write-only argument: + + + +```hcl +resource "aws_db_instance" "test" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + skip_final_snapshot = true + password_wo = "old-password-here" + password_wo_version = 1 +} +``` + + + +The provider uses the write-only argument value when creating the `aws_db_instance` resource and Terraform stores the `password_wo_version` argument value in state. + +To trigger an update of a write-only argument, increment the version argument's value in your configuration: + +```hcl +resource "aws_db_instance" "main" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + password_wo = "new-password-here" + password_wo_version = 2 +} +``` + +When you increment the `password_wo_version` argument, Terraform notices that change in its plan and notifies the `aws` provider. The `aws` provider then uses the new `password_wo` value to update the `aws_db_instance` resource. + + +## Examples + +The following demonstrates how to use write-only arguments with different cloud providers. + +### Set and store an ephemeral password in AWS Secrets Manager + +You can use an `ephemeral` resource to generate a random password, store it in AWS Secrets Manager, and then retrieve it using another `ephemeral` resource. Finally, you can pass the password to the `password_wo` write-only argument of the `aws_db_instance` resource: + + + +```hcl +ephemeral "random_password" "db_password" { + length = 16 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +resource "aws_secretsmanager_secret" "db_password" { + name = "db_password" +} + +resource "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret.db_password.id + secret_string_wo = ephemeral.random_password.db_password.result + secret_string_wo_version = 1 +} + +ephemeral "aws_secretsmanager_secret_version" "db_password" { + secret_id = aws_secretsmanager_secret_version.db_password.secret_id +} + +resource "aws_db_instance" "example" { + instance_class = "db.t3.micro" + allocated_storage = "5" + engine = "postgres" + username = "example" + skip_final_snapshot = true + password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string + password_wo_version = aws_secretsmanager_secret_version.db_password.secret_string_wo_version +} +``` + + + +In the above example, the ephemeral resource `aws_secretsmanager_secret_version` references an argument that Terraform initially does not know. Terraform defers executing `aws_secretsmanager_secret_version` until the apply stage, to ensure that Terraform evaluates the resource after it has the information it needs. + +Terraform first creates the secret in AWS Secrets Manager using the ephemeral `random_password`, then retrieve it using the ephemeral `aws_secretsmanager_secret_version` resource, and finally write the password to the write-only `password_wo` argument of the `aws_db_instance` resource. + +### Set and store an ephemeral password in Azure Key Vault + +You can use a write-only argument to store a password in Azure's Key Vault, then use that password to create a MySQL database in Azure. In the following example, Terraform generates an password using an `ephemeral` resource, stores that password in a `azurerm_key_vault_secret`, then retrieves it in the `azurerm_mysql_flexible_server` resource: + +```hcl +provider "azurerm" { + features {} +} + +ephemeral "random_password" "db_password" { + length = 16 + override_special = "!#$%&*()-_=+[]{}<>:?" +} + +locals { + db_password_version = 1 +} + +resource "azurerm_resource_group" "example" { + name = "example-resource-group" + location = "westeurope" +} + +data "azurerm_client_config" "current" {} + +resource "azurerm_key_vault" "example" { + name = "example-key-vault" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + + access_policy { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = data.azurerm_client_config.current.object_id + + key_permissions = [ + "Get", + ] + + secret_permissions = [ + "Get", + "Delete", + "List", + "Purge", + "Recover", + "Set", + ] + } +} + +resource "azurerm_key_vault_secret" "example" { + name = "example-secret" + value_wo = ephemeral.random_password.db_password.result + value_wo_version = local.db_password_version + key_vault_id = azurerm_key_vault.example.id +} + +ephemeral "azurerm_key_vault_secret" "db_password" { + name = azurerm_key_vault_secret.example.name + key_vault_id = azurerm_key_vault.example.id +} + +resource "azurerm_mysql_flexible_server" "example" { + name = "example-mysql-flexible-server" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + sku_name = "B_Standard_B1s" + + administrator_login = "newuser" + administrator_password_wo = ephemeral.azurerm_key_vault_secret.db_password.value + administrator_password_wo_version = local.db_password_version +} +``` + +The above configuration stores your password in Azure's Key Vault and uses it to create a database in Azure without ever storing that password in a Terraform artifact. \ No newline at end of file diff --git a/website/docs/language/resources/index.mdx b/website/docs/language/resources/index.mdx index 79df607b1f..0c3d230f91 100644 --- a/website/docs/language/resources/index.mdx +++ b/website/docs/language/resources/index.mdx @@ -7,29 +7,29 @@ description: >- # Resources -> **Hands-on:** Try the [Terraform: Get Started](https://learn.hashicorp.com/collections/terraform/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Terraform: Get Started](/terraform/tutorials/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. _Resources_ are the most important element in the Terraform language. Each resource block describes one or more infrastructure objects, such as virtual networks, compute instances, or higher-level components such as DNS records. -- [Resource Blocks](/language/resources/syntax) documents +- [Resource Blocks](/terraform/language/resources/syntax) documents the syntax for declaring resources. -- [Resource Behavior](/language/resources/behavior) explains in +- [Resource Behavior](/terraform/language/resources/behavior) explains in more detail how Terraform handles resource declarations when applying a configuration. - The Meta-Arguments section documents special arguments that can be used with every resource type, including - [`depends_on`](/language/meta-arguments/depends_on), - [`count`](/language/meta-arguments/count), - [`for_each`](/language/meta-arguments/for_each), - [`provider`](/language/meta-arguments/resource-provider), - and [`lifecycle`](/language/meta-arguments/lifecycle). + [`depends_on`](/terraform/language/meta-arguments/depends_on), + [`count`](/terraform/language/meta-arguments/count), + [`for_each`](/terraform/language/meta-arguments/for_each), + [`provider`](/terraform/language/meta-arguments/resource-provider), + and [`lifecycle`](/terraform/language/meta-arguments/lifecycle). -- [Provisioners](/language/resources/provisioners/syntax) +- [Provisioners](/terraform/language/resources/provisioners/syntax) documents configuring post-creation actions for a resource using the `provisioner` and `connection` blocks. Since provisioners are non-declarative and potentially unpredictable, we strongly recommend that you treat them as a diff --git a/website/docs/language/resources/provisioners/chef.mdx b/website/docs/language/resources/provisioners/chef.mdx deleted file mode 100644 index cf1a2b2a5a..0000000000 --- a/website/docs/language/resources/provisioners/chef.mdx +++ /dev/null @@ -1,181 +0,0 @@ ---- -page_title: 'Provisioner: chef' -description: >- - The `chef` provisioner installs, configures and runs the Chef client on a - resource. ---- - -# Chef Provisioner - -The `chef` provisioner installs, configures and runs the Chef Client on a remote -resource. The `chef` provisioner supports both `ssh` and `winrm` type -[connections](/language/resources/provisioners/connection). - -!> **Warning:** This provisioner was removed in the 0.15.0 version of Terraform after being deprecated as of Terraform 0.13.4. Provisioners should also be a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. - -## Requirements - -The `chef` provisioner has some prerequisites for specific connection types: - -* For `ssh` type connections, `cURL` must be available on the remote host. -* For `winrm` connections, `PowerShell 2.0` must be available on the remote host. - -[Chef end user license agreement](https://www.chef.io/end-user-license-agreement/) must be accepted by setting `chef_license` to `accept` in `client_options` argument unless you are installing an old version of Chef client. - -Without these prerequisites, your provisioning execution will fail. - -## Example usage - -```hcl -resource "aws_instance" "web" { - # ... - - provisioner "chef" { - attributes_json = < **Important:** Use provisioners as a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. +[Declaring Provisioners](/terraform/language/resources/provisioners/syntax) for more details. ## Connection Block @@ -34,6 +34,19 @@ for some connection settings, so that `connection` blocks could sometimes be omitted. This feature was removed in 0.12 in order to make Terraform's behavior more predictable. +### Ephemeral values + +-> **Note**: Ephemeral values are available in Terraform v1.10 and later. + +The configuration for a `connection` block may use ephemeral values, such as +[`ephemeral` resources](/terraform/language/resources/ephemeral), [`ephemeral` +local values](/terraform/language/values/locals#ephemeral-values), [`ephemeral` +variables](/terraform/language/values/variables#ephemeral), or [`ephemeral` +output +values](/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files). + +Terraform will not store these values in your plan or state, or output them in +logs. ### Example usage @@ -46,8 +59,8 @@ provisioner "file" { connection { type = "ssh" user = "root" - password = "${var.root_password}" - host = "${var.host}" + password = var.root_password + host = var.host } } @@ -59,8 +72,8 @@ provisioner "file" { connection { type = "winrm" user = "Administrator" - password = "${var.admin_password}" - host = "${var.host}" + password = var.admin_password + host = var.host } } ``` @@ -72,7 +85,7 @@ Expressions in `connection` blocks cannot refer to their parent resource by name ### Argument Reference -The `connection` block supports the following argments. Some arguments are only supported by either the SSH or the WinRM connection type. +The `connection` block supports the following arguments. Some arguments are only supported by either the SSH or the WinRM connection type. | Argument | Connection Type | Description | Default | @@ -84,8 +97,8 @@ The `connection` block supports the following argments. Some arguments are only | `port` | Both| The port to connect to. | `22` for type `"ssh"`
`5985` for type `"winrm"` | | `timeout` | Both | The timeout to wait for the connection to become available. Should be provided as a string (e.g., `"30s"` or `"5m"`.) | `"5m"` | | `script_path` | Both | The path used to copy scripts meant for remote execution. Refer to [How Provisioners Execute Remote Scripts](#how-provisioners-execute-remote-scripts) below for more details. | (details below) | -| `private_key` | SSH | The contents of an SSH key to use for the connection. These can be loaded from a file on disk using [the `file` function](/language/functions/file). This takes preference over `password` if provided. | | -| `certificate` | SSH | The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `private_key`. These can be loaded from a file on disk using the [the `file` function](/language/functions/file). | | +| `private_key` | SSH | The contents of an SSH key to use for the connection. These can be loaded from a file on disk using [the `file` function](/terraform/language/functions/file). This takes preference over `password` if provided. | | +| `certificate` | SSH | The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `private_key`. These can be loaded from a file on disk using the [the `file` function](/terraform/language/functions/file). | | | `agent` | SSH | Set to `false` to disable using `ssh-agent` to authenticate. On Windows the only supported SSH authentication agent is [Pageant](http://the.earth.li/\~sgtatham/putty/0.66/htmldoc/Chapter9.html#pageant). | | | `agent_identity` | SSH | The preferred identity from the ssh agent for authentication. | | | `host_key` | SSH | The public key from the remote host or the signing CA, used to verify the connection. | | @@ -110,16 +123,16 @@ indirectly with a [bastion host](https://en.wikipedia.org/wiki/Bastion_host). | `bastion_port` | The port to use connect to the bastion host. | The value of the `port` field.| | `bastion_user`| The user for the connection to the bastion host. | The value of the `user` field. | | `bastion_password` | The password to use for the bastion host. | The value of the `password` field. | -| `bastion_private_key` | The contents of an SSH key file to use for the bastion host. These can be loaded from a file on disk using [the `file` function](/language/functions/file). | The value of the `private_key` field. | -| `bastion_certificate` | The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `bastion_private_key`. These can be loaded from a file on disk using the [the `file` function](/language/functions/file). | +| `bastion_private_key` | The contents of an SSH key file to use for the bastion host. These can be loaded from a file on disk using [the `file` function](/terraform/language/functions/file). | The value of the `private_key` field. | +| `bastion_certificate` | The contents of a signed CA Certificate. The certificate argument must be used in conjunction with a `bastion_private_key`. These can be loaded from a file on disk using the [the `file` function](/terraform/language/functions/file). | -## Connection through a HTTP Proxy with SSH +## Connection through HTTP and SOCKS5 proxies with SSH -The `ssh` connection also supports the following fields to facilitate connections by SSH over HTTP proxy. +The `ssh` connection also supports the following fields to facilitate connections by SSH over HTTP or SOCKS5 proxy. | Argument | Description | Default | |---------------|-------------|---------| -| `proxy_scheme` | http or https | | +| `proxy_scheme` | You can specify one of the following values: `http`, `https`, `socks5` | | | `proxy_host` | Setting this enables the SSH over HTTP connection. This host will be connected to first, and then the `host` or `bastion_host` connection will be made from there. | | | `proxy_port` | The port to use connect to the proxy host. | | | `proxy_user_name` | The username to use connect to the private proxy host. This argument should be specified only if authentication is required for the HTTP Proxy server. | | diff --git a/website/docs/language/resources/provisioners/file.mdx b/website/docs/language/resources/provisioners/file.mdx index 7618e3d04d..9655e8a3de 100644 --- a/website/docs/language/resources/provisioners/file.mdx +++ b/website/docs/language/resources/provisioners/file.mdx @@ -10,10 +10,10 @@ description: >- The `file` provisioner copies files or directories from the machine running Terraform to the newly created resource. The `file` provisioner -supports both `ssh` and `winrm` type [connections](/language/resources/provisioners/connection). +supports both `ssh` and `winrm` type [connections](/terraform/language/resources/provisioners/connection). ~> **Important:** Use provisioners as a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. +[Declaring Provisioners](/terraform/language/resources/provisioners/syntax) for more details. ## Example usage diff --git a/website/docs/language/resources/provisioners/habitat.mdx b/website/docs/language/resources/provisioners/habitat.mdx deleted file mode 100644 index 9d5b747744..0000000000 --- a/website/docs/language/resources/provisioners/habitat.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -page_title: 'Provisioner: habitat' -description: >- - The `habitat` provisioner installs the Habitat supervisor, and loads - configured services. ---- - -# Habitat Provisioner - -The `habitat` provisioner installs the [Habitat](https://habitat.sh) supervisor and loads configured services. This provisioner only supports Linux targets using the `ssh` connection type at this time. - -!> **Warning:** This provisioner was removed in the 0.15.0 version of Terraform after being deprecated as of Terraform 0.13.4. Provisioners should also be a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. - -## Requirements - -The `habitat` provisioner has some prerequisites for specific connection types: - -* For `ssh` type connections, we assume a few tools to be available on the remote host: - * `curl` - * `tee` - * `setsid` - Only if using the `unmanaged` service type. - -Without these prerequisites, your provisioning execution will fail. - -## Example usage - -```hcl -resource "aws_instance" "redis" { - count = 3 - - provisioner "habitat" { - peers = [aws_instance.redis[0].private_ip] - use_sudo = true - service_type = "systemd" - accept_license = true - - service { - name = "core/redis" - topology = "leader" - user_toml = file("conf/redis.toml") - } - } -} - -``` - -## Argument Reference - -There are 2 configuration levels, `supervisor` and `service`. Configuration placed directly within the `provisioner` block are supervisor configurations, and a provisioner can define zero or more services to run, and each service will have a `service` block within the `provisioner`. A `service` block can also contain zero or more `bind` blocks to create service group bindings. - -### Supervisor Arguments - -* `accept_license (bool)` - (Required) Set to true to accept [Habitat end user license agreement](https://www.chef.io/end-user-license-agreement/) -* `version (string)` - (Optional) The Habitat version to install on the remote machine. If not specified, the latest available version is used. -* `auto_update (bool)` - (Optional) If set to `true`, the supervisor will auto-update itself as soon as new releases are available on the specified `channel`. -* `http_disable (bool)` - (Optional) If set to `true`, disables the supervisor HTTP listener entirely. -* `peer (string)` - (Optional, deprecated) IP addresses or FQDN's for other Habitat supervisors to peer with, like: `--peer 1.2.3.4 --peer 5.6.7.8`. (Defaults to none) -* `peers (array)` - (Optional) A list of IP or FQDN's of other supervisor instance(s) to peer with. (Defaults to none) -* `service_type (string)` - (Optional) Method used to run the Habitat supervisor. Valid options are `unmanaged` and `systemd`. (Defaults to `systemd`) -* `service_name (string)` - (Optional) The name of the Habitat supervisor service, if using an init system such as `systemd`. (Defaults to `hab-supervisor`) -* `use_sudo (bool)` - (Optional) Use `sudo` when executing remote commands. Required when the user specified in the `connection` block is not `root`. (Defaults to `true`) -* `permanent_peer (bool)` - (Optional) Marks this supervisor as a permanent peer. (Defaults to false) -* `listen_ctl (string)` - (Optional) The listen address for the countrol gateway system (Defaults to 127.0.0.1:9632) -* `listen_gossip (string)` - (Optional) The listen address for the gossip system (Defaults to 0.0.0.0:9638) -* `listen_http (string)` - (Optional) The listen address for the HTTP gateway (Defaults to 0.0.0.0:9631) -* `ring_key (string)` - (Optional) The name of the ring key for encrypting gossip ring communication (Defaults to no encryption) -* `ring_key_content (string)` - (Optional) The key content. Only needed if using ring encryption and want the provisioner to take care of uploading and importing it. Easiest to source from a file (eg `ring_key_content = "${file("conf/foo-123456789.sym.key")}"`) (Defaults to none) -* `ctl_secret (string)` - (Optional) Specify a secret to use (from `hab sup secret generate`) for control gateway communication between hab client(s) and the supervisor. (Defaults to none) -* `url (string)` - (Optional) The URL of a Builder service to download packages and receive updates from. (Defaults to ) -* `channel (string)` - (Optional) The release channel in the Builder service to use. (Defaults to `stable`) -* `events (string)` - (Optional) Name of the service group running a Habitat EventSrv to forward Supervisor and service event data to. (Defaults to none) -* `organization (string)` - (Optional) The organization that the Supervisor and it's subsequent services are part of. (Defaults to `default`) -* `gateway_auth_token (string)` - (Optional) The http gateway authorization token (Defaults to none) -* `builder_auth_token (string)` - (Optional) The builder authorization token when using a private origin. (Defaults to none) - -### Service Arguments - -* `name (string)` - (Required) The Habitat package identifier of the service to run. (ie `core/haproxy` or `core/redis/3.2.4/20171002182640`) -* `binds (array)` - (Optional) An array of bind specifications. (ie `binds = ["backend:nginx.default"]`) -* `bind` - (Optional) An alternative way of declaring binds. This method can be easier to deal with when populating values from other values or variable inputs without having to do string interpolation. The following example is equivalent to `binds = ["backend:nginx.default"]`: - -```hcl -bind { - alias = "backend" - service = "nginx" - group = "default" -} -``` - -* `topology (string)` - (Optional) Topology to start service in. Possible values `standalone` or `leader`. (Defaults to `standalone`) -* `strategy (string)` - (Optional) Update strategy to use. Possible values `at-once`, `rolling` or `none`. (Defaults to `none`) -* `user_toml (string)` - (Optional) TOML formatted user configuration for the service. Easiest to source from a file (eg `user_toml = "${file("conf/redis.toml")}"`). (Defaults to none) -* `channel (string)` - (Optional) The release channel in the Builder service to use. (Defaults to `stable`) -* `group (string)` - (Optional) The service group to join. (Defaults to `default`) -* `url (string)` - (Optional) The URL of a Builder service to download packages and receive updates from. (Defaults to ) -* `application (string)` - (Optional) The application name. (Defaults to none) -* `environment (string)` - (Optional) The environment name. (Defaults to none) -* `service_key (string)` - (Optional) The key content of a service private key, if using service group encryption. Easiest to source from a file (eg `service_key = "${file("conf/redis.default@org-123456789.box.key")}"`) (Defaults to none) diff --git a/website/docs/language/resources/provisioners/local-exec.mdx b/website/docs/language/resources/provisioners/local-exec.mdx index 261e74cb35..c3bd882908 100644 --- a/website/docs/language/resources/provisioners/local-exec.mdx +++ b/website/docs/language/resources/provisioners/local-exec.mdx @@ -11,7 +11,7 @@ description: >- The `local-exec` provisioner invokes a local executable after a resource is created. This invokes a process on the machine running Terraform, not on the resource. See the `remote-exec` -[provisioner](/language/resources/provisioners/remote-exec) to run commands on the +[provisioner](/terraform/language/resources/provisioners/remote-exec) to run commands on the resource. Note that even though the resource will be fully created when the provisioner is @@ -19,7 +19,7 @@ run, there is no guarantee that it will be in an operable state - for example system services such as `sshd` may not be started yet on compute resources. ~> **Important:** Use provisioners as a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. +[Declaring Provisioners](/terraform/language/resources/provisioners/syntax) for more details. ## Example usage @@ -39,8 +39,11 @@ The following arguments are supported: * `command` - (Required) This is the command to execute. It can be provided as a relative path to the current working directory or as an absolute path. - It is evaluated in a shell, and can use environment variables or Terraform - variables. + The `command` is evaluated in a shell and can use environment variables for + variable substitution. We do not recommend using Terraform variables for variable + substitution because doing so can lead to shell injection vulnerabilities. Instead, you should pass Terraform variables to a command + through the `environment` parameter and use environment variable substitution + instead. Refer to the following OWASP article for additional information about injection flaws: [Code Injection](https://owasp.org/www-community/attacks/Code_Injection). * `working_dir` - (Optional) If provided, specifies the working directory where `command` will be executed. It can be provided as a relative path to the @@ -59,13 +62,15 @@ The following arguments are supported: * `when` - (Optional) If provided, specifies when Terraform will execute the command. For example, `when = destroy` specifies that the provisioner will run when the associated resource - is destroyed. Refer to [Destroy-Time Provisioners](/language/resources/provisioners/syntax#destroy-time-provisioners) + is destroyed. Refer to [Destroy-Time Provisioners](/terraform/language/resources/provisioners/syntax#destroy-time-provisioners) for details. + +* `quiet` - (Optional) If set to `true`, Terraform will not print the command to be executed to stdout, and will instead print "Suppressed by quiet=true". Note that the output of the command will still be printed in any case. ### Interpreter Examples ```hcl -resource "null_resource" "example1" { +resource "terraform_data" "example1" { provisioner "local-exec" { command = "open WFH, '>completed.txt' and print WFH scalar localtime" interpreter = ["perl", "-e"] @@ -74,7 +79,7 @@ resource "null_resource" "example1" { ``` ```hcl -resource "null_resource" "example2" { +resource "terraform_data" "example2" { provisioner "local-exec" { command = "Get-Date > completed.txt" interpreter = ["PowerShell", "-Command"] diff --git a/website/docs/language/resources/provisioners/null_resource.mdx b/website/docs/language/resources/provisioners/null_resource.mdx index fc270e364c..b91430807b 100644 --- a/website/docs/language/resources/provisioners/null_resource.mdx +++ b/website/docs/language/resources/provisioners/null_resource.mdx @@ -1,26 +1,25 @@ --- page_title: Provisioners Without a Resource description: >- - A null_resource allows you to configure provisioners that are not directly - associated with a single existing resource. + A terraform_data managed resource allows you to configure provisioners that + are not directly associated with a single existing resource. --- # Provisioners Without a Resource -[null]: https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource - If you need to run provisioners that aren't directly associated with a specific -resource, you can associate them with a `null_resource`. +resource, you can associate them with a `terraform_data`. -Instances of [`null_resource`][null] are treated like normal resources, but they -don't do anything. Like with any other resource, you can configure -[provisioners](/language/resources/provisioners/syntax) and [connection -details](/language/resources/provisioners/connection) on a `null_resource`. You can also -use its `triggers` argument and any meta-arguments to control exactly where in -the dependency graph its provisioners will run. +Instances of [`terraform_data`](/terraform/language/resources/terraform-data) are treated +like normal resources, but they don't do anything. Like with any other resource +type, you can configure [provisioners](/terraform/language/resources/provisioners/syntax) +and [connection details](/terraform/language/resources/provisioners/connection) on a +`terraform_data` resource. You can also use its `input` argument, `triggers_replace` argument, and any +meta-arguments to control exactly where in the dependency graph its +provisioners will run. ~> **Important:** Use provisioners as a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. +[Declaring Provisioners](/terraform/language/resources/provisioners/syntax) for more details. ## Example usage @@ -31,32 +30,21 @@ resource "aws_instance" "cluster" { # ... } -resource "null_resource" "cluster" { - # Changes to any instance of the cluster requires re-provisioning - triggers = { - cluster_instance_ids = "${join(",", aws_instance.cluster.*.id)}" - } +resource "terraform_data" "cluster" { + # Replacement of any instance of the cluster requires re-provisioning + triggers_replace = aws_instance.cluster[*].id # Bootstrap script can run on any instance of the cluster # So we just choose the first in this case connection { - host = "${element(aws_instance.cluster.*.public_ip, 0)}" + host = aws_instance.cluster[0].public_ip } provisioner "remote-exec" { # Bootstrap script called with private_ip of each node in the cluster inline = [ - "bootstrap-cluster.sh ${join(" ", aws_instance.cluster.*.private_ip)}", + "bootstrap-cluster.sh ${join(" ", aws_instance.cluster[*].private_ip)}", ] } } ``` - -## Argument Reference - -In addition to meta-arguments supported by all resources, `null_resource` -supports the following specific arguments: - -- `triggers` - A map of values which should cause this set of provisioners to - re-run. Values are meant to be interpolated references to variables or - attributes of other resources. diff --git a/website/docs/language/resources/provisioners/puppet.mdx b/website/docs/language/resources/provisioners/puppet.mdx deleted file mode 100644 index 3199caf28d..0000000000 --- a/website/docs/language/resources/provisioners/puppet.mdx +++ /dev/null @@ -1,95 +0,0 @@ ---- -page_title: 'Provisioner: puppet' -description: >- - The `puppet` provisioner installs, configures and runs the Puppet agent on a - resource. ---- - -# Puppet Provisioner - -The `puppet` provisioner installs, configures and runs the Puppet agent on a -remote resource. The `puppet` provisioner supports both `ssh` and `winrm` type -[connections](/language/resources/provisioners/connection). - -!> **Warning:** This provisioner was removed in the 0.15.0 version of Terraform after being deprecated as of Terraform 0.13.4. Provisioners should also be a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. - -## Requirements - -The `puppet` provisioner has some prerequisites for specific connection types: - -* For `ssh` type connections, `cURL` must be available on the remote host. -* For `winrm` connections, `PowerShell 2.0` must be available on the remote host. - -Without these prerequisites, your provisioning execution will fail. - -Additionally, the `puppet` provisioner requires -[Bolt](https://puppet.com/docs/bolt/latest/bolt.html) to be installed on your workstation -with the following [modules -installed](https://puppet.com/docs/bolt/latest/bolt_installing_modules.html#install-modules) - -* `danieldreier/autosign` -* `puppetlabs/puppet_agent` - -## Example usage - -```hcl -resource "aws_instance" "web" { - # ... - - provisioner "puppet" { - server = aws_instance.puppetmaster.public_dns - server_user = "ubuntu" - extension_requests = { - pp_role = "webserver" - } - } -} -``` - -## Argument Reference - -The following arguments are supported: - -* `server (string)` - (Required) The FQDN of the Puppet master that the agent - is to connect to. - -* `server_user (string)` - (Optional) The user that Bolt should connect to the - server as (defaults to `root`). - -* `os_type (string)` - (Optional) The OS type of the resource. Valid options - are: `linux` and `windows`. If not supplied, the connection type will be used - to determine the OS type (`ssh` will assume `linux` and `winrm` will assume - `windows`). - -* `use_sudo (boolean)` - (Optional) If `true`, commands run on the resource - will have their privileges elevated with sudo (defaults to `true` when the OS - type is `linux` and `false` when the OS type is `windows`). - -* `autosign (boolean)` - (Optional) Set to `true` if the Puppet master is using an autosigner such as - [Daniel Dreier's policy-based autosigning - tool](https://danieldreier.github.io/autosign). If `false` new agent certificate requests will have to be signed manually (defaults to `true`). - -* `open_source (boolean)` - (Optional) If `true` the provisioner uses an open source Puppet compatible agent install method (push via the Bolt agent install task). If `false` the simplified Puppet Enterprise installer will pull the agent from the Puppet master (defaults to `true`). - -* `certname (string)` - (Optional) The Subject CN used when requesting - a certificate from the Puppet master CA (defaults to the FQDN of the - resource). - -* `extension_requests (map)` - (Optional) A map of [extension - requests](https://puppet.com/docs/puppet/latest/ssl_attributes_extensions.html#concept-932) - to be embedded in the certificate signing request before it is sent to the - Puppet master CA and then transferred to the final certificate when the CSR - is signed. These become available during Puppet agent runs as [trusted facts](https://puppet.com/docs/puppet/latest/lang_facts_and_builtin_vars.html#trusted-facts). Friendly names for common extensions such as pp_role and pp_environment have [been predefined](https://puppet.com/docs/puppet/latest/ssl_attributes_extensions.html#recommended-oids-for-extensions). - -* `custom_attributes (map)` - (Optional) A map of [custom - attributes](https://puppet.com/docs/puppet/latest/ssl_attributes_extensions.html#concept-5488) - to be embedded in the certificate signing request before it is sent to the - Puppet master CA. - -* `environment (string)` - (Optional) The name of the Puppet environment that the - Puppet agent will be running in (defaults to `production`). - -* `bolt_timeout (string)` - (Optional) The timeout to wait for Bolt tasks to - complete. This should be specified as a string like `30s` or `5m` (defaults - to `5m` - 5 minutes). diff --git a/website/docs/language/resources/provisioners/remote-exec.mdx b/website/docs/language/resources/provisioners/remote-exec.mdx index 0203231a60..ef548d5ddd 100644 --- a/website/docs/language/resources/provisioners/remote-exec.mdx +++ b/website/docs/language/resources/provisioners/remote-exec.mdx @@ -13,12 +13,12 @@ description: >- The `remote-exec` provisioner invokes a script on a remote resource after it is created. This can be used to run a configuration management tool, bootstrap into a cluster, etc. To invoke a local process, see the `local-exec` -[provisioner](/language/resources/provisioners/local-exec) instead. The `remote-exec` -provisioner requires a [connection](/language/resources/provisioners/connection) +[provisioner](/terraform/language/resources/provisioners/local-exec) instead. The `remote-exec` +provisioner requires a [connection](/terraform/language/resources/provisioners/connection) and supports both `ssh` and `winrm`. ~> **Important:** Use provisioners as a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. +[Declaring Provisioners](/terraform/language/resources/provisioners/syntax) for more details. ## Example usage @@ -60,14 +60,14 @@ The following arguments are supported: that will be copied to the remote resource and then executed. They are executed in the order they are provided. This cannot be provided with `inline` or `script`. --> **Note:** Since `inline` is implemented by concatenating commands into a script, [`on_failure`](/language/resources/provisioners/syntax#failure-behavior) applies only to the final command in the list. In particular, with `on_failure = fail` (the default behaviour) earlier commands will be allowed to fail, and later commands will also execute. If this behaviour is not desired, consider using `"set -o errexit"` as the first command. +-> **Note:** Since `inline` is implemented by concatenating commands into a script, [`on_failure`](/terraform/language/resources/provisioners/syntax#failure-behavior) applies only to the final command in the list. In particular, with `on_failure = fail` (the default behaviour) earlier commands will be allowed to fail, and later commands will also execute. If this behaviour is not desired, consider using `"set -o errexit"` as the first command. ## Script Arguments You cannot pass any arguments to scripts using the `script` or `scripts` arguments to this provisioner. If you want to specify arguments, upload the script with the -[file provisioner](/language/resources/provisioners/file) +[file provisioner](/terraform/language/resources/provisioners/file) and then use `inline` to call it. Example: ```hcl diff --git a/website/docs/language/resources/provisioners/salt-masterless.mdx b/website/docs/language/resources/provisioners/salt-masterless.mdx deleted file mode 100644 index fbe54fed09..0000000000 --- a/website/docs/language/resources/provisioners/salt-masterless.mdx +++ /dev/null @@ -1,90 +0,0 @@ ---- -page_title: 'Provisioner: salt-masterless' -description: >- - The salt-masterless Terraform provisioner provisions machines built by - Terraform ---- - -# Salt Masterless Provisioner - -Type: `salt-masterless` - -The `salt-masterless` Terraform provisioner provisions machines built by Terraform -using [Salt](http://saltstack.com/) states, without connecting to a Salt master. The `salt-masterless` provisioner supports `ssh` [connections](/language/resources/provisioners/connection). - -!> **Warning:** This provisioner was removed in the 0.15.0 version of Terraform after being deprecated as of Terraform 0.13.4. Provisioners should also be a last resort. There are better alternatives for most situations. Refer to -[Declaring Provisioners](/language/resources/provisioners/syntax) for more details. - -## Requirements - -The `salt-masterless` provisioner has some prerequisites. `cURL` must be available on the remote host. - -## Example usage - -The example below is fully functional. - -```hcl - -provisioner "salt-masterless" { - "local_state_tree" = "/srv/salt" -} -``` - -## Argument Reference - -The reference of available configuration options is listed below. The only -required argument is the path to your local salt state tree. - -Optional: - -- `bootstrap_args` (string) - Arguments to send to the bootstrap script. Usage - is somewhat documented on - [github](https://github.com/saltstack/salt-bootstrap), but the [script - itself](https://github.com/saltstack/salt-bootstrap/blob/develop/bootstrap-salt.sh) - has more detailed usage instructions. By default, no arguments are sent to - the script. - -- `disable_sudo` (boolean) - By default, the bootstrap install command is prefixed with `sudo`. When using a - Docker builder, you will likely want to pass `true` since `sudo` is often not pre-installed. - -- `remote_pillar_roots` (string) - The path to your remote [pillar - roots](https://docs.saltproject.io/en/latest/ref/configuration/master.html#pillar-configuration). - default: `/srv/pillar`. This option cannot be used with `minion_config`. - -- `remote_state_tree` (string) - The path to your remote [state - tree](https://docs.saltproject.io/en/latest/ref/states/highstate.html#the-salt-state-tree). - default: `/srv/salt`. This option cannot be used with `minion_config`. - -- `local_pillar_roots` (string) - The path to your local [pillar - roots](https://docs.saltproject.io/en/latest/ref/configuration/master.html#pillar-configuration). - This will be uploaded to the `remote_pillar_roots` on the remote. - -- `local_state_tree` (string) - The path to your local [state - tree](https://docs.saltproject.io/en/latest/ref/states/highstate.html#the-salt-state-tree). - This will be uploaded to the `remote_state_tree` on the remote. - -- `custom_state` (string) - A state to be run instead of `state.highstate`. - Defaults to `state.highstate` if unspecified. - -- `minion_config_file` (string) - The path to your local [minion config - file](https://docs.saltproject.io/en/latest/ref/configuration/minion.html). This will be uploaded to the `/etc/salt` on the remote. This option overrides the `remote_state_tree` or `remote_pillar_roots` options. - -- `skip_bootstrap` (boolean) - By default the salt provisioner runs [salt - bootstrap](https://github.com/saltstack/salt-bootstrap) to install salt. Set - this to true to skip this step. - -- `temp_config_dir` (string) - Where your local state tree will be copied - before moving to the `/srv/salt` directory. Default is `/tmp/salt`. - -- `no_exit_on_failure` (boolean) - Terraform will exit if the `salt-call` command - fails. Set this option to true to ignore Salt failures. - -- `log_level` (string) - Set the logging level for the `salt-call` run. - -- `salt_call_args` (string) - Additional arguments to pass directly to `salt-call`. See - [salt-call](https://docs.saltproject.io/en/latest/ref/cli/salt-call.html) documentation for more - information. By default no additional arguments (besides the ones Terraform generates) - are passed to `salt-call`. - -- `salt_bin_dir` (string) - Path to the `salt-call` executable. Useful if it is not - on the PATH. diff --git a/website/docs/language/resources/provisioners/syntax.mdx b/website/docs/language/resources/provisioners/syntax.mdx index 421e5d2eec..cf73194a10 100644 --- a/website/docs/language/resources/provisioners/syntax.mdx +++ b/website/docs/language/resources/provisioners/syntax.mdx @@ -7,16 +7,18 @@ description: >- # Provisioners -Provisioners can be used to model specific actions on the local machine or on +You can use provisioners to model specific actions on the local machine or on a remote machine in order to prepare servers or other infrastructure objects for service. +-> **Note:** We removed the Chef, Habitat, Puppet, and Salt Masterless provisioners in Terraform v0.15.0. Information about these legacy provisioners is still available in the documentation for [Terraform v1.1 (and earlier)](/terraform/language/v1.1.x/resources/provisioners/syntax). + ## Provisioners are a Last Resort -> **Hands-on:** To learn about more declarative ways to handle provisioning actions, try the [Provision Infrastructure Deployed with Terraform](https://learn.hashicorp.com/collections/terraform/provision?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Provision Infrastructure Deployed with Terraform](/terraform/tutorials/provision?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials to learn about more declarative ways to handle provisioning actions. Terraform includes the concept of provisioners as a measure of pragmatism, -knowing that there will always be certain behaviors that can't be directly +knowing that there are always certain behaviors that cannot be directly represented in Terraform's declarative model. However, they also add a considerable amount of complexity and uncertainty to @@ -64,8 +66,8 @@ is immediately available on system boot. For example: [`google_compute_instance`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance) or [`google_compute_instance_group`](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance_group). * Oracle Cloud Infrastructure: `metadata` or `extended_metadata` on - [`oci_core_instance`](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/resources/core_instance) - or [`oci_core_instance_configuration`](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/resources/core_instance_configuration). + [`oci_core_instance`](https://registry.terraform.io/providers/oracle/oci/) + or [`oci_core_instance_configuration`](https://registry.terraform.io/providers/oracle/oci/). * VMware vSphere: Attach a virtual CDROM to [`vsphere_virtual_machine`](https://registry.terraform.io/providers/hashicorp/vsphere/latest/docs/resources/virtual_machine) using the `cdrom` block, containing a file called `user-data.txt`. @@ -76,7 +78,7 @@ process in various ways data passed via the means described above, allowing you to run arbitrary scripts and do basic system configuration immediately during the boot process and without the need to access the machine over SSH. -> **Hands-on:** Try the [Provision Infrastructure with Cloud-Init](https://learn.hashicorp.com/tutorials/terraform/cloud-init?in=terraform/provision&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Provision Infrastructure with Cloud-Init](/terraform/tutorials/provision/cloud-init?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. If you are building custom machine images, you can make use of the "user data" or "metadata" passed by the above means in whatever way makes sense to your @@ -93,6 +95,42 @@ data this way will allow faster boot times and simplify deployment by avoiding the need for direct network access from Terraform to the new server and for remote access credentials to be provided. +### Provisioning files using cloud-config + +You can add the [`cloudinit_config`](https://registry.terraform.io/providers/hashicorp/cloudinit/latest/docs) data source to your Terraform configuration and specify the files you want to provision as `text/cloud-config` content. The `cloudinit_config` data source renders multi-part MIME configurations for use with [cloud-init](https://cloudinit.readthedocs.io/en/latest/). Pass the files in the `content` field as YAML-encoded configurations using the `write_files` block. + +In the following example, the `my_cloud_config` data source specifies a `text/cloud-config` MIME part named `cloud.conf`. The `part.content` field is set to [`yamlencode`](/terraform/language/functions/yamlencode), which encodes the `write_files` JSON object as YAML so that the system can provision the referenced files. + +```hcl +data "cloudinit_config" "my_cloud_config" { + gzip = false + base64_encode = false + + part { + content_type = "text/cloud-config" + filename = "cloud.conf" + content = yamlencode( + { + "write_files" : [ + { + "path" : "/etc/foo.conf", + "content" : "foo contents", + }, + { + "path" : "/etc/bar.conf", + "content" : file("bar.conf"), + }, + { + "path" : "/etc/baz.conf", + "content" : templatefile("baz.tpl.conf", { SOME_VAR = "qux" }), + }, + ], + } + ) + } +} +``` + ### Running configuration management software As a convenience to users who are forced to use generic operating system @@ -106,7 +144,7 @@ configuration management provisioners and can run their installation steps during a separate build process, before creating a system disk image that you can deploy many times. -> **Hands-on:** Try the [Provision Infrastructure with Packer](https://learn.hashicorp.com/tutorials/terraform/packer?in=terraform/provision&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial on HashiCorp Learn. +> **Hands-on:** Try the [Provision Infrastructure with Packer](/terraform/tutorials/provision/packer?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorial. If you are using configuration management software that has a centralized server component, you will need to delay the _registration_ step until the final @@ -161,13 +199,9 @@ resource "aws_instance" "web" { The `local-exec` provisioner requires no other configuration, but most other provisioners must connect to the remote system using SSH or WinRM. -You must include [a `connection` block](/language/resources/provisioners/connection) so that Terraform -will know how to communicate with the server. +You must include [a `connection` block](/terraform/language/resources/provisioners/connection) so that Terraform knows how to communicate with the server. -Terraform includes several built-in provisioners; use the navigation sidebar to -view their documentation. - -It's also possible to use third-party provisioners as plugins, by placing them +Terraform includes several built-in provisioners. You can also use third-party provisioners as plugins, by placing them in `%APPDATA%\terraform.d\plugins`, `~/.terraform.d/plugins`, or the same directory where the Terraform binary is installed. However, we do not recommend using any provisioners except the built-in `file`, `local-exec`, and @@ -190,13 +224,26 @@ that resource's attributes. For example, use `self.public_ip` to reference an references create dependencies. Referring to a resource by name within its own block would create a dependency cycle. -## Suppressing Provisioner Logs in CLI Output +### Ephemeral values + +-> **Note**: Ephemeral values are available in Terraform v1.10 and later. + +The configuration for a `provisioner` block may use ephemeral values, such as +[`ephemeral` resources](/terraform/language/resources/ephemeral), [`ephemeral` +local values](/terraform/language/values/locals#ephemeral-values), [`ephemeral` +variables](/terraform/language/values/variables#ephemeral), or [`ephemeral` +output +values](/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files). + +Terraform does not store these values in your plan or state, or output them in +logs. + +### Sensitive values The configuration for a `provisioner` block may use sensitive values, such as -[`sensitive` variables](/language/values/variables#suppressing-values-in-cli-output) or -[`sensitive` output values](/language/values/outputs#sensitive-suppressing-values-in-cli-output). -In this case, all log output from the provisioner is automatically suppressed to -prevent the sensitive values from being displayed. +[`sensitive` variables](/terraform/language/values/variables#suppressing-values-in-cli-output) +or [`sensitive` output values](/terraform/language/values/outputs#sensitive-suppressing-values-in-cli-output). +Terraform suppresses sensitive values in all log output. ## Creation-Time Provisioners @@ -212,8 +259,7 @@ can leave a resource in a semi-configured state. Because Terraform cannot reason about what the provisioner does, the only way to ensure proper creation of a resource is to recreate it. This is tainting. -You can change this behavior by setting the `on_failure` attribute, -which is covered in detail below. +You can change this behavior by setting the `on_failure` attribute to `continue`. Refer to [Failure Behavior](#failure-behavior) for additional information. ## Destroy-Time Provisioners @@ -236,10 +282,7 @@ fail, Terraform will error and rerun the provisioners again on the next `terraform apply`. Due to this behavior, care should be taken for destroy provisioners to be safe to run multiple times. -``` -Destroy provisioners of this resource will not run if `create_before_destroy` -is set to `true`. We may address this in the future, and this [GitHub issue](https://github.com/hashicorp/terraform/issues/13549) contains more details. -``` +~> **Note**: A resource's destroy-time provisioners do not run if you enable [`create_before_destroy`](/terraform/language/meta-arguments/lifecycle#syntax-and-arguments) on that resource. Destroy-time provisioners can only run if they remain in the configuration at the time a resource is destroyed. If a resource block with a destroy-time @@ -253,8 +296,7 @@ remove a resource with a destroy-time provisioner: * Remove the resource block entirely from configuration, along with its `provisioner` blocks. * Apply again, at which point no further action should be taken since the resources were already destroyed. -This limitation may be addressed in future versions of Terraform. For now, -destroy-time provisioners must be used sparingly and with care. +Because of this limitation, you should use destroy-time provisioners sparingly and with care. ~> **NOTE:** A destroy-time provisioner within a resource that is tainted _will not_ run. This includes resources that are marked tainted from a failed creation-time provisioner or tainted manually using `terraform taint`. diff --git a/website/docs/language/resources/syntax.mdx b/website/docs/language/resources/syntax.mdx index cabb1ef9aa..9cdf9b1410 100644 --- a/website/docs/language/resources/syntax.mdx +++ b/website/docs/language/resources/syntax.mdx @@ -8,19 +8,23 @@ description: >- # Resource Blocks -> **Hands-on:** Try the [Terraform: Get Started](https://learn.hashicorp.com/collections/terraform/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) collection on HashiCorp Learn. +> **Hands-on:** Try the [Terraform: Get Started](/terraform/tutorials/aws-get-started?utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) tutorials. _Resources_ are the most important element in the Terraform language. Each resource block describes one or more infrastructure objects, such as virtual networks, compute instances, or higher-level components such as DNS records. +For information about how Terraform manages resources after applying a configuration, refer to +[Resource Behavior](/terraform/language/resources/behavior). + ## Resource Syntax -Resource declarations can include a number of advanced features, but only -a small subset are required for initial use. More advanced syntax features, -such as single resource declarations that produce multiple similar remote -objects, are described later in this page. +A `resource` block declares a resource of a specific type +with a specific local name. Terraform uses the name when referring to the resource +in the same module, but it has no meaning outside that module's scope. + +In the following example, the `aws_instance` resource type is named `web`. The resource type and name must be unique within a module because they serve as an identifier for a given resource. ```hcl resource "aws_instance" "web" { @@ -29,22 +33,18 @@ resource "aws_instance" "web" { } ``` -A `resource` block declares a resource of a given type ("aws_instance") -with a given local name ("web"). The name is used to refer to this resource -from elsewhere in the same Terraform module, but has no significance outside -that module's scope. - -The resource type and name together serve as an identifier for a given -resource and so must be unique within a module. - Within the block body (between `{` and `}`) are the configuration arguments -for the resource itself. Most arguments in this section depend on the -resource type, and indeed in this example both `ami` and `instance_type` are -arguments defined specifically for [the `aws_instance` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance). +for the resource itself. The arguments often depend on the +resource type. In this example, both `ami` and `instance_type` are special +arguments for [the `aws_instance` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance). -> **Note:** Resource names must start with a letter or underscore, and may contain only letters, digits, underscores, and dashes. +Resource declarations can include more advanced features, such as single +resource declarations that produce multiple similar remote objects, but only +a small subset is required for initial use. + ## Resource Types Each resource is associated with a single _resource type_, which determines @@ -53,29 +53,25 @@ attributes the resource supports. ### Providers -Each resource type is implemented by a [provider](/language/providers/requirements), -which is a plugin for Terraform that offers a collection of resource types. A -provider usually provides resources to manage a single cloud or on-premises -infrastructure platform. Providers are distributed separately from Terraform -itself, but Terraform can automatically install most providers when initializing +A [provider](/terraform/language/providers/requirements) is a plugin for Terraform +that offers a collection of resource types. Each resource type is implemented by a provider. A +provider provides resources to manage a single cloud or on-premises +infrastructure platform. Providers are distributed separately from Terraform, +but Terraform can automatically install most providers when initializing a working directory. -In order to manage resources, a Terraform module must specify which providers it -requires. Additionally, most providers need some configuration in order to -access their remote APIs, and the root module must provide that configuration. +To manage resources, a Terraform module must specify the required providers. Refer to +[Provider Requirements](/terraform/language/providers/requirements) for additional information. -For more information, see: +Most providers need some configuration to access their remote API, +which is provided by the root module. Refer to +[Provider Configuration](/terraform/language/providers/configuration) for additional information. -- [Provider Requirements](/language/providers/requirements), for declaring which - providers a module uses. -- [Provider Configuration](/language/providers/configuration), for configuring provider settings. - -Terraform usually automatically determines which provider to use based on a -resource type's name. (By convention, resource type names start with their -provider's preferred local name.) When using multiple configurations of a -provider (or non-preferred local provider names), you must use the `provider` -meta-argument to manually choose an alternate provider configuration. See -[the `provider` meta-argument](/language/meta-arguments/resource-provider) for more details. +Based on a resource type's name, Terraform can usually determine which provider to use. +By convention, resource type names start with their provider's preferred local name. +When using multiple configurations of a provider or non-preferred local provider names, +you must use [the `provider` meta-argument](/terraform/language/meta-arguments/resource-provider) +to manually choose a provider configuration. ### Resource Arguments @@ -84,52 +80,81 @@ selected resource type. The resource type's documentation lists which arguments are available and how their values should be formatted. The values for resource arguments can make full use of -[expressions](/language/expressions) and other dynamic Terraform +[expressions](/terraform/language/expressions) and other dynamic Terraform language features. -There are also some _meta-arguments_ that are defined by Terraform itself -and apply across all resource types. (See [Meta-Arguments](#meta-arguments) below.) +[Meta-arguments](#meta-arguments) are defined by Terraform +and apply across all resource types. ### Documentation for Resource Types Every Terraform provider has its own documentation, describing its resource types and their arguments. -Most publicly available providers are distributed on the -[Terraform Registry](https://registry.terraform.io/browse/providers), which also -hosts their documentation. When viewing a provider's page on the Terraform -Registry, you can click the "Documentation" link in the header to browse its -documentation. Provider documentation on the registry is versioned, and you can -use the dropdown version menu in the header to switch which version's -documentation you are viewing. +Some provider documentation is still part of Terraform's core documentation, +but the [Terraform Registry](https://registry.terraform.io/browse/providers) +is the main home for all publicly available provider docs. -To browse the publicly available providers and their documentation, see -[the providers section of the Terraform Registry](https://registry.terraform.io/browse/providers). - --> **Note:** Provider documentation used to be hosted directly on terraform.io, -as part of Terraform's core documentation. Although some provider documentation -might still be hosted here, the Terraform Registry is now the main home for all -public provider docs. - -## Resource Behavior - -For more information about how Terraform manages resources when applying a -configuration, see -[Resource Behavior](/language/resources/behavior). +When viewing a provider's page on the Terraform +Registry, you can click the **Documentation** link in the header to browse its +documentation. The documentation is versioned. To choose a different version of the provider documentation, click on the version in the provider breadcrumbs to choose a version from the drop-down menu. ## Meta-Arguments -The Terraform language defines several meta-arguments, which can be used with -any resource type to change the behavior of resources. +The Terraform language defines the following meta-arguments, which can be used with +any resource type to change the behavior of resources: -The following meta-arguments are documented on separate pages: +- [`depends_on`, for specifying hidden dependencies](/terraform/language/meta-arguments/depends_on) +- [`count`, for creating multiple resource instances according to a count](/terraform/language/meta-arguments/count) +- [`for_each`, to create multiple instances according to a map, or set of strings](/terraform/language/meta-arguments/for_each) +- [`provider`, for selecting a non-default provider configuration](/terraform/language/meta-arguments/resource-provider) +- [`lifecycle`, for lifecycle customizations](/terraform/language/meta-arguments/lifecycle) +- [`provisioner`, for taking extra actions after resource creation](/terraform/language/resources/provisioners/syntax) -- [`depends_on`, for specifying hidden dependencies](/language/meta-arguments/depends_on) -- [`count`, for creating multiple resource instances according to a count](/language/meta-arguments/count) -- [`for_each`, to create multiple instances according to a map, or set of strings](/language/meta-arguments/for_each) -- [`provider`, for selecting a non-default provider configuration](/language/meta-arguments/resource-provider) -- [`lifecycle`, for lifecycle customizations](/language/meta-arguments/lifecycle) -- [`provisioner`, for taking extra actions after resource creation](/language/resources/provisioners/syntax) +## Removing Resources + +-> **Note:** The `removed` block is available in Terraform v1.7 and later. For earlier Terraform versions, you can use the [`terraform state rm` CLI command](/terraform/cli/commands/state/rm) as a separate step. + +To remove a resource from Terraform, simply delete the `resource` block from your Terraform configuration. + +By default, after you remove the `resource` block, Terraform will plan to destroy any real infrastructure object managed by that resource. + +Sometimes you may wish to remove a resource from your Terraform configuration without destroying the real infrastructure object it manages. In this case, the resource will be removed from the [Terraform state](/terraform/language/state), but the real infrastructure object will not be destroyed. + +To declare that a resource was removed from Terraform configuration but that its managed object should not be destroyed, remove the `resource` block from your configuration and replace it with a `removed` block: + +```hcl +removed { + from = aws_instance.example + + lifecycle { + destroy = false + } +} +``` + +The `from` argument is the address of the resource you want to remove, without any instance keys (such as "aws_instance.example[1]"). + +The `lifecycle` block is required. The `destroy` argument determines whether Terraform will attempt to destroy the object managed by the resource or not. A value of `false` means that Terraform will remove the resource from state without destroying it. + +A `removed` block may also contain a [Destroy-Time Provisioner](/terraform/language/resources/provisioners/syntax#destroy-time-provisioners), so that the provisioner can remain in the configuration even though the `resource` block has been removed. + +```hcl +removed { + from = aws_instance.example + + lifecycle { + destroy = true + } + + provisioner "local-exec" { + when = destroy + command = "echo 'Instance ${self.id} has been destroyed.'" + } +} +``` + +The same referencing rules apply as in normal destroy-time provisioners, with only `count.index`, `each.key`, and `self` allowed. The provisioner must specify `when = destroy`, and the `removed` block must use `destroy = true` in order for the provisioner to execute. ## Custom Condition Checks @@ -151,9 +176,11 @@ resource "aws_instance" "example" { } ``` -Custom conditions can help capture assumptions, helping future maintainers understand the configuration design and intent. They also return useful information about errors earlier and in context, helping consumers more easily diagnose issues in their configurations. - -Refer to [Custom Condition Checks](/language/expressions/custom-conditions#preconditions-and-postconditions) for more details. +[Custom condition checks](/terraform/language/expressions/custom-conditions#preconditions-and-postconditions) +can help capture assumptions so that future maintainers +understand the configuration design and intent. They also return useful +information about errors earlier and in context, helping consumers to diagnose +issues in their configuration. ## Operation Timeouts @@ -161,7 +188,7 @@ Some resource types provide a special `timeouts` nested block argument that allows you to customize how long certain operations are allowed to take before being considered to have failed. For example, [`aws_db_instance`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_instance) -allows configurable timeouts for `create`, `update` and `delete` operations. +allows configurable timeouts for `create`, `update`, and `delete` operations. Timeouts are handled entirely by the resource type implementation in the provider, but resource types offering these features follow the convention diff --git a/website/docs/language/resources/terraform-data.mdx b/website/docs/language/resources/terraform-data.mdx new file mode 100644 index 0000000000..4e82c44815 --- /dev/null +++ b/website/docs/language/resources/terraform-data.mdx @@ -0,0 +1,82 @@ +--- +page_title: The terraform_data Managed Resource Type +description: >- + Retrieves the root module output values from a Terraform state snapshot stored + in a remote backend. +--- + +# The `terraform_data` Managed Resource Type + +The `terraform_data` implements the standard resource lifecycle, but does not directly take any other actions. +You can use the `terraform_data` resource without requiring or configuring a provider. It is always available through a built-in provider with the [source address](/terraform/language/providers/requirements#source-addresses) `terraform.io/builtin/terraform`. + +The `terraform_data` resource is useful for storing values which need to follow a manage resource lifecycle, and for triggering provisioners when there is no other logical managed resource in which to place them. + + +## Example Usage (data for `replace_triggered_by`) + + +[The `replace_triggered_by` lifecycle argument](/terraform/language/meta-arguments/lifecycle#replace_triggered_by) requires all of the given addresses to be for resources, because the decision to force replacement is based on the planned actions for all of the mentioned resources. + +Plain data values such as [Local Values](/terraform/language/values/locals) and [Input Variables](/terraform/language/values/variables) don't have any side-effects to plan against and so they aren't valid in `replace_triggered_by`. You can use `terraform_data`'s behavior of planning an action each time `input` changes to _indirectly_ use a plain value to trigger replacement. + + +```hcl +variable "revision" { + default = 1 +} + +resource "terraform_data" "replacement" { + input = var.revision +} + +# This resource has no convenient attribute which forces replacement, +# but can now be replaced by any change to the revision variable value. +resource "example_database" "test" { + lifecycle { + replace_triggered_by = [terraform_data.replacement] + } +} +``` + +## Example Usage (`null_resource` replacement) + +```hcl +resource "aws_instance" "web" { + # ... +} + +resource "aws_instance" "database" { + # ... +} + +# A use-case for terraform_data is as a do-nothing container +# for arbitrary actions taken by a provisioner. +resource "terraform_data" "bootstrap" { + triggers_replace = [ + aws_instance.web.id, + aws_instance.database.id + ] + + provisioner "local-exec" { + command = "bootstrap-hosts.sh" + } +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `input` - (Optional) A value which will be stored in the instance state, and reflected in the `output` attribute after apply. + +* `triggers_replace` - (Optional) A value which is stored in the instance state, and will force replacement when the value changes. + +## Attributes Reference + +In addition to the above, the following attributes are exported: + +* `id` - A string value unique to the resource instance. + +* `output` - The computed value derived from the `input` argument. During a plan where `output` is unknown, it will still be of the same type as `input`. diff --git a/website/docs/language/settings/backends/artifactory.mdx b/website/docs/language/settings/backends/artifactory.mdx deleted file mode 100644 index 574c5b3525..0000000000 --- a/website/docs/language/settings/backends/artifactory.mdx +++ /dev/null @@ -1,60 +0,0 @@ ---- -page_title: 'Backend Type: artifactory' -description: Terraform can store state in artifactory. ---- - -# artifactory (deprecated) - --> **Note:** The `artifactory` backend is deprecated and will be removed in a future Terraform release. - -Stores the state as an artifact in a given repository in -[Artifactory](https://www.jfrog.com/artifactory/). - -Generic HTTP repositories are supported, and state from different -configurations may be kept at different subpaths within the repository. - --> **Note:** The URL must include the path to the Artifactory installation. -It will likely end in `/artifactory`. - -This backend does **not** support [state locking](/language/state/locking). - -## Example Configuration - -```hcl -terraform { - backend "artifactory" { - username = "SheldonCooper" - password = "AmyFarrahFowler" - url = "https://custom.artifactoryonline.com/artifactory" - repo = "foo" - subpath = "terraform-bar" - } -} -``` - -## Data Source Configuration - -```hcl -data "terraform_remote_state" "foo" { - backend = "artifactory" - config = { - username = "SheldonCooper" - password = "AmyFarrahFowler" - url = "https://custom.artifactoryonline.com/artifactory" - repo = "foo" - subpath = "terraform-bar" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options / environment variables are supported: - -- `username` / `ARTIFACTORY_USERNAME` (Required) - The username -- `password` / `ARTIFACTORY_PASSWORD` (Required) - The password -- `url` / `ARTIFACTORY_URL` (Required) - The URL. Note that this is the base url to artifactory not the full repo and subpath. -- `repo` (Required) - The repository name -- `subpath` (Required) - Path within the repository diff --git a/website/docs/language/settings/backends/azurerm.mdx b/website/docs/language/settings/backends/azurerm.mdx deleted file mode 100644 index a9676b7756..0000000000 --- a/website/docs/language/settings/backends/azurerm.mdx +++ /dev/null @@ -1,326 +0,0 @@ ---- -page_title: 'Backend Type: azurerm' -description: Terraform can store state remotely in Azure Blob Storage. ---- - -# azurerm - -Stores the state as a Blob with the given Key within the Blob Container within [the Blob Storage Account](https://docs.microsoft.com/en-us/azure/storage/common/storage-introduction). - -This backend supports state locking and consistency checking with Azure Blob Storage native capabilities. - -~> **Terraform 1.1 and 1.2 supported a feature-flag to allow enabling/disabling the use of Microsoft Graph (and MSAL) rather than Azure Active Directory Graph (and ADAL) - however this flag has since been removed in Terraform 1.3. Microsoft Graph (and MSAL) are now enabled by default and Azure Active Directory Graph (and ADAL) can no longer be used. - -## Example Configuration - -When authenticating using the Azure CLI or a Service Principal (either with a Client Certificate or a Client Secret): - -```hcl -terraform { - backend "azurerm" { - resource_group_name = "StorageAccount-ResourceGroup" - storage_account_name = "abcd1234" - container_name = "tfstate" - key = "prod.terraform.tfstate" - } -} -``` - -*** - -When authenticating using Managed Service Identity (MSI): - -```hcl -terraform { - backend "azurerm" { - resource_group_name = "StorageAccount-ResourceGroup" - storage_account_name = "abcd1234" - container_name = "tfstate" - key = "prod.terraform.tfstate" - use_msi = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" - } -} -``` - -*** - -When authenticating using OpenID Connect (OIDC): - -```hcl -terraform { - backend "azurerm" { - resource_group_name = "StorageAccount-ResourceGroup" - storage_account_name = "abcd1234" - container_name = "tfstate" - key = "prod.terraform.tfstate" - use_oidc = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" - } -} -``` - -*** - -When authenticating using Azure AD Authentication: - -```hcl -terraform { - backend "azurerm" { - storage_account_name = "abcd1234" - container_name = "tfstate" - key = "prod.terraform.tfstate" - use_azuread_auth = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" - } -} -``` - --> **Note:** When using AzureAD for Authentication to Storage you also need to ensure the `Storage Blob Data Owner` role is assigned. - -*** - -When authenticating using the Access Key associated with the Storage Account: - -```hcl -terraform { - backend "azurerm" { - storage_account_name = "abcd1234" - container_name = "tfstate" - key = "prod.terraform.tfstate" - - # rather than defining this inline, the Access Key can also be sourced - # from an Environment Variable - more information is available below. - access_key = "abcdefghijklmnopqrstuvwxyz0123456789..." - } -} -``` - -*** - -When authenticating using a SAS Token associated with the Storage Account: - -```hcl -terraform { - backend "azurerm" { - storage_account_name = "abcd1234" - container_name = "tfstate" - key = "prod.terraform.tfstate" - - # rather than defining this inline, the SAS Token can also be sourced - # from an Environment Variable - more information is available below. - sas_token = "abcdefghijklmnopqrstuvwxyz0123456789..." - } -} -``` - --> **NOTE:** When using a Service Principal or an Access Key - we recommend using a [Partial Configuration](/language/settings/backends/configuration#partial-configuration) for the credentials. - -## Data Source Configuration - -When authenticating using a Service Principal (either with a Client Certificate or a Client Secret): - -```hcl -data "terraform_remote_state" "foo" { - backend = "azurerm" - config = { - storage_account_name = "terraform123abc" - container_name = "terraform-state" - key = "prod.terraform.tfstate" - } -} -``` - -*** - -When authenticating using Managed Service Identity (MSI): - -```hcl -data "terraform_remote_state" "foo" { - backend = "azurerm" - config = { - resource_group_name = "StorageAccount-ResourceGroup" - storage_account_name = "terraform123abc" - container_name = "terraform-state" - key = "prod.terraform.tfstate" - use_msi = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" - } -} -``` - -*** - -When authenticating using OpenID Connect (OIDC): - -```hcl -data "terraform_remote_state" "foo" { - backend = "azurerm" - config = { - resource_group_name = "StorageAccount-ResourceGroup" - storage_account_name = "terraform123abc" - container_name = "terraform-state" - key = "prod.terraform.tfstate" - use_oidc = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" - } -} -``` - -*** - -When authenticating using AzureAD Authentication: - -```hcl -data "terraform_remote_state" "foo" { - backend = "azurerm" - config = { - storage_account_name = "terraform123abc" - container_name = "terraform-state" - key = "prod.terraform.tfstate" - use_azuread_auth = true - subscription_id = "00000000-0000-0000-0000-000000000000" - tenant_id = "00000000-0000-0000-0000-000000000000" - } -} -``` - --> **Note:** When using AzureAD for Authentication to Storage you also need to ensure the `Storage Blob Data Owner` role is assigned. - -*** - -When authenticating using the Access Key associated with the Storage Account: - -```hcl -data "terraform_remote_state" "foo" { - backend = "azurerm" - config = { - storage_account_name = "terraform123abc" - container_name = "terraform-state" - key = "prod.terraform.tfstate" - - # rather than defining this inline, the Access Key can also be sourced - # from an Environment Variable - more information is available below. - access_key = "abcdefghijklmnopqrstuvwxyz0123456789..." - } -} -``` - -*** - -When authenticating using a SAS Token associated with the Storage Account: - -```hcl -data "terraform_remote_state" "foo" { - backend = "azurerm" - config = { - storage_account_name = "terraform123abc" - container_name = "terraform-state" - key = "prod.terraform.tfstate" - - # rather than defining this inline, the SAS Token can also be sourced - # from an Environment Variable - more information is available below. - sas_token = "abcdefghijklmnopqrstuvwxyz0123456789..." - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - - -The following configuration options are supported: - -* `storage_account_name` - (Required) The Name of [the Storage Account](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account). - -* `container_name` - (Required) The Name of [the Storage Container](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container) within the Storage Account. - -* `key` - (Required) The name of the Blob used to retrieve/store Terraform's State file inside the Storage Container. - -* `environment` - (Optional) The Azure Environment which should be used. This can also be sourced from the `ARM_ENVIRONMENT` environment variable. Possible values are `public`, `china`, `german`, `stack` and `usgovernment`. Defaults to `public`. - -* `endpoint` - (Optional) The Custom Endpoint for Azure Resource Manager. This can also be sourced from the `ARM_ENDPOINT` environment variable. - -~> **NOTE:** An `endpoint` should only be configured when using Azure Stack. - -* `snapshot` - (Optional) Should the Blob used to store the Terraform Statefile be snapshotted before use? Defaults to `false`. This value can also be sourced from the `ARM_SNAPSHOT` environment variable. - -*** - -When authenticating using the Managed Service Identity (MSI) - the following fields are also supported: - -* `resource_group_name` - (Required) The Name of the Resource Group in which the Storage Account exists. - -* `msi_endpoint` - (Optional) The path to a custom Managed Service Identity endpoint which is automatically determined if not specified. This can also be sourced from the `ARM_MSI_ENDPOINT` environment variable. - -* `subscription_id` - (Optional) The Subscription ID in which the Storage Account exists. This can also be sourced from the `ARM_SUBSCRIPTION_ID` environment variable. - -* `tenant_id` - (Optional) The Tenant ID in which the Subscription exists. This can also be sourced from the `ARM_TENANT_ID` environment variable. - -* `use_msi` - (Optional) Should Managed Service Identity authentication be used? This can also be sourced from the `ARM_USE_MSI` environment variable. - -*** - -When authenticating using a Service Principal with OpenID Connect (OIDC) - the following fields are also supported: - -* `oidc_request_url` - (Optional) The URL for the OIDC provider from which to request an ID token. This can also be sourced from the `ARM_OIDC_REQUEST_URL` or `ACTIONS_ID_TOKEN_REQUEST_URL` environment variables. - -* `oidc_request_token` - (Optional) The bearer token for the request to the OIDC provider. This can also be sourced from the `ARM_OIDC_REQUEST_TOKEN` or `ACTIONS_ID_TOKEN_REQUEST_TOKEN` environment variables. - -* `use_oidc` - (Optional) Should OIDC authentication be used? This can also be sourced from the `ARM_USE_OIDC` environment variable. - -*** - -When authenticating using a SAS Token associated with the Storage Account - the following fields are also supported: - -* `sas_token` - (Optional) The SAS Token used to access the Blob Storage Account. This can also be sourced from the `ARM_SAS_TOKEN` environment variable. - -*** - -When authenticating using the Storage Account's Access Key - the following fields are also supported: - -* `access_key` - (Optional) The Access Key used to access the Blob Storage Account. This can also be sourced from the `ARM_ACCESS_KEY` environment variable. - -*** - -When authenticating using AzureAD Authentication - the following fields are also supported: - -* `use_azuread_auth` - (Optional) Should AzureAD Authentication be used to access the Blob Storage Account. This can also be sourced from the `ARM_USE_AZUREAD` environment variable. - --> **Note:** When using AzureAD for Authentication to Storage you also need to ensure the `Storage Blob Data Owner` role is assigned. - -*** - -When authenticating using a Service Principal with a Client Certificate - the following fields are also supported: - -* `resource_group_name` - (Required) The Name of the Resource Group in which the Storage Account exists. - -* `client_id` - (Optional) The Client ID of the Service Principal. This can also be sourced from the `ARM_CLIENT_ID` environment variable. - -* `client_certificate_password` - (Optional) The password associated with the Client Certificate specified in `client_certificate_path`. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PASSWORD` environment variable. - -* `client_certificate_path` - (Optional) The path to the PFX file used as the Client Certificate when authenticating as a Service Principal. This can also be sourced from the `ARM_CLIENT_CERTIFICATE_PATH` environment variable. - -* `subscription_id` - (Optional) The Subscription ID in which the Storage Account exists. This can also be sourced from the `ARM_SUBSCRIPTION_ID` environment variable. - -* `tenant_id` - (Optional) The Tenant ID in which the Subscription exists. This can also be sourced from the `ARM_TENANT_ID` environment variable. - -*** - -When authenticating using a Service Principal with a Client Secret - the following fields are also supported: - -* `resource_group_name` - (Required) The Name of the Resource Group in which the Storage Account exists. - -* `client_id` - (Optional) The Client ID of the Service Principal. This can also be sourced from the `ARM_CLIENT_ID` environment variable. - -* `client_secret` - (Optional) The Client Secret of the Service Principal. This can also be sourced from the `ARM_CLIENT_SECRET` environment variable. - -* `subscription_id` - (Optional) The Subscription ID in which the Storage Account exists. This can also be sourced from the `ARM_SUBSCRIPTION_ID` environment variable. - -* `tenant_id` - (Optional) The Tenant ID in which the Subscription exists. This can also be sourced from the `ARM_TENANT_ID` environment variable. diff --git a/website/docs/language/settings/backends/cos.mdx b/website/docs/language/settings/backends/cos.mdx deleted file mode 100644 index 7f7077c599..0000000000 --- a/website/docs/language/settings/backends/cos.mdx +++ /dev/null @@ -1,61 +0,0 @@ ---- -page_title: 'Backend Type: cos' -description: >- - Terraform can store the state remotely, making it easier to version and work - with in a team. ---- - -# COS - -Stores the state as an object in a configurable prefix in a given bucket on [Tencent Cloud Object Storage](https://intl.cloud.tencent.com/product/cos) (COS). - -This backend supports [state locking](/language/state/locking). - -~> **Warning!** It is highly recommended that you enable [Object Versioning](https://intl.cloud.tencent.com/document/product/436/19883) -on the COS bucket to allow for state recovery in the case of accidental deletions and human error. - -## Example Configuration - -```hcl -terraform { - backend "cos" { - region = "ap-guangzhou" - bucket = "bucket-for-terraform-state-1258798060" - prefix = "terraform/state" - } -} -``` - -This assumes we have a [COS Bucket](https://registry.terraform.io/providers/tencentcloudstack/tencentcloud/latest/docs/resources/cos_bucket) created named `bucket-for-terraform-state-1258798060`, -Terraform state will be written into the file `terraform/state/terraform.tfstate`. - -## Data Source Configuration - -To make use of the COS remote state in another configuration, use the [`terraform_remote_state` data source](/language/state/remote-state-data). - -```hcl -data "terraform_remote_state" "foo" { - backend = "cos" - - config = { - region = "ap-guangzhou" - bucket = "bucket-for-terraform-state-1258798060" - prefix = "terraform/state" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options or environment variables are supported: - -- `secret_id` - (Optional) Secret id of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_ID`. -- `secret_key` - (Optional) Secret key of Tencent Cloud. It supports environment variables `TENCENTCLOUD_SECRET_KEY`. -- `region` - (Optional) The region of the COS bucket. It supports environment variables `TENCENTCLOUD_REGION`. -- `bucket` - (Required) The name of the COS bucket. You shall manually create it first. -- `prefix` - (Optional) The directory for saving the state file in bucket. Default to "env:". -- `key` - (Optional) The path for saving the state file in bucket. Defaults to `terraform.tfstate`. -- `encrypt` - (Optional) Whether to enable server side encryption of the state file. If it is true, COS will use 'AES256' encryption algorithm to encrypt state file. -- `acl` - (Optional) Object ACL to be applied to the state file, allows `private` and `public-read`. Defaults to `private`. diff --git a/website/docs/language/settings/backends/etcd.mdx b/website/docs/language/settings/backends/etcd.mdx deleted file mode 100644 index 9304d64865..0000000000 --- a/website/docs/language/settings/backends/etcd.mdx +++ /dev/null @@ -1,46 +0,0 @@ ---- -page_title: 'Backend Type: etcd' -description: Terraform can store state remotely in etcd 2.x. ---- - -# etcd (deprecated) - --> **Note:** The `etcd` backend is deprecated and will be removed in a future Terraform release. - -Stores the state in [etcd 2.x](https://coreos.com/etcd/docs/latest/v2/README.html) at a given path. - -This backend does **not** support [state locking](/language/state/locking). - -## Example Configuration - -```hcl -terraform { - backend "etcd" { - path = "path/to/terraform.tfstate" - endpoints = "http://one:4001 http://two:4001" - } -} -``` - -## Data Source Configuration - -```hcl -data "terraform_remote_state" "foo" { - backend = "etcd" - config = { - path = "path/to/terraform.tfstate" - endpoints = "http://one:4001 http://two:4001" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options are supported: - -- `path` - (Required) The path where to store the state -- `endpoints` - (Required) A space-separated list of the etcd endpoints -- `username` - (Optional) The username -- `password` - (Optional) The password diff --git a/website/docs/language/settings/backends/etcdv3.mdx b/website/docs/language/settings/backends/etcdv3.mdx deleted file mode 100644 index 04f46b381f..0000000000 --- a/website/docs/language/settings/backends/etcdv3.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -page_title: 'Backend Type: etcdv3' -description: Terraform can store state remotely in etcd 3.x. ---- - -# etcdv3 (deprecated) - --> **Note:** The `etcdv3` backend is deprecated and will be removed in a future Terraform release. - -Stores the state in the [etcd](https://etcd.io/) KV store with a given prefix. - -This backend supports [state locking](/language/state/locking). - -## Example Configuration - -```hcl -terraform { - backend "etcdv3" { - endpoints = ["etcd-1:2379", "etcd-2:2379", "etcd-3:2379"] - lock = true - prefix = "terraform-state/" - } -} -``` - -Note that for the access credentials we recommend using a -[partial configuration](/language/settings/backends/configuration#partial-configuration). - -## Data Source Configuration - -```hcl -data "terraform_remote_state" "foo" { - backend = "etcdv3" - config = { - endpoints = ["etcd-1:2379", "etcd-2:2379", "etcd-3:2379"] - lock = true - prefix = "terraform-state/" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both state and plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options / environment variables are supported: - -- `endpoints` - (Required) The list of 'etcd' endpoints which to connect to. -- `username` / `ETCDV3_USERNAME` - (Optional) Username used to connect to the etcd cluster. -- `password` / `ETCDV3_PASSWORD` - (Optional) Password used to connect to the etcd cluster. -- `prefix` - (Optional) An optional prefix to be added to keys when to storing state in etcd. Defaults to `""`. -- `lock` - (Optional) Whether to lock state access. Defaults to `true`. -- `cacert_path` - (Optional) The path to a PEM-encoded CA bundle with which to verify certificates of TLS-enabled etcd servers. -- `cert_path` - (Optional) The path to a PEM-encoded certificate to provide to etcd for secure client identification. -- `key_path` - (Optional) The path to a PEM-encoded key to provide to etcd for secure client identification. -- `max_request_bytes` - (Optional) The max request size to send to etcd. This can be increased to enable storage of larger state. You must set the corresponding server-side flag [--max-request-bytes](https://etcd.io/docs/current/dev-guide/limit/#request-size-limit) as well and the value should be less than the client setting. Defaults to `2097152` (2.0 MiB). **Please Note:** Increasing etcd's request size limit may negatively impact overall latency. diff --git a/website/docs/language/settings/backends/gcs.mdx b/website/docs/language/settings/backends/gcs.mdx deleted file mode 100644 index 91dc0e4248..0000000000 --- a/website/docs/language/settings/backends/gcs.mdx +++ /dev/null @@ -1,108 +0,0 @@ ---- -page_title: 'Backend Type: gcs' -description: >- - Terraform can store the state remotely, making it easier to version and work - with in a team. ---- - -# gcs - -Stores the state as an object in a configurable prefix in a pre-existing bucket on [Google Cloud Storage](https://cloud.google.com/storage/) (GCS). -The bucket must exist prior to configuring the backend. - -This backend supports [state locking](/language/state/locking). - -~> **Warning!** It is highly recommended that you enable -[Object Versioning](https://cloud.google.com/storage/docs/object-versioning) -on the GCS bucket to allow for state recovery in the case of accidental deletions and human error. - -## Example Configuration - -```hcl -terraform { - backend "gcs" { - bucket = "tf-state-prod" - prefix = "terraform/state" - } -} -``` - -## Data Source Configuration - -```hcl -data "terraform_remote_state" "foo" { - backend = "gcs" - config = { - bucket = "terraform-state" - prefix = "prod" - } -} - -resource "template_file" "bar" { - template = "${greeting}" - - vars { - greeting = "${data.terraform_remote_state.foo.greeting}" - } -} -``` - -## Authentication - -IAM Changes to buckets are [eventually consistent](https://cloud.google.com/storage/docs/consistency#eventually_consistent_operations) and may take upto a few minutes to take effect. Terraform will return 403 errors till it is eventually consistent. - -### Running Terraform on your workstation. - -If you are using terraform on your workstation, you will need to install the Google Cloud SDK and authenticate using [User Application Default -Credentials](https://cloud.google.com/sdk/gcloud/reference/auth/application-default). - -User ADCs do [expire](https://developers.google.com/identity/protocols/oauth2#expiration) and you can refresh them by running `gcloud auth application-default login`. - -### Running Terraform on Google Cloud - -If you are running terraform on Google Cloud, you can configure that instance or cluster to use a [Google Service -Account](https://cloud.google.com/compute/docs/authentication). This will allow Terraform to authenticate to Google Cloud without having to bake in a separate -credential/authentication file. Make sure that the scope of the VM/Cluster is set to cloud-platform. - -### Running Terraform outside of Google Cloud - -If you are running terraform outside of Google Cloud, generate a service account key and set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to -the path of the service account key. Terraform will use that key for authentication. - -### Impersonating Service Accounts - -Terraform can impersonate a Google Service Account as described [here](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials). A valid credential must be provided as mentioned in the earlier section and that identity must have the `roles/iam.serviceAccountTokenCreator` role on the service account you are impersonating. - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options are supported: - -- `bucket` - (Required) The name of the GCS bucket. This name must be - globally unique. For more information, see [Bucket Naming - Guidelines](https://cloud.google.com/storage/docs/bucketnaming.html#requirements). -- `credentials` / `GOOGLE_BACKEND_CREDENTIALS` / `GOOGLE_CREDENTIALS` - - (Optional) Local path to Google Cloud Platform account credentials in JSON - format. If unset, [Google Application Default - Credentials](https://developers.google.com/identity/protocols/application-default-credentials) - are used. The provided credentials must have Storage Object Admin role on the bucket. - **Warning**: if using the Google Cloud Platform provider as well, it will - also pick up the `GOOGLE_CREDENTIALS` environment variable. -- `impersonate_service_account` - (Optional) The service account to impersonate for accessing the State Bucket. - You must have `roles/iam.serviceAccountTokenCreator` role on that account for the impersonation to succeed. - If you are using a delegation chain, you can specify that using the `impersonate_service_account_delegates` field. - Alternatively, this can be specified using the `GOOGLE_IMPERSONATE_SERVICE_ACCOUNT` environment - variable. -- `impersonate_service_account_delegates` - (Optional) The delegation chain for an impersonating a service account as described [here](https://cloud.google.com/iam/docs/creating-short-lived-service-account-credentials#sa-credentials-delegated). -- `access_token` - (Optional) A temporary \[OAuth 2.0 access token] obtained - from the Google Authorization server, i.e. the `Authorization: Bearer` token - used to authenticate HTTP requests to GCP APIs. This is an alternative to - `credentials`. If both are specified, `access_token` will be used over the - `credentials` field. -- `prefix` - (Optional) GCS prefix inside the bucket. Named states for - workspaces are stored in an object called `/.tfstate`. -- `encryption_key` / `GOOGLE_ENCRYPTION_KEY` - (Optional) A 32 byte base64 - encoded 'customer supplied encryption key' used to encrypt all state. For - more information see [Customer Supplied Encryption - Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied). diff --git a/website/docs/language/settings/backends/manta.mdx b/website/docs/language/settings/backends/manta.mdx deleted file mode 100644 index f4b751cf3f..0000000000 --- a/website/docs/language/settings/backends/manta.mdx +++ /dev/null @@ -1,53 +0,0 @@ ---- -page_title: 'Backend Type: manta' -description: Terraform can store state in manta. ---- - -# manta (deprecated) - --> **Note:** The `manta` backend is deprecated and will be removed in a future Terraform release. - -Stores the state as an artifact in [Manta](https://www.joyent.com/manta). - -This backend supports [state locking](/language/state/locking), with locking within Manta. - -## Example Configuration - -```hcl -terraform { - backend "manta" { - path = "random/path" - object_name = "terraform.tfstate" - } -} -``` - -Note that for the access credentials we recommend using a -[partial configuration](/language/settings/backends/configuration#partial-configuration). - -## Data Source Configuration - -```hcl -data "terraform_remote_state" "foo" { - backend = "manta" - config = { - path = "random/path" - object_name = "terraform.tfstate" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options are supported: - -- `account` - (Required) This is the name of the Manta account. It can also be provided via the `SDC_ACCOUNT` or `TRITON_ACCOUNT` environment variables. -- `user` - (Optional) The username of the Triton account used to authenticate with the Triton API. It can also be provided via the `SDC_USER` or `TRITON_USER` environment variables. -- `url` - (Optional) The Manta API Endpoint. It can also be provided via the `MANTA_URL` environment variable. Defaults to `https://us-east.manta.joyent.com`. -- `key_material` - (Optional) This is the private key of an SSH key associated with the Triton account to be used. If this is not set, the private key corresponding to the fingerprint in key_id must be available via an SSH Agent. Can be set via the `SDC_KEY_MATERIAL` or `TRITON_KEY_MATERIAL` environment variables. -- `key_id` - (Required) This is the fingerprint of the public key matching the key specified in key_path. It can be obtained via the command ssh-keygen -l -E md5 -f /path/to/key. Can be set via the `SDC_KEY_ID` or `TRITON_KEY_ID` environment variables. -- `insecure_skip_tls_verify` - (Optional) This allows skipping TLS verification of the Triton endpoint. It is useful when connecting to a temporary Triton installation such as Cloud-On-A-Laptop which does not generally use a certificate signed by a trusted root CA. Defaults to `false`. -- `path` - (Required) The path relative to your private storage directory (`/$MANTA_USER/stor`) where the state file will be stored. **Please Note:** If this path does not exist, then the backend will create this folder location as part of backend creation. -- `object_name` - (Optional) The name of the state file (defaults to `terraform.tfstate`) diff --git a/website/docs/language/settings/backends/pg.mdx b/website/docs/language/settings/backends/pg.mdx deleted file mode 100644 index 1375792fda..0000000000 --- a/website/docs/language/settings/backends/pg.mdx +++ /dev/null @@ -1,89 +0,0 @@ ---- -page_title: 'Backend Type: pg' -description: Terraform can store state remotely in a Postgres database with locking. ---- - -# pg - -Stores the state in a [Postgres database](https://www.postgresql.org) version 10 or newer. - -This backend supports [state locking](/language/state/locking). - -## Example Configuration - -```hcl -terraform { - backend "pg" { - conn_str = "postgres://user:pass@db.example.com/terraform_backend" - } -} -``` - -Before initializing the backend with `terraform init`, the database must already exist: - -``` -createdb terraform_backend -``` - -This `createdb` command is found in [Postgres client applications](https://www.postgresql.org/docs/10/reference-client.html) which are installed along with the database server. - -We recommend using a -[partial configuration](/language/settings/backends/configuration#partial-configuration) -for the `conn_str` variable, because it typically contains access credentials that should not be committed to source control: - -```hcl -terraform { - backend "pg" {} -} -``` - -Then, set the credentials when initializing the configuration: - -``` -terraform init -backend-config="conn_str=postgres://user:pass@db.example.com/terraform_backend" -``` - -To use a Postgres server running on the same machine as Terraform, configure localhost with SSL disabled: - -``` -terraform init -backend-config="conn_str=postgres://localhost/terraform_backend?sslmode=disable" -``` - -## Data Source Configuration - -To make use of the pg remote state in another configuration, use the [`terraform_remote_state` data source](/language/state/remote-state-data). - -```hcl -data "terraform_remote_state" "network" { - backend = "pg" - config = { - conn_str = "postgres://localhost/terraform_backend" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options or environment variables are supported: - -- `conn_str` - (Required) Postgres connection string; a `postgres://` URL -- `schema_name` - Name of the automatically-managed Postgres schema, default `terraform_remote_state`. -- `skip_schema_creation` - If set to `true`, the Postgres schema must already exist. Terraform won't try to create the schema, this is useful when it has already been created by a database administrator. -- `skip_table_creation` - If set to `true`, the Postgres table must already exist. Terraform won't try to create the table, this is useful when it has already been created by a database administrator. -- `skip_index_creation` - If set to `true`, the Postgres index must already exist. Terraform won't try to create the index, this is useful when it has already been created by a database administrator. - -## Technical Design - -This backend creates one table **states** in the automatically-managed Postgres schema configured by the `schema_name` variable. - -The table is keyed by the [workspace](/language/state/workspaces) name. If workspaces are not in use, the name `default` is used. - -Locking is supported using [Postgres advisory locks](https://www.postgresql.org/docs/9.5/explicit-locking.html#ADVISORY-LOCKS). [`force-unlock`](/cli/commands/force-unlock) is not supported, because these database-native locks will automatically unlock when the session is aborted or the connection fails. To see outstanding locks in a Postgres server, use the [`pg_locks` system view](https://www.postgresql.org/docs/9.5/view-pg-locks.html). - -The **states** table contains: - -- a serial integer `id`, used as the key for advisory locks -- the workspace `name` key as _text_ with a unique index -- the Terraform state `data` as _text_ diff --git a/website/docs/language/settings/backends/s3.mdx b/website/docs/language/settings/backends/s3.mdx deleted file mode 100644 index 2774c11af8..0000000000 --- a/website/docs/language/settings/backends/s3.mdx +++ /dev/null @@ -1,457 +0,0 @@ ---- -page_title: 'Backend Type: s3' -description: Terraform can store state remotely in S3 and lock that state with DynamoDB. ---- - -# S3 - -Stores the state as a given key in a given bucket on -[Amazon S3](https://aws.amazon.com/s3/). -This backend also supports state locking and consistency checking via -[Dynamo DB](https://aws.amazon.com/dynamodb/), which can be enabled by setting -the `dynamodb_table` field to an existing DynamoDB table name. -A single DynamoDB table can be used to lock multiple remote state files. Terraform generates key names that include the values of the `bucket` and `key` variables. - -~> **Warning!** It is highly recommended that you enable -[Bucket Versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/manage-versioning-examples.html) -on the S3 bucket to allow for state recovery in the case of accidental deletions and human error. - -## Example Configuration - -```hcl -terraform { - backend "s3" { - bucket = "mybucket" - key = "path/to/my/key" - region = "us-east-1" - } -} -``` - -This assumes we have a bucket created called `mybucket`. The -Terraform state is written to the key `path/to/my/key`. - -Note that for the access credentials we recommend using a -[partial configuration](/language/settings/backends/configuration#partial-configuration). - -### S3 Bucket Permissions - -Terraform will need the following AWS IAM permissions on -the target backend bucket: - -* `s3:ListBucket` on `arn:aws:s3:::mybucket` -* `s3:GetObject` on `arn:aws:s3:::mybucket/path/to/my/key` -* `s3:PutObject` on `arn:aws:s3:::mybucket/path/to/my/key` -* `s3:DeleteObject` on `arn:aws:s3:::mybucket/path/to/my/key` - -This is seen in the following AWS IAM Statement: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": "s3:ListBucket", - "Resource": "arn:aws:s3:::mybucket" - }, - { - "Effect": "Allow", - "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"], - "Resource": "arn:aws:s3:::mybucket/path/to/my/key" - } - ] -} -``` - --> **Note:** AWS can control access to S3 buckets with either IAM policies -attached to users/groups/roles (like the example above) or resource policies -attached to bucket objects (which look similar but also require a `Principal` to -indicate which entity has those permissions). For more details, see Amazon's -documentation about -[S3 access control](https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-access-control.html). - -### DynamoDB Table Permissions - -If you are using state locking, Terraform will need the following AWS IAM -permissions on the DynamoDB table (`arn:aws:dynamodb:::table/mytable`): - -* `dynamodb:GetItem` -* `dynamodb:PutItem` -* `dynamodb:DeleteItem` - -This is seen in the following AWS IAM Statement: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:DeleteItem" - ], - "Resource": "arn:aws:dynamodb:*:*:table/mytable" - } - ] -} -``` - -## Data Source Configuration - -To make use of the S3 remote state in another configuration, use the -[`terraform_remote_state` data -source](/language/state/remote-state-data). - -```hcl -data "terraform_remote_state" "network" { - backend = "s3" - config = { - bucket = "terraform-state-prod" - key = "network/terraform.tfstate" - region = "us-east-1" - } -} -``` - -The `terraform_remote_state` data source will return all of the root module -outputs defined in the referenced remote state (but not any outputs from -nested modules unless they are explicitly output again in the root). An -example output might look like: - -``` -data.terraform_remote_state.network: - id = 2016-10-29 01:57:59.780010914 +0000 UTC - addresses.# = 2 - addresses.0 = 52.207.220.222 - addresses.1 = 54.196.78.166 - backend = s3 - config.% = 3 - config.bucket = terraform-state-prod - config.key = network/terraform.tfstate - config.region = us-east-1 - elb_address = web-elb-790251200.us-east-1.elb.amazonaws.com - public_subnet_id = subnet-1e05dd33 -``` - -## Configuration - -This backend requires the configuration of the AWS Region and S3 state storage. Other configuration, such as enabling DynamoDB state locking, is optional. - -### Credentials and Shared Configuration - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration is required: - -* `region` - (Required) AWS Region of the S3 Bucket and DynamoDB Table (if used). This can also be sourced from the `AWS_DEFAULT_REGION` and `AWS_REGION` environment variables. - -The following configuration is optional: - -* `access_key` - (Optional) AWS access key. If configured, must also configure `secret_key`. This can also be sourced from the `AWS_ACCESS_KEY_ID` environment variable, AWS shared credentials file (e.g. `~/.aws/credentials`), or AWS shared configuration file (e.g. `~/.aws/config`). -* `secret_key` - (Optional) AWS access key. If configured, must also configure `access_key`. This can also be sourced from the `AWS_SECRET_ACCESS_KEY` environment variable, AWS shared credentials file (e.g. `~/.aws/credentials`), or AWS shared configuration file (e.g. `~/.aws/config`). -* `iam_endpoint` - (Optional) Custom endpoint for the AWS Identity and Access Management (IAM) API. This can also be sourced from the `AWS_IAM_ENDPOINT` environment variable. -* `max_retries` - (Optional) The maximum number of times an AWS API request is retried on retryable failure. Defaults to 5. -* `profile` - (Optional) Name of AWS profile in AWS shared credentials file (e.g. `~/.aws/credentials`) or AWS shared configuration file (e.g. `~/.aws/config`) to use for credentials and/or configuration. This can also be sourced from the `AWS_PROFILE` environment variable. -* `shared_credentials_file` - (Optional) Path to the AWS shared credentials file. Defaults to `~/.aws/credentials`. -* `skip_credentials_validation` - (Optional) Skip credentials validation via the STS API. -* `skip_region_validation` - (Optional) Skip validation of provided region name. -* `skip_metadata_api_check` - (Optional) Skip usage of EC2 Metadata API. -* `sts_endpoint` - (Optional) Custom endpoint for the AWS Security Token Service (STS) API. This can also be sourced from the `AWS_STS_ENDPOINT` environment variable. -* `token` - (Optional) Multi-Factor Authentication (MFA) token. This can also be sourced from the `AWS_SESSION_TOKEN` environment variable. - -#### Assume Role Configuration - -The following configuration is optional: - -* `assume_role_duration_seconds` - (Optional) Number of seconds to restrict the assume role session duration. -* `assume_role_policy` - (Optional) IAM Policy JSON describing further restricting permissions for the IAM Role being assumed. -* `assume_role_policy_arns` - (Optional) Set of Amazon Resource Names (ARNs) of IAM Policies describing further restricting permissions for the IAM Role being assumed. -* `assume_role_tags` - (Optional) Map of assume role session tags. -* `assume_role_transitive_tag_keys` - (Optional) Set of assume role session tag keys to pass to any subsequent sessions. -* `external_id` - (Optional) External identifier to use when assuming the role. -* `role_arn` - (Optional) Amazon Resource Name (ARN) of the IAM Role to assume. -* `session_name` - (Optional) Session name to use when assuming the role. - -### S3 State Storage - -The following configuration is required: - -* `bucket` - (Required) Name of the S3 Bucket. -* `key` - (Required) Path to the state file inside the S3 Bucket. When using a non-default [workspace](/language/state/workspaces), the state path will be `/workspace_key_prefix/workspace_name/key` (see also the `workspace_key_prefix` configuration). - -The following configuration is optional: - -* `acl` - (Optional) [Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#canned-acl) to be applied to the state file. -* `encrypt` - (Optional) Enable [server side encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingServerSideEncryption.html) of the state file. -* `endpoint` - (Optional) Custom endpoint for the AWS S3 API. This can also be sourced from the `AWS_S3_ENDPOINT` environment variable. -* `force_path_style` - (Optional) Enable path-style S3 URLs (`https:///` instead of `https://.`). -* `kms_key_id` - (Optional) Amazon Resource Name (ARN) of a Key Management Service (KMS) Key to use for encrypting the state. Note that if this value is specified, Terraform will need `kms:Encrypt`, `kms:Decrypt` and `kms:GenerateDataKey` permissions on this KMS key. -* `sse_customer_key` - (Optional) The key to use for encrypting state with [Server-Side Encryption with Customer-Provided Keys (SSE-C)](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html). This is the base64-encoded value of the key, which must decode to 256 bits. This can also be sourced from the `AWS_SSE_CUSTOMER_KEY` environment variable, which is recommended due to the sensitivity of the value. Setting it inside a terraform file will cause it to be persisted to disk in `terraform.tfstate`. -* `workspace_key_prefix` - (Optional) Prefix applied to the state path inside the bucket. This is only relevant when using a non-default workspace. Defaults to `env:`. - -### DynamoDB State Locking - -The following configuration is optional: - -* `dynamodb_endpoint` - (Optional) Custom endpoint for the AWS DynamoDB API. This can also be sourced from the `AWS_DYNAMODB_ENDPOINT` environment variable. -* `dynamodb_table` - (Optional) Name of DynamoDB Table to use for state locking and consistency. The table must have a partition key named `LockID` with type of `String`. If not configured, state locking will be disabled. - -## Multi-account AWS Architecture - -A common architectural pattern is for an organization to use a number of -separate AWS accounts to isolate different teams and environments. For example, -a "staging" system will often be deployed into a separate AWS account than -its corresponding "production" system, to minimize the risk of the staging -environment affecting production infrastructure, whether via rate limiting, -misconfigured access controls, or other unintended interactions. - -The S3 backend can be used in a number of different ways that make different -tradeoffs between convenience, security, and isolation in such an organization. -This section describes one such approach that aims to find a good compromise -between these tradeoffs, allowing use of -[Terraform's workspaces feature](/language/state/workspaces) to switch -conveniently between multiple isolated deployments of the same configuration. - -Use this section as a starting-point for your approach, but note that -you will probably need to make adjustments for the unique standards and -regulations that apply to your organization. You will also need to make some -adjustments to this approach to account for _existing_ practices within your -organization, if for example other tools have previously been used to manage -infrastructure. - -Terraform is an administrative tool that manages your infrastructure, and so -ideally the infrastructure that is used by Terraform should exist outside of -the infrastructure that Terraform manages. This can be achieved by creating a -separate _administrative_ AWS account which contains the user accounts used by -human operators and any infrastructure and tools used to manage the other -accounts. Isolating shared administrative tools from your main environments -has a number of advantages, such as avoiding accidentally damaging the -administrative infrastructure while changing the target infrastructure, and -reducing the risk that an attacker might abuse production infrastructure to -gain access to the (usually more privileged) administrative infrastructure. - -### Administrative Account Setup - -Your administrative AWS account will contain at least the following items: - -* One or more [IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html) - for system administrators that will log in to maintain infrastructure in - the other accounts. -* Optionally, one or more [IAM groups](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_groups.html) - to differentiate between different groups of users that have different - levels of access to the other AWS accounts. -* An [S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingBucket.html) - that will contain the Terraform state files for each workspace. -* A [DynamoDB table](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.TablesItemsAttributes) - that will be used for locking to prevent concurrent operations on a single - workspace. - -Provide the S3 bucket name and DynamoDB table name to Terraform within the -S3 backend configuration using the `bucket` and `dynamodb_table` arguments -respectively, and configure a suitable `workspace_key_prefix` to contain -the states of the various workspaces that will subsequently be created for -this configuration. - -### Environment Account Setup - -For the sake of this section, the term "environment account" refers to one -of the accounts whose contents are managed by Terraform, separate from the -administrative account described above. - -Your environment accounts will eventually contain your own product-specific -infrastructure. Along with this it must contain one or more -[IAM roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html) -that grant sufficient access for Terraform to perform the desired management -tasks. - -### Delegating Access - -Each Administrator will run Terraform using credentials for their IAM user -in the administrative account. -[IAM Role Delegation](https://docs.aws.amazon.com/IAM/latest/UserGuide/tutorial_cross-account-with-roles.html) -is used to grant these users access to the roles created in each environment -account. - -Full details on role delegation are covered in the AWS documentation linked -above. The most important details are: - -* Each role's _Assume Role Policy_ must grant access to the administrative AWS - account, which creates a trust relationship with the administrative AWS - account so that its users may assume the role. -* The users or groups within the administrative account must also have a - policy that creates the converse relationship, allowing these users or groups - to assume that role. - -Since the purpose of the administrative account is only to host tools for -managing other accounts, it is useful to give the administrative accounts -restricted access only to the specific operations needed to assume the -environment account role and access the Terraform state. By blocking all -other access, you remove the risk that user error will lead to staging or -production resources being created in the administrative account by mistake. - -When configuring Terraform, use either environment variables or the standard -credentials file `~/.aws/credentials` to provide the administrator user's -IAM credentials within the administrative account to both the S3 backend _and_ -to Terraform's AWS provider. - -Use conditional configuration to pass a different `assume_role` value to -the AWS provider depending on the selected workspace. For example: - -```hcl -variable "workspace_iam_roles" { - default = { - staging = "arn:aws:iam::STAGING-ACCOUNT-ID:role/Terraform" - production = "arn:aws:iam::PRODUCTION-ACCOUNT-ID:role/Terraform" - } -} - -provider "aws" { - # No credentials explicitly set here because they come from either the - # environment or the global credentials file. - - assume_role = { - role_arn = "${var.workspace_iam_roles[terraform.workspace]}" - } -} -``` - -If workspace IAM roles are centrally managed and shared across many separate -Terraform configurations, the role ARNs could also be obtained via a data -source such as [`terraform_remote_state`](/language/state/remote-state-data) -to avoid repeating these values. - -### Creating and Selecting Workspaces - -With the necessary objects created and the backend configured, run -`terraform init` to initialize the backend and establish an initial workspace -called "default". This workspace will not be used, but is created automatically -by Terraform as a convenience for users who are not using the workspaces -feature. - -Create a workspace corresponding to each key given in the `workspace_iam_roles` -variable value above: - -``` -$ terraform workspace new staging -Created and switched to workspace "staging"! - -... - -$ terraform workspace new production -Created and switched to workspace "production"! - -... -``` - -Due to the `assume_role` setting in the AWS provider configuration, any -management operations for AWS resources will be performed via the configured -role in the appropriate environment AWS account. The backend operations, such -as reading and writing the state from S3, will be performed directly as the -administrator's own user within the administrative account. - -``` -$ terraform workspace select staging -$ terraform apply -... -``` - -### Running Terraform in Amazon EC2 - -Teams that make extensive use of Terraform for infrastructure management -often [run Terraform in automation](https://learn.hashicorp.com/tutorials/terraform/automate-terraform?in=terraform/automation&utm_source=WEBSITE&utm_medium=WEB_IO&utm_offer=ARTICLE_PAGE&utm_content=DOCS) -to ensure a consistent operating environment and to limit access to the -various secrets and other sensitive information that Terraform configurations -tend to require. - -When running Terraform in an automation tool running on an Amazon EC2 instance, -consider running this instance in the administrative account and using an -[instance profile](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html) -in place of the various administrator IAM users suggested above. An IAM -instance profile can also be granted cross-account delegation access via -an IAM policy, giving this instance the access it needs to run Terraform. - -To isolate access to different environment accounts, use a separate EC2 -instance for each target account so that its access can be limited only to -the single account. - -Similar approaches can be taken with equivalent features in other AWS compute -services, such as ECS. - -### Protecting Access to Workspace State - -In a simple implementation of the pattern described in the prior sections, -all users have access to read and write states for all workspaces. In many -cases it is desirable to apply more precise access constraints to the -Terraform state objects in S3, so that for example only trusted administrators -are allowed to modify the production state, or to control _reading_ of a state -that contains sensitive information. - -Amazon S3 supports fine-grained access control on a per-object-path basis -using IAM policy. A full description of S3's access control mechanism is -beyond the scope of this guide, but an example IAM policy granting access -to only a single state object within an S3 bucket is shown below: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": "s3:ListBucket", - "Resource": "arn:aws:s3:::myorg-terraform-states" - }, - { - "Effect": "Allow", - "Action": ["s3:GetObject", "s3:PutObject"], - "Resource": "arn:aws:s3:::myorg-terraform-states/myapp/production/tfstate" - } - ] -} -``` - -It is also possible to apply fine-grained access control to the DynamoDB -table used for locking. When Terraform puts the state lock in place during `terraform plan`, it stores the full state file as a document and sets the s3 object key as the partition key for the document. After the state lock is released, Terraform places a digest of the updated state file in DynamoDB. The key is similar to the one for the original state file, but is suffixed with `-md5`. - -The example below shows a simple IAM policy that allows the backend operations role to perform these operations: - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect" : "Allow", - "Action" : [ - "dynamodb:DeleteItem", - "dynamodb:GetItem", - "dynamodb:PutItem", - "dynamodb:Query", - "dynamodb:UpdateItem" - ], - "Resource" : ["arn:aws:dynamodb:*:*:table/myorg-state-lock-table"], - "Condition" : { - "ForAllValues:StringEquals" : { - "dynamodb:LeadingKeys" : [ - "myorg-terraform-states/myapp/production/tfstate", // during a state lock the full state file is stored with this key - "myorg-terraform-states/myapp/production/tfstate-md5" // after the lock is released a hash of the statefile's contents are stored with this key - ] - } - } - } - ] -} -``` - -Refer to the [AWS documentation on DynamoDB fine-grained locking](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/specifying-conditions.html) for more details. - -### Configuring Custom User-Agent Information - -Note this feature is optional and only available in Terraform v0.13.1+. - -By default, the underlying AWS client used by the Terraform AWS Provider creates requests with User-Agent headers including information about Terraform and AWS Go SDK versions. To provide additional information in the User-Agent headers, the `TF_APPEND_USER_AGENT` environment variable can be set and its value will be directly added to HTTP requests. e.g. - -```sh -$ export TF_APPEND_USER_AGENT="JenkinsAgent/i-12345678 BuildID/1234 (Optional Extra Information)" -``` diff --git a/website/docs/language/settings/backends/swift.mdx b/website/docs/language/settings/backends/swift.mdx deleted file mode 100644 index eb20bae88c..0000000000 --- a/website/docs/language/settings/backends/swift.mdx +++ /dev/null @@ -1,176 +0,0 @@ ---- -page_title: 'Backend Type: swift' -description: Terraform can store state remotely in Swift. ---- - -# swift (deprecated) - --> **Note:** The `swift` backend is deprecated and will be removed in a future Terraform release. - -Stores the state as an artifact in [Swift](http://docs.openstack.org/developer/swift/latest/). - -This backend supports [state locking](/language/state/locking). - -~> Warning! It is highly recommended that you enable [Object Versioning](https://docs.openstack.org/developer/swift/latest/overview_object_versioning.html) by setting the [`archive_container`](/language/settings/backends/swift#archive_container) configuration. This allows for state recovery in the case of accidental deletions and human error. - -## Example Configuration - -```hcl -terraform { - backend "swift" { - container = "terraform-state" - archive_container = "terraform-state-archive" - } -} -``` - -This will create a container called `terraform-state` and an object within that container called `tfstate.tf`. It will enable versioning using the `terraform-state-archive` container to contain the older version. - -For the access credentials we recommend using a -[partial configuration](/language/settings/backends/configuration#partial-configuration). - -## Data Source Configuration - -```hcl -data "terraform_remote_state" "foo" { - backend = "swift" - config = { - container = "terraform_state" - archive_container = "terraform_state-archive" - } -} -``` - -## Configuration Variables - -!> **Warning:** We recommend using environment variables to supply credentials and other sensitive data. If you use `-backend-config` or hardcode these values directly in your configuration, Terraform will include these values in both the `.terraform` subdirectory and in plan files. Refer to [Credentials and Sensitive Data](/language/settings/backends/configuration#credentials-and-sensitive-data) for details. - -The following configuration options are supported: - -* `auth_url` - (Optional) The Identity authentication URL. If omitted, the - `OS_AUTH_URL` environment variable is used. - -* `cloud` - (Optional; required if `auth_url` is not specified) An entry in a - `clouds.yaml` file. See the OpenStack `os-client-config` - [documentation](https://docs.openstack.org/os-client-config/latest/user/configuration.html) - for more information about `clouds.yaml` files. If omitted, the `OS_CLOUD` - environment variable is used. - -* `region_name` - (Optional) - The region in which to store `terraform.tfstate`. If - omitted, the `OS_REGION_NAME` environment variable is used. - -* `container` - (Required) The name of the container to create for storing - the Terraform state file. - -* `state_name` - (Optional) The name of the state file in the container. - Defaults to `tfstate.tf`. - -* `path` - (Optional) DEPRECATED: Use `container` instead. - The name of the container to create in order to store the state file. - -* `user_name` - (Optional) The Username to login with. If omitted, the - `OS_USERNAME` environment variable is used. - -* `user_id` - (Optional) The User ID to login with. If omitted, the - `OS_USER_ID` environment variable is used. - -* `application_credential_id` - (Optional) (Identity v3 only) The ID of an - application credential to authenticate with. An - `application_credential_secret` has to bet set along with this parameter. - -* `application_credential_name` - (Optional) (Identity v3 only) The name of an - application credential to authenticate with. Requires `user_id`, or - `user_name` and `user_domain_name` (or `user_domain_id`) to be set. - -* `application_credential_secret` - (Optional) (Identity v3 only) The secret of an - application credential to authenticate with. Required by - `application_credential_id` or `application_credential_name`. - -* `tenant_id` - (Optional) The ID of the Tenant (Identity v2) or Project - (Identity v3) to login with. If omitted, the `OS_TENANT_ID` or - `OS_PROJECT_ID` environment variables are used. - -* `tenant_name` - (Optional) The Name of the Tenant (Identity v2) or Project - (Identity v3) to login with. If omitted, the `OS_TENANT_NAME` or - `OS_PROJECT_NAME` environment variable are used. - -* `password` - (Optional) The Password to login with. If omitted, the - `OS_PASSWORD` environment variable is used. - -* `token` - (Optional; Required if not using `user_name` and `password`) - A token is an expiring, temporary means of access issued via the Keystone - service. By specifying a token, you do not have to specify a username/password - combination, since the token was already created by a username/password out of - band of Terraform. If omitted, the `OS_TOKEN` or `OS_AUTH_TOKEN` environment - variables are used. - -* `user_domain_name` - (Optional) The domain name where the user is located. If - omitted, the `OS_USER_DOMAIN_NAME` environment variable is checked. - -* `user_domain_id` - (Optional) The domain ID where the user is located. If - omitted, the `OS_USER_DOMAIN_ID` environment variable is checked. - -* `project_domain_name` - (Optional) The domain name where the project is - located. If omitted, the `OS_PROJECT_DOMAIN_NAME` environment variable is - checked. - -* `project_domain_id` - (Optional) The domain ID where the project is located - If omitted, the `OS_PROJECT_DOMAIN_ID` environment variable is checked. - -* `domain_id` - (Optional) The ID of the Domain to scope to (Identity v3). If - omitted, the following environment variables are checked (in this order): - `OS_USER_DOMAIN_ID`, `OS_PROJECT_DOMAIN_ID`, `OS_DOMAIN_ID`. - -* `domain_name` - (Optional) The Name of the Domain to scope to (Identity v3). - If omitted, the following environment variables are checked (in this order): - `OS_USER_DOMAIN_NAME`, `OS_PROJECT_DOMAIN_NAME`, `OS_DOMAIN_NAME`, - `DEFAULT_DOMAIN`. - -* `default_domain` - (Optional) The ID of the Domain to scope to if no other - domain is specified (Identity v3). If omitted, the environment variable - `OS_DEFAULT_DOMAIN` is checked or a default value of "default" will be - used. - -* `insecure` - (Optional) Trust self-signed SSL certificates. If omitted, the - `OS_INSECURE` environment variable is used. - -* `cacert_file` - (Optional) Specify a custom CA certificate when communicating - over SSL. You can specify either a path to the file or the contents of the - certificate. If omitted, the `OS_CACERT` environment variable is used. - -* `cert` - (Optional) Specify client certificate file for SSL client authentication. - If omitted the `OS_CERT` environment variable is used. - -* `key` - (Optional) Specify client private key file for SSL client authentication. - If omitted the `OS_KEY` environment variable is used. - -* `endpoint_type` - (Optional) Specify which type of endpoint to use from the - service catalog. It can be set using the OS_ENDPOINT_TYPE environment - variable. If not set, public endpoints is used. - -* `swauth` - (Optional) Set to `true` to authenticate against Swauth, a - Swift-native authentication system. If omitted, the `OS_SWAUTH` environment - variable is used. You must also set `username` to the Swauth/Swift username - such as `username:project`. Set the `password` to the Swauth/Swift key. This feature supports v1.0 of the Swauth system. - Finally, set `auth_url` as the location of the Swift service. - -* `disable_no_cache_header` - (Optional) If set to `true`, the HTTP - `Cache-Control: no-cache` header will not be added by default to all API requests. - If omitted this header is added to all API requests to force HTTP caches (if any) - to go upstream instead of serving cached responses. - -* `allow_reauth` - (Optional) If set to `true`, OpenStack authorization will be - perfomed automatically, if the initial auth token get expired. This is useful, - when the token TTL is low or the overall Terraform provider execution time - expected to be greater than the initial token TTL. - -* `archive_container` - (Optional) The container to create to store archived copies - of the Terraform state file. If specified, Swift [object versioning](https://docs.openstack.org/developer/swift/latest/overview_object_versioning.html) is enabled on the container created at `container`. - -* `archive_path` - (Optional) DEPRECATED: Use `archive_container` instead. - The path to store archived copied of `terraform.tfstate`. If specified, - Swift [object versioning](https://docs.openstack.org/developer/swift/latest/overview_object_versioning.html) is enabled on the container created at `path`. - -* `expire_after` - (Optional) How long should the `terraform.tfstate` created at `container` - be retained for? If specified, Swift [expiring object support](https://docs.openstack.org/developer/swift/latest/overview_expiring_objects.html) is enabled on the state. Supported durations: `m` - Minutes, `h` - Hours, `d` - Days. - ~> **NOTE:** Since Terraform is inherently stateful - we'd strongly recommend against auto-expiring Statefiles. diff --git a/website/docs/language/settings/index.mdx b/website/docs/language/settings/index.mdx deleted file mode 100644 index 213e01f1a2..0000000000 --- a/website/docs/language/settings/index.mdx +++ /dev/null @@ -1,139 +0,0 @@ ---- -page_title: Terraform Settings - Configuration Language -description: >- - The terraform block allows you to configure Terraform behavior, including the - Terraform version, backend, integration with Terraform Cloud, and required - providers. ---- - -# Terraform Settings - -The special `terraform` configuration block type is used to configure some -behaviors of Terraform itself, such as requiring a minimum Terraform version to -apply your configuration. - -## Terraform Block Syntax - -Terraform settings are gathered together into `terraform` blocks: - -```hcl -terraform { - # ... -} -``` - -Each `terraform` block can contain a number of settings related to Terraform's -behavior. Within a `terraform` block, only constant values can be used; -arguments may not refer to named objects such as resources, input variables, -etc, and may not use any of the Terraform language built-in functions. - -The various options supported within a `terraform` block are described in the -following sections. - -## Configuring Terraform Cloud - -The nested `cloud` block configures Terraform Cloud for enabling its -[CLI-driven run workflow](/cloud-docs/run/cli). - -- Refer to [Terraform Cloud Configuration](/language/settings/terraform-cloud) for a summary of the `cloud` block's syntax. - -- Refer to [Using Terraform Cloud](/cli/cloud) in the - Terraform CLI documentation for complete details about how to initialize and configure the Terraform Cloud CLI integration. - -## Configuring a Terraform Backend - -The nested `backend` block configures which state backend Terraform should use. - -The syntax and behavior of the `backend` block is described in [Backend -Configuration](/language/settings/backends/configuration). - -## Specifying a Required Terraform Version - -> **Hands-on:** Try the [Manage Terraform Versions](https://learn.hashicorp.com/tutorials/terraform/versions?in=terraform/configuration-language) or [Manage Terraform Versions in Terraform Cloud](https://learn.hashicorp.com/tutorials/terraform/cloud-versions?in=terraform/cloud) tutorials on HashiCorp Learn. - -The `required_version` setting accepts a [version constraint -string,](/language/expressions/version-constraints) which specifies which versions of Terraform -can be used with your configuration. - -If the running version of Terraform doesn't match the constraints specified, -Terraform will produce an error and exit without taking any further actions. - -When you use [child modules](/language/modules), each module can specify its own -version requirements. The requirements of all modules in the tree must be -satisfied. - -Use Terraform version constraints in a collaborative environment to -ensure that everyone is using a specific Terraform version, or using at least -a minimum Terraform version that has behavior expected by the configuration. - -The `required_version` setting applies only to the version of Terraform CLI. -Terraform's resource types are implemented by provider plugins, -whose release cycles are independent of Terraform CLI and of each other. -Use [the `required_providers` block](/language/providers/requirements) to manage -the expected versions for each provider you use. - -## Specifying Provider Requirements - -[inpage-source]: #specifying-provider-requirements - -The `required_providers` block specifies all of the providers required by the -current module, mapping each local provider name to a source address and a -version constraint. - -```hcl -terraform { - required_providers { - aws = { - version = ">= 2.7.0" - source = "hashicorp/aws" - } - } -} -``` - -For more information, see [Provider Requirements](/language/providers/requirements). - -## Experimental Language Features - -The Terraform team will sometimes introduce new language features initially via -an opt-in experiment, so that the community can try the new feature and give -feedback on it prior to it becoming a backward-compatibility constraint. - -In releases where experimental features are available, you can enable them on -a per-module basis by setting the `experiments` argument inside a `terraform` -block: - -```hcl -terraform { - experiments = [example] -} -``` - -The above would opt in to an experiment named `example`, assuming such an -experiment were available in the current Terraform version. - -Experiments are subject to arbitrary changes in later releases and, depending on -the outcome of the experiment, may change drastically before final release or -may not be released in stable form at all. Such breaking changes may appear -even in minor and patch releases. We do not recommend using experimental -features in Terraform modules intended for production use. - -In order to make that explicit and to avoid module callers inadvertently -depending on an experimental feature, any module with experiments enabled will -generate a warning on every `terraform plan` or `terraform apply`. If you -want to try experimental features in a shared module, we recommend enabling the -experiment only in alpha or beta releases of the module. - -The introduction and completion of experiments is reported in -[Terraform's changelog](https://github.com/hashicorp/terraform/blob/main/CHANGELOG.md), -so you can watch the release notes there to discover which experiment keywords, -if any, are available in a particular Terraform release. - -## Passing Metadata to Providers - -The `terraform` block can have a nested `provider_meta` block for each -provider a module is using, if the provider defines a schema for it. This -allows the provider to receive module-specific information, and is primarily -intended for modules distributed by the same vendor as the associated provider. - -For more information, see [Provider Metadata](/internals/provider-meta). diff --git a/website/docs/language/settings/terraform-cloud.mdx b/website/docs/language/settings/terraform-cloud.mdx deleted file mode 100644 index bdfe6a3e64..0000000000 --- a/website/docs/language/settings/terraform-cloud.mdx +++ /dev/null @@ -1,36 +0,0 @@ ---- -page_title: Terraform Cloud Configuration - Terraform Settings - Configuration Language -description: >- - The nested `cloud` block configures Terraform's integration with Terraform - Cloud. ---- - -# Terraform Cloud Configuration - -The main module of a Terraform configuration can integrate with Terraform Cloud to enable its -[CLI-driven run workflow](/cloud-docs/run/cli). You only need to configure these settings when you want to use Terraform CLI to interact with Terraform Cloud. Terraform Cloud ignores them when interacting with -Terraform through version control or the API. - -> **Hands On:** Try the [Migrate State to Terraform Cloud](https://learn.hashicorp.com/tutorials/terraform/cloud-migrate) tutorial on HashiCorp Learn. - -You can configure the Terraform Cloud CLI integration by adding a nested `cloud` block within the top-level -`terraform` block: - -```hcl -terraform { - cloud { - organization = "example_corp" - hostname = "app.terraform.io" # Optional; defaults to app.terraform.io - - workspaces { - tags = ["app"] - } - } -} -``` - -If you do not specify the `hostname`, it defaults to `app.terraform.io` for Terraform Cloud. For Terraform Enterprise installations, include the [hostname](/cli/cloud/settings#hostname) configuration argument. - -You cannot use the CLI integration and a [state backend](/language/settings/backends) in the same configuration; they are mutually exclusive. A configuration can only provide one `cloud` block and the `cloud` block cannot refer to named values like input variables, locals, or data source attributes. - -Refer to [Using Terraform Cloud](/cli/cloud) in the Terraform CLI docs for more information. diff --git a/website/docs/language/stacks/create/config.mdx b/website/docs/language/stacks/create/config.mdx new file mode 100644 index 0000000000..eb62f63b77 --- /dev/null +++ b/website/docs/language/stacks/create/config.mdx @@ -0,0 +1,91 @@ +--- +page_title: Define configuration +description: Learn to write Stack configuration files to declare what infrastructure your Stack deploys. +--- + +# Define configuration + +In the Stack configuration file, you declare what infrastructure components are part of the Stack. + +Your Stack configuration file defines multiple components that share a lifecycle you can repeatedly deploy. This helps ensure consistency across environments and reduces the complexity of provisioning at scale. + +## Background + +You declare the infrastructure that makes up your Stack in the Stack configuration file. Stack configuration files replace Terraform’s traditional root module and serve as the blueprint for what your Stack deploys. + +Stack configuration files use a new file type, `tfstack.hcl`, to define everything that shares your Stack’s lifecycle. After writing your Stack configuration, you can write a deployment configuration to dictate how HCP Terraform deploys your Stack’s infrastructure. + +As with Terraform configuration files, HCP Terraform processes all of the blocks in your Stack configuration and deployment configuration files in your Stack's root directory in dependency order. You can organize your Stack configuration into multiple files as in traditional Terraform configurations. + +### Requirements + +Before you begin writing your Stack configuration, ensure you have the `terraform-stacks-cli` for initializing and validating your Stack configurations. For installation guidance, refer to the [Stacks CLI reference](/terraform/language/stacks/reference/tfstacks-cli). + +## Define your Stack configuration + +We recommend [designing your Stack](/terraform/language/stacks/design) before you begin writing your configuration files. + +All of your Stack’s configuration files must use the `.tfstack.hcl` file type. You can set up your Stack configuration into multiple files as in traditional Terraform configurations. For example, you can have `variables.tfstack.hcl`, `providers.tfstack.hcl`, and we recommend creating one root-level file for your `component` blocks, such as `components.tfstack.hcl`. + +The `component` block defines the pieces that make up your Stack. Add a `component` block for each top-level module you want to include in the Stack. Specify the source module, inputs, and providers for each component. + +```hcl +# components.tfstack.hcl + +component "cluster" { + source = "./eks" + inputs = { + aws_region = var.aws_region + cluster_name_prefix = var.prefix + instance_type = "t2.medium" + } + providers = { + aws = provider.aws.this + random = provider.random.this + tls = provider.tls.this + cloudinit = provider.cloudinit.this + } +} +``` + +After establishing your top-level modules, you can use child modules without adding additional `component` blocks. + +The Stack configuration file type accepts most classic Terraform configuration blocks but with some key differences. For more details on declaring providers in Stacks, refer to [Declare providers](/terraform/language/stacks/create/declare-providers). For more information on the Stack configuration file type and all the blocks and attributes it accepts, refer to the [Configuration file reference](/terraform/language/stacks/reference/tfstack). + +The `component` block supports the `for_each` meta-argument if you need to replicate components across multiple instances. For example, the following configuration uses `for_each` to provision modules in multiple AWS regions for a given environment. + +```hcl +# components.tfstack.hcl + +component "s3" { + for_each = var.regions + + source = "./s3" + + inputs = { + region = each.value + } + + providers = { + aws = provider.aws.configurations[each.value] + random = provider.random.this + } +} +``` + +After writing your Stack configuration, use the Terraform Stacks CLI to validate it. + +## Validate your configuration + +Once you have finished your Stack configuration, use the `terraform-stacks-cli` tool to validate your configuration and generate the necessary provider lock files. + +```shell-session +$ tfstacks init +$ tfstacks validate +``` + +For installation guidance and more details, refer to the [Stacks CLI](/terraform/language/stacks/reference/tfstacks-cli). + +## Next steps + +If you have not yet defined the providers for your Stack, proceed to [Declare providers](/terraform/language/stacks/create/declare-providers). You can also learn how to [Authenticate your Stack](/terraform/language/stacks/deploy/authenticate) to ensure your providers are properly set up. diff --git a/website/docs/language/stacks/create/declare-providers.mdx b/website/docs/language/stacks/create/declare-providers.mdx new file mode 100644 index 0000000000..188e8beff8 --- /dev/null +++ b/website/docs/language/stacks/create/declare-providers.mdx @@ -0,0 +1,152 @@ +--- +page_title: Declare providers +description: Learn how to declare providers in Stack configurations. +--- + +# Declare providers + +Terraform relies on plugins called providers to interact with cloud providers, SaaS providers, and other APIs. + +Like traditional Terraform configurations, Terraform Stack configurations declare which providers they require at the top level so that Terraform can install and use them. + +Providers in Stack configurations differ from normal Terraform configurations in the following ways: +* Modules sourced by `component` blocks cannot declare their own providers. Instead, each `component` block accepts a top-level map of providers. +* You must pass attributes to providers using a `config` block. +* You define provider alias names in the header of its block. +* Providers in Stack configurations support the [`for_each`](/terraform/language/meta-arguments/for_each) meta argument. + +After defining your providers, you must use the Terraform Stacks CLI to install the providers and create a provider lock file before you can use deploy your Stack. + +## Use a provider in a Stack + +Define your Stack’s `provider` blocks in a top-level `.tfstack.hcl` file. The following example file named `providers.tfstack.hcl` defines an AWS provider. + +```hcl +# providers.tfstack.hcl + +required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.7.0" + } +} + +# Setting "this" as the alias name +provider "aws" "this" { + config { + region = var.region + + assume_role_with_web_identity { + role_arn = var.role_arn + web_identity_token = var.identity_token + } + + default_tags { + tags = var.default_tags + } + } +} +``` + +When you define a provider, you must also add that provider to a `required_providers` to ensure the Terraform Stacks CLI knows which provider version to download. For more details on Stack-specific provider syntax, refer to the [Stack configuration file reference](/terraform/language/stacks/reference/tfstack). + +You must authenticate that provider to ensure the provider can create your infrastructure. Refer to [Authenticate a Stack](/terraform/language/stacks/deploy/authenticate) for details and examples. + +After you define your providers, you can pass them to your Stack’s `component` blocks. A `component` block accepts a mapping of provider names to the provider(s) on which your component’s module relies. + +After defining your Stack’s providers, pass each `component` block the provider(s) it requires. + +```hcl +# components.tfstack.hcl + +component "s3" { + source = "./s3" + inputs = { + aws_region = var.aws_region + } + providers = { + aws = provider.aws.this + } +} +``` + +After configuring your provider, you can use the Terraform Stacks CLI to [generate a provider lock file](/terraform/language/stacks/reference/tfstacks-cli#tfstack-providers-lock-command). + +### Dynamic provider configurations + +Unlike traditional Terraform providers, Stack providers also support the `for_each` meta-argument. The `for_each` meta-argument lets you dynamically create provider configurations based on your inputs, which is beneficial for multi-region deployments. + +```hcl +# providers.tfstack.hcl + +required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.7.0" + } +} + + +provider "aws" "configurations" { +# This provider configuration iterates through and creates a configuration +# for each region. + for_each = var.regions + + config { + region = each.value + + assume_role_with_web_identity { + role_arn = var.role_arn + web_identity_token = var.identity_token + } + } +} +``` + +This example lets your Stack’s deployments use a separate AWS provider configuration for each region you defined in a `regions` variable. + +In your component configuration, you could the `for_each` block to define configuration in with multiple AWS providers. + + +```hcl +# components.tfstack.hcl + +component "s3" { + for_each = var.regions + + source = "./s3" + + inputs = { + region = each.value + } + + providers = { + aws = provider.aws.configurations[each.value] + random = provider.random.this + } +} +``` + +The components in this example use the `for_each` meta-argument to deploy their Terraform module in the region for the current deployment. + +### Provider support for Stack features + +For most providers, the version you use does not affect your Stack’s functionality. However, only certain providers and versions support [deferred changes](/terraform/cloud-docs/stacks/deploy/plans#deferred-changes). For example, you must use version 2.32.0 or higher of the Kubernetes provider to take advantage of deferred changes. + +Check your provider in the [Terraform registry](https://registry.terraform.io/) for more information on versions that support various Stack features. + +## Provider lock file + +A Stack cannot run without a lock file for its providers. After defining your providers, use the Terraform Stacks CLI to generate a `.terraform.lock.hcl` lock file. The `tfstacks providers lock` command creates and updates your `.terraform.lock.hcl` file. + +```shell-session +$ tfstacks providers lock +``` + +The `tfstacks providers lock` command uses the `required_providers` block from your configuration to download the listed providers and compute the provider lock hashes. The `tfstacks providers lock` command only checks the `required_providers` block, so you must list all of the providers you use in that block to ensure that the `tfstacks` CLI can find them. + +If you update your Stack’s providers, you must rerun the `tfstacks providers lock` command to update your Stack’s providers lock file. For more details, refer to [Terraform Stacks CLI](/terraform/language/stacks/reference/tfstacks-cli). + +## Next steps + +After declaring your providers, you can either finish [defining the rest of your Stack configuration](/terraform/language/stacks/create/config) or move on to [define your Stack’s deployment configuration](/terraform/language/stacks/deploy/config). diff --git a/website/docs/language/stacks/deploy/authenticate.mdx b/website/docs/language/stacks/deploy/authenticate.mdx new file mode 100644 index 0000000000..441900179e --- /dev/null +++ b/website/docs/language/stacks/deploy/authenticate.mdx @@ -0,0 +1,309 @@ +--- +page_title: Authenticate a Stack +description: Learn how to set up OIDC authentication or use the store block to authenticate Stacks with their providers. +--- + +# Authenticate a Stack + +You can authenticate a Stack in two ways: by using OIDC to dynamically authenticate with the built-in `identity_token` block or by accessing existing authentication credentials with the `store` block. We recommend authenticating your Stack with OIDC because using static credentials to authenticate providers presents a security risk, even if you rotate your credentials regularly. + +## Authenticate with OIDC + +[OpenID Connect](https://openid.net/connect/) (OIDC) is an identity layer on top of the OAuth 2.0 protocol. You can use HCP Terraform’s [Workload identity](/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens) tokens, built on the OpenID Connect protocol, to securely connect and authenticate your Stacks with cloud providers. + +-> **Hands on**: Walk through setting up OIDC for a Stack in the [Deploy a Stack with HCP Terraform](/terraform/tutorials/cloud/stacks-deploy) tutorial. + +Stacks have a built-in `identity_token` block that creates workload identity tokens, also known as JWT tokens. You can use these tokens to authenticate Stacks with Terraform providers securely. + +This guide covers how to set up OIDC for Stacks using the `identity_token` block. + +### Overview + +The details of Stack authentication differ based on which cloud provider you are setting up, but the basic steps remain the same: + +1. Set up the trust configuration between your cloud provider and HCP Terraform, which usually includes creating roles and policies for your cloud provider. +1. Add an `identity_token` block to your Stack’s deployment file using the audience and roles you created in the previous step. + +Your deployments can reference the value of the `identity_token` block to pass the trust relationship role to your Stack’s operations. + +### Configure trust configuration + +In this example, you will set up an example trust policy with AWS. For examples of setting up trust policies with other providers, refer to our [repository of example configurations](https://github.com/hashicorp-guides/terraform-stacks-identity-tokens/tree/main). + +We recommend using Terraform to create and configure the trust relationship and permissions for the cloud provider you want to authenticate with. Using the following Terraform configuration, [create a new workspace](/terraform/cloud-docs/workspaces/create) and configure the `aws_region`, `tf_organization`, `tf_project,` and `tf_stack` variables for your specific set up. + +```hcl +# main.tf + +variable "aws_region" { + type = string + description = "The AWS region to create the role in." +} + +variable "tf_organization" { + type = string + description = "The name of the organization that this workspace and Stack live in." +} + +variable "tf_project" { + type = string + description = "The name of the project that this workspace and Stack live in." +} + +variable "tf_stack" { + type = string + description = "The name of the Stack you will you use this token in." +} + +provider "aws" { + region = var.aws_region +} + +resource "aws_iam_openid_connect_provider" "stacks_openid_provider" { + url = "https://app.terraform.io" + client_id_list = ["aws.workload.identity"] + + # This is the thumbprint of https://app.terraform.io as of 2024/08/07 + # Refer to "Adjust access of trust" to learn how to update this thumbprint + thumbprint_list = ["9e99a48a9960b14926bb7f3b02e22da2b0ab7280"] +} + +resource "aws_iam_role" "stacks_role" { + name = "stacks-${var.tf_organization}-${var.tf_project}-${var.tf_stack}" + assume_role_policy = data.aws_iam_policy_document.stacks_role_policy.json +} + +data "aws_iam_policy_document" "stacks_role_policy" { + statement { + effect = "Allow" + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.stacks_openid_provider.arn] + } + actions = ["sts:AssumeRoleWithWebIdentity"] + condition { + test = "StringEquals" + variable = "app.terraform.io:aud" + values = ["aws.workload.identity"] + } + condition { + test = "StringLike" + variable = "app.terraform.io:sub" + # This value dictates which HCP Terraform organizations, projects, + # and stacks can assume the new role you are creating. + # + # You can widen access to an entire organization or project by + # tweaking the value below. You can also restrict access to specific + # deployments or operations. Refer to Configure trust for more information. + values = ["organization:${var.tf_organization}:project:${var.tf_project}:stack:${var.tf_stack}:*"] + } + } +} + +# Now, you give the new role access to things you want to manage in your Stack. +# +# The policies below are too broad for a production use case, but you set them +# broadly for now to ensure this Stacks can do anything during development and +# testing. In practice, only give your Stack access to what it needs to manage. + +resource "aws_iam_role_policy_attachment" "iam" { + role = aws_iam_role.stacks_role.name + policy_arn = "arn:aws:iam::aws:policy/IAMFullAccess" +} + +resource "aws_iam_role_policy_attachment" "sudo" { + role = aws_iam_role.stacks_role.name + policy_arn = "arn:aws:iam::aws:policy/PowerUserAccess" +} + +# Your workspace returns this output role, which you use to configure your +# deployments. +output "role_arn" { + value = aws_iam_role.stacks_role.arn +} +``` + +After setting up your workspace in HCP Terraform, perform a plan and apply operation. HCP Terraform will create your OpenID provider, policy, and role before outputting your new role’s ARN with the `role_arn` output. Copy your new AWS ARN role because this is the value you use to configure your cloud provider with your Stack and HCP Terraform. + +#### Adjust access of trust + +Workload identity tokens contain useful metadata in their payloads, known as claims. Once a cloud platform verifies a token using HCP Terraform’s public key, it can look at the identity token's claims to match it to the correct permissions or reject it. + +You do not need to understand the full token specification or what every claim means, but you can use the claims to adjust your trust relationship. + +Workload identity tokens commonly reference the following claims. + +| Claim | Explanation | +| :---- | :---- | +| `jti` (JWT ID) | A unique identifier for each token, which is a UUID. | +| `iss` (issuer) | Full URL of the HCP Terraform instance that signed the token. For example, “https://app.terraform.io”. | +| `iat` (issued at) | Unix timestamp when the token was issued. May be required by certain relying parties. | +| `nbf` (not before) | Unix Timestamp when the token can start being used. Should be the same as `iat` for your purposes and may be required by certain relying parties. | +| `aud` (audience) | Intended audience for the token. For example, “api://AzureADTokenExchange” for Azure. | +| `exp` (expiration) | Unix timestamp based on the timeout of the operation phase that it was issued for. | +| `sub` (subject) | Fully qualified path to a Stack deployment, followed by the operation type:
`organization::project::stack::deployment:my-deployment-name:operation:`

For example:
`organization:My_Org:project:My_Project:stack:My_Stack:deployment:staging:operation:apply` | + +You can further customize your token’s permissions using the following custom claims. + +| Claim | Explanation | +| :---- | :---- | +| `terraform_operation` ("run phase") | The Terraform operation this token was issued for. For example, “plan” or “apply”. | +| `terraform_stack_deployment_name` | Name of the deployment that the operation is for. | +| `terraform_stack_id` (stack ID) | External ID of the Stack that the operation is for. | +| `terraform_stack_name` (stack name) | Name of the Stack that the operation is for. | +| `terraform_project_id` (project ID) | External ID of the project that the operation is taking place in. Useful if you want to use the same role across Stacks in a given project. | +| `terraform_project_name` (project name) | Name of the project that the operation is taking place in. | +| `terraform_organization_id` (organization ID) | External ID of the HCP Terraform organization that the operation is taking place in. Useful if you are in a large company that may have more than one organization or if you want to use the same role across your entire organization. | +| `terraform_organization_name` (organization name) | Name of the HCP Terraform organization that the operation is taking place in. | +| `terraform_plan_id` (plan ID) | External ID of the plan that this token is being used for creating or applying. This might be used to track down where a token has been leaked from or to block a specific token if it has been leaked. | + +You can also update your configuration to use the current thumbprint for HCP Terraform by running the following command and removing the colons from the value it returns. + +```shell-session +$ echo | openssl s_client -showcerts -servername app.terraform.io -connect app.terraform.io:443 2>/dev/null | openssl x509 -fingerprint -noout -sha1 +sha1 Fingerprint=FD:88:23:AE:FB:96:13:28:A1:34:70:6D:C8:57:5A:17:0F:0B:B3:7C +``` + +### Configure HCP Terraform + +Stacks use the [`identity_token` block](/terraform/language/stacks/reference/tfdeploy#identity_token-block-configuration) to define a JSON Web Token (JWT) for a specific deployment. This token enables OIDC-based authentication, allowing your Stack deployments to securely connect to cloud providers like AWS, Azure, and GCP. + +When you define the `identity_token` block, you specify its audience. For example, the example Stack deployment configuration defines an `identity_token` block specifying the AWS audience. + +```hcl +identity_token "aws" { + audience = ["aws.workload.identity"] +} +``` + +Once defined, you can reference an identity token in a deployment's input variables and reference that token in a provider’s configuration. + +You also need to add your trust relationship, your role ARN, to configure your token with the trust relationship to authenticate AWS resources. + +```hcl +# deployments.tfdeploy.hcl + +identity_token "aws" { + audience = ["aws.workload.identity"] +} + +deployment "development" { + inputs = { + role_arn = "" + identity_token = identity_token.aws.jwt + } +} +``` + +Now, the deployments are authenticated with AWS and can pass the trust configuration to your Stack’s providers for authentication. + +```hcl +# variables.tfstack.hcl + +variable "role_arn" { + type = string +} + +variable "identity_token" { + type = string + ephemeral = true +} + +# providers.tfstack.hcl + +required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.7.0" + } +} + +provider "aws" "this" { + config { + region = var.region + assume_role_with_web_identity { + role_arn = var.aws_role + web_identity_token = var.aws_token + } + } +} +``` + +With this setup, each of your deployments pass the ARN of your trust relationship role and a JWT token generated by AWS each time you plan or apply your Stack. Each of your deployments uses their own role, configured with the specific permissions you require. + +For more examples of setting up OIDC with different providers, refer to our [repository of example configurations](https://github.com/hashicorp-guides/terraform-stacks-identity-tokens/tree/main). + +## Authenticate with existing credentials + +You can use the `store` block to define key-value secrets in your deployment configuration and then access those values in your deployments. + +You can use the `store` block to access credentials stored in an [HCP Terraform variable set](/terraform/cloud-docs/workspaces/variables/managing-variables#variable-sets). This example uses a `store` block with a variable set to access credentials stored in HCP Terraform. For more information about using `store` blocks, refer to the [Deployment configuration reference](/terraform/language/stacks/reference/tfdeploy). + +Before writing any configuration, ensure your Stack can access the variable set you are targeting by allowing your project to access the set or making the set globally available. + +Next, add a `store` block to your deployment configuration file to access your variable set. Once defined, your deployments can access your variable set’s values. + +```hcl +# deployments.tfdeploy.hcl + +# Source environment secrets from your HCP Terraform variable set +store "varset" "tokens" { + id = "varset-" + category = "env" +} + +# Access your variable set's value using your store and pass them into your +# deployment's inputs. +deployment "test" { + inputs = { + access_key = store.varset.tokens.AWS_ACCESS_KEY_ID + secret_key = store.varset.tokens.AWS_SECRET_ACCESS_KEY + session_token = store.varset.tokens.AWS_SESSION_TOKEN + } +} +``` + +For example, if your `test` deployment can use your `store` to access your variable set’s keys, and therefore, your variable set’s AWS provider credentials. + +Your `test` deployment can define your AWS credentials as `inputs`, allowing your `providers` to access those credentials. + +```hcl +# providers.tfstack.hcl + +variable "access_key" { + description = "AWS access key" + type = string + ephemeral = true +} + +variable "secret_key" { + description = "AWS sensitive secret key." + type = string + sensitive = true + ephemeral = true +} + +variable "session_token" { + description = "AWS session token." + type = string + sensitive = true + ephemeral = true +} + +required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.7.0" + } +} + +provider "aws" "this" { + config { + access_key = var.access_key + secret_key = var.secret_key + token = var.session_token + } +} +``` + +This example allows your `aws.this` provider to authenticate to AWS using the credentials from a variable set stored in HCP Terraform. For more information about using `store` blocks, refer to the [Deployment configuration reference](/terraform/language/stacks/reference/tfdeploy). \ No newline at end of file diff --git a/website/docs/language/stacks/deploy/conditions.mdx b/website/docs/language/stacks/deploy/conditions.mdx new file mode 100644 index 0000000000..b14dbbc590 --- /dev/null +++ b/website/docs/language/stacks/deploy/conditions.mdx @@ -0,0 +1,107 @@ +--- +page_title: Set conditions for deployment plans +description: Learn how to use the orchestrate block to automatically approve Stack deployment plans and help you manage your Stack deployments at scale. +--- + +# Set conditions for deployment plans + +Orchestration rules allow you to automate aspects of your Stack deployments, such as auto-approving plans when those plans meet certain conditions. By setting orchestration rules, you can help streamline managing many deployments. + +## Create orchestration rules + +Add `orchestrate` blocks to define rules for handling deployment plans in your deployment configuration file (`tfdeploy.hcl`). + +The `orchestrate` block uses a `check` block to determine if a condition has passed or not, and every condition must pass for the `orchestrate` rule to take effect. For details on the syntax of an `orchestrate` block, refer to the [Deployment configuration file reference](/terraform/language/stacks/reference/tfdeploy). + +### Orchestrate rule types + +There are two orchestration rule types to choose from: + +1. The `auto_approve` rule executes after a Stack creates a plan and automatically approves a plan if all checks pass. +1. The `replan` rule executes after a Stack applies a plan, automatically triggering a replan if all the checks pass. + +The `auto_approve` rule is helpful when you know you want a certain type of plan to go ahead without manual intervention. For example, Stacks have a default `auto_approve` rule named `empty_plan`, which automatically approves a plan if it has no changes. + +As another example, you could create a rule that automatically approves deployments if a component has not changed. + +```hcl +# deployments.tfdeploy.hcl + +orchestrate "auto_approve" "no_pet_changes" { + check { + # Check that the pet component has no changes + condition = context.plan.component_changes["component.pet"].total == 0 + reason = "Not automatically approved because changes proposed to pet component." + } +} +``` + +The above `orchestrate` block automatically approves plans if they do not include any changes for the `pet` component. + +The `replan` rule is helpful when you need your Stack to perform another plan after it applies the last one. You could create a rule to automatically replan a deployment if the production deployment errors out. + +```hcl +# deployments.tfdeploy.hcl + +orchestrate "replan" "replan_prod_for_errors" { + check { + condition = context.plan.deployment == deployment.production + reason = "Only automatically replan production deployments." + } + + check { + condition = context.plan.applyable == false + reason = "Only automatically replan plans that were not applyable." + } + + check { + condition = context.plan.replans < 2 + reason = "Only automatically replan failed plans once." + } +} +``` + +The above `orchestrate` block triggers a replan if the production deployment of a Stack errors out and if HCP Terraform hasn't already attempted to replan. + +### Leverage orchestration context + +The `orchestrate` block can use the `context` variable, which provides access to detailed information about the deployment plan, such as the number of changes, the specific components involved, and the deployment’s status. + +Use `context` to create more sophisticated checks and conditions. For example, you can automatically approve all planning operations. + +```hcl +# deployments.tfdeploy.hcl + +orchestrate "auto_approve" "allow_plans" { + check { + # Check that the operation is a plan + condition = context.operation == "plan" + reason = "Apply operations need manual approval." + } +} +``` + +Another example of using `context` is automatically approving plans that do not remove resources in non-production deployments. + +```hcl +# deployments.tfdeploy.hcl +orchestrate "auto_approve" "safe_plans" { + # Ensure that no resource is removed. + check { + condition = context.plan.changes.remove == 0 + reason = "Plan is destroying ${context.plan.changes.remove} resources." + } + + # Ensure that the deployment is not your production environment. + check { + condition = context.plan.deployment != deployment.production + reason = "Production plans are not eligible for auto_approve." + } +} +``` + +For more information on the information available in the context variable, refer to the [Deployment configuration file reference](/terraform/language/stacks/reference/tfdeploy). + +## Next steps + +With your Stack and deployment configurations complete, your Stack is ready to be deployed by HCP Terraform. To learn more about creating a Stack in HCP Terraform, refer to [Create a Stack](/terraform/cloud-docs/stacks/create). diff --git a/website/docs/language/stacks/deploy/config.mdx b/website/docs/language/stacks/deploy/config.mdx new file mode 100644 index 0000000000..e09b3c3f4d --- /dev/null +++ b/website/docs/language/stacks/deploy/config.mdx @@ -0,0 +1,115 @@ +--- +page_title: Define deployment configuration +description: Learn how to write stack deployment configuration files to define how to deploy your stack’s infrastructure. +--- + +# Define deployment configuration + +In Stacks, deployments allow you to replicate infrastructure across multiple environments, regions, or accounts. Each deployment runs in its own isolated agent, ensuring that changes in one deployment do not affect others. + +-> **Note**: HCP Terraform supports up to a maximum of 20 deployments. + +Learn how to write deployment configuration files that allow you to specify the environments, regions, and other parameters that will vary across deployments, while keeping your stack’s configuration nonrepetitive and reusable. + +## Background + +The deployment configuration file (`tfdeploy.hcl`) defines how many times HCP Terraform should deploy a stack’s infrastructure. A stack must have at least one `deployment` block. Adding a new `deployment` block tells Terraform to redeploy a stack’s infrastructure with that `deployment` block’s input values. + +You can automatically manage plans for your deployments by defining orchestrate blocks to create orchestration rules for your stack deployments. For example, you can set up automatic approvals when a deployment meets specific criteria. Learn more about orchestrating deployments in [Set conditions for deployment plans](/terraform/language/stacks/deploy/conditions). + +## Write a deployment configuration file + +Deployment configuration files live at the top level of your repository and use the `.tfdeploy.hcl` file extension. We recommend naming your deployments file `deployments.tfdeploy.hcl`. In your deployment configuration file, create a new `deployment` block for each environment or region where you want to deploy your stack. + +```hcl +# deployments.tfdeploy.hcl + +deployment "production" { + inputs = { + aws_region = "us-west-1" + instance_count = 2 + } +} +``` + +The `deployment` block accepts a map of `inputs` where you can specify any unique configurations that deployment may need. The `deployment` block does not accept any meta-arguments. + +```hcl +# deployments.tfdeploy.hcl + +deployment "production" { + inputs = { + aws_region = "us-west-1" + instance_count = 2 + } +} + +deployment "staging" { + inputs = { + aws_region = "us-west-1" + instance_count = 1 + } +} +``` + +You can also specify `identity_token` blocks and `orchestrate` blocks in a deployment configuration file. + +## Deployment authentication + +The `identity_token` block tells HCP Terraform to generate a JSON Web Token (JWT) for that deployment’s components to authenticate to providers using OIDC. For example, you can set up your deployment to authenticate with the AWS provider using a particular role ARN. + +```hcl +# deployments.tfdeploy.hcl + +identity_token "aws" { + audience = ["aws.workload.identity"] +} + +deployment "staging" { + inputs = { + aws_region = "eu-west-1" + role_arn = "arn:aws:iam::123456789101:role/my-oidc-role" + aws_token = identity_token.aws.jwt + } +} +``` + +That deployment generates a JWT token named `aws_token`, which is available to that deployment’s components and providers. + +```hcl +# providers.tfstack.hcl + +provider "aws" "this" { + config { + region = var.aws_region + assume_role_with_web_identity { + role_arn = var.aws_role + web_identity_token = var.aws_token + } + } +} +``` + +For more details and examples of using the `identity_token` block, refer to [Authenticate a stack](/terraform/language/stacks/deploy/authenticate). + +## Automatically manage deployments + +The `orchestrate` block lets you codify your stack’s behavior by creating orchestration rules, enabling you to manage deployments at scale. For example, you could set an orchestration rule to automatically approve plans or apply operations that don’t contain any changes. + +```hcl +# deployments.tfdeploy.hcl + +orchestrate "auto_approve" "no_pet_changes" { + check { + # Check that the pet component has no changes + condition = context.plan.component_changes["component.pet"].total == 0 + reason = "Changes proposed to pet component." + } +} +``` + +For more information and examples of using the `orchestrate` block, refer to [Set conditions for deployment plans](/terraform/language/stacks/deploy/conditions). + +## Next steps + +Refer to the [Deployment file reference](/terraform/language/stacks/reference/tfdeploy) to learn more about the syntax of the blocks and attributes you can use in deployment configuration files. Refer to [Create a stack](/terraform/cloud-docs/stacks/create) to learn how to deploy your stack’s infrastructure with HCP Terraform. diff --git a/website/docs/language/stacks/deploy/pass-data.mdx b/website/docs/language/stacks/deploy/pass-data.mdx new file mode 100644 index 0000000000..1f8d6e2921 --- /dev/null +++ b/website/docs/language/stacks/deploy/pass-data.mdx @@ -0,0 +1,112 @@ +--- +page_title: Pass data from one Stack to another +description: Learn how to link Stacks together to pass data from one Stack to another using `publish_output` blocks to export data and `upstream_input` blocks to consume that data. +--- + +# Pass data from one Stack to another + +If you have multiple Stacks that do not share a provisioning lifecycle, you can link Stacks together to export data from one Stack for another to consume. If the output value of a Stack changes after a run, HCP Terraform automatically triggers runs for any Stacks that depend on those outputs. + +## Background + +You may need to pass data between different Stacks in your project. For example, one Stack in your organization may manage shared services, such as networking infrastructure, and another Stack may manage application components. Using separate Stacks lets you manage the infrastructure independently, but you may still need to share data from your networking Stack to your application Stack. + +To output information from a Stack, declare a `publish_output` block in the deployment configuration of the Stack exporting data. We refer to the Stack that declares a `publish_output` block as the upstream Stack. + +To consume the data exported by the upstream Stack, declare an `upstream_input` block in the deployment configuration of a different Stack in the same project. We refer to the Stack that declares an `upstream_input` block as the downstream Stack. + +## Requirements + +The `publish_output` and `upstream_input` blocks require at least Terraform version `terraform_1.10.0-alpha20241009` or higher. Download the [latest version of Terraform](https://releases.hashicorp.com/terraform/) to use the most up-to-date functionality. + +Downstream Stacks must also reside in the same project as their upstream Stacks. + +## Declare outputs + +You must declare a `publish_output` block in your deployment configuration for each value you want to output from your current Stack. + +For example, you can add a `publish_output` block for the `vpc_id` in your upstream Stack’s deployment configuration. You can directly reference a deployment's values with the `deployment.deployment_name` syntax. + + + +```hcl +# Networking Stack deployment configuration + +publish_output "vpc_id" { + description = "The networking Stack's VPC's ID." + value = deployment.network.vpc_id +} +``` + + + +After applying your configuration, any Stack in the same project can now reference your network deployment's `vpc_id` output by declaring an `upstream_input` block. + +Once you apply a Stack configuration version that includes your `publish_output` block, HCP Terraform publishes a snapshot of those values, which allows HCP Terraform to resolve them. Meaning, you must apply your Stack’s deployment configuration before any downstream Stacks can reference your Stack's outputs. + +Learn more about the [`publish_output` block](/terraform/language/stacks/reference/tfdeploy#publish_output-block-configuration). + +## Consume the output from an upstream Stack + +Declare an `upstream_input` block in your Stack’s deployment configuration to read values from another Stack's `publish_output` block. Adding an `upstream_input` block creates a dependency on the upstream Stack. + +For example, if you want to use the output `vpc_id` from an upstream Stack in the same project, declare an `upstream_input` block in your deployment configuration. + + + +```hcl +# Application Stack deployment configuration + +upstream_input "network_stack" { + type = "stack" + source = "app.terraform.io/hashicorp/Default Project/networking-stack" +} + +deployment "application" { + inputs = { + vpc_id = upstream_input.network_stack.vpc_id + } +} +``` + + + +After pushing your Stack's configuration into HCP Terraform, HCP Terraform searches for the most recently published snapshot of the upstream Stack your configuration references. If no snapshot exists, the downstream Stack's run fails. + +If HCP Terraform finds a published snapshot for your referenced upstream Stack, then all of that Stack's outputs are available to this downstream Stack. Add `upstream_input` blocks for every upstream Stack you want to reference. Learn more about the [`upstream_input` block](/terraform/language/stacks/reference/tfdeploy#upstream_input-block-configuration). + + +## Trigger runs when output values change + +If an upstream Stack's published output values change, HCP Terraform automatically triggers runs for any downstream Stacks that rely on those outputs. + +In the following example, the `application` deployment depends on the upstream networking Stack. + + + +```hcl +# Application Stack deployment configuration + +upstream_input "network_stack" { + type = "stack" + source = "app.terraform.io/hashicorp/Default Project/networking-stack" +} + +deployment "application" { + inputs = { + vpc_id = upstream_input.network_stack.vpc_id + } +} +``` + + + +The application Stack depends on the networking Stack’s output, so if the `vpc_id` changes then HCP Terraform triggers a new run for the application Stack. This approach allows you to decouple Stacks that have separate life cycles and ensures that updates in an upstream Stack propagate to downstream Stacks. + +## Remove upstream Stack dependencies + +To stop depending on an upstream Stack’s outputs, do the following in your downstream Stack's deployment configuration: + +1. Remove the upstream Stack's `upstream_input` block +1. Remove any references to the upstream Stack's outputs +1. Push your configuration changes to HCP Terraform and apply the new configuration diff --git a/website/docs/language/stacks/design.mdx b/website/docs/language/stacks/design.mdx new file mode 100644 index 0000000000..480d05852a --- /dev/null +++ b/website/docs/language/stacks/design.mdx @@ -0,0 +1,66 @@ +--- +page_title: Design a Stack +description: Learn how to design a Stack that aligns with your existing infrastructure and deployment needs. +--- + +# Design a Stack + +Stacks allow you to break down your existing infrastructure into logical components, making it easier to deploy and manage as a cohesive system. Designing a Stack begins with planning how to structure your infrastructure and thinking about how you intend to deploy the resources your Stack defines. + +## Background + +Every Stack requires two files: a Stack configuration file and a deployment configuration file. The Stack configuration file defines the infrastructure your Stack will provision when you deploy it. The deployment configuration files are where you define how many times to deploy the infrastructure your Stack defines. + +Before writing your Stack configuration, we recommend planning and designing how you want to use your new Stack: + +* Understand your existing infrastructure and break it down into manageable components. +* Understand how you want to deploy the infrastructure your Stack defines. +* Align your Stack’s component organization with your deployment strategy. + +You aim to create a flexible and reusable Stack configuration, enabling you to manage your infrastructure lifecycle efficiently. + +## Understand your infrastructure + +Before writing your Stack configuration, we recommend assessing your current infrastructure setup and planning how you want to deploy your Stack’s infrastructure. + +### Break down your infrastructure + +Each Stack should represent a single system or application with a shared lifecycle. Start by analyzing your current infrastructure and identifying the components HCP Terraform should manage together. Components are typically groups of related resources, such as an application’s backend, frontend, or database layer, deployed and scaled together. + +We recommend structuring your Stacks along technical boundaries to keep them modular and manageable. For example, you can create a dedicated Stack for shared services, such as networking infrastructure for VPCs, subnets, or routing tables, and separate Stacks for application components that consume those shared services. This separation allows you to manage shared services independently while passing information between Stacks. For more details, refer to [Pass data from one Stack to another](/terraform/language/stacks/deploy/pass-data). + +### Sketch out your configuration + +We recommend sticking to technical boundaries when structuring a Stack configuration. A single Stack should represent a single system with a shared lifecycle. + +While keeping a Stack as self-contained as possible is ideal, there are valid cases where a Stack must consume outputs from another Stack. For example, a shared networking Stack can publish outputs, such as `vpc_id` or subnet IDs, that downstream application Stacks can consume as inputs. This approach ensures modularity and allows you to manage dependencies between Stacks using well-defined interfaces. For more details, refer to [Pass data from one Stack to another](/terraform/language/stacks/deploy/pass-data). + +Plan to add a component block to your configuration for every top-level module you want to include in your Stack. After establishing your top-level modules, you can use child modules without adding additional `component` blocks. + +## Plan your deployments + +Stack deployments define how to repeat your Stack's defined infrastructure. We recommend considering how HCP Terraform should deploy the infrastructure your Stack defines. Your deployment plan can change depending on whether you need separate deployments for different environments, cloud provider accounts, or regions. + +If your deployment process requires automation, such as auto-approving specific changes in your test deployment, you can also design orchestration rules to help manage your Stack deployments. Learn more about [setting conditions for stack deployment plans](/terraform/language/stacks/deploy/conditions). + +### Plan for scalability + +When designing your Stack, consider how it should scale as your infrastructure grows. For example, if you anticipate adding more environments or regions, design your Stack to accommodate these changes. + +To keep your Stack configuration concise, you can use the [locals block](/terraform/language/values/locals) to share values across your deployments. + +```hcl +# deployments.tfdeploy.hcl + +# Define variables that multiple deployments use. +locals { + tf_organization = "" + tf_project_name = "" +} +``` + +Additionally, using meta-arguments like `for_each` in your configuration can help streamline the creation of multiple components, making your configuration more flexible and scalable. + +## Next steps + +After planning out your Stack, learn how to make your design a reality by [defining a Stack configuration](/terraform/language/stacks/create/config). diff --git a/website/docs/language/stacks/index.mdx b/website/docs/language/stacks/index.mdx new file mode 100644 index 0000000000..6745d1bd1b --- /dev/null +++ b/website/docs/language/stacks/index.mdx @@ -0,0 +1,74 @@ +--- +page_title: Stacks overview +description: Stacks are a configuration layer that simplify managing infrastructure modules. Learn how Stacks help you provision and coordinate your infrastructure lifecycle at scale. +--- + +# Stacks overview + +As your infrastructure grows, managing Terraform configurations becomes increasingly complex. Stacks are a powerful configuration layer in HCP Terraform that simplifies managing your infrastructure modules and then repeating that infrastructure. + +> **Hands-on**: Try out the [Deploy a Stack with HCP Terraform](/terraform/tutorials/cloud/stacks-deploy) tutorial to get started with Stacks quickly. + +Stacks replace Terraform's traditional root module structure with a new configuration layer of modular components built on top of your Terraform child and shared modules. Stacks enable you to provision and coordinate your infrastructure lifecycle at scale, offering an organized and reusable approach that expands upon infrastructure as code (IaC). + +-> **Public Beta**: Stacks are in public beta. Once we generally release them, we do not guarantee backward compatibility support for the public beta. All APIs, workflows, and HCL syntax for Stacks are subject to change. + +## Background + +Stacks are an alternative way to organize your infrastructure and fundamentally differ from [HCP Terraform workspaces](/terraform/cloud-docs/workspaces). Stacks are not built on top of HCP Terraform workspaces, but can exist alongside them in the same project. + +Previously, those looking to deploy repeatable infrastructure using HCP Terraform would create separate workspaces and then use run triggers or other automation tools to coordinate changes between them. However, workspaces are not truly coupled to each other, hampering your ability to flexibly manage infrastructure as it scales. + +Terraform Stacks enable you to split your Terraform configuration into components and then deploy and manage those components across multiple environments. You can manage the lifecycle of each deployment separately, roll out configuration changes across your deployments, and manage your Stack as a unit in HCP Terraform. + +Each deployment in a Stack represents a group of components that work together, such as a development and production environment. The configuration for each Stack is identical across deployments, and you can further customize each environment with input variables. You can then use HCP Terraform to roll out changes to each deployment and keep track of changes across your environments. + +Stacks are unavailable for users on legacy HCP Terraform team plans. Learn more about [migrating to current HCP Terraform plan](/terraform/cloud-docs/overview/migrate-teams-standard). + +## Workflow + +Start by creating a Stack configuration file and filling it with the `component` blocks. Each `component` block includes a Terraform module as its source, and you can configure a `component` further using input arguments. Your Stack components share a lifecycle, which you can repeatedly deploy together using HCP Terraform. + +![Diagram of the architecture of a Stack. A Stack of two components (Kubernetes and Namespace) are rolled out to three deploymentes (US, Europe, and Asia) ](/img/docs/stacks-example.png) + +After configuring your Stack’s `components`, you create a separate deployment configuration file to define how you want to repeat its infrastructure. Each deployment in a Stack represents a group of infrastructure that works together. You can define `deployment` blocks for your development environments, cloud provider accounts, or regions. + +Once ready to deploy your Stack’s infrastructure, you can help manage your deployments by defining orchestration rules. For example, you can automatically approve operations whenever a deployment meets certain criteria. + +## Primary workflow + +The overall process for managing your infrastructure using Stacks consists of the following steps. + +### Design a Stack + +Consider your current infrastructure lifecycle and how the pieces interact with each other. Then, determine how to break down your infrastructure into components and think about how you want to deploy the infrastructure your Stack describes. Refer to [Design a Stack](/terraform/language/stacks/design) to learn more. + +### Write a Stack configuration + +Configure and define your Stack in a `tfstack.hcl` file. To define a Stack, add and configure `component` blocks, each representing an individual Terraform module. Refer to [Define configuration](/terraform/language/stacks/create/config) to learn more. + +Stacks declare providers in a different workflow than in traditional Terraform configurations, refer to [Declare providers](/terraform/language/stacks/create/declare-providers) to learn more. Stacks also have their own way of authenticating with providers, refer to [Authenticate a Stack](/terraform/language/stacks/deploy/authenticate) for details. + +### Define deployments + +With your Stack configuration file complete, the next step is to define how you want to deploy your Stack. Create a `tfdeploy.hcl` file defining how HCP Terraform should deploy your Stack’s infrastructure. In Stacks, deployments allow you to replicate infrastructure across multiple environments, regions, or accounts. Each deployment runs in an isolated agent, ensuring that changes in one deployment do not affect others. Refer to [Define deployment configuration](/terraform/language/stacks/deploy/config) to learn more. + +### Manage and orchestrate deployments + +Use your Stack’s context to define orchestration rules with the `orchestrate` block. You can define rules for automated approvals, error handling, and other events within your deployment configuration. Refer to [Set conditions for deployment plans](/terraform/language/stacks/deploy/conditions) to learn more. + +### Deploy + +After fully configuring your Stack, use HCP Terraform to deploy your Stack’s infrastructure. Refer to [Create a Stack](/terraform/cloud-docs/stacks/create) to learn more. + +## Guidance + +Stacks are useful when managing complex, multi-environment infrastructures where consistency and reusability are crucial. Refer to Stack [use cases](/terraform/language/stacks/use-cases) for inspiration on situations where Stacks are particularly well suited. + +## Constraints, limitations, and troubleshooting + +While Stacks provide powerful capabilities, there are some limitations to be aware of during this beta phase: + +* Each Stack currently supports a maximum of 20 deployments, which may limit scalability for large environments. +* Stacks have a limit of 500 [managed resources per HCP Terraform organization](/terraform/cloud-docs/overview/estimate-hcp-terraform-cost#what-is-a-managed-resource) while in public beta. If your organization exceeds this limit, you can no longer apply new resources until you reduce your resource count. If you need more resources, [fill out our form to request a resource extension](https://forms.gle/uGhn5CL1krf5T93m9). +* Stacks are not available for users on legacy HCP Terraform team plans. Learn more about [migrating to a current HCP Terraform plan](/terraform/cloud-docs/overview/migrate-teams-standard). diff --git a/website/docs/language/stacks/reference/tfdeploy.mdx b/website/docs/language/stacks/reference/tfdeploy.mdx new file mode 100644 index 0000000000..dd899b9c45 --- /dev/null +++ b/website/docs/language/stacks/reference/tfdeploy.mdx @@ -0,0 +1,427 @@ +--- +page_title: Deployment configuration file reference +description: Stacks help you provision and coordinate your infrastructure lifecycle at scale. Learn how to write a deployment configurion to tell HCP Terraform how many times to deploy your Stack's infrastructure. +--- + +# Deployment configuration file reference + +A deployment configuration file defines how to deploy your Stack's infrastructure. Each Stack deployment runs in its agent, wholly isolated from other Stack deployments. + +Every Stack needs a deployment configuration file, `tfdeploy.hcl`, and this page describes all of the blocks you can use within a deployment configuration file. Note that none of the blocks in the deployment configuration file support [meta-arguments](/terraform/language/resources/syntax#meta-arguments). + +## `deployment` block configuration + +The `deployment` block is where you define how many times you want to deploy your Stack's infrastructure. Each Stack requires at least one `deployment` block, and you can add a new `deployment` block every time you want to deploy your Stack’s infrastructure again. + +-> **Note**: HCP Terraform supports up to a maximum of 20 deployments. + +The following list outlines field hierarchy, language-specific data types, and requirements in the `deployment` block. + +### Complete configuration + +When every field is defined, a `deployment` block has the following form: + +```hcl +deployment "unique_name" { + inputs = { + key = "value" + } +} +``` + +For examples and guidance on defining deployments, refer to [Define deployment configuration](/terraform/language/stacks/deploy/config). + +### Specification + +This section details the fields you can configure in the `deployment` block. + +Each Stack must have at least one `deployment` block, and the label of the `deployment` block must be unique within your Stack. The `deployment` block is a map that defines the appropriate input values for each Stack instance to deploy. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `inputs` | A mapping of Stack variable names for this deployment. The keys in this map must correspond to the names of variables defined in the Stack. The values must be valid HCL literals meeting the type constraint of those variables. | map | Required | + +### Reference + +For example, the following `deployment` block accepts inputs for variables named `aws_region` and `instance_count` and creates a new deployment in HCP Terraform named “production”. + +```hcl +deployment "production" { + inputs = { + aws_region = "us-west-1" + instance_count = 2 + } +} +``` + +To learn more about defining deployments, refer to [Define deployment configuration](/terraform/language/stacks/deploy/config). + +## `orchestrate` block configuration + +The orchestrate block defines orchestration rules that you can use to manage your deployment plans. Your orchestration rules can automate tasks like auto-approving plans that meet specific conditions. + +The following list outlines field hierarchy, language-specific data types, and requirements in the `orchestrate` block. + +### Complete configuration + +When every field is defined, an `orchestrate` block has the following form: + +```hcl +# auto_approve is the rule type +orchestrate "auto_approve" "name_of_check" { + check { + condition = + reason ="Provide the reason why this check failed." + } +} +``` + +The `orchestrate` block label includes the rule type and the rule name, which together must be unique within the Stack. + +There are two orchestration rules to choose from: + +* The `auto_approve` rule executes after a Stack creates a plan and automatically approves a plan if all checks pass. +* The `replan` rule executes after a Stack applies a plan, automatically triggering a replan if all the checks pass. + +HCP Terraform evaluates the `check` blocks within your `orchestrate` block to determine if it should approve a plan. If all of the checks pass, then HCP Terraform approves the plan for you. If one or more `conditions` do not pass, then HCP Terraform shows the `reason` why, and you must manually approve that plan. + +By default, each Stack has an `auto_approve` rule named `empty_plan`, which automatically approves a plan if it contains no changes. + +### Specification + +This section provides details about the fields you can configure in the `orchestrate` block. + + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `check` | A block containing conditions and reasons for the orchestration rule. All checks must pass for an orchestration rule to be triggered. | block | Required | + +The `check` block contains the following configurable fields. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `condition` | An expression that determines whether the check passes. | expression | Required | +| `reason` | A message explaining why the check failed is shown in HCP Terraform, prompting you to manually approve a plan. | string | Required | + +### Orchestration Context + +A `check` block’s `condition` field has access to a `context` variable, which includes information about the context of the current deployment plan. The `context` variable contains the following fields. + +| Field | Description | Type | +| :---- | :---- | :---- | +| `operation` | The current operation HCP Terraform is performing. | string ("plan" or "apply") | +| `success` | Indicates whether the operation failed. If this field is `false`, then the `errors` field contains the message of why the operation failed. | boolean | +| `plan` | An object including data about the current plan. | object | +| `errors` | A set of diagnostic error message objects. | set of objects | +| `warnings` | A set of diagnostic warning message objects. | set of objects | + +The diagnostic message objects that the `context.errors` and `context.warnings` fields return includes the following information. + +| Field | Description | Type | +| :---- | :---- | :---- | +| `summary` | A brief summary of the diagnostic message | string | +| `detail` | Detailed information about the diagnostic message | string | + +The `context.plan` field returns an object including the following fields. + +| Field | Description | Type | +| :---- | :---- | :---- | +| `mode` | The plan mode. | string ("normal", "refresh-only", or "destroy") | +| `applyable` | Whether or not the plan can be applied. | boolean | +| `changes` | A change summary object. | object | +| `component_changes` | A map of change summary objects for each component instance. | map of objects | +| `replans` | The number of replans in this plan's sequence, starting at `0`. | integer | +| `deployment` | A direct reference to the current deployment. | Deployment object containing a `deployment_name` field. | + +The objects in the `context.plan.component_changes` field include the following fields. + +| Field | Description | Type | +| :---- | :---- | :---- | +| `add` | Number of resources to be added. | integer | +| `change` | Number of resources to be changed. | integer | +| `import` | Number of resources to be imported. | integer | +| `remove` | Number of resources to be removed. | integer | +| `total` | Total number of resource actions. | integer | + +The object in the `context.plan.deployment` field includes the following fields. + +| Field | Description | Type | +| :---- | :---- | :---- | +| `deployment_name` | The name of the current deployment HCP Terraform is running this plan on. You can use this field to check which deployment is running this plan. For example, you can check if this plan is on your production deployment: `context.plan.deployment == deployment.production`. | string | + +### Reference + +For example, the following `orchestrate` block automatically approves deployments if a component has not changed. + +```hcl +orchestrate "auto_approve" "no_pet_changes" { + check { + condition = context.plan.component_changes["component.pet"].total == 0 + reason = "Changes proposed to pet component." + } +} +``` + +If nothing changes in the `component.pet` component, HCP Terraform automatically approves the plan. + +## `identity_token` block configuration + +The `identity_token` block defines a JSON Web Token (JWT) that Terraform generates for a given deployment if that `deployment` block references an `identity_token` in its `inputs`. + +You can directly pass the token generated by the `identity_token` block to a provider's configuration for OIDC authentication. For more information on authenticating a Stack using OIDC, refer to [Authenticate a Stack](/terraform/language/stacks/deploy/authenticate). + +### Complete configuration + +When every field is defined, an `identity_token` block has the following form: + +```hcl +identity_token "unique_to_stack_name" { + audience = ["audience this token is intended for"] +} +``` + +### Specification + +This section provides details about the fields you can configure in the `identity_token` block. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `audience` | The audience of your token is the resource(s) that uses this token after Terraform generates it. You specify an audience to ensure that the cloud service authorizing the workload is confident that the token you present is intended for that service. | set of strings | Required | + +### Reference + +Once defined, you can reference an identity token's `jwt` attribute in a deployment's inputs. For example, below we generate a token for a particular role ARN in AWS. + +```hcl +# main.tfdeploy.hcl +identity_token "aws" { + audience = ["aws.workload.identity"] +} + +deployment "staging" { + inputs = { + aws_region = "eu-west-1" + role_arn = "arn:aws:iam::123456789101:role/my-oidc-role" + aws_token = identity_token.aws.jwt + } +} +``` + +Terraform generates an identity token that you can use in your Stack configuration to configure your AWS provider with your role in AWS. + +```hcl +# providers.tfstack.hcl + +variable "aws_token" { + type = string + ephemeral = true +} + +variable "aws_region" { + type = string +} + +variable "aws_role" { + type = string +} + +provider "aws" "this" { + config { + region = var.aws_region + assume_role_with_web_identity { + role_arn = var.aws_role + web_identity_token = var.aws_token + } + } +} +``` + +With this setup, every time your `staging` deployment performs a plan or apply operation, we authenticate with AWS using your established ARN role and a new JWT token. For more information on authenticating a Stack using OIDC, refer to [Authenticate a Stack](/terraform/language/stacks/authenticate). + +## `store` block configuration + +Use the `store` block to define key-value secrets in your deployment configuration. After storing a secret, you can reference it in your deployment block input variable values. + +When you define a `store` block, you define two headers: the first specifies the store type, and the second is the store's name. + +When every field is defined, a `store` block has the following form. + +```hcl +store "store_type" "store_name" { + +} +``` + +A store’s type defines where Terraform should look for that store’s credentials and how to decode the credentials it finds. You cannot share arguments across store types. + +### `varset` specification and configuration + +Use the `varset` store to enable your Stacks to access [variable sets](/terraform/cloud-docs/workspaces/variables/managing-variables#variable-sets) in HCP Terraform. Your Stack must have access to the variable set you are targeting, meaning it must be globally available or assigned to the project containing your Stack. + +This section details the fields you can configure in the `store` block that uses the `varset` store type. + +```hcl +store "varset" "store_name" { + id = "" + category = <"terraform" or "env"> +} +``` + +The `varset` store type accepts the following fields. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `id` | The external ID of the variable set you want to access. | string | Required | +| `category` | The `category` argument specifies whether to use Terraform or environment variables from the variable set. Specify "terraform" or "env" depending on the variable you want to access. | string | Required | + +The `store.varset` block can only use a variable set’s Terraform or environment variables. You can define two `store` blocks if you need to access Terraform and environment variables in your deployment configuration. + +```hcl +store "varset" "tokens" { + id = "varset-###" + category = "terraform" +} + +store "varset" "available_regions" { + id = "varset-###" + category = "env" +} + +deployment "main" { + inputs = { + regions = store.varset.available_regions.regions + tfe_token = store.varset.tokens.tfe_token + } +} +``` + +You can access specific environment variables by key from the `store.varset.available_regions` store, and you can access specific Terraform variables by key using the `store.varset.tokens` store. + +### Reference + +For example, if you have an HCP Terraform [variable set](/terraform/cloud-docs/workspaces/variables/managing-variables#variable-sets) that contains a value you want to use in your deployment, you can create a `store` block to access that variable set. + +```hcl +store "varset" "tokens" { + id = "" + category = "terraform" +} + +deployment "main" { + inputs = { + token = store.varset.tokens.example_token + } +} +``` + +You cannot access an entire store and must specifically access individual keys within that store. Meaning, we can access your example’s `example_token` variable by accessing the store’s `varset` type, `store.varset`, then accessing the specific store, `store.varset.tokens.example_token`. + +## `locals` block configuration + +A local value assigns a name to an expression, so you can use the name multiple times within your Stack configuration instead of repeating the expression. The `locals` block works exactly as it does in traditional Terraform configurations. Learn more about [the `locals` block](/terraform/language/values/locals). + +## `publish_output` block configuration + +The `publish_output` block requires at least Terraform version `terraform_1.10.0-alpha20241009` or higher. Download [latest version of Terraform](https://releases.hashicorp.com/terraform/) to use the most up-to-date functionality. + +The `publish_output` block specifies a value to export from your current Stack, which other Stacks in the same project can consume. Declare one `publish_output` block for each value to export from your Stack. + +### Complete configuration + +When every field is defined, a `publish_output` block has the following form: + + + +```hcl +publish_output "output_name" { + description = "Description of the purpose of this output" + value = deployment.deployment_name.some_value +} +``` + + + +### Specification + +This section provides details about the fields you can configure in the output block. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `description` | A human-friendly description for the output. | string | Optional | +| `value` | The value to output. | any | Required | + +### Reference + +For example, you could output the VPC ID from your networking deployment, making it available to other Stacks to input. + + + +```hcl +# Network Stack's deployment configuration + +publish_output "vpc_id" { + description = "The networking Stack's VPC's ID." + value = deployment.network.vpc_id +} +``` + + + +To learn more about passing information between Stacks, refer to [Pass data from one Stack to another](/terraform/language/stacks/deploy/pass-data). + +## `upstream_input` block configuration + +The `upstream_input` block requires at least Terraform version `terraform_1.10.0-alpha20241009` or higher. Download [latest version of Terraform](https://releases.hashicorp.com/terraform/) to use the most up-to-date functionality. + +The `upstream_input` block specifies another Stack in the same project to consume outputs from. Declare an `upstream_input` block for each Stack you want to reference. If an output from a upstream Stack changes, HCP Terraform automatically triggers runs for any Stacks that depend on those outputs. + +To learn more about passing information between Stacks, refer to [Pass data from one Stack to another](/terraform/language/stacks/deploy/link-stacks). + +### Complete configuration + +When every field is defined, an `upstream_input` block has the following form: + + + +```hcl +upstream_input "upstream_stack_name" { + type = "stack" + source = "app.terraform.io/{organization_name}/{project_name}/{upstream_stack_name}" +} +``` + + + +### Specification + +This section provides details about the fields you can configure in the `upstream_input` block. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `type` | The only supported type is “stack”. | string | Required | +| `source` | The upstream Stack’s URL, in the format: `"app.terraform.io/{organization_name}/{project_name}/{upstream_stack_name}"` | string | Required | + +### Reference + +For example, you could input a VPC ID from an upstream Stack that manages your shared networking service. You can use the `upstream_input` block to pass information from your network Stack into your application Stack. + + + +```hcl +# Application Stack's deployment configuration + +upstream_input "network_stack" { + type = "stack" + source = "app.terraform.io/hashicorp/Default Project/networking-stack" +} + +deployment "application" { + inputs = { + vpc_id = upstream_input.network_stack.vpc_id + } +} +``` + + + +Your application Stack can now securely consume and use outputs from your network Stack. To learn more about passing information between Stacks, reference [Pass data from one Stack to another](/terraform/language/stacks/deploy/pass-data). diff --git a/website/docs/language/stacks/reference/tfstack.mdx b/website/docs/language/stacks/reference/tfstack.mdx new file mode 100644 index 0000000000..d776834126 --- /dev/null +++ b/website/docs/language/stacks/reference/tfstack.mdx @@ -0,0 +1,307 @@ +--- +page_title: Stack configuration file reference +description: Stacks help you provision and coordinate your infrastructure lifecycle at scale. Learn how to write a Stack configurion file to create a Stack for your infrastructure. +--- + +# Stack configuration file reference + +A Stack configuration file defines all the infrastructure pieces included in a Stack. Every Stack needs a configuration file, `tfstack.hcl`, and this page describes all the blocks you can use within a Stack configuration file. + +## `component` block configuration + +The `component` block defines the infrastructure to include in your Stack. Each Stack requires at least one `component` block. Add a `component` block to your configuration for every module you want to include in your Stack. + +The following list outlines field hierarchy, language-specific data types, and requirements in the `component` block. + +### Complete configuration + +When every field is defined, a `component` block has the following form: + +```hcl +component "unique_name" { + source = + inputs = { + input_name = + } + providers = { + random = provider.provider_name.provider_alias + } +} +``` + +### Specification + +This section details the fields you can configure in the `component` block. + +Each Stack must have at least one `component` block, and the label of the component block must be unique within your Stack. The `component` block is a map that defines a module to source, input variables for that module, and the names of the providers that your module requires. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `source` | The Terraform module to [source](https://developer.hashicorp.com/terraform/language/modules/sources) for this component. | string | Required | +| `version` | If you declare a module from the public Terraform registry in the source field, you can define which module version to use. | string | Optional | +| `inputs` | A mapping of module input variable names to values. The keys of this map must correspond to the variable names defined by the `source` module. The values can be one of three types: A variable reference such as `var.variable_name`, a component output such as `component.component_name.output_name`, or a literal valid HCL value such as "string value". | map | Required | +| `providers` | A mapping of provider names to providers declared in your Stack configuration. Modules cannot configure their own providers. You must [declare providers](/terraform/language/stacks/create/declare-providers) in the top level of the Stack and pass them into each module in the Stack. | map | Required | +| `depends_on` | A list of other components that HCP Terraform must execute before this component. You do not need to include another component’s outputs in this list, because Terraform automatically recognizes them. | list | Optional | + +### Reference + +The `component` block also supports the `for_each` meta-argument. For example, the following configuration uses `for_each` to provision modules in multiple AWS regions for a given environment. + +```hcl +component "s3" { + for_each = var.regions + + source = "./s3" + + inputs = { + region = each.value + } + + providers = { + aws = provider.aws.configurations[each.value] + random = provider.random.this + } +} +``` + +To learn more about breaking down your infrastructure into components, refer to [Define Stack configuration](/terraform/language/stacks/create/config). + +## `variable` block configuration + +Use the `variable` block to declare input variables for your Stack configuration. Using `variable` blocks in Stacks is similar to traditional Terraform configurations but with a few minor differences. + +In Stack configurations, `variable` blocks must define a `type` field and do not support the `validation` argument. Learn more about Terraform [Variables](/terraform/language/values/variables). + +### Complete configuration + +When every field is defined, a `variable` block has the following form: +```hcl +variable "unique_variable_name" { + description = "Description of the purpose of this variable" + type = string + default = "Default variable value" + sensitive = false + nullable = false +} +``` + +### Specification + +This section provides details about the fields you can configure in the `variable` block. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `type` | A type constraint for the variable's value. Can be string, number, bool, list, map, set, tuple, or object. Stacks require you to declare a `type` for your variables. | string | Required | +| `description` | A human-friendly description for the variable. | string | Optional | +| `default` | A default value for the variable which Terraform uses if no value is provided. | any | Optional | +| `sensitive` | Marks the variable as sensitive, which prevents its value from being displayed in logs or in the HCP Terraform user interface. | bool | Optional | +| `nullable` | Specifies whether the variable can be null. | bool | Optional | +| `ephemeral` | Marks that this variable’s value is dynamic. For example, an expiring authentication token is `ephemeral`. Learn more about [Ephemeral variables](/terraform/language/values/variables#exclude-values-from-state). | bool | Optional | + +## `output` block configuration + +Use the `output` block to make information about your infrastructure available in the HCP Terraform UI to expose information about your Stack. The `output` block functions the same way in Stack configuration as it does in traditional Terraform configurations with a few small differences. + +In Stack configurations, `output` blocks require the `type` argument, and they do not support the `preconditions` block. Learn more about [Outputs](/terraform/language/values/outputs). + +### Complete configuration + +When every field is defined, an `output` block has the following form: +```hcl +output "unique_name_of_output" { + description = "Description of the purpose of this output" + type = string + value = component.component_name.some_value + sensitive = false + ephemeral = false +} +``` + +### Specification + +This section provides details about the fields you can configure in the `output` block. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `description` | A human-friendly description for the output. | string | Optional | +| `type` | The type of the output. | type constraint | Required | +| `value` | The value to output. | any | Required | +| `sensitive` | Marks the variable as sensitive, which prevents its value from being displayed in logs or in the HCP Terraform UI. | bool | Optional | +| `ephemeral` | Whether to exclude the value from plans and state data. | bool | Optional | + +## `required_providers` and `provider` block configuration + +Terraform relies on plugins called providers to interact with cloud providers, SaaS providers, and other APIs. + +The `required_providers` block works exactly as it does in traditional Terraform configurations. Learn more about [the `required_providers` block](/terraform/language/providers/requirements). The `provider` block functions mostly the same way in Stack configuration as it does in traditional Terraform configurations, with a few differences. + +The `provider` block differs in Stack configurations in the following ways: + +* Supports the `for_each` [meta-argument](/terraform/language/meta-arguments/for_each) +* Defines aliases in the `provider` block header rather than as an argument +* Accepts arguments using a config block + +You must also define your providers at the top level of your Stack configuration in a `tfstack.hcl` file. You cannot define providers within the modules your component blocks source. + +### Complete configuration + +Provider configuration differs depending on which API you are interacting with. Below, you can configure the AWS provider and pass it to your S3 component. + +```hcl +required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.7.0" + } +} + +# "main" is the alias for this provider +provider "aws" "main" { +# The config block accepts the configuration for a provider + config { + region = var.region + + assume_role_with_web_identity { + role_arn = var.role_arn + web_identity_token = var.identity_token + } + } +} +``` + +The `provider` block also supports the `for_each` meta-argument. For example, the following provider uses `for_each` to create multiple AWS configurations. + +```hcl +provider "aws" "configurations" { + for_each = var.regions + + config { + region = each.value + + assume_role_with_web_identity { + role_arn = var.role_arn + web_identity_token = var.identity_token + } + + default_tags { + tags = var.default_tags + } + } +} +``` + +For more information on declaring providers in Stacks, refer to [Declare providers](/terraform/language/stacks/create/declare-providers). + +## `locals` block configuration + +A local value assigns a name to an expression, so you can use the name multiple times within your Stack configuration instead of repeating the expression. The `locals` block works exactly as it does in traditional Terraform configurations. Learn more about [the `locals` block](/terraform/language/values/locals). + + +## `removed` block configuration + +Stacks take a systematic approach to removing components. To remove a component from a Stack, you must use the `removed` block to ensure HCP Terraform knows which components to remove and which providers it requires to remove that component. + +Do not remove providers from your stack configuration without first removing the components that require those providers. HCP Terraform requires a component's providers to ensure it can successfully remove that component. + + +### Complete configuration + +When every field is defined, a `removed` block has the following form: + +```hcl + +removed { + source = "" + + from = component.component_name + providers = { + aws = provider.aws.this + } +} +``` + + +### Specification + +This section provides details about the fields you can configure in the `removed` block. + +| Field | Description | Type | Required | +| :---- | :---- | :---- | :---- | +| `from` | The name of the resource you want to remove. | string | Required | +| `source` | The source module of the component you want to remove. | string | Required | +| `providers` | A mapping of provider names to the providers that the component you want to remove uses. HCP Terraform needs your component providers to remove that component properly. | map | Required | + +### Reference + +The `removed` block also supports the `for_each` meta-argument to support removing multiple `components`. For example, if you are trying to remove a dynamic component that HCP Terraform deploys in multiple AWS regions. + +```hcl +component "s3_buckets" { + source = "./s3" + + for_each = var.regions + + providers = { + aws = provider.aws.config[each.value] + } + + variables = { + region = each.value + } +} +``` + +You can use the `for_each` meta-argument to remove each component dynamically. + +```hcl +removed { + source = "./s3" + + for_each = var.regions + + from = component.s3_buckets[each.value] + providers = { + aws = provider.aws.config[each.value] + } +} +``` + +HCP Terraform iterates through the `var.regions` variable and removes each AWS region's corresponding component. Expanding on this example, you could add a new variable to keep track of the regions you want to remove. + +```hcl +variable "regions" { + type = set(string) +} + +# Adding a region to this variable instructs HCP Terraform to remove it. +variable "removed_regions" { + type = set(string) +} + +component "s3_buckets" { + source = "./s3" + + for_each = var.regions + + providers = { + aws = provider.aws.config[each.value] + } + + variables = { + region = each.value + } +} + +removed { + source = "./s3" + # Iterate and remove the regions in our new removed_regions variable. + for_each = var.removed_regions + + from = component.s3_buckets[each.value] + providers = { + aws = provider.aws.config[each.value] + } +} +``` + +When you move a region from `regions` to the `removed_regions` variable, HCP Terraform plans to remove that region's corresponding components. diff --git a/website/docs/language/stacks/reference/tfstacks-cli.mdx b/website/docs/language/stacks/reference/tfstacks-cli.mdx new file mode 100644 index 0000000000..5f0ed006e3 --- /dev/null +++ b/website/docs/language/stacks/reference/tfstacks-cli.mdx @@ -0,0 +1,165 @@ +--- +page_title: Terraform Stack CLI reference +description: The terraform-stacks-cli is a command-line tool for validating, initializing, and managing Stack configurations. Learn about the terraform-stacks-cli commands and options. +--- + +# Terraform Stack CLI reference + +The `terraform-stacks-cli` is a command-line tool for validating, initializing, and testing Stack configurations. + +## Requirements + +The `terraform-stacks-cli` requires an `alpha` version of Terraform, and you must use at least version `terraform_1.10.0-alpha20241009` or higher. You can download an `alpha` version of Terraform on the [releases page](https://releases.hashicorp.com/terraform/). We recommend downloading the latest alpha version of Terraform to use the most up-to-date functionality. + +## Installation + +To install the `terraform-stacks-cli`, you can download it directly [on the HashiCorp releases page](https://releases.hashicorp.com/tfstacks) or install it with one of the following package managers: Homebrew, Debian/Ubuntu, CentOS/RHEL, Fedora, or Amazon Linux. + +#### Homebrew + +Run the following commands to install the `terraform-stacks-cli` using Homebrew. + +```shell-session +$ brew tap hashicorp/tap +$ brew install hashicorp/tap/tfstacks +``` + +#### Linux Package Managers + +Run the following commands to install the `terraform-stacks-cli` with the following Linux package managers. + +##### Debian or Ubuntu + +Run the following commands to install the `terraform-stacks-cli` using Debian or Ubuntu. + +```shell-session +$ wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg +$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list +$ sudo apt update && sudo apt install terraform-stacks-cli +``` + +##### CentOS or RHEL + +Run the following commands to install the terraform-stacks-cli using CentOS or RHEL. + +```shell-session +$ sudo yum install -y yum-utils +$ sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo +$ sudo yum -y install terraform-stacks-cli +``` + +##### Fedora + +Run the following commands to install the `terraform-stacks-cli` using Fedora. + +```shell-session +$ sudo dnf install -y dnf-plugins-core +$ sudo dnf config-manager addrepo --from-repofile=https://rpm.releases.hashicorp.com/fedora/hashicorp.repo +$ sudo dnf -y install terraform-stacks-cli +``` + +##### Amazon Linux + +Run the following commands to install the `terraform-stacks-cli` using Amazon Linux. + +```shell-session +$ sudo yum install -y yum-utils shadow-utils +$ sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo +$ sudo yum -y install terraform-stacks-cli +``` + +## Commands + +The `terraform-stacks-cli` supports the following four commands: + +* [`tfstacks init`](#tfstacks-init-command) +* [`tfstacks validate`](#tfstacks-validate-command) +* [`tfstacks providers lock [-platform=os_arch]`](#tfstacks-providers-lock-command) +* [`tfstacks plan -organization=org_name -stack=stack_id -deployment=deployment_name [-hostname=hostname]`](#tfstacks-plan-command) +* [`tfstacks fmt`](#tfstacks-fmt-command) + +Each Stack command supports [global options](#global-options). + +### `tfstacks init` command + +The `tfstacks init` command attempts to download any dependencies your Stack configuration requires. + +```shell-session +$ tfstacks init +``` + +If the `tfstacks init` command cannot find a dependency, it warns you of the reason why and installs a false dependency in the real one's place to unblock any future validation. Meaning, that it the `tfstacks init` command cannot download a dependency, you can still run `tfstacks validate`, but the missing dependencies are not validated. + +### `tfstacks validate` command + +The `tfstacks validate` command loads your Stack configuration and validates that your stack’s syntax and static types are correct. + +```shell-session +$ tfstacks validate +``` + +We recommend running this command whenever you change your Stack configuration. + +### `tfstacks providers lock` command + +The `tfstacks providers lock` command creates and updates your stack’s provider lock file. + +```shell-session +$ tfstacks providers lock [-platform=os_arch] +``` + +The `tfstacks providers lock` command references the `required_providers` block from your configuration to know which providers to download and compute the provider lock hashes for. The `tfstacks providers lock` command only checks the `required_providers` block, so you must list all of the providers you use in the `required_providers` block to ensure that the `tfstacks providers lock` command can find them. + +By default, the `tfstacks providers lock` installs hashes for providers built for `linux_amd64` machines, which HCP Terraform requires for execution. You can optionally use the `-platform=os_arch` argument to create hashes for other platforms. + +### `tfstacks plan` command + +The `tfstacks plan` runs a remote, speculative plan with local code in the current directory. If you are not currently logged in to HCP Terraform when you run this command, the output prompts you to log in. + +```shell-session +$ tfstacks plan -organization=REQUIRED_ORG_NAME -stack=REQUIRED_STACK_ID -deployment=REQUIRED_DEPLOYMENT [-hostname=hostname] +``` + +The `tfstacks plan` accepts the following arguments. + +| Field | Description | Required | +| :---- | :---- | :---- | +| `-organization` | Set the `organization` flag to the name of this stack’s organization.

Alternatively, you can set an environment variable named `TFSTACKS_ORGANIZATION` with the same value. | Required | +| `-stack=` | Set the `stack` flag to the ID of the Stack you want to perform this plan in. You can find a stack’s ID underneath its name when you view that Stack in HCP Terraform.

Alternatively, you can set an environment variable named `TFSTACKS_STACK_ID` with the same value. | Required | +| `-deployment=` | Set the `deployment` flag to the deployment name you want to perform this plan in. The deployment name must match one of the deployment names you specified in the `tfdeploy.hcl` file.

Alternatively, you can set an environment variable named `TFSTACKS_DEPLOYMENT` with the same value. | Required | +| `-hostname=` | You can set the `hostname` flag to the hostname of the HCP Terraform instance you want to perform this plan in. The default value is `app.terraform.io`.

Alternatively, you can set an environment variable named `TFSTACKS_HOSTNAME` with the same value. | Optional | + +### `tfstacks fmt` command + +-> The `tfstacks fmt` command requires v0.6.0 of the `terraform-stacks-cli` or higher. Refer to [installation](#installation) for instructions. + +The `tfstacks fmt` command scans your current directory for Stack configuration files, `.tfstack.hcl` and `.tfdeploy.hcl`, and formats those files to match Terraform's canonical format and style. + +```shell-session +$ tfstacks fmt [options] [target] +``` + +By default, the `tfstacks fmt` command scans your current directory for configuration files. You can also provide a `target` argument to tell `tfstacks fmt` to scan: +* A directory +* A specific file +* Standard input by supplying a single dash (`-`). + +The `tfstacks fmt` command accepts the following arguments. + +| Flag | Description | Required | +| :---- | :---- | :---- | +| `-list=false` | Prevents the command from listing the files containing formatting inconsistencies. | Optional | +| `-diff` | Displays the diffs of formatting changes. | Optional | +| `-write=false` | Prevents the command from overwriting files. This behavior is implied by the `-check` flag or if the input is from `STDIN`. | Optional | +| `-check` | Checks if the input is formatted. The exit status is `0` if the command's input is properly formatted. Otherwise, the exit status is non-zero, and the command outputs a list of improperly formatted file names. | Optional | +| `-recursive` | Processes files in subdirectories in addition to the current directory. By default, only the specified directory is processed. | Optional | + +The `tfstacks fmt` command uses the same formatting rules as the Terraform CLI's `terraform fmt` command. Refer to [`terraform fmt`](/terraform/cli/commands/fmt) for more information on formatting rules. + +## Global options + +You can apply the following global options to any `tfstacks` command: + +* The `-terraform-binary` flag specifies a specific version of Terraform to use instead of the one in your PATH. +* The `-chdir` flag points to an alternate configuration directory. +* The `-no-color` flag disables color output in the CLI. diff --git a/website/docs/language/stacks/use-cases.mdx b/website/docs/language/stacks/use-cases.mdx new file mode 100644 index 0000000000..c796c84a67 --- /dev/null +++ b/website/docs/language/stacks/use-cases.mdx @@ -0,0 +1,240 @@ +--- +page_title: Use cases +description: Learn about example use cases to better understand how to use Stacks to manage your infrastructure lifecycle more effectively. +--- + +# Use Cases + +Stacks provide a way to define, organize, and reuse components and then deploy the infrastructure those components describe consistently across different deployments. + +Stacks are ideal for the following situations: +* Managing infrastructure with a common lifecycle, such as a microservices architecture where HCP Terraform needs to deploy multiple services together. +* Repeating infrastructure across different environments, regions, or accounts that require consistent and synchronized deployments. +* Tightly orchestrating infrastructure changes across different environments. Stacks help you manage your dependencies and ensure Terraform creates resources in the correct order. + +Stacks can simplify the complexity of wrangling the infrastructure in these scenarios, ensuring that your configurations remain organized, maintainable, and scalable. + +## Manage deployment dependencies + +Stacks allow you to manage dependencies within complex infrastructure deployments automatically. If your infrastructure has interdependencies between components, HCP Terraform recognizes if a Stack component requires attributes that are not yet available and defers planning those changes until it can apply them. + +-> **Hands-on**: Go through our tutorial on [Managing Kubernetes workloads using Stacks](/terraform/tutorials/cloud/stacks-eks-deferred) for examples of Stacks that manage deployment dependencies. + +For example, when managing Kubernetes workloads that use custom resources, you cannot provision a custom resource definition (CRD) API and the resources that use those APIs in a single step. In these scenarios, HCP Terraform first needs to deploy your CRD and then deploy the resources that require your CRD in a second plan and apply. When you deploy this setup with HCP Terraform workspaces, you manage multiple workspaces to accomplish this workflow, which can get complicated. + +When you use a Stack, HCP Terraform recognizes the dependency between components, automatically deferring the component's plan and apply steps until it can complete them successfully. Learn more about how Stacks plan [deferred changes](/terraform/cloud-docs/stacks/deploy/plans#deferred-changes). + +## Deploy to multiple environments + +Managing infrastructure across multiple environments can be challenging, especially when you want to ensure that each environment remains consistent. For example, if you manage infrastructure for multiple environments, such as development, staging, and production. Each environment needs to be isolated, but they all mirror each other’s configuration. + +Stacks allow you to define deployments for each environment within a single configuration, making it easier to maintain uniformity and make updates across all your environments. + +```hcl +# deployments.tfdeploy.hcl + +deployment "production" { + inputs = { + aws_region = "us-west-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws.jwt + } +} + +deployment "development" { + inputs = { + aws_region = "us-east-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws.jwt + } +} +``` + +Stacks also simplify adding and removing environments. You can add a `deployment` block to create a new environment using the same Stack configuration and infrastructure components. + +```hcl +# deployments.tfdeploy.hcl + +deployment "production" { + inputs = { + aws_region = "us-west-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws.jwt + } +} + +deployment "development" { + inputs = { + aws_region = "us-east-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws.jwt + } +} + +deployment "staging" { + inputs = { + aws_region = "us-east-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws.jwt + } +} +``` + +By default, HCP Terraform notices that you changed your configuration and will create a new plan for your new deployment. + +## Deploy across regions + +For global applications, deploying infrastructure across multiple cloud regions is often necessary to ensure low latency and high availability. Stacks enable you to manage cross-region deployments with a unified configuration, ensuring that HCP Terraform sets up each region consistently while allowing for region-specific customization. + +For example, if you want to deploy the same environment to two different regions, you could set up your deployments to be region-specific. + +```hcl +# deployments.tfdeploy.hcl + +identity_token "aws_west" { + audience = ["aws.workload.identity.west"] +} + +identity_token "aws_east" { + audience = ["aws.workload.identity.east"] +} + +deployment "us_dev_east" { + inputs = { + aws_region = "us-east-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws_east.jwt + } +} + +deployment "us_dev_west" { + inputs = { + aws_region = "us-west-1" + instance_count = 2 + role_arn = "" + identity_token = identity_token.aws_west.jwt + } +} +``` + +Stacks support using the [`for_each` meta argument](/terraform/language/meta-arguments/for_each) in both `provider` and `component` blocks. You can use this support to streamline infrastructure deployment in repeating regions. For example, you could set up a variable to expect a set of regions. + +```hcl +# variables.tfstack.hcl + +# The regions variable is where we specify each region we want to deploy this +# Stack's infrastructure within. +variable "regions" { + type = set(string) +} + +# The identity_token and role_arn are for configuring this Stack to +# authenticate with AWS using OIDC. +variable "identity_token" { + type = string + ephemeral = true +} + +variable "role_arn" { + type = string +} +``` + +Then, set up your provider configuration to iterate through the `var.regions` variable to configure a version of that provider for each region. + +```hcl +# providers.tfstack.hcl + +required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.7.0" + } + + random = { + source = "hashicorp/random" + version = "~> 3.5.1" + } +} + + +provider "aws" "configurations" { +# This provider configuration iterates through and creates a configuration +# for each region. + for_each = var.regions + + config { + region = each.value + + assume_role_with_web_identity { + role_arn = var.role_arn + web_identity_token = var.identity_token + } + } +} +``` + +Each of your Stack’s deployments uses a separate AWS provider configuration for each region you defined in the `regions` variable. This means that in your configuration, you refer to the AWS provider for the `us-east-1` region as `provider.aws.configurations["us-east-1"]`. + +You can keep Stack components dynamic by using the `for_each` meta-argument to deploy a `component`’s Terraform module in each region. + +```hcl +# components.tfstack.hcl + +component "s3" { + for_each = var.regions + + source = "./s3" + + inputs = { + region = each.value + } + + providers = { + aws = provider.aws.configurations[each.value] + random = provider.random.this + } +} +``` + +The components in this example use the `for_each` meta-argument to deploy their Terraform module in the region for the current deployment. Notice how the component configures the AWS provider to use the correct provider for the given region with `provider.aws.configurations[each.value]`. + +## Deploy across accounts + +Stacks can also be helpful when managing infrastructure across different cloud provider accounts, ensuring that each account's infrastructure is consistent but isolated. + +```hcl +# deployments.tfdeploy.hcl + +identity_token "aws_prod" { + audience = ["aws.prod.workload.identity"] +} + +identity_token "aws_dev" { + audience = ["aws.dev.workload.identity"] +} + +deployment "development" { + inputs = { + aws_region = "us-east-1" + role_arn = "" + identity_token = identity_token.aws_dev.jwt + } +} + +deployment "production" { + inputs = { + regions = ["us-east-1", "us-west-1"] + role_arn = "" + identity_token = identity_token.aws_prod.jwt + } +} +``` + +Stacks enable you to define and deploy shared infrastructure components across different accounts, maintaining consistency while adhering to security and organizational boundaries. diff --git a/website/docs/language/state/backends.mdx b/website/docs/language/state/backends.mdx index 03a1195842..fc302fdbcd 100644 --- a/website/docs/language/state/backends.mdx +++ b/website/docs/language/state/backends.mdx @@ -8,7 +8,7 @@ description: >- # State Storage and Locking Backends are responsible for storing state and providing an API for -[state locking](/language/state/locking). State locking is optional. +[state locking](/terraform/language/state/locking). State locking is optional. Despite the state being stored remotely, all Terraform commands such as `terraform console`, the `terraform state` operations, `terraform taint`, @@ -28,7 +28,7 @@ sensitive values are in your state, using a remote backend allows you to use Terraform without that state ever being persisted to disk. In the case of an error persisting the state to the backend, Terraform will -write the state locally. This is to prevent data loss. If this happens the +write the state locally. This is to prevent data loss. If this happens, the end user must manually push the state to the remote backend once the error is resolved. @@ -63,12 +63,10 @@ prior to forcing the overwrite. ## State Locking -Backends are responsible for supporting [state locking](/language/state/locking) +Backends are responsible for supporting [state locking](/terraform/language/state/locking) if possible. -Not all backends support locking. The -[documentation for each backend](/language/settings/backends) -includes details on whether it supports locking or not. +Not all backends support locking. The [documentation for each backend](/terraform/language/backend#available-backends) includes details about whether it supports locking or not. For more information on state locking, view the -[page dedicated to state locking](/language/state/locking). +[page dedicated to state locking](/terraform/language/state/locking). diff --git a/website/docs/language/state/import.mdx b/website/docs/language/state/import.mdx index bbbf92d039..1bf2c7d5fd 100644 --- a/website/docs/language/state/import.mdx +++ b/website/docs/language/state/import.mdx @@ -7,8 +7,7 @@ description: >- # Import Existing Resources -Terraform is able to import existing infrastructure. This allows you take -resources you've created by some other means and bring it under Terraform management. +Terraform is able to import existing infrastructure. This allows you to take +resources you have created by some other means and bring them under Terraform management. -To learn more about this, please visit the -[pages dedicated to import](/cli/import). +To learn more, see [Import](/terraform/language/import). diff --git a/website/docs/language/state/index.mdx b/website/docs/language/state/index.mdx index fea81b2cbe..89b539ad36 100644 --- a/website/docs/language/state/index.mdx +++ b/website/docs/language/state/index.mdx @@ -13,11 +13,12 @@ resources to your configuration, keep track of metadata, and to improve performance for large infrastructures. This state is stored by default in a local file named "terraform.tfstate", -but it can also be stored remotely, which works better in a team environment. +but we recommend [storing it in HCP Terraform](/terraform/cloud-docs/migrate) +to version, encrypt, and securely share it with your team. -Terraform uses this local state to create plans and make changes to your +Terraform uses state to determine which changes to make to your infrastructure. Prior to any operation, Terraform does a -[refresh](/cli/commands/refresh) to update the state with the +[refresh](/terraform/cli/commands/refresh) to update the state with the real infrastructure. The primary purpose of Terraform state is to store bindings between objects in @@ -28,13 +29,13 @@ resource instance, and then potentially update or delete that object in response to future configuration changes. For more information on why Terraform requires state and why Terraform cannot -function without state, please see the page [state purpose](/language/state/purpose). +function without state, please see the page [state purpose](/terraform/language/state/purpose). ## Inspection and Modification While the format of the state files are just JSON, direct file editing of the state is discouraged. Terraform provides the -[terraform state](/cli/commands/state) command to perform +[terraform state](/terraform/cli/commands/state) command to perform basic modifications of the state using the CLI. The CLI usage and output of the state commands is structured to be @@ -67,10 +68,10 @@ in new versions. Alternatively, there are several integration points which produce JSON output that is specifically intended for consumption by external software: -* [The `terraform output` command](/cli/commands/output) +* [The `terraform output` command](/terraform/cli/commands/output) has a `-json` option, for obtaining either the full set of root module output values or a specific named output value from the latest state snapshot. -* [The `terraform show` command](/cli/commands/show) has a `-json` +* [The `terraform show` command](/terraform/cli/commands/show) has a `-json` option for inspecting the latest state snapshot in full, and also for inspecting saved plan files which include a copy of the prior state at the time the plan was made. diff --git a/website/docs/language/state/locking.mdx b/website/docs/language/state/locking.mdx index 970c14f2fe..95b3ba4570 100644 --- a/website/docs/language/state/locking.mdx +++ b/website/docs/language/state/locking.mdx @@ -7,26 +7,26 @@ description: >- # State Locking -If supported by your [backend](/language/settings/backends), Terraform will lock your +If supported by your [backend](/terraform/language/backend), Terraform will lock your state for all operations that could write state. This prevents others from acquiring the lock and potentially corrupting your state. State locking happens automatically on all operations that could write -state. You won't see any message that it is happening. If state locking fails, -Terraform will not continue. You can disable state locking for most commands -with the `-lock` flag but it is not recommended. +state. You do not see any message that it happens. If state locking fails, +Terraform does not continue. You can disable state locking for most commands +with the `-lock=false` flag, but we do not recommend it. -If acquiring the lock is taking longer than expected, Terraform will output -a status message. If Terraform doesn't output a message, state locking is +If acquiring the lock takes longer than expected, Terraform outputs +a status message. If Terraform does not output a message, state locking is still occurring if your backend supports it. Not all backends support locking. The -[documentation for each backend](/language/settings/backends) +[documentation for each backend](/terraform/language/backend) includes details on whether it supports locking or not. ## Force Unlock -Terraform has a [force-unlock command](/cli/commands/force-unlock) +Terraform has a [force-unlock command](/terraform/cli/commands/force-unlock) to manually unlock the state if unlocking failed. **Be very careful with this command.** If you unlock the state when someone diff --git a/website/docs/language/state/purpose.mdx b/website/docs/language/state/purpose.mdx index 45fef6cb25..bd66636753 100644 --- a/website/docs/language/state/purpose.mdx +++ b/website/docs/language/state/purpose.mdx @@ -59,7 +59,7 @@ of dependencies within the state. Now Terraform can still determine the correct order for destruction from the state when you delete one or more items from the configuration. -One way to avoid this would be for Terraform to know a required ordering +Terraform could take another approach to dependency order by using an underlying hierarchy of order between resource types. For example, Terraform could know that servers must be deleted before the subnets they are a part of. The complexity for this approach quickly explodes, however: in addition to Terraform having to understand the @@ -102,7 +102,7 @@ started, but when using Terraform in a team it is important for everyone to be working with the same state so that operations will be applied to the same remote objects. -[Remote state](/language/state/remote) is the recommended solution +[Remote state](/terraform/language/state/remote) is the recommended solution to this problem. With a fully-featured state backend, Terraform can use remote locking as a measure to avoid two or more different users accidentally running Terraform at the same time, and thus ensure that each Terraform run diff --git a/website/docs/language/state/refactor.mdx b/website/docs/language/state/refactor.mdx new file mode 100644 index 0000000000..1e4833d60e --- /dev/null +++ b/website/docs/language/state/refactor.mdx @@ -0,0 +1,269 @@ +--- +page_title: Refactor Terraform state +description: >- + Learn how to move resources between state files and identify dependencies between resources. +--- + +# Refactor Terraform state + +As your Terraform configuration grows in complexity, you might want to reorganize your resources to improve maintainability and performance. Terraform operations complete faster when you split configuration with large state files. + +This topic reviews the steps you must take to split your Terraform state across multiple configurations. First, you must plan how you will group your resources and identify inter-resource dependencies. Then you must choose the best approach to migrate resources from one state file to another. Finally, you must decide which approach to take to move resources between state files. + +Even small refactors require that you keep these principles in mind. Keep in mind that refactoring your configuration requires updating both the configuration and the corresponding state files. + +## Identify opportunities to refactor + +When refactoring your Terraform configuration, identify groups of resources that may benefit from a separate cadence of lifecycle management, or logically-related collections of resources that you can capture in a module instead. Refactoring your Terraform configuration should make it easier to maintain. + +The following are some common opportunities for refactoring: + +- **Long Terraform applies.** Your configuration has grown over time and become cumbersome to manage. Large, monolithic configuration can cause Terraform plan and apply operations to take a long time to complete and cause unintended changes. +- **Changes to management lifecycles.** Managing frequently updated resources separately from infrequently updated resources can help simplify your operations and reduce the blast radius of unintended changes. +- **Changes to resource ownership.** In some organizations, teams may split the responsibility of maintaining different parts of the architecture. In these cases, it is common for teams to refactor the configuration and state to match their area of ownership and scope. +- **Reusable module opportunity.** You have identified a subsection of your configuration that would make for a good module. If you create the same set of resources in multiple configurations, we recommend grouping those resources into a module and reusing it across your organization. + +## Plan your refactored state + +You must plan how you will group resources before starting refactoring your configuration. +Consider the following resource properties when deciding how to group resources together: + +- **Volatility and rate of change:** By exposing your long-living infrastructure to unnecessary volatility, you introduce more opportunities for accidental changes. For example, you may scale the number of compute resources your configuration deploys several times a day, but your networking configuration may stay static for months. By managing your network resources separately, you reduce the risk of making unintended changes to it. +- **Stateful versus stateless:** By managing stateful resources independently of stateless ones, such as separating databases from compute instances, you limit the blast radius of operations that re-provision resources and help protect against accidental data loss. +- **Access and team responsibility:** By splitting your workspaces by team, you limit the responsibility per workspace and allow teams to maintain distinct areas of ownership. This helps ensure that only users familiar with specific parts of your infrastructure make changes to the related configuration. + +To learn about different strategies for grouping resources, refer to the [Workspace best practices](/terraform/cloud-docs/workspaces/best-practices) documentation. + +## Identify dependencies + +As you migrate resources from one state file to another, you must identify dependencies between resources. For example, you may have compute resources that depend on your network resources. If you migrate those network resources to a new state file, your compute resources no longer be able to reference the network. + +We recommend using dynamic references rather than hardcoding information about resources in another configuration. Hardcoding information requires you to manually update the configuration whenever the data changes, which can lead to errors in your configuration or your deployed resources. + +You can use the following dynamic approaches to reference resources in another state file: + +- If your provider supports it, you can use a resource-specific data source to query your cloud provider. For example, you can use the [`aws_vpc`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) data source to look up information about a VPC created in another configuration. +- If you use HCP Terraform or Terraform Enterprise, you can use the [`tfe_outputs`](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) data source to reference outputs from another workspace. +- If you are using another remote backend, or the local backend, you can use the [`terraform_remote_state`](/terraform/language/state/remote-state-data) data source. To access state in an HCP Terraform workspace, you must explicitly specify which other workspaces have access. Refer to [remote state sharing](/terraform/cloud-docs/workspaces/settings#remote-state-sharing) for more information. + + +You can use the [`terraform graph`](/terraform/cli/commands/graph) command to help visualize the relationships between the resources in your configuration and identify any dependencies. + +## Migrate resources + +When you refactor your Terraform configuration, we recommend that you recreate stateless resources in a new Terraform configuration if you can do so without incurring downtime or extra cost. + +Stateful resources, such as databases and object stores, are more complicated to migrate. Often, you cannot delete and recreate them, or it may be complex and expensive to backup and restore the data. In this case, you can migrate your resources by moving them between state files. + +There are two common approaches to migrating resources between two Terraform state files: + +- [Remove and import](#remove-and-import): Explicitly remove resources from one Terraform state file, then add it to another. We recommend this approach since the `removed` and `import` blocks help keep a record of the configuration history. +- [Move resources directly to a new state file](#move-resources-directly-to-a-new-state-file): Use the `terraform state mv` command to move resources into a different state file. This is a legacy command, but is still supported in all versions of Terraform version 1.0 and newer. + +### Requirements + +[Removing and importing resources](#remove-and-import) requires Terraform version 1.7 or newer. + +[Moving resources directly to a new state file](#move-resources-directly-to-a-new-state-file) using the Terraform CLI requires Terraform version 1.0 or newer. + +### Remove and import + +You can use Terraform's configuration-driven `removed` and `import` blocks to move resources between state files without destroying them. + +The following example is an initial resource configuration that you will move to a new state file. + + + +```hcl +resource "aws_instance" "example" { + instance_type = "t3.micro" + ami = data.aws_ami.example.id +} +``` + + + +In your source configuration, complete the following steps to remove the resources: + +1. Retrieve your latest Terraform state and save the output to a file as a backup. + + ```shell-session + $ terraform state pull > terraform.tfstate.backup + ``` + +1. Review your provider's configuration to check which attribute you must use to import your resource type before you remove it. For example, the import [`aws_instance`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance#import) resource uses the `id` attribute to import an EC2 instance into your Terraform state. + + ```shell-session + $ terrafrom state show aws_instance.example + ##... + id = "i-07b510cff5f79af00" + ##... + ``` + +1. For each resource you want to migrate, replace the `resource` block with a `removed` block to remove the resource from your state without destroying it. These `removed` blocks can exist anywhere in your configuration, but we recommend your organization standardizes where to declare these blocks to help with future maintenance. One common approach is defining the `removed` block in the file that previously contained the corresponding `resource` block. + + + + ```diff + - resource "aws_instance" "example" { + - instance_type = "t3.micro" + - ami = data.aws_ami.example.id + - } + + + removed { + + from = aws_instance.example + + lifecycle { + + destroy = false + + } + + } + ``` + + + +1. Run `terraform plan` to ensure that Terraform will not destroy the resources. + + + + ```bash + # aws_instance.example will no longer be managed by Terraform, but will not be destroyed + # (destroy = false is set in the configuration) + . resource "aws_instance" "example" { + id = "i-07b510cff5f79af00" + ##... + ``` + + + +1. Run `terraform apply` to remove the resource from state. + +Then, in your destination configuration, complete the following steps to import the resources: + +1. Add the resources to the configuration you are migrating the resources to. +1. For each added resource, add an `import` block to add the resource to your state without recreating it. These `import` blocks can exist anywhere in your configuration, but we recommend you standardize on location in your organization to help with future maintenance. One common practice is to define the `import` block in the same file you add the `resource` block to. + + ```hcl + resource "aws_instance" "example" { + instance_type = "t3.micro" + ami = data.aws_ami.example.id + } + + import { + id = "i-07b510cff5f79af00" + to = aws_instance.example + } + ``` + +1. Run `terraform plan` to ensure that Terraform will properly import the resources. + + + + ```bash + # aws_instance.example will be imported + resource "aws_instance" "example" { + ``` + + + +1. Run `terraform apply` to complete the import. + + ```shell-session + $ terraform apply + + ##... + + Plan: 1 to import, 0 to add, 0 to change, 0 to destroy. + + Do you want to perform these actions? + Terraform will perform the actions described above. + Only 'yes' will be accepted to approve. + + Enter a value: yes + + aws_instance.example: Importing... [id=i-12345678901234567] + aws_instance.example: Import complete [id=i-12345678901234567] + + Apply complete! Resources: 1 imported, 0 added, 0 changed, 0 destroyed. + ``` + +After you complete the migration you can optionally remove the `removed` and `import` blocks, or choose to keep them as a record of the resource's lifecycle. + +Refer to the following documentation to learn more about the `removed` and `import` blocks: + +- [Removing resources](/terraform/language/resources/syntax#removing-resources) +- [Import block reference documentation](/terraform/language/import) + +### Move resources directly to a new state file + +You can also use the `terraform state mv` command to move resources directly between state files. + + + +The `-state` and `-state-out` flags are legacy options that Terraform maintains for backwards compatibility. + +This approach also uses the `terraform state pull` and `terraform state push` commands. While Terraform performs safety checks when you manually update remote state, this method does have some risk of corrupting your remote state. + +We recommend that you use the `removed` and `import` blocks documented above for any new migrations. + + + +#### Prepare the state files + +If you are using the local state backend, you can move resources directly between two state files. If you are using a remote state backend, such as HCP Terraform or AWS S3, you must first download the state files for the source and destination workspaces. + +To download the state files, complete the following steps: + +1. In the directory with your source configuration, use the `terraform state pull` command to save the state file locally. + + ```shell-session + $ terraform state pull > source.tfstate + ``` + +1. In the directory with your destination configuration, use the `terraform state pull` command to save the state file locally. + + ```shell-session + $ terraform state pull > destination.tfstate + ``` + +#### Move the resources + +Next, use the `terraform state mv` command to move the resource from the source state to the destination state. + +```shell-session +$ terraform state mv -state source/source.tfstate -state-out destination/destination.tfstate aws_instance.example aws_instance.example + +Move "aws_instance.example" to "aws_instance.example" +Successfully moved 1 object(s). +``` + +The `terraform state mv` command uses the `-state` flag to point to the source state file, and the `-state-out` flag to point to the destination state file. The final two arguments tell Terraform which resource to move out from the source state, and the resource to move it to in the destination state. + +Repeat this step for every resource you want to migrate. + +#### Push the state files + +If you are using a remote backend, you must push the state files to the backend with the `terraform state push` command. + +1. In the directory with your source configuration, use the `terraform state push` command to update the remote state. + + ```shell-session + $ terraform state push source.tfstate + ``` + +1. In the directory with your destination configuration, use the `terraform state push` command to update the remote state. + + ```shell-session + $ terraform state push destination.tfstate + ``` + +#### Update your configuration and verify the migration + +Next, update your configuration to match your state, and then verify that your changes are correct. + +1. Remove the resources that you migrated from your source configuration. +1. Run `terraform plan` on your source configuration and ensure that Terraform will not make any changes to your infrastructure. +1. Add the resources that you migrated to your destination configuration. +1. Run `terraform plan` on your destination configuration and ensure that Terraform will not make any changes to your infrastructure. +1. Create pull requests for the repositories that store your source and destination configurations. If your code review process creates a speculative plan for changes to your Terraform configuration, review the results and ensure that Terraform will not make any changes to your infrastructure. +1. Merge the pull requests. + +Refer to the [`terraform state mv` command reference documentation](/terraform/cli/commands/state/mv) to learn more. diff --git a/website/docs/language/state/remote-state-data.mdx b/website/docs/language/state/remote-state-data.mdx index 48ead8ea43..c3c230e6b2 100644 --- a/website/docs/language/state/remote-state-data.mdx +++ b/website/docs/language/state/remote-state-data.mdx @@ -7,14 +7,14 @@ description: >- # The `terraform_remote_state` Data Source -[backends]: /language/settings/backends +[backends]: /terraform/language/backend The `terraform_remote_state` data source uses the latest state snapshot from a specified state backend to retrieve the root module output values from some other Terraform configuration. -You can use the `terraform_remote_state` data source without requiring or configuring a provider. It is always available through a built-in provider with the [source address](/language/providers/requirements#source-addresses) `terraform.io/builtin/terraform`. That provider does not include any other resources or data sources. +You can use the `terraform_remote_state` data source without requiring or configuring a provider. It is always available through a built-in provider with the [source address](/terraform/language/providers/requirements#source-addresses) `terraform.io/builtin/terraform`. That provider does not include any other resources or data sources. -~> **Important:** We recommend using the [`tfe_outputs` data source](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) in the [Terraform Cloud/Enterprise Provider](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs) to access remote state outputs in Terraform Cloud or Terraform Enterprise. The `tfe_outputs` data source is more secure because it does not require full access to workspace state to fetch outputs. +~> **Important:** We recommend using the [`tfe_outputs` data source](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) in the [HCP Terraform/Enterprise Provider](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs) to access remote state outputs in HCP Terraform or Terraform Enterprise. The `tfe_outputs` data source is more secure because it does not require full access to workspace state to fetch outputs. ## Alternative Ways to Share Data Between Configurations @@ -35,14 +35,14 @@ limited to) the following: | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Alibaba Cloud DNS
(for IP addresses and hostnames) | [`alicloud_alidns_record` resource type](https://registry.terraform.io/providers/aliyun/alicloud/latest/docs/resources/alidns_record) | Normal DNS lookups, or [the `dns` provider](https://registry.terraform.io/providers/hashicorp/dns/latest/docs) | | Amazon Route53
(for IP addresses and hostnames) | [`aws_route53_record` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | Normal DNS lookups, or [the `dns` provider](https://registry.terraform.io/providers/hashicorp/dns/latest/docs) | -| Amazon S3 | [`aws_s3_bucket_object` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_object) | [`aws_s3_bucket_object` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_bucket_object) | +| Amazon S3 | [`aws_s3_object` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | [`aws_s3_object` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/s3_object) | | Amazon SSM Parameter Store | [`aws_ssm_parameter` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssm_parameter) | [`aws_ssm_parameter` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter) | | Azure Automation | [`azurerm_automation_variable_string` resource type](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/automation_variable_string) | [`azurerm_automation_variable_string` data source](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/automation_variable_string) | | Azure DNS
(for IP addresses and hostnames) | [`azurerm_dns_a_record` resource type](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/dns_a_record), etc | Normal DNS lookups, or [the `dns` provider](https://registry.terraform.io/providers/hashicorp/dns/latest/docs) | | Google Cloud DNS
(for IP addresses and hostnames) | [`google_dns_record_set` resource type](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/dns_record_set) | Normal DNS lookups, or [the `dns` provider](https://registry.terraform.io/providers/hashicorp/dns/latest/docs) | | Google Cloud Storage | [`google_storage_bucket_object` resource type](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket_object) | [`google_storage_bucket_object` data source](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/storage_bucket_object) and [`http` data source](https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http) | | HashiCorp Consul | [`consul_key_prefix` resource type](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/resources/key_prefix) | [`consul_key_prefix` data source](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/data-sources/key_prefix) | -| HashiCorp Terraform Cloud | Normal `outputs` terraform block | [`tfe_outputs` data source](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) | +| HashiCorp HCP Terraform | Normal `outputs` terraform block | [`tfe_outputs` data source](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) | | Kubernetes | [`kubernetes_config_map` resource type](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/config_map) | [`kubernetes_config_map` data source](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/data-sources/config_map) | | OCI Object Storage | [`oci_objectstorage_bucket` resource type](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/resources/objectstorage_object) | [`oci_objectstorage_bucket` data source](https://registry.terraform.io/providers/hashicorp/oci/latest/docs/data-sources/objectstorage_object) | @@ -68,7 +68,7 @@ use of. For example: store or Consul service catalog can make that data also accessible via [Consul Template](https://github.com/hashicorp/consul-template) or the - [HashiCorp Nomad](https://www.nomadproject.io/docs/job-specification/template) + [HashiCorp Nomad](/nomad/docs/job-specification/template) `template` stanza. * If you use Kubernetes then you can [make Config Maps available to your Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/). @@ -76,14 +76,14 @@ use of. For example: Some of the data stores listed above are specifically designed for storing small configuration values, while others are generic blob storage systems. For those generic systems, you can use -[the `jsonencode` function](/language/functions/jsonencode) +[the `jsonencode` function](/terraform/language/functions/jsonencode) and -[the `jsondecode` function](/language/functions/jsondecode) respectively +[the `jsondecode` function](/terraform/language/functions/jsondecode) respectively to store and retrieve structured data. You can encapsulate the implementation details of retrieving your published configuration data by writing a -[data-only module](/language/modules/develop/composition#data-only-modules) +[data-only module](/terraform/language/modules/develop/composition#data-only-modules) containing the necessary data source configuration and any necessary post-processing such as JSON decoding. You can then change that module later if you switch to a different strategy for sharing data between multiple @@ -153,7 +153,7 @@ The following arguments are supported: The `config` object can use any arguments that would be valid in the equivalent `terraform { backend "" { ... } }` block. See - [the documentation of your chosen backend](/language/settings/backends) + [the documentation of your chosen backend](/terraform/language/backend) for details. -> **Note:** If the backend configuration requires a nested block, specify @@ -167,8 +167,8 @@ The following arguments are supported: In addition to the above, the following attributes are exported: * (v0.12+) `outputs` - An object containing every root-level - [output](/language/values/outputs) in the remote state. -* (<= v0.11) `` - Each root-level [output](/language/values/outputs) + [output](/terraform/language/values/outputs) in the remote state. +* (<= v0.11) `` - Each root-level [output](/terraform/language/values/outputs) in the remote state appears as a top level attribute on the data source. ## Root Outputs Only diff --git a/website/docs/language/state/remote.mdx b/website/docs/language/state/remote.mdx index 77cce472c8..73875a2c17 100644 --- a/website/docs/language/state/remote.mdx +++ b/website/docs/language/state/remote.mdx @@ -15,16 +15,16 @@ Terraform at the same time. With _remote_ state, Terraform writes the state data to a remote data store, which can then be shared between all members of a team. Terraform supports -storing state in [Terraform Cloud](https://www.hashicorp.com/products/terraform/), +storing state in [HCP Terraform](https://www.hashicorp.com/products/terraform/), [HashiCorp Consul](https://www.consul.io/), Amazon S3, Azure Blob Storage, Google Cloud Storage, Alibaba Cloud OSS, and more. -Remote state is implemented by a [backend](/language/settings/backends) or by -Terraform Cloud, both of which you can configure in your configuration's root module. +Remote state is implemented by a [backend](/terraform/language/backend) or by +HCP Terraform, both of which you can configure in your configuration's root module. ## Delegation and Teamwork Remote state allows you to share -[output values](/language/values/outputs) with other configurations. +[output values](/terraform/language/values/outputs) with other configurations. This allows your infrastructure to be decomposed into smaller components. Put another way, remote state also allows teams to share infrastructure @@ -38,7 +38,7 @@ you can expose things such as VPC IDs, subnets, NAT instance IDs, etc. through remote state and have other Terraform states consume that. For example usage, see -[the `terraform_remote_state` data source](/language/state/remote-state-data). +[the `terraform_remote_state` data source](/terraform/language/state/remote-state-data). While remote state can be a convenient, built-in mechanism for sharing data between configurations, you may prefer to use more general stores to @@ -52,10 +52,10 @@ another that consumes those values using ## Locking and Teamwork For fully-featured remote backends, Terraform can also use -[state locking](/language/state/locking) to prevent concurrent runs of +[state locking](/terraform/language/state/locking) to prevent concurrent runs of Terraform against the same state. -[Terraform Cloud by HashiCorp](https://www.hashicorp.com/products/terraform/) +[HCP Terraform by HashiCorp](https://www.hashicorp.com/products/terraform/) is a commercial offering that supports an even stronger locking concept that can also detect attempts to create a new plan when an existing plan is already awaiting approval, by queuing Terraform operations in a central location. diff --git a/website/docs/language/state/sensitive-data.mdx b/website/docs/language/state/sensitive-data.mdx index 63f624751e..788654a0d6 100644 --- a/website/docs/language/state/sensitive-data.mdx +++ b/website/docs/language/state/sensitive-data.mdx @@ -6,20 +6,24 @@ description: Sensitive data in Terraform state. # Sensitive Data in State Terraform state can contain sensitive data, depending on the resources in use -and your definition of "sensitive." The state contains resource IDs and all -resource attributes. For resources such as databases, this may contain initial +and your definition of "sensitive." Unless your variables or resources are `ephemeral`, the state contains resource IDs and all resource attributes. For resources such as databases, this can contain initial passwords. When using local state, state is stored in plain-text JSON files. -When using [remote state](/language/state/remote), state is only ever held in -memory when used by Terraform. It may be encrypted at rest, but this depends on -the specific remote state backend. +When using [remote state](/terraform/language/state/remote), state is only ever held in memory when used by Terraform. It may be encrypted at rest, but this depends on the specific remote state backend. + +## Ephemeral data + +-> **Note**: Ephemeral variables, outputs, and resources are available in Terraform v1.10 and later. + +Terraform allows you to mark variables and outputs as ephemeral. Providers can also support specific `ephemeral` resources. Ephemerality in Terraform means that the data of a block is available during runtime, but Terraform does not write that data to state or plan files. The `ephemeral` property is helpful when managing credentials, tokens, or other temporary resources you do not want to store in Terraform state files. + +Learn more about defining ephemeral [input variables](/terraform/language/values/variables#exclude-values-from-state), [outputs](/terraform/language/values/outputs#ephemeral-avoid-storing-values-in-state-or-plan-files), and [resources](/terraform/language/resources/ephemeral). ## Recommendations -If you manage any sensitive data with Terraform (like database passwords, user -passwords, or private keys), treat the state itself as sensitive data. +Treat the state as sensitive data if you manage secret credentials like database passwords, user passwords, or private keys with Terraform. You can also mark your sensitive data in variables as `ephemeral` to prevent Terraform from writing those variables to your state and plan files. Storing state remotely can provide better security. As of Terraform 0.9, Terraform does not persist state to the local disk when remote state is in use, @@ -27,11 +31,12 @@ and some backends can be configured to encrypt the state data at rest. For example: -- [Terraform Cloud](/cloud) always encrypts state at rest and - protects it with TLS in transit. Terraform Cloud also knows the identity of +- [HCP Terraform](https://cloud.hashicorp.com/products/terraform) always encrypts state at rest and + protects it with TLS in transit. HCP Terraform also knows the identity of the user requesting state and maintains a history of state changes. This can - be used to control access and track activity. [Terraform Enterprise](/enterprise) + be used to control access and track activity. [Terraform Enterprise](/terraform/enterprise) also supports detailed audit logging. - The S3 backend supports encryption at rest when the `encrypt` option is enabled. IAM policies and logging can be used to identify any invalid access. Requests for the state go over a TLS connection. +- The GCS (Google Cloud Storage) backend supports using [customer-supplied](/terraform/language/backend/gcs#customer-supplied-encryption-keys) or [customer-managed (Cloud KMS)](/terraform/language/backend/gcs#customer-managed-encryption-keys-cloud-kms) encryption keys. diff --git a/website/docs/language/state/workspaces.mdx b/website/docs/language/state/workspaces.mdx index 424aaed645..88f7515248 100644 --- a/website/docs/language/state/workspaces.mdx +++ b/website/docs/language/state/workspaces.mdx @@ -7,74 +7,39 @@ description: >- # Workspaces -Each Terraform configuration has an associated [backend](/language/settings/backends) -that defines how operations are executed and where persistent data such as -[the Terraform state](/language/state/purpose) are -stored. +Each Terraform configuration has an associated [backend](/terraform/language/backend) that defines how Terraform executes operations and where Terraform stores persistent data, like [state](/terraform/language/state/purpose). -The persistent data stored in the backend belongs to a _workspace_. Initially -the backend has only one workspace, called "default", and thus there is only -one Terraform state associated with that configuration. - -Certain backends support _multiple_ named workspaces, allowing multiple states -to be associated with a single configuration. The configuration still -has only one backend, but multiple distinct instances of that configuration -to be deployed without configuring a new backend or changing authentication +The persistent data stored in the backend belongs to a workspace. The backend initially has only one workspace containing one Terraform state associated with that configuration. Some backends support multiple named workspaces, allowing multiple states to be associated with a single configuration. The configuration still has only one backend, but you can deploy multiple distinct instances of that configuration without configuring a new backend or changing authentication credentials. -Multiple workspaces are currently supported by the following backends: +-> **Note**: The Terraform CLI workspaces are different from [workspaces in HCP Terraform](/terraform/cloud-docs/workspaces). Refer to [Connect to HCP Terraform](/terraform/cli/cloud/settings) for details about migrating a configuration with multiple workspaces to HCP Terraform. -* [AzureRM](/language/settings/backends/azurerm) -* [Consul](/language/settings/backends/consul) -* [COS](/language/settings/backends/cos) -* [etcdv3](/language/settings/backends/etcdv3) -* [GCS](/language/settings/backends/gcs) -* [Kubernetes](/language/settings/backends/kubernetes) -* [Local](/language/settings/backends/local) -* [Manta](/language/settings/backends/manta) -* [OSS](/language/settings/backends/oss) -* [Postgres](/language/settings/backends/pg) -* [Remote](/language/settings/backends/remote) -* [S3](/language/settings/backends/s3) -* [Swift](/language/settings/backends/swift) +## Backends Supporting Multiple Workspaces -In the 0.9 line of Terraform releases, this concept was known as "environment". -It was renamed in 0.10 based on feedback about confusion caused by the -overloading of the word "environment" both within Terraform itself and within -organizations that use Terraform. +You can use multiple workspaces with the following backends: + +- [AzureRM](/terraform/language/backend/azurerm) +- [Consul](/terraform/language/backend/consul) +- [COS](/terraform/language/backend/cos) +- [GCS](/terraform/language/backend/gcs) +- [Kubernetes](/terraform/language/backend/kubernetes) +- [Local](/terraform/language/backend/local) +- [OSS](/terraform/language/backend/oss) +- [Postgres](/terraform/language/backend/pg) +- [Remote](/terraform/language/backend/remote) +- [S3](/terraform/language/backend/s3) --> **Note**: The Terraform CLI workspace concept described in this document is -different from but related to the Terraform Cloud -[workspace](/cloud-docs/workspaces) concept. -If you use multiple Terraform CLI workspaces in a single Terraform configuration -and are migrating that configuration to Terraform Cloud, refer to [Initializing and Migrating](/cli/cloud/migrating). ## Using Workspaces -Terraform starts with a single workspace named "default". This -workspace is special both because it is the default and also because -it cannot ever be deleted. If you've never explicitly used workspaces, then -you've only ever worked on the "default" workspace. +~> **Important:** Workspaces are not appropriate for system decomposition or deployments requiring separate credentials and access controls. Refer to [Use Cases](/terraform/cli/workspaces#use-cases) in the Terraform CLI documentation for details and recommended alternatives. -Workspaces are managed with the `terraform workspace` set of commands. To -create a new workspace and switch to it, you can use `terraform workspace new`; -to switch workspaces you can use `terraform workspace select`; etc. +Terraform starts with a single, default workspace named `default` that you cannot delete. If you have not created a new workspace, you are using the default workspace in your Terraform working directory. -For example, creating a new workspace: +When you run `terraform plan` in a new workspace, Terraform does not access existing resources in other workspaces. These resources still physically exist, but you must switch workspaces to manage them. -```text -$ terraform workspace new bar -Created and switched to workspace "bar"! +Refer to the [Terraform CLI workspaces](/terraform/cli/workspaces) documentation for full details about how to create and use workspaces. -You're now on a new, empty workspace. Workspaces isolate their state, -so if you run "terraform plan" Terraform will not see any existing state -for this configuration. -``` - -As the command says, if you run `terraform plan`, Terraform will not see -any existing resources that existed on the default (or any other) workspace. -**These resources still physically exist,** but are managed in another -Terraform workspace. ## Current Workspace Interpolation @@ -88,7 +53,7 @@ to spin up smaller cluster sizes. For example: ```hcl resource "aws_instance" "example" { - count = "${terraform.workspace == "default" ? 5 : 1}" + count = terraform.workspace == "default" ? 5 : 1 # ... other arguments } @@ -106,103 +71,3 @@ resource "aws_instance" "example" { # ... other arguments } ``` - -## When to use Multiple Workspaces - -Named workspaces allow conveniently switching between multiple instances of -a _single_ configuration within its _single_ backend. They are convenient in -a number of situations, but cannot solve all problems. - -A common use for multiple workspaces is to create a parallel, distinct copy of -a set of infrastructure in order to test a set of changes before modifying the -main production infrastructure. For example, a developer working on a complex -set of infrastructure changes might create a new temporary workspace in order -to freely experiment with changes without affecting the default workspace. - -Non-default workspaces are often related to feature branches in version control. -The default workspace might correspond to the "main" or "trunk" branch, -which describes the intended state of production infrastructure. When a -feature branch is created to develop a change, the developer of that feature -might create a corresponding workspace and deploy into it a temporary "copy" -of the main infrastructure so that changes can be tested without affecting -the production infrastructure. Once the change is merged and deployed to the -default workspace, the test infrastructure can be destroyed and the temporary -workspace deleted. - -When Terraform is used to manage larger systems, teams should use multiple -separate Terraform configurations that correspond with suitable architectural -boundaries within the system so that different components can be managed -separately and, if appropriate, by distinct teams. Workspaces _alone_ -are not a suitable tool for system decomposition, because each subsystem should -have its own separate configuration and backend, and will thus have its own -distinct set of workspaces. - -In particular, organizations commonly want to create a strong separation -between multiple deployments of the same infrastructure serving different -development stages (e.g. staging vs. production) or different internal teams. -In this case, the backend used for each deployment often belongs to that -deployment, with different credentials and access controls. Named workspaces -are _not_ a suitable isolation mechanism for this scenario. - -Instead, use one or more [re-usable modules](/language/modules/develop) to -represent the common elements, and then represent each instance as a separate -configuration that instantiates those common elements in the context of a -different backend. In that case, the root module of each configuration will -consist only of a backend configuration and a small number of `module` blocks -whose arguments describe any small differences between the deployments. - -Where multiple configurations are representing distinct system components -rather than multiple deployments, data can be passed from one component to -another using paired resources types and data sources. For example: - -* Where a shared [Consul](https://www.consul.io/) cluster is available, use - [`consul_key_prefix`](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/resources/key_prefix) to - publish to the key/value store and [`consul_keys`](https://registry.terraform.io/providers/hashicorp/consul/latest/docs/data-sources/keys) - to retrieve those values in other configurations. - -* In systems that support user-defined labels or tags, use a tagging convention - to make resources automatically discoverable. For example, use - [the `aws_vpc` resource type](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) - to assign suitable tags and then - [the `aws_vpc` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) - to query by those tags in other configurations. - -* For server addresses, use a provider-specific resource to create a DNS - record with a predictable name and then either use that name directly or - use [the `dns` provider](https://registry.terraform.io/providers/hashicorp/dns/latest/docs) to retrieve - the published addresses in other configurations. - -* If a Terraform state for one configuration is stored in a remote backend - that is accessible to other configurations then - [`terraform_remote_state`](/language/state/remote-state-data) - can be used to directly consume its root module outputs from those other - configurations. This creates a tighter coupling between configurations, - but avoids the need for the "producer" configuration to explicitly - publish its results in a separate system. - -## Workspace Internals - -Workspaces are technically equivalent to renaming your state file. They -aren't any more complex than that. Terraform wraps this simple notion with -a set of protections and support for remote state. - -For local state, Terraform stores the workspace states in a directory called -`terraform.tfstate.d`. This directory should be treated similarly to -local-only `terraform.tfstate`; some teams commit these files to version -control, although using a remote backend instead is recommended when there are -multiple collaborators. - -For [remote state](/language/state/remote), the workspaces are stored -directly in the configured [backend](/language/settings/backends). For example, if you -use [Consul](/language/settings/backends/consul), the workspaces are stored -by appending the workspace name to the state path. To ensure that -workspace names are stored correctly and safely in all backends, the name -must be valid to use in a URL path segment without escaping. - -The important thing about workspace internals is that workspaces are -meant to be a shared resource. They aren't a private, local-only notion -(unless you're using purely local state and not committing it). - -The "current workspace" name is stored locally in the ignored -`.terraform` directory. This allows multiple team members to work on -different workspaces concurrently. Workspace names are also attached to associated remote workspaces in Terraform Cloud. For more details about workspace names in Terraform Cloud, refer to the [remote backend](/language/settings/backends/remote#workspaces) and [CLI Integration (recommended)](/cli/cloud/settings#arguments) documentation. diff --git a/website/docs/language/style.mdx b/website/docs/language/style.mdx new file mode 100644 index 0000000000..9bd0e3ef82 --- /dev/null +++ b/website/docs/language/style.mdx @@ -0,0 +1,681 @@ +--- +page_title: Style Guide - Configuration Language +description: >- + Learn recommended style conventions for Terraform configuration and + workflows. +--- + +# Style Guide + +The flexibility of Terraform's configuration language gives you many options to choose from as you write your code, structure your directories, and test your configuration. While some design decisions depend on your organization's needs or preferences, there are some common patterns that we suggest you adopt. Adopting and adhering to a style guide keeps your Terraform code legible, scalable, and maintainable. + +This article discusses best practices and some considerations to keep in mind as you develop your organization's style guide. The article is split into two sections. The first section covers code style recommendations, such as formatting and resource organization. The second section covers operations and workflow recommendations, such as lifecycle management through meta-arguments, versioning, and sensitive data management. + +## Code style + +Writing Terraform code in a consistent style makes it easier to read and maintain. The following sections discuss code style recommendations, including the following: + +- Run `terraform fmt` and `terraform validate` before committing your code to version control. +- Use a linter such as [TFLint](https://github.com/terraform-linters/tflint) to enforce your organization's own coding best practices. +- Use `#` for single and multi-line comments. +- Use nouns for resource names and do not include the resource type in the name. +- Use underscores to separate multiple words in names. Wrap the resource type and name in double quotes in your resource definition. +- Let your code build on itself: define dependent resources after the resources that reference them. +- Include a type and description for every variable. +- Include a description for every output. +- Avoid overuse of variables and local values. +- Always include a default provider configuration. +- Use `count` and `for_each` sparingly. + +## Code formatting + +The Terraform parser allows you some flexibility in how you lay out the elements in your configuration files, but the Terraform language also has some idiomatic style conventions which we recommend users always follow for consistency between files and modules written by different teams. + +- Indent two spaces for each nesting level +- When multiple arguments with single-line values appear on consecutive lines at the same nesting level, align their equals signs: + + + + ```hcl + ami = "abc123" + instance_type = "t2.micro" + ``` + + + +- When both arguments and blocks appear together inside a block body, place all of the arguments together at the top and then place nested blocks below them. Use one blank line to separate the arguments from the blocks. +- Use empty lines to separate logical groups of arguments within a block. +- For blocks that contain both arguments and "meta-arguments" (as defined by the Terraform language semantics), list meta-arguments first and separate them from other arguments with one blank line. Place meta-argument blocks last and separate them from other blocks with one blank line. Refer to [dynamic resource count](#dynamic-resource-count) for more information on meta-arguments. + + + + ```hcl + resource "aws_instance" "example" { + # meta-argument first + count = 2 + + ami = "abc123" + instance_type = "t2.micro" + + network_interface { + # ... + } + + # meta-argument block last + lifecycle { + create_before_destroy = true + } + } + ``` + + + +- Top-level blocks should always be separated from one another by one blank line. Nested blocks should also be separated by blank lines, except when grouping together related blocks of the same type (like multiple `provisioner` blocks in a resource). +- Avoid grouping multiple blocks of the same type with other blocks of a different type, unless the block types are defined by semantics to form a family. (For example: `root_block_device`, `ebs_block_device` and `ephemeral_block_device` on `aws_instance` form a family of block types describing AWS block devices, and can therefore be grouped together and mixed.) + +The `terraform fmt` command formats your Terraform configuration to a subset of the above recommendations. By default, the `terraform fmt` command will only modify your Terraform code in the directory that you execute it in, but you can include the `-recursive` flag to modify code in all subdirectories as well. + +We recommend that you run `terraform fmt` before each commit to version control. You can use mechanisms such as [Git pre-commit hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) to automatically run this command each time you commit your code. + +If you use Microsoft VS Code, use the [Terraform VS Code extension](https://marketplace.visualstudio.com/items?itemName=HashiCorp.terraform) to enable features such as syntax highlighting and validation, automatic code formatting, and integration with HCP Terraform. If your development environment or text editor supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/), you can use the [Terraform Language Server](https://github.com/hashicorp/terraform-ls) to access most of the VS Code extension features. + +## Code validation + +The `terraform validate` command checks that your configuration is syntactically valid and internally consistent. The `validate` command does not check if argument values are valid for a specific provider, but it will verify that they are the correct type. It does not evaluate any existing state. + +The `terraform validate` command is safe to run automatically and frequently. You can configure your text editor to run this command as a post-save check, define it as a pre-commit hook in a Git repository, or run it as a step in a CI/CD pipeline. + +For more information, refer to the [Terraform `validate` documentation](/terraform/cli/commands/validate). + +## File names + +We recommend the following file naming conventions: + +- A `backend.tf` file that contains your [backend configuration](/terraform/language/backend). You can define multiple `terraform` blocks in your configuration to separate your backend configuration from your Terraform and provider versioning configuration. +- A `main.tf` file that contains all resource and data source blocks. +- A `outputs.tf` file that contains all output blocks in alphabetical order. +- A `providers.tf` file that contains all `provider` blocks and configuration. +- A `terraform.tf` file that contains a single `terraform` block which defines your `required_version` and `required_providers`. +- A `variables.tf` file that contains all variable blocks in alphabetical order. +- A `locals.tf` file that contains local values. Refer to [local values](#local-values) for more information. +- A `override.tf` file that contains override definitions for your configuration. Terraform loads this and all files ending with `_override.tf` last. Use them sparingly and add comments to the original resource definitions, as these overrides make your code harder to reason about. Refer to the [override files](/terraform/language/files/override) documentation for more information. + +As your codebase grows, limiting it to just these files can become difficult to maintain. If your code becomes hard to navigate due to its size, we recommend that you organize resources and data sources in separate files by logical groups. For example, if your web application requires networking, storage, and compute resources, you might create the following files: + +- A `network.tf` file that contains your VPC, subnets, load balancers, and all other networking resources. +- A `storage.tf` file that contains your object storage and related permissions configuration. +- A `compute.tf` file that contains your compute instances. + + +No matter how you decide to split your code, it should be immediately clear where a maintainer can find a specific resource or data source definition. + +As your configuration grows, you may need to separate it into multiple state files. The HashiCorp Well-Architected Framework provides more guidance about [configuration structure and scope](/well-architected-framework/operational-excellence/operational-excellence-workspaces-projects#workspace-structure). + +## Linting and static code analysis + +Terraform does not have a built-in linter, but many organizations rely on a third party linting tool such as [TFLint](https://github.com/terraform-linters/tflint) to enforce code standards. A linter uses static code analysis to compare your Terraform code against a set of rules. Most linters ship with a default set of rules, but also let you write your own. + +## Comments + +Write your code so it is easy to understand. Only when necessary, use comments to clarify complexity for other maintainers. + +Use `#` for both single- and multi-line comments. The `//` and `/* */` comment syntaxes are not considered idiomatic, but Terraform supports them to remain backwards-compatible with earlier versions of HCL. + + + +```hcl +# Each tunnel is responsible for encrypting and decrypting traffic exiting +# and leaving its associated gateway. +resource "google_compute_vpn_tunnel" "tunnel1" { + ## ... +``` + + + +## Resource naming + +Every resource within a configuration must have a unique name. For consistency and readability, use a descriptive noun and separate words with underscores. Do not include the resource type in the resource identifier since the resource address already includes it. Wrap the resource type and name in double quotes. + +❌ Bad: + + + +```hcl +resource aws_instance webAPI-aws-instance {...} +``` + + + +✅ Good: + + + +```hcl +resource "aws_instance" "web_api" {...} +``` + + + +## Resource order + +The order of the resources and data sources in your code does not affect how Terraform builds them, so organize your resources for readability. Terraform determines the creation order based on cross-resource dependencies. + +How you order your resources largely depends on the size and complexity of your code, but we recommend defining data sources alongside the resources that reference them. For readability, your Terraform code should “build on itself” — you should define a data source before the resource that references it. + +The following example defines an `aws_instance` that relies on two data sources, `aws_ami` and `aws_availability_zone`. For readability and continuity, it defines the data sources before the `aws_instance` resource. + + + +```hcl +data "aws_ami" "web" { + ##... +} + +data "aws_availability_zones" "available" { + ##... +} + +resource "aws_instance" "web" { + ami = data.aws_ami.web.id + availability_zone = data.aws_availability_zones.available.names[0] + ##... +} +``` + + + +We recommend following a consistent order for resource parameters: + +1. If present, The `count` or `for_each` meta-argument. +1. Resource-specific non-block parameters. +1. Resource-specific block parameters. +1. If required, a `lifecycle` block. +1. If required, the `depends_on` parameter. + +## Variables + +While variables make your modules more flexible, overusing variables can make code difficult to understand. When deciding whether to expose a variable for a resource setting, consider whether that parameter will change between deployments. + +- Define a `type` and a `description` for every variable. +- If the variable is optional, define a reasonable `default`. +- For sensitive variables, such as passwords and private keys, set the `sensitive` parameter to `true`. Remember that Terraform will still store this value in plain text in its state, but it will not display it when you run `terraform plan` or `terraform apply`. Refer to [secrets management](#secrets-management) for more information on how to securely handle sensitive values. +- Use [input variable validation](/terraform/language/values/variables#custom-validation-rules) to create additional rules for your variable values in addition to Terraform's type validation. Only use variable validation when your variable values have uniquely restrictive requirements. For example, if your Terraform configuration requires two web instances, add a `validation` block to enforce it: + + + + ```hcl + variable "web_instance_count" { + type = number + description = "Number of web instances to deploy. This application requires at least two instances." + + validation { + condition = var.web_instance_count > 1 + error_message = "This application requires at least two web instances." + } + } + ``` + + + +We recommend following a consistent order for variable parameters: + +1. Type +1. Description +1. Default (optional) +1. Sensitive (optional) +1. [Validation blocks](/terraform/language/values/variables#custom-validation-rules) + +## Outputs + +Output values let you expose data about your infrastructure on the command line and make it easy to reference in other Terraform configurations. Like you would for variables, provide a `description` for each output. + +We recommend that you use the following order for your output parameters: + +1. Description +1. Value +1. Sensitive (optional) + +Every variable and output requires a unique name. For consistency and readability, we recommend that you use a descriptive noun and separate words with underscores. + + + + ```hcl +variable "db_disk_size" { + type = number + description = "Disk size for the API database" + default = 100 +} + +variable "db_password" { + type = string + description = "Database password" + sensitive = true +} + +output "web_public_ip" { + description = "Public IP of the web instance" + value = aws_instance.web.public_ip +} +``` + + + +## Local values + +Local values let you reference an [expression](/terraform/language/expressions) or value multiple times. Use local values sparingly, as overuse can make your code harder to understand. + +For example, you can use a local value to create a suffix for the region and environment (for example, `development` or `test`), and append it to multiple resources. + + + +```hcl +locals { + name_suffix = "${var.region}-${var.environment}" +} + +resource "aws_instance" "web" { + ami = data.aws_ami.ubuntu.id + instance_type = "t3.micro" + + tags = { + Name = "web-${local.name_suffix}" + } +} +``` + + + +Define local values in one of two places: + +- If you reference the local value in multiple files, define it in a file named `locals.tf`. +- If the local is specific to a file, define it at the top of that file. + +As for other Terraform objects, use descriptive nouns for local value names and underscores to separate multiple words. + +For more information, refer to the [local values documentation](/terraform/language/values/locals) and the [Simplify Terraform configuration with locals](/terraform/tutorials/configuration-language/locals) tutorial. + +## Provider aliasing + +Provider aliasing lets you define multiple `provider` blocks for the same Terraform provider. Potential use cases for aliases include provisioning resources in multiple regions within a single configuration. The `provider` meta-argument for resources and the `providers` meta-argument for modules specifies which provider to use. + + + +```hcl +provider "aws" { + region = "us-east-1" +} + +provider "aws" { + alias = "west" + region = "us-west-2" +} +``` + + + + + +```hcl +resource "aws_instance" "example" { + provider = aws.west + # ... +} + +module "aws_vpc" { + source = "./aws_vpc" + providers = { + aws = aws.west + } +} +``` + + + +- Any provider block that does not define the `alias` parameter is the default provider configuration. +- Always include a default provider configuration and define all of your providers in the same file. +- If you define multiple instances of a provider, define the default first. +- For non-default providers, define the `alias` as the first parameter of the `provider` block. + +## Dynamic resource count + +The `for_each` and `count` meta-arguments let you create multiple resources from a single `resource` block depending on run-time conditions. You can use these meta-arguments to make your code flexible and reduce duplicate resource blocks. If the resources are almost identical, use `count`. If some of arguments need distinct values that you cannot derive from an integer, use `for_each`. + +The `for_each` meta-argument accepts a `map` or `set` value, and Terraform will create an instance of that resource for each element in the value you provide. In the following example, Terraform creates an `aws_instance` for each of the strings defined in the `web_instances` variable: "ui", "api", "db" and "metrics". The example uses `each.key` to give each instance a unique name. The `web_private_ips` output uses a [for expression](https://developer.hashicorp.com/terraform/language/expressions/for) to create a map of instance names and their private IP addresses, while the `web_ui_public_ip` output addresses the instance with the key "ui" directly. + + + +```hcl +variable "web_instances" { + type = list(string) + description = "A list of instances for the web application" + default = [ + "ui", + "api", + "db", + "metrics" + ] +} +resource "aws_instance" "web" { + for_each = toset(var.web_instances) + ami = data.aws_ami.webapp.id + instance_type = "t3.micro" + tags = { + Name = "web_${each.key}" + } +} +output "web_private_ips" { + description = "Private IPs of the web instances" + value = { + for k, v in aws_instance.web : k => v.private_ip + } +} +output "web_ui_public_ip" { + description = "Public IP of the web UI instance" + value = aws_instance.web["ui"].public_ip +} +``` + + + +The above example will create the following output: + + + +```hcl +web_private_ips = { + "api" = "172.31.25.29" + "db" = "172.31.18.33" + "metrics" = "172.31.26.112" + "ui" = "172.31.20.142" +} +web_ui_public_ip = "18.216.208.182" +``` + + + +Refer to the [for_each meta-argument documentation](/terraform/language/meta-arguments/for_each) for more examples. + +The `count` meta-argument lets you create multiple instances of a resource from a single resource block. Refer to the [count meta-argument documentation](/terraform/language/meta-arguments/count) for examples. + +A common practice to conditionally create resources is to use the `count` meta-argument with a [conditional expression](/terraform/language/expressions/conditionals). In the following example, Terraform will only create the `aws_instance` if `var.enable_metrics` is `true`. + + + +```hcl +variable "enable_metrics" { + description = "True if the metrics server should be deployed" + type = bool + default = true +} + +resource "aws_instance" "web" { + count = var.enable_metrics ? 1 : 0 + + ami = data.aws_ami.webapp.id + instance_type = "t3.micro" + ##... +} +``` + + + +Meta-arguments simplify your code but add complexity, so use them in moderation. If the effect of the meta-argument is not immediately obvious, use a comment for clarification. + +To learn more about these meta-arguments, refer to the [`for_each`](/terraform/language/meta-arguments/for_each) and [`count`](/terraform/language/meta-arguments/count) documentation. + +## .gitignore + +Define a `.gitignore` file for your repository to exclude files that you should not publish to version control, such as your state file. + +Do not commit: + +- Your `terraform.tfstate` state file, including `terraform.tfstate.*` backup state files. +- Your `.terraform.tfstate.lock.info` file. Terraform creates and deletes this file automatically when you run a `terraform apply` command and contains info about your [state lock](/terraform/language/state/locking) +- Your `.terraform` directory, where Terraform downloads providers and child modules. +[Saved plan files](/terraform/cli/commands/plan#out-filename) that you create when you include the `-out` flag when you run `terraform plan`. +- Any `.tfvars` files that contain sensitive information. + +Always commit: + +- All Terraform code files +- Your `.terraform.lock.hcl` [dependency lock file](/terraform/language/files/dependency-lock) +- A `.gitignore` file that excludes the files listed below +- A `README.md` to describe the code, input variables, and outputs + +For an example, refer to [GitHub's Terraform .gitignore file](https://github.com/github/gitignore/blob/main/Terraform.gitignore). + +## Workflow style + +This section reviews standards that enable predictable and secure Terraform workflows, such as: + +- Pin your Terraform, provider, and module versions. +- Name your module repositories using this three-part name `terraform--` when using the HCP Terraform registry. +- Store local modules at `./modules/`. +- Use the [`tfe_outputs`](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) data source or provider-specific data sources to share state between two state files. +- Protect credentials by using [dynamic provider credentials](/terraform/tutorials/cloud/dynamic-credentials) or a secrets manager such as HashiCorp Vault. +- Write [tests](/terraform/language/tests) for your modules. +- Use [policy enforcement](/terraform/cloud-docs/policy-enforcement) on HCP Terraform to set guardrails for infrastructure operations. + +## Version pinning + +To prevent providers and modules upgrades from introducing unintentional changes to your infrastructure, use version pinning. + +Specify provider versions using the [required_providers block](/terraform/language/providers/requirements#requiring-providers). Terraform [version constraints](/terraform/language/providers/requirements#version-constraints) support a range of accepted versions. + +Pin modules to a specific major and minor version as shown in the example below to ensure stability. You can use looser restrictions if you are certain that the module does not introduce breaking changes outside of major version updates. + +We also recommend that you set a minimum required version of the Terraform binary using the `required_version` in your `terraform` block. This requires all operators to use a Terraform version that has all of your configuration's required features. + + + +```hcl +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.34.0" + } + } + + required_version = ">= 1.7" +} +``` + + + +The above example pins the version of the `hashicorp/aws` provider to version `5.34.0`, and requires that operators use Terraform `1.7` or newer. + +For modules sourced from a registry, use the `version` parameter in the `module` block to pin the version. For local modules, Terraform ignores the `version` parameter. + + + +```hcl +module "vault_starter" { + source = "hashicorp/vault-starter/aws" + version = "1.0.0" + ##... +} +``` + + + +## Module repository names + +The Terraform registry requires that repositories match a naming convention for all modules that you publish to the registry. Module repositories must use this three-part name `terraform--`, where `` reflects the type of infrastructure the module manages and `` is the main provider the module uses. The `` segment can contain additional hyphens, for example, `terraform-google-vault` or `terraform-aws-ec2-instance`. + +## Module structure + +Terraform modules define self-contained, reusable pieces of infrastructure-as-code. + +Use modules to group together logically related resources that you need to provision together. For example: + +- A networking module that defines a VPC, along with its subnets, gateway, and security groups. +- An application module defining all resources required for each deployment. This stack could include web servers, databases, storage, and supported networking. + +Review the [module creation recommended pattern documentation](/terraform/tutorials/modules/pattern-module-creation) and [standard module structure](/terraform/language/modules/develop/structure) for guidance on how to structure your modules. + +## Local modules + +Local modules are sourced from local disk rather than a remote module registry. We recommend publishing your modules to a module registry, such as the [HCP Terraform private registry](/terraform/cloud-docs/registry), to easily version, share, and reuse modules across your organization. If you cannot use a module registry, using local modules can simplify maintaining and updating your code. + +We recommend that you define child modules in the `./modules/` directory. + +## Repository structure + +How you structure your modules and Terraform configuration in version control significantly impacts versioning and operations. We recommend that you store your actual infrastructure configuration separately from your module code. + +Store each module in an individual repository. This lets you independently version each module and makes it easier to publish your modules in the private Terraform registry. + +Organize your infrastructure configuration in repositories that group together logically-related resources. For example, a single repository for a web application that requires compute, networking, and database resources . By separating your resources into groups, you limit the number of resources that may be impacted by failures for any operation. + +Another approach is to group all modules and infrastructure configuration into a single monolithic repository, or monorepo. For example, a monorepo may define a collection of local modules for each component of the infrastructure stack, and deploy them in the root module. + + + +``` +. +├── modules +│ ├── function +│ │ ├── main.tf # contains aws_iam_role, aws_lambda_function +│ │ ├── outputs.tf +│ │ └── variables.tf +│ ├── queue +│ │ ├── main.tf # contains aws_sqs_queue +│ │ ├── outputs.tf +│ │ └── variables.tf +│ └── vpc +│ ├── main.tf # contains aws_vpc, aws_subnet +│ ├── outputs.tf +│ └── variables.tf +├── main.tf +├── outputs.tf +└── variables.tf +``` + + + +The advantage of monolithic repositories is having a single source of truth that tracks every infrastructure change. However, monolithic repositories can complicate your CI/CD automation: since any code change triggers a deployment that operates on your entire repository, your workflow must target only the modified directories. You also lose the granular access control, since anyone with repository access can modify any file in it. + +If your organization requires a monolithic approach, HCP Terraform and Terraform Enterprise let you scope a workspace to a specific directory in a repository, simplifying your workflows. + +## Branching strategy + +To collaborate on your Terraform code, we recommend using the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow). This approach uses short-lived branches to help your team quickly review, test, and merge changes to your code. To make changes to your code, you would: + +1. Create a new branch from your main branch +1. Write, commit, and push your changes to the new branch +1. Create a pull request +1. Review the changes with your team +1. Merge the pull request +1. Delete the branch + +HCP Terraform and Terraform Enterprise can run [speculative plans for pull requests](/terraform/cloud-docs/run/ui#speculative-plans-on-pull-requests). These speculative plans run automatically when you create or update a pull request, and you can use them to see the effect that your changes will have on your infrastructure before you merge them to your main branch. When you merge your pull request, HCP Terraform will start a new run to apply these changes. + +## Multiple environments + +We recommend that your repository's `main` branch be the source of truth for all environments. For HCP Terraform and Terraform Enterprise users, we recommend that you use separate workspaces for each environment. For larger codebases, we recommend that you split your resources across multiple workspaces to prevent large state files and limit unintended consequences from changes. For example, you could structure your code as follows: + + + +``` +. +├── compute +│ ├── main.tf +│ ├── outputs.tf +│ └── variables.tf +├── database +│ ├── main.tf +│ ├── outputs.tf +│ └── variables.tf +└── networking + ├── main.tf + ├── outputs.tf + └── variables.tf +``` + + + +In this scenario, you would create three workspaces per environment. For example, your production environment would have a `prod-compute`, `prod-database`, and `prod-networking` workspace. Read more about [Terraform workspace and project best practices](/well-architected-framework/operational-excellence/operational-excellence-workspaces-projects). + +If you do not use HCP Terraform or Terraform Enterprise, we recommend that you use modules to encapsulate your configuration, and use a directory for each environment so that each one has a separate state file. The configuration in each of these directories would call the local modules, each with parameters specific to their environment. This also lets you maintain separate variable and backend configurations for each environment. + + + +``` +├── modules +│ ├── compute +│ │ └── main.tf +│ ├── database +│ │ └── main.tf +│ └── network +│ └── main.tf +├── dev +│ ├── backend.tf +│ ├── main.tf +│ └── variables.tf +├── prod +│ ├── backend.tf +│ ├── main.tf +│ └── variables.tf +└── staging + ├── backend.tf + ├── main.tf + └── variables.tf +``` + + + +## State sharing + +Since your state contains sensitive information, avoid sharing full state files when possible. + +If you use HCP Terraform or Terraform Enterprise and need to reference resources across workspaces, use the [`tfe_outputs`](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) data source. + +If you do not use HCP Terraform or Terraform Enterprise but still need to reference data about other infrastructure resources, use data sources to query the provider. For example, you can use the [`aws_instance` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/instance) to look up an AWS EC2 instance by its ID or tags. + + +## Secrets management + +If you do not configure remote state storage, the Terraform CLI stores the entire state in plaintext on the local disk. State can include sensitive data, such as passwords and private keys. HCP Terraform and Terraform Enterprise provide state encryption through HashiCorp Vault. + +If you use HCP Terraform or Terraform Enterprise, we recommend the following: + +- When using Terraform Enterprise, define and enforce a Sentinel policy to prevent use of the `local_exec` provisioner or external data sources. +- When using HCP Terraform or Terraform Enterprise, use [dynamic provider credentials](/terraform/tutorials/cloud/dynamic-credentials) to avoid using long-lived static credentials. + + +If you use Terraform Community Edition, we recommend the following: + +- Configure provider credentials using provider-specific environment variables. +- Access secrets from a secrets management system such as HashiCorp Vault with the [Terraform Vault provider](https://registry.terraform.io/providers/hashicorp/vault/latest/docs). Be aware that Terraform will still write these values in plaintext to your state file. + +If you use a custom CI/CD pipeline, review your CI/CD tool's best practices for managing sensitive values. Most tools let you access sensitive values as environment variables. For more information, refer to your CI/CD documentation. + +- [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) +- [Gitlab pipeline security](https://docs.gitlab.com/ee/ci/pipelines/pipeline_security.html) +- [Integrate Vault into your CI/CD pipeline](/well-architected-framework/security/security-cicd-vault) + +## Integration and unit testing + +Terraform tests let you validate your modules and catch breaking changes. We recommend that you write tests for your Terraform modules and run them just as you run your tests for your application code, such as pre-merge check in your pull requests or as a prerequisite step in your automated CI/CD pipeline. + +Tests differ from validation methods such as variable validation, preconditions, postconditions, and check blocks. These features focus on verifying the infrastructure deployed by your code, while tests validate the behavior and logic of your code itself. For more information, refer to the [Terraform test documentation](/terraform/language/tests) and the [Write Terraform tests tutorial](/terraform/tutorials/configuration-language/test). + +## Policy + +Policies are rules that HCP Terraform enforces on Terraform runs. You can use policies to validate that the Terraform plan complies with your organization's best practices. For example, you can write policies that: + +- Limit the size of a web instance +- Check for required resource tags +- Block deployments on Fridays +- Enforce security configuration and cost management + +We recommend that you store policies in a separate VCS repository from your Terraform code. + +For more information, refer to the [policy enforcement documentation](/terraform/cloud-docs/policy-enforcement), as well as the [enforce policy with Sentinel](/terraform/tutorials/policy) and [detect infrastructure drift and enforce OPA policies](/terraform/tutorials/cloud/drift-and-opa) tutorials. + +## Next steps + +This article introduces some considerations to keep in mind as you standardize your organization's Terraform style guidelines. Enforcing a standard way of writing and organizing your Terraform code across your organization ensures that it is readable, maintainable, and shareable. + +To learn more Terraform adoption best practices, refer to [Phases of Terraform adoption](/terraform/intro/phases). diff --git a/website/docs/language/syntax/configuration.mdx b/website/docs/language/syntax/configuration.mdx index 01467288b0..2ca57fe5bd 100644 --- a/website/docs/language/syntax/configuration.mdx +++ b/website/docs/language/syntax/configuration.mdx @@ -15,7 +15,7 @@ those constructs are built from. This page describes the _native syntax_ of the Terraform language, which is a rich language designed to be relatively easy for humans to read and write. The constructs in the Terraform language can also be expressed in -[JSON syntax](/language/syntax/json), which is harder for humans +[JSON syntax](/terraform/language/syntax/json), which is harder for humans to read and edit but easier to generate and parse programmatically. This low-level syntax of the Terraform language is defined in terms of a @@ -46,7 +46,7 @@ after the equals sign is the argument's value. The context where the argument appears determines what value types are valid (for example, each resource type has a schema that defines the types of its arguments), but many arguments accept arbitrary -[expressions](/language/expressions), which allow the value to +[expressions](/terraform/language/expressions), which allow the value to either be specified literally or generated from other values programmatically. -> **Note:** Terraform's configuration language is based on a more general @@ -76,6 +76,7 @@ resource "aws_instance" "example" { A block has a _type_ (`resource` in this example). Each block type defines how many _labels_ must follow the type keyword. The `resource` block type expects two labels, which are `aws_instance` and `example` in the example above. +The `aws_instance` label is specific to the AWS provider. It specifies the `resource` type that Terraform provisions when you apply the configuration. The second label is an arbitrary name that you can add to the particular instance of the resource. You can create multiple instances of the same block type and differentiate them by giving each instance a unique name. In this example, the Terraform configuration author assigned the `example` label to this instance of the `aws_instance` resource. A particular block type may have any number of required labels, or it may require none as with the nested `network_interface` block type. diff --git a/website/docs/language/syntax/index.mdx b/website/docs/language/syntax/index.mdx index 742a40fef8..8cf3a25400 100644 --- a/website/docs/language/syntax/index.mdx +++ b/website/docs/language/syntax/index.mdx @@ -11,13 +11,13 @@ The majority of the Terraform language documentation focuses on the practical uses of the language and the specific constructs it uses. The pages in this section offer a more abstract view of the Terraform language. -- [Configuration Syntax](/language/syntax/configuration) describes the native +- [Configuration Syntax](/terraform/language/syntax/configuration) describes the native grammar of the Terraform language. -- [JSON Configuration Syntax](/language/syntax/json) documents +- [JSON Configuration Syntax](/terraform/language/syntax/json) documents how to represent Terraform language constructs in the pure JSON variant of the Terraform language. Terraform's JSON syntax is unfriendly to humans, but can be very useful when generating infrastructure as code with other systems that don't have a readily available HCL library. -- [Style Conventions](/language/syntax/style) documents some commonly +- [Style Conventions](/terraform/language/style#code-formatting) documents some commonly accepted formatting guidelines for Terraform code. These conventions can be - enforced automatically with [`terraform fmt`](/cli/commands/fmt). + enforced automatically with [`terraform fmt`](/terraform/cli/commands/fmt). diff --git a/website/docs/language/syntax/json.mdx b/website/docs/language/syntax/json.mdx index 477ce9fd7e..9f8ae509fa 100644 --- a/website/docs/language/syntax/json.mdx +++ b/website/docs/language/syntax/json.mdx @@ -8,7 +8,7 @@ description: >- # JSON Configuration Syntax Most Terraform configurations are written in -[the native Terraform language syntax](/language/syntax/configuration), which is designed to be +[the native Terraform language syntax](/terraform/language/syntax/configuration), which is designed to be relatively easy for humans to read and update. Terraform also supports an alternative syntax that is JSON-compatible. This @@ -92,7 +92,7 @@ different (see the [block-type-specific exceptions](#block-type-specific-excepti correspond either to argument names or to nested block type names. * Where a property corresponds to an argument that accepts - [arbitrary expressions](/language/expressions) in the native syntax, the + [arbitrary expressions](/terraform/language/expressions) in the native syntax, the property value is mapped to an expression as described under [_Expression Mapping_](#expression-mapping) below. For arguments that do _not_ accept arbitrary expressions, the interpretation of the property @@ -109,7 +109,7 @@ different (see the [block-type-specific exceptions](#block-type-specific-excepti ## Expression Mapping Since JSON grammar is not able to represent all of the Terraform language -[expression syntax](/language/expressions), JSON values interpreted as expressions +[expression syntax](/terraform/language/expressions), JSON values interpreted as expressions are mapped as follows: | JSON | Terraform Language Interpretation | @@ -121,7 +121,7 @@ are mapped as follows: | Array | Each element is mapped per this table, producing a `tuple(...)` value with suitable element types. | | Null | A literal `null`. | -[string template]: /language/expressions/strings#string-templates +[string template]: /terraform/language/expressions/strings#string-templates When a JSON string is encountered in a location where arbitrary expressions are expected, its value is first parsed as a [string template][] diff --git a/website/docs/language/syntax/style.mdx b/website/docs/language/syntax/style.mdx deleted file mode 100644 index b29195dcae..0000000000 --- a/website/docs/language/syntax/style.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -page_title: Style Conventions - Configuration Language -description: >- - Learn recommended formatting conventions for the Terraform language and a - command to automatically enforce them. ---- - -# Style Conventions - -The Terraform parser allows you some flexibility in how you lay out the -elements in your configuration files, but the Terraform language also has some -idiomatic style conventions which we recommend users always follow -for consistency between files and modules written by different teams. -Automatic source code formatting tools may apply these conventions -automatically. - --> **Note**: You can enforce these conventions automatically by running [`terraform fmt`](/cli/commands/fmt). - -* Indent two spaces for each nesting level. - -* When multiple arguments with single-line values appear on consecutive lines - at the same nesting level, align their equals signs: - - ```hcl - ami = "abc123" - instance_type = "t2.micro" - ``` - -* When both arguments and blocks appear together inside a block body, - place all of the arguments together at the top and then place nested - blocks below them. Use one blank line to separate the arguments from - the blocks. - -* Use empty lines to separate logical groups of arguments within a block. - -* For blocks that contain both arguments and "meta-arguments" (as defined by - the Terraform language semantics), list meta-arguments first - and separate them from other arguments with one blank line. Place - meta-argument blocks _last_ and separate them from other blocks with - one blank line. - - ```hcl - resource "aws_instance" "example" { - count = 2 # meta-argument first - - ami = "abc123" - instance_type = "t2.micro" - - network_interface { - # ... - } - - lifecycle { # meta-argument block last - create_before_destroy = true - } - } - ``` - -* Top-level blocks should always be separated from one another by one - blank line. Nested blocks should also be separated by blank lines, except - when grouping together related blocks of the same type (like multiple - `provisioner` blocks in a resource). - -* Avoid separating multiple blocks of the same type with other blocks of - a different type, unless the block types are defined by semantics to - form a family. - (For example: `root_block_device`, `ebs_block_device` and - `ephemeral_block_device` on `aws_instance` form a family of block types - describing AWS block devices, and can therefore be grouped together and - mixed.) diff --git a/website/docs/language/terraform.mdx b/website/docs/language/terraform.mdx new file mode 100644 index 0000000000..704fb87c03 --- /dev/null +++ b/website/docs/language/terraform.mdx @@ -0,0 +1,430 @@ +--- +page_title: Terraform block configuration reference +description: >- + The `terraform` block allows you to configure Terraform behavior, including the Terraform version, backend, integration with HCP Terraform, and required providers. +--- + +# `terraform` block reference + +This topic provides reference information about the `terraform` block. The `terraform` block allows you to configure Terraform behavior, including the Terraform version, backend, integration with HCP Terraform, and required providers. + +## Configuration model + +The `terraform` block supports the following arguments: + +- [`terraform`](#terraform) + - [`required_version`](#required_version):   string + - [`required_providers`](#required_providers):   block + - [``](#provider):   block + - [`version`](#provider):   string + - [`source`](#provider):   string + - [`provider_meta "